nolo-cli 0.1.22 → 0.1.26

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 (128) hide show
  1. package/README.md +25 -3
  2. package/agent-runtime/agentRecordConfig.ts +4 -0
  3. package/agent-runtime/hostAdapter.ts +2 -0
  4. package/agent-runtime/hybridRecordStore.ts +30 -9
  5. package/agent-runtime/index.ts +7 -0
  6. package/agent-runtime/localLoop.ts +14 -4
  7. package/agent-runtime/localToolPolicy.ts +22 -10
  8. package/agent-runtime/localWorkspaceTools.ts +140 -374
  9. package/agent-runtime/platformChatProvider.ts +3 -0
  10. package/agent-runtime/runtimeToolPolicy.ts +133 -0
  11. package/agent-runtime/types.ts +47 -0
  12. package/agentAliases.ts +1 -1
  13. package/agentCommandSupport.ts +77 -0
  14. package/agentInternalCommandEntries.ts +34 -0
  15. package/agentListCommands.ts +123 -0
  16. package/agentListHelpers.ts +187 -0
  17. package/agentMachineCommands.ts +402 -0
  18. package/agentRecordCommands.ts +141 -0
  19. package/agentRecordHelpers.ts +395 -0
  20. package/agentRunCommand.ts +75 -3
  21. package/agentRuntimeCommands.ts +7 -773
  22. package/ai/agent/_executeModel.ts +29 -3
  23. package/ai/agent/buildSystemPrompt.ts +17 -0
  24. package/ai/agent/cliChatClient.ts +38 -4
  25. package/ai/agent/cliExecutor.ts +304 -25
  26. package/ai/agent/createAgentSchema.ts +2 -2
  27. package/ai/agent/guidedCreation/assistant.ts +82 -0
  28. package/ai/agent/guidedCreation/capabilities.ts +84 -0
  29. package/ai/agent/guidedCreation/draft.ts +62 -0
  30. package/ai/agent/guidedCreation/types.ts +38 -0
  31. package/ai/agent/machineRunPermissions.ts +21 -3
  32. package/ai/agent/runtimeGuidance.ts +1 -1
  33. package/ai/agent/server/fetchPublicAgents.ts +6 -1
  34. package/ai/agent/streamAgentChatTurn.ts +593 -36
  35. package/ai/agent/web/useGuidedAgentCreation.ts +217 -0
  36. package/ai/ai.locale.ts +14 -2
  37. package/ai/chat/fetchUtils.native.ts +2 -0
  38. package/ai/chat/fetchUtils.ts +2 -0
  39. package/ai/chat/sendOpenAICompletionsRequest.ts +82 -1
  40. package/ai/chat/sendOpenAIResponseRequest.ts +65 -0
  41. package/ai/llm/getPricing.ts +14 -0
  42. package/ai/llm/kimi.ts +4 -1
  43. package/ai/llm/modelAvatar.ts +1 -0
  44. package/ai/llm/providers.ts +4 -1
  45. package/ai/llm/reasoningModels.ts +1 -0
  46. package/ai/taskRun/taskRunProtocol.ts +25 -10
  47. package/ai/token/modelUsageQuery.ts +17 -0
  48. package/ai/tools/agent/agentTools.ts +38 -0
  49. package/ai/tools/agent/prepareAgentDraftTool.ts +159 -0
  50. package/ai/tools/agent/startAgentDialogTool.ts +53 -0
  51. package/ai/tools/checkEnvTool.ts +52 -20
  52. package/ai/tools/convertMarxistsBookTool.ts +103 -0
  53. package/ai/tools/{execBashTool.ts → execShellTool.ts} +44 -161
  54. package/ai/tools/index.ts +24 -14
  55. package/ai/tools/marxistsOfflineBook.ts +253 -0
  56. package/ai/tools/modelUsageTools.ts +9 -2
  57. package/ai/tools/readTool.ts +31 -0
  58. package/ai/tools/toolApiClient.ts +35 -1
  59. package/ai/tools/toolNameAliases.ts +13 -0
  60. package/cliCommandAdapters.ts +28 -0
  61. package/cliCommandDispatch.ts +66 -0
  62. package/cliCommandFactories.ts +34 -0
  63. package/cliCommandTypes.ts +49 -0
  64. package/cliEnvHelpers.ts +44 -0
  65. package/client/agentRun.test.ts +330 -24
  66. package/client/agentRun.ts +117 -35
  67. package/client/hybridRecordStore.test.ts +42 -0
  68. package/client/hybridRecordStore.ts +11 -0
  69. package/client/localRuntimeAdapter.test.ts +191 -21
  70. package/client/localRuntimeAdapter.ts +99 -10
  71. package/client/localRuntimeDryRun.test.ts +5 -2
  72. package/client/taskRunPrompt.ts +1 -0
  73. package/commandRegistry.ts +11 -61
  74. package/connector-experimental/capabilities.ts +1 -0
  75. package/database/actions/common.ts +4 -3
  76. package/database/config.ts +19 -0
  77. package/database/server/coreDataProxy.ts +43 -26
  78. package/index.ts +54 -191
  79. package/internalCommandEntries.ts +12 -0
  80. package/machineCommands.ts +688 -64
  81. package/offlineMarxistsAgentCommand.ts +314 -0
  82. package/package.json +22 -2
  83. package/render/canvas/canvasEditContext.ts +127 -0
  84. package/render/canvas/canvasRuntime.ts +57 -0
  85. package/render/canvas/canvasSnapshotParser.ts +76 -0
  86. package/render/canvas/canvasTree.ts +308 -0
  87. package/render/canvas/types.ts +46 -0
  88. package/render/layout/deleteBehavior.ts +52 -0
  89. package/render/layout/mainLayoutSidebar.ts +17 -0
  90. package/render/layout/mainLayoutViewMode.ts +56 -0
  91. package/render/layout/topbarUtils.ts +87 -0
  92. package/render/layout/useDevReloadPending.ts +30 -0
  93. package/render/page/createPageAction.ts +183 -0
  94. package/render/page/docSlice.ts +468 -0
  95. package/render/page/server/createPage.ts +174 -0
  96. package/render/page/server/handleCreatePage.ts +91 -0
  97. package/render/page/server/index.ts +4 -0
  98. package/render/page/types.ts +17 -0
  99. package/render/page/useKeyboardSave.ts +48 -0
  100. package/render/styles/zIndex.ts +12 -0
  101. package/render/surf/WeatherIconStyles.ts +17 -0
  102. package/render/surf/color.ts +9 -0
  103. package/render/surf/config.ts +46 -0
  104. package/render/surf/screens/style.ts +1 -0
  105. package/render/surf/styles/ToggleButtonStyles.ts +8 -0
  106. package/render/surf/utils/groupedWeatherData.ts +32 -0
  107. package/render/surf/weatherUtils.ts +50 -0
  108. package/render/table/activityColumns.ts +6 -0
  109. package/render/table/createTableAction.ts +270 -0
  110. package/render/table/deleteTableAction.ts +129 -0
  111. package/render/table/fetchAndCacheTableRows.ts +174 -0
  112. package/render/table/tableSlice.ts +1106 -0
  113. package/render/table/tableView.ts +289 -0
  114. package/render/table/toolValueUtils.ts +363 -0
  115. package/render/table/types.ts +252 -0
  116. package/render/table/useCreateTable.ts +72 -0
  117. package/render/table/useTable.ts +61 -0
  118. package/render/table/utils/tableSerialization.ts +50 -0
  119. package/render/web/elements/artifactPreviewCode.ts +43 -0
  120. package/render/web/elements/artifactRuntimePreload.ts +52 -0
  121. package/render/web/elements/codeBlockAutoPreview.ts +10 -0
  122. package/render/web/elements/mermaidPreview.ts +21 -0
  123. package/render/web/ui/useInlineEdit.ts +135 -0
  124. package/runtimeDoctorCommands.ts +118 -0
  125. package/scriptCommandEntries.ts +71 -0
  126. package/systemInternalCommandEntries.ts +71 -0
  127. package/tableCommands.ts +42 -5
  128. package/workflowInternalCommandEntries.ts +11 -0
