Repository navigation
Add agent tool protocol crate #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| [package] | ||
| publish = false | ||
| name = "tinytools-agent" | ||
| version.workspace = true | ||
| edition.workspace = true | ||
| rust-version.workspace = true | ||
| license.workspace = true | ||
| repository.workspace = true | ||
| description = "Agent-facing tool-call parsing, rendering, dialects, and transcript blocks." | ||
| documentation = "https://docs.rs/tinytools-agent" | ||
| readme = "README.md" | ||
|
|
||
| [dependencies] | ||
| regex = { workspace = true } | ||
| serde = { workspace = true } | ||
| serde_json = { workspace = true } | ||
| tinytools = { path = "../tinytools", version = "0.1.0" } | ||
| tracing = { workspace = true, optional = true } | ||
|
|
||
| [features] | ||
| default = [] | ||
| tracing = ["dep:tracing"] | ||
|
|
||
| [lints] | ||
| workspace = true |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # TinyTools Agent Protocols | ||
|
|
||
| `tinytools-agent` owns the model-facing protocol around a tool declaration: | ||
| tool-call parsing, P-Format signatures, XML and native dialects, catalogue and | ||
| result-block rendering, and safe transcript replay. | ||
|
|
||
| It consumes `tinytools::ToolSpec` and never executes a tool. Permission checks, | ||
| approvals, sandboxing, timeouts, provider requests, and progress reporting | ||
| remain responsibilities of the consuming harness or host. | ||
|
|
||
| The primary API is: | ||
|
|
||
| - root parsing and P-Format helpers; | ||
| - `dialect::ToolDialect` and the XML, P-Format, and native implementations; | ||
| - provider-neutral transcript, call, result, and message block types. | ||
|
|
||
| The crate has no dependency on TinyAgents or an inference/provider runtime. | ||
| The optional `tracing` feature emits diagnostics for parser recovery and | ||
| transcript repair decisions. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| //! Rendering a tool catalogue into a system prompt. | ||
| //! | ||
| //! Two shapes, because there are two things a model can be told: | ||
| //! | ||
| //! * [`render_pformat_catalogue`] gives it a **call signature** — | ||
| //! `read_file[path]` — which is all a positional dialect needs and costs a | ||
| //! handful of tokens per tool. | ||
| //! * [`render_json_catalogue`] gives it the **whole parameter schema**, which | ||
| //! is what a JSON-in-tag dialect needs because the model has to name each | ||
| //! argument itself. | ||
| //! | ||
| //! Neither is the "informational" case: when the provider receives real tool | ||
| //! specs in the request, repeating them in the prompt is pure token bloat, so | ||
| //! the native dialect renders no catalogue at all. | ||
|
|
||
| use std::fmt::Write as _; | ||
|
|
||
| use crate::pformat::render_signature_from_schema; | ||
| use tinytools::ToolSpec; | ||
|
|
||
| /// Heading the catalogue is rendered under. | ||
| pub const CATALOGUE_HEADING: &str = "## Tools\n\n"; | ||
|
|
||
| /// Render the compact positional catalogue: one line per tool carrying its | ||
| /// description and its p-format call signature. | ||
| /// | ||
| /// The signature comes straight from the parameter schema, through the same | ||
| /// [`render_signature_from_schema`] the parser reconstructs arguments with — so | ||
| /// the order the model reads is by construction the order the parser expects. | ||
| /// Rendering it any other way is how a catalogue and a parser drift apart. | ||
| #[must_use] | ||
| pub fn render_pformat_catalogue(tools: &[ToolSpec]) -> String { | ||
| let mut out = String::from(CATALOGUE_HEADING); | ||
| for tool in tools { | ||
| let signature = render_signature_from_schema(&tool.name, &tool.parameters); | ||
| let _ = writeln!( | ||
| out, | ||
| "- **{}**: {}\n Call as: `{}`", | ||
| tool.name, tool.description, signature | ||
| ); | ||
| } | ||
| out | ||
| } | ||
|
|
||
| /// Render the full-schema catalogue: one line per tool carrying its description | ||
| /// and its complete JSON-Schema parameter object. | ||
| /// | ||
| /// Used by the JSON-in-tag dialect, where the model writes argument names out | ||
| /// itself and therefore has to see them. | ||
| #[must_use] | ||
| pub fn render_json_catalogue(tools: &[ToolSpec]) -> String { | ||
| let mut out = String::new(); | ||
| for tool in tools { | ||
| let _ = writeln!( | ||
| out, | ||
| "- **{}**: {}\n Parameters: `{}`", | ||
| tool.name, tool.description, tool.parameters | ||
| ); | ||
| } | ||
| out | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| //! End-to-end tool calling for a host that drives its own model loop. | ||
| //! | ||
| //! [`super`] answers *"what did the model ask for"*. This module answers | ||
| //! everything around it: how the model is **told** to ask, how results are | ||
| //! **rendered** back, and how a transcript is **replayed** onto the provider | ||
| //! wire so the next iteration is well-formed. Together those are one thing — | ||
| //! a **dialect** — and they have to move together, because a catalogue that | ||
| //! advertises one grammar and a parser that expects another is a silent | ||
| //! whole-turn failure with no error anywhere. | ||
| //! | ||
| //! Three dialects ship: [`XmlDialect`] (JSON in a tag), [`PFormatDialect`] | ||
| //! (compact positional), and [`NativeDialect`] (the provider's structured | ||
| //! channel). | ||
| //! | ||
| //! This module speaks [`TranscriptEntry`], a deliberately thin record shape a | ||
| //! host maps onto in a few `From` impls. It does not assume a provider message | ||
| //! type or anything about how and when the host calls the model. | ||
| //! | ||
| //! The second case is not a lesser one. A host with years of persisted | ||
| //! transcripts, per-turn security policy, and provider quirks encoded in its | ||
| //! own storage cannot adopt a foreign message model just to stop maintaining a | ||
| //! tool-call parser — and the parser is the part that is genuinely universal. | ||
| //! This module is the seam that lets it hand over the universal part alone. | ||
| //! | ||
| //! # What stays with the host | ||
| //! | ||
| //! Executing a tool: permission checks, sandboxing, approval gates, timeouts, | ||
| //! progress events. A dialect decides what the model *reads and writes*; it | ||
| //! never decides what is allowed to *happen*. That boundary is what keeps a | ||
| //! host's security policy in the host, where it can be audited. | ||
|
|
||
| mod catalogue; | ||
| mod native; | ||
| mod pairing; | ||
| mod pformat; | ||
| mod text; | ||
| mod types; | ||
| mod xml; | ||
|
|
||
| pub use catalogue::{CATALOGUE_HEADING, render_json_catalogue, render_pformat_catalogue}; | ||
| pub use native::NativeDialect; | ||
| pub use pairing::pair_tool_cycles; | ||
| pub use pformat::PFormatDialect; | ||
| pub use text::TOOL_RESULTS_PREFIX; | ||
| pub use types::{ | ||
| DialectMessage, DialectResponse, DialectRole, NativeToolCall, ToolCallFormat, ToolOutcome, | ||
| ToolResultEntry, TranscriptEntry, | ||
| }; | ||
| pub use xml::XmlDialect; | ||
|
|
||
| use crate::ParsedToolCall; | ||
| use tinytools::ToolSpec; | ||
|
|
||
| /// One complete way of speaking tools to a model. | ||
| /// | ||
| /// Every method is a face of the same contract, which is why they are one | ||
| /// trait: the catalogue the model reads, the calls it writes, the results it | ||
| /// gets back, and the history it is re-shown all have to describe the same | ||
| /// protocol. Splitting them into separate configurable pieces is how they drift. | ||
| pub trait ToolDialect: Send + Sync { | ||
| /// Split a model response into narrative text and the calls it requested. | ||
| fn parse_response(&self, response: &DialectResponse) -> (String, Vec<ParsedToolCall>); | ||
|
|
||
| /// Render executed outcomes into the transcript records that follow the | ||
| /// assistant turn. | ||
| /// | ||
| /// Usually one record. It is a `Vec` because a | ||
| /// [`trusted_verbatim`](ToolOutcome::trusted_verbatim) outcome must reach | ||
| /// the model at byte 0 of its own message, which a batch cannot provide: a | ||
| /// text dialect closes the batch before such an outcome and reopens it | ||
| /// after. A dialect that never reshapes output — the native one — always | ||
| /// returns exactly one record. | ||
| fn format_results(&self, results: &[ToolOutcome]) -> Vec<TranscriptEntry>; | ||
|
|
||
| /// The protocol block for the system prompt. | ||
| /// | ||
| /// Whether `tools` is used depends on the dialect: only a dialect that | ||
| /// [embeds its own catalogue](Self::embeds_tool_catalogue) reads it. The | ||
| /// others get their catalogue from the prompt's tool section, or need none | ||
| /// at all. | ||
| fn prompt_instructions(&self, tools: &[ToolSpec]) -> String; | ||
|
|
||
| /// Replay a transcript as flat provider messages. | ||
| fn to_provider_messages(&self, history: &[TranscriptEntry]) -> Vec<DialectMessage>; | ||
|
|
||
| /// Whether structured tool specs belong in the API request. | ||
| /// | ||
| /// `false` for the text dialects: sending specs a dialect cannot read back | ||
| /// invites the model onto a channel nothing is listening on. | ||
| fn should_send_tool_specs(&self) -> bool; | ||
|
|
||
| /// Whether [`Self::prompt_instructions`] already lists the tools. | ||
| /// | ||
| /// A host uses this to avoid rendering the catalogue twice. Defaults to | ||
| /// `false` — the common case, where the prompt's tool section owns it. | ||
| fn embeds_tool_catalogue(&self) -> bool { | ||
| false | ||
| } | ||
|
|
||
| /// How the tool catalogue should spell each entry, so the prompt and this | ||
| /// dialect's parser cannot disagree. | ||
| fn tool_call_format(&self) -> ToolCallFormat; | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| #[path = "test.rs"] | ||
| mod test; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new
dialectmodule spans seven implementation files and owns parsing, rendering, transcript repair, and provider replay, but it has no module-levelREADME.md. Add one undersrc/dialect/documenting its design, public surface, and operational constraints.AGENTS.md reference: AGENTS.md:L202-L203
Useful? React with 👍 / 👎.