@sema-agent/core 5.54.0 → 5.56.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 +160 -0
- package/dist/agents/cumulative-stats.d.ts +26 -0
- package/dist/agents/cumulative-stats.js +56 -0
- package/dist/agents/observer.d.ts +11 -7
- package/dist/agents/observer.js +2 -4
- package/dist/agents/send-message-tool.js +48 -2
- package/dist/agents/subagent.js +250 -89
- package/dist/agents/verify.d.ts +27 -3
- package/dist/agents/verify.js +7 -2
- package/dist/core/auto-compaction.d.ts +17 -4
- package/dist/core/auto-compaction.js +3 -0
- package/dist/core/context-edit.d.ts +55 -6
- package/dist/core/context-edit.js +12 -1
- package/dist/core/governance-codes.js +14 -0
- package/dist/core/hooks.d.ts +293 -11
- package/dist/core/hooks.js +159 -12
- package/dist/core/human-input-projection.d.ts +20 -2
- package/dist/core/human-input-projection.js +9 -0
- package/dist/core/lsp-diagnostics.d.ts +19 -17
- package/dist/core/lsp-diagnostics.js +11 -5
- package/dist/core/mcp.d.ts +46 -0
- package/dist/core/mcp.js +132 -6
- package/dist/core/memory-engine/consolidation.d.ts +378 -0
- package/dist/core/memory-engine/consolidation.js +342 -0
- package/dist/core/memory-engine/dual-root.js +3 -0
- package/dist/core/memory-engine/engine.d.ts +237 -4
- package/dist/core/memory-engine/engine.js +1111 -4
- package/dist/core/memory-engine/export-bundle.js +9 -0
- package/dist/core/memory-engine/file-backend.js +27 -1
- package/dist/core/memory-engine/frontmatter.d.ts +20 -1
- package/dist/core/memory-engine/frontmatter.js +111 -0
- package/dist/core/memory-engine/index.d.ts +4 -2
- package/dist/core/memory-engine/index.js +3 -1
- package/dist/core/memory-engine/memory-backend-contract.js +131 -0
- package/dist/core/memory-engine/sync-client.js +26 -0
- package/dist/core/memory-engine/tools.d.ts +9 -0
- package/dist/core/memory-engine/tools.js +57 -13
- package/dist/core/memory-engine/types.d.ts +99 -0
- package/dist/core/memory-recall.js +4 -3
- package/dist/core/memory.d.ts +33 -3
- package/dist/core/memory.js +6 -4
- package/dist/core/permission-rules.d.ts +30 -0
- package/dist/core/permission-rules.js +71 -8
- package/dist/core/reminder-disclosure.d.ts +29 -4
- package/dist/core/reminder-disclosure.js +60 -12
- package/dist/core/runner/prepare-memory.js +7 -2
- package/dist/core/runner/prepare-task.d.ts +39 -1
- package/dist/core/runner/prepare-task.js +63 -35
- package/dist/core/runner/runtask.d.ts +8 -1
- package/dist/core/runner/runtask.js +170 -31
- package/dist/core/runner/session-rule-policy.js +5 -3
- package/dist/core/runner/synthetic-tools.js +4 -2
- package/dist/core/runner/turn-attachments.d.ts +16 -6
- package/dist/core/runner/turn-attachments.js +34 -20
- package/dist/core/session-reconcile.d.ts +32 -0
- package/dist/core/session-reconcile.js +15 -0
- package/dist/core/task-notification.d.ts +34 -7
- package/dist/core/task-notification.js +11 -1
- package/dist/core/task-registry-agent.d.ts +20 -3
- package/dist/core/task-registry-agent.js +31 -2
- package/dist/core/tool-policy.d.ts +23 -0
- package/dist/core/tool-policy.js +29 -13
- package/dist/core/types.d.ts +126 -17
- package/dist/core/untrusted-egress.js +12 -2
- package/dist/core/untrusted-text.d.ts +189 -3
- package/dist/core/untrusted-text.js +424 -6
- package/dist/engine/compaction/compaction.d.ts +77 -7
- package/dist/engine/compaction/compaction.js +98 -9
- package/dist/engine/compaction/utils.d.ts +4 -0
- package/dist/engine/compaction/utils.js +6 -0
- package/dist/engine/harness/agent-harness.d.ts +84 -0
- package/dist/engine/harness/agent-harness.js +88 -12
- package/dist/engine/harness/messages.d.ts +4 -2
- package/dist/engine/harness/messages.js +7 -2
- package/dist/engine/harness/types.d.ts +11 -5
- package/dist/engine/loop/types.d.ts +14 -0
- package/dist/engine/session/import-validate.js +10 -0
- package/dist/engine/session/session.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/orchestration/run-spec.js +8 -1
- package/dist/prompts/default.d.ts +22 -6
- package/dist/tools/fs/index.d.ts +3 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +28 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,165 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.56.0 — 2026-08-23
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **A2A covering rules join the mcp form** (#410, ruled): `a2a__peer` / `a2a__peer__*` deny, ask
|
|
7
|
+
and allow now fire in all five name-keyed lanes (rule DSL, allow/deny policy, approval policy,
|
|
8
|
+
frozen projection replay, persisted session rules) via the protocol-table-driven
|
|
9
|
+
`namespacedRuleNameCovers`. **Narrowings**: a frozen projection replayed under 5.56 honors
|
|
10
|
+
covering spellings it previously read as inert (heaviest); covering-name parenthesised rules
|
|
11
|
+
refuse (`unsupported.covering_paren`, additive union member); empty-peer spellings refuse at
|
|
12
|
+
construction. **Widening / false-refusal repair**: real minted names with separator-bearing tool
|
|
13
|
+
segments (`mcp__srv____foo` and twins) that previously THREW at construction now compile as
|
|
14
|
+
exact entries (the name-set screen judges the peer boundary only).
|
|
15
|
+
- **Delegation entry caps cover every local spawn leg** (#385, anchor campaign): sync spawn,
|
|
16
|
+
sync fork and background fork now pass the same concurrency/cumulative gate as background
|
|
17
|
+
spawns (one pool, four admit sites; in-flight reservations returned on settle). Sync legs mint
|
|
18
|
+
`delegation.concurrency_cap` / `delegation.session_cap` under the `Sub-agent not started: `
|
|
19
|
+
prefix (new code+prefix combination); counted nouns drop the word "background";
|
|
20
|
+
`AgentDefinition.background: true` is honored (previously a dead field); no-principal mounts
|
|
21
|
+
fold to the engine default pool instead of skipping the gate.
|
|
22
|
+
- **Every deployment hook seat is time-bounded** (#388, anchor campaign): the ten `Hooks` seats
|
|
23
|
+
ride one bounded family (default 600s; `Hooks.timeoutMs` knob, bad values refused loudly).
|
|
24
|
+
Decision seats fail closed on expiry (preToolUse → deny + permissionDenied(source "hook");
|
|
25
|
+
userPromptSubmit → block; stop → run ends); observation seats discard and shrink to a 1s grace
|
|
26
|
+
after task abort; hook contexts carry `signal` and live tracked `cwd`. Pre-ship rescan closures:
|
|
27
|
+
the runSpec hook mirror now carries `timeoutMs` (a deployment's bound was silently replaced by
|
|
28
|
+
the default on composed runs); stopFailure reads the deadline rather than a constant grace; the
|
|
29
|
+
post-abort grace caps remaining time instead of restarting the clock.
|
|
30
|
+
- **interrupt() keeps accepted user input; an interrupt leaves CC's transcript marker** (#389,
|
|
31
|
+
anchor campaign): steer queues survive `interrupt()` (one-way abort latch + in-flight slot);
|
|
32
|
+
a durable park migrates queued steers onto the checkpoint (idempotent CAS verb) and the
|
|
33
|
+
undrained account announces only the remainder; an explicit interrupt that won the abort mints
|
|
34
|
+
CC's verbatim `[Request interrupted by user]` marker (double-gated); unknown
|
|
35
|
+
`SystemInjectionPriority` values refuse and the unimplemented `"now"` ladder is disclosed once
|
|
36
|
+
per run (`task.injection_priority_unimplemented`); the untrusted caller steer lane gains CC's
|
|
37
|
+
mid-turn delivery frame.
|
|
38
|
+
- **A SendMessage name that changed hands refuses instead of silently redirecting** (CC's
|
|
39
|
+
`send_message_pin_guard` rebound arm): "Nothing was sent." plus both ways forward; a precise
|
|
40
|
+
task_id is never refused and re-pins; the tool description states the refusal.
|
|
41
|
+
- **A legacy-decided parked row converges** (#400, test P0-KPI): the live reconcile's resolved
|
|
42
|
+
skip now means "a claim is in flight" (read from the store row) — a checkpoint decided by the
|
|
43
|
+
task-level legacy lane settles the row instead of leaving a process-lifetime zombie; the
|
|
44
|
+
decision is re-read immediately before the verdict so a reopen CAS is never judged dead.
|
|
45
|
+
- **Compaction PTL retries disclose what they dropped** (#407, test P0-KPI):
|
|
46
|
+
`CompactionDetails.unsummarizedMessages` accumulates the true uncovered count across split
|
|
47
|
+
legs, with a shell-copy clause and an import-gate check. The summary prompt carries CC's
|
|
48
|
+
`<example>` skeleton and a compliant `<summary>` envelope is unwrapped in the persisted body
|
|
49
|
+
(pre-ship rescan closure: a fence-only EMPTY response is refused loudly, same class and wording
|
|
50
|
+
as the bare-empty arm — it could previously persist a footer-only summary).
|
|
51
|
+
`keepRecentToolResults` floors at 1 and refuses NaN/negative loudly.
|
|
52
|
+
- **#384 (keepRecentTokens default) closed as REFUTED, with evidence**: the anchor-campaign claim
|
|
53
|
+
that the 0 default meant "no kept tail" was disproved on a real Runner — `findCutPoint` has a
|
|
54
|
+
structural floor, the kept tail is never empty, and the pinned behavior matches CC's posture
|
|
55
|
+
via the other knob. The default is unchanged; the floor is now pinned
|
|
56
|
+
(test/compaction-keep-tail-floor.test.ts) and four stale anchor comments were corrected.
|
|
57
|
+
|
|
58
|
+
### Notes
|
|
59
|
+
- Anchor-campaign satellite dispositions ride along (A3/A4/A8/A14 candidate tables in the
|
|
60
|
+
respective cars' receipts): truthfulness rewrites, registry entries, and the
|
|
61
|
+
PermissionDenied trigger-surface divergence registered.
|
|
62
|
+
- Erratum owed to sibling repos (also stated in src/core/hooks.ts): "CC fails open on hook
|
|
63
|
+
faults except exit 2" is true only of the command(shell) hook channel; CC's in-process
|
|
64
|
+
callback channel is fail-closed with dedicated copy — cited as a fail-open precedent it is
|
|
65
|
+
the wrong channel.
|
|
66
|
+
- Residuals filed, not shipped silently: #417-#423 (auth seats, Agent(<type>) governance,
|
|
67
|
+
lifecycle legs, postToolUse payload asymmetry, reopen fence, classic-arm Trust-but-verify).
|
|
68
|
+
|
|
69
|
+
## 5.55.0 — 2026-08-23
|
|
70
|
+
|
|
71
|
+
### BREAKING
|
|
72
|
+
- **MemoryBackend conformance suite tightens 34 → 40 clauses** (design/339 B-1): c35-c40 cover the
|
|
73
|
+
distilled carrier's immutability seat, the supersession read face, and plan storage. A third-party
|
|
74
|
+
backend green at 34 can be red at 40 — **upgrade the backend BEFORE the engine** (the suite is the
|
|
75
|
+
executable contract; run `memoryBackendContract` against your backend first).
|
|
76
|
+
- **New power face** (design/339 B-5): the engine can now restructure memory — but ONLY through
|
|
77
|
+
two-phase intents under a per-scope seat (all-satisfied settlement; conflict parks; delete is a
|
|
78
|
+
terminal-state judgment; `resolveConsolidationPlan(planId, "retry" | "discard")` is the host valve).
|
|
79
|
+
A deployment that assumed the engine never writes distilled products must review its mounts.
|
|
80
|
+
- **memory_search/get face rev 4 → 5** (design/339 §3.4): SUPERSEDED entries leave the default
|
|
81
|
+
result face and the MEMORY.md mechanical index (data-driven, including pre-existing stores whose
|
|
82
|
+
entries acquire edges); they return under `includeSuperseded` and `memory_get` renders a
|
|
83
|
+
supersession banner. `CleanMemorySearchHit.supersededBy` / handle-shape twin are the new seats.
|
|
84
|
+
|
|
85
|
+
### Added
|
|
86
|
+
- **design/339 consolidation write protocol**: typed distilled carrier (planId/at/carrierRev
|
|
87
|
+
self-anchor outside the rev tuple; inputs with `superseded: true` edges), pure active-set
|
|
88
|
+
derivation (input-rev + carrier-rev anchors, challenge/latch suspension, chain rule, cross-scope
|
|
89
|
+
edges inert), four verbs (snapshot/freeze/apply/reconcile+resolve), gate-store minter accounting,
|
|
90
|
+
construction-time refusals, multiNode bit. New notice codes: `memory.consolidation_recommended` /
|
|
91
|
+
`_committed` / `_conflict` / `_refused` (+ the `memory.consolidation_*` refusal/incident family).
|
|
92
|
+
- **`tool_end.gatedCallId`** (top-level, optional): a park-contamination frame names the toolCallId
|
|
93
|
+
the committed gate holds, minted engine-side (not forgeable by tools); absent when the park holds
|
|
94
|
+
no call (plan review / resource slice). Interrupt/timeout aborts are byte-unchanged. **Wire
|
|
95
|
+
erratum**: an earlier board announcement placed this under `details` — `tool_end` frames carry no
|
|
96
|
+
`details`; the key is top-level.
|
|
97
|
+
- **Authority-envelope anti-forgery** (#391): all five harness-minted envelopes
|
|
98
|
+
(system-reminder / task-notification / new-diagnostics / user_memory / scope) are census-registered
|
|
99
|
+
(single source) and sink-neutralized by source; verbatim lanes stay byte-frozen with a positional
|
|
100
|
+
judgment bit. New disclosure counters `<outlet>.envelope` / `<outlet>.envelope_throttled` in
|
|
101
|
+
`TaskResult.stats.mechanisms.reminderDisclosures`; the trailer grows an envelope-family sentence.
|
|
102
|
+
- **MCP covering rules + resources-only mount** (#387/#386): `mcpRuleNameCovers` (CC's three
|
|
103
|
+
covering shapes) consulted in all five name-keyed lanes; resources-capable tools-less servers
|
|
104
|
+
mount; an out-of-charset top-level parameter name mounts WITH an operator advisory (connect leg).
|
|
105
|
+
- **openclaw benchmark rig** (design/184 ticket 2): HTTP-boundary comparison harness for the v3
|
|
106
|
+
auto-consolidation acceptance obligation (baseline bracket measured; head-to-head numbers pending
|
|
107
|
+
the openclaw build).
|
|
108
|
+
|
|
109
|
+
### Fixed
|
|
110
|
+
- **Verification loops report the CUMULATIVE spend** (#406, test P0-KPI): runWithVerification /
|
|
111
|
+
resumeWithVerification returned only the last leg's stats (a $1.70 two-round run disclosed $0.70).
|
|
112
|
+
Every leg now folds through `buildCumulativeStats`; early-exit paths return the
|
|
113
|
+
accumulated-so-far account; the RB-368 unpriced axis is preserved.
|
|
114
|
+
- **The persisted-rule seat is time-bounded** (#415, test P0-KPI): `persistedRules.admits` was
|
|
115
|
+
awaited bare — a hanging provider wedged the approval gate forever. The wait now has the org
|
|
116
|
+
sibling's bound (15s + abort); timeout reads as could-not-answer (ask + unavailable), never a
|
|
117
|
+
clean no-rule.
|
|
118
|
+
- **MCP model-facing schemas keep validation-bearing values byte-faithful** (pre-release rescan,
|
|
119
|
+
fixed before publish): the intake neutralizer introduced in this window NFKC-folded enum/const/
|
|
120
|
+
default/examples/pattern values, so the model was shown spellings the server refuses. The schema
|
|
121
|
+
walk now carries a seat: prose (`description`/`title`/`$comment`) and parameter-name keys are
|
|
122
|
+
neutralized; validation-bearing values, unknown/vendor subtrees, and patternProperties KEYS
|
|
123
|
+
(regexes) transit byte-verbatim; `required`/`$ref`/`$defs`/anchor families move in lockstep with
|
|
124
|
+
rewritten names. **Flipped pin**: an advertised ZWSP-bearing enum member is now shown verbatim
|
|
125
|
+
(wire correctness over card-face literal defusing; prose/name lanes still guard spoofing).
|
|
126
|
+
- **Fence truncation is self-reported** (pre-release rescan): `fenceMcpServerInstructions`
|
|
127
|
+
pre-measured with the narrow tag set while the fence clipped the widened form — a server placing
|
|
128
|
+
instructions in the (cap−N, cap] window got a silent cut with no truncation note, on both the
|
|
129
|
+
stable and delta lanes. `delimitUntrustedWithClip` now reports its own clip; the RB-152 egress
|
|
130
|
+
`…[+N chars]` count stays anchored to the true source omission.
|
|
131
|
+
- **The consolidation scope wall holds on the read side** (pre-release rescan): a carrier in scope A
|
|
132
|
+
could suppress a target in scope B on every read face (edge validity never compared scopes; a
|
|
133
|
+
supported cross-scope MOVE keeps the rev byte-identical). Cross-scope edges are now inert —
|
|
134
|
+
existing stores: any cross-scope-suppressed entry RETURNS to the default face (revival direction).
|
|
135
|
+
- **The rewrite terminal carries provenance/trust**: dropping them minted the whitewash patch every
|
|
136
|
+
conforming backend must refuse (the plan parked forever holding the scope seat); a trust-only
|
|
137
|
+
entry was instead silently whitewashed on stores keying the guard on provenance.kind. Both fields
|
|
138
|
+
now carry forward beside origin/distilled (this also corrects plannedPostRev).
|
|
139
|
+
- **The CONFLICT notice stops overstating** (server/cli pins on the old string will red): it names
|
|
140
|
+
all three park causes (concurrent write / store-declined patch / re-judgment) and scopes the
|
|
141
|
+
no-overwrite claim to REFUSED targets (applied siblings stand; discard rolls nothing back);
|
|
142
|
+
retry-synthesized parks now write their own per-target audit rows.
|
|
143
|
+
- **memory_get pages the displayed string**: budgets/cursors previously addressed the raw body while
|
|
144
|
+
the fence displayed a longer neutralized one; the body is now neutralized once into a canonical
|
|
145
|
+
line array every page and cursor reads. Envelope-tag-bearing bodies page differently (fewer lines
|
|
146
|
+
per page, never over budget); marker-free bodies are byte-identical.
|
|
147
|
+
- **The third memory composer joins the widened tag family** (#391 class sweep):
|
|
148
|
+
`composeSelectiveBody` hard-coded `["user_memory"]` in its three sanitize seats while its two
|
|
149
|
+
siblings were widened to `MEMORY_INJECTION_TAGS`.
|
|
150
|
+
- **Contract texts catch up with minted disclosure keys**: `envelope`/`envelope_throttled`/
|
|
151
|
+
`mark_echo` join the `reminderDisclosures` JSDoc and docs/REMINDER-PROVENANCE.md (ratchet-pinned:
|
|
152
|
+
minted set ⊆ both texts; DISCLOSED tag set must be named in both).
|
|
153
|
+
|
|
154
|
+
### Notes
|
|
155
|
+
- Conformance fixture rename (`id-legacy-0001` → `id-plain-0001`); 14 `memory.consolidation_*`
|
|
156
|
+
codes registered non-governance (mechanism receipts, not per-principal verdicts); type-hygiene
|
|
157
|
+
memory-engine checked-cast baseline 14 → 23 (each behind a runtime validator added in the same
|
|
158
|
+
commits).
|
|
159
|
+
- Residuals filed, not shipped silently: #414① (refresh-leg advisory drop), #416 (loosenReasons
|
|
160
|
+
covering-awareness — a published-corpus change, scheduled with the names window), patternProperties
|
|
161
|
+
`$ref` pointer form (disclosed at the seat table).
|
|
162
|
+
|
|
3
163
|
## 5.54.0 — 2026-08-22
|
|
4
164
|
|
|
5
165
|
### Fixed
|
|
@@ -83,3 +83,29 @@ export interface CumulativeStatsAccumulator {
|
|
|
83
83
|
* base: a ratio is not additive, so the last leg's rate is never the operation's rate.
|
|
84
84
|
*/
|
|
85
85
|
export declare function buildCumulativeStats(base: TaskResult["stats"], acc: CumulativeStatsAccumulator): TaskResult["stats"];
|
|
86
|
+
/** A running {@link CumulativeStatsAccumulator} with the per-leg fold built in. */
|
|
87
|
+
export interface CumulativeStatsTracker {
|
|
88
|
+
/** Fold ONE leg's stats into the running totals. Call once per leg, in dispatch order. */
|
|
89
|
+
add(s: TaskResult["stats"]): void;
|
|
90
|
+
/** Build the cumulative stats over `base` — conventionally the FINAL leg's stats (it carries the
|
|
91
|
+
* non-accumulated fields such as `model`). */
|
|
92
|
+
build(base: TaskResult["stats"]): TaskResult["stats"];
|
|
93
|
+
/** How many legs have been folded in. A composition that ran exactly ONE leg can use this to hand that
|
|
94
|
+
* leg's own stats back untouched: the cumulative account over one leg IS that leg, and re-deriving it
|
|
95
|
+
* would rewrite a caller's object (0 → `undefined` normalization, a re-derived `cacheHitRate`) for no
|
|
96
|
+
* gain. */
|
|
97
|
+
readonly legs: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Create a {@link CumulativeStatsTracker}.
|
|
101
|
+
*
|
|
102
|
+
* {@link buildCumulativeStats} made the OUTPUT literal single-source, but the INPUT fold stayed copied:
|
|
103
|
+
* cascade / repair-loop / teacher each hand-maintain the same dozen `+=` lines, so a new stats field still
|
|
104
|
+
* has to be remembered in four places — the same duplication one level down from the hole this module
|
|
105
|
+
* exists to close. New call sites take this factory instead. Migrating the three originals onto it is a
|
|
106
|
+
* separate change (their folds are equivalent to this one today, but rewriting three shipped compositions
|
|
107
|
+
* is not part of a cost-accounting bug fix).
|
|
108
|
+
*
|
|
109
|
+
* OWN and NESTED stay DISJOINT here too: `add` never folds a leg's `nested.*` into the own totals.
|
|
110
|
+
*/
|
|
111
|
+
export declare function createCumulativeStatsTracker(): CumulativeStatsTracker;
|
|
@@ -27,3 +27,59 @@ export function buildCumulativeStats(base, acc) {
|
|
|
27
27
|
: undefined,
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
+
export function createCumulativeStatsTracker() {
|
|
31
|
+
let legs = 0;
|
|
32
|
+
const acc = {
|
|
33
|
+
tokens: 0,
|
|
34
|
+
turns: 0,
|
|
35
|
+
promptTokens: 0,
|
|
36
|
+
totalInputTokens: 0,
|
|
37
|
+
cachedTokens: 0,
|
|
38
|
+
outputTokens: 0,
|
|
39
|
+
cacheWriteTokens: 0,
|
|
40
|
+
cacheWriteTokensLong: 0,
|
|
41
|
+
costMicroUsd: 0,
|
|
42
|
+
anyLegUnpriced: false,
|
|
43
|
+
llmRootMicroUsd: 0,
|
|
44
|
+
compactionMicroUsd: 0,
|
|
45
|
+
nested: { tokens: 0, turns: 0, tasks: 0, costMicroUsd: 0 },
|
|
46
|
+
humanReview: { count: 0, totalWaitMs: 0, gates: [] },
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
get legs() {
|
|
50
|
+
return legs;
|
|
51
|
+
},
|
|
52
|
+
add(s) {
|
|
53
|
+
legs += 1;
|
|
54
|
+
acc.tokens += s.tokens;
|
|
55
|
+
acc.turns += s.turns;
|
|
56
|
+
acc.promptTokens += s.promptTokens ?? 0;
|
|
57
|
+
acc.totalInputTokens += s.totalInputTokens ?? 0;
|
|
58
|
+
acc.cachedTokens += s.cachedTokens ?? 0;
|
|
59
|
+
acc.outputTokens += s.outputTokens ?? 0;
|
|
60
|
+
acc.cacheWriteTokens += s.cacheWriteTokens ?? 0;
|
|
61
|
+
acc.cacheWriteTokensLong += s.cacheWriteTokensLong ?? 0;
|
|
62
|
+
acc.costMicroUsd = (acc.costMicroUsd ?? 0) + (s.costMicroUsd ?? 0);
|
|
63
|
+
if (s.costMicroUsd === undefined)
|
|
64
|
+
acc.anyLegUnpriced = true;
|
|
65
|
+
if (s.costBreakdown) {
|
|
66
|
+
acc.llmRootMicroUsd += s.costBreakdown.llmRootMicroUsd;
|
|
67
|
+
acc.compactionMicroUsd += s.costBreakdown.compactionMicroUsd;
|
|
68
|
+
}
|
|
69
|
+
if (s.humanReview) {
|
|
70
|
+
acc.humanReview.count += s.humanReview.count;
|
|
71
|
+
acc.humanReview.totalWaitMs += s.humanReview.totalWaitMs;
|
|
72
|
+
acc.humanReview.gates.push(...s.humanReview.gates);
|
|
73
|
+
}
|
|
74
|
+
if (s.nested) {
|
|
75
|
+
acc.nested.tokens += s.nested.tokens;
|
|
76
|
+
acc.nested.turns += s.nested.turns;
|
|
77
|
+
acc.nested.tasks += s.nested.tasks;
|
|
78
|
+
acc.nested.costMicroUsd += s.nested.costMicroUsd ?? 0;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
build(base) {
|
|
82
|
+
return buildCumulativeStats(base, acc);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { Type } from "typebox";
|
|
18
18
|
import type { AgentDefinition, TaskEvent } from "../core/types.js";
|
|
19
19
|
import { defineTool } from "../core/tools.js";
|
|
20
|
+
import { escapeEnvelopeTag } from "../core/untrusted-text.js";
|
|
20
21
|
/** The four tagged digest event kinds (CC `Tcg`) — `assistant_text` renders untagged. */
|
|
21
22
|
export declare const OBSERVER_EVENT_TAGS: readonly ["tool-call", "user-message", "tool-result", "turn-ended"];
|
|
22
23
|
export type ObserverDigestEvent = {
|
|
@@ -39,14 +40,17 @@ export type ObserverDigestEvent = {
|
|
|
39
40
|
/** CC `S$r` — anti-injection: a caller-declared envelope tag inside observed content is defused in place. */
|
|
40
41
|
export declare function escapeObserverTags(text: string): string;
|
|
41
42
|
/**
|
|
42
|
-
* CC `zZe` @8339092 —
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
43
|
+
* CC `zZe` @8339092 — the named-envelope tag escape. It is the second escape layer CC applies to the
|
|
44
|
+
* DYNAMIC envelope tag (`<{slug}-activity>`) after joining, which the static four-tag
|
|
45
|
+
* {@link escapeObserverTags} (Scg layer) cannot cover.
|
|
46
|
+
*
|
|
47
|
+
* RE-HOMED to `core/untrusted-text.ts` beside the ENGINE_ENVELOPES registry (whose guard column
|
|
48
|
+
* points at it): "escape one named envelope tag" is a containment primitive, not an observer
|
|
49
|
+
* concern, and having it live under `agents/` is what let two later envelopes (`skills`, `scope`)
|
|
50
|
+
* ship with no guard — nothing in the containment module named it. Re-exported here so this
|
|
51
|
+
* module's own call sites and `send-message-tool.ts` are unchanged.
|
|
48
52
|
*/
|
|
49
|
-
export
|
|
53
|
+
export { escapeEnvelopeTag };
|
|
50
54
|
/** CC `Yp` @8338930 — attribute-value escape: `oc` entities (& < >) plus quote/apostrophe. */
|
|
51
55
|
export declare function escapeAttributeValue(value: string): string;
|
|
52
56
|
/** CC `Eqi` = 2000 — the per-payload digest truncation bound (tool inputs/results, user messages). */
|
package/dist/agents/observer.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { defineTool } from "../core/tools.js";
|
|
3
3
|
import { createSafeNotifier } from "../core/safe-notify.js";
|
|
4
|
+
import { escapeEnvelopeTag } from "../core/untrusted-text.js";
|
|
4
5
|
export const OBSERVER_EVENT_TAGS = ["tool-call", "user-message", "tool-result", "turn-ended"];
|
|
5
6
|
const TAG_ESCAPE_RE = new RegExp(`<(?=/?(?:${OBSERVER_EVENT_TAGS.join("|")})(?:[>\\s/]|$))`, "gi");
|
|
6
7
|
export function escapeObserverTags(text) {
|
|
7
8
|
return text.replace(TAG_ESCAPE_RE, "<\\");
|
|
8
9
|
}
|
|
9
|
-
export
|
|
10
|
-
const escapedTag = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11
|
-
return text.replace(new RegExp(`<(?=/?${escapedTag}(?:[>\\s/]|$))`, "gi"), "<\\");
|
|
12
|
-
}
|
|
10
|
+
export { escapeEnvelopeTag };
|
|
13
11
|
export function escapeAttributeValue(value) {
|
|
14
12
|
return value
|
|
15
13
|
.replaceAll("&", "&")
|
|
@@ -5,6 +5,7 @@ import { announceTranscriptIntegrityGapOnce, canAccessAgentRecord, clearRevivedR
|
|
|
5
5
|
import { uuidv7 } from "../internal/harness.js";
|
|
6
6
|
import { MAILBOX_TOMBSTONED_RECIPIENT_CODE } from "../core/mailbox-store.js";
|
|
7
7
|
import { escapeAttributeValue, escapeEnvelopeTag, isObserverTaskId, OBSERVER_SENDMESSAGE_SENDER_REFUSAL, OBSERVER_SENDMESSAGE_TARGET_REFUSAL, } from "./observer.js";
|
|
8
|
+
import { inlineUntrusted } from "../core/untrusted-text.js";
|
|
8
9
|
import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getSessionRetainLedger } from "./retain-ledger.js";
|
|
9
10
|
import { createSubagentResume } from "./subagent.js";
|
|
10
11
|
import { appendHopToken, judgePeerAdmission, peerAxisToken, resolvePeerAdmissionConfig, PEER_MESSAGE_NOTICE, } from "./peer-admission.js";
|
|
@@ -45,6 +46,35 @@ function targetLaneKey(scope, targetId) {
|
|
|
45
46
|
const OPERATOR_CONTINUATION_CTX = Symbol("sema.operator_continuation");
|
|
46
47
|
export function createSendMessageTool(opts) {
|
|
47
48
|
const tier3Capable = opts.agentStore !== undefined && opts.mailbox !== undefined && opts.reviveSpawn !== undefined;
|
|
49
|
+
const sendMessagePins = new Map();
|
|
50
|
+
const pinGuard = (targetId, targetName, rung, to) => {
|
|
51
|
+
if (rung === "other")
|
|
52
|
+
return undefined;
|
|
53
|
+
const pinKey = normalizeAgentName(targetName ?? "");
|
|
54
|
+
if (pinKey === "")
|
|
55
|
+
return undefined;
|
|
56
|
+
const pinned = sendMessagePins.get(pinKey);
|
|
57
|
+
if (pinned === undefined || pinned.id === targetId) {
|
|
58
|
+
sendMessagePins.set(pinKey, { id: targetId, name: targetName ?? to });
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
if (rung === "ref") {
|
|
62
|
+
sendMessagePins.set(pinKey, { id: targetId, name: targetName ?? to });
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
if (targetName !== undefined && normalizeAgentName(to) === pinKey && normalizeAgentName(pinned.name) !== pinKey) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
content: `Message not sent: "${inlineUntrusted(to, 120)}" now resolves to a different agent than it did earlier in ` +
|
|
70
|
+
`this run — this name previously resolved to ${pinned.id.slice(0, 64)}, which it no longer reaches. Nothing was sent.\n` +
|
|
71
|
+
`It now resolves to ${targetId.slice(0, 64)}. To message that agent, re-send with its task_id: ` +
|
|
72
|
+
`{"to": "${targetId.slice(0, 64)}", …}. To reach the earlier agent, use its task_id ` +
|
|
73
|
+
`${pinned.id.slice(0, 64)} from its spawn result.`,
|
|
74
|
+
details: { error: "rebound", to, taskId: targetId, previousTaskId: pinned.id },
|
|
75
|
+
isError: true,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
48
78
|
return defineTool({
|
|
49
79
|
name: SEND_MESSAGE_TOOL_NAME,
|
|
50
80
|
contract: { contractId: "core.send_message@1", implementationRevision: "1" },
|
|
@@ -54,8 +84,9 @@ export function createSendMessageTool(opts) {
|
|
|
54
84
|
`'to' is the agent's name (set at spawn via Agent({name})) or its task_id (a…). A RUNNING agent receives the ` +
|
|
55
85
|
`message at its next turn (queued — never interrupts its current work); a FINISHED agent resumes as a new ` +
|
|
56
86
|
`background run with its full prior conversation preserved, so don't re-explain what it already knows. Names ` +
|
|
57
|
-
`keep working after an agent completes;
|
|
58
|
-
`
|
|
87
|
+
`keep working after an agent completes; but if a name you already messaged is later taken by a NEWER agent, the ` +
|
|
88
|
+
`send is refused rather than silently redirected — re-send with the task_id of whichever agent you meant. ` +
|
|
89
|
+
`You will be notified automatically when it completes — prefer ending your turn; do not ` +
|
|
59
90
|
`poll. ` +
|
|
60
91
|
(tier3Capable
|
|
61
92
|
? `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.`
|
|
@@ -523,6 +554,11 @@ export function createSendMessageTool(opts) {
|
|
|
523
554
|
}
|
|
524
555
|
}
|
|
525
556
|
if (rosterHit !== undefined) {
|
|
557
|
+
{
|
|
558
|
+
const guard = pinGuard(rosterHit.agentId, to, "name", to);
|
|
559
|
+
if (guard !== undefined)
|
|
560
|
+
return guard;
|
|
561
|
+
}
|
|
526
562
|
const revived = await tier3Revive(rosterHit.agentId);
|
|
527
563
|
if (revived !== undefined)
|
|
528
564
|
return revived;
|
|
@@ -563,6 +599,16 @@ export function createSendMessageTool(opts) {
|
|
|
563
599
|
if (row.type !== "background_agent") {
|
|
564
600
|
return { content: `Message not sent: ${targetId} is a ${row.type} task, not a background agent.`, details: { error: "wrong_type", to }, isError: true };
|
|
565
601
|
}
|
|
602
|
+
{
|
|
603
|
+
const rung = idRow !== undefined
|
|
604
|
+
? "ref"
|
|
605
|
+
: row.name !== undefined && (row.name === to || normalizeAgentName(row.name) === normalizeAgentName(to))
|
|
606
|
+
? "name"
|
|
607
|
+
: "other";
|
|
608
|
+
const guard = pinGuard(targetId, row.name, rung, to);
|
|
609
|
+
if (guard !== undefined)
|
|
610
|
+
return guard;
|
|
611
|
+
}
|
|
566
612
|
if (row.status === "running" || row.status === "pending") {
|
|
567
613
|
return await withTargetLane(targetLaneKey(access.scope, targetId), async () => {
|
|
568
614
|
const liveRecipientKey = peerAxisToken(resolvedAccess.scope, "h", targetId);
|