@poncho-ai/cli 0.4.2 → 0.5.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.
Files changed (36) hide show
  1. package/.turbo/turbo-build.log +5 -5
  2. package/CHANGELOG.md +17 -0
  3. package/dist/chunk-2AZ3Y6R2.js +4121 -0
  4. package/dist/chunk-3273VMA7.js +4182 -0
  5. package/dist/chunk-6REJ5J4T.js +4142 -0
  6. package/dist/chunk-BSW557BB.js +4058 -0
  7. package/dist/chunk-GJYE4S3D.js +4164 -0
  8. package/dist/chunk-HGZTVHBT.js +4089 -0
  9. package/dist/chunk-HNYADV2K.js +4164 -0
  10. package/dist/chunk-IE47LJ33.js +4166 -0
  11. package/dist/chunk-MFVXK3SX.js +4177 -0
  12. package/dist/chunk-N7ZAHMBR.js +4178 -0
  13. package/dist/chunk-TYL4SGJE.js +4177 -0
  14. package/dist/chunk-VIX7Y2YC.js +4169 -0
  15. package/dist/chunk-WCIUVLV3.js +4171 -0
  16. package/dist/chunk-WXFCSFXF.js +4178 -0
  17. package/dist/cli.js +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/run-interactive-ink-4EWW4AJ6.js +463 -0
  20. package/dist/run-interactive-ink-642LZIYZ.js +494 -0
  21. package/dist/run-interactive-ink-72H5IUTC.js +494 -0
  22. package/dist/run-interactive-ink-7P4WWB2Y.js +494 -0
  23. package/dist/run-interactive-ink-EP7GIKLH.js +463 -0
  24. package/dist/run-interactive-ink-FASKW7SN.js +463 -0
  25. package/dist/run-interactive-ink-LLNLDCES.js +494 -0
  26. package/dist/run-interactive-ink-MO6MGLEY.js +494 -0
  27. package/dist/run-interactive-ink-OQZN5DQE.js +463 -0
  28. package/dist/run-interactive-ink-QKB6CG3W.js +494 -0
  29. package/dist/run-interactive-ink-RJCA5IQA.js +494 -0
  30. package/dist/run-interactive-ink-RU2PH6R5.js +494 -0
  31. package/dist/run-interactive-ink-T36C6TJ2.js +463 -0
  32. package/dist/run-interactive-ink-ZL6RAS2O.js +494 -0
  33. package/package.json +3 -2
  34. package/src/index.ts +46 -10
  35. package/src/run-interactive-ink.ts +45 -10
  36. package/src/web-ui.ts +206 -103
