nolo-cli 0.1.39 → 0.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +4 -4
  2. package/agent-runtime/dialogWritePlan.ts +39 -0
  3. package/agent-runtime/hostAdapter.ts +4 -0
  4. package/agent-runtime/hybridRecordStore.ts +1 -1
  5. package/agent-runtime/index.ts +15 -0
  6. package/agent-runtime/localLoop.ts +87 -7
  7. package/agent-runtime/localToolPolicy.ts +2 -13
  8. package/agent-runtime/localWorkspaceTools.ts +42 -110
  9. package/agent-runtime/runtimeToolPolicy.ts +2 -11
  10. package/agent-runtime/runtimeToolSurface.ts +198 -0
  11. package/agent-runtime/types.ts +1 -0
  12. package/agentAliases.ts +2 -1
  13. package/agentRunCommand.ts +10 -32
  14. package/agentRuntimeLocal.ts +1 -0
  15. package/ai/agent/cliExecutor.ts +249 -34
  16. package/cli/agentAliases.ts +2 -1
  17. package/cli/agentRunCommand.ts +10 -32
  18. package/cli/agentRuntimeLocal.ts +1 -0
  19. package/cli/client/agentRun.ts +40 -87
  20. package/cli/client/localRuntimeAdapter.ts +179 -26
  21. package/cli/commandRegistry.ts +2 -2
  22. package/cli/machineWsRunDispatch.ts +51 -6
  23. package/cli/offlineMarxistsAgentCommand.ts +6 -4
  24. package/client/agentConfigResolver.test.ts +2 -2
  25. package/client/agentRun.test.ts +123 -57
  26. package/client/agentRun.ts +40 -87
  27. package/client/localRuntimeAdapter.test.ts +245 -31
  28. package/client/localRuntimeAdapter.ts +179 -26
  29. package/client/localRuntimeDryRun.test.ts +23 -9
  30. package/client/localToolPolicy.test.ts +4 -4
  31. package/commandRegistry.ts +2 -2
  32. package/database/server/levelAuthorityStore.ts +20 -1
  33. package/machineWsRunDispatch.ts +51 -6
  34. package/offlineMarxistsAgentCommand.ts +6 -4
  35. package/package.json +2 -1
@@ -44,7 +44,6 @@ type RunAgentTurnOptions = {
44
44
  parentDialogId?: string;
45
45
  background?: boolean;
46
46
  noStream?: boolean;
47
- noDefaultTestRoot?: boolean;
48
47
  scriptDir: string;
49
48
  env: EnvLike;
50
49
  output: OutputLike;
@@ -52,9 +51,9 @@ type RunAgentTurnOptions = {
52
51
  localRuntimeAdapter?: AgentRuntimeHostAdapter;
53
52
  localRuntimeAdapterFactory?: (env: EnvLike, options?: { cwd?: string }) => AgentRuntimeHostAdapter;
54
53
  localRuntimeCwd?: string;
55
- maxToolRounds?: number;
56
54
  timeoutMs?: number;
57
55
  traceTools?: boolean;
56
+ eventsMode?: "jsonl";
58
57
  taskRunContext?: TaskRunPromptContext;
59
58
  taskRunRecorder?: (args: {
60
59
  options: RunAgentTurnOptions;
@@ -232,7 +231,7 @@ function resolveLocalRuntimeAdapter(options: RunAgentTurnOptions) {
232
231
 
233
232
  async function shouldSkipAutoLocalForServerPlatformTools(options: RunAgentTurnOptions) {
234
233
  if (isBuiltinNoloAgentRef(options.agentKey)) return false;
235
- if (options.localRuntimeCwd && options.env.NOLO_LOCAL_SHELL_MODE === "worktree") {
234
+ if (options.localRuntimeCwd) {
236
235
  return false;
237
236
  }
238
237
  if (isKnownServerPlatformAgent(options)) {
@@ -278,7 +277,14 @@ function buildUserInputContent(message: string, imageUrls: string[] = []) {
278
277
  }
279
278
 
280
279
  function shouldTraceLocalTools(options: RunAgentTurnOptions) {
281
- return Boolean(options.traceTools || options.env.NOLO_TRACE_TOOLS === "1");
280
+ const setting = (options.env.NOLO_TRACE_TOOLS ?? "").trim().toLowerCase();
281
+ if (setting === "0" || setting === "false" || setting === "off") return false;
282
+ return true;
283
+ }
284
+
285
+ function resolveAgentEventMode(options: RunAgentTurnOptions): "text" | "jsonl" {
286
+ if (options.eventsMode === "jsonl") return "jsonl";
287
+ return options.env.NOLO_AGENT_EVENTS === "jsonl" ? "jsonl" : "text";
282
288
  }
283
289
 
284
290
  function shouldCollectLocalToolEvidence(options: RunAgentTurnOptions) {
@@ -307,15 +313,6 @@ function summarizeLocalToolEvidence(events: LocalAgentToolEvent[]): TaskRunToolE
307
313
  };
308
314
  }
309
315
 
310
- const DEFAULT_LOCAL_AGENT_TOOL_ROUND_RETRY_LIMIT = 512;
311
-
312
- function parseLocalToolRoundLimitError(error: unknown) {
313
- const message = error instanceof Error ? error.message : String(error);
314
- const match = message.match(/Local agent exceeded max tool rounds:\s*(\d+)/);
315
- if (!match) return null;
316
- return Number(match[1]);
317
- }
318
-
319
316
  function isMissingLocalAgentConfigError(error: unknown, agentRef: string) {
320
317
  return Boolean(
321
318
  error &&
@@ -326,33 +323,33 @@ function isMissingLocalAgentConfigError(error: unknown, agentRef: string) {
326
323
  );
327
324
  }
328
325
 
329
- function parsePositiveInteger(value: string | undefined) {
330
- if (!value) return null;
331
- const parsed = Number(value);
332
- if (!Number.isInteger(parsed) || parsed <= 0) return null;
333
- return parsed;
334
- }
335
-
336
- function resolveLocalToolRoundRetryLimit(options: RunAgentTurnOptions) {
337
- return (
338
- parsePositiveInteger(options.env.NOLO_LOCAL_MAX_TOOL_ROUNDS_LIMIT) ??
339
- DEFAULT_LOCAL_AGENT_TOOL_ROUND_RETRY_LIMIT
340
- );
341
- }
342
-
343
- function nextLocalToolRoundLimit(current: number) {
344
- return current <= 0 ? 1 : current * 2;
345
- }
346
-
347
326
  function formatToolTraceEvent(event: LocalAgentToolEvent) {
348
327
  const round = event.round + 1;
328
+ const detail = event.argumentsPreview ? ` ${event.argumentsPreview}` : "";
349
329
  if (event.type === "tool-call") {
350
- return `[nolo:tool] round ${round} -> ${event.toolName} (${event.toolCallId})\n`;
330
+ return `[nolo:tool] #${round} -> ${event.toolName}${detail}\n`;
351
331
  }
352
332
  if (event.type === "tool-error") {
353
- return `[nolo:tool] round ${round} !! ${event.toolName}: ${event.message ?? "failed"}\n`;
333
+ const elapsed = typeof event.elapsedMs === "number" ? ` ${event.elapsedMs}ms` : "";
334
+ return `[nolo:tool] #${round} !! ${event.toolName}${elapsed}: ${event.message ?? "failed"}\n`;
354
335
  }
355
- return `[nolo:tool] round ${round} <- ${event.toolName} (${event.toolCallId})\n`;
336
+ const elapsed = typeof event.elapsedMs === "number" ? ` ${event.elapsedMs}ms` : "";
337
+ const summary = event.summary ? ` ${event.summary}` : "";
338
+ return `[nolo:tool] #${round} <- ${event.toolName}${elapsed}${summary}\n`;
339
+ }
340
+
341
+ function formatToolJsonEvent(event: LocalAgentToolEvent) {
342
+ return `${JSON.stringify({
343
+ schemaVersion: 1,
344
+ type: event.type,
345
+ round: event.round + 1,
346
+ tool: event.toolName,
347
+ toolCallId: event.toolCallId,
348
+ ...(event.argumentsPreview ? { argsPreview: event.argumentsPreview } : {}),
349
+ ...(typeof event.elapsedMs === "number" ? { elapsedMs: event.elapsedMs } : {}),
350
+ ...(event.summary ? { summary: event.summary } : {}),
351
+ ...(event.message ? { message: event.message } : {}),
352
+ })}\n`;
356
353
  }
357
354
 
358
355
  function shouldAttemptAutoLocal(options: RunAgentTurnOptions) {
@@ -629,7 +626,6 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
629
626
  : {}),
630
627
  ...(options.parentDialogId ? { parentDialogId: options.parentDialogId } : {}),
631
628
  ...(options.background ? { background: true } : {}),
632
- ...(typeof options.maxToolRounds === "number" ? { maxRounds: options.maxToolRounds } : {}),
633
629
  ...(typeof options.timeoutMs === "number" ? { timeoutMs: options.timeoutMs } : {}),
634
630
  stream: !options.noStream && !options.background,
635
631
  }),
@@ -717,7 +713,7 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
717
713
  }
718
714
 
719
715
  async function runInjectedLocalAgentTurn(options: RunAgentTurnOptions) {
720
- return runLocalAgentTurnWithAutoRoundExpansion(options, { reportFailure: true });
716
+ return runLocalAgentTurnForCli(options, { reportFailure: true });
721
717
  }
722
718
 
723
719
  async function refreshMissingLocalAgentConfig(options: RunAgentTurnOptions) {
@@ -727,53 +723,6 @@ async function refreshMissingLocalAgentConfig(options: RunAgentTurnOptions) {
727
723
  return Boolean(agentConfig);
728
724
  }
729
725
 
730
- async function runLocalAgentTurnWithAutoRoundExpansion(
731
- options: RunAgentTurnOptions,
732
- settings: { reportFailure: boolean }
733
- ) {
734
- const maxRetryLimit = resolveLocalToolRoundRetryLimit(options);
735
- let currentMaxToolRounds = options.maxToolRounds;
736
- let lastResult: Awaited<ReturnType<typeof runLocalAgentTurnForCli>> | null = null;
737
-
738
- while (true) {
739
- const attemptOptions = {
740
- ...options,
741
- ...(typeof currentMaxToolRounds === "number" ? { maxToolRounds: currentMaxToolRounds } : {}),
742
- };
743
- const result = await runLocalAgentTurnForCli(attemptOptions, { reportFailure: false });
744
- lastResult = result;
745
- if (result.exitCode === 0) return result;
746
-
747
- const exhaustedRounds = parseLocalToolRoundLimitError(result.localError);
748
- if (exhaustedRounds === null) break;
749
-
750
- const nextMaxToolRounds = nextLocalToolRoundLimit(exhaustedRounds);
751
- if (nextMaxToolRounds > maxRetryLimit) break;
752
-
753
- options.output.write(
754
- `[nolo] Local agent used all ${exhaustedRounds} tool rounds; retrying with ${nextMaxToolRounds}.\n`
755
- );
756
- currentMaxToolRounds = nextMaxToolRounds;
757
- }
758
-
759
- if (settings.reportFailure && lastResult?.localError) {
760
- options.output.write(
761
- `[nolo] Local agent run failed: ${
762
- lastResult.localError instanceof Error ? lastResult.localError.message : String(lastResult.localError)
763
- }\n`
764
- );
765
- await recordTaskRunOutcome({
766
- options,
767
- status: "failed",
768
- errorCode: "LOCAL_AGENT_FAILED",
769
- resultSummary:
770
- lastResult.localError instanceof Error ? lastResult.localError.message : String(lastResult.localError),
771
- toolEvidence: lastResult.toolEvidence,
772
- });
773
- }
774
- return lastResult ?? { exitCode: 1 };
775
- }
776
-
777
726
  async function runLocalAgentTurnForCli(
778
727
  options: RunAgentTurnOptions,
779
728
  settings: { reportFailure: boolean }
@@ -790,6 +739,7 @@ async function runLocalAgentTurnForCli(
790
739
  try {
791
740
  const traceLocalTools = shouldTraceLocalTools(options);
792
741
  const collectToolEvidence = shouldCollectLocalToolEvidence(options);
742
+ const eventMode = resolveAgentEventMode(options);
793
743
  const result = await runLocalAgentTurn({
794
744
  adapter,
795
745
  agentRef: options.agentKey,
@@ -804,7 +754,6 @@ async function runLocalAgentTurnForCli(
804
754
  parentDialogId: options.parentDialogId,
805
755
  background: options.background,
806
756
  noStream: options.noStream,
807
- maxToolRounds: options.maxToolRounds,
808
757
  ...(typeof options.timeoutMs === "number" ? { timeoutMs: options.timeoutMs } : {}),
809
758
  ...(traceLocalTools || collectToolEvidence
810
759
  ? {
@@ -812,7 +761,11 @@ async function runLocalAgentTurnForCli(
812
761
  if (collectToolEvidence) toolEvents.push(event);
813
762
  if (traceLocalTools) {
814
763
  spinner.stop();
815
- options.output.write(formatToolTraceEvent(event));
764
+ options.output.write(
765
+ eventMode === "jsonl"
766
+ ? formatToolJsonEvent(event)
767
+ : formatToolTraceEvent(event)
768
+ );
816
769
  }
817
770
  },
818
771
  }
@@ -968,7 +921,7 @@ export async function runAgentTurn(options: RunAgentTurnOptions) {
968
921
  if (runtimeMode === "auto" && shouldAttemptAutoLocal(options)) {
969
922
  const skipLocal = await shouldSkipAutoLocalForServerPlatformTools(options);
970
923
  if (!skipLocal) {
971
- const localResult = await runLocalAgentTurnWithAutoRoundExpansion(options, { reportFailure: false });
924
+ const localResult = await runLocalAgentTurnForCli(options, { reportFailure: false });
972
925
  if (localResult.exitCode === 0) {
973
926
  return {
974
927
  exitCode: localResult.exitCode,
@@ -982,7 +935,7 @@ export async function runAgentTurn(options: RunAgentTurnOptions) {
982
935
  try {
983
936
  const refreshed = await refreshMissingLocalAgentConfig(options);
984
937
  if (refreshed) {
985
- const retriedLocalResult = await runLocalAgentTurnWithAutoRoundExpansion(options, {
938
+ const retriedLocalResult = await runLocalAgentTurnForCli(options, {
986
939
  reportFailure: false,
987
940
  });
988
941
  if (retriedLocalResult.exitCode === 0) {
@@ -21,6 +21,7 @@ import {
21
21
  resolveCurrentRunRuntimeToolPolicy,
22
22
  resolveLocalWorkspaceExecutorOptionsFromPolicy,
23
23
  resolveRequestedRuntimeToolNames,
24
+ resolveRuntimeToolSurfaceForAgent,
24
25
  shouldUsePlatformChatProvider,
25
26
  } from "../agentRuntimeLocal";
26
27
  import type { AgentRuntimeChatMessage } from "../../agent-runtime";
@@ -43,6 +44,7 @@ import {
43
44
  import { executeLocalToolWithPolicy } from "./localToolPolicy";
44
45
  import { taskRunFunctionSchema } from "../../ai/tools/agent/taskRunTool";
45
46
  import {
47
+ MIMO_MONTH_AGENT_KEY,
46
48
  NOLO_DEFAULT_AGENT_ID,
47
49
  NOLO_DEFAULT_AGENT_KEY,
48
50
  } from "../agentAliases";
@@ -50,14 +52,26 @@ import {
50
52
  buildNoloWorkspaceCliToolExecutors,
51
53
  buildNoloWorkspaceOpenAiTools,
52
54
  filterNoloWorkspaceToolNames,
53
- NOLO_WORKSPACE_TOOL_NAMES,
54
- NOLO_WORKSPACE_TOOL_PROMPT,
55
55
  parseNoloWorkspaceToolArguments,
56
56
  } from "../../agent-runtime/noloWorkspaceTools";
57
+ import { executeCli as defaultExecuteCli, type CliExecuteResult, type CliImageInput } from "../../ai/agent/cliExecutor";
58
+ import { buildCliPrompt } from "../../ai/agent/cliPrompt";
57
59
 
58
60
  type EnvLike = Record<string, string | undefined>;
59
61
  type FetchInput = Parameters<typeof fetch>[0];
60
62
  type FetchInit = Parameters<typeof fetch>[1];
63
+ type LocalCliExecutor = (
64
+ provider: string,
65
+ prompt: string,
66
+ options: {
67
+ model?: string;
68
+ timeout?: number;
69
+ cwd?: string;
70
+ yolo?: boolean;
71
+ env?: Record<string, string | undefined>;
72
+ imageInputs?: CliImageInput[];
73
+ }
74
+ ) => Promise<CliExecuteResult>;
61
75
  const TRANSIENT_FETCH_MAX_ATTEMPTS = 8;
62
76
  const TRANSIENT_FETCH_RETRY_BASE_DELAY_MS = 250;
63
77
  export const BUILTIN_NOLO_AGENT_ID = NOLO_DEFAULT_AGENT_ID;
@@ -77,6 +91,7 @@ export type CliLocalRuntimeAdapterDeps = {
77
91
  cwd?: string;
78
92
  output?: { write(chunk: string): unknown };
79
93
  localToolExecutors?: Record<string, (call: any) => Promise<{ content: string; metadata?: Record<string, unknown> }>>;
94
+ executeCli?: LocalCliExecutor;
80
95
  sleep?: (ms: number) => Promise<void>;
81
96
  loopbackRequest?: (input: FetchInput, init?: FetchInit) => Promise<Response>;
82
97
  };
@@ -93,10 +108,6 @@ function resolveLocalUserId(env: EnvLike) {
93
108
  return env.NOLO_LOCAL_USER_ID || env.NOLO_USER_ID || "local";
94
109
  }
95
110
 
96
- function shellModeEnabled(env: EnvLike) {
97
- return env.NOLO_LOCAL_SHELL_MODE === "worktree";
98
- }
99
-
100
111
  function parseLocalToolBudgets(env: EnvLike) {
101
112
  const raw = env.NOLO_LOCAL_TOOL_BUDGETS?.trim();
102
113
  if (!raw) return {};
@@ -251,7 +262,79 @@ function parseJsonObject(raw: string) {
251
262
  }
252
263
  }
253
264
 
265
+ function isCliProviderAgent(agentConfig: AgentRuntimeAgentConfig) {
266
+ return Boolean(
267
+ agentConfig.apiSource === "cli" ||
268
+ agentConfig.provider === "cli" ||
269
+ agentConfig.cliProvider
270
+ );
271
+ }
272
+
273
+ function resolveCliProviderName(agentConfig: AgentRuntimeAgentConfig) {
274
+ return (agentConfig.cliProvider || agentConfig.provider || "codex").trim() || "codex";
275
+ }
276
+
277
+ function stringifyRuntimeMessageContent(content: AgentRuntimeChatMessage["content"]) {
278
+ if (typeof content === "string") return content;
279
+ if (Array.isArray(content)) {
280
+ return content
281
+ .map((part) => {
282
+ if (typeof part === "string") return part;
283
+ if (part && typeof part === "object" && "text" in part) {
284
+ return String((part as any).text ?? "");
285
+ }
286
+ return JSON.stringify(part);
287
+ })
288
+ .filter((part) => part.trim())
289
+ .join("\n");
290
+ }
291
+ return content == null ? "" : String(content);
292
+ }
293
+
294
+ function buildPromptForCliProvider(messages: AgentRuntimeChatMessage[]) {
295
+ const systemPrompt = messages
296
+ .filter((message) => message.role === "system")
297
+ .map((message) => stringifyRuntimeMessageContent(message.content).trim())
298
+ .filter(Boolean)
299
+ .join("\n\n");
300
+ const taskPrompt = messages
301
+ .filter((message) => message.role !== "system")
302
+ .map((message) => {
303
+ const content = stringifyRuntimeMessageContent(message.content).trim();
304
+ if (!content) return "";
305
+ return `[${message.role}]\n${content}`;
306
+ })
307
+ .filter(Boolean)
308
+ .join("\n\n");
309
+ return buildCliPrompt(systemPrompt, taskPrompt);
310
+ }
311
+
312
+ function collectCliProviderImageInputs(messages: AgentRuntimeChatMessage[]) {
313
+ const urls: string[] = [];
314
+ for (const message of messages) {
315
+ if (!Array.isArray(message.content)) continue;
316
+ for (const part of message.content) {
317
+ if (
318
+ part &&
319
+ typeof part === "object" &&
320
+ (part as any).type === "image_url" &&
321
+ typeof (part as any).image_url?.url === "string" &&
322
+ (part as any).image_url.url.trim()
323
+ ) {
324
+ urls.push((part as any).image_url.url.trim());
325
+ }
326
+ }
327
+ }
328
+ return urls;
329
+ }
330
+
254
331
  function logLocalRuntimeDiagnostic(event: string, fields: Record<string, unknown>) {
332
+ if (
333
+ process.env.NOLO_LOCAL_RUNTIME_DEBUG !== "1" &&
334
+ process.env.NOLO_DEBUG !== "1"
335
+ ) {
336
+ return;
337
+ }
255
338
  console.error(`[nolo-local-runtime] ${event} ${JSON.stringify(fields)}`);
256
339
  }
257
340
 
@@ -266,31 +349,38 @@ function summarizeOpenAiToolNames(tools: Array<Record<string, unknown>>) {
266
349
  .filter((name): name is string => Boolean(name));
267
350
  }
268
351
 
269
- function buildOpenAiTools(args: { toolNames?: string[]; env: EnvLike }) {
352
+ function shouldExposeLocalPlatformTools(agentKey?: string) {
353
+ return agentKey !== MIMO_MONTH_AGENT_KEY;
354
+ }
355
+
356
+ function buildOpenAiTools(args: { agentKey?: string; toolNames?: string[]; env: EnvLike }) {
270
357
  const toolset = buildLocalWorkspaceToolsetForEnv(args);
358
+ const exposePlatformTools = shouldExposeLocalPlatformTools(args.agentKey);
271
359
  return [
272
360
  ...buildLocalWorkspaceOpenAiTools({
273
361
  toolNames: toolset.toolNames,
274
362
  exposeShellTools: toolset.exposeShellTools,
275
363
  }),
276
- ...buildServerPlatformOpenAiTools({ toolNames: args.toolNames }),
277
- ...buildNoloWorkspaceOpenAiTools({ toolNames: args.toolNames }),
364
+ ...(exposePlatformTools ? buildServerPlatformOpenAiTools({ toolNames: args.toolNames }) : []),
365
+ ...(exposePlatformTools ? buildNoloWorkspaceOpenAiTools({ toolNames: args.toolNames }) : []),
278
366
  ];
279
367
  }
280
368
 
281
369
  function buildLocalWorkspaceToolsetForEnv(args: { toolNames?: string[]; env: EnvLike }) {
370
+ void args.env;
282
371
  const toolset = buildLocalWorkspaceToolset({
283
372
  declaredToolNames: args.toolNames,
284
- exposeShellTools: shellModeEnabled(args.env),
373
+ exposeShellTools: true,
285
374
  });
286
375
  return toolset;
287
376
  }
288
377
 
289
378
  function buildLocalPolicyToolNames(args: { toolNames?: string[]; env: EnvLike }) {
379
+ void args.env;
290
380
  return [
291
381
  ...buildLocalWorkspacePolicyToolNames({
292
382
  declaredToolNames: args.toolNames,
293
- exposeShellTools: shellModeEnabled(args.env),
383
+ exposeShellTools: true,
294
384
  }),
295
385
  ...((args.toolNames ?? []).includes("taskRun") ? ["taskRun"] : []),
296
386
  ...filterNoloWorkspaceToolNames(args.toolNames),
@@ -336,16 +426,32 @@ function isBuiltinNoloAgentConfig(agentConfig: AgentRuntimeAgentConfig | null |
336
426
  return isBuiltinNoloAgentRef(key) || isBuiltinNoloAgentRef(id);
337
427
  }
338
428
 
339
- function withBuiltinNoloCliTools(agentConfig: AgentRuntimeAgentConfig | null) {
340
- if (!agentConfig || !isBuiltinNoloAgentConfig(agentConfig)) return agentConfig;
341
- const toolNames = [...new Set([...(agentConfig.toolNames ?? []), ...NOLO_WORKSPACE_TOOL_NAMES])];
429
+ function withResolvedRuntimeToolSurface(
430
+ agentConfig: AgentRuntimeAgentConfig | null,
431
+ env: EnvLike
432
+ ) {
433
+ if (!agentConfig) return agentConfig;
434
+ const currentUserId = resolveLocalUserId(env);
435
+ const rawRecord = (agentConfig as any).rawRecord ?? {};
436
+ const ownerId =
437
+ typeof rawRecord.userId === "string" && rawRecord.userId.trim()
438
+ ? rawRecord.userId.trim()
439
+ : null;
440
+ const toolSurface = resolveRuntimeToolSurfaceForAgent({
441
+ explicitToolNames: agentConfig.toolNames,
442
+ currentUserId,
443
+ agentOwnerId: ownerId,
444
+ agentKey: rawRecord.dbKey ?? agentConfig.key,
445
+ isPublic: !isBuiltinNoloAgentConfig(agentConfig) && rawRecord.isPublic === true,
446
+ sharingLevel: typeof rawRecord.sharingLevel === "string" ? rawRecord.sharingLevel : null,
447
+ trustedPrivateInvocation: isBuiltinNoloAgentConfig(agentConfig),
448
+ runtimeHost: "cli",
449
+ });
342
450
  return {
343
451
  ...agentConfig,
344
- toolNames,
345
- prompt: [
346
- agentConfig.prompt?.trim() ?? "",
347
- NOLO_WORKSPACE_TOOL_PROMPT,
348
- ].filter(Boolean).join("\n\n"),
452
+ toolNames: toolSurface.finalToolNames,
453
+ toolSurface,
454
+ prompt: agentConfig.prompt,
349
455
  };
350
456
  }
351
457
 
@@ -500,11 +606,14 @@ export function createCliLocalRuntimeAdapter(
500
606
  host: "cli",
501
607
  capabilities: ["leveldb-agent-config", "local-provider", "leveldb-persistence", "local-tools"],
502
608
  loadAgentConfig: async (agentRef) => {
503
- const agentConfig = withBuiltinNoloCliTools(await readAgentFromStore({
504
- agentRef,
505
- store: await resolveStore(deps),
506
- userId,
507
- }));
609
+ const agentConfig = withResolvedRuntimeToolSurface(
610
+ await readAgentFromStore({
611
+ agentRef,
612
+ store: await resolveStore(deps),
613
+ userId,
614
+ }),
615
+ deps.env
616
+ );
508
617
  const requestedToolNames = agentConfig
509
618
  ? resolveRequestedRuntimeToolNames({ agentConfig })
510
619
  : [];
@@ -537,6 +646,50 @@ export function createCliLocalRuntimeAdapter(
537
646
  cwd: workspaceRoot,
538
647
  }),
539
648
  resolveProvider: async (agentConfig) => {
649
+ if (isCliProviderAgent(agentConfig)) {
650
+ const provider = resolveCliProviderName(agentConfig);
651
+ logLocalRuntimeDiagnostic("provider.selected", {
652
+ agentKey: agentConfig.key,
653
+ transport: "local-cli",
654
+ apiSource: agentConfig.apiSource ?? null,
655
+ provider,
656
+ model: agentConfig.model ?? null,
657
+ cwd: workspaceRoot,
658
+ });
659
+ return {
660
+ model: agentConfig.model || provider,
661
+ complete: async (messages, options) => {
662
+ const executeCli = deps.executeCli ?? (defaultExecuteCli as LocalCliExecutor);
663
+ const imageUrls = collectCliProviderImageInputs(messages);
664
+ const imageInputs: CliImageInput[] | undefined =
665
+ imageUrls.length > 0
666
+ ? imageUrls.map((url) => ({ source: url }))
667
+ : undefined;
668
+ const prompt = buildPromptForCliProvider(messages);
669
+ try {
670
+ const result = await executeCli(provider, prompt, {
671
+ ...(agentConfig.model ? { model: agentConfig.model } : {}),
672
+ ...(options?.timeoutMs ? { timeout: options.timeoutMs } : {}),
673
+ cwd: workspaceRoot,
674
+ yolo: true,
675
+ env: deps.env,
676
+ ...(imageInputs ? { imageInputs } : {}),
677
+ });
678
+ return {
679
+ content: result.text,
680
+ model: agentConfig.model || provider,
681
+ raw: result.raw,
682
+ };
683
+ } catch (error) {
684
+ const message = error instanceof Error ? error.message : String(error);
685
+ throw new Error(
686
+ `Local CLI provider "${provider}" is unavailable or failed: ${message}`
687
+ );
688
+ }
689
+ },
690
+ };
691
+ }
692
+
540
693
  if (shouldUsePlatformChatProvider(deps.env, agentConfig)) {
541
694
  const providerConfig = resolvePlatformChatProviderConfig({
542
695
  agentConfig,
@@ -563,6 +716,7 @@ export function createCliLocalRuntimeAdapter(
563
716
  agentConfig,
564
717
  });
565
718
  const tools = buildOpenAiTools({
719
+ agentKey: agentConfig.key,
566
720
  toolNames: requestedToolNames,
567
721
  env: deps.env,
568
722
  });
@@ -582,7 +736,6 @@ export function createCliLocalRuntimeAdapter(
582
736
  toolCount: tools.length,
583
737
  requestedToolNames,
584
738
  openAiToolNames: summarizeOpenAiToolNames(tools),
585
- shellMode: deps.env.NOLO_LOCAL_SHELL_MODE ?? null,
586
739
  timeoutMs: options?.timeoutMs ?? null,
587
740
  });
588
741
  const res = await fetchWithTransientRetry(fetchImpl, request.url, {
@@ -636,6 +789,7 @@ export function createCliLocalRuntimeAdapter(
636
789
  agentConfig,
637
790
  });
638
791
  const tools = buildOpenAiTools({
792
+ agentKey: agentConfig.key,
639
793
  toolNames: requestedToolNames,
640
794
  env: deps.env,
641
795
  });
@@ -654,7 +808,6 @@ export function createCliLocalRuntimeAdapter(
654
808
  toolCount: tools.length,
655
809
  requestedToolNames,
656
810
  openAiToolNames: summarizeOpenAiToolNames(tools),
657
- shellMode: deps.env.NOLO_LOCAL_SHELL_MODE ?? null,
658
811
  timeoutMs: options?.timeoutMs ?? null,
659
812
  });
660
813
  const res = await fetchWithTransientRetry(fetchImpl, request.url, {
@@ -69,8 +69,8 @@ export function renderHelpText() {
69
69
  " nolo space read 01KKY77TT0DA9NY7TNW3R7255N --content-key page-user-id --brief",
70
70
  " nolo space delete --name-prefix rn_owner_verify_0504 --yes",
71
71
  " nolo table query --table 01ABCXYZ",
72
- ' nolo table query --table meta-b2e06f801f-NOLOTASKBOARD --columns \'["title","status","owner","priority","codeStatus"]\' --no-base-fields --output items',
73
- ' nolo table update-row --table meta-b2e06f801f-NOLOTASKBOARD --row 01ROWID --changes \'{"status":"已完成"}\'',
72
+ ' nolo table query --table meta-0e95801d90-NOLOTASKBOARD --columns \'["title","status","owner","priority","codeStatus"]\' --no-base-fields --output items',
73
+ ' nolo table update-row --table meta-0e95801d90-NOLOTASKBOARD --row 01ROWID --changes \'{"status":"已完成"}\'',
74
74
  " nolo task-run read-context --row-dbkey row-user-table-row",
75
75
  " nolo llama status",
76
76
  ];
@@ -20,9 +20,52 @@ type EnvLike = Record<string, string | undefined>;
20
20
  export type LocalCliExecutor = (
21
21
  provider: string,
22
22
  prompt: string,
23
- options: { model?: string; timeout?: number; cwd?: string; yolo?: boolean; env?: EnvLike }
23
+ options: {
24
+ model?: string;
25
+ timeout?: number;
26
+ cwd?: string;
27
+ yolo?: boolean;
28
+ env?: EnvLike;
29
+ imageInputs?: Array<{ source: string; materializedPath?: string }>;
30
+ }
24
31
  ) => Promise<{ text: string; raw?: string; elapsed?: number }>;
25
32
 
33
+ /**
34
+ * 从 connector payload 的 userInput 中提取 multimodal 内容。
35
+ * 如果 userInput 是字符串,直接返回 { text, imageUrls: [] }。
36
+ * 如果 userInput 是 multimodal content array(`[{ type: "text" }, { type: "image_url" }]`),
37
+ * 提取 text 和 image_url parts。
38
+ */
39
+ function extractMultimodalUserInput(userInput: unknown): {
40
+ text: string;
41
+ imageUrls: string[];
42
+ } {
43
+ if (typeof userInput === "string") {
44
+ return { text: userInput, imageUrls: [] };
45
+ }
46
+ if (!Array.isArray(userInput)) {
47
+ return { text: String(userInput ?? ""), imageUrls: [] };
48
+ }
49
+
50
+ const textParts: string[] = [];
51
+ const imageUrls: string[] = [];
52
+
53
+ for (const part of userInput) {
54
+ if (!part || typeof part !== "object") continue;
55
+ if ((part as any).type === "text" && typeof (part as any).text === "string") {
56
+ textParts.push((part as any).text);
57
+ } else if (
58
+ (part as any).type === "image_url" &&
59
+ typeof (part as any).image_url?.url === "string" &&
60
+ (part as any).image_url.url.trim()
61
+ ) {
62
+ imageUrls.push((part as any).image_url.url.trim());
63
+ }
64
+ }
65
+
66
+ return { text: textParts.join("\n"), imageUrls };
67
+ }
68
+
26
69
  type ConnectorRuntimePolicy = {
27
70
  runtimeTools?: string[];
28
71
  agentTools?: string[];
@@ -103,7 +146,6 @@ function summarizeRuntimePolicyForLogs(policy?: ConnectorRuntimePolicy) {
103
146
  workspaceMode: policy?.workspace?.mode ?? null,
104
147
  workspaceCwd: policy?.workspace?.cwd ?? null,
105
148
  shellEnabled: policy?.shell?.enabled ?? null,
106
- shellMode: policy?.shell?.mode ?? null,
107
149
  };
108
150
  }
109
151
 
@@ -188,13 +230,13 @@ function runtimeWorkspacePermissionPolicy(
188
230
  runtimeEnv: EnvLike,
189
231
  cwd: string
190
232
  ): PermissionPolicy | null {
233
+ void runtimeEnv;
191
234
  if (!requestsLocalWorkspaceRuntime(runtimePolicy)) return null;
192
235
  return {
193
236
  mode: "full_access",
194
237
  allowFilesystemRead: true,
195
238
  allowFilesystemWrite: true,
196
239
  allowShell:
197
- runtimeEnv.NOLO_LOCAL_SHELL_MODE === "worktree" &&
198
240
  Array.isArray(runtimePolicy?.runtimeTools) &&
199
241
  runtimePolicy.runtimeTools.includes("execShell"),
200
242
  writableRoots: [cwd],
@@ -332,8 +374,6 @@ async function defaultRunConnectorLocalRuntimeAgent(args: {
332
374
  requestId: args.parsed.requestId,
333
375
  agentKey,
334
376
  cwd: args.cwd,
335
- runtimeEnvShellModeBeforePolicy: args.runtimeEnv.NOLO_LOCAL_SHELL_MODE ?? null,
336
- runtimeEnvShellModeAfterPolicy: env.NOLO_LOCAL_SHELL_MODE ?? null,
337
377
  ...summarizeRuntimePolicyForLogs(policy),
338
378
  });
339
379
  const adapter = createCliLocalRuntimeAdapter({
@@ -454,7 +494,9 @@ export async function handleConnectorRunMessage(
454
494
  try {
455
495
  const machinePermissionPolicy = resolveMachineRunPermissionPolicy(agentConfig);
456
496
  const runtimePolicy = runtimePolicyFromConnectorPayload(parsed);
457
- const userInput = String(parsed.payload?.userInput ?? "");
497
+ const extractedInput = extractMultimodalUserInput(parsed.payload?.userInput);
498
+ const userInput = extractedInput.text;
499
+ const connectorImageUrls = extractedInput.imageUrls;
458
500
  const timeout = normalizeConnectorRunTimeoutMs(parsed.payload?.timeoutMs);
459
501
  let cwd = resolveConnectorRunCwd({ env: runtimeEnv, policy: machinePermissionPolicy });
460
502
  let runContent = "";
@@ -553,6 +595,9 @@ export async function handleConnectorRunMessage(
553
595
  effectiveRuntimeEnv.AUTH_TOKEN ||
554
596
  effectiveRuntimeEnv.AUTH,
555
597
  },
598
+ ...(connectorImageUrls.length > 0
599
+ ? { imageInputs: connectorImageUrls.map((url) => ({ source: url })) }
600
+ : {}),
556
601
  }).finally(() => clearInterval(heartbeat));
557
602
  logConnectorRun("agent.run.cli.result", {
558
603
  requestId: parsed.requestId,