akribes 0.21.17
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/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/dist/client.d.ts +240 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +272 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +196 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +274 -0
- package/dist/errors.js.map +1 -0
- package/dist/execution/index.d.ts +3 -0
- package/dist/execution/index.d.ts.map +1 -0
- package/dist/execution/index.js +3 -0
- package/dist/execution/index.js.map +1 -0
- package/dist/execution/replay.d.ts +37 -0
- package/dist/execution/replay.d.ts.map +1 -0
- package/dist/execution/replay.js +59 -0
- package/dist/execution/replay.js.map +1 -0
- package/dist/execution/steps.d.ts +327 -0
- package/dist/execution/steps.d.ts.map +1 -0
- package/dist/execution/steps.js +1068 -0
- package/dist/execution/steps.js.map +1 -0
- package/dist/http.d.ts +53 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +141 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/runStream.d.ts +176 -0
- package/dist/runStream.d.ts.map +1 -0
- package/dist/runStream.js +408 -0
- package/dist/runStream.js.map +1 -0
- package/dist/sse.d.ts +46 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +218 -0
- package/dist/sse.js.map +1 -0
- package/dist/sub/bench.d.ts +182 -0
- package/dist/sub/bench.d.ts.map +1 -0
- package/dist/sub/bench.js +420 -0
- package/dist/sub/bench.js.map +1 -0
- package/dist/sub/channels.d.ts +22 -0
- package/dist/sub/channels.d.ts.map +1 -0
- package/dist/sub/channels.js +32 -0
- package/dist/sub/channels.js.map +1 -0
- package/dist/sub/clients.d.ts +79 -0
- package/dist/sub/clients.d.ts.map +1 -0
- package/dist/sub/clients.js +190 -0
- package/dist/sub/clients.js.map +1 -0
- package/dist/sub/documents.d.ts +113 -0
- package/dist/sub/documents.d.ts.map +1 -0
- package/dist/sub/documents.js +329 -0
- package/dist/sub/documents.js.map +1 -0
- package/dist/sub/evals.d.ts +71 -0
- package/dist/sub/evals.d.ts.map +1 -0
- package/dist/sub/evals.js +86 -0
- package/dist/sub/evals.js.map +1 -0
- package/dist/sub/events.d.ts +65 -0
- package/dist/sub/events.d.ts.map +1 -0
- package/dist/sub/events.js +154 -0
- package/dist/sub/events.js.map +1 -0
- package/dist/sub/executions.d.ts +255 -0
- package/dist/sub/executions.d.ts.map +1 -0
- package/dist/sub/executions.js +322 -0
- package/dist/sub/executions.js.map +1 -0
- package/dist/sub/mcp.d.ts +51 -0
- package/dist/sub/mcp.d.ts.map +1 -0
- package/dist/sub/mcp.js +42 -0
- package/dist/sub/mcp.js.map +1 -0
- package/dist/sub/projects.d.ts +73 -0
- package/dist/sub/projects.d.ts.map +1 -0
- package/dist/sub/projects.js +101 -0
- package/dist/sub/projects.js.map +1 -0
- package/dist/sub/scripts.d.ts +58 -0
- package/dist/sub/scripts.d.ts.map +1 -0
- package/dist/sub/scripts.js +82 -0
- package/dist/sub/scripts.js.map +1 -0
- package/dist/sub/tokens.d.ts +126 -0
- package/dist/sub/tokens.d.ts.map +1 -0
- package/dist/sub/tokens.js +105 -0
- package/dist/sub/tokens.js.map +1 -0
- package/dist/sub/versions.d.ts +29 -0
- package/dist/sub/versions.d.ts.map +1 -0
- package/dist/sub/versions.js +52 -0
- package/dist/sub/versions.js.map +1 -0
- package/dist/tokenSafety.d.ts +15 -0
- package/dist/tokenSafety.d.ts.map +1 -0
- package/dist/tokenSafety.js +24 -0
- package/dist/tokenSafety.js.map +1 -0
- package/dist/types.d.ts +1147 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +132 -0
- package/dist/types.js.map +1 -0
- package/dist/workflowEvents.d.ts +297 -0
- package/dist/workflowEvents.d.ts.map +1 -0
- package/dist/workflowEvents.js +612 -0
- package/dist/workflowEvents.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,1068 @@
|
|
|
1
|
+
import { normalizeSuspendTrigger } from '../workflowEvents';
|
|
2
|
+
function parseSubScriptEnvelope(payload) {
|
|
3
|
+
if (!payload || typeof payload !== 'object')
|
|
4
|
+
return null;
|
|
5
|
+
const p = payload;
|
|
6
|
+
const scriptName = typeof p.script_name === 'string' ? p.script_name : '';
|
|
7
|
+
const parentTask = typeof p.parent_task === 'string' ? p.parent_task : '';
|
|
8
|
+
const frame = { scriptName, parentTask };
|
|
9
|
+
const child = p.child;
|
|
10
|
+
if (!child || typeof child !== 'object') {
|
|
11
|
+
return {
|
|
12
|
+
frame,
|
|
13
|
+
kind: 'leaf',
|
|
14
|
+
effects: { childIsTerminal: false, childIsError: false },
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const c = child;
|
|
18
|
+
const cType = typeof c.type === 'string' ? c.type : '';
|
|
19
|
+
const cPayload = c.payload;
|
|
20
|
+
// Nested sub-script: defer to the caller for one more level of unwrapping.
|
|
21
|
+
if (cType === 'SubScript') {
|
|
22
|
+
return { frame, kind: 'nested', innerPayload: cPayload };
|
|
23
|
+
}
|
|
24
|
+
switch (cType) {
|
|
25
|
+
case 'StateUpdate': {
|
|
26
|
+
// Wire shape: payload is `[name, value]`.
|
|
27
|
+
if (Array.isArray(cPayload) && typeof cPayload[0] === 'string') {
|
|
28
|
+
return {
|
|
29
|
+
frame,
|
|
30
|
+
kind: 'leaf',
|
|
31
|
+
effects: {
|
|
32
|
+
maybeInput: { name: cPayload[0], value: cPayload[1] },
|
|
33
|
+
childIsTerminal: false,
|
|
34
|
+
childIsError: false,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return { frame, kind: 'leaf', effects: { childIsTerminal: false, childIsError: false } };
|
|
39
|
+
}
|
|
40
|
+
case 'TaskEnd': {
|
|
41
|
+
const obj = cPayload && typeof cPayload === 'object' && !Array.isArray(cPayload)
|
|
42
|
+
? cPayload
|
|
43
|
+
: null;
|
|
44
|
+
const usage = obj && typeof obj.usage === 'object' && obj.usage !== null
|
|
45
|
+
? obj.usage
|
|
46
|
+
: undefined;
|
|
47
|
+
const taskName = obj && typeof obj.task === 'string' ? obj.task : '';
|
|
48
|
+
const attempt = obj && typeof obj.attempt === 'number' ? obj.attempt : 1;
|
|
49
|
+
const duration = obj && typeof obj.duration === 'object' && obj.duration !== null
|
|
50
|
+
? obj.duration
|
|
51
|
+
: null;
|
|
52
|
+
const durationMs = duration
|
|
53
|
+
? Number(duration.secs ?? 0) * 1000 + Number(duration.nanos ?? 0) / 1_000_000
|
|
54
|
+
: 0;
|
|
55
|
+
const tokens = parseTokens(usage);
|
|
56
|
+
// #871: per-task USD if the engine attached it. The current server
|
|
57
|
+
// doesn't include `cost_usd` on TaskEnd, but the reducer reads it
|
|
58
|
+
// opportunistically so the field surfaces in the UI as soon as the
|
|
59
|
+
// server side starts emitting it.
|
|
60
|
+
const costUsdRaw = obj && typeof obj.cost_usd === 'number' ? obj.cost_usd : undefined;
|
|
61
|
+
const summary = {
|
|
62
|
+
kind: 'task_end',
|
|
63
|
+
taskName,
|
|
64
|
+
durationMs,
|
|
65
|
+
attempt,
|
|
66
|
+
tokens,
|
|
67
|
+
...(costUsdRaw !== undefined ? { costUsd: costUsdRaw } : {}),
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
frame,
|
|
71
|
+
kind: 'leaf',
|
|
72
|
+
effects: {
|
|
73
|
+
maybeUsage: usage,
|
|
74
|
+
maybeTaskSummary: summary,
|
|
75
|
+
childIsTerminal: false,
|
|
76
|
+
childIsError: false,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
case 'AgentOutput': {
|
|
81
|
+
// Per-task streaming text inside a sub-script. Wire shape mirrors the
|
|
82
|
+
// top-level `AgentOutput` arm: `{ task_name, agent_name, task_id,
|
|
83
|
+
// schema_type, chunk }`. We carry just `task_name` + `chunk` — the
|
|
84
|
+
// task summary keys by name, and the chunk is what the operator wants
|
|
85
|
+
// to read in the drill-in.
|
|
86
|
+
const obj = cPayload && typeof cPayload === 'object' && !Array.isArray(cPayload)
|
|
87
|
+
? cPayload
|
|
88
|
+
: null;
|
|
89
|
+
const taskName = obj && typeof obj.task_name === 'string' ? obj.task_name : '';
|
|
90
|
+
const chunk = obj && typeof obj.chunk === 'string' ? obj.chunk : '';
|
|
91
|
+
if (!taskName || !chunk) {
|
|
92
|
+
return { frame, kind: 'leaf', effects: { childIsTerminal: false, childIsError: false } };
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
frame,
|
|
96
|
+
kind: 'leaf',
|
|
97
|
+
effects: {
|
|
98
|
+
maybeAgentOutput: { taskName, chunk },
|
|
99
|
+
childIsTerminal: false,
|
|
100
|
+
childIsError: false,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
case 'ValidationFailure': {
|
|
105
|
+
const obj = cPayload && typeof cPayload === 'object' && !Array.isArray(cPayload)
|
|
106
|
+
? cPayload
|
|
107
|
+
: null;
|
|
108
|
+
const taskName = obj && typeof obj.task_name === 'string' ? obj.task_name : '';
|
|
109
|
+
const attempt = obj && typeof obj.attempt === 'number' ? obj.attempt : 1;
|
|
110
|
+
return {
|
|
111
|
+
frame,
|
|
112
|
+
kind: 'leaf',
|
|
113
|
+
effects: {
|
|
114
|
+
maybeTaskSummary: { kind: 'validation_failure', taskName, attempt },
|
|
115
|
+
childIsTerminal: false,
|
|
116
|
+
childIsError: false,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
case 'WorkflowEnd': {
|
|
121
|
+
// Issue #1173: WorkflowEnd payload may be either the new
|
|
122
|
+
// `{ value, total_input_tokens, ... }` struct or the legacy bare
|
|
123
|
+
// output value. Recover the output via the same disambiguator
|
|
124
|
+
// used in `workflowEvents.ts`'s parseWorkflowEndPayload (presence
|
|
125
|
+
// of `value` + any `total_*` key signals new shape).
|
|
126
|
+
let output = cPayload;
|
|
127
|
+
if (cPayload && typeof cPayload === 'object' && !Array.isArray(cPayload)) {
|
|
128
|
+
const o = cPayload;
|
|
129
|
+
const aggKeys = [
|
|
130
|
+
'total_input_tokens',
|
|
131
|
+
'total_output_tokens',
|
|
132
|
+
'total_cached_input_tokens',
|
|
133
|
+
'total_thinking_tokens',
|
|
134
|
+
'total_tool_tokens',
|
|
135
|
+
'total_cost_usd',
|
|
136
|
+
'task_count',
|
|
137
|
+
];
|
|
138
|
+
if ('value' in o && aggKeys.some((k) => k in o)) {
|
|
139
|
+
output = o.value;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
frame,
|
|
144
|
+
kind: 'leaf',
|
|
145
|
+
effects: {
|
|
146
|
+
maybeOutput: output,
|
|
147
|
+
childIsTerminal: true,
|
|
148
|
+
childIsError: false,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
case 'Error': {
|
|
153
|
+
return {
|
|
154
|
+
frame,
|
|
155
|
+
kind: 'leaf',
|
|
156
|
+
effects: { childIsTerminal: true, childIsError: true },
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
default:
|
|
160
|
+
return { frame, kind: 'leaf', effects: { childIsTerminal: false, childIsError: false } };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Unwrap a chain of nested `SubScript` envelopes into:
|
|
165
|
+
* * the ordered stack of frames (`[outermost, …, innermost]`)
|
|
166
|
+
* * the leaf-level effects to apply at the innermost frame
|
|
167
|
+
*
|
|
168
|
+
* Returns `null` only when the outermost payload itself is malformed (which
|
|
169
|
+
* the reducer treats as a no-op, mirroring the v1 behavior).
|
|
170
|
+
*/
|
|
171
|
+
function unwrapSubScriptChain(payload) {
|
|
172
|
+
// Issue #993: the new flat wire shape carries the ancestor chain via
|
|
173
|
+
// `parent_path` on the OUTER envelope (frames ordered outermost →
|
|
174
|
+
// immediate parent). Read those first so the chain reads correctly
|
|
175
|
+
// even when the engine emitted a depth-1 (`parent_path` empty) +
|
|
176
|
+
// flat-child envelope. Pre-#993 emissions had no `parent_path` and
|
|
177
|
+
// nested every level via `child`; we still walk that case for
|
|
178
|
+
// back-compat against archived event logs.
|
|
179
|
+
const frames = [];
|
|
180
|
+
if (payload && typeof payload === 'object') {
|
|
181
|
+
const outer = payload;
|
|
182
|
+
if (Array.isArray(outer.parent_path)) {
|
|
183
|
+
for (const f of outer.parent_path) {
|
|
184
|
+
if (!f || typeof f !== 'object')
|
|
185
|
+
continue;
|
|
186
|
+
const ff = f;
|
|
187
|
+
frames.push({
|
|
188
|
+
scriptName: typeof ff.script_name === 'string' ? ff.script_name : '',
|
|
189
|
+
parentTask: typeof ff.parent_task === 'string' ? ff.parent_task : '',
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
let current = payload;
|
|
195
|
+
// Hard upper bound to keep the loop honest if a future engine ever produces
|
|
196
|
+
// a cycle (it can't today — `Box<EngineEvent>` is a tree, not a graph).
|
|
197
|
+
for (let depth = 0; depth < 64; depth += 1) {
|
|
198
|
+
const parsed = parseSubScriptEnvelope(current);
|
|
199
|
+
if (!parsed)
|
|
200
|
+
return null;
|
|
201
|
+
frames.push(parsed.frame);
|
|
202
|
+
if (parsed.kind === 'leaf') {
|
|
203
|
+
return { frames, effects: parsed.effects };
|
|
204
|
+
}
|
|
205
|
+
current = parsed.innerPayload;
|
|
206
|
+
}
|
|
207
|
+
// Defensive fallback for an absurdly deep chain — render as a single leaf
|
|
208
|
+
// with no effects rather than crashing the reducer.
|
|
209
|
+
return { frames, effects: { childIsTerminal: false, childIsError: false } };
|
|
210
|
+
}
|
|
211
|
+
function aggregateSubScriptTokens(prev, raw) {
|
|
212
|
+
const input = Number(raw.input_tokens ?? 0);
|
|
213
|
+
const output = Number(raw.output_tokens ?? 0);
|
|
214
|
+
const cachedInput = Number(raw.cached_input_tokens ?? 0);
|
|
215
|
+
const model = typeof raw.model === 'string' ? raw.model : '';
|
|
216
|
+
// Skip empty-usage events (mock provider emits `{}` sometimes).
|
|
217
|
+
if (!input && !output && !cachedInput && !model)
|
|
218
|
+
return prev;
|
|
219
|
+
const base = prev ?? { input: 0, output: 0, cachedInput: 0, models: [] };
|
|
220
|
+
const models = model && !base.models.includes(model) ? [...base.models, model] : base.models;
|
|
221
|
+
return {
|
|
222
|
+
input: base.input + input,
|
|
223
|
+
output: base.output + output,
|
|
224
|
+
cachedInput: base.cachedInput + cachedInput,
|
|
225
|
+
models,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Walk a list of nested sub-script steps and roll their token totals into a
|
|
230
|
+
* single aggregate. Used when a parent sub-script's totals need to be
|
|
231
|
+
* recomputed after a descendant updated.
|
|
232
|
+
*/
|
|
233
|
+
function rollupTokensFromChildren(children) {
|
|
234
|
+
let agg = undefined;
|
|
235
|
+
for (const c of children) {
|
|
236
|
+
const t = c.subScript?.subScriptTokens;
|
|
237
|
+
if (!t)
|
|
238
|
+
continue;
|
|
239
|
+
const base = agg ?? { input: 0, output: 0, cachedInput: 0, models: [] };
|
|
240
|
+
const models = [...base.models];
|
|
241
|
+
for (const m of t.models)
|
|
242
|
+
if (!models.includes(m))
|
|
243
|
+
models.push(m);
|
|
244
|
+
agg = {
|
|
245
|
+
input: base.input + t.input,
|
|
246
|
+
output: base.output + t.output,
|
|
247
|
+
cachedInput: base.cachedInput + t.cachedInput,
|
|
248
|
+
models,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return agg;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Sum the `nestedTaskCount` across a list of children. Used to keep an
|
|
255
|
+
* ancestor's count in sync as descendants accumulate `TaskEnd` envelopes.
|
|
256
|
+
*/
|
|
257
|
+
function rollupTaskCountFromChildren(children) {
|
|
258
|
+
let n = 0;
|
|
259
|
+
for (const c of children)
|
|
260
|
+
n += c.subScript?.nestedTaskCount ?? 0;
|
|
261
|
+
return n;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Combine this level's `selfTokens` (tokens from `TaskEnd`s wrapped DIRECTLY
|
|
265
|
+
* at this frame, never from descendants) with the recursive rollup of every
|
|
266
|
+
* child's `subScriptTokens`. The result is the level's recursive
|
|
267
|
+
* `subScriptTokens`. Returns `undefined` only when both inputs are
|
|
268
|
+
* absent/empty so the UI can preserve its "no tokens yet" state.
|
|
269
|
+
*
|
|
270
|
+
* Invariant the SubScript reducer relies on:
|
|
271
|
+
*
|
|
272
|
+
* subScriptTokens = selfTokens + Σ children[i].subScriptTokens
|
|
273
|
+
*
|
|
274
|
+
* Maintaining this invariant explicitly (rather than the previous
|
|
275
|
+
* `prevTokens - prevChildrenRollup` subtraction) avoids the floating-point
|
|
276
|
+
* + model-list drift the old recompute path was vulnerable to when a level
|
|
277
|
+
* accumulated multiple direct `TaskEnd`s in between child updates.
|
|
278
|
+
*/
|
|
279
|
+
function combineSelfAndChildrenTokens(self, children) {
|
|
280
|
+
const childRoll = rollupTokensFromChildren(children ?? []);
|
|
281
|
+
if (!self && !childRoll)
|
|
282
|
+
return undefined;
|
|
283
|
+
const merged = {
|
|
284
|
+
input: (self?.input ?? 0) + (childRoll?.input ?? 0),
|
|
285
|
+
output: (self?.output ?? 0) + (childRoll?.output ?? 0),
|
|
286
|
+
cachedInput: (self?.cachedInput ?? 0) + (childRoll?.cachedInput ?? 0),
|
|
287
|
+
models: [],
|
|
288
|
+
};
|
|
289
|
+
for (const m of self?.models ?? [])
|
|
290
|
+
if (!merged.models.includes(m))
|
|
291
|
+
merged.models.push(m);
|
|
292
|
+
for (const m of childRoll?.models ?? [])
|
|
293
|
+
if (!merged.models.includes(m))
|
|
294
|
+
merged.models.push(m);
|
|
295
|
+
if (!merged.input && !merged.output && !merged.cachedInput && merged.models.length === 0) {
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
return merged;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* If the summary is a `task_end` and the streaming buffer holds matching
|
|
302
|
+
* text, attach it as `streamingOutput`. The reducer drops the buffer entry
|
|
303
|
+
* after this call so it doesn't keep accumulating across the sub-script's
|
|
304
|
+
* lifetime. Returns `undefined` when there is no summary (caller no-ops).
|
|
305
|
+
*/
|
|
306
|
+
function attachStreamingToSummary(summary, streamingByTask) {
|
|
307
|
+
if (!summary)
|
|
308
|
+
return undefined;
|
|
309
|
+
if (summary.kind !== 'task_end')
|
|
310
|
+
return summary;
|
|
311
|
+
const buffered = streamingByTask?.[summary.taskName];
|
|
312
|
+
if (!buffered)
|
|
313
|
+
return summary;
|
|
314
|
+
return { ...summary, streamingOutput: buffered };
|
|
315
|
+
}
|
|
316
|
+
function parseTokens(raw) {
|
|
317
|
+
if (!raw || typeof raw !== 'object')
|
|
318
|
+
return undefined;
|
|
319
|
+
const u = raw;
|
|
320
|
+
const input = Number(u.input_tokens ?? 0);
|
|
321
|
+
const output = Number(u.output_tokens ?? 0);
|
|
322
|
+
const cachedInput = Number(u.cached_input_tokens ?? 0);
|
|
323
|
+
if (!input && !output && !cachedInput)
|
|
324
|
+
return undefined;
|
|
325
|
+
return {
|
|
326
|
+
input,
|
|
327
|
+
output,
|
|
328
|
+
cachedInput,
|
|
329
|
+
model: typeof u.model === 'string' ? u.model : '',
|
|
330
|
+
provider: typeof u.provider === 'string' ? u.provider : '',
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Pure reducer: takes current steps + a hub event, returns new steps + side
|
|
335
|
+
* effects. This is the core event-handling logic shared between Studio's
|
|
336
|
+
* live panel and the docs runner.
|
|
337
|
+
*/
|
|
338
|
+
export function reduceExecutionEvent(prev, hubEvt, activeLineRef, activeNodeRef) {
|
|
339
|
+
if (hubEvt.type !== 'Execution')
|
|
340
|
+
return { steps: prev, effects: {} };
|
|
341
|
+
const evt = hubEvt.payload.event;
|
|
342
|
+
const evName = evt.type;
|
|
343
|
+
const evPayload = evt.payload;
|
|
344
|
+
const timestamp = Date.now();
|
|
345
|
+
const id = `${evName}-${timestamp}-${Math.random()}`;
|
|
346
|
+
const effects = {};
|
|
347
|
+
// Server-attached fields (Workstream 04 §B). Optional — older servers don't
|
|
348
|
+
// stamp these, so the reducer treats both as undefined-by-default. The UI
|
|
349
|
+
// displays the seq badge only when present.
|
|
350
|
+
const wirePayload = hubEvt.payload;
|
|
351
|
+
const wireSeq = typeof wirePayload.seq === 'number' ? wirePayload.seq : undefined;
|
|
352
|
+
const wireServerTs = typeof wirePayload.at === 'string' ? Date.parse(wirePayload.at) : undefined;
|
|
353
|
+
let steps;
|
|
354
|
+
switch (evName) {
|
|
355
|
+
case 'NodeStart': {
|
|
356
|
+
const [nodeId, span] = evPayload;
|
|
357
|
+
activeLineRef.current = span.line;
|
|
358
|
+
activeNodeRef.current = nodeId;
|
|
359
|
+
effects.setActiveLine = span.line;
|
|
360
|
+
steps = [...prev, { id, line: span.line, type: 'execution', content: `Executing Node ${nodeId}`, status: 'running', timestamp, nodeId, visibility: 'hidden', seq: wireSeq, serverTs: wireServerTs }];
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
case 'TaskPrompt': {
|
|
364
|
+
const [name, prompt] = evPayload;
|
|
365
|
+
// Set `taskName` on the chat step (Workstream 04 \u00a7A.2): without it, the
|
|
366
|
+
// later `TaskEnd` merge \u2014 which keys off `s.taskName === name` \u2014 appends
|
|
367
|
+
// a second step instead of folding into the streaming chat row.
|
|
368
|
+
steps = [...prev, { id, line: activeLineRef.current || 0, type: 'chat', agent: name, taskName: name, content: 'Generating response\u2026', prompt, timestamp, nodeId: activeNodeRef.current ?? undefined, visibility: 'hidden', seq: wireSeq, serverTs: wireServerTs }];
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
case 'AgentOutput': {
|
|
372
|
+
const { task_name, agent_name, task_id, schema_type, chunk } = evPayload;
|
|
373
|
+
// First try the per-`task_id` merge (used for repeated chunks once the
|
|
374
|
+
// chat step has captured a task_id).
|
|
375
|
+
const byTaskId = prev.findIndex(s => s.type === 'chat' && s.taskId === task_id);
|
|
376
|
+
// Fall back to the chat step opened by `TaskPrompt` for this task name
|
|
377
|
+
// on the same active node \u2014 that's the row we want to fill in. The
|
|
378
|
+
// TaskPrompt arm sets taskName but no taskId yet.
|
|
379
|
+
const byTaskName = byTaskId === -1
|
|
380
|
+
? prev.findIndex(s => s.type === 'chat'
|
|
381
|
+
&& s.taskName === task_name
|
|
382
|
+
&& !s.taskId
|
|
383
|
+
&& s.nodeId === activeNodeRef.current)
|
|
384
|
+
: -1;
|
|
385
|
+
const idx = byTaskId !== -1 ? byTaskId : byTaskName;
|
|
386
|
+
if (idx !== -1) {
|
|
387
|
+
const newSteps = [...prev];
|
|
388
|
+
const cur = newSteps[idx];
|
|
389
|
+
newSteps[idx] = {
|
|
390
|
+
...cur,
|
|
391
|
+
// The TaskPrompt arm seeds with the placeholder "Generating response\u2026";
|
|
392
|
+
// overwrite that on the first chunk instead of concatenating to it.
|
|
393
|
+
content: (cur.content === 'Generating response\u2026' ? '' : cur.content) + chunk,
|
|
394
|
+
taskId: task_id, // absorb so future AgentOutput chunks hit byTaskId
|
|
395
|
+
schemaType: schema_type ?? cur.schemaType,
|
|
396
|
+
visibility: 'inline',
|
|
397
|
+
seq: wireSeq ?? cur.seq,
|
|
398
|
+
serverTs: wireServerTs ?? cur.serverTs,
|
|
399
|
+
};
|
|
400
|
+
steps = newSteps;
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
steps = [...prev, { id, line: activeLineRef.current || 0, type: 'chat', agent: agent_name || task_name, taskId: task_id, taskName: task_name, schemaType: schema_type, content: chunk, timestamp, nodeId: activeNodeRef.current ?? undefined, visibility: 'inline', seq: wireSeq, serverTs: wireServerTs }];
|
|
404
|
+
}
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
case 'StateUpdate': {
|
|
408
|
+
const [name, value] = evPayload;
|
|
409
|
+
effects.globalEnvUpdates = { [name]: value };
|
|
410
|
+
// Workstream 04 \u00a7A.3: `StateUpdate` events are panel-only \u2014 the
|
|
411
|
+
// Variables segment shows them; the inline Output stream filters them
|
|
412
|
+
// out so users don't see "Variable updated: x" rows next to the chat
|
|
413
|
+
// step that already produced `x`.
|
|
414
|
+
steps = [...prev, { id, line: activeLineRef.current || 0, type: 'variable', content: `Variable updated: ${name}`, variables: { [name]: value }, status: 'success', timestamp, nodeId: activeNodeRef.current ?? undefined, visibility: 'panel-only', seq: wireSeq, serverTs: wireServerTs }];
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
case 'TaskEnd': {
|
|
418
|
+
// TaskEnd payload: { task, on_error_label, value, value_type, duration, attempt, usage }
|
|
419
|
+
const { task: name, value: result, value_type, duration, attempt, usage } = evPayload;
|
|
420
|
+
const durationMs = duration.secs * 1000 + duration.nanos / 1000000;
|
|
421
|
+
const tokens = parseTokens(usage);
|
|
422
|
+
// If we already have a chat step from this task's AgentOutput chunks,
|
|
423
|
+
// merge the structured result into it rather than appending a duplicate.
|
|
424
|
+
// Without this the panel renders the streamed text AND the parsed object
|
|
425
|
+
// as two separate rows with the same timestamp.
|
|
426
|
+
const chatIdx = (() => {
|
|
427
|
+
for (let i = prev.length - 1; i >= 0; i -= 1) {
|
|
428
|
+
const s = prev[i];
|
|
429
|
+
if (s.type === 'chat' && s.taskName === name && s.valueType == null)
|
|
430
|
+
return i;
|
|
431
|
+
}
|
|
432
|
+
return -1;
|
|
433
|
+
})();
|
|
434
|
+
if (chatIdx !== -1) {
|
|
435
|
+
const merged = [...prev];
|
|
436
|
+
merged[chatIdx] = {
|
|
437
|
+
...merged[chatIdx],
|
|
438
|
+
status: 'success',
|
|
439
|
+
variables: { ...(merged[chatIdx].variables ?? {}), result },
|
|
440
|
+
duration: durationMs,
|
|
441
|
+
tokens,
|
|
442
|
+
valueType: value_type ?? null,
|
|
443
|
+
attempt: typeof attempt === 'number' ? attempt : undefined,
|
|
444
|
+
seq: wireSeq ?? merged[chatIdx].seq,
|
|
445
|
+
serverTs: wireServerTs ?? merged[chatIdx].serverTs,
|
|
446
|
+
};
|
|
447
|
+
steps = merged;
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
steps = [...prev, { id, line: activeLineRef.current || 0, type: 'execution', content: `Finished task: ${name}`, status: 'success', variables: { result }, duration: durationMs, timestamp, nodeId: activeNodeRef.current ?? undefined, visibility: 'inline', tokens, valueType: value_type ?? null, attempt: typeof attempt === 'number' ? attempt : undefined, seq: wireSeq, serverTs: wireServerTs }];
|
|
451
|
+
}
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
case 'NodeEnd': {
|
|
455
|
+
// Support object form { node_id, span, target_var, value, duration },
|
|
456
|
+
// 2-tuple [nodeId, duration], and enriched array [nodeId, span, ..., duration].
|
|
457
|
+
let nodeId;
|
|
458
|
+
let durationMs;
|
|
459
|
+
if (typeof evPayload === 'object' && !Array.isArray(evPayload) && 'node_id' in evPayload) {
|
|
460
|
+
nodeId = evPayload.node_id;
|
|
461
|
+
const dur = evPayload.duration;
|
|
462
|
+
durationMs = dur.secs * 1000 + dur.nanos / 1000000;
|
|
463
|
+
}
|
|
464
|
+
else if (Array.isArray(evPayload)) {
|
|
465
|
+
if (evPayload.length === 2) {
|
|
466
|
+
const [nid, duration] = evPayload;
|
|
467
|
+
nodeId = nid;
|
|
468
|
+
durationMs = duration.secs * 1000 + duration.nanos / 1000000;
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
nodeId = evPayload[0];
|
|
472
|
+
const duration = evPayload[evPayload.length - 1];
|
|
473
|
+
durationMs = duration.secs * 1000 + duration.nanos / 1000000;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
steps = prev;
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
steps = prev.map(s => s.nodeId === nodeId && s.content.startsWith('Executing Node') ? { ...s, status: 'success', duration: durationMs, seq: wireSeq ?? s.seq, serverTs: wireServerTs ?? s.serverTs } : s);
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
case 'WorkflowEnd':
|
|
484
|
+
effects.executionFinished = true;
|
|
485
|
+
effects.refreshHistory = true;
|
|
486
|
+
steps = [...prev, { id, line: activeLineRef.current || 0, type: 'execution', content: 'Workflow completed', status: 'success', variables: { final_result: evPayload }, timestamp, nodeId: activeNodeRef.current ?? undefined, visibility: 'inline', seq: wireSeq, serverTs: wireServerTs }];
|
|
487
|
+
break;
|
|
488
|
+
case 'Error': {
|
|
489
|
+
effects.executionFinished = true;
|
|
490
|
+
effects.refreshHistory = true;
|
|
491
|
+
// Error payload can be a bare string (legacy) or the structured
|
|
492
|
+
// envelope `{ message, kind, code, user_message, retry_after_ms,
|
|
493
|
+
// source }`. We forward every field present so the UI doesn't
|
|
494
|
+
// have to re-derive them.
|
|
495
|
+
let message;
|
|
496
|
+
let kind;
|
|
497
|
+
let code;
|
|
498
|
+
let userMessage;
|
|
499
|
+
let retryAfterMs;
|
|
500
|
+
let source;
|
|
501
|
+
if (typeof evPayload === 'string') {
|
|
502
|
+
message = evPayload;
|
|
503
|
+
}
|
|
504
|
+
else if (evPayload && typeof evPayload === 'object') {
|
|
505
|
+
message = typeof evPayload.message === 'string' ? evPayload.message : JSON.stringify(evPayload);
|
|
506
|
+
kind = typeof evPayload.kind === 'string' ? evPayload.kind : undefined;
|
|
507
|
+
code = typeof evPayload.code === 'string' ? evPayload.code : undefined;
|
|
508
|
+
userMessage = typeof evPayload.user_message === 'string' ? evPayload.user_message : undefined;
|
|
509
|
+
retryAfterMs = typeof evPayload.retry_after_ms === 'number' ? evPayload.retry_after_ms : undefined;
|
|
510
|
+
if (evPayload.source && typeof evPayload.source === 'object') {
|
|
511
|
+
const s = evPayload.source;
|
|
512
|
+
source = {
|
|
513
|
+
task: typeof s.task === 'string' ? s.task : undefined,
|
|
514
|
+
agent: typeof s.agent === 'string' ? s.agent : undefined,
|
|
515
|
+
provider: typeof s.provider === 'string' ? s.provider : undefined,
|
|
516
|
+
model: typeof s.model === 'string' ? s.model : undefined,
|
|
517
|
+
toolRef: typeof s.tool_ref === 'string' ? s.tool_ref : undefined,
|
|
518
|
+
script: typeof s.script === 'string' ? s.script : undefined,
|
|
519
|
+
line: typeof s.line === 'number' ? s.line : undefined,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
message = String(evPayload);
|
|
525
|
+
}
|
|
526
|
+
steps = [...prev, {
|
|
527
|
+
id,
|
|
528
|
+
line: activeLineRef.current || 0,
|
|
529
|
+
type: 'execution',
|
|
530
|
+
content: `Error: ${message}`,
|
|
531
|
+
status: 'error',
|
|
532
|
+
timestamp,
|
|
533
|
+
nodeId: activeNodeRef.current ?? undefined,
|
|
534
|
+
visibility: 'inline',
|
|
535
|
+
errorKind: kind,
|
|
536
|
+
errorCode: code,
|
|
537
|
+
errorUserMessage: userMessage,
|
|
538
|
+
errorRetryAfterMs: retryAfterMs,
|
|
539
|
+
errorSource: source,
|
|
540
|
+
seq: wireSeq,
|
|
541
|
+
serverTs: wireServerTs,
|
|
542
|
+
}];
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
case 'TaskStart': {
|
|
546
|
+
const [name] = evPayload;
|
|
547
|
+
steps = [...prev, { id, line: activeLineRef.current || 0, type: 'execution', content: `Starting task: ${name}`, status: 'running', timestamp, nodeId: activeNodeRef.current ?? undefined, visibility: 'panel-only', seq: wireSeq, serverTs: wireServerTs }];
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
case 'TaskCacheHit': {
|
|
551
|
+
// P3: the engine emits `TaskCacheHit { agent, key_prefix }` right
|
|
552
|
+
// before the cached `AgentOutput` + `TaskEnd` arrive. Find the
|
|
553
|
+
// most recent open chat step for this agent and flip its
|
|
554
|
+
// `cached` flag so the renderer can show a "cached" pill before
|
|
555
|
+
// the row settles. The subsequent `TaskEnd` merges into the
|
|
556
|
+
// same step and spreads the rest of the fields without touching
|
|
557
|
+
// `cached` — so the flag survives end-to-end.
|
|
558
|
+
const agent = (evPayload && typeof evPayload === 'object' && 'agent' in evPayload)
|
|
559
|
+
? String(evPayload.agent)
|
|
560
|
+
: '';
|
|
561
|
+
// Walk newest-first: the most recently-opened chat row for this
|
|
562
|
+
// agent on the active node is the one the upcoming TaskEnd will
|
|
563
|
+
// fold into. We match on `taskName === agent` (the TaskPrompt
|
|
564
|
+
// arm seeds the chat step with `taskName = name` where `name`
|
|
565
|
+
// is the task identifier — which is also what the engine emits
|
|
566
|
+
// as `agent` on the cache-hit event for an agent-bound task).
|
|
567
|
+
let hitIdx = -1;
|
|
568
|
+
for (let i = prev.length - 1; i >= 0; i -= 1) {
|
|
569
|
+
const s = prev[i];
|
|
570
|
+
if (s.type === 'chat' && (s.taskName === agent || s.agent === agent)) {
|
|
571
|
+
hitIdx = i;
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (hitIdx === -1) {
|
|
576
|
+
// Defensive: replay edge cases (e.g. event arrives before its
|
|
577
|
+
// TaskPrompt) shouldn't produce a stray step. Forward-only.
|
|
578
|
+
steps = prev;
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
const updated = [...prev];
|
|
582
|
+
updated[hitIdx] = { ...updated[hitIdx], cached: true };
|
|
583
|
+
steps = updated;
|
|
584
|
+
}
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
case 'ValidationFailure': {
|
|
588
|
+
// Workstream 04 §A.4: structured payload from `EngineEvent::ValidationFailure`.
|
|
589
|
+
// `ValidationFailureCard` consumes `step.validationFailure` directly; the
|
|
590
|
+
// detail page re-renders the same card expanded.
|
|
591
|
+
const p = evPayload;
|
|
592
|
+
steps = [...prev, {
|
|
593
|
+
id,
|
|
594
|
+
line: activeLineRef.current || 0,
|
|
595
|
+
type: 'validation_failure',
|
|
596
|
+
content: `Validation failed on ${p.task_name} (attempt ${p.attempt})`,
|
|
597
|
+
status: 'error',
|
|
598
|
+
timestamp,
|
|
599
|
+
seq: wireSeq,
|
|
600
|
+
serverTs: wireServerTs,
|
|
601
|
+
nodeId: activeNodeRef.current ?? undefined,
|
|
602
|
+
visibility: 'inline',
|
|
603
|
+
validationFailure: {
|
|
604
|
+
taskName: p.task_name,
|
|
605
|
+
attempt: p.attempt,
|
|
606
|
+
modelResponse: p.model_response,
|
|
607
|
+
missingFields: p.missing_fields,
|
|
608
|
+
extraFields: p.extra_fields,
|
|
609
|
+
typeErrors: p.type_errors,
|
|
610
|
+
stopReason: p.stop_reason ?? null,
|
|
611
|
+
},
|
|
612
|
+
}];
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
case 'Suspended': {
|
|
616
|
+
const payloadObj = evPayload && typeof evPayload === 'object' && !Array.isArray(evPayload)
|
|
617
|
+
? evPayload
|
|
618
|
+
: null;
|
|
619
|
+
const checkpointName = payloadObj
|
|
620
|
+
? (typeof payloadObj.checkpoint_name === 'string' ? payloadObj.checkpoint_name : '')
|
|
621
|
+
: (Array.isArray(evPayload) && typeof evPayload[0] === 'string' ? evPayload[0] : '');
|
|
622
|
+
const suspendToken = payloadObj && typeof payloadObj.token === 'string' ? payloadObj.token : undefined;
|
|
623
|
+
const suspendPrompt = payloadObj && typeof payloadObj.prompt === 'string' ? payloadObj.prompt : undefined;
|
|
624
|
+
const suspendSchema = payloadObj ? payloadObj.schema : undefined;
|
|
625
|
+
const trigger = payloadObj
|
|
626
|
+
? normalizeSuspendTrigger(payloadObj.trigger)
|
|
627
|
+
: { kind: 'DagPosition' };
|
|
628
|
+
const exhausted = trigger.kind === 'ValidationExhausted'
|
|
629
|
+
? trigger
|
|
630
|
+
: null;
|
|
631
|
+
const step = {
|
|
632
|
+
id,
|
|
633
|
+
line: activeLineRef.current || 0,
|
|
634
|
+
type: 'execution',
|
|
635
|
+
content: exhausted
|
|
636
|
+
? `Validation exhausted after ${exhausted.retryCount} attempts on '${exhausted.taskName}' — suspended at ${checkpointName}`
|
|
637
|
+
: `Suspended at checkpoint: ${checkpointName}`,
|
|
638
|
+
status: 'pending',
|
|
639
|
+
timestamp,
|
|
640
|
+
seq: wireSeq,
|
|
641
|
+
serverTs: wireServerTs,
|
|
642
|
+
nodeId: activeNodeRef.current ?? undefined,
|
|
643
|
+
visibility: 'inline',
|
|
644
|
+
checkpointName,
|
|
645
|
+
suspendTrigger: trigger,
|
|
646
|
+
suspendToken,
|
|
647
|
+
suspendPrompt,
|
|
648
|
+
suspendSchema,
|
|
649
|
+
};
|
|
650
|
+
if (exhausted) {
|
|
651
|
+
step.retryCount = exhausted.retryCount;
|
|
652
|
+
step.lastAttempt = exhausted.lastAttempt;
|
|
653
|
+
step.validationErrors = exhausted.validationErrors;
|
|
654
|
+
}
|
|
655
|
+
steps = [...prev, step];
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
case 'Resumed': {
|
|
659
|
+
const payloadObj = evPayload && typeof evPayload === 'object' && !Array.isArray(evPayload)
|
|
660
|
+
? evPayload
|
|
661
|
+
: null;
|
|
662
|
+
const checkpointName = payloadObj
|
|
663
|
+
? (typeof payloadObj.checkpoint_name === 'string' ? payloadObj.checkpoint_name : '')
|
|
664
|
+
: (Array.isArray(evPayload) && typeof evPayload[0] === 'string' ? evPayload[0] : '');
|
|
665
|
+
steps = [...prev, { id, line: activeLineRef.current || 0, type: 'execution', content: `Resumed from checkpoint: ${checkpointName}`, status: 'running', timestamp, nodeId: activeNodeRef.current ?? undefined, visibility: 'inline', seq: wireSeq, serverTs: wireServerTs }];
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
case 'Breakpoint': {
|
|
669
|
+
const { node_id, span, token, env_snapshot } = evPayload;
|
|
670
|
+
activeLineRef.current = span.line;
|
|
671
|
+
activeNodeRef.current = node_id;
|
|
672
|
+
effects.setActiveLine = span.line;
|
|
673
|
+
effects.breakpoint = { nodeId: node_id, token, envSnapshot: env_snapshot, line: span.line };
|
|
674
|
+
steps = [...prev, { id, line: span.line, type: 'execution', content: `Paused at breakpoint (line ${span.line})`, status: 'running', variables: env_snapshot, timestamp, nodeId: node_id, visibility: 'inline', seq: wireSeq, serverTs: wireServerTs }];
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
case 'BreakpointResumed': {
|
|
678
|
+
const { node_id } = evPayload;
|
|
679
|
+
effects.breakpointResumed = true;
|
|
680
|
+
steps = prev.map(s => s.nodeId === node_id && s.content.startsWith('Paused at breakpoint') ? { ...s, content: `Resumed from breakpoint (line ${s.line})`, status: 'success', seq: wireSeq ?? s.seq, serverTs: wireServerTs ?? s.serverTs } : s);
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
case 'Log': {
|
|
684
|
+
const message = typeof evPayload === 'string' ? evPayload : JSON.stringify(evPayload);
|
|
685
|
+
steps = [...prev, { id, line: activeLineRef.current || 0, type: 'execution', content: message, status: 'success', timestamp, nodeId: activeNodeRef.current ?? undefined, visibility: 'inline', seq: wireSeq, serverTs: wireServerTs }];
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
case 'ToolCallStart': {
|
|
689
|
+
const step = {
|
|
690
|
+
id,
|
|
691
|
+
line: activeLineRef.current || 0,
|
|
692
|
+
type: 'tool_call',
|
|
693
|
+
content: `Calling ${evPayload.tool_name}...`,
|
|
694
|
+
status: 'running',
|
|
695
|
+
timestamp,
|
|
696
|
+
seq: wireSeq,
|
|
697
|
+
serverTs: wireServerTs,
|
|
698
|
+
toolName: evPayload.tool_name,
|
|
699
|
+
serverName: evPayload.server_name,
|
|
700
|
+
toolInput: evPayload.input,
|
|
701
|
+
nodeId: activeNodeRef.current ?? undefined,
|
|
702
|
+
visibility: 'panel-only',
|
|
703
|
+
};
|
|
704
|
+
steps = [...prev, step];
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
case 'SubScript': {
|
|
708
|
+
// Cross-script `call(...)` envelope (akribes-core EngineEvent::SubScript,
|
|
709
|
+
// PR #360). Each event wraps ONE inner sub-engine event; one logical
|
|
710
|
+
// call therefore arrives as a stream of SubScript envelopes that share
|
|
711
|
+
// the same `parent_task`.
|
|
712
|
+
//
|
|
713
|
+
// Strategy:
|
|
714
|
+
// * Maintain ONE `sub_script` step per (parent_task, currently open
|
|
715
|
+
// call). The first envelope for a parent_task with no open call
|
|
716
|
+
// creates the step; subsequent envelopes accumulate into that step
|
|
717
|
+
// until the wrapped child event is `WorkflowEnd` (or `Error`),
|
|
718
|
+
// which closes the call. A later envelope with the same
|
|
719
|
+
// `parent_task` — e.g. the same variable being assigned a fresh
|
|
720
|
+
// `call(...)` later in the workflow — opens a new step.
|
|
721
|
+
//
|
|
722
|
+
// * Nested calls (depth > 1, i.e. A → B → C): the engine wraps each
|
|
723
|
+
// level as a `SubScript` envelope, so a depth-2 grandchild event
|
|
724
|
+
// arrives as `SubScript { child: SubScript { child: <leaf> } }`.
|
|
725
|
+
// `unwrapSubScriptChain` peels off the wrappers into a frame stack
|
|
726
|
+
// `[A, B, C]`. We then walk into the open A-step's `children`,
|
|
727
|
+
// find/open the open B-step, walk into ITS `children`, and apply
|
|
728
|
+
// the leaf effects on the innermost (C) step. The outermost
|
|
729
|
+
// `SubScriptCard` recursively renders nested cards from `children`.
|
|
730
|
+
//
|
|
731
|
+
// * Token rollup: each wrapped `TaskEnd` reports usage at the level
|
|
732
|
+
// that hosts the task (so C's `TaskEnd` lands on the C step). We
|
|
733
|
+
// ALSO add it to every ancestor's `subScriptTokens` so the
|
|
734
|
+
// outermost card keeps showing the run-wide total — preserving the
|
|
735
|
+
// existing v1 "tokens still aggregate correctly" guarantee even
|
|
736
|
+
// across nesting. `nestedTaskCount` is rolled up the same way.
|
|
737
|
+
//
|
|
738
|
+
// Pricing: token totals accumulate; USD is intentionally NOT computed
|
|
739
|
+
// client-side. See the note on `SubScriptTokens` for the rationale.
|
|
740
|
+
const chain = unwrapSubScriptChain(evPayload);
|
|
741
|
+
if (!chain) {
|
|
742
|
+
steps = prev;
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
const { frames, effects } = chain;
|
|
746
|
+
const outermostFrame = frames[0];
|
|
747
|
+
// Walk the chain top-down: at each level, locate (or create) the open
|
|
748
|
+
// sub-script step matching that level's `parent_task`. The leaf-level
|
|
749
|
+
// effects are applied to the innermost frame. Token usage is folded
|
|
750
|
+
// into every ancestor's totals via the explicit
|
|
751
|
+
// `subScriptTokens = selfTokens + Σ child.subScriptTokens` invariant —
|
|
752
|
+
// see `combineSelfAndChildrenTokens`.
|
|
753
|
+
const updateFrame = (steps, frameIndex) => {
|
|
754
|
+
const frame = frames[frameIndex];
|
|
755
|
+
const isLeaf = frameIndex === frames.length - 1;
|
|
756
|
+
const openIdx = (() => {
|
|
757
|
+
for (let i = steps.length - 1; i >= 0; i -= 1) {
|
|
758
|
+
const s = steps[i];
|
|
759
|
+
if (s.type === 'sub_script'
|
|
760
|
+
&& s.subScript?.parentTask === frame.parentTask
|
|
761
|
+
&& !s.subScript?.closed) {
|
|
762
|
+
return i;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return -1;
|
|
766
|
+
})();
|
|
767
|
+
if (openIdx === -1) {
|
|
768
|
+
// First envelope for this call at this level — create the step.
|
|
769
|
+
// For a non-leaf frame (an outer level on a nested chain), the
|
|
770
|
+
// creation happens here on the recursion's way down; the leaf
|
|
771
|
+
// effects are applied below when `isLeaf` is true.
|
|
772
|
+
let childrenSteps = [];
|
|
773
|
+
if (!isLeaf) {
|
|
774
|
+
childrenSteps = updateFrame([], frameIndex + 1);
|
|
775
|
+
}
|
|
776
|
+
// selfTokens: only set when a leaf TaskEnd lands at this level on
|
|
777
|
+
// creation (rare — usually the level was opened by an upstream
|
|
778
|
+
// event before its first TaskEnd). Children's tokens are NEVER
|
|
779
|
+
// folded into selfTokens, only into the recursive total.
|
|
780
|
+
const selfTokens = isLeaf && effects.maybeUsage
|
|
781
|
+
? aggregateSubScriptTokens(undefined, effects.maybeUsage)
|
|
782
|
+
: undefined;
|
|
783
|
+
// Apply AgentOutput streaming buffer at the leaf level on creation.
|
|
784
|
+
let streamingBuffer;
|
|
785
|
+
if (isLeaf && effects.maybeAgentOutput) {
|
|
786
|
+
streamingBuffer = {
|
|
787
|
+
[effects.maybeAgentOutput.taskName]: effects.maybeAgentOutput.chunk,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
// Attach the in-flight streaming text to the new task summary if
|
|
791
|
+
// this creation event IS a TaskEnd.
|
|
792
|
+
const newSummary = isLeaf
|
|
793
|
+
? attachStreamingToSummary(effects.maybeTaskSummary, streamingBuffer)
|
|
794
|
+
: undefined;
|
|
795
|
+
// Drop the streaming entry once consumed by its TaskEnd so the
|
|
796
|
+
// buffer doesn't grow unbounded across a long-running sub-script.
|
|
797
|
+
if (newSummary && newSummary.kind === 'task_end' && streamingBuffer) {
|
|
798
|
+
const { [newSummary.taskName]: _drop, ...rest } = streamingBuffer;
|
|
799
|
+
streamingBuffer = Object.keys(rest).length > 0 ? rest : undefined;
|
|
800
|
+
}
|
|
801
|
+
const taskCountForThisLevel = isLeaf
|
|
802
|
+
? (selfTokens ? 1 : 0)
|
|
803
|
+
: rollupTaskCountFromChildren(childrenSteps);
|
|
804
|
+
const next = {
|
|
805
|
+
id: `${id}-l${frameIndex}`,
|
|
806
|
+
line: activeLineRef.current || 0,
|
|
807
|
+
type: 'sub_script',
|
|
808
|
+
content: `call("${frame.scriptName}")`,
|
|
809
|
+
status: isLeaf
|
|
810
|
+
? effects.childIsTerminal
|
|
811
|
+
? effects.childIsError
|
|
812
|
+
? 'error'
|
|
813
|
+
: 'success'
|
|
814
|
+
: 'running'
|
|
815
|
+
: 'running',
|
|
816
|
+
timestamp,
|
|
817
|
+
seq: wireSeq,
|
|
818
|
+
serverTs: wireServerTs,
|
|
819
|
+
nodeId: activeNodeRef.current ?? undefined,
|
|
820
|
+
visibility: 'inline',
|
|
821
|
+
subScript: {
|
|
822
|
+
scriptName: frame.scriptName,
|
|
823
|
+
parentTask: frame.parentTask,
|
|
824
|
+
inputs: isLeaf && effects.maybeInput ? [effects.maybeInput] : [],
|
|
825
|
+
output: isLeaf ? effects.maybeOutput : undefined,
|
|
826
|
+
subScriptTokens: combineSelfAndChildrenTokens(selfTokens, childrenSteps),
|
|
827
|
+
selfTokens,
|
|
828
|
+
nestedTaskCount: taskCountForThisLevel,
|
|
829
|
+
closed: isLeaf ? effects.childIsTerminal : false,
|
|
830
|
+
taskSummaries: newSummary ? [newSummary] : undefined,
|
|
831
|
+
children: isLeaf ? undefined : childrenSteps,
|
|
832
|
+
_streamingByTask: streamingBuffer,
|
|
833
|
+
},
|
|
834
|
+
};
|
|
835
|
+
return [...steps, next];
|
|
836
|
+
}
|
|
837
|
+
const cur = steps[openIdx];
|
|
838
|
+
const curSub = cur.subScript;
|
|
839
|
+
let nextChildren = curSub.children;
|
|
840
|
+
if (!isLeaf) {
|
|
841
|
+
nextChildren = updateFrame(curSub.children ?? [], frameIndex + 1);
|
|
842
|
+
}
|
|
843
|
+
// selfTokens accumulates ONLY at the leaf level. Ancestors keep
|
|
844
|
+
// their existing selfTokens unchanged; their `subScriptTokens` is
|
|
845
|
+
// re-derived from `selfTokens + children` so the descendant update
|
|
846
|
+
// bubbles up cleanly without any subtraction-based bookkeeping.
|
|
847
|
+
const nextSelfTokens = isLeaf && effects.maybeUsage
|
|
848
|
+
? aggregateSubScriptTokens(curSub.selfTokens, effects.maybeUsage)
|
|
849
|
+
: curSub.selfTokens;
|
|
850
|
+
// Update the streaming buffer for this leaf if AgentOutput arrived.
|
|
851
|
+
let nextStreaming = curSub._streamingByTask;
|
|
852
|
+
if (isLeaf && effects.maybeAgentOutput) {
|
|
853
|
+
const { taskName, chunk } = effects.maybeAgentOutput;
|
|
854
|
+
nextStreaming = {
|
|
855
|
+
...(nextStreaming ?? {}),
|
|
856
|
+
[taskName]: (nextStreaming?.[taskName] ?? '') + chunk,
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
// Build the (possibly enriched) new summary entry.
|
|
860
|
+
const enrichedSummary = isLeaf
|
|
861
|
+
? attachStreamingToSummary(effects.maybeTaskSummary, nextStreaming)
|
|
862
|
+
: undefined;
|
|
863
|
+
// Drop the streaming entry from the buffer once it has been
|
|
864
|
+
// attached to a finalized `task_end` summary; keeps memory flat.
|
|
865
|
+
if (enrichedSummary && enrichedSummary.kind === 'task_end' && nextStreaming) {
|
|
866
|
+
const { [enrichedSummary.taskName]: _drop, ...rest } = nextStreaming;
|
|
867
|
+
nextStreaming = Object.keys(rest).length > 0 ? rest : undefined;
|
|
868
|
+
}
|
|
869
|
+
const updatedSub = {
|
|
870
|
+
...curSub,
|
|
871
|
+
inputs: isLeaf && effects.maybeInput
|
|
872
|
+
? [...curSub.inputs.filter((i) => i.name !== effects.maybeInput.name), effects.maybeInput]
|
|
873
|
+
: curSub.inputs,
|
|
874
|
+
output: isLeaf && effects.maybeOutput !== undefined ? effects.maybeOutput : curSub.output,
|
|
875
|
+
selfTokens: nextSelfTokens,
|
|
876
|
+
subScriptTokens: combineSelfAndChildrenTokens(nextSelfTokens, isLeaf ? curSub.children : nextChildren),
|
|
877
|
+
nestedTaskCount: isLeaf
|
|
878
|
+
? (curSub.nestedTaskCount ?? 0) + (effects.maybeUsage ? 1 : 0)
|
|
879
|
+
: (curSub.nestedTaskCount ?? 0)
|
|
880
|
+
- rollupTaskCountFromChildren(curSub.children ?? [])
|
|
881
|
+
+ rollupTaskCountFromChildren(nextChildren ?? []),
|
|
882
|
+
closed: isLeaf ? (curSub.closed || effects.childIsTerminal) : curSub.closed,
|
|
883
|
+
taskSummaries: enrichedSummary
|
|
884
|
+
? [...(curSub.taskSummaries ?? []), enrichedSummary]
|
|
885
|
+
: curSub.taskSummaries,
|
|
886
|
+
children: isLeaf ? curSub.children : nextChildren,
|
|
887
|
+
_streamingByTask: nextStreaming,
|
|
888
|
+
};
|
|
889
|
+
const updated = {
|
|
890
|
+
...cur,
|
|
891
|
+
status: isLeaf
|
|
892
|
+
? (effects.childIsTerminal
|
|
893
|
+
? (effects.childIsError ? 'error' : 'success')
|
|
894
|
+
: cur.status)
|
|
895
|
+
: cur.status,
|
|
896
|
+
seq: wireSeq ?? cur.seq,
|
|
897
|
+
serverTs: wireServerTs ?? cur.serverTs,
|
|
898
|
+
subScript: updatedSub,
|
|
899
|
+
};
|
|
900
|
+
const next = [...steps];
|
|
901
|
+
next[openIdx] = updated;
|
|
902
|
+
return next;
|
|
903
|
+
};
|
|
904
|
+
// Sanity: outermost frame must be at the top level of `steps`.
|
|
905
|
+
void outermostFrame; // silence unused-var linter when no debugger active
|
|
906
|
+
steps = updateFrame(prev, 0);
|
|
907
|
+
break;
|
|
908
|
+
}
|
|
909
|
+
case 'LoopStart': {
|
|
910
|
+
// Open a new loop step. Subsequent `LoopTurn`s and the terminal
|
|
911
|
+
// `LoopEnd` will fold back into this step keyed by `loopName`. The
|
|
912
|
+
// step starts with an empty `turns` array; the panel renders a
|
|
913
|
+
// "running…" pulse until LoopEnd flips status to success/error.
|
|
914
|
+
const p = evPayload;
|
|
915
|
+
const loopName = typeof p.name === 'string' ? p.name : '';
|
|
916
|
+
const maxTurns = typeof p.max_turns === 'number' ? p.max_turns : 0;
|
|
917
|
+
steps = [...prev, {
|
|
918
|
+
id,
|
|
919
|
+
line: activeLineRef.current || 0,
|
|
920
|
+
type: 'loop',
|
|
921
|
+
content: `loop ${loopName}`,
|
|
922
|
+
status: 'running',
|
|
923
|
+
timestamp,
|
|
924
|
+
seq: wireSeq,
|
|
925
|
+
serverTs: wireServerTs,
|
|
926
|
+
nodeId: activeNodeRef.current ?? undefined,
|
|
927
|
+
visibility: 'inline',
|
|
928
|
+
loopName,
|
|
929
|
+
maxTurns,
|
|
930
|
+
turns: [],
|
|
931
|
+
}];
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
case 'LoopTurn': {
|
|
935
|
+
// Append a turn summary to the most-recent open (status === 'running')
|
|
936
|
+
// loop step with the matching `loopName`. We scan from the back so a
|
|
937
|
+
// later loop with a name that happens to collide with an earlier one
|
|
938
|
+
// (sequential loops in the same workflow) hits the right step. If no
|
|
939
|
+
// open loop is found we silently no-op — better than mis-attributing.
|
|
940
|
+
const p = evPayload;
|
|
941
|
+
const loopName = typeof p.name === 'string' ? p.name : '';
|
|
942
|
+
const turn = typeof p.turn === 'number' ? p.turn : 0;
|
|
943
|
+
const toolCalls = Array.isArray(p.tool_calls)
|
|
944
|
+
? p.tool_calls.filter((t) => typeof t === 'string')
|
|
945
|
+
: [];
|
|
946
|
+
const idx = (() => {
|
|
947
|
+
for (let i = prev.length - 1; i >= 0; i -= 1) {
|
|
948
|
+
const s = prev[i];
|
|
949
|
+
if (s.type === 'loop' && s.loopName === loopName && s.status === 'running')
|
|
950
|
+
return i;
|
|
951
|
+
}
|
|
952
|
+
return -1;
|
|
953
|
+
})();
|
|
954
|
+
if (idx === -1) {
|
|
955
|
+
steps = prev;
|
|
956
|
+
break;
|
|
957
|
+
}
|
|
958
|
+
const cur = prev[idx];
|
|
959
|
+
const next = [...prev];
|
|
960
|
+
next[idx] = {
|
|
961
|
+
...cur,
|
|
962
|
+
turns: [...(cur.turns ?? []), { turn, toolCalls }],
|
|
963
|
+
seq: wireSeq ?? cur.seq,
|
|
964
|
+
serverTs: wireServerTs ?? cur.serverTs,
|
|
965
|
+
};
|
|
966
|
+
steps = next;
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
case 'LoopEnd': {
|
|
970
|
+
// Finalize the matching loop step. Status is 'error' when the value is
|
|
971
|
+
// a `Value::FatalError` envelope (max_turns exhaustion) and 'success'
|
|
972
|
+
// otherwise. The full `value` is carried as `loopResult` so the UI can
|
|
973
|
+
// render it via `AkribesValueViewerWithRawToggle`.
|
|
974
|
+
//
|
|
975
|
+
// Wire shape: `Value` is serialised via `Value::to_wire_json` per the
|
|
976
|
+
// contract in `docs/src/content/docs/reference/engine-events.mdx` —
|
|
977
|
+
// scalars (`Value::String`, `Value::Int`, `Value::Bool`) emit bare JSON
|
|
978
|
+
// values, `Value::Object`/`Value::List` emit clean JSON containers, and
|
|
979
|
+
// `Value::FatalError` emits a `{ "FatalError": <msg>, "error_kind": ...,
|
|
980
|
+
// "code": ..., "error_detail": { ... } }` envelope. We detect the
|
|
981
|
+
// FatalError arm by structural shape so future additions to the
|
|
982
|
+
// FatalError wire envelope (e.g. extending `error_detail`) don't break
|
|
983
|
+
// the check.
|
|
984
|
+
const p = evPayload;
|
|
985
|
+
const loopName = typeof p.name === 'string' ? p.name : '';
|
|
986
|
+
const value = p.value;
|
|
987
|
+
const isFatal = !!value
|
|
988
|
+
&& typeof value === 'object'
|
|
989
|
+
&& !Array.isArray(value)
|
|
990
|
+
&& 'FatalError' in value;
|
|
991
|
+
const idx = (() => {
|
|
992
|
+
for (let i = prev.length - 1; i >= 0; i -= 1) {
|
|
993
|
+
const s = prev[i];
|
|
994
|
+
if (s.type === 'loop' && s.loopName === loopName && s.status === 'running')
|
|
995
|
+
return i;
|
|
996
|
+
}
|
|
997
|
+
return -1;
|
|
998
|
+
})();
|
|
999
|
+
if (idx === -1) {
|
|
1000
|
+
steps = prev;
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
const cur = prev[idx];
|
|
1004
|
+
const next = [...prev];
|
|
1005
|
+
next[idx] = {
|
|
1006
|
+
...cur,
|
|
1007
|
+
status: isFatal ? 'error' : 'success',
|
|
1008
|
+
loopResult: value,
|
|
1009
|
+
seq: wireSeq ?? cur.seq,
|
|
1010
|
+
serverTs: wireServerTs ?? cur.serverTs,
|
|
1011
|
+
};
|
|
1012
|
+
steps = next;
|
|
1013
|
+
break;
|
|
1014
|
+
}
|
|
1015
|
+
case 'ToolCallEnd': {
|
|
1016
|
+
const toolStepIndex = prev.findLastIndex((s) => s.type === 'tool_call' && s.toolName === evPayload.tool_name && s.status === 'running');
|
|
1017
|
+
if (toolStepIndex >= 0) {
|
|
1018
|
+
const updated = [...prev];
|
|
1019
|
+
const durationMs = evPayload.duration
|
|
1020
|
+
? evPayload.duration.secs * 1000 + evPayload.duration.nanos / 1000000
|
|
1021
|
+
: undefined;
|
|
1022
|
+
updated[toolStepIndex] = {
|
|
1023
|
+
...updated[toolStepIndex],
|
|
1024
|
+
status: 'success',
|
|
1025
|
+
content: `${evPayload.tool_name} completed`,
|
|
1026
|
+
toolOutput: evPayload.output,
|
|
1027
|
+
duration: durationMs,
|
|
1028
|
+
seq: wireSeq ?? updated[toolStepIndex].seq,
|
|
1029
|
+
serverTs: wireServerTs ?? updated[toolStepIndex].serverTs,
|
|
1030
|
+
};
|
|
1031
|
+
steps = updated;
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
steps = prev;
|
|
1035
|
+
}
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1038
|
+
default:
|
|
1039
|
+
steps = prev;
|
|
1040
|
+
}
|
|
1041
|
+
return { steps, effects };
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Helper to build run-from-line parameters from a previous execution's steps.
|
|
1045
|
+
* Returns null if there's no previous execution to build from.
|
|
1046
|
+
*/
|
|
1047
|
+
export function buildRunFromParams(executionSteps, targetLine) {
|
|
1048
|
+
if (executionSteps.length === 0)
|
|
1049
|
+
return null;
|
|
1050
|
+
const upstreamSteps = executionSteps.filter(s => s.line < targetLine && s.line > 0 && s.nodeId != null && s.status === 'success');
|
|
1051
|
+
if (upstreamSteps.length === 0)
|
|
1052
|
+
return null;
|
|
1053
|
+
const seedEnv = {};
|
|
1054
|
+
const skipNodeIds = new Set();
|
|
1055
|
+
for (const step of upstreamSteps) {
|
|
1056
|
+
if (step.nodeId != null) {
|
|
1057
|
+
skipNodeIds.add(step.nodeId);
|
|
1058
|
+
}
|
|
1059
|
+
if (step.variables) {
|
|
1060
|
+
Object.assign(seedEnv, step.variables);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
return {
|
|
1064
|
+
seedEnv,
|
|
1065
|
+
skipNodeIds: Array.from(skipNodeIds),
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
//# sourceMappingURL=steps.js.map
|