Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (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 / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

401 lines
13 KiB
TypeScript

import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { isPlainRecord } from '@sim/utils/object'
import {
ASYNC_TOOL_CONFIRMATION_STATUS,
type AsyncTerminalCompletionSnapshot,
isAsyncTerminalConfirmationStatus,
} from '@/lib/copilot/async-runs/lifecycle'
import { replaceTerminalAsyncToolCallResult } from '@/lib/copilot/async-runs/repository'
import { MothershipStreamV1ToolOutcome } from '@/lib/copilot/generated/mothership-stream-v1'
import { waitForToolConfirmation } from '@/lib/copilot/persistence/tool-confirm'
import {
unsealClientToolCompletion,
unsealClientToolContext,
} from '@/lib/copilot/request/tools/client-completion-seal.server'
import { inspectToolResultForCopilot } from '@/lib/copilot/request/tools/resolved-secret-result'
import {
type AsyncWorkflowDeploymentError,
createStructuralWorkflowToolCompletionData,
getAsyncWorkflowDeploymentError,
getWorkflowToolCompletionExecutionId,
getWorkflowToolCompletionMessage,
getWorkflowToolConfirmationStatus,
} from '@/lib/copilot/tools/workflow-tools'
import { getTrustedWorkflowToolExecution } from '@/lib/workflows/executor/execution-state'
import type { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
const logger = createLogger('CopilotClientToolWaiter')
/**
* Wait for a client-executable workflow tool to report back.
*
* Current browser runtime outcomes are:
* - `success`, `error`, `cancelled`: the workflow finished in the browser
* - `background`: the browser detached on `pagehide`, so the server should stop
* waiting for a foreground result
*/
export async function waitForToolCompletion(
toolCallId: string,
timeoutMs: number,
abortSignal?: AbortSignal
): Promise<AsyncTerminalCompletionSnapshot | null> {
const decision = await waitForToolConfirmation(toolCallId, timeoutMs, abortSignal, {
acceptStatus: (status) =>
status === MothershipStreamV1ToolOutcome.success ||
status === MothershipStreamV1ToolOutcome.error ||
status === ASYNC_TOOL_CONFIRMATION_STATUS.background ||
status === MothershipStreamV1ToolOutcome.cancelled,
})
if (decision && isAsyncTerminalConfirmationStatus(decision.status)) {
return { ...decision, status: decision.status }
}
return null
}
interface WaitForClientToolCompletionOptions {
toolCallId: string
runId?: string
userId: string
timeoutMs: number
abortSignal?: AbortSignal
registry?: ResolvedSecretTraceRegistry
}
function getGenericCompletionMessage(status: AsyncTerminalCompletionSnapshot['status']): string {
if (status === MothershipStreamV1ToolOutcome.success) return 'Tool completed'
if (status === ASYNC_TOOL_CONFIRMATION_STATUS.background) return 'Tool is running in background'
if (status === MothershipStreamV1ToolOutcome.cancelled) return 'Tool cancelled'
return 'Tool failed'
}
/**
* Restores a generic browser/terminal result from its sealed transport envelope,
* projects active Secrets values, then replaces the durable row before delivery.
*/
export async function waitForClientToolCompletion({
toolCallId,
runId,
userId,
timeoutMs,
abortSignal,
registry,
}: WaitForClientToolCompletionOptions): Promise<AsyncTerminalCompletionSnapshot | null> {
const completion = await waitForToolCompletion(toolCallId, timeoutMs, abortSignal)
if (!completion) return null
const toolRegistry = registry?.forkForInputPaths([])
const genericMessage = getGenericCompletionMessage(completion.status)
const binding = runId ? { toolCallId, runId, userId } : undefined
const registryCanImport = toolRegistry !== undefined && !toolRegistry.isPermanentlyIncomplete()
const finishPendingActivation = toolRegistry?.beginPendingActivation()
let content: Awaited<ReturnType<typeof unsealClientToolCompletion>> = null
try {
const [sealedContent, sealedContext] =
binding && registry && toolRegistry && registryCanImport
? await Promise.all([
unsealClientToolCompletion(completion.data, binding),
unsealClientToolContext(completion.data, binding, registry),
])
: [null, null]
if (toolRegistry && registryCanImport) {
if (!sealedContent || !sealedContext) {
toolRegistry.markIncomplete()
} else {
const imported = await toolRegistry.importProvenance(sealedContext.provenance, {
trusted: true,
})
if (!imported || !sealedContext.provenance.complete) {
toolRegistry.markIncomplete()
} else {
content = sealedContent
}
}
}
} catch {
toolRegistry?.markIncomplete()
} finally {
finishPendingActivation?.()
}
if (!toolRegistry?.isComplete()) content = null
const rawOutput: Record<string, unknown> = {
...(content?.message !== undefined ? { message: content.message } : {}),
...(content && Object.hasOwn(content, 'data') ? { data: content.data } : {}),
}
const succeeded = completion.status === MothershipStreamV1ToolOutcome.success
const projection = inspectToolResultForCopilot(
{
success: succeeded,
output: rawOutput,
...(!succeeded ? { error: content?.message ?? genericMessage } : {}),
},
toolRegistry
)
const projected = projection.result
const projectedOutput = isPlainRecord(projected.output) ? projected.output : undefined
const modelSucceeded = succeeded && projected.success
const message =
typeof projectedOutput?.message === 'string'
? projectedOutput.message
: !projected.success && projected.error
? projected.error
: genericMessage
const data =
projectedOutput && Object.hasOwn(projectedOutput, 'data') ? projectedOutput.data : undefined
const terminalData =
data === undefined
? modelSucceeded
? { success: true }
: { error: message }
: data === null && modelSucceeded
? { success: true, data: null }
: data
if (completion.status !== ASYNC_TOOL_CONFIRMATION_STATUS.background) {
const status =
completion.status === MothershipStreamV1ToolOutcome.success
? modelSucceeded
? 'completed'
: 'failed'
: completion.status === MothershipStreamV1ToolOutcome.cancelled
? 'cancelled'
: 'failed'
try {
const updated = await replaceTerminalAsyncToolCallResult({
toolCallId,
status,
result: terminalData,
error: modelSucceeded ? null : message,
})
if (!updated) {
logger.warn('Client tool row was no longer terminal during safe payload update', {
toolCallId,
})
}
} catch (error) {
logger.warn('Failed to persist projected client tool result', {
toolCallId,
error: getErrorMessage(error),
})
}
}
if (projection.safe && registry && toolRegistry?.isComplete()) {
registry.mergeToolCallRegistry(toolRegistry)
}
return {
status:
completion.status === MothershipStreamV1ToolOutcome.success && !modelSucceeded
? MothershipStreamV1ToolOutcome.error
: completion.status,
message,
data: terminalData,
}
}
interface WaitForWorkflowToolCompletionOptions {
toolCallId: string
workflowId?: string
timeoutMs: number
abortSignal?: AbortSignal
registry?: ResolvedSecretTraceRegistry
}
function structuralWorkflowCompletion(
status: AsyncTerminalCompletionSnapshot['status'],
workflowId?: string,
executionId?: string,
deploymentError?: AsyncWorkflowDeploymentError
): AsyncTerminalCompletionSnapshot {
return {
status,
message: deploymentError?.message ?? getWorkflowToolCompletionMessage(status),
data: createStructuralWorkflowToolCompletionData(
status,
workflowId,
executionId,
deploymentError
),
}
}
/**
* Restores a client-run workflow result from the bound server execution log.
* The browser confirmation is only a wakeup and structural identity carrier.
*/
export async function waitForWorkflowToolCompletion({
toolCallId,
workflowId,
timeoutMs,
abortSignal,
registry,
}: WaitForWorkflowToolCompletionOptions): Promise<AsyncTerminalCompletionSnapshot | null> {
const toolRegistry = registry?.forkForInputPaths([])
const finishPendingActivation = toolRegistry?.beginPendingActivation()
let completion: AsyncTerminalCompletionSnapshot | null = null
let trustedExecution: Awaited<ReturnType<typeof getTrustedWorkflowToolExecution>> = null
try {
completion = await waitForToolCompletion(toolCallId, timeoutMs, abortSignal)
if (!completion) {
toolRegistry?.markIncomplete()
return null
}
const executionId = getWorkflowToolCompletionExecutionId(completion.data)
const deploymentError = getAsyncWorkflowDeploymentError(completion.data)
if (completion.status === ASYNC_TOOL_CONFIRMATION_STATUS.background) {
toolRegistry?.markIncomplete()
return structuralWorkflowCompletion(completion.status, workflowId, executionId)
}
if (!workflowId || !executionId) {
toolRegistry?.markIncomplete()
const structuralStatus =
completion.status === MothershipStreamV1ToolOutcome.success
? MothershipStreamV1ToolOutcome.error
: completion.status
return structuralWorkflowCompletion(
structuralStatus,
workflowId,
executionId,
deploymentError
)
}
try {
trustedExecution = await getTrustedWorkflowToolExecution(executionId, workflowId, toolCallId)
} catch (error) {
logger.warn('Failed to restore bound workflow tool execution', {
toolCallId,
workflowId,
executionId,
error: getErrorMessage(error),
})
}
if (!trustedExecution) {
toolRegistry?.markIncomplete()
return structuralWorkflowCompletion(completion.status, workflowId, executionId)
}
if (!trustedExecution.contentAvailable) {
toolRegistry?.markIncomplete()
return structuralWorkflowCompletion(
getWorkflowToolConfirmationStatus(trustedExecution.status),
workflowId,
executionId
)
}
if (
!toolRegistry ||
toolRegistry.isPermanentlyIncomplete() ||
!trustedExecution.provenance.complete
) {
if (!trustedExecution.provenance.complete) toolRegistry?.markIncomplete()
return structuralWorkflowCompletion(
getWorkflowToolConfirmationStatus(trustedExecution.status),
workflowId,
executionId
)
}
try {
const imported = await toolRegistry.importCrossingProvenance(
trustedExecution.provenance,
{
...(Object.hasOwn(trustedExecution, 'finalOutput')
? { finalOutput: trustedExecution.finalOutput }
: {}),
blockLogs: trustedExecution.blockLogs,
...(trustedExecution.error !== undefined ? { error: trustedExecution.error } : {}),
},
{ trusted: true }
)
if (!imported) toolRegistry.markIncomplete()
} catch (error) {
toolRegistry.markIncomplete()
logger.warn('Failed to import bound workflow provenance', {
toolCallId,
workflowId,
executionId,
error: getErrorMessage(error),
})
}
} finally {
finishPendingActivation?.()
}
if (!completion || !trustedExecution || !workflowId) return completion
const executionId = trustedExecution.executionId
const status = getWorkflowToolConfirmationStatus(trustedExecution.status)
const genericMessage = getWorkflowToolCompletionMessage(status)
const rawData: Record<string, unknown> = {
success: status === MothershipStreamV1ToolOutcome.success,
workflowId,
executionId,
...(Object.hasOwn(trustedExecution, 'finalOutput')
? { output: trustedExecution.finalOutput }
: {}),
logs: trustedExecution.blockLogs,
...(trustedExecution.error !== undefined ? { error: trustedExecution.error } : {}),
...(status === MothershipStreamV1ToolOutcome.cancelled
? { reason: 'user_cancelled', cancelledByUser: true }
: {}),
}
const projection = inspectToolResultForCopilot(
{
success: status === MothershipStreamV1ToolOutcome.success,
output: rawData,
...(status !== MothershipStreamV1ToolOutcome.success
? { error: trustedExecution.error ?? genericMessage }
: {}),
},
toolRegistry
)
const projected = projection.result
const projectedData = isPlainRecord(projected.output) ? projected.output : {}
const data = {
...projectedData,
...createStructuralWorkflowToolCompletionData(status, workflowId, executionId),
}
const message =
status === MothershipStreamV1ToolOutcome.success
? genericMessage
: Object.hasOwn(projected, 'output') && projected.error
? projected.error
: genericMessage
try {
const updated = await replaceTerminalAsyncToolCallResult({
toolCallId,
status: trustedExecution.status,
result: data,
error: status === MothershipStreamV1ToolOutcome.success ? null : message,
})
if (!updated) {
logger.warn('Bound workflow tool row was no longer terminal during safe payload update', {
toolCallId,
workflowId,
executionId,
})
}
} catch (error) {
logger.warn('Failed to persist projected workflow tool result', {
toolCallId,
workflowId,
executionId,
error: getErrorMessage(error),
})
}
if (projection.safe && registry && toolRegistry?.isComplete()) {
registry.mergeToolCallRegistry(toolRegistry)
}
return { status, message, data }
}