nolo-cli 0.1.37 → 0.1.39
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 +0 -14
- package/agent-runtime/hostAdapter.ts +0 -1
- package/agent-runtime/index.ts +24 -18
- package/agent-runtime/localDialogRead.ts +23 -0
- package/agent-runtime/localToolPolicy.ts +2 -7
- package/agent-runtime/localWorkspaceTools.ts +9 -0
- package/agent-runtime/noloWorkspaceTools.ts +371 -0
- package/agent-runtime/runtimeToolPolicy.ts +1 -3
- package/agent-runtime/types.ts +1 -1
- package/agentAliases.ts +6 -0
- package/agentRunCommand.ts +57 -65
- package/agentRuntimeLocal.ts +0 -7
- package/chat/messages/fetchMessages.ts +84 -0
- package/chat/messages/types.ts +185 -0
- package/cli/agentAliases.ts +6 -0
- package/cli/agentRunCommand.ts +57 -65
- package/cli/agentRuntimeLocal.ts +0 -7
- package/cli/cliEnvHelpers.ts +11 -7
- package/cli/client/agentRun.ts +123 -24
- package/cli/client/localRuntimeAdapter.ts +87 -59
- package/cli/connectorRunArtifact.ts +0 -1
- package/cli/dialogCommands.ts +572 -0
- package/cli/dialogInternalCommandEntries.ts +4 -0
- package/cli/machineWsRunDispatch.ts +29 -37
- package/cli/scriptCommandEntries.ts +0 -2
- package/cli/tui/readlineWorkspace.ts +69 -0
- package/cli/tui/session.ts +176 -1
- package/cliEnvHelpers.ts +11 -7
- package/client/agentConfigResolver.test.ts +2 -3
- package/client/agentRun.test.ts +185 -1
- package/client/agentRun.ts +123 -24
- package/client/localRuntimeAdapter.test.ts +90 -25
- package/client/localRuntimeAdapter.ts +87 -59
- package/connectorRunArtifact.ts +0 -1
- package/database/server/db.ts +99 -0
- package/database/server/ensureDbOpen.ts +12 -0
- package/database/server/memoryAuthorityStore.ts +133 -0
- package/database/server/serverStoreFactory.ts +118 -0
- package/dialogCommands.ts +572 -0
- package/dialogInternalCommandEntries.ts +4 -0
- package/machineWsRunDispatch.ts +29 -37
- package/package.json +10 -9
- package/scriptCommandEntries.ts +0 -2
- package/tui/readlineWorkspace.ts +69 -0
- package/tui/session.ts +176 -1
- package/agent-runtime/taskWorkspace.ts +0 -193
- package/agent-runtime/workspaceSession.ts +0 -76
- package/cli/client/taskWorktree.ts +0 -8
- package/cli/client/workspaceSession.ts +0 -11
- package/client/taskWorktree.ts +0 -8
- package/client/workspaceSession.test.ts +0 -57
- package/client/workspaceSession.ts +0 -11
package/client/agentRun.test.ts
CHANGED
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
findServerPlatformTools,
|
|
7
7
|
runAgentTurn,
|
|
8
8
|
} from "./agentRun";
|
|
9
|
-
import {
|
|
9
|
+
import { BUILTIN_NOLO_AGENT_KEY } from "./localRuntimeAdapter";
|
|
10
|
+
import { MIMO_MONTH_AGENT_KEY, NOLO_PROJECT_MANAGER_AGENT_KEY } from "../agentAliases";
|
|
10
11
|
|
|
11
12
|
class CaptureOutput extends Writable {
|
|
12
13
|
chunks: string[] = [];
|
|
@@ -93,6 +94,34 @@ describe("cli agent run client", () => {
|
|
|
93
94
|
expect(output.text()).not.toContain("tokens=");
|
|
94
95
|
});
|
|
95
96
|
|
|
97
|
+
test("prints agent run error details from failed HTTP responses", async () => {
|
|
98
|
+
const output = new CaptureOutput();
|
|
99
|
+
|
|
100
|
+
const result = await runAgentTurn({
|
|
101
|
+
agentName: "win-codex",
|
|
102
|
+
agentKey: "agent-win-codex",
|
|
103
|
+
serverUrl: "https://nolo.chat",
|
|
104
|
+
message: "restart connector",
|
|
105
|
+
scriptDir: "C:/missing/scripts",
|
|
106
|
+
env: { AUTH_TOKEN: "token-123" },
|
|
107
|
+
output,
|
|
108
|
+
fetchImpl: async () =>
|
|
109
|
+
Response.json({
|
|
110
|
+
error: "Connector execution failed",
|
|
111
|
+
reason: "connector_execution_failed",
|
|
112
|
+
code: "connector_disconnected_mid_run",
|
|
113
|
+
message: "Connector disconnected for machine: machine-win",
|
|
114
|
+
}, { status: 502 }),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(result).toEqual({ exitCode: 1 });
|
|
118
|
+
expect(output.text()).toContain("[nolo] Agent request failed: HTTP 502");
|
|
119
|
+
expect(output.text()).toContain("Connector execution failed");
|
|
120
|
+
expect(output.text()).toContain("Connector disconnected for machine: machine-win");
|
|
121
|
+
expect(output.text()).toContain("code=connector_disconnected_mid_run");
|
|
122
|
+
expect(output.text()).toContain("reason=connector_execution_failed");
|
|
123
|
+
});
|
|
124
|
+
|
|
96
125
|
test("prepends task-run context for HTTP agent runs", async () => {
|
|
97
126
|
const output = new CaptureOutput();
|
|
98
127
|
const requests: Array<{ body: any }> = [];
|
|
@@ -791,6 +820,161 @@ describe("cli agent run client", () => {
|
|
|
791
820
|
expect(output.text()).toContain("pm > server ok");
|
|
792
821
|
});
|
|
793
822
|
|
|
823
|
+
test("auto mode does not skip monthly Mimo when a local shell cwd is already bound", async () => {
|
|
824
|
+
const output = new CaptureOutput();
|
|
825
|
+
const httpCalls: string[] = [];
|
|
826
|
+
let providerCalled = false;
|
|
827
|
+
|
|
828
|
+
const result = await runAgentTurn({
|
|
829
|
+
agentName: MIMO_MONTH_AGENT_KEY,
|
|
830
|
+
agentKey: MIMO_MONTH_AGENT_KEY,
|
|
831
|
+
serverUrl: "https://us.nolo.chat",
|
|
832
|
+
message: "fix tests and commit",
|
|
833
|
+
scriptDir: "C:/missing/scripts",
|
|
834
|
+
env: {
|
|
835
|
+
AUTH_TOKEN: "token-123",
|
|
836
|
+
NOLO_LOCAL_SHELL_MODE: "worktree",
|
|
837
|
+
},
|
|
838
|
+
output,
|
|
839
|
+
runtimeMode: "auto",
|
|
840
|
+
localRuntimeCwd: "/repo/.worktrees/nolo-agent-mimo",
|
|
841
|
+
localRuntimeAdapter: {
|
|
842
|
+
host: "cli",
|
|
843
|
+
capabilities: ["leveldb-agent-config", "local-provider", "local-tools"],
|
|
844
|
+
loadAgentConfig: async (agentRef) => ({
|
|
845
|
+
key: agentRef,
|
|
846
|
+
name: "包月 Mimo",
|
|
847
|
+
prompt: "Implement backend tasks.",
|
|
848
|
+
provider: "custom",
|
|
849
|
+
model: "mimo-v2.5-pro",
|
|
850
|
+
toolNames: ["queryTableRows"],
|
|
851
|
+
}),
|
|
852
|
+
loadDialogHistory: async () => [],
|
|
853
|
+
saveTurn: async () => ({ dialogId: "dialog-mimo-local" }),
|
|
854
|
+
resolveProvider: async () => ({
|
|
855
|
+
model: "mimo-v2.5-pro",
|
|
856
|
+
complete: async () => {
|
|
857
|
+
providerCalled = true;
|
|
858
|
+
return { content: "local mimo ok", model: "mimo-v2.5-pro" };
|
|
859
|
+
},
|
|
860
|
+
}),
|
|
861
|
+
executeTool: async () => ({ content: "ok" }),
|
|
862
|
+
},
|
|
863
|
+
fetchImpl: async (url) => {
|
|
864
|
+
httpCalls.push(String(url));
|
|
865
|
+
return Response.json({ content: "server" });
|
|
866
|
+
},
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
expect(providerCalled).toBe(true);
|
|
870
|
+
expect(result).toEqual({ exitCode: 0, dialogId: "dialog-mimo-local" });
|
|
871
|
+
expect(httpCalls).toEqual([]);
|
|
872
|
+
expect(output.text()).not.toContain("skipping local runtime");
|
|
873
|
+
expect(output.text()).toContain(`${MIMO_MONTH_AGENT_KEY} -> working locally`);
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
test("auto mode skips local runtime for machine-bound localhost custom providers", async () => {
|
|
877
|
+
const output = new CaptureOutput();
|
|
878
|
+
const httpCalls: Array<{ url: string; body: any }> = [];
|
|
879
|
+
|
|
880
|
+
const result = await runAgentTurn({
|
|
881
|
+
agentName: "win-qwen",
|
|
882
|
+
agentKey: "agent-win-qwen",
|
|
883
|
+
serverUrl: "https://nolo.chat",
|
|
884
|
+
message: "run on the bound Windows machine",
|
|
885
|
+
scriptDir: "C:/missing/scripts",
|
|
886
|
+
env: { AUTH_TOKEN: "token-123", OPENAI_API_KEY: "local-provider-present" },
|
|
887
|
+
output,
|
|
888
|
+
runtimeMode: "auto",
|
|
889
|
+
localRuntimeAdapter: {
|
|
890
|
+
host: "cli",
|
|
891
|
+
capabilities: ["leveldb-agent-config", "local-provider", "local-tools"],
|
|
892
|
+
loadAgentConfig: async (agentRef) => ({
|
|
893
|
+
key: agentRef,
|
|
894
|
+
name: "Win Qwen",
|
|
895
|
+
prompt: "Windows local model",
|
|
896
|
+
provider: "custom",
|
|
897
|
+
model: "qwen",
|
|
898
|
+
customProviderUrl: "http://127.0.0.1:8080/v1/chat/completions",
|
|
899
|
+
runtimeBinding: {
|
|
900
|
+
machineId: "machine-win",
|
|
901
|
+
ownerUserId: "user-1",
|
|
902
|
+
connectorSurface: "cli",
|
|
903
|
+
},
|
|
904
|
+
}),
|
|
905
|
+
loadDialogHistory: async () => [],
|
|
906
|
+
saveTurn: async () => {
|
|
907
|
+
throw new Error("local runtime should be skipped");
|
|
908
|
+
},
|
|
909
|
+
resolveProvider: async () => {
|
|
910
|
+
throw new Error("local provider should be skipped");
|
|
911
|
+
},
|
|
912
|
+
executeTool: async () => {
|
|
913
|
+
throw new Error("local tools should be skipped");
|
|
914
|
+
},
|
|
915
|
+
},
|
|
916
|
+
fetchImpl: async (url, init) => {
|
|
917
|
+
httpCalls.push({ url: String(url), body: JSON.parse(String(init?.body)) });
|
|
918
|
+
return Response.json({ content: "server connector ok", dialogId: "dialog-win" });
|
|
919
|
+
},
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
expect(result).toEqual({ exitCode: 0, dialogId: "dialog-win" });
|
|
923
|
+
expect(httpCalls).toHaveLength(1);
|
|
924
|
+
expect(httpCalls[0]?.body.agentKey).toBe("agent-win-qwen");
|
|
925
|
+
expect(output.text()).toContain("auto runtime: skipping local runtime");
|
|
926
|
+
expect(output.text()).toContain("machine-bound localhost custom provider");
|
|
927
|
+
expect(output.text()).not.toContain("win-qwen -> working locally");
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
test("auto mode keeps builtin nolo on local runtime so CLI workspace tools are available", async () => {
|
|
931
|
+
const output = new CaptureOutput();
|
|
932
|
+
const httpCalls: string[] = [];
|
|
933
|
+
let providerCalled = false;
|
|
934
|
+
|
|
935
|
+
const result = await runAgentTurn({
|
|
936
|
+
agentName: "nolo",
|
|
937
|
+
agentKey: BUILTIN_NOLO_AGENT_KEY,
|
|
938
|
+
serverUrl: "https://nolo.chat",
|
|
939
|
+
message: "帮我总结最近 10 个对话",
|
|
940
|
+
scriptDir: "C:/missing/scripts",
|
|
941
|
+
env: { AUTH_TOKEN: "token-123" },
|
|
942
|
+
output,
|
|
943
|
+
runtimeMode: "auto",
|
|
944
|
+
localRuntimeAdapter: {
|
|
945
|
+
host: "cli",
|
|
946
|
+
capabilities: ["leveldb-agent-config", "local-provider", "local-tools"],
|
|
947
|
+
loadAgentConfig: async () => ({
|
|
948
|
+
key: BUILTIN_NOLO_AGENT_KEY,
|
|
949
|
+
name: "nolo",
|
|
950
|
+
prompt: "Use workspace tools.",
|
|
951
|
+
model: "fake-local",
|
|
952
|
+
toolNames: ["queryTableRows", "addTableRow", "updateTableRow"],
|
|
953
|
+
}),
|
|
954
|
+
loadDialogHistory: async () => [],
|
|
955
|
+
saveTurn: async () => ({ dialogId: "dialog-builtin-nolo-local" }),
|
|
956
|
+
resolveProvider: async () => ({
|
|
957
|
+
model: "fake-local",
|
|
958
|
+
complete: async () => {
|
|
959
|
+
providerCalled = true;
|
|
960
|
+
return { content: "local nolo with tools", model: "fake-local" };
|
|
961
|
+
},
|
|
962
|
+
}),
|
|
963
|
+
executeTool: async () => ({ content: "[]" }),
|
|
964
|
+
},
|
|
965
|
+
fetchImpl: async (url) => {
|
|
966
|
+
httpCalls.push(String(url));
|
|
967
|
+
return Response.json({ content: "server" });
|
|
968
|
+
},
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
expect(providerCalled).toBe(true);
|
|
972
|
+
expect(result).toEqual({ exitCode: 0, dialogId: "dialog-builtin-nolo-local" });
|
|
973
|
+
expect(httpCalls).toEqual([]);
|
|
974
|
+
expect(output.text()).not.toContain("skipping local runtime");
|
|
975
|
+
expect(output.text()).toContain("nolo -> working locally");
|
|
976
|
+
});
|
|
977
|
+
|
|
794
978
|
test("auto mode skips local runtime for server platform tools declared by runtime policy", async () => {
|
|
795
979
|
const output = new CaptureOutput();
|
|
796
980
|
const httpCalls: Array<{ url: string; body: any }> = [];
|
package/client/agentRun.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "../agentAliases";
|
|
10
10
|
import type { LocalAgentToolEvent } from "../agent-runtime/localLoop";
|
|
11
11
|
import type { AgentRuntimeHostAdapter, AgentRuntimeRequestedMode } from "../agentRuntimeLocal";
|
|
12
|
-
import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
|
|
12
|
+
import { createCliLocalRuntimeAdapter, isBuiltinNoloAgentRef } from "./localRuntimeAdapter";
|
|
13
13
|
import { createStreamingTextWriter } from "./streamingOutput";
|
|
14
14
|
import { prependTaskRunPrompt, type TaskRunPromptContext } from "./taskRunPrompt";
|
|
15
15
|
|
|
@@ -73,6 +73,47 @@ export type RunAgentTurnResult = {
|
|
|
73
73
|
streamInterrupted?: boolean;
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
+
class Spinner {
|
|
77
|
+
private timer: any = null;
|
|
78
|
+
private frameIndex = 0;
|
|
79
|
+
private frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
80
|
+
private isTTY: boolean;
|
|
81
|
+
|
|
82
|
+
constructor(
|
|
83
|
+
private output: OutputLike,
|
|
84
|
+
private text: string
|
|
85
|
+
) {
|
|
86
|
+
this.isTTY = Boolean((output as any).isTTY ?? process.stdout.isTTY);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
start() {
|
|
90
|
+
if (!this.isTTY) {
|
|
91
|
+
this.output.write(`\n${this.text}...\n`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.frameIndex = 0;
|
|
96
|
+
this.output.write(`\x1b[36m${this.frames[0]}\x1b[39m ${this.text}...`);
|
|
97
|
+
|
|
98
|
+
this.timer = setInterval(() => {
|
|
99
|
+
this.frameIndex = (this.frameIndex + 1) % this.frames.length;
|
|
100
|
+
this.output.write(`\r\x1b[36m${this.frames[this.frameIndex]}\x1b[39m ${this.text}...`);
|
|
101
|
+
}, 80);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
stop() {
|
|
105
|
+
if (this.timer) {
|
|
106
|
+
clearInterval(this.timer);
|
|
107
|
+
this.timer = null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (this.isTTY) {
|
|
111
|
+
this.output.write("\r\x1b[K");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
76
117
|
const SERVER_PLATFORM_TOOL_NAMES = new Set([
|
|
77
118
|
"addTableRow",
|
|
78
119
|
"addTableRows",
|
|
@@ -139,10 +180,27 @@ function isKnownServerPlatformAgent(options: RunAgentTurnOptions) {
|
|
|
139
180
|
const normalizedKey = normalizeAgentRef(options.agentKey);
|
|
140
181
|
return Boolean(normalizedKey && KNOWN_SERVER_PLATFORM_AGENT_ALIASES.has(normalizedKey));
|
|
141
182
|
}
|
|
142
|
-
|
|
143
|
-
function
|
|
144
|
-
|
|
145
|
-
|
|
183
|
+
|
|
184
|
+
function isMachineBoundLocalhostCustomProvider(agentConfig: any) {
|
|
185
|
+
const machineId =
|
|
186
|
+
agentConfig?.runtimeBinding && typeof agentConfig.runtimeBinding === "object"
|
|
187
|
+
? String(agentConfig.runtimeBinding.machineId ?? "").trim()
|
|
188
|
+
: "";
|
|
189
|
+
const providerUrl = typeof agentConfig?.customProviderUrl === "string"
|
|
190
|
+
? agentConfig.customProviderUrl.trim()
|
|
191
|
+
: "";
|
|
192
|
+
if (!machineId || !providerUrl) return false;
|
|
193
|
+
try {
|
|
194
|
+
const hostname = new URL(providerUrl).hostname.toLowerCase();
|
|
195
|
+
return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1";
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function resolveAuthToken(env: EnvLike) {
|
|
202
|
+
return env.AUTH_TOKEN || env.AUTH || env.BENCHMARK_AUTH_TOKEN || "";
|
|
203
|
+
}
|
|
146
204
|
|
|
147
205
|
function shouldShowUsage(env: EnvLike) {
|
|
148
206
|
return env.NOLO_DEBUG === "1" || env.NOLO_SHOW_USAGE === "1";
|
|
@@ -173,6 +231,10 @@ function resolveLocalRuntimeAdapter(options: RunAgentTurnOptions) {
|
|
|
173
231
|
}
|
|
174
232
|
|
|
175
233
|
async function shouldSkipAutoLocalForServerPlatformTools(options: RunAgentTurnOptions) {
|
|
234
|
+
if (isBuiltinNoloAgentRef(options.agentKey)) return false;
|
|
235
|
+
if (options.localRuntimeCwd && options.env.NOLO_LOCAL_SHELL_MODE === "worktree") {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
176
238
|
if (isKnownServerPlatformAgent(options)) {
|
|
177
239
|
options.output.write(
|
|
178
240
|
`[nolo] auto runtime: skipping local runtime because ${options.agentKey} is a known platform agent. ` +
|
|
@@ -188,6 +250,13 @@ async function shouldSkipAutoLocalForServerPlatformTools(options: RunAgentTurnOp
|
|
|
188
250
|
} catch {
|
|
189
251
|
return false;
|
|
190
252
|
}
|
|
253
|
+
if (isMachineBoundLocalhostCustomProvider(agentConfig)) {
|
|
254
|
+
options.output.write(
|
|
255
|
+
`[nolo] auto runtime: skipping local runtime because ${options.agentKey} is a machine-bound localhost custom provider. ` +
|
|
256
|
+
"Use --local explicitly to force the current machine.\n"
|
|
257
|
+
);
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
191
260
|
const serverTools = resolveServerPlatformToolNames(agentConfig);
|
|
192
261
|
if (serverTools.length === 0) return false;
|
|
193
262
|
options.output.write(
|
|
@@ -288,6 +357,13 @@ function formatToolTraceEvent(event: LocalAgentToolEvent) {
|
|
|
288
357
|
|
|
289
358
|
function shouldAttemptAutoLocal(options: RunAgentTurnOptions) {
|
|
290
359
|
if (options.localRuntimeAdapter || options.localRuntimeAdapterFactory) return true;
|
|
360
|
+
if (
|
|
361
|
+
options.env.NOLO_DISABLE_CLI_WORKSPACE_TOOLS !== "1" &&
|
|
362
|
+
isBuiltinNoloAgentRef(options.agentKey) &&
|
|
363
|
+
resolveAuthToken(options.env)
|
|
364
|
+
) {
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
291
367
|
return Boolean(
|
|
292
368
|
options.env.NOLO_LOCAL_OPENAI_API_KEY ||
|
|
293
369
|
options.env.OPENAI_API_KEY ||
|
|
@@ -502,7 +578,8 @@ function buildTransportErrorHint(serverUrl: string, error: unknown) {
|
|
|
502
578
|
}
|
|
503
579
|
|
|
504
580
|
async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string) {
|
|
505
|
-
options.output
|
|
581
|
+
const spinner = new Spinner(options.output, `${options.agentName} -> working`);
|
|
582
|
+
spinner.start();
|
|
506
583
|
|
|
507
584
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
508
585
|
let res: Response;
|
|
@@ -556,14 +633,16 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
|
|
|
556
633
|
...(typeof options.timeoutMs === "number" ? { timeoutMs: options.timeoutMs } : {}),
|
|
557
634
|
stream: !options.noStream && !options.background,
|
|
558
635
|
}),
|
|
559
|
-
});
|
|
560
|
-
} catch (error) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
636
|
+
});
|
|
637
|
+
} catch (error) {
|
|
638
|
+
spinner.stop();
|
|
639
|
+
options.output.write(buildTransportErrorHint(options.serverUrl, error));
|
|
640
|
+
return { exitCode: 1 };
|
|
641
|
+
}
|
|
642
|
+
|
|
565
643
|
const contentType = res.headers.get("content-type") || "";
|
|
566
644
|
if (contentType.includes("text/event-stream") && res.body) {
|
|
645
|
+
spinner.stop();
|
|
567
646
|
const result = await readStreamingAgentRun(options, res);
|
|
568
647
|
await recordTaskRunOutcome({
|
|
569
648
|
options,
|
|
@@ -577,18 +656,32 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
|
|
|
577
656
|
});
|
|
578
657
|
return result;
|
|
579
658
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
659
|
+
|
|
660
|
+
spinner.stop();
|
|
661
|
+
let data: any = {};
|
|
662
|
+
try {
|
|
663
|
+
data = await res.json();
|
|
664
|
+
} catch {
|
|
665
|
+
data = {};
|
|
666
|
+
}
|
|
667
|
+
|
|
588
668
|
if (!res.ok) {
|
|
589
669
|
options.output.write(`[nolo] Agent request failed: HTTP ${res.status}\n`);
|
|
590
|
-
|
|
591
|
-
|
|
670
|
+
const errorText = typeof data?.error === "string" ? data.error.trim() : "";
|
|
671
|
+
const messageText = typeof data?.message === "string" ? data.message.trim() : "";
|
|
672
|
+
const reasonText = typeof data?.reason === "string" ? data.reason.trim() : "";
|
|
673
|
+
const codeText = typeof data?.code === "string" ? data.code.trim() : "";
|
|
674
|
+
if (errorText || messageText) {
|
|
675
|
+
options.output.write(`${errorText || messageText}\n`);
|
|
676
|
+
if (messageText && messageText !== errorText) {
|
|
677
|
+
options.output.write(`${messageText}\n`);
|
|
678
|
+
}
|
|
679
|
+
if (codeText && codeText !== errorText && codeText !== messageText) {
|
|
680
|
+
options.output.write(`code=${codeText}\n`);
|
|
681
|
+
}
|
|
682
|
+
if (reasonText && reasonText !== errorText && reasonText !== messageText) {
|
|
683
|
+
options.output.write(`reason=${reasonText}\n`);
|
|
684
|
+
}
|
|
592
685
|
}
|
|
593
686
|
await recordTaskRunOutcome({
|
|
594
687
|
options,
|
|
@@ -691,7 +784,8 @@ async function runLocalAgentTurnForCli(
|
|
|
691
784
|
return { exitCode: 1 };
|
|
692
785
|
}
|
|
693
786
|
|
|
694
|
-
options.output
|
|
787
|
+
const spinner = new Spinner(options.output, `${options.agentName} -> working locally`);
|
|
788
|
+
spinner.start();
|
|
695
789
|
const toolEvents: LocalAgentToolEvent[] = [];
|
|
696
790
|
try {
|
|
697
791
|
const traceLocalTools = shouldTraceLocalTools(options);
|
|
@@ -716,11 +810,15 @@ async function runLocalAgentTurnForCli(
|
|
|
716
810
|
? {
|
|
717
811
|
onToolEvent: (event) => {
|
|
718
812
|
if (collectToolEvidence) toolEvents.push(event);
|
|
719
|
-
if (traceLocalTools)
|
|
813
|
+
if (traceLocalTools) {
|
|
814
|
+
spinner.stop();
|
|
815
|
+
options.output.write(formatToolTraceEvent(event));
|
|
816
|
+
}
|
|
720
817
|
},
|
|
721
818
|
}
|
|
722
819
|
: {}),
|
|
723
820
|
});
|
|
821
|
+
spinner.stop();
|
|
724
822
|
const content = result.content.trim();
|
|
725
823
|
if (content) {
|
|
726
824
|
options.output.write(`\n${options.agentName} > ${content}\n`);
|
|
@@ -736,6 +834,7 @@ async function runLocalAgentTurnForCli(
|
|
|
736
834
|
});
|
|
737
835
|
return { exitCode: 0, dialogId: result.dialogId };
|
|
738
836
|
} catch (error) {
|
|
837
|
+
spinner.stop();
|
|
739
838
|
if (settings.reportFailure) {
|
|
740
839
|
options.output.write(
|
|
741
840
|
`[nolo] Local agent run failed: ${
|
|
@@ -1043,17 +1043,20 @@ describe("CLI local runtime adapter", () => {
|
|
|
1043
1043
|
});
|
|
1044
1044
|
});
|
|
1045
1045
|
|
|
1046
|
-
test("
|
|
1046
|
+
test("requires explicit shell mode for registered execShell", async () => {
|
|
1047
1047
|
const adapter = createCliLocalRuntimeAdapter({
|
|
1048
1048
|
env: {},
|
|
1049
|
+
localToolExecutors: {
|
|
1050
|
+
execShell: async (call) => ({ content: `shell:${call.arguments}` }),
|
|
1051
|
+
},
|
|
1049
1052
|
fetchImpl: async () => Response.json({}),
|
|
1050
1053
|
});
|
|
1051
1054
|
|
|
1052
1055
|
await expect(adapter.executeTool({
|
|
1053
1056
|
id: "call-1",
|
|
1054
1057
|
name: "execShell",
|
|
1055
|
-
arguments: "{}",
|
|
1056
|
-
})).rejects.toThrow("execShell requires NOLO_LOCAL_SHELL_MODE");
|
|
1058
|
+
arguments: "{\"cmd\":\"pwd\"}",
|
|
1059
|
+
})).rejects.toThrow("execShell requires NOLO_LOCAL_SHELL_MODE=worktree");
|
|
1057
1060
|
});
|
|
1058
1061
|
|
|
1059
1062
|
test("executes explicitly allowed registered local tools declared by the agent", async () => {
|
|
@@ -1095,7 +1098,7 @@ describe("CLI local runtime adapter", () => {
|
|
|
1095
1098
|
expect(result.content).toContain("README.md");
|
|
1096
1099
|
});
|
|
1097
1100
|
|
|
1098
|
-
test("advertises execShell to OpenAI-compatible providers
|
|
1101
|
+
test("advertises execShell to OpenAI-compatible providers only in explicit shell mode", async () => {
|
|
1099
1102
|
const requests: Array<{ body: any }> = [];
|
|
1100
1103
|
const adapter = createCliLocalRuntimeAdapter({
|
|
1101
1104
|
env: {
|
|
@@ -1248,46 +1251,108 @@ describe("CLI local runtime adapter", () => {
|
|
|
1248
1251
|
expect(result.metadata).toMatchObject({ serverPlatformTool: true });
|
|
1249
1252
|
});
|
|
1250
1253
|
|
|
1251
|
-
test("
|
|
1252
|
-
const
|
|
1253
|
-
const
|
|
1254
|
+
test("adds typed CLI workspace tools to the default nolo local agent request", async () => {
|
|
1255
|
+
const requests: Array<{ body: any }> = [];
|
|
1256
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
1257
|
+
env: {
|
|
1258
|
+
OPENAI_API_KEY: "sk-local",
|
|
1259
|
+
NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
|
|
1260
|
+
},
|
|
1261
|
+
db: {
|
|
1262
|
+
get: async () => ({
|
|
1263
|
+
dbKey: "agent-pub-01NOLOAPPBLD000000019KCKT0",
|
|
1264
|
+
id: "01NOLOAPPBLD000000019KCKT0",
|
|
1265
|
+
name: "nolo",
|
|
1266
|
+
prompt: "Route through typed tools.",
|
|
1267
|
+
model: "gpt-4.1-mini",
|
|
1268
|
+
tools: ["fetchWebpage"],
|
|
1269
|
+
}),
|
|
1270
|
+
put: async () => {},
|
|
1271
|
+
batch: async () => {},
|
|
1272
|
+
iterator: () => (async function* () {})(),
|
|
1273
|
+
},
|
|
1274
|
+
fetchImpl: async (_url, init) => {
|
|
1275
|
+
requests.push({ body: JSON.parse(String(init?.body)) });
|
|
1276
|
+
return Response.json({
|
|
1277
|
+
choices: [{ message: { content: "done" } }],
|
|
1278
|
+
});
|
|
1279
|
+
},
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
await runLocalAgentTurn({
|
|
1283
|
+
adapter,
|
|
1284
|
+
agentRef: "agent-pub-01NOLOAPPBLD000000019KCKT0",
|
|
1285
|
+
input: "帮我总结最近 10 个对话",
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
const toolNames = toolNamesFromRequest(requests[0]);
|
|
1289
|
+
expect(toolNames).toContain("listDialogs");
|
|
1290
|
+
expect(toolNames).toContain("readDialog");
|
|
1291
|
+
expect(toolNames).toContain("listAgents");
|
|
1292
|
+
expect(toolNames).toContain("readSpace");
|
|
1293
|
+
expect(toolNames).toContain("queryTableRows");
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
test("executes typed CLI workspace tools through whitelisted nolo commands", async () => {
|
|
1297
|
+
const spawnCalls: Array<{ cmd: string[]; env: NodeJS.ProcessEnv }> = [];
|
|
1298
|
+
const originalSpawn = Bun.spawn;
|
|
1299
|
+
Bun.spawn = ((options: { cmd: string[]; env: NodeJS.ProcessEnv }) => {
|
|
1300
|
+
spawnCalls.push(options);
|
|
1301
|
+
return {
|
|
1302
|
+
stdout: new ReadableStream({
|
|
1303
|
+
start(controller) {
|
|
1304
|
+
controller.enqueue(new TextEncoder().encode("dialog output\n"));
|
|
1305
|
+
controller.close();
|
|
1306
|
+
},
|
|
1307
|
+
}),
|
|
1308
|
+
stderr: new ReadableStream({
|
|
1309
|
+
start(controller) {
|
|
1310
|
+
controller.close();
|
|
1311
|
+
},
|
|
1312
|
+
}),
|
|
1313
|
+
exited: Promise.resolve(0),
|
|
1314
|
+
};
|
|
1315
|
+
}) as unknown as typeof Bun.spawn;
|
|
1316
|
+
|
|
1254
1317
|
try {
|
|
1255
|
-
await Bun.write(join(taskWorktreeRoot, "README.md"), "worktree ok\n");
|
|
1256
1318
|
const adapter = createCliLocalRuntimeAdapter({
|
|
1257
1319
|
env: {
|
|
1258
1320
|
NOLO_LOCAL_USER_ID: "user-1",
|
|
1321
|
+
AUTH_TOKEN: "token-1",
|
|
1259
1322
|
},
|
|
1260
|
-
output: { write: (chunk) => chunks.push(chunk) },
|
|
1261
1323
|
db: {
|
|
1262
1324
|
get: async () => ({
|
|
1263
|
-
dbKey: "agent-
|
|
1264
|
-
id: "
|
|
1265
|
-
prompt: "Use local workspace tools.",
|
|
1266
|
-
runtimeBinding: { localWorkspaceMode: "task-worktree" },
|
|
1325
|
+
dbKey: "agent-pub-01NOLOAPPBLD000000019KCKT0",
|
|
1326
|
+
id: "01NOLOAPPBLD000000019KCKT0",
|
|
1267
1327
|
}),
|
|
1268
1328
|
put: async () => {},
|
|
1269
1329
|
batch: async () => {},
|
|
1270
1330
|
iterator: () => (async function* () {})(),
|
|
1271
1331
|
},
|
|
1272
|
-
prepareTaskWorktree: async ({ agentKey }) => ({
|
|
1273
|
-
path: taskWorktreeRoot,
|
|
1274
|
-
branchName: `nolo-agent-${agentKey}`,
|
|
1275
|
-
}),
|
|
1276
1332
|
fetchImpl: async () => Response.json({}),
|
|
1277
1333
|
});
|
|
1278
1334
|
|
|
1279
|
-
await adapter.loadAgentConfig("
|
|
1335
|
+
await adapter.loadAgentConfig("agent-pub-01NOLOAPPBLD000000019KCKT0");
|
|
1280
1336
|
const result = await adapter.executeTool({
|
|
1281
|
-
id: "call-
|
|
1282
|
-
name: "
|
|
1283
|
-
arguments: JSON.stringify({
|
|
1337
|
+
id: "call-list-dialogs",
|
|
1338
|
+
name: "listDialogs",
|
|
1339
|
+
arguments: JSON.stringify({ limit: 3 }),
|
|
1284
1340
|
});
|
|
1285
1341
|
|
|
1286
|
-
expect(result.content).toBe("
|
|
1287
|
-
expect(
|
|
1288
|
-
|
|
1342
|
+
expect(result.content).toBe("dialog output\n");
|
|
1343
|
+
expect(result.metadata).toMatchObject({
|
|
1344
|
+
cliWorkspaceTool: true,
|
|
1345
|
+
exitCode: 0,
|
|
1346
|
+
});
|
|
1347
|
+
expect(spawnCalls[0]?.cmd.at(-5)?.endsWith("packages/cli/index.ts")).toBe(true);
|
|
1348
|
+
expect(spawnCalls[0]?.cmd.slice(-4)).toEqual([
|
|
1349
|
+
"dialog",
|
|
1350
|
+
"list",
|
|
1351
|
+
"--limit",
|
|
1352
|
+
"3",
|
|
1353
|
+
]);
|
|
1289
1354
|
} finally {
|
|
1290
|
-
|
|
1355
|
+
Bun.spawn = originalSpawn;
|
|
1291
1356
|
}
|
|
1292
1357
|
});
|
|
1293
1358
|
|