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}