nolo-cli 0.1.38 → 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 +55 -7
- 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 +55 -7
- 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
|
|
|
@@ -180,10 +180,27 @@ function isKnownServerPlatformAgent(options: RunAgentTurnOptions) {
|
|
|
180
180
|
const normalizedKey = normalizeAgentRef(options.agentKey);
|
|
181
181
|
return Boolean(normalizedKey && KNOWN_SERVER_PLATFORM_AGENT_ALIASES.has(normalizedKey));
|
|
182
182
|
}
|
|
183
|
-
|
|
184
|
-
function
|
|
185
|
-
|
|
186
|
-
|
|
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
|
+
}
|
|
187
204
|
|
|
188
205
|
function shouldShowUsage(env: EnvLike) {
|
|
189
206
|
return env.NOLO_DEBUG === "1" || env.NOLO_SHOW_USAGE === "1";
|
|
@@ -214,6 +231,10 @@ function resolveLocalRuntimeAdapter(options: RunAgentTurnOptions) {
|
|
|
214
231
|
}
|
|
215
232
|
|
|
216
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
|
+
}
|
|
217
238
|
if (isKnownServerPlatformAgent(options)) {
|
|
218
239
|
options.output.write(
|
|
219
240
|
`[nolo] auto runtime: skipping local runtime because ${options.agentKey} is a known platform agent. ` +
|
|
@@ -229,6 +250,13 @@ async function shouldSkipAutoLocalForServerPlatformTools(options: RunAgentTurnOp
|
|
|
229
250
|
} catch {
|
|
230
251
|
return false;
|
|
231
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
|
+
}
|
|
232
260
|
const serverTools = resolveServerPlatformToolNames(agentConfig);
|
|
233
261
|
if (serverTools.length === 0) return false;
|
|
234
262
|
options.output.write(
|
|
@@ -329,6 +357,13 @@ function formatToolTraceEvent(event: LocalAgentToolEvent) {
|
|
|
329
357
|
|
|
330
358
|
function shouldAttemptAutoLocal(options: RunAgentTurnOptions) {
|
|
331
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
|
+
}
|
|
332
367
|
return Boolean(
|
|
333
368
|
options.env.NOLO_LOCAL_OPENAI_API_KEY ||
|
|
334
369
|
options.env.OPENAI_API_KEY ||
|
|
@@ -632,8 +667,21 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
|
|
|
632
667
|
|
|
633
668
|
if (!res.ok) {
|
|
634
669
|
options.output.write(`[nolo] Agent request failed: HTTP ${res.status}\n`);
|
|
635
|
-
|
|
636
|
-
|
|
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
|
+
}
|
|
637
685
|
}
|
|
638
686
|
await recordTaskRunOutcome({
|
|
639
687
|
options,
|
|
@@ -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
|
|