@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.
- package/dist/agents/send-message-tool.d.ts +4 -0
- package/dist/agents/send-message-tool.js +37 -24
- package/dist/agents/subagent.js +279 -128
- package/dist/brain/errors.d.ts +1 -0
- package/dist/brain/errors.js +14 -0
- package/dist/brain/stream-engine.js +3 -3
- package/dist/core/auto-compaction.d.ts +2 -0
- package/dist/core/auto-compaction.js +2 -1
- package/dist/core/context-edit.js +2 -1
- package/dist/core/mcp.js +4 -1
- package/dist/core/runner/prepare-task.d.ts +5 -0
- package/dist/core/runner/prepare-task.js +41 -2
- package/dist/core/runner/runtask.js +39 -9
- package/dist/core/runner/tool-disclosure.d.ts +1 -0
- package/dist/core/runner/tool-disclosure.js +16 -5
- package/dist/core/runner/tool-output-projection.js +2 -1
- package/dist/core/session-reconcile.d.ts +1 -0
- package/dist/core/session-reconcile.js +40 -20
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +37 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +195 -0
- package/dist/core/store-contracts/contract-harness.d.ts +6 -0
- package/dist/core/store-contracts/contract-harness.js +16 -0
- package/dist/core/store-contracts/contract-kit-version.d.ts +1 -0
- package/dist/core/store-contracts/contract-kit-version.js +2 -0
- package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +3 -0
- package/dist/core/store-contracts/file-snapshot-store-contract.js +126 -0
- package/dist/core/store-contracts/mailbox-store-contract.d.ts +6 -0
- package/dist/core/store-contracts/mailbox-store-contract.js +193 -0
- package/dist/core/store-contracts/session-repo-contract.d.ts +3 -0
- package/dist/core/store-contracts/session-repo-contract.js +36 -0
- package/dist/core/store-contracts/tool-result-store-contract.d.ts +3 -0
- package/dist/core/store-contracts/tool-result-store-contract.js +35 -0
- package/dist/core/task-notification.d.ts +2 -0
- package/dist/core/task-registry-agent.d.ts +6 -0
- package/dist/core/task-registry-agent.js +24 -1
- package/dist/core/task-registry-monitor.js +6 -6
- package/dist/core/task-registry-shared.d.ts +9 -2
- package/dist/core/task-registry-shared.js +1 -1
- package/dist/core/task-registry.d.ts +8 -0
- package/dist/core/task-registry.js +59 -4
- package/dist/core/tool-result-store.d.ts +3 -2
- package/dist/core/tool-result-store.js +12 -4
- package/dist/core/trace.d.ts +7 -0
- package/dist/core/types.d.ts +10 -3
- package/dist/engine/compaction/compaction.d.ts +5 -0
- package/dist/engine/compaction/compaction.js +68 -2
- package/dist/engine/compaction/utils.d.ts +6 -0
- package/dist/engine/compaction/utils.js +53 -3
- package/dist/engine/harness/messages.d.ts +1 -1
- package/dist/engine/harness/messages.js +11 -3
- package/dist/engine/loop/types.d.ts +2 -0
- package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
- package/dist/engine/lsp/node-lsp-manager.js +16 -0
- package/dist/engine/session/import-validate.js +30 -1
- package/dist/engine/session/session.js +7 -5
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/internal/harness.d.ts +1 -1
- package/dist/internal/harness.js +1 -1
- package/dist/orchestration/builtin-workflows.d.ts +1 -1
- package/dist/orchestration/builtin-workflows.js +11 -2
- package/dist/orchestration/workflow-governance.d.ts +6 -1
- package/dist/orchestration/workflow-governance.js +24 -4
- package/dist/orchestration/workflow-primitives.js +7 -1
- package/dist/orchestration/workflow-types.d.ts +1 -0
- package/dist/orchestration/workflow.d.ts +1 -0
- package/dist/orchestration/workflow.js +41 -3
- package/dist/tools/fs/fs-bash.d.ts +7 -1
- package/dist/tools/fs/fs-bash.js +59 -22
- package/dist/tools/fs/fs-read.js +22 -11
- package/dist/tools/fs/fs-search-tools.js +3 -3
- package/dist/tools/fs/fs-shared.d.ts +20 -7
- package/dist/tools/fs/fs-shared.js +17 -3
- package/dist/tools/fs/fs-write.js +4 -4
- package/dist/tools/fs/index.d.ts +2 -0
- package/dist/tools/fs/index.js +7 -1
- package/dist/tools/fs/repo-map.js +2 -2
- package/dist/tools/fs/safety.d.ts +10 -0
- package/dist/tools/fs/safety.js +15 -1
- package/dist/tools/monitor.js +18 -4
- package/dist/tools/web.js +6 -2
- package/dist/tools/worktree.js +46 -25
- 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
|
-
|
|
221
|
-
|
|
222
|
-
.replace(/\n\n<modified-files>\n[\s\S]*?\n
|
|
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
|
-
|
|
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";
|
package/dist/internal/harness.js
CHANGED
|
@@ -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
|
|
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
|
|
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();
|
|
@@ -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;
|