@@ -0,0 +1,494 @@
1
+ import {
2
+ consumeFirstRunIntro,
3
+ inferConversationTitle,
4
+ resolveHarnessEnvironment
5
+ } from "./chunk-HNYADV2K.js";
6
+
7
+ // src/run-interactive-ink.ts
8
+ import * as readline from "readline";
9
+ import { stdout } from "process";
10
+ import {
11
+ parseAgentFile
12
+ } from "@poncho-ai/harness";
13
+ var C = {
14
+ reset: "\x1B[0m",
15
+ bold: "\x1B[1m",
16
+ dim: "\x1B[2m",
17
+ cyan: "\x1B[36m",
18
+ green: "\x1B[32m",
19
+ yellow: "\x1B[33m",
20
+ red: "\x1B[31m",
21
+ gray: "\x1B[90m",
22
+ magenta: "\x1B[35m"
23
+ };
24
+ var green = (s) => `${C.green}${s}${C.reset}`;
25
+ var yellow = (s) => `${C.yellow}${s}${C.reset}`;
26
+ var red = (s) => `${C.red}${s}${C.reset}`;
27
+ var gray = (s) => `${C.gray}${s}${C.reset}`;
28
+ var magenta = (s) => `${C.magenta}${s}${C.reset}`;
29
+ var FAUX_TOOL_LOG_PATTERN = /Tool Used:|Tool Result:|\blist_skills\b|\bcreate_skill\b|\bedit_skill\b/i;
30
+ var formatDuration = (ms) => ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
31
+ var stringifyValue = (v) => {
32
+ try {
33
+ return JSON.stringify(v);
34
+ } catch {
35
+ return String(v);
36
+ }
37
+ };
38
+ var truncate = (v, max) => v.length <= max ? v : `${v.slice(0, Math.max(0, max - 3))}...`;
39
+ var compactPreview = (v, max = 120) => truncate(stringifyValue(v).replace(/\s+/g, " "), max);
40
+ var loadMetadata = async (workingDir) => {
41
+ let agentName = "agent";
42
+ let model = "unknown";
43
+ let provider = "unknown";
44
+ try {
45
+ const parsed = await parseAgentFile(workingDir);
46
+ agentName = parsed.frontmatter.name ?? agentName;
47
+ model = parsed.frontmatter.model?.name ?? model;
48
+ provider = parsed.frontmatter.model?.provider ?? provider;
49
+ } catch {
50
+ }
51
+ return {
52
+ agentName,
53
+ model,
54
+ provider,
55
+ workingDir,
56
+ environment: resolveHarnessEnvironment()
57
+ };
58
+ };
59
+ var ask = (rl, prompt) => new Promise((res) => {
60
+ rl.question(prompt, (answer) => res(answer));
61
+ });
62
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
63
+ var streamTextAsTokens = async (text) => {
64
+ const tokens = text.match(/\S+\s*|\n/g) ?? [text];
65
+ for (const token of tokens) {
66
+ stdout.write(token);
67
+ const trimmed = token.trim();
68
+ const delay = trimmed.length === 0 ? 0 : Math.max(4, Math.min(18, Math.floor(trimmed.length / 2)));
69
+ await sleep(delay);
70
+ }
71
+ };
72
+ var OWNER_ID = "local-owner";
73
+ var computeTurn = (messages) => Math.max(1, Math.floor(messages.length / 2) + 1);
74
+ var formatDate = (value) => {
75
+ try {
76
+ return new Date(value).toLocaleString();
77
+ } catch {
78
+ return String(value);
79
+ }
80
+ };
81
+ var handleSlash = async (command, state, conversationStore) => {
82
+ const [rawCommand, ...args] = command.trim().split(/\s+/);
83
+ const norm = rawCommand.toLowerCase();
84
+ if (norm === "/help") {
85
+ console.log(
86
+ gray(
87
+ "commands> /help /clear /exit /tools /list /open <id> /new [title] /delete [id] /continue /reset [all]"
88
+ )
89
+ );
90
+ return { shouldExit: false };
91
+ }
92
+ if (norm === "/clear") {
93
+ process.stdout.write("\x1B[2J\x1B[H");
94
+ return { shouldExit: false };
95
+ }
96
+ if (norm === "/exit") {
97
+ return { shouldExit: true };
98
+ }
99
+ if (norm === "/list") {
100
+ const conversations = await conversationStore.list(OWNER_ID);
101
+ if (conversations.length === 0) {
102
+ console.log(gray("conversations> none"));
103
+ return { shouldExit: false };
104
+ }
105
+ console.log(gray("conversations>"));
106
+ for (const conversation of conversations) {
107
+ const activeMarker = state.activeConversationId === conversation.conversationId ? "*" : " ";
108
+ console.log(
109
+ gray(
110
+ `${activeMarker} ${conversation.conversationId} | ${conversation.title} | ${formatDate(conversation.updatedAt)}`
111
+ )
112
+ );
113
+ }
114
+ return { shouldExit: false };
115
+ }
116
+ if (norm === "/open") {
117
+ const conversationId = args[0];
118
+ if (!conversationId) {
119
+ console.log(yellow("usage> /open <conversationId>"));
120
+ return { shouldExit: false };
121
+ }
122
+ const conversation = await conversationStore.get(conversationId);
123
+ if (!conversation) {
124
+ console.log(yellow(`conversations> not found: ${conversationId}`));
125
+ return { shouldExit: false };
126
+ }
127
+ state.activeConversationId = conversation.conversationId;
128
+ state.messages = [...conversation.messages];
129
+ state.turn = computeTurn(state.messages);
130
+ console.log(gray(`conversations> opened ${conversation.conversationId}`));
131
+ return { shouldExit: false };
132
+ }
133
+ if (norm === "/new") {
134
+ const title = args.join(" ").trim();
135
+ const conversation = await conversationStore.create(OWNER_ID, title || void 0);
136
+ state.activeConversationId = conversation.conversationId;
137
+ state.messages = [];
138
+ state.turn = 1;
139
+ console.log(gray(`conversations> new ${conversation.conversationId}`));
140
+ return { shouldExit: false };
141
+ }
142
+ if (norm === "/delete") {
143
+ const targetConversationId = args[0] ?? state.activeConversationId ?? "";
144
+ if (!targetConversationId) {
145
+ console.log(yellow("usage> /delete <conversationId>"));
146
+ return { shouldExit: false };
147
+ }
148
+ const removed = await conversationStore.delete(targetConversationId);
149
+ if (!removed) {
150
+ console.log(yellow(`conversations> not found: ${targetConversationId}`));
151
+ return { shouldExit: false };
152
+ }
153
+ if (state.activeConversationId === targetConversationId) {
154
+ state.activeConversationId = null;
155
+ state.messages = [];
156
+ state.turn = 1;
157
+ }
158
+ console.log(gray(`conversations> deleted ${targetConversationId}`));
159
+ return { shouldExit: false };
160
+ }
161
+ if (norm === "/continue") {
162
+ const conversations = await conversationStore.list(OWNER_ID);
163
+ const latest = conversations[0];
164
+ if (!latest) {
165
+ console.log(yellow("conversations> no conversations to continue"));
166
+ return { shouldExit: false };
167
+ }
168
+ state.activeConversationId = latest.conversationId;
169
+ state.messages = [...latest.messages];
170
+ state.turn = computeTurn(state.messages);
171
+ console.log(gray(`conversations> continued ${latest.conversationId}`));
172
+ return { shouldExit: false };
173
+ }
174
+ if (norm === "/reset") {
175
+ if (args[0]?.toLowerCase() === "all") {
176
+ const conversations = await conversationStore.list(OWNER_ID);
177
+ for (const conversation2 of conversations) {
178
+ await conversationStore.delete(conversation2.conversationId);
179
+ }
180
+ state.activeConversationId = null;
181
+ state.messages = [];
182
+ state.turn = 1;
183
+ console.log(gray("conversations> reset all"));
184
+ return { shouldExit: false };
185
+ }
186
+ if (!state.activeConversationId) {
187
+ state.messages = [];
188
+ state.turn = 1;
189
+ console.log(gray("conversations> current session reset"));
190
+ return { shouldExit: false };
191
+ }
192
+ const conversation = await conversationStore.get(state.activeConversationId);
193
+ if (!conversation) {
194
+ state.activeConversationId = null;
195
+ state.messages = [];
196
+ state.turn = 1;
197
+ console.log(yellow("conversations> active conversation no longer exists"));
198
+ return { shouldExit: false };
199
+ }
200
+ await conversationStore.update({
201
+ ...conversation,
202
+ messages: []
203
+ });
204
+ state.messages = [];
205
+ state.turn = 1;
206
+ console.log(gray(`conversations> reset ${conversation.conversationId}`));
207
+ return { shouldExit: false };
208
+ }
209
+ console.log(yellow(`Unknown command: ${command}`));
210
+ return { shouldExit: false };
211
+ };
212
+ var runInteractiveInk = async ({
213
+ harness,
214
+ params,
215
+ workingDir,
216
+ config,
217
+ conversationStore,
218
+ onSetApprovalCallback
219
+ }) => {
220
+ const metadata = await loadMetadata(workingDir);
221
+ const rl = readline.createInterface({
222
+ input: process.stdin,
223
+ output: process.stdout,
224
+ terminal: true
225
+ });
226
+ if (onSetApprovalCallback) {
227
+ onSetApprovalCallback((req) => {
228
+ process.stdout.write("\n");
229
+ const preview = compactPreview(req.input, 100);
230
+ rl.question(
231
+ `${C.yellow}${C.bold}Tool "${req.tool}" requires approval${C.reset}
232
+ ${C.gray}input: ${preview}${C.reset}
233
+ ${C.yellow}approve? (y/n): ${C.reset}`,
234
+ (answer) => {
235
+ const approved = answer.trim().toLowerCase() === "y";
236
+ console.log(
237
+ approved ? green(` approved ${req.tool}`) : magenta(` denied ${req.tool}`)
238
+ );
239
+ req.resolve(approved);
240
+ }
241
+ );
242
+ });
243
+ }
244
+ console.log(
245
+ gray(
246
+ `
247
+ ${metadata.agentName} | ${metadata.provider}/${metadata.model} | ${metadata.environment}`
248
+ )
249
+ );
250
+ console.log(gray('Type "exit" to quit, "/help" for commands'));
251
+ console.log(
252
+ gray("Conversation controls: /list /open <id> /new [title] /delete [id] /continue /reset [all]\n")
253
+ );
254
+ const intro = await consumeFirstRunIntro(workingDir, {
255
+ agentName: metadata.agentName,
256
+ provider: metadata.provider,
257
+ model: metadata.model,
258
+ config
259
+ });
260
+ if (intro) {
261
+ console.log(green("assistant>"));
262
+ await streamTextAsTokens(intro);
263
+ stdout.write("\n\n");
264
+ }
265
+ let messages = intro ? [{ role: "assistant", content: intro }] : [];
266
+ let turn = 1;
267
+ let activeConversationId = null;
268
+ let showToolPayloads = false;
269
+ const prompt = `${C.cyan}you> ${C.reset}`;
270
+ while (true) {
271
+ let task;
272
+ try {
273
+ task = await ask(rl, prompt);
274
+ } catch {
275
+ break;
276
+ }
277
+ const trimmed = task.trim();
278
+ if (!trimmed) continue;
279
+ if (trimmed.toLowerCase() === "exit") break;
280
+ if (trimmed.startsWith("/")) {
281
+ if (trimmed.toLowerCase() === "/exit") break;
282
+ if (trimmed.toLowerCase() === "/tools") {
283
+ showToolPayloads = !showToolPayloads;
284
+ console.log(gray(`tool payloads: ${showToolPayloads ? "on" : "off"}`));
285
+ continue;
286
+ }
287
+ const interactiveState = {
288
+ messages,
289
+ turn,
290
+ activeConversationId
291
+ };
292
+ const slashResult = await handleSlash(
293
+ trimmed,
294
+ interactiveState,
295
+ conversationStore
296
+ );
297
+ if (slashResult.shouldExit) {
298
+ break;
299
+ }
300
+ messages = interactiveState.messages;
301
+ turn = interactiveState.turn;
302
+ activeConversationId = interactiveState.activeConversationId;
303
+ continue;
304
+ }
305
+ console.log(gray(`
306
+ --- turn ${turn} ---`));
307
+ process.stdout.write(gray("thinking..."));
308
+ let thinkingCleared = false;
309
+ const clearThinking = () => {
310
+ if (thinkingCleared) return;
311
+ thinkingCleared = true;
312
+ readline.clearLine(process.stdout, 0);
313
+ readline.cursorTo(process.stdout, 0);
314
+ };
315
+ let responseText = "";
316
+ let streamedText = "";
317
+ let committedText = false;
318
+ let sawChunk = false;
319
+ let toolEvents = 0;
320
+ const toolTimeline = [];
321
+ const sections = [];
322
+ let currentText = "";
323
+ let currentTools = [];
324
+ let runFailed = false;
325
+ let usage;
326
+ let latestRunId = "";
327
+ const startedAt = Date.now();
328
+ try {
329
+ for await (const event of harness.run({
330
+ task: trimmed,
331
+ parameters: params,
332
+ messages
333
+ })) {
334
+ if (event.type === "run:started") {
335
+ latestRunId = event.runId;
336
+ }
337
+ if (event.type === "model:chunk") {
338
+ sawChunk = true;
339
+ if (currentTools.length > 0) {
340
+ sections.push({ type: "tools", content: currentTools });
341
+ currentTools = [];
342
+ }
343
+ responseText += event.content;
344
+ streamedText += event.content;
345
+ currentText += event.content;
346
+ if (!thinkingCleared) {
347
+ clearThinking();
348
+ process.stdout.write(`${C.green}assistant> ${C.reset}`);
349
+ }
350
+ process.stdout.write(event.content);
351
+ } else if (event.type === "tool:started" || event.type === "tool:completed" || event.type === "tool:error" || event.type === "tool:approval:required" || event.type === "tool:approval:granted" || event.type === "tool:approval:denied") {
352
+ if (streamedText.length > 0) {
353
+ committedText = true;
354
+ streamedText = "";
355
+ process.stdout.write("\n");
356
+ }
357
+ clearThinking();
358
+ if (event.type === "tool:started") {
359
+ if (currentText.length > 0) {
360
+ sections.push({ type: "text", content: currentText });
361
+ currentText = "";
362
+ }
363
+ const preview = showToolPayloads ? compactPreview(event.input, 400) : compactPreview(event.input, 100);
364
+ console.log(yellow(`tools> start ${event.tool} input=${preview}`));
365
+ const toolText = `- start \`${event.tool}\``;
366
+ toolTimeline.push(toolText);
367
+ currentTools.push(toolText);
368
+ toolEvents += 1;
369
+ } else if (event.type === "tool:completed") {
370
+ const preview = showToolPayloads ? compactPreview(event.output, 400) : compactPreview(event.output, 100);
371
+ console.log(
372
+ yellow(
373
+ `tools> done ${event.tool} in ${formatDuration(event.duration)}`
374
+ )
375
+ );
376
+ if (showToolPayloads) {
377
+ console.log(yellow(`tools> output ${preview}`));
378
+ }
379
+ const toolText = `- done \`${event.tool}\` in ${formatDuration(event.duration)}`;
380
+ toolTimeline.push(toolText);
381
+ currentTools.push(toolText);
382
+ } else if (event.type === "tool:error") {
383
+ console.log(
384
+ red(`tools> error ${event.tool}: ${event.error}`)
385
+ );
386
+ const toolText = `- error \`${event.tool}\`: ${event.error}`;
387
+ toolTimeline.push(toolText);
388
+ currentTools.push(toolText);
389
+ } else if (event.type === "tool:approval:required") {
390
+ console.log(
391
+ magenta(`tools> approval required for ${event.tool}`)
392
+ );
393
+ const toolText = `- approval required \`${event.tool}\``;
394
+ toolTimeline.push(toolText);
395
+ currentTools.push(toolText);
396
+ } else if (event.type === "tool:approval:granted") {
397
+ console.log(
398
+ gray(`tools> approval granted (${event.approvalId})`)
399
+ );
400
+ const toolText = `- approval granted (${event.approvalId})`;
401
+ toolTimeline.push(toolText);
402
+ currentTools.push(toolText);
403
+ } else if (event.type === "tool:approval:denied") {
404
+ console.log(
405
+ magenta(`tools> approval denied (${event.approvalId})`)
406
+ );
407
+ const toolText = `- approval denied (${event.approvalId})`;
408
+ toolTimeline.push(toolText);
409
+ currentTools.push(toolText);
410
+ }
411
+ } else if (event.type === "run:error") {
412
+ clearThinking();
413
+ runFailed = true;
414
+ console.log(red(`error> ${event.error.message}`));
415
+ } else if (event.type === "model:response") {
416
+ usage = event.usage;
417
+ } else if (event.type === "run:completed" && !sawChunk) {
418
+ clearThinking();
419
+ responseText = event.result.response ?? "";
420
+ if (responseText.length > 0) {
421
+ process.stdout.write(
422
+ `${C.green}assistant> ${C.reset}${responseText}
423
+ `
424
+ );
425
+ }
426
+ }
427
+ }
428
+ } catch (error) {
429
+ clearThinking();
430
+ runFailed = true;
431
+ console.log(
432
+ red(
433
+ `error> ${error instanceof Error ? error.message : "Unknown error"}`
434
+ )
435
+ );
436
+ }
437
+ if (sawChunk && streamedText.length > 0) {
438
+ process.stdout.write("\n");
439
+ } else if (!sawChunk && !runFailed && responseText.length === 0) {
440
+ clearThinking();
441
+ console.log(green("assistant> (no response)"));
442
+ }
443
+ const fullResponse = responseText || streamedText;
444
+ if (!runFailed && toolEvents === 0 && FAUX_TOOL_LOG_PATTERN.test(fullResponse)) {
445
+ console.log(
446
+ magenta(
447
+ "warning> assistant described tool execution but no real tool events occurred."
448
+ )
449
+ );
450
+ }
451
+ const durationMs = Date.now() - startedAt;
452
+ console.log(
453
+ gray(`meta> ${formatDuration(durationMs)} | tools: ${toolEvents}
454
+ `)
455
+ );
456
+ if (!activeConversationId) {
457
+ const created = await conversationStore.create(
458
+ OWNER_ID,
459
+ inferConversationTitle(trimmed)
460
+ );
461
+ activeConversationId = created.conversationId;
462
+ }
463
+ if (currentTools.length > 0) {
464
+ sections.push({ type: "tools", content: currentTools });
465
+ }
466
+ if (currentText.length > 0) {
467
+ sections.push({ type: "text", content: currentText });
468
+ }
469
+ messages.push({ role: "user", content: trimmed });
470
+ messages.push({
471
+ role: "assistant",
472
+ content: responseText,
473
+ metadata: toolTimeline.length > 0 || sections.length > 0 ? {
474
+ toolActivity: toolTimeline,
475
+ sections: sections.length > 0 ? sections : void 0
476
+ } : void 0
477
+ });
478
+ turn = computeTurn(messages);
479
+ const conversation = await conversationStore.get(activeConversationId);
480
+ if (conversation) {
481
+ const maybeTitle = conversation.messages.length === 0 && (conversation.title === "New conversation" || conversation.title.trim().length === 0) ? inferConversationTitle(trimmed) : conversation.title;
482
+ await conversationStore.update({
483
+ ...conversation,
484
+ title: maybeTitle,
485
+ messages: [...messages],
486
+ runtimeRunId: latestRunId || conversation.runtimeRunId
487
+ });
488
+ }
489
+ }
490
+ rl.close();
491
+ };
492
+ export {
493
+ runInteractiveInk
494
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/cli",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "CLI for building and deploying AI agents",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,10 +22,11 @@
22
22
  "dotenv": "^16.4.0",
23
23
  "esbuild": "^0.25.9",
24
24
  "ink": "^6.7.0",
25
+ "marked": "^17.0.2",
25
26
  "react": "^19.2.4",
26
27
  "react-devtools-core": "^6.1.5",
27
28
  "yaml": "^2.8.1",
28
- "@poncho-ai/harness": "0.5.0",
29
+ "@poncho-ai/harness": "0.6.0",
29
30
  "@poncho-ai/sdk": "0.2.0"
30
31
  },
