Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e954887
Added: Stateless Task delegation foundations across three crates
Sewer56 Mar 15, 2026
91c3e08
Added: Wire Task tool into default tool catalog and agent build path
Sewer56 Mar 15, 2026
cc9e8dc
Removed: Delete build_delegated_message wrapper
Sewer56 Mar 15, 2026
996005a
Added: Stateless Task tool execution with credential-injected agent b…
Sewer56 Mar 15, 2026
efd5b57
Added: Task tool examples and README documentation
Sewer56 Mar 15, 2026
92bfa05
Fixed: Mismatched XML closing tag in task.txt
Sewer56 Mar 16, 2026
b8392f8
Added: Configurable max Task delegation depth to prevent unbounded re…
Sewer56 Mar 16, 2026
1af61ba
Fixed: Formatter Issue
Sewer56 Mar 16, 2026
a7c0f96
Changed: Delegate sub-agent task calls as tool-call context instead o…
Sewer56 Mar 16, 2026
c345ffc
Changed: Reorganize task-demo agents into dedicated subfolder
Sewer56 Mar 16, 2026
312df50
Fixed: keep the Task tool available when delegation is allowed
Sewer56 Mar 16, 2026
4ffff70
Revert "Changed: Delegate sub-agent task calls as tool-call context i…
Sewer56 Mar 16, 2026
7d7c0cc
Changed: Stream task example output in real time with XML transcript
Sewer56 Mar 17, 2026
b85c26c
Changed: Document llm-coding-tools-models-dev dependency in Task Tool…
Sewer56 Mar 17, 2026
6d86737
Changed: Add task to default tools listed in agents README
Sewer56 Mar 17, 2026
44904eb
Fixed: Add missing subagent_type to Task tool example
Sewer56 Mar 17, 2026
0e0ef25
Fixed: Add missing task tool to runtime module doc comment
Sewer56 Mar 17, 2026
7cace31
Changed: Document intentional implicit-allow semantics in validate_ta…
Sewer56 Mar 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/llm-coding-tools-agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ loader.add_directory(&mut catalog, "/home/user/.opencode")?;
let runtime = AgentRuntimeBuilder::new()
.catalog(catalog)
.defaults(AgentDefaults::with_model("openai/gpt-4o-mini"))
// .tools(my_custom_tools) // optional; defaults to read/write/edit/glob/grep/bash/webfetch/todoread/todowrite
// .max_task_depth(5) // optional; defaults to 3 Task hops
// .tools(my_custom_tools) // optional; defaults to read/write/edit/glob/grep/bash/webfetch/todoread/todowrite/task
.build();

// Pass `runtime` to your framework adapter to build agents by name
Expand All @@ -79,6 +80,9 @@ approval flows).
To avoid false expectations, settings that require interaction are rejected,
while settings with no runtime effect are accepted and ignored:

