@sema-agent/core 2.2.0 → 2.4.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 +275 -127
- package/dist/agents/teacher.js +51 -23
- 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/context-edit.js +2 -1
- package/dist/core/runner/assemble-result.d.ts +1 -0
- package/dist/core/runner/assemble-result.js +12 -8
- package/dist/core/runner/prepare-task.js +32 -7
- package/dist/core/runner/runtask.js +18 -5
- package/dist/core/runner/tool-output-projection.js +2 -1
- 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 +7 -1
- package/dist/core/task-registry-agent.js +31 -2
- package/dist/core/task-registry-monitor.js +71 -6
- package/dist/core/task-registry-shared.d.ts +18 -1
- package/dist/core/task-registry-shared.js +5 -2
- package/dist/core/task-registry.d.ts +9 -0
- package/dist/core/task-registry.js +52 -7
- package/dist/core/tool-result-store.d.ts +3 -2
- package/dist/core/tool-result-store.js +12 -4
- package/dist/core/tools.d.ts +2 -0
- package/dist/core/tools.js +9 -0
- package/dist/core/trace.d.ts +7 -0
- package/dist/core/workflow-journal-store.d.ts +16 -0
- package/dist/core/workflow-journal-store.js +28 -0
- package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
- package/dist/engine/lsp/node-lsp-manager.js +16 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- 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.d.ts +16 -0
- package/dist/orchestration/workflow.js +94 -21
- package/dist/tools/fs/fs-bash.d.ts +7 -1
- package/dist/tools/fs/fs-bash.js +51 -20
- 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.d.ts +2 -0
- package/dist/tools/monitor.js +20 -4
- package/dist/tools/web.js +6 -2
- package/dist/tools/worktree.js +46 -25
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -129,5 +130,20 @@ export interface WorkflowTimers {
|
|
|
129
130
|
setTimeout(fn: () => void, ms: number): unknown;
|
|
130
131
|
clearTimeout(handle: unknown): void;
|
|
131
132
|
}
|
|
133
|
+
export declare const workflowResumeClaimFallback: {
|
|
134
|
+
acquire(store: WorkflowJournalStore, input: {
|
|
135
|
+
sourceRunId: string;
|
|
136
|
+
newRunId: string;
|
|
137
|
+
scope: string;
|
|
138
|
+
}): {
|
|
139
|
+
granted: boolean;
|
|
140
|
+
holder?: string;
|
|
141
|
+
};
|
|
142
|
+
release(store: WorkflowJournalStore, input: {
|
|
143
|
+
sourceRunId: string;
|
|
144
|
+
newRunId: string;
|
|
145
|
+
scope: string;
|
|
146
|
+
}): void;
|
|
147
|
+
};
|
|
132
148
|
export declare function startWorkflow<T>(runner: Runner, fn: (ctx: WorkflowRunContext) => Promise<T>, opts?: RunWorkflowOptions, internals?: WorkflowInternals): WorkflowHandle<T>;
|
|
133
149
|
export declare function runWorkflow<T>(runner: Runner, fn: (ctx: WorkflowRunContext) => Promise<T>, opts?: RunWorkflowOptions, internals?: WorkflowInternals): Promise<RunWorkflowResult<T>>;
|
|
@@ -20,6 +20,7 @@ const MAX_TRANSCRIPT_CHARS = 4000;
|
|
|
20
20
|
const WORKFLOW_RESULT_MAX = 4000;
|
|
21
21
|
const WORKFLOW_RESULT_FULL_MAX = 200_000;
|
|
22
22
|
const MAX_ACTIVITY = 30;
|
|
23
|
+
const RUNNING_AGENT_PERSIST_EVERY_BEATS = 4;
|
|
23
24
|
function workflowModelLabel(spec) {
|
|
24
25
|
const model = spec.model;
|
|
25
26
|
if (model === undefined)
|
|
@@ -103,6 +104,21 @@ export function workflowAgentCallKey(ordinal, spec, opts) {
|
|
|
103
104
|
};
|
|
104
105
|
return `${ordinal}:${boundInputHashOf(identity)}`;
|
|
105
106
|
}
|
|
107
|
+
function resolveChildSessionIdAtSpawn(spec) {
|
|
108
|
+
if (spec.sessionId)
|
|
109
|
+
return spec.sessionId;
|
|
110
|
+
if (spec.requireExistingSession === true || spec.resumeAt !== undefined)
|
|
111
|
+
return undefined;
|
|
112
|
+
return randomUUID();
|
|
113
|
+
}
|
|
114
|
+
export function assertSupportedAgentIsolation(isolation) {
|
|
115
|
+
if (isolation !== undefined && isolation !== "worktree") {
|
|
116
|
+
const shown = typeof isolation === "string" ? JSON.stringify(isolation) : String(isolation);
|
|
117
|
+
const e = new Error(`isolation ${shown} is not supported in workflow agents — only "worktree" (omit the option to run in the shared working tree). The agent was not started (fail-closed: an unrecognized isolation value must never silently run in the shared working tree).`);
|
|
118
|
+
e.code = "isolation.invalid";
|
|
119
|
+
throw e;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
106
122
|
function rethrowIfMaxAgents(e) {
|
|
107
123
|
if (e instanceof WorkflowMaxAgentsError)
|
|
108
124
|
throw e;
|
|
@@ -206,6 +222,33 @@ function createSemaphore(max) {
|
|
|
206
222
|
function clampRunIdText(raw) {
|
|
207
223
|
return raw.replace(/[^A-Za-z0-9_.:-]/g, "").slice(0, 64);
|
|
208
224
|
}
|
|
225
|
+
const fallbackResumeClaimTables = new WeakMap();
|
|
226
|
+
function fallbackResumeClaimKey(scope, sourceRunId) {
|
|
227
|
+
return JSON.stringify([scope, sourceRunId]);
|
|
228
|
+
}
|
|
229
|
+
export const workflowResumeClaimFallback = {
|
|
230
|
+
acquire(store, input) {
|
|
231
|
+
let table = fallbackResumeClaimTables.get(store);
|
|
232
|
+
if (table === undefined) {
|
|
233
|
+
table = new Map();
|
|
234
|
+
fallbackResumeClaimTables.set(store, table);
|
|
235
|
+
}
|
|
236
|
+
const key = fallbackResumeClaimKey(input.scope, input.sourceRunId);
|
|
237
|
+
const holder = table.get(key);
|
|
238
|
+
if (holder !== undefined && holder !== input.newRunId)
|
|
239
|
+
return { granted: false, holder };
|
|
240
|
+
table.set(key, input.newRunId);
|
|
241
|
+
return { granted: true };
|
|
242
|
+
},
|
|
243
|
+
release(store, input) {
|
|
244
|
+
const table = fallbackResumeClaimTables.get(store);
|
|
245
|
+
if (table === undefined)
|
|
246
|
+
return;
|
|
247
|
+
const key = fallbackResumeClaimKey(input.scope, input.sourceRunId);
|
|
248
|
+
if (table.get(key) === input.newRunId)
|
|
249
|
+
table.delete(key);
|
|
250
|
+
},
|
|
251
|
+
};
|
|
209
252
|
export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
210
253
|
const alsDepth = currentWorkflowDepth();
|
|
211
254
|
const internalDepth = internals?.workflowDepth;
|
|
@@ -566,8 +609,9 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
566
609
|
throw err;
|
|
567
610
|
}
|
|
568
611
|
};
|
|
569
|
-
const createActivityCapture = (callKey, label, groupId) => {
|
|
612
|
+
const createActivityCapture = (callKey, label, groupId, rec) => {
|
|
570
613
|
const tail = [];
|
|
614
|
+
let beatCount = 0;
|
|
571
615
|
const onActivity = (a) => {
|
|
572
616
|
if (finalized)
|
|
573
617
|
return;
|
|
@@ -587,6 +631,13 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
587
631
|
tail.push(a);
|
|
588
632
|
if (tail.length > MAX_ACTIVITY)
|
|
589
633
|
tail.shift();
|
|
634
|
+
if (a.phase === "start")
|
|
635
|
+
rec.toolCalls = (rec.toolCalls ?? 0) + 1;
|
|
636
|
+
rec.activity = tail;
|
|
637
|
+
beatCount += 1;
|
|
638
|
+
if (beatCount === 1 || beatCount % RUNNING_AGENT_PERSIST_EVERY_BEATS === 0) {
|
|
639
|
+
void persist("update");
|
|
640
|
+
}
|
|
590
641
|
};
|
|
591
642
|
return { tail, onActivity };
|
|
592
643
|
};
|
|
@@ -664,6 +715,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
664
715
|
if ("error" in compiled)
|
|
665
716
|
throw new TypeError(`agent({schema}) received an invalid JSON Schema: ${compiled.error}`);
|
|
666
717
|
}
|
|
718
|
+
assertSupportedAgentIsolation(agentOpts.isolation);
|
|
667
719
|
const label = agentOpts.label ?? `agent-${run.agents.length + 1}`;
|
|
668
720
|
const phase = agentOpts.phase ?? currentPhase?.title;
|
|
669
721
|
const phaseInstance = resolveAgentPhase(agentOpts.phase);
|
|
@@ -734,8 +786,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
734
786
|
if (budgetTotal !== null && spent() >= budgetTotal) {
|
|
735
787
|
throw new WorkflowBudgetExceededError(spent(), budgetTotal);
|
|
736
788
|
}
|
|
737
|
-
const { tail: activityTail, onActivity } = createActivityCapture(callKey, label, groupId);
|
|
738
789
|
const rec = { label, callKey, ...(groupId !== undefined ? { groupId } : {}), phase, prompt, ...(model !== undefined ? { model } : {}), status: "running", queuedAt: now() };
|
|
790
|
+
const { tail: activityTail, onActivity } = createActivityCapture(callKey, label, groupId, rec);
|
|
739
791
|
run.agents.push(rec);
|
|
740
792
|
if (phaseInstance)
|
|
741
793
|
agentPhaseOf.set(rec, phaseInstance);
|
|
@@ -792,7 +844,12 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
792
844
|
const onCallerAbort = () => attemptCtl.abort(new Error("workflow aborted"));
|
|
793
845
|
effectiveSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
794
846
|
armWatchdog();
|
|
795
|
-
const
|
|
847
|
+
const attemptSessionId = resolveChildSessionIdAtSpawn(runSpec);
|
|
848
|
+
const attemptSpec = attemptSessionId !== undefined ? { ...runSpec, sessionId: attemptSessionId, signal: attemptCtl.signal } : { ...runSpec, signal: attemptCtl.signal };
|
|
849
|
+
if (attemptSessionId !== undefined && !finalized) {
|
|
850
|
+
rec.sessionId = attemptSessionId;
|
|
851
|
+
void persist("update");
|
|
852
|
+
}
|
|
796
853
|
const attemptInternals = {
|
|
797
854
|
...baseInternals,
|
|
798
855
|
...(bceSink !== undefined
|
|
@@ -1035,6 +1092,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1035
1092
|
if ("error" in compiled)
|
|
1036
1093
|
throw new TypeError(`agent({schema}) received an invalid JSON Schema: ${compiled.error}`);
|
|
1037
1094
|
}
|
|
1095
|
+
assertSupportedAgentIsolation(agentOpts.isolation);
|
|
1038
1096
|
const label = agentOpts.label ?? `agent-${run.agents.length + 1}`;
|
|
1039
1097
|
const phase = agentOpts.phase ?? currentPhase?.title;
|
|
1040
1098
|
const phaseInstance = resolveAgentPhase(agentOpts.phase);
|
|
@@ -1047,8 +1105,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1047
1105
|
const model = workflowModelLabel(specForIdentity);
|
|
1048
1106
|
noteDivergence(run.agents.length, "ctx.agentStream results are never replayed");
|
|
1049
1107
|
diverged = true;
|
|
1050
|
-
const { tail: activityTail, onActivity } = createActivityCapture(callKey, label, groupId);
|
|
1051
1108
|
const rec = { label, callKey, ...(groupId !== undefined ? { groupId } : {}), phase, prompt, ...(model !== undefined ? { model } : {}), status: "running", queuedAt: now() };
|
|
1109
|
+
const { tail: activityTail, onActivity } = createActivityCapture(callKey, label, groupId, rec);
|
|
1052
1110
|
run.agents.push(rec);
|
|
1053
1111
|
if (phaseInstance)
|
|
1054
1112
|
agentPhaseOf.set(rec, phaseInstance);
|
|
@@ -1079,14 +1137,17 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1079
1137
|
rec.startedAt = now();
|
|
1080
1138
|
bceSpawn(callKey, label, agentOpts.agentType, false);
|
|
1081
1139
|
let stream;
|
|
1140
|
+
let childSessionId;
|
|
1082
1141
|
try {
|
|
1083
1142
|
const typedSpec0 = applyWorkflowAgentType(spec, agentOpts.agentType, agentRegistry);
|
|
1084
1143
|
const typedSpec = typedSpec0.model === undefined && inheritedModelSnap !== undefined ? { ...typedSpec0, model: inheritedModelSnap } : typedSpec0;
|
|
1085
1144
|
const framedSpec = withWorkflowChildPersona(typedSpec, agentOpts.schema ?? typedSpec.outputSchema);
|
|
1086
1145
|
const authInherit = framedSpec.getApiKeyAndHeaders === undefined && opts.defaultGetApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: opts.defaultGetApiKeyAndHeaders } : {};
|
|
1087
|
-
const
|
|
1146
|
+
const baseRunSpec = agentOpts.schema
|
|
1088
1147
|
? { ...framedSpec, ...authInherit, signal: effectiveSignal, outputSchema: agentOpts.schema }
|
|
1089
1148
|
: { ...framedSpec, ...authInherit, signal: effectiveSignal };
|
|
1149
|
+
childSessionId = resolveChildSessionIdAtSpawn(baseRunSpec);
|
|
1150
|
+
const runSpec = childSessionId !== undefined ? { ...baseRunSpec, sessionId: childSessionId } : baseRunSpec;
|
|
1090
1151
|
const enrichedForwardS = opts.onForwardEvent !== undefined
|
|
1091
1152
|
? (e) => {
|
|
1092
1153
|
opts.onForwardEvent(e.type === "task_progress" ? { ...e, workflowRunId: runId, workflowAgentLabel: label } : e);
|
|
@@ -1121,6 +1182,10 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1121
1182
|
bceTerminal(callKey, "failed", err instanceof Error ? err.message : String(err), undefined, undefined);
|
|
1122
1183
|
throw err;
|
|
1123
1184
|
}
|
|
1185
|
+
if (childSessionId !== undefined && !finalized) {
|
|
1186
|
+
rec.sessionId = childSessionId;
|
|
1187
|
+
void persist("update");
|
|
1188
|
+
}
|
|
1124
1189
|
const steer = async (content) => {
|
|
1125
1190
|
const marker = `steer-${++steerMarkerSeq}`;
|
|
1126
1191
|
const framed = `[operator steer ${marker}] An operator/leader sent guidance for your task. Take it into account on your NEXT step. ` +
|
|
@@ -1388,18 +1453,19 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1388
1453
|
try {
|
|
1389
1454
|
const runBody = async () => {
|
|
1390
1455
|
if (opts.resumeFromRunId !== undefined && journalStore) {
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1456
|
+
const usingFallback = journalStore.resumeClaim === undefined;
|
|
1457
|
+
const verdict = usingFallback
|
|
1458
|
+
? Promise.resolve(workflowResumeClaimFallback.acquire(journalStore, { sourceRunId: opts.resumeFromRunId, newRunId: runId, scope }))
|
|
1459
|
+
: journalStore.resumeClaim({ sourceRunId: opts.resumeFromRunId, newRunId: runId, scope });
|
|
1460
|
+
resumeClaim = { sourceRunId: opts.resumeFromRunId, verdict, fallback: usingFallback };
|
|
1461
|
+
const decision = await verdict;
|
|
1462
|
+
if (!decision.granted) {
|
|
1463
|
+
const holder = decision.holder !== undefined ? clampRunIdText(decision.holder) : "";
|
|
1464
|
+
throw new Error(`startWorkflow: resume from "${clampRunIdText(opts.resumeFromRunId)}" was REFUSED — ` +
|
|
1465
|
+
`another run already holds the resume claim on it${holder ? ` (holder: ${holder})` : " (holder unknown to the store)"}. ` +
|
|
1466
|
+
"Two concurrent resumes of one source run fork its execution: both replay the same prefix and then " +
|
|
1467
|
+
"re-run the whole suffix live, duplicating every side effect. Wait for the holder to reach a terminal " +
|
|
1468
|
+
"state (or stop it) and resume again.");
|
|
1403
1469
|
}
|
|
1404
1470
|
const entries = await journalStore.load(opts.resumeFromRunId, scope);
|
|
1405
1471
|
for (const e of entries)
|
|
@@ -1501,13 +1567,20 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1501
1567
|
finally {
|
|
1502
1568
|
finalized = true;
|
|
1503
1569
|
await journalTail.catch(() => undefined);
|
|
1504
|
-
if (resumeClaim !== undefined
|
|
1570
|
+
if (resumeClaim !== undefined) {
|
|
1505
1571
|
const granted = await finalizeWithin(timers, resumeClaim.verdict.then((v) => v.granted, () => false), false);
|
|
1506
1572
|
if (granted) {
|
|
1507
|
-
|
|
1508
|
-
|
|
1573
|
+
if (resumeClaim.fallback) {
|
|
1574
|
+
if (journalStore) {
|
|
1575
|
+
workflowResumeClaimFallback.release(journalStore, { sourceRunId: resumeClaim.sourceRunId, newRunId: runId, scope });
|
|
1576
|
+
}
|
|
1509
1577
|
}
|
|
1510
|
-
|
|
1578
|
+
else if (journalStore?.releaseResumeClaim) {
|
|
1579
|
+
try {
|
|
1580
|
+
await finalizeWithin(timers, journalStore.releaseResumeClaim({ sourceRunId: resumeClaim.sourceRunId, newRunId: runId, scope }), undefined);
|
|
1581
|
+
}
|
|
1582
|
+
catch {
|
|
1583
|
+
}
|
|
1511
1584
|
}
|
|
1512
1585
|
}
|
|
1513
1586
|
}
|
|
@@ -26,7 +26,13 @@ export declare function createBashTool(env: ExecutionEnv, rootCanonical: string,
|
|
|
26
26
|
execClamp?: ExecClampOption;
|
|
27
27
|
autoBackgroundOnTimeout?: boolean;
|
|
28
28
|
oneShot?: boolean;
|
|
29
|
+
additionalRoots?: readonly string[];
|
|
30
|
+
bashDefaultTimeoutMs?: number;
|
|
31
|
+
bashMaxTimeoutMs?: number;
|
|
32
|
+
}): AgentTool;
|
|
33
|
+
export declare function createBashReadonlyTool(env: ExecutionEnv, rootCanonical: string, allow: ReadonlySet<string>, execClamp?: ExecClampOption, timeoutOpts?: {
|
|
34
|
+
bashDefaultTimeoutMs?: number;
|
|
35
|
+
bashMaxTimeoutMs?: number;
|
|
29
36
|
}): AgentTool;
|
|
30
|
-
export declare function createBashReadonlyTool(env: ExecutionEnv, rootCanonical: string, allow: ReadonlySet<string>, execClamp?: ExecClampOption): AgentTool;
|
|
31
37
|
export declare function createEnvTaskOutputTool(env: ExecutionEnv): AgentTool;
|
|
32
38
|
export declare function createEnvTaskStopTool(env: ExecutionEnv, registry?: TaskRegistry): AgentTool;
|