@@ -45,7 +45,7 @@ describe("cli agent run client", () => {
45
45
  "queryTableRows",
46
46
  "updateTableRow",
47
47
  ]);
48
- expect(findServerPlatformTools(["readWorkspaceFile", "gitStatus"])).toEqual([]);
48
+ expect(findServerPlatformTools(["readWorkspaceFile", "execShell"])).toEqual([]);
49
49
  });
50
50
 
51
51
  test("calls the Nolo HTTP API directly when AUTH_TOKEN is present", async () => {
@@ -84,15 +84,15 @@ describe("cli agent run client", () => {
84
84
  agentKey: "agent-pub-test",
85
85
  userInput: "hello",
86
86
  continueDialogId: "dialog-existing",
87
- runtimeContext: {
88
- surface: "cli",
89
- host: "terminal",
90
- runtime: "bun",
91
- entrypoint: "nolo-cli",
92
- capabilities: ["text-io", "streaming", "slash-commands"],
93
- },
94
- stream: true,
95
- },
87
+ runtimeContext: {
88
+ surface: "cli",
89
+ host: "terminal",
90
+ runtime: "bun",
91
+ entrypoint: "nolo-cli",
92
+ capabilities: ["text-io", "streaming", "slash-commands"],
93
+ },
94
+ stream: true,
95
+ },
96
96
  },
97
97
  ]);
