Skip to content

Deferred tool results — let tool handlers suspend a turn and resume via submitToolResult() #1552

@karanbirsingh

Description

@karanbirsingh

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.call comes 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.call comes 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions