Skip to main content

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}
379
380impl Item {
381    /// Builds an item with the given role and parts.
382    pub fn new(kind: ItemKind, parts: Vec<Part>) -> Self {
383        Self {
384            id: None,
385            kind,
386            parts,
387            metadata: MetadataMap::new(),
388        }
389    }
390
391    /// Builds a single-text-part item.
392    pub fn text(kind: ItemKind, text: impl Into<String>) -> Self {
393        Self::new(kind, vec![Part::Text(TextPart::new(text))])
394    }
395
396    /// Builds a [`ItemKind::Notification`] item carrying free-form text.
397    /// Adapters wrap the content in `<system-reminder>` and deliver it as
398    /// a user-role message so the model can react to the notification on
399    /// its next turn without violating tool_use/tool_result pairing.
400    pub fn notification(text: impl Into<String>) -> Self {
401        Self::text(ItemKind::Notification, text)
402    }
403
404    /// Sets the item identifier.
405    pub fn with_id(mut self, id: impl Into<MessageId>) -> Self {
406        self.id = Some(id.into());
407        self
408    }
409
410    /// Replaces the item metadata.
411    pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
412        self.metadata = metadata;
413        self
414    }
415
416    /// Appends one part to the item.
417    pub fn push_part(mut self, part: Part) -> Self {
418        self.parts.push(part);
419        self
420    }
421}
422
423/// The role of an [`Item`] in the transcript.
424///
425/// Variants are ordered so that
426/// `System < Developer < User < Assistant < Tool < Context < Notification`,
427/// which is useful for sorting items by priority during compaction.
428#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
429pub enum ItemKind {
430    /// Instructions provided by the application author (highest priority).
431    System,
432    /// Developer-level instructions that sit between system and user.
433    Developer,
434    /// A message from the end user.
435    User,
436    /// A response generated by the model.
437    Assistant,
438    /// Output from a tool execution.
439    Tool,
440    /// Ambient context injected by context loaders (e.g. project files, docs).
441    Context,
442    /// Out-of-band side-channel signal injected mid-conversation:
443    /// background-task completions, environment changes, system reminders.
444    /// Adapters render it as a user-role message wrapped in
445    /// `<system-reminder>` so the model interprets it as a notification
446    /// rather than user input. Distinct from [`ItemKind::Context`] in two
447    /// ways: (1) temporal placement is preserved (Anthropic adapter does
448    /// NOT hoist it to the top-level `system` field), (2) UI hosts can
449    /// filter or render notifications differently from user turns.
450    ///
451    /// Use this when a tool runs in the background and its result
452    /// arrives after the original `tool_use` was already paired and
453    /// closed — emitting another `tool_result` for the same call_id
454    /// would violate the provider schema.
455    Notification,
456}
457
458/// A content part within an [`Item`].
459///
460/// Items are composed of one or more parts, each carrying a different kind of
461/// content -- plain text, images, files, tool calls, tool results, or
462/// provider-specific custom payloads.
463///
464/// # Example
465///
466/// ```rust
467/// use agentkit_core::{Part, ToolCallPart};
468/// use serde_json::json;
469///
470/// let parts: Vec<Part> = vec![
471///     Part::text("Reading the config file..."),
472///     Part::ToolCall(ToolCallPart::new(
473///         "call-42",
474///         "fs_read_file",
475///         json!({ "path": "config.toml" }),
476///     )),
477/// ];
478///
479/// assert!(matches!(parts[0], Part::Text(_)));
480/// assert!(matches!(parts[1], Part::ToolCall(_)));
481/// ```
482#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
483pub enum Part {
484    /// Plain text content.
485    Text(TextPart),
486    /// Binary or encoded media (images, audio, video).
487    Media(MediaPart),
488    /// A file attachment.
489    File(FilePart),
490    /// Structured JSON data, optionally validated against a schema.
491    Structured(StructuredPart),
492    /// Model reasoning / chain-of-thought output.
493    Reasoning(ReasoningPart),
494    /// A tool invocation request emitted by the model.
495    ToolCall(ToolCallPart),
496    /// The result returned by a tool after execution.
497    ToolResult(ToolResultPart),
498    /// A provider-specific part that does not fit the standard variants.
499    Custom(CustomPart),
500}
501
502impl Part {
503    /// Builds a text part.
504    pub fn text(text: impl Into<String>) -> Self {
505        Self::Text(TextPart::new(text))
506    }
507
508    /// Builds a media part.
509    pub fn media(modality: Modality, mime_type: impl Into<String>, data: DataRef) -> Self {
510        Self::Media(MediaPart::new(modality, mime_type, data))
511    }
512
513    /// Builds a file part.
514    pub fn file(data: DataRef) -> Self {
515        Self::File(FilePart::new(data))
516    }
517
518    /// Builds a structured part.
519    pub fn structured(value: Value) -> Self {
520        Self::Structured(StructuredPart::new(value))
521    }
522
523    /// Builds a reasoning-summary part.
524    pub fn reasoning(summary: impl Into<String>) -> Self {
525        Self::Reasoning(ReasoningPart::summary(summary))
526    }
527}
528
529/// Discriminant for [`Part`] variants, used in streaming [`Delta`]s.
530///
531/// When a [`Delta::BeginPart`] arrives the consumer uses the `PartKind` to
532/// allocate the right buffer before subsequent append deltas arrive.
533#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
534pub enum PartKind {
535    /// Corresponds to [`Part::Text`].
536    Text,
537    /// Corresponds to [`Part::Media`].
538    Media,
539    /// Corresponds to [`Part::File`].
540    File,
541    /// Corresponds to [`Part::Structured`].
542    Structured,
543    /// Corresponds to [`Part::Reasoning`].
544    Reasoning,
545    /// Corresponds to [`Part::ToolCall`].
546    ToolCall,
547    /// Corresponds to [`Part::ToolResult`].
548    ToolResult,
549    /// Corresponds to [`Part::Custom`].
550    Custom,
551}
552
553/// Plain text content within an [`Item`].
554///
555/// This is the most common part type: user messages, assistant replies, and
556/// system prompts are all represented as `TextPart`s.
557///
558/// # Example
559///
560/// ```rust
561/// use agentkit_core::TextPart;
562///
563/// let part = TextPart::new("Hello, world!");
564/// assert_eq!(part.text, "Hello, world!");
565/// ```
566#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
567pub struct TextPart {
568    /// The text content.
569    pub text: String,
570    /// Arbitrary key-value metadata.
571    pub metadata: MetadataMap,
572}
573
574impl TextPart {
575    /// Builds a text part with empty metadata.
576    pub fn new(text: impl Into<String>) -> Self {
577        Self {
578            text: text.into(),
579            metadata: MetadataMap::new(),
580        }
581    }
582
583    /// Replaces the text-part metadata.
584    pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
585        self.metadata = metadata;
586        self
587    }
588}
589
590/// Binary or encoded media content (image, audio, video).
591///
592/// The actual bytes are referenced through a [`DataRef`] which can be inline,
593/// a URI, or a handle to an external artifact store.
594#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
595pub struct MediaPart {
596    /// The kind of media (audio, image, video, or raw binary).
597    pub modality: Modality,
598    /// MIME type of the media, e.g. `"image/png"` or `"audio/wav"`.
599    pub mime_type: String,
600    /// Reference to the media data.
601    pub data: DataRef,
602    /// Arbitrary key-value metadata.
603    pub metadata: MetadataMap,
604}
605
606impl MediaPart {
607    /// Builds a media part with empty metadata.
608    pub fn new(modality: Modality, mime_type: impl Into<String>, data: DataRef) -> Self {
609        Self {
610            modality,
611            mime_type: mime_type.into(),
612            data,
613            metadata: MetadataMap::new(),
614        }
615    }
616
617    /// Replaces the media-part metadata.
618    pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
619        self.metadata = metadata;
620        self
621    }
622}
623
624/// The kind of media carried by a [`MediaPart`].
625#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
626pub enum Modality {
627    /// Audio content (e.g. WAV, MP3).
628    Audio,
629    /// Image content (e.g. PNG, JPEG).
630    Image,
631    /// Video content (e.g. MP4).
632    Video,
633    /// Opaque binary data that does not fit another category.
634    Binary,
635}
636
637/// A reference to content data that may live inline, at a URI, or in an artifact store.
638///
639/// Used by [`MediaPart`], [`FilePart`], [`ReasoningPart`], and [`CustomPart`]
640/// to point at their underlying data without dictating storage.
641#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
642pub enum DataRef {
643    /// UTF-8 text stored inline (e.g. base64-encoded image data).
644    InlineText(String),
645    /// Raw bytes stored inline.
646    InlineBytes(Vec<u8>),
647    /// A URI pointing to externally hosted content.
648    Uri(String),
649    /// A handle to an artifact managed by an external store.
650    Handle(ArtifactId),
651}
652
653impl DataRef {
654    /// Stores UTF-8 text inline.
655    pub fn inline_text(text: impl Into<String>) -> Self {
656        Self::InlineText(text.into())
657    }
658
659    /// Stores bytes inline.
660    pub fn inline_bytes(bytes: impl Into<Vec<u8>>) -> Self {
661        Self::InlineBytes(bytes.into())
662    }
663
664    /// References externally hosted content by URI.
665    pub fn uri(uri: impl Into<String>) -> Self {
666        Self::Uri(uri.into())
667    }
668
669    /// References content through an artifact handle.
670    pub fn handle(id: impl Into<ArtifactId>) -> Self {
671        Self::Handle(id.into())
672    }
673}
674
675/// A file attachment within an [`Item`].
676///
677/// Files are distinct from [`MediaPart`] in that they carry an optional
678/// filename and are not necessarily displayable media.
679#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
680pub struct FilePart {
681    /// Optional human-readable filename (e.g. `"report.csv"`).
682    pub name: Option<String>,
683    /// Optional MIME type of the file.
684    pub mime_type: Option<String>,
685    /// Reference to the file data.
686    pub data: DataRef,
687    /// Arbitrary key-value metadata.
688    pub metadata: MetadataMap,
689}
690
691impl FilePart {
692    /// Builds an unnamed file part with empty metadata.
693    pub fn new(data: DataRef) -> Self {
694        Self {
695            name: None,
696            mime_type: None,
697            data,
698            metadata: MetadataMap::new(),
699        }
700    }
701
702    /// Builds a named file part with empty metadata.
703    pub fn named(name: impl Into<String>, data: DataRef) -> Self {
704        Self::new(data).with_name(name)
705    }
706
707    /// Sets the file name.
708    pub fn with_name(mut self, name: impl Into<String>) -> Self {
709        self.name = Some(name.into());
710        self
711    }
712
713    /// Sets the file mime type.
714    pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
715        self.mime_type = Some(mime_type.into());
716        self
717    }
718
719    /// Replaces the file-part metadata.
720    pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
721        self.metadata = metadata;
722        self
723    }
724}
725
726/// Structured JSON content, optionally paired with a JSON Schema for validation.
727///
728/// Providers that support structured output (e.g. function-calling mode) may
729/// return a `StructuredPart` instead of free-form text.
730#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
731pub struct StructuredPart {
732    /// The structured data as a JSON [`Value`].
733    pub value: Value,
734    /// An optional JSON Schema that `value` conforms to.
735    pub schema: Option<Value>,
736    /// Arbitrary key-value metadata.
737    pub metadata: MetadataMap,
738}
739
740impl StructuredPart {
741    /// Builds a structured part with empty metadata and no schema.
742    pub fn new(value: Value) -> Self {
743        Self {
744            value,
745            schema: None,
746            metadata: MetadataMap::new(),
747        }
748    }
749
750    /// Sets the optional schema.
751    pub fn with_schema(mut self, schema: Value) -> Self {
752        self.schema = Some(schema);
753        self
754    }
755
756    /// Replaces the structured-part metadata.
757    pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
758        self.metadata = metadata;
759        self
760    }
761}
762
763/// Model reasoning or chain-of-thought output.
764///
765/// Some providers expose the model's internal reasoning alongside the final
766/// answer. The reasoning may be a readable summary, opaque data, or both.
767/// The `redacted` flag indicates provider-side filtering.
768#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
769pub struct ReasoningPart {
770    /// A human-readable summary of the model's reasoning.
771    pub summary: Option<String>,
772    /// Opaque or detailed reasoning data.
773    pub data: Option<DataRef>,
774    /// `true` if the provider redacted the full reasoning content.
775    pub redacted: bool,
776    /// Arbitrary key-value metadata.
777    pub metadata: MetadataMap,
778}
779
780impl ReasoningPart {
781    /// Builds a readable reasoning summary.
782    pub fn summary(summary: impl Into<String>) -> Self {
783        Self {
784            summary: Some(summary.into()),
785            data: None,
786            redacted: false,
787            metadata: MetadataMap::new(),
788        }
789    }
790
791    /// Builds a redacted readable reasoning summary.
792    pub fn redacted_summary(summary: impl Into<String>) -> Self {
793        Self::summary(summary).with_redacted(true)
794    }
795
796    /// Sets the optional reasoning data reference.
797    pub fn with_data(mut self, data: DataRef) -> Self {
798        self.data = Some(data);
799        self
800    }
801
802    /// Sets whether the reasoning content was redacted.
803    pub fn with_redacted(mut self, redacted: bool) -> Self {
804        self.redacted = redacted;
805        self
806    }
807
808    /// Replaces the reasoning-part metadata.
809    pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
810        self.metadata = metadata;
811        self
812    }
813}
814
815/// A tool invocation request emitted by the model.
816///
817/// The agent loop receives this part, executes the named tool, and appends a
818/// [`ToolResultPart`] back to the transcript for the model to observe.
819///
820/// # Example
821///
822/// ```rust
823/// use agentkit_core::ToolCallPart;
824/// use serde_json::json;
825///
826/// let call = ToolCallPart::new("call-7", "fs_read_file", json!({ "path": "src/main.rs" }));
827///
828/// assert_eq!(call.name, "fs_read_file");
829/// ```
830#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
831pub struct ToolCallPart {
832    /// Unique identifier for this tool call, used to correlate with [`ToolResultPart::call_id`].
833    pub id: ToolCallId,
834    /// The name of the tool to invoke (e.g. `"fs_read_file"`, `"shell_exec"`).
835    pub name: String,
836    /// The JSON arguments to pass to the tool.
837    pub input: Value,
838    /// Arbitrary key-value metadata.
839    pub metadata: MetadataMap,
840}
841
842impl ToolCallPart {
843    /// Builds a tool-call part with empty metadata.
844    pub fn new(id: impl Into<ToolCallId>, name: impl Into<String>, input: Value) -> Self {
845        Self {
846            id: id.into(),
847            name: name.into(),
848            input,
849            metadata: MetadataMap::new(),
850        }
851    }
852
853    /// Replaces the tool-call metadata.
854    pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
855        self.metadata = metadata;
856        self
857    }
858}
859
860/// The result of executing a tool, sent back to the model.
861///
862/// Each `ToolResultPart` references the [`ToolCallPart`] it answers via
863/// `call_id`. The `is_error` flag tells the model whether the tool succeeded.
864///
865/// # Example
866///
867/// ```rust
868/// use agentkit_core::{ToolOutput, ToolResultPart};
869///
870/// let result = ToolResultPart::success("call-7", ToolOutput::text("fn main() { ... }"));
871///
872/// assert!(!result.is_error);
873/// ```
874#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
875pub struct ToolResultPart {
876    /// The [`ToolCallId`] of the tool call this result answers.
877    pub call_id: ToolCallId,
878    /// The output produced by the tool.
879    pub output: ToolOutput,
880    /// `true` if the tool execution failed.
881    pub is_error: bool,
882    /// Arbitrary key-value metadata.
883    pub metadata: MetadataMap,
884}
885
886impl ToolResultPart {
887    /// Builds a successful tool-result part with empty metadata.
888    pub fn success(call_id: impl Into<ToolCallId>, output: ToolOutput) -> Self {
889        Self {
890            call_id: call_id.into(),
891            output,
892            is_error: false,
893            metadata: MetadataMap::new(),
894        }
895    }
896
897    /// Builds an error tool-result part with empty metadata.
898    pub fn error(call_id: impl Into<ToolCallId>, output: ToolOutput) -> Self {
899        Self::success(call_id, output).with_is_error(true)
900    }
901
902    /// Sets the error flag explicitly.
903    pub fn with_is_error(mut self, is_error: bool) -> Self {
904        self.is_error = is_error;
905        self
906    }
907
908    /// Replaces the tool-result metadata.
909    pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
910        self.metadata = metadata;
911        self
912    }
913}
914
915/// The payload returned by a tool execution.
916///
917/// Tools may return plain text, structured JSON, a composite list of
918/// [`Part`]s, or a collection of files.
919#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
920pub enum ToolOutput {
921    /// Plain text output.
922    Text(String),
923    /// Structured JSON output.
924    Structured(Value),
925    /// A list of content parts (e.g. text + images).
926    Parts(Vec<Part>),
927    /// A list of file attachments.
928    Files(Vec<FilePart>),
929}
930
931impl ToolOutput {
932    /// Builds plain-text tool output.
933    pub fn text(text: impl Into<String>) -> Self {
934        Self::Text(text.into())
935    }
936
937    /// Builds structured tool output.
938    pub fn structured(value: Value) -> Self {
939        Self::Structured(value)
940    }
941
942    /// Builds multipart tool output.
943    pub fn parts(parts: Vec<Part>) -> Self {
944        Self::Parts(parts)
945    }
946
947    /// Builds file-based tool output.
948    pub fn files(files: Vec<FilePart>) -> Self {
949        Self::Files(files)
950    }
951}
952
953/// A provider-specific content part that does not fit the standard variants.
954///
955/// Use this for extensions or experimental features that have not been
956/// promoted to a first-class [`Part`] variant.
957#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
958pub struct CustomPart {
959    /// A free-form string identifying the custom part type.
960    pub kind: String,
961    /// Optional data reference.
962    pub data: Option<DataRef>,
963    /// Optional structured value.
964    pub value: Option<Value>,
965    /// Arbitrary key-value metadata.
966    pub metadata: MetadataMap,
967}
968
969impl CustomPart {
970    /// Builds a custom part with empty metadata.
971    pub fn new(kind: impl Into<String>) -> Self {
972        Self {
973            kind: kind.into(),
974            data: None,
975            value: None,
976            metadata: MetadataMap::new(),
977        }
978    }
979
980    /// Sets the custom part data reference.
981    pub fn with_data(mut self, data: DataRef) -> Self {
982        self.data = Some(data);
983        self
984    }
985
986    /// Sets the custom part structured value.
987    pub fn with_value(mut self, value: Value) -> Self {
988        self.value = Some(value);
989        self
990    }
991
992    /// Replaces the custom-part metadata.
993    pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
994        self.metadata = metadata;
995        self
996    }
997}
998
999/// An incremental update emitted while a model turn is streaming.
1000///
1001/// The provider adapter emits a sequence of `Delta` values that the loop and
1002/// reporters consume to reconstruct the full [`Item`] progressively. A typical
1003/// sequence looks like:
1004///
1005/// 1. [`BeginPart`](Self::BeginPart) -- allocates a new part buffer.
1006/// 2. One or more [`AppendText`](Self::AppendText) /
1007///    [`AppendBytes`](Self::AppendBytes) -- fills the buffer.
1008/// 3. [`CommitPart`](Self::CommitPart) -- finalises the part.
1009///
1010/// # Example
1011///
1012/// ```rust
1013/// use agentkit_core::{Delta, PartId, PartKind};
1014///
1015/// let deltas = vec![
1016///     Delta::BeginPart { part_id: PartId::new("p1"), kind: PartKind::Text },
1017///     Delta::AppendText { part_id: PartId::new("p1"), chunk: "Hello".into() },
1018///     Delta::AppendText { part_id: PartId::new("p1"), chunk: ", world!".into() },
1019/// ];
1020///
1021/// assert_eq!(deltas.len(), 3);
1022/// ```
1023#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1024pub enum Delta {
1025    /// Signals the start of a new part with the given kind.
1026    BeginPart {
1027        /// Identifier for the part being constructed.
1028        part_id: PartId,
1029        /// The kind of part being started.
1030        kind: PartKind,
1031    },
1032    /// Appends a text chunk to an in-progress part.
1033    AppendText {
1034        /// Identifier of the target part.
1035        part_id: PartId,
1036        /// The text chunk to append.
1037        chunk: String,
1038    },
1039    /// Appends raw bytes to an in-progress part.
1040    AppendBytes {
1041        /// Identifier of the target part.
1042        part_id: PartId,
1043        /// The byte chunk to append.
1044        chunk: Vec<u8>,
1045    },
1046    /// Replaces the structured value of an in-progress part wholesale.
1047    ReplaceStructured {
1048        /// Identifier of the target part.
1049        part_id: PartId,
1050        /// The new structured value.
1051        value: Value,
1052    },
1053    /// Sets or replaces the metadata on an in-progress part.
1054    SetMetadata {
1055        /// Identifier of the target part.
1056        part_id: PartId,
1057        /// The new metadata map.
1058        metadata: MetadataMap,
1059    },
1060    /// Finalises a part, providing the fully assembled [`Part`].
1061    CommitPart {
1062        /// The completed part.
1063        part: Part,
1064    },
1065}
1066
1067/// Token and cost usage reported by a model provider for a single turn.
1068///
1069/// Reporters and compaction triggers inspect `Usage` to log progress, enforce
1070/// budgets, and decide when to compact the transcript.
1071///
1072/// # Example
1073///
1074/// ```rust
1075/// use agentkit_core::{TokenUsage, Usage};
1076///
1077/// let usage = Usage::new(
1078///     TokenUsage::new(1500, 200)
1079///         .with_cached_input_tokens(1000)
1080///         .with_cache_write_input_tokens(1200),
1081/// );
1082///
1083/// let tokens = usage.tokens.as_ref().unwrap();
1084/// assert_eq!(tokens.input_tokens, 1500);
1085/// ```
1086#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1087pub struct Usage {
1088    /// Token counts for this turn, if the provider reports them.
1089    pub tokens: Option<TokenUsage>,
1090    /// Monetary cost for this turn, if the provider reports it.
1091    pub cost: Option<CostUsage>,
1092    /// Arbitrary key-value metadata.
1093    pub metadata: MetadataMap,
1094}
1095
1096impl Usage {
1097    /// Builds a usage record with token counts and no cost.
1098    pub fn new(tokens: TokenUsage) -> Self {
1099        Self {
1100            tokens: Some(tokens),
1101            cost: None,
1102            metadata: MetadataMap::new(),
1103        }
1104    }
1105
1106    /// Sets the cost information.
1107    pub fn with_cost(mut self, cost: CostUsage) -> Self {
1108        self.cost = Some(cost);
1109        self
1110    }
1111
1112    /// Replaces the usage metadata.
1113    pub fn with_metadata(mut self, metadata: MetadataMap) -> Self {
1114        self.metadata = metadata;
1115        self
1116    }
1117}
1118
1119/// Token counts broken down by direction and special categories.
1120#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1121pub struct TokenUsage {
1122    /// Number of tokens in the input (prompt) sent to the model.
1123    pub input_tokens: u64,
1124    /// Number of tokens in the model's output (completion).
1125    pub output_tokens: u64,
1126    /// Tokens consumed by the model's internal reasoning, if reported.
1127    pub reasoning_tokens: Option<u64>,
1128    /// Input tokens served from the provider's prompt cache, if reported.
1129    pub cached_input_tokens: Option<u64>,
1130    /// Input tokens written into the provider's prompt cache, if reported.
1131    pub cache_write_input_tokens: Option<u64>,
1132}
1133
1134impl TokenUsage {
1135    /// Builds token usage with required input and output counts.
1136    pub fn new(input_tokens: u64, output_tokens: u64) -> Self {
1137        Self {
1138            input_tokens,
1139            output_tokens,
1140            reasoning_tokens: None,
1141            cached_input_tokens: None,
1142            cache_write_input_tokens: None,
1143        }
1144    }
1145
1146    /// Sets reasoning token count.
1147    pub fn with_reasoning_tokens(mut self, reasoning_tokens: u64) -> Self {
1148        self.reasoning_tokens = Some(reasoning_tokens);
1149        self
1150    }
1151
1152    /// Sets cached input token count.
1153    pub fn with_cached_input_tokens(mut self, cached_input_tokens: u64) -> Self {
1154        self.cached_input_tokens = Some(cached_input_tokens);
1155        self
1156    }
1157
1158    /// Sets cache-write input token count.
1159    pub fn with_cache_write_input_tokens(mut self, cache_write_input_tokens: u64) -> Self {
1160        self.cache_write_input_tokens = Some(cache_write_input_tokens);
1161        self
1162    }
1163}
1164
1165/// Monetary cost for a single model turn.
1166#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1167pub struct CostUsage {
1168    /// The cost amount as a floating-point number.
1169    pub amount: f64,
1170    /// The ISO 4217 currency code (e.g. `"USD"`).
1171    pub currency: String,
1172    /// An optional provider-specific cost string for display purposes.
1173    pub provider_amount: Option<String>,
1174}
1175
1176impl CostUsage {
1177    /// Builds cost usage with no provider-specific display string.
1178    pub fn new(amount: f64, currency: impl Into<String>) -> Self {
1179        Self {
1180            amount,
1181            currency: currency.into(),
1182            provider_amount: None,
1183        }
1184    }
1185
1186    /// Sets the optional provider-specific display value.
1187    pub fn with_provider_amount(mut self, provider_amount: impl Into<String>) -> Self {
1188        self.provider_amount = Some(provider_amount.into());
1189        self
1190    }
1191}
1192
1193/// The reason a model turn ended.
1194///
1195/// The loop inspects the `FinishReason` to decide whether to execute tool
1196/// calls, request more input, or report an error.
1197#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1198pub enum FinishReason {
1199    /// The model finished generating its response normally.
1200    Completed,
1201    /// The model stopped to invoke one or more tools.
1202    ToolCall,
1203    /// The response was truncated because the token limit was reached.
1204    MaxTokens,
1205    /// The turn was cancelled via [`TurnCancellation`].
1206    Cancelled,
1207    /// The provider blocked the response (e.g. content policy violation).
1208    Blocked,
1209    /// An error occurred during generation.
1210    Error,
1211    /// A provider-specific reason not covered by the standard variants.
1212    Other(String),
1213}
1214
1215/// Read-only view over an [`Item`]'s essential fields.
1216///
1217/// This trait lets downstream crates (compaction, reporting) operate on
1218/// item-like types without depending on the concrete [`Item`] struct.
1219pub trait ItemView {
1220    /// Returns the role of this item.
1221    fn kind(&self) -> ItemKind;
1222    /// Returns the content parts.
1223    fn parts(&self) -> &[Part];
1224    /// Returns the metadata map.
1225    fn metadata(&self) -> &MetadataMap;
1226}
1227
1228impl ItemView for Item {
1229    fn kind(&self) -> ItemKind {
1230        self.kind
1231    }
1232
1233    fn parts(&self) -> &[Part] {
1234        &self.parts
1235    }
1236
1237    fn metadata(&self) -> &MetadataMap {
1238        &self.metadata
1239    }
1240}
1241
1242/// Error returned when content cannot be normalised into the agentkit data model.
1243#[derive(Debug, Error)]
1244pub enum NormalizeError {
1245    /// The content shape is not supported by the current provider adapter.
1246    #[error("unsupported content shape: {0}")]
1247    Unsupported(String),
1248}
1249
1250/// Error indicating an invalid state in the provider protocol.
1251#[derive(Debug, Error)]
1252pub enum ProtocolError {
1253    /// The provider or loop reached a state that violates protocol invariants.
1254    #[error("invalid protocol state: {0}")]
1255    InvalidState(String),
1256}
1257
1258/// Top-level error type that unifies normalisation and protocol errors.
1259///
1260/// Provider adapters and the agent loop surface this type so callers can
1261/// handle both categories uniformly.
1262#[derive(Debug, Error)]
1263pub enum AgentError {
1264    /// A content normalisation error.
1265    #[error(transparent)]
1266    Normalize(#[from] NormalizeError),
1267    /// A protocol-level error.
1268    #[error(transparent)]
1269    Protocol(#[from] ProtocolError),
1270}