98
98
  expect(result).toEqual({ exitCode: 0, dialogId: "dialog-1" });
@@ -132,6 +132,12 @@ describe("cli agent run client", () => {
132
132
  expect(requests[0]?.body.userInput).toContain("rowDbKey: row-b2e06f801f-01TASK");
133
133
  expect(requests[0]?.body.userInput).toContain("workItemId: frontend-filter");
134
134
  expect(requests[0]?.body.userInput).toContain("User task:\nFix the filter UI");
135
+ expect(requests[0]?.body.runtimeContext.taskRun).toEqual({
136
+ rowDbKey: "row-b2e06f801f-01TASK",
137
+ taskRunId: "taskrun-1",
138
+ workItemId: "frontend-filter",
139
+ artifactIds: ["artifact-1"],
140
+ });
135
141
  });
136
142
 
137
143
  test("records task-run completion after an HTTP agent run returns a dialog", async () => {
@@ -243,6 +249,42 @@ describe("cli agent run client", () => {
243
249
  expect(result).toEqual({ exitCode: 0, dialogId: "dialog-1" });
244
250
  });
245
251
 
252
+ test("records background task-run handoff as running instead of completed", async () => {
253
+ const output = new CaptureOutput();
254
+ const writebacks: any[] = [];
255
+
256
+ const result = await runAgentTurn({
257
+ agentName: "frontend-implementer",
258
+ agentKey: "agent-frontend",
259
+ serverUrl: "https://nolo.chat",
260
+ message: "Fix settings style FOUC",
261
+ scriptDir: "C:/missing/scripts",
262
+ env: { AUTH_TOKEN: "token-123" },
263
+ runtimeMode: "server",
264
+ background: true,
265
+ noStream: true,
266
+ taskRunContext: {
267
+ rowDbKey: "row-b2e06f801f-01TASK",
268
+ workItemId: "self",
269
+ },
270
+ taskRunRecorder: async (args) => {
271
+ writebacks.push(args);
272
+ },
273
+ output,
274
+ scriptPathExists: () => false,
275
+ fetchImpl: async () => {
276
+ return Response.json({ dialogId: "dialog-bg", status: "pending" }, { status: 202 });
277
+ },
278
+ });
279
+
280
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-bg" });
281
+ expect(writebacks).toHaveLength(1);
282
+ expect(writebacks[0]).toMatchObject({
283
+ status: "running",
284
+ dialogId: "dialog-bg",
285
+ });
286
+ });
287
+
246
288
  test("runs forced local turns through the injected runtime adapter without HTTP", async () => {
247
289
  const output = new CaptureOutput();
248
290
  const result = await runAgentTurn({
@@ -421,7 +463,7 @@ describe("cli agent run client", () => {
421
463
  name: "Frontend",
422
464
  prompt: "Fix UI",
423
465
  model: "fake-local",
424
- toolNames: ["gitStatus"],
466
+ toolNames: ["readWorkspaceFile"],
425
467
  }),
426
468
  loadDialogHistory: async () => [],
427
469
  saveTurn: async () => ({ dialogId: "dialog-local" }),
@@ -434,9 +476,9 @@ describe("cli agent run client", () => {
434
476
  content: "",
435
477
  model: "fake-local",
436
478
  tool_calls: [{
437
- id: "call-status",
479
+ id: "call-read",
438
480
  type: "function",
439
- function: { name: "gitStatus", arguments: "{}" },
481
+ function: { name: "readWorkspaceFile", arguments: JSON.stringify({ path: "README.md" }) },
440
482
  }],
441
483
  };
442
484
  }
@@ -449,8 +491,8 @@ describe("cli agent run client", () => {
449
491
  });
450
492
 
451
493
  expect(result).toMatchObject({ exitCode: 0, dialogId: "dialog-local" });
452
- expect(output.text()).toContain("[nolo:tool] round 1 -> gitStatus (call-status)");
453
- expect(output.text()).toContain("[nolo:tool] round 1 <- gitStatus (call-status)");
494
+ expect(output.text()).toContain("[nolo:tool] round 1 -> readWorkspaceFile (call-read)");
495
+ expect(output.text()).toContain("[nolo:tool] round 1 <- readWorkspaceFile (call-read)");
454
496
  });
455
497
 
456
498
  test("passes local tool evidence to task-run writeback", async () => {
@@ -482,7 +524,7 @@ describe("cli agent run client", () => {
482
524
  name: "Frontend",
483
525
  prompt: "Fix UI",
484
526
  model: "fake-local",
485
- toolNames: ["gitStatus"],
527
+ toolNames: ["readWorkspaceFile"],
486
528
  }),
487
529
  loadDialogHistory: async () => [],
488
530
  saveTurn: async () => ({ dialogId: "dialog-local" }),
@@ -495,9 +537,9 @@ describe("cli agent run client", () => {
495
537
  content: "",
496
538
  model: "fake-local",
497
539
  tool_calls: [{
498
- id: "call-status",
540
+ id: "call-read",
499
541
  type: "function",
500
- function: { name: "gitStatus", arguments: "{}" },
542
+ function: { name: "readWorkspaceFile", arguments: JSON.stringify({ path: "README.md" }) },
501
543
  }],
502
544
  };
503
545
  }
@@ -512,7 +554,7 @@ describe("cli agent run client", () => {
512
554
  expect(result).toMatchObject({ exitCode: 0, dialogId: "dialog-local" });
513
555
  expect(writebacks).toHaveLength(1);
514
556
  expect(writebacks[0].toolEvidence).toEqual({
515
- toolsUsed: ["gitStatus"],
557
+ toolsUsed: ["readWorkspaceFile"],
516
558
  summary: {
517
559
  callCount: 1,
518
560
  resultCount: 1,
@@ -522,6 +564,58 @@ describe("cli agent run client", () => {
522
564
  });
523
565
  });
524
566
 
567
+ test("records local background task-run handoff as running instead of completed", async () => {
568
+ const output = new CaptureOutput();
569
+ const writebacks: any[] = [];
570
+
571
+ const result = await runAgentTurn({
572
+ agentName: "frontend",
573
+ agentKey: "frontend-local",
574
+ serverUrl: "https://nolo.chat",
575
+ message: "inspect repo in background",
576
+ scriptDir: "C:/missing/scripts",
577
+ env: { AUTH_TOKEN: "token-123" },
578
+ output,
579
+ runtimeMode: "local",
580
+ background: true,
581
+ taskRunContext: {
582
+ rowDbKey: "row-b2e06f801f-01TASK",
583
+ workItemId: "self",
584
+ },
585
+ taskRunRecorder: async (args) => {
586
+ writebacks.push(args);
587
+ },
588
+ localRuntimeAdapter: {
589
+ host: "cli",
590
+ capabilities: ["local-provider", "local-persistence"],
591
+ loadAgentConfig: async (agentRef) => ({
592
+ key: agentRef,
593
+ name: "Frontend",
594
+ prompt: "Fix UI",
595
+ model: "fake-local",
596
+ }),
597
+ loadDialogHistory: async () => [],
598
+ saveTurn: async () => ({ dialogId: "dialog-local-bg" }),
599
+ resolveProvider: async () => ({
600
+ model: "fake-local",
601
+ complete: async () => ({ content: "accepted locally", model: "fake-local" }),
602
+ }),
603
+ executeTool: async () => {
604
+ throw new Error("no tools expected");
605
+ },
606
+ },
607
+ scriptPathExists: () => false,
608
+ });
609
+
610
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-local-bg" });
611
+ expect(writebacks).toHaveLength(1);
612
+ expect(writebacks[0]).toMatchObject({
613
+ status: "running",
614
+ dialogId: "dialog-local-bg",
615
+ });
616
+ expect(writebacks[0].resultSummary).toBeUndefined();
617
+ });
618
+
525
619
  test("auto mode prefers a working local runtime before HTTP", async () => {
526
620
  const output = new CaptureOutput();
527
621
  const httpCalls: string[] = [];
@@ -566,6 +660,110 @@ describe("cli agent run client", () => {
566
660
  expect(output.text()).toContain("frontend -> working locally");
567
661
  });
568
662
 
663
+ test("auto mode refreshes missing local agent config and retries local once before HTTP", async () => {
664
+ const output = new CaptureOutput();
665
+ const httpCalls: string[] = [];
666
+ let loadAgentConfigCalls = 0;
667
+ let localCompletions = 0;
668
+
669
+ const result = await runAgentTurn({
670
+ agentName: "frontend",
671
+ agentKey: "frontend-local",
672
+ serverUrl: "https://nolo.chat",
673
+ message: "polish notifications",
674
+ scriptDir: "C:/missing/scripts",
675
+ env: { AUTH_TOKEN: "token-123" },
676
+ output,
677
+ runtimeMode: "auto",
678
+ localRuntimeAdapter: {
679
+ host: "cli",
680
+ capabilities: ["leveldb-agent-config", "local-provider", "leveldb-persistence"],
681
+ loadAgentConfig: async (agentRef) => {
682
+ loadAgentConfigCalls += 1;
683
+ if (loadAgentConfigCalls < 3) return null;
684
+ return {
685
+ key: agentRef,
686
+ name: "Frontend",
687
+ prompt: "Fix UI",
688
+ model: "fake-local",
689
+ };
690
+ },
691
+ loadDialogHistory: async () => [],
692
+ saveTurn: async () => ({ dialogId: "dialog-auto-local-retried" }),
693
+ resolveProvider: async () => ({
694
+ model: "fake-local",
695
+ complete: async () => {
696
+ localCompletions += 1;
697
+ return { content: "auto local after refresh", model: "fake-local" };
698
+ },
699
+ }),
700
+ executeTool: async () => {
701
+ throw new Error("no tools expected");
702
+ },
703
+ },
704
+ scriptPathExists: () => false,
705
+ fetchImpl: async (url) => {
706
+ httpCalls.push(String(url));
707
+ return Response.json({ content: "server" });
708
+ },
709
+ });
710
+
711
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-auto-local-retried" });
712
+ expect(loadAgentConfigCalls).toBe(4);
713
+ expect(localCompletions).toBe(1);
714
+ expect(httpCalls).toEqual([]);
715
+ expect(output.text()).toContain("refreshing from the configured server and retrying local once");
716
+ expect(output.text().match(/working locally/g)?.length).toBe(2);
717
+ });
718
+
719
+ test("auto mode falls back to HTTP when local agent config is still missing after one refresh retry", async () => {
720
+ const output = new CaptureOutput();
721
+ const httpCalls: Array<{ url: string; body: any }> = [];
722
+ let loadAgentConfigCalls = 0;
723
+
724
+ const result = await runAgentTurn({
725
+ agentName: "frontend",
726
+ agentKey: "frontend-local",
727
+ serverUrl: "https://nolo.chat",
728
+ message: "polish notifications",
729
+ scriptDir: "C:/missing/scripts",
730
+ env: { AUTH_TOKEN: "token-123" },
731
+ output,
732
+ runtimeMode: "auto",
733
+ localRuntimeAdapter: {
734
+ host: "cli",
735
+ capabilities: ["leveldb-agent-config", "local-provider", "leveldb-persistence"],
736
+ loadAgentConfig: async () => {
737
+ loadAgentConfigCalls += 1;
738
+ return null;
739
+ },
740
+ loadDialogHistory: async () => [],
741
+ saveTurn: async () => {
742
+ throw new Error("local runtime should not save when config is missing");
743
+ },
744
+ resolveProvider: async () => {
745
+ throw new Error("local provider should not run when config is missing");
746
+ },
747
+ executeTool: async () => {
748
+ throw new Error("no tools expected");
749
+ },
750
+ },
751
+ scriptPathExists: () => false,
752
+ fetchImpl: async (url, init) => {
753
+ httpCalls.push({ url: String(url), body: JSON.parse(String(init?.body)) });
754
+ return Response.json({ content: "server fallback", dialogId: "dialog-server-fallback" });
755
+ },
756
+ });
757
+
758
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-server-fallback" });
759
+ expect(loadAgentConfigCalls).toBe(3);
760
+ expect(httpCalls).toHaveLength(1);
761
+ expect(httpCalls[0]?.body.agentKey).toBe("frontend-local");
762
+ expect(output.text()).toContain("refreshing from the configured server and retrying local once");
763
+ expect(output.text()).toContain("frontend -> working");
764
+ expect(output.text()).toContain("frontend > server fallback");
765
+ });
766
+
569
767
  test("auto mode skips local runtime for agents that declare server platform tools", async () => {
570
768
  const output = new CaptureOutput();
571
769
  const httpCalls: Array<{ url: string; body: any }> = [];
@@ -616,6 +814,58 @@ describe("cli agent run client", () => {
616
814
  expect(output.text()).toContain("pm > server ok");
617
815
  });
618
816
 
817
+ test("auto mode skips local runtime for server platform tools declared by runtime policy", async () => {
818
+ const output = new CaptureOutput();
819
+ const httpCalls: Array<{ url: string; body: any }> = [];
820
+
821
+ const result = await runAgentTurn({
822
+ agentName: "pm",
823
+ agentKey: "agent-policy-platform-tools",
824
+ serverUrl: "https://nolo.chat",
825
+ message: "query task rows",
826
+ scriptDir: "C:/missing/scripts",
827
+ env: { AUTH_TOKEN: "token-123" },
828
+ output,
829
+ runtimeMode: "auto",
830
+ localRuntimeAdapter: {
831
+ host: "cli",
832
+ capabilities: ["leveldb-agent-config", "local-provider", "local-tools"],
833
+ loadAgentConfig: async (agentRef) => ({
834
+ key: agentRef,
835
+ name: "PM",
836
+ prompt: "Manage task rows",
837
+ model: "fake-local",
838
+ runtimeToolPolicy: {
839
+ version: 1,
840
+ agentTools: ["queryTableRows", "taskRun"],
841
+ runtimeTools: ["execShell"],
842
+ },
843
+ }),
844
+ loadDialogHistory: async () => [],
845
+ saveTurn: async () => {
846
+ throw new Error("local runtime should be skipped");
847
+ },
848
+ resolveProvider: async () => {
849
+ throw new Error("local provider should be skipped");
850
+ },
851
+ executeTool: async () => {
852
+ throw new Error("local tools should be skipped");
853
+ },
854
+ },
855
+ scriptPathExists: () => false,
856
+ fetchImpl: async (url, init) => {
857
+ httpCalls.push({ url: String(url), body: JSON.parse(String(init?.body)) });
858
+ return Response.json({ content: "server ok", dialogId: "dialog-server-policy" });
859
+ },
860
+ });
861
+
862
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-server-policy" });
863
+ expect(httpCalls).toHaveLength(1);
864
+ expect(httpCalls[0]?.body.agentKey).toBe("agent-policy-platform-tools");
865
+ expect(output.text()).toContain("auto runtime: skipping local runtime");
866
+ expect(output.text()).toContain("queryTableRows, taskRun");
867
+ });
868
+
619
869
  test("auto mode skips known platform agents when local config cannot be read", async () => {
620
870
  const output = new CaptureOutput();
621
871
  const httpCalls: Array<{ url: string; body: any }> = [];
@@ -733,11 +983,67 @@ describe("cli agent run client", () => {
733
983
  });
734
984
 
735
985
  expect(result).toEqual({ exitCode: 0, dialogId: "dialog-stream" });
736
- expect(output.text()).toContain("nolo -> working");
737
- expect(output.text()).toContain("nolo > 你好");
738
- });
739
-
740
- test("prints an auth hint when installed without repo scripts or AUTH_TOKEN", async () => {
986
+ expect(output.text()).toContain("nolo -> working");
987
+ expect(output.text()).toContain("nolo > 你好");
988
+ });
989
+
990
+ test("returns a recoverable dialog when a server stream drops after dialog creation", async () => {
991
+ const output = new CaptureOutput();
992
+
993
+ const result = await runAgentTurn({
994
+ agentName: "nolo",
995
+ agentKey: "agent-pub-test",
996
+ serverUrl: "https://nolo.chat",
997
+ message: "hello",
998
+ scriptDir: "C:/missing/scripts",
999
+ env: { AUTH_TOKEN: "token-123" },
1000
+ output,
1001
+ scriptPathExists: () => false,
1002
+ fetchImpl: async () => {
1003
+ let sent = false;
1004
+ return new Response(
1005
+ new ReadableStream({
1006
+ pull(controller) {
1007
+ const encoder = new TextEncoder();
1008
+ if (!sent) {
1009
+ sent = true;
1010
+ controller.enqueue(
1011
+ encoder.encode(
1012
+ [
1013
+ `data: ${JSON.stringify({
1014
+ type: "dialog",
1015
+ dialogId: "dialog-recoverable",
1016
+ status: "running",
1017
+ })}`,
1018
+ "",
1019
+ `data: ${JSON.stringify({ type: "text", content: "partial" })}`,
1020
+ "",
1021
+ ].join("\n")
1022
+ )
1023
+ );
1024
+ return;
1025
+ }
1026
+ controller.error(new Error("socket closed"));
1027
+ },
1028
+ }),
1029
+ {
1030
+ status: 200,
1031
+ headers: { "Content-Type": "text/event-stream" },
1032
+ }
1033
+ );
1034
+ },
1035
+ });
1036
+
1037
+ expect(result).toEqual({
1038
+ exitCode: 0,
1039
+ dialogId: "dialog-recoverable",
1040
+ streamInterrupted: true,
1041
+ });
1042
+ expect(output.text()).toContain("dialog dialog-recoverable was created");
1043
+ expect(output.text()).toContain("read the dialog before retrying");
1044
+ });
1045
+
1046
+ test("prints an auth hint when installed without repo scripts or AUTH_TOKEN", async () => {
741
1047
  const output = new CaptureOutput();
742
1048
 
743
1049
  const result = await runAgentTurn({