@botbotgo/agent-harness 0.0.298 → 0.0.300

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 (166) hide show
  1. package/README.md +78 -38
  2. package/README.zh.md +80 -31
  3. package/dist/acp.d.ts +3 -0
  4. package/dist/acp.js +10 -2
  5. package/dist/api.d.ts +14 -2
  6. package/dist/api.js +19 -3
  7. package/dist/cli.d.ts +18 -1
  8. package/dist/cli.js +1408 -319
  9. package/dist/client/acp.d.ts +9 -3
  10. package/dist/client/acp.js +55 -1
  11. package/dist/client/in-process.d.ts +5 -2
  12. package/dist/client/in-process.js +4 -6
  13. package/dist/client/index.d.ts +1 -1
  14. package/dist/client/types.d.ts +6 -5
  15. package/dist/config/agents/direct.yaml +7 -17
  16. package/dist/config/agents/orchestra.yaml +9 -65
  17. package/dist/config/catalogs/embedding-models.yaml +1 -1
  18. package/dist/config/catalogs/stores.yaml +1 -1
  19. package/dist/config/knowledge/knowledge-runtime.yaml +36 -2
  20. package/dist/config/knowledge/procedural-memory-runtime.yaml +78 -0
  21. package/dist/config/{catalogs/models.yaml → models.yaml} +2 -2
  22. package/dist/config/prompts/direct-system.md +16 -0
  23. package/dist/config/prompts/orchestra-system.md +62 -0
  24. package/dist/config/prompts/routing-system.md +14 -0
  25. package/dist/config/runtime/runtime-memory.yaml +39 -5
  26. package/dist/config/runtime/workspace.yaml +7 -16
  27. package/dist/contracts/runtime.d.ts +242 -1
  28. package/dist/contracts/workspace.d.ts +2 -0
  29. package/dist/index.d.ts +5 -3
  30. package/dist/index.js +2 -1
  31. package/dist/init-project.js +178 -33
  32. package/dist/knowledge/contracts.d.ts +5 -0
  33. package/dist/knowledge/module.d.ts +5 -0
  34. package/dist/knowledge/module.js +340 -18
  35. package/dist/package-version.d.ts +1 -1
  36. package/dist/package-version.js +1 -1
  37. package/dist/persistence/file-store.d.ts +5 -1
  38. package/dist/persistence/file-store.js +16 -0
  39. package/dist/persistence/sqlite-store.d.ts +4 -1
  40. package/dist/persistence/sqlite-store.js +88 -14
  41. package/dist/persistence/types.d.ts +4 -1
  42. package/dist/procedural/config.d.ts +63 -0
  43. package/dist/procedural/config.js +125 -0
  44. package/dist/procedural/index.d.ts +2 -0
  45. package/dist/procedural/index.js +1 -0
  46. package/dist/protocol/ag-ui/http.d.ts +3 -0
  47. package/dist/protocol/ag-ui/http.js +10 -0
  48. package/dist/request-events.d.ts +63 -0
  49. package/dist/request-events.js +400 -0
  50. package/dist/resource/isolation.js +11 -0
  51. package/dist/resource/resource-impl.d.ts +1 -0
  52. package/dist/resource/resource-impl.js +103 -12
  53. package/dist/resources/init-templates/agent-context/deep-research.md +5 -0
  54. package/dist/resources/init-templates/prompts/research-analyst-basic.md +1 -0
  55. package/dist/resources/init-templates/prompts/research-analyst-web-search.md +1 -0
  56. package/dist/resources/init-templates/prompts/research-host-deep-research-basic.md +1 -0
  57. package/dist/resources/init-templates/prompts/research-host-deep-research-web-search.md +1 -0
  58. package/dist/resources/init-templates/prompts/research-host-single-agent-basic.md +1 -0
  59. package/dist/resources/init-templates/prompts/research-host-single-agent-web-search.md +1 -0
  60. package/dist/resources/prompts/runtime/browser-capability-disclaimer-recovery.md +1 -0
  61. package/dist/resources/prompts/runtime/default-subagent.md +2 -0
  62. package/dist/resources/prompts/runtime/durable-memory-context.md +7 -0
  63. package/dist/resources/prompts/runtime/execution-with-tool-evidence-retry.md +1 -0
  64. package/dist/resources/prompts/runtime/execution-with-tool-evidence.md +1 -0
  65. package/dist/resources/prompts/runtime/invalid-tool-selection-recovery.md +1 -0
  66. package/dist/resources/prompts/runtime/memory-manager.md +31 -0
  67. package/dist/resources/prompts/runtime/memory-mutation-reconciliation.md +22 -0
  68. package/dist/resources/prompts/runtime/slash-command-skill.md +6 -0
  69. package/dist/resources/prompts/runtime/strict-tool-json.md +1 -0
  70. package/dist/resources/prompts/runtime/workspace-boundary-guidance.md +3 -0
  71. package/dist/resources/prompts/runtime/workspace-relative-path.md +1 -0
  72. package/dist/resources/prompts/runtime/write-todos-descriptive-content.md +1 -0
  73. package/dist/resources/prompts/runtime/write-todos-full-entry.md +1 -0
  74. package/dist/resources/prompts/runtime/write-todos-non-empty-initial-list.md +1 -0
  75. package/dist/resources/tools/_runtime_tool_helpers.mjs +152 -0
  76. package/dist/resources/tools/cancel_request.mjs +21 -0
  77. package/dist/resources/tools/fetch_url.mjs +23 -0
  78. package/dist/resources/tools/http_request.mjs +30 -0
  79. package/dist/resources/tools/inspect_approvals.mjs +27 -0
  80. package/dist/resources/tools/inspect_artifacts.mjs +21 -0
  81. package/dist/resources/tools/inspect_events.mjs +21 -0
  82. package/dist/resources/tools/inspect_requests.mjs +27 -0
  83. package/dist/resources/tools/inspect_sessions.mjs +21 -0
  84. package/dist/resources/tools/list_files.mjs +27 -0
  85. package/dist/resources/tools/read_artifact.mjs +22 -0
  86. package/dist/resources/tools/request_approval.mjs +27 -0
  87. package/dist/resources/tools/run_command.mjs +21 -0
  88. package/dist/resources/tools/schedule_task.mjs +76 -0
  89. package/dist/resources/tools/search_files.mjs +47 -0
  90. package/dist/resources/tools/send_message.mjs +23 -0
  91. package/dist/runtime/adapter/direct-builtin-utility.d.ts +1 -0
  92. package/dist/runtime/adapter/direct-builtin-utility.js +90 -0
  93. package/dist/runtime/adapter/flow/execution-context.d.ts +1 -1
  94. package/dist/runtime/adapter/flow/execution-context.js +1 -1
  95. package/dist/runtime/adapter/flow/invocation-flow.d.ts +1 -0
  96. package/dist/runtime/adapter/flow/invocation-flow.js +9 -1
  97. package/dist/runtime/adapter/flow/invoke-runtime.d.ts +1 -1
  98. package/dist/runtime/adapter/flow/stream-runtime.d.ts +5 -1
  99. package/dist/runtime/adapter/flow/stream-runtime.js +556 -35
  100. package/dist/runtime/adapter/invocation-result.js +3 -2
  101. package/dist/runtime/adapter/local-tool-invocation.d.ts +1 -1
  102. package/dist/runtime/adapter/local-tool-invocation.js +28 -4
  103. package/dist/runtime/adapter/middleware-assembly.js +3 -1
  104. package/dist/runtime/adapter/model/invocation-request.d.ts +4 -1
  105. package/dist/runtime/adapter/model/invocation-request.js +138 -16
  106. package/dist/runtime/adapter/model/message-assembly.js +2 -6
  107. package/dist/runtime/adapter/model/model-providers.js +103 -5
  108. package/dist/runtime/adapter/resilience.js +17 -2
  109. package/dist/runtime/adapter/runtime-adapter-support.d.ts +11 -7
  110. package/dist/runtime/adapter/runtime-adapter-support.js +39 -5
  111. package/dist/runtime/adapter/tool/builtin-middleware-tools.d.ts +63 -1
  112. package/dist/runtime/adapter/tool/builtin-middleware-tools.js +193 -21
  113. package/dist/runtime/adapter/tool/tool-arguments.d.ts +3 -1
  114. package/dist/runtime/adapter/tool/tool-arguments.js +52 -17
  115. package/dist/runtime/adapter/tool-resolution.d.ts +1 -0
  116. package/dist/runtime/adapter/tool-resolution.js +4 -2
  117. package/dist/runtime/agent-runtime-adapter.d.ts +27 -0
  118. package/dist/runtime/agent-runtime-adapter.js +163 -11
  119. package/dist/runtime/harness/events/event-bus.d.ts +1 -0
  120. package/dist/runtime/harness/events/event-bus.js +3 -0
  121. package/dist/runtime/harness/events/event-sink.d.ts +3 -0
  122. package/dist/runtime/harness/events/event-sink.js +16 -7
  123. package/dist/runtime/harness/events/streaming.d.ts +18 -1
  124. package/dist/runtime/harness/events/streaming.js +23 -10
  125. package/dist/runtime/harness/run/inspection.js +26 -5
  126. package/dist/runtime/harness/run/stream-run.d.ts +13 -4
  127. package/dist/runtime/harness/run/stream-run.js +448 -4
  128. package/dist/runtime/harness/run/surface-semantics.js +7 -34
  129. package/dist/runtime/harness/system/runtime-memory-manager.d.ts +3 -0
  130. package/dist/runtime/harness/system/runtime-memory-manager.js +384 -69
  131. package/dist/runtime/harness/system/runtime-memory-policy.d.ts +20 -1
  132. package/dist/runtime/harness/system/runtime-memory-policy.js +65 -17
  133. package/dist/runtime/harness/system/runtime-memory-records.js +100 -0
  134. package/dist/runtime/harness/system/runtime-memory-sync.js +2 -2
  135. package/dist/runtime/harness/system/store.d.ts +4 -0
  136. package/dist/runtime/harness/system/store.js +153 -0
  137. package/dist/runtime/harness.d.ts +9 -1
  138. package/dist/runtime/harness.js +141 -7
  139. package/dist/runtime/maintenance/sqlite-checkpoint-saver.d.ts +8 -3
  140. package/dist/runtime/maintenance/sqlite-checkpoint-saver.js +152 -53
  141. package/dist/runtime/parsing/output-parsing.d.ts +10 -2
  142. package/dist/runtime/parsing/output-parsing.js +223 -16
  143. package/dist/runtime/parsing/stream-event-parsing.d.ts +7 -0
  144. package/dist/runtime/parsing/stream-event-parsing.js +51 -1
  145. package/dist/runtime/scheduling/system-schedule-manager.d.ts +41 -0
  146. package/dist/runtime/scheduling/system-schedule-manager.js +532 -0
  147. package/dist/runtime/support/embedding-models.d.ts +1 -1
  148. package/dist/runtime/support/embedding-models.js +5 -2
  149. package/dist/runtime/support/runtime-factories.js +1 -1
  150. package/dist/runtime/support/runtime-layout.d.ts +3 -0
  151. package/dist/runtime/support/runtime-layout.js +10 -1
  152. package/dist/runtime/support/runtime-prompts.d.ts +30 -0
  153. package/dist/runtime/support/runtime-prompts.js +55 -0
  154. package/dist/runtime/support/vector-stores.d.ts +1 -1
  155. package/dist/runtime/support/vector-stores.js +5 -2
  156. package/dist/upstream-events.js +8 -7
  157. package/dist/utils/bundled-text.d.ts +3 -0
  158. package/dist/utils/bundled-text.js +25 -0
  159. package/dist/utils/id.js +3 -2
  160. package/dist/workspace/agent-binding-compiler.js +53 -13
  161. package/dist/workspace/object-loader.js +64 -2
  162. package/dist/workspace/support/workspace-ref-utils.d.ts +2 -1
  163. package/dist/workspace/support/workspace-ref-utils.js +24 -5
  164. package/dist/workspace/yaml-object-reader.d.ts +1 -0
  165. package/dist/workspace/yaml-object-reader.js +95 -17
  166. package/package.json +11 -5
package/dist/cli.js CHANGED
@@ -1,23 +1,27 @@
1
1
  #!/usr/bin/env node
2
- import { spawn } from "node:child_process";
3
- import { existsSync, readFileSync } from "node:fs";
2
+ import { EventEmitter } from "node:events";
3
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
4
4
  import { createInterface as createReadlineInterface } from "node:readline";
5
5
  import { Writable } from "node:stream";
6
6
  import path from "node:path";
7
- import { fileURLToPath, pathToFileURL } from "node:url";
7
+ import { fileURLToPath } from "node:url";
8
+ import { pathToFileURL } from "node:url";
8
9
  import YAML from "yaml";
9
10
  import { createAgentHarness } from "./api.js";
10
- import { createAcpHttpHarnessClient, createAcpStdioHarnessClient } from "./client.js";
11
+ import { createAcpHttpHarnessClient, createInProcessHarnessClient, } from "./client.js";
11
12
  import { initProject } from "./init-project.js";
12
13
  import { serveA2aOverHttp } from "./protocol/a2a/http.js";
13
14
  import { serveAgUiOverHttp } from "./protocol/ag-ui/http.js";
14
15
  import { serveAcpOverHttp } from "./protocol/acp/http.js";
15
16
  import { serveAcpOverStdio } from "./protocol/acp/stdio.js";
16
17
  import { serveRuntimeMcpOverStdio } from "./mcp.js";
18
+ import { interpolateEnvPlaceholders } from "./workspace/yaml-object-reader.js";
17
19
  function renderUsage() {
18
20
  return `Usage:
19
21
  agent-harness init <project-name> [--template deep-research|single-agent] [--provider <provider>] [--model <model>] [--with-web-search|--no-web-search]
20
- agent-harness chat [--workspace <path>] [--transport stdio|http] [--host <hostname>] [--port <port>] [--agent <agentId>] [--session <sessionId>] [--message <text>]
22
+ agent-harness chat [-w <path>] [--transport stdio|http] [--host <hostname>] [--port <port>] [--agent <agentId>] [--session <sessionId>] [--message <text>]
23
+ agent-harness [-w <path>] [prompt]
24
+ botbotgo [-w <path>] [prompt]
21
25
  agent-harness acp serve [--workspace <path>] [--transport stdio|http] [--host <hostname>] [--port <port>]
22
26
  agent-harness a2a serve [--workspace <path>] [--host <hostname>] [--port <port>]
23
27
  agent-harness ag-ui serve [--workspace <path>] [--host <hostname>] [--port <port>]
@@ -27,9 +31,14 @@ function renderUsage() {
27
31
  agent-harness runtime approvals watch [--workspace <path>] [--status <pending|approved|edited|rejected|expired>] [--poll-ms <ms>] [--once] [--json]
28
32
  agent-harness runtime requests list [--workspace <path>] [--agent <agentId>] [--session <sessionId>] [--state <state>] [--json]
29
33
  agent-harness runtime requests tail [--workspace <path>] [--agent <agentId>] [--session <sessionId>] [--state <state>] [--poll-ms <ms>] [--once] [--json]
34
+ agent-harness runtime scheduled-run --workspace <path> --schedule <scheduleId>
30
35
  agent-harness runtime export request --workspace <path> --session <sessionId> --request <requestId> [--artifacts] [--artifact-contents] [--health] [--json]
31
36
  agent-harness runtime export session --workspace <path> --session <sessionId> [--artifacts] [--artifact-contents] [--health] [--json]
32
37
  agent-harness runtime-mcp serve [--workspace <path>]
38
+
39
+ Run botbotgo or agent-harness from any folder.
40
+ If ./config/ is absent, the runtime falls back to the bundled system defaults and bundled resources.
41
+ Chat defaults to the current directory as the workspace. Use -w to point at another workspace.
33
42
  `;
34
43
  }
