nolo-cli 0.1.33 → 0.1.34
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 +4 -4
- package/agent-runtime/agentRecordConfig.ts +8 -0
- package/agent-runtime/hostAdapter.ts +9 -1
- package/agent-runtime/index.ts +10 -0
- package/agent-runtime/localLoop.ts +49 -2
- package/agent-runtime/localToolPolicy.ts +70 -21
- package/agent-runtime/openAiCompatibleProvider.ts +7 -3
- package/agent-runtime/openAiCompatibleProviderConfig.ts +15 -26
- package/agent-runtime/platformChatProvider.ts +48 -71
- package/agent-runtime/providerResolution.ts +244 -0
- package/agent-runtime/runtimeToolPolicy.ts +33 -0
- package/agent-runtime/shellCommandPolicy.ts +120 -0
- package/agentInternalCommandEntries.ts +2 -0
- package/agentListCommands.ts +2 -2
- package/agentPullCommand.ts +6 -11
- package/agentRecordCommands.ts +87 -2
- package/agentRecordHelpers.ts +87 -4
- package/agentRuntimeLocal.ts +3 -0
- package/ai/tools/toolNameAliases.ts +70 -0
- package/cli/agentInternalCommandEntries.ts +2 -0
- package/cli/agentListCommands.ts +2 -2
- package/cli/agentPullCommand.ts +6 -11
- package/cli/agentRecordCommands.ts +87 -2
- package/cli/agentRecordHelpers.ts +87 -4
- package/cli/agentRuntimeLocal.ts +3 -0
- package/cli/cliCommandDispatch.ts +2 -2
- package/cli/cliEnvHelpers.ts +80 -12
- package/cli/client/agentRun.ts +1 -0
- package/cli/client/localRuntimeAdapter.ts +44 -7
- package/cli/commandRegistry.ts +2 -2
- package/cli/machineCommands.ts +1 -1
- package/cli/machineWsRunDispatch.ts +29 -9
- package/cli/offlineMarxistsAgentCommand.ts +1 -1
- package/cli/runtimeDoctorCommands.ts +10 -4
- package/cli/tableCommands.ts +1 -1
- package/cli/taskRunCommand.ts +1 -1
- package/cliCommandDispatch.ts +2 -2
- package/cliEnvHelpers.ts +80 -12
- package/client/agentRun.ts +1 -0
- package/client/localProviderResolver.test.ts +26 -2
- package/client/localRuntimeAdapter.test.ts +287 -3
- package/client/localRuntimeAdapter.ts +44 -7
- package/commandRegistry.ts +2 -2
- package/database/server/authorityStoreTypes.ts +31 -0
- package/database/server/db.ts +15 -34
- package/database/server/legacyServerDb.ts +58 -0
- package/database/server/levelAuthorityStore.ts +75 -0
- package/database/server/memoryAuthorityStore.ts +133 -0
- package/database/server/serverStoreFactory.ts +118 -0
- package/machineCommands.ts +1 -1
- package/machineWsRunDispatch.ts +29 -9
- package/offlineMarxistsAgentCommand.ts +1 -1
- package/package.json +10 -2
- package/runtimeDoctorCommands.ts +10 -4
- package/tableCommands.ts +1 -1
- package/taskRunCommand.ts +1 -1
|
@@ -278,8 +278,8 @@ describe("CLI local runtime adapter", () => {
|
|
|
278
278
|
});
|
|
279
279
|
});
|
|
280
280
|
|
|
281
|
-
test("uses agent
|
|
282
|
-
const requests: Array<{ url: string; auth: string | null; body: any }> = [];
|
|
281
|
+
test("uses agent-owned custom credentials for local OpenAI-compatible requests", async () => {
|
|
282
|
+
const requests: Array<{ url: string; auth: string | null; apiKeyHeader: string | null; body: any }> = [];
|
|
283
283
|
const store = new Map<string, any>([
|
|
284
284
|
["agent-user-1-custom", {
|
|
285
285
|
dbKey: "agent-user-1-custom",
|
|
@@ -289,6 +289,8 @@ describe("CLI local runtime adapter", () => {
|
|
|
289
289
|
provider: "custom-openai-compatible",
|
|
290
290
|
apiSource: "custom",
|
|
291
291
|
customProviderUrl: "https://provider.example/v1/chat/completions",
|
|
292
|
+
apiKey: "sk-agent-custom",
|
|
293
|
+
apiKeyHeader: "api-key",
|
|
292
294
|
temperature: 0.2,
|
|
293
295
|
max_tokens: 4096,
|
|
294
296
|
reasoning_effort: "medium",
|
|
@@ -319,6 +321,7 @@ describe("CLI local runtime adapter", () => {
|
|
|
319
321
|
requests.push({
|
|
320
322
|
url: String(url),
|
|
321
323
|
auth: new Headers(init?.headers).get("Authorization"),
|
|
324
|
+
apiKeyHeader: new Headers(init?.headers).get("api-key"),
|
|
322
325
|
body: JSON.parse(String(init?.body)),
|
|
323
326
|
});
|
|
324
327
|
return Response.json({
|
|
@@ -337,7 +340,8 @@ describe("CLI local runtime adapter", () => {
|
|
|
337
340
|
expect(result.provider).toBe("custom-openai-compatible");
|
|
338
341
|
expect(requests[0]).toMatchObject({
|
|
339
342
|
url: "https://provider.example/v1/chat/completions",
|
|
340
|
-
auth:
|
|
343
|
+
auth: null,
|
|
344
|
+
apiKeyHeader: "sk-agent-custom",
|
|
341
345
|
body: {
|
|
342
346
|
model: "custom-coder",
|
|
343
347
|
messages: [
|
|
@@ -353,6 +357,57 @@ describe("CLI local runtime adapter", () => {
|
|
|
353
357
|
expect(toolNamesFromRequest(requests[0])).toEqual(DEFAULT_LOCAL_CODING_TOOL_NAMES);
|
|
354
358
|
});
|
|
355
359
|
|
|
360
|
+
test("aborts stalled custom provider requests when timeoutMs is provided", async () => {
|
|
361
|
+
const store = new Map<string, any>([
|
|
362
|
+
["agent-user-1-mimo", {
|
|
363
|
+
dbKey: "agent-user-1-mimo",
|
|
364
|
+
id: "mimo",
|
|
365
|
+
prompt: "Use monthly mimo.",
|
|
366
|
+
model: "mimo-v2.5-pro",
|
|
367
|
+
provider: "custom",
|
|
368
|
+
apiSource: "custom",
|
|
369
|
+
customProviderUrl: "https://token-plan-cn.xiaomimimo.com/v1",
|
|
370
|
+
apiKey: "mimo-monthly-key",
|
|
371
|
+
}],
|
|
372
|
+
]);
|
|
373
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
374
|
+
env: {
|
|
375
|
+
NOLO_LOCAL_USER_ID: "user-1",
|
|
376
|
+
},
|
|
377
|
+
db: {
|
|
378
|
+
get: async (key) => {
|
|
379
|
+
if (!store.has(key)) throw new Error(`not found: ${key}`);
|
|
380
|
+
return store.get(key);
|
|
381
|
+
},
|
|
382
|
+
put: async (key, value) => {
|
|
383
|
+
store.set(key, value);
|
|
384
|
+
},
|
|
385
|
+
batch: async (ops) => {
|
|
386
|
+
for (const op of ops) {
|
|
387
|
+
if (op.type === "put") store.set(op.key, op.value);
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
iterator: () => (async function* () {})(),
|
|
391
|
+
},
|
|
392
|
+
fetchImpl: async (_url, init) => {
|
|
393
|
+
if (init?.signal?.aborted) {
|
|
394
|
+
throw init.signal.reason ?? new Error("aborted");
|
|
395
|
+
}
|
|
396
|
+
await new Promise((_, reject) => {
|
|
397
|
+
init?.signal?.addEventListener("abort", () => reject(init.signal?.reason ?? new Error("aborted")));
|
|
398
|
+
});
|
|
399
|
+
throw new Error("unreachable");
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
await expect(runLocalAgentTurn({
|
|
404
|
+
adapter,
|
|
405
|
+
agentRef: "mimo",
|
|
406
|
+
input: "hello",
|
|
407
|
+
timeoutMs: 10,
|
|
408
|
+
})).rejects.toThrow(/timed out|aborted/i);
|
|
409
|
+
});
|
|
410
|
+
|
|
356
411
|
test("uses loopback transport for localhost custom providers when the default fetch path cannot connect", async () => {
|
|
357
412
|
const store = new Map<string, any>([
|
|
358
413
|
["agent-user-1-localhost", {
|
|
@@ -1269,6 +1324,235 @@ describe("CLI local runtime adapter", () => {
|
|
|
1269
1324
|
expect(result.metadata).toMatchObject({ exitCode: 0 });
|
|
1270
1325
|
});
|
|
1271
1326
|
|
|
1327
|
+
test("accepts execShell aliases from the local model", async () => {
|
|
1328
|
+
const requests: Array<{ body: any }> = [];
|
|
1329
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
1330
|
+
env: {
|
|
1331
|
+
NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
|
|
1332
|
+
NOLO_LOCAL_SHELL_MODE: "worktree",
|
|
1333
|
+
},
|
|
1334
|
+
db: {
|
|
1335
|
+
get: async () => ({
|
|
1336
|
+
dbKey: "agent-local-shell-alias",
|
|
1337
|
+
prompt: "Use shell when needed.",
|
|
1338
|
+
model: "qwen-coder",
|
|
1339
|
+
toolNames: ["execShell"],
|
|
1340
|
+
}),
|
|
1341
|
+
put: async () => {},
|
|
1342
|
+
batch: async () => {},
|
|
1343
|
+
iterator: () => (async function* () {})(),
|
|
1344
|
+
},
|
|
1345
|
+
cwd: import.meta.dir,
|
|
1346
|
+
localToolExecutors: {
|
|
1347
|
+
execShell: async () => ({
|
|
1348
|
+
content: import.meta.dir,
|
|
1349
|
+
metadata: { exitCode: 0, timedOut: false },
|
|
1350
|
+
}),
|
|
1351
|
+
},
|
|
1352
|
+
fetchImpl: async (_url, init) => {
|
|
1353
|
+
requests.push({ body: JSON.parse(String(init?.body)) });
|
|
1354
|
+
if (requests.length === 1) {
|
|
1355
|
+
return Response.json({
|
|
1356
|
+
choices: [{
|
|
1357
|
+
message: {
|
|
1358
|
+
content: "",
|
|
1359
|
+
tool_calls: [{
|
|
1360
|
+
id: "call-1",
|
|
1361
|
+
type: "function",
|
|
1362
|
+
function: { name: "runCommand", arguments: "{\"cmd\":\"pwd\"}" },
|
|
1363
|
+
}],
|
|
1364
|
+
},
|
|
1365
|
+
}],
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
return Response.json({
|
|
1369
|
+
choices: [{ message: { content: "alias ok" } }],
|
|
1370
|
+
});
|
|
1371
|
+
},
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
const result = await runLocalAgentTurn({
|
|
1375
|
+
adapter,
|
|
1376
|
+
agentRef: "shell",
|
|
1377
|
+
input: "print cwd",
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
expect(result.content).toBe("alias ok");
|
|
1381
|
+
expect(requests[1]?.body.messages.at(-1)?.content).toContain(import.meta.dir);
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
test("blocks destructive local execShell calls unless the user explicitly asked to delete", async () => {
|
|
1385
|
+
let executorCalls = 0;
|
|
1386
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
1387
|
+
env: {
|
|
1388
|
+
NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
|
|
1389
|
+
NOLO_LOCAL_SHELL_MODE: "worktree",
|
|
1390
|
+
},
|
|
1391
|
+
db: {
|
|
1392
|
+
get: async () => ({
|
|
1393
|
+
dbKey: "agent-local-shell-guard",
|
|
1394
|
+
prompt: "Use shell when useful.",
|
|
1395
|
+
model: "qwen-coder",
|
|
1396
|
+
toolNames: ["execShell"],
|
|
1397
|
+
}),
|
|
1398
|
+
put: async () => {},
|
|
1399
|
+
batch: async () => {},
|
|
1400
|
+
iterator: () => (async function* () {})(),
|
|
1401
|
+
},
|
|
1402
|
+
cwd: import.meta.dir,
|
|
1403
|
+
localToolExecutors: {
|
|
1404
|
+
execShell: async () => {
|
|
1405
|
+
executorCalls += 1;
|
|
1406
|
+
return {
|
|
1407
|
+
content: "should not run",
|
|
1408
|
+
};
|
|
1409
|
+
},
|
|
1410
|
+
},
|
|
1411
|
+
fetchImpl: async (_url, init) => {
|
|
1412
|
+
const body = JSON.parse(String(init?.body));
|
|
1413
|
+
const lastToolMessage = [...body.messages]
|
|
1414
|
+
.reverse()
|
|
1415
|
+
.find((message: any) => message.role === "tool");
|
|
1416
|
+
if (lastToolMessage) {
|
|
1417
|
+
expect(String(lastToolMessage.content)).toContain(
|
|
1418
|
+
"destructive_action_requires_confirmation",
|
|
1419
|
+
);
|
|
1420
|
+
return Response.json({
|
|
1421
|
+
choices: [{ message: { content: "guard ok" } }],
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
return Response.json({
|
|
1425
|
+
choices: [{
|
|
1426
|
+
message: {
|
|
1427
|
+
content: "",
|
|
1428
|
+
tool_calls: [{
|
|
1429
|
+
id: "call-1",
|
|
1430
|
+
type: "function",
|
|
1431
|
+
function: {
|
|
1432
|
+
name: "execShell",
|
|
1433
|
+
arguments: "{\"cmd\":\"rm -rf ./tmp\"}",
|
|
1434
|
+
},
|
|
1435
|
+
}],
|
|
1436
|
+
},
|
|
1437
|
+
}],
|
|
1438
|
+
});
|
|
1439
|
+
},
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
const result = await runLocalAgentTurn({
|
|
1443
|
+
adapter,
|
|
1444
|
+
agentRef: "shell",
|
|
1445
|
+
input: "inspect cwd but don't delete files",
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
expect(result.content).toBe("guard ok");
|
|
1449
|
+
expect(executorCalls).toBe(0);
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
test("exposes runtime policy tools to the local model even when toolNames omits them", async () => {
|
|
1453
|
+
const requests: Array<{ body: any }> = [];
|
|
1454
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
1455
|
+
env: {
|
|
1456
|
+
NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
|
|
1457
|
+
NOLO_LOCAL_SHELL_MODE: "worktree",
|
|
1458
|
+
},
|
|
1459
|
+
db: {
|
|
1460
|
+
get: async () => ({
|
|
1461
|
+
dbKey: "agent-local-policy-shell",
|
|
1462
|
+
prompt: "Use shell when needed.",
|
|
1463
|
+
model: "qwen-coder",
|
|
1464
|
+
runtimeToolPolicy: {
|
|
1465
|
+
version: 1,
|
|
1466
|
+
runtimeTools: ["execShell"],
|
|
1467
|
+
},
|
|
1468
|
+
}),
|
|
1469
|
+
put: async () => {},
|
|
1470
|
+
batch: async () => {},
|
|
1471
|
+
iterator: () => (async function* () {})(),
|
|
1472
|
+
},
|
|
1473
|
+
cwd: import.meta.dir,
|
|
1474
|
+
localToolExecutors: {
|
|
1475
|
+
execShell: async () => ({
|
|
1476
|
+
content: import.meta.dir,
|
|
1477
|
+
metadata: { exitCode: 0, timedOut: false },
|
|
1478
|
+
}),
|
|
1479
|
+
},
|
|
1480
|
+
fetchImpl: async (_url, init) => {
|
|
1481
|
+
requests.push({ body: JSON.parse(String(init?.body)) });
|
|
1482
|
+
if (requests.length === 1) {
|
|
1483
|
+
return Response.json({
|
|
1484
|
+
choices: [{
|
|
1485
|
+
message: {
|
|
1486
|
+
content: "",
|
|
1487
|
+
tool_calls: [{
|
|
1488
|
+
id: "call-1",
|
|
1489
|
+
type: "function",
|
|
1490
|
+
function: { name: "execShell", arguments: "{\"cmd\":\"pwd\"}" },
|
|
1491
|
+
}],
|
|
1492
|
+
},
|
|
1493
|
+
}],
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
return Response.json({
|
|
1497
|
+
choices: [{ message: { content: "policy shell ok" } }],
|
|
1498
|
+
});
|
|
1499
|
+
},
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
const result = await runLocalAgentTurn({
|
|
1503
|
+
adapter,
|
|
1504
|
+
agentRef: "shell",
|
|
1505
|
+
input: "print cwd",
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
expect(result.content).toBe("policy shell ok");
|
|
1509
|
+
expect(toolNamesFromRequest(requests[0])).toContain("execShell");
|
|
1510
|
+
expect(requests[1]?.body.messages.at(-1)?.content).toContain(import.meta.dir);
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
test("treats runtime policy visual tools as declared local tools", async () => {
|
|
1514
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
1515
|
+
env: {
|
|
1516
|
+
NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
|
|
1517
|
+
},
|
|
1518
|
+
db: {
|
|
1519
|
+
get: async () => ({
|
|
1520
|
+
dbKey: "agent-local-visual",
|
|
1521
|
+
prompt: "Inspect the page.",
|
|
1522
|
+
model: "qwen-coder",
|
|
1523
|
+
runtimeToolPolicy: {
|
|
1524
|
+
version: 1,
|
|
1525
|
+
runtimeTools: ["captureVisualState"],
|
|
1526
|
+
},
|
|
1527
|
+
}),
|
|
1528
|
+
put: async () => {},
|
|
1529
|
+
batch: async () => {},
|
|
1530
|
+
iterator: () => (async function* () {})(),
|
|
1531
|
+
},
|
|
1532
|
+
cwd: import.meta.dir,
|
|
1533
|
+
localToolExecutors: {
|
|
1534
|
+
captureVisualState: async () => ({
|
|
1535
|
+
content: "{\"status\":\"ok\"}",
|
|
1536
|
+
}),
|
|
1537
|
+
},
|
|
1538
|
+
fetchImpl: async (_url, init) => {
|
|
1539
|
+
expect(toolNamesFromRequest({ body: JSON.parse(String(init?.body)) })).toContain("captureVisualState");
|
|
1540
|
+
return Response.json({
|
|
1541
|
+
choices: [{ message: { content: "visual ok" } }],
|
|
1542
|
+
});
|
|
1543
|
+
},
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
await adapter.loadAgentConfig("visual");
|
|
1547
|
+
await expect(adapter.executeTool({
|
|
1548
|
+
id: "call-visual",
|
|
1549
|
+
name: "captureVisualState",
|
|
1550
|
+
arguments: JSON.stringify({ waitSelector: "body" }),
|
|
1551
|
+
})).resolves.toMatchObject({
|
|
1552
|
+
content: "{\"status\":\"ok\"}",
|
|
1553
|
+
});
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1272
1556
|
test("runs allowed workspace file tools through default local executors", async () => {
|
|
1273
1557
|
const workspaceRoot = mkdtempSync(join(tmpdir(), "nolo-cli-runtime-"));
|
|
1274
1558
|
try {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
parsePlatformChatCompletionData,
|
|
17
17
|
parsePlatformChatCompletionResponse,
|
|
18
18
|
resolvePlatformChatProviderConfig,
|
|
19
|
+
resolveRequestedRuntimeToolNames,
|
|
19
20
|
shouldUsePlatformChatProvider,
|
|
20
21
|
} from "../agentRuntimeLocal";
|
|
21
22
|
import type { AgentRuntimeChatMessage } from "../agent-runtime";
|
|
@@ -169,6 +170,12 @@ async function defaultLoopbackRequest(input: FetchInput, init?: FetchInit) {
|
|
|
169
170
|
});
|
|
170
171
|
});
|
|
171
172
|
req.on("error", reject);
|
|
173
|
+
init?.signal?.addEventListener("abort", () => {
|
|
174
|
+
req.destroy(
|
|
175
|
+
init.signal?.reason instanceof Error ? init.signal.reason : new Error("request aborted")
|
|
176
|
+
);
|
|
177
|
+
reject(init.signal?.reason ?? new Error("request aborted"));
|
|
178
|
+
}, { once: true });
|
|
172
179
|
if (body) req.write(body);
|
|
173
180
|
req.end();
|
|
174
181
|
});
|
|
@@ -191,6 +198,7 @@ async function fetchWithTransientRetry(
|
|
|
191
198
|
}
|
|
192
199
|
return await fetchImpl(input, init);
|
|
193
200
|
} catch (error) {
|
|
201
|
+
if (init?.signal?.aborted) throw error;
|
|
194
202
|
if (!isTransientFetchError(error)) throw error;
|
|
195
203
|
lastError = error;
|
|
196
204
|
if (attempt < TRANSIENT_FETCH_MAX_ATTEMPTS) {
|
|
@@ -201,6 +209,18 @@ async function fetchWithTransientRetry(
|
|
|
201
209
|
throw lastError;
|
|
202
210
|
}
|
|
203
211
|
|
|
212
|
+
function buildRequestTimeoutSignal(timeoutMs?: number) {
|
|
213
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") {
|
|
217
|
+
return AbortSignal.timeout(timeoutMs);
|
|
218
|
+
}
|
|
219
|
+
const controller = new AbortController();
|
|
220
|
+
setTimeout(() => controller.abort(new Error(`request timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
221
|
+
return controller.signal;
|
|
222
|
+
}
|
|
223
|
+
|
|
204
224
|
function buildOpenAiTools(args: { toolNames?: string[]; env: EnvLike }) {
|
|
205
225
|
const toolset = buildLocalWorkspaceToolsetForEnv(args);
|
|
206
226
|
return [
|
|
@@ -428,8 +448,11 @@ export function createCliLocalRuntimeAdapter(
|
|
|
428
448
|
store: await resolveStore(deps),
|
|
429
449
|
userId,
|
|
430
450
|
});
|
|
451
|
+
const requestedToolNames = agentConfig
|
|
452
|
+
? resolveRequestedRuntimeToolNames({ agentConfig })
|
|
453
|
+
: [];
|
|
431
454
|
activeAgentToolNames = buildLocalPolicyToolNames({
|
|
432
|
-
toolNames:
|
|
455
|
+
toolNames: requestedToolNames,
|
|
433
456
|
env: deps.env,
|
|
434
457
|
});
|
|
435
458
|
await activateAgentWorkspaceSession({ agentRef, agentConfig });
|
|
@@ -455,9 +478,12 @@ export function createCliLocalRuntimeAdapter(
|
|
|
455
478
|
});
|
|
456
479
|
return {
|
|
457
480
|
model: providerConfig.model,
|
|
458
|
-
complete: async (messages) => {
|
|
481
|
+
complete: async (messages, options) => {
|
|
482
|
+
const requestedToolNames = resolveRequestedRuntimeToolNames({
|
|
483
|
+
agentConfig,
|
|
484
|
+
});
|
|
459
485
|
const tools = buildOpenAiTools({
|
|
460
|
-
toolNames:
|
|
486
|
+
toolNames: requestedToolNames,
|
|
461
487
|
env: deps.env,
|
|
462
488
|
});
|
|
463
489
|
const request = buildPlatformChatCompletionRequest({
|
|
@@ -465,7 +491,11 @@ export function createCliLocalRuntimeAdapter(
|
|
|
465
491
|
messages,
|
|
466
492
|
tools,
|
|
467
493
|
});
|
|
468
|
-
const
|
|
494
|
+
const signal = buildRequestTimeoutSignal(options?.timeoutMs);
|
|
495
|
+
const res = await fetchWithTransientRetry(fetchImpl, request.url, {
|
|
496
|
+
...request.init,
|
|
497
|
+
...(signal ? { signal } : {}),
|
|
498
|
+
}, {
|
|
469
499
|
sleep: deps.sleep,
|
|
470
500
|
loopbackRequest,
|
|
471
501
|
});
|
|
@@ -488,9 +518,12 @@ export function createCliLocalRuntimeAdapter(
|
|
|
488
518
|
});
|
|
489
519
|
return {
|
|
490
520
|
model: providerConfig.model,
|
|
491
|
-
complete: async (messages) => {
|
|
521
|
+
complete: async (messages, options) => {
|
|
522
|
+
const requestedToolNames = resolveRequestedRuntimeToolNames({
|
|
523
|
+
agentConfig,
|
|
524
|
+
});
|
|
492
525
|
const tools = buildOpenAiTools({
|
|
493
|
-
toolNames:
|
|
526
|
+
toolNames: requestedToolNames,
|
|
494
527
|
env: deps.env,
|
|
495
528
|
});
|
|
496
529
|
const request = buildOpenAiCompatibleChatCompletionRequest({
|
|
@@ -498,7 +531,11 @@ export function createCliLocalRuntimeAdapter(
|
|
|
498
531
|
messages,
|
|
499
532
|
tools,
|
|
500
533
|
});
|
|
501
|
-
const
|
|
534
|
+
const signal = buildRequestTimeoutSignal(options?.timeoutMs);
|
|
535
|
+
const res = await fetchWithTransientRetry(fetchImpl, request.url, {
|
|
536
|
+
...request.init,
|
|
537
|
+
...(signal ? { signal } : {}),
|
|
538
|
+
}, {
|
|
502
539
|
sleep: deps.sleep,
|
|
503
540
|
loopbackRequest,
|
|
504
541
|
});
|
package/commandRegistry.ts
CHANGED
|
@@ -49,8 +49,8 @@ export function renderHelpText() {
|
|
|
49
49
|
" nolo connect",
|
|
50
50
|
" nolo connect --watch",
|
|
51
51
|
" nolo connect --ws",
|
|
52
|
-
" nolo connect --ws --server-url https://api.nolo.chat --
|
|
53
|
-
" nolo connect --daemon --server-url https://api.nolo.chat --
|
|
52
|
+
" nolo connect --ws --server-url https://api.nolo.chat --machine-key sk_machine_xxx",
|
|
53
|
+
" nolo connect --daemon --server-url https://api.nolo.chat --machine-key sk_machine_xxx",
|
|
54
54
|
" nolo machine status",
|
|
55
55
|
" nolo doctor",
|
|
56
56
|
" nolo doctor runtime",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type AuthorityBatchOperation =
|
|
2
|
+
| { type: "put"; key: string; value: unknown }
|
|
3
|
+
| { type: "del"; key: string };
|
|
4
|
+
|
|
5
|
+
export type AuthorityIteratorOptions = {
|
|
6
|
+
gte?: string;
|
|
7
|
+
lte?: string;
|
|
8
|
+
lt?: string;
|
|
9
|
+
reverse?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export interface AuthorityBatchWriter {
|
|
13
|
+
put(key: string, value: unknown): void;
|
|
14
|
+
del(key: string): void;
|
|
15
|
+
write(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AuthorityStore {
|
|
19
|
+
readonly location?: string;
|
|
20
|
+
readonly status?: string;
|
|
21
|
+
open(): Promise<void>;
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
get<T = any>(key: string): Promise<T>;
|
|
24
|
+
put(key: string, value: unknown): Promise<void>;
|
|
25
|
+
del(key: string): Promise<void>;
|
|
26
|
+
batchWrite(ops: AuthorityBatchOperation[]): Promise<void>;
|
|
27
|
+
createBatch(): AuthorityBatchWriter;
|
|
28
|
+
iterator(
|
|
29
|
+
options?: AuthorityIteratorOptions
|
|
30
|
+
): AsyncIterableIterator<[string, unknown]>;
|
|
31
|
+
}
|
package/database/server/db.ts
CHANGED
|
@@ -1,37 +1,17 @@
|
|
|
1
1
|
// 文件路径: packages/database/server/db.ts
|
|
2
2
|
|
|
3
|
-
import { Level } from "level";
|
|
4
|
-
import path from "path";
|
|
5
|
-
import fs from "fs";
|
|
6
3
|
import { ensureDbOpen } from "./ensureDbOpen";
|
|
7
4
|
import { resolveServerDbPath } from "./dbPath";
|
|
5
|
+
import type { LegacyServerDb } from "./legacyServerDb";
|
|
6
|
+
import type { AuthorityStore } from "./authorityStoreTypes";
|
|
7
|
+
import { getOrCreateServerStoreRuntime } from "./serverStoreFactory";
|
|
8
8
|
|
|
9
9
|
const DB_PATH = resolveServerDbPath();
|
|
10
10
|
|
|
11
|
-
// 确保目录存在
|
|
12
|
-
if (!fs.existsSync(path.dirname(DB_PATH))) {
|
|
13
|
-
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
|
|
14
|
-
}
|
|
15
|
-
|
|
16
11
|
console.log("数据库配置:");
|
|
17
12
|
console.log("- 当前工作目录:", process.cwd());
|
|
18
13
|
console.log("- 数据库路径:", DB_PATH);
|
|
19
14
|
|
|
20
|
-
// 安全 JSON 编码:解码失败时返回 null 而非抛异常,防止损坏记录导致进程崩溃
|
|
21
|
-
const safeJsonEncoding = {
|
|
22
|
-
name: "safe-json",
|
|
23
|
-
format: "utf8" as const,
|
|
24
|
-
encode: (data: any) => JSON.stringify(data),
|
|
25
|
-
decode: (data: string) => {
|
|
26
|
-
try {
|
|
27
|
-
return JSON.parse(data);
|
|
28
|
-
} catch {
|
|
29
|
-
console.warn("[LevelDB] Corrupted value:", data?.substring?.(0, 60));
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
|
|
35
15
|
export function isServerDbLockError(error: unknown): boolean {
|
|
36
16
|
const queue: unknown[] = [error];
|
|
37
17
|
const seen = new Set<unknown>();
|
|
@@ -55,13 +35,11 @@ export function isServerDbLockError(error: unknown): boolean {
|
|
|
55
35
|
return false;
|
|
56
36
|
}
|
|
57
37
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
const serverDb: Level<string, any> = (globalThis as any).__serverDb;
|
|
38
|
+
const {
|
|
39
|
+
authorityStore,
|
|
40
|
+
serverDb,
|
|
41
|
+
}: { authorityStore: AuthorityStore; serverDb: LegacyServerDb } =
|
|
42
|
+
getOrCreateServerStoreRuntime(DB_PATH);
|
|
65
43
|
|
|
66
44
|
console.log("- LevelDB实际路径:", serverDb.location);
|
|
67
45
|
|
|
@@ -70,6 +48,9 @@ export default serverDb;
|
|
|
70
48
|
|
|
71
49
|
// 具名导出:需要的时候可以引用
|
|
72
50
|
export { serverDb };
|
|
51
|
+
export function getServerAuthorityStore() {
|
|
52
|
+
return authorityStore;
|
|
53
|
+
}
|
|
73
54
|
|
|
74
55
|
/**
|
|
75
56
|
* 确保数据库处于 open 状态。
|
|
@@ -77,11 +58,11 @@ export { serverDb };
|
|
|
77
58
|
* - 其他状态:尝试 open,一旦失败抛错
|
|
78
59
|
*/
|
|
79
60
|
export async function ensureServerDbOpen() {
|
|
80
|
-
const status =
|
|
61
|
+
const status = authorityStore.status;
|
|
81
62
|
if (status === "open") return;
|
|
82
63
|
|
|
83
64
|
try {
|
|
84
|
-
await ensureDbOpen(
|
|
65
|
+
await ensureDbOpen(authorityStore);
|
|
85
66
|
if (status !== "opening") {
|
|
86
67
|
console.log("✅ LevelDB 已打开");
|
|
87
68
|
}
|
|
@@ -106,11 +87,11 @@ export async function openServerDb() {
|
|
|
106
87
|
* 优雅关机时关闭 DB
|
|
107
88
|
*/
|
|
108
89
|
export async function closeServerDb() {
|
|
109
|
-
const status =
|
|
90
|
+
const status = authorityStore.status;
|
|
110
91
|
if (status !== "open") return;
|
|
111
92
|
|
|
112
93
|
try {
|
|
113
|
-
await
|
|
94
|
+
await authorityStore.close();
|
|
114
95
|
console.log("✅ LevelDB 已关闭");
|
|
115
96
|
} catch (err) {
|
|
116
97
|
console.error("❌ 关闭 LevelDB 失败:", err);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthorityBatchOperation,
|
|
3
|
+
AuthorityBatchWriter,
|
|
4
|
+
AuthorityIteratorOptions,
|
|
5
|
+
AuthorityStore,
|
|
6
|
+
} from "./authorityStoreTypes";
|
|
7
|
+
|
|
8
|
+
export type LegacyServerDb = {
|
|
9
|
+
readonly location?: string;
|
|
10
|
+
readonly status?: string;
|
|
11
|
+
open(): Promise<void>;
|
|
12
|
+
close(): Promise<void>;
|
|
13
|
+
get<T = any>(key: string): Promise<T>;
|
|
14
|
+
put(key: string, value: unknown): Promise<void>;
|
|
15
|
+
del(key: string): Promise<void>;
|
|
16
|
+
batch(ops: AuthorityBatchOperation[]): Promise<void>;
|
|
17
|
+
batch(): AuthorityBatchWriter;
|
|
18
|
+
iterator(
|
|
19
|
+
options?: AuthorityIteratorOptions
|
|
20
|
+
): AsyncIterableIterator<[string, unknown]>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function createLegacyServerDb(store: AuthorityStore): LegacyServerDb {
|
|
24
|
+
const batch = (ops?: AuthorityBatchOperation[]) => {
|
|
25
|
+
if (Array.isArray(ops)) {
|
|
26
|
+
return store.batchWrite(ops);
|
|
27
|
+
}
|
|
28
|
+
return store.createBatch();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
get location() {
|
|
33
|
+
return store.location;
|
|
34
|
+
},
|
|
35
|
+
get status() {
|
|
36
|
+
return store.status;
|
|
37
|
+
},
|
|
38
|
+
async open() {
|
|
39
|
+
await store.open();
|
|
40
|
+
},
|
|
41
|
+
async close() {
|
|
42
|
+
await store.close();
|
|
43
|
+
},
|
|
44
|
+
async get<T = any>(key: string) {
|
|
45
|
+
return store.get<T>(key);
|
|
46
|
+
},
|
|
47
|
+
async put(key: string, value: unknown) {
|
|
48
|
+
await store.put(key, value);
|
|
49
|
+
},
|
|
50
|
+
async del(key: string) {
|
|
51
|
+
await store.del(key);
|
|
52
|
+
},
|
|
53
|
+
batch: batch as LegacyServerDb["batch"],
|
|
54
|
+
iterator(options: AuthorityIteratorOptions = {}) {
|
|
55
|
+
return store.iterator(options);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|