nolo-cli 0.1.19 → 0.1.21

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 (111) hide show
  1. package/README.md +9 -1
  2. package/agent-runtime/agentConfigOptions.ts +12 -0
  3. package/agent-runtime/agentRecordConfig.ts +99 -0
  4. package/agent-runtime/agentRecordKeys.ts +14 -0
  5. package/agent-runtime/dialogMessageRecord.ts +16 -0
  6. package/agent-runtime/dialogWritePlan.ts +130 -0
  7. package/agent-runtime/hostAdapter.ts +13 -0
  8. package/agent-runtime/hybridRecordStore.ts +147 -0
  9. package/agent-runtime/index.ts +69 -0
  10. package/agent-runtime/localLoop.ts +69 -5
  11. package/agent-runtime/localToolPolicy.ts +130 -0
  12. package/agent-runtime/localWorkspaceTools.ts +1532 -0
  13. package/agent-runtime/openAiCompatibleProvider.ts +70 -0
  14. package/agent-runtime/openAiCompatibleProviderConfig.ts +38 -0
  15. package/agent-runtime/platformChatProvider.ts +241 -0
  16. package/agent-runtime/taskWorkspace.ts +193 -0
  17. package/agent-runtime/types.ts +1 -0
  18. package/agent-runtime/workspaceSession.ts +76 -0
  19. package/agentAliases.ts +37 -0
  20. package/agentPullCommand.ts +1 -1
  21. package/agentRunCommand.ts +278 -52
  22. package/agentRuntimeCommands.ts +354 -164
  23. package/agentRuntimeLocal.ts +38 -0
  24. package/ai/agent/agentSlice.ts +10 -0
  25. package/ai/agent/buildEditingContext.ts +5 -0
  26. package/ai/agent/buildSystemPrompt.ts +41 -18
  27. package/ai/agent/canvasEditingContext.ts +49 -0
  28. package/ai/agent/cliExecutor.ts +15 -4
  29. package/ai/agent/createAgentSchema.ts +2 -0
  30. package/ai/agent/executeToolCall.ts +3 -2
  31. package/ai/agent/hooks/usePublicAgents.ts +6 -0
  32. package/ai/agent/pageBuilderHandoffRules.ts +75 -0
  33. package/ai/agent/runAgentClientLoop.ts +4 -1
  34. package/ai/agent/runtimeGuidance.ts +19 -0
  35. package/ai/agent/server/fetchPublicAgents.ts +51 -1
  36. package/ai/agent/streamAgentChatTurn.ts +20 -2
  37. package/ai/agent/streamAgentChatTurnUtils.ts +60 -16
  38. package/ai/chat/accumulateToolCallChunks.ts +40 -9
  39. package/ai/chat/parseApiError.ts +3 -0
  40. package/ai/chat/sendOpenAICompletionsRequest.native.ts +23 -10
  41. package/ai/chat/sendOpenAICompletionsRequest.ts +13 -1
  42. package/ai/chat/updateTotalUsage.ts +26 -9
  43. package/ai/llm/deepinfra.ts +51 -0
  44. package/ai/llm/getPricing.ts +6 -0
  45. package/ai/llm/kimi.ts +2 -0
  46. package/ai/llm/openrouterModels.ts +0 -135
  47. package/ai/llm/providers.ts +1 -0
  48. package/ai/llm/types.ts +8 -0
  49. package/ai/taskRun/taskRunProtocol.ts +882 -0
  50. package/ai/token/calculatePrice.ts +30 -0
  51. package/ai/token/externalToolCost.ts +49 -29
  52. package/ai/token/prepareTokenUsageData.ts +6 -1
  53. package/ai/token/serverTokenWriter.ts +4 -2
  54. package/ai/tools/agent/agentTools.ts +21 -0
  55. package/ai/tools/agent/presets/appBuilderPreset.ts +7 -0
  56. package/ai/tools/agent/streamParallelAgentsTool.ts +2 -1
  57. package/ai/tools/agent/taskRunTool.ts +112 -0
  58. package/ai/tools/applyEditTool.ts +6 -3
  59. package/ai/tools/applyLineEditsTool.ts +6 -3
  60. package/ai/tools/checkEnvTool.ts +14 -9
  61. package/ai/tools/codeSearchTool.ts +17 -5
  62. package/ai/tools/execBashTool.ts +33 -29
  63. package/ai/tools/fetchWebpageSupport.ts +24 -0
  64. package/ai/tools/fetchWebpageTool.ts +18 -5
  65. package/ai/tools/index.ts +158 -0
  66. package/ai/tools/jdProductScraperTool.ts +821 -0
  67. package/ai/tools/listFilesTool.ts +6 -3
  68. package/ai/tools/localFilesTool.ts +200 -0
  69. package/ai/tools/readFileTool.ts +6 -3
  70. package/ai/tools/searchRepoTool.ts +6 -3
  71. package/ai/tools/table/rowTools.ts +6 -1
  72. package/ai/tools/taobaoTmallProductScraperTool.ts +49 -0
  73. package/ai/tools/toolApiClient.ts +20 -6
  74. package/ai/tools/wereadGatewayTool.ts +152 -0
  75. package/ai/tools/writeFileTool.ts +6 -3
  76. package/client/agentConfigResolver.test.ts +70 -0
  77. package/client/agentConfigResolver.ts +1 -0
  78. package/client/agentRun.test.ts +430 -7
  79. package/client/agentRun.ts +504 -64
  80. package/client/hybridRecordStore.test.ts +115 -0
  81. package/client/hybridRecordStore.ts +41 -0
  82. package/client/localAgentRecords.test.ts +27 -0
  83. package/client/localAgentRecords.ts +7 -0
  84. package/client/localDialogRecords.test.ts +124 -0
  85. package/client/localDialogRecords.ts +30 -0
  86. package/client/localProviderResolver.test.ts +78 -0
  87. package/client/localProviderResolver.ts +1 -0
  88. package/client/localRuntimeAdapter.test.ts +621 -9
  89. package/client/localRuntimeAdapter.ts +275 -250
  90. package/client/localRuntimeDryRun.test.ts +116 -0
  91. package/client/localToolPolicy.ts +8 -81
  92. package/client/taskRunPrompt.ts +26 -0
  93. package/client/taskWorktree.ts +8 -0
  94. package/client/workspaceSession.test.ts +57 -0
  95. package/client/workspaceSession.ts +11 -0
  96. package/commandRegistry.ts +23 -6
  97. package/connectorRunArtifact.ts +121 -0
  98. package/database/actions/write.ts +16 -2
  99. package/database/hooks/useUserData.ts +9 -3
  100. package/database/server/dataHandlers.ts +18 -20
  101. package/database/server/emailRepository.ts +3 -3
  102. package/database/server/patch.ts +18 -10
  103. package/database/server/query.ts +43 -4
  104. package/database/server/read.ts +24 -38
  105. package/database/server/recordIdentity.ts +100 -0
  106. package/database/server/write.ts +21 -25
  107. package/index.ts +70 -33
  108. package/machineCommands.ts +318 -144
  109. package/package.json +4 -1
  110. package/tableCommands.ts +181 -0
  111. package/taskRunCommand.ts +265 -0
