| import { |
| decodeCanonicalToolResultContent, |
| projectAgentSwarmResult, |
| projectToolActivityArgs, |
| type CreateSandboxBoundaryRequest, |
| type ExecutionBoundary, |
| type SandboxBoundaryDecision, |
| type SandboxBoundaryExpansion, |
| type SandboxBoundaryRequest, |
| type SandboxBoundarySettlement, |
| type SettleSandboxBoundaryRequest, |
| } from '@maka/core'; |
| import { ToolOutcomeUnknownError } from '@maka/core/events'; |
| import type { |
| SandboxBoundaryDecisionAckEvent, |
| SandboxBoundaryRequestEvent, |
| SessionEvent, |
| SandboxDenialSignal, |
| ToolActivityKind, |
| ToolOutputStream, |
| ToolResultContent, |
| ToolResultEvent, |
| ToolStartEvent, |
| ToolUncertainOutcomeSignal, |
| UserQuestionRequestEvent, |
| } from '@maka/core/events'; |
| import type { ToolCallMessage, ToolResultMessage } from '@maka/core/session'; |
| import type { |
| HostedInteractionBridge, |
| HostedUserQuestionAnswer, |
| HostedUserQuestionSettlement, |
| } from '@maka/core/backend-types'; |
| import type { AgentSpec } from '@maka/core/runtime-inputs'; |
| import type { PermissionMode, ToolCategory, ToolExecutionFacts } from '@maka/core/permission'; |
| import type { RuntimeExecutionConnection } from '@maka/core/llm-connections'; |
| import type { |
| UserQuestion, |
| UserQuestionResponse, |
| UserQuestionResult, |
| } from '@maka/core/user-question'; |
| import { computerUseApprovalSummary } from '@maka/core'; |
| import type { SessionHeader } from '@maka/core/session'; |
| import type { ToolInvocationRecord } from '@maka/core/usage-stats/types'; |
| import type { EffectiveOrchestration } from '@maka/core/orchestration'; |
| import { redactSecrets } from '@maka/core/redaction'; |
| import { TOOL_BOUNDARY_PROTOCOL_V1, type RuntimeEvent } from '@maka/core'; |
| |
| import { recordToolArtifactsSafely, type ToolArtifactRecorder } from './tool-artifacts.js'; |
| import { createToolOutputDeltaEmitter } from './tool-output-delta.js'; |
| import { truncateToolOutput } from './tool-output.js'; |
| import { stableHash } from './request-shape.js'; |
| import { classifyError } from './provider-error-classification.js'; |
| import type { RunTraceLike } from './run-trace.js'; |
| import { TurnScopedAwaitRegistry } from './turn-scoped-await-registry.js'; |
| import { jsonValue } from './tool-result-output.js'; |
| import type { ToolResultOutput } from './model-protocol.js'; |
| import { |
| buildToolOperationId, |
| canonicalToolArgsHash, |
| type RuntimeCommitSink, |
| type ToolRecoveryMode, |
| } from './runtime-commit-sink.js'; |
| import { ChildAgentRunLimiter } from './child-agent-run-limiter.js'; |
| import type { AgentProfile } from './agent-catalog.js'; |
| import type { SubagentExecutionRef } from './subagent-execution.js'; |
| import { sandboxErrorMetadata, serializeSandboxError } from './sandbox/errors.js'; |
| import { normalizeSandboxBoundaryExpansion } from './sandbox-boundary-path.js'; |
| import { |
| RuntimeInteractionAdmissionRejectedError, |
| RuntimeInteractionClosedError, |
| RuntimeInteractionFailStopError, |
| RuntimeInteractionInvariantError, |
| type RuntimeInteractionClosureReason, |
| type RuntimeUserQuestionClosureReason, |
| } from './interaction-authority.js'; |
| |
| export interface ResolvedMakaToolCall { |
| tool: MakaTool; |
| turnId: string; |
| stepId?: string; |
| toolCallId: string; |
| input: unknown; |
| providerOptions?: Record<string, unknown>; |
| abortSignal: AbortSignal; |
| eventSink: DurableSessionEventSink; |
| } |
| |
| export interface DurableSessionEventSink { |
| push(event: SessionEvent): void; |
| pushAndWaitUntilConsumed(event: SessionEvent): Promise<void>; |
| } |
| |
| export interface ToolSettlement { |
| result: unknown; |
| modelOutput: ToolResultOutput; |
| } |
| |
| export interface MakaTool<P = any, R = unknown> { |
| /** Canonical (Claude-SDK-style) name. Pi adapter translates to canonical. */ |
| name: string; |
| /** Human-readable description shown to the model. */ |
| description: string; |
| /** Zod schema describing the tool's argument shape. */ |
| parameters: unknown; |
| /** Optional UI display name. */ |
| displayName?: string; |
| /** Stable semantic category used by UI presentation; never carries styling. */ |
| activityKind?: ToolActivityKind; |
| /** Optional trusted category override for custom tools. */ |
| categoryHint?: ToolCategory; |
| /** Optional trusted facts about the executor that runs this tool. */ |
| executionFacts?: ToolExecutionFacts; |
| /** Crash-recovery contract used by the durable tool boundary. */ |
| recoveryMode?: ToolRecoveryMode; |
| /** Step-level admission contract. Exclusive tools cannot share an assistant step. */ |
| executionSemantics?: 'parallel' | 'exclusive_step'; |
| /** Optional permission/persistence projection derived from isolated execution args. */ |
| permissionArgs?: ( |
| args: P, |
| context: Pick<MakaToolContext, 'sessionId' | 'turnId' | 'toolCallId'>, |
| ) => unknown; |
| /** Real tool implementation. */ |
| impl: (args: P, ctx: MakaToolContext) => Promise<R> | R; |
| /** Optional provider-visible content mapping, used for screenshot image parts. */ |
| toModelOutput?: (options: { |
| toolCallId: string; |
| input: unknown; |
| output: unknown; |
| }) => ToolResultOutput | PromiseLike<ToolResultOutput>; |
| } |
| |
| export interface MakaToolContext { |
| sessionId: string; |
| runId?: string; |
| turnId: string; |
| /** Session working directory. */ |
| cwd: string; |
| /** Authoritative session boundary read immediately before Runtime-dispatched execution. */ |
| executionBoundary?: ExecutionBoundary; |
| permissionMode?: PermissionMode; |
| toolCallId: string; |
| abortSignal: AbortSignal; |
| emitOutput: (stream: ToolOutputStream, chunk: string) => void; |
| /** Diagnostic-only trace projection. It must never affect tool execution. */ |
| emitRunTrace?: ( |
| type: |
| | 'tool_started' |
| | 'tool_completed' |
| | 'tool_failed' |
| | 'skill_searched' |
| | 'skill_loaded' |
| | 'skill_load_failed', |
| message: string, |
| data?: Record<string, unknown>, |
| ) => void; |
| spawnChildAgent?: (input: { |
| spec: AgentSpec; |
| prompt: string; |
| /** Optional per-child signal, always composed with the owning tool invocation signal. */ |
| abortSignal?: AbortSignal; |
| onReady?: (input: { |
| turnId: string; |
| agentId: string; |
| agentName: string; |
| }) => void | Promise<void>; |
| onEvent?: (event: SessionEvent) => void; |
| }) => Promise<unknown>; |
| spawnChildSession?: (input: { |
| agentProfile: AgentProfile; |
| prompt: string; |
| /** Optional swarm identity, scoped to the owning tool call. */ |
| swarm?: { |
| swarmId: string; |
| itemId: string; |
| }; |
| /** Optional per-child signal, always composed with the owning tool invocation signal. */ |
| abortSignal?: AbortSignal; |
| onReady?: (input: { |
| childSessionId: string; |
| turnId: string; |
| runId: string; |
| agentId: string; |
| agentName: string; |
| }) => void | Promise<void>; |
| onEvent?: (event: SessionEvent) => void; |
| }) => Promise<unknown>; |
| prepareChildAgentResume?: (sourceRunId: string) => Promise<{ |
| sourceRunId: string; |
| execution: SubagentExecutionRef; |
| agentId: string; |
| agentName: string; |
| profile: string; |
| }>; |
| resumeChildAgent?: (input: { |
| sourceRunId: string; |
| prompt: string; |
| /** Optional per-child signal, always composed with the owning tool invocation signal. */ |
| abortSignal?: AbortSignal; |
| onReady?: (input: { |
| childSessionId?: string; |
| turnId: string; |
| runId?: string; |
| agentId: string; |
| agentName: string; |
| }) => void | Promise<void>; |
| onEvent?: (event: SessionEvent) => void; |
| }) => Promise<unknown>; |
| retryChildAgent?: (input: { |
| sourceRunId: string; |
| execution?: SubagentExecutionRef; |
| /** Optional per-child signal, always composed with the owning tool invocation signal. */ |
| abortSignal?: AbortSignal; |
| onReady?: (input: { |
| childSessionId?: string; |
| turnId: string; |
| runId?: string; |
| agentId: string; |
| agentName: string; |
| }) => void | Promise<void>; |
| onEvent?: (event: SessionEvent) => void; |
| }) => Promise<unknown>; |
| listChildAgents?: () => Promise<unknown>; |
| readChildAgentOutput?: (input: { |
| execution?: SubagentExecutionRef; |
| runId?: string; |
| turnId?: string; |
| maxEvents?: number; |
| maxBytes?: number; |
| view?: 'result' | 'events' | 'runtime_events' | 'all'; |
| }) => Promise<unknown>; |
| askUserQuestion?: (questions: UserQuestion[]) => Promise<UserQuestionResult>; |
| requestSandboxBoundary?: ( |
| expansion: SandboxBoundaryExpansion, |
| justification: string, |
| ) => Promise<SandboxBoundarySettlement>; |
| } |
| |
| export type AppendMessageFn = (m: ToolCallMessage | ToolResultMessage) => Promise<void>; |
| export type ToolTelemetryRecorder = (record: ToolInvocationRecord) => void; |
| |
| /** |
| * Per-step tool-availability gating for the execute boundary. `ToolAvailabilityRuntime` |
| * installs it each turn: `gatedNames` is the static set of tools that may be |
| * hidden this turn (group members when economy is on); `activeNames` returns the |
| * model-visible set for the step currently executing, recomputed before each |
| * step. The guard rejects a *gated* tool that is not yet active — core tools and |
| * the repair fallback are never in `gatedNames`, so they are never gated. |
| */ |
| export interface ToolGating { |
| gatedNames: ReadonlySet<string>; |
| activeNames: () => ReadonlySet<string>; |
| } |
| |
| export const TOOL_ERROR_RESULT_MAX_CHARS = 4000; |
| export const MAX_ACTIVE_SUBAGENT_TOOLS_PER_TURN = 5; |
| export const MAX_ACTIVE_CHILD_AGENT_RUNS_PER_TURN = 5; |
| export const DEFAULT_PERMISSION_TIMEOUT_MS = 300_000; |
| |
| /** |
| * Loop-gate: block a tool call once this many byte-identical calls (same tool + |
| * same args) have FAILED back-to-back with nothing different in between. Mirrors |
| * opencode's doom-loop threshold (#92: "same tool+args failing N times"). A |
| * success, or any different tool/args, resets the streak — so legitimate polling |
| * (re-run the same status check until it passes) and iterate-then-retry (edit a |
| * file, re-run the same failing test) are never gated; only a no-progress loop of |
| * identical *failures* is. |
| */ |
| export const LOOP_GATE_IDENTICAL_THRESHOLD = 3; |
| |
| const SUBAGENT_TOOL_LIMIT_MESSAGE = |
| '只读探索并发过多:同一轮最多 5 个子代理。请等待已有探索完成后再继续。'; |
| const CLIENT_CAPABILITY_BOUNDARY_MESSAGE = |
| 'Client Capability tools require the Bypass execution boundary because their client-side effects cannot be sandboxed by the Host. Switch this Session to Bypass and retry.'; |
| |
| function composeChildAbortSignal( |
| invocationSignal: AbortSignal, |
| childSignal: AbortSignal | undefined, |
| ): AbortSignal { |
| if (!childSignal || childSignal === invocationSignal) return invocationSignal; |
| return AbortSignal.any([invocationSignal, childSignal]); |
| } |
| |
| export interface ToolRuntimeInput { |
| sessionId: string; |
| header: SessionHeader; |
| connection: RuntimeExecutionConnection; |
| modelId: string; |
| appendMessage: AppendMessageFn; |
| readExecutionBoundary: () => Promise<ExecutionBoundary>; |
| createSandboxBoundaryRequest?: ( |
| input: CreateSandboxBoundaryRequest, |
| ) => Promise<SandboxBoundaryRequest>; |
| settleSandboxBoundaryRequest?: ( |
| input: SettleSandboxBoundaryRequest, |
| ) => Promise<SandboxBoundarySettlement>; |
| newId: () => string; |
| now: () => number; |
| getPermissionPauseTarget: () => { pause(): void; resume(): void } | null; |
| getCurrentInvocationId?: () => string | undefined; |
| getCurrentRunId?: () => string | undefined; |
| materializeDefaultToolResultOutput?: (options: { |
| toolCallId: string; |
| output: unknown; |
| }) => ToolResultOutput | PromiseLike<ToolResultOutput>; |
| /** Effective orchestration for the active send; undefined between turns. */ |
| getCurrentOrchestration?: () => EffectiveOrchestration | undefined; |
| spawnChildAgent?: (input: { |
| parentRunId: string; |
| spec: AgentSpec; |
| prompt: string; |
| abortSignal: AbortSignal; |
| onReady?: (input: { |
| turnId: string; |
| agentId: string; |
| agentName: string; |
| }) => void | Promise<void>; |
| onEvent?: (event: SessionEvent) => void; |
| }) => Promise<unknown>; |
| spawnChildSession?: (input: { |
| parentRunId: string; |
| parentTurnId: string; |
| toolCallId: string; |
| agentProfile: AgentProfile; |
| prompt: string; |
| swarm?: { |
| swarmId: string; |
| itemId: string; |
| }; |
| abortSignal: AbortSignal; |
| onReady?: (input: { |
| childSessionId: string; |
| turnId: string; |
| runId: string; |
| agentId: string; |
| agentName: string; |
| }) => void | Promise<void>; |
| onEvent?: (event: SessionEvent) => void; |
| }) => Promise<unknown>; |
| prepareChildAgentResume?: (sourceRunId: string) => Promise<{ |
| sourceRunId: string; |
| execution: SubagentExecutionRef; |
| agentId: string; |
| agentName: string; |
| profile: string; |
| }>; |
| resumeChildAgent?: (input: { |
| parentRunId: string; |
| sourceRunId: string; |
| prompt: string; |
| abortSignal: AbortSignal; |
| onReady?: (input: { |
| childSessionId?: string; |
| turnId: string; |
| runId?: string; |
| agentId: string; |
| agentName: string; |
| }) => void | Promise<void>; |
| onEvent?: (event: SessionEvent) => void; |
| }) => Promise<unknown>; |
| retryChildAgent?: (input: { |
| parentRunId: string; |
| sourceRunId: string; |
| execution?: SubagentExecutionRef; |
| abortSignal: AbortSignal; |
| onReady?: (input: { |
| childSessionId?: string; |
| turnId: string; |
| runId?: string; |
| agentId: string; |
| agentName: string; |
| }) => void | Promise<void>; |
| onEvent?: (event: SessionEvent) => void; |
| }) => Promise<unknown>; |
| listChildAgents?: () => Promise<unknown>; |
| readChildAgentOutput?: (input: { |
| execution?: SubagentExecutionRef; |
| runId?: string; |
| turnId?: string; |
| maxEvents?: number; |
| maxBytes?: number; |
| view?: 'result' | 'events' | 'runtime_events' | 'all'; |
| }) => Promise<unknown>; |
| getRunTrace?: () => RunTraceLike | null; |
| recordToolInvocation?: ToolTelemetryRecorder; |
| recordToolArtifacts?: ToolArtifactRecorder; |
| /** Optional Phase 2 T1/T2 commit boundary. Omitted on legacy JSONL hosts. */ |
| runtimeCommitSink?: RuntimeCommitSink; |
| } |
| |
| interface DurableToolAttempt { |
| operationId: string; |
| responseEventId: string; |
| commitOutcome( |
| result: unknown, |
| isError: boolean, |
| durationMs?: number, |
| ): Promise<{ id: string; operationId: string; ts: number }>; |
| } |
| |
| class RuntimeCommitBoundaryError extends Error { |
| constructor( |
| readonly phase: 'T1' | 'T2', |
| cause: unknown, |
| ) { |
| const detail = cause instanceof Error ? cause.message : String(cause); |
| super(`${phase} runtime commit failed: ${detail}`, { cause }); |
| this.name = 'RuntimeCommitBoundaryError'; |
| } |
| } |
| |
| export class ToolRuntime { |
| private readonly sandboxBoundaryRequests = new TurnScopedAwaitRegistry< |
| SandboxBoundarySettlement, |
| { toolUseId: string; creation: Promise<SandboxBoundaryRequest> } |
| >(); |
| private readonly userQuestions = new TurnScopedAwaitRegistry< |
| UserQuestionResponse, |
| { toolUseId: string; questions: UserQuestion[]; hosted: boolean } |
| >(); |
| private readonly hostedInteractions = new Map<string, HostedInteractionBridge>(); |
| private readonly deferredQuestionTurnClosures = new Set<string>(); |
| private activeSubagentToolCount = 0; |
| private childAgentRunLimiter = new ChildAgentRunLimiter(MAX_ACTIVE_CHILD_AGENT_RUNS_PER_TURN); |
| /** |
| * Tool-availability gating for the execute boundary. Set by the backend each |
| * turn from `ToolAvailabilityRuntime`. Undefined when gating is off (economy |
| * off / no hidden groups) — the guard is then fully inert. |
| */ |
| private gating?: ToolGating; |
| /** |
| * Loop-gate state: the signature (tool + canonical args) of the last *failed* |
| * call and how many byte-identical calls have failed back-to-back, including |
| * the most recent. A success or a different call clears it (see |
| * {@link recordLoopGateOutcome}). Only a consecutive count is needed, so two |
| * fields suffice. Reset each turn. |
| */ |
| private lastFailedToolCallSignature: string | undefined; |
| private failedToolCallStreak = 0; |
| private lastAmbiguousComputerSignature: string | undefined; |
| private readonly recentSandboxDenials = new Set<string>(); |
| private readonly durableToolAttempts = new Map<string, DurableToolAttempt>(); |
| private readonly readExecutionBoundary: NonNullable<ToolRuntimeInput['readExecutionBoundary']>; |
| private readonly stepAdmissions = new Map< |
| string, |
| { callCount: number; exclusiveToolName?: string } |
| >(); |
| constructor(private readonly input: ToolRuntimeInput) { |
| if (!input.readExecutionBoundary) { |
| throw new Error('ToolRuntime requires explicit execution boundary authority'); |
| } |
| this.readExecutionBoundary = input.readExecutionBoundary; |
| } |
| |
| beginTurn(turnId: string, hostedInteraction?: HostedInteractionBridge): void { |
| if ( |
| hostedInteraction && |
| (hostedInteraction.sessionId !== this.input.sessionId || hostedInteraction.turnId !== turnId) |
| ) { |
| throw new RuntimeInteractionInvariantError( |
| `ToolRuntime received a mismatched hosted Interaction Run for turn ${turnId}`, |
| ); |
| } |
| if (hostedInteraction) this.hostedInteractions.set(turnId, hostedInteraction); |
| else this.hostedInteractions.delete(turnId); |
| this.resetTurnState(); |
| this.sandboxBoundaryRequests.beginTurn(turnId); |
| this.userQuestions.beginTurn(turnId); |
| } |
| |
| async endTurn(turnId: string, reason: 'completed' | 'aborted' = 'completed'): Promise<void> { |
| const boundaryRequests = this.sandboxBoundaryRequests.entries(turnId); |
| const boundarySettlementErrors: unknown[] = []; |
| if (boundaryRequests.length > 0) { |
| if (!this.input.settleSandboxBoundaryRequest) { |
| boundarySettlementErrors.push( |
| new Error('Sandbox boundary settlement is unavailable on this surface'), |
| ); |
| } else { |
| const results = await Promise.allSettled( |
| boundaryRequests.map(async ([requestId, metadata]) => { |
| try { |
| await metadata.creation; |
| } catch { |
| return; |
| } |
| await this.input.settleSandboxBoundaryRequest?.({ |
| sessionId: this.input.sessionId, |
| requestId, |
| decision: 'deny', |
| }); |
| }), |
| ); |
| for (const result of results) { |
| if (result.status === 'rejected') boundarySettlementErrors.push(result.reason); |
| } |
| } |
| } |
| |
| const hasHostedPending = this.userQuestions |
| .entries(turnId) |
| .some(([, question]) => question.hosted); |
| this.hostedInteractions.delete(turnId); |
| this.sandboxBoundaryRequests.endTurn( |
| turnId, |
| (requestId) => |
| new Error(`Turn ${turnId} ${reason} before sandbox boundary ${requestId} was settled`), |
| ); |
| if (hasHostedPending) { |
| this.deferredQuestionTurnClosures.add(turnId); |
| this.finishDeferredQuestionTurnClosure(turnId); |
| this.resetTurnState(); |
| } else { |
| this.userQuestions.endTurn( |
| turnId, |
| (requestId) => |
| new Error(`Turn ${turnId} ${reason} before user question ${requestId} was answered`), |
| ); |
| this.deferredQuestionTurnClosures.delete(turnId); |
| this.resetTurnState(); |
| } |
| if (boundarySettlementErrors.length > 0) { |
| throw new AggregateError( |
| boundarySettlementErrors, |
| `Could not durably deny every sandbox boundary request for turn ${turnId}`, |
| ); |
| } |
| } |
| |
| respondToUserQuestion(turnId: string, response: UserQuestionResponse): boolean { |
| if (!response || typeof response.requestId !== 'string' || !Array.isArray(response.answers)) { |
| throw new Error('Invalid user question response'); |
| } |
| const pending = this.userQuestions |
| .entries(turnId) |
| .find(([requestId]) => requestId === response.requestId)?.[1]; |
| if (!pending) return false; |
| if (pending.hosted) { |
| throw new RuntimeInteractionInvariantError( |
| `Hosted question ${response.requestId} must settle through its captured continuation`, |
| ); |
| } |
| return this.settleUserQuestionAnswer(turnId, response, pending); |
| } |
| |
| async respondToSandboxBoundaryRequest( |
| turnId: string, |
| response: { requestId: string; decision: SandboxBoundaryDecision }, |
| ): Promise<boolean> { |
| if ( |
| !response || |
| typeof response.requestId !== 'string' || |
| (response.decision !== 'allow' && response.decision !== 'deny') |
| ) { |
| throw new Error('Invalid sandbox boundary response'); |
| } |
| const pending = this.sandboxBoundaryRequests |
| .entries(turnId) |
| .find(([requestId]) => requestId === response.requestId); |
| if (!pending) return false; |
| if (!this.input.settleSandboxBoundaryRequest) { |
| throw new Error('Sandbox boundary settlement is unavailable on this surface'); |
| } |
| const settlement = await this.input.settleSandboxBoundaryRequest({ |
| sessionId: this.input.sessionId, |
| requestId: response.requestId, |
| decision: response.decision, |
| }); |
| return this.sandboxBoundaryRequests.resolve(turnId, response.requestId, settlement) !== null; |
| } |
| |
| async respondToSandboxBoundaryResponse(response: { |
| requestId: string; |
| decision: SandboxBoundaryDecision; |
| }): Promise<boolean> { |
| const turnId = this.sandboxBoundaryRequests.findTurn(response.requestId); |
| if (!turnId) return false; |
| return this.respondToSandboxBoundaryRequest(turnId, response); |
| } |
| |
| private settleUserQuestionAnswer( |
| turnId: string, |
| response: UserQuestionResponse, |
| pending: { toolUseId: string; questions: UserQuestion[]; hosted: boolean }, |
| ): boolean { |
| if ( |
| response.answers.length !== pending.questions.length || |
| response.answers.some( |
| (answer) => answer !== null && (typeof answer !== 'string' || answer.length === 0), |
| ) |
| ) { |
| throw new Error('Invalid user question response'); |
| } |
| const resolved = this.userQuestions.resolve(turnId, response.requestId, response) !== null; |
| this.finishDeferredQuestionTurnClosure(turnId); |
| return resolved; |
| } |
| |
| closeUserQuestion( |
| turnId: string, |
| requestId: string, |
| reason: RuntimeInteractionClosureReason, |
| ): boolean { |
| const closed = |
| this.userQuestions.reject( |
| turnId, |
| requestId, |
| new RuntimeInteractionClosedError(requestId, reason), |
| ) !== null; |
| this.finishDeferredQuestionTurnClosure(turnId); |
| return closed; |
| } |
| |
| pendingUserQuestionCount(turnId: string): number { |
| return this.userQuestions.pendingCount(turnId); |
| } |
| |
| /** |
| * Settle one resolved Maka tool call. Tool/business failures resolve with a |
| * provider-facing error output; durable runtime commit failures still reject. |
| */ |
| async settleToolCall(call: ResolvedMakaToolCall): Promise<ToolSettlement> { |
| const result = await this.executeTool( |
| call.tool, |
| call.turnId, |
| call.eventSink, |
| call.input, |
| { |
| toolCallId: call.toolCallId, |
| abortSignal: call.abortSignal, |
| ...(call.providerOptions !== undefined ? { providerOptions: call.providerOptions } : {}), |
| }, |
| call.stepId, |
| ); |
| const providerError = providerToolErrorMessage(result); |
| const modelOutput = providerError |
| ? { type: 'error-text' as const, value: new Error(providerError).toString() } |
| : call.tool.toModelOutput |
| ? await call.tool.toModelOutput({ |
| toolCallId: call.toolCallId, |
| input: call.input, |
| output: result, |
| }) |
| : this.input.materializeDefaultToolResultOutput |
| ? await this.input.materializeDefaultToolResultOutput({ |
| toolCallId: call.toolCallId, |
| output: result, |
| }) |
| : typeof result === 'string' |
| ? { type: 'text' as const, value: result } |
| : { type: 'json' as const, value: jsonValue(result) }; |
| return { result, modelOutput }; |
| } |
| |
| /** |
| * Install the per-step tool-availability gating used at the execute boundary. |
| * The backend recomputes the active snapshot before each step; the guard in |
| * `executeTool` rejects a gated tool whose name is not in it. Pass `undefined` |
| * to disable gating. |
| */ |
| setGating(gating: ToolGating | undefined): void { |
| this.gating = gating; |
| } |
| |
| resetTurnState(): void { |
| const priorChildAgentRunLimiter = this.childAgentRunLimiter; |
| this.childAgentRunLimiter = new ChildAgentRunLimiter(MAX_ACTIVE_CHILD_AGENT_RUNS_PER_TURN); |
| priorChildAgentRunLimiter.close( |
| new Error('Child agent run permit scope ended before capacity became available'), |
| ); |
| this.activeSubagentToolCount = 0; |
| this.gating = undefined; |
| this.lastFailedToolCallSignature = undefined; |
| this.failedToolCallStreak = 0; |
| this.lastAmbiguousComputerSignature = undefined; |
| this.recentSandboxDenials.clear(); |
| this.durableToolAttempts.clear(); |
| this.stepAdmissions.clear(); |
| } |
| |
| /** |
| * Record the terminal outcome of one tool call for the loop-gate. A success (or |
| * any call with a different signature) resets the streak; a failure with the |
| * same signature as the last failure extends it. Called once per call at every |
| * exit — the pre-impl guards call it explicitly before their early returns, and |
| * the impl section calls it from its `finally`. The pre-block itself is the one |
| * exception: a blocked call records nothing, so the streak stays parked at the |
| * threshold and every further identical repeat keeps being blocked. |
| */ |
| private recordLoopGateOutcome(signature: string, failed: boolean): void { |
| if (!failed) { |
| this.lastFailedToolCallSignature = undefined; |
| this.failedToolCallStreak = 0; |
| return; |
| } |
| if (signature === this.lastFailedToolCallSignature) { |
| this.failedToolCallStreak += 1; |
| } else { |
| this.lastFailedToolCallSignature = signature; |
| this.failedToolCallStreak = 1; |
| } |
| } |
| |
| async writeSyntheticToolResult( |
| toolUseId: string, |
| turnId: string, |
| text: string, |
| queue: DurableSessionEventSink, |
| sandboxDenial?: SandboxDenialSignal, |
| sandboxFailure?: Extract<ToolResultContent, { kind: 'text' }>['sandboxFailure'], |
| uncertainOutcome?: ToolUncertainOutcomeSignal, |
| ): Promise<void> { |
| const content: ToolResultContent = { |
| kind: 'text', |
| text: formatSyntheticToolErrorText(text), |
| ...(sandboxDenial ? { sandboxDenial } : {}), |
| ...(sandboxFailure ? { sandboxFailure } : {}), |
| ...(uncertainOutcome ? { uncertainOutcome } : {}), |
| }; |
| const durableAttempt = this.durableToolAttempts.get(durableAttemptKey(turnId, toolUseId)); |
| const durableOutcome = await durableAttempt?.commitOutcome(content, true); |
| const msg: ToolResultMessage = { |
| type: 'tool_result', |
| id: this.input.newId(), |
| turnId, |
| ts: this.input.now(), |
| toolUseId, |
| isError: true, |
| content, |
| }; |
| await this.input.appendMessage(msg); |
| queue.push({ |
| type: 'tool_result', |
| id: durableOutcome?.id ?? this.input.newId(), |
| turnId, |
| ts: durableOutcome?.ts ?? this.input.now(), |
| toolUseId, |
| ...(durableOutcome ? { operationId: durableOutcome.operationId } : {}), |
| isError: true, |
| content, |
| } satisfies ToolResultEvent); |
| } |
| |
| private async executeTool( |
| tool: MakaTool, |
| turnId: string, |
| queue: DurableSessionEventSink, |
| args: unknown, |
| ctx: { |
| toolCallId: string; |
| abortSignal: AbortSignal; |
| providerOptions?: Record<string, unknown>; |
| }, |
| stepId?: string, |
| ): Promise<unknown> { |
| const executionArgs = snapshotToolArgs(args); |
| const toolUseId = ctx.toolCallId; |
| // Registration is synchronous and happens before the first await, so |
| // parallel Runtime settlements cannot race past exclusive admission. |
| const admissionFailure = this.admitToolForStep(tool, stepId); |
| let permissionArgs = executionArgs; |
| let permissionArgsError: unknown; |
| try { |
| permissionArgs = tool.permissionArgs |
| ? snapshotToolArgs( |
| tool.permissionArgs(structuredClone(executionArgs) as never, { |
| sessionId: this.input.sessionId, |
| turnId, |
| toolCallId: toolUseId, |
| }), |
| ) |
| : executionArgs; |
| } catch (error) { |
| permissionArgsError = error; |
| } |
| const persistedArgs = |
| tool.categoryHint === 'computer_use' |
| ? snapshotToolArgs(computerUseApprovalSummary(permissionArgs)) |
| : permissionArgs; |
| const now = this.input.now(); |
| const toolIntent = describeToolIntent(tool, persistedArgs); |
| const trace = this.input.getRunTrace?.() ?? null; |
| |
| const runId = this.input.getCurrentRunId?.(); |
| const invocationId = this.input.getCurrentInvocationId?.() ?? runId; |
| if (this.input.runtimeCommitSink && !runId) { |
| throw new RuntimeCommitBoundaryError( |
| 'T1', |
| new Error('Durable tool execution requires a run id'), |
| ); |
| } |
| // Exclusive-step rejection is preflight: it must remain on the generic |
| // call/response lane instead of claiming the T1 dispatch protocol. If the |
| // call carried an operationId here, AgentRun would (correctly) skip its |
| // generic projection assuming commitToolPrepared already persisted it; |
| // the synthetic response would then become an orphan. |
| const operationId = |
| this.input.runtimeCommitSink && invocationId && !admissionFailure |
| ? buildToolOperationId({ invocationId, providerToolCallId: toolUseId }) |
| : undefined; |
| const startEv: ToolStartEvent = { |
| type: 'tool_start', |
| id: operationId ? `${operationId}_call` : this.input.newId(), |
| turnId, |
| ts: now, |
| toolUseId, |
| toolName: tool.name, |
| ...(operationId ? { operationId } : {}), |
| ...(tool.activityKind ? { activityKind: tool.activityKind } : {}), |
| args: structuredClone(persistedArgs), |
| ...(ctx.providerOptions !== undefined |
| ? { providerOptions: structuredClone(ctx.providerOptions) } |
| : {}), |
| ...(tool.displayName ? { displayName: tool.displayName } : {}), |
| ...(toolIntent ? { intent: toolIntent } : {}), |
| ...(stepId !== undefined ? { stepId } : {}), |
| }; |
| const callMsg: ToolCallMessage = { |
| type: 'tool_call', |
| id: toolUseId, |
| turnId, |
| ts: now, |
| toolName: tool.name, |
| ...(tool.activityKind ? { activityKind: tool.activityKind } : {}), |
| ...(tool.displayName ? { displayName: tool.displayName } : {}), |
| ...(toolIntent ? { intent: toolIntent } : {}), |
| args: structuredClone(persistedArgs), |
| ...(ctx.providerOptions !== undefined |
| ? { providerOptions: structuredClone(ctx.providerOptions) } |
| : {}), |
| // Persist the same step id the tool_start event carries so the UI |
| // timeline and post-restart backfill can pair this call with its step. |
| ...(stepId !== undefined ? { stepId } : {}), |
| }; |
| await this.input.appendMessage(callMsg); |
| queue.push(startEv); |
| trace?.emit('tool', 'tool_started', 'Tool execution started', { |
| toolUseId, |
| toolName: tool.name, |
| ...(tool.categoryHint !== undefined ? { categoryHint: tool.categoryHint } : {}), |
| }); |
| const callSignature = `${tool.name} ${loopGateArgsKey(executionArgs, toolUseId)}`; |
| if (admissionFailure) { |
| await this.writeSyntheticToolResult(toolUseId, turnId, admissionFailure, queue); |
| trace?.emit('tool', 'tool_failed', 'Tool rejected by exclusive-step admission', { |
| toolUseId, |
| toolName: tool.name, |
| stepId, |
| status: 'error', |
| errorClass: 'ExclusiveStepConflict', |
| }); |
| this.recordLoopGateOutcome(callSignature, true); |
| return this.errorReturn(admissionFailure); |
| } |
| const computerSemanticSignature = |
| tool.categoryHint === 'computer_use' |
| ? computerUseSemanticSignature(permissionArgs) |
| : undefined; |
| if (permissionArgsError !== undefined) { |
| const msg = |
| tool.categoryHint === 'computer_use' |
| ? 'Computer Use arguments failed validation' |
| : formatSyntheticToolErrorText(permissionArgsError); |
| await this.writeSyntheticToolResult(toolUseId, turnId, msg, queue); |
| this.input.recordToolInvocation?.({ |
| sessionId: this.input.sessionId, |
| turnId, |
| toolCallId: toolUseId, |
| toolName: tool.name, |
| providerId: this.input.connection.providerType, |
| modelId: this.input.modelId, |
| durationMs: 0, |
| status: 'error', |
| errorClass: 'InvalidArguments', |
| argsSummary: |
| tool.categoryHint === 'computer_use' |
| ? summarizePersistedArgs(persistedArgs) |
| : summarizeArgs(tool.name, executionArgs), |
| bytesIn: byteLength(persistedArgs), |
| bytesOut: byteLength(msg), |
| startedAt: now, |
| }); |
| trace?.emit('tool', 'tool_failed', 'Tool arguments failed validation', { |
| toolUseId, |
| toolName: tool.name, |
| status: 'error', |
| errorClass: 'InvalidArguments', |
| }); |
| this.recordLoopGateOutcome(callSignature, true); |
| return this.errorReturn(msg); |
| } |
| |
| // Loop-gate (#92): block this call up front — before the guards and the real |
| // impl — if this exact call (tool + canonical args) has already FAILED |
| // back-to-back the last (THRESHOLD-1) times. Re-running an identical failing |
| // call cannot change the outcome; it only drains the turn. Checked first so a |
| // tool that keeps failing the availability guard (not loaded) or permission |
| // also trips it — those rejections count as failures (see |
| // recordLoopGateOutcome). A success or any different call resets the streak, |
| // so polling and iterate-then-retry are never gated. Recoverable: the model |
| // is told to change its approach. The block itself records no outcome, so the |
| // streak stays parked and every further identical repeat stays blocked. |
| if ( |
| computerSemanticSignature && |
| computerSemanticSignature === this.lastAmbiguousComputerSignature |
| ) { |
| const reason = formatAmbiguousComputerLoopGateText(); |
| await this.writeSyntheticToolResult(toolUseId, turnId, reason, queue); |
| trace?.emit('tool', 'tool_failed', 'Blocked repeated ambiguous Computer Use target', { |
| toolUseId, |
| toolName: tool.name, |
| status: 'error', |
| errorClass: 'AmbiguousComputerTarget', |
| }); |
| return this.errorReturn(reason); |
| } |
| if ( |
| this.lastAmbiguousComputerSignature && |
| computerSemanticSignature && |
| computerSemanticSignature !== this.lastAmbiguousComputerSignature |
| ) { |
| this.lastAmbiguousComputerSignature = undefined; |
| } |
| if ( |
| callSignature === this.lastFailedToolCallSignature && |
| this.failedToolCallStreak >= LOOP_GATE_IDENTICAL_THRESHOLD - 1 |
| ) { |
| const reason = formatLoopGateText(tool.name); |
| await this.writeSyntheticToolResult(toolUseId, turnId, reason, queue); |
| trace?.emit('tool', 'tool_failed', 'Loop-gate blocked a repeated identical failing call', { |
| toolUseId, |
| toolName: tool.name, |
| status: 'error', |
| errorClass: 'LoopGate', |
| }); |
| return this.errorReturn(reason); |
| } |
| |
| // Tool-availability execute-boundary guard (Codex Δ5). Uses the step-start |
| // snapshot, NOT a cumulative loaded-set: if one step emits `load_tools(g)` |
| // and a tool from group `g` in parallel, that tool is not yet active (it |
| // activates only in the next request projection), so it is rejected here — |
| // before permission eval and before the real impl. This also closes the AI |
| // SDK `activeTools` leak (vercel/ai#8653). The rejection is recoverable: the |
| // model loads via `load_tools`, then retries next step. |
| if ( |
| this.gating && |
| this.gating.gatedNames.has(tool.name) && |
| !this.gating.activeNames().has(tool.name) |
| ) { |
| const reason = formatDeferredNotLoadedText(tool.name); |
| await this.writeSyntheticToolResult(toolUseId, turnId, reason, queue); |
| trace?.emit('tool', 'tool_failed', 'Deferred tool used before load', { |
| toolUseId, |
| toolName: tool.name, |
| status: 'error', |
| errorClass: 'DeferredNotLoaded', |
| }); |
| this.recordLoopGateOutcome(callSignature, true); |
| return this.errorReturn(reason); |
| } |
| |
| this.assertCapturedRunOwner(tool.name, runId); |
| let clientCapabilityBoundary: ExecutionBoundary | undefined; |
| if (tool.categoryHint === 'client_capability') { |
| try { |
| clientCapabilityBoundary = await this.readExecutionBoundary(); |
| } catch (error) { |
| const reason = formatSyntheticToolErrorText(error); |
| await this.writeSyntheticToolResult(toolUseId, turnId, reason, queue); |
| trace?.emit('tool', 'tool_failed', 'Client Capability boundary read failed', { |
| toolUseId, |
| toolName: tool.name, |
| status: 'error', |
| errorClass: 'ExecutionBoundaryUnavailable', |
| }); |
| this.recordLoopGateOutcome(callSignature, true); |
| return this.errorReturn(reason); |
| } |
| if (clientCapabilityBoundary.kind !== 'bypass') { |
| await this.writeSyntheticToolResult( |
| toolUseId, |
| turnId, |
| CLIENT_CAPABILITY_BOUNDARY_MESSAGE, |
| queue, |
| ); |
| trace?.emit('tool', 'tool_failed', 'Client Capability blocked by execution boundary', { |
| toolUseId, |
| toolName: tool.name, |
| status: 'error', |
| errorClass: 'ClientCapabilityBoundary', |
| }); |
| this.recordLoopGateOutcome(callSignature, true); |
| return this.errorReturn(CLIENT_CAPABILITY_BOUNDARY_MESSAGE); |
| } |
| } |
| |
| const reservedSubagentSlot = this.reserveSubagentSlot(tool); |
| if (!reservedSubagentSlot) { |
| trace?.emit('tool', 'tool_failed', 'Tool execution rejected by runtime limit', { |
| toolUseId, |
| toolName: tool.name, |
| errorClass: 'RuntimeLimit', |
| boundary: 'subagent_tool_admission', |
| }); |
| await this.writeSyntheticToolResult(toolUseId, turnId, SUBAGENT_TOOL_LIMIT_MESSAGE, queue); |
| this.recordLoopGateOutcome(callSignature, true); |
| return this.errorReturn(SUBAGENT_TOOL_LIMIT_MESSAGE); |
| } |
| |
| let durableAttempt: DurableToolAttempt | undefined; |
| try { |
| this.assertCapturedRunOwner(tool.name, runId); |
| durableAttempt = await this.prepareDurableToolAttempt({ |
| tool, |
| startEvent: startEv, |
| persistedArgs, |
| abortSignal: ctx.abortSignal, |
| ...(invocationId ? { invocationId } : {}), |
| ...(runId ? { runId } : {}), |
| }); |
| } catch (error) { |
| if (reservedSubagentSlot) this.releaseSubagentSlot(tool); |
| throw error; |
| } |
| if (durableAttempt) { |
| this.durableToolAttempts.set(durableAttemptKey(turnId, toolUseId), durableAttempt); |
| } |
| const startedAt = this.input.now(); |
| const output = createToolOutputDeltaEmitter({ |
| sessionId: this.input.sessionId, |
| turnId, |
| toolUseId, |
| newId: this.input.newId, |
| now: this.input.now, |
| push: (event) => queue.push(event), |
| }); |
| // Loop-gate outcome for the real impl. Default failed; the success path below |
| // overwrites it from the derived result status, and the finally records it |
| // once for every exit (return or throw). The pre-impl guards record their own |
| // failures above, since they early-return before this point. |
| let attemptFailed = true; |
| try { |
| // Pause the stream idle watchdog for the whole tool execution. In the |
| // ai-sdk step loop a tool runs *between* model requests — the tool-call |
| // step's stream already finished and the next request has not started — |
| // so provider silence here is expected, not a stalled model stream. A |
| // long-running tool (apt-get install, a build, an ML training step, a |
| // subagent loop) must not trip the idle timeout and abort the whole |
| // invocation; the tool carries its own timeout (e.g. Bash timeout_ms) |
| // and the trial/run layer is the outer backstop. |
| const pauseTarget = this.input.getPermissionPauseTarget(); |
| pauseTarget?.pause(); |
| try { |
| const runId = this.input.getCurrentRunId?.(); |
| const executionBoundary = clientCapabilityBoundary ?? (await this.readExecutionBoundary()); |
| const result = await tool.impl(structuredClone(executionArgs) as never, { |
| sessionId: this.input.sessionId, |
| turnId, |
| ...(runId ? { runId } : {}), |
| cwd: this.input.header.cwd, |
| executionBoundary, |
| permissionMode: this.input.header.permissionMode, |
| toolCallId: toolUseId, |
| abortSignal: ctx.abortSignal, |
| emitOutput: output.emit, |
| ...(trace |
| ? { |
| emitRunTrace: ( |
| type: |
| | 'tool_started' |
| | 'tool_completed' |
| | 'tool_failed' |
| | 'skill_searched' |
| | 'skill_loaded' |
| | 'skill_load_failed', |
| message: string, |
| data?: Record<string, unknown>, |
| ) => |
| trace.emit(type.startsWith('skill_') ? 'skill' : 'tool', type, message, { |
| toolUseId, |
| toolName: tool.name, |
| ...(data ?? {}), |
| }), |
| } |
| : {}), |
| ...(this.input.listChildAgents ? { listChildAgents: this.input.listChildAgents } : {}), |
| ...(this.input.readChildAgentOutput |
| ? { readChildAgentOutput: this.input.readChildAgentOutput } |
| : {}), |
| ...this.buildChildAgentContext({ |
| turnId, |
| abortSignal: ctx.abortSignal, |
| trace, |
| toolUseId, |
| toolName: tool.name, |
| }), |
| askUserQuestion: (questions) => this.askUserQuestion(turnId, toolUseId, questions, queue), |
| requestSandboxBoundary: (expansion, justification) => |
| this.requestSandboxBoundary(turnId, toolUseId, expansion, justification, queue), |
| }); |
| output.flush(); |
| const durationMs = this.input.now() - startedAt; |
| |
| const content = coerceResultContent(result); |
| const toolResultStatus = deriveToolResultStatus(content, result); |
| const durableOutcome = await durableAttempt?.commitOutcome( |
| content, |
| toolResultStatus !== 'success', |
| durationMs, |
| ); |
| if (hasSandboxDenial(content)) { |
| const denialKey = sandboxDenialKey(tool.name, this.input.header.cwd, executionArgs); |
| this.recentSandboxDenials.add(denialKey); |
| if (content.kind === 'terminal' || content.kind === 'shell_run') { |
| this.recentSandboxDenials.add( |
| sandboxDenialKey('Bash', this.input.header.cwd, { |
| command: content.cmd, |
| }), |
| ); |
| } |
| trace?.emit( |
| 'sandbox', |
| 'sandbox_denial_detected', |
| 'Command likely failed because of sandbox enforcement', |
| { |
| toolUseId, |
| toolName: tool.name, |
| commandHash: denialKey, |
| }, |
| ); |
| } |
| const resultMsg: ToolResultMessage = { |
| type: 'tool_result', |
| id: this.input.newId(), |
| turnId, |
| ts: this.input.now(), |
| toolUseId, |
| isError: toolResultStatus !== 'success', |
| content, |
| durationMs, |
| }; |
| await this.input.appendMessage(resultMsg); |
| queue.push({ |
| type: 'tool_result', |
| id: durableOutcome?.id ?? this.input.newId(), |
| turnId, |
| ts: durableOutcome?.ts ?? this.input.now(), |
| toolUseId, |
| ...(durableOutcome ? { operationId: durableOutcome.operationId } : {}), |
| isError: toolResultStatus !== 'success', |
| content, |
| durationMs, |
| } satisfies ToolResultEvent); |
| |
| this.input.recordToolInvocation?.({ |
| sessionId: this.input.sessionId, |
| turnId, |
| toolCallId: toolUseId, |
| toolName: tool.name, |
| providerId: this.input.connection.providerType, |
| modelId: this.input.modelId, |
| durationMs, |
| status: toolResultStatus, |
| argsSummary: |
| tool.categoryHint === 'computer_use' |
| ? summarizePersistedArgs(persistedArgs) |
| : summarizeArgs(tool.name, executionArgs), |
| resultSummary: summarizeToolResultForTelemetry(content), |
| bytesIn: byteLength(persistedArgs), |
| bytesOut: byteLength(result), |
| startedAt, |
| }); |
| trace?.emit('tool', 'tool_completed', 'Tool execution completed', { |
| toolUseId, |
| toolName: tool.name, |
| durationMs, |
| status: toolResultStatus, |
| resultSummary: summarizeToolResultForTelemetry(content), |
| }); |
| |
| void recordToolArtifactsSafely( |
| { |
| sessionId: this.input.sessionId, |
| turnId, |
| toolUseId, |
| toolName: tool.name, |
| cwd: this.input.header.cwd, |
| args: structuredClone(persistedArgs), |
| result, |
| }, |
| this.input.recordToolArtifacts, |
| (message) => { |
| queue.push({ |
| type: 'tool_progress', |
| id: this.input.newId(), |
| turnId, |
| ts: this.input.now(), |
| toolUseId, |
| chunk: message, |
| }); |
| }, |
| ); |
| |
| attemptFailed = toolResultStatus !== 'success'; |
| if (isAmbiguousComputerFailure(result)) { |
| this.lastAmbiguousComputerSignature = computerSemanticSignature; |
| } else if (computerSemanticSignature) { |
| this.lastAmbiguousComputerSignature = undefined; |
| } |
| return result; |
| } finally { |
| pauseTarget?.resume(); |
| } |
| } catch (err) { |
| if (err instanceof RuntimeCommitBoundaryError) throw err; |
| if (isInteractionControlError(err)) throw err; |
| output.flush(); |
| const sandboxError = serializeSandboxError(err); |
| const uncertainOutcome = uncertainOutcomeSignalFromError(err); |
| const errorClass = uncertainOutcome ? 'OutcomeUnknown' : classifyError(err); |
| const terminalFailure = coerceTerminalFailure( |
| tool, |
| this.input.header.cwd, |
| executionArgs, |
| err, |
| ); |
| if (terminalFailure) { |
| if (terminalFailure.sandboxDenied) { |
| const denialKey = sandboxDenialKey(tool.name, this.input.header.cwd, executionArgs); |
| this.recentSandboxDenials.add(denialKey); |
| trace?.emit( |
| 'sandbox', |
| 'sandbox_denial_detected', |
| 'Command likely failed because of sandbox enforcement', |
| { |
| toolUseId, |
| toolName: tool.name, |
| commandHash: denialKey, |
| }, |
| ); |
| } |
| const durationMs = Math.max(0, this.input.now() - startedAt); |
| const durableOutcome = await durableAttempt?.commitOutcome( |
| terminalFailure.content, |
| true, |
| durationMs, |
| ); |
| const resultMsg: ToolResultMessage = { |
| type: 'tool_result', |
| id: this.input.newId(), |
| turnId, |
| ts: this.input.now(), |
| toolUseId, |
| isError: true, |
| content: terminalFailure.content, |
| durationMs, |
| }; |
| await this.input.appendMessage(resultMsg); |
| queue.push({ |
| type: 'tool_result', |
| id: durableOutcome?.id ?? this.input.newId(), |
| turnId, |
| ts: durableOutcome?.ts ?? this.input.now(), |
| toolUseId, |
| ...(durableOutcome ? { operationId: durableOutcome.operationId } : {}), |
| isError: true, |
| content: terminalFailure.content, |
| durationMs, |
| } satisfies ToolResultEvent); |
| this.input.recordToolInvocation?.({ |
| sessionId: this.input.sessionId, |
| turnId, |
| toolCallId: toolUseId, |
| toolName: tool.name, |
| providerId: this.input.connection.providerType, |
| modelId: this.input.modelId, |
| durationMs, |
| status: 'error', |
| errorClass, |
| argsSummary: |
| tool.categoryHint === 'computer_use' |
| ? summarizePersistedArgs(persistedArgs) |
| : summarizeArgs(tool.name, executionArgs), |
| resultSummary: summarizeToolResultForTelemetry(terminalFailure.content), |
| bytesIn: byteLength(persistedArgs), |
| bytesOut: byteLength(terminalFailure.content), |
| startedAt, |
| }); |
| trace?.emit('tool', 'tool_failed', 'Tool execution failed', { |
| toolUseId, |
| toolName: tool.name, |
| durationMs, |
| status: 'error', |
| errorClass, |
| ...(sandboxError ? { sandbox: sandboxError } : {}), |
| }); |
| return this.errorReturn(terminalFailure.message); |
| } |
| const msg = |
| tool.categoryHint === 'computer_use' |
| ? `Computer Use failed: ${errorClass}` |
| : uncertainOutcome |
| ? `outcome_unknown: ${formatSyntheticToolErrorText(err)}` |
| : formatSyntheticToolErrorText(err); |
| await this.writeSyntheticToolResult( |
| toolUseId, |
| turnId, |
| msg, |
| queue, |
| sandboxDenialSignalFromError(err), |
| sandboxBoundaryFailureSignal(sandboxError), |
| uncertainOutcome, |
| ); |
| this.input.recordToolInvocation?.({ |
| sessionId: this.input.sessionId, |
| turnId, |
| toolCallId: toolUseId, |
| toolName: tool.name, |
| providerId: this.input.connection.providerType, |
| modelId: this.input.modelId, |
| durationMs: Math.max(0, this.input.now() - startedAt), |
| status: 'error', |
| errorClass, |
| argsSummary: |
| tool.categoryHint === 'computer_use' |
| ? summarizePersistedArgs(persistedArgs) |
| : summarizeArgs(tool.name, executionArgs), |
| bytesIn: byteLength(persistedArgs), |
| bytesOut: 0, |
| startedAt, |
| }); |
| trace?.emit('tool', 'tool_failed', 'Tool execution failed', { |
| toolUseId, |
| toolName: tool.name, |
| durationMs: Math.max(0, this.input.now() - startedAt), |
| status: 'error', |
| errorClass, |
| ...(sandboxError ? { sandbox: sandboxError } : {}), |
| }); |
| return sandboxError ? { error: msg, sandbox: sandboxError } : this.errorReturn(msg); |
| } finally { |
| this.recordLoopGateOutcome(callSignature, attemptFailed); |
| if (reservedSubagentSlot) this.releaseSubagentSlot(tool); |
| } |
| } |
| |
| private async prepareDurableToolAttempt(input: { |
| tool: MakaTool; |
| startEvent: ToolStartEvent; |
| persistedArgs: unknown; |
| abortSignal: AbortSignal; |
| invocationId?: string; |
| runId?: string; |
| }): Promise<DurableToolAttempt | undefined> { |
| const sink = this.input.runtimeCommitSink; |
| if (!sink) return undefined; |
| const runId = input.runId; |
| const invocationId = input.invocationId; |
| if (!runId) { |
| throw new RuntimeCommitBoundaryError( |
| 'T1', |
| new Error('Durable tool execution requires a run id'), |
| ); |
| } |
| if (!invocationId) { |
| throw new RuntimeCommitBoundaryError( |
| 'T1', |
| new Error('Durable tool execution requires an invocation id'), |
| ); |
| } |
| const operationId = input.startEvent.operationId; |
| if (!operationId) |
| throw new RuntimeCommitBoundaryError('T1', new Error('Tool start has no operation id')); |
| const stateDelta: Record<string, unknown> = {}; |
| if (input.startEvent.activityKind !== undefined) |
| stateDelta.activityKind = input.startEvent.activityKind; |
| if (input.startEvent.displayName !== undefined) |
| stateDelta.displayName = input.startEvent.displayName; |
| if (input.startEvent.intent !== undefined) stateDelta.intent = input.startEvent.intent; |
| const callEvent: RuntimeEvent = { |
| id: input.startEvent.id, |
| invocationId, |
| runId, |
| sessionId: this.input.sessionId, |
| turnId: input.startEvent.turnId, |
| ts: input.startEvent.ts, |
| partial: false, |
| role: 'model', |
| author: 'agent', |
| content: { |
| kind: 'function_call', |
| id: input.startEvent.toolUseId, |
| name: input.tool.name, |
| args: structuredClone(input.persistedArgs), |
| ...(input.startEvent.providerOptions !== undefined |
| ? { providerOptions: structuredClone(input.startEvent.providerOptions) } |
| : {}), |
| }, |
| refs: { |
| operationId, |
| toolCallId: input.startEvent.toolUseId, |
| ...(input.startEvent.stepId ? { stepId: input.startEvent.stepId } : {}), |
| }, |
| ...(Object.keys(stateDelta).length > 0 ? { actions: { stateDelta } } : {}), |
| }; |
| const canonicalArgsHash = canonicalToolArgsHash(input.tool.name, input.persistedArgs); |
| const recoveryMode = input.tool.recoveryMode ?? 'never_auto_retry'; |
| const dispatchEvent: RuntimeEvent = { |
| id: `${operationId}_dispatch`, |
| invocationId, |
| runId, |
| sessionId: this.input.sessionId, |
| turnId: input.startEvent.turnId, |
| ts: input.startEvent.ts, |
| partial: false, |
| role: 'system', |
| author: 'system', |
| actions: { |
| toolDispatch: { |
| protocol: TOOL_BOUNDARY_PROTOCOL_V1, |
| operationId, |
| providerToolCallId: input.startEvent.toolUseId, |
| toolName: input.tool.name, |
| canonicalArgsHash, |
| recoveryMode, |
| }, |
| }, |
| refs: { operationId, toolCallId: input.startEvent.toolUseId }, |
| }; |
| try { |
| this.assertCapturedRunOwner(input.tool.name, runId); |
| this.assertDurableDispatchNotAborted(input.tool.name, input.abortSignal); |
| const prepared = await sink.commitToolPrepared({ |
| operationId, |
| journalEventId: `${operationId}_prepared`, |
| runtimeEvent: callEvent, |
| dispatchRuntimeEvent: dispatchEvent, |
| providerToolCallId: input.startEvent.toolUseId, |
| toolName: input.tool.name, |
| canonicalArgsHash, |
| recoveryMode, |
| committedAt: this.input.now(), |
| }); |
| if (!prepared.created) { |
| throw new Error(`Tool operation ${operationId} is already claimed`); |
| } |
| } catch (error) { |
| throw new RuntimeCommitBoundaryError('T1', error); |
| } |
| let committedOutcome: { id: string; operationId: string; ts: number } | undefined; |
| return { |
| operationId, |
| responseEventId: `${operationId}_response`, |
| commitOutcome: async (result, isError, durationMs) => { |
| if (committedOutcome) return committedOutcome; |
| const responseEvent: RuntimeEvent = { |
| id: `${operationId}_response`, |
| invocationId, |
| runId, |
| sessionId: this.input.sessionId, |
| turnId: input.startEvent.turnId, |
| ts: this.input.now(), |
| partial: false, |
| role: 'tool', |
| author: 'tool', |
| content: { |
| kind: 'function_response', |
| id: input.startEvent.toolUseId, |
| name: input.tool.name, |
| result, |
| ...(isError ? { isError: true } : {}), |
| }, |
| refs: { |
| operationId, |
| toolCallId: input.startEvent.toolUseId, |
| }, |
| ...(durationMs !== undefined ? { actions: { stateDelta: { durationMs } } } : {}), |
| }; |
| try { |
| await sink.commitToolOutcome({ |
| operationId, |
| journalEventId: `${operationId}_outcome`, |
| runtimeEvent: responseEvent, |
| committedAt: responseEvent.ts, |
| }); |
| } catch (error) { |
| throw new RuntimeCommitBoundaryError('T2', error); |
| } |
| committedOutcome = { |
| id: responseEvent.id, |
| operationId, |
| ts: responseEvent.ts, |
| }; |
| this.durableToolAttempts.delete( |
| durableAttemptKey(input.startEvent.turnId, input.startEvent.toolUseId), |
| ); |
| return committedOutcome; |
| }, |
| }; |
| } |
| |
| private admitToolForStep(tool: MakaTool, stepId: string | undefined): string | undefined { |
| if (!stepId) return undefined; |
| const existing = this.stepAdmissions.get(stepId) ?? { callCount: 0 }; |
| const exclusive = tool.executionSemantics === 'exclusive_step'; |
| if (existing.exclusiveToolName) { |
| return `Tool ${tool.name} cannot share an assistant step with exclusive tool ${existing.exclusiveToolName}. Retry it in a separate step.`; |
| } |
| if (exclusive && existing.callCount > 0) { |
| return `Exclusive tool ${tool.name} cannot share an assistant step with other tool calls. Retry it in a separate step.`; |
| } |
| existing.callCount += 1; |
| if (exclusive) existing.exclusiveToolName = tool.name; |
| this.stepAdmissions.set(stepId, existing); |
| return undefined; |
| } |
| |
| private assertCapturedRunOwner(toolName: string, expectedRunId: string | undefined): void { |
| if (expectedRunId && this.input.getCurrentRunId?.() !== expectedRunId) { |
| throw new Error(`Tool ${toolName} lost Run ownership before durable dispatch`); |
| } |
| } |
| |
| private assertDurableDispatchNotAborted(toolName: string, abortSignal: AbortSignal): void { |
| if (!abortSignal.aborted) return; |
| throw abortSignal.reason instanceof Error |
| ? abortSignal.reason |
| : new Error(`Tool ${toolName} was cancelled before durable dispatch`); |
| } |
| |
| private reserveSubagentSlot(tool: MakaTool): boolean { |
| if (tool.categoryHint !== 'subagent') return true; |
| if (this.activeSubagentToolCount >= MAX_ACTIVE_SUBAGENT_TOOLS_PER_TURN) return false; |
| this.activeSubagentToolCount += 1; |
| return true; |
| } |
| |
| private releaseSubagentSlot(tool: MakaTool): void { |
| if (tool.categoryHint !== 'subagent') return; |
| this.activeSubagentToolCount = Math.max(0, this.activeSubagentToolCount - 1); |
| } |
| |
| private errorReturn(message: string): unknown { |
| return { error: message }; |
| } |
| |
| private buildChildAgentContext(input: { |
| turnId: string; |
| abortSignal: AbortSignal; |
| trace: RunTraceLike | null; |
| toolUseId: string; |
| toolName: string; |
| }): Pick< |
| MakaToolContext, |
| | 'spawnChildAgent' |
| | 'spawnChildSession' |
| | 'prepareChildAgentResume' |
| | 'resumeChildAgent' |
| | 'retryChildAgent' |
| > { |
| const parentRunId = this.input.getCurrentRunId?.(); |
| if (!parentRunId) return {}; |
| const limiter = this.childAgentRunLimiter; |
| const runWithPermit = async <T>( |
| mode: 'spawn' | 'spawn_session' | 'resume' | 'retry', |
| abortSignal: AbortSignal, |
| execute: () => Promise<T>, |
| ): Promise<T> => { |
| const waitingForPermit = limiter.activeCount >= limiter.capacity || limiter.waitingCount > 0; |
| if (waitingForPermit) { |
| input.trace?.emit('tool', 'tool_started', 'Child run waiting for shared runtime capacity', { |
| toolUseId: input.toolUseId, |
| toolName: input.toolName, |
| boundary: 'shared_child_run_permit', |
| stage: 'waiting', |
| mode, |
| activeChildRuns: limiter.activeCount, |
| waitingChildRuns: limiter.waitingCount + 1, |
| capacity: limiter.capacity, |
| }); |
| } |
| let permit; |
| try { |
| permit = await limiter.acquire(abortSignal); |
| } catch (error) { |
| input.trace?.emit( |
| 'tool', |
| 'tool_failed', |
| 'Child run did not acquire shared runtime capacity', |
| { |
| toolUseId: input.toolUseId, |
| toolName: input.toolName, |
| boundary: 'shared_child_run_permit', |
| stage: 'cancelled_while_waiting', |
| mode, |
| status: abortSignal.aborted ? 'aborted' : 'error', |
| }, |
| ); |
| throw error; |
| } |
| const childStartedAt = this.input.now(); |
| input.trace?.emit('tool', 'tool_started', 'Child run execution started', { |
| toolUseId: input.toolUseId, |
| toolName: input.toolName, |
| boundary: 'child_run_execution', |
| stage: 'started', |
| mode, |
| waitedForPermit: waitingForPermit, |
| activeChildRuns: limiter.activeCount, |
| waitingChildRuns: limiter.waitingCount, |
| capacity: limiter.capacity, |
| }); |
| try { |
| if (abortSignal.aborted) { |
| throw abortSignal.reason instanceof Error |
| ? abortSignal.reason |
| : new Error('Child agent run cancelled before it started'); |
| } |
| const result = await execute(); |
| input.trace?.emit('tool', 'tool_completed', 'Child run execution completed', { |
| toolUseId: input.toolUseId, |
| toolName: input.toolName, |
| boundary: 'child_run_execution', |
| stage: 'completed', |
| mode, |
| status: 'success', |
| durationMs: Math.max(0, this.input.now() - childStartedAt), |
| }); |
| return result; |
| } catch (error) { |
| input.trace?.emit('tool', 'tool_failed', 'Child run execution failed', { |
| toolUseId: input.toolUseId, |
| toolName: input.toolName, |
| boundary: 'child_run_execution', |
| stage: 'completed', |
| mode, |
| status: abortSignal.aborted ? 'aborted' : 'error', |
| durationMs: Math.max(0, this.input.now() - childStartedAt), |
| }); |
| throw error; |
| } finally { |
| permit.release(); |
| } |
| }; |
| |
| const spawnChildAgent = this.input.spawnChildAgent; |
| const spawnChildSession = this.input.spawnChildSession; |
| const prepareChildAgentResume = this.input.prepareChildAgentResume; |
| const resumeChildAgent = this.input.resumeChildAgent; |
| const retryChildAgent = this.input.retryChildAgent; |
| return { |
| ...(spawnChildAgent |
| ? { |
| spawnChildAgent: async (spawnInput) => { |
| const abortSignal = composeChildAbortSignal( |
| input.abortSignal, |
| spawnInput.abortSignal, |
| ); |
| return await runWithPermit( |
| 'spawn', |
| abortSignal, |
| async () => |
| await spawnChildAgent({ |
| parentRunId, |
| spec: spawnInput.spec, |
| prompt: spawnInput.prompt, |
| abortSignal, |
| ...(spawnInput.onReady ? { onReady: spawnInput.onReady } : {}), |
| ...(spawnInput.onEvent ? { onEvent: spawnInput.onEvent } : {}), |
| }), |
| ); |
| }, |
| } |
| : {}), |
| ...(spawnChildSession |
| ? { |
| spawnChildSession: async (spawnInput) => { |
| const abortSignal = composeChildAbortSignal( |
| input.abortSignal, |
| spawnInput.abortSignal, |
| ); |
| return await runWithPermit( |
| 'spawn_session', |
| abortSignal, |
| async () => |
| await spawnChildSession({ |
| parentRunId, |
| parentTurnId: input.turnId, |
| toolCallId: input.toolUseId, |
| agentProfile: spawnInput.agentProfile, |
| prompt: spawnInput.prompt, |
| ...(spawnInput.swarm ? { swarm: spawnInput.swarm } : {}), |
| abortSignal, |
| ...(spawnInput.onReady ? { onReady: spawnInput.onReady } : {}), |
| ...(spawnInput.onEvent ? { onEvent: spawnInput.onEvent } : {}), |
| }), |
| ); |
| }, |
| } |
| : {}), |
| ...(prepareChildAgentResume |
| ? { |
| prepareChildAgentResume: (sourceRunId) => prepareChildAgentResume(sourceRunId), |
| } |
| : {}), |
| ...(resumeChildAgent |
| ? { |
| resumeChildAgent: async (resumeInput) => { |
| const abortSignal = composeChildAbortSignal( |
| input.abortSignal, |
| resumeInput.abortSignal, |
| ); |
| return await runWithPermit( |
| 'resume', |
| abortSignal, |
| async () => |
| await resumeChildAgent({ |
| parentRunId, |
| sourceRunId: resumeInput.sourceRunId, |
| prompt: resumeInput.prompt, |
| abortSignal, |
| ...(resumeInput.onReady ? { onReady: resumeInput.onReady } : {}), |
| ...(resumeInput.onEvent ? { onEvent: resumeInput.onEvent } : {}), |
| }), |
| ); |
| }, |
| } |
| : {}), |
| ...(retryChildAgent |
| ? { |
| retryChildAgent: async (retryInput) => { |
| const abortSignal = composeChildAbortSignal( |
| input.abortSignal, |
| retryInput.abortSignal, |
| ); |
| return await runWithPermit( |
| 'retry', |
| abortSignal, |
| async () => |
| await retryChildAgent({ |
| parentRunId, |
| sourceRunId: retryInput.sourceRunId, |
| ...(retryInput.execution ? { execution: retryInput.execution } : {}), |
| abortSignal, |
| ...(retryInput.onReady ? { onReady: retryInput.onReady } : {}), |
| ...(retryInput.onEvent ? { onEvent: retryInput.onEvent } : {}), |
| }), |
| ); |
| }, |
| } |
| : {}), |
| }; |
| } |
| |
| private async askUserQuestion( |
| turnId: string, |
| toolUseId: string, |
| questions: UserQuestion[], |
| queue: DurableSessionEventSink, |
| ): Promise<UserQuestionResult> { |
| const hostedRun = this.interactionRun(turnId); |
| const requestId = this.input.newId(); |
| const parked = this.userQuestions.park(turnId, requestId, { |
| toolUseId, |
| questions, |
| hosted: hostedRun !== undefined, |
| }); |
| if (hostedRun) void parked.catch(() => undefined); |
| const request: UserQuestionRequestEvent = { |
| type: 'user_question_request', |
| id: this.input.newId(), |
| turnId, |
| ts: this.input.now(), |
| requestId, |
| toolUseId, |
| questions, |
| }; |
| if (hostedRun) { |
| const settlement = this.createUserQuestionSettlement(turnId, requestId); |
| try { |
| await hostedRun.admitUserQuestionRequest({ |
| request, |
| settlement, |
| }); |
| } catch (error) { |
| this.userQuestions.reject( |
| turnId, |
| requestId, |
| error instanceof Error |
| ? error |
| : new RuntimeInteractionFailStopError( |
| `Could not confirm admission for question ${requestId}`, |
| error, |
| ), |
| ); |
| this.finishDeferredQuestionTurnClosure(turnId); |
| await parked.catch(() => undefined); |
| throw interactionAuthorityError( |
| `Could not confirm admission for question ${requestId}`, |
| error, |
| ); |
| } |
| } |
| queue.push(request); |
| const response = await parked; |
| const answerAck = { |
| type: 'user_question_answer_ack', |
| id: this.input.newId(), |
| turnId, |
| ts: this.input.now(), |
| requestId, |
| toolUseId, |
| } as const; |
| if (hostedRun) await this.publishHostedSettlementAck(queue, answerAck); |
| else queue.push(answerAck); |
| return { |
| answers: questions.map((question, index) => ({ |
| question: question.question, |
| answer: response.answers[index] ?? null, |
| })), |
| }; |
| } |
| |
| private async requestSandboxBoundary( |
| turnId: string, |
| toolUseId: string, |
| expansion: SandboxBoundaryExpansion, |
| justification: string, |
| queue: DurableSessionEventSink, |
| ): Promise<SandboxBoundarySettlement> { |
| if (!this.input.createSandboxBoundaryRequest || !this.input.settleSandboxBoundaryRequest) { |
| throw new Error('Sandbox boundary expansion is unavailable on this surface'); |
| } |
| const normalized = await normalizeSandboxBoundaryExpansion(expansion, this.input.header.cwd); |
| if (typeof justification !== 'string' || justification.trim().length === 0) { |
| throw new Error('Sandbox boundary justification must not be empty'); |
| } |
| const requestId = this.input.newId(); |
| // Provenance travels with the row, not with the RuntimeEvent published |
| // below: the event append is fail-open, so a crash between the two would |
| // otherwise leave the request unattributable to the work it blocked. |
| const runId = this.input.getCurrentRunId?.(); |
| const creation = this.input.createSandboxBoundaryRequest({ |
| sessionId: this.input.sessionId, |
| requestId, |
| turnId, |
| ...(runId ? { runId } : {}), |
| expansion: normalized, |
| justification, |
| }); |
| const parked = this.sandboxBoundaryRequests.park(turnId, requestId, { |
| toolUseId, |
| creation, |
| }); |
| void parked.catch(() => undefined); |
| let request: SandboxBoundaryRequest; |
| try { |
| request = await creation; |
| } catch (error) { |
| this.sandboxBoundaryRequests.reject( |
| turnId, |
| requestId, |
| error instanceof Error ? error : new Error(String(error)), |
| ); |
| throw error; |
| } |
| const requestEvent: SandboxBoundaryRequestEvent = { |
| type: 'sandbox_boundary_request', |
| id: this.input.newId(), |
| turnId, |
| ts: this.input.now(), |
| requestId, |
| toolUseId, |
| justification: request.justification, |
| expansion: request.expansion, |
| }; |
| queue.push(requestEvent); |
| const settlement = await parked; |
| const decisionAck: SandboxBoundaryDecisionAckEvent = { |
| type: 'sandbox_boundary_decision_ack', |
| id: this.input.newId(), |
| turnId, |
| ts: this.input.now(), |
| requestId, |
| toolUseId, |
| decision: settlement.request.status === 'denied' ? 'deny' : 'allow', |
| status: |
| settlement.request.status === 'pending' |
| ? (() => { |
| throw new Error(`Sandbox boundary request ${requestId} is still pending`); |
| })() |
| : settlement.request.status, |
| revision: settlement.boundary.revision, |
| }; |
| queue.push(decisionAck); |
| return settlement; |
| } |
| |
| private interactionRun(turnId: string): HostedInteractionBridge | undefined { |
| return this.hostedInteractions.get(turnId); |
| } |
| |
| private async publishHostedSettlementAck( |
| queue: DurableSessionEventSink, |
| event: SessionEvent, |
| ): Promise<void> { |
| try { |
| await queue.pushAndWaitUntilConsumed(event); |
| } catch (error) { |
| throw new RuntimeInteractionFailStopError( |
| `Could not durably acknowledge hosted ${event.type}`, |
| error, |
| ); |
| } |
| } |
| |
| private finishDeferredQuestionTurnClosure(turnId: string): void { |
| if ( |
| !this.deferredQuestionTurnClosures.has(turnId) || |
| this.userQuestions.pendingCount(turnId) !== 0 |
| ) { |
| return; |
| } |
| this.deferredQuestionTurnClosures.delete(turnId); |
| this.userQuestions.endTurn( |
| turnId, |
| (requestId) => |
| new RuntimeInteractionInvariantError( |
| `Hosted question ${requestId} escaped exact Run closure`, |
| ), |
| ); |
| } |
| |
| private createUserQuestionSettlement( |
| turnId: string, |
| requestId: string, |
| ): HostedUserQuestionSettlement { |
| return Object.freeze({ |
| applyAnswer: async (answer: HostedUserQuestionAnswer): Promise<void> => { |
| if (Object.hasOwn(answer, 'requestId')) { |
| throw new RuntimeInteractionInvariantError( |
| `Question settlement ${requestId} received a routed answer`, |
| ); |
| } |
| const pending = this.userQuestions |
| .entries(turnId) |
| .find(([candidateId]) => candidateId === requestId)?.[1]; |
| if ( |
| !pending || |
| !this.settleUserQuestionAnswer( |
| turnId, |
| { requestId, answers: [...answer.answers] }, |
| pending, |
| ) |
| ) { |
| throw new RuntimeInteractionInvariantError( |
| `Question settlement did not take ${requestId} from turn ${turnId}`, |
| ); |
| } |
| }, |
| applyClosure: async (reason: RuntimeUserQuestionClosureReason): Promise<void> => { |
| if (!this.closeUserQuestion(turnId, requestId, reason)) { |
| throw new RuntimeInteractionInvariantError( |
| `Question closure did not take ${requestId} from turn ${turnId}`, |
| ); |
| } |
| }, |
| }); |
| } |
| } |
| |
| function isInteractionControlError(error: unknown): boolean { |
| return ( |
| error instanceof RuntimeInteractionAdmissionRejectedError || |
| error instanceof RuntimeInteractionClosedError || |
| error instanceof RuntimeInteractionInvariantError || |
| error instanceof RuntimeInteractionFailStopError |
| ); |
| } |
| |
| function interactionAuthorityError(message: string, error: unknown): Error { |
| return isInteractionControlError(error) |
| ? (error as Error) |
| : new RuntimeInteractionFailStopError(message, error); |
| } |
| |
| /** |
| * Recoverable message returned when a gated tool is invoked before its group is |
| * loaded. Tells the model exactly how to self-correct: load via `load_tools`, |
| * then retry on a later step. |
| */ |
| export function formatDeferredNotLoadedText(toolName: string): string { |
| return ( |
| `Tool "${toolName}" is available but not loaded yet. ` + |
| `Call load_tools to load its group first, then call "${toolName}" on a later step.` |
| ); |
| } |
| |
| /** |
| * Canonical key for a tool call's args; order-independent so identical calls |
| * match. Hashed, not the raw args, so large Write/Edit payloads are not retained |
| * (only the last signature is kept per turn). Args that cannot be canonicalized |
| * (cyclic / throwing getters — impossible for JSON tool args, but be safe) fall |
| * back to the unique call id, so distinct calls never collapse into one signature |
| * and trip a false block, and no raw args are retained. |
| */ |
| function loopGateArgsKey(args: unknown, callId: string): string { |
| try { |
| return stableHash(args ?? null); |
| } catch { |
| return `unhashable:${callId}`; |
| } |
| } |
| |
| function computerUseSemanticSignature(args: unknown): string | undefined { |
| if (!args || typeof args !== 'object' || Array.isArray(args)) return undefined; |
| const record = args as Record<string, unknown>; |
| if ( |
| record.action !== 'click_element' && |
| record.action !== 'set_value' && |
| record.action !== 'select_text' && |
| record.action !== 'secondary_action' |
| ) |
| return undefined; |
| try { |
| const elementIdentity = stableElementIdentity(record.element_identity); |
| return stableHash({ |
| action: record.action, |
| app: record.app, |
| window_id: record.window_id, |
| ...(elementIdentity === undefined |
| ? { element_id: record.element_id } |
| : { element_identity: elementIdentity }), |
| value: record.value, |
| text: record.text, |
| }); |
| } catch { |
| return undefined; |
| } |
| } |
| |
| function stableElementIdentity(value: unknown): unknown { |
| if (!value || typeof value !== 'object' || Array.isArray(value)) return value; |
| const record = value as Record<string, unknown>; |
| return { |
| role: record.role, |
| label: record.label, |
| value: record.value, |
| frame: record.frame, |
| }; |
| } |
| |
| /** |
| * Recoverable message returned when the loop-gate blocks a repeated identical |
| * failing call. Tells the model the retry is pointless and to change its approach. |
| */ |
| export function formatLoopGateText(toolName: string): string { |
| return ( |
| `Blocked: this exact ${toolName} call (identical arguments) has already failed ` + |
| `repeatedly with no change between attempts, so it was not run again — the result ` + |
| `would be the same. Change the arguments or take a different step (for example ` + |
| `Read the file or inspect the relevant state) before retrying.` |
| ); |
| } |
| |
| export function formatAmbiguousComputerLoopGateText(): string { |
| return ( |
| 'Blocked: this Computer Use semantic target was already rejected as ambiguous ' + |
| 'after a fresh observation. Do not retry the same element identity or guess ' + |
| 'between duplicates; choose a uniquely identified target or stop.' |
| ); |
| } |
| |
| export function formatSyntheticToolErrorText(error: unknown): string { |
| const raw = error instanceof Error ? error.message : String(error); |
| const redacted = redactSecrets(raw || 'Tool failed'); |
| if (redacted.length <= TOOL_ERROR_RESULT_MAX_CHARS) return redacted; |
| return `${redacted.slice(0, TOOL_ERROR_RESULT_MAX_CHARS - 1)}…`; |
| } |
| |
| function sandboxBoundaryFailureSignal( |
| metadata: ReturnType<typeof serializeSandboxError>, |
| ): Extract<ToolResultContent, { kind: 'text' }>['sandboxFailure'] { |
| if (metadata?.reason !== 'sandbox_boundary_required' && metadata?.reason !== 'requires_bypass') { |
| return undefined; |
| } |
| return { |
| reason: metadata.reason, |
| ...(metadata.requiredExpansion |
| ? { requiredExpansion: metadata.requiredExpansion as SandboxBoundaryExpansion } |
| : {}), |
| }; |
| } |
| |
| function uncertainOutcomeSignalFromError(error: unknown): ToolUncertainOutcomeSignal | undefined { |
| if (!(error instanceof ToolOutcomeUnknownError)) return undefined; |
| return { |
| code: 'outcome_unknown', |
| retrySafe: false, |
| }; |
| } |
| |
| function coerceResultContent(raw: unknown): ToolResultContent { |
| if (typeof raw === 'string') return { kind: 'text', text: raw }; |
| if (raw && typeof raw === 'object') { |
| const obj = raw as { kind?: string; text?: string }; |
| if (typeof obj.kind === 'string') { |
| try { |
| return decodeCanonicalToolResultContent(raw); |
| } catch { |
| return { kind: 'json', value: raw }; |
| } |
| } |
| if (typeof obj.text === 'string') return { kind: 'text', text: obj.text }; |
| return { kind: 'json', value: raw }; |
| } |
| return { kind: 'text', text: String(raw ?? '') }; |
| } |
| |
| function coerceTerminalFailure( |
| tool: MakaTool, |
| cwd: string, |
| args: unknown, |
| err: unknown, |
| ): { |
| content: Extract<ToolResultContent, { kind: 'terminal' }>; |
| message: string; |
| sandboxDenied: boolean; |
| } | null { |
| if (tool.name !== 'Bash' || !err || typeof err !== 'object') return null; |
| const error = err as { |
| code?: unknown; |
| stdout?: unknown; |
| stderr?: unknown; |
| stdoutTruncated?: unknown; |
| stderrTruncated?: unknown; |
| reason?: unknown; |
| sandboxed?: unknown; |
| sandboxType?: unknown; |
| }; |
| if (typeof error.code !== 'number') return null; |
| const command = |
| args && typeof args === 'object' && typeof (args as { command?: unknown }).command === 'string' |
| ? (args as { command: string }).command |
| : ''; |
| const stdout = redactSecrets(String(error.stdout ?? '')); |
| const stderr = redactSecrets(String(error.stderr ?? '')); |
| const sandboxDenied = error.reason === 'sandbox_denial' && error.sandboxed === true; |
| return { |
| content: { |
| kind: 'terminal', |
| cwd, |
| cmd: redactSecrets(command), |
| status: error.code === 124 ? 'timed_out' : error.code === 130 ? 'cancelled' : 'failed', |
| exitCode: error.code, |
| output: { |
| mode: 'pipes', |
| stdout, |
| stderr, |
| stdoutTruncated: error.stdoutTruncated === true, |
| stderrTruncated: error.stderrTruncated === true, |
| redacted: stdout !== String(error.stdout ?? '') || stderr !== String(error.stderr ?? ''), |
| }, |
| ...(sandboxDenied |
| ? { |
| sandboxDenial: { |
| likely: true, |
| ...(error.sandboxType === 'macos-seatbelt' || error.sandboxType === 'linux' |
| ? { backend: error.sandboxType } |
| : {}), |
| }, |
| } |
| : {}), |
| }, |
| // The in-turn result the model acts on is just this message (the structured |
| // content above goes to session history). Without the actual output the |
| // model is blind to *why* the command failed, so fold in a bounded tail of |
| // stderr/stdout — the tail is where shell errors land. |
| message: buildTerminalFailureMessage(error.code, stdout, stderr, sandboxDenied), |
| sandboxDenied, |
| }; |
| } |
| |
| function buildTerminalFailureMessage( |
| code: number, |
| stdout: string, |
| stderr: string, |
| sandboxDenied: boolean, |
| ): string { |
| const parts = [`命令退出码 ${code}`]; |
| const view = (text: string) => |
| truncateToolOutput(text, { |
| maxLines: 40, |
| maxBytes: 1500, |
| direction: 'tail', |
| }).content.trim(); |
| const stderrView = view(stderr); |
| if (stderrView) parts.push(`--- stderr ---\n${stderrView}`); |
| const stdoutView = view(stdout); |
| if (stdoutView) parts.push(`--- stdout ---\n${stdoutView}`); |
| if (sandboxDenied) { |
| parts.push( |
| '该失败很可能来自 Maka sandbox。请先尝试不扩大边界的替代方案;只有工具明确返回 sandbox_boundary_required 和具体 expansion 时,才能请求会话边界扩张。不要从命令文本猜测权限,也不要静默绕过 sandbox。', |
| ); |
| } |
| return parts.join('\n\n'); |
| } |
| |
| function hasSandboxDenial( |
| content: ToolResultContent, |
| ): content is Extract<ToolResultContent, { kind: 'text' | 'terminal' | 'shell_run' }> { |
| return 'sandboxDenial' in content && content.sandboxDenial?.likely === true; |
| } |
| |
| function sandboxDenialSignalFromError(error: unknown): SandboxDenialSignal | undefined { |
| const metadata = sandboxErrorMetadata(error); |
| if (!metadata) return undefined; |
| const backend = |
| metadata.backend === 'macos-seatbelt' || metadata.backend === 'linux' |
| ? metadata.backend |
| : undefined; |
| if (metadata.reason === 'sandbox_denial' || metadata.reason === 'sandbox_denied') { |
| return { likely: true, ...(backend ? { backend } : {}) }; |
| } |
| return undefined; |
| } |
| |
| function sandboxDenialKey(toolName: string, cwd: string, args: unknown): string { |
| const command = |
| args && typeof args === 'object' && typeof (args as { command?: unknown }).command === 'string' |
| ? (args as { command: string }).command |
| : ''; |
| return `${toolName}\u0000${cwd}\u0000${command}`; |
| } |
| |
| function deriveToolResultStatus( |
| content: ToolResultContent, |
| raw?: unknown, |
| ): ToolInvocationRecord['status'] { |
| if ( |
| raw && |
| typeof raw === 'object' && |
| typeof (raw as { error?: unknown }).error === 'string' && |
| (raw as { error: string }).error.length > 0 |
| ) |
| return 'error'; |
| if (content.kind === 'explore_agent' && content.ok === false) { |
| return content.reason === 'aborted' ? 'aborted' : 'error'; |
| } |
| if (content.kind === 'subagent') { |
| if (content.status === 'completed') return 'success'; |
| if (content.status === 'cancelled') return 'aborted'; |
| return 'error'; |
| } |
| if (content.kind === 'agent_swarm') { |
| return content.status === 'cancelled' ? 'aborted' : 'success'; |
| } |
| if (content.kind === 'rive_workflow' && content.ok === false) return 'error'; |
| if (content.kind === 'web_search_error') return 'error'; |
| // Bash returns terminal facts instead of throwing for ordinary shell failure. |
| // The explicit status is the shared classification point for isError, |
| // telemetry, and loop-gate failure streaks. |
| if (content.kind === 'terminal') { |
| if (content.status === 'completed') return 'success'; |
| if (content.status === 'cancelled') return 'aborted'; |
| return 'error'; |
| } |
| if ( |
| content.kind === 'shell_run' && |
| content.operation?.kind === 'pty_control' && |
| content.operation.failed |
| ) |
| return 'error'; |
| // All other structured results are successful tool executions. That includes |
| // ShellRun observations: their embedded process status stays model-visible, |
| // but reading or returning the observation itself succeeded. |
| return 'success'; |
| } |
| |
| function summarizeToolResultForTelemetry( |
| content: ToolResultContent, |
| ): NonNullable<ToolInvocationRecord['resultSummary']> { |
| if (content.kind === 'agent_swarm') { |
| const projection = projectAgentSwarmResult(content); |
| return { |
| kind: content.kind, |
| status: projection.status, |
| itemCount: projection.itemCount, |
| startedItemCount: projection.startedItemCount, |
| completedItemCount: projection.completedItemCount, |
| failedItemCount: projection.failedItemCount, |
| cancelledItemCount: projection.cancelledItemCount, |
| artifactCount: projection.artifactCount, |
| }; |
| } |
| if (content.kind === 'terminal' || content.kind === 'shell_run' || content.kind === 'subagent') { |
| return { kind: content.kind, status: content.status }; |
| } |
| if (content.kind === 'explore_agent') { |
| return { |
| kind: content.kind, |
| status: content.terminalStatus ?? (content.ok ? 'completed' : 'failed'), |
| }; |
| } |
| if (content.kind === 'rive_workflow') { |
| return { |
| kind: content.kind, |
| status: content.state ?? (content.ok ? 'completed' : 'failed'), |
| }; |
| } |
| return { kind: content.kind }; |
| } |
| |
| function isAmbiguousComputerFailure(raw: unknown): boolean { |
| return Boolean( |
| raw && |
| typeof raw === 'object' && |
| (raw as { error?: unknown }).error === 'stale_frame' && |
| (raw as { failureClass?: unknown }).failureClass === 'ambiguous_target', |
| ); |
| } |
| |
| function durableAttemptKey(turnId: string, toolUseId: string): string { |
| return JSON.stringify([turnId, toolUseId]); |
| } |
| |
| function providerToolErrorMessage(output: unknown): string | undefined { |
| if (!output || typeof output !== 'object' || Array.isArray(output)) return undefined; |
| const record = output as Record<string, unknown>; |
| if (typeof record.error !== 'string' || record.error.length === 0) return undefined; |
| if (typeof record.modelText === 'string' && record.modelText.length > 0) { |
| return record.modelText; |
| } |
| if (typeof record.text === 'string' && record.text.length > 0) { |
| return record.text; |
| } |
| return record.error; |
| } |
| |
| function summarizeArgs(toolName: string, args: unknown): string { |
| const projected = projectToolActivityArgs(toolName, args); |
| const raw = typeof projected === 'string' ? projected : JSON.stringify(projected ?? null); |
| const text = toolName === 'WriteStdin' ? raw : redactSecrets(raw); |
| return text.length <= 512 ? text : `${text.slice(0, 511)}…`; |
| } |
| |
| function summarizePersistedArgs(args: unknown): string { |
| const raw = typeof args === 'string' ? args : JSON.stringify(args ?? null); |
| const text = redactSecrets(raw); |
| return text.length <= 512 ? text : `${text.slice(0, 511)}…`; |
| } |
| |
| function describeToolIntent(tool: MakaTool, args: unknown): string | undefined { |
| if (tool.categoryHint !== 'subagent' || tool.name !== 'ExploreAgent') return undefined; |
| if (!args || typeof args !== 'object') return undefined; |
| const objective = (args as { objective?: unknown }).objective; |
| if (typeof objective !== 'string') return undefined; |
| const normalized = redactSecrets(objective.replace(/\s+/g, ' ').trim()); |
| if (normalized.length === 0) return undefined; |
| const capped = normalized.length <= 180 ? normalized : `${normalized.slice(0, 179)}…`; |
| return `只读探索:${capped}`; |
| } |
| |
| function byteLength(value: unknown): number { |
| if (value === undefined) return 0; |
| const text = typeof value === 'string' ? value : JSON.stringify(value ?? null); |
| return Buffer.byteLength(text, 'utf8'); |
| } |
| |
| function snapshotToolArgs(value: unknown): unknown { |
| return snapshotJsonValue(value, new WeakSet<object>()); |
| } |
| |
| function snapshotJsonValue(value: unknown, seen: WeakSet<object>): unknown { |
| if (value === null || typeof value !== 'object') return value; |
| if (seen.has(value)) throw new Error('Tool arguments must not contain cycles'); |
| seen.add(value); |
| if (Array.isArray(value)) { |
| return Object.freeze(value.map((entry) => snapshotJsonValue(entry, seen))); |
| } |
| const output: Record<string, unknown> = {}; |
| for (const key of Object.keys(value)) { |
| const descriptor = Object.getOwnPropertyDescriptor(value, key); |
| if (!descriptor || !('value' in descriptor)) { |
| throw new Error(`Tool argument ${key} must be a plain data property`); |
| } |
| output[key] = snapshotJsonValue(descriptor.value, seen); |
| } |
| return Object.freeze(output); |
| } |