Skip to content

The closed status and stop-limit vocabularies every task/run result uses

Rust · crate toolnexus · SPEC §7D · rust/src/agents/mod.rs

// toolnexus::agents — the §7D TaskResult.status set (seven values)
TASK_STATUS_DONE, TASK_STATUS_PENDING, TASK_STATUS_INCOMPLETE, TASK_STATUS_INTERRUPTED,
TASK_STATUS_CLOSED, TASK_STATUS_TIMEOUT, TASK_STATUS_ERROR
// TaskResult.limit — the closed stop vocabulary
LIMIT_MAX_TURNS, LIMIT_MAX_TOKENS, LIMIT_MAX_TOOL_CALLS, LIMIT_MAX_WALL_MS, LIMIT_MAX_CHILDREN,
LIMIT_MAX_CONCURRENT, LIMIT_MAX_DEPTH, LIMIT_COMPLETION, LIMIT_TIMEOUT

The strings a host branches on, identical in all ports. The §7D agent set is not the §8 client’s (RunResult.status is done | pending | incomplete): "timeout" belongs only to wait(handle, timeout) — a wait deadline while the child keeps running. Every limit stop (incomplete, timeout) names its limit; every other status leaves limit empty.

Use the constants — never a bare string literal — whenever you branch on TaskResult.status or TaskResult.limit. A literal "timeout" compiles against either vocabulary and silently reads the wrong one if you branch on the wrong result type; the named constant makes which vocabulary you meant visible at the call site, and a rename anywhere in the closed set becomes a compile error instead of a silent drift.

if r.status == agents::TASK_STATUS_INCOMPLETE && r.limit == agents::LIMIT_MAX_TOOL_CALLS {
// this child ran out of tool-call budget
}

1. The smallest useful call — branch on the §8 client vocabulary

Section titled “1. The smallest useful call — branch on the §8 client vocabulary”
use std::sync::Arc;
use serde_json::json;
use toolnexus::client::{InProcessOptions, InProcessResponse, InProcessToolCall, create_in_process_client};
use toolnexus::{BuiltinsConfig, ToolkitOptions, create_toolkit, define_tool};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let noop = define_tool("noop", "does nothing", None, |_, _| async { Ok::<_, String>("ok".into()) });
let tk = create_toolkit(ToolkitOptions {
builtins: Some(BuiltinsConfig { enabled: Some(false), ..Default::default() }),
extra_tools: vec![noop],
..Default::default()
})
.await?;
// An in-process model that ALWAYS calls a tool, so max_turns is exhausted before "done".
let client = create_in_process_client(InProcessOptions {
model: "stub".into(),
max_turns: 2,
generate: Some(Arc::new(|_req| Box::pin(async {
Ok(InProcessResponse {
tool_calls: vec![InProcessToolCall { name: "noop".into(), arguments: json!({}), ..Default::default() }],
..Default::default()
})
}))),
..Default::default()
});
let r = client.run("go", &tk).await?;
assert_eq!((r.status.as_str(), r.limit.as_str()), ("incomplete", "maxTurns"));
println!("ok: {} {}", r.status, r.limit);
Ok(())
}

2. The realistic case — branch on the §7D task/agent vocabulary

Section titled “2. The realistic case — branch on the §7D task/agent vocabulary”
use toolnexus::agents::{
LIMIT_MAX_TOOL_CALLS, TASK_STATUS_INCOMPLETE, TASK_STATUS_TIMEOUT, TaskResult,
};
fn main() {
// A task that ran out of its own tool-call budget reports at the AGENT level.
let tr = TaskResult {
status: TASK_STATUS_INCOMPLETE.into(),
limit: LIMIT_MAX_TOOL_CALLS.into(),
text: "stopped: tool-call budget".into(),
..Default::default()
};
match tr.status.as_str() {
TASK_STATUS_INCOMPLETE => {}
TASK_STATUS_TIMEOUT => panic!("this branch would be a WAIT deadline, not this scenario"),
other => panic!("unexpected status {other:?}"),
}
assert_eq!(tr.limit, LIMIT_MAX_TOOL_CALLS);
println!("ok: incomplete, limit = {}", tr.limit);
}

3. The full surface — the two “timeout” strings side by side, and why they never collide

Section titled “3. The full surface — the two “timeout” strings side by side, and why they never collide”
use toolnexus::agents::*;
fn main() {
// Same literal string, two closed vocabularies, two different meanings.
assert_eq!(TASK_STATUS_TIMEOUT, "timeout");
assert_eq!(LIMIT_TIMEOUT, "timeout");
// The §7D status set: seven values, all distinct.
let statuses = [TASK_STATUS_DONE, TASK_STATUS_PENDING, TASK_STATUS_INCOMPLETE,
TASK_STATUS_INTERRUPTED, TASK_STATUS_CLOSED, TASK_STATUS_TIMEOUT, TASK_STATUS_ERROR];
let mut uniq = statuses.to_vec();
uniq.sort();
uniq.dedup();
assert_eq!(uniq.len(), 7);
// The closed limit vocabulary, spelled as SPEC spells the Budget fields.
let limits = [LIMIT_MAX_TURNS, LIMIT_MAX_TOKENS, LIMIT_MAX_TOOL_CALLS, LIMIT_MAX_WALL_MS,
LIMIT_MAX_CHILDREN, LIMIT_MAX_CONCURRENT, LIMIT_MAX_DEPTH, LIMIT_COMPLETION, LIMIT_TIMEOUT];
assert_eq!(limits.len(), 9);
// The §8 client set has NO timeout status: a run deadline is incomplete + limit "timeout".
let run_statuses = ["done", "pending", "incomplete"];
assert!(!run_statuses.contains(&"timeout"));
println!("ok: agent-level timeout and client-level run-deadline are two different things");
}