@sema-agent/core 5.16.0 → 5.17.0-pre.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 +118 -0
- package/dist/agents/peer-admission.d.ts +58 -0
- package/dist/agents/peer-admission.js +175 -0
- package/dist/agents/retain-ledger.d.ts +1 -1
- package/dist/agents/retain-ledger.js +9 -1
- package/dist/agents/send-message-tool.d.ts +8 -0
- package/dist/agents/send-message-tool.js +171 -21
- package/dist/agents/subagent.d.ts +5 -0
- package/dist/agents/subagent.js +21 -3
- package/dist/core/ask-question.js +10 -0
- package/dist/core/mailbox-store.d.ts +2 -0
- package/dist/core/mailbox-store.js +2 -2
- package/dist/core/runner/prepare-task.d.ts +3 -0
- package/dist/core/runner/prepare-task.js +92 -25
- package/dist/core/runner/runtask.js +10 -0
- package/dist/core/shared-memory/contract.d.ts +17 -0
- package/dist/core/shared-memory/contract.js +138 -0
- package/dist/core/shared-memory/normalize.d.ts +73 -0
- package/dist/core/shared-memory/normalize.js +259 -0
- package/dist/core/shared-memory/tools.d.ts +7 -0
- package/dist/core/shared-memory/tools.js +289 -0
- package/dist/core/shared-memory/types.d.ts +95 -0
- package/dist/core/shared-memory/types.js +18 -0
- package/dist/core/task-notification.d.ts +3 -0
- package/dist/core/task-registry-agent.d.ts +1 -1
- package/dist/core/task-registry-agent.js +1 -1
- package/dist/core/task-registry.d.ts +1 -1
- package/dist/core/task-registry.js +2 -0
- package/dist/core/types.d.ts +7 -0
- package/dist/core/untrusted-text.d.ts +1 -0
- package/dist/core/untrusted-text.js +10 -0
- package/dist/engine/harness/agent-harness.d.ts +1 -0
- package/dist/engine/harness/agent-harness.js +21 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/stores/cc/mailbox-store.js +4 -0
- package/dist/stores/file/mailbox-store.js +2 -2
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ import { canAccessAgentRecord, clearRevivedRowTerminalPayload } from "../core/ba
|
|
|
5
5
|
import { escapeAttributeValue, escapeEnvelopeTag, isObserverTaskId, OBSERVER_SENDMESSAGE_SENDER_REFUSAL, OBSERVER_SENDMESSAGE_TARGET_REFUSAL, } from "./observer.js";
|
|
6
6
|
import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getSessionRetainLedger } from "./retain-ledger.js";
|
|
7
7
|
import { createSubagentResume } from "./subagent.js";
|
|
8
|
+
import { appendHopToken, judgePeerAdmission, peerAxisToken, resolvePeerAdmissionConfig, PEER_MESSAGE_NOTICE, } from "./peer-admission.js";
|
|
8
9
|
export const SEND_MESSAGE_TOOL_NAME = "SendMessage";
|
|
9
10
|
let uplinkSeqGlobal = Date.now();
|
|
10
11
|
const UPLINK_RESULT_MAX = 8000;
|
|
@@ -104,23 +105,55 @@ export function createSendMessageTool(opts) {
|
|
|
104
105
|
if (isObserverTaskId(to)) {
|
|
105
106
|
return { content: OBSERVER_SENDMESSAGE_TARGET_REFUSAL, details: { error: "observer_target", to }, isError: true };
|
|
106
107
|
}
|
|
108
|
+
const admissionConfig = resolvePeerAdmissionConfig(opts.admission);
|
|
109
|
+
const guardScope = ctx.principal ?? opts.scope;
|
|
110
|
+
const selfRef = ctx.peerSelfRef ?? opts.peerSelf;
|
|
111
|
+
const directMountSessionId = ctx.sessionId ?? opts.sessionId;
|
|
112
|
+
const directMountTaskId = ctx.taskId ?? opts.owner;
|
|
113
|
+
const senderKey = selfRef?.current.key ??
|
|
114
|
+
(directMountSessionId !== undefined
|
|
115
|
+
? peerAxisToken(guardScope, "s", directMountSessionId)
|
|
116
|
+
: directMountTaskId !== undefined
|
|
117
|
+
? peerAxisToken(guardScope, "t", directMountTaskId)
|
|
118
|
+
: undefined);
|
|
119
|
+
const inboundChain = (ctx.peerInboundChainRef ?? opts.peerInbound)?.current ?? [];
|
|
120
|
+
const prospectiveChain = appendHopToken(inboundChain, senderKey);
|
|
121
|
+
const admitFor = (recipientKey, ownTokens) => judgePeerAdmission(guardScope, recipientKey, { senderKey, body: message, prospectiveChain, ownTokens }, admissionConfig);
|
|
122
|
+
const DEDUP_RETRY_NOTE = "Note: resending the identical text within the dedup window reads as duplicate — rephrase or wait it out.";
|
|
123
|
+
const admissionRefusal = (reason, whoLabel) => {
|
|
124
|
+
const text = reason === "rate_limited"
|
|
125
|
+
? `you are sending messages to ${whoLabel} faster than its rate limit — the budget refills on its own; slow down and resend in a moment.`
|
|
126
|
+
: reason === "duplicate"
|
|
127
|
+
? `an identical message was already sent to ${whoLabel} moments ago — if this is genuinely new, rephrase it or wait out the dedup window.`
|
|
128
|
+
: reason === "hop_loop"
|
|
129
|
+
? `this message has already passed through ${whoLabel} too many times (a forwarding loop) — stop relaying it; act on it or drop it.`
|
|
130
|
+
: `this message's forwarding chain is too long (runaway relay) — stop relaying it; act on it or drop it.`;
|
|
131
|
+
return { content: `Message not sent: ${text}`, details: { error: reason, to }, isError: true };
|
|
132
|
+
};
|
|
107
133
|
if (normalizeAgentName(to) === "main") {
|
|
108
134
|
if (opts.uplink && senderId !== undefined) {
|
|
135
|
+
const uplinkIdentity = opts.uplinkRecipient?.current;
|
|
136
|
+
if (uplinkIdentity?.key !== undefined) {
|
|
137
|
+
const verdict = admitFor(uplinkIdentity.key, uplinkIdentity.ownTokens);
|
|
138
|
+
if (!verdict.ok)
|
|
139
|
+
return admissionRefusal(verdict.reason, "main");
|
|
140
|
+
}
|
|
109
141
|
try {
|
|
110
142
|
opts.uplink({
|
|
111
143
|
task_id: senderId,
|
|
112
144
|
task_type: "background_agent",
|
|
113
145
|
status: "event",
|
|
114
146
|
summary: `message from ${senderLabel}: ${summary}`,
|
|
115
|
-
result: message.length > UPLINK_RESULT_MAX ? `${message.slice(0, UPLINK_RESULT_MAX)}\n[uplink truncated: ${message.length} chars total — read the agent's transcript for the rest]` : message
|
|
147
|
+
result: `${message.length > UPLINK_RESULT_MAX ? `${message.slice(0, UPLINK_RESULT_MAX)}\n[uplink truncated: ${message.length} chars total — read the agent's transcript for the rest]` : message}\n\n${PEER_MESSAGE_NOTICE}`,
|
|
116
148
|
seq: ++uplinkSeqGlobal,
|
|
149
|
+
peer: { hopChain: prospectiveChain },
|
|
117
150
|
}, { priority: "next" });
|
|
118
151
|
}
|
|
119
152
|
catch (e) {
|
|
120
|
-
return { content: `Message not sent: the parent's notification lane rejected it (${e instanceof Error ? e.message : String(e)})
|
|
153
|
+
return { content: `Message not sent: the parent's notification lane rejected it (${e instanceof Error ? e.message : String(e)}). ${DEDUP_RETRY_NOTE}`, details: { error: "uplink_failed", to }, isError: true };
|
|
121
154
|
}
|
|
122
155
|
return {
|
|
123
|
-
content: `Message sent to main —
|
|
156
|
+
content: `Message sent to main — queued for the spawning conversation at its next turn boundary. If that conversation finishes before reading it, the message may not survive. Continue with your task; do not wait for a reply.`,
|
|
124
157
|
details: { type: "send-message", status: "uplinked", to: "main", seq: uplinkSeqGlobal },
|
|
125
158
|
};
|
|
126
159
|
}
|
|
@@ -207,9 +240,102 @@ export function createSendMessageTool(opts) {
|
|
|
207
240
|
}
|
|
208
241
|
if (row.sessionId === undefined)
|
|
209
242
|
return undefined;
|
|
243
|
+
const t3RecipientKey = peerAxisToken(scope, "h", handle);
|
|
244
|
+
const t3Verdict = admitFor(t3RecipientKey, [t3RecipientKey, peerAxisToken(scope, "s", row.sessionId)]);
|
|
245
|
+
if (!t3Verdict.ok)
|
|
246
|
+
return admissionRefusal(t3Verdict.reason, whoT3);
|
|
247
|
+
let boxFull = false;
|
|
248
|
+
try {
|
|
249
|
+
boxFull = (await opts.mailbox.peekCount(scope, handle)) >= admissionConfig.maxQueuedPeerMessages;
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
}
|
|
253
|
+
if (boxFull) {
|
|
254
|
+
const queueFullReceipt = (guidance) => ({
|
|
255
|
+
content: `Message not sent: ${whoT3}'s mailbox is at its queued-message limit (${admissionConfig.maxQueuedPeerMessages}) — the message was NOT queued. ${guidance} ${DEDUP_RETRY_NOTE}`,
|
|
256
|
+
details: { error: "queue_full", to },
|
|
257
|
+
isError: true,
|
|
258
|
+
});
|
|
259
|
+
if (!opts.registry.beginDurableClaim(handle)) {
|
|
260
|
+
return queueFullReceipt(`Another lifecycle operation currently owns this agent's claim — resend in a moment.`);
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const now = Date.now();
|
|
264
|
+
const nextSeq = (row.seq ?? 1) + 1;
|
|
265
|
+
const claimed = { ...row, status: "running", writerId: opts.registry.writerId, writerEpoch: (row.writerEpoch ?? 0) + 1, updatedAt: now, seq: nextSeq };
|
|
266
|
+
clearRevivedRowTerminalPayload(claimed);
|
|
267
|
+
let won = false;
|
|
268
|
+
try {
|
|
269
|
+
won = await opts.agentStore.update(handle, scope, claimed, { rev: row.rev });
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
won = false;
|
|
273
|
+
}
|
|
274
|
+
if (!won) {
|
|
275
|
+
return queueFullReceipt(`Another delivery claimed the agent's record first — this send did not start a backlog drain; a later resend will try again.`);
|
|
276
|
+
}
|
|
277
|
+
const claimedRev = row.rev + 1;
|
|
278
|
+
const rollbackDrain = async () => {
|
|
279
|
+
try {
|
|
280
|
+
await opts.agentStore.update(handle, scope, { ...row, updatedAt: Date.now() }, { rev: claimedRev });
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
try {
|
|
286
|
+
await opts.runner.sessions.acquire(row.sessionId, { requireExisting: true });
|
|
287
|
+
}
|
|
288
|
+
catch (e) {
|
|
289
|
+
await rollbackDrain();
|
|
290
|
+
if (e?.code === "not_found") {
|
|
291
|
+
return queueFullReceipt(`Its transcript session no longer exists (evicted or reaped), so a revival cannot drain the backlog.`);
|
|
292
|
+
}
|
|
293
|
+
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.`);
|
|
294
|
+
}
|
|
295
|
+
const drainLeaseOwner = `${opts.registry.writerId}:${ctx.toolCallId}`;
|
|
296
|
+
let drainLease = null;
|
|
297
|
+
try {
|
|
298
|
+
drainLease = await opts.mailbox.claimLease(scope, handle, drainLeaseOwner, REVIVE_LEASE_TTL_MS);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
drainLease = null;
|
|
302
|
+
}
|
|
303
|
+
if (drainLease === null) {
|
|
304
|
+
await rollbackDrain();
|
|
305
|
+
return queueFullReceipt(`No backlog batch was claimable this time — this send did not start a backlog drain; a later resend will try again.`);
|
|
306
|
+
}
|
|
307
|
+
const drainPrompt = `${drainLease.messages.map((m) => frameTeammateMessage({ from: m.from ?? "main", text: m.content })).join("\n")}\n\n${PEER_MESSAGE_NOTICE}`;
|
|
308
|
+
const drainSeed = drainLease.messages[drainLease.messages.length - 1]?.hopChain ?? [];
|
|
309
|
+
let drainSpawned;
|
|
310
|
+
try {
|
|
311
|
+
drainSpawned = await opts.reviveSpawn({ row: claimed, rev: claimedRev, prompt: drainPrompt, peerSeed: { hopChain: [...drainSeed] } });
|
|
312
|
+
}
|
|
313
|
+
catch (e) {
|
|
314
|
+
drainSpawned = { isError: true, content: e instanceof Error ? e.message : String(e) };
|
|
315
|
+
}
|
|
316
|
+
if (drainSpawned.isError === true) {
|
|
317
|
+
try {
|
|
318
|
+
await opts.mailbox.releaseLease(scope, handle, drainLeaseOwner);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
}
|
|
322
|
+
await rollbackDrain();
|
|
323
|
+
return queueFullReceipt(`Reviving it to drain the backlog failed (${drainSpawned.content}) — this send did not start a backlog drain; a later resend will try again.`);
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
await opts.mailbox.ack(scope, handle, drainLeaseOwner, drainLease.maxSeq);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
}
|
|
330
|
+
return queueFullReceipt(`The agent is being revived to digest its ${drainLease.messages.length} queued message(s) — resend after it settles.`);
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
opts.registry.endDurableClaim(handle);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
210
336
|
if (!opts.registry.beginDurableClaim(handle)) {
|
|
211
337
|
return {
|
|
212
|
-
content: `Message not sent: ${whoT3} is being revived or recycled right now — send again in a moment
|
|
338
|
+
content: `Message not sent: ${whoT3} is being revived or recycled right now — send again in a moment. ${DEDUP_RETRY_NOTE}`,
|
|
213
339
|
details: { error: "claim_contended", to },
|
|
214
340
|
isError: true,
|
|
215
341
|
};
|
|
@@ -235,13 +361,13 @@ export function createSendMessageTool(opts) {
|
|
|
235
361
|
}
|
|
236
362
|
if (fresh?.status === "running") {
|
|
237
363
|
return {
|
|
238
|
-
content: `Message not sent: ${whoT3} was just revived by another delivery — it is running now. Send again to reach the running agent
|
|
364
|
+
content: `Message not sent: ${whoT3} was just revived by another delivery — it is running now. Send again to reach the running agent. ${DEDUP_RETRY_NOTE}`,
|
|
239
365
|
details: { error: "claim_lost", to },
|
|
240
366
|
isError: true,
|
|
241
367
|
};
|
|
242
368
|
}
|
|
243
369
|
return {
|
|
244
|
-
content: `Message not sent: ${whoT3}'s durable record changed underneath this delivery — send again
|
|
370
|
+
content: `Message not sent: ${whoT3}'s durable record changed underneath this delivery — send again. ${DEDUP_RETRY_NOTE}`,
|
|
245
371
|
details: { error: "claim_lost", to },
|
|
246
372
|
isError: true,
|
|
247
373
|
};
|
|
@@ -267,19 +393,19 @@ export function createSendMessageTool(opts) {
|
|
|
267
393
|
};
|
|
268
394
|
}
|
|
269
395
|
return {
|
|
270
|
-
content: `Message not sent: ${whoT3}'s transcript store did not answer (${e instanceof Error ? e.message : String(e)}) — nothing was parked; send again
|
|
396
|
+
content: `Message not sent: ${whoT3}'s transcript store did not answer (${e instanceof Error ? e.message : String(e)}) — nothing was parked; send again. ${DEDUP_RETRY_NOTE}`,
|
|
271
397
|
details: { error: "session_store_unavailable", to },
|
|
272
398
|
isError: true,
|
|
273
399
|
};
|
|
274
400
|
}
|
|
275
401
|
const clipped = clipCarrierMessage(message);
|
|
276
402
|
try {
|
|
277
|
-
await opts.mailbox.append(scope, handle, { from: senderLabel, content: `[${summary}] ${clipped}`, sentAt: now });
|
|
403
|
+
await opts.mailbox.append(scope, handle, { from: senderLabel, content: `[${summary}] ${clipped}`, sentAt: now, hopChain: prospectiveChain });
|
|
278
404
|
}
|
|
279
405
|
catch (e) {
|
|
280
406
|
await rollback();
|
|
281
407
|
return {
|
|
282
|
-
content: `Message not sent: the durable mailbox refused the message (${e instanceof Error ? e.message : String(e)}) — nothing was parked and the agent was not revived
|
|
408
|
+
content: `Message not sent: the durable mailbox refused the message (${e instanceof Error ? e.message : String(e)}) — nothing was parked and the agent was not revived. ${DEDUP_RETRY_NOTE}`,
|
|
283
409
|
details: { error: "mailbox_failed", to },
|
|
284
410
|
isError: true,
|
|
285
411
|
};
|
|
@@ -299,12 +425,13 @@ export function createSendMessageTool(opts) {
|
|
|
299
425
|
details: { error: "mailbox_leased", to },
|
|
300
426
|
};
|
|
301
427
|
}
|
|
302
|
-
const revivePrompt = lease.messages
|
|
428
|
+
const revivePrompt = `${lease.messages
|
|
303
429
|
.map((m) => frameTeammateMessage({ from: m.from ?? "main", text: m.content }))
|
|
304
|
-
.join("\n")
|
|
430
|
+
.join("\n")}\n\n${PEER_MESSAGE_NOTICE}`;
|
|
431
|
+
const reviveSeed = lease.messages[lease.messages.length - 1]?.hopChain ?? [];
|
|
305
432
|
let spawned;
|
|
306
433
|
try {
|
|
307
|
-
spawned = await opts.reviveSpawn({ row: claimed, rev: claimedRev, prompt: revivePrompt });
|
|
434
|
+
spawned = await opts.reviveSpawn({ row: claimed, rev: claimedRev, prompt: revivePrompt, peerSeed: { hopChain: [...reviveSeed] } });
|
|
308
435
|
}
|
|
309
436
|
catch (e) {
|
|
310
437
|
spawned = { isError: true, content: e instanceof Error ? e.message : String(e), details: { error: "revive_failed" } };
|
|
@@ -418,8 +545,12 @@ export function createSendMessageTool(opts) {
|
|
|
418
545
|
}
|
|
419
546
|
if (row.status === "running" || row.status === "pending") {
|
|
420
547
|
return await withTargetLane(targetLaneKey(access.scope, targetId), async () => {
|
|
548
|
+
const liveRecipientKey = peerAxisToken(resolvedAccess.scope, "h", targetId);
|
|
549
|
+
const liveVerdict = admitFor(liveRecipientKey, [liveRecipientKey]);
|
|
550
|
+
if (!liveVerdict.ok)
|
|
551
|
+
return admissionRefusal(liveVerdict.reason, who);
|
|
421
552
|
const s2Clipped = clipCarrierMessage(message);
|
|
422
|
-
const teammateXml = frameTeammateMessage({ from: senderLabel, summary, text: s2Clipped })
|
|
553
|
+
const teammateXml = `${frameTeammateMessage({ from: senderLabel, summary, text: s2Clipped })}\n\n${PEER_MESSAGE_NOTICE}`;
|
|
423
554
|
const delivered = await opts.registry.deliverToRunningAgent(targetId, resolvedAccess, {
|
|
424
555
|
task_id: senderLabel,
|
|
425
556
|
task_type: "background_agent",
|
|
@@ -427,13 +558,14 @@ export function createSendMessageTool(opts) {
|
|
|
427
558
|
summary: `message from ${senderLabel}: ${summary}`,
|
|
428
559
|
result: teammateXml,
|
|
429
560
|
seq: ++uplinkSeqGlobal,
|
|
561
|
+
peer: { hopChain: prospectiveChain },
|
|
430
562
|
}, { priority: "next" });
|
|
431
563
|
if (delivered.ok) {
|
|
432
564
|
const receiptText = delivered.disposition === "parked"
|
|
433
565
|
? `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.`
|
|
434
566
|
: delivered.disposition === "pending"
|
|
435
|
-
? `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
|
|
436
|
-
: `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
|
|
567
|
+
? `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}`
|
|
568
|
+
: `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}`;
|
|
437
569
|
return {
|
|
438
570
|
content: receiptText,
|
|
439
571
|
details: { type: "send-message", status: "delivered_running", disposition: delivered.disposition, to, task_id: targetId, summary },
|
|
@@ -441,15 +573,22 @@ export function createSendMessageTool(opts) {
|
|
|
441
573
|
}
|
|
442
574
|
if (delivered.reason === "no_channel") {
|
|
443
575
|
return {
|
|
444
|
-
content: `Message not sent: ${who} is still running and this deployment has no mid-run delivery channel for it. Wait for its completion notification, then SendMessage to continue it
|
|
576
|
+
content: `Message not sent: ${who} is still running and this deployment has no mid-run delivery channel for it. Wait for its completion notification, then SendMessage to continue it. ${DEDUP_RETRY_NOTE}`,
|
|
445
577
|
details: { error: "still_running", to },
|
|
446
578
|
isError: true,
|
|
447
579
|
};
|
|
448
580
|
}
|
|
581
|
+
if (delivered.reason === "queue_full") {
|
|
582
|
+
return {
|
|
583
|
+
content: `Message not sent: ${who} is still starting up and its startup message buffer is full — resend in a moment. ${DEDUP_RETRY_NOTE}`,
|
|
584
|
+
details: { error: "queue_full", to },
|
|
585
|
+
isError: true,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
449
588
|
const nowRow = opts.registry.getAccessibleTask(targetId, resolvedAccess);
|
|
450
589
|
if (nowRow?.status === "parked") {
|
|
451
590
|
return {
|
|
452
|
-
content: `Message not sent: ${who} is parked on a pending approval — it resumes when the approval is decided, not by message delivery. Send again after it resumes
|
|
591
|
+
content: `Message not sent: ${who} is parked on a pending approval — it resumes when the approval is decided, not by message delivery. Send again after it resumes. ${DEDUP_RETRY_NOTE}`,
|
|
453
592
|
details: { error: "parked_pending_approval", to },
|
|
454
593
|
isError: true,
|
|
455
594
|
};
|
|
@@ -457,12 +596,12 @@ export function createSendMessageTool(opts) {
|
|
|
457
596
|
const stillLive = nowRow !== undefined && (nowRow.status === "running" || nowRow.status === "pending");
|
|
458
597
|
return stillLive
|
|
459
598
|
? {
|
|
460
|
-
content: `Message not sent: ${who} is still starting up (it has not begun reading messages yet) — the message was not delivered. Send again shortly
|
|
599
|
+
content: `Message not sent: ${who} is still starting up (it has not begun reading messages yet) — the message was not delivered. Send again shortly. ${DEDUP_RETRY_NOTE}`,
|
|
461
600
|
details: { error: "settle_race", to },
|
|
462
601
|
isError: true,
|
|
463
602
|
}
|
|
464
603
|
: {
|
|
465
|
-
content: `Message not sent: ${who} just finished (delivery raced its completion). Send again to continue it from its transcript
|
|
604
|
+
content: `Message not sent: ${who} just finished (delivery raced its completion). Send again to continue it from its transcript. ${DEDUP_RETRY_NOTE}`,
|
|
466
605
|
details: { error: "settle_race", to },
|
|
467
606
|
isError: true,
|
|
468
607
|
};
|
|
@@ -506,10 +645,20 @@ export function createSendMessageTool(opts) {
|
|
|
506
645
|
};
|
|
507
646
|
}
|
|
508
647
|
return await withTargetLane(targetLaneKey(access.scope, targetId), async () => {
|
|
648
|
+
const l3Entry = ledger.get(resumeToolUseId);
|
|
649
|
+
const l3RecipientKey = peerAxisToken(access.scope, "h", targetId);
|
|
650
|
+
const l3Verdict = admitFor(l3RecipientKey, [
|
|
651
|
+
l3RecipientKey,
|
|
652
|
+
...(l3Entry?.childSessionId !== undefined ? [peerAxisToken(access.scope, "s", l3Entry.childSessionId)] : []),
|
|
653
|
+
]);
|
|
654
|
+
if (!l3Verdict.ok)
|
|
655
|
+
return admissionRefusal(l3Verdict.reason, who);
|
|
509
656
|
const resume = createSubagentResume({
|
|
510
657
|
ledger,
|
|
511
658
|
parentToolCallId: resumeToolUseId,
|
|
512
659
|
runner: opts.runner,
|
|
660
|
+
peerOrigin: { hopChain: prospectiveChain },
|
|
661
|
+
...(opts.notify !== undefined && selfRef !== undefined ? { currentParentPeerRef: selfRef } : {}),
|
|
513
662
|
...(opts.notify ? { notify: opts.notify } : {}),
|
|
514
663
|
...(opts.sink ? { sink: opts.sink } : {}),
|
|
515
664
|
registry: opts.registry,
|
|
@@ -558,10 +707,11 @@ export function createSendMessageTool(opts) {
|
|
|
558
707
|
: code === "resume.cap"
|
|
559
708
|
? `${who} reached its resume cap (${SUBAGENT_RESUME_CAP} follow-ups per agent) — relaunch a new agent instead.`
|
|
560
709
|
: code === "steering.still_running"
|
|
561
|
-
?
|
|
710
|
+
?
|
|
711
|
+
`${who} (or a prior follow-up to it) is still running — wait for its completion notification. ${DEDUP_RETRY_NOTE}`
|
|
562
712
|
: code === "resume.row_gone"
|
|
563
713
|
? `${who}'s registry row no longer exists (terminal GC) — relaunch a new agent instead.`
|
|
564
|
-
: `${e instanceof Error ? e.message : String(e)}`;
|
|
714
|
+
: `${e instanceof Error ? e.message : String(e)} ${DEDUP_RETRY_NOTE}`;
|
|
565
715
|
return { content: `Message not sent: ${text}`, details: { error: code ?? "resume_failed", to }, isError: true };
|
|
566
716
|
}
|
|
567
717
|
});
|
|
@@ -6,6 +6,7 @@ import type { TaskNotificationPayload } from "../core/task-notification.js";
|
|
|
6
6
|
import { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX } from "../config/defaults.js";
|
|
7
7
|
export { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX };
|
|
8
8
|
import { SubagentRetainLedger } from "./retain-ledger.js";
|
|
9
|
+
import { type PeerSelfRef } from "./peer-admission.js";
|
|
9
10
|
export type { SubagentStep, SubagentEditedFile } from "./subagent-steps.js";
|
|
10
11
|
export declare function notifyResultField(result: string | undefined): string | undefined;
|
|
11
12
|
export declare function inheritedManifestScopeFor(snapshot: readonly unknown[] | undefined): RunInternals["inheritedManifestScope"];
|
|
@@ -100,6 +101,10 @@ export declare function createSubagentResume(deps: {
|
|
|
100
101
|
currentAutoModeReview?: {
|
|
101
102
|
decider: import("../core/auto-mode.js").AutoModeDecider;
|
|
102
103
|
};
|
|
104
|
+
peerOrigin?: {
|
|
105
|
+
hopChain: string[];
|
|
106
|
+
};
|
|
107
|
+
currentParentPeerRef?: PeerSelfRef;
|
|
103
108
|
taskId?: string;
|
|
104
109
|
taskAccess?: import("../core/task-registry.js").TaskAccess;
|
|
105
110
|
bgSink?: (event: import("../core/types.js").BackgroundChildEvent) => void;
|
package/dist/agents/subagent.js
CHANGED
|
@@ -34,6 +34,7 @@ function delegatedCostField(stats) {
|
|
|
34
34
|
}
|
|
35
35
|
export { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX };
|
|
36
36
|
import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getOrCreateSessionRetainLedger, ensureSessionReapHook, createResumePrompt, } from "./retain-ledger.js";
|
|
37
|
+
import { createPeerInboundChainRef, createPeerSelfRef } from "./peer-admission.js";
|
|
37
38
|
import { recordRosterSpawn } from "./roster-store.js";
|
|
38
39
|
import { ObserverDigestTap, ObserverPairing, createObserverReportToolSpec, markObserverTaskId, unmarkObserverTaskId, isObserverTaskId, ObserverResumeStateError, ObserverStoppedByUserError, observerFramingPrompt, observerSlug, resolveObserverDeclaration, } from "./observer.js";
|
|
39
40
|
import { SubagentStepRecorder } from "./subagent-steps.js";
|
|
@@ -437,7 +438,7 @@ export function createSubagentResume(deps) {
|
|
|
437
438
|
entry.cycleSeq += 1;
|
|
438
439
|
const resumeSpec = {
|
|
439
440
|
...entry.specSnapshot,
|
|
440
|
-
objective: createResumePrompt(marker, content),
|
|
441
|
+
objective: createResumePrompt(marker, content, deps.peerOrigin !== undefined ? "peer" : "operator"),
|
|
441
442
|
sessionId: entry.childSessionId,
|
|
442
443
|
requireExistingSession: true,
|
|
443
444
|
...(deps.currentOnQuestion !== undefined ? { onQuestion: deps.currentOnQuestion } : {}),
|
|
@@ -534,7 +535,10 @@ export function createSubagentResume(deps) {
|
|
|
534
535
|
},
|
|
535
536
|
}
|
|
536
537
|
: {}),
|
|
537
|
-
...(deps.currentParentNotify !== undefined
|
|
538
|
+
...(deps.currentParentNotify !== undefined
|
|
539
|
+
? { parentNotify: deps.currentParentNotify, parentPeerRef: deps.currentParentPeerRef }
|
|
540
|
+
: {}),
|
|
541
|
+
peerInboundChainRef: createPeerInboundChainRef(deps.peerOrigin?.hopChain),
|
|
538
542
|
...(reviveCycle !== undefined
|
|
539
543
|
? {
|
|
540
544
|
onNotifyInjectorReady: (inject) => {
|
|
@@ -1499,6 +1503,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1499
1503
|
...(treeScope !== undefined ? { registryScope: treeScope } : {}),
|
|
1500
1504
|
...(ctx.inheritedGateForChildren ? { inheritedGate: ctx.inheritedGateForChildren() } : {}),
|
|
1501
1505
|
ownOrgAdmissionRef: { current: reviveClaim !== undefined ? readDurableOrgAdmission(reviveClaim.row) : undefined },
|
|
1506
|
+
peerSelfRef: (() => {
|
|
1507
|
+
const ref = createPeerSelfRef(treeScope ?? "default");
|
|
1508
|
+
if (reviveClaim !== undefined)
|
|
1509
|
+
ref.addAxis("h", reviveClaim.row.handle);
|
|
1510
|
+
return ref;
|
|
1511
|
+
})(),
|
|
1512
|
+
peerInboundChainRef: createPeerInboundChainRef(reviveClaim?.peerSeed?.hopChain),
|
|
1502
1513
|
...(childDefaultPersona !== undefined ? { defaultSystemPrompt: childDefaultPersona } : {}),
|
|
1503
1514
|
isDelegatedChild: true,
|
|
1504
1515
|
...(ctx.interactionPosture !== undefined ? { parentInteractionPosture: ctx.interactionPosture } : {}),
|
|
@@ -1520,7 +1531,12 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1520
1531
|
}),
|
|
1521
1532
|
...(ctx.centerArtifactDigest !== undefined ? { parentCenterArtifactDigest: ctx.centerArtifactDigest } : {}),
|
|
1522
1533
|
...(ctx.centerSourceRevision !== undefined ? { parentCenterSourceRevision: ctx.centerSourceRevision } : {}),
|
|
1523
|
-
...(ctx.onTaskNotification !== undefined
|
|
1534
|
+
...(ctx.onTaskNotification !== undefined
|
|
1535
|
+
? {
|
|
1536
|
+
parentNotify: ctx.onTaskNotification,
|
|
1537
|
+
...(ctx.peerSelfRef !== undefined ? { parentPeerRef: ctx.peerSelfRef } : {}),
|
|
1538
|
+
}
|
|
1539
|
+
: {}),
|
|
1524
1540
|
...(ctx.subagentRetain !== undefined ? { parentRetainLedger: ctx.subagentRetain } : {}),
|
|
1525
1541
|
onForwardEvent: recordingForward,
|
|
1526
1542
|
...(worktreeDir !== undefined
|
|
@@ -1805,6 +1821,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1805
1821
|
const wt = await finishWorktree();
|
|
1806
1822
|
return { isError: true, content: `Sub-agent not started in background: ${e instanceof Error ? e.message : String(e)}${wt ? `\n${wt}` : ""}`, details: { error: "register_failed" } };
|
|
1807
1823
|
}
|
|
1824
|
+
childInternals.peerSelfRef?.addAxis("h", taskId);
|
|
1808
1825
|
bg.registry.bindBackgroundAgentSession(taskId, forkedId);
|
|
1809
1826
|
const forkBgHangAt = Date.now();
|
|
1810
1827
|
bg.registry.attachAgentTerminalNotifier(taskId, () => {
|
|
@@ -2255,6 +2272,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2255
2272
|
const wt = await finishWorktree();
|
|
2256
2273
|
return { isError: true, content: `Sub-agent not started in background: ${e instanceof Error ? e.message : String(e)}${wt ? `\n${wt}` : ""}`, details: { error: "register_failed" } };
|
|
2257
2274
|
}
|
|
2275
|
+
childInternals.peerSelfRef?.addAxis("h", taskId);
|
|
2258
2276
|
if (agentName !== undefined) {
|
|
2259
2277
|
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" }));
|
|
2260
2278
|
}
|
|
@@ -251,6 +251,16 @@ export function createAskUserQuestionTool(onQuestion, source, opts) {
|
|
|
251
251
|
details: { type: "ask-question", questionId: ctx.toolCallId, code: "question.human_unavailable", reason: "declined_unavailable" },
|
|
252
252
|
};
|
|
253
253
|
}
|
|
254
|
+
if (opts?.posture === "interactive" && opts.interactiveFallback !== true) {
|
|
255
|
+
return {
|
|
256
|
+
content: `Error (AskUserQuestion): the wired human channel reported that nobody was reachable for this ` +
|
|
257
|
+
`question. This run declared interaction posture "interactive", so the engine will not silently ` +
|
|
258
|
+
`self-answer; re-deliver the question once a human is reachable (or opt into the synthetic ` +
|
|
259
|
+
`continuation explicitly via interactiveQuestionFallback).`,
|
|
260
|
+
isError: true,
|
|
261
|
+
details: { type: "ask-question", questionId: ctx.toolCallId, code: "question.human_unavailable", reason: "declined_unavailable" },
|
|
262
|
+
};
|
|
263
|
+
}
|
|
254
264
|
discloseSyntheticContinuation(ctx.toolCallId, "declined_unavailable");
|
|
255
265
|
return {
|
|
256
266
|
content: NO_HUMAN,
|
|
@@ -3,6 +3,7 @@ export interface MailboxMessage {
|
|
|
3
3
|
from?: string;
|
|
4
4
|
content: string;
|
|
5
5
|
sentAt: number;
|
|
6
|
+
hopChain?: string[];
|
|
6
7
|
}
|
|
7
8
|
export interface MailboxLease {
|
|
8
9
|
messages: MailboxMessage[];
|
|
@@ -12,6 +13,7 @@ export interface MailboxAppendMessage {
|
|
|
12
13
|
from?: string;
|
|
13
14
|
content: string;
|
|
14
15
|
sentAt: number;
|
|
16
|
+
hopChain?: string[];
|
|
15
17
|
}
|
|
16
18
|
export interface MailboxStore {
|
|
17
19
|
append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
|
|
@@ -30,7 +30,7 @@ export class InMemoryMailboxStore {
|
|
|
30
30
|
throw new Error("MailboxStore.append: refusing a message without a scope");
|
|
31
31
|
const b = this.box(scope, handle);
|
|
32
32
|
const seq = b.nextSeq++;
|
|
33
|
-
b.messages.push({ seq, ...(msg.from !== undefined ? { from: msg.from } : {}), content: msg.content, sentAt: msg.sentAt });
|
|
33
|
+
b.messages.push({ seq, ...(msg.from !== undefined ? { from: msg.from } : {}), content: msg.content, sentAt: msg.sentAt, ...(msg.hopChain !== undefined ? { hopChain: [...msg.hopChain] } : {}) });
|
|
34
34
|
return seq;
|
|
35
35
|
}
|
|
36
36
|
async claimLease(scope, handle, owner, ttlMs, now = Date.now()) {
|
|
@@ -41,7 +41,7 @@ export class InMemoryMailboxStore {
|
|
|
41
41
|
return null;
|
|
42
42
|
const maxSeq = b.messages[b.messages.length - 1].seq;
|
|
43
43
|
b.lease = { owner, expiresAt: now + ttlMs, maxSeq };
|
|
44
|
-
return { messages: b.messages.map((m) => ({ ...m })), maxSeq };
|
|
44
|
+
return { messages: b.messages.map((m) => ({ ...m, ...(m.hopChain !== undefined ? { hopChain: [...m.hopChain] } : {}) })), maxSeq };
|
|
45
45
|
}
|
|
46
46
|
async ack(scope, handle, owner, upToSeq) {
|
|
47
47
|
const b = this.boxes.get(this.key(scope, handle));
|
|
@@ -339,6 +339,9 @@ export interface RunInternals {
|
|
|
339
339
|
ownOrgAdmissionRef?: {
|
|
340
340
|
current: import("../memory-admission.js").OwnOrgAdmissionVerdict | undefined;
|
|
341
341
|
};
|
|
342
|
+
peerSelfRef?: import("../../agents/peer-admission.js").PeerSelfRef;
|
|
343
|
+
peerInboundChainRef?: import("../../agents/peer-admission.js").PeerInboundChainRef;
|
|
344
|
+
parentPeerRef?: import("../../agents/peer-admission.js").PeerSelfRef;
|
|
342
345
|
workflowDepth?: number;
|
|
343
346
|
insideFork?: boolean;
|
|
344
347
|
isDelegatedChild?: boolean;
|