Files
simstudioai--sim/apps/sim/lib/copilot/request/session/contract.ts
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

476 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { getErrorMessage } from '@sim/utils/errors'
import { isRecordLike } from '@sim/utils/object'
import type {
MothershipStreamV1EventEnvelope,
MothershipStreamV1StreamRef,
MothershipStreamV1StreamScope,
MothershipStreamV1Trace,
} from '@/lib/copilot/generated/mothership-stream-v1'
import {
MothershipStreamV1EventType,
MothershipStreamV1ResourceOp,
MothershipStreamV1RunKind,
MothershipStreamV1SessionKind,
MothershipStreamV1SpanPayloadKind,
MothershipStreamV1TextChannel,
MothershipStreamV1ToolPhase,
} from '@/lib/copilot/generated/mothership-stream-v1'
import type { FilePreviewTargetKind } from './file-preview-session-contract'
type JsonRecord = Record<string, unknown>
const FILE_PREVIEW_PHASE = {
start: 'file_preview_start',
target: 'file_preview_target',
editMeta: 'file_preview_edit_meta',
content: 'file_preview_content',
complete: 'file_preview_complete',
} as const
type EnvelopeToStreamEvent<T> = T extends {
type: infer TType
payload: infer TPayload
scope?: infer TScope
}
? { type: TType; payload: TPayload; scope?: Exclude<TScope, undefined> }
: never
export type SyntheticFilePreviewPhase = (typeof FILE_PREVIEW_PHASE)[keyof typeof FILE_PREVIEW_PHASE]
export interface SyntheticFilePreviewTarget {
kind: FilePreviewTargetKind
fileId?: string
fileName?: string
}
export interface SyntheticFilePreviewStartPayload {
previewPhase: typeof FILE_PREVIEW_PHASE.start
toolCallId: string
toolName: 'workspace_file'
}
export interface SyntheticFilePreviewTargetPayload {
operation?: string
previewPhase: typeof FILE_PREVIEW_PHASE.target
target: SyntheticFilePreviewTarget
title?: string
toolCallId: string
toolName: 'workspace_file'
}
export interface SyntheticFilePreviewEditMetaPayload {
edit: JsonRecord
previewPhase: typeof FILE_PREVIEW_PHASE.editMeta
toolCallId: string
toolName: 'workspace_file'
}
export interface SyntheticFilePreviewContentPayload {
content: string
contentMode: 'delta' | 'snapshot'
edit?: JsonRecord
fileId?: string
fileName: string
operation?: string
previewPhase: typeof FILE_PREVIEW_PHASE.content
previewVersion: number
targetKind?: string
toolCallId: string
toolName: 'workspace_file'
}
export interface SyntheticFilePreviewCompletePayload {
fileId?: string
output?: unknown
previewPhase: typeof FILE_PREVIEW_PHASE.complete
previewVersion?: number
toolCallId: string
toolName: 'workspace_file'
}
export type SyntheticFilePreviewPayload =
| SyntheticFilePreviewStartPayload
| SyntheticFilePreviewTargetPayload
| SyntheticFilePreviewEditMetaPayload
| SyntheticFilePreviewContentPayload
| SyntheticFilePreviewCompletePayload
export interface SyntheticFilePreviewEventEnvelope {
payload: SyntheticFilePreviewPayload
scope?: MothershipStreamV1StreamScope
seq: number
stream: MothershipStreamV1StreamRef
trace?: MothershipStreamV1Trace
ts: string
type: 'tool'
v: 1
}
export type PersistedStreamEventEnvelope =
| MothershipStreamV1EventEnvelope
| SyntheticFilePreviewEventEnvelope
export type ContractStreamEvent = EnvelopeToStreamEvent<MothershipStreamV1EventEnvelope>
export type SyntheticStreamEvent = EnvelopeToStreamEvent<SyntheticFilePreviewEventEnvelope>
export type SessionStreamEvent = ContractStreamEvent | SyntheticStreamEvent
export type StreamEvent = SessionStreamEvent
export type ToolCallStreamEvent = Extract<
ContractStreamEvent,
{ type: 'tool'; payload: { phase: 'call' } }
>
export type ToolArgsDeltaStreamEvent = Extract<
ContractStreamEvent,
{ type: 'tool'; payload: { phase: 'args_delta' } }
>
export type ToolResultStreamEvent = Extract<
ContractStreamEvent,
{ type: 'tool'; payload: { phase: 'result' } }
>
export type SubagentSpanStreamEvent = Extract<
ContractStreamEvent,
{ type: 'span'; payload: { kind: 'subagent' } }
>
export interface ParseStreamEventEnvelopeSuccess {
ok: true
event: PersistedStreamEventEnvelope
}
export interface ParseStreamEventEnvelopeFailure {
errors?: string[]
message: string
ok: false
reason: 'invalid_json' | 'invalid_stream_event'
}
export type ParseStreamEventEnvelopeResult =
| ParseStreamEventEnvelopeSuccess
| ParseStreamEventEnvelopeFailure
// ---------------------------------------------------------------------------
// Structural helpers (CSP-safe no codegen / eval / new Function)
// ---------------------------------------------------------------------------
function isOptionalString(value: unknown): value is string | undefined {
return value === undefined || typeof value === 'string'
}
function isOptionalFiniteNumber(value: unknown): value is number | undefined {
return value === undefined || (typeof value === 'number' && Number.isFinite(value))
}
function isStreamRef(value: unknown): value is MothershipStreamV1StreamRef {
return (
isRecordLike(value) &&
typeof value.streamId === 'string' &&
value.streamId.length > 0 &&
isOptionalString(value.chatId) &&
isOptionalString(value.cursor)
)
}
function isTrace(value: unknown): value is MothershipStreamV1Trace {
return (
isRecordLike(value) &&
typeof value.requestId === 'string' &&
isOptionalString(value.goTraceId) &&
isOptionalString(value.spanId)
)
}
function isStreamScope(value: unknown): value is MothershipStreamV1StreamScope {
return (
isRecordLike(value) &&
value.lane === 'subagent' &&
isOptionalString(value.agentId) &&
isOptionalString(value.parentToolCallId)
)
}
// ---------------------------------------------------------------------------
// Contract envelope validator (replaces Ajv runtime compilation)
//
// Validates the envelope shell (v, seq, ts, stream, trace?, scope?) and that
// `type` is one of the known event types with a non-null payload object.
// Per-payload-variant validation is intentionally lightweight: the server
// already performs strict schema validation; the client only needs enough
// structural checking to safely dispatch inside the switch statement.
// ---------------------------------------------------------------------------
const KNOWN_EVENT_TYPES: ReadonlySet<string> = new Set(Object.values(MothershipStreamV1EventType))
function isValidEnvelopeShell(value: unknown): value is JsonRecord & {
v: 1
seq: number
ts: string
stream: MothershipStreamV1StreamRef
type: string
payload: JsonRecord
} {
if (!isRecordLike(value)) return false
if (value.v !== 1) return false
if (typeof value.seq !== 'number' || !Number.isFinite(value.seq)) return false
if (typeof value.ts !== 'string') return false
if (!isStreamRef(value.stream)) return false
if (value.trace !== undefined && !isTrace(value.trace)) return false
if (value.scope !== undefined && !isStreamScope(value.scope)) return false
if (typeof value.type !== 'string' || !KNOWN_EVENT_TYPES.has(value.type)) return false
if (!isRecordLike(value.payload)) return false
return true
}
function isValidSessionPayload(payload: JsonRecord): boolean {
const kind = payload.kind
if (typeof kind !== 'string') return false
switch (kind) {
case MothershipStreamV1SessionKind.start:
return true
case MothershipStreamV1SessionKind.chat:
return typeof payload.chatId === 'string'
case MothershipStreamV1SessionKind.title:
return typeof payload.title === 'string'
case MothershipStreamV1SessionKind.trace:
return typeof payload.requestId === 'string'
default:
return false
}
}
function isValidTextPayload(payload: JsonRecord): boolean {
return (
(payload.channel === MothershipStreamV1TextChannel.assistant ||
payload.channel === MothershipStreamV1TextChannel.thinking) &&
typeof payload.text === 'string'
)
}
function isValidToolPayload(payload: JsonRecord): boolean {
if (typeof payload.toolCallId !== 'string') return false
if (typeof payload.toolName !== 'string') return false
const phase = payload.phase
return (
phase === MothershipStreamV1ToolPhase.call ||
phase === MothershipStreamV1ToolPhase.args_delta ||
phase === MothershipStreamV1ToolPhase.result
)
}
function isValidSpanPayload(payload: JsonRecord): boolean {
const kind = payload.kind
return (
kind === MothershipStreamV1SpanPayloadKind.subagent ||
kind === MothershipStreamV1SpanPayloadKind.structured_result ||
kind === MothershipStreamV1SpanPayloadKind.subagent_result
)
}
function isValidResourcePayload(payload: JsonRecord): boolean {
return (
(payload.op === MothershipStreamV1ResourceOp.upsert ||
payload.op === MothershipStreamV1ResourceOp.remove) &&
isRecordLike(payload.resource) &&
typeof (payload.resource as JsonRecord).id === 'string' &&
typeof (payload.resource as JsonRecord).type === 'string'
)
}
function isValidRunPayload(payload: JsonRecord): boolean {
const kind = payload.kind
return (
kind === MothershipStreamV1RunKind.checkpoint_pause ||
kind === MothershipStreamV1RunKind.resumed ||
kind === MothershipStreamV1RunKind.compaction_start ||
kind === MothershipStreamV1RunKind.compaction_done
)
}
function isValidErrorPayload(payload: JsonRecord): boolean {
return typeof payload.message === 'string' || typeof payload.error === 'string'
}
function isValidCompletePayload(payload: JsonRecord): boolean {
return typeof payload.status === 'string'
}
function isContractEnvelope(value: unknown): value is MothershipStreamV1EventEnvelope {
if (!isValidEnvelopeShell(value)) return false
const payload = value.payload as JsonRecord
switch (value.type) {
case MothershipStreamV1EventType.session:
return isValidSessionPayload(payload)
case MothershipStreamV1EventType.text:
return isValidTextPayload(payload)
case MothershipStreamV1EventType.tool:
return isValidToolPayload(payload)
case MothershipStreamV1EventType.span:
return isValidSpanPayload(payload)
case MothershipStreamV1EventType.resource:
return isValidResourcePayload(payload)
case MothershipStreamV1EventType.run:
return isValidRunPayload(payload)
case MothershipStreamV1EventType.error:
return isValidErrorPayload(payload)
case MothershipStreamV1EventType.complete:
return isValidCompletePayload(payload)
default:
return false
}
}
// ---------------------------------------------------------------------------
// Synthetic file-preview envelope validators
// ---------------------------------------------------------------------------
function isSyntheticEnvelopeBase(value: unknown): value is Omit<
SyntheticFilePreviewEventEnvelope,
'payload'
> & {
payload?: unknown
} {
return (
isRecordLike(value) &&
value.v === 1 &&
value.type === 'tool' &&
typeof value.seq === 'number' &&
Number.isFinite(value.seq) &&
typeof value.ts === 'string' &&
isStreamRef(value.stream) &&
(value.trace === undefined || isTrace(value.trace)) &&
(value.scope === undefined || isStreamScope(value.scope))
)
}
function isSyntheticFilePreviewTarget(value: unknown): value is SyntheticFilePreviewTarget {
return (
isRecordLike(value) &&
(value.kind === 'new_file' || value.kind === 'file_id') &&
isOptionalString(value.fileId) &&
isOptionalString(value.fileName)
)
}
function isSyntheticFilePreviewPayload(value: unknown): value is SyntheticFilePreviewPayload {
if (!isRecordLike(value)) {
return false
}
if (typeof value.toolCallId !== 'string' || value.toolName !== 'workspace_file') {
return false
}
switch (value.previewPhase) {
case FILE_PREVIEW_PHASE.start:
return true
case FILE_PREVIEW_PHASE.target:
return (
isSyntheticFilePreviewTarget(value.target) &&
isOptionalString(value.operation) &&
isOptionalString(value.title)
)
case FILE_PREVIEW_PHASE.editMeta:
return isRecordLike(value.edit)
case FILE_PREVIEW_PHASE.content:
return (
typeof value.content === 'string' &&
(value.contentMode === 'delta' || value.contentMode === 'snapshot') &&
typeof value.previewVersion === 'number' &&
Number.isFinite(value.previewVersion) &&
typeof value.fileName === 'string' &&
isOptionalString(value.fileId) &&
isOptionalString(value.targetKind) &&
isOptionalString(value.operation) &&
(value.edit === undefined || isRecordLike(value.edit))
)
case FILE_PREVIEW_PHASE.complete:
return isOptionalString(value.fileId) && isOptionalFiniteNumber(value.previewVersion)
default:
return false
}
}
export function isSyntheticFilePreviewEventEnvelope(
value: unknown
): value is SyntheticFilePreviewEventEnvelope {
return isSyntheticEnvelopeBase(value) && isSyntheticFilePreviewPayload(value.payload)
}
// ---------------------------------------------------------------------------
// Stream event type guards
// ---------------------------------------------------------------------------
export function isToolCallStreamEvent(event: SessionStreamEvent): event is ToolCallStreamEvent {
return event.type === 'tool' && isRecordLike(event.payload) && event.payload.phase === 'call'
}
export function isToolArgsDeltaStreamEvent(
event: SessionStreamEvent
): event is ToolArgsDeltaStreamEvent {
return (
event.type === 'tool' && isRecordLike(event.payload) && event.payload.phase === 'args_delta'
)
}
export function isToolResultStreamEvent(event: SessionStreamEvent): event is ToolResultStreamEvent {
return event.type === 'tool' && isRecordLike(event.payload) && event.payload.phase === 'result'
}
export function isSubagentSpanStreamEvent(
event: SessionStreamEvent
): event is SubagentSpanStreamEvent {
return event.type === 'span' && isRecordLike(event.payload) && event.payload.kind === 'subagent'
}
// ---------------------------------------------------------------------------
// Public contract validators & parsers
// ---------------------------------------------------------------------------
export function isContractStreamEventEnvelope(
value: unknown
): value is MothershipStreamV1EventEnvelope {
return isContractEnvelope(value)
}
export function parsePersistedStreamEventEnvelope(value: unknown): ParseStreamEventEnvelopeResult {
if (isContractEnvelope(value)) {
return { ok: true, event: value }
}
if (isSyntheticFilePreviewEventEnvelope(value)) {
return { ok: true, event: value }
}
const hints: string[] = []
if (!isRecordLike(value)) {
hints.push('value is not an object')
} else {
if (value.v !== 1) hints.push(`unexpected v=${JSON.stringify(value.v)}`)
if (typeof value.type !== 'string') hints.push('missing type')
else if (!KNOWN_EVENT_TYPES.has(value.type)) hints.push(`unknown type="${value.type}"`)
if (!isRecordLike(value.payload)) hints.push('missing or invalid payload')
}
return {
ok: false,
reason: 'invalid_stream_event',
message: 'A stream event failed validation.',
...(hints.length > 0 ? { errors: hints } : {}),
}
}
export function parsePersistedStreamEventEnvelopeJson(raw: string): ParseStreamEventEnvelopeResult {
let parsed: unknown
try {
parsed = JSON.parse(raw)
} catch (error) {
const rawMessage = getErrorMessage(error, 'Invalid JSON')
return {
ok: false,
reason: 'invalid_json',
message: 'Received invalid JSON while parsing a stream event.',
...(rawMessage ? { errors: [rawMessage] } : {}),
}
}
return parsePersistedStreamEventEnvelope(parsed)
}