mirror of
https://github.com/docker/docker-agent.git
synced 2026-11-04 10:02:15 +00:00
Update agent-schema.json with new hook fields (strict_output, guard phase). Revise hooks documentation with phase ordering, compatibility notes (no-op scripts must exit 0, on_error:block rejected on non-blocking events, customized built-ins are distinct from automatic defaults). Add tool_hook_phases.yaml example demonstrating the full phase pipeline.
569 lines
29 KiB
YAML
569 lines
29 KiB
YAML
#
|
|
# Hooks - one-stop example
|
|
# ==========================================================================
|
|
#
|
|
# Hooks let you observe and shape an agent's lifecycle. This file is the
|
|
# single canonical example covering every supported event AND both handler
|
|
# kinds, so you can see them side by side.
|
|
#
|
|
# Events:
|
|
#
|
|
# pre_tool_use - allow / deny / modify a tool call
|
|
# post_tool_use - inspect a tool call's result (success OR failure)
|
|
# permission_request- programmatically approve / deny tool calls without
|
|
# prompting the user
|
|
# session_start - one-time setup; AdditionalContext PERSISTS in the session
|
|
# user_prompt_submit- runs once per user message, before the first LLM call
|
|
# user_steering_messages_submit
|
|
# - runs each time queued steering messages are drained
|
|
# (mid-turn, after stop, or while idle); AdditionalContext
|
|
# is TRANSIENT, like user_prompt_submit
|
|
# user_followup_submit
|
|
# - runs each time a queued follow-up message starts a fresh
|
|
# turn at end-of-turn; AdditionalContext is TRANSIENT
|
|
# turn_start - per-turn context; AdditionalContext is TRANSIENT
|
|
# turn_end - per-turn finalizer; fires no matter why the turn ended
|
|
# before_llm_call - just before each model call (observability, guardrails)
|
|
# after_llm_call - just after a successful model call
|
|
# session_end - cleanup when the session terminates
|
|
# pre_compact - just before context compaction; can steer or veto
|
|
# subagent_stop - a sub-agent (transferred task / background) finished
|
|
# on_user_input - the agent is waiting on the user
|
|
# stop - the model finished its response
|
|
# notification - errors / warnings emitted by the runtime (catch-all)
|
|
# on_error - structured handler for runtime errors
|
|
# on_max_iterations - structured handler for hitting max_iterations
|
|
# before_compaction - veto a compaction OR supply a custom summary string
|
|
# after_compaction - observe a successful compaction (logging, audits)
|
|
#
|
|
# Handler kinds:
|
|
#
|
|
# command - shell command, JSON Input on stdin, decision returned via
|
|
# stdout JSON or exit codes (2 = block)
|
|
# builtin - in-process Go function registered by the runtime; reuses the
|
|
# `command` field for the builtin's name and the `args` field
|
|
# for parameters. Shipped builtins:
|
|
# add_context (context events; args = Go templates)
|
|
# add_date (turn_start)
|
|
# add_environment_info (session_start)
|
|
# add_prompt_files (turn_start; args = file names)
|
|
# add_git_status (turn_start)
|
|
# add_git_diff (turn_start; args = ["full"] for unified diff)
|
|
# add_directory_listing (session_start)
|
|
# add_user_info (session_start)
|
|
# add_recent_commits (session_start; args = ["<N>"], default 10)
|
|
# max_iterations (before_llm_call; args = ["<N>"], hard stop)
|
|
# snapshot (session_start / turn_start / turn_end /
|
|
# pre_tool_use / post_tool_use / session_end;
|
|
# shadow-git filesystem snapshots)
|
|
#
|
|
# Requirements: the command-style hooks below pipe stdin through `jq` for
|
|
# convenient JSON access. If you don't have jq installed (`brew install jq`
|
|
# on macOS, `apt-get install jq` on Debian/Ubuntu), a hook will exit non-zero
|
|
# and fail-closed pre_tool_use hooks will deny the call; other events
|
|
# follow on_error — use plain `awk`/`sed`/`grep` or any other parser if you prefer.
|
|
#
|
|
# Try these prompts:
|
|
# "Run: echo hello" → allowed
|
|
# "Run: rm -rf /tmp/test" → denied by pre_tool_use
|
|
# "Run: sudo apt update" → denied by pre_tool_use
|
|
# "List files in current dir" → ls gets -h injected (modify)
|
|
#
|
|
# Logs (tail these in another terminal):
|
|
# /tmp/agent-session.log (session_start, session_end)
|
|
# /tmp/agent-prompts.log (user_prompt_submit)
|
|
# /tmp/agent-steering.log (user_steering_messages_submit)
|
|
# /tmp/agent-followups.log (user_followup_submit)
|
|
# /tmp/agent-llm-calls.log (before_llm_call, after_llm_call)
|
|
# /tmp/agent-cost-ledger.csv (after_llm_call: per-call token usage + cost)
|
|
# /tmp/agent-turns.log (turn_end)
|
|
# /tmp/agent-tool-results.log (post_tool_use)
|
|
# /tmp/agent-permissions.log (permission_request)
|
|
# /tmp/agent-compactions.log (pre_compact)
|
|
# /tmp/agent-subagents.log (subagent_stop)
|
|
# /tmp/agent-responses.log (stop)
|
|
# /tmp/agent-notifications.log (notification)
|
|
# /tmp/agent-errors.log (on_error)
|
|
# /tmp/agent-max-iter.log (on_max_iterations)
|
|
# /tmp/agent-compactions.log (before_compaction, after_compaction)
|
|
#
|
|
|
|
agents:
|
|
root:
|
|
model: openai/gpt-4o
|
|
description: One-stop example demonstrating every hook event and both handler kinds.
|
|
instruction: |
|
|
You are a helpful assistant with access to shell and filesystem tools.
|
|
Use them to help the user with their tasks. When asked to run a
|
|
command, use the shell tool directly.
|
|
toolsets:
|
|
- type: shell
|
|
- type: filesystem
|
|
|
|
hooks:
|
|
# ====================================================================
|
|
# PRE-TOOL-USE - control what happens BEFORE a tool runs.
|
|
# Tool-matched (regex on tool name); each entry can deny, allow with
|
|
# a modified input, or stay silent (default allow).
|
|
# ====================================================================
|
|
pre_tool_use:
|
|
# DENY dangerous shell commands.
|
|
- matcher: "shell"
|
|
hooks:
|
|
- type: command
|
|
timeout: 10
|
|
command: |
|
|
INPUT=$(cat)
|
|
CMD=$(echo "$INPUT" | jq -r '.tool_input.cmd // ""')
|
|
if echo "$CMD" | grep -qiE 'rm\s+(-[^\s]*)?\s*-rf|rm\s+(-[^\s]*)?\s*-fr|sudo|mkfs|dd\s+if=|:(\(\)\{|\s*){.*}'; then
|
|
cat <<EOF
|
|
{"hook_specific_output":{"permission_decision":"deny","permission_decision_reason":"🚫 HOOK BLOCKED: dangerous command pattern detected. rm -rf, sudo, mkfs, dd are not allowed."}}
|
|
EOF
|
|
else
|
|
echo '{"continue": true}'
|
|
fi
|
|
|
|
# MODIFY: inject -h into bare `ls` calls for human-readable sizes.
|
|
- matcher: "shell"
|
|
hooks:
|
|
- type: command
|
|
timeout: 10
|
|
command: |
|
|
INPUT=$(cat)
|
|
CMD=$(echo "$INPUT" | jq -r '.tool_input.cmd // ""')
|
|
if echo "$CMD" | grep -qE '^ls(\s|$)' && ! echo "$CMD" | grep -q '\-h'; then
|
|
NEW_CMD=$(echo "$CMD" | sed 's/^ls/ls -h/')
|
|
cat <<EOF
|
|
{"hook_specific_output":{"permission_decision":"allow","updated_input":{"cmd":"$NEW_CMD"}},"system_message":"📝 Hook modified command: added -h for human-readable output"}
|
|
EOF
|
|
fi
|
|
|
|
# ====================================================================
|
|
# POST-TOOL-USE - run AFTER a tool completes (logging, audits).
|
|
# Fires for BOTH success and failure: branch on tool_response.is_error
|
|
# to handle them differently if you only care about one outcome.
|
|
# ====================================================================
|
|
post_tool_use:
|
|
- matcher: "shell"
|
|
hooks:
|
|
- name: summarize shell output
|
|
type: command
|
|
timeout: 10
|
|
working_dir: .
|
|
env:
|
|
HOOK_PROFILE: dev
|
|
on_error: warn
|
|
command: |
|
|
INPUT=$(cat)
|
|
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
|
|
ERROR=$(echo "$INPUT" | jq -r '.tool_error // false')
|
|
OUTPUT_LEN=$(echo "$INPUT" | jq -r '.tool_response // ""' | wc -c | tr -d ' ')
|
|
echo "✅ Post-hook: $TOOL completed (error=$ERROR, output=$OUTPUT_LEN chars, profile=$HOOK_PROFILE)"
|
|
|
|
# ====================================================================
|
|
# PERMISSION-REQUEST - run just before the runtime would prompt the
|
|
# user to approve a tool call. Auto-allow safe read-only commands so
|
|
# the user is never asked; auto-deny anything you don't want to even
|
|
# show in the confirmation UI. Returning nothing falls through to the
|
|
# interactive prompt.
|
|
# ====================================================================
|
|
permission_request:
|
|
- matcher: "shell"
|
|
hooks:
|
|
- type: command
|
|
timeout: 5
|
|
command: |
|
|
INPUT=$(cat)
|
|
CMD=$(echo "$INPUT" | jq -r '.tool_input.cmd // ""')
|
|
echo "[$(date)] permission request: $CMD" >> /tmp/agent-permissions.log
|
|
if echo "$CMD" | grep -qE '^(ls|pwd|cat|echo|date|whoami)( |$)'; then
|
|
echo '{"hook_specific_output":{"permission_decision":"allow","permission_decision_reason":"safe read-only command"}}'
|
|
fi
|
|
|
|
# ====================================================================
|
|
# SESSION-START - runs ONCE when the session begins.
|
|
# Result.AdditionalContext is appended to the session as a SystemMessage
|
|
# and persists across turns and resumes.
|
|
# ====================================================================
|
|
session_start:
|
|
- type: builtin
|
|
command: add_context
|
|
args:
|
|
- "Current session ID: {{ .SessionID }}"
|
|
|
|
# In-process Go function: reports working dir, OS, arch, git status.
|
|
- type: builtin
|
|
command: add_environment_info
|
|
|
|
# Custom command hook: log session start and tell the model where
|
|
# the log lives via additional_context.
|
|
- type: command
|
|
timeout: 10
|
|
command: |
|
|
INPUT=$(cat)
|
|
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
|
|
echo "🚀 Session $SESSION_ID started at $(date)" >> /tmp/agent-session.log
|
|
echo '{"hook_specific_output":{"additional_context":"Session log: /tmp/agent-session.log"}}'
|
|
|
|
# ====================================================================
|
|
# USER-PROMPT-SUBMIT - runs ONCE per user message, after the prompt
|
|
# has been recorded in the session and before the first LLM call.
|
|
# Use it to validate / redact / enrich the prompt, or to inject
|
|
# per-turn context (additional_context becomes a transient system
|
|
# message for that turn).
|
|
# ====================================================================
|
|
user_prompt_submit:
|
|
- type: command
|
|
timeout: 5
|
|
command: |
|
|
INPUT=$(cat)
|
|
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
|
|
PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""')
|
|
echo "[$(date)] [$SESSION_ID] $PROMPT" >> /tmp/agent-prompts.log
|
|
# Example: inject a hint when the user asks about logs.
|
|
if echo "$PROMPT" | grep -qiE '\blogs?\b'; then
|
|
echo '{"hook_specific_output":{"additional_context":"Hook hint: agent log files live under /tmp/agent-*.log"}}'
|
|
fi
|
|
|
|
# ====================================================================
|
|
# USER-STEERING-MESSAGES-SUBMIT - runs each time the runtime drains
|
|
# the steering queue, i.e. messages the user submitted while the
|
|
# agent was already working (mid-turn, after the model stopped, or
|
|
# while idle before the first LLM call). The drained messages arrive
|
|
# as a JSON array in the steering_messages field. This is the
|
|
# steering-queue analogue of user_prompt_submit: you can block the
|
|
# run (decision=block / continue=false / exit code 2) or inject
|
|
# additional_context, which becomes a TRANSIENT system message for
|
|
# the steered turn only (never persisted).
|
|
# ====================================================================
|
|
user_steering_messages_submit:
|
|
- type: command
|
|
timeout: 5
|
|
command: |
|
|
INPUT=$(cat)
|
|
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
|
|
COUNT=$(echo "$INPUT" | jq -r '.steering_messages | length')
|
|
echo "$INPUT" | jq -r '.steering_messages[]' \
|
|
| sed "s/^/[$(date)] [$SESSION_ID] steer: /" >> /tmp/agent-steering.log
|
|
# Example: nudge the model to acknowledge mid-flight steering.
|
|
if [ "$COUNT" -gt 0 ]; then
|
|
echo '{"hook_specific_output":{"additional_context":"The user sent new instructions while you were working — re-read the latest user messages and adjust course before continuing."}}'
|
|
fi
|
|
|
|
# ====================================================================
|
|
# USER-FOLLOWUP-SUBMIT - runs each time the runtime dequeues a
|
|
# follow-up message at the end of a turn and starts a fresh turn
|
|
# for it. Follow-ups are user messages queued for end-of-turn
|
|
# processing (the FollowUp API / queue), distinct from mid-turn
|
|
# steering: the model sees them as fresh input. The follow-up text
|
|
# is in the prompt field. Like user_prompt_submit, you can block
|
|
# the run (decision=block / continue=false / exit code 2) or inject
|
|
# additional_context, which becomes a TRANSIENT system message for
|
|
# the follow-up turn only (never persisted).
|
|
# ====================================================================
|
|
user_followup_submit:
|
|
- type: command
|
|
timeout: 5
|
|
command: |
|
|
INPUT=$(cat)
|
|
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
|
|
echo "$INPUT" | jq -r '.prompt' \
|
|
| sed "s/^/[$(date)] [$SESSION_ID] followup: /" >> /tmp/agent-followups.log
|
|
|
|
# ====================================================================
|
|
# TURN-START - runs at the start of every model call.
|
|
# Result.AdditionalContext is spliced after the invariant cache
|
|
# checkpoint and is NOT persisted, so per-turn signals refresh every
|
|
# turn without bloating the message history.
|
|
# ====================================================================
|
|
turn_start:
|
|
# Built-in: prepends "Today's date: YYYY-MM-DD".
|
|
- type: builtin
|
|
command: add_date
|
|
|
|
# Built-in: read each file under the working dir and join its
|
|
# contents into a system message. The `args` field carries per-hook
|
|
# parameters that builtin handlers receive directly.
|
|
- type: builtin
|
|
command: add_prompt_files
|
|
args:
|
|
- GUIDELINES.md
|
|
- PROJECT.md
|
|
|
|
# ====================================================================
|
|
# TURN-END - runs ONCE per turn after the iteration 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 .reason field:
|
|
#
|
|
# normal - model finished cleanly, no follow-up
|
|
# continue - more iterations to come (e.g. tool calls)
|
|
# steered - drained steered messages prompted a re-entry
|
|
# error - model call failed (handleStreamError)
|
|
# canceled - context cancellation (Ctrl+C, parent ctx done)
|
|
# hook_blocked - before_llm_call or post_tool_use signalled stop
|
|
# loop_detected - degenerate consecutive-tool-call loop
|
|
#
|
|
# Observational; the result is ignored. Use it to time turns,
|
|
# accumulate per-turn metrics (token usage, tool counts), or notify
|
|
# external observability pipelines.
|
|
# ====================================================================
|
|
turn_end:
|
|
- type: command
|
|
timeout: 5
|
|
command: |
|
|
INPUT=$(cat)
|
|
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
|
|
AGENT=$(echo "$INPUT" | jq -r '.agent_name // "unknown"')
|
|
REASON=$(echo "$INPUT" | jq -r '.reason // "unknown"')
|
|
echo "[$(date)] [←] $SESSION_ID $AGENT turn ended (reason=$REASON)" >> /tmp/agent-turns.log
|
|
|
|
# ====================================================================
|
|
# BEFORE-LLM-CALL - fires just before every model invocation, AFTER
|
|
# turn_start has assembled the messages slice. Use for observability
|
|
# / cost guardrails / auditing without contributing system messages
|
|
# (turn_start is the right place for the latter).
|
|
# ====================================================================
|
|
before_llm_call:
|
|
- type: command
|
|
timeout: 5
|
|
command: |
|
|
INPUT=$(cat)
|
|
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
|
|
echo "[$(date)] [→] $SESSION_ID llm call starting" >> /tmp/agent-llm-calls.log
|
|
|
|
# ====================================================================
|
|
# AFTER-LLM-CALL - fires just after a successful model call. The
|
|
# assistant text content arrives via stop_response (matching the
|
|
# stop event's payload). Failed calls fire on_error instead and
|
|
# skip this event.
|
|
#
|
|
# The payload also carries this call's token usage in .usage and its
|
|
# computed USD cost in .cost. .cost is ABSENT for an unpriced model
|
|
# (test with `has("cost")`) and an explicit 0 for a priced free call,
|
|
# so a present cost is authoritative without checking usage. That is
|
|
# everything a sidecar cost ledger needs — no event-channel wiring.
|
|
# after_llm_call also fires for sub-session turns (each with its own
|
|
# session_id), so summing .cost is the full spend for the run.
|
|
# ====================================================================
|
|
after_llm_call:
|
|
- type: command
|
|
timeout: 5
|
|
command: |
|
|
INPUT=$(cat)
|
|
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
|
|
LEN=$(echo "$INPUT" | jq -r '.stop_response // ""' | wc -c | tr -d ' ')
|
|
echo "[$(date)] [←] $SESSION_ID llm call complete, content=$LEN chars" >> /tmp/agent-llm-calls.log
|
|
# Per-call cost ledger: timestamp, session, model, tokens, cost.
|
|
echo "$INPUT" | jq -r '[
|
|
(now | todateiso8601), .session_id, .model_id,
|
|
(.usage.input_tokens // 0), (.usage.output_tokens // 0),
|
|
(if has("cost") then (.cost | tostring) else "unpriced" end)
|
|
] | @csv' >> /tmp/agent-cost-ledger.csv
|
|
|
|
# ====================================================================
|
|
# SESSION-END - cleanup when the session terminates.
|
|
# ====================================================================
|
|
session_end:
|
|
- type: command
|
|
timeout: 10
|
|
command: |
|
|
INPUT=$(cat)
|
|
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
|
|
REASON=$(echo "$INPUT" | jq -r '.reason // "unknown"')
|
|
echo "👋 Session $SESSION_ID ended (reason: $REASON) at $(date)" >> /tmp/agent-session.log
|
|
|
|
# ====================================================================
|
|
# PRE-COMPACT - runs just before the runtime compacts the transcript.
|
|
# source is one of: manual (user /compact), auto (proactive threshold),
|
|
# overflow (context-overflow recovery), tool_overflow (proactive after
|
|
# tool results pushed past threshold). Return additional_context to
|
|
# steer the summary without editing the agent's instruction; return
|
|
# decision=block to cancel compaction entirely.
|
|
# ====================================================================
|
|
pre_compact:
|
|
- type: command
|
|
timeout: 5
|
|
command: |
|
|
INPUT=$(cat)
|
|
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
|
|
SOURCE=$(echo "$INPUT" | jq -r '.source // "unknown"')
|
|
echo "[$(date)] $SESSION_ID compacting (source=$SOURCE)" >> /tmp/agent-compactions.log
|
|
echo '{"hook_specific_output":{"additional_context":"When summarizing, please preserve any TODOs, file paths, and command outputs verbatim."}}'
|
|
|
|
# ====================================================================
|
|
# SUBAGENT-STOP - fires when a sub-agent (transfer_task, background
|
|
# agent, skill sub-session) completes. Runs against the PARENT
|
|
# agent's hooks executor, so handlers placed on the orchestrator
|
|
# see every child completion in one place.
|
|
# ====================================================================
|
|
subagent_stop:
|
|
- type: command
|
|
timeout: 5
|
|
command: |
|
|
INPUT=$(cat)
|
|
AGENT=$(echo "$INPUT" | jq -r '.agent_name // "unknown"')
|
|
CHILD=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
|
|
PARENT=$(echo "$INPUT" | jq -r '.parent_session_id // "unknown"')
|
|
LEN=$(echo "$INPUT" | jq -r '.stop_response // ""' | wc -c | tr -d ' ')
|
|
echo "[$(date)] $AGENT child=$CHILD parent=$PARENT len=$LEN" >> /tmp/agent-subagents.log
|
|
|
|
# ====================================================================
|
|
# ON-USER-INPUT - the agent is waiting on the user.
|
|
# Useful for desktop notifications.
|
|
# ====================================================================
|
|
on_user_input:
|
|
- type: command
|
|
command: |
|
|
# macOS only — adapt for Linux notify-send / Windows toast.
|
|
osascript -e 'display notification "ready!" with title "docker-agent"' 2>/dev/null || true
|
|
|
|
# ====================================================================
|
|
# STOP - runs when the model finishes a response.
|
|
# Receives the response text via the stop_response field.
|
|
# ====================================================================
|
|
stop:
|
|
- type: command
|
|
timeout: 10
|
|
command: |
|
|
INPUT=$(cat)
|
|
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
|
|
LEN=$(echo "$INPUT" | jq -r '.stop_response // ""' | wc -c | tr -d ' ')
|
|
echo "[$(date)] Session $SESSION_ID - response length: $LEN chars" >> /tmp/agent-responses.log
|
|
|
|
# ====================================================================
|
|
# NOTIFICATION - runs when the runtime emits a warning or error
|
|
# (max iterations, model errors, ...). Forward to Slack, Teams, or
|
|
# a logfile.
|
|
# ====================================================================
|
|
notification:
|
|
- type: command
|
|
timeout: 10
|
|
command: |
|
|
INPUT=$(cat)
|
|
LEVEL=$(echo "$INPUT" | jq -r '.notification_level // "unknown"')
|
|
MESSAGE=$(echo "$INPUT" | jq -r '.notification_message // "no message"')
|
|
echo "[$(date)] [$LEVEL] $MESSAGE" >> /tmp/agent-notifications.log
|
|
|
|
# ====================================================================
|
|
# ON-ERROR - fires alongside `notification` (level=error) but ONLY
|
|
# for runtime errors. Use this when you want a dedicated entry point
|
|
# for errors without filtering on .notification_level inside the
|
|
# handler. Both events fire for the same condition; you can have
|
|
# either or both.
|
|
# ====================================================================
|
|
on_error:
|
|
- type: command
|
|
timeout: 10
|
|
command: |
|
|
INPUT=$(cat)
|
|
MESSAGE=$(echo "$INPUT" | jq -r '.notification_message // "no message"')
|
|
echo "[$(date)] error: $MESSAGE" >> /tmp/agent-errors.log
|
|
|
|
# ====================================================================
|
|
# ON-MAX-ITERATIONS - fires alongside `notification` (level=warning)
|
|
# but ONLY when the agent hits its iteration cap. Useful for
|
|
# metrics pipelines that want to count runaway sessions without
|
|
# parsing the notification text.
|
|
# ====================================================================
|
|
on_max_iterations:
|
|
- type: command
|
|
timeout: 10
|
|
command: |
|
|
INPUT=$(cat)
|
|
MESSAGE=$(echo "$INPUT" | jq -r '.notification_message // "no message"')
|
|
echo "[$(date)] max-iterations: $MESSAGE" >> /tmp/agent-max-iter.log
|
|
|
|
# ====================================================================
|
|
# BEFORE-COMPACTION - fires immediately before the runtime compacts
|
|
# the session. The Input carries:
|
|
# .input_tokens - current session input-token count
|
|
# .output_tokens - current session output-token count
|
|
# .context_limit - the model's context-window size, when known
|
|
# .compaction_reason - "threshold" (proactive 90% trigger),
|
|
# "overflow" (post-overflow auto-recovery),
|
|
# or "manual" (user-invoked /compact).
|
|
#
|
|
# Two ways to influence the runtime:
|
|
#
|
|
# 1. Veto: exit with code 2 (or return permission_decision="deny").
|
|
# The runtime skips compaction entirely. CAUTION: vetoing during
|
|
# compaction_reason="overflow" leaves the session unable to make
|
|
# progress — gate by reason if you need this.
|
|
#
|
|
# 2. Replace: return hookSpecificOutput.summary to supply a custom
|
|
# summary string. The runtime applies it verbatim and skips the
|
|
# LLM-based summarization. Useful for non-LLM strategies (drop
|
|
# oldest tool results, dedupe, stub-out long blobs, ...).
|
|
# ====================================================================
|
|
before_compaction:
|
|
- type: command
|
|
timeout: 30
|
|
command: |
|
|
INPUT=$(cat)
|
|
REASON=$(echo "$INPUT" | jq -r '.compaction_reason // "manual"')
|
|
INPUT_TOK=$(echo "$INPUT" | jq -r '.input_tokens // 0')
|
|
OUTPUT_TOK=$(echo "$INPUT" | jq -r '.output_tokens // 0')
|
|
LIMIT=$(echo "$INPUT" | jq -r '.context_limit // 0')
|
|
echo "[$(date)] [→] compacting (reason=$REASON, in=$INPUT_TOK, out=$OUTPUT_TOK, ctx=$LIMIT)" \
|
|
>> /tmp/agent-compactions.log
|
|
# Pure observability — fall through to the LLM-based default.
|
|
echo '{"continue": true}'
|
|
|
|
# ====================================================================
|
|
# AFTER-COMPACTION - fires after a successful compaction has applied
|
|
# a summary to the session. The Input carries the produced summary
|
|
# text in .summary alongside the *pre-compaction* .input_tokens /
|
|
# .output_tokens (what was summarized) so handlers can naturally
|
|
# express "compacted from X to Y". Purely observational — output is
|
|
# ignored.
|
|
# ====================================================================
|
|
after_compaction:
|
|
- type: command
|
|
timeout: 10
|
|
command: |
|
|
INPUT=$(cat)
|
|
REASON=$(echo "$INPUT" | jq -r '.compaction_reason // "manual"')
|
|
BEFORE_IN=$(echo "$INPUT" | jq -r '.input_tokens // 0')
|
|
BEFORE_OUT=$(echo "$INPUT" | jq -r '.output_tokens // 0')
|
|
LEN=$(echo "$INPUT" | jq -r '.summary // ""' | wc -c | tr -d ' ')
|
|
echo "[$(date)] [✓] compacted (reason=$REASON, ${BEFORE_IN}+${BEFORE_OUT} tokens → ${LEN} chars summary)" \
|
|
>> /tmp/agent-compactions.log
|
|
|
|
# ====================================================================
|
|
# CUSTOM-SUMMARY EXAMPLE (commented out) - demonstrates the non-LLM
|
|
# replacement path. Returning a non-empty hookSpecificOutput.summary
|
|
# makes the runtime apply that string verbatim and skip the LLM call
|
|
# entirely. Useful for:
|
|
#
|
|
# - deterministic / fast strategies (drop oldest tool results,
|
|
# dedupe identical tool calls, stub long blob attachments);
|
|
# - regulated environments where shipping conversation history to
|
|
# a summarization model isn't permitted;
|
|
# - testing: pin a known summary string to make compaction
|
|
# deterministic in CI.
|
|
#
|
|
# If multiple before_compaction hooks return a summary, the first
|
|
# non-empty one in config order wins (hooks run concurrently but
|
|
# the result slots are indexed, so the verdict is deterministic).
|
|
#
|
|
# Uncomment to replace the observational hook above with a custom
|
|
# strategy. Note: only one before_compaction handler block is
|
|
# supported per agent in YAML; merge your observability logic into
|
|
# the same handler if you also want logging.
|
|
#
|
|
# before_compaction:
|
|
# - type: command
|
|
# timeout: 5
|
|
# command: |
|
|
# INPUT=$(cat)
|
|
# cat <<'JSON'
|
|
# {
|
|
# "hookSpecificOutput": {
|
|
# "hookEventName": "before_compaction",
|
|
# "summary": "Conversation summarized by the deterministic strategy. Latest user request and tool results preserved verbatim."
|
|
# }
|
|
# }
|
|
# JSON
|