@agent-compose/sdk 0.7.0 → 0.8.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/README.md +66 -39
- package/dist/agent/__tests__/runtime-json-schema.test.d.ts +10 -0
- package/dist/agent/agent-context.d.ts +21 -1
- package/dist/agent/agent-loop.d.ts +24 -1
- package/dist/client.d.ts +338 -534
- package/dist/directives.d.ts +112 -0
- package/dist/display.d.ts +242 -0
- package/dist/errors.d.ts +24 -1
- package/dist/index.d.ts +24 -12
- package/dist/index.js +3545 -1667
- package/dist/pause/wrappers.d.ts +31 -9
- package/dist/runtimes/_acp-client.d.ts +46 -1
- package/dist/runtimes/_cli-agent.d.ts +49 -4
- package/dist/runtimes/_jsonl-guard.d.ts +103 -0
- package/dist/runtimes/amp.d.ts +2 -2
- package/dist/runtimes/claude-code.d.ts +59 -0
- package/dist/runtimes/claude-code.test.d.ts +14 -0
- package/dist/runtimes/claude.d.ts +16 -0
- package/dist/runtimes/claude.test.d.ts +8 -0
- package/dist/runtimes/codex.d.ts +9 -3
- package/dist/runtimes/cursor.d.ts +2 -2
- package/dist/runtimes/droid.d.ts +2 -2
- package/dist/runtimes/jsonl-guard.test.d.ts +19 -0
- package/dist/runtimes/openai-desktop.js +2691 -864
- package/dist/runtimes/opencode.d.ts +2 -2
- package/dist/runtimes/vercel.js +12 -1
- package/dist/sandbox/devbox.d.ts +42 -0
- package/dist/sandbox/exec-stream.d.ts +14 -0
- package/dist/sandbox/network-policy.d.ts +100 -0
- package/dist/sandbox/provider-def.d.ts +79 -0
- package/dist/sandbox/providers/desktop.d.ts +10 -0
- package/dist/sandbox/providers/e2b.d.ts +17 -0
- package/dist/sandbox/providers/local.d.ts +11 -0
- package/dist/sandbox/providers/vercel.d.ts +18 -0
- package/dist/sandbox/registry.d.ts +45 -0
- package/dist/sandbox/sizes.d.ts +68 -0
- package/dist/sandbox.d.ts +24 -299
- package/dist/step-invocation/__tests__/foreground-recovery.test.d.ts +1 -0
- package/dist/step-invocation/invoker.d.ts +10 -0
- package/dist/step-invocation/protocol.d.ts +5 -0
- package/dist/types/api-compliance.d.ts +71 -0
- package/dist/types/api-conversations.d.ts +492 -0
- package/dist/types/api-factory.d.ts +309 -0
- package/dist/types/api-projects.d.ts +131 -0
- package/dist/types/api-runs.d.ts +377 -0
- package/dist/types/api-scopes.d.ts +102 -0
- package/dist/types/conversation-stream.d.ts +191 -0
- package/dist/types/execution-context.d.ts +12 -2
- package/dist/types/protocol.d.ts +30 -1
- package/dist/types/sandbox-environment.d.ts +8 -5
- package/dist/types/sandbox.d.ts +74 -4
- package/dist/types/workflow-metadata.d.ts +33 -8
- package/dist/types/workflow-plan.d.ts +10 -0
- package/dist/types/workflow.d.ts +18 -205
- package/dist/utils/bundler.d.ts +12 -1
- package/dist/workflow-steps/index.d.ts +1 -1
- package/dist/workflow-steps/observability.d.ts +8 -1
- package/dist/workflow-steps/runner.d.ts +3 -3
- package/dist/workflow-steps/step.d.ts +15 -1
- package/dist/workflow-steps/types.d.ts +19 -5
- package/dist/workflow-steps/workflow.d.ts +22 -1
- package/dist/workflows/engine.d.ts +3 -2
- package/dist/workflows/invoke-child.d.ts +2 -2
- package/package.json +1 -1
- package/src/agent/agent-context.ts +186 -3
- package/src/agent/agent-loop.ts +31 -2
- package/src/client.ts +909 -621
- package/src/directives.ts +184 -0
- package/src/display.ts +788 -0
- package/src/errors.ts +39 -0
- package/src/index.ts +104 -10
- package/src/pause/wrappers.ts +44 -9
- package/src/runtimes/_acp-client.ts +72 -3
- package/src/runtimes/_cli-agent.ts +159 -36
- package/src/runtimes/_jsonl-guard.ts +219 -0
- package/src/runtimes/claude-code.ts +246 -0
- package/src/runtimes/claude.ts +32 -2
- package/src/runtimes/codex.ts +55 -3
- package/src/runtimes/openai-desktop.ts +59 -14
- package/src/sandbox/devbox.ts +48 -0
- package/src/sandbox/exec-stream.ts +48 -0
- package/src/sandbox/network-policy.ts +181 -0
- package/src/sandbox/provider-def.ts +94 -0
- package/src/sandbox/providers/desktop.ts +57 -0
- package/src/sandbox/providers/e2b.ts +354 -0
- package/src/sandbox/providers/local.ts +106 -0
- package/src/sandbox/providers/vercel.ts +331 -0
- package/src/sandbox/registry.ts +198 -0
- package/src/sandbox/sizes.ts +95 -0
- package/src/sandbox.ts +59 -1275
- package/src/step-invocation/invoker.ts +151 -28
- package/src/step-invocation/protocol.ts +8 -0
- package/src/types/api-compliance.ts +79 -0
- package/src/types/api-conversations.ts +522 -0
- package/src/types/api-factory.ts +336 -0
- package/src/types/api-projects.ts +140 -0
- package/src/types/api-runs.ts +412 -0
- package/src/types/api-scopes.ts +102 -0
- package/src/types/conversation-stream.ts +231 -0
- package/src/types/execution-context.ts +10 -2
- package/src/types/protocol.ts +33 -0
- package/src/types/sandbox-environment.ts +28 -9
- package/src/types/sandbox.ts +73 -4
- package/src/types/workflow-metadata.ts +35 -8
- package/src/types/workflow-plan.ts +11 -0
- package/src/types/workflow.ts +25 -292
- package/src/utils/bundler.ts +32 -5
- package/src/utils/errors.ts +16 -1
- package/src/workflow-steps/index.ts +1 -0
- package/src/workflow-steps/observability.ts +19 -8
- package/src/workflow-steps/runner.ts +4 -4
- package/src/workflow-steps/step.ts +49 -1
- package/src/workflow-steps/types.ts +20 -5
- package/src/workflow-steps/workflow.ts +22 -1
- package/src/workflows/engine.ts +3 -2
- package/src/workflows/invoke-child.ts +2 -2
package/src/display.ts
ADDED
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The display palette (ADR-0040 §7, CLI-first amendment) — the typed marker
|
|
3
|
+
* contract between `agentc display` and the session executors.
|
|
4
|
+
*
|
|
5
|
+
* A native-harness session (Claude Code / codex running in-sandbox, or the
|
|
6
|
+
* local bridge daemon) has no typed platform tools — its platform surface is
|
|
7
|
+
* the `agentc` CLI. To keep RENDERING parity with platform sessions, the
|
|
8
|
+
* model is given an explicit palette of display commands
|
|
9
|
+
* (`agentc display run|changes|document|plan|image|table|chart|ask …`) whose stdout is a one-line
|
|
10
|
+
* TYPED MARKER. The executors (server cloud-executor, CLI bridge daemon)
|
|
11
|
+
* detect these markers in tool results and persist the SAME `MessagePart`
|
|
12
|
+
* shapes the platform harness's tools produce — so the dashboard renders the
|
|
13
|
+
* same rich cards (run card, reviewed-changes diff, document tile, plan
|
|
14
|
+
* checklist) with NO new renderer vocabulary beyond the additive
|
|
15
|
+
* `display_*` tool names.
|
|
16
|
+
*
|
|
17
|
+
* Known agentc invocations are ALSO auto-promoted without an explicit
|
|
18
|
+
* display call: `agentc run dispatch` → the dispatched-run card (the exact
|
|
19
|
+
* `dispatch_workflow` part shapes turn.ts persists), `agentc run get` → the
|
|
20
|
+
* run card, `agentc files read` → the document tile. Claude Code's own
|
|
21
|
+
* TodoWrite tool additionally forwards as a durable `plan` part (the same
|
|
22
|
+
* shape the ACP daemon posts), so converged sessions get the plan checklist
|
|
23
|
+
* for free.
|
|
24
|
+
*
|
|
25
|
+
* Everything here is pure and executor-agnostic: the promoter takes the
|
|
26
|
+
* runtime's tool_use / tool_result events and returns plain part-shaped
|
|
27
|
+
* objects that are structurally identical to the server's `MessagePart`
|
|
28
|
+
* union (pinned by tests on both sides). Promotion is strictly a REWRITE of
|
|
29
|
+
* the pair's shape — it never suppresses data: an unrecognized command,
|
|
30
|
+
* unparseable arguments, or a missing marker all fall back to the raw pair.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { REVISION_SELECTOR_RE } from "./directives.js";
|
|
34
|
+
|
|
35
|
+
// ── Marker contract ──────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
export const DISPLAY_MARKER_KEY = "acDisplay";
|
|
38
|
+
export const DISPLAY_MARKER_VERSION = "v1";
|
|
39
|
+
|
|
40
|
+
export type DisplayPlanStatus = "pending" | "in_progress" | "completed";
|
|
41
|
+
export interface DisplayPlanEntry {
|
|
42
|
+
content: string;
|
|
43
|
+
status: DisplayPlanStatus;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Plan bounds — mirror the server's BridgePartSchema (entries ≤ 64,
|
|
47
|
+
* content ≤ 1000) so a marker-built plan part is always postable. */
|
|
48
|
+
export const PLAN_MAX_ENTRIES = 64;
|
|
49
|
+
export const PLAN_ENTRY_MAX_CHARS = 1_000;
|
|
50
|
+
|
|
51
|
+
export type DisplayTableCell = string | number | boolean | null;
|
|
52
|
+
export interface DisplayTableColumn { key: string; label: string }
|
|
53
|
+
export interface DisplayTableData {
|
|
54
|
+
columns: DisplayTableColumn[];
|
|
55
|
+
rows: Array<Record<string, DisplayTableCell>>;
|
|
56
|
+
truncated: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type DisplayChartKind = "bar" | "line";
|
|
60
|
+
export interface DisplayChartPoint { x: string | number; y: number }
|
|
61
|
+
export interface DisplayChartSeries { label: string; points: DisplayChartPoint[] }
|
|
62
|
+
|
|
63
|
+
export interface DisplayAskOption { id: string; label: string }
|
|
64
|
+
|
|
65
|
+
export type DisplayMarker =
|
|
66
|
+
| { kind: "run"; runId: string }
|
|
67
|
+
| { kind: "changes"; runId: string; factorySlug?: string; endedAt?: string | null }
|
|
68
|
+
| { kind: "document"; path: string; factorySlug?: string; preview?: string | null }
|
|
69
|
+
| { kind: "plan"; entries: DisplayPlanEntry[] }
|
|
70
|
+
| { kind: "image"; path: string; factorySlug?: string }
|
|
71
|
+
| { kind: "table"; columns: DisplayTableColumn[]; rows: Array<Record<string, DisplayTableCell>>; truncated: boolean }
|
|
72
|
+
| { kind: "chart"; chartKind: DisplayChartKind; series: DisplayChartSeries[]; xLabel?: string; yLabel?: string }
|
|
73
|
+
| { kind: "ask"; askId: string; prompt: string; options?: DisplayAskOption[]; freeText: boolean };
|
|
74
|
+
|
|
75
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
76
|
+
|
|
77
|
+
const PLAN_STATUSES: ReadonlySet<string> = new Set(["pending", "in_progress", "completed"]);
|
|
78
|
+
|
|
79
|
+
/** Clamp raw plan entries to the marker/part bounds; invalid rows drop. */
|
|
80
|
+
export function clampPlanEntries(raw: unknown): DisplayPlanEntry[] {
|
|
81
|
+
if (!Array.isArray(raw)) return [];
|
|
82
|
+
const entries: DisplayPlanEntry[] = [];
|
|
83
|
+
for (const e of raw) {
|
|
84
|
+
if (typeof e !== "object" || e === null) continue;
|
|
85
|
+
const { content, status } = e as { content?: unknown; status?: unknown };
|
|
86
|
+
if (typeof content !== "string" || content.trim().length === 0) continue;
|
|
87
|
+
if (typeof status !== "string" || !PLAN_STATUSES.has(status)) continue;
|
|
88
|
+
entries.push({ content: content.slice(0, PLAN_ENTRY_MAX_CHARS), status: status as DisplayPlanStatus });
|
|
89
|
+
if (entries.length >= PLAN_MAX_ENTRIES) break;
|
|
90
|
+
}
|
|
91
|
+
return entries;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Table / chart / ask clamps ───────────────────────────────────────────────
|
|
95
|
+
// Shared by the CLI (before it serializes the marker) and the marker parser
|
|
96
|
+
// (so a hand-forged oversized marker can never bloat a persisted part) —
|
|
97
|
+
// the `clampPlanEntries` pattern.
|
|
98
|
+
|
|
99
|
+
/** Table bounds — mirror the pinned `display_table` output shape. */
|
|
100
|
+
export const TABLE_MAX_COLUMNS = 12;
|
|
101
|
+
export const TABLE_MAX_ROWS = 100;
|
|
102
|
+
export const TABLE_CELL_MAX_CHARS = 200;
|
|
103
|
+
|
|
104
|
+
/** Drive-path bound for image markers (mirrors the directive path cap). */
|
|
105
|
+
const IMAGE_PATH_MAX_CHARS = 512;
|
|
106
|
+
|
|
107
|
+
function clampCellString(s: string): string {
|
|
108
|
+
return s.length > TABLE_CELL_MAX_CHARS ? `${s.slice(0, TABLE_CELL_MAX_CHARS - 1)}…` : s;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function clampCell(value: unknown): DisplayTableCell {
|
|
112
|
+
if (typeof value === "string") return clampCellString(value);
|
|
113
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
114
|
+
if (typeof value === "boolean") return value;
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Clamp raw table input to the marker/part bounds. Accepts an array of flat
|
|
120
|
+
* objects (columns inferred from the key union, first-seen order) or a
|
|
121
|
+
* `{ columns, rows }` object. Null when nothing tabular survives — the CLI
|
|
122
|
+
* hard-errors, the parser treats the marker as not-a-marker.
|
|
123
|
+
*/
|
|
124
|
+
export function clampTableData(raw: unknown): DisplayTableData | null {
|
|
125
|
+
let rawColumns: unknown;
|
|
126
|
+
let rawRows: unknown;
|
|
127
|
+
if (Array.isArray(raw)) {
|
|
128
|
+
rawRows = raw;
|
|
129
|
+
} else if (typeof raw === "object" && raw !== null) {
|
|
130
|
+
rawColumns = (raw as { columns?: unknown }).columns;
|
|
131
|
+
rawRows = (raw as { rows?: unknown }).rows;
|
|
132
|
+
} else {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
if (!Array.isArray(rawRows)) return null;
|
|
136
|
+
|
|
137
|
+
// Columns: explicit `{ key, label }` list, or the key union of the rows
|
|
138
|
+
// in first-seen order. `sourceKey` (unclamped) reads the raw rows; the
|
|
139
|
+
// clamped `key` is what the part carries and what rows are re-keyed to.
|
|
140
|
+
let truncated = false;
|
|
141
|
+
const columns: Array<{ sourceKey: string; key: string; label: string }> = [];
|
|
142
|
+
const seenKeys = new Set<string>();
|
|
143
|
+
const pushColumn = (key: string, label: string): void => {
|
|
144
|
+
if (seenKeys.has(key)) return;
|
|
145
|
+
seenKeys.add(key);
|
|
146
|
+
if (columns.length >= TABLE_MAX_COLUMNS) { truncated = true; return; }
|
|
147
|
+
columns.push({ sourceKey: key, key: clampCellString(key), label: clampCellString(label) });
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (rawColumns !== undefined) {
|
|
151
|
+
if (!Array.isArray(rawColumns)) return null;
|
|
152
|
+
for (const c of rawColumns) {
|
|
153
|
+
if (typeof c !== "object" || c === null) continue;
|
|
154
|
+
const { key, label } = c as { key?: unknown; label?: unknown };
|
|
155
|
+
if (typeof key !== "string" || key.length === 0) continue;
|
|
156
|
+
pushColumn(key, typeof label === "string" && label.length > 0 ? label : key);
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
for (const row of rawRows) {
|
|
160
|
+
if (typeof row !== "object" || row === null || Array.isArray(row)) continue;
|
|
161
|
+
for (const key of Object.keys(row)) pushColumn(key, key);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (columns.length === 0) return null;
|
|
165
|
+
|
|
166
|
+
const rows: Array<Record<string, DisplayTableCell>> = [];
|
|
167
|
+
for (const rawRow of rawRows) {
|
|
168
|
+
if (typeof rawRow !== "object" || rawRow === null || Array.isArray(rawRow)) continue;
|
|
169
|
+
if (rows.length >= TABLE_MAX_ROWS) { truncated = true; break; }
|
|
170
|
+
const row: Record<string, DisplayTableCell> = {};
|
|
171
|
+
for (const c of columns) row[c.key] = clampCell((rawRow as Record<string, unknown>)[c.sourceKey]);
|
|
172
|
+
rows.push(row);
|
|
173
|
+
}
|
|
174
|
+
if (rows.length === 0) return null;
|
|
175
|
+
return { columns: columns.map(({ key, label }) => ({ key, label })), rows, truncated };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Chart bounds — mirror the pinned `display_chart` output shape. */
|
|
179
|
+
export const CHART_MAX_SERIES = 4;
|
|
180
|
+
export const CHART_MAX_POINTS_PER_SERIES = 60;
|
|
181
|
+
export const CHART_LABEL_MAX_CHARS = 40;
|
|
182
|
+
|
|
183
|
+
/** Clamp raw chart series to the marker/part bounds; invalid series and
|
|
184
|
+
* non-finite points drop. Empty result → the CLI hard-errors / the parser
|
|
185
|
+
* rejects the marker. */
|
|
186
|
+
export function clampChartSeries(raw: unknown): DisplayChartSeries[] {
|
|
187
|
+
if (!Array.isArray(raw)) return [];
|
|
188
|
+
const series: DisplayChartSeries[] = [];
|
|
189
|
+
for (const s of raw) {
|
|
190
|
+
if (series.length >= CHART_MAX_SERIES) break;
|
|
191
|
+
if (typeof s !== "object" || s === null) continue;
|
|
192
|
+
const { label, points } = s as { label?: unknown; points?: unknown };
|
|
193
|
+
if (typeof label !== "string" || label.trim().length === 0) continue;
|
|
194
|
+
if (!Array.isArray(points)) continue;
|
|
195
|
+
const clamped: DisplayChartPoint[] = [];
|
|
196
|
+
for (const p of points) {
|
|
197
|
+
if (clamped.length >= CHART_MAX_POINTS_PER_SERIES) break;
|
|
198
|
+
if (typeof p !== "object" || p === null) continue;
|
|
199
|
+
const { x, y } = p as { x?: unknown; y?: unknown };
|
|
200
|
+
if (typeof y !== "number" || !Number.isFinite(y)) continue;
|
|
201
|
+
if (typeof x === "number" && Number.isFinite(x)) clamped.push({ x, y });
|
|
202
|
+
else if (typeof x === "string" && x.length > 0) clamped.push({ x: x.slice(0, CHART_LABEL_MAX_CHARS), y });
|
|
203
|
+
}
|
|
204
|
+
if (clamped.length === 0) continue;
|
|
205
|
+
series.push({ label: label.slice(0, CHART_LABEL_MAX_CHARS), points: clamped });
|
|
206
|
+
}
|
|
207
|
+
return series;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Clamp an axis label to the chart bound; undefined when unusable. */
|
|
211
|
+
export function clampChartAxisLabel(raw: unknown): string | undefined {
|
|
212
|
+
return typeof raw === "string" && raw.trim().length > 0 ? raw.slice(0, CHART_LABEL_MAX_CHARS) : undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Ask bounds — mirror the pinned `display_ask` output shape (the
|
|
216
|
+
* promoter path; the approver directive has its own tighter caps in
|
|
217
|
+
* directives.ts). */
|
|
218
|
+
export const ASK_PROMPT_MAX_CHARS = 1_000;
|
|
219
|
+
export const ASK_MAX_OPTIONS = 8;
|
|
220
|
+
export const ASK_OPTION_ID_MAX_CHARS = 100;
|
|
221
|
+
export const ASK_OPTION_LABEL_MAX_CHARS = 200;
|
|
222
|
+
|
|
223
|
+
/** Normalize + clamp ask options. Accepts `["a","b"]` (id = label) or
|
|
224
|
+
* `[{ id, label }]`; invalid entries drop, duplicate ids drop. */
|
|
225
|
+
export function clampAskOptions(raw: unknown): DisplayAskOption[] {
|
|
226
|
+
if (!Array.isArray(raw)) return [];
|
|
227
|
+
const options: DisplayAskOption[] = [];
|
|
228
|
+
const seen = new Set<string>();
|
|
229
|
+
for (const o of raw) {
|
|
230
|
+
if (options.length >= ASK_MAX_OPTIONS) break;
|
|
231
|
+
let id: string | null = null;
|
|
232
|
+
let label: string | null = null;
|
|
233
|
+
if (typeof o === "string" && o.trim().length > 0) {
|
|
234
|
+
id = o;
|
|
235
|
+
label = o;
|
|
236
|
+
} else if (typeof o === "object" && o !== null) {
|
|
237
|
+
const cand = o as { id?: unknown; label?: unknown };
|
|
238
|
+
if (typeof cand.id === "string" && cand.id.length > 0 && typeof cand.label === "string" && cand.label.length > 0) {
|
|
239
|
+
id = cand.id;
|
|
240
|
+
label = cand.label;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (id === null || label === null) continue;
|
|
244
|
+
const clampedId = id.slice(0, ASK_OPTION_ID_MAX_CHARS);
|
|
245
|
+
if (seen.has(clampedId)) continue;
|
|
246
|
+
seen.add(clampedId);
|
|
247
|
+
options.push({ id: clampedId, label: label.slice(0, ASK_OPTION_LABEL_MAX_CHARS) });
|
|
248
|
+
}
|
|
249
|
+
return options;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** One-line JSON marker — exactly what `agentc display` prints. */
|
|
253
|
+
export function serializeDisplayMarker(marker: DisplayMarker): string {
|
|
254
|
+
return JSON.stringify({ [DISPLAY_MARKER_KEY]: DISPLAY_MARKER_VERSION, ...marker });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Parse one line as a display marker; null when it isn't one (or fails
|
|
258
|
+
* validation — a malformed marker is not a marker). */
|
|
259
|
+
export function parseDisplayMarker(line: string): DisplayMarker | null {
|
|
260
|
+
const trimmed = line.trim();
|
|
261
|
+
if (!trimmed.startsWith("{") || !trimmed.includes(DISPLAY_MARKER_KEY)) return null;
|
|
262
|
+
let obj: Record<string, unknown>;
|
|
263
|
+
try {
|
|
264
|
+
const parsed: unknown = JSON.parse(trimmed);
|
|
265
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
266
|
+
obj = parsed as Record<string, unknown>;
|
|
267
|
+
} catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
if (obj[DISPLAY_MARKER_KEY] !== DISPLAY_MARKER_VERSION) return null;
|
|
271
|
+
const str = (key: string): string | undefined => {
|
|
272
|
+
const v = obj[key];
|
|
273
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
274
|
+
};
|
|
275
|
+
switch (obj.kind) {
|
|
276
|
+
case "run": {
|
|
277
|
+
const runId = str("runId");
|
|
278
|
+
return runId && UUID_RE.test(runId) ? { kind: "run", runId } : null;
|
|
279
|
+
}
|
|
280
|
+
case "changes": {
|
|
281
|
+
const runId = str("runId");
|
|
282
|
+
if (!runId || !UUID_RE.test(runId)) return null;
|
|
283
|
+
return {
|
|
284
|
+
kind: "changes", runId,
|
|
285
|
+
...(str("factorySlug") ? { factorySlug: str("factorySlug") } : {}),
|
|
286
|
+
endedAt: str("endedAt") ?? null,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
case "document": {
|
|
290
|
+
const path = str("path");
|
|
291
|
+
if (!path) return null;
|
|
292
|
+
return {
|
|
293
|
+
kind: "document", path,
|
|
294
|
+
...(str("factorySlug") ? { factorySlug: str("factorySlug") } : {}),
|
|
295
|
+
preview: str("preview") ?? null,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
case "plan": {
|
|
299
|
+
const entries = clampPlanEntries(obj.entries);
|
|
300
|
+
return entries.length > 0 ? { kind: "plan", entries } : null;
|
|
301
|
+
}
|
|
302
|
+
case "image": {
|
|
303
|
+
const path = str("path");
|
|
304
|
+
if (!path || path.length > IMAGE_PATH_MAX_CHARS) return null;
|
|
305
|
+
return {
|
|
306
|
+
kind: "image", path,
|
|
307
|
+
...(str("factorySlug") ? { factorySlug: str("factorySlug") } : {}),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
case "table": {
|
|
311
|
+
// Re-clamp through the shared helper — a hand-forged oversized
|
|
312
|
+
// marker cannot bloat the part beyond the pinned bounds.
|
|
313
|
+
const data = clampTableData({ columns: obj.columns, rows: obj.rows });
|
|
314
|
+
if (!data) return null;
|
|
315
|
+
return { kind: "table", ...data, truncated: data.truncated || obj.truncated === true };
|
|
316
|
+
}
|
|
317
|
+
case "chart": {
|
|
318
|
+
if (obj.chartKind !== "bar" && obj.chartKind !== "line") return null;
|
|
319
|
+
const series = clampChartSeries(obj.series);
|
|
320
|
+
if (series.length === 0) return null;
|
|
321
|
+
const xLabel = clampChartAxisLabel(obj.xLabel);
|
|
322
|
+
const yLabel = clampChartAxisLabel(obj.yLabel);
|
|
323
|
+
return {
|
|
324
|
+
kind: "chart", chartKind: obj.chartKind, series,
|
|
325
|
+
...(xLabel !== undefined ? { xLabel } : {}),
|
|
326
|
+
...(yLabel !== undefined ? { yLabel } : {}),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
case "ask": {
|
|
330
|
+
const askId = str("askId");
|
|
331
|
+
const rawPrompt = str("prompt");
|
|
332
|
+
if (!askId || !UUID_RE.test(askId) || !rawPrompt) return null;
|
|
333
|
+
const options = clampAskOptions(obj.options);
|
|
334
|
+
return {
|
|
335
|
+
kind: "ask", askId, prompt: rawPrompt.slice(0, ASK_PROMPT_MAX_CHARS),
|
|
336
|
+
...(options.length > 0 ? { options } : {}),
|
|
337
|
+
freeText: true, // always true in v1
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
default:
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** First display marker in a tool result's text output (the CLI prints
|
|
346
|
+
* exactly one; scanning lines keeps it robust to surrounding stderr/echo). */
|
|
347
|
+
export function findDisplayMarker(text: string): DisplayMarker | null {
|
|
348
|
+
for (const line of text.split("\n")) {
|
|
349
|
+
const marker = parseDisplayMarker(line);
|
|
350
|
+
if (marker) return marker;
|
|
351
|
+
}
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ── agentc invocation detection ──────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
export type AgentcInvocation =
|
|
358
|
+
| { verb: "display-run"; runId: string }
|
|
359
|
+
| { verb: "display-changes"; runId: string; factorySlug?: string }
|
|
360
|
+
| { verb: "display-document"; path: string; factorySlug?: string }
|
|
361
|
+
| { verb: "display-plan" }
|
|
362
|
+
| { verb: "display-image"; path: string; factorySlug?: string }
|
|
363
|
+
/** Directive verb — detection only arms the tool_call promotion; the
|
|
364
|
+
* result keeps its raw output so the directive marker reaches the cloud
|
|
365
|
+
* executor. Same for table `source:"file"`, diff, and ask-with-approver. */
|
|
366
|
+
| { verb: "display-preview"; path: string }
|
|
367
|
+
| { verb: "display-table"; source: "inline" | "file"; path?: string }
|
|
368
|
+
| { verb: "display-chart"; chartKind: DisplayChartKind }
|
|
369
|
+
| { verb: "display-diff"; path: string; from: string; to: string }
|
|
370
|
+
| { verb: "display-ask"; prompt: string; approver?: string }
|
|
371
|
+
| { verb: "run-dispatch"; templateName: string; input?: unknown }
|
|
372
|
+
| { verb: "run-get"; runId: string }
|
|
373
|
+
| { verb: "files-read"; path: string; factorySlug?: string };
|
|
374
|
+
|
|
375
|
+
/** Shell operators that end the agentc invocation's own argument list. */
|
|
376
|
+
const SHELL_STOPPERS = new Set(["|", "||", "&&", ";", ">", ">>", "<", "2>", "2>>", "&"]);
|
|
377
|
+
|
|
378
|
+
/** Minimal shell-word split: whitespace-separated, single/double quotes
|
|
379
|
+
* group. Unclosed quotes swallow the rest (matching sh's prompt-for-more
|
|
380
|
+
* by simply taking what's there — promotion is best-effort). */
|
|
381
|
+
export function shellWords(command: string): string[] {
|
|
382
|
+
const words: string[] = [];
|
|
383
|
+
let current = "";
|
|
384
|
+
let started = false;
|
|
385
|
+
let quote: '"' | "'" | null = null;
|
|
386
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
387
|
+
const ch = command[i];
|
|
388
|
+
if (quote) {
|
|
389
|
+
if (ch === quote) quote = null;
|
|
390
|
+
else current += ch;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (ch === "'" || ch === '"') { quote = ch; started = true; continue; }
|
|
394
|
+
if (ch === " " || ch === "\t" || ch === "\n") {
|
|
395
|
+
if (started) { words.push(current); current = ""; started = false; }
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
current += ch;
|
|
399
|
+
started = true;
|
|
400
|
+
}
|
|
401
|
+
if (started) words.push(current);
|
|
402
|
+
return words;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Flags that take a value — consumed silently when not of interest so
|
|
406
|
+
* positional collection stays correct. */
|
|
407
|
+
const VALUE_FLAGS = new Set([
|
|
408
|
+
"--factory", "--input", "--entries", "--url", "--api-key", "--limit",
|
|
409
|
+
"--thread", "--content-type", "--file", "--out", "--revision", "--ended-at",
|
|
410
|
+
"--data", "--kind", "--from", "--to", "--prompt", "--options", "--approver",
|
|
411
|
+
]);
|
|
412
|
+
|
|
413
|
+
/** Detect a known agentc invocation inside a shell command string. Returns
|
|
414
|
+
* null for anything unrecognized or with unparseable arguments (a `$VAR`
|
|
415
|
+
* run id, a missing positional) — the pair then persists raw. */
|
|
416
|
+
/** A word that chains, redirects, or substitutes — its presence means the
|
|
417
|
+
* command is a PIPELINE/COMPOUND, where agentc is a data source (e.g.
|
|
418
|
+
* `agentc run get … --json | python3 …`), not a display action. Promoting
|
|
419
|
+
* those turned parse-helper failures into errored "Showed run" cards. */
|
|
420
|
+
function isShellControlWord(w: string): boolean {
|
|
421
|
+
// `2>&1` (stderr merged INTO stdout) is the one harmless redirect: the
|
|
422
|
+
// marker still lands in captured output. Agents append it habitually —
|
|
423
|
+
// live failure: a valid `display chart … 2>&1` stayed a raw text row.
|
|
424
|
+
if (/^\d*>&\d+$/.test(w)) return false;
|
|
425
|
+
return SHELL_STOPPERS.has(w) || /^\d*>>?(&\d+)?$/.test(w) || w === "<" || w.startsWith("$(") || w.startsWith("`");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function detectAgentcInvocation(command: string): AgentcInvocation | null {
|
|
429
|
+
const words = shellWords(command);
|
|
430
|
+
const start = words.findIndex((w) => w === "agentc" || w.endsWith("/agentc"));
|
|
431
|
+
if (start < 0) return null;
|
|
432
|
+
// Pure invocations only: any control word ANYWHERE (before or after)
|
|
433
|
+
// makes this a compound command — persist the pair raw.
|
|
434
|
+
if (words.some(isShellControlWord)) return null;
|
|
435
|
+
// Words before `agentc` must be env assignments (FOO=bar agentc …) or a
|
|
436
|
+
// shell-invoker wrapper (codex runs commands as `bash -lc "agentc …"`,
|
|
437
|
+
// whose argv joins to `bash -lc agentc …`).
|
|
438
|
+
for (let i = 0; i < start; i += 1) {
|
|
439
|
+
const w = words[i];
|
|
440
|
+
const isEnvAssign = /^[A-Za-z_][A-Za-z0-9_]*=/.test(w);
|
|
441
|
+
const isShellInvoker = /^(?:[\w./-]*\/)?(?:sh|bash|zsh|dash)$/.test(w);
|
|
442
|
+
if (!isEnvAssign && !isShellInvoker && !w.startsWith("-")) return null;
|
|
443
|
+
}
|
|
444
|
+
const args: string[] = [];
|
|
445
|
+
const flags = new Map<string, string>();
|
|
446
|
+
for (let i = start + 1; i < words.length; i += 1) {
|
|
447
|
+
const w = words[i];
|
|
448
|
+
if (SHELL_STOPPERS.has(w)) break;
|
|
449
|
+
if (w.startsWith("--")) {
|
|
450
|
+
if (VALUE_FLAGS.has(w) && i + 1 < words.length && !SHELL_STOPPERS.has(words[i + 1])) {
|
|
451
|
+
flags.set(w, words[i + 1]);
|
|
452
|
+
i += 1;
|
|
453
|
+
}
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
args.push(w);
|
|
457
|
+
}
|
|
458
|
+
const factorySlug = flags.get("--factory");
|
|
459
|
+
const withSlug = factorySlug ? { factorySlug } : {};
|
|
460
|
+
const uuid = (v: string | undefined): string | null => (v && UUID_RE.test(v) ? v : null);
|
|
461
|
+
|
|
462
|
+
if (args[0] === "display") {
|
|
463
|
+
switch (args[1]) {
|
|
464
|
+
case "run": {
|
|
465
|
+
const runId = uuid(args[2]);
|
|
466
|
+
return runId ? { verb: "display-run", runId } : null;
|
|
467
|
+
}
|
|
468
|
+
case "changes": {
|
|
469
|
+
const runId = uuid(args[2]);
|
|
470
|
+
return runId ? { verb: "display-changes", runId, ...withSlug } : null;
|
|
471
|
+
}
|
|
472
|
+
case "document":
|
|
473
|
+
return args[2] ? { verb: "display-document", path: args[2], ...withSlug } : null;
|
|
474
|
+
case "plan":
|
|
475
|
+
return { verb: "display-plan" };
|
|
476
|
+
case "image":
|
|
477
|
+
return args[2] ? { verb: "display-image", path: args[2], ...withSlug } : null;
|
|
478
|
+
case "preview":
|
|
479
|
+
return args[2] ? { verb: "display-preview", path: args[2] } : null;
|
|
480
|
+
case "table": {
|
|
481
|
+
const file = flags.get("--file");
|
|
482
|
+
const hasData = flags.has("--data");
|
|
483
|
+
// Exactly one source; both or neither is an unpromotable combination.
|
|
484
|
+
if (file && !hasData) return { verb: "display-table", source: "file", path: file };
|
|
485
|
+
if (hasData && !file) return { verb: "display-table", source: "inline" };
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
case "chart": {
|
|
489
|
+
const chartKind = flags.get("--kind");
|
|
490
|
+
return chartKind === "bar" || chartKind === "line"
|
|
491
|
+
? { verb: "display-chart", chartKind }
|
|
492
|
+
: null;
|
|
493
|
+
}
|
|
494
|
+
case "diff": {
|
|
495
|
+
const from = flags.get("--from");
|
|
496
|
+
const to = flags.get("--to");
|
|
497
|
+
if (!args[2] || !from || !to) return null;
|
|
498
|
+
if (!REVISION_SELECTOR_RE.test(from) || !REVISION_SELECTOR_RE.test(to)) return null;
|
|
499
|
+
return { verb: "display-diff", path: args[2], from, to };
|
|
500
|
+
}
|
|
501
|
+
case "ask": {
|
|
502
|
+
const prompt = flags.get("--prompt");
|
|
503
|
+
if (!prompt || prompt.trim().length === 0) return null;
|
|
504
|
+
const approver = flags.get("--approver");
|
|
505
|
+
return {
|
|
506
|
+
verb: "display-ask",
|
|
507
|
+
prompt: prompt.slice(0, ASK_PROMPT_MAX_CHARS),
|
|
508
|
+
...(approver ? { approver } : {}),
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
default:
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (args[0] === "run" && args[1] === "dispatch" && args[2]) {
|
|
516
|
+
let input: unknown;
|
|
517
|
+
const rawInput = flags.get("--input");
|
|
518
|
+
if (rawInput !== undefined) {
|
|
519
|
+
try { input = JSON.parse(rawInput); } catch { /* raw string stays out */ }
|
|
520
|
+
}
|
|
521
|
+
return { verb: "run-dispatch", templateName: args[2], ...(input !== undefined ? { input } : {}) };
|
|
522
|
+
}
|
|
523
|
+
if (args[0] === "run" && args[1] === "get") {
|
|
524
|
+
const runId = uuid(args[2]);
|
|
525
|
+
return runId ? { verb: "run-get", runId } : null;
|
|
526
|
+
}
|
|
527
|
+
if (args[0] === "files" && args[1] === "read" && args[2]) {
|
|
528
|
+
return { verb: "files-read", path: args[2], ...withSlug };
|
|
529
|
+
}
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ── The promoter ─────────────────────────────────────────────────────────────
|
|
534
|
+
|
|
535
|
+
/** Plain part shapes, structurally identical to the server's `MessagePart`
|
|
536
|
+
* union (and the bridge POST schema) — pinned by shape-fidelity tests on
|
|
537
|
+
* the server side. */
|
|
538
|
+
export type PromotedPart =
|
|
539
|
+
| { type: "tool_call"; toolName: string; input: Record<string, unknown>; toolCallId: string }
|
|
540
|
+
| { type: "tool_result"; toolCallId: string; output: unknown; isError: boolean }
|
|
541
|
+
| { type: "plan"; entries: DisplayPlanEntry[] };
|
|
542
|
+
|
|
543
|
+
/** Preview / content clamps for promoted document results. */
|
|
544
|
+
const DOCUMENT_PREVIEW_CHARS = 240;
|
|
545
|
+
const DOCUMENT_CONTENT_CHARS = 4_000;
|
|
546
|
+
|
|
547
|
+
interface PendingPromotion {
|
|
548
|
+
invocation: AgentcInvocation;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export interface DisplayPromoterContext {
|
|
552
|
+
/** The session's factory id (cloud sessions know it from the session row)
|
|
553
|
+
* — stamped onto document/changes parts when the command names no
|
|
554
|
+
* `--factory` slug, so the renderer can resolve the drive link. */
|
|
555
|
+
factoryId?: string;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export interface DisplayPromoter {
|
|
559
|
+
/** Promote a tool_use event. Non-null → persist these parts INSTEAD of
|
|
560
|
+
* the default mapping (the promoted tool_call reuses the toolUseId, so
|
|
561
|
+
* the eventual result still pairs). Null → not a promotable call. */
|
|
562
|
+
promoteToolUse(msg: { toolName: string; toolInput: unknown; toolUseId: string }): PromotedPart[] | null;
|
|
563
|
+
/** Promote the matching tool_result. Non-null → persist these parts
|
|
564
|
+
* instead of the default mapping. Null → no promotion pending. */
|
|
565
|
+
promoteToolResult(msg: { toolUseId: string; output: unknown; isError?: boolean }): PromotedPart[] | null;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** The command string of a shell-ish tool input — Claude Code's Bash tool
|
|
569
|
+
* ({ command: "…" }) and codex's shell tool ({ command: [argv…] }). */
|
|
570
|
+
function commandOf(toolInput: unknown): string | null {
|
|
571
|
+
if (typeof toolInput !== "object" || toolInput === null) return null;
|
|
572
|
+
const cmd = (toolInput as { command?: unknown }).command;
|
|
573
|
+
if (typeof cmd === "string") return cmd;
|
|
574
|
+
if (Array.isArray(cmd) && cmd.every((c) => typeof c === "string")) return cmd.join(" ");
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** Flatten a tool result's output to text, however the runtime wrapped it. */
|
|
579
|
+
function textOf(output: unknown): string | null {
|
|
580
|
+
if (typeof output === "string") return output;
|
|
581
|
+
if (typeof output !== "object" || output === null) return null;
|
|
582
|
+
const o = output as Record<string, unknown>;
|
|
583
|
+
if (typeof o.output === "string") return o.output;
|
|
584
|
+
if (typeof o.text === "string") return o.text;
|
|
585
|
+
if (typeof o.content === "string") return o.content;
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** Last JSON object found on a line of `text` carrying a string `runId` —
|
|
590
|
+
* how a dispatch result names its run with or without `--json`. */
|
|
591
|
+
function runIdIn(text: string): { runId: string; parsed: Record<string, unknown> | null } | null {
|
|
592
|
+
for (const line of [...text.split("\n")].reverse()) {
|
|
593
|
+
const trimmed = line.trim();
|
|
594
|
+
if (trimmed.startsWith("{")) {
|
|
595
|
+
try {
|
|
596
|
+
const parsed: unknown = JSON.parse(trimmed);
|
|
597
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
598
|
+
const runId = (parsed as { runId?: unknown }).runId;
|
|
599
|
+
if (typeof runId === "string" && UUID_RE.test(runId)) {
|
|
600
|
+
return { runId, parsed: parsed as Record<string, unknown> };
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
} catch { /* not JSON — keep scanning */ }
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const m = /run ([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i.exec(text);
|
|
607
|
+
return m ? { runId: m[1], parsed: null } : null;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export function createDisplayPromoter(ctx: DisplayPromoterContext = {}): DisplayPromoter {
|
|
611
|
+
const pending = new Map<string, PendingPromotion>();
|
|
612
|
+
|
|
613
|
+
/** factorySlug from the command when present, else the session's factory
|
|
614
|
+
* id — the renderer accepts either (slug direct, or id via its cached
|
|
615
|
+
* factories lookup). */
|
|
616
|
+
const factoryRef = (slug: string | undefined): Record<string, string> => {
|
|
617
|
+
if (slug) return { factorySlug: slug };
|
|
618
|
+
if (ctx.factoryId) return { factoryId: ctx.factoryId };
|
|
619
|
+
return {};
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
return {
|
|
623
|
+
promoteToolUse(msg) {
|
|
624
|
+
// Claude Code's native TodoWrite → ADDITIONALLY forward the durable
|
|
625
|
+
// `plan` part (the ACP daemon's shape) so the checklist renders; the
|
|
626
|
+
// raw tool pair still persists unchanged (strictly additive).
|
|
627
|
+
if (msg.toolName === "TodoWrite") {
|
|
628
|
+
const todos = (msg.toolInput as { todos?: unknown } | null)?.todos;
|
|
629
|
+
const entries = clampPlanEntries(todos);
|
|
630
|
+
if (entries.length === 0) return null;
|
|
631
|
+
return [
|
|
632
|
+
{ type: "tool_call", toolName: msg.toolName, input: msg.toolInput as Record<string, unknown>, toolCallId: msg.toolUseId },
|
|
633
|
+
{ type: "plan", entries },
|
|
634
|
+
];
|
|
635
|
+
}
|
|
636
|
+
const command = commandOf(msg.toolInput);
|
|
637
|
+
if (!command || !command.includes("agentc")) return null;
|
|
638
|
+
const invocation = detectAgentcInvocation(command);
|
|
639
|
+
if (!invocation) return null;
|
|
640
|
+
pending.set(msg.toolUseId, { invocation });
|
|
641
|
+
const call = (toolName: string, input: Record<string, unknown>): PromotedPart[] => [
|
|
642
|
+
{ type: "tool_call", toolName, input: { ...input, command }, toolCallId: msg.toolUseId },
|
|
643
|
+
];
|
|
644
|
+
switch (invocation.verb) {
|
|
645
|
+
case "display-run":
|
|
646
|
+
case "run-get":
|
|
647
|
+
return call("display_run", { runId: invocation.runId });
|
|
648
|
+
case "display-changes":
|
|
649
|
+
return call("display_changes", { runId: invocation.runId, ...factoryRef(invocation.factorySlug) });
|
|
650
|
+
case "display-document":
|
|
651
|
+
case "files-read":
|
|
652
|
+
return call("display_document", { path: invocation.path, ...factoryRef(invocation.factorySlug) });
|
|
653
|
+
case "display-plan":
|
|
654
|
+
return call("display_plan", {});
|
|
655
|
+
case "display-image":
|
|
656
|
+
return call("display_image", { path: invocation.path, ...factoryRef(invocation.factorySlug) });
|
|
657
|
+
case "display-preview":
|
|
658
|
+
return call("display_preview", { path: invocation.path, ...factoryRef(undefined) });
|
|
659
|
+
case "display-table":
|
|
660
|
+
return call("display_table", {
|
|
661
|
+
source: invocation.source,
|
|
662
|
+
...(invocation.path !== undefined ? { path: invocation.path } : {}),
|
|
663
|
+
...(invocation.source === "file" ? factoryRef(undefined) : {}),
|
|
664
|
+
});
|
|
665
|
+
case "display-chart":
|
|
666
|
+
return call("display_chart", { chartKind: invocation.chartKind });
|
|
667
|
+
case "display-diff":
|
|
668
|
+
return call("display_diff", {
|
|
669
|
+
path: invocation.path, from: invocation.from, to: invocation.to,
|
|
670
|
+
...factoryRef(undefined),
|
|
671
|
+
});
|
|
672
|
+
case "display-ask":
|
|
673
|
+
return call("display_ask", { prompt: invocation.prompt });
|
|
674
|
+
case "run-dispatch":
|
|
675
|
+
return call("dispatch_workflow", {
|
|
676
|
+
templateName: invocation.templateName,
|
|
677
|
+
...(invocation.input !== undefined ? { input: invocation.input } : {}),
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
},
|
|
681
|
+
|
|
682
|
+
promoteToolResult(msg) {
|
|
683
|
+
const entry = pending.get(msg.toolUseId);
|
|
684
|
+
if (!entry) return null;
|
|
685
|
+
pending.delete(msg.toolUseId);
|
|
686
|
+
const isError = msg.isError === true;
|
|
687
|
+
const result = (output: unknown): PromotedPart => ({
|
|
688
|
+
type: "tool_result", toolCallId: msg.toolUseId, output, isError,
|
|
689
|
+
});
|
|
690
|
+
// An errored command keeps its raw output — failures are never dressed
|
|
691
|
+
// up as cards (the renderer shows the promoted row with its error).
|
|
692
|
+
const text = textOf(msg.output);
|
|
693
|
+
if (isError || text === null) return [result(msg.output)];
|
|
694
|
+
|
|
695
|
+
const inv = entry.invocation;
|
|
696
|
+
switch (inv.verb) {
|
|
697
|
+
case "display-run": {
|
|
698
|
+
const marker = findDisplayMarker(text);
|
|
699
|
+
return [result(marker?.kind === "run" ? { runId: marker.runId } : { runId: inv.runId })];
|
|
700
|
+
}
|
|
701
|
+
case "run-get": {
|
|
702
|
+
const found = runIdIn(text);
|
|
703
|
+
return [result(found?.parsed ?? { runId: found?.runId ?? inv.runId })];
|
|
704
|
+
}
|
|
705
|
+
case "display-changes": {
|
|
706
|
+
const marker = findDisplayMarker(text);
|
|
707
|
+
const m = marker?.kind === "changes" ? marker : null;
|
|
708
|
+
return [result({
|
|
709
|
+
runId: m?.runId ?? inv.runId,
|
|
710
|
+
...factoryRef(m?.factorySlug ?? inv.factorySlug),
|
|
711
|
+
endedAt: m?.endedAt ?? null,
|
|
712
|
+
})];
|
|
713
|
+
}
|
|
714
|
+
case "display-document": {
|
|
715
|
+
const marker = findDisplayMarker(text);
|
|
716
|
+
const m = marker?.kind === "document" ? marker : null;
|
|
717
|
+
return [result({
|
|
718
|
+
path: m?.path ?? inv.path,
|
|
719
|
+
...factoryRef(m?.factorySlug ?? inv.factorySlug),
|
|
720
|
+
preview: m?.preview ?? null,
|
|
721
|
+
})];
|
|
722
|
+
}
|
|
723
|
+
case "files-read":
|
|
724
|
+
return [result({
|
|
725
|
+
path: inv.path,
|
|
726
|
+
...factoryRef(inv.factorySlug),
|
|
727
|
+
preview: text.slice(0, DOCUMENT_PREVIEW_CHARS) || null,
|
|
728
|
+
content: text.slice(0, DOCUMENT_CONTENT_CHARS),
|
|
729
|
+
})];
|
|
730
|
+
case "display-plan": {
|
|
731
|
+
const marker = findDisplayMarker(text);
|
|
732
|
+
if (marker?.kind !== "plan") return [result(msg.output)];
|
|
733
|
+
return [
|
|
734
|
+
result({ entries: marker.entries.length }),
|
|
735
|
+
{ type: "plan", entries: marker.entries },
|
|
736
|
+
];
|
|
737
|
+
}
|
|
738
|
+
case "display-image": {
|
|
739
|
+
const marker = findDisplayMarker(text);
|
|
740
|
+
const m = marker?.kind === "image" ? marker : null;
|
|
741
|
+
return [result({
|
|
742
|
+
path: m?.path ?? inv.path,
|
|
743
|
+
...factoryRef(m?.factorySlug ?? inv.factorySlug),
|
|
744
|
+
})];
|
|
745
|
+
}
|
|
746
|
+
case "display-table": {
|
|
747
|
+
// File-sourced tables are a DIRECTIVE: keep the raw output so the
|
|
748
|
+
// marker inside it reaches the cloud executor (the real card pair).
|
|
749
|
+
if (inv.source === "file") return [result(msg.output)];
|
|
750
|
+
const marker = findDisplayMarker(text);
|
|
751
|
+
if (marker?.kind !== "table") return [result(msg.output)];
|
|
752
|
+
return [result({ columns: marker.columns, rows: marker.rows, truncated: marker.truncated })];
|
|
753
|
+
}
|
|
754
|
+
case "display-chart": {
|
|
755
|
+
const marker = findDisplayMarker(text);
|
|
756
|
+
if (marker?.kind !== "chart") return [result(msg.output)];
|
|
757
|
+
return [result({
|
|
758
|
+
chartKind: marker.chartKind, series: marker.series,
|
|
759
|
+
...(marker.xLabel !== undefined ? { xLabel: marker.xLabel } : {}),
|
|
760
|
+
...(marker.yLabel !== undefined ? { yLabel: marker.yLabel } : {}),
|
|
761
|
+
})];
|
|
762
|
+
}
|
|
763
|
+
case "display-ask": {
|
|
764
|
+
// Approver asks are a DIRECTIVE (server-authoritative resolution
|
|
765
|
+
// + ping) — raw output through, marker intact.
|
|
766
|
+
if (inv.approver !== undefined) return [result(msg.output)];
|
|
767
|
+
const marker = findDisplayMarker(text);
|
|
768
|
+
if (marker?.kind !== "ask") return [result(msg.output)];
|
|
769
|
+
return [result({
|
|
770
|
+
askId: marker.askId, prompt: marker.prompt,
|
|
771
|
+
...(marker.options !== undefined ? { options: marker.options } : {}),
|
|
772
|
+
freeText: marker.freeText,
|
|
773
|
+
})];
|
|
774
|
+
}
|
|
775
|
+
case "display-preview":
|
|
776
|
+
case "display-diff":
|
|
777
|
+
// Directive verbs: the promoted tool_call is armed above; the raw
|
|
778
|
+
// result keeps the directive marker for the cloud executor.
|
|
779
|
+
return [result(msg.output)];
|
|
780
|
+
case "run-dispatch": {
|
|
781
|
+
const found = runIdIn(text);
|
|
782
|
+
if (!found) return [result(msg.output)];
|
|
783
|
+
return [result(found.parsed ?? { runId: found.runId })];
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
};
|
|
788
|
+
}
|