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,16 +1,30 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { runLocalAgentTurn } from "../agentRuntimeLocal";
4
+ import type { LocalAgentToolEvent } from "../agent-runtime/localLoop";
4
5
  import type { AgentRuntimeHostAdapter, AgentRuntimeRequestedMode } from "../agentRuntimeLocal";
5
6
  import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
6
7
  import { createStreamingTextWriter } from "./streamingOutput";
8
+ import { prependTaskRunPrompt, type TaskRunPromptContext } from "./taskRunPrompt";
7
9
 
8
10
  type EnvLike = Record<string, string | undefined>;
9
11
 
10
- type OutputLike = {
11
- write(chunk: string): unknown;
12
- };
13
-
12
+ type OutputLike = {
13
+ write(chunk: string): unknown;
14
+ };
15
+
16
+ type TaskRunAgentRole = "pm" | "frontend" | "fullstack" | "reviewer" | "codex";
17
+ type TaskRunReviewStatus = "passed" | "needs_changes" | "blocked";
18
+ type TaskRunToolEvidence = {
19
+ toolsUsed: string[];
20
+ summary: {
21
+ callCount: number;
22
+ resultCount: number;
23
+ errorCount: number;
24
+ rounds: number;
25
+ };
26
+ };
27
+
14
28
  type RunAgentTurnOptions = {
15
29
  agentName: string;
16
30
  agentKey: string;
@@ -18,13 +32,32 @@ type RunAgentTurnOptions = {
18
32
  message: string;
19
33
  imageUrls?: string[];
20
34
  continueDialogId?: string;
21
- scriptDir: string;
35
+ spaceId?: string;
36
+ category?: string;
37
+ inheritedFromDialogKey?: string;
38
+ parentDialogId?: string;
39
+ background?: boolean;
40
+ noStream?: boolean;
41
+ noDefaultTestRoot?: boolean;
42
+ scriptDir: string;
22
43
  env: EnvLike;
23
44
  output: OutputLike;
24
45
  runtimeMode?: AgentRuntimeRequestedMode;
25
46
  localRuntimeAdapter?: AgentRuntimeHostAdapter;
26
- localRuntimeAdapterFactory?: (env: EnvLike) => AgentRuntimeHostAdapter;
47
+ localRuntimeAdapterFactory?: (env: EnvLike, options?: { cwd?: string }) => AgentRuntimeHostAdapter;
27
48
  localRuntimeCwd?: string;
49
+ maxToolRounds?: number;
50
+ timeoutMs?: number;
51
+ traceTools?: boolean;
52
+ taskRunContext?: TaskRunPromptContext;
53
+ taskRunRecorder?: (args: {
54
+ options: RunAgentTurnOptions;
55
+ status: "completed" | "failed";
56
+ dialogId?: string;
57
+ resultSummary?: string;
58
+ errorCode?: string;
59
+ toolEvidence?: TaskRunToolEvidence;
60
+ }) => Promise<void>;
28
61
  scriptPathExists?: (path: string) => boolean;
29
62
  fetchImpl?: typeof fetch;
30
63
  };
@@ -34,14 +67,31 @@ export type RunAgentTurnResult = {
34
67
  dialogId?: string;
35
68
  };
36
69
 
37
- type ScriptBridgeDecision = {
38
- hasAuthToken: boolean;
39
- scriptPathExists: boolean;
40
- };
41
-
42
- export function shouldUseScriptBridge(decision: ScriptBridgeDecision) {
43
- return !decision.hasAuthToken && decision.scriptPathExists;
44
- }
70
+ type ScriptBridgeDecision = {
71
+ hasAuthToken: boolean;
72
+ scriptPathExists: boolean;
73
+ };
74
+
75
+ const SERVER_PLATFORM_TOOL_NAMES = new Set([
76
+ "addTableRow",
77
+ "addTableRows",
78
+ "deleteTableRow",
79
+ "deleteTableRows",
80
+ "queryTableRows",
81
+ "streamParallelAgents",
82
+ "taskRun",
83
+ "updateTableRow",
84
+ "updateTableRows",
85
+ ]);
86
+
87
+ export function shouldUseScriptBridge(decision: ScriptBridgeDecision) {
88
+ return !decision.hasAuthToken && decision.scriptPathExists;
89
+ }
90
+
91
+ export function findServerPlatformTools(toolNames?: string[]) {
92
+ if (!Array.isArray(toolNames)) return [];
93
+ return toolNames.filter((toolName) => SERVER_PLATFORM_TOOL_NAMES.has(toolName));
94
+ }
45
95
 
46
96
  function resolveAuthToken(env: EnvLike) {
47
97
  return env.AUTH_TOKEN || env.AUTH || env.BENCHMARK_AUTH_TOKEN || "";
@@ -63,9 +113,36 @@ function buildDefaultLocalRuntimeAdapter(options: RunAgentTurnOptions) {
63
113
  env: options.env,
64
114
  fetchImpl: options.fetchImpl,
65
115
  cwd: options.localRuntimeCwd,
116
+ output: options.output,
66
117
  });
67
118
  }
