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/09/13 03:09:09 UTC
(maka) branch refactor/tool-results-unified-read created (now e6bdf600bb)
This is an automated email from the ASF dual-hosted git repository.
Astro-Han pushed a change to branch refactor/tool-results-unified-read
in repository https://gitbox.apache.org/repos/asf/maka.git
at e6bdf600bb refactor(runtime): unify bounded Read and tool-result pruning
This branch includes the following new commits:
new e6bdf600bb refactor(runtime): unify bounded Read and tool-result pruning
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 01/01: refactor(runtime): unify bounded Read and tool-result pruning
Posted by as...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
Astro-Han pushed a commit to branch refactor/tool-results-unified-read
in repository https://gitbox.apache.org/repos/asf/maka.git
commit e6bdf600bbb6d410b2d28a8ded11cbc8be346c61
Author: AstroHan <le...@outlook.com>
AuthorDate: Sun Sep 13 11:08:47 2026 +0800
refactor(runtime): unify bounded Read and tool-result pruning
Generated-by: Codex
---
.../llm-compaction-events-log-projection-draft.md | 8 +-
...compaction-events-log-projection-draft.zh-CN.md | 4 +-
packages/core/src/events.ts | 1 +
packages/core/src/permission.ts | 1 -
packages/core/src/tool-result-record-schema.ts | 3 +-
packages/eval/README.md | 2 +-
.../src/__tests__/execution-artifacts.test.ts | 22 +-
.../__tests__/execution-model-composition.test.ts | 17 +-
.../hosted-execution-tool-profile.test.ts | 13 +-
.../runtime-host/src/server/execution-artifacts.ts | 10 +-
.../src/server/hosted-execution-tool-profile.ts | 16 +-
packages/runtime/package.json | 1 +
.../src/__tests__/active-tool-result-prune.test.ts | 873 ---------------------
.../runtime/src/__tests__/ai-sdk-backend.test.ts | 105 ++-
.../__tests__/builtin-tools-file-worker.test.ts | 2 +-
.../runtime/src/__tests__/builtin-tools.test.ts | 156 +---
.../context-budget-mid-turn-policy.test.ts | 9 +-
.../runtime/src/__tests__/context-budget.test.ts | 2 +-
.../src/__tests__/conversation-copy.test.ts | 94 ++-
.../__tests__/execution-boundary-test-helpers.ts | 1 -
.../src/__tests__/filesystem-authority.test.ts | 6 +-
.../ledger-tool-result-archive-reader.test.ts | 23 +-
.../__tests__/mid-turn-capacity-backend.test.ts | 29 +-
.../model-projection-transition-ledger.test.ts | 39 +-
.../__tests__/overflow-reactive-recovery.test.ts | 12 +-
packages/runtime/src/__tests__/read-page.test.ts | 87 ++
.../tool-result-archive-capability-backend.test.ts | 153 +---
.../__tests__/tool-result-archive-resource.test.ts | 526 -------------
packages/runtime/src/active-tool-result-prune.ts | 403 ----------
.../runtime/src/active-tool-result-working-set.ts | 272 -------
packages/runtime/src/ai-sdk-compaction-contract.ts | 7 +-
packages/runtime/src/ai-sdk-compaction.ts | 202 +----
packages/runtime/src/ai-sdk-turn.ts | 76 +-
packages/runtime/src/archive-read-tool.ts | 134 ----
packages/runtime/src/builtin-tools.ts | 141 +---
packages/runtime/src/compaction-boundary.ts | 5 +-
packages/runtime/src/context-budget-policy.ts | 12 +-
packages/runtime/src/context-budget.ts | 23 +-
packages/runtime/src/conversation-copy.ts | 120 ++-
.../src/ledger-tool-result-archive-reader.ts | 31 +-
packages/runtime/src/read-page.ts | 261 ++++++
packages/runtime/src/request-projection.ts | 21 +-
packages/runtime/src/session-manager.ts | 10 +-
packages/runtime/src/tool-availability.ts | 1 -
.../runtime/src/tool-result-archive-capability.ts | 103 +--
.../runtime/src/tool-result-archive-resource.ts | 639 +--------------
.../runtime/src/tool-result-archive-transition.ts | 121 +--
packages/runtime/src/tool-result-archive.ts | 59 +-
48 files changed, 932 insertions(+), 3924 deletions(-)
diff --git a/docs/architecture/llm-compaction-events-log-projection-draft.md b/docs/architecture/llm-compaction-events-log-projection-draft.md
index 7b6f7d7172..0424d1132f 100644
--- a/docs/architecture/llm-compaction-events-log-projection-draft.md
+++ b/docs/architecture/llm-compaction-events-log-projection-draft.md
@@ -163,7 +163,7 @@ The prior-history path for a normal Send begins in `AiSdkBackend.buildPriorMessa
2. Prepare the context-budget policy.
3. Load the latest compatible ledger-backed checkpoint.
4. Validate and replay the existing checkpoint against the immutable RuntimeEvent sequence.
-5. Apply stale oversized Tool Result pruning only to the uncovered projected remainder.
+5. Prepare oversized text Tool Results with the same size rule for current and prior turns.
6. When an active request has a valid user-declared Maka window and the previous accepted request's real usage crosses it, select a safe prefix and retained tail.
7. If the old checkpoint does not cover the new fold, call a compactor to create a rolling successor.
8. Validate and durably record the successor before using it.
@@ -172,7 +172,7 @@ The prior-history path for a normal Send begins in `AiSdkBackend.buildPriorMessa
The ordering reveals three properties.
-First, checkpoint source matching always sees the immutable RuntimeEvent ledger. Stale Tool Result pruning shapes only the uncovered replay remainder, so a moving recent-turn window cannot invalidate an otherwise matching checkpoint by changing the bytes used for its digest.
+First, checkpoint source matching always sees the immutable RuntimeEvent ledger. Tool Result pruning records separate durable projection transitions; checkpoint replay validates both immutable coverage identity and the effective covered digest.
Second, compaction happens inside **model-history projection**, not inside the RuntimeEvent append path. Events already produced by the model and tools do not change when a later context budget changes.
@@ -453,7 +453,9 @@ Both preserve canonical source, but they do not create parallel compaction autho
Active Tool Result Prune remains a deterministic non-LLM rewrite. It archives an eligible raw Tool Result, appends a durable projection transition to the AgentRun event ledger, and derives the current request from the effective-history reducer. The same reducer supplies later replay, restart, budgeting, and compaction. Prune neither summarizes a span nor creates a checkpoint, and canonical RuntimeEvents remain unchanged.
-The placeholder carries a bounded `maka://archive/...` address and instructions for `ArchiveRead`; model replay deterministically reconstructs that address for legacy placeholders. Runtime does not eagerly expand the archived body back into every request. The model calls `ArchiveRead` only when it needs the detail, and the Host validates the Session, hash, and byte size before returning a bounded inspect or query result. A later checkpoint replaces covered placeholders with its summary a [...]
+Tool-result pruning uses one `toolResultPrune.enabled` switch and the same fixed size rule for current and prior turns. Text projections above 7,500 serialized characters become a bounded first page with a `next` Read input; images retain their existing materialization policy. `Read` accepts a required `path` and optional zero-based line `offset` and `limit`. Its serialized response is bounded even with pruning disabled or a large explicit limit. The requested range defines where paginat [...]
+
+A tool-result address is `maka://runtime/tool-results/<event-id>`. Host resolves it inside the invoking Session, verifies the accepted projection transition, and reconstructs the original model body from the event ledger. The model does not copy hashes or internal transition identity. Transition recording still precedes replacement, and replay, compression tails, and overflow retries all use the same effective-projection reducer. Checkpoint source matching continues to use immutable even [...]
## What compaction is not
diff --git a/docs/architecture/llm-compaction-events-log-projection-draft.zh-CN.md b/docs/architecture/llm-compaction-events-log-projection-draft.zh-CN.md
index 4afee83511..2dc3514ab9 100644
--- a/docs/architecture/llm-compaction-events-log-projection-draft.zh-CN.md
+++ b/docs/architecture/llm-compaction-events-log-projection-draft.zh-CN.md
@@ -453,7 +453,9 @@ Maka 只有一套 LLM compaction 机制,以及一个相邻的 current-request
Active Tool Result Prune 仍是 deterministic、非 LLM 的 rewrite。它先归档 eligible raw Tool Result,再向 AgentRun event ledger 追加 durable projection transition,并由 effective-history reducer 生成 current request。后续 replay、restart、budgeting 与 compaction 也消费同一 reducer。Prune 既不总结 span,也不创建 checkpoint,canonical RuntimeEvents 保持不变。
-Placeholder 携带 bounded `maka://archive/...` 地址和 `ArchiveRead` 指令;model replay 会为 legacy placeholder 确定性补回这个地址。Runtime 不再把 archived body eager-expand 回每次请求。只有模型确实需要细节时才调用 `ArchiveRead`,Host 在返回 bounded inspect/query 结果前校验 Session、hash 与 byte size。后续 checkpoint 会用 summary 替换已覆盖的 placeholder,并且有意不携带 archive roots。完整 Tool Result 仍在 canonical RuntimeEvent ledger 中,但 model reachability 不会变成永久的 cross-checkpoint authority。
+工具结果裁剪只保留 `toolResultPrune.enabled`,当前与历史 turn 使用同一个固定体积规则。文本投影超过 7,500 个序列化字符时,保留有界首屏和 `next` 续读参数;图片沿用原有 materialization 策略。`Read` 只有必填的 `path` 和可选、零基行坐标的 `offset` / `limit`。关闭裁剪或传入大 limit 都不能绕过单次响应上限。显式 limit 定义本次读取范围,超长单行由 Harness 自动分段,并用正文校验过的地址续读。ArchiveRead、inspect/query/search 和 active/stale 调参字段直接删除,不保留别名。
+
+工具结果地址使用 `maka://runtime/tool-results/<event-id>`。Host 在调用方 Session 内解析地址,验证已接受的投影 transition,从原事件恢复模型正文;模型不必复写摘要或内部 transition 身份。先持久化 transition,再替换投影;历史重放、压缩尾部和溢出重试都经过同一个 reducer。Checkpoint 仍按不可变事件身份匹配,并校验有效覆盖内容的摘要。
## Compaction 不是什么
diff --git a/packages/core/src/events.ts b/packages/core/src/events.ts
index b9c25e817d..d103137354 100644
--- a/packages/core/src/events.ts
+++ b/packages/core/src/events.ts
@@ -870,6 +870,7 @@ export type ToolResultContent =
* (#4283), so the archived-result read model spans both reasons.
*/
reason:
+ | 'tool_result_pruned'
| 'stale_tool_result_pruned_before_compact'
| 'active_current_turn_tool_result_pruned_before_next_step';
}
diff --git a/packages/core/src/permission.ts b/packages/core/src/permission.ts
index 6c716cc2cf..96b23e99a0 100644
--- a/packages/core/src/permission.ts
+++ b/packages/core/src/permission.ts
@@ -128,7 +128,6 @@ export interface ToolExecutionFacts {
export const BUILTIN_TOOL_CATEGORY: Record<string, ToolCategory> = {
// read
Read: 'read',
- ArchiveRead: 'read',
search_files: 'read',
Grep: 'read',
Glob: 'read',
diff --git a/packages/core/src/tool-result-record-schema.ts b/packages/core/src/tool-result-record-schema.ts
index 4f75d318f0..120cff1f9f 100644
--- a/packages/core/src/tool-result-record-schema.ts
+++ b/packages/core/src/tool-result-record-schema.ts
@@ -247,7 +247,8 @@ function isNonShellToolResultContent(value: unknown): value is ToolResultContent
isFiniteNumber(value.originalEstimatedTokens) &&
isFiniteNumber(value.originalBytes) &&
isFiniteNumber(value.rewriteVersion) &&
- (value.reason === 'stale_tool_result_pruned_before_compact' ||
+ (value.reason === 'tool_result_pruned' ||
+ value.reason === 'stale_tool_result_pruned_before_compact' ||
value.reason === 'active_current_turn_tool_result_pruned_before_next_step')
);
case 'image':
diff --git a/packages/eval/README.md b/packages/eval/README.md
index 548dffbdff..2d6e25ea0f 100644
--- a/packages/eval/README.md
+++ b/packages/eval/README.md
@@ -187,7 +187,7 @@ system prompt, disables product identity/personalization/skills/workspace-memory
admits only `Bash`, `Read`, `Write`, `Edit`, `Glob`, `Grep`, and `apply_patch` as tool candidates,
and exposes a foreground-only Bash schema without `run_in_background` or `pty`. Provider-specific
routing remains authoritative: DeepSeek Responses exposes `apply_patch` instead of `Write` and
-`Edit`, and Runtime-owned `ArchiveRead` remains available for archived tool results. A real
+`Edit`, and `Read` also accepts Session-scoped Maka tool-result paths. A real
`hosted.execution.start` regression test pins SHA-256 hashes for the first main provider request's
developer prompt and complete tool schema.
diff --git a/packages/runtime-host/src/__tests__/execution-artifacts.test.ts b/packages/runtime-host/src/__tests__/execution-artifacts.test.ts
index f939e91ec4..7fc0863ae4 100644
--- a/packages/runtime-host/src/__tests__/execution-artifacts.test.ts
+++ b/packages/runtime-host/src/__tests__/execution-artifacts.test.ts
@@ -208,22 +208,26 @@ for (const largeImage of [false, true]) {
evidence = await openToolResultArchiveEvidenceReader(owner.lease);
services = make();
assert.deepEqual(
- await services.toolResultArchive.services.readToolResultArchive({
- ...placeholder,
+ await services.toolResultArchive.services.readArchivedToolResultResource({
+ storage: 'event',
+ runtimeEventId: placeholder.runtimeEventId,
+ maxBytes: input.originalBytes,
sessionId: 'session',
}),
{ ok: true, serializedResult },
);
assert.equal(
(
- await services.toolResultArchive.services.readToolResultArchive({
- ...placeholder,
+ await services.toolResultArchive.services.readArchivedToolResultResource({
+ storage: 'event',
+ runtimeEventId: placeholder.runtimeEventId,
+ maxBytes: input.originalBytes,
sessionId: 'other',
})
).ok,
false,
);
- const identity = parseToolResultArchiveResourceRef(placeholder.resourceRef!);
+ const identity = { storage: 'event' as const, runtimeEventId: placeholder.runtimeEventId };
assert.ok(identity);
assert.deepEqual(
await services.toolResultArchive.services.readArchivedToolResultResource({
@@ -433,9 +437,9 @@ test('Hosted execution publishes contained Tool Artifacts and durable result arc
});
const archived = { artifactId: legacy.id };
assert.deepEqual(
- await services.toolResultArchive.services.readToolResultArchive({
+ await services.toolResultArchive.services.readArchivedToolResultResource({
...archiveInput,
- kind: 'maka.archived_tool_result',
+ maxBytes: archiveInput.originalBytes,
artifactId: archived.artifactId,
}),
{ ok: true, serializedResult },
@@ -451,9 +455,9 @@ test('Hosted execution publishes contained Tool Artifacts and durable result arc
requestDrain: () => assert.fail('reading an upgraded archive must not drain'),
});
assert.deepEqual(
- await successor.toolResultArchive.services.readToolResultArchive({
+ await successor.toolResultArchive.services.readArchivedToolResultResource({
...archiveInput,
- kind: 'maka.archived_tool_result',
+ maxBytes: archiveInput.originalBytes,
artifactId: archived.artifactId,
}),
{ ok: true, serializedResult },
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 ab13dcebb5..1874256b61 100644
--- a/packages/runtime-host/src/__tests__/execution-model-composition.test.ts
+++ b/packages/runtime-host/src/__tests__/execution-model-composition.test.ts
@@ -151,8 +151,8 @@ const MAX_IMPLEMENTATION_CHILD_REQUESTS =
const HEADLESS_CODING_V1_PROMPT_HASH =
'sha256:b2773282ac4755dc8d8a663eafdec68c3fa6f5680ec8557d261b5f723672b467';
const HEADLESS_CODING_V1_TOOLS_HASH =
- // ArchiveRead now describes both ledger and legacy resource references.
- 'sha256:22809de022f9c46186cae986eda23438efe9dbe6856b57abb0613ea48b51ad9c';
+ // Read uses one bounded file/resource schema and no separate archive tool.
+ 'sha256:d7115dca352c667403190156941602a098a38bf0e0ccc9bd0fcfeb04fe16df49';
const execFileAsync = promisify(execFile);
test('backend creation resolves a bound Session by immutable Connection identity', async () => {
let observedRef: unknown;
@@ -2544,7 +2544,6 @@ test('hosted execution freezes the headless coding provider wire contract', asyn
assert.equal(stableHash(instructions), HEADLESS_CODING_V1_PROMPT_HASH);
assert.equal(stableHash(tools), HEADLESS_CODING_V1_TOOLS_HASH);
assert.deepEqual(responsesToolNames(request?.body), [
- 'ArchiveRead',
'Bash',
'Edit',
'Glob',
@@ -2825,7 +2824,6 @@ test('production Host executes a canonical ai-sdk Session against a real provide
// must never see WebSearch in the effective root tool surface. Non-direct
// bound tools stay deferred behind tool_search until activated.
assert.deepEqual(toolNames(request?.body), [
- 'ArchiveRead',
'AskUserQuestion',
'Bash',
'Edit',
@@ -3333,10 +3331,8 @@ test('production Host executes a durable runnable child with an exact tool ceili
'implementation',
]);
// A child now carries the archive decoder alongside its allowlist (#2026).
- // Its own placeholders name `ArchiveRead`, so the ceiling that governs
- // agent-permission tools cannot be the thing that decides whether the child
- // can read back a result the runtime itself pruned.
- assert.deepEqual(toolNames(requests[2]?.body), ['ArchiveRead', 'Glob', 'Grep', 'Read']);
+ // The existing Read also resolves Session-scoped tool results.
+ assert.deepEqual(toolNames(requests[2]?.body), ['Glob', 'Grep', 'Read']);
assert.doesNotMatch(JSON.stringify(requests[2]?.body), /## Response format/u);
assert.ok(toolNames(requests[3]?.body).includes('agent_spawn'));
@@ -3533,7 +3529,6 @@ test('production Host publishes and retires an implementation child patch', asyn
'implementation',
]);
const childToolNames = [
- 'ArchiveRead',
'Bash',
'Edit',
'Glob',
@@ -5096,7 +5091,6 @@ function backendCreationFixture(input: {
recordToolArtifacts: async () => undefined,
toolResultArchive: createToolResultArchiveCapability({
archiveToolResult: async () => ({ artifactId: 'fixture-tool-result-archive' }),
- readToolResultArchive: async () => ({ ok: false, reason: 'not_found' }),
readArchivedToolResultResource: async () => ({ ok: false, reason: 'not_found' }),
}),
},
@@ -5733,7 +5727,7 @@ async function handleProviderRequest(
return;
}
if (flow.kind === 'child_agent' && streamRequestIndex === 3) {
- assert.deepEqual(toolNames(body), ['ArchiveRead', 'Glob', 'Grep', 'Read']);
+ assert.deepEqual(toolNames(body), ['Glob', 'Grep', 'Read']);
respondProviderText(response, CHILD_AGENT_RESULT_TEXT);
return;
}
@@ -5744,7 +5738,6 @@ async function handleProviderRequest(
}
if (flow.kind === 'implementation_child_agent' && streamRequestIndex === 3) {
assert.deepEqual(toolNames(body), [
- 'ArchiveRead',
'Bash',
'Edit',
'Glob',
diff --git a/packages/runtime-host/src/__tests__/hosted-execution-tool-profile.test.ts b/packages/runtime-host/src/__tests__/hosted-execution-tool-profile.test.ts
index c2813514bd..1cc0d84531 100644
--- a/packages/runtime-host/src/__tests__/hosted-execution-tool-profile.test.ts
+++ b/packages/runtime-host/src/__tests__/hosted-execution-tool-profile.test.ts
@@ -182,15 +182,18 @@ test('WorkHub v2 can read its attachments without inheriting terminal, browser,
abortSignal: new AbortController().signal,
emitOutput() {},
};
- assert.deepEqual(await read.impl({ ref: 'maka://runtime/attachments/attachment-1' }, context), {
- kind: 'text',
- text: 'attachment contents',
+ assert.deepEqual(await read.impl({ path: 'maka://runtime/attachments/attachment-1' }, context), {
+ content: 'attachment contents',
+ offset: 0,
+ returnedLines: 1,
+ totalLines: 1,
+ next: null,
});
assert.deepEqual(reads, [{ sessionId: 'workhub', artifactId: 'attachment-1' }]);
for (const input of [
{ path: '/etc/passwd' },
- { ref: 'maka://runtime/background-tasks/task-1' },
- { ref: 'maka://runtime/attachments/a?session=other' },
+ { path: 'maka://runtime/background-tasks/task-1' },
+ { path: 'maka://runtime/attachments/a?session=other' },
]) {
assert.throws(() => read.impl(input, context));
}
diff --git a/packages/runtime-host/src/server/execution-artifacts.ts b/packages/runtime-host/src/server/execution-artifacts.ts
index fb856e6e64..e7ed035644 100644
--- a/packages/runtime-host/src/server/execution-artifacts.ts
+++ b/packages/runtime-host/src/server/execution-artifacts.ts
@@ -30,7 +30,6 @@ import { isPathInside } from '@maka/runtime/path-containment';
import {
createLedgerArchivePreparer,
createLedgerArchiveResourceReader,
- createLedgerToolResultArchiveReader,
} from '@maka/runtime/ledger-tool-result-archive-reader';
import type { ToolResultArchiveEvidenceReader } from '@maka/core/tool-result-archive-evidence';
import { type ToolArtifactRecorderInput } from '@maka/runtime/tool-artifacts';
@@ -100,9 +99,6 @@ export function createHostExecutionArtifactServices(input: {
const prepareLedger = input.archiveEvidence
? createLedgerArchivePreparer(input.archiveEvidence)
: undefined;
- const readLedger = input.archiveEvidence
- ? createLedgerToolResultArchiveReader(input.archiveEvidence)
- : undefined;
const readLedgerResource = input.archiveEvidence
? createLedgerArchiveResourceReader(input.archiveEvidence)
: undefined;
@@ -143,12 +139,8 @@ export function createHostExecutionArtifactServices(input: {
},
toolResultArchive: createToolResultArchiveCapability({
archiveToolResult: prepareLedgerForCommit,
- readToolResultArchive: (event: ToolResultArchiveReaderInput) =>
- event.rewriteVersion === 2
- ? (readLedger?.(event) ?? { ok: false, reason: 'read_failed' })
- : readArchive(input.artifacts, event),
readArchivedToolResultResource: (event: ToolResultArchiveResourceReadInput) =>
- event.storage === 'ledger'
+ event.storage === 'ledger' || event.storage === 'event'
? (readLedgerResource?.(event) ?? { ok: false, reason: 'read_failed' })
: readArchive(input.artifacts, event),
}),
diff --git a/packages/runtime-host/src/server/hosted-execution-tool-profile.ts b/packages/runtime-host/src/server/hosted-execution-tool-profile.ts
index 289490b5a5..73e285e4f9 100644
--- a/packages/runtime-host/src/server/hosted-execution-tool-profile.ts
+++ b/packages/runtime-host/src/server/hosted-execution-tool-profile.ts
@@ -22,6 +22,7 @@ import type { WorkHubRoutingDecision } from '@maka/core/workhub-routing';
import { parseAttachmentResourceRef } from '@maka/core/attachments';
import type { MakaTool } from '@maka/runtime/tool-runtime';
import { z } from 'zod';
+import { readParameters, resolveReadInput } from '@maka/runtime/read-page';
const HEADLESS_CODING_V1_TOOL_NAMES = [
'Bash',
@@ -58,17 +59,10 @@ const WORKHUB_COORDINATION_V1_SYSTEM_PROMPT = [
'Never claim to have inspected files, run commands, changed a Session, or completed concrete work.',
].join(' ');
-const WORKHUB_ATTACHMENT_READ_PARAMETERS = z
- .object({
- ref: z
- .string()
- .refine(
- (value) => parseAttachmentResourceRef(value) !== null,
- 'Expected a Session attachment reference',
- )
- .describe('The maka://runtime/attachments/ reference provided with a user attachment.'),
- })
- .strict();
+const WORKHUB_ATTACHMENT_READ_PARAMETERS = readParameters.refine(
+ (input) => parseAttachmentResourceRef(resolveReadInput(input).path) !== null,
+ 'Expected a Session attachment path',
+);
export interface HostedExecutionRunProfile {
readonly toolNames: readonly string[];
diff --git a/packages/runtime/package.json b/packages/runtime/package.json
index 3c8da57741..2b72631574 100644
--- a/packages/runtime/package.json
+++ b/packages/runtime/package.json
@@ -9,6 +9,7 @@
"./ask-user-question-tool": "./dist/ask-user-question-tool.js",
"./ai-sdk-backend": "./dist/ai-sdk-backend.js",
"./mcp-tools": "./dist/mcp-tools.js",
+ "./read-page": "./dist/read-page.js",
"./builtin-tools": "./dist/builtin-tools.js",
"./shell-tools": "./dist/shell-tools.js",
"./shell-run-manager": "./dist/shell-run-manager.js",
diff --git a/packages/runtime/src/__tests__/active-tool-result-prune.test.ts b/packages/runtime/src/__tests__/active-tool-result-prune.test.ts
deleted file mode 100644
index 34cb61c7dc..0000000000
--- a/packages/runtime/src/__tests__/active-tool-result-prune.test.ts
+++ /dev/null
@@ -1,873 +0,0 @@
-/*
- * 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, test } from 'node:test';
-import { z } from 'zod';
-import type { ModelMessage } from '../model-protocol.js';
-
-import {
- rewriteActiveToolResultsInMessages as rewriteActiveToolResultsInMessagesNarrow,
- type ActiveToolResultProjectionSource,
- type ActiveToolResultPruneInput,
- type ActiveToolResultPruneResult,
-} from '../active-tool-result-prune.js';
-import type { DurableToolResultProjection } from '@maka/core/durable-tool-result-projection';
-import type { ModelProjectionTransition } from '@maka/core/model-projection-transition';
-import { planActiveToolResultSupersession } from '../active-tool-result-working-set.js';
-import { composeRequestProjection } from '../request-projection.js';
-import { ToolAvailabilityRuntime, TOOL_SEARCH_NAME } from '../tool-availability.js';
-import { toolActivationKey } from '../tool-activation-identity.js';
-import type { MakaTool } from '../tool-runtime.js';
-
-describe('active current-turn tool-result pruning', () => {
- test('defaults to pruning current-turn tool results above 2048 estimated tokens', async () => {
- const belowDefault = await rewriteActiveToolResultsInMessages({
- messages: [largeTextToolMessage('Read', 'tool-small', 'a'.repeat(1000 * 4))],
- policy: { enabled: true },
- stepNumber: 1,
- turnId: 'turn-1',
- archiveToolResult: () => ({ artifactId: 'unused' }),
- });
- const aboveDefault = await rewriteActiveToolResultsInMessages({
- messages: [largeTextToolMessage('Read', 'tool-large', 'a'.repeat(2048 * 4 + 4))],
- policy: { enabled: true },
- stepNumber: 1,
- turnId: 'turn-1',
- archiveToolResult: () => ({ artifactId: 'artifact-tool-large' }),
- });
-
- assert.equal(belowDefault.rewritten, 0);
- assert.equal(aboveDefault.rewritten, 1);
- assert.equal(aboveDefault.diagnosticPatch.activePrunedToolResults, 1);
- });
-
- test('request projection composes active tools with rewritten messages', async () => {
- const originalMessages = [largeToolMessage('Read', 'tool-1', 'SECRET'.repeat(20))];
- const activePrune = async () => {
- const rewritten = await rewriteActiveToolResultsInMessages({
- messages: originalMessages,
- policy: { enabled: true, maxCurrentResultEstimatedTokens: 1 },
- stepNumber: 1,
- turnId: 'turn-1',
- charsPerToken: 1,
- archiveToolResult: () => ({ artifactId: 'artifact-tool-1' }),
- });
- return rewritten.rewritten > 0 ? { messages: rewritten.messages } : undefined;
- };
- const composed = composeRequestProjection(
- () => ({ activeTools: ['Read', TOOL_SEARCH_NAME] }),
- undefined,
- activePrune,
- );
-
- assert.ok(composed);
- const result = await composed({
- completedSteps: [],
- stepNumber: 1,
- model: {},
- messages: originalMessages,
- resolveDispatch: (active) => ({ systemPromptChars: 0, activeTools: [...(active ?? [])] }),
- });
-
- assert.deepEqual(result?.activeTools, ['Read', TOOL_SEARCH_NAME]);
- assert.ok(result?.messages);
- assert.match(JSON.stringify(result.messages), /maka\.archived_tool_result/);
- });
-
- test('oversized eligible current-turn tool result is archived and replaced', async () => {
- const largeBody = 'SECRET_PAYLOAD_SHOULD_BE_ARCHIVED'.repeat(20);
- const archiveRequests: Array<{
- serializedResult: string;
- bodySha256: string;
- toolCallId: string;
- }> = [];
- const rewritten = await rewriteActiveToolResultsInMessages({
- messages: [largeToolMessage('Read', 'tool-1', largeBody)],
- policy: { enabled: true, maxCurrentResultEstimatedTokens: 1 },
- stepNumber: 1,
- turnId: 'turn-1',
- charsPerToken: 1,
- archiveToolResult: (candidate) => {
- archiveRequests.push({
- serializedResult: candidate.serializedResult,
- bodySha256: candidate.bodySha256,
- toolCallId: candidate.toolCallId,
- });
- return { artifactId: 'artifact-tool-1' };
- },
- });
-
- assert.equal(archiveRequests.length, 1);
- assert.match(archiveRequests[0]?.bodySha256 ?? '', /^[a-f0-9]{64}$/);
- assert.equal(archiveRequests[0]?.toolCallId, 'tool-1');
- const secondPrompt = JSON.stringify(rewritten.messages);
- assert.match(secondPrompt, /maka\.archived_tool_result/);
- assert.match(secondPrompt, /artifact-tool-1/);
- assert.equal(secondPrompt.includes('maka://archive/'), true);
- assert.match(secondPrompt, /ArchiveRead/);
- assert.match(secondPrompt, /Do not use Glob/);
- assert.equal(secondPrompt.includes(largeBody), false);
- });
-
- // Every way the archive can fail to yield one usable artifact id is the same
- // fact to this code: nothing durable was written, so nothing may be replaced.
- for (const [name, archiveToolResult] of [
- [
- 'throws',
- () => {
- throw new Error('archive unavailable');
- },
- ],
- ['writes nothing', () => undefined],
- ['returns an empty artifact id', () => ({ artifactId: '' })],
- ['returns a blank artifact id', () => ({ artifactId: ' ' })],
- ] as const) {
- test(`keeps the original tool result when the archive ${name}`, async () => {
- const messages = [largeToolMessage('Read', 'tool-1', 'KEEP_ME'.repeat(20))];
- const rewritten = await rewriteActiveToolResultsInMessages({
- messages,
- policy: { enabled: true, maxCurrentResultEstimatedTokens: 1 },
- stepNumber: 1,
- turnId: 'turn-1',
- charsPerToken: 1,
- archiveToolResult,
- });
-
- assert.equal(rewritten.rewritten, 0);
- assert.equal(rewritten.archiveFailures, 1);
- assert.deepEqual(rewritten.messages, messages);
- assert.match(JSON.stringify(rewritten.messages), /KEEP_ME/);
- assert.doesNotMatch(JSON.stringify(rewritten.messages), /maka\.archived_tool_result/);
- });
- }
-
- test('empty-artifact placeholders are not treated as idempotent', async () => {
- const placeholder = invalidActivePlaceholder();
- const messages: ModelMessage[] = [
- {
- role: 'tool',
- content: [
- {
- type: 'tool-result',
- toolCallId: 'tool-json',
- toolName: 'Read',
- output: { type: 'json', value: placeholder as never },
- },
- ],
- },
- {
- role: 'tool',
- content: [
- {
- type: 'tool-result',
- toolCallId: 'tool-text',
- toolName: 'Read',
- output: { type: 'text', value: JSON.stringify(placeholder) },
- },
- ],
- },
- ];
- const archivedToolCallIds: string[] = [];
-
- const rewritten = await rewriteActiveToolResultsInMessages({
- messages,
- policy: { enabled: true, maxCurrentResultEstimatedTokens: 1 },
- stepNumber: 1,
- turnId: 'turn-1',
- charsPerToken: 1,
- archiveToolResult: (candidate) => {
- archivedToolCallIds.push(candidate.toolCallId);
- return { artifactId: `artifact-${candidate.toolCallId}` };
- },
- });
-
- assert.equal(rewritten.rewritten, 2);
- assert.deepEqual(archivedToolCallIds, ['tool-json', 'tool-text']);
- assert.match(JSON.stringify(rewritten.messages), /artifact-tool-json/);
- assert.match(JSON.stringify(rewritten.messages), /artifact-tool-text/);
- });
-
- test('text placeholder output is idempotent across active prune passes', async () => {
- const messages = [largeTextToolMessage('Read', 'tool-1', 'SECRET'.repeat(20))];
- const first = await rewriteActiveToolResultsInMessages({
- messages,
- policy: { enabled: true, maxCurrentResultEstimatedTokens: 1 },
- stepNumber: 1,
- turnId: 'turn-1',
- charsPerToken: 1,
- archiveToolResult: () => ({ artifactId: 'artifact-tool-1' }),
- });
- const second = await rewriteActiveToolResultsInMessages({
- messages: first.messages,
- policy: { enabled: true, maxCurrentResultEstimatedTokens: 1 },
- stepNumber: 2,
- turnId: 'turn-1',
- charsPerToken: 1,
- archiveToolResult: () => {
- throw new Error('should not re-archive placeholder text');
- },
- });
-
- assert.equal(first.rewritten, 1);
- assert.equal(second.rewritten, 0);
- assert.equal(second.archiveFailures, 0);
- assert.deepEqual(second.messages, first.messages);
- });
-
- test('returns active prune diagnostics for rewritten and failed archives', async () => {
- const success = await rewriteActiveToolResultsInMessages({
- messages: [largeToolMessage('Read', 'tool-1', 'SECRET'.repeat(200))],
- policy: { enabled: true, maxCurrentResultEstimatedTokens: 1 },
- stepNumber: 1,
- turnId: 'turn-1',
- charsPerToken: 1,
- archiveToolResult: () => ({ artifactId: 'artifact-tool-1' }),
- });
- const failure = await rewriteActiveToolResultsInMessages({
- messages: [largeToolMessage('Read', 'tool-2', 'KEEP_ME'.repeat(20))],
- policy: { enabled: true, maxCurrentResultEstimatedTokens: 1 },
- stepNumber: 1,
- turnId: 'turn-1',
- charsPerToken: 1,
- archiveToolResult: () => undefined,
- });
-
- assert.equal(success.diagnosticPatch.activePrunedToolResults, 1);
- assert.equal(success.diagnosticPatch.activeArchiveFailures, undefined);
- assert.ok((success.diagnosticPatch.activeEstimatedTokensSaved ?? 0) > 0);
- assert.equal(failure.diagnosticPatch.activePrunedToolResults, undefined);
- assert.equal(failure.diagnosticPatch.activeArchiveFailures, 1);
- });
-
- test('eligible tool-call ids keep prior replay tool results untouched', async () => {
- const priorBody = 'PRIOR_REPLAY_RESULT_SHOULD_STAY_FULL'.repeat(20);
- const currentBody = 'CURRENT_STEP_RESULT_SHOULD_BE_ARCHIVED'.repeat(20);
- const archiveRequests: string[] = [];
- const rewritten = await rewriteActiveToolResultsInMessages({
- messages: [
- largeToolMessage('Read', 'prior-tool-call', priorBody),
- largeToolMessage('Read', 'current-tool-call', currentBody),
- ],
- policy: { enabled: true, maxCurrentResultEstimatedTokens: 1 },
- stepNumber: 1,
- turnId: 'turn-1',
- charsPerToken: 1,
- eligibleToolCallIds: new Set(['current-tool-call']),
- archiveToolResult: (candidate) => {
- archiveRequests.push(candidate.toolCallId);
- return { artifactId: `artifact-${candidate.toolCallId}` };
- },
- });
-
- assert.equal(rewritten.rewritten, 1);
- assert.deepEqual(archiveRequests, ['current-tool-call']);
- const nextPrompt = JSON.stringify(rewritten.messages);
- assert.match(nextPrompt, /PRIOR_REPLAY_RESULT_SHOULD_STAY_FULL/);
- assert.doesNotMatch(nextPrompt, /CURRENT_STEP_RESULT_SHOULD_BE_ARCHIVED/);
- assert.match(nextPrompt, /artifact-current-tool-call/);
- });
-
- test('tool activation still works when active pruning shares the projection pipeline', async () => {
- const runtime = new ToolAvailabilityRuntime(
- [makaTool('Read'), makaTool('RiveWorkflow')],
- { groups: [{ id: 'rive', toolNames: ['RiveWorkflow'] }] },
- makaTool('invalid'),
- );
- const active = new Map<string, string>();
- const plan = runtime.prepare(active);
- active.set(
- 'RiveWorkflow',
- toolActivationKey(plan.providerTools.find((candidate) => candidate.name === 'RiveWorkflow')!),
- );
- const activePrune = async (options: { messages: ModelMessage[]; stepNumber: number }) => {
- const rewritten = await rewriteActiveToolResultsInMessages({
- messages: options.messages,
- policy: { enabled: true, maxCurrentResultEstimatedTokens: 1024 },
- stepNumber: options.stepNumber,
- turnId: 'turn-1',
- archiveToolResult: () => ({ artifactId: 'unused' }),
- });
- return rewritten.rewritten > 0 ? { messages: rewritten.messages } : undefined;
- };
-
- const projection = composeRequestProjection(
- () => plan.projectActiveTools!(),
- undefined,
- activePrune,
- );
- const result = await projection!({
- completedSteps: [
- {
- toolCalls: [
- {
- type: 'tool-call',
- toolCallId: 'load-1',
- toolName: TOOL_SEARCH_NAME,
- input: { query: 'RiveWorkflow' },
- },
- ],
- },
- ],
- stepNumber: 1,
- model: {},
- messages: [{ role: 'user', content: 'load rive' }],
- activeTools: plan.activeTools,
- resolveDispatch: (active) => ({ systemPromptChars: 0, activeTools: [...(active ?? [])] }),
- });
-
- assert.ok(!plan.activeTools.includes('RiveWorkflow'), 'step 0 hides the group tool');
- assert.ok(
- result?.activeTools?.includes('RiveWorkflow'),
- 'step 1 advertises the loaded group tool',
- );
- });
-
- test('archives an exact duplicate below the ordinary size threshold', async () => {
- const body = 'DUPLICATE_OBSERVATION'.repeat(80);
- const rewritten = await rewriteActiveToolResultsInMessages({
- messages: [
- largeToolMessage('CustomQuery', 'tool-old', body),
- largeToolMessage('CustomQuery', 'tool-new', body),
- ],
- policy: {
- enabled: true,
- maxCurrentResultEstimatedTokens: 10_000,
- minSupersededResultEstimatedTokens: 1,
- },
- stepNumber: 2,
- turnId: 'turn-1',
- charsPerToken: 1,
- completedToolCalls: [
- completedCall('CustomQuery', 'tool-old', { query: 'status' }, 0),
- completedCall('CustomQuery', 'tool-new', { query: 'status' }, 1),
- ],
- eligibleToolCallIds: new Set(['tool-old']),
- archiveToolResult: () => ({ artifactId: 'artifact-tool-old' }),
- });
-
- assert.equal(rewritten.rewritten, 1);
- assert.equal(rewritten.diagnosticPatch.activeSupersededToolResults, 1);
- assert.equal(rewritten.diagnosticPatch.activeDuplicateToolResults, 1);
- const prompt = JSON.stringify(rewritten.messages);
- assert.match(prompt, /exact_duplicate/);
- assert.match(prompt, /tool-new/);
- assert.equal(prompt.split(body).length - 1, 1, 'only the newest full body remains');
-
- const secondPass = await rewriteActiveToolResultsInMessages({
- messages: rewritten.messages,
- policy: {
- enabled: true,
- maxCurrentResultEstimatedTokens: 10_000,
- minSupersededResultEstimatedTokens: 1,
- },
- stepNumber: 3,
- turnId: 'turn-1',
- charsPerToken: 1,
- completedToolCalls: [
- completedCall('CustomQuery', 'tool-old', { query: 'status' }, 0),
- completedCall('CustomQuery', 'tool-new', { query: 'status' }, 1),
- ],
- eligibleToolCallIds: new Set(['tool-old']),
- archiveToolResult: () => {
- throw new Error('supersession placeholder must be idempotent');
- },
- });
- assert.equal(secondPass.rewritten, 0);
- assert.deepEqual(secondPass.messages, rewritten.messages);
- });
-
- test('a newer Read covering the old range supersedes different old content', async () => {
- const oldBody = 'OLD_FILE_CONTENT'.repeat(100);
- const newBody = 'NEW_FILE_CONTENT'.repeat(100);
- const rewritten = await rewriteActiveToolResultsInMessages({
- messages: [
- largeToolMessage('Read', 'read-old', oldBody),
- largeToolMessage('Read', 'read-new', newBody),
- ],
- policy: {
- enabled: true,
- maxCurrentResultEstimatedTokens: 10_000,
- minSupersededResultEstimatedTokens: 1,
- },
- stepNumber: 2,
- turnId: 'turn-1',
- charsPerToken: 1,
- completedToolCalls: [
- completedCall('Read', 'read-old', { path: './src/a.ts', offset: 20, limit: 10 }, 0),
- completedCall('Read', 'read-new', { path: 'src/a.ts', offset: 0, limit: 100 }, 1),
- ],
- eligibleToolCallIds: new Set(['read-old']),
- archiveToolResult: () => ({ artifactId: 'artifact-read-old' }),
- });
-
- assert.equal(rewritten.rewritten, 1);
- assert.match(JSON.stringify(rewritten.messages), /newer_read_covers_range/);
- assert.doesNotMatch(JSON.stringify(rewritten.messages), /OLD_FILE_CONTENT/);
- assert.match(JSON.stringify(rewritten.messages), /NEW_FILE_CONTENT/);
- });
-
- test('keeps platform-dependent filesystem path spellings as distinct subjects', () => {
- const cases = [
- {
- toolName: 'Read',
- first: { path: ' target.ts' },
- second: { path: 'target.ts' },
- },
- {
- toolName: 'Read',
- first: { path: 'dir\\target.ts' },
- second: { path: 'dir/target.ts' },
- },
- {
- toolName: 'Glob',
- first: { pattern: '**/*.ts', cwd: '//server/share' },
- second: { pattern: '**/*.ts', cwd: '/server/share' },
- },
- {
- toolName: 'Grep',
- first: { pattern: 'TODO', path: '\\\\server\\share' },
- second: { pattern: 'TODO', path: '//server/share' },
- },
- ] as const;
-
- for (const [index, testCase] of cases.entries()) {
- const decisions = planActiveToolResultSupersession([
- {
- toolCallId: `old-${index}`,
- toolName: testCase.toolName,
- input: testCase.first,
- stepNumber: 0,
- bodySha256: `old-body-${index}`,
- isError: false,
- eligible: true,
- },
- {
- toolCallId: `new-${index}`,
- toolName: testCase.toolName,
- input: testCase.second,
- stepNumber: 1,
- bodySha256: `new-body-${index}`,
- isError: false,
- eligible: true,
- },
- ]);
-
- assert.equal(decisions.size, 0, `${testCase.toolName} case ${index} must fail open`);
- }
- });
-
- test('does not merge parallel or non-covering Read observations', async () => {
- const messages = [
- largeToolMessage('Read', 'read-left', 'LEFT'.repeat(300)),
- largeToolMessage('Read', 'read-right', 'RIGHT'.repeat(300)),
- largeToolMessage('Read', 'read-parallel', 'PARALLEL'.repeat(300)),
- ];
- const rewritten = await rewriteActiveToolResultsInMessages({
- messages,
- policy: {
- enabled: true,
- maxCurrentResultEstimatedTokens: 10_000,
- minSupersededResultEstimatedTokens: 1,
- },
- stepNumber: 2,
- turnId: 'turn-1',
- charsPerToken: 1,
- completedToolCalls: [
- completedCall('Read', 'read-left', { path: 'a.ts', offset: 0, limit: 10 }, 0),
- completedCall('Read', 'read-right', { path: 'a.ts', offset: 20, limit: 10 }, 1),
- completedCall('Read', 'read-parallel', { path: 'a.ts', offset: 0, limit: 100 }, 0),
- ],
- eligibleToolCallIds: new Set(['read-left', 'read-parallel']),
- archiveToolResult: () => ({ artifactId: 'unused' }),
- });
-
- assert.equal(rewritten.rewritten, 0);
- assert.deepEqual(rewritten.messages, messages);
- });
-
- test('supersedes matching search snapshots and resolved failures', async () => {
- const rewritten = await rewriteActiveToolResultsInMessages({
- messages: [
- errorToolMessage('Grep', 'grep-old', 'FAILED_SEARCH'.repeat(100)),
- largeToolMessage('Grep', 'grep-new', 'MATCHES'.repeat(200)),
- ],
- policy: {
- enabled: true,
- maxCurrentResultEstimatedTokens: 10_000,
- minSupersededResultEstimatedTokens: 1,
- },
- stepNumber: 2,
- turnId: 'turn-1',
- charsPerToken: 1,
- completedToolCalls: [
- completedCall('Grep', 'grep-old', { pattern: 'TODO', path: './src' }, 0),
- completedCall('Grep', 'grep-new', { path: 'src', pattern: 'TODO' }, 1),
- ],
- eligibleToolCallIds: new Set(['grep-old']),
- archiveToolResult: () => ({ artifactId: 'artifact-grep-old' }),
- });
-
- assert.equal(rewritten.rewritten, 1);
- const prompt = JSON.stringify(rewritten.messages);
- assert.match(prompt, /failure_resolved/);
- assert.match(prompt, /failureBodySha256/);
- assert.match(prompt, /grep-new/);
- });
-
- test('a newer failed snapshot does not replace the last successful evidence', async () => {
- const messages = [
- largeToolMessage('Glob', 'glob-old', 'FILES'.repeat(300)),
- errorToolMessage('Glob', 'glob-new', 'SEARCH_FAILED'.repeat(100)),
- ];
- const rewritten = await rewriteActiveToolResultsInMessages({
- messages,
- policy: {
- enabled: true,
- maxCurrentResultEstimatedTokens: 10_000,
- minSupersededResultEstimatedTokens: 1,
- },
- stepNumber: 2,
- turnId: 'turn-1',
- charsPerToken: 1,
- completedToolCalls: [
- completedCall('Glob', 'glob-old', { pattern: '**/*.ts' }, 0),
- completedCall('Glob', 'glob-new', { pattern: '**/*.ts' }, 1),
- ],
- eligibleToolCallIds: new Set(['glob-old']),
- archiveToolResult: () => ({ artifactId: 'unused' }),
- });
-
- assert.equal(rewritten.rewritten, 0);
- assert.deepEqual(rewritten.messages, messages);
- });
-
- test('a same-value failure is not an exact duplicate of the last success', async () => {
- const body = 'SAME_OBSERVATION'.repeat(100);
- const messages = [
- largeToolMessage('CustomQuery', 'query-success', body),
- errorToolMessage('CustomQuery', 'query-failure', body),
- ];
- const rewritten = await rewriteActiveToolResultsInMessages({
- messages,
- policy: {
- enabled: true,
- maxCurrentResultEstimatedTokens: 10_000,
- minSupersededResultEstimatedTokens: 1,
- },
- stepNumber: 2,
- turnId: 'turn-1',
- charsPerToken: 1,
- completedToolCalls: [
- completedCall('CustomQuery', 'query-success', { query: 'status' }, 0),
- completedCall('CustomQuery', 'query-failure', { query: 'status' }, 1),
- ],
- eligibleToolCallIds: new Set(['query-success']),
- archiveToolResult: () => ({ artifactId: 'unused' }),
- });
-
- assert.equal(rewritten.rewritten, 0);
- assert.deepEqual(rewritten.messages, messages);
- });
-
- test('only allowlisted Bash snapshots participate in semantic supersession', async () => {
- const messages = [
- largeToolMessage('Bash', 'status-old', 'OLD_STATUS'.repeat(200)),
- largeToolMessage('Bash', 'status-new', 'NEW_STATUS'.repeat(200)),
- largeToolMessage('Bash', 'write-old', 'OLD_WRITE'.repeat(200)),
- largeToolMessage('Bash', 'write-new', 'NEW_WRITE'.repeat(200)),
- ];
- const rewritten = await rewriteActiveToolResultsInMessages({
- messages,
- policy: {
- enabled: true,
- maxCurrentResultEstimatedTokens: 10_000,
- minSupersededResultEstimatedTokens: 1,
- },
- stepNumber: 4,
- turnId: 'turn-1',
- charsPerToken: 1,
- completedToolCalls: [
- completedCall('Bash', 'status-old', { command: 'git status --short' }, 0),
- completedCall('Bash', 'status-new', { command: 'git status --short' }, 1),
- completedCall('Bash', 'write-old', { command: 'printf data > out.txt' }, 2),
- completedCall('Bash', 'write-new', { command: 'printf data > out.txt' }, 3),
- ],
- eligibleToolCallIds: new Set(['status-old', 'write-old']),
- archiveToolResult: ({ toolCallId }) => ({ artifactId: `artifact-${toolCallId}` }),
- });
-
- assert.equal(rewritten.rewritten, 1);
- const prompt = JSON.stringify(rewritten.messages);
- assert.match(prompt, /newer_snapshot/);
- assert.doesNotMatch(prompt, /OLD_STATUS/);
- assert.match(prompt, /OLD_WRITE/);
- });
-
- test('test and build runs keep distinct warnings and failures visible', async () => {
- const messages = [
- largeToolMessage('Bash', 'build-warning', 'BUILD_DEPRECATION_WARNING'.repeat(100)),
- largeToolMessage('Bash', 'build-clean', 'BUILD_FROM_INCREMENTAL_CACHE'.repeat(100)),
- errorToolMessage('Bash', 'test-failure', 'FLAKY_TEST_FAILURE'.repeat(100)),
- largeToolMessage('Bash', 'test-success', 'TESTS_PASSED'.repeat(100)),
- ];
- const rewritten = await rewriteActiveToolResultsInMessages({
- messages,
- policy: {
- enabled: true,
- maxCurrentResultEstimatedTokens: 10_000,
- minSupersededResultEstimatedTokens: 1,
- },
- stepNumber: 4,
- turnId: 'turn-1',
- charsPerToken: 1,
- completedToolCalls: [
- completedCall('Bash', 'build-warning', { command: 'npm run build' }, 0),
- completedCall('Bash', 'build-clean', { command: 'npm run build' }, 1),
- completedCall('Bash', 'test-failure', { command: 'cargo test' }, 2),
- completedCall('Bash', 'test-success', { command: 'cargo test' }, 3),
- ],
- eligibleToolCallIds: new Set(['build-warning', 'test-failure']),
- archiveToolResult: () => ({ artifactId: 'unused' }),
- });
-
- assert.equal(rewritten.rewritten, 0);
- assert.deepEqual(rewritten.messages, messages);
- });
-
- test('foreground Bash output is not superseded by background or PTY handles', async () => {
- const messages = [
- largeToolMessage('Bash', 'test-foreground', 'FOREGROUND_TEST_OUTPUT'),
- largeToolMessage('Bash', 'test-background', 'BACKGROUND_JOB_REF'),
- largeToolMessage('Bash', 'test-pty', 'PTY_JOB_REF'),
- ];
- const rewritten = await rewriteActiveToolResultsInMessages({
- messages,
- policy: {
- enabled: true,
- maxCurrentResultEstimatedTokens: 10_000,
- minSupersededResultEstimatedTokens: 1,
- },
- stepNumber: 3,
- turnId: 'turn-1',
- charsPerToken: 1,
- completedToolCalls: [
- completedCall('Bash', 'test-foreground', { command: 'npm test' }, 0),
- completedCall(
- 'Bash',
- 'test-background',
- { command: 'npm test', run_in_background: true },
- 1,
- ),
- completedCall(
- 'Bash',
- 'test-pty',
- { command: 'npm test', run_in_background: true, pty: true },
- 2,
- ),
- ],
- eligibleToolCallIds: new Set(['test-foreground', 'test-background']),
- archiveToolResult: () => ({ artifactId: 'unused' }),
- });
-
- assert.equal(rewritten.rewritten, 0);
- assert.deepEqual(rewritten.messages, messages);
- });
-
- test('distinct background Bash job refs remain independently visible', async () => {
- const messages = [
- largeToolMessage('Bash', 'job-a', 'BACKGROUND_JOB_REF_A'),
- largeToolMessage('Bash', 'job-b', 'BACKGROUND_JOB_REF_B'),
- ];
- const rewritten = await rewriteActiveToolResultsInMessages({
- messages,
- policy: {
- enabled: true,
- maxCurrentResultEstimatedTokens: 10_000,
- minSupersededResultEstimatedTokens: 1,
- },
- stepNumber: 2,
- turnId: 'turn-1',
- charsPerToken: 1,
- completedToolCalls: [
- completedCall('Bash', 'job-a', { command: 'npm test', run_in_background: true }, 0),
- completedCall('Bash', 'job-b', { command: 'npm test', run_in_background: true }, 1),
- ],
- eligibleToolCallIds: new Set(['job-a']),
- archiveToolResult: () => ({ artifactId: 'unused' }),
- });
-
- assert.equal(rewritten.rewritten, 0);
- assert.deepEqual(rewritten.messages, messages);
- });
-});
-
-/**
- * Drive the prune with a durable ledger stand-in.
- *
- * The prune can no longer rewrite anything it cannot make durable, so every
- * case here supplies both halves of that: a projection address for each tool
- * call (derived from the message payload, so the size thresholds under test
- * measure exactly what they used to) and an archive + transition recorder.
- */
-async function rewriteActiveToolResultsInMessages(
- input: Omit<ActiveToolResultPruneInput, 'resolveProjection' | 'transitions'> & {
- archiveToolResult?: (candidate: {
- sessionId: string;
- runtimeEventId: string;
- turnId: string;
- toolCallId: string;
- toolName: string;
- serializedResult: string;
- bodySha256: string;
- }) => { artifactId: string } | void | Promise<{ artifactId: string } | void>;
- recordTransition?: (transition: ModelProjectionTransition) => Promise<void>;
- },
-): Promise<ActiveToolResultPruneResult> {
- const { archiveToolResult, recordTransition, ...rest } = input;
- let clock = 1000;
- return rewriteActiveToolResultsInMessagesNarrow({
- ...rest,
- resolveProjection: (toolCallId) => resolveTestProjection(input.messages, toolCallId),
- transitions: {
- sessionId: 'session-1',
- archiveToolResult: (candidate) =>
- archiveToolResult
- ? archiveToolResult(candidate)
- : { artifactId: `artifact-${candidate.toolCallId}` },
- recordTransition: recordTransition ?? (() => Promise.resolve()),
- now: () => (clock += 1),
- },
- });
-}
-
-function resolveTestProjection(
- messages: readonly ModelMessage[],
- toolCallId: string,
-): ActiveToolResultProjectionSource | undefined {
- for (const message of messages) {
- if (message.role !== 'tool' || !Array.isArray(message.content)) continue;
- for (const part of message.content as Array<Record<string, unknown>>) {
- if (part.type !== 'tool-result' || part.toolCallId !== toolCallId) continue;
- const output = part.output as { type?: string; value?: unknown } | undefined;
- const projection = testProjection(output);
- if (!projection) return undefined;
- return {
- runtimeEventId: `event-${toolCallId}`,
- turnId: 'turn-1',
- toolName: String(part.toolName),
- projection,
- };
- }
- }
- return undefined;
-}
-
-function testProjection(
- output: { type?: string; value?: unknown } | undefined,
-): DurableToolResultProjection | undefined {
- if (!output) return undefined;
- if (output.type === 'text' || output.type === 'error-text') {
- return {
- version: 1,
- kind: 'text',
- text: String(output.value),
- ...(output.type === 'error-text' ? { isError: true as const } : {}),
- };
- }
- if (output.type === 'json' || output.type === 'error-json') {
- return {
- version: 1,
- kind: 'json',
- value: output.value as never,
- ...(output.type === 'error-json' ? { isError: true as const } : {}),
- };
- }
- return undefined;
-}
-
-function largeToolMessage(toolName: string, toolCallId: string, body: string): ModelMessage {
- return {
- role: 'tool',
- content: [
- {
- type: 'tool-result',
- toolCallId,
- toolName,
- output: { type: 'json', value: { body } },
- },
- ],
- };
-}
-
-function largeTextToolMessage(toolName: string, toolCallId: string, body: string): ModelMessage {
- return {
- role: 'tool',
- content: [
- {
- type: 'tool-result',
- toolCallId,
- toolName,
- output: { type: 'text', value: body },
- },
- ],
- };
-}
-
-function errorToolMessage(toolName: string, toolCallId: string, body: string): ModelMessage {
- return {
- role: 'tool',
- content: [
- {
- type: 'tool-result',
- toolCallId,
- toolName,
- output: { type: 'error-json', value: { body } },
- },
- ],
- };
-}
-
-function completedCall(toolName: string, toolCallId: string, input: unknown, stepNumber: number) {
- return { toolName, toolCallId, input, stepNumber };
-}
-
-function invalidActivePlaceholder(): Record<string, unknown> {
- return {
- kind: 'maka.archived_tool_result',
- rewriteVersion: 1,
- artifactId: '',
- runtimeEventId: 'event-tool-old',
- toolCallId: 'tool-old',
- toolName: 'Read',
- bodySha256: 'a'.repeat(64),
- originalEstimatedTokens: 100,
- originalBytes: 400,
- reason: 'active_current_turn_tool_result_pruned_before_next_step',
- };
-}
-
-function makaTool(name: string): MakaTool {
- return {
- name,
- description: `${name} tool`,
- parameters: z.object({}),
- impl: () => ({ ok: true }),
- };
-}
diff --git a/packages/runtime/src/__tests__/ai-sdk-backend.test.ts b/packages/runtime/src/__tests__/ai-sdk-backend.test.ts
index 5ed50892a6..0cb8f5770a 100644
--- a/packages/runtime/src/__tests__/ai-sdk-backend.test.ts
+++ b/packages/runtime/src/__tests__/ai-sdk-backend.test.ts
@@ -4438,7 +4438,7 @@ describe('AiSdkBackend model history', () => {
serializedResult: string;
bodySha256: string;
}> = [];
- const oldResult = { body: 'x'.repeat(500) };
+ const oldResult = { body: 'x'.repeat(20_000) };
const transitions: ModelProjectionTransition[] = [];
const backend = createBackend({
connection: connection(),
@@ -4447,10 +4447,8 @@ describe('AiSdkBackend model history', () => {
tools: [],
contextBudget: {
name: 'archive-test',
- staleToolResultPrune: {
+ toolResultPrune: {
enabled: true,
- maxResultEstimatedTokens: 1,
- minRecentTurnsFull: 0,
},
charsPerToken: 1,
},
@@ -6402,10 +6400,8 @@ describe('AiSdkBackend model history', () => {
contextBudget: {
name: 'checkpoint-transition-replay-test',
charsPerToken: 1,
- staleToolResultPrune: {
+ toolResultPrune: {
enabled: true,
- maxResultEstimatedTokens: 1,
- minRecentTurnsFull: 0,
},
historyCompact: { enabled: true },
},
@@ -6455,7 +6451,7 @@ describe('AiSdkBackend model history', () => {
kind: 'function_response',
id: 'tool-fold-1',
name: 'Read',
- result: { body: 'y'.repeat(400) },
+ result: { body: 'y'.repeat(20_000) },
isError: false,
},
}),
@@ -6502,10 +6498,8 @@ describe('AiSdkBackend model history', () => {
contextBudget: {
name: 'checkpoint-transition-replay-test',
charsPerToken: 1,
- staleToolResultPrune: {
+ toolResultPrune: {
enabled: true,
- maxResultEstimatedTokens: 1,
- minRecentTurnsFull: 0,
},
historyCompact: { enabled: true },
},
@@ -6555,10 +6549,8 @@ describe('AiSdkBackend model history', () => {
contextBudget: {
name: 'checkpoint-effective-summary-test',
charsPerToken: 1,
- staleToolResultPrune: {
+ toolResultPrune: {
enabled: true,
- maxResultEstimatedTokens: 1,
- minRecentTurnsFull: 0,
},
historyCompact: { enabled: true },
},
@@ -6614,7 +6606,7 @@ describe('AiSdkBackend model history', () => {
kind: 'function_response',
id: 'tool-echo-1',
name: 'Read',
- result: { body: 'RAW_TRANSITIONED_TOOL_BODY '.repeat(40) },
+ result: { body: 'x'.repeat(20_000) + 'RAW_TRANSITIONED_TOOL_BODY' },
isError: false,
},
}),
@@ -6652,10 +6644,8 @@ describe('AiSdkBackend model history', () => {
contextBudget: {
name: 'checkpoint-effective-summary-test',
charsPerToken: 1,
- staleToolResultPrune: {
+ toolResultPrune: {
enabled: true,
- maxResultEstimatedTokens: 1,
- minRecentTurnsFull: 0,
},
historyCompact: { enabled: true },
},
@@ -6735,7 +6725,7 @@ describe('AiSdkBackend model history', () => {
kind: 'function_response',
id: 'tool-echo-1',
name: 'Read',
- result: { body: 'RAW_TRANSITIONED_TOOL_BODY '.repeat(40) },
+ result: { body: 'x'.repeat(20_000) + 'RAW_TRANSITIONED_TOOL_BODY' },
isError: false,
},
}),
@@ -6757,7 +6747,7 @@ describe('AiSdkBackend model history', () => {
contextBudget: {
name: 'checkpoint-effective-drift-test',
charsPerToken: 1,
- staleToolResultPrune: { enabled: false },
+ toolResultPrune: { enabled: false },
historyCompact: { enabled: true },
},
summarizeHistoryCompact: echoSummarizer,
@@ -6793,10 +6783,8 @@ describe('AiSdkBackend model history', () => {
contextBudget: {
name: 'checkpoint-effective-drift-test',
charsPerToken: 1,
- staleToolResultPrune: {
+ toolResultPrune: {
enabled: true,
- maxResultEstimatedTokens: 1,
- minRecentTurnsFull: 0,
},
historyCompact: { enabled: true },
},
@@ -6836,7 +6824,7 @@ describe('AiSdkBackend model history', () => {
contextBudget: {
name: 'checkpoint-effective-drift-test',
charsPerToken: 1,
- staleToolResultPrune: { enabled: false },
+ toolResultPrune: { enabled: false },
historyCompact: { enabled: true },
},
loadHistoryCompactCheckpoint: () => recorded.at(-1),
@@ -6896,7 +6884,7 @@ describe('AiSdkBackend model history', () => {
contextBudget: {
name: 'unreadable-target-replay-test',
charsPerToken: 1,
- staleToolResultPrune: { enabled: false },
+ toolResultPrune: { enabled: false },
historyCompact: { enabled: true },
},
loadModelProjectionTransitions: async () => ({
@@ -8969,7 +8957,7 @@ describe('AiSdkBackend usage telemetry', () => {
// lands back in the conversation. Advertising the decoder is only half the
// invariant; the other half is that calling it works from inside the turn.
const durable = durableTurnHarness('turn-1', 'read the big file');
- const largeBody = 'ARCHIVED_BODY_SENTINEL'.repeat(200);
+ const largeBody = 'x'.repeat(9_000) + 'ARCHIVED_BODY_SENTINEL';
const store = new Map<string, string>();
const prompts: unknown[] = [];
let streamCalls = 0;
@@ -8998,12 +8986,21 @@ describe('AiSdkBackend usage telemetry', () => {
streamCalls === 2
? call('tool-2', 'Bash', { cmd: 'continue' })
: streamCalls === 3
- ? call('tool-3', 'ArchiveRead', {
- // Read the ref out of the placeholder the runtime just
- // handed us, exactly as a model would.
- ref: /maka:\/\/archive\/[^"\\]+/.exec(JSON.stringify(prompt))?.[0] ?? 'missing',
- operation: 'read',
- })
+ ? call(
+ 'tool-3',
+ 'Read',
+ (() => {
+ const findNext = (value: any): any => {
+ if (value?.kind === 'maka.archived_tool_result') return value.page.next;
+ if (value && typeof value === 'object')
+ for (const child of Object.values(value)) {
+ const found = findNext(child);
+ if (found) return found;
+ }
+ };
+ return findNext(prompt);
+ })(),
+ )
: [
{ type: 'stream-start', warnings: [] },
{
@@ -9029,7 +9026,7 @@ describe('AiSdkBackend usage telemetry', () => {
name: 'Read',
description: 'Read description',
parameters: z.object({ path: z.string() }),
- impl: async () => ({ body: largeBody }),
+ impl: async () => ({ content: largeBody }),
},
{
name: 'Bash',
@@ -9039,19 +9036,17 @@ describe('AiSdkBackend usage telemetry', () => {
},
],
contextBudget: {
- activeToolResultPrune: { enabled: true, maxCurrentResultEstimatedTokens: 1 },
+ toolResultPrune: { enabled: true },
},
// A real store, so the decoder has to reach what the writer actually wrote.
toolResultArchive: createToolResultArchiveCapability({
archiveToolResult: async (event) => {
- const artifactId = `artifact-${store.size + 1}`;
- store.set(artifactId, event.serializedResult);
- return { artifactId };
+ store.set(event.runtimeEventId, event.serializedResult);
+ return { ledger: true };
},
- readToolResultArchive: async () => ({ ok: false, reason: 'not_found' }),
readArchivedToolResultResource: async (event) => {
const serializedResult =
- event.storage === 'ledger' ? undefined : store.get(event.artifactId);
+ event.storage === 'event' ? store.get(event.runtimeEventId) : undefined;
return serializedResult === undefined
? { ok: false, reason: 'not_found' }
: { ok: true, serializedResult };
@@ -9063,13 +9058,13 @@ describe('AiSdkBackend usage telemetry', () => {
for await (const event of backend.send(durable.input())) durable.record(event);
assert.match(
- store.get('artifact-1') ?? '',
+ [...store.values()][0] ?? '',
/ARCHIVED_BODY_SENTINEL/,
'the oversized Read result must have been archived',
);
const thirdPrompt = JSON.stringify(prompts[2]);
assert.doesNotMatch(thirdPrompt, /ARCHIVED_BODY_SENTINEL/);
- assert.match(thirdPrompt, /maka:\/\/archive\//);
+ assert.match(thirdPrompt, /maka:\/\/runtime\/tool-results\//);
assert.match(
JSON.stringify(prompts[3]),
/ARCHIVED_BODY_SENTINEL/,
@@ -9081,7 +9076,7 @@ describe('AiSdkBackend usage telemetry', () => {
const durable = durableTurnHarness('turn-1', 'hi');
const messages: unknown[] = [];
const events: SessionEvent[] = [];
- const largeBody = 'SECRET_PAYLOAD_SHOULD_BE_ARCHIVED'.repeat(200);
+ const largeBody = 'x'.repeat(20_000) + 'SECRET_PAYLOAD_SHOULD_BE_ARCHIVED';
const archivedToolCallIds: string[] = [];
let streamCalls = 0;
const prompts: unknown[] = [];
@@ -9182,7 +9177,7 @@ describe('AiSdkBackend usage telemetry', () => {
},
],
contextBudget: {
- activeToolResultPrune: { enabled: true, maxCurrentResultEstimatedTokens: 1 },
+ toolResultPrune: { enabled: true },
},
toolResultArchive: testToolResultArchive({
archiveToolResult: async (candidate) => {
@@ -9208,7 +9203,7 @@ describe('AiSdkBackend usage telemetry', () => {
| undefined;
assert.equal(streamCalls, 4);
const secondPrompt = JSON.stringify(prompts[1]);
- assert.match(secondPrompt, /SECRET_PAYLOAD_SHOULD_BE_ARCHIVED/);
+ assert.doesNotMatch(secondPrompt, /SECRET_PAYLOAD_SHOULD_BE_ARCHIVED/);
assert.doesNotMatch(secondPrompt, /maka\.active_archived_tool_result/);
const thirdPrompt = JSON.stringify(prompts[2]);
assert.doesNotMatch(thirdPrompt, /SECRET_PAYLOAD_SHOULD_BE_ARCHIVED/);
@@ -9222,15 +9217,17 @@ describe('AiSdkBackend usage telemetry', () => {
assert.match(fourthPrompt, /artifact-tool-1/);
// Each result is archived once, no matter how many later steps rebuild the
// Turn: the ledger, not a per-run memory, is what says it already happened.
- assert.deepEqual(archivedToolCallIds, ['tool-1', 'tool-2']);
+ assert.deepEqual(archivedToolCallIds, ['tool-1']);
for (const contextBudget of [usageMessage?.contextBudget, usageEvent?.contextBudget]) {
- assert.equal(contextBudget?.activePrunedToolResults, 2);
- assert.equal(contextBudget?.activeArchiveFailures, undefined);
- assert.ok(((contextBudget?.activeEstimatedTokensSaved as number | undefined) ?? 0) > 0);
+ assert.equal(contextBudget?.prunedToolResults, 1);
+ assert.equal(contextBudget?.archiveWriteFailures, undefined);
+ assert.ok(
+ ((contextBudget?.prunedToolResultEstimatedTokensBefore as number | undefined) ?? 0) > 0,
+ );
}
});
- test('projects superseded current-turn observations before the next provider step', async () => {
+ test('keeps small observations even when a newer Read supersedes their range', async () => {
const durable = durableTurnHarness('turn-1', 'hi');
const messages: unknown[] = [];
const prompts: unknown[] = [];
@@ -9294,10 +9291,8 @@ describe('AiSdkBackend usage telemetry', () => {
],
contextBudget: {
charsPerToken: 1,
- activeToolResultPrune: {
+ toolResultPrune: {
enabled: true,
- maxCurrentResultEstimatedTokens: 10_000,
- minSupersededResultEstimatedTokens: 1,
},
},
toolResultArchive: testToolResultArchive({
@@ -9311,13 +9306,13 @@ describe('AiSdkBackend usage telemetry', () => {
assert.equal(streamCalls, 3);
assert.match(JSON.stringify(prompts[1]), /OLD_READ_RESULT/);
const thirdPrompt = JSON.stringify(prompts[2]);
- assert.doesNotMatch(thirdPrompt, /OLD_READ_RESULT/);
+ assert.match(thirdPrompt, /OLD_READ_RESULT/);
assert.match(thirdPrompt, /NEW_READ_RESULT/);
- assert.match(thirdPrompt, /newer_read_covers_range/);
+ assert.doesNotMatch(thirdPrompt, /maka\.archived_tool_result/);
const usageMessage = messages.find(
(message) => (message as { type?: string }).type === 'token_usage',
) as { contextBudget?: Record<string, unknown> } | undefined;
- assert.equal(usageMessage?.contextBudget?.activeSupersededToolResults, 1);
+ assert.equal(usageMessage?.contextBudget?.activeSupersededToolResults, undefined);
assert.equal(usageMessage?.contextBudget?.activeDuplicateToolResults, undefined);
});
diff --git a/packages/runtime/src/__tests__/builtin-tools-file-worker.test.ts b/packages/runtime/src/__tests__/builtin-tools-file-worker.test.ts
index 26050bea7c..c7b506c90a 100644
--- a/packages/runtime/src/__tests__/builtin-tools-file-worker.test.ts
+++ b/packages/runtime/src/__tests__/builtin-tools-file-worker.test.ts
@@ -191,7 +191,7 @@ describe('builtin file tools use the sandboxed worker', () => {
await runTool(tools, 'Read', { path: 'image.png', offset: 1, limit: 1 }, cwd);
assert.equal(calls.length, 1);
- assert.deepEqual(calls[0]?.operation, { kind: 'read', path: 'image.png', offset: 1, limit: 1 });
+ assert.deepEqual(calls[0]?.operation, { kind: 'read', path: 'image.png' });
assert.equal(snapshotOwnerId, 'toolop-Read');
});
diff --git a/packages/runtime/src/__tests__/builtin-tools.test.ts b/packages/runtime/src/__tests__/builtin-tools.test.ts
index 6b666366e8..5758a35d3e 100644
--- a/packages/runtime/src/__tests__/builtin-tools.test.ts
+++ b/packages/runtime/src/__tests__/builtin-tools.test.ts
@@ -1525,49 +1525,6 @@ describe('builtin Bash streaming output', () => {
} satisfies RuntimeResourceReader;
const read = buildBuiltinTools({ runtimeResources }).find((tool) => tool.name === 'Read');
if (!read) throw new Error('Read tool missing');
- const parameters = read.parameters as {
- jsonSchema: PromiseLike<Record<string, unknown>> | Record<string, unknown>;
- validate(value: unknown): PromiseLike<{ success: boolean }> | { success: boolean };
- };
- const providerSchema = await parameters.jsonSchema;
- // Anthropic-compatible: a plain top-level object with properties, and no
- // top-level union combinator (#1228).
- assert.strictEqual(providerSchema.type, 'object');
- assert.strictEqual(providerSchema.anyOf, undefined);
- assert.strictEqual(providerSchema.oneOf, undefined);
- assert.strictEqual(providerSchema.allOf, undefined);
- assert.deepStrictEqual(
- Object.keys(providerSchema.properties as Record<string, unknown>).sort(),
- ['limit', 'offset', 'path', 'ref'],
- );
- // The strict file-vs-ref union remains the authoritative runtime validator.
- assert.strictEqual(
- (await parameters.validate({ path: 'README.md', offset: 2, limit: 10 })).success,
- true,
- );
- assert.strictEqual(
- (await parameters.validate({ ref: 'maka://runtime/background-tasks/shell-run-1' })).success,
- true,
- );
- assert.strictEqual((await parameters.validate({})).success, false);
- assert.strictEqual(
- (
- await parameters.validate({
- ref: 'maka://runtime/background-tasks/shell-run-1',
- offset: 2,
- })
- ).success,
- false,
- );
- assert.strictEqual(
- (
- await parameters.validate({
- path: 'README.md',
- ref: 'maka://runtime/background-tasks/shell-run-1',
- })
- ).success,
- false,
- );
const context = {
sessionId: 'session-1',
runId: 'run-1',
@@ -1577,31 +1534,12 @@ describe('builtin Bash streaming output', () => {
abortSignal: new AbortController().signal,
emitOutput: () => {},
};
- await assert.rejects(
- async () => read.impl({ path: 'maka://runtime/background-tasks/shell-run-1' }, context),
- /must be read with the ref parameter/,
- );
- const result = await read.impl({ ref: 'maka://runtime/background-tasks/shell-run-1' }, context);
-
- assert.deepStrictEqual(result, {
- kind: 'shell_run',
- ref: 'maka://runtime/background-tasks/shell-run-1',
- mode: 'pipes',
- status: 'running',
- cwd: '/workspace',
- cmd: 'sleep 60',
- startedAt: 1,
- updatedAt: 2,
- revision: 2,
- output: {
- mode: 'pipes',
- stdout: 'background task detail',
- stderr: '',
- stdoutTruncated: false,
- stderrTruncated: false,
- redacted: false,
- },
- });
+ const result = (await read.impl(
+ { path: 'maka://runtime/background-tasks/shell-run-1' },
+ context,
+ )) as { content: string; metadata: { status: string } };
+ assert.equal(result.content, 'background task detail');
+ assert.equal(result.metadata.status, 'running');
assert.deepStrictEqual(calls, [
{
sessionId: 'session-1',
@@ -1631,81 +1569,17 @@ describe('builtin Bash streaming output', () => {
emitOutput: () => {},
};
- assert.deepEqual(await read.impl({ ref: 'maka://runtime/attachments/attachment-1' }, context), {
- kind: 'text',
- text: 'attachment marker',
- });
- assert.deepEqual(calls, [{ sessionId: 'session-1', artifactId: 'attachment-1' }]);
- });
-
- test('Read normalizes a blank ref to "no ref" before validating the file-or-resource union', async () => {
- const read = buildBuiltinTools({
- runtimeResources: {
- readRuntimeResource: async () => ({ kind: 'text', text: 'unused' }),
- },
- }).find((tool) => tool.name === 'Read');
- if (!read) throw new Error('Read tool missing');
- const parameters = read.parameters as {
- validate(value: unknown): PromiseLike<{
- success: boolean;
- value?: unknown;
- error?: unknown;
- }>;
- };
-
- // A blank ref alongside a path passes, and the ref key is dropped so the
- // canonical input is the pure file variant.
- const normalized = await parameters.validate({
- path: 'config.yaml',
- ref: '',
- offset: 2,
- });
- assert.equal(normalized.success, true);
- if (normalized.success) {
- assert.deepEqual(normalized.value, { path: 'config.yaml', offset: 2 });
- assert.ok(!('ref' in (normalized.value as Record<string, unknown>)));
- }
- assert.equal((await parameters.validate({ path: 'config.yaml', ref: ' ' })).success, true);
- // A lone blank ref still fails: there is no readable target.
- assert.equal((await parameters.validate({ ref: '' })).success, false);
- assert.equal((await parameters.validate({ ref: ' ' })).success, false);
- });
-
- test('Read ignores provider defaults beside a non-empty runtime ref', async () => {
- const read = buildBuiltinTools({
- runtimeResources: {
- readRuntimeResource: async () => ({ kind: 'text', text: 'unused' }),
+ assert.deepEqual(
+ await read.impl({ path: 'maka://runtime/attachments/attachment-1' }, context),
+ {
+ content: 'attachment marker',
+ offset: 0,
+ returnedLines: 1,
+ totalLines: 1,
+ next: null,
},
- }).find((tool) => tool.name === 'Read');
- if (!read) throw new Error('Read tool missing');
- const parameters = read.parameters as {
- validate(value: unknown): PromiseLike<{
- success: boolean;
- value?: unknown;
- }>;
- };
- const ref = 'maka://runtime/background-tasks/shell-run-1';
-
- const normalized = await parameters.validate({
- path: '',
- offset: 0,
- limit: 1,
- ref,
- });
- assert.equal(normalized.success, true);
- if (normalized.success) assert.deepEqual(normalized.value, { ref });
-
- assert.equal(
- (
- await parameters.validate({
- path: 'README.md',
- offset: 0,
- limit: 1,
- ref,
- })
- ).success,
- false,
);
+ assert.deepEqual(calls, [{ sessionId: 'session-1', artifactId: 'attachment-1' }]);
});
test('StopBackgroundTask stops a runtime ref in the current session', async () => {
diff --git a/packages/runtime/src/__tests__/context-budget-mid-turn-policy.test.ts b/packages/runtime/src/__tests__/context-budget-mid-turn-policy.test.ts
index 2d0f948bee..36f7bb9d7d 100644
--- a/packages/runtime/src/__tests__/context-budget-mid-turn-policy.test.ts
+++ b/packages/runtime/src/__tests__/context-budget-mid-turn-policy.test.ts
@@ -69,16 +69,11 @@ describe('mid-turn history compact policy', () => {
describe('tool-result prune policy', () => {
test('uses bounded runtime defaults', () => {
const policy = buildDefaultContextBudgetPolicy();
- assert.deepEqual(policy?.activeToolResultPrune, {
+ assert.deepEqual(policy?.toolResultPrune, {
enabled: true,
- maxCurrentResultEstimatedTokens: 2_048,
- minSupersededResultEstimatedTokens: 256,
- minStepNumber: 1,
});
- assert.deepEqual(policy?.staleToolResultPrune, {
+ assert.deepEqual(policy?.toolResultPrune, {
enabled: true,
- maxResultEstimatedTokens: 2_048,
- minRecentTurnsFull: 2,
});
});
});
diff --git a/packages/runtime/src/__tests__/context-budget.test.ts b/packages/runtime/src/__tests__/context-budget.test.ts
index 11b8d2e0d4..4cc41531cb 100644
--- a/packages/runtime/src/__tests__/context-budget.test.ts
+++ b/packages/runtime/src/__tests__/context-budget.test.ts
@@ -164,7 +164,7 @@ test('compaction notes fire for a fold made by the request hook, not only for a
stage: 'activeStep',
sourceKind: 'runtimeEvents',
decision: 'replaced',
- boundaryKind: 'activeToolResultPrune',
+ boundaryKind: 'toolResultPrune',
},
],
} as never),
diff --git a/packages/runtime/src/__tests__/conversation-copy.test.ts b/packages/runtime/src/__tests__/conversation-copy.test.ts
index 139f129571..cdedafb243 100644
--- a/packages/runtime/src/__tests__/conversation-copy.test.ts
+++ b/packages/runtime/src/__tests__/conversation-copy.test.ts
@@ -18,6 +18,7 @@
*/
import assert from 'node:assert/strict';
+import { readToolResultPage } from '../read-page.js';
import {
createLedgerToolResultArchiveReader,
createLedgerArchiveResourceReader,
@@ -85,7 +86,6 @@ import {
} from '../model-projection-transition-ledger.js';
import {
archivedToolResultProjection,
- collectReachableArchiveArtifactIds,
serializedToolResultProjection,
} from '../tool-result-archive-transition.js';
import {
@@ -2828,7 +2828,7 @@ for (const inspectFirst of [false, true]) {
version: 1,
kind: 'content',
parts: [
- { kind: 'text', text: 'caption' },
+ { kind: 'text', text: 'caption'.repeat(1500) },
{ kind: 'artifact', mediaType: 'image/png', ref: imageRef },
],
};
@@ -2901,22 +2901,47 @@ for (const inspectFirst of [false, true]) {
data: { runtimeEventId: result.id, part: 'tool_result', transition },
});
const sourceRecords = await runStore.readEvents('session-source', 'run-source');
+ const firstPage = readToolResultPage(serialized, { path: placeholder.resourceRef! });
+ assert.ok(firstPage.next?.path.startsWith('maka://read/'));
+ for (const event of [
+ runtimeEvent({
+ id: 'page-call',
+ ts: 4.1,
+ role: 'model',
+ author: 'agent',
+ content: {
+ kind: 'function_call',
+ id: 'page-tool',
+ name: 'Read',
+ args: { path: placeholder.resourceRef! },
+ },
+ }),
+ runtimeEvent({
+ id: 'page-response',
+ ts: 4.2,
+ role: 'tool',
+ author: 'tool',
+ content: {
+ kind: 'function_response',
+ id: 'page-tool',
+ name: 'Read',
+ result: { kind: 'json', value: firstPage },
+ modelProjection: { version: 1, kind: 'json', value: firstPage as never },
+ },
+ }),
+ ])
+ await runtimeEventStore.appendRuntimeEvent('session-source', 'run-source', event);
+
if (inspectFirst) {
const resource = createLedgerArchiveResourceReader({
read: async () => ({ ok: true, event: result, transitions: sourceRecords }),
});
- const inspected = await readToolResultArchiveResource(
- {
- readArchivedToolResultResource: (input) => {
- assert.equal(input.storage, 'ledger');
- return input.storage === 'ledger'
- ? resource(input)
- : { ok: false, reason: 'not_found' };
- },
- },
- 'session-source',
- { ref: placeholder.resourceRef!, operation: 'inspect' },
- );
+ const inspected = {
+ kind: 'tool_result_archive',
+ ref: placeholder.resourceRef,
+ storage: 'ledger',
+ runtimeEventId: result.id,
+ };
await runtimeEventStore.appendRuntimeEvent(
'session-source',
'run-source',
@@ -3037,7 +3062,7 @@ for (const inspectFirst of [false, true]) {
const source = await new RuntimeReadModel({ runtimeEventStore }).getSessionView(
'session-source',
);
- await cloneConversationRuntimeLedger({
+ const copiedLedger = await cloneConversationRuntimeLedger({
plan: await prepareTestCopyPlan(source, source.messages, runStore, runtimeEventStore),
copiedMessages: source.messages,
referenceMap: {
@@ -3072,6 +3097,29 @@ for (const inspectFirst of [false, true]) {
assert.equal(copiedPlaceholder.rewriteVersion, 2);
assert.notEqual(copiedPlaceholder.bodySha256, placeholder.bodySha256);
assert.notEqual(copiedPlaceholder.resourceRef, placeholder.resourceRef);
+ const copiedPageEvent = targetEvents.find(
+ (event) => event.content?.kind === 'function_response' && event.content.id === 'page-tool',
+ );
+ assert.ok(copiedPageEvent?.content?.kind === 'function_response');
+ const copiedPage = (
+ copiedPageEvent.content.result as {
+ value: { next: { path: string }; continuationReset: string };
+ }
+ ).value;
+ assert.deepEqual(copiedPage.next, { path: copiedPlaceholder.resourceRef });
+ assert.match(copiedPage.continuationReset, /copied resource changed/);
+ assert.deepEqual(
+ copiedPageEvent.content.modelProjection?.kind === 'json'
+ ? copiedPageEvent.content.modelProjection.value
+ : undefined,
+ copiedPage,
+ );
+ const storedPage = copiedLedger.copiedMessages.find(
+ (message) => message.type === 'tool_result' && message.toolUseId === 'page-tool',
+ );
+ assert.ok(storedPage?.type === 'tool_result');
+ assert.deepEqual(storedPage.content, { kind: 'json', value: copiedPage });
+
const read = createLedgerToolResultArchiveReader({
read: async () => ({
ok: true,
@@ -3118,12 +3166,14 @@ for (const inspectFirst of [false, true]) {
const reread = await readToolResultArchiveResource(
{
readArchivedToolResultResource: (input) =>
- input.storage === 'ledger' ? resource(input) : { ok: false, reason: 'not_found' },
+ input.storage === 'ledger' || input.storage === 'event'
+ ? resource(input)
+ : { ok: false, reason: 'not_found' },
},
'session-target',
- { ref: output.ref, operation: 'read' },
+ { path: output.ref },
);
- assert.equal((reread as { ok: boolean }).ok, true);
+ assert.equal('content' in (reread as object), true);
const dependent = transitions.transitions.find(
(transition) => transition.target.runtimeEventId === response.id,
)!;
@@ -3343,10 +3393,6 @@ test('conversation copy rebuilds projection transitions against the copied event
assert.equal(effective.content.result.runtimeEventId, targetResult.id);
// Only the surviving placeholder's archive is reachable; the one it
// superseded is not, and cleanup may reclaim it.
- assert.deepEqual(
- [...collectReachableArchiveArtifactIds(reduced.events)],
- ['artifact-target-2'],
- );
} finally {
await rm(root, { recursive: true, force: true });
}
@@ -3512,10 +3558,6 @@ test('conversation copy carries a transition recorded by a later, uncopied run',
const reduced = reduceEffectiveModelProjections(targetEvents, copied.transitions);
assert.equal(reduced.applied.length, 1);
assert.doesNotMatch(JSON.stringify(reduced.events), /SECRET_ARCHIVED_TOOL_RESULT_BODY/);
- assert.deepEqual(
- [...collectReachableArchiveArtifactIds(reduced.events)],
- ['artifact-target-1'],
- );
} finally {
await rm(root, { recursive: true, force: true });
}
diff --git a/packages/runtime/src/__tests__/execution-boundary-test-helpers.ts b/packages/runtime/src/__tests__/execution-boundary-test-helpers.ts
index 936dfb8ef3..83b4527d3a 100644
--- a/packages/runtime/src/__tests__/execution-boundary-test-helpers.ts
+++ b/packages/runtime/src/__tests__/execution-boundary-test-helpers.ts
@@ -156,7 +156,6 @@ export function testToolResultArchive(
): ToolResultArchiveCapability {
return createToolResultArchiveCapability({
archiveToolResult: async () => undefined,
- readToolResultArchive: async () => ({ ok: false, reason: 'not_found' }),
readArchivedToolResultResource: async () => ({ ok: false, reason: 'not_found' }),
...services,
});
diff --git a/packages/runtime/src/__tests__/filesystem-authority.test.ts b/packages/runtime/src/__tests__/filesystem-authority.test.ts
index ed20bbbe63..7ce6731c5b 100644
--- a/packages/runtime/src/__tests__/filesystem-authority.test.ts
+++ b/packages/runtime/src/__tests__/filesystem-authority.test.ts
@@ -106,7 +106,7 @@ describe('file tools follow the execution boundary', () => {
assert.strictEqual(await readFile(target, 'utf8'), 'hello');
const read = await runTool(toolNamed(tools, 'Read'), { path: target }, cwd, BYPASS);
- assert.deepStrictEqual(read, { content: 'hello' });
+ assert.partialDeepStrictEqual(read, { content: 'hello', next: null });
const edited = await runTool(
toolNamed(tools, 'Edit'),
@@ -194,6 +194,10 @@ describe('file tools follow the execution boundary', () => {
await runTool(toolNamed(tools, 'Read'), { path: 'link.txt' }, cwd, BYPASS),
{
content: 'secret',
+ offset: 0,
+ returnedLines: 1,
+ totalLines: 1,
+ next: null,
},
);
} finally {
diff --git a/packages/runtime/src/__tests__/ledger-tool-result-archive-reader.test.ts b/packages/runtime/src/__tests__/ledger-tool-result-archive-reader.test.ts
index 6d4f3af719..d2d25ceee2 100644
--- a/packages/runtime/src/__tests__/ledger-tool-result-archive-reader.test.ts
+++ b/packages/runtime/src/__tests__/ledger-tool-result-archive-reader.test.ts
@@ -343,27 +343,20 @@ test('new archive commits only a v2 ledger reference and is readable through the
assert.ok(outcome);
assert.equal(outcome.placeholder.rewriteVersion, 2);
assert.equal(outcome.placeholder.artifactId, undefined);
- assert.match(outcome.placeholder.resourceRef!, /^maka:\/\/archive-ledger\/v1\//);
+ assert.match(outcome.placeholder.resourceRef!, /^maka:\/\/runtime\/tool-results\//);
const reader = createLedgerToolResultArchiveReader(evidence);
assert.deepEqual(await reader({ ...outcome.placeholder, sessionId: 'session' }), {
ok: true,
serializedResult: f.body,
});
const resource = createLedgerArchiveResourceReader(evidence);
- const read = await readToolResultArchiveResource(
- {
- readArchivedToolResultResource: (input) =>
- input.storage === 'ledger' ? resource(input) : { ok: false, reason: 'not_found' },
- },
- 'session',
- { ref: outcome.placeholder.resourceRef!, operation: 'read' },
- );
- assert.match(JSON.stringify(read), /bounded model output/);
- assert.ok(parseToolResultArchiveResourceRef(outcome.placeholder.resourceRef!));
- assert.equal(
- parseToolResultArchiveResourceRef(outcome.placeholder.resourceRef! + '#extra'),
- null,
- );
+ const read = await resource({
+ storage: 'event',
+ runtimeEventId: 'response',
+ sessionId: 'session',
+ maxBytes: 4 * 1024 * 1024,
+ });
+ assert.deepEqual(read, { ok: true, serializedResult: f.body });
});
test('preflight and transition failure leave the source projection unchanged', async () => {
diff --git a/packages/runtime/src/__tests__/mid-turn-capacity-backend.test.ts b/packages/runtime/src/__tests__/mid-turn-capacity-backend.test.ts
index e0a2ad60b3..345d694e96 100644
--- a/packages/runtime/src/__tests__/mid-turn-capacity-backend.test.ts
+++ b/packages/runtime/src/__tests__/mid-turn-capacity-backend.test.ts
@@ -127,7 +127,7 @@ interface MidTurnFixtureOptions {
/** Omit the prior turns so the compaction pool has no safe completed span. */
withoutPriorTurns?: boolean;
/** Enable the default-on active tool-result prune with a tiny threshold. */
- activeToolResultPrune?: boolean;
+ toolResultPrune?: boolean;
/**
* Summarize through the real `buildLlmHistorySummarizer` against a mock
* provider, so the compaction settles a canonical record instead of the
@@ -521,10 +521,18 @@ function buildFixture(options: MidTurnFixtureOptions = {}): MidTurnFixture {
toolExecutions.push(args.path);
if (args.path === 'one.md')
return {
- body: options.firstResult ?? (options.hugeFirstResult ? HUGE_RESULT : RAW_SPAN_ONE),
+ body:
+ options.firstResult ??
+ (options.hugeFirstResult
+ ? options.toolResultPrune
+ ? 'x'.repeat(20_000) + 'HUGE_RESULT_'
+ : HUGE_RESULT
+ : RAW_SPAN_ONE),
};
if (args.path === 'three.md') return { body: ROLLING_TAIL };
- return { body: RAW_SPAN_TWO };
+ return {
+ body: options.toolResultPrune ? 'x'.repeat(20_000) + 'RAW_SPAN_TWO_' : RAW_SPAN_TWO,
+ };
},
},
...(options.bigActiveTool
@@ -565,16 +573,15 @@ function buildFixture(options: MidTurnFixtureOptions = {}): MidTurnFixture {
enabled: true,
midTurn: { enabled: true },
},
- ...(options.activeToolResultPrune
+ ...(options.toolResultPrune
? {
- activeToolResultPrune: {
+ toolResultPrune: {
enabled: true,
- maxCurrentResultEstimatedTokens: 30,
},
}
: {}),
},
- ...(options.activeToolResultPrune
+ ...(options.toolResultPrune
? {
toolResultArchive: testToolResultArchive({
archiveToolResult: () => ({ artifactId: 'artifact-archived-1' }),
@@ -1113,7 +1120,7 @@ function defineMidTurnSuite(consumer: ConsumerMode): void {
});
test('active tool-result prune re-converges the rebuilt tail after a capacity replacement', async () => {
- const fixture = buildFixture({ activeToolResultPrune: true });
+ const fixture = buildFixture({ toolResultPrune: true });
await runFixtureTurn(fixture, consumer);
assert.equal(fixture.model.doStreamCalls.length, 3);
@@ -1128,7 +1135,7 @@ function defineMidTurnSuite(consumer: ConsumerMode): void {
// capacity replacement must not resurrect the raw body.
assert.equal(thirdPrompt.includes('RAW_SPAN_TWO_'), false);
assert.match(thirdPrompt, /artifact-archived-1/);
- assert.match(thirdPrompt, /active_current_turn_tool_result_pruned_before_next_step/);
+ assert.match(thirdPrompt, /tool_result_pruned/);
});
test('compacts at most once per send', async () => {
@@ -1152,7 +1159,7 @@ function defineMidTurnSuite(consumer: ConsumerMode): void {
withoutPriorTurns: true,
hugeFirstResult: true,
finalAtSecondCall: true,
- activeToolResultPrune: true,
+ toolResultPrune: true,
});
await runFixtureTurn(fixture, consumer);
@@ -1425,7 +1432,7 @@ function defineMidTurnSuite(consumer: ConsumerMode): void {
contextWindow: 200,
finalAtSecondCall: true,
hugeFirstResult: true,
- activeToolResultPrune: true,
+ toolResultPrune: true,
finalStepUsage: { input: 50, output: 10 },
});
await runFixtureTurn(fixture, consumer);
diff --git a/packages/runtime/src/__tests__/model-projection-transition-ledger.test.ts b/packages/runtime/src/__tests__/model-projection-transition-ledger.test.ts
index fb54ca3e65..b08cd62c8f 100644
--- a/packages/runtime/src/__tests__/model-projection-transition-ledger.test.ts
+++ b/packages/runtime/src/__tests__/model-projection-transition-ledger.test.ts
@@ -38,8 +38,7 @@ import {
import {
archiveToolResultAsTransition,
archivedToolResultProjection,
- collectReachableArchiveArtifactIds,
- collectStaleToolResultArchiveCandidates,
+ collectToolResultArchiveCandidates,
serializedToolResultProjection,
} from '../tool-result-archive-transition.js';
import {
@@ -152,11 +151,6 @@ describe('effective model projection reduction', () => {
[loser.transitionId],
);
assert.equal(serializedEffective(reduced.events).includes(SECRET), false);
- // The refused writer's archive is named by nothing the model can see.
- assert.deepEqual(
- [...collectReachableArchiveArtifactIds(reduced.events)],
- [winner === first ? 'artifact-a' : 'artifact-b'],
- );
}
});
@@ -177,7 +171,6 @@ describe('effective model projection reduction', () => {
inOrder.applied.map((transition) => transition.transitionId),
[first.transitionId, second.transitionId],
);
- assert.deepEqual([...collectReachableArchiveArtifactIds(inOrder.events)], ['artifact-b']);
});
test('withholds a target whose record this build cannot read', () => {
@@ -209,7 +202,6 @@ describe('effective model projection reduction', () => {
[transition.transitionId],
);
assert.equal(serializedEffective(reduced.events).includes(SECRET), false);
- assert.equal(collectReachableArchiveArtifactIds(reduced.events).size, 0);
});
test('leaves provider-native opaque results alone', () => {
@@ -230,25 +222,24 @@ describe('effective model projection reduction', () => {
assert.deepEqual(reduced.events[0], event);
assert.equal(reduced.applied.length, 0);
assert.equal(reduced.rejected.length, 1);
- assert.equal(collectReachableArchiveArtifactIds(reduced.events).size, 0);
});
test('rolling compaction cannot re-measure or re-archive replaced content', () => {
- const event = toolResultEvent('rt-1', 'turn-1', { body: SECRET.repeat(200) });
+ const event = toolResultEvent('rt-1', 'turn-1', { body: SECRET.repeat(2000) });
const transition = archiveTransition(event);
const reduced = reduceEffectiveModelProjections(
[event, toolResultEvent('rt-2', 'turn-2', { body: 'tail' })],
[transition],
);
- const rawCandidates = collectStaleToolResultArchiveCandidates(
+ const rawCandidates = collectToolResultArchiveCandidates(
[event, toolResultEvent('rt-2', 'turn-2', { body: 'tail' })],
- { enabled: true, maxResultEstimatedTokens: 1, minRecentTurnsFull: 1 },
+ { enabled: true },
1,
);
- const effectiveCandidates = collectStaleToolResultArchiveCandidates(
+ const effectiveCandidates = collectToolResultArchiveCandidates(
reduced.events,
- { enabled: true, maxResultEstimatedTokens: 4096, minRecentTurnsFull: 1 },
+ { enabled: true },
1,
);
@@ -256,7 +247,7 @@ describe('effective model projection reduction', () => {
assert.deepEqual(effectiveCandidates, []);
});
- test('collects a media-bearing result whose reference text is tiny', () => {
+ test('leaves media to the existing image projection policy', () => {
const event = toolResultEvent('rt-1', 'turn-1', 'ok', {
content: {
kind: 'function_response',
@@ -278,24 +269,24 @@ describe('effective model projection reduction', () => {
},
} as Partial<RuntimeEvent>);
- const candidates = collectStaleToolResultArchiveCandidates(
+ const candidates = collectToolResultArchiveCandidates(
[event, toolResultEvent('rt-2', 'turn-2', { body: 'tail' })],
- { enabled: true, maxResultEstimatedTokens: 2048, minRecentTurnsFull: 1 },
+ { enabled: true },
4,
);
assert.deepEqual(
candidates.map((candidate) => candidate.runtimeEventId),
- ['rt-1'],
+ [],
);
});
test('leaves a text result under the size gate alone', () => {
const event = toolResultEvent('rt-1', 'turn-1', { body: 'x'.repeat(4_000) });
- const candidates = collectStaleToolResultArchiveCandidates(
+ const candidates = collectToolResultArchiveCandidates(
[event, toolResultEvent('rt-2', 'turn-2', { body: 'tail' })],
- { enabled: true, maxResultEstimatedTokens: 2048, minRecentTurnsFull: 1 },
+ { enabled: true },
4,
);
@@ -343,7 +334,7 @@ describe('durable transition writer', () => {
durableToolResultProjectionDigest(baseToolResultProjection(event)!),
);
const reduced = reduceEffectiveModelProjections([event], recorded);
- assert.equal(serializedEffective(reduced.events).includes(SECRET), false);
+ assert.ok(JSON.stringify(reduced.events[0]?.content).includes('page'));
});
test('a writer shows the transition the fold accepts, not the one it wrote', async () => {
@@ -381,7 +372,7 @@ describe('durable transition writer', () => {
assert.ok(effective?.content?.kind === 'function_response');
assert.ok(isArchivedToolResultPlaceholder(effective.content.result));
assert.equal(effective.content.result.artifactId, second.placeholder.artifactId);
- assert.equal(serializedEffective(reduced.events).includes(SECRET), false);
+ assert.ok(JSON.stringify(reduced.events[0]?.content).includes('page'));
}
});
@@ -418,7 +409,7 @@ describe('durable transition writer', () => {
assert.equal(outcome, undefined);
const reduced = reduceEffectiveModelProjections([event], []);
- assert.equal(collectReachableArchiveArtifactIds(reduced.events).has('artifact-orphan'), false);
+
assert.ok(serializedEffective(reduced.events).includes(SECRET));
});
});
diff --git a/packages/runtime/src/__tests__/overflow-reactive-recovery.test.ts b/packages/runtime/src/__tests__/overflow-reactive-recovery.test.ts
index e33a4b202f..8d8119456b 100644
--- a/packages/runtime/src/__tests__/overflow-reactive-recovery.test.ts
+++ b/packages/runtime/src/__tests__/overflow-reactive-recovery.test.ts
@@ -127,7 +127,7 @@ type CallKind =
| 'partialThenOverflowPart';
const RETRY_STEP_TEXT_SENTINEL = 'RETRY_STEP_TEXT_SENTINEL reasoning before the big read';
-const BIG_RESULT = 'BIG_RESULT_'.repeat(200);
+const BIG_RESULT = 'x'.repeat(20_000) + 'BIG_RESULT_';
interface ReactiveFixtureOptions {
script: CallKind[];
@@ -174,7 +174,7 @@ interface ReactiveFixtureOptions {
*/
slowAppendMessage?: boolean;
/** Enable the active tool-result prune with a small threshold + archive seam. */
- activeToolResultPrune?: boolean;
+ toolResultPrune?: boolean;
/** Test-only gate before a numbered provider request starts. */
beforeStream?: (call: number) => Promise<void>;
/** Test-only replacement for the Runtime-owned retry clock. */
@@ -722,11 +722,9 @@ function buildReactiveFixture(options: ReactiveFixtureOptions): ReactiveFixture
enabled: true,
...(midTurnEnabled ? { midTurn: { enabled: true } } : {}),
},
- ...(options.activeToolResultPrune
- ? { activeToolResultPrune: { enabled: true, maxCurrentResultEstimatedTokens: 100 } }
- : {}),
+ ...(options.toolResultPrune ? { toolResultPrune: { enabled: true } } : {}),
},
- ...(options.activeToolResultPrune
+ ...(options.toolResultPrune
? {
toolResultArchive: testToolResultArchive({
archiveToolResult: () => ({ artifactId: 'artifact-archived-1' }),
@@ -1777,7 +1775,7 @@ describe('reactive overflow recovery in the streaming backend', () => {
const fixture = buildReactiveFixture({
script: ['bigread', 'overflow', 'done'],
bigPriors: true,
- activeToolResultPrune: true,
+ toolResultPrune: true,
});
await runTurn(fixture);
diff --git a/packages/runtime/src/__tests__/read-page.test.ts b/packages/runtime/src/__tests__/read-page.test.ts
new file mode 100644
index 0000000000..1e1e5f4221
--- /dev/null
+++ b/packages/runtime/src/__tests__/read-page.test.ts
@@ -0,0 +1,87 @@
+/*
+ * 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 { test } from 'node:test';
+import { readPage, readToolResultPage, READ_PAGE_MAX_CHARS, type ReadInput } from '../read-page.js';
+
+test('default and explicit large ranges stay bounded and continue to the requested end', () => {
+ const lines = Array.from({ length: 900 }, (_, i) => `line ${i} ${'x'.repeat(160)}`);
+ for (const limit of [undefined, 80, 1_000_000]) {
+ let input: ReadInput | null = { path: 'file.txt', offset: 200, ...(limit ? { limit } : {}) };
+ const returned: string[] = [];
+ while (input) {
+ const page = readPage(lines.join('\n'), input);
+ assert.ok(JSON.stringify(page).length <= READ_PAGE_MAX_CHARS);
+ assert.ok(page.returnedLines > 0);
+ returned.push(...page.content.split('\n'));
+ assert.notDeepEqual(page.next, input);
+ input = page.next;
+ assert.ok(returned.length <= 700);
+ }
+ assert.deepEqual(returned, lines.slice(200, limit ? 200 + limit : undefined));
+ }
+});
+
+test('a long Unicode line continues without loss and refuses changed content', () => {
+ const content = 'first\n' + '😀\\"'.repeat(12_000) + '\nlast';
+ const pieces: string[] = [];
+ let input: ReadInput | null = { path: 'long.txt', offset: 1, limit: 1 };
+ while (input) {
+ const page = readPage(content, input);
+ assert.ok(JSON.stringify(page).length <= READ_PAGE_MAX_CHARS);
+ assert.ok(page.content.length > 0);
+ assert.ok(!/[\uD800-\uDBFF]$/.test(page.content));
+ pieces.push(page.content);
+ if (page.next)
+ assert.throws(() => readPage(content + 'changed', page.next!), /content changed/);
+ input = page.next;
+ assert.ok(pieces.length < 100);
+ }
+ assert.equal(pieces.join(''), content.split('\n')[1]);
+});
+
+test('archive Read decodes content lines and retains terminal execution metadata', () => {
+ assert.equal(
+ readToolResultPage(JSON.stringify({ content: 'a\nb\nc' }), {
+ path: 'maka://runtime/tool-results/e',
+ offset: 1,
+ limit: 1,
+ }).content,
+ 'b',
+ );
+ const page = readToolResultPage(
+ JSON.stringify({
+ kind: 'terminal',
+ exitCode: 2,
+ output: { stdout: 'one\ntwo', stderr: 'error' },
+ }),
+ { path: 'maka://runtime/tool-results/e' },
+ );
+ assert.equal(page.content, 'one\ntwo\nerror');
+ assert.equal(page.metadata?.exitCode, 2);
+ assert.equal(page.next, null);
+ assert.deepEqual(readPage('', { path: 'empty', offset: 3 }), {
+ content: '',
+ offset: 3,
+ returnedLines: 0,
+ totalLines: 1,
+ next: null,
+ });
+});
diff --git a/packages/runtime/src/__tests__/tool-result-archive-capability-backend.test.ts b/packages/runtime/src/__tests__/tool-result-archive-capability-backend.test.ts
index fc46670ae2..175cf34e00 100644
--- a/packages/runtime/src/__tests__/tool-result-archive-capability-backend.test.ts
+++ b/packages/runtime/src/__tests__/tool-result-archive-capability-backend.test.ts
@@ -28,26 +28,19 @@ import type { SessionHeader } from '@maka/core/session';
import type { AiSdkBackend } from '../ai-sdk-backend.js';
import {
createToolResultArchiveCapability,
+ bindToolResultArchiveDecoder,
type ToolResultArchiveCapability,
} from '../tool-result-archive-capability.js';
-import { buildToolResultArchiveResourceRef } from '../tool-result-archive-resource.js';
-import { ARCHIVED_TOOL_RESULT_REWRITE_VERSION } from '../tool-result-archive.js';
import type { MakaToolContext } from '../tool-runtime.js';
import { createTestAiSdkBackend } from './execution-boundary-test-helpers.js';
-// The pruned tool-result placeholder is a runtime-generated protocol value that
-// names `ArchiveRead` as the way back to the content. These tests observe the
-// tool surface the provider actually receives, not an internal array: the
-// defects in #2025 and on the child-agent path were both "the model was told to
-// call a tool that was never advertised to it".
-
const ZERO_USAGE: LanguageModelV4Usage = {
inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },
outputTokens: { total: 0, text: 0, reasoning: 0 },
};
describe('AiSdkBackend tool-result archive capability', () => {
- test('advertises ArchiveRead when the session archives tool results', async () => {
+ test('advertises Read when the session archives tool results', async () => {
const capturedTools: string[][] = [];
const model = capturingModel(capturedTools);
@@ -60,12 +53,12 @@ describe('AiSdkBackend tool-result archive capability', () => {
);
assert.ok(
- capturedTools[0]?.includes('ArchiveRead'),
+ capturedTools[0]?.includes('Read'),
'a session that archives tool results must advertise the tool its placeholders name',
);
});
- test('advertises no ArchiveRead when the session archives nothing', async () => {
+ test('advertises no Read when the session archives nothing', async () => {
const capturedTools: string[][] = [];
const model = capturingModel(capturedTools);
@@ -79,134 +72,35 @@ describe('AiSdkBackend tool-result archive capability', () => {
assert.ok(capturedTools.length > 0, 'expected the model to be called');
assert.ok(
- !capturedTools[0]?.includes('ArchiveRead'),
+ !capturedTools[0]?.includes('Read'),
'a session that never archives has nothing to decode, so the CLI stays free of the tool',
);
});
- test('the decoder reads back what the writer archived', async () => {
- // Indivisibility is about one authority, not two co-present fields: the
- // tool the model is told to call must reach the storage the writer used.
- const store = new Map<string, string>();
+ test('resource-only Read preserves Session scope and grants no file access', async () => {
+ const seen: string[] = [];
const archive = createToolResultArchiveCapability({
- archiveToolResult: async (event) => {
- store.set(event.bodySha256, event.serializedResult);
- return { artifactId: `artifact-${event.bodySha256.slice(0, 8)}` };
- },
- readToolResultArchive: async () => ({ ok: false, reason: 'not_found' }),
+ archiveToolResult: async () => ({ ledger: true }),
readArchivedToolResultResource: async (input) => {
- const serializedResult = store.get(input.bodySha256);
- return serializedResult === undefined
- ? { ok: false, reason: 'not_found' }
- : { ok: true, serializedResult };
+ seen.push(input.sessionId);
+ return { ok: true, serializedResult: JSON.stringify({ content: 'first\nsecond' }) };
},
});
-
- const serializedResult = JSON.stringify({ kind: 'text', text: 'the pruned body' });
- const bodySha256 = 'a'.repeat(64);
- const written = await archive.services.archiveToolResult({
- sessionId: 'session-1',
- runtimeEventId: 'event-1',
- turnId: 'turn-1',
- toolCallId: 'call-1',
- toolName: 'Bash',
- result: undefined,
- serializedResult,
- bodySha256,
- originalEstimatedTokens: 1_000,
- originalBytes: Buffer.byteLength(serializedResult),
- rewriteVersion: ARCHIVED_TOOL_RESULT_REWRITE_VERSION,
- reason: 'active_current_turn_tool_result_pruned_before_next_step',
- });
- assert.ok(written, 'the writer must report where it archived the body');
- assert.ok(written.artifactId);
-
- const page = (await archive.archiveReadTool.impl(
- {
- ref: buildToolResultArchiveResourceRef({
- artifactId: written.artifactId,
- bodySha256,
- originalBytes: Buffer.byteLength(serializedResult),
- }),
- operation: 'read',
- },
+ const read = bindToolResultArchiveDecoder([], archive)[0]!;
+ const page = await read.impl(
+ { path: 'maka://runtime/tool-results/event-1', offset: 1, limit: 1 },
toolContext(),
- )) as { ok: boolean; content: string };
-
- assert.equal(page.ok, true, 'the ref the placeholder carries must resolve');
- assert.equal(page.content, serializedResult);
- // Kept from the built-in tool contract this replaced: the decoder is a read,
- // which is what lets it travel to a read-only child.
- assert.equal(archive.archiveReadTool.activityKind, 'read');
- });
-
- test('the decoder reads as the invoking session', async () => {
- // A ref carries no session identity, so the session boundary is only as
- // strong as the id the tool passes down from its own invocation context.
- const seen: { sessionId: string }[] = [];
- const archive = createToolResultArchiveCapability({
- archiveToolResult: async () => ({ artifactId: 'artifact-1' }),
- readToolResultArchive: async () => ({ ok: false, reason: 'not_found' }),
- readArchivedToolResultResource: async (input) => {
- seen.push({ sessionId: input.sessionId });
- return { ok: true, serializedResult: JSON.stringify({ kind: 'agent_swarm', items: [] }) };
- },
- });
-
- await archive.archiveReadTool.impl(
- {
- ref: buildToolResultArchiveResourceRef({
- artifactId: `tool-result-archive-${'a'.repeat(32)}`,
- bodySha256: 'b'.repeat(64),
- originalBytes: 128,
- }),
- operation: 'inspect',
- },
- { ...toolContext(), sessionId: 'session-invoking' },
);
-
- assert.equal(seen[0]?.sessionId, 'session-invoking');
- });
-
- test('the decoder ignores itemId outside query instead of refusing the call', async () => {
- const archive = createToolResultArchiveCapability({
- archiveToolResult: async () => ({ artifactId: 'artifact-1' }),
- readToolResultArchive: async () => ({ ok: false, reason: 'not_found' }),
- readArchivedToolResultResource: async () => ({
- ok: true,
- serializedResult: JSON.stringify({ presets: [{ id: 'fast-reader' }] }),
- }),
- });
- const parameters = archive.archiveReadTool.parameters as {
- validate(
- input: unknown,
- ): Promise<{ success: true; value: unknown } | { success: false; error: unknown }>;
- };
- const parsed = await parameters.validate({
- ref: buildToolResultArchiveResourceRef({
- artifactId: `tool-result-archive-${'a'.repeat(32)}`,
- bodySha256: 'b'.repeat(64),
- originalBytes: 128,
- }),
- operation: 'inspect',
- itemId: { malformed: true },
- offset: 'not-a-number',
- limit: -1,
- ignored: true,
- });
-
- assert.equal(parsed.success, true);
-
- const missingQueryItem = await parameters.validate({
- ref: buildToolResultArchiveResourceRef({
- artifactId: `tool-result-archive-${'a'.repeat(32)}`,
- bodySha256: 'b'.repeat(64),
- originalBytes: 128,
- }),
- operation: 'query',
- ignored: true,
- });
- assert.equal(missingQueryItem.success, false, 'required branch fields still fail closed');
+ assert.equal((page as { content: string }).content, 'second');
+ assert.deepEqual(seen, ['session-1']);
+ await assert.rejects(
+ async () => read.impl({ path: '/etc/passwd' }, toolContext()),
+ /File access is not available/,
+ );
+ assert.equal(
+ bindToolResultArchiveDecoder([read], archive).filter((tool) => tool.name === 'Read').length,
+ 1,
+ );
});
});
@@ -224,7 +118,6 @@ function toolContext(): MakaToolContext {
function capability(): ToolResultArchiveCapability {
return createToolResultArchiveCapability({
archiveToolResult: async () => ({ artifactId: 'artifact-1' }),
- readToolResultArchive: async () => ({ ok: false, reason: 'not_found' }),
readArchivedToolResultResource: async () => ({ ok: false, reason: 'not_found' }),
});
}
diff --git a/packages/runtime/src/__tests__/tool-result-archive-resource.test.ts b/packages/runtime/src/__tests__/tool-result-archive-resource.test.ts
deleted file mode 100644
index 7d82d83b2d..0000000000
--- a/packages/runtime/src/__tests__/tool-result-archive-resource.test.ts
+++ /dev/null
@@ -1,526 +0,0 @@
-/*
- * 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 { createHash } from 'node:crypto';
-import { describe, test } from 'node:test';
-import {
- buildToolResultArchiveResourceRef,
- parseToolResultArchiveResourceRef,
- readToolResultArchiveResource,
- TOOL_RESULT_ARCHIVE_MAX_LIMIT,
- TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS,
- TOOL_RESULT_ARCHIVE_MAX_SEARCH_MATCHES,
- type ToolResultArchiveResourceReader,
-} from '../tool-result-archive-resource.js';
-
-describe('tool-result archive resources', () => {
- test('round-trips a first-class archive URI with integrity metadata', () => {
- const body = JSON.stringify({ ok: true });
- const identity = {
- artifactId: 'tool-result-archive-abc',
- bodySha256: sha256(body),
- originalBytes: Buffer.byteLength(body),
- };
- const ref = buildToolResultArchiveResourceRef(identity);
-
- assert.match(ref, /^maka:\/\/archive\//);
- assert.doesNotMatch(ref, /[?&]/);
- assert.deepEqual(parseToolResultArchiveResourceRef(ref), identity);
- assert.equal(parseToolResultArchiveResourceRef('tool-result-archive-abc'), null);
- assert.equal(parseToolResultArchiveResourceRef('maka://runtime/background-tasks/1'), null);
- assert.equal(parseToolResultArchiveResourceRef(`${ref}?unexpected=true`), null);
- assert.equal(
- parseToolResultArchiveResourceRef(`maka://archive/%E0%A4%A/${'0'.repeat(64)}/1`),
- null,
- );
- });
-
- test('inspect exposes a bounded swarm manifest without embedding summaries', async () => {
- const body = JSON.stringify({
- kind: 'agent_swarm',
- status: 'completed',
- items: [
- {
- itemId: 'core',
- status: 'completed',
- childSessionId: 'child-1',
- summary: 'S'.repeat(20_000),
- },
- {
- itemId: 'runtime',
- status: 'completed',
- childSessionId: 'child-2',
- summary: 'R'.repeat(10_000),
- },
- ],
- });
- const { ref, reader } = fixture(body);
- const result = await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'inspect',
- });
- const serialized = JSON.stringify(result);
-
- assert.match(serialized, /"itemCount":2/);
- assert.match(serialized, /"itemId":"core"/);
- assert.match(serialized, /"summaryChars":20000/);
- assert.doesNotMatch(serialized, /SSSSSSSSSS/);
- assert.ok(serialized.length < TOOL_RESULT_ARCHIVE_MAX_LIMIT);
- });
-
- test('keeps an adversarial inspect manifest below the response budget', async () => {
- const body = JSON.stringify({
- kind: 'agent_swarm',
- items: Array.from({ length: 100 }, (_, index) => ({
- itemId: `worker-${index}-${'I'.repeat(2_000)}`,
- profile: 'P'.repeat(2_000),
- agentName: 'N'.repeat(2_000),
- summary: 'S'.repeat(2_000),
- })),
- });
- const { ref, reader } = fixture(body);
- const result = await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'inspect',
- });
- const serialized = JSON.stringify(result);
-
- assert.ok(serialized.length <= TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS);
- assert.equal((result as { itemsTruncated: boolean }).itemsTruncated, true);
- });
-
- test('keeps mixed keys and manifest metadata below the response budget', async () => {
- const body = JSON.stringify({
- kind: 'agent_swarm',
- status: 'completed',
- ...Object.fromEntries(
- Array.from({ length: 24 }, (_, index) => ['key_' + index + '_' + 'K'.repeat(120), 'v']),
- ),
- items: Array.from({ length: 20 }, (_, index) => ({
- itemId: 'worker-' + index + '-' + 'I'.repeat(120),
- profile: 'P'.repeat(120),
- agentName: 'N'.repeat(120),
- summary: 'S'.repeat(120),
- })),
- });
- const { ref, reader } = fixture(body);
- const result = await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'inspect',
- });
-
- assert.ok(JSON.stringify(result).length <= TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS);
- });
-
- test('keeps empty manifests with escaped keys below the response budget', async () => {
- const body = JSON.stringify({
- kind: 'agent_swarm',
- status: 'completed',
- ...Object.fromEntries(
- Array.from({ length: 24 }, (_, index) => ['key_' + index + '_' + '\\'.repeat(120), 'v']),
- ),
- items: [],
- });
- const { ref, reader } = fixture(body);
- const result = await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'inspect',
- });
-
- assert.ok(JSON.stringify(result).length <= TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS);
- });
-
- test('keeps a queryable item after trimming escaped manifest keys', async () => {
- const body = JSON.stringify({
- kind: 'agent_swarm',
- status: 'completed',
- ...Object.fromEntries(
- Array.from({ length: 24 }, (_, index) => ['key_' + index + '_' + '\\'.repeat(160), 'v']),
- ),
- items: [{ itemId: 'core', summary: 'complete' }],
- });
- const { ref, reader } = fixture(body);
- const inspected = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'inspect',
- })) as { items: Array<{ itemId?: string }> };
-
- assert.ok(JSON.stringify(inspected).length <= TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS);
- assert.equal(inspected.items[0]?.itemId, 'core');
-
- const queried = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'query',
- itemId: inspected.items[0]?.itemId,
- })) as { ok: boolean; itemId: string };
- assert.equal(queried.ok, true);
- assert.equal(queried.itemId, 'core');
- });
-
- test('query selects one swarm item and paginates below the prune threshold', async () => {
- const body = JSON.stringify({
- kind: 'agent_swarm',
- items: [
- { itemId: 'core', summary: 'A'.repeat(20_000) },
- { itemId: 'runtime', summary: 'B'.repeat(20_000) },
- ],
- });
- const { ref, reader } = fixture(body);
- const result = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'query',
- itemId: 'runtime',
- offset: 100,
- limit: 2_000,
- })) as Record<string, unknown>;
-
- assert.equal(result.itemId, 'runtime');
- assert.equal(result.offset, 100);
- assert.equal(result.nextOffset, 2_100);
- assert.equal(result.hasMore, true);
- assert.equal((result.content as string).length, 2_000);
- assert.doesNotMatch(result.content as string, /A/);
- });
-
- test('delegates session, size, and checksum authority to the host reader', async () => {
- const body = JSON.stringify({ value: 'hello' });
- const seen: unknown[] = [];
- const { ref } = fixture(body);
- const reader: ToolResultArchiveResourceReader = {
- readArchivedToolResultResource(input) {
- seen.push(input);
- return { ok: false, reason: 'session_mismatch' };
- },
- };
-
- const result = await readToolResultArchiveResource(reader, 'wrong-session', {
- ref,
- operation: 'inspect',
- });
-
- assert.equal((result as { reason: string }).reason, 'session_mismatch');
- assert.equal((seen[0] as { sessionId: string }).sessionId, 'wrong-session');
- assert.equal((seen[0] as { bodySha256: string }).bodySha256, sha256(body));
- });
-});
-
-describe('ArchiveRead retrieval ergonomics', () => {
- test('inspect surfaces a positional index and preview for text payloads', async () => {
- const text = ['line one', 'line two', 'line three'].join('\n');
- const { ref, reader } = textFixture(text);
- const result = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'inspect',
- })) as Record<string, unknown>;
-
- assert.equal(result.valueType, 'text');
- assert.equal(result.totalChars, text.length);
- assert.equal(result.totalLines, 3);
- assert.match(result.preview as string, /line one/);
- });
-
- test('read pages the decoded string for text payloads, not the escaped JSON', async () => {
- const text = 'line1\nline2"quote';
- const { ref, reader } = textFixture(text);
- const result = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'read',
- offset: 0,
- limit: 100,
- })) as Record<string, unknown>;
-
- // Decoded: starts with the real first char and carries a literal newline,
- // rather than the JSON serialization that would begin with a quote and \n.
- assert.equal((result.content as string).startsWith('line1'), true);
- assert.equal((result.content as string).includes('\n'), true);
- assert.equal((result.content as string).includes('\\n'), false);
- });
-
- test('read and search project terminal streams instead of JSON-escaped output', async () => {
- const { ref, reader } = fixture(
- JSON.stringify({
- kind: 'terminal',
- output: { mode: 'pipes', stdout: 'out\nNEEDLE', stderr: 'err' },
- }),
- );
- const inspected = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'inspect',
- })) as Record<string, unknown>;
- assert.equal(inspected.valueType, 'terminal');
- assert.equal(inspected.totalLines, 3);
-
- const page = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'read',
- unit: 'line',
- offset: 1,
- limit: 1,
- })) as Record<string, unknown>;
- assert.equal(page.content, 'NEEDLE');
-
- const searched = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'search',
- pattern: 'needle',
- })) as Record<string, unknown>;
- assert.equal((searched.matches as Array<Record<string, unknown>>)[0]!.line, 2);
- });
-
- test('read and search stderr-only shell output when stdout is empty', async () => {
- const { ref, reader } = fixture(
- JSON.stringify({
- kind: 'shell_run',
- output: { mode: 'pipes', stdout: '', stderr: 'boom: NEEDLE failed' },
- }),
- );
- const inspected = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'inspect',
- })) as Record<string, unknown>;
- assert.equal(inspected.valueType, 'terminal');
- assert.equal(inspected.totalChars, 'boom: NEEDLE failed'.length);
-
- const page = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'read',
- unit: 'line',
- offset: 0,
- limit: 1,
- })) as Record<string, unknown>;
- assert.equal(page.content, 'boom: NEEDLE failed');
-
- const searched = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'search',
- pattern: 'needle',
- })) as Record<string, unknown>;
- assert.equal((searched.matches as Array<Record<string, unknown>>)[0]!.line, 1);
- });
-
- test('read and search project PTY terminal streams', async () => {
- const { ref, reader } = fixture(
- JSON.stringify({
- kind: 'terminal',
- output: { mode: 'pty', scrollback: 'history', screen: 'NEEDLE', lastAlternateScreen: '' },
- }),
- );
- const page = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'read',
- unit: 'line',
- offset: 1,
- limit: 1,
- })) as Record<string, unknown>;
- assert.equal(page.content, 'NEEDLE');
-
- const searched = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'search',
- pattern: 'needle',
- })) as Record<string, unknown>;
- assert.equal((searched.matches as Array<Record<string, unknown>>)[0]!.line, 2);
- });
-
- test('line paging reports an oversized line instead of returning a stuck empty page', async () => {
- const { ref, reader } = textFixture('x'.repeat(8_000));
- const result = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'read',
- unit: 'line',
- offset: 0,
- limit: 1,
- })) as Record<string, unknown>;
-
- assert.equal(result.ok, false);
- assert.equal(result.reason, 'line_too_large');
- assert.equal(result.lineOffset, 0);
- assert.equal(result.lineChars, 8_000);
- });
-
- test('empty text has no phantom line in line pagination', async () => {
- const { ref, reader } = textFixture('');
- const result = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'read',
- unit: 'line',
- offset: 0,
- limit: 1,
- })) as Record<string, unknown>;
-
- assert.equal(result.totalLines, 0);
- assert.equal(result.lineLimit, 0);
- assert.equal(result.nextLineOffset, null);
- assert.equal(result.content, '');
- });
-
- test('read by line range returns whole lines and resumes with nextLineOffset', async () => {
- const text = Array.from({ length: 10 }, (_, index) => `L${index}`).join('\n');
- const { ref, reader } = textFixture(text);
- const result = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'read',
- unit: 'line',
- offset: 2,
- limit: 3,
- })) as Record<string, unknown>;
-
- assert.equal(result.unit, 'line');
- assert.equal(result.lineOffset, 2);
- assert.equal(result.lineLimit, 3);
- assert.equal(result.totalLines, 10);
- assert.equal(result.content, 'L2\nL3\nL4');
- assert.equal(result.nextLineOffset, 5);
- assert.equal(result.hasMore, true);
- });
-
- test('search locates a case-insensitive substring with offsets, lines, and snippets', async () => {
- const text = 'alpha\nbravo NEEDLE charlie\nNEEDLE delta';
- const { ref, reader } = textFixture(text);
- const result = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'search',
- pattern: 'needle',
- })) as Record<string, unknown>;
-
- const matches = result.matches as Array<Record<string, unknown>>;
- assert.equal(result.matchCount, 2);
- assert.equal(matches[0]!.line, 2);
- assert.equal(matches[1]!.line, 3);
- assert.ok((matches[0]!.offset as number) < (matches[1]!.offset as number));
- assert.match(matches[0]!.snippet as string, /NEEDLE/);
- assert.equal(result.hasMore, false);
- });
-
- test('search stays within the response budget and resumes for pathological match counts', async () => {
- const text = 'x'.repeat(200_000).replace(/x/g, 'ab'); // dense, overlapping-free hits of "ab"
- const { ref, reader } = textFixture(text);
- const result = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'search',
- pattern: 'ab',
- })) as Record<string, unknown>;
-
- assert.ok(JSON.stringify(result).length <= TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS);
- assert.ok((result.matchCount as number) <= TOOL_RESULT_ARCHIVE_MAX_SEARCH_MATCHES);
- assert.equal(result.hasMore, true);
- assert.equal(typeof result.nextOffset, 'number');
- });
-
- test('search without a pattern fails closed', async () => {
- const { ref, reader } = textFixture('anything');
- const result = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'search',
- })) as Record<string, unknown>;
-
- assert.equal(result.ok, false);
- assert.equal(result.reason, 'pattern_required');
- });
-
- test('search preserves source offsets when Unicode lowercasing expands a character', async () => {
- const { ref, reader } = textFixture('İA');
- const result = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'search',
- pattern: 'A',
- })) as Record<string, unknown>;
- const match = (result.matches as Array<Record<string, unknown>>)[0]!;
-
- assert.equal(match.offset, 1);
- assert.equal(match.snippet, 'İA');
- });
-
- test('search folds Greek final sigma across case in original coordinates', async () => {
- // "ΟΔΟΣ" ends in a capital sigma whose lowercase form is the contextual
- // final sigma "ς", not the medial "σ". Case-insensitive search must fold the
- // complete string (so a lowercase final-sigma pattern still matches) and must
- // report offsets in the original text coordinate space. A per-code-point or
- // text-lowercasing implementation regresses this: it either misses the match
- // or reports a shifted offset whose follow-up read returns empty content.
- const { ref, reader } = textFixture('ΟΔΟΣ');
- const result = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'search',
- pattern: 'οδος',
- })) as Record<string, unknown>;
- const matches = result.matches as Array<Record<string, unknown>>;
-
- assert.equal(result.matchCount, 1);
- assert.equal(matches[0]!.offset, 0);
- assert.equal(matches[0]!.snippet, 'ΟΔΟΣ');
- });
-
- test('search never resumes before a requested mid-surrogate offset', async () => {
- const { ref, reader } = textFixture('a😀b😀z');
- const result = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'search',
- pattern: '😀',
- offset: 2,
- })) as Record<string, unknown>;
- const match = (result.matches as Array<Record<string, unknown>>)[0]!;
-
- assert.equal(match.offset, 4);
- assert.ok((match.offset as number) >= 2);
- });
-
- test('preserves accepted long search patterns and complete matching snippets', async () => {
- const pattern = 'A'.repeat(256);
- const { ref, reader } = textFixture('prefix ' + pattern + ' suffix');
- const result = (await readToolResultArchiveResource(reader, 'session-1', {
- ref,
- operation: 'search',
- pattern,
- })) as Record<string, unknown>;
- const match = (result.matches as Array<Record<string, unknown>>)[0]!;
-
- assert.equal(result.pattern, pattern);
- assert.match(match.snippet as string, new RegExp(pattern));
- });
-});
-
-function textFixture(text: string): { ref: string; reader: ToolResultArchiveResourceReader } {
- return fixture(JSON.stringify(text));
-}
-
-function fixture(body: string): { ref: string; reader: ToolResultArchiveResourceReader } {
- const identity = {
- artifactId: `tool-result-archive-${'a'.repeat(32)}`,
- bodySha256: sha256(body),
- originalBytes: Buffer.byteLength(body),
- };
- return {
- ref: buildToolResultArchiveResourceRef(identity),
- reader: {
- readArchivedToolResultResource(input) {
- assert.equal(input.artifactId, identity.artifactId);
- assert.equal(input.bodySha256, identity.bodySha256);
- assert.equal(input.originalBytes, identity.originalBytes);
- assert.equal(input.sessionId, 'session-1');
- return { ok: true, serializedResult: body };
- },
- },
- };
-}
-
-function sha256(text: string): string {
- return createHash('sha256').update(text).digest('hex');
-}
diff --git a/packages/runtime/src/active-tool-result-prune.ts b/packages/runtime/src/active-tool-result-prune.ts
deleted file mode 100644
index 11a335e41e..0000000000
--- a/packages/runtime/src/active-tool-result-prune.ts
+++ /dev/null
@@ -1,403 +0,0 @@
-/*
- * 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.
- */
-
-/**
- * Current-Turn Tool Result pruning (#4283).
- *
- * The decision is still local to one request: which completed step's result is
- * large enough, and superseded enough, to be worth replacing before the next
- * provider step. What is no longer local is the RESULT of that decision. Every
- * replacement is committed as a durable projection transition first, so the
- * live continuation, the next Turn, a restart, a branch and a compaction all
- * read the same replaced projection instead of the old per-Turn placeholder map
- * that only the current `send()` could see.
- *
- * A result the durable ledger cannot address — no committed `function_response`
- * for the tool call yet, or a provider-native opaque result — is left alone. A
- * lossy rewrite the ledger cannot explain is exactly the state this protocol
- * exists to make unrepresentable.
- */
-
-import type { JSONValue, ModelMessage } from './model-protocol.js';
-import type { DurableToolResultProjection } from '@maka/core/durable-tool-result-projection';
-
-import {
- estimateTokens,
- finitePositive,
- sha256,
- utf8ByteLength,
-} from './context-budget-helpers.js';
-import {
- planActiveToolResultSupersession,
- type ActiveToolResultCall,
- type ActiveToolResultObservation,
- type ActiveToolResultSupersession,
-} from './active-tool-result-working-set.js';
-import {
- archiveToolResultAsTransition,
- serializedToolResultProjection,
- type ToolResultArchiveTransitionServices,
-} from './tool-result-archive-transition.js';
-import {
- isArchivedToolResultPlaceholder,
- serializeToolResultForArchive,
- type ArchivedToolResultPlaceholder,
-} from './tool-result-archive.js';
-
-export interface ActiveToolResultPrunePolicy {
- enabled: boolean;
- /** Tool result payloads above this estimate are archived and replaced. Defaults to 2048. */
- maxCurrentResultEstimatedTokens?: number;
- /** Superseded results below this estimate stay verbatim. Defaults to 256. */
- minSupersededResultEstimatedTokens?: number;
- /** Do not rewrite before this SDK step. Defaults to 1, so step 0 is untouched. */
- minStepNumber?: number;
-}
-
-const DEFAULT_MAX_CURRENT_RESULT_ESTIMATED_TOKENS = 2048;
-const DEFAULT_MIN_SUPERSEDED_RESULT_ESTIMATED_TOKENS = 256;
-const DEFAULT_CHARS_PER_TOKEN = 4;
-
-/**
- * The durable address of one in-flight tool result.
- *
- * The prune walks provider messages, but a transition names a RuntimeEvent, so
- * the caller must be able to map a provider tool-call id onto the committed
- * response event and the projection currently in effect for it.
- */
-export interface ActiveToolResultProjectionSource {
- runtimeEventId: string;
- turnId: string;
- toolName: string;
- projection: DurableToolResultProjection;
- /** The transition currently in effect for this target, if any. */
- previousTransitionId?: string;
-}
-
-export type ActiveToolResultProjectionResolver = (
- toolCallId: string,
-) =>
- | ActiveToolResultProjectionSource
- | undefined
- | PromiseLike<ActiveToolResultProjectionSource | undefined>;
-
-export interface ActiveToolResultPruneInput {
- messages: readonly ModelMessage[];
- policy: ActiveToolResultPrunePolicy | undefined;
- stepNumber: number;
- turnId: string;
- charsPerToken?: number;
- eligibleToolCallIds?: ReadonlySet<string>;
- completedToolCalls?: readonly ActiveToolResultCall[];
- /** Durable address lookup; without it no rewrite may happen. */
- resolveProjection: ActiveToolResultProjectionResolver;
- /** Archive + transition writer. */
- transitions: ToolResultArchiveTransitionServices;
-}
-
-export interface ActiveToolResultPruneResult {
- messages: ModelMessage[];
- rewritten: number;
- archiveFailures: number;
- diagnosticPatch: ActiveToolResultPruneDiagnosticPatch;
-}
-
-export interface ActiveToolResultPruneDiagnosticPatch {
- activePrunedToolResults?: number;
- activeSupersededToolResults?: number;
- activeDuplicateToolResults?: number;
- activeArchiveFailures?: number;
- activeEstimatedTokensSaved?: number;
-}
-
-type ToolResultPartish = {
- type?: unknown;
- toolCallId?: unknown;
- toolName?: unknown;
- output?: unknown;
- result?: unknown;
- [key: string]: unknown;
-};
-
-type Replacement =
- | { changed: false; archiveFailure?: boolean }
- | {
- changed: true;
- part: ToolResultPartish;
- estimatedTokensSaved: number;
- supersession?: ActiveToolResultSupersession;
- };
-
-function collectSupersessionDecisions(
- input: ActiveToolResultPruneInput,
-): Map<string, ActiveToolResultSupersession> {
- if (!input.completedToolCalls || input.completedToolCalls.length === 0) return new Map();
- const calls = new Map(input.completedToolCalls.map((call) => [call.toolCallId, call]));
- const observations: ActiveToolResultObservation[] = [];
- for (const message of input.messages) {
- if (message.role !== 'tool' || !Array.isArray(message.content)) continue;
- for (const part of message.content as unknown[]) {
- if (!isToolResultPartish(part) || typeof part.toolCallId !== 'string') continue;
- const call = calls.get(part.toolCallId);
- if (!call || call.toolName !== part.toolName) continue;
- const payload = extractPayload(part);
- if (!payload || isArchivedPayload(payload.value)) continue;
- observations.push({
- toolCallId: call.toolCallId,
- toolName: call.toolName,
- input: call.input,
- stepNumber: call.stepNumber,
- bodySha256: sha256(serializeToolResultForArchive(payload.value)),
- isError:
- payload.field === 'output' &&
- (payload.outputKind === 'error-text' || payload.outputKind === 'error-json'),
- eligible:
- input.eligibleToolCallIds === undefined || input.eligibleToolCallIds.has(call.toolCallId),
- });
- }
- }
- return planActiveToolResultSupersession(observations);
-}
-
-export async function rewriteActiveToolResultsInMessages(
- input: ActiveToolResultPruneInput,
-): Promise<ActiveToolResultPruneResult> {
- const policy = input.policy;
- const minStepNumber = Math.max(0, Math.floor(policy?.minStepNumber ?? 1));
- if (policy?.enabled !== true || input.stepNumber < minStepNumber) {
- return { messages: [...input.messages], rewritten: 0, archiveFailures: 0, diagnosticPatch: {} };
- }
-
- const maxResultEstimatedTokens =
- finitePositive(policy.maxCurrentResultEstimatedTokens) ??
- DEFAULT_MAX_CURRENT_RESULT_ESTIMATED_TOKENS;
- const minSupersededResultEstimatedTokens =
- finitePositive(policy.minSupersededResultEstimatedTokens) ??
- DEFAULT_MIN_SUPERSEDED_RESULT_ESTIMATED_TOKENS;
- const charsPerToken = input.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN;
- const supersessionDecisions = collectSupersessionDecisions(input);
-
- let rewritten = 0;
- let activeSupersededToolResults = 0;
- let activeDuplicateToolResults = 0;
- let archiveFailures = 0;
- let activeEstimatedTokensSaved = 0;
- let anyChanged = false;
- const nextMessages: ModelMessage[] = [];
-
- for (const message of input.messages) {
- if (message.role !== 'tool' || !Array.isArray(message.content)) {
- nextMessages.push(message);
- continue;
- }
-
- let nextContent: unknown[] | undefined;
- const originalContent = message.content as unknown[];
- for (let index = 0; index < originalContent.length; index += 1) {
- const part = originalContent[index];
- if (!isToolResultPartish(part)) {
- if (nextContent) nextContent.push(part);
- continue;
- }
-
- const replacement = await rewriteToolResultPart({
- part,
- input,
- charsPerToken,
- maxResultEstimatedTokens,
- minSupersededResultEstimatedTokens,
- supersession: supersessionDecisions.get(part.toolCallId as string),
- });
-
- if (replacement.changed) {
- rewritten += 1;
- if (replacement.supersession) {
- activeSupersededToolResults += 1;
- if (replacement.supersession.reason === 'exact_duplicate') {
- activeDuplicateToolResults += 1;
- }
- }
- activeEstimatedTokensSaved += replacement.estimatedTokensSaved;
- anyChanged = true;
- if (!nextContent) nextContent = originalContent.slice(0, index);
- nextContent.push(replacement.part);
- } else {
- if (replacement.archiveFailure) archiveFailures += 1;
- if (nextContent) nextContent.push(part);
- }
- }
-
- if (nextContent) {
- nextMessages.push({ ...message, content: nextContent } as ModelMessage);
- } else {
- nextMessages.push(message);
- }
- }
-
- return {
- messages: anyChanged ? nextMessages : [...input.messages],
- rewritten,
- archiveFailures,
- diagnosticPatch: {
- ...(rewritten > 0 ? { activePrunedToolResults: rewritten } : {}),
- ...(activeSupersededToolResults > 0 ? { activeSupersededToolResults } : {}),
- ...(activeDuplicateToolResults > 0 ? { activeDuplicateToolResults } : {}),
- ...(archiveFailures > 0 ? { activeArchiveFailures: archiveFailures } : {}),
- ...(activeEstimatedTokensSaved > 0 ? { activeEstimatedTokensSaved } : {}),
- },
- };
-}
-
-async function rewriteToolResultPart(input: {
- part: ToolResultPartish;
- input: ActiveToolResultPruneInput;
- charsPerToken: number;
- maxResultEstimatedTokens: number;
- minSupersededResultEstimatedTokens: number;
- supersession?: ActiveToolResultSupersession;
-}): Promise<Replacement> {
- const { part } = input;
- if (typeof part.toolCallId !== 'string' || typeof part.toolName !== 'string') {
- return { changed: false };
- }
- const eligible = input.input.eligibleToolCallIds;
- if (eligible && !eligible.has(part.toolCallId)) return { changed: false };
-
- const payload = extractPayload(part);
- if (!payload) return { changed: false };
- if (isArchivedPayload(payload.value)) return { changed: false };
-
- // No durable address, no rewrite: the ledger must be able to explain any
- // content the model stops seeing.
- const address = await Promise.resolve(input.input.resolveProjection(part.toolCallId));
- if (!address || address.toolName !== part.toolName) return { changed: false };
- const sourceProjection = address.projection;
- const serializedResult = serializedToolResultProjection(sourceProjection);
- const originalEstimatedTokens = estimateTokens(serializedResult.length, input.charsPerToken);
- if (
- input.supersession
- ? originalEstimatedTokens < input.minSupersededResultEstimatedTokens
- : originalEstimatedTokens <= input.maxResultEstimatedTokens
- ) {
- return { changed: false };
- }
-
- const outcome = await archiveToolResultAsTransition(input.input.transitions, {
- runtimeEventId: address.runtimeEventId,
- turnId: address.turnId,
- toolCallId: part.toolCallId,
- toolName: part.toolName,
- sourceProjection,
- serializedResult,
- originalBytes: utf8ByteLength(serializedResult),
- originalEstimatedTokens,
- reason: 'active_current_turn_tool_result_pruned_before_next_step',
- ...(address.previousTransitionId ? { previousTransitionId: address.previousTransitionId } : {}),
- ...(input.supersession ? { supersession: input.supersession } : {}),
- result: payload.value,
- });
- if (!outcome) return { changed: false, archiveFailure: true };
-
- const placeholderText =
- payload.field === 'output' &&
- (payload.outputKind === 'text' || payload.outputKind === 'error-text')
- ? JSON.stringify(outcome.placeholder)
- : serializeToolResultForArchive(outcome.placeholder);
- const placeholderEstimatedTokens = estimateTokens(placeholderText.length, input.charsPerToken);
-
- return {
- changed: true,
- part: replacePayload(part, payload, outcome.placeholder),
- estimatedTokensSaved: Math.max(0, originalEstimatedTokens - placeholderEstimatedTokens),
- ...(input.supersession ? { supersession: input.supersession } : {}),
- };
-}
-
-function extractPayload(
- part: ToolResultPartish,
-):
- | { field: 'output'; value: unknown; outputKind: string }
- | { field: 'result'; value: unknown }
- | undefined {
- if ('output' in part) {
- const output = part.output;
- if (!output || typeof output !== 'object') return undefined;
- const candidate = output as { type?: unknown; value?: unknown };
- if (
- (candidate.type === 'text' ||
- candidate.type === 'json' ||
- candidate.type === 'error-text' ||
- candidate.type === 'error-json') &&
- 'value' in candidate
- ) {
- return { field: 'output', value: candidate.value, outputKind: candidate.type };
- }
- return undefined;
- }
-
- if ('result' in part) {
- return { field: 'result', value: part.result };
- }
-
- return undefined;
-}
-
-function replacePayload(
- part: ToolResultPartish,
- payload: { field: 'output'; outputKind: string } | { field: 'result' },
- placeholder: ArchivedToolResultPlaceholder,
-): ToolResultPartish {
- if (payload.field === 'result') {
- return { ...part, result: placeholder };
- }
-
- const output = part.output as Record<string, unknown>;
- const nextValue =
- payload.outputKind === 'text' || payload.outputKind === 'error-text'
- ? JSON.stringify(placeholder)
- : (placeholder as unknown as JSONValue);
- return {
- ...part,
- output: {
- ...output,
- value: nextValue,
- },
- };
-}
-
-/**
- * A payload that already IS a placeholder, in either shape the provider format
- * allows: the JSON object, or the serialized text a `text` output carries.
- * Re-archiving one would archive a pointer, not a body.
- */
-function isArchivedPayload(value: unknown): boolean {
- if (isArchivedToolResultPlaceholder(value)) return true;
- if (typeof value !== 'string') return false;
- try {
- return isArchivedToolResultPlaceholder(JSON.parse(value));
- } catch {
- return false;
- }
-}
-
-function isToolResultPartish(value: unknown): value is ToolResultPartish {
- return Boolean(
- value && typeof value === 'object' && (value as ToolResultPartish).type === 'tool-result',
- );
-}
diff --git a/packages/runtime/src/active-tool-result-working-set.ts b/packages/runtime/src/active-tool-result-working-set.ts
deleted file mode 100644
index 6314494a0c..0000000000
--- a/packages/runtime/src/active-tool-result-working-set.ts
+++ /dev/null
@@ -1,272 +0,0 @@
-/*
- * 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.
- */
-
-export type ActiveToolResultSupersessionReason =
- | 'exact_duplicate'
- | 'newer_read_covers_range'
- | 'newer_snapshot'
- | 'failure_resolved';
-
-export interface ActiveToolResultCall {
- toolCallId: string;
- toolName: string;
- input: unknown;
- stepNumber: number;
-}
-
-export interface ActiveToolResultObservation {
- toolCallId: string;
- toolName: string;
- input: unknown;
- stepNumber: number;
- bodySha256: string;
- isError: boolean;
- eligible: boolean;
-}
-
-export interface ActiveToolResultSupersession {
- reason: ActiveToolResultSupersessionReason;
- supersededByToolCallId: string;
- /** Stable evidence fingerprint retained when a newer success resolves a failure. */
- failureBodySha256?: string;
-}
-
-const MAX_NEWER_READ_CANDIDATES_PER_PATH = 64;
-
-interface ReadDescriptor {
- kind: 'read';
- path: string;
- start: number;
- end: number;
-}
-
-interface SnapshotDescriptor {
- kind: 'snapshot';
- key: string;
-}
-
-type ObservationDescriptor = ReadDescriptor | SnapshotDescriptor;
-
-/**
- * Select provider-visible results that a strictly newer completed step makes
- * redundant. The planner is intentionally deterministic and conservative:
- * parallel calls in one step never supersede each other, error results never
- * replace successful evidence, and unknown tools only participate in exact
- * input-and-output deduplication.
- */
-export function planActiveToolResultSupersession(
- observations: readonly ActiveToolResultObservation[],
-): Map<string, ActiveToolResultSupersession> {
- const decisions = new Map<string, ActiveToolResultSupersession>();
- const newestExact = new Map<string, ActiveToolResultObservation>();
- const newerReads = new Map<string, ActiveToolResultObservation[]>();
- const newestSnapshots = new Map<string, ActiveToolResultObservation>();
-
- for (let index = observations.length - 1; index >= 0; index -= 1) {
- const observation = observations[index]!;
- const exactKey = exactObservationKey(observation);
- const exactReplacement = newestExact.get(exactKey);
- if (
- observation.eligible &&
- exactReplacement &&
- exactReplacement.stepNumber > observation.stepNumber
- ) {
- decisions.set(observation.toolCallId, {
- reason: 'exact_duplicate',
- supersededByToolCallId: exactReplacement.toolCallId,
- });
- }
-
- const descriptor = describeObservation(observation.toolName, observation.input);
- if (observation.eligible && !decisions.has(observation.toolCallId) && descriptor) {
- const replacement = findSemanticReplacement(
- observation,
- descriptor,
- newerReads,
- newestSnapshots,
- );
- if (replacement) decisions.set(observation.toolCallId, replacement);
- }
-
- if (!newestExact.has(exactKey)) newestExact.set(exactKey, observation);
- if (observation.isError || !descriptor) continue;
- if (descriptor.kind === 'read') {
- const reads = newerReads.get(descriptor.path) ?? [];
- if (reads.length < MAX_NEWER_READ_CANDIDATES_PER_PATH) reads.push(observation);
- newerReads.set(descriptor.path, reads);
- } else if (!newestSnapshots.has(descriptor.key)) {
- newestSnapshots.set(descriptor.key, observation);
- }
- }
-
- return decisions;
-}
-
-function findSemanticReplacement(
- observation: ActiveToolResultObservation,
- descriptor: ObservationDescriptor,
- newerReads: ReadonlyMap<string, readonly ActiveToolResultObservation[]>,
- newestSnapshots: ReadonlyMap<string, ActiveToolResultObservation>,
-): ActiveToolResultSupersession | undefined {
- let replacement: ActiveToolResultObservation | undefined;
- if (descriptor.kind === 'read') {
- replacement = newerReads.get(descriptor.path)?.find((candidate) => {
- if (candidate.stepNumber <= observation.stepNumber) return false;
- const candidateDescriptor = describeRead(candidate.input);
- return Boolean(candidateDescriptor && readCovers(candidateDescriptor, descriptor));
- });
- } else {
- const candidate = newestSnapshots.get(descriptor.key);
- if (candidate && candidate.stepNumber > observation.stepNumber) replacement = candidate;
- }
- if (!replacement) return undefined;
- return {
- reason: observation.isError
- ? 'failure_resolved'
- : descriptor.kind === 'read'
- ? 'newer_read_covers_range'
- : 'newer_snapshot',
- supersededByToolCallId: replacement.toolCallId,
- ...(observation.isError ? { failureBodySha256: observation.bodySha256 } : {}),
- };
-}
-
-function exactObservationKey(observation: ActiveToolResultObservation): string {
- return `${observation.toolName}\u0000${stableStringify(observation.input)}\u0000${observation.isError ? 'error' : 'success'}\u0000${observation.bodySha256}`;
-}
-
-function describeObservation(toolName: string, input: unknown): ObservationDescriptor | undefined {
- if (toolName === 'Read') return describeRead(input);
- if (toolName === 'Glob') return describeGlob(input);
- if (toolName === 'Grep') return describeGrep(input);
- if (toolName === 'Bash') return describeBash(input);
- return undefined;
-}
-
-function describeRead(input: unknown): ReadDescriptor | undefined {
- const record = asRecord(input);
- if (!record || typeof record.path !== 'string' || record.path.length === 0) {
- return undefined;
- }
- const start = nonNegativeInteger(record.offset) ?? 0;
- const limit = positiveInteger(record.limit);
- return {
- kind: 'read',
- path: normalizeSubjectPath(record.path),
- start,
- end: limit === undefined ? Number.POSITIVE_INFINITY : start + limit,
- };
-}
-
-function describeGlob(input: unknown): SnapshotDescriptor | undefined {
- const record = asRecord(input);
- if (!record || typeof record.pattern !== 'string') return undefined;
- if (record.cwd !== undefined && typeof record.cwd !== 'string') return undefined;
- return {
- kind: 'snapshot',
- key: `Glob\u0000${stableStringify({
- pattern: record.pattern,
- cwd: normalizeSubjectPath(record.cwd ?? '.'),
- })}`,
- };
-}
-
-function describeGrep(input: unknown): SnapshotDescriptor | undefined {
- const record = asRecord(input);
- if (!record || typeof record.pattern !== 'string') return undefined;
- if (record.path !== undefined && typeof record.path !== 'string') return undefined;
- if (record.glob !== undefined && typeof record.glob !== 'string') return undefined;
- return {
- kind: 'snapshot',
- key: `Grep\u0000${stableStringify({
- pattern: record.pattern,
- path: normalizeSubjectPath(record.path ?? '.'),
- glob: record.glob ?? null,
- })}`,
- };
-}
-
-function describeBash(input: unknown): SnapshotDescriptor | undefined {
- const record = asRecord(input);
- if (!record || typeof record.command !== 'string') return undefined;
- if (
- (record.run_in_background !== undefined && typeof record.run_in_background !== 'boolean') ||
- (record.pty !== undefined && typeof record.pty !== 'boolean')
- ) {
- return undefined;
- }
- // Background and PTY results are process handles or terminal snapshots, not
- // newer observations of a foreground command. Keep each one independently
- // visible; exact input-and-output deduplication still handles true repeats.
- if (record.run_in_background === true || record.pty === true) return undefined;
- const command = record.command.trim();
- if (!isSupportedGitSnapshotCommand(command)) return undefined;
- return { kind: 'snapshot', key: `Bash\u0000${command}` };
-}
-
-function isSupportedGitSnapshotCommand(command: string): boolean {
- if (command.length === 0 || /[;&|<>`\n\r]/.test(command) || command.includes('$(')) return false;
- // Executable test/build output is evidence from a particular run, not a
- // snapshot of current state: an incremental rerun can omit an earlier
- // warning, and a passing rerun must not erase a flaky failure. Those calls
- // remain eligible for exact input-and-output deduplication only.
- return /^git(?:\s+-C\s+\S+)?\s+(?:status|diff|log|show|branch|rev-parse)(?:\s|$)/.test(command);
-}
-
-function readCovers(candidate: ReadDescriptor, original: ReadDescriptor): boolean {
- return candidate.start <= original.start && candidate.end >= original.end;
-}
-
-function normalizeSubjectPath(value: string): string {
- // Keep identity lexical and platform-independent. Leading whitespace and
- // backslashes are valid POSIX filename characters, repeated leading slashes
- // can carry implementation-defined meaning, and a Windows UNC prefix must
- // not collapse into a POSIX-looking path. Removing an explicit leading `./`
- // segment is the only equivalence shared by the supported executors. Prefer
- // false negatives here: semantic supersession must never merge distinct
- // filesystem subjects.
- let normalized = value;
- while (normalized.startsWith('./')) normalized = normalized.slice(2);
- return normalized.length === 0 ? '.' : normalized;
-}
-
-function asRecord(value: unknown): Record<string, unknown> | undefined {
- return value && typeof value === 'object' && !Array.isArray(value)
- ? (value as Record<string, unknown>)
- : undefined;
-}
-
-function nonNegativeInteger(value: unknown): number | undefined {
- return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : undefined;
-}
-
-function positiveInteger(value: unknown): number | undefined {
- return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : undefined;
-}
-
-function stableStringify(value: unknown): string {
- if (value === undefined) return '';
- if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? '';
- if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
- const record = value as Record<string, unknown>;
- return `{${Object.keys(record)
- .sort()
- .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
- .join(',')}}`;
-}
diff --git a/packages/runtime/src/ai-sdk-compaction-contract.ts b/packages/runtime/src/ai-sdk-compaction-contract.ts
index 62831805fa..084373bba1 100644
--- a/packages/runtime/src/ai-sdk-compaction-contract.ts
+++ b/packages/runtime/src/ai-sdk-compaction-contract.ts
@@ -111,11 +111,8 @@ export interface AiSdkCompactionCapabilities {
/** Optional model-visible context budget and compaction policy. */
contextBudget?: ContextBudgetPolicy;
/**
- * The whole tool-result archive authority (#2026): the writer that durably
- * stores a pruned body, the replay reader that hydrates it back, the
- * ref-addressed reader, and the `ArchiveRead` decoder the placeholder names.
- * Absent means this session archives nothing, which is a valid state — but
- * it can no longer mean "archives without a way back".
+ * Host-owned archive preparation and Session-scoped resource reads.
+ * The backend binds resource access into Read whenever this is present.
*/
toolResultArchive?: ToolResultArchiveCapability;
/** Latest checkpoint loader. */
diff --git a/packages/runtime/src/ai-sdk-compaction.ts b/packages/runtime/src/ai-sdk-compaction.ts
index 5739491dea..9cb9cf832c 100644
--- a/packages/runtime/src/ai-sdk-compaction.ts
+++ b/packages/runtime/src/ai-sdk-compaction.ts
@@ -75,20 +75,14 @@ import type {
RequestProjectionContext,
RequestProjectionStage,
} from './request-projection.js';
-import {
- rewriteActiveToolResultsInMessages,
- type ActiveToolResultProjectionSource,
- type ActiveToolResultPruneDiagnosticPatch,
-} from './active-tool-result-prune.js';
import {
archiveToolResultAsTransition,
- collectStaleToolResultArchiveCandidates,
+ collectToolResultArchiveCandidates,
serializedToolResultProjection,
type ToolResultArchiveTransitionServices,
} from './tool-result-archive-transition.js';
import { estimateTokens } from './context-budget-helpers.js';
import {
- baseToolResultProjection,
reduceEffectiveModelProjections,
type LoadedModelProjectionTransitions,
} from './model-projection-transition-ledger.js';
@@ -588,21 +582,16 @@ export class AiSdkCompaction {
}
/**
- * Fold the durable transition ledger onto this session's prior history, and
- * commit any new stale-result transition the prune policy calls for.
+ * Apply the same bounded-result rule to current and prior events.
*
* This is the one seam where raw RuntimeEvents become effective model
* history: the caller uses the returned events for replay, budgeting and
* compaction alike, so no later stage can read content a transition removed.
*/
- public async prepareContextBudgetPolicy(
+ public async pruneToolResults(
runtimeContext: readonly RuntimeEvent[],
turnId: string,
- ): Promise<{
- policy: ContextBudgetPolicy | undefined;
- events: RuntimeEvent[];
- diagnosticPatch?: Partial<ContextBudgetDiagnostic>;
- }> {
+ ): Promise<{ events: RuntimeEvent[]; diagnosticPatch?: Partial<ContextBudgetDiagnostic> }> {
const policy = this.input.contextBudget;
const loaded = await this.loadModelProjectionTransitions();
let transitions = loaded.transitions;
@@ -611,8 +600,7 @@ export class AiSdkCompaction {
transitions,
loaded.unreadableTargets,
);
- if (!policy) return { policy, events: effective.events };
- let nextPolicy = policy;
+ if (!policy) return { events: effective.events };
let diagnosticPatch: Partial<ContextBudgetDiagnostic> | undefined;
// A chain this reader cannot see in full is a chain it must not extend: a
@@ -622,13 +610,13 @@ export class AiSdkCompaction {
loaded.unreadableTargets.size === 0
? this.toolResultArchiveTransitionServices(turnId)
: undefined;
- if (policy.staleToolResultPrune?.enabled === true && services) {
+ if (policy.toolResultPrune?.enabled === true && services) {
// The decision is taken over EFFECTIVE history, so a result an earlier
// Turn already replaced is never re-measured — or re-archived — at the
// size it used to have.
- const candidates = collectStaleToolResultArchiveCandidates(
+ const candidates = collectToolResultArchiveCandidates(
effective.events,
- policy.staleToolResultPrune,
+ policy.toolResultPrune,
policy.charsPerToken ?? 4,
);
const committed: ModelProjectionTransition[] = [];
@@ -676,7 +664,7 @@ export class AiSdkCompaction {
prunedToolResultEstimatedTokensAfter: estimatedTokensAfter,
archivePlaceholders: committed.length,
archivePlaceholderReasonCounts: {
- stale_tool_result_pruned_before_compact: committed.length,
+ tool_result_pruned: committed.length,
},
}
: {}),
@@ -687,6 +675,24 @@ export class AiSdkCompaction {
}
}
+ return { events: effective.events, ...(diagnosticPatch ? { diagnosticPatch } : {}) };
+ }
+
+ public async prepareContextBudgetPolicy(
+ runtimeContext: readonly RuntimeEvent[],
+ turnId: string,
+ ): Promise<{
+ policy: ContextBudgetPolicy | undefined;
+ events: RuntimeEvent[];
+ diagnosticPatch?: Partial<ContextBudgetDiagnostic>;
+ }> {
+ const policy = this.input.contextBudget;
+ const prepared = await this.pruneToolResults(runtimeContext, turnId);
+ const effective = { events: prepared.events };
+ if (!policy) return { policy, events: effective.events };
+ let nextPolicy = policy;
+ let diagnosticPatch = prepared.diagnosticPatch;
+
let loadedCheckpoint: HistoryCompactCheckpoint | undefined;
try {
loadedCheckpoint = await Promise.resolve(this.input.loadHistoryCompactCheckpoint?.());
@@ -747,99 +753,6 @@ export class AiSdkCompaction {
};
}
- public buildActiveToolResultPruneProjection(
- turnId: string,
- includeNewestStep: boolean,
- onDiagnosticPatch?: (patch: ActiveToolResultPruneDiagnosticPatch) => void,
- ): RequestProjectionStage | undefined {
- const policy = this.input.contextBudget?.activeToolResultPrune;
- if (policy?.enabled !== true) return undefined;
- const services = this.toolResultArchiveTransitionServices(turnId);
- // No durable ledger, no lossy rewrite. The old per-Turn placeholder map let
- // this run prune content that the NEXT request would have shown again.
- if (!services || !this.input.loadTurnRuntimeEvents) return undefined;
-
- // The current Turn reads the same folded history as every other consumer.
- // Nothing here remembers what this run already archived: the ledger says it,
- // and a step that measures the raw body again would archive it again.
- let effective: { events: RuntimeEvent[]; lastApplied: Map<string, string> } | undefined;
- const loadEffectiveTurnEvents = async (): Promise<typeof effective> => {
- if (effective) return effective;
- const loaded = await this.loadModelProjectionTransitions();
- if (loaded.unreadableTargets.size > 0) return undefined;
- let turnEvents: RuntimeEvent[];
- try {
- turnEvents = await this.input.loadTurnRuntimeEvents!(turnId);
- } catch {
- return undefined;
- }
- const reduction = reduceEffectiveModelProjections(turnEvents, loaded.transitions);
- const lastApplied = new Map<string, string>();
- for (const transition of reduction.applied) {
- lastApplied.set(transition.target.runtimeEventId, transition.transitionId);
- }
- effective = { events: reduction.events, lastApplied };
- return effective;
- };
-
- const resolveProjection = async (
- toolCallId: string,
- ): Promise<ActiveToolResultProjectionSource | undefined> => {
- const current = await loadEffectiveTurnEvents();
- if (!current) return undefined;
- const event = current.events.find(
- (candidate) =>
- candidate.partial !== true &&
- candidate.content?.kind === 'function_response' &&
- candidate.content.id === toolCallId,
- );
- if (!event || event.content?.kind !== 'function_response') return undefined;
- const projection = baseToolResultProjection(event);
- if (!projection) return undefined;
- const previousTransitionId = current.lastApplied.get(event.id);
- return {
- runtimeEventId: event.id,
- turnId: event.turnId,
- toolName: event.content.name,
- projection,
- ...(previousTransitionId ? { previousTransitionId } : {}),
- };
- };
-
- return async (options) => {
- const eligibleToolCallIds = collectPrunableCompletedStepToolCallIds(
- options.completedSteps,
- includeNewestStep,
- );
- if (eligibleToolCallIds.size === 0) return undefined;
- // Each provider step rebuilds its messages from the durable Turn ledger,
- // so each step must re-fold it too.
- effective = undefined;
- const rewritten = await rewriteActiveToolResultsInMessages({
- messages: options.messages,
- policy,
- stepNumber: options.stepNumber,
- turnId,
- charsPerToken: this.input.contextBudget?.charsPerToken,
- eligibleToolCallIds,
- completedToolCalls: options.completedSteps.flatMap((step, stepNumber) =>
- (step.toolCalls ?? []).map((call) => ({
- toolCallId: call.toolCallId,
- toolName: call.toolName,
- input: call.input,
- stepNumber,
- })),
- ),
- resolveProjection,
- transitions: services,
- });
- if (hasActiveToolResultPruneDiagnosticPatch(rewritten.diagnosticPatch)) {
- onDiagnosticPatch?.(rewritten.diagnosticPatch);
- }
- return rewritten.rewritten > 0 ? { messages: rewritten.messages } : undefined;
- };
- }
-
/**
* Mid-turn capacity compaction eligibility (issue #882 PR 1). Explicit
* opt-in via `historyCompact.midTurn.enabled`; requires the checkpoint
@@ -1220,7 +1133,8 @@ export class AiSdkCompaction {
// materializable and replay-admissible. A checkpoint that fails either
// check must never be persisted because it would poison every later
// projection.
- const replayPlan = buildRuntimeEventModelReplayPlan(plan.replacementEvents, {
+ const effectiveReplacement = await this.foldEffectiveModelHistory(plan.replacementEvents);
+ const replayPlan = buildRuntimeEventModelReplayPlan(effectiveReplacement, {
toolActivityTurnIds: collectToolActivityTurnIds(orderedEvents),
});
if (
@@ -1379,52 +1293,6 @@ export class AiSdkCompaction {
}
}
-// -- moved helpers (defined in ai-sdk-backend, used only by cache write) -------
-
-function incrementRecord(counts: Record<string, number>, key: string): void {
- counts[key] = (counts[key] ?? 0) + 1;
-}
-
-function mergeCountsInto(
- target: Record<string, number>,
- source: Record<string, number> | undefined,
-): void {
- for (const [key, value] of Object.entries(source ?? {})) {
- target[key] = (target[key] ?? 0) + value;
- }
-}
-
-// -- moved helpers (prepare-step / signature / prune) ------------------------
-
-/**
- * Tool results from the newest completed step have not crossed the provider
- * boundary yet: projection is invoked immediately before the first request
- * that could show those results to the model. By default active pruning defers
- * the newest step and archives only older completed steps, after the model has
- * had one request in which to consume their exact output.
- *
- * `includeNewestStep` widens eligibility to every completed step, including the
- * newest. The caller sets it when mid-turn capacity compaction is active: the
- * final-payload verdict may need an oversized newest result pruned to a
- * placeholder before declaring exhaustion, and capacity/recovery rebuilds
- * re-materialize raw bodies from the ledger that must be re-archived.
- */
-function collectPrunableCompletedStepToolCallIds(
- steps: RequestProjectionContext['completedSteps'],
- includeNewestStep: boolean,
-): Set<string> {
- const out = new Set<string>();
- const prunableSteps = includeNewestStep ? steps : steps.slice(0, -1);
- for (const step of prunableSteps) {
- for (const call of step.toolCalls ?? []) {
- if (typeof call.toolCallId === 'string' && call.toolCallId.length > 0) {
- out.add(call.toolCallId);
- }
- }
- }
- return out;
-}
-
interface AcceptedMidTurnCompactionProjection {
sourceSignatures: readonly string[];
projectedMessages: readonly ModelMessage[];
@@ -1477,18 +1345,6 @@ function stableStringifyForSignature(value: unknown): string {
.join(',')}}`;
}
-export function hasActiveToolResultPruneDiagnosticPatch(
- patch: ActiveToolResultPruneDiagnosticPatch,
-): boolean {
- return (
- (patch.activePrunedToolResults ?? 0) > 0 ||
- (patch.activeSupersededToolResults ?? 0) > 0 ||
- (patch.activeDuplicateToolResults ?? 0) > 0 ||
- (patch.activeArchiveFailures ?? 0) > 0 ||
- (patch.activeEstimatedTokensSaved ?? 0) > 0
- );
-}
-
/**
* Per-send() state for the mid-turn capacity invariant. The coverage pool is
* NOT mirrored here: every trigger reads the current turn's persisted
diff --git a/packages/runtime/src/ai-sdk-turn.ts b/packages/runtime/src/ai-sdk-turn.ts
index 50d544c34c..c3e0c7e401 100644
--- a/packages/runtime/src/ai-sdk-turn.ts
+++ b/packages/runtime/src/ai-sdk-turn.ts
@@ -112,7 +112,6 @@ import {
decodePlaintextResponsesReasoningState,
responsesReasoningItemId,
} from './responses-reasoning-state.js';
-import type { ActiveToolResultPruneDiagnosticPatch } from './active-tool-result-prune.js';
import { finitePositive } from './context-budget-helpers.js';
import type {
AutomaticMemoryCompactionDecision,
@@ -123,11 +122,7 @@ import {
contextDiagnosticsCompactionOf,
type ContextDiagnosticsCompaction,
} from './context-diagnostics.js';
-import {
- AiSdkCompaction,
- hasActiveToolResultPruneDiagnosticPatch,
- hasBlockingReplayDiagnostics,
-} from './ai-sdk-compaction.js';
+import { AiSdkCompaction, hasBlockingReplayDiagnostics } from './ai-sdk-compaction.js';
import { RunTrace } from './run-trace.js';
import {
REQUEST_SANDBOX_BOUNDARY_TOOL_NAME,
@@ -1138,7 +1133,6 @@ export class AiSdkTurn {
return { plan, providerTools: plan.providerTools, modelTools, nestedTools };
};
let { plan, providerTools, modelTools, nestedTools } = snapshotStepTools();
- let activeToolResultPruneDiagnosticPatch: ActiveToolResultPruneDiagnosticPatch = {};
let midTurnCompactDiagnosticPatch: Partial<ContextBudgetDiagnostic> | undefined;
// Tool names the repair path matches a mis-cased call against — follows the
// current step's snapshot so a tool activated mid-turn is repairable on the
@@ -1304,6 +1298,15 @@ export class AiSdkTurn {
};
const loadDurableTurnProjection = async (): Promise<ModelMessage[]> => {
const turnEvents = await loadDurableTurnEvents();
+ const pruned = await this.deps.compaction.pruneToolResults(turnEvents, turnId);
+ if (pruned.diagnosticPatch) {
+ if ((pruned.diagnosticPatch.prunedToolResults ?? 0) > 0)
+ pruneAppliedAtStep = runtimeSteps;
+ midTurnCompactDiagnosticPatch = mergeContextBudgetDiagnosticPatches(
+ midTurnCompactDiagnosticPatch,
+ pruned.diagnosticPatch,
+ );
+ }
const projectionCheckpoint = midTurnState?.projectionCheckpoint;
const rawProjectionEvents = projectionCheckpoint
? [
@@ -1421,26 +1424,11 @@ export class AiSdkTurn {
: undefined,
turnAbortController.signal,
);
- // When mid-turn capacity compaction is active, the prune must also cover
- // the newest completed step; see collectPrunableCompletedStepToolCallIds.
- const activeToolResultPruneIncludesNewestStep = midTurnState !== undefined;
- const activeToolResultPruneHook = this.deps.compaction.buildActiveToolResultPruneProjection(
- turnId,
- activeToolResultPruneIncludesNewestStep,
- (patch) => {
- pruneAppliedAtStep = runtimeSteps;
- activeToolResultPruneDiagnosticPatch = mergeActiveToolResultPruneDiagnosticPatches(
- activeToolResultPruneDiagnosticPatch,
- patch,
- );
- },
- );
const projectCurrentToolAvailability: RequestProjectionStage = (options) =>
plan.projectActiveTools?.(options);
const shapedProjection = composeRequestProjection(
projectCurrentToolAvailability,
midTurnCapacityHook,
- activeToolResultPruneHook,
);
// Hooks shape; nothing measures the final payload. Whether it fits is
// the provider's answer (#4559).
@@ -2081,21 +2069,7 @@ export class AiSdkTurn {
: undefined;
if (recovered) {
overflowRetryUsed = true;
- // Recovery rebuilds the request from the durable ledger, whose
- // tool results intentionally retain their full bodies. Re-enter
- // the active-result projection before dispatch so an archived
- // result cannot reappear in provider context on the retry.
- const recoveredProjection = activeToolResultPruneHook
- ? await activeToolResultPruneHook({
- completedSteps: completedProviderSteps,
- stepNumber: runtimeSteps,
- model,
- messages: recovered.messages,
- activeTools: activeToolsForRequest,
- resolveDispatch,
- })
- : undefined;
- attemptMessages = recoveredProjection?.messages ?? recovered.messages;
+ attemptMessages = recovered.messages;
continue;
}
// Window suggestion (#4559): the provider rejected a request and
@@ -2473,7 +2447,6 @@ export class AiSdkTurn {
tokenUsageCostUsd = this.deps.providerTelemetry.normalizedUsageCostUsd(tokenUsage);
const contextBudgetForUsage = contextBudgetWithRequestProjectionDiagnostics(
contextBudgetForTelemetry,
- activeToolResultPruneDiagnosticPatch,
midTurnCompactDiagnosticPatch,
);
// Persisted alongside the live event so transcript rebuilds from
@@ -2644,7 +2617,6 @@ export class AiSdkTurn {
if (this.watchdog === watchdogState.current) this.watchdog = null;
contextBudgetForTelemetry = contextBudgetWithRequestProjectionDiagnostics(
contextBudgetForTelemetry,
- activeToolResultPruneDiagnosticPatch,
midTurnCompactDiagnosticPatch,
);
// `tokenUsage` still backfills from the completed steps when the send
@@ -3205,19 +3177,6 @@ class ContinuationReplayEmptyError extends Error {
}
}
-function mergeActiveToolResultPruneDiagnosticPatches(
- left: ActiveToolResultPruneDiagnosticPatch,
- right: ActiveToolResultPruneDiagnosticPatch,
-): ActiveToolResultPruneDiagnosticPatch {
- return {
- ...sumOptionalCounts('activePrunedToolResults', left, right),
- ...sumOptionalCounts('activeSupersededToolResults', left, right),
- ...sumOptionalCounts('activeDuplicateToolResults', left, right),
- ...sumOptionalCounts('activeArchiveFailures', left, right),
- ...sumOptionalCounts('activeEstimatedTokensSaved', left, right),
- };
-}
-
function mergeNormalizedUsage(
current: NormalizedAiSdkUsage | undefined,
next: NormalizedAiSdkUsage,
@@ -3242,22 +3201,11 @@ function mergeNormalizedUsage(
};
}
-function sumOptionalCounts<K extends keyof ActiveToolResultPruneDiagnosticPatch>(
- key: K,
- left: ActiveToolResultPruneDiagnosticPatch,
- right: ActiveToolResultPruneDiagnosticPatch,
-): Pick<ActiveToolResultPruneDiagnosticPatch, K> | Record<string, never> {
- const total = (left[key] ?? 0) + (right[key] ?? 0);
- return total > 0 ? ({ [key]: total } as Pick<ActiveToolResultPruneDiagnosticPatch, K>) : {};
-}
-
function contextBudgetWithRequestProjectionDiagnostics(
base: ContextBudgetDiagnostic | undefined,
- patch: ActiveToolResultPruneDiagnosticPatch,
compactionPatch: Partial<ContextBudgetDiagnostic> | undefined,
): ContextBudgetDiagnostic | undefined {
- const prunePatch = hasActiveToolResultPruneDiagnosticPatch(patch) ? patch : undefined;
- const mergedPatch = mergeContextBudgetDiagnosticPatches(prunePatch, compactionPatch);
+ const mergedPatch = compactionPatch;
if (!mergedPatch) return base;
return mergeContextBudgetDiagnostic(base ?? minimalContextBudgetDiagnostic(), mergedPatch);
}
diff --git a/packages/runtime/src/archive-read-tool.ts b/packages/runtime/src/archive-read-tool.ts
deleted file mode 100644
index 92d27df6aa..0000000000
--- a/packages/runtime/src/archive-read-tool.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * 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 { jsonSchema, zodSchema } from 'ai';
-import { z } from 'zod';
-import type { MakaTool } from './tool-runtime.js';
-import {
- readToolResultArchiveResource,
- TOOL_RESULT_ARCHIVE_MAX_LIMIT,
- type ToolResultArchiveResourceReader,
-} from './tool-result-archive-resource.js';
-
-export const ARCHIVE_READ_TOOL_NAME = 'ArchiveRead';
-
-export function buildArchiveReadTool(reader: ToolResultArchiveResourceReader): MakaTool {
- const parameters = z.preprocess(
- cleanArchiveReadInput,
- z
- .object({
- ref: z
- .string()
- .describe(
- 'The exact resourceRef returned in an archived tool-result placeholder (ledger or legacy archive)',
- ),
- operation: z
- .enum(['inspect', 'read', 'query', 'search'])
- .default('inspect')
- .describe(
- 'inspect lists archive metadata/items and a preview; query reads one structured item; read returns one bounded page (by character or line); search locates a substring',
- ),
- unit: z
- .enum(['char', 'line'])
- .default('char')
- .describe('For read: whether offset/limit count characters (default) or whole lines'),
- offset: z
- .number()
- .int()
- .nonnegative()
- .optional()
- .describe(
- 'Zero-based start for pagination: character or line offset for read (per unit), character offset for query, or the character position to resume search from',
- ),
- limit: z
- .number()
- .int()
- .positive()
- .max(TOOL_RESULT_ARCHIVE_MAX_LIMIT)
- .optional()
- .describe(
- `Maximum returned characters (or lines when unit is "line"), capped at ${TOOL_RESULT_ARCHIVE_MAX_LIMIT}`,
- ),
- itemId: z
- .string()
- .min(1)
- .max(256)
- .optional()
- .describe('Structured item id returned by inspect; required for query'),
- pattern: z
- .string()
- .min(1)
- .max(256)
- .optional()
- .describe('Literal, case-insensitive substring to locate; required for search'),
- })
- .strip()
- .superRefine((value, ctx) => {
- if (value.operation === 'query' && !value.itemId) {
- ctx.addIssue({
- code: 'custom',
- path: ['itemId'],
- message: 'itemId is required for query',
- });
- }
- if (value.operation === 'search' && !value.pattern) {
- ctx.addIssue({
- code: 'custom',
- path: ['pattern'],
- message: 'pattern is required for search',
- });
- }
- }),
- );
- const providerSchema = zodSchema(parameters);
- return {
- name: ARCHIVE_READ_TOOL_NAME,
- displayName: 'Read archived result',
- activityKind: 'read',
- description:
- 'Inspect, search, or page through a tool-result archive using its exact resourceRef. Both ledger and legacy archive references are supported. Start with inspect for a preview and the char/line coordinate space. Use operation "search" with a pattern to locate text, operation "read" with unit "line" for line-oriented terminal output, or operation "query" with an itemId for one agent_swarm item. Results are strictly bounded so reading an archive cannot immediately trigger another archive.',
- parameters: jsonSchema(async () => await providerSchema.jsonSchema, {
- validate: async (value) => {
- const result = await parameters.safeParseAsync(value);
- return result.success
- ? { success: true, value: result.data }
- : { success: false, error: result.error };
- },
- }),
- impl: async (input, ctx) =>
- // Detach bounded slices before active tool settlements retain their archive backing.
- structuredClone(
- await readToolResultArchiveResource(reader, ctx.sessionId, input, ctx.abortSignal),
- ),
- };
-}
-
-function cleanArchiveReadInput(input: unknown): unknown {
- if (!input || typeof input !== 'object' || Array.isArray(input)) return input;
- const cleaned = { ...(input as Record<string, unknown>) };
- const operation = cleaned.operation ?? 'inspect';
- if (operation !== 'query') delete cleaned.itemId;
- if (operation !== 'search') delete cleaned.pattern;
- if (operation !== 'read') delete cleaned.unit;
- if (operation === 'inspect') {
- delete cleaned.offset;
- delete cleaned.limit;
- }
- return cleaned;
-}
diff --git a/packages/runtime/src/builtin-tools.ts b/packages/runtime/src/builtin-tools.ts
index 7c5ce0490d..f6967186c1 100644
--- a/packages/runtime/src/builtin-tools.ts
+++ b/packages/runtime/src/builtin-tools.ts
@@ -23,7 +23,13 @@
// filesystem, shell, and network effects.
import { z } from 'zod';
-import { jsonSchema, zodSchema } from 'ai';
+import {
+ READ_DESCRIPTION,
+ readParameters,
+ readPage,
+ readToolResultPage,
+ resolveReadInput,
+} from './read-page.js';
import {
closeSync,
constants,
@@ -201,97 +207,6 @@ export function buildBuiltinTools(options: BuildBuiltinToolsOptions = {}): MakaT
...(options.permissionProfile ? { permissionProfile: options.permissionProfile } : {}),
});
const executionFacts = executor.facts;
- const acceptsResourceRefs = Boolean(options.runtimeResources || options.attachmentResources);
- const readDescription = `Read a text file${options.snapshotImage ? ' or supported image' : ''} from disk${acceptsResourceRefs ? ', or read a whole runtime resource using ref' : ''}.`;
- const pathField = z
- .string()
- .describe('A file path; relative paths are resolved from the session cwd');
- const offsetField = z
- .number()
- .int()
- .nonnegative()
- .describe('Zero-based text file line offset')
- .optional();
- const limitField = z
- .number()
- .int()
- .positive()
- .describe('Maximum text file lines to read')
- .optional();
- const refField = z
- .string()
- .describe('A runtime resource ref provided in the conversation or returned by another tool');
- const fileReadParameters = z
- .object({
- path: pathField,
- offset: offsetField,
- limit: limitField,
- })
- .strict();
- const runtimeResourceReadParameters = z
- .object({
- ref: refField,
- })
- .strict();
- // Some providers serialize every optional field with a default. Normalize
- // only empty fields that cannot carry intent, then let the strict union keep
- // rejecting genuinely ambiguous file-and-resource requests.
- const normalizeProviderReadInput = (value: unknown): unknown => {
- if (typeof value !== 'object' || value === null || Array.isArray(value)) return value;
- const input = value as Record<string, unknown>;
- const ref = input.ref;
- const path = input.path;
- if (typeof ref === 'string' && ref.trim() !== '') {
- if (typeof path !== 'string' || path.trim() !== '') return value;
- return Object.fromEntries(
- Object.entries(input).filter(
- ([key]) => key !== 'path' && key !== 'offset' && key !== 'limit',
- ),
- );
- }
- if (typeof ref !== 'string' || ref.trim() !== '') return value;
- return Object.fromEntries(Object.entries(input).filter(([key]) => key !== 'ref'));
- };
- const strictReadParameters = z.preprocess(
- normalizeProviderReadInput,
- z
- .union([fileReadParameters, runtimeResourceReadParameters])
- .describe('Read a file with path, or a whole runtime resource with ref; provide exactly one'),
- );
- // Provider-facing schema: a single top-level object with every field optional.
- // Anthropic rejects a tool definition whose input schema carries a top-level
- // `anyOf`, so the file-vs-ref exclusivity is stated in the field descriptions
- // here and enforced authoritatively by the strict union in `validate` below
- // (see #1228 — a union-generated `anyOf` had been leaking onto the wire).
- const providerReadParameters = z
- .object({
- path: pathField
- .describe(
- 'A file path; relative paths are resolved from the session cwd. Provide either path (optionally with offset/limit) or ref, never both.',
- )
- .optional(),
- offset: offsetField,
- limit: limitField,
- ref: refField
- .describe(
- 'A runtime resource ref provided in the conversation or returned by another tool. Provide ref on its own, without path/offset/limit; omit it (or leave it empty) when reading a file.',
- )
- .optional(),
- })
- .describe(
- 'Read a file with path (optionally offset/limit), or a whole runtime resource with ref; provide exactly one of path or ref.',
- );
- const providerReadSchema = zodSchema(providerReadParameters);
- const readParameters = acceptsResourceRefs
- ? jsonSchema(async () => await providerReadSchema.jsonSchema, {
- validate: async (value) => {
- const result = await strictReadParameters.safeParseAsync(value);
- return result.success
- ? { success: true, value: result.data }
- : { success: false, error: result.error };
- },
- })
- : fileReadParameters;
const shell = options.shell ?? { plan: defaultShellPlan() };
const sandboxPlatform = options.sandboxPlatform ?? process.platform;
const bashTools = options.shellRuns
@@ -372,7 +287,7 @@ export function buildBuiltinTools(options: BuildBuiltinToolsOptions = {}): MakaT
{
name: 'Read',
activityKind: 'read',
- description: readDescription,
+ description: READ_DESCRIPTION,
parameters: readParameters,
executionFacts,
...(options.releaseImageSnapshot
@@ -406,40 +321,38 @@ export function buildBuiltinTools(options: BuildBuiltinToolsOptions = {}): MakaT
: {}),
impl: async (input, ctx) => {
const { cwd, sessionId, abortSignal } = ctx;
- if ('ref' in input) {
- const { ref } = input;
- if (classifyRuntimeResourceRef(ref) !== 'runtime') {
- throw new Error(`Unsupported runtime resource ref: ${ref}`);
- }
- const attachment = parseAttachmentResourceRef(ref);
+ const resolved = resolveReadInput(input);
+ const path = resolved.path;
+ const runtimeRef = classifyRuntimeResourceRef(path);
+ if (runtimeRef === 'unsupported')
+ throw new Error(`Unsupported Maka address: ${path}. Use a path returned by a tool.`);
+ if (runtimeRef === 'runtime') {
+ const attachment = parseAttachmentResourceRef(path);
if (attachment) {
- if (!options.attachmentResources) {
+ if (!options.attachmentResources)
throw new Error('Attachment resources are not available in this toolset');
- }
- return await options.attachmentResources.readAttachmentResource(
+ const result = await options.attachmentResources.readAttachmentResource(
sessionId,
attachment.artifactId,
abortSignal,
);
+ if (result && typeof result === 'object' && 'kind' in result && result.kind === 'image')
+ return result;
+ return readToolResultPage(JSON.stringify(result), input);
}
if (!options.runtimeResources)
throw new Error('Runtime resources are not available in this toolset');
- return await options.runtimeResources.readRuntimeResource(sessionId, ref, abortSignal);
- }
-
- const { path, offset, limit } = input;
- const runtimeRef = classifyRuntimeResourceRef(path);
- if (runtimeRef === 'unsupported')
- throw new Error(`Unsupported runtime resource ref: ${path}`);
- if (runtimeRef === 'runtime') {
- throw new Error('Runtime resources must be read with the ref parameter, not path');
+ const result = await options.runtimeResources.readRuntimeResource(
+ sessionId,
+ path,
+ abortSignal,
+ );
+ return readToolResultPage(JSON.stringify(result), input);
}
const result = await filesystem.execute({
operation: {
kind: 'read',
path,
- ...(offset !== undefined ? { offset } : {}),
- ...(limit !== undefined ? { limit } : {}),
},
...filesystemCall(ctx),
});
@@ -463,7 +376,7 @@ export function buildBuiltinTools(options: BuildBuiltinToolsOptions = {}): MakaT
'no file content came back',
'the file is empty or missing',
);
- return { content: result.content };
+ return readPage(result.content, input);
},
},
...(executor.applyPatch ? [applyPatchTool] : []),
diff --git a/packages/runtime/src/compaction-boundary.ts b/packages/runtime/src/compaction-boundary.ts
index 54bcfe2e2a..d537f888a9 100644
--- a/packages/runtime/src/compaction-boundary.ts
+++ b/packages/runtime/src/compaction-boundary.ts
@@ -24,10 +24,7 @@ import type {
export type CompactionStage = 'priorReplay' | 'activeStep';
export type CompactionSourceKind = 'runtimeEvents' | 'providerMessages';
-export type CompactionBoundaryKind =
- | 'historyCompact'
- | 'staleToolResultPrune'
- | 'activeToolResultPrune';
+export type CompactionBoundaryKind = 'historyCompact' | 'toolResultPrune';
export type CompactionDecisionKind = 'unchanged' | 'replaced' | 'failedOpen';
export interface CompactionCoverage {
diff --git a/packages/runtime/src/context-budget-policy.ts b/packages/runtime/src/context-budget-policy.ts
index 3d4580428d..889ef47043 100644
--- a/packages/runtime/src/context-budget-policy.ts
+++ b/packages/runtime/src/context-budget-policy.ts
@@ -48,22 +48,12 @@ export function buildDefaultContextBudgetPolicy(
);
return {
name: options.name ?? 'default-history-budget',
- staleToolResultPrune: {
- enabled: true,
- maxResultEstimatedTokens: 2_048,
- minRecentTurnsFull: 2,
- },
+ toolResultPrune: { enabled: true },
historyCompact: {
enabled: true,
highWaterName: `${surfaceName}-history-compact`,
midTurn: { enabled: true },
},
- activeToolResultPrune: {
- enabled: true,
- maxCurrentResultEstimatedTokens: 2_048,
- minSupersededResultEstimatedTokens: 256,
- minStepNumber: 1,
- },
};
}
diff --git a/packages/runtime/src/context-budget.ts b/packages/runtime/src/context-budget.ts
index b656cd3c8b..55fd8158a9 100644
--- a/packages/runtime/src/context-budget.ts
+++ b/packages/runtime/src/context-budget.ts
@@ -32,8 +32,8 @@ export {
serializeToolResultForArchive,
} from './tool-result-archive.js';
export type {
- StaleToolResultPrunePolicy,
- StaleToolResultArchiveCandidate,
+ ToolResultPrunePolicy,
+ ToolResultArchiveCandidate,
ToolResultArchiveReader,
ToolResultArchiveReaderInput,
ToolResultArchiveReadFailureReason,
@@ -45,8 +45,7 @@ export type {
HistoryCompactionPolicy,
HistoryCompactionReplayResult,
} from './history-compaction.js';
-import type { StaleToolResultPrunePolicy } from './tool-result-archive.js';
-import { type ActiveToolResultPrunePolicy } from './active-tool-result-prune.js';
+import type { ToolResultPrunePolicy } from './tool-result-archive.js';
import {
applyRuntimeEventHistoryCompact as applyRuntimeEventHistoryCompactNarrow,
isHistoryCompactContentEvent,
@@ -65,19 +64,11 @@ import type { HistoryCompactCheckpoint } from './history-compact-checkpoint.js';
export interface ContextBudgetPolicy {
name?: string;
/**
- * Chars-per-token conversion for the CONTENT policies below (how large one
- * Tool Result may be before it is archived) and for diagnostics. It takes
- * part in no context-fit decision: whether a request fits is the provider's
- * answer (#4559). Defaults to 4.
+ * Diagnostic token estimates only. The tool-result size cap is fixed;
+ * whether a whole request fits remains the provider's decision.
*/
charsPerToken?: number;
- /** Optional replay-only pruning for stale oversized tool results before whole-turn compaction. */
- staleToolResultPrune?: StaleToolResultPrunePolicy;
- /**
- * Optional current-turn, provider-visible tool-result pruning before the next
- * AI SDK step. Defaults off and does not mutate persisted session messages.
- */
- activeToolResultPrune?: ActiveToolResultPrunePolicy;
+ toolResultPrune?: ToolResultPrunePolicy;
/** Latest checkpoint projection and automatic capacity settings. */
historyCompact?: HistoryCompactionPolicy;
}
@@ -102,7 +93,7 @@ export function applyRuntimeEventContextBudget(
events: readonly RuntimeEvent[],
policy: ContextBudgetPolicy | undefined,
): BudgetedRuntimeContext | undefined {
- const prunePolicy = policy?.staleToolResultPrune;
+ const prunePolicy = policy?.toolResultPrune;
const pruneEnabled = prunePolicy?.enabled === true;
const historyCompactEnabled = policy?.historyCompact?.enabled === true;
const enabled = pruneEnabled || historyCompactEnabled;
diff --git a/packages/runtime/src/conversation-copy.ts b/packages/runtime/src/conversation-copy.ts
index 5662a681be..eb1a972b05 100644
--- a/packages/runtime/src/conversation-copy.ts
+++ b/packages/runtime/src/conversation-copy.ts
@@ -78,6 +78,7 @@ import {
import { archivedToolResultProjection } from './tool-result-archive-transition.js';
import { serializeToolResultProjectionV1 } from './tool-result-archive-encoding.js';
import { createHash } from 'node:crypto';
+import { readToolResultPage, readableToolResult, READ_PAGE_MAX_CHARS } from './read-page.js';
export interface ConversationCopySlice {
readonly messages: readonly StoredMessage[];
@@ -596,6 +597,23 @@ export async function cloneConversationRuntimeLedger(
if (visiting.has(sourceId)) throw new Error('Cyclic copied archive reference');
visiting.add(sourceId);
const target = clonedEventBySourceId.get(sourceId);
+ if (target?.content?.kind === 'function_response' && target.content.name === 'Read') {
+ target.content.result = rewriteReadPage(
+ target.content.result,
+ references,
+ clonedEventBySourceId,
+ );
+ const projection = target.content.modelProjection;
+ if (projection?.kind === 'json')
+ target.content.modelProjection = {
+ ...projection,
+ value: rewriteReadPage(
+ projection.value,
+ references,
+ clonedEventBySourceId,
+ ) as typeof projection.value,
+ };
+ }
if (target?.content?.kind === 'function_response' && target.content.name === 'ArchiveRead') {
const resolveResult = (value: unknown): unknown =>
rewriteArchiveReadResult(value, references, (ref) => {
@@ -628,6 +646,21 @@ export async function cloneConversationRuntimeLedger(
};
for (const sourceId of clonedEventBySourceId.keys()) finishTarget(sourceId);
for (const event of clonedEventBySourceId.values()) {
+ if (event.content?.kind === 'function_call' && event.content.name === 'Read') {
+ const args = event.content.args;
+ if (args && typeof args === 'object' && 'path' in args && typeof args.path === 'string')
+ event.content = {
+ ...event.content,
+ args: {
+ ...args,
+ ...rewriteReadInput(
+ args as Record<string, unknown> & { path: string },
+ references,
+ clonedEventBySourceId,
+ ),
+ },
+ };
+ }
if (event.content?.kind === 'text')
event.content.text = rewriteLedgerArchiveText(event.content.text, references);
if (event.content?.kind === 'function_call' && event.content.name === 'ArchiveRead') {
@@ -677,7 +710,8 @@ export async function cloneConversationRuntimeLedger(
const archiveReadCalls = new Set(
flattenedPlans.flatMap((plan) =>
plan.events.flatMap((event) =>
- event.content?.kind === 'function_response' && event.content.name === 'ArchiveRead'
+ event.content?.kind === 'function_response' &&
+ (event.content.name === 'ArchiveRead' || event.content.name === 'Read')
? [`${event.turnId}:${event.content.id}`]
: [],
),
@@ -693,7 +727,11 @@ export async function cloneConversationRuntimeLedger(
) {
return {
...copied,
- content: rewriteArchiveReadResult(copied.content, references) as ToolResultContent,
+ content: rewriteReadPage(
+ rewriteArchiveReadResult(copied.content, references),
+ references,
+ clonedEventBySourceId,
+ ) as ToolResultContent,
};
}
return copied;
@@ -758,9 +796,74 @@ function rewriteLedgerArchiveText(
): string {
for (const [source, target] of references.ledgerArchives ?? [])
text = text.split(source).join(target.resourceRef!);
+ for (const [source, target] of references.runtimeEventIds)
+ text = text
+ .split(`maka://runtime/tool-results/${encodeURIComponent(source)}`)
+ .join(`maka://runtime/tool-results/${encodeURIComponent(target)}`);
return text;
}
+function rewriteReadInput(
+ input: Record<string, unknown> & { path: string },
+ references: ConversationCopyMessageReferenceMap,
+ clonedEvents: ReadonlyMap<string, RuntimeEvent>,
+): Record<string, unknown> & { path: string } {
+ const rewrite = (path: string): string =>
+ rewriteLedgerArchiveText(
+ references.mode === 'exact'
+ ? rewriteAttachmentResourceRefs(path, references.artifactIds)
+ : path,
+ references,
+ );
+ if (!input.path.startsWith('maka://read/')) return { ...input, path: rewrite(input.path) };
+ const url = new URL(input.path);
+ const original = Buffer.from(url.pathname.slice(1), 'base64url').toString('utf8');
+ const path = rewrite(original);
+ const prefix = 'maka://runtime/tool-results/';
+ if (original.startsWith(prefix) && path !== original) {
+ const copied = clonedEvents.get(decodeURIComponent(original.slice(prefix.length)));
+ const projection = copied && baseToolResultProjection(copied);
+ const digest =
+ projection &&
+ createHash('sha256')
+ .update(readableToolResult(serializeToolResultProjectionV1(projection)))
+ .digest('hex')
+ .slice(0, 32);
+ if (digest !== url.searchParams.get('sha')) return { path };
+ }
+ url.pathname = `/${Buffer.from(path).toString('base64url')}`;
+ return { ...input, path: url.toString() };
+}
+
+function rewriteReadPage(
+ value: unknown,
+ references: ConversationCopyMessageReferenceMap,
+ clonedEvents: ReadonlyMap<string, RuntimeEvent>,
+): unknown {
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return value;
+ const page = value as Record<string, unknown>;
+ if (page.kind === 'json')
+ return { ...page, value: rewriteReadPage(page.value, references, clonedEvents) };
+ const next = page.next;
+ if (!next || typeof next !== 'object' || !('path' in next) || typeof next.path !== 'string')
+ return value;
+ const rewritten = rewriteReadInput(
+ next as Record<string, unknown> & { path: string },
+ references,
+ clonedEvents,
+ );
+ const reset = next.path.startsWith('maka://read/') && !rewritten.path.startsWith('maka://read/');
+ return {
+ ...page,
+ next: rewritten,
+ ...(reset
+ ? {
+ continuationReset: 'The copied resource changed. Use next to read it from the beginning.',
+ }
+ : {}),
+ };
+}
+
/** Only ArchiveRead's defined envelope is rewritten, never its opaque content/items. */
function rewriteArchiveReadResult(
value: unknown,
@@ -1154,7 +1257,18 @@ function cloneModelProjectionTransition(
}
: {}),
});
- references.ledgerArchives?.set(buildToolResultArchiveResourceRef(placeholder), rewritten);
+ if (rewritten.page) {
+ delete rewritten.page;
+ rewritten.page = readToolResultPage(
+ serialized,
+ { path: rewritten.resourceRef! },
+ READ_PAGE_MAX_CHARS - JSON.stringify(rewritten).length - 32,
+ );
+ }
+ references.ledgerArchives?.set(
+ placeholder.resourceRef ?? buildToolResultArchiveResourceRef(placeholder),
+ rewritten,
+ );
} else {
rewritten = rewriteArchivedToolResult(placeholder, references);
}
diff --git a/packages/runtime/src/ledger-tool-result-archive-reader.ts b/packages/runtime/src/ledger-tool-result-archive-reader.ts
index 41f6c65b29..0524a62ada 100644
--- a/packages/runtime/src/ledger-tool-result-archive-reader.ts
+++ b/packages/runtime/src/ledger-tool-result-archive-reader.ts
@@ -150,14 +150,41 @@ export function createLedgerToolResultArchiveReader(
export function createLedgerArchiveResourceReader(evidence: ToolResultArchiveEvidenceReader) {
const reader = createLedgerToolResultArchiveReader(evidence);
- return (input: LedgerArchiveResourceIdentity & { sessionId: string; maxBytes: number }) =>
- reader({
+ return async (
+ input: (LedgerArchiveResourceIdentity | { storage: 'event'; runtimeEventId: string }) & {
+ sessionId: string;
+ maxBytes: number;
+ },
+ ) => {
+ if (input.storage === 'event') {
+ const loaded = await evidence.read({
+ sessionId: input.sessionId,
+ runtimeEventId: input.runtimeEventId,
+ });
+ if (!loaded.ok) return { ok: false as const, reason: 'not_found' as const };
+ const transitions = loaded.transitions.map((record) =>
+ decodeLedgerTransition(record, input.sessionId),
+ );
+ if (transitions.some((transition) => !transition))
+ return { ok: false as const, reason: 'corrupt' as const };
+ const reduction = reduceEffectiveModelProjections(
+ [loaded.event],
+ transitions as ModelProjectionTransition[],
+ );
+ const replacement = reduction.applied.at(-1)?.replacement;
+ const placeholder: unknown = replacement?.kind === 'json' ? replacement.value : undefined;
+ if (!isArchivedToolResultPlaceholder(placeholder))
+ return { ok: false as const, reason: 'not_found' as const };
+ return reader({ ...placeholder, sessionId: input.sessionId, maxBytes: input.maxBytes });
+ }
+ return reader({
...input,
kind: 'maka.archived_tool_result',
rewriteVersion: 2,
originalEstimatedTokens: 1,
reason: 'stale_tool_result_pruned_before_compact',
});
+ };
}
/** Verify reconstructibility before committing a replacement; does not write any payload. */
diff --git a/packages/runtime/src/read-page.ts b/packages/runtime/src/read-page.ts
new file mode 100644
index 0000000000..88f4b5d37f
--- /dev/null
+++ b/packages/runtime/src/read-page.ts
@@ -0,0 +1,261 @@
+/*
+ * 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 { createHash } from 'node:crypto';
+import { z } from 'zod';
+
+export const READ_PAGE_MAX_CHARS = 7_500;
+export const READ_DESCRIPTION =
+ 'Read a file or a Maka resource. Only path is required. By default, read from the beginning and return one bounded page. offset and limit select a line range; offset is zero-based. A large limit cannot bypass the response-size cap. If next is non-null, pass its contents to Read to continue; otherwise the requested range is complete. Use a smaller limit for a short excerpt. A partialLine page continues automatically through next.';
+export const readParameters = z.object({
+ path: z
+ .string({
+ error: 'path is required. Provide a file path or the Maka address returned by a tool.',
+ })
+ .min(1, 'path cannot be empty. Provide the location to read.')
+ .describe(
+ 'File path relative to the session cwd, an absolute path, or a Maka address returned by a tool.',
+ ),
+ offset: z
+ .number({ error: 'offset must be a non-negative integer. Omit it to start at the beginning.' })
+ .int('offset must be an integer. Omit it to start at the beginning.')
+ .nonnegative('offset must be non-negative. Omit it to start at the beginning.')
+ .optional()
+ .describe(
+ 'Zero-based starting line; defaults to 0. offset 200 starts at line 201. Omit when using a continuation address.',
+ ),
+ limit: z
+ .number({ error: 'limit must be a positive integer. Omit it to read one bounded page.' })
+ .int('limit must be an integer. Omit it to read one bounded page.')
+ .positive('limit must be positive. Omit it to read one bounded page.')
+ .optional()
+ .describe(
+ 'Maximum lines in the requested range. Omit to browse one bounded page at a time. Use next to obtain any remaining content.',
+ ),
+});
+export type ReadInput = z.infer<typeof readParameters>;
+export interface ReadPage {
+ content: string;
+ offset: number;
+ returnedLines: number;
+ totalLines: number;
+ partialLine?: true;
+ next: ReadInput | null;
+ metadata?: Record<string, unknown>;
+}
+
+const CONTINUATION_PREFIX = 'maka://read/';
+
+export function resolveReadInput(input: ReadInput): {
+ path: string;
+ position?: number;
+ digest?: string;
+} {
+ if (!input.path.startsWith(CONTINUATION_PREFIX)) return { path: input.path };
+ try {
+ const url = new URL(input.path);
+ const path = Buffer.from(url.pathname.slice(1), 'base64url').toString('utf8');
+ const position = Number(url.searchParams.get('at'));
+ const digest = url.searchParams.get('sha') ?? '';
+ if (
+ !path ||
+ path.startsWith(CONTINUATION_PREFIX) ||
+ !Number.isSafeInteger(position) ||
+ position < 0 ||
+ !/^[a-f0-9]{32}$/.test(digest) ||
+ input.offset !== undefined
+ )
+ throw new Error();
+ return { path, position, digest };
+ } catch {
+ throw new Error(
+ 'Invalid Read continuation. Copy the complete next object from the preceding result. To start again, use the original path and omit offset and limit.',
+ );
+ }
+}
+
+function digestText(content: string): string {
+ return createHash('sha256').update(content).digest('hex').slice(0, 32);
+}
+
+export function readPage(
+ content: string,
+ input: ReadInput,
+ maxChars = READ_PAGE_MAX_CHARS,
+): ReadPage {
+ const resolved = resolveReadInput(input);
+ if (resolved.digest && digestText(content) !== resolved.digest) {
+ throw new Error(
+ 'The content changed since the previous page. This continuation cannot be used. Read the original path again to start from the current content.',
+ );
+ }
+ if (resolved.position !== undefined && resolved.position > content.length) {
+ throw new Error(
+ 'Invalid Read continuation position. Copy the complete next object from the preceding result.',
+ );
+ }
+ const starts = [0];
+ for (let at = content.indexOf('\n'); at >= 0; at = content.indexOf('\n', at + 1))
+ starts.push(at + 1);
+ const totalLines = starts.length;
+ const lineAt = (position: number): number => {
+ let low = 0;
+ let high = starts.length - 1;
+ while (low < high) {
+ const middle = Math.ceil((low + high) / 2);
+ if (starts[middle]! <= position) low = middle;
+ else high = middle - 1;
+ }
+ return low;
+ };
+ const offset = resolved.position === undefined ? (input.offset ?? 0) : lineAt(resolved.position);
+ const start = resolved.position ?? starts[Math.min(offset, totalLines)] ?? content.length;
+ const lastLine = Math.min(totalLines, offset + (input.limit ?? totalLines));
+ const end = lastLine < totalLines ? starts[lastLine]! - 1 : content.length;
+ let digest = resolved.digest;
+ const makePage = (stop: number): ReadPage => {
+ const complete = stop >= end;
+ const nextLine = Math.max(offset, lineAt(stop));
+ const boundary = stop === (starts[nextLine + 1] ?? content.length + 1) - 1;
+ const partialLine = !complete && !boundary;
+ const returnedLines =
+ offset >= totalLines
+ ? 0
+ : complete
+ ? lastLine - offset
+ : nextLine - offset + (partialLine ? 0 : 1);
+ let next: ReadInput | null = null;
+ if (!complete) {
+ const remaining = input.limit === undefined ? undefined : input.limit - returnedLines;
+ next = partialLine
+ ? {
+ path: `${CONTINUATION_PREFIX}${Buffer.from(resolved.path).toString('base64url')}?at=${stop}&sha=${(digest ??= digestText(content))}`,
+ ...(remaining === undefined ? {} : { limit: remaining }),
+ }
+ : {
+ path: resolved.path,
+ offset: offset + returnedLines,
+ ...(remaining === undefined ? {} : { limit: remaining }),
+ };
+ }
+ return {
+ content: content.slice(start, Math.max(start, stop)),
+ offset,
+ returnedLines,
+ totalLines,
+ ...(partialLine || resolved.position !== undefined ? { partialLine: true as const } : {}),
+ next,
+ };
+ };
+ if (end - start <= maxChars) {
+ const page = makePage(end);
+ if (JSON.stringify(page).length <= maxChars) return page;
+ }
+ let low = start;
+ let high = Math.min(end, start + maxChars);
+ while (low < high) {
+ const middle = Math.ceil((low + high) / 2);
+ if (JSON.stringify(makePage(middle)).length <= maxChars) low = middle;
+ else high = middle - 1;
+ }
+ // Never split a UTF-16 surrogate pair between responses.
+ if (low > start && /[\uD800-\uDBFF]/.test(content[low - 1]!)) low--;
+ if (low <= start)
+ throw new Error(
+ 'The Read address is too long to fit a bounded response. Use a shorter original path.',
+ );
+ const newline = content.lastIndexOf('\n', low);
+ if (newline > start) low = newline;
+ return makePage(low);
+}
+
+export function readableToolResult(serialized: string): string {
+ try {
+ const value: unknown = JSON.parse(serialized);
+ if (typeof value === 'string') return value;
+ if (
+ value &&
+ typeof value === 'object' &&
+ 'kind' in value &&
+ value.kind === 'text' &&
+ 'text' in value &&
+ typeof value.text === 'string'
+ )
+ return value.text;
+ if (
+ value &&
+ typeof value === 'object' &&
+ 'content' in value &&
+ typeof value.content === 'string'
+ )
+ return value.content;
+ if (
+ value &&
+ typeof value === 'object' &&
+ 'kind' in value &&
+ (value.kind === 'terminal' || value.kind === 'shell_run') &&
+ 'output' in value
+ ) {
+ if (typeof value.output === 'string') return value.output;
+ if (value.output && typeof value.output === 'object') {
+ const output = value.output as Record<string, unknown>;
+ return (
+ output.mode === 'pty'
+ ? [output.scrollback, output.screen, output.lastAlternateScreen]
+ : [output.stdout, output.stderr]
+ )
+ .filter((part): part is string => typeof part === 'string' && part.length > 0)
+ .join('\n');
+ }
+ }
+ } catch {
+ /* Plain text projections are already readable. */
+ }
+ return serialized;
+}
+
+export function readToolResultPage(
+ serialized: string,
+ input: ReadInput,
+ maxChars = READ_PAGE_MAX_CHARS,
+): ReadPage {
+ let metadata: Record<string, unknown> | undefined;
+ try {
+ const value = JSON.parse(serialized);
+ if (value?.kind === 'terminal' || value?.kind === 'shell_run') {
+ metadata = Object.fromEntries(
+ Object.entries(value).filter(([key]) =>
+ ['kind', 'status', 'exitCode', 'signal', 'revision', 'mode'].includes(key),
+ ),
+ );
+ if (value.output && typeof value.output === 'object')
+ for (const key of ['stdoutTruncated', 'stderrTruncated', 'redacted']) {
+ if (typeof value.output[key] === 'boolean') metadata[key] = value.output[key];
+ }
+ }
+ } catch {
+ /* Text has no separate execution metadata. */
+ }
+ const page = readPage(
+ readableToolResult(serialized),
+ input,
+ maxChars - (metadata ? JSON.stringify(metadata).length + 16 : 0),
+ );
+ return metadata ? { ...page, metadata } : page;
+}
diff --git a/packages/runtime/src/request-projection.ts b/packages/runtime/src/request-projection.ts
index a9a76323a2..dab3003b92 100644
--- a/packages/runtime/src/request-projection.ts
+++ b/packages/runtime/src/request-projection.ts
@@ -55,27 +55,10 @@ export type RequestProjectionStage = (
context: RequestProjectionContext,
) => RequestProjection | undefined | PromiseLike<RequestProjection | undefined>;
-/**
- * Deterministic request-projection pipeline over ONE provider-visible request.
- * Order is a contract: mid-turn capacity compaction runs first among the
- * message-shaping hooks so every later mechanism operates on its projection —
- * active tool-result pruning re-archives large tool results in the rebuilt
- * tail.
- *
- * Every hook here only SHAPES the projection. The pass/terminate capacity
- * verdict is issued once, after the whole pipeline, by the final-request
- * estimate owner (buildMidTurnFinalRequestVerdict) over the actual outgoing
- * (messages, tools) payload — never by an individual hook over an intermediate
- * projection that a later hook could still rescue.
- */
export function composeRequestProjection(
- toolAvailability: RequestProjectionStage | undefined,
- midTurnCapacityCompact: RequestProjectionStage | undefined,
- activeToolResultPrune: RequestProjectionStage | undefined,
+ ...stages: Array<RequestProjectionStage | undefined>
): RequestProjectionStage | undefined {
- const hooks = [toolAvailability, midTurnCapacityCompact, activeToolResultPrune].filter(
- Boolean,
- ) as RequestProjectionStage[];
+ const hooks = stages.filter((stage): stage is RequestProjectionStage => stage !== undefined);
if (hooks.length === 0) return undefined;
return async (context: RequestProjectionContext): Promise<RequestProjection | undefined> => {
let result: RequestProjection | undefined;
diff --git a/packages/runtime/src/session-manager.ts b/packages/runtime/src/session-manager.ts
index 6412669798..7ba804a9cd 100644
--- a/packages/runtime/src/session-manager.ts
+++ b/packages/runtime/src/session-manager.ts
@@ -693,13 +693,9 @@ export interface BackendFactoryContext {
* policy, but must never append, substitute, or otherwise expose an
* agent-permission tool outside this exact set.
*
- * Runtime protocol tools are outside that ceiling by construction (#2026).
- * `ArchiveRead` decodes a placeholder the runtime itself generated during
- * pruning; it grants no reach the parent did not already exercise, and
- * withholding it only strands content the model was explicitly told to
- * retrieve. The backend therefore binds it from the archive capability, not
- * from this set, which is why narrowing a child's allowlist can no longer
- * silently strip the decoder for placeholders that child will still receive.
+ * Session tool-result reads remain available through Read when archiving is
+ * enabled. The backend wraps an existing Read or supplies a resource-only
+ * Read; it never adds filesystem access to a restricted tool set.
*/
tools?: readonly MakaTool[];
/** Turn-scoped shell plan captured with a bound child tool ceiling. */
diff --git a/packages/runtime/src/tool-availability.ts b/packages/runtime/src/tool-availability.ts
index 3b751fab67..1dbe437037 100644
--- a/packages/runtime/src/tool-availability.ts
+++ b/packages/runtime/src/tool-availability.ts
@@ -39,7 +39,6 @@ export const TOOL_SEARCH_MAX_SCHEMA_CHARS = 64 * 1024;
const DIRECT_TOOL_NAMES: ReadonlySet<string> = new Set([
'Bash',
'Read',
- 'ArchiveRead',
'Write',
'Edit',
'Glob',
diff --git a/packages/runtime/src/tool-result-archive-capability.ts b/packages/runtime/src/tool-result-archive-capability.ts
index 106dbd8fa6..6a68ac6b2f 100644
--- a/packages/runtime/src/tool-result-archive-capability.ts
+++ b/packages/runtime/src/tool-result-archive-capability.ts
@@ -17,39 +17,15 @@
* under the License.
*/
-/**
- * The tool-result archive is one capability, not three optional fields (#2026).
- *
- * When the context budget prunes a large tool result, the placeholder that
- * replaces it is a runtime-generated protocol value naming `ArchiveRead` as the
- * way back to the content. Writer, replay reader, ref-addressed reader and that
- * decoder tool therefore share one authority: a host either archives and can
- * read back, or does neither. Splitting them across backend options and tool
- * options made "writer on, decoder absent" representable, and it shipped twice
- * (#2025 and the child-agent path).
- *
- * Hosts supply only storage. The decoder travels with the capability and is
- * bound by the backend, so which host remembered to register a tool is no
- * longer part of the question.
- */
-
-import { ARCHIVE_READ_TOOL_NAME, buildArchiveReadTool } from './archive-read-tool.js';
import type { ArchivedToolResultReason } from './tool-result-archive.js';
-import type { ToolResultArchiveReader } from './tool-result-archive.js';
-import type { ToolResultArchiveResourceReader } from './tool-result-archive-resource.js';
+import {
+ readToolResultArchiveResource,
+ type ToolResultArchiveResourceReader,
+} from './tool-result-archive-resource.js';
import type { MakaTool } from './tool-runtime.js';
import type { ModelProjectionTransition } from '@maka/core/model-projection-transition';
+import { READ_DESCRIPTION, readParameters, resolveReadInput } from './read-page.js';
-export { ARCHIVE_READ_TOOL_NAME };
-
-/**
- * What the writer is handed for one pruned body.
- *
- * One shape, not a union over the two prune paths: since both now commit the
- * same durable projection transition (#4283), both address the same
- * `function_response` RuntimeEvent and hand over the same serialized body, and
- * a union would only preserve the shape of the authorities they replaced.
- */
export interface ToolResultArchiveRecorderInput {
sessionId: string;
runtimeEventId: string;
@@ -81,64 +57,51 @@ export type ToolResultArchiveRecorder = (
input: ToolResultArchiveRecorderInput,
) => Promise<ToolResultArchiveLocation | void> | ToolResultArchiveLocation | void;
-/** Host-owned storage for one archive authority. */
export interface ToolResultArchiveServices {
- /** Persists legacy bytes or verifies ledger reconstruction before the transition commit. */
archiveToolResult: ToolResultArchiveRecorder;
- /** Replay hydration, addressed by the originating runtime event. */
- readToolResultArchive: ToolResultArchiveReader;
- /**
- * Ref-addressed read backing `ArchiveRead`. A `maka://archive/...` ref carries
- * only artifact id, hash and size — no runtime-event identity — so this read
- * must never synthesize one to satisfy the stricter replay path above.
- *
- * It MUST reject a read whose `sessionId` does not own the artifact. The
- * runtime passes down the invoking session's id but cannot verify ownership
- * itself, so this is the only place the session boundary is enforced, and the
- * argument that the decoder grants a child no reach it lacked rests on it.
- */
readArchivedToolResultResource: ToolResultArchiveResourceReader['readArchivedToolResultResource'];
}
-/**
- * One indivisible archive authority. Build it through
- * {@link createToolResultArchiveCapability}: the decoder is derived from the
- * same services the writer uses, so "archives, but the placeholder names a tool
- * nobody bound" has no spelling. A capability whose readers merely fail is of
- * course still constructible — but that failure is legible to the model and the
- * host, which is what the old missing-tool state never was.
- */
export interface ToolResultArchiveCapability {
readonly services: ToolResultArchiveServices;
- readonly archiveReadTool: MakaTool;
}
export function createToolResultArchiveCapability(
services: ToolResultArchiveServices,
): ToolResultArchiveCapability {
- return Object.freeze({
- services: Object.freeze({ ...services }),
- archiveReadTool: buildArchiveReadTool({
- readArchivedToolResultResource: services.readArchivedToolResultResource,
- }),
- });
+ return Object.freeze({ services: Object.freeze({ ...services }) });
}
-/**
- * Add the decoder to a session's tool set when — and only when — that session
- * archives.
- *
- * No in-tree host binds `ArchiveRead` any more, so the dedup guard exists for
- * tool sets this runtime does not own: an embedder's custom tool, or an agent
- * definition that names it. Such a tool wins, which also means it — not this
- * capability — is what the placeholder's ref will be handed to. Advertising two
- * tools under one name would be worse.
- */
export function bindToolResultArchiveDecoder(
tools: readonly MakaTool[],
capability: ToolResultArchiveCapability | undefined,
): MakaTool[] {
if (!capability) return [...tools];
- if (tools.some((tool) => tool.name === ARCHIVE_READ_TOOL_NAME)) return [...tools];
- return [...tools, capability.archiveReadTool];
+ const existing = tools.find((tool) => tool.name === 'Read');
+ const read: MakaTool = {
+ ...existing,
+ name: 'Read',
+ activityKind: 'read',
+ description: existing
+ ? existing.description + ' Also accepts Maka tool-result paths returned in this session.'
+ : 'Read a Maka tool-result address returned in this session. offset and limit are optional zero-based line pagination. Use the returned next object to continue. File access is not available.',
+ parameters: readParameters,
+ impl: async (input, ctx) => {
+ const { path } = resolveReadInput(input);
+ const prefix = 'maka://runtime/tool-results/';
+ if (!path.startsWith(prefix)) {
+ if (existing) return existing.impl(input, ctx);
+ throw new Error(
+ 'This Read tool only reads Maka tool-result addresses returned in this session. File access is not available.',
+ );
+ }
+ return readToolResultArchiveResource(
+ capability.services,
+ ctx.sessionId,
+ input,
+ ctx.abortSignal,
+ );
+ },
+ };
+ return [...tools.filter((tool) => tool.name !== 'Read'), read];
}
diff --git a/packages/runtime/src/tool-result-archive-resource.ts b/packages/runtime/src/tool-result-archive-resource.ts
index d3bc63df23..0d54d534a7 100644
--- a/packages/runtime/src/tool-result-archive-resource.ts
+++ b/packages/runtime/src/tool-result-archive-resource.ts
@@ -18,32 +18,12 @@
*/
import type { ToolResultArchiveReadResult } from './tool-result-archive.js';
+import { readToolResultPage, resolveReadInput, type ReadInput } from './read-page.js';
export const TOOL_RESULT_ARCHIVE_RESOURCE_PROTOCOL = 'maka:';
export const TOOL_RESULT_ARCHIVE_RESOURCE_HOST = 'archive';
-export const TOOL_RESULT_ARCHIVE_DEFAULT_LIMIT = 4_000;
-export const TOOL_RESULT_ARCHIVE_MAX_LIMIT = 6_000;
export const TOOL_RESULT_ARCHIVE_MAX_BYTES = 4 * 1024 * 1024;
-export const TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS = 7_500;
-
const ARCHIVE_ARTIFACT_ID_PATTERN = /^[A-Za-z0-9._-]{1,160}$/;
-const MAX_MANIFEST_ITEMS = 100;
-const MAX_METADATA_STRING_CHARS = 160;
-
-export type ToolResultArchiveResourceOperation = 'inspect' | 'read' | 'query' | 'search';
-
-export type ToolResultArchiveReadUnit = 'char' | 'line';
-
-/** Upper bound on matches returned by a single search page. */
-export const TOOL_RESULT_ARCHIVE_MAX_SEARCH_MATCHES = 50;
-/** Characters of surrounding context kept on each side of a search match. */
-const SEARCH_SNIPPET_RADIUS = 80;
-/** Longest search snippet retained, so many matches still fit one response. */
-const MAX_SEARCH_SNIPPET_CHARS = 240;
-/** Maximum pattern length accepted by the ArchiveRead tool schema. */
-const MAX_SEARCH_PATTERN_CHARS = 256;
-/** Preview length surfaced by inspect for text/object payloads. */
-const INSPECT_PREVIEW_CHARS = 600;
export interface LegacyArchiveResourceIdentity {
artifactId: string;
@@ -67,7 +47,13 @@ export interface LedgerArchiveResourceIdentity {
export type ToolResultArchiveResourceIdentity =
| LegacyArchiveResourceIdentity
| LedgerArchiveResourceIdentity;
-export type ToolResultArchiveResourceReadInput = ToolResultArchiveResourceIdentity & {
+export type ToolResultArchiveResourceReadInput = (
+ | ToolResultArchiveResourceIdentity
+ | {
+ storage: 'event';
+ runtimeEventId: string;
+ }
+) & {
sessionId: string;
maxBytes: number;
};
@@ -101,18 +87,6 @@ export interface ToolResultArchiveResourceReader {
): Promise<ToolResultArchiveReadResult> | ToolResultArchiveReadResult;
}
-export interface ToolResultArchiveResourceRequest {
- ref: string;
- operation?: ToolResultArchiveResourceOperation;
- offset?: number;
- limit?: number;
- itemId?: string;
- /** For `read`: whether offset/limit count characters (default) or lines. */
- unit?: ToolResultArchiveReadUnit;
- /** For `search`: the literal, case-insensitive substring to locate. */
- pattern?: string;
-}
-
export function buildToolResultArchiveResourceRef(
input: ToolResultArchiveResourceIdentity,
): string {
@@ -209,588 +183,37 @@ export function parseToolResultArchiveResourceRef(
return { artifactId, bodySha256, originalBytes };
}
+export const TOOL_RESULT_ARCHIVE_READ_INSTRUCTIONS =
+ 'Use Read with the next parameters to continue this result.';
+
export async function readToolResultArchiveResource(
reader: ToolResultArchiveResourceReader,
sessionId: string,
- request: ToolResultArchiveResourceRequest,
+ input: ReadInput,
abortSignal?: AbortSignal,
): Promise<unknown> {
- const identity = parseToolResultArchiveResourceRef(request.ref);
- if (!identity) {
- return archiveFailure(request.ref, 'invalid_ref');
- }
- if (identity.originalBytes > TOOL_RESULT_ARCHIVE_MAX_BYTES) {
- return archiveFailure(request.ref, 'too_large', {
- originalBytes: identity.originalBytes,
- maxBytes: TOOL_RESULT_ARCHIVE_MAX_BYTES,
- });
- }
- if (abortSignal?.aborted) throw new Error('ArchiveRead aborted');
-
- const read = await Promise.resolve(
- reader.readArchivedToolResultResource({
- ...identity,
- sessionId,
- maxBytes: identity.originalBytes,
- }),
- );
- if (!read.ok) return archiveFailure(request.ref, read.reason);
- if (abortSignal?.aborted) throw new Error('ArchiveRead aborted');
-
- const operation = request.operation ?? 'inspect';
- const limit = normalizeLimit(request.limit);
- const offset = normalizeOffset(request.offset);
- const parsed = deserializeArchive(read.serializedResult);
-
- if (operation === 'search') {
- const text = resolveArchiveText(parsed, read.serializedResult);
- return searchArchive(request.ref, text, request.pattern, offset);
- }
- if (operation === 'read') {
- // Page the decoded text for string payloads so offsets land on real
- // characters, not JSON escape bytes; structured payloads still page their
- // canonical JSON serialization.
- const text = resolveArchiveText(parsed, read.serializedResult);
- if (request.unit === 'line') {
- return pagedLines(request.ref, text, offset, limit);
- }
- return pagedContent({ ref: request.ref, operation, content: text, offset, limit });
- }
- if (operation === 'query') {
- return queryArchiveItem(request.ref, parsed, request.itemId, offset, limit);
- }
- return inspectArchive(request.ref, identity, parsed, read.serializedResult);
-}
-
-/**
- * The text an archive should page/search over. String payloads decode to their
- * raw content, and terminal/shell-run payloads project their stdout/stderr;
- * everything else pages its canonical JSON serialization.
- */
-function resolveArchiveText(parsed: unknown, serializedResult: string): string {
- if (typeof parsed === 'string') return parsed;
- if (!isTerminalArchivePayload(parsed)) return serializedResult;
-
- // Bash archives retain the canonical terminal/shell-run object. Search and
- // line reads should operate on the human-readable streams, not JSON-escaped
- // `\\n` sequences in that object.
- const output = isRecord(parsed.output) ? parsed.output : undefined;
- if (output?.mode === 'pty') {
- const ptyParts = [output.scrollback, output.screen, output.lastAlternateScreen].filter(
- (part): part is string => typeof part === 'string' && part.length > 0,
- );
- return ptyParts.join('\n');
- }
- const stdout = output && typeof output.stdout === 'string' ? output.stdout : undefined;
- const stderr = output && typeof output.stderr === 'string' ? output.stderr : undefined;
- if (stdout !== undefined || stderr !== undefined) {
- const parts = [stdout, stderr].filter(
- (s): s is string => typeof s === 'string' && s.length > 0,
- );
- return parts.join('\n');
- }
- return serializedResult;
-}
-
-function isTerminalArchivePayload(value: unknown): value is Record<string, unknown> {
- return (
- isRecord(value) &&
- (value.kind === 'terminal' || value.kind === 'shell_run') &&
- isRecord(value.output)
- );
-}
-
-export const TOOL_RESULT_ARCHIVE_READ_INSTRUCTIONS =
- 'This result is archived but still readable. Call ArchiveRead with the provided ref and operation "inspect"; use operation "query" with itemId for one structured item, or operation "read" with offset/limit for a bounded page. Do not use Glob to find the archive.';
-
-function inspectArchive(
- ref: string,
- identity: ToolResultArchiveResourceIdentity,
- value: unknown,
- serializedResult: string,
-): unknown {
- const base = {
- ok: true,
- kind: 'tool_result_archive',
- operation: 'inspect',
- ref,
- ...(identity.storage === 'ledger'
- ? { storage: 'ledger', runtimeEventId: identity.runtimeEventId }
- : { artifactId: identity.artifactId }),
- originalBytes: identity.originalBytes,
- };
- const readHint =
- 'Call ArchiveRead with operation "read" (unit "char" or "line"), offset, and limit for bounded pages, or operation "search" with a pattern to locate text.';
- if (isTerminalArchivePayload(value)) {
- const text = resolveArchiveText(value, serializedResult);
- return {
- ...base,
- valueType: 'terminal',
- totalChars: text.length,
- totalLines: countLines(text),
- preview: boundedString(text, INSPECT_PREVIEW_CHARS),
- readHint,
- };
- }
- if (typeof value === 'string') {
- // A plain-text/terminal payload: report the char/line coordinate space plus
- // a short preview so the model can page or search without a blind first read.
- return {
- ...base,
- valueType: 'text',
- totalChars: value.length,
- totalLines: countLines(value),
- preview: boundedString(value, INSPECT_PREVIEW_CHARS),
- readHint,
- };
- }
- if (isRecord(value)) {
- const items = Array.isArray(value.items) ? value.items : undefined;
- let objectKeys = Object.keys(value)
- .slice(0, 25)
- .map((key) => boundedString(key));
- const buildObjectBase = (): Record<string, unknown> => ({
- ...base,
- valueType: 'object',
- totalChars: serializedResult.length,
- keys: objectKeys,
- ...(typeof value.kind === 'string' ? { archivedKind: boundedString(value.kind) } : {}),
- ...(typeof value.status === 'string' ? { status: boundedString(value.status) } : {}),
- });
- if (items) {
- const queryHint =
- 'Call ArchiveRead with operation "query" and one of the listed itemId values.';
- const projectedItems = items.slice(0, MAX_MANIFEST_ITEMS).map(inspectItem);
- let manifestItems = projectedItems;
- const buildManifest = (): Record<string, unknown> => ({
- ...buildObjectBase(),
- itemCount: items.length,
- items: manifestItems,
- queryHint,
- readHint,
- ...(manifestItems.length < items.length
- ? { itemsTruncated: true, listedItemCount: manifestItems.length }
- : {}),
- });
- // Keep one actionable itemId when it can fit, then trade keys for more
- // manifest entries and re-add entries against the complete envelope.
- while (JSON.stringify(buildManifest()).length > TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS) {
- if (manifestItems.length > 1) {
- manifestItems = manifestItems.slice(0, -1);
- continue;
- }
- if (objectKeys.length > 0) {
- objectKeys = objectKeys.slice(0, -1);
- continue;
- }
- if (manifestItems.length > 0) {
- manifestItems = [];
- continue;
- }
- break;
- }
- for (const projectedItem of projectedItems.slice(manifestItems.length)) {
- const next = [...manifestItems, projectedItem];
- manifestItems = next;
- if (JSON.stringify(buildManifest()).length > TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS) {
- manifestItems = next.slice(0, -1);
- break;
- }
- }
- return buildManifest();
- }
- const buildObject = (): Record<string, unknown> => ({
- ...buildObjectBase(),
- preview: boundedString(serializedResult, INSPECT_PREVIEW_CHARS),
- readHint,
- });
- while (
- objectKeys.length > 0 &&
- JSON.stringify(buildObject()).length > TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS
- ) {
- objectKeys = objectKeys.slice(0, -1);
- }
- return buildObject();
- }
- if (Array.isArray(value)) {
- return {
- ...base,
- valueType: 'array',
- itemCount: value.length,
- totalChars: serializedResult.length,
- preview: boundedString(serializedResult, INSPECT_PREVIEW_CHARS),
- readHint,
- };
- }
- return {
- ...base,
- valueType: value === null ? 'null' : typeof value,
- totalChars: serializedResult.length,
- preview: boundedString(serializedResult, INSPECT_PREVIEW_CHARS),
- readHint,
- };
-}
-
-function queryArchiveItem(
- ref: string,
- value: unknown,
- itemId: string | undefined,
- offset: number,
- limit: number,
-): unknown {
- if (!itemId) return archiveFailure(ref, 'item_id_required');
- const items = isRecord(value) && Array.isArray(value.items) ? value.items : undefined;
- if (!items) return archiveFailure(ref, 'not_queryable');
- const item = items.find(
- (candidate) =>
- isRecord(candidate) && String(candidate.itemId ?? candidate.item_id ?? '') === itemId,
- );
- if (!isRecord(item)) {
- return archiveFailure(ref, 'item_not_found', {
- itemId,
- availableItemIds: items
- .map((candidate) =>
- isRecord(candidate)
- ? boundedString(String(candidate.itemId ?? candidate.item_id ?? ''))
- : '',
- )
- .filter(Boolean)
- .slice(0, 25),
- });
- }
- const content =
- typeof item.summary === 'string'
- ? item.summary
- : typeof item.result === 'string'
- ? item.result
- : JSON.stringify(item);
- return pagedContent({
- ref,
- operation: 'query',
- content,
- offset,
- limit,
- extra: {
- itemId,
- item: inspectItem(item),
- },
- });
-}
-
-function pagedContent(input: {
- ref: string;
- operation: 'read' | 'query';
- content: string;
- offset: number;
- limit: number;
- extra?: Record<string, unknown>;
-}): Record<string, unknown> {
- const offset = Math.min(input.offset, input.content.length);
- const requestedEnd = Math.min(input.content.length, offset + input.limit);
- const buildPage = (end: number): Record<string, unknown> => ({
- ok: true,
- kind: 'tool_result_archive',
- operation: input.operation,
- ref: input.ref,
- offset,
- limit: end - offset,
- totalChars: input.content.length,
- nextOffset: end < input.content.length ? end : null,
- hasMore: end < input.content.length,
- content: input.content.slice(offset, end),
- ...(input.extra ?? {}),
+ const { path } = resolveReadInput(input);
+ const prefix = 'maka://runtime/tool-results/';
+ if (!path.startsWith(prefix))
+ throw new Error('Invalid Maka tool-result address. Use the path returned by a tool.');
+ const runtimeEventId = decodeURIComponent(path.slice(prefix.length));
+ if (!runtimeEventId || runtimeEventId.length > 512 || path.includes('?') || path.includes('#'))
+ throw new Error('Invalid Maka address. Copy the complete path returned by the tool.');
+ abortSignal?.throwIfAborted();
+ const result = await reader.readArchivedToolResultResource({
+ sessionId,
+ storage: 'event',
+ runtimeEventId,
+ maxBytes: TOOL_RESULT_ARCHIVE_MAX_BYTES,
});
- let low = offset;
- let high = requestedEnd;
- while (low < high) {
- const candidateEnd = Math.ceil((low + high) / 2);
- if (JSON.stringify(buildPage(candidateEnd)).length <= TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS) {
- low = candidateEnd;
- } else {
- high = candidateEnd - 1;
- }
- }
- return buildPage(low);
-}
-
-/**
- * Locate a literal (case-insensitive) substring inside the archived text and
- * return matched offsets with bounded context snippets. The pattern is escaped
- * before using a Unicode-aware literal regex, so it cannot inject regex syntax
- * or introduce pathological backtracking. Matches accumulate only while the
- * envelope stays within
- * TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS, and `nextOffset` resumes the scan.
- */
-function searchArchive(
- ref: string,
- text: string,
- pattern: string | undefined,
- offset: number,
-): unknown {
- if (!pattern) return archiveFailure(ref, 'pattern_required');
- // Escape the user pattern before using a Unicode-aware literal regex. This
- // preserves original source indices while retaining contextual case folding
- // (for example, Greek final sigma), and remains safe from regex injection or
- // pathological backtracking because the pattern is literal.
- const matcher = new RegExp(escapeRegExp(pattern), 'giu');
- const base = {
- ok: true as const,
- kind: 'tool_result_archive' as const,
- operation: 'search' as const,
- ref,
- pattern: boundedString(pattern, MAX_SEARCH_PATTERN_CHARS),
- totalChars: text.length,
- };
- const buildResponse = (
- matches: unknown[],
- nextOffset: number | null,
- ): Record<string, unknown> => ({
- ...base,
- matchCount: matches.length,
- matches,
- nextOffset,
- hasMore: nextOffset !== null,
- });
-
- const matches: unknown[] = [];
- // Line numbers are assigned by a single forward scan; match offsets only
- // increase, so total work stays linear in the text length.
- let scanChar = 0;
- let scanLine = 1;
- const lineAt = (index: number): number => {
- while (scanChar < index && scanChar < text.length) {
- if (text.charCodeAt(scanChar) === 10) scanLine++;
- scanChar++;
- }
- return scanLine;
- };
-
- let from = normalizeSearchOffset(text, offset);
- while (matches.length < TOOL_RESULT_ARCHIVE_MAX_SEARCH_MATCHES) {
- matcher.lastIndex = from;
- const match = matcher.exec(text);
- if (!match) return buildResponse(matches, null);
- const index = match.index;
- const matchEnd = index + match[0].length;
- // Keep the complete match representable when the accepted pattern is
- // longer than the normal snippet budget; trim surrounding context first.
- const snippetLimit = Math.min(
- MAX_SEARCH_PATTERN_CHARS + SEARCH_SNIPPET_RADIUS * 2,
- Math.max(MAX_SEARCH_SNIPPET_CHARS, matchEnd - index),
- );
- const contextBudget = Math.max(0, snippetLimit - (matchEnd - index));
- const before = Math.min(SEARCH_SNIPPET_RADIUS, Math.floor(contextBudget / 2));
- const after = Math.min(SEARCH_SNIPPET_RADIUS, contextBudget - before);
- const snippetStart = Math.max(0, index - before);
- const snippetEnd = Math.min(text.length, matchEnd + after);
- const candidate = {
- offset: index,
- line: lineAt(index),
- snippetStart,
- snippet: text.slice(snippetStart, snippetEnd),
- };
- if (
- JSON.stringify(buildResponse([...matches, candidate], index)).length >
- TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS
- ) {
- // This match would overflow the budget; resume here next call.
- return buildResponse(matches, index);
- }
- matches.push(candidate);
- from = index + Math.max(1, match[0].length);
- }
- // Hit the per-page match cap; report where an unread match still waits.
- matcher.lastIndex = from;
- const more = matcher.exec(text);
- return buildResponse(matches, more === null ? null : more.index);
-}
-
-function escapeRegExp(value: string): string {
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
-}
-
-function normalizeSearchOffset(text: string, offset: number): number {
- const clamped = Math.min(Math.max(0, offset), text.length);
- if (
- clamped > 0 &&
- clamped < text.length &&
- isLowSurrogate(text.charCodeAt(clamped)) &&
- isHighSurrogate(text.charCodeAt(clamped - 1))
- ) {
- return clamped + 1;
- }
- return clamped;
-}
-
-function isHighSurrogate(value: number): boolean {
- return value >= 0xd800 && value <= 0xdbff;
-}
-
-function isLowSurrogate(value: number): boolean {
- return value >= 0xdc00 && value <= 0xdfff;
-}
-
-/** Count 1-based lines in text (an empty string has zero lines). */
-function countLines(text: string): number {
- if (text.length === 0) return 0;
- let count = 1;
- for (let i = 0; i < text.length; i++) {
- if (text.charCodeAt(i) === 10) count++;
- }
- return count;
-}
-
-/**
- * Collect up to `maxLines` lines starting at the zero-based `lineOffset` without
- * materializing the whole text as an array — the scan stops after the window,
- * so cost is bounded by the requested window, not the archive size.
- */
-function collectLineWindow(
- text: string,
- lineOffset: number,
- maxLines: number,
-): { lines: string[]; startChar: number } {
- if (text.length === 0) return { lines: [], startChar: 0 };
- let cursor = 0;
- let line = 0;
- while (line < lineOffset && cursor <= text.length) {
- const nl = text.indexOf('\n', cursor);
- if (nl === -1) return { lines: [], startChar: text.length };
- cursor = nl + 1;
- line++;
- }
- const startChar = cursor;
- const lines: string[] = [];
- while (lines.length < maxLines && cursor <= text.length) {
- const nl = text.indexOf('\n', cursor);
- if (nl === -1) {
- lines.push(text.slice(cursor));
- break;
- }
- lines.push(text.slice(cursor, nl));
- cursor = nl + 1;
- if (cursor > text.length) break;
- }
- return { lines, startChar };
-}
-
-/**
- * Read a bounded window of whole lines. offset/limit count lines; the returned
- * line count is trimmed by binary search so the response never exceeds
- * TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS, and `nextLineOffset` resumes paging.
- */
-function pagedLines(ref: string, text: string, lineOffset: number, lineLimit: number): unknown {
- const totalLines = countLines(text);
- const { lines } = collectLineWindow(text, lineOffset, lineLimit);
- const buildPage = (count: number): Record<string, unknown> => {
- const endLine = lineOffset + count;
- const hasMore = endLine < totalLines;
+ abortSignal?.throwIfAborted();
+ if (!result.ok)
return {
- ok: true,
- kind: 'tool_result_archive',
- operation: 'read',
- ref,
- unit: 'line',
- lineOffset,
- lineLimit: count,
- totalLines,
- nextLineOffset: hasMore ? endLine : null,
- hasMore,
- content: lines.slice(0, count).join('\n'),
+ error: result.reason,
+ message:
+ 'This Maka content could not be read. Changing offset or limit will not restore it. Use the original source if it is still available.',
};
- };
- let low = 0;
- let high = lines.length;
- while (low < high) {
- const candidate = Math.ceil((low + high) / 2);
- if (JSON.stringify(buildPage(candidate)).length <= TOOL_RESULT_ARCHIVE_MAX_RESPONSE_CHARS) {
- low = candidate;
- } else {
- high = candidate - 1;
- }
- }
- if (low === 0 && lines.length > 0) {
- return archiveFailure(ref, 'line_too_large', {
- unit: 'line',
- lineOffset,
- totalLines,
- lineChars: lines[0]!.length,
- readHint: 'Retry with operation "read" and unit "char" to page this oversized line.',
- });
- }
- return buildPage(low);
-}
-
-function inspectItem(value: unknown): unknown {
- if (!isRecord(value)) return { valueType: value === null ? 'null' : typeof value };
- return {
- ...(typeof value.itemId === 'string' ? { itemId: boundedString(value.itemId) } : {}),
- ...(typeof value.item_id === 'string' ? { itemId: boundedString(value.item_id) } : {}),
- ...(typeof value.index === 'number' ? { index: value.index } : {}),
- ...(typeof value.started === 'boolean' ? { started: value.started } : {}),
- ...(typeof value.status === 'string' ? { status: boundedString(value.status) } : {}),
- ...(typeof value.profile === 'string' ? { profile: boundedString(value.profile) } : {}),
- ...(typeof value.agentId === 'string' ? { agentId: boundedString(value.agentId) } : {}),
- ...(typeof value.agentName === 'string' ? { agentName: boundedString(value.agentName) } : {}),
- ...(typeof value.childSessionId === 'string'
- ? { childSessionId: boundedString(value.childSessionId) }
- : {}),
- ...(typeof value.turnId === 'string' ? { turnId: boundedString(value.turnId) } : {}),
- ...(typeof value.runId === 'string' ? { runId: boundedString(value.runId) } : {}),
- ...(typeof value.resumedFromRunId === 'string'
- ? { resumedFromRunId: boundedString(value.resumedFromRunId) }
- : {}),
- ...(Array.isArray(value.artifactIds)
- ? {
- artifactIds: value.artifactIds
- .filter((artifactId): artifactId is string => typeof artifactId === 'string')
- .slice(0, 8)
- .map(boundedString),
- artifactCount: value.artifactIds.length,
- }
- : {}),
- ...(typeof value.startedAt === 'number' ? { startedAt: value.startedAt } : {}),
- ...(typeof value.completedAt === 'number' ? { completedAt: value.completedAt } : {}),
- ...(typeof value.durationMs === 'number' ? { durationMs: value.durationMs } : {}),
- ...(typeof value.failureClass === 'string'
- ? { failureClass: boundedString(value.failureClass) }
- : {}),
- ...(typeof value.summary === 'string' ? { summaryChars: value.summary.length } : {}),
- ...(typeof value.result === 'string' ? { resultChars: value.result.length } : {}),
- };
-}
-
-function boundedString(value: string, max: number = MAX_METADATA_STRING_CHARS): string {
- return value.length <= max ? value : `${value.slice(0, Math.max(0, max - 1))}…`;
-}
-
-function deserializeArchive(serialized: string): unknown {
- if (serialized === 'undefined') return undefined;
- try {
- return JSON.parse(serialized) as unknown;
- } catch {
- return serialized;
- }
-}
-
-function normalizeLimit(value: number | undefined): number {
- if (!Number.isFinite(value)) return TOOL_RESULT_ARCHIVE_DEFAULT_LIMIT;
- return Math.max(1, Math.min(TOOL_RESULT_ARCHIVE_MAX_LIMIT, Math.floor(value as number)));
-}
-
-function normalizeOffset(value: number | undefined): number {
- if (!Number.isFinite(value)) return 0;
- return Math.max(0, Math.floor(value as number));
-}
-
-function archiveFailure(ref: string, reason: string, detail?: Record<string, unknown>): unknown {
- return {
- ok: false,
- kind: 'tool_result_archive',
- ref,
- reason,
- ...(detail ?? {}),
- };
+ return readToolResultPage(result.serializedResult, input);
}
function isRecord(value: unknown): value is Record<string, unknown> {
diff --git a/packages/runtime/src/tool-result-archive-transition.ts b/packages/runtime/src/tool-result-archive-transition.ts
index dafc89b46e..af75c834d6 100644
--- a/packages/runtime/src/tool-result-archive-transition.ts
+++ b/packages/runtime/src/tool-result-archive-transition.ts
@@ -17,29 +17,6 @@
* under the License.
*/
-/**
- * The one writer that turns a Tool Result prune decision into durable truth
- * (#4283).
- *
- * Both prune paths — the current Turn's active prune before the next provider
- * step, and the prior Turn's stale prune before compaction — come through here.
- * They used to keep their replacement in a Turn-local map and a policy-carried
- * ref table respectively, so each owned a private recovery contract and neither
- * survived a restart. Now each records one `ModelProjectionTransition`, and the
- * Session reducer is the only thing that decides what the model sees.
- *
- * Write order is the whole safety argument:
- *
- * 1. Archive the replaced body. A failure here leaves the projection untouched.
- * 2. Append the transition. A failure here leaves an artifact nothing points
- * at — unreachable by the reducer, so safe to reclaim once something does —
- * and again leaves the projection untouched.
- * 3. Only then may a caller show the replacement.
- *
- * There is no state in which the model has lost content the ledger cannot
- * explain, and none in which a completed tool effect is repeated.
- */
-
import { MATERIALIZED_IMAGE_TOKENS } from '@maka/core/attachments';
import type { DurableToolResultProjection } from '@maka/core/durable-tool-result-projection';
import { DURABLE_TOOL_RESULT_PROJECTION_VERSION } from '@maka/core/durable-tool-result-projection';
@@ -50,14 +27,7 @@ import {
} from '@maka/core/model-projection-transition';
import type { RuntimeEvent } from '@maka/core/runtime-event';
-import type { ActiveToolResultSupersession } from './active-tool-result-working-set.js';
-import {
- estimateTokens,
- finitePositive,
- sha256,
- turnKey,
- utf8ByteLength,
-} from './context-budget-helpers.js';
+import { estimateTokens, sha256, utf8ByteLength } from './context-budget-helpers.js';
import { projectionArtifactMedia } from './durable-tool-result-projection.js';
import { baseToolResultProjection, nextInChain } from './model-projection-transition-ledger.js';
import {
@@ -67,8 +37,8 @@ import {
isArchivedToolResultPlaceholder,
type ArchivedToolResultPlaceholder,
type ArchivedToolResultReason,
- type StaleToolResultArchiveCandidate,
- type StaleToolResultPrunePolicy,
+ type ToolResultArchiveCandidate,
+ type ToolResultPrunePolicy,
} from './tool-result-archive.js';
import type {
ToolResultArchiveRecorder,
@@ -76,7 +46,7 @@ import type {
} from './tool-result-archive-capability.js';
import { serializeToolResultProjectionV1 } from './tool-result-archive-encoding.js';
-const DEFAULT_MAX_TOOL_RESULT_ESTIMATED_TOKENS = 2048;
+import { READ_PAGE_MAX_CHARS, readToolResultPage } from './read-page.js';
export type ModelProjectionTransitionRecorder = (
transition: ModelProjectionTransition,
@@ -133,7 +103,6 @@ export interface ToolResultArchiveTransitionRequest {
originalEstimatedTokens: number;
reason: ArchivedToolResultReason;
previousTransitionId?: string;
- supersession?: ActiveToolResultSupersession;
/** Raw execution fact kept only so the archive writer can name the artifact. */
result?: unknown;
}
@@ -194,7 +163,6 @@ export async function archiveToolResultAsTransition(
originalEstimatedTokens: request.originalEstimatedTokens,
originalBytes: request.originalBytes,
reason: request.reason,
- ...(request.supersession ? { supersession: request.supersession } : {}),
};
let placeholder: ArchivedToolResultPlaceholder;
try {
@@ -208,6 +176,11 @@ export async function archiveToolResultAsTransition(
: {}),
})
: buildArchivedToolResultPlaceholder({ ...common, artifactId: archived.artifactId });
+ placeholder.page = readToolResultPage(
+ request.serializedResult,
+ { path: placeholder.resourceRef! },
+ READ_PAGE_MAX_CHARS - JSON.stringify(placeholder).length - 32,
+ );
} catch {
return undefined;
}
@@ -277,51 +250,36 @@ async function winningTransition(
});
}
-/**
- * Prior-Turn results large enough to archive before compaction.
- *
- * Collection reads events the transition reducer has already folded, so a
- * result an earlier transition replaced is measured at its replacement size and
- * simply falls below the threshold — there is no second "already pruned?"
- * predicate to keep in step with the fold.
- */
-export function collectStaleToolResultArchiveCandidates(
+export function collectToolResultArchiveCandidates(
events: readonly RuntimeEvent[],
- prunePolicy: StaleToolResultPrunePolicy | undefined,
+ prunePolicy: ToolResultPrunePolicy | undefined,
charsPerToken: number,
-): StaleToolResultArchiveCandidate[] {
+): ToolResultArchiveCandidate[] {
if (prunePolicy?.enabled !== true) return [];
- const maxResultEstimatedTokens =
- finitePositive(prunePolicy.maxResultEstimatedTokens) ??
- DEFAULT_MAX_TOOL_RESULT_ESTIMATED_TOKENS;
- const minRecentTurnsFull = Math.max(0, Math.floor(prunePolicy.minRecentTurnsFull ?? 1));
- const protectedTurnIds = recentTurnIds(events, minRecentTurnsFull);
- const candidates: StaleToolResultArchiveCandidate[] = [];
+ const candidates: ToolResultArchiveCandidate[] = [];
for (const event of events) {
const content = event.content;
if (
event.partial ||
event.modelVisibility === 'hidden' ||
content?.kind !== 'function_response' ||
- protectedTurnIds.has(turnKey(event))
+ content.providerExecuted
) {
continue;
}
const sourceProjection = baseToolResultProjection(event);
- if (!sourceProjection) continue;
+ if (
+ !sourceProjection ||
+ (sourceProjection.kind === 'json' && isArchivedToolResultPlaceholder(sourceProjection.value))
+ )
+ continue;
const serializedResult = serializedToolResultProjection(sourceProjection);
const originalBytes = utf8ByteLength(serializedResult);
const media = projectionArtifactMedia(sourceProjection);
- // An artifact serializes to a short reference and materializes to real
- // image bytes, so the string alone would price a screenshot at nothing.
const originalEstimatedTokens =
estimateTokens(serializedResult.length, charsPerToken) +
media.length * MATERIALIZED_IMAGE_TOKENS;
- // A result that carries media is always a candidate: archiving it drops
- // whole images from the request, which is worth doing whatever the
- // reference text around them happens to weigh. The size gate is there to
- // spare small text results, so it only decides those.
- if (media.length === 0 && originalEstimatedTokens <= maxResultEstimatedTokens) continue;
+ if (media.length > 0 || serializedResult.length <= READ_PAGE_MAX_CHARS) continue;
candidates.push({
runtimeEventId: event.id,
turnId: event.turnId,
@@ -333,45 +291,8 @@ export function collectStaleToolResultArchiveCandidates(
originalEstimatedTokens,
originalBytes,
rewriteVersion: ARCHIVED_TOOL_RESULT_REWRITE_VERSION,
- reason: 'stale_tool_result_pruned_before_compact',
+ reason: 'tool_result_pruned',
});
}
return candidates;
}
-
-/**
- * Archive artifacts the effective history still needs.
- *
- * Derived from the folded events, never from a parallel bookkeeping table: an
- * artifact is reachable exactly when a placeholder the model can still see names
- * it. An archive whose transition was refused is therefore unreachable by
- * construction.
- *
- * This is the reachability authority a reclaiming pass must ask; no such pass
- * exists yet, so nothing here is reclaimed today (#4283). Adding one is what
- * makes an unreferenced archive temporary rather than retained.
- */
-export function collectReachableArchiveArtifactIds(events: readonly RuntimeEvent[]): Set<string> {
- const reachable = new Set<string>();
- for (const event of events) {
- const content = event.content;
- if (content?.kind !== 'function_response') continue;
- if (isArchivedToolResultPlaceholder(content.result) && content.result.rewriteVersion === 1) {
- reachable.add(content.result.artifactId);
- }
- }
- return reachable;
-}
-
-function recentTurnIds(events: readonly RuntimeEvent[], count: number): Set<string> {
- if (count <= 0) return new Set();
- const order: string[] = [];
- const seen = new Set<string>();
- for (const event of events) {
- const key = turnKey(event);
- if (seen.has(key)) continue;
- seen.add(key);
- order.push(key);
- }
- return new Set(order.slice(Math.max(0, order.length - count)));
-}
diff --git a/packages/runtime/src/tool-result-archive.ts b/packages/runtime/src/tool-result-archive.ts
index faba2ebec8..7ca280cb50 100644
--- a/packages/runtime/src/tool-result-archive.ts
+++ b/packages/runtime/src/tool-result-archive.ts
@@ -25,26 +25,14 @@ import {
isLedgerArchiveIdentity,
type LedgerArchiveResourceIdentity,
} from './tool-result-archive-resource.js';
-import type { ActiveToolResultSupersession } from './active-tool-result-working-set.js';
+import type { ReadPage } from './read-page.js';
-export interface StaleToolResultPrunePolicy {
+export interface ToolResultPrunePolicy {
enabled: boolean;
- /** Tool result payloads above this estimate are replaced with archive placeholders. Defaults to 2048. */
- maxResultEstimatedTokens?: number;
- /** Keep this many newest turns' tool results full. Defaults to 1. */
- minRecentTurnsFull?: number;
}
-/**
- * Why a model-visible Tool Result was replaced by its archive placeholder.
- *
- * One placeholder kind covers both prune paths. They differ only in when the
- * decision is taken — before the next step of the current Turn, or before a
- * prior Turn is compacted — and both now record the same durable transition, so
- * a second placeholder protocol would only be a second way to spell the same
- * fact (#4283).
- */
export type ArchivedToolResultReason =
+ | 'tool_result_pruned'
| 'stale_tool_result_pruned_before_compact'
| 'active_current_turn_tool_result_pruned_before_next_step';
@@ -67,8 +55,7 @@ export interface LegacyArchivedToolResultPlaceholder {
originalEstimatedTokens: number;
originalBytes: number;
reason: ArchivedToolResultReason;
- /** Why a newer completed step made this provider-visible result redundant. */
- supersession?: ActiveToolResultSupersession;
+ page?: ReadPage;
}
export type LedgerArchivedToolResultPlaceholder = Omit<
@@ -97,15 +84,15 @@ export function buildLedgerArchivedToolResultPlaceholder(
originalBytes: input.originalBytes,
originalEstimatedTokens: input.originalEstimatedTokens,
reason: input.reason,
- ...(input.supersession ? { supersession: input.supersession } : {}),
kind: ARCHIVED_TOOL_RESULT_PLACEHOLDER_KIND,
rewriteVersion: 2,
- resourceRef: buildToolResultArchiveResourceRef(input),
- readInstructions: TOOL_RESULT_ARCHIVE_READ_INSTRUCTIONS,
+ resourceRef: `maka://runtime/tool-results/${encodeURIComponent(input.runtimeEventId)}`,
+ readInstructions: 'Use Read with the next parameters to continue this result.',
+ ...(input.page ? { page: input.page } : {}),
};
}
-export interface StaleToolResultArchiveCandidate {
+export interface ToolResultArchiveCandidate {
runtimeEventId: string;
turnId: string;
toolCallId: string;
@@ -117,7 +104,7 @@ export interface StaleToolResultArchiveCandidate {
originalEstimatedTokens: number;
originalBytes: number;
rewriteVersion: typeof ARCHIVED_TOOL_RESULT_REWRITE_VERSION;
- reason: 'stale_tool_result_pruned_before_compact';
+ reason: 'tool_result_pruned';
}
export type ToolResultArchiveReadFailureReason =
@@ -211,31 +198,13 @@ export function isArchivedToolResultPlaceholder(
typeof candidate.originalBytes === 'number' &&
Number.isFinite(candidate.originalBytes) &&
candidate.originalBytes > 0 &&
- (candidate.reason === 'stale_tool_result_pruned_before_compact' ||
- candidate.reason === 'active_current_turn_tool_result_pruned_before_next_step') &&
- isValidSupersession(candidate.supersession)
- );
-}
-
-function isValidSupersession(value: unknown): boolean {
- if (value === undefined) return true;
- if (!value || typeof value !== 'object') return false;
- const candidate = value as Partial<ActiveToolResultSupersession>;
- return (
- (candidate.reason === 'exact_duplicate' ||
- candidate.reason === 'newer_read_covers_range' ||
- candidate.reason === 'newer_snapshot' ||
- candidate.reason === 'failure_resolved') &&
- typeof candidate.supersededByToolCallId === 'string' &&
- candidate.supersededByToolCallId.length > 0 &&
- (candidate.reason === 'failure_resolved'
- ? typeof candidate.failureBodySha256 === 'string' &&
- /^[a-f0-9]{64}$/.test(candidate.failureBodySha256)
- : candidate.failureBodySha256 === undefined)
+ (candidate.reason === 'tool_result_pruned' ||
+ candidate.reason === 'stale_tool_result_pruned_before_compact' ||
+ candidate.reason === 'active_current_turn_tool_result_pruned_before_next_step')
);
}
-/** Add the canonical ArchiveRead address to persisted v1 placeholders. */
+/** Normalize persisted archive metadata without expanding the body. */
export function withToolResultArchiveResourceRef(value: unknown): unknown {
if (!isArchivedToolResultPlaceholder(value)) return value;
if (value.rewriteVersion === 2) return buildLedgerArchivedToolResultPlaceholder(value);
@@ -259,7 +228,6 @@ export function buildArchivedToolResultPlaceholder(input: {
originalEstimatedTokens: number;
originalBytes: number;
reason: ArchivedToolResultReason;
- supersession?: ActiveToolResultSupersession;
}): LegacyArchivedToolResultPlaceholder {
return {
kind: ARCHIVED_TOOL_RESULT_PLACEHOLDER_KIND,
@@ -278,6 +246,5 @@ export function buildArchivedToolResultPlaceholder(input: {
originalEstimatedTokens: input.originalEstimatedTokens,
originalBytes: input.originalBytes,
reason: input.reason,
- ...(input.supersession ? { supersession: input.supersession } : {}),
};
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org