nolo-cli 0.1.22 → 0.1.26
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 +25 -3
- package/agent-runtime/agentRecordConfig.ts +4 -0
- package/agent-runtime/hostAdapter.ts +2 -0
- package/agent-runtime/hybridRecordStore.ts +30 -9
- package/agent-runtime/index.ts +7 -0
- package/agent-runtime/localLoop.ts +14 -4
- package/agent-runtime/localToolPolicy.ts +22 -10
- package/agent-runtime/localWorkspaceTools.ts +140 -374
- package/agent-runtime/platformChatProvider.ts +3 -0
- package/agent-runtime/runtimeToolPolicy.ts +133 -0
- package/agent-runtime/types.ts +47 -0
- package/agentAliases.ts +1 -1
- package/agentCommandSupport.ts +77 -0
- package/agentInternalCommandEntries.ts +34 -0
- package/agentListCommands.ts +123 -0
- package/agentListHelpers.ts +187 -0
- package/agentMachineCommands.ts +402 -0
- package/agentRecordCommands.ts +141 -0
- package/agentRecordHelpers.ts +395 -0
- package/agentRunCommand.ts +75 -3
- package/agentRuntimeCommands.ts +7 -773
- package/ai/agent/_executeModel.ts +29 -3
- package/ai/agent/buildSystemPrompt.ts +17 -0
- package/ai/agent/cliChatClient.ts +38 -4
- package/ai/agent/cliExecutor.ts +304 -25
- package/ai/agent/createAgentSchema.ts +2 -2
- package/ai/agent/guidedCreation/assistant.ts +82 -0
- package/ai/agent/guidedCreation/capabilities.ts +84 -0
- package/ai/agent/guidedCreation/draft.ts +62 -0
- package/ai/agent/guidedCreation/types.ts +38 -0
- package/ai/agent/machineRunPermissions.ts +21 -3
- package/ai/agent/runtimeGuidance.ts +1 -1
- package/ai/agent/server/fetchPublicAgents.ts +6 -1
- package/ai/agent/streamAgentChatTurn.ts +593 -36
- package/ai/agent/web/useGuidedAgentCreation.ts +217 -0
- package/ai/ai.locale.ts +14 -2
- package/ai/chat/fetchUtils.native.ts +2 -0
- package/ai/chat/fetchUtils.ts +2 -0
- package/ai/chat/sendOpenAICompletionsRequest.ts +82 -1
- package/ai/chat/sendOpenAIResponseRequest.ts +65 -0
- package/ai/llm/getPricing.ts +14 -0
- package/ai/llm/kimi.ts +4 -1
- package/ai/llm/modelAvatar.ts +1 -0
- package/ai/llm/providers.ts +4 -1
- package/ai/llm/reasoningModels.ts +1 -0
- package/ai/taskRun/taskRunProtocol.ts +25 -10
- package/ai/token/modelUsageQuery.ts +17 -0
- package/ai/tools/agent/agentTools.ts +38 -0
- package/ai/tools/agent/prepareAgentDraftTool.ts +159 -0
- package/ai/tools/agent/startAgentDialogTool.ts +53 -0
- package/ai/tools/checkEnvTool.ts +52 -20
- package/ai/tools/convertMarxistsBookTool.ts +103 -0
- package/ai/tools/{execBashTool.ts → execShellTool.ts} +44 -161
- package/ai/tools/index.ts +24 -14
- package/ai/tools/marxistsOfflineBook.ts +253 -0
- package/ai/tools/modelUsageTools.ts +9 -2
- package/ai/tools/readTool.ts +31 -0
- package/ai/tools/toolApiClient.ts +35 -1
- package/ai/tools/toolNameAliases.ts +13 -0
- package/cliCommandAdapters.ts +28 -0
- package/cliCommandDispatch.ts +66 -0
- package/cliCommandFactories.ts +34 -0
- package/cliCommandTypes.ts +49 -0
- package/cliEnvHelpers.ts +44 -0
- package/client/agentRun.test.ts +330 -24
- package/client/agentRun.ts +117 -35
- package/client/hybridRecordStore.test.ts +42 -0
- package/client/hybridRecordStore.ts +11 -0
- package/client/localRuntimeAdapter.test.ts +191 -21
- package/client/localRuntimeAdapter.ts +99 -10
- package/client/localRuntimeDryRun.test.ts +5 -2
- package/client/taskRunPrompt.ts +1 -0
- package/commandRegistry.ts +11 -61
- package/connector-experimental/capabilities.ts +1 -0
- package/database/actions/common.ts +4 -3
- package/database/config.ts +19 -0
- package/database/server/coreDataProxy.ts +43 -26
- package/index.ts +54 -191
- package/internalCommandEntries.ts +12 -0
- package/machineCommands.ts +688 -64
- package/offlineMarxistsAgentCommand.ts +314 -0
- package/package.json +22 -2
- package/render/canvas/canvasEditContext.ts +127 -0
- package/render/canvas/canvasRuntime.ts +57 -0
- package/render/canvas/canvasSnapshotParser.ts +76 -0
- package/render/canvas/canvasTree.ts +308 -0
- package/render/canvas/types.ts +46 -0
- package/render/layout/deleteBehavior.ts +52 -0
- package/render/layout/mainLayoutSidebar.ts +17 -0
- package/render/layout/mainLayoutViewMode.ts +56 -0
- package/render/layout/topbarUtils.ts +87 -0
- package/render/layout/useDevReloadPending.ts +30 -0
- package/render/page/createPageAction.ts +183 -0
- package/render/page/docSlice.ts +468 -0
- package/render/page/server/createPage.ts +174 -0
- package/render/page/server/handleCreatePage.ts +91 -0
- package/render/page/server/index.ts +4 -0
- package/render/page/types.ts +17 -0
- package/render/page/useKeyboardSave.ts +48 -0
- package/render/styles/zIndex.ts +12 -0
- package/render/surf/WeatherIconStyles.ts +17 -0
- package/render/surf/color.ts +9 -0
- package/render/surf/config.ts +46 -0
- package/render/surf/screens/style.ts +1 -0
- package/render/surf/styles/ToggleButtonStyles.ts +8 -0
- package/render/surf/utils/groupedWeatherData.ts +32 -0
- package/render/surf/weatherUtils.ts +50 -0
- package/render/table/activityColumns.ts +6 -0
- package/render/table/createTableAction.ts +270 -0
- package/render/table/deleteTableAction.ts +129 -0
- package/render/table/fetchAndCacheTableRows.ts +174 -0
- package/render/table/tableSlice.ts +1106 -0
- package/render/table/tableView.ts +289 -0
- package/render/table/toolValueUtils.ts +363 -0
- package/render/table/types.ts +252 -0
- package/render/table/useCreateTable.ts +72 -0
- package/render/table/useTable.ts +61 -0
- package/render/table/utils/tableSerialization.ts +50 -0
- package/render/web/elements/artifactPreviewCode.ts +43 -0
- package/render/web/elements/artifactRuntimePreload.ts +52 -0
- package/render/web/elements/codeBlockAutoPreview.ts +10 -0
- package/render/web/elements/mermaidPreview.ts +21 -0
- package/render/web/ui/useInlineEdit.ts +135 -0
- package/runtimeDoctorCommands.ts +118 -0
- package/scriptCommandEntries.ts +71 -0
- package/systemInternalCommandEntries.ts +71 -0
- package/tableCommands.ts +42 -5
- package/workflowInternalCommandEntries.ts +11 -0
package/client/agentRun.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { runLocalAgentTurn } from "../agentRuntimeLocal";
|
|
4
|
+
import { LOCAL_AGENT_CONFIG_MISSING_CODE } from "../agent-runtime/localLoop";
|
|
4
5
|
import {
|
|
5
6
|
FRONTEND_IMPLEMENTER_AGENT_KEY,
|
|
6
7
|
NOLO_FULLSTACK_AGENT_KEY,
|
|
@@ -59,7 +60,7 @@ type RunAgentTurnOptions = {
|
|
|
59
60
|
taskRunContext?: TaskRunPromptContext;
|
|
60
61
|
taskRunRecorder?: (args: {
|
|
61
62
|
options: RunAgentTurnOptions;
|
|
62
|
-
status: "completed" | "failed";
|
|
63
|
+
status: "running" | "completed" | "failed";
|
|
63
64
|
dialogId?: string;
|
|
64
65
|
resultSummary?: string;
|
|
65
66
|
errorCode?: string;
|
|
@@ -69,10 +70,11 @@ type RunAgentTurnOptions = {
|
|
|
69
70
|
fetchImpl?: typeof fetch;
|
|
70
71
|
};
|
|
71
72
|
|
|
72
|
-
export type RunAgentTurnResult = {
|
|
73
|
-
exitCode: number;
|
|
74
|
-
dialogId?: string;
|
|
75
|
-
|
|
73
|
+
export type RunAgentTurnResult = {
|
|
74
|
+
exitCode: number;
|
|
75
|
+
dialogId?: string;
|
|
76
|
+
streamInterrupted?: boolean;
|
|
77
|
+
};
|
|
76
78
|
|
|
77
79
|
type ScriptBridgeDecision = {
|
|
78
80
|
hasAuthToken: boolean;
|
|
@@ -132,6 +134,15 @@ export function findServerPlatformTools(toolNames?: string[]) {
|
|
|
132
134
|
return toolNames.filter((toolName) => SERVER_PLATFORM_TOOL_NAMES.has(toolName));
|
|
133
135
|
}
|
|
134
136
|
|
|
137
|
+
function resolveServerPlatformToolNames(agentConfig: any) {
|
|
138
|
+
return findServerPlatformTools([
|
|
139
|
+
...(Array.isArray(agentConfig?.toolNames) ? agentConfig.toolNames : []),
|
|
140
|
+
...(Array.isArray(agentConfig?.runtimeToolPolicy?.agentTools)
|
|
141
|
+
? agentConfig.runtimeToolPolicy.agentTools
|
|
142
|
+
: []),
|
|
143
|
+
]);
|
|
144
|
+
}
|
|
145
|
+
|
|
135
146
|
function normalizeAgentRef(ref?: string) {
|
|
136
147
|
return ref?.trim().toLowerCase().replace(/\s+/g, " ");
|
|
137
148
|
}
|
|
@@ -190,7 +201,7 @@ async function shouldSkipAutoLocalForServerPlatformTools(options: RunAgentTurnOp
|
|
|
190
201
|
} catch {
|
|
191
202
|
return false;
|
|
192
203
|
}
|
|
193
|
-
const serverTools =
|
|
204
|
+
const serverTools = resolveServerPlatformToolNames(agentConfig);
|
|
194
205
|
if (serverTools.length === 0) return false;
|
|
195
206
|
options.output.write(
|
|
196
207
|
`[nolo] auto runtime: skipping local runtime because ${options.agentKey} declares server platform tools ` +
|
|
@@ -240,7 +251,7 @@ function summarizeLocalToolEvidence(events: LocalAgentToolEvent[]): TaskRunToolE
|
|
|
240
251
|
};
|
|
241
252
|
}
|
|
242
253
|
|
|
243
|
-
const DEFAULT_LOCAL_AGENT_TOOL_ROUND_RETRY_LIMIT =
|
|
254
|
+
const DEFAULT_LOCAL_AGENT_TOOL_ROUND_RETRY_LIMIT = 512;
|
|
244
255
|
|
|
245
256
|
function parseLocalToolRoundLimitError(error: unknown) {
|
|
246
257
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -249,6 +260,16 @@ function parseLocalToolRoundLimitError(error: unknown) {
|
|
|
249
260
|
return Number(match[1]);
|
|
250
261
|
}
|
|
251
262
|
|
|
263
|
+
function isMissingLocalAgentConfigError(error: unknown, agentRef: string) {
|
|
264
|
+
return Boolean(
|
|
265
|
+
error &&
|
|
266
|
+
typeof error === "object" &&
|
|
267
|
+
"code" in error &&
|
|
268
|
+
(error as { code?: string; agentRef?: string }).code === LOCAL_AGENT_CONFIG_MISSING_CODE &&
|
|
269
|
+
(error as { code?: string; agentRef?: string }).agentRef === agentRef
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
252
273
|
function parsePositiveInteger(value: string | undefined) {
|
|
253
274
|
if (!value) return null;
|
|
254
275
|
const parsed = Number(value);
|
|
@@ -348,7 +369,7 @@ async function runTaskRunCli(args: {
|
|
|
348
369
|
|
|
349
370
|
async function defaultTaskRunRecorder(args: {
|
|
350
371
|
options: RunAgentTurnOptions;
|
|
351
|
-
status: "completed" | "failed";
|
|
372
|
+
status: "running" | "completed" | "failed";
|
|
352
373
|
dialogId?: string;
|
|
353
374
|
resultSummary?: string;
|
|
354
375
|
errorCode?: string;
|
|
@@ -357,7 +378,7 @@ async function defaultTaskRunRecorder(args: {
|
|
|
357
378
|
const context = args.options.taskRunContext;
|
|
358
379
|
if (!context?.rowDbKey) return;
|
|
359
380
|
const role = inferTaskRunAgentRole(args.options.agentName || args.options.agentKey);
|
|
360
|
-
if (role === "reviewer") {
|
|
381
|
+
if (role === "reviewer" && args.status !== "running") {
|
|
361
382
|
await runTaskRunCli({
|
|
362
383
|
options: args.options,
|
|
363
384
|
cliArgs: [
|
|
@@ -415,7 +436,7 @@ async function defaultTaskRunRecorder(args: {
|
|
|
415
436
|
],
|
|
416
437
|
});
|
|
417
438
|
}
|
|
418
|
-
if (role === "reviewer") {
|
|
439
|
+
if (role === "reviewer" && args.status !== "running") {
|
|
419
440
|
const reviewStatus =
|
|
420
441
|
args.status === "failed" ? "blocked" : classifyTaskRunReviewStatus(args.resultSummary);
|
|
421
442
|
if (reviewStatus) {
|
|
@@ -441,7 +462,7 @@ async function defaultTaskRunRecorder(args: {
|
|
|
441
462
|
|
|
442
463
|
async function recordTaskRunOutcome(args: {
|
|
443
464
|
options: RunAgentTurnOptions;
|
|
444
|
-
status: "completed" | "failed";
|
|
465
|
+
status: "running" | "completed" | "failed";
|
|
445
466
|
dialogId?: string;
|
|
446
467
|
resultSummary?: string;
|
|
447
468
|
errorCode?: string;
|
|
@@ -566,12 +587,28 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
|
|
|
566
587
|
options.imageUrls
|
|
567
588
|
),
|
|
568
589
|
runtimeContext: {
|
|
569
|
-
surface: "cli",
|
|
570
|
-
host: "terminal",
|
|
571
|
-
runtime: "bun",
|
|
572
|
-
entrypoint: "nolo-cli",
|
|
573
|
-
capabilities: ["text-io", "streaming", "slash-commands"],
|
|
574
|
-
|
|
590
|
+
surface: "cli",
|
|
591
|
+
host: "terminal",
|
|
592
|
+
runtime: "bun",
|
|
593
|
+
entrypoint: "nolo-cli",
|
|
594
|
+
capabilities: ["text-io", "streaming", "slash-commands"],
|
|
595
|
+
...(options.taskRunContext
|
|
596
|
+
? {
|
|
597
|
+
taskRun: {
|
|
598
|
+
rowDbKey: options.taskRunContext.rowDbKey,
|
|
599
|
+
...(options.taskRunContext.taskRunId
|
|
600
|
+
? { taskRunId: options.taskRunContext.taskRunId }
|
|
601
|
+
: {}),
|
|
602
|
+
...(options.taskRunContext.workItemId
|
|
603
|
+
? { workItemId: options.taskRunContext.workItemId }
|
|
604
|
+
: {}),
|
|
605
|
+
...(options.taskRunContext.artifactIds?.length
|
|
606
|
+
? { artifactIds: options.taskRunContext.artifactIds }
|
|
607
|
+
: {}),
|
|
608
|
+
},
|
|
609
|
+
}
|
|
610
|
+
: {}),
|
|
611
|
+
},
|
|
575
612
|
...(options.continueDialogId
|
|
576
613
|
? { continueDialogId: options.continueDialogId }
|
|
577
614
|
: {}),
|
|
@@ -597,7 +634,11 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
|
|
|
597
634
|
const result = await readStreamingAgentRun(options, res);
|
|
598
635
|
await recordTaskRunOutcome({
|
|
599
636
|
options,
|
|
600
|
-
status: result.
|
|
637
|
+
status: result.streamInterrupted
|
|
638
|
+
? "running"
|
|
639
|
+
: result.exitCode === 0
|
|
640
|
+
? "completed"
|
|
641
|
+
: "failed",
|
|
601
642
|
dialogId: result.dialogId,
|
|
602
643
|
...(result.exitCode !== 0 ? { errorCode: "STREAM_FAILED" } : {}),
|
|
603
644
|
});
|
|
@@ -642,9 +683,9 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
|
|
|
642
683
|
};
|
|
643
684
|
await recordTaskRunOutcome({
|
|
644
685
|
options,
|
|
645
|
-
status: "completed",
|
|
686
|
+
status: options.background ? "running" : "completed",
|
|
646
687
|
dialogId: result.dialogId,
|
|
647
|
-
resultSummary: content
|
|
688
|
+
...(!options.background && content ? { resultSummary: content } : {}),
|
|
648
689
|
});
|
|
649
690
|
return result;
|
|
650
691
|
}
|
|
@@ -653,6 +694,13 @@ async function runInjectedLocalAgentTurn(options: RunAgentTurnOptions) {
|
|
|
653
694
|
return runLocalAgentTurnWithAutoRoundExpansion(options, { reportFailure: true });
|
|
654
695
|
}
|
|
655
696
|
|
|
697
|
+
async function refreshMissingLocalAgentConfig(options: RunAgentTurnOptions) {
|
|
698
|
+
const adapter = resolveLocalRuntimeAdapter(options);
|
|
699
|
+
if (!adapter) return false;
|
|
700
|
+
const agentConfig = await adapter.loadAgentConfig(options.agentKey);
|
|
701
|
+
return Boolean(agentConfig);
|
|
702
|
+
}
|
|
703
|
+
|
|
656
704
|
async function runLocalAgentTurnWithAutoRoundExpansion(
|
|
657
705
|
options: RunAgentTurnOptions,
|
|
658
706
|
settings: { reportFailure: boolean }
|
|
@@ -747,9 +795,9 @@ async function runLocalAgentTurnForCli(
|
|
|
747
795
|
}
|
|
748
796
|
await recordTaskRunOutcome({
|
|
749
797
|
options,
|
|
750
|
-
status: "completed",
|
|
798
|
+
status: options.background ? "running" : "completed",
|
|
751
799
|
dialogId: result.dialogId,
|
|
752
|
-
resultSummary: content
|
|
800
|
+
...(!options.background && content ? { resultSummary: content } : {}),
|
|
753
801
|
toolEvidence: summarizeLocalToolEvidence(toolEvents),
|
|
754
802
|
});
|
|
755
803
|
return { exitCode: 0, dialogId: result.dialogId };
|
|
@@ -791,15 +839,20 @@ async function readStreamingAgentRun(
|
|
|
791
839
|
hasPrintedLabel = true;
|
|
792
840
|
};
|
|
793
841
|
|
|
794
|
-
const handlePayload = (payload: any) => {
|
|
795
|
-
if (
|
|
796
|
-
|
|
797
|
-
}
|
|
798
|
-
if (payload?.type === "
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
842
|
+
const handlePayload = (payload: any) => {
|
|
843
|
+
if (typeof payload?.dialogId === "string" && payload.dialogId.trim()) {
|
|
844
|
+
dialogId = payload.dialogId;
|
|
845
|
+
}
|
|
846
|
+
if (payload?.error || payload?.type === "error") {
|
|
847
|
+
throw new Error(String(payload.error || payload.message || "Agent stream failed"));
|
|
848
|
+
}
|
|
849
|
+
if (payload?.type === "done") {
|
|
850
|
+
usage = payload.usage;
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (payload?.type === "dialog" || payload?.type === "status") {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
803
856
|
|
|
804
857
|
const chunk =
|
|
805
858
|
payload?.type === "text"
|
|
@@ -842,10 +895,18 @@ async function readStreamingAgentRun(
|
|
|
842
895
|
.trim();
|
|
843
896
|
if (raw) handlePayload(JSON.parse(raw));
|
|
844
897
|
}
|
|
845
|
-
} catch (error) {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
898
|
+
} catch (error) {
|
|
899
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
900
|
+
if (dialogId) {
|
|
901
|
+
options.output.write(
|
|
902
|
+
`\n[nolo] Agent stream transport interrupted after dialog ${dialogId} was created: ${message}\n`
|
|
903
|
+
);
|
|
904
|
+
options.output.write("[nolo] The agent run may still finish on the server; read the dialog before retrying.\n");
|
|
905
|
+
return { exitCode: 0, dialogId, streamInterrupted: true };
|
|
906
|
+
}
|
|
907
|
+
options.output.write(`\n[nolo] Agent stream failed: ${message}\n`);
|
|
908
|
+
return { exitCode: 1 };
|
|
909
|
+
} finally {
|
|
849
910
|
writer.flushAll();
|
|
850
911
|
}
|
|
851
912
|
|
|
@@ -883,6 +944,27 @@ export async function runAgentTurn(options: RunAgentTurnOptions) {
|
|
|
883
944
|
...(localResult.dialogId ? { dialogId: localResult.dialogId } : {}),
|
|
884
945
|
};
|
|
885
946
|
}
|
|
947
|
+
if (isMissingLocalAgentConfigError(localResult.localError, options.agentKey)) {
|
|
948
|
+
options.output.write(
|
|
949
|
+
`[nolo] Local agent config was missing; refreshing from the configured server and retrying local once.\n`
|
|
950
|
+
);
|
|
951
|
+
try {
|
|
952
|
+
const refreshed = await refreshMissingLocalAgentConfig(options);
|
|
953
|
+
if (refreshed) {
|
|
954
|
+
const retriedLocalResult = await runLocalAgentTurnWithAutoRoundExpansion(options, {
|
|
955
|
+
reportFailure: false,
|
|
956
|
+
});
|
|
957
|
+
if (retriedLocalResult.exitCode === 0) {
|
|
958
|
+
return {
|
|
959
|
+
exitCode: retriedLocalResult.exitCode,
|
|
960
|
+
...(retriedLocalResult.dialogId ? { dialogId: retriedLocalResult.dialogId } : {}),
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
} catch {
|
|
965
|
+
// Fall through to the existing server runtime fallback below.
|
|
966
|
+
}
|
|
967
|
+
}
|
|
886
968
|
}
|
|
887
969
|
}
|
|
888
970
|
|
|
@@ -87,6 +87,48 @@ describe("CLI hybrid record store", () => {
|
|
|
87
87
|
});
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
test("falls back across known cluster servers on local miss and caches the first remote hit", async () => {
|
|
91
|
+
const { db, store: memory } = createMemoryDb();
|
|
92
|
+
const requests: string[] = [];
|
|
93
|
+
const hybrid = createCliHybridRecordStore({
|
|
94
|
+
db,
|
|
95
|
+
env: {
|
|
96
|
+
NOLO_SERVER: "http://127.0.0.1:38123",
|
|
97
|
+
AUTH_TOKEN: "token-1",
|
|
98
|
+
},
|
|
99
|
+
fetchImpl: async (url) => {
|
|
100
|
+
const target = String(url);
|
|
101
|
+
requests.push(target);
|
|
102
|
+
if (target.startsWith("http://127.0.0.1:38123/")) {
|
|
103
|
+
throw new Error("ConnectionRefused");
|
|
104
|
+
}
|
|
105
|
+
if (target.startsWith("https://nolo.chat/")) {
|
|
106
|
+
return Response.json({
|
|
107
|
+
data: {
|
|
108
|
+
dbKey: "agent-user-1-cluster",
|
|
109
|
+
name: "cluster remote",
|
|
110
|
+
updatedAt: "2026-05-24T00:00:00.000Z",
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return new Response(null, { status: 404 });
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await expect(hybrid.read("agent-user-1-cluster")).resolves.toMatchObject({
|
|
119
|
+
name: "cluster remote",
|
|
120
|
+
serverOrigin: "https://nolo.chat",
|
|
121
|
+
});
|
|
122
|
+
expect(requests).toEqual([
|
|
123
|
+
"http://127.0.0.1:38123/api/v1/db/read/agent-user-1-cluster",
|
|
124
|
+
"https://nolo.chat/api/v1/db/read/agent-user-1-cluster",
|
|
125
|
+
]);
|
|
126
|
+
expect(memory.get("agent-user-1-cluster")).toMatchObject({
|
|
127
|
+
name: "cluster remote",
|
|
128
|
+
serverOrigin: "https://nolo.chat",
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
90
132
|
test("keeps newer local records over stale remote records", () => {
|
|
91
133
|
expect(shouldCacheRemoteRecord(
|
|
92
134
|
{ updatedAt: "2026-05-12T00:00:00.000Z" },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_NOLO_SERVER_URL } from "../defaultServer";
|
|
2
|
+
import { NOLO_CLUSTER_SERVERS } from "../database/config";
|
|
2
3
|
import {
|
|
3
4
|
createHybridRecordStore,
|
|
4
5
|
shouldCacheHybridRemoteRecord,
|
|
@@ -21,6 +22,15 @@ function normalizeServer(value: string) {
|
|
|
21
22
|
return value.trim().replace(/\/+$/, "");
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
function resolveFallbackServers(env: EnvLike) {
|
|
26
|
+
const values = [
|
|
27
|
+
env.NOLO_SERVER_URL,
|
|
28
|
+
env.BASE_URL,
|
|
29
|
+
...NOLO_CLUSTER_SERVERS,
|
|
30
|
+
].filter((value): value is string => typeof value === "string" && value.trim().length > 0);
|
|
31
|
+
return [...new Set(values.map(normalizeServer))];
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
function resolveAuthToken(env: EnvLike) {
|
|
25
35
|
return env.AUTH_TOKEN || env.AUTH || env.BENCHMARK_AUTH_TOKEN || "";
|
|
26
36
|
}
|
|
@@ -35,6 +45,7 @@ export function createCliHybridRecordStore(
|
|
|
35
45
|
return createHybridRecordStore({
|
|
36
46
|
db: deps.db,
|
|
37
47
|
defaultServer: normalizeServer(deps.env.NOLO_SERVER || deps.env.BASE_URL || DEFAULT_NOLO_SERVER_URL),
|
|
48
|
+
fallbackServers: resolveFallbackServers(deps.env),
|
|
38
49
|
authToken: resolveAuthToken(deps.env),
|
|
39
50
|
fetchImpl: deps.fetchImpl,
|
|
40
51
|
});
|
|
@@ -8,26 +8,22 @@ import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
|
|
|
8
8
|
|
|
9
9
|
describe("CLI local runtime adapter", () => {
|
|
10
10
|
const DEFAULT_LOCAL_CODING_TOOL_NAMES = [
|
|
11
|
-
"
|
|
11
|
+
"listFiles",
|
|
12
|
+
"readFile",
|
|
13
|
+
"writeFile",
|
|
14
|
+
"editFile",
|
|
15
|
+
"searchFiles",
|
|
16
|
+
];
|
|
17
|
+
const LEGACY_WRITE_LOCAL_CODING_TOOL_NAMES = [
|
|
12
18
|
"writeWorkspaceFile",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"gitDiff",
|
|
18
|
-
"gitCreateBranch",
|
|
19
|
-
"commitWorkspace",
|
|
20
|
-
"runPackageScript",
|
|
21
|
-
"startPreview",
|
|
22
|
-
"getPreviewStatus",
|
|
23
|
-
"stopPreview",
|
|
24
|
-
"releasePreview",
|
|
25
|
-
"captureVisualState",
|
|
19
|
+
"listFiles",
|
|
20
|
+
"readFile",
|
|
21
|
+
"editFile",
|
|
22
|
+
"searchFiles",
|
|
26
23
|
];
|
|
27
24
|
const SHELL_LOCAL_CODING_TOOL_NAMES = [
|
|
28
25
|
...DEFAULT_LOCAL_CODING_TOOL_NAMES,
|
|
29
26
|
"execShell",
|
|
30
|
-
"execBash",
|
|
31
27
|
];
|
|
32
28
|
|
|
33
29
|
function toolNamesFromRequest(request: any) {
|
|
@@ -211,6 +207,77 @@ describe("CLI local runtime adapter", () => {
|
|
|
211
207
|
});
|
|
212
208
|
});
|
|
213
209
|
|
|
210
|
+
test("keeps local runtime runnable when the configured local server is down but a cluster server can provide agent config", async () => {
|
|
211
|
+
const memory = new Map<string, any>();
|
|
212
|
+
const requests: string[] = [];
|
|
213
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
214
|
+
env: {
|
|
215
|
+
NOLO_LOCAL_USER_ID: "user-1",
|
|
216
|
+
NOLO_SERVER: "http://127.0.0.1:38123",
|
|
217
|
+
AUTH_TOKEN: "token-1",
|
|
218
|
+
OPENAI_API_KEY: "sk-local",
|
|
219
|
+
NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
|
|
220
|
+
},
|
|
221
|
+
db: {
|
|
222
|
+
get: async (key) => {
|
|
223
|
+
if (!memory.has(key)) throw new Error(`not found: ${key}`);
|
|
224
|
+
return memory.get(key);
|
|
225
|
+
},
|
|
226
|
+
put: async (key, value) => {
|
|
227
|
+
memory.set(key, value);
|
|
228
|
+
},
|
|
229
|
+
batch: async (ops) => {
|
|
230
|
+
for (const op of ops) {
|
|
231
|
+
if (op.type === "put") memory.set(op.key, op.value);
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
iterator: () => (async function* () {})(),
|
|
235
|
+
},
|
|
236
|
+
createId: () => "01CLUSTER",
|
|
237
|
+
fetchImpl: async (url, init) => {
|
|
238
|
+
const target = String(url);
|
|
239
|
+
requests.push(target);
|
|
240
|
+
if (target === "http://127.0.0.1:38123/api/v1/db/read/agent-user-1-cluster") {
|
|
241
|
+
throw new Error("ConnectionRefused");
|
|
242
|
+
}
|
|
243
|
+
if (target === "https://nolo.chat/api/v1/db/read/agent-user-1-cluster") {
|
|
244
|
+
expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer token-1");
|
|
245
|
+
return Response.json({
|
|
246
|
+
data: {
|
|
247
|
+
dbKey: "agent-user-1-cluster",
|
|
248
|
+
name: "Cluster cached",
|
|
249
|
+
prompt: "Use cached config",
|
|
250
|
+
model: "gpt-4.1-mini",
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
if (target === "http://127.0.0.1:11434/v1/chat/completions") {
|
|
255
|
+
return Response.json({
|
|
256
|
+
choices: [{ message: { content: "cluster cache ok" } }],
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return new Response(null, { status: 404 });
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const result = await runLocalAgentTurn({
|
|
264
|
+
adapter,
|
|
265
|
+
agentRef: "agent-user-1-cluster",
|
|
266
|
+
input: "hello",
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
expect(result.content).toBe("cluster cache ok");
|
|
270
|
+
expect(requests).toEqual([
|
|
271
|
+
"http://127.0.0.1:38123/api/v1/db/read/agent-user-1-cluster",
|
|
272
|
+
"https://nolo.chat/api/v1/db/read/agent-user-1-cluster",
|
|
273
|
+
"http://127.0.0.1:11434/v1/chat/completions",
|
|
274
|
+
]);
|
|
275
|
+
expect(memory.get("agent-user-1-cluster")).toMatchObject({
|
|
276
|
+
name: "Cluster cached",
|
|
277
|
+
serverOrigin: "https://nolo.chat",
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
214
281
|
test("uses agent customProviderUrl for local OpenAI-compatible requests", async () => {
|
|
215
282
|
const requests: Array<{ url: string; auth: string | null; body: any }> = [];
|
|
216
283
|
const store = new Map<string, any>([
|
|
@@ -353,9 +420,10 @@ describe("CLI local runtime adapter", () => {
|
|
|
353
420
|
tool_choice: "auto",
|
|
354
421
|
url: "https://api.fireworks.ai/inference/v1/chat/completions",
|
|
355
422
|
provider: "fireworks",
|
|
423
|
+
agentKey: "agent-user-1-frontend",
|
|
356
424
|
},
|
|
357
425
|
});
|
|
358
|
-
expect(toolNamesFromRequest(requests[0])).toEqual(
|
|
426
|
+
expect(toolNamesFromRequest(requests[0])).toEqual(LEGACY_WRITE_LOCAL_CODING_TOOL_NAMES);
|
|
359
427
|
});
|
|
360
428
|
|
|
361
429
|
test("uses the Nolo chat proxy for platform agents even when direct provider env exists", async () => {
|
|
@@ -369,7 +437,7 @@ describe("CLI local runtime adapter", () => {
|
|
|
369
437
|
provider: "fireworks",
|
|
370
438
|
apiSource: "platform",
|
|
371
439
|
useServerProxy: true,
|
|
372
|
-
tools: ["
|
|
440
|
+
tools: ["readWorkspaceFile"],
|
|
373
441
|
}],
|
|
374
442
|
]);
|
|
375
443
|
const adapter = createCliLocalRuntimeAdapter({
|
|
@@ -419,6 +487,7 @@ describe("CLI local runtime adapter", () => {
|
|
|
419
487
|
body: {
|
|
420
488
|
provider: "fireworks",
|
|
421
489
|
apiSource: "platform",
|
|
490
|
+
agentKey: "agent-user-1-frontend",
|
|
422
491
|
},
|
|
423
492
|
});
|
|
424
493
|
});
|
|
@@ -573,6 +642,12 @@ describe("CLI local runtime adapter", () => {
|
|
|
573
642
|
cwd: import.meta.dir,
|
|
574
643
|
now: () => 1710000000000,
|
|
575
644
|
createId: () => "01TRACE",
|
|
645
|
+
localToolExecutors: {
|
|
646
|
+
execShell: async () => ({
|
|
647
|
+
content: "stdout:\ntrace-ok\n\nexitCode: 0",
|
|
648
|
+
metadata: { exitCode: 0, timedOut: false },
|
|
649
|
+
}),
|
|
650
|
+
},
|
|
576
651
|
fetchImpl: async (_url, init) => {
|
|
577
652
|
const body = JSON.parse(String(init?.body));
|
|
578
653
|
const hasToolResult = body.messages.some((message: any) => message.role === "tool");
|
|
@@ -586,7 +661,11 @@ describe("CLI local runtime adapter", () => {
|
|
|
586
661
|
type: "function",
|
|
587
662
|
function: {
|
|
588
663
|
name: "execShell",
|
|
589
|
-
arguments: JSON.stringify({
|
|
664
|
+
arguments: JSON.stringify({
|
|
665
|
+
cmd: process.platform === "win32"
|
|
666
|
+
? "Write-Output trace-ok"
|
|
667
|
+
: "printf trace-ok",
|
|
668
|
+
}),
|
|
590
669
|
},
|
|
591
670
|
}],
|
|
592
671
|
},
|
|
@@ -951,7 +1030,7 @@ describe("CLI local runtime adapter", () => {
|
|
|
951
1030
|
await adapter.loadAgentConfig("default-tools");
|
|
952
1031
|
await expect(adapter.executeTool({
|
|
953
1032
|
id: "call-read",
|
|
954
|
-
name: "
|
|
1033
|
+
name: "readFile",
|
|
955
1034
|
arguments: JSON.stringify({ path: "README.md" }),
|
|
956
1035
|
})).resolves.toMatchObject({
|
|
957
1036
|
content: "local ok\n",
|
|
@@ -961,6 +1040,91 @@ describe("CLI local runtime adapter", () => {
|
|
|
961
1040
|
}
|
|
962
1041
|
});
|
|
963
1042
|
|
|
1043
|
+
test("exposes declared taskRun as a platform tool in local runtime requests", async () => {
|
|
1044
|
+
const requests: Array<{ body: any }> = [];
|
|
1045
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
1046
|
+
env: {
|
|
1047
|
+
OPENAI_API_KEY: "sk-local",
|
|
1048
|
+
NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
|
|
1049
|
+
},
|
|
1050
|
+
db: {
|
|
1051
|
+
get: async () => ({
|
|
1052
|
+
dbKey: "agent-local-taskrun",
|
|
1053
|
+
prompt: "Use taskRun.",
|
|
1054
|
+
model: "gpt-4.1-mini",
|
|
1055
|
+
toolNames: ["taskRun"],
|
|
1056
|
+
}),
|
|
1057
|
+
put: async () => {},
|
|
1058
|
+
batch: async () => {},
|
|
1059
|
+
iterator: () => (async function* () {})(),
|
|
1060
|
+
},
|
|
1061
|
+
fetchImpl: async (_url, init) => {
|
|
1062
|
+
requests.push({ body: JSON.parse(String(init?.body)) });
|
|
1063
|
+
return Response.json({
|
|
1064
|
+
choices: [{ message: { content: "done" } }],
|
|
1065
|
+
});
|
|
1066
|
+
},
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
await runLocalAgentTurn({
|
|
1070
|
+
adapter,
|
|
1071
|
+
agentRef: "taskrun",
|
|
1072
|
+
input: "read task context",
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
expect(toolNamesFromRequest(requests[0])).toContain("taskRun");
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
test("executes declared taskRun through the server task-run bridge", async () => {
|
|
1079
|
+
const requests: Array<{ url: string; auth: string | null; body: any }> = [];
|
|
1080
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
1081
|
+
env: {
|
|
1082
|
+
BASE_URL: "https://us.nolo.chat",
|
|
1083
|
+
AUTH_TOKEN: "machine-token",
|
|
1084
|
+
},
|
|
1085
|
+
db: {
|
|
1086
|
+
get: async () => ({
|
|
1087
|
+
dbKey: "agent-local-taskrun",
|
|
1088
|
+
toolNames: ["taskRun"],
|
|
1089
|
+
}),
|
|
1090
|
+
put: async () => {},
|
|
1091
|
+
batch: async () => {},
|
|
1092
|
+
iterator: () => (async function* () {})(),
|
|
1093
|
+
},
|
|
1094
|
+
fetchImpl: async (url, init) => {
|
|
1095
|
+
requests.push({
|
|
1096
|
+
url: String(url),
|
|
1097
|
+
auth: new Headers(init?.headers).get("authorization"),
|
|
1098
|
+
body: JSON.parse(String(init?.body)),
|
|
1099
|
+
});
|
|
1100
|
+
return Response.json({ ok: true, row: { rowId: "task-1" } });
|
|
1101
|
+
},
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
await adapter.loadAgentConfig("taskrun");
|
|
1105
|
+
const result = await adapter.executeTool({
|
|
1106
|
+
id: "call-taskrun",
|
|
1107
|
+
name: "taskRun",
|
|
1108
|
+
arguments: JSON.stringify({
|
|
1109
|
+
action: "readTaskContext",
|
|
1110
|
+
dbKey: "row-user-NOLOTASKBOARD-task-1",
|
|
1111
|
+
}),
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
expect(requests).toEqual([
|
|
1115
|
+
{
|
|
1116
|
+
url: "https://us.nolo.chat/api/task-run",
|
|
1117
|
+
auth: "Bearer machine-token",
|
|
1118
|
+
body: {
|
|
1119
|
+
action: "readTaskContext",
|
|
1120
|
+
dbKey: "row-user-NOLOTASKBOARD-task-1",
|
|
1121
|
+
},
|
|
1122
|
+
},
|
|
1123
|
+
]);
|
|
1124
|
+
expect(result.content).toContain("\"ok\":true");
|
|
1125
|
+
expect(result.metadata).toMatchObject({ serverPlatformTool: true });
|
|
1126
|
+
});
|
|
1127
|
+
|
|
964
1128
|
test("activates an agent-declared task worktree before running workspace tools", async () => {
|
|
965
1129
|
const taskWorktreeRoot = mkdtempSync(join(tmpdir(), "nolo-cli-runtime-worktree-"));
|
|
966
1130
|
const chunks: string[] = [];
|
|
@@ -992,7 +1156,7 @@ describe("CLI local runtime adapter", () => {
|
|
|
992
1156
|
await adapter.loadAgentConfig("frontend");
|
|
993
1157
|
const result = await adapter.executeTool({
|
|
994
1158
|
id: "call-read",
|
|
995
|
-
name: "
|
|
1159
|
+
name: "readFile",
|
|
996
1160
|
arguments: JSON.stringify({ path: "README.md" }),
|
|
997
1161
|
});
|
|
998
1162
|
|
|
@@ -1017,6 +1181,12 @@ describe("CLI local runtime adapter", () => {
|
|
|
1017
1181
|
iterator: () => (async function* () {})(),
|
|
1018
1182
|
},
|
|
1019
1183
|
cwd: import.meta.dir,
|
|
1184
|
+
localToolExecutors: {
|
|
1185
|
+
execShell: async () => ({
|
|
1186
|
+
content: import.meta.dir,
|
|
1187
|
+
metadata: { exitCode: 0, timedOut: false },
|
|
1188
|
+
}),
|
|
1189
|
+
},
|
|
1020
1190
|
fetchImpl: async () => Response.json({}),
|
|
1021
1191
|
});
|
|
1022
1192
|
|
|
@@ -1068,7 +1238,7 @@ describe("CLI local runtime adapter", () => {
|
|
|
1068
1238
|
}),
|
|
1069
1239
|
});
|
|
1070
1240
|
|
|
1071
|
-
expect(result.content).toBe(
|
|
1241
|
+
expect(result.content).toBe(`wrote ${join("src", "app.ts")}`);
|
|
1072
1242
|
expect(readFileSync(join(workspaceRoot, "src/app.ts"), "utf8")).toBe("export const cliValue = 1;\n");
|
|
1073
1243
|
} finally {
|
|
1074
1244
|
rmSync(workspaceRoot, { recursive: true, force: true });
|