@sema-agent/core 2.1.0 → 2.3.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/dist/agents/observer.d.ts +14 -0
- package/dist/agents/observer.js +58 -9
- package/dist/agents/send-message-tool.js +55 -10
- package/dist/agents/subagent.d.ts +1 -0
- package/dist/agents/subagent.js +69 -15
- package/dist/agents/teacher.js +51 -23
- package/dist/core/context-edit.js +16 -3
- package/dist/core/file-snapshot-store.js +10 -1
- package/dist/core/runner/assemble-result.d.ts +1 -0
- package/dist/core/runner/assemble-result.js +12 -8
- package/dist/core/runner/prepare-task.d.ts +1 -0
- package/dist/core/runner/prepare-task.js +40 -10
- package/dist/core/runner/runtask.js +27 -5
- package/dist/core/runner/synthetic-tools.d.ts +1 -0
- package/dist/core/runner/synthetic-tools.js +18 -15
- package/dist/core/runner/turn-attachments.d.ts +12 -2
- package/dist/core/runner/turn-attachments.js +33 -3
- package/dist/core/task-registry-agent.d.ts +11 -1
- package/dist/core/task-registry-agent.js +40 -3
- package/dist/core/task-registry-monitor.js +149 -29
- package/dist/core/task-registry-shared.d.ts +25 -2
- package/dist/core/task-registry-shared.js +26 -2
- package/dist/core/task-registry.d.ts +5 -0
- package/dist/core/task-registry.js +25 -26
- package/dist/core/tools.d.ts +2 -0
- package/dist/core/tools.js +9 -0
- package/dist/core/types.d.ts +3 -1
- package/dist/core/workflow-journal-store.d.ts +16 -0
- package/dist/core/workflow-journal-store.js +28 -0
- package/dist/engine/session/memory-repo.js +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/orchestration/workflow-size-guideline.d.ts +6 -1
- package/dist/orchestration/workflow-size-guideline.js +19 -9
- package/dist/orchestration/workflow.d.ts +16 -0
- package/dist/orchestration/workflow.js +80 -20
- package/dist/prompt-assembly/assemble.js +3 -7
- package/dist/prompt-assembly/packs/sema-default.js +8 -5
- package/dist/prompts/coordinator.d.ts +1 -1
- package/dist/prompts/coordinator.js +45 -0
- package/dist/prompts/default.d.ts +4 -5
- package/dist/prompts/default.js +16 -18
- package/dist/prompts/simple-sections.d.ts +3 -1
- package/dist/prompts/simple-sections.js +11 -1
- package/dist/stores/file/workflow-journal-store.d.ts +7 -1
- package/dist/stores/file/workflow-journal-store.js +70 -33
- package/dist/tools/fs/bash-readonly-classifier.js +20 -1
- package/dist/tools/fs/fs-bash.d.ts +1 -0
- package/dist/tools/fs/fs-bash.js +10 -3
- package/dist/tools/fs/fs-read.js +10 -10
- package/dist/tools/fs/fs-search-tools.js +42 -7
- package/dist/tools/fs/fs-write.js +18 -6
- package/dist/tools/fs/index.d.ts +1 -0
- package/dist/tools/fs/index.js +2 -1
- package/dist/tools/fs/safety.d.ts +4 -0
- package/dist/tools/fs/safety.js +102 -6
- package/dist/tools/fs/search.d.ts +1 -0
- package/dist/tools/fs/search.js +23 -3
- package/dist/tools/monitor.d.ts +2 -0
- package/dist/tools/monitor.js +3 -1
- package/package.json +3 -2
|
@@ -68,6 +68,15 @@ export declare class ObserverDigestTap {
|
|
|
68
68
|
finish(reason: string): void;
|
|
69
69
|
}
|
|
70
70
|
export type ObserverPairingState = "armed" | "denied" | "stopped" | "retired" | "blocked";
|
|
71
|
+
export declare const OBSERVER_STOPPED_BY_USER_ERROR_NAME = "AgentStoppedByUserError";
|
|
72
|
+
export declare const OBSERVER_RESUME_STATE_ERROR_NAME = "ResumeAgentStateError";
|
|
73
|
+
export declare class ObserverStoppedByUserError extends Error {
|
|
74
|
+
constructor(message: string);
|
|
75
|
+
}
|
|
76
|
+
export declare class ObserverResumeStateError extends Error {
|
|
77
|
+
constructor(message: string);
|
|
78
|
+
}
|
|
79
|
+
export declare const OBSERVER_FRESH_START_NOTE = "[Note: your previous observation context was lost; this is a fresh start mid-task.]";
|
|
71
80
|
export interface ObserverSpawner {
|
|
72
81
|
spawnFirstRun(args: {
|
|
73
82
|
framingPrompt: string;
|
|
@@ -76,6 +85,10 @@ export interface ObserverSpawner {
|
|
|
76
85
|
deliver(args: {
|
|
77
86
|
digest: string;
|
|
78
87
|
}): Promise<void>;
|
|
88
|
+
restartFresh?(args: {
|
|
89
|
+
framingPrompt: string;
|
|
90
|
+
digest: string;
|
|
91
|
+
}): Promise<void>;
|
|
79
92
|
}
|
|
80
93
|
export declare class ObserverPairing {
|
|
81
94
|
state: ObserverPairingState;
|
|
@@ -102,6 +115,7 @@ export declare class ObserverPairing {
|
|
|
102
115
|
private safeOnError;
|
|
103
116
|
drain(): Promise<void>;
|
|
104
117
|
private pump;
|
|
118
|
+
private deliverBatch;
|
|
105
119
|
retire(state: Exclude<ObserverPairingState, "armed">): void;
|
|
106
120
|
}
|
|
107
121
|
export declare function markObserverTaskId(taskId: string): void;
|
package/dist/agents/observer.js
CHANGED
|
@@ -209,6 +209,24 @@ export class ObserverDigestTap {
|
|
|
209
209
|
this.safeFlush(slice);
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
+
export const OBSERVER_STOPPED_BY_USER_ERROR_NAME = "AgentStoppedByUserError";
|
|
213
|
+
export const OBSERVER_RESUME_STATE_ERROR_NAME = "ResumeAgentStateError";
|
|
214
|
+
export class ObserverStoppedByUserError extends Error {
|
|
215
|
+
constructor(message) {
|
|
216
|
+
super(message);
|
|
217
|
+
this.name = OBSERVER_STOPPED_BY_USER_ERROR_NAME;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
export class ObserverResumeStateError extends Error {
|
|
221
|
+
constructor(message) {
|
|
222
|
+
super(message);
|
|
223
|
+
this.name = OBSERVER_RESUME_STATE_ERROR_NAME;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function errorName(err) {
|
|
227
|
+
return err instanceof Error ? err.name : undefined;
|
|
228
|
+
}
|
|
229
|
+
export const OBSERVER_FRESH_START_NOTE = "[Note: your previous observation context was lost; this is a fresh start mid-task.]";
|
|
212
230
|
export class ObserverPairing {
|
|
213
231
|
state = "armed";
|
|
214
232
|
observedEnvelopeName;
|
|
@@ -278,18 +296,22 @@ export class ObserverPairing {
|
|
|
278
296
|
const batch = this.buffer.splice(0, this.buffer.length);
|
|
279
297
|
try {
|
|
280
298
|
const digest = renderObserverDigestBatch(this, batch);
|
|
281
|
-
|
|
282
|
-
await this.spawner.spawnFirstRun({ framingPrompt: this.framingPrompt, digest });
|
|
283
|
-
this.firstRunDone = true;
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
await this.spawner.deliver({ digest });
|
|
287
|
-
}
|
|
299
|
+
await this.deliverBatch(digest);
|
|
288
300
|
}
|
|
289
301
|
catch (err) {
|
|
290
|
-
|
|
291
|
-
|
|
302
|
+
if (errorName(err) === OBSERVER_STOPPED_BY_USER_ERROR_NAME) {
|
|
303
|
+
this.state = "stopped";
|
|
304
|
+
this.buffer = [];
|
|
305
|
+
this.safeOnError(err);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
292
308
|
this.safeOnError(err);
|
|
309
|
+
if (this.buffer.length > 0) {
|
|
310
|
+
const strandedCount = this.buffer.length;
|
|
311
|
+
this.buffer = [];
|
|
312
|
+
this.safeOnError(new Error(`observer delivery: ${strandedCount} segment(s) enqueued mid-fault were stranded and dropped (never attempted) after: ${err instanceof Error ? err.message : String(err)}`));
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
293
315
|
}
|
|
294
316
|
}
|
|
295
317
|
}
|
|
@@ -297,6 +319,33 @@ export class ObserverPairing {
|
|
|
297
319
|
this.delivering = false;
|
|
298
320
|
}
|
|
299
321
|
}
|
|
322
|
+
async deliverBatch(digest) {
|
|
323
|
+
if (!this.firstRunDone) {
|
|
324
|
+
await this.spawner.spawnFirstRun({ framingPrompt: this.framingPrompt, digest });
|
|
325
|
+
this.firstRunDone = true;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
await this.spawner.deliver({ digest });
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
if (errorName(err) !== OBSERVER_RESUME_STATE_ERROR_NAME)
|
|
333
|
+
throw err;
|
|
334
|
+
const restart = this.spawner.restartFresh?.bind(this.spawner);
|
|
335
|
+
if (restart === undefined)
|
|
336
|
+
throw err;
|
|
337
|
+
try {
|
|
338
|
+
await restart({ framingPrompt: `${this.framingPrompt}\n\n${OBSERVER_FRESH_START_NOTE}`, digest });
|
|
339
|
+
this.firstRunDone = true;
|
|
340
|
+
}
|
|
341
|
+
catch (restartErr) {
|
|
342
|
+
this.firstRunDone = false;
|
|
343
|
+
throw restartErr instanceof Error
|
|
344
|
+
? new Error(`${restartErr.message} (after resume-state loss: ${err instanceof Error ? err.message : String(err)})`, { cause: restartErr })
|
|
345
|
+
: restartErr;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
300
349
|
retire(state) {
|
|
301
350
|
if (this.state !== "armed")
|
|
302
351
|
return;
|
|
@@ -37,6 +37,7 @@ export function createSendMessageTool(opts) {
|
|
|
37
37
|
contract: { contractId: "core.send_message@1", implementationRevision: "1" },
|
|
38
38
|
executionMode: "parallel",
|
|
39
39
|
description: `Send a follow-up message to a previously spawned background agent. ` +
|
|
40
|
+
`Your plain text output is NOT visible to other agents — to communicate, you MUST call this tool. Messages addressed to you are delivered automatically; you don't check an inbox. ` +
|
|
40
41
|
`'to' is the agent's name (set at spawn via Agent({name})) or its task_id (a…). A RUNNING agent receives the ` +
|
|
41
42
|
`message at its next turn (queued — never interrupts its current work); a FINISHED agent resumes as a new ` +
|
|
42
43
|
`background run with its full prior conversation preserved, so don't re-explain what it already knows. Names ` +
|
|
@@ -50,7 +51,7 @@ export function createSendMessageTool(opts) {
|
|
|
50
51
|
parameters: Type.Object({
|
|
51
52
|
to: Type.String({ description: 'Recipient: the agent\'s name, or its task_id (a…) returned by the Agent tool with run_in_background. "main" is reserved for the spawning conversation.' }),
|
|
52
53
|
message: Type.String({ description: "The follow-up request. The agent continues from its full prior context." }),
|
|
53
|
-
summary: Type.Optional(Type.String({ maxLength: 200, description: "A
|
|
54
|
+
summary: Type.Optional(Type.String({ maxLength: 200, description: "A 5-10 word summary shown as a preview in the UI (required when message is a string)" })),
|
|
54
55
|
}),
|
|
55
56
|
execute: async (args, ctx) => {
|
|
56
57
|
const a = args;
|
|
@@ -60,6 +61,28 @@ export function createSendMessageTool(opts) {
|
|
|
60
61
|
return { content: "Message not sent: 'to' was empty. Pass the agent's task_id (a…).", details: { error: "empty to" }, isError: true };
|
|
61
62
|
if (!message)
|
|
62
63
|
return { content: "Message not sent: 'message' was empty.", details: { error: "empty message" }, isError: true };
|
|
64
|
+
if (to === "*") {
|
|
65
|
+
return {
|
|
66
|
+
content: 'Message not sent: broadcast (to: "*") is no longer supported — send a message per recipient.',
|
|
67
|
+
details: { error: "broadcast_unsupported", to },
|
|
68
|
+
isError: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (to.includes("@")) {
|
|
72
|
+
return {
|
|
73
|
+
content: "Message not sent: to must be a bare teammate name — there is only one team per session.",
|
|
74
|
+
details: { error: "qualified_name_unsupported", to },
|
|
75
|
+
isError: true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const summaryArg = typeof a.summary === "string" ? a.summary.trim() : "";
|
|
79
|
+
if (summaryArg === "") {
|
|
80
|
+
return {
|
|
81
|
+
content: "Message not sent: summary is required when message is a string — pass a 5-10 word summary of the follow-up.",
|
|
82
|
+
details: { error: "summary_required", to },
|
|
83
|
+
isError: true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
63
86
|
const senderId = ctx.taskId ?? opts.owner;
|
|
64
87
|
if (senderId !== undefined && isObserverTaskId(senderId)) {
|
|
65
88
|
return { content: OBSERVER_SENDMESSAGE_SENDER_REFUSAL, details: { error: "observer_sender" }, isError: true };
|
|
@@ -89,6 +112,17 @@ export function createSendMessageTool(opts) {
|
|
|
89
112
|
details: { type: "send-message", status: "uplinked", to: "main", seq: uplinkSeqGlobal },
|
|
90
113
|
};
|
|
91
114
|
}
|
|
115
|
+
const hasParent = ctx.parentTaskId !== undefined ||
|
|
116
|
+
opts.uplink !== undefined ||
|
|
117
|
+
opts.senderName !== undefined ||
|
|
118
|
+
opts.siblingRetain !== undefined;
|
|
119
|
+
if (!hasParent) {
|
|
120
|
+
return {
|
|
121
|
+
content: `You are the main conversation — "main" addresses you. Send to a named agent instead.`,
|
|
122
|
+
details: { error: "main_is_self", to },
|
|
123
|
+
isError: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
92
126
|
return {
|
|
93
127
|
content: `Message not sent: "main" (the spawning conversation) is not a deliverable target here. ` +
|
|
94
128
|
`Your completion is reported to it automatically — finish your task and your final report will be relayed.`,
|
|
@@ -126,10 +160,10 @@ export function createSendMessageTool(opts) {
|
|
|
126
160
|
if (row.name === undefined || row.agentType === "fork")
|
|
127
161
|
return undefined;
|
|
128
162
|
const whoT3 = handle === to ? `agent ${handle}` : `agent "${to}" (${handle})`;
|
|
129
|
-
if (row.status === "killed") {
|
|
163
|
+
if (row.status === "killed" && row.stoppedBy === "user") {
|
|
130
164
|
return {
|
|
131
|
-
content: `Message not sent: ${whoT3} was stopped
|
|
132
|
-
details: { error: "killed", to,
|
|
165
|
+
content: `Message not sent: ${whoT3} was stopped by the user and was not resumed. Treat its work as cancelled; only start a new agent for it if the user explicitly asks.`,
|
|
166
|
+
details: { error: "killed", to, stoppedBy: row.stoppedBy },
|
|
133
167
|
isError: true,
|
|
134
168
|
};
|
|
135
169
|
}
|
|
@@ -419,11 +453,13 @@ export function createSendMessageTool(opts) {
|
|
|
419
453
|
}
|
|
420
454
|
if (row.status === "killed") {
|
|
421
455
|
const by = opts.registry.getStopAttribution(targetId);
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
456
|
+
if (by === "user") {
|
|
457
|
+
return {
|
|
458
|
+
content: `Message not sent: ${who} was stopped by the user and was not resumed. Treat its work as cancelled; only start a new agent for it if the user explicitly asks.`,
|
|
459
|
+
details: { error: "killed", to, stoppedBy: by },
|
|
460
|
+
isError: true,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
427
463
|
}
|
|
428
464
|
const runLedger = ctx.subagentRetain ?? opts.retain;
|
|
429
465
|
const smLedgerSessionId = ctx.sessionId ?? opts.sessionId;
|
|
@@ -445,6 +481,13 @@ export function createSendMessageTool(opts) {
|
|
|
445
481
|
isError: true,
|
|
446
482
|
};
|
|
447
483
|
}
|
|
484
|
+
if (ledger.get(resumeToolUseId)?.running === true) {
|
|
485
|
+
return {
|
|
486
|
+
content: `Message not sent: ${who} (or a prior follow-up to it) is still running — wait for its completion notification.`,
|
|
487
|
+
details: { error: "steering.still_running", to },
|
|
488
|
+
isError: true,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
448
491
|
return await withTargetLane(targetLaneKey(access.scope, targetId), async () => {
|
|
449
492
|
const resume = createSubagentResume({
|
|
450
493
|
ledger,
|
|
@@ -470,7 +513,9 @@ export function createSendMessageTool(opts) {
|
|
|
470
513
|
const summary = typeof a.summary === "string" && a.summary.trim() !== "" ? a.summary.trim().slice(0, 200) : undefined;
|
|
471
514
|
const fromPrefix = ctx.parentTaskId !== undefined ? `(message from teammate "${opts.senderName ?? senderId ?? "unknown"}")\n` : "";
|
|
472
515
|
try {
|
|
473
|
-
const
|
|
516
|
+
const safeSummary = summary !== undefined ? escapeEnvelopeTag(TEAMMATE_MESSAGE_TAG, summary) : undefined;
|
|
517
|
+
const safeMessage = escapeEnvelopeTag(TEAMMATE_MESSAGE_TAG, message);
|
|
518
|
+
const marker = await resume(`${fromPrefix}${safeSummary ? `[${safeSummary}] ${safeMessage}` : safeMessage}`);
|
|
474
519
|
return {
|
|
475
520
|
content: `Message sent — ${who} resumed in the background with its prior context intact (correlation marker [${marker}]).\n` +
|
|
476
521
|
`You will be notified automatically when it completes; its reply will carry [${marker}]. Continue with other work — do not poll.`,
|
|
@@ -133,6 +133,7 @@ export declare function asyncLaunchedReceipt(p: {
|
|
|
133
133
|
taskId: string;
|
|
134
134
|
workingLine: string;
|
|
135
135
|
notify: boolean;
|
|
136
|
+
oneShot?: boolean;
|
|
136
137
|
notes?: (string | undefined)[];
|
|
137
138
|
}): string;
|
|
138
139
|
export declare function createSubagentTool(opts: SubagentToolOptions): ToolSpec;
|
package/dist/agents/subagent.js
CHANGED
|
@@ -21,7 +21,7 @@ import { FORK_DEFAULT_MAX_TURNS, SESSION_BG_DEFAULT_TIMEOUT_SEC, RETAIN_DEFAULT_
|
|
|
21
21
|
export { FORK_DEFAULT_MAX_TURNS, SESSION_BG_DEFAULT_TIMEOUT_SEC, RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX };
|
|
22
22
|
import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getOrCreateSessionRetainLedger, ensureSessionReapHook, createResumePrompt, } from "./retain-ledger.js";
|
|
23
23
|
import { recordRosterSpawn } from "./roster-store.js";
|
|
24
|
-
import { ObserverDigestTap, ObserverPairing, createObserverReportToolSpec, markObserverTaskId, unmarkObserverTaskId, isObserverTaskId, observerFramingPrompt, observerSlug, resolveObserverDeclaration, } from "./observer.js";
|
|
24
|
+
import { ObserverDigestTap, ObserverPairing, createObserverReportToolSpec, markObserverTaskId, unmarkObserverTaskId, isObserverTaskId, ObserverResumeStateError, ObserverStoppedByUserError, observerFramingPrompt, observerSlug, resolveObserverDeclaration, } from "./observer.js";
|
|
25
25
|
import { SubagentStepRecorder } from "./subagent-steps.js";
|
|
26
26
|
const BG_AGENT_RESULT_MAX = 4_000;
|
|
27
27
|
const BG_AGENT_RESULT_FULL_MAX = 200_000;
|
|
@@ -129,6 +129,9 @@ function createToolStatsCounter(delegationToolName) {
|
|
|
129
129
|
snapshot() {
|
|
130
130
|
return t.readCount + t.searchCount + t.bashCount + t.editFileCount + t.otherToolCount > 0 ? { ...t } : undefined;
|
|
131
131
|
},
|
|
132
|
+
total() {
|
|
133
|
+
return t.readCount + t.searchCount + t.bashCount + t.editFileCount + t.otherToolCount;
|
|
134
|
+
},
|
|
132
135
|
};
|
|
133
136
|
}
|
|
134
137
|
export const FORK_SUBAGENT_TYPE = "fork";
|
|
@@ -622,15 +625,20 @@ export function forkWorktreeTranslationNote(parentCwd, worktreeDir) {
|
|
|
622
625
|
}
|
|
623
626
|
export function asyncLaunchedReceipt(p) {
|
|
624
627
|
const noteLines = (p.notes ?? []).filter((n) => n !== undefined && n !== "");
|
|
628
|
+
const oneShotBlocked = p.notify && p.oneShot === true;
|
|
625
629
|
return (`Async agent launched successfully. (This tool result is internal metadata — never quote or paste any part of it, including the task_id below, into a user-facing reply.)\n` +
|
|
626
630
|
`task_id: ${p.taskId} (internal ID - do not mention to user. Use SendMessage with to: '${p.taskId}', summary: '<5-10 word recap>' to continue this agent.)\n` +
|
|
627
|
-
(
|
|
628
|
-
? `${p.workingLine}
|
|
629
|
-
:
|
|
631
|
+
(oneShotBlocked
|
|
632
|
+
? `${p.workingLine} This is a ONE-SHOT submission — there is no later turn for a background notification to land in, so do NOT end your turn expecting one. Actively wait instead: TaskOutput(task_id: "${p.taskId}", block: true). If it is still running after the wait, wait again (bounded) rather than ending the turn, or write out your best available answer now if you are near your own time budget.\n`
|
|
633
|
+
: p.notify
|
|
634
|
+
? `${p.workingLine} You will be notified automatically when it completes. You know nothing about its results until that notification arrives — do not report, assume, or predict them; continue other work or respond to the user in the meantime.\n`
|
|
635
|
+
: `${p.workingLine} Its result is NOT pushed automatically — retrieve progress and results with TaskOutput(task_id) where mounted.\n`) +
|
|
630
636
|
noteLines.map((n) => `${n}\n`).join("") +
|
|
631
|
-
(
|
|
632
|
-
? `In your own words, briefly tell the user what you launched — do not echo this tool result.
|
|
633
|
-
:
|
|
637
|
+
(oneShotBlocked
|
|
638
|
+
? `In your own words, briefly tell the user what you launched — do not echo this tool result. Do not assume a later message will deliver the result: this submission ends after this turn, so retrieve it now with the blocking TaskOutput wait above before writing your final answer.`
|
|
639
|
+
: p.notify
|
|
640
|
+
? `In your own words, briefly tell the user what you launched — do not echo this tool result. Agent results will arrive in a subsequent message. If the user asks for progress, say the agent is still running.`
|
|
641
|
+
: `In your own words, briefly tell the user what you launched — do not echo this tool result. The result will NOT arrive on its own — retrieve it with TaskOutput(task_id) where mounted before relying on it.`));
|
|
634
642
|
}
|
|
635
643
|
export function createSubagentTool(opts) {
|
|
636
644
|
const catalog = opts.runner.agentCatalog;
|
|
@@ -1105,9 +1113,17 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1105
1113
|
let observerTap;
|
|
1106
1114
|
let observerSessionId;
|
|
1107
1115
|
let observerFailure;
|
|
1116
|
+
let observerDegradedCount = 0;
|
|
1117
|
+
let observerDegradedFirst;
|
|
1108
1118
|
const noteObserverFailure = (err) => {
|
|
1109
|
-
|
|
1110
|
-
|
|
1119
|
+
const msg = (err instanceof Error ? err.message : String(err)).slice(0, REPORT_FIELD_MAX);
|
|
1120
|
+
if (observerPairing !== undefined && observerPairing.state === "armed") {
|
|
1121
|
+
observerDegradedCount++;
|
|
1122
|
+
if (observerDegradedFirst === undefined)
|
|
1123
|
+
observerDegradedFirst = msg;
|
|
1124
|
+
}
|
|
1125
|
+
else if (observerFailure === undefined) {
|
|
1126
|
+
observerFailure = msg;
|
|
1111
1127
|
}
|
|
1112
1128
|
try {
|
|
1113
1129
|
opts.onObserverError?.(err, {
|
|
@@ -1139,7 +1155,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1139
1155
|
pendingTrigger.push(prompt);
|
|
1140
1156
|
const observerDef = observerArm.observerDefinition;
|
|
1141
1157
|
const envelopeName = observerSlug(childAgentName ?? def.name);
|
|
1142
|
-
|
|
1158
|
+
let sid = uuidv7();
|
|
1143
1159
|
observerSessionId = sid;
|
|
1144
1160
|
markObserverTaskId(sid);
|
|
1145
1161
|
const reportOpts = {
|
|
@@ -1163,15 +1179,36 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1163
1179
|
});
|
|
1164
1180
|
const runObserverLeg = async (spec) => {
|
|
1165
1181
|
if (!isObserverTaskId(sid))
|
|
1166
|
-
throw new
|
|
1182
|
+
throw new ObserverStoppedByUserError("observer identity revoked — delivery refused");
|
|
1167
1183
|
const r = await opts.runner.runTask(spec);
|
|
1168
1184
|
if (r.status !== "completed") {
|
|
1185
|
+
if (r.errorCode === "resume.session_not_found") {
|
|
1186
|
+
throw new ObserverResumeStateError(`observer session no longer exists${r.errorMessage ? `: ${r.errorMessage}` : ""}`);
|
|
1187
|
+
}
|
|
1169
1188
|
throw new Error(`observer run ended ${r.status}${r.errorMessage ? `: ${r.errorMessage}` : ""}`);
|
|
1170
1189
|
}
|
|
1171
1190
|
};
|
|
1172
1191
|
const spawner = {
|
|
1173
1192
|
spawnFirstRun: ({ framingPrompt, digest }) => runObserverLeg(observerSpec(`${framingPrompt}\n\n${digest}`, false)),
|
|
1174
1193
|
deliver: ({ digest }) => runObserverLeg(observerSpec(digest, true)),
|
|
1194
|
+
restartFresh: async ({ framingPrompt, digest }) => {
|
|
1195
|
+
const dead = sid;
|
|
1196
|
+
const fresh = uuidv7();
|
|
1197
|
+
markObserverTaskId(fresh);
|
|
1198
|
+
sid = fresh;
|
|
1199
|
+
observerSessionId = fresh;
|
|
1200
|
+
try {
|
|
1201
|
+
await runObserverLeg(observerSpec(`${framingPrompt}\n\n${digest}`, false));
|
|
1202
|
+
}
|
|
1203
|
+
finally {
|
|
1204
|
+
unmarkObserverTaskId(dead);
|
|
1205
|
+
try {
|
|
1206
|
+
await opts.runner.sessions.release(dead);
|
|
1207
|
+
}
|
|
1208
|
+
catch {
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
},
|
|
1175
1212
|
};
|
|
1176
1213
|
observerPairing = new ObserverPairing({
|
|
1177
1214
|
observedEnvelopeName: envelopeName,
|
|
@@ -1209,9 +1246,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1209
1246
|
t.unref?.();
|
|
1210
1247
|
});
|
|
1211
1248
|
const outcome = await Promise.race([observerPairing.drain().then(() => "drained"), timeout]);
|
|
1249
|
+
observerPairing.retire(outcome === "timeout" ? "stopped" : "retired");
|
|
1212
1250
|
if (outcome === "timeout")
|
|
1213
1251
|
noteObserverFailure(new Error("observer unresponsive at settle (delivery drain timed out)"));
|
|
1214
|
-
observerPairing.retire(outcome === "timeout" ? "stopped" : "retired");
|
|
1215
1252
|
if (observerSessionId !== undefined) {
|
|
1216
1253
|
try {
|
|
1217
1254
|
await opts.runner.sessions.release(observerSessionId);
|
|
@@ -1240,7 +1277,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1240
1277
|
clearTimeout(timer);
|
|
1241
1278
|
return outcome;
|
|
1242
1279
|
};
|
|
1243
|
-
const observerNoteFor = (outcome) => observerFailure !== undefined
|
|
1280
|
+
const observerNoteFor = (outcome) => observerFailure !== undefined
|
|
1281
|
+
? " [observer: failed]"
|
|
1282
|
+
: outcome === "still_draining"
|
|
1283
|
+
? " [observer: still draining]"
|
|
1284
|
+
: observerDegradedCount > 0
|
|
1285
|
+
? ` [observer: degraded, ${observerDegradedCount} digest batch(es) lost]`
|
|
1286
|
+
: "";
|
|
1244
1287
|
const stepRecorder = new SubagentStepRecorder(ctx.toolCallId);
|
|
1245
1288
|
const toolStatsCounter = createToolStatsCounter(opts.name ?? DEFAULT_SUBAGENT_TOOL_NAME);
|
|
1246
1289
|
let assistantTextTail = "";
|
|
@@ -1736,6 +1779,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1736
1779
|
taskId,
|
|
1737
1780
|
workingLine: `The forked agent inherits your full current context and works on its prompt in the background.`,
|
|
1738
1781
|
notify: Boolean(notify),
|
|
1782
|
+
oneShot: ctx.oneShot === true,
|
|
1739
1783
|
notes: [
|
|
1740
1784
|
modelNote,
|
|
1741
1785
|
extraToolsNote,
|
|
@@ -2366,6 +2410,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2366
2410
|
taskId,
|
|
2367
2411
|
workingLine: `The agent is working in the background.`,
|
|
2368
2412
|
notify: Boolean(notify),
|
|
2413
|
+
oneShot: ctx.oneShot === true,
|
|
2369
2414
|
notes: [
|
|
2370
2415
|
modelNote,
|
|
2371
2416
|
extraToolsNote,
|
|
@@ -2381,6 +2426,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2381
2426
|
};
|
|
2382
2427
|
}
|
|
2383
2428
|
let child;
|
|
2429
|
+
const syncStartedAt = Date.now();
|
|
2384
2430
|
let retainEntry;
|
|
2385
2431
|
if (ctx.onSubagentSpawn || observerPairing) {
|
|
2386
2432
|
retainEntry = ctx.onSubagentSpawn ? await tryRetainChild(ctx.subagentRetain) : undefined;
|
|
@@ -2488,9 +2534,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2488
2534
|
child.blockedReason ? `blocked_reason: ${inlineUntrusted(child.blockedReason, REPORT_FIELD_MAX)}` : "",
|
|
2489
2535
|
child.errorMessage ? `error: ${inlineUntrusted(child.errorMessage, REPORT_FIELD_MAX)}` : "",
|
|
2490
2536
|
errClass ? `error_kind: ${errClass.errorKind} (retryable: ${errClass.retryable})` : "",
|
|
2491
|
-
observerFailure !== undefined
|
|
2537
|
+
observerFailure !== undefined
|
|
2538
|
+
? `observer: failed (${inlineUntrusted(observerFailure, REPORT_FIELD_MAX)})`
|
|
2539
|
+
: observerDegradedCount > 0
|
|
2540
|
+
? `observer: degraded — ${observerDegradedCount} digest batch(es) never reached it (first: ${inlineUntrusted(observerDegradedFirst ?? "", REPORT_FIELD_MAX)})`
|
|
2541
|
+
: "",
|
|
2492
2542
|
`stats: turns=${child.stats.turns} tokens=${child.stats.tokens}`,
|
|
2493
|
-
retainEntry
|
|
2543
|
+
!retainEntry && failureRetained ? `transcript_id: ${child.sessionId} (internal ID - do not mention to user; inspect via AgentTranscript where mounted)` : "",
|
|
2494
2544
|
retainEntry ? `resumable: true` : "",
|
|
2495
2545
|
failureRetained
|
|
2496
2546
|
? `session_retained: this child's session is kept for a short time (~15 min) so its transcript stays inspectable via transcript_id; it is released automatically afterwards.`
|
|
@@ -2521,6 +2571,10 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2521
2571
|
: "(no text result)",
|
|
2522
2572
|
"",
|
|
2523
2573
|
"Verify this result before concluding. If it leaves more to do, continue or delegate again.",
|
|
2574
|
+
retainEntry
|
|
2575
|
+
? `transcript_id: ${child.sessionId} (internal ID - do not mention to user. Transcript inspectable via AgentTranscript where mounted; this child is continued through the deployment's resume handle, not by SendMessage.)`
|
|
2576
|
+
: "",
|
|
2577
|
+
`<usage>subagent_tokens: ${child.stats.tokens}\ntool_uses: ${toolStatsCounter.total()}\nduration_ms: ${Date.now() - syncStartedAt}</usage>`,
|
|
2524
2578
|
].filter(Boolean);
|
|
2525
2579
|
return {
|
|
2526
2580
|
content: lines.join("\n"),
|
package/dist/agents/teacher.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { mapNestedSuspend, isDurablePause } from "./suspend-guard.js";
|
|
3
|
+
import { isDefineToolProduct, stampDefineToolBrand } from "../core/tools.js";
|
|
3
4
|
import { delimitUntrusted, inlineUntrusted } from "../core/untrusted-text.js";
|
|
4
5
|
export const TEACHER_PROMPT = `You are an expert advisor to a less-capable "student" agent that got stuck.
|
|
5
6
|
You receive the task and the student's recent failed attempts. Your job is to help the student RECOVER,
|
|
@@ -119,29 +120,56 @@ async function runTeacherCore(runner, studentSpec, teacher) {
|
|
|
119
120
|
errorStreak = sig === lastErrSig ? errorStreak + 1 : 1;
|
|
120
121
|
lastErrSig = sig;
|
|
121
122
|
};
|
|
122
|
-
const wrap = (t) =>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
?
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
123
|
+
const wrap = (t) => {
|
|
124
|
+
if (isDefineToolProduct(t)) {
|
|
125
|
+
const product = t;
|
|
126
|
+
return stampDefineToolBrand({
|
|
127
|
+
...t,
|
|
128
|
+
execute: async (toolCallId, rawParams, signal, onUpdate) => {
|
|
129
|
+
try {
|
|
130
|
+
const out = await product.execute(toolCallId, rawParams, signal, onUpdate);
|
|
131
|
+
const blocks = out && typeof out === "object" && "content" in out ? out.content : out;
|
|
132
|
+
const text = Array.isArray(blocks)
|
|
133
|
+
? blocks.map((b) => (b && typeof b === "object" && "text" in b ? String(b.text) : "")).join("\n")
|
|
134
|
+
: String(blocks ?? "");
|
|
135
|
+
logTool(t.name, rawParams, text);
|
|
136
|
+
errorStreak = 0;
|
|
137
|
+
lastErrSig = "";
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
const errText = String(e);
|
|
142
|
+
logTool(t.name, rawParams, `ERROR: ${errText}`);
|
|
143
|
+
onToolFail(t.name, errText);
|
|
144
|
+
throw e;
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
...t,
|
|
151
|
+
execute: async (args, ctx) => {
|
|
152
|
+
try {
|
|
153
|
+
const out = await t.execute(args, ctx);
|
|
154
|
+
const text = typeof out === "string"
|
|
155
|
+
? out
|
|
156
|
+
: out && typeof out === "object" && "content" in out
|
|
157
|
+
? String(out.content)
|
|
158
|
+
: (JSON.stringify(out) ?? "");
|
|
159
|
+
logTool(t.name, args, text);
|
|
160
|
+
errorStreak = 0;
|
|
161
|
+
lastErrSig = "";
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
const errText = String(e);
|
|
166
|
+
logTool(t.name, args, `ERROR: ${errText}`);
|
|
167
|
+
onToolFail(t.name, errText);
|
|
168
|
+
throw e;
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
};
|
|
145
173
|
const tools = studentSpec.tools?.map(wrap);
|
|
146
174
|
const helperBase = () => teacher.helperModel
|
|
147
175
|
? { model: teacher.helperModel }
|
|
@@ -2,7 +2,20 @@ import { DEFAULT_CHARS_PER_TOKEN, estimateContextTokens, estimateTokens } from "
|
|
|
2
2
|
import { isToolResult } from "./message-utils.js";
|
|
3
3
|
const CLEARED_MARKER = "[tool result cleared to save context]";
|
|
4
4
|
const refNote = (ref) => `full text persisted; call ReadToolResult with ref "${ref}" to read it back`;
|
|
5
|
-
const mediaNote = (
|
|
5
|
+
const mediaNote = (blocks) => {
|
|
6
|
+
const byType = new Map();
|
|
7
|
+
for (const b of blocks) {
|
|
8
|
+
const t = b.type ?? "untyped";
|
|
9
|
+
byType.set(t, (byType.get(t) ?? 0) + 1);
|
|
10
|
+
}
|
|
11
|
+
const n = blocks.length;
|
|
12
|
+
if (byType.size === 1) {
|
|
13
|
+
const [t, c] = [...byType][0];
|
|
14
|
+
return `${c} ${t} attachment${c === 1 ? "" : "s"} no longer visible after this clear`;
|
|
15
|
+
}
|
|
16
|
+
const breakdown = [...byType].map(([t, c]) => `${c} ${t}`).join(", ");
|
|
17
|
+
return `${n} attachments (${breakdown}) no longer visible after this clear`;
|
|
18
|
+
};
|
|
6
19
|
const clearedMarker = (notes) => {
|
|
7
20
|
const present = notes.filter((n) => n !== undefined);
|
|
8
21
|
return present.length > 0 ? `[tool result cleared to save context — ${present.join("; ")}]` : CLEARED_MARKER;
|
|
@@ -91,8 +104,8 @@ export function clearStaleToolResults(messages, opts) {
|
|
|
91
104
|
}
|
|
92
105
|
}
|
|
93
106
|
}
|
|
94
|
-
const
|
|
95
|
-
const marker = clearedMarker([ref,
|
|
107
|
+
const mediaBlocks = rawContent.filter((c) => c?.type !== "text");
|
|
108
|
+
const marker = clearedMarker([ref, mediaBlocks.length > 0 ? mediaNote(mediaBlocks) : undefined]);
|
|
96
109
|
const cleared = {
|
|
97
110
|
...out[idx],
|
|
98
111
|
content: [{ type: "text", text: marker }],
|
|
@@ -204,9 +204,18 @@ export class InMemoryFileSnapshotStore {
|
|
|
204
204
|
try {
|
|
205
205
|
for (const hash of new Set(manifest.values())) {
|
|
206
206
|
written.add(hash);
|
|
207
|
+
if (hash === "" || hash === "." || hash === ".." || !/^[A-Za-z0-9_.-]+$/.test(hash)) {
|
|
208
|
+
return { ok: false, error: { code: "read_failed", message: `unsafe blob hash ${JSON.stringify(hash)}` } };
|
|
209
|
+
}
|
|
207
210
|
if (this.blobs.has(hash))
|
|
208
211
|
continue;
|
|
209
|
-
|
|
212
|
+
let bytes;
|
|
213
|
+
try {
|
|
214
|
+
bytes = await srcGetBlob(hash);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
return { ok: false, error: { code: "read_failed", message: `source blob ${hash} fetch failed: ${err.message}` } };
|
|
218
|
+
}
|
|
210
219
|
if (!bytes)
|
|
211
220
|
return { ok: false, error: { code: "read_failed", message: `missing source blob ${hash}` } };
|
|
212
221
|
if (createHash("sha256").update(bytes).digest("hex") !== hash) {
|
|
@@ -36,14 +36,16 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
36
36
|
const taskId = spec.taskId ?? sessionId;
|
|
37
37
|
const text = final ? assistantText(final) : "";
|
|
38
38
|
stats.totalInputTokens = stats.promptTokens;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
if (!flags.unpricedSpend) {
|
|
40
|
+
const compactionMicroUsd = stats.compactionMicroUsd ?? 0;
|
|
41
|
+
const nestedSubagentMicroUsd = stats.nested?.costMicroUsd ?? 0;
|
|
42
|
+
stats.costBreakdown = {
|
|
43
|
+
llmRootMicroUsd: Math.max(0, stats.costMicroUsd - compactionMicroUsd),
|
|
44
|
+
nestedSubagentMicroUsd,
|
|
45
|
+
memoryConsolidationMicroUsd: 0,
|
|
46
|
+
compactionMicroUsd,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
47
49
|
let status;
|
|
48
50
|
const result = text;
|
|
49
51
|
let errorMessage;
|
|
@@ -139,5 +141,7 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
139
141
|
}
|
|
140
142
|
const { compactionMicroUsd: _internalCompaction, ...publicStats } = stats;
|
|
141
143
|
void _internalCompaction;
|
|
144
|
+
if (flags.unpricedSpend)
|
|
145
|
+
delete publicStats.costMicroUsd;
|
|
142
146
|
return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, checkpointToken, checkpointGate, stats: publicStats };
|
|
143
147
|
}
|
|
@@ -114,6 +114,7 @@ export interface Prepared {
|
|
|
114
114
|
apply: (committedArtifactDigest?: string) => void;
|
|
115
115
|
} | undefined;
|
|
116
116
|
activeTools: Set<string>;
|
|
117
|
+
deferredToolNames?: ReadonlySet<string>;
|
|
117
118
|
memoryEngineSession?: {
|
|
118
119
|
engine: MemoryEngine;
|
|
119
120
|
handle: MemorySessionHandle;
|