@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/agents/subagent.js +4 -2
  3. package/dist/core/auto-compaction.d.ts +12 -1
  4. package/dist/core/auto-compaction.js +3 -1
  5. package/dist/core/checkpoint-store.d.ts +8 -1
  6. package/dist/core/checkpoint-store.js +3 -1
  7. package/dist/core/compliance.d.ts +11 -0
  8. package/dist/core/compliance.js +34 -0
  9. package/dist/core/exec-gate.js +12 -1
  10. package/dist/core/governance-codes.d.ts +12 -0
  11. package/dist/core/governance-codes.js +24 -0
  12. package/dist/core/locked-config.d.ts +27 -0
  13. package/dist/core/locked-config.js +42 -0
  14. package/dist/core/memory-admission.d.ts +47 -0
  15. package/dist/core/memory-admission.js +156 -0
  16. package/dist/core/memory.d.ts +2 -0
  17. package/dist/core/memory.js +3 -2
  18. package/dist/core/retention.d.ts +36 -0
  19. package/dist/core/retention.js +31 -0
  20. package/dist/core/runner/assemble-result.js +1 -1
  21. package/dist/core/runner/compaction-call-options.d.ts +13 -1
  22. package/dist/core/runner/compaction-call-options.js +85 -0
  23. package/dist/core/runner/prepare-memory.d.ts +9 -0
  24. package/dist/core/runner/prepare-memory.js +28 -2
  25. package/dist/core/runner/prepare-task.d.ts +4 -0
  26. package/dist/core/runner/prepare-task.js +183 -26
  27. package/dist/core/runner/runtask.js +101 -47
  28. package/dist/core/runner/turn-attachments.js +3 -1
  29. package/dist/core/session-store.d.ts +1 -0
  30. package/dist/core/session-store.js +1 -0
  31. package/dist/core/tool-result-store.d.ts +2 -0
  32. package/dist/core/tool-result-store.js +1 -0
  33. package/dist/core/types.d.ts +12 -2
  34. package/dist/engine/compaction/compaction.d.ts +11 -2
  35. package/dist/engine/compaction/compaction.js +87 -9
  36. package/dist/engine/harness/agent-harness.js +11 -1
  37. package/dist/engine/llm/validation.js +11 -1
  38. package/dist/index.d.ts +7 -2
  39. package/dist/index.js +6 -1
  40. package/dist/prompt-assembly/event-registry.js +1 -1
  41. 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;