31
32
  "devDependencies": {
package/src/index.ts CHANGED
@@ -1090,6 +1090,9 @@ export const createRequestHandler = async (options?: {
1090
1090
  let latestRunId = conversation.runtimeRunId ?? "";
1091
1091
  let assistantResponse = "";
1092
1092
  const toolTimeline: string[] = [];
1093
+ const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
1094
+ let currentText = "";
1095
+ let currentTools: string[] = [];
1093
1096
  try {
1094
1097
  const recallCorpus = (await conversationStore.list(ownerId))
1095
1098
  .filter((item) => item.conversationId !== conversationId)
@@ -1106,7 +1109,7 @@ export const createRequestHandler = async (options?: {
1106
1109
  }))
1107
1110
  .filter((item) => item.content.length > 0);
1108
1111
 
1109
- for await (const event of harness.run({
1112
+ for await (const event of harness.runWithTelemetry({
1110
1113
  task: messageText,
1111
1114
  parameters: {
1112
1115
  ...(body.parameters ?? {}),
@@ -1119,25 +1122,48 @@ export const createRequestHandler = async (options?: {
1119
1122
  latestRunId = event.runId;
1120
1123
  }
1121
1124
  if (event.type === "model:chunk") {
1125
+ // If we have tools accumulated and text starts again, push tools as a section
1126
+ if (currentTools.length > 0) {
1127
+ sections.push({ type: "tools", content: currentTools });
1128
+ currentTools = [];
1129
+ }
1122
1130
  assistantResponse += event.content;
1131
+ currentText += event.content;
1123
1132
  }
1124
1133
  if (event.type === "tool:started") {
1125
- toolTimeline.push(`- start \`${event.tool}\``);
1134
+ // If we have text accumulated, push it as a text section
1135
+ if (currentText.length > 0) {
1136
+ sections.push({ type: "text", content: currentText });
1137
+ currentText = "";
1138
+ }
1139
+ const toolText = `- start \`${event.tool}\``;
1140
+ toolTimeline.push(toolText);
1141
+ currentTools.push(toolText);
1126
1142
  }
1127
1143
  if (event.type === "tool:completed") {
1128
- toolTimeline.push(`- done \`${event.tool}\` (${event.duration}ms)`);
1144
+ const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
1145
+ toolTimeline.push(toolText);
1146
+ currentTools.push(toolText);
1129
1147
  }
1130
1148
  if (event.type === "tool:error") {
1131
- toolTimeline.push(`- error \`${event.tool}\`: ${event.error}`);
1149
+ const toolText = `- error \`${event.tool}\`: ${event.error}`;
1150
+ toolTimeline.push(toolText);
1151
+ currentTools.push(toolText);
1132
1152
  }
1133
1153
  if (event.type === "tool:approval:required") {
1134
- toolTimeline.push(`- approval required \`${event.tool}\``);
1154
+ const toolText = `- approval required \`${event.tool}\``;
1155
+ toolTimeline.push(toolText);
1156
+ currentTools.push(toolText);
1135
1157
  }
1136
1158
  if (event.type === "tool:approval:granted") {
1137
- toolTimeline.push(`- approval granted (${event.approvalId})`);
1159
+ const toolText = `- approval granted (${event.approvalId})`;
1160
+ toolTimeline.push(toolText);
1161
+ currentTools.push(toolText);
1138
1162
  }
1139
1163
  if (event.type === "tool:approval:denied") {
1140
- toolTimeline.push(`- approval denied (${event.approvalId})`);
1164
+ const toolText = `- approval denied (${event.approvalId})`;
1165
+ toolTimeline.push(toolText);
1166
+ currentTools.push(toolText);
1141
1167
  }
1142
1168
  if (
1143
1169
  event.type === "run:completed" &&
@@ -1149,6 +1175,13 @@ export const createRequestHandler = async (options?: {
1149
1175
  await telemetry.emit(event);
1150
1176
  response.write(formatSseEvent(event));
1151
1177
  }
1178
+ // Finalize sections
1179
+ if (currentTools.length > 0) {
1180
+ sections.push({ type: "tools", content: currentTools });
1181
+ }
1182
+ if (currentText.length > 0) {
1183
+ sections.push({ type: "text", content: currentText });
1184
+ }
1152
1185
  conversation.messages = [
1153
1186
  ...conversation.messages,
1154
1187
  { role: "user", content: messageText },
@@ -1156,8 +1189,11 @@ export const createRequestHandler = async (options?: {
1156
1189
  role: "assistant",
1157
1190
  content: assistantResponse,
1158
1191
  metadata:
1159
- toolTimeline.length > 0
1160
- ? ({ toolActivity: toolTimeline } as Message["metadata"])
1192
+ toolTimeline.length > 0 || sections.length > 0
1193
+ ? ({
1194
+ toolActivity: toolTimeline,
1195
+ sections: sections.length > 0 ? sections : undefined,
1196
+ } as Message["metadata"])
1161
1197
  : undefined,
1162
1198
  },
1163
1199
  ];
@@ -1246,7 +1282,7 @@ export const runOnce = async (
1246
1282
  return;
1247
1283
  }
1248
1284
 
1249
- for await (const event of harness.run(input)) {
1285
+ for await (const event of harness.runWithTelemetry(input)) {
1250
1286
  await telemetry.emit(event);
1251
1287
  if (event.type === "model:chunk") {
1252
1288
  process.stdout.write(event.content);