nolo-cli 0.1.22 → 0.1.23
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/agent-runtime/agentRecordConfig.ts +4 -0
- package/agent-runtime/hostAdapter.ts +2 -0
- package/agent-runtime/index.ts +7 -0
- package/agent-runtime/localLoop.ts +2 -0
- package/agent-runtime/platformChatProvider.ts +3 -0
- package/agent-runtime/runtimeToolPolicy.ts +92 -0
- package/agent-runtime/types.ts +42 -0
- package/agentRunCommand.ts +74 -1
- package/agentRuntimeCommands.ts +17 -89
- package/ai/agent/streamAgentChatTurn.ts +104 -20
- package/ai/chat/fetchUtils.native.ts +2 -0
- package/ai/chat/fetchUtils.ts +2 -0
- package/ai/chat/sendOpenAICompletionsRequest.ts +56 -0
- package/ai/chat/sendOpenAIResponseRequest.ts +64 -0
- package/ai/llm/kimi.ts +1 -1
- package/ai/llm/providers.ts +3 -0
- package/ai/llm/reasoningModels.ts +1 -0
- package/ai/taskRun/taskRunProtocol.ts +1 -0
- package/ai/tools/agent/agentTools.ts +17 -0
- package/ai/tools/agent/startAgentDialogTool.ts +53 -0
- package/ai/tools/modelUsageTools.ts +5 -0
- package/client/agentRun.test.ts +207 -5
- package/client/agentRun.ts +77 -34
- package/client/localRuntimeAdapter.test.ts +2 -0
- package/client/localRuntimeAdapter.ts +15 -2
- package/database/actions/common.ts +4 -3
- package/database/config.ts +19 -0
- package/machineCommands.ts +400 -45
- package/package.json +4 -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/tableCommands.ts +42 -5
package/client/agentRun.test.ts
CHANGED
|
@@ -132,6 +132,12 @@ describe("cli agent run client", () => {
|
|
|
132
132
|
expect(requests[0]?.body.userInput).toContain("rowDbKey: row-b2e06f801f-01TASK");
|
|
133
133
|
expect(requests[0]?.body.userInput).toContain("workItemId: frontend-filter");
|
|
134
134
|
expect(requests[0]?.body.userInput).toContain("User task:\nFix the filter UI");
|
|
135
|
+
expect(requests[0]?.body.runtimeContext.taskRun).toEqual({
|
|
136
|
+
rowDbKey: "row-b2e06f801f-01TASK",
|
|
137
|
+
taskRunId: "taskrun-1",
|
|
138
|
+
workItemId: "frontend-filter",
|
|
139
|
+
artifactIds: ["artifact-1"],
|
|
140
|
+
});
|
|
135
141
|
});
|
|
136
142
|
|
|
137
143
|
test("records task-run completion after an HTTP agent run returns a dialog", async () => {
|
|
@@ -243,6 +249,42 @@ describe("cli agent run client", () => {
|
|
|
243
249
|
expect(result).toEqual({ exitCode: 0, dialogId: "dialog-1" });
|
|
244
250
|
});
|
|
245
251
|
|
|
252
|
+
test("records background task-run handoff as running instead of completed", async () => {
|
|
253
|
+
const output = new CaptureOutput();
|
|
254
|
+
const writebacks: any[] = [];
|
|
255
|
+
|
|
256
|
+
const result = await runAgentTurn({
|
|
257
|
+
agentName: "frontend-implementer",
|
|
258
|
+
agentKey: "agent-frontend",
|
|
259
|
+
serverUrl: "https://nolo.chat",
|
|
260
|
+
message: "Fix settings style FOUC",
|
|
261
|
+
scriptDir: "C:/missing/scripts",
|
|
262
|
+
env: { AUTH_TOKEN: "token-123" },
|
|
263
|
+
runtimeMode: "server",
|
|
264
|
+
background: true,
|
|
265
|
+
noStream: true,
|
|
266
|
+
taskRunContext: {
|
|
267
|
+
rowDbKey: "row-b2e06f801f-01TASK",
|
|
268
|
+
workItemId: "self",
|
|
269
|
+
},
|
|
270
|
+
taskRunRecorder: async (args) => {
|
|
271
|
+
writebacks.push(args);
|
|
272
|
+
},
|
|
273
|
+
output,
|
|
274
|
+
scriptPathExists: () => false,
|
|
275
|
+
fetchImpl: async () => {
|
|
276
|
+
return Response.json({ dialogId: "dialog-bg", status: "pending" }, { status: 202 });
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(result).toEqual({ exitCode: 0, dialogId: "dialog-bg" });
|
|
281
|
+
expect(writebacks).toHaveLength(1);
|
|
282
|
+
expect(writebacks[0]).toMatchObject({
|
|
283
|
+
status: "running",
|
|
284
|
+
dialogId: "dialog-bg",
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
246
288
|
test("runs forced local turns through the injected runtime adapter without HTTP", async () => {
|
|
247
289
|
const output = new CaptureOutput();
|
|
248
290
|
const result = await runAgentTurn({
|
|
@@ -522,6 +564,58 @@ describe("cli agent run client", () => {
|
|
|
522
564
|
});
|
|
523
565
|
});
|
|
524
566
|
|
|
567
|
+
test("records local background task-run handoff as running instead of completed", async () => {
|
|
568
|
+
const output = new CaptureOutput();
|
|
569
|
+
const writebacks: any[] = [];
|
|
570
|
+
|
|
571
|
+
const result = await runAgentTurn({
|
|
572
|
+
agentName: "frontend",
|
|
573
|
+
agentKey: "frontend-local",
|
|
574
|
+
serverUrl: "https://nolo.chat",
|
|
575
|
+
message: "inspect repo in background",
|
|
576
|
+
scriptDir: "C:/missing/scripts",
|
|
577
|
+
env: { AUTH_TOKEN: "token-123" },
|
|
578
|
+
output,
|
|
579
|
+
runtimeMode: "local",
|
|
580
|
+
background: true,
|
|
581
|
+
taskRunContext: {
|
|
582
|
+
rowDbKey: "row-b2e06f801f-01TASK",
|
|
583
|
+
workItemId: "self",
|
|
584
|
+
},
|
|
585
|
+
taskRunRecorder: async (args) => {
|
|
586
|
+
writebacks.push(args);
|
|
587
|
+
},
|
|
588
|
+
localRuntimeAdapter: {
|
|
589
|
+
host: "cli",
|
|
590
|
+
capabilities: ["local-provider", "local-persistence"],
|
|
591
|
+
loadAgentConfig: async (agentRef) => ({
|
|
592
|
+
key: agentRef,
|
|
593
|
+
name: "Frontend",
|
|
594
|
+
prompt: "Fix UI",
|
|
595
|
+
model: "fake-local",
|
|
596
|
+
}),
|
|
597
|
+
loadDialogHistory: async () => [],
|
|
598
|
+
saveTurn: async () => ({ dialogId: "dialog-local-bg" }),
|
|
599
|
+
resolveProvider: async () => ({
|
|
600
|
+
model: "fake-local",
|
|
601
|
+
complete: async () => ({ content: "accepted locally", model: "fake-local" }),
|
|
602
|
+
}),
|
|
603
|
+
executeTool: async () => {
|
|
604
|
+
throw new Error("no tools expected");
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
scriptPathExists: () => false,
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
expect(result).toEqual({ exitCode: 0, dialogId: "dialog-local-bg" });
|
|
611
|
+
expect(writebacks).toHaveLength(1);
|
|
612
|
+
expect(writebacks[0]).toMatchObject({
|
|
613
|
+
status: "running",
|
|
614
|
+
dialogId: "dialog-local-bg",
|
|
615
|
+
});
|
|
616
|
+
expect(writebacks[0].resultSummary).toBeUndefined();
|
|
617
|
+
});
|
|
618
|
+
|
|
525
619
|
test("auto mode prefers a working local runtime before HTTP", async () => {
|
|
526
620
|
const output = new CaptureOutput();
|
|
527
621
|
const httpCalls: string[] = [];
|
|
@@ -616,6 +710,58 @@ describe("cli agent run client", () => {
|
|
|
616
710
|
expect(output.text()).toContain("pm > server ok");
|
|
617
711
|
});
|
|
618
712
|
|
|
713
|
+
test("auto mode skips local runtime for server platform tools declared by runtime policy", async () => {
|
|
714
|
+
const output = new CaptureOutput();
|
|
715
|
+
const httpCalls: Array<{ url: string; body: any }> = [];
|
|
716
|
+
|
|
717
|
+
const result = await runAgentTurn({
|
|
718
|
+
agentName: "pm",
|
|
719
|
+
agentKey: "agent-policy-platform-tools",
|
|
720
|
+
serverUrl: "https://nolo.chat",
|
|
721
|
+
message: "query task rows",
|
|
722
|
+
scriptDir: "C:/missing/scripts",
|
|
723
|
+
env: { AUTH_TOKEN: "token-123" },
|
|
724
|
+
output,
|
|
725
|
+
runtimeMode: "auto",
|
|
726
|
+
localRuntimeAdapter: {
|
|
727
|
+
host: "cli",
|
|
728
|
+
capabilities: ["leveldb-agent-config", "local-provider", "local-tools"],
|
|
729
|
+
loadAgentConfig: async (agentRef) => ({
|
|
730
|
+
key: agentRef,
|
|
731
|
+
name: "PM",
|
|
732
|
+
prompt: "Manage task rows",
|
|
733
|
+
model: "fake-local",
|
|
734
|
+
runtimeToolPolicy: {
|
|
735
|
+
version: 1,
|
|
736
|
+
agentTools: ["queryTableRows", "taskRun"],
|
|
737
|
+
runtimeTools: ["execShell"],
|
|
738
|
+
},
|
|
739
|
+
}),
|
|
740
|
+
loadDialogHistory: async () => [],
|
|
741
|
+
saveTurn: async () => {
|
|
742
|
+
throw new Error("local runtime should be skipped");
|
|
743
|
+
},
|
|
744
|
+
resolveProvider: async () => {
|
|
745
|
+
throw new Error("local provider should be skipped");
|
|
746
|
+
},
|
|
747
|
+
executeTool: async () => {
|
|
748
|
+
throw new Error("local tools should be skipped");
|
|
749
|
+
},
|
|
750
|
+
},
|
|
751
|
+
scriptPathExists: () => false,
|
|
752
|
+
fetchImpl: async (url, init) => {
|
|
753
|
+
httpCalls.push({ url: String(url), body: JSON.parse(String(init?.body)) });
|
|
754
|
+
return Response.json({ content: "server ok", dialogId: "dialog-server-policy" });
|
|
755
|
+
},
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
expect(result).toEqual({ exitCode: 0, dialogId: "dialog-server-policy" });
|
|
759
|
+
expect(httpCalls).toHaveLength(1);
|
|
760
|
+
expect(httpCalls[0]?.body.agentKey).toBe("agent-policy-platform-tools");
|
|
761
|
+
expect(output.text()).toContain("auto runtime: skipping local runtime");
|
|
762
|
+
expect(output.text()).toContain("queryTableRows, taskRun");
|
|
763
|
+
});
|
|
764
|
+
|
|
619
765
|
test("auto mode skips known platform agents when local config cannot be read", async () => {
|
|
620
766
|
const output = new CaptureOutput();
|
|
621
767
|
const httpCalls: Array<{ url: string; body: any }> = [];
|
|
@@ -733,11 +879,67 @@ describe("cli agent run client", () => {
|
|
|
733
879
|
});
|
|
734
880
|
|
|
735
881
|
expect(result).toEqual({ exitCode: 0, dialogId: "dialog-stream" });
|
|
736
|
-
expect(output.text()).toContain("nolo -> working");
|
|
737
|
-
expect(output.text()).toContain("nolo > 你好");
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
test("
|
|
882
|
+
expect(output.text()).toContain("nolo -> working");
|
|
883
|
+
expect(output.text()).toContain("nolo > 你好");
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
test("returns a recoverable dialog when a server stream drops after dialog creation", async () => {
|
|
887
|
+
const output = new CaptureOutput();
|
|
888
|
+
|
|
889
|
+
const result = await runAgentTurn({
|
|
890
|
+
agentName: "nolo",
|
|
891
|
+
agentKey: "agent-pub-test",
|
|
892
|
+
serverUrl: "https://nolo.chat",
|
|
893
|
+
message: "hello",
|
|
894
|
+
scriptDir: "C:/missing/scripts",
|
|
895
|
+
env: { AUTH_TOKEN: "token-123" },
|
|
896
|
+
output,
|
|
897
|
+
scriptPathExists: () => false,
|
|
898
|
+
fetchImpl: async () => {
|
|
899
|
+
let sent = false;
|
|
900
|
+
return new Response(
|
|
901
|
+
new ReadableStream({
|
|
902
|
+
pull(controller) {
|
|
903
|
+
const encoder = new TextEncoder();
|
|
904
|
+
if (!sent) {
|
|
905
|
+
sent = true;
|
|
906
|
+
controller.enqueue(
|
|
907
|
+
encoder.encode(
|
|
908
|
+
[
|
|
909
|
+
`data: ${JSON.stringify({
|
|
910
|
+
type: "dialog",
|
|
911
|
+
dialogId: "dialog-recoverable",
|
|
912
|
+
status: "running",
|
|
913
|
+
})}`,
|
|
914
|
+
"",
|
|
915
|
+
`data: ${JSON.stringify({ type: "text", content: "partial" })}`,
|
|
916
|
+
"",
|
|
917
|
+
].join("\n")
|
|
918
|
+
)
|
|
919
|
+
);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
controller.error(new Error("socket closed"));
|
|
923
|
+
},
|
|
924
|
+
}),
|
|
925
|
+
{
|
|
926
|
+
status: 200,
|
|
927
|
+
headers: { "Content-Type": "text/event-stream" },
|
|
928
|
+
}
|
|
929
|
+
);
|
|
930
|
+
},
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
expect(result).toEqual({
|
|
934
|
+
exitCode: 0,
|
|
935
|
+
dialogId: "dialog-recoverable",
|
|
936
|
+
streamInterrupted: true,
|
|
937
|
+
});
|
|
938
|
+
expect(output.text()).toContain("dialog dialog-recoverable was created");
|
|
939
|
+
expect(output.text()).toContain("read the dialog before retrying");
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
test("prints an auth hint when installed without repo scripts or AUTH_TOKEN", async () => {
|
|
741
943
|
const output = new CaptureOutput();
|
|
742
944
|
|
|
743
945
|
const result = await runAgentTurn({
|
package/client/agentRun.ts
CHANGED
|
@@ -59,7 +59,7 @@ type RunAgentTurnOptions = {
|
|
|
59
59
|
taskRunContext?: TaskRunPromptContext;
|
|
60
60
|
taskRunRecorder?: (args: {
|
|
61
61
|
options: RunAgentTurnOptions;
|
|
62
|
-
status: "completed" | "failed";
|
|
62
|
+
status: "running" | "completed" | "failed";
|
|
63
63
|
dialogId?: string;
|
|
64
64
|
resultSummary?: string;
|
|
65
65
|
errorCode?: string;
|
|
@@ -69,10 +69,11 @@ type RunAgentTurnOptions = {
|
|
|
69
69
|
fetchImpl?: typeof fetch;
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
-
export type RunAgentTurnResult = {
|
|
73
|
-
exitCode: number;
|
|
74
|
-
dialogId?: string;
|
|
75
|
-
|
|
72
|
+
export type RunAgentTurnResult = {
|
|
73
|
+
exitCode: number;
|
|
74
|
+
dialogId?: string;
|
|
75
|
+
streamInterrupted?: boolean;
|
|
76
|
+
};
|
|
76
77
|
|
|
77
78
|
type ScriptBridgeDecision = {
|
|
78
79
|
hasAuthToken: boolean;
|
|
@@ -132,6 +133,15 @@ export function findServerPlatformTools(toolNames?: string[]) {
|
|
|
132
133
|
return toolNames.filter((toolName) => SERVER_PLATFORM_TOOL_NAMES.has(toolName));
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
function resolveServerPlatformToolNames(agentConfig: any) {
|
|
137
|
+
return findServerPlatformTools([
|
|
138
|
+
...(Array.isArray(agentConfig?.toolNames) ? agentConfig.toolNames : []),
|
|
139
|
+
...(Array.isArray(agentConfig?.runtimeToolPolicy?.agentTools)
|
|
140
|
+
? agentConfig.runtimeToolPolicy.agentTools
|
|
141
|
+
: []),
|
|
142
|
+
]);
|
|
143
|
+
}
|
|
144
|
+
|
|
135
145
|
function normalizeAgentRef(ref?: string) {
|
|
136
146
|
return ref?.trim().toLowerCase().replace(/\s+/g, " ");
|
|
137
147
|
}
|
|
@@ -190,7 +200,7 @@ async function shouldSkipAutoLocalForServerPlatformTools(options: RunAgentTurnOp
|
|
|
190
200
|
} catch {
|
|
191
201
|
return false;
|
|
192
202
|
}
|
|
193
|
-
const serverTools =
|
|
203
|
+
const serverTools = resolveServerPlatformToolNames(agentConfig);
|
|
194
204
|
if (serverTools.length === 0) return false;
|
|
195
205
|
options.output.write(
|
|
196
206
|
`[nolo] auto runtime: skipping local runtime because ${options.agentKey} declares server platform tools ` +
|
|
@@ -348,7 +358,7 @@ async function runTaskRunCli(args: {
|
|
|
348
358
|
|
|
349
359
|
async function defaultTaskRunRecorder(args: {
|
|
350
360
|
options: RunAgentTurnOptions;
|
|
351
|
-
status: "completed" | "failed";
|
|
361
|
+
status: "running" | "completed" | "failed";
|
|
352
362
|
dialogId?: string;
|
|
353
363
|
resultSummary?: string;
|
|
354
364
|
errorCode?: string;
|
|
@@ -357,7 +367,7 @@ async function defaultTaskRunRecorder(args: {
|
|
|
357
367
|
const context = args.options.taskRunContext;
|
|
358
368
|
if (!context?.rowDbKey) return;
|
|
359
369
|
const role = inferTaskRunAgentRole(args.options.agentName || args.options.agentKey);
|
|
360
|
-
if (role === "reviewer") {
|
|
370
|
+
if (role === "reviewer" && args.status !== "running") {
|
|
361
371
|
await runTaskRunCli({
|
|
362
372
|
options: args.options,
|
|
363
373
|
cliArgs: [
|
|
@@ -415,7 +425,7 @@ async function defaultTaskRunRecorder(args: {
|
|
|
415
425
|
],
|
|
416
426
|
});
|
|
417
427
|
}
|
|
418
|
-
if (role === "reviewer") {
|
|
428
|
+
if (role === "reviewer" && args.status !== "running") {
|
|
419
429
|
const reviewStatus =
|
|
420
430
|
args.status === "failed" ? "blocked" : classifyTaskRunReviewStatus(args.resultSummary);
|
|
421
431
|
if (reviewStatus) {
|
|
@@ -441,7 +451,7 @@ async function defaultTaskRunRecorder(args: {
|
|
|
441
451
|
|
|
442
452
|
async function recordTaskRunOutcome(args: {
|
|
443
453
|
options: RunAgentTurnOptions;
|
|
444
|
-
status: "completed" | "failed";
|
|
454
|
+
status: "running" | "completed" | "failed";
|
|
445
455
|
dialogId?: string;
|
|
446
456
|
resultSummary?: string;
|
|
447
457
|
errorCode?: string;
|
|
@@ -566,12 +576,28 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
|
|
|
566
576
|
options.imageUrls
|
|
567
577
|
),
|
|
568
578
|
runtimeContext: {
|
|
569
|
-
surface: "cli",
|
|
570
|
-
host: "terminal",
|
|
571
|
-
runtime: "bun",
|
|
572
|
-
entrypoint: "nolo-cli",
|
|
573
|
-
capabilities: ["text-io", "streaming", "slash-commands"],
|
|
574
|
-
|
|
579
|
+
surface: "cli",
|
|
580
|
+
host: "terminal",
|
|
581
|
+
runtime: "bun",
|
|
582
|
+
entrypoint: "nolo-cli",
|
|
583
|
+
capabilities: ["text-io", "streaming", "slash-commands"],
|
|
584
|
+
...(options.taskRunContext
|
|
585
|
+
? {
|
|
586
|
+
taskRun: {
|
|
587
|
+
rowDbKey: options.taskRunContext.rowDbKey,
|
|
588
|
+
...(options.taskRunContext.taskRunId
|
|
589
|
+
? { taskRunId: options.taskRunContext.taskRunId }
|
|
590
|
+
: {}),
|
|
591
|
+
...(options.taskRunContext.workItemId
|
|
592
|
+
? { workItemId: options.taskRunContext.workItemId }
|
|
593
|
+
: {}),
|
|
594
|
+
...(options.taskRunContext.artifactIds?.length
|
|
595
|
+
? { artifactIds: options.taskRunContext.artifactIds }
|
|
596
|
+
: {}),
|
|
597
|
+
},
|
|
598
|
+
}
|
|
599
|
+
: {}),
|
|
600
|
+
},
|
|
575
601
|
...(options.continueDialogId
|
|
576
602
|
? { continueDialogId: options.continueDialogId }
|
|
577
603
|
: {}),
|
|
@@ -597,7 +623,11 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
|
|
|
597
623
|
const result = await readStreamingAgentRun(options, res);
|
|
598
624
|
await recordTaskRunOutcome({
|
|
599
625
|
options,
|
|
600
|
-
status: result.
|
|
626
|
+
status: result.streamInterrupted
|
|
627
|
+
? "running"
|
|
628
|
+
: result.exitCode === 0
|
|
629
|
+
? "completed"
|
|
630
|
+
: "failed",
|
|
601
631
|
dialogId: result.dialogId,
|
|
602
632
|
...(result.exitCode !== 0 ? { errorCode: "STREAM_FAILED" } : {}),
|
|
603
633
|
});
|
|
@@ -642,9 +672,9 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
|
|
|
642
672
|
};
|
|
643
673
|
await recordTaskRunOutcome({
|
|
644
674
|
options,
|
|
645
|
-
status: "completed",
|
|
675
|
+
status: options.background ? "running" : "completed",
|
|
646
676
|
dialogId: result.dialogId,
|
|
647
|
-
resultSummary: content
|
|
677
|
+
...(!options.background && content ? { resultSummary: content } : {}),
|
|
648
678
|
});
|
|
649
679
|
return result;
|
|
650
680
|
}
|
|
@@ -747,9 +777,9 @@ async function runLocalAgentTurnForCli(
|
|
|
747
777
|
}
|
|
748
778
|
await recordTaskRunOutcome({
|
|
749
779
|
options,
|
|
750
|
-
status: "completed",
|
|
780
|
+
status: options.background ? "running" : "completed",
|
|
751
781
|
dialogId: result.dialogId,
|
|
752
|
-
resultSummary: content
|
|
782
|
+
...(!options.background && content ? { resultSummary: content } : {}),
|
|
753
783
|
toolEvidence: summarizeLocalToolEvidence(toolEvents),
|
|
754
784
|
});
|
|
755
785
|
return { exitCode: 0, dialogId: result.dialogId };
|
|
@@ -791,15 +821,20 @@ async function readStreamingAgentRun(
|
|
|
791
821
|
hasPrintedLabel = true;
|
|
792
822
|
};
|
|
793
823
|
|
|
794
|
-
const handlePayload = (payload: any) => {
|
|
795
|
-
if (
|
|
796
|
-
|
|
797
|
-
}
|
|
798
|
-
if (payload?.type === "
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
824
|
+
const handlePayload = (payload: any) => {
|
|
825
|
+
if (typeof payload?.dialogId === "string" && payload.dialogId.trim()) {
|
|
826
|
+
dialogId = payload.dialogId;
|
|
827
|
+
}
|
|
828
|
+
if (payload?.error || payload?.type === "error") {
|
|
829
|
+
throw new Error(String(payload.error || payload.message || "Agent stream failed"));
|
|
830
|
+
}
|
|
831
|
+
if (payload?.type === "done") {
|
|
832
|
+
usage = payload.usage;
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (payload?.type === "dialog" || payload?.type === "status") {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
803
838
|
|
|
804
839
|
const chunk =
|
|
805
840
|
payload?.type === "text"
|
|
@@ -842,10 +877,18 @@ async function readStreamingAgentRun(
|
|
|
842
877
|
.trim();
|
|
843
878
|
if (raw) handlePayload(JSON.parse(raw));
|
|
844
879
|
}
|
|
845
|
-
} catch (error) {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
880
|
+
} catch (error) {
|
|
881
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
882
|
+
if (dialogId) {
|
|
883
|
+
options.output.write(
|
|
884
|
+
`\n[nolo] Agent stream transport interrupted after dialog ${dialogId} was created: ${message}\n`
|
|
885
|
+
);
|
|
886
|
+
options.output.write("[nolo] The agent run may still finish on the server; read the dialog before retrying.\n");
|
|
887
|
+
return { exitCode: 0, dialogId, streamInterrupted: true };
|
|
888
|
+
}
|
|
889
|
+
options.output.write(`\n[nolo] Agent stream failed: ${message}\n`);
|
|
890
|
+
return { exitCode: 1 };
|
|
891
|
+
} finally {
|
|
849
892
|
writer.flushAll();
|
|
850
893
|
}
|
|
851
894
|
|
|
@@ -353,6 +353,7 @@ describe("CLI local runtime adapter", () => {
|
|
|
353
353
|
tool_choice: "auto",
|
|
354
354
|
url: "https://api.fireworks.ai/inference/v1/chat/completions",
|
|
355
355
|
provider: "fireworks",
|
|
356
|
+
agentKey: "agent-user-1-frontend",
|
|
356
357
|
},
|
|
357
358
|
});
|
|
358
359
|
expect(toolNamesFromRequest(requests[0])).toEqual(DEFAULT_LOCAL_CODING_TOOL_NAMES);
|
|
@@ -419,6 +420,7 @@ describe("CLI local runtime adapter", () => {
|
|
|
419
420
|
body: {
|
|
420
421
|
provider: "fireworks",
|
|
421
422
|
apiSource: "platform",
|
|
423
|
+
agentKey: "agent-user-1-frontend",
|
|
422
424
|
},
|
|
423
425
|
});
|
|
424
426
|
});
|
|
@@ -58,6 +58,7 @@ export type CliLocalRuntimeAdapterDeps = {
|
|
|
58
58
|
createId?: () => string;
|
|
59
59
|
fetchImpl?: typeof fetch;
|
|
60
60
|
cwd?: string;
|
|
61
|
+
useCwdAsTaskWorkspaceBase?: boolean;
|
|
61
62
|
output?: { write(chunk: string): unknown };
|
|
62
63
|
prepareTaskWorktree?: typeof prepareTaskWorktree;
|
|
63
64
|
localToolExecutors?: Record<string, (call: any) => Promise<{ content: string; metadata?: Record<string, unknown> }>>;
|
|
@@ -254,7 +255,11 @@ export function createCliLocalRuntimeAdapter(
|
|
|
254
255
|
const localToolBudgets = parseLocalToolBudgets(deps.env);
|
|
255
256
|
const localToolUsage = new Map<string, number>();
|
|
256
257
|
let activeAgentToolNames: string[] = [];
|
|
257
|
-
let workspaceSession: WorkspaceSession = createWorkspaceSession(
|
|
258
|
+
let workspaceSession: WorkspaceSession = createWorkspaceSession(
|
|
259
|
+
deps.useCwdAsTaskWorkspaceBase
|
|
260
|
+
? { defaultCwd: deps.cwd }
|
|
261
|
+
: { cwd: deps.cwd }
|
|
262
|
+
);
|
|
258
263
|
let localToolExecutors = buildLocalToolExecutors({
|
|
259
264
|
workspaceRoot: workspaceSession.workspaceRoot,
|
|
260
265
|
localToolExecutors: deps.localToolExecutors,
|
|
@@ -378,12 +383,20 @@ export function createCliLocalRuntimeAdapter(
|
|
|
378
383
|
budgets: localToolBudgets,
|
|
379
384
|
usage: localToolUsage,
|
|
380
385
|
});
|
|
381
|
-
|
|
386
|
+
const result = await executeLocalToolWithPolicy({
|
|
382
387
|
env: deps.env,
|
|
383
388
|
agentToolNames: activeAgentToolNames,
|
|
384
389
|
call,
|
|
385
390
|
executors: localToolExecutors,
|
|
386
391
|
});
|
|
392
|
+
return {
|
|
393
|
+
...result,
|
|
394
|
+
metadata: {
|
|
395
|
+
...(result.metadata ?? {}),
|
|
396
|
+
workspaceRoot: workspaceSession.workspaceRoot,
|
|
397
|
+
workspaceKind: workspaceSession.kind,
|
|
398
|
+
},
|
|
399
|
+
};
|
|
387
400
|
},
|
|
388
401
|
};
|
|
389
402
|
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import pino from "pino";
|
|
4
4
|
import { getIsDesktopApp } from "app/utils/env";
|
|
5
|
-
import {
|
|
5
|
+
import { fetchWithTransientReadRetry } from "app/utils/retryFetch";
|
|
6
|
+
import { API_ENDPOINTS, NOLO_CLUSTER_SERVERS, normalizeKnownServerOrigin } from "../config";
|
|
6
7
|
|
|
7
8
|
// RN 下 pino 的 browser 写法可能有兼容性问题
|
|
8
9
|
// 使用简单的 console 封装作为 fallback
|
|
@@ -23,7 +24,7 @@ export const logger = isRN ? {
|
|
|
23
24
|
// },
|
|
24
25
|
});
|
|
25
26
|
const normalizeServer = (server: string): string =>
|
|
26
|
-
server.trim().replace(/\/+$/, "");
|
|
27
|
+
normalizeKnownServerOrigin(server) ?? server.trim().replace(/\/+$/, "");
|
|
27
28
|
const isNoloClusterServer = (server: string): boolean =>
|
|
28
29
|
/^https?:\/\/(?:us\.)?nolo\.chat$/i.test(normalizeServer(server));
|
|
29
30
|
|
|
@@ -202,7 +203,7 @@ export const fetchFromServer = async (
|
|
|
202
203
|
signal?.addEventListener("abort", onExternalAbort);
|
|
203
204
|
|
|
204
205
|
try {
|
|
205
|
-
const res = await
|
|
206
|
+
const res = await fetchWithTransientReadRetry(
|
|
206
207
|
`${server}${buildReadUrl(dbKey)}`,
|
|
207
208
|
{
|
|
208
209
|
signal: controller.signal as any,
|
package/database/config.ts
CHANGED
|
@@ -8,6 +8,25 @@ export const SERVERS = {
|
|
|
8
8
|
} as const;
|
|
9
9
|
export const NOLO_CLUSTER_SERVERS = Object.values(SERVERS);
|
|
10
10
|
|
|
11
|
+
const LEGACY_SERVER_ORIGIN_MAP: Record<string, string> = {
|
|
12
|
+
"https://nolotus.com": SERVERS.MAIN,
|
|
13
|
+
"https://www.nolotus.com": SERVERS.MAIN,
|
|
14
|
+
"https://us.nolotus.com": SERVERS.US,
|
|
15
|
+
"https://www.us.nolotus.com": SERVERS.US,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const normalizeKnownServerOrigin = (server: unknown): string | null => {
|
|
19
|
+
if (typeof server !== "string" || server.trim().length === 0) return null;
|
|
20
|
+
const trimmed = server.trim();
|
|
21
|
+
let origin: string;
|
|
22
|
+
try {
|
|
23
|
+
origin = new URL(trimmed).origin;
|
|
24
|
+
} catch {
|
|
25
|
+
origin = trimmed.replace(/\/+$/, "");
|
|
26
|
+
}
|
|
27
|
+
return LEGACY_SERVER_ORIGIN_MAP[origin.toLowerCase()] ?? origin;
|
|
28
|
+
};
|
|
29
|
+
|
|
11
30
|
export const API_ENDPOINTS = {
|
|
12
31
|
DATABASE: `${API_VERSION}/db`,
|
|
13
32
|
SHARE: `${API_VERSION}/share`,
|