@sema-agent/core 5.25.0 → 5.26.0
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/CHANGELOG.md +66 -0
- package/dist/agents/agent-definition.js +5 -0
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +1 -0
- package/dist/agents/subagent.js +5 -0
- package/dist/core/hooks.js +3 -2
- package/dist/core/memory-engine/dual-root.js +3 -1
- package/dist/core/memory-engine/engine.d.ts +45 -1
- package/dist/core/memory-engine/engine.js +23 -5
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/index.js +1 -1
- package/dist/core/permission-rule-consent.js +8 -1
- package/dist/core/runner/compaction-call-options.d.ts +4 -4
- package/dist/core/runner/compaction-call-options.js +3 -4
- package/dist/core/runner/prepare-memory.d.ts +34 -15
- package/dist/core/runner/prepare-memory.js +85 -17
- package/dist/core/runner/prepare-task.js +31 -7
- package/dist/core/store-contracts/tool-result-store-contract.d.ts +6 -0
- package/dist/core/store-contracts/tool-result-store-contract.js +24 -0
- package/dist/core/task-registry-agent.js +3 -3
- package/dist/core/task-registry-monitor.js +6 -5
- package/dist/core/tool-result-budget.d.ts +1 -1
- package/dist/core/tool-result-budget.js +3 -3
- package/dist/core/tool-result-store.d.ts +164 -9
- package/dist/core/tool-result-store.js +82 -23
- package/dist/core/types.d.ts +68 -0
- package/dist/core/untrusted-text.d.ts +6 -2
- package/dist/core/untrusted-text.js +1 -1
- package/dist/engine/session/import-validate.js +2 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/orchestration/workflow.js +2 -0
- package/dist/prompts/default.d.ts +11 -0
- package/dist/prompts/default.js +3 -0
- package/dist/stores/file/fs-atomic.d.ts +1 -1
- package/dist/stores/file/tool-result-store.d.ts +45 -9
- package/dist/stores/file/tool-result-store.js +76 -9
- package/dist/tools/fs/fs-shared.js +5 -4
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { sep } from "node:path";
|
|
1
2
|
import { admitMemoryScopes } from "../memory-admission.js";
|
|
2
|
-
import { adoptLegacyRepoDirs, deriveRepoControlPlaneDir, deriveProjectControlDir, deriveProjectMemoryDir, deriveRepoMemoryDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, lookupProjectIdHint, recordProjectIdHint, resolveMemoryEngineRoot } from "../memory-engine/layout.js";
|
|
3
|
+
import { adoptLegacyRepoDirs, canonicalize, deriveRepoControlPlaneDir, deriveProjectControlDir, deriveProjectMemoryDir, deriveRepoMemoryDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, isContainedIn, lookupProjectIdHint, recordProjectIdHint, resolveMemoryEngineRoot } from "../memory-engine/layout.js";
|
|
3
4
|
import { classifyScopePlanes, derivePersonalControlDir, derivePersonalMemoryDir, mergeHarvestReports, mergeInjections, needsDualRoots, parsedProjectPlane } from "../memory-engine/dual-root.js";
|
|
4
5
|
import { normalizeMemorySpec } from "../memory.js";
|
|
5
|
-
import { MEMORY_PREFERENCE_DISCIPLINE, MEMORY_RECALL_DISCIPLINE, MemoryEngine } from "../memory-engine/engine.js";
|
|
6
|
+
import { MEMORY_ANNOUNCEMENT_READONLY_CODA, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_RECALL_DISCIPLINE, MemoryEngine } from "../memory-engine/engine.js";
|
|
6
7
|
import { createMemoryEngineTools } from "../memory-engine/tools.js";
|
|
7
8
|
import { assertScopeContractPlacement, parseScopeKey, resolveProjectId } from "../memory-engine/scope-contract.js";
|
|
8
9
|
import { FileMemoryEngineBackend } from "../memory-engine/file-backend.js";
|
|
@@ -113,10 +114,12 @@ export async function prepareMemory(input) {
|
|
|
113
114
|
}
|
|
114
115
|
const personalMemoryDir = derivePersonalMemoryDir(engineRoot);
|
|
115
116
|
const personalControlDir = derivePersonalControlDir(engineRoot);
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
const effectiveControlDir = (b, fallback) => {
|
|
118
|
+
const pinnedCtl = b.controlPlaneRoot;
|
|
119
|
+
return typeof pinnedCtl === "string" && pinnedCtl ? pinnedCtl : fallback;
|
|
120
|
+
};
|
|
121
|
+
const choosePersonalBackend = () => typeof pinned === "string" && pinned ? new FileMemoryEngineBackend(personalMemoryDir, { controlDir: personalControlDir }) : backend;
|
|
122
|
+
const createPersonalEngine = (personalBackend) => {
|
|
120
123
|
return {
|
|
121
124
|
engine: new MemoryEngine({ backend: personalBackend, memoryDir: personalMemoryDir, controlDir: personalControlDir, onIncident: onEngineIncident }),
|
|
122
125
|
backend: personalBackend,
|
|
@@ -131,17 +134,43 @@ export async function prepareMemory(input) {
|
|
|
131
134
|
};
|
|
132
135
|
let writeEngine;
|
|
133
136
|
let writeHandle;
|
|
137
|
+
let readOnlyEngine;
|
|
138
|
+
let readOnlyHandle;
|
|
134
139
|
let injectFn;
|
|
135
140
|
let harvestBoth;
|
|
136
141
|
let toolPlanes;
|
|
142
|
+
const admitNothingOpts = input.memoryPersistenceDeclared === false
|
|
143
|
+
? { admitNothing: { reason: "harvest admitted nothing: memory persistence is declared unavailable for this session (memoryPersistenceCapable: false)" } }
|
|
144
|
+
: {};
|
|
137
145
|
if (dual) {
|
|
146
|
+
const personalBackendChosen = choosePersonalBackend();
|
|
147
|
+
{
|
|
148
|
+
const projCtl = effectiveControlDir(backend, identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot));
|
|
149
|
+
const persCtl = effectiveControlDir(personalBackendChosen, personalControlDir);
|
|
150
|
+
const overlap = (a, b) => isContainedIn(a, b) || isContainedIn(b, a);
|
|
151
|
+
const refuse = (what, a, b) => {
|
|
152
|
+
const e = new Error(`memory dual-root configuration error: ${what} overlap (${canonicalize(a)} vs ${canonicalize(b)}) — the planes' data roots and control planes must all be disjoint.`);
|
|
153
|
+
e.code = "config.memory_dual_root_overlap";
|
|
154
|
+
throw e;
|
|
155
|
+
};
|
|
156
|
+
if (overlap(memoryDir, personalMemoryDir))
|
|
157
|
+
refuse("the project and personal memory roots", memoryDir, personalMemoryDir);
|
|
158
|
+
if (overlap(projCtl, persCtl))
|
|
159
|
+
refuse("the project and personal control planes", projCtl, persCtl);
|
|
160
|
+
for (const [ctlName, ctl] of [["project control plane", projCtl], ["personal control plane", persCtl]]) {
|
|
161
|
+
for (const [rootName, dataRoot] of [["project memory root", memoryDir], ["personal memory mount", personalMemoryDir]]) {
|
|
162
|
+
if (overlap(ctl, dataRoot))
|
|
163
|
+
refuse(`the ${ctlName} and the ${rootName}`, ctl, dataRoot);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
138
167
|
const projectEngine = new MemoryEngine({
|
|
139
168
|
backend,
|
|
140
169
|
memoryDir,
|
|
141
170
|
controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot),
|
|
142
171
|
onIncident: onEngineIncident,
|
|
143
172
|
});
|
|
144
|
-
const personal = createPersonalEngine();
|
|
173
|
+
const personal = createPersonalEngine(personalBackendChosen);
|
|
145
174
|
const personalEngine = personal.engine;
|
|
146
175
|
const p = planes;
|
|
147
176
|
const projectHandle = await projectEngine.materialize(p.project, p.writePlane === "project" ? memorySpec.writeScope : null);
|
|
@@ -149,6 +178,8 @@ export async function prepareMemory(input) {
|
|
|
149
178
|
const writeIsPersonal = p.writePlane === "personal";
|
|
150
179
|
writeEngine = writeIsPersonal ? personalEngine : projectEngine;
|
|
151
180
|
writeHandle = writeIsPersonal ? personalHandle : projectHandle;
|
|
181
|
+
readOnlyEngine = writeIsPersonal ? projectEngine : personalEngine;
|
|
182
|
+
readOnlyHandle = writeIsPersonal ? projectHandle : personalHandle;
|
|
152
183
|
injectFn = () => mergeInjections(projectEngine.inject(projectHandle, { writeToolMounted: input.writeToolsMounted }), personalEngine.inject(personalHandle, { writeToolMounted: input.writeToolsMounted }));
|
|
153
184
|
toolPlanes = [
|
|
154
185
|
{
|
|
@@ -167,7 +198,7 @@ export async function prepareMemory(input) {
|
|
|
167
198
|
harvestBoth = async () => {
|
|
168
199
|
const writeFirst = writeIsPersonal ? [personalEngine, personalHandle] : [projectEngine, projectHandle];
|
|
169
200
|
const readOther = writeIsPersonal ? [projectEngine, projectHandle] : [personalEngine, personalHandle];
|
|
170
|
-
const writeReport = await writeFirst[0].harvest(writeFirst[1], { ...pollutedOpts(writeFirst[0]), sessionId });
|
|
201
|
+
const writeReport = await writeFirst[0].harvest(writeFirst[1], { ...pollutedOpts(writeFirst[0]), sessionId, ...admitNothingOpts });
|
|
171
202
|
let readReport;
|
|
172
203
|
let readFailure;
|
|
173
204
|
try {
|
|
@@ -183,13 +214,13 @@ export async function prepareMemory(input) {
|
|
|
183
214
|
};
|
|
184
215
|
}
|
|
185
216
|
else if (personalOnly) {
|
|
186
|
-
const personal = createPersonalEngine();
|
|
217
|
+
const personal = createPersonalEngine(choosePersonalBackend());
|
|
187
218
|
const personalEngine = personal.engine;
|
|
188
219
|
const handle = await personalEngine.materialize(memorySpec.scopes, memorySpec.writeScope);
|
|
189
220
|
writeEngine = personalEngine;
|
|
190
221
|
writeHandle = handle;
|
|
191
222
|
injectFn = () => personalEngine.inject(handle, { writeToolMounted: input.writeToolsMounted });
|
|
192
|
-
harvestBoth = () => personalEngine.harvest(handle, { ...pollutedOpts(personalEngine), sessionId });
|
|
223
|
+
harvestBoth = () => personalEngine.harvest(handle, { ...pollutedOpts(personalEngine), sessionId, ...admitNothingOpts });
|
|
193
224
|
toolPlanes = [
|
|
194
225
|
{
|
|
195
226
|
backend: retrievalBackend(personal.backend),
|
|
@@ -210,7 +241,7 @@ export async function prepareMemory(input) {
|
|
|
210
241
|
writeEngine = engine;
|
|
211
242
|
writeHandle = handle;
|
|
212
243
|
injectFn = () => engine.inject(handle, { writeToolMounted: input.writeToolsMounted });
|
|
213
|
-
harvestBoth = () => engine.harvest(handle, { ...pollutedOpts(engine), sessionId });
|
|
244
|
+
harvestBoth = () => engine.harvest(handle, { ...pollutedOpts(engine), sessionId, ...admitNothingOpts });
|
|
214
245
|
toolPlanes = [
|
|
215
246
|
{
|
|
216
247
|
backend: retrievalBackend(backend),
|
|
@@ -220,7 +251,25 @@ export async function prepareMemory(input) {
|
|
|
220
251
|
},
|
|
221
252
|
];
|
|
222
253
|
}
|
|
223
|
-
memoryWriteGateRef.current = (w) =>
|
|
254
|
+
memoryWriteGateRef.current = (w) => {
|
|
255
|
+
if (readOnlyEngine !== undefined && readOnlyHandle !== undefined) {
|
|
256
|
+
const ro = readOnlyEngine.gateWrite(readOnlyHandle, w.key, w.content);
|
|
257
|
+
if (!ro.ok)
|
|
258
|
+
return ro;
|
|
259
|
+
}
|
|
260
|
+
if (input.memoryPersistenceDeclared === false) {
|
|
261
|
+
const root = writeHandle.writableRoot;
|
|
262
|
+
if (w.key === root || w.key.startsWith(`${root}${sep}`)) {
|
|
263
|
+
return {
|
|
264
|
+
ok: false,
|
|
265
|
+
code: "read_only_layering",
|
|
266
|
+
reason: `memory persistence is declared unavailable for this session (memoryPersistenceCapable: false) — writes into the memory store are refused. Nothing was written.`,
|
|
267
|
+
muted: false,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return writeEngine.gateWrite(writeHandle, w.key, w.content);
|
|
272
|
+
};
|
|
224
273
|
const harvestSafe = async (phase = "terminal") => {
|
|
225
274
|
try {
|
|
226
275
|
const report = await harvestBoth();
|
|
@@ -261,7 +310,10 @@ export async function prepareMemory(input) {
|
|
|
261
310
|
memoryTools = createMemoryEngineTools({ planes: toolPlanes });
|
|
262
311
|
}
|
|
263
312
|
catch (err) {
|
|
264
|
-
if (typeof err.code === "string" &&
|
|
313
|
+
if (typeof err.code === "string" &&
|
|
314
|
+
(err.code.startsWith("config.memory_scope") ||
|
|
315
|
+
err.code.startsWith("config.memory_project") ||
|
|
316
|
+
err.code === "config.memory_dual_root_overlap")) {
|
|
265
317
|
throw err;
|
|
266
318
|
}
|
|
267
319
|
deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "memory", sessionId });
|
|
@@ -275,15 +327,31 @@ export async function prepareMemory(input) {
|
|
|
275
327
|
}
|
|
276
328
|
if (memoryEngineSession) {
|
|
277
329
|
const injection = memoryEngineSession.inject();
|
|
278
|
-
|
|
279
|
-
|
|
330
|
+
let blockBody = injection.block;
|
|
331
|
+
if (!input.rosterCanPersist && injection.instruction === "" && injection.readOnlyNotice === undefined) {
|
|
332
|
+
blockBody = blockBody.trim() ? `${MEMORY_READONLY_NOTICE}\n\n${blockBody}` : MEMORY_READONLY_NOTICE;
|
|
333
|
+
}
|
|
334
|
+
else if (!input.rosterCanPersist && injection.instruction !== "") {
|
|
335
|
+
blockBody = [MEMORY_READONLY_NOTICE, injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
|
|
336
|
+
}
|
|
337
|
+
else if (input.memoryPersistenceDeclared === true && injection.readOnlyNotice !== undefined) {
|
|
338
|
+
blockBody = [injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
|
|
339
|
+
}
|
|
340
|
+
if (!input.rosterCanPersist &&
|
|
341
|
+
(injection.announceBlock?.trim() ?? "") !== "" &&
|
|
342
|
+
blockBody.includes(injection.announceBlock) &&
|
|
343
|
+
!blockBody.trimEnd().endsWith(MEMORY_ANNOUNCEMENT_READONLY_CODA)) {
|
|
344
|
+
blockBody = `${blockBody}\n\n${MEMORY_ANNOUNCEMENT_READONLY_CODA}`;
|
|
345
|
+
}
|
|
346
|
+
if (blockBody.trim())
|
|
347
|
+
memoryBlock = blockBody;
|
|
280
348
|
if (memoryTools !== undefined) {
|
|
281
349
|
memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${MEMORY_RECALL_DISCIPLINE}` : MEMORY_RECALL_DISCIPLINE;
|
|
282
350
|
}
|
|
283
|
-
if (memoryEngineSession.handle.writeScope !== null && input.writeToolsMounted) {
|
|
351
|
+
if (memoryEngineSession.handle.writeScope !== null && input.writeToolsMounted && input.rosterCanPersist) {
|
|
284
352
|
memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${MEMORY_PREFERENCE_DISCIPLINE}` : MEMORY_PREFERENCE_DISCIPLINE;
|
|
285
353
|
}
|
|
286
|
-
if (memoryBlock !== undefined && injection.indexSeed !== undefined)
|
|
354
|
+
if (memoryBlock !== undefined && injection.indexSeed !== undefined && input.rosterCanPersist)
|
|
287
355
|
seedFiles = [injection.indexSeed];
|
|
288
356
|
}
|
|
289
357
|
return { memoryEngineSession, memoryBlock, admittedOrgScopes, ownOrgVerdict, ...(memoryTools !== undefined ? { memoryTools } : {}), ...(seedFiles !== undefined ? { seedFiles } : {}) };
|
|
@@ -352,6 +352,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
352
352
|
e.code = "config.tool_materialize_invalid";
|
|
353
353
|
throw e;
|
|
354
354
|
}
|
|
355
|
+
if (spec.memoryPersistenceCapable !== undefined && typeof spec.memoryPersistenceCapable !== "boolean") {
|
|
356
|
+
const e = new Error(`memoryPersistenceCapable must be a boolean when present (got ${JSON.stringify(spec.memoryPersistenceCapable)}) — a non-boolean would silently read as capable.`);
|
|
357
|
+
e.code = "config.memory_persistence_invalid";
|
|
358
|
+
throw e;
|
|
359
|
+
}
|
|
355
360
|
if (spec.toolMaterializeStrategy === "static" && spec.deferSelfResolve === false) {
|
|
356
361
|
const e = new Error(`toolMaterializeStrategy "static" cannot be combined with deferSelfResolve: false — with the direct-call ` +
|
|
357
362
|
`lane disabled a placeholder is never swapped and never self-resolves, so no deferred tool could ever be ` +
|
|
@@ -1154,6 +1159,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1154
1159
|
...(spec.additionalDirectories !== undefined ? { additionalDirectories: Object.freeze([...spec.additionalDirectories]) } : {}),
|
|
1155
1160
|
...(spec.additionalReadDirectories !== undefined ? { additionalReadDirectories: Object.freeze([...spec.additionalReadDirectories]) } : {}),
|
|
1156
1161
|
...(spec.envFacts !== undefined ? { envFacts: { ...spec.envFacts } } : {}),
|
|
1162
|
+
...(spec.memoryPersistenceCapable !== undefined ? { memoryPersistenceCapable: spec.memoryPersistenceCapable } : {}),
|
|
1157
1163
|
getApiKeyAndHeaders: spec.getApiKeyAndHeaders,
|
|
1158
1164
|
parentCwd: taskRootPath,
|
|
1159
1165
|
...(centerAdoption !== undefined ? { centerArtifactDigest: centerAdoption.artifact.artifactDigest } : {}),
|
|
@@ -1687,20 +1693,24 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1687
1693
|
`Set spec.shellGate to "classify" or "always" if this deployment expects doctrine-gated shell behavior.`), { phase: "config", sessionId, classification: "shell-gate-off" });
|
|
1688
1694
|
}
|
|
1689
1695
|
if (shellGate !== "off" && !(executionEnv instanceof StubExecutionEnv) && spec.handsReadOnly !== true) {
|
|
1690
|
-
|
|
1691
|
-
|
|
1696
|
+
const bashTierBefore = irreversibilityTier.get("Bash");
|
|
1697
|
+
shellGatedBash = !egressTools.has("Bash") && bashTierBefore !== "always" && bashTierBefore !== "maybe";
|
|
1698
|
+
const bashEffectiveTier = shellGate === "always" || bashTierBefore === "always" ? "always" : "maybe";
|
|
1699
|
+
irreversibilityTier.set("Bash", bashEffectiveTier);
|
|
1692
1700
|
irreversibleTools.add("Bash");
|
|
1693
1701
|
const shellReadBoundary = () => ({
|
|
1694
1702
|
roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical],
|
|
1695
1703
|
...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}),
|
|
1696
1704
|
});
|
|
1697
|
-
if (shellGate === "classify")
|
|
1705
|
+
if (shellGate === "classify" && shellGatedBash)
|
|
1698
1706
|
reversibilityProbes.set("Bash", bashReversibilityProbe(undefined, shellReadBoundary));
|
|
1699
1707
|
if (backgroundTaskToolsActive) {
|
|
1700
|
-
|
|
1701
|
-
|
|
1708
|
+
const monitorTierBefore = irreversibilityTier.get("Monitor");
|
|
1709
|
+
shellGatedMonitor = !egressTools.has("Monitor") && monitorTierBefore !== "always" && monitorTierBefore !== "maybe";
|
|
1710
|
+
const monitorEffectiveTier = shellGate === "always" || monitorTierBefore === "always" ? "always" : "maybe";
|
|
1711
|
+
irreversibilityTier.set("Monitor", monitorEffectiveTier);
|
|
1702
1712
|
irreversibleTools.add("Monitor");
|
|
1703
|
-
if (shellGate === "classify")
|
|
1713
|
+
if (shellGate === "classify" && shellGatedMonitor)
|
|
1704
1714
|
reversibilityProbes.set("Monitor", bashReversibilityProbe(undefined, shellReadBoundary));
|
|
1705
1715
|
}
|
|
1706
1716
|
}
|
|
@@ -1936,7 +1946,21 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1936
1946
|
sessionId,
|
|
1937
1947
|
taskRootPath,
|
|
1938
1948
|
memoryWriteGateRef,
|
|
1939
|
-
writeToolsMounted: tools.some((t) => t.name === "Write") &&
|
|
1949
|
+
writeToolsMounted: tools.some((t) => t.name === "Write") &&
|
|
1950
|
+
!(toolFaceSnapshot.exclude?.includes("Write") ?? false) &&
|
|
1951
|
+
!(handsEnabled && isRemoteExecutionEnv(executionEnv) && spec.memoryPersistenceCapable !== true),
|
|
1952
|
+
memoryPersistenceDeclared: spec.memoryPersistenceCapable,
|
|
1953
|
+
rosterCanPersist: spec.memoryPersistenceCapable ??
|
|
1954
|
+
tools.some((t) => {
|
|
1955
|
+
if (toolFaceSnapshot.exclude?.includes(t.name) ?? false)
|
|
1956
|
+
return false;
|
|
1957
|
+
const effect = (t.effect ?? toolEffects.get(t.name) ?? "write");
|
|
1958
|
+
if (effect === "read")
|
|
1959
|
+
return false;
|
|
1960
|
+
if (t.name !== "Write" && t.name !== "Edit" && t.name !== "NotebookEdit" && t.name !== "Bash")
|
|
1961
|
+
return false;
|
|
1962
|
+
return !(handsEnabled && isRemoteExecutionEnv(executionEnv));
|
|
1963
|
+
}),
|
|
1940
1964
|
memorySearchToolsPlanned,
|
|
1941
1965
|
admissionCtx: {
|
|
1942
1966
|
orgMemoryDenied: complianceDenies.has("org_memory_mount"),
|
|
@@ -12,5 +12,11 @@ import { type ContractAssertionRunner } from "./contract-harness.js";
|
|
|
12
12
|
* with a narrower native key space owns an injective encoding, not a rejection). Before 2.1.0 the
|
|
13
13
|
* bundled backends genuinely diverged on both halves (dev-green / deployment-red), so running this
|
|
14
14
|
* kit against a pre-2.1.0-modeled backend is expected to go red on those two entries.
|
|
15
|
+
*
|
|
16
|
+
* Backlog #119 adds the provenance leg: `put`'s third argument and the `ownerOf` read face. `ownerOf`
|
|
17
|
+
* is typed OPTIONAL on the interface (source compatibility for a backend written against an older
|
|
18
|
+
* engine), but this kit REQUIRES it — a backend that cannot say who owns a ref cannot authorize a
|
|
19
|
+
* host-side read face, and an optional parameter one store honors while another drops it silently is
|
|
20
|
+
* exactly the divergence the two legs above exist to prevent.
|
|
15
21
|
*/
|
|
16
22
|
export declare function toolResultStoreContract(make: () => ToolResultStore, runAssertion?: ContractAssertionRunner): Promise<void>;
|
|
@@ -31,5 +31,29 @@ export async function toolResultStoreContract(make, runAssertion) {
|
|
|
31
31
|
}
|
|
32
32
|
assert.equal((await store.get("tr_sess_call:1")).content, "payload-tr_sess_call:1");
|
|
33
33
|
});
|
|
34
|
+
run("#119 provenance: content+owner are one write; ownerOf answers; a different owner is a TYPED refusal; unowned rows stay unowned", async () => {
|
|
35
|
+
const store = make();
|
|
36
|
+
assert.equal(typeof store.ownerOf, "function", "a backend must implement ownerOf — a store that cannot say who owns a ref cannot back a host read face");
|
|
37
|
+
const ownerOf = (ref) => Promise.resolve(store.ownerOf(ref));
|
|
38
|
+
const ref = "tr_s1~c1";
|
|
39
|
+
await store.put(ref, "0123456789", { sessionId: "sess-A", taskId: "task-1" });
|
|
40
|
+
assert.deepEqual(await ownerOf(ref), { sessionId: "sess-A", taskId: "task-1" }, "the winning write's owner round-trips");
|
|
41
|
+
await store.put(ref, "IGNORED", { sessionId: "sess-A", taskId: "task-1" });
|
|
42
|
+
assert.equal((await store.get(ref)).content, "0123456789");
|
|
43
|
+
assert.deepEqual(await ownerOf(ref), { sessionId: "sess-A", taskId: "task-1" });
|
|
44
|
+
await store.put(ref, "IGNORED");
|
|
45
|
+
assert.deepEqual(await ownerOf(ref), { sessionId: "sess-A", taskId: "task-1" }, "an ownerless put must not clear the owner");
|
|
46
|
+
for (const other of [{ sessionId: "sess-B" }, { sessionId: "sess-A", taskId: "task-2" }]) {
|
|
47
|
+
await assert.rejects((async () => store.put(ref, "other tenant's bytes", other))(), (err) => err.code === "tool_result.ref_conflict", `put(${JSON.stringify(other)}) on an occupied ref must reject with code tool_result.ref_conflict`);
|
|
48
|
+
}
|
|
49
|
+
assert.equal((await store.get(ref)).content, "0123456789", "a refused put must not have overwritten anything");
|
|
50
|
+
const unowned = "tr_no_owner~c";
|
|
51
|
+
await store.put(unowned, "bytes from a write site that stated no owner");
|
|
52
|
+
assert.equal(await ownerOf(unowned), undefined, "a row stored without provenance is UNOWNED");
|
|
53
|
+
await store.put(unowned, "bytes from a write site that stated no owner", { sessionId: "sess-A" });
|
|
54
|
+
assert.equal(await ownerOf(unowned), undefined, "put must not back-fill an owner onto an unowned row");
|
|
55
|
+
assert.equal((await store.get(unowned)).content, "bytes from a write site that stated no owner");
|
|
56
|
+
assert.equal(await ownerOf("tr_never~written"), undefined);
|
|
57
|
+
});
|
|
34
58
|
await settle();
|
|
35
59
|
}
|
|
@@ -6,7 +6,7 @@ import { shutdownDebug } from "./shutdown-debug.js";
|
|
|
6
6
|
import { delimitUntrusted } from "./untrusted-text.js";
|
|
7
7
|
import { boundedRedactedSummary } from "./untrusted-egress.js";
|
|
8
8
|
import { mintCompletionId, commitCompletionIdIfEmpty, clipTaskOutput, assertOwnership, sleepPollStep, alreadyTerminalStopNote, canAccess, normalizeAgentName, closestName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR, } from "./task-registry-shared.js";
|
|
9
|
-
import { buildToolResultRef, OFFLOAD_TOOL_NAME } from "./tool-result-store.js";
|
|
9
|
+
import { buildToolResultRef, OFFLOAD_TOOL_NAME, toolResultProvenanceOf } from "./tool-result-store.js";
|
|
10
10
|
export function ensureDurableHeartbeatLane(core) {
|
|
11
11
|
if (core.durableHeartbeatTimer !== undefined)
|
|
12
12
|
return;
|
|
@@ -1232,8 +1232,8 @@ export async function spillClippedAgentResult(handle, full, clipped, store, sess
|
|
|
1232
1232
|
if (store === undefined)
|
|
1233
1233
|
return clipped;
|
|
1234
1234
|
if (handle.spillRef === undefined) {
|
|
1235
|
-
const ref = buildToolResultRef(sessionId ?? "no-session",
|
|
1236
|
-
await store.put(ref, full);
|
|
1235
|
+
const ref = buildToolResultRef(sessionId ?? "no-session", handle.id, `c${handle.reviveCycle ?? 0}`);
|
|
1236
|
+
await store.put(ref, full, sessionId === undefined ? undefined : toolResultProvenanceOf(sessionId, handle.id));
|
|
1237
1237
|
handle.spillRef = ref;
|
|
1238
1238
|
}
|
|
1239
1239
|
return `${clipped}\n\n[full output persisted — call ${OFFLOAD_TOOL_NAME} with ref "${handle.spillRef}" to read it back.]`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { delimitUntrusted } from "./untrusted-text.js";
|
|
2
2
|
import { assertOwnership, defaultMonitorTimers, MONITOR_MAX_TIMEOUT_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_BATCH_WINDOW_MS, MONITOR_MAX_BATCHES_PER_MINUTE, MONITOR_STORM_BURST, MONITOR_STORM_KILL_AFTER_MS, MONITOR_LINE_BUF_CAP, MONITOR_SPILL_CAP_CHARS, TASK_OUTPUT_MAX_CHARS, mintCompletionId, clipMonitorEvent, clipMonitorLine, terminalTaskSummary, accountDroppedBytes, renderSpoolBody, rollSpoolText, statusFromBackground, droppedGapNote, firstDropNote, alreadyTerminalStopNote, clipTaskOutput, sleepPollStep, } from "./task-registry-shared.js";
|
|
3
|
-
import { buildToolResultRef, OFFLOAD_TOOL_NAME } from "./tool-result-store.js";
|
|
3
|
+
import { buildToolResultRef, OFFLOAD_TOOL_NAME, toolResultProvenanceOf } from "./tool-result-store.js";
|
|
4
4
|
export function registerMonitorLane(core, input) {
|
|
5
5
|
assertOwnership(input, "registerMonitor");
|
|
6
6
|
const id = core.mintTaskId("monitor");
|
|
@@ -61,14 +61,15 @@ function spillRolledMonitorChunk(handle, stream, dropped) {
|
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
63
|
const n = stream === "out" ? (handle.spillSegCount ?? 0) : (handle.spillErrSegCount ?? 0);
|
|
64
|
-
const ref = buildToolResultRef(handle.spillSessionId ?? "no-session",
|
|
64
|
+
const ref = buildToolResultRef(handle.spillSessionId ?? "no-session", handle.id, `${stream}_seg${n}`);
|
|
65
65
|
handle.spillCharsUsed = used + dropped.length;
|
|
66
66
|
if (stream === "out")
|
|
67
67
|
handle.spillSegCount = n + 1;
|
|
68
68
|
else
|
|
69
69
|
handle.spillErrSegCount = n + 1;
|
|
70
70
|
try {
|
|
71
|
-
|
|
71
|
+
const provenance = handle.spillSessionId === undefined ? undefined : toolResultProvenanceOf(handle.spillSessionId, handle.id);
|
|
72
|
+
void Promise.resolve(store.put(ref, dropped, provenance)).catch(() => {
|
|
72
73
|
handle.spillFailed = true;
|
|
73
74
|
});
|
|
74
75
|
}
|
|
@@ -83,8 +84,8 @@ function monitorSpillNote(handle) {
|
|
|
83
84
|
return "";
|
|
84
85
|
const sid = handle.spillSessionId ?? "no-session";
|
|
85
86
|
const segLabel = (stream, n) => {
|
|
86
|
-
const first = buildToolResultRef(sid,
|
|
87
|
-
return n <= 1 ? `ref "${first}"` : `refs "${first}" .. "${buildToolResultRef(sid,
|
|
87
|
+
const first = buildToolResultRef(sid, handle.id, `${stream}_seg0`);
|
|
88
|
+
return n <= 1 ? `ref "${first}"` : `refs "${first}" .. "${buildToolResultRef(sid, handle.id, `${stream}_seg${n - 1}`)}"`;
|
|
88
89
|
};
|
|
89
90
|
const clauses = [];
|
|
90
91
|
if (outN > 0)
|
|
@@ -10,7 +10,7 @@ import { type ToolResultStore } from "./tool-result-store.js";
|
|
|
10
10
|
* **Simpler than CC by construction (§24.2):** a request-only, non-destructive transform (returns a new
|
|
11
11
|
* array; the durable session keeps full results) applied in the existing `harness.on("context")` hook —
|
|
12
12
|
* the same per-query point as `clearStaleToolResults` (the mirror of CC's query.ts:379). It is DETERMINISTIC
|
|
13
|
-
* (stable `ref = tr_<sessionId
|
|
13
|
+
* (stable `ref = tr_<sessionId>~<toolCallId>` + deterministic {@link buildPreview}), so re-running it every
|
|
14
14
|
* query yields byte-identical previews → prompt-cache safe **without** CC's ContentReplacementState freeze
|
|
15
15
|
* machine (the determinism IS the freeze), and resume-free (the transcript holds originals; this re-applies
|
|
16
16
|
* on replay). Reuses design/30's offload store + preview format; with no store it falls back to a
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isToolResult } from "./message-utils.js";
|
|
2
|
-
import { buildPreview, buildToolResultRef, OFFLOAD_TOOL_NAME, PERSISTED_OUTPUT_PREFIX } from "./tool-result-store.js";
|
|
2
|
+
import { buildPreview, buildToolResultRef, OFFLOAD_TOOL_NAME, PERSISTED_OUTPUT_PREFIX, toolResultContentSegment, toolResultProvenanceOf, } from "./tool-result-store.js";
|
|
3
3
|
export const AGGREGATE_TOOL_RESULT_BUDGET_CHARS = 200_000;
|
|
4
4
|
const HEAD = 1_000;
|
|
5
5
|
const TAIL = 1_000;
|
|
@@ -61,13 +61,13 @@ export async function capAggregateToolResults(messages, opts) {
|
|
|
61
61
|
const previewOne = async (k, before, head, tail, fromOriginal) => {
|
|
62
62
|
const m = out[k];
|
|
63
63
|
const full = textOf((fromOriginal ? messages[k] : m).content);
|
|
64
|
-
const ref = buildToolResultRef(opts.sessionId, m.toolCallId ?? `idx${k}
|
|
64
|
+
const ref = buildToolResultRef(opts.sessionId, m.toolCallId ?? `idx${k}`, toolResultContentSegment(full));
|
|
65
65
|
const sizes = head === HEAD && tail === TAIL ? undefined : { head, tail };
|
|
66
66
|
let previewText;
|
|
67
67
|
let storeFallback;
|
|
68
68
|
if (opts.store) {
|
|
69
69
|
try {
|
|
70
|
-
await opts.store.put(ref, full);
|
|
70
|
+
await opts.store.put(ref, full, toolResultProvenanceOf(opts.sessionId));
|
|
71
71
|
previewText = buildPreview(full, ref, sizes);
|
|
72
72
|
storeFallback = false;
|
|
73
73
|
}
|