feat: prepare agen crates for publication
This commit is contained in:
Generated
+44
-44
@@ -37,6 +37,42 @@ dependencies = [
|
|||||||
"cpufeatures 0.2.17",
|
"cpufeatures 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "agen"
|
||||||
|
version = "0.2.1"
|
||||||
|
dependencies = [
|
||||||
|
"agen-macros",
|
||||||
|
"async-trait",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"chrono",
|
||||||
|
"clap",
|
||||||
|
"dotenv",
|
||||||
|
"eventsource-stream",
|
||||||
|
"futures",
|
||||||
|
"reqwest",
|
||||||
|
"schemars",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
"trybuild",
|
||||||
|
"wiremock",
|
||||||
|
"zstd",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "agen-macros"
|
||||||
|
version = "0.2.0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
@@ -2261,42 +2297,6 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "llm-engine"
|
|
||||||
version = "0.2.1"
|
|
||||||
dependencies = [
|
|
||||||
"async-trait",
|
|
||||||
"base64 0.22.1",
|
|
||||||
"chrono",
|
|
||||||
"clap",
|
|
||||||
"dotenv",
|
|
||||||
"eventsource-stream",
|
|
||||||
"futures",
|
|
||||||
"llm-engine-macros",
|
|
||||||
"reqwest",
|
|
||||||
"schemars",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tempfile",
|
|
||||||
"thiserror 2.0.18",
|
|
||||||
"tokio",
|
|
||||||
"tokio-util",
|
|
||||||
"tracing",
|
|
||||||
"tracing-subscriber",
|
|
||||||
"trybuild",
|
|
||||||
"wiremock",
|
|
||||||
"zstd",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "llm-engine-macros"
|
|
||||||
version = "0.2.0"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.117",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@@ -2384,8 +2384,8 @@ dependencies = [
|
|||||||
name = "manifest"
|
name = "manifest"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"agen",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"llm-engine",
|
|
||||||
"protocol",
|
"protocol",
|
||||||
"secrets",
|
"secrets",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -2512,11 +2512,11 @@ dependencies = [
|
|||||||
name = "memory"
|
name = "memory"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"agen",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
"libc",
|
"libc",
|
||||||
"lint-common",
|
"lint-common",
|
||||||
"llm-engine",
|
|
||||||
"manifest",
|
"manifest",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -3967,10 +3967,10 @@ dependencies = [
|
|||||||
name = "session-store"
|
name = "session-store"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"agen",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"futures",
|
"futures",
|
||||||
"llm-engine",
|
|
||||||
"protocol",
|
"protocol",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -4408,10 +4408,10 @@ dependencies = [
|
|||||||
name = "ticket"
|
name = "ticket"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"agen",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
"fs4",
|
"fs4",
|
||||||
"llm-engine",
|
|
||||||
"project-record",
|
"project-record",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"schemars",
|
"schemars",
|
||||||
@@ -4612,11 +4612,11 @@ checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
|
|||||||
name = "tools"
|
name = "tools"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"agen",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"filetime",
|
"filetime",
|
||||||
"fs-operation",
|
"fs-operation",
|
||||||
"html5ever",
|
"html5ever",
|
||||||
"llm-engine",
|
|
||||||
"manifest",
|
"manifest",
|
||||||
"markup5ever_rcdom",
|
"markup5ever_rcdom",
|
||||||
"pdf-extract",
|
"pdf-extract",
|
||||||
@@ -4794,11 +4794,11 @@ checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
|||||||
name = "tui"
|
name = "tui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"agen",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"client",
|
"client",
|
||||||
"crossterm 0.28.1",
|
"crossterm 0.28.1",
|
||||||
"fs4",
|
"fs4",
|
||||||
"llm-engine",
|
|
||||||
"manifest",
|
"manifest",
|
||||||
"protocol",
|
"protocol",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
@@ -6060,6 +6060,7 @@ dependencies = [
|
|||||||
name = "worker"
|
name = "worker"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"agen",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -6074,7 +6075,6 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
"include_dir",
|
"include_dir",
|
||||||
"libc",
|
"libc",
|
||||||
"llm-engine",
|
|
||||||
"manifest",
|
"manifest",
|
||||||
"mcp",
|
"mcp",
|
||||||
"memory",
|
"memory",
|
||||||
@@ -6108,13 +6108,13 @@ dependencies = [
|
|||||||
name = "worker-runtime"
|
name = "worker-runtime"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"agen",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"decodal",
|
"decodal",
|
||||||
"flow",
|
"flow",
|
||||||
"futures",
|
"futures",
|
||||||
"llm-engine",
|
|
||||||
"manifest",
|
"manifest",
|
||||||
"protocol",
|
"protocol",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
|||||||
+6
-6
@@ -2,8 +2,8 @@
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"crates/client",
|
"crates/client",
|
||||||
"crates/llm-engine",
|
"crates/agen",
|
||||||
"crates/llm-engine-macros",
|
"crates/agen-macros",
|
||||||
"crates/session-store",
|
"crates/session-store",
|
||||||
"crates/secrets",
|
"crates/secrets",
|
||||||
"crates/manifest",
|
"crates/manifest",
|
||||||
@@ -33,8 +33,8 @@ members = [
|
|||||||
]
|
]
|
||||||
default-members = [
|
default-members = [
|
||||||
"crates/client",
|
"crates/client",
|
||||||
"crates/llm-engine",
|
"crates/agen",
|
||||||
"crates/llm-engine-macros",
|
"crates/agen-macros",
|
||||||
"crates/session-store",
|
"crates/session-store",
|
||||||
"crates/secrets",
|
"crates/secrets",
|
||||||
"crates/manifest",
|
"crates/manifest",
|
||||||
@@ -69,8 +69,8 @@ license = "MIT"
|
|||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
# Internal crates
|
# Internal crates
|
||||||
client = { path = "crates/client" }
|
client = { path = "crates/client" }
|
||||||
llm-engine = { path = "crates/llm-engine", version = "0.2" }
|
agen = { path = "crates/agen", version = "0.2" }
|
||||||
llm-engine-macros = { path = "crates/llm-engine-macros", version = "0.2" }
|
agen-macros = { path = "crates/agen-macros", version = "0.2" }
|
||||||
manifest = { path = "crates/manifest" }
|
manifest = { path = "crates/manifest" }
|
||||||
mcp = { path = "crates/mcp" }
|
mcp = { path = "crates/mcp" }
|
||||||
lint-common = { path = "crates/lint-common" }
|
lint-common = { path = "crates/lint-common" }
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "agen-macros"
|
||||||
|
description = "Procedural macros for declaring agen tools"
|
||||||
|
version = "0.2.0"
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version = "1.85"
|
||||||
|
license.workspace = true
|
||||||
|
readme = "README.md"
|
||||||
|
repository = "https://gitea.hareworks.net/Hare/yoi"
|
||||||
|
homepage = "https://gitea.hareworks.net/Hare/yoi"
|
||||||
|
documentation = "https://docs.rs/agen-macros"
|
||||||
|
keywords = ["llm", "agent", "tools", "macros"]
|
||||||
|
categories = ["development-tools::procedural-macro-helpers"]
|
||||||
|
include = ["src/**", "README.md", "LICENSE"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
proc-macro2 = "1"
|
||||||
|
quote = "1"
|
||||||
|
syn = { version = "2", features = ["full"] }
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Copyright 2026 Hare
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# agen-macros
|
||||||
|
|
||||||
|
Procedural macros used by [`agen`](https://crates.io/crates/agen) to declare LLM tools from Rust methods.
|
||||||
|
|
||||||
|
Applications should normally depend only on `agen` and import its re-exports:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use agen::tool_registry;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Tools;
|
||||||
|
|
||||||
|
#[tool_registry]
|
||||||
|
impl Tools {
|
||||||
|
/// Returns the supplied text.
|
||||||
|
#[tool]
|
||||||
|
async fn echo(
|
||||||
|
&self,
|
||||||
|
#[description = "Text to return"] text: String,
|
||||||
|
) -> Result<String, std::io::Error> {
|
||||||
|
Ok(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`#[tool_registry]` generates the argument schema, a `Tool` implementation, and an `<method>_definition` constructor. It rejects arguments of its own, duplicate `#[tool]` markers, malformed or duplicate `#[description = "..."]` attributes, and non-identifier argument patterns.
|
||||||
|
|
||||||
|
Generated code targets the canonical `::agen` path and uses implementation dependencies re-exported by `agen`; consumers do not need direct `serde`, `schemars`, `serde_json`, or `async-trait` dependencies. Renaming the `agen` dependency in `Cargo.toml` is not currently supported.
|
||||||
|
|
||||||
|
This companion package is published before the matching `agen` release. Its public contract is the generated API consumed by `agen`, and its minor version compatibility follows the `agen` 0.2 series.
|
||||||
|
|
||||||
|
Licensed under the [MIT License](https://gitea.hareworks.net/Hare/yoi/src/branch/develop/LICENSE).
|
||||||
@@ -0,0 +1,482 @@
|
|||||||
|
//! Procedural macros for declaring [`agen`](https://docs.rs/agen) tools.
|
||||||
|
//!
|
||||||
|
//! [`tool_registry`] expands methods marked with `#[tool]` into `agen::tool::Tool`
|
||||||
|
//! implementations and tool definitions. Applications normally use the re-exports from
|
||||||
|
//! `agen`; this companion crate exists so those macros can be published and versioned
|
||||||
|
//! independently.
|
||||||
|
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use syn::{
|
||||||
|
Attribute, FnArg, ImplItem, ItemImpl, Lit, Meta, Pat, ReturnType, Type, parse_macro_input,
|
||||||
|
spanned::Spanned,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Generates tools for methods marked with `#[tool]` in an `impl` block.
|
||||||
|
///
|
||||||
|
/// Method doc comments become the tool description. An argument can use
|
||||||
|
/// `#[description = "..."]` to supply its JSON Schema description.
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// #[derive(Clone)]
|
||||||
|
/// struct MyApp;
|
||||||
|
///
|
||||||
|
/// #[agen::tool_registry]
|
||||||
|
/// impl MyApp {
|
||||||
|
/// /// Retrieves a user by ID.
|
||||||
|
/// #[tool]
|
||||||
|
/// async fn get_user(
|
||||||
|
/// &self,
|
||||||
|
/// #[description = "The user ID"] user_id: String,
|
||||||
|
/// ) -> Result<String, std::io::Error> {
|
||||||
|
/// todo!()
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This generates a `ToolGetUser` wrapper, a `GetUserArgs` schema type, and
|
||||||
|
/// `MyApp::get_user_definition()`.
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn tool_registry(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
let attr = proc_macro2::TokenStream::from(attr);
|
||||||
|
let impl_block = parse_macro_input!(item as ItemImpl);
|
||||||
|
|
||||||
|
expand_tool_registry(attr, impl_block)
|
||||||
|
.unwrap_or_else(syn::Error::into_compile_error)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_tool_registry(
|
||||||
|
attr: proc_macro2::TokenStream,
|
||||||
|
mut impl_block: ItemImpl,
|
||||||
|
) -> syn::Result<proc_macro2::TokenStream> {
|
||||||
|
if !attr.is_empty() {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
attr.span(),
|
||||||
|
"tool_registry does not accept arguments",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let self_ty = impl_block.self_ty.as_ref().clone();
|
||||||
|
let mut generated_items = Vec::new();
|
||||||
|
|
||||||
|
for item in &mut impl_block.items {
|
||||||
|
let ImplItem::Fn(method) = item else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let tool_attrs: Vec<_> = method
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
|
.filter(|attr| attr.path().is_ident("tool"))
|
||||||
|
.collect();
|
||||||
|
if tool_attrs.len() > 1 {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
tool_attrs[1],
|
||||||
|
"duplicate #[tool] attribute",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let Some(tool_attr) = tool_attrs.first() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !matches!(tool_attr.meta, Meta::Path(_)) {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
tool_attr,
|
||||||
|
"#[tool] does not accept arguments",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
method.attrs.retain(|attr| !attr.path().is_ident("tool"));
|
||||||
|
generated_items.push(generate_tool_impl(&self_ty, method)?);
|
||||||
|
|
||||||
|
for input in &mut method.sig.inputs {
|
||||||
|
if let FnArg::Typed(pat_type) = input {
|
||||||
|
pat_type
|
||||||
|
.attrs
|
||||||
|
.retain(|attr| !attr.path().is_ident("description"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
#impl_block
|
||||||
|
|
||||||
|
#(#generated_items)*
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_doc_comment(attrs: &[Attribute]) -> String {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
|
||||||
|
for attr in attrs {
|
||||||
|
if attr.path().is_ident("doc")
|
||||||
|
&& let Meta::NameValue(meta) = &attr.meta
|
||||||
|
&& let syn::Expr::Lit(expr_lit) = &meta.value
|
||||||
|
&& let Lit::Str(lit_str) = &expr_lit.lit
|
||||||
|
{
|
||||||
|
let line = lit_str.value();
|
||||||
|
let trimmed = line.strip_prefix(' ').unwrap_or(&line);
|
||||||
|
lines.push(trimmed.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_description_attr(attrs: &[Attribute]) -> syn::Result<Option<String>> {
|
||||||
|
let mut description = None;
|
||||||
|
|
||||||
|
for attr in attrs
|
||||||
|
.iter()
|
||||||
|
.filter(|attr| attr.path().is_ident("description"))
|
||||||
|
{
|
||||||
|
let value = match &attr.meta {
|
||||||
|
Meta::NameValue(meta) => match &meta.value {
|
||||||
|
syn::Expr::Lit(expr_lit) => match &expr_lit.lit {
|
||||||
|
Lit::Str(value) => value.value(),
|
||||||
|
_ => {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
attr,
|
||||||
|
"description must be a string literal",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
attr,
|
||||||
|
"description must be a string literal",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
attr,
|
||||||
|
"expected #[description = \"...\"]",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if description.replace(value).is_some() {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
attr,
|
||||||
|
"duplicate #[description] attribute",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(description)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn argument_ident(pat: &Pat) -> syn::Result<&syn::Ident> {
|
||||||
|
match pat {
|
||||||
|
Pat::Ident(pat_ident) => Ok(&pat_ident.ident),
|
||||||
|
_ => Err(syn::Error::new_spanned(
|
||||||
|
pat,
|
||||||
|
"tool arguments must use simple identifier patterns",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_tool_execution_context_type(ty: &Type) -> bool {
|
||||||
|
let Type::Path(path) = ty else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
path.path
|
||||||
|
.segments
|
||||||
|
.last()
|
||||||
|
.is_some_and(|segment| segment.ident == "ToolExecutionContext")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_tool_impl(
|
||||||
|
self_ty: &Type,
|
||||||
|
method: &syn::ImplItemFn,
|
||||||
|
) -> syn::Result<proc_macro2::TokenStream> {
|
||||||
|
let sig = &method.sig;
|
||||||
|
let method_name = &sig.ident;
|
||||||
|
let tool_name = method_name.to_string();
|
||||||
|
|
||||||
|
let pascal_name = to_pascal_case(&method_name.to_string());
|
||||||
|
let tool_struct_name = format_ident!("Tool{}", pascal_name);
|
||||||
|
let args_struct_name = format_ident!("{}Args", pascal_name);
|
||||||
|
let definition_name = format_ident!("{}_definition", method_name);
|
||||||
|
|
||||||
|
let description = extract_doc_comment(&method.attrs);
|
||||||
|
let description = if description.is_empty() {
|
||||||
|
format!("Tool: {}", tool_name)
|
||||||
|
} else {
|
||||||
|
description
|
||||||
|
};
|
||||||
|
|
||||||
|
let method_args: Vec<_> = sig
|
||||||
|
.inputs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|arg| match arg {
|
||||||
|
FnArg::Typed(pat_type) => Some(pat_type),
|
||||||
|
FnArg::Receiver(_) => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let json_args: Vec<_> = method_args
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|pat_type| !is_tool_execution_context_type(pat_type.ty.as_ref()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let arg_fields: Vec<_> = json_args
|
||||||
|
.iter()
|
||||||
|
.map(|pat_type| {
|
||||||
|
let field_name = argument_ident(pat_type.pat.as_ref())?;
|
||||||
|
let ty = &pat_type.ty;
|
||||||
|
let description = extract_description_attr(&pat_type.attrs)?;
|
||||||
|
|
||||||
|
Ok(if let Some(description) = description {
|
||||||
|
quote! {
|
||||||
|
#[schemars(description = #description)]
|
||||||
|
pub #field_name: #ty
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
pub #field_name: #ty
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<syn::Result<_>>()?;
|
||||||
|
|
||||||
|
let call_args: Vec<_> = method_args
|
||||||
|
.iter()
|
||||||
|
.map(|pat_type| {
|
||||||
|
if is_tool_execution_context_type(pat_type.ty.as_ref()) {
|
||||||
|
Ok(quote! { ctx.clone() })
|
||||||
|
} else {
|
||||||
|
let ident = argument_ident(pat_type.pat.as_ref())?;
|
||||||
|
Ok(quote! { args.#ident })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<syn::Result<_>>()?;
|
||||||
|
let method_call = if call_args.is_empty() {
|
||||||
|
quote! { self.ctx.#method_name() }
|
||||||
|
} else {
|
||||||
|
quote! { self.ctx.#method_name(#(#call_args),*) }
|
||||||
|
};
|
||||||
|
|
||||||
|
let awaiter = if sig.asyncness.is_some() {
|
||||||
|
quote! { .await }
|
||||||
|
} else {
|
||||||
|
quote! {}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result_handling = if is_result_type(&sig.output) {
|
||||||
|
quote! {
|
||||||
|
match result {
|
||||||
|
Ok(val) => Ok(format!("{:?}", val).into()),
|
||||||
|
Err(error) => Err(::agen::tool::ToolError::ExecutionFailed(format!("{}", error))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
Ok(format!("{:?}", result).into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let args_struct_def = quote! {
|
||||||
|
#[derive(
|
||||||
|
::agen::__private::serde::Deserialize,
|
||||||
|
::agen::__private::schemars::JsonSchema,
|
||||||
|
)]
|
||||||
|
#[serde(crate = "::agen::__private::serde")]
|
||||||
|
#[schemars(crate = "::agen::__private::schemars")]
|
||||||
|
struct #args_struct_name {
|
||||||
|
#(#arg_fields),*
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let execute_body = if json_args.is_empty() {
|
||||||
|
quote! {
|
||||||
|
let _: #args_struct_name = ::agen::__private::serde_json::from_str(input_json)
|
||||||
|
.unwrap_or(#args_struct_name {});
|
||||||
|
|
||||||
|
let result = #method_call #awaiter;
|
||||||
|
#result_handling
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
let args: #args_struct_name = ::agen::__private::serde_json::from_str(input_json)
|
||||||
|
.map_err(|error| ::agen::tool::ToolError::InvalidArgument(error.to_string()))?;
|
||||||
|
|
||||||
|
let result = #method_call #awaiter;
|
||||||
|
#result_handling
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
#args_struct_def
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct #tool_struct_name {
|
||||||
|
ctx: #self_ty,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[::agen::__private::async_trait::async_trait]
|
||||||
|
impl ::agen::tool::Tool for #tool_struct_name {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
input_json: &str,
|
||||||
|
ctx: ::agen::tool::ToolExecutionContext,
|
||||||
|
) -> Result<::agen::tool::ToolOutput, ::agen::tool::ToolError> {
|
||||||
|
let _ = &ctx;
|
||||||
|
#execute_body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #self_ty {
|
||||||
|
/// Returns a tool definition for registration with an `agen::Engine`.
|
||||||
|
pub fn #definition_name(&self) -> ::agen::tool::ToolDefinition {
|
||||||
|
let ctx = self.clone();
|
||||||
|
::std::sync::Arc::new(move || {
|
||||||
|
let schema = ::agen::__private::schemars::schema_for!(#args_struct_name);
|
||||||
|
let meta = ::agen::tool::ToolMeta::new(#tool_name)
|
||||||
|
.description(#description)
|
||||||
|
.input_schema(
|
||||||
|
::agen::__private::serde_json::to_value(schema)
|
||||||
|
.unwrap_or_else(|_| ::agen::__private::serde_json::json!({})),
|
||||||
|
);
|
||||||
|
let tool: ::std::sync::Arc<dyn ::agen::tool::Tool> =
|
||||||
|
::std::sync::Arc::new(#tool_struct_name { ctx: ctx.clone() });
|
||||||
|
(meta, tool)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_result_type(return_type: &ReturnType) -> bool {
|
||||||
|
match return_type {
|
||||||
|
ReturnType::Default => false,
|
||||||
|
ReturnType::Type(_, ty) => {
|
||||||
|
if let Type::Path(type_path) = ty.as_ref()
|
||||||
|
&& let Some(segment) = type_path.path.segments.last()
|
||||||
|
{
|
||||||
|
return segment.ident == "Result";
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_pascal_case(s: &str) -> String {
|
||||||
|
s.split('_')
|
||||||
|
.map(|part| {
|
||||||
|
let mut chars = part.chars();
|
||||||
|
match chars.next() {
|
||||||
|
None => String::new(),
|
||||||
|
Some(first) => first.to_uppercase().chain(chars).collect(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marker attribute interpreted by [`tool_registry`].
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn tool(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
marker_attribute("tool", attr, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Argument description marker interpreted by [`tool_registry`].
|
||||||
|
///
|
||||||
|
/// Use it as `#[description = "The argument description"]` on a tool method argument.
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn description(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
marker_attribute("description", attr, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn marker_attribute(name: &str, attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
if attr.is_empty() {
|
||||||
|
item
|
||||||
|
} else {
|
||||||
|
syn::Error::new(
|
||||||
|
proc_macro2::Span::call_site(),
|
||||||
|
format!("{name} is a marker interpreted by #[tool_registry]"),
|
||||||
|
)
|
||||||
|
.into_compile_error()
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::parse_quote;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_tool_registry_arguments() {
|
||||||
|
let implementation: ItemImpl = parse_quote!(impl Registry {});
|
||||||
|
let error = expand_tool_registry(quote!(unexpected), implementation).unwrap_err();
|
||||||
|
|
||||||
|
assert!(error.to_string().contains("does not accept arguments"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_duplicate_tool_markers() {
|
||||||
|
let implementation: ItemImpl = parse_quote! {
|
||||||
|
impl Registry {
|
||||||
|
#[tool]
|
||||||
|
#[tool]
|
||||||
|
fn inspect(&self) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let error = expand_tool_registry(quote!(), implementation).unwrap_err();
|
||||||
|
|
||||||
|
assert!(error.to_string().contains("duplicate #[tool]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_invalid_description_attributes() {
|
||||||
|
let implementation: ItemImpl = parse_quote! {
|
||||||
|
impl Registry {
|
||||||
|
#[tool]
|
||||||
|
fn inspect(&self, #[description] input: String) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let error = expand_tool_registry(quote!(), implementation).unwrap_err();
|
||||||
|
|
||||||
|
assert!(error.to_string().contains("expected #[description"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_duplicate_description_attributes() {
|
||||||
|
let implementation: ItemImpl = parse_quote! {
|
||||||
|
impl Registry {
|
||||||
|
#[tool]
|
||||||
|
fn inspect(
|
||||||
|
&self,
|
||||||
|
#[description = "first"]
|
||||||
|
#[description = "second"]
|
||||||
|
input: String,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let error = expand_tool_registry(quote!(), implementation).unwrap_err();
|
||||||
|
|
||||||
|
assert!(error.to_string().contains("duplicate #[description]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generated_code_uses_only_agen_runtime_paths() {
|
||||||
|
let implementation: ItemImpl = parse_quote! {
|
||||||
|
impl Registry {
|
||||||
|
#[tool]
|
||||||
|
fn inspect(&self, input: String) -> Result<String, Error> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let expanded = expand_tool_registry(quote!(), implementation)
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
assert!(expanded.contains(":: agen :: tool :: Tool"));
|
||||||
|
assert!(expanded.contains(":: agen :: __private :: serde_json"));
|
||||||
|
assert!(expanded.contains(":: agen :: __private :: serde"));
|
||||||
|
assert!(expanded.contains(":: agen :: __private :: schemars"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,18 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "llm-engine"
|
name = "agen"
|
||||||
description = "A library for building autonomous LLM-powered systems"
|
description = "Provider-neutral orchestration for tool-using LLM applications"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
rust-version = "1.86"
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
readme = "README.md"
|
||||||
|
repository = "https://gitea.hareworks.net/Hare/yoi"
|
||||||
|
homepage = "https://gitea.hareworks.net/Hare/yoi"
|
||||||
|
documentation = "https://docs.rs/agen"
|
||||||
|
keywords = ["llm", "agent", "tools", "streaming", "orchestration"]
|
||||||
|
categories = ["api-bindings", "asynchronous"]
|
||||||
|
include = ["src/**", "tests/**", "examples/*.rs", "docs/**", "README.md", "LICENSE"]
|
||||||
|
autoexamples = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
@@ -12,6 +21,7 @@ codex = ["dep:chrono"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
schemars = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
@@ -23,13 +33,23 @@ eventsource-stream = "0.2"
|
|||||||
zstd = "0.13"
|
zstd = "0.13"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
chrono = { version = "0.4", default-features = false, features = ["serde", "clock"], optional = true }
|
chrono = { version = "0.4", default-features = false, features = ["serde", "clock"], optional = true }
|
||||||
llm-engine-macros = { workspace = true }
|
agen-macros = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
clap = { version = "4.5", features = ["derive", "env"] }
|
clap = { version = "4.5", features = ["derive", "env"] }
|
||||||
schemars = { workspace = true }
|
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
trybuild = "1.0.116"
|
trybuild = "1.0.116"
|
||||||
wiremock = "0.6.5"
|
wiremock = "0.6.5"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "engine_cancel_demo"
|
||||||
|
path = "examples/engine_cancel_demo.rs"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "engine_cli"
|
||||||
|
path = "examples/engine_cli.rs"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Copyright 2026 Hare
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# agen
|
||||||
|
|
||||||
|
`agen` is a provider-neutral Rust engine for streaming LLM applications that use tools. It owns the turn loop, typed conversation history, provider wire-format adapters, tool execution, interceptors, usage accounting, and cache-aware state transitions.
|
||||||
|
|
||||||
|
> `agen` is pre-1.0. Public APIs may change between minor releases.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
agen = "0.2.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
The default feature set is intentionally empty. Enable the experimental Codex/ChatGPT authentication adapter when needed:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
agen = { version = "0.2.1", features = ["codex"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
`agen` requires Rust 1.86 or newer. The companion `agen-macros` package requires Rust 1.85 or newer.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
Supply an implementation of [`LlmClient`](https://docs.rs/agen/latest/agen/llm_client/trait.LlmClient.html), then run a turn. The first call consumes the mutable engine and returns a cache-locked engine for later turns.
|
||||||
|
|
||||||
|
```no_run
|
||||||
|
use agen::{Engine, EngineError};
|
||||||
|
use agen::llm_client::LlmClient;
|
||||||
|
|
||||||
|
async fn conversation<C: LlmClient>(client: C) -> Result<(), EngineError> {
|
||||||
|
let output = Engine::new(client)
|
||||||
|
.system_prompt("You are a concise assistant.")
|
||||||
|
.run("Explain typed state in one sentence.")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut engine = output.engine;
|
||||||
|
let _result = engine.run("Give a Rust example.").await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Declaring tools
|
||||||
|
|
||||||
|
The tool macros are re-exported by `agen`; applications do not need direct dependencies on `serde`, `schemars`, `serde_json`, or `async-trait` for generated code.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use agen::tool_registry;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Tools;
|
||||||
|
|
||||||
|
#[tool_registry]
|
||||||
|
impl Tools {
|
||||||
|
/// Returns the supplied text.
|
||||||
|
#[tool]
|
||||||
|
async fn echo(
|
||||||
|
&self,
|
||||||
|
#[description = "Text to return"] text: String,
|
||||||
|
) -> Result<String, std::io::Error> {
|
||||||
|
Ok(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let definition = Tools.echo_definition();
|
||||||
|
assert_eq!(definition().0.name, "echo");
|
||||||
|
```
|
||||||
|
|
||||||
|
The generated API uses the canonical crate name `agen`. Renaming the `agen` dependency in `Cargo.toml` is not currently supported by these macros.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
| Feature | Default | Adds |
|
||||||
|
|---|---:|---|
|
||||||
|
| `codex` | No | Experimental Codex/ChatGPT auth-file loading and token refresh support |
|
||||||
|
|
||||||
|
The base crate includes provider-neutral transport and Anthropic, OpenAI-compatible, Gemini, and Ollama wire-format schemes. See [`llm_client`](https://docs.rs/agen/latest/agen/llm_client/) for the client boundary.
|
||||||
|
|
||||||
|
## Architecture and API scope
|
||||||
|
|
||||||
|
The current public modules cover the engine, typed history, client transport/schemes, timeline events, tools, interceptors, pruning, token estimation, and usage records. Their relationships are described in [Architecture](https://gitea.hareworks.net/Hare/yoi/src/branch/develop/crates/agen/docs/architecture.md); behavioral requirements are summarized in [Requirements](https://gitea.hareworks.net/Hare/yoi/src/branch/develop/crates/agen/docs/requirements.md).
|
||||||
|
|
||||||
|
Low-level modules remain public in the 0.2 series because downstream Yoi components implement custom clients, event handlers, pruning policies, and tool registries against them. This surface is versioned as pre-1.0 API rather than declared stable.
|
||||||
|
|
||||||
|
## Packaging and security
|
||||||
|
|
||||||
|
The published package contains source, public documentation, curated examples, and deterministic tests/fixtures. Credentialed fixture-recording utilities are intentionally excluded. Examples that contact a provider read credentials from environment variables and never embed production credentials.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under the [MIT License](https://gitea.hareworks.net/Hare/yoi/src/branch/develop/LICENSE).
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# agen architecture
|
||||||
|
|
||||||
|
`agen` separates orchestration, event projection, and provider transport so applications can replace an LLM client without changing the turn loop or tool model.
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌────────────────────────────────────────────┐
|
||||||
|
│ Engine │
|
||||||
|
│ turn loop · interceptors · tool execution │
|
||||||
|
│ typed state: Mutable → Locked → Mutable │
|
||||||
|
└─────────────────────┬──────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────────▼──────────────────────┐
|
||||||
|
│ Timeline │
|
||||||
|
│ event dispatch · block collectors │
|
||||||
|
└─────────────────────┬──────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────────▼──────────────────────┐
|
||||||
|
│ LlmClient │
|
||||||
|
│ transport · provider wire-format schemes │
|
||||||
|
└────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Main modules
|
||||||
|
|
||||||
|
| Module | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `engine` | Turn execution, pause/resume, retries, tool integration, and callbacks |
|
||||||
|
| `state` | Sealed `Mutable` and `Locked` type-state markers |
|
||||||
|
| `interceptor` | Application-owned control decisions at orchestration boundaries |
|
||||||
|
| `tool` / `tool_server` | Tool metadata, registration, execution, and bounded output |
|
||||||
|
| `timeline` | Streaming event dispatch, handlers, and block assembly |
|
||||||
|
| `llm_client` | Provider-neutral request, response, auth, transport, and scheme contracts |
|
||||||
|
| `providers` | Optional higher-level provider adapters such as the `codex` feature |
|
||||||
|
| `prune` / `token_counter` | Cache-aware history reduction and token estimation |
|
||||||
|
| `usage_record` | Request and token usage accounting |
|
||||||
|
|
||||||
|
## Request flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Engine history
|
||||||
|
→ provider-neutral Request
|
||||||
|
→ Scheme::build_request
|
||||||
|
→ Provider transport
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
streaming response bytes
|
||||||
|
→ Scheme event parsing
|
||||||
|
→ unified Event values
|
||||||
|
→ Timeline handlers and collectors
|
||||||
|
→ Engine history/tool decisions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type state and cache protection
|
||||||
|
|
||||||
|
`Engine<C, Mutable>` permits configuration and history editing. `Engine::run` or `Engine::lock` commits the current prefix and produces `Engine<C, Locked>`. The locked engine may append turns without mutating the committed prefix. `Engine::unlock` explicitly returns to mutable state when an application accepts losing that cache guarantee.
|
||||||
|
|
||||||
|
## Public surface
|
||||||
|
|
||||||
|
The 0.2 series exposes the low-level client, timeline, tool, pruning, and usage modules because custom clients and orchestration hosts build directly on them. These APIs are intentionally provider-neutral but remain pre-1.0 and may change in later minor releases.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# agen requirements
|
||||||
|
|
||||||
|
## R1: Turn execution and continuation
|
||||||
|
|
||||||
|
- `Engine::run` starts a turn and loops through provider output and tool calls.
|
||||||
|
- An `Interceptor` may continue, cancel, or pause work at defined orchestration boundaries.
|
||||||
|
- `Engine::resume` continues paused generation without fabricating another user message.
|
||||||
|
- Cancellation and provider errors are represented as typed `EngineError` values.
|
||||||
|
|
||||||
|
## R2: Explicit cache-preserving state
|
||||||
|
|
||||||
|
- `Engine<C, Mutable>` permits configuration and history edits.
|
||||||
|
- `Engine::run` or `Engine::lock` transitions to `Engine<C, Locked>` and records the committed prefix.
|
||||||
|
- A locked engine appends turns but cannot mutate that prefix through mutable-only APIs.
|
||||||
|
- `Engine::unlock` explicitly abandons the lock before configuration or history changes.
|
||||||
|
|
||||||
|
## R3: Tool declarations and execution
|
||||||
|
|
||||||
|
- `#[tool_registry]` generates a schema and `Tool` implementation for methods marked `#[tool]`.
|
||||||
|
- `#[description = "..."]` supplies argument descriptions in generated JSON Schema.
|
||||||
|
- Generated code resolves its runtime and helper dependencies through `::agen`.
|
||||||
|
- Invalid and duplicate marker attributes produce compile errors rather than panics.
|
||||||
|
- Tools execute through `ToolServer` with typed context, errors, and output limits.
|
||||||
|
|
||||||
|
## R4: Provider-neutral orchestration
|
||||||
|
|
||||||
|
- `LlmClient` is the boundary between the engine and provider-specific transport.
|
||||||
|
- Request/response schemes translate provider wire formats into shared request and event types.
|
||||||
|
- Interceptors, tool execution, timeline collection, and pruning stay above the provider transport.
|
||||||
|
- Provider-specific capabilities are optional features when they require additional policy or dependencies.
|
||||||
|
|
||||||
|
## R5: Publication quality
|
||||||
|
|
||||||
|
- crates.io metadata includes license, repository, documentation, README, categories, keywords, and MSRV.
|
||||||
|
- The default feature set and each optional feature compile and test independently.
|
||||||
|
- Macro expansion compiles in a downstream-style integration test without direct helper dependencies.
|
||||||
|
- rustdoc builds without dependency documentation.
|
||||||
|
- Package contents are explicitly bounded and exclude credentialed fixture-recording utilities.
|
||||||
|
- `cargo package` and `cargo publish --dry-run` are run for `agen-macros` before `agen` because the main package depends on its companion package.
|
||||||
+3
-3
@@ -2,9 +2,9 @@
|
|||||||
//!
|
//!
|
||||||
//! Example of cancelling from another thread during streaming
|
//! Example of cancelling from another thread during streaming
|
||||||
|
|
||||||
use llm_engine::llm_client::scheme::{Scheme, anthropic::AnthropicScheme};
|
use agen::llm_client::scheme::{Scheme, anthropic::AnthropicScheme};
|
||||||
use llm_engine::llm_client::transport::{HttpTransport, ResolvedAuth};
|
use agen::llm_client::transport::{HttpTransport, ResolvedAuth};
|
||||||
use llm_engine::{Engine, EngineResult};
|
use agen::{Engine, EngineResult};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -38,8 +38,7 @@ use async_trait::async_trait;
|
|||||||
use tracing::info;
|
use tracing::info;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
use clap::{Parser, ValueEnum};
|
use agen::{
|
||||||
use llm_engine::{
|
|
||||||
Engine,
|
Engine,
|
||||||
interceptor::{Interceptor, PostToolAction, ToolResultInfo},
|
interceptor::{Interceptor, PostToolAction, ToolResultInfo},
|
||||||
llm_client::{
|
llm_client::{
|
||||||
@@ -51,12 +50,9 @@ use llm_engine::{
|
|||||||
transport::{HttpTransport, ResolvedAuth},
|
transport::{HttpTransport, ResolvedAuth},
|
||||||
},
|
},
|
||||||
timeline::{Handler, TextBlockEvent, TextBlockKind, ToolUseBlockEvent, ToolUseBlockKind},
|
timeline::{Handler, TextBlockEvent, TextBlockKind, ToolUseBlockEvent, ToolUseBlockKind},
|
||||||
|
tool_registry,
|
||||||
};
|
};
|
||||||
use llm_engine_macros::tool_registry;
|
use clap::{Parser, ValueEnum};
|
||||||
|
|
||||||
// Required imports for macro expansion
|
|
||||||
use schemars;
|
|
||||||
use serde;
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Provider Definition
|
// Provider Definition
|
||||||
+3
-3
@@ -19,11 +19,11 @@
|
|||||||
mod recorder;
|
mod recorder;
|
||||||
mod scenarios;
|
mod scenarios;
|
||||||
|
|
||||||
use clap::{Parser, ValueEnum};
|
use agen::llm_client::scheme::{
|
||||||
use llm_engine::llm_client::scheme::{
|
|
||||||
Scheme, anthropic::AnthropicScheme, gemini::GeminiScheme, openai_chat::OpenAIScheme,
|
Scheme, anthropic::AnthropicScheme, gemini::GeminiScheme, openai_chat::OpenAIScheme,
|
||||||
};
|
};
|
||||||
use llm_engine::llm_client::transport::{HttpTransport, ResolvedAuth};
|
use agen::llm_client::transport::{HttpTransport, ResolvedAuth};
|
||||||
|
use clap::{Parser, ValueEnum};
|
||||||
|
|
||||||
fn make_transport<S: Scheme>(scheme: S, model: &str, auth: ResolvedAuth) -> HttpTransport<S> {
|
fn make_transport<S: Scheme>(scheme: S, model: &str, auth: ResolvedAuth) -> HttpTransport<S> {
|
||||||
let cap = scheme.default_capability();
|
let cap = scheme.default_capability();
|
||||||
+1
-1
@@ -7,8 +7,8 @@ use std::io::{BufWriter, Write};
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::{Instant, SystemTime, UNIX_EPOCH};
|
use std::time::{Instant, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use agen::llm_client::{LlmClient, Request};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use llm_engine::llm_client::{LlmClient, Request};
|
|
||||||
|
|
||||||
/// Recorded event
|
/// Recorded event
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
//!
|
//!
|
||||||
//! Defines requests and output file names for each scenario
|
//! Defines requests and output file names for each scenario
|
||||||
|
|
||||||
use llm_engine::llm_client::{Request, ToolDefinition};
|
use agen::llm_client::{Request, ToolDefinition};
|
||||||
|
|
||||||
/// Test scenario
|
/// Test scenario
|
||||||
pub struct TestScenario {
|
pub struct TestScenario {
|
||||||
@@ -88,7 +88,7 @@ pub enum EngineResult {
|
|||||||
Yielded,
|
Yielded,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result of [`Engine<C, Mutable>::run()`] / [`Engine<C, Mutable>::resume()`].
|
/// Result of [`Engine::run`] or [`Engine::resume`].
|
||||||
///
|
///
|
||||||
/// Contains the `Locked` Engine (ready for subsequent runs) and the outcome.
|
/// Contains the `Locked` Engine (ready for subsequent runs) and the outcome.
|
||||||
pub struct EngineRunOutput<C: LlmClient> {
|
pub struct EngineRunOutput<C: LlmClient> {
|
||||||
@@ -181,7 +181,7 @@ pub struct Engine<C: LlmClient, S: EngineState = Mutable> {
|
|||||||
locked_prefix_len: usize,
|
locked_prefix_len: usize,
|
||||||
/// AgentTurn count.
|
/// AgentTurn count.
|
||||||
///
|
///
|
||||||
/// Once retry (`llm-engine-stream-continuation`) is implemented, an
|
/// Once retry (`agen-stream-continuation`) is implemented, an
|
||||||
/// AgentTurn collapses N retried `LlmCall`s with identical input;
|
/// AgentTurn collapses N retried `LlmCall`s with identical input;
|
||||||
/// today retry is not implemented so AgentTurn and LlmCall fire 1:1
|
/// today retry is not implemented so AgentTurn and LlmCall fire 1:1
|
||||||
/// and the increment site (the LLM-call loop) is shared.
|
/// and the increment site (the LLM-call loop) is shared.
|
||||||
@@ -1242,7 +1242,7 @@ impl<C: LlmClient, S: EngineState> Engine<C, S> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LlmCall boundary fires per LLM generation request — today
|
// LlmCall boundary fires per LLM generation request — today
|
||||||
// 1:1 with AgentTurn, but retry (`llm-engine-stream-continuation`)
|
// 1:1 with AgentTurn, but retry (`agen-stream-continuation`)
|
||||||
// will multiply this within a single AgentTurn.
|
// will multiply this within a single AgentTurn.
|
||||||
let current_llm_call = self.llm_call_count;
|
let current_llm_call = self.llm_call_count;
|
||||||
for cb in &self.llm_call_start_cbs {
|
for cb in &self.llm_call_start_cbs {
|
||||||
@@ -1696,8 +1696,7 @@ impl<C: LlmClient> Engine<C, Mutable> {
|
|||||||
/// Register a tool factory for deferred initialization.
|
/// Register a tool factory for deferred initialization.
|
||||||
///
|
///
|
||||||
/// The factory is queued and executed at the next `run()` or `resume()` call.
|
/// The factory is queued and executed at the next `run()` or `resume()` call.
|
||||||
/// Duplicate name detection occurs at that point and surfaces as
|
/// Duplicate name detection occurs when pending tools are flushed before that call.
|
||||||
/// [`EngineError::ToolRegistry`].
|
|
||||||
pub fn register_tool(&mut self, factory: EngineToolDefinition) {
|
pub fn register_tool(&mut self, factory: EngineToolDefinition) {
|
||||||
self.tool_server.register_tool(factory);
|
self.tool_server.register_tool(factory);
|
||||||
}
|
}
|
||||||
@@ -1880,7 +1879,7 @@ impl<C: LlmClient> Engine<C, Mutable> {
|
|||||||
/// This is the primary entry point for first use. Equivalent to
|
/// This is the primary entry point for first use. Equivalent to
|
||||||
/// `self.lock()` followed by `locked.run(user_input)`.
|
/// `self.lock()` followed by `locked.run(user_input)`.
|
||||||
///
|
///
|
||||||
/// Subsequent runs can use [`Engine<C, Locked>::run()`] directly.
|
/// Subsequent runs can call [`Engine::run`] directly.
|
||||||
/// To edit state between turns, call [`unlock()`](Engine::unlock) first.
|
/// To edit state between turns, call [`unlock()`](Engine::unlock) first.
|
||||||
pub async fn run(
|
pub async fn run(
|
||||||
self,
|
self,
|
||||||
@@ -32,7 +32,7 @@ pub trait Kind {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// use llm_engine::timeline::{Handler, TextBlockEvent, TextBlockKind};
|
/// use agen::timeline::{Handler, TextBlockEvent, TextBlockKind};
|
||||||
///
|
///
|
||||||
/// struct TextCollector {
|
/// struct TextCollector {
|
||||||
/// texts: Vec<String>,
|
/// texts: Vec<String>,
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
|
mod engine;
|
||||||
|
mod handler;
|
||||||
|
mod message;
|
||||||
|
|
||||||
|
pub(crate) mod callback;
|
||||||
|
pub mod event;
|
||||||
|
pub mod interceptor;
|
||||||
|
pub mod llm_client;
|
||||||
|
pub mod providers;
|
||||||
|
pub mod prune;
|
||||||
|
pub mod state;
|
||||||
|
pub mod timeline;
|
||||||
|
pub mod token_counter;
|
||||||
|
pub mod tool;
|
||||||
|
pub mod tool_server;
|
||||||
|
pub mod usage_record;
|
||||||
|
|
||||||
|
pub use agen_macros::{description, tool, tool_registry};
|
||||||
|
pub use callback::{TextBlockScope, ThinkingBlockScope, ToolUseBlockScope};
|
||||||
|
pub use engine::{
|
||||||
|
Engine, EngineConfig, EngineError, EngineResult, EngineRunOutput, LlmRetryNotice,
|
||||||
|
ToolRegistryError,
|
||||||
|
};
|
||||||
|
pub use handler::ToolUseBlockStart;
|
||||||
|
pub use interceptor::Interceptor;
|
||||||
|
pub use message::{ContentPart, Item, Message, Role};
|
||||||
|
pub use tool::{ToolCall, ToolExecutionContext, ToolOutputLimits, ToolResult};
|
||||||
|
pub use usage_record::UsageRecord;
|
||||||
|
|
||||||
|
/// Implementation dependencies used by code generated from `agen` macros.
|
||||||
|
///
|
||||||
|
/// This module is not a stable user-facing API. It is public only because macro expansion
|
||||||
|
/// happens in the downstream crate.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub mod __private {
|
||||||
|
pub use async_trait;
|
||||||
|
pub use schemars;
|
||||||
|
pub use serde;
|
||||||
|
pub use serde_json;
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ pub enum AuthRequirement {
|
|||||||
/// リクエスト毎に認証ヘッダを動的に組み立てるプロバイダ。
|
/// リクエスト毎に認証ヘッダを動的に組み立てるプロバイダ。
|
||||||
///
|
///
|
||||||
/// access token が refresh で更新されたり、複数ヘッダを同時に注入する
|
/// access token が refresh で更新されたり、複数ヘッダを同時に注入する
|
||||||
/// 必要があるケースで使う。実体は呼び出し側に置き、llm-engine は
|
/// 必要があるケースで使う。実体は呼び出し側に置き、agen は
|
||||||
/// trait を知るだけ。
|
/// trait を知るだけ。
|
||||||
///
|
///
|
||||||
/// 返したヘッダはそのまま `HeaderMap` に挿入される。`Authorization`
|
/// 返したヘッダはそのまま `HeaderMap` に挿入される。`Authorization`
|
||||||
@@ -8,7 +8,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
/// 指数バックオフ + ジッター + 累積タイムアウトを表すポリシー。
|
/// 指数バックオフ + ジッター + 累積タイムアウトを表すポリシー。
|
||||||
///
|
///
|
||||||
/// `Default` は llm-engine 全体の固定値を返す。呼び出し側からの上書きが
|
/// `Default` は agen 全体の固定値を返す。呼び出し側からの上書きが
|
||||||
/// 必要になったら拡張する。
|
/// 必要になったら拡張する。
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RetryPolicy {
|
pub struct RetryPolicy {
|
||||||
+2
-2
@@ -4,8 +4,8 @@
|
|||||||
//! function_call item が first-class で、SSE イベントも `response.*` 名前空間で
|
//! function_call item が first-class で、SSE イベントも `response.*` 名前空間で
|
||||||
//! 流れる。
|
//! 流れる。
|
||||||
//!
|
//!
|
||||||
//! - リクエスト JSON 生成: [`request`]
|
//! - リクエスト JSON 生成: `request`
|
||||||
//! - SSE イベントパース → [`Event`](crate::llm_client::event::Event) 変換: [`events`]
|
//! - SSE イベントパース → [`Event`](crate::llm_client::event::Event) 変換: `events`
|
||||||
|
|
||||||
mod capability;
|
mod capability;
|
||||||
mod events;
|
mod events;
|
||||||
@@ -69,7 +69,7 @@ impl fmt::Debug for RequestTrace {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// use llm_engine::Item;
|
/// use agen::Item;
|
||||||
///
|
///
|
||||||
/// let user = Item::user_message("Hello!");
|
/// let user = Item::user_message("Hello!");
|
||||||
/// let assistant = Item::assistant_message("Hi there!");
|
/// let assistant = Item::assistant_message("Hi there!");
|
||||||
@@ -24,7 +24,7 @@ mod private {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// use llm_engine::Engine;
|
/// use agen::Engine;
|
||||||
///
|
///
|
||||||
/// let mut engine = Engine::new(client)
|
/// let mut engine = Engine::new(client)
|
||||||
/// .system_prompt("You are helpful.");
|
/// .system_prompt("You are helpful.");
|
||||||
@@ -348,7 +348,7 @@ where
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// use llm_engine::{Timeline, Handler, TextBlockKind, TextBlockEvent};
|
/// use agen::{Timeline, Handler, TextBlockKind, TextBlockEvent};
|
||||||
///
|
///
|
||||||
/// struct MyHandler;
|
/// struct MyHandler;
|
||||||
/// impl Handler<TextBlockKind> for MyHandler {
|
/// impl Handler<TextBlockKind> for MyHandler {
|
||||||
@@ -364,7 +364,7 @@ impl Default for ToolExecutionContext {
|
|||||||
/// # Manual Implementation
|
/// # Manual Implementation
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// use llm_engine::tool::{Tool, ToolError, ToolExecutionContext, ToolMeta, ToolDefinition, ToolOutput};
|
/// use agen::tool::{Tool, ToolError, ToolExecutionContext, ToolMeta, ToolDefinition, ToolOutput};
|
||||||
/// use std::sync::Arc;
|
/// use std::sync::Arc;
|
||||||
///
|
///
|
||||||
/// struct MyTool { counter: std::sync::atomic::AtomicUsize }
|
/// struct MyTool { counter: std::sync::atomic::AtomicUsize }
|
||||||
@@ -8,13 +8,13 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use agen::Engine;
|
||||||
|
use agen::llm_client::event::{Event, ResponseStatus, StatusEvent as ClientStatusEvent};
|
||||||
|
use agen::llm_client::retry::RetryPolicy;
|
||||||
|
use agen::llm_client::{ClientError, LlmClient, Request, ResponseStream};
|
||||||
|
use agen::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use common::MockLlmClient;
|
use common::MockLlmClient;
|
||||||
use llm_engine::Engine;
|
|
||||||
use llm_engine::llm_client::event::{Event, ResponseStatus, StatusEvent as ClientStatusEvent};
|
|
||||||
use llm_engine::llm_client::retry::RetryPolicy;
|
|
||||||
use llm_engine::llm_client::{ClientError, LlmClient, Request, ResponseStream};
|
|
||||||
use llm_engine::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct FailOnceClient {
|
struct FailOnceClient {
|
||||||
@@ -221,7 +221,7 @@ impl Tool for FixedOutputTool {
|
|||||||
async fn execute(
|
async fn execute(
|
||||||
&self,
|
&self,
|
||||||
_input_json: &str,
|
_input_json: &str,
|
||||||
_ctx: llm_engine::tool::ToolExecutionContext,
|
_ctx: agen::tool::ToolExecutionContext,
|
||||||
) -> Result<ToolOutput, ToolError> {
|
) -> Result<ToolOutput, ToolError> {
|
||||||
Ok(self.output.clone())
|
Ok(self.output.clone())
|
||||||
}
|
}
|
||||||
@@ -260,6 +260,7 @@ async fn test_callback_tool_result_events() {
|
|||||||
ToolOutput {
|
ToolOutput {
|
||||||
summary: "did the thing".into(),
|
summary: "did the thing".into(),
|
||||||
content: Some("full detail body".into()),
|
content: Some("full detail body".into()),
|
||||||
|
attachments: Vec::new(),
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -296,7 +297,7 @@ impl Tool for ErroringTool {
|
|||||||
async fn execute(
|
async fn execute(
|
||||||
&self,
|
&self,
|
||||||
_input_json: &str,
|
_input_json: &str,
|
||||||
_ctx: llm_engine::tool::ToolExecutionContext,
|
_ctx: agen::tool::ToolExecutionContext,
|
||||||
) -> Result<ToolOutput, ToolError> {
|
) -> Result<ToolOutput, ToolError> {
|
||||||
Err(ToolError::ExecutionFailed(self.message.clone()))
|
Err(ToolError::ExecutionFailed(self.message.clone()))
|
||||||
}
|
}
|
||||||
@@ -6,11 +6,11 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use agen::llm_client::event::{BlockType, DeltaContent, Event};
|
||||||
|
use agen::llm_client::{ClientError, LlmClient, Request};
|
||||||
|
use agen::timeline::{Handler, TextBlockEvent, TextBlockKind, Timeline};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
use llm_engine::llm_client::event::{BlockType, DeltaContent, Event};
|
|
||||||
use llm_engine::llm_client::{ClientError, LlmClient, Request};
|
|
||||||
use llm_engine::timeline::{Handler, TextBlockEvent, TextBlockKind, Timeline};
|
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
@@ -272,7 +272,7 @@ pub fn assert_timeline_integration(subdir: &str) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for event in &events {
|
for event in &events {
|
||||||
let timeline_event: llm_engine::timeline::event::Event = event.clone().into();
|
let timeline_event: agen::timeline::event::Event = event.clone().into();
|
||||||
timeline.dispatch(&timeline_event);
|
timeline.dispatch(&timeline_event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9,10 +9,10 @@ use std::path::Path;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
use agen::Engine;
|
||||||
|
use agen::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use common::MockLlmClient;
|
use common::MockLlmClient;
|
||||||
use llm_engine::Engine;
|
|
||||||
use llm_engine::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
|
||||||
|
|
||||||
/// Fixture directory path
|
/// Fixture directory path
|
||||||
fn fixtures_dir() -> std::path::PathBuf {
|
fn fixtures_dir() -> std::path::PathBuf {
|
||||||
@@ -61,7 +61,7 @@ impl Tool for MockWeatherTool {
|
|||||||
async fn execute(
|
async fn execute(
|
||||||
&self,
|
&self,
|
||||||
input_json: &str,
|
input_json: &str,
|
||||||
_ctx: llm_engine::tool::ToolExecutionContext,
|
_ctx: agen::tool::ToolExecutionContext,
|
||||||
) -> Result<ToolOutput, ToolError> {
|
) -> Result<ToolOutput, ToolError> {
|
||||||
self.call_count.fetch_add(1, Ordering::SeqCst);
|
self.call_count.fetch_add(1, Ordering::SeqCst);
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ fn test_mock_client_from_fixture() {
|
|||||||
/// Creates a client with programmatically constructed events instead of using fixture files.
|
/// Creates a client with programmatically constructed events instead of using fixture files.
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mock_client_from_events() {
|
fn test_mock_client_from_events() {
|
||||||
use llm_engine::llm_client::event::Event;
|
use agen::llm_client::event::Event;
|
||||||
|
|
||||||
// Specify events directly
|
// Specify events directly
|
||||||
let events = vec![
|
let events = vec![
|
||||||
@@ -180,7 +180,7 @@ async fn test_engine_tool_call() {
|
|||||||
/// Useful when test independence is needed and external file dependency should be eliminated.
|
/// Useful when test independence is needed and external file dependency should be eliminated.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_engine_with_programmatic_events() {
|
async fn test_engine_with_programmatic_events() {
|
||||||
use llm_engine::llm_client::event::{Event, ResponseStatus, StatusEvent};
|
use agen::llm_client::event::{Event, ResponseStatus, StatusEvent};
|
||||||
|
|
||||||
// Construct event sequence programmatically
|
// Construct event sequence programmatically
|
||||||
let events = vec![
|
let events = vec![
|
||||||
@@ -208,8 +208,8 @@ async fn test_engine_with_programmatic_events() {
|
|||||||
/// correctly extracts id, name, and input (JSON).
|
/// correctly extracts id, name, and input (JSON).
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_tool_call_collector_integration() {
|
async fn test_tool_call_collector_integration() {
|
||||||
use llm_engine::llm_client::event::Event;
|
use agen::llm_client::event::Event;
|
||||||
use llm_engine::timeline::{Timeline, ToolCallCollector};
|
use agen::timeline::{Timeline, ToolCallCollector};
|
||||||
|
|
||||||
// Event sequence containing ToolUse block
|
// Event sequence containing ToolUse block
|
||||||
let events = vec![
|
let events = vec![
|
||||||
@@ -225,7 +225,7 @@ async fn test_tool_call_collector_integration() {
|
|||||||
|
|
||||||
// Dispatch events
|
// Dispatch events
|
||||||
for event in &events {
|
for event in &events {
|
||||||
let timeline_event: llm_engine::timeline::event::Event = event.clone().into();
|
let timeline_event: agen::timeline::event::Event = event.clone().into();
|
||||||
timeline.dispatch(&timeline_event);
|
timeline.dispatch(&timeline_event);
|
||||||
}
|
}
|
||||||
|
|
||||||
+5
-5
@@ -8,12 +8,12 @@ mod common;
|
|||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use agen::Item;
|
||||||
|
use agen::llm_client::event::{Event, ResponseStatus, StatusEvent};
|
||||||
|
use agen::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
||||||
|
use agen::{Engine, EngineError};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use common::MockLlmClient;
|
use common::MockLlmClient;
|
||||||
use llm_engine::Item;
|
|
||||||
use llm_engine::llm_client::event::{Event, ResponseStatus, StatusEvent};
|
|
||||||
use llm_engine::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
|
||||||
use llm_engine::{Engine, EngineError};
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Mutable State Tests
|
// Mutable State Tests
|
||||||
@@ -150,7 +150,7 @@ impl Tool for CountingTool {
|
|||||||
async fn execute(
|
async fn execute(
|
||||||
&self,
|
&self,
|
||||||
_input_json: &str,
|
_input_json: &str,
|
||||||
_ctx: llm_engine::tool::ToolExecutionContext,
|
_ctx: agen::tool::ToolExecutionContext,
|
||||||
) -> Result<ToolOutput, ToolError> {
|
) -> Result<ToolOutput, ToolError> {
|
||||||
self.calls.fetch_add(1, Ordering::SeqCst);
|
self.calls.fetch_add(1, Ordering::SeqCst);
|
||||||
Ok(format!("{}-ok", self.name).into())
|
Ok(format!("{}-ok", self.name).into())
|
||||||
+8
-10
@@ -6,15 +6,13 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use agen::Engine;
|
||||||
use llm_engine::Engine;
|
use agen::interceptor::{Interceptor, PostToolAction, PreToolAction, ToolCallInfo, ToolResultInfo};
|
||||||
use llm_engine::interceptor::{
|
use agen::llm_client::event::{Event, ResponseStatus, StatusEvent};
|
||||||
Interceptor, PostToolAction, PreToolAction, ToolCallInfo, ToolResultInfo,
|
use agen::tool::{
|
||||||
};
|
|
||||||
use llm_engine::llm_client::event::{Event, ResponseStatus, StatusEvent};
|
|
||||||
use llm_engine::tool::{
|
|
||||||
Tool, ToolDefinition, ToolError, ToolExecutionContext, ToolMeta, ToolOutput, ToolResult,
|
Tool, ToolDefinition, ToolError, ToolExecutionContext, ToolMeta, ToolOutput, ToolResult,
|
||||||
};
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
use common::MockLlmClient;
|
use common::MockLlmClient;
|
||||||
@@ -64,7 +62,7 @@ impl Tool for SlowTool {
|
|||||||
async fn execute(
|
async fn execute(
|
||||||
&self,
|
&self,
|
||||||
_input_json: &str,
|
_input_json: &str,
|
||||||
_ctx: llm_engine::tool::ToolExecutionContext,
|
_ctx: agen::tool::ToolExecutionContext,
|
||||||
) -> Result<ToolOutput, ToolError> {
|
) -> Result<ToolOutput, ToolError> {
|
||||||
self.call_count.fetch_add(1, Ordering::SeqCst);
|
self.call_count.fetch_add(1, Ordering::SeqCst);
|
||||||
tokio::time::sleep(Duration::from_millis(self.delay_ms)).await;
|
tokio::time::sleep(Duration::from_millis(self.delay_ms)).await;
|
||||||
@@ -468,7 +466,7 @@ async fn test_post_tool_call_modification() {
|
|||||||
async fn execute(
|
async fn execute(
|
||||||
&self,
|
&self,
|
||||||
_: &str,
|
_: &str,
|
||||||
_ctx: llm_engine::tool::ToolExecutionContext,
|
_ctx: agen::tool::ToolExecutionContext,
|
||||||
) -> Result<ToolOutput, ToolError> {
|
) -> Result<ToolOutput, ToolError> {
|
||||||
Ok("Original Result".to_string().into())
|
Ok("Original Result".to_string().into())
|
||||||
}
|
}
|
||||||
@@ -565,7 +563,7 @@ async fn test_before_tool_call_synthetic_result_committed() {
|
|||||||
assert_eq!(blocked_clone.call_count(), 0, "Blocked tool should not run");
|
assert_eq!(blocked_clone.call_count(), 0, "Blocked tool should not run");
|
||||||
assert!(result.engine.history().iter().any(|item| matches!(
|
assert!(result.engine.history().iter().any(|item| matches!(
|
||||||
item,
|
item,
|
||||||
llm_engine::Item::ToolResult {
|
agen::Item::ToolResult {
|
||||||
call_id,
|
call_id,
|
||||||
summary,
|
summary,
|
||||||
is_error: true,
|
is_error: true,
|
||||||
+7
-7
@@ -13,13 +13,13 @@
|
|||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
use common::MockLlmClient;
|
use agen::Engine;
|
||||||
use llm_engine::Engine;
|
use agen::Item;
|
||||||
use llm_engine::Item;
|
use agen::llm_client::event::{
|
||||||
use llm_engine::llm_client::event::{
|
|
||||||
BlockMetadata, BlockStart, BlockStop, BlockType, Event, ReasoningBlockData, ResponseStatus,
|
BlockMetadata, BlockStart, BlockStop, BlockType, Event, ReasoningBlockData, ResponseStatus,
|
||||||
StatusEvent,
|
StatusEvent,
|
||||||
};
|
};
|
||||||
|
use common::MockLlmClient;
|
||||||
|
|
||||||
fn reasoning_block(text: impl Into<String>, data: ReasoningBlockData) -> Vec<Event> {
|
fn reasoning_block(text: impl Into<String>, data: ReasoningBlockData) -> Vec<Event> {
|
||||||
vec![
|
vec![
|
||||||
@@ -28,9 +28,9 @@ fn reasoning_block(text: impl Into<String>, data: ReasoningBlockData) -> Vec<Eve
|
|||||||
block_type: BlockType::Thinking,
|
block_type: BlockType::Thinking,
|
||||||
metadata: BlockMetadata::Thinking,
|
metadata: BlockMetadata::Thinking,
|
||||||
}),
|
}),
|
||||||
Event::BlockDelta(llm_engine::llm_client::event::BlockDelta {
|
Event::BlockDelta(agen::llm_client::event::BlockDelta {
|
||||||
index: 100,
|
index: 100,
|
||||||
delta: llm_engine::llm_client::event::DeltaContent::Thinking(text.into()),
|
delta: agen::llm_client::event::DeltaContent::Thinking(text.into()),
|
||||||
}),
|
}),
|
||||||
Event::BlockStop(BlockStop {
|
Event::BlockStop(BlockStop {
|
||||||
index: 100,
|
index: 100,
|
||||||
@@ -174,7 +174,7 @@ async fn injected_reasoning_survives_into_outgoing_request() {
|
|||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use llm_engine::llm_client::{ClientError, LlmClient, Request};
|
use agen::llm_client::{ClientError, LlmClient, Request};
|
||||||
|
|
||||||
/// Request を 1 度だけキャプチャして空ストリームを返す client。
|
/// Request を 1 度だけキャプチャして空ストリームを返す client。
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -5,12 +5,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
// Imports needed for macro expansion
|
use agen::{ToolExecutionContext, tool_registry};
|
||||||
use schemars;
|
|
||||||
use serde;
|
|
||||||
|
|
||||||
use llm_engine::ToolExecutionContext;
|
|
||||||
use llm_engine_macros::tool_registry;
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Test: Basic Tool Generation
|
// Test: Basic Tool Generation
|
||||||
+8
-8
@@ -3,15 +3,15 @@
|
|||||||
//! Retry/backoff は Engine の lifecycle 管理に属するため、transport は 1 回だけ
|
//! Retry/backoff は Engine の lifecycle 管理に属するため、transport は 1 回だけ
|
||||||
//! request を送り、HTTP status / Retry-After を `ClientError` に載せて返す。
|
//! request を送り、HTTP status / Retry-After を `ClientError` に載せて返す。
|
||||||
|
|
||||||
|
use agen::llm_client::LlmClient;
|
||||||
|
use agen::llm_client::auth::AuthRequirement;
|
||||||
|
use agen::llm_client::capability::ModelCapability;
|
||||||
|
use agen::llm_client::error::ClientError;
|
||||||
|
use agen::llm_client::event::Event;
|
||||||
|
use agen::llm_client::scheme::Scheme;
|
||||||
|
use agen::llm_client::transport::{HttpTransport, ResolvedAuth};
|
||||||
|
use agen::llm_client::types::Request;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use llm_engine::llm_client::LlmClient;
|
|
||||||
use llm_engine::llm_client::auth::AuthRequirement;
|
|
||||||
use llm_engine::llm_client::capability::ModelCapability;
|
|
||||||
use llm_engine::llm_client::error::ClientError;
|
|
||||||
use llm_engine::llm_client::event::Event;
|
|
||||||
use llm_engine::llm_client::scheme::Scheme;
|
|
||||||
use llm_engine::llm_client::transport::{HttpTransport, ResolvedAuth};
|
|
||||||
use llm_engine::llm_client::types::Request;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use wiremock::matchers::{method, path};
|
use wiremock::matchers::{method, path};
|
||||||
+5
-5
@@ -1,9 +1,9 @@
|
|||||||
use llm_engine::Engine;
|
use agen::Engine;
|
||||||
use llm_engine::llm_client::capability::{
|
use agen::llm_client::capability::{
|
||||||
CacheStrategy, ModelCapability, StructuredOutput, ToolCallingSupport,
|
CacheStrategy, ModelCapability, StructuredOutput, ToolCallingSupport,
|
||||||
};
|
};
|
||||||
use llm_engine::llm_client::scheme::anthropic::AnthropicScheme;
|
use agen::llm_client::scheme::anthropic::AnthropicScheme;
|
||||||
use llm_engine::llm_client::transport::{HttpTransport, ResolvedAuth};
|
use agen::llm_client::transport::{HttpTransport, ResolvedAuth};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -23,6 +23,6 @@ fn main() {
|
|||||||
);
|
);
|
||||||
let engine = Engine::new(client);
|
let engine = Engine::new(client);
|
||||||
let mut locked = engine.lock();
|
let mut locked = engine.lock();
|
||||||
let def: llm_engine::tool::ToolDefinition = Arc::new(|| panic!("unused"));
|
let def: agen::tool::ToolDefinition = Arc::new(|| panic!("unused"));
|
||||||
let _ = locked.register_tool(def);
|
let _ = locked.register_tool(def);
|
||||||
}
|
}
|
||||||
+5
-5
@@ -1,9 +1,9 @@
|
|||||||
use llm_engine::Engine;
|
use agen::Engine;
|
||||||
use llm_engine::llm_client::capability::{
|
use agen::llm_client::capability::{
|
||||||
CacheStrategy, ModelCapability, StructuredOutput, ToolCallingSupport,
|
CacheStrategy, ModelCapability, StructuredOutput, ToolCallingSupport,
|
||||||
};
|
};
|
||||||
use llm_engine::llm_client::scheme::anthropic::AnthropicScheme;
|
use agen::llm_client::scheme::anthropic::AnthropicScheme;
|
||||||
use llm_engine::llm_client::transport::{HttpTransport, ResolvedAuth};
|
use agen::llm_client::transport::{HttpTransport, ResolvedAuth};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -23,6 +23,6 @@ fn main() {
|
|||||||
);
|
);
|
||||||
let engine = Engine::new(client);
|
let engine = Engine::new(client);
|
||||||
let handle = engine.tool_server_handle();
|
let handle = engine.tool_server_handle();
|
||||||
let def: llm_engine::tool::ToolDefinition = Arc::new(|| panic!("unused"));
|
let def: agen::tool::ToolDefinition = Arc::new(|| panic!("unused"));
|
||||||
let _ = handle.register_tool(def);
|
let _ = handle.register_tool(def);
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user