| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| /** |
| * Deterministic SessionEvent -> RuntimeEvent mapping. |
| * |
| * RuntimeKernel owns backend execution and lifecycle. This module is only the |
| * pure vocabulary bridge from backend events to the canonical Runtime Event |
| * ledger. |
| */ |
| |
| import { |
| failureClassFromCompleteStopReason, |
| normalizeMessageContent, |
| type CompleteEvent, |
| type SessionEvent, |
| } from '@maka/core/events'; |
| import type { RuntimeEvent, RuntimeEventStatus } from '@maka/core/runtime-event'; |
| |
| import type { BackendSessionEvent } from '@maka/core/backend-types'; |
| import { compatibilityToolResultProjection } from './durable-tool-result-projection.js'; |
| |
| export interface RuntimeEventMapContext { |
| readonly sessionId: string; |
| readonly invocationId: string; |
| readonly runId: string; |
| readonly turnId: string; |
| readonly branch?: string; |
| readonly now?: () => number; |
| } |
| // ============================================================================ |
| // SessionEvent → RuntimeEvent mapping (placeholder, Phase 4) |
| // ============================================================================ |
| |
| /** The `CompleteEvent.stopReason` literal union, re-declared for portability. */ |
| export type CompleteStopReason = CompleteEvent['stopReason']; |
| |
| /** |
| * Map a `CompleteEvent.stopReason` onto a terminal `RuntimeEventStatus`. |
| * |
| * `end_turn` / `max_tokens` / `*_handoff` all represent the streaming phase |
| * ending normally (control may be handed off, but the run is not a failure), |
| * so they map to `completed`. `user_stop` maps to `aborted`; `error` to |
| * `failed`. An explicit `step_limit` is also failed because the requested work |
| * may be incomplete. Phase 5+ may introduce a richer `waiting`/`handoff` status. |
| */ |
| export function mapCompleteStopReason(reason: CompleteStopReason): RuntimeEventStatus { |
| if (reason === 'user_stop') return 'aborted'; |
| return failureClassFromCompleteStopReason(reason) ? 'failed' : 'completed'; |
| } |
| |
| /** |
| * Shared, mutable tool-name lookup accumulated as the stream flows. The AI |
| * SDK backend emits `ToolStartEvent` (which carries `toolName`) before the |
| * matching `ToolResultEvent` (which does not). Remembering the name keeps |
| * `function_response` content populated without a second source of truth. |
| */ |
| export interface SessionEventMapMemory { |
| toolNameByUseId: Map<string, string>; |
| failureClass?: string; |
| failureContent?: Extract<RuntimeEvent['content'], { kind: 'error' }>; |
| } |
| |
| export function createSessionEventMapMemory(): SessionEventMapMemory { |
| return { toolNameByUseId: new Map() }; |
| } |
| |
| /** |
| * Resolve the runtime identity shared by every event of an invocation. |
| * Reuses the source `SessionEvent.id` as the canonical event id so the |
| * adapter keeps 1:1 dedup linkage with the backend stream. |
| */ |
| function resolveBase(event: SessionEvent, ctx: RuntimeEventMapContext) { |
| const now = ctx.now ?? (() => Date.now()); |
| const base = { |
| id: event.id, |
| invocationId: ctx.invocationId, |
| runId: ctx.runId, |
| sessionId: ctx.sessionId, |
| turnId: ctx.turnId, |
| ts: typeof event.ts === 'number' ? event.ts : now(), |
| partial: false, |
| }; |
| if (ctx.branch !== undefined) (base as { branch?: string }).branch = ctx.branch; |
| return base; |
| } |
| |
| /** |
| * Map one renderer-facing `SessionEvent` onto a canonical `RuntimeEvent`. |
| * |
| * This is the Phase 4 placeholder mapping documented in the architecture |
| * doc. It is deterministic given `(event, ctx, memory)` and carries no I/O. |
| * Role/author choices: |
| * |
| * - model text/thinking → role 'model', author 'agent' |
| * - tool_start (function call) → role 'model', author 'agent' |
| * - tool progress/output deltas → role 'tool', author 'tool' (partial) |
| * - tool_result (function resp) → role 'tool', author 'tool' |
| * - sandbox_boundary_request → role 'system', author 'system' |
| * - sandbox_boundary_decision_ack → role 'system', author 'user' |
| * - user_question_answer_ack → role 'system', author 'user' |
| * - form_request → role 'system', author 'system' |
| * - form_answer_ack → role 'system', author 'user' |
| * - plan_submitted → role 'system', author 'agent' |
| * - token_usage → role 'system', author 'system' |
| * - error → role 'system', author 'system' |
| * - abort → role 'system', author 'system' (terminal) |
| * - complete → role 'system', author 'system' (terminal) |
| * |
| * `memory` is mutated for `tool_start` (records `toolName`) and read for |
| * `tool_result`. Callers SHOULD pass one memory instance per invocation so |
| * the `toolUseId → toolName` linkage is consistent across the stream. |
| */ |
| export function mapSessionEventToRuntimeEvent( |
| event: SessionEvent, |
| ctx: RuntimeEventMapContext, |
| memory: SessionEventMapMemory = createSessionEventMapMemory(), |
| ): RuntimeEvent { |
| if (isHostProjectionSessionEvent(event)) { |
| // These are Host/kernel projection facts, not backend events. The live |
| // ingress drops them, so reaching this line bypassed that authority boundary. |
| // `context_compaction_started` is one of these: synthesized by the Runtime |
| // Host session projector for the renderer's live "compacting" row, never |
| // produced by a backend or the kernel. |
| throw new Error(`${event.type} is not a backend event`); |
| } |
| if (isLegacyPermissionSessionEvent(event)) { |
| throw new Error(`${event.type} is a legacy permission event and is not backend-mappable`); |
| } |
| const narrowed: BackendSessionEvent = event; |
| return mapBackendSessionEvent(narrowed, ctx, memory); |
| } |
| |
| export function isLiveBackendSessionEvent(event: SessionEvent): event is BackendSessionEvent { |
| return !isHostProjectionSessionEvent(event) && !isLegacyPermissionSessionEvent(event); |
| } |
| |
| function isHostProjectionSessionEvent(event: SessionEvent): event is Extract< |
| SessionEvent, |
| { |
| type: |
| | 'queue_update' |
| | 'message_admission' |
| | 'context_compaction_started' |
| | 'client_capability_request' |
| | 'client_capability_decision_ack'; |
| } |
| > { |
| return ( |
| event.type === 'queue_update' || |
| event.type === 'message_admission' || |
| event.type === 'context_compaction_started' || |
| event.type === 'client_capability_request' || |
| event.type === 'client_capability_decision_ack' |
| ); |
| } |
| |
| function isLegacyPermissionSessionEvent(event: SessionEvent): event is Extract< |
| SessionEvent, |
| { |
| type: |
| | 'permission_request' |
| | 'permission_answer_ack' |
| | 'permission_closure_ack' |
| | 'permission_decision_ack'; |
| } |
| > { |
| return ( |
| event.type === 'permission_request' || |
| event.type === 'permission_answer_ack' || |
| event.type === 'permission_closure_ack' || |
| event.type === 'permission_decision_ack' |
| ); |
| } |
| |
| function mapBackendSessionEvent( |
| event: BackendSessionEvent, |
| ctx: RuntimeEventMapContext, |
| memory: SessionEventMapMemory, |
| ): RuntimeEvent { |
| const base = resolveBase(event, ctx); |
| |
| switch (event.type) { |
| // ── Model text ──────────────────────────────────────────────────────── |
| case 'text_delta': |
| return { |
| ...base, |
| partial: true, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'text', text: event.text }, |
| refs: { providerEventId: event.messageId }, |
| }; |
| case 'text_complete': |
| return { |
| ...base, |
| ...(event.interrupted ? { modelVisibility: 'hidden' as const } : {}), |
| role: 'model', |
| author: 'agent', |
| content: { |
| kind: 'text', |
| text: event.text, |
| ...(event.interrupted ? { interrupted: true } : {}), |
| ...(event.providerOptions !== undefined |
| ? { providerOptions: structuredClone(event.providerOptions) } |
| : {}), |
| }, |
| refs: { providerEventId: event.messageId }, |
| }; |
| |
| // ── Model thinking ──────────────────────────────────────────────────── |
| case 'thinking_delta': |
| return { |
| ...base, |
| partial: true, |
| role: 'model', |
| author: 'agent', |
| content: { kind: 'thinking', text: event.text }, |
| refs: { providerEventId: event.messageId }, |
| }; |
| case 'thinking_complete': |
| return { |
| ...base, |
| ...(event.interrupted ? { modelVisibility: 'hidden' as const } : {}), |
| role: 'model', |
| author: 'agent', |
| content: { |
| kind: 'thinking', |
| text: event.text, |
| ...(event.signature !== undefined ? { signature: event.signature } : {}), |
| ...(event.providerOptions !== undefined |
| ? { providerOptions: structuredClone(event.providerOptions) } |
| : {}), |
| }, |
| refs: { providerEventId: event.messageId }, |
| }; |
| |
| // ── Tool calls / results ────────────────────────────────────────────── |
| case 'tool_start': { |
| memory.toolNameByUseId.set(event.toolUseId, event.toolName); |
| const ev: RuntimeEvent = { |
| ...base, |
| role: 'model', |
| author: 'agent', |
| ...(event.origin !== undefined ? { origin: event.origin } : {}), |
| ...(event.modelVisibility !== undefined ? { modelVisibility: event.modelVisibility } : {}), |
| content: { |
| kind: 'function_call', |
| id: event.toolUseId, |
| name: event.toolName, |
| args: structuredClone(event.args), |
| ...(event.providerOptions !== undefined |
| ? { providerOptions: structuredClone(event.providerOptions) } |
| : {}), |
| ...(event.providerExecuted !== undefined |
| ? { providerExecuted: event.providerExecuted } |
| : {}), |
| }, |
| refs: { |
| toolCallId: event.toolUseId, |
| ...(event.operationId !== undefined ? { operationId: event.operationId } : {}), |
| ...(event.parentToolCallId !== undefined |
| ? { parentToolCallId: event.parentToolCallId } |
| : {}), |
| ...(event.parentOperationId !== undefined |
| ? { parentOperationId: event.parentOperationId } |
| : {}), |
| ...(event.stepId !== undefined ? { stepId: event.stepId } : {}), |
| }, |
| }; |
| if ( |
| event.activityKind !== undefined || |
| event.displayName !== undefined || |
| event.intent !== undefined |
| ) { |
| const stateDelta: Record<string, unknown> = {}; |
| if (event.activityKind !== undefined) stateDelta.activityKind = event.activityKind; |
| if (event.displayName !== undefined) stateDelta.displayName = event.displayName; |
| if (event.intent !== undefined) stateDelta.intent = event.intent; |
| ev.actions = { stateDelta }; |
| } |
| return ev; |
| } |
| case 'tool_output_delta': |
| // Transient tool stdout/stderr side-channel. Carried as a partial |
| // tool-role heartbeat; the canonical tool result is the function_response |
| // below. Phase 5 may promote this to a dedicated tool-progress action. |
| return { |
| ...base, |
| partial: true, |
| role: 'tool', |
| author: 'tool', |
| ...(event.origin !== undefined ? { origin: event.origin } : {}), |
| ...(event.modelVisibility !== undefined ? { modelVisibility: event.modelVisibility } : {}), |
| refs: { |
| toolCallId: event.toolUseId, |
| ...(event.parentToolCallId !== undefined |
| ? { parentToolCallId: event.parentToolCallId } |
| : {}), |
| ...(event.parentOperationId !== undefined |
| ? { parentOperationId: event.parentOperationId } |
| : {}), |
| }, |
| }; |
| case 'tool_progress': |
| return { |
| ...base, |
| partial: true, |
| role: 'tool', |
| author: 'tool', |
| ...(event.origin !== undefined ? { origin: event.origin } : {}), |
| ...(event.modelVisibility !== undefined ? { modelVisibility: event.modelVisibility } : {}), |
| refs: { |
| toolCallId: event.toolUseId, |
| ...(event.parentToolCallId !== undefined |
| ? { parentToolCallId: event.parentToolCallId } |
| : {}), |
| ...(event.parentOperationId !== undefined |
| ? { parentOperationId: event.parentOperationId } |
| : {}), |
| }, |
| }; |
| case 'tool_result_preview': |
| // Live-only mid-flight open-facts. Not function_response. |
| return { |
| ...base, |
| partial: true, |
| role: 'tool', |
| author: 'tool', |
| ...(event.origin !== undefined ? { origin: event.origin } : {}), |
| ...(event.modelVisibility !== undefined ? { modelVisibility: event.modelVisibility } : {}), |
| refs: { |
| toolCallId: event.toolUseId, |
| ...(event.parentToolCallId !== undefined |
| ? { parentToolCallId: event.parentToolCallId } |
| : {}), |
| ...(event.parentOperationId !== undefined |
| ? { parentOperationId: event.parentOperationId } |
| : {}), |
| }, |
| }; |
| case 'tool_result': { |
| const name = memory.toolNameByUseId.get(event.toolUseId) ?? ''; |
| const content = { |
| kind: 'function_response' as const, |
| id: event.toolUseId, |
| name, |
| result: event.content, |
| ...(event.isError ? { isError: true as const } : {}), |
| ...(event.providerExecuted !== undefined |
| ? { providerExecuted: event.providerExecuted } |
| : {}), |
| ...(event.providerExecuted && event.providerOutput !== undefined |
| ? { providerOutput: structuredClone(event.providerOutput) } |
| : {}), |
| }; |
| const modelProjection = |
| event.modelProjection ?? |
| (event.operationId === undefined |
| ? compatibilityToolResultProjection(content, ctx.sessionId) |
| : undefined); |
| const ev: RuntimeEvent = { |
| ...base, |
| role: 'tool', |
| author: 'tool', |
| ...(event.origin !== undefined ? { origin: event.origin } : {}), |
| ...(event.modelVisibility !== undefined ? { modelVisibility: event.modelVisibility } : {}), |
| content: { |
| ...content, |
| ...(modelProjection ? { modelProjection } : {}), |
| }, |
| refs: { |
| toolCallId: event.toolUseId, |
| ...(event.operationId !== undefined ? { operationId: event.operationId } : {}), |
| ...(event.parentToolCallId !== undefined |
| ? { parentToolCallId: event.parentToolCallId } |
| : {}), |
| ...(event.parentOperationId !== undefined |
| ? { parentOperationId: event.parentOperationId } |
| : {}), |
| }, |
| }; |
| if (event.durationMs !== undefined) { |
| ev.actions = { stateDelta: { durationMs: event.durationMs } }; |
| } |
| return ev; |
| } |
| |
| // ── Session sandbox boundary ────────────────────────────────────────── |
| case 'sandbox_boundary_request': |
| return { |
| ...base, |
| role: 'system', |
| author: 'system', |
| actions: { |
| stateDelta: { |
| sandboxBoundaryRequest: { |
| requestId: event.requestId, |
| toolUseId: event.toolUseId, |
| justification: event.justification, |
| expansion: event.expansion, |
| }, |
| }, |
| }, |
| refs: { toolCallId: event.toolUseId }, |
| }; |
| case 'sandbox_boundary_decision_ack': |
| return { |
| ...base, |
| role: 'system', |
| author: 'user', |
| actions: { |
| stateDelta: { |
| sandboxBoundaryDecision: { |
| requestId: event.requestId, |
| decision: event.decision, |
| status: event.status, |
| revision: event.revision, |
| }, |
| }, |
| }, |
| refs: { toolCallId: event.toolUseId }, |
| }; |
| case 'user_question_request': |
| return { |
| ...base, |
| role: 'system', |
| author: 'system', |
| actions: { |
| userQuestionRequest: { |
| requestId: event.requestId, |
| toolUseId: event.toolUseId, |
| questions: event.questions, |
| }, |
| }, |
| refs: { toolCallId: event.toolUseId }, |
| }; |
| case 'user_question_answer_ack': |
| return { |
| ...base, |
| role: 'system', |
| author: 'user', |
| actions: { |
| userQuestionAnswerAccepted: { |
| requestId: event.requestId, |
| }, |
| }, |
| refs: { toolCallId: event.toolUseId }, |
| }; |
| case 'form_request': |
| return { |
| ...base, |
| role: 'system', |
| author: 'system', |
| actions: { |
| formRequest: { |
| requestId: event.requestId, |
| toolUseId: event.toolUseId, |
| message: event.message, |
| requester: event.requester, |
| fields: event.fields, |
| }, |
| }, |
| refs: { toolCallId: event.toolUseId }, |
| }; |
| case 'form_answer_ack': |
| return { |
| ...base, |
| role: 'system', |
| author: 'user', |
| actions: { formAnswerAccepted: { requestId: event.requestId } }, |
| refs: { toolCallId: event.toolUseId }, |
| }; |
| |
| // ── Steering: a user message injected mid-turn at a step boundary ───── |
| // Persisted as a first-class user event so the ledger, transcript, and |
| // future-turn context all carry the interjection in place. |
| case 'steering_message': |
| return { |
| ...base, |
| role: 'user', |
| author: 'user', |
| // Canonical content + steering marker: read models may prefer |
| // displayText, while model replay uses text and materializes attachments. |
| content: { kind: 'text', ...normalizeMessageContent(event.content), steering: true }, |
| refs: { |
| providerEventId: event.messageId, |
| ...(event.submittedContentDigest |
| ? { sourceMessageDigest: event.submittedContentDigest } |
| : {}), |
| }, |
| }; |
| |
| // (queue_update is deliberately NOT mappable: the kernel is its only |
| // legal producer and pushes it directly into the turn stream. The flow |
| // drops a backend-yielded one at the ingress — see run() — so it is |
| // excluded from this function's input vocabulary.) |
| |
| // ── Transient provider retry progress ──────────────────────────────── |
| case 'provider_retry': |
| return { |
| ...base, |
| partial: true, |
| role: 'system', |
| author: 'system', |
| actions: { |
| stateDelta: { |
| providerRetry: { |
| phase: event.phase, |
| attempt: event.attempt, |
| maxAttempts: event.maxAttempts, |
| ...(event.phase === 'scheduled' ? { delayMs: event.delayMs } : {}), |
| reason: event.reason, |
| }, |
| }, |
| }, |
| }; |
| |
| // ── Plan handoff (placeholder; Phase 5/7 refines) ───────────────────── |
| case 'plan_submitted': |
| return { |
| ...base, |
| role: 'system', |
| author: 'agent', |
| actions: { |
| stateDelta: { |
| planId: event.planId, |
| ...(event.proposalId ? { proposalId: event.proposalId } : {}), |
| ...(event.revision !== undefined ? { revision: event.revision } : {}), |
| title: event.title, |
| ...(event.overview ? { overview: event.overview } : {}), |
| ...(event.risks ? { risks: event.risks } : {}), |
| ...(event.markdownPath ? { markdownPath: event.markdownPath } : {}), |
| ...(event.steps ? { steps: event.steps } : {}), |
| }, |
| }, |
| }; |
| |
| // ── Token usage ─────────────────────────────────────────────────────── |
| case 'token_usage': |
| return { |
| ...base, |
| role: 'system', |
| author: 'system', |
| actions: { |
| tokenUsage: { |
| input: event.input, |
| output: event.output, |
| ...(event.cacheHitInput !== undefined ? { cacheHitInput: event.cacheHitInput } : {}), |
| ...(event.cacheMissInput !== undefined ? { cacheMissInput: event.cacheMissInput } : {}), |
| ...(event.cacheMissInputSource !== undefined |
| ? { cacheMissInputSource: event.cacheMissInputSource } |
| : {}), |
| ...(event.cacheWriteInput !== undefined |
| ? { cacheWriteInput: event.cacheWriteInput } |
| : {}), |
| ...(event.reasoning !== undefined ? { reasoning: event.reasoning } : {}), |
| ...(event.total !== undefined ? { total: event.total } : {}), |
| ...(event.rawFinishReason !== undefined |
| ? { rawFinishReason: event.rawFinishReason } |
| : {}), |
| ...(event.runtimeSteps !== undefined ? { runtimeSteps: event.runtimeSteps } : {}), |
| ...(event.cacheRead !== undefined ? { cacheRead: event.cacheRead } : {}), |
| ...(event.cacheCreation !== undefined ? { cacheCreation: event.cacheCreation } : {}), |
| ...(event.costUsd !== undefined ? { costUsd: event.costUsd } : {}), |
| ...(event.contextRemaining !== undefined |
| ? { contextRemaining: event.contextRemaining } |
| : {}), |
| ...(event.systemPromptHash !== undefined |
| ? { systemPromptHash: event.systemPromptHash } |
| : {}), |
| ...(event.prefixHash !== undefined ? { prefixHash: event.prefixHash } : {}), |
| ...(event.prefixChangeReason !== undefined |
| ? { prefixChangeReason: event.prefixChangeReason } |
| : {}), |
| ...(event.requestShapeHash !== undefined |
| ? { requestShapeHash: event.requestShapeHash } |
| : {}), |
| ...(event.requestShapeChangeReason !== undefined |
| ? { requestShapeChangeReason: event.requestShapeChangeReason } |
| : {}), |
| ...(event.promptSegments !== undefined ? { promptSegments: event.promptSegments } : {}), |
| ...(event.contextBudget !== undefined ? { contextBudget: event.contextBudget } : {}), |
| ...(event.lastRequestAnchor !== undefined |
| ? { lastRequestAnchor: event.lastRequestAnchor } |
| : {}), |
| }, |
| }, |
| ...(event.providerRequestTraceId !== undefined |
| ? { refs: { providerRequestTraceId: event.providerRequestTraceId } } |
| : {}), |
| }; |
| |
| // ── Error ───────────────────────────────────────────────────────────── |
| case 'error': { |
| // No status here: the backend follows with a terminal `complete(error)`. |
| // Keeping status off the error event avoids a double-terminal in the |
| // error path; the trailing complete carries the terminal signal. |
| memory.failureClass = event.reason ?? event.code ?? 'unknown'; |
| const content = { |
| kind: 'error' as const, |
| ...(event.code !== undefined ? { code: event.code } : {}), |
| ...(event.reason !== undefined ? { reason: event.reason } : {}), |
| message: event.message, |
| ...(event.details !== undefined ? { details: event.details } : {}), |
| ...(event.retry !== undefined ? { retry: event.retry } : {}), |
| }; |
| memory.failureContent = content; |
| return { |
| ...base, |
| role: 'system', |
| author: 'system', |
| content, |
| }; |
| } |
| |
| // ── Terminal: abort + complete ──────────────────────────────────────── |
| case 'abort': |
| return { |
| ...base, |
| role: 'system', |
| author: 'system', |
| status: 'aborted', |
| actions: { endInvocation: true, stateDelta: { abortSource: event.reason } }, |
| }; |
| case 'complete': |
| return completeRuntimeEvent(base, event, memory); |
| default: { |
| // Exhaustiveness guard: if SessionEvent grows a new variant, the |
| // mapping falls through to a diagnostic event instead of dropping it. |
| // |
| // The fallback carries no `content` on purpose. That is what keeps |
| // "don't drop the event" from escalating into "lose the session": the |
| // read-model projection does not claim this shape, and an unclaimed |
| // event with no message payload degrades to a reported diagnostic |
| // instead of discarding the whole session view. Naming the type in the |
| // state delta is the only user-visible trace an unknown event has left. |
| const _exhaustive: never = event; |
| void _exhaustive; |
| return { |
| ...base, |
| role: 'system', |
| author: 'system', |
| actions: { |
| stateDelta: { unmappedSessionEventType: (event as { type?: string }).type ?? 'unknown' }, |
| }, |
| }; |
| } |
| } |
| } |
| |
| function completeRuntimeEvent( |
| base: ReturnType<typeof resolveBase>, |
| event: CompleteEvent, |
| memory: SessionEventMapMemory, |
| ): RuntimeEvent { |
| const stopReason = event.stopReason; |
| const status = |
| memory.failureClass && stopReason !== 'user_stop' |
| ? 'failed' |
| : mapCompleteStopReason(stopReason); |
| const stateDelta: Record<string, unknown> = { stopReason }; |
| if (status === 'failed') { |
| stateDelta.failureClass = |
| memory.failureClass ?? failureClassFromCompleteStopReason(stopReason) ?? 'runtime_error'; |
| } |
| if (event.contextCompactionOutcome !== undefined) { |
| stateDelta.contextCompactionOutcome = event.contextCompactionOutcome; |
| } |
| if (status === 'aborted') stateDelta.abortSource = stopReason; |
| return { |
| ...base, |
| role: 'system', |
| author: 'system', |
| status, |
| ...(status === 'failed' && memory.failureContent ? { content: memory.failureContent } : {}), |
| actions: { endInvocation: true, stateDelta }, |
| }; |
| } |