@crewhaus/ir 0.3.2 → 0.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/index.d.ts +615 -6
- package/dist/index.js +5 -0
- package/dist/loop.d.ts +132 -0
- package/dist/loop.js +641 -0
- package/package.json +1 -1
package/dist/loop.js
ADDED
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
/** Canonical segment order — every ring and every node mini uses exactly this. */
|
|
2
|
+
export const SEGMENT_ORDER = [
|
|
3
|
+
"perceive",
|
|
4
|
+
"reason",
|
|
5
|
+
"act",
|
|
6
|
+
"evaluate",
|
|
7
|
+
"update",
|
|
8
|
+
"stop",
|
|
9
|
+
"safety",
|
|
10
|
+
];
|
|
11
|
+
// --- target families -----------------------------------------------------------
|
|
12
|
+
/** Single-agent shapes rendered as the seven-component ring. */
|
|
13
|
+
export const RING_TARGETS = ["cli", "channel", "managed"];
|
|
14
|
+
/** Step/node/role shapes rendered as a node canvas. */
|
|
15
|
+
export const CANVAS_TARGETS = [
|
|
16
|
+
"workflow",
|
|
17
|
+
"graph",
|
|
18
|
+
"crew",
|
|
19
|
+
"pipeline",
|
|
20
|
+
"research",
|
|
21
|
+
"batch",
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* The exact defaults-only Stop warning (guardrails-first affordance): with
|
|
25
|
+
* neither `budget:` nor `limits:` the loop's only boundary is the runtime's
|
|
26
|
+
* hardcoded tool-iteration cap. Shared verbatim with the studio.
|
|
27
|
+
*/
|
|
28
|
+
export const NO_BUDGET_WARNING = "no budget: — stops only at the 500-iteration default";
|
|
29
|
+
/**
|
|
30
|
+
* Tool names that count as PERCEPTION (bringing outside state into the
|
|
31
|
+
* loop) rather than plain action. Everything in `tools` still counts toward
|
|
32
|
+
* the Act segment; matching names ALSO light Perceive.
|
|
33
|
+
*/
|
|
34
|
+
export const PERCEIVE_TOOL_RE = /(browse|fetch|web|search|crawl|navigate|retrieve)/i;
|
|
35
|
+
// --- tiny formatting helpers ----------------------------------------------------
|
|
36
|
+
function truncate(s, max) {
|
|
37
|
+
const flat = s.replace(/\s+/g, " ").trim();
|
|
38
|
+
return flat.length <= max ? flat : `${flat.slice(0, max - 1)}…`;
|
|
39
|
+
}
|
|
40
|
+
/** "3 tools (read, write, bash)" — list up to `max` names, then "+n more". */
|
|
41
|
+
function countList(n, noun, names, max = 6) {
|
|
42
|
+
const shown = names.slice(0, max);
|
|
43
|
+
const extra = names.length - shown.length;
|
|
44
|
+
const list = shown.length > 0 ? ` (${shown.join(", ")}${extra > 0 ? `, +${extra} more` : ""})` : "";
|
|
45
|
+
return `${n} ${noun}${n === 1 ? "" : "s"}${list}`;
|
|
46
|
+
}
|
|
47
|
+
function segment(id, keys, activeSummary, inactiveSummary) {
|
|
48
|
+
const active = keys.length > 0;
|
|
49
|
+
return { id, active, keys, summary: active ? activeSummary : inactiveSummary };
|
|
50
|
+
}
|
|
51
|
+
/** IR limits fields, rendered back as the spec's snake_case key names. */
|
|
52
|
+
const LIMITS_SPEC_KEYS = [
|
|
53
|
+
["maxToolIterations", "max_tool_iterations"],
|
|
54
|
+
["maxConcurrentTools", "max_concurrent_tools"],
|
|
55
|
+
["contextLimit", "context_limit"],
|
|
56
|
+
["deadlineMs", "deadline_ms"],
|
|
57
|
+
["turnTimeoutMs", "turn_timeout_ms"],
|
|
58
|
+
["modelCallTimeoutMs", "model_call_timeout_ms"],
|
|
59
|
+
["loopDetection", "loop_detection"],
|
|
60
|
+
["crew", "crew"],
|
|
61
|
+
];
|
|
62
|
+
function limitsSpecKeyNames(limits) {
|
|
63
|
+
return LIMITS_SPEC_KEYS.filter(([irKey]) => limits[irKey] !== undefined).map(([, specKey]) => specKey);
|
|
64
|
+
}
|
|
65
|
+
function describeEvaluation(e) {
|
|
66
|
+
const threshold = e.threshold !== undefined ? `, threshold ${e.threshold}` : "";
|
|
67
|
+
const retries = e.maxRetries > 1 ? `, ≤ ${e.maxRetries} retries` : "";
|
|
68
|
+
return `in-loop evaluation (${e.grader.type}${threshold}, on fail: ${e.onFail}${retries})`;
|
|
69
|
+
}
|
|
70
|
+
function describeJudge(j) {
|
|
71
|
+
return `judge gate (threshold ${j.threshold}, on fail: ${j.onFail})`;
|
|
72
|
+
}
|
|
73
|
+
function describeModelPool(pool) {
|
|
74
|
+
return `adaptive model pool (${pool.candidates.length} candidates, policy: ${pool.policy})`;
|
|
75
|
+
}
|
|
76
|
+
function ringSegmentsFromView(view) {
|
|
77
|
+
const tools = view.tools ?? [];
|
|
78
|
+
const toolsKey = view.toolsKey ?? "tools";
|
|
79
|
+
// perceive ← browse/fetch/web tools + channel ingress (+ heartbeat timer).
|
|
80
|
+
const perceiveTools = tools.filter((n) => PERCEIVE_TOOL_RE.test(n));
|
|
81
|
+
const perceiveKeys = perceiveTools.map((n) => `${toolsKey}[${n}]`);
|
|
82
|
+
const perceiveParts = [];
|
|
83
|
+
if (perceiveTools.length > 0)
|
|
84
|
+
perceiveParts.push(`web tools: ${perceiveTools.join(", ")}`);
|
|
85
|
+
if (view.channels !== undefined && view.channels.length > 0) {
|
|
86
|
+
perceiveKeys.push(...view.channels.map((c) => `channels.${c}`));
|
|
87
|
+
perceiveParts.push(`channel ingress: ${view.channels.join(", ")}`);
|
|
88
|
+
}
|
|
89
|
+
if (view.heartbeat === true) {
|
|
90
|
+
perceiveKeys.push("heartbeat");
|
|
91
|
+
perceiveParts.push("heartbeat timer");
|
|
92
|
+
}
|
|
93
|
+
const perceive = segment("perceive", perceiveKeys, perceiveParts.join(" · "), "input arrives only from the incoming message — no browse/fetch/web tools or channel ingress");
|
|
94
|
+
// reason ← agent.thinking / agent.model_tiers / agent.model_pool.
|
|
95
|
+
const reasonKeys = [];
|
|
96
|
+
const reasonParts = [];
|
|
97
|
+
if (view.thinking !== undefined) {
|
|
98
|
+
reasonKeys.push("agent.thinking");
|
|
99
|
+
reasonParts.push("extended thinking");
|
|
100
|
+
}
|
|
101
|
+
if (view.modelTiers !== undefined) {
|
|
102
|
+
reasonKeys.push("agent.model_tiers");
|
|
103
|
+
reasonParts.push("two-tier turn routing");
|
|
104
|
+
}
|
|
105
|
+
if (view.modelPool !== undefined) {
|
|
106
|
+
reasonKeys.push("agent.model_pool");
|
|
107
|
+
reasonParts.push(describeModelPool(view.modelPool));
|
|
108
|
+
}
|
|
109
|
+
const reason = segment("reason", reasonKeys, `${reasonParts.join(" · ")} on ${view.model}`, `single fixed model (${view.model}) — no thinking, tiers, or pool`);
|
|
110
|
+
// act ← tools / mcp_servers / sub_agents.
|
|
111
|
+
const actKeys = [];
|
|
112
|
+
const actParts = [];
|
|
113
|
+
if (tools.length > 0) {
|
|
114
|
+
actKeys.push(toolsKey);
|
|
115
|
+
actParts.push(countList(tools.length, "tool", tools));
|
|
116
|
+
}
|
|
117
|
+
const mcpNames = view.mcpServers !== undefined ? Object.keys(view.mcpServers) : [];
|
|
118
|
+
if (mcpNames.length > 0) {
|
|
119
|
+
actKeys.push("mcp_servers");
|
|
120
|
+
actParts.push(countList(mcpNames.length, "MCP server", mcpNames));
|
|
121
|
+
}
|
|
122
|
+
if (view.subAgents !== undefined && view.subAgents.length > 0) {
|
|
123
|
+
actKeys.push("agent.sub_agents");
|
|
124
|
+
actParts.push(countList(view.subAgents.length, "sub-agent", view.subAgents.map((s) => s.name)));
|
|
125
|
+
}
|
|
126
|
+
const act = segment("act", actKeys, actParts.join(" · "), "no tools, MCP servers, or sub-agents — replies in text only");
|
|
127
|
+
// evaluate ← evaluation / learning.exam / security.justification.
|
|
128
|
+
const evalKeys = [];
|
|
129
|
+
const evalParts = [];
|
|
130
|
+
if (view.evaluation !== undefined) {
|
|
131
|
+
evalKeys.push("evaluation");
|
|
132
|
+
evalParts.push(describeEvaluation(view.evaluation));
|
|
133
|
+
}
|
|
134
|
+
if (view.learning?.exam !== undefined) {
|
|
135
|
+
evalKeys.push("learning.exam");
|
|
136
|
+
evalParts.push(`competency exam (${view.learning.exam.dataset})`);
|
|
137
|
+
}
|
|
138
|
+
if (view.security?.justification !== undefined) {
|
|
139
|
+
evalKeys.push("security.justification");
|
|
140
|
+
evalParts.push(`justification intent gate (judge: ${view.security.justification.judge})`);
|
|
141
|
+
}
|
|
142
|
+
const evaluate = segment("evaluate", evalKeys, evalParts.join(" · "), "no in-loop evaluation — output is never checked before it ships");
|
|
143
|
+
// update ← memory / continuity / thredz / compaction.
|
|
144
|
+
const updKeys = [];
|
|
145
|
+
const updParts = [];
|
|
146
|
+
if (view.memory !== undefined) {
|
|
147
|
+
updKeys.push("memory");
|
|
148
|
+
const quals = [];
|
|
149
|
+
if (view.memory.backend !== undefined)
|
|
150
|
+
quals.push(`${view.memory.backend} backend`);
|
|
151
|
+
if (view.memory.wiki !== undefined)
|
|
152
|
+
quals.push("wiki");
|
|
153
|
+
if (view.memory.dream !== undefined)
|
|
154
|
+
quals.push("dream");
|
|
155
|
+
updParts.push(`memory${quals.length > 0 ? ` (${quals.join(", ")})` : ""}`);
|
|
156
|
+
}
|
|
157
|
+
if (view.continuity !== undefined) {
|
|
158
|
+
updKeys.push("continuity");
|
|
159
|
+
updParts.push(`continuity (proof: ${view.continuity.proof})`);
|
|
160
|
+
}
|
|
161
|
+
if (view.thredz === true) {
|
|
162
|
+
updKeys.push("thredz");
|
|
163
|
+
updParts.push("thredz wiki");
|
|
164
|
+
}
|
|
165
|
+
if (view.compaction !== undefined && Object.keys(view.compaction).length > 0) {
|
|
166
|
+
updKeys.push("compaction");
|
|
167
|
+
updParts.push(`compaction${view.compaction.curate === true ? " (curated)" : ""}`);
|
|
168
|
+
}
|
|
169
|
+
const update = segment("update", updKeys, updParts.join(" · "), view.continuityDefaultOn === true && view.continuity === undefined
|
|
170
|
+
? "continuity explicitly disabled — nothing durable persists between sessions"
|
|
171
|
+
: "no memory/continuity/thredz configured — facts are not persisted between sessions");
|
|
172
|
+
// stop ← budget / limits (hardcoded runtime defaults when absent).
|
|
173
|
+
const stopKeys = [];
|
|
174
|
+
const stopParts = [];
|
|
175
|
+
if (view.budget !== undefined) {
|
|
176
|
+
stopKeys.push("budget");
|
|
177
|
+
const usd = view.budget.usdMicros / 1_000_000;
|
|
178
|
+
const onExceed = view.budget.onExceed.kind === "degrade"
|
|
179
|
+
? ` (on exceed: degrade → ${view.budget.onExceed.model})`
|
|
180
|
+
: " (on exceed: stop)";
|
|
181
|
+
stopParts.push(`budget $${usd}${onExceed}`);
|
|
182
|
+
}
|
|
183
|
+
if (view.limits !== undefined) {
|
|
184
|
+
stopKeys.push("limits");
|
|
185
|
+
const names = limitsSpecKeyNames(view.limits);
|
|
186
|
+
stopParts.push(`limits${names.length > 0 ? ` (${names.join(", ")})` : ""}`);
|
|
187
|
+
}
|
|
188
|
+
const stop = segment("stop", stopKeys, stopParts.join(" · "), "defaults only — stops at the 500-iteration cap");
|
|
189
|
+
// safety ← permissions / security / hooks / transaction_policy.
|
|
190
|
+
const safeKeys = [];
|
|
191
|
+
const safeParts = [];
|
|
192
|
+
const perms = view.permissions;
|
|
193
|
+
if (perms !== undefined && (perms.mode !== undefined || perms.rules.length > 0)) {
|
|
194
|
+
safeKeys.push("permissions");
|
|
195
|
+
const quals = [
|
|
196
|
+
perms.mode !== undefined ? `mode: ${perms.mode}` : "",
|
|
197
|
+
perms.rules.length > 0
|
|
198
|
+
? `${perms.rules.length} rule${perms.rules.length === 1 ? "" : "s"}`
|
|
199
|
+
: "",
|
|
200
|
+
]
|
|
201
|
+
.filter((q) => q.length > 0)
|
|
202
|
+
.join(", ");
|
|
203
|
+
safeParts.push(`permissions${quals ? ` (${quals})` : ""}`);
|
|
204
|
+
}
|
|
205
|
+
if (view.security !== undefined) {
|
|
206
|
+
safeKeys.push("security");
|
|
207
|
+
const egress = view.security.egressMatcher;
|
|
208
|
+
safeParts.push(`security fabric${egress !== undefined ? ` (egress: ${egress})` : ""}`);
|
|
209
|
+
}
|
|
210
|
+
if (view.hooks !== undefined && view.hooks.length > 0) {
|
|
211
|
+
safeKeys.push("hooks");
|
|
212
|
+
safeParts.push("hooks");
|
|
213
|
+
}
|
|
214
|
+
if (view.transactionPolicy === true) {
|
|
215
|
+
safeKeys.push("transaction_policy");
|
|
216
|
+
safeParts.push("transaction policy");
|
|
217
|
+
}
|
|
218
|
+
const safety = segment("safety", safeKeys, safeParts.join(" · "), "no permissions, security, or transaction policy — runtime defaults only");
|
|
219
|
+
return [perceive, reason, act, evaluate, update, stop, safety];
|
|
220
|
+
}
|
|
221
|
+
function miniSegments(view) {
|
|
222
|
+
const tools = view.tools ?? [];
|
|
223
|
+
const perceiveTools = tools.filter((n) => PERCEIVE_TOOL_RE.test(n));
|
|
224
|
+
const perceive = segment("perceive", perceiveTools.map((n) => `tools[${n}]`), `web tools: ${perceiveTools.join(", ")}`, "sees only upstream state and its instructions");
|
|
225
|
+
const reasonKeys = [];
|
|
226
|
+
const reasonParts = [];
|
|
227
|
+
if (view.model !== undefined) {
|
|
228
|
+
reasonKeys.push("model");
|
|
229
|
+
reasonParts.push(`model: ${view.model}`);
|
|
230
|
+
}
|
|
231
|
+
if (view.thinking !== undefined) {
|
|
232
|
+
reasonKeys.push("thinking");
|
|
233
|
+
reasonParts.push("extended thinking");
|
|
234
|
+
}
|
|
235
|
+
if (view.modelPool !== undefined) {
|
|
236
|
+
reasonKeys.push("model_pool");
|
|
237
|
+
reasonParts.push(describeModelPool(view.modelPool));
|
|
238
|
+
}
|
|
239
|
+
const reason = segment("reason", reasonKeys, reasonParts.join(" · "), "inherits the spec-level model");
|
|
240
|
+
const actKeys = [];
|
|
241
|
+
const actParts = [];
|
|
242
|
+
if (tools.length > 0) {
|
|
243
|
+
actKeys.push("tools");
|
|
244
|
+
actParts.push(countList(tools.length, "tool", tools));
|
|
245
|
+
}
|
|
246
|
+
if (view.subAgents !== undefined && view.subAgents.length > 0) {
|
|
247
|
+
actKeys.push("sub_agents");
|
|
248
|
+
actParts.push(countList(view.subAgents.length, "sub-agent", view.subAgents.map((s) => s.name)));
|
|
249
|
+
}
|
|
250
|
+
const act = segment("act", actKeys, actParts.join(" · "), "no tools — replies in text only");
|
|
251
|
+
const evaluate = segment("evaluate", view.judge !== undefined ? ["judge"] : [], view.judge !== undefined ? describeJudge(view.judge) : "", "no in-loop evaluation");
|
|
252
|
+
// IR carries no node-level memory or budget/limits — those are spec-level.
|
|
253
|
+
const update = segment("update", [], "", "no node-level memory — shares the spec's stores");
|
|
254
|
+
const stop = segment("stop", [], "", "bounded by the surrounding orchestration");
|
|
255
|
+
const safety = segment("safety", view.hitlPrompt !== undefined ? ["hitl"] : [], `human approval gate${view.hitlPrompt !== undefined ? `: "${truncate(view.hitlPrompt, 60)}"` : ""}`, "no approval gate on this node");
|
|
256
|
+
return [perceive, reason, act, evaluate, update, stop, safety];
|
|
257
|
+
}
|
|
258
|
+
/** The all-inactive mini used by artifact nodes (docs, queues, reports). */
|
|
259
|
+
function emptyMini() {
|
|
260
|
+
return miniSegments({});
|
|
261
|
+
}
|
|
262
|
+
// --- canvas builders ---------------------------------------------------------------
|
|
263
|
+
/** Allocate a unique node id, suffixing duplicates ("draft", "draft-2", …). */
|
|
264
|
+
function claimId(used, wanted) {
|
|
265
|
+
let id = wanted;
|
|
266
|
+
let n = 2;
|
|
267
|
+
while (used.has(id)) {
|
|
268
|
+
id = `${wanted}-${n}`;
|
|
269
|
+
n += 1;
|
|
270
|
+
}
|
|
271
|
+
used.add(id);
|
|
272
|
+
return id;
|
|
273
|
+
}
|
|
274
|
+
function workflowCanvas(ir, warnings) {
|
|
275
|
+
if (ir.steps.length === 0)
|
|
276
|
+
warnings.push("workflow has no steps — nothing to run");
|
|
277
|
+
const used = new Set();
|
|
278
|
+
const nodes = ir.steps.map((step, i) => ({
|
|
279
|
+
id: claimId(used, step.name),
|
|
280
|
+
label: `${i + 1}. ${step.name}`,
|
|
281
|
+
kind: "step",
|
|
282
|
+
mini: miniSegments(step.kind === "judge"
|
|
283
|
+
? { model: step.model, ...(step.judge !== undefined ? { judge: step.judge } : {}) }
|
|
284
|
+
: {
|
|
285
|
+
model: step.model,
|
|
286
|
+
...(step.thinking !== undefined ? { thinking: step.thinking } : {}),
|
|
287
|
+
// Item 9 (G37) — per-step model pool surfaces in the reason segment.
|
|
288
|
+
...(step.modelPool !== undefined ? { modelPool: step.modelPool } : {}),
|
|
289
|
+
tools: step.tools,
|
|
290
|
+
}),
|
|
291
|
+
}));
|
|
292
|
+
const edges = [];
|
|
293
|
+
for (let i = 0; i < ir.steps.length; i += 1) {
|
|
294
|
+
const step = ir.steps[i];
|
|
295
|
+
const node = nodes[i];
|
|
296
|
+
const next = nodes[i + 1];
|
|
297
|
+
if (step === undefined || node === undefined)
|
|
298
|
+
continue;
|
|
299
|
+
if (next !== undefined) {
|
|
300
|
+
// The edge LEAVING a judge step only fires when the gate passes
|
|
301
|
+
// (except on_fail: continue, where output flows regardless).
|
|
302
|
+
if (step.kind === "judge" && step.judge !== undefined && step.judge.onFail !== "continue") {
|
|
303
|
+
edges.push({ from: node.id, to: next.id, label: "pass", conditional: true });
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
edges.push({ from: node.id, to: next.id });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const prev = nodes[i - 1];
|
|
310
|
+
if (step.kind === "judge" &&
|
|
311
|
+
step.judge !== undefined &&
|
|
312
|
+
step.judge.onFail === "retry_previous" &&
|
|
313
|
+
prev !== undefined) {
|
|
314
|
+
edges.push({
|
|
315
|
+
from: node.id,
|
|
316
|
+
to: prev.id,
|
|
317
|
+
label: `retry ≤ ${step.judge.maxRetries}`,
|
|
318
|
+
conditional: true,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return { nodes, edges };
|
|
323
|
+
}
|
|
324
|
+
function graphCanvas(ir, warnings) {
|
|
325
|
+
const ids = ir.nodes.map((n) => n.name);
|
|
326
|
+
if (ids.length === 0)
|
|
327
|
+
warnings.push("graph has no nodes — nothing to run");
|
|
328
|
+
if (ids.length > 0 && !ids.includes(ir.entry)) {
|
|
329
|
+
warnings.push(`entry "${ir.entry}" is not a declared node`);
|
|
330
|
+
}
|
|
331
|
+
const nodes = ir.nodes.map((node) => ({
|
|
332
|
+
id: node.name,
|
|
333
|
+
label: node.name === ir.entry ? `${node.name} (entry)` : node.name,
|
|
334
|
+
kind: "node",
|
|
335
|
+
...(node.hitlPrompt !== undefined ? { hitl: true } : {}),
|
|
336
|
+
mini: miniSegments(node.kind === "judge"
|
|
337
|
+
? { model: node.model, ...(node.judge !== undefined ? { judge: node.judge } : {}) }
|
|
338
|
+
: {
|
|
339
|
+
model: node.model,
|
|
340
|
+
...(node.thinking !== undefined ? { thinking: node.thinking } : {}),
|
|
341
|
+
tools: node.tools,
|
|
342
|
+
...(node.hitlPrompt !== undefined ? { hitlPrompt: node.hitlPrompt } : {}),
|
|
343
|
+
}),
|
|
344
|
+
}));
|
|
345
|
+
const known = new Set(ids);
|
|
346
|
+
const edges = [];
|
|
347
|
+
for (const edge of ir.edges) {
|
|
348
|
+
for (const end of [edge.from, edge.to]) {
|
|
349
|
+
if (!known.has(end)) {
|
|
350
|
+
warnings.push(`edge ${edge.from} → ${edge.to} references unknown node "${end}"`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const when = edge.when;
|
|
354
|
+
const label = when === undefined
|
|
355
|
+
? undefined
|
|
356
|
+
: when.exists === true
|
|
357
|
+
? `${when.key} exists`
|
|
358
|
+
: `${when.key} == ${String(when.equals)}`;
|
|
359
|
+
edges.push({
|
|
360
|
+
from: edge.from,
|
|
361
|
+
to: edge.to,
|
|
362
|
+
...(label !== undefined ? { label, conditional: true } : {}),
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
// Parallel barrier groups have no per-edge representation in the declared
|
|
366
|
+
// edge list; surface them as structural notes so the canvas stays honest.
|
|
367
|
+
if (ir.parallel !== undefined) {
|
|
368
|
+
for (const group of ir.parallel) {
|
|
369
|
+
warnings.push(`parallel group: ${group.join(", ")} run concurrently (barrier)`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return { nodes, edges };
|
|
373
|
+
}
|
|
374
|
+
function crewCanvas(ir, warnings) {
|
|
375
|
+
if (ir.roles.length === 0)
|
|
376
|
+
warnings.push("crew has no roles — nothing to run");
|
|
377
|
+
const nodes = ir.roles.map((role) => ({
|
|
378
|
+
id: role.name,
|
|
379
|
+
label: role.name === ir.entry ? `${role.name} (entry)` : role.name,
|
|
380
|
+
kind: "role",
|
|
381
|
+
mini: miniSegments({
|
|
382
|
+
model: role.model,
|
|
383
|
+
...(role.thinking !== undefined ? { thinking: role.thinking } : {}),
|
|
384
|
+
// Item 9 (G37) — per-role model pool surfaces in the reason segment.
|
|
385
|
+
...(role.modelPool !== undefined ? { modelPool: role.modelPool } : {}),
|
|
386
|
+
tools: role.tools,
|
|
387
|
+
subAgents: role.subAgents,
|
|
388
|
+
}),
|
|
389
|
+
}));
|
|
390
|
+
const edges = [];
|
|
391
|
+
if (ir.routing === undefined) {
|
|
392
|
+
warnings.push(`no routing: — the entry role ("${ir.entry}") handles every message`);
|
|
393
|
+
}
|
|
394
|
+
else if (ir.routing.kind === "llm") {
|
|
395
|
+
warnings.push("routing.kind: llm — an LLM router picks the next role at runtime");
|
|
396
|
+
for (const role of ir.roles) {
|
|
397
|
+
if (role.name !== ir.entry) {
|
|
398
|
+
edges.push({ from: ir.entry, to: role.name, label: "llm router", conditional: true });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
else if (ir.routing.match !== undefined) {
|
|
403
|
+
for (const [from, rules] of Object.entries(ir.routing.match)) {
|
|
404
|
+
for (const rule of rules) {
|
|
405
|
+
edges.push({
|
|
406
|
+
from,
|
|
407
|
+
to: rule.to,
|
|
408
|
+
label: `contains "${truncate(rule.contains, 24)}"`,
|
|
409
|
+
conditional: true,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return { nodes, edges };
|
|
415
|
+
}
|
|
416
|
+
function pipelineCanvas(ir, warnings) {
|
|
417
|
+
const used = new Set();
|
|
418
|
+
if (ir.indexing.documents.length === 0) {
|
|
419
|
+
warnings.push("pipeline declares no indexing.documents — nothing to index");
|
|
420
|
+
}
|
|
421
|
+
const nodes = [];
|
|
422
|
+
const edges = [];
|
|
423
|
+
for (const doc of ir.indexing.documents) {
|
|
424
|
+
const id = claimId(used, `doc:${doc.id}`);
|
|
425
|
+
nodes.push({ id, label: doc.id, kind: "doc", mini: emptyMini() });
|
|
426
|
+
edges.push({ from: id, to: "index" });
|
|
427
|
+
}
|
|
428
|
+
nodes.push({
|
|
429
|
+
id: claimId(used, "index"),
|
|
430
|
+
label: `index (${ir.indexing.chunkStrategy})`,
|
|
431
|
+
kind: "node",
|
|
432
|
+
mini: emptyMini(),
|
|
433
|
+
});
|
|
434
|
+
nodes.push({
|
|
435
|
+
id: claimId(used, "agent"),
|
|
436
|
+
label: "chat agent",
|
|
437
|
+
kind: "node",
|
|
438
|
+
mini: miniSegments({
|
|
439
|
+
model: ir.agent.model,
|
|
440
|
+
...(ir.agent.modelPool !== undefined ? { modelPool: ir.agent.modelPool } : {}),
|
|
441
|
+
}),
|
|
442
|
+
});
|
|
443
|
+
edges.push({
|
|
444
|
+
from: "index",
|
|
445
|
+
to: "agent",
|
|
446
|
+
label: `retrieve (k=${ir.retrieve.defaultK}, ${ir.retrieve.vectorBackend})`,
|
|
447
|
+
});
|
|
448
|
+
return { nodes, edges };
|
|
449
|
+
}
|
|
450
|
+
function researchCanvas(ir) {
|
|
451
|
+
const agentMini = () => miniSegments({
|
|
452
|
+
model: ir.agent.model,
|
|
453
|
+
...(ir.agent.modelPool !== undefined ? { modelPool: ir.agent.modelPool } : {}),
|
|
454
|
+
tools: ir.tools,
|
|
455
|
+
});
|
|
456
|
+
const nodes = [
|
|
457
|
+
{ id: "goal", label: `goal: ${truncate(ir.goal, 40)}`, kind: "node", mini: agentMini() },
|
|
458
|
+
];
|
|
459
|
+
const edges = [];
|
|
460
|
+
for (let i = 1; i <= ir.branchingFactor; i += 1) {
|
|
461
|
+
nodes.push({ id: `branch-${i}`, label: `branch ${i}`, kind: "node", mini: agentMini() });
|
|
462
|
+
edges.push({ from: "goal", to: `branch-${i}` });
|
|
463
|
+
edges.push({ from: `branch-${i}`, to: "report" });
|
|
464
|
+
}
|
|
465
|
+
nodes.push({ id: "report", label: "report", kind: "doc", mini: emptyMini() });
|
|
466
|
+
return { nodes, edges };
|
|
467
|
+
}
|
|
468
|
+
function batchCanvas(ir) {
|
|
469
|
+
const nodes = [
|
|
470
|
+
{ id: "queue", label: `queue (${ir.queue.adapter})`, kind: "node", mini: emptyMini() },
|
|
471
|
+
{
|
|
472
|
+
id: "agent",
|
|
473
|
+
label: `worker × ${ir.concurrency}`,
|
|
474
|
+
kind: "node",
|
|
475
|
+
mini: miniSegments({
|
|
476
|
+
model: ir.agent.model,
|
|
477
|
+
...(ir.agent.modelPool !== undefined ? { modelPool: ir.agent.modelPool } : {}),
|
|
478
|
+
tools: ir.tools,
|
|
479
|
+
}),
|
|
480
|
+
},
|
|
481
|
+
];
|
|
482
|
+
const edges = [
|
|
483
|
+
{ from: "queue", to: "agent", label: "jobs" },
|
|
484
|
+
{ from: "agent", to: "queue", label: `retries (≤ ${ir.queue.maxRetries})`, conditional: true },
|
|
485
|
+
];
|
|
486
|
+
return { nodes, edges };
|
|
487
|
+
}
|
|
488
|
+
// --- per-shape ring views ---------------------------------------------------------
|
|
489
|
+
function cliRingView(ir) {
|
|
490
|
+
return {
|
|
491
|
+
model: ir.agent.model,
|
|
492
|
+
toolsKey: "tools",
|
|
493
|
+
tools: ir.tools,
|
|
494
|
+
mcpServers: ir.mcp_servers,
|
|
495
|
+
subAgents: ir.subAgents,
|
|
496
|
+
...(ir.agent.thinking !== undefined ? { thinking: ir.agent.thinking } : {}),
|
|
497
|
+
...(ir.agent.modelTiers !== undefined ? { modelTiers: ir.agent.modelTiers } : {}),
|
|
498
|
+
...(ir.agent.modelPool !== undefined ? { modelPool: ir.agent.modelPool } : {}),
|
|
499
|
+
...(ir.evaluation !== undefined ? { evaluation: ir.evaluation } : {}),
|
|
500
|
+
...(ir.learning !== undefined ? { learning: ir.learning } : {}),
|
|
501
|
+
...(ir.security !== undefined ? { security: ir.security } : {}),
|
|
502
|
+
...(ir.memory !== undefined ? { memory: ir.memory } : {}),
|
|
503
|
+
...(ir.continuity !== undefined ? { continuity: ir.continuity } : {}),
|
|
504
|
+
continuityDefaultOn: true,
|
|
505
|
+
thredz: ir.thredz !== undefined,
|
|
506
|
+
compaction: ir.compaction,
|
|
507
|
+
...(ir.budget !== undefined ? { budget: ir.budget } : {}),
|
|
508
|
+
...(ir.limits !== undefined ? { limits: ir.limits } : {}),
|
|
509
|
+
permissions: ir.permissions,
|
|
510
|
+
...(ir.hooks !== undefined ? { hooks: ir.hooks } : {}),
|
|
511
|
+
transactionPolicy: ir.transactionPolicy !== undefined,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function channelRingView(ir) {
|
|
515
|
+
const channelNames = Object.keys(ir.channels).filter((name) => ir.channels[name] !== undefined);
|
|
516
|
+
return {
|
|
517
|
+
model: ir.agent.model,
|
|
518
|
+
toolsKey: "agent.tools",
|
|
519
|
+
tools: ir.tools,
|
|
520
|
+
mcpServers: ir.mcp_servers,
|
|
521
|
+
subAgents: ir.subAgents,
|
|
522
|
+
channels: channelNames,
|
|
523
|
+
heartbeat: ir.heartbeat !== undefined,
|
|
524
|
+
...(ir.agent.thinking !== undefined ? { thinking: ir.agent.thinking } : {}),
|
|
525
|
+
...(ir.agent.modelTiers !== undefined ? { modelTiers: ir.agent.modelTiers } : {}),
|
|
526
|
+
...(ir.agent.modelPool !== undefined ? { modelPool: ir.agent.modelPool } : {}),
|
|
527
|
+
...(ir.evaluation !== undefined ? { evaluation: ir.evaluation } : {}),
|
|
528
|
+
...(ir.learning !== undefined ? { learning: ir.learning } : {}),
|
|
529
|
+
...(ir.memory !== undefined ? { memory: ir.memory } : {}),
|
|
530
|
+
...(ir.continuity !== undefined ? { continuity: ir.continuity } : {}),
|
|
531
|
+
continuityDefaultOn: true,
|
|
532
|
+
thredz: ir.thredz !== undefined,
|
|
533
|
+
compaction: ir.compaction,
|
|
534
|
+
...(ir.budget !== undefined ? { budget: ir.budget } : {}),
|
|
535
|
+
...(ir.limits !== undefined ? { limits: ir.limits } : {}),
|
|
536
|
+
permissions: ir.permissions,
|
|
537
|
+
...(ir.hooks !== undefined ? { hooks: ir.hooks } : {}),
|
|
538
|
+
transactionPolicy: ir.transactionPolicy !== undefined,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function managedRingView(ir) {
|
|
542
|
+
return {
|
|
543
|
+
model: ir.agent.model,
|
|
544
|
+
...(ir.agent.thinking !== undefined ? { thinking: ir.agent.thinking } : {}),
|
|
545
|
+
...(ir.agent.modelTiers !== undefined ? { modelTiers: ir.agent.modelTiers } : {}),
|
|
546
|
+
...(ir.agent.modelPool !== undefined ? { modelPool: ir.agent.modelPool } : {}),
|
|
547
|
+
...(ir.evaluation !== undefined ? { evaluation: ir.evaluation } : {}),
|
|
548
|
+
...(ir.learning !== undefined ? { learning: ir.learning } : {}),
|
|
549
|
+
...(ir.memory !== undefined ? { memory: ir.memory } : {}),
|
|
550
|
+
...(ir.continuity !== undefined ? { continuity: ir.continuity } : {}),
|
|
551
|
+
continuityDefaultOn: true,
|
|
552
|
+
thredz: ir.thredz !== undefined,
|
|
553
|
+
compaction: ir.compaction,
|
|
554
|
+
...(ir.budget !== undefined ? { budget: ir.budget } : {}),
|
|
555
|
+
...(ir.limits !== undefined ? { limits: ir.limits } : {}),
|
|
556
|
+
permissions: ir.permissions,
|
|
557
|
+
...(ir.hooks !== undefined ? { hooks: ir.hooks } : {}),
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
/** Generic fallback view for the shapes without a dedicated projection. */
|
|
561
|
+
function fallbackRingView(ir) {
|
|
562
|
+
const bag = ir;
|
|
563
|
+
const topTools = bag.tools ?? [];
|
|
564
|
+
const agentTools = bag.agent?.tools ?? [];
|
|
565
|
+
const useAgentTools = topTools.length === 0 && agentTools.length > 0;
|
|
566
|
+
return {
|
|
567
|
+
model: bag.agent?.model ?? "",
|
|
568
|
+
toolsKey: useAgentTools ? "agent.tools" : "tools",
|
|
569
|
+
tools: useAgentTools ? agentTools : topTools,
|
|
570
|
+
...(bag.mcp_servers !== undefined ? { mcpServers: bag.mcp_servers } : {}),
|
|
571
|
+
...(bag.compaction !== undefined ? { compaction: bag.compaction } : {}),
|
|
572
|
+
...(bag.permissions !== undefined ? { permissions: bag.permissions } : {}),
|
|
573
|
+
...(bag.continuity !== undefined ? { continuity: bag.continuity } : {}),
|
|
574
|
+
...(bag.budget !== undefined ? { budget: bag.budget } : {}),
|
|
575
|
+
...(bag.limits !== undefined ? { limits: bag.limits } : {}),
|
|
576
|
+
...(bag.hooks !== undefined ? { hooks: bag.hooks } : {}),
|
|
577
|
+
transactionPolicy: bag.transactionPolicy !== undefined,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
// --- projectLoop -------------------------------------------------------------------
|
|
581
|
+
function ringProjection(target, view, warnings) {
|
|
582
|
+
const segments = ringSegmentsFromView(view);
|
|
583
|
+
// Guardrails-first: the defaults-only Stop warning applies to the true
|
|
584
|
+
// ring families (their loop really does run to the iteration cap).
|
|
585
|
+
// Fallback targets have their own boundaries (call length, page budget,
|
|
586
|
+
// dataset size), so the 500-iteration claim would be dishonest there.
|
|
587
|
+
if (RING_TARGETS.includes(target)) {
|
|
588
|
+
const stop = segments.find((s) => s.id === "stop");
|
|
589
|
+
if (stop !== undefined && !stop.active)
|
|
590
|
+
warnings.push(NO_BUDGET_WARNING);
|
|
591
|
+
}
|
|
592
|
+
return { kind: "ring", target, ring: { segments }, warnings };
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Project a lowered IR into its canonical loop view. Total over the IrNode
|
|
596
|
+
* union and never throws: every variant maps to a ring or canvas, and the
|
|
597
|
+
* shapes without a dedicated projection fall back to the generic ring with
|
|
598
|
+
* an honest warning.
|
|
599
|
+
*/
|
|
600
|
+
export function projectLoop(ir) {
|
|
601
|
+
switch (ir.target) {
|
|
602
|
+
case "cli":
|
|
603
|
+
return ringProjection("cli", cliRingView(ir), []);
|
|
604
|
+
case "channel":
|
|
605
|
+
return ringProjection("channel", channelRingView(ir), []);
|
|
606
|
+
case "managed":
|
|
607
|
+
return ringProjection("managed", managedRingView(ir), []);
|
|
608
|
+
case "workflow": {
|
|
609
|
+
const warnings = [];
|
|
610
|
+
const canvas = workflowCanvas(ir, warnings);
|
|
611
|
+
return { kind: "canvas", target: "workflow", canvas, warnings };
|
|
612
|
+
}
|
|
613
|
+
case "graph": {
|
|
614
|
+
const warnings = [];
|
|
615
|
+
const canvas = graphCanvas(ir, warnings);
|
|
616
|
+
return { kind: "canvas", target: "graph", canvas, warnings };
|
|
617
|
+
}
|
|
618
|
+
case "crew": {
|
|
619
|
+
const warnings = [];
|
|
620
|
+
const canvas = crewCanvas(ir, warnings);
|
|
621
|
+
return { kind: "canvas", target: "crew", canvas, warnings };
|
|
622
|
+
}
|
|
623
|
+
case "pipeline": {
|
|
624
|
+
const warnings = [];
|
|
625
|
+
const canvas = pipelineCanvas(ir, warnings);
|
|
626
|
+
return { kind: "canvas", target: "pipeline", canvas, warnings };
|
|
627
|
+
}
|
|
628
|
+
case "research":
|
|
629
|
+
return { kind: "canvas", target: "research", canvas: researchCanvas(ir), warnings: [] };
|
|
630
|
+
case "batch":
|
|
631
|
+
return { kind: "canvas", target: "batch", canvas: batchCanvas(ir), warnings: [] };
|
|
632
|
+
case "voice":
|
|
633
|
+
case "browser":
|
|
634
|
+
case "eval":
|
|
635
|
+
case "onchain":
|
|
636
|
+
case "onchain-game":
|
|
637
|
+
return ringProjection(ir.target, fallbackRingView(ir), [
|
|
638
|
+
`target "${ir.target}" has no dedicated loop projection yet — showing the generic single-agent ring`,
|
|
639
|
+
]);
|
|
640
|
+
}
|
|
641
|
+
}
|