@@ -1,7 +1,12 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { Writable } from "node:stream";
3
3
 
4
- import { runAgentTurn, shouldUseScriptBridge } from "./agentRun";
4
+ import {
5
+ classifyTaskRunReviewStatus,
6
+ findServerPlatformTools,
7
+ runAgentTurn,
8
+ shouldUseScriptBridge,
9
+ } from "./agentRun";
5
10
 
6
11
  class CaptureOutput extends Writable {
7
12
  chunks: string[] = [];
@@ -16,12 +21,31 @@ class CaptureOutput extends Writable {
16
21
  }
17
22
  }
18
23
 
19
- describe("cli agent run client", () => {
20
- test("uses the repo script bridge only when no auth token is available", () => {
21
- expect(shouldUseScriptBridge({ hasAuthToken: false, scriptPathExists: true })).toBe(true);
22
- expect(shouldUseScriptBridge({ hasAuthToken: true, scriptPathExists: true })).toBe(false);
23
- expect(shouldUseScriptBridge({ hasAuthToken: false, scriptPathExists: false })).toBe(false);
24
- });
24
+ describe("cli agent run client", () => {
25
+ test("classifies only clear reviewer outcomes", () => {
26
+ expect(classifyTaskRunReviewStatus("Review decision: passed")).toBe("passed");
27
+ expect(classifyTaskRunReviewStatus("Review decision: needs_changes")).toBe("needs_changes");
28
+ expect(classifyTaskRunReviewStatus("Review decision: blocked")).toBe("blocked");
29
+ expect(classifyTaskRunReviewStatus("LGTM, no issues found")).toBe("passed");
30
+ expect(classifyTaskRunReviewStatus("Changes requested for missing test coverage")).toBe(
31
+ "needs_changes"
32
+ );
33
+ expect(classifyTaskRunReviewStatus("I looked at the diff and have notes")).toBeUndefined();
34
+ });
35
+
36
+ test("uses the repo script bridge only when no auth token is available", () => {
37
+ expect(shouldUseScriptBridge({ hasAuthToken: false, scriptPathExists: true })).toBe(true);
38
+ expect(shouldUseScriptBridge({ hasAuthToken: true, scriptPathExists: true })).toBe(false);
39
+ expect(shouldUseScriptBridge({ hasAuthToken: false, scriptPathExists: false })).toBe(false);
40
+ });
41
+
42
+ test("identifies server platform tools that local runtime cannot provide", () => {
43
+ expect(findServerPlatformTools(["readWorkspaceFile", "queryTableRows", "updateTableRow"])).toEqual([
44
+ "queryTableRows",
45
+ "updateTableRow",
46
+ ]);
47
+ expect(findServerPlatformTools(["readWorkspaceFile", "gitStatus"])).toEqual([]);
48
+ });
25
49
 
26
50
  test("calls the Nolo HTTP API directly when AUTH_TOKEN is present", async () => {
27
51
  const output = new CaptureOutput();
@@ -76,6 +100,78 @@ describe("cli agent run client", () => {
76
100
  expect(output.text()).not.toContain("tokens=");
77
101
  });
78
102
 
103
+ test("prepends task-run context for HTTP agent runs", async () => {
104
+ const output = new CaptureOutput();
105
+ const requests: Array<{ body: any }> = [];
106
+
107
+ await runAgentTurn({
108
+ agentName: "frontend",
109
+ agentKey: "frontend-implementer",
110
+ serverUrl: "https://nolo.chat",
111
+ message: "Fix the filter UI",
112
+ scriptDir: "C:/missing/scripts",
113
+ env: { AUTH_TOKEN: "token-123" },
114
+ output,
115
+ runtimeMode: "server",
116
+ taskRunContext: {
117
+ rowDbKey: "row-b2e06f801f-01TASK",
118
+ taskRunId: "taskrun-1",
119
+ workItemId: "frontend-filter",
120
+ artifactIds: ["artifact-1"],
121
+ },
122
+ scriptPathExists: () => false,
123
+ fetchImpl: async (_url, init) => {
124
+ requests.push({ body: JSON.parse(String(init?.body)) });
125
+ return Response.json({ content: "ok", dialogId: "dialog-1" });
126
+ },
127
+ });
128
+
129
+ expect(requests[0]?.body.userInput).toContain("AI-native task-run context:");
130
+ expect(requests[0]?.body.userInput).toContain("current task board is only the present observation and validation use case");
131
+ expect(requests[0]?.body.userInput).toContain("rowDbKey: row-b2e06f801f-01TASK");
132
+ expect(requests[0]?.body.userInput).toContain("workItemId: frontend-filter");
133
+ expect(requests[0]?.body.userInput).toContain("User task:\nFix the filter UI");
134
+ });
135
+
136
+ test("records task-run completion after an HTTP agent run returns a dialog", async () => {
137
+ const output = new CaptureOutput();
138
+ const writebacks: any[] = [];
139
+
140
+ const result = await runAgentTurn({
141
+ agentName: "frontend-implementer",
142
+ agentKey: "agent-frontend",
143
+ serverUrl: "https://nolo.chat",
144
+ message: "Fix the filter UI",
145
+ scriptDir: "C:/missing/scripts",
146
+ env: { AUTH_TOKEN: "token-123" },
147
+ output,
148
+ runtimeMode: "server",
149
+ taskRunContext: {
150
+ rowDbKey: "row-b2e06f801f-01TASK",
151
+ workItemId: "frontend-filter",
152
+ },
153
+ taskRunRecorder: async (args) => {
154
+ writebacks.push(args);
155
+ },
156
+ scriptPathExists: () => false,
157
+ fetchImpl: async () => {
158
+ return Response.json({ content: "patched", dialogId: "dialog-1" });
159
+ },
160
+ });
161
+
162
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-1" });
163
+ expect(writebacks).toHaveLength(1);
164
+ expect(writebacks[0]).toMatchObject({
165
+ status: "completed",
166
+ dialogId: "dialog-1",
167
+ resultSummary: "patched",
168
+ });
169
+ expect(writebacks[0].options.taskRunContext).toEqual({
170
+ rowDbKey: "row-b2e06f801f-01TASK",
171
+ workItemId: "frontend-filter",
172
+ });
173
+ });
174
+
79
175
  test("sends image inputs as multimodal userInput over HTTP", async () => {
80
176
  const output = new CaptureOutput();
81
177
  const requests: Array<{ body: any }> = [];
@@ -103,6 +199,49 @@ describe("cli agent run client", () => {
103
199
  ]);
104
200
  });
105
201
 
202
+ test("sends script-compatible dialog metadata over HTTP", async () => {
203
+ const output = new CaptureOutput();
204
+ const requests: Array<{ body: any }> = [];
205
+
206
+ const result = await runAgentTurn({
207
+ agentName: "nolo",
208
+ agentKey: "agent-pub-test",
209
+ serverUrl: "https://nolo.chat",
210
+ message: "hello",
211
+ spaceId: "space-1",
212
+ category: "manual-checks",
213
+ inheritedFromDialogKey: "dialog-user-1-dialog-2",
214
+ parentDialogId: "dialog-2",
215
+ background: true,
216
+ noStream: true,
217
+ maxToolRounds: 12,
218
+ timeoutMs: 600000,
219
+ scriptDir: "C:/missing/scripts",
220
+ env: { AUTH_TOKEN: "token-123" },
221
+ output,
222
+ runtimeMode: "server",
223
+ scriptPathExists: () => false,
224
+ fetchImpl: async (_url, init) => {
225
+ requests.push({ body: JSON.parse(String(init?.body)) });
226
+ return Response.json({ dialogId: "dialog-1", status: "queued" });
227
+ },
228
+ });
229
+
230
+ expect(requests[0]?.body).toMatchObject({
231
+ agentKey: "agent-pub-test",
232
+ userInput: "hello",
233
+ spaceId: "space-1",
234
+ category: "manual-checks",
235
+ inheritedFromDialogKey: "dialog-user-1-dialog-2",
236
+ parentDialogId: "dialog-2",
237
+ background: true,
238
+ maxRounds: 12,
239
+ timeoutMs: 600000,
240
+ stream: false,
241
+ });
242
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-1" });
243
+ });
244
+
106
245
  test("runs forced local turns through the injected runtime adapter without HTTP", async () => {
107
246
  const output = new CaptureOutput();
108
247
  const result = await runAgentTurn({
@@ -148,6 +287,240 @@ describe("cli agent run client", () => {
148
287
  expect(output.text()).toContain("frontend > local:polish notifications");
149
288
  });
150
289
 
290
+ test("increases local max tool rounds when a forced local run exhausts them", async () => {
291
+ const output = new CaptureOutput();
292
+ let completeCalls = 0;
293
+ const result = await runAgentTurn({
294
+ agentName: "frontend",
295
+ agentKey: "frontend-local",
296
+ serverUrl: "https://nolo.chat",
297
+ message: "polish notifications",
298
+ scriptDir: "C:/missing/scripts",
299
+ env: { AUTH_TOKEN: "token-123", NOLO_LOCAL_MAX_TOOL_ROUNDS_LIMIT: "1" },
300
+ output,
301
+ runtimeMode: "local",
302
+ maxToolRounds: 0,
303
+ localRuntimeAdapter: {
304
+ host: "cli",
305
+ capabilities: ["local-provider", "local-persistence"],
306
+ loadAgentConfig: async (agentRef) => ({
307
+ key: agentRef,
308
+ name: "Frontend",
309
+ prompt: "Fix UI",
310
+ model: "fake-local",
311
+ }),
312
+ loadDialogHistory: async () => [],
313
+ saveTurn: async () => ({ dialogId: "dialog-local" }),
314
+ resolveProvider: async () => ({
315
+ model: "fake-local",
316
+ complete: async () => {
317
+ completeCalls += 1;
318
+ return {
319
+ content: "",
320
+ model: "fake-local",
321
+ tool_calls: [{
322
+ id: `call-${completeCalls}`,
323
+ type: "function",
324
+ function: { name: "readWorkspaceFile", arguments: "{}" },
325
+ }],
326
+ };
327
+ },
328
+ }),
329
+ executeTool: async () => ({ content: "ok" }),
330
+ },
331
+ scriptPathExists: () => false,
332
+ fetchImpl: async () => {
333
+ throw new Error("HTTP should not be called for forced local runs");
334
+ },
335
+ });
336
+
337
+ expect(result).toMatchObject({ exitCode: 1 });
338
+ expect(output.text()).toContain("Local agent used all 0 tool rounds; retrying with 1.");
339
+ expect(output.text()).toContain("Local agent exceeded max tool rounds: 1");
340
+ });
341
+
342
+ test("completes a forced local run after automatically increasing tool rounds", async () => {
343
+ const output = new CaptureOutput();
344
+ let completeCalls = 0;
345
+ let toolCalls = 0;
346
+
347
+ const result = await runAgentTurn({
348
+ agentName: "frontend",
349
+ agentKey: "frontend-local",
350
+ serverUrl: "https://nolo.chat",
351
+ message: "make screenshots",
352
+ scriptDir: "C:/missing/scripts",
353
+ env: { AUTH_TOKEN: "token-123" },
354
+ output,
355
+ runtimeMode: "local",
356
+ maxToolRounds: 1,
357
+ localRuntimeAdapter: {
358
+ host: "cli",
359
+ capabilities: ["local-provider", "local-persistence", "local-tools"],
360
+ loadAgentConfig: async (agentRef) => ({
361
+ key: agentRef,
362
+ name: "Frontend",
363
+ prompt: "Fix UI",
364
+ model: "fake-local",
365
+ toolNames: ["readWorkspaceFile"],
366
+ }),
367
+ loadDialogHistory: async () => [],
368
+ saveTurn: async () => ({ dialogId: "dialog-local-retry" }),
369
+ resolveProvider: async () => ({
370
+ model: "fake-local",
371
+ complete: async () => {
372
+ completeCalls += 1;
373
+ if (completeCalls <= 3) {
374
+ return {
375
+ content: "",
376
+ model: "fake-local",
377
+ tool_calls: [{
378
+ id: `call-${completeCalls}`,
379
+ type: "function",
380
+ function: { name: "readWorkspaceFile", arguments: "{}" },
381
+ }],
382
+ };
383
+ }
384
+ return { content: "done after retry", model: "fake-local" };
385
+ },
386
+ }),
387
+ executeTool: async () => {
388
+ toolCalls += 1;
389
+ return { content: "ok" };
390
+ },
391
+ },
392
+ scriptPathExists: () => false,
393
+ });
394
+
395
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-local-retry" });
396
+ expect(toolCalls).toBe(2);
397
+ expect(output.text()).toContain("Local agent used all 1 tool rounds; retrying with 2.");
398
+ expect(output.text()).toContain("frontend > done after retry");
399
+ });
400
+
401
+ test("prints local tool trace when requested", async () => {
402
+ const output = new CaptureOutput();
403
+ let completeCalls = 0;
404
+
405
+ const result = await runAgentTurn({
406
+ agentName: "frontend",
407
+ agentKey: "frontend-local",
408
+ serverUrl: "https://nolo.chat",
409
+ message: "inspect repo",
410
+ scriptDir: "C:/missing/scripts",
411
+ env: { AUTH_TOKEN: "token-123" },
412
+ output,
413
+ runtimeMode: "local",
414
+ traceTools: true,
415
+ localRuntimeAdapter: {
416
+ host: "cli",
417
+ capabilities: ["local-provider", "local-persistence", "local-tools"],
418
+ loadAgentConfig: async (agentRef) => ({
419
+ key: agentRef,
420
+ name: "Frontend",
421
+ prompt: "Fix UI",
422
+ model: "fake-local",
423
+ toolNames: ["gitStatus"],
424
+ }),
425
+ loadDialogHistory: async () => [],
426
+ saveTurn: async () => ({ dialogId: "dialog-local" }),
427
+ resolveProvider: async () => ({
428
+ model: "fake-local",
429
+ complete: async () => {
430
+ completeCalls += 1;
431
+ if (completeCalls === 1) {
432
+ return {
433
+ content: "",
434
+ model: "fake-local",
435
+ tool_calls: [{
436
+ id: "call-status",
437
+ type: "function",
438
+ function: { name: "gitStatus", arguments: "{}" },
439
+ }],
440
+ };
441
+ }
442
+ return { content: "done", model: "fake-local" };
443
+ },
444
+ }),
445
+ executeTool: async () => ({ content: "clean" }),
446
+ },
447
+ scriptPathExists: () => false,
448
+ });
449
+
450
+ expect(result).toMatchObject({ exitCode: 0, dialogId: "dialog-local" });
451
+ expect(output.text()).toContain("[nolo:tool] round 1 -> gitStatus (call-status)");
452
+ expect(output.text()).toContain("[nolo:tool] round 1 <- gitStatus (call-status)");
453
+ });
454
+
455
+ test("passes local tool evidence to task-run writeback", async () => {
456
+ const output = new CaptureOutput();
457
+ const writebacks: any[] = [];
458
+ let completeCalls = 0;
459
+
460
+ const result = await runAgentTurn({
461
+ agentName: "frontend",
462
+ agentKey: "frontend-local",
463
+ serverUrl: "https://nolo.chat",
464
+ message: "inspect repo",
465
+ scriptDir: "C:/missing/scripts",
466
+ env: { AUTH_TOKEN: "token-123" },
467
+ output,
468
+ runtimeMode: "local",
469
+ taskRunContext: {
470
+ rowDbKey: "row-b2e06f801f-01TASK",
471
+ workItemId: "self",
472
+ },
473
+ taskRunRecorder: async (args) => {
474
+ writebacks.push(args);
475
+ },
476
+ localRuntimeAdapter: {
477
+ host: "cli",
478
+ capabilities: ["local-provider", "local-persistence", "local-tools"],
479
+ loadAgentConfig: async (agentRef) => ({
480
+ key: agentRef,
481
+ name: "Frontend",
482
+ prompt: "Fix UI",
483
+ model: "fake-local",
484
+ toolNames: ["gitStatus"],
485
+ }),
486
+ loadDialogHistory: async () => [],
487
+ saveTurn: async () => ({ dialogId: "dialog-local" }),
488
+ resolveProvider: async () => ({
489
+ model: "fake-local",
490
+ complete: async () => {
491
+ completeCalls += 1;
492
+ if (completeCalls === 1) {
493
+ return {
494
+ content: "",
495
+ model: "fake-local",
496
+ tool_calls: [{
497
+ id: "call-status",
498
+ type: "function",
499
+ function: { name: "gitStatus", arguments: "{}" },
500
+ }],
501
+ };
502
+ }
503
+ return { content: "done", model: "fake-local" };
504
+ },
505
+ }),
506
+ executeTool: async () => ({ content: "clean" }),
507
+ },
508
+ scriptPathExists: () => false,
509
+ });
510
+
511
+ expect(result).toMatchObject({ exitCode: 0, dialogId: "dialog-local" });
512
+ expect(writebacks).toHaveLength(1);
513
+ expect(writebacks[0].toolEvidence).toEqual({
514
+ toolsUsed: ["gitStatus"],
515
+ summary: {
516
+ callCount: 1,
517
+ resultCount: 1,
518
+ errorCount: 0,
519
+ rounds: 1,
520
+ },
521
+ });
522
+ });
523
+
151
524
  test("auto mode prefers a working local runtime before HTTP", async () => {
152
525
  const output = new CaptureOutput();
153
526
  const httpCalls: string[] = [];
@@ -192,6 +565,56 @@ describe("cli agent run client", () => {
192
565
  expect(output.text()).toContain("frontend -> working locally");
193
566
  });
194
567
 
568
+ test("auto mode skips local runtime for agents that declare server platform tools", async () => {
569
+ const output = new CaptureOutput();
570
+ const httpCalls: Array<{ url: string; body: any }> = [];
571
+
572
+ const result = await runAgentTurn({
573
+ agentName: "pm",
574
+ agentKey: "nolo-project-manager",
575
+ serverUrl: "https://nolo.chat",
576
+ message: "write task rows",
577
+ scriptDir: "C:/missing/scripts",
578
+ env: { AUTH_TOKEN: "token-123" },
579
+ output,
580
+ runtimeMode: "auto",
581
+ localRuntimeAdapter: {
582
+ host: "cli",
583
+ capabilities: ["leveldb-agent-config", "local-provider", "local-tools"],
584
+ loadAgentConfig: async (agentRef) => ({
585
+ key: agentRef,
586
+ name: "PM",
587
+ prompt: "Manage task rows",
588
+ model: "fake-local",
589
+ toolNames: ["queryTableRows", "addTableRow", "updateTableRow"],
590
+ }),
591
+ loadDialogHistory: async () => [],
592
+ saveTurn: async () => {
593
+ throw new Error("local runtime should be skipped");
594
+ },
595
+ resolveProvider: async () => {
596
+ throw new Error("local provider should be skipped");
597
+ },
598
+ executeTool: async () => {
599
+ throw new Error("local tools should be skipped");
600
+ },
601
+ },
602
+ scriptPathExists: () => false,
603
+ fetchImpl: async (url, init) => {
604
+ httpCalls.push({ url: String(url), body: JSON.parse(String(init?.body)) });
605
+ return Response.json({ content: "server ok", dialogId: "dialog-server" });
606
+ },
607
+ });
608
+
609
+ expect(result).toEqual({ exitCode: 0, dialogId: "dialog-server" });
610
+ expect(httpCalls).toHaveLength(1);
611
+ expect(httpCalls[0]?.body.agentKey).toBe("nolo-project-manager");
612
+ expect(output.text()).toContain("auto runtime: skipping local runtime");
613
+ expect(output.text()).toContain("queryTableRows, addTableRow, updateTableRow");
614
+ expect(output.text()).toContain("pm -> working");
615
+ expect(output.text()).toContain("pm > server ok");
616
+ });
617
+
195
618
  test("builds the default local adapter when env requests local mode", async () => {
196
619
  const output = new CaptureOutput();
197
620
  const builtModes: string[] = [];