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.
- package/README.md +9 -1
- package/agent-runtime/agentConfigOptions.ts +12 -0
- package/agent-runtime/agentRecordConfig.ts +99 -0
- package/agent-runtime/agentRecordKeys.ts +14 -0
- package/agent-runtime/dialogMessageRecord.ts +16 -0
- package/agent-runtime/dialogWritePlan.ts +130 -0
- package/agent-runtime/hostAdapter.ts +13 -0
- package/agent-runtime/hybridRecordStore.ts +147 -0
- package/agent-runtime/index.ts +69 -0
- package/agent-runtime/localLoop.ts +69 -5
- package/agent-runtime/localToolPolicy.ts +130 -0
- package/agent-runtime/localWorkspaceTools.ts +1532 -0
- package/agent-runtime/openAiCompatibleProvider.ts +70 -0
- package/agent-runtime/openAiCompatibleProviderConfig.ts +38 -0
- package/agent-runtime/platformChatProvider.ts +241 -0
- package/agent-runtime/taskWorkspace.ts +193 -0
- package/agent-runtime/types.ts +1 -0
- package/agent-runtime/workspaceSession.ts +76 -0
- package/agentAliases.ts +37 -0
- package/agentPullCommand.ts +1 -1
- package/agentRunCommand.ts +278 -52
- package/agentRuntimeCommands.ts +354 -164
- package/agentRuntimeLocal.ts +38 -0
- package/ai/agent/agentSlice.ts +10 -0
- package/ai/agent/buildEditingContext.ts +5 -0
- package/ai/agent/buildSystemPrompt.ts +41 -18
- package/ai/agent/canvasEditingContext.ts +49 -0
- package/ai/agent/cliExecutor.ts +15 -4
- package/ai/agent/createAgentSchema.ts +2 -0
- package/ai/agent/executeToolCall.ts +3 -2
- package/ai/agent/hooks/usePublicAgents.ts +6 -0
- package/ai/agent/pageBuilderHandoffRules.ts +75 -0
- package/ai/agent/runAgentClientLoop.ts +4 -1
- package/ai/agent/runtimeGuidance.ts +19 -0
- package/ai/agent/server/fetchPublicAgents.ts +51 -1
- package/ai/agent/streamAgentChatTurn.ts +20 -2
- package/ai/agent/streamAgentChatTurnUtils.ts +60 -16
- package/ai/chat/accumulateToolCallChunks.ts +40 -9
- package/ai/chat/parseApiError.ts +3 -0
- package/ai/chat/sendOpenAICompletionsRequest.native.ts +23 -10
- package/ai/chat/sendOpenAICompletionsRequest.ts +13 -1
- package/ai/chat/updateTotalUsage.ts +26 -9
- package/ai/llm/deepinfra.ts +51 -0
- package/ai/llm/getPricing.ts +6 -0
- package/ai/llm/kimi.ts +2 -0
- package/ai/llm/openrouterModels.ts +0 -135
- package/ai/llm/providers.ts +1 -0
- package/ai/llm/types.ts +8 -0
- package/ai/taskRun/taskRunProtocol.ts +882 -0
- package/ai/token/calculatePrice.ts +30 -0
- package/ai/token/externalToolCost.ts +49 -29
- package/ai/token/prepareTokenUsageData.ts +6 -1
- package/ai/token/serverTokenWriter.ts +4 -2
- package/ai/tools/agent/agentTools.ts +21 -0
- package/ai/tools/agent/presets/appBuilderPreset.ts +7 -0
- package/ai/tools/agent/streamParallelAgentsTool.ts +2 -1
- package/ai/tools/agent/taskRunTool.ts +112 -0
- package/ai/tools/applyEditTool.ts +6 -3
- package/ai/tools/applyLineEditsTool.ts +6 -3
- package/ai/tools/checkEnvTool.ts +14 -9
- package/ai/tools/codeSearchTool.ts +17 -5
- package/ai/tools/execBashTool.ts +33 -29
- package/ai/tools/fetchWebpageSupport.ts +24 -0
- package/ai/tools/fetchWebpageTool.ts +18 -5
- package/ai/tools/index.ts +158 -0
- package/ai/tools/jdProductScraperTool.ts +821 -0
- package/ai/tools/listFilesTool.ts +6 -3
- package/ai/tools/localFilesTool.ts +200 -0
- package/ai/tools/readFileTool.ts +6 -3
- package/ai/tools/searchRepoTool.ts +6 -3
- package/ai/tools/table/rowTools.ts +6 -1
- package/ai/tools/taobaoTmallProductScraperTool.ts +49 -0
- package/ai/tools/toolApiClient.ts +20 -6
- package/ai/tools/wereadGatewayTool.ts +152 -0
- package/ai/tools/writeFileTool.ts +6 -3
- package/client/agentConfigResolver.test.ts +70 -0
- package/client/agentConfigResolver.ts +1 -0
- package/client/agentRun.test.ts +430 -7
- package/client/agentRun.ts +504 -64
- package/client/hybridRecordStore.test.ts +115 -0
- package/client/hybridRecordStore.ts +41 -0
- package/client/localAgentRecords.test.ts +27 -0
- package/client/localAgentRecords.ts +7 -0
- package/client/localDialogRecords.test.ts +124 -0
- package/client/localDialogRecords.ts +30 -0
- package/client/localProviderResolver.test.ts +78 -0
- package/client/localProviderResolver.ts +1 -0
- package/client/localRuntimeAdapter.test.ts +621 -9
- package/client/localRuntimeAdapter.ts +275 -250
- package/client/localRuntimeDryRun.test.ts +116 -0
- package/client/localToolPolicy.ts +8 -81
- package/client/taskRunPrompt.ts +26 -0
- package/client/taskWorktree.ts +8 -0
- package/client/workspaceSession.test.ts +57 -0
- package/client/workspaceSession.ts +11 -0
- package/commandRegistry.ts +23 -6
- package/connectorRunArtifact.ts +121 -0
- package/database/actions/write.ts +16 -2
- package/database/hooks/useUserData.ts +9 -3
- package/database/server/dataHandlers.ts +18 -20
- package/database/server/emailRepository.ts +3 -3
- package/database/server/patch.ts +18 -10
- package/database/server/query.ts +43 -4
- package/database/server/read.ts +24 -38
- package/database/server/recordIdentity.ts +100 -0
- package/database/server/write.ts +21 -25
- package/index.ts +70 -33
- package/machineCommands.ts +318 -144
- package/package.json +4 -1
- package/tableCommands.ts +181 -0
- package/taskRunCommand.ts +265 -0
|
@@ -1,9 +1,39 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
2
5
|
|
|
3
|
-
import { runLocalAgentTurn } from "
|
|
6
|
+
import { runLocalAgentTurn } from "../agent-runtime/localLoop";
|
|
4
7
|
import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
|
|
5
8
|
|
|
6
9
|
describe("CLI local runtime adapter", () => {
|
|
10
|
+
const DEFAULT_LOCAL_CODING_TOOL_NAMES = [
|
|
11
|
+
"readWorkspaceFile",
|
|
12
|
+
"writeWorkspaceFile",
|
|
13
|
+
"replaceWorkspaceText",
|
|
14
|
+
"searchWorkspace",
|
|
15
|
+
"applyPatch",
|
|
16
|
+
"gitStatus",
|
|
17
|
+
"gitDiff",
|
|
18
|
+
"gitCreateBranch",
|
|
19
|
+
"commitWorkspace",
|
|
20
|
+
"runPackageScript",
|
|
21
|
+
"startPreview",
|
|
22
|
+
"getPreviewStatus",
|
|
23
|
+
"stopPreview",
|
|
24
|
+
"releasePreview",
|
|
25
|
+
"captureVisualState",
|
|
26
|
+
];
|
|
27
|
+
const SHELL_LOCAL_CODING_TOOL_NAMES = [
|
|
28
|
+
...DEFAULT_LOCAL_CODING_TOOL_NAMES,
|
|
29
|
+
"execShell",
|
|
30
|
+
"execBash",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function toolNamesFromRequest(request: any) {
|
|
34
|
+
return request?.body?.tools?.map((tool: any) => tool.function.name) ?? [];
|
|
35
|
+
}
|
|
36
|
+
|
|
7
37
|
test("loads agent/history from LevelDB and saves dialog/message records back to LevelDB", async () => {
|
|
8
38
|
const requests: Array<{ url: string; body: any; auth: string | null }> = [];
|
|
9
39
|
const store = new Map<string, any>([
|
|
@@ -83,7 +113,7 @@ describe("CLI local runtime adapter", () => {
|
|
|
83
113
|
model: "gpt-4.1-mini",
|
|
84
114
|
dialogId: "dialog-existing",
|
|
85
115
|
});
|
|
86
|
-
expect(requests).
|
|
116
|
+
expect(requests[0]).toMatchObject({
|
|
87
117
|
url: "http://127.0.0.1:11434/v1/chat/completions",
|
|
88
118
|
auth: "Bearer sk-local",
|
|
89
119
|
body: {
|
|
@@ -95,7 +125,8 @@ describe("CLI local runtime adapter", () => {
|
|
|
95
125
|
],
|
|
96
126
|
stream: false,
|
|
97
127
|
},
|
|
98
|
-
}
|
|
128
|
+
});
|
|
129
|
+
expect(toolNamesFromRequest(requests[0])).toEqual(DEFAULT_LOCAL_CODING_TOOL_NAMES);
|
|
99
130
|
expect(batchOps.map((op) => op.key)).toEqual([
|
|
100
131
|
"dialog-user-1-dialog-existing",
|
|
101
132
|
"dialog-dialog-existing-msg-1710000000000-001",
|
|
@@ -120,6 +151,395 @@ describe("CLI local runtime adapter", () => {
|
|
|
120
151
|
});
|
|
121
152
|
});
|
|
122
153
|
|
|
154
|
+
test("loads a missing explicit agent key through the hybrid store remote cache", async () => {
|
|
155
|
+
const memory = new Map<string, any>();
|
|
156
|
+
const requests: string[] = [];
|
|
157
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
158
|
+
env: {
|
|
159
|
+
NOLO_LOCAL_USER_ID: "user-1",
|
|
160
|
+
NOLO_SERVER: "https://us.nolo.chat",
|
|
161
|
+
AUTH_TOKEN: "token-1",
|
|
162
|
+
OPENAI_API_KEY: "sk-local",
|
|
163
|
+
NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
|
|
164
|
+
},
|
|
165
|
+
db: {
|
|
166
|
+
get: async (key) => {
|
|
167
|
+
if (!memory.has(key)) throw new Error(`not found: ${key}`);
|
|
168
|
+
return memory.get(key);
|
|
169
|
+
},
|
|
170
|
+
put: async (key, value) => {
|
|
171
|
+
memory.set(key, value);
|
|
172
|
+
},
|
|
173
|
+
batch: async (ops) => {
|
|
174
|
+
for (const op of ops) {
|
|
175
|
+
if (op.type === "put") memory.set(op.key, op.value);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
iterator: () => (async function* () {})(),
|
|
179
|
+
},
|
|
180
|
+
createId: () => "01REMOTE",
|
|
181
|
+
fetchImpl: async (url, init) => {
|
|
182
|
+
requests.push(String(url));
|
|
183
|
+
if (String(url).includes("/api/v1/db/read/")) {
|
|
184
|
+
expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer token-1");
|
|
185
|
+
return Response.json({
|
|
186
|
+
data: {
|
|
187
|
+
dbKey: "agent-user-1-remote",
|
|
188
|
+
name: "Remote cached",
|
|
189
|
+
prompt: "Remote prompt",
|
|
190
|
+
model: "gpt-4.1-mini",
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return Response.json({
|
|
195
|
+
choices: [{ message: { content: "remote cache ok" } }],
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const result = await runLocalAgentTurn({
|
|
201
|
+
adapter,
|
|
202
|
+
agentRef: "agent-user-1-remote",
|
|
203
|
+
input: "hello",
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(result.content).toBe("remote cache ok");
|
|
207
|
+
expect(requests[0]).toBe("https://us.nolo.chat/api/v1/db/read/agent-user-1-remote");
|
|
208
|
+
expect(memory.get("agent-user-1-remote")).toMatchObject({
|
|
209
|
+
name: "Remote cached",
|
|
210
|
+
serverOrigin: "https://us.nolo.chat",
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("uses agent customProviderUrl for local OpenAI-compatible requests", async () => {
|
|
215
|
+
const requests: Array<{ url: string; auth: string | null; body: any }> = [];
|
|
216
|
+
const store = new Map<string, any>([
|
|
217
|
+
["agent-user-1-custom", {
|
|
218
|
+
dbKey: "agent-user-1-custom",
|
|
219
|
+
id: "custom",
|
|
220
|
+
prompt: "Use custom provider.",
|
|
221
|
+
model: "custom-coder",
|
|
222
|
+
provider: "custom-openai-compatible",
|
|
223
|
+
apiSource: "custom",
|
|
224
|
+
customProviderUrl: "https://provider.example/v1/chat/completions",
|
|
225
|
+
temperature: 0.2,
|
|
226
|
+
max_tokens: 4096,
|
|
227
|
+
reasoning_effort: "medium",
|
|
228
|
+
}],
|
|
229
|
+
]);
|
|
230
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
231
|
+
env: {
|
|
232
|
+
NOLO_LOCAL_USER_ID: "user-1",
|
|
233
|
+
OPENAI_API_KEY: "sk-custom",
|
|
234
|
+
NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
|
|
235
|
+
},
|
|
236
|
+
db: {
|
|
237
|
+
get: async (key) => {
|
|
238
|
+
if (!store.has(key)) throw new Error(`not found: ${key}`);
|
|
239
|
+
return store.get(key);
|
|
240
|
+
},
|
|
241
|
+
put: async (key, value) => {
|
|
242
|
+
store.set(key, value);
|
|
243
|
+
},
|
|
244
|
+
batch: async (ops) => {
|
|
245
|
+
for (const op of ops) {
|
|
246
|
+
if (op.type === "put") store.set(op.key, op.value);
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
iterator: () => (async function* () {})(),
|
|
250
|
+
},
|
|
251
|
+
fetchImpl: async (url, init) => {
|
|
252
|
+
requests.push({
|
|
253
|
+
url: String(url),
|
|
254
|
+
auth: new Headers(init?.headers).get("Authorization"),
|
|
255
|
+
body: JSON.parse(String(init?.body)),
|
|
256
|
+
});
|
|
257
|
+
return Response.json({
|
|
258
|
+
choices: [{ message: { content: "custom ok" } }],
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const result = await runLocalAgentTurn({
|
|
264
|
+
adapter,
|
|
265
|
+
agentRef: "custom",
|
|
266
|
+
input: "hello",
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
expect(result.content).toBe("custom ok");
|
|
270
|
+
expect(result.provider).toBe("custom-openai-compatible");
|
|
271
|
+
expect(requests[0]).toMatchObject({
|
|
272
|
+
url: "https://provider.example/v1/chat/completions",
|
|
273
|
+
auth: "Bearer sk-custom",
|
|
274
|
+
body: {
|
|
275
|
+
model: "custom-coder",
|
|
276
|
+
messages: [
|
|
277
|
+
{ role: "system", content: "Use custom provider." },
|
|
278
|
+
{ role: "user", content: "hello" },
|
|
279
|
+
],
|
|
280
|
+
stream: false,
|
|
281
|
+
temperature: 0.2,
|
|
282
|
+
max_tokens: 4096,
|
|
283
|
+
reasoning_effort: "medium",
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
expect(toolNamesFromRequest(requests[0])).toEqual(DEFAULT_LOCAL_CODING_TOOL_NAMES);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("uses the Nolo chat proxy when local provider keys are absent", async () => {
|
|
290
|
+
const requests: Array<{ url: string; auth: string | null; body: any }> = [];
|
|
291
|
+
const store = new Map<string, any>([
|
|
292
|
+
["agent-user-1-frontend", {
|
|
293
|
+
dbKey: "agent-user-1-frontend",
|
|
294
|
+
id: "frontend",
|
|
295
|
+
prompt: "Fix UI.",
|
|
296
|
+
model: "accounts/fireworks/models/kimi-k2p6",
|
|
297
|
+
provider: "fireworks",
|
|
298
|
+
tools: ["writeWorkspaceFile"],
|
|
299
|
+
}],
|
|
300
|
+
]);
|
|
301
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
302
|
+
env: {
|
|
303
|
+
NOLO_LOCAL_USER_ID: "user-1",
|
|
304
|
+
NOLO_SERVER: "https://us.nolo.chat",
|
|
305
|
+
AUTH_TOKEN: "token-1",
|
|
306
|
+
},
|
|
307
|
+
db: {
|
|
308
|
+
get: async (key) => {
|
|
309
|
+
if (!store.has(key)) throw new Error(`not found: ${key}`);
|
|
310
|
+
return store.get(key);
|
|
311
|
+
},
|
|
312
|
+
put: async (key, value) => {
|
|
313
|
+
store.set(key, value);
|
|
314
|
+
},
|
|
315
|
+
batch: async (ops) => {
|
|
316
|
+
for (const op of ops) {
|
|
317
|
+
if (op.type === "put") store.set(op.key, op.value);
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
iterator: () => (async function* () {})(),
|
|
321
|
+
},
|
|
322
|
+
fetchImpl: async (url, init) => {
|
|
323
|
+
requests.push({
|
|
324
|
+
url: String(url),
|
|
325
|
+
auth: new Headers(init?.headers).get("Authorization"),
|
|
326
|
+
body: JSON.parse(String(init?.body)),
|
|
327
|
+
});
|
|
328
|
+
return Response.json({
|
|
329
|
+
choices: [{ message: { content: "platform ok" } }],
|
|
330
|
+
usage: { prompt_tokens: 7, completion_tokens: 2 },
|
|
331
|
+
});
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const result = await runLocalAgentTurn({
|
|
336
|
+
adapter,
|
|
337
|
+
agentRef: "frontend",
|
|
338
|
+
input: "make notifications cleaner",
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
expect(result.content).toBe("platform ok");
|
|
342
|
+
expect(result.provider).toBe("fireworks");
|
|
343
|
+
expect(requests[0]).toMatchObject({
|
|
344
|
+
url: "https://us.nolo.chat/api/v1/chat",
|
|
345
|
+
auth: "Bearer token-1",
|
|
346
|
+
body: {
|
|
347
|
+
model: "accounts/fireworks/models/kimi-k2p6",
|
|
348
|
+
messages: [
|
|
349
|
+
{ role: "system", content: "Fix UI." },
|
|
350
|
+
{ role: "user", content: "make notifications cleaner" },
|
|
351
|
+
],
|
|
352
|
+
stream: false,
|
|
353
|
+
tool_choice: "auto",
|
|
354
|
+
url: "https://api.fireworks.ai/inference/v1/chat/completions",
|
|
355
|
+
provider: "fireworks",
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
expect(toolNamesFromRequest(requests[0])).toEqual(DEFAULT_LOCAL_CODING_TOOL_NAMES);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("uses the Nolo chat proxy for platform agents even when direct provider env exists", async () => {
|
|
362
|
+
const requests: Array<{ url: string; auth: string | null; body: any }> = [];
|
|
363
|
+
const store = new Map<string, any>([
|
|
364
|
+
["agent-user-1-frontend", {
|
|
365
|
+
dbKey: "agent-user-1-frontend",
|
|
366
|
+
id: "frontend",
|
|
367
|
+
prompt: "Fix UI.",
|
|
368
|
+
model: "accounts/fireworks/models/kimi-k2p6",
|
|
369
|
+
provider: "fireworks",
|
|
370
|
+
apiSource: "platform",
|
|
371
|
+
useServerProxy: true,
|
|
372
|
+
tools: ["gitStatus"],
|
|
373
|
+
}],
|
|
374
|
+
]);
|
|
375
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
376
|
+
env: {
|
|
377
|
+
NOLO_LOCAL_USER_ID: "user-1",
|
|
378
|
+
NOLO_SERVER: "https://us.nolo.chat",
|
|
379
|
+
AUTH_TOKEN: "token-1",
|
|
380
|
+
OPENAI_API_KEY: "sk-direct",
|
|
381
|
+
},
|
|
382
|
+
db: {
|
|
383
|
+
get: async (key) => {
|
|
384
|
+
if (!store.has(key)) throw new Error(`not found: ${key}`);
|
|
385
|
+
return store.get(key);
|
|
386
|
+
},
|
|
387
|
+
put: async (key, value) => {
|
|
388
|
+
store.set(key, value);
|
|
389
|
+
},
|
|
390
|
+
batch: async (ops) => {
|
|
391
|
+
for (const op of ops) {
|
|
392
|
+
if (op.type === "put") store.set(op.key, op.value);
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
iterator: () => (async function* () {})(),
|
|
396
|
+
},
|
|
397
|
+
fetchImpl: async (url, init) => {
|
|
398
|
+
requests.push({
|
|
399
|
+
url: String(url),
|
|
400
|
+
auth: new Headers(init?.headers).get("Authorization"),
|
|
401
|
+
body: JSON.parse(String(init?.body)),
|
|
402
|
+
});
|
|
403
|
+
return Response.json({
|
|
404
|
+
choices: [{ message: { content: "platform ok" } }],
|
|
405
|
+
});
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const result = await runLocalAgentTurn({
|
|
410
|
+
adapter,
|
|
411
|
+
agentRef: "frontend",
|
|
412
|
+
input: "inspect",
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
expect(result.content).toBe("platform ok");
|
|
416
|
+
expect(requests[0]).toMatchObject({
|
|
417
|
+
url: "https://us.nolo.chat/api/v1/chat",
|
|
418
|
+
auth: "Bearer token-1",
|
|
419
|
+
body: {
|
|
420
|
+
provider: "fireworks",
|
|
421
|
+
apiSource: "platform",
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("retries transient certificate failures from the platform chat proxy", async () => {
|
|
427
|
+
let attempts = 0;
|
|
428
|
+
const store = new Map<string, any>([
|
|
429
|
+
["agent-user-1-frontend", {
|
|
430
|
+
dbKey: "agent-user-1-frontend",
|
|
431
|
+
id: "frontend",
|
|
432
|
+
prompt: "Fix UI.",
|
|
433
|
+
model: "accounts/fireworks/models/kimi-k2p6",
|
|
434
|
+
provider: "fireworks",
|
|
435
|
+
apiSource: "platform",
|
|
436
|
+
useServerProxy: true,
|
|
437
|
+
}],
|
|
438
|
+
]);
|
|
439
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
440
|
+
env: {
|
|
441
|
+
NOLO_LOCAL_USER_ID: "user-1",
|
|
442
|
+
NOLO_SERVER: "https://us.nolo.chat",
|
|
443
|
+
AUTH_TOKEN: "token-1",
|
|
444
|
+
},
|
|
445
|
+
db: {
|
|
446
|
+
get: async (key) => {
|
|
447
|
+
if (!store.has(key)) throw new Error(`not found: ${key}`);
|
|
448
|
+
return store.get(key);
|
|
449
|
+
},
|
|
450
|
+
put: async (key, value) => {
|
|
451
|
+
store.set(key, value);
|
|
452
|
+
},
|
|
453
|
+
batch: async (ops) => {
|
|
454
|
+
for (const op of ops) {
|
|
455
|
+
if (op.type === "put") store.set(op.key, op.value);
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
iterator: () => (async function* () {})(),
|
|
459
|
+
},
|
|
460
|
+
fetchImpl: async () => {
|
|
461
|
+
attempts += 1;
|
|
462
|
+
if (attempts <= 2) {
|
|
463
|
+
throw new Error("unknown certificate verification error");
|
|
464
|
+
}
|
|
465
|
+
return Response.json({
|
|
466
|
+
choices: [{ message: { content: "platform retry ok" } }],
|
|
467
|
+
});
|
|
468
|
+
},
|
|
469
|
+
sleep: async () => {},
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const result = await runLocalAgentTurn({
|
|
473
|
+
adapter,
|
|
474
|
+
agentRef: "frontend",
|
|
475
|
+
input: "inspect",
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
expect(result.content).toBe("platform retry ok");
|
|
479
|
+
expect(attempts).toBe(3);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("keeps retrying repeated transient certificate failures with backoff hooks", async () => {
|
|
483
|
+
let attempts = 0;
|
|
484
|
+
const retryDelays: number[] = [];
|
|
485
|
+
const store = new Map<string, any>([
|
|
486
|
+
["agent-user-1-frontend", {
|
|
487
|
+
dbKey: "agent-user-1-frontend",
|
|
488
|
+
id: "frontend",
|
|
489
|
+
prompt: "Fix UI.",
|
|
490
|
+
model: "accounts/fireworks/models/kimi-k2p6",
|
|
491
|
+
provider: "fireworks",
|
|
492
|
+
apiSource: "platform",
|
|
493
|
+
useServerProxy: true,
|
|
494
|
+
}],
|
|
495
|
+
]);
|
|
496
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
497
|
+
env: {
|
|
498
|
+
NOLO_LOCAL_USER_ID: "user-1",
|
|
499
|
+
NOLO_SERVER: "https://us.nolo.chat",
|
|
500
|
+
AUTH_TOKEN: "token-1",
|
|
501
|
+
},
|
|
502
|
+
db: {
|
|
503
|
+
get: async (key) => {
|
|
504
|
+
if (!store.has(key)) throw new Error(`not found: ${key}`);
|
|
505
|
+
return store.get(key);
|
|
506
|
+
},
|
|
507
|
+
put: async (key, value) => {
|
|
508
|
+
store.set(key, value);
|
|
509
|
+
},
|
|
510
|
+
batch: async (ops) => {
|
|
511
|
+
for (const op of ops) {
|
|
512
|
+
if (op.type === "put") store.set(op.key, op.value);
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
iterator: () => (async function* () {})(),
|
|
516
|
+
},
|
|
517
|
+
fetchImpl: async () => {
|
|
518
|
+
attempts += 1;
|
|
519
|
+
if (attempts <= 5) {
|
|
520
|
+
throw new Error("unknown certificate verification error");
|
|
521
|
+
}
|
|
522
|
+
return Response.json({
|
|
523
|
+
choices: [{ message: { content: "platform longer retry ok" } }],
|
|
524
|
+
});
|
|
525
|
+
},
|
|
526
|
+
sleep: async (ms) => {
|
|
527
|
+
retryDelays.push(ms);
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
const result = await runLocalAgentTurn({
|
|
532
|
+
adapter,
|
|
533
|
+
agentRef: "frontend",
|
|
534
|
+
input: "inspect",
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
expect(result.content).toBe("platform longer retry ok");
|
|
538
|
+
expect(attempts).toBe(6);
|
|
539
|
+
expect(retryDelays.length).toBe(5);
|
|
540
|
+
expect(retryDelays[0]).toBeGreaterThan(0);
|
|
541
|
+
});
|
|
542
|
+
|
|
123
543
|
test("saves local tool call trace and shell metadata into the local dialog", async () => {
|
|
124
544
|
const store = new Map<string, any>([
|
|
125
545
|
["agent-user-1-shell", {
|
|
@@ -221,6 +641,83 @@ describe("CLI local runtime adapter", () => {
|
|
|
221
641
|
});
|
|
222
642
|
});
|
|
223
643
|
|
|
644
|
+
test("returns tool budget errors to the local loop", async () => {
|
|
645
|
+
const store = new Map<string, any>([
|
|
646
|
+
["agent-user-1-reader", {
|
|
647
|
+
dbKey: "agent-user-1-reader",
|
|
648
|
+
id: "reader",
|
|
649
|
+
prompt: "Read narrowly",
|
|
650
|
+
model: "gpt-4.1-mini",
|
|
651
|
+
provider: "openai-compatible",
|
|
652
|
+
tools: ["readWorkspaceFile"],
|
|
653
|
+
}],
|
|
654
|
+
]);
|
|
655
|
+
let requestCount = 0;
|
|
656
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
657
|
+
env: {
|
|
658
|
+
NOLO_LOCAL_USER_ID: "user-1",
|
|
659
|
+
NOLO_LOCAL_OPENAI_BASE_URL: "https://llm.example/v1",
|
|
660
|
+
NOLO_LOCAL_OPENAI_API_KEY: "sk-test",
|
|
661
|
+
NOLO_LOCAL_TOOL_BUDGETS: "readWorkspaceFile=1",
|
|
662
|
+
},
|
|
663
|
+
store: {
|
|
664
|
+
read: async (key) => {
|
|
665
|
+
if (!store.has(key)) throw new Error(`not found: ${key}`);
|
|
666
|
+
return store.get(key);
|
|
667
|
+
},
|
|
668
|
+
put: async (key, value) => {
|
|
669
|
+
store.set(key, value);
|
|
670
|
+
},
|
|
671
|
+
batch: async (ops) => {
|
|
672
|
+
for (const op of ops) {
|
|
673
|
+
if (op.type === "put") store.set(op.key, op.value);
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
iterator: () => (async function* () {})(),
|
|
677
|
+
},
|
|
678
|
+
cwd: import.meta.dir,
|
|
679
|
+
now: () => 1710000000000,
|
|
680
|
+
createId: () => "01BUDGET",
|
|
681
|
+
localToolExecutors: {
|
|
682
|
+
readWorkspaceFile: async () => ({ content: "file content" }),
|
|
683
|
+
},
|
|
684
|
+
fetchImpl: async (_url, init) => {
|
|
685
|
+
requestCount += 1;
|
|
686
|
+
const body = JSON.parse(String(init?.body));
|
|
687
|
+
const toolMessages = body.messages.filter((message: any) => message.role === "tool");
|
|
688
|
+
if (toolMessages.some((message: any) => String(message.content).includes("exceeded local tool budget"))) {
|
|
689
|
+
return Response.json({ choices: [{ message: { content: "budget handled" } }] });
|
|
690
|
+
}
|
|
691
|
+
return Response.json({
|
|
692
|
+
choices: [{
|
|
693
|
+
message: {
|
|
694
|
+
content: "",
|
|
695
|
+
tool_calls: [{
|
|
696
|
+
id: `call-read-${requestCount}`,
|
|
697
|
+
type: "function",
|
|
698
|
+
function: {
|
|
699
|
+
name: "readWorkspaceFile",
|
|
700
|
+
arguments: JSON.stringify({ path: "file.ts" }),
|
|
701
|
+
},
|
|
702
|
+
}],
|
|
703
|
+
},
|
|
704
|
+
}],
|
|
705
|
+
});
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
const result = await runLocalAgentTurn({
|
|
710
|
+
adapter,
|
|
711
|
+
agentRef: "agent-user-1-reader",
|
|
712
|
+
input: "inspect",
|
|
713
|
+
maxToolRounds: 3,
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
expect(result.content).toBe("budget handled");
|
|
717
|
+
const messages = [...store.values()];
|
|
718
|
+
expect(messages.some((message) => String(message.content).includes("exceeded local tool budget 1"))).toBe(true);
|
|
719
|
+
});
|
|
720
|
+
|
|
224
721
|
test("continues a local dialog instead of creating a new one", async () => {
|
|
225
722
|
const store = new Map<string, any>([
|
|
226
723
|
["agent-user-1-frontend", {
|
|
@@ -428,12 +925,83 @@ describe("CLI local runtime adapter", () => {
|
|
|
428
925
|
input: "pwd",
|
|
429
926
|
});
|
|
430
927
|
|
|
431
|
-
expect(requests[0]
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
928
|
+
expect(toolNamesFromRequest(requests[0])).toEqual(SHELL_LOCAL_CODING_TOOL_NAMES);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
test("allows default semantic workspace tools without legacy agent tool declarations", async () => {
|
|
932
|
+
const workspaceRoot = mkdtempSync(join(tmpdir(), "nolo-cli-runtime-"));
|
|
933
|
+
try {
|
|
934
|
+
await Bun.write(join(workspaceRoot, "README.md"), "local ok\n");
|
|
935
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
936
|
+
env: {},
|
|
937
|
+
cwd: workspaceRoot,
|
|
938
|
+
db: {
|
|
939
|
+
get: async () => ({
|
|
940
|
+
dbKey: "agent-local-default-tools",
|
|
941
|
+
prompt: "Use local workspace tools.",
|
|
942
|
+
model: "gpt-4.1-mini",
|
|
943
|
+
}),
|
|
944
|
+
put: async () => {},
|
|
945
|
+
batch: async () => {},
|
|
946
|
+
iterator: () => (async function* () {})(),
|
|
947
|
+
},
|
|
948
|
+
fetchImpl: async () => Response.json({}),
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
await adapter.loadAgentConfig("default-tools");
|
|
952
|
+
await expect(adapter.executeTool({
|
|
953
|
+
id: "call-read",
|
|
954
|
+
name: "readWorkspaceFile",
|
|
955
|
+
arguments: JSON.stringify({ path: "README.md" }),
|
|
956
|
+
})).resolves.toMatchObject({
|
|
957
|
+
content: "local ok\n",
|
|
958
|
+
});
|
|
959
|
+
} finally {
|
|
960
|
+
rmSync(workspaceRoot, { recursive: true, force: true });
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
test("activates an agent-declared task worktree before running workspace tools", async () => {
|
|
965
|
+
const taskWorktreeRoot = mkdtempSync(join(tmpdir(), "nolo-cli-runtime-worktree-"));
|
|
966
|
+
const chunks: string[] = [];
|
|
967
|
+
try {
|
|
968
|
+
await Bun.write(join(taskWorktreeRoot, "README.md"), "worktree ok\n");
|
|
969
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
970
|
+
env: {
|
|
971
|
+
NOLO_LOCAL_USER_ID: "user-1",
|
|
972
|
+
},
|
|
973
|
+
output: { write: (chunk) => chunks.push(chunk) },
|
|
974
|
+
db: {
|
|
975
|
+
get: async () => ({
|
|
976
|
+
dbKey: "agent-user-1-frontend",
|
|
977
|
+
id: "frontend",
|
|
978
|
+
prompt: "Use local workspace tools.",
|
|
979
|
+
runtimeBinding: { localWorkspaceMode: "task-worktree" },
|
|
980
|
+
}),
|
|
981
|
+
put: async () => {},
|
|
982
|
+
batch: async () => {},
|
|
983
|
+
iterator: () => (async function* () {})(),
|
|
984
|
+
},
|
|
985
|
+
prepareTaskWorktree: async ({ agentKey }) => ({
|
|
986
|
+
path: taskWorktreeRoot,
|
|
987
|
+
branchName: `nolo-agent-${agentKey}`,
|
|
988
|
+
}),
|
|
989
|
+
fetchImpl: async () => Response.json({}),
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
await adapter.loadAgentConfig("frontend");
|
|
993
|
+
const result = await adapter.executeTool({
|
|
994
|
+
id: "call-read",
|
|
995
|
+
name: "readWorkspaceFile",
|
|
996
|
+
arguments: JSON.stringify({ path: "README.md" }),
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
expect(result.content).toBe("worktree ok\n");
|
|
1000
|
+
expect(chunks.join("")).toContain(`workspace session: task-worktree ${taskWorktreeRoot}`);
|
|
1001
|
+
expect(chunks.join("")).toContain("branch nolo-agent-frontend");
|
|
1002
|
+
} finally {
|
|
1003
|
+
rmSync(taskWorktreeRoot, { recursive: true, force: true });
|
|
1004
|
+
}
|
|
437
1005
|
});
|
|
438
1006
|
|
|
439
1007
|
test("runs execShell locally in explicit worktree shell mode", async () => {
|
|
@@ -462,4 +1030,48 @@ describe("CLI local runtime adapter", () => {
|
|
|
462
1030
|
expect(result.content).toContain(import.meta.dir);
|
|
463
1031
|
expect(result.metadata).toMatchObject({ exitCode: 0 });
|
|
464
1032
|
});
|
|
1033
|
+
|
|
1034
|
+
test("runs allowed workspace file tools through default local executors", async () => {
|
|
1035
|
+
const workspaceRoot = mkdtempSync(join(tmpdir(), "nolo-cli-runtime-"));
|
|
1036
|
+
try {
|
|
1037
|
+
const store = new Map<string, any>([
|
|
1038
|
+
["agent-user-1-writer", {
|
|
1039
|
+
dbKey: "agent-user-1-writer",
|
|
1040
|
+
id: "writer",
|
|
1041
|
+
toolNames: ["writeWorkspaceFile"],
|
|
1042
|
+
}],
|
|
1043
|
+
]);
|
|
1044
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
1045
|
+
env: {
|
|
1046
|
+
NOLO_LOCAL_USER_ID: "user-1",
|
|
1047
|
+
},
|
|
1048
|
+
db: {
|
|
1049
|
+
get: async (key) => {
|
|
1050
|
+
if (!store.has(key)) throw new Error(`not found: ${key}`);
|
|
1051
|
+
return store.get(key);
|
|
1052
|
+
},
|
|
1053
|
+
put: async () => {},
|
|
1054
|
+
batch: async () => {},
|
|
1055
|
+
iterator: () => (async function* () {})(),
|
|
1056
|
+
},
|
|
1057
|
+
cwd: workspaceRoot,
|
|
1058
|
+
fetchImpl: async () => Response.json({}),
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
await adapter.loadAgentConfig("writer");
|
|
1062
|
+
const result = await adapter.executeTool({
|
|
1063
|
+
id: "call-1",
|
|
1064
|
+
name: "writeWorkspaceFile",
|
|
1065
|
+
arguments: JSON.stringify({
|
|
1066
|
+
path: "src/app.ts",
|
|
1067
|
+
content: "export const cliValue = 1;\n",
|
|
1068
|
+
}),
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
expect(result.content).toBe("wrote src/app.ts");
|
|
1072
|
+
expect(readFileSync(join(workspaceRoot, "src/app.ts"), "utf8")).toBe("export const cliValue = 1;\n");
|
|
1073
|
+
} finally {
|
|
1074
|
+
rmSync(workspaceRoot, { recursive: true, force: true });
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
465
1077
|
});
|