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

    area:sessionsSession management, resume, history, session picker, and session state
    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions