@sema-agent/core 5.47.0 → 5.49.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 +115 -0
- package/dist/agents/agent-transcript-tool.d.ts +4 -0
- package/dist/agents/agent-transcript-tool.js +10 -3
- package/dist/agents/send-message-tool.d.ts +43 -1
- package/dist/agents/send-message-tool.js +50 -11
- package/dist/agents/subagent.d.ts +18 -0
- package/dist/agents/subagent.js +102 -2
- package/dist/agents/teacher.d.ts +25 -1
- package/dist/agents/teacher.js +85 -12
- package/dist/config/defaults.d.ts +20 -0
- package/dist/config/defaults.js +5 -0
- package/dist/core/background-agent-store.d.ts +1 -0
- package/dist/core/background-agent-store.js +14 -0
- package/dist/core/mcp.d.ts +6 -1
- package/dist/core/mcp.js +34 -7
- 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 +6 -1
- package/dist/core/memory-engine/engine.js +136 -21
- package/dist/core/memory-engine/memory-backend-contract.js +33 -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/reminder-disclosure.d.ts +90 -0
- package/dist/core/reminder-disclosure.js +64 -0
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +6 -0
- package/dist/core/runner/prepare-acquire-reconcile.js +1 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
- package/dist/core/runner/prepare-hands-readface.js +1 -0
- package/dist/core/runner/prepare-task.d.ts +15 -0
- package/dist/core/runner/prepare-task.js +51 -33
- package/dist/core/runner/runtask.d.ts +26 -1
- package/dist/core/runner/runtask.js +21 -3
- package/dist/core/session-store.d.ts +59 -1
- package/dist/core/session-store.js +82 -14
- package/dist/core/session.d.ts +83 -1
- package/dist/core/strategy-store.d.ts +180 -3
- package/dist/core/strategy-store.js +172 -23
- package/dist/core/task-registry-agent.d.ts +28 -0
- package/dist/core/task-registry-agent.js +63 -2
- package/dist/core/task-registry.d.ts +21 -0
- package/dist/core/task-registry.js +4 -1
- package/dist/core/types.d.ts +66 -0
- package/dist/core/untrusted-text.d.ts +63 -0
- package/dist/core/untrusted-text.js +48 -0
- package/dist/core/wiring-manifest.d.ts +35 -0
- package/dist/core/wiring-manifest.js +21 -1
- package/dist/engine/harness/types.d.ts +36 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.js +6 -5
- package/dist/internal/harness-types.d.ts +1 -0
- package/dist/stores/file/file-snapshot-store.js +7 -1
- package/dist/stores/file/index.d.ts +27 -3
- package/dist/stores/file/index.js +36 -1
- 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 +22 -5
- package/dist/stores/file/session-store.js +80 -13
- package/dist/stores/file/strategy-store.d.ts +97 -0
- package/dist/stores/file/strategy-store.js +340 -0
- package/dist/tools/fs/fs-pdf.d.ts +12 -1
- package/dist/tools/fs/fs-pdf.js +17 -3
- package/dist/tools/fs/fs-read.d.ts +2 -1
- package/dist/tools/fs/fs-read.js +33 -5
- package/dist/tools/fs/fs-shared.d.ts +6 -2
- package/dist/tools/fs/index.d.ts +7 -0
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/web.js +21 -2
- package/package.json +3 -2
- package/test/export-surface.snapshot.json +22 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,120 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.49.0 — 2026-08-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`Runner.swapModels({models, tiers?})`** — the model-catalog hot-generation seat: switching
|
|
7
|
+
models is now a zero-restart operation. Atomic (the candidate is tier-expanded and validated
|
|
8
|
+
first; a failed swap leaves the current generation byte-identical); in-flight tasks finish on
|
|
9
|
+
their prepare-resolved models (natural snapshot — documented divergence window = exactly the
|
|
10
|
+
in-flight lifetime); `tiers` omitted keeps current bindings re-applied over the new models.
|
|
11
|
+
Announced via `config.models_swapped` (key counts only). Consumers: a deployment that deferred
|
|
12
|
+
model-catalog changes to a process restart can retire that arm.
|
|
13
|
+
- design/337 (escalation/advisor mode) — the strategy repository becomes real cross-session
|
|
14
|
+
infrastructure:
|
|
15
|
+
- **`FileStrategyStore`** (persistence twin; per-strategy files under hashed scope dirs, atomic
|
|
16
|
+
writes, corrupt-entry quarantine, read-side hardening incl. symlink refusal and byte-bounded
|
|
17
|
+
reads) aggregated as `FileStorageBackend.strategyStore`.
|
|
18
|
+
- **CJK retrieval fixed**: term extraction shares the memory tokenizer (alnum runs + CJK
|
|
19
|
+
bigrams/unigrams) — a pure-CJK strategy was stored but never retrievable; old hits preserved
|
|
20
|
+
via a legacy acceptance arm.
|
|
21
|
+
- `TeacherRunResult.strategiesInjected` (id-level injection audit); store faults contained
|
|
22
|
+
(an optimization's failure no longer kills the run; disclosed via
|
|
23
|
+
`TeacherConfig.onStrategyStoreIncident`); injected strategies ride inside the untrusted
|
|
24
|
+
fence; write-side validation doors (`strategy.entry_invalid`); `seedStrategies` two-phase
|
|
25
|
+
batch install. Export surface +7, additive.
|
|
26
|
+
- Conformance clause 31: an AMBIGUOUS origin representation (disagreeing carriers) is refused on
|
|
27
|
+
add and update by every backend; agreeing multi-carrier shapes stay legal (the refusal keys on
|
|
28
|
+
disagreement — serialization closure). The bundled File backend already refused; the clause is
|
|
29
|
+
the machine face proving all backends answer alike, and it caught the reference template
|
|
30
|
+
lacking the refusal the moment it landed (template fixed in the same commit).
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- A 13-finding memory-engine hardening batch (post-release scan of the 5.47 window; all
|
|
34
|
+
externally disclosed):
|
|
35
|
+
- **Dual-root deployments regain the hold-notice family**: `mergeHarvestReports` now merges
|
|
36
|
+
`HarvestReport.containment` (booleans OR, lists concatenated) — under a two-plane scope
|
|
37
|
+
contract the entire `memory.hold_*` family and the index-rollback announce arm were
|
|
38
|
+
structurally unreachable.
|
|
39
|
+
- Hold lifecycle: a refused instruction intake finalizes its row (no resurrection by
|
|
40
|
+
reconciliation); a failed expired-hold release announces once and consumes its verdict
|
|
41
|
+
(generation-keyed — no infinite re-fire); disposal reporting rides the one signal path.
|
|
42
|
+
- Origin clearance: a foreign deletion is no longer read as "our tombstone committed"
|
|
43
|
+
(`tombstonedAt` sidecar; bare absence refuses with custody retained); a concurrent on-disk
|
|
44
|
+
edit is not overwritten by the clearance re-record (divergence guard, byte-equality
|
|
45
|
+
exemption for pristine projections); a challenged entry cannot be washed through the resume
|
|
46
|
+
path.
|
|
47
|
+
- Read side: the recall-taint judge accepts the Read tool's `path` argument spelling; the
|
|
48
|
+
marked-projection index drop judges path tokens per occurrence (an innocent clean entry
|
|
49
|
+
whose name merely contains a marked path no longer over-drops); a read-only plane under
|
|
50
|
+
carry injects the treated index text; index-drop rewrites are announced when they land.
|
|
51
|
+
- The opaque handle/search rows no longer carry a model-authored entry id verbatim (digest
|
|
52
|
+
re-mint on marked commits; the passive-face zero-model-text contract now holds).
|
|
53
|
+
- `memory_search`'s header states band order truthfully on mixed-band pages (single-band and
|
|
54
|
+
off pages byte-identical).
|
|
55
|
+
- The transcript-integrity notice keys its once-latch by (scope, handle) and carries `scope` in
|
|
56
|
+
the detail — multi-tenant attribution without reverse lookup.
|
|
57
|
+
|
|
58
|
+
### Notes
|
|
59
|
+
- Behavior narrowings (all refusals in existing coded families, disclosed): clearEntryOrigin
|
|
60
|
+
gains three refusals (divergence / bare-absence / challenged-resume); marked adds do not adopt
|
|
61
|
+
model-supplied ids; a failed expired-hold release stops re-firing after one loud announcement;
|
|
62
|
+
refused intakes finalize. Two optional sidecar row members (`OriginClearanceRow.tombstonedAt`,
|
|
63
|
+
`HoldRow.resolvedAt`) — strict coercers accept old rows. No BREAKING changes; no export
|
|
64
|
+
removals.
|
|
65
|
+
|
|
66
|
+
## 5.48.0 — 2026-08-20
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
- design/319 tickets B+C+D — the reminder-impersonation defense completes (A shipped 5.46.0):
|
|
70
|
+
- **Detect-and-disclose trailer** (four outlets: plain-text Read, notebook projection,
|
|
71
|
+
extracted PDF text, MCP success results): external bytes containing reminder-shaped text
|
|
72
|
+
gain a trailing disclosure block. Two wordings: bare form (reminder-shaped, markless —
|
|
73
|
+
throttled 60s per file / per server:tool) and marked form (carries the CURRENT session mark,
|
|
74
|
+
definitionally zero legitimate probability — never throttled). Scan point = the final
|
|
75
|
+
model-facing projection at each outlet.
|
|
76
|
+
- **Narrow defuse** (MCP success + web only): a block carrying the exact current mark is
|
|
77
|
+
defused (mark bytes broken) with a disclosure line. Read-family outlets stay byte-verbatim
|
|
78
|
+
(editing-loop preservation; disclosure-only there).
|
|
79
|
+
- Observability: `TaskResult.stats.mechanisms.reminderDisclosures`
|
|
80
|
+
(`Record<"outlet.form", number>`). Gate: `npm run gate:reminder-literal` (AST; mint-home
|
|
81
|
+
exclusivity). Public doc: `docs/REMINDER-PROVENANCE.md` (mechanism, explicit limits,
|
|
82
|
+
standards-evolution swap point).
|
|
83
|
+
- design/323 — subagent transcript persistence on the EXISTING SessionStore ("no new store"),
|
|
84
|
+
root-fixing revival across process restarts:
|
|
85
|
+
- **Placement contract** (all additive/optional): `SessionStore.placements` declaration,
|
|
86
|
+
placement-bearing acquire, `listPlaced`, six obligations (host-list exclusion, claim
|
|
87
|
+
refusal `placement_refused`, tuple immutability, mirror-direction identity check,
|
|
88
|
+
placed release = real row deletion with failures surfaced, partition enumeration).
|
|
89
|
+
`FileStorageBackend` gains `backgroundAgentStore`/`mailboxStore`/`rosterStore` (aggregation
|
|
90
|
+
parity with session/checkpoint/tool-result; half-wired silent-degradation shape closed) and
|
|
91
|
+
declares durable subagent placements.
|
|
92
|
+
- **Revival**: a completed background subagent now answers `SendMessage` across a process
|
|
93
|
+
restart (disk-replayed transcript; `status:"revived"`, cycle sequence continuous). The
|
|
94
|
+
anonymous-agent gate is retired (anonymous rows revive too). User-stopped agents stay
|
|
95
|
+
blocked on the model lane; `createAgentContinuationVerb` (module-private symbol,
|
|
96
|
+
`userInitiated`) is the operator door. `resolveSubagentTranscriptTier` (none/rows/full)
|
|
97
|
+
projects as `WiringManifest.fleet.subagentTranscripts`.
|
|
98
|
+
- **Caps + reaping**: `RunnerDeps.delegationEntryCaps` (defaults 20 concurrent / 200 retained;
|
|
99
|
+
bad values refuse via `config.delegation_entry_caps`; rejections
|
|
100
|
+
`delegation.concurrency_cap`/`delegation.session_cap`); the durable-agent reap gains a
|
|
101
|
+
placed-partition leg (`orphanPlacedReleased`) with same- and cross-process live-agent
|
|
102
|
+
protection; `delegation.transcript_integrity` notice. New exports: placement types,
|
|
103
|
+
tier/caps resolvers, continuation verb, five constants (+14, all additive).
|
|
104
|
+
|
|
105
|
+
### Fixed
|
|
106
|
+
- The shipped third-party memory-backend reference template refuses cross-scope re-adds and
|
|
107
|
+
judges whitewash on the PRE-BATCH committed snapshot (delete-then-readd in one batch can no
|
|
108
|
+
longer blank an origin; `guard:"absent"` same-rev idempotent arm kept; presence-judged strip
|
|
109
|
+
refusal). A resident pin now runs the template through the full conformance suite on every
|
|
110
|
+
suite execution.
|
|
111
|
+
|
|
112
|
+
### Notes
|
|
113
|
+
- Read/MCP results that contain reminder-shaped data grow a trailing block (additive, ~40
|
|
114
|
+
tokens); MCP/web segments carrying the exact current session mark are byte-defused (narrowing;
|
|
115
|
+
legitimate-content probability zero, disclosed inline). Everything else in the 319/323 batch
|
|
116
|
+
is additive; no BREAKING changes.
|
|
117
|
+
|
|
3
118
|
## 5.47.0 — 2026-08-20
|
|
4
119
|
|
|
5
120
|
### Added
|
|
@@ -28,6 +28,10 @@ export interface AgentTranscriptToolOptions {
|
|
|
28
28
|
* design/157 A7 — behind). Absent ⇒ unchanged behavior: the `opts` axes alone supply the identity.
|
|
29
29
|
*/
|
|
30
30
|
enrichCtx?: import("../core/tools.js").ToolCtxEnricher;
|
|
31
|
+
/** Subagent transcript persistence — the declared-durability integrity-gap disclosure sink (see
|
|
32
|
+
* the SendMessage twin): a durable row with a bound sessionId whose session store attests
|
|
33
|
+
* not_found. Announced once per (handle, process); absent ⇒ the honest per-call degrade alone. */
|
|
34
|
+
onTranscriptIntegrityGap?: (handle: string, scope: string | undefined) => void;
|
|
31
35
|
}
|
|
32
36
|
/**
|
|
33
37
|
* Residual observability (lane C) — build the MODEL-VISIBLE **AgentTranscript** tool: read the last N tool steps of a
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { defineTool } from "../core/tools.js";
|
|
3
|
-
import { canAccessAgentRecord } from "../core/background-agent-store.js";
|
|
3
|
+
import { announceTranscriptIntegrityGapOnce, canAccessAgentRecord } from "../core/background-agent-store.js";
|
|
4
|
+
import { DURABLE_AGENT_HANDLE_RE } from "../core/task-registry.js";
|
|
4
5
|
import { inlineUntrusted } from "../core/untrusted-text.js";
|
|
5
6
|
import { stepsFromMessages } from "./subagent-steps.js";
|
|
6
7
|
import { REPORT_FIELD_MAX } from "./subagent.js";
|
|
@@ -61,7 +62,10 @@ export function createAgentTranscriptTool(opts) {
|
|
|
61
62
|
details: { type: "agent-transcript", id, steps },
|
|
62
63
|
};
|
|
63
64
|
}
|
|
64
|
-
catch {
|
|
65
|
+
catch (e) {
|
|
66
|
+
if (e?.code === "not_found") {
|
|
67
|
+
announceTranscriptIntegrityGapOnce(id, access.scope, opts.onTranscriptIntegrityGap);
|
|
68
|
+
}
|
|
65
69
|
return undefined;
|
|
66
70
|
}
|
|
67
71
|
};
|
|
@@ -77,7 +81,10 @@ export function createAgentTranscriptTool(opts) {
|
|
|
77
81
|
const durable = await readDurableSteps();
|
|
78
82
|
if (durable)
|
|
79
83
|
return durable;
|
|
80
|
-
|
|
84
|
+
if (DURABLE_AGENT_HANDLE_RE.test(id)) {
|
|
85
|
+
return { content: `No transcript found for agent ${id}.`, details: { error: "not_found", id }, isError: true };
|
|
86
|
+
}
|
|
87
|
+
return { content: `No transcript: no background agent task "${id}" (unknown id, or not yours).`, details: { error: "not_found", id }, isError: true };
|
|
81
88
|
}
|
|
82
89
|
const runLedger = ctx.subagentRetain ?? undefined;
|
|
83
90
|
const atLedgerSessionId = ctx.sessionId ?? opts.sessionId;
|
|
@@ -2,6 +2,7 @@ import { Type } from "typebox";
|
|
|
2
2
|
import type { Runner } from "../core/runner/runtask.js";
|
|
3
3
|
import type { TaskNotificationPayload } from "../core/task-notification.js";
|
|
4
4
|
import { type ToolCtxEnricher } from "../core/tools.js";
|
|
5
|
+
import { type TaskAccess } from "../core/task-registry.js";
|
|
5
6
|
import { SubagentRetainLedger } from "./retain-ledger.js";
|
|
6
7
|
import { type SubagentSteerHandle } from "./subagent.js";
|
|
7
8
|
import { type PeerAdmissionConfig, type PeerInboundChainRef, type PeerSelfRef } from "./peer-admission.js";
|
|
@@ -125,6 +126,14 @@ export interface SendMessageToolOptions {
|
|
|
125
126
|
* A direct mount that wires `uplink` without this leaves L1 ungated (recorded honest boundary:
|
|
126
127
|
* the library does not fabricate an identity on the host's behalf). */
|
|
127
128
|
uplinkRecipient?: PeerSelfRef;
|
|
129
|
+
/**
|
|
130
|
+
* Subagent transcript persistence — the "declared durability contradicted by reality" disclosure
|
|
131
|
+
* sink: called (at most once per (scope, handle) per process, {@link announceTranscriptIntegrityGapOnce})
|
|
132
|
+
* when a durable row with a bound sessionId meets a session store that attests `not_found`. The
|
|
133
|
+
* first-party mount wires an EngineNotice (`delegation.transcript_integrity`); absent ⇒ the
|
|
134
|
+
* per-call honest refusal is the only loudness (unchanged behavior).
|
|
135
|
+
*/
|
|
136
|
+
onTranscriptIntegrityGap?: (handle: string, scope: string | undefined) => void;
|
|
128
137
|
}
|
|
129
138
|
/** RB-382 — max chars of the `summary` arg (the short recap label, distinct from `message`'s much
|
|
130
139
|
* larger {@link UPLINK_RESULT_MAX}) before ITS OWN truncation. Exported for direct unit-testing only
|
|
@@ -152,8 +161,41 @@ export declare function clipSendMessageSummary(raw: string): string;
|
|
|
152
161
|
* second revive). The auto-mode peer-referral tighten keys on the WIRE NAME and applies to a direct
|
|
153
162
|
* mount all the same.
|
|
154
163
|
*/
|
|
155
|
-
export declare function createSendMessageTool(opts: SendMessageToolOptions): import("../
|
|
164
|
+
export declare function createSendMessageTool(opts: SendMessageToolOptions): import("../internal/harness.js").AgentTool<Type.TObject<{
|
|
156
165
|
to: Type.TString;
|
|
157
166
|
message: Type.TString;
|
|
158
167
|
summary: Type.TOptional<Type.TString>;
|
|
159
168
|
}>, unknown>;
|
|
169
|
+
/** The receipt {@link createAgentContinuationVerb} resolves with — the SendMessage tool's own
|
|
170
|
+
* receipt family, flattened to one text (same content, same `details.error` code vocabulary). */
|
|
171
|
+
export interface AgentContinuationReceipt {
|
|
172
|
+
content: string;
|
|
173
|
+
details?: unknown;
|
|
174
|
+
isError?: boolean;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Subagent transcript persistence — the OPERATOR continuation verb: the SendMessage delivery ladder
|
|
178
|
+
* as a reusable, non-model face for a deployment's own continuation endpoint. SINGLE-SOURCE by
|
|
179
|
+
* construction: the verb routes THROUGH `createSendMessageTool`'s product (status arms, admission
|
|
180
|
+
* gates, preflight order, claim CAS + rollback, parked/running refusals and the whole refusal-code
|
|
181
|
+
* family are the tool's own, byte-shared — never a twin implementation). The one fork is the
|
|
182
|
+
* `userInitiated` mode bit ({@link OPERATOR_CONTINUATION_CTX}): `true` waives BOTH user-stop gates —
|
|
183
|
+
* the durable row's `stoppedBy === "user"` arm and the live registry's stop-attribution arm — so an
|
|
184
|
+
* explicit human "continue it" overrides an earlier human "stop it" (the winning claim's
|
|
185
|
+
* clear-terminal-payload erases the durable mark; the model-facing tool NEVER softens).
|
|
186
|
+
*
|
|
187
|
+
* Authorization is three-layered and this verb owns only the first: ① the same
|
|
188
|
+
* `canAccessAgentRecord` row predicate the tool runs (an out-of-scope handle reads not_found —
|
|
189
|
+
* non-leaking); ② "this really is an explicit human instruction" is the DEPLOYMENT's obligation —
|
|
190
|
+
* the same trust seat that stamps `stopSource:"user"` on the stop side (a server exposes this only
|
|
191
|
+
* through a verified interactive-user principal endpoint, never to unattended machine clients); ③ the model
|
|
192
|
+
* tool face has no such parameter (the bit travels on a module-private symbol — zero forgeable
|
|
193
|
+
* surface from arguments).
|
|
194
|
+
*
|
|
195
|
+
* Store-conditional like every continuation face: with no durable stores wired the live-gate waiver
|
|
196
|
+
* still works for same-process handles, and the durable leg refuses honestly.
|
|
197
|
+
*/
|
|
198
|
+
export declare function createAgentContinuationVerb(opts: SendMessageToolOptions): (handle: string, content: string, access: TaskAccess, o?: {
|
|
199
|
+
userInitiated?: true;
|
|
200
|
+
summary?: string;
|
|
201
|
+
}) => Promise<AgentContinuationReceipt>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { defineTool } from "../core/tools.js";
|
|
3
3
|
import { normalizeAgentName, DURABLE_AGENT_HANDLE_RE, DURABLE_AGENT_HEARTBEAT_MS } from "../core/task-registry.js";
|
|
4
|
-
import { canAccessAgentRecord, clearRevivedRowTerminalPayload } from "../core/background-agent-store.js";
|
|
4
|
+
import { announceTranscriptIntegrityGapOnce, canAccessAgentRecord, clearRevivedRowTerminalPayload } from "../core/background-agent-store.js";
|
|
5
|
+
import { uuidv7 } from "../internal/harness.js";
|
|
5
6
|
import { MAILBOX_TOMBSTONED_RECIPIENT_CODE } from "../core/mailbox-store.js";
|
|
6
7
|
import { escapeAttributeValue, escapeEnvelopeTag, isObserverTaskId, OBSERVER_SENDMESSAGE_SENDER_REFUSAL, OBSERVER_SENDMESSAGE_TARGET_REFUSAL, } from "./observer.js";
|
|
7
8
|
import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getSessionRetainLedger } from "./retain-ledger.js";
|
|
@@ -41,6 +42,7 @@ function withTargetLane(key, fn) {
|
|
|
41
42
|
function targetLaneKey(scope, targetId) {
|
|
42
43
|
return JSON.stringify([scope ?? "", targetId]);
|
|
43
44
|
}
|
|
45
|
+
const OPERATOR_CONTINUATION_CTX = Symbol("sema.operator_continuation");
|
|
44
46
|
export function createSendMessageTool(opts) {
|
|
45
47
|
const tier3Capable = opts.agentStore !== undefined && opts.mailbox !== undefined && opts.reviveSpawn !== undefined;
|
|
46
48
|
return defineTool({
|
|
@@ -56,9 +58,9 @@ export function createSendMessageTool(opts) {
|
|
|
56
58
|
`address an older one. You will be notified automatically when it completes — prefer ending your turn; do not ` +
|
|
57
59
|
`poll. ` +
|
|
58
60
|
(tier3Capable
|
|
59
|
-
? `Continuing a finished agent works for
|
|
61
|
+
? `Continuing a finished agent works for agents with a durable record — by name or task_id, even across restarts — and for runs that retain sub-agent sessions; when neither covers it you get an honest error and should launch a new agent with the needed context instead.`
|
|
60
62
|
: `Continuing a finished agent requires the run to retain sub-agent sessions; when the session was not ` +
|
|
61
|
-
`retained (or
|
|
63
|
+
`retained (or is no longer held) you get an honest error and should launch a new agent with the needed context instead.`),
|
|
62
64
|
parameters: Type.Object({
|
|
63
65
|
to: Type.String({ description: 'Recipient: the agent\'s name, or its task_id (a…) returned by the Agent tool with run_in_background. "main" is reserved for the spawning conversation.' }),
|
|
64
66
|
message: Type.String({ description: "The follow-up request. The agent continues from its full prior context." }),
|
|
@@ -66,6 +68,7 @@ export function createSendMessageTool(opts) {
|
|
|
66
68
|
}),
|
|
67
69
|
execute: async (args, ctx) => {
|
|
68
70
|
const a = args;
|
|
71
|
+
const operatorUserInitiated = ctx[OPERATOR_CONTINUATION_CTX] === true;
|
|
69
72
|
const to = String(a.to ?? "").trim();
|
|
70
73
|
const message = String(a.message ?? "").trim();
|
|
71
74
|
if (!to)
|
|
@@ -201,10 +204,10 @@ export function createSendMessageTool(opts) {
|
|
|
201
204
|
if (!canAccessAgentRecord(row, parentAccess))
|
|
202
205
|
return undefined;
|
|
203
206
|
}
|
|
204
|
-
if (row.
|
|
207
|
+
if (row.agentType === "fork")
|
|
205
208
|
return undefined;
|
|
206
209
|
const whoT3 = handle === to ? `agent ${handle}` : `agent "${to}" (${handle})`;
|
|
207
|
-
if (row.status === "killed" && row.stoppedBy === "user") {
|
|
210
|
+
if (row.status === "killed" && row.stoppedBy === "user" && !operatorUserInitiated) {
|
|
208
211
|
return {
|
|
209
212
|
content: `Message not sent: ${whoT3} was stopped by the user and was not resumed. Treat its work as cancelled; only start a new agent for it if the user explicitly asks.`,
|
|
210
213
|
details: { error: "killed", to, stoppedBy: row.stoppedBy },
|
|
@@ -289,7 +292,8 @@ export function createSendMessageTool(opts) {
|
|
|
289
292
|
catch (e) {
|
|
290
293
|
await rollbackDrain();
|
|
291
294
|
if (e?.code === "not_found") {
|
|
292
|
-
|
|
295
|
+
announceTranscriptIntegrityGapOnce(handle, scope, opts.onTranscriptIntegrityGap);
|
|
296
|
+
return queueFullReceipt(`No transcript found for agent ${handle}, so a revival cannot drain the backlog.`);
|
|
293
297
|
}
|
|
294
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.`);
|
|
295
299
|
}
|
|
@@ -387,8 +391,9 @@ export function createSendMessageTool(opts) {
|
|
|
387
391
|
catch (e) {
|
|
388
392
|
await rollback();
|
|
389
393
|
if (e?.code === "not_found") {
|
|
394
|
+
announceTranscriptIntegrityGapOnce(handle, scope, opts.onTranscriptIntegrityGap);
|
|
390
395
|
return {
|
|
391
|
-
content: `Message not sent:
|
|
396
|
+
content: `Message not sent: no transcript found for agent ${handle} — it cannot be revived. Launch a new agent with the needed context instead.`,
|
|
392
397
|
details: { error: "resume.session_not_found", to },
|
|
393
398
|
isError: true,
|
|
394
399
|
};
|
|
@@ -533,8 +538,15 @@ export function createSendMessageTool(opts) {
|
|
|
533
538
|
const labels = parentAccess !== undefined
|
|
534
539
|
? [...ownLabels, ...opts.registry.runningBackgroundAgentLabels(parentAccess).filter((l) => !ownLabels.includes(l))]
|
|
535
540
|
: ownLabels;
|
|
541
|
+
if (DURABLE_AGENT_HANDLE_RE.test(to)) {
|
|
542
|
+
return {
|
|
543
|
+
content: `Message not sent: no transcript found for agent ${to} — launch a new agent with the needed context instead.`,
|
|
544
|
+
details: { error: "not_found", to },
|
|
545
|
+
isError: true,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
536
548
|
return {
|
|
537
|
-
content: `Message not sent: no agent matches "${to}" (unknown id or name, not yours
|
|
549
|
+
content: `Message not sent: no agent matches "${to}" (unknown id or name, or not yours).` +
|
|
538
550
|
(suggestion !== undefined ? ` Did you mean: ${suggestion}?` : "") +
|
|
539
551
|
(labels.length > 0 ? ` Running background agents: ${labels.join(", ")}.` : "") +
|
|
540
552
|
` Note: a completed foreground agent is not resumable (its transcript is not retained) — spawn with run_in_background to keep an agent addressable, or launch a new agent.`,
|
|
@@ -617,7 +629,7 @@ export function createSendMessageTool(opts) {
|
|
|
617
629
|
}
|
|
618
630
|
if (row.status === "killed") {
|
|
619
631
|
const by = opts.registry.getStopAttribution(targetId);
|
|
620
|
-
if (by === "user") {
|
|
632
|
+
if (by === "user" && !operatorUserInitiated) {
|
|
621
633
|
return {
|
|
622
634
|
content: `Message not sent: ${who} was stopped by the user and was not resumed. Treat its work as cancelled; only start a new agent for it if the user explicitly asks.`,
|
|
623
635
|
details: { error: "killed", to, stoppedBy: by },
|
|
@@ -712,9 +724,10 @@ export function createSendMessageTool(opts) {
|
|
|
712
724
|
const text = code === "resume.retain_off"
|
|
713
725
|
? `${who}'s session was not retained (retainSubagentSessions is off) — relaunch a new agent instead.`
|
|
714
726
|
: code === "resume.evicted"
|
|
715
|
-
?
|
|
727
|
+
?
|
|
728
|
+
`${who} is no longer held in this run's fast lane (retain window or capacity passed, or the parent run ended) — relaunch a new agent with the needed context instead.`
|
|
716
729
|
: code === "resume.session_not_found"
|
|
717
|
-
?
|
|
730
|
+
? `no transcript found for ${who} — relaunch a new agent with the needed context instead.`
|
|
718
731
|
: code === "resume.cap"
|
|
719
732
|
? `${who} reached its resume cap (${SUBAGENT_RESUME_CAP} follow-ups per agent) — relaunch a new agent instead.`
|
|
720
733
|
: code === "steering.still_running"
|
|
@@ -732,3 +745,29 @@ export function createSendMessageTool(opts) {
|
|
|
732
745
|
},
|
|
733
746
|
}, opts.enrichCtx !== undefined ? { enrichCtx: opts.enrichCtx } : {});
|
|
734
747
|
}
|
|
748
|
+
export function createAgentContinuationVerb(opts) {
|
|
749
|
+
return async (handle, content, access, o) => {
|
|
750
|
+
const tool = createSendMessageTool({
|
|
751
|
+
...opts,
|
|
752
|
+
enrichCtx: (base) => {
|
|
753
|
+
const enriched = {
|
|
754
|
+
...base,
|
|
755
|
+
...(access.owner !== undefined ? { taskId: access.owner } : {}),
|
|
756
|
+
...(access.scope !== undefined ? { principal: access.scope } : {}),
|
|
757
|
+
...(access.sessionId !== undefined ? { sessionId: access.sessionId } : {}),
|
|
758
|
+
};
|
|
759
|
+
if (o?.userInitiated === true)
|
|
760
|
+
enriched[OPERATOR_CONTINUATION_CTX] = true;
|
|
761
|
+
return enriched;
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
const summaryRaw = (o?.summary ?? content).replace(/\s+/g, " ").trim().slice(0, 200);
|
|
765
|
+
const summary = summaryRaw === "" ? "operator follow-up" : summaryRaw;
|
|
766
|
+
const out = await tool.execute(`op-${uuidv7()}`, { to: handle, message: content, summary });
|
|
767
|
+
const text = out.content
|
|
768
|
+
.filter((c) => c.type === "text")
|
|
769
|
+
.map((c) => c.text)
|
|
770
|
+
.join("\n");
|
|
771
|
+
return { content: text, ...(out.details !== undefined ? { details: out.details } : {}), ...(out.isError === true ? { isError: true } : {}) };
|
|
772
|
+
};
|
|
773
|
+
}
|
|
@@ -466,6 +466,24 @@ export declare function createSubagentResume(deps: {
|
|
|
466
466
|
* isolation scope (first failure per site; later ones counted only). */
|
|
467
467
|
onNotifyError?: (failure: import("../core/safe-notify.js").SafeNotifyFailure) => void;
|
|
468
468
|
}): (content: string) => Promise<string>;
|
|
469
|
+
/** Subagent transcript persistence — the RESOLVED delegation entry-caps pair (both members present;
|
|
470
|
+
* see {@link resolveDelegationEntryCaps}). */
|
|
471
|
+
export interface ResolvedDelegationEntryCaps {
|
|
472
|
+
maxConcurrent: number;
|
|
473
|
+
maxCumulativePerSession: number;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Subagent transcript persistence — resolve + LOUDLY validate `RunnerDeps.delegationEntryCaps`
|
|
477
|
+
* (#123 bad-value law: a garbage knob refuses with a coded error, it is never silently folded to
|
|
478
|
+
* the defaults). Refusal shapes: a member that is not a positive integer (NaN, 0, negative,
|
|
479
|
+
* fractional, non-number), and a RESOLVED pair where `maxConcurrent > maxCumulativePerSession`
|
|
480
|
+
* (you cannot run more agents at once than the tree may ever create — a contradiction, not a
|
|
481
|
+
* preference). Absent members take the exported CC-parity defaults.
|
|
482
|
+
*/
|
|
483
|
+
export declare function resolveDelegationEntryCaps(caps: {
|
|
484
|
+
maxConcurrent?: number;
|
|
485
|
+
maxCumulativePerSession?: number;
|
|
486
|
+
} | undefined): ResolvedDelegationEntryCaps;
|
|
469
487
|
/**
|
|
470
488
|
* Options for {@link createSubagentTool}.
|
|
471
489
|
*
|
package/dist/agents/subagent.js
CHANGED
|
@@ -24,7 +24,7 @@ import { BG_AGENT_REAP_STOP_ERROR } from "../core/task-registry.js";
|
|
|
24
24
|
import { readDurableOrgAdmission } from "../core/memory-admission.js";
|
|
25
25
|
import { extractErrorCode } from "../brain/errors.js";
|
|
26
26
|
import { governanceRetryClass } from "../core/governance-codes.js";
|
|
27
|
-
import { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX, RUNNING_AGENT_OBSERVE_EVERY_BEATS } from "../config/defaults.js";
|
|
27
|
+
import { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX, RUNNING_AGENT_OBSERVE_EVERY_BEATS, DELEGATION_MAX_CONCURRENT_DEFAULT, DELEGATION_MAX_PER_SESSION_DEFAULT } from "../config/defaults.js";
|
|
28
28
|
function rollupDelegatedCost(stats, nested) {
|
|
29
29
|
if (stats.costMicroUsd === undefined)
|
|
30
30
|
return undefined;
|
|
@@ -875,6 +875,43 @@ function parkCompletionNotify(deps) {
|
|
|
875
875
|
};
|
|
876
876
|
entry.deferredNotify = { ...(payload.seq !== undefined ? { seq: payload.seq } : {}), cancel, flush: () => deliver(false) };
|
|
877
877
|
}
|
|
878
|
+
export function resolveDelegationEntryCaps(caps) {
|
|
879
|
+
const bad = (member, value) => {
|
|
880
|
+
const e = new Error(`RunnerDeps.delegationEntryCaps.${member} holds ${typeof value === "number" ? String(value) : value === null ? "null" : typeof value} — must be a positive integer (or absent for the default). A bad cap refuses loudly; it is never silently folded to the default.`);
|
|
881
|
+
e.code = "config.delegation_entry_caps";
|
|
882
|
+
throw e;
|
|
883
|
+
};
|
|
884
|
+
const member = (name, fallback) => {
|
|
885
|
+
const v = caps?.[name];
|
|
886
|
+
if (v === undefined)
|
|
887
|
+
return fallback;
|
|
888
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v <= 0)
|
|
889
|
+
bad(name, v);
|
|
890
|
+
return v;
|
|
891
|
+
};
|
|
892
|
+
const maxConcurrent = member("maxConcurrent", DELEGATION_MAX_CONCURRENT_DEFAULT);
|
|
893
|
+
const maxCumulativePerSession = member("maxCumulativePerSession", DELEGATION_MAX_PER_SESSION_DEFAULT);
|
|
894
|
+
if (maxConcurrent > maxCumulativePerSession) {
|
|
895
|
+
const e = new Error(`RunnerDeps.delegationEntryCaps resolves to maxConcurrent ${maxConcurrent} > maxCumulativePerSession ${maxCumulativePerSession} — a tree cannot run more agents at once than it may ever create. Fix the pair; a contradictory configuration refuses loudly.`);
|
|
896
|
+
e.code = "config.delegation_entry_caps";
|
|
897
|
+
throw e;
|
|
898
|
+
}
|
|
899
|
+
return { maxConcurrent, maxCumulativePerSession };
|
|
900
|
+
}
|
|
901
|
+
const delegationEntryLedgers = new WeakMap();
|
|
902
|
+
function delegationEntryLedger(registry, key) {
|
|
903
|
+
let byKey = delegationEntryLedgers.get(registry);
|
|
904
|
+
if (byKey === undefined) {
|
|
905
|
+
byKey = new Map();
|
|
906
|
+
delegationEntryLedgers.set(registry, byKey);
|
|
907
|
+
}
|
|
908
|
+
let set = byKey.get(key);
|
|
909
|
+
if (set === undefined) {
|
|
910
|
+
set = new Set();
|
|
911
|
+
byKey.set(key, set);
|
|
912
|
+
}
|
|
913
|
+
return set;
|
|
914
|
+
}
|
|
878
915
|
export function normalizeSubagentType(value) {
|
|
879
916
|
return value.normalize("NFKC").toLowerCase().replace(/[\p{White_Space}\p{Pd}_]+/gu, "");
|
|
880
917
|
}
|
|
@@ -2432,6 +2469,53 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2432
2469
|
const shortDesc = reviveRow?.description ?? String(a.description ?? "sub-agent").slice(0, 200);
|
|
2433
2470
|
const bgOwner = reviveRow !== undefined ? reviveRow.owner : sessionScopedBg ? ctx.sessionId : ctx.taskId ?? bg.owner;
|
|
2434
2471
|
const bgScope = treeScope;
|
|
2472
|
+
const capScope = bgScope;
|
|
2473
|
+
const capRoot = reviveRow !== undefined
|
|
2474
|
+
? (reviveRow.rootSessionId ?? reviveRow.parentSessionId ?? (reviveRow.sessionScoped ? reviveRow.owner : undefined))
|
|
2475
|
+
: (ctx.rootSessionId ?? ctx.sessionId);
|
|
2476
|
+
const entryCaps = ctx.delegationEntryCaps ?? { maxConcurrent: DELEGATION_MAX_CONCURRENT_DEFAULT, maxCumulativePerSession: DELEGATION_MAX_PER_SESSION_DEFAULT };
|
|
2477
|
+
const capLedgerKey = capScope !== undefined && capRoot !== undefined ? JSON.stringify([capScope, capRoot]) : undefined;
|
|
2478
|
+
if (capScope !== undefined && capRoot !== undefined) {
|
|
2479
|
+
let storedHandles = [];
|
|
2480
|
+
let storedEnumerationOk = false;
|
|
2481
|
+
if (bg.agentStore !== undefined && reviveRow === undefined) {
|
|
2482
|
+
try {
|
|
2483
|
+
storedHandles = (await bg.agentStore.listBySession(capScope, capRoot)).map((r) => r.handle);
|
|
2484
|
+
storedEnumerationOk = true;
|
|
2485
|
+
}
|
|
2486
|
+
catch {
|
|
2487
|
+
storedHandles = [];
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
const capRefusal = async (code, text) => {
|
|
2491
|
+
dropHostAbortListener();
|
|
2492
|
+
await cancelObserver();
|
|
2493
|
+
const wt = await finishWorktree();
|
|
2494
|
+
return { isError: true, content: `Sub-agent not started in background: ${text}${wt ? `\n${wt}` : ""}`, details: { error: code } };
|
|
2495
|
+
};
|
|
2496
|
+
const activeFace = bg.registry.activeDelegationHandles;
|
|
2497
|
+
const active = typeof activeFace === "function" ? activeFace.call(bg.registry, capScope, capRoot) : [];
|
|
2498
|
+
if (typeof activeFace === "function" && active.length >= entryCaps.maxConcurrent) {
|
|
2499
|
+
return await capRefusal("delegation.concurrency_cap", `this session tree already has ${active.length} background agents running — the concurrency cap (${entryCaps.maxConcurrent}; RunnerDeps.delegationEntryCaps.maxConcurrent) refuses another. Wait for one to complete (you will be notified) or stop one, then relaunch.`);
|
|
2500
|
+
}
|
|
2501
|
+
if (reviveRow === undefined) {
|
|
2502
|
+
const ledger = delegationEntryLedger(bg.registry, capLedgerKey);
|
|
2503
|
+
let cumulative;
|
|
2504
|
+
if (bg.agentStore !== undefined && storedEnumerationOk) {
|
|
2505
|
+
const keep = new Set([...storedHandles, ...active]);
|
|
2506
|
+
for (const h of [...ledger])
|
|
2507
|
+
if (!keep.has(h))
|
|
2508
|
+
ledger.delete(h);
|
|
2509
|
+
cumulative = new Set([...storedHandles, ...ledger]).size;
|
|
2510
|
+
}
|
|
2511
|
+
else {
|
|
2512
|
+
cumulative = ledger.size;
|
|
2513
|
+
}
|
|
2514
|
+
if (cumulative >= entryCaps.maxCumulativePerSession) {
|
|
2515
|
+
return await capRefusal("delegation.session_cap", `this session tree has already launched ${cumulative} background agents in its retained window — the cumulative cap (${entryCaps.maxCumulativePerSession}; RunnerDeps.delegationEntryCaps.maxCumulativePerSession) refuses more. Continue an existing agent (SendMessage) instead of launching new ones.`);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2435
2519
|
const settlementSeat = ctx.delegationSettlement?.();
|
|
2436
2520
|
const bgSettleId = settlementSeat !== undefined ? `bg-${uuidv7()}` : undefined;
|
|
2437
2521
|
if (settlementSeat !== undefined && bgSettleId !== undefined) {
|
|
@@ -2554,6 +2638,8 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2554
2638
|
}
|
|
2555
2639
|
return { isError: true, content: `Sub-agent not started in background: ${e instanceof Error ? e.message : String(e)}${wt ? `\n${wt}` : ""}`, details: { error: "register_failed" } };
|
|
2556
2640
|
}
|
|
2641
|
+
if (capLedgerKey !== undefined && reviveRow === undefined)
|
|
2642
|
+
delegationEntryLedger(bg.registry, capLedgerKey).add(taskId);
|
|
2557
2643
|
childInternals.peerSelfRef?.addAxis("h", taskId);
|
|
2558
2644
|
if (agentName !== undefined) {
|
|
2559
2645
|
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" }));
|
|
@@ -2691,10 +2777,22 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2691
2777
|
usage: { toolUses: toolStarts },
|
|
2692
2778
|
});
|
|
2693
2779
|
}, () => bg.registry.noteBackgroundAgentActivity(taskId));
|
|
2780
|
+
const bgPlacementRoot = reviveRow !== undefined ? (reviveRow.rootSessionId ?? reviveRow.parentSessionId) : (ctx.rootSessionId ?? ctx.sessionId);
|
|
2781
|
+
const bgPlacementParent = reviveRow !== undefined ? reviveRow.parentSessionId : ctx.sessionId;
|
|
2782
|
+
const bgPlacement = bg.agentStore !== undefined && opts.runner.sessions.placements?.subagent !== undefined
|
|
2783
|
+
? {
|
|
2784
|
+
kind: "subagent",
|
|
2785
|
+
...(bgScope !== undefined ? { scope: bgScope } : {}),
|
|
2786
|
+
...(bgPlacementParent !== undefined ? { parentSessionId: bgPlacementParent } : {}),
|
|
2787
|
+
...(bgPlacementRoot !== undefined ? { rootSessionId: bgPlacementRoot } : {}),
|
|
2788
|
+
handle: taskId,
|
|
2789
|
+
}
|
|
2790
|
+
: undefined;
|
|
2694
2791
|
const bgInternals = bgSink
|
|
2695
2792
|
? {
|
|
2696
2793
|
...childInternals,
|
|
2697
2794
|
...(bgQuestionStrip !== undefined ? bgQuestionStrip.internalsFlag : {}),
|
|
2795
|
+
...(bgPlacement !== undefined ? { sessionPlacement: bgPlacement } : {}),
|
|
2698
2796
|
onNotifyInjectorReady: s2NotifyReady,
|
|
2699
2797
|
delegationTaskType: "background_agent",
|
|
2700
2798
|
...(bgCycleSeq !== undefined ? { cycleSeq: bgCycleSeq } : {}),
|
|
@@ -2730,6 +2828,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2730
2828
|
: {
|
|
2731
2829
|
...childInternals,
|
|
2732
2830
|
...(bgQuestionStrip !== undefined ? bgQuestionStrip.internalsFlag : {}),
|
|
2831
|
+
...(bgPlacement !== undefined ? { sessionPlacement: bgPlacement } : {}),
|
|
2733
2832
|
onNotifyInjectorReady: s2NotifyReady,
|
|
2734
2833
|
delegationTaskType: "background_agent",
|
|
2735
2834
|
...(bgCycleSeq !== undefined ? { cycleSeq: bgCycleSeq } : {}),
|
|
@@ -2760,7 +2859,8 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2760
2859
|
};
|
|
2761
2860
|
try {
|
|
2762
2861
|
await opts.runner.sessions.acquire(bgChildSessionId, { requireExisting: true });
|
|
2763
|
-
|
|
2862
|
+
if (opts.runner.sessions.forget)
|
|
2863
|
+
await opts.runner.sessions.forget(bgChildSessionId);
|
|
2764
2864
|
}
|
|
2765
2865
|
catch (e) {
|
|
2766
2866
|
const code = e.code;
|
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
|