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
@@ -3,27 +3,65 @@ import type {
3
3
  AgentRuntimeHostAdapter,
4
4
  AgentRuntimeSaveTurnInput,
5
5
  } from "../agentRuntimeLocal";
6
- import type { AgentRuntimeChatMessage } from "../../agent-runtime";
6
+ import {
7
+ buildLocalWorkspacePolicyToolNames,
8
+ buildLocalWorkspaceToolset,
9
+ buildLocalWorkspaceOpenAiTools,
10
+ buildOpenAiCompatibleChatCompletionRequest,
11
+ buildPlatformChatCompletionRequest,
12
+ createLocalWorkspaceToolExecutors,
13
+ parseOpenAiCompatibleChatCompletionResponse,
14
+ parsePlatformChatCompletionData,
15
+ parsePlatformChatCompletionResponse,
16
+ resolvePlatformChatProviderConfig,
17
+ shouldUsePlatformChatProvider,
18
+ } from "../agentRuntimeLocal";
19
+ import type { AgentRuntimeChatMessage } from "../agent-runtime";
7
20
  import { getDefaultCliLocalRuntimeDb } from "../localRuntimeDb";
21
+ import { resolveAgentRuntimeConfigFromRecord } from "./agentConfigResolver";
22
+ import { resolveCliOpenAiProviderConfig } from "./localProviderResolver";
23
+ import {
24
+ buildLocalDialogWritePlan,
25
+ localDialogMessageRecordToRuntimeMessage,
26
+ } from "./localDialogRecords";
27
+ import {
28
+ buildLocalAgentLookupKeys,
29
+ shouldReadAgentKeyRemotely,
30
+ } from "./localAgentRecords";
31
+ import {
32
+ createCliHybridRecordStore,
33
+ type CliKvDb,
34
+ type HybridRecordStore,
35
+ } from "./hybridRecordStore";
8
36
  import { executeLocalToolWithPolicy } from "./localToolPolicy";
37
+ import { prepareTaskWorktree } from "./taskWorktree";
38
+ import {
39
+ activateWorkspaceSession,
40
+ createWorkspaceSession,
41
+ formatWorkspaceSessionActivation,
42
+ type WorkspaceSession,
43
+ } from "./workspaceSession";
9
44
 
10
45
  type EnvLike = Record<string, string | undefined>;
46
+ type FetchInput = Parameters<typeof fetch>[0];
47
+ type FetchInit = Parameters<typeof fetch>[1];
48
+ const TRANSIENT_FETCH_MAX_ATTEMPTS = 8;
49
+ const TRANSIENT_FETCH_RETRY_BASE_DELAY_MS = 250;
11
50
 
12
- export type CliLocalRuntimeDb = {
13
- get(key: string): Promise<any>;
14
- put(key: string, value: any): Promise<unknown>;
15
- batch(ops: Array<{ type: "put"; key: string; value: any }>): Promise<unknown>;
16
- iterator(options: { gte: string; lte?: string; lt?: string; reverse?: boolean; limit?: number }): AsyncIterable<[string, any]>;
17
- };
51
+ export type CliLocalRuntimeDb = CliKvDb;
18
52
 
19
53
  export type CliLocalRuntimeAdapterDeps = {
20
54
  env: EnvLike;
21
55
  db?: CliLocalRuntimeDb;
56
+ store?: HybridRecordStore;
22
57
  now?: () => number;
23
58
  createId?: () => string;
24
59
  fetchImpl?: typeof fetch;
25
60
  cwd?: string;
61
+ output?: { write(chunk: string): unknown };
62
+ prepareTaskWorktree?: typeof prepareTaskWorktree;
26
63
  localToolExecutors?: Record<string, (call: any) => Promise<{ content: string; metadata?: Record<string, unknown> }>>;
64
+ sleep?: (ms: number) => Promise<void>;
27
65
  };
28
66
 
29
67
  async function defaultLocalRuntimeDb(): Promise<CliLocalRuntimeDb> {
@@ -34,252 +72,176 @@ function createFallbackId() {
34
72
  return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`.toUpperCase();
35
73
  }
36
74
 
37
- function resolveOpenAiCompatibleBaseUrl(env: EnvLike) {
38
- return (env.NOLO_LOCAL_OPENAI_BASE_URL || env.OPENAI_BASE_URL || "https://api.openai.com/v1")
39
- .replace(/\/+$/, "");
75
+ function resolveLocalUserId(env: EnvLike) {
76
+ return env.NOLO_LOCAL_USER_ID || env.NOLO_USER_ID || "local";
40
77
  }
41
78
 
42
- function resolveApiKey(env: EnvLike) {
43
- return env.OPENAI_API_KEY || env.NOLO_LOCAL_OPENAI_API_KEY || "";
79
+ function shellModeEnabled(env: EnvLike) {
80
+ return ["worktree", "dangerous", "1", "true"].includes(env.NOLO_LOCAL_SHELL_MODE || "");
44
81
  }
45
82
 
46
- function resolveLocalUserId(env: EnvLike) {
47
- return env.NOLO_LOCAL_USER_ID || env.NOLO_USER_ID || "local";
83
+ function parseLocalToolBudgets(env: EnvLike) {
84
+ const raw = env.NOLO_LOCAL_TOOL_BUDGETS?.trim();
85
+ if (!raw) return {};
86
+ const budgets: Record<string, number> = {};
87
+ for (const part of raw.split(",")) {
88
+ const [name, value] = part.split("=").map((item) => item.trim());
89
+ const limit = Number(value);
90
+ if (name && Number.isFinite(limit) && limit >= 0) budgets[name] = Math.floor(limit);
91
+ }
92
+ return budgets;
48
93
  }
49
94
 
50
- function buildExecShellTool() {
51
- return {
52
- type: "function",
53
- function: {
54
- name: "execShell",
55
- description: "Run a shell command in the current isolated local workspace.",
56
- parameters: {
57
- type: "object",
58
- properties: {
59
- cmd: {
60
- type: "string",
61
- description: "Shell command to run.",
62
- },
63
- },
64
- required: ["cmd"],
65
- },
66
- },
67
- };
95
+ function assertWithinLocalToolBudget(args: {
96
+ toolName: string;
97
+ budgets: Record<string, number>;
98
+ usage: Map<string, number>;
99
+ }) {
100
+ const limit = args.budgets[args.toolName];
101
+ if (typeof limit !== "number") return;
102
+ const nextCount = (args.usage.get(args.toolName) ?? 0) + 1;
103
+ args.usage.set(args.toolName, nextCount);
104
+ if (nextCount <= limit) return;
105
+ throw new Error(
106
+ `${args.toolName} exceeded local tool budget ${limit}. Stop broad discovery; edit the narrowest likely file or report a blocker.`
107
+ );
68
108
  }
69
109
 
70
- function shellModeEnabled(env: EnvLike) {
71
- return ["worktree", "dangerous", "1", "true"].includes(env.NOLO_LOCAL_SHELL_MODE || "");
110
+ function isTransientFetchError(error: unknown) {
111
+ const message = error instanceof Error ? error.message : String(error);
112
+ return /certificate|handshake|network|socket|timed out|timeout|ECONNRESET/i.test(message);
72
113
  }
73
114
 
74
- function buildOpenAiTools(args: { toolNames?: string[]; env: EnvLike }) {
75
- return args.toolNames?.includes("execShell") || shellModeEnabled(args.env)
76
- ? [buildExecShellTool()]
77
- : [];
115
+ async function defaultSleep(ms: number) {
116
+ await new Promise((resolve) => setTimeout(resolve, ms));
117
+ }
118
+
119
+ function transientFetchRetryDelayMs(attempt: number) {
120
+ return Math.min(attempt * TRANSIENT_FETCH_RETRY_BASE_DELAY_MS, 2_000);
78
121
  }
79
122
 
80
- function parseToolArguments(raw: string) {
81
- try {
82
- const parsed = JSON.parse(raw || "{}");
83
- return parsed && typeof parsed === "object" ? parsed : {};
84
- } catch {
85
- return {};
123
+ async function fetchWithTransientRetry(
124
+ fetchImpl: typeof fetch,
125
+ input: FetchInput,
126
+ init?: FetchInit,
127
+ options: { sleep?: (ms: number) => Promise<void> } = {}
128
+ ) {
129
+ let lastError: unknown;
130
+ for (let attempt = 1; attempt <= TRANSIENT_FETCH_MAX_ATTEMPTS; attempt += 1) {
131
+ try {
132
+ return await fetchImpl(input, init);
133
+ } catch (error) {
134
+ if (!isTransientFetchError(error)) throw error;
135
+ lastError = error;
136
+ if (attempt < TRANSIENT_FETCH_MAX_ATTEMPTS) {
137
+ await (options.sleep ?? defaultSleep)(transientFetchRetryDelayMs(attempt));
138
+ }
139
+ }
86
140
  }
141
+ throw lastError;
87
142
  }
88
143
 
89
- async function executeShellCommand(args: {
90
- call: any;
91
- cwd: string;
92
- }) {
93
- const parsed = parseToolArguments(String(args.call.arguments ?? ""));
94
- const cmd = String(parsed.cmd ?? parsed.command ?? "").trim();
95
- if (!cmd) throw new Error("execShell requires a non-empty cmd argument.");
96
- const proc = Bun.spawn(["/bin/sh", "-lc", cmd], {
97
- cwd: args.cwd,
98
- stdout: "pipe",
99
- stderr: "pipe",
100
- stdin: "ignore",
144
+ function buildOpenAiTools(args: { toolNames?: string[]; env: EnvLike }) {
145
+ const toolset = buildLocalWorkspaceToolsetForEnv(args);
146
+ return buildLocalWorkspaceOpenAiTools({
147
+ toolNames: toolset.toolNames,
148
+ exposeShellTools: toolset.exposeShellTools,
101
149
  });
102
- const [stdout, stderr, exitCode] = await Promise.all([
103
- new Response(proc.stdout).text(),
104
- new Response(proc.stderr).text(),
105
- proc.exited,
106
- ]);
107
- return {
108
- content: [
109
- stdout.trim() ? `stdout:\n${stdout.trim()}` : "",
110
- stderr.trim() ? `stderr:\n${stderr.trim()}` : "",
111
- `exitCode: ${exitCode}`,
112
- ].filter(Boolean).join("\n\n"),
113
- metadata: { exitCode },
114
- };
115
150
  }
116
151
 
117
- function buildLocalToolExecutors(deps: CliLocalRuntimeAdapterDeps) {
152
+ function buildLocalWorkspaceToolsetForEnv(args: { toolNames?: string[]; env: EnvLike }) {
153
+ const toolset = buildLocalWorkspaceToolset({
154
+ declaredToolNames: args.toolNames,
155
+ exposeShellTools: shellModeEnabled(args.env),
156
+ });
157
+ return toolset;
158
+ }
159
+
160
+ function buildLocalPolicyToolNames(args: { toolNames?: string[]; env: EnvLike }) {
161
+ return buildLocalWorkspacePolicyToolNames({
162
+ declaredToolNames: args.toolNames,
163
+ exposeShellTools: shellModeEnabled(args.env),
164
+ });
165
+ }
166
+
167
+ function buildLocalToolExecutors(args: {
168
+ workspaceRoot: string;
169
+ localToolExecutors?: CliLocalRuntimeAdapterDeps["localToolExecutors"];
170
+ }) {
118
171
  return {
119
- execShell: (call: any) => executeShellCommand({
120
- call,
121
- cwd: deps.cwd ?? process.cwd(),
122
- }),
123
- ...(deps.localToolExecutors ?? {}),
172
+ ...createLocalWorkspaceToolExecutors({ workspaceRoot: args.workspaceRoot }),
173
+ ...(args.localToolExecutors ?? {}),
124
174
  };
125
175
  }
126
176
 
127
- async function resolveDb(deps: CliLocalRuntimeAdapterDeps) {
128
- return deps.db ?? defaultLocalRuntimeDb();
177
+ function readLocalWorkspaceMode(agentConfig: AgentRuntimeAgentConfig | null) {
178
+ if (agentConfig?.localWorkspaceMode === "task-worktree") return "task-worktree";
179
+ const runtimeBinding = agentConfig?.runtimeBinding;
180
+ const value = runtimeBinding?.localWorkspaceMode ?? runtimeBinding?.workspaceMode;
181
+ return value === "task-worktree" ? "task-worktree" : "current";
129
182
  }
130
183
 
131
- async function readAgentFromDb(args: {
132
- db: CliLocalRuntimeDb;
184
+ async function resolveStore(deps: CliLocalRuntimeAdapterDeps) {
185
+ if (deps.store) return deps.store;
186
+ return createCliHybridRecordStore({
187
+ db: deps.db ?? await defaultLocalRuntimeDb(),
188
+ env: deps.env,
189
+ fetchImpl: deps.fetchImpl,
190
+ });
191
+ }
192
+
193
+ async function readAgentFromStore(args: {
194
+ store: HybridRecordStore;
133
195
  agentRef: string;
134
196
  userId: string;
135
197
  }): Promise<AgentRuntimeAgentConfig | null> {
136
- const candidates = [
137
- args.agentRef,
138
- `agent-${args.userId}-${args.agentRef}`,
139
- `cybot-${args.userId}-${args.agentRef}`,
140
- ];
141
- for (const key of candidates) {
142
- try {
143
- const record = await args.db.get(key);
144
- if (!record || typeof record !== "object") continue;
145
- return {
146
- key,
147
- ...(typeof record.name === "string" ? { name: record.name } : {}),
148
- ...(typeof record.prompt === "string" ? { prompt: record.prompt } : {}),
149
- ...(typeof record.model === "string" ? { model: record.model } : {}),
150
- ...(typeof record.provider === "string" ? { provider: record.provider } : {}),
151
- ...(Array.isArray(record.toolNames) ? { toolNames: record.toolNames } : {}),
152
- };
153
- } catch {
154
- // Try the next local LevelDB key convention.
155
- }
198
+ for (const key of buildLocalAgentLookupKeys(args)) {
199
+ const record = await args.store.read(key, {
200
+ remote: shouldReadAgentKeyRemotely(key),
201
+ });
202
+ if (!record || typeof record !== "object") continue;
203
+ return resolveAgentRuntimeConfigFromRecord(key, record);
156
204
  }
157
205
  return null;
158
206
  }
159
207
 
160
- function toOpenAiMessages(messages: AgentRuntimeChatMessage[]) {
161
- return messages.map((message) => ({
162
- role: message.role,
163
- content: message.content ?? "",
164
- ...(message.tool_call_id ? { tool_call_id: message.tool_call_id } : {}),
165
- ...(Array.isArray(message.tool_calls) ? { tool_calls: message.tool_calls } : {}),
166
- }));
167
- }
168
-
169
208
  async function readDialogMessages(args: {
170
- db: CliLocalRuntimeDb;
209
+ store: HybridRecordStore;
171
210
  dialogId: string;
172
211
  }) {
173
212
  const messages: AgentRuntimeChatMessage[] = [];
174
213
  const prefix = `dialog-${args.dialogId}-msg-`;
175
- const iterator = args.db.iterator({ gte: prefix, lte: `${prefix}\uffff` });
214
+ const iterator = args.store.iterator({ gte: prefix, lte: `${prefix}\uffff` });
176
215
  for await (const [, value] of iterator) {
177
- if (!value || typeof value !== "object") continue;
178
- if (value.role === "system") continue;
179
- if (value.role === "user" || value.role === "assistant" || value.role === "tool") {
180
- messages.push({
181
- role: value.role,
182
- content: value.content ?? null,
183
- ...(typeof value.toolCallId === "string" ? { tool_call_id: value.toolCallId } : {}),
184
- ...(Array.isArray(value.tool_calls) ? { tool_calls: value.tool_calls } : {}),
185
- });
186
- }
216
+ const message = localDialogMessageRecordToRuntimeMessage(value);
217
+ if (message) messages.push(message);
187
218
  }
188
219
  return messages;
189
220
  }
190
221
 
191
222
  async function writeDialog(args: {
192
- db: CliLocalRuntimeDb;
223
+ store: HybridRecordStore;
193
224
  input: AgentRuntimeSaveTurnInput;
194
225
  userId: string;
195
226
  now: () => number;
196
227
  createId: () => string;
197
228
  cwd?: string;
198
229
  }) {
199
- const dialogId = args.input.continueDialogId || args.createId();
200
- const now = args.now();
201
- const nowIso = new Date(now).toISOString();
202
- const dialogKey = `dialog-${args.userId}-${dialogId}`;
203
- const lastUser = [...args.input.messages].reverse().find((message) => message.role === "user");
204
- const lastUserText = typeof lastUser?.content === "string"
205
- ? lastUser.content
206
- : Array.isArray(lastUser?.content)
207
- ? lastUser.content
208
- .filter((part) => part.type === "text")
209
- .map((part) => part.text)
210
- .join(" ")
211
- : "";
212
230
  let existingDialog: any = null;
213
231
  if (args.input.continueDialogId) {
214
- try {
215
- existingDialog = await args.db.get(dialogKey);
216
- } catch {
217
- existingDialog = null;
218
- }
232
+ const dialogKey = `dialog-${args.userId}-${args.input.continueDialogId}`;
233
+ existingDialog = await args.store.read(dialogKey);
219
234
  }
220
- const messageOps = args.input.messages
221
- .filter((message) => message.role !== "system")
222
- .map((message, index) => {
223
- const id = `${now}-${String(index + 1).padStart(3, "0")}`;
224
- const key = `dialog-${dialogId}-msg-${id}`;
225
- return {
226
- type: "put" as const,
227
- key,
228
- value: {
229
- id,
230
- dbKey: key,
231
- dialogId,
232
- role: message.role,
233
- content: message.content ?? "",
234
- ...(message.role === "user" ? { userId: args.userId } : {}),
235
- ...(message.role === "assistant" ? {
236
- agentKey: args.input.agentKey,
237
- cybotKey: args.input.agentKey,
238
- } : {}),
239
- ...(message.tool_call_id ? { toolCallId: message.tool_call_id } : {}),
240
- ...(Array.isArray(message.tool_calls) ? { tool_calls: message.tool_calls } : {}),
241
- ...(message.tool_result_metadata ? { metadata: message.tool_result_metadata } : {}),
242
- createdAt: nowIso,
243
- },
244
- };
245
- });
246
- const ops: Array<{ type: "put"; key: string; value: any }> = [
247
- {
248
- type: "put",
249
- key: dialogKey,
250
- value: {
251
- ...(existingDialog && typeof existingDialog === "object" ? existingDialog : {}),
252
- id: dialogId,
253
- dbKey: dialogKey,
254
- type: "dialog",
255
- userId: args.userId,
256
- cybots: [args.input.agentKey],
257
- primaryAgentKey: args.input.agentKey,
258
- title: typeof existingDialog?.title === "string" && existingDialog.title.trim()
259
- ? existingDialog.title
260
- : lastUserText.trim()
261
- ? lastUserText.trim().slice(0, 80)
262
- : "Local agent run",
263
- status: "done",
264
- triggerType: "cli-local",
265
- executionMode: "foreground",
266
- createdAt: existingDialog?.createdAt ?? nowIso,
267
- updatedAt: nowIso,
268
- finishedAt: now,
269
- usage: args.input.result.usage,
270
- ...(typeof args.input.result.toolCallCount === "number"
271
- ? { toolCallCount: args.input.result.toolCallCount }
272
- : {}),
273
- localRuntime: {
274
- host: "cli",
275
- ...(args.cwd ? { worktreePath: args.cwd } : {}),
276
- },
277
- },
278
- },
279
- ...messageOps,
280
- ];
281
- await args.db.batch(ops);
282
- return { dialogId };
235
+ const plan = buildLocalDialogWritePlan({
236
+ input: args.input,
237
+ userId: args.userId,
238
+ now: args.now(),
239
+ createId: args.createId,
240
+ existingDialog,
241
+ cwd: args.cwd,
242
+ });
243
+ await args.store.batch(plan.ops);
244
+ return { dialogId: plan.dialogId };
283
245
  }
284
246
 
285
247
  export function createCliLocalRuntimeAdapter(
@@ -289,76 +251,139 @@ export function createCliLocalRuntimeAdapter(
289
251
  const createId = deps.createId ?? createFallbackId;
290
252
  const fetchImpl = deps.fetchImpl ?? fetch;
291
253
  const userId = resolveLocalUserId(deps.env);
254
+ const localToolBudgets = parseLocalToolBudgets(deps.env);
255
+ const localToolUsage = new Map<string, number>();
292
256
  let activeAgentToolNames: string[] = [];
293
- const localToolExecutors = buildLocalToolExecutors(deps);
257
+ let workspaceSession: WorkspaceSession = createWorkspaceSession({ cwd: deps.cwd });
258
+ let localToolExecutors = buildLocalToolExecutors({
259
+ workspaceRoot: workspaceSession.workspaceRoot,
260
+ localToolExecutors: deps.localToolExecutors,
261
+ });
262
+
263
+ async function activateAgentWorkspaceSession(args: {
264
+ agentRef: string;
265
+ agentConfig: AgentRuntimeAgentConfig | null;
266
+ }) {
267
+ const previousWorkspaceRoot = workspaceSession.workspaceRoot;
268
+ workspaceSession = await activateWorkspaceSession(workspaceSession, {
269
+ agentKey: args.agentConfig?.key ?? args.agentRef,
270
+ mode: readLocalWorkspaceMode(args.agentConfig),
271
+ env: deps.env,
272
+ prepareTaskWorkspace: deps.prepareTaskWorktree,
273
+ });
274
+ if (workspaceSession.workspaceRoot === previousWorkspaceRoot) return;
275
+ localToolExecutors = buildLocalToolExecutors({
276
+ workspaceRoot: workspaceSession.workspaceRoot,
277
+ localToolExecutors: deps.localToolExecutors,
278
+ });
279
+ deps.output?.write(formatWorkspaceSessionActivation(workspaceSession));
280
+ }
294
281
 
295
282
  return {
296
283
  host: "cli",
297
- capabilities: ["leveldb-agent-config", "local-provider", "leveldb-persistence"],
284
+ capabilities: ["leveldb-agent-config", "local-provider", "leveldb-persistence", "local-tools"],
298
285
  loadAgentConfig: async (agentRef) => {
299
- const agentConfig = await readAgentFromDb({
286
+ const agentConfig = await readAgentFromStore({
300
287
  agentRef,
301
- db: await resolveDb(deps),
288
+ store: await resolveStore(deps),
302
289
  userId,
303
290
  });
304
- activeAgentToolNames = agentConfig?.toolNames ?? [];
291
+ activeAgentToolNames = buildLocalPolicyToolNames({
292
+ toolNames: agentConfig?.toolNames,
293
+ env: deps.env,
294
+ });
295
+ await activateAgentWorkspaceSession({ agentRef, agentConfig });
305
296
  return agentConfig;
306
297
  },
307
298
  loadDialogHistory: async (dialogId) => readDialogMessages({
308
299
  dialogId,
309
- db: await resolveDb(deps),
300
+ store: await resolveStore(deps),
310
301
  }),
311
302
  saveTurn: async (input) => writeDialog({
312
- db: await resolveDb(deps),
303
+ store: await resolveStore(deps),
313
304
  input,
314
305
  userId,
315
306
  now,
316
307
  createId,
317
- cwd: deps.cwd,
308
+ cwd: workspaceSession.workspaceRoot,
318
309
  }),
319
- resolveProvider: async (agentConfig) => ({
320
- model: agentConfig.model || "gpt-4.1-mini",
321
- complete: async (messages) => {
322
- const tools = buildOpenAiTools({
323
- toolNames: agentConfig.toolNames,
310
+ resolveProvider: async (agentConfig) => {
311
+ if (shouldUsePlatformChatProvider(deps.env, agentConfig)) {
312
+ const providerConfig = resolvePlatformChatProviderConfig({
313
+ agentConfig,
324
314
  env: deps.env,
325
315
  });
326
- const res = await fetchImpl(`${resolveOpenAiCompatibleBaseUrl(deps.env)}/chat/completions`, {
327
- method: "POST",
328
- headers: {
329
- "Content-Type": "application/json",
330
- ...(resolveApiKey(deps.env)
331
- ? { Authorization: `Bearer ${resolveApiKey(deps.env)}` }
332
- : {}),
333
- },
334
- body: JSON.stringify({
335
- model: agentConfig.model || "gpt-4.1-mini",
336
- messages: toOpenAiMessages(messages),
337
- stream: false,
338
- ...(tools.length > 0 ? { tools } : {}),
339
- }),
340
- });
341
- const data = await res.json().catch(() => ({}));
342
- if (!res.ok) {
343
- throw new Error(`local provider failed: HTTP ${res.status} ${JSON.stringify(data)}`);
344
- }
345
- const choiceMessage = data?.choices?.[0]?.message ?? {};
346
- const content = String(choiceMessage?.content ?? "");
347
316
  return {
348
- content,
349
- model: agentConfig.model || "gpt-4.1-mini",
350
- provider: agentConfig.provider || "openai-compatible",
351
- ...(Array.isArray(choiceMessage?.tool_calls) ? { tool_calls: choiceMessage.tool_calls } : {}),
352
- usage: data?.usage,
353
- trace: messages,
317
+ model: providerConfig.model,
318
+ complete: async (messages) => {
319
+ const tools = buildOpenAiTools({
320
+ toolNames: agentConfig.toolNames,
321
+ env: deps.env,
322
+ });
323
+ const request = buildPlatformChatCompletionRequest({
324
+ providerConfig,
325
+ messages,
326
+ tools,
327
+ });
328
+ const res = await fetchWithTransientRetry(fetchImpl, request.url, request.init, {
329
+ sleep: deps.sleep,
330
+ });
331
+ const data = parsePlatformChatCompletionData(await res.text().catch(() => ""));
332
+ if (!res.ok) {
333
+ throw new Error(`platform provider failed: HTTP ${res.status} ${JSON.stringify(data)}`);
334
+ }
335
+ return parsePlatformChatCompletionResponse({
336
+ providerConfig,
337
+ data,
338
+ trace: messages,
339
+ });
340
+ },
354
341
  };
355
- },
356
- }),
357
- executeTool: async (call) => executeLocalToolWithPolicy({
358
- env: deps.env,
359
- agentToolNames: activeAgentToolNames,
360
- call,
361
- executors: localToolExecutors,
362
- }),
342
+ }
343
+
344
+ const providerConfig = resolveCliOpenAiProviderConfig({
345
+ agentConfig,
346
+ env: deps.env,
347
+ });
348
+ return {
349
+ model: providerConfig.model,
350
+ complete: async (messages) => {
351
+ const tools = buildOpenAiTools({
352
+ toolNames: agentConfig.toolNames,
353
+ env: deps.env,
354
+ });
355
+ const request = buildOpenAiCompatibleChatCompletionRequest({
356
+ providerConfig,
357
+ messages,
358
+ tools,
359
+ });
360
+ const res = await fetchWithTransientRetry(fetchImpl, request.url, request.init, {
361
+ sleep: deps.sleep,
362
+ });
363
+ const data = await res.json().catch(() => ({}));
364
+ if (!res.ok) {
365
+ throw new Error(`local provider failed: HTTP ${res.status} ${JSON.stringify(data)}`);
366
+ }
367
+ return parseOpenAiCompatibleChatCompletionResponse({
368
+ providerConfig,
369
+ data,
370
+ trace: messages,
371
+ });
372
+ },
373
+ };
374
+ },
375
+ executeTool: async (call) => {
376
+ assertWithinLocalToolBudget({
377
+ toolName: call.name,
378
+ budgets: localToolBudgets,
379
+ usage: localToolUsage,
380
+ });
381
+ return executeLocalToolWithPolicy({
382
+ env: deps.env,
383
+ agentToolNames: activeAgentToolNames,
384
+ call,
385
+ executors: localToolExecutors,
386
+ });
387
+ },
363
388
  };
364
389
  }