@sema-agent/core 5.48.0 → 5.50.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 +112 -0
- package/dist/agents/agent-transcript-tool.d.ts +1 -1
- package/dist/agents/agent-transcript-tool.js +1 -1
- package/dist/agents/roster-store.js +4 -1
- package/dist/agents/send-message-tool.d.ts +2 -2
- package/dist/agents/send-message-tool.js +2 -2
- package/dist/agents/subagent.d.ts +6 -0
- package/dist/agents/subagent.js +126 -1
- package/dist/agents/teacher.d.ts +25 -1
- package/dist/agents/teacher.js +89 -13
- package/dist/brain/anthropic.js +11 -20
- package/dist/brain/open-responses.js +6 -14
- package/dist/brain/openai.js +6 -18
- package/dist/brain/reasoning.d.ts +100 -8
- package/dist/brain/reasoning.js +39 -15
- package/dist/brain/request-params.d.ts +37 -1
- package/dist/brain/request-params.js +40 -2
- package/dist/core/background-agent-store.d.ts +1 -1
- package/dist/core/background-agent-store.js +5 -4
- package/dist/core/mcp.d.ts +7 -1
- package/dist/core/mcp.js +64 -8
- package/dist/core/memory-engine/delegation-settlement.d.ts +27 -0
- package/dist/core/memory-engine/delegation-settlement.js +31 -4
- package/dist/core/memory-engine/dual-root.js +11 -0
- package/dist/core/memory-engine/engine.d.ts +36 -2
- package/dist/core/memory-engine/engine.js +354 -38
- package/dist/core/memory-engine/layout.d.ts +43 -0
- package/dist/core/memory-engine/layout.js +59 -0
- package/dist/core/memory-engine/memory-backend-contract.js +120 -0
- package/dist/core/memory-engine/origin-clearance.d.ts +19 -0
- package/dist/core/memory-engine/origin-clearance.js +10 -0
- package/dist/core/memory-engine/provenance-wording.d.ts +15 -1
- package/dist/core/memory-engine/provenance-wording.js +1 -0
- package/dist/core/memory-engine/tools.js +6 -4
- package/dist/core/memory-engine/types.d.ts +13 -1
- package/dist/core/runner/prepare-task.js +22 -8
- package/dist/core/runner/runtask.d.ts +26 -1
- package/dist/core/runner/runtask.js +18 -2
- package/dist/core/strategy-store.d.ts +180 -3
- package/dist/core/strategy-store.js +172 -23
- package/dist/core/task-registry-agent.js +6 -0
- package/dist/core/types.d.ts +24 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/orchestration/run-workflow-tool.d.ts +12 -0
- package/dist/orchestration/run-workflow-tool.js +1 -1
- package/dist/orchestration/workflow-governance.d.ts +27 -0
- package/dist/orchestration/workflow-governance.js +13 -0
- package/dist/orchestration/workflow-primitives.d.ts +8 -1
- package/dist/orchestration/workflow-primitives.js +11 -3
- package/dist/stores/file/file-snapshot-store.js +7 -1
- package/dist/stores/file/index.d.ts +8 -0
- package/dist/stores/file/index.js +12 -0
- package/dist/stores/file/session-policy-store.d.ts +0 -13
- package/dist/stores/file/session-policy-store.js +7 -1
- package/dist/stores/file/session-store.d.ts +4 -1
- package/dist/stores/file/session-store.js +7 -1
- package/dist/stores/file/strategy-store.d.ts +97 -0
- package/dist/stores/file/strategy-store.js +340 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,117 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.50.0 — 2026-08-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`RunnerDeps.mcpRevocations`** (design/338, the #322 batch-3 hook — mid-turn MCP revocation):
|
|
7
|
+
the host's revocation ledger, probed synchronously at every MCP dispatch (tool call + the three
|
|
8
|
+
resource tools) BEFORE the transport. A revoked server's call settles as the coded refusal
|
|
9
|
+
`mcp.server_revoked` with known-not-executed wording; the ledger is live (revoke/un-revoke take
|
|
10
|
+
effect on the very next call, no engine-side caching); a throwing probe fails OPEN with a
|
|
11
|
+
once-per-run `mcp.revocation_probe_failed` notice. Absent seat = pre-338 semantics.
|
|
12
|
+
- **Machine-readable refusal codes on the wire**: `delegation.concurrency_cap`,
|
|
13
|
+
`delegation.session_cap` and `mcp.server_revoked` now carry a `details.code` twin beside
|
|
14
|
+
`details.error` (same value; the wire errorCode lift reads `code`) — tool_end frames for these
|
|
15
|
+
refusals become classifiable. `HarvestRejectionCode` gains `"deferred"` (additive).
|
|
16
|
+
- Conformance clauses **31 → 34**: the hold×applyPatches protocol (hold-unaware plain write /
|
|
17
|
+
release-replay CAS conflict carrying currentRev / snapshot faces tracking the post-UPDATE tuple)
|
|
18
|
+
and the whitewash-precedence UPDATE spelling. `MemorySessionHandle` gains a keyset gate
|
|
19
|
+
(machine face for consumer obligations).
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **Workflow mount batch** (merged-scan FAM-3): a generic own-nullish merger
|
|
23
|
+
(`overlayWorktreeBaseline`) replaces the per-key drop list — an overlay's own-undefined keys no
|
|
24
|
+
longer erase base governance (excludeTools/deferTools/toolPolicy/shellGate/…); new keys are safe
|
|
25
|
+
by construction (`checkpointStore: null` is the one registered exception). Host tighten-only
|
|
26
|
+
clamps (`handsReadOnly:true` / `interactiveTools:false`) now ride into workflow children at the
|
|
27
|
+
same injection point as the read-face clamp — a clamped host's workflow child can no longer
|
|
28
|
+
unclamp itself.
|
|
29
|
+
- **Brain effort batch** (FAM-4): the effective-reasoning verdict is minted at the single point
|
|
30
|
+
that writes the wire and echoed back (three single-sources: the carried predicate, the wire
|
|
31
|
+
value mint, the declared-levels shape gate) — a garbled tier (`thinking:"hgih"`) now reports the
|
|
32
|
+
same fact the wire shows instead of a clamped-to-minimal or echo-garbage lie; the anthropic
|
|
33
|
+
budget arm reports its cap-wins drop; truthy non-array effortLevels no longer throw at request
|
|
34
|
+
construction. Per-call headers are case-fold deduplicated (`X-Tenant` + `x-tenant` no longer
|
|
35
|
+
ride the wire as two entries; auth carriers exempt as pinned).
|
|
36
|
+
- **Memory projection write-back debt** (#366 root fix): a committed entry whose id write-back to
|
|
37
|
+
the projection file failed is now a durable DEBT row (`projection-debts.json`, write-ahead
|
|
38
|
+
staged across all four mint lanes) — the next harvest validates and re-binds the id-less file to
|
|
39
|
+
its committed identity instead of re-adopting it as a duplicate; stale rows drop, unvalidatable
|
|
40
|
+
rows defer fail-closed. The old silent swallow is loud in both directions.
|
|
41
|
+
- The durable reap drops the stale terminal in-process handle after a winning row delete (#361) —
|
|
42
|
+
inside the reap-to-restart window a same-process SendMessage answered the pre-deletion "not
|
|
43
|
+
retained" text while a fresh process answered the honest no-transcript form.
|
|
44
|
+
- Delegation entry ledgers get a lifecycle (reap anchor + amortized sweep) — one Map entry per
|
|
45
|
+
(scope, rootSessionId) no longer accumulates for the life of the process.
|
|
46
|
+
|
|
47
|
+
### Notes
|
|
48
|
+
- Conformance 31→34 is a tightening for backend implementers (a hold-unaware backend fails the
|
|
49
|
+
new cases; upgrade-order duty as before). Everything else is additive or a narrowing inside
|
|
50
|
+
existing coded families; no BREAKING changes.
|
|
51
|
+
|
|
52
|
+
## 5.49.0 — 2026-08-20
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
- **`Runner.swapModels({models, tiers?})`** — the model-catalog hot-generation seat: switching
|
|
56
|
+
models is now a zero-restart operation. Atomic (the candidate is tier-expanded and validated
|
|
57
|
+
first; a failed swap leaves the current generation byte-identical); in-flight tasks finish on
|
|
58
|
+
their prepare-resolved models (natural snapshot — documented divergence window = exactly the
|
|
59
|
+
in-flight lifetime); `tiers` omitted keeps current bindings re-applied over the new models.
|
|
60
|
+
Announced via `config.models_swapped` (key counts only). Consumers: a deployment that deferred
|
|
61
|
+
model-catalog changes to a process restart can retire that arm.
|
|
62
|
+
- design/337 (escalation/advisor mode) — the strategy repository becomes real cross-session
|
|
63
|
+
infrastructure:
|
|
64
|
+
- **`FileStrategyStore`** (persistence twin; per-strategy files under hashed scope dirs, atomic
|
|
65
|
+
writes, corrupt-entry quarantine, read-side hardening incl. symlink refusal and byte-bounded
|
|
66
|
+
reads) aggregated as `FileStorageBackend.strategyStore`.
|
|
67
|
+
- **CJK retrieval fixed**: term extraction shares the memory tokenizer (alnum runs + CJK
|
|
68
|
+
bigrams/unigrams) — a pure-CJK strategy was stored but never retrievable; old hits preserved
|
|
69
|
+
via a legacy acceptance arm.
|
|
70
|
+
- `TeacherRunResult.strategiesInjected` (id-level injection audit); store faults contained
|
|
71
|
+
(an optimization's failure no longer kills the run; disclosed via
|
|
72
|
+
`TeacherConfig.onStrategyStoreIncident`); injected strategies ride inside the untrusted
|
|
73
|
+
fence; write-side validation doors (`strategy.entry_invalid`); `seedStrategies` two-phase
|
|
74
|
+
batch install. Export surface +7, additive.
|
|
75
|
+
- Conformance clause 31: an AMBIGUOUS origin representation (disagreeing carriers) is refused on
|
|
76
|
+
add and update by every backend; agreeing multi-carrier shapes stay legal (the refusal keys on
|
|
77
|
+
disagreement — serialization closure). The bundled File backend already refused; the clause is
|
|
78
|
+
the machine face proving all backends answer alike, and it caught the reference template
|
|
79
|
+
lacking the refusal the moment it landed (template fixed in the same commit).
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
- A 13-finding memory-engine hardening batch (post-release scan of the 5.47 window; all
|
|
83
|
+
externally disclosed):
|
|
84
|
+
- **Dual-root deployments regain the hold-notice family**: `mergeHarvestReports` now merges
|
|
85
|
+
`HarvestReport.containment` (booleans OR, lists concatenated) — under a two-plane scope
|
|
86
|
+
contract the entire `memory.hold_*` family and the index-rollback announce arm were
|
|
87
|
+
structurally unreachable.
|
|
88
|
+
- Hold lifecycle: a refused instruction intake finalizes its row (no resurrection by
|
|
89
|
+
reconciliation); a failed expired-hold release announces once and consumes its verdict
|
|
90
|
+
(generation-keyed — no infinite re-fire); disposal reporting rides the one signal path.
|
|
91
|
+
- Origin clearance: a foreign deletion is no longer read as "our tombstone committed"
|
|
92
|
+
(`tombstonedAt` sidecar; bare absence refuses with custody retained); a concurrent on-disk
|
|
93
|
+
edit is not overwritten by the clearance re-record (divergence guard, byte-equality
|
|
94
|
+
exemption for pristine projections); a challenged entry cannot be washed through the resume
|
|
95
|
+
path.
|
|
96
|
+
- Read side: the recall-taint judge accepts the Read tool's `path` argument spelling; the
|
|
97
|
+
marked-projection index drop judges path tokens per occurrence (an innocent clean entry
|
|
98
|
+
whose name merely contains a marked path no longer over-drops); a read-only plane under
|
|
99
|
+
carry injects the treated index text; index-drop rewrites are announced when they land.
|
|
100
|
+
- The opaque handle/search rows no longer carry a model-authored entry id verbatim (digest
|
|
101
|
+
re-mint on marked commits; the passive-face zero-model-text contract now holds).
|
|
102
|
+
- `memory_search`'s header states band order truthfully on mixed-band pages (single-band and
|
|
103
|
+
off pages byte-identical).
|
|
104
|
+
- The transcript-integrity notice keys its once-latch by (scope, handle) and carries `scope` in
|
|
105
|
+
the detail — multi-tenant attribution without reverse lookup.
|
|
106
|
+
|
|
107
|
+
### Notes
|
|
108
|
+
- Behavior narrowings (all refusals in existing coded families, disclosed): clearEntryOrigin
|
|
109
|
+
gains three refusals (divergence / bare-absence / challenged-resume); marked adds do not adopt
|
|
110
|
+
model-supplied ids; a failed expired-hold release stops re-firing after one loud announcement;
|
|
111
|
+
refused intakes finalize. Two optional sidecar row members (`OriginClearanceRow.tombstonedAt`,
|
|
112
|
+
`HoldRow.resolvedAt`) — strict coercers accept old rows. No BREAKING changes; no export
|
|
113
|
+
removals.
|
|
114
|
+
|
|
3
115
|
## 5.48.0 — 2026-08-20
|
|
4
116
|
|
|
5
117
|
### Added
|
|
@@ -31,7 +31,7 @@ export interface AgentTranscriptToolOptions {
|
|
|
31
31
|
/** Subagent transcript persistence — the declared-durability integrity-gap disclosure sink (see
|
|
32
32
|
* the SendMessage twin): a durable row with a bound sessionId whose session store attests
|
|
33
33
|
* not_found. Announced once per (handle, process); absent ⇒ the honest per-call degrade alone. */
|
|
34
|
-
onTranscriptIntegrityGap?: (handle: string) => void;
|
|
34
|
+
onTranscriptIntegrityGap?: (handle: string, scope: string | undefined) => void;
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
37
|
* Residual observability (lane C) — build the MODEL-VISIBLE **AgentTranscript** tool: read the last N tool steps of a
|
|
@@ -64,7 +64,7 @@ export function createAgentTranscriptTool(opts) {
|
|
|
64
64
|
}
|
|
65
65
|
catch (e) {
|
|
66
66
|
if (e?.code === "not_found") {
|
|
67
|
-
announceTranscriptIntegrityGapOnce(id, opts.onTranscriptIntegrityGap);
|
|
67
|
+
announceTranscriptIntegrityGapOnce(id, access.scope, opts.onTranscriptIntegrityGap);
|
|
68
68
|
}
|
|
69
69
|
return undefined;
|
|
70
70
|
}
|
|
@@ -110,7 +110,10 @@ export class FileRosterStore {
|
|
|
110
110
|
if (text.trim() === "")
|
|
111
111
|
return [];
|
|
112
112
|
try {
|
|
113
|
-
const
|
|
113
|
+
const rawU = JSON.parse(text);
|
|
114
|
+
if (typeof rawU !== "object" || rawU === null)
|
|
115
|
+
throw new Error("roster file: not a JSON object");
|
|
116
|
+
const raw = rawU;
|
|
114
117
|
if (!Array.isArray(raw.entries))
|
|
115
118
|
throw new Error("roster file: entries is not an array");
|
|
116
119
|
const kept = raw.entries.filter((e) => typeof e === "object" && e !== null && typeof e.name === "string" && typeof e.agentId === "string" && typeof e.createdAt === "number");
|
|
@@ -128,12 +128,12 @@ export interface SendMessageToolOptions {
|
|
|
128
128
|
uplinkRecipient?: PeerSelfRef;
|
|
129
129
|
/**
|
|
130
130
|
* Subagent transcript persistence — the "declared durability contradicted by reality" disclosure
|
|
131
|
-
* sink: called (at most once per handle per process, {@link announceTranscriptIntegrityGapOnce})
|
|
131
|
+
* sink: called (at most once per (scope, handle) per process, {@link announceTranscriptIntegrityGapOnce})
|
|
132
132
|
* when a durable row with a bound sessionId meets a session store that attests `not_found`. The
|
|
133
133
|
* first-party mount wires an EngineNotice (`delegation.transcript_integrity`); absent ⇒ the
|
|
134
134
|
* per-call honest refusal is the only loudness (unchanged behavior).
|
|
135
135
|
*/
|
|
136
|
-
onTranscriptIntegrityGap?: (handle: string) => void;
|
|
136
|
+
onTranscriptIntegrityGap?: (handle: string, scope: string | undefined) => void;
|
|
137
137
|
}
|
|
138
138
|
/** RB-382 — max chars of the `summary` arg (the short recap label, distinct from `message`'s much
|
|
139
139
|
* larger {@link UPLINK_RESULT_MAX}) before ITS OWN truncation. Exported for direct unit-testing only
|
|
@@ -292,7 +292,7 @@ export function createSendMessageTool(opts) {
|
|
|
292
292
|
catch (e) {
|
|
293
293
|
await rollbackDrain();
|
|
294
294
|
if (e?.code === "not_found") {
|
|
295
|
-
announceTranscriptIntegrityGapOnce(handle, opts.onTranscriptIntegrityGap);
|
|
295
|
+
announceTranscriptIntegrityGapOnce(handle, scope, opts.onTranscriptIntegrityGap);
|
|
296
296
|
return queueFullReceipt(`No transcript found for agent ${handle}, so a revival cannot drain the backlog.`);
|
|
297
297
|
}
|
|
298
298
|
return queueFullReceipt(`Its transcript store did not answer (${e instanceof Error ? e.message : String(e)}) — this send did not start a backlog drain; a later resend will try again.`);
|
|
@@ -391,7 +391,7 @@ export function createSendMessageTool(opts) {
|
|
|
391
391
|
catch (e) {
|
|
392
392
|
await rollback();
|
|
393
393
|
if (e?.code === "not_found") {
|
|
394
|
-
announceTranscriptIntegrityGapOnce(handle, opts.onTranscriptIntegrityGap);
|
|
394
|
+
announceTranscriptIntegrityGapOnce(handle, scope, opts.onTranscriptIntegrityGap);
|
|
395
395
|
return {
|
|
396
396
|
content: `Message not sent: no transcript found for agent ${handle} — it cannot be revived. Launch a new agent with the needed context instead.`,
|
|
397
397
|
details: { error: "resume.session_not_found", to },
|
|
@@ -484,6 +484,12 @@ export declare function resolveDelegationEntryCaps(caps: {
|
|
|
484
484
|
maxConcurrent?: number;
|
|
485
485
|
maxCumulativePerSession?: number;
|
|
486
486
|
} | undefined): ResolvedDelegationEntryCaps;
|
|
487
|
+
/** @internal — observability seam for the lifecycle pins (tests only; never a public surface):
|
|
488
|
+
* the ledger footprint of one registry. */
|
|
489
|
+
export declare function delegationEntryLedgerFootprint(registry: object): {
|
|
490
|
+
keys: number;
|
|
491
|
+
handles: number;
|
|
492
|
+
};
|
|
487
493
|
/**
|
|
488
494
|
* Options for {@link createSubagentTool}.
|
|
489
495
|
*
|
package/dist/agents/subagent.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { isAbsolute } from "node:path";
|
|
3
3
|
import { withDelegationProvenance } from "../core/tool-policy.js";
|
|
4
|
+
import { errorResult } from "../core/tools.js";
|
|
4
5
|
import { isHighSurrogate, isLowSurrogate } from "../core/surrogate-safe-slice.js";
|
|
5
6
|
import { newDelegationProvenanceAggregate, reduceDelegationAttestation } from "../core/memory-engine/delegation-provenance.js";
|
|
6
7
|
import { registerDelegationLaunch, replayExternalSettlementEffects, settleDelegation } from "../core/memory-engine/delegation-settlement.js";
|
|
@@ -912,6 +913,125 @@ function delegationEntryLedger(registry, key) {
|
|
|
912
913
|
}
|
|
913
914
|
return set;
|
|
914
915
|
}
|
|
916
|
+
const delegationLedgerLifecycles = new WeakMap();
|
|
917
|
+
const delegationLedgerKeyStores = new WeakMap();
|
|
918
|
+
function bindDelegationLedgerKeyStore(registry, key, store) {
|
|
919
|
+
let byKey = delegationLedgerKeyStores.get(registry);
|
|
920
|
+
if (byKey === undefined) {
|
|
921
|
+
byKey = new Map();
|
|
922
|
+
delegationLedgerKeyStores.set(registry, byKey);
|
|
923
|
+
}
|
|
924
|
+
if (byKey.get(key) === null)
|
|
925
|
+
return;
|
|
926
|
+
byKey.set(key, store ?? null);
|
|
927
|
+
}
|
|
928
|
+
async function sweepDelegationEntryLedgerKeys(registry, keys) {
|
|
929
|
+
const byKey = delegationEntryLedgers.get(registry);
|
|
930
|
+
if (byKey === undefined)
|
|
931
|
+
return;
|
|
932
|
+
const keyStores = delegationLedgerKeyStores.get(registry);
|
|
933
|
+
const activeFace = registry.activeDelegationHandles;
|
|
934
|
+
if (typeof activeFace !== "function")
|
|
935
|
+
return;
|
|
936
|
+
for (const key of keys) {
|
|
937
|
+
const set = byKey.get(key);
|
|
938
|
+
if (set === undefined)
|
|
939
|
+
continue;
|
|
940
|
+
const store = keyStores?.get(key);
|
|
941
|
+
if (store === undefined || store === null)
|
|
942
|
+
continue;
|
|
943
|
+
let scopeK;
|
|
944
|
+
let rootK;
|
|
945
|
+
try {
|
|
946
|
+
const parsed = JSON.parse(key);
|
|
947
|
+
if (!Array.isArray(parsed) || typeof parsed[0] !== "string" || typeof parsed[1] !== "string")
|
|
948
|
+
continue;
|
|
949
|
+
scopeK = parsed[0];
|
|
950
|
+
rootK = parsed[1];
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
if (activeFace.call(registry, scopeK, rootK).length > 0)
|
|
956
|
+
continue;
|
|
957
|
+
const sizeBefore = set.size;
|
|
958
|
+
let stored;
|
|
959
|
+
try {
|
|
960
|
+
stored = (await store.listBySession(scopeK, rootK)).map((r) => r.handle);
|
|
961
|
+
}
|
|
962
|
+
catch {
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
if (byKey.get(key) !== set || set.size !== sizeBefore || activeFace.call(registry, scopeK, rootK).length > 0)
|
|
966
|
+
continue;
|
|
967
|
+
if (stored.length === 0) {
|
|
968
|
+
byKey.delete(key);
|
|
969
|
+
keyStores?.delete(key);
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
const keep = new Set(stored);
|
|
973
|
+
for (const h of [...set])
|
|
974
|
+
if (!keep.has(h))
|
|
975
|
+
set.delete(h);
|
|
976
|
+
if (set.size === 0) {
|
|
977
|
+
byKey.delete(key);
|
|
978
|
+
keyStores?.delete(key);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
function armDelegationLedgerLifecycle(registry) {
|
|
983
|
+
let life = delegationLedgerLifecycles.get(registry);
|
|
984
|
+
if (life === undefined) {
|
|
985
|
+
life = { cursor: 0, subscribed: false };
|
|
986
|
+
delegationLedgerLifecycles.set(registry, life);
|
|
987
|
+
}
|
|
988
|
+
if (life.subscribed)
|
|
989
|
+
return;
|
|
990
|
+
const reapFace = registry.onSessionReap;
|
|
991
|
+
if (typeof reapFace !== "function")
|
|
992
|
+
return;
|
|
993
|
+
life.subscribed = true;
|
|
994
|
+
reapFace.call(registry, (sessionId, scope) => {
|
|
995
|
+
const byKey = delegationEntryLedgers.get(registry);
|
|
996
|
+
if (byKey === undefined)
|
|
997
|
+
return;
|
|
998
|
+
const keys = [...byKey.keys()].filter((k) => {
|
|
999
|
+
try {
|
|
1000
|
+
const parsed = JSON.parse(k);
|
|
1001
|
+
if (!Array.isArray(parsed) || typeof parsed[0] !== "string" || typeof parsed[1] !== "string")
|
|
1002
|
+
return false;
|
|
1003
|
+
return parsed[1] === sessionId && (scope === undefined || parsed[0] === scope);
|
|
1004
|
+
}
|
|
1005
|
+
catch {
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
if (keys.length > 0)
|
|
1010
|
+
void sweepDelegationEntryLedgerKeys(registry, keys).catch(() => undefined);
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
function sweepDelegationLedgerRound(registry, n) {
|
|
1014
|
+
const life = delegationLedgerLifecycles.get(registry);
|
|
1015
|
+
const byKey = delegationEntryLedgers.get(registry);
|
|
1016
|
+
if (life === undefined || byKey === undefined || byKey.size === 0)
|
|
1017
|
+
return;
|
|
1018
|
+
const keys = [...byKey.keys()];
|
|
1019
|
+
const start = life.cursor % keys.length;
|
|
1020
|
+
const slice = [];
|
|
1021
|
+
for (let i = 0; i < Math.min(n, keys.length); i++)
|
|
1022
|
+
slice.push(keys[(start + i) % keys.length]);
|
|
1023
|
+
life.cursor = (start + slice.length) % keys.length;
|
|
1024
|
+
void sweepDelegationEntryLedgerKeys(registry, slice).catch(() => undefined);
|
|
1025
|
+
}
|
|
1026
|
+
export function delegationEntryLedgerFootprint(registry) {
|
|
1027
|
+
const byKey = delegationEntryLedgers.get(registry);
|
|
1028
|
+
if (byKey === undefined)
|
|
1029
|
+
return { keys: 0, handles: 0 };
|
|
1030
|
+
let handles = 0;
|
|
1031
|
+
for (const s of byKey.values())
|
|
1032
|
+
handles += s.size;
|
|
1033
|
+
return { keys: byKey.size, handles };
|
|
1034
|
+
}
|
|
915
1035
|
export function normalizeSubagentType(value) {
|
|
916
1036
|
return value.normalize("NFKC").toLowerCase().replace(/[\p{White_Space}\p{Pd}_]+/gu, "");
|
|
917
1037
|
}
|
|
@@ -2491,7 +2611,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2491
2611
|
dropHostAbortListener();
|
|
2492
2612
|
await cancelObserver();
|
|
2493
2613
|
const wt = await finishWorktree();
|
|
2494
|
-
return
|
|
2614
|
+
return errorResult(`Sub-agent not started in background: ${text}${wt ? `\n${wt}` : ""}`, { error: code, code });
|
|
2495
2615
|
};
|
|
2496
2616
|
const activeFace = bg.registry.activeDelegationHandles;
|
|
2497
2617
|
const active = typeof activeFace === "function" ? activeFace.call(bg.registry, capScope, capRoot) : [];
|
|
@@ -2640,6 +2760,11 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2640
2760
|
}
|
|
2641
2761
|
if (capLedgerKey !== undefined && reviveRow === undefined)
|
|
2642
2762
|
delegationEntryLedger(bg.registry, capLedgerKey).add(taskId);
|
|
2763
|
+
if (capLedgerKey !== undefined) {
|
|
2764
|
+
bindDelegationLedgerKeyStore(bg.registry, capLedgerKey, bg.agentStore);
|
|
2765
|
+
armDelegationLedgerLifecycle(bg.registry);
|
|
2766
|
+
sweepDelegationLedgerRound(bg.registry, 3);
|
|
2767
|
+
}
|
|
2643
2768
|
childInternals.peerSelfRef?.addAxis("h", taskId);
|
|
2644
2769
|
if (agentName !== undefined) {
|
|
2645
2770
|
recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}), ...(reviveRow !== undefined ? ((reviveRow.rootSessionId ?? reviveRow.parentSessionId) !== undefined ? { rootSessionId: reviveRow.rootSessionId ?? reviveRow.parentSessionId } : {}) : (ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: reviveRow?.spawnedAt ?? Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
|
package/dist/agents/teacher.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Runner } from "../core/runner/runtask.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type StrategyOrigin, type StrategyStore, type StrategyStoreIncident } from "../core/strategy-store.js";
|
|
3
3
|
import type { ModelRef, TaskResult, TaskSpec } from "../core/types.js";
|
|
4
4
|
/**
|
|
5
5
|
* Teacher mode (escalation cascade): a cheap "student" does the work; a strong "teacher" is consulted
|
|
@@ -85,6 +85,17 @@ export interface TeacherConfig {
|
|
|
85
85
|
minConfidenceToStore?: number;
|
|
86
86
|
/** Inject retrieved strategies into the objective. Default true when a store+scope are set. */
|
|
87
87
|
injectStrategies?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Disclosure sink for a strategy-store failure this loop CONTAINED (store-fault containment): a
|
|
90
|
+
* `find` that failed injects nothing, a `save` that failed stores nothing — neither ever fails the
|
|
91
|
+
* task (the repository is an optimization; an optimization's fault must not kill the work it was
|
|
92
|
+
* optimizing) — but each is disclosed here so a broken store is discoverable. The one exception
|
|
93
|
+
* stays loud: an error carrying a `config.`-prefixed code (an illegal `retrieveK` etc.) is a
|
|
94
|
+
* PROGRAMMER error and rethrows — containing it would silence the validation door entirely.
|
|
95
|
+
* Callback faults are themselves contained (safe-notify posture); absent ⇒ `console.warn`, once
|
|
96
|
+
* per op kind.
|
|
97
|
+
*/
|
|
98
|
+
onStrategyStoreIncident?: (i: StrategyStoreIncident) => void;
|
|
88
99
|
}
|
|
89
100
|
export type EscalationTrigger = "stuck" | "blocked" | "failed" | "verify-fail";
|
|
90
101
|
export interface TeacherAdvice {
|
|
@@ -105,6 +116,19 @@ export interface EscalationRecord {
|
|
|
105
116
|
export interface TeacherRunResult extends TaskResult {
|
|
106
117
|
/** Escalations that occurred during this run (empty if the student succeeded alone). */
|
|
107
118
|
escalations: EscalationRecord[];
|
|
119
|
+
/**
|
|
120
|
+
* Audit trail of the strategies retrieved into the student's objective for THIS run — id-level (the
|
|
121
|
+
* text is not copied; the id is the join key into the store), with each entry's stored timestamp and
|
|
122
|
+
* provenance stamp. Lets a host connect "what was injected" to "how the run ended" offline, which is
|
|
123
|
+
* the only way retrieval-induced degradation is ever measurable. PRESENT iff ≥1 strategy was
|
|
124
|
+
* injected; ABSENT means no store was configured, injection was disabled, retrieval matched nothing,
|
|
125
|
+
* or the find failed (a failure additionally raises `onStrategyStoreIncident`).
|
|
126
|
+
*/
|
|
127
|
+
strategiesInjected?: Array<{
|
|
128
|
+
id: string;
|
|
129
|
+
ts: string;
|
|
130
|
+
origin?: StrategyOrigin;
|
|
131
|
+
}>;
|
|
108
132
|
/** Usage spent on the teacher (advisor), separate from the student's `stats` (which includes the
|
|
109
133
|
* cheap helper monitor/verifier calls). `tasks` = teacher runs (escalation asks + any takeover).
|
|
110
134
|
* `humanReview` = the HITL-gate burden of the teacher's runs (notably a takeover that hit an approval
|
package/dist/agents/teacher.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { MAX_STRATEGY_INJECTION_TOTAL_BYTES, MAX_STRATEGY_PROBLEM_BYTES, MAX_STRATEGY_TEXT_BYTES } from "../core/strategy-store.js";
|
|
3
|
+
import { createSafeNotifier, observeThenableRejection } from "../core/safe-notify.js";
|
|
2
4
|
import { mapNestedSuspend, isDurablePause } from "./suspend-guard.js";
|
|
3
5
|
import { isDefineToolProduct, stampDefineToolBrand } from "../core/tools.js";
|
|
4
6
|
import { delimitUntrusted, inlineUntrusted } from "../core/untrusted-text.js";
|
|
@@ -29,7 +31,10 @@ export function parseTeacherAdvice(text) {
|
|
|
29
31
|
const end = text.lastIndexOf("}");
|
|
30
32
|
if (start !== -1 && end > start) {
|
|
31
33
|
try {
|
|
32
|
-
const
|
|
34
|
+
const oU = JSON.parse(text.slice(start, end + 1));
|
|
35
|
+
if (typeof oU !== "object" || oU === null)
|
|
36
|
+
throw new Error("not an object");
|
|
37
|
+
const o = oU;
|
|
33
38
|
return {
|
|
34
39
|
strategy: typeof o.strategy === "string" ? o.strategy : undefined,
|
|
35
40
|
correction: typeof o.correction === "string" ? o.correction : undefined,
|
|
@@ -46,31 +51,102 @@ export function parseTeacherAdvice(text) {
|
|
|
46
51
|
export async function runWithTeacher(runner, studentSpec, teacher = {}) {
|
|
47
52
|
const store = teacher.strategyStore;
|
|
48
53
|
const scope = teacher.scope;
|
|
54
|
+
const incidentNotifier = createSafeNotifier();
|
|
55
|
+
const warnedIncidentOps = new Set();
|
|
56
|
+
const discloseStoreIncident = (i) => {
|
|
57
|
+
const seat = teacher.onStrategyStoreIncident;
|
|
58
|
+
if (seat) {
|
|
59
|
+
const site = `teacher.onStrategyStoreIncident.${i.op}`;
|
|
60
|
+
incidentNotifier.notify(() => observeThenableRejection(seat(i), incidentNotifier, site), site);
|
|
61
|
+
}
|
|
62
|
+
else if (!warnedIncidentOps.has(i.op)) {
|
|
63
|
+
warnedIncidentOps.add(i.op);
|
|
64
|
+
console.warn(`runWithTeacher: strategy store ${i.op} failed (contained — the run continues): ${i.error}`);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const isConfigCoded = (e) => typeof e?.code === "string" && (e.code).startsWith("config.");
|
|
49
68
|
let spec = studentSpec;
|
|
69
|
+
let injected;
|
|
50
70
|
if (store && scope && teacher.injectStrategies !== false) {
|
|
51
|
-
|
|
71
|
+
let found = [];
|
|
72
|
+
try {
|
|
73
|
+
const raw = await store.find(scope, studentSpec.objective, teacher.retrieveK ?? 3);
|
|
74
|
+
if (!Array.isArray(raw)) {
|
|
75
|
+
discloseStoreIncident({ op: "find", error: "store.find returned a non-array result — injecting nothing" });
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
let dropped = 0;
|
|
79
|
+
for (const e of raw) {
|
|
80
|
+
const c = e;
|
|
81
|
+
const id = c?.id;
|
|
82
|
+
const ts = c?.ts;
|
|
83
|
+
const strategy = c?.strategy;
|
|
84
|
+
if (typeof id !== "string" || typeof ts !== "string" || typeof strategy !== "string") {
|
|
85
|
+
dropped++;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const origin = c?.origin;
|
|
89
|
+
found.push({ id, ts, strategy, ...(origin === "earned" || origin === "seeded" ? { origin } : {}) });
|
|
90
|
+
}
|
|
91
|
+
if (dropped > 0) {
|
|
92
|
+
discloseStoreIncident({ op: "find", error: `${dropped} malformed retrieval entries dropped (id/ts/strategy must be strings)` });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
if (isConfigCoded(e))
|
|
98
|
+
throw e;
|
|
99
|
+
discloseStoreIncident({ op: "find", error: String(e?.message ?? e) });
|
|
100
|
+
found = [];
|
|
101
|
+
}
|
|
102
|
+
let budget = 0;
|
|
103
|
+
for (let i = 0; i < found.length; i++) {
|
|
104
|
+
budget += Buffer.byteLength(found[i].strategy, "utf8");
|
|
105
|
+
if (budget > MAX_STRATEGY_INJECTION_TOTAL_BYTES) {
|
|
106
|
+
discloseStoreIncident({
|
|
107
|
+
op: "find",
|
|
108
|
+
error: `injection truncated to ${i} of ${found.length} retrieved strategies — combined text exceeded ${MAX_STRATEGY_INJECTION_TOTAL_BYTES} bytes`,
|
|
109
|
+
});
|
|
110
|
+
found = found.slice(0, i);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
52
114
|
if (found.length > 0) {
|
|
53
115
|
const block = `[STRATEGIES from PAST, DIFFERENT tasks — they may NOT apply. If unsure, IGNORE them and proceed normally.]\n` +
|
|
54
|
-
found.map((s) => `- ${s.strategy}`).join("\n");
|
|
116
|
+
delimitUntrusted("PAST STRATEGIES (from DIFFERENT tasks; may NOT apply — verify before use)", found.map((s) => `- ${s.strategy}`).join("\n"));
|
|
55
117
|
spec = { ...studentSpec, objective: `${block}\n\n${studentSpec.objective}` };
|
|
118
|
+
injected = found.map((s) => ({ id: s.id, ts: s.ts, ...(s.origin !== undefined ? { origin: s.origin } : {}) }));
|
|
56
119
|
}
|
|
57
120
|
}
|
|
58
121
|
const result = await runTeacherCore(runner, spec, teacher);
|
|
59
122
|
if (store && scope && result.status === "completed" && result.escalations.length > 0) {
|
|
60
123
|
const advice = result.escalations[result.escalations.length - 1].teacher;
|
|
61
124
|
if (advice.strategy && advice.confidence >= (teacher.minConfidenceToStore ?? 2)) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
125
|
+
try {
|
|
126
|
+
const clip = (text, maxBytes) => {
|
|
127
|
+
let out = text;
|
|
128
|
+
while (Buffer.byteLength(out) > maxBytes)
|
|
129
|
+
out = out.slice(0, -1);
|
|
130
|
+
return out;
|
|
131
|
+
};
|
|
132
|
+
await store.save({
|
|
133
|
+
id: randomUUID(),
|
|
134
|
+
problem: clip(studentSpec.objective, MAX_STRATEGY_PROBLEM_BYTES),
|
|
135
|
+
strategy: clip(advice.strategy, MAX_STRATEGY_TEXT_BYTES),
|
|
136
|
+
confidence: Math.min(3, Math.max(0, advice.confidence)),
|
|
137
|
+
scope,
|
|
138
|
+
ts: new Date().toISOString(),
|
|
139
|
+
teacherModel: typeof teacher.model === "string" ? teacher.model : teacher.model?.id,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
if (isConfigCoded(e))
|
|
144
|
+
throw e;
|
|
145
|
+
discloseStoreIncident({ op: "save", error: String(e?.message ?? e) });
|
|
146
|
+
}
|
|
71
147
|
}
|
|
72
148
|
}
|
|
73
|
-
return result;
|
|
149
|
+
return injected !== undefined && injected.length > 0 ? { ...result, strategiesInjected: injected } : result;
|
|
74
150
|
}
|
|
75
151
|
async function runTeacherCore(runner, studentSpec, teacher) {
|
|
76
152
|
const maxEscalations = teacher.maxEscalations ?? 3;
|
package/dist/brain/anthropic.js
CHANGED
|
@@ -5,10 +5,9 @@ import { createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js"
|
|
|
5
5
|
import { mintFallbackToolCallId } from "./tool-call-id.js";
|
|
6
6
|
import { emitBrainTelemetry } from "./status-sink.js";
|
|
7
7
|
import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, modelSupportsVision, sendableImages } from "./media-degrade.js";
|
|
8
|
-
import { ANTHROPIC_RESERVED, OUTPUT_CAP_KEYS, applyExtraBody, effectiveOutputCap, stripAuthHeaders } from "./request-params.js";
|
|
9
|
-
import {
|
|
8
|
+
import { ANTHROPIC_RESERVED, OUTPUT_CAP_KEYS, applyExtraBody, effectiveOutputCap, lockHeader, mergeHeaders, stripAuthHeaders, takeHeaderCasefold } from "./request-params.js";
|
|
9
|
+
import { MIN_THINKING_TOKENS, budgetCapSkipsThinking, declaredEffortLevels, reasoningBudgetShare, reasoningRequestCarried, resolveEffort } from "./reasoning.js";
|
|
10
10
|
import { runStreamingBrain } from "./stream-engine.js";
|
|
11
|
-
const MIN_THINKING_TOKENS = 1024;
|
|
12
11
|
function thinkingBudget(maxTokens, share, fixed, hardCap = false) {
|
|
13
12
|
const max = hardCap ? maxTokens : Math.max(maxTokens, MIN_THINKING_TOKENS * 2);
|
|
14
13
|
const want = fixed ?? Math.floor(max * share);
|
|
@@ -261,13 +260,13 @@ export function createAnthropicBrain(config = {}) {
|
|
|
261
260
|
if (options?.temperature !== undefined && anthCompat.supportsTemperature !== false) {
|
|
262
261
|
body.temperature = options.temperature;
|
|
263
262
|
}
|
|
264
|
-
if (model
|
|
263
|
+
if (reasoningRequestCarried(model, options?.reasoning)) {
|
|
265
264
|
if (anthCompat.thinkingMode === "adaptive") {
|
|
266
265
|
body.thinking = { type: "adaptive" };
|
|
267
266
|
}
|
|
268
267
|
else {
|
|
269
268
|
const hardCap = overrides?.maxOutputTokens !== undefined || options?.maxTokens !== undefined;
|
|
270
|
-
if (
|
|
269
|
+
if (budgetCapSkipsThinking(body.max_tokens, hardCap)) {
|
|
271
270
|
body.thinking = undefined;
|
|
272
271
|
delete body.thinking;
|
|
273
272
|
}
|
|
@@ -283,13 +282,9 @@ export function createAnthropicBrain(config = {}) {
|
|
|
283
282
|
}
|
|
284
283
|
const betas = [];
|
|
285
284
|
let sendEffortBeta = false;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
options?.reasoning &&
|
|
290
|
-
options.reasoning !== "off" &&
|
|
291
|
-
isThinkingLevel(options.reasoning)) {
|
|
292
|
-
const { effective } = resolveEffort(options.reasoning, anthCompat.effortLevels.filter(isThinkingLevel));
|
|
285
|
+
const declaredEffort = declaredEffortLevels(anthCompat.effortLevels);
|
|
286
|
+
if (declaredEffort !== undefined && reasoningRequestCarried(model, options?.reasoning)) {
|
|
287
|
+
const { effective } = resolveEffort(options.reasoning, declaredEffort);
|
|
293
288
|
const wireEffort = effective === "minimal" ? "low" : effective;
|
|
294
289
|
body.output_config = { effort: wireEffort };
|
|
295
290
|
sendEffortBeta = true;
|
|
@@ -303,23 +298,19 @@ export function createAnthropicBrain(config = {}) {
|
|
|
303
298
|
if (anthCompat.interleavedThinking) {
|
|
304
299
|
betas.push("interleaved-thinking-2025-05-14");
|
|
305
300
|
}
|
|
306
|
-
const headers =
|
|
307
|
-
...model.headers,
|
|
308
|
-
...config.headers,
|
|
309
|
-
...options?.headers,
|
|
310
|
-
};
|
|
301
|
+
const headers = mergeHeaders(model.headers, config.headers, options?.headers);
|
|
311
302
|
if (options?.apiKey !== undefined)
|
|
312
303
|
stripAuthHeaders(headers);
|
|
313
304
|
if (betas.length > 0) {
|
|
314
|
-
const existing = (headers
|
|
305
|
+
const existing = (takeHeaderCasefold(headers, "anthropic-beta") ?? "").split(",").map((b) => b.trim()).filter(Boolean);
|
|
315
306
|
for (const beta of betas) {
|
|
316
307
|
if (!existing.includes(beta))
|
|
317
308
|
existing.push(beta);
|
|
318
309
|
}
|
|
319
310
|
headers["anthropic-beta"] = existing.join(",");
|
|
320
311
|
}
|
|
321
|
-
headers
|
|
322
|
-
headers
|
|
312
|
+
lockHeader(headers, "content-type", "application/json");
|
|
313
|
+
lockHeader(headers, "anthropic-version", config.version ?? "2023-06-01");
|
|
323
314
|
if (apiKey)
|
|
324
315
|
headers["x-api-key"] = apiKey;
|
|
325
316
|
const builtThinkingRequested = body.thinking !== undefined;
|