flowviant 0.125.29 → 0.125.31
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/README.md +364 -0
- package/bin/cli.mjs +23 -0
- package/bin/lib/agentPlan.mjs +8 -0
- package/bin/lib/agentQueueContext.mjs +6 -0
- package/bin/lib/agentSplit.mjs +90 -0
- package/bin/lib/architectureContract.mjs +57 -0
- package/bin/lib/architectureReport.mjs +38 -0
- package/bin/lib/artifactContracts.mjs +21 -2
- package/bin/lib/claudeInput.mjs +1 -1
- package/bin/lib/claudeSubagents.mjs +46 -6
- package/bin/lib/claudeWorkflows.mjs +94 -0
- package/bin/lib/cliLimit.mjs +5 -0
- package/bin/lib/codexInput.mjs +191 -0
- package/bin/lib/codexSubagentEvents.mjs +3 -1
- package/bin/lib/codexSubagents.mjs +34 -9
- package/bin/lib/conversationOutput.mjs +14 -0
- package/bin/lib/crossCliChild.mjs +327 -0
- package/bin/lib/crossCliLaunch.mjs +92 -0
- package/bin/lib/crossCliPosture.mjs +55 -0
- package/bin/lib/crossCliResearch.mjs +19 -0
- package/bin/lib/environmentProcess.mjs +44 -0
- package/bin/lib/fixtures/workflow-ultracode.jsonl +22 -0
- package/bin/lib/fleet.mjs +2 -0
- package/bin/lib/linuxDesktopBridge.mjs +62 -0
- package/bin/lib/managedBrowser.mjs +50 -10
- package/bin/lib/mergeBatch.mjs +1 -1
- package/bin/lib/nativeAgentTools.mjs +12 -5
- package/bin/lib/noWindowEnv.mjs +4 -2
- package/bin/lib/orchestrator.mjs +24 -7
- package/bin/lib/orchestratorClient.mjs +8 -2
- package/bin/lib/orchestratorPrompt.mjs +13 -5
- package/bin/lib/orchestratorProtocol.mjs +18 -0
- package/bin/lib/outsideCli.mjs +124 -0
- package/bin/lib/projectToolRuntimes.mjs +4 -2
- package/bin/lib/projectTools.mjs +2 -2
- package/bin/lib/prompts.mjs +7 -5
- package/bin/lib/repositoryPlaces.mjs +1 -1
- package/bin/lib/repositorySetup.mjs +15 -24
- package/bin/lib/reviewBridge.mjs +110 -0
- package/bin/lib/reviewDatabase.mjs +112 -0
- package/bin/lib/reviewDesktop.mjs +153 -0
- package/bin/lib/reviewEnvironment.mjs +286 -0
- package/bin/lib/reviewEnvironmentConfig.mjs +104 -0
- package/bin/lib/reviewEnvironmentMain.mjs +38 -0
- package/bin/lib/reviewImage.mjs +58 -0
- package/bin/lib/reviewMcpClient.mjs +69 -0
- package/bin/lib/reviewProcess.mjs +51 -0
- package/bin/lib/reviewTargets.mjs +50 -0
- package/bin/lib/robloxReview.mjs +234 -0
- package/bin/lib/robloxStudioBridge.mjs +68 -0
- package/bin/lib/runTurn.mjs +30 -8
- package/bin/lib/runtimeClaude.mjs +7 -3
- package/bin/lib/runtimeCodex.mjs +12 -6
- package/bin/lib/runtimeLimits.mjs +59 -1
- package/bin/lib/runtimeReview.mjs +185 -0
- package/bin/lib/splitGate.mjs +115 -0
- package/bin/lib/subagentBounds.mjs +26 -0
- package/bin/lib/subagentLedger.mjs +110 -0
- package/bin/lib/trace.mjs +134 -6
- package/bin/lib/turnDeclaration.mjs +4 -4
- package/bin/lib/turnGaps.mjs +6 -2
- package/bin/lib/turnSubagents.mjs +23 -9
- package/bin/lib/uiReview.mjs +5 -2
- package/bin/lib/wikiDraft.mjs +9 -0
- package/bin/lib/work.mjs +22 -1
- package/bin/lib/workAgentCheck.mjs +24 -1
- package/bin/lib/workAgentResume.mjs +11 -2
- package/bin/lib/workAgentTools.mjs +6 -4
- package/bin/lib/workAgentTurnExecution.mjs +77 -9
- package/bin/lib/workAgentTurnOutcome.mjs +11 -0
- package/bin/lib/workAgentTurnReports.mjs +5 -2
- package/package.json +1 -1
|
@@ -116,8 +116,27 @@ export const WHOLE_REQUEST = `THE WHOLE REQUEST, NOT ONLY THE BRIEF:
|
|
|
116
116
|
delivered JSON object, one entry per item this card was meant to deliver
|
|
117
117
|
that you did not build ("not built"), built in part ("partly") or did not
|
|
118
118
|
check against the real thing ("unverified"), and per part of the request
|
|
119
|
-
that NO card in this run owns ("unowned")
|
|
120
|
-
|
|
119
|
+
that NO card in this run owns ("unowned"). Outside this run does not mean
|
|
120
|
+
unowned in the project: check relatedWork and the current queue first.
|
|
121
|
+
"gaps": [{ "item": "what is missing", "status": "not built | partly | unverified | unowned", "why": "one sentence", "disposition": "required | owned | optional | decision", "ownerTaskId": "existing card id only when owned" }]
|
|
122
|
+
ROUTE FINDINGS BEFORE DELIVERY:
|
|
123
|
+
- Already assigned, queued, running or completed on another card: use
|
|
124
|
+
disposition "owned" and its actual ownerTaskId. Link it; do not repeat it
|
|
125
|
+
or stage another task. An older Done receipt is not proof a recurrence is fixed.
|
|
126
|
+
- Necessary to meet this card's approved outcome: finish it now in this same
|
|
127
|
+
worktree and verify the combined result. Record completed incidental repairs
|
|
128
|
+
through raised. If another work step is needed, report disposition "required"
|
|
129
|
+
with why it is necessary; the server queues the same card's next step before
|
|
130
|
+
Review. This does not authorize another card's work, a product-kind change,
|
|
131
|
+
optional improvements, merging or deployment. Keep commit attribution.
|
|
132
|
+
- Optional improvement: use disposition "optional". Describe it as an offer in
|
|
133
|
+
the agent's summary; the person may reply here to include it. It creates no
|
|
134
|
+
New task tab and does not block delivery of the approved scope.
|
|
135
|
+
- A requirement, ownership or scope decision only the person can settle:
|
|
136
|
+
use the existing blocked reply with a specific question and options. Stay in
|
|
137
|
+
this agent conversation. Never claim delivery while that decision is required.
|
|
138
|
+
- After a person says "Finish the current scope", retain declined additions as
|
|
139
|
+
optional and finish the approved request; do not ask the same scope question again.
|
|
121
140
|
"gaps": [] says you walked the request and nothing is missing. Never recast
|
|
122
141
|
a gap as a decision: a choice you made in place of the request is a gap
|
|
123
142
|
("partly") with its reason.
|
package/bin/lib/claudeInput.mjs
CHANGED
|
@@ -95,7 +95,7 @@ export function createClaudeInput(child, { onTaken = () => {}, onContext = null
|
|
|
95
95
|
}
|
|
96
96
|
if (event.type === 'system' && event.subtype === 'compact_boundary') { compacting = false; askContext(true); }
|
|
97
97
|
if (event.type === 'system' && event.subtype === 'background_tasks_changed' && Array.isArray(event.tasks)) {
|
|
98
|
-
agents = event.tasks.filter(task => task?.task_type
|
|
98
|
+
agents = event.tasks.filter(task => ['local_agent', 'local_workflow'].includes(task?.task_type)).length;
|
|
99
99
|
if (agents > 0) draining = false; // a drained message's turn started agents
|
|
100
100
|
}
|
|
101
101
|
if (event.type === 'result') {
|
|
@@ -1,11 +1,34 @@
|
|
|
1
1
|
import { NATIVE_AGENT_TOOLS } from './nativeAgentTools.mjs';
|
|
2
|
+
import { createClaudeWorkflows } from './claudeWorkflows.mjs';
|
|
3
|
+
import { turnModelOf } from './turnModel.mjs';
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
+
const textOf = content => typeof content === 'string' ? content
|
|
6
|
+
: Array.isArray(content) ? content.filter(b => b?.type === 'text' && typeof b.text === 'string').map(b => b.text).join('\n') : '';
|
|
7
|
+
const bounded = (map, n) => { if (map.size > n) map.delete(map.keys().next().value); };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Native tool-use ids are stable for the lifetime of an Agent/Task call.
|
|
11
|
+
*
|
|
12
|
+
* WHAT A CHILD SAYS OF ITSELF (2026-11-06, S4.1): its model off its own
|
|
13
|
+
* `message.model` (never the spawn's `model` input: what was asked is not
|
|
14
|
+
* what ran), its final words off its own sidechain `result`, else the
|
|
15
|
+
* foreground call's tool_result, else its last message once it is done; its
|
|
16
|
+
* parent is the stream the spawn rode (the lead, or a child that spawned
|
|
17
|
+
* one). Claude states no per-child effort, so none is reported.
|
|
18
|
+
*/
|
|
19
|
+
export function createClaudeSubagents(report = () => {}, onWorkflow = () => {}) {
|
|
20
|
+
const workflows = createClaudeWorkflows(report, onWorkflow);
|
|
5
21
|
const calls = new Map();
|
|
6
22
|
const aliases = new Map();
|
|
7
23
|
const questions = new Map();
|
|
8
24
|
const seen = new Set();
|
|
25
|
+
const models = new Map(), said = new Map(), resulted = new Set();
|
|
26
|
+
const finish = (id, result) => {
|
|
27
|
+
const text = typeof result === 'string' && result.trim() ? result : said.get(id);
|
|
28
|
+
if (!text || resulted.has(id)) return {};
|
|
29
|
+
resulted.add(id); bounded(resulted, 128);
|
|
30
|
+
return { result: text };
|
|
31
|
+
};
|
|
9
32
|
const identify = ev => ev.parent_tool_use_id != null
|
|
10
33
|
? aliases.get(ev.parent_tool_use_id) ?? ev.parent_tool_use_id
|
|
11
34
|
: ev.isSidechain ? aliases.get(ev.agentId ?? ev.agent_id ?? ev.sessionId) : undefined;
|
|
@@ -28,7 +51,18 @@ export function createClaudeSubagents(report = () => {}) {
|
|
|
28
51
|
return true;
|
|
29
52
|
},
|
|
30
53
|
observe(ev) {
|
|
54
|
+
workflows.observe(ev);
|
|
31
55
|
const agentId = identify(ev);
|
|
56
|
+
if (ev.type === 'assistant' && agentId) {
|
|
57
|
+
const model = turnModelOf(ev.message?.model);
|
|
58
|
+
if (model && models.get(agentId) !== model) { models.set(agentId, model); bounded(models, 128); report({ id: agentId, model }); }
|
|
59
|
+
const words = textOf(ev.message?.content);
|
|
60
|
+
if (words.trim()) { said.set(agentId, words); bounded(said, 128); }
|
|
61
|
+
}
|
|
62
|
+
if (ev.type === 'result' && agentId && !ev.is_error && typeof ev.result === 'string' && ev.result.trim()) {
|
|
63
|
+
resulted.delete(agentId);
|
|
64
|
+
report({ id: agentId, ...finish(agentId, ev.result) });
|
|
65
|
+
}
|
|
32
66
|
if (ev.type === 'assistant') {
|
|
33
67
|
for (const b of ev.message?.content ?? []) {
|
|
34
68
|
if (b.type === 'tool_use' && NATIVE_AGENT_TOOLS.claude.ask.includes(b.name) && agentId) {
|
|
@@ -43,8 +77,11 @@ export function createClaudeSubagents(report = () => {}) {
|
|
|
43
77
|
aliases.set(b.id, id);
|
|
44
78
|
if (calls.size > 128) calls.delete(calls.keys().next().value);
|
|
45
79
|
if (aliases.size > 256) aliases.delete(aliases.keys().next().value);
|
|
80
|
+
// A resume names the child's native id: the identity it keeps across turns.
|
|
81
|
+
const resumed = typeof input.resume === 'string' && input.resume ? { nativeId: input.resume } : {};
|
|
82
|
+
resulted.delete(id);
|
|
46
83
|
report({ id, kind: input.subagent_type ?? '', title: input.description ?? input.name ?? '',
|
|
47
|
-
brief: input.prompt ?? '', status: 'running', now: input.description ?? '' });
|
|
84
|
+
brief: input.prompt ?? '', status: 'running', now: input.description ?? '', parent: agentId ?? 'lead', ...resumed });
|
|
48
85
|
}
|
|
49
86
|
}
|
|
50
87
|
if (ev.type === 'user') {
|
|
@@ -60,8 +97,10 @@ export function createClaudeSubagents(report = () => {}) {
|
|
|
60
97
|
if (nativeId) aliases.set(nativeId, id);
|
|
61
98
|
// Background launch is not completion; its notification closes it.
|
|
62
99
|
const background = result.status === 'async_launched' || result.isAsync === true || calls.get(b.tool_use_id).input.run_in_background === true;
|
|
63
|
-
|
|
64
|
-
|
|
100
|
+
const status = b.is_error ? 'stopped' : background ? 'running' : 'done';
|
|
101
|
+
report({ id, ...(nativeId ? { nativeId } : {}), status,
|
|
102
|
+
...(typeof result.description === 'string' ? { now: result.description } : {}),
|
|
103
|
+
...(status === 'done' ? finish(id, textOf(b.content)) : {}) });
|
|
65
104
|
}
|
|
66
105
|
}
|
|
67
106
|
if (ev.type === 'system' && ['task_started', 'task_progress', 'task_notification'].includes(ev.subtype)) {
|
|
@@ -70,7 +109,8 @@ export function createClaudeSubagents(report = () => {}) {
|
|
|
70
109
|
if (id) {
|
|
71
110
|
if (ev.task_id) aliases.set(ev.task_id, id);
|
|
72
111
|
const status = ev.status === 'completed' ? 'done' : ['failed', 'stopped'].includes(ev.status) ? 'stopped' : 'running';
|
|
73
|
-
report({ id, status, ...(ev.description || ev.summary ? { now: ev.description ?? ev.summary } : {})
|
|
112
|
+
report({ id, status, ...(ev.description || ev.summary ? { now: ev.description ?? ev.summary } : {}),
|
|
113
|
+
...(status === 'done' ? finish(id) : {}) });
|
|
74
114
|
}
|
|
75
115
|
}
|
|
76
116
|
return agentId;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Workflow children have no stdout of their own: task_progress carries snapshots. */
|
|
2
|
+
import { turnModelOf } from './turnModel.mjs';
|
|
3
|
+
import { WORKFLOW_AGENT_CAP, WORKFLOW_CAP } from './subagentBounds.mjs';
|
|
4
|
+
|
|
5
|
+
const text = (s, n) => typeof s === 'string' ? s.slice(0, n) : '';
|
|
6
|
+
const idOf = s => typeof s === 'string' && s.length > 0 && s.length <= 96 ? s : null;
|
|
7
|
+
const instant = v => {
|
|
8
|
+
const n = typeof v === 'number' ? v : Date.parse(v);
|
|
9
|
+
return Number.isFinite(n) && Math.abs(n) < 8.64e15 ? new Date(n).toISOString() : undefined;
|
|
10
|
+
};
|
|
11
|
+
const count = v => Number.isSafeInteger(v) && v >= 0 ? v : undefined;
|
|
12
|
+
const statusOf = s => s === 'done' || s === 'completed' ? 'done'
|
|
13
|
+
: s === 'failed' || s === 'stopped' || s === 'cancelled' ? 'stopped' : 'running';
|
|
14
|
+
|
|
15
|
+
export function createClaudeWorkflows(reportAgent = () => {}, report = () => {}) {
|
|
16
|
+
const runs = new Map(), aliases = new Map(), launches = new Map();
|
|
17
|
+
return {
|
|
18
|
+
observe(ev) {
|
|
19
|
+
if (ev.type === 'assistant' && !ev.parent_tool_use_id && !ev.isSidechain) {
|
|
20
|
+
for (const b of ev.message?.content ?? []) if (b?.type === 'tool_use' && b.name === 'Workflow' && idOf(b.id)) {
|
|
21
|
+
launches.set(b.id, text(b.input?.description, 600));
|
|
22
|
+
if (launches.size > WORKFLOW_CAP) launches.delete(launches.keys().next().value);
|
|
23
|
+
report({ launcherId: b.id });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (ev.type !== 'system' || !['task_started', 'task_progress', 'task_updated', 'task_notification'].includes(ev.subtype)) return;
|
|
27
|
+
const id = idOf(ev.run_id) ?? aliases.get(ev.task_id);
|
|
28
|
+
if (!id) return;
|
|
29
|
+
if (ev.subtype === 'task_started' && ev.task_type === 'local_workflow' && !runs.has(id)) {
|
|
30
|
+
if (runs.size >= WORKFLOW_CAP) return;
|
|
31
|
+
runs.set(id, { row: { id, key: `claude-workflow:${id}`, name: text(ev.workflow_name, 200),
|
|
32
|
+
description: text(ev.description ?? launches.get(ev.tool_use_id), 600), status: 'running',
|
|
33
|
+
startedAt: instant(ev.startedAt) ?? new Date().toISOString(), phases: [], notRecorded: 0 },
|
|
34
|
+
agents: new Map(), seen: new Set(), tool: ev.tool_use_id });
|
|
35
|
+
if (idOf(ev.task_id)) aliases.set(ev.task_id, id);
|
|
36
|
+
}
|
|
37
|
+
const run = runs.get(id);
|
|
38
|
+
if (!run) return;
|
|
39
|
+
const { row, agents, seen } = run;
|
|
40
|
+
const before = JSON.stringify(row);
|
|
41
|
+
const progress = Array.isArray(ev.workflow_progress) ? ev.workflow_progress.slice(0, 2000) : [];
|
|
42
|
+
for (const a of progress) {
|
|
43
|
+
if (a?.type !== 'workflow_agent' || !idOf(a.agentId)) continue;
|
|
44
|
+
seen.add(a.agentId);
|
|
45
|
+
if (seen.size > 4096) seen.delete(seen.values().next().value);
|
|
46
|
+
const previous = agents.get(a.agentId);
|
|
47
|
+
const status = statusOf(a.state);
|
|
48
|
+
const startedAt = instant(a.startedAt) ?? previous?.startedAt;
|
|
49
|
+
const endedAt = status !== 'running' ? instant(a.endedAt) ?? instant(
|
|
50
|
+
Number.isFinite(a.startedAt) && Number.isFinite(a.durationMs) ? a.startedAt + a.durationMs : a.lastProgressAt
|
|
51
|
+
) ?? previous?.endedAt ?? new Date().toISOString() : undefined;
|
|
52
|
+
const next = { ...previous, id: a.agentId, nativeId: a.agentId, key: `claude:${a.agentId}`, cli: 'claude',
|
|
53
|
+
kind: 'workflow', parentKey: row.key, title: text(a.label, 200), status,
|
|
54
|
+
...(typeof a.phaseTitle === 'string' ? { phase: text(a.phaseTitle, 200) } : {}),
|
|
55
|
+
...(startedAt ? { startedAt } : {}), ...(endedAt ? { endedAt } : {}),
|
|
56
|
+
...(turnModelOf(a.model) ? { model: turnModelOf(a.model) } : {}),
|
|
57
|
+
...(a.lastToolName || a.lastToolSummary ? { now: [text(a.lastToolName, 80), text(a.lastToolSummary, 400)].filter(Boolean).join(' ').slice(0, 400) } : {}),
|
|
58
|
+
...(typeof a.promptPreview === 'string' ? { brief: text(a.promptPreview, 24000), briefStatus: 'truncated' } : {}),
|
|
59
|
+
...(typeof a.resultPreview === 'string' ? { result: text(a.resultPreview, 8000), resultStatus: 'truncated' } : {}),
|
|
60
|
+
...(count(a.tokens) !== undefined ? { tokens: a.tokens } : {}),
|
|
61
|
+
...(count(a.toolCalls) !== undefined ? { toolCalls: a.toolCalls } : {}) };
|
|
62
|
+
if (status === 'running') delete next.endedAt;
|
|
63
|
+
agents.set(a.agentId, next);
|
|
64
|
+
}
|
|
65
|
+
const newest = [...agents.values()].sort((a, b) => (b.startedAt ?? '').localeCompare(a.startedAt ?? ''));
|
|
66
|
+
for (const a of newest.slice(WORKFLOW_AGENT_CAP)) agents.delete(a.id);
|
|
67
|
+
row.notRecorded = Math.max(row.notRecorded, seen.size - agents.size);
|
|
68
|
+
if (progress.length) row.phases = progress.filter(p => p?.type === 'workflow_phase').slice(0, 64).map(p => {
|
|
69
|
+
const children = progress.filter(a => a?.type === 'workflow_agent' && a.phaseTitle === p.title);
|
|
70
|
+
return { title: text(p.title, 200), status: p.state ? statusOf(p.state)
|
|
71
|
+
: children.length && children.every(a => statusOf(a.state) === 'done') ? 'done' : 'running' };
|
|
72
|
+
});
|
|
73
|
+
if (count(ev.usage?.total_tokens) !== undefined) row.totalTokens = ev.usage.total_tokens;
|
|
74
|
+
if (count(ev.usage?.tool_uses) !== undefined) row.toolCalls = ev.usage.tool_uses;
|
|
75
|
+
if (ev.subtype === 'task_updated' && instant(ev.patch?.end_time)) run.endedAt = instant(ev.patch.end_time);
|
|
76
|
+
if (ev.subtype === 'task_notification') {
|
|
77
|
+
row.status = statusOf(ev.status);
|
|
78
|
+
row.endedAt = instant(ev.endedAt) ?? run.endedAt ?? new Date().toISOString();
|
|
79
|
+
row.phases = row.phases.map(p => ({ ...p, status: p.status === 'done' ? 'done' : row.status }));
|
|
80
|
+
for (const a of agents.values()) if (a.status === 'running') Object.assign(a, { status: row.status, endedAt: row.endedAt });
|
|
81
|
+
}
|
|
82
|
+
// Report only changed children, even though the CLI resends the whole snapshot.
|
|
83
|
+
for (const a of agents.values()) {
|
|
84
|
+
const signature = JSON.stringify(a);
|
|
85
|
+
if (run.sent?.get(a.id) !== signature) reportAgent(a);
|
|
86
|
+
}
|
|
87
|
+
// Signatures stay outside the record that travels on the wire.
|
|
88
|
+
run.sent ??= new Map();
|
|
89
|
+
for (const key of run.sent.keys()) if (!agents.has(key)) run.sent.delete(key);
|
|
90
|
+
for (const a of agents.values()) run.sent.set(a.id, JSON.stringify(a));
|
|
91
|
+
if (before !== JSON.stringify(row) || ev.subtype === 'task_started') report({ ...row, launcherId: run.tool });
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
package/bin/lib/cliLimit.mjs
CHANGED
|
@@ -19,6 +19,11 @@ import { scrubCut } from './uplinkScrub.mjs';
|
|
|
19
19
|
* attached, and is a perfectly survivable second-best.
|
|
20
20
|
*/
|
|
21
21
|
const LIMIT_PHRASES = [
|
|
22
|
+
// Installed codex-cli 0.166.0 native binary, strings (2026-11-06):
|
|
23
|
+
// ~/.codex/packages/standalone/releases/0.166.0-x86_64-unknown-linux-musl/bin/codex
|
|
24
|
+
// Core error display: plain, model-specific ("for ..."), Plus, credits,
|
|
25
|
+
// and admin-request variants all share this exact literal prefix.
|
|
26
|
+
/you've hit your usage limit/i,
|
|
22
27
|
/usage limit reached/i,
|
|
23
28
|
/rate limit/i,
|
|
24
29
|
/you've reached your .* limit/i,
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/** Codex's native application protocol. Keep exec's config and event readers;
|
|
2
|
+
* only conversational build turns change transport. Fenced/structured jobs
|
|
3
|
+
* retain exec rather than translate a permission profile incompletely. */
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { versionAtLeast } from './versionOrder.mjs';
|
|
6
|
+
|
|
7
|
+
export function codexAppArgs(execArgs, version = null) {
|
|
8
|
+
if (version && !versionAtLeast(String(version).replace(/^codex-cli\s+/, ''), '0.166.0')) return null;
|
|
9
|
+
if (execArgs.includes('--ignore-user-config') || execArgs.includes('--output-schema')) return null;
|
|
10
|
+
const out = ['app-server', '--listen', 'stdio://'];
|
|
11
|
+
for (let i = 1; i < execArgs.length - 1; i++) {
|
|
12
|
+
const arg = execArgs[i];
|
|
13
|
+
if (arg === 'resume') {
|
|
14
|
+
if (execArgs[++i] === '--last') return null; // never guess another tab's thread
|
|
15
|
+
} else if (arg === '-c' || arg === '--config' || arg === '--enable' || arg === '--disable') {
|
|
16
|
+
out.push(arg, execArgs[++i]);
|
|
17
|
+
} else if (arg === '--model') out.push('-c', `model=${JSON.stringify(execArgs[++i])}`);
|
|
18
|
+
else if (arg === '--cd') i++;
|
|
19
|
+
else if (!['--json', '--skip-git-repo-check'].includes(arg)) return null;
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const snake = s => s.replace(/[A-Z]/g, c => `_${c.toLowerCase()}`);
|
|
25
|
+
export function codexAppItem(item) {
|
|
26
|
+
if (!item?.type) return null;
|
|
27
|
+
const out = Object.fromEntries(Object.entries(item).map(([key, value]) => [snake(key), value]));
|
|
28
|
+
out.type = item.type === 'collabAgentToolCall' ? 'collab_tool_call' : snake(item.type);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A response means the runtime accepted input, not that the model acted on
|
|
33
|
+
* it. clientUserMessageId is retained in native history for restart dedup.
|
|
34
|
+
* A completion racing a steer drains its RPC before closing stdin. */
|
|
35
|
+
export function createCodexInput(child, { cwd, threadId = null, onTaken = () => {}, onThread = () => {},
|
|
36
|
+
onEvent = () => {}, onFailure = () => {}, onContext = null, rpcTimeoutMs = 30_000 } = {}) {
|
|
37
|
+
let seq = 0, ready = false, active = null, starting = false, ended = false, broken = false;
|
|
38
|
+
let usage = null, previousUsage = null, lastContext = null;
|
|
39
|
+
const requests = new Map(), queued = [], sent = new Set(), seen = new Set(), completed = new Set();
|
|
40
|
+
const write = value => {
|
|
41
|
+
if (ended || broken || child.stdin.destroyed || child.stdin.writableEnded) return false;
|
|
42
|
+
try { child.stdin.write(JSON.stringify(value) + '\n'); return true; }
|
|
43
|
+
catch { broken = true; return false; }
|
|
44
|
+
};
|
|
45
|
+
const rpc = (method, params, done) => {
|
|
46
|
+
const id = ++seq;
|
|
47
|
+
const timer = setTimeout(() => {
|
|
48
|
+
requests.delete(id);
|
|
49
|
+
fail(`Codex did not acknowledge ${method} within ${rpcTimeoutMs / 1000} seconds. Unacknowledged input remains queued.`);
|
|
50
|
+
}, rpcTimeoutMs);
|
|
51
|
+
timer.unref?.();
|
|
52
|
+
requests.set(id, { method, done, timer });
|
|
53
|
+
if (!write({ id, method, params })) { clearTimeout(timer); requests.delete(id); return false; }
|
|
54
|
+
return true;
|
|
55
|
+
};
|
|
56
|
+
const end = () => { if (!ended) {
|
|
57
|
+
ended = true;
|
|
58
|
+
for (const request of requests.values()) clearTimeout(request.timer);
|
|
59
|
+
requests.clear(); child.stdin.end();
|
|
60
|
+
} };
|
|
61
|
+
const finishUsage = () => {
|
|
62
|
+
if (usage) onEvent({ type: 'turn.completed', usage });
|
|
63
|
+
usage = null;
|
|
64
|
+
};
|
|
65
|
+
const fail = message => {
|
|
66
|
+
if (ended) return;
|
|
67
|
+
finishUsage();
|
|
68
|
+
onFailure();
|
|
69
|
+
onEvent({ type: 'turn.failed', error: { message } });
|
|
70
|
+
end();
|
|
71
|
+
};
|
|
72
|
+
const inflight = () => [...requests.values()].some(r => r.method.startsWith('turn/'));
|
|
73
|
+
const pump = () => {
|
|
74
|
+
if (!ready || ended || broken || starting || inflight()) return;
|
|
75
|
+
while (queued.length && seen.has(queued[0].id)) onTaken(queued.shift().id);
|
|
76
|
+
const next = queued.shift();
|
|
77
|
+
if (!next) return;
|
|
78
|
+
const method = active ? 'turn/steer' : 'turn/start';
|
|
79
|
+
starting = !active;
|
|
80
|
+
const expected = active;
|
|
81
|
+
const params = { threadId, input: [{ type: 'text', text: next.prompt }], clientUserMessageId: next.id,
|
|
82
|
+
...(active ? { expectedTurnId: active } : {}) };
|
|
83
|
+
if (!rpc(method, params, response => {
|
|
84
|
+
starting = false;
|
|
85
|
+
if (response.error) {
|
|
86
|
+
// The run can finish while this RPC is in the pipe. Only a measured
|
|
87
|
+
// completed turn permits a new native turn with the same input id.
|
|
88
|
+
if (method === 'turn/steer' && active === null) queued.unshift(next);
|
|
89
|
+
else if (method === 'turn/start') { fail(response.error.message || 'Codex could not start the turn.'); return; }
|
|
90
|
+
else onEvent({ type: 'error', message: response.error.message || 'Codex could not accept the follow-up; it remains queued.' });
|
|
91
|
+
} else {
|
|
92
|
+
seen.add(next.id);
|
|
93
|
+
onTaken(next.id);
|
|
94
|
+
if (method === 'turn/start' && response.result?.turn?.status === 'inProgress' && !completed.has(response.result.turn.id)) active = response.result.turn.id;
|
|
95
|
+
// A steer response must never resurrect its completed predecessor.
|
|
96
|
+
if (method === 'turn/steer' && active && active !== expected) return;
|
|
97
|
+
}
|
|
98
|
+
pump();
|
|
99
|
+
if (!active && !starting && !queued.length && !inflight()) end();
|
|
100
|
+
})) fail('Codex could not receive the complete turn input.');
|
|
101
|
+
};
|
|
102
|
+
child.stdin.on('error', () => { broken = true; onFailure(); });
|
|
103
|
+
return {
|
|
104
|
+
start() {
|
|
105
|
+
rpc('initialize', { clientInfo: { name: 'flowviant', title: 'Flowviant', version: '1' } }, response => {
|
|
106
|
+
if (response.error) { fail(response.error.message); return; }
|
|
107
|
+
write({ method: 'initialized', params: {} });
|
|
108
|
+
rpc(threadId ? 'thread/resume' : 'thread/start', { ...(threadId ? { threadId } : {}), cwd, approvalPolicy: 'never' }, response => {
|
|
109
|
+
if (response.error) { fail(response.error.message); return; }
|
|
110
|
+
const thread = response.result?.thread;
|
|
111
|
+
if (!thread?.id) { fail('Codex returned no conversation id.'); return; }
|
|
112
|
+
threadId = thread.id;
|
|
113
|
+
for (const turn of thread.turns ?? []) for (const item of turn.items ?? [])
|
|
114
|
+
if (item.type === 'userMessage' && item.clientId) seen.add(item.clientId);
|
|
115
|
+
onThread(threadId);
|
|
116
|
+
onEvent({ type: 'thread.started', thread_id: threadId });
|
|
117
|
+
ready = true;
|
|
118
|
+
// Recovery of the initial prompt: continue its existing work rather
|
|
119
|
+
// than repeat the original request already in the native transcript.
|
|
120
|
+
if (queued[0] && seen.has(queued[0].id)) {
|
|
121
|
+
const initial = queued.shift(); onTaken(initial.id);
|
|
122
|
+
queued.unshift({ id: `recovery-${randomUUID()}`, prompt: 'Continue the request already in this conversation. Inspect work in flight before repeating commands.' });
|
|
123
|
+
}
|
|
124
|
+
pump();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
send(id, prompt) {
|
|
129
|
+
if (ended || broken || sent.has(id)) return false;
|
|
130
|
+
sent.add(id);
|
|
131
|
+
queued.push({ id, prompt });
|
|
132
|
+
pump();
|
|
133
|
+
return true;
|
|
134
|
+
},
|
|
135
|
+
event(event) {
|
|
136
|
+
if (ended) return;
|
|
137
|
+
if ('id' in event && !event.method) {
|
|
138
|
+
const request = requests.get(event.id);
|
|
139
|
+
if (request) { clearTimeout(request.timer); requests.delete(event.id); request.done(event); }
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const p = event.params ?? {};
|
|
143
|
+
// Server-originated approval/tool requests cannot hang an unattended
|
|
144
|
+
// process. Return a protocol refusal, never synthesize permission.
|
|
145
|
+
if ('id' in event && event.method) {
|
|
146
|
+
write({ id: event.id, error: { code: -32601, message: 'This unattended conversation cannot answer interactive requests.' } });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (p.threadId && p.threadId !== threadId) return;
|
|
150
|
+
if (event.method === 'turn/started') {
|
|
151
|
+
active = p.turn?.id ?? active;
|
|
152
|
+
onEvent({ type: 'turn.started' });
|
|
153
|
+
}
|
|
154
|
+
if (event.method === 'item/started' || event.method === 'item/completed') {
|
|
155
|
+
const item = codexAppItem(p.item);
|
|
156
|
+
if (item) onEvent({ type: event.method.replace('/', '.'), item });
|
|
157
|
+
}
|
|
158
|
+
if (event.method === 'thread/tokenUsage/updated') {
|
|
159
|
+
const last = p.tokenUsage?.last;
|
|
160
|
+
const fingerprint = JSON.stringify(p.tokenUsage?.total);
|
|
161
|
+
if (last && (active || starting) && fingerprint !== previousUsage) {
|
|
162
|
+
previousUsage = fingerprint;
|
|
163
|
+
usage ??= { input_tokens: 0, output_tokens: 0, cached_input_tokens: 0 };
|
|
164
|
+
usage.input_tokens += last.inputTokens ?? 0;
|
|
165
|
+
usage.output_tokens += last.outputTokens ?? 0;
|
|
166
|
+
usage.cached_input_tokens += last.cachedInputTokens ?? 0;
|
|
167
|
+
}
|
|
168
|
+
const window = p.tokenUsage?.modelContextWindow;
|
|
169
|
+
// Last request occupancy, never the thread's cumulative billable spend.
|
|
170
|
+
if (onContext && last && Number.isFinite(window) && window > 0) {
|
|
171
|
+
lastContext = { usedTokens: last.inputTokens + last.outputTokens, windowTokens: window, compactAt: null, compacting: false };
|
|
172
|
+
onContext(lastContext);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (p.item?.type === 'contextCompaction' && lastContext && onContext && ['item/started', 'item/completed'].includes(event.method))
|
|
176
|
+
onContext({ ...lastContext, compacting: event.method === 'item/started' });
|
|
177
|
+
if (event.method === 'error' && p.willRetry !== true) fail(p.error?.message || 'Codex reported an error.');
|
|
178
|
+
if (event.method === 'turn/completed') {
|
|
179
|
+
if (active && p.turn?.id !== active) return;
|
|
180
|
+
completed.add(p.turn?.id);
|
|
181
|
+
active = null;
|
|
182
|
+
finishUsage();
|
|
183
|
+
if (p.turn?.status === 'failed') { fail(p.turn.error?.message || 'Codex failed the turn.'); return; }
|
|
184
|
+
if (p.turn?.status === 'interrupted') { fail('Codex interrupted the turn.'); return; }
|
|
185
|
+
pump();
|
|
186
|
+
if (!starting && !queued.length && !inflight()) end();
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
close() { ended = true; for (const request of requests.values()) clearTimeout(request.timer); requests.clear(); queued.length = 0; },
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -27,7 +27,9 @@ export function codexCollabUpdates(item) {
|
|
|
27
27
|
const receiver = item.receiver_agents?.find(a => a.thread_id === id);
|
|
28
28
|
const status = statusOf(state?.status ?? state);
|
|
29
29
|
const message = state?.message ?? (typeof state?.completed === 'string' ? state.completed : null);
|
|
30
|
-
|
|
30
|
+
// The spawning thread is the child's parent; the reader names the lead's 'lead'.
|
|
31
|
+
const parent = tool === 'spawn_agent' && CODEX_CHILD_THREAD_RE.test(item.sender_thread_id ?? '') ? { parent: item.sender_thread_id } : {};
|
|
32
|
+
return [{ id, ...parent, ...(tool === 'spawn_agent' ? { ...(item.id ? { callId: item.id } : {}), ...(typeof item.prompt === 'string' ? { brief: item.prompt } : {}),
|
|
31
33
|
...(receiver?.agent_path || receiver?.agent_nickname || item.title ? { title: receiver?.agent_path ?? receiver?.agent_nickname ?? item.title } : {}),
|
|
32
34
|
...(receiver?.agent_role || item.agent_type ? { kind: receiver?.agent_role ?? item.agent_type } : {}),
|
|
33
35
|
} : {}),
|
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
* rust-v0.160.0: exec_events.rs CollabToolCallItem; core/agent/child_config.rs
|
|
5
5
|
* reapplies the live turn's approval and permission profile after role config.
|
|
6
6
|
* V2 rollout shapes measured on codex-cli 0.163.0 (codexSubagents.test.mjs).
|
|
7
|
+
*
|
|
8
|
+
* A child's own rollout says what it ran on (2026-11-06, S4.1): its newest
|
|
9
|
+
* `turn_context` names the model and effort that turn resolved, never what
|
|
10
|
+
* the spawn asked for; `session_meta`'s thread_spawn names the thread that
|
|
11
|
+
* spawned it (the lead, or another child: nesting); `task_complete` carries
|
|
12
|
+
* its final message, the result. `knownTargets` are a resumed agent's
|
|
13
|
+
* children from earlier turns (workAgentResume.mjs), name → thread, so a
|
|
14
|
+
* follow-up by task path reaches the record it continues.
|
|
7
15
|
*/
|
|
8
16
|
import { open } from 'node:fs/promises';
|
|
9
17
|
import { codexRolloutFile } from './runtimeLimits.mjs';
|
|
@@ -13,6 +21,9 @@ import { codexCollabUpdates, codexSpawnResult, statusOf } from './codexSubagentE
|
|
|
13
21
|
import { NATIVE_AGENT_TOOLS } from './nativeAgentTools.mjs';
|
|
14
22
|
import { isSealedMessage } from './orchestratorNative.mjs';
|
|
15
23
|
import { SUBAGENT_CAP } from './subagentBounds.mjs';
|
|
24
|
+
import { turnModelOf } from './turnModel.mjs';
|
|
25
|
+
|
|
26
|
+
const EFFORT_RE = /^[a-z]{1,24}$/;
|
|
16
27
|
|
|
17
28
|
const CODEX = NATIVE_AGENT_TOOLS.codex;
|
|
18
29
|
// A rollout shows a superset of what the gate sees: send_message and the
|
|
@@ -22,7 +33,7 @@ const COLLAB_CALLS = [...CODEX.spawn, ...CODEX.followup, ...CODEX.message, ...CO
|
|
|
22
33
|
const RECEIPT_CALLS = [...CODEX.spawn, ...CODEX.followup];
|
|
23
34
|
|
|
24
35
|
export function createCodexSubagentReader({ since = Date.now(), env = process.env, onSubagent, onActivity, onToolEvent, onSpawnResult,
|
|
25
|
-
findFile = codexRolloutFile } = {}) {
|
|
36
|
+
findFile = codexRolloutFile, knownTargets = null } = {}) {
|
|
26
37
|
const readers = new Map();
|
|
27
38
|
const calls = new Map();
|
|
28
39
|
const targets = new Map();
|
|
@@ -33,11 +44,12 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
|
|
|
33
44
|
let chain = Promise.resolve();
|
|
34
45
|
const report = update => {
|
|
35
46
|
if (!update?.id || !CODEX_CHILD_THREAD_RE.test(update.id)) return;
|
|
36
|
-
// The root thread plus the run's children.
|
|
37
|
-
|
|
38
|
-
if (!readers.has(update.id))
|
|
47
|
+
// The root thread plus the run's children are followed. A child past the
|
|
48
|
+
// cap is still reported, unfollowed: the relay counts it as not recorded.
|
|
49
|
+
if (!readers.has(update.id) && readers.size < SUBAGENT_CAP + 1)
|
|
39
50
|
readers.set(update.id, { offset: null, rest: Buffer.alloc(0), seen: new Set(), live: false });
|
|
40
51
|
const { message, ...record } = update;
|
|
52
|
+
if (record.parent && record.parent === root) record.parent = 'lead';
|
|
41
53
|
onSubagent?.(record);
|
|
42
54
|
};
|
|
43
55
|
const rememberTarget = (path, id) => {
|
|
@@ -45,6 +57,7 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
|
|
|
45
57
|
if (targets.size > 128) targets.delete(targets.keys().next().value);
|
|
46
58
|
};
|
|
47
59
|
const targetId = value => targets.get(value) ?? (CODEX_CHILD_THREAD_RE.test(value ?? '') ? value : null);
|
|
60
|
+
if (knownTargets) for (const [name, id] of knownTargets) if (CODEX_CHILD_THREAD_RE.test(id ?? '')) rememberTarget(name, id);
|
|
48
61
|
|
|
49
62
|
function observe(ev, owner) {
|
|
50
63
|
const at = Date.parse(ev?.timestamp ?? '');
|
|
@@ -65,10 +78,18 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
|
|
|
65
78
|
if (src) {
|
|
66
79
|
const title = src.agent_path ?? src.agent_nickname;
|
|
67
80
|
if (src.agent_path) rememberTarget(src.agent_path, owner);
|
|
68
|
-
|
|
81
|
+
const parent = CODEX_CHILD_THREAD_RE.test(src.parent_thread_id ?? '') ? { parent: src.parent_thread_id } : {};
|
|
82
|
+
report({ id: owner, ...(title ? { title } : {}), ...(src.agent_role ? { kind: src.agent_role } : {}), ...parent });
|
|
69
83
|
}
|
|
70
84
|
return;
|
|
71
85
|
}
|
|
86
|
+
if (ev.type === 'turn_context' && owner !== root) {
|
|
87
|
+
const model = turnModelOf(p.model);
|
|
88
|
+
const effort = p.effort ?? p.collaboration_mode?.settings?.reasoning_effort;
|
|
89
|
+
const brain = { ...(model ? { model } : {}), ...(typeof effort === 'string' && EFFORT_RE.test(effort) ? { effort } : {}) };
|
|
90
|
+
if (Object.keys(brain).length) report({ id: owner, ...brain });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
72
93
|
if (ev.type === 'response_item' && p.type === 'function_call') {
|
|
73
94
|
const name = String(p.name ?? '').split('.').at(-1);
|
|
74
95
|
if (owner !== root && ['exec_command', 'shell_command', 'shell'].includes(name)) {
|
|
@@ -126,7 +147,7 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
|
|
|
126
147
|
// A V2 message is ciphertext (orchestratorNative.mjs `isSealedMessage`):
|
|
127
148
|
// its words are unknown, so the child carries no brief rather than that.
|
|
128
149
|
const sealed = isSealedMessage(call.namespace ? `${call.namespace}.${call.name}` : call.name, args.message);
|
|
129
|
-
report({ id, kind: args.agent_type ?? '', title, ...(sealed ? {} : { brief: args.message ?? '' }), status: 'running', now: '' });
|
|
150
|
+
report({ id, kind: args.agent_type ?? '', title, ...(sealed ? {} : { brief: args.message ?? '' }), status: 'running', now: '', parent: owner });
|
|
130
151
|
return;
|
|
131
152
|
}
|
|
132
153
|
if (owner !== root && ev.type === 'response_item' && p.type === 'custom_tool_call') {
|
|
@@ -167,7 +188,7 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
|
|
|
167
188
|
return;
|
|
168
189
|
}
|
|
169
190
|
if (p.type === 'collab_agent_spawn_end' && p.new_thread_id) {
|
|
170
|
-
report({ id: p.new_thread_id, brief: p.prompt ?? '', kind: p.new_agent_role ?? '', title: p.new_agent_nickname ?? '',
|
|
191
|
+
report({ id: p.new_thread_id, brief: p.prompt ?? '', kind: p.new_agent_role ?? '', title: p.new_agent_nickname ?? '', parent: owner,
|
|
171
192
|
status: statusOf(typeof p.status === 'object' ? Object.keys(p.status)[0]?.toLowerCase() : p.status) ?? 'running' });
|
|
172
193
|
}
|
|
173
194
|
if (owner === root) return;
|
|
@@ -181,7 +202,7 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
|
|
|
181
202
|
onToolEvent?.('Write', { file_path: path, content: change?.add?.content ?? '' }, { agentId: owner, id: p.call_id });
|
|
182
203
|
}
|
|
183
204
|
if (p.type === 'task_started' || p.type === 'turn_started') report({ id: owner, status: 'running' });
|
|
184
|
-
if (p.type === 'task_complete' || p.type === 'turn_complete') report({ id: owner, status: 'done', ...(p.last_agent_message ? { now: p.last_agent_message } : {}) });
|
|
205
|
+
if (p.type === 'task_complete' || p.type === 'turn_complete') report({ id: owner, status: 'done', ...(p.last_agent_message ? { now: p.last_agent_message, result: p.last_agent_message } : {}) });
|
|
185
206
|
if (p.type === 'turn_aborted') report({ id: owner, status: 'stopped' });
|
|
186
207
|
}
|
|
187
208
|
|
|
@@ -199,7 +220,7 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
|
|
|
199
220
|
try {
|
|
200
221
|
const ev = JSON.parse(line);
|
|
201
222
|
if (initial && id !== root) initialEvents.push(ev);
|
|
202
|
-
else if (id === root || state.live || ev.type === 'session_meta') observe(ev, id);
|
|
223
|
+
else if (id === root || state.live || ev.type === 'session_meta' || ev.type === 'turn_context') observe(ev, id);
|
|
203
224
|
else if (ev.type === 'event_msg' && ['task_started', 'turn_started'].includes(ev.payload?.type)) {
|
|
204
225
|
state.live = true; observe(ev, id);
|
|
205
226
|
}
|
|
@@ -210,6 +231,10 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
|
|
|
210
231
|
// native turn belongs to this card; inherited history is never replayed.
|
|
211
232
|
for (const ev of initialEvents) if (ev.type === 'session_meta') observe(ev, id);
|
|
212
233
|
const start = initialEvents.findLastIndex(ev => ev.type === 'event_msg' && ['task_started', 'turn_started'].includes(ev.payload?.type));
|
|
234
|
+
// The newest context is this turn's, wherever the CLI wrote it; an
|
|
235
|
+
// older turn's is refused by `since`.
|
|
236
|
+
const context = initialEvents.findLastIndex(ev => ev.type === 'turn_context');
|
|
237
|
+
if (context >= 0 && (start < 0 || context < start)) observe(initialEvents[context], id);
|
|
213
238
|
if (start >= 0) {
|
|
214
239
|
state.live = true;
|
|
215
240
|
for (const ev of initialEvents.slice(start)) observe(ev, id);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** One output vocabulary for Terminal, capture and agent conversations. */
|
|
2
|
+
export const CONVERSATION_OUTPUT = `CONVERSATION OUTPUT: both Terminal and agent chats render Markdown tables, highlighted code and LaTeX (use $$ delimiters for math; shell/console blocks use the reading font, source code uses monospace).
|
|
3
|
+
Printed source fences have Copy and Download; add file=script.py after the language to name the downloaded code file.
|
|
4
|
+
For a diagram, use a mermaid fence. For an illustration or an interactive view, use a flowviant-svg or flowviant-html fence. Those explicit fences render in an opaque sandbox: inline CSS/JS and data: images work; network requests, external scripts, forms, popups and access to the app do not. Ordinary html/svg/js fences remain printed source code. Keep inline visuals under 100,000 characters. Prefer retained artifacts for large, downloadable or reusable outputs.
|
|
5
|
+
For a native data card, use a flowviant-card fence containing JSON. Every card has type, title and optional description, source (absolute http/https URL), asOf. Supported shapes:
|
|
6
|
+
- weather, places, map, itinerary, products, comparison, options, links, sports, images, voting, dashboard, design-system, walkthrough: items [{title, description?, value?, detail?, url?}]. Map/image URLs are explicit links; no remote image loads on view.
|
|
7
|
+
- tasks: items [{title, description?, checked?}]. Checkboxes are local to the view, not changes to the project board.
|
|
8
|
+
- flashcards: items [{title, answer}].
|
|
9
|
+
- recipe: servings (1-100), ingredients [{name, quantity (number), unit?}], steps [string]. Servings scale ingredient quantities.
|
|
10
|
+
- quiz: questions [{question, options:[string], answer:zero-based-index, explanation?}].
|
|
11
|
+
- translation or email: body, optional original, to, subject. Copy is available; sending is never authorized by a card.
|
|
12
|
+
- chart: kind (line, bar or scatter), points [{label, y:number, x?:number}], optional xLabel and yLabel. Include numeric x for scatter plots.
|
|
13
|
+
Cards display supplied facts, never fetch live data: use your available tools for current facts, name the source and observation time, and retain uncertainty. Do not fabricate weather, places, prices, scores or voting information to fill a card. A card cannot authorize an action. Use the existing flowviant-ask format when the person must answer a question.
|
|
14
|
+
Kept documents, slides, spreadsheets, designs, whiteboards, trackers, dashboards, animations, design systems and web pages belong in .flowviant/artifacts/ using the supported artifact formats. Generate actual DOCX/PPTX/XLSX/PDF/CSV/image/video files when requested; the app previews supported formats and offers downloads. Do not claim an Office editor, external integration, live data lookup or file generation happened just because you printed a card.`;
|