nolo-cli 0.1.39 → 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 (35) hide show
  1. package/README.md +4 -4
  2. package/agent-runtime/dialogWritePlan.ts +39 -0
  3. package/agent-runtime/hostAdapter.ts +4 -0
  4. package/agent-runtime/hybridRecordStore.ts +1 -1
  5. package/agent-runtime/index.ts +15 -0
  6. package/agent-runtime/localLoop.ts +87 -7
  7. package/agent-runtime/localToolPolicy.ts +2 -13
  8. package/agent-runtime/localWorkspaceTools.ts +42 -110
  9. package/agent-runtime/runtimeToolPolicy.ts +2 -11
  10. package/agent-runtime/runtimeToolSurface.ts +198 -0
  11. package/agent-runtime/types.ts +1 -0
  12. package/agentAliases.ts +2 -1
  13. package/agentRunCommand.ts +10 -32
  14. package/agentRuntimeLocal.ts +1 -0
  15. package/ai/agent/cliExecutor.ts +249 -34
  16. package/cli/agentAliases.ts +2 -1
  17. package/cli/agentRunCommand.ts +10 -32
  18. package/cli/agentRuntimeLocal.ts +1 -0
  19. package/cli/client/agentRun.ts +40 -87
  20. package/cli/client/localRuntimeAdapter.ts +179 -26
  21. package/cli/commandRegistry.ts +2 -2
  22. package/cli/machineWsRunDispatch.ts +51 -6
  23. package/cli/offlineMarxistsAgentCommand.ts +6 -4
  24. package/client/agentConfigResolver.test.ts +2 -2
  25. package/client/agentRun.test.ts +123 -57
  26. package/client/agentRun.ts +40 -87
  27. package/client/localRuntimeAdapter.test.ts +245 -31
  28. package/client/localRuntimeAdapter.ts +179 -26
  29. package/client/localRuntimeDryRun.test.ts +23 -9
  30. package/client/localToolPolicy.test.ts +4 -4
  31. package/commandRegistry.ts +2 -2
  32. package/database/server/levelAuthorityStore.ts +20 -1
  33. package/machineWsRunDispatch.ts +51 -6
  34. package/offlineMarxistsAgentCommand.ts +6 -4
  35. package/package.json +2 -1
@@ -40,15 +40,17 @@ function readFlagValue(args: string[], flag: string) {
40
40
  }
41
41
 
42
42
  function parseUserIdFromToken(token: string) {
43
- const payload = token.split(".")[0];
44
- if (!payload) return "";
43
+ const payload = token.split(".")[1];
44
+ if (!payload) return undefined;
45
45
  try {
46
46
  const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
47
47
  const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
48
48
  const parsed = JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
49
- return typeof parsed?.userId === "string" ? parsed.userId : "";
49
+ return typeof parsed?.userId === "string" && parsed.userId.trim()
50
+ ? parsed.userId.trim()
51
+ : undefined;
50
52
  } catch {
51
- return "";
53
+ return undefined;
52
54
  }
53
55
  }
54
56
 