35
44
  function isTemplate(value) {
@@ -171,17 +180,26 @@ function parseChatOptions(args) {
171
180
  let agentId;
172
181
  let sessionId;
173
182
  let message;
183
+ let requestEvents = false;
174
184
  let transport = "stdio";
175
185
  let hostname;
176
186
  let port;
187
+ const positional = [];
177
188
  for (let index = 0; index < args.length; index += 1) {
178
189
  const arg = args[index];
179
- if (arg === "--workspace" || arg === "--agent" || arg === "--session" || arg === "--message" || arg === "--transport" || arg === "--host" || arg === "--port") {
190
+ if (arg === "--workspace"
191
+ || arg === "-w"
192
+ || arg === "--agent"
193
+ || arg === "--session"
194
+ || arg === "--message"
195
+ || arg === "--transport"
196
+ || arg === "--host"
197
+ || arg === "--port") {
180
198
  const value = args[index + 1];
181
199
  if (!value) {
182
- return { workspaceRoot, agentId, sessionId, message, transport, hostname, port, error: `Missing value for ${arg}` };
200
+ return { workspaceRoot, agentId, sessionId, message, requestEvents, transport, hostname, port, error: `Missing value for ${arg}` };
183
201
  }
184
- if (arg === "--workspace") {
202
+ if (arg === "--workspace" || arg === "-w") {
185
203
  workspaceRoot = value;
186
204
  }
187
205
  else if (arg === "--agent") {
@@ -195,7 +213,7 @@ function parseChatOptions(args) {
195
213
  }
196
214
  else if (arg === "--transport") {
197
215
  if (value !== "stdio" && value !== "http") {
198
- return { workspaceRoot, agentId, sessionId, message, transport, hostname, port, error: `Unsupported chat transport: ${value}` };
216
+ return { workspaceRoot, agentId, sessionId, message, requestEvents, transport, hostname, port, error: `Unsupported chat transport: ${value}` };
199
217
  }
200
218
  transport = value;
201
219
  }
@@ -205,16 +223,35 @@ function parseChatOptions(args) {
205
223
  else {
206
224
  const parsedPort = Number.parseInt(value, 10);
207
225
  if (!Number.isFinite(parsedPort) || parsedPort <= 0) {
208
- return { workspaceRoot, agentId, sessionId, message, transport, hostname, port, error: `Invalid chat port: ${value}` };
226
+ return { workspaceRoot, agentId, sessionId, message, requestEvents, transport, hostname, port, error: `Invalid chat port: ${value}` };
209
227
  }
210
228
  port = parsedPort;
211
229
  }
212
230
  index += 1;
213
231
  continue;
214
232
  }
215
- return { workspaceRoot, agentId, sessionId, message, transport, hostname, port, error: `Unknown option: ${arg}` };
233
+ if (arg === "--request-events") {
234
+ requestEvents = true;
235
+ continue;
236
+ }
237
+ if (arg.startsWith("-")) {
238
+ return { workspaceRoot, agentId, sessionId, message, requestEvents, transport, hostname, port, error: `Unknown option: ${arg}` };
239
+ }
240
+ positional.push(arg);
216
241
  }
217
- return { workspaceRoot, agentId, sessionId, message, transport, hostname, port };
242
+ if (!message && positional.length > 0) {
243
+ message = positional.join(" ");
244
+ }
245
+ return { workspaceRoot, agentId, sessionId, message, requestEvents, transport, hostname, port };
246
+ }
247
+ function isTopLevelCliCommand(value) {
248
+ return value === "init"
249
+ || value === "chat"
250
+ || value === "acp"
251
+ || value === "a2a"
252
+ || value === "ag-ui"
253
+ || value === "runtime"
254
+ || value === "runtime-mcp";
218
255
  }
219
256
  function parseRuntimeInspectOptions(args) {
220
257
  let workspaceRoot;
@@ -351,28 +388,390 @@ function parseRuntimeExportOptions(args) {
351
388
  json,
352
389
  };
353
390
  }
391
+ function parseScheduledRunOptions(args) {
392
+ let workspaceRoot;
393
+ let scheduleId;
394
+ for (let index = 0; index < args.length; index += 1) {
395
+ const arg = args[index];
396
+ if (arg === "--workspace" || arg === "--schedule") {
397
+ const value = args[index + 1];
398
+ if (!value) {
399
+ return { workspaceRoot, scheduleId, error: `Missing value for ${arg}` };
400
+ }
401
+ if (arg === "--workspace") {
402
+ workspaceRoot = value;
403
+ }
404
+ else {
405
+ scheduleId = value;
406
+ }
407
+ index += 1;
408
+ continue;
409
+ }
410
+ return { workspaceRoot, scheduleId, error: `Unknown option: ${arg}` };
411
+ }
412
+ return { workspaceRoot, scheduleId };
413
+ }
354
414
  function resolveCliWorkspaceRoot(cwd, inputPath) {
355
415
  const resolved = path.resolve(cwd, inputPath ?? ".");
356
- const configRuntimePath = path.join(resolved, "config", "runtime", "workspace.yaml");
357
- if (existsSync(configRuntimePath)) {
416
+ if (existsSync(path.join(resolved, "config", "runtime", "workspace.yaml"))) {
358
417
  return resolved;
359
418
  }
360
- const directRuntimePath = path.join(resolved, "runtime", "workspace.yaml");
361
- if (existsSync(directRuntimePath) && path.basename(resolved) === "config") {
419
+ if (path.basename(resolved) === "config" && hasCliConfigYaml(resolved)) {
362
420
  return path.dirname(resolved);
363
421
  }
364
422
  return resolved;
365
423
  }
424
+ function getWorkspaceConfigPath(workspaceRoot) {
425
+ return path.join(workspaceRoot, "config", "runtime", "workspace.yaml");
426
+ }
427
+ function hasCliConfigYaml(configRoot) {
428
+ if (!existsSync(configRoot) || !statSync(configRoot).isDirectory()) {
429
+ return false;
430
+ }
431
+ const pending = [configRoot];
432
+ while (pending.length > 0) {
433
+ const current = pending.pop();
434
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
435
+ if (entry.isDirectory()) {
436
+ if (entry.name === "node_modules") {
437
+ continue;
438
+ }
439
+ pending.push(path.join(current, entry.name));
440
+ continue;
441
+ }
442
+ if (entry.isFile() && /\.ya?ml$/i.test(entry.name)) {
443
+ return true;
444
+ }
445
+ }
446
+ }
447
+ return false;
448
+ }
449
+ function validateCliWorkspaceRoot(workspaceRoot, inputPath) {
450
+ if (!existsSync(workspaceRoot)) {
451
+ return undefined;
452
+ }
453
+ if (!statSync(workspaceRoot).isDirectory()) {
454
+ const workspaceLabel = inputPath ? "Workspace path" : "Current directory";
455
+ return `${workspaceLabel} is not a directory: ${workspaceRoot}`;
456
+ }
457
+ return undefined;
458
+ }
366
459
  function resolveCliConfigRoot(workspaceRoot) {
367
- const nested = path.join(workspaceRoot, "config");
368
- if (existsSync(path.join(nested, "runtime", "workspace.yaml"))) {
369
- return nested;
460
+ if (path.basename(workspaceRoot) === "config") {
461
+ return workspaceRoot;
462
+ }
463
+ return path.join(workspaceRoot, "config");
464
+ }
465
+ function frameworkCliWorkspaceRoot() {
466
+ const resolved = fileURLToPath(new URL("..", import.meta.url));
467
+ const distSegment = `${path.sep}dist`;
468
+ if (!resolved.endsWith(distSegment)) {
469
+ return resolved;
470
+ }
471
+ const sourceRoot = resolved.slice(0, -distSegment.length);
472
+ if (!existsSync(sourceRoot)) {
473
+ return resolved;
474
+ }
475
+ try {
476
+ return statSync(sourceRoot).mtimeMs >= statSync(resolved).mtimeMs ? sourceRoot : resolved;
477
+ }
478
+ catch {
479
+ return sourceRoot;
370
480
  }
371
- return workspaceRoot;
372
481
  }
373
482
  function renderJson(value) {
374
483
  return `${JSON.stringify(value, null, 2)}\n`;
375
484
  }
485
+ function formatTreeBranch(isLast) {
486
+ return isLast ? "└─" : "├─";
487
+ }
488
+ function formatTreeIndent(ancestorLast) {
489
+ return ancestorLast ? " " : "│ ";
490
+ }
491
+ function applyAnsi(text, code, enabled) {
492
+ if (!enabled) {
493
+ return text;
494
+ }
495
+ return `\x1b[${code}m${text}\x1b[0m`;
496
+ }
497
+ function renderRequestStateBadge(state, color) {
498
+ if (state === "completed") {
499
+ return color ? `\x1b[32;1m✔ DONE\x1b[0m` : "✔ DONE";
500
+ }
501
+ if (state === "running") {
502
+ return color ? `\x1b[36;1m▶ RUNNING\x1b[0m` : "▶ RUNNING";
503
+ }
504
+ if (state === "waiting" || state === "waiting_for_approval") {
505
+ return color ? `\x1b[33;1m⚠ WAITING\x1b[0m` : "⚠ WAITING";
506
+ }
507
+ if (state === "failed") {
508
+ return color ? `\x1b[31;1m✘ FAILED\x1b[0m` : "✘ FAILED";
509
+ }
510
+ if (state === "cancelled") {
511
+ return color ? `\x1b[31m○ CANCELLED\x1b[0m` : "○ CANCELLED";
512
+ }
513
+ if (state === "queued") {
514
+ return color ? `\x1b[38;5;242m· QUEUED\x1b[0m` : "· QUEUED";
515
+ }
516
+ return color ? `\x1b[37m${state.toUpperCase()}\x1b[0m` : state.toUpperCase();
517
+ }
518
+ function summarizeRequestPlan(snapshot) {
519
+ const summary = snapshot.plan.summary;
520
+ const eventCount = flattenRequestExecutionSteps(snapshot).length;
521
+ const parts = [
522
+ `${summary.total} todo${summary.total === 1 ? "" : "s"}`,
523
+ summary.inProgress > 0 ? `${summary.inProgress} active` : undefined,
524
+ summary.completed > 0 ? `${summary.completed} done` : undefined,
525
+ summary.failed > 0 ? `${summary.failed} failed` : undefined,
526
+ eventCount > 0 ? `${eventCount} event${eventCount === 1 ? "" : "s"}` : undefined,
527
+ ].filter((part) => typeof part === "string");
528
+ return parts.join(" · ");
529
+ }
530
+ function summarizeRequestOutput(output, color) {
531
+ const trimmed = output.trim();
532
+ if (!trimmed) {
533
+ return applyAnsi("empty", "2", color);
534
+ }
535
+ const normalized = trimmed.replace(/\s+/g, " ");
536
+ const preview = normalized.length > 72 ? `${normalized.slice(0, 69)}...` : normalized;
537
+ return applyAnsi(preview, "2", color);
538
+ }
539
+ function formatCompactClockTime(value) {
540
+ const date = new Date(value);
541
+ if (Number.isNaN(date.getTime())) {
542
+ return value;
543
+ }
544
+ return new Intl.DateTimeFormat("en-US", {
545
+ hour12: false,
546
+ hour: "2-digit",
547
+ minute: "2-digit",
548
+ second: "2-digit",
549
+ }).format(date);
550
+ }
551
+ function renderTimeRange(startedAt, endedAt, detail, color) {
552
+ if (!startedAt && !endedAt) {
553
+ return "";
554
+ }
555
+ const from = startedAt ? formatCompactClockTime(startedAt) : "?";
556
+ const to = endedAt ? formatCompactClockTime(endedAt) : "now";
557
+ const durationMs = typeof detail?.durationMs === "number" ? detail.durationMs : undefined;
558
+ const durationLabel = durationMs !== undefined
559
+ ? ` · ${(durationMs / 1000).toFixed(durationMs >= 10_000 ? 0 : durationMs >= 1_000 ? 1 : 3)}s`
560
+ : "";
561
+ return ` ${applyAnsi(`[${from} -> ${to}${durationLabel}]`, "2", color)}`;
562
+ }
563
+ function readStepSkillNames(step) {
564
+ const value = step.detail?.skillNames;
565
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
566
+ }
567
+ function buildRequestStepEventKey(step) {
568
+ return `${step.kind}:${step.id}`;
569
+ }
570
+ function resolveEventTimestamp(step) {
571
+ return step.occurredAt ?? step.endedAt ?? step.startedAt;
572
+ }
573
+ function summarizeRequestStepDetail(step, displayName, color) {
574
+ if (step.kind === "memory" && step.detail?.source === "runtime-durable-memory") {
575
+ const count = typeof step.detail.count === "number" ? step.detail.count : undefined;
576
+ const scopes = Array.isArray(step.detail.scopes)
577
+ ? step.detail.scopes.filter((item) => typeof item === "string" && item.trim().length > 0)
578
+ : [];
579
+ const countLabel = count !== undefined ? `${count} record${count === 1 ? "" : "s"}` : undefined;
580
+ const scopeLabel = scopes.length > 0 ? scopes.join(", ") : undefined;
581
+ const summary = typeof step.detail.summary === "string" ? step.detail.summary.trim() : "";
582
+ const detailLabel = [countLabel, scopeLabel].filter((value) => Boolean(value)).join(" · ");
583
+ if (detailLabel) {
584
+ return ` ${applyAnsi(`(${detailLabel})`, "2", color)}`;
585
+ }
586
+ if (summary && !summary.toLowerCase().includes(displayName.toLowerCase())) {
587
+ const preview = summary.length > 48 ? `${summary.slice(0, 45)}...` : summary;
588
+ return ` ${applyAnsi(`(${preview})`, "2", color)}`;
589
+ }
590
+ return "";
591
+ }
592
+ if (step.ownerAgentId && step.ownerAgentId !== step.agentId) {
593
+ return ` ${applyAnsi(`(from ${step.ownerAgentId})`, "2", color)}`;
594
+ }
595
+ return "";
596
+ }
597
+ function renderRequestStepLine(step, prefix, isLast, _activeEventId, color = false) {
598
+ const skillNames = step.kind === "skill" ? readStepSkillNames(step) : [];
599
+ const profileDisplayName = step.id.startsWith("profile:") && step.kind === "agent"
600
+ ? `${step.name} ${step.action ?? ""}`.trim()
601
+ : undefined;
602
+ const displayName = profileDisplayName
603
+ ? profileDisplayName
604
+ : step.kind === "skill" && skillNames.length === 1
605
+ ? skillNames[0]
606
+ : step.kind === "skill" && skillNames.length > 1
607
+ ? skillNames.join(", ")
608
+ : step.name;
609
+ const icon = STEP_KIND_ICON[step.kind] ?? "·";
610
+ const kindLabel = color
611
+ ? `\x1b[38;5;242m${icon} ${step.kind}\x1b[0m`
612
+ : `${icon} ${step.kind}`;
613
+ const stIcon = statusIcon(step.status === "started" ? "in_progress" : step.status, color);
614
+ const nameFormatted = color ? `\x1b[38;5;252m${displayName}\x1b[0m` : displayName;
615
+ const eventTime = resolveEventTimestamp(step);
616
+ const timeLabel = eventTime ? applyAnsi(`[${formatCompactClockTime(eventTime)}]`, "2", color) : applyAnsi("[time?]", "2", color);
617
+ const detailSuffix = summarizeRequestStepDetail(step, displayName, color);
618
+ return `${prefix}${formatTreeBranch(isLast)} ${timeLabel} ${stIcon} ${kindLabel} ${nameFormatted}${detailSuffix}`;
619
+ }
620
+ function resolveRequestStepDisplayName(step) {
621
+ const skillNames = step.kind === "skill" ? readStepSkillNames(step) : [];
622
+ if (step.id.startsWith("profile:") && step.kind === "agent") {
623
+ return `${step.name} ${step.action}`.trim();
624
+ }
625
+ if (step.kind === "skill" && skillNames.length === 1) {
626
+ return skillNames[0];
627
+ }
628
+ if (step.kind === "skill" && skillNames.length > 1) {
629
+ return skillNames.join(", ");
630
+ }
631
+ return step.name;
632
+ }
633
+ function isInternalAgentLifecycleStep(step) {
634
+ return step.kind === "agent" && step.id.startsWith("profile:agent:");
635
+ }
636
+ function isVisibleRequestTreeStep(step) {
637
+ if (step.kind === "skill") {
638
+ return false;
639
+ }
640
+ return !isInternalAgentLifecycleStep(step);
641
+ }
642
+ function getVisibleTodoEvents(todo) {
643
+ return todo.events.filter(isVisibleRequestTreeStep);
644
+ }
645
+ function getVisibleRootEvents(snapshot) {
646
+ return snapshot.events.filter(isVisibleRequestTreeStep);
647
+ }
648
+ function flattenRequestExecutionSteps(snapshot) {
649
+ return [
650
+ ...snapshot.plan.items.flatMap((item) => getVisibleTodoEvents(item)),
651
+ ...getVisibleRootEvents(snapshot),
652
+ ];
653
+ }
654
+ function buildExecutionStepEventKey(step) {
655
+ return [
656
+ buildRequestStepEventKey(step),
657
+ step.status,
658
+ step.startedAt ?? "",
659
+ step.endedAt ?? "",
660
+ ].join("|");
661
+ }
662
+ function renderRequestSnapshotTree(snapshot, color = false, timingSummary) {
663
+ const lines = [];
664
+ const eventSteps = flattenRequestExecutionSteps(snapshot);
665
+ const rootEvents = getVisibleRootEvents(snapshot);
666
+ // Header row: status badge + agent
667
+ const badge = renderRequestStateBadge(snapshot.state, color);
668
+ const agentLabel = snapshot.agentId
669
+ ? (color ? `\x1b[38;5;33m${snapshot.agentId}\x1b[0m` : snapshot.agentId)
670
+ : applyAnsi("unknown", "2", color);
671
+ const headerLabel = color ? `\x1b[1;37mREQUEST\x1b[0m` : "REQUEST";
672
+ lines.push(`${headerLabel} ${badge} ${applyAnsi("agent:", "2", color)}${agentLabel}`);
673
+ // IDs (compact, muted)
674
+ const muted = (s) => applyAnsi(s, "2", color);
675
+ lines.push(`├─ ${muted("session")} ${applyAnsi(ellipsizeChatId(snapshot.sessionId, 24), "2", color)}`);
676
+ lines.push(`├─ ${muted("request")} ${applyAnsi(ellipsizeChatId(snapshot.requestId, 24), "2", color)}`);
677
+ // Summary + timing on one line
678
+ const planSummary = summarizeRequestPlan(snapshot);
679
+ const timing = timingSummary ?? "collecting";
680
+ lines.push(`├─ ${muted("plan")} ${applyAnsi(`v${snapshot.plan.version}`, "2", color)} ${muted("·")} ${planSummary ? applyAnsi(planSummary, "2", color) : muted("no tracked work yet")}`);
681
+ lines.push(`├─ ${muted("timing")} ${applyAnsi(timing, "2", color)}`);
682
+ // Todo items
683
+ lines.push(`├─ ${muted("todos")}`);
684
+ const planPrefix = `${formatTreeIndent(false)}`;
685
+ if (snapshot.plan.items.length === 0) {
686
+ lines.push(`${planPrefix}└─ ${muted("none yet")}`);
687
+ }
688
+ else {
689
+ snapshot.plan.items.forEach((todo, todoIndex) => {
690
+ const todoEvents = getVisibleTodoEvents(todo);
691
+ const todoIsLast = todoIndex === snapshot.plan.items.length - 1
692
+ && rootEvents.length === 0
693
+ && !snapshot.approval;
694
+ const todoActive = todo.key === snapshot.activeTodoKey
695
+ ? (color ? ` \x1b[36m●\x1b[0m` : " ●")
696
+ : "";
697
+ const tIcon = statusIcon(todo.status, color);
698
+ const todoTimeRange = renderTimeRange(todo.startedAt, todo.endedAt, undefined, color);
699
+ const todoContent = color ? `\x1b[38;5;253m${todo.content}\x1b[0m` : todo.content;
700
+ lines.push(`${planPrefix}${formatTreeBranch(todoIsLast)} ${tIcon} ${todoContent}${todoTimeRange}${todoActive}`);
701
+ if (todoEvents.length > 0) {
702
+ const todoEventPrefix = `${planPrefix}${formatTreeIndent(todoIsLast)}`;
703
+ todoEvents.forEach((step, eventIndex) => {
704
+ lines.push(renderRequestStepLine(step, todoEventPrefix, eventIndex === todoEvents.length - 1, snapshot.activeEventId, color));
705
+ });
706
+ }
707
+ });
708
+ }
709
+ // Root-level execution events only: keep streamed data outside the tree.
710
+ if (rootEvents.length > 0) {
711
+ lines.push(`├─ ${muted("events")}`);
712
+ rootEvents.forEach((step, index) => {
713
+ lines.push(renderRequestStepLine(step, `${formatTreeIndent(false)}`, index === rootEvents.length - 1 && !snapshot.approval, snapshot.activeEventId, color));
714
+ });
715
+ }
716
+ // Approval
717
+ if (snapshot.approval) {
718
+ const approvalStatus = snapshot.approval.status ?? "pending";
719
+ const approvalColor = approvalStatus === "approved" ? "32" : approvalStatus === "rejected" ? "31" : "33";
720
+ lines.push(`└─ ${color ? `\x1b[33m⚠ approval\x1b[0m` : "⚠ approval"} ${muted("·")} ${snapshot.approval.toolName ?? "unknown-tool"} ${applyAnsi(`(${approvalStatus})`, approvalColor, color)}`);
721
+ }
722
+ return `${lines.join("\n")}\n`;
723
+ }
724
+ function renderRequestEventContinuation(snapshot, fromIndex, color = false) {
725
+ const eventSteps = flattenRequestExecutionSteps(snapshot).slice(fromIndex);
726
+ if (eventSteps.length === 0) {
727
+ return "";
728
+ }
729
+ const muted = (s) => applyAnsi(s, "2", color);
730
+ const lines = [`${muted("events (continued)")}`];
731
+ eventSteps.forEach((step, index) => {
732
+ lines.push(renderRequestStepLine(step, "", index === eventSteps.length - 1, snapshot.activeEventId, color));
733
+ });
734
+ return `${lines.join("\n")}\n`;
735
+ }
736
+ function buildTodoContinuationSignature(snapshot) {
737
+ return snapshot.plan.items
738
+ .map((item) => [
739
+ item.id ?? "",
740
+ item.content,
741
+ item.status,
742
+ item.ownerAgentId ?? "",
743
+ item.startedAt ?? "",
744
+ item.endedAt ?? "",
745
+ ].join("|"))
746
+ .join(";");
747
+ }
748
+ function renderRequestTodoContinuation(snapshot, previousSignature, color = false) {
749
+ const nextSignature = buildTodoContinuationSignature(snapshot);
750
+ if (!nextSignature || nextSignature === previousSignature || snapshot.plan.items.length === 0) {
751
+ return "";
752
+ }
753
+ const muted = (s) => applyAnsi(s, "2", color);
754
+ const lines = [`${muted("todos (continued)")}`];
755
+ snapshot.plan.items.forEach((todo, index) => {
756
+ const tIcon = statusIcon(todo.status, color);
757
+ const todoTimeRange = renderTimeRange(todo.startedAt, todo.endedAt, undefined, color);
758
+ const todoContent = color ? `\x1b[38;5;253m${todo.content}\x1b[0m` : todo.content;
759
+ lines.push(`${formatTreeBranch(index === snapshot.plan.items.length - 1)} ${tIcon} ${todoContent}${todoTimeRange}`);
760
+ });
761
+ return `${lines.join("\n")}\n`;
762
+ }
763
+ function buildTerminalRequestSnapshot(snapshot, result) {
764
+ return {
765
+ ...snapshot,
766
+ state: result.state,
767
+ updatedAt: new Date().toISOString(),
768
+ output: result.output,
769
+ activeEventId: undefined,
770
+ };
771
+ }
772
+ function countRenderedLines(text) {
773
+ return text.replace(/\n$/, "").split("\n").length;
774
+ }
376
775
  function isObject(value) {
377
776
  return typeof value === "object" && value !== null && !Array.isArray(value);
378
777
  }
@@ -445,6 +844,39 @@ function renderApprovalList(approvals) {
445
844
  return `${approvalId} status=${status} tool=${toolName}${sessionId}${requestId}${reason}${requested}${resolved}`;
446
845
  }).join("\n") + "\n";
447
846
  }
847
+ function renderPlanResultSummary(result) {
848
+ if (typeof result === "string" && result.trim().length > 0) {
849
+ return result.trim();
850
+ }
851
+ if (!isObject(result)) {
852
+ return undefined;
853
+ }
854
+ const summary = typeof result.summary === "string" && result.summary.trim().length > 0
855
+ ? result.summary.trim()
856
+ : typeof result.message === "string" && result.message.trim().length > 0
857
+ ? result.message.trim()
858
+ : undefined;
859
+ if (summary) {
860
+ return summary;
861
+ }
862
+ const serialized = JSON.stringify(result);
863
+ return serialized && serialized !== "{}" ? serialized : undefined;
864
+ }
865
+ function renderChatPlanState(planState) {
866
+ const lines = [`[plan v${planState.version}] ${planState.summary.total} todo(s)`];
867
+ if (planState.summary.total === 0) {
868
+ lines.push("No todos tracked yet.");
869
+ return `${lines.join("\n")}\n`;
870
+ }
871
+ for (const [index, item] of planState.items.entries()) {
872
+ lines.push(`${index + 1}. [${item.status}] ${item.content}`);
873
+ const resultSummary = renderPlanResultSummary(item.result);
874
+ if (resultSummary) {
875
+ lines.push(` result: ${resultSummary}`);
876
+ }
877
+ }
878
+ return `${lines.join("\n")}\n`;
879
+ }
448
880
  function renderRequestList(requests) {
449
881
  if (requests.length === 0) {
450
882
  return "No requests matched.\n";
@@ -500,41 +932,64 @@ function renderOperatorOverview(overview, workspacePath) {
500
932
  return `${lines.join("\n")}\n`;
501
933
  }
502
934
  function renderChatHelp() {
503
- const rows = [
504
- ["/context", "Show the current agent/session/request context"],
505
- ["/new", "Clear the current session/request context"],
506
- ["/help", "Show chat commands"],
507
- ["/agent <agentId>", "Switch the active agent for new requests"],
508
- ["/session", "Show current session id"],
509
- ["/request [requestId]", "Show or switch the active request id"],
510
- ["/sessions", "List recent sessions"],
511
- ["/requests", "List requests for the current session or agent"],
512
- ["/resume <sessionId>", "Switch chat to an existing session"],
513
- ["/cancel", "Cancel the latest active request"],
514
- ["/approvals", "List pending approvals for the current session"],
515
- ["/approve <id>", "Approve a pending approval"],
516
- ["/reject <id>", "Reject a pending approval"],
517
- ["/events", "Show persisted events for the latest request"],
518
- ["/trace", "Show persisted trace items for the latest request"],
519
- ["/health", "Show runtime health"],
520
- ["/overview", "Show runtime overview"],
521
- ["/exit", "Exit chat"],
935
+ const sections = [
936
+ {
937
+ heading: "Context & navigation",
938
+ rows: [
939
+ ["/context", "Show current agent / session / request"],
940
+ ["/new", "Clear session — start a fresh conversation"],
941
+ ["/agent <id>", "Switch agent for new requests"],
942
+ ["/session", "Print current session id"],
943
+ ["/sessions", "List recent sessions"],
944
+ ["/resume <id>", "Resume an existing session"],
945
+ ["/request [id]", "Show or select active request"],
946
+ ["/requests", "List requests for current session / agent"],
947
+ ],
948
+ },
949
+ {
950
+ heading: "Approvals",
951
+ rows: [
952
+ ["/approvals", "List pending approvals"],
953
+ ["/approve <id>", "Approve a pending approval"],
954
+ ["/reject <id>", "Reject a pending approval"],
955
+ ["/cancel", "Cancel the latest active request"],
956
+ ],
957
+ },
958
+ {
959
+ heading: "Diagnostics",
960
+ rows: [
961
+ ["/events", "Persisted events for the latest request"],
962
+ ["/trace", "Trace items for the latest request"],
963
+ ["/health", "Runtime health snapshot"],
964
+ ["/overview", "Runtime operator overview"],
965
+ ],
966
+ },
967
+ {
968
+ heading: "Session",
969
+ rows: [
970
+ ["/help", "Show this help"],
971
+ ["/exit", "Quit chat"],
972
+ ],
973
+ },
522
974
  ];
523
- const cmdW = Math.max(...rows.map(([cmd]) => cmd.length));
524
- const lines = rows.map(([cmd, desc]) => ` ${cmd.padEnd(cmdW)} ${desc}`);
525
- return [
526
- "Commands:",
527
- "────────",
528
- ...lines,
529
- "",
530
- "Starter tasks:",
531
- "─────────────",
532
- " - Inspect this workspace and explain the main entry points.",
533
- " - Review this project structure before making any edits.",
534
- " - Update README.md to make the setup steps clearer.",
535
- " - Find the likeliest config issue in this workspace and propose the smallest fix.",
536
- "",
537
- ].join("\n");
975
+ const allRows = sections.flatMap((s) => s.rows);
976
+ const cmdW = Math.max(...allRows.map(([cmd]) => cmd.length));
977
+ const out = [""];
978
+ for (const section of sections) {
979
+ out.push(` ${section.heading}`);
980
+ out.push(` ${"─".repeat(section.heading.length)}`);
981
+ for (const [cmd, desc] of section.rows) {
982
+ out.push(` ${cmd.padEnd(cmdW)} ${desc}`);
983
+ }
984
+ out.push("");
985
+ }
986
+ out.push(" Starter tasks:");
987
+ out.push(" ─────────────");
988
+ out.push(" Inspect this workspace and explain the main entry points.");
989
+ out.push(" Review this project structure before making any edits.");
990
+ out.push(" Find the likeliest config issue and propose the smallest fix.");
991
+ out.push("");
992
+ return out.join("\n");
538
993
  }
539
994
  function trimAsciiBlock(block) {
540
995
  return block
@@ -558,8 +1013,31 @@ const CHAT_ASCII_AGENT_HARNESS = trimAsciiBlock(" _ ____ _____ _ _ ____
558
1013
  * BOTBOTGO ≈ US flag: blue / white / red / white / red stripes top→bottom.
559
1014
  * AGENT HARNESS ≈ 中国国旗: upper rows gold (星区黄), lower rows field red.
560
1015
  */
561
- const CHAT_LOGO_BRAND_LINE_COLORS = [27, 255, 196, 255, 196];
562
- const CHAT_LOGO_PRODUCT_LINE_COLORS = [226, 226, 196, 196, 196];
1016
+ const CHAT_LOGO_BRAND_LINE_COLORS = [25, 238, 160, 238, 160];
1017
+ const CHAT_LOGO_PRODUCT_LINE_COLORS = [136, 136, 160, 160, 160];
1018
+ /** Step-kind icons for the request tree */
1019
+ const STEP_KIND_ICON = {
1020
+ llm: "◆",
1021
+ tool: "⚙",
1022
+ skill: "★",
1023
+ agent: "◉",
1024
+ memory: "◈",
1025
+ approval: "⚠",
1026
+ };
1027
+ /** Status icons for todos and steps */
1028
+ function statusIcon(status, color) {
1029
+ if (status === "completed" || status === "done")
1030
+ return applyAnsi("✔", "32", color);
1031
+ if (status === "in_progress" || status === "started")
1032
+ return applyAnsi("▶", "36", color);
1033
+ if (status === "failed")
1034
+ return applyAnsi("✘", "31", color);
1035
+ if (status === "cancelled")
1036
+ return applyAnsi("○", "31", color);
1037
+ if (status === "pending" || status === "queued")
1038
+ return applyAnsi("·", "238", color);
1039
+ return applyAnsi("?", "37", color);
1040
+ }
563
1041
  /** One foreground color per entire line — flat, no relief shading */
564
1042
  function colorizeSolidAsciiBlock(block, lineColors, enabled) {
565
1043
  const trimmed = trimAsciiBlock(block);
@@ -583,15 +1061,23 @@ function ellipsizeChatId(value, maxChars) {
583
1061
  }
584
1062
  return `${value.slice(0, maxChars - 1)}…`;
585
1063
  }
586
- function renderChatPromptLine(input) {
1064
+ export function renderChatPromptLine(input) {
1065
+ const c = input.color;
587
1066
  const agent = input.agentId ?? "—";
588
- const separator = " ────────────────────────────────────────";
589
- if (!input.color) {
590
- return `${separator}\n botbotgo agent ${agent} `;
591
- }
592
- return (` \x1b[38;5;240m${"─".repeat(40)}\x1b[0m\n ` +
593
- `\x1b[1;36mbotbotgo\x1b[0m \x1b[38;5;240m│\x1b[0m ` +
594
- `\x1b[90magent\x1b[0m \x1b[97m${agent}\x1b[0m \x1b[32m›\x1b[0m `);
1067
+ const sessionShort = input.sessionId ? ellipsizeChatId(input.sessionId, 8) : null;
1068
+ if (!c) {
1069
+ const ctx = sessionShort ? ` [${sessionShort}]` : "";
1070
+ return `\n ─────────────────────────────────────────────\n ❯ agent:${agent}${ctx} › `;
1071
+ }
1072
+ const sep = ` \x1b[38;5;236m${"─".repeat(45)}\x1b[0m`;
1073
+ // left: brand tag + agent
1074
+ const brand = `\x1b[38;5;33m◆\x1b[0m \x1b[1;37magent\x1b[0m\x1b[38;5;242m:\x1b[0m\x1b[38;5;252m${agent}\x1b[0m`;
1075
+ // right: session pill (muted)
1076
+ const sessionTag = sessionShort
1077
+ ? ` \x1b[38;5;238m[\x1b[0m\x1b[38;5;244m${sessionShort}\x1b[0m\x1b[38;5;238m]\x1b[0m`
1078
+ : "";
1079
+ const arrow = `\x1b[38;5;33m›\x1b[0m`;
1080
+ return `\n${sep}\n ${brand}${sessionTag} ${arrow} `;
595
1081
  }
596
1082
  async function* iterateChatLines(rl, nextPrompt) {
597
1083
  rl.setPrompt(nextPrompt());
@@ -607,49 +1093,72 @@ async function* iterateChatLines(rl, nextPrompt) {
607
1093
  rl.prompt();
608
1094
  }
609
1095
  }
610
- function renderChatBanner(input) {
1096
+ export function renderChatBanner(input) {
611
1097
  const color = input.color === true;
612
- const subtitle = "ACP workspace shell · @botbotgo/agent-harness";
613
- const labelW = 12;
614
- const rows = [
615
- ["Workspace", input.workspacePath],
616
- ["Transport", input.transport],
617
- ];
618
- if (input.agentId) {
619
- rows.push(["Agent", input.agentId]);
620
- }
621
- if (input.sessionId) {
622
- rows.push(["Session", input.sessionId]);
623
- }
624
- const bodyLines = rows.map(([label, value]) => `${label.padEnd(labelW)} ${value}`);
625
- const inner = Math.max(subtitle.length, ...bodyLines.map((line) => line.length), 44);
626
- const horizontal = (left, mid, right) => ` ${left}${mid.repeat(inner + 2)}${right}`;
627
- const boxed = (text) => ` │ ${text.padEnd(inner)} │`;
628
- const logoWidth = Math.max(...CHAT_ASCII_BOTBOTGO.split("\n").map((line) => line.length), ...CHAT_ASCII_AGENT_HARNESS.split("\n").map((line) => line.length), inner + 4);
629
- const ruleLen = Math.min(logoWidth, 96);
630
- const rulePlain = ` ${"·".repeat(ruleLen)}`;
631
- const rule = color ? `\x1b[38;5;253m${rulePlain}\x1b[0m` : rulePlain;
1098
+ // Logo block (single ASCII art)
632
1099
  const brandArt = colorizeSolidAsciiBlock(CHAT_ASCII_BOTBOTGO, CHAT_LOGO_BRAND_LINE_COLORS, color);
633
1100
  const productArt = colorizeSolidAsciiBlock(CHAT_ASCII_AGENT_HARNESS, CHAT_LOGO_PRODUCT_LINE_COLORS, color);
634
- const blockSepWidth = Math.max(36, ruleLen - 4);
635
- const blockSep = color ? ` \x1b[38;5;255m${"─".repeat(blockSepWidth)}\x1b[0m` : "";
636
- const boxBorder = color ? "\x1b[38;5;27m" : "";
637
- const boxBorderReset = color ? "\x1b[0m" : "";
638
- const horizontalColored = (left, mid, right) => color
639
- ? ` ${boxBorder}${left}${boxBorderReset}\x1b[38;5;253m${mid.repeat(inner + 2)}\x1b[0m${boxBorder}${right}${boxBorderReset}`
640
- : horizontal(left, mid, right);
641
- const boxedColored = (text) => color
642
- ? ` ${boxBorder}│${boxBorderReset} \x1b[38;5;255m${text.padEnd(inner)}\x1b[0m ${boxBorder}│${boxBorderReset}`
643
- : boxed(text);
1101
+ const logoWidth = Math.max(...CHAT_ASCII_BOTBOTGO.split("\n").map((l) => l.length), ...CHAT_ASCII_AGENT_HARNESS.split("\n").map((l) => l.length));
1102
+ const ruleLen = Math.min(logoWidth + 4, 96);
1103
+ const dimRule = (ch, len) => color ? `\x1b[38;5;237m${ch.repeat(len)}\x1b[0m` : ch.repeat(len);
1104
+ // Info panel
1105
+ const labelColor = color ? "\x1b[38;5;242m" : "";
1106
+ const valueColor = color ? "\x1b[38;5;252m" : "";
1107
+ const accentColor = color ? "\x1b[38;5;33m" : ""; // bright cyan-blue
1108
+ const resetColor = color ? "\x1b[0m" : "";
1109
+ const mutedColor = color ? "\x1b[38;5;238m" : "";
1110
+ const borderColor = color ? "\x1b[38;5;237m" : "";
1111
+ const kvRow = (label, value, labelW, accent = false) => {
1112
+ const lPad = label.padEnd(labelW);
1113
+ const lFormatted = `${labelColor}${lPad}${resetColor}`;
1114
+ const vFormatted = accent
1115
+ ? `${accentColor}${value}${resetColor}`
1116
+ : `${valueColor}${value}${resetColor}`;
1117
+ return ` ${borderColor}│${resetColor} ${lFormatted} ${mutedColor}·${resetColor} ${vFormatted}`;
1118
+ };
1119
+ const rows = [
1120
+ ["workspace", input.workspacePath, false],
1121
+ ["transport", input.transport, false],
1122
+ ];
1123
+ if (input.agentId)
1124
+ rows.push(["agent", input.agentId, true]);
1125
+ if (input.sessionId)
1126
+ rows.push(["session", input.sessionId, false]);
1127
+ const labelW = Math.max(...rows.map(([l]) => l.length));
1128
+ const panelInner = Math.max(...rows.map(([, v]) => labelW + 4 + v.length), 44);
1129
+ const hLine = (l, m, r) => color
1130
+ ? ` ${borderColor}${l}${resetColor}${borderColor}${m.repeat(panelInner + 2)}${r}${resetColor}`
1131
+ : ` ${l}${m.repeat(panelInner + 2)}${r}`;
1132
+ const versionTagText = "agent-harness";
1133
+ const versionTag = color
1134
+ ? `${mutedColor}agent-harness${resetColor}`
1135
+ : versionTagText;
1136
+ const versionDividerSpan = Math.max(36, ruleLen);
1137
+ const versionGap = 2;
1138
+ const versionRuleBudget = Math.max(24, versionDividerSpan - versionTagText.length - versionGap * 2);
1139
+ const versionRuleLeftLen = Math.floor(versionRuleBudget / 2);
1140
+ const versionRuleRightLen = versionRuleBudget - versionRuleLeftLen;
644
1141
  const hint = color
645
- ? ` \x1b[2m\x1b[38;5;253mType /help for commands and starter tasks\x1b[0m`
646
- : ` Type /help for commands and starter tasks`;
647
- const mid = [brandArt, ""];
648
- if (color) {
649
- mid.push(blockSep, "");
650
- }
651
- mid.push(productArt, "", rule, "", horizontalColored("╭", "─", "╮"), boxedColored(subtitle), horizontalColored("├", "─", "┤"), ...bodyLines.map((line) => boxedColored(line)), horizontalColored("╰", "─", "╯"), "", rule, hint, "");
652
- return ["", ...mid].join("\n");
1142
+ ? ` ${mutedColor}Type \x1b[38;5;246m/help\x1b[0m${mutedColor} for commands · \x1b[38;5;246m/exit\x1b[0m${mutedColor} to quit${resetColor}`
1143
+ : ` Type /help for commands · /exit to quit`;
1144
+ const lines = [
1145
+ "",
1146
+ brandArt,
1147
+ "",
1148
+ ` ${dimRule("─", Math.max(36, ruleLen - 4))}`,
1149
+ "",
1150
+ productArt,
1151
+ "",
1152
+ ` ${dimRule("─", versionRuleLeftLen)}${" ".repeat(versionGap)}${versionTag}${" ".repeat(versionGap)}${dimRule("─", versionRuleRightLen)}`,
1153
+ "",
1154
+ hLine("╭", "─", "╮"),
1155
+ ...rows.map(([l, v, accent]) => kvRow(l, v, labelW, accent)),
1156
+ hLine("╰", "─", "╯"),
1157
+ "",
1158
+ hint,
1159
+ "",
1160
+ ];
1161
+ return lines.join("\n");
653
1162
  }
654
1163
  function renderRequestEvents(events) {
655
1164
  if (events.length === 0) {
@@ -688,11 +1197,14 @@ function renderRequestTraceItems(items) {
688
1197
  }).join("\n") + "\n";
689
1198
  }
690
1199
  function renderChatContext(input) {
691
- return [
692
- `agent=${input.agentId ?? "none"}`,
693
- `session=${input.sessionId ?? "none"}`,
694
- `request=${input.requestId ?? "none"}`,
695
- ].join(" ") + "\n";
1200
+ const rows = [
1201
+ ["agent", input.agentId ?? ""],
1202
+ ["session", input.sessionId ?? ""],
1203
+ ["request", input.requestId ?? ""],
1204
+ ];
1205
+ const labelW = Math.max(...rows.map(([l]) => l.length));
1206
+ const lines = rows.map(([label, value]) => ` ${label.padEnd(labelW)} ${value}`);
1207
+ return ["", " Context", " ───────", ...lines, ""].join("\n");
696
1208
  }
697
1209
  function normalizeChatCommand(line) {
698
1210
  const trimmed = line.trim();
@@ -709,40 +1221,100 @@ function asRecord(value) {
709
1221
  return typeof value === "object" && value !== null ? value : undefined;
710
1222
  }
711
1223
  function readYamlFile(filePath) {
712
- return YAML.parse(readFileSync(filePath, "utf8"));
1224
+ const parsed = YAML.parse(readFileSync(filePath, "utf8"));
1225
+ return interpolateEnvPlaceholders(parsed, filePath);
713
1226
  }
714
- function getWorkspaceDefaultAgentId(workspaceRoot) {
715
- const runtimePath = path.join(resolveCliConfigRoot(workspaceRoot), "runtime", "workspace.yaml");
716
- if (!existsSync(runtimePath)) {
717
- return undefined;
1227
+ function readCliConfigObjects(workspaceRoot) {
1228
+ const roots = [
1229
+ resolveCliConfigRoot(frameworkCliWorkspaceRoot()),
1230
+ resolveCliConfigRoot(workspaceRoot),
1231
+ ];
1232
+ const merged = new Map();
1233
+ for (const configRoot of roots) {
1234
+ if (!hasCliConfigYaml(configRoot)) {
1235
+ continue;
1236
+ }
1237
+ const files = [];
1238
+ const pending = [configRoot];
1239
+ while (pending.length > 0) {
1240
+ const current = pending.pop();
1241
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
1242
+ const entryPath = path.join(current, entry.name);
1243
+ if (entry.isDirectory()) {
1244
+ if (entry.name === "node_modules") {
1245
+ continue;
1246
+ }
1247
+ pending.push(entryPath);
1248
+ continue;
1249
+ }
1250
+ if (entry.isFile() && /\.ya?ml$/i.test(entry.name)) {
1251
+ files.push(entryPath);
1252
+ }
1253
+ }
1254
+ }
1255
+ for (const filePath of files.sort()) {
1256
+ const content = readFileSync(filePath, "utf8");
1257
+ for (const document of YAML.parseAllDocuments(content)) {
1258
+ const resolved = interpolateEnvPlaceholders(document.toJSON(), filePath);
1259
+ const typed = asRecord(resolved);
1260
+ if (!typed) {
1261
+ continue;
1262
+ }
1263
+ if (typed.kind === "Models" && Array.isArray(typed.spec)) {
1264
+ for (const item of typed.spec) {
1265
+ const record = asRecord(item);
1266
+ if (!record) {
1267
+ continue;
1268
+ }
1269
+ const id = typeof record.name === "string" ? record.name : record.id;
1270
+ if (typeof id !== "string" || !id) {
1271
+ continue;
1272
+ }
1273
+ merged.set(`Model/${id}`, {
1274
+ ...record,
1275
+ kind: "Model",
1276
+ id,
1277
+ });
1278
+ }
1279
+ continue;
1280
+ }
1281
+ const metadata = asRecord(typed.metadata);
1282
+ const spec = asRecord(typed.spec);
1283
+ if (typeof typed.kind === "string" && spec) {
1284
+ const id = typeof metadata?.name === "string" ? metadata.name : typed.id;
1285
+ if (typeof id !== "string" || !id) {
1286
+ continue;
1287
+ }
1288
+ merged.set(`${typed.kind}/${id}`, {
1289
+ ...spec,
1290
+ kind: typed.kind,
1291
+ id,
1292
+ });
1293
+ continue;
1294
+ }
1295
+ const kind = typeof typed.kind === "string" ? typed.kind : undefined;
1296
+ const id = typeof typed.id === "string" ? typed.id : undefined;
1297
+ if (!kind || !id) {
1298
+ continue;
1299
+ }
1300
+ merged.set(`${kind}/${id}`, typed);
1301
+ }
1302
+ }
718
1303
  }
719
- const parsed = asRecord(readYamlFile(runtimePath));
720
- const spec = asRecord(parsed?.spec);
721
- const routing = asRecord(spec?.routing);
722
- return typeof routing?.defaultAgentId === "string" && routing.defaultAgentId.trim().length > 0
723
- ? routing.defaultAgentId.trim()
724
- : undefined;
1304
+ return Array.from(merged.values());
1305
+ }
1306
+ function getWorkspaceDefaultAgentId(workspaceRoot) {
1307
+ const runtime = readCliConfigObjects(workspaceRoot).find((item) => item.kind === "Runtime" && item.id === "default");
1308
+ const routing = asRecord(runtime?.routing);
1309
+ return typeof routing?.defaultAgentId === "string" && routing.defaultAgentId.trim().length > 0 ? routing.defaultAgentId.trim() : undefined;
725
1310
  }
726
1311
  function getAgentModelRef(workspaceRoot, agentId) {
727
- const agentPath = path.join(resolveCliConfigRoot(workspaceRoot), "agents", `${agentId}.yaml`);
728
- if (!existsSync(agentPath)) {
729
- return undefined;
730
- }
731
- const parsed = asRecord(readYamlFile(agentPath));
732
- const spec = asRecord(parsed?.spec);
733
- return typeof spec?.modelRef === "string" && spec.modelRef.trim().length > 0
734
- ? spec.modelRef.trim()
735
- : undefined;
1312
+ const agent = readCliConfigObjects(workspaceRoot).find((item) => item.kind === "Agent" && item.id === agentId);
1313
+ return typeof agent?.modelRef === "string" && agent.modelRef.trim().length > 0 ? agent.modelRef.trim() : undefined;
736
1314
  }
737
1315
  function getModelInfo(workspaceRoot, modelRef) {
738
- const modelsPath = path.join(resolveCliConfigRoot(workspaceRoot), "catalogs", "models.yaml");
739
- if (!existsSync(modelsPath)) {
740
- return undefined;
741
- }
742
- const parsed = asRecord(readYamlFile(modelsPath));
743
- const spec = Array.isArray(parsed?.spec) ? parsed.spec.filter(asRecord) : [];
744
1316
  const modelName = modelRef.startsWith("model/") ? modelRef.slice("model/".length) : modelRef;
745
- const model = spec.find((item) => item.name === modelName);
1317
+ const model = readCliConfigObjects(workspaceRoot).find((item) => item.kind === "Model" && item.id === modelName);
746
1318
  if (!model) {
747
1319
  return undefined;
748
1320
  }
@@ -818,6 +1390,37 @@ export function renderChatRuntimeFailure(output, modelInfo) {
818
1390
  function renderChatTextChunk(text, modelInfo) {
819
1391
  return renderChatRuntimeFailure(text, modelInfo);
820
1392
  }
1393
+ function renderChatRequestRunning(input) {
1394
+ const parts = [
1395
+ input.sessionId ? `session=${input.sessionId}` : undefined,
1396
+ input.requestId ? `request=${input.requestId}` : undefined,
1397
+ input.agentId ? `agent=${input.agentId}` : undefined,
1398
+ "running: waiting for model output",
1399
+ ].filter((part) => typeof part === "string" && part.length > 0);
1400
+ return `\n${parts.join(" ")}\n`;
1401
+ }
1402
+ function summarizeChatToolError(output) {
1403
+ if (typeof output === "string") {
1404
+ const trimmed = output.trim();
1405
+ return trimmed.length > 0 ? truncateChatToolPreview(trimmed, 240) : "failed";
1406
+ }
1407
+ if (typeof output === "number" || typeof output === "boolean") {
1408
+ return String(output);
1409
+ }
1410
+ if (!output || typeof output !== "object") {
1411
+ return "failed";
1412
+ }
1413
+ const typed = output;
1414
+ const content = extractChatToolTextContent(output);
1415
+ if (content && content.trim().length > 0) {
1416
+ return truncateChatToolPreview(content.trim(), 240);
1417
+ }
1418
+ const summary = typeof typed.summary === "object" && typed.summary !== null ? typed.summary : undefined;
1419
+ if (summary) {
1420
+ return truncateChatToolPreview(JSON.stringify(summary, null, 2), 240);
1421
+ }
1422
+ return truncateChatToolPreview(JSON.stringify(output, null, 2), 240);
1423
+ }
821
1424
  function truncateChatToolPreview(value, maxChars = 800) {
822
1425
  if (value.length <= maxChars) {
823
1426
  return value;
@@ -876,26 +1479,11 @@ function extractChatToolTextContent(value) {
876
1479
  }
877
1480
  return "";
878
1481
  }
879
- function summarizeChatToolResult(output) {
880
- if (typeof output === "string") {
881
- return truncateChatToolPreview(output);
1482
+ function summarizeChatToolResult(output, isError) {
1483
+ if (!isError) {
1484
+ return "completed";
882
1485
  }
883
- if (typeof output === "number" || typeof output === "boolean") {
884
- return String(output);
885
- }
886
- if (!output || typeof output !== "object") {
887
- return JSON.stringify(output);
888
- }
889
- const typed = output;
890
- const content = extractChatToolTextContent(output);
891
- if (content && content.trim().length > 0) {
892
- return truncateChatToolPreview(content.trim());
893
- }
894
- const summary = typeof typed.summary === "object" && typed.summary !== null ? typed.summary : undefined;
895
- if (summary) {
896
- return truncateChatToolPreview(JSON.stringify(summary, null, 2));
897
- }
898
- return truncateChatToolPreview(JSON.stringify(output, null, 2));
1486
+ return summarizeChatToolError(output);
899
1487
  }
900
1488
  export async function probeChatWorkspace(input) {
901
1489
  const modelInfo = readChatWorkspaceModelInfo(input.workspaceRoot, input.agentId);
@@ -929,143 +1517,512 @@ export function isChatServerNoiseLine(line) {
929
1517
  trimmed === "langsmith/experimental/sandbox is in alpha. This feature is experimental, and breaking changes are expected." ||
930
1518
  trimmed === "llamaindex was already imported. This breaks constructor checks and will lead to issues!");
931
1519
  }
932
- async function createSubprocessChatClient(input) {
933
- if (input.transport === "http") {
934
- return createAcpHttpHarnessClient({
935
- rpcUrl: `http://${input.hostname ?? "127.0.0.1"}:${input.port ?? 8787}/rpc`,
936
- eventsUrl: `http://${input.hostname ?? "127.0.0.1"}:${input.port ?? 8787}/events`,
937
- });
938
- }
939
- const cliFilePath = fileURLToPath(import.meta.url);
940
- const child = spawn(process.execPath, [
941
- cliFilePath,
942
- "acp",
943
- "serve",
944
- "--workspace",
945
- input.workspaceRoot,
946
- "--transport",
947
- "stdio",
948
- ], {
949
- stdio: ["pipe", "pipe", "pipe"],
950
- });
951
- if (!child.stdin || !child.stdout || !child.stderr) {
952
- throw new Error("Failed to open ACP stdio pipes for chat client.");
1520
+ const CLI_IGNORED_STDERR_WARNING_SNIPPETS = [
1521
+ "llamaindex was already imported. This breaks constructor checks and will lead to issues!",
1522
+ "MaxListenersExceededWarning: Possible EventEmitter memory leak detected.",
1523
+ "(Use `node --trace-warnings ...` to show where the warning was created)",
1524
+ ];
1525
+ function installCliWriteListenerGuard(streams, minimum = 64) {
1526
+ const restore = [];
1527
+ for (const stream of streams) {
1528
+ if (!stream?.getMaxListeners || !stream?.setMaxListeners) {
1529
+ continue;
1530
+ }
1531
+ const previous = stream.getMaxListeners();
1532
+ if (previous === 0 || previous >= minimum) {
1533
+ continue;
1534
+ }
1535
+ stream.setMaxListeners(minimum);
1536
+ restore.push(() => stream.setMaxListeners?.(previous));
953
1537
  }
954
- let stderrBuffer = "";
955
- child.stderr.setEncoding("utf8");
956
- child.stderr.on("data", (chunk) => {
957
- stderrBuffer += chunk;
958
- const lines = stderrBuffer.split(/\r?\n/);
959
- stderrBuffer = lines.pop() ?? "";
960
- for (const line of lines) {
961
- if (!isChatServerNoiseLine(line)) {
962
- input.stderr?.(`[runtime] ${line}\n`);
1538
+ return () => {
1539
+ for (const reset of restore.reverse()) {
1540
+ reset();
1541
+ }
1542
+ };
1543
+ }
1544
+ function installCliEmitterListenerGuard(minimum = 64) {
1545
+ const previous = EventEmitter.defaultMaxListeners;
1546
+ const hadPrototypeMaxListeners = Object.prototype.hasOwnProperty.call(EventEmitter.prototype, "_maxListeners");
1547
+ const previousPrototypeMaxListeners = EventEmitter.prototype._maxListeners;
1548
+ if (previous === 0 || previous >= minimum) {
1549
+ return () => undefined;
1550
+ }
1551
+ EventEmitter.defaultMaxListeners = minimum;
1552
+ EventEmitter.prototype._maxListeners = minimum;
1553
+ return () => {
1554
+ EventEmitter.defaultMaxListeners = previous;
1555
+ if (hadPrototypeMaxListeners) {
1556
+ EventEmitter.prototype._maxListeners = previousPrototypeMaxListeners;
1557
+ return;
1558
+ }
1559
+ delete EventEmitter.prototype._maxListeners;
1560
+ };
1561
+ }
1562
+ function installCliStderrNoiseFilter(enabled) {
1563
+ if (!enabled) {
1564
+ return () => undefined;
1565
+ }
1566
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
1567
+ process.stderr.write = ((chunk, encoding, callback) => {
1568
+ const text = typeof chunk === "string"
1569
+ ? chunk
1570
+ : chunk instanceof Uint8Array
1571
+ ? Buffer.from(chunk).toString(typeof encoding === "string" ? encoding : undefined)
1572
+ : "";
1573
+ if (CLI_IGNORED_STDERR_WARNING_SNIPPETS.some((snippet) => text.includes(snippet))) {
1574
+ if (typeof encoding === "function") {
1575
+ encoding();
1576
+ }
1577
+ else if (typeof callback === "function") {
1578
+ callback();
963
1579
  }
1580
+ return true;
964
1581
  }
1582
+ if (typeof encoding === "function") {
1583
+ return originalStderrWrite(chunk, encoding);
1584
+ }
1585
+ return originalStderrWrite(chunk, encoding, callback);
965
1586
  });
966
- const baseClient = createAcpStdioHarnessClient({
967
- input: child.stdin,
968
- output: child.stdout,
969
- idPrefix: "chat",
970
- });
971
- return wrapChatClientForLifecycle(baseClient, async () => {
972
- if (!child.killed) {
973
- child.kill();
1587
+ return () => {
1588
+ process.stderr.write = originalStderrWrite;
1589
+ };
1590
+ }
1591
+ function installCliWarningFilter() {
1592
+ const originalEmitWarning = process.emitWarning.bind(process);
1593
+ process.emitWarning = ((warning, ...args) => {
1594
+ const text = typeof warning === "string" ? warning : warning?.message ?? "";
1595
+ const name = typeof warning === "string" ? String(args[1] ?? args[0] ?? "") : warning?.name ?? "";
1596
+ if (CLI_IGNORED_STDERR_WARNING_SNIPPETS.some((snippet) => text.includes(snippet))
1597
+ || name === "MaxListenersExceededWarning") {
1598
+ return;
974
1599
  }
1600
+ return originalEmitWarning(warning, ...args);
975
1601
  });
1602
+ return () => {
1603
+ process.emitWarning = originalEmitWarning;
1604
+ };
976
1605
  }
977
- export function wrapChatClientForLifecycle(client, onStop) {
978
- return {
979
- request: client.request.bind(client),
980
- streamRequest: client.streamRequest.bind(client),
981
- resolveApproval: client.resolveApproval.bind(client),
982
- cancelRequest: client.cancelRequest.bind(client),
983
- subscribe: client.subscribe.bind(client),
984
- listSessions: client.listSessions.bind(client),
985
- listSessionSummaries: client.listSessionSummaries.bind(client),
986
- listRequests: client.listRequests.bind(client),
987
- getSession: client.getSession.bind(client),
988
- getRequest: client.getRequest.bind(client),
989
- listApprovals: client.listApprovals.bind(client),
990
- getApproval: client.getApproval.bind(client),
991
- listRequestEvents: client.listRequestEvents.bind(client),
992
- listRequestTraceItems: client.listRequestTraceItems.bind(client),
993
- getHealth: client.getHealth.bind(client),
994
- getOperatorOverview: client.getOperatorOverview.bind(client),
995
- async stop() {
996
- try {
997
- await client.stop();
998
- }
999
- finally {
1000
- await onStop();
1001
- }
1002
- },
1606
+ function terminateChatSubprocess(child, signal = "SIGTERM") {
1607
+ if (child.killed || child.exitCode !== null || child.signalCode !== null) {
1608
+ return;
1609
+ }
1610
+ try {
1611
+ child.kill(signal);
1612
+ }
1613
+ catch {
1614
+ // Ignore termination races during shutdown.
1615
+ }
1616
+ }
1617
+ export function installChatSubprocessExitGuard(child, host = process) {
1618
+ let cleanedUp = false;
1619
+ let forceKillTimer;
1620
+ const handleParentExit = () => {
1621
+ terminateChatSubprocess(child, "SIGTERM");
1622
+ if (!forceKillTimer && child.exitCode === null && child.signalCode === null) {
1623
+ forceKillTimer = setTimeout(() => {
1624
+ terminateChatSubprocess(child, "SIGKILL");
1625
+ }, 2_000);
1626
+ forceKillTimer.unref?.();
1627
+ }
1628
+ };
1629
+ const cleanup = () => {
1630
+ if (cleanedUp) {
1631
+ return;
1632
+ }
1633
+ cleanedUp = true;
1634
+ if (forceKillTimer) {
1635
+ clearTimeout(forceKillTimer);
1636
+ forceKillTimer = undefined;
1637
+ }
1638
+ host.off("beforeExit", handleParentExit);
1639
+ host.off("exit", handleParentExit);
1640
+ child.off("close", cleanup);
1641
+ child.off("exit", cleanup);
1003
1642
  };
1643
+ host.on("beforeExit", handleParentExit);
1644
+ host.on("exit", handleParentExit);
1645
+ child.on("close", cleanup);
1646
+ child.on("exit", cleanup);
1647
+ return cleanup;
1004
1648
  }
1005
1649
  async function streamChatMessage(input) {
1650
+ const requestStartedAt = Date.now();
1651
+ let firstSnapshotAt;
1652
+ let firstDataAt;
1653
+ let latestSnapshot;
1006
1654
  let latestSessionId = input.sessionId;
1007
1655
  let latestRequestId;
1008
1656
  let latestAgentId = input.agentId;
1009
1657
  let wroteContent = false;
1010
1658
  let wroteRenderableBlocks = false;
1011
- for await (const item of input.client.streamRequest({
1012
- ...(input.agentId ? { agentId: input.agentId } : {}),
1013
- ...(input.sessionId ? { sessionId: input.sessionId } : {}),
1014
- input: input.message,
1015
- })) {
1016
- if (item.type === "content") {
1017
- input.stdout(renderChatTextChunk(item.content, input.modelInfo));
1018
- latestSessionId = item.sessionId;
1019
- latestRequestId = item.requestId;
1020
- latestAgentId = item.agentId;
1021
- wroteContent = true;
1022
- continue;
1659
+ let lastRenderedRequestTreeKey;
1660
+ let lastRenderedRequestTree = "";
1661
+ let lastRenderedRequestTreeLineCount = 0;
1662
+ let lastRenderedRequestTreeAt = 0;
1663
+ let announcedRunningState = false;
1664
+ let requestTreeVisible = false;
1665
+ let requestTreePersisted = false;
1666
+ let liveRequestAnnotations = [];
1667
+ let persistedRequestTreeEventCount = 0;
1668
+ let persistedRequestTreeTodoSignature = "";
1669
+ const requestTreeRenderThrottleMs = 75;
1670
+ let suppressRequestTreeRendering = false;
1671
+ let lastStableRequestTreeKey;
1672
+ let stdoutWriteChain = Promise.resolve();
1673
+ let stderrWriteChain = Promise.resolve();
1674
+ const enqueueChatWrite = (sink, _stream, chain, message) => chain.then(async () => {
1675
+ sink(message);
1676
+ });
1677
+ const summarizeRequestTreeSteps = (steps) => steps
1678
+ .map((step) => [
1679
+ step.id ?? "",
1680
+ step.kind,
1681
+ step.name,
1682
+ step.status,
1683
+ step.startedAt ?? "",
1684
+ step.endedAt ?? "",
1685
+ ].join("|"))
1686
+ .join(";");
1687
+ const buildRequestSnapshotRenderKey = (snapshot) => {
1688
+ const todoSignatures = snapshot.plan.items
1689
+ .map((item) => [
1690
+ item.id ?? "",
1691
+ item.content,
1692
+ item.status,
1693
+ item.ownerAgentId ?? "",
1694
+ item.startedAt ?? "",
1695
+ item.endedAt ?? "",
1696
+ item.result === undefined ? "" : String(item.result),
1697
+ summarizeRequestTreeSteps(item.events),
1698
+ ].join("|"))
1699
+ .join(";");
1700
+ const approvalSignature = snapshot.approval
1701
+ ? [
1702
+ snapshot.approval.approvalId,
1703
+ snapshot.approval.status,
1704
+ snapshot.approval.toolName ?? "",
1705
+ ].join("|")
1706
+ : "";
1707
+ return [
1708
+ snapshot.sessionId,
1709
+ snapshot.requestId,
1710
+ snapshot.state,
1711
+ snapshot.agentId ?? "",
1712
+ snapshot.plan.version,
1713
+ snapshot.plan.updatedAt,
1714
+ snapshot.plan.summary.total,
1715
+ snapshot.plan.summary.inProgress,
1716
+ snapshot.plan.summary.completed,
1717
+ snapshot.plan.summary.failed,
1718
+ approvalSignature,
1719
+ summarizeRequestTreeSteps(snapshot.events),
1720
+ todoSignatures,
1721
+ ].join("||");
1722
+ };
1723
+ const buildPlanSnapshotRenderKey = (snapshot) => [
1724
+ snapshot.plan.version,
1725
+ snapshot.plan.updatedAt,
1726
+ snapshot.plan.summary.total,
1727
+ snapshot.plan.summary.inProgress,
1728
+ snapshot.plan.summary.completed,
1729
+ snapshot.plan.summary.failed,
1730
+ snapshot.plan.items
1731
+ .map((item) => [
1732
+ item.id ?? "",
1733
+ item.content,
1734
+ item.status,
1735
+ item.ownerAgentId ?? "",
1736
+ item.startedAt ?? "",
1737
+ item.endedAt ?? "",
1738
+ item.result === undefined ? "" : String(item.result),
1739
+ ].join("|"))
1740
+ .join(";"),
1741
+ ].join("||");
1742
+ const formatPerfClock = (timestamp) => {
1743
+ const value = new Date(timestamp);
1744
+ return value.toLocaleTimeString("en-US", {
1745
+ hour12: false,
1746
+ hour: "2-digit",
1747
+ minute: "2-digit",
1748
+ second: "2-digit",
1749
+ fractionalSecondDigits: 3,
1750
+ });
1751
+ };
1752
+ const formatElapsed = (timestamp) => `${((timestamp - requestStartedAt) / 1000).toFixed(3)}s`;
1753
+ const buildTimingSummary = (completedAt) => {
1754
+ const parts = [`start ${formatPerfClock(requestStartedAt)}`];
1755
+ if (firstSnapshotAt) {
1756
+ parts.push(`first event +${formatElapsed(firstSnapshotAt)}`);
1023
1757
  }
1024
- if (item.type === "content-blocks") {
1025
- latestSessionId = item.sessionId;
1026
- latestRequestId = item.requestId;
1027
- latestAgentId = item.agentId;
1028
- if (!wroteContent) {
1029
- const rendered = item.contentBlocks
1030
- .map((block) => {
1031
- if (typeof block === "string") {
1032
- return block;
1033
- }
1034
- if (block && typeof block === "object" && "text" in block && typeof block.text === "string") {
1035
- return block.text;
1036
- }
1037
- return "";
1038
- })
1039
- .filter((block) => block.trim().length > 0)
1040
- .join("");
1041
- if (rendered) {
1042
- input.stdout(renderChatTextChunk(rendered, input.modelInfo));
1043
- wroteRenderableBlocks = true;
1758
+ if (firstDataAt) {
1759
+ parts.push(`first data +${formatElapsed(firstDataAt)}`);
1760
+ }
1761
+ if (completedAt) {
1762
+ parts.push(`done +${formatElapsed(completedAt)}`);
1763
+ }
1764
+ else {
1765
+ parts.push(`live +${formatElapsed(Date.now())}`);
1766
+ }
1767
+ return parts.join(" · ");
1768
+ };
1769
+ const buildLiveRequestTreeBlock = () => {
1770
+ if (!lastRenderedRequestTree) {
1771
+ return "";
1772
+ }
1773
+ const annotationBlock = liveRequestAnnotations.length > 0
1774
+ ? `\n${liveRequestAnnotations.join("")}`
1775
+ : "";
1776
+ return `${lastRenderedRequestTree}${annotationBlock}`;
1777
+ };
1778
+ const clearLiveRequestTree = () => {
1779
+ if (!input.requestEvents || !input.liveRequestTree || !requestTreeVisible || lastRenderedRequestTreeLineCount <= 0) {
1780
+ return;
1781
+ }
1782
+ stderrWriteChain = enqueueChatWrite(input.stderr, input.stderrStream, stderrWriteChain, `\x1b[${lastRenderedRequestTreeLineCount}F\x1b[0J`);
1783
+ requestTreeVisible = false;
1784
+ };
1785
+ const drawLiveRequestTree = () => {
1786
+ if (!input.requestEvents || !input.liveRequestTree || !lastRenderedRequestTree) {
1787
+ return;
1788
+ }
1789
+ const block = buildLiveRequestTreeBlock();
1790
+ stderrWriteChain = enqueueChatWrite(input.stderr, input.stderrStream, stderrWriteChain, block);
1791
+ lastRenderedRequestTreeLineCount = countRenderedLines(block);
1792
+ requestTreeVisible = true;
1793
+ };
1794
+ const persistLiveRequestTree = () => {
1795
+ if (!input.requestEvents || !input.liveRequestTree || requestTreePersisted || !lastRenderedRequestTree) {
1796
+ return;
1797
+ }
1798
+ if (requestTreeVisible && lastRenderedRequestTreeLineCount > 0) {
1799
+ stderrWriteChain = enqueueChatWrite(input.stderr, input.stderrStream, stderrWriteChain, `\x1b[${lastRenderedRequestTreeLineCount}F\x1b[0J`);
1800
+ requestTreeVisible = false;
1801
+ }
1802
+ const combinedBlock = buildLiveRequestTreeBlock();
1803
+ const treeBlock = combinedBlock.endsWith("\n")
1804
+ ? combinedBlock
1805
+ : `${combinedBlock}\n`;
1806
+ stderrWriteChain = enqueueChatWrite(input.stderr, input.stderrStream, stderrWriteChain, treeBlock);
1807
+ persistedRequestTreeEventCount = latestSnapshot ? flattenRequestExecutionSteps(latestSnapshot).length : 0;
1808
+ persistedRequestTreeTodoSignature = latestSnapshot ? buildTodoContinuationSignature(latestSnapshot) : "";
1809
+ requestTreePersisted = true;
1810
+ };
1811
+ const enqueueOrderedStdout = (message) => {
1812
+ const barrier = Promise.allSettled([stdoutWriteChain, stderrWriteChain]).then(() => undefined);
1813
+ stdoutWriteChain = enqueueChatWrite(input.stdout, input.stdoutStream, barrier, message);
1814
+ };
1815
+ const writeChatStdout = (message) => {
1816
+ if (input.requestEvents && input.liveRequestTree && !suppressRequestTreeRendering) {
1817
+ clearLiveRequestTree();
1818
+ enqueueOrderedStdout(message);
1819
+ drawLiveRequestTree();
1820
+ return;
1821
+ }
1822
+ if (input.requestEvents && input.liveRequestTree) {
1823
+ enqueueOrderedStdout(message);
1824
+ return;
1825
+ }
1826
+ stdoutWriteChain = enqueueChatWrite(input.stdout, input.stdoutStream, stdoutWriteChain, message);
1827
+ };
1828
+ const writeChatStderr = (message) => {
1829
+ if (input.requestEvents && input.liveRequestTree && !suppressRequestTreeRendering) {
1830
+ clearLiveRequestTree();
1831
+ stderrWriteChain = enqueueChatWrite(input.stderr, input.stderrStream, stderrWriteChain, message);
1832
+ drawLiveRequestTree();
1833
+ return;
1834
+ }
1835
+ stderrWriteChain = enqueueChatWrite(input.stderr, input.stderrStream, stderrWriteChain, message);
1836
+ };
1837
+ const renderPlanSnapshot = (snapshot) => {
1838
+ return;
1839
+ };
1840
+ const suspendRequestTreeRendering = () => {
1841
+ if (!input.requestEvents || !input.liveRequestTree) {
1842
+ return;
1843
+ }
1844
+ persistLiveRequestTree();
1845
+ suppressRequestTreeRendering = true;
1846
+ clearLiveRequestTree();
1847
+ };
1848
+ const renderExecutionStepEvents = (snapshot) => {
1849
+ return;
1850
+ };
1851
+ const renderContentBlocks = (contentBlocks, agentId) => {
1852
+ latestAgentId = agentId || latestAgentId;
1853
+ if (!wroteContent) {
1854
+ const rendered = contentBlocks
1855
+ .map((block) => {
1856
+ if (typeof block === "string") {
1857
+ return block;
1044
1858
  }
1859
+ if (block && typeof block === "object" && "text" in block && typeof block.text === "string") {
1860
+ return block.text;
1861
+ }
1862
+ return "";
1863
+ })
1864
+ .filter((block) => block.trim().length > 0)
1865
+ .join("");
1866
+ if (rendered) {
1867
+ if (input.requestEvents) {
1868
+ suspendRequestTreeRendering();
1869
+ }
1870
+ writeChatStdout(renderChatTextChunk(rendered, input.modelInfo));
1871
+ wroteRenderableBlocks = true;
1045
1872
  }
1046
- continue;
1047
1873
  }
1048
- if (item.type === "tool-result") {
1049
- latestSessionId = item.sessionId;
1050
- latestRequestId = item.requestId;
1051
- latestAgentId = item.agentId;
1052
- input.stderr(`\n[tool:${item.toolName}] ${summarizeChatToolResult(item.output)}${item.isError ? " (error)" : ""}\n`);
1053
- continue;
1874
+ };
1875
+ let result;
1876
+ try {
1877
+ result = await input.client.request({
1878
+ ...(input.agentId ? { agentId: input.agentId } : {}),
1879
+ ...(input.sessionId ? { sessionId: input.sessionId } : {}),
1880
+ input: input.message,
1881
+ eventListener(snapshot) {
1882
+ latestSessionId = snapshot.sessionId || latestSessionId;
1883
+ latestRequestId = snapshot.requestId || latestRequestId;
1884
+ latestAgentId = snapshot.agentId || latestAgentId;
1885
+ latestSnapshot = snapshot;
1886
+ firstSnapshotAt ??= Date.now();
1887
+ if (input.requestEvents && !suppressRequestTreeRendering) {
1888
+ const now = Date.now();
1889
+ const nextTreeKey = buildRequestSnapshotRenderKey(snapshot);
1890
+ const terminalSnapshot = snapshot.state !== "queued" && snapshot.state !== "claimed" && snapshot.state !== "running";
1891
+ const snapshotCarriesRenderableOutput = snapshot.output.trim().length > 0;
1892
+ const shouldRenderSnapshot = !snapshotCarriesRenderableOutput
1893
+ && nextTreeKey !== lastRenderedRequestTreeKey
1894
+ && nextTreeKey !== lastStableRequestTreeKey
1895
+ && ((terminalSnapshot && !input.liveRequestTree)
1896
+ || !input.liveRequestTree
1897
+ || now - lastRenderedRequestTreeAt >= requestTreeRenderThrottleMs);
1898
+ if (shouldRenderSnapshot) {
1899
+ lastRenderedRequestTree = renderRequestSnapshotTree(snapshot, input.colorRequestTree === true, buildTimingSummary());
1900
+ if (input.liveRequestTree) {
1901
+ clearLiveRequestTree();
1902
+ drawLiveRequestTree();
1903
+ }
1904
+ else {
1905
+ input.stderr(`\n${lastRenderedRequestTree}`);
1906
+ }
1907
+ lastRenderedRequestTreeKey = nextTreeKey;
1908
+ if (!snapshot.activeEventId && snapshot.state !== "running") {
1909
+ lastStableRequestTreeKey = nextTreeKey;
1910
+ }
1911
+ lastRenderedRequestTreeAt = now;
1912
+ }
1913
+ }
1914
+ if ((input.showRunningState ?? true) && !input.requestEvents && !announcedRunningState && !wroteContent && !wroteRenderableBlocks) {
1915
+ input.stderr(renderChatRequestRunning({
1916
+ sessionId: snapshot.sessionId,
1917
+ requestId: snapshot.requestId,
1918
+ agentId: snapshot.agentId,
1919
+ }));
1920
+ announcedRunningState = true;
1921
+ }
1922
+ renderExecutionStepEvents(snapshot);
1923
+ renderPlanSnapshot({
1924
+ sessionId: snapshot.sessionId,
1925
+ requestId: snapshot.requestId,
1926
+ plan: {
1927
+ version: snapshot.plan.version,
1928
+ updatedAt: snapshot.plan.updatedAt,
1929
+ items: snapshot.plan.items.map((item) => ({
1930
+ id: item.id,
1931
+ content: item.content,
1932
+ status: item.status,
1933
+ ownerAgentId: item.ownerAgentId,
1934
+ startedAt: item.startedAt,
1935
+ endedAt: item.endedAt,
1936
+ result: item.result,
1937
+ metadata: item.metadata,
1938
+ })),
1939
+ summary: snapshot.plan.summary,
1940
+ },
1941
+ });
1942
+ },
1943
+ dataListener(delta) {
1944
+ latestSessionId = delta.sessionId || latestSessionId;
1945
+ latestRequestId = delta.requestId || latestRequestId;
1946
+ firstDataAt ??= Date.now();
1947
+ if (delta.type === "output.text.delta") {
1948
+ latestAgentId = delta.agentId || latestAgentId;
1949
+ if (input.requestEvents) {
1950
+ suspendRequestTreeRendering();
1951
+ }
1952
+ writeChatStdout(renderChatTextChunk(delta.text, input.modelInfo));
1953
+ wroteContent = true;
1954
+ return;
1955
+ }
1956
+ if (delta.type === "output.content-blocks") {
1957
+ suspendRequestTreeRendering();
1958
+ renderContentBlocks(delta.contentBlocks, delta.agentId);
1959
+ return;
1960
+ }
1961
+ if (delta.type === "tool.result") {
1962
+ latestAgentId = delta.agentId || latestAgentId;
1963
+ if ((input.showToolResults ?? true) && !input.requestEvents) {
1964
+ writeChatStderr(`\n[${formatPerfClock(Date.now())} +${formatElapsed(Date.now())}] [tool:${delta.toolName}] ${summarizeChatToolResult(delta.output, delta.isError === true)}${delta.isError ? " (error)" : ""}\n`);
1965
+ }
1966
+ return;
1967
+ }
1968
+ if (delta.type === "progress.commentary") {
1969
+ latestAgentId = delta.agentId || latestAgentId;
1970
+ if (wroteContent || wroteRenderableBlocks) {
1971
+ return;
1972
+ }
1973
+ const progressLine = `[${formatPerfClock(Date.now())} +${formatElapsed(Date.now())}] ${delta.text}\n`;
1974
+ if (input.requestEvents && input.liveRequestTree && !suppressRequestTreeRendering && lastRenderedRequestTree) {
1975
+ liveRequestAnnotations.push(progressLine);
1976
+ clearLiveRequestTree();
1977
+ drawLiveRequestTree();
1978
+ return;
1979
+ }
1980
+ writeChatStderr(`[${formatPerfClock(Date.now())} +${formatElapsed(Date.now())}] ${delta.text}\n`);
1981
+ }
1982
+ },
1983
+ });
1984
+ }
1985
+ catch (error) {
1986
+ throw error;
1987
+ }
1988
+ if (!result) {
1989
+ throw new Error("chat request completed without a terminal result");
1990
+ }
1991
+ latestSessionId = result.sessionId;
1992
+ latestRequestId = result.requestId;
1993
+ latestAgentId = result.agentId ?? latestAgentId;
1994
+ const completedAt = Date.now();
1995
+ if (input.requestEvents && latestSnapshot) {
1996
+ const terminalSnapshot = buildTerminalRequestSnapshot(latestSnapshot, { state: result.state, output: result.output });
1997
+ lastRenderedRequestTree = renderRequestSnapshotTree(terminalSnapshot, input.colorRequestTree === true, buildTimingSummary(completedAt));
1998
+ if (input.liveRequestTree && !requestTreePersisted) {
1999
+ clearLiveRequestTree();
2000
+ drawLiveRequestTree();
1054
2001
  }
1055
- if (item.type === "result") {
1056
- latestSessionId = item.result.sessionId;
1057
- latestRequestId = item.result.requestId;
1058
- if (!wroteContent && !wroteRenderableBlocks && item.result.output.trim().length > 0) {
1059
- input.stdout(renderChatRuntimeFailure(item.result.output, input.modelInfo));
2002
+ else if (input.liveRequestTree && requestTreePersisted) {
2003
+ const todoContinuation = renderRequestTodoContinuation(terminalSnapshot, persistedRequestTreeTodoSignature, input.colorRequestTree === true);
2004
+ const continuation = renderRequestEventContinuation(terminalSnapshot, persistedRequestTreeEventCount, input.colorRequestTree === true);
2005
+ if (todoContinuation) {
2006
+ stderrWriteChain = enqueueChatWrite(input.stderr, input.stderrStream, stderrWriteChain, `\n${todoContinuation}`);
1060
2007
  }
1061
- if (item.result.state === "waiting_for_approval") {
1062
- input.stderr(`\nRequest is waiting for approval${item.result.approvalId ? ` (${item.result.approvalId})` : ""}.\n`);
1063
- }
1064
- else if (wroteContent || wroteRenderableBlocks || item.result.output.trim().length > 0) {
1065
- input.stdout("\n");
2008
+ if (continuation) {
2009
+ stderrWriteChain = enqueueChatWrite(input.stderr, input.stderrStream, stderrWriteChain, `\n${continuation}`);
1066
2010
  }
1067
2011
  }
2012
+ else if (!input.liveRequestTree) {
2013
+ input.stderr(`\n${lastRenderedRequestTree}`);
2014
+ }
2015
+ }
2016
+ if (!wroteContent && !wroteRenderableBlocks && result.output.trim().length > 0) {
2017
+ writeChatStdout(renderChatTextChunk(result.output, input.modelInfo));
2018
+ }
2019
+ if (result.state === "waiting_for_approval") {
2020
+ writeChatStderr(`\nRequest is waiting for approval${result.approvalId ? ` (${result.approvalId})` : ""}.\n`);
1068
2021
  }
2022
+ else if ((wroteContent || wroteRenderableBlocks || result.output.trim().length > 0)) {
2023
+ writeChatStdout("\n");
2024
+ }
2025
+ await Promise.allSettled([stdoutWriteChain, stderrWriteChain]);
1069
2026
  return { sessionId: latestSessionId, requestId: latestRequestId, agentId: latestAgentId };
1070
2027
  }
1071
2028
  export async function runCli(argv, io = {}, deps = {}) {
@@ -1073,8 +2030,9 @@ export async function runCli(argv, io = {}, deps = {}) {
1073
2030
  const stdin = io.stdin ?? process.stdin;
1074
2031
  const stdout = io.stdout ?? ((message) => process.stdout.write(message));
1075
2032
  const stderr = io.stderr ?? ((message) => process.stderr.write(message));
2033
+ const stdoutStream = io.stdout === undefined ? process.stdout : undefined;
2034
+ const stderrStream = io.stderr === undefined ? process.stderr : undefined;
1076
2035
  const [command, projectName, ...rest] = argv;
1077
- const createChatClient = deps.createChatClient ?? createSubprocessChatClient;
1078
2036
  const probeWorkspace = deps.probeChatWorkspace ?? probeChatWorkspace;
1079
2037
  const createChatLineReader = deps.createReadlineInterface ?? createReadlineInterface;
1080
2038
  const createHarness = deps.createAgentHarness ?? createAgentHarness;
@@ -1083,47 +2041,34 @@ export async function runCli(argv, io = {}, deps = {}) {
1083
2041
  const serveAcpHttp = deps.serveAcpOverHttp ?? serveAcpOverHttp;
1084
2042
  const serveAcp = deps.serveAcpOverStdio ?? serveAcpOverStdio;
1085
2043
  const serveRuntimeMcp = deps.serveRuntimeMcpOverStdio ?? serveRuntimeMcpOverStdio;
1086
- if (command === "init") {
1087
- if (!projectName?.trim()) {
1088
- stderr(renderUsage());
1089
- return 1;
1090
- }
1091
- const parsed = parseInitOptions(rest);
1092
- if (parsed.error) {
1093
- stderr(`${parsed.error}\n`);
1094
- stderr(renderUsage());
1095
- return 1;
1096
- }
1097
- try {
1098
- const projectRoot = path.resolve(cwd, projectName);
1099
- const result = await initProject(projectRoot, projectName, parsed.options);
1100
- stdout(`Created ${result.projectSlug} at ${result.projectRoot}\n`);
1101
- stdout("Next steps:\n");
1102
- stdout(` cd ${projectName}\n`);
1103
- stdout(" npm install\n");
1104
- if ((parsed.options?.provider ?? "openai") === "openai") {
1105
- stdout(" export OPENAI_API_KEY=your_key_here\n");
1106
- }
1107
- stdout(' npm run start -- "Research a topic"\n');
1108
- return 0;
1109
- }
1110
- catch (error) {
1111
- const message = error instanceof Error ? error.message : String(error);
1112
- stderr(`${message}\n`);
1113
- return 1;
2044
+ const createChatClient = deps.createChatClient ?? (async (input) => {
2045
+ if (input.transport === "http") {
2046
+ return createAcpHttpHarnessClient({
2047
+ rpcUrl: `http://${input.hostname ?? "127.0.0.1"}:${input.port ?? 8787}/rpc`,
2048
+ eventsUrl: `http://${input.hostname ?? "127.0.0.1"}:${input.port ?? 8787}/events`,
2049
+ });
1114
2050
  }
1115
- }
1116
- if (command === "chat") {
1117
- const parsed = parseChatOptions([projectName, ...rest].filter((item) => typeof item === "string"));
1118
- if (parsed.error) {
1119
- stderr(`${parsed.error}\n`);
1120
- stderr(renderUsage());
2051
+ const runtime = await createHarness(input.workspaceRoot);
2052
+ return createInProcessHarnessClient(runtime);
2053
+ });
2054
+ const executeChat = async (parsed) => {
2055
+ const workspacePath = resolveCliWorkspaceRoot(cwd, parsed.workspaceRoot);
2056
+ const workspaceError = validateCliWorkspaceRoot(workspacePath, parsed.workspaceRoot);
2057
+ if (workspaceError) {
2058
+ stderr(`${workspaceError}\n`);
1121
2059
  return 1;
1122
2060
  }
1123
- const workspacePath = resolveCliWorkspaceRoot(cwd, parsed.workspaceRoot);
1124
2061
  const workspaceModelInfo = readChatWorkspaceModelInfo(workspacePath, parsed.agentId);
2062
+ const chatStdinStream = stdin;
1125
2063
  let client;
2064
+ let restoreWriteListenerGuard;
2065
+ let restoreEmitterListenerGuard;
2066
+ let restoreStderrNoiseFilter;
2067
+ let restoreWarningFilter;
1126
2068
  try {
2069
+ restoreEmitterListenerGuard = installCliEmitterListenerGuard();
2070
+ restoreStderrNoiseFilter = installCliStderrNoiseFilter(io.stderr === undefined);
2071
+ restoreWarningFilter = installCliWarningFilter();
1127
2072
  client = await createChatClient({
1128
2073
  workspaceRoot: workspacePath,
1129
2074
  transport: parsed.transport,
@@ -1131,34 +2076,41 @@ export async function runCli(argv, io = {}, deps = {}) {
1131
2076
  port: parsed.port,
1132
2077
  stderr,
1133
2078
  });
2079
+ restoreWriteListenerGuard = installCliWriteListenerGuard([stdoutStream, stderrStream]);
1134
2080
  let activeAgentId = parsed.agentId;
1135
2081
  let activeSessionId = parsed.sessionId;
1136
2082
  let latestRequestId;
1137
- const preflightWarning = await probeWorkspace({
1138
- workspaceRoot: workspacePath,
1139
- agentId: activeAgentId,
1140
- });
2083
+ const useColor = false;
2084
+ const preflightWarning = parsed.message
2085
+ ? undefined
2086
+ : await probeWorkspace({
2087
+ workspaceRoot: workspacePath,
2088
+ agentId: activeAgentId,
2089
+ });
1141
2090
  if (parsed.message) {
1142
- const streamed = await streamChatMessage({
2091
+ await streamChatMessage({
1143
2092
  client,
1144
2093
  stdout,
2094
+ stdoutStream,
1145
2095
  stderr,
2096
+ stderrStream,
1146
2097
  agentId: activeAgentId,
1147
2098
  sessionId: activeSessionId,
1148
2099
  message: parsed.message,
1149
2100
  modelInfo: workspaceModelInfo,
2101
+ requestEvents: parsed.requestEvents,
2102
+ liveRequestTree: parsed.requestEvents && chatStdinStream.isTTY === true,
2103
+ colorRequestTree: useColor,
2104
+ showToolResults: parsed.requestEvents,
2105
+ showRunningState: false,
1150
2106
  });
1151
- activeSessionId = streamed.sessionId;
1152
- latestRequestId = streamed.requestId;
1153
- activeAgentId = streamed.agentId ?? activeAgentId;
1154
- if (activeSessionId) {
1155
- stderr(`session=${activeSessionId}${latestRequestId ? ` request=${latestRequestId}` : ""}\n`);
1156
- }
1157
2107
  await client.stop();
2108
+ restoreWriteListenerGuard?.();
2109
+ restoreEmitterListenerGuard?.();
2110
+ restoreStderrNoiseFilter?.();
2111
+ restoreWarningFilter?.();
1158
2112
  return 0;
1159
2113
  }
1160
- const stdinStream = stdin;
1161
- const useColor = stdinStream.isTTY === true && io.stdout === undefined;
1162
2114
  stdout(renderChatBanner({
1163
2115
  workspacePath,
1164
2116
  transport: parsed.transport,
@@ -1180,7 +2132,7 @@ export async function runCli(argv, io = {}, deps = {}) {
1180
2132
  input: stdin,
1181
2133
  output: stdoutSink,
1182
2134
  crlfDelay: Infinity,
1183
- terminal: stdinStream.isTTY === true,
2135
+ terminal: chatStdinStream.isTTY === true,
1184
2136
  });
1185
2137
  try {
1186
2138
  for await (const raw of iterateChatLines(lineReader, () => renderChatPromptLine({
@@ -1198,11 +2150,18 @@ export async function runCli(argv, io = {}, deps = {}) {
1198
2150
  const streamed = await streamChatMessage({
1199
2151
  client,
1200
2152
  stdout,
2153
+ stdoutStream,
1201
2154
  stderr,
2155
+ stderrStream,
1202
2156
  agentId: activeAgentId,
1203
2157
  sessionId: activeSessionId,
1204
2158
  message: trimmed,
1205
2159
  modelInfo: workspaceModelInfo,
2160
+ requestEvents: parsed.requestEvents,
2161
+ liveRequestTree: parsed.requestEvents && chatStdinStream.isTTY === true,
2162
+ colorRequestTree: useColor,
2163
+ showToolResults: parsed.requestEvents,
2164
+ showRunningState: false,
1206
2165
  });
1207
2166
  activeSessionId = streamed.sessionId;
1208
2167
  latestRequestId = streamed.requestId;
@@ -1364,9 +2323,17 @@ export async function runCli(argv, io = {}, deps = {}) {
1364
2323
  lineReader.close();
1365
2324
  }
1366
2325
  await client.stop();
2326
+ restoreWriteListenerGuard?.();
2327
+ restoreEmitterListenerGuard?.();
2328
+ restoreStderrNoiseFilter?.();
2329
+ restoreWarningFilter?.();
1367
2330
  return 0;
1368
2331
  }
1369
2332
  catch (error) {
2333
+ restoreWriteListenerGuard?.();
2334
+ restoreEmitterListenerGuard?.();
2335
+ restoreStderrNoiseFilter?.();
2336
+ restoreWarningFilter?.();
1370
2337
  const message = error instanceof Error ? error.message : String(error);
1371
2338
  stderr(`${message}\n`);
1372
2339
  if (client) {
@@ -1374,6 +2341,54 @@ export async function runCli(argv, io = {}, deps = {}) {
1374
2341
  }
1375
2342
  return 1;
1376
2343
  }
2344
+ };
2345
+ if (command === "init") {
2346
+ if (!projectName?.trim()) {
2347
+ stderr(renderUsage());
2348
+ return 1;
2349
+ }
2350
+ const parsed = parseInitOptions(rest);
2351
+ if (parsed.error) {
2352
+ stderr(`${parsed.error}\n`);
2353
+ stderr(renderUsage());
2354
+ return 1;
2355
+ }
2356
+ try {
2357
+ const projectRoot = path.resolve(cwd, projectName);
2358
+ const result = await initProject(projectRoot, projectName, parsed.options);
2359
+ stdout(`Created ${result.projectSlug} at ${result.projectRoot}\n`);
2360
+ stdout("Next steps:\n");
2361
+ stdout(` cd ${projectName}\n`);
2362
+ stdout(" npm install\n");
2363
+ if ((parsed.options?.provider ?? "openai") === "openai") {
2364
+ stdout(" export OPENAI_API_KEY=your_key_here\n");
2365
+ }
2366
+ stdout(' npm run start -- "Research a topic"\n');
2367
+ return 0;
2368
+ }
2369
+ catch (error) {
2370
+ const message = error instanceof Error ? error.message : String(error);
2371
+ stderr(`${message}\n`);
2372
+ return 1;
2373
+ }
2374
+ }
2375
+ if (command === "chat") {
2376
+ const parsed = parseChatOptions([projectName, ...rest].filter((item) => typeof item === "string"));
2377
+ if (parsed.error) {
2378
+ stderr(`${parsed.error}\n`);
2379
+ stderr(renderUsage());
2380
+ return 1;
2381
+ }
2382
+ return executeChat(parsed);
2383
+ }
2384
+ if (!isTopLevelCliCommand(command)) {
2385
+ const parsed = parseChatOptions(argv);
2386
+ if (parsed.error) {
2387
+ stderr(`${parsed.error}\n`);
2388
+ stderr(renderUsage());
2389
+ return 1;
2390
+ }
2391
+ return executeChat(parsed);
1377
2392
  }
1378
2393
  if (command === "acp") {
1379
2394
  const [subcommand, ...subcommandArgs] = [projectName, ...rest];
@@ -1389,6 +2404,11 @@ export async function runCli(argv, io = {}, deps = {}) {
1389
2404
  }
1390
2405
  try {
1391
2406
  const workspacePath = resolveCliWorkspaceRoot(cwd, parsed.workspaceRoot);
2407
+ const workspaceError = validateCliWorkspaceRoot(workspacePath, parsed.workspaceRoot);
2408
+ if (workspaceError) {
2409
+ stderr(`${workspaceError}\n`);
2410
+ return 1;
2411
+ }
1392
2412
  const runtime = await createHarness(workspacePath);
1393
2413
  if (parsed.transport === "http") {
1394
2414
  const server = await serveAcpHttp(runtime, {
@@ -1426,6 +2446,11 @@ export async function runCli(argv, io = {}, deps = {}) {
1426
2446
  }
1427
2447
  try {
1428
2448
  const workspacePath = resolveCliWorkspaceRoot(cwd, parsed.workspaceRoot);
2449
+ const workspaceError = validateCliWorkspaceRoot(workspacePath, parsed.workspaceRoot);
2450
+ if (workspaceError) {
2451
+ stderr(`${workspaceError}\n`);
2452
+ return 1;
2453
+ }
1429
2454
  const runtime = await createHarness(workspacePath);
1430
2455
  const server = await serveAgUi(runtime, {
1431
2456
  hostname: parsed.hostname,
@@ -1456,6 +2481,11 @@ export async function runCli(argv, io = {}, deps = {}) {
1456
2481
  }
1457
2482
  try {
1458
2483
  const workspacePath = resolveCliWorkspaceRoot(cwd, parsed.workspaceRoot);
2484
+ const workspaceError = validateCliWorkspaceRoot(workspacePath, parsed.workspaceRoot);
2485
+ if (workspaceError) {
2486
+ stderr(`${workspaceError}\n`);
2487
+ return 1;
2488
+ }
1459
2489
  const runtime = await createHarness(workspacePath);
1460
2490
  const server = await serveA2a(runtime, {
1461
2491
  hostname: parsed.hostname,
@@ -1486,6 +2516,11 @@ export async function runCli(argv, io = {}, deps = {}) {
1486
2516
  }
1487
2517
  try {
1488
2518
  const workspacePath = resolveCliWorkspaceRoot(cwd, parsed.workspaceRoot);
2519
+ const workspaceError = validateCliWorkspaceRoot(workspacePath, parsed.workspaceRoot);
2520
+ if (workspaceError) {
2521
+ stderr(`${workspaceError}\n`);
2522
+ return 1;
2523
+ }
1489
2524
  const runtime = await createHarness(workspacePath);
1490
2525
  stderr(`Serving runtime MCP over stdio from ${workspacePath}\n`);
1491
2526
  const server = await serveRuntimeMcp(runtime);
@@ -1509,6 +2544,41 @@ export async function runCli(argv, io = {}, deps = {}) {
1509
2544
  stderr(renderUsage());
1510
2545
  return 1;
1511
2546
  }
2547
+ if (subcommand === "scheduled-run") {
2548
+ const parsed = parseScheduledRunOptions([possibleNestedCommand, possibleThirdCommand, ...remainingArgs].filter((item) => typeof item === "string"));
2549
+ if (parsed.error) {
2550
+ stderr(`${parsed.error}\n`);
2551
+ stderr(renderUsage());
2552
+ return 1;
2553
+ }
2554
+ if (!parsed.scheduleId) {
2555
+ stderr("Missing value for --schedule\n");
2556
+ stderr(renderUsage());
2557
+ return 1;
2558
+ }
2559
+ try {
2560
+ const workspacePath = resolveCliWorkspaceRoot(cwd, parsed.workspaceRoot);
2561
+ const workspaceError = validateCliWorkspaceRoot(workspacePath, parsed.workspaceRoot);
2562
+ if (workspaceError) {
2563
+ stderr(`${workspaceError}\n`);
2564
+ return 1;
2565
+ }
2566
+ const runtime = await createHarness(workspacePath);
2567
+ try {
2568
+ const result = await runtime.runScheduledTask(parsed.scheduleId);
2569
+ stdout(`${String(result.output ?? "").trim()}\n`);
2570
+ }
2571
+ finally {
2572
+ await runtime.stop();
2573
+ }
2574
+ return 0;
2575
+ }
2576
+ catch (error) {
2577
+ const message = error instanceof Error ? error.message : String(error);
2578
+ stderr(`${message}\n`);
2579
+ return 1;
2580
+ }
2581
+ }
1512
2582
  if (subcommand === "export") {
1513
2583
  const exportTarget = possibleNestedCommand;
1514
2584
  const parsed = parseRuntimeExportOptions([possibleThirdCommand, ...remainingArgs].filter((item) => typeof item === "string"));
@@ -1532,7 +2602,13 @@ export async function runCli(argv, io = {}, deps = {}) {
1532
2602
  return 1;
1533
2603
  }
1534
2604
  try {
1535
- const runtime = await createHarness(resolveCliWorkspaceRoot(cwd, parsed.workspaceRoot));
2605
+ const workspacePath = resolveCliWorkspaceRoot(cwd, parsed.workspaceRoot);
2606
+ const workspaceError = validateCliWorkspaceRoot(workspacePath, parsed.workspaceRoot);
2607
+ if (workspaceError) {
2608
+ stderr(`${workspaceError}\n`);
2609
+ return 1;
2610
+ }
2611
+ const runtime = await createHarness(workspacePath);
1536
2612
  try {
1537
2613
  if (exportTarget === "request") {
1538
2614
  const pkg = await runtime.exportRequestPackage({
@@ -1579,6 +2655,11 @@ export async function runCli(argv, io = {}, deps = {}) {
1579
2655
  }
1580
2656
  try {
1581
2657
  const workspacePath = resolveCliWorkspaceRoot(cwd, parsed.workspaceRoot);
2658
+ const workspaceError = validateCliWorkspaceRoot(workspacePath, parsed.workspaceRoot);
2659
+ if (workspaceError) {
2660
+ stderr(`${workspaceError}\n`);
2661
+ return 1;
2662
+ }
1582
2663
  const runtime = await createHarness(workspacePath);
1583
2664
  if (subcommand === "health") {
1584
2665
  const snapshot = await runtime.getHealth();
@@ -1643,7 +2724,15 @@ export async function runCli(argv, io = {}, deps = {}) {
1643
2724
  stderr(renderUsage());
1644
2725
  return 1;
1645
2726
  }
1646
- const invokedPath = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : "";
2727
+ export function resolveInvokedCliHref(argvPath) {
2728
+ if (!argvPath) {
2729
+ return "";
2730
+ }
2731
+ const resolved = path.resolve(argvPath);
2732
+ const realPath = existsSync(resolved) ? realpathSync(resolved) : resolved;
2733
+ return pathToFileURL(realPath).href;
2734
+ }
2735
+ const invokedPath = resolveInvokedCliHref(process.argv[1]);
1647
2736
  if (import.meta.url === invokedPath) {
1648
2737
  const exitCode = await runCli(process.argv.slice(2));
1649
2738
  process.exitCode = exitCode;