- Unspecified permissions default to `deny` for normal tools. `permission.task`
is special: if omitted, Task still allows delegation to callable
`mode: all` / `mode: subagent` targets for OpenCode compatibility.
- [`permission.task`](https://opencode.ai/docs/agents#task-permissions):
`ask` is rejected with a schema validation error (`allow`/`deny` only),
because `ask` is an interactive approval mode in OpenCode
Expand Down
64 changes: 1 addition & 63 deletions src/llm-coding-tools-agents/src/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
//! Helpers for converting agent permission config into runtime [`Ruleset`] values.
//!
//! ## What This Module Provides
//! - [`RulesetExt`] trait for building a [`Ruleset`] from frontmatter data and
//! filtering tool entries by permission.
//! - [`RulesetExt`] trait for building a [`Ruleset`] from frontmatter data.
//! - Support for scalar (`allow`/`deny`) and pattern-map permission rules.
//! - Iteration-order preservation via [`IndexMap`] (important for precedence).

use crate::runtime::ToolCatalogEntry;
use crate::types::PermissionRule;
use indexmap::IndexMap;
use llm_coding_tools_core::permissions::{Rule, Ruleset};
Expand Down Expand Up @@ -40,37 +38,6 @@ pub trait RulesetExt: Sized {
/// assert!(ruleset.is_allowed("bash", "*"));
/// ```
fn from_permission_config(config: &IndexMap<String, PermissionRule>) -> Self;

/// Filters tool entries to those allowed by this ruleset.
///
/// Returns only entries whose `name` passes `is_allowed(name, "*")`.
///
/// # Arguments
///
/// * `tools` - Slice of tool entries to filter.
///
/// # Returns
///
/// A vector containing only the tool entries allowed by this ruleset,
/// preserving the original order.
///
/// # Example
///
/// ```
/// use llm_coding_tools_agents::{
/// default_tools, PermissionRule, RulesetExt,
/// };
/// use llm_coding_tools_core::permissions::{PermissionAction, Ruleset};
/// use indexmap::IndexMap;
///
/// let mut config = IndexMap::new();
/// config.insert("read".to_string(), PermissionRule::Action(PermissionAction::Allow));
///
/// let ruleset = Ruleset::from_permission_config(&config);
/// let allowed = ruleset.filter_allowed_tools(&default_tools());
/// assert!(allowed.iter().any(|t| t.name == "read"));
/// ```
fn filter_allowed_tools(&self, tools: &[ToolCatalogEntry]) -> Vec<ToolCatalogEntry>;
}

impl RulesetExt for Ruleset {
Expand All @@ -92,20 +59,11 @@ impl RulesetExt for Ruleset {

ruleset
}

fn filter_allowed_tools(&self, tools: &[ToolCatalogEntry]) -> Vec<ToolCatalogEntry> {
tools
.iter()
.copied()
.filter(|entry| self.is_allowed(entry.name, "*"))
.collect()
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::default_tools;
use llm_coding_tools_core::permissions::PermissionAction;

#[test]
Expand Down Expand Up @@ -144,24 +102,4 @@ mod tests {
PermissionAction::Deny
);
}

#[test]
fn filter_allowed_tools_returns_allowed_entries() {
let mut config = IndexMap::new();
config.insert(
"read".to_string(),
PermissionRule::Action(PermissionAction::Allow),
);
config.insert(
"glob".to_string(),
PermissionRule::Action(PermissionAction::Allow),
);

let ruleset = Ruleset::from_permission_config(&config);
let allowed = ruleset.filter_allowed_tools(&default_tools());

assert!(allowed.iter().any(|t| t.name == "read"));
assert!(allowed.iter().any(|t| t.name == "glob"));
assert!(!allowed.iter().any(|t| t.name == "bash"));
}
}
5 changes: 3 additions & 2 deletions src/llm-coding-tools-agents/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ pub use extensions::RulesetExt;
pub use loader::AgentLoader;
pub use parser::AgentParseError;
pub use runtime::{
default_tools, resolve_model_with_catalog, AgentDefaults, AgentRuntime, AgentRuntimeBuilder,
ModelResolutionError, ResolvedModel, ToolCatalogEntry, ToolCatalogKind,
callable_targets, default_tools, resolve_model_with_catalog, summarize_callable_targets,
AgentDefaults, AgentRuntime, AgentRuntimeBuilder, ModelResolutionError, ResolvedModel,
TaskSettings, TaskTargetSummary, ToolCatalogEntry, ToolCatalogKind,
};
pub use types::{
parse_model_parts, AgentConfig, AgentLoadError, AgentLoadResult, AgentMode, PermissionRule,
Expand Down
31 changes: 29 additions & 2 deletions src/llm-coding-tools-agents/src/runtime/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
use super::state::{AgentDefaults, AgentRuntime};
use super::tool_catalog::{default_tools, ToolCatalogEntry};
use crate::AgentCatalog;
use llm_coding_tools_core::TaskSettings;

/// Builds an [`AgentRuntime`] step by step.
#[derive(Debug, Clone)]
pub struct AgentRuntimeBuilder {
catalog: AgentCatalog,
defaults: AgentDefaults,
task_settings: TaskSettings,
tools: Vec<ToolCatalogEntry>,
}

Expand All @@ -20,12 +22,13 @@ impl Default for AgentRuntimeBuilder {
}

impl AgentRuntimeBuilder {
/// Creates a builder with empty catalog, empty defaults, and the standard tool set.
/// Creates a builder with empty catalog, empty defaults, default Task settings, and the standard tool set.
#[inline]
pub fn new() -> Self {
Self {
catalog: AgentCatalog::new(),
defaults: AgentDefaults::default(),
task_settings: TaskSettings::default(),
tools: default_tools(),
}
}
Expand All @@ -44,6 +47,20 @@ impl AgentRuntimeBuilder {
self
}

/// Sets the shared Task delegation settings.
#[inline]
pub fn task_settings(mut self, task_settings: TaskSettings) -> Self {
self.task_settings = task_settings;
self
}

/// Sets the maximum number of Task delegation hops.
#[inline]
pub fn max_task_depth(mut self, max_depth: u8) -> Self {
self.task_settings = TaskSettings::with_max_depth(max_depth);
self
}

/// Sets the available tools.
#[inline]
pub fn tools(mut self, tools: Vec<ToolCatalogEntry>) -> Self {
Expand All @@ -54,7 +71,7 @@ impl AgentRuntimeBuilder {
/// Finishes building and returns the [`AgentRuntime`].
#[inline]
pub fn build(self) -> AgentRuntime {
AgentRuntime::from_parts(self.catalog, self.defaults, self.tools)
AgentRuntime::from_parts(self.catalog, self.defaults, self.task_settings, self.tools)
}
}

Expand All @@ -65,6 +82,7 @@ mod tests {
use crate::runtime::AgentDefaults;
use crate::{AgentCatalog, AgentConfig, AgentMode};
use llm_coding_tools_core::tool_names;
use llm_coding_tools_core::TaskSettings;

fn sample_config(name: &str, model: Option<&str>) -> AgentConfig {
AgentConfig {
Expand Down Expand Up @@ -108,15 +126,24 @@ mod tests {
Some("openai/gpt-4o"),
);
assert_eq!(runtime.defaults(), &defaults);
assert_eq!(runtime.task_settings(), TaskSettings::default());
assert_eq!(runtime.tools(), tools.as_slice());
}

#[test]
fn builder_overrides_task_settings() {
let runtime = AgentRuntimeBuilder::new().max_task_depth(5).build();

assert_eq!(runtime.task_settings(), TaskSettings::with_max_depth(5));
}

#[test]
fn builder_defaults_to_empty_catalog_defaults_and_default_tools() {
let runtime = AgentRuntimeBuilder::new().build();

assert_eq!(runtime.catalog().iter().count(), 0);
assert_eq!(runtime.defaults(), &AgentDefaults::default());
assert_eq!(runtime.task_settings(), TaskSettings::default());
assert_eq!(runtime.tools(), default_tools().as_slice());
}
}
11 changes: 10 additions & 1 deletion src/llm-coding-tools-agents/src/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@
//! - [`AgentRuntime`] - Your agents plus their default settings and tools
//! - [`AgentRuntimeBuilder`] - Builds an [`AgentRuntime`]
//! - [`AgentDefaults`] - Default model, temperature, and top-p when agents don't specify them
//! - [`TaskSettings`] - Shared Task delegation limits for all integrations using the runtime
//!
//! Tools:
//! - [`ToolCatalogEntry`] - One tool the runtime can provide to agents
//! - [`ToolCatalogKind`] - Which tools are available
//! - [`default_tools()`] - The standard tool set (read, write, edit, glob, grep, bash, webfetch, todo)
//! - [`default_tools()`] - The standard tool set (read, write, edit, glob, grep, bash, webfetch, todo, task)
//!
//! Task delegation:
//! - [`summarize_callable_targets()`] - Builds target summaries with names and descriptions
//! - [`callable_targets()`] - Returns the agents the active agent may delegate to
//! - [`TaskTargetSummary`] - Metadata for a callable Task target
//!
Comment thread
Sewer56 marked this conversation as resolved.
//! Model resolution:
//! - [`ResolvedModel`] - A model identifier that's been validated against your catalog
Expand All @@ -36,9 +42,12 @@
mod builder;
mod model;
mod state;
mod task;
mod tool_catalog;

pub use builder::AgentRuntimeBuilder;
pub use llm_coding_tools_core::TaskSettings;
pub use model::{resolve_model_with_catalog, ModelResolutionError, ResolvedModel};
pub use state::{AgentDefaults, AgentRuntime};
pub use task::{callable_targets, summarize_callable_targets, TaskTargetSummary};
pub use tool_catalog::{default_tools, ToolCatalogEntry, ToolCatalogKind};
27 changes: 24 additions & 3 deletions src/llm-coding-tools-agents/src/runtime/state.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
//! Holds your loaded agents, default settings, and available tools.
//! Holds your loaded agents, default settings, Task settings, and available tools.
//!
//! ## Public API
//!
//! - [`AgentRuntime`] — Container for loaded agents, defaults, and tools.
//! - [`AgentRuntime`] — Container for loaded agents, defaults, Task settings, and tools.
//! - [`AgentDefaults`] — Fallback settings when an agent doesn't specify them.

use super::task::resolve_allowed_tools;
use super::tool_catalog::ToolCatalogEntry;
use crate::AgentCatalog;
use llm_coding_tools_core::TaskSettings;

/// Default settings used when an agent doesn't specify them.
#[derive(Debug, Clone, Default, PartialEq)]
Expand All @@ -31,11 +33,12 @@ impl AgentDefaults {
}
}

/// Your loaded agents plus their default settings and available tools.
/// Your loaded agents plus their default settings, Task settings, and available tools.
#[derive(Debug, Clone)]
pub struct AgentRuntime {
catalog: AgentCatalog,
defaults: AgentDefaults,
task_settings: TaskSettings,
tools: Vec<ToolCatalogEntry>,
}

Expand All @@ -44,11 +47,13 @@ impl AgentRuntime {
pub(super) fn from_parts(
catalog: AgentCatalog,
defaults: AgentDefaults,
task_settings: TaskSettings,
tools: Vec<ToolCatalogEntry>,
) -> Self {
Self {
catalog,
defaults,
task_settings,
tools,
}
}
Expand All @@ -65,9 +70,25 @@ impl AgentRuntime {
&self.defaults
}

/// Returns the shared Task delegation settings.
#[inline]
pub fn task_settings(&self) -> TaskSettings {
self.task_settings
}

/// Returns the tools available to agents.
#[inline]
pub fn tools(&self) -> &[ToolCatalogEntry] {
&self.tools
}

/// Returns the tool entries exposed to the named caller.
///
/// Most tools use the standard wildcard permission check (`permission -> "*"`).
/// `task` is only included when at least one `mode: all` or `mode: subagent`
/// target remains callable after applying `permission.task`.
#[inline]
pub fn allowed_tools(&self, caller_name: &str) -> Vec<ToolCatalogEntry> {
resolve_allowed_tools(self, caller_name)
}
}
Loading
Loading