You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/08/31 09:51:51 UTC
(maka) branch main updated: refactor(runtime): persist durable Tool Result projections (#4287)
This is an automated email from the ASF dual-hosted git repository.
Astro-Han pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/main by this push:
new bd951aa881 refactor(runtime): persist durable Tool Result projections (#4287)
bd951aa881 is described below
commit bd951aa8819c5c03e39d2878f6866ef5aa53a075
Author: AstroHan <le...@outlook.com>
AuthorDate: Mon Aug 31 17:51:46 2026 +0800
refactor(runtime): persist durable Tool Result projections (#4287)
* feat(core): define durable tool result projections
Generated-by: Codex
* feat(runtime): add durable projection codec
Generated-by: Codex
* feat(runtime): commit projections atomically at T2
Generated-by: Codex
* refactor(runtime): replay effective tool result projections
Generated-by: Codex
* feat(runtime-host): persist projected artifacts and recoveries
Generated-by: Codex
* refactor(runtime): remove transient Tool Result authority
Generated-by: Codex
* fix(storage): reuse projected image artifacts
Generated-by: Codex
* fix(runtime): remap projected artifacts on copy
Generated-by: Codex
* refactor(runtime): remove synthetic projection return
Generated-by: Codex
* refactor(runtime): privatize synthetic result writer
Generated-by: Codex
* fix(runtime): validate projected images before persistence
Generated-by: Codex
* fix(runtime): reject unowned projection copies
Generated-by: Codex
* refactor(runtime): remove retired conversation copy owner
Generated-by: Codex
* test(runtime): verify durable projection owner boundaries
Generated-by: Codex
* refactor(runtime): keep projection helper types private
Generated-by: Codex
* test(runtime): target durable projection T2 rollback
Generated-by: Codex
* refactor(runtime): trim projection artifact input
Generated-by: Codex
* test(runtime): produce restart projection through T2
Generated-by: Codex
* style(runtime): format session manager tests
Generated-by: Codex
* fix(runtime): make projection admission side-effect free
Generated-by: Codex
* test(runtime): replay persisted image after restart
Generated-by: Codex
* test(runtime-host): cover projection artifact restart
Generated-by: Codex
* refactor(runtime): preserve projection content fidelity
Generated-by: Codex
* fix(storage): protect projection artifacts
Generated-by: Codex
* fix(runtime): admit tool calls before publication
Generated-by: Codex
* fix(runtime): bound projection copies before allocation
Generated-by: Codex
* test(runtime-host): consolidate projection lifecycle coverage
Generated-by: Codex
* test(runtime): lock T1 failure side effects
Generated-by: Codex
* refactor(core): unify artifact source policy
Generated-by: Codex
* fix(runtime): make tool result projectors total
Generated-by: Codex
---
.../workbar/tools/artifacts/artifact-visibility.ts | 22 +-
packages/core/package.json | 1 +
packages/core/src/__tests__/artifacts.test.ts | 20 +
.../durable-tool-result-projection.test.ts | 160 ++++
packages/core/src/__tests__/runtime-event.test.ts | 34 +
packages/core/src/artifacts.ts | 48 +-
.../core/src/durable-tool-result-projection.ts | 189 ++++
packages/core/src/events.ts | 3 +
packages/core/src/runtime-event.ts | 41 +-
packages/core/src/session-name.ts | 1 -
.../__tests__/client-capability-recovery.test.ts | 7 +
.../__tests__/execution-model-composition.test.ts | 197 +++-
.../session-revision-two-client-uds.test.ts | 92 +-
.../src/server/artifact-coordinator.ts | 8 +-
.../src/server/client-capability-recovery.ts | 17 +-
.../src/server/execution-model-composition.ts | 10 +
packages/runtime/package.json | 1 +
.../runtime/src/__tests__/ai-sdk-backend.test.ts | 107 ++-
.../src/__tests__/computer-use-model-loop.test.ts | 1 +
.../computer-use-provider-protocol.test.ts | 6 +
.../durable-tool-result-projection.test.ts | 315 +++++++
.../__tests__/execution-boundary-test-helpers.ts | 36 +-
.../__tests__/overflow-reactive-recovery.test.ts | 2 +-
.../__tests__/pre-dispatch-refusal-ledger.test.ts | 4 +-
.../src/__tests__/recovery-resolver.test.ts | 82 ++
.../runtime/src/__tests__/session-manager.test.ts | 995 +--------------------
.../tool-runtime-durable-boundary.test.ts | 207 ++++-
.../tool-runtime-sandbox-boundary.test.ts | 16 +-
.../src/__tests__/tool-runtime-settlement.test.ts | 114 +--
.../__tests__/tool-runtime-sqlite-boundary.test.ts | 147 ++-
packages/runtime/src/ai-sdk-backend.ts | 143 ++-
packages/runtime/src/conversation-copy.ts | 20 +
.../runtime/src/durable-tool-result-projection.ts | 478 ++++++++++
packages/runtime/src/model-history.ts | 61 +-
packages/runtime/src/recovery-resolver.ts | 32 +-
.../runtime/src/session-event-runtime-mapper.ts | 32 +-
packages/runtime/src/session-manager.ts | 206 -----
packages/runtime/src/tool-runtime.ts | 271 ++++--
.../src/__tests__/artifact-attachments.test.ts | 86 +-
.../src/__tests__/sqlite-runtime-store.test.ts | 70 +-
packages/storage/src/artifact-attachments.ts | 100 ++-
packages/storage/src/artifact-stores.ts | 2 +
packages/storage/src/sqlite-runtime-store.ts | 7 +
43 files changed, 2811 insertions(+), 1580 deletions(-)
diff --git a/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-visibility.ts b/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-visibility.ts
index cc35bb97c5..cd7bb4c89f 100644
--- a/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-visibility.ts
+++ b/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-visibility.ts
@@ -17,28 +17,10 @@
* under the License.
*/
-import type { ArtifactDescriptor, ArtifactSource } from '@maka/core/artifacts';
-
-const USER_VISIBLE_ARTIFACT_SOURCES = {
- tool_result: false,
- tool_result_archive: false,
- synthesis_cache_block: false,
- history_compact_block: false,
- history_compact_source: false,
- provider_request_capture: false,
- session_effect: false,
- subagent_writeback: true,
- deep_research: true,
- user_upload: false,
- export: true,
- snapshot: true,
- fixture: true,
-} satisfies Record<ArtifactSource, boolean>;
+import { isArtifactUserVisible, type ArtifactDescriptor } from '@maka/core/artifacts';
export function filterUserVisibleArtifacts(
records: readonly ArtifactDescriptor[],
): ArtifactDescriptor[] {
- return records.filter(
- (record) => record.source === undefined || USER_VISIBLE_ARTIFACT_SOURCES[record.source],
- );
+ return records.filter(isArtifactUserVisible);
}
diff --git a/packages/core/package.json b/packages/core/package.json
index 7c0dfeec11..b9752a2fbc 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -7,6 +7,7 @@
"sideEffects": false,
"private": true,
"exports": {
+ "./durable-tool-result-projection": "./dist/durable-tool-result-projection.js",
"./canonical-runtime-event": "./dist/canonical-runtime-event.js",
"./runtime-boundary": "./dist/runtime-boundary.js",
"./runtime-event": "./dist/runtime-event.js",
diff --git a/packages/core/src/__tests__/artifacts.test.ts b/packages/core/src/__tests__/artifacts.test.ts
index 0fc3499a5f..53f5d4b2fb 100644
--- a/packages/core/src/__tests__/artifacts.test.ts
+++ b/packages/core/src/__tests__/artifacts.test.ts
@@ -23,6 +23,8 @@ import {
ARTIFACT_ENTITY_ID_MAX_CHARS,
ARTIFACT_TURN_KEY_MAX_CHARS,
canUserDeleteArtifact,
+ isArtifactSharedSessionReadable,
+ isArtifactUserVisible,
isArtifactTurnKey,
isCanonicalArtifactEntityId,
} from '../artifacts.js';
@@ -61,3 +63,21 @@ describe('Artifact user-delete policy', () => {
assert.equal(canUserDeleteArtifact({ source: undefined }), true);
});
});
+
+describe('Artifact source policy', () => {
+ test('keeps projection artifacts internal, durable, and readable in shared sessions', () => {
+ const projection = { source: 'tool_result_projection' as const };
+
+ assert.equal(canUserDeleteArtifact(projection), false);
+ assert.equal(isArtifactUserVisible(projection), false);
+ assert.equal(isArtifactSharedSessionReadable(projection), true);
+ });
+
+ test('preserves unattributed artifact defaults', () => {
+ const unattributed = { source: undefined };
+
+ assert.equal(canUserDeleteArtifact(unattributed), true);
+ assert.equal(isArtifactUserVisible(unattributed), true);
+ assert.equal(isArtifactSharedSessionReadable(unattributed), false);
+ });
+});
diff --git a/packages/core/src/__tests__/durable-tool-result-projection.test.ts b/packages/core/src/__tests__/durable-tool-result-projection.test.ts
new file mode 100644
index 0000000000..9726ccbaa9
--- /dev/null
+++ b/packages/core/src/__tests__/durable-tool-result-projection.test.ts
@@ -0,0 +1,160 @@
+/*
+ * 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.
+ */
+
+import assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+
+import {
+ decodeDurableToolResultProjection,
+ DURABLE_TOOL_RESULT_PROJECTION_MAX_BYTES,
+ DURABLE_TOOL_RESULT_PROJECTION_MAX_JSON_DEPTH,
+ DURABLE_TOOL_RESULT_PROJECTION_MAX_JSON_NODES,
+} from '../durable-tool-result-projection.js';
+
+describe('durable Tool Result projection', () => {
+ it('accepts only the closed current-version schema', () => {
+ const projection = { version: 1, kind: 'text', text: 'safe' } as const;
+
+ assert.deepEqual(decodeDurableToolResultProjection(projection), projection);
+ assert.throws(
+ () => decodeDurableToolResultProjection({ ...projection, version: 2 }),
+ /Invalid durable Tool Result projection/,
+ );
+ assert.throws(
+ () => decodeDurableToolResultProjection({ ...projection, providerOptions: {} }),
+ /Invalid durable Tool Result projection/,
+ );
+ assert.throws(
+ () =>
+ decodeDurableToolResultProjection({
+ version: 1,
+ kind: 'content',
+ parts: [],
+ }),
+ /Invalid durable Tool Result projection/,
+ );
+ });
+
+ it('rejects projections whose serialized JSON exceeds the durable byte bound', () => {
+ assert.throws(
+ () =>
+ decodeDurableToolResultProjection({
+ version: 1,
+ kind: 'text',
+ text: 'x'.repeat(DURABLE_TOOL_RESULT_PROJECTION_MAX_BYTES),
+ }),
+ /Invalid durable Tool Result projection/,
+ );
+ });
+
+ it('accepts only content-addressed references owned by the same Session protocol', () => {
+ assert.deepEqual(
+ decodeDurableToolResultProjection({
+ version: 1,
+ kind: 'content',
+ parts: [
+ {
+ kind: 'artifact',
+ mediaType: 'image/png',
+ ref: { kind: 'session_context', sessionId: 'session-1', refId: 'sha256-image' },
+ },
+ ],
+ }).kind,
+ 'content',
+ );
+ assert.throws(
+ () =>
+ decodeDurableToolResultProjection({
+ version: 1,
+ kind: 'content',
+ parts: [
+ {
+ kind: 'artifact',
+ mediaType: 'image/png',
+ ref: { kind: 'external_file', absolutePath: '/private/image.png' },
+ },
+ ],
+ }),
+ /Invalid durable Tool Result projection/,
+ );
+ assert.throws(
+ () =>
+ decodeDurableToolResultProjection({
+ version: 1,
+ kind: 'content',
+ parts: [
+ {
+ kind: 'artifact',
+ mediaType: 'image/png',
+ ref: {
+ kind: 'session_file',
+ sessionId: 'session-1',
+ relativePath: 'arbitrary/path.png',
+ },
+ },
+ ],
+ }),
+ /Invalid durable Tool Result projection/,
+ );
+ });
+
+ it('admits only canonical safe image media types', () => {
+ const projection = (mediaType: string) => ({
+ version: 1,
+ kind: 'content',
+ parts: [
+ {
+ kind: 'artifact',
+ mediaType,
+ ref: { kind: 'session_file', sessionId: 'session-1', relativePath: 'artifact-1' },
+ },
+ ],
+ });
+
+ assert.equal(decodeDurableToolResultProjection(projection('image/png')).kind, 'content');
+ assert.throws(
+ () => decodeDurableToolResultProjection(projection('image/png; token=sk-secret')),
+ /Invalid durable Tool Result projection/,
+ );
+ assert.throws(
+ () => decodeDurableToolResultProjection(projection('image/svg+xml')),
+ /Invalid durable Tool Result projection/,
+ );
+ });
+
+ it('applies the same bounded JSON budget to persisted projections', () => {
+ let tooDeep: unknown = 'leaf';
+ for (let depth = 0; depth <= DURABLE_TOOL_RESULT_PROJECTION_MAX_JSON_DEPTH; depth += 1) {
+ tooDeep = [tooDeep];
+ }
+ assert.throws(
+ () => decodeDurableToolResultProjection({ version: 1, kind: 'json', value: tooDeep }),
+ /Invalid durable Tool Result projection/,
+ );
+ assert.throws(
+ () =>
+ decodeDurableToolResultProjection({
+ version: 1,
+ kind: 'json',
+ value: Array.from({ length: DURABLE_TOOL_RESULT_PROJECTION_MAX_JSON_NODES }, () => null),
+ }),
+ /Invalid durable Tool Result projection/,
+ );
+ });
+});
diff --git a/packages/core/src/__tests__/runtime-event.test.ts b/packages/core/src/__tests__/runtime-event.test.ts
index b0e13b3ba7..31bd12c9b1 100644
--- a/packages/core/src/__tests__/runtime-event.test.ts
+++ b/packages/core/src/__tests__/runtime-event.test.ts
@@ -498,6 +498,40 @@ describe('RuntimeEvent content variants', () => {
});
});
+test('rejects a Tool Result projection that references another Session artifact', () => {
+ assert.throws(
+ () =>
+ decodeRuntimeEvent(
+ baseEvent({
+ role: 'tool',
+ author: 'tool',
+ content: {
+ kind: 'function_response',
+ id: 'call-1',
+ name: 'Read',
+ result: { kind: 'image' },
+ modelProjection: {
+ version: 1,
+ kind: 'content',
+ parts: [
+ {
+ kind: 'artifact',
+ mediaType: 'image/png',
+ ref: {
+ kind: 'session_context',
+ sessionId: 'another-session',
+ refId: 'image-1',
+ },
+ },
+ ],
+ },
+ },
+ }),
+ ),
+ /Invalid RuntimeEvent schema/,
+ );
+});
+
describe('RuntimeEvent actions', () => {
test('binds the managed mutation digest to its canonical execution semantics', () => {
const canonicalProfile = JSON.stringify({
diff --git a/packages/core/src/artifacts.ts b/packages/core/src/artifacts.ts
index 02acd38dad..0e203c9f92 100644
--- a/packages/core/src/artifacts.ts
+++ b/packages/core/src/artifacts.ts
@@ -88,6 +88,7 @@ export function resolveArtifactImagePreview(
export const ARTIFACT_SOURCES = [
'tool_result',
+ 'tool_result_projection',
'tool_result_archive',
'synthesis_cache_block',
'history_compact_block',
@@ -152,24 +153,39 @@ export interface ArtifactRecord extends ArtifactDescriptor {
deepResearchRole?: import('./deep-research-run.js').DeepResearchArtifactRole;
}
-const ARTIFACT_USER_DELETE_ALLOWED_BY_SOURCE = {
- tool_result: true,
- tool_result_archive: false,
- synthesis_cache_block: true,
- history_compact_block: true,
- history_compact_source: true,
- provider_request_capture: true,
- subagent_writeback: false,
- deep_research: false,
- user_upload: true,
- export: true,
- snapshot: true,
- session_effect: false,
- fixture: true,
-} as const satisfies Record<ArtifactSource, boolean>;
+interface ArtifactSourcePolicy {
+ readonly userDeletable: boolean;
+ readonly userVisible: boolean;
+ readonly sharedReadable: boolean;
+}
+
+const ARTIFACT_SOURCE_POLICIES = {
+ tool_result: { userDeletable: true, userVisible: false, sharedReadable: true },
+ tool_result_projection: { userDeletable: false, userVisible: false, sharedReadable: true },
+ tool_result_archive: { userDeletable: false, userVisible: false, sharedReadable: false },
+ synthesis_cache_block: { userDeletable: true, userVisible: false, sharedReadable: false },
+ history_compact_block: { userDeletable: true, userVisible: false, sharedReadable: false },
+ history_compact_source: { userDeletable: true, userVisible: false, sharedReadable: false },
+ provider_request_capture: { userDeletable: true, userVisible: false, sharedReadable: false },
+ subagent_writeback: { userDeletable: false, userVisible: true, sharedReadable: false },
+ deep_research: { userDeletable: false, userVisible: true, sharedReadable: false },
+ user_upload: { userDeletable: true, userVisible: false, sharedReadable: true },
+ export: { userDeletable: true, userVisible: true, sharedReadable: false },
+ snapshot: { userDeletable: true, userVisible: true, sharedReadable: false },
+ session_effect: { userDeletable: false, userVisible: false, sharedReadable: false },
+ fixture: { userDeletable: true, userVisible: true, sharedReadable: false },
+} as const satisfies Record<ArtifactSource, ArtifactSourcePolicy>;
export function canUserDeleteArtifact(record: Pick<ArtifactRecord, 'source'>): boolean {
- return record.source === undefined || ARTIFACT_USER_DELETE_ALLOWED_BY_SOURCE[record.source];
+ return record.source === undefined || ARTIFACT_SOURCE_POLICIES[record.source].userDeletable;
+}
+
+export function isArtifactUserVisible(record: Pick<ArtifactRecord, 'source'>): boolean {
+ return record.source === undefined || ARTIFACT_SOURCE_POLICIES[record.source].userVisible;
+}
+
+export function isArtifactSharedSessionReadable(record: Pick<ArtifactRecord, 'source'>): boolean {
+ return record.source !== undefined && ARTIFACT_SOURCE_POLICIES[record.source].sharedReadable;
}
export type ArtifactChangedReason = 'created' | 'deleted' | 'purged';
diff --git a/packages/core/src/durable-tool-result-projection.ts b/packages/core/src/durable-tool-result-projection.ts
new file mode 100644
index 0000000000..3a5a397597
--- /dev/null
+++ b/packages/core/src/durable-tool-result-projection.ts
@@ -0,0 +1,189 @@
+/*
+ * 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.
+ */
+
+import { isCanonicalStorageRef, type StorageRef } from './events.js';
+import { isCanonicalArtifactEntityId, normalizeArtifactImagePreviewMime } from './artifacts.js';
+import { hasExactShape, isRecord } from './record-schema.js';
+import { serializedByteLength } from './serialized-byte-length.js';
+
+export const DURABLE_TOOL_RESULT_PROJECTION_VERSION = 1 as const;
+export const DURABLE_TOOL_RESULT_PROJECTION_MAX_BYTES = 256 * 1024;
+export const DURABLE_TOOL_RESULT_PROJECTION_MAX_PARTS = 64;
+export const DURABLE_TOOL_RESULT_PROJECTION_MAX_JSON_DEPTH = 32;
+export const DURABLE_TOOL_RESULT_PROJECTION_MAX_JSON_NODES = 20_000;
+export const DURABLE_TOOL_RESULT_PROJECTION_FAILURE_MESSAGE =
+ 'The tool completed, but its model-visible result could not be projected safely.';
+
+export type DurableProjectionJson =
+ | null
+ | boolean
+ | number
+ | string
+ | DurableProjectionJson[]
+ | { [key: string]: DurableProjectionJson };
+
+export type DurableProjectionArtifactRef = Extract<
+ StorageRef,
+ { kind: 'session_context' | 'session_file' }
+>;
+
+export type DurableToolResultProjectionPart =
+ | { kind: 'text'; text: string }
+ | {
+ kind: 'artifact';
+ mediaType: string;
+ ref: DurableProjectionArtifactRef;
+ };
+
+export type DurableToolResultProjection =
+ | { version: 1; kind: 'text'; text: string; isError?: true }
+ | { version: 1; kind: 'json'; value: DurableProjectionJson; isError?: true }
+ | { version: 1; kind: 'content'; parts: DurableToolResultProjectionPart[] }
+ | { version: 1; kind: 'execution_denied'; reason?: string }
+ | {
+ version: 1;
+ kind: 'failure';
+ reason: 'projection_failed';
+ message: typeof DURABLE_TOOL_RESULT_PROJECTION_FAILURE_MESSAGE;
+ };
+
+export const DURABLE_TOOL_RESULT_PROJECTION_FAILURE: DurableToolResultProjection = Object.freeze({
+ version: DURABLE_TOOL_RESULT_PROJECTION_VERSION,
+ kind: 'failure',
+ reason: 'projection_failed',
+ message: DURABLE_TOOL_RESULT_PROJECTION_FAILURE_MESSAGE,
+});
+
+export function decodeDurableToolResultProjection(value: unknown): DurableToolResultProjection {
+ if (
+ !isDurableToolResultProjection(value) ||
+ serializedByteLength(value, DURABLE_TOOL_RESULT_PROJECTION_MAX_BYTES) >
+ DURABLE_TOOL_RESULT_PROJECTION_MAX_BYTES
+ ) {
+ throw new Error('Invalid durable Tool Result projection');
+ }
+ return value;
+}
+
+function isDurableToolResultProjection(value: unknown): value is DurableToolResultProjection {
+ if (!isRecord(value) || value.version !== DURABLE_TOOL_RESULT_PROJECTION_VERSION) return false;
+ switch (value.kind) {
+ case 'text':
+ return (
+ hasExactShape(value, {
+ required: ['version', 'kind', 'text'],
+ allowed: new Set(['version', 'kind', 'text', 'isError']),
+ }) &&
+ typeof value.text === 'string' &&
+ (value.isError === undefined || value.isError === true)
+ );
+ case 'json':
+ return (
+ hasExactShape(value, {
+ required: ['version', 'kind', 'value'],
+ allowed: new Set(['version', 'kind', 'value', 'isError']),
+ }) &&
+ isDurableProjectionJson(value.value, { nodes: 0 }, 0) &&
+ (value.isError === undefined || value.isError === true)
+ );
+ case 'content':
+ return (
+ hasExactShape(value, {
+ required: ['version', 'kind', 'parts'],
+ allowed: new Set(['version', 'kind', 'parts']),
+ }) &&
+ Array.isArray(value.parts) &&
+ value.parts.length > 0 &&
+ value.parts.length <= DURABLE_TOOL_RESULT_PROJECTION_MAX_PARTS &&
+ value.parts.every(isProjectionPart)
+ );
+ case 'execution_denied':
+ return (
+ hasExactShape(value, {
+ required: ['version', 'kind'],
+ allowed: new Set(['version', 'kind', 'reason']),
+ }) &&
+ (value.reason === undefined || typeof value.reason === 'string')
+ );
+ case 'failure':
+ return (
+ hasExactShape(value, {
+ required: ['version', 'kind', 'reason', 'message'],
+ allowed: new Set(['version', 'kind', 'reason', 'message']),
+ }) &&
+ value.reason === 'projection_failed' &&
+ value.message === DURABLE_TOOL_RESULT_PROJECTION_FAILURE_MESSAGE
+ );
+ default:
+ return false;
+ }
+}
+
+function isProjectionPart(value: unknown): value is DurableToolResultProjectionPart {
+ if (!isRecord(value)) return false;
+ if (value.kind === 'text') {
+ return (
+ hasExactShape(value, {
+ required: ['kind', 'text'],
+ allowed: new Set(['kind', 'text']),
+ }) && typeof value.text === 'string'
+ );
+ }
+ return (
+ value.kind === 'artifact' &&
+ hasExactShape(value, {
+ required: ['kind', 'mediaType', 'ref'],
+ allowed: new Set(['kind', 'mediaType', 'ref']),
+ }) &&
+ typeof value.mediaType === 'string' &&
+ normalizeArtifactImagePreviewMime(value.mediaType) === value.mediaType &&
+ isCanonicalStorageRef(value.ref) &&
+ (value.ref.kind === 'session_context' ||
+ (value.ref.kind === 'session_file' && isCanonicalArtifactEntityId(value.ref.relativePath)))
+ );
+}
+
+function isDurableProjectionJson(
+ value: unknown,
+ state: { nodes: number },
+ depth: number,
+): value is DurableProjectionJson {
+ state.nodes += 1;
+ if (
+ state.nodes > DURABLE_TOOL_RESULT_PROJECTION_MAX_JSON_NODES ||
+ depth > DURABLE_TOOL_RESULT_PROJECTION_MAX_JSON_DEPTH
+ ) {
+ return false;
+ }
+ if (
+ value === null ||
+ typeof value === 'string' ||
+ typeof value === 'boolean' ||
+ (typeof value === 'number' && Number.isFinite(value))
+ ) {
+ return true;
+ }
+ if (Array.isArray(value)) {
+ return value.every((item) => isDurableProjectionJson(item, state, depth + 1));
+ }
+ return (
+ isRecord(value) &&
+ Object.values(value).every((item) => isDurableProjectionJson(item, state, depth + 1))
+ );
+}
diff --git a/packages/core/src/events.ts b/packages/core/src/events.ts
index 386b5c598b..c5ccd3b0ff 100644
--- a/packages/core/src/events.ts
+++ b/packages/core/src/events.ts
@@ -48,6 +48,7 @@ import type {
export { SHELL_RUN_SOURCE_TOOL_CALL_ID_MAX_BYTES } from './shell-run.js';
import { type TokenUsageFields } from './usage-record-schema.js';
import { defineObjectShape, hasExactShape, isRecord } from './record-schema.js';
+import type { DurableToolResultProjection } from './durable-tool-result-projection.js';
export const TOOL_OUTPUT_STREAMS = ['stdout', 'stderr'] as const;
export const TOOL_OUTPUT_DELTA_MAX_CHARS = 8192;
@@ -698,6 +699,8 @@ export interface ToolResultEvent extends BaseEvent, ToolActivityIdentity {
providerExecuted?: boolean;
/** Raw provider result retained for provider-native replay; never rendered directly. */
providerOutput?: unknown;
+ /** Provider-neutral model-visible output computed before durable publication. */
+ modelProjection?: DurableToolResultProjection;
/** The transport omitted durable result content; consumers must not treat the placeholder as authoritative. */
contentOmitted?: true;
isError: boolean;
diff --git a/packages/core/src/runtime-event.ts b/packages/core/src/runtime-event.ts
index 644f3c89fe..8dbe8e31e9 100644
--- a/packages/core/src/runtime-event.ts
+++ b/packages/core/src/runtime-event.ts
@@ -63,6 +63,10 @@ import {
isRuntimeEventWorkspaceFactEnvelope,
type RuntimeEventWorkspaceFactEnvelope,
} from './workspace-version-authority.js';
+import {
+ decodeDurableToolResultProjection,
+ type DurableToolResultProjection,
+} from './durable-tool-result-projection.js';
// ============================================================================
// Role / Author / Status
@@ -188,6 +192,8 @@ export interface RuntimeEventFunctionResponseContent {
providerExecuted?: boolean;
/** Raw provider result retained for provider-native replay; never rendered directly. */
providerOutput?: unknown;
+ /** Frozen provider-neutral content consumed by every model-history projection. */
+ modelProjection?: DurableToolResultProjection;
}
export interface RuntimeEventErrorContent {
@@ -249,6 +255,8 @@ export type ToolBoundaryProtocol = typeof TOOL_BOUNDARY_PROTOCOL_V1;
*/
export interface RuntimeEventToolDispatch {
protocol: ToolBoundaryProtocol;
+ /** New writes require this exact durable Tool Result projection protocol. */
+ resultProjectionVersion?: 1;
operationId: string;
providerToolCallId: string;
toolName: string;
@@ -548,7 +556,7 @@ const FUNCTION_CALL_CONTENT_SHAPE = defineObjectShape<RuntimeEventFunctionCallCo
);
const FUNCTION_RESPONSE_CONTENT_SHAPE = defineObjectShape<RuntimeEventFunctionResponseContent>()(
['kind', 'id', 'name', 'result'],
- ['isError', 'providerExecuted', 'providerOutput'],
+ ['isError', 'providerExecuted', 'providerOutput', 'modelProjection'],
);
const ERROR_CONTENT_SHAPE = defineObjectShape<RuntimeEventErrorContent>()(
['kind', 'message'],
@@ -601,7 +609,7 @@ const RUNTIME_TOOL_DISPATCH_SHAPE = defineObjectShape<RuntimeEventToolDispatch>(
'canonicalArgsHash',
'recoveryMode',
],
- ['managedMutation'],
+ ['managedMutation', 'resultProjectionVersion'],
);
const RUNTIME_MANAGED_WORKSPACE_MUTATION_SHAPE =
defineObjectShape<RuntimeEventManagedWorkspaceMutationV2>()(
@@ -712,6 +720,7 @@ export function decodeRuntimeEvent(value: unknown): RuntimeEvent {
!isOptionalMember(value.modelVisibility, RUNTIME_EVENT_MODEL_VISIBILITIES) ||
(value.status !== undefined && !isRuntimeEventStatus(value.status)) ||
(value.content !== undefined && !isRuntimeEventContent(value.content)) ||
+ !hasOwnedModelProjection(value.content, value.sessionId) ||
(value.actions !== undefined && !isRuntimeEventActions(value.actions)) ||
(value.refs !== undefined && !isRuntimeEventRefs(value.refs))
) {
@@ -733,6 +742,20 @@ export function decodeRuntimeEvent(value: unknown): RuntimeEvent {
return value as unknown as RuntimeEvent;
}
+function hasOwnedModelProjection(content: unknown, sessionId: string): boolean {
+ if (!isRecord(content) || content.kind !== 'function_response') return true;
+ const projection = content.modelProjection;
+ if (!isRecord(projection) || projection.kind !== 'content' || !Array.isArray(projection.parts)) {
+ return true;
+ }
+ return projection.parts.every(
+ (part) =>
+ !isRecord(part) ||
+ part.kind !== 'artifact' ||
+ (isRecord(part.ref) && part.ref.sessionId === sessionId),
+ );
+}
+
function isRuntimeEventContent(value: unknown): value is RuntimeEventContent {
if (!isRecord(value)) return false;
switch (value.kind) {
@@ -777,7 +800,9 @@ function isRuntimeEventContent(value: unknown): value is RuntimeEventContent {
typeof value.name === 'string' &&
Object.hasOwn(value, 'result') &&
(value.isError === undefined || typeof value.isError === 'boolean') &&
- (value.providerExecuted === undefined || typeof value.providerExecuted === 'boolean')
+ (value.providerExecuted === undefined || typeof value.providerExecuted === 'boolean') &&
+ (value.modelProjection === undefined ||
+ decodesDurableToolResultProjection(value.modelProjection))
);
case 'error':
return (
@@ -792,6 +817,15 @@ function isRuntimeEventContent(value: unknown): value is RuntimeEventContent {
}
}
+function decodesDurableToolResultProjection(value: unknown): boolean {
+ try {
+ decodeDurableToolResultProjection(value);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
function isTurnOrigin(value: unknown): value is TurnOrigin {
return decodeTurnOrigin(value) !== undefined;
}
@@ -896,6 +930,7 @@ function isRuntimeToolDispatch(value: unknown): value is RuntimeEventToolDispatc
isRecord(value) &&
hasExactShape(value, RUNTIME_TOOL_DISPATCH_SHAPE) &&
value.protocol === TOOL_BOUNDARY_PROTOCOL_V1 &&
+ (value.resultProjectionVersion === undefined || value.resultProjectionVersion === 1) &&
typeof value.operationId === 'string' &&
typeof value.providerToolCallId === 'string' &&
typeof value.toolName === 'string' &&
diff --git a/packages/core/src/session-name.ts b/packages/core/src/session-name.ts
index 7b670b6e66..993e426d16 100644
--- a/packages/core/src/session-name.ts
+++ b/packages/core/src/session-name.ts
@@ -31,7 +31,6 @@
* module so every write path can call it:
* - `sessions:create` IPC → runtime.create → store.create
* - `sessions:rename` IPC → runtime.renameSession → store.rename
- * - `sessions:branchFromTurn` IPC → runtime.branchFromTurn → store.create
*
* Pipeline (applied in order):
* 1. **Runtime type guard**: `typeof input !== 'string'` → typed
diff --git a/packages/runtime-host/src/__tests__/client-capability-recovery.test.ts b/packages/runtime-host/src/__tests__/client-capability-recovery.test.ts
index badfba53e2..19874cce2c 100644
--- a/packages/runtime-host/src/__tests__/client-capability-recovery.test.ts
+++ b/packages/runtime-host/src/__tests__/client-capability-recovery.test.ts
@@ -60,6 +60,12 @@ test('successor recovery durably settles dispatched Client Capabilities as outco
},
},
isError: true,
+ modelProjection: {
+ version: 1,
+ kind: 'text',
+ text: 'Error: outcome_unknown: the Host restarted after dispatching this Client Capability call. The client-side effect may have happened; do not retry it automatically.',
+ isError: true,
+ },
},
refs: {
operationId: 'capability-operation',
@@ -132,6 +138,7 @@ async function prepare(
toolName: 'client_tool',
canonicalArgsHash,
recoveryMode,
+ resultProjectionVersion: 1,
},
},
refs: { operationId, toolCallId: providerToolCallId },
diff --git a/packages/runtime-host/src/__tests__/execution-model-composition.test.ts b/packages/runtime-host/src/__tests__/execution-model-composition.test.ts
index e7a74b52a6..698da29dab 100644
--- a/packages/runtime-host/src/__tests__/execution-model-composition.test.ts
+++ b/packages/runtime-host/src/__tests__/execution-model-composition.test.ts
@@ -573,6 +573,178 @@ test('backend creation admits an enabled model a snapshot never listed', async (
await backend.dispose();
});
+test('Host reopens one projected image from its ArtifactStore authority', async () => {
+ const base = await mkdtemp(join(tmpdir(), 'maka-host-projection-image-'));
+ const capability = await resolveStorageRoot({
+ path: join(base, 'interactive'),
+ kind: 'interactive',
+ });
+ const runtimePath = join(base, 'runtime.sqlite');
+ const pngBytes = Buffer.from(
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==',
+ 'base64',
+ );
+ const sessionId = 'backend-creation-session';
+ const runId = 'projection-image-run';
+ const turnId = 'projection-image-turn';
+ const head: RuntimeEvent = {
+ id: 'projection-image-head',
+ invocationId: runId,
+ runId,
+ sessionId,
+ turnId,
+ ts: 1,
+ partial: false,
+ role: 'user',
+ author: 'user',
+ content: { kind: 'text', text: 'Return the projected image.' },
+ };
+ let owner = await tryAcquireInteractiveRootOwner(capability);
+ assert.ok(owner);
+ if (!owner) return;
+ const provider = await startProvider();
+ provider.configureProjectionImageFlow('ProjectedImage');
+ const assertProjectedImage = (body: Record<string, unknown> | undefined) => {
+ assert.ok(body);
+ assert.doesNotMatch(JSON.stringify(body), /raw execution fact/u);
+ assert.deepEqual(JSON.parse(latestToolResultText(body) ?? 'null'), [
+ {
+ type: 'file',
+ mediaType: 'image/png',
+ data: { type: 'data', data: pngBytes.toString('base64') },
+ },
+ ]);
+ };
+ let backend: Awaited<ReturnType<typeof createHostAiSdkBackend>> | undefined;
+ let artifacts: Awaited<ReturnType<typeof openInteractiveArtifactStoreForWrite>> | undefined;
+ let runtime = createSqliteRuntimeStore(runtimePath);
+ try {
+ artifacts = await openInteractiveArtifactStoreForWrite(owner.lease);
+ await artifacts.recover();
+ await runtime.appendRuntimeEvent(sessionId, runId, head);
+ backend = await createHostAiSdkBackend(
+ backendCreationFixture({
+ abortSignal: new AbortController().signal,
+ resolveExecutionConnection: async () =>
+ readyExecutionConnection(provider.baseUrl, { vision: true }),
+ readPricing: async () => ({ revision: 0, overrides: [] }),
+ executionBoundary: createBypassExecutionBoundary(0),
+ tools: [
+ {
+ name: 'ProjectedImage',
+ description: 'Return one inline image.',
+ parameters: z.object({}),
+ recoveryMode: 'replay_safe',
+ impl: async () => ({ private: 'raw execution fact' }),
+ toModelOutput: () => ({
+ type: 'content',
+ value: [
+ {
+ type: 'file',
+ data: { type: 'data', data: pngBytes.toString('base64') },
+ mediaType: 'image/png',
+ },
+ ],
+ }),
+ },
+ ],
+ artifacts,
+ loadTurnRuntimeEvents: () => runtime.readImmutableRuntimeEvents(sessionId, runId),
+ runtimeCommitSink: runtime,
+ }),
+ );
+ for await (const _event of backend.send({
+ invocationId: runId,
+ runId,
+ turnId,
+ headAnchorRuntimeEvent: head,
+ text: 'Return the projected image.',
+ context: [],
+ runtimeContext: [head],
+ })) {
+ // Drain the complete live tool step.
+ }
+ const liveRequests = provider.requests.filter((request) => request.body.stream === true);
+ assert.equal(liveRequests.length, 2);
+ assertProjectedImage(liveRequests[1]?.body);
+
+ const nextRunId = 'projection-image-next-run';
+ const nextText = 'Continue in the same process.';
+ const nextHead: RuntimeEvent = {
+ id: 'projection-image-next-head',
+ invocationId: nextRunId,
+ runId: nextRunId,
+ sessionId,
+ turnId: 'projection-image-next-turn',
+ ts: 2,
+ partial: false,
+ role: 'user',
+ author: 'user',
+ content: { kind: 'text', text: nextText },
+ };
+ await runtime.appendRuntimeEvent(sessionId, nextRunId, nextHead);
+ const nextTurnContext = [...(await runtime.readRuntimeEvents(sessionId, runId)), nextHead];
+ for await (const _event of backend.send({
+ invocationId: nextRunId,
+ runId: nextRunId,
+ turnId: nextHead.turnId,
+ headAnchorRuntimeEvent: nextHead,
+ text: nextText,
+ context: [],
+ runtimeContext: nextTurnContext,
+ })) {
+ // Drain the next Turn built from the same committed projection.
+ }
+ const nextTurnRequests = provider.requests.filter((request) => request.body.stream === true);
+ assert.equal(nextTurnRequests.length, 3);
+ assertProjectedImage(nextTurnRequests[2]?.body);
+
+ await backend.dispose();
+ backend = undefined;
+ artifacts.close();
+ artifacts = undefined;
+ runtime.close();
+ await owner.close();
+
+ owner = await tryAcquireInteractiveRootOwner(capability);
+ assert.ok(owner);
+ if (!owner) return;
+ artifacts = await openInteractiveArtifactStoreForWrite(owner.lease);
+ await artifacts.recover();
+ runtime = createSqliteRuntimeStore(runtimePath);
+ const recoveredEvents = await runtime.readRuntimeEvents(sessionId, runId);
+ backend = await createHostAiSdkBackend(
+ backendCreationFixture({
+ abortSignal: new AbortController().signal,
+ resolveExecutionConnection: async () =>
+ readyExecutionConnection(provider.baseUrl, { vision: true }),
+ readPricing: async () => ({ revision: 0, overrides: [] }),
+ artifacts,
+ }),
+ );
+ for await (const _event of backend.send({
+ invocationId: 'projection-image-replay-invocation',
+ runId: 'projection-image-replay-run',
+ turnId: 'projection-image-replay-turn',
+ text: 'Continue after restart.',
+ context: [],
+ runtimeContext: recoveredEvents,
+ })) {
+ // Drain the replay request built from the reopened authorities.
+ }
+ const streamRequests = provider.requests.filter((request) => request.body.stream === true);
+ assert.equal(streamRequests.length, 4);
+ assertProjectedImage(streamRequests[3]?.body);
+ } finally {
+ await backend?.dispose();
+ artifacts?.close();
+ runtime.close();
+ await owner?.close();
+ await provider.close();
+ await rm(base, { recursive: true, force: true });
+ }
+});
+
test('provider dispatch fails closed when the Run Composition commit fails', async () => {
const provider = await startProvider();
let commits = 0;
@@ -3558,6 +3730,7 @@ function backendCreationFixture(input: {
recordModelCallAttempt?: BackendFactoryContext['recordModelCallAttempt'];
createFetchTransport?: HostAiSdkBackendInput['createFetchTransport'];
createRunComposer?: HostAiSdkBackendInput['createRunComposer'];
+ artifacts?: HostAiSdkBackendInput['artifacts'];
}): HostAiSdkBackendInput {
const runtimePolicy =
input.runtimePolicy ??
@@ -3631,7 +3804,7 @@ function backendCreationFixture(input: {
runtimePolicy,
...(input.oauthCredentials ? { oauthCredentials: input.oauthCredentials } : {}),
createRunComposer,
- artifacts: {},
+ artifacts: input.artifacts ?? {},
executionArtifacts: {
recordToolArtifacts: async () => undefined,
toolResultArchive: createToolResultArchiveCapability({
@@ -3667,6 +3840,7 @@ function readyExecutionConnection(
customization: {
readonly requestHeaders?: Readonly<Record<string, string>>;
readonly requestBodyOverlay?: Readonly<Record<string, unknown>>;
+ readonly vision?: boolean;
} = {},
) {
return {
@@ -3682,7 +3856,11 @@ function readyExecutionConnection(
models: [
{
id: MODEL_ID,
- capabilities: { chat: true, functionCalling: true },
+ capabilities: {
+ chat: true,
+ functionCalling: true,
+ ...(customization.vision !== undefined ? { vision: customization.vision } : {}),
+ },
contextWindow: 8_192,
maxOutputTokens: 1_024,
},
@@ -3959,6 +4137,7 @@ type ProviderFlow =
readonly groupId: string;
readonly toolName: string;
}
+ | { readonly kind: 'projection_image'; readonly toolName: string }
| { readonly kind: 'child_agent' }
| {
readonly kind: 'implementation_child_agent';
@@ -3972,6 +4151,7 @@ async function startProvider(): Promise<{
readonly requests: ProviderRequest[];
configureManagedBashFlow(sandboxPaths?: ManagedSandboxPaths): void;
configureClientCapability(input: { groupId: string; toolName: string }): void;
+ configureProjectionImageFlow(toolName: string): void;
configureChildAgentFlow(): void;
configureImplementationChildAgentFlow(): void;
configureAgentGraphFlow(): void;
@@ -4001,6 +4181,10 @@ async function startProvider(): Promise<{
if (flow.kind !== 'default') throw new Error('Provider flow is already configured');
flow = { kind: 'client_capability', ...input };
},
+ configureProjectionImageFlow: (toolName) => {
+ if (flow.kind !== 'default') throw new Error('Provider flow is already configured');
+ flow = { kind: 'projection_image', toolName };
+ },
configureChildAgentFlow: () => {
if (flow.kind !== 'default') throw new Error('Provider flow is already configured');
flow = { kind: 'child_agent' };
@@ -4077,6 +4261,15 @@ async function handleProviderRequest(
return;
}
const streamRequestIndex = requests.filter((candidate) => candidate.body.stream === true).length;
+ if (flow.kind === 'projection_image' && streamRequestIndex === 1) {
+ assert.ok(toolNames(body).includes(flow.toolName));
+ respondProviderToolCall(response, streamRequestIndex, flow.toolName, {});
+ return;
+ }
+ if (flow.kind === 'projection_image') {
+ respondProviderText(response, RESPONSE_TEXT);
+ return;
+ }
if (flow.kind === 'managed_bash' && streamRequestIndex === 1) {
assert.ok(toolNames(body).includes('Bash'));
respondProviderToolCall(response, streamRequestIndex, 'Bash', {
diff --git a/packages/runtime-host/src/__tests__/session-revision-two-client-uds.test.ts b/packages/runtime-host/src/__tests__/session-revision-two-client-uds.test.ts
index a77e117d70..6c09508c64 100644
--- a/packages/runtime-host/src/__tests__/session-revision-two-client-uds.test.ts
+++ b/packages/runtime-host/src/__tests__/session-revision-two-client-uds.test.ts
@@ -363,7 +363,7 @@ async function verifyConcurrentRevisionAuthority(
});
assert.equal(artifactPage.kind, 'page');
if (artifactPage.kind !== 'page') assert.fail('Branch Artifact query must return a page');
- assert.equal(artifactPage.artifacts.length, 2);
+ assert.equal(artifactPage.artifacts.length, 3);
assert.notEqual(artifactPage.artifacts[0]?.id, 'source-artifact');
const taskPage = await tui.request('task.ledger.query', {
kind: 'list_start',
@@ -881,6 +881,17 @@ async function seedSource(
source: 'user_upload',
now: 1,
});
+ const projectionArtifact = await artifacts.create({
+ id: 'source-projection-artifact',
+ sessionId: source.id,
+ turnId: 'turn-1',
+ name: 'projection.png',
+ kind: 'file',
+ content: new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
+ mimeType: 'image/png',
+ source: 'tool_result',
+ now: 2,
+ });
await artifacts.create({
id: 'legacy-child-artifact',
sessionId: source.id,
@@ -981,6 +992,53 @@ async function seedSource(
author: 'agent',
content: { kind: 'text', text: 'first response' },
}),
+ runtimeEvent(source.id, 'run-turn-1', 'invocation-turn-1', 'turn-1', {
+ id: 'projection-call',
+ ts: 2.1,
+ role: 'model',
+ author: 'agent',
+ content: {
+ kind: 'function_call',
+ id: 'projection-tool-call',
+ name: 'Read',
+ args: { path: 'projection.png' },
+ },
+ }),
+ runtimeEvent(source.id, 'run-turn-1', 'invocation-turn-1', 'turn-1', {
+ id: 'projection-result',
+ ts: 2.2,
+ role: 'tool',
+ author: 'tool',
+ content: {
+ kind: 'function_response',
+ id: 'projection-tool-call',
+ name: 'Read',
+ result: {
+ kind: 'image',
+ mimeType: 'image/png',
+ ref: {
+ kind: 'session_file',
+ sessionId: source.id,
+ relativePath: projectionArtifact.id,
+ },
+ },
+ modelProjection: {
+ version: 1,
+ kind: 'content',
+ parts: [
+ {
+ kind: 'artifact',
+ mediaType: 'image/png',
+ ref: {
+ kind: 'session_file',
+ sessionId: source.id,
+ relativePath: projectionArtifact.id,
+ },
+ },
+ ],
+ },
+ },
+ }),
runtimeEvent(source.id, 'run-turn-1', 'invocation-turn-1', 'turn-1', {
id: 'terminal-1',
ts: 2.5,
@@ -1552,7 +1610,7 @@ async function verifyDurableBranch(
});
};
const messages = await execution.sessionStore.readMessagesSnapshot(branchSessionId);
- assert.equal(messages.length, 3);
+ assert.equal(messages.length, 5);
const user = messages.find((message) => message.type === 'user');
assert.ok(user?.attachments?.[0]);
const ref = user?.attachments?.[0]?.ref;
@@ -1562,7 +1620,8 @@ async function verifyDurableBranch(
// The user-upload attachment ref carries the source artifact id; the copy
// must rewrite it to a fresh target artifact id, never leave the source id.
assert.notEqual(ref.relativePath, 'source-artifact');
- assert.equal((await artifacts.listPage(branchSessionId, { offset: 0, limit: 10 })).total, 2);
+ const branchArtifacts = await artifacts.listPage(branchSessionId, { offset: 0, limit: 10 });
+ assert.equal(branchArtifacts.total, 3);
assert.deepEqual(await artifacts.readTextInSession(branchSessionId, ref.relativePath), {
ok: true,
text: 'retained bytes',
@@ -1579,6 +1638,33 @@ async function verifyDurableBranch(
assert.ok(copiedChild);
assert.ok(copiedParent);
assert.equal(copiedChild.parentRunId, copiedParent.runId);
+ const copiedProjectionResult = (
+ await execution.runtimeEventStore.readRuntimeEvents(branchSessionId, copiedParent.runId)
+ ).find((event) => event.content?.kind === 'function_response');
+ assert.equal(copiedProjectionResult?.content?.kind, 'function_response');
+ if (copiedProjectionResult?.content?.kind !== 'function_response') {
+ assert.fail('Copied branch must retain the durable Tool Result projection');
+ }
+ const copiedProjection = copiedProjectionResult.content.modelProjection;
+ assert.equal(copiedProjection?.kind, 'content');
+ if (copiedProjection?.kind !== 'content') {
+ assert.fail('Copied Tool Result must retain artifact projection content');
+ }
+ const copiedProjectionPart = copiedProjection.parts[0];
+ assert.equal(copiedProjectionPart?.kind, 'artifact');
+ if (copiedProjectionPart?.kind !== 'artifact') {
+ assert.fail('Copied Tool Result must retain its projected artifact');
+ }
+ assert.equal(copiedProjectionPart.ref.kind, 'session_file');
+ if (copiedProjectionPart.ref.kind !== 'session_file') {
+ assert.fail('Copied Tool Result artifact must remain Session-backed');
+ }
+ assert.equal(copiedProjectionPart.ref.sessionId, branchSessionId);
+ const copiedProjectionArtifact = branchArtifacts.records.find(
+ (record) => record.name === 'projection.png',
+ );
+ assert.ok(copiedProjectionArtifact);
+ assert.equal(copiedProjectionPart.ref.relativePath, copiedProjectionArtifact.id);
assert.equal((await artifacts.listPage('revision-target', { offset: 0, limit: 10 })).total, 0);
assert.deepEqual(await tasks.list('revision-target'), []);
assert.deepEqual(await execution.agentRunStore.listSessionRuns('revision-target'), []);
diff --git a/packages/runtime-host/src/server/artifact-coordinator.ts b/packages/runtime-host/src/server/artifact-coordinator.ts
index 195d5fb40c..3a278deb99 100644
--- a/packages/runtime-host/src/server/artifact-coordinator.ts
+++ b/packages/runtime-host/src/server/artifact-coordinator.ts
@@ -20,7 +20,7 @@
import { createHash } from 'node:crypto';
import { attachmentKindFromMimeType } from '@maka/core/attachments';
import type { AttachmentRef } from '@maka/core/events';
-import type { ArtifactRecord } from '@maka/core/artifacts';
+import { isArtifactSharedSessionReadable, type ArtifactRecord } from '@maka/core/artifacts';
import {
authenticateInteractiveArtifactStoreWriter,
sanitizeArtifactName,
@@ -51,8 +51,6 @@ import { ConnectionBoundChunkUploads } from './connection-bound-chunk-uploads.js
const MAX_ACTIVE_ARTIFACT_UPLOADS = 16;
const MAX_STAGED_ARTIFACT_UPLOAD_BYTES = 128 * 1024 * 1024;
const ARTIFACT_UPLOAD_TTL_MS = 5 * 60 * 1000;
-const SHARED_ARTIFACT_SOURCES = new Set(['user_upload', 'tool_result']);
-
interface ArtifactUploadMetadata {
readonly attachmentKind: AttachmentRef['kind'];
readonly name: string;
@@ -443,9 +441,7 @@ export class HostArtifactCoordinator {
);
if (!grant) return;
const entry = await this.#store.getInSession(input.sessionId, input.artifactId);
- return entry.record?.status === 'live' &&
- entry.record.source !== undefined &&
- SHARED_ARTIFACT_SOURCES.has(entry.record.source)
+ return entry.record?.status === 'live' && isArtifactSharedSessionReadable(entry.record)
? grant.grantId
: undefined;
}
diff --git a/packages/runtime-host/src/server/client-capability-recovery.ts b/packages/runtime-host/src/server/client-capability-recovery.ts
index 1ad74e8402..43aec6f104 100644
--- a/packages/runtime-host/src/server/client-capability-recovery.ts
+++ b/packages/runtime-host/src/server/client-capability-recovery.ts
@@ -19,6 +19,7 @@
import type { ToolResultContent } from '@maka/core/events';
import type { RuntimeEvent } from '@maka/core/runtime-event';
+import { compatibilityToolResultProjection } from '@maka/runtime/durable-tool-result-projection';
import type { ExecutionRuntimeEventWriter } from '@maka/storage/execution-stores';
const OUTCOME_UNKNOWN_TEXT =
@@ -43,6 +44,14 @@ export async function recoverClientCapabilityOutcomes(
retrySafe: false,
},
} as const satisfies ToolResultContent;
+ const responseContent = {
+ kind: 'function_response' as const,
+ id: operation.providerToolCallId,
+ name: operation.toolName,
+ result,
+ isError: true as const,
+ };
+ const modelProjection = compatibilityToolResultProjection(responseContent, sessionId);
const runtimeEvent: RuntimeEvent = {
id: `${operation.operationId}_response`,
invocationId: operation.invocationId,
@@ -53,13 +62,7 @@ export async function recoverClientCapabilityOutcomes(
partial: false,
role: 'tool',
author: 'tool',
- content: {
- kind: 'function_response',
- id: operation.providerToolCallId,
- name: operation.toolName,
- result,
- isError: true,
- },
+ content: { ...responseContent, ...(modelProjection ? { modelProjection } : {}) },
refs: {
operationId: operation.operationId,
toolCallId: operation.providerToolCallId,
diff --git a/packages/runtime-host/src/server/execution-model-composition.ts b/packages/runtime-host/src/server/execution-model-composition.ts
index 454aa2338a..3408e5cf50 100644
--- a/packages/runtime-host/src/server/execution-model-composition.ts
+++ b/packages/runtime-host/src/server/execution-model-composition.ts
@@ -48,6 +48,7 @@ import { type BackendFactoryContext } from '@maka/runtime/session-manager';
import { type RuntimeCommitSink } from '@maka/runtime/runtime-commit-sink';
import {
createAttachmentByteReader,
+ createReadImageSnapshotPlanner,
persistProviderRequestCaptureArtifact,
type InteractiveArtifactStoreWriter,
} from '@maka/storage/artifact-stores';
@@ -332,6 +333,7 @@ export async function createHostAiSdkBackend(input: HostAiSdkBackendInput): Prom
);
}
: undefined;
+ const planProjectionImage = createReadImageSnapshotPlanner(input.artifacts);
try {
return new HostAiSdkBackend(
@@ -398,6 +400,14 @@ export async function createHostAiSdkBackend(input: HostAiSdkBackendInput): Prom
? { readImageSnapshotsUnavailable: true }
: {}),
}),
+ prepareDurableProjectionArtifact: ({ turnId, bytes, mediaType }) =>
+ planProjectionImage({
+ sessionId: input.context.sessionId,
+ turnId,
+ name: 'Tool Result image',
+ bytes,
+ mimeType: mediaType,
+ }),
recordToolArtifacts: input.executionArtifacts.recordToolArtifacts,
toolResultArchive: input.executionArtifacts.toolResultArchive,
...(!input.context.tools &&
diff --git a/packages/runtime/package.json b/packages/runtime/package.json
index 35b4ad5e48..fd2c28d77e 100644
--- a/packages/runtime/package.json
+++ b/packages/runtime/package.json
@@ -13,6 +13,7 @@
"./shell-tools": "./dist/shell-tools.js",
"./shell-run-manager": "./dist/shell-run-manager.js",
"./deep-research-tools": "./dist/deep-research-tools.js",
+ "./durable-tool-result-projection": "./dist/durable-tool-result-projection.js",
"./tool-artifacts": "./dist/tool-artifacts.js",
"./model-factory": "./dist/model-factory.js",
"./model-runtime": "./dist/model-runtime.js",
diff --git a/packages/runtime/src/__tests__/ai-sdk-backend.test.ts b/packages/runtime/src/__tests__/ai-sdk-backend.test.ts
index 8ac41d9d43..50a956ba7b 100644
--- a/packages/runtime/src/__tests__/ai-sdk-backend.test.ts
+++ b/packages/runtime/src/__tests__/ai-sdk-backend.test.ts
@@ -20,7 +20,7 @@
import assert from 'node:assert/strict';
import { Buffer } from 'node:buffer';
import { createHash } from 'node:crypto';
-import { resolve } from 'node:path';
+import { join, resolve } from 'node:path';
import { describe, test } from 'node:test';
import type { ModelMessage, ModelStreamResult } from '../model-protocol.js';
import { MockLanguageModelV4, simulateReadableStream } from 'ai/test';
@@ -341,6 +341,78 @@ describe('AiSdkBackend ApplyPatch routing', () => {
await assertApplyPatchHistoryDowngraded(targetConnection, targetConnection.defaultModel!);
});
+ test('preserves a durable projection failure when apply_patch history is downgraded', async () => {
+ const model = completionModel();
+ const backend = createTestAiSdkBackend({
+ sessionId: 'session-1',
+ header: header(),
+ appendMessage: async () => {},
+ connection: connection(),
+ apiKey: 'sk-test',
+ modelId: 'mock-model-id',
+ modelFactory: () => model,
+ tools: [nativeApplyPatchTool()],
+ newId: idGenerator(),
+ now: monotonicClock(),
+ });
+
+ await drain(
+ backend.send({
+ turnId: 'turn-current',
+ text: 'continue',
+ context: [],
+ runtimeContext: [
+ runtimeEvent({
+ id: 'rt-call',
+ turnId: 'turn-previous',
+ role: 'model',
+ author: 'agent',
+ content: {
+ kind: 'function_call',
+ id: 'call-1',
+ name: 'apply_patch',
+ args: [
+ '*** Begin Patch',
+ '*** Update File: file.txt',
+ '@@',
+ '-before',
+ '+after',
+ '*** End Patch',
+ ].join('\n'),
+ },
+ }),
+ runtimeEvent({
+ id: 'rt-result',
+ turnId: 'turn-previous',
+ role: 'tool',
+ author: 'tool',
+ content: {
+ kind: 'function_response',
+ id: 'call-1',
+ name: 'apply_patch',
+ result: { status: 'completed', output: 'Applied 1 file operation.' },
+ modelProjection: {
+ version: 1,
+ kind: 'failure',
+ reason: 'projection_failed',
+ message:
+ 'The tool completed, but its model-visible result could not be projected safely.',
+ },
+ },
+ }),
+ ],
+ }),
+ );
+
+ const replayText = (compactPrompt(model) as Array<{ content: any[] }>)
+ .flatMap((message) => message.content)
+ .filter((part) => part.type === 'text')
+ .map((part) => part.text)
+ .join('\n');
+ assert.match(replayText, /could not be projected safely/);
+ assert.doesNotMatch(replayText, /ApplyPatch completed/);
+ });
+
test('preserves a multi-file ApplyPatch fact when structured replay cannot represent it', async () => {
const model = completionModel();
const backend = createTestAiSdkBackend({
@@ -3710,6 +3782,7 @@ describe('AiSdkBackend model history', () => {
'base64',
);
let calls = 0;
+ let artifactReads = 0;
const anchor = runtimeTextEvent({
id: 'runtime-user',
turnId: 'turn-1',
@@ -3785,7 +3858,11 @@ describe('AiSdkBackend model history', () => {
},
],
supportsVision: true,
- readAttachmentBytes: async () => ({ ok: true, bytes: pngBytes }),
+ maxProviderImageRequestBytes: pngBytes.byteLength,
+ readAttachmentBytes: async () => {
+ artifactReads += 1;
+ return { ok: true, bytes: pngBytes };
+ },
loadTurnRuntimeEvents: async () => ledger,
newId: idGenerator(),
now: monotonicClock(),
@@ -3806,6 +3883,7 @@ describe('AiSdkBackend model history', () => {
assert.ok(
result.value.some((part: any) => part.type === 'file' && part.mediaType === 'image/png'),
);
+ assert.equal(artifactReads, 1);
});
test('reloads durable multi-tool settlement before terminal continuation', async () => {
@@ -6907,7 +6985,7 @@ describe('AiSdkBackend error surfaces', () => {
assert.equal(text.endsWith('…'), true);
});
- test('writeSyntheticToolResult never persists raw secret-shaped errors', async () => {
+ test('tool settlement never persists raw secret-shaped synthetic errors', async () => {
const messages: ToolResultMessage[] = [];
const events: SessionEvent[] = [];
const backend = createTestAiSdkBackend({
@@ -6925,19 +7003,18 @@ describe('AiSdkBackend error surfaces', () => {
now: () => 1,
});
- await turnScope(backend, 'turn-1').toolRuntime.writeSyntheticToolResult(
- 'tool-1',
- 'turn-1',
- 'failed with api_key=sk-live-secret-token-value',
- {
- push: (event) => {
- events.push(event);
- },
- pushAndWaitUntilConsumed: async (event) => {
- events.push(event);
- },
+ const tool: MakaTool = {
+ name: 'FailingTool',
+ description: 'fails with a provider secret',
+ parameters: {},
+ impl: async () => {
+ throw new Error('failed with api_key=sk-live-secret-token-value');
},
- );
+ };
+
+ await runtimeExecute(backend, tool, 'turn-1', {
+ push: (event) => events.push(event),
+ })({}, { toolCallId: 'tool-1', abortSignal: new AbortController().signal });
assert.equal(JSON.stringify(messages).includes('sk-live-secret-token-value'), false);
assert.equal(JSON.stringify(events).includes('sk-live-secret-token-value'), false);
diff --git a/packages/runtime/src/__tests__/computer-use-model-loop.test.ts b/packages/runtime/src/__tests__/computer-use-model-loop.test.ts
index 2e071cad89..3ba7e3c99c 100644
--- a/packages/runtime/src/__tests__/computer-use-model-loop.test.ts
+++ b/packages/runtime/src/__tests__/computer-use-model-loop.test.ts
@@ -394,6 +394,7 @@ function createRuntime(input: {
modelId: 'mock-computer-model',
modelFactory: () => input.model,
tools: [input.computerTool],
+ testProjectionArtifacts: true,
...(input.durable ? { loadTurnRuntimeEvents: input.durable.loadTurnRuntimeEvents } : {}),
newId: idGenerator(),
now: monotonicClock(),
diff --git a/packages/runtime/src/__tests__/computer-use-provider-protocol.test.ts b/packages/runtime/src/__tests__/computer-use-provider-protocol.test.ts
index cbac5ac106..e928514689 100644
--- a/packages/runtime/src/__tests__/computer-use-provider-protocol.test.ts
+++ b/packages/runtime/src/__tests__/computer-use-provider-protocol.test.ts
@@ -150,6 +150,7 @@ describe('Anthropic-compatible Computer Use product loops', () => {
provider.expectedWireOutputLimit,
);
const runtime = createTestAiSdkBackend({
+ testProjectionArtifacts: true,
sessionId,
header: header(provider.providerType, provider.modelId),
appendMessage: async () => {},
@@ -275,6 +276,7 @@ describe('Anthropic-compatible Computer Use product loops', () => {
});
const events: SessionEvent[] = [];
const runtime = createTestAiSdkBackend({
+ testProjectionArtifacts: true,
sessionId,
header: header(provider.providerType, provider.modelId),
appendMessage: async () => {},
@@ -378,6 +380,7 @@ describe('OpenAI-compatible product loops', () => {
'openai-chat',
);
const runtime = createTestAiSdkBackend({
+ testProjectionArtifacts: true,
sessionId,
header: header(provider.providerType, provider.modelId),
appendMessage: async () => {},
@@ -494,6 +497,7 @@ describe('OpenAI-compatible product loops', () => {
now: monotonicClock(),
});
const runtime = createTestAiSdkBackend({
+ testProjectionArtifacts: true,
sessionId,
header: header('kimi-coding-plan', 'k3'),
appendMessage: async () => {},
@@ -576,6 +580,7 @@ describe('OpenAI-compatible product loops', () => {
);
const createRuntime = () =>
createTestAiSdkBackend({
+ testProjectionArtifacts: true,
sessionId,
header: header('kimi-coding-plan', 'k3'),
appendMessage: async (message) => {
@@ -695,6 +700,7 @@ describe('OpenAI-compatible product loops', () => {
);
const events: SessionEvent[] = [];
const runtime = createTestAiSdkBackend({
+ testProjectionArtifacts: true,
sessionId,
header: header('kimi-coding-plan', 'k3'),
appendMessage: async () => {},
diff --git a/packages/runtime/src/__tests__/durable-tool-result-projection.test.ts b/packages/runtime/src/__tests__/durable-tool-result-projection.test.ts
new file mode 100644
index 0000000000..836797f49d
--- /dev/null
+++ b/packages/runtime/src/__tests__/durable-tool-result-projection.test.ts
@@ -0,0 +1,315 @@
+/*
+ * 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.
+ */
+
+import assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+import { MAX_READ_IMAGE_BYTES } from '@maka/core/attachments';
+import { DURABLE_TOOL_RESULT_PROJECTION_MAX_BYTES } from '@maka/core/durable-tool-result-projection';
+import { serializedByteLength } from '@maka/core/serialized-byte-length';
+
+import {
+ decodeEffectiveToolResultProjection,
+ encodeDefaultDurableToolResultOutput,
+ encodeDurableToolResultOutput,
+ encodeDurableToolResultOutputWithArtifacts,
+} from '../durable-tool-result-projection.js';
+
+describe('durable Tool Result projection codec', () => {
+ it('preserves arbitrary text and JSON content faithfully', () => {
+ assert.deepEqual(
+ encodeDurableToolResultOutput(
+ { type: 'text', value: 'Authorization: Bearer sk-live-secret-token-value' },
+ 'session-1',
+ ),
+ {
+ version: 1,
+ kind: 'text',
+ text: 'Authorization: Bearer sk-live-secret-token-value',
+ },
+ );
+ assert.deepEqual(
+ encodeDurableToolResultOutput(
+ {
+ type: 'json',
+ value: { password: 'correct-horse-battery-staple', keep: 'visible' },
+ },
+ 'session-1',
+ ),
+ {
+ version: 1,
+ kind: 'json',
+ value: { password: 'correct-horse-battery-staple', keep: 'visible' },
+ },
+ );
+ });
+
+ it('never persists inline binary or provider options', () => {
+ const projection = encodeDurableToolResultOutput(
+ {
+ type: 'content',
+ value: [
+ { type: 'text', text: 'visible', providerOptions: { provider: { opaque: true } } },
+ {
+ type: 'file',
+ data: { type: 'data', data: 'unbounded-base64-payload' },
+ mediaType: 'image/png',
+ providerOptions: { provider: { opaque: true } },
+ },
+ ],
+ },
+ 'session-1',
+ );
+
+ assert.doesNotMatch(JSON.stringify(projection), /unbounded-base64-payload|providerOptions/);
+ assert.deepEqual(projection, {
+ version: 1,
+ kind: 'content',
+ parts: [
+ { kind: 'text', text: 'visible' },
+ {
+ kind: 'text',
+ text: '[Binary tool output omitted from the durable model projection; repeat the tool call if it is still needed.]',
+ },
+ ],
+ });
+ });
+
+ it('maps opaque and oversized JSON to the same deterministic failure sentinel', () => {
+ const opaque = encodeDurableToolResultOutput(
+ { type: 'json', value: new URL('https://example.test') as never },
+ 'session-1',
+ );
+ const oversized = encodeDurableToolResultOutput(
+ { type: 'json', value: { body: 'x'.repeat(300_000) } },
+ 'session-1',
+ );
+
+ assert.deepEqual(opaque, oversized);
+ assert.deepEqual(opaque, {
+ version: 1,
+ kind: 'failure',
+ reason: 'projection_failed',
+ message: 'The tool completed, but its model-visible result could not be projected safely.',
+ });
+ });
+
+ it('fails deterministically instead of silently dropping excess content parts', () => {
+ const projection = encodeDurableToolResultOutput(
+ {
+ type: 'content',
+ value: Array.from({ length: 65 }, (_, index) => ({
+ type: 'text' as const,
+ text: `part-${index}`,
+ })),
+ },
+ 'session-1',
+ );
+
+ assert.equal(projection.kind, 'failure');
+ });
+
+ it('validates every inline image before persisting any projection artifact', async () => {
+ let writes = 0;
+ const projection = await encodeDurableToolResultOutputWithArtifacts(
+ {
+ type: 'content',
+ value: [
+ {
+ type: 'file',
+ data: { type: 'data', data: Buffer.from('valid').toString('base64') },
+ mediaType: 'image/png',
+ },
+ {
+ type: 'file',
+ data: { type: 'data', data: 'not-canonical-base64' },
+ mediaType: 'image/png',
+ },
+ ],
+ },
+ 'session-1',
+ artifactPlanner(() => {
+ writes += 1;
+ }),
+ );
+
+ assert.equal(projection.kind, 'failure');
+ assert.equal(writes, 0);
+ });
+
+ it('rejects an oversized ArrayBuffer before copying its bytes', async () => {
+ class ObservableArrayBuffer extends ArrayBuffer {
+ copies = 0;
+
+ override slice(begin?: number, end?: number): ArrayBuffer {
+ this.copies += 1;
+ return super.slice(begin, end);
+ }
+ }
+ const data = new ObservableArrayBuffer(MAX_READ_IMAGE_BYTES + 1);
+ const projection = await encodeDurableToolResultOutputWithArtifacts(
+ {
+ type: 'content',
+ value: [
+ {
+ type: 'file',
+ data: { type: 'data', data },
+ mediaType: 'image/png',
+ },
+ ],
+ },
+ 'session-1',
+ () => {
+ throw new Error('oversized bytes must not reach artifact planning');
+ },
+ );
+
+ assert.equal(projection.kind, 'failure');
+ assert.equal(data.copies, 0);
+ });
+
+ it('validates the complete projection before persisting any artifact', async () => {
+ let writes = 0;
+ const projection = await encodeDurableToolResultOutputWithArtifacts(
+ {
+ type: 'content',
+ value: [
+ ...Array.from({ length: 64 }, (_, index) => ({
+ type: 'text' as const,
+ text: `part-${index}`,
+ })),
+ {
+ type: 'file',
+ data: { type: 'data' as const, data: Buffer.from('valid').toString('base64') },
+ mediaType: 'image/png',
+ },
+ ],
+ },
+ 'session-1',
+ artifactPlanner(() => {
+ writes += 1;
+ }),
+ );
+
+ assert.equal(projection.kind, 'failure');
+ assert.equal(writes, 0);
+ });
+
+ it('uses the exact planned artifact ref as the projection size authority', async () => {
+ const ref = {
+ kind: 'session_file' as const,
+ sessionId: 'session-1',
+ relativePath: 'artifact-1',
+ };
+ const emptyProjection = {
+ version: 1 as const,
+ kind: 'content' as const,
+ parts: [
+ { kind: 'text' as const, text: '' },
+ { kind: 'artifact' as const, mediaType: 'image/png', ref },
+ ],
+ };
+ const textLength =
+ DURABLE_TOOL_RESULT_PROJECTION_MAX_BYTES - serializedByteLength(emptyProjection);
+ let writes = 0;
+ const projection = await encodeDurableToolResultOutputWithArtifacts(
+ {
+ type: 'content',
+ value: [
+ { type: 'text', text: 'x'.repeat(textLength) },
+ {
+ type: 'file',
+ data: { type: 'data', data: Buffer.from('valid').toString('base64') },
+ mediaType: 'image/png',
+ },
+ ],
+ },
+ 'session-1',
+ () => ({
+ ref,
+ persist: async () => {
+ writes += 1;
+ },
+ }),
+ );
+
+ assert.equal(projection.kind, 'content');
+ assert.equal(writes, 1);
+ });
+
+ it('rejects unsafe image metadata before persisting it', async () => {
+ let writes = 0;
+ const projection = await encodeDurableToolResultOutputWithArtifacts(
+ {
+ type: 'content',
+ value: [
+ {
+ type: 'file',
+ data: { type: 'data', data: Buffer.from('valid').toString('base64') },
+ mediaType: 'image/png; token=sk-secret',
+ },
+ ],
+ },
+ 'session-1',
+ artifactPlanner(() => {
+ writes += 1;
+ }),
+ );
+
+ assert.equal(projection.kind, 'failure');
+ assert.equal(writes, 0);
+ });
+
+ it('validates default image refs through the same closed schema', () => {
+ const legacyImage = {
+ kind: 'image',
+ mimeType: 'image/png',
+ ref: {
+ kind: 'session_file',
+ sessionId: 'session-1',
+ relativePath: 'legacy/path.png',
+ },
+ } as const;
+ assert.equal(encodeDefaultDurableToolResultOutput(legacyImage, 'session-1').kind, 'failure');
+ assert.deepEqual(
+ decodeEffectiveToolResultProjection(
+ {
+ kind: 'function_response',
+ id: 'legacy-image-1',
+ name: 'Read',
+ result: legacyImage,
+ },
+ 'session-1',
+ ),
+ { kind: 'legacy_output', output: legacyImage },
+ );
+ });
+});
+
+function artifactPlanner(onPersist: () => void) {
+ let nextId = 0;
+ return () => {
+ const relativePath = `artifact-${++nextId}`;
+ return {
+ ref: { kind: 'session_file' as const, sessionId: 'session-1', relativePath },
+ persist: async () => {
+ onPersist();
+ },
+ };
+ };
+}
diff --git a/packages/runtime/src/__tests__/execution-boundary-test-helpers.ts b/packages/runtime/src/__tests__/execution-boundary-test-helpers.ts
index 65900de151..a40de02125 100644
--- a/packages/runtime/src/__tests__/execution-boundary-test-helpers.ts
+++ b/packages/runtime/src/__tests__/execution-boundary-test-helpers.ts
@@ -31,12 +31,44 @@ export const readExternalExecutionBoundary: AiSdkBackendInput['readExecutionBoun
createExternalExecutionBoundary();
type TestAiSdkBackendInput = Omit<AiSdkBackendInput, 'readExecutionBoundary'> &
- Partial<Pick<AiSdkBackendInput, 'readExecutionBoundary'>>;
+ Partial<Pick<AiSdkBackendInput, 'readExecutionBoundary'>> & {
+ testProjectionArtifacts?: boolean;
+ };
export function createTestAiSdkBackend(input: TestAiSdkBackendInput): AiSdkBackend {
+ const { testProjectionArtifacts, ...backendInput } = input;
+ const artifacts = new Map<string, Uint8Array>();
+ let nextArtifactId = 0;
return new AiSdkBackend({
readExecutionBoundary: readExternalExecutionBoundary,
- ...input,
+ ...backendInput,
+ ...(testProjectionArtifacts
+ ? {
+ prepareDurableProjectionArtifact: ({ bytes }: { bytes: Uint8Array }) => {
+ const relativePath = `artifact-${++nextArtifactId}`;
+ const accepted = bytes.slice();
+ return {
+ ref: {
+ kind: 'session_file' as const,
+ sessionId: input.sessionId,
+ relativePath,
+ },
+ persist: async () => {
+ artifacts.set(relativePath, accepted);
+ },
+ };
+ },
+ readAttachmentBytes:
+ input.readAttachmentBytes ??
+ (async (ref) => {
+ const bytes =
+ ref.kind === 'session_file' ? artifacts.get(ref.relativePath) : undefined;
+ return bytes
+ ? { ok: true as const, bytes: bytes.slice() }
+ : { ok: false as const, reason: 'not_found' as const };
+ }),
+ }
+ : {}),
});
}
diff --git a/packages/runtime/src/__tests__/overflow-reactive-recovery.test.ts b/packages/runtime/src/__tests__/overflow-reactive-recovery.test.ts
index 510795351b..b726de0dd3 100644
--- a/packages/runtime/src/__tests__/overflow-reactive-recovery.test.ts
+++ b/packages/runtime/src/__tests__/overflow-reactive-recovery.test.ts
@@ -574,7 +574,7 @@ function buildReactiveFixture(options: ReactiveFixtureOptions): ReactiveFixture
ref: {
kind: 'session_file' as const,
sessionId: 'session-1',
- relativePath: 'live.png',
+ relativePath: 'live_image',
},
};
}
diff --git a/packages/runtime/src/__tests__/pre-dispatch-refusal-ledger.test.ts b/packages/runtime/src/__tests__/pre-dispatch-refusal-ledger.test.ts
index a85749edf0..884d8647ea 100644
--- a/packages/runtime/src/__tests__/pre-dispatch-refusal-ledger.test.ts
+++ b/packages/runtime/src/__tests__/pre-dispatch-refusal-ledger.test.ts
@@ -372,7 +372,7 @@ test('client-capability refusal carries actionable bypass metadata', async () =>
test('arguments the schema rejects leave a matched call/response pair on the generic lane', async () => {
const h = harness();
- const { result } = await settle(h, swarmTool, {
+ const settlement = await settle(h, swarmTool, {
items: [
{ item_id: 'a', task: 'one', subagent_id: 'reviewer' },
{ item_id: 'b', task: 'two', subagent_id: 'reviewer' },
@@ -383,7 +383,7 @@ test('arguments the schema rejects leave a matched call/response pair on the gen
// The refusal still reaches the model, unchanged.
assert.match(
- (result as { error?: string }).error ?? '',
+ (settlement.result as { error?: string }).error ?? '',
/Tool "exclusive_batch" arguments failed validation/,
);
diff --git a/packages/runtime/src/__tests__/recovery-resolver.test.ts b/packages/runtime/src/__tests__/recovery-resolver.test.ts
index dd975fcd74..7fd2c4c5a8 100644
--- a/packages/runtime/src/__tests__/recovery-resolver.test.ts
+++ b/packages/runtime/src/__tests__/recovery-resolver.test.ts
@@ -18,9 +18,13 @@
*/
import assert from 'node:assert/strict';
+import { mkdtemp, rm } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
import { describe, it } from 'node:test';
import { canonicalToolArgsHash } from '@maka/core/tool-args-identity';
import type { RuntimeEvent } from '@maka/core/runtime-event';
+import { createSqliteRuntimeStore } from '@maka/storage/sqlite-runtime-store';
import {
buildInterruptedCodeModeOutcomeCommits,
resolveRuntimeRecovery,
@@ -139,6 +143,80 @@ describe('RecoveryResolver', () => {
},
},
);
+ assert.deepEqual(
+ commits[0]?.runtimeEvent.content?.kind === 'function_response'
+ ? commits[0].runtimeEvent.content.modelProjection
+ : undefined,
+ {
+ version: 1,
+ kind: 'json',
+ value: {
+ kind: 'json',
+ value: {
+ kind: 'code_mode',
+ status: 'interrupted',
+ message: 'Code Mode execution was interrupted by runtime recovery.',
+ },
+ },
+ isError: true,
+ },
+ );
+ });
+
+ it('commits a projected recovery outcome through the projection-aware SQLite T2 gate', async () => {
+ const root = await mkdtemp(join(tmpdir(), 'maka-recovery-projection-'));
+ const store = createSqliteRuntimeStore(join(root, 'runtime.sqlite'));
+ try {
+ const call = event({
+ id: 'outer-call',
+ role: 'model',
+ author: 'agent',
+ origin: 'provider',
+ modelVisibility: 'visible',
+ content: { kind: 'function_call', id: 'exec-1', name: 'exec', args: { code: 'work()' } },
+ refs: { operationId: 'outer-op', toolCallId: 'exec-1' },
+ });
+ const dispatch = dispatchFor({
+ id: 'outer-dispatch',
+ operationId: 'outer-op',
+ toolCallId: 'exec-1',
+ toolName: 'exec',
+ args: { code: 'work()' },
+ resultProjectionVersion: 1,
+ });
+ await store.commitToolPrepared({
+ operationId: 'outer-op',
+ journalEventId: 'outer-op_prepared',
+ runtimeEvent: call,
+ dispatchRuntimeEvent: dispatch,
+ providerToolCallId: 'exec-1',
+ toolName: 'exec',
+ canonicalArgsHash: canonicalToolArgsHash('exec', { code: 'work()' }),
+ recoveryMode: 'never_auto_retry',
+ committedAt: 10,
+ });
+
+ const [commit] = buildInterruptedCodeModeOutcomeCommits(
+ await store.readImmutableRuntimeEvents('session-1', 'run-1'),
+ 50,
+ 'code_mode',
+ );
+ assert.ok(commit);
+ await store.commitToolOutcome(commit);
+
+ assert.equal((await store.readToolOperation('outer-op'))?.currentState, 'outcome_committed');
+ const response = (await store.readImmutableRuntimeEvents('session-1', 'run-1')).at(-1);
+ assert.equal(response?.content?.kind, 'function_response');
+ assert.equal(
+ response?.content?.kind === 'function_response'
+ ? response.content.modelProjection?.version
+ : undefined,
+ 1,
+ );
+ } finally {
+ store.close();
+ await rm(root, { recursive: true, force: true });
+ }
});
it('does not infer Code Mode recovery from a custom direct exec name', () => {
@@ -399,6 +477,7 @@ function dispatchFor(input: {
modelVisibility?: 'visible' | 'hidden';
parentOperationId?: string;
parentToolCallId?: string;
+ resultProjectionVersion?: 1;
}): RuntimeEvent {
return event({
id: input.id,
@@ -412,6 +491,9 @@ function dispatchFor(input: {
toolName: input.toolName,
canonicalArgsHash: canonicalToolArgsHash(input.toolName, input.args),
recoveryMode: 'never_auto_retry',
+ ...(input.resultProjectionVersion !== undefined
+ ? { resultProjectionVersion: input.resultProjectionVersion }
+ : {}),
},
},
refs: {
diff --git a/packages/runtime/src/__tests__/session-manager.test.ts b/packages/runtime/src/__tests__/session-manager.test.ts
index 880a44922e..b1b9bb7e12 100644
--- a/packages/runtime/src/__tests__/session-manager.test.ts
+++ b/packages/runtime/src/__tests__/session-manager.test.ts
@@ -28,7 +28,6 @@ import {
} from '@maka/core/sandbox-boundary';
import { createWorkspaceWritePermissionProfile } from '@maka/core/permission-profile';
import { DEEP_RESEARCH_SESSION_LABEL } from '@maka/core/deep-research';
-import { SIDE_CONVERSATION_SESSION_LABEL } from '@maka/core/side-conversation';
import { RUNTIME_CONTINUATION_AUTHORITY_V1 } from '@maka/core/runtime-event-store';
import { deriveTurnRecords } from '@maka/core/session';
import { isSessionInlineRun } from '@maka/core/agent-run';
@@ -48,7 +47,7 @@ import type {
UserMessageInput,
} from '@maka/core/runtime-inputs';
import type { ExecutionBoundary } from '@maka/core/sandbox-boundary';
-import type { SessionEvent, ShellRunSnapshotResult } from '@maka/core/events';
+import type { SessionEvent } from '@maka/core/events';
import type {
AgentGraphIntentClaim,
AgentGraphIntentClaimStore,
@@ -8632,277 +8631,6 @@ describe('SessionManager permission mode updates', () => {
expect(regenUser?.type === 'user' ? regenUser.text : undefined).toBe('aborted turn text');
});
- test('branchFromTurn copies through the RuntimeEvent-primary message boundary', async () => {
- const store = new MemorySessionStore();
- const runStore = new MemoryAgentRunStore();
- const manager = makeManagerForReadCutover(store, runStore);
- const session = await manager.createSession(makeInput({ name: 'Parent' }));
- await seedRuntimeReadTurn({
- store,
- runStore,
- sessionId: session.id,
- turnId: 'source',
- runId: 'source-run',
- userText: 'runtime branch context',
- assistantText: 'runtime branch answer',
- legacyIdPrefix: 'legacy',
- });
- store.failNextReadMessagesFor.set(session.id, 1);
-
- const child = await manager.branchFromTurn(session.id, {
- sourceTurnId: 'source',
- name: 'Child',
- });
-
- const childMessages = await store.readMessages(child.id);
- expect(childMessages[0]).toMatchObject({
- type: 'user',
- turnId: 'source',
- text: 'runtime branch context',
- });
- expect(childMessages[1]).toMatchObject({
- type: 'assistant',
- turnId: 'source',
- text: 'runtime branch answer',
- });
- expect(childMessages[2]).toMatchObject({ type: 'system_note', kind: 'session_start' });
- expect(childMessages.some((message) => message.type === 'turn_state')).toBe(false);
-
- const runtimeMessages = await manager.getMessages(child.id);
- expect(runtimeMessages[0]).toMatchObject({
- type: 'user',
- turnId: 'source',
- text: 'runtime branch context',
- });
- expect(runtimeMessages[1]).toMatchObject({
- type: 'assistant',
- turnId: 'source',
- text: 'runtime branch answer',
- });
- });
-
- test('conversation copies materialize requestless hosted permission history', async () => {
- const store = new MemorySessionStore();
- const runStore = new MemoryAgentRunStore();
- const backends = new BackendRegistry();
- const backendInstances: TestBackend[] = [];
- backends.register('ai-sdk', (ctx) => {
- const backend = new TestBackend(ctx);
- backendInstances.push(backend);
- return backend;
- });
- const session = await store.create(makeInput({ name: 'Parent' }));
- const header = makeRunHeader({
- sessionId: session.id,
- runId: 'source-run',
- turnId: 'source',
- status: 'completed',
- createdAt: 100,
- updatedAt: 130,
- completedAt: 130,
- });
- await seedCanonicalPermissionRun(runStore, header, false);
- const sourcePermissionEvents = await runStore.readRuntimeEvents(session.id, header.runId);
- expect(sourcePermissionEvents.some((event) => event.actions?.permissionRequest)).toBe(false);
- expect(sourcePermissionEvents.some((event) => event.content?.kind === 'function_call')).toBe(
- false,
- );
- await seedRuntimeRun(
- runStore,
- makeRunHeader({
- sessionId: session.id,
- runId: 'later-run',
- turnId: 'later',
- status: 'completed',
- createdAt: 140,
- updatedAt: 141,
- completedAt: 141,
- }),
- [
- runtimeEvent({
- id: 'later-user',
- sessionId: session.id,
- runId: 'later-run',
- turnId: 'later',
- ts: 140,
- role: 'user',
- author: 'user',
- content: { kind: 'text', text: 'replace this turn' },
- }),
- runtimeEvent({
- id: 'later-terminal',
- sessionId: session.id,
- runId: 'later-run',
- turnId: 'later',
- ts: 141,
- status: 'completed',
- actions: { endInvocation: true },
- }),
- ],
- );
- let sourceInteractionAvailable = true;
- const manager = new SessionManager({
- store,
- runStore,
- runtimeEventStore: runStore,
- backends,
- newId: nextId(),
- now: nextNow(6_755),
- interactionAuthority: testInteractionAuthority(),
- canonicalPermissionOutcomes: {
- readPermissionOutcome: async () =>
- sourceInteractionAvailable ? canonicalPermissionRecord(header) : undefined,
- },
- });
-
- const child = await manager.branchFromTurn(session.id, {
- sourceTurnId: header.turnId,
- name: 'Child',
- });
- const revision = await manager.reviseBeforeTurn(session.id, { sourceTurnId: 'later' });
- sourceInteractionAvailable = false;
-
- const [childSourceRun] = await runStore.listSessionRuns(child.id);
- const childSourceEvents = childSourceRun
- ? await runStore.readRuntimeEvents(child.id, childSourceRun.runId)
- : [];
- expect(
- childSourceEvents.find((event) => event.actions?.permissionDecision)?.actions
- ?.permissionDecision,
- ).toMatchObject({
- requestId: 'request-canonical',
- toolName: 'Write',
- decision: 'deny',
- });
- expect(childSourceEvents.some((event) => event.actions?.permissionRequest)).toBe(false);
- expect(childSourceEvents.some((event) => event.actions?.permissionAnswerAccepted)).toBe(false);
-
- for (const copiedSession of [child, revision]) {
- expect(
- (await manager.getMessages(copiedSession.id)).find(
- (message) => message.type === 'permission_decision',
- ),
- ).toMatchObject({
- type: 'permission_decision',
- id: 'request-canonical',
- turnId: header.turnId,
- toolUseId: 'tool-canonical',
- toolName: 'Write',
- decision: 'deny',
- reviewer: 'user',
- });
- }
-
- await drain(manager.sendMessage(child.id, { turnId: 'child-next', text: 'continue' }));
- expect(
- backendInstances
- .find((backend) => backend.sessionId === child.id)
- ?.sendInputs[0]?.context.find((message) => message.type === 'permission_decision'),
- ).toMatchObject({
- id: 'request-canonical',
- toolUseId: 'tool-canonical',
- toolName: 'Write',
- decision: 'deny',
- });
- });
-
- test('branch child next turn receives cloned RuntimeEvent context', async () => {
- const store = new MemorySessionStore();
- const runStore = new MemoryAgentRunStore();
- const backends = new BackendRegistry();
- const backendInstances: TestBackend[] = [];
- backends.register('ai-sdk', (ctx) => {
- const backend = new TestBackend(ctx);
- backendInstances.push(backend);
- return backend;
- });
- const manager = new SessionManager({
- store,
- runStore,
- runtimeEventStore: runStore,
- backends,
- newId: nextId(),
- now: nextNow(6_870),
- });
- const session = await manager.createSession(makeInput({ name: 'Parent' }));
-
- await drain(manager.sendMessage(session.id, { turnId: 'source', text: 'branch seed' }));
- const child = await manager.branchFromTurn(session.id, {
- sourceTurnId: 'source',
- name: 'Child',
- });
- await store.appendMessage(child.id, {
- type: 'assistant',
- id: 'child-cache-only',
- turnId: 'cache-only',
- ts: 6_999,
- text: 'cache-only child context',
- modelId: 'fake-model',
- });
-
- await drain(manager.sendMessage(child.id, { turnId: 'child-next', text: 'child follow-up' }));
-
- const childInput = backendInstances[1]?.sendInputs[0];
- if (!childInput) throw new Error('child backend input was not recorded');
- expect(
- childInput.context.some(
- (message) =>
- message.type === 'user' && message.turnId === 'source' && message.text === 'branch seed',
- ),
- ).toBe(true);
- expect(
- childInput.context.some(
- (message) => message.type === 'assistant' && message.id === 'child-cache-only',
- ),
- ).toBe(false);
- expect(childInput.runtimeContext?.map((event) => event.turnId)).toEqual([
- 'source',
- 'source',
- 'source',
- ]);
- expect(childInput.runtimeContext?.[0]?.sessionId).toBe(child.id);
- });
-
- test('branchFromTurn removes an incomplete target when Runtime ledger copy fails', async () => {
- const store = new MemorySessionStore();
- const runStore = new MemoryAgentRunStore({ failRuntimeEventAppendAfter: 5 });
- const manager = makeManagerForReadCutover(store, runStore);
- const session = await manager.createSession(makeInput({ name: 'Parent' }));
- await seedRuntimeReadTurn({
- store,
- runStore,
- sessionId: session.id,
- turnId: 'source',
- runId: 'source-run',
- userText: 'runtime branch context',
- assistantText: 'runtime branch answer',
- legacyIdPrefix: 'legacy',
- });
-
- let branchError: unknown;
- try {
- await manager.branchFromTurn(session.id, { sourceTurnId: 'source', name: 'Child' });
- } catch (error) {
- branchError = error;
- }
- expect(branchError instanceof Error ? branchError.message : String(branchError)).toContain(
- 'runtime event append failed',
- );
-
- const child = (await store.list()).find((summary) => summary.parentSessionId === session.id);
- expect(child).toBeUndefined();
- const childRuns = await runStore.listSessionRuns('session-2');
- for (const run of childRuns) {
- const runtimeEvents = await runStore.readRuntimeEvents('session-2', run.runId);
- const hasTerminalFact = runtimeEvents.some(isTerminalRuntimeEvent);
- expect(
- run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled'
- ? hasTerminalFact
- : true,
- ).toBe(true);
- }
- });
-
test('multi-run RuntimeEvent projection preserves retry regenerate and branch lineage on turns', async () => {
const store = new MemorySessionStore();
const runStore = new MemoryAgentRunStore();
@@ -11737,727 +11465,6 @@ describe('SessionManager permission mode updates', () => {
const [turn] = await store.listTurns(session.id);
expect(turn?.errorClass).toBe('app_restarted');
});
-
- test('branchFromTurn marks side conversations without changing ordinary branches', async () => {
- const store = new MemorySessionStore();
- const runStore = new MemoryAgentRunStore();
- const backends = new BackendRegistry();
- backends.register(
- 'ai-sdk',
- (ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]),
- );
- const manager = new SessionManager({
- store,
- runStore,
- runtimeEventStore: runStore,
- backends,
- newId: nextId(),
- now: nextNow(15_500),
- });
- const session = await manager.createSession(makeInput({ name: 'Parent', labels: ['kept'] }));
- await drain(manager.sendMessage(session.id, { turnId: 'source', text: 'context' }));
-
- const ordinary = await manager.branchFromTurn(session.id, {
- sourceTurnId: 'source',
- name: 'Ordinary',
- });
- const side = await manager.branchFromTurn(session.id, {
- sourceTurnId: 'source',
- name: 'Side',
- sideConversation: true,
- });
-
- expect(ordinary.labels).toEqual(['kept']);
- expect(side.labels).toEqual(['kept', SIDE_CONVERSATION_SESSION_LABEL]);
- });
-
- test('conversation copies inherit the authoritative execution boundary', async () => {
- const store = new AtomicBoundaryMemorySessionStore();
- const runStore = new MemoryAgentRunStore();
- const backends = new BackendRegistry();
- backends.register(
- 'ai-sdk',
- (ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]),
- );
- const manager = new SessionManager({
- store,
- runStore,
- runtimeEventStore: runStore,
- backends,
- newId: nextId(),
- now: nextNow(15_050),
- });
- const boundaries: ExecutionBoundary[] = [
- {
- kind: 'managed',
- revision: 4,
- profile: applySandboxBoundaryExpansion(createWorkspaceWritePermissionProfile(), {
- filesystem: {
- entries: [{ path: '/approved/release.txt', access: 'write', scope: 'exact' }],
- },
- }),
- },
- { kind: 'external', revision: 3 },
- ];
-
- for (const [index, boundary] of boundaries.entries()) {
- const source = await manager.createSession(makeInput({ name: `Source ${index}` }));
- await drain(manager.sendMessage(source.id, { turnId: 'first', text: 'keep' }));
- await drain(manager.sendMessage(source.id, { turnId: 'second', text: 'replace' }));
- store.forceBoundary(source.id, boundary);
-
- const branch = await manager.branchFromTurn(source.id, { sourceTurnId: 'first' });
- const revision = await manager.reviseBeforeTurn(source.id, { sourceTurnId: 'second' });
-
- for (const copy of [branch, revision]) {
- expect(await store.readExecutionBoundary(copy.id)).toEqual({
- ...boundary,
- revision: 0,
- });
- }
- }
- });
-
- test('branch and revision sessions rewrite tool operation authority facts', async () => {
- const store = new MemorySessionStore();
- const runStore = new MemoryAgentRunStore();
- const backends = new BackendRegistry();
- const manager = new SessionManager({
- store,
- runStore,
- runtimeEventStore: runStore,
- backends,
- newId: nextId(),
- now: nextNow(15_100),
- });
- const session = await manager.createSession(makeInput({ name: 'Authority source' }));
- const header = await store.readHeader(session.id);
- await seedRuntimeRun(
- runStore,
- makeRunHeader({
- sessionId: session.id,
- runId: 'authority-source-run',
- turnId: 'authority-source-turn',
- status: 'completed',
- cwd: header.cwd,
- createdAt: 1,
- updatedAt: 3,
- completedAt: 3,
- }),
- [
- runtimeEvent({
- id: 'authority-source-user',
- sessionId: session.id,
- invocationId: 'authority-source-invocation',
- runId: 'authority-source-run',
- turnId: 'authority-source-turn',
- ts: 1,
- role: 'user',
- author: 'user',
- content: { kind: 'text', text: 'authority-bearing history' },
- }),
- runtimeEvent({
- id: 'authority-source-dispatch',
- sessionId: session.id,
- invocationId: 'authority-source-invocation',
- runId: 'authority-source-run',
- turnId: 'authority-source-turn',
- ts: 2,
- role: 'system',
- author: 'system',
- actions: {
- toolDispatch: {
- protocol: 't1_after_preflight_v1',
- operationId: 'authority-operation',
- providerToolCallId: 'authority-call',
- toolName: 'Write',
- canonicalArgsHash: `sha256:${'a'.repeat(64)}`,
- recoveryMode: 'reconcile',
- },
- },
- refs: { toolCallId: 'authority-call', operationId: 'authority-operation' },
- }),
- runtimeEvent({
- id: 'authority-source-terminal',
- sessionId: session.id,
- invocationId: 'authority-source-invocation',
- runId: 'authority-source-run',
- turnId: 'authority-source-turn',
- ts: 3,
- status: 'completed',
- actions: { endInvocation: true },
- }),
- ],
- );
- await seedRuntimeRun(
- runStore,
- makeRunHeader({
- sessionId: session.id,
- runId: 'authority-later-run',
- turnId: 'authority-later-turn',
- status: 'completed',
- cwd: header.cwd,
- createdAt: 4,
- updatedAt: 5,
- completedAt: 5,
- }),
- [
- runtimeEvent({
- id: 'authority-later-user',
- sessionId: session.id,
- invocationId: 'authority-later-invocation',
- runId: 'authority-later-run',
- turnId: 'authority-later-turn',
- ts: 4,
- role: 'user',
- author: 'user',
- content: { kind: 'text', text: 'later boundary' },
- }),
- runtimeEvent({
- id: 'authority-later-terminal',
- sessionId: session.id,
- invocationId: 'authority-later-invocation',
- runId: 'authority-later-run',
- turnId: 'authority-later-turn',
- ts: 5,
- status: 'completed',
- actions: { endInvocation: true },
- }),
- ],
- );
-
- const copies = [
- await manager.branchFromTurn(session.id, {
- sourceTurnId: 'authority-source-turn',
- name: 'Tool branch',
- }),
- await manager.branchBeforeTurn(session.id, {
- sourceTurnId: 'authority-later-turn',
- name: 'Tool branch before later turn',
- }),
- await manager.reviseBeforeTurn(session.id, {
- sourceTurnId: 'authority-later-turn',
- }),
- ];
- for (const copy of copies) {
- const [targetRun] = await runStore.listSessionRuns(copy.id);
- assert.ok(targetRun);
- const targetEvents = await runStore.readRuntimeEvents(copy.id, targetRun.runId);
- const dispatch = targetEvents.find((event) => event.actions?.toolDispatch);
- const targetOperationId = dispatch?.actions?.toolDispatch?.operationId;
- assert.ok(targetOperationId);
- assert.notEqual(targetOperationId, 'authority-operation');
- assert.equal(dispatch?.refs?.operationId, targetOperationId);
- assert.equal(targetRun.sessionId, copy.id);
- }
- });
-
- test('fails before creating branch or revision sessions for child continuation authority', async () => {
- const store = new MemorySessionStore();
- const runStore = new MemoryAgentRunStore();
- const manager = new SessionManager({
- store,
- runStore,
- runtimeEventStore: runStore,
- backends: new BackendRegistry(),
- newId: nextId(),
- now: nextNow(15_150),
- });
- const session = await manager.createSession(makeInput({ name: 'Legacy continuation' }));
- const header = await store.readHeader(session.id);
- await seedRuntimeRun(
- runStore,
- makeRunHeader({
- sessionId: session.id,
- runId: 'continuation-parent-run',
- turnId: 'continuation-parent-turn',
- status: 'completed',
- cwd: header.cwd,
- createdAt: 1,
- updatedAt: 2,
- completedAt: 2,
- }),
- [
- runtimeEvent({
- id: 'continuation-parent-user',
- sessionId: session.id,
- invocationId: 'continuation-parent-invocation',
- runId: 'continuation-parent-run',
- turnId: 'continuation-parent-turn',
- ts: 1,
- role: 'user',
- author: 'user',
- content: { kind: 'text', text: 'retain parent and child closure' },
- }),
- runtimeEvent({
- id: 'continuation-parent-terminal',
- sessionId: session.id,
- invocationId: 'continuation-parent-invocation',
- runId: 'continuation-parent-run',
- turnId: 'continuation-parent-turn',
- ts: 2,
- status: 'completed',
- actions: { endInvocation: true },
- }),
- ],
- );
- await seedRuntimeRun(
- runStore,
- makeRunHeader({
- sessionId: session.id,
- runId: 'legacy-continuation-run',
- turnId: 'legacy-child-turn',
- parentRunId: 'continuation-parent-run',
- agentId: 'child-agent',
- status: 'completed',
- cwd: header.cwd,
- continuationSource: {
- sourceInvocationId: 'legacy-source-invocation',
- sourceRunId: 'legacy-source-run',
- sourceTurnId: 'legacy-source-turn',
- sourceRuntimeEventHighWater: 1,
- },
- createdAt: 3,
- updatedAt: 4,
- completedAt: 4,
- }),
- [
- runtimeEvent({
- id: 'legacy-continuation-user',
- sessionId: session.id,
- invocationId: 'legacy-continuation-run',
- runId: 'legacy-continuation-run',
- turnId: 'legacy-child-turn',
- ts: 3,
- role: 'user',
- author: 'user',
- content: { kind: 'text', text: 'legacy continuation history' },
- }),
- runtimeEvent({
- id: 'legacy-continuation-terminal',
- sessionId: session.id,
- invocationId: 'legacy-continuation-run',
- runId: 'legacy-continuation-run',
- turnId: 'legacy-child-turn',
- ts: 4,
- status: 'completed',
- actions: { endInvocation: true },
- }),
- ],
- );
- await seedRuntimeRun(
- runStore,
- makeRunHeader({
- sessionId: session.id,
- runId: 'continuation-later-run',
- turnId: 'continuation-later-turn',
- status: 'completed',
- cwd: header.cwd,
- createdAt: 5,
- updatedAt: 6,
- completedAt: 6,
- }),
- [
- runtimeEvent({
- id: 'continuation-later-user',
- sessionId: session.id,
- invocationId: 'continuation-later-invocation',
- runId: 'continuation-later-run',
- turnId: 'continuation-later-turn',
- ts: 5,
- role: 'user',
- author: 'user',
- content: { kind: 'text', text: 'later boundary' },
- }),
- runtimeEvent({
- id: 'continuation-later-terminal',
- sessionId: session.id,
- invocationId: 'continuation-later-invocation',
- runId: 'continuation-later-run',
- turnId: 'continuation-later-turn',
- ts: 6,
- status: 'completed',
- actions: { endInvocation: true },
- }),
- ],
- );
-
- const attempts = [
- () =>
- manager.branchFromTurn(session.id, {
- sourceTurnId: 'continuation-parent-turn',
- name: 'Blocked child continuation branch',
- }),
- () =>
- manager.branchBeforeTurn(session.id, {
- sourceTurnId: 'continuation-later-turn',
- name: 'Blocked child continuation branch before',
- }),
- () =>
- manager.reviseBeforeTurn(session.id, {
- sourceTurnId: 'continuation-later-turn',
- }),
- ];
- for (const attempt of attempts) {
- await expectRejects(attempt(), /typed identity rewriting/i);
- }
- expect((await store.list()).map((candidate) => candidate.id)).toEqual([session.id]);
- });
-
- test('hydrates an inherited running ShellRun with its source-session owner', async () => {
- const store = new MemorySessionStore();
- const runStore = new MemoryAgentRunStore();
- const backends = new BackendRegistry();
- const ref = 'maka://runtime/background-tasks/pty-parent';
- const sourceSnapshot: ShellRunSnapshotResult = {
- kind: 'shell_run',
- ref,
- mode: 'pty',
- status: 'running',
- cwd: '/tmp/workspace',
- cmd: 'interactive',
- startedAt: 1,
- updatedAt: 2,
- revision: 2,
- output: {
- mode: 'pty',
- screen: 'ready',
- scrollback: '',
- cols: 80,
- rows: 24,
- cursor: { x: 5, y: 0, visible: true },
- alternateScreen: false,
- truncated: false,
- redacted: false,
- },
- };
- let ownerAvailable = true;
- const shellRuns = {
- async listSessionUpdates() {
- return [];
- },
- async getSessionUpdate() {
- return undefined;
- },
- async inspectResource(sessionId: string, candidateRef: string) {
- if (ownerAvailable && candidateRef === ref && sessionId === 'session-1')
- return sourceSnapshot;
- const error = new Error('missing') as NodeJS.ErrnoException;
- error.code = 'ENOENT';
- throw error;
- },
- } as unknown as ShellRunProcessManager;
- const manager = new SessionManager({
- store,
- runStore,
- runtimeEventStore: runStore,
- backends,
- shellRuns,
- newId: nextId(),
- now: nextNow(15_250),
- });
- const parent = await manager.createSession(makeInput({ name: 'Parent' }));
- await seedRuntimeRun(
- runStore,
- makeRunHeader({
- sessionId: parent.id,
- runId: 'source-run',
- turnId: 'source',
- status: 'completed',
- createdAt: 1,
- updatedAt: 4,
- completedAt: 4,
- }),
- [
- runtimeEvent({
- id: 'user-1',
- sessionId: parent.id,
- runId: 'source-run',
- turnId: 'source',
- ts: 1,
- role: 'user',
- author: 'user',
- content: { kind: 'text', text: 'start' },
- }),
- runtimeEvent({
- id: 'call-1',
- sessionId: parent.id,
- runId: 'source-run',
- turnId: 'source',
- ts: 2,
- role: 'model',
- author: 'agent',
- content: {
- kind: 'function_call',
- id: 'bash-1',
- name: 'Bash',
- args: { command: 'interactive', run_in_background: true, pty: true },
- },
- refs: { toolCallId: 'bash-1' },
- }),
- runtimeEvent({
- id: 'result-1',
- sessionId: parent.id,
- runId: 'source-run',
- turnId: 'source',
- ts: 3,
- role: 'tool',
- author: 'tool',
- content: {
- kind: 'function_response',
- id: 'bash-1',
- name: 'Bash',
- result: {
- kind: 'shell_run',
- ref,
- mode: 'pty',
- status: 'running',
- cwd: '/tmp/workspace',
- cmd: 'interactive',
- startedAt: 1,
- updatedAt: 1,
- revision: 1,
- },
- isError: false,
- },
- refs: { toolCallId: 'bash-1' },
- }),
- runtimeEvent({
- id: 'complete-1',
- sessionId: parent.id,
- runId: 'source-run',
- turnId: 'source',
- ts: 4,
- role: 'system',
- author: 'system',
- status: 'completed',
- actions: { endInvocation: true },
- }),
- ],
- );
- await seedRuntimeRun(
- runStore,
- makeRunHeader({
- sessionId: parent.id,
- runId: 'later-run',
- turnId: 'later',
- status: 'completed',
- createdAt: 5,
- updatedAt: 6,
- completedAt: 6,
- }),
- [
- runtimeEvent({
- id: 'later-user',
- sessionId: parent.id,
- runId: 'later-run',
- turnId: 'later',
- ts: 5,
- role: 'user',
- author: 'user',
- content: { kind: 'text', text: 'later' },
- }),
- runtimeEvent({
- id: 'later-complete',
- sessionId: parent.id,
- runId: 'later-run',
- turnId: 'later',
- ts: 6,
- role: 'system',
- author: 'system',
- status: 'completed',
- actions: { endInvocation: true },
- }),
- ],
- );
- const child = await manager.branchFromTurn(parent.id, {
- sourceTurnId: 'source',
- name: 'Child',
- });
- const revision = await manager.reviseBeforeTurn(parent.id, { sourceTurnId: 'later' });
-
- const updates = await manager.listShellRunUpdates(child.id);
-
- expect(updates).toHaveLength(1);
- expect(updates[0]?.sessionId).toBe(child.id);
- expect(updates[0]?.ownership).toEqual({
- kind: 'source_owned',
- sourceSessionId: parent.id,
- ownerSessionId: parent.id,
- });
- expect(updates[0]?.sourceToolCallId).toBe('bash-1');
- expect(updates[0]?.result).toEqual(sourceSnapshot);
- expect(await manager.getShellRunUpdate(child.id, sourceSnapshot.ref)).toEqual(updates[0]);
- expect(
- await manager.getShellRunUpdate(child.id, 'maka://runtime/background-tasks/missing-shell'),
- ).toBeNull();
-
- const revisionUpdates = await manager.listShellRunUpdates(revision.id);
- expect(revisionUpdates).toHaveLength(1);
- expect(revisionUpdates[0]?.ownership).toEqual({
- kind: 'source_owned',
- sourceSessionId: parent.id,
- ownerSessionId: parent.id,
- });
- expect(revisionUpdates[0]?.result).toEqual(sourceSnapshot);
-
- ownerAvailable = false;
- await store.remove(parent.id);
- const danglingUpdates = await manager.listShellRunUpdates(child.id);
- expect(danglingUpdates).toHaveLength(1);
- expect(danglingUpdates[0]?.sessionId).toBe(child.id);
- expect(danglingUpdates[0]?.ownership).toEqual({
- kind: 'source_unavailable',
- sourceSessionId: parent.id,
- });
- expect(danglingUpdates[0]?.result.status).toBe('running');
- expect(danglingUpdates[0]?.result.output).toBe(undefined);
- expect(await manager.getShellRunUpdate(child.id, sourceSnapshot.ref)).toEqual(
- danglingUpdates[0],
- );
- });
-
- test('reviseBeforeTurn creates an in-conversation version without ordinary branch lineage', async () => {
- const store = new MemorySessionStore();
- const runStore = new MemoryAgentRunStore();
- const backends = new BackendRegistry();
- backends.register(
- 'ai-sdk',
- (ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]),
- );
- const manager = new SessionManager({
- store,
- runStore,
- runtimeEventStore: runStore,
- backends,
- newId: nextId(),
- now: nextNow(16_700),
- });
- const session = await manager.createSession(
- makeInput({
- name: 'Conversation',
- projectId: 'project-1',
- collaborationMode: 'plan',
- orchestrationMode: 'swarm',
- }),
- );
- await manager.setFlagged(session.id, true);
- await drain(manager.sendMessage(session.id, { turnId: 'first', text: 'keep me' }));
- await drain(manager.sendMessage(session.id, { turnId: 'second', text: 'replace me' }));
-
- const version2 = await manager.reviseBeforeTurn(session.id, { sourceTurnId: 'second' });
-
- expect(version2.name).toBe('Conversation');
- expect(version2.projectId).toBe('project-1');
- expect(version2.isFlagged).toBe(true);
- expect(version2.collaborationMode).toBe('plan');
- expect(version2.orchestrationMode).toBe('swarm');
- expect(version2.parentSessionId).toBeUndefined();
- expect(version2.branchOfTurnId).toBeUndefined();
- expect(version2.revisionRootSessionId).toBe(session.id);
- expect(version2.revisionParentSessionId).toBe(session.id);
- expect(version2.revisionOfTurnId).toBe('second');
- expect(version2.revisionIndex).toBe(2);
- expect(version2.revisionState).toBe('preparing');
- expect((await store.readHeader(version2.id)).collaborationMode).toBe('plan');
- expect((await store.readHeader(version2.id)).orchestrationMode).toBe('swarm');
- await drain(
- manager.sendMessage(
- version2.id,
- { turnId: 'edited-second', text: 'replacement' },
- {
- onRunStarted: async () => {
- await manager.commitRevisionVersion(version2.id);
- },
- },
- ),
- );
- expect((await store.readHeader(version2.id)).revisionState).toBe('committed');
- const messages = await store.readMessages(version2.id);
- expect(messages.some((message) => (message as { turnId?: string }).turnId === 'first')).toBe(
- true,
- );
- expect(messages.some((message) => (message as { turnId?: string }).turnId === 'second')).toBe(
- false,
- );
-
- const version3 = await manager.reviseBeforeTurn(version2.id, { sourceTurnId: 'first' });
- expect(version3.projectId).toBe('project-1');
- expect(version3.revisionRootSessionId).toBe(session.id);
- expect(version3.revisionParentSessionId).toBe(version2.id);
- expect(version3.revisionIndex).toBe(3);
- expect(version3.revisionState).toBe('preparing');
- expect(version3.parentSessionId).toBeUndefined();
- });
-
- test('startup recovery removes empty preparing revisions and commits admitted edits', async () => {
- const store = new MemorySessionStore();
- const runStore = new MemoryAgentRunStore();
- const backends = new BackendRegistry();
- backends.register(
- 'ai-sdk',
- (ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]),
- );
- const manager = new SessionManager({
- store,
- runStore,
- runtimeEventStore: runStore,
- backends,
- newId: nextId(),
- now: nextNow(16_900),
- });
- const root = await manager.createSession(makeInput({ name: 'Recovery root' }));
- await drain(manager.sendMessage(root.id, { turnId: 'first', text: 'original' }));
-
- const empty = await manager.reviseBeforeTurn(root.id, { sourceTurnId: 'first' });
- expect(empty.revisionState).toBe('preparing');
- await manager.recoverInterruptedSessions();
- let removedError: unknown;
- try {
- await store.readHeader(empty.id);
- } catch (error) {
- removedError = error;
- }
- expect(removedError instanceof Error ? removedError.message : String(removedError)).toContain(
- 'Unknown session',
- );
-
- const admitted = await manager.reviseBeforeTurn(root.id, { sourceTurnId: 'first' });
- await drain(manager.sendMessage(admitted.id, { turnId: 'edited', text: 'edited prompt' }));
- expect((await store.readHeader(admitted.id)).revisionState).toBe('preparing');
- await manager.recoverInterruptedSessions();
- expect((await store.readHeader(admitted.id)).revisionState).toBe('committed');
- });
-
- test('branchBeforeTurn rejects an unknown turn', async () => {
- const store = new MemorySessionStore();
- const runStore = new MemoryAgentRunStore();
- const backends = new BackendRegistry();
- backends.register(
- 'ai-sdk',
- (ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]),
- );
- const manager = new SessionManager({
- store,
- runStore,
- runtimeEventStore: runStore,
- backends,
- newId: nextId(),
- now: nextNow(16_800),
- });
- const session = await manager.createSession(makeInput({ name: 'Parent' }));
- await drain(manager.sendMessage(session.id, { turnId: 'only', text: 'the one prompt' }));
-
- let branchError: unknown;
- try {
- await manager.branchBeforeTurn(session.id, { sourceTurnId: 'nope' });
- } catch (error) {
- branchError = error;
- }
- expect(branchError instanceof Error ? branchError.message : String(branchError)).toContain(
- 'Cannot branch before unknown turn',
- );
- });
});
async function drainAll(iterable: AsyncIterable<SessionEvent>): Promise<SessionEvent[]> {
diff --git a/packages/runtime/src/__tests__/tool-runtime-durable-boundary.test.ts b/packages/runtime/src/__tests__/tool-runtime-durable-boundary.test.ts
index e6a56f7c66..c6f52e935f 100644
--- a/packages/runtime/src/__tests__/tool-runtime-durable-boundary.test.ts
+++ b/packages/runtime/src/__tests__/tool-runtime-durable-boundary.test.ts
@@ -24,6 +24,7 @@ import type { LlmConnection } from '@maka/core/llm-connections';
import type { SessionEvent } from '@maka/core/events';
import type { SessionHeader, StoredMessage } from '@maka/core/session';
import { ToolOutcomeUnknownError } from '@maka/core/events';
+import type { RuntimeEvent } from '@maka/core/runtime-event';
import type {
RuntimeCommitSink,
ToolOutcomeCommit,
@@ -59,17 +60,11 @@ describe('ToolRuntime durable boundary', () => {
);
assert.equal(implementationCalls, 0);
- assert.equal(
- harness.events.some((event) => event.type === 'tool_result'),
- false,
- );
- assert.equal(
- harness.messages.some((message) => message.type === 'tool_result'),
- false,
- );
+ assert.deepEqual(harness.events, []);
+ assert.deepEqual(harness.messages, []);
});
- it('does not invoke a tool when another local dispatcher already owns its operation', async () => {
+ it('publishes no call side effects when another dispatcher owns the operation', async () => {
let implementationCalls = 0;
const harness = makeHarness({
commitToolPrepared: async () => ({ created: false, runtimeEventSeq: 1 }),
@@ -89,14 +84,8 @@ describe('ToolRuntime durable boundary', () => {
);
assert.equal(implementationCalls, 0);
- assert.deepEqual(
- harness.events.map((event) => event.type),
- ['tool_start'],
- );
- assert.deepEqual(
- harness.messages.map((message) => message.type),
- ['tool_call'],
- );
+ assert.deepEqual(harness.events, []);
+ assert.deepEqual(harness.messages, []);
});
it('refuses durable tool execution when the turn carries no run id', async () => {
@@ -194,6 +183,10 @@ describe('ToolRuntime durable boundary', () => {
prepared[0]?.dispatchRuntimeEvent.actions?.toolDispatch?.protocol,
't1_after_preflight_v1',
);
+ assert.equal(
+ prepared[0]?.dispatchRuntimeEvent.actions?.toolDispatch?.resultProjectionVersion,
+ 1,
+ );
assert.equal(prepared[0]?.dispatchRuntimeEvent.content, undefined);
assert.equal(outcomes[0]?.runtimeEvent.content?.kind, 'function_response');
assert.equal(prepared[0]?.operationId, outcomes[0]?.operationId);
@@ -202,6 +195,162 @@ describe('ToolRuntime durable boundary', () => {
assert.equal(outcomes[0]?.runtimeEvent.refs?.operationId, prepared[0]?.operationId);
});
+ it('commits the completed outcome with its model projection in T2', async () => {
+ const order: string[] = [];
+ const outcomes: ToolOutcomeCommit[] = [];
+ const harness = makeHarness({
+ commitToolPrepared: async () => ({ created: true, runtimeEventSeq: 1 }),
+ commitToolOutcome: async (input) => {
+ outcomes.push(input);
+ order.push('t2');
+ return { created: true, runtimeEventSeq: 2 };
+ },
+ });
+ const projectedTool = tool(() => ({ private: 'raw execution fact' }));
+ projectedTool.toModelOutput = () => {
+ order.push('project');
+ return { type: 'text', value: 'bounded model fact' };
+ };
+
+ await harness.execute(projectedTool);
+
+ assert.deepEqual(order, ['project', 't2']);
+ const response = outcomes[0]?.runtimeEvent.content;
+ assert.deepEqual(
+ response?.kind === 'function_response' ? response.modelProjection : undefined,
+ {
+ version: 1,
+ kind: 'text',
+ text: 'bounded model fact',
+ },
+ );
+ });
+
+ it('commits one deterministic fallback when projection fails', async () => {
+ let implementationCalls = 0;
+ const outcomes: ToolOutcomeCommit[] = [];
+ const harness = makeHarness({
+ commitToolPrepared: async () => ({ created: true, runtimeEventSeq: 1 }),
+ commitToolOutcome: async (input) => {
+ outcomes.push(input);
+ return { created: true, runtimeEventSeq: 2 };
+ },
+ });
+ const unprojectableTool = tool(() => {
+ implementationCalls += 1;
+ return { private: 'completed execution fact' };
+ });
+ unprojectableTool.toModelOutput = () => {
+ throw new Error('projection implementation failed');
+ };
+
+ assert.deepEqual(await harness.execute(unprojectableTool), {
+ private: 'completed execution fact',
+ });
+
+ assert.equal(implementationCalls, 1);
+ assert.equal(outcomes.length, 1);
+ const response = outcomes[0]?.runtimeEvent.content;
+ assert.deepEqual(
+ response?.kind === 'function_response' ? response.modelProjection : undefined,
+ {
+ version: 1,
+ kind: 'failure',
+ reason: 'projection_failed',
+ message: 'The tool completed, but its model-visible result could not be projected safely.',
+ },
+ );
+ });
+
+ it('commits fallback instead of awaiting an asynchronous projector', {
+ timeout: 1_000,
+ }, async () => {
+ const outcomes: ToolOutcomeCommit[] = [];
+ const harness = makeHarness({
+ commitToolPrepared: async () => ({ created: true, runtimeEventSeq: 1 }),
+ commitToolOutcome: async (input) => {
+ outcomes.push(input);
+ return { created: true, runtimeEventSeq: 2 };
+ },
+ });
+ const invalidTool = tool(() => ({ private: 'completed execution fact' }));
+ invalidTool.toModelOutput = (() =>
+ new Promise<never>(() => undefined)) as unknown as NonNullable<MakaTool['toModelOutput']>;
+
+ await harness.execute(invalidTool);
+
+ const response = outcomes[0]?.runtimeEvent.content;
+ assert.deepEqual(
+ response?.kind === 'function_response' ? response.modelProjection : undefined,
+ {
+ version: 1,
+ kind: 'failure',
+ reason: 'projection_failed',
+ message: 'The tool completed, but its model-visible result could not be projected safely.',
+ },
+ );
+ });
+
+ it('persists inline image output as a Session artifact before committing T2', async () => {
+ const order: string[] = [];
+ const outcomes: ToolOutcomeCommit[] = [];
+ const artifactRef = {
+ kind: 'session_file' as const,
+ sessionId: 'session-1',
+ relativePath: 'artifact-1',
+ };
+ const harness = makeHarness(
+ {
+ commitToolPrepared: async () => ({ created: true, runtimeEventSeq: 1 }),
+ commitToolOutcome: async (input) => {
+ outcomes.push(input);
+ order.push('t2');
+ return { created: true, runtimeEventSeq: 2 };
+ },
+ },
+ undefined,
+ 'run-1',
+ {
+ prepareDurableProjectionArtifact: (input) => {
+ assert.equal(input.turnId, 'turn-1');
+ assert.equal(input.mediaType, 'image/png');
+ assert.deepEqual([...input.bytes], [137, 80, 78, 71]);
+ return {
+ ref: artifactRef,
+ persist: async () => {
+ order.push('artifact');
+ },
+ };
+ },
+ },
+ );
+ const imageTool = tool(() => ({ private: 'raw execution fact' }));
+ imageTool.toModelOutput = () => ({
+ type: 'content',
+ value: [
+ {
+ type: 'file',
+ data: { type: 'data', data: Buffer.from([137, 80, 78, 71]).toString('base64') },
+ mediaType: 'image/png',
+ },
+ ],
+ });
+
+ await harness.execute(imageTool);
+
+ assert.deepEqual(order, ['artifact', 't2']);
+ const response = outcomes[0]?.runtimeEvent.content;
+ assert.deepEqual(
+ response?.kind === 'function_response' ? response.modelProjection : undefined,
+ {
+ version: 1,
+ kind: 'content',
+ parts: [{ kind: 'artifact', mediaType: 'image/png', ref: artifactRef }],
+ },
+ );
+ assert.doesNotMatch(JSON.stringify(response), /iVBORw/);
+ });
+
it('adopts an owner-committed managed successor without invoking generic T2', async () => {
const order: string[] = [];
const prepared: ToolPreparedCommit[] = [];
@@ -1627,6 +1776,9 @@ function managedOutcomeEvent(
isError: boolean,
options: {
durationMs?: number;
+ modelProjection?: NonNullable<
+ Extract<RuntimeEvent['content'], { kind: 'function_response' }>['modelProjection']
+ >;
origin?: 'provider' | 'code_mode';
modelVisibility?: 'visible' | 'hidden';
toolCallId?: string;
@@ -1653,6 +1805,7 @@ function managedOutcomeEvent(
name: 'Write',
result,
...(isError ? { isError: true } : {}),
+ modelProjection: options.modelProjection ?? managedModelProjection(result),
},
refs: {
operationId,
@@ -1664,6 +1817,26 @@ function managedOutcomeEvent(
};
}
+function managedModelProjection(
+ result: unknown,
+): NonNullable<Extract<RuntimeEvent['content'], { kind: 'function_response' }>['modelProjection']> {
+ const raw =
+ result &&
+ typeof result === 'object' &&
+ !Array.isArray(result) &&
+ (result as { kind?: unknown }).kind === 'json'
+ ? (result as { value: unknown }).value
+ : result;
+ const providerError =
+ raw && typeof raw === 'object' && !Array.isArray(raw)
+ ? (raw as { error?: unknown }).error
+ : undefined;
+ if (typeof providerError === 'string') {
+ return { version: 1, kind: 'text', text: `Error: ${providerError}`, isError: true };
+ }
+ return { version: 1, kind: 'json', value: raw as never };
+}
+
function managedMutationDispatch() {
return {
protocol: 'managed_mutation_v2' as const,
diff --git a/packages/runtime/src/__tests__/tool-runtime-sandbox-boundary.test.ts b/packages/runtime/src/__tests__/tool-runtime-sandbox-boundary.test.ts
index df190f08c7..e38ca4ebd9 100644
--- a/packages/runtime/src/__tests__/tool-runtime-sandbox-boundary.test.ts
+++ b/packages/runtime/src/__tests__/tool-runtime-sandbox-boundary.test.ts
@@ -1006,6 +1006,7 @@ describe('ToolRuntime session sandbox boundary', () => {
getPermissionPauseTarget: () => null,
});
+ const events: SessionEvent[] = [];
const settlement = await runtime.settleToolCall({
tool: buildRequestSandboxBoundaryTool() as unknown as MakaTool,
turnId: 'turn-1',
@@ -1016,14 +1017,19 @@ describe('ToolRuntime session sandbox boundary', () => {
},
abortSignal: new AbortController().signal,
eventSink: {
- push: () => {},
- pushAndWaitUntilConsumed: async () => {},
+ push: (event) => events.push(event),
+ pushAndWaitUntilConsumed: async (event) => {
+ events.push(event);
+ },
},
});
- assert.deepEqual(settlement.modelOutput, {
- type: 'error-text',
- value: `Error: ${SANDBOX_BOUNDARY_UNAVAILABLE}`,
+ assert.equal(settlement.providerError, SANDBOX_BOUNDARY_UNAVAILABLE);
+ assert.deepEqual(events.find((event) => event.type === 'tool_result')?.modelProjection, {
+ version: 1,
+ kind: 'text',
+ text: `Error: ${SANDBOX_BOUNDARY_UNAVAILABLE}`,
+ isError: true,
});
});
});
diff --git a/packages/runtime/src/__tests__/tool-runtime-settlement.test.ts b/packages/runtime/src/__tests__/tool-runtime-settlement.test.ts
index 12a34a65c6..8130a76531 100644
--- a/packages/runtime/src/__tests__/tool-runtime-settlement.test.ts
+++ b/packages/runtime/src/__tests__/tool-runtime-settlement.test.ts
@@ -25,6 +25,7 @@ import {
createGenesisExecutionBoundary,
} from '@maka/core/sandbox-boundary';
import { type LlmConnection } from '@maka/core/llm-connections';
+import type { SessionEvent } from '@maka/core/events';
import { type SessionHeader } from '@maka/core/session';
import { buildForegroundBashTool, buildManagedBashTool } from '../shell-tools.js';
import { ToolRuntime, type MakaTool, type ToolRuntimeInput } from '../tool-runtime.js';
@@ -78,8 +79,9 @@ describe('ToolRuntime settlement', () => {
assert.deepEqual(allowed.result, { ok: true });
});
- it('keeps the durable Bash command while omitting it from the live model output', async () => {
+ it('keeps the durable Bash command while omitting it from the durable projection', async () => {
const runtime = makeRuntime();
+ const events: SessionEvent[] = [];
const bash = buildForegroundBashTool({
description: 'shell',
execute: async () => ({
@@ -98,14 +100,17 @@ describe('ToolRuntime settlement', () => {
input: { command: 'printf durable-command' },
abortSignal: new AbortController().signal,
eventSink: {
- push: () => {},
- pushAndWaitUntilConsumed: async () => {},
+ push: (event) => events.push(event),
+ pushAndWaitUntilConsumed: async (event) => {
+ events.push(event);
+ },
},
});
assert.equal((settlement.result as { cmd?: unknown }).cmd, 'printf durable-command');
- assert.deepEqual(settlement.modelOutput, {
- type: 'json',
+ assert.deepEqual(settledProjection(events), {
+ version: 1,
+ kind: 'json',
value: {
kind: 'terminal',
cwd: '/workspace/repo',
@@ -194,6 +199,7 @@ describe('ToolRuntime settlement', () => {
for (const [index, terminal] of terminalResults.entries()) {
const runtime = makeRuntime();
+ const events: SessionEvent[] = [];
const bash = buildManagedBashTool({
runForegroundBash: async () => terminal,
runBackgroundBash: async () => {
@@ -208,20 +214,27 @@ describe('ToolRuntime settlement', () => {
input: { command: terminal.cmd, boundary_intent: 'current' },
abortSignal: new AbortController().signal,
eventSink: {
- push: () => {},
- pushAndWaitUntilConsumed: async () => {},
+ push: (event) => events.push(event),
+ pushAndWaitUntilConsumed: async (event) => {
+ events.push(event);
+ },
},
});
const { cmd: _cmd, ...projected } = terminal;
assert.deepEqual(settlement.result, terminal);
- assert.deepEqual(settlement.modelOutput, { type: 'json', value: projected });
+ assert.deepEqual(settledProjection(events), {
+ version: 1,
+ kind: 'json',
+ value: projected,
+ });
assert.equal(terminalResults[index]?.cmd, terminal.cmd);
}
});
it('preserves live provider error mapping', async () => {
const runtime = makeRuntime();
+ const events: SessionEvent[] = [];
const result = {
error: 'internal detail',
text: 'tool text',
@@ -242,19 +255,27 @@ describe('ToolRuntime settlement', () => {
input: {},
abortSignal: new AbortController().signal,
eventSink: {
- push: () => {},
- pushAndWaitUntilConsumed: async () => {},
+ push: (event) => events.push(event),
+ pushAndWaitUntilConsumed: async (event) => {
+ events.push(event);
+ },
},
});
assert.deepEqual(settlement, {
result,
- modelOutput: { type: 'error-text', value: 'Error: safe model detail' },
+ providerError: 'safe model detail',
+ });
+ assert.deepEqual(settledProjection(events), {
+ version: 1,
+ kind: 'text',
+ text: 'Error: safe model detail',
+ isError: true,
});
});
it('records apply_patch failures without changing their provider output shape', async () => {
- const events: Array<{ type: string; isError?: boolean }> = [];
+ const events: SessionEvent[] = [];
const runtime = makeRuntime();
const settlement = await runtime.settleToolCall({
tool: {
@@ -279,8 +300,10 @@ describe('ToolRuntime settlement', () => {
events.some((event) => event.type === 'tool_result' && event.isError === true),
true,
);
- assert.deepEqual(settlement.modelOutput, {
- type: 'json',
+ assert.deepEqual(settlement.providerError, 'dispatch failed');
+ assert.deepEqual(settledProjection(events), {
+ version: 1,
+ kind: 'json',
value: { status: 'failed', output: 'dispatch failed' },
});
});
@@ -288,8 +311,8 @@ describe('ToolRuntime settlement', () => {
it('falls back from provider text to the raw error message', async () => {
const runtime = makeRuntime();
for (const [result, expected] of [
- [{ error: 'internal detail', text: 'tool text' }, 'Error: tool text'],
- [{ error: 'internal detail' }, 'Error: internal detail'],
+ [{ error: 'internal detail', text: 'tool text' }, 'tool text'],
+ [{ error: 'internal detail' }, 'internal detail'],
] as const) {
const settlement = await runtime.settleToolCall({
tool: tool(() => result),
@@ -304,13 +327,13 @@ describe('ToolRuntime settlement', () => {
},
});
- assert.deepEqual(settlement.modelOutput, { type: 'error-text', value: expected });
+ assert.equal(settlement.providerError, expected);
}
});
it('keeps structured durable failures on the live success arm', async () => {
const runtime = makeRuntime();
- const events: Array<{ type: string; isError?: boolean }> = [];
+ const events: SessionEvent[] = [];
const result = {
kind: 'subagent',
agentName: 'Reviewer',
@@ -340,39 +363,18 @@ describe('ToolRuntime settlement', () => {
events.some((event) => event.type === 'tool_result' && event.isError === true),
true,
);
- assert.deepEqual(settlement.modelOutput, { type: 'json', value: result });
- });
-
- it('uses the runtime model-output materializer for default tool results', async () => {
- const result = { kind: 'image', ref: 'artifact-1' };
- const runtime = makeRuntime({
- materializeDefaultToolResultOutput: async ({ toolCallId, output }) => {
- assert.equal(toolCallId, 'call-1');
- assert.equal(output, result);
- return { type: 'text', value: 'materialized image' };
- },
- });
-
- const settlement = await runtime.settleToolCall({
- tool: tool(() => result),
- turnId: 'turn-1',
- stepId: 'step-1',
- toolCallId: 'call-1',
- input: {},
- abortSignal: new AbortController().signal,
- eventSink: {
- push: () => {},
- pushAndWaitUntilConsumed: async () => {},
- },
+ assert.deepEqual(settledProjection(events), {
+ version: 1,
+ kind: 'json',
+ value: result,
});
-
- assert.deepEqual(settlement.modelOutput, { type: 'text', value: 'materialized image' });
});
it('rejects direct-only nested tools before permission argument projection or implementation', async () => {
let permissionProjectionCalls = 0;
let implementationCalls = 0;
const runtime = makeRuntime();
+ const events: SessionEvent[] = [];
const settlement = await runtime.settleToolCall({
tool: {
@@ -391,8 +393,10 @@ describe('ToolRuntime settlement', () => {
input: {},
abortSignal: new AbortController().signal,
eventSink: {
- push: () => {},
- pushAndWaitUntilConsumed: async () => {},
+ push: (event) => events.push(event),
+ pushAndWaitUntilConsumed: async (event) => {
+ events.push(event);
+ },
},
origin: 'code_mode',
parentToolCallId: 'exec-1',
@@ -400,9 +404,12 @@ describe('ToolRuntime settlement', () => {
assert.equal(permissionProjectionCalls, 0);
assert.equal(implementationCalls, 0);
- assert.deepEqual(settlement.modelOutput, {
- type: 'error-text',
- value: 'Error: Tool Read is direct-only and cannot run inside exec.',
+ assert.equal(settlement.providerError, 'Tool Read is direct-only and cannot run inside exec.');
+ assert.deepEqual(settledProjection(events), {
+ version: 1,
+ kind: 'text',
+ text: 'Error: Tool Read is direct-only and cannot run inside exec.',
+ isError: true,
});
});
@@ -527,10 +534,7 @@ describe('ToolRuntime settlement', () => {
function makeRuntime(
overrides: Partial<
- Pick<
- ToolRuntimeInput,
- 'materializeDefaultToolResultOutput' | 'readExecutionBoundary' | 'spawnChildSession' | 'runId'
- >
+ Pick<ToolRuntimeInput, 'readExecutionBoundary' | 'spawnChildSession' | 'runId'>
> = {},
): ToolRuntime {
return createTestToolRuntime({
@@ -546,6 +550,10 @@ function makeRuntime(
});
}
+function settledProjection(events: readonly SessionEvent[]) {
+ return events.find((event) => event.type === 'tool_result')?.modelProjection;
+}
+
function tool(impl: MakaTool['impl']): MakaTool {
return {
name: 'Read',
diff --git a/packages/runtime/src/__tests__/tool-runtime-sqlite-boundary.test.ts b/packages/runtime/src/__tests__/tool-runtime-sqlite-boundary.test.ts
index 43a9c952f3..e378e8bd86 100644
--- a/packages/runtime/src/__tests__/tool-runtime-sqlite-boundary.test.ts
+++ b/packages/runtime/src/__tests__/tool-runtime-sqlite-boundary.test.ts
@@ -19,6 +19,7 @@
import { createTestToolRuntime } from './execution-boundary-test-helpers.js';
import assert from 'node:assert/strict';
+import { Buffer } from 'node:buffer';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
@@ -26,9 +27,12 @@ import { describe, it } from 'node:test';
import { createGenesisExecutionBoundary } from '@maka/core/sandbox-boundary';
import { type LlmConnection } from '@maka/core/llm-connections';
import { type SessionEvent } from '@maka/core/events';
-import { type SessionHeader } from '@maka/core/session';
+import { type SessionHeader, type StoredMessage } from '@maka/core/session';
import type { McpToolBinding } from '@maka/core/mcp';
-import { createSqliteRuntimeStore } from '@maka/storage/sqlite-runtime-store';
+import {
+ createSqliteRuntimeStore,
+ type SqliteRuntimeStoreFailpoint,
+} from '@maka/storage/sqlite-runtime-store';
import {
createSessionEventMapMemory,
mapSessionEventToRuntimeEvent,
@@ -471,6 +475,16 @@ describe('ToolRuntime with real SQLite boundary', () => {
);
assert.equal((await store.readImmutableRuntimeEvents('session-1', 'run-1')).length, 3);
+ const response = events.find((event) => event.content?.kind === 'function_response');
+ const durableProjection =
+ response?.content?.kind === 'function_response'
+ ? response.content.modelProjection
+ : undefined;
+ assert.deepEqual(durableProjection, {
+ version: 1,
+ kind: 'json',
+ value: { ok: true, text: 'contents' },
+ });
const context = invocationContext();
const memory = createSessionEventMapMemory();
const durableEvents = published.filter(
@@ -482,10 +496,13 @@ describe('ToolRuntime with real SQLite boundary', () => {
);
assert.deepEqual(
mappedEvents,
- events.filter(
- (event) =>
- event.content?.kind === 'function_call' || event.content?.kind === 'function_response',
- ),
+ events
+ .filter(
+ (event) =>
+ event.content?.kind === 'function_call' ||
+ event.content?.kind === 'function_response',
+ )
+ .map((event) => JSON.parse(JSON.stringify(event))),
);
assert.equal((await store.readRuntimeEvents('session-1', 'run-1')).length, 3);
@@ -496,6 +513,113 @@ describe('ToolRuntime with real SQLite boundary', () => {
}
});
+ it('does not repeat tool or projection side effects after an atomic T2 failure', async () => {
+ const root = await mkdtemp(join(tmpdir(), 'maka-tool-sqlite-t2-retry-'));
+ let runtimeEventInsertions = 0;
+ let failT2 = true;
+ const store = createSqliteRuntimeStore(join(root, 'runtime.sqlite'), {
+ failpoint: (point) => {
+ if (
+ point === ('after_runtime_event_insert' satisfies SqliteRuntimeStoreFailpoint) &&
+ failT2 &&
+ ++runtimeEventInsertions === 2
+ ) {
+ throw new Error(`sqlite runtime failpoint: ${point}`);
+ }
+ },
+ });
+ try {
+ let implementationCalls = 0;
+ let artifactWrites = 0;
+ const appendedMessages: StoredMessage[] = [];
+ const runtime = createTestToolRuntime({
+ sessionId: 'session-1',
+ header: header(),
+ connection: connection(),
+ modelId: 'model-1',
+ appendMessage: async (message) => {
+ appendedMessages.push(message);
+ },
+ newId: nextId(),
+ now: nextNow(),
+ getPermissionPauseTarget: () => null,
+ runId: 'run-1',
+ invocationId: 'invocation-1',
+ runtimeCommitSink: store,
+ prepareDurableProjectionArtifact: () => {
+ return {
+ ref: {
+ kind: 'session_file',
+ sessionId: 'session-1',
+ relativePath: 'projection-artifact',
+ },
+ persist: async () => {
+ artifactWrites += 1;
+ },
+ };
+ },
+ });
+ const imageTool: MakaTool = {
+ name: 'Read',
+ description: 'read',
+ parameters: {},
+ recoveryMode: 'replay_safe',
+ impl: async () => {
+ implementationCalls += 1;
+ return { private: 'completed execution fact' };
+ },
+ toModelOutput: () => ({
+ type: 'content',
+ value: [
+ {
+ type: 'file',
+ data: { type: 'data', data: Buffer.from([137, 80, 78, 71]).toString('base64') },
+ mediaType: 'image/png',
+ },
+ ],
+ }),
+ };
+ const published: SessionEvent[] = [];
+ const settle = () =>
+ runtime.settleToolCall({
+ tool: imageTool,
+ turnId: 'turn-1',
+ toolCallId: 'provider-call-1',
+ input: {},
+ abortSignal: new AbortController().signal,
+ eventSink: {
+ push: (event) => published.push(event),
+ pushAndWaitUntilConsumed: async (event) => {
+ published.push(event);
+ },
+ },
+ });
+
+ await assert.rejects(settle(), /sqlite runtime failpoint: after_runtime_event_insert/u);
+ assert.equal(implementationCalls, 1);
+ assert.equal(artifactWrites, 1);
+ failT2 = false;
+ await assert.rejects(settle(), /duplicate_event_id/u);
+
+ assert.equal(implementationCalls, 1);
+ assert.equal(artifactWrites, 1);
+ assert.equal(published.filter((event) => event.type === 'tool_result').length, 0);
+ assert.equal(published.filter((event) => event.type === 'tool_start').length, 1);
+ assert.equal(appendedMessages.filter((message) => message.type === 'tool_call').length, 1);
+ const events = await store.readRuntimeEvents('session-1', 'run-1');
+ assert.deepEqual(
+ events.map((event) => event.content?.kind),
+ ['function_call', undefined],
+ );
+ const operationId = events[0]?.refs?.operationId;
+ assert.ok(operationId);
+ assert.equal((await store.readToolOperation(operationId))?.currentState, 'prepared');
+ } finally {
+ store.close();
+ await rm(root, { recursive: true, force: true });
+ }
+ });
+
it('persists the same normalized error event that the Runtime flow later observes', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-tool-sqlite-error-'));
const store = createSqliteRuntimeStore(join(root, 'runtime.sqlite'));
@@ -545,10 +669,13 @@ describe('ToolRuntime with real SQLite boundary', () => {
const events = await store.readRuntimeEvents('session-1', 'run-1');
assert.deepEqual(
mappedEvents,
- events.filter(
- (event) =>
- event.content?.kind === 'function_call' || event.content?.kind === 'function_response',
- ),
+ events
+ .filter(
+ (event) =>
+ event.content?.kind === 'function_call' ||
+ event.content?.kind === 'function_response',
+ )
+ .map((event) => JSON.parse(JSON.stringify(event))),
);
assert.equal(events.length, 3);
assert.equal(events[2]?.content?.kind, 'function_response');
diff --git a/packages/runtime/src/ai-sdk-backend.ts b/packages/runtime/src/ai-sdk-backend.ts
index 2364129d91..b51269200d 100644
--- a/packages/runtime/src/ai-sdk-backend.ts
+++ b/packages/runtime/src/ai-sdk-backend.ts
@@ -194,6 +194,8 @@ import {
} from './ai-sdk-compaction.js';
import type { AiSdkCompactionCapabilities } from './ai-sdk-compaction-contract.js';
import type { ToolArtifactRecorder } from './tool-artifacts.js';
+import { durableProjectionToToolResultOutput } from './durable-tool-result-projection.js';
+import type { DurableToolResultProjection } from '@maka/core/durable-tool-result-projection';
import { openAiChatReasoningFieldFromProviderOptions } from './openai-chat-reasoning-transport.js';
import { RunTrace, type RunTraceRecorder } from './run-trace.js';
import { SandboxCommandError } from './sandbox/errors.js';
@@ -801,6 +803,8 @@ export interface AiSdkBackendInput extends AiSdkCompactionCapabilities {
* Caller wires this to the session ArtifactStore; runtime never imports storage.
*/
readAttachmentBytes?: AttachmentByteReader;
+ /** Host-owned exact ref plan for inline images, persisted only after projection validation. */
+ prepareDurableProjectionArtifact?: ToolRuntimeInput['prepareDurableProjectionArtifact'];
/**
* Whether the selected model accepts image input. Only explicit true sends
* image parts; false/unknown stay as text refs with a fallback note.
@@ -859,6 +863,37 @@ function nativeApplyPatchFailureOutput(output: ToolResultOutput): ToolResultOutp
};
}
+function durableApplyPatchReplayFactText(
+ input: unknown,
+ projection: DurableToolResultProjection,
+ isError: boolean,
+): string | null {
+ if (projection.kind === 'json') {
+ const fact = applyPatchReplayFactText(input, projection, isError);
+ if (fact) return fact;
+ }
+ const output = durableProjectionToToolResultOutput(projection);
+ switch (output.type) {
+ case 'text':
+ case 'error-text':
+ return output.value;
+ case 'json':
+ case 'error-json':
+ return JSON.stringify(output.value);
+ case 'content': {
+ const text = output.value
+ .filter((part): part is Extract<typeof part, { type: 'text' }> => part.type === 'text')
+ .map((part) => part.text)
+ .join('\n');
+ return text || null;
+ }
+ case 'execution-denied':
+ return output.reason
+ ? `ApplyPatch execution denied: ${output.reason}`
+ : 'ApplyPatch execution denied.';
+ }
+}
+
/**
* One Code Mode cell runs at a time on a backend, with one allowed to wait.
* Widening either needs evidence that concurrent cells are wanted; none exists
@@ -1106,7 +1141,7 @@ export class AiSdkBackend implements AgentBackend {
createProviderRequestTracker: (trackerInput) =>
this.createProviderRequestTracker(trackerInput),
materializeRuntimeReplayPlan: (plan, imageBudget, checkpoint) =>
- this.materializeRuntimeReplayPlan(plan, imageBudget, undefined, checkpoint),
+ this.materializeRuntimeReplayPlan(plan, imageBudget, checkpoint),
canReplayProviderNative: (plan) => this.canReplayProviderNative(plan),
});
if (
@@ -1323,8 +1358,7 @@ export class AiSdkBackend implements AgentBackend {
...(identity.runId ? { runId: identity.runId } : {}),
orchestrationMode: identity.orchestrationMode,
...(identity.invocationId ? { invocationId: identity.invocationId } : {}),
- materializeDefaultToolResultOutput: ({ toolCallId, output }) =>
- this.materializeToolResultOutput(identity.scope().imageBudget, output, false, toolCallId),
+ prepareDurableProjectionArtifact: input.prepareDurableProjectionArtifact,
spawnChildSession: input.spawnChildSession,
listChildAgents: input.listChildAgents,
readChildAgentOutput: input.readChildAgentOutput,
@@ -1854,7 +1888,6 @@ export class AiSdkBackend implements AgentBackend {
content: currentUserContent,
} as ModelMessage,
];
- const settledModelOutputs = new Map<string, ToolResultOutput>();
const loadDurableTurnEvents = async (): Promise<RuntimeEvent[]> => {
const loadTurnRuntimeEvents = this.input.loadTurnRuntimeEvents;
if (!loadTurnRuntimeEvents) {
@@ -1906,7 +1939,6 @@ export class AiSdkBackend implements AgentBackend {
const currentTurnMessages = await this.materializeRuntimeReplayPlan(
replayPlan,
scope.imageBudget,
- settledModelOutputs,
projectionCheckpoint,
);
return projectionCheckpoint
@@ -2675,13 +2707,6 @@ export class AiSdkBackend implements AgentBackend {
}
}
await queue.waitUntilConsumedThroughCurrent();
- for (let index = 0; index < returnedToolCalls.length; index += 1) {
- const toolCall = returnedToolCalls[index];
- const settlement = settlements[index];
- if (toolCall && settlement) {
- settledModelOutputs.set(toolCall.toolCallId, settlement.modelOutput);
- }
- }
const continuationWillRun =
(maxSteps === undefined || runtimeSteps < maxSteps) &&
@@ -3104,7 +3129,7 @@ export class AiSdkBackend implements AgentBackend {
const tool = snapshot.get(name);
if (!tool) throw new Error(`Tool "${name}" is not active or nestable in this cell`);
const parsedInput = await validateCodeModeToolInput(tool, input);
- const settlement = await scope.toolRuntime.settleToolCallRaw({
+ const settlement = await scope.toolRuntime.settleToolCall({
tool,
turnId: context.turnId,
toolCallId: `${context.toolCallId}:nested:${this.newId()}`,
@@ -3615,7 +3640,6 @@ export class AiSdkBackend implements AgentBackend {
messages: await this.materializeRuntimeReplayPlan(
plan,
scope.imageBudget,
- undefined,
projectedHistoryCompactCheckpoint,
),
gate: 'runtime_replay_text_only',
@@ -3639,7 +3663,6 @@ export class AiSdkBackend implements AgentBackend {
? await this.materializeRuntimeReplayPlan(
degradedPlan,
scope.imageBudget,
- undefined,
projectedHistoryCompactCheckpoint,
)
: await materializeReplayFallback(),
@@ -3658,7 +3681,6 @@ export class AiSdkBackend implements AgentBackend {
messages: await this.materializeRuntimeReplayPlan(
plan,
scope.imageBudget,
- undefined,
projectedHistoryCompactCheckpoint,
),
gate: 'runtime_replay_provider_native',
@@ -3730,7 +3752,6 @@ export class AiSdkBackend implements AgentBackend {
private async materializeRuntimeReplayPlan(
plan: RuntimeEventModelReplayPlan,
budget: ProviderImageBudget,
- settledModelOutputs?: ReadonlyMap<string, ToolResultOutput>,
historyCompactCheckpoint?: HistoryCompactCheckpoint,
): Promise<ModelMessage[]> {
type ToolCallItem = Extract<RuntimeEventModelReplayItem, { kind: 'tool_call' }>;
@@ -3854,14 +3875,18 @@ export class AiSdkBackend implements AgentBackend {
result: ToolResultItem,
toolName: string,
): Promise<ToolResultOutput> => {
- const output =
- settledModelOutputs?.get(result.toolCallId) ??
- (await this.materializeToolResultOutput(
- budget,
- result.output,
- result.isError,
- `runtime-event:${result.eventId}:tool-result`,
- ));
+ const output = result.modelProjection
+ ? await this.materializeDurableToolResultProjection(
+ budget,
+ result.modelProjection,
+ `runtime-event:${result.eventId}:tool-result`,
+ )
+ : await this.materializeToolResultOutput(
+ budget,
+ result.output,
+ result.isError,
+ `runtime-event:${result.eventId}:tool-result`,
+ );
if (toolName !== 'apply_patch') return output;
return result.isError ? nativeApplyPatchFailureOutput(output) : output;
};
@@ -4053,11 +4078,13 @@ export class AiSdkBackend implements AgentBackend {
break;
}
downgradedApplyPatchCalls.delete(item.toolCallId);
- const replayFact = applyPatchReplayFactText(
- downgradedCall.input,
- item.output,
- item.isError,
- );
+ const replayFact = item.modelProjection
+ ? durableApplyPatchReplayFactText(
+ downgradedCall.input,
+ item.modelProjection,
+ item.isError,
+ )
+ : applyPatchReplayFactText(downgradedCall.input, item.output, item.isError);
if (!replayFact) break;
if (downgradedCall.stepId) {
const stepFacts = replayFactsByStep.get(downgradedCall.stepId) ?? [];
@@ -4413,6 +4440,62 @@ export class AiSdkBackend implements AgentBackend {
};
}
+ private async materializeDurableToolResultProjection(
+ budget: ProviderImageBudget,
+ projection: DurableToolResultProjection,
+ decisionKey: string,
+ ): Promise<ToolResultOutput> {
+ if (projection.kind !== 'content') return durableProjectionToToolResultOutput(projection);
+ const value: Extract<ToolResultOutput, { type: 'content' }>['value'] = [];
+ for (const [index, part] of projection.parts.entries()) {
+ if (part.kind === 'text') {
+ value.push({ type: 'text', text: part.text });
+ continue;
+ }
+ if (this.input.supportsVision !== true) {
+ value.push({
+ type: 'text',
+ text: 'Image was read, but the selected model does not support image input.',
+ });
+ continue;
+ }
+ if (!this.input.readAttachmentBytes) {
+ value.push({
+ type: 'text',
+ text: 'Image was read, but its stored bytes are unavailable.',
+ });
+ continue;
+ }
+ let read: Awaited<ReturnType<AttachmentByteReader>>;
+ try {
+ read = await this.input.readAttachmentBytes(part.ref);
+ } catch {
+ value.push({
+ type: 'text',
+ text: 'Image could not be loaded from artifact storage: read_failed.',
+ });
+ continue;
+ }
+ if (!read.ok) {
+ value.push({
+ type: 'text',
+ text: `Image could not be loaded from artifact storage: ${read.reason}.`,
+ });
+ continue;
+ }
+ if (!this.chargeImageBudget(budget, read.bytes.length, `${decisionKey}:artifact:${index}`)) {
+ value.push({ type: 'text', text: PROVIDER_IMAGE_BUDGET_EXCEEDED_MESSAGE });
+ continue;
+ }
+ value.push({
+ type: 'file',
+ data: { type: 'data', data: Buffer.from(read.bytes).toString('base64') },
+ mediaType: part.mediaType,
+ });
+ }
+ return { type: 'content', value };
+ }
+
private async buildCurrentUserContent(
budget: ProviderImageBudget,
text: string,
diff --git a/packages/runtime/src/conversation-copy.ts b/packages/runtime/src/conversation-copy.ts
index 6a4054f81c..41d3c72618 100644
--- a/packages/runtime/src/conversation-copy.ts
+++ b/packages/runtime/src/conversation-copy.ts
@@ -58,6 +58,7 @@ import {
isArchivedToolResultPlaceholder,
type ArchivedToolResultPlaceholder,
} from './tool-result-archive.js';
+import { rewriteDurableToolResultProjectionArtifactRefs } from './durable-tool-result-projection.js';
export interface ConversationCopySlice {
readonly messages: readonly StoredMessage[];
@@ -1161,6 +1162,14 @@ function rewriteRuntimeEventReferences(
? {
...event.content,
result: rewriteRuntimeToolResult(event.content.result, references),
+ ...(event.content.modelProjection
+ ? {
+ modelProjection: rewriteDurableToolResultProjectionArtifactRefs(
+ event.content.modelProjection,
+ (ref) => rewriteProjectionArtifactRef(ref, references),
+ ),
+ }
+ : {}),
}
: event.content;
const refs = event.refs
@@ -1588,6 +1597,17 @@ function rewriteStorageRef(
};
}
+function rewriteProjectionArtifactRef(
+ ref: Extract<StorageRef, { kind: 'session_context' | 'session_file' }>,
+ references: ConversationCopyArtifactReferenceMap,
+): Extract<StorageRef, { kind: 'session_context' | 'session_file' }> {
+ const rewritten = rewriteStorageRef(ref, references);
+ if (rewritten.kind !== 'session_context' && rewritten.kind !== 'session_file') {
+ throw new Error('Conversation copy produced an invalid projection Artifact reference');
+ }
+ return rewritten;
+}
+
function rewriteArchivedToolResult(
value: ArchivedToolResultPlaceholder,
references: ConversationCopyMessageReferenceMap,
diff --git a/packages/runtime/src/durable-tool-result-projection.ts b/packages/runtime/src/durable-tool-result-projection.ts
new file mode 100644
index 0000000000..f81e4d5f82
--- /dev/null
+++ b/packages/runtime/src/durable-tool-result-projection.ts
@@ -0,0 +1,478 @@
+/*
+ * 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.
+ */
+
+import {
+ decodeDurableToolResultProjection,
+ DURABLE_TOOL_RESULT_PROJECTION_FAILURE,
+ DURABLE_TOOL_RESULT_PROJECTION_MAX_JSON_DEPTH,
+ DURABLE_TOOL_RESULT_PROJECTION_MAX_JSON_NODES,
+ DURABLE_TOOL_RESULT_PROJECTION_MAX_PARTS,
+ DURABLE_TOOL_RESULT_PROJECTION_VERSION,
+ type DurableProjectionArtifactRef,
+ type DurableProjectionJson,
+ type DurableToolResultProjection,
+ type DurableToolResultProjectionPart,
+} from '@maka/core/durable-tool-result-projection';
+import { MAX_READ_IMAGE_BYTES } from '@maka/core/attachments';
+import {
+ isCanonicalArtifactEntityId,
+ normalizeArtifactImagePreviewMime,
+} from '@maka/core/artifacts';
+import { isCanonicalStorageRef } from '@maka/core/events';
+import type { ToolResultContent } from '@maka/core/events';
+import type { RuntimeEventFunctionResponseContent } from '@maka/core/runtime-event';
+import { decodeCanonicalShellToolResultContent } from '@maka/core/shell-run-result';
+import { markPersisted } from '@maka/core/persisted-value';
+import { decodePersistedToolResultContent } from '@maka/core/tool-result-record-schema';
+
+import type { ToolResultOutput } from './model-protocol.js';
+import { toolResultOutput } from './tool-result-output.js';
+import { withToolResultArchiveResourceRef } from './tool-result-archive.js';
+import { projectBashToolResultForModel } from './bash-model-output.js';
+import { projectFileWriteToolResultForModel } from './file-tool-model-output.js';
+
+const OMITTED_BINARY_TEXT =
+ '[Binary tool output omitted from the durable model projection; repeat the tool call if it is still needed.]';
+
+/**
+ * The only new-write codec from Runtime's tool-output contract into the
+ * provider-neutral durable projection protocol. It is total by construction:
+ * invalid, unsafe, or oversized output becomes one stable failure sentinel.
+ */
+export function encodeDurableToolResultOutput(
+ output: ToolResultOutput,
+ sessionId: string,
+): DurableToolResultProjection {
+ try {
+ const projection = encodeOutput(output, sessionId);
+ return decodeDurableToolResultProjection(projection);
+ } catch {
+ return DURABLE_TOOL_RESULT_PROJECTION_FAILURE;
+ }
+}
+
+interface DurableProjectionArtifactPlan {
+ ref: Extract<DurableProjectionArtifactRef, { kind: 'session_file' }>;
+ persist(): Promise<void>;
+}
+
+type DurableProjectionArtifactPlanner = (input: {
+ bytes: Uint8Array;
+ mediaType: string;
+}) => DurableProjectionArtifactPlan;
+
+export function encodeDurableToolResultOutputWithArtifacts(
+ output: ToolResultOutput,
+ sessionId: string,
+ planArtifact: DurableProjectionArtifactPlanner | undefined,
+): DurableToolResultProjection | PromiseLike<DurableToolResultProjection> {
+ if (!planArtifact || output.type !== 'content' || !hasInlineImage(output)) {
+ return encodeDurableToolResultOutput(output, sessionId);
+ }
+ return (async () => {
+ try {
+ const prepared = prepareContentProjection(output, sessionId, planArtifact);
+ const persisted = new Set<string>();
+ for (const artifact of prepared.artifacts) {
+ if (persisted.has(artifact.ref.relativePath)) continue;
+ await artifact.persist();
+ persisted.add(artifact.ref.relativePath);
+ }
+ return prepared.projection;
+ } catch {
+ return DURABLE_TOOL_RESULT_PROJECTION_FAILURE;
+ }
+ })();
+}
+
+export function encodeDefaultDurableToolResultOutput(
+ result: unknown,
+ sessionId: string,
+): DurableToolResultProjection {
+ const image = sessionImageResult(result, sessionId);
+ if (image) {
+ try {
+ const mediaType = normalizeArtifactImagePreviewMime(image.mimeType);
+ if (!mediaType) throw new Error('Image has an unsafe media type');
+ return decodeDurableToolResultProjection({
+ version: DURABLE_TOOL_RESULT_PROJECTION_VERSION,
+ kind: 'content',
+ parts: [{ kind: 'artifact', mediaType, ref: image.ref }],
+ });
+ } catch {
+ return DURABLE_TOOL_RESULT_PROJECTION_FAILURE;
+ }
+ }
+ return encodeDurableToolResultOutput(
+ typeof result === 'string'
+ ? { type: 'text', value: result }
+ : { type: 'json', value: result as never },
+ sessionId,
+ );
+}
+
+export function durableProjectionToToolResultOutput(
+ projection: DurableToolResultProjection,
+): ToolResultOutput {
+ switch (projection.kind) {
+ case 'text':
+ return projection.isError
+ ? { type: 'error-text', value: projection.text }
+ : { type: 'text', value: projection.text };
+ case 'json':
+ return projection.isError
+ ? { type: 'error-json', value: projection.value }
+ : { type: 'json', value: projection.value };
+ case 'content':
+ return {
+ type: 'content',
+ value: projection.parts.map((part) =>
+ part.kind === 'text'
+ ? { type: 'text' as const, text: part.text }
+ : {
+ type: 'text' as const,
+ text: `[Artifact ${JSON.stringify(
+ part.ref.kind === 'session_context' ? part.ref.refId : part.ref.relativePath,
+ )} (${part.mediaType}) is stored in this Session.]`,
+ },
+ ),
+ };
+ case 'execution_denied':
+ return {
+ type: 'execution-denied',
+ ...(projection.reason !== undefined ? { reason: projection.reason } : {}),
+ };
+ case 'failure':
+ return { type: 'error-text', value: projection.message };
+ }
+}
+
+export function rewriteDurableToolResultProjectionArtifactRefs(
+ projection: DurableToolResultProjection,
+ rewrite: (ref: DurableProjectionArtifactRef) => DurableProjectionArtifactRef,
+): DurableToolResultProjection {
+ if (projection.kind !== 'content') return projection;
+ return {
+ ...projection,
+ parts: projection.parts.map((part) =>
+ part.kind === 'artifact' ? { ...part, ref: rewrite(part.ref) } : part,
+ ),
+ };
+}
+
+type EffectiveToolResultProjection =
+ | {
+ kind: 'projection';
+ projection: DurableToolResultProjection;
+ legacyOutput: unknown;
+ }
+ | { kind: 'provider_native'; output: unknown }
+ | { kind: 'legacy_output'; output: unknown }
+ | { kind: 'invalid_legacy'; message: string };
+
+/** The single compatibility boundary for both current and legacy response events. */
+export function decodeEffectiveToolResultProjection(
+ content: RuntimeEventFunctionResponseContent,
+ sessionId: string,
+): EffectiveToolResultProjection {
+ if (content.providerExecuted && content.providerOutput !== undefined) {
+ return { kind: 'provider_native', output: content.providerOutput };
+ }
+ if (content.modelProjection !== undefined) {
+ try {
+ return {
+ kind: 'projection',
+ projection: decodeDurableToolResultProjection(content.modelProjection),
+ legacyOutput: content.result,
+ };
+ } catch {
+ return {
+ kind: 'projection',
+ projection: DURABLE_TOOL_RESULT_PROJECTION_FAILURE,
+ legacyOutput: content.result,
+ };
+ }
+ }
+
+ let output = withToolResultArchiveResourceRef(content.result);
+ if (isRetiredExploreAgentResult(output)) {
+ try {
+ output = decodePersistedToolResultContent(markPersisted<ToolResultContent>(output));
+ } catch {
+ return {
+ kind: 'invalid_legacy',
+ message: 'function_response contains an invalid retired tool result',
+ };
+ }
+ }
+ const shellResult = decodeCanonicalShellToolResultContent(output);
+ if (shellResult.state === 'invalid') {
+ return {
+ kind: 'invalid_legacy',
+ message: 'function_response contains an invalid shell tool result',
+ };
+ }
+ if (shellResult.state === 'valid') output = shellResult.content;
+ output =
+ content.name === 'Bash'
+ ? projectBashToolResultForModel(output)
+ : projectFileWriteToolResultForModel(content.name, output);
+ const projection =
+ content.isError === true
+ ? encodeDurableToolResultOutput(compatibilityErrorOutput(output), sessionId)
+ : encodeDefaultDurableToolResultOutput(output, sessionId);
+ if (projection.kind === 'failure' && isLegacyPathImageResult(output, sessionId)) {
+ return { kind: 'legacy_output', output };
+ }
+ return {
+ kind: 'projection',
+ projection,
+ legacyOutput: output,
+ };
+}
+
+function compatibilityErrorOutput(output: unknown): ToolResultOutput {
+ return output !== null &&
+ typeof output === 'object' &&
+ !Array.isArray(output) &&
+ (output as { kind?: unknown }).kind === 'text' &&
+ typeof (output as { text?: unknown }).text === 'string'
+ ? { type: 'error-text', value: new Error((output as { text: string }).text).toString() }
+ : toolResultOutput(output, true);
+}
+
+export function compatibilityToolResultProjection(
+ content: RuntimeEventFunctionResponseContent,
+ sessionId: string,
+): DurableToolResultProjection | undefined {
+ const effective = decodeEffectiveToolResultProjection(content, sessionId);
+ if (effective.kind === 'provider_native') return undefined;
+ if (effective.kind === 'legacy_output') return DURABLE_TOOL_RESULT_PROJECTION_FAILURE;
+ return effective.kind === 'projection'
+ ? effective.projection
+ : DURABLE_TOOL_RESULT_PROJECTION_FAILURE;
+}
+
+function encodeOutput(output: ToolResultOutput, sessionId: string): DurableToolResultProjection {
+ switch (output.type) {
+ case 'text':
+ case 'error-text':
+ return {
+ version: DURABLE_TOOL_RESULT_PROJECTION_VERSION,
+ kind: 'text',
+ text: output.value,
+ ...(output.type === 'error-text' ? { isError: true as const } : {}),
+ };
+ case 'json':
+ case 'error-json':
+ return {
+ version: DURABLE_TOOL_RESULT_PROJECTION_VERSION,
+ kind: 'json',
+ value: sanitizeJson(output.value),
+ ...(output.type === 'error-json' ? { isError: true as const } : {}),
+ };
+ case 'execution-denied':
+ return {
+ version: DURABLE_TOOL_RESULT_PROJECTION_VERSION,
+ kind: 'execution_denied',
+ ...(output.reason !== undefined ? { reason: output.reason } : {}),
+ };
+ case 'content': {
+ return prepareContentProjection(output, sessionId).projection;
+ }
+ }
+}
+
+function prepareContentProjection(
+ output: Extract<ToolResultOutput, { type: 'content' }>,
+ sessionId: string,
+ planArtifact?: DurableProjectionArtifactPlanner,
+): {
+ projection: DurableToolResultProjection;
+ artifacts: DurableProjectionArtifactPlan[];
+} {
+ if (output.value.length > DURABLE_TOOL_RESULT_PROJECTION_MAX_PARTS) {
+ throw new Error('Tool Result content exceeds the durable part limit');
+ }
+ const parts: DurableToolResultProjectionPart[] = [];
+ const artifacts: DurableProjectionArtifactPlan[] = [];
+ for (const part of output.value) {
+ if (part.type === 'text') {
+ parts.push({ kind: 'text', text: part.text });
+ continue;
+ }
+ if (part.type !== 'file') continue;
+ const ref = readSessionArtifactRef(part, sessionId);
+ if (ref) {
+ const mediaType = normalizeArtifactImagePreviewMime(part.mediaType);
+ if (!mediaType) throw new Error('Artifact has an unsafe media type');
+ parts.push({ kind: 'artifact', mediaType, ref });
+ continue;
+ }
+ if (
+ planArtifact &&
+ part.data.type === 'data' &&
+ part.mediaType.toLowerCase().startsWith('image/')
+ ) {
+ const mediaType = normalizeArtifactImagePreviewMime(part.mediaType);
+ if (!mediaType) throw new Error('Inline image has an unsafe media type');
+ const artifact = planArtifact({
+ bytes: decodeBoundedImageData(part.data.data),
+ mediaType,
+ });
+ artifacts.push(artifact);
+ parts.push({ kind: 'artifact', mediaType, ref: artifact.ref });
+ continue;
+ }
+ parts.push({ kind: 'text', text: OMITTED_BINARY_TEXT });
+ }
+ if (parts.length === 0) parts.push({ kind: 'text', text: 'Tool completed with no content.' });
+ return {
+ projection: decodeDurableToolResultProjection({
+ version: DURABLE_TOOL_RESULT_PROJECTION_VERSION,
+ kind: 'content',
+ parts,
+ }),
+ artifacts,
+ };
+}
+
+function readSessionArtifactRef(
+ part: Extract<ToolResultOutput, { type: 'content' }>['value'][number],
+ sessionId: string,
+) {
+ if (part.type !== 'file') return undefined;
+ const data = part.data as unknown;
+ if (!data || typeof data !== 'object' || Array.isArray(data)) return undefined;
+ const ref = (data as { ref?: unknown }).ref;
+ return isCanonicalStorageRef(ref) &&
+ (ref.kind === 'session_context' || ref.kind === 'session_file') &&
+ ref.sessionId === sessionId
+ ? ref
+ : undefined;
+}
+
+function hasInlineImage(output: Extract<ToolResultOutput, { type: 'content' }>): boolean {
+ return output.value.some(
+ (part) =>
+ part.type === 'file' &&
+ part.data.type === 'data' &&
+ part.mediaType.toLowerCase().startsWith('image/'),
+ );
+}
+
+function decodeBoundedImageData(data: unknown): Uint8Array {
+ let bytes: Uint8Array;
+ if (typeof data === 'string') {
+ if (data.length > Math.ceil((MAX_READ_IMAGE_BYTES * 4) / 3) + 4) {
+ throw new Error('Inline image exceeds the artifact byte limit');
+ }
+ const decoded = Buffer.from(data, 'base64');
+ if (decoded.toString('base64') !== data)
+ throw new Error('Inline image is not canonical base64');
+ bytes = decoded;
+ } else if (data instanceof ArrayBuffer) {
+ if (data.byteLength > MAX_READ_IMAGE_BYTES) {
+ throw new Error('Inline image exceeds the artifact byte limit');
+ }
+ bytes = new Uint8Array(data.slice(0));
+ } else if (ArrayBuffer.isView(data)) {
+ if (data.byteLength > MAX_READ_IMAGE_BYTES) {
+ throw new Error('Inline image exceeds the artifact byte limit');
+ }
+ bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength).slice();
+ } else {
+ throw new Error('Inline image data is not representable');
+ }
+ if (bytes.byteLength > MAX_READ_IMAGE_BYTES) {
+ throw new Error('Inline image exceeds the artifact byte limit');
+ }
+ return bytes;
+}
+
+function sessionImageResult(result: unknown, sessionId: string) {
+ if (!result || typeof result !== 'object' || Array.isArray(result)) return undefined;
+ const image = result as { kind?: unknown; mimeType?: unknown; ref?: unknown };
+ return image.kind === 'image' &&
+ typeof image.mimeType === 'string' &&
+ image.mimeType.length > 0 &&
+ isCanonicalStorageRef(image.ref) &&
+ (image.ref.kind === 'session_context' || image.ref.kind === 'session_file') &&
+ image.ref.sessionId === sessionId
+ ? { mimeType: image.mimeType, ref: image.ref }
+ : undefined;
+}
+
+function isLegacyPathImageResult(result: unknown, sessionId: string): boolean {
+ const image = sessionImageResult(result, sessionId);
+ return image?.ref.kind === 'session_file' && !isCanonicalArtifactEntityId(image.ref.relativePath);
+}
+
+function sanitizeJson(value: unknown): DurableProjectionJson {
+ const state = { nodes: 0 };
+ return sanitizeJsonValue(value, state, 0);
+}
+
+function sanitizeJsonValue(
+ value: unknown,
+ state: { nodes: number },
+ depth: number,
+): DurableProjectionJson {
+ state.nodes += 1;
+ if (
+ state.nodes > DURABLE_TOOL_RESULT_PROJECTION_MAX_JSON_NODES ||
+ depth > DURABLE_TOOL_RESULT_PROJECTION_MAX_JSON_DEPTH
+ ) {
+ throw new Error('JSON exceeds limit');
+ }
+ if (value === null) return null;
+ if (typeof value === 'string') return value;
+ if (typeof value === 'boolean') return value;
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
+ if (Array.isArray(value)) {
+ return value.map((item) => sanitizeJsonValue(item, state, depth + 1));
+ }
+ if (!value || typeof value !== 'object') throw new Error('JSON is not representable');
+ const prototype = Object.getPrototypeOf(value);
+ if (prototype !== Object.prototype && prototype !== null) {
+ throw new Error('JSON contains an opaque object');
+ }
+ if (typeof (value as { toJSON?: unknown }).toJSON === 'function') {
+ throw new Error('JSON contains an opaque serializer');
+ }
+ const result: Record<string, DurableProjectionJson> = {};
+ for (const [key, item] of Object.entries(value)) {
+ Object.defineProperty(result, key, {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: sanitizeJsonValue(item, state, depth + 1),
+ });
+ }
+ return result;
+}
+
+function isRetiredExploreAgentResult(value: unknown): boolean {
+ return (
+ value !== null &&
+ typeof value === 'object' &&
+ !Array.isArray(value) &&
+ (value as { kind?: unknown }).kind === 'explore_agent'
+ );
+}
diff --git a/packages/runtime/src/model-history.ts b/packages/runtime/src/model-history.ts
index 53332e4343..1a74cde6c6 100644
--- a/packages/runtime/src/model-history.ts
+++ b/packages/runtime/src/model-history.ts
@@ -61,16 +61,11 @@ import {
type RuntimeEventContent,
type RuntimeEventRole,
} from '@maka/core/runtime-event';
-import { withToolResultArchiveResourceRef } from './tool-result-archive.js';
import { formatAttachmentResourceRef } from '@maka/core/attachments';
-import { decodeCanonicalShellToolResultContent } from '@maka/core/shell-run-result';
-import { markPersisted } from '@maka/core/persisted-value';
-import { decodePersistedToolResultContent } from '@maka/core/tool-result-record-schema';
-import type { ToolResultContent } from '@maka/core/events';
import type { AttachmentRef, QuoteRef } from '@maka/core/events';
import type { ModelMessage, UserContent, UserModelMessage } from './model-protocol.js';
-import { projectBashToolResultForModel } from './bash-model-output.js';
-import { projectFileWriteToolResultForModel } from './file-tool-model-output.js';
+import { decodeEffectiveToolResultProjection } from './durable-tool-result-projection.js';
+import type { DurableToolResultProjection } from '@maka/core/durable-tool-result-projection';
export const PROVIDER_REPLAY_PROJECTION_VERSION = 1;
@@ -178,6 +173,7 @@ export type RuntimeEventModelReplayItem =
toolName: string;
output: unknown;
isError: boolean;
+ modelProjection?: DurableToolResultProjection;
providerExecuted?: boolean;
eventId: string;
ts: number;
@@ -610,47 +606,21 @@ export function buildRuntimeEventModelReplayPlan(
);
continue;
}
- const usesProviderOutput =
- event.content.providerExecuted && event.content.providerOutput !== undefined;
- let invalidResultMessage: string | undefined;
- let normalizedResult: unknown = usesProviderOutput
- ? event.content.providerOutput
- : withToolResultArchiveResourceRef(event.content.result);
- if (!usesProviderOutput && isRetiredExploreAgentResult(normalizedResult)) {
- try {
- normalizedResult = decodePersistedToolResultContent(
- markPersisted<ToolResultContent>(normalizedResult),
- );
- } catch {
- invalidResultMessage = 'function_response contains an invalid retired tool result';
- }
- }
- if (!invalidResultMessage) {
- const shellResult = decodeCanonicalShellToolResultContent(normalizedResult);
- if (shellResult.state === 'invalid') {
- invalidResultMessage = 'function_response contains an invalid shell tool result';
- } else if (shellResult.state === 'valid') {
- normalizedResult = shellResult.content;
- }
- }
- if (!invalidResultMessage && event.content.name === 'Bash') {
- normalizedResult = projectBashToolResultForModel(normalizedResult);
- } else if (!invalidResultMessage) {
- normalizedResult = projectFileWriteToolResultForModel(
- event.content.name,
- normalizedResult,
- );
- }
- if (invalidResultMessage) {
+ const effective = decodeEffectiveToolResultProjection(event.content, event.sessionId);
+ if (effective.kind === 'invalid_legacy') {
const call = callsById.get(event.content.id);
if (call) {
const callIndex = items.indexOf(call.item);
if (callIndex >= 0) items.splice(callIndex, 1);
callsById.delete(event.content.id);
}
- diagnostics.push(diagnostic(event, 'unsupported_content', invalidResultMessage));
+ diagnostics.push(diagnostic(event, 'unsupported_content', effective.message));
continue;
}
+ const normalizedResult =
+ effective.kind === 'provider_native' || effective.kind === 'legacy_output'
+ ? effective.output
+ : effective.legacyOutput;
const call = callsById.get(event.content.id);
if (!call) {
diagnostics.push(
@@ -683,6 +653,11 @@ export function buildRuntimeEventModelReplayPlan(
toolCallId: event.content.id,
toolName: event.content.name,
output: normalizedResult,
+ ...(effective.kind === 'projection'
+ ? {
+ modelProjection: effective.projection,
+ }
+ : {}),
isError: event.content.isError === true,
...(event.content.providerExecuted !== undefined
? { providerExecuted: event.content.providerExecuted }
@@ -749,12 +724,6 @@ export function buildRuntimeEventModelReplayPlan(
};
}
-function isRetiredExploreAgentResult(value: unknown): boolean {
- return (
- typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'explore_agent'
- );
-}
-
/**
* Convert projected RuntimeEvent history into the current AI SDK text-only
* message shape. Tool/function and thinking entries are intentionally skipped.
diff --git a/packages/runtime/src/recovery-resolver.ts b/packages/runtime/src/recovery-resolver.ts
index 925944c8c0..aac21f7c78 100644
--- a/packages/runtime/src/recovery-resolver.ts
+++ b/packages/runtime/src/recovery-resolver.ts
@@ -30,6 +30,7 @@ import {
import { interpretScannedToolRecovery } from '@maka/core/tool-recovery-bundle';
import type { ToolOutcomeCommit } from './runtime-commit-sink.js';
import type { ToolMode } from '@maka/core/tool-mode';
+import { compatibilityToolResultProjection } from './durable-tool-result-projection.js';
export type ToolRecoveryDecisionStatus =
| 'completed'
@@ -151,6 +152,22 @@ export function buildInterruptedCodeModeOutcomeCommits(
) {
return [];
}
+ const result = {
+ kind: 'json' as const,
+ value: {
+ kind: 'code_mode' as const,
+ status: 'interrupted' as const,
+ message: 'Code Mode execution was interrupted by runtime recovery.',
+ },
+ };
+ const responseContent = {
+ kind: 'function_response' as const,
+ id: call.id,
+ name: call.name,
+ result,
+ isError: true as const,
+ };
+ const modelProjection = compatibilityToolResultProjection(responseContent, callEvent.sessionId);
const runtimeEvent: RuntimeEvent = {
id: `${decision.operationId}_response`,
invocationId: callEvent.invocationId,
@@ -163,20 +180,7 @@ export function buildInterruptedCodeModeOutcomeCommits(
author: 'tool',
origin: 'provider',
modelVisibility: 'visible',
- content: {
- kind: 'function_response',
- id: call.id,
- name: call.name,
- result: {
- kind: 'json',
- value: {
- kind: 'code_mode',
- status: 'interrupted',
- message: 'Code Mode execution was interrupted by runtime recovery.',
- },
- },
- isError: true,
- },
+ content: { ...responseContent, ...(modelProjection ? { modelProjection } : {}) },
refs: { operationId: decision.operationId, toolCallId: call.id },
};
return [
diff --git a/packages/runtime/src/session-event-runtime-mapper.ts b/packages/runtime/src/session-event-runtime-mapper.ts
index 3044d110f9..63e251c84e 100644
--- a/packages/runtime/src/session-event-runtime-mapper.ts
+++ b/packages/runtime/src/session-event-runtime-mapper.ts
@@ -34,6 +34,7 @@ import {
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;
@@ -332,6 +333,24 @@ function mapBackendSessionEvent(
};
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',
@@ -339,17 +358,8 @@ function mapBackendSessionEvent(
...(event.origin !== undefined ? { origin: event.origin } : {}),
...(event.modelVisibility !== undefined ? { modelVisibility: event.modelVisibility } : {}),
content: {
- kind: 'function_response',
- id: event.toolUseId,
- name,
- result: event.content,
- ...(event.isError ? { isError: true } : {}),
- ...(event.providerExecuted !== undefined
- ? { providerExecuted: event.providerExecuted }
- : {}),
- ...(event.providerExecuted && event.providerOutput !== undefined
- ? { providerOutput: structuredClone(event.providerOutput) }
- : {}),
+ ...content,
+ ...(modelProjection ? { modelProjection } : {}),
},
refs: {
toolCallId: event.toolUseId,
diff --git a/packages/runtime/src/session-manager.ts b/packages/runtime/src/session-manager.ts
index 96591f9d73..b8497a9df0 100644
--- a/packages/runtime/src/session-manager.ts
+++ b/packages/runtime/src/session-manager.ts
@@ -61,9 +61,7 @@ import type {
} from '@maka/core/session';
import type {
CreateSessionInput,
- BranchFromTurnInput,
RegenerateTurnInput,
- ReviseBeforeTurnInput,
UserMessageInput,
SessionListFilter,
} from '@maka/core/runtime-inputs';
@@ -91,7 +89,6 @@ import {
} from '@maka/core/plan';
import { DEFAULT_SESSION_NAME } from '@maka/core/session-name';
import { DEEP_RESEARCH_SESSION_LABEL, isDeepResearchSession } from '@maka/core/deep-research';
-import { SIDE_CONVERSATION_SESSION_LABEL } from '@maka/core/side-conversation';
import {
SUBAGENT_SESSION_RUNTIME_SCHEMA_VERSION,
SUBAGENT_SESSION_SPAWN_SCHEMA_VERSION,
@@ -144,12 +141,6 @@ import {
type RuntimeReadModelSessionView,
} from './runtime-read-model.js';
import { inspectAgentRunReadModel, type AgentRunInspectModel } from './agent-run-inspect.js';
-import {
- cloneConversationRuntimeLedger as cloneConversationLedger,
- createConversationCopySlice,
- prepareConversationRuntimeLedgerCopy,
- type ConversationRuntimeLedgerCopyPlan,
-} from './conversation-copy.js';
import { firstRuntimeRepairRunId, RuntimeLedgerRepair } from './runtime-ledger-repair.js';
import {
buildRecoveredTerminalRuntimeEvent,
@@ -3875,13 +3866,6 @@ export class SessionManager {
};
}
- async branchFromTurn(sessionId: string, input: BranchFromTurnInput): Promise<SessionSummary> {
- const sourceView = await this.getSessionView(sessionId);
- const slice = createConversationCopySlice(sourceView.messages, input.sourceTurnId, 'through');
- if (!slice) throw new Error(`Cannot branch from unknown turn ${input.sourceTurnId}`);
- return this.createBranchSession(sessionId, sourceView, [...slice.messages], input);
- }
-
/** Canonical, repaired source view for a Host-owned cross-Session copy. */
async readConversationCopySnapshot(sessionId: string): Promise<RuntimeReadModelSessionView> {
const readMessagesSnapshot = this.deps.store.readMessagesSnapshot;
@@ -3900,148 +3884,6 @@ export class SessionManager {
};
}
- async branchBeforeTurn(sessionId: string, input: BranchFromTurnInput): Promise<SessionSummary> {
- const sourceView = await this.getSessionView(sessionId);
- const slice = createConversationCopySlice(sourceView.messages, input.sourceTurnId, 'before');
- if (!slice) throw new Error(`Cannot branch before unknown turn ${input.sourceTurnId}`);
- return this.createBranchSession(sessionId, sourceView, [...slice.messages], input);
- }
-
- /**
- * Create a non-destructive edit-and-resend version. Unlike branchBeforeTurn,
- * this is not a new sidebar conversation: revision lineage lets hosts fold
- * every version into one conversation slot while keeping old transcripts.
- */
- async reviseBeforeTurn(sessionId: string, input: ReviseBeforeTurnInput): Promise<SessionSummary> {
- const sourceView = await this.getSessionView(sessionId);
- const slice = createConversationCopySlice(sourceView.messages, input.sourceTurnId, 'before');
- if (!slice) throw new Error(`Cannot revise before unknown turn ${input.sourceTurnId}`);
- return this.createRevisionSession(sessionId, sourceView, [...slice.messages], input);
- }
-
- private async createRevisionSession(
- sessionId: string,
- sourceView: RuntimeReadModelSessionView,
- copied: StoredMessage[],
- input: ReviseBeforeTurnInput,
- ): Promise<SessionSummary> {
- const plan = await this.prepareConversationRuntimeLedgerClone(sessionId, sourceView, copied);
- const [header, boundary] = await Promise.all([
- this.deps.store.readHeader(sessionId),
- this.deps.store.readExecutionBoundary(sessionId),
- ]);
- const revisionRootSessionId = header.revisionRootSessionId ?? sessionId;
- const family = (await this.deps.store.list()).filter(
- (candidate) =>
- candidate.id === revisionRootSessionId ||
- candidate.revisionRootSessionId === revisionRootSessionId,
- );
- const revisionIndex =
- Math.max(1, ...family.map((candidate) => candidate.revisionIndex ?? 1)) + 1;
- const next = await this.deps.store.create(
- {
- cwd: header.cwd,
- ...(header.projectId !== undefined ? { projectId: header.projectId } : {}),
- ...(header.llmConnectionId === undefined
- ? {}
- : { llmConnectionId: header.llmConnectionId }),
- llmConnectionSlug: header.llmConnectionSlug,
- model: header.model,
- thinkingLevel: header.thinkingLevel,
- permissionMode: header.permissionMode,
- collaborationMode: header.collaborationMode,
- orchestrationMode: header.orchestrationMode ?? 'default',
- name: header.name,
- labels: header.labels,
- // A revision of a real branch remains in that branch's conversation
- // slot; revision lineage itself must not create a branch banner.
- parentSessionId: header.parentSessionId,
- branchOfTurnId: header.branchOfTurnId,
- revisionRootSessionId,
- revisionParentSessionId: sessionId,
- revisionOfTurnId: input.sourceTurnId,
- revisionIndex,
- revisionState: 'preparing',
- status: 'active',
- },
- boundary,
- );
- try {
- const rewritten = await this.cloneConversationRuntimeLedger(next.id, copied, plan);
- if (rewritten.length > 0) await this.deps.store.appendMessages(next.id, [...rewritten]);
- await this.deps.store.appendMessage(next.id, {
- type: 'system_note',
- id: this.deps.newId(),
- ts: this.deps.now(),
- kind: 'session_start',
- data: {
- revisionRootSessionId,
- revisionParentSessionId: sessionId,
- revisionOfTurnId: input.sourceTurnId,
- revisionIndex,
- revisionState: 'preparing',
- },
- });
- await this.deps.store.updateHeader(next.id, {
- isFlagged: header.isFlagged,
- titleIsManual: header.titleIsManual,
- });
- return headerToSummary(await this.deps.store.readHeader(next.id));
- } catch (error) {
- return this.rollbackLegacyConversationCopy(next.id, error);
- }
- }
-
- private async createBranchSession(
- sessionId: string,
- sourceView: RuntimeReadModelSessionView,
- copied: StoredMessage[],
- input: BranchFromTurnInput,
- ): Promise<SessionSummary> {
- const plan = await this.prepareConversationRuntimeLedgerClone(sessionId, sourceView, copied);
- const [header, boundary] = await Promise.all([
- this.deps.store.readHeader(sessionId),
- this.deps.store.readExecutionBoundary(sessionId),
- ]);
- const next = await this.deps.store.create(
- {
- cwd: header.cwd,
- ...(header.projectId !== undefined ? { projectId: header.projectId } : {}),
- ...(header.llmConnectionId === undefined
- ? {}
- : { llmConnectionId: header.llmConnectionId }),
- llmConnectionSlug: header.llmConnectionSlug,
- model: header.model,
- thinkingLevel: header.thinkingLevel,
- permissionMode: header.permissionMode,
- collaborationMode: header.collaborationMode,
- orchestrationMode: header.orchestrationMode ?? 'default',
- name: input.name ?? `${header.name} · 分支`,
- labels: input.sideConversation
- ? [...new Set([...header.labels, SIDE_CONVERSATION_SESSION_LABEL])]
- : header.labels,
- parentSessionId: sessionId,
- branchOfTurnId: input.sourceTurnId,
- status: 'active',
- },
- boundary,
- );
- try {
- const rewritten = await this.cloneConversationRuntimeLedger(next.id, copied, plan);
- if (rewritten.length > 0) await this.deps.store.appendMessages(next.id, [...rewritten]);
- await this.deps.store.appendMessage(next.id, {
- type: 'system_note',
- id: this.deps.newId(),
- ts: this.deps.now(),
- kind: 'session_start',
- data: { parentSessionId: sessionId, branchOfTurnId: input.sourceTurnId },
- });
- return headerToSummary(await this.deps.store.readHeader(next.id));
- } catch (error) {
- return this.rollbackLegacyConversationCopy(next.id, error);
- }
- }
-
async respondToSandboxBoundary(
sessionId: string,
response: SandboxBoundaryResponse,
@@ -4471,54 +4313,6 @@ export class SessionManager {
this.preparedTranscriptLedgers.add(sessionId);
}
- private async prepareConversationRuntimeLedgerClone(
- sourceSessionId: string,
- sourceView: RuntimeReadModelSessionView,
- copiedMessages: readonly StoredMessage[],
- ): Promise<ConversationRuntimeLedgerCopyPlan | undefined> {
- if (!this.deps.runStore || !this.deps.runtimeEventStore) return undefined;
- return prepareConversationRuntimeLedgerCopy({
- sourceSessionId,
- sourceEvents: sourceView.events,
- copiedMessages,
- runStore: this.deps.runStore,
- runtimeEventStore: this.deps.runtimeEventStore,
- });
- }
-
- private async cloneConversationRuntimeLedger(
- childSessionId: string,
- copiedMessages: readonly StoredMessage[],
- plan: ConversationRuntimeLedgerCopyPlan | undefined,
- ): Promise<readonly StoredMessage[]> {
- if (!plan || !this.deps.runStore || !this.deps.runtimeEventStore) return copiedMessages;
- const copied = await cloneConversationLedger({
- plan,
- copiedMessages,
- referenceMap: {
- mode: 'preserve_external',
- sourceSessionId: plan.sourceSessionId,
- targetSessionId: childSessionId,
- },
- runStore: this.deps.runStore,
- runtimeEventStore: this.deps.runtimeEventStore,
- newId: this.deps.newId,
- });
- return copied.copiedMessages;
- }
-
- private async rollbackLegacyConversationCopy(sessionId: string, error: unknown): Promise<never> {
- try {
- await this.deps.store.remove(sessionId);
- } catch (cleanupError) {
- throw new AggregateError(
- [error, cleanupError],
- `Conversation copy ${sessionId} failed and could not be removed`,
- );
- }
- throw error;
- }
-
/**
* Closes only the two provably pre-provider crash windows owned by B2:
* claim-only and target-Run-created-without-start. A repaired claim never
diff --git a/packages/runtime/src/tool-runtime.ts b/packages/runtime/src/tool-runtime.ts
index a5d612072a..66d2797313 100644
--- a/packages/runtime/src/tool-runtime.ts
+++ b/packages/runtime/src/tool-runtime.ts
@@ -82,8 +82,18 @@ import { stableHash } from './request-shape.js';
import { classifyError } from './provider-error-classification.js';
import type { RunTraceLike } from './run-trace.js';
import { AwaitRegistry } from './await-registry.js';
-import { jsonValue } from './tool-result-output.js';
import type { ToolResultOutput } from './model-protocol.js';
+import {
+ compatibilityToolResultProjection,
+ encodeDefaultDurableToolResultOutput,
+ encodeDurableToolResultOutput,
+ encodeDurableToolResultOutputWithArtifacts,
+} from './durable-tool-result-projection.js';
+import {
+ DURABLE_TOOL_RESULT_PROJECTION_FAILURE,
+ type DurableProjectionArtifactRef,
+ type DurableToolResultProjection,
+} from '@maka/core/durable-tool-result-projection';
import {
buildToolOperationId,
canonicalToolArgsHash,
@@ -137,11 +147,6 @@ export interface DurableSessionEventSink {
}
export interface ToolSettlement {
- result: unknown;
- modelOutput: ToolResultOutput;
-}
-
-export interface RawToolSettlement {
result: unknown;
providerError?: string;
}
@@ -194,12 +199,12 @@ export interface MakaTool<P = any, R = unknown> {
* settlement instead of detaching, so late side effects cannot outlive `exec`.
*/
impl: (args: P, ctx: MakaToolContext) => Promise<R> | R;
- /** Optional provider-visible content mapping, used for screenshot image parts. */
+ /** Optional synchronous provider-visible content mapping, used for screenshot image parts. */
toModelOutput?: (options: {
toolCallId: string;
input: unknown;
output: unknown;
- }) => ToolResultOutput | PromiseLike<ToolResultOutput>;
+ }) => ToolResultOutput;
}
export interface MakaToolContext {
@@ -349,10 +354,14 @@ export interface ToolRuntimeInput {
runId?: string;
orchestrationMode?: OrchestrationMode;
invocationId?: string;
- materializeDefaultToolResultOutput?: (options: {
- toolCallId: string;
- output: unknown;
- }) => ToolResultOutput | PromiseLike<ToolResultOutput>;
+ prepareDurableProjectionArtifact?: (input: {
+ turnId: string;
+ bytes: Uint8Array;
+ mediaType: string;
+ }) => {
+ ref: Extract<DurableProjectionArtifactRef, { kind: 'session_file' }>;
+ persist(): Promise<void>;
+ };
spawnChildSession?: (input: {
parentRunId: string;
parentTurnId: string;
@@ -404,6 +413,7 @@ interface RuntimeManagedMutationOperationValue<T> {
readonly content: ToolResultContent;
readonly isError: boolean;
readonly durationMs: number;
+ readonly modelProjection: DurableToolResultProjection;
};
}
@@ -416,6 +426,7 @@ export interface RuntimeManagedMutationOperationProof {
readonly content: ToolResultContent;
readonly isError: boolean;
readonly durationMs: number;
+ readonly modelProjection: DurableToolResultProjection;
}
export type RuntimeManagedMutationSettlement =
@@ -446,12 +457,14 @@ interface DurableToolAttempt {
commitOutcome(
result: unknown,
isError: boolean,
+ modelProjection: DurableToolResultProjection,
durationMs?: number,
): Promise<{ id: string; operationId: string; ts: number }>;
adoptCommittedOutcome(
event: RuntimeEvent,
result: ToolResultContent,
isError: boolean,
+ modelProjection: DurableToolResultProjection,
durationMs: number,
): { id: string; operationId: string; ts: number };
}
@@ -709,38 +722,6 @@ export class ToolRuntime {
* provider-facing error output; durable runtime commit failures still reject.
*/
async settleToolCall(call: ResolvedMakaToolCall): Promise<ToolSettlement> {
- const settlement = await this.settleToolCallRaw(call);
- const modelOutput = settlement.providerError
- ? call.tool.providerTool?.kind === 'openai-apply-patch'
- ? {
- type: 'json' as const,
- value: { status: 'failed' as const, output: settlement.providerError },
- }
- : { type: 'error-text' as const, value: new Error(settlement.providerError).toString() }
- : call.tool.toModelOutput
- ? await call.tool.toModelOutput({
- toolCallId: call.toolCallId,
- input: call.input,
- output: settlement.result,
- })
- : this.input.materializeDefaultToolResultOutput
- ? await this.input.materializeDefaultToolResultOutput({
- toolCallId: call.toolCallId,
- output: settlement.result,
- })
- : typeof settlement.result === 'string'
- ? { type: 'text' as const, value: settlement.result }
- : { type: 'json' as const, value: jsonValue(settlement.result) };
- return { result: settlement.result, modelOutput };
- }
-
- /**
- * Settle a tool without producing provider-visible output. Runtime-owned
- * nested calls use this path because their result is consumed by Code Mode,
- * not sent as a provider tool-result part; materializing it could spend
- * turn-scoped provider resources such as the image budget.
- */
- async settleToolCallRaw(call: ResolvedMakaToolCall): Promise<RawToolSettlement> {
const settlement = this.performToolSettlement(call);
// Tracked so endTurn can wait out unwinds already in flight (#2253):
// their T2 outcomes must land before the stop path settles the run's
@@ -753,7 +734,7 @@ export class ToolRuntime {
return settlement;
}
- private async performToolSettlement(call: ResolvedMakaToolCall): Promise<RawToolSettlement> {
+ private async performToolSettlement(call: ResolvedMakaToolCall): Promise<ToolSettlement> {
const result = await this.executeTool(
call.tool,
call.turnId,
@@ -774,6 +755,55 @@ export class ToolRuntime {
return { result, ...(providerError ? { providerError } : {}) };
}
+ private projectToolResult(
+ tool: MakaTool,
+ turnId: string,
+ toolCallId: string,
+ input: unknown,
+ result: unknown,
+ ): DurableToolResultProjection | PromiseLike<DurableToolResultProjection> {
+ try {
+ const providerError = providerToolErrorMessage(result);
+ if (providerError) {
+ return encodeDurableToolResultOutput(
+ tool.providerTool?.kind === 'openai-apply-patch'
+ ? {
+ type: 'json' as const,
+ value: { status: 'failed' as const, output: providerError },
+ }
+ : { type: 'error-text' as const, value: new Error(providerError).toString() },
+ this.input.sessionId,
+ );
+ }
+ if (!tool.toModelOutput) {
+ return encodeDefaultDurableToolResultOutput(result, this.input.sessionId);
+ }
+ const output = tool.toModelOutput({ toolCallId, input, output: result });
+ // Projection is deliberately synchronous and total at the tool boundary.
+ // Fail closed for untyped/plugin implementations that violate the
+ // contract so a completed effect can never be stranded before T2.
+ if (isPromiseLike<ToolResultOutput>(output)) {
+ return DURABLE_TOOL_RESULT_PROJECTION_FAILURE;
+ }
+ const encode = (resolved: ToolResultOutput) =>
+ encodeDurableToolResultOutputWithArtifacts(
+ resolved,
+ this.input.sessionId,
+ this.input.prepareDurableProjectionArtifact
+ ? ({ bytes, mediaType }) =>
+ this.input.prepareDurableProjectionArtifact!({
+ turnId,
+ bytes,
+ mediaType,
+ })
+ : undefined,
+ );
+ return encode(output);
+ } catch {
+ return DURABLE_TOOL_RESULT_PROJECTION_FAILURE;
+ }
+ }
+
/**
* Install the per-step tool-availability gating used at the execute boundary.
* The backend recomputes the active snapshot before each step; the guard in
@@ -891,9 +921,10 @@ export class ToolRuntime {
this.lastFailedToolCallBoundaryDetails = boundaryDetails;
}
- async writeSyntheticToolResult(
+ private async writeSyntheticToolResult(
toolUseId: string,
turnId: string,
+ toolName: string,
text: string,
queue: DurableSessionEventSink,
sandboxDenial?: SandboxDenialSignal,
@@ -924,7 +955,18 @@ export class ToolRuntime {
// guards, where no attempt exists and no identity is owed.
const durableAttempt =
attempt ?? this.durableToolAttempts.get(durableAttemptKey(turnId, toolUseId));
- const durableOutcome = await durableAttempt?.commitOutcome(content, true);
+ const modelProjection =
+ compatibilityToolResultProjection(
+ {
+ kind: 'function_response',
+ id: toolUseId,
+ name: toolName,
+ result: content,
+ isError: true,
+ },
+ this.input.sessionId,
+ ) ?? DURABLE_TOOL_RESULT_PROJECTION_FAILURE;
+ const durableOutcome = await durableAttempt?.commitOutcome(content, true, modelProjection);
const msg: ToolResultMessage = {
type: 'tool_result',
id: this.input.newId(),
@@ -945,6 +987,7 @@ export class ToolRuntime {
...(durableOutcome ? { operationId: durableOutcome.operationId } : {}),
isError: true,
content,
+ modelProjection,
...activityIdentity,
} satisfies ToolResultEvent);
}
@@ -1111,20 +1154,52 @@ export class ToolRuntime {
...(tool.displayName ? { displayName: tool.displayName } : {}),
...(stepId !== undefined ? { stepId } : {}),
};
- let pushedCallEvent: ToolStartEvent | undefined;
- const pushCallEvent = (lane: 'dispatch' | 'preflight'): ToolStartEvent => {
- // Idempotent by construction: one call, one call event, whichever lane
- // asks for it first. A second ask cannot mint a second id.
- if (pushedCallEvent) return pushedCallEvent;
+ let callEvent: ToolStartEvent | undefined;
+ const buildCallEvent = (lane: 'dispatch' | 'preflight'): ToolStartEvent => {
+ if (callEvent) return callEvent;
const operationId = lane === 'dispatch' ? dispatchOperationId : undefined;
- const event: ToolStartEvent = {
+ callEvent = {
...callEventFacts,
id: operationId ? `${operationId}_call` : this.input.newId(),
...(operationId ? { operationId } : {}),
};
+ return callEvent;
+ };
+ let callEventPublished = false;
+ const publishCallEvent = (event: ToolStartEvent): void => {
+ if (callEventPublished) return;
queue.push(event);
- pushedCallEvent = event;
- return event;
+ callEventPublished = true;
+ };
+ const callMsg: ToolCallMessage = {
+ type: 'tool_call',
+ id: toolUseId,
+ turnId,
+ ts: now,
+ toolName: tool.name,
+ ...activityIdentity,
+ ...(tool.activityKind ? { activityKind: tool.activityKind } : {}),
+ ...(tool.displayName ? { displayName: tool.displayName } : {}),
+ 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 } : {}),
+ };
+ let callMessageAppended = false;
+ const appendCallMessage = async (): Promise<void> => {
+ if (callMessageAppended) return;
+ await this.input.appendMessage(callMsg);
+ callMessageAppended = true;
+ };
+ const emitToolStartedTrace = (): void => {
+ trace?.emit('tool', 'tool_started', 'Tool execution started', {
+ toolUseId,
+ toolName: tool.name,
+ ...(tool.categoryHint !== undefined ? { categoryHint: tool.categoryHint } : {}),
+ });
};
/**
* One pre-dispatch refusal: the call fact on the generic lane, then the
@@ -1135,10 +1210,13 @@ export class ToolRuntime {
text: string,
sandboxFailure?: Extract<ToolResultContent, { kind: 'text' }>['sandboxFailure'],
): Promise<void> => {
- pushCallEvent('preflight');
+ await appendCallMessage();
+ publishCallEvent(buildCallEvent('preflight'));
+ emitToolStartedTrace();
await this.writeSyntheticToolResult(
toolUseId,
turnId,
+ tool.name,
text,
queue,
undefined,
@@ -1147,29 +1225,6 @@ export class ToolRuntime {
activityIdentity,
);
};
- const callMsg: ToolCallMessage = {
- type: 'tool_call',
- id: toolUseId,
- turnId,
- ts: now,
- toolName: tool.name,
- ...activityIdentity,
- ...(tool.activityKind ? { activityKind: tool.activityKind } : {}),
- ...(tool.displayName ? { displayName: tool.displayName } : {}),
- 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);
- trace?.emit('tool', 'tool_started', 'Tool execution started', {
- toolUseId,
- toolName: tool.name,
- ...(tool.categoryHint !== undefined ? { categoryHint: tool.categoryHint } : {}),
- });
if (admissionFailure) {
const boundaryKind = boundaryAuthorityAttempt ? ('invalid' as const) : undefined;
if (boundaryKind) {
@@ -1405,7 +1460,7 @@ export class ToolRuntime {
try {
durableAttempt = await this.prepareDurableToolAttempt({
tool,
- startEvent: pushCallEvent('dispatch'),
+ startEvent: buildCallEvent('dispatch'),
persistedArgs,
modelFacingArgs,
abortSignal: ctx.abortSignal,
@@ -1420,6 +1475,9 @@ export class ToolRuntime {
await disposeManagedMutationAdmission(managedMutationAdmission);
throw error;
}
+ await appendCallMessage();
+ publishCallEvent(buildCallEvent('dispatch'));
+ emitToolStartedTrace();
if (durableAttempt) {
this.durableToolAttempts.set(durableAttemptKey(turnId, toolUseId), durableAttempt);
}
@@ -1468,7 +1526,7 @@ export class ToolRuntime {
toolCallId: toolUseId,
// The id the call event actually carries, not the candidate: by here
// `prepareDurableToolAttempt` has pushed it on the dispatch lane.
- ...(pushedCallEvent?.operationId ? { operationId: pushedCallEvent.operationId } : {}),
+ ...(callEvent?.operationId ? { operationId: callEvent.operationId } : {}),
abortSignal: ctx.abortSignal,
emitOutput: output.emit,
emitProgress: (current, total) => {
@@ -1549,10 +1607,13 @@ export class ToolRuntime {
const content = immutableSnapshot
? Object.freeze(coerceResultContent(result))
: coerceResultContent(result);
+ const projected = this.projectToolResult(tool, turnId, toolUseId, executionArgs, result);
+ const modelProjection = isPromiseLike(projected) ? await projected : projected;
const outcome = {
content,
isError: deriveToolResultStatus(content, result) !== 'success',
durationMs: this.input.now() - startedAt,
+ modelProjection,
};
const value = {
result,
@@ -1593,6 +1654,7 @@ export class ToolRuntime {
content: value.outcome.content,
isError: value.outcome.isError,
durationMs: value.outcome.durationMs,
+ modelProjection: value.outcome.modelProjection,
};
} finally {
if (operationLifecycle.state === 'running') {
@@ -1664,7 +1726,7 @@ export class ToolRuntime {
}
const { result, outcome } = settledExecution.value;
output.flush();
- const { content, durationMs } = outcome;
+ const { content, durationMs, modelProjection } = outcome;
// Keep the full provider-facing terminal classification. `isError` is
// sufficient for the durable response envelope, but it intentionally
// collapses `aborted` into an error bit and therefore cannot drive live
@@ -1681,12 +1743,14 @@ export class ToolRuntime {
settledExecution.durableOutcome,
content,
outcome.isError,
+ modelProjection,
durationMs,
);
} else {
durableOutcome = await durableAttempt?.commitOutcome(
content,
outcome.isError,
+ modelProjection,
durationMs,
);
}
@@ -1732,6 +1796,7 @@ export class ToolRuntime {
...(durableOutcome ? { operationId: durableOutcome.operationId } : {}),
isError: toolResultStatus !== 'success',
content,
+ modelProjection,
durationMs,
...activityIdentity,
} satisfies ToolResultEvent);
@@ -1846,9 +1911,19 @@ export class ToolRuntime {
);
}
const durationMs = Math.max(0, this.input.now() - startedAt);
+ const terminalResult = this.errorReturn(terminalFailure.message);
+ const projected = this.projectToolResult(
+ tool,
+ turnId,
+ toolUseId,
+ executionArgs,
+ terminalResult,
+ );
+ const modelProjection = isPromiseLike(projected) ? await projected : projected;
const durableOutcome = await durableAttempt?.commitOutcome(
terminalFailure.content,
true,
+ modelProjection,
durationMs,
);
const resultMsg: ToolResultMessage = {
@@ -1872,6 +1947,7 @@ export class ToolRuntime {
...(durableOutcome ? { operationId: durableOutcome.operationId } : {}),
isError: true,
content: terminalFailure.content,
+ modelProjection,
durationMs,
...activityIdentity,
} satisfies ToolResultEvent);
@@ -1902,7 +1978,7 @@ export class ToolRuntime {
errorClass,
...(sandboxError ? { sandbox: sandboxError } : {}),
});
- return this.errorReturn(terminalFailure.message);
+ return terminalResult;
}
const msg =
err instanceof ToolResultLimitError
@@ -1915,6 +1991,7 @@ export class ToolRuntime {
await this.writeSyntheticToolResult(
toolUseId,
turnId,
+ tool.name,
msg,
queue,
sandboxDenialSignalFromError(err),
@@ -2049,6 +2126,7 @@ export class ToolRuntime {
actions: {
toolDispatch: {
protocol: TOOL_BOUNDARY_PROTOCOL_V1,
+ resultProjectionVersion: 1,
operationId,
providerToolCallId: input.startEvent.toolUseId,
toolName: input.tool.name,
@@ -2109,6 +2187,7 @@ export class ToolRuntime {
const buildResponseEvent = (
result: unknown,
isError: boolean,
+ modelProjection: DurableToolResultProjection,
durationMs: number | undefined,
ts: number,
): RuntimeEvent => ({
@@ -2129,6 +2208,7 @@ export class ToolRuntime {
name: input.tool.name,
result,
...(isError ? { isError: true } : {}),
+ modelProjection,
},
refs: {
operationId,
@@ -2146,9 +2226,15 @@ export class ToolRuntime {
return {
operationId,
responseEventId: `${operationId}_response`,
- commitOutcome: async (result, isError, durationMs) => {
+ commitOutcome: async (result, isError, modelProjection, durationMs) => {
if (committedOutcome) return committedOutcome;
- const responseEvent = buildResponseEvent(result, isError, durationMs, this.input.now());
+ const responseEvent = buildResponseEvent(
+ result,
+ isError,
+ modelProjection,
+ durationMs,
+ this.input.now(),
+ );
try {
await sink.commitToolOutcome({
operationId,
@@ -2169,9 +2255,9 @@ export class ToolRuntime {
);
return committedOutcome;
},
- adoptCommittedOutcome: (event, result, isError, durationMs) => {
+ adoptCommittedOutcome: (event, result, isError, modelProjection, durationMs) => {
if (committedOutcome) return committedOutcome;
- const expected = buildResponseEvent(result, isError, durationMs, event.ts);
+ const expected = buildResponseEvent(result, isError, modelProjection, durationMs, event.ts);
if (!Number.isFinite(event.ts) || !isDeepStrictEqual(event, expected)) {
throw new RuntimeCommitBoundaryError(
'T2',
@@ -3249,6 +3335,7 @@ function normalizeManagedMutationSettlement(
const expectedError = kind === 'operation_failed_no_effect_committed';
if (
response?.kind !== 'function_response' ||
+ response.modelProjection === undefined ||
(expectedError ? response.isError !== true : response.isError === true)
) {
throw new Error('Managed no-effect settlement has the wrong durable outcome state');
@@ -3257,6 +3344,7 @@ function normalizeManagedMutationSettlement(
const outcome = Object.freeze({
content,
isError: expectedError,
+ modelProjection: response.modelProjection,
durationMs:
typeof durableOutcome.actions?.stateDelta?.durationMs === 'number'
? durableOutcome.actions.stateDelta.durationMs
@@ -3709,6 +3797,15 @@ function providerToolErrorMessage(output: unknown): string | undefined {
return record.error;
}
+function isPromiseLike<T>(value: T | PromiseLike<T>): value is PromiseLike<T> {
+ return (
+ typeof value === 'object' &&
+ value !== null &&
+ 'then' in value &&
+ typeof (value as { then?: unknown }).then === 'function'
+ );
+}
+
function summarizeArgs(toolName: string, args: unknown): string {
const projected =
toolName === 'WebSearch'
diff --git a/packages/storage/src/__tests__/artifact-attachments.test.ts b/packages/storage/src/__tests__/artifact-attachments.test.ts
index a84c3366e9..ddc63dd389 100644
--- a/packages/storage/src/__tests__/artifact-attachments.test.ts
+++ b/packages/storage/src/__tests__/artifact-attachments.test.ts
@@ -28,9 +28,13 @@ import { type StorageRef } from '@maka/core/events';
import {
createArtifactAttachmentResourceReader,
createAttachmentByteReader,
+ createReadImageSnapshotPlanner,
createReadImageSnapshotter,
} from '../artifact-attachments.js';
-import { createSqliteArtifactStore as createArtifactStore } from '../artifact-store.js';
+import {
+ createSqliteArtifactStoreWriteAuthority,
+ type ArtifactAuthorityStore,
+} from '../artifact-store.js';
const png = Uint8Array.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
@@ -220,6 +224,76 @@ describe('artifact attachment authority', () => {
assert.deepEqual(await store.list('session-1'), []);
});
});
+
+ test('snapshotter reuses one content-addressed artifact for the same turn image', async () => {
+ await withStore(async (store) => {
+ const snapshot = createReadImageSnapshotter(store);
+ const input = {
+ sessionId: 'session-1',
+ turnId: 'turn-1',
+ name: 'Tool Result image',
+ bytes: Uint8Array.from([1, 2, 3]),
+ mimeType: 'image/png',
+ };
+
+ const first = await snapshot(input);
+ const repeated = await snapshot(input);
+
+ assert.deepEqual(repeated, first);
+ assert.equal((await store.list('session-1')).length, 1);
+ });
+ });
+
+ test('keeps a durable projection image live when a user requests deletion', async () => {
+ await withStore(async (store) => {
+ const ref = await createReadImageSnapshotter(store)({
+ sessionId: 'session-1',
+ turnId: 'turn-1',
+ name: 'Tool Result image',
+ bytes: png,
+ mimeType: 'image/png',
+ });
+
+ assert.deepEqual(await store.deleteUserArtifactInSession('session-1', ref.relativePath), {
+ kind: 'protected',
+ });
+ assert.deepEqual(await store.readBinary(ref.relativePath), {
+ ok: true,
+ base64: Buffer.from(png).toString('base64'),
+ mimeType: 'image/png',
+ });
+ });
+ });
+
+ test('planner derives the final ref without publishing before commit', async () => {
+ await withStore(async (store) => {
+ const bytes = png.slice();
+ const input = {
+ sessionId: 'session-1',
+ turnId: 'turn-1',
+ name: 'Tool Result image',
+ bytes,
+ mimeType: 'image/png',
+ };
+ const plan = createReadImageSnapshotPlanner(store)(input);
+
+ assert.deepEqual(await store.list('session-1'), []);
+ bytes[0] = 0;
+ input.name = 'mutated after prepare';
+ await Promise.all([plan.persist(), plan.persist()]);
+ const published = await store.list('session-1');
+ assert.deepEqual(
+ published.map((artifact) => artifact.id),
+ [plan.ref.relativePath],
+ );
+ assert.equal(published[0]?.name, 'Tool Result image');
+ assert.deepEqual(await store.readBinary(plan.ref.relativePath), {
+ ok: true,
+ base64: Buffer.from(png).toString('base64'),
+ mimeType: 'image/png',
+ });
+ });
+ });
});
function sessionFileRef(relativePath: string, sessionId = 'session-1'): StorageRef {
@@ -230,15 +304,15 @@ function sessionContextRef(refId: string, sessionId = 'session-1'): StorageRef {
return { kind: 'session_context', sessionId, refId };
}
-async function withStore(
- run: (store: ReturnType<typeof createArtifactStore>) => Promise<void>,
-): Promise<void> {
+async function withStore(run: (store: ArtifactAuthorityStore) => Promise<void>): Promise<void> {
const root = await mkdtemp(join(tmpdir(), 'maka-artifact-attachment-'));
- const store = createArtifactStore(root);
+ const authority = createSqliteArtifactStoreWriteAuthority(root);
try {
+ await authority.recover();
+ const { store } = authority;
await run(store);
} finally {
- store.close?.();
+ authority.close();
await rm(root, { recursive: true, force: true });
}
}
diff --git a/packages/storage/src/__tests__/sqlite-runtime-store.test.ts b/packages/storage/src/__tests__/sqlite-runtime-store.test.ts
index be1d955a10..3fa3060403 100644
--- a/packages/storage/src/__tests__/sqlite-runtime-store.test.ts
+++ b/packages/storage/src/__tests__/sqlite-runtime-store.test.ts
@@ -420,8 +420,16 @@ describe('SqliteRuntimeStore', () => {
it('commits function_response, outcome journal fact, and projection atomically in T2', async () => {
await withStore(async (store) => {
- await commitPrepared(store);
- const outcome = functionResponseEvent();
+ await commitPrepared(store, { resultProjectionVersion: 1 });
+ const outcome = functionResponseEvent({
+ content: {
+ kind: 'function_response',
+ id: 'provider-call-1',
+ name: 'Read',
+ result: 'private execution contents',
+ modelProjection: { version: 1, kind: 'text', text: 'bounded model contents' },
+ },
+ });
const result = await store.commitToolOutcome({
operationId: 'operation-1',
@@ -434,7 +442,19 @@ describe('SqliteRuntimeStore', () => {
assert.equal(result.runtimeEventSeq, 3);
assert.deepEqual(await store.readRuntimeEvents('session-1', 'run-1'), [
functionCallEvent(),
- toolDispatchEvent(),
+ toolDispatchEvent({
+ actions: {
+ toolDispatch: {
+ protocol: 't1_after_preflight_v1',
+ operationId: 'operation-1',
+ providerToolCallId: 'provider-call-1',
+ toolName: 'Read',
+ canonicalArgsHash: READ_ARGS_HASH,
+ recoveryMode: 'replay_safe',
+ resultProjectionVersion: 1,
+ },
+ },
+ }),
outcome,
]);
assert.equal((await store.readImmutableRuntimeEvents('session-1', 'run-1')).length, 3);
@@ -461,6 +481,32 @@ describe('SqliteRuntimeStore', () => {
});
});
+ it('keeps projected T2 prepared when its atomic model projection is missing', async () => {
+ await withStore(async (store) => {
+ await commitPrepared(store, { resultProjectionVersion: 1 });
+
+ await assert.rejects(
+ store.commitToolOutcome({
+ operationId: 'operation-1',
+ journalEventId: 'operation-1_outcome',
+ runtimeEvent: functionResponseEvent(),
+ committedAt: 20,
+ }),
+ /requires its durable model projection/,
+ );
+
+ assert.deepEqual(
+ (await store.readRuntimeEvents('session-1', 'run-1')).map((event) => event.id),
+ ['call-event-1', 'dispatch-event-1'],
+ );
+ assert.equal((await store.readToolOperation('operation-1'))?.currentState, 'prepared');
+ assert.deepEqual(
+ (await store.readToolJournal('operation-1')).map((event) => event.state),
+ ['prepared'],
+ );
+ });
+ });
+
it('rolls back T2 without hiding the previously committed prepared boundary', async () => {
await withStore(async (store, _dbPath, setFailpoint) => {
await commitPrepared(store);
@@ -2069,12 +2115,26 @@ function toolDispatchEvent(overrides: Partial<RuntimeEvent> = {}): RuntimeEvent
};
}
-function commitPrepared(store: Store) {
+function commitPrepared(store: Store, options: { resultProjectionVersion?: 1 } = {}) {
return store.commitToolPrepared({
operationId: 'operation-1',
journalEventId: 'operation-1_prepared',
runtimeEvent: functionCallEvent(),
- dispatchRuntimeEvent: toolDispatchEvent(),
+ dispatchRuntimeEvent: toolDispatchEvent({
+ actions: {
+ toolDispatch: {
+ protocol: 't1_after_preflight_v1',
+ operationId: 'operation-1',
+ providerToolCallId: 'provider-call-1',
+ toolName: 'Read',
+ canonicalArgsHash: READ_ARGS_HASH,
+ recoveryMode: 'replay_safe',
+ ...(options.resultProjectionVersion !== undefined
+ ? { resultProjectionVersion: options.resultProjectionVersion }
+ : {}),
+ },
+ },
+ }),
providerToolCallId: 'provider-call-1',
toolName: 'Read',
canonicalArgsHash: READ_ARGS_HASH,
diff --git a/packages/storage/src/artifact-attachments.ts b/packages/storage/src/artifact-attachments.ts
index c274c0c628..d281fe8206 100644
--- a/packages/storage/src/artifact-attachments.ts
+++ b/packages/storage/src/artifact-attachments.ts
@@ -23,6 +23,12 @@ import {
READ_IMAGE_TOO_LARGE_MESSAGE,
type AttachmentByteReader,
} from '@maka/core/attachments';
+import {
+ isArtifactTurnKey,
+ isCanonicalArtifactEntityId,
+ normalizeArtifactImagePreviewMime,
+} from '@maka/core/artifacts';
+import { createHash } from 'node:crypto';
import { type StorageRef, type ToolResultContent } from '@maka/core/events';
import type { ReadImageSnapshotReader } from '@maka/core/context-offload';
import type {
@@ -30,6 +36,7 @@ import type {
ArtifactStore,
DurableArtifactAttachmentReader,
} from './artifact-store.js';
+import { sanitizeArtifactName } from './artifact-store.js';
export interface ArtifactAttachmentResourceReader {
readAttachmentResource(
@@ -119,30 +126,87 @@ export function createAttachmentByteReader(input: {
};
}
-export function createReadImageSnapshotter(artifactStore: Pick<ArtifactStore, 'create'>) {
- return async (input: {
- sessionId: string;
- turnId: string;
- name: string;
- bytes: Uint8Array;
- mimeType: string;
- }): Promise<Extract<StorageRef, { kind: 'session_file' }>> => {
+interface ReadImageSnapshotInput {
+ sessionId: string;
+ turnId: string;
+ name: string;
+ bytes: Uint8Array;
+ mimeType: string;
+}
+
+export interface ReadImageSnapshotPlan {
+ ref: Extract<StorageRef, { kind: 'session_file' }>;
+ persist(): Promise<void>;
+}
+
+export function createReadImageSnapshotPlanner(artifactStore: Pick<ArtifactStore, 'create'>) {
+ return (input: ReadImageSnapshotInput): ReadImageSnapshotPlan => {
if (input.bytes.byteLength > MAX_READ_IMAGE_BYTES) {
throw new Error(READ_IMAGE_TOO_LARGE_MESSAGE);
}
- const artifact = await artifactStore.create({
+ if (normalizeArtifactImagePreviewMime(input.mimeType) !== input.mimeType) {
+ throw new Error('Image media type is not canonical or safe');
+ }
+ if (!isCanonicalArtifactEntityId(input.sessionId)) {
+ throw new Error('Image Session id is not canonical');
+ }
+ if (!isArtifactTurnKey(input.turnId)) {
+ throw new Error('Image turn id is not canonical');
+ }
+ const accepted = Object.freeze({
sessionId: input.sessionId,
turnId: input.turnId,
- name: input.name,
- kind: 'image',
- content: input.bytes,
+ name: sanitizeArtifactName(input.name),
+ bytes: input.bytes.slice(),
mimeType: input.mimeType,
- source: 'tool_result',
});
- return {
- kind: 'session_file',
- sessionId: input.sessionId,
- relativePath: artifact.id,
- };
+ const id = `image_${createHash('sha256')
+ .update(accepted.sessionId, 'utf8')
+ .update('\0', 'utf8')
+ .update(accepted.turnId, 'utf8')
+ .update('\0', 'utf8')
+ .update(accepted.name, 'utf8')
+ .update('\0', 'utf8')
+ .update(accepted.mimeType, 'utf8')
+ .update('\0', 'utf8')
+ .update(accepted.bytes)
+ .digest('hex')}`;
+ let publication: Promise<void> | undefined;
+ const ref = Object.freeze({
+ kind: 'session_file' as const,
+ sessionId: accepted.sessionId,
+ relativePath: id,
+ });
+ return Object.freeze({
+ ref,
+ persist() {
+ publication ??= artifactStore
+ .create({
+ id,
+ sessionId: accepted.sessionId,
+ turnId: accepted.turnId,
+ name: accepted.name,
+ kind: 'image',
+ content: accepted.bytes,
+ mimeType: accepted.mimeType,
+ source: 'tool_result_projection',
+ })
+ .then((artifact) => {
+ if (artifact.id !== id) throw new Error('Artifact publication changed its planned id');
+ });
+ return publication;
+ },
+ });
+ };
+}
+
+export function createReadImageSnapshotter(artifactStore: Pick<ArtifactStore, 'create'>) {
+ const planSnapshot = createReadImageSnapshotPlanner(artifactStore);
+ return async (
+ input: ReadImageSnapshotInput,
+ ): Promise<Extract<StorageRef, { kind: 'session_file' }>> => {
+ const plan = planSnapshot(input);
+ await plan.persist();
+ return plan.ref;
};
}
diff --git a/packages/storage/src/artifact-stores.ts b/packages/storage/src/artifact-stores.ts
index 99a5e1f232..9fbfa807da 100644
--- a/packages/storage/src/artifact-stores.ts
+++ b/packages/storage/src/artifact-stores.ts
@@ -41,8 +41,10 @@ import {
export {
createArtifactAttachmentResourceReader,
createAttachmentByteReader,
+ createReadImageSnapshotPlanner,
createReadImageSnapshotter,
type ArtifactAttachmentResourceReader,
+ type ReadImageSnapshotPlan,
} from './artifact-attachments.js';
export { persistProviderRequestCaptureArtifact } from './provider-request-capture-artifact.js';
diff --git a/packages/storage/src/sqlite-runtime-store.ts b/packages/storage/src/sqlite-runtime-store.ts
index c060f26833..c2c459e46c 100644
--- a/packages/storage/src/sqlite-runtime-store.ts
+++ b/packages/storage/src/sqlite-runtime-store.ts
@@ -2799,6 +2799,13 @@ export class SqliteRuntimeStore
const dispatchEvent = dispatchJson
? decodeRuntimeEvent(JSON.parse(dispatchJson) as unknown)
: undefined;
+ if (
+ dispatchEvent?.actions?.toolDispatch?.resultProjectionVersion === 1 &&
+ input.runtimeEvent.content?.kind === 'function_response' &&
+ input.runtimeEvent.content.modelProjection === undefined
+ ) {
+ throw new Error('Projected Tool Result T2 requires its durable model projection');
+ }
if (dispatchEvent?.actions?.toolDispatch?.managedMutation) {
const reservation = this.db
.prepare(`
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org