|
| 1 | +//! End-to-end tool calling for a host that drives its own model loop. |
| 2 | +//! |
| 3 | +//! [`super`] answers *"what did the model ask for"*. This module answers |
| 4 | +//! everything around it: how the model is **told** to ask, how results are |
| 5 | +//! **rendered** back, and how a transcript is **replayed** onto the provider |
| 6 | +//! wire so the next iteration is well-formed. Together those are one thing — |
| 7 | +//! a **dialect** — and they have to move together, because a catalogue that |
| 8 | +//! advertises one grammar and a parser that expects another is a silent |
| 9 | +//! whole-turn failure with no error anywhere. |
| 10 | +//! |
| 11 | +//! Three dialects ship: [`XmlDialect`] (JSON in a tag), [`PFormatDialect`] |
| 12 | +//! (compact positional), and [`NativeDialect`] (the provider's structured |
| 13 | +//! channel). |
| 14 | +//! |
| 15 | +//! This module speaks [`TranscriptEntry`], a deliberately thin record shape a |
| 16 | +//! host maps onto in a few `From` impls. It does not assume a provider message |
| 17 | +//! type or anything about how and when the host calls the model. |
| 18 | +//! |
| 19 | +//! The second case is not a lesser one. A host with years of persisted |
| 20 | +//! transcripts, per-turn security policy, and provider quirks encoded in its |
| 21 | +//! own storage cannot adopt a foreign message model just to stop maintaining a |
| 22 | +//! tool-call parser — and the parser is the part that is genuinely universal. |
| 23 | +//! This module is the seam that lets it hand over the universal part alone. |
| 24 | +//! |
| 25 | +//! # What stays with the host |
| 26 | +//! |
| 27 | +//! Executing a tool: permission checks, sandboxing, approval gates, timeouts, |
| 28 | +//! progress events. A dialect decides what the model *reads and writes*; it |
| 29 | +//! never decides what is allowed to *happen*. That boundary is what keeps a |
| 30 | +//! host's security policy in the host, where it can be audited. |
| 31 | +
|
| 32 | +mod catalogue; |
| 33 | +mod native; |
| 34 | +mod pairing; |
| 35 | +mod pformat; |
| 36 | +mod text; |
| 37 | +mod types; |
| 38 | +mod xml; |
| 39 | + |
| 40 | +pub use catalogue::{CATALOGUE_HEADING, render_json_catalogue, render_pformat_catalogue}; |
| 41 | +pub use native::NativeDialect; |
| 42 | +pub use pairing::pair_tool_cycles; |
| 43 | +pub use pformat::PFormatDialect; |
| 44 | +pub use text::TOOL_RESULTS_PREFIX; |
| 45 | +pub use types::{ |
| 46 | + DialectMessage, DialectResponse, DialectRole, NativeToolCall, ToolCallFormat, ToolOutcome, |
| 47 | + ToolResultEntry, TranscriptEntry, |
| 48 | +}; |
| 49 | +pub use xml::XmlDialect; |
| 50 | + |
| 51 | +use crate::ParsedToolCall; |
| 52 | +use tinytools::ToolSpec; |
| 53 | + |
| 54 | +/// One complete way of speaking tools to a model. |
| 55 | +/// |
| 56 | +/// Every method is a face of the same contract, which is why they are one |
| 57 | +/// trait: the catalogue the model reads, the calls it writes, the results it |
| 58 | +/// gets back, and the history it is re-shown all have to describe the same |
| 59 | +/// protocol. Splitting them into separate configurable pieces is how they drift. |
| 60 | +pub trait ToolDialect: Send + Sync { |
| 61 | + /// Split a model response into narrative text and the calls it requested. |
| 62 | + fn parse_response(&self, response: &DialectResponse) -> (String, Vec<ParsedToolCall>); |
| 63 | + |
| 64 | + /// Render executed outcomes into the transcript records that follow the |
| 65 | + /// assistant turn. |
| 66 | + /// |
| 67 | + /// Usually one record. It is a `Vec` because a |
| 68 | + /// [`trusted_verbatim`](ToolOutcome::trusted_verbatim) outcome must reach |
| 69 | + /// the model at byte 0 of its own message, which a batch cannot provide: a |
| 70 | + /// text dialect closes the batch before such an outcome and reopens it |
| 71 | + /// after. A dialect that never reshapes output — the native one — always |
| 72 | + /// returns exactly one record. |
| 73 | + fn format_results(&self, results: &[ToolOutcome]) -> Vec<TranscriptEntry>; |
| 74 | + |
| 75 | + /// The protocol block for the system prompt. |
| 76 | + /// |
| 77 | + /// Whether `tools` is used depends on the dialect: only a dialect that |
| 78 | + /// [embeds its own catalogue](Self::embeds_tool_catalogue) reads it. The |
| 79 | + /// others get their catalogue from the prompt's tool section, or need none |
| 80 | + /// at all. |
| 81 | + fn prompt_instructions(&self, tools: &[ToolSpec]) -> String; |
| 82 | + |
| 83 | + /// Replay a transcript as flat provider messages. |
| 84 | + fn to_provider_messages(&self, history: &[TranscriptEntry]) -> Vec<DialectMessage>; |
| 85 | + |
| 86 | + /// Whether structured tool specs belong in the API request. |
| 87 | + /// |
| 88 | + /// `false` for the text dialects: sending specs a dialect cannot read back |
| 89 | + /// invites the model onto a channel nothing is listening on. |
| 90 | + fn should_send_tool_specs(&self) -> bool; |
| 91 | + |
| 92 | + /// Whether [`Self::prompt_instructions`] already lists the tools. |
| 93 | + /// |
| 94 | + /// A host uses this to avoid rendering the catalogue twice. Defaults to |
| 95 | + /// `false` — the common case, where the prompt's tool section owns it. |
| 96 | + fn embeds_tool_catalogue(&self) -> bool { |
| 97 | + false |
| 98 | + } |
| 99 | + |
| 100 | + /// How the tool catalogue should spell each entry, so the prompt and this |
| 101 | + /// dialect's parser cannot disagree. |
| 102 | + fn tool_call_format(&self) -> ToolCallFormat; |
| 103 | +} |
| 104 | + |
| 105 | +#[cfg(test)] |
| 106 | +#[path = "test.rs"] |
| 107 | +mod test; |
0 commit comments