agentkit_core/lib.rs
1//! Core transcript, content, usage, and cancellation primitives for agentkit.
2//!
3//! This crate provides the foundational data model shared by every crate in
4//! the agentkit workspace. It defines:
5//!
6//! - **Transcript items** ([`Item`], [`ItemKind`]) -- the messages exchanged
7//! between system, user, assistant, and tools.
8//! - **Content parts** ([`Part`], [`TextPart`], [`ToolCallPart`], etc.) --
9//! the multimodal pieces that make up each item.
10//! - **Streaming deltas** ([`Delta`]) -- incremental updates emitted while a
11//! model turn is in progress.
12//! - **Usage tracking** ([`Usage`], [`TokenUsage`], [`CostUsage`]) -- token
13//! counts and cost accounting reported by providers.
14//! - **Cancellation** ([`CancellationController`], [`CancellationHandle`],
15//! [`TurnCancellation`]) -- cooperative interruption of running turns.
16//! - **Typed identifiers** ([`SessionId`], [`TurnId`], [`ToolCallId`], etc.)
17//! -- lightweight newtypes that prevent accidental ID mix-ups.
18//! - **Error types** ([`NormalizeError`], [`ProtocolError`], [`AgentError`])
19//! -- shared error variants used across the workspace.
20//!
21//! # Example
22//!
23//! ```rust
24//! use agentkit_core::{Item, ItemKind};
25//!
26//! // Build a minimal transcript to feed into the agent loop.
27//! let transcript = vec![
28//! Item::text(ItemKind::System, "You are a coding assistant."),
29//! Item::text(ItemKind::User, "What files are in this repo?"),
30//! ];
31//!
32//! assert_eq!(transcript[0].kind, ItemKind::System);
33//! ```
34
35use std::collections::BTreeMap;
36use std::fmt;
37use std::sync::Arc;
38use std::sync::atomic::{AtomicU64, Ordering};
39use std::time::Duration;
40
41use futures_timer::Delay;
42use serde::{Deserialize, Serialize};
43use serde_json::Value;
44use thiserror::Error;
45
46macro_rules! id_newtype {
47 ($(#[$meta:meta])* $name:ident) => {
48 $(#[$meta])*
49 #[derive(
50 Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
51 )]
52 pub struct $name(pub String);
53
54 impl $name {
55 /// Creates a new identifier from any value that can be converted into a [`String`].
56 pub fn new(value: impl Into<String>) -> Self {
57 Self(value.into())
58 }
59 }
60
61 impl fmt::Display for $name {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 self.0.fmt(f)
64 }
65 }
66
67 impl From<&str> for $name {
68 fn from(value: &str) -> Self {
69 Self::new(value)
70 }
71 }
72
73 impl From<String> for $name {
74 fn from(value: String) -> Self {
75 Self(value)
76 }
77 }
78 };
79}
80
81id_newtype!(
82 /// Identifies an agent session.
83 ///
84 /// A session groups one or more turns that share the same model connection
85 /// and transcript history. Pass this to [`agentkit_loop`] when starting a
86 /// new session.
87 ///
88 /// # Example
89 ///
90 /// ```rust
91 /// use agentkit_core::SessionId;
92 ///
93 /// let id = SessionId::new("coding-agent-1");
94 /// assert_eq!(id.to_string(), "coding-agent-1");
95 /// ```
96 SessionId
97);
98id_newtype!(
99 /// Identifies a single turn within a session.
100 ///
101 /// Each call to the model within a session gets a unique `TurnId`. This is
102 /// used by compaction and reporting to attribute work to specific turns.
103 TurnId
104);
105id_newtype!(
106 /// Identifies a transcript [`Item`].
107 ///
108 /// Providers may assign message IDs to track items across API calls.
109 /// This is optional -- locally constructed items typically leave it as `None`.
110 MessageId
111);
112id_newtype!(
113 /// Identifies a tool call emitted by the model.
114 ///
115 /// The model produces a [`ToolCallPart`] with this ID; the corresponding
116 /// [`ToolResultPart`] references it via `call_id` so the model can match
117 /// results back to requests.
118 ToolCallId
119);
120id_newtype!(
121 /// Identifies a tool result.
122 ///
123 /// Used by providers that assign their own IDs to tool-result payloads.
124 ToolResultId
125);
126id_newtype!(
127 /// Identifies a task tracked by a task manager.
128 ///
129 /// Unlike [`ToolCallId`], which is model/provider-facing, `TaskId` is a
130 /// runtime-facing identifier used to inspect, cancel, or correlate work
131 /// managed by a task scheduler.
132 TaskId
133);
134id_newtype!(
135 /// Provider-assigned identifier for a message.
136 ///
137 /// Some providers return an opaque ID for each completion response;
138 /// this type preserves it for tracing and debugging.
139 ProviderMessageId
140);
141id_newtype!(
142 /// Identifies a binary or text artifact stored externally.
143 ///
144 /// Referenced by [`DataRef::Handle`] when content lives outside the
145 /// transcript (e.g. in an artifact store).
146 ArtifactId
147);
148id_newtype!(
149 /// Identifies a content part within a streaming [`Delta`] sequence.
150 ///
151 /// Deltas reference parts by `PartId` so the consumer can reconstruct the
152 /// full item as chunks arrive.
153 PartId
154);
155id_newtype!(
156 /// Identifies a pending tool-use approval request.
157 ///
158 /// When a tool requires human approval, the loop emits an approval request
159 /// tagged with this ID. The caller responds with a decision keyed to the
160 /// same ID.
161 ApprovalId
162);
163
164/// A map of arbitrary key-value metadata attached to items, parts, and other structures.
165///
166/// Most agentkit types carry a `metadata` field of this type. Providers,
167/// tools, and observers can store domain-specific information here without
168/// changing the core schema.
169///
170/// # Example
171///
172/// ```rust
173/// use agentkit_core::MetadataMap;
174/// use serde_json::json;
175///
176/// let mut meta = MetadataMap::new();
177/// meta.insert("source".into(), json!("user-clipboard"));
178/// assert_eq!(meta["source"], "user-clipboard");
179/// ```
180pub type MetadataMap = BTreeMap<String, Value>;
181
182#[derive(Default)]
183struct CancellationState {
184 generation: AtomicU64,
185}
186
187/// Owner-side handle for broadcasting cancellation to running turns.
188///
189/// Create one `CancellationController` per agent and hand out
190/// [`CancellationHandle`]s to the loop and tool executors. Calling
191/// [`interrupt`](Self::interrupt) bumps an internal generation counter so that
192/// every outstanding [`TurnCancellation`] checkpoint becomes cancelled.
193///
194/// # Example
195///
196/// ```rust
197/// use agentkit_core::CancellationController;
198///
199/// let controller = CancellationController::new();
200/// let handle = controller.handle();
201///
202/// // Before an interrupt the generation is 0.
203/// assert_eq!(handle.generation(), 0);
204///
205/// // Signal cancellation (e.g. from a Ctrl-C handler).
206/// controller.interrupt();
207/// assert_eq!(handle.generation(), 1);
208/// ```
209#[derive(Clone, Default)]
210pub struct CancellationController {
211 state: Arc<CancellationState>,
212}
213
214/// Read-only view of cancellation state, cheaply cloneable.
215///
216/// The loop and tool executors receive a `CancellationHandle` and use it to
217/// create [`TurnCancellation`] checkpoints or poll for interrupts directly.
218///
219/// Obtain one from [`CancellationController::handle`].
220#[derive(Clone, Default)]
221pub struct CancellationHandle {
222 state: Arc<CancellationState>,
223}
224
225/// A snapshot of the cancellation generation at the start of a turn.
226///
227/// Created via [`CancellationHandle::checkpoint`] or [`TurnCancellation::new`],
228/// this value records the generation counter at creation time. If the counter
229/// changes (because [`CancellationController::interrupt`] was called), the
230/// checkpoint reports itself as cancelled.
231///
232/// The agent loop passes a `TurnCancellation` into model and tool calls so
233/// they can bail out cooperatively.
234///
235/// # Example
236///
237/// ```rust
238/// use agentkit_core::{CancellationController, TurnCancellation};
239///
240/// let controller = CancellationController::new();
241/// let checkpoint = TurnCancellation::new(controller.handle());
242///
243/// assert!(!checkpoint.is_cancelled());
244/// controller.interrupt();
245/// assert!(checkpoint.is_cancelled());
246/// ```
247#[derive(Clone, Default)]
248pub struct TurnCancellation {
249 handle: CancellationHandle,
250 generation: u64,
251}
252
253impl CancellationController {
254 /// Creates a new controller with generation starting at 0.
255 pub fn new() -> Self {
256 Self::default()
257 }
258
259 /// Returns a cloneable [`CancellationHandle`] that shares state with this controller.
260 pub fn handle(&self) -> CancellationHandle {
261 CancellationHandle {
262 state: Arc::clone(&self.state),
263 }
264 }
265
266 /// Broadcasts a cancellation by incrementing the generation counter.
267 ///
268 /// Returns the new generation value. All [`TurnCancellation`] checkpoints
269 /// created before this call will report themselves as cancelled.
270 pub fn interrupt(&self) -> u64 {
271 self.state.generation.fetch_add(1, Ordering::SeqCst) + 1
272 }
273}
274
275impl CancellationHandle {
276 /// Returns the current generation counter.
277 pub fn generation(&self) -> u64 {
278 self.state.generation.load(Ordering::SeqCst)
279 }
280
281 /// Creates a [`TurnCancellation`] checkpoint capturing the current generation.
282 pub fn checkpoint(&self) -> TurnCancellation {
283 TurnCancellation {
284 handle: self.clone(),
285 generation: self.generation(),
286 }
287 }
288
289 /// Returns `true` if the generation has changed since `generation`.
290 ///
291 /// # Arguments
292 ///
293 /// * `generation` - The generation value to compare against, typically
294 /// obtained from a prior call to [`generation`](Self::generation).
295 pub fn is_cancelled_since(&self, generation: u64) -> bool {
296 self.generation() != generation
297 }
298
299 /// Waits asynchronously until the generation changes from `generation`.
300 ///
301 /// Polls the generation counter every 10 ms. Prefer using
302 /// [`TurnCancellation::cancelled`] instead, which captures the generation
303 /// automatically.
304 ///
305 /// # Arguments
306 ///
307 /// * `generation` - The generation value to wait for a change from.
308 pub async fn cancelled_since(&self, generation: u64) {
309 while !self.is_cancelled_since(generation) {
310 Delay::new(Duration::from_millis(10)).await;
311 }
312 }
313}
314
315impl TurnCancellation {
316 /// Creates a checkpoint from the given handle, capturing its current generation.
317 ///
318 /// # Arguments
319 ///
320 /// * `handle` - The [`CancellationHandle`] to observe.
321 pub fn new(handle: CancellationHandle) -> Self {
322 handle.checkpoint()
323 }
324
325 /// Returns the generation that was captured when this checkpoint was created.
326 pub fn generation(&self) -> u64 {
327 self.generation
328 }
329
330 /// Returns `true` if the controller has been interrupted since this checkpoint was created.
331 pub fn is_cancelled(&self) -> bool {
332 self.handle.is_cancelled_since(self.generation)
333 }
334
335 /// Waits asynchronously until this checkpoint becomes cancelled.
336 ///
337 /// Useful in `tokio::select!` to race a model call against user interruption.
338 pub async fn cancelled(&self) {
339 self.handle.cancelled_since(self.generation).await;
340 }
341
342 /// Returns a reference to the underlying [`CancellationHandle`].
343 pub fn handle(&self) -> &CancellationHandle {
344 &self.handle
345 }
346}
347
348/// A single entry in the agent transcript.
349///
350/// An `Item` represents one message or event in the conversation between
351/// the system, user, assistant, and tools. Each item has a [`kind`](ItemKind)
352/// that determines its role and a vector of [`Part`]s that carry its content.
353///
354/// Items are the primary unit of data flowing through the agentkit loop:
355/// callers submit user and system items, the loop appends assistant and tool
356/// items, and compaction strategies operate on the full `Vec<Item>` transcript.
357///
358/// # Example
359///
360/// ```rust
361/// use agentkit_core::{Item, ItemKind};
362///
363/// let user_msg = Item::text(ItemKind::User, "List the workspace crates.");
364///
365/// assert_eq!(user_msg.kind, ItemKind::User);
366/// assert_eq!(user_msg.parts.len(), 1);
367/// ```
368#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
369pub struct Item {
370 /// Optional provider-assigned or user-supplied message identifier.
371 pub id: Option<MessageId>,
372 /// The role of this item in the transcript.
373 pub kind: ItemKind,
374 /// The content parts that make up this item.
375 pub parts: Vec<Part>,
376 /// Arbitrary key-value metadata for this item.
377 pub metadata: MetadataMap,
378 /// Token / cost usage produced by the model turn that emitted this item.
379 /// Populated by the loop on assistant items when the provider reports it.
380 pub usage: Option<Usage>,
381 /// Why the model turn that produced this item ended. Populated by the
382 /// loop on assistant items.
383 pub finish_reason: Option<FinishReason>,
384 /// When this item was appended to the transcript. Populated by the loop
385 /// for every appended item.
386 pub created_at: Option<Timestamp>,
387}
388
389impl Item {
390 /// Builds an item with the given role and parts.
391 pub fn new(kind: ItemKind, parts: Vec<Part>) -> Self {
392 Self {
393 id: None,
394 kind,
395 parts,
396 metadata: MetadataMap::new(),
397 usage: None,
398 finish_reason: None,
399 created_at: None,
400 }
401 }
402
403 /// Builds a single-text-part item.
404 pub fn text(kind: ItemKind, text: impl Into<String>) -> Self {
405 Self::new(kind, vec![Part::Text(TextPart::new(text))])
406 }
407
408 /// Builds a [`ItemKind::Notification`] item carrying free-form text.
409 /// Adapters wrap the content in `<system-reminder>` and deliver it as
410 /// a user-role message so the model can react to the notification on
411 /// its next turn without violating tool_use/tool_result pairing.
412 pub fn notification(text: impl Into<String>) -> Self {
413 Self::text(ItemKind::Notification, text)
414 }
415
416 /// Sets the item identifier.
417 pub fn with_id(mut self, id: impl Into<MessageId>) -> Self {
418 self.id = Some(id.into());
419 self
420 }
421
422 /// Replaces the item metadata.
423 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
424 self.metadata = metadata;
425 self
426 }
427
428 /// Appends one part to the item.
429 pub fn push_part(mut self, part: Part) -> Self {
430 self.parts.push(part);
431 self
432 }
433
434 /// Sets the model usage that produced this item.
435 pub fn with_usage(mut self, usage: Usage) -> Self {
436 self.usage = Some(usage);
437 self
438 }
439
440 /// Sets the finish reason for the model turn that produced this item.
441 pub fn with_finish_reason(mut self, reason: FinishReason) -> Self {
442 self.finish_reason = Some(reason);
443 self
444 }
445
446 /// Sets when this item was created.
447 pub fn with_created_at(mut self, ts: Timestamp) -> Self {
448 self.created_at = Some(ts);
449 self
450 }
451}
452
453/// A wall-clock instant carried on items, expressed as milliseconds since
454/// the Unix epoch in UTC. Stamped by the loop when items land in the
455/// transcript so consumers can sort, filter, or expire by age without
456/// depending on a particular date-time crate.
457#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
458pub struct Timestamp(pub u64);
459
460impl Timestamp {
461 /// Captures the current wall-clock time. Returns `Timestamp(0)` on the
462 /// vanishingly unlikely event the system clock is before the epoch.
463 pub fn now() -> Self {
464 let millis = std::time::SystemTime::now()
465 .duration_since(std::time::UNIX_EPOCH)
466 .map(|d| d.as_millis() as u64)
467 .unwrap_or(0);
468 Self(millis)
469 }
470}
471
472/// The role of an [`Item`] in the transcript.
473///
474/// Variants are ordered so that
475/// `System < Developer < User < Assistant < Tool < Context < Notification`,
476/// which is useful for sorting items by priority during compaction.
477#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
478pub enum ItemKind {
479 /// Instructions provided by the application author (highest priority).
480 System,
481 /// Developer-level instructions that sit between system and user.
482 Developer,
483 /// A message from the end user.
484 User,
485 /// A response generated by the model.
486 Assistant,
487 /// Output from a tool execution.
488 Tool,
489 /// Ambient context injected by context loaders (e.g. project files, docs).
490 Context,
491 /// Out-of-band side-channel signal injected mid-conversation:
492 /// background-task completions, environment changes, system reminders.
493 /// Adapters render it as a user-role message wrapped in
494 /// `<system-reminder>` so the model interprets it as a notification
495 /// rather than user input. Distinct from [`ItemKind::Context`] in two
496 /// ways: (1) temporal placement is preserved (Anthropic adapter does
497 /// NOT hoist it to the top-level `system` field), (2) UI hosts can
498 /// filter or render notifications differently from user turns.
499 ///
500 /// Use this when a tool runs in the background and its result
501 /// arrives after the original `tool_use` was already paired and
502 /// closed — emitting another `tool_result` for the same call_id
503 /// would violate the provider schema.
504 Notification,
505}
506
507/// A content part within an [`Item`].
508///
509/// Items are composed of one or more parts, each carrying a different kind of
510/// content -- plain text, images, files, tool calls, tool results, or
511/// provider-specific custom payloads.
512///
513/// # Example
514///
515/// ```rust
516/// use agentkit_core::{Part, ToolCallPart};
517/// use serde_json::json;
518///
519/// let parts: Vec<Part> = vec![
520/// Part::text("Reading the config file..."),
521/// Part::ToolCall(ToolCallPart::new(
522/// "call-42",
523/// "fs_read_file",
524/// json!({ "path": "config.toml" }),
525/// )),
526/// ];
527///
528/// assert!(matches!(parts[0], Part::Text(_)));
529/// assert!(matches!(parts[1], Part::ToolCall(_)));
530/// ```
531#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
532pub enum Part {
533 /// Plain text content.
534 Text(TextPart),
535 /// Binary or encoded media (images, audio, video).
536 Media(MediaPart),
537 /// A file attachment.
538 File(FilePart),
539 /// Structured JSON data, optionally validated against a schema.
540 Structured(StructuredPart),
541 /// Model reasoning / chain-of-thought output.
542 Reasoning(ReasoningPart),
543 /// A tool invocation request emitted by the model.
544 ToolCall(ToolCallPart),
545 /// The result returned by a tool after execution.
546 ToolResult(ToolResultPart),
547 /// A provider-specific part that does not fit the standard variants.
548 Custom(CustomPart),
549}
550
551impl Part {
552 /// Builds a text part.
553 pub fn text(text: impl Into<String>) -> Self {
554 Self::Text(TextPart::new(text))
555 }
556
557 /// Builds a media part.
558 pub fn media(modality: Modality, mime_type: impl Into<String>, data: DataRef) -> Self {
559 Self::Media(MediaPart::new(modality, mime_type, data))
560 }
561
562 /// Builds a file part.
563 pub fn file(data: DataRef) -> Self {
564 Self::File(FilePart::new(data))
565 }
566
567 /// Builds a structured part.
568 pub fn structured(value: Value) -> Self {
569 Self::Structured(StructuredPart::new(value))
570 }
571
572 /// Builds a reasoning-summary part.
573 pub fn reasoning(summary: impl Into<String>) -> Self {
574 Self::Reasoning(ReasoningPart::summary(summary))
575 }
576}
577
578/// Discriminant for [`Part`] variants, used in streaming [`Delta`]s.
579///
580/// When a [`Delta::BeginPart`] arrives the consumer uses the `PartKind` to
581/// allocate the right buffer before subsequent append deltas arrive.
582#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
583pub enum PartKind {
584 /// Corresponds to [`Part::Text`].
585 Text,
586 /// Corresponds to [`Part::Media`].
587 Media,
588 /// Corresponds to [`Part::File`].
589 File,
590 /// Corresponds to [`Part::Structured`].
591 Structured,
592 /// Corresponds to [`Part::Reasoning`].
593 Reasoning,
594 /// Corresponds to [`Part::ToolCall`].
595 ToolCall,
596 /// Corresponds to [`Part::ToolResult`].
597 ToolResult,
598 /// Corresponds to [`Part::Custom`].
599 Custom,
600}
601
602/// Plain text content within an [`Item`].
603///
604/// This is the most common part type: user messages, assistant replies, and
605/// system prompts are all represented as `TextPart`s.
606///
607/// # Example
608///
609/// ```rust
610/// use agentkit_core::TextPart;
611///
612/// let part = TextPart::new("Hello, world!");
613/// assert_eq!(part.text, "Hello, world!");
614/// ```
615#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
616pub struct TextPart {
617 /// The text content.
618 pub text: String,
619 /// Arbitrary key-value metadata.
620 pub metadata: MetadataMap,
621}
622
623impl TextPart {
624 /// Builds a text part with empty metadata.
625 pub fn new(text: impl Into<String>) -> Self {
626 Self {
627 text: text.into(),
628 metadata: MetadataMap::new(),
629 }
630 }
631
632 /// Replaces the text-part metadata.
633 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
634 self.metadata = metadata;
635 self
636 }
637}
638
639/// Binary or encoded media content (image, audio, video).
640///
641/// The actual bytes are referenced through a [`DataRef`] which can be inline,
642/// a URI, or a handle to an external artifact store.
643#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
644pub struct MediaPart {
645 /// The kind of media (audio, image, video, or raw binary).
646 pub modality: Modality,
647 /// MIME type of the media, e.g. `"image/png"` or `"audio/wav"`.
648 pub mime_type: String,
649 /// Reference to the media data.
650 pub data: DataRef,
651 /// Arbitrary key-value metadata.
652 pub metadata: MetadataMap,
653}
654
655impl MediaPart {
656 /// Builds a media part with empty metadata.
657 pub fn new(modality: Modality, mime_type: impl Into<String>, data: DataRef) -> Self {
658 Self {
659 modality,
660 mime_type: mime_type.into(),
661 data,
662 metadata: MetadataMap::new(),
663 }
664 }
665
666 /// Replaces the media-part metadata.
667 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
668 self.metadata = metadata;
669 self
670 }
671}
672
673/// The kind of media carried by a [`MediaPart`].
674#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
675pub enum Modality {
676 /// Audio content (e.g. WAV, MP3).
677 Audio,
678 /// Image content (e.g. PNG, JPEG).
679 Image,
680 /// Video content (e.g. MP4).
681 Video,
682 /// Opaque binary data that does not fit another category.
683 Binary,
684}
685
686/// A reference to content data that may live inline, at a URI, or in an artifact store.
687///
688/// Used by [`MediaPart`], [`FilePart`], [`ReasoningPart`], and [`CustomPart`]
689/// to point at their underlying data without dictating storage.
690#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
691pub enum DataRef {
692 /// UTF-8 text stored inline (e.g. base64-encoded image data).
693 InlineText(String),
694 /// Raw bytes stored inline.
695 InlineBytes(Vec<u8>),
696 /// A URI pointing to externally hosted content.
697 Uri(String),
698 /// A handle to an artifact managed by an external store.
699 Handle(ArtifactId),
700}
701
702impl DataRef {
703 /// Stores UTF-8 text inline.
704 pub fn inline_text(text: impl Into<String>) -> Self {
705 Self::InlineText(text.into())
706 }
707
708 /// Stores bytes inline.
709 pub fn inline_bytes(bytes: impl Into<Vec<u8>>) -> Self {
710 Self::InlineBytes(bytes.into())
711 }
712
713 /// References externally hosted content by URI.
714 pub fn uri(uri: impl Into<String>) -> Self {
715 Self::Uri(uri.into())
716 }
717
718 /// References content through an artifact handle.
719 pub fn handle(id: impl Into<ArtifactId>) -> Self {
720 Self::Handle(id.into())
721 }
722}
723
724/// A file attachment within an [`Item`].
725///
726/// Files are distinct from [`MediaPart`] in that they carry an optional
727/// filename and are not necessarily displayable media.
728#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
729pub struct FilePart {
730 /// Optional human-readable filename (e.g. `"report.csv"`).
731 pub name: Option<String>,
732 /// Optional MIME type of the file.
733 pub mime_type: Option<String>,
734 /// Reference to the file data.
735 pub data: DataRef,
736 /// Arbitrary key-value metadata.
737 pub metadata: MetadataMap,
738}
739
740impl FilePart {
741 /// Builds an unnamed file part with empty metadata.
742 pub fn new(data: DataRef) -> Self {
743 Self {
744 name: None,
745 mime_type: None,
746 data,
747 metadata: MetadataMap::new(),
748 }
749 }
750
751 /// Builds a named file part with empty metadata.
752 pub fn named(name: impl Into<String>, data: DataRef) -> Self {
753 Self::new(data).with_name(name)
754 }
755
756 /// Sets the file name.
757 pub fn with_name(mut self, name: impl Into<String>) -> Self {
758 self.name = Some(name.into());
759 self
760 }
761
762 /// Sets the file mime type.
763 pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
764 self.mime_type = Some(mime_type.into());
765 self
766 }
767
768 /// Replaces the file-part metadata.
769 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
770 self.metadata = metadata;
771 self
772 }
773}
774
775/// Structured JSON content, optionally paired with a JSON Schema for validation.
776///
777/// Providers that support structured output (e.g. function-calling mode) may
778/// return a `StructuredPart` instead of free-form text.
779#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
780pub struct StructuredPart {
781 /// The structured data as a JSON [`Value`].
782 pub value: Value,
783 /// An optional JSON Schema that `value` conforms to.
784 pub schema: Option<Value>,
785 /// Arbitrary key-value metadata.
786 pub metadata: MetadataMap,
787}
788
789impl StructuredPart {
790 /// Builds a structured part with empty metadata and no schema.
791 pub fn new(value: Value) -> Self {
792 Self {
793 value,
794 schema: None,
795 metadata: MetadataMap::new(),
796 }
797 }
798
799 /// Sets the optional schema.
800 pub fn with_schema(mut self, schema: Value) -> Self {
801 self.schema = Some(schema);
802 self
803 }
804
805 /// Replaces the structured-part metadata.
806 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
807 self.metadata = metadata;
808 self
809 }
810}
811
812/// Model reasoning or chain-of-thought output.
813///
814/// Some providers expose the model's internal reasoning alongside the final
815/// answer. The reasoning may be a readable summary, opaque data, or both.
816/// The `redacted` flag indicates provider-side filtering.
817#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
818pub struct ReasoningPart {
819 /// A human-readable summary of the model's reasoning.
820 pub summary: Option<String>,
821 /// Opaque or detailed reasoning data.
822 pub data: Option<DataRef>,
823 /// `true` if the provider redacted the full reasoning content.
824 pub redacted: bool,
825 /// Arbitrary key-value metadata.
826 pub metadata: MetadataMap,
827}
828
829impl ReasoningPart {
830 /// Builds a readable reasoning summary.
831 pub fn summary(summary: impl Into<String>) -> Self {
832 Self {
833 summary: Some(summary.into()),
834 data: None,
835 redacted: false,
836 metadata: MetadataMap::new(),
837 }
838 }
839
840 /// Builds a redacted readable reasoning summary.
841 pub fn redacted_summary(summary: impl Into<String>) -> Self {
842 Self::summary(summary).with_redacted(true)
843 }
844
845 /// Sets the optional reasoning data reference.
846 pub fn with_data(mut self, data: DataRef) -> Self {
847 self.data = Some(data);
848 self
849 }
850
851 /// Sets whether the reasoning content was redacted.
852 pub fn with_redacted(mut self, redacted: bool) -> Self {
853 self.redacted = redacted;
854 self
855 }
856
857 /// Replaces the reasoning-part metadata.
858 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
859 self.metadata = metadata;
860 self
861 }
862}
863
864/// A tool invocation request emitted by the model.
865///
866/// The agent loop receives this part, executes the named tool, and appends a
867/// [`ToolResultPart`] back to the transcript for the model to observe.
868///
869/// # Example
870///
871/// ```rust
872/// use agentkit_core::ToolCallPart;
873/// use serde_json::json;
874///
875/// let call = ToolCallPart::new("call-7", "fs_read_file", json!({ "path": "src/main.rs" }));
876///
877/// assert_eq!(call.name, "fs_read_file");
878/// ```
879#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
880pub struct ToolCallPart {
881 /// Unique identifier for this tool call, used to correlate with [`ToolResultPart::call_id`].
882 pub id: ToolCallId,
883 /// The name of the tool to invoke (e.g. `"fs_read_file"`, `"shell_exec"`).
884 pub name: String,
885 /// The JSON arguments to pass to the tool.
886 pub input: Value,
887 /// Arbitrary key-value metadata.
888 pub metadata: MetadataMap,
889}
890
891impl ToolCallPart {
892 /// Builds a tool-call part with empty metadata.
893 pub fn new(id: impl Into<ToolCallId>, name: impl Into<String>, input: Value) -> Self {
894 Self {
895 id: id.into(),
896 name: name.into(),
897 input,
898 metadata: MetadataMap::new(),
899 }
900 }
901
902 /// Replaces the tool-call metadata.
903 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
904 self.metadata = metadata;
905 self
906 }
907}
908
909/// The result of executing a tool, sent back to the model.
910///
911/// Each `ToolResultPart` references the [`ToolCallPart`] it answers via
912/// `call_id`. The `is_error` flag tells the model whether the tool succeeded.
913///
914/// # Example
915///
916/// ```rust
917/// use agentkit_core::{ToolOutput, ToolResultPart};
918///
919/// let result = ToolResultPart::success("call-7", ToolOutput::text("fn main() { ... }"));
920///
921/// assert!(!result.is_error);
922/// ```
923#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
924pub struct ToolResultPart {
925 /// The [`ToolCallId`] of the tool call this result answers.
926 pub call_id: ToolCallId,
927 /// The output produced by the tool.
928 pub output: ToolOutput,
929 /// `true` if the tool execution failed.
930 pub is_error: bool,
931 /// Arbitrary key-value metadata.
932 pub metadata: MetadataMap,
933}
934
935impl ToolResultPart {
936 /// Builds a successful tool-result part with empty metadata.
937 pub fn success(call_id: impl Into<ToolCallId>, output: ToolOutput) -> Self {
938 Self {
939 call_id: call_id.into(),
940 output,
941 is_error: false,
942 metadata: MetadataMap::new(),
943 }
944 }
945
946 /// Builds an error tool-result part with empty metadata.
947 pub fn error(call_id: impl Into<ToolCallId>, output: ToolOutput) -> Self {
948 Self::success(call_id, output).with_is_error(true)
949 }
950
951 /// Sets the error flag explicitly.
952 pub fn with_is_error(mut self, is_error: bool) -> Self {
953 self.is_error = is_error;
954 self
955 }
956
957 /// Replaces the tool-result metadata.
958 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
959 self.metadata = metadata;
960 self
961 }
962}
963
964/// The payload returned by a tool execution.
965///
966/// Tools may return plain text, structured JSON, a composite list of
967/// [`Part`]s, or a collection of files.
968#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
969pub enum ToolOutput {
970 /// Plain text output.
971 Text(String),
972 /// Structured JSON output.
973 Structured(Value),
974 /// A list of content parts (e.g. text + images).
975 Parts(Vec<Part>),
976 /// A list of file attachments.
977 Files(Vec<FilePart>),
978}
979
980impl ToolOutput {
981 /// Builds plain-text tool output.
982 pub fn text(text: impl Into<String>) -> Self {
983 Self::Text(text.into())
984 }
985
986 /// Builds structured tool output.
987 pub fn structured(value: Value) -> Self {
988 Self::Structured(value)
989 }
990
991 /// Builds multipart tool output.
992 pub fn parts(parts: Vec<Part>) -> Self {
993 Self::Parts(parts)
994 }
995
996 /// Builds file-based tool output.
997 pub fn files(files: Vec<FilePart>) -> Self {
998 Self::Files(files)
999 }
1000}
1001
1002/// A provider-specific content part that does not fit the standard variants.
1003///
1004/// Use this for extensions or experimental features that have not been
1005/// promoted to a first-class [`Part`] variant.
1006#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1007pub struct CustomPart {
1008 /// A free-form string identifying the custom part type.
1009 pub kind: String,
1010 /// Optional data reference.
1011 pub data: Option<DataRef>,
1012 /// Optional structured value.
1013 pub value: Option<Value>,
1014 /// Arbitrary key-value metadata.
1015 pub metadata: MetadataMap,
1016}
1017
1018impl CustomPart {
1019 /// Builds a custom part with empty metadata.
1020 pub fn new(kind: impl Into<String>) -> Self {
1021 Self {
1022 kind: kind.into(),
1023 data: None,
1024 value: None,
1025 metadata: MetadataMap::new(),
1026 }
1027 }
1028
1029 /// Sets the custom part data reference.
1030 pub fn with_data(mut self, data: DataRef) -> Self {
1031 self.data = Some(data);
1032 self
1033 }
1034
1035 /// Sets the custom part structured value.
1036 pub fn with_value(mut self, value: Value) -> Self {
1037 self.value = Some(value);
1038 self
1039 }
1040
1041 /// Replaces the custom-part metadata.
1042 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
1043 self.metadata = metadata;
1044 self
1045 }
1046}
1047
1048/// An incremental update emitted while a model turn is streaming.
1049///
1050/// The provider adapter emits a sequence of `Delta` values that the loop and
1051/// reporters consume to reconstruct the full [`Item`] progressively. A typical
1052/// sequence looks like:
1053///
1054/// 1. [`BeginPart`](Self::BeginPart) -- allocates a new part buffer.
1055/// 2. One or more [`AppendText`](Self::AppendText) /
1056/// [`AppendBytes`](Self::AppendBytes) -- fills the buffer.
1057/// 3. [`CommitPart`](Self::CommitPart) -- finalises the part.
1058///
1059/// # Example
1060///
1061/// ```rust
1062/// use agentkit_core::{Delta, PartId, PartKind};
1063///
1064/// let deltas = vec![
1065/// Delta::BeginPart { part_id: PartId::new("p1"), kind: PartKind::Text },
1066/// Delta::AppendText { part_id: PartId::new("p1"), chunk: "Hello".into() },
1067/// Delta::AppendText { part_id: PartId::new("p1"), chunk: ", world!".into() },
1068/// ];
1069///
1070/// assert_eq!(deltas.len(), 3);
1071/// ```
1072#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1073pub enum Delta {
1074 /// Signals the start of a new part with the given kind.
1075 BeginPart {
1076 /// Identifier for the part being constructed.
1077 part_id: PartId,
1078 /// The kind of part being started.
1079 kind: PartKind,
1080 },
1081 /// Appends a text chunk to an in-progress part.
1082 AppendText {
1083 /// Identifier of the target part.
1084 part_id: PartId,
1085 /// The text chunk to append.
1086 chunk: String,
1087 },
1088 /// Appends raw bytes to an in-progress part.
1089 AppendBytes {
1090 /// Identifier of the target part.
1091 part_id: PartId,
1092 /// The byte chunk to append.
1093 chunk: Vec<u8>,
1094 },
1095 /// Replaces the structured value of an in-progress part wholesale.
1096 ReplaceStructured {
1097 /// Identifier of the target part.
1098 part_id: PartId,
1099 /// The new structured value.
1100 value: Value,
1101 },
1102 /// Sets or replaces the metadata on an in-progress part.
1103 SetMetadata {
1104 /// Identifier of the target part.
1105 part_id: PartId,
1106 /// The new metadata map.
1107 metadata: MetadataMap,
1108 },
1109 /// Finalises a part, providing the fully assembled [`Part`].
1110 CommitPart {
1111 /// The completed part.
1112 part: Part,
1113 },
1114}
1115
1116/// Token and cost usage reported by a model provider for a single turn.
1117///
1118/// Reporters and compaction triggers inspect `Usage` to log progress, enforce
1119/// budgets, and decide when to compact the transcript.
1120///
1121/// # Example
1122///
1123/// ```rust
1124/// use agentkit_core::{TokenUsage, Usage};
1125///
1126/// let usage = Usage::new(
1127/// TokenUsage::new(1500, 200)
1128/// .with_cached_input_tokens(1000)
1129/// .with_cache_write_input_tokens(1200),
1130/// );
1131///
1132/// let tokens = usage.tokens.as_ref().unwrap();
1133/// assert_eq!(tokens.input_tokens, 1500);
1134/// ```
1135#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1136pub struct Usage {
1137 /// Token counts for this turn, if the provider reports them.
1138 pub tokens: Option<TokenUsage>,
1139 /// Monetary cost for this turn, if the provider reports it.
1140 pub cost: Option<CostUsage>,
1141 /// Arbitrary key-value metadata.
1142 pub metadata: MetadataMap,
1143}
1144
1145impl Usage {
1146 /// Builds a usage record with token counts and no cost.
1147 pub fn new(tokens: TokenUsage) -> Self {
1148 Self {
1149 tokens: Some(tokens),
1150 cost: None,
1151 metadata: MetadataMap::new(),
1152 }
1153 }
1154
1155 /// Sets the cost information.
1156 pub fn with_cost(mut self, cost: CostUsage) -> Self {
1157 self.cost = Some(cost);
1158 self
1159 }
1160
1161 /// Replaces the usage metadata.
1162 pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
1163 self.metadata = metadata;
1164 self
1165 }
1166}
1167
1168/// Token counts broken down by direction and special categories.
1169#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1170pub struct TokenUsage {
1171 /// Number of tokens in the input (prompt) sent to the model.
1172 pub input_tokens: u64,
1173 /// Number of tokens in the model's output (completion).
1174 pub output_tokens: u64,
1175 /// Tokens consumed by the model's internal reasoning, if reported.
1176 pub reasoning_tokens: Option<u64>,
1177 /// Input tokens served from the provider's prompt cache, if reported.
1178 pub cached_input_tokens: Option<u64>,
1179 /// Input tokens written into the provider's prompt cache, if reported.
1180 pub cache_write_input_tokens: Option<u64>,
1181}
1182
1183impl TokenUsage {
1184 /// Builds token usage with required input and output counts.
1185 pub fn new(input_tokens: u64, output_tokens: u64) -> Self {
1186 Self {
1187 input_tokens,
1188 output_tokens,
1189 reasoning_tokens: None,
1190 cached_input_tokens: None,
1191 cache_write_input_tokens: None,
1192 }
1193 }
1194
1195 /// Sets reasoning token count.
1196 pub fn with_reasoning_tokens(mut self, reasoning_tokens: u64) -> Self {
1197 self.reasoning_tokens = Some(reasoning_tokens);
1198 self
1199 }
1200
1201 /// Sets cached input token count.
1202 pub fn with_cached_input_tokens(mut self, cached_input_tokens: u64) -> Self {
1203 self.cached_input_tokens = Some(cached_input_tokens);
1204 self
1205 }
1206
1207 /// Sets cache-write input token count.
1208 pub fn with_cache_write_input_tokens(mut self, cache_write_input_tokens: u64) -> Self {
1209 self.cache_write_input_tokens = Some(cache_write_input_tokens);
1210 self
1211 }
1212}
1213
1214/// Monetary cost for a single model turn.
1215#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1216pub struct CostUsage {
1217 /// The cost amount as a floating-point number.
1218 pub amount: f64,
1219 /// The ISO 4217 currency code (e.g. `"USD"`).
1220 pub currency: String,
1221 /// An optional provider-specific cost string for display purposes.
1222 pub provider_amount: Option<String>,
1223}
1224
1225impl CostUsage {
1226 /// Builds cost usage with no provider-specific display string.
1227 pub fn new(amount: f64, currency: impl Into<String>) -> Self {
1228 Self {
1229 amount,
1230 currency: currency.into(),
1231 provider_amount: None,
1232 }
1233 }
1234
1235 /// Sets the optional provider-specific display value.
1236 pub fn with_provider_amount(mut self, provider_amount: impl Into<String>) -> Self {
1237 self.provider_amount = Some(provider_amount.into());
1238 self
1239 }
1240}
1241
1242/// The reason a model turn ended.
1243///
1244/// The loop inspects the `FinishReason` to decide whether to execute tool
1245/// calls, request more input, or report an error.
1246#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1247pub enum FinishReason {
1248 /// The model finished generating its response normally.
1249 Completed,
1250 /// The model stopped to invoke one or more tools.
1251 ToolCall,
1252 /// The response was truncated because the token limit was reached.
1253 MaxTokens,
1254 /// The turn was cancelled via [`TurnCancellation`].
1255 Cancelled,
1256 /// The provider blocked the response (e.g. content policy violation).
1257 Blocked,
1258 /// An error occurred during generation.
1259 Error,
1260 /// A provider-specific reason not covered by the standard variants.
1261 Other(String),
1262}
1263
1264/// Error returned when content cannot be normalised into the agentkit data model.
1265#[derive(Debug, Error)]
1266pub enum NormalizeError {
1267 /// The content shape is not supported by the current provider adapter.
1268 #[error("unsupported content shape: {0}")]
1269 Unsupported(String),
1270}
1271
1272/// Error indicating an invalid state in the provider protocol.
1273#[derive(Debug, Error)]
1274pub enum ProtocolError {
1275 /// The provider or loop reached a state that violates protocol invariants.
1276 #[error("invalid protocol state: {0}")]
1277 InvalidState(String),
1278}
1279
1280/// Top-level error type that unifies normalisation and protocol errors.
1281///
1282/// Provider adapters and the agent loop surface this type so callers can
1283/// handle both categories uniformly.
1284#[derive(Debug, Error)]
1285pub enum AgentError {
1286 /// A content normalisation error.
1287 #[error(transparent)]
1288 Normalize(#[from] NormalizeError),
1289 /// A protocol-level error.
1290 #[error(transparent)]
1291 Protocol(#[from] ProtocolError),
1292}