@deepstrike/wasm 0.2.49 → 0.2.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/runtime/canonical-kernel-step.d.ts +146 -0
- package/dist/runtime/canonical-kernel-step.js +1484 -0
- package/dist/runtime/execution-plane.d.ts +0 -3
- package/dist/runtime/execution-plane.js +0 -25
- package/dist/runtime/facade.js +3 -0
- package/dist/runtime/index.d.ts +7 -7
- package/dist/runtime/index.js +4 -3
- package/dist/runtime/kernel-event-log.d.ts +0 -1
- package/dist/runtime/kernel-event-log.js +7 -13
- package/dist/runtime/kernel-journal.d.ts +332 -0
- package/dist/runtime/kernel-journal.js +681 -0
- package/dist/runtime/kernel-step.d.ts +34 -89
- package/dist/runtime/kernel-step.js +67 -167
- package/dist/runtime/os-snapshot.d.ts +2 -2
- package/dist/runtime/os-snapshot.js +2 -6
- package/dist/runtime/payload-store.d.ts +30 -0
- package/dist/runtime/payload-store.js +66 -0
- package/dist/runtime/runner.d.ts +72 -79
- package/dist/runtime/runner.js +424 -637
- package/dist/runtime/session-log.d.ts +24 -27
- package/dist/runtime/session-log.js +11 -52
- package/dist/runtime/session-repair.d.ts +2 -36
- package/dist/runtime/session-repair.js +2 -47
- package/dist/runtime/sub-agent-orchestrator.d.ts +2 -2
- package/dist/runtime/sub-agent-orchestrator.js +36 -40
- package/dist/runtime/types/agent.d.ts +21 -16
- package/dist/runtime/types/agent.js +21 -41
- package/package.json +4 -3
- package/dist/runtime/kernel-rebuild.d.ts +0 -8
- package/dist/runtime/kernel-rebuild.js +0 -65
- package/dist/runtime/kernel-transaction-log.d.ts +0 -61
- package/dist/runtime/kernel-transaction-log.js +0 -140
- package/dist/runtime/large-result-spool.d.ts +0 -57
- package/dist/runtime/large-result-spool.js +0 -169
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { KernelLogIntegrityError, kernelRecordDigest, verifyKernelTransactionStream, } from "./kernel-transaction-log.js";
|
|
2
|
-
/** Deterministically fold an authoritative stream into a fresh WASM runtime. */
|
|
3
|
-
export async function rebuildKernelRuntime(runtime, genesis, transactions) {
|
|
4
|
-
const cursor = await verifyKernelTransactionStream(genesis, transactions);
|
|
5
|
-
await assertFreshRuntimeMatchesGenesis(runtime, genesis);
|
|
6
|
-
for (const transaction of transactions) {
|
|
7
|
-
const prepared = JSON.parse(runtime.prepareStep(JSON.stringify(transaction.input)));
|
|
8
|
-
const token = prepared.prepare_token;
|
|
9
|
-
try {
|
|
10
|
-
if (prepared.status !== "prepared" || !token) {
|
|
11
|
-
throw new KernelLogIntegrityError(`kernel rebuild step ${transaction.step_seq} was not accepted as a new transition`);
|
|
12
|
-
}
|
|
13
|
-
if (prepared.base_generation !== transaction.base_generation) {
|
|
14
|
-
throw new KernelLogIntegrityError(`kernel rebuild generation diverged at step ${transaction.step_seq}`);
|
|
15
|
-
}
|
|
16
|
-
if (prepared.step.step_seq !== transaction.step_seq) {
|
|
17
|
-
throw new KernelLogIntegrityError(`kernel rebuild step_seq diverged at step ${transaction.step_seq}`);
|
|
18
|
-
}
|
|
19
|
-
if (await kernelRecordDigest(prepared.input) !== transaction.input_digest) {
|
|
20
|
-
throw new KernelLogIntegrityError(`kernel rebuild normalized input diverged at step ${transaction.step_seq}`);
|
|
21
|
-
}
|
|
22
|
-
if (await kernelRecordDigest(prepared.step) !== transaction.step_digest) {
|
|
23
|
-
throw new KernelLogIntegrityError(`kernel rebuild planned step diverged at step ${transaction.step_seq}`);
|
|
24
|
-
}
|
|
25
|
-
const committed = JSON.parse(runtime.commitPrepared(token));
|
|
26
|
-
if (await kernelRecordDigest(committed) !== transaction.step_digest) {
|
|
27
|
-
throw new KernelLogIntegrityError(`kernel rebuild committed step diverged at step ${transaction.step_seq}`);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
catch (error) {
|
|
31
|
-
if (token && prepared.status === "prepared") {
|
|
32
|
-
try {
|
|
33
|
-
runtime.abortPrepared(token);
|
|
34
|
-
}
|
|
35
|
-
catch { /* discard failed runtime */ }
|
|
36
|
-
}
|
|
37
|
-
throw error;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return { runtime, cursor };
|
|
41
|
-
}
|
|
42
|
-
async function assertFreshRuntimeMatchesGenesis(runtime, genesis) {
|
|
43
|
-
const snapshot = JSON.parse(runtime.snapshot());
|
|
44
|
-
if (snapshot.abi_version !== genesis.abi_version) {
|
|
45
|
-
throw new KernelLogIntegrityError("kernel runtime ABI version does not match operation genesis");
|
|
46
|
-
}
|
|
47
|
-
if (genesis.default_policy_version !== 1) {
|
|
48
|
-
throw new KernelLogIntegrityError("kernel operation uses an unsupported default policy version");
|
|
49
|
-
}
|
|
50
|
-
if (snapshot.operation_id || snapshot.next_step_seq !== 1 || snapshot.accepted_inputs.length !== 0) {
|
|
51
|
-
throw new KernelLogIntegrityError("kernel rebuild requires a fresh runtime");
|
|
52
|
-
}
|
|
53
|
-
if (await kernelRecordDigest(snapshot.initial_policy) !== await kernelRecordDigest(genesis.initial_scheduler_policy)) {
|
|
54
|
-
throw new KernelLogIntegrityError("kernel runtime initial policy does not match operation genesis");
|
|
55
|
-
}
|
|
56
|
-
const defaults = {
|
|
57
|
-
snapshot_version: snapshot.snapshot_version,
|
|
58
|
-
snapshot_input_limit: snapshot.snapshot_input_limit,
|
|
59
|
-
max_input_bytes: snapshot.max_input_bytes,
|
|
60
|
-
snapshot_journal_bytes_limit: snapshot.snapshot_journal_bytes_limit,
|
|
61
|
-
};
|
|
62
|
-
if (await kernelRecordDigest(defaults) !== await kernelRecordDigest(genesis.resolved_runtime_defaults)) {
|
|
63
|
-
throw new KernelLogIntegrityError("kernel runtime defaults do not match operation genesis");
|
|
64
|
-
}
|
|
65
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
export declare const KERNEL_LOG_RECORD_VERSION: 1;
|
|
2
|
-
export interface KernelOperationGenesisBody {
|
|
3
|
-
record_version: typeof KERNEL_LOG_RECORD_VERSION;
|
|
4
|
-
abi_version: number;
|
|
5
|
-
operation_id: string;
|
|
6
|
-
initial_scheduler_policy: Record<string, unknown>;
|
|
7
|
-
resolved_runtime_defaults: Record<string, unknown>;
|
|
8
|
-
default_policy_version: number;
|
|
9
|
-
}
|
|
10
|
-
export interface KernelOperationGenesis extends KernelOperationGenesisBody {
|
|
11
|
-
genesis_digest: string;
|
|
12
|
-
}
|
|
13
|
-
export interface KernelTransactionBody {
|
|
14
|
-
record_version: typeof KERNEL_LOG_RECORD_VERSION;
|
|
15
|
-
operation_id: string;
|
|
16
|
-
step_seq: number;
|
|
17
|
-
base_generation: number;
|
|
18
|
-
input: Record<string, unknown>;
|
|
19
|
-
input_digest: string;
|
|
20
|
-
previous_transaction_digest: string;
|
|
21
|
-
step_digest: string;
|
|
22
|
-
}
|
|
23
|
-
export interface KernelTransaction extends KernelTransactionBody {
|
|
24
|
-
transaction_digest: string;
|
|
25
|
-
}
|
|
26
|
-
export interface KernelGenesisReceipt {
|
|
27
|
-
log_seq: number;
|
|
28
|
-
genesis_digest: string;
|
|
29
|
-
}
|
|
30
|
-
export interface DurableAppendReceipt {
|
|
31
|
-
log_seq: number;
|
|
32
|
-
transaction_digest: string;
|
|
33
|
-
}
|
|
34
|
-
export interface KernelOperationCursor {
|
|
35
|
-
operation_id: string;
|
|
36
|
-
next_event_sequence: number;
|
|
37
|
-
next_step_seq: number;
|
|
38
|
-
transaction_head_digest: string;
|
|
39
|
-
}
|
|
40
|
-
export declare class KernelLogConflictError extends Error {
|
|
41
|
-
constructor(message: string);
|
|
42
|
-
}
|
|
43
|
-
export declare class KernelLogIntegrityError extends Error {
|
|
44
|
-
constructor(message: string);
|
|
45
|
-
}
|
|
46
|
-
export declare function canonicalKernelJson(value: unknown): string;
|
|
47
|
-
export declare function kernelRecordDigest(value: unknown): Promise<string>;
|
|
48
|
-
export declare function createKernelOperationGenesis(input: Omit<KernelOperationGenesisBody, "record_version">): Promise<KernelOperationGenesis>;
|
|
49
|
-
export declare function createKernelTransaction(input: {
|
|
50
|
-
operation_id: string;
|
|
51
|
-
step_seq: number;
|
|
52
|
-
base_generation: number;
|
|
53
|
-
input: Record<string, unknown>;
|
|
54
|
-
step: Record<string, unknown>;
|
|
55
|
-
previous_transaction_digest: string;
|
|
56
|
-
}): Promise<KernelTransaction>;
|
|
57
|
-
export declare function verifyKernelOperationGenesis(genesis: KernelOperationGenesis): Promise<void>;
|
|
58
|
-
export declare function verifyKernelTransaction(transaction: KernelTransaction): Promise<void>;
|
|
59
|
-
export declare function verifyKernelTransactionSuccessor(previous: KernelTransaction | undefined, transaction: KernelTransaction): void;
|
|
60
|
-
/** Validate one complete authoritative operation stream and derive its next wire cursor. */
|
|
61
|
-
export declare function verifyKernelTransactionStream(genesis: KernelOperationGenesis, transactions: readonly KernelTransaction[]): Promise<KernelOperationCursor>;
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
export const KERNEL_LOG_RECORD_VERSION = 1;
|
|
2
|
-
export class KernelLogConflictError extends Error {
|
|
3
|
-
constructor(message) {
|
|
4
|
-
super(message);
|
|
5
|
-
this.name = "KernelLogConflictError";
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
export class KernelLogIntegrityError extends Error {
|
|
9
|
-
constructor(message) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.name = "KernelLogIntegrityError";
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
export function canonicalKernelJson(value) {
|
|
15
|
-
if (value === null)
|
|
16
|
-
return "null";
|
|
17
|
-
if (typeof value === "string" || typeof value === "boolean")
|
|
18
|
-
return JSON.stringify(value);
|
|
19
|
-
if (typeof value === "number") {
|
|
20
|
-
if (!Number.isFinite(value))
|
|
21
|
-
throw new KernelLogIntegrityError("canonical records require finite numbers");
|
|
22
|
-
if (Number.isSafeInteger(value))
|
|
23
|
-
return JSON.stringify(Object.is(value, -0) ? 0 : value);
|
|
24
|
-
if (Number.isInteger(value)) {
|
|
25
|
-
throw new KernelLogIntegrityError("canonical record integer exceeds the cross-SDK safe range");
|
|
26
|
-
}
|
|
27
|
-
const bytes = new Uint8Array(8);
|
|
28
|
-
new DataView(bytes.buffer).setFloat64(0, value, false);
|
|
29
|
-
return `f64:${Array.from(bytes, byte => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
30
|
-
}
|
|
31
|
-
if (Array.isArray(value))
|
|
32
|
-
return `[${value.map(canonicalKernelJson).join(",")}]`;
|
|
33
|
-
if (typeof value === "object") {
|
|
34
|
-
const object = value;
|
|
35
|
-
const keys = Object.keys(object).sort();
|
|
36
|
-
return `{${keys.map(key => `${JSON.stringify(key)}:${canonicalKernelJson(object[key])}`).join(",")}}`;
|
|
37
|
-
}
|
|
38
|
-
throw new KernelLogIntegrityError(`unsupported canonical record value: ${typeof value}`);
|
|
39
|
-
}
|
|
40
|
-
export async function kernelRecordDigest(value) {
|
|
41
|
-
const bytes = new TextEncoder().encode(canonicalKernelJson(value));
|
|
42
|
-
const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
|
|
43
|
-
return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, "0")).join("");
|
|
44
|
-
}
|
|
45
|
-
export async function createKernelOperationGenesis(input) {
|
|
46
|
-
const body = { record_version: KERNEL_LOG_RECORD_VERSION, ...input };
|
|
47
|
-
validateGenesisBody(body);
|
|
48
|
-
return { ...body, genesis_digest: await kernelRecordDigest(body) };
|
|
49
|
-
}
|
|
50
|
-
export async function createKernelTransaction(input) {
|
|
51
|
-
const body = {
|
|
52
|
-
record_version: KERNEL_LOG_RECORD_VERSION,
|
|
53
|
-
operation_id: input.operation_id,
|
|
54
|
-
step_seq: input.step_seq,
|
|
55
|
-
base_generation: input.base_generation,
|
|
56
|
-
input: input.input,
|
|
57
|
-
input_digest: await kernelRecordDigest(input.input),
|
|
58
|
-
previous_transaction_digest: input.previous_transaction_digest,
|
|
59
|
-
step_digest: await kernelRecordDigest(input.step),
|
|
60
|
-
};
|
|
61
|
-
validateTransactionBody(body);
|
|
62
|
-
return { ...body, transaction_digest: await kernelRecordDigest(body) };
|
|
63
|
-
}
|
|
64
|
-
export async function verifyKernelOperationGenesis(genesis) {
|
|
65
|
-
const { genesis_digest, ...body } = genesis;
|
|
66
|
-
validateGenesisBody(body);
|
|
67
|
-
if (await kernelRecordDigest(body) !== genesis_digest) {
|
|
68
|
-
throw new KernelLogIntegrityError("kernel genesis digest does not match its canonical body");
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
export async function verifyKernelTransaction(transaction) {
|
|
72
|
-
const { transaction_digest, ...body } = transaction;
|
|
73
|
-
validateTransactionBody(body);
|
|
74
|
-
if (await kernelRecordDigest(body.input) !== body.input_digest) {
|
|
75
|
-
throw new KernelLogIntegrityError("kernel transaction input digest does not match its input");
|
|
76
|
-
}
|
|
77
|
-
if (await kernelRecordDigest(body) !== transaction_digest) {
|
|
78
|
-
throw new KernelLogIntegrityError("kernel transaction digest does not match its canonical body");
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
export function verifyKernelTransactionSuccessor(previous, transaction) {
|
|
82
|
-
const expectedStepSeq = previous ? previous.step_seq + 1 : 1;
|
|
83
|
-
const expectedGeneration = previous ? previous.base_generation + 1 : 0;
|
|
84
|
-
if (transaction.step_seq !== expectedStepSeq) {
|
|
85
|
-
throw new KernelLogIntegrityError(`kernel transaction step_seq ${transaction.step_seq} does not follow ${expectedStepSeq - 1}`);
|
|
86
|
-
}
|
|
87
|
-
if (transaction.base_generation !== expectedGeneration) {
|
|
88
|
-
throw new KernelLogIntegrityError(`kernel transaction base_generation ${transaction.base_generation} does not match ${expectedGeneration}`);
|
|
89
|
-
}
|
|
90
|
-
if (transaction.input.operation_id !== transaction.operation_id) {
|
|
91
|
-
throw new KernelLogIntegrityError("kernel transaction input operation_id does not match its envelope");
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
/** Validate one complete authoritative operation stream and derive its next wire cursor. */
|
|
95
|
-
export async function verifyKernelTransactionStream(genesis, transactions) {
|
|
96
|
-
await verifyKernelOperationGenesis(genesis);
|
|
97
|
-
let previous;
|
|
98
|
-
let head = genesis.genesis_digest;
|
|
99
|
-
for (const transaction of transactions) {
|
|
100
|
-
await verifyKernelTransaction(transaction);
|
|
101
|
-
if (transaction.operation_id !== genesis.operation_id) {
|
|
102
|
-
throw new KernelLogIntegrityError("kernel transaction operation_id does not match genesis");
|
|
103
|
-
}
|
|
104
|
-
if (transaction.previous_transaction_digest !== head) {
|
|
105
|
-
throw new KernelLogIntegrityError("kernel transaction digest chain is not continuous");
|
|
106
|
-
}
|
|
107
|
-
verifyKernelTransactionSuccessor(previous, transaction);
|
|
108
|
-
previous = transaction;
|
|
109
|
-
head = transaction.transaction_digest;
|
|
110
|
-
}
|
|
111
|
-
const nextSequence = (previous?.step_seq ?? 0) + 1;
|
|
112
|
-
return {
|
|
113
|
-
operation_id: genesis.operation_id,
|
|
114
|
-
next_event_sequence: nextSequence,
|
|
115
|
-
next_step_seq: nextSequence,
|
|
116
|
-
transaction_head_digest: head,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
function validateGenesisBody(genesis) {
|
|
120
|
-
if (genesis.record_version !== KERNEL_LOG_RECORD_VERSION)
|
|
121
|
-
throw new KernelLogIntegrityError("unsupported kernel genesis record version");
|
|
122
|
-
if (!Number.isSafeInteger(genesis.abi_version) || genesis.abi_version <= 0)
|
|
123
|
-
throw new KernelLogIntegrityError("kernel genesis abi_version must be a positive safe integer");
|
|
124
|
-
if (!genesis.operation_id)
|
|
125
|
-
throw new KernelLogIntegrityError("kernel genesis operation_id is required");
|
|
126
|
-
if (!Number.isSafeInteger(genesis.default_policy_version) || genesis.default_policy_version <= 0)
|
|
127
|
-
throw new KernelLogIntegrityError("kernel genesis default_policy_version must be a positive safe integer");
|
|
128
|
-
}
|
|
129
|
-
function validateTransactionBody(transaction) {
|
|
130
|
-
if (transaction.record_version !== KERNEL_LOG_RECORD_VERSION)
|
|
131
|
-
throw new KernelLogIntegrityError("unsupported kernel transaction record version");
|
|
132
|
-
if (!transaction.operation_id)
|
|
133
|
-
throw new KernelLogIntegrityError("kernel transaction operation_id is required");
|
|
134
|
-
if (!Number.isSafeInteger(transaction.step_seq) || transaction.step_seq <= 0)
|
|
135
|
-
throw new KernelLogIntegrityError("kernel transaction step_seq must be a positive safe integer");
|
|
136
|
-
if (!Number.isSafeInteger(transaction.base_generation) || transaction.base_generation < 0)
|
|
137
|
-
throw new KernelLogIntegrityError("kernel transaction base_generation must be a non-negative safe integer");
|
|
138
|
-
if (!transaction.previous_transaction_digest)
|
|
139
|
-
throw new KernelLogIntegrityError("kernel transaction previous_transaction_digest is required");
|
|
140
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
export interface ToolResult {
|
|
2
|
-
callId: string;
|
|
3
|
-
tool: string;
|
|
4
|
-
output: string;
|
|
5
|
-
isError?: boolean;
|
|
6
|
-
}
|
|
7
|
-
export interface SpooledToolResult {
|
|
8
|
-
originalOutput: string;
|
|
9
|
-
preview: string;
|
|
10
|
-
spoolRef: string;
|
|
11
|
-
wasSpooled: boolean;
|
|
12
|
-
}
|
|
13
|
-
export interface SpoolConfig {
|
|
14
|
-
spoolThresholdBytes: number;
|
|
15
|
-
previewTokens: number;
|
|
16
|
-
totalMessageLimitBytes: number;
|
|
17
|
-
maxAgeMs?: number;
|
|
18
|
-
driver?: SpoolStorageDriver;
|
|
19
|
-
}
|
|
20
|
-
export interface SpoolStorageDriver {
|
|
21
|
-
write(key: string, content: string): Promise<void> | void;
|
|
22
|
-
read(key: string): Promise<string> | string;
|
|
23
|
-
delete(key: string): Promise<void> | void;
|
|
24
|
-
list(): Promise<string[]> | string[];
|
|
25
|
-
mtime?(key: string): Promise<number> | number;
|
|
26
|
-
}
|
|
27
|
-
export declare class MemorySpoolDriver implements SpoolStorageDriver {
|
|
28
|
-
private cache;
|
|
29
|
-
write(key: string, content: string): Promise<void>;
|
|
30
|
-
read(key: string): Promise<string>;
|
|
31
|
-
delete(key: string): Promise<void>;
|
|
32
|
-
list(): Promise<string[]>;
|
|
33
|
-
mtime(key: string): Promise<number>;
|
|
34
|
-
}
|
|
35
|
-
export declare const DEFAULT_SPOOL_CONFIG: SpoolConfig;
|
|
36
|
-
export declare class LargeResultSpool {
|
|
37
|
-
private config;
|
|
38
|
-
private driver;
|
|
39
|
-
private activeWrites;
|
|
40
|
-
constructor(config?: Partial<SpoolConfig>);
|
|
41
|
-
private needsSpool;
|
|
42
|
-
private getSpoolKey;
|
|
43
|
-
private writeToDriver;
|
|
44
|
-
private generatePreview;
|
|
45
|
-
processToolResult(result: ToolResult): Promise<SpooledToolResult>;
|
|
46
|
-
private callKey;
|
|
47
|
-
persistOutput(sessionId: string, callId: string, content: string): Promise<string>;
|
|
48
|
-
readSpooledResult(spoolRef: string): Promise<string>;
|
|
49
|
-
/**
|
|
50
|
-
* O7: locate a spooled output by the tool call's id (the `read_result` meta-tool only knows
|
|
51
|
-
* `call_id`, not the content-hashed key `persistOutput` chose). Scans the driver's key list for
|
|
52
|
-
* the hashed session-scoped call-key prefix; returns `undefined` if nothing was ever
|
|
53
|
-
* spooled for that call.
|
|
54
|
-
*/
|
|
55
|
-
findByCallId(sessionId: string, callId: string): Promise<string | undefined>;
|
|
56
|
-
cleanup(maxAgeMs?: number): Promise<number>;
|
|
57
|
-
}
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
export class MemorySpoolDriver {
|
|
2
|
-
cache = new Map();
|
|
3
|
-
async write(key, content) {
|
|
4
|
-
this.cache.set(key, { content, mtime: Date.now() });
|
|
5
|
-
}
|
|
6
|
-
async read(key) {
|
|
7
|
-
const val = this.cache.get(key);
|
|
8
|
-
if (!val)
|
|
9
|
-
throw new Error(`Spooled result not found: ${key}`);
|
|
10
|
-
return val.content;
|
|
11
|
-
}
|
|
12
|
-
async delete(key) {
|
|
13
|
-
this.cache.delete(key);
|
|
14
|
-
}
|
|
15
|
-
async list() {
|
|
16
|
-
return Array.from(this.cache.keys());
|
|
17
|
-
}
|
|
18
|
-
async mtime(key) {
|
|
19
|
-
return this.cache.get(key)?.mtime ?? 0;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
export const DEFAULT_SPOOL_CONFIG = {
|
|
23
|
-
spoolThresholdBytes: 50 * 1024, // 50KB
|
|
24
|
-
previewTokens: 500, // ~2KB
|
|
25
|
-
totalMessageLimitBytes: 200 * 1024, // 200KB
|
|
26
|
-
};
|
|
27
|
-
function simpleHash(content) {
|
|
28
|
-
let hash = 5381;
|
|
29
|
-
for (let i = 0; i < content.length; i++) {
|
|
30
|
-
hash = (hash * 33) ^ content.charCodeAt(i);
|
|
31
|
-
}
|
|
32
|
-
return (hash >>> 0).toString(16);
|
|
33
|
-
}
|
|
34
|
-
export class LargeResultSpool {
|
|
35
|
-
config;
|
|
36
|
-
driver;
|
|
37
|
-
activeWrites = new Map();
|
|
38
|
-
constructor(config = {}) {
|
|
39
|
-
this.config = { ...DEFAULT_SPOOL_CONFIG, ...config };
|
|
40
|
-
this.driver = this.config.driver ?? new MemorySpoolDriver();
|
|
41
|
-
}
|
|
42
|
-
needsSpool(result) {
|
|
43
|
-
return result.output.length > this.config.spoolThresholdBytes;
|
|
44
|
-
}
|
|
45
|
-
getSpoolKey(hash) {
|
|
46
|
-
return `.spool/${hash}.txt`;
|
|
47
|
-
}
|
|
48
|
-
async writeToDriver(content, hash) {
|
|
49
|
-
const key = this.getSpoolKey(hash);
|
|
50
|
-
let promise = this.activeWrites.get(key);
|
|
51
|
-
if (!promise) {
|
|
52
|
-
promise = (async () => {
|
|
53
|
-
try {
|
|
54
|
-
await this.driver.write(key, content);
|
|
55
|
-
return key;
|
|
56
|
-
}
|
|
57
|
-
finally {
|
|
58
|
-
this.activeWrites.delete(key);
|
|
59
|
-
}
|
|
60
|
-
})();
|
|
61
|
-
this.activeWrites.set(key, promise);
|
|
62
|
-
}
|
|
63
|
-
return promise;
|
|
64
|
-
}
|
|
65
|
-
generatePreview(content) {
|
|
66
|
-
const previewTokens = Math.min(this.config.previewTokens, content.length / 4);
|
|
67
|
-
const preview = content.substring(0, previewTokens);
|
|
68
|
-
const omitted = content.length - previewTokens;
|
|
69
|
-
return `[tool_result_spooled]
|
|
70
|
-
size: ${content.length} bytes
|
|
71
|
-
preview: first ${previewTokens} chars
|
|
72
|
-
omitted: ${omitted} chars
|
|
73
|
-
[full content available via Read tool]
|
|
74
|
-
`;
|
|
75
|
-
}
|
|
76
|
-
async processToolResult(result) {
|
|
77
|
-
if (!this.needsSpool(result)) {
|
|
78
|
-
return {
|
|
79
|
-
originalOutput: result.output,
|
|
80
|
-
preview: result.output,
|
|
81
|
-
spoolRef: "",
|
|
82
|
-
wasSpooled: false,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
const hash = simpleHash(result.output);
|
|
86
|
-
const spoolRef = await this.writeToDriver(result.output, hash);
|
|
87
|
-
const preview = this.generatePreview(result.output);
|
|
88
|
-
return {
|
|
89
|
-
originalOutput: result.output,
|
|
90
|
-
preview,
|
|
91
|
-
spoolRef,
|
|
92
|
-
wasSpooled: true,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
callKey(sessionId, callId) {
|
|
96
|
-
// Session-scoped and hashed: the driver's key space is shared across sessions and outlives
|
|
97
|
-
// runs, while vendor call ids can be index-style ("call_0") and repeat — an unscoped key
|
|
98
|
-
// lets read_result in one session fetch another session's spooled output. Hashing also keeps
|
|
99
|
-
// untrusted call-id characters out of driver keys (parity with Node/Python).
|
|
100
|
-
return simpleHash(`${sessionId}\u0000${callId}`);
|
|
101
|
-
}
|
|
102
|
-
async persistOutput(sessionId, callId, content) {
|
|
103
|
-
const hash = simpleHash(content);
|
|
104
|
-
const key = `.spool/${this.callKey(sessionId, callId)}-${hash.slice(0, 16)}.txt`;
|
|
105
|
-
let promise = this.activeWrites.get(key);
|
|
106
|
-
if (!promise) {
|
|
107
|
-
promise = (async () => {
|
|
108
|
-
try {
|
|
109
|
-
await this.driver.write(key, content);
|
|
110
|
-
return key;
|
|
111
|
-
}
|
|
112
|
-
finally {
|
|
113
|
-
this.activeWrites.delete(key);
|
|
114
|
-
}
|
|
115
|
-
})();
|
|
116
|
-
this.activeWrites.set(key, promise);
|
|
117
|
-
}
|
|
118
|
-
return promise;
|
|
119
|
-
}
|
|
120
|
-
async readSpooledResult(spoolRef) {
|
|
121
|
-
return this.driver.read(spoolRef);
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* O7: locate a spooled output by the tool call's id (the `read_result` meta-tool only knows
|
|
125
|
-
* `call_id`, not the content-hashed key `persistOutput` chose). Scans the driver's key list for
|
|
126
|
-
* the hashed session-scoped call-key prefix; returns `undefined` if nothing was ever
|
|
127
|
-
* spooled for that call.
|
|
128
|
-
*/
|
|
129
|
-
async findByCallId(sessionId, callId) {
|
|
130
|
-
let keys;
|
|
131
|
-
try {
|
|
132
|
-
keys = await this.driver.list();
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
return undefined;
|
|
136
|
-
}
|
|
137
|
-
const prefix = `.spool/${this.callKey(sessionId, callId)}-`;
|
|
138
|
-
const match = keys.find(k => k.startsWith(prefix) && k.endsWith('.txt'));
|
|
139
|
-
if (!match)
|
|
140
|
-
return undefined;
|
|
141
|
-
try {
|
|
142
|
-
return await this.driver.read(match);
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
return undefined;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
async cleanup(maxAgeMs) {
|
|
149
|
-
const limit = maxAgeMs ?? this.config.maxAgeMs ?? 7 * 24 * 60 * 60 * 1000;
|
|
150
|
-
try {
|
|
151
|
-
const keys = await this.driver.list();
|
|
152
|
-
let count = 0;
|
|
153
|
-
const now = Date.now();
|
|
154
|
-
for (const key of keys) {
|
|
155
|
-
if (this.driver.mtime) {
|
|
156
|
-
const mtime = await this.driver.mtime(key);
|
|
157
|
-
if (now - mtime > limit) {
|
|
158
|
-
await this.driver.delete(key);
|
|
159
|
-
count++;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return count;
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
166
|
-
return 0;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|