use std::sync::Arc;
use polyc_llm::{DynProvider, ToolSpec};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ShareInCeiling {
pub allow: Vec<String>,
pub max_files: usize,
pub max_bytes: u64,
}
impl ShareInCeiling {
#[must_use]
pub const fn admits_anything(&self) -> bool {
!self.allow.is_empty() && self.max_files > 0 && self.max_bytes > 0
}
}
#[derive(Debug, Clone, Copy)]
pub struct WorkerScope<'a> {
pub worker_id: &'a str,
pub share_in: &'a [String],
pub ceiling: &'a ShareInCeiling,
}
impl<'a> WorkerScope<'a> {
#[must_use]
pub const fn bare(worker_id: &'a str) -> Self {
Self {
worker_id,
share_in: &[],
ceiling: &EMPTY_CEILING,
}
}
}
static EMPTY_CEILING: ShareInCeiling = ShareInCeiling {
allow: Vec::new(),
max_files: 0,
max_bytes: 0,
};
#[derive(Debug, thiserror::Error)]
pub enum ShareInError {
#[error("cannot share in `{path}`: {reason}")]
Escapes {
path: String,
reason: String,
},
#[error("cannot share in `{path}`: it is inside a delegated worker's workspace")]
WorkerSubtree {
path: String,
},
#[error("cannot share in `{path}`: this agent's share-in ceiling does not include it")]
OutsideCeiling {
path: String,
},
#[error("cannot share in {found} files: this agent's ceiling allows {limit}")]
TooManyFiles {
found: usize,
limit: usize,
},
#[error("cannot share in {found} bytes: this agent's ceiling allows {limit}")]
TooManyBytes {
found: u64,
limit: u64,
},
#[error("cannot share in `{path}`: no such file or directory in the workspace")]
NotFound {
path: String,
},
#[error("could not share in `{path}`: {reason}")]
Io {
path: String,
reason: String,
},
}
pub struct WorkerHandoff {
pub tools: Arc<dyn crate::ToolExecutor>,
pub seeded: Vec<String>,
}
impl std::fmt::Debug for WorkerHandoff {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WorkerHandoff")
.field("tools", &"<dyn ToolExecutor>")
.field("seeded", &self.seeded)
.finish()
}
}
pub const DELEGATE_TOOL_NAME: &str = "__delegate_to";
pub const WORKER_CONDENSATION_CONTRACT: &str = "You are completing one delegated task. The \
caller sees only your final message — none of your tool calls, intermediate work, or \
earlier drafts reach it. Make your final message a self-contained summary of the outcome: \
what you did or found, the key details the caller needs, and anything that failed. Keep it \
concise — an overlong answer is trimmed from the middle.";
#[must_use]
pub(crate) fn worker_system_text(
instructions: Option<&str>,
has_result_schema: bool,
) -> Option<String> {
if has_result_schema {
return instructions.map(str::to_owned);
}
Some(instructions.map_or_else(
|| WORKER_CONDENSATION_CONTRACT.to_owned(),
|instructions| format!("{instructions}\n\n{WORKER_CONDENSATION_CONTRACT}"),
))
}
#[must_use]
pub(crate) fn worker_turn_start_block(unix_ms: u64) -> Option<String> {
let instant = i64::try_from(unix_ms)
.ok()
.and_then(|ms| jiff::Timestamp::from_millisecond(ms).ok())?
.strftime("%Y-%m-%d %H:%M")
.to_string();
Some(format!(
"This turn started at {instant} UTC. Later steps in this turn may \
run after this instant."
))
}
#[must_use]
pub fn delegate_tool_spec() -> ToolSpec {
ToolSpec::new(
DELEGATE_TOOL_NAME,
"Hand a single, self-contained task to a specialized worker and wait for its answer. \
The worker runs in an isolated context — it does NOT see this conversation's history, \
only `task` and, if given, `context` — so state everything the worker needs to know. \
`target_agent_id` selects which worker runs the task. Set `result_schema` (a JSON \
Schema) to force the worker's answer into that shape instead of free text — the worker \
gets one retry if its first answer doesn't match, and reports a structured failure if it \
still can't conform.",
serde_json::json!({
"type": "object",
"properties": {
"target_agent_id": {
"type": "string",
"description": "Identifier of the worker agent to run the task."
},
"task": {
"type": "string",
"description": "The self-contained task for the worker to perform."
},
"context": {
"type": "string",
"description": "Optional extra context the worker needs — the worker sees no \
other history, so include anything relevant here."
},
"result_schema": {
"type": "object",
"description": "Optional JSON Schema the worker's final answer must satisfy. \
Omit for a free-text answer."
},
"share_in": {
"type": "array",
"items": {"type": "string"},
"description": "Optional workspace files to copy into the worker's own \
workspace before it starts. The worker has a separate workspace and \
cannot see yours, so name every file its task is about — each entry is \
a path relative to the workspace, either a file or a directory. The \
worker gets its own copy; its edits never reach your files."
}
},
"required": ["target_agent_id", "task"],
"additionalProperties": false
}),
)
}
#[derive(Clone)]
pub struct DelegateDescriptor {
pub agent_id: String,
pub instructions: Option<String>,
pub provider: Arc<DynProvider>,
pub provider_name: String,
pub model: String,
pub tool_specs: Vec<ToolSpec>,
pub max_steps: usize,
pub native_search_allowed: bool,
pub share_in: ShareInCeiling,
}
impl std::fmt::Debug for DelegateDescriptor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DelegateDescriptor")
.field("agent_id", &self.agent_id)
.field("provider_name", &self.provider_name)
.field("model", &self.model)
.field(
"tool_specs",
&self.tool_specs.iter().map(|s| &s.name).collect::<Vec<_>>(),
)
.field("max_steps", &self.max_steps)
.finish_non_exhaustive()
}
}
fn trailing_name(entry: &str) -> &str {
entry.rsplit('/').next().unwrap_or(entry)
}
#[must_use]
pub fn find_descriptor<'a>(
descriptors: &'a [DelegateDescriptor],
target_agent_id: &str,
) -> Option<&'a DelegateDescriptor> {
let target = trailing_name(target_agent_id);
descriptors
.iter()
.find(|d| trailing_name(&d.agent_id) == target)
}
#[derive(Debug, Clone)]
pub struct DelegateRequest {
pub call_id: String,
pub target_agent_id: String,
pub task: String,
pub context: Option<String>,
pub result_schema: Option<serde_json::Value>,
pub share_in: Vec<String>,
}
#[must_use]
pub fn parse_delegate_args(call_id: &str, args_json: &str) -> Option<DelegateRequest> {
let v: serde_json::Value = serde_json::from_str(args_json).ok()?;
let target_agent_id = v.get("target_agent_id")?.as_str()?.to_owned();
if target_agent_id.is_empty() {
return None;
}
let task = v.get("task")?.as_str()?.to_owned();
if task.is_empty() {
return None;
}
let context = v
.get("context")
.and_then(serde_json::Value::as_str)
.filter(|s| !s.is_empty())
.map(str::to_owned);
let result_schema = v.get("result_schema").cloned();
let share_in = v
.get("share_in")
.and_then(serde_json::Value::as_array)
.map(|entries| {
entries
.iter()
.filter_map(serde_json::Value::as_str)
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect()
})
.unwrap_or_default();
Some(DelegateRequest {
call_id: call_id.to_owned(),
target_agent_id,
task,
context,
result_schema,
share_in,
})
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use super::*;
fn descriptor(agent_id: &str) -> DelegateDescriptor {
DelegateDescriptor {
agent_id: agent_id.to_owned(),
instructions: None,
provider: polyc_llm::into_dyn(polyc_llm::turn::StubProvider),
provider_name: "stub".to_owned(),
model: "stub".to_owned(),
tool_specs: Vec::new(),
max_steps: 4,
native_search_allowed: false,
share_in: ShareInCeiling::default(),
}
}
#[test]
fn parses_minimum_required_args() {
let req = parse_delegate_args(
"c-1",
r#"{"target_agent_id":"researcher","task":"find the answer"}"#,
)
.unwrap();
assert_eq!(req.target_agent_id, "researcher");
assert_eq!(req.task, "find the answer");
assert!(req.context.is_none());
assert_eq!(req.call_id, "c-1");
}
#[test]
fn parses_optional_context() {
let req = parse_delegate_args(
"c-2",
r#"{"target_agent_id":"x","task":"t","context":"extra"}"#,
)
.unwrap();
assert_eq!(req.context.as_deref(), Some("extra"));
}
#[test]
fn parses_optional_result_schema() {
let req = parse_delegate_args(
"c-3",
r#"{"target_agent_id":"x","task":"t","result_schema":{"type":"object"}}"#,
)
.unwrap();
assert_eq!(
req.result_schema,
Some(serde_json::json!({"type":"object"}))
);
}
#[test]
fn result_schema_absent_by_default() {
let req = parse_delegate_args("c-4", r#"{"target_agent_id":"x","task":"t"}"#).unwrap();
assert!(req.result_schema.is_none());
}
#[test]
fn rejects_missing_target_agent_id() {
assert!(parse_delegate_args("c", r#"{"task":"t"}"#).is_none());
}
#[test]
fn rejects_empty_target_agent_id() {
assert!(parse_delegate_args("c", r#"{"target_agent_id":"","task":"t"}"#).is_none());
}
#[test]
fn rejects_missing_task() {
assert!(parse_delegate_args("c", r#"{"target_agent_id":"x"}"#).is_none());
}
#[test]
fn rejects_empty_task() {
assert!(parse_delegate_args("c", r#"{"target_agent_id":"x","task":""}"#).is_none());
}
#[test]
fn rejects_garbage_json() {
assert!(parse_delegate_args("c", "not-json").is_none());
}
#[test]
fn delegate_tool_spec_has_required_fields() {
let spec = delegate_tool_spec();
assert_eq!(spec.name, DELEGATE_TOOL_NAME);
let required = spec
.schema_json
.get("required")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
assert!(required.iter().any(|v| v == "target_agent_id"));
assert!(required.iter().any(|v| v == "task"));
assert_eq!(
spec.schema_json.get("additionalProperties"),
Some(&serde_json::json!(false))
);
}
#[test]
fn find_descriptor_matches_by_trailing_name() {
let descriptors = vec![descriptor("agent:default/researcher"), descriptor("coder")];
assert!(find_descriptor(&descriptors, "researcher").is_some());
assert!(find_descriptor(&descriptors, "agent:other-ns/researcher").is_some());
assert!(find_descriptor(&descriptors, "coder").is_some());
assert!(find_descriptor(&descriptors, "ghost").is_none());
}
#[test]
fn contract_appended_after_instructions_without_schema() {
let text = worker_system_text(Some("You are a scoped worker."), false)
.expect("no-schema path always returns Some");
assert_eq!(
text,
format!("You are a scoped worker.\n\n{WORKER_CONDENSATION_CONTRACT}")
);
}
#[test]
fn contract_alone_without_instructions_or_schema() {
let text = worker_system_text(None, false).expect("no-schema path always returns Some");
assert_eq!(text, WORKER_CONDENSATION_CONTRACT);
}
#[test]
fn instructions_unchanged_with_schema() {
let text = worker_system_text(Some("You are a scoped worker."), true);
assert_eq!(text.as_deref(), Some("You are a scoped worker."));
}
#[test]
fn no_system_text_with_schema_and_no_instructions() {
assert_eq!(worker_system_text(None, true), None);
}
#[test]
fn renders_utc_at_minute_precision() {
let block = worker_turn_start_block(1_715_938_439_000).expect("in-range");
assert_eq!(
block,
"This turn started at 2024-05-17 09:33 UTC. Later steps in this turn may run after \
this instant."
);
}
#[test]
fn matches_the_top_level_blocks_wording() {
let block = worker_turn_start_block(0).expect("epoch is in range");
assert!(block.starts_with("This turn started at "));
assert!(!block.to_lowercase().contains("now"));
}
#[test]
fn out_of_range_instant_renders_no_block() {
assert_eq!(worker_turn_start_block(u64::MAX), None);
}
#[test]
fn same_input_ms_renders_identical_bytes() {
let a = worker_turn_start_block(1_715_938_439_000);
let b = worker_turn_start_block(1_715_938_439_000);
assert_eq!(a, b);
}
}