You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: agent-schema.json
+7Lines changed: 7 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -620,6 +620,13 @@
620
620
"$ref": "#/definitions/HookDefinition"
621
621
}
622
622
},
623
+
"turn_end": {
624
+
"type": "array",
625
+
"description": "Hooks that run once per agent turn when the turn finishes — the symmetric counterpart of turn_start. Fires no matter why the turn ended: a normal stop, an error, a hook-driven shutdown, the loop detector, or context cancellation. The reason is reported via the hook input's reason field ('normal', 'continue', 'steered', 'error', 'canceled', 'hook_blocked', 'loop_detected'). Observational; output is ignored.",
626
+
"items": {
627
+
"$ref": "#/definitions/HookDefinition"
628
+
}
629
+
},
623
630
"before_llm_call": {
624
631
"type": "array",
625
632
"description": "Hooks that run just before each model call (after turn_start has assembled the messages). Use for observability, cost guardrails, or auditing without contributing system messages — turn_start is the right event for the latter.",
| `session_end` | `reason` — one of `clear`, `logout`, `prompt_input_exit`, `other` |
@@ -495,6 +497,24 @@ Use `on_error` and `on_max_iterations` instead of `notification` when you want a
495
497
496
498
`turn_start`fires at the start of every agent turn (each model call). Anything you contribute via `additional_context` (or plain stdout) is appended as a **transient** system message for that turn only — it is *not* persisted to the session. Use it for fast-moving signals like the date, current git state, or per-turn prompt files. The built-in hooks `add_date`, `add_prompt_files`, `add_git_status`, and `add_git_diff` all target this event.
497
499
500
+
### Turn-End: per-turn finalizer
501
+
502
+
`turn_end`is the symmetric counterpart of `turn_start`. It fires once per turn when the iteration finishes — no matter why. The runtime guarantees the dispatch on every exit path (a normal stop, an error, a hook-driven shutdown, the loop detector, even context cancellation), and it uses `context.WithoutCancel` internally so handlers run to completion on Ctrl+C.
503
+
504
+
The `reason` field classifies the exit:
505
+
506
+
| `reason` | When |
507
+
| --------------- | ---- |
508
+
| `normal` | Model finished cleanly with no follow-up |
509
+
| `continue` | More iterations to come (e.g. tool calls, follow-up message) |
510
+
| `steered` | Drained steered messages prompted a re-entry |
511
+
| `error` | Model call failed (`handleStreamError` exited the loop) |
512
+
| `canceled` | Context was cancelled (e.g. Ctrl+C) |
513
+
| `hook_blocked` | `before_llm_call` or `post_tool_use` denied the call |
514
+
| `loop_detected` | The consecutive-tool-call loop detector terminated the turn |
515
+
516
+
`turn_end`is observational — the result is ignored. Use it to time turns, accumulate per-turn metrics (token usage, tool counts), or notify external observability pipelines symmetrically with `turn_start`.
517
+
498
518
### Before/After-LLM-Call: budget guards and model auditing
499
519
500
520
`before_llm_call` fires immediately before every model call (after `turn_start` has assembled the messages). It cannot contribute context — use `turn_start` for that — but it can **stop the run** by returning `decision: block` (or exit code 2). The built-in `max_iterations` hook implements a hard cap on top of this event.
0 commit comments