agents.Handle — a running sub-agent
Go · package github.com/muthuishere/toolnexus/golang/agents · SPEC §7D · golang/agents/runtime.go
type State string
const ( StateIdle State = "idle" StateRunning State = "running" StateSuspended State = "suspended" StateClosed State = "closed")// states: idle → running → (idle | suspended | closed)// suspended → running ONLY via the Answer to its pending Request
type Handle struct { ID string // deterministic, parent-scoped: root/coordinator.1/explore.2 // … everything else is runtime-owned; read it through the Runtime, below}
func (rt *Runtime) StateOf(h *Handle) Statefunc (rt *Runtime) Inspect(h *Handle) HandleViewfunc (rt *Runtime) InboxLen(h *Handle) intfunc (rt *Runtime) PoolTokens(h *Handle) int64func (rt *Runtime) UsageTokens(h *Handle) intHandle is {id, def, state, inbox, budget, children} — SPEC §7D’s exact shape. Every field past
ID is unexported and mutated under the runtime’s own mutex; you never poke a Handle directly.
Instead, the Runtime that spawned it exposes race-safe read views: StateOf for the raw
transition state, Inspect/List for the fuller HandleView (tokens, turns, inbox depth, the
pending Request’s kind when suspended).
When to use it
Section titled “When to use it”Read a Handle’s state to build observability (a dashboard of running sub-agents), to poll for a
suspension before deciding whether to answer it, or to assert on transition traces in tests. You
obtain a Handle from Spawn — never construct one yourself.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. idle right after spawn, before any wake
Section titled “1. idle right after spawn, before any wake”package main
import ( "fmt" "log"
"github.com/muthuishere/toolnexus/golang/agents")
func main() { reg := map[string]agents.Def{"peer": {Name: "peer", Does: "a worker", Model: "m-peer"}} rt := agents.NewRuntime(agents.Options{Registry: reg}) // no wake in this example — Transport unused
h, err := rt.Spawn(rt.Root, "peer", nil) if err != nil { log.Fatal(err) } if rt.StateOf(h) != agents.StateIdle { log.Fatalf("expected idle right after spawn, got %s", rt.StateOf(h)) } v := rt.Inspect(h) if v.State != agents.StateIdle || v.Turns != 0 || v.PendingKind != "" { log.Fatalf("unexpected view: %+v", v) }
fmt.Println("ok:", h.ID, "is", v.State)}2. idle → running → idle (done), observed live
Section titled “2. idle → running → idle (done), observed live”package main
import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "time"
"github.com/muthuishere/toolnexus/golang/agents")
// gatedLLM blocks until released, so the example can OBSERVE the running state// before the turn completes.type gatedLLM struct{ gate chan struct{} }
func (g gatedLLM) RoundTrip(req *http.Request) (*http.Response, error) { <-g.gate body, _ := json.Marshal(map[string]any{ "choices": []any{map[string]any{"message": map[string]any{"role": "assistant", "content": "finished"}}}, "usage": map[string]any{"prompt_tokens": 3, "completion_tokens": 3, "total_tokens": 6}, }) return &http.Response{StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(bytes.NewReader(body))}, nil}
func main() { reg := map[string]agents.Def{"worker": {Name: "worker", Does: "a worker", Model: "m-worker"}} gate := make(chan struct{}) rt := agents.NewRuntime(agents.Options{Transport: gatedLLM{gate: gate}, Registry: reg})
h, err := rt.Spawn(rt.Root, "worker", nil) if err != nil { log.Fatal(err) } rt.Wake(h, "go")
for i := 0; i < 200 && rt.StateOf(h) != agents.StateRunning; i++ { time.Sleep(time.Millisecond) } if rt.StateOf(h) != agents.StateRunning { log.Fatal("expected to observe the running state before completion") }
close(gate) // let the turn finish r := rt.Wait(h, 0) if r.Status != "done" || rt.StateOf(h) != agents.StateIdle { log.Fatalf("unexpected: status=%q state=%s", r.Status, rt.StateOf(h)) } v := rt.Inspect(h) if v.Turns != 1 || v.Tokens != 6 { log.Fatalf("unexpected view after done: %+v", v) }
fmt.Println("ok: idle -> running -> idle, turns =", v.Turns, "tokens =", v.Tokens)}3. The full surface — suspended, PendingKind, and durable Resume
Section titled “3. The full surface — suspended, PendingKind, and durable Resume”No WaitFor anywhere in the chain ⇒ the suspension goes durable: the handle parks in
StateSuspended and Runtime.Resume is the ONLY way back to running.
package main
import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http"
tn "github.com/muthuishere/toolnexus/golang" "github.com/muthuishere/toolnexus/golang/agents")
// checkSecret needs human approval the first time; once ctx.Answer carries a// satisfied Answer, it succeeds.var checkSecret = tn.Tool{ Name: "check_secret", Description: "needs approval", Source: tn.SourceCustom, InputSchema: tn.JSONSchema{"type": "object", "properties": map[string]any{}}, Execute: func(_ map[string]any, tctx *tn.ToolContext) (tn.ToolResult, error) { if tctx != nil && tctx.Answer != nil && tctx.Answer.Ok { return tn.ToolResult{Output: "secret-token"}, nil } return tn.Pending(tn.Request{Kind: "approval", Prompt: "approve secret access?"}), nil },}
type approvalLLM struct{}
func (approvalLLM) RoundTrip(req *http.Request) (*http.Response, error) { b, _ := io.ReadAll(req.Body) var parsed struct { Messages []map[string]any `json:"messages"` } _ = json.Unmarshal(b, &parsed) sawToolResult := false for _, m := range parsed.Messages { if m["role"] == "tool" { sawToolResult = true } } var resp map[string]any if !sawToolResult { args, _ := json.Marshal(map[string]any{}) resp = map[string]any{"role": "assistant", "content": nil, "tool_calls": []any{ map[string]any{"id": "c1", "type": "function", "function": map[string]any{"name": "check_secret", "arguments": string(args)}}, }} } else { resp = map[string]any{"role": "assistant", "content": "approved and done"} } body, _ := json.Marshal(map[string]any{ "choices": []any{map[string]any{"message": resp}}, "usage": map[string]any{"prompt_tokens": 3, "completion_tokens": 3, "total_tokens": 6}, }) return &http.Response{StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(bytes.NewReader(body))}, nil}
func main() { reg := map[string]agents.Def{ // No WaitFor on this def, and none higher in the chain: the suspension // goes durable instead of being answered inline. "asker": {Name: "asker", Does: "needs approvals", Model: "m-asker", Tools: []tn.Tool{checkSecret}}, } rt := agents.NewRuntime(agents.Options{Transport: approvalLLM{}, Registry: reg})
h, err := rt.Spawn(rt.Root, "asker", nil) if err != nil { log.Fatal(err) } rt.Wake(h, "get the secret") r := rt.Wait(h, 0) if r.Status != "pending" || rt.StateOf(h) != agents.StateSuspended { log.Fatalf("expected a durable pending, got status=%q state=%s", r.Status, rt.StateOf(h)) } v := rt.Inspect(h) if v.PendingKind != "approval" { log.Fatalf("expected PendingKind=approval, got %q", v.PendingKind) }
// The out-of-band Answer resumes the DEEPEST suspended handle. if err := rt.Resume(tn.Answer{ID: r.Pending.ID, Ok: true}); err != nil { log.Fatal(err) } if rt.StateOf(h) != agents.StateIdle { log.Fatalf("expected idle after resume, got %s", rt.StateOf(h)) }
fmt.Println("ok: suspended (", v.PendingKind, ") -> resumed ->", rt.StateOf(h))}HandleView fields
Section titled “HandleView fields”| Field | Type | What it holds |
|---|---|---|
ID |
string |
The handle’s deterministic id. |
State |
State |
idle | running | suspended | closed. |
Tokens |
int |
Rolled-up token usage of the subtree — the budget ledger. |
Turns |
int |
Cumulative LLM round trips. |
Inbox |
int |
Current inbox depth. |
PendingKind |
string |
The suspended Request’s Kind; "" unless State == suspended. |
See also
Section titled “See also”agents.New— Define a sub-agent with its own toolkit, prompt and budget, callable as a tool by its parent.NewRuntime— The six host verbs that drive sub-agents, plus the read-only list and inspect views.agents.Budget— Cap tool calls and wall-clock per agent and per team, enforced while the run is in flight.