Gate agent actions on application or human input
Custom tools let an Agent ask your application to perform work that should not run inside its sandbox. The application can call an internal service, record an audited decision, or wait for a human reviewer, then return the result to the same durable Session.
This example includes a runnable program over Mango's public HTTP API. Mango does
not yet publish an SDK, so the example deliberately uses Go's standard
net/http client and exposes the same requests an SDK would eventually wrap.
It never calls the model provider directly: the configured Mango worker owns
that credential and model request. Complete Getting started
first and use the exact request shapes from the Agents and
Events references.
Define the gate
A custom tool has a name, description, and JSON Schema. It does not need a sandbox implementation:
{
"name": "expense-gate",
"model": "your-model-id",
"system": "Use decide for clear expenses and escalate when a human judgment is required.",
"tools": [
{
"type": "custom",
"name": "decide",
"description": "Record a final approve or reject decision.",
"input_schema": {
"type": "object",
"properties": {
"receipt_id": {"type": "string"},
"action": {"type": "string", "enum": ["approve", "reject"]},
"reason": {"type": "string"}
},
"required": ["receipt_id", "action", "reason"]
}
},
{
"type": "custom",
"name": "escalate",
"description": "Request a human decision for an ambiguous expense.",
"input_schema": {
"type": "object",
"properties": {
"receipt_id": {"type": "string"},
"question": {"type": "string"}
},
"required": ["receipt_id", "question"]
}
}
]
}
When the model calls either tool, Mango persists an
agent.custom_tool_use. It does not execute application or human work on the
model's behalf.
Observe the barrier
After committing all custom calls from that model round, Mango emits
session.status_idle with stop_reason.type=requires_action. Its event_ids
contains every action that must be answered before the model can continue.
Store your business result idempotently before sending the corresponding
user.custom_tool_result. Use the agent.custom_tool_use event ID as the
custom_tool_use_id; do not invent a separate correlation ID.
Multiple results can be sent in one Events request:
{
"events": [
{
"type": "user.custom_tool_result",
"custom_tool_use_id": "sevt_decide",
"content": [{"type": "text", "text": "{\"recorded\":true,\"receipt_id\":\"r01\",\"decision\":\"approve\"}"}]
},
{
"type": "user.custom_tool_result",
"custom_tool_use_id": "sevt_escalate",
"content": [{"type": "text", "text": "{\"recorded\":true,\"receipt_id\":\"r02\",\"human_decision\":\"reject\"}"}]
}
]
}
Replace sevt_decide and sevt_escalate with the actual
agent.custom_tool_use event IDs returned by the Session.
Partial results remain durable without reopening the Session. The final result claims the complete barrier, changes the Session to running, and wakes one resume turn containing every paired tool result. A duplicate or mismatched result is rejected. An ordinary user message may be accepted during the wait, but cannot run before the result round completes.
Recover safely
PostgreSQL, not the API connection or worker memory, owns the barrier. If the client, API, or execution worker restarts:
- open the Session event stream;
- list persisted history while the stream is open;
- find the latest
requires_actionboundary; - correlate its action IDs with already persisted
user.custom_tool_resultevents; - submit only missing results.
An application may subscribe to session.status_idled through
Webhooks to start this recovery promptly. The notification
is deliberately thin, at least once, and not an infinite log: production
consumers must still use the documented stream-plus-history recovery pattern or
bounded reconciliation after receiving it.
Executable verification
Start the local stack with the real model values in
~/.config/mango/dev.env, then run the interactive example:
make local-up
make local-health
scripts/with-dev-env make demo-hitl-gate
The program creates an Environment, Agent, and Session through public HTTP,
sends two receipts, and waits for the complete requires_action barrier. The
real model must produce decide for the clear receipt and escalate for the
ambiguous one. The application records decide automatically; for escalate,
the terminal displays the model's question and waits for you to type
approve or reject. Results are submitted separately, and the program waits
for the real model's final response after the complete barrier.
The runnable source is
examples/hitl-gate.
It defaults to http://localhost:8080 and the documented local Mango API key.
Set MANGO_EXAMPLE_BASE_URL, MANGO_API_KEY, or
MANGO_EXAMPLE_MODEL_ID to override them. Set
MANGO_EXAMPLE_KEEP_RESOURCES=1 to retain the created resources for history
inspection; otherwise the program deletes the Session and Environment and
archives the Agent after success. The Make target removes model-provider
credentials from the example process environment before starting it.
A successful run resembles the following. Model wording can vary, but the program requires the two typed actions and the final completed turn:
The real model requested these application-owned actions:
decide {"action":"approve",...,"receipt_id":"r01"}
escalate {"question":"This USD 900 expense ... can an itemized receipt be provided?","receipt_id":"r02"}
Application records approve for r01.
First result persisted; the Session remains at the incomplete barrier.
Human review required for r02: This USD 900 expense ... can an itemized receipt be provided?
Decision [approve/reject]: reject
Agent final response:
Summary of recorded outcomes:
Receipt r01: Approved ...
Receipt r02: Rejected (after human review) ...
A separate offline test creates seven parallel actions, answers only three, rejects a duplicate without a partial commit, replaces the execution worker, and then resumes the complete barrier exactly once:
make test-hitl-gate
That deterministic test proves failure and concurrency invariants; it is not presented as the Cookbook user example.
Design boundary
The expense-approval user problem and custom-tool round trip are informed by Anthropic's public CMA gate cookbook. Mango uses its own complete-barrier, PostgreSQL admission, Temporal recovery, and event semantics. See Design provenance for the adopted and changed parts.