Summary
Add an agent:turn:end internal hook event that fires once after a complete agent turn cycle (user message → agent processing → all tool calls → delivery) has finished.
Motivation
The gateway already tracks turn completion internally via AcpSessionManager.recordTurnCompletion(), including duration, success/failure, and error codes. This state is used for observability snapshots but is not exposed to the hook system, leaving a gap between the sub-turn cognitive events (agent:thinking:*, agent:response:*, agent:tool:*) and session-level events (session:end).
There is currently no way for a hook to reliably know when an entire turn is done. The closest alternatives don't fit:
message:sent fires per outbound message — multiple times if the agent sends several messages in one turn
agent_end / agent:response:end fires when LLM processing finishes, before delivery, and potentially multiple times per turn in multi-step tool-call loops
session:end fires when the entire session terminates, not per-turn
Use Cases
- Post-turn memory extraction: Summarize or persist context after each turn without blocking the agent loop
- Session pruning: Implement summary-and-forget strategies that trigger after turn completion, when it's safe to modify context
- Turn-level analytics: Track turn duration, success rates, and tool usage patterns per turn
- Workflow integration: Signal external systems (e.g., orchestrators) that a unit of work is complete
Proposed Event
Following the existing agent:*:start/end naming convention established in #7724:
{
type: 'agent',
action: 'turn:end',
sessionKey: string,
timestamp: Date,
messages: string[],
context: {
success: boolean,
durationMs: number,
errorCode?: string,
}
}
Implementation Notes
The natural hook point already exists in the codebase. AcpSessionManager.recordTurnCompletion() is called on both the success and error paths after the turn loop completes:
// Success path
this.recordTurnCompletion({ startedAt: turnStartedAt });
await this.setSessionState({ cfg, sessionKey, state: "idle" });
// Error path
this.recordTurnCompletion({ startedAt: turnStartedAt, errorCode: acpError.code });
await this.setSessionState({ cfg, sessionKey, state: "error" });
This method already computes durationMs and tracks completed vs failed counts. Emitting a triggerInternalHook call here would surface an already-tracked lifecycle moment without introducing new state management.
Relationship to Existing Work
Summary
Add an
agent:turn:endinternal hook event that fires once after a complete agent turn cycle (user message → agent processing → all tool calls → delivery) has finished.Motivation
The gateway already tracks turn completion internally via
AcpSessionManager.recordTurnCompletion(), including duration, success/failure, and error codes. This state is used for observability snapshots but is not exposed to the hook system, leaving a gap between the sub-turn cognitive events (agent:thinking:*,agent:response:*,agent:tool:*) and session-level events (session:end).There is currently no way for a hook to reliably know when an entire turn is done. The closest alternatives don't fit:
message:sentfires per outbound message — multiple times if the agent sends several messages in one turnagent_end/agent:response:endfires when LLM processing finishes, before delivery, and potentially multiple times per turn in multi-step tool-call loopssession:endfires when the entire session terminates, not per-turnUse Cases
Proposed Event
Following the existing
agent:*:start/endnaming convention established in #7724:Implementation Notes
The natural hook point already exists in the codebase.
AcpSessionManager.recordTurnCompletion()is called on both the success and error paths after the turn loop completes:This method already computes
durationMsand trackscompletedvsfailedcounts. Emitting atriggerInternalHookcall here would surface an already-tracked lifecycle moment without introducing new state management.Relationship to Existing Work
agent:thinking:*,agent:response:*,agent:tool:*).agent:turn:endoperates at a higher abstraction level — one event per complete turn rather than per LLM call or tool invocation. Complementary, not overlapping.onPostTurnas part of a broader lifecycle framework using a different hook architecture. This request is narrower — a single event using the currentHOOK.md/triggerInternalHooksystem.session:endcovers session lifecycle.agent:turn:endcovers turn lifecycle within a session.