d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
561 lines
18 KiB
TypeScript
561 lines
18 KiB
TypeScript
import { createLogger } from '@sim/logger'
|
|
import { getErrorMessage, toError } from '@sim/utils/errors'
|
|
import { ASYNC_TOOL_CONFIRMATION_STATUS } from '@/lib/copilot/async-runs/lifecycle'
|
|
import { markAsyncToolDelivered, upsertAsyncToolCall } from '@/lib/copilot/async-runs/repository'
|
|
import { STREAM_TIMEOUT_MS } from '@/lib/copilot/constants'
|
|
import {
|
|
MothershipStreamV1AsyncToolRecordStatus,
|
|
type MothershipStreamV1ToolCallDescriptor,
|
|
MothershipStreamV1ToolOutcome,
|
|
type MothershipStreamV1ToolResultPayload,
|
|
} from '@/lib/copilot/generated/mothership-stream-v1'
|
|
import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1'
|
|
import { TraceSpan } from '@/lib/copilot/generated/trace-spans-v1'
|
|
import { withCopilotSpan } from '@/lib/copilot/request/otel'
|
|
import {
|
|
isToolArgsDeltaStreamEvent,
|
|
isToolCallStreamEvent,
|
|
isToolResultStreamEvent,
|
|
TOOL_CALL_STATUS,
|
|
} from '@/lib/copilot/request/session'
|
|
import { markToolResultSeen, wasToolResultSeen } from '@/lib/copilot/request/sse-utils'
|
|
import { setTerminalToolCallState } from '@/lib/copilot/request/tool-call-state'
|
|
import { executeToolAndReport, waitForToolCompletion } from '@/lib/copilot/request/tools/executor'
|
|
import type {
|
|
ExecutionContext,
|
|
OrchestratorOptions,
|
|
StreamEvent,
|
|
StreamingContext,
|
|
ToolCallState,
|
|
} from '@/lib/copilot/request/types'
|
|
import { getToolEntry, isSimExecuted } from '@/lib/copilot/tool-executor'
|
|
import { isToolHiddenInUi } from '@/lib/copilot/tools/client/hidden-tools'
|
|
import { getToolDisplayTitle } from '@/lib/copilot/tools/tool-display'
|
|
import { isWorkflowToolName } from '@/lib/copilot/tools/workflow-tools'
|
|
import type { ToolScope } from './types'
|
|
import {
|
|
abortPendingToolIfStreamDead,
|
|
addContentBlock,
|
|
emitSyntheticToolResult,
|
|
ensureTerminalToolCallState,
|
|
flushSubagentThinkingBlock,
|
|
flushThinkingBlock,
|
|
getScopedParentToolCallId,
|
|
getScopedSpanIdentity,
|
|
getToolCallUI,
|
|
getToolResultErrorMessage,
|
|
handleClientCompletion,
|
|
inferToolSuccess,
|
|
registerPendingToolPromise,
|
|
} from './types'
|
|
|
|
const logger = createLogger('CopilotToolHandler')
|
|
|
|
function applyToolDisplay(toolCall: ToolCallState | undefined): void {
|
|
if (!toolCall?.name) return
|
|
toolCall.displayTitle = getToolDisplayTitle(
|
|
toolCall.name,
|
|
toolCall.params as Record<string, unknown> | undefined
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Upsert the durable `async_tool_calls` row before the authoritative tool-call
|
|
* SSE frame is forwarded to the client, so `/api/copilot/confirm` can never
|
|
* race ahead of the row that identifies the call. This is the sole
|
|
* persistence point for client-executable tools; gating mirrors the
|
|
* client-wait branch in `dispatchToolExecution`.
|
|
*/
|
|
export async function prePersistClientExecutableToolCall(
|
|
event: StreamEvent,
|
|
context: StreamingContext
|
|
): Promise<void> {
|
|
if (event.type !== 'tool') return
|
|
if (!isToolCallStreamEvent(event)) return
|
|
|
|
const data = event.payload
|
|
const isGenerating = data.status === TOOL_CALL_STATUS.generating
|
|
const isPartial = data.partial === true || isGenerating
|
|
if (isPartial) return
|
|
|
|
const ui = getToolCallUI(data)
|
|
if (!ui.clientExecutable) return
|
|
|
|
const catalogEntry = getToolEntry(data.toolName)
|
|
const isInternal = ui.internal === true || catalogEntry?.internal === true
|
|
if (isInternal) return
|
|
|
|
const delegateWorkflowRunToClient = isWorkflowToolName(data.toolName)
|
|
if (isSimExecuted(data.toolName) && !delegateWorkflowRunToClient) return
|
|
|
|
if (!context.runId) return
|
|
|
|
await upsertAsyncToolCall({
|
|
runId: context.runId,
|
|
toolCallId: data.toolCallId,
|
|
toolName: data.toolName,
|
|
args: data.arguments,
|
|
status: MothershipStreamV1AsyncToolRecordStatus.running,
|
|
}).catch((err) => {
|
|
logger.warn('Failed to pre-persist async tool row before forwarding call frame', {
|
|
toolCallId: data.toolCallId,
|
|
toolName: data.toolName,
|
|
error: getErrorMessage(err),
|
|
})
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Unified tool event handler for both main and subagent scopes.
|
|
*
|
|
* The main vs subagent differences are:
|
|
* - Subagent requires a parentToolCallId and tracks tool calls in subAgentToolCalls
|
|
* - Subagent result phase also updates the subAgentToolCalls record
|
|
* - Subagent call phase stores in both subAgentToolCalls and context.toolCalls
|
|
* - Main call phase only stores in context.toolCalls
|
|
*/
|
|
export async function handleToolEvent(
|
|
event: StreamEvent,
|
|
context: StreamingContext,
|
|
execContext: ExecutionContext,
|
|
options: OrchestratorOptions,
|
|
scope: ToolScope
|
|
): Promise<void> {
|
|
const isSubagent = scope === 'subagent'
|
|
const parentToolCallId = isSubagent ? getScopedParentToolCallId(event, context) : undefined
|
|
|
|
if (isSubagent && !parentToolCallId) return
|
|
|
|
if (event.type !== 'tool') {
|
|
return
|
|
}
|
|
|
|
if (isToolArgsDeltaStreamEvent(event)) {
|
|
return
|
|
}
|
|
|
|
// A tool event breaks the thinking stream. Flush any open thinking
|
|
// block into contentBlocks BEFORE we add the tool_call block, or
|
|
// contentBlocks will end up with tool_call before thinking — which
|
|
// re-renders on reload in the wrong order (Mothership group above
|
|
// the Thinking block, even though thinking happened first). A subagent
|
|
// tool event flushes only its OWN lane so a concurrent sibling's thinking
|
|
// is left intact; a main tool event flushes all subagent lanes.
|
|
if (isSubagent && parentToolCallId) {
|
|
flushSubagentThinkingBlock(context, parentToolCallId)
|
|
} else {
|
|
flushSubagentThinkingBlock(context)
|
|
}
|
|
flushThinkingBlock(context)
|
|
|
|
if (isToolResultStreamEvent(event)) {
|
|
handleResultPhase(event.payload, context, parentToolCallId)
|
|
return
|
|
}
|
|
|
|
if (!isToolCallStreamEvent(event)) {
|
|
return
|
|
}
|
|
|
|
if (!parentToolCallId) {
|
|
context.sawMainToolCall = true
|
|
context.finalAssistantContent = ''
|
|
}
|
|
|
|
await handleCallPhase(
|
|
event.payload,
|
|
context,
|
|
execContext,
|
|
options,
|
|
parentToolCallId,
|
|
scope,
|
|
getScopedSpanIdentity(event)
|
|
)
|
|
}
|
|
|
|
function handleResultPhase(
|
|
data: MothershipStreamV1ToolResultPayload,
|
|
context: StreamingContext,
|
|
parentToolCallId: string | undefined
|
|
): void {
|
|
const { toolCallId, toolName } = data
|
|
const mainToolCall = ensureTerminalToolCallState(context, toolCallId, toolName)
|
|
const { success, hasResultData } = inferToolSuccess(data)
|
|
let status: MothershipStreamV1ToolOutcome
|
|
if (data.status === MothershipStreamV1ToolOutcome.cancelled) {
|
|
status = MothershipStreamV1ToolOutcome.cancelled
|
|
} else if (data.status === MothershipStreamV1ToolOutcome.skipped) {
|
|
status = MothershipStreamV1ToolOutcome.skipped
|
|
} else if (data.status === MothershipStreamV1ToolOutcome.rejected) {
|
|
status = MothershipStreamV1ToolOutcome.rejected
|
|
} else {
|
|
status = success ? MothershipStreamV1ToolOutcome.success : MothershipStreamV1ToolOutcome.error
|
|
}
|
|
const endTime = Date.now()
|
|
const errorMessage =
|
|
!success && status !== MothershipStreamV1ToolOutcome.skipped
|
|
? getToolResultErrorMessage(data) ||
|
|
(status === MothershipStreamV1ToolOutcome.cancelled
|
|
? 'Tool cancelled'
|
|
: status === MothershipStreamV1ToolOutcome.rejected
|
|
? 'Tool rejected'
|
|
: 'Tool failed')
|
|
: undefined
|
|
|
|
if (parentToolCallId) {
|
|
const toolCalls = context.subAgentToolCalls[parentToolCallId] || []
|
|
const subAgentToolCall = toolCalls.find((tc) => tc.id === toolCallId)
|
|
if (subAgentToolCall) {
|
|
setTerminalToolCallState(subAgentToolCall, {
|
|
status,
|
|
...(hasResultData ? { output: data.output } : {}),
|
|
...(errorMessage ? { error: errorMessage } : {}),
|
|
endTime,
|
|
})
|
|
}
|
|
}
|
|
|
|
setTerminalToolCallState(mainToolCall, {
|
|
status,
|
|
...(hasResultData ? { output: data.output } : {}),
|
|
...(errorMessage ? { error: errorMessage } : {}),
|
|
endTime,
|
|
})
|
|
stampToolCallBlockEnd(context, toolCallId, endTime)
|
|
markToolResultSeen(toolCallId)
|
|
}
|
|
|
|
function stampToolCallBlockEnd(
|
|
context: StreamingContext,
|
|
toolCallId: string,
|
|
endTime: number
|
|
): void {
|
|
for (let i = context.contentBlocks.length - 1; i >= 0; i--) {
|
|
const block = context.contentBlocks[i]
|
|
if (block.type === 'tool_call' && block.toolCall?.id === toolCallId) {
|
|
if (block.endedAt === undefined) block.endedAt = endTime
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
async function handleCallPhase(
|
|
data: MothershipStreamV1ToolCallDescriptor,
|
|
context: StreamingContext,
|
|
execContext: ExecutionContext,
|
|
options: OrchestratorOptions,
|
|
parentToolCallId: string | undefined,
|
|
scope: ToolScope,
|
|
spanIdentity: { spanId?: string; parentSpanId?: string }
|
|
): Promise<void> {
|
|
const { toolCallId, toolName } = data
|
|
const args = data.arguments
|
|
const isGenerating = data.status === TOOL_CALL_STATUS.generating
|
|
const isPartial = data.partial === true || isGenerating
|
|
const existing = context.toolCalls.get(toolCallId)
|
|
const isSubagent = scope === 'subagent'
|
|
const ui = getToolCallUI(data)
|
|
|
|
if (isPartial && shouldDelayVfsPlaceholder(toolName, args)) return
|
|
|
|
if (isSubagent) {
|
|
if (wasToolResultSeen(toolCallId) || existing?.endTime) {
|
|
if (existing && !existing.name && toolName) existing.name = toolName
|
|
if (existing && !existing.params && args) existing.params = args
|
|
applyToolDisplay(existing)
|
|
return
|
|
}
|
|
} else {
|
|
if (
|
|
existing?.endTime ||
|
|
(existing && existing.status !== 'pending' && existing.status !== 'executing')
|
|
) {
|
|
if (!existing.name && toolName) existing.name = toolName
|
|
if (!existing.params && args) existing.params = args
|
|
applyToolDisplay(existing)
|
|
return
|
|
}
|
|
}
|
|
|
|
if (isSubagent) {
|
|
registerSubagentToolCall(
|
|
context,
|
|
toolCallId,
|
|
toolName,
|
|
args,
|
|
parentToolCallId!,
|
|
ui,
|
|
spanIdentity
|
|
)
|
|
} else {
|
|
registerMainToolCall(context, toolCallId, toolName, args, existing, ui)
|
|
}
|
|
|
|
if (isPartial) return
|
|
if (!isSubagent && wasToolResultSeen(toolCallId)) return
|
|
if (context.pendingToolPromises.has(toolCallId) || existing?.status === 'executing') {
|
|
return
|
|
}
|
|
|
|
const toolCall = context.toolCalls.get(toolCallId)
|
|
if (!toolCall) return
|
|
|
|
// Capture the invoking subagent's channel id so the executor can thread it
|
|
// into the server tool context — this is what scopes the workspace_file ->
|
|
// edit_content intent handoff to one file subagent under concurrency.
|
|
if (parentToolCallId) toolCall.parentToolCallId = parentToolCallId
|
|
|
|
const readPath = typeof args?.path === 'string' ? args.path : undefined
|
|
if (toolName === 'read' && readPath?.startsWith('internal/')) return
|
|
|
|
const { clientExecutable, simExecutable, internal } = ui
|
|
const catalogEntry = getToolEntry(toolName)
|
|
const isInternal = internal || catalogEntry?.internal === true
|
|
const staticSimExecuted = isSimExecuted(toolName)
|
|
const willDispatch = !isInternal && (staticSimExecuted || simExecutable || clientExecutable)
|
|
logger.info('Tool call routing decision', {
|
|
toolCallId,
|
|
toolName,
|
|
scope,
|
|
isSubagent,
|
|
parentToolCallId,
|
|
executor: data.executor,
|
|
clientExecutable,
|
|
simExecutable,
|
|
staticSimExecuted,
|
|
internal: isInternal,
|
|
hasPendingPromise: context.pendingToolPromises.has(toolCallId),
|
|
existingStatus: existing?.status,
|
|
willDispatch,
|
|
})
|
|
if (isInternal) return
|
|
if (!willDispatch) return
|
|
|
|
await dispatchToolExecution(
|
|
toolCall,
|
|
toolCallId,
|
|
toolName,
|
|
args,
|
|
context,
|
|
execContext,
|
|
options,
|
|
clientExecutable,
|
|
scope
|
|
)
|
|
}
|
|
|
|
function shouldDelayVfsPlaceholder(
|
|
toolName: string,
|
|
args: Record<string, unknown> | undefined
|
|
): boolean {
|
|
return (toolName === 'read' || toolName === 'glob') && !args
|
|
}
|
|
|
|
function removeToolCallContentBlock(context: StreamingContext, toolCallId: string): void {
|
|
for (let i = context.contentBlocks.length - 1; i >= 0; i--) {
|
|
const block = context.contentBlocks[i]
|
|
if (block.type === 'tool_call' && block.toolCall?.id === toolCallId) {
|
|
context.contentBlocks.splice(i, 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
function registerSubagentToolCall(
|
|
context: StreamingContext,
|
|
toolCallId: string,
|
|
toolName: string,
|
|
args: Record<string, unknown> | undefined,
|
|
parentToolCallId: string,
|
|
ui: { title?: string; phaseLabel?: string; hidden?: boolean },
|
|
spanIdentity: { spanId?: string; parentSpanId?: string }
|
|
): void {
|
|
if (!context.subAgentToolCalls[parentToolCallId]) {
|
|
context.subAgentToolCalls[parentToolCallId] = []
|
|
}
|
|
const hideFromUi = isToolHiddenInUi(toolName) || ui.hidden === true
|
|
let toolCall = context.toolCalls.get(toolCallId)
|
|
if (toolCall) {
|
|
if (!toolCall.name && toolName) toolCall.name = toolName
|
|
if (args && !toolCall.params) toolCall.params = args
|
|
applyToolDisplay(toolCall)
|
|
if (hideFromUi) removeToolCallContentBlock(context, toolCallId)
|
|
} else {
|
|
toolCall = {
|
|
id: toolCallId,
|
|
name: toolName,
|
|
status: 'pending',
|
|
params: args,
|
|
startTime: Date.now(),
|
|
}
|
|
applyToolDisplay(toolCall)
|
|
context.toolCalls.set(toolCallId, toolCall)
|
|
const parentToolCall = context.toolCalls.get(parentToolCallId)
|
|
if (!hideFromUi) {
|
|
addContentBlock(context, {
|
|
type: 'tool_call',
|
|
toolCall,
|
|
calledBy: parentToolCall?.name,
|
|
parentToolCallId,
|
|
...spanIdentity,
|
|
})
|
|
}
|
|
}
|
|
|
|
const subagentToolCalls = context.subAgentToolCalls[parentToolCallId]
|
|
const existingSubagentToolCall = subagentToolCalls.find((tc) => tc.id === toolCallId)
|
|
if (existingSubagentToolCall) {
|
|
if (!existingSubagentToolCall.name && toolName) existingSubagentToolCall.name = toolName
|
|
if (args && !existingSubagentToolCall.params) existingSubagentToolCall.params = args
|
|
applyToolDisplay(existingSubagentToolCall)
|
|
} else {
|
|
subagentToolCalls.push(toolCall)
|
|
}
|
|
}
|
|
|
|
function registerMainToolCall(
|
|
context: StreamingContext,
|
|
toolCallId: string,
|
|
toolName: string,
|
|
args: Record<string, unknown> | undefined,
|
|
existing: ToolCallState | undefined,
|
|
ui: { title?: string; phaseLabel?: string; hidden?: boolean }
|
|
): void {
|
|
const hideFromUi = isToolHiddenInUi(toolName) || ui.hidden === true
|
|
if (existing) {
|
|
if (args && !existing.params) existing.params = args
|
|
applyToolDisplay(existing)
|
|
if (hideFromUi) {
|
|
removeToolCallContentBlock(context, toolCallId)
|
|
return
|
|
}
|
|
if (
|
|
!hideFromUi &&
|
|
!context.contentBlocks.some((b) => b.type === 'tool_call' && b.toolCall?.id === toolCallId)
|
|
) {
|
|
addContentBlock(context, { type: 'tool_call', toolCall: existing })
|
|
}
|
|
} else {
|
|
const created: ToolCallState = {
|
|
id: toolCallId,
|
|
name: toolName,
|
|
status: 'pending',
|
|
params: args,
|
|
startTime: Date.now(),
|
|
}
|
|
applyToolDisplay(created)
|
|
context.toolCalls.set(toolCallId, created)
|
|
if (!hideFromUi) {
|
|
addContentBlock(context, { type: 'tool_call', toolCall: created })
|
|
}
|
|
}
|
|
}
|
|
|
|
async function dispatchToolExecution(
|
|
toolCall: ToolCallState,
|
|
toolCallId: string,
|
|
toolName: string,
|
|
args: Record<string, unknown> | undefined,
|
|
context: StreamingContext,
|
|
execContext: ExecutionContext,
|
|
options: OrchestratorOptions,
|
|
clientExecutable: boolean,
|
|
scope: ToolScope
|
|
): Promise<void> {
|
|
const scopeLabel = scope === 'subagent' ? 'subagent ' : ''
|
|
|
|
const fireToolExecution = () => {
|
|
const pendingPromise = (async () => {
|
|
return executeToolAndReport(toolCallId, context, execContext, options)
|
|
})().catch((err) => {
|
|
logger.error(`Parallel ${scopeLabel}tool execution failed`, {
|
|
toolCallId,
|
|
toolName,
|
|
error: toError(err).message,
|
|
})
|
|
return {
|
|
status: MothershipStreamV1ToolOutcome.error,
|
|
message: 'Tool execution failed',
|
|
data: { error: 'Tool execution failed' },
|
|
}
|
|
})
|
|
registerPendingToolPromise(context, toolCallId, pendingPromise)
|
|
}
|
|
|
|
if (options.interactive === false) {
|
|
if (options.autoExecuteTools !== false) {
|
|
if (!abortPendingToolIfStreamDead(toolCall, toolCallId, options, context)) {
|
|
fireToolExecution()
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
if (clientExecutable) {
|
|
const delegateWorkflowRunToClient = isWorkflowToolName(toolName)
|
|
if (isSimExecuted(toolName) && !delegateWorkflowRunToClient) {
|
|
if (!abortPendingToolIfStreamDead(toolCall, toolCallId, options, context)) {
|
|
fireToolExecution()
|
|
}
|
|
} else {
|
|
toolCall.status = 'executing'
|
|
const pendingPromise = withCopilotSpan(
|
|
TraceSpan.CopilotToolWaitForClientResult,
|
|
{
|
|
[TraceAttr.ToolName]: toolName,
|
|
[TraceAttr.ToolCallId]: toolCallId,
|
|
[TraceAttr.ToolTimeoutMs]: options.timeout || STREAM_TIMEOUT_MS,
|
|
...(context.runId ? { [TraceAttr.RunId]: context.runId } : {}),
|
|
},
|
|
async (span) => {
|
|
const completion = await waitForToolCompletion(
|
|
toolCallId,
|
|
options.timeout || STREAM_TIMEOUT_MS,
|
|
options.abortSignal
|
|
)
|
|
span.setAttribute(TraceAttr.ToolCompletionReceived, completion !== undefined)
|
|
if (completion) {
|
|
span.setAttribute(TraceAttr.ToolOutcome, completion.status)
|
|
}
|
|
handleClientCompletion(toolCall, toolCallId, completion)
|
|
if (completion?.status === ASYNC_TOOL_CONFIRMATION_STATUS.background) {
|
|
await markAsyncToolDelivered(toolCallId).catch((err) => {
|
|
logger.warn(`Failed to mark background ${scopeLabel}tool delivered`, {
|
|
toolCallId,
|
|
toolName,
|
|
error: toError(err).message,
|
|
})
|
|
})
|
|
}
|
|
await emitSyntheticToolResult(toolCallId, toolCall.name, completion, options)
|
|
return (
|
|
completion ?? {
|
|
status: MothershipStreamV1ToolOutcome.error,
|
|
message: 'Tool completion missing',
|
|
data: { error: 'Tool completion missing' },
|
|
}
|
|
)
|
|
}
|
|
).catch((err) => {
|
|
logger.error(`Client-executable ${scopeLabel}tool wait failed`, {
|
|
toolCallId,
|
|
toolName,
|
|
error: toError(err).message,
|
|
})
|
|
return {
|
|
status: MothershipStreamV1ToolOutcome.error,
|
|
message: 'Tool wait failed',
|
|
data: { error: 'Tool wait failed' },
|
|
}
|
|
})
|
|
registerPendingToolPromise(context, toolCallId, pendingPromise)
|
|
}
|
|
return
|
|
}
|
|
|
|
if (options.autoExecuteTools !== false) {
|
|
if (!abortPendingToolIfStreamDead(toolCall, toolCallId, options, context)) {
|
|
fireToolExecution()
|
|
}
|
|
}
|
|
}
|