@sema-agent/core 2.1.0 → 2.2.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.
Files changed (53) hide show
  1. package/dist/agents/observer.d.ts +14 -0
  2. package/dist/agents/observer.js +58 -9
  3. package/dist/agents/send-message-tool.js +55 -10
  4. package/dist/agents/subagent.d.ts +1 -0
  5. package/dist/agents/subagent.js +69 -15
  6. package/dist/core/context-edit.js +16 -3
  7. package/dist/core/file-snapshot-store.js +10 -1
  8. package/dist/core/runner/prepare-task.d.ts +1 -0
  9. package/dist/core/runner/prepare-task.js +29 -5
  10. package/dist/core/runner/runtask.js +9 -0
  11. package/dist/core/runner/synthetic-tools.d.ts +1 -0
  12. package/dist/core/runner/synthetic-tools.js +18 -15
  13. package/dist/core/runner/turn-attachments.d.ts +12 -2
  14. package/dist/core/runner/turn-attachments.js +33 -3
  15. package/dist/core/task-registry-agent.d.ts +9 -1
  16. package/dist/core/task-registry-agent.js +23 -2
  17. package/dist/core/task-registry-monitor.js +79 -24
  18. package/dist/core/task-registry-shared.d.ts +13 -1
  19. package/dist/core/task-registry-shared.js +21 -0
  20. package/dist/core/task-registry.d.ts +2 -0
  21. package/dist/core/task-registry.js +24 -26
  22. package/dist/core/types.d.ts +3 -1
  23. package/dist/engine/session/memory-repo.js +5 -0
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/orchestration/workflow-size-guideline.d.ts +6 -1
  27. package/dist/orchestration/workflow-size-guideline.js +19 -9
  28. package/dist/orchestration/workflow.d.ts +1 -0
  29. package/dist/orchestration/workflow.js +18 -2
  30. package/dist/prompt-assembly/assemble.js +3 -7
  31. package/dist/prompt-assembly/packs/sema-default.js +8 -5
  32. package/dist/prompts/coordinator.d.ts +1 -1
  33. package/dist/prompts/coordinator.js +45 -0
  34. package/dist/prompts/default.d.ts +4 -5
  35. package/dist/prompts/default.js +16 -18
  36. package/dist/prompts/simple-sections.d.ts +3 -1
  37. package/dist/prompts/simple-sections.js +11 -1
  38. package/dist/stores/file/workflow-journal-store.d.ts +7 -1
  39. package/dist/stores/file/workflow-journal-store.js +70 -33
  40. package/dist/tools/fs/bash-readonly-classifier.js +20 -1
  41. package/dist/tools/fs/fs-bash.d.ts +1 -0
  42. package/dist/tools/fs/fs-bash.js +10 -3
  43. package/dist/tools/fs/fs-read.js +10 -10
  44. package/dist/tools/fs/fs-search-tools.js +42 -7
  45. package/dist/tools/fs/fs-write.js +18 -6
  46. package/dist/tools/fs/index.d.ts +1 -0
  47. package/dist/tools/fs/index.js +2 -1
  48. package/dist/tools/fs/safety.d.ts +4 -0
  49. package/dist/tools/fs/safety.js +102 -6
  50. package/dist/tools/fs/search.d.ts +1 -0
  51. package/dist/tools/fs/search.js +23 -3
  52. package/dist/tools/monitor.js +1 -1
  53. package/package.json +3 -2
@@ -26,6 +26,7 @@ import { cacheFamilyOf, usageCostMicroUsd } from "./usage-accounting.js";
26
26
  import { assembleResult, errorCodeOf } from "./assemble-result.js";
27
27
  import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTING_REMOVED_HEADER, advanceCadenceClock, agentListingDeltaHeader, agentListingInitialHeader, replayAnnouncedListing, replayAnnouncedModels, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, createAttachmentState, rebaseCadenceWindows, reduceToolEnd, renderAgentListingDelta, renderMcpDroppedTools, renderMcpInstructionsDelta, renderOrphanedBackgroundTasks, selectMcpDroppedBatch, renderSkillsListingDelta, renderToolsDelta, stampWriteAnchor } from "./turn-attachments.js";
28
28
  import { prepareTask } from "./prepare-task.js";
29
+ import { TOOL_SEARCH_NAME } from "./tool-disclosure.js";
29
30
  import { hasVerifiableStructureSignal } from "./grounding-signal.js";
30
31
  import { hasDestroy, isIsolated } from "../remote-env.js";
31
32
  import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
@@ -150,6 +151,8 @@ function writeFamilyOfCanonical(name) {
150
151
  return "task";
151
152
  if (name === "TodoWrite")
152
153
  return "todo";
154
+ if (name === TOOL_SEARCH_NAME)
155
+ return "tool_search";
153
156
  return undefined;
154
157
  }
