@botbotgo/agent-harness 0.0.294 → 0.0.296

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 (38) hide show
  1. package/README.md +27 -0
  2. package/README.zh.md +27 -0
  3. package/dist/acp.d.ts +28 -3
  4. package/dist/acp.js +100 -7
  5. package/dist/api.d.ts +2 -2
  6. package/dist/cli.d.ts +28 -0
  7. package/dist/cli.js +923 -12
  8. package/dist/client/acp.d.ts +44 -0
  9. package/dist/client/acp.js +165 -0
  10. package/dist/client/in-process.d.ts +44 -0
  11. package/dist/client/in-process.js +69 -0
  12. package/dist/client/index.d.ts +4 -0
  13. package/dist/client/index.js +2 -0
  14. package/dist/client/types.d.ts +56 -0
  15. package/dist/client/types.js +1 -0
  16. package/dist/client.d.ts +1 -0
  17. package/dist/client.js +1 -0
  18. package/dist/config/agents/orchestra.yaml +16 -3
  19. package/dist/index.d.ts +4 -2
  20. package/dist/index.js +1 -0
  21. package/dist/init-project.js +89 -0
  22. package/dist/package-version.d.ts +1 -1
  23. package/dist/package-version.js +1 -1
  24. package/dist/protocol/acp/client.d.ts +8 -2
  25. package/dist/protocol/acp/client.js +143 -0
  26. package/dist/resource/resource-impl.js +21 -4
  27. package/dist/resources/package.json +6 -0
  28. package/dist/resources/skills/approval-execution-policy/SKILL.md +22 -0
  29. package/dist/resources/skills/completion-discipline/SKILL.md +22 -0
  30. package/dist/resources/skills/delegation-discipline/SKILL.md +22 -0
  31. package/dist/resources/skills/safe-editing/SKILL.md +22 -0
  32. package/dist/resources/skills/workspace-inspection/SKILL.md +22 -0
  33. package/dist/runtime/adapter/runtime-adapter-support.d.ts +4 -2
  34. package/dist/runtime/adapter/runtime-adapter-support.js +10 -0
  35. package/dist/runtime/adapter/tool/builtin-middleware-tools.d.ts +74 -0
  36. package/dist/runtime/adapter/tool/builtin-middleware-tools.js +192 -1
  37. package/dist/runtime/agent-runtime-adapter.js +10 -0
  38. package/package.json +12 -2
package/dist/cli.js CHANGED
@@ -1,7 +1,13 @@
1
1
  #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { createInterface as createReadlineInterface } from "node:readline";
5
+ import { Writable } from "node:stream";
2
6
  import path from "node:path";
3
- import { pathToFileURL } from "node:url";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+ import YAML from "yaml";
4
9
  import { createAgentHarness } from "./api.js";
10
+ import { createAcpHttpHarnessClient, createAcpStdioHarnessClient } from "./client.js";
5
11
  import { initProject } from "./init-project.js";
6
12
  import { serveA2aOverHttp } from "./protocol/a2a/http.js";
7
13
  import { serveAgUiOverHttp } from "./protocol/ag-ui/http.js";