@@ -15,7 +15,7 @@ describe("agent config resolver", () => {
15
15
  cliProvider: "codex",
16
16
  customProviderUrl: "https://provider.example/v1",
17
17
  toolNames: ["legacyTool", "readFile"],
18
- tools: ["readFile", "applyEdit"],
18
+ tools: ["readFile", "editFile"],
19
19
  runtimeBinding: { machineId: "machine-1" },
20
20
  delegation: { target: "local" },
21
21
  temperature: 0.2,
@@ -33,7 +33,7 @@ describe("agent config resolver", () => {
33
33
  apiSource: "platform",
34
34
  cliProvider: "codex",
35
35
  customProviderUrl: "https://provider.example/v1",
36
- toolNames: ["legacyTool", "readFile", "applyEdit"],
36
+ toolNames: ["legacyTool", "readFile", "editFile"],
37
37
  runtimeBinding: { machineId: "machine-1" },
38
38
  delegation: { target: "local" },
39
39
  temperature: 0.2,
@@ -35,11 +35,11 @@ describe("cli agent run client", () => {
35
35
  });
36
36
 
37
37
  test("identifies server platform tools that local runtime cannot provide", () => {
38
- expect(findServerPlatformTools(["readWorkspaceFile", "queryTableRows", "updateTableRow"])).toEqual([
38
+ expect(findServerPlatformTools(["readFile", "queryTableRows", "updateTableRow"])).toEqual([
39
39
  "queryTableRows",
40
40
  "updateTableRow",
41
41
  ]);
42
- expect(findServerPlatformTools(["readWorkspaceFile", "execShell"])).toEqual([]);
42
+ expect(findServerPlatformTools(["readFile", "execShell"])).toEqual([]);
43
43
  });
44
44
 
45
45
  test("calls the Nolo HTTP API directly when AUTH_TOKEN is present", async () => {
@@ -239,7 +239,6 @@ describe("cli agent run client", () => {
239
239
  parentDialogId: "dialog-2",
240
240
  background: true,
241
241
  noStream: true,
242
- maxToolRounds: 12,
243
242
  timeoutMs: 600000,
244
243
  scriptDir: "C:/missing/scripts",
245
244
  env: { AUTH_TOKEN: "token-123" },
@@ -259,7 +258,6 @@ describe("cli agent run client", () => {
259
258
  inheritedFromDialogKey: "dialog-user-1-dialog-2",
260
259
  parentDialogId: "dialog-2",
261
260
  background: true,
262
- maxRounds: 12,
263
261
  timeoutMs: 600000,
264
262
  stream: false,
265
263
  });
@@ -345,72 +343,75 @@ describe("cli agent run client", () => {
345
343
  expect(output.text()).toContain("frontend > local:polish notifications");
346
344
  });
347
345
 
348
- 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 () => {
349
347
  const output = new CaptureOutput();
350
348
  let completeCalls = 0;
349
+ let toolCalls = 0;
350
+
351
351
  const result = await runAgentTurn({
352
352
  agentName: "frontend",
353
353
  agentKey: "frontend-local",
354
354
  serverUrl: "https://nolo.chat",
355
- message: "polish notifications",
355
+ message: "make screenshots",
356
356
  scriptDir: "C:/missing/scripts",
357
- env: { AUTH_TOKEN: "token-123", NOLO_LOCAL_MAX_TOOL_ROUNDS_LIMIT: "1" },
357
+ env: { AUTH_TOKEN: "token-123" },
358
358
  output,
359
359
  runtimeMode: "local",
360
- maxToolRounds: 0,
361
360
  localRuntimeAdapter: {
362
361
  host: "cli",
363
- capabilities: ["local-provider", "local-persistence"],
362
+ capabilities: ["local-provider", "local-persistence", "local-tools"],
364
363
  loadAgentConfig: async (agentRef) => ({
365
364
  key: agentRef,
366
365
  name: "Frontend",
367
366
  prompt: "Fix UI",
368
367
  model: "fake-local",
368
+ toolNames: ["readFile"],
369
369
  }),
370
370
  loadDialogHistory: async () => [],
371
- saveTurn: async () => ({ dialogId: "dialog-local" }),
371
+ saveTurn: async () => ({ dialogId: "dialog-local-retry" }),
372
372
  resolveProvider: async () => ({
373
373
  model: "fake-local",
374
374
  complete: async () => {
375
375
  completeCalls += 1;
376
- return {
377
- content: "",
378
- model: "fake-local",
379
- tool_calls: [{
380
- id: `call-${completeCalls}`,
381
- type: "function",
382
- function: { name: "readWorkspaceFile", arguments: "{}" },
383
- }],
384
- };
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" };
385
388
  },
386
389
  }),
387
- executeTool: async () => ({ content: "ok" }),
388
- },
389
- fetchImpl: async () => {
390
- throw new Error("HTTP should not be called for forced local runs");
390
+ executeTool: async () => {
391
+ toolCalls += 1;
392
+ return { content: "ok" };
393
+ },
391
394
  },
392
395
  });
393
396
 
394
- expect(result).toMatchObject({ exitCode: 1 });
395
- expect(output.text()).toContain("Local agent used all 0 tool rounds; retrying with 1.");
396
- 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");
397
400
  });
398
401
 
399
- test("completes a forced local run after automatically increasing tool rounds", async () => {
402
+ test("prints compact local tool trace by default", async () => {
400
403
  const output = new CaptureOutput();
401
404
  let completeCalls = 0;
402
- let toolCalls = 0;
403
405
 
404
406
  const result = await runAgentTurn({
405
407
  agentName: "frontend",
406
408
  agentKey: "frontend-local",
407
409
  serverUrl: "https://nolo.chat",
408
- message: "make screenshots",
410
+ message: "inspect repo",
409
411
  scriptDir: "C:/missing/scripts",
410
412
  env: { AUTH_TOKEN: "token-123" },
411
413
  output,
412
414
  runtimeMode: "local",
413
- maxToolRounds: 1,
414
415
  localRuntimeAdapter: {
415
416
  host: "cli",
416
417
  capabilities: ["local-provider", "local-persistence", "local-tools"],
@@ -419,42 +420,39 @@ describe("cli agent run client", () => {
419
420
  name: "Frontend",
420
421
  prompt: "Fix UI",
421
422
  model: "fake-local",
422
- toolNames: ["readWorkspaceFile"],
423
+ toolNames: ["readFile"],
423
424
  }),
424
425
  loadDialogHistory: async () => [],
425
- saveTurn: async () => ({ dialogId: "dialog-local-retry" }),
426
+ saveTurn: async () => ({ dialogId: "dialog-local" }),
426
427
  resolveProvider: async () => ({
427
428
  model: "fake-local",
428
429
  complete: async () => {
429
430
  completeCalls += 1;
430
- if (completeCalls <= 3) {
431
+ if (completeCalls === 1) {
431
432
  return {
432
433
  content: "",
433
434
  model: "fake-local",
434
435
  tool_calls: [{
435
- id: `call-${completeCalls}`,
436
+ id: "call-read",
436
437
  type: "function",
437
- function: { name: "readWorkspaceFile", arguments: "{}" },
438
+ function: { name: "readFile", arguments: JSON.stringify({ path: "README.md" }) },
438
439
  }],
439
440
  };
440
441
  }
441
- return { content: "done after retry", model: "fake-local" };
442
+ return { content: "done", model: "fake-local" };
442
443
  },
443
444
  }),
444
- executeTool: async () => {
445
- toolCalls += 1;
446
- return { content: "ok" };
447
- },
445
+ executeTool: async () => ({ content: "clean" }),
448
446
  },
449
447
  });
450
448
 
451
- expect(result).toEqual({ exitCode: 0, dialogId: "dialog-local-retry" });
452
- expect(toolCalls).toBe(2);
453
- expect(output.text()).toContain("Local agent used all 1 tool rounds; retrying with 2.");
454
- 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");
455
453
  });
456
454
 
457
- test("prints local tool trace when requested", async () => {
455
+ test("does not print local tool trace when disabled by env", async () => {
458
456
  const output = new CaptureOutput();
459
457
  let completeCalls = 0;
460
458
 
@@ -464,10 +462,9 @@ describe("cli agent run client", () => {
464
462
  serverUrl: "https://nolo.chat",
465
463
  message: "inspect repo",
466
464
  scriptDir: "C:/missing/scripts",
467
- env: { AUTH_TOKEN: "token-123" },
465
+ env: { AUTH_TOKEN: "token-123", NOLO_TRACE_TOOLS: "0" },
468
466
  output,
469
467
  runtimeMode: "local",
470
- traceTools: true,
471
468
  localRuntimeAdapter: {
472
469
  host: "cli",
473
470
  capabilities: ["local-provider", "local-persistence", "local-tools"],
@@ -476,10 +473,10 @@ describe("cli agent run client", () => {
476
473
  name: "Frontend",
477
474
  prompt: "Fix UI",
478
475
  model: "fake-local",
479
- toolNames: ["readWorkspaceFile"],
476
+ toolNames: ["readFile"],
480
477
  }),
481
478
  loadDialogHistory: async () => [],
482
- saveTurn: async () => ({ dialogId: "dialog-local" }),
479
+ saveTurn: async () => ({ dialogId: "dialog-local-no-trace" }),
483
480
  resolveProvider: async () => ({
484
481
  model: "fake-local",
485
482
  complete: async () => {
@@ -491,7 +488,7 @@ describe("cli agent run client", () => {
491
488
  tool_calls: [{
492
489
  id: "call-read",
493
490
  type: "function",
494
- function: { name: "readWorkspaceFile", arguments: JSON.stringify({ path: "README.md" }) },
491
+ function: { name: "readFile", arguments: JSON.stringify({ path: "README.md" }) },
495
492
  }],
496
493
  };
497
494
  }
@@ -502,9 +499,79 @@ describe("cli agent run client", () => {
502
499
  },
503
500
  });
504
501
 
505
- expect(result).toMatchObject({ exitCode: 0, dialogId: "dialog-local" });
506
- expect(output.text()).toContain("[nolo:tool] round 1 -> readWorkspaceFile (call-read)");
507
- 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
+ });
508
575
  });
509
576
 
510
577
  test("passes local tool evidence to task-run writeback", async () => {
@@ -536,7 +603,7 @@ describe("cli agent run client", () => {
536
603
  name: "Frontend",
537
604
  prompt: "Fix UI",
538
605
  model: "fake-local",
539
- toolNames: ["readWorkspaceFile"],
606
+ toolNames: ["readFile"],
540
607
  }),
541
608
  loadDialogHistory: async () => [],
542
609
  saveTurn: async () => ({ dialogId: "dialog-local" }),
@@ -551,7 +618,7 @@ describe("cli agent run client", () => {
551
618
  tool_calls: [{
552
619
  id: "call-read",
553
620
  type: "function",
554
- function: { name: "readWorkspaceFile", arguments: JSON.stringify({ path: "README.md" }) },
621
+ function: { name: "readFile", arguments: JSON.stringify({ path: "README.md" }) },
555
622
  }],
556
623
  };
557
624
  }
@@ -565,7 +632,7 @@ describe("cli agent run client", () => {
565
632
  expect(result).toMatchObject({ exitCode: 0, dialogId: "dialog-local" });
566
633
  expect(writebacks).toHaveLength(1);
567
634
  expect(writebacks[0].toolEvidence).toEqual({
568
- toolsUsed: ["readWorkspaceFile"],
635
+ toolsUsed: ["readFile"],
569
636
  summary: {
570
637
  callCount: 1,
571
638
  resultCount: 1,
@@ -833,7 +900,6 @@ describe("cli agent run client", () => {
833
900
  scriptDir: "C:/missing/scripts",
834
901
  env: {
835
902
  AUTH_TOKEN: "token-123",
836
- NOLO_LOCAL_SHELL_MODE: "worktree",
837
903
  },
838
904
  output,
839
905
  runtimeMode: "auto",
@@ -44,7 +44,6 @@ type RunAgentTurnOptions = {
44
44
  parentDialogId?: string;
45
45
  background?: boolean;
46
46
  noStream?: boolean;
47
- noDefaultTestRoot?: boolean;
48
47
  scriptDir: string;
49
48
  env: EnvLike;
50
49
  output: OutputLike;
@@ -52,9 +51,9 @@ type RunAgentTurnOptions = {
52
51
  localRuntimeAdapter?: AgentRuntimeHostAdapter;
53
52
  localRuntimeAdapterFactory?: (env: EnvLike, options?: { cwd?: string }) => AgentRuntimeHostAdapter;
54
53
  localRuntimeCwd?: string;
55
- maxToolRounds?: number;
56
54
  timeoutMs?: number;
57
55
  traceTools?: boolean;
56
+ eventsMode?: "jsonl";
58
57
  taskRunContext?: TaskRunPromptContext;
59
58
  taskRunRecorder?: (args: {
60
59
  options: RunAgentTurnOptions;
@@ -232,7 +231,7 @@ function resolveLocalRuntimeAdapter(options: RunAgentTurnOptions) {
232
231
 
233
232
  async function shouldSkipAutoLocalForServerPlatformTools(options: RunAgentTurnOptions) {
234
233
  if (isBuiltinNoloAgentRef(options.agentKey)) return false;
235
- if (options.localRuntimeCwd && options.env.NOLO_LOCAL_SHELL_MODE === "worktree") {
234
+ if (options.localRuntimeCwd) {
236
235
  return false;
237
236
  }
238
237
  if (isKnownServerPlatformAgent(options)) {
@@ -278,7 +277,14 @@ function buildUserInputContent(message: string, imageUrls: string[] = []) {
278
277
  }
279
278
 
280
279
  function shouldTraceLocalTools(options: RunAgentTurnOptions) {
281
- return Boolean(options.traceTools || options.env.NOLO_TRACE_TOOLS === "1");
280
+ const setting = (options.env.NOLO_TRACE_TOOLS ?? "").trim().toLowerCase();
281
+ if (setting === "0" || setting === "false" || setting === "off") return false;
282
+ return true;
283
+ }
284
+
285
+ function resolveAgentEventMode(options: RunAgentTurnOptions): "text" | "jsonl" {
286
+ if (options.eventsMode === "jsonl") return "jsonl";
287
+ return options.env.NOLO_AGENT_EVENTS === "jsonl" ? "jsonl" : "text";
282
288
  }
283
289
 
284
290
  function shouldCollectLocalToolEvidence(options: RunAgentTurnOptions) {
@@ -307,15 +313,6 @@ function summarizeLocalToolEvidence(events: LocalAgentToolEvent[]): TaskRunToolE
307
313
  };
308
314
  }
309
315
 
310
- const DEFAULT_LOCAL_AGENT_TOOL_ROUND_RETRY_LIMIT = 512;
311
-
312
- function parseLocalToolRoundLimitError(error: unknown) {
313
- const message = error instanceof Error ? error.message : String(error);
314
- const match = message.match(/Local agent exceeded max tool rounds:\s*(\d+)/);
315
- if (!match) return null;
316
- return Number(match[1]);
317
- }
318
-
319
316
  function isMissingLocalAgentConfigError(error: unknown, agentRef: string) {
320
317
  return Boolean(
321
318
  error &&
@@ -326,33 +323,33 @@ function isMissingLocalAgentConfigError(error: unknown, agentRef: string) {
326
323
  );
327
324
  }
328
325
 
329
- function parsePositiveInteger(value: string | undefined) {
330
- if (!value) return null;
331
- const parsed = Number(value);
332
- if (!Number.isInteger(parsed) || parsed <= 0) return null;
333
- return parsed;
334
- }
335
-
336
- function resolveLocalToolRoundRetryLimit(options: RunAgentTurnOptions) {
337
- return (
338
- parsePositiveInteger(options.env.NOLO_LOCAL_MAX_TOOL_ROUNDS_LIMIT) ??
339
- DEFAULT_LOCAL_AGENT_TOOL_ROUND_RETRY_LIMIT
340
- );
341
- }
342
-
343
- function nextLocalToolRoundLimit(current: number) {
344
- return current <= 0 ? 1 : current * 2;
345
- }
346
-
347
326
  function formatToolTraceEvent(event: LocalAgentToolEvent) {
348
327
  const round = event.round + 1;
328
+ const detail = event.argumentsPreview ? ` ${event.argumentsPreview}` : "";
349
329
  if (event.type === "tool-call") {
350
- return `[nolo:tool] round ${round} -> ${event.toolName} (${event.toolCallId})\n`;
330
+ return `[nolo:tool] #${round} -> ${event.toolName}${detail}\n`;
351
331
  }
352
332
  if (event.type === "tool-error") {
353
- return `[nolo:tool] round ${round} !! ${event.toolName}: ${event.message ?? "failed"}\n`;
333
+ const elapsed = typeof event.elapsedMs === "number" ? ` ${event.elapsedMs}ms` : "";
334
+ return `[nolo:tool] #${round} !! ${event.toolName}${elapsed}: ${event.message ?? "failed"}\n`;
354
335
  }
355
- return `[nolo:tool] round ${round} <- ${event.toolName} (${event.toolCallId})\n`;
336
+ const elapsed = typeof event.elapsedMs === "number" ? ` ${event.elapsedMs}ms` : "";
337
+ const summary = event.summary ? ` ${event.summary}` : "";
338
+ return `[nolo:tool] #${round} <- ${event.toolName}${elapsed}${summary}\n`;
339
+ }
340
+
341
+ function formatToolJsonEvent(event: LocalAgentToolEvent) {
342
+ return `${JSON.stringify({
343
+ schemaVersion: 1,
344
+ type: event.type,
345
+ round: event.round + 1,
346
+ tool: event.toolName,
347
+ toolCallId: event.toolCallId,
348
+ ...(event.argumentsPreview ? { argsPreview: event.argumentsPreview } : {}),
349
+ ...(typeof event.elapsedMs === "number" ? { elapsedMs: event.elapsedMs } : {}),
350
+ ...(event.summary ? { summary: event.summary } : {}),
351
+ ...(event.message ? { message: event.message } : {}),
352
+ })}\n`;
356
353
  }
357
354
 
358
355
  function shouldAttemptAutoLocal(options: RunAgentTurnOptions) {
@@ -629,7 +626,6 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
629
626
  : {}),
630
627
  ...(options.parentDialogId ? { parentDialogId: options.parentDialogId } : {}),
631
628
  ...(options.background ? { background: true } : {}),
632
- ...(typeof options.maxToolRounds === "number" ? { maxRounds: options.maxToolRounds } : {}),
633
629
  ...(typeof options.timeoutMs === "number" ? { timeoutMs: options.timeoutMs } : {}),
634
630
  stream: !options.noStream && !options.background,
635
631
  }),
@@ -717,7 +713,7 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
717
713
  }
718
714
 
719
715
  async function runInjectedLocalAgentTurn(options: RunAgentTurnOptions) {
720
- return runLocalAgentTurnWithAutoRoundExpansion(options, { reportFailure: true });
716
+ return runLocalAgentTurnForCli(options, { reportFailure: true });
721
717
  }
722
718
 
723
719
  async function refreshMissingLocalAgentConfig(options: RunAgentTurnOptions) {
@@ -727,53 +723,6 @@ async function refreshMissingLocalAgentConfig(options: RunAgentTurnOptions) {
727
723
  return Boolean(agentConfig);
728
724
  }
729
725
 
730
- async function runLocalAgentTurnWithAutoRoundExpansion(
731
- options: RunAgentTurnOptions,
732
- settings: { reportFailure: boolean }
733
- ) {
734
- const maxRetryLimit = resolveLocalToolRoundRetryLimit(options);
735
- let currentMaxToolRounds = options.maxToolRounds;
736
- let lastResult: Awaited<ReturnType<typeof runLocalAgentTurnForCli>> | null = null;
737
-
738
- while (true) {
739
- const attemptOptions = {
740
- ...options,
741
- ...(typeof currentMaxToolRounds === "number" ? { maxToolRounds: currentMaxToolRounds } : {}),
742
- };
743
- const result = await runLocalAgentTurnForCli(attemptOptions, { reportFailure: false });
744
- lastResult = result;
745
- if (result.exitCode === 0) return result;
746
-
747
- const exhaustedRounds = parseLocalToolRoundLimitError(result.localError);
748
- if (exhaustedRounds === null) break;
749
-
750
- const nextMaxToolRounds = nextLocalToolRoundLimit(exhaustedRounds);
751
- if (nextMaxToolRounds > maxRetryLimit) break;
752
-
753
- options.output.write(
754
- `[nolo] Local agent used all ${exhaustedRounds} tool rounds; retrying with ${nextMaxToolRounds}.\n`
755
- );
756
- currentMaxToolRounds = nextMaxToolRounds;
757
- }
758
-
759
- if (settings.reportFailure && lastResult?.localError) {
760
- options.output.write(
761
- `[nolo] Local agent run failed: ${
762
- lastResult.localError instanceof Error ? lastResult.localError.message : String(lastResult.localError)
763
- }\n`
764
- );
765
- await recordTaskRunOutcome({
766
- options,
767
- status: "failed",
768
- errorCode: "LOCAL_AGENT_FAILED",
769
- resultSummary:
770
- lastResult.localError instanceof Error ? lastResult.localError.message : String(lastResult.localError),
771
- toolEvidence: lastResult.toolEvidence,
772
- });
773
- }
774
- return lastResult ?? { exitCode: 1 };
775
- }
776
-
777
726
  async function runLocalAgentTurnForCli(
778
727
  options: RunAgentTurnOptions,
779
728
  settings: { reportFailure: boolean }
@@ -790,6 +739,7 @@ async function runLocalAgentTurnForCli(
790
739
  try {
791
740
  const traceLocalTools = shouldTraceLocalTools(options);
792
741
  const collectToolEvidence = shouldCollectLocalToolEvidence(options);
742
+ const eventMode = resolveAgentEventMode(options);
793
743
  const result = await runLocalAgentTurn({
794
744
  adapter,
795
745
  agentRef: options.agentKey,
@@ -804,7 +754,6 @@ async function runLocalAgentTurnForCli(
804
754
  parentDialogId: options.parentDialogId,
805
755
  background: options.background,
806
756
  noStream: options.noStream,
807
- maxToolRounds: options.maxToolRounds,
808
757
  ...(typeof options.timeoutMs === "number" ? { timeoutMs: options.timeoutMs } : {}),
809
758
  ...(traceLocalTools || collectToolEvidence
810
759
  ? {
@@ -812,7 +761,11 @@ async function runLocalAgentTurnForCli(
812
761
  if (collectToolEvidence) toolEvents.push(event);
813
762
  if (traceLocalTools) {
814
763
  spinner.stop();
815
- options.output.write(formatToolTraceEvent(event));
764
+ options.output.write(
765
+ eventMode === "jsonl"
766
+ ? formatToolJsonEvent(event)
767
+ : formatToolTraceEvent(event)
768
+ );
816
769
  }
817
770
  },
818
771
  }
@@ -968,7 +921,7 @@ export async function runAgentTurn(options: RunAgentTurnOptions) {
968
921
  if (runtimeMode === "auto" && shouldAttemptAutoLocal(options)) {
969
922
  const skipLocal = await shouldSkipAutoLocalForServerPlatformTools(options);
970
923
  if (!skipLocal) {
971
- const localResult = await runLocalAgentTurnWithAutoRoundExpansion(options, { reportFailure: false });
924
+ const localResult = await runLocalAgentTurnForCli(options, { reportFailure: false });
972
925
  if (localResult.exitCode === 0) {
973
926
  return {
974
927
  exitCode: localResult.exitCode,
@@ -982,7 +935,7 @@ export async function runAgentTurn(options: RunAgentTurnOptions) {
982
935
  try {
983
936
  const refreshed = await refreshMissingLocalAgentConfig(options);
984
937
  if (refreshed) {
985
- const retriedLocalResult = await runLocalAgentTurnWithAutoRoundExpansion(options, {
938
+ const retriedLocalResult = await runLocalAgentTurnForCli(options, {
986
939
  reportFailure: false,
987
940
  });
988
941
  if (retriedLocalResult.exitCode === 0) {