155
158
  function toolResultMsg(toolCallId, toolName, text, isError) {
@@ -366,6 +369,10 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
366
369
  const mcpRemovals = mcpInstructionsOn && mcpDelta.pendingRemovals.length > 0 ? [...mcpDelta.pendingRemovals] : undefined;
367
370
  const mcpDropped = mcpInstructionsOn && prepared.mcp.droppedTools.length > 0 ? selectMcpDroppedBatch(prepared.mcp.droppedTools) : undefined;
368
371
  const budgetUsdOn = rs.attach.attachmentsCfg?.budgetUsd === true && rs.budget.maxCostMicroUsd !== undefined && rs.turn.lastTurnHadToolCalls;
372
+ const toolSearchReminderOn = rs.attach.attachmentsCfg?.toolSearchReminder === true && prepared.deferredToolNames !== undefined;
373
+ const undiscoveredTools = toolSearchReminderOn
374
+ ? [...prepared.deferredToolNames].filter((n) => !prepared.activeTools.has(n)).sort()
375
+ : undefined;
369
376
  due.push(...collectDueAttachments(rs.attach.attachState, {
370
377
  turn: stats.turns,
371
378
  cadenceTurn: rs.counters.cadenceTurns,
@@ -375,6 +382,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
375
382
  config: {
376
383
  todoReminder: rs.attach.attachmentsCfg?.todoReminder === true,
377
384
  ...(rs.attach.attachmentsCfg?.todoReminderMode !== undefined ? { todoReminderMode: rs.attach.attachmentsCfg.todoReminderMode } : {}),
385
+ toolSearchReminder: toolSearchReminderOn,
378
386
  changedFiles: changedFilesOn,
379
387
  planModeReminder: rs.attach.attachmentsCfg?.planModeReminder === true,
380
388
  budgetUsd: budgetUsdOn,
@@ -384,6 +392,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
384
392
  skillsListing: rs.attach.skillsListingOn,
385
393
  mcpInstructions: mcpInstructionsOn,
386
394
  },
395
+ ...(undiscoveredTools !== undefined && undiscoveredTools.length > 0 ? { undiscoveredTools } : {}),
387
396
  ...(changed !== undefined ? { changedFiles: changed } : {}),
388
397
  ...(budgetUsdOn && rs.budget.maxCostMicroUsd !== undefined
389
398
  ? { budgetUsd: { used: stats.costMicroUsd / 1e6, total: rs.budget.maxCostMicroUsd / 1e6 } }
@@ -47,6 +47,7 @@ export interface SkillListingEntry {
47
47
  files?: Array<{
48
48
  path: string;
49
49
  }>;
50
+ declaredRank?: number;
50
51
  }
51
52
  export declare function normalizeSkills(skills: SkillLike[] | undefined): SkillLike[];
52
53
  export declare const SKILLS_LISTING_PROBE_HEADER = "The following skills are available for this task.";
@@ -85,7 +85,7 @@ export function createReportFindingsTool() {
85
85
  }
86
86
  export const SKILL_TOOL_NAME = "Skill";
87
87
  export const SKILL_CONTENT_MAX_CHARS = 1024 * 1024;
88
- const SKILL_DESC_MAX_CHARS = 1_000;
88
+ const SKILL_DESC_MAX_CHARS = 1_536;
89
89
  export const SKILLS_BLOCK_MAX_BYTES = 8_000;
90
90
  export const SKILL_ATTACHMENTS_MAX_CHARS = 50_000;
91
91
  export const SKILL_ATTACHMENT_PATH_MAX_CHARS = 512;
@@ -121,11 +121,11 @@ export function normalizeSkills(skills) {
121
121
  export const SKILLS_LISTING_PROBE_HEADER = "The following skills are available for this task.";
122
122
  export function skillListingLine(s) {
123
123
  const desc = s.description.length > SKILL_DESC_MAX_CHARS
124
- ? `${s.description.slice(0, SKILL_DESC_MAX_CHARS)}…[description truncated]`
124
+ ? `${s.description.slice(0, SKILL_DESC_MAX_CHARS - 1)}…`
125
125
  : s.description;
126
126
  return `- ${s.name}: ${desc}${skillFilesSuffix(s.files)}`;
127
127
  }
128
- const SKILLS_BLOCK_CAP_MARKER = "(block size cap reached — remaining skills listed name-only; descriptions via the tool)";
128
+ const SKILLS_BLOCK_CAP_MARKER = "(block size cap reached — lower-priority skills are listed name-only; descriptions via the tool)";
129
129
  export function buildSkillsBlock(skills) {
130
130
  if (skills.length === 0)
131
131
  return undefined;
@@ -145,20 +145,23 @@ export function buildSkillsBlock(skills) {
145
145
  const fullTotal = fullLines.reduce((acc, l) => acc + lineCost(l), 0);
146
146
  if (fullTotal <= budget)
147
147
  return render(fullLines);
148
- const suffixNameCost = new Array(skills.length + 1);
149
- suffixNameCost[skills.length] = 0;
150
- for (let i = skills.length - 1; i >= 0; i--)
151
- suffixNameCost[i] = suffixNameCost[i + 1] + lineCost(nameLines[i]);
152
148
  const markerCost = lineCost(SKILLS_BLOCK_CAP_MARKER);
153
- let bestK = -1;
154
- let prefixCost = 0;
155
- for (let k = 0; k < skills.length; k++) {
156
- if (prefixCost + markerCost + suffixNameCost[k] <= budget)
157
- bestK = k;
158
- prefixCost += lineCost(fullLines[k]);
149
+ const floorCost = markerCost + nameLines.reduce((acc, l) => acc + lineCost(l), 0);
150
+ if (floorCost <= budget) {
151
+ let slack = budget - floorCost;
152
+ const byPriority = skills
153
+ .map((s, i) => ({ i, rank: s.declaredRank ?? i }))
154
+ .toSorted((a, b) => a.rank - b.rank || a.i - b.i);
155
+ const withDescription = new Set();
156
+ for (const { i } of byPriority) {
157
+ const delta = lineCost(fullLines[i]) - lineCost(nameLines[i]);
158
+ if (delta <= slack) {
159
+ withDescription.add(i);
160
+ slack -= delta;
161
+ }
162
+ }
163
+ return render([SKILLS_BLOCK_CAP_MARKER, ...skills.map((_s, i) => (withDescription.has(i) ? fullLines[i] : nameLines[i]))]);
159
164
  }
160
- if (bestK >= 0)
161
- return render([...fullLines.slice(0, bestK), SKILLS_BLOCK_CAP_MARKER, ...nameLines.slice(bestK)]);
162
165
  for (let m = skills.length - 1; m >= 0; m--) {
163
166
  const overflowLine = `(… +${skills.length - m} more skills not listed — the ${SKILL_TOOL_NAME} tool serves them all)`;
164
167
  const cost = markerCost + nameLines.slice(0, m).reduce((acc, l) => acc + lineCost(l), 0) + lineCost(overflowLine);
@@ -7,10 +7,14 @@ export declare const PLAN_MODE_ATTACHMENT_CONFIG: {
7
7
  readonly TURNS_BETWEEN_ATTACHMENTS: 5;
8
8
  readonly FULL_REMINDER_EVERY_N_ATTACHMENTS: 5;
9
9
  };
10
+ export declare const TOOL_SEARCH_REMINDER_CONFIG: {
11
+ readonly EVERY_N_TURNS: 15;
12
+ readonly MAX_NAMES: 10;
13
+ };
10
14
  export declare const CHANGED_FILES_MAX = 20;
11
15
  export declare const CHANGED_FILES_MTIME_EPS_MS = 2000;
12
16
  export declare const ATTACHMENT_BYTE_CAP: number;
13
- export type AttachmentSource = "todo_reminder" | "task_reminder" | "changed_files" | "plan_mode" | "date_change" | "instructions_change" | "budget_usd" | "background_tasks" | "tools_delta" | "agent_listing" | "skills_listing" | "mcp_instructions" | "mcp_dropped_tools";
17
+ export type AttachmentSource = "todo_reminder" | "task_reminder" | "tool_search_usage_reminder" | "changed_files" | "plan_mode" | "date_change" | "instructions_change" | "budget_usd" | "background_tasks" | "tools_delta" | "agent_listing" | "skills_listing" | "mcp_instructions" | "mcp_dropped_tools";
14
18
  export interface AgentListingEntry {
15
19
  name: string;
16
20
  description: string;
@@ -42,6 +46,8 @@ export interface AttachmentState {
42
46
  todoLastWriteTurn?: number;
43
47
  lastTodoReminderTurn: number;
44
48
  lastTaskReminderTurn: number;
49
+ toolSearchLastUseTurn?: number;
50
+ lastToolSearchReminderTurn: number;
45
51
  planAttachmentCount: number;
46
52
  lastPlanReminderTurn: number;
47
53
  surfacedMtime: Map<string, number>;
@@ -60,7 +66,7 @@ export interface InstructionsChangeState {
60
66
  export declare const INSTRUCTIONS_CHANGE_BYTE_CAP = 512;
61
67
  export declare function createAttachmentState(): AttachmentState;
62
68
  export declare function reduceToolEnd(state: AttachmentState, details: unknown): void;
63
- export type WriteFamily = "todo" | "task";
69
+ export type WriteFamily = "todo" | "task" | "tool_search";
64
70
  export declare function stampWriteAnchor(state: AttachmentState, family: WriteFamily, clock: number): void;
65
71
  export declare function advanceCadenceClock(state: AttachmentState, clock: number, content: unknown, writeFamilyOf: (toolName: string) => WriteFamily | undefined): number;
66
72
  export declare function rebaseCadenceWindows(state: AttachmentState, clock: number): void;
@@ -77,6 +83,7 @@ export interface AttachmentInputs {
77
83
  config: {
78
84
  todoReminder: boolean;
79
85
  todoReminderMode?: "baseline" | "off";
86
+ toolSearchReminder?: boolean;
80
87
  changedFiles: boolean;
81
88
  planModeReminder: boolean;
82
89
  budgetUsd?: boolean;
@@ -86,6 +93,7 @@ export interface AttachmentInputs {
86
93
  skillsListing?: boolean;
87
94
  mcpInstructions?: boolean;
88
95
  };
96
+ undiscoveredTools?: readonly string[];
89
97
  changedFiles?: ReadonlyArray<{
90
98
  path: string;
91
99
  mtimeMs: number;
@@ -111,6 +119,7 @@ export interface AttachmentInputs {
111
119
  }>;
112
120
  }
113
121
  export declare function collectDueAttachments(state: AttachmentState, inp: AttachmentInputs): readonly TurnAttachment[];
122
+ export declare function renderToolSearchUsageReminder(undiscovered: readonly string[]): string;
114
123
  export declare function renderDateChange(newDate: string): string;
115
124
  export declare function collectDateChange(state: DateChangeState, today: string): TurnAttachment | undefined;
116
125
  export declare function collectInstructionsChange(state: InstructionsChangeState, probed: ReadonlyArray<{
@@ -144,6 +153,7 @@ export interface McpToolsDeltaFacts {
144
153
  export declare function renderToolsDelta(input: {
145
154
  added?: readonly string[];
146
155
  } & McpToolsDeltaFacts): string | undefined;
156
+ export declare const AGENT_TOOLS_NOTE_DEFAULT = "All tools";
147
157
  export declare const AGENT_CONCURRENCY_NOTE = "When you launch multiple agents for independent work, send them in a single message with multiple tool uses so they run concurrently.";
148
158
  export declare const AMBIENT_CONTEXT_NOTE = "This is ambient context \u2014 do not narrate it to the user unless they ask or it is directly relevant to their request.";
149
159
  export declare function agentListingInitialHeader(toolName: string): string;
@@ -10,6 +10,10 @@ export const PLAN_MODE_ATTACHMENT_CONFIG = {
10
10
  TURNS_BETWEEN_ATTACHMENTS: 5,
11
11
  FULL_REMINDER_EVERY_N_ATTACHMENTS: 5,
12
12
  };
13
+ export const TOOL_SEARCH_REMINDER_CONFIG = {
14
+ EVERY_N_TURNS: 15,
15
+ MAX_NAMES: 10,
16
+ };
13
17
  export const CHANGED_FILES_MAX = 20;
14
18
  export const CHANGED_FILES_MTIME_EPS_MS = 2000;
15
19
  export const ATTACHMENT_BYTE_CAP = 8 * 1024;
@@ -20,6 +24,7 @@ export function createAttachmentState() {
20
24
  return {
21
25
  lastTodoReminderTurn: 0,
22
26
  lastTaskReminderTurn: 0,
27
+ lastToolSearchReminderTurn: 0,
23
28
  planAttachmentCount: 0,
24
29
  lastPlanReminderTurn: 0,
25
30
  surfacedMtime: new Map(),
@@ -84,6 +89,8 @@ export function reduceToolEnd(state, details) {
84
89
  export function stampWriteAnchor(state, family, clock) {
85
90
  if (family === "task")
86
91
  state.taskLastWriteTurn = clock;
92
+ else if (family === "tool_search")
93
+ state.toolSearchLastUseTurn = clock;
87
94
  else
88
95
  state.todoLastWriteTurn = clock;
89
96
  }
@@ -109,6 +116,8 @@ export function rebaseCadenceWindows(state, clock) {
109
116
  state.todoLastWriteTurn = clock;
110
117
  state.lastTaskReminderTurn = clock;
111
118
  state.lastTodoReminderTurn = clock;
119
+ state.toolSearchLastUseTurn = clock;
120
+ state.lastToolSearchReminderTurn = clock;
112
121
  }
113
122
  function countStatuses(items) {
114
123
  const counts = {};
@@ -138,6 +147,17 @@ export function collectDueAttachments(state, inp) {
138
147
  }
139
148
  }
140
149
  }
150
+ if (inp.config.toolSearchReminder &&
151
+ inp.undiscoveredTools !== undefined &&
152
+ inp.undiscoveredTools.length > 0 &&
153
+ !(out ?? []).some((a) => a.source === "todo_reminder" || a.source === "task_reminder")) {
154
+ const cadenceNow = inp.cadenceTurn ?? inp.turn;
155
+ const sinceUse = state.toolSearchLastUseTurn === undefined ? Number.POSITIVE_INFINITY : cadenceNow - state.toolSearchLastUseTurn;
156
+ if (sinceUse >= TOOL_SEARCH_REMINDER_CONFIG.EVERY_N_TURNS && cadenceNow - state.lastToolSearchReminderTurn >= TOOL_SEARCH_REMINDER_CONFIG.EVERY_N_TURNS) {
157
+ state.lastToolSearchReminderTurn = cadenceNow;
158
+ (out ??= []).push({ source: "tool_search_usage_reminder", body: renderToolSearchUsageReminder(inp.undiscoveredTools) });
159
+ }
160
+ }
141
161
  if (inp.config.planModeReminder && inp.planActive) {
142
162
  const due = state.planAttachmentCount === 0 ||
143
163
  inp.turn - state.lastPlanReminderTurn >= PLAN_MODE_ATTACHMENT_CONFIG.TURNS_BETWEEN_ATTACHMENTS;
@@ -228,6 +248,15 @@ function renderTaskReminder(state) {
228
248
  }
229
249
  return message;
230
250
  }
251
+ export function renderToolSearchUsageReminder(undiscovered) {
252
+ const shown = undiscovered.slice(0, TOOL_SEARCH_REMINDER_CONFIG.MAX_NAMES);
253
+ const more = undiscovered.length - shown.length;
254
+ const names = `${shown.join(", ")}${more > 0 ? ` (+${more} more)` : ""}`;
255
+ return (`Some available tools' schemas are not loaded in this conversation yet: ${names}. Before concluding a ` +
256
+ `capability is missing or building a workaround, use ${TOOL_SEARCH_TOOL_NAME} to find and load relevant tools — ` +
257
+ `keywords to search, or query "select:<name>[,<name>...]" for specific tools. Calling a tool before its schema ` +
258
+ `is loaded will fail. This is just a gentle reminder - ignore if not applicable to the current work.`);
259
+ }
231
260
  export function renderDateChange(newDate) {
232
261
  return `The date has changed. Today's date is now ${newDate}. DO NOT mention this to the user explicitly because they are already aware.`;
233
262
  }
@@ -399,6 +428,7 @@ export function renderToolsDelta(input) {
399
428
  }
400
429
  return blocks.length > 0 ? blocks.join("\n\n") : undefined;
401
430
  }
431
+ export const AGENT_TOOLS_NOTE_DEFAULT = "All tools";
402
432
  export const AGENT_CONCURRENCY_NOTE = "When you launch multiple agents for independent work, send them in a single message with multiple tool uses so they run concurrently.";
403
433
  export const AMBIENT_CONTEXT_NOTE = "This is ambient context — do not narrate it to the user unless they ask or it is directly relevant to their request.";
404
434
  export function agentListingInitialHeader(toolName) {
@@ -451,7 +481,7 @@ export function agentListingDeltaHeader(toolName) {
451
481
  export function renderAgentListingDelta(state, entries, toolName, models) {
452
482
  const line = (e) => {
453
483
  const base = e.description ? `- ${e.name}: ${e.description}` : `- ${e.name}`;
454
- return e.tools !== undefined ? `${base} (Tools: ${e.tools})` : base;
484
+ return `${base} (Tools: ${e.tools ?? AGENT_TOOLS_NOTE_DEFAULT})`;
455
485
  };
456
486
  const announced = state.announcedAgentTypes;
457
487
  if (announced === undefined) {
@@ -464,8 +494,8 @@ export function renderAgentListingDelta(state, entries, toolName, models) {
464
494
  blocks.push(modelsAvailableLine(models));
465
495
  return blocks.join("\n\n");
466
496
  }
467
- const added = entries.filter((e) => !announced.has(e.name));
468
- const removed = [...announced.keys()].filter((n) => !entries.some((e) => e.name === n));
497
+ const added = entries.filter((e) => !announced.has(e.name)).toSorted((a, b) => a.name.localeCompare(b.name));
498
+ const removed = [...announced.keys()].filter((n) => !entries.some((e) => e.name === n)).sort();
469
499
  const modelsDrifted = state.announcedModels !== undefined &&
470
500
  models !== undefined &&
471
501
  (state.announcedModels.length !== models.length || state.announcedModels.some((m, i) => m !== models[i]));
@@ -110,6 +110,14 @@ export declare function deliverToRunningAgentLane(core: DurableAgentCore, id: st
110
110
  reason: "not_found" | "not_running" | "no_channel";
111
111
  }>;
112
112
  export declare function runningBackgroundAgentLabelsLane(core: DurableAgentCore, access: TaskAccess): string[];
113
+ export declare function runningAgentFooterLane(core: DurableAgentCore, access: TaskAccess): {
114
+ named: string[];
115
+ background: string[];
116
+ };
117
+ export declare function notFoundRunningAgentsTail(footer: {
118
+ named: string[];
119
+ background: string[];
120
+ }): string;
113
121
  export declare function serveDurableAgentRowLane(row: BackgroundAgentRecord): UnifiedTaskResult;
114
- export declare function pollBackgroundAgentLane(handle: BackgroundAgentTaskHandle, deadline?: number, signal?: AbortSignal): Promise<UnifiedTaskResult>;
122
+ export declare function pollBackgroundAgentLane(handle: BackgroundAgentTaskHandle, deadline?: number, signal?: AbortSignal, oneShot?: boolean): Promise<UnifiedTaskResult>;
115
123
  export declare function stopBackgroundAgentLane(core: DurableAgentCore, handle: BackgroundAgentTaskHandle): Promise<UnifiedTaskResult>;
@@ -972,6 +972,24 @@ export function runningBackgroundAgentLabelsLane(core, access) {
972
972
  }
973
973
  return out;
974
974
  }
975
+ export function runningAgentFooterLane(core, access) {
976
+ const named = [];
977
+ const background = [];
978
+ for (const h of core.handles.values()) {
979
+ if (h.type !== "background_agent" || h.status !== "running" || !canAccess(h, access))
980
+ continue;
981
+ if (h.name !== undefined && h.name !== "") {
982
+ named.push(h.name);
983
+ continue;
984
+ }
985
+ background.push(h.description ? `${h.id} (${h.description})` : h.id);
986
+ }
987
+ return { named, background };
988
+ }
989
+ export function notFoundRunningAgentsTail(footer) {
990
+ return ((footer.named.length > 0 ? `. Running named agents: ${footer.named.join(", ")}` : "") +
991
+ (footer.background.length > 0 ? `. Running background agents: ${footer.background.join(", ")}` : ""));
992
+ }
975
993
  export function serveDurableAgentRowLane(row) {
976
994
  if (row.status === "parked") {
977
995
  return {
@@ -1004,7 +1022,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
1004
1022
  },
1005
1023
  };
1006
1024
  }
1007
- export async function pollBackgroundAgentLane(handle, deadline, signal) {
1025
+ export async function pollBackgroundAgentLane(handle, deadline, signal, oneShot) {
1008
1026
  while (handle.status === "running" && deadline !== undefined && Date.now() < deadline && !signal?.aborted) {
1009
1027
  await sleepPollStep(deadline, signal);
1010
1028
  }
@@ -1024,7 +1042,10 @@ The agent is durably suspended, waiting for an approval decision. It resumes whe
1024
1042
  };
1025
1043
  }
1026
1044
  const body = running
1027
- ? `status: running
1045
+ ? oneShot === true
1046
+ ? `status: running
1047
+ 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: "${handle.id}", 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.`
1048
+ : `status: running
1028
1049
  The agent is still working — you will be notified when it completes.`
1029
1050
  : `status: ${handle.status}
1030
1051
  ${handle.error ? `error: ${handle.error}
@@ -1,5 +1,5 @@
1
1
  import { delimitUntrusted } from "./untrusted-text.js";
2
- import { assertOwnership, defaultMonitorTimers, MONITOR_MAX_TIMEOUT_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_BATCH_WINDOW_MS, MONITOR_MAX_BATCHES_PER_MINUTE, TASK_OUTPUT_MAX_CHARS, mintCompletionId, clipMonitorEvent, accountDroppedBytes, rollSpoolText, statusFromBackground, droppedGapNote, firstDropNote, alreadyTerminalStopNote, clipTaskOutput, sleepPollStep, } from "./task-registry-shared.js";
2
+ import { assertOwnership, defaultMonitorTimers, MONITOR_MAX_TIMEOUT_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_BATCH_WINDOW_MS, MONITOR_MAX_BATCHES_PER_MINUTE, MONITOR_STORM_BURST, MONITOR_STORM_KILL_AFTER_MS, MONITOR_LINE_BUF_CAP, TASK_OUTPUT_MAX_CHARS, mintCompletionId, clipMonitorEvent, clipMonitorLine, terminalTaskSummary, accountDroppedBytes, rollSpoolText, statusFromBackground, droppedGapNote, firstDropNote, alreadyTerminalStopNote, clipTaskOutput, sleepPollStep, } from "./task-registry-shared.js";
3
3
  export function registerMonitorLane(core, input) {
4
4
  assertOwnership(input, "registerMonitor");
5
5
  const id = core.mintTaskId("monitor");
@@ -9,6 +9,8 @@ export function registerMonitorLane(core, input) {
9
9
  const timeoutMs = persistent
10
10
  ? undefined
11
11
  : Math.min(MONITOR_MAX_TIMEOUT_MS, Math.max(1, Math.floor(input.timeoutMs ?? MONITOR_DEFAULT_TIMEOUT_MS)));
12
+ const maxBatchesPerMinute = Math.max(1, Math.floor(input.maxBatchesPerMinute ?? MONITOR_MAX_BATCHES_PER_MINUTE));
13
+ const stormBurst = Math.max(1, Math.floor(input.stormBurst ?? MONITOR_STORM_BURST));
12
14
  const handle = {
13
15
  id,
14
16
  type: "monitor",
@@ -26,9 +28,12 @@ export function registerMonitorLane(core, input) {
26
28
  spool: { stdout: "", stderr: "", rolledChars: 0 },
27
29
  lineBuf: "",
28
30
  seq: 0,
29
- batchTimes: [],
31
+ stormTokens: stormBurst,
32
+ stormBurst,
33
+ stormRefillIntervalMs: Math.max(1, Math.round(60_000 / maxBatchesPerMinute)),
34
+ stormRefilledAt: timers.now(),
30
35
  batchWindowMs: Math.max(1, Math.floor(input.batchWindowMs ?? MONITOR_BATCH_WINDOW_MS)),
31
- maxBatchesPerMinute: Math.max(1, Math.floor(input.maxBatchesPerMinute ?? MONITOR_MAX_BATCHES_PER_MINUTE)),
36
+ maxBatchesPerMinute,
32
37
  ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt: timers.now() + timeoutMs } : {}),
33
38
  timers,
34
39
  ...(input.onEvent !== undefined ? { onEvent: input.onEvent } : {}),
@@ -44,10 +49,23 @@ export function absorbMonitorPollLane(handle, v) {
44
49
  spool.stdout = rollSpoolText(spool, spool.stdout + v.stdout, ROLL_CAP);
45
50
  spool.stderr = rollSpoolText(spool, spool.stderr + v.stderr, ROLL_CAP);
46
51
  handle.lineBuf += v.stdout;
52
+ if (handle.lineBuf.length > MONITOR_LINE_BUF_CAP)
53
+ handle.lineBuf = handle.lineBuf.slice(-MONITOR_LINE_BUF_CAP);
47
54
  const parts = handle.lineBuf.split(/\r?\n/);
48
55
  handle.lineBuf = parts.pop() ?? "";
49
56
  return parts.filter((l) => l !== "");
50
57
  }
58
+ function consumeStormToken(handle, now) {
59
+ const gained = Math.floor((now - handle.stormRefilledAt) / handle.stormRefillIntervalMs);
60
+ if (gained > 0) {
61
+ handle.stormTokens = Math.min(handle.stormBurst, handle.stormTokens + gained);
62
+ handle.stormRefilledAt += gained * handle.stormRefillIntervalMs;
63
+ }
64
+ if (handle.stormTokens <= 0)
65
+ return false;
66
+ handle.stormTokens -= 1;
67
+ return true;
68
+ }
51
69
  export function emitMonitorEventLane(handle, n) {
52
70
  try {
53
71
  handle.onEvent?.(n);
@@ -75,7 +93,8 @@ export function startMonitorWatcherLane(core, id) {
75
93
  handle.lineBuf = "";
76
94
  return out;
77
95
  };
78
- const terminal = (status, summary, finalLines) => {
96
+ const pendingSuppressionNote = () => consumePendingSuppressionNote(handle);
97
+ const terminal = (status, detail, finalLines, exitCode) => {
79
98
  handle.updatedAt = Date.now();
80
99
  mintCompletionId(handle);
81
100
  stop();
@@ -85,8 +104,8 @@ export function startMonitorWatcherLane(core, id) {
85
104
  ...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
86
105
  status,
87
106
  ...(status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
88
- summary: `${(handle.description ?? "monitor").slice(0, 200)} — ${summary}${droppedGapNote(handle.spool)}`,
89
- ...(finalLines.length > 0 ? { lines: finalLines, result: clipMonitorEvent(finalLines.join("\n")), ...(status === "killed" ? { partial: true } : {}) } : {}),
107
+ summary: `${terminalTaskSummary("monitor", (handle.description ?? "monitor").slice(0, 200), status, exitCode)} — ${detail}${droppedGapNote(handle.spool)}`,
108
+ ...(finalLines.length > 0 ? { lines: finalLines.map(clipMonitorLine), result: clipMonitorEvent(finalLines.map(clipMonitorLine).join("\n")), ...(status === "killed" ? { partial: true } : {}) } : {}),
90
109
  ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
91
110
  }));
92
111
  };
@@ -107,7 +126,7 @@ export function startMonitorWatcherLane(core, id) {
107
126
  if ((r.error.code === "timeout" || r.error.code === "io") && ++pollFailures <= MAX_RETRYABLE_POLL_FAILURES)
108
127
  return;
109
128
  handle.status = "failed";
110
- return terminal("failed", `monitor lost its process (${r.error.message}); watch ended.`, pendingFinalLines([]));
129
+ return terminal("failed", `monitor lost its process (${r.error.message}); watch ended.${pendingSuppressionNote()}`, pendingFinalLines([]));
111
130
  }
112
131
  pollFailures = 0;
113
132
  const spool = handle.spool;
@@ -127,7 +146,7 @@ export function startMonitorWatcherLane(core, id) {
127
146
  : r.value.timedOut === true
128
147
  ? `timed out after ${r.value.timeoutSec !== undefined ? `${r.value.timeoutSec}s` : "its background time budget"} (the execution environment's background time budget); the process was killed and the watch ended.`
129
148
  : `${r.value.status}; watch ended.`;
130
- return terminal(handle.status === "completed" ? "completed" : handle.status === "killed" ? "killed" : "failed", label, pendingFinalLines(lines));
149
+ return terminal(handle.status === "completed" ? "completed" : handle.status === "killed" ? "killed" : "failed", `${label}${pendingSuppressionNote()}`, pendingFinalLines(lines), r.value.status === "exited" ? r.value.exitCode : undefined);
131
150
  }
132
151
  const now = timers.now();
133
152
  if (handle.deadlineAt !== undefined && now >= handle.deadlineAt) {
@@ -137,45 +156,74 @@ export function startMonitorWatcherLane(core, id) {
137
156
  return stop();
138
157
  if (!k.ok && k.error.code !== "not_found") {
139
158
  handle.status = "failed";
140
- return terminal("failed", `timed out after ${handle.timeoutMs}ms, but the kill failed (${k.error.message}); the process may still be running. Watch ended.`, pendingFinalLines(lines));
159
+ return terminal("failed", `timed out after ${handle.timeoutMs}ms, but the kill failed (${k.error.message}); the process may still be running. Watch ended.${pendingSuppressionNote()}`, pendingFinalLines(lines));
141
160
  }
142
161
  handle.status = "killed";
143
162
  handle.stoppedBy = handle.stopSource ?? "timeout";
144
- return terminal("killed", `timed out after ${handle.timeoutMs}ms; the process was killed and the watch ended.`, pendingFinalLines(lines));
163
+ return terminal("killed", `timed out after ${handle.timeoutMs}ms; the process was killed and the watch ended.${pendingSuppressionNote()}`, pendingFinalLines(lines));
145
164
  }
146
165
  if (lines.length === 0) {
147
166
  if (r.value.stdout.length > 0 || r.value.stderr.length > 0)
148
167
  handle.updatedAt = Date.now();
149
168
  return;
150
169
  }
151
- const cutoff = now - 60_000;
152
- handle.batchTimes = handle.batchTimes.filter((t) => t > cutoff);
153
- handle.batchTimes.push(now);
154
- if (handle.batchTimes.length > handle.maxBatchesPerMinute) {
170
+ if (!consumeStormToken(handle, now)) {
171
+ const previousSuppressedAt = handle.lastSuppressedAt;
172
+ handle.suppressedBatches = (handle.suppressedBatches ?? 0) + 1;
173
+ handle.lastSuppressedAt = now;
174
+ if (previousSuppressedAt !== undefined && now - previousSuppressedAt > handle.stormRefillIntervalMs * 3) {
175
+ handle.overloadSince = now;
176
+ }
177
+ else {
178
+ handle.overloadSince ??= now;
179
+ }
180
+ handle.updatedAt = Date.now();
181
+ if (now - handle.overloadSince <= MONITOR_STORM_KILL_AFTER_MS)
182
+ return;
183
+ const suppressed = handle.suppressedBatches;
184
+ const overloadSecs = Math.round((now - handle.overloadSince) / 1000);
155
185
  core.markStopSource(id, "system");
156
186
  const k = await handle.env.killBackground(handle.shellId);
157
187
  if (core.handles.get(id) !== handle || handle.status !== "running")
158
188
  return stop();
159
189
  if (!k.ok && k.error.code !== "not_found") {
160
190
  handle.status = "failed";
161
- return terminal("failed", `auto-stop triggered (more than ${handle.maxBatchesPerMinute} notification batches within a minute), but the kill failed (${k.error.message}); the process may still be running. Watch ended.`, pendingFinalLines(lines));
191
+ return terminal("failed", `auto-stop triggered (${suppressed} event batch(es) suppressed over ${overloadSecs}s of sustained overload), but the kill failed (${k.error.message}); the process may still be running. Watch ended.`, pendingFinalLines(lines));
162
192
  }
163
193
  handle.status = "killed";
164
194
  handle.stoppedBy = handle.stopSource ?? "system";
165
- return terminal("killed", `auto-stopped: it produced too many events (more than ${handle.maxBatchesPerMinute} notification batches within a minute). ` +
195
+ return terminal("killed", `auto-stopped: too much output (${suppressed} event batch(es) suppressed over ${overloadSecs}s of sustained overload; sustained budget is ${handle.maxBatchesPerMinute} batches/minute). ` +
166
196
  `The process was killed; its full output remains readable via TaskOutput. Restart it with a quieter command (filter its output) if you still need the watch.`, pendingFinalLines(lines));
167
197
  }
198
+ if ((handle.suppressedBatches ?? 0) > 0) {
199
+ const suppressed = handle.suppressedBatches;
200
+ handle.suppressedBatches = 0;
201
+ if (handle.lastSuppressedAt !== undefined && now - handle.lastSuppressedAt > handle.stormRefillIntervalMs * 3)
202
+ handle.overloadSince = undefined;
203
+ handle.seq += 1;
204
+ const note = `[${suppressed} events suppressed — output rate too high. Consider using TaskStop to restart this monitor with a more selective filter.]`;
205
+ emit({
206
+ task_id: id,
207
+ task_type: "monitor",
208
+ ...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
209
+ status: "event",
210
+ seq: handle.seq,
211
+ summary: `${(handle.description ?? "monitor").slice(0, 200)} — ${suppressed} event batch(es) suppressed (output rate too high); the suppressed output is still readable via TaskOutput`,
212
+ result: note,
213
+ });
214
+ }
168
215
  handle.seq += 1;
169
216
  handle.updatedAt = Date.now();
217
+ const shown = lines.map(clipMonitorLine);
170
218
  emit({
171
219
  task_id: id,
172
220
  task_type: "monitor",
173
221
  ...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
174
222
  status: "event",
175
223
  seq: handle.seq,
176
- lines,
224
+ lines: shown,
177
225
  summary: `${(handle.description ?? "monitor").slice(0, 200)} — ${lines.length} new output line(s)${firstDropNote(spool)}`,
178
- result: clipMonitorEvent(lines.join("\n")),
226
+ result: clipMonitorEvent(shown.join("\n")),
179
227
  });
180
228
  }
181
229
  catch (e) {
@@ -184,7 +232,7 @@ export function startMonitorWatcherLane(core, id) {
184
232
  if (++pollFailures <= MAX_RETRYABLE_POLL_FAILURES)
185
233
  return;
186
234
  handle.status = "failed";
187
- terminal("failed", `monitor watcher crashed (${e instanceof Error ? e.message : String(e)}); watch ended.`, pendingFinalLines([]));
235
+ terminal("failed", `monitor watcher crashed (${e instanceof Error ? e.message : String(e)}); watch ended.${pendingSuppressionNote()}`, pendingFinalLines([]));
188
236
  }
189
237
  finally {
190
238
  ticking = false;
@@ -244,6 +292,13 @@ export async function pollMonitorLane(handle, filter, deadline, signal) {
244
292
  },
245
293
  };
246
294
  }
295
+ function consumePendingSuppressionNote(handle) {
296
+ const suppressed = handle.suppressedBatches ?? 0;
297
+ if (suppressed <= 0)
298
+ return "";
299
+ handle.suppressedBatches = 0;
300
+ return ` [${suppressed} event batch(es) suppressed before the watch ended — output rate was too high; the suppressed output is still readable via TaskOutput.]`;
301
+ }
247
302
  export async function stopMonitorLane(core, handle) {
248
303
  const wasRunning = handle.status === "running";
249
304
  if (handle.watcher !== undefined) {
@@ -266,9 +321,9 @@ export async function stopMonitorLane(core, handle) {
266
321
  ...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
267
322
  status: "event",
268
323
  seq: handle.seq,
269
- lines: finalLines,
324
+ lines: finalLines.map(clipMonitorLine),
270
325
  summary: `${(handle.description ?? "monitor").slice(0, 200)} — ${finalLines.length} final output line(s) drained at stop${firstDropNote(handle.spool)}`,
271
- result: clipMonitorEvent(finalLines.join("\n")),
326
+ result: clipMonitorEvent(finalLines.map(clipMonitorLine).join("\n")),
272
327
  });
273
328
  }
274
329
  }
@@ -285,7 +340,7 @@ export async function stopMonitorLane(core, handle) {
285
340
  const r = await handle.env.killBackground(handle.shellId);
286
341
  if (handle.status !== "running") {
287
342
  return {
288
- content: wasRunning ? `Terminated ${handle.id}.${drainNote}` : alreadyTerminalStopNote(handle.id, handle.status),
343
+ content: wasRunning ? `Terminated ${handle.id}.${drainNote}${consumePendingSuppressionNote(handle)}` : alreadyTerminalStopNote(handle.id, handle.status),
289
344
  details: {
290
345
  task_id: handle.id,
291
346
  type: "monitor",
@@ -305,7 +360,7 @@ export async function stopMonitorLane(core, handle) {
305
360
  mintCompletionId(handle);
306
361
  core.pokeBgQuiescence(handle.owner);
307
362
  return {
308
- content: `Terminated ${handle.id} (the process was already gone).${drainNote}`,
363
+ content: `Terminated ${handle.id} (the process was already gone).${drainNote}${consumePendingSuppressionNote(handle)}`,
309
364
  details: {
310
365
  task_id: handle.id,
311
366
  type: "monitor",
@@ -334,7 +389,7 @@ export async function stopMonitorLane(core, handle) {
334
389
  core.pokeBgQuiescence(handle.owner);
335
390
  }
336
391
  return {
337
- content: `Terminated ${handle.id}.${drainNote}`,
392
+ content: `Terminated ${handle.id}.${drainNote}${consumePendingSuppressionNote(handle)}`,
338
393
  details: {
339
394
  task_id: handle.id,
340
395
  type: "monitor",
@@ -161,7 +161,13 @@ export interface MonitorTaskHandle extends SemaTaskHandle {
161
161
  };
162
162
  lineBuf: string;
163
163
  seq: number;
164
- batchTimes: number[];
164
+ stormTokens: number;
165
+ stormBurst: number;
166
+ stormRefillIntervalMs: number;
167
+ stormRefilledAt: number;
168
+ suppressedBatches?: number;
169
+ overloadSince?: number;
170
+ lastSuppressedAt?: number;
165
171
  batchWindowMs: number;
166
172
  maxBatchesPerMinute: number;
167
173
  persistent?: true;
@@ -190,6 +196,7 @@ export interface RegisterMonitorInput extends TaskAccess {
190
196
  timers?: MonitorTimers;
191
197
  batchWindowMs?: number;
192
198
  maxBatchesPerMinute?: number;
199
+ stormBurst?: number;
193
200
  now?: number;
194
201
  }
195
202
  export interface RegisterWorkflowInput extends TaskAccess {
@@ -208,7 +215,12 @@ export declare const MONITOR_BATCH_WINDOW_MS = 200;
208
215
  export declare const MONITOR_DEFAULT_TIMEOUT_MS = 300000;
209
216
  export declare const MONITOR_MAX_TIMEOUT_MS = 3600000;
210
217
  export declare const MONITOR_MAX_BATCHES_PER_MINUTE = 50;
218
+ export declare const MONITOR_STORM_BURST = 10;
219
+ export declare const MONITOR_STORM_KILL_AFTER_MS = 30000;
220
+ export declare const MONITOR_LINE_BUF_CAP = 1048576;
211
221
  export declare function clipMonitorEvent(s: string): string;
222
+ export declare function clipMonitorLine(s: string): string;
223
+ export declare function terminalTaskSummary(kind: "bash" | "monitor", label: string, status: "completed" | "failed" | "killed", exitCode?: number): string;
212
224
  export declare const TASK_OUTPUT_MAX_CHARS = 160000;
213
225
  export declare function clipTaskOutput(s: string, fullOutputPath?: string): string;
214
226
  export declare function statusFromBackground(status: string, exitCode?: number): SemaTaskStatus;
@@ -32,10 +32,31 @@ export const MONITOR_BATCH_WINDOW_MS = 200;
32
32
  export const MONITOR_DEFAULT_TIMEOUT_MS = 300_000;
33
33
  export const MONITOR_MAX_TIMEOUT_MS = 3_600_000;
34
34
  export const MONITOR_MAX_BATCHES_PER_MINUTE = 50;
35
+ export const MONITOR_STORM_BURST = 10;
36
+ export const MONITOR_STORM_KILL_AFTER_MS = 30_000;
35
37
  const MONITOR_EVENT_RESULT_CAP = 2_000;
38
+ const MONITOR_EVENT_LINE_CAP = 500;
39
+ export const MONITOR_LINE_BUF_CAP = 1_048_576;
36
40
  export function clipMonitorEvent(s) {
37
41
  return s.length <= MONITOR_EVENT_RESULT_CAP ? s : `${s.slice(0, MONITOR_EVENT_RESULT_CAP)}\n…[+${s.length - MONITOR_EVENT_RESULT_CAP} chars truncated]`;
38
42
  }
43
+ export function clipMonitorLine(s) {
44
+ return s.length <= MONITOR_EVENT_LINE_CAP ? s : `${s.slice(0, MONITOR_EVENT_LINE_CAP)}...(truncated)`;
45
+ }
46
+ export function terminalTaskSummary(kind, label, status, exitCode) {
47
+ if (kind === "monitor") {
48
+ if (status === "completed")
49
+ return `Monitor "${label}" stream ended`;
50
+ if (status === "failed")
51
+ return `Monitor "${label}" script failed${exitCode !== undefined ? ` (exit ${exitCode})` : ""}`;
52
+ return `Monitor "${label}" stopped`;
53
+ }
54
+ if (status === "completed")
55
+ return `Background command "${label}" completed${exitCode !== undefined ? ` (exit code ${exitCode})` : ""}`;
56
+ if (status === "failed")
57
+ return `Background command "${label}" failed${exitCode !== undefined ? ` with exit code ${exitCode}` : ""}`;
58
+ return `Background command "${label}" was stopped`;
59
+ }
39
60
  const TASK_OUTPUT_DEFAULT_CHARS = 32_000;
40
61
  export const TASK_OUTPUT_MAX_CHARS = 160_000;
41
62
  const TASK_OUTPUT_MIN_CHARS = 512;