@@ -11,6 +17,7 @@ import { serveRuntimeMcpOverStdio } from "./mcp.js";
11
17
  function renderUsage() {
12
18
  return `Usage:
13
19
  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>]
14
21
  agent-harness acp serve [--workspace <path>] [--transport stdio|http] [--host <hostname>] [--port <port>]
15
22
  agent-harness a2a serve [--workspace <path>] [--host <hostname>] [--port <port>]
16
23
  agent-harness ag-ui serve [--workspace <path>] [--host <hostname>] [--port <port>]
@@ -159,6 +166,56 @@ function parseHttpServeOptions(args, serviceLabel = "HTTP") {
159
166
  }
160
167
  return { workspaceRoot, hostname, port };
161
168
  }
169
+ function parseChatOptions(args) {
170
+ let workspaceRoot;
171
+ let agentId;
172
+ let sessionId;
173
+ let message;
174
+ let transport = "stdio";
175
+ let hostname;
176
+ let port;
177
+ for (let index = 0; index < args.length; index += 1) {
178
+ const arg = args[index];
179
+ if (arg === "--workspace" || arg === "--agent" || arg === "--session" || arg === "--message" || arg === "--transport" || arg === "--host" || arg === "--port") {
180
+ const value = args[index + 1];
181
+ if (!value) {
182
+ return { workspaceRoot, agentId, sessionId, message, transport, hostname, port, error: `Missing value for ${arg}` };
183
+ }
184
+ if (arg === "--workspace") {
185
+ workspaceRoot = value;
186
+ }
187
+ else if (arg === "--agent") {
188
+ agentId = value;
189
+ }
190
+ else if (arg === "--session") {
191
+ sessionId = value;
192
+ }
193
+ else if (arg === "--message") {
194
+ message = value;
195
+ }
196
+ else if (arg === "--transport") {
197
+ if (value !== "stdio" && value !== "http") {
198
+ return { workspaceRoot, agentId, sessionId, message, transport, hostname, port, error: `Unsupported chat transport: ${value}` };
199
+ }
200
+ transport = value;
201
+ }
202
+ else if (arg === "--host") {
203
+ hostname = value;
204
+ }
205
+ else {
206
+ const parsedPort = Number.parseInt(value, 10);
207
+ if (!Number.isFinite(parsedPort) || parsedPort <= 0) {
208
+ return { workspaceRoot, agentId, sessionId, message, transport, hostname, port, error: `Invalid chat port: ${value}` };
209
+ }
210
+ port = parsedPort;
211
+ }
212
+ index += 1;
213
+ continue;
214
+ }
215
+ return { workspaceRoot, agentId, sessionId, message, transport, hostname, port, error: `Unknown option: ${arg}` };
216
+ }
217
+ return { workspaceRoot, agentId, sessionId, message, transport, hostname, port };
218
+ }
162
219
  function parseRuntimeInspectOptions(args) {
163
220
  let workspaceRoot;
164
221
  let json = false;
@@ -294,6 +351,25 @@ function parseRuntimeExportOptions(args) {
294
351
  json,
295
352
  };
296
353
  }
354
+ function resolveCliWorkspaceRoot(cwd, inputPath) {
355
+ const resolved = path.resolve(cwd, inputPath ?? ".");
356
+ const configRuntimePath = path.join(resolved, "config", "runtime", "workspace.yaml");
357
+ if (existsSync(configRuntimePath)) {
358
+ return resolved;
359
+ }
360
+ const directRuntimePath = path.join(resolved, "runtime", "workspace.yaml");
361
+ if (existsSync(directRuntimePath) && path.basename(resolved) === "config") {
362
+ return path.dirname(resolved);
363
+ }
364
+ return resolved;
365
+ }
366
+ function resolveCliConfigRoot(workspaceRoot) {
367
+ const nested = path.join(workspaceRoot, "config");
368
+ if (existsSync(path.join(nested, "runtime", "workspace.yaml"))) {
369
+ return nested;
370
+ }
371
+ return workspaceRoot;
372
+ }
297
373
  function renderJson(value) {
298
374
  return `${JSON.stringify(value, null, 2)}\n`;
299
375
  }
@@ -423,11 +499,584 @@ function renderOperatorOverview(overview, workspacePath) {
423
499
  }
424
500
  return `${lines.join("\n")}\n`;
425
501
  }
502
+ 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"],
522
+ ];
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");
538
+ }
539
+ function trimAsciiBlock(block) {
540
+ return block
541
+ .split("\n")
542
+ .map((line) => line.replace(/\s+$/u, ""))
543
+ .join("\n");
544
+ }
545
+ /** Figlet Small font — trimmed for clean right edge */
546
+ const CHAT_ASCII_BOTBOTGO = trimAsciiBlock(" ____ ___ _____ ____ ___ _____ ____ ___ \n" +
547
+ " | __ ) / _ \\_ _| __ ) / _ \\_ _/ ___|/ _ \\ \n" +
548
+ " | _ \\| | | || | | _ \\| | | || || | _| | | |\n" +
549
+ " | |_) | |_| || | | |_) | |_| || || |_| | |_| |\n" +
550
+ " |____/ \\___/ |_| |____/ \\___/ |_| \\____|\\___/");
551
+ const CHAT_ASCII_AGENT_HARNESS = trimAsciiBlock(" _ ____ _____ _ _ _____ _ _ _ ____ _ _ _____ ____ ____ \n" +
552
+ " / \\ / ___| ____| \\ | |_ _| | | | | / \\ | _ \\| \\ | | ____/ ___/ ___| \n" +
553
+ " / _ \\| | _| _| | \\| | | | | |_| | / _ \\ | |_) | \\| | _| \\___ \\___ \\ \n" +
554
+ " / ___ \\ |_| | |___| |\\ | | | | _ |/ ___ \\| _ <| |\\ | |___ ___) |__) |\n" +
555
+ " /_/ \\_\\____|_____|_| \\_| |_| |_| |_/_/ \\_\\_| \\_\\_| \\_|_____|____/____/");
556
+ /**
557
+ * Solid fills (one 256-color per Figlet row, no gradient):
558
+ * BOTBOTGO ≈ US flag: blue / white / red / white / red stripes top→bottom.
559
+ * AGENT HARNESS ≈ 中国国旗: upper rows gold (星区黄), lower rows field red.
560
+ */
561
+ const CHAT_LOGO_BRAND_LINE_COLORS = [27, 255, 196, 255, 196];
562
+ const CHAT_LOGO_PRODUCT_LINE_COLORS = [226, 226, 196, 196, 196];
563
+ /** One foreground color per entire line — flat, no relief shading */
564
+ function colorizeSolidAsciiBlock(block, lineColors, enabled) {
565
+ const trimmed = trimAsciiBlock(block);
566
+ const lines = trimmed.split("\n");
567
+ if (!enabled) {
568
+ return trimmed;
569
+ }
570
+ return lines
571
+ .map((line, i) => {
572
+ const code = lineColors[Math.min(i, lineColors.length - 1)] ?? lineColors[lineColors.length - 1];
573
+ return `\x1b[38;5;${code}m${line}\x1b[0m`;
574
+ })
575
+ .join("\n");
576
+ }
577
+ function ellipsizeChatId(value, maxChars) {
578
+ if (value.length <= maxChars) {
579
+ return value;
580
+ }
581
+ if (maxChars <= 1) {
582
+ return "…";
583
+ }
584
+ return `${value.slice(0, maxChars - 1)}…`;
585
+ }
586
+ function renderChatPromptLine(input) {
587
+ 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 `);
595
+ }
596
+ async function* iterateChatLines(rl, nextPrompt) {
597
+ rl.setPrompt(nextPrompt());
598
+ rl.prompt();
599
+ const asyncIterator = rl[Symbol.asyncIterator]();
600
+ while (true) {
601
+ const step = await asyncIterator.next();
602
+ if (step.done) {
603
+ break;
604
+ }
605
+ yield step.value;
606
+ rl.setPrompt(nextPrompt());
607
+ rl.prompt();
608
+ }
609
+ }
610
+ function renderChatBanner(input) {
611
+ 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;
632
+ const brandArt = colorizeSolidAsciiBlock(CHAT_ASCII_BOTBOTGO, CHAT_LOGO_BRAND_LINE_COLORS, color);
633
+ 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);
644
+ 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");
653
+ }
654
+ function renderRequestEvents(events) {
655
+ if (events.length === 0) {
656
+ return "No events recorded.\n";
657
+ }
658
+ return events.map((event) => {
659
+ const timestamp = formatTimestamp(event.timestamp) ?? "unknown-time";
660
+ const eventType = typeof event.eventType === "string" ? event.eventType : "unknown-event";
661
+ return `${timestamp} ${eventType}`;
662
+ }).join("\n") + "\n";
663
+ }
664
+ function renderSessionSummaries(summaries) {
665
+ if (summaries.length === 0) {
666
+ return "No sessions recorded.\n";
667
+ }
668
+ return summaries.map((summary) => {
669
+ const sessionId = typeof summary.sessionId === "string" ? summary.sessionId : "unknown";
670
+ const entryAgentId = typeof summary.entryAgentId === "string" ? ` agent=${summary.entryAgentId}` : "";
671
+ const state = typeof summary.currentState === "string" ? ` state=${summary.currentState}` : "";
672
+ const messageCount = typeof summary.messageCount === "number" ? ` messages=${summary.messageCount}` : "";
673
+ const title = typeof summary.title === "string" && summary.title.trim().length > 0 ? ` title=${summary.title}` : "";
674
+ const snippet = typeof summary.snippet === "string" && summary.snippet.trim().length > 0 ? ` snippet=${summary.snippet}` : "";
675
+ return `${sessionId}${entryAgentId}${state}${messageCount}${title}${snippet}`;
676
+ }).join("\n") + "\n";
677
+ }
678
+ function renderRequestTraceItems(items) {
679
+ if (items.length === 0) {
680
+ return "No trace items recorded.\n";
681
+ }
682
+ return items.map((item) => {
683
+ const surfaceItem = isObject(item.surfaceItem) ? item.surfaceItem : {};
684
+ const kind = typeof surfaceItem.kind === "string" ? surfaceItem.kind : "unknown";
685
+ const id = typeof surfaceItem.id === "string" ? surfaceItem.id : typeof surfaceItem.name === "string" ? surfaceItem.name : "unknown";
686
+ const agentId = typeof surfaceItem.agentId === "string" ? ` agent=${surfaceItem.agentId}` : "";
687
+ return `${kind}:${id}${agentId}`;
688
+ }).join("\n") + "\n";
689
+ }
690
+ function renderChatContext(input) {
691
+ return [
692
+ `agent=${input.agentId ?? "none"}`,
693
+ `session=${input.sessionId ?? "none"}`,
694
+ `request=${input.requestId ?? "none"}`,
695
+ ].join(" ") + "\n";
696
+ }
697
+ function normalizeChatCommand(line) {
698
+ const trimmed = line.trim();
699
+ if (!trimmed.startsWith("/")) {
700
+ return null;
701
+ }
702
+ const [name, ...rest] = trimmed.slice(1).split(/\s+/);
703
+ return {
704
+ name,
705
+ arg: rest.length > 0 ? rest.join(" ") : undefined,
706
+ };
707
+ }
708
+ function asRecord(value) {
709
+ return typeof value === "object" && value !== null ? value : undefined;
710
+ }
711
+ function readYamlFile(filePath) {
712
+ return YAML.parse(readFileSync(filePath, "utf8"));
713
+ }
714
+ function getWorkspaceDefaultAgentId(workspaceRoot) {
715
+ const runtimePath = path.join(resolveCliConfigRoot(workspaceRoot), "runtime", "workspace.yaml");
716
+ if (!existsSync(runtimePath)) {
717
+ return undefined;
718
+ }
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;
725
+ }
726
+ 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;
736
+ }
737
+ 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
+ const modelName = modelRef.startsWith("model/") ? modelRef.slice("model/".length) : modelRef;
745
+ const model = spec.find((item) => item.name === modelName);
746
+ if (!model) {
747
+ return undefined;
748
+ }
749
+ return {
750
+ provider: typeof model.provider === "string" ? model.provider : undefined,
751
+ model: typeof model.model === "string" ? model.model : undefined,
752
+ baseUrl: typeof model.baseUrl === "string" ? model.baseUrl : undefined,
753
+ };
754
+ }
755
+ function readChatWorkspaceModelInfo(workspaceRoot, agentId) {
756
+ try {
757
+ const resolvedAgentId = agentId ?? getWorkspaceDefaultAgentId(workspaceRoot);
758
+ if (!resolvedAgentId) {
759
+ return getModelInfo(workspaceRoot, "model/default");
760
+ }
761
+ const modelRef = getAgentModelRef(workspaceRoot, resolvedAgentId) ?? "model/default";
762
+ return getModelInfo(workspaceRoot, modelRef);
763
+ }
764
+ catch {
765
+ return undefined;
766
+ }
767
+ }
768
+ function renderProviderFailureHint(modelInfo) {
769
+ if (!modelInfo?.provider) {
770
+ return undefined;
771
+ }
772
+ if (modelInfo.provider === "ollama") {
773
+ const modelText = modelInfo.model ? ` and ensure \`${modelInfo.model}\` is available` : "";
774
+ return `Hint: start Ollama${modelInfo.baseUrl ? ` at ${modelInfo.baseUrl}` : ""}${modelText}. Example: \`ollama serve\`${modelInfo.model ? ` and \`ollama pull ${modelInfo.model}\`` : ""}.`;
775
+ }
776
+ if (modelInfo.provider === "openai") {
777
+ return "Hint: verify network access and that `OPENAI_API_KEY` is set for this shell.";
778
+ }
779
+ if (modelInfo.provider === "anthropic") {
780
+ return "Hint: verify network access and that `ANTHROPIC_API_KEY` is set for this shell.";
781
+ }
782
+ if (modelInfo.provider === "google" || modelInfo.provider === "google-genai" || modelInfo.provider === "gemini") {
783
+ return "Hint: verify network access and that the configured Google API key is available in this shell.";
784
+ }
785
+ if (modelInfo.provider === "openai-compatible") {
786
+ return `Hint: verify the configured endpoint${modelInfo.baseUrl ? ` (${modelInfo.baseUrl})` : ""} and the required API key for that provider.`;
787
+ }
788
+ return undefined;
789
+ }
790
+ export function renderChatRuntimeFailure(output, modelInfo) {
791
+ const trimmed = output.trim();
792
+ if (!trimmed.startsWith("runtime_error=")) {
793
+ return output;
794
+ }
795
+ const normalized = trimmed.toLowerCase();
796
+ if (!normalized.includes("fetch failed") &&
797
+ !normalized.includes("connection error") &&
798
+ !normalized.includes("timed out") &&
799
+ !normalized.includes("404 page not found")) {
800
+ return output;
801
+ }
802
+ const lines = [trimmed];
803
+ if (modelInfo?.provider || modelInfo?.model) {
804
+ lines.push(`provider=${modelInfo?.provider ?? "unknown"}${modelInfo?.model ? ` model=${modelInfo.model}` : ""}`);
805
+ }
806
+ if (modelInfo?.baseUrl) {
807
+ lines.push(`endpoint=${modelInfo.baseUrl}`);
808
+ }
809
+ if (normalized.includes("404 page not found") && modelInfo?.provider === "ollama") {
810
+ lines.push("Hint: the configured endpoint responded, but it does not look like an Ollama API route. Check that `baseUrl` points at the Ollama server root and that no other service is bound to this port.");
811
+ }
812
+ const hint = renderProviderFailureHint(modelInfo);
813
+ if (hint) {
814
+ lines.push(hint);
815
+ }
816
+ return lines.join("\n");
817
+ }
818
+ function renderChatTextChunk(text, modelInfo) {
819
+ return renderChatRuntimeFailure(text, modelInfo);
820
+ }
821
+ function truncateChatToolPreview(value, maxChars = 800) {
822
+ if (value.length <= maxChars) {
823
+ return value;
824
+ }
825
+ return `${value.slice(0, maxChars - 15)}\n...[truncated]`;
826
+ }
827
+ function extractChatToolTextContent(value) {
828
+ if (typeof value === "string") {
829
+ return value;
830
+ }
831
+ if (Array.isArray(value)) {
832
+ return value
833
+ .map((item) => extractChatToolTextContent(item))
834
+ .filter((item) => item.trim().length > 0)
835
+ .join("\n");
836
+ }
837
+ if (!value || typeof value !== "object") {
838
+ return "";
839
+ }
840
+ const typed = value;
841
+ if (typeof typed.text === "string") {
842
+ return typed.text;
843
+ }
844
+ if (typeof typed.content === "string") {
845
+ return typed.content;
846
+ }
847
+ if (typed.content !== undefined) {
848
+ const nestedContent = extractChatToolTextContent(typed.content);
849
+ if (nestedContent.trim().length > 0) {
850
+ return nestedContent;
851
+ }
852
+ }
853
+ if (typed.kwargs !== undefined) {
854
+ const nestedKwargs = extractChatToolTextContent(typed.kwargs);
855
+ if (nestedKwargs.trim().length > 0) {
856
+ return nestedKwargs;
857
+ }
858
+ }
859
+ if (typed.message !== undefined) {
860
+ const nestedMessage = extractChatToolTextContent(typed.message);
861
+ if (nestedMessage.trim().length > 0) {
862
+ return nestedMessage;
863
+ }
864
+ }
865
+ if (typed.body !== undefined) {
866
+ const nestedBody = extractChatToolTextContent(typed.body);
867
+ if (nestedBody.trim().length > 0) {
868
+ return nestedBody;
869
+ }
870
+ }
871
+ if (typed.answer !== undefined) {
872
+ const nestedAnswer = extractChatToolTextContent(typed.answer);
873
+ if (nestedAnswer.trim().length > 0) {
874
+ return nestedAnswer;
875
+ }
876
+ }
877
+ return "";
878
+ }
879
+ function summarizeChatToolResult(output) {
880
+ if (typeof output === "string") {
881
+ return truncateChatToolPreview(output);
882
+ }
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));
899
+ }
900
+ export async function probeChatWorkspace(input) {
901
+ const modelInfo = readChatWorkspaceModelInfo(input.workspaceRoot, input.agentId);
902
+ if (!modelInfo?.provider || modelInfo.provider !== "ollama" || !modelInfo.baseUrl) {
903
+ return undefined;
904
+ }
905
+ try {
906
+ const response = await fetch(new URL("/api/tags", modelInfo.baseUrl), {
907
+ method: "GET",
908
+ headers: { accept: "application/json" },
909
+ });
910
+ if (response.ok) {
911
+ return undefined;
912
+ }
913
+ if (response.status === 404) {
914
+ return renderChatRuntimeFailure("runtime_error=404 page not found", modelInfo);
915
+ }
916
+ return renderChatRuntimeFailure(`runtime_error=HTTP ${response.status} ${response.statusText}`.trim(), modelInfo);
917
+ }
918
+ catch (error) {
919
+ const message = error instanceof Error ? error.message : String(error);
920
+ return renderChatRuntimeFailure(`runtime_error=${message}`, modelInfo);
921
+ }
922
+ }
923
+ export function isChatServerNoiseLine(line) {
924
+ const trimmed = line.trim();
925
+ if (!trimmed) {
926
+ return true;
927
+ }
928
+ return (trimmed.startsWith("Serving ACP over stdio from ") ||
929
+ trimmed === "langsmith/experimental/sandbox is in alpha. This feature is experimental, and breaking changes are expected." ||
930
+ trimmed === "llamaindex was already imported. This breaks constructor checks and will lead to issues!");
931
+ }
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.");
953
+ }
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`);
963
+ }
964
+ }
965
+ });
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();
974
+ }
975
+ });
976
+ }
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
+ },
1003
+ };
1004
+ }
1005
+ async function streamChatMessage(input) {
1006
+ let latestSessionId = input.sessionId;
1007
+ let latestRequestId;
1008
+ let latestAgentId = input.agentId;
1009
+ let wroteContent = false;
1010
+ 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;
1023
+ }
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;
1044
+ }
1045
+ }
1046
+ continue;
1047
+ }
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;
1054
+ }
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));
1060
+ }
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");
1066
+ }
1067
+ }
1068
+ }
1069
+ return { sessionId: latestSessionId, requestId: latestRequestId, agentId: latestAgentId };
1070
+ }
426
1071
  export async function runCli(argv, io = {}, deps = {}) {
427
1072
  const cwd = io.cwd ?? process.cwd();
1073
+ const stdin = io.stdin ?? process.stdin;
428
1074
  const stdout = io.stdout ?? ((message) => process.stdout.write(message));
429
1075
  const stderr = io.stderr ?? ((message) => process.stderr.write(message));
430
1076
  const [command, projectName, ...rest] = argv;
1077
+ const createChatClient = deps.createChatClient ?? createSubprocessChatClient;
1078
+ const probeWorkspace = deps.probeChatWorkspace ?? probeChatWorkspace;
1079
+ const createChatLineReader = deps.createReadlineInterface ?? createReadlineInterface;
431
1080
  const createHarness = deps.createAgentHarness ?? createAgentHarness;
432
1081
  const serveA2a = deps.serveA2aOverHttp ?? serveA2aOverHttp;
433
1082
  const serveAgUi = deps.serveAgUiOverHttp ?? serveAgUiOverHttp;
@@ -464,6 +1113,268 @@ export async function runCli(argv, io = {}, deps = {}) {
464
1113
  return 1;
465
1114
  }
466
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());
1121
+ return 1;
1122
+ }
1123
+ const workspacePath = resolveCliWorkspaceRoot(cwd, parsed.workspaceRoot);
1124
+ const workspaceModelInfo = readChatWorkspaceModelInfo(workspacePath, parsed.agentId);
1125
+ let client;
1126
+ try {
1127
+ client = await createChatClient({
1128
+ workspaceRoot: workspacePath,
1129
+ transport: parsed.transport,
1130
+ hostname: parsed.hostname,
1131
+ port: parsed.port,
1132
+ stderr,
1133
+ });
1134
+ let activeAgentId = parsed.agentId;
1135
+ let activeSessionId = parsed.sessionId;
1136
+ let latestRequestId;
1137
+ const preflightWarning = await probeWorkspace({
1138
+ workspaceRoot: workspacePath,
1139
+ agentId: activeAgentId,
1140
+ });
1141
+ if (parsed.message) {
1142
+ const streamed = await streamChatMessage({
1143
+ client,
1144
+ stdout,
1145
+ stderr,
1146
+ agentId: activeAgentId,
1147
+ sessionId: activeSessionId,
1148
+ message: parsed.message,
1149
+ modelInfo: workspaceModelInfo,
1150
+ });
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
+ await client.stop();
1158
+ return 0;
1159
+ }
1160
+ const stdinStream = stdin;
1161
+ const useColor = stdinStream.isTTY === true && io.stdout === undefined;
1162
+ stdout(renderChatBanner({
1163
+ workspacePath,
1164
+ transport: parsed.transport,
1165
+ agentId: activeAgentId,
1166
+ sessionId: activeSessionId,
1167
+ color: useColor,
1168
+ }));
1169
+ if (preflightWarning) {
1170
+ stdout(`${preflightWarning}\n\n`);
1171
+ }
1172
+ const stdoutSink = new Writable({
1173
+ decodeStrings: false,
1174
+ write(chunk, _encoding, callback) {
1175
+ stdout(typeof chunk === "string" ? chunk : String(chunk));
1176
+ callback();
1177
+ },
1178
+ });
1179
+ const lineReader = createChatLineReader({
1180
+ input: stdin,
1181
+ output: stdoutSink,
1182
+ crlfDelay: Infinity,
1183
+ terminal: stdinStream.isTTY === true,
1184
+ });
1185
+ try {
1186
+ for await (const raw of iterateChatLines(lineReader, () => renderChatPromptLine({
1187
+ agentId: activeAgentId,
1188
+ sessionId: activeSessionId,
1189
+ requestId: latestRequestId,
1190
+ color: useColor,
1191
+ }))) {
1192
+ const trimmed = raw.trim();
1193
+ if (!trimmed) {
1194
+ continue;
1195
+ }
1196
+ const chatCommand = normalizeChatCommand(trimmed);
1197
+ if (!chatCommand) {
1198
+ const streamed = await streamChatMessage({
1199
+ client,
1200
+ stdout,
1201
+ stderr,
1202
+ agentId: activeAgentId,
1203
+ sessionId: activeSessionId,
1204
+ message: trimmed,
1205
+ modelInfo: workspaceModelInfo,
1206
+ });
1207
+ activeSessionId = streamed.sessionId;
1208
+ latestRequestId = streamed.requestId;
1209
+ activeAgentId = streamed.agentId ?? activeAgentId;
1210
+ continue;
1211
+ }
1212
+ if (chatCommand.name === "exit" || chatCommand.name === "quit") {
1213
+ break;
1214
+ }
1215
+ if (chatCommand.name === "help") {
1216
+ stdout(renderChatHelp());
1217
+ continue;
1218
+ }
1219
+ if (chatCommand.name === "context") {
1220
+ stdout(renderChatContext({
1221
+ agentId: activeAgentId,
1222
+ sessionId: activeSessionId,
1223
+ requestId: latestRequestId,
1224
+ }));
1225
+ continue;
1226
+ }
1227
+ if (chatCommand.name === "new") {
1228
+ activeSessionId = undefined;
1229
+ latestRequestId = undefined;
1230
+ stdout(renderChatContext({
1231
+ agentId: activeAgentId,
1232
+ sessionId: activeSessionId,
1233
+ requestId: latestRequestId,
1234
+ }));
1235
+ continue;
1236
+ }
1237
+ if (chatCommand.name === "agent") {
1238
+ if (!chatCommand.arg) {
1239
+ stdout(activeAgentId ? `${activeAgentId}\n` : "No active agent override.\n");
1240
+ continue;
1241
+ }
1242
+ activeAgentId = chatCommand.arg;
1243
+ stdout(`agent=${activeAgentId}\n`);
1244
+ continue;
1245
+ }
1246
+ if (chatCommand.name === "session") {
1247
+ stdout(activeSessionId ? `${activeSessionId}\n` : "No active session.\n");
1248
+ continue;
1249
+ }
1250
+ if (chatCommand.name === "request") {
1251
+ if (!chatCommand.arg) {
1252
+ stdout(latestRequestId ? `${latestRequestId}\n` : "No active request.\n");
1253
+ continue;
1254
+ }
1255
+ const selected = await client.getRequest(chatCommand.arg);
1256
+ if (!selected) {
1257
+ stdout(`Request not found: ${chatCommand.arg}\n`);
1258
+ continue;
1259
+ }
1260
+ latestRequestId = selected.requestId;
1261
+ activeSessionId = selected.sessionId;
1262
+ activeAgentId = selected.agentId;
1263
+ stdout(`request=${latestRequestId} session=${activeSessionId}\n`);
1264
+ continue;
1265
+ }
1266
+ if (chatCommand.name === "sessions") {
1267
+ const summaries = await client.listSessionSummaries(parsed.agentId ? { agentId: parsed.agentId } : undefined);
1268
+ stdout(renderSessionSummaries(summaries));
1269
+ continue;
1270
+ }
1271
+ if (chatCommand.name === "requests") {
1272
+ const requests = await client.listRequests(activeSessionId
1273
+ ? { sessionId: activeSessionId }
1274
+ : activeAgentId
1275
+ ? { agentId: activeAgentId }
1276
+ : undefined);
1277
+ stdout(renderRequestList(requests));
1278
+ continue;
1279
+ }
1280
+ if (chatCommand.name === "resume") {
1281
+ if (!chatCommand.arg) {
1282
+ stdout("Usage: /resume <sessionId>\n");
1283
+ continue;
1284
+ }
1285
+ const session = await client.getSession(chatCommand.arg);
1286
+ if (!session) {
1287
+ stdout(`Session not found: ${chatCommand.arg}\n`);
1288
+ continue;
1289
+ }
1290
+ activeSessionId = chatCommand.arg;
1291
+ latestRequestId = session.latestRequestId;
1292
+ activeAgentId = session.currentAgentId ?? session.entryAgentId ?? activeAgentId;
1293
+ stdout(`session=${activeSessionId}\n`);
1294
+ continue;
1295
+ }
1296
+ if (chatCommand.name === "cancel") {
1297
+ if (!latestRequestId) {
1298
+ stdout("No active request.\n");
1299
+ continue;
1300
+ }
1301
+ const result = await client.cancelRequest({
1302
+ requestId: latestRequestId,
1303
+ reason: "Cancelled from chat CLI",
1304
+ });
1305
+ activeSessionId = result.sessionId;
1306
+ latestRequestId = result.requestId;
1307
+ stdout(`${result.state}: ${result.output}\n`);
1308
+ continue;
1309
+ }
1310
+ if (chatCommand.name === "approvals") {
1311
+ const approvals = await client.listApprovals(activeSessionId ? { sessionId: activeSessionId, status: "pending" } : { status: "pending" });
1312
+ stdout(renderApprovalList(approvals));
1313
+ continue;
1314
+ }
1315
+ if ((chatCommand.name === "approve" || chatCommand.name === "reject") && chatCommand.arg) {
1316
+ const result = await client.resolveApproval({
1317
+ approvalId: chatCommand.arg,
1318
+ decision: chatCommand.name === "approve" ? "approve" : "reject",
1319
+ ...(activeSessionId ? { sessionId: activeSessionId } : {}),
1320
+ });
1321
+ activeSessionId = result.sessionId;
1322
+ latestRequestId = result.requestId;
1323
+ stdout(`${result.state}: ${result.output}\n`);
1324
+ continue;
1325
+ }
1326
+ if (chatCommand.name === "events") {
1327
+ if (!activeSessionId || !latestRequestId) {
1328
+ stdout("No active request.\n");
1329
+ continue;
1330
+ }
1331
+ const events = await client.listRequestEvents({
1332
+ sessionId: activeSessionId,
1333
+ requestId: latestRequestId,
1334
+ });
1335
+ stdout(renderRequestEvents(events));
1336
+ continue;
1337
+ }
1338
+ if (chatCommand.name === "trace") {
1339
+ if (!activeSessionId || !latestRequestId) {
1340
+ stdout("No active request.\n");
1341
+ continue;
1342
+ }
1343
+ const traceItems = await client.listRequestTraceItems({
1344
+ sessionId: activeSessionId,
1345
+ requestId: latestRequestId,
1346
+ });
1347
+ stdout(renderRequestTraceItems(traceItems));
1348
+ continue;
1349
+ }
1350
+ if (chatCommand.name === "health") {
1351
+ const health = await client.getHealth();
1352
+ stdout(renderHealthSnapshot(health, workspacePath));
1353
+ continue;
1354
+ }
1355
+ if (chatCommand.name === "overview") {
1356
+ const overview = await client.getOperatorOverview({ limit: 5 });
1357
+ stdout(renderOperatorOverview(overview, workspacePath));
1358
+ continue;
1359
+ }
1360
+ stdout("Unknown chat command. Use /help.\n");
1361
+ }
1362
+ }
1363
+ finally {
1364
+ lineReader.close();
1365
+ }
1366
+ await client.stop();
1367
+ return 0;
1368
+ }
1369
+ catch (error) {
1370
+ const message = error instanceof Error ? error.message : String(error);
1371
+ stderr(`${message}\n`);
1372
+ if (client) {
1373
+ await client.stop().catch(() => undefined);
1374
+ }
1375
+ return 1;
1376
+ }
1377
+ }
467
1378
  if (command === "acp") {
468
1379
  const [subcommand, ...subcommandArgs] = [projectName, ...rest];
469
1380
  if (subcommand !== "serve") {
@@ -477,8 +1388,8 @@ export async function runCli(argv, io = {}, deps = {}) {
477
1388
  return 1;
478
1389
  }
479
1390
  try {
480
- const runtime = await createHarness(path.resolve(cwd, parsed.workspaceRoot ?? "."));
481
- const workspacePath = path.resolve(cwd, parsed.workspaceRoot ?? ".");
1391
+ const workspacePath = resolveCliWorkspaceRoot(cwd, parsed.workspaceRoot);
1392
+ const runtime = await createHarness(workspacePath);
482
1393
  if (parsed.transport === "http") {
483
1394
  const server = await serveAcpHttp(runtime, {
484
1395
  hostname: parsed.hostname,
@@ -514,8 +1425,8 @@ export async function runCli(argv, io = {}, deps = {}) {
514
1425
  return 1;
515
1426
  }
516
1427
  try {
517
- const runtime = await createHarness(path.resolve(cwd, parsed.workspaceRoot ?? "."));
518
- const workspacePath = path.resolve(cwd, parsed.workspaceRoot ?? ".");
1428
+ const workspacePath = resolveCliWorkspaceRoot(cwd, parsed.workspaceRoot);
1429
+ const runtime = await createHarness(workspacePath);
519
1430
  const server = await serveAgUi(runtime, {
520
1431
  hostname: parsed.hostname,
521
1432
  port: parsed.port,
@@ -544,8 +1455,8 @@ export async function runCli(argv, io = {}, deps = {}) {
544
1455
  return 1;
545
1456
  }
546
1457
  try {
547
- const runtime = await createHarness(path.resolve(cwd, parsed.workspaceRoot ?? "."));
548
- const workspacePath = path.resolve(cwd, parsed.workspaceRoot ?? ".");
1458
+ const workspacePath = resolveCliWorkspaceRoot(cwd, parsed.workspaceRoot);
1459
+ const runtime = await createHarness(workspacePath);
549
1460
  const server = await serveA2a(runtime, {
550
1461
  hostname: parsed.hostname,
551
1462
  port: parsed.port,
@@ -574,8 +1485,8 @@ export async function runCli(argv, io = {}, deps = {}) {
574
1485
  return 1;
575
1486
  }
576
1487
  try {
577
- const runtime = await createHarness(path.resolve(cwd, parsed.workspaceRoot ?? "."));
578
- const workspacePath = path.resolve(cwd, parsed.workspaceRoot ?? ".");
1488
+ const workspacePath = resolveCliWorkspaceRoot(cwd, parsed.workspaceRoot);
1489
+ const runtime = await createHarness(workspacePath);
579
1490
  stderr(`Serving runtime MCP over stdio from ${workspacePath}\n`);
580
1491
  const server = await serveRuntimeMcp(runtime);
581
1492
  await new Promise((resolve, reject) => {
@@ -621,7 +1532,7 @@ export async function runCli(argv, io = {}, deps = {}) {
621
1532
  return 1;
622
1533
  }
623
1534
  try {
624
- const runtime = await createHarness(path.resolve(cwd, parsed.workspaceRoot ?? "."));
1535
+ const runtime = await createHarness(resolveCliWorkspaceRoot(cwd, parsed.workspaceRoot));
625
1536
  try {
626
1537
  if (exportTarget === "request") {
627
1538
  const pkg = await runtime.exportRequestPackage({
@@ -667,8 +1578,8 @@ export async function runCli(argv, io = {}, deps = {}) {
667
1578
  return 1;
668
1579
  }
669
1580
  try {
670
- const runtime = await createHarness(path.resolve(cwd, parsed.workspaceRoot ?? "."));
671
- const workspacePath = path.resolve(cwd, parsed.workspaceRoot ?? ".");
1581
+ const workspacePath = resolveCliWorkspaceRoot(cwd, parsed.workspaceRoot);
1582
+ const runtime = await createHarness(workspacePath);
672
1583
  if (subcommand === "health") {
673
1584
  const snapshot = await runtime.getHealth();
674
1585
  stdout(parsed.json ? renderJson(snapshot) : renderHealthSnapshot(snapshot, workspacePath));