@sema-agent/core 5.11.0 → 5.13.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 +52 -0
- package/dist/agents/subagent.js +4 -2
- package/dist/core/auto-compaction.d.ts +12 -1
- package/dist/core/auto-compaction.js +3 -1
- package/dist/core/checkpoint-store.d.ts +8 -1
- package/dist/core/checkpoint-store.js +3 -1
- package/dist/core/compliance.d.ts +11 -0
- package/dist/core/compliance.js +34 -0
- package/dist/core/exec-gate.js +12 -1
- package/dist/core/governance-codes.d.ts +12 -0
- package/dist/core/governance-codes.js +24 -0
- package/dist/core/locked-config.d.ts +27 -0
- package/dist/core/locked-config.js +42 -0
- package/dist/core/memory-admission.d.ts +47 -0
- package/dist/core/memory-admission.js +156 -0
- package/dist/core/memory.d.ts +2 -0
- package/dist/core/memory.js +3 -2
- package/dist/core/retention.d.ts +36 -0
- package/dist/core/retention.js +31 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/compaction-call-options.d.ts +13 -1
- package/dist/core/runner/compaction-call-options.js +85 -0
- package/dist/core/runner/prepare-memory.d.ts +9 -0
- package/dist/core/runner/prepare-memory.js +28 -2
- package/dist/core/runner/prepare-task.d.ts +4 -0
- package/dist/core/runner/prepare-task.js +183 -26
- package/dist/core/runner/runtask.js +101 -47
- package/dist/core/runner/turn-attachments.js +3 -1
- package/dist/core/session-store.d.ts +1 -0
- package/dist/core/session-store.js +1 -0
- package/dist/core/tool-result-store.d.ts +2 -0
- package/dist/core/tool-result-store.js +1 -0
- package/dist/core/types.d.ts +12 -2
- package/dist/engine/compaction/compaction.d.ts +11 -2
- package/dist/engine/compaction/compaction.js +87 -9
- package/dist/engine/harness/agent-harness.js +11 -1
- package/dist/engine/llm/validation.js +11 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.js +6 -1
- package/dist/prompt-assembly/event-registry.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type RetentionDeclaration = "managed" | "none";
|
|
2
|
+
export interface RetentionDeclaring {
|
|
3
|
+
readonly retention?: RetentionDeclaration;
|
|
4
|
+
}
|
|
5
|
+
export interface RetentionPolicy {
|
|
6
|
+
maxAgeDays: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ManagedRetentionCapability {
|
|
9
|
+
listRetentionDomains(): Promise<readonly string[]>;
|
|
10
|
+
expireCheckpoints(input: {
|
|
11
|
+
domain: string;
|
|
12
|
+
cutoffMs: number;
|
|
13
|
+
}): Promise<RetentionReceipt>;
|
|
14
|
+
deleteExpiredSessions(input: {
|
|
15
|
+
domain: string;
|
|
16
|
+
cutoffMs: number;
|
|
17
|
+
}): Promise<RetentionReceipt>;
|
|
18
|
+
deleteOrphanToolResults(input: {
|
|
19
|
+
domain: string;
|
|
20
|
+
cutoffMs: number;
|
|
21
|
+
}): Promise<RetentionReceipt>;
|
|
22
|
+
}
|
|
23
|
+
export interface RetentionReceipt {
|
|
24
|
+
domain: string;
|
|
25
|
+
deleted: number;
|
|
26
|
+
skipped: number;
|
|
27
|
+
tombstones: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function assertRetentionCapability(input: {
|
|
30
|
+
policy: RetentionPolicy | undefined;
|
|
31
|
+
locked: boolean;
|
|
32
|
+
stores: ReadonlyArray<{
|
|
33
|
+
name: string;
|
|
34
|
+
store: object | undefined;
|
|
35
|
+
}>;
|
|
36
|
+
}): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
function codedError(code, message) {
|
|
2
|
+
const e = new Error(message);
|
|
3
|
+
e.code = code;
|
|
4
|
+
return e;
|
|
5
|
+
}
|
|
6
|
+
export function assertRetentionCapability(input) {
|
|
7
|
+
const { policy, locked, stores } = input;
|
|
8
|
+
if (policy !== undefined && (!Number.isFinite(policy.maxAgeDays) || policy.maxAgeDays < 0)) {
|
|
9
|
+
throw codedError("config.retention_policy", `retentionPolicy.maxAgeDays must be a finite non-negative number of days (got ${String(policy.maxAgeDays)}).`);
|
|
10
|
+
}
|
|
11
|
+
const declaredRetention = (name, store) => {
|
|
12
|
+
const r = store.retention;
|
|
13
|
+
if (r === undefined || r === "none")
|
|
14
|
+
return "none";
|
|
15
|
+
if (r === "managed")
|
|
16
|
+
return "managed";
|
|
17
|
+
throw codedError("config.retention_capability", `store "${name}" declares an unparseable retention value ${JSON.stringify(r)} — expected "managed" or "none".`);
|
|
18
|
+
};
|
|
19
|
+
const declarations = stores
|
|
20
|
+
.filter((s) => s.store !== undefined)
|
|
21
|
+
.map(({ name, store }) => ({ name, retention: declaredRetention(name, store) }));
|
|
22
|
+
if (!locked || policy === undefined)
|
|
23
|
+
return;
|
|
24
|
+
const incapable = declarations.filter(({ retention }) => retention !== "managed");
|
|
25
|
+
if (incapable.length > 0) {
|
|
26
|
+
throw codedError("config.retention_capability", `retention policy is LOCKED but ${incapable.map((s) => s.name).join(", ")} ` +
|
|
27
|
+
`declare${incapable.length === 1 ? "s" : ""} retention "none" (or no declaration — read fail-closed as "none"): ` +
|
|
28
|
+
`a locked retention policy over stores that cannot delete would be "policy locked, data immortal". ` +
|
|
29
|
+
`Wire managed-retention stores, or unlock/remove the retention policy.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -154,7 +154,7 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
154
154
|
if (errorCode !== undefined && SALVAGE_ELIGIBLE_TERMINALS.has(errorCode)) {
|
|
155
155
|
salvagedOutput = text.trim() || undefined;
|
|
156
156
|
}
|
|
157
|
-
const retryAfterMs = errorCode === "usage.window_exhausted" ? flags.retryAfterMs : undefined;
|
|
157
|
+
const retryAfterMs = errorCode === "usage.window_exhausted" || errorCode === "memory.admission_required" ? flags.retryAfterMs : undefined;
|
|
158
158
|
const { compactionMicroUsd: _internalCompaction, ...publicStats } = stats;
|
|
159
159
|
void _internalCompaction;
|
|
160
160
|
if (flags.unpricedSpend)
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { type TracerHook } from "../trace.js";
|
|
2
2
|
import type { MaybeCompactOptions } from "../auto-compaction.js";
|
|
3
|
-
import type { TaskSpec } from "../types.js";
|
|
3
|
+
import type { StaleToolResultOffloadOptions, TaskSpec } from "../types.js";
|
|
4
|
+
import type { Context } from "../../internal/llm.js";
|
|
5
|
+
import { type ToolResultStore } from "../tool-result-store.js";
|
|
4
6
|
import type { Prepared } from "./prepare-task.js";
|
|
5
7
|
export declare function buildWorkingFileAttachments(spec: TaskSpec, prepared: Prepared): MaybeCompactOptions["workingFileAttachments"];
|
|
8
|
+
export declare function forkContextOption(prepared: Prepared, disable: boolean): Pick<MaybeCompactOptions, "forkContext">;
|
|
6
9
|
export declare function centerAdoptionOption(prepared: Prepared): Partial<Pick<MaybeCompactOptions, "centerAdoption">>;
|
|
10
|
+
export declare const STALE_OFFLOAD_DEFAULT_KEEP_RECENT_PER_TOOL = 3;
|
|
11
|
+
export declare const STALE_OFFLOAD_DEFAULT_MIN_SAVINGS_CHARS = 2000;
|
|
12
|
+
export interface ResolvedStaleToolResultOffload {
|
|
13
|
+
keepRecentPerTool: number;
|
|
14
|
+
minSavingsChars: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function resolveStaleToolResultOffload(knob: StaleToolResultOffloadOptions | undefined): ResolvedStaleToolResultOffload | undefined;
|
|
17
|
+
export declare function buildStaleOffloadPointer(toolName: string, ref: string, chars: number): string;
|
|
18
|
+
export declare function projectStaleToolResults(context: Context, cfg: ResolvedStaleToolResultOffload, store: ToolResultStore, sessionId: string, writtenRefs: Set<string>): Promise<Context>;
|
|
7
19
|
export declare function emitInputTruncated(tracer: TracerHook | undefined, taskId: string): NonNullable<MaybeCompactOptions["onInputTruncated"]>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { emitTrace } from "../trace.js";
|
|
3
|
+
import { buildToolResultRef, OFFLOAD_TOOL_NAME, PERSISTED_OUTPUT_PREFIX } from "../tool-result-store.js";
|
|
2
4
|
export function buildWorkingFileAttachments(spec, prepared) {
|
|
3
5
|
if (spec.compaction?.attachWorkingFiles === false || !prepared.readTaskFile)
|
|
4
6
|
return undefined;
|
|
@@ -13,10 +15,93 @@ export function buildWorkingFileAttachments(spec, prepared) {
|
|
|
13
15
|
...(typeof spec.compaction?.attachWorkingFiles === "object" ? spec.compaction.attachWorkingFiles : undefined),
|
|
14
16
|
};
|
|
15
17
|
}
|
|
18
|
+
export function forkContextOption(prepared, disable) {
|
|
19
|
+
return disable ? {} : { forkContext: prepared.lastBrainContext };
|
|
20
|
+
}
|
|
16
21
|
export function centerAdoptionOption(prepared) {
|
|
17
22
|
const ca = prepared.centerCompactionCandidate?.();
|
|
18
23
|
return ca !== undefined ? { centerAdoption: ca } : {};
|
|
19
24
|
}
|
|
25
|
+
export const STALE_OFFLOAD_DEFAULT_KEEP_RECENT_PER_TOOL = 3;
|
|
26
|
+
export const STALE_OFFLOAD_DEFAULT_MIN_SAVINGS_CHARS = 2000;
|
|
27
|
+
export function resolveStaleToolResultOffload(knob) {
|
|
28
|
+
if (knob === undefined)
|
|
29
|
+
return undefined;
|
|
30
|
+
const check = (name, v, fallback) => {
|
|
31
|
+
if (v === undefined)
|
|
32
|
+
return fallback;
|
|
33
|
+
if (!Number.isInteger(v) || v < 0) {
|
|
34
|
+
const e = new Error(`compaction.staleToolResultOffload.${name} must be a non-negative integer, got ${String(v)}`);
|
|
35
|
+
e.code = "config.stale_tool_result_offload_invalid";
|
|
36
|
+
throw e;
|
|
37
|
+
}
|
|
38
|
+
return v;
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
keepRecentPerTool: check("keepRecentPerTool", knob.keepRecentPerTool, STALE_OFFLOAD_DEFAULT_KEEP_RECENT_PER_TOOL),
|
|
42
|
+
minSavingsChars: check("minSavingsChars", knob.minSavingsChars, STALE_OFFLOAD_DEFAULT_MIN_SAVINGS_CHARS),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function buildStaleOffloadPointer(toolName, ref, chars) {
|
|
46
|
+
return (`[Stale tool result offloaded to save context: ${chars} chars from an earlier "${toolName}" call ` +
|
|
47
|
+
`saved to persisted output ref "${ref}". Newer results of this tool are still shown in full below; ` +
|
|
48
|
+
`use the ${OFFLOAD_TOOL_NAME} tool with this ref if you need the offloaded content again.]`);
|
|
49
|
+
}
|
|
50
|
+
function toolResultText(m) {
|
|
51
|
+
return m.content
|
|
52
|
+
.filter((b) => b.type === "text")
|
|
53
|
+
.map((b) => b.text)
|
|
54
|
+
.join("\n");
|
|
55
|
+
}
|
|
56
|
+
export async function projectStaleToolResults(context, cfg, store, sessionId, writtenRefs) {
|
|
57
|
+
const byTool = new Map();
|
|
58
|
+
context.messages.forEach((m, i) => {
|
|
59
|
+
if (m.role !== "toolResult" || m.isError)
|
|
60
|
+
return;
|
|
61
|
+
const list = byTool.get(m.toolName);
|
|
62
|
+
const row = { idx: i, msg: m };
|
|
63
|
+
if (list === undefined)
|
|
64
|
+
byTool.set(m.toolName, [row]);
|
|
65
|
+
else
|
|
66
|
+
list.push(row);
|
|
67
|
+
});
|
|
68
|
+
const replacements = new Map();
|
|
69
|
+
for (const [toolName, rows] of byTool) {
|
|
70
|
+
const staleCount = rows.length - cfg.keepRecentPerTool;
|
|
71
|
+
for (let k = 0; k < staleCount; k++) {
|
|
72
|
+
const { idx, msg } = rows[k];
|
|
73
|
+
const text = toolResultText(msg);
|
|
74
|
+
if (text.startsWith(PERSISTED_OUTPUT_PREFIX))
|
|
75
|
+
continue;
|
|
76
|
+
const ref = buildToolResultRef(sessionId, `${msg.toolCallId}_s${createHash("sha256").update(text, "utf8").digest("hex").slice(0, 32)}`);
|
|
77
|
+
const pointer = buildStaleOffloadPointer(toolName, ref, text.length);
|
|
78
|
+
if (text.length - pointer.length < cfg.minSavingsChars)
|
|
79
|
+
continue;
|
|
80
|
+
if (!writtenRefs.has(ref)) {
|
|
81
|
+
try {
|
|
82
|
+
await store.put(ref, text);
|
|
83
|
+
writtenRefs.add(ref);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
replacements.set(idx, { msg, pointer });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (replacements.size === 0)
|
|
93
|
+
return context;
|
|
94
|
+
return {
|
|
95
|
+
...context,
|
|
96
|
+
messages: context.messages.map((m, i) => {
|
|
97
|
+
const hit = replacements.get(i);
|
|
98
|
+
if (hit === undefined)
|
|
99
|
+
return m;
|
|
100
|
+
const rest = hit.msg.content.filter((b) => b.type !== "text");
|
|
101
|
+
return { ...hit.msg, content: [{ type: "text", text: hit.pointer }, ...rest] };
|
|
102
|
+
}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
20
105
|
export function emitInputTruncated(tracer, taskId) {
|
|
21
106
|
return (info) => emitTrace(tracer, () => ({
|
|
22
107
|
kind: "compaction.input_truncated",
|
|
@@ -8,6 +8,13 @@ export interface PrepareMemoryInput {
|
|
|
8
8
|
memoryWriteGateRef: {
|
|
9
9
|
current?: BeforeWriteHook;
|
|
10
10
|
};
|
|
11
|
+
admissionCtx: {
|
|
12
|
+
orgMemoryDenied: boolean;
|
|
13
|
+
complianceDegraded: boolean;
|
|
14
|
+
parentAdmittedOrgScopes: readonly string[] | undefined;
|
|
15
|
+
priorOwnVerdict: import("../memory-admission.js").OwnOrgAdmissionVerdict | undefined;
|
|
16
|
+
governedProvenance: boolean;
|
|
17
|
+
};
|
|
11
18
|
}
|
|
12
19
|
export interface PrepareMemoryResult {
|
|
13
20
|
memoryEngineSession: Prepared["memoryEngineSession"];
|
|
@@ -16,5 +23,7 @@ export interface PrepareMemoryResult {
|
|
|
16
23
|
path: string;
|
|
17
24
|
content: string;
|
|
18
25
|
}>;
|
|
26
|
+
admittedOrgScopes: readonly string[];
|
|
27
|
+
ownOrgVerdict: import("../memory-admission.js").OwnOrgAdmissionVerdict | undefined;
|
|
19
28
|
}
|
|
20
29
|
export declare function prepareMemory(input: PrepareMemoryInput): Promise<PrepareMemoryResult>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { admitMemoryScopes } from "../memory-admission.js";
|
|
1
2
|
import { adoptLegacyRepoDirs, deriveRepoControlPlaneDir, deriveProjectControlDir, deriveProjectMemoryDir, deriveRepoMemoryDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, lookupProjectIdHint, recordProjectIdHint, resolveMemoryEngineRoot } from "../memory-engine/layout.js";
|
|
2
3
|
import { classifyScopePlanes, derivePersonalControlDir, derivePersonalMemoryDir, mergeHarvestReports, mergeInjections, needsDualRoots, parsedProjectPlane } from "../memory-engine/dual-root.js";
|
|
3
4
|
import { normalizeMemorySpec } from "../memory.js";
|
|
@@ -5,8 +6,33 @@ import { MemoryEngine } from "../memory-engine/engine.js";
|
|
|
5
6
|
import { assertScopeContractPlacement, parseScopeKey, resolveProjectId } from "../memory-engine/scope-contract.js";
|
|
6
7
|
import { FileMemoryEngineBackend } from "../memory-engine/file-backend.js";
|
|
7
8
|
export async function prepareMemory(input) {
|
|
8
|
-
const { spec, deps, sessionId, taskRootPath, memoryWriteGateRef } = input;
|
|
9
|
+
const { spec, deps, sessionId, taskRootPath, memoryWriteGateRef, admissionCtx } = input;
|
|
9
10
|
const memorySpec = normalizeMemorySpec(spec.memory);
|
|
11
|
+
let admittedOrgScopes = [];
|
|
12
|
+
let ownOrgVerdict;
|
|
13
|
+
if (memorySpec && memorySpec.enabled && deps.memoryBackend) {
|
|
14
|
+
const outcome = await admitMemoryScopes({
|
|
15
|
+
memorySpec,
|
|
16
|
+
principal: spec.principal,
|
|
17
|
+
admission: deps.memoryScopeAdmission,
|
|
18
|
+
deploymentScopes: new Set(deps.deploymentMemoryScopes ?? []),
|
|
19
|
+
orgMemoryDenied: admissionCtx.orgMemoryDenied,
|
|
20
|
+
complianceDegraded: admissionCtx.complianceDegraded,
|
|
21
|
+
parentAdmittedOrgScopes: admissionCtx.parentAdmittedOrgScopes,
|
|
22
|
+
priorOwnVerdict: admissionCtx.priorOwnVerdict,
|
|
23
|
+
governedProvenance: admissionCtx.governedProvenance,
|
|
24
|
+
});
|
|
25
|
+
admittedOrgScopes = outcome.admittedOrgScopes;
|
|
26
|
+
ownOrgVerdict = outcome.ownVerdict;
|
|
27
|
+
for (const droppedScope of outcome.droppedDeploymentScopes) {
|
|
28
|
+
deps.onError?.(new Error(`org memory scope "${droppedScope}" (deployment-declared) was narrowed away (admission resolver or the session's frozen verdict) — the layer is not mounted this session.`), { phase: "memory", sessionId });
|
|
29
|
+
}
|
|
30
|
+
if (outcome.writeScopeNarrowed) {
|
|
31
|
+
deps.onError?.(new Error(`org memory writeScope "${String(memorySpec.writeScope)}" was not explicitly granted by admission — the session's memory write face is read-only (org layers default read-only).`), { phase: "memory", sessionId });
|
|
32
|
+
}
|
|
33
|
+
memorySpec.scopes = outcome.scopes;
|
|
34
|
+
memorySpec.writeScope = outcome.writeScope;
|
|
35
|
+
}
|
|
10
36
|
const useMemoryEngine = Boolean(memorySpec && memorySpec.enabled && deps.memoryBackend);
|
|
11
37
|
let memoryEngineSession;
|
|
12
38
|
if (useMemoryEngine && memorySpec) {
|
|
@@ -193,5 +219,5 @@ export async function prepareMemory(input) {
|
|
|
193
219
|
if (memoryBlock !== undefined && injection.indexSeed !== undefined)
|
|
194
220
|
seedFiles = [injection.indexSeed];
|
|
195
221
|
}
|
|
196
|
-
return { memoryEngineSession, memoryBlock, ...(seedFiles !== undefined ? { seedFiles } : {}) };
|
|
222
|
+
return { memoryEngineSession, memoryBlock, admittedOrgScopes, ownOrgVerdict, ...(seedFiles !== undefined ? { seedFiles } : {}) };
|
|
197
223
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AgentHarness, type ThinkingLevel } from "../../internal/harness.js";
|
|
2
2
|
import type { Model } from "../../internal/llm.js";
|
|
3
|
+
import { type CompactionForkContext } from "../auto-compaction.js";
|
|
3
4
|
import { type MaterializedMcp } from "../mcp.js";
|
|
4
5
|
import { type MaterializedA2a } from "../a2a.js";
|
|
5
6
|
import type { HarvestReport, MemorySessionHandle } from "../memory-engine/types.js";
|
|
@@ -199,6 +200,7 @@ export interface Prepared {
|
|
|
199
200
|
toolEffects: Map<string, ToolEffect>;
|
|
200
201
|
wakeRecovered: RecoveredOrphan[];
|
|
201
202
|
promptOverheadTokens: number;
|
|
203
|
+
lastBrainContext: () => CompactionForkContext | undefined;
|
|
202
204
|
readTaskFile?: (path: string) => Promise<string | null>;
|
|
203
205
|
recentlyReadFiles?: () => string[];
|
|
204
206
|
normalizeAttachmentPath?: (raw: string) => Promise<string>;
|
|
@@ -302,6 +304,8 @@ export interface InheritedGate {
|
|
|
302
304
|
rules: SessionPermissionRules;
|
|
303
305
|
}>;
|
|
304
306
|
shellGate?: "off" | "always" | "classify";
|
|
307
|
+
admittedOrgScopes?: readonly string[];
|
|
308
|
+
orgAdmissionGoverned?: true;
|
|
305
309
|
parentConstraints?: ReadonlyArray<{
|
|
306
310
|
policy: ToolPolicy;
|
|
307
311
|
onAsk?: OnAsk;
|