Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,7 @@ export type ToolResultContent =
toolCallId: string;
toolName: string;
artifactId?: string;
resourceRef?: string;
bodySha256?: string;
originalEstimatedTokens: number;
originalBytes: number;
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/tool-result-archive-evidence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import type { AgentRunEvent } from './agent-run.js';
export type ToolResultArchiveEvidence =
| {
readonly ok: true;
/** Reconstruction view: required event identity/projection only; raw result is null. */
readonly event: RuntimeEvent;
readonly transitions: readonly AgentRunEvent[];
readonly storedBytes?: number;
}
| { readonly ok: false; readonly reason: 'not_found' | 'too_large' | 'corrupt' | 'unavailable' };

Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/tool-result-record-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const ARCHIVED_SHAPE = defineObjectShape<Result<'archived_tool_result'>>()(
'rewriteVersion',
'reason',
],
['artifactId', 'bodySha256'],
['artifactId', 'resourceRef', 'bodySha256'],
);
const IMAGE_SHAPE = defineObjectShape<Result<'image'>>()(['kind', 'mimeType', 'ref'], []);
const SUMMARY_SHAPE = defineObjectShape<Result<'summary'>>()(
Expand Down Expand Up @@ -242,6 +242,7 @@ function isNonShellToolResultContent(value: unknown): value is ToolResultContent
typeof value.toolCallId === 'string' &&
typeof value.toolName === 'string' &&
isOptionalString(value.artifactId) &&
isOptionalString(value.resourceRef) &&
isOptionalString(value.bodySha256) &&
isFiniteNumber(value.originalEstimatedTokens) &&
isFiniteNumber(value.originalBytes) &&
Expand Down
235 changes: 231 additions & 4 deletions packages/runtime-host/src/__tests__/execution-artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@

import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { DatabaseSync } from 'node:sqlite';
import { openToolResultArchiveEvidenceReader } from '@maka/storage/tool-result-archive-evidence';
import {
buildModelProjectionTransition,
durableToolResultProjectionDigest,
} from '@maka/core/model-projection-transition';
import { buildLedgerArchivedToolResultPlaceholder } from '@maka/runtime/tool-result-archive';
import type { DurableToolResultProjection } from '@maka/core/durable-tool-result-projection';
import { parseToolResultArchiveResourceRef } from '@maka/runtime/tool-result-archive-resource';
import { mkdir, mkdtemp, rm, stat, truncate, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
Expand All @@ -29,12 +38,222 @@ import {
createReadImageSnapshotPlanner,
} from '@maka/storage/artifact-stores';
import { encodeDurableToolResultOutputWithArtifacts } from '@maka/runtime/durable-tool-result-projection';
import { durableProjectionToToolResultOutput } from '@maka/runtime/durable-tool-result-projection';
import { deferred } from '@maka/core/test-only/async-primitives';
import { resolveStorageRoot, tryAcquireInteractiveRootOwner } from '@maka/storage/root-authority';
import { createHostExecutionArtifactServices } from '../server/execution-artifacts.js';
import { restoreArtifactV1Shape } from './fixtures/artifact-v1.js';
import { SessionAdmissionGate } from '../server/session-admission-gate.js';

for (const largeImage of [false, true]) {
test(`production archives survive reopen (large raw MCP image: ${largeImage})`, async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-ledger-archive-host-'));
const owner = await tryAcquireInteractiveRootOwner(
await resolveStorageRoot({ path: root, kind: 'interactive' }),
);
assert.ok(owner);
const artifacts = await openInteractiveArtifactStoreForWrite(owner.lease);
let evidence = await openToolResultArchiveEvidenceReader(owner.lease);
const db = new DatabaseSync(join(root, 'runtime.sqlite'));
try {
const projection: DurableToolResultProjection = largeImage
? {
version: 1,
kind: 'content',
parts: [
{
kind: 'artifact',
mediaType: 'image/png',
ref: { kind: 'session_file', sessionId: 'session', relativePath: 'mcp-image' },
},
],
}
: { version: 1, kind: 'text', text: 'durable ledger body' };
const output = durableProjectionToToolResultOutput(projection);
assert.ok('value' in output);
const serializedResult = JSON.stringify(output.value);
const bodySha256 = createHash('sha256').update(serializedResult).digest('hex');
const event = {
id: 'response',
sessionId: 'session',
runId: 'run',
invocationId: 'invocation',
turnId: 'turn',
ts: 1,
partial: false,
author: 'tool',
role: 'tool',
content: {
kind: 'function_response',
id: 'call',
name: 'Read',
result: largeImage
? {
content: [
{
type: 'image',
mimeType: 'image/png',
data: Buffer.alloc(2 * 1024 * 1024).toString('base64'),
},
],
}
: 'raw execution body',
modelProjection: projection,
},
};
db.prepare(
'INSERT INTO runtime_events(event_id, session_id, invocation_id, run_id, turn_id, event_seq, event_kind, payload_json, committed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
).run(
'response',
'session',
'invocation',
'run',
'turn',
1,
'function_response',
JSON.stringify(event),
1,
);
const projectedEvidence = await evidence.read({
sessionId: 'session',
runtimeEventId: 'response',
});
assert.ok(projectedEvidence.ok);
assert.ok(projectedEvidence.storedBytes! < 4096);
assert.equal(
projectedEvidence.event.content?.kind === 'function_response'
? projectedEvidence.event.content.result
: undefined,
null,
);
const old = await artifacts.create({
id: 'legacy-archive',
sessionId: 'session',
turnId: 'turn',
name: 'legacy.json',
kind: 'file',
content: serializedResult,
source: 'tool_result_archive',
});
const input = {
sessionId: 'session',
runtimeEventId: 'response',
turnId: 'turn',
toolCallId: 'call',
toolName: 'Read',
serializedResult,
bodySha256,
originalBytes: Buffer.byteLength(serializedResult),
originalEstimatedTokens: 10,
rewriteVersion: 1,
sourceProjectionDigest: durableToolResultProjectionDigest(projection),
reason: 'stale_tool_result_pruned_before_compact' as const,
};
const make = () =>
createHostExecutionArtifactServices({
artifacts,
archiveEvidence: evidence,
sessionAdmission: new SessionAdmissionGate(),
sessions: { probeSessionRemoval: async () => ({ kind: 'present' }) },
requestDrain: () => assert.fail('archive failure must not drain'),
});
let services = make();
const prepared = await services.toolResultArchive.services.archiveToolResult(input);
assert.ok(prepared?.ledger);
assert.equal(typeof prepared.commitTransition, 'function');
assert.equal((await artifacts.listPage('session', { offset: 0, limit: 10 })).total, 1);
const placeholder = buildLedgerArchivedToolResultPlaceholder({ ...input, storage: 'ledger' });
const transition = buildModelProjectionTransition({
sessionId: 'session',
target: {
runtimeEventId: 'response',
part: 'tool_result',
toolCallId: 'call',
toolName: 'Read',
},
sourceProjection: projection,
replacement: { version: 1, kind: 'json', value: placeholder as never },
now: 2,
});
db.prepare(
'INSERT INTO core_agent_runs(session_id, run_id, created_at) VALUES (?, ?, ?)',
).run('session', 'run', 1);
const persist = async () => {
db.prepare('INSERT INTO core_agent_run_events VALUES (?, ?, ?, ?, ?, ?, ?)').run(
'session',
'run',
1,
transition.transitionId,
'model_projection_transition_recorded',
2,
JSON.stringify({
id: transition.transitionId,
type: 'model_projection_transition_recorded',
sessionId: 'session',
runId: 'run',
turnId: 'turn',
ts: 2,
data: { runtimeEventId: 'response', part: 'tool_result', transition },
}),
);
};
assert.equal(await prepared.commitTransition!(transition, persist), true);
assert.equal(
await prepared.commitTransition!(transition, async () =>
assert.fail('stale preparation must not append'),
),
false,
);
evidence.close();
evidence = await openToolResultArchiveEvidenceReader(owner.lease);
services = make();
assert.deepEqual(
await services.toolResultArchive.services.readToolResultArchive({
...placeholder,
sessionId: 'session',
}),
{ ok: true, serializedResult },
);
assert.equal(
(
await services.toolResultArchive.services.readToolResultArchive({
...placeholder,
sessionId: 'other',
})
).ok,
false,
);
const identity = parseToolResultArchiveResourceRef(placeholder.resourceRef!);
assert.ok(identity);
assert.deepEqual(
await services.toolResultArchive.services.readArchivedToolResultResource({
...identity,
sessionId: 'session',
maxBytes: input.originalBytes,
}),
{ ok: true, serializedResult },
);
assert.deepEqual(
await services.toolResultArchive.services.readArchivedToolResultResource({
artifactId: old.id,
bodySha256,
originalBytes: input.originalBytes,
sessionId: 'session',
maxBytes: input.originalBytes,
}),
{ ok: true, serializedResult },
);
} finally {
db.close();
evidence.close();
artifacts.close();
await owner.close();
await rm(root, { recursive: true, force: true });
await rm(owner.controlDirectory, { recursive: true, force: true });
}
});
}

test('a refused projection preserves a shared image until Session cleanup', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-shared-projection-'));
const owner = await tryAcquireInteractiveRootOwner(
Expand Down Expand Up @@ -199,12 +418,20 @@ test('Hosted execution publishes contained Tool Artifacts and durable result arc
reason: 'stale_tool_result_pruned_before_compact' as const,
bodySha256,
};
const archived = await services.toolResultArchive.services.archiveToolResult(archiveInput);
assert.ok(archived, 'the host archive writer always reports where it stored the body');
assert.deepEqual(
assert.equal(
await services.toolResultArchive.services.archiveToolResult(archiveInput),
archived,
undefined,
'without ledger evidence the Host must not fall back to publishing an Artifact',
);
const legacy = await store.create({
sessionId: archiveInput.sessionId,
turnId: archiveInput.turnId,
name: 'legacy.json',
kind: 'file',
content: serializedResult,
source: 'tool_result_archive',
});
const archived = { artifactId: legacy.id };
assert.deepEqual(
await services.toolResultArchive.services.readToolResultArchive({
...archiveInput,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ const MAX_IMPLEMENTATION_CHILD_REQUESTS =
const HEADLESS_CODING_V1_PROMPT_HASH =
'sha256:b2773282ac4755dc8d8a663eafdec68c3fa6f5680ec8557d261b5f723672b467';
const HEADLESS_CODING_V1_TOOLS_HASH =
'sha256:aa3ab56a7b67dde133fffe885f4def81735c93015202e31ecb339a84863f6d03';
// ArchiveRead now describes both ledger and legacy resource references.
'sha256:22809de022f9c46186cae986eda23438efe9dbe6856b57abb0613ea48b51ad9c';
const execFileAsync = promisify(execFile);
test('backend creation resolves a bound Session by immutable Connection identity', async () => {
let observedRef: unknown;
Expand Down
3 changes: 2 additions & 1 deletion packages/runtime-host/src/protocol/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const;
export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const;
// Increment when the same protocol version no longer guarantees safe Client-Host
// interoperability. Mismatches are rejected before domain commands are admitted.
export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 131 as const;
// 132: new Tool Result archives use versioned ledger references, not Artifact payloads.
export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 132 as const;
// 131: Logical model steps bind durable Request Composition identities.
// 130: Turn contributions carry the optional bounded `failureMessage` diagnostic.
// Epoch-129 peers reject this added field on the strict contribution shape.
Expand Down
Loading