@sema-agent/core 2.3.0 → 2.5.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 (83) hide show
  1. package/dist/agents/send-message-tool.d.ts +4 -0
  2. package/dist/agents/send-message-tool.js +37 -24
  3. package/dist/agents/subagent.js +279 -128
  4. package/dist/brain/errors.d.ts +1 -0
  5. package/dist/brain/errors.js +14 -0
  6. package/dist/brain/stream-engine.js +3 -3
  7. package/dist/core/auto-compaction.d.ts +2 -0
  8. package/dist/core/auto-compaction.js +2 -1
  9. package/dist/core/context-edit.js +2 -1
  10. package/dist/core/mcp.js +4 -1
  11. package/dist/core/runner/prepare-task.d.ts +5 -0
  12. package/dist/core/runner/prepare-task.js +41 -2
  13. package/dist/core/runner/runtask.js +39 -9
  14. package/dist/core/runner/tool-disclosure.d.ts +1 -0
  15. package/dist/core/runner/tool-disclosure.js +16 -5
  16. package/dist/core/runner/tool-output-projection.js +2 -1
  17. package/dist/core/session-reconcile.d.ts +1 -0
  18. package/dist/core/session-reconcile.js +40 -20
  19. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +37 -0
  20. package/dist/core/store-contracts/checkpoint-store-contract.js +195 -0
  21. package/dist/core/store-contracts/contract-harness.d.ts +6 -0
  22. package/dist/core/store-contracts/contract-harness.js +16 -0
  23. package/dist/core/store-contracts/contract-kit-version.d.ts +1 -0
  24. package/dist/core/store-contracts/contract-kit-version.js +2 -0
  25. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +3 -0
  26. package/dist/core/store-contracts/file-snapshot-store-contract.js +126 -0
  27. package/dist/core/store-contracts/mailbox-store-contract.d.ts +6 -0
  28. package/dist/core/store-contracts/mailbox-store-contract.js +193 -0
  29. package/dist/core/store-contracts/session-repo-contract.d.ts +3 -0
  30. package/dist/core/store-contracts/session-repo-contract.js +36 -0
  31. package/dist/core/store-contracts/tool-result-store-contract.d.ts +3 -0
  32. package/dist/core/store-contracts/tool-result-store-contract.js +35 -0
  33. package/dist/core/task-notification.d.ts +2 -0
  34. package/dist/core/task-registry-agent.d.ts +6 -0
  35. package/dist/core/task-registry-agent.js +24 -1
  36. package/dist/core/task-registry-monitor.js +6 -6
  37. package/dist/core/task-registry-shared.d.ts +9 -2
  38. package/dist/core/task-registry-shared.js +1 -1
  39. package/dist/core/task-registry.d.ts +8 -0
  40. package/dist/core/task-registry.js +59 -4
  41. package/dist/core/tool-result-store.d.ts +3 -2
  42. package/dist/core/tool-result-store.js +12 -4
  43. package/dist/core/trace.d.ts +7 -0
  44. package/dist/core/types.d.ts +10 -3
  45. package/dist/engine/compaction/compaction.d.ts +5 -0
  46. package/dist/engine/compaction/compaction.js +68 -2
  47. package/dist/engine/compaction/utils.d.ts +6 -0
  48. package/dist/engine/compaction/utils.js +53 -3
  49. package/dist/engine/harness/messages.d.ts +1 -1
  50. package/dist/engine/harness/messages.js +11 -3
  51. package/dist/engine/loop/types.d.ts +2 -0
  52. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  53. package/dist/engine/lsp/node-lsp-manager.js +16 -0
  54. package/dist/engine/session/import-validate.js +30 -1
  55. package/dist/engine/session/session.js +7 -5
  56. package/dist/index.d.ts +8 -0
  57. package/dist/index.js +8 -0
  58. package/dist/internal/harness.d.ts +1 -1
  59. package/dist/internal/harness.js +1 -1
  60. package/dist/orchestration/builtin-workflows.d.ts +1 -1
  61. package/dist/orchestration/builtin-workflows.js +11 -2
  62. package/dist/orchestration/workflow-governance.d.ts +6 -1
  63. package/dist/orchestration/workflow-governance.js +24 -4
  64. package/dist/orchestration/workflow-primitives.js +7 -1
  65. package/dist/orchestration/workflow-types.d.ts +1 -0
  66. package/dist/orchestration/workflow.d.ts +1 -0
  67. package/dist/orchestration/workflow.js +41 -3
  68. package/dist/tools/fs/fs-bash.d.ts +7 -1
  69. package/dist/tools/fs/fs-bash.js +59 -22
  70. package/dist/tools/fs/fs-read.js +22 -11
  71. package/dist/tools/fs/fs-search-tools.js +3 -3
  72. package/dist/tools/fs/fs-shared.d.ts +20 -7
  73. package/dist/tools/fs/fs-shared.js +17 -3
  74. package/dist/tools/fs/fs-write.js +4 -4
  75. package/dist/tools/fs/index.d.ts +2 -0
  76. package/dist/tools/fs/index.js +7 -1
  77. package/dist/tools/fs/repo-map.js +2 -2
  78. package/dist/tools/fs/safety.d.ts +10 -0
  79. package/dist/tools/fs/safety.js +15 -1
  80. package/dist/tools/monitor.js +18 -4
  81. package/dist/tools/web.js +6 -2
  82. package/dist/tools/worktree.js +46 -25
  83. package/package.json +1 -1
@@ -182,6 +182,52 @@ export function renderInvokedSkillsRetention(skills) {
182
182
  `If a skill's content was truncated, invoke the Skill tool again for the full text.\n\n` +
183
183
  `${sections.join("\n\n")}\n</invoked-skills>`);
184
184
  }
