Skip to content

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) State
func (rt *Runtime) Inspect(h *Handle) HandleView
func (rt *Runtime) InboxLen(h *Handle) int
func (rt *Runtime) PoolTokens(h *Handle) int64
func (rt *Runtime) UsageTokens(h *Handle) int

Handle 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).

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.

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))
}
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.
  • 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.