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