flowviant 0.86.0 → 0.88.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/bin/lib/agentCards.mjs +122 -0
- package/bin/lib/agentPlan.mjs +124 -31
- package/bin/lib/claude.mjs +32 -5
- package/bin/lib/fleet.mjs +8 -1
- package/bin/lib/prompts.mjs +173 -5
- package/bin/lib/runtimes.mjs +91 -10
- package/bin/lib/trace.mjs +72 -9
- package/bin/lib/work.mjs +415 -11
- package/package.json +1 -1
package/bin/lib/runtimes.mjs
CHANGED
|
@@ -46,6 +46,41 @@ import { SAFE, MODEL, USER_AGENT } from './config.mjs';
|
|
|
46
46
|
const oneLine = (s, n = 140) =>
|
|
47
47
|
String(s ?? '').replace(/\s+/g, ' ').trim().slice(0, n);
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* `label` IS THE READOUT; `full` IS THE RELAY (2026-09-16) — the convention
|
|
51
|
+
* every prose activity in this daemon follows, stated once here because three
|
|
52
|
+
* parsers produce one and three consumers read them.
|
|
53
|
+
*
|
|
54
|
+
* `label` is one collapsed, clamped line, because its first two consumers are a
|
|
55
|
+
* terminal console and a one-line pulse that is overwritten every two seconds,
|
|
56
|
+
* and neither can show a paragraph. The turn TRACE is a third consumer with the
|
|
57
|
+
* opposite need: it is scrollback, and clipping a sentence at 140 characters
|
|
58
|
+
* there is the product summarizing its own agent.
|
|
59
|
+
*
|
|
60
|
+
* So a prose activity may ALSO carry `full`: the text EXACTLY as the CLI
|
|
61
|
+
* emitted it, uncollapsed and unclipped. Nothing derives it and nothing but the
|
|
62
|
+
* trace reads it — `label` is untouched, so the console and the pulse are
|
|
63
|
+
* byte-identical to before. Absent means there was no fuller text than the
|
|
64
|
+
* label (a tool line, a bare thinking marker), and the trace falls back to the
|
|
65
|
+
* label — which is what every pre-0.87.0 daemon does for everything.
|
|
66
|
+
*
|
|
67
|
+
* THE MARKER BELOW IS THE label-ONLY CASE, and one definition with four
|
|
68
|
+
* readers: claude.mjs writes it, this file's codex parser falls back to it, and
|
|
69
|
+
* TWO collapses key on it — `trace.mjs` for the turn trace, `fleet.mjs` for the
|
|
70
|
+
* wiki sweep's feed. A collapse keyed on a string typed out four times would
|
|
71
|
+
* silently stop collapsing the first time somebody reworded one of them, and a
|
|
72
|
+
* collapse that stops collapsing fails no test — it just fills a feed.
|
|
73
|
+
*
|
|
74
|
+
* It exists at all because the CLIs do not hand over the thinking itself:
|
|
75
|
+
* measured 2026-09-16 across three real transcripts, 93 thinking blocks, every
|
|
76
|
+
* one of them empty (signature only), and a live probe with MAX_THINKING_TOKENS
|
|
77
|
+
* and `--include-partial-messages` returned an empty `thinking_delta` and a
|
|
78
|
+
* zero-length complete block. So the marker is the honest whole of what is
|
|
79
|
+
* known — "it is reasoning, not hung" — and the day text does arrive it rides
|
|
80
|
+
* `full` and this marker is simply not used.
|
|
81
|
+
*/
|
|
82
|
+
export const THINK_MARKER = 'thinking…';
|
|
83
|
+
|
|
49
84
|
const shortPath = (p, cwd) => {
|
|
50
85
|
const s = String(p ?? '');
|
|
51
86
|
return cwd && s.startsWith(cwd) ? s.slice(cwd.length).replace(/^\//, '') : s;
|
|
@@ -239,10 +274,22 @@ export const CLAUDE_TOOL_PROSE_KINDS = new Set([
|
|
|
239
274
|
*/
|
|
240
275
|
function humanizeCodexItem(item = {}, cwd = '') {
|
|
241
276
|
switch (item.item_type ?? item.type) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
277
|
+
// `full` rides beside the label wherever the item carries more than the
|
|
278
|
+
// label can hold — see the `label`/`full` note above. Codex is the runtime
|
|
279
|
+
// that actually SENDS reasoning text today, so its `think` is the one place
|
|
280
|
+
// in this daemon where a real thought reaches the trace whole.
|
|
281
|
+
case 'agent_message': {
|
|
282
|
+
const text = String(item.text ?? item.message ?? '');
|
|
283
|
+
return { kind: 'say', label: oneLine(text), ...(text ? { full: text } : {}) };
|
|
284
|
+
}
|
|
285
|
+
case 'reasoning': {
|
|
286
|
+
const text = String(item.text ?? '');
|
|
287
|
+
return {
|
|
288
|
+
kind: 'think',
|
|
289
|
+
label: oneLine(text) || THINK_MARKER,
|
|
290
|
+
...(text.trim() ? { full: text } : {}),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
246
293
|
case 'command_execution':
|
|
247
294
|
return {
|
|
248
295
|
kind: 'bash',
|
|
@@ -301,11 +348,28 @@ function parseCodexLine(line, cwd) {
|
|
|
301
348
|
: '';
|
|
302
349
|
return { activity, text };
|
|
303
350
|
}
|
|
304
|
-
case 'turn.failed':
|
|
351
|
+
case 'turn.failed': {
|
|
352
|
+
// An error is prose too, and the sentence that explains a failed turn is
|
|
353
|
+
// routinely longer than a 140-char label — a stack-shaped message loses
|
|
354
|
+
// its cause exactly where somebody is reading to find it.
|
|
355
|
+
//
|
|
356
|
+
// THE LABEL KEEPS ITS OWN `??`, not `msg || …`, and the difference is one
|
|
357
|
+
// input: an EXPLICITLY EMPTY message. `??` lets `''` through as an empty
|
|
358
|
+
// label, which every consumer swallows (`if (!label)`); `||` would print
|
|
359
|
+
// "turn failed" and fire a pulse where this daemon printed nothing. That
|
|
360
|
+
// may well be the better readout, but it is a change to the console and
|
|
361
|
+
// the pulse, and `full` shipped on the promise that neither moved — so it
|
|
362
|
+
// is argued for on its own day, not smuggled in beside a relay field.
|
|
363
|
+
const msg = String(ev.error?.message ?? '');
|
|
305
364
|
return {
|
|
306
|
-
activity: {
|
|
365
|
+
activity: {
|
|
366
|
+
kind: 'error',
|
|
367
|
+
label: oneLine(ev.error?.message ?? 'turn failed'),
|
|
368
|
+
...(msg.trim() ? { full: msg } : {}),
|
|
369
|
+
},
|
|
307
370
|
text: '',
|
|
308
371
|
};
|
|
372
|
+
}
|
|
309
373
|
// A bare `error` event — the shape an auth failure arrives in ("401
|
|
310
374
|
// Unauthorized: Missing bearer…", observed against 0.147.0 with no
|
|
311
375
|
// credentials). It used to fall through to `default` and be dropped, which
|
|
@@ -314,11 +378,19 @@ function parseCodexLine(line, cwd) {
|
|
|
314
378
|
// gave up rather than that the CLI is not signed in. The message goes into
|
|
315
379
|
// `text` so it reaches the operator's console AND the usage-limit
|
|
316
380
|
// classifier, which reads exactly this stream.
|
|
317
|
-
case 'error':
|
|
381
|
+
case 'error': {
|
|
382
|
+
// Same split as the arm above: `??` for the label (untouched behaviour),
|
|
383
|
+
// `msg` for the relay field only.
|
|
384
|
+
const msg = String(ev.message ?? '');
|
|
318
385
|
return {
|
|
319
|
-
activity: {
|
|
386
|
+
activity: {
|
|
387
|
+
kind: 'error',
|
|
388
|
+
label: oneLine(ev.message ?? 'error'),
|
|
389
|
+
...(msg.trim() ? { full: msg } : {}),
|
|
390
|
+
},
|
|
320
391
|
text: `${ev.message ?? ''}\n`,
|
|
321
392
|
};
|
|
393
|
+
}
|
|
322
394
|
default:
|
|
323
395
|
return null; // turn.started / item.started / item.updated
|
|
324
396
|
}
|
|
@@ -376,7 +448,14 @@ function parseAgyLine(line, cwd) {
|
|
|
376
448
|
const ti = su.tool_info;
|
|
377
449
|
if (!ti) return null;
|
|
378
450
|
const err = ti.error?.message;
|
|
379
|
-
|
|
451
|
+
// Same rule as codex's error lines: the label is the console's, `full` is
|
|
452
|
+
// the trace's, and a tool error's message is exactly the kind of sentence a
|
|
453
|
+
// 140-char clamp cuts the cause out of.
|
|
454
|
+
if (err)
|
|
455
|
+
return {
|
|
456
|
+
activity: { kind: 'error', label: oneLine(err), full: String(err) },
|
|
457
|
+
text: '',
|
|
458
|
+
};
|
|
380
459
|
// Each tool is reported twice — once ACTIVE, once DONE — so only the
|
|
381
460
|
// terminal state emits, otherwise every action appears in the thread twice.
|
|
382
461
|
if (su.state && su.state !== 'DONE') return null;
|
|
@@ -387,7 +466,9 @@ function parseAgyLine(line, cwd) {
|
|
|
387
466
|
// The final answer is the ONLY sentinel-bearing text: agy has no incremental
|
|
388
467
|
// assistant-message event, so a turn's whole verdict arrives here at once.
|
|
389
468
|
return {
|
|
390
|
-
activity: r.error
|
|
469
|
+
activity: r.error
|
|
470
|
+
? { kind: 'error', label: oneLine(r.error), full: String(r.error) }
|
|
471
|
+
: null,
|
|
391
472
|
text: `${r.response ?? ''}${r.error ? `\n${r.error}` : ''}\n`,
|
|
392
473
|
};
|
|
393
474
|
}
|
package/bin/lib/trace.mjs
CHANGED
|
@@ -54,6 +54,8 @@
|
|
|
54
54
|
* tail of a trace, never the settle behind it.
|
|
55
55
|
*/
|
|
56
56
|
|
|
57
|
+
import { THINK_MARKER } from './runtimes.mjs';
|
|
58
|
+
|
|
57
59
|
/** One batch every two seconds — the discipline the tab's narrator keeps, for
|
|
58
60
|
* the same reason: a turn emits hundreds of entries and nobody is reading them
|
|
59
61
|
* faster than that. */
|
|
@@ -64,9 +66,22 @@ export const TRACE_BATCH = 40;
|
|
|
64
66
|
* scrollback, and the newest steps are the ones somebody watching wants. The
|
|
65
67
|
* drop is not silent — see the seq rule above. */
|
|
66
68
|
export const TRACE_BUFFER = 120;
|
|
67
|
-
/**
|
|
68
|
-
*
|
|
69
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Longest prose entry. The server clamps to the same number
|
|
71
|
+
* (`agentTrace.ts.TRACE_PROSE_CAP`); doing it here too means a pathological line
|
|
72
|
+
* never becomes the POST. The two must move together — daemon at or below the
|
|
73
|
+
* server's, or the server silently does the cutting and this file's caps stop
|
|
74
|
+
* describing what ships.
|
|
75
|
+
*
|
|
76
|
+
* 300 UNTIL 2026-09-16, when it was the thing clipping sentences. A trace entry
|
|
77
|
+
* used to be the humanized 160-char label the console prints, so a 300 cap could
|
|
78
|
+
* not bite; it now carries the FULL text of what the CLI said or thought, and at
|
|
79
|
+
* 300 a paragraph of narration was cut mid-sentence. The real bound is the
|
|
80
|
+
* TURN's (400 entries / 96KB server-side, oldest shed with the count said out
|
|
81
|
+
* loud) — a long thought spends budget older steps would have held, and an
|
|
82
|
+
* admitted shed beats a silent clip.
|
|
83
|
+
*/
|
|
84
|
+
export const TRACE_PROSE_CAP = 4_000;
|
|
70
85
|
|
|
71
86
|
/**
|
|
72
87
|
* ONE ATTEMPT AT ONE TURN, named.
|
|
@@ -93,6 +108,11 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
93
108
|
* server's business: it rebases each run onto its own high-water mark. */
|
|
94
109
|
const queue = [];
|
|
95
110
|
let base = 0;
|
|
111
|
+
/** The last entry this relay ACCEPTED, kept across flushes and across a shed
|
|
112
|
+
* buffer — `queue[queue.length - 1]` is not the same thing, because a flush
|
|
113
|
+
* empties the queue and a batch boundary is not a change in the stream. Read
|
|
114
|
+
* by the bare-marker collapse in `prose()` and nothing else. */
|
|
115
|
+
let lastEntry = null;
|
|
96
116
|
let dirty = false;
|
|
97
117
|
let timer = null;
|
|
98
118
|
let stopped = false;
|
|
@@ -111,6 +131,7 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
111
131
|
|
|
112
132
|
const push = (entry) => {
|
|
113
133
|
if (stopped || !entry) return;
|
|
134
|
+
lastEntry = entry;
|
|
114
135
|
queue.push(entry);
|
|
115
136
|
while (queue.length > TRACE_BUFFER) {
|
|
116
137
|
queue.shift();
|
|
@@ -157,17 +178,59 @@ export function makeTraceRelay({ agentId, turnId, post, scrub = (s) => s, run =
|
|
|
157
178
|
};
|
|
158
179
|
|
|
159
180
|
return {
|
|
160
|
-
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
181
|
+
/**
|
|
182
|
+
* A prose line from the stream. `kind` is the daemon's activity vocabulary
|
|
183
|
+
* (runtimes.mjs); anything that is not thinking or the model speaking is a
|
|
184
|
+
* `note` — the honest bucket for a codex error line or an agy tool name,
|
|
185
|
+
* rather than a wire value invented per runtime.
|
|
186
|
+
*
|
|
187
|
+
* THE NEWLINES SURVIVE (2026-09-16). This used to be `\s+ → ' '`, which was
|
|
188
|
+
* right while an entry WAS a one-line label and wrong the moment the caller
|
|
189
|
+
* started handing over the whole of what the CLI said: a model writes in
|
|
190
|
+
* paragraphs and lists, and flattening them here is the relay deciding how
|
|
191
|
+
* the agent's own words should be shaped. So only HORIZONTAL runs collapse,
|
|
192
|
+
* and a wall of blank lines becomes one — that second rule is not cosmetic,
|
|
193
|
+
* it stops padding eating the cap that the real sentences need.
|
|
194
|
+
*
|
|
195
|
+
* The scrub still runs over the WHOLE text, before the cap: this is the
|
|
196
|
+
* CLI's own stdout, and a secret in a thought must not ride further than it
|
|
197
|
+
* did when a thought was 300 characters.
|
|
198
|
+
*/
|
|
164
199
|
prose(kind, text) {
|
|
165
200
|
const t = scrub(String(text ?? ''))
|
|
166
|
-
|
|
201
|
+
// One line ending, whatever the CLI printed.
|
|
202
|
+
.replace(/\r\n?/g, '\n')
|
|
203
|
+
// Spaces and tabs collapse; `\n` is deliberately excluded from the class.
|
|
204
|
+
.replace(/[^\S\n]+/g, ' ')
|
|
205
|
+
// Three or more breaks in a row — two or more blank lines — become one.
|
|
206
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
167
207
|
.trim()
|
|
168
208
|
.slice(0, TRACE_PROSE_CAP);
|
|
169
209
|
if (!t) return;
|
|
170
|
-
|
|
210
|
+
const k = kind === 'think' ? 'think' : kind === 'say' ? 'say' : 'note';
|
|
211
|
+
/**
|
|
212
|
+
* A RUN OF BARE "thinking…" IS ONE STEP, not forty.
|
|
213
|
+
*
|
|
214
|
+
* Claude emits a thinking block per burst and (measured 2026-09-16, three
|
|
215
|
+
* real transcripts: 93 blocks, all of them empty) carries no text in any
|
|
216
|
+
* of them — so the trace filled with the identical marker repeated, which
|
|
217
|
+
* is noise standing exactly where the thought would have been. The wiki
|
|
218
|
+
* feed collapses the same run for the same reason (fleet.mjs: "Collapse
|
|
219
|
+
* runs of bare 'thinking…' so the feed doesn't fill with it").
|
|
220
|
+
*
|
|
221
|
+
* ONLY THE BARE MARKER, and that is the whole safety of it: a think WITH
|
|
222
|
+
* text is never equal to it, so no real thought is ever eaten — the day
|
|
223
|
+
* the CLI starts emitting thinking text, every one of those blocks lands
|
|
224
|
+
* whole beside the others.
|
|
225
|
+
*
|
|
226
|
+
* A collapsed marker is NOT a drop: it never becomes an entry, so it
|
|
227
|
+
* never takes a seq, exactly like the empty line above it. The "N earlier
|
|
228
|
+
* steps are not shown" count stays a count of steps that existed.
|
|
229
|
+
*/
|
|
230
|
+
if (k === 'think' && t === THINK_MARKER && lastEntry?.k === 'think' && lastEntry.t === t) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
push({ k, t });
|
|
171
234
|
},
|
|
172
235
|
/** One structured tool event, exactly as `toolEventOf` built it. A tool this
|
|
173
236
|
* builder does not know returns null there and nothing is pushed here — a
|