nolo-cli 0.1.18 → 0.1.20

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 +14 -0
  8. package/agent-runtime/hybridRecordStore.ts +147 -0
  9. package/agent-runtime/index.ts +69 -0
  10. package/agent-runtime/localLoop.ts +78 -6
  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 +2 -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 +289 -54
  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 +823 -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 +361 -7
  79. package/client/agentRun.ts +449 -63
  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 +813 -20
  89. package/client/localRuntimeAdapter.ts +279 -232
  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 +237 -0
@@ -1,16 +1,21 @@
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
+
14
19
  type RunAgentTurnOptions = {
15
20
  agentName: string;
16
21
  agentKey: string;
@@ -18,13 +23,31 @@ type RunAgentTurnOptions = {
18
23
  message: string;
19
24
  imageUrls?: string[];
20
25
  continueDialogId?: string;
21
- scriptDir: string;
26
+ spaceId?: string;
27
+ category?: string;
28
+ inheritedFromDialogKey?: string;
29
+ parentDialogId?: string;
30
+ background?: boolean;
31
+ noStream?: boolean;
32
+ noDefaultTestRoot?: boolean;
33
+ scriptDir: string;
22
34
  env: EnvLike;
23
35
  output: OutputLike;
24
36
  runtimeMode?: AgentRuntimeRequestedMode;
25
37
  localRuntimeAdapter?: AgentRuntimeHostAdapter;
26
- localRuntimeAdapterFactory?: (env: EnvLike) => AgentRuntimeHostAdapter;
38
+ localRuntimeAdapterFactory?: (env: EnvLike, options?: { cwd?: string }) => AgentRuntimeHostAdapter;
27
39
  localRuntimeCwd?: string;
40
+ maxToolRounds?: number;
41
+ timeoutMs?: number;
42
+ traceTools?: boolean;
43
+ taskRunContext?: TaskRunPromptContext;
44
+ taskRunRecorder?: (args: {
45
+ options: RunAgentTurnOptions;
46
+ status: "completed" | "failed";
47
+ dialogId?: string;
48
+ resultSummary?: string;
49
+ errorCode?: string;
50
+ }) => Promise<void>;
28
51
  scriptPathExists?: (path: string) => boolean;
29
52
  fetchImpl?: typeof fetch;
30
53
  };
@@ -34,14 +57,31 @@ export type RunAgentTurnResult = {
34
57
  dialogId?: string;
35
58
  };
36
59
 
37
- type ScriptBridgeDecision = {
38
- hasAuthToken: boolean;
39
- scriptPathExists: boolean;
40
- };
41
-
42
- export function shouldUseScriptBridge(decision: ScriptBridgeDecision) {
43
- return !decision.hasAuthToken && decision.scriptPathExists;
44
- }
60
+ type ScriptBridgeDecision = {
61
+ hasAuthToken: boolean;
62
+ scriptPathExists: boolean;
63
+ };
64
+
65
+ const SERVER_PLATFORM_TOOL_NAMES = new Set([
66
+ "addTableRow",
67
+ "addTableRows",
68
+ "deleteTableRow",
69
+ "deleteTableRows",
70
+ "queryTableRows",
71
+ "streamParallelAgents",
72
+ "taskRun",
73
+ "updateTableRow",
74
+ "updateTableRows",
75
+ ]);
76
+
77
+ export function shouldUseScriptBridge(decision: ScriptBridgeDecision) {
78
+ return !decision.hasAuthToken && decision.scriptPathExists;
79
+ }
80
+
81
+ export function findServerPlatformTools(toolNames?: string[]) {
82
+ if (!Array.isArray(toolNames)) return [];
83
+ return toolNames.filter((toolName) => SERVER_PLATFORM_TOOL_NAMES.has(toolName));
84
+ }
45
85
 
46
86
  function resolveAuthToken(env: EnvLike) {
47
87
  return env.AUTH_TOKEN || env.AUTH || env.BENCHMARK_AUTH_TOKEN || "";
@@ -63,9 +103,36 @@ function buildDefaultLocalRuntimeAdapter(options: RunAgentTurnOptions) {
63
103
  env: options.env,
64
104
  fetchImpl: options.fetchImpl,
65
105
  cwd: options.localRuntimeCwd,
106
+ output: options.output,
66
107
  });
67
108
  }
68
109
 
110
+ function resolveLocalRuntimeAdapter(options: RunAgentTurnOptions) {
111
+ return (
112
+ options.localRuntimeAdapter ||
113
+ options.localRuntimeAdapterFactory?.(options.env, { cwd: options.localRuntimeCwd }) ||
114
+ buildDefaultLocalRuntimeAdapter(options)
115
+ );
116
+ }
117
+
118
+ async function shouldSkipAutoLocalForServerPlatformTools(options: RunAgentTurnOptions) {
119
+ const adapter = resolveLocalRuntimeAdapter(options);
120
+ if (!adapter) return false;
121
+ let agentConfig;
122
+ try {
123
+ agentConfig = await adapter.loadAgentConfig(options.agentKey);
124
+ } catch {
125
+ return false;
126
+ }
127
+ const serverTools = findServerPlatformTools(agentConfig?.toolNames);
128
+ if (serverTools.length === 0) return false;
129
+ options.output.write(
130
+ `[nolo] auto runtime: skipping local runtime because ${options.agentKey} declares server platform tools ` +
131
+ `(${serverTools.join(", ")}). Use --local explicitly to force local workspace tools.\n`
132
+ );
133
+ return true;
134
+ }
135
+
69
136
  function buildUserInputContent(message: string, imageUrls: string[] = []) {
70
137
  if (imageUrls.length === 0) return message;
71
138
  return [
@@ -77,6 +144,48 @@ function buildUserInputContent(message: string, imageUrls: string[] = []) {
77
144
  ];
78
145
  }
79
146
 
147
+ function shouldTraceLocalTools(options: RunAgentTurnOptions) {
148
+ return Boolean(options.traceTools || options.env.NOLO_TRACE_TOOLS === "1");
149
+ }
150
+
151
+ const DEFAULT_LOCAL_AGENT_TOOL_ROUND_RETRY_LIMIT = 128;
152
+
153
+ function parseLocalToolRoundLimitError(error: unknown) {
154
+ const message = error instanceof Error ? error.message : String(error);
155
+ const match = message.match(/Local agent exceeded max tool rounds:\s*(\d+)/);
156
+ if (!match) return null;
157
+ return Number(match[1]);
158
+ }
159
+
160
+ function parsePositiveInteger(value: string | undefined) {
161
+ if (!value) return null;
162
+ const parsed = Number(value);
163
+ if (!Number.isInteger(parsed) || parsed <= 0) return null;
164
+ return parsed;
165
+ }
166
+
167
+ function resolveLocalToolRoundRetryLimit(options: RunAgentTurnOptions) {
168
+ return (
169
+ parsePositiveInteger(options.env.NOLO_LOCAL_MAX_TOOL_ROUNDS_LIMIT) ??
170
+ DEFAULT_LOCAL_AGENT_TOOL_ROUND_RETRY_LIMIT
171
+ );
172
+ }
173
+
174
+ function nextLocalToolRoundLimit(current: number) {
175
+ return current <= 0 ? 1 : current * 2;
176
+ }
177
+
178
+ function formatToolTraceEvent(event: LocalAgentToolEvent) {
179
+ const round = event.round + 1;
180
+ if (event.type === "tool-call") {
181
+ return `[nolo:tool] round ${round} -> ${event.toolName} (${event.toolCallId})\n`;
182
+ }
183
+ if (event.type === "tool-error") {
184
+ return `[nolo:tool] round ${round} !! ${event.toolName}: ${event.message ?? "failed"}\n`;
185
+ }
186
+ return `[nolo:tool] round ${round} <- ${event.toolName} (${event.toolCallId})\n`;
187
+ }
188
+
80
189
  function shouldAttemptAutoLocal(options: RunAgentTurnOptions) {
81
190
  if (options.localRuntimeAdapter || options.localRuntimeAdapterFactory) return true;
82
191
  return Boolean(
@@ -87,6 +196,166 @@ function shouldAttemptAutoLocal(options: RunAgentTurnOptions) {
87
196
  options.env.NOLO_LOCAL_AGENT_KEY
88
197
  );
89
198
  }
199
+
200
+ function inferTaskRunAgentRole(agentRef: string): TaskRunAgentRole {
201
+ const normalized = agentRef.toLowerCase();
202
+ if (normalized.includes("project-manager") || normalized.includes("manager")) return "pm";
203
+ if (normalized.includes("frontend")) return "frontend";
204
+ if (normalized.includes("fullstack")) return "fullstack";
205
+ if (normalized.includes("review")) return "reviewer";
206
+ return "codex";
207
+ }
208
+
209
+ export function classifyTaskRunReviewStatus(summary?: string): TaskRunReviewStatus | undefined {
210
+ const normalized = summary?.toLowerCase().trim();
211
+ if (!normalized) return undefined;
212
+
213
+ const explicit = normalized.match(/review\s+decision\s*:\s*(passed|needs_changes|blocked)/);
214
+ if (explicit?.[1]) return explicit[1] as TaskRunReviewStatus;
215
+
216
+ if (/\b(blocked|cannot review|unable to review)\b|无法审查|阻塞/.test(normalized)) {
217
+ return "blocked";
218
+ }
219
+ if (
220
+ /\b(needs changes|request changes|changes requested|not approved)\b|需要修改|需修改|发现问题/.test(
221
+ normalized
222
+ )
223
+ ) {
224
+ return "needs_changes";
225
+ }
226
+ if (/\b(approved|lgtm|no issues|passed)\b|通过|无问题/.test(normalized)) {
227
+ return "passed";
228
+ }
229
+ return undefined;
230
+ }
231
+
232
+ async function runTaskRunCli(args: {
233
+ options: RunAgentTurnOptions;
234
+ cliArgs: string[];
235
+ }) {
236
+ const scriptPath = join(args.options.scriptDir, "taskRun.ts");
237
+ const proc = Bun.spawn({
238
+ cmd: [process.execPath, scriptPath, ...args.cliArgs],
239
+ stdout: "pipe",
240
+ stderr: "pipe",
241
+ env: {
242
+ ...process.env,
243
+ ...args.options.env,
244
+ BASE_URL: args.options.serverUrl,
245
+ },
246
+ });
247
+ const [stdout, stderr, exitCode] = await Promise.all([
248
+ new Response(proc.stdout).text(),
249
+ new Response(proc.stderr).text(),
250
+ proc.exited,
251
+ ]);
252
+ if (exitCode !== 0) {
253
+ throw new Error((stderr || stdout || `taskRun.ts exited ${exitCode}`).trim());
254
+ }
255
+ }
256
+
257
+ async function defaultTaskRunRecorder(args: {
258
+ options: RunAgentTurnOptions;
259
+ status: "completed" | "failed";
260
+ dialogId?: string;
261
+ resultSummary?: string;
262
+ errorCode?: string;
263
+ }) {
264
+ const context = args.options.taskRunContext;
265
+ if (!context?.rowDbKey) return;
266
+ const role = inferTaskRunAgentRole(args.options.agentName || args.options.agentKey);
267
+ if (role === "reviewer") {
268
+ await runTaskRunCli({
269
+ options: args.options,
270
+ cliArgs: [
271
+ "request-review",
272
+ "--row-dbkey",
273
+ context.rowDbKey,
274
+ "--base-url",
275
+ args.options.serverUrl,
276
+ "--reviewer-agent-key",
277
+ args.options.agentKey,
278
+ ...(context.artifactIds?.length ? ["--artifact-ids", context.artifactIds.join(",")] : []),
279
+ ...(args.dialogId ? ["--dialog-id", args.dialogId] : []),
280
+ ],
281
+ });
282
+ }
283
+ await runTaskRunCli({
284
+ options: args.options,
285
+ cliArgs: [
286
+ "record-agent-run",
287
+ "--row-dbkey",
288
+ context.rowDbKey,
289
+ "--base-url",
290
+ args.options.serverUrl,
291
+ "--agent-key",
292
+ args.options.agentKey,
293
+ "--role",
294
+ role,
295
+ "--run-status",
296
+ args.status,
297
+ ...(context.workItemId ? ["--work-item-id", context.workItemId] : []),
298
+ ...(args.dialogId ? ["--dialog-id", args.dialogId] : []),
299
+ ...(args.resultSummary ? ["--result-summary", args.resultSummary.slice(0, 240)] : []),
300
+ ...(args.errorCode ? ["--error-code", args.errorCode] : []),
301
+ ],
302
+ });
303
+ if (context.workItemId && args.dialogId) {
304
+ await runTaskRunCli({
305
+ options: args.options,
306
+ cliArgs: [
307
+ "update-work-item",
308
+ "--row-dbkey",
309
+ context.rowDbKey,
310
+ "--base-url",
311
+ args.options.serverUrl,
312
+ "--id",
313
+ context.workItemId,
314
+ "--dialog-ids",
315
+ args.dialogId,
316
+ ],
317
+ });
318
+ }
319
+ if (role === "reviewer") {
320
+ const reviewStatus =
321
+ args.status === "failed" ? "blocked" : classifyTaskRunReviewStatus(args.resultSummary);
322
+ if (reviewStatus) {
323
+ await runTaskRunCli({
324
+ options: args.options,
325
+ cliArgs: [
326
+ "review-result",
327
+ "--row-dbkey",
328
+ context.rowDbKey,
329
+ "--base-url",
330
+ args.options.serverUrl,
331
+ "--review-status",
332
+ reviewStatus,
333
+ "--reviewer-agent-key",
334
+ args.options.agentKey,
335
+ ...(context.artifactIds?.length ? ["--artifact-ids", context.artifactIds.join(",")] : []),
336
+ ...(args.dialogId ? ["--dialog-id", args.dialogId] : []),
337
+ ],
338
+ });
339
+ }
340
+ }
341
+ }
342
+
343
+ async function recordTaskRunOutcome(args: {
344
+ options: RunAgentTurnOptions;
345
+ status: "completed" | "failed";
346
+ dialogId?: string;
347
+ resultSummary?: string;
348
+ errorCode?: string;
349
+ }) {
350
+ if (!args.options.taskRunContext?.rowDbKey) return;
351
+ try {
352
+ await (args.options.taskRunRecorder ?? defaultTaskRunRecorder)(args);
353
+ } catch (error) {
354
+ args.options.output.write(
355
+ `[nolo] task-run writeback failed: ${error instanceof Error ? error.message : String(error)}\n`
356
+ );
357
+ }
358
+ }
90
359
 
91
360
  function formatUsage(usage: any, dialogId: unknown) {
92
361
  const parts: string[] = [];
@@ -134,12 +403,32 @@ async function runScriptBridge(options: RunAgentTurnOptions, scriptPath: string)
134
403
  options.agentKey,
135
404
  "--server",
136
405
  options.serverUrl,
137
- "--msg",
138
- options.message,
139
- "--no-default-test-root",
140
- ...(options.continueDialogId
141
- ? ["--continue", options.continueDialogId]
142
- : []),
406
+ "--msg",
407
+ options.message,
408
+ ...(options.taskRunContext?.rowDbKey
409
+ ? ["--task-row-dbkey", options.taskRunContext.rowDbKey]
410
+ : []),
411
+ ...(options.taskRunContext?.taskRunId
412
+ ? ["--task-run-id", options.taskRunContext.taskRunId]
413
+ : []),
414
+ ...(options.taskRunContext?.workItemId
415
+ ? ["--work-item-id", options.taskRunContext.workItemId]
416
+ : []),
417
+ ...(options.taskRunContext?.artifactIds?.length
418
+ ? ["--artifact-ids", options.taskRunContext.artifactIds.join(",")]
419
+ : []),
420
+ ...(options.noDefaultTestRoot ? ["--no-default-test-root"] : []),
421
+ ...(options.continueDialogId
422
+ ? ["--continue", options.continueDialogId]
423
+ : []),
424
+ ...(options.spaceId ? ["--space", options.spaceId] : []),
425
+ ...(options.category ? ["--category", options.category] : []),
426
+ ...(options.inheritedFromDialogKey
427
+ ? ["--inherit-from-dialog", options.inheritedFromDialogKey]
428
+ : []),
429
+ ...(options.background ? ["--bg"] : []),
430
+ ...(typeof options.timeoutMs === "number" ? ["--timeout-ms", String(options.timeoutMs)] : []),
431
+ ...(options.noStream ? ["--no-stream"] : []),
143
432
  ],
144
433
  stdin: "inherit",
145
434
  stdout: "inherit",
@@ -151,12 +440,12 @@ async function runScriptBridge(options: RunAgentTurnOptions, scriptPath: string)
151
440
  },
152
441
  });
153
442
 
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
- }
443
+ const exitCode = await proc.exited;
444
+ if (exitCode !== 0) {
445
+ options.output.write(`\n[nolo] Agent run exited with code ${exitCode}.\n`);
446
+ }
447
+ return { exitCode };
448
+ }
160
449
 
161
450
  async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string) {
162
451
  options.output.write(`\n${options.agentName} -> working...\n`);
@@ -172,7 +461,10 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
172
461
  },
173
462
  body: JSON.stringify({
174
463
  agentKey: options.agentKey,
175
- userInput: buildUserInputContent(options.message, options.imageUrls),
464
+ userInput: buildUserInputContent(
465
+ prependTaskRunPrompt(options.message, options.taskRunContext),
466
+ options.imageUrls
467
+ ),
176
468
  runtimeContext: {
177
469
  surface: "cli",
178
470
  host: "terminal",
@@ -180,21 +472,37 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
180
472
  entrypoint: "nolo-cli",
181
473
  capabilities: ["text-io", "streaming", "slash-commands"],
182
474
  },
183
- ...(options.continueDialogId
184
- ? { continueDialogId: options.continueDialogId }
185
- : {}),
186
- stream: true,
187
- }),
475
+ ...(options.continueDialogId
476
+ ? { continueDialogId: options.continueDialogId }
477
+ : {}),
478
+ ...(options.spaceId ? { spaceId: options.spaceId } : {}),
479
+ ...(options.category ? { category: options.category } : {}),
480
+ ...(options.inheritedFromDialogKey
481
+ ? { inheritedFromDialogKey: options.inheritedFromDialogKey }
482
+ : {}),
483
+ ...(options.parentDialogId ? { parentDialogId: options.parentDialogId } : {}),
484
+ ...(options.background ? { background: true } : {}),
485
+ ...(typeof options.maxToolRounds === "number" ? { maxRounds: options.maxToolRounds } : {}),
486
+ ...(typeof options.timeoutMs === "number" ? { timeoutMs: options.timeoutMs } : {}),
487
+ stream: !options.noStream && !options.background,
488
+ }),
188
489
  });
189
490
  } catch (error) {
190
491
  options.output.write(buildTransportErrorHint(options.serverUrl, error));
191
492
  return { exitCode: 1 };
192
493
  }
193
494
 
194
- const contentType = res.headers.get("content-type") || "";
195
- if (contentType.includes("text/event-stream") && res.body) {
196
- return readStreamingAgentRun(options, res);
197
- }
495
+ const contentType = res.headers.get("content-type") || "";
496
+ if (contentType.includes("text/event-stream") && res.body) {
497
+ const result = await readStreamingAgentRun(options, res);
498
+ await recordTaskRunOutcome({
499
+ options,
500
+ status: result.exitCode === 0 ? "completed" : "failed",
501
+ dialogId: result.dialogId,
502
+ ...(result.exitCode !== 0 ? { errorCode: "STREAM_FAILED" } : {}),
503
+ });
504
+ return result;
505
+ }
198
506
 
199
507
  let data: any = {};
200
508
  try {
@@ -203,13 +511,19 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
203
511
  data = {};
204
512
  }
205
513
 
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
- }
514
+ if (!res.ok) {
515
+ options.output.write(`[nolo] Agent request failed: HTTP ${res.status}\n`);
516
+ if (data?.error || data?.message) {
517
+ options.output.write(`${data.error || data.message}\n`);
518
+ }
519
+ await recordTaskRunOutcome({
520
+ options,
521
+ status: "failed",
522
+ errorCode: `HTTP_${res.status}`,
523
+ resultSummary: String(data?.error || data?.message || "").trim() || undefined,
524
+ });
525
+ return { exitCode: 1 };
526
+ }
213
527
 
214
528
  const content = String(data?.content ?? data?.message ?? "").trim();
215
529
  if (content) {
@@ -218,28 +532,78 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
218
532
  options.output.write(`\n${options.agentName} > (no text response)\n`);
219
533
  }
220
534
 
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
- };
535
+ const usage = formatUsage(data?.usage, data?.dialogId);
536
+ if (usage && shouldShowUsage(options.env)) options.output.write(`${usage}\n`);
537
+ const result = {
538
+ exitCode: 0,
539
+ ...(typeof data?.dialogId === "string" && data.dialogId
540
+ ? { dialogId: data.dialogId }
541
+ : {}),
542
+ };
543
+ await recordTaskRunOutcome({
544
+ options,
545
+ status: "completed",
546
+ dialogId: result.dialogId,
547
+ resultSummary: content || undefined,
548
+ });
549
+ return result;
229
550
  }
230
551
 
231
552
  async function runInjectedLocalAgentTurn(options: RunAgentTurnOptions) {
232
- return runLocalAgentTurnForCli(options, { reportFailure: true });
553
+ return runLocalAgentTurnWithAutoRoundExpansion(options, { reportFailure: true });
554
+ }
555
+
556
+ async function runLocalAgentTurnWithAutoRoundExpansion(
557
+ options: RunAgentTurnOptions,
558
+ settings: { reportFailure: boolean }
559
+ ) {
560
+ const maxRetryLimit = resolveLocalToolRoundRetryLimit(options);
561
+ let currentMaxToolRounds = options.maxToolRounds;
562
+ let lastResult: Awaited<ReturnType<typeof runLocalAgentTurnForCli>> | null = null;
563
+
564
+ while (true) {
565
+ const attemptOptions = {
566
+ ...options,
567
+ ...(typeof currentMaxToolRounds === "number" ? { maxToolRounds: currentMaxToolRounds } : {}),
568
+ };
569
+ const result = await runLocalAgentTurnForCli(attemptOptions, { reportFailure: false });
570
+ lastResult = result;
571
+ if (result.exitCode === 0) return result;
572
+
573
+ const exhaustedRounds = parseLocalToolRoundLimitError(result.localError);
574
+ if (exhaustedRounds === null) break;
575
+
576
+ const nextMaxToolRounds = nextLocalToolRoundLimit(exhaustedRounds);
577
+ if (nextMaxToolRounds > maxRetryLimit) break;
578
+
579
+ options.output.write(
580
+ `[nolo] Local agent used all ${exhaustedRounds} tool rounds; retrying with ${nextMaxToolRounds}.\n`
581
+ );
582
+ currentMaxToolRounds = nextMaxToolRounds;
583
+ }
584
+
585
+ if (settings.reportFailure && lastResult?.localError) {
586
+ options.output.write(
587
+ `[nolo] Local agent run failed: ${
588
+ lastResult.localError instanceof Error ? lastResult.localError.message : String(lastResult.localError)
589
+ }\n`
590
+ );
591
+ await recordTaskRunOutcome({
592
+ options,
593
+ status: "failed",
594
+ errorCode: "LOCAL_AGENT_FAILED",
595
+ resultSummary:
596
+ lastResult.localError instanceof Error ? lastResult.localError.message : String(lastResult.localError),
597
+ });
598
+ }
599
+ return lastResult ?? { exitCode: 1 };
233
600
  }
234
601
 
235
602
  async function runLocalAgentTurnForCli(
236
603
  options: RunAgentTurnOptions,
237
604
  settings: { reportFailure: boolean }
238
605
  ) {
239
- const adapter =
240
- options.localRuntimeAdapter ||
241
- options.localRuntimeAdapterFactory?.(options.env) ||
242
- buildDefaultLocalRuntimeAdapter(options);
606
+ const adapter = resolveLocalRuntimeAdapter(options);
243
607
  if (!adapter) {
244
608
  options.output.write("[nolo] Local runtime was requested but no local runtime adapter is available.\n");
245
609
  return { exitCode: 1 };
@@ -250,8 +614,21 @@ async function runLocalAgentTurnForCli(
250
614
  const result = await runLocalAgentTurn({
251
615
  adapter,
252
616
  agentRef: options.agentKey,
253
- input: buildUserInputContent(options.message, options.imageUrls),
617
+ input: buildUserInputContent(
618
+ prependTaskRunPrompt(options.message, options.taskRunContext),
619
+ options.imageUrls
620
+ ),
254
621
  continueDialogId: options.continueDialogId,
622
+ spaceId: options.spaceId,
623
+ category: options.category,
624
+ inheritedFromDialogKey: options.inheritedFromDialogKey,
625
+ parentDialogId: options.parentDialogId,
626
+ background: options.background,
627
+ noStream: options.noStream,
628
+ maxToolRounds: options.maxToolRounds,
629
+ ...(shouldTraceLocalTools(options)
630
+ ? { onToolEvent: (event) => options.output.write(formatToolTraceEvent(event)) }
631
+ : {}),
255
632
  });
256
633
  const content = result.content.trim();
257
634
  if (content) {
@@ -259,6 +636,12 @@ async function runLocalAgentTurnForCli(
259
636
  } else {
260
637
  options.output.write(`\n${options.agentName} > (no text response)\n`);
261
638
  }
639
+ await recordTaskRunOutcome({
640
+ options,
641
+ status: "completed",
642
+ dialogId: result.dialogId,
643
+ resultSummary: content || undefined,
644
+ });
262
645
  return { exitCode: 0, dialogId: result.dialogId };
263
646
  } catch (error) {
264
647
  if (settings.reportFailure) {
@@ -381,12 +764,15 @@ export async function runAgentTurn(options: RunAgentTurnOptions) {
381
764
  }
382
765
 
383
766
  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
- };
767
+ const skipLocal = await shouldSkipAutoLocalForServerPlatformTools(options);
768
+ if (!skipLocal) {
769
+ const localResult = await runLocalAgentTurnWithAutoRoundExpansion(options, { reportFailure: false });
770
+ if (localResult.exitCode === 0) {
771
+ return {
772
+ exitCode: localResult.exitCode,
773
+ ...(localResult.dialogId ? { dialogId: localResult.dialogId } : {}),
774
+ };
775
+ }
390
776
  }
391
777
  }
392
778