-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Description
Describe the feature or problem you'd like to solve
Custom tool handlers must resolve before the tool.call JSON-RPC response is sent — there's no way to signal "I'll have the result later."
Proposed solution
If tool.call can accept a deferred result, CLI can remain in a suspended state (e.g. further messages are queued) until client submits tool results later.
This enables long-running tools (human-in-the-loop approvals, etc) especially in durable orchestration where the CLI cannot be kept running for long periods of time. Today these scenarios require something like the following workaround:
tool.callcomes in- client returns 'the tool call ABC has been processed; you will be notified when it completes'
- (while waiting for long-running tool) client may destroy and resume CLI session
- client resumes CLI session and sends message
tool call ABC has results; call get_deferred_result(ABC) - CLI calls
get_deferred_result(ABC)so that the result goes through same tool result flow
This adds extra assistant turns / token usage. It would be useful to have something like:
tool.callcomes in- client returns something like
{ 'result': 'deferred' } - (while waiting for long-running tool) client may destroy and resume CLI session
- client resumes CLI session with some new payload
{ 'type': 'submit-deferred-tool-results', 'toolCallId': '...', 'result': { ... } } - CLI immediately interprets the results and continues the session
This would simplify the back and forth and does not rely on LLM intelligently knowing to call get_deferred_result. In a durable/serverless scenario the CLI may be stopped and resumed on a different worker, so it is challenging to hold the tool.call JSON-RPC call open. The deferred result type lets the CLI checkpoint the pending tool call into session state so it survives destroy/resume.
Example prompts or workflows
Sample workflows include human-in-the-loop, webhooks, or any long-running tool call in an environment where we expect the CLI session may be destroyed and resumed during the tool execution (e.g. in durable / serverless environments).
This is pseudocode for example SDK code this might enable:
import { CopilotClient, defineTool } from "@github/copilot-sdk";
const client = new CopilotClient();
// 1. Create session with a deferred-capable tool
const session = await client.createSession({
model: "claude-sonnet-4.5",
tools: [
defineTool("request_approval", {
description: "Request human approval for an action.",
parameters: { type: "object", properties: { reason: { type: "string" } }, required: ["reason"] },
handler: async ({ reason }, invocation) => {
// Kick off external process, pass toolCallId for correlation
await sendSlackApprovalRequest(invocation.toolCallId, reason);
return { resultType: "deferred" };
},
}),
],
});
// 2. Send prompt — LLM calls the tool, gets deferred, turn is parked
await session.send({ prompt: "Get approval to deploy v2.0 to production." });
// 3. Save IDs, tear down — worker can scale to zero
const sessionId = session.sessionId;
await session.destroy();
await client.stop();
// ─── time passes: minutes, hours, days ─────────────────────────────
// Slack webhook fires on a completely different worker/container.
// It has sessionId + toolCallId from the original request.
// 4. New worker resumes the session and submits the result
const client2 = new CopilotClient();
const resumed = await client2.resumeSession(sessionId);
await resumed.submitToolResult(toolCallId, {
textResultForLlm: "Approved by @alice at 3:42 PM",
resultType: "success",
});
// CLI resumes the LLM turn with a proper tool_result → assistant responds
await resumed.destroy();
await client2.stop();Additional context
No response