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
@@ -4,13 +4,7 @@ import type { AgentRuntimeHostAdapter, AgentRuntimeRequestedMode } from "./agent
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { extname, resolve } from "node:path";
6
6
  import { parseSkillDocProtocol, type WorkflowReferenceConfig } from "../ai/skills/skillDocProtocol";
7
- import { resolveCliAgentKeyInput } from "./agentAliases";
8
- import {
9
- ensureWorkspacePackageLinks,
10
- formatPreparedTaskWorktree,
11
- prepareTaskWorktree,
12
- type PreparedTaskWorktree,
13
- } from "./client/taskWorktree";
7
+ import { MIMO_MONTH_AGENT_KEY, resolveCliAgentKeyInput } from "./agentAliases";
14
8
  import type { TaskRunPromptContext } from "./client/taskRunPrompt";
15
9
 
16
10
  type EnvLike = Record<string, string | undefined>;
@@ -25,8 +19,6 @@ type AgentRunCommandDeps = {
25
19
  output?: OutputLike;
26
20
  runner?: typeof runAgentTurn;
27
21
  localRuntimeAdapterFactory?: (env: EnvLike, options?: { cwd?: string }) => AgentRuntimeHostAdapter;
28
- prepareTaskWorktree?: typeof prepareTaskWorktree;
29
- ensureWorkspacePackageLinks?: typeof ensureWorkspacePackageLinks;
30
22
  inspectLocalRunWorkspace?: typeof inspectLocalRunWorkspace;
31
23
  resolveWorkflowReference?: typeof resolveWorkflowReference;
32
24
  };
@@ -58,12 +50,11 @@ type ParsedAgentRunArgs = {
58
50
  parentDialogId?: string;
59
51
  background: boolean;
60
52
  noStream: boolean;
61
- noDefaultTestRoot: boolean;
62
53
  cwd?: string;
63
- maxToolRounds?: number;
64
54
  timeoutMs?: number;
65
55
  traceTools: boolean;
66
- useTaskWorktree: boolean;
56
+ eventsMode?: "jsonl";
57
+ injectFeatureWorktreeInstruction: boolean;
67
58
  taskRunContext?: TaskRunPromptContext;
68
59
  workflowRef?: string;
69
60
  };
@@ -74,10 +65,6 @@ type ResolvedWorkflowReference = {
74
65
  config?: Partial<WorkflowReferenceConfig>;
75
66
  };
76
67
 
77
- const DEFAULT_TEST_ROOT_DIALOG_ID = "01KN6V3MVEQA4Q1PNKVM2W7YY8";
78
- const DEFAULT_TEST_ROOT_DIALOG_KEY = `dialog-b2e06f801f-${DEFAULT_TEST_ROOT_DIALOG_ID}`;
79
- const DEFAULT_TEST_DIALOG_CATEGORY_ID = "test-dialogs";
80
-
81
68
  function readFlagValue(args: string[], flag: string) {
82
69
  const index = args.indexOf(flag);
83
70
  if (index < 0) return undefined;
@@ -102,11 +89,19 @@ function runtimeModeFromArgs(args: string[]): AgentRuntimeRequestedMode | undefi
102
89
  return undefined;
103
90
  }
104
91
 
105
- function parseNonNegativeInteger(value: string | undefined) {
106
- if (!value) return undefined;
107
- const parsed = Number(value);
108
- if (!Number.isFinite(parsed) || parsed < 0) return undefined;
109
- return Math.floor(parsed);
92
+ function isMonthlyMimoAgentRef(raw: string | undefined, resolved: string) {
93
+ const normalized = raw?.trim().toLowerCase();
94
+ return (
95
+ resolved === MIMO_MONTH_AGENT_KEY ||
96
+ normalized === "fullstack" ||
97
+ normalized === "full-stack" ||
98
+ normalized === "nolo-fullstack" ||
99
+ normalized === "包月mimo" ||
100
+ normalized === "包月mimo2.5" ||
101
+ normalized === "mimo-month" ||
102
+ normalized === "全栈" ||
103
+ normalized === "nolo 全栈工程师"
104
+ );
110
105
  }
111
106
 
112
107
  function parsePositiveInteger(value: string | undefined) {
@@ -123,12 +118,10 @@ function positionalArgs(args: string[]) {
123
118
  "--server",
124
119
  "--auto",
125
120
  "--dangerously-allow-shell",
126
- "--worktree-task",
127
121
  "--trace-tools",
128
122
  "--bg",
129
123
  "--no-stream",
130
124
  "--debug",
131
- "--no-default-test-root",
132
125
  ]);
133
126
  for (let index = 0; index < args.length; index += 1) {
134
127
  const arg = args[index];
@@ -146,7 +139,8 @@ export function parseAgentRunArgs(
146
139
  args: string[],
147
140
  options: ParseAgentRunArgsOptions = {}
148
141
  ): ParsedAgentRunArgs | null {
149
- const agentKey = readFlagValue(args, "--agent") ?? positionalArgs(args)[0];
142
+ const rawAgentKey = readFlagValue(args, "--agent") ?? positionalArgs(args)[0];
143
+ const agentKey = rawAgentKey ? resolveCliAgentKeyInput(rawAgentKey) : undefined;
150
144
  const explicitMsg = readFlagValue(args, "--msg");
151
145
  const msgFile = readFlagValue(args, "--msg-file");
152
146
  const fileMessage = msgFile
@@ -160,25 +154,19 @@ export function parseAgentRunArgs(
160
154
  const spaceId = readFlagValue(args, "--space");
161
155
  const category = readFlagValue(args, "--category");
162
156
  const inheritedFromDialog = readFlagValue(args, "--inherit-from-dialog");
163
- const noDefaultTestRoot = args.includes("--no-default-test-root");
164
157
  const cwd = readFlagValue(args, "--cwd");
165
- const maxToolRounds = parseNonNegativeInteger(
166
- readFlagValue(args, "--max-tool-rounds") ?? readFlagValue(args, "--rounds")
167
- );
168
158
  const timeoutMs = parsePositiveInteger(readFlagValue(args, "--timeout-ms"));
159
+ const rawEventsMode = readFlagValue(args, "--events");
160
+ const eventsMode = rawEventsMode === "jsonl" ? "jsonl" : undefined;
169
161
  const inheritedRef = inheritedFromDialog
170
162
  ? parseDialogReference(inheritedFromDialog)
171
- : !continueDialogId && !noDefaultTestRoot
172
- ? {
173
- dialogKey: DEFAULT_TEST_ROOT_DIALOG_KEY,
174
- dialogId: DEFAULT_TEST_ROOT_DIALOG_ID,
175
- }
176
- : undefined;
163
+ : undefined;
177
164
  const imageUrls = [
178
165
  ...readRepeatedFlagValues(args, "--image"),
179
166
  ...readRepeatedFlagValues(args, "--image-url"),
180
167
  ];
181
- const useTaskWorktree = args.includes("--worktree-task");
168
+ const mimoLocalWorkspaceDefault =
169
+ isMonthlyMimoAgentRef(rawAgentKey, agentKey) && runtimeMode !== "server";
182
170
  const workflowRef = readFlagValue(args, "--workflow");
183
171
  const taskRowDbKey = readFlagValue(args, "--task-row-dbkey");
184
172
  const artifactIds = readFlagValue(args, "--artifact-ids")
@@ -186,12 +174,13 @@ export function parseAgentRunArgs(
186
174
  .map((item) => item.trim())
187
175
  .filter(Boolean);
188
176
  return {
189
- agentKey: resolveCliAgentKeyInput(agentKey),
177
+ agentKey,
190
178
  message,
191
179
  imageUrls,
192
- allowShell: args.includes("--dangerously-allow-shell"),
180
+ allowShell: args.includes("--dangerously-allow-shell") || mimoLocalWorkspaceDefault,
193
181
  traceTools: args.includes("--trace-tools"),
194
- useTaskWorktree,
182
+ ...(eventsMode ? { eventsMode } : {}),
183
+ injectFeatureWorktreeInstruction: mimoLocalWorkspaceDefault,
195
184
  ...(workflowRef ? { workflowRef } : {}),
196
185
  ...(taskRowDbKey
197
186
  ? {
@@ -203,17 +192,19 @@ export function parseAgentRunArgs(
203
192
  },
204
193
  }
205
194
  : {}),
206
- ...(runtimeMode ? { runtimeMode } : useTaskWorktree ? { runtimeMode: "local" as const } : {}),
195
+ ...(runtimeMode
196
+ ? { runtimeMode }
197
+ : mimoLocalWorkspaceDefault || args.includes("--dangerously-allow-shell")
198
+ ? { runtimeMode: "local" as const }
199
+ : {}),
207
200
  background: args.includes("--bg"),
208
201
  noStream: args.includes("--no-stream"),
209
- noDefaultTestRoot,
210
202
  ...(continueDialogId ? { continueDialogId } : {}),
211
203
  ...(!continueDialogId && spaceId ? { spaceId } : {}),
212
- ...(category ? { category } : !continueDialogId && !noDefaultTestRoot ? { category: DEFAULT_TEST_DIALOG_CATEGORY_ID } : {}),
204
+ ...(category ? { category } : {}),
213
205
  ...(inheritedRef?.dialogKey ? { inheritedFromDialogKey: inheritedRef.dialogKey } : {}),
214
206
  ...(inheritedRef?.dialogId ? { parentDialogId: inheritedRef.dialogId } : {}),
215
207
  ...(cwd ? { cwd } : {}),
216
- ...(typeof maxToolRounds === "number" ? { maxToolRounds } : {}),
217
208
  ...(typeof timeoutMs === "number" ? { timeoutMs } : {}),
218
209
  };
219
210
  }
@@ -269,6 +260,20 @@ export function prependWorkflowReferencePrompt(message: string, workflow?: Resol
269
260
  ].join("\n");
270
261
  }
271
262
 
263
+ function prependFeatureWorktreeInstruction(message: string, enabled: boolean) {
264
+ if (!enabled) return message;
265
+ return [
266
+ "Local execution rule:",
267
+ "- You are running in the current git checkout with shell access.",
268
+ "- For read-only checks, smoke tests, or answering questions, stay in the current directory.",
269
+ "- Before developing a new feature or making non-trivial code changes, create a separate git worktree yourself with git worktree and do the edits there.",
270
+ "- Commit and push only when the user explicitly asks or the task requires it.",
271
+ "",
272
+ "User task:",
273
+ message,
274
+ ].join("\n");
275
+ }
276
+
272
277
  function parseDialogReference(rawInput: string) {
273
278
  const normalized = rawInput.trim();
274
279
  if (normalized.startsWith("dialog-")) {
@@ -381,26 +386,20 @@ function resolveServerUrl(env: EnvLike) {
381
386
  return (env.NOLO_SERVER || env.BASE_URL || DEFAULT_NOLO_SERVER_URL).replace(/\/+$/, "");
382
387
  }
383
388
 
384
- function shouldPrepareTaskWorktree(parsed: ParsedAgentRunArgs) {
385
- return !parsed.cwd && (parsed.useTaskWorktree || parsed.allowShell);
386
- }
387
-
388
- function buildTaskWorktreeEnv(args: {
389
+ function buildLocalRunEnv(args: {
389
390
  env: EnvLike;
390
- worktreePath?: string;
391
391
  allowShell: boolean;
392
392
  }) {
393
+ void args.allowShell;
393
394
  return {
394
395
  ...args.env,
395
- ...(args.allowShell ? { NOLO_LOCAL_SHELL_MODE: "worktree" } : {}),
396
- ...(args.worktreePath ? { NOLO_LOCAL_WORKTREE: args.worktreePath } : {}),
397
396
  };
398
397
  }
399
398
 
400
399
  function writeUsage(output: OutputLike) {
401
400
  output.write(
402
- "Usage: nolo agent run <agent> <message> [--local|--server|--auto] [--continue <dialogId>] [--cwd <path>] [--max-tool-rounds <n>]\n" +
403
- " nolo agent run --agent <agent> (--msg <message>|--msg-file <path>) [--image <url-or-path>] [--space <spaceId>] [--category <name>] [--inherit-from-dialog <dialog>] [--task-row-dbkey <key>] [--bg] [--timeout-ms <n>] [--no-stream]\n"
401
+ "Usage: nolo agent run <agent> <message> [--local|--server|--auto] [--continue <dialogId>] [--cwd <path>]\n" +
402
+ " nolo agent run --agent <agent> (--msg <message>|--msg-file <path>) [--image <url-or-path>] [--space <spaceId>] [--category <name>] [--inherit-from-dialog <dialog>] [--task-row-dbkey <key>] [--bg] [--timeout-ms <n>] [--events jsonl] [--no-stream]\n"
404
403
  );
405
404
  }
406
405
 
@@ -426,52 +425,24 @@ export async function runAgentRunCommand(args: string[], deps: AgentRunCommandDe
426
425
  }
427
426
  }
428
427
  let localRuntimeCwd = parsed.cwd;
429
- if (shouldPrepareTaskWorktree(parsed)) {
430
- try {
431
- const preparedWorktree = await (deps.prepareTaskWorktree ?? prepareTaskWorktree)({
432
- agentKey: parsed.agentKey,
433
- env,
434
- });
435
- localRuntimeCwd = preparedWorktree.path;
436
- output.write(formatPreparedTaskWorktree(preparedWorktree));
437
- } catch (error) {
438
- output.write(
439
- "[nolo] Could not prepare a local task worktree.\n" +
440
- "Run from inside a git repository, or set NOLO_LOCAL_WORKTREE to an existing isolated checkout.\n" +
441
- `Reason: ${error instanceof Error ? error.message : String(error)}\n`
442
- );
443
- return 1;
444
- }
445
- }
446
- const shouldBindLocalWorkspace =
447
- !!localRuntimeCwd &&
448
- (parsed.runtimeMode === "local" || parsed.allowShell || parsed.useTaskWorktree);
449
- if (shouldBindLocalWorkspace) {
450
- try {
451
- await (deps.ensureWorkspacePackageLinks ?? ensureWorkspacePackageLinks)(
452
- localRuntimeCwd!
453
- );
454
- } catch (error) {
455
- output.write(
456
- "[nolo] Dependency preflight failed for local task worktree.\n" +
457
- "The worktree may have node_modules entries that point outside the task checkout, or paths that cannot be repaired safely.\n" +
458
- `Reason: ${error instanceof Error ? error.message : String(error)}\n`
459
- );
460
- return 1;
461
- }
428
+ if (!localRuntimeCwd && parsed.runtimeMode === "local") {
429
+ localRuntimeCwd = process.cwd();
462
430
  }
463
- const runEnv = shouldBindLocalWorkspace
464
- ? buildTaskWorktreeEnv({
465
- env,
466
- worktreePath: localRuntimeCwd,
467
- allowShell: parsed.allowShell,
468
- })
469
- : env;
431
+ const runEnv = buildLocalRunEnv({
432
+ env,
433
+ allowShell: parsed.allowShell,
434
+ });
470
435
  const result: RunAgentTurnResult = await runner({
471
436
  agentName: parsed.agentKey,
472
437
  agentKey: parsed.agentKey,
473
438
  serverUrl: resolveServerUrl(env),
474
- message: prependWorkflowReferencePrompt(parsed.message, workflowReference),
439
+ message: prependWorkflowReferencePrompt(
440
+ prependFeatureWorktreeInstruction(
441
+ parsed.message,
442
+ parsed.injectFeatureWorktreeInstruction
443
+ ),
444
+ workflowReference
445
+ ),
475
446
  imageUrls: parsed.imageUrls.map(normalizeCliImageInput),
476
447
  scriptDir: deps.scriptDir,
477
448
  env: runEnv,
@@ -488,10 +459,9 @@ export async function runAgentRunCommand(args: string[], deps: AgentRunCommandDe
488
459
  ...(parsed.parentDialogId ? { parentDialogId: parsed.parentDialogId } : {}),
489
460
  background: parsed.background,
490
461
  noStream: parsed.noStream,
491
- noDefaultTestRoot: parsed.noDefaultTestRoot,
492
- ...(typeof parsed.maxToolRounds === "number" ? { maxToolRounds: parsed.maxToolRounds } : {}),
493
462
  ...(typeof parsed.timeoutMs === "number" ? { timeoutMs: parsed.timeoutMs } : {}),
494
463
  traceTools: parsed.traceTools,
464
+ ...(parsed.eventsMode ? { eventsMode: parsed.eventsMode } : {}),
495
465
  ...(parsed.taskRunContext ? { taskRunContext: parsed.taskRunContext } : {}),
496
466
  });
497
467
 
@@ -5,12 +5,9 @@ export {
5
5
  hasDirectOpenAiCompatibleProvider,
6
6
  createHybridRecordStore,
7
7
  executeLocalToolWithPolicy,
8
- activateWorkspaceSession,
9
- createWorkspaceSession,
10
8
  buildAgentRuntimeAgentLookupKeys,
11
9
  buildAgentRuntimeDialogWritePlan,
12
10
  dialogMessageRecordToAgentRuntimeMessage,
13
- formatWorkspaceSessionActivation,
14
11
  pickAgentRuntimeInferenceOptions,
15
12
  parseOpenAiCompatibleChatCompletionResponse,
16
13
  parsePlatformChatCompletionData,
@@ -23,6 +20,7 @@ export {
23
20
  resolveLocalToolPolicy,
24
21
  resolveOpenAiCompatibleProviderConfig,
25
22
  resolveRequestedRuntimeToolNames,
23
+ resolveRuntimeToolSurfaceForAgent,
26
24
  evaluateShellCommandPolicy,
27
25
  isDestructiveShellCommand,
28
26
  shouldUsePlatformChatProvider,
@@ -44,8 +42,4 @@ export type {
44
42
  HybridRecordKvDb,
45
43
  HybridRecordStore,
46
44
  LocalToolPolicyDecision,
47
- PreparedTaskWorkspace,
48
- PrepareTaskWorkspace,
49
- WorkspaceSession,
50
- WorkspaceSessionMode,
51
45
  } from "../agent-runtime";
@@ -96,14 +96,18 @@ export function resolveAuthToken(
96
96
  }
97
97
 
98
98
  export function parseUserIdFromAuthToken(token: string) {
99
- const payload = token.trim().split(".")[0];
100
- if (!payload) return "";
101
- try {
102
- const parsed = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
103
- return typeof parsed?.userId === "string" ? parsed.userId : "";
104
- } catch {
105
- return "";
99
+ const parts = token.trim().split(".").filter(Boolean);
100
+ const payloadCandidates = parts.length >= 3 ? [parts[1], parts[0]] : [parts[0]];
101
+ for (const payload of payloadCandidates) {
102
+ if (!payload) continue;
103
+ try {
104
+ const parsed = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
105
+ if (typeof parsed?.userId === "string") return parsed.userId;
106
+ } catch {
107
+ // Try the next token shape.
108
+ }
106
109
  }
110
+ return "";
107
111
  }
108
112
 
109
113
  export function readOption(args: string[], flag: string) {
@@ -9,7 +9,7 @@ import {
9
9
  } from "../agentAliases";
10
10
  import type { LocalAgentToolEvent } from "../../agent-runtime/localLoop";
11
11
  import type { AgentRuntimeHostAdapter, AgentRuntimeRequestedMode } from "../agentRuntimeLocal";
12
- import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
12
+ import { createCliLocalRuntimeAdapter, isBuiltinNoloAgentRef } from "./localRuntimeAdapter";
13
13
  import { createStreamingTextWriter } from "./streamingOutput";
14
14
  import { prependTaskRunPrompt, type TaskRunPromptContext } from "./taskRunPrompt";
15
15
 
@@ -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;
@@ -180,10 +179,27 @@ function isKnownServerPlatformAgent(options: RunAgentTurnOptions) {
180
179
  const normalizedKey = normalizeAgentRef(options.agentKey);
181
180
  return Boolean(normalizedKey && KNOWN_SERVER_PLATFORM_AGENT_ALIASES.has(normalizedKey));
182
181
  }
183
-
184
- function resolveAuthToken(env: EnvLike) {
185
- return env.AUTH_TOKEN || env.AUTH || env.BENCHMARK_AUTH_TOKEN || "";
186
- }
182
+
183
+ function isMachineBoundLocalhostCustomProvider(agentConfig: any) {
184
+ const machineId =
185
+ agentConfig?.runtimeBinding && typeof agentConfig.runtimeBinding === "object"
186
+ ? String(agentConfig.runtimeBinding.machineId ?? "").trim()
187
+ : "";
188
+ const providerUrl = typeof agentConfig?.customProviderUrl === "string"
189
+ ? agentConfig.customProviderUrl.trim()
190
+ : "";
191
+ if (!machineId || !providerUrl) return false;
192
+ try {
193
+ const hostname = new URL(providerUrl).hostname.toLowerCase();
194
+ return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1";
195
+ } catch {
196
+ return false;
197
+ }
198
+ }
199
+
200
+ function resolveAuthToken(env: EnvLike) {
201
+ return env.AUTH_TOKEN || env.AUTH || env.BENCHMARK_AUTH_TOKEN || "";
202
+ }
187
203
 
188
204
  function shouldShowUsage(env: EnvLike) {
189
205
  return env.NOLO_DEBUG === "1" || env.NOLO_SHOW_USAGE === "1";
@@ -214,6 +230,10 @@ function resolveLocalRuntimeAdapter(options: RunAgentTurnOptions) {
214
230
  }
215
231
 
216
232
  async function shouldSkipAutoLocalForServerPlatformTools(options: RunAgentTurnOptions) {
233
+ if (isBuiltinNoloAgentRef(options.agentKey)) return false;
234
+ if (options.localRuntimeCwd) {
235
+ return false;
236
+ }
217
237
  if (isKnownServerPlatformAgent(options)) {
218
238
  options.output.write(
219
239
  `[nolo] auto runtime: skipping local runtime because ${options.agentKey} is a known platform agent. ` +
@@ -229,6 +249,13 @@ async function shouldSkipAutoLocalForServerPlatformTools(options: RunAgentTurnOp
229
249
  } catch {
230
250
  return false;
231
251
  }
252
+ if (isMachineBoundLocalhostCustomProvider(agentConfig)) {
253
+ options.output.write(
254
+ `[nolo] auto runtime: skipping local runtime because ${options.agentKey} is a machine-bound localhost custom provider. ` +
255
+ "Use --local explicitly to force the current machine.\n"
256
+ );
257
+ return true;
258
+ }
232
259
  const serverTools = resolveServerPlatformToolNames(agentConfig);
233
260
  if (serverTools.length === 0) return false;
234
261
  options.output.write(
@@ -250,7 +277,14 @@ function buildUserInputContent(message: string, imageUrls: string[] = []) {
250
277
  }
251
278
 
252
279
  function shouldTraceLocalTools(options: RunAgentTurnOptions) {
253
- 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";
254
288
  }
255
289
 
256
290
  function shouldCollectLocalToolEvidence(options: RunAgentTurnOptions) {
@@ -279,15 +313,6 @@ function summarizeLocalToolEvidence(events: LocalAgentToolEvent[]): TaskRunToolE
279
313
  };
280
314
  }
281
315
 
282
- const DEFAULT_LOCAL_AGENT_TOOL_ROUND_RETRY_LIMIT = 512;
283
-
284
- function parseLocalToolRoundLimitError(error: unknown) {
285
- const message = error instanceof Error ? error.message : String(error);
286
- const match = message.match(/Local agent exceeded max tool rounds:\s*(\d+)/);
287
- if (!match) return null;
288
- return Number(match[1]);
289
- }
290
-
291
316
  function isMissingLocalAgentConfigError(error: unknown, agentRef: string) {
292
317
  return Boolean(
293
318
  error &&
@@ -298,37 +323,44 @@ function isMissingLocalAgentConfigError(error: unknown, agentRef: string) {
298
323
  );
299
324
  }
300
325
 
301
- function parsePositiveInteger(value: string | undefined) {
302
- if (!value) return null;
303
- const parsed = Number(value);
304
- if (!Number.isInteger(parsed) || parsed <= 0) return null;
305
- return parsed;
306
- }
307
-
308
- function resolveLocalToolRoundRetryLimit(options: RunAgentTurnOptions) {
309
- return (
310
- parsePositiveInteger(options.env.NOLO_LOCAL_MAX_TOOL_ROUNDS_LIMIT) ??
311
- DEFAULT_LOCAL_AGENT_TOOL_ROUND_RETRY_LIMIT
312
- );
313
- }
314
-
315
- function nextLocalToolRoundLimit(current: number) {
316
- return current <= 0 ? 1 : current * 2;
317
- }
318
-
319
326
  function formatToolTraceEvent(event: LocalAgentToolEvent) {
320
327
  const round = event.round + 1;
328
+ const detail = event.argumentsPreview ? ` ${event.argumentsPreview}` : "";
321
329
  if (event.type === "tool-call") {
322
- return `[nolo:tool] round ${round} -> ${event.toolName} (${event.toolCallId})\n`;
330
+ return `[nolo:tool] #${round} -> ${event.toolName}${detail}\n`;
323
331
  }
324
332
  if (event.type === "tool-error") {
325
- 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`;
326
335
  }
327
- 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`;
328
353
  }
329
354
 
330
355
  function shouldAttemptAutoLocal(options: RunAgentTurnOptions) {
331
356
  if (options.localRuntimeAdapter || options.localRuntimeAdapterFactory) return true;
357
+ if (
358
+ options.env.NOLO_DISABLE_CLI_WORKSPACE_TOOLS !== "1" &&
359
+ isBuiltinNoloAgentRef(options.agentKey) &&
360
+ resolveAuthToken(options.env)
361
+ ) {
362
+ return true;
363
+ }
332
364
  return Boolean(
333
365
  options.env.NOLO_LOCAL_OPENAI_API_KEY ||
334
366
  options.env.OPENAI_API_KEY ||
@@ -594,7 +626,6 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
594
626
  : {}),
595
627
  ...(options.parentDialogId ? { parentDialogId: options.parentDialogId } : {}),
596
628
  ...(options.background ? { background: true } : {}),
597
- ...(typeof options.maxToolRounds === "number" ? { maxRounds: options.maxToolRounds } : {}),
598
629
  ...(typeof options.timeoutMs === "number" ? { timeoutMs: options.timeoutMs } : {}),
599
630
  stream: !options.noStream && !options.background,
600
631
  }),
@@ -632,8 +663,21 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
632
663
 
633
664
  if (!res.ok) {
634
665
  options.output.write(`[nolo] Agent request failed: HTTP ${res.status}\n`);
635
- if (data?.error || data?.message) {
636
- options.output.write(`${data.error || data.message}\n`);
666
+ const errorText = typeof data?.error === "string" ? data.error.trim() : "";
667
+ const messageText = typeof data?.message === "string" ? data.message.trim() : "";
668
+ const reasonText = typeof data?.reason === "string" ? data.reason.trim() : "";
669
+ const codeText = typeof data?.code === "string" ? data.code.trim() : "";
670
+ if (errorText || messageText) {
671
+ options.output.write(`${errorText || messageText}\n`);
672
+ if (messageText && messageText !== errorText) {
673
+ options.output.write(`${messageText}\n`);
674
+ }
675
+ if (codeText && codeText !== errorText && codeText !== messageText) {
676
+ options.output.write(`code=${codeText}\n`);
677
+ }
678
+ if (reasonText && reasonText !== errorText && reasonText !== messageText) {
679
+ options.output.write(`reason=${reasonText}\n`);
680
+ }
637
681
  }
638
682
  await recordTaskRunOutcome({
639
683
  options,
@@ -669,7 +713,7 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
669
713
  }
670
714
 
671
715
  async function runInjectedLocalAgentTurn(options: RunAgentTurnOptions) {
672
- return runLocalAgentTurnWithAutoRoundExpansion(options, { reportFailure: true });
716
+ return runLocalAgentTurnForCli(options, { reportFailure: true });
673
717
  }
674
718
 
675
719
  async function refreshMissingLocalAgentConfig(options: RunAgentTurnOptions) {
@@ -679,53 +723,6 @@ async function refreshMissingLocalAgentConfig(options: RunAgentTurnOptions) {
679
723
  return Boolean(agentConfig);
680
724
  }
681
725
 
682
- async function runLocalAgentTurnWithAutoRoundExpansion(
683
- options: RunAgentTurnOptions,
684
- settings: { reportFailure: boolean }
685
- ) {
686
- const maxRetryLimit = resolveLocalToolRoundRetryLimit(options);
687
- let currentMaxToolRounds = options.maxToolRounds;
688
- let lastResult: Awaited<ReturnType<typeof runLocalAgentTurnForCli>> | null = null;
689
-
690
- while (true) {
691
- const attemptOptions = {
692
- ...options,
693
- ...(typeof currentMaxToolRounds === "number" ? { maxToolRounds: currentMaxToolRounds } : {}),
694
- };
695
- const result = await runLocalAgentTurnForCli(attemptOptions, { reportFailure: false });
696
- lastResult = result;
697
- if (result.exitCode === 0) return result;
698
-
699
- const exhaustedRounds = parseLocalToolRoundLimitError(result.localError);
700
- if (exhaustedRounds === null) break;
701
-
702
- const nextMaxToolRounds = nextLocalToolRoundLimit(exhaustedRounds);
703
- if (nextMaxToolRounds > maxRetryLimit) break;
704
-
705
- options.output.write(
706
- `[nolo] Local agent used all ${exhaustedRounds} tool rounds; retrying with ${nextMaxToolRounds}.\n`
707
- );
708
- currentMaxToolRounds = nextMaxToolRounds;
709
- }
710
-
711
- if (settings.reportFailure && lastResult?.localError) {
712
- options.output.write(
713
- `[nolo] Local agent run failed: ${
714
- lastResult.localError instanceof Error ? lastResult.localError.message : String(lastResult.localError)
715
- }\n`
716
- );
717
- await recordTaskRunOutcome({
718
- options,
719
- status: "failed",
720
- errorCode: "LOCAL_AGENT_FAILED",
721
- resultSummary:
722
- lastResult.localError instanceof Error ? lastResult.localError.message : String(lastResult.localError),
723
- toolEvidence: lastResult.toolEvidence,
724
- });
725
- }
726
- return lastResult ?? { exitCode: 1 };
727
- }
728
-
729
726
  async function runLocalAgentTurnForCli(
730
727
  options: RunAgentTurnOptions,
731
728
  settings: { reportFailure: boolean }
@@ -742,6 +739,7 @@ async function runLocalAgentTurnForCli(
742
739
  try {
743
740
  const traceLocalTools = shouldTraceLocalTools(options);
744
741
  const collectToolEvidence = shouldCollectLocalToolEvidence(options);
742
+ const eventMode = resolveAgentEventMode(options);
745
743
  const result = await runLocalAgentTurn({
746
744
  adapter,
747
745
  agentRef: options.agentKey,
@@ -756,7 +754,6 @@ async function runLocalAgentTurnForCli(
756
754
  parentDialogId: options.parentDialogId,
757
755
  background: options.background,
758
756
  noStream: options.noStream,
759
- maxToolRounds: options.maxToolRounds,
760
757
  ...(typeof options.timeoutMs === "number" ? { timeoutMs: options.timeoutMs } : {}),
761
758
  ...(traceLocalTools || collectToolEvidence
762
759
  ? {
@@ -764,7 +761,11 @@ async function runLocalAgentTurnForCli(
764
761
  if (collectToolEvidence) toolEvents.push(event);
765
762
  if (traceLocalTools) {
766
763
  spinner.stop();
767
- options.output.write(formatToolTraceEvent(event));
764
+ options.output.write(
765
+ eventMode === "jsonl"
766
+ ? formatToolJsonEvent(event)
767
+ : formatToolTraceEvent(event)
768
+ );
768
769
  }
769
770
  },
770
771
  }
@@ -920,7 +921,7 @@ export async function runAgentTurn(options: RunAgentTurnOptions) {
920
921
  if (runtimeMode === "auto" && shouldAttemptAutoLocal(options)) {
921
922
  const skipLocal = await shouldSkipAutoLocalForServerPlatformTools(options);
922
923
  if (!skipLocal) {
923
- const localResult = await runLocalAgentTurnWithAutoRoundExpansion(options, { reportFailure: false });
924
+ const localResult = await runLocalAgentTurnForCli(options, { reportFailure: false });
924
925
  if (localResult.exitCode === 0) {
925
926
  return {
926
927
  exitCode: localResult.exitCode,
@@ -934,7 +935,7 @@ export async function runAgentTurn(options: RunAgentTurnOptions) {
934
935
  try {
935
936
  const refreshed = await refreshMissingLocalAgentConfig(options);
936
937
  if (refreshed) {
937
- const retriedLocalResult = await runLocalAgentTurnWithAutoRoundExpansion(options, {
938
+ const retriedLocalResult = await runLocalAgentTurnForCli(options, {
938
939
  reportFailure: false,
939
940
  });
940
941
  if (retriedLocalResult.exitCode === 0) {