185
+ const PERSISTED_OUTPUT_REF_RE = /<persisted-output ref="([^"]+)"/g;
186
+ const OFFLOAD_TOOL_NAME_LITERAL = "ReadToolResult";
187
+ export const PERSISTED_OUTPUT_REFS_MAX_ENTRIES = 50;
188
+ export function extractPersistedOutputRefs(messages, into) {
189
+ for (const msg of messages) {
190
+ if (msg.role !== "toolResult" || !Array.isArray(msg.content))
191
+ continue;
192
+ for (const block of msg.content) {
193
+ if (block.type !== "text" || typeof block.text !== "string")
194
+ continue;
195
+ for (const m of block.text.matchAll(PERSISTED_OUTPUT_REF_RE)) {
196
+ into.add(m[1]);
197
+ }
198
+ }
199
+ }
200
+ }
201
+ export function readElidedMessages(details) {
202
+ if (details === null || typeof details !== "object")
203
+ return undefined;
204
+ const raw = details.elidedMessages;
205
+ return typeof raw === "number" && Number.isSafeInteger(raw) && raw >= 0 ? raw : undefined;
206
+ }
207
+ export function readCompactionActiveTools(details) {
208
+ if (details === null || typeof details !== "object")
209
+ return [];
210
+ const raw = details.activeTools;
211
+ if (!Array.isArray(raw))
212
+ return [];
213
+ return raw.filter((n) => typeof n === "string" && n.length > 0);
214
+ }
215
+ export function readPersistedOutputRefs(details) {
216
+ if (details === null || typeof details !== "object")
217
+ return [];
218
+ const raw = details.persistedOutputRefs;
219
+ if (!Array.isArray(raw))
220
+ return [];
221
+ return raw.filter((r) => typeof r === "string" && r.length > 0);
222
+ }
223
+ export function formatPersistedOutputRefs(refs) {
224
+ if (refs.length === 0)
225
+ return "";
226
+ return (`\n\n<persisted-tool-outputs>\n` +
227
+ `Tool outputs elided by compaction were persisted in full and remain retrievable — call ` +
228
+ `${OFFLOAD_TOOL_NAME_LITERAL} with a ref to read one back:\n` +
229
+ `${capList(refs)}\n</persisted-tool-outputs>`);
230
+ }
185
231
  function touchModified(fileOps, path) {
186
232
  fileOps.modifiedOrder.delete(path);
187
233
  fileOps.modifiedOrder.add(path);
@@ -217,9 +263,13 @@ export function formatFileOperations(readFiles, modifiedFiles) {
217
263
  return `\n\n${sections.join("\n\n")}`;
218
264
  }
219
265
  export function stripFileOperationsFooter(summary) {
220
- return summary
221
- .replace(/\n\n<read-files>\n[\s\S]*?\n<\/read-files>(\n\n<modified-files>\n[\s\S]*?\n<\/modified-files>)?\s*$/, "")
222
- .replace(/\n\n<modified-files>\n[\s\S]*?\n<\/modified-files>\s*$/, "");
266
+ let out = summary;
267
+ for (;;) {
268
+ const next = out.replace(/\n\n<(read-files|modified-files|persisted-tool-outputs)>\n[\s\S]*?\n<\/\1>\s*$/, "");
269
+ if (next === out)
270
+ return out;
271
+ out = next;
272
+ }
223
273
  }
224
274
  const TOOL_RESULT_MAX_CHARS = 2000;
225
275
  function safeJsonStringify(value) {
@@ -6,7 +6,7 @@ export declare function asAgentMessage(message: HarnessMessage): AgentMessage;
6
6
  export declare const COMPACTION_SUMMARY_PREFIX = "This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.\n\n<summary>\n";
7
7
  export declare const COMPACTION_SUMMARY_SUFFIX = "\n</summary>\n\nRecent messages are preserved verbatim below. Continue the conversation from where it left off without asking the user any further questions. Resume directly \u2014 do not acknowledge the summary, do not recap what was happening, do not preface with \"I'll continue\" or similar. Pick up the last task as if the break never happened.";
8
8
  export declare function bashExecutionToText(msg: BashExecutionMessage): string;
9
- export declare function createCompactionSummaryMessage(summary: string, tokensBefore: number, timestamp: string): CompactionSummaryMessage;
9
+ export declare function createCompactionSummaryMessage(summary: string, tokensBefore: number, timestamp: string, elidedMessages?: number): CompactionSummaryMessage;
10
10
  export declare function createCustomMessage(customType: string, content: string | (TextContent | ImageContent)[], display: boolean, details: unknown, timestamp: string): CustomMessage;
11
11
  export declare const NORMALIZED_CONTENT_PREFIX = "[invalid content block normalized to text]";
12
12
  export declare function normalizeLlmMessageContent<T extends Message>(message: T): T;
@@ -36,12 +36,13 @@ export function bashExecutionToText(msg) {
36
36
  }
37
37
  return text;
38
38
  }
39
- export function createCompactionSummaryMessage(summary, tokensBefore, timestamp) {
39
+ export function createCompactionSummaryMessage(summary, tokensBefore, timestamp, elidedMessages) {
40
40
  return {
41
41
  role: "compactionSummary",
42
42
  summary,
43
43
  tokensBefore,
44
44
  timestamp: requireSessionTimestampMs(timestamp, "compaction summary timestamp"),
45
+ ...(elidedMessages !== undefined ? { elidedMessages } : {}),
45
46
  };
46
47
  }
47
48
  export function createCustomMessage(customType, content, display, details, timestamp) {
@@ -128,17 +129,24 @@ export function convertToLlm(messages) {
128
129
  ...(message.provenance === "engine-note" ? { provenance: "engine-note" } : {}),
129
130
  });
130
131
  }
131
- case "compactionSummary":
132
+ case "compactionSummary": {
133
+ const n = message.elidedMessages;
134
+ const disclosure = n !== undefined && n > 0
135
+ ? `\n\n(For scale: this summary stands in for ${n} earlier message${n === 1 ? "" : "s"}${message.tokensBefore > 0
136
+ ? ` — approximately ${message.tokensBefore} tokens of context before compaction`
137
+ : ""}.)`
138
+ : "";
132
139
  return {
133
140
  role: "user",
134
141
  content: [
135
142
  {
136
143
  type: "text",
137
- text: COMPACTION_SUMMARY_PREFIX + message.summary + COMPACTION_SUMMARY_SUFFIX,
144
+ text: COMPACTION_SUMMARY_PREFIX + message.summary + COMPACTION_SUMMARY_SUFFIX + disclosure,
138
145
  },
139
146
  ],
140
147
  timestamp: normalizeCompactionSummaryTimestamp(message.timestamp),
141
148
  };
149
+ }
142
150
  case "user":
143
151
  case "assistant":
144
152
  case "toolResult":
@@ -129,6 +129,7 @@ export interface CompactionSummaryMessage {
129
129
  summary: string;
130
130
  tokensBefore: number;
131
131
  timestamp: number | string;
132
+ elidedMessages?: number;
132
133
  tokensAfter?: number;
133
134
  firstKeptEntryId?: string;
134
135
  details?: unknown;
@@ -161,6 +162,7 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = unk
161
162
  execute: (toolCallId: string, params: Static<TParameters>, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback<TDetails>) => Promise<AgentToolResult<TDetails>>;
162
163
  executionMode?: ToolExecutionMode;
163
164
  mcpMaxResultSizeChars?: number;
165
+ mcpAlwaysLoad?: boolean;
164
166
  isConcurrencySafe?: (args: unknown) => boolean;
165
167
  }
166
168
  export interface AgentContext {
@@ -28,6 +28,7 @@ export declare class NodeLspManager implements LspServerManager {
28
28
  private sweepTimer?;
29
29
  private readonly cache;
30
30
  private readonly opening;
31
+ private readonly failed;
31
32
  private disposed;
32
33
  constructor(opts?: NodeLspManagerOptions);
33
34
  private sweepIdle;
@@ -35,6 +36,7 @@ export declare class NodeLspManager implements LspServerManager {
35
36
  private open;
36
37
  private evictOverCap;
37
38
  evict(root: string): Promise<void>;
39
+ clearFailed(root?: string): void;
38
40
  private openSession;
39
41
  private serverCmd;
40
42
  dispose(): Promise<void>;
@@ -59,6 +59,7 @@ export class NodeLspManager {
59
59
  sweepTimer;
60
60
  cache = new Map();
61
61
  opening = new Map();
62
+ failed = new Map();
62
63
  disposed = false;
63
64
  constructor(opts = {}) {
64
65
  this.servers = { ...DEFAULT_LSP_SERVERS, ...opts.servers };
@@ -96,6 +97,11 @@ export class NodeLspManager {
96
97
  cached.lastUsed = Date.now();
97
98
  return cached.session;
98
99
  }
100
+ const neg = this.failed.get(key);
101
+ if (neg !== undefined) {
102
+ neg.attempts += 1;
103
+ return undefined;
104
+ }
99
105
  if (cached) {
100
106
  this.cache.delete(key);
101
107
  const carry = cached.session.openedFiles();
@@ -163,6 +169,14 @@ export class NodeLspManager {
163
169
  }
164
170
  await Promise.all(closing);
165
171
  }
172
+ clearFailed(root) {
173
+ for (const [key, e] of [...this.failed]) {
174
+ if (root !== undefined && e.root !== root)
175
+ continue;
176
+ this.failed.delete(key);
177
+ this.log("lsp_negative_cache_cleared", { key, attempts: e.attempts });
178
+ }
179
+ }
166
180
  async openSession(language, root, signal, _env) {
167
181
  const cmd = this.serverCmd(language);
168
182
  if (!cmd)
@@ -171,6 +185,8 @@ export class NodeLspManager {
171
185
  const [command, ...args] = tokens.length > 0 ? tokens : [""];
172
186
  const child = await this.spawnFn(command, args, root, signal).catch(() => undefined);
173
187
  if (!child) {
188
+ if (signal?.aborted !== true)
189
+ this.failed.set(`${language} ${root}`, { root, attempts: 1 });
174
190
  this.log("lsp_spawn_degraded", { language, root, command });
175
191
  return undefined;
176
192
  }
@@ -3,11 +3,14 @@ import { leafIdAfterEntry } from "./storage-base.js";
3
3
  import { parseSessionTimestampMs } from "./timestamps.js";
4
4
  import { flattenableUserText, normalizeEngineSegments } from "../../core/untrusted-text.js";
5
5
  import { normalizePromptEpoch } from "../../prompt-assembly/epoch.js";
6
- import { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, } from "../compaction/utils.js";
6
+ import { PERSISTED_OUTPUT_REFS_MAX_ENTRIES, SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, } from "../compaction/utils.js";
7
7
  const PATH_LIST_MAX_CHARS = 4096;
8
8
  const PATH_LIST_MAX_ENTRIES = 1000;
9
9
  const INVOKED_SKILLS_MAX_ENTRIES = 1000;
10
10
  const INVOKED_SKILL_NAME_MAX_CHARS = 1024;
11
+ const PERSISTED_REF_MAX_CHARS = 256;
12
+ const ACTIVE_TOOL_NAME_MAX_CHARS = 1024;
13
+ const ACTIVE_TOOLS_MAX_ENTRIES = 1000;
11
14
  export class StreamingImportValidator {
12
15
  seen = new Set();
13
16
  parentOf = new Map();
@@ -157,6 +160,32 @@ export class StreamingImportValidator {
157
160
  throw new SessionError("invalid_session", `compaction "${e.id}" carries an invokedSkills area of ${totalChars} chars (max ${SKILL_RETENTION_TOTAL_MAX_CHARS})`);
158
161
  }
159
162
  }
163
+ const elided = e.details?.elidedMessages;
164
+ if (elided !== undefined && !(typeof elided === "number" && Number.isSafeInteger(elided) && elided >= 0)) {
165
+ throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid elidedMessages count (non-negative integer)`);
166
+ }
167
+ const refs = e.details?.persistedOutputRefs;
168
+ if (refs !== undefined) {
169
+ if (!Array.isArray(refs) || refs.length > PERSISTED_OUTPUT_REFS_MAX_ENTRIES) {
170
+ throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid persistedOutputRefs list (array of at most ${PERSISTED_OUTPUT_REFS_MAX_ENTRIES} refs)`);
171
+ }
172
+ for (const r of refs) {
173
+ if (typeof r !== "string" || r.length === 0 || r.length > PERSISTED_REF_MAX_CHARS) {
174
+ throw new SessionError("invalid_session", `compaction "${e.id}" carries a persistedOutputRefs entry that is not a non-empty string of at most ${PERSISTED_REF_MAX_CHARS} chars`);
175
+ }
176
+ }
177
+ }
178
+ const activeToolsCarrier = e.details?.activeTools;
179
+ if (activeToolsCarrier !== undefined) {
180
+ if (!Array.isArray(activeToolsCarrier) || activeToolsCarrier.length > ACTIVE_TOOLS_MAX_ENTRIES) {
181
+ throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid activeTools list (array of at most ${ACTIVE_TOOLS_MAX_ENTRIES} names)`);
182
+ }
183
+ for (const n of activeToolsCarrier) {
184
+ if (typeof n !== "string" || n.length === 0 || n.length > ACTIVE_TOOL_NAME_MAX_CHARS) {
185
+ throw new SessionError("invalid_session", `compaction "${e.id}" carries an activeTools entry that is not a non-empty string of at most ${ACTIVE_TOOL_NAME_MAX_CHARS} chars`);
186
+ }
187
+ }
188
+ }
160
189
  }
161
190
  this.parentOf.set(e.id, e.parentId);
162
191
  this.seen.add(e.id);
@@ -1,7 +1,7 @@
1
1
  import { asAgentMessage, createCompactionSummaryMessage, createCustomMessage, } from "../harness/messages.js";
2
- import { SessionError, normalizeAnnouncedListing, normalizeCompactionStateCarrier, normalizeWorkspaceState } from "../harness/types.js";
2
+ import { SessionError, isValidModelChange, normalizeAnnouncedListing, normalizeCompactionStateCarrier, normalizeWorkspaceState } from "../harness/types.js";
3
3
  import { normalizePromptEpoch } from "../../prompt-assembly/epoch.js";
4
- import { budgetInvokedSkillsRetention, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../compaction/utils.js";
4
+ import { budgetInvokedSkillsRetention, readElidedMessages, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../compaction/utils.js";
5
5
  const RETENTION_CLAMP_DEFAULT_CHARS_PER_TOKEN = 4;
6
6
  const RETENTION_CLAMP_WINDOW_FRACTION = 0.5;
7
7
  export function buildSessionContext(pathEntries, opts) {
@@ -25,7 +25,9 @@ export function buildSessionContext(pathEntries, opts) {
25
25
  model = { provider: entry.provider, modelId: entry.modelId };
26
26
  }
27
27
  else if (entry.type === "message" && entry.message.role === "assistant") {
28
- model = { provider: entry.message.provider, modelId: entry.message.model };
28
+ if (isValidModelChange({ provider: entry.message.provider, modelId: entry.message.model })) {
29
+ model = { provider: entry.message.provider, modelId: entry.message.model };
30
+ }
29
31
  }
30
32
  else if (entry.type === "compaction") {
31
33
  compaction = entry;
@@ -64,7 +66,7 @@ export function buildSessionContext(pathEntries, opts) {
64
66
  retainedSkills = budgetInvokedSkillsRetention(retainedSkills, budgetChars);
65
67
  }
66
68
  const retainedSkillsBlock = renderInvokedSkillsRetention(retainedSkills);
67
- messages.push(asAgentMessage(createCompactionSummaryMessage(compaction.summary + retainedSkillsBlock, compaction.tokensBefore, compaction.timestamp)));
69
+ messages.push(asAgentMessage(createCompactionSummaryMessage(compaction.summary + retainedSkillsBlock, compaction.tokensBefore, compaction.timestamp, readElidedMessages(compaction.details))));
68
70
  const compactionIdx = pathEntries.findIndex((e) => e.type === "compaction" && e.id === compaction.id);
69
71
  let foundFirstKept = false;
70
72
  for (let i = 0; i < compactionIdx; i++) {
@@ -242,7 +244,7 @@ export class StoredSession {
242
244
  const carried = await (async () => {
243
245
  try {
244
246
  const ctx = buildSessionContext(await this.getBranch());
245
- return { thinkingLevel: ctx.thinkingLevel, model: ctx.model };
247
+ return { thinkingLevel: ctx.thinkingLevel, ...(ctx.model !== null ? { model: ctx.model } : {}) };
246
248
  }
247
249
  catch {
248
250
  return undefined;
package/dist/index.d.ts CHANGED
@@ -66,6 +66,7 @@ export { sanitizeUntrustedText, delimitUntrusted, inlineUntrusted } from "./core
66
66
  export { deriveInvariants, checkInvariants } from "./core/property-harness.js";
67
67
  export type { InvariantKind, FunctionContract, Invariant, InvariantViolation, CheckResult, } from "./core/property-harness.js";
68
68
  export { HAND_TOOL_EFFECTS, bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName, classifyCompoundReadonly, MAX_EDIT_BYTES } from "./tools/fs/index.js";
69
+ export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
69
70
  export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, type ToolResultStore, type ToolResultSlice, } from "./core/tool-result-store.js";
70
71
  export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, WALLTIME_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingWalltimeMs, winnerFromOutcome, validatePendingSteer, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, type RiskDescriptor, type CheckpointStore, type CheckpointSummary, type Checkpoint, type CheckpointToken, type CheckpointGate, type CheckpointState, type SerializedCheckpointState, type CheckpointFaultMode, type PendingAction, type ResumeOutcome, type ResolvedOutcome, type ReopenReason, type ResolveExpectation, type SafetyAxis, type ResourceLedger, type ResourceLimitReason, } from "./core/checkpoint-store.js";
71
72
  export { InMemoryFileSnapshotStore, DEFAULT_SNAPSHOT_BOUNDS } from "./core/file-snapshot-store.js";
@@ -150,6 +151,13 @@ export { PRESENT_PLAN_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME } from "./core/presen
150
151
  export { type WorkflowRunStore, type WorkflowRunSummary, summarizeWorkflowRun, isTerminalWorkflowStatus, WorkflowRunStoreError, InMemoryWorkflowRunStore, } from "./core/workflow-run-store.js";
151
152
  export { FileWorkflowRunStore, type FileWorkflowRunStoreOptions } from "./stores/file/workflow-run-store.js";
152
153
  export { workflowRunStoreContract } from "./core/workflow-run-store-contract.js";
154
+ export { CONTRACT_KIT_ENGINE_VERSION } from "./core/store-contracts/contract-kit-version.js";
155
+ export { type ContractAssertionRunner } from "./core/store-contracts/contract-harness.js";
156
+ export { checkpointStoreContract } from "./core/store-contracts/checkpoint-store-contract.js";
157
+ export { sessionRepoContract } from "./core/store-contracts/session-repo-contract.js";
158
+ export { toolResultStoreContract } from "./core/store-contracts/tool-result-store-contract.js";
159
+ export { fileSnapshotStoreContract } from "./core/store-contracts/file-snapshot-store-contract.js";
160
+ export { MAILBOX_CONTRACT_SCOPE, mailboxStoreContract, mailboxAckOwnershipContract, mailboxBundledOnlyContract, } from "./core/store-contracts/mailbox-store-contract.js";
153
161
  export { type BackgroundAgentStore, type BackgroundAgentRecord, type BackgroundAgentRowSummary, type BackgroundAgentUsage, canAccessAgentRecord, BackgroundAgentStoreError, InMemoryBackgroundAgentStore, reconcileParkedAgents, } from "./core/background-agent-store.js";
154
162
  export { FileBackgroundAgentStore, type FileBackgroundAgentStoreOptions } from "./stores/file/background-agent-store.js";
155
163
  export { type WorkflowJournalStore, type WorkflowJournalEntry, InMemoryWorkflowJournalStore, callKeyOrdinal, JOURNAL_OVERSIZE_ERROR_CODE, } from "./core/workflow-journal-store.js";
package/dist/index.js CHANGED
@@ -57,6 +57,7 @@ export { runExecGate } from "./core/exec-gate.js";
57
57
  export { sanitizeUntrustedText, delimitUntrusted, inlineUntrusted } from "./core/untrusted-text.js";
58
58
  export { deriveInvariants, checkInvariants } from "./core/property-harness.js";
59
59
  export { HAND_TOOL_EFFECTS, bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName, classifyCompoundReadonly, MAX_EDIT_BYTES } from "./tools/fs/index.js";
60
+ export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
60
61
  export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, } from "./core/tool-result-store.js";
61
62
  export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, WALLTIME_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingWalltimeMs, winnerFromOutcome, validatePendingSteer, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, } from "./core/checkpoint-store.js";
62
63
  export { InMemoryFileSnapshotStore, DEFAULT_SNAPSHOT_BOUNDS } from "./core/file-snapshot-store.js";
@@ -137,6 +138,13 @@ export { PRESENT_PLAN_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME } from "./core/presen
137
138
  export { summarizeWorkflowRun, isTerminalWorkflowStatus, WorkflowRunStoreError, InMemoryWorkflowRunStore, } from "./core/workflow-run-store.js";
138
139
  export { FileWorkflowRunStore } from "./stores/file/workflow-run-store.js";
139
140
  export { workflowRunStoreContract } from "./core/workflow-run-store-contract.js";
141
+ export { CONTRACT_KIT_ENGINE_VERSION } from "./core/store-contracts/contract-kit-version.js";
142
+ export {} from "./core/store-contracts/contract-harness.js";
143
+ export { checkpointStoreContract } from "./core/store-contracts/checkpoint-store-contract.js";
144
+ export { sessionRepoContract } from "./core/store-contracts/session-repo-contract.js";
145
+ export { toolResultStoreContract } from "./core/store-contracts/tool-result-store-contract.js";
146
+ export { fileSnapshotStoreContract } from "./core/store-contracts/file-snapshot-store-contract.js";
147
+ export { MAILBOX_CONTRACT_SCOPE, mailboxStoreContract, mailboxAckOwnershipContract, mailboxBundledOnlyContract, } from "./core/store-contracts/mailbox-store-contract.js";
140
148
  export { canAccessAgentRecord, BackgroundAgentStoreError, InMemoryBackgroundAgentStore, reconcileParkedAgents, } from "./core/background-agent-store.js";
141
149
  export { FileBackgroundAgentStore } from "./stores/file/background-agent-store.js";
142
150
  export { InMemoryWorkflowJournalStore, callKeyOrdinal, JOURNAL_OVERSIZE_ERROR_CODE, } from "./core/workflow-journal-store.js";
@@ -2,7 +2,7 @@ export * from "./harness-types.js";
2
2
  export { AgentHarness } from "../engine/harness/agent-harness.js";
3
3
  export { CompactionError, ExecutionError, FileError, ok, err } from "../engine/harness/types.js";
4
4
  export { DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, compact, computeFileLists, dryRunSummarizationClamp, estimateContextTokens, estimateTokens, prepareCompaction, shouldCompact, summaryOutputBudgetTokens, } from "../engine/compaction/compaction.js";
5
- export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
5
+ export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, readCompactionActiveTools, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
6
6
  export { NodeExecutionEnv } from "../engine/execution-env/node-execution-env.js";
7
7
  export { StoredSession, buildSessionContext } from "../engine/session/session.js";
8
8
  export { getEntriesToFork } from "../engine/session/repo-utils.js";
@@ -2,7 +2,7 @@ export * from "./harness-types.js";
2
2
  export { AgentHarness } from "../engine/harness/agent-harness.js";
3
3
  export { CompactionError, ExecutionError, FileError, ok, err } from "../engine/harness/types.js";
4
4
  export { DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, compact, computeFileLists, dryRunSummarizationClamp, estimateContextTokens, estimateTokens, prepareCompaction, shouldCompact, summaryOutputBudgetTokens, } from "../engine/compaction/compaction.js";
5
- export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
5
+ export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, readCompactionActiveTools, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
6
6
  export { NodeExecutionEnv } from "../engine/execution-env/node-execution-env.js";
7
7
  export { StoredSession, buildSessionContext } from "../engine/session/session.js";
8
8
  export { getEntriesToFork } from "../engine/session/repo-utils.js";
@@ -1,6 +1,6 @@
1
1
  import type { NamedWorkflowListing } from "./workflow-script-store.js";
2
2
  export declare const TEAM_DISCUSSION_WORKFLOW_NAME = "team-discussion";
3
- export declare const TEAM_DISCUSSION_SCRIPT = "export const meta = {\n name: \"team-discussion\",\n description: \"Round-based team discussion: configurable members debate a topic across rounds (each member sees the transcript so far), then a finalizer synthesizes a structured verdict.\",\n whenToUse: \"Use for a genuinely contested question that benefits from several perspectives arguing across rounds - design trade-offs, plan or risk reviews, adversarial critique of a proposal. Do NOT use it for a single factual question, a task with one obvious answer, or a budget-sensitive run: every round costs one agent call per member, so a discussion is never cheaper than asking once. args (all optional): { topic, members?: [{ role, prompt?, model? }], rounds?, finalizer?: { prompt?, model? } }.\",\n phases: [\n { title: \"Discussion\" },\n { title: \"Synthesis\" },\n ],\n};\n// Zero-config runnable (design/140 \u00A76 1c): every arg has an opinionated fallback.\nconst raw = args;\nconst a = raw !== null && typeof raw === \"object\" && !Array.isArray(raw) ? raw : {};\nconst topic =\n typeof a.topic === \"string\" && a.topic.trim() !== \"\"\n ? a.topic\n : typeof raw === \"string\" && raw.trim() !== \"\"\n ? raw // ergonomic form: a bare string args IS the topic\n : \"No topic was provided. Discuss: what information should a caller supply to make a team discussion like this productive, and when should they NOT convene one?\";\nconst defaultMembers = [\n { role: \"advocate\", prompt: \"Make the strongest constructive case. Propose concrete options and argue their benefits with specifics.\" },\n { role: \"skeptic\", prompt: \"Stress-test every claim made so far. Surface risks, hidden costs, failure modes, and cheaper alternatives.\" },\n];\nconst rawMembers = Array.isArray(a.members) && a.members.length > 0 ? a.members : defaultMembers;\nconst members = rawMembers.slice(0, 6).map((m, i) => {\n const mm = m !== null && typeof m === \"object\" ? m : {};\n const member = {\n role: typeof mm.role === \"string\" && mm.role.trim() !== \"\" ? mm.role : \"member-\" + (i + 1),\n prompt: typeof mm.prompt === \"string\" && mm.prompt.trim() !== \"\" ? mm.prompt : \"Contribute your own distinct perspective: be concrete, give reasons, and engage with what others said.\",\n };\n if (typeof mm.model === \"string\" && mm.model.trim() !== \"\") member.model = mm.model;\n // Slot-tools carrier (design/140 \u2461-3 + F4): a member may BE a registered agent type ({agent:\"reviewer\"}) \u2014\n // persona/tools/model then come from the deployment's AgentDefinition (role library), args stay thin.\n if (typeof mm.agent === \"string\" && mm.agent.trim() !== \"\") member.agent = mm.agent;\n return member;\n});\n// Deterministic budget truncation (design/140 \u00A71 \u9884\u7B97 row): a HARD rounds ceiling + member cap \u2014 never an\n// evaluator agent. The engine's budget/maxAgents hard stops remain the backstop.\nconst requestedRounds = Math.floor(Number(a.rounds));\nconst rounds = Math.min(Number.isFinite(requestedRounds) && requestedRounds >= 1 ? requestedRounds : 2, 5);\nconst fin = a.finalizer !== null && typeof a.finalizer === \"object\" && !Array.isArray(a.finalizer) ? a.finalizer : {};\nconst finalizerPrompt = typeof fin.prompt === \"string\" && fin.prompt.trim() !== \"\"\n ? fin.prompt\n : \"You are the synthesis lead. Read the full discussion transcript and produce the final verdict: the decision/answer, the key supporting points, and the strongest unresolved dissent (if any). Do not introduce new arguments of your own.\";\nconst clip = (s) => { const t = String(s); return t.length > 4000 ? t.slice(0, 4000) + \" ...[truncated]\" : t; };\nconst isBudgetStop = (e) => e !== null && typeof e === \"object\" && e.code === \"workflow.budget_exceeded\";\n\nphase(\"Discussion\");\nconst transcript = [];\nlet truncated = null;\nfor (let r = 1; r <= rounds && truncated === null; r++) {\n // Deterministic early stop on an exhausted budget (a live read of the engine budget; the engine's\n // hard WorkflowBudgetExceededError remains the backstop if a member call itself crosses the line).\n if (budget.total !== null && budget.remaining() <= 0) { truncated = \"budget exhausted before round \" + r; break; }\n for (const m of members) {\n const history = transcript.length === 0 ? \"(none yet - you open the discussion)\" : transcript.join(\"\\n\\n\");\n const spec = {\n objective:\n \"Team discussion on: \" + topic + \"\\n\\n\" +\n 'You are \"' + m.role + '\" in round ' + r + \" of \" + rounds + \".\\n\" +\n \"Your brief: \" + m.prompt + \"\\n\\n\" +\n \"Transcript so far:\\n\" + history + \"\\n\\n\" +\n \"Respond to the strongest points others made (do not repeat yourself), then advance your own position. Be concise: a few tight paragraphs at most.\",\n };\n if (m.model !== undefined) spec.modelName = m.model;\n let res;\n try {\n res = await agent(spec, m.agent !== undefined ? { label: m.role + \"-r\" + r, phase: \"Discussion\", agentType: m.agent } : { label: m.role + \"-r\" + r, phase: \"Discussion\" });\n } catch (e) {\n // The engine's budget hard stop: keep what the discussion already produced instead of failing the run.\n if (isBudgetStop(e)) { truncated = \"budget exhausted at \" + m.role + \", round \" + r; break; }\n throw e;\n }\n const text = res && res.status === \"completed\" ? clip(res.result) : \"(no contribution - agent ended \" + (res ? res.status : \"unknown\") + \")\";\n transcript.push(m.role + \" (round \" + r + \"): \" + text);\n }\n}\n\nphase(\"Synthesis\");\nconst finalSpec = {\n objective:\n finalizerPrompt + \"\\n\\nTopic: \" + topic + \"\\n\\nFull transcript:\\n\" +\n (transcript.length === 0 ? \"(the discussion produced no contributions)\" : transcript.join(\"\\n\\n\")) +\n (truncated ? \"\\n\\nNote: the discussion was cut short (\" + truncated + \").\" : \"\"),\n};\nif (typeof fin.model === \"string\" && fin.model.trim() !== \"\") finalSpec.modelName = fin.model;\nlet verdict = null;\ntry {\n verdict = await agent(finalSpec, {\n label: \"finalizer\",\n phase: \"Synthesis\",\n schema: {\n type: \"object\",\n properties: {\n decision: { type: \"string\", description: \"The final answer/decision, one paragraph.\" },\n keyPoints: { type: \"array\", items: { type: \"string\" }, description: \"The strongest supporting points from the discussion.\" },\n dissent: { type: \"string\", description: \"The strongest unresolved counter-position, if any.\" },\n },\n required: [\"decision\", \"keyPoints\"],\n },\n });\n} catch (e) {\n // Budget died before synthesis: return the transcript honestly rather than failing the whole run.\n if (!isBudgetStop(e)) throw e;\n truncated = truncated === null ? \"budget exhausted before synthesis\" : truncated;\n}\n\nreturn {\n topic,\n rounds,\n members: members.map((m) => m.role),\n ...(truncated ? { truncated } : {}),\n transcript,\n verdict: verdict && verdict.structuredOutput !== undefined ? verdict.structuredOutput : (verdict ? verdict.result : null),\n};\n";
3
+ export declare const TEAM_DISCUSSION_SCRIPT = "export const meta = {\n name: \"team-discussion\",\n description: \"Round-based team discussion: configurable members debate a topic across rounds (each member sees the transcript so far), then a finalizer synthesizes a structured verdict.\",\n whenToUse: \"Use for a genuinely contested question that benefits from several perspectives arguing across rounds - design trade-offs, plan or risk reviews, adversarial critique of a proposal. Do NOT use it for a single factual question, a task with one obvious answer, or a budget-sensitive run: every round costs one agent call per member, so a discussion is never cheaper than asking once. args (all optional): { topic, members?: [{ role, prompt?, model? }], rounds?, finalizer?: { prompt?, model? } }. Hard ceilings: members is capped at 6 and rounds is capped at 5 regardless of what you pass; the run reports it via log() and a capped field on the result when a request exceeds either.\",\n phases: [\n { title: \"Discussion\" },\n { title: \"Synthesis\" },\n ],\n};\n// Zero-config runnable (design/140 \u00A76 1c): every arg has an opinionated fallback.\nconst raw = args;\nconst a = raw !== null && typeof raw === \"object\" && !Array.isArray(raw) ? raw : {};\nconst topic =\n typeof a.topic === \"string\" && a.topic.trim() !== \"\"\n ? a.topic\n : typeof raw === \"string\" && raw.trim() !== \"\"\n ? raw // ergonomic form: a bare string args IS the topic\n : \"No topic was provided. Discuss: what information should a caller supply to make a team discussion like this productive, and when should they NOT convene one?\";\nconst defaultMembers = [\n { role: \"advocate\", prompt: \"Make the strongest constructive case. Propose concrete options and argue their benefits with specifics.\" },\n { role: \"skeptic\", prompt: \"Stress-test every claim made so far. Surface risks, hidden costs, failure modes, and cheaper alternatives.\" },\n];\nconst rawMembers = Array.isArray(a.members) && a.members.length > 0 ? a.members : defaultMembers;\nconst members = rawMembers.slice(0, 6).map((m, i) => {\n const mm = m !== null && typeof m === \"object\" ? m : {};\n const member = {\n role: typeof mm.role === \"string\" && mm.role.trim() !== \"\" ? mm.role : \"member-\" + (i + 1),\n prompt: typeof mm.prompt === \"string\" && mm.prompt.trim() !== \"\" ? mm.prompt : \"Contribute your own distinct perspective: be concrete, give reasons, and engage with what others said.\",\n };\n if (typeof mm.model === \"string\" && mm.model.trim() !== \"\") member.model = mm.model;\n // Slot-tools carrier (design/140 \u2461-3 + F4): a member may BE a registered agent type ({agent:\"reviewer\"}) \u2014\n // persona/tools/model then come from the deployment's AgentDefinition (role library), args stay thin.\n if (typeof mm.agent === \"string\" && mm.agent.trim() !== \"\") member.agent = mm.agent;\n return member;\n});\n// Deterministic budget truncation (design/140 \u00A71 \u9884\u7B97 row): a HARD rounds ceiling + member cap \u2014 never an\n// evaluator agent. The engine's budget/maxAgents hard stops remain the backstop.\nconst requestedRounds = Math.floor(Number(a.rounds));\nconst normalizedRounds = Number.isFinite(requestedRounds) && requestedRounds >= 1 ? requestedRounds : 2;\nconst rounds = Math.min(normalizedRounds, 5);\n// RB-380 disclosure: the member/round slices above are silent by construction (Array.prototype.slice /\n// Math.min just drop the excess) \u2014 record + surface it instead of a caller finding out only by counting\n// transcript entries. Fires only when a request actually exceeded a ceiling (never on the common path).\nconst capNotes = [];\nif (rawMembers.length > 6) capNotes.push(\"requested \" + rawMembers.length + \" members, capped at 6\");\nif (normalizedRounds > 5) capNotes.push(\"requested \" + normalizedRounds + \" rounds, capped at 5\");\nfor (const note of capNotes) log(\"team-discussion: \" + note);\nconst fin = a.finalizer !== null && typeof a.finalizer === \"object\" && !Array.isArray(a.finalizer) ? a.finalizer : {};\nconst finalizerPrompt = typeof fin.prompt === \"string\" && fin.prompt.trim() !== \"\"\n ? fin.prompt\n : \"You are the synthesis lead. Read the full discussion transcript and produce the final verdict: the decision/answer, the key supporting points, and the strongest unresolved dissent (if any). Do not introduce new arguments of your own.\";\nconst clip = (s) => { const t = String(s); return t.length > 4000 ? t.slice(0, 4000) + \" ...[truncated]\" : t; };\nconst isBudgetStop = (e) => e !== null && typeof e === \"object\" && e.code === \"workflow.budget_exceeded\";\n\nphase(\"Discussion\");\nconst transcript = [];\nlet truncated = null;\nfor (let r = 1; r <= rounds && truncated === null; r++) {\n // Deterministic early stop on an exhausted budget (a live read of the engine budget; the engine's\n // hard WorkflowBudgetExceededError remains the backstop if a member call itself crosses the line).\n if (budget.total !== null && budget.remaining() <= 0) { truncated = \"budget exhausted before round \" + r; break; }\n for (const m of members) {\n const history = transcript.length === 0 ? \"(none yet - you open the discussion)\" : transcript.join(\"\\n\\n\");\n const spec = {\n objective:\n \"Team discussion on: \" + topic + \"\\n\\n\" +\n 'You are \"' + m.role + '\" in round ' + r + \" of \" + rounds + \".\\n\" +\n \"Your brief: \" + m.prompt + \"\\n\\n\" +\n \"Transcript so far:\\n\" + history + \"\\n\\n\" +\n \"Respond to the strongest points others made (do not repeat yourself), then advance your own position. Be concise: a few tight paragraphs at most.\",\n };\n if (m.model !== undefined) spec.modelName = m.model;\n let res;\n try {\n res = await agent(spec, m.agent !== undefined ? { label: m.role + \"-r\" + r, phase: \"Discussion\", agentType: m.agent } : { label: m.role + \"-r\" + r, phase: \"Discussion\" });\n } catch (e) {\n // The engine's budget hard stop: keep what the discussion already produced instead of failing the run.\n if (isBudgetStop(e)) { truncated = \"budget exhausted at \" + m.role + \", round \" + r; break; }\n throw e;\n }\n const text = res && res.status === \"completed\" ? clip(res.result) : \"(no contribution - agent ended \" + (res ? res.status : \"unknown\") + \")\";\n transcript.push(m.role + \" (round \" + r + \"): \" + text);\n }\n}\n\nphase(\"Synthesis\");\nconst finalSpec = {\n objective:\n finalizerPrompt + \"\\n\\nTopic: \" + topic + \"\\n\\nFull transcript:\\n\" +\n (transcript.length === 0 ? \"(the discussion produced no contributions)\" : transcript.join(\"\\n\\n\")) +\n (truncated ? \"\\n\\nNote: the discussion was cut short (\" + truncated + \").\" : \"\"),\n};\nif (typeof fin.model === \"string\" && fin.model.trim() !== \"\") finalSpec.modelName = fin.model;\nlet verdict = null;\ntry {\n verdict = await agent(finalSpec, {\n label: \"finalizer\",\n phase: \"Synthesis\",\n schema: {\n type: \"object\",\n properties: {\n decision: { type: \"string\", description: \"The final answer/decision, one paragraph.\" },\n keyPoints: { type: \"array\", items: { type: \"string\" }, description: \"The strongest supporting points from the discussion.\" },\n dissent: { type: \"string\", description: \"The strongest unresolved counter-position, if any.\" },\n },\n required: [\"decision\", \"keyPoints\"],\n },\n });\n} catch (e) {\n // Budget died before synthesis: return the transcript honestly rather than failing the whole run.\n if (!isBudgetStop(e)) throw e;\n truncated = truncated === null ? \"budget exhausted before synthesis\" : truncated;\n}\n\nreturn {\n topic,\n rounds,\n members: members.map((m) => m.role),\n ...(capNotes.length > 0 ? { capped: capNotes } : {}),\n ...(truncated ? { truncated } : {}),\n transcript,\n verdict: verdict && verdict.structuredOutput !== undefined ? verdict.structuredOutput : (verdict ? verdict.result : null),\n};\n";
4
4
  export interface BuiltinWorkflowDefinition {
5
5
  name: string;
6
6
  script: string;
@@ -3,7 +3,7 @@ export const TEAM_DISCUSSION_WORKFLOW_NAME = "team-discussion";
3
3
  export const TEAM_DISCUSSION_SCRIPT = `export const meta = {
4
4
  name: "team-discussion",
5
5
  description: "Round-based team discussion: configurable members debate a topic across rounds (each member sees the transcript so far), then a finalizer synthesizes a structured verdict.",
6
- whenToUse: "Use for a genuinely contested question that benefits from several perspectives arguing across rounds - design trade-offs, plan or risk reviews, adversarial critique of a proposal. Do NOT use it for a single factual question, a task with one obvious answer, or a budget-sensitive run: every round costs one agent call per member, so a discussion is never cheaper than asking once. args (all optional): { topic, members?: [{ role, prompt?, model? }], rounds?, finalizer?: { prompt?, model? } }.",
6
+ whenToUse: "Use for a genuinely contested question that benefits from several perspectives arguing across rounds - design trade-offs, plan or risk reviews, adversarial critique of a proposal. Do NOT use it for a single factual question, a task with one obvious answer, or a budget-sensitive run: every round costs one agent call per member, so a discussion is never cheaper than asking once. args (all optional): { topic, members?: [{ role, prompt?, model? }], rounds?, finalizer?: { prompt?, model? } }. Hard ceilings: members is capped at 6 and rounds is capped at 5 regardless of what you pass; the run reports it via log() and a capped field on the result when a request exceeds either.",
7
7
  phases: [
8
8
  { title: "Discussion" },
9
9
  { title: "Synthesis" },
@@ -38,7 +38,15 @@ const members = rawMembers.slice(0, 6).map((m, i) => {
38
38
  // Deterministic budget truncation (design/140 §1 预算 row): a HARD rounds ceiling + member cap — never an
39
39
  // evaluator agent. The engine's budget/maxAgents hard stops remain the backstop.
40
40
  const requestedRounds = Math.floor(Number(a.rounds));
41
- const rounds = Math.min(Number.isFinite(requestedRounds) && requestedRounds >= 1 ? requestedRounds : 2, 5);
41
+ const normalizedRounds = Number.isFinite(requestedRounds) && requestedRounds >= 1 ? requestedRounds : 2;
42
+ const rounds = Math.min(normalizedRounds, 5);
43
+ // RB-380 disclosure: the member/round slices above are silent by construction (Array.prototype.slice /
44
+ // Math.min just drop the excess) — record + surface it instead of a caller finding out only by counting
45
+ // transcript entries. Fires only when a request actually exceeded a ceiling (never on the common path).
46
+ const capNotes = [];
47
+ if (rawMembers.length > 6) capNotes.push("requested " + rawMembers.length + " members, capped at 6");
48
+ if (normalizedRounds > 5) capNotes.push("requested " + normalizedRounds + " rounds, capped at 5");
49
+ for (const note of capNotes) log("team-discussion: " + note);
42
50
  const fin = a.finalizer !== null && typeof a.finalizer === "object" && !Array.isArray(a.finalizer) ? a.finalizer : {};
43
51
  const finalizerPrompt = typeof fin.prompt === "string" && fin.prompt.trim() !== ""
44
52
  ? fin.prompt
@@ -110,6 +118,7 @@ return {
110
118
  topic,
111
119
  rounds,
112
120
  members: members.map((m) => m.role),
121
+ ...(capNotes.length > 0 ? { capped: capNotes } : {}),
113
122
  ...(truncated ? { truncated } : {}),
114
123
  transcript,
115
124
  verdict: verdict && verdict.structuredOutput !== undefined ? verdict.structuredOutput : (verdict ? verdict.result : null),
@@ -25,5 +25,10 @@ export interface WorkflowChildCaps {
25
25
  childMaxTokens?: number;
26
26
  childMaxTurns?: number;
27
27
  }
28
+ export interface ResourceClampNote {
29
+ field: "maxCostUsd" | "maxTokens" | "timeoutSec" | "maxTurns";
30
+ requested: number | undefined;
31
+ applied: number;
32
+ }
28
33
  export declare function resolveModelName(name: string, allowlist: string[] | undefined, models: Record<string, Model> | undefined): Model;
29
- export declare function buildGovernedChildSpec(scriptSpec: unknown, baseline: WorkflowGovernanceBaseline, models: Record<string, Model> | undefined, caps?: WorkflowChildCaps): TaskSpec;
34
+ export declare function buildGovernedChildSpec(scriptSpec: unknown, baseline: WorkflowGovernanceBaseline, models: Record<string, Model> | undefined, caps?: WorkflowChildCaps, onResourceClamp?: (notes: ResourceClampNote[]) => void): TaskSpec;
@@ -99,26 +99,46 @@ function pickWhitelist(scriptSpec) {
99
99
  return { safe, modelName };
100
100
  }
101
101
  function clampResourceLimits(safe, base, caps) {
102
+ const notes = [];
103
+ const requestedCost = safe.maxCostUsd;
102
104
  const cost = minDefined(safe.maxCostUsd, base.maxCostUsd, caps?.childMaxCostUsd);
103
- if (cost !== undefined)
105
+ if (cost !== undefined) {
106
+ if (cost !== requestedCost)
107
+ notes.push({ field: "maxCostUsd", requested: requestedCost, applied: cost });
104
108
  safe.maxCostUsd = cost;
109
+ }
110
+ const requestedTokens = safe.maxTokens;
105
111
  const tokens = minDefined(safe.maxTokens, base.maxTokens, caps?.childMaxTokens);
106
- if (tokens !== undefined)
112
+ if (tokens !== undefined) {
113
+ if (tokens !== requestedTokens)
114
+ notes.push({ field: "maxTokens", requested: requestedTokens, applied: tokens });
107
115
  safe.maxTokens = tokens;
116
+ }
117
+ const requestedTimeoutSec = safe.limits?.timeoutSec;
118
+ const requestedMaxTurns = safe.limits?.maxTurns;
108
119
  const timeoutSec = minDefined(safe.limits?.timeoutSec, base.limits?.timeoutSec, caps?.perAgentTimeoutSec);
109
120
  const maxTurns = minDefined(safe.limits?.maxTurns, base.limits?.maxTurns, caps?.childMaxTurns);
121
+ if (timeoutSec !== undefined && timeoutSec !== requestedTimeoutSec) {
122
+ notes.push({ field: "timeoutSec", requested: requestedTimeoutSec, applied: timeoutSec });
123
+ }
124
+ if (maxTurns !== undefined && maxTurns !== requestedMaxTurns) {
125
+ notes.push({ field: "maxTurns", requested: requestedMaxTurns, applied: maxTurns });
126
+ }
110
127
  if (timeoutSec !== undefined || maxTurns !== undefined) {
111
128
  safe.limits = {
112
129
  ...(maxTurns !== undefined ? { maxTurns } : {}),
113
130
  ...(timeoutSec !== undefined ? { timeoutSec } : {}),
114
131
  };
115
132
  }
133
+ return notes;
116
134
  }
117
- export function buildGovernedChildSpec(scriptSpec, baseline, models, caps) {
135
+ export function buildGovernedChildSpec(scriptSpec, baseline, models, caps, onResourceClamp) {
118
136
  const { safe, modelName } = pickWhitelist(scriptSpec);
119
137
  if (modelName !== undefined) {
120
138
  safe.model = resolveModelName(modelName, baseline.workflowModelAllowlist, models);
121
139
  }
122
- clampResourceLimits(safe, baseline.base, caps);
140
+ const clampNotes = clampResourceLimits(safe, baseline.base, caps);
141
+ if (clampNotes.length > 0 && onResourceClamp)
142
+ onResourceClamp(clampNotes);
123
143
  return tightenTaskSpec(baseline.base, safe);
124
144
  }
@@ -1,3 +1,4 @@
1
+ import { assertSupportedAgentIsolation } from "./workflow.js";
1
2
  import { buildGovernedChildSpec } from "./workflow-governance.js";
2
3
  function safeAgentOptions(opts) {
3
4
  if (typeof opts !== "object" || opts === null)
@@ -12,10 +13,15 @@ function safeAgentOptions(opts) {
12
13
  out.schema = o.schema;
13
14
  if (typeof o.agentType === "string")
14
15
  out.agentType = o.agentType;
16
+ assertSupportedAgentIsolation(o.isolation);
15
17
  if (o.isolation === "worktree")
16
18
  out.isolation = "worktree";
17
19
  return out;
18
20
  }
21
+ function formatResourceClampNote(notes) {
22
+ const parts = notes.map((n) => `${n.field}: requested ${n.requested === undefined ? "unset" : n.requested} → applied ${n.applied}`);
23
+ return `workflow governance tightened this agent's resource limits (${parts.join("; ")})`;
24
+ }
19
25
  export function buildWorkflowPrimitives(ctx, governance, onAgentSpawn, parentThinking, parentPrincipal) {
20
26
  const agent = (spec, opts) => {
21
27
  if (typeof spec === "string")
@@ -23,7 +29,7 @@ export function buildWorkflowPrimitives(ctx, governance, onAgentSpawn, parentThi
23
29
  const agentOpts = safeAgentOptions(opts);
24
30
  const effectiveBaseline = (b) => agentOpts.isolation === "worktree" && b.worktreeBase !== undefined ? { ...b, base: { ...b.base, ...b.worktreeBase } } : b;
25
31
  const childSpec = governance
26
- ? buildGovernedChildSpec(spec, effectiveBaseline(governance.baseline), governance.models, governance.caps)
32
+ ? buildGovernedChildSpec(spec, effectiveBaseline(governance.baseline), governance.models, governance.caps, (notes) => ctx.log(formatResourceClampNote(notes)))
27
33
  : { ...spec };
28
34
  if (childSpec.thinking === undefined && parentThinking) {
29
35
  const inherited = parentThinking();
@@ -43,6 +43,7 @@ export interface WorkflowAgentRun {
43
43
  attempts?: number;
44
44
  lastAttemptReason?: string;
45
45
  sessionId?: string;
46
+ worktreeDir?: string;
46
47
  }
47
48
  export interface WorkflowRunStats {
48
49
  tokens: number;
@@ -18,6 +18,7 @@ export declare function workflowAgentCallKey(ordinal: number, spec: TaskSpec, op
18
18
  schema?: TSchema;
19
19
  isolation?: "worktree";
20
20
  }): string;
21
+ export declare function assertSupportedAgentIsolation(isolation: unknown): asserts isolation is "worktree" | undefined;
21
22
  export interface WorkflowFanOutSlotError {
22
23
  index: number;
23
24
  kind: string;