Copilot SDK Agent Stop Hooks and Graceful Shutdown Handling: Implementation Guide
The Copilot SDK provides a session-lifecycle agent-stop hook that triggers when the top-level agent finishes its turn, allowing you to run cleanup logic or request a follow-up turn before the session idles or shuts down.
The github/copilot-sdk repository defines a cross-language framework for managing AI agent sessions, including a critical agent stop hook that enables graceful shutdown handling. Understanding how to implement this hook ensures your applications can cleanly release resources, flush telemetry, and perform final operations before termination. This guide examines the hook's architecture, contract, and implementation across all supported languages.
Architecture of the Agent Stop Hook
The agent stop hook sits between the core runtime and language bindings, intercepting the session lifecycle at a precise moment: immediately after the top-level agent completes its current turn but before the session transitions to an idle state.
Unlike onSessionEnd callbacks that run after session destruction, this hook executes while the session remains alive. In rust/src/hooks.rs, the runtime defines AgentStopHookInput and AgentStopHookOutput structures that serve as the cross-language contract. The session stays active during hook execution, allowing handlers to spin up new turns or perform cleanup that requires an active session context.
Hook Contract and Data Structures
The hook operates on a strict contract defined across language bindings. All implementations utilize two primary structures: AgentStopHookInput containing request metadata, and AgentStopHookOutput containing optional follow-up actions.
The input structure carries these critical fields:
requestId– Identifier of the turn that is endingsessionId– Current session identifier for correlationcancelSignal– Signal that the runtime sets if shutdown is forced
The output structure contains:
result– OptionalAgentActionthat requests a follow-up turn; if omitted, the session idles
When the runtime receives a stop-hook response, it awaits the handler completion while respecting cancellation tokens. If the handler returns an AgentAction in the result field, the runtime immediately starts a new turn, extending the session. If the handler returns empty output or throws, the runtime proceeds to normal shutdown.
Graceful Shutdown Flow
The SDK implements a four-phase graceful shutdown sequence that protects against data loss in long-running services:
- Signal Interception – The host process sends
SIGTERMorCTRL-C, which the SDK's internal event loop intercepts - Hook Queuing – The runtime queues the agent-stop hook rather than terminating immediately
- Handler Execution – User-registered handlers run final logging, flush pending writes to
session-fs, or schedule last agent turns - Resource Disposal – After the hook resolves or a timeout expires, the SDK tears down the session, disposes of adapters (FS, telemetry, RPC), and exits
This design guarantees that background workers, database connections, and telemetry buffers receive deterministic cleanup time before process termination.
Implementation Examples by Language
Python
In python/copilot/session.py, the SDK exposes AgentStopHookInput and AgentStopHookOutput classes with a registration API on the Session object.
import asyncio
from copilot.session import Session, AgentStopHookInput, AgentStopHookOutput
async def on_agent_stop(input: AgentStopHookInput) -> AgentStopHookOutput:
# Flush pending telemetry and close DB connections
await my_db.flush()
await my_telemetry.flush()
# No follow‑up turn needed → return empty output
return AgentStopHookOutput()
session = Session()
session.register_agent_stop_hook(on_agent_stop)
# Run the session; the SDK will invoke the hook on graceful shutdown
asyncio.run(session.start())
Node.js
The TypeScript definitions in nodejs/src/types.ts provide AgentStopHookInput, AgentStopHookOutput, and the AgentStopHandler type for async implementations.
import { Session, AgentStopHookInput, AgentStopHookOutput } from "./src/types";
async function onAgentStop(input: AgentStopHookInput): Promise<AgentStopHookOutput> {
await db.flush();
await telemetry.flush();
// Returning undefined signals no extra turn
return {};
}
const session = new Session();
session.registerAgentStopHook(onAgentStop);
await session.start(); // SDK calls the hook when process receives SIGTERM
Rust
The core runtime in rust/src/hooks.rs defines the fundamental AgentStopHookInput and AgentStopHookOutput structs used by all language bindings.
use copilot_sdk::hooks::{AgentStopHookInput, AgentStopHookOutput};
async fn on_agent_stop(input: AgentStopHookInput) -> AgentStopHookOutput {
// Perform graceful cleanup here
my_db.flush().await;
my_telemetry.flush().await;
AgentStopHookOutput::default() // No extra turn
}
#[tokio::main]
async fn main() {
let mut session = copilot_sdk::Session::new();
session.register_agent_stop_hook(on_agent_stop);
session.start().await;
}
Go
The Go bindings in go/types.go expose AgentStopHookInput and AgentStopHookOutput structs with typical Go error handling patterns.
func onAgentStop(input *AgentStopHookInput) (*AgentStopHookOutput, error) {
// Flush resources
db.Flush()
telemetry.Flush()
// Return nil to indicate no further turn
return &AgentStopHookOutput{}, nil
}
func main() {
sess := copilot.NewSession()
sess.RegisterAgentStopHook(onAgentStop)
sess.Start(context.Background())
}
Java
Java implementations utilize the AgentStopHandler interface defined in java/src/main/java/com/github/copilot/rpc/AgentStopHandler.java, returning CompletableFuture for async operations.
public class MyAgentStopHandler implements AgentStopHandler {
@Override
public CompletableFuture<AgentStopHookOutput> handle(AgentStopHookInput input) {
return CompletableFuture.supplyAsync(() -> {
db.flush();
telemetry.flush();
return new AgentStopHookOutput(); // no extra turn
});
}
}
// Register:
Session session = new Session();
session.registerAgentStopHook(new MyAgentStopHandler());
session.start();
.NET
The C# implementation in dotnet/src/Types.cs defines AgentStopHookInput and AgentStopHookOutput structs with IAgentStopHandler interface compliance.
public class MyAgentStopHandler : IAgentStopHandler {
public async Task<AgentStopHookOutput> HandleAsync(AgentStopHookInput input) {
await db.FlushAsync();
await telemetry.FlushAsync();
return new AgentStopHookOutput(); // no follow‑up turn
}
}
// Usage:
var session = new Session();
session.RegisterAgentStopHook(new MyAgentStopHandler());
await session.StartAsync();
Key Source Files
| Language | File | Purpose |
|---|---|---|
| Rust | rust/src/hooks.rs |
Core hook definitions and runtime invocation logic |
| Python | python/copilot/session.py |
Exposes AgentStopHookInput/Output and registration API |
| Node.js | nodejs/src/types.ts |
TypeScript definitions and hook registration helpers |
| Go | go/types.go |
Structs and handler interface for stop hook |
| Java | java/src/main/java/com/github/copilot/rpc/AgentStopHandler.java |
Java-side handler interface |
| .NET | dotnet/src/Types.cs |
C# structs and handler contract |
| Docs | docs/hooks/session-lifecycle.md | Conceptual description and usage guidelines |
Summary
- The agent stop hook triggers after the top-level agent completes its turn but before the session idles, providing a deterministic cleanup point
- Handlers receive
AgentStopHookInputwithrequestId,sessionId, andcancelSignal, and returnAgentStopHookOutputoptionally containing a follow-upAgentAction - The graceful shutdown flow queues the hook upon
SIGTERM, allowing resource flushing before the runtime disposes of adapters and exits - Language bindings in
python/copilot/session.py,nodejs/src/types.ts,rust/src/hooks.rs,go/types.go,java/src/main/java/com/github/copilot/rpc/AgentStopHandler.java, anddotnet/src/Types.csmaintain semantic parity with the core Rust runtime
Frequently Asked Questions
What triggers the agent-stop hook in the Copilot SDK?
The hook triggers when the top-level agent finishes its current turn and the session is about to transition to an idle state. According to the SDK architecture implemented in rust/src/hooks.rs, this occurs before the session terminates, distinguishing it from cleanup hooks that run after session destruction.
How does the agent-stop hook differ from onSessionEnd callbacks?
While onSessionEnd runs after the session has already been destroyed, the agent-stop hook executes while the session remains active. This distinction allows the stop hook to request follow-up turns, access session-scoped resources, and perform operations that require an active session context.
Can the agent-stop hook prevent session termination?
Yes. If the hook returns an AgentAction in the result field of AgentStopHookOutput, the runtime immediately starts a new turn instead of idling, effectively extending the session. If the hook returns an empty result or throws an exception, the session proceeds to shutdown.
How should I handle cancellation signals in the stop hook?
The cancelSignal field in AgentStopHookInput indicates when the runtime forces shutdown. Your handler should check this signal and expedite cleanup, potentially skipping non-critical operations. The runtime respects cancellation tokens during hook execution, but handlers should still attempt minimal cleanup (flushing buffers, closing connections) before yielding control.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →