Think: public applyToolResult() / applyToolApproval() for answering a parked tool call from the server side · Issue #2636 · cloudflare/agents · GitHub
Skip to content

Think: public applyToolResult() / applyToolApproval() for answering a parked tool call from the server side #2636

Open

Description

@TheDominator54

Versions: @cloudflare/think 0.20.2. Unchanged on 0.21.0 and on the inspected main at fc7f59f, where both methods are private.

Use case

Our app speaks its own protocol to its clients, so it doesn't use Think's chat WebSocket. Two cases park a turn:

  • a tool with no execute (a client-style tool, such as "ask the user", whose answer arrives later over our protocol, possibly after hibernation);
  • a needsApproval tool (Allow or Deny arrives over our protocol).

Think already applies both answers. Its WebSocket handler takes tool-result and tool-approval frames and calls _applyToolResult(toolCallId, output, state?, errorText?) and _applyToolApproval(toolCallId, approved) through _enqueueInteractionApply. Then continueLastTurn() finishes the turn. But those methods are private, and there is no public way to do the same from server code: an RPC method, a webhook, or a queue consumer.

Reproduction

getTools() {
  return {
    ask_user: tool({ inputSchema: z.object({ question: z.string() }) }),            // no execute
    dangerous: tool({ inputSchema: z.object({ what: z.string() }), needsApproval: true,
                      execute: async ({ what }) => ({ ran: what }) })
  };
}
async answer(kind: "result" | "approval") {
  await this.runTurn({ input: kind === "result" ? "ask" : "danger" });
  const part = lastToolPart(this.messages);  // state: "input-available" / "approval-requested"
  // Nothing public: applyToolResult, addToolOutput, addToolResult, applyToolApproval,
  // addToolApprovalResponse are all undefined on the instance.
  const priv = this as unknown as { _applyToolResult(id: string, o: unknown): Promise<void>;
                                    _applyToolApproval(id: string, ok: boolean): Promise<void> };
  if (kind === "result") await priv._applyToolResult(part.toolCallId, { answer: "blue" });
  else await priv._applyToolApproval(part.toolCallId, true);
  await this.continueLastTurn();   // completes: the model gets {answer:"blue"} / the tool runs
}

Why not the existing public paths

  • action({ kind: "durable-pause" }) with approveExecution() and rejectExecution() is public and durable. It models a human approving an action, though. The parked output tells the model that it is awaiting approval, the model keeps writing after the pause, and the continuation runs outside the app's own turn runner. It doesn't fit "deliver this answer to that tool call", and it doesn't cover AI SDK needsApproval tools or tools with no execute.
  • Replace client-defined tool round-trips with server-side RPC delegation #1414 (server-side RPC delegation instead of client round-trips) was closed as "might not do". This request is much smaller: the same entry points the WebSocket uses, made public.

Proposal

/** Apply a result to a pending client-style tool call (as the `tool-result` frame does). */
applyToolResult(toolCallId: string, output: unknown, options?: { state?: "output-error"; errorText?: string; continue?: boolean }): Promise<void>;
/** Approve or deny a pending `needsApproval` call (as the `tool-approval` frame does). */
applyToolApproval(toolCallId: string, approved: boolean, options?: { continue?: boolean }): Promise<void>;

Both should go through _enqueueInteractionApply, so they are ordered with any frame-driven applies. continue: true would call continueLastTurn(), or schedule the same auto-continuation the frames use. Being public also makes the ordering contract part of the API: calling the private methods directly, as we do, skips the queue.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions