nolo-cli 0.1.38 → 0.1.40

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 (65) hide show
  1. package/README.md +4 -4
  2. package/agent-runtime/agentRecordConfig.ts +0 -14
  3. package/agent-runtime/dialogWritePlan.ts +39 -0
  4. package/agent-runtime/hostAdapter.ts +4 -1
  5. package/agent-runtime/hybridRecordStore.ts +1 -1
  6. package/agent-runtime/index.ts +39 -18
  7. package/agent-runtime/localDialogRead.ts +23 -0
  8. package/agent-runtime/localLoop.ts +87 -7
  9. package/agent-runtime/localToolPolicy.ts +1 -17
  10. package/agent-runtime/localWorkspaceTools.ts +51 -110
  11. package/agent-runtime/noloWorkspaceTools.ts +371 -0
  12. package/agent-runtime/runtimeToolPolicy.ts +3 -14
  13. package/agent-runtime/runtimeToolSurface.ts +198 -0
  14. package/agent-runtime/types.ts +2 -1
  15. package/agentAliases.ts +8 -1
  16. package/agentRunCommand.ts +65 -95
  17. package/agentRuntimeLocal.ts +1 -7
  18. package/ai/agent/cliExecutor.ts +249 -34
  19. package/chat/messages/fetchMessages.ts +84 -0
  20. package/chat/messages/types.ts +185 -0
  21. package/cli/agentAliases.ts +8 -1
  22. package/cli/agentRunCommand.ts +65 -95
  23. package/cli/agentRuntimeLocal.ts +1 -7
  24. package/cli/cliEnvHelpers.ts +11 -7
  25. package/cli/client/agentRun.ts +94 -93
  26. package/cli/client/localRuntimeAdapter.ts +250 -69
  27. package/cli/commandRegistry.ts +2 -2
  28. package/cli/connectorRunArtifact.ts +0 -1
  29. package/cli/dialogCommands.ts +572 -0
  30. package/cli/dialogInternalCommandEntries.ts +4 -0
  31. package/cli/machineWsRunDispatch.ts +76 -39
  32. package/cli/offlineMarxistsAgentCommand.ts +6 -4
  33. package/cli/scriptCommandEntries.ts +0 -2
  34. package/cli/tui/readlineWorkspace.ts +69 -0
  35. package/cli/tui/session.ts +176 -1
  36. package/cliEnvHelpers.ts +11 -7
  37. package/client/agentConfigResolver.test.ts +4 -5
  38. package/client/agentRun.test.ts +307 -57
  39. package/client/agentRun.ts +94 -93
  40. package/client/localRuntimeAdapter.test.ts +332 -53
  41. package/client/localRuntimeAdapter.ts +250 -69
  42. package/client/localRuntimeDryRun.test.ts +23 -9
  43. package/client/localToolPolicy.test.ts +4 -4
  44. package/commandRegistry.ts +2 -2
  45. package/connectorRunArtifact.ts +0 -1
  46. package/database/server/db.ts +99 -0
  47. package/database/server/ensureDbOpen.ts +12 -0
  48. package/database/server/levelAuthorityStore.ts +20 -1
  49. package/database/server/memoryAuthorityStore.ts +133 -0
  50. package/database/server/serverStoreFactory.ts +118 -0
  51. package/dialogCommands.ts +572 -0
  52. package/dialogInternalCommandEntries.ts +4 -0
  53. package/machineWsRunDispatch.ts +76 -39
  54. package/offlineMarxistsAgentCommand.ts +6 -4
  55. package/package.json +11 -9
  56. package/scriptCommandEntries.ts +0 -2
  57. package/tui/readlineWorkspace.ts +69 -0
  58. package/tui/session.ts +176 -1
  59. package/agent-runtime/taskWorkspace.ts +0 -193
  60. package/agent-runtime/workspaceSession.ts +0 -76
  61. package/cli/client/taskWorktree.ts +0 -8
  62. package/cli/client/workspaceSession.ts +0 -11
  63. package/client/taskWorktree.ts +0 -8
  64. package/client/workspaceSession.test.ts +0 -57
  65. package/client/workspaceSession.ts +0 -11
@@ -6,7 +6,8 @@ import {
6
6
  findServerPlatformTools,
7
7
  runAgentTurn,
8
8
  } from "./agentRun";
9
- import { NOLO_PROJECT_MANAGER_AGENT_KEY } from "../agentAliases";
9
+ import { BUILTIN_NOLO_AGENT_KEY } from "./localRuntimeAdapter";
10
+ import { MIMO_MONTH_AGENT_KEY, NOLO_PROJECT_MANAGER_AGENT_KEY } from "../agentAliases";
10
11
 
11
12
  class CaptureOutput extends Writable {
12
13
  chunks: string[] = [];
@@ -34,11 +35,11 @@ describe("cli agent run client", () => {
34
35
  });
35
36
 
36
37
  test("identifies server platform tools that local runtime cannot provide", () => {
37
- expect(findServerPlatformTools(["readWorkspaceFile", "queryTableRows", "updateTableRow"])).toEqual([
38
+ expect(findServerPlatformTools(["readFile", "queryTableRows", "updateTableRow"])).toEqual([
38
39
  "queryTableRows",
39
40
  "updateTableRow",
40
41
  ]);
41
- expect(findServerPlatformTools(["readWorkspaceFile", "execShell"])).toEqual([]);
42
+ expect(findServerPlatformTools(["readFile", "execShell"])).toEqual([]);
42
43
  });
43
44
 
44
45
  test("calls the Nolo HTTP API directly when AUTH_TOKEN is present", async () => {
@@ -93,6 +94,34 @@ describe("cli agent run client", () => {
93
94
  expect(output.text()).not.toContain("tokens=");
94
95
  });
95
96
 
97
+ test("prints agent run error details from failed HTTP responses", async () => {
98
+ const output = new CaptureOutput();
99
+
100
+ const result = await runAgentTurn({
101
+ agentName: "win-codex",
102
+ agentKey: "agent-win-codex",
103
+ serverUrl: "https://nolo.chat",
104
+ message: "restart connector",
105
+ scriptDir: "C:/missing/scripts",
106
+ env: { AUTH_TOKEN: "token-123" },
107
+ output,
108
+ fetchImpl: async () =>
109
+ Response.json({
110
+ error: "Connector execution failed",
111
+ reason: "connector_execution_failed",
112
+ code: "connector_disconnected_mid_run",
113
+ message: "Connector disconnected for machine: machine-win",
114
+ }, { status: 502 }),
115
+ });
116
+
117
+ expect(result).toEqual({ exitCode: 1 });
118
+ expect(output.text()).toContain("[nolo] Agent request failed: HTTP 502");
119
+ expect(output.text()).toContain("Connector execution failed");
120
+ expect(output.text()).toContain("Connector disconnected for machine: machine-win");
121
+ expect(output.text()).toContain("code=connector_disconnected_mid_run");
122
+ expect(output.text()).toContain("reason=connector_execution_failed");
123
+ });
124
+
96
125
  test("prepends task-run context for HTTP agent runs", async () => {
97
126
  const output = new CaptureOutput();
98
127
  const requests: Array<{ body: any }> = [];
@@ -210,7 +239,6 @@ describe("cli agent run client", () => {
210
239
  parentDialogId: "dialog-2",
211
240
  background: true,
212
241
  noStream: true,
213
- maxToolRounds: 12,
214
242
  timeoutMs: 600000,
215
243
  scriptDir: "C:/missing/scripts",
216
244
  env: { AUTH_TOKEN: "token-123" },
@@ -230,7 +258,6 @@ describe("cli agent run client", () => {
230
258
  inheritedFromDialogKey: "dialog-user-1-dialog-2",
231
259
  parentDialogId: "dialog-2",
232
260
  background: true,
233
- maxRounds: 12,
234
261
  timeoutMs: 600000,
235
262
  stream: false,
236
263
  });
@@ -316,72 +343,75 @@ describe("cli agent run client", () => {
316
343
  expect(output.text()).toContain("frontend > local:polish notifications");
317
344
  });
318
345
 
319
- test("increases local max tool rounds when a forced local run exhausts them", async () => {
346
+ test("continues forced local tool loops until the provider returns final text", async () => {
320
347
  const output = new CaptureOutput();
321
348
  let completeCalls = 0;
349
+ let toolCalls = 0;
350
+
322
351
  const result = await runAgentTurn({
323
352
  agentName: "frontend",
324
353
  agentKey: "frontend-local",
325
354
  serverUrl: "https://nolo.chat",
326
- message: "polish notifications",
355
+ message: "make screenshots",
327
356
  scriptDir: "C:/missing/scripts",
328
- env: { AUTH_TOKEN: "token-123", NOLO_LOCAL_MAX_TOOL_ROUNDS_LIMIT: "1" },
357
+ env: { AUTH_TOKEN: "token-123" },
329
358
  output,
330
359
  runtimeMode: "local",
331
- maxToolRounds: 0,
332
360
  localRuntimeAdapter: {
333
361
  host: "cli",
334
- capabilities: ["local-provider", "local-persistence"],
362
+ capabilities: ["local-provider", "local-persistence", "local-tools"],
335
363
  loadAgentConfig: async (agentRef) => ({
336
364
  key: agentRef,
337
365
  name: "Frontend",
338
366
  prompt: "Fix UI",
339
367
  model: "fake-local",
368
+ toolNames: ["readFile"],
340
369
  }),
341
370
  loadDialogHistory: async () => [],
342
- saveTurn: async () => ({ dialogId: "dialog-local" }),
371
+ saveTurn: async () => ({ dialogId: "dialog-local-retry" }),
343
372
  resolveProvider: async () => ({
344
373
  model: "fake-local",
345
374
  complete: async () => {
346
375
  completeCalls += 1;
347
- return {
348
- content: "",
349
- model: "fake-local",
350
- tool_calls: [{
351
- id: `call-${completeCalls}`,
352
- type: "function",
353
- function: { name: "readWorkspaceFile", arguments: "{}" },
354
- }],
355
- };
376
+ if (completeCalls <= 3) {
377
+ return {
378
+ content: "",
379
+ model: "fake-local",
380
+ tool_calls: [{
381
+ id: `call-${completeCalls}`,
382
+ type: "function",
383
+ function: { name: "readFile", arguments: "{}" },
384
+ }],
385
+ };
386
+ }
387
+ return { content: "done after retry", model: "fake-local" };
356
388
  },
357
389
  }),
358
- executeTool: async () => ({ content: "ok" }),
359
- },
360
- fetchImpl: async () => {
361
- throw new Error("HTTP should not be called for forced local runs");
390
+ executeTool: async () => {
391
+ toolCalls += 1;
392
+ return { content: "ok" };
393
+ },
362
394
  },
363
395
  });
364
396
 
365
- expect(result).toMatchObject({ exitCode: 1 });
366
- expect(output.text()).toContain("Local agent used all 0 tool rounds; retrying with 1.");
367
- expect(output.text()).toContain("Local agent exceeded max tool rounds: 1");
397
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-local-retry" });
398
+ expect(toolCalls).toBe(3);
399
+ expect(output.text()).toContain("frontend > done after retry");
368
400
  });
369
401
 
370
- test("completes a forced local run after automatically increasing tool rounds", async () => {
402
+ test("prints compact local tool trace by default", async () => {
371
403
  const output = new CaptureOutput();
372
404
  let completeCalls = 0;
373
- let toolCalls = 0;
374
405
 
375
406
  const result = await runAgentTurn({
376
407
  agentName: "frontend",
377
408
  agentKey: "frontend-local",
378
409
  serverUrl: "https://nolo.chat",
379
- message: "make screenshots",
410
+ message: "inspect repo",
380
411
  scriptDir: "C:/missing/scripts",
381
412
  env: { AUTH_TOKEN: "token-123" },
382
413
  output,
383
414
  runtimeMode: "local",
384
- maxToolRounds: 1,
385
415
  localRuntimeAdapter: {
386
416
  host: "cli",
387
417
  capabilities: ["local-provider", "local-persistence", "local-tools"],
@@ -390,42 +420,39 @@ describe("cli agent run client", () => {
390
420
  name: "Frontend",
391
421
  prompt: "Fix UI",
392
422
  model: "fake-local",
393
- toolNames: ["readWorkspaceFile"],
423
+ toolNames: ["readFile"],
394
424
  }),
395
425
  loadDialogHistory: async () => [],
396
- saveTurn: async () => ({ dialogId: "dialog-local-retry" }),
426
+ saveTurn: async () => ({ dialogId: "dialog-local" }),
397
427
  resolveProvider: async () => ({
398
428
  model: "fake-local",
399
429
  complete: async () => {
400
430
  completeCalls += 1;
401
- if (completeCalls <= 3) {
431
+ if (completeCalls === 1) {
402
432
  return {
403
433
  content: "",
404
434
  model: "fake-local",
405
435
  tool_calls: [{
406
- id: `call-${completeCalls}`,
436
+ id: "call-read",
407
437
  type: "function",
408
- function: { name: "readWorkspaceFile", arguments: "{}" },
438
+ function: { name: "readFile", arguments: JSON.stringify({ path: "README.md" }) },
409
439
  }],
410
440
  };
411
441
  }
412
- return { content: "done after retry", model: "fake-local" };
442
+ return { content: "done", model: "fake-local" };
413
443
  },
414
444
  }),
415
- executeTool: async () => {
416
- toolCalls += 1;
417
- return { content: "ok" };
418
- },
445
+ executeTool: async () => ({ content: "clean" }),
419
446
  },
420
447
  });
421
448
 
422
- expect(result).toEqual({ exitCode: 0, dialogId: "dialog-local-retry" });
423
- expect(toolCalls).toBe(2);
424
- expect(output.text()).toContain("Local agent used all 1 tool rounds; retrying with 2.");
425
- expect(output.text()).toContain("frontend > done after retry");
449
+ expect(result).toMatchObject({ exitCode: 0, dialogId: "dialog-local" });
450
+ expect(output.text()).toContain("[nolo:tool] #1 -> readFile README.md");
451
+ expect(output.text()).toContain("[nolo:tool] #1 <- readFile");
452
+ expect(output.text()).toContain("1 line");
426
453
  });
427
454
 
428
- test("prints local tool trace when requested", async () => {
455
+ test("does not print local tool trace when disabled by env", async () => {
429
456
  const output = new CaptureOutput();
430
457
  let completeCalls = 0;
431
458
 
@@ -435,10 +462,9 @@ describe("cli agent run client", () => {
435
462
  serverUrl: "https://nolo.chat",
436
463
  message: "inspect repo",
437
464
  scriptDir: "C:/missing/scripts",
438
- env: { AUTH_TOKEN: "token-123" },
465
+ env: { AUTH_TOKEN: "token-123", NOLO_TRACE_TOOLS: "0" },
439
466
  output,
440
467
  runtimeMode: "local",
441
- traceTools: true,
442
468
  localRuntimeAdapter: {
443
469
  host: "cli",
444
470
  capabilities: ["local-provider", "local-persistence", "local-tools"],
@@ -447,10 +473,10 @@ describe("cli agent run client", () => {
447
473
  name: "Frontend",
448
474
  prompt: "Fix UI",
449
475
  model: "fake-local",
450
- toolNames: ["readWorkspaceFile"],
476
+ toolNames: ["readFile"],
451
477
  }),
452
478
  loadDialogHistory: async () => [],
453
- saveTurn: async () => ({ dialogId: "dialog-local" }),
479
+ saveTurn: async () => ({ dialogId: "dialog-local-no-trace" }),
454
480
  resolveProvider: async () => ({
455
481
  model: "fake-local",
456
482
  complete: async () => {
@@ -462,7 +488,7 @@ describe("cli agent run client", () => {
462
488
  tool_calls: [{
463
489
  id: "call-read",
464
490
  type: "function",
465
- function: { name: "readWorkspaceFile", arguments: JSON.stringify({ path: "README.md" }) },
491
+ function: { name: "readFile", arguments: JSON.stringify({ path: "README.md" }) },
466
492
  }],
467
493
  };
468
494
  }
@@ -473,9 +499,79 @@ describe("cli agent run client", () => {
473
499
  },
474
500
  });
475
501
 
476
- expect(result).toMatchObject({ exitCode: 0, dialogId: "dialog-local" });
477
- expect(output.text()).toContain("[nolo:tool] round 1 -> readWorkspaceFile (call-read)");
478
- expect(output.text()).toContain("[nolo:tool] round 1 <- readWorkspaceFile (call-read)");
502
+ expect(result).toMatchObject({ exitCode: 0, dialogId: "dialog-local-no-trace" });
503
+ expect(output.text()).not.toContain("[nolo:tool]");
504
+ });
505
+
506
+ test("prints local tool events as jsonl when requested", async () => {
507
+ const output = new CaptureOutput();
508
+ let completeCalls = 0;
509
+
510
+ const result = await runAgentTurn({
511
+ agentName: "frontend",
512
+ agentKey: "frontend-local",
513
+ serverUrl: "https://nolo.chat",
514
+ message: "inspect repo",
515
+ scriptDir: "C:/missing/scripts",
516
+ env: { AUTH_TOKEN: "token-123" },
517
+ output,
518
+ runtimeMode: "local",
519
+ eventsMode: "jsonl",
520
+ localRuntimeAdapter: {
521
+ host: "cli",
522
+ capabilities: ["local-provider", "local-persistence", "local-tools"],
523
+ loadAgentConfig: async (agentRef) => ({
524
+ key: agentRef,
525
+ name: "Frontend",
526
+ prompt: "Fix UI",
527
+ model: "fake-local",
528
+ toolNames: ["execShell"],
529
+ }),
530
+ loadDialogHistory: async () => [],
531
+ saveTurn: async () => ({ dialogId: "dialog-local-jsonl" }),
532
+ resolveProvider: async () => ({
533
+ model: "fake-local",
534
+ complete: async () => {
535
+ completeCalls += 1;
536
+ if (completeCalls === 1) {
537
+ return {
538
+ content: "",
539
+ model: "fake-local",
540
+ tool_calls: [{
541
+ id: "call-shell",
542
+ type: "function",
543
+ function: { name: "execShell", arguments: JSON.stringify({ command: "git status --short" }) },
544
+ }],
545
+ };
546
+ }
547
+ return { content: "done", model: "fake-local" };
548
+ },
549
+ }),
550
+ executeTool: async () => ({ content: "stdout:\nclean\n\nexitCode: 0", metadata: { exitCode: 0 } }),
551
+ },
552
+ });
553
+
554
+ expect(result).toMatchObject({ exitCode: 0, dialogId: "dialog-local-jsonl" });
555
+ const events = output.text()
556
+ .split(/\r?\n/)
557
+ .filter((line) => line.startsWith("{"))
558
+ .map((line) => JSON.parse(line))
559
+ .filter((event) => typeof event.type === "string" && event.type.startsWith("tool-"));
560
+ expect(events).toHaveLength(2);
561
+ expect(events[0]).toMatchObject({
562
+ schemaVersion: 1,
563
+ type: "tool-call",
564
+ round: 1,
565
+ tool: "execShell",
566
+ argsPreview: "git status --short",
567
+ });
568
+ expect(events[1]).toMatchObject({
569
+ schemaVersion: 1,
570
+ type: "tool-result",
571
+ round: 1,
572
+ tool: "execShell",
573
+ summary: expect.stringContaining("exit=0"),
574
+ });
479
575
  });
480
576
 
481
577
  test("passes local tool evidence to task-run writeback", async () => {
@@ -507,7 +603,7 @@ describe("cli agent run client", () => {
507
603
  name: "Frontend",
508
604
  prompt: "Fix UI",
509
605
  model: "fake-local",
510
- toolNames: ["readWorkspaceFile"],
606
+ toolNames: ["readFile"],
511
607
  }),
512
608
  loadDialogHistory: async () => [],
513
609
  saveTurn: async () => ({ dialogId: "dialog-local" }),
@@ -522,7 +618,7 @@ describe("cli agent run client", () => {
522
618
  tool_calls: [{
523
619
  id: "call-read",
524
620
  type: "function",
525
- function: { name: "readWorkspaceFile", arguments: JSON.stringify({ path: "README.md" }) },
621
+ function: { name: "readFile", arguments: JSON.stringify({ path: "README.md" }) },
526
622
  }],
527
623
  };
528
624
  }
@@ -536,7 +632,7 @@ describe("cli agent run client", () => {
536
632
  expect(result).toMatchObject({ exitCode: 0, dialogId: "dialog-local" });
537
633
  expect(writebacks).toHaveLength(1);
538
634
  expect(writebacks[0].toolEvidence).toEqual({
539
- toolsUsed: ["readWorkspaceFile"],
635
+ toolsUsed: ["readFile"],
540
636
  summary: {
541
637
  callCount: 1,
542
638
  resultCount: 1,
@@ -791,6 +887,160 @@ describe("cli agent run client", () => {
791
887
  expect(output.text()).toContain("pm > server ok");
792
888
  });
793
889
 
890
+ test("auto mode does not skip monthly Mimo when a local shell cwd is already bound", async () => {
891
+ const output = new CaptureOutput();
892
+ const httpCalls: string[] = [];
893
+ let providerCalled = false;
894
+
895
+ const result = await runAgentTurn({
896
+ agentName: MIMO_MONTH_AGENT_KEY,
897
+ agentKey: MIMO_MONTH_AGENT_KEY,
898
+ serverUrl: "https://us.nolo.chat",
899
+ message: "fix tests and commit",
900
+ scriptDir: "C:/missing/scripts",
901
+ env: {
902
+ AUTH_TOKEN: "token-123",
903
+ },
904
+ output,
905
+ runtimeMode: "auto",
906
+ localRuntimeCwd: "/repo/.worktrees/nolo-agent-mimo",
907
+ localRuntimeAdapter: {
908
+ host: "cli",
909
+ capabilities: ["leveldb-agent-config", "local-provider", "local-tools"],
910
+ loadAgentConfig: async (agentRef) => ({
911
+ key: agentRef,
912
+ name: "包月 Mimo",
913
+ prompt: "Implement backend tasks.",
914
+ provider: "custom",
915
+ model: "mimo-v2.5-pro",
916
+ toolNames: ["queryTableRows"],
917
+ }),
918
+ loadDialogHistory: async () => [],
919
+ saveTurn: async () => ({ dialogId: "dialog-mimo-local" }),
920
+ resolveProvider: async () => ({
921
+ model: "mimo-v2.5-pro",
922
+ complete: async () => {
923
+ providerCalled = true;
924
+ return { content: "local mimo ok", model: "mimo-v2.5-pro" };
925
+ },
926
+ }),
927
+ executeTool: async () => ({ content: "ok" }),
928
+ },
929
+ fetchImpl: async (url) => {
930
+ httpCalls.push(String(url));
931
+ return Response.json({ content: "server" });
932
+ },
933
+ });
934
+
935
+ expect(providerCalled).toBe(true);
936
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-mimo-local" });
937
+ expect(httpCalls).toEqual([]);
938
+ expect(output.text()).not.toContain("skipping local runtime");
939
+ expect(output.text()).toContain(`${MIMO_MONTH_AGENT_KEY} -> working locally`);
940
+ });
941
+
942
+ test("auto mode skips local runtime for machine-bound localhost custom providers", async () => {
943
+ const output = new CaptureOutput();
944
+ const httpCalls: Array<{ url: string; body: any }> = [];
945
+
946
+ const result = await runAgentTurn({
947
+ agentName: "win-qwen",
948
+ agentKey: "agent-win-qwen",
949
+ serverUrl: "https://nolo.chat",
950
+ message: "run on the bound Windows machine",
951
+ scriptDir: "C:/missing/scripts",
952
+ env: { AUTH_TOKEN: "token-123", OPENAI_API_KEY: "local-provider-present" },
953
+ output,
954
+ runtimeMode: "auto",
955
+ localRuntimeAdapter: {
956
+ host: "cli",
957
+ capabilities: ["leveldb-agent-config", "local-provider", "local-tools"],
958
+ loadAgentConfig: async (agentRef) => ({
959
+ key: agentRef,
960
+ name: "Win Qwen",
961
+ prompt: "Windows local model",
962
+ provider: "custom",
963
+ model: "qwen",
964
+ customProviderUrl: "http://127.0.0.1:8080/v1/chat/completions",
965
+ runtimeBinding: {
966
+ machineId: "machine-win",
967
+ ownerUserId: "user-1",
968
+ connectorSurface: "cli",
969
+ },
970
+ }),
971
+ loadDialogHistory: async () => [],
972
+ saveTurn: async () => {
973
+ throw new Error("local runtime should be skipped");
974
+ },
975
+ resolveProvider: async () => {
976
+ throw new Error("local provider should be skipped");
977
+ },
978
+ executeTool: async () => {
979
+ throw new Error("local tools should be skipped");
980
+ },
981
+ },
982
+ fetchImpl: async (url, init) => {
983
+ httpCalls.push({ url: String(url), body: JSON.parse(String(init?.body)) });
984
+ return Response.json({ content: "server connector ok", dialogId: "dialog-win" });
985
+ },
986
+ });
987
+
988
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-win" });
989
+ expect(httpCalls).toHaveLength(1);
990
+ expect(httpCalls[0]?.body.agentKey).toBe("agent-win-qwen");
991
+ expect(output.text()).toContain("auto runtime: skipping local runtime");
992
+ expect(output.text()).toContain("machine-bound localhost custom provider");
993
+ expect(output.text()).not.toContain("win-qwen -> working locally");
994
+ });
995
+
996
+ test("auto mode keeps builtin nolo on local runtime so CLI workspace tools are available", async () => {
997
+ const output = new CaptureOutput();
998
+ const httpCalls: string[] = [];
999
+ let providerCalled = false;
1000
+
1001
+ const result = await runAgentTurn({
1002
+ agentName: "nolo",
1003
+ agentKey: BUILTIN_NOLO_AGENT_KEY,
1004
+ serverUrl: "https://nolo.chat",
1005
+ message: "帮我总结最近 10 个对话",
1006
+ scriptDir: "C:/missing/scripts",
1007
+ env: { AUTH_TOKEN: "token-123" },
1008
+ output,
1009
+ runtimeMode: "auto",
1010
+ localRuntimeAdapter: {
1011
+ host: "cli",
1012
+ capabilities: ["leveldb-agent-config", "local-provider", "local-tools"],
1013
+ loadAgentConfig: async () => ({
1014
+ key: BUILTIN_NOLO_AGENT_KEY,
1015
+ name: "nolo",
1016
+ prompt: "Use workspace tools.",
1017
+ model: "fake-local",
1018
+ toolNames: ["queryTableRows", "addTableRow", "updateTableRow"],
1019
+ }),
1020
+ loadDialogHistory: async () => [],
1021
+ saveTurn: async () => ({ dialogId: "dialog-builtin-nolo-local" }),
1022
+ resolveProvider: async () => ({
1023
+ model: "fake-local",
1024
+ complete: async () => {
1025
+ providerCalled = true;
1026
+ return { content: "local nolo with tools", model: "fake-local" };
1027
+ },
1028
+ }),
1029
+ executeTool: async () => ({ content: "[]" }),
1030
+ },
1031
+ fetchImpl: async (url) => {
1032
+ httpCalls.push(String(url));
1033
+ return Response.json({ content: "server" });
1034
+ },
1035
+ });
1036
+
1037
+ expect(providerCalled).toBe(true);
1038
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-builtin-nolo-local" });
1039
+ expect(httpCalls).toEqual([]);
1040
+ expect(output.text()).not.toContain("skipping local runtime");
1041
+ expect(output.text()).toContain("nolo -> working locally");
1042
+ });
1043
+
794
1044
  test("auto mode skips local runtime for server platform tools declared by runtime policy", async () => {
795
1045
  const output = new CaptureOutput();
796
1046
  const httpCalls: Array<{ url: string; body: any }> = [];