68
119
 
120
+ function resolveLocalRuntimeAdapter(options: RunAgentTurnOptions) {
121
+ return (
122
+ options.localRuntimeAdapter ||
123
+ options.localRuntimeAdapterFactory?.(options.env, { cwd: options.localRuntimeCwd }) ||
124
+ buildDefaultLocalRuntimeAdapter(options)
125
+ );
126
+ }
127
+
128
+ async function shouldSkipAutoLocalForServerPlatformTools(options: RunAgentTurnOptions) {
129
+ const adapter = resolveLocalRuntimeAdapter(options);
130
+ if (!adapter) return false;
131
+ let agentConfig;
132
+ try {
133
+ agentConfig = await adapter.loadAgentConfig(options.agentKey);
134
+ } catch {
135
+ return false;
136
+ }
137
+ const serverTools = findServerPlatformTools(agentConfig?.toolNames);
138
+ if (serverTools.length === 0) return false;
139
+ options.output.write(
140
+ `[nolo] auto runtime: skipping local runtime because ${options.agentKey} declares server platform tools ` +
141
+ `(${serverTools.join(", ")}). Use --local explicitly to force local workspace tools.\n`
142
+ );
143
+ return true;
144
+ }
145
+
69
146
  function buildUserInputContent(message: string, imageUrls: string[] = []) {
70
147
  if (imageUrls.length === 0) return message;
71
148
  return [
@@ -77,6 +154,74 @@ function buildUserInputContent(message: string, imageUrls: string[] = []) {
77
154
  ];
78
155
  }
79
156
 
157
+ function shouldTraceLocalTools(options: RunAgentTurnOptions) {
158
+ return Boolean(options.traceTools || options.env.NOLO_TRACE_TOOLS === "1");
159
+ }
160
+
161
+ function shouldCollectLocalToolEvidence(options: RunAgentTurnOptions) {
162
+ return Boolean(options.taskRunContext?.rowDbKey);
163
+ }
164
+
165
+ function summarizeLocalToolEvidence(events: LocalAgentToolEvent[]): TaskRunToolEvidence | undefined {
166
+ if (events.length === 0) return undefined;
167
+ const toolsUsed = [
168
+ ...new Set(
169
+ events
170
+ .filter((event) => event.type === "tool-call")
171
+ .map((event) => event.toolName)
172
+ .filter(Boolean)
173
+ ),
174
+ ];
175
+ const rounds = new Set(events.map((event) => event.round)).size;
176
+ return {
177
+ toolsUsed,
178
+ summary: {
179
+ callCount: events.filter((event) => event.type === "tool-call").length,
180
+ resultCount: events.filter((event) => event.type === "tool-result").length,
181
+ errorCount: events.filter((event) => event.type === "tool-error").length,
182
+ rounds,
183
+ },
184
+ };
185
+ }
186
+
187
+ const DEFAULT_LOCAL_AGENT_TOOL_ROUND_RETRY_LIMIT = 128;
188
+
189
+ function parseLocalToolRoundLimitError(error: unknown) {
190
+ const message = error instanceof Error ? error.message : String(error);
191
+ const match = message.match(/Local agent exceeded max tool rounds:\s*(\d+)/);
192
+ if (!match) return null;
193
+ return Number(match[1]);
194
+ }
195
+
196
+ function parsePositiveInteger(value: string | undefined) {
197
+ if (!value) return null;
198
+ const parsed = Number(value);
199
+ if (!Number.isInteger(parsed) || parsed <= 0) return null;
200
+ return parsed;
201
+ }
202
+
203
+ function resolveLocalToolRoundRetryLimit(options: RunAgentTurnOptions) {
204
+ return (
205
+ parsePositiveInteger(options.env.NOLO_LOCAL_MAX_TOOL_ROUNDS_LIMIT) ??
206
+ DEFAULT_LOCAL_AGENT_TOOL_ROUND_RETRY_LIMIT
207
+ );
208
+ }
209
+
210
+ function nextLocalToolRoundLimit(current: number) {
211
+ return current <= 0 ? 1 : current * 2;
212
+ }
213
+
214
+ function formatToolTraceEvent(event: LocalAgentToolEvent) {
215
+ const round = event.round + 1;
216
+ if (event.type === "tool-call") {
217
+ return `[nolo:tool] round ${round} -> ${event.toolName} (${event.toolCallId})\n`;
218
+ }
219
+ if (event.type === "tool-error") {
220
+ return `[nolo:tool] round ${round} !! ${event.toolName}: ${event.message ?? "failed"}\n`;
221
+ }
222
+ return `[nolo:tool] round ${round} <- ${event.toolName} (${event.toolCallId})\n`;
223
+ }
224
+
80
225
  function shouldAttemptAutoLocal(options: RunAgentTurnOptions) {
81
226
  if (options.localRuntimeAdapter || options.localRuntimeAdapterFactory) return true;
82
227
  return Boolean(
@@ -87,6 +232,174 @@ function shouldAttemptAutoLocal(options: RunAgentTurnOptions) {
87
232
  options.env.NOLO_LOCAL_AGENT_KEY
88
233
  );
89
234
  }
235
+
236
+ function inferTaskRunAgentRole(agentRef: string): TaskRunAgentRole {
237
+ const normalized = agentRef.toLowerCase();
238
+ if (normalized.includes("project-manager") || normalized.includes("manager")) return "pm";
239
+ if (normalized.includes("frontend")) return "frontend";
240
+ if (normalized.includes("fullstack")) return "fullstack";
241
+ if (normalized.includes("review")) return "reviewer";
242
+ return "codex";
243
+ }
244
+
245
+ export function classifyTaskRunReviewStatus(summary?: string): TaskRunReviewStatus | undefined {
246
+ const normalized = summary?.toLowerCase().trim();
247
+ if (!normalized) return undefined;
248
+
249
+ const explicit = normalized.match(/review\s+decision\s*:\s*(passed|needs_changes|blocked)/);
250
+ if (explicit?.[1]) return explicit[1] as TaskRunReviewStatus;
251
+
252
+ if (/\b(blocked|cannot review|unable to review)\b|无法审查|阻塞/.test(normalized)) {
253
+ return "blocked";
254
+ }
255
+ if (
256
+ /\b(needs changes|request changes|changes requested|not approved)\b|需要修改|需修改|发现问题/.test(
257
+ normalized
258
+ )
259
+ ) {
260
+ return "needs_changes";
261
+ }
262
+ if (/\b(approved|lgtm|no issues|passed)\b|通过|无问题/.test(normalized)) {
263
+ return "passed";
264
+ }
265
+ return undefined;
266
+ }
267
+
268
+ async function runTaskRunCli(args: {
269
+ options: RunAgentTurnOptions;
270
+ cliArgs: string[];
271
+ }) {
272
+ const scriptPath = join(args.options.scriptDir, "taskRun.ts");
273
+ const proc = Bun.spawn({
274
+ cmd: [process.execPath, scriptPath, ...args.cliArgs],
275
+ stdout: "pipe",
276
+ stderr: "pipe",
277
+ env: {
278
+ ...process.env,
279
+ ...args.options.env,
280
+ BASE_URL: args.options.serverUrl,
281
+ },
282
+ });
283
+ const [stdout, stderr, exitCode] = await Promise.all([
284
+ new Response(proc.stdout).text(),
285
+ new Response(proc.stderr).text(),
286
+ proc.exited,
287
+ ]);
288
+ if (exitCode !== 0) {
289
+ throw new Error((stderr || stdout || `taskRun.ts exited ${exitCode}`).trim());
290
+ }
291
+ }
292
+
293
+ async function defaultTaskRunRecorder(args: {
294
+ options: RunAgentTurnOptions;
295
+ status: "completed" | "failed";
296
+ dialogId?: string;
297
+ resultSummary?: string;
298
+ errorCode?: string;
299
+ toolEvidence?: TaskRunToolEvidence;
300
+ }) {
301
+ const context = args.options.taskRunContext;
302
+ if (!context?.rowDbKey) return;
303
+ const role = inferTaskRunAgentRole(args.options.agentName || args.options.agentKey);
304
+ if (role === "reviewer") {
305
+ await runTaskRunCli({
306
+ options: args.options,
307
+ cliArgs: [
308
+ "request-review",
309
+ "--row-dbkey",
310
+ context.rowDbKey,
311
+ "--base-url",
312
+ args.options.serverUrl,
313
+ "--reviewer-agent-key",
314
+ args.options.agentKey,
315
+ ...(context.artifactIds?.length ? ["--artifact-ids", context.artifactIds.join(",")] : []),
316
+ ...(args.dialogId ? ["--dialog-id", args.dialogId] : []),
317
+ ],
318
+ });
319
+ }
320
+ await runTaskRunCli({
321
+ options: args.options,
322
+ cliArgs: [
323
+ "record-agent-run",
324
+ "--row-dbkey",
325
+ context.rowDbKey,
326
+ "--base-url",
327
+ args.options.serverUrl,
328
+ "--agent-key",
329
+ args.options.agentKey,
330
+ "--role",
331
+ role,
332
+ "--run-status",
333
+ args.status,
334
+ ...(context.workItemId ? ["--work-item-id", context.workItemId] : []),
335
+ ...(args.dialogId ? ["--dialog-id", args.dialogId] : []),
336
+ ...(args.resultSummary ? ["--result-summary", args.resultSummary.slice(0, 240)] : []),
337
+ ...(args.errorCode ? ["--error-code", args.errorCode] : []),
338
+ ...(args.toolEvidence?.toolsUsed?.length
339
+ ? ["--tools-used", args.toolEvidence.toolsUsed.join(",")]
340
+ : []),
341
+ ...(args.toolEvidence?.summary
342
+ ? ["--tool-trace-summary", JSON.stringify(args.toolEvidence.summary)]
343
+ : []),
344
+ ],
345
+ });
346
+ if (context.workItemId && args.dialogId) {
347
+ await runTaskRunCli({
348
+ options: args.options,
349
+ cliArgs: [
350
+ "update-work-item",
351
+ "--row-dbkey",
352
+ context.rowDbKey,
353
+ "--base-url",
354
+ args.options.serverUrl,
355
+ "--id",
356
+ context.workItemId,
357
+ "--dialog-ids",
358
+ args.dialogId,
359
+ ],
360
+ });
361
+ }
362
+ if (role === "reviewer") {
363
+ const reviewStatus =
364
+ args.status === "failed" ? "blocked" : classifyTaskRunReviewStatus(args.resultSummary);
365
+ if (reviewStatus) {
366
+ await runTaskRunCli({
367
+ options: args.options,
368
+ cliArgs: [
369
+ "review-result",
370
+ "--row-dbkey",
371
+ context.rowDbKey,
372
+ "--base-url",
373
+ args.options.serverUrl,
374
+ "--review-status",
375
+ reviewStatus,
376
+ "--reviewer-agent-key",
377
+ args.options.agentKey,
378
+ ...(context.artifactIds?.length ? ["--artifact-ids", context.artifactIds.join(",")] : []),
379
+ ...(args.dialogId ? ["--dialog-id", args.dialogId] : []),
380
+ ],
381
+ });
382
+ }
383
+ }
384
+ }
385
+
386
+ async function recordTaskRunOutcome(args: {
387
+ options: RunAgentTurnOptions;
388
+ status: "completed" | "failed";
389
+ dialogId?: string;
390
+ resultSummary?: string;
391
+ errorCode?: string;
392
+ toolEvidence?: TaskRunToolEvidence;
393
+ }) {
394
+ if (!args.options.taskRunContext?.rowDbKey) return;
395
+ try {
396
+ await (args.options.taskRunRecorder ?? defaultTaskRunRecorder)(args);
397
+ } catch (error) {
398
+ args.options.output.write(
399
+ `[nolo] task-run writeback failed: ${error instanceof Error ? error.message : String(error)}\n`
400
+ );
401
+ }
402
+ }
90
403
 
91
404
  function formatUsage(usage: any, dialogId: unknown) {
92
405
  const parts: string[] = [];
@@ -134,12 +447,32 @@ async function runScriptBridge(options: RunAgentTurnOptions, scriptPath: string)
134
447
  options.agentKey,
135
448
  "--server",
136
449
  options.serverUrl,
137
- "--msg",
138
- options.message,
139
- "--no-default-test-root",
140
- ...(options.continueDialogId
141
- ? ["--continue", options.continueDialogId]
142
- : []),
450
+ "--msg",
451
+ options.message,
452
+ ...(options.taskRunContext?.rowDbKey
453
+ ? ["--task-row-dbkey", options.taskRunContext.rowDbKey]
454
+ : []),
455
+ ...(options.taskRunContext?.taskRunId
456
+ ? ["--task-run-id", options.taskRunContext.taskRunId]
457
+ : []),
458
+ ...(options.taskRunContext?.workItemId
459
+ ? ["--work-item-id", options.taskRunContext.workItemId]
460
+ : []),
461
+ ...(options.taskRunContext?.artifactIds?.length
462
+ ? ["--artifact-ids", options.taskRunContext.artifactIds.join(",")]
463
+ : []),
464
+ ...(options.noDefaultTestRoot ? ["--no-default-test-root"] : []),
465
+ ...(options.continueDialogId
466
+ ? ["--continue", options.continueDialogId]
467
+ : []),
468
+ ...(options.spaceId ? ["--space", options.spaceId] : []),
469
+ ...(options.category ? ["--category", options.category] : []),
470
+ ...(options.inheritedFromDialogKey
471
+ ? ["--inherit-from-dialog", options.inheritedFromDialogKey]
472
+ : []),
473
+ ...(options.background ? ["--bg"] : []),
474
+ ...(typeof options.timeoutMs === "number" ? ["--timeout-ms", String(options.timeoutMs)] : []),
475
+ ...(options.noStream ? ["--no-stream"] : []),
143
476
  ],
144
477
  stdin: "inherit",
145
478
  stdout: "inherit",
@@ -151,12 +484,12 @@ async function runScriptBridge(options: RunAgentTurnOptions, scriptPath: string)
151
484
  },
152
485
  });
153
486
 
154
- const exitCode = await proc.exited;
155
- if (exitCode !== 0) {
156
- options.output.write(`\n[nolo] Agent run exited with code ${exitCode}.\n`);
157
- }
158
- return { exitCode };
159
- }
487
+ const exitCode = await proc.exited;
488
+ if (exitCode !== 0) {
489
+ options.output.write(`\n[nolo] Agent run exited with code ${exitCode}.\n`);
490
+ }
491
+ return { exitCode };
492
+ }
160
493
 
161
494
  async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string) {
162
495
  options.output.write(`\n${options.agentName} -> working...\n`);
@@ -172,7 +505,10 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
172
505
  },
173
506
  body: JSON.stringify({
174
507
  agentKey: options.agentKey,
175
- userInput: buildUserInputContent(options.message, options.imageUrls),
508
+ userInput: buildUserInputContent(
509
+ prependTaskRunPrompt(options.message, options.taskRunContext),
510
+ options.imageUrls
511
+ ),
176
512
  runtimeContext: {
177
513
  surface: "cli",
178
514
  host: "terminal",
@@ -180,21 +516,37 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
180
516
  entrypoint: "nolo-cli",
181
517
  capabilities: ["text-io", "streaming", "slash-commands"],
182
518
  },
183
- ...(options.continueDialogId
184
- ? { continueDialogId: options.continueDialogId }
185
- : {}),
186
- stream: true,
187
- }),
519
+ ...(options.continueDialogId
520
+ ? { continueDialogId: options.continueDialogId }
521
+ : {}),
522
+ ...(options.spaceId ? { spaceId: options.spaceId } : {}),
523
+ ...(options.category ? { category: options.category } : {}),
524
+ ...(options.inheritedFromDialogKey
525
+ ? { inheritedFromDialogKey: options.inheritedFromDialogKey }
526
+ : {}),
527
+ ...(options.parentDialogId ? { parentDialogId: options.parentDialogId } : {}),
528
+ ...(options.background ? { background: true } : {}),
529
+ ...(typeof options.maxToolRounds === "number" ? { maxRounds: options.maxToolRounds } : {}),
530
+ ...(typeof options.timeoutMs === "number" ? { timeoutMs: options.timeoutMs } : {}),
531
+ stream: !options.noStream && !options.background,
532
+ }),
188
533
  });
189
534
  } catch (error) {
190
535
  options.output.write(buildTransportErrorHint(options.serverUrl, error));
191
536
  return { exitCode: 1 };
192
537
  }
193
538
 
194
- const contentType = res.headers.get("content-type") || "";
195
- if (contentType.includes("text/event-stream") && res.body) {
196
- return readStreamingAgentRun(options, res);
197
- }
539
+ const contentType = res.headers.get("content-type") || "";
540
+ if (contentType.includes("text/event-stream") && res.body) {
541
+ const result = await readStreamingAgentRun(options, res);
542
+ await recordTaskRunOutcome({
543
+ options,
544
+ status: result.exitCode === 0 ? "completed" : "failed",
545
+ dialogId: result.dialogId,
546
+ ...(result.exitCode !== 0 ? { errorCode: "STREAM_FAILED" } : {}),
547
+ });
548
+ return result;
549
+ }
198
550
 
199
551
  let data: any = {};
200
552
  try {
@@ -203,13 +555,19 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
203
555
  data = {};
204
556
  }
205
557
 
206
- if (!res.ok) {
207
- options.output.write(`[nolo] Agent request failed: HTTP ${res.status}\n`);
208
- if (data?.error || data?.message) {
209
- options.output.write(`${data.error || data.message}\n`);
210
- }
211
- return { exitCode: 1 };
212
- }
558
+ if (!res.ok) {
559
+ options.output.write(`[nolo] Agent request failed: HTTP ${res.status}\n`);
560
+ if (data?.error || data?.message) {
561
+ options.output.write(`${data.error || data.message}\n`);
562
+ }
563
+ await recordTaskRunOutcome({
564
+ options,
565
+ status: "failed",
566
+ errorCode: `HTTP_${res.status}`,
567
+ resultSummary: String(data?.error || data?.message || "").trim() || undefined,
568
+ });
569
+ return { exitCode: 1 };
570
+ }
213
571
 
214
572
  const content = String(data?.content ?? data?.message ?? "").trim();
215
573
  if (content) {
@@ -218,40 +576,112 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
218
576
  options.output.write(`\n${options.agentName} > (no text response)\n`);
219
577
  }
220
578
 
221
- const usage = formatUsage(data?.usage, data?.dialogId);
222
- if (usage && shouldShowUsage(options.env)) options.output.write(`${usage}\n`);
223
- return {
224
- exitCode: 0,
225
- ...(typeof data?.dialogId === "string" && data.dialogId
226
- ? { dialogId: data.dialogId }
227
- : {}),
228
- };
579
+ const usage = formatUsage(data?.usage, data?.dialogId);
580
+ if (usage && shouldShowUsage(options.env)) options.output.write(`${usage}\n`);
581
+ const result = {
582
+ exitCode: 0,
583
+ ...(typeof data?.dialogId === "string" && data.dialogId
584
+ ? { dialogId: data.dialogId }
585
+ : {}),
586
+ };
587
+ await recordTaskRunOutcome({
588
+ options,
589
+ status: "completed",
590
+ dialogId: result.dialogId,
591
+ resultSummary: content || undefined,
592
+ });
593
+ return result;
229
594
  }
230
595
 
231
596
  async function runInjectedLocalAgentTurn(options: RunAgentTurnOptions) {
232
- return runLocalAgentTurnForCli(options, { reportFailure: true });
597
+ return runLocalAgentTurnWithAutoRoundExpansion(options, { reportFailure: true });
598
+ }
599
+
600
+ async function runLocalAgentTurnWithAutoRoundExpansion(
601
+ options: RunAgentTurnOptions,
602
+ settings: { reportFailure: boolean }
603
+ ) {
604
+ const maxRetryLimit = resolveLocalToolRoundRetryLimit(options);
605
+ let currentMaxToolRounds = options.maxToolRounds;
606
+ let lastResult: Awaited<ReturnType<typeof runLocalAgentTurnForCli>> | null = null;
607
+
608
+ while (true) {
609
+ const attemptOptions = {
610
+ ...options,
611
+ ...(typeof currentMaxToolRounds === "number" ? { maxToolRounds: currentMaxToolRounds } : {}),
612
+ };
613
+ const result = await runLocalAgentTurnForCli(attemptOptions, { reportFailure: false });
614
+ lastResult = result;
615
+ if (result.exitCode === 0) return result;
616
+
617
+ const exhaustedRounds = parseLocalToolRoundLimitError(result.localError);
618
+ if (exhaustedRounds === null) break;
619
+
620
+ const nextMaxToolRounds = nextLocalToolRoundLimit(exhaustedRounds);
621
+ if (nextMaxToolRounds > maxRetryLimit) break;
622
+
623
+ options.output.write(
624
+ `[nolo] Local agent used all ${exhaustedRounds} tool rounds; retrying with ${nextMaxToolRounds}.\n`
625
+ );
626
+ currentMaxToolRounds = nextMaxToolRounds;
627
+ }
628
+
629
+ if (settings.reportFailure && lastResult?.localError) {
630
+ options.output.write(
631
+ `[nolo] Local agent run failed: ${
632
+ lastResult.localError instanceof Error ? lastResult.localError.message : String(lastResult.localError)
633
+ }\n`
634
+ );
635
+ await recordTaskRunOutcome({
636
+ options,
637
+ status: "failed",
638
+ errorCode: "LOCAL_AGENT_FAILED",
639
+ resultSummary:
640
+ lastResult.localError instanceof Error ? lastResult.localError.message : String(lastResult.localError),
641
+ toolEvidence: lastResult.toolEvidence,
642
+ });
643
+ }
644
+ return lastResult ?? { exitCode: 1 };
233
645
  }
234
646
 
235
647
  async function runLocalAgentTurnForCli(
236
648
  options: RunAgentTurnOptions,
237
649
  settings: { reportFailure: boolean }
238
650
  ) {
239
- const adapter =
240
- options.localRuntimeAdapter ||
241
- options.localRuntimeAdapterFactory?.(options.env) ||
242
- buildDefaultLocalRuntimeAdapter(options);
651
+ const adapter = resolveLocalRuntimeAdapter(options);
243
652
  if (!adapter) {
244
653
  options.output.write("[nolo] Local runtime was requested but no local runtime adapter is available.\n");
245
654
  return { exitCode: 1 };
246
655
  }
247
656
 
248
657
  options.output.write(`\n${options.agentName} -> working locally...\n`);
658
+ const toolEvents: LocalAgentToolEvent[] = [];
249
659
  try {
660
+ const traceLocalTools = shouldTraceLocalTools(options);
661
+ const collectToolEvidence = shouldCollectLocalToolEvidence(options);
250
662
  const result = await runLocalAgentTurn({
251
663
  adapter,
252
664
  agentRef: options.agentKey,
253
- input: buildUserInputContent(options.message, options.imageUrls),
665
+ input: buildUserInputContent(
666
+ prependTaskRunPrompt(options.message, options.taskRunContext),
667
+ options.imageUrls
668
+ ),
254
669
  continueDialogId: options.continueDialogId,
670
+ spaceId: options.spaceId,
671
+ category: options.category,
672
+ inheritedFromDialogKey: options.inheritedFromDialogKey,
673
+ parentDialogId: options.parentDialogId,
674
+ background: options.background,
675
+ noStream: options.noStream,
676
+ maxToolRounds: options.maxToolRounds,
677
+ ...(traceLocalTools || collectToolEvidence
678
+ ? {
679
+ onToolEvent: (event) => {
680
+ if (collectToolEvidence) toolEvents.push(event);
681
+ if (traceLocalTools) options.output.write(formatToolTraceEvent(event));
682
+ },
683
+ }
684
+ : {}),
255
685
  });
256
686
  const content = result.content.trim();
257
687
  if (content) {
@@ -259,6 +689,13 @@ async function runLocalAgentTurnForCli(
259
689
  } else {
260
690
  options.output.write(`\n${options.agentName} > (no text response)\n`);
261
691
  }
692
+ await recordTaskRunOutcome({
693
+ options,
694
+ status: "completed",
695
+ dialogId: result.dialogId,
696
+ resultSummary: content || undefined,
697
+ toolEvidence: summarizeLocalToolEvidence(toolEvents),
698
+ });
262
699
  return { exitCode: 0, dialogId: result.dialogId };
263
700
  } catch (error) {
264
701
  if (settings.reportFailure) {
@@ -268,7 +705,7 @@ async function runLocalAgentTurnForCli(
268
705
  }\n`
269
706
  );
270
707
  }
271
- return { exitCode: 1, localError: error };
708
+ return { exitCode: 1, localError: error, toolEvidence: summarizeLocalToolEvidence(toolEvents) };
272
709
  }
273
710
  }
274
711
 
@@ -381,12 +818,15 @@ export async function runAgentTurn(options: RunAgentTurnOptions) {
381
818
  }
382
819
 
383
820
  if (runtimeMode === "auto" && shouldAttemptAutoLocal(options)) {
384
- const localResult = await runLocalAgentTurnForCli(options, { reportFailure: false });
385
- if (localResult.exitCode === 0) {
386
- return {
387
- exitCode: localResult.exitCode,
388
- ...(localResult.dialogId ? { dialogId: localResult.dialogId } : {}),
389
- };
821
+ const skipLocal = await shouldSkipAutoLocalForServerPlatformTools(options);
822
+ if (!skipLocal) {
823
+ const localResult = await runLocalAgentTurnWithAutoRoundExpansion(options, { reportFailure: false });
824
+ if (localResult.exitCode === 0) {
825
+ return {
826
+ exitCode: localResult.exitCode,
827
+ ...(localResult.dialogId ? { dialogId: localResult.dialogId } : {}),
828
+ };
829
+ }
390
830
  }
391
831
  }
392
832