@sema-agent/core 5.55.0 → 5.57.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 +140 -0
- package/dist/agents/send-message-tool.d.ts +11 -0
- package/dist/agents/send-message-tool.js +81 -13
- package/dist/agents/subagent.js +250 -89
- package/dist/agents/team.d.ts +10 -1
- package/dist/agents/team.js +1 -0
- package/dist/brain/anthropic.js +15 -5
- package/dist/brain/circuit-breaker.js +2 -1
- package/dist/brain/degrading.js +4 -1
- package/dist/brain/failover.js +16 -1
- package/dist/brain/open-responses.js +15 -5
- package/dist/brain/openai.js +16 -5
- package/dist/brain/request-params.d.ts +30 -27
- package/dist/brain/request-params.js +1 -7
- package/dist/brain/route-adjudicator.d.ts +190 -0
- package/dist/brain/route-adjudicator.js +189 -0
- package/dist/brain/route-conformance.d.ts +55 -0
- package/dist/brain/route-conformance.js +136 -0
- package/dist/brain/routing.js +8 -3
- 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/hooks.d.ts +293 -11
- package/dist/core/hooks.js +158 -11
- package/dist/core/human-input-projection.d.ts +20 -2
- package/dist/core/human-input-projection.js +9 -0
- package/dist/core/mcp.js +4 -4
- package/dist/core/memory-engine/engine.d.ts +15 -5
- package/dist/core/memory-engine/engine.js +3 -1
- package/dist/core/permission-rule-consent.d.ts +45 -0
- package/dist/core/permission-rule-consent.js +40 -11
- package/dist/core/permission-rule-model.d.ts +110 -75
- package/dist/core/permission-rule-model.js +61 -28
- package/dist/core/permission-rules.d.ts +23 -15
- package/dist/core/permission-rules.js +40 -31
- package/dist/core/runner/prepare-task.d.ts +8 -0
- package/dist/core/runner/prepare-task.js +66 -26
- package/dist/core/runner/runtask.d.ts +4 -1
- package/dist/core/runner/runtask.js +206 -21
- package/dist/core/runner/session-rule-policy.js +5 -5
- package/dist/core/scheduler.d.ts +5 -0
- package/dist/core/session-reconcile.d.ts +32 -0
- package/dist/core/session-reconcile.js +15 -0
- package/dist/core/side-query.d.ts +12 -5
- 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 +14 -9
- package/dist/core/tool-policy.js +27 -22
- package/dist/core/types.d.ts +69 -11
- package/dist/core/untrusted-text.js +8 -0
- 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 +114 -13
- package/dist/engine/harness/messages.d.ts +4 -2
- package/dist/engine/harness/messages.js +7 -2
- package/dist/engine/harness/types.d.ts +16 -6
- package/dist/engine/llm/types.d.ts +65 -0
- package/dist/engine/loop/types.d.ts +7 -0
- package/dist/engine/session/import-validate.js +10 -0
- package/dist/engine/session/session.js +2 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/internal/llm.d.ts +1 -1
- package/dist/orchestration/run-spec.js +8 -1
- package/dist/prompts/default.d.ts +12 -6
- package/dist/prompts/default.js +2 -0
- package/dist/scenarios/scenario-registry.d.ts +5 -1
- package/dist/scenarios/scenario-registry.js +4 -2
- package/dist/tools/fs/index.js +8 -1
- package/dist/tools/scheduler-tools.js +28 -6
- package/dist/tools/web.d.ts +15 -0
- package/dist/tools/web.js +8 -2
- package/dist/tools/worktree.js +2 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +19 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,145 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.57.0 — 2026-08-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **key×URL pairing adjudicator** (#309 cross-repo case, [5060]-[5063] consensus): `adjudicateModelRoute`
|
|
7
|
+
is the single source of the credential-pairing law. Per-model credential = paired by construction;
|
|
8
|
+
the deployment-config credential passes only on the deployment's own declared root; absent credential
|
|
9
|
+
is legal keyless except for an entry declaring its own off-root URL. Codes `route.credential_mismatch`
|
|
10
|
+
/ `route.credential_missing` (refusal detail carries entry id + both URL halves + a one-line fixHint,
|
|
11
|
+
fail-closed by ruling). The three first-party brains gate inside buildRequest (refusal terminates
|
|
12
|
+
before any fetch) and expose `Brain.adjudicateRoute`; routing/failover/degrading/circuit-breaker
|
|
13
|
+
re-judge per leg (failover serves from the paired leg; a broken degrading hop is skipped). Two-leg
|
|
14
|
+
disposition: a NAMED model surfaces the refusal; a DERIVED model (compaction summarize, auto-mode
|
|
15
|
+
classifier, suggestions) pre-flights and falls back to the primary with a `route.fallback_to_primary`
|
|
16
|
+
notice; `route.base_url_changed_key_unchanged` mints at swapModels. `ROUTE_ADJUDICATION_CONFORMANCE_CORPUS`
|
|
17
|
+
(16 vectors) exported as the cross-codebase anchor. **Narrowings (each with a fixHint)**: declared-root
|
|
18
|
+
deployments' off-root entries now refuse instead of silently borrowing the gateway credential; an
|
|
19
|
+
entry-declared auth header now wins over the deployment key. Preserved byte-identical: unpinned
|
|
20
|
+
quick-start, keyless single-gateway, header-only boot token, gateway dual-auth.
|
|
21
|
+
- **Per-model auth seats for WebFetch summarizer and team surfaces** (#417): `getApiKeyAndHeaders`
|
|
22
|
+
on `WebFetchSummarizerOptions` / `TeamDiscussionOptions` / `runScenario` (absent seat = byte-identical).
|
|
23
|
+
- **Compound-prefix permission rules** (#364 case B, ruled; registered divergence): `Bash(cd /tmp && adb pull:*)`
|
|
24
|
+
— leading segments byte-exact, only the final segment takes arguments. The validator accepts the
|
|
25
|
+
chain form (a `*` in any other position still refuses); the matcher admits on segment-count equality
|
|
26
|
+
+ byte-prefix at a word boundary (a single-command prefix still never admits a compound; the segment
|
|
27
|
+
deny/ask fence stays strictly ahead of the allow lane); the suggester mints the compound-prefix
|
|
28
|
+
sibling at index 1 when the final segment fits the same grammar as the simple seat; the card-edit
|
|
29
|
+
face accepts the respelling; org tighten gains the same grammar (a compound-prefix deny is enforceable
|
|
30
|
+
instead of bricking its snapshot). **Widenings**: validator accepts the new form; eligible compounds
|
|
31
|
+
offer 2 seats; card edit + import accept it; org snapshots carrying it are enforceable. **Mixed-fleet
|
|
32
|
+
note**: older engines quarantine compound-prefix rows (bytes preserved, disclosed) — do not publish
|
|
33
|
+
compound-prefix org rules before the fleet is level.
|
|
34
|
+
- **Generic prefix grammar for rule suggestions** (#364 R1, CC 2.1.223 iNo corpus): the prefix arm
|
|
35
|
+
uses the corpus rule `^[a-z][a-z0-9]*(-[a-z0-9]+)*$` with the interpreter/wrapper blacklist; the
|
|
36
|
+
125-line lexicon demotes to a bonus for multi-word deepening; ASCII-closed head-name screen.
|
|
37
|
+
- **`precheckEditedRuleText(text, command)`** (#428, server pickup): the card-edit face's three text
|
|
38
|
+
gates (spelling normalization → grammar → coverage bound) extracted to one shared body and exported
|
|
39
|
+
(`EditedRuleTextPrecheck`). `ok` means SUBMITTABLE (record-level gates stay with `confirmRuleApproval`);
|
|
40
|
+
`canonicalRule` may differ from the submitted bytes; refusal words match `edit_rejected.detail`
|
|
41
|
+
verbatim (`code` rides validator refusals only). The entry screens unrenderable command bytes loudly
|
|
42
|
+
(`config.invalid_argument`).
|
|
43
|
+
- **ScheduleWakeup honesty family** (#424 A-1/A-4): the wakeup intent carries `lifetime:"session"`
|
|
44
|
+
where the backend vouched for the reap contract; an undeclared backend keeps the historic durable
|
|
45
|
+
intent with receipt + description disclosure; `reason` rides `details.reason`. An incomplete
|
|
46
|
+
replace (stale wakeup possibly still armed) is disclosed on the receipt.
|
|
47
|
+
- **SendMessage completion promises follow mount truth** (#424 A-6): `notificationWired?` / `oneShot?`
|
|
48
|
+
seats (defaults preserve wired behavior byte-identical); the unwired arm teaches retrieval, the
|
|
49
|
+
one-shot arm instructs blocking retrieval now; ten sites across two wordings guarded.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
- **Derived-route pre-flight can never sink the main task** (pre-ship rescan P1): the judge call and
|
|
53
|
+
the whole seat (including URL normalization of garbage catalog values) now abstain on throw instead
|
|
54
|
+
of failing task preparation and leaking the acquired session view; both derived seats share the
|
|
55
|
+
guarded helper.
|
|
56
|
+
- **Route URL comparison is wire-equivalent** (rescan): scheme/host case-fold + default-port elision
|
|
57
|
+
(path half stays byte-ordered; parse failure falls back to raw) — case-only respellings of the same
|
|
58
|
+
endpoint no longer refuse as `credential_mismatch`.
|
|
59
|
+
- **MCP revocation copy tells the standing truth at all four outlets** (rescan; #424 A-5 completed):
|
|
60
|
+
the ListMcpResources arm joins the other three ("stays on this run's roster; every call refused
|
|
61
|
+
the same way"), with an extinction tripwire for the old sentence.
|
|
62
|
+
- **AgentHarness auth carriers refuse loudly at the two host seats** (rescan hardening): a
|
|
63
|
+
deployment-wide auth header in `streamOptions.headers` is refused with a migration hint instead of
|
|
64
|
+
silently riding beside per-model credentials; `stripAuthHeaders` retired (zero callers).
|
|
65
|
+
- **ExitWorktree parameter texts describe this tool** (#424 A-8): detached worktrees have no branch;
|
|
66
|
+
a changed tree is KEPT (not refused) — CC originals and reasoning recorded at the schema.
|
|
67
|
+
- **Read-only memory notice restores CC's recall-framing paragraph** (#424 A-9, byte-compared).
|
|
68
|
+
- **MCP/scheduler small truths** (rescan): the wakeup mount note is led by the `supportsSessionWakeup:false`
|
|
69
|
+
arm; an explicitly-declared `supportsSessionLifetime:false` backend behaves like an undeclared one
|
|
70
|
+
(seam defines the two spellings as one state — the refusal arm from an unshipped interim build rolled back).
|
|
71
|
+
|
|
72
|
+
### Bench (not part of the package surface)
|
|
73
|
+
- LLM distillation arm for the memory benchmark (three-clause instruction arc: retained 20.8%→100%,
|
|
74
|
+
convergence restored; distillation quality flat with the hand-authored ceiling on this corpus's
|
|
75
|
+
measurable axes); live provider key no longer travels to the loopback observation plane.
|
|
76
|
+
|
|
77
|
+
## 5.56.0 — 2026-08-23
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
- **A2A covering rules join the mcp form** (#410, ruled): `a2a__peer` / `a2a__peer__*` deny, ask
|
|
81
|
+
and allow now fire in all five name-keyed lanes (rule DSL, allow/deny policy, approval policy,
|
|
82
|
+
frozen projection replay, persisted session rules) via the protocol-table-driven
|
|
83
|
+
`namespacedRuleNameCovers`. **Narrowings**: a frozen projection replayed under 5.56 honors
|
|
84
|
+
covering spellings it previously read as inert (heaviest); covering-name parenthesised rules
|
|
85
|
+
refuse (`unsupported.covering_paren`, additive union member); empty-peer spellings refuse at
|
|
86
|
+
construction. **Widening / false-refusal repair**: real minted names with separator-bearing tool
|
|
87
|
+
segments (`mcp__srv____foo` and twins) that previously THREW at construction now compile as
|
|
88
|
+
exact entries (the name-set screen judges the peer boundary only).
|
|
89
|
+
- **Delegation entry caps cover every local spawn leg** (#385, anchor campaign): sync spawn,
|
|
90
|
+
sync fork and background fork now pass the same concurrency/cumulative gate as background
|
|
91
|
+
spawns (one pool, four admit sites; in-flight reservations returned on settle). Sync legs mint
|
|
92
|
+
`delegation.concurrency_cap` / `delegation.session_cap` under the `Sub-agent not started: `
|
|
93
|
+
prefix (new code+prefix combination); counted nouns drop the word "background";
|
|
94
|
+
`AgentDefinition.background: true` is honored (previously a dead field); no-principal mounts
|
|
95
|
+
fold to the engine default pool instead of skipping the gate.
|
|
96
|
+
- **Every deployment hook seat is time-bounded** (#388, anchor campaign): the ten `Hooks` seats
|
|
97
|
+
ride one bounded family (default 600s; `Hooks.timeoutMs` knob, bad values refused loudly).
|
|
98
|
+
Decision seats fail closed on expiry (preToolUse → deny + permissionDenied(source "hook");
|
|
99
|
+
userPromptSubmit → block; stop → run ends); observation seats discard and shrink to a 1s grace
|
|
100
|
+
after task abort; hook contexts carry `signal` and live tracked `cwd`. Pre-ship rescan closures:
|
|
101
|
+
the runSpec hook mirror now carries `timeoutMs` (a deployment's bound was silently replaced by
|
|
102
|
+
the default on composed runs); stopFailure reads the deadline rather than a constant grace; the
|
|
103
|
+
post-abort grace caps remaining time instead of restarting the clock.
|
|
104
|
+
- **interrupt() keeps accepted user input; an interrupt leaves CC's transcript marker** (#389,
|
|
105
|
+
anchor campaign): steer queues survive `interrupt()` (one-way abort latch + in-flight slot);
|
|
106
|
+
a durable park migrates queued steers onto the checkpoint (idempotent CAS verb) and the
|
|
107
|
+
undrained account announces only the remainder; an explicit interrupt that won the abort mints
|
|
108
|
+
CC's verbatim `[Request interrupted by user]` marker (double-gated); unknown
|
|
109
|
+
`SystemInjectionPriority` values refuse and the unimplemented `"now"` ladder is disclosed once
|
|
110
|
+
per run (`task.injection_priority_unimplemented`); the untrusted caller steer lane gains CC's
|
|
111
|
+
mid-turn delivery frame.
|
|
112
|
+
- **A SendMessage name that changed hands refuses instead of silently redirecting** (CC's
|
|
113
|
+
`send_message_pin_guard` rebound arm): "Nothing was sent." plus both ways forward; a precise
|
|
114
|
+
task_id is never refused and re-pins; the tool description states the refusal.
|
|
115
|
+
- **A legacy-decided parked row converges** (#400, test P0-KPI): the live reconcile's resolved
|
|
116
|
+
skip now means "a claim is in flight" (read from the store row) — a checkpoint decided by the
|
|
117
|
+
task-level legacy lane settles the row instead of leaving a process-lifetime zombie; the
|
|
118
|
+
decision is re-read immediately before the verdict so a reopen CAS is never judged dead.
|
|
119
|
+
- **Compaction PTL retries disclose what they dropped** (#407, test P0-KPI):
|
|
120
|
+
`CompactionDetails.unsummarizedMessages` accumulates the true uncovered count across split
|
|
121
|
+
legs, with a shell-copy clause and an import-gate check. The summary prompt carries CC's
|
|
122
|
+
`<example>` skeleton and a compliant `<summary>` envelope is unwrapped in the persisted body
|
|
123
|
+
(pre-ship rescan closure: a fence-only EMPTY response is refused loudly, same class and wording
|
|
124
|
+
as the bare-empty arm — it could previously persist a footer-only summary).
|
|
125
|
+
`keepRecentToolResults` floors at 1 and refuses NaN/negative loudly.
|
|
126
|
+
- **#384 (keepRecentTokens default) closed as REFUTED, with evidence**: the anchor-campaign claim
|
|
127
|
+
that the 0 default meant "no kept tail" was disproved on a real Runner — `findCutPoint` has a
|
|
128
|
+
structural floor, the kept tail is never empty, and the pinned behavior matches CC's posture
|
|
129
|
+
via the other knob. The default is unchanged; the floor is now pinned
|
|
130
|
+
(test/compaction-keep-tail-floor.test.ts) and four stale anchor comments were corrected.
|
|
131
|
+
|
|
132
|
+
### Notes
|
|
133
|
+
- Anchor-campaign satellite dispositions ride along (A3/A4/A8/A14 candidate tables in the
|
|
134
|
+
respective cars' receipts): truthfulness rewrites, registry entries, and the
|
|
135
|
+
PermissionDenied trigger-surface divergence registered.
|
|
136
|
+
- Erratum owed to sibling repos (also stated in src/core/hooks.ts): "CC fails open on hook
|
|
137
|
+
faults except exit 2" is true only of the command(shell) hook channel; CC's in-process
|
|
138
|
+
callback channel is fail-closed with dedicated copy — cited as a fail-open precedent it is
|
|
139
|
+
the wrong channel.
|
|
140
|
+
- Residuals filed, not shipped silently: #417-#423 (auth seats, Agent(<type>) governance,
|
|
141
|
+
lifecycle legs, postToolUse payload asymmetry, reopen fence, classic-arm Trust-but-verify).
|
|
142
|
+
|
|
3
143
|
## 5.55.0 — 2026-08-23
|
|
4
144
|
|
|
5
145
|
### BREAKING
|
|
@@ -33,6 +33,17 @@ export interface SendMessageToolOptions {
|
|
|
33
33
|
notify?: (n: TaskNotificationPayload, opts?: {
|
|
34
34
|
priority?: "now" | "next" | "later";
|
|
35
35
|
}) => void;
|
|
36
|
+
/** Whether a completion notification really reaches the sender — drives every "you will be notified"
|
|
37
|
+
* sentence on this face (description + four receipts), mirroring TaskOutput's `notificationWired`.
|
|
38
|
+
* Default = `notify !== undefined`, which is the truth on both the first-party mount and a direct
|
|
39
|
+
* mount that wires its own sink here. Pass `true` only when the deployment announces a finished
|
|
40
|
+
* agent through a channel of its own (this tool then keeps the wired wording); `false` states the
|
|
41
|
+
* degraded truth explicitly. */
|
|
42
|
+
notificationWired?: boolean;
|
|
43
|
+
/** RB-220 mirror of {@link import("../core/types.js").TaskSpec.oneShot} (the Runner mount fills it):
|
|
44
|
+
* this run's process exits when the turn ends, so no completion notification can ever land — checked
|
|
45
|
+
* AHEAD of {@link notificationWired}, since no amount of wiring makes a later turn exist. */
|
|
46
|
+
oneShot?: boolean;
|
|
36
47
|
/** Steer-handle sink: the revived run re-emits a FRESH handle (design/122 risk-table contract). */
|
|
37
48
|
sink?: (handle: SubagentSteerHandle) => void;
|
|
38
49
|
/**
|
|
@@ -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,50 @@ 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 completionMode = opts.oneShot === true ? "one_shot" : (opts.notificationWired ?? opts.notify !== undefined) ? "notified" : "silent";
|
|
50
|
+
const notifyWired = completionMode === "notified";
|
|
51
|
+
const NO_COMPLETION_NOTICE = completionMode === "one_shot"
|
|
52
|
+
? "this is a ONE-SHOT submission — there is no later turn for a completion notification to land in, so retrieve its result NOW with TaskOutput(task_id, block: true) rather than ending your turn"
|
|
53
|
+
: "its completion is NOT announced on this mount — retrieve its status and result with TaskOutput(task_id) where mounted";
|
|
54
|
+
const AWAIT_COMPLETION = notifyWired
|
|
55
|
+
? "Wait for its completion notification"
|
|
56
|
+
: completionMode === "one_shot"
|
|
57
|
+
? "Wait for it NOW with TaskOutput(task_id, block: true) — this one-shot submission ends with this turn"
|
|
58
|
+
: "Check for its completion with TaskOutput(task_id) where mounted";
|
|
59
|
+
const awaitCompletion = notifyWired
|
|
60
|
+
? "wait for its completion notification"
|
|
61
|
+
: completionMode === "one_shot"
|
|
62
|
+
? "wait for it NOW with TaskOutput(task_id, block: true) — this one-shot submission ends with this turn"
|
|
63
|
+
: "check for its completion with TaskOutput(task_id) where mounted";
|
|
64
|
+
const sendMessagePins = new Map();
|
|
65
|
+
const pinGuard = (targetId, targetName, rung, to) => {
|
|
66
|
+
if (rung === "other")
|
|
67
|
+
return undefined;
|
|
68
|
+
const pinKey = normalizeAgentName(targetName ?? "");
|
|
69
|
+
if (pinKey === "")
|
|
70
|
+
return undefined;
|
|
71
|
+
const pinned = sendMessagePins.get(pinKey);
|
|
72
|
+
if (pinned === undefined || pinned.id === targetId) {
|
|
73
|
+
sendMessagePins.set(pinKey, { id: targetId, name: targetName ?? to });
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
if (rung === "ref") {
|
|
77
|
+
sendMessagePins.set(pinKey, { id: targetId, name: targetName ?? to });
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
if (targetName !== undefined && normalizeAgentName(to) === pinKey && normalizeAgentName(pinned.name) !== pinKey) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
content: `Message not sent: "${inlineUntrusted(to, 120)}" now resolves to a different agent than it did earlier in ` +
|
|
85
|
+
`this run — this name previously resolved to ${pinned.id.slice(0, 64)}, which it no longer reaches. Nothing was sent.\n` +
|
|
86
|
+
`It now resolves to ${targetId.slice(0, 64)}. To message that agent, re-send with its task_id: ` +
|
|
87
|
+
`{"to": "${targetId.slice(0, 64)}", …}. To reach the earlier agent, use its task_id ` +
|
|
88
|
+
`${pinned.id.slice(0, 64)} from its spawn result.`,
|
|
89
|
+
details: { error: "rebound", to, taskId: targetId, previousTaskId: pinned.id },
|
|
90
|
+
isError: true,
|
|
91
|
+
};
|
|
92
|
+
};
|
|
48
93
|
return defineTool({
|
|
49
94
|
name: SEND_MESSAGE_TOOL_NAME,
|
|
50
95
|
contract: { contractId: "core.send_message@1", implementationRevision: "1" },
|
|
@@ -54,9 +99,13 @@ export function createSendMessageTool(opts) {
|
|
|
54
99
|
`'to' is the agent's name (set at spawn via Agent({name})) or its task_id (a…). A RUNNING agent receives the ` +
|
|
55
100
|
`message at its next turn (queued — never interrupts its current work); a FINISHED agent resumes as a new ` +
|
|
56
101
|
`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
|
-
`
|
|
59
|
-
|
|
102
|
+
`keep working after an agent completes; but if a name you already messaged is later taken by a NEWER agent, the ` +
|
|
103
|
+
`send is refused rather than silently redirected — re-send with the task_id of whichever agent you meant. ` +
|
|
104
|
+
(notifyWired
|
|
105
|
+
? `You will be notified automatically when it completes — prefer ending your turn; do not poll. `
|
|
106
|
+
: completionMode === "one_shot"
|
|
107
|
+
? `This is a ONE-SHOT submission: there is no later turn for a completion notification to land in, so do NOT end your turn expecting one — wait actively with TaskOutput(task_id, block: true) when you need a continued agent's result. `
|
|
108
|
+
: `A finished agent is NOT announced on this mount — check on it with TaskOutput(task_id) where mounted rather than ending your turn to wait for a notification that never comes. `) +
|
|
60
109
|
(tier3Capable
|
|
61
110
|
? `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.`
|
|
62
111
|
: `Continuing a finished agent requires the run to retain sub-agent sessions; when the session was not ` +
|
|
@@ -224,7 +273,7 @@ export function createSendMessageTool(opts) {
|
|
|
224
273
|
if (row.status === "running") {
|
|
225
274
|
if (row.writerId === opts.registry.writerId) {
|
|
226
275
|
return {
|
|
227
|
-
content: `Message not sent: ${whoT3} is currently running in this process but its mid-run delivery channel is not reachable from here.
|
|
276
|
+
content: `Message not sent: ${whoT3} is currently running in this process but its mid-run delivery channel is not reachable from here. ${AWAIT_COMPLETION}, then send again to continue it.`,
|
|
228
277
|
details: { error: "still_running", to },
|
|
229
278
|
isError: true,
|
|
230
279
|
};
|
|
@@ -237,7 +286,7 @@ export function createSendMessageTool(opts) {
|
|
|
237
286
|
};
|
|
238
287
|
}
|
|
239
288
|
return {
|
|
240
|
-
content: `Message not sent: ${whoT3} is running on another host — this process has no delivery channel to it.
|
|
289
|
+
content: `Message not sent: ${whoT3} is running on another host — this process has no delivery channel to it. ${AWAIT_COMPLETION}, then send again to continue it.`,
|
|
241
290
|
details: { error: "running_elsewhere", to },
|
|
242
291
|
isError: true,
|
|
243
292
|
};
|
|
@@ -470,7 +519,9 @@ export function createSendMessageTool(opts) {
|
|
|
470
519
|
const priorCount = lease.messages.length - 1;
|
|
471
520
|
return {
|
|
472
521
|
content: `Message sent — ${whoT3} was revived in the background with its prior context intact${priorCount > 0 ? ` (${priorCount} earlier pending message(s) delivered with it)` : ""}. ` +
|
|
473
|
-
|
|
522
|
+
(notifyWired
|
|
523
|
+
? `You will be notified automatically when it completes. Continue with other work — do not poll.`
|
|
524
|
+
: `${NO_COMPLETION_NOTICE.charAt(0).toUpperCase()}${NO_COMPLETION_NOTICE.slice(1)}.`),
|
|
474
525
|
details: { type: "send-message", status: "revived", to, task_id: handle, seq: nextSeq, ...(priorCount > 0 ? { priorMessages: priorCount } : {}) },
|
|
475
526
|
};
|
|
476
527
|
}
|
|
@@ -523,6 +574,11 @@ export function createSendMessageTool(opts) {
|
|
|
523
574
|
}
|
|
524
575
|
}
|
|
525
576
|
if (rosterHit !== undefined) {
|
|
577
|
+
{
|
|
578
|
+
const guard = pinGuard(rosterHit.agentId, to, "name", to);
|
|
579
|
+
if (guard !== undefined)
|
|
580
|
+
return guard;
|
|
581
|
+
}
|
|
526
582
|
const revived = await tier3Revive(rosterHit.agentId);
|
|
527
583
|
if (revived !== undefined)
|
|
528
584
|
return revived;
|
|
@@ -563,6 +619,16 @@ export function createSendMessageTool(opts) {
|
|
|
563
619
|
if (row.type !== "background_agent") {
|
|
564
620
|
return { content: `Message not sent: ${targetId} is a ${row.type} task, not a background agent.`, details: { error: "wrong_type", to }, isError: true };
|
|
565
621
|
}
|
|
622
|
+
{
|
|
623
|
+
const rung = idRow !== undefined
|
|
624
|
+
? "ref"
|
|
625
|
+
: row.name !== undefined && (row.name === to || normalizeAgentName(row.name) === normalizeAgentName(to))
|
|
626
|
+
? "name"
|
|
627
|
+
: "other";
|
|
628
|
+
const guard = pinGuard(targetId, row.name, rung, to);
|
|
629
|
+
if (guard !== undefined)
|
|
630
|
+
return guard;
|
|
631
|
+
}
|
|
566
632
|
if (row.status === "running" || row.status === "pending") {
|
|
567
633
|
return await withTargetLane(targetLaneKey(access.scope, targetId), async () => {
|
|
568
634
|
const liveRecipientKey = peerAxisToken(resolvedAccess.scope, "h", targetId);
|
|
@@ -582,10 +648,10 @@ export function createSendMessageTool(opts) {
|
|
|
582
648
|
}, { priority: "next" });
|
|
583
649
|
if (delivered.ok) {
|
|
584
650
|
const receiptText = delivered.disposition === "parked"
|
|
585
|
-
? `Message parked for ${who}: the agent finished before reading it — it will be delivered when the agent is next continued. You will be notified of its completion; continue with other work.`
|
|
651
|
+
? `Message parked for ${who}: the agent finished before reading it — it will be delivered when the agent is next continued. ${notifyWired ? "You will be notified of its completion; continue with other work." : `Note: ${NO_COMPLETION_NOTICE}.`}`
|
|
586
652
|
: delivered.disposition === "pending"
|
|
587
|
-
? `Message accepted for ${who} but delivery is UNCONFIRMED (its channel did not confirm within the wait window) — it stays queued and will deliver if the channel binds. You will be notified of the agent's completion either way; resend then if it went unanswered. ${DEDUP_RETRY_NOTE}`
|
|
588
|
-
: `Message queued for delivery to ${who} at its next turn. If it finishes before reading it, the message may not survive — you will be notified of its completion either way; resend then if it went unanswered. Continue with other work; do not poll. ${DEDUP_RETRY_NOTE}`;
|
|
653
|
+
? `Message accepted for ${who} but delivery is UNCONFIRMED (its channel did not confirm within the wait window) — it stays queued and will deliver if the channel binds. ${notifyWired ? "You will be notified of the agent's completion either way" : `Note: ${NO_COMPLETION_NOTICE}`}; resend then if it went unanswered. ${DEDUP_RETRY_NOTE}`
|
|
654
|
+
: `Message queued for delivery to ${who} at its next turn. If it finishes before reading it, the message may not survive — ${notifyWired ? "you will be notified of its completion either way" : NO_COMPLETION_NOTICE}; resend then if it went unanswered. ${notifyWired ? "Continue with other work; do not poll. " : ""}${DEDUP_RETRY_NOTE}`;
|
|
589
655
|
return {
|
|
590
656
|
content: receiptText,
|
|
591
657
|
details: { type: "send-message", status: "delivered_running", disposition: delivered.disposition, to, task_id: targetId, summary },
|
|
@@ -593,7 +659,7 @@ export function createSendMessageTool(opts) {
|
|
|
593
659
|
}
|
|
594
660
|
if (delivered.reason === "no_channel") {
|
|
595
661
|
return {
|
|
596
|
-
content: `Message not sent: ${who} is still running and this deployment has no mid-run delivery channel for it.
|
|
662
|
+
content: `Message not sent: ${who} is still running and this deployment has no mid-run delivery channel for it. ${AWAIT_COMPLETION}, then SendMessage to continue it. ${DEDUP_RETRY_NOTE}`,
|
|
597
663
|
details: { error: "still_running", to },
|
|
598
664
|
isError: true,
|
|
599
665
|
};
|
|
@@ -659,7 +725,7 @@ export function createSendMessageTool(opts) {
|
|
|
659
725
|
}
|
|
660
726
|
if (ledger.get(resumeToolUseId)?.running === true) {
|
|
661
727
|
return {
|
|
662
|
-
content: `Message not sent: ${who} (or a prior follow-up to it) is still running —
|
|
728
|
+
content: `Message not sent: ${who} (or a prior follow-up to it) is still running — ${awaitCompletion}.`,
|
|
663
729
|
details: { error: "steering.still_running", to },
|
|
664
730
|
isError: true,
|
|
665
731
|
};
|
|
@@ -715,7 +781,9 @@ export function createSendMessageTool(opts) {
|
|
|
715
781
|
const marker = await resume(`${fromPrefix}[${safeSummary}] ${safeMessage}`);
|
|
716
782
|
return {
|
|
717
783
|
content: `Message sent — ${who} resumed in the background with its prior context intact (correlation marker [${marker}]).\n` +
|
|
718
|
-
|
|
784
|
+
(notifyWired
|
|
785
|
+
? `You will be notified automatically when it completes; its reply will carry [${marker}]. Continue with other work — do not poll.`
|
|
786
|
+
: `Its reply will carry [${marker}], but ${NO_COMPLETION_NOTICE}.`),
|
|
719
787
|
details: { type: "send-message", status: "resumed", to, task_id: targetId, marker, summary },
|
|
720
788
|
};
|
|
721
789
|
}
|
|
@@ -732,7 +800,7 @@ export function createSendMessageTool(opts) {
|
|
|
732
800
|
? `${who} reached its resume cap (${SUBAGENT_RESUME_CAP} follow-ups per agent) — relaunch a new agent instead.`
|
|
733
801
|
: code === "steering.still_running"
|
|
734
802
|
?
|
|
735
|
-
`${who} (or a prior follow-up to it) is still running —
|
|
803
|
+
`${who} (or a prior follow-up to it) is still running — ${awaitCompletion}. ${DEDUP_RETRY_NOTE}`
|
|
736
804
|
: code === "resume.row_recycling"
|
|
737
805
|
?
|
|
738
806
|
`${who}'s registry row is being adjudicated right now (a revival claim or a reap sweep holds it) — send again in a moment. ${DEDUP_RETRY_NOTE}`
|