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
@@ -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,231 +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));
78
117
  }
79
118
 
80
- function parseToolArguments(raw: string) {
81
- try {
82
- const parsed = JSON.parse(raw || "{}");
83
- return parsed && typeof parsed === "object" ? parsed : {};
84
- } catch {
85
- return {};
119
+ function transientFetchRetryDelayMs(attempt: number) {
120
+ return Math.min(attempt * TRANSIENT_FETCH_RETRY_BASE_DELAY_MS, 2_000);
121
+ }
122
+
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,
149
+ });
150
+ }
151
+
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),
101
164
  });
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
165
  }
116
166
 
117
- function buildLocalToolExecutors(deps: CliLocalRuntimeAdapterDeps) {
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
- }));
165
- }
166
-
167
208
  async function readDialogMessages(args: {
168
- db: CliLocalRuntimeDb;
209
+ store: HybridRecordStore;
169
210
  dialogId: string;
170
211
  }) {
171
212
  const messages: AgentRuntimeChatMessage[] = [];
172
213
  const prefix = `dialog-${args.dialogId}-msg-`;
173
- const iterator = args.db.iterator({ gte: prefix, lte: `${prefix}\uffff` });
214
+ const iterator = args.store.iterator({ gte: prefix, lte: `${prefix}\uffff` });
174
215
  for await (const [, value] of iterator) {
175
- if (!value || typeof value !== "object") continue;
176
- if (value.role === "system") continue;
177
- if (value.role === "user" || value.role === "assistant" || value.role === "tool") {
178
- messages.push({
179
- role: value.role,
180
- content: value.content ?? null,
181
- ...(typeof value.toolCallId === "string" ? { tool_call_id: value.toolCallId } : {}),
182
- ...(Array.isArray(value.tool_calls) ? { tool_calls: value.tool_calls } : {}),
183
- });
184
- }
216
+ const message = localDialogMessageRecordToRuntimeMessage(value);
217
+ if (message) messages.push(message);
185
218
  }
186
219
  return messages;
187
220
  }
188
221
 
189
222
  async function writeDialog(args: {
190
- db: CliLocalRuntimeDb;
223
+ store: HybridRecordStore;
191
224
  input: AgentRuntimeSaveTurnInput;
192
225
  userId: string;
193
226
  now: () => number;
194
227
  createId: () => string;
228
+ cwd?: string;
195
229
  }) {
196
- const dialogId = args.createId();
197
- const now = args.now();
198
- const nowIso = new Date(now).toISOString();
199
- const dialogKey = `dialog-${args.userId}-${dialogId}`;
200
- const lastUser = [...args.input.messages].reverse().find((message) => message.role === "user");
201
- const lastUserText = typeof lastUser?.content === "string"
202
- ? lastUser.content
203
- : Array.isArray(lastUser?.content)
204
- ? lastUser.content
205
- .filter((part) => part.type === "text")
206
- .map((part) => part.text)
207
- .join(" ")
208
- : "";
209
- const ops: Array<{ type: "put"; key: string; value: any }> = [
210
- {
211
- type: "put",
212
- key: dialogKey,
213
- value: {
214
- id: dialogId,
215
- dbKey: dialogKey,
216
- type: "dialog",
217
- userId: args.userId,
218
- cybots: [args.input.agentKey],
219
- primaryAgentKey: args.input.agentKey,
220
- title: lastUserText.trim()
221
- ? lastUserText.trim().slice(0, 80)
222
- : "Local agent run",
223
- status: "done",
224
- triggerType: "cli-local",
225
- executionMode: "foreground",
226
- createdAt: nowIso,
227
- updatedAt: nowIso,
228
- finishedAt: now,
229
- usage: args.input.result.usage,
230
- },
231
- },
232
- {
233
- type: "put",
234
- key: `dialog-${dialogId}-msg-user`,
235
- value: {
236
- id: "msg-user",
237
- dbKey: `dialog-${dialogId}-msg-user`,
238
- dialogId,
239
- role: "user",
240
- content: lastUser?.content ?? "",
241
- userId: args.userId,
242
- createdAt: nowIso,
243
- },
244
- },
245
- {
246
- type: "put",
247
- key: `dialog-${dialogId}-msg-assistant`,
248
- value: {
249
- id: "msg-assistant",
250
- dbKey: `dialog-${dialogId}-msg-assistant`,
251
- dialogId,
252
- role: "assistant",
253
- content: args.input.result.content,
254
- agentKey: args.input.agentKey,
255
- cybotKey: args.input.agentKey,
256
- createdAt: nowIso,
257
- },
258
- },
259
- ];
260
- await args.db.batch(ops);
261
- return { dialogId };
230
+ let existingDialog: any = null;
231
+ if (args.input.continueDialogId) {
232
+ const dialogKey = `dialog-${args.userId}-${args.input.continueDialogId}`;
233
+ existingDialog = await args.store.read(dialogKey);
234
+ }
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 };
262
245
  }
263
246
 
264
247
  export function createCliLocalRuntimeAdapter(
@@ -268,75 +251,139 @@ export function createCliLocalRuntimeAdapter(
268
251
  const createId = deps.createId ?? createFallbackId;
269
252
  const fetchImpl = deps.fetchImpl ?? fetch;
270
253
  const userId = resolveLocalUserId(deps.env);
254
+ const localToolBudgets = parseLocalToolBudgets(deps.env);
255
+ const localToolUsage = new Map<string, number>();
271
256
  let activeAgentToolNames: string[] = [];
272
- 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
+ }
273
281
 
274
282
  return {
275
283
  host: "cli",
276
- capabilities: ["leveldb-agent-config", "local-provider", "leveldb-persistence"],
284
+ capabilities: ["leveldb-agent-config", "local-provider", "leveldb-persistence", "local-tools"],
277
285
  loadAgentConfig: async (agentRef) => {
278
- const agentConfig = await readAgentFromDb({
286
+ const agentConfig = await readAgentFromStore({
279
287
  agentRef,
280
- db: await resolveDb(deps),
288
+ store: await resolveStore(deps),
281
289
  userId,
282
290
  });
283
- activeAgentToolNames = agentConfig?.toolNames ?? [];
291
+ activeAgentToolNames = buildLocalPolicyToolNames({
292
+ toolNames: agentConfig?.toolNames,
293
+ env: deps.env,
294
+ });
295
+ await activateAgentWorkspaceSession({ agentRef, agentConfig });
284
296
  return agentConfig;
285
297
  },
286
298
  loadDialogHistory: async (dialogId) => readDialogMessages({
287
299
  dialogId,
288
- db: await resolveDb(deps),
300
+ store: await resolveStore(deps),
289
301
  }),
290
302
  saveTurn: async (input) => writeDialog({
291
- db: await resolveDb(deps),
303
+ store: await resolveStore(deps),
292
304
  input,
293
305
  userId,
294
306
  now,
295
307
  createId,
308
+ cwd: workspaceSession.workspaceRoot,
296
309
  }),
297
- resolveProvider: async (agentConfig) => ({
298
- model: agentConfig.model || "gpt-4.1-mini",
299
- complete: async (messages) => {
300
- const tools = buildOpenAiTools({
301
- toolNames: agentConfig.toolNames,
310
+ resolveProvider: async (agentConfig) => {
311
+ if (shouldUsePlatformChatProvider(deps.env, agentConfig)) {
312
+ const providerConfig = resolvePlatformChatProviderConfig({
313
+ agentConfig,
302
314
  env: deps.env,
303
315
  });
304
- const res = await fetchImpl(`${resolveOpenAiCompatibleBaseUrl(deps.env)}/chat/completions`, {
305
- method: "POST",
306
- headers: {
307
- "Content-Type": "application/json",
308
- ...(resolveApiKey(deps.env)
309
- ? { Authorization: `Bearer ${resolveApiKey(deps.env)}` }
310
- : {}),
311
- },
312
- body: JSON.stringify({
313
- model: agentConfig.model || "gpt-4.1-mini",
314
- messages: toOpenAiMessages(messages),
315
- stream: false,
316
- ...(tools.length > 0 ? { tools } : {}),
317
- }),
318
- });
319
- const data = await res.json().catch(() => ({}));
320
- if (!res.ok) {
321
- throw new Error(`local provider failed: HTTP ${res.status} ${JSON.stringify(data)}`);
322
- }
323
- const choiceMessage = data?.choices?.[0]?.message ?? {};
324
- const content = String(choiceMessage?.content ?? "");
325
316
  return {
326
- content,
327
- model: agentConfig.model || "gpt-4.1-mini",
328
- provider: agentConfig.provider || "openai-compatible",
329
- ...(Array.isArray(choiceMessage?.tool_calls) ? { tool_calls: choiceMessage.tool_calls } : {}),
330
- usage: data?.usage,
331
- 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
+ },
332
341
  };
333
- },
334
- }),
335
- executeTool: async (call) => executeLocalToolWithPolicy({
336
- env: deps.env,
337
- agentToolNames: activeAgentToolNames,
338
- call,
339
- executors: localToolExecutors,
340
- }),
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
+ },
341
388
  };
342
389
  }