nolo-cli 0.1.36 → 0.1.38
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 +11 -1
- package/agent-runtime/agentRecordConfig.ts +5 -1
- package/agent-runtime/index.ts +3 -0
- package/agent-runtime/localLoop.ts +1 -2
- package/agent-runtime/localToolPolicy.ts +8 -5
- package/agent-runtime/localWorkspaceTools.ts +68 -180
- package/agent-runtime/runtimeToolPolicy.ts +46 -6
- package/agent-runtime/taskWorkspace.ts +23 -23
- package/agent-runtime/types.ts +0 -1
- package/agentAliases.ts +50 -30
- package/agentRunCommand.ts +1 -1
- package/agentRuntimeLocal.ts +3 -0
- package/cli/agentAliases.ts +50 -30
- package/cli/agentRunCommand.ts +1 -1
- package/cli/agentRuntimeLocal.ts +3 -0
- package/cli/client/agentRun.ts +83 -105
- package/cli/client/localRuntimeAdapter.ts +22 -1
- package/cli/machineWsRunDispatch.ts +3 -5
- package/client/agentRun.test.ts +2 -32
- package/client/agentRun.ts +83 -105
- package/client/localRuntimeAdapter.test.ts +64 -0
- package/client/localRuntimeAdapter.ts +22 -1
- package/machineWsRunDispatch.ts +3 -5
- package/package.json +3 -3
package/client/agentRun.test.ts
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
classifyTaskRunReviewStatus,
|
|
6
6
|
findServerPlatformTools,
|
|
7
7
|
runAgentTurn,
|
|
8
|
-
shouldUseScriptBridge,
|
|
9
8
|
} from "./agentRun";
|
|
10
9
|
import { NOLO_PROJECT_MANAGER_AGENT_KEY } from "../agentAliases";
|
|
11
10
|
|
|
@@ -34,12 +33,6 @@ describe("cli agent run client", () => {
|
|
|
34
33
|
expect(classifyTaskRunReviewStatus("I looked at the diff and have notes")).toBeUndefined();
|
|
35
34
|
});
|
|
36
35
|
|
|
37
|
-
test("uses the repo script bridge only when no auth token is available", () => {
|
|
38
|
-
expect(shouldUseScriptBridge({ hasAuthToken: false, scriptPathExists: true })).toBe(true);
|
|
39
|
-
expect(shouldUseScriptBridge({ hasAuthToken: true, scriptPathExists: true })).toBe(false);
|
|
40
|
-
expect(shouldUseScriptBridge({ hasAuthToken: false, scriptPathExists: false })).toBe(false);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
36
|
test("identifies server platform tools that local runtime cannot provide", () => {
|
|
44
37
|
expect(findServerPlatformTools(["readWorkspaceFile", "queryTableRows", "updateTableRow"])).toEqual([
|
|
45
38
|
"queryTableRows",
|
|
@@ -61,7 +54,6 @@ describe("cli agent run client", () => {
|
|
|
61
54
|
scriptDir: "C:/missing/scripts",
|
|
62
55
|
env: { AUTH_TOKEN: "token-123" },
|
|
63
56
|
output,
|
|
64
|
-
scriptPathExists: () => false,
|
|
65
57
|
fetchImpl: async (url, init) => {
|
|
66
58
|
requests.push({
|
|
67
59
|
url: String(url),
|
|
@@ -120,7 +112,6 @@ describe("cli agent run client", () => {
|
|
|
120
112
|
workItemId: "frontend-filter",
|
|
121
113
|
artifactIds: ["artifact-1"],
|
|
122
114
|
},
|
|
123
|
-
scriptPathExists: () => false,
|
|
124
115
|
fetchImpl: async (_url, init) => {
|
|
125
116
|
requests.push({ body: JSON.parse(String(init?.body)) });
|
|
126
117
|
return Response.json({ content: "ok", dialogId: "dialog-1" });
|
|
@@ -160,7 +151,6 @@ describe("cli agent run client", () => {
|
|
|
160
151
|
taskRunRecorder: async (args) => {
|
|
161
152
|
writebacks.push(args);
|
|
162
153
|
},
|
|
163
|
-
scriptPathExists: () => false,
|
|
164
154
|
fetchImpl: async () => {
|
|
165
155
|
return Response.json({ content: "patched", dialogId: "dialog-1" });
|
|
166
156
|
},
|
|
@@ -193,7 +183,6 @@ describe("cli agent run client", () => {
|
|
|
193
183
|
env: { AUTH_TOKEN: "token-123" },
|
|
194
184
|
output,
|
|
195
185
|
runtimeMode: "server",
|
|
196
|
-
scriptPathExists: () => false,
|
|
197
186
|
fetchImpl: async (_url, init) => {
|
|
198
187
|
requests.push({ body: JSON.parse(String(init?.body)) });
|
|
199
188
|
return Response.json({ content: "ok" });
|
|
@@ -206,7 +195,7 @@ describe("cli agent run client", () => {
|
|
|
206
195
|
]);
|
|
207
196
|
});
|
|
208
197
|
|
|
209
|
-
test("sends
|
|
198
|
+
test("sends CLI dialog metadata over HTTP", async () => {
|
|
210
199
|
const output = new CaptureOutput();
|
|
211
200
|
const requests: Array<{ body: any }> = [];
|
|
212
201
|
|
|
@@ -227,7 +216,6 @@ describe("cli agent run client", () => {
|
|
|
227
216
|
env: { AUTH_TOKEN: "token-123" },
|
|
228
217
|
output,
|
|
229
218
|
runtimeMode: "server",
|
|
230
|
-
scriptPathExists: () => false,
|
|
231
219
|
fetchImpl: async (_url, init) => {
|
|
232
220
|
requests.push({ body: JSON.parse(String(init?.body)) });
|
|
233
221
|
return Response.json({ dialogId: "dialog-1", status: "queued" });
|
|
@@ -271,7 +259,6 @@ describe("cli agent run client", () => {
|
|
|
271
259
|
writebacks.push(args);
|
|
272
260
|
},
|
|
273
261
|
output,
|
|
274
|
-
scriptPathExists: () => false,
|
|
275
262
|
fetchImpl: async () => {
|
|
276
263
|
return Response.json({ dialogId: "dialog-bg", status: "pending" }, { status: 202 });
|
|
277
264
|
},
|
|
@@ -319,7 +306,6 @@ describe("cli agent run client", () => {
|
|
|
319
306
|
throw new Error("no tools expected");
|
|
320
307
|
},
|
|
321
308
|
},
|
|
322
|
-
scriptPathExists: () => false,
|
|
323
309
|
fetchImpl: async () => {
|
|
324
310
|
throw new Error("HTTP should not be called for forced local runs");
|
|
325
311
|
},
|
|
@@ -371,7 +357,6 @@ describe("cli agent run client", () => {
|
|
|
371
357
|
}),
|
|
372
358
|
executeTool: async () => ({ content: "ok" }),
|
|
373
359
|
},
|
|
374
|
-
scriptPathExists: () => false,
|
|
375
360
|
fetchImpl: async () => {
|
|
376
361
|
throw new Error("HTTP should not be called for forced local runs");
|
|
377
362
|
},
|
|
@@ -432,7 +417,6 @@ describe("cli agent run client", () => {
|
|
|
432
417
|
return { content: "ok" };
|
|
433
418
|
},
|
|
434
419
|
},
|
|
435
|
-
scriptPathExists: () => false,
|
|
436
420
|
});
|
|
437
421
|
|
|
438
422
|
expect(result).toEqual({ exitCode: 0, dialogId: "dialog-local-retry" });
|
|
@@ -487,7 +471,6 @@ describe("cli agent run client", () => {
|
|
|
487
471
|
}),
|
|
488
472
|
executeTool: async () => ({ content: "clean" }),
|
|
489
473
|
},
|
|
490
|
-
scriptPathExists: () => false,
|
|
491
474
|
});
|
|
492
475
|
|
|
493
476
|
expect(result).toMatchObject({ exitCode: 0, dialogId: "dialog-local" });
|
|
@@ -548,7 +531,6 @@ describe("cli agent run client", () => {
|
|
|
548
531
|
}),
|
|
549
532
|
executeTool: async () => ({ content: "clean" }),
|
|
550
533
|
},
|
|
551
|
-
scriptPathExists: () => false,
|
|
552
534
|
});
|
|
553
535
|
|
|
554
536
|
expect(result).toMatchObject({ exitCode: 0, dialogId: "dialog-local" });
|
|
@@ -604,7 +586,6 @@ describe("cli agent run client", () => {
|
|
|
604
586
|
throw new Error("no tools expected");
|
|
605
587
|
},
|
|
606
588
|
},
|
|
607
|
-
scriptPathExists: () => false,
|
|
608
589
|
});
|
|
609
590
|
|
|
610
591
|
expect(result).toEqual({ exitCode: 0, dialogId: "dialog-local-bg" });
|
|
@@ -648,7 +629,6 @@ describe("cli agent run client", () => {
|
|
|
648
629
|
throw new Error("no tools expected");
|
|
649
630
|
},
|
|
650
631
|
},
|
|
651
|
-
scriptPathExists: () => false,
|
|
652
632
|
fetchImpl: async (url) => {
|
|
653
633
|
httpCalls.push(String(url));
|
|
654
634
|
return Response.json({ content: "server" });
|
|
@@ -701,7 +681,6 @@ describe("cli agent run client", () => {
|
|
|
701
681
|
throw new Error("no tools expected");
|
|
702
682
|
},
|
|
703
683
|
},
|
|
704
|
-
scriptPathExists: () => false,
|
|
705
684
|
fetchImpl: async (url) => {
|
|
706
685
|
httpCalls.push(String(url));
|
|
707
686
|
return Response.json({ content: "server" });
|
|
@@ -748,7 +727,6 @@ describe("cli agent run client", () => {
|
|
|
748
727
|
throw new Error("no tools expected");
|
|
749
728
|
},
|
|
750
729
|
},
|
|
751
|
-
scriptPathExists: () => false,
|
|
752
730
|
fetchImpl: async (url, init) => {
|
|
753
731
|
httpCalls.push({ url: String(url), body: JSON.parse(String(init?.body)) });
|
|
754
732
|
return Response.json({ content: "server fallback", dialogId: "dialog-server-fallback" });
|
|
@@ -798,7 +776,6 @@ describe("cli agent run client", () => {
|
|
|
798
776
|
throw new Error("local tools should be skipped");
|
|
799
777
|
},
|
|
800
778
|
},
|
|
801
|
-
scriptPathExists: () => false,
|
|
802
779
|
fetchImpl: async (url, init) => {
|
|
803
780
|
httpCalls.push({ url: String(url), body: JSON.parse(String(init?.body)) });
|
|
804
781
|
return Response.json({ content: "server ok", dialogId: "dialog-server" });
|
|
@@ -852,7 +829,6 @@ describe("cli agent run client", () => {
|
|
|
852
829
|
throw new Error("local tools should be skipped");
|
|
853
830
|
},
|
|
854
831
|
},
|
|
855
|
-
scriptPathExists: () => false,
|
|
856
832
|
fetchImpl: async (url, init) => {
|
|
857
833
|
httpCalls.push({ url: String(url), body: JSON.parse(String(init?.body)) });
|
|
858
834
|
return Response.json({ content: "server ok", dialogId: "dialog-server-policy" });
|
|
@@ -898,7 +874,6 @@ describe("cli agent run client", () => {
|
|
|
898
874
|
throw new Error("local tools should be skipped");
|
|
899
875
|
},
|
|
900
876
|
},
|
|
901
|
-
scriptPathExists: () => false,
|
|
902
877
|
fetchImpl: async (url, init) => {
|
|
903
878
|
httpCalls.push({ url: String(url), body: JSON.parse(String(init?.body)) });
|
|
904
879
|
return Response.json({ content: "server ok", dialogId: "dialog-server" });
|
|
@@ -942,7 +917,6 @@ describe("cli agent run client", () => {
|
|
|
942
917
|
},
|
|
943
918
|
};
|
|
944
919
|
},
|
|
945
|
-
scriptPathExists: () => false,
|
|
946
920
|
fetchImpl: async () => {
|
|
947
921
|
throw new Error("HTTP should not be called for env local runs");
|
|
948
922
|
},
|
|
@@ -964,7 +938,6 @@ describe("cli agent run client", () => {
|
|
|
964
938
|
scriptDir: "C:/missing/scripts",
|
|
965
939
|
env: { AUTH_TOKEN: "token-123" },
|
|
966
940
|
output,
|
|
967
|
-
scriptPathExists: () => false,
|
|
968
941
|
fetchImpl: async () =>
|
|
969
942
|
new Response(
|
|
970
943
|
[
|
|
@@ -998,7 +971,6 @@ describe("cli agent run client", () => {
|
|
|
998
971
|
scriptDir: "C:/missing/scripts",
|
|
999
972
|
env: { AUTH_TOKEN: "token-123" },
|
|
1000
973
|
output,
|
|
1001
|
-
scriptPathExists: () => false,
|
|
1002
974
|
fetchImpl: async () => {
|
|
1003
975
|
let sent = false;
|
|
1004
976
|
return new Response(
|
|
@@ -1054,14 +1026,13 @@ describe("cli agent run client", () => {
|
|
|
1054
1026
|
scriptDir: "C:/missing/scripts",
|
|
1055
1027
|
env: {},
|
|
1056
1028
|
output,
|
|
1057
|
-
scriptPathExists: () => false,
|
|
1058
1029
|
fetchImpl: async () => {
|
|
1059
1030
|
throw new Error("fetch should not be called");
|
|
1060
1031
|
},
|
|
1061
1032
|
});
|
|
1062
1033
|
|
|
1063
1034
|
expect(result.exitCode).toBe(1);
|
|
1064
|
-
expect(output.text()).toContain("
|
|
1035
|
+
expect(output.text()).toContain("Run `nolo login`");
|
|
1065
1036
|
});
|
|
1066
1037
|
|
|
1067
1038
|
test("prints a friendly connection hint instead of crashing on transport errors", async () => {
|
|
@@ -1075,7 +1046,6 @@ describe("cli agent run client", () => {
|
|
|
1075
1046
|
scriptDir: "C:/missing/scripts",
|
|
1076
1047
|
env: { AUTH_TOKEN: "token-123" },
|
|
1077
1048
|
output,
|
|
1078
|
-
scriptPathExists: () => false,
|
|
1079
1049
|
fetchImpl: async () => {
|
|
1080
1050
|
throw new Error("ConnectionRefused");
|
|
1081
1051
|
},
|
package/client/agentRun.ts
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
1
|
import { join } from "node:path";
|
|
3
2
|
import { runLocalAgentTurn } from "../agentRuntimeLocal";
|
|
4
3
|
import { LOCAL_AGENT_CONFIG_MISSING_CODE } from "../agent-runtime/localLoop";
|
|
5
4
|
import {
|
|
6
|
-
|
|
7
|
-
NOLO_FULLSTACK_AGENT_KEY,
|
|
5
|
+
MIMO_MONTH_AGENT_KEY,
|
|
8
6
|
NOLO_PROJECT_MANAGER_AGENT_KEY,
|
|
9
|
-
NOLO_REVIEW_AGENT_KEY,
|
|
10
7
|
NOLO_SENIOR_FULLSTACK_AGENT_KEY,
|
|
8
|
+
WIN_CODEX_AGENT_KEY,
|
|
11
9
|
} from "../agentAliases";
|
|
12
10
|
import type { LocalAgentToolEvent } from "../agent-runtime/localLoop";
|
|
13
11
|
import type { AgentRuntimeHostAdapter, AgentRuntimeRequestedMode } from "../agentRuntimeLocal";
|
|
14
12
|
import { createCliLocalRuntimeAdapter } from "./localRuntimeAdapter";
|
|
15
13
|
import { createStreamingTextWriter } from "./streamingOutput";
|
|
16
14
|
import { prependTaskRunPrompt, type TaskRunPromptContext } from "./taskRunPrompt";
|
|
17
|
-
|
|
18
|
-
type EnvLike = Record<string, string | undefined>;
|
|
19
|
-
|
|
15
|
+
|
|
16
|
+
type EnvLike = Record<string, string | undefined>;
|
|
17
|
+
|
|
20
18
|
type OutputLike = {
|
|
21
19
|
write(chunk: string): unknown;
|
|
22
20
|
};
|
|
@@ -48,7 +46,7 @@ type RunAgentTurnOptions = {
|
|
|
48
46
|
noStream?: boolean;
|
|
49
47
|
noDefaultTestRoot?: boolean;
|
|
50
48
|
scriptDir: string;
|
|
51
|
-
env: EnvLike;
|
|
49
|
+
env: EnvLike;
|
|
52
50
|
output: OutputLike;
|
|
53
51
|
runtimeMode?: AgentRuntimeRequestedMode;
|
|
54
52
|
localRuntimeAdapter?: AgentRuntimeHostAdapter;
|
|
@@ -66,20 +64,55 @@ type RunAgentTurnOptions = {
|
|
|
66
64
|
errorCode?: string;
|
|
67
65
|
toolEvidence?: TaskRunToolEvidence;
|
|
68
66
|
}) => Promise<void>;
|
|
69
|
-
scriptPathExists?: (path: string) => boolean;
|
|
70
67
|
fetchImpl?: typeof fetch;
|
|
71
68
|
};
|
|
72
|
-
|
|
69
|
+
|
|
73
70
|
export type RunAgentTurnResult = {
|
|
74
71
|
exitCode: number;
|
|
75
72
|
dialogId?: string;
|
|
76
73
|
streamInterrupted?: boolean;
|
|
77
74
|
};
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
|
|
83
116
|
|
|
84
117
|
const SERVER_PLATFORM_TOOL_NAMES = new Set([
|
|
85
118
|
"addTableRow",
|
|
@@ -94,10 +127,9 @@ const SERVER_PLATFORM_TOOL_NAMES = new Set([
|
|
|
94
127
|
]);
|
|
95
128
|
|
|
96
129
|
const KNOWN_SERVER_PLATFORM_AGENT_KEYS = new Set([
|
|
97
|
-
|
|
98
|
-
NOLO_FULLSTACK_AGENT_KEY,
|
|
130
|
+
MIMO_MONTH_AGENT_KEY,
|
|
99
131
|
NOLO_PROJECT_MANAGER_AGENT_KEY,
|
|
100
|
-
|
|
132
|
+
WIN_CODEX_AGENT_KEY,
|
|
101
133
|
NOLO_SENIOR_FULLSTACK_AGENT_KEY,
|
|
102
134
|
]);
|
|
103
135
|
|
|
@@ -125,10 +157,6 @@ const KNOWN_SERVER_PLATFORM_AGENT_ALIASES = new Set([
|
|
|
125
157
|
"reviewer",
|
|
126
158
|
]);
|
|
127
159
|
|
|
128
|
-
export function shouldUseScriptBridge(decision: ScriptBridgeDecision) {
|
|
129
|
-
return !decision.hasAuthToken && decision.scriptPathExists;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
160
|
export function findServerPlatformTools(toolNames?: string[]) {
|
|
133
161
|
if (!Array.isArray(toolNames)) return [];
|
|
134
162
|
return toolNames.filter((toolName) => SERVER_PLATFORM_TOOL_NAMES.has(toolName));
|
|
@@ -514,62 +542,9 @@ function buildTransportErrorHint(serverUrl: string, error: unknown) {
|
|
|
514
542
|
return detail;
|
|
515
543
|
}
|
|
516
544
|
|
|
517
|
-
async function runScriptBridge(options: RunAgentTurnOptions, scriptPath: string) {
|
|
518
|
-
options.output.write(`\n${options.agentName} -> working via local script...\n\n`);
|
|
519
|
-
const proc = Bun.spawn({
|
|
520
|
-
cmd: [
|
|
521
|
-
process.execPath,
|
|
522
|
-
scriptPath,
|
|
523
|
-
"--agent",
|
|
524
|
-
options.agentKey,
|
|
525
|
-
"--server",
|
|
526
|
-
options.serverUrl,
|
|
527
|
-
"--msg",
|
|
528
|
-
options.message,
|
|
529
|
-
...(options.taskRunContext?.rowDbKey
|
|
530
|
-
? ["--task-row-dbkey", options.taskRunContext.rowDbKey]
|
|
531
|
-
: []),
|
|
532
|
-
...(options.taskRunContext?.taskRunId
|
|
533
|
-
? ["--task-run-id", options.taskRunContext.taskRunId]
|
|
534
|
-
: []),
|
|
535
|
-
...(options.taskRunContext?.workItemId
|
|
536
|
-
? ["--work-item-id", options.taskRunContext.workItemId]
|
|
537
|
-
: []),
|
|
538
|
-
...(options.taskRunContext?.artifactIds?.length
|
|
539
|
-
? ["--artifact-ids", options.taskRunContext.artifactIds.join(",")]
|
|
540
|
-
: []),
|
|
541
|
-
...(options.noDefaultTestRoot ? ["--no-default-test-root"] : []),
|
|
542
|
-
...(options.continueDialogId
|
|
543
|
-
? ["--continue", options.continueDialogId]
|
|
544
|
-
: []),
|
|
545
|
-
...(options.spaceId ? ["--space", options.spaceId] : []),
|
|
546
|
-
...(options.category ? ["--category", options.category] : []),
|
|
547
|
-
...(options.inheritedFromDialogKey
|
|
548
|
-
? ["--inherit-from-dialog", options.inheritedFromDialogKey]
|
|
549
|
-
: []),
|
|
550
|
-
...(options.background ? ["--bg"] : []),
|
|
551
|
-
...(typeof options.timeoutMs === "number" ? ["--timeout-ms", String(options.timeoutMs)] : []),
|
|
552
|
-
...(options.noStream ? ["--no-stream"] : []),
|
|
553
|
-
],
|
|
554
|
-
stdin: "inherit",
|
|
555
|
-
stdout: "inherit",
|
|
556
|
-
stderr: "inherit",
|
|
557
|
-
env: {
|
|
558
|
-
...process.env,
|
|
559
|
-
...options.env,
|
|
560
|
-
ASSISTANT_LABEL: options.agentName,
|
|
561
|
-
},
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
const exitCode = await proc.exited;
|
|
565
|
-
if (exitCode !== 0) {
|
|
566
|
-
options.output.write(`\n[nolo] Agent run exited with code ${exitCode}.\n`);
|
|
567
|
-
}
|
|
568
|
-
return { exitCode };
|
|
569
|
-
}
|
|
570
|
-
|
|
571
545
|
async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string) {
|
|
572
|
-
options.output
|
|
546
|
+
const spinner = new Spinner(options.output, `${options.agentName} -> working`);
|
|
547
|
+
spinner.start();
|
|
573
548
|
|
|
574
549
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
575
550
|
let res: Response;
|
|
@@ -623,14 +598,16 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
|
|
|
623
598
|
...(typeof options.timeoutMs === "number" ? { timeoutMs: options.timeoutMs } : {}),
|
|
624
599
|
stream: !options.noStream && !options.background,
|
|
625
600
|
}),
|
|
626
|
-
});
|
|
627
|
-
} catch (error) {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
601
|
+
});
|
|
602
|
+
} catch (error) {
|
|
603
|
+
spinner.stop();
|
|
604
|
+
options.output.write(buildTransportErrorHint(options.serverUrl, error));
|
|
605
|
+
return { exitCode: 1 };
|
|
606
|
+
}
|
|
607
|
+
|
|
632
608
|
const contentType = res.headers.get("content-type") || "";
|
|
633
609
|
if (contentType.includes("text/event-stream") && res.body) {
|
|
610
|
+
spinner.stop();
|
|
634
611
|
const result = await readStreamingAgentRun(options, res);
|
|
635
612
|
await recordTaskRunOutcome({
|
|
636
613
|
options,
|
|
@@ -644,14 +621,15 @@ async function runHttpAgentTurn(options: RunAgentTurnOptions, authToken: string)
|
|
|
644
621
|
});
|
|
645
622
|
return result;
|
|
646
623
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
624
|
+
|
|
625
|
+
spinner.stop();
|
|
626
|
+
let data: any = {};
|
|
627
|
+
try {
|
|
628
|
+
data = await res.json();
|
|
629
|
+
} catch {
|
|
630
|
+
data = {};
|
|
631
|
+
}
|
|
632
|
+
|
|
655
633
|
if (!res.ok) {
|
|
656
634
|
options.output.write(`[nolo] Agent request failed: HTTP ${res.status}\n`);
|
|
657
635
|
if (data?.error || data?.message) {
|
|
@@ -758,7 +736,8 @@ async function runLocalAgentTurnForCli(
|
|
|
758
736
|
return { exitCode: 1 };
|
|
759
737
|
}
|
|
760
738
|
|
|
761
|
-
options.output
|
|
739
|
+
const spinner = new Spinner(options.output, `${options.agentName} -> working locally`);
|
|
740
|
+
spinner.start();
|
|
762
741
|
const toolEvents: LocalAgentToolEvent[] = [];
|
|
763
742
|
try {
|
|
764
743
|
const traceLocalTools = shouldTraceLocalTools(options);
|
|
@@ -783,11 +762,15 @@ async function runLocalAgentTurnForCli(
|
|
|
783
762
|
? {
|
|
784
763
|
onToolEvent: (event) => {
|
|
785
764
|
if (collectToolEvidence) toolEvents.push(event);
|
|
786
|
-
if (traceLocalTools)
|
|
765
|
+
if (traceLocalTools) {
|
|
766
|
+
spinner.stop();
|
|
767
|
+
options.output.write(formatToolTraceEvent(event));
|
|
768
|
+
}
|
|
787
769
|
},
|
|
788
770
|
}
|
|
789
771
|
: {}),
|
|
790
772
|
});
|
|
773
|
+
spinner.stop();
|
|
791
774
|
const content = result.content.trim();
|
|
792
775
|
if (content) {
|
|
793
776
|
options.output.write(`\n${options.agentName} > ${content}\n`);
|
|
@@ -803,6 +786,7 @@ async function runLocalAgentTurnForCli(
|
|
|
803
786
|
});
|
|
804
787
|
return { exitCode: 0, dialogId: result.dialogId };
|
|
805
788
|
} catch (error) {
|
|
789
|
+
spinner.stop();
|
|
806
790
|
if (settings.reportFailure) {
|
|
807
791
|
options.output.write(
|
|
808
792
|
`[nolo] Local agent run failed: ${
|
|
@@ -927,8 +911,6 @@ async function readStreamingAgentRun(
|
|
|
927
911
|
|
|
928
912
|
export async function runAgentTurn(options: RunAgentTurnOptions) {
|
|
929
913
|
const authToken = resolveAuthToken(options.env);
|
|
930
|
-
const scriptPath = join(options.scriptDir, "chatWithAgent.ts");
|
|
931
|
-
const scriptPathExists = (options.scriptPathExists ?? existsSync)(scriptPath);
|
|
932
914
|
const runtimeMode = resolveRequestedRuntimeMode(options);
|
|
933
915
|
|
|
934
916
|
if (runtimeMode === "local") {
|
|
@@ -969,17 +951,13 @@ export async function runAgentTurn(options: RunAgentTurnOptions) {
|
|
|
969
951
|
}
|
|
970
952
|
}
|
|
971
953
|
|
|
972
|
-
if (
|
|
973
|
-
|
|
954
|
+
if (!authToken) {
|
|
955
|
+
options.output.write(
|
|
956
|
+
"[nolo] This install needs an auth token before it can talk to agents.\n" +
|
|
957
|
+
"Run `nolo login`, or set AUTH_TOKEN / NOLO_SERVER for non-interactive runs.\n"
|
|
958
|
+
);
|
|
959
|
+
return { exitCode: 1 };
|
|
974
960
|
}
|
|
975
961
|
|
|
976
|
-
if (!authToken) {
|
|
977
|
-
options.output.write(
|
|
978
|
-
"[nolo] This install needs an auth token before it can talk to agents.\n" +
|
|
979
|
-
"Set AUTH_TOKEN, or use the repo-local CLI where the dev script bridge is available.\n"
|
|
980
|
-
);
|
|
981
|
-
return { exitCode: 1 };
|
|
982
|
-
}
|
|
983
|
-
|
|
984
962
|
return runHttpAgentTurn(options, authToken);
|
|
985
963
|
}
|
|
@@ -1324,6 +1324,70 @@ describe("CLI local runtime adapter", () => {
|
|
|
1324
1324
|
expect(result.metadata).toMatchObject({ exitCode: 0 });
|
|
1325
1325
|
});
|
|
1326
1326
|
|
|
1327
|
+
test("applies runtime policy shell output limit to local executors without adding a timeout", async () => {
|
|
1328
|
+
const requests: Array<{ body: any }> = [];
|
|
1329
|
+
const adapter = createCliLocalRuntimeAdapter({
|
|
1330
|
+
env: {
|
|
1331
|
+
NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
|
|
1332
|
+
NOLO_LOCAL_SHELL_MODE: "worktree",
|
|
1333
|
+
},
|
|
1334
|
+
db: {
|
|
1335
|
+
get: async () => ({
|
|
1336
|
+
dbKey: "agent-local-shell-limits",
|
|
1337
|
+
prompt: "Use shell when needed.",
|
|
1338
|
+
model: "qwen-coder",
|
|
1339
|
+
runtimeToolPolicy: {
|
|
1340
|
+
version: 1,
|
|
1341
|
+
runtimeTools: ["execShell"],
|
|
1342
|
+
shell: { enabled: true, mode: "worktree", maxOutputBytes: 120 },
|
|
1343
|
+
},
|
|
1344
|
+
}),
|
|
1345
|
+
put: async () => {},
|
|
1346
|
+
batch: async () => {},
|
|
1347
|
+
iterator: () => (async function* () {})(),
|
|
1348
|
+
},
|
|
1349
|
+
cwd: import.meta.dir,
|
|
1350
|
+
fetchImpl: async (_url, init) => {
|
|
1351
|
+
requests.push({ body: JSON.parse(String(init?.body)) });
|
|
1352
|
+
if (requests.length === 1) {
|
|
1353
|
+
return Response.json({
|
|
1354
|
+
choices: [{
|
|
1355
|
+
message: {
|
|
1356
|
+
content: "",
|
|
1357
|
+
tool_calls: [{
|
|
1358
|
+
id: "call-1",
|
|
1359
|
+
type: "function",
|
|
1360
|
+
function: {
|
|
1361
|
+
name: "execShell",
|
|
1362
|
+
arguments: JSON.stringify({
|
|
1363
|
+
cmd: process.platform === "win32"
|
|
1364
|
+
? "'abcdefghijklmnopqrstuvwxyz0123456789'.PadRight(500, 'x')"
|
|
1365
|
+
: "bun -e 'console.log(\"x\".repeat(500))'",
|
|
1366
|
+
}),
|
|
1367
|
+
},
|
|
1368
|
+
}],
|
|
1369
|
+
},
|
|
1370
|
+
}],
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
return Response.json({
|
|
1374
|
+
choices: [{ message: { content: "limits applied" } }],
|
|
1375
|
+
});
|
|
1376
|
+
},
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
const result = await runLocalAgentTurn({
|
|
1380
|
+
adapter,
|
|
1381
|
+
agentRef: "shell-limits",
|
|
1382
|
+
input: "run output limit check",
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
expect(result.content).toBe("limits applied");
|
|
1386
|
+
const toolResult = requests[1]?.body.messages.at(-1)?.content ?? "";
|
|
1387
|
+
expect(toolResult).toContain("[truncated ");
|
|
1388
|
+
expect(toolResult).not.toContain("command timed out");
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1327
1391
|
test("accepts execShell aliases from the local model", async () => {
|
|
1328
1392
|
const requests: Array<{ body: any }> = [];
|
|
1329
1393
|
const adapter = createCliLocalRuntimeAdapter({
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
parsePlatformChatCompletionData,
|
|
17
17
|
parsePlatformChatCompletionResponse,
|
|
18
18
|
resolvePlatformChatProviderConfig,
|
|
19
|
+
resolveCurrentRunRuntimeToolPolicy,
|
|
20
|
+
resolveLocalWorkspaceExecutorOptionsFromPolicy,
|
|
19
21
|
resolveRequestedRuntimeToolNames,
|
|
20
22
|
shouldUsePlatformChatProvider,
|
|
21
23
|
} from "../agentRuntimeLocal";
|
|
@@ -337,9 +339,15 @@ function buildLocalToolExecutors(args: {
|
|
|
337
339
|
env: EnvLike;
|
|
338
340
|
fetchImpl: typeof fetch;
|
|
339
341
|
localToolExecutors?: CliLocalRuntimeAdapterDeps["localToolExecutors"];
|
|
342
|
+
commandTimeoutMs?: number;
|
|
343
|
+
commandOutputLimit?: number;
|
|
340
344
|
}) {
|
|
341
345
|
return {
|
|
342
|
-
...createLocalWorkspaceToolExecutors({
|
|
346
|
+
...createLocalWorkspaceToolExecutors({
|
|
347
|
+
workspaceRoot: args.workspaceRoot,
|
|
348
|
+
commandTimeoutMs: args.commandTimeoutMs,
|
|
349
|
+
commandOutputLimit: args.commandOutputLimit,
|
|
350
|
+
}),
|
|
343
351
|
...buildServerPlatformToolExecutors({
|
|
344
352
|
env: args.env,
|
|
345
353
|
fetchImpl: args.fetchImpl,
|
|
@@ -434,11 +442,13 @@ export function createCliLocalRuntimeAdapter(
|
|
|
434
442
|
? { defaultCwd: deps.cwd }
|
|
435
443
|
: { cwd: deps.cwd }
|
|
436
444
|
);
|
|
445
|
+
let runtimeToolExecutionLimits: ReturnType<typeof resolveLocalWorkspaceExecutorOptionsFromPolicy> = {};
|
|
437
446
|
let localToolExecutors = buildLocalToolExecutors({
|
|
438
447
|
workspaceRoot: workspaceSession.workspaceRoot,
|
|
439
448
|
env: deps.env,
|
|
440
449
|
fetchImpl,
|
|
441
450
|
localToolExecutors: deps.localToolExecutors,
|
|
451
|
+
...runtimeToolExecutionLimits,
|
|
442
452
|
});
|
|
443
453
|
|
|
444
454
|
async function activateAgentWorkspaceSession(args: {
|
|
@@ -458,6 +468,7 @@ export function createCliLocalRuntimeAdapter(
|
|
|
458
468
|
env: deps.env,
|
|
459
469
|
fetchImpl,
|
|
460
470
|
localToolExecutors: deps.localToolExecutors,
|
|
471
|
+
...runtimeToolExecutionLimits,
|
|
461
472
|
});
|
|
462
473
|
deps.output?.write(formatWorkspaceSessionActivation(workspaceSession));
|
|
463
474
|
}
|
|
@@ -478,7 +489,17 @@ export function createCliLocalRuntimeAdapter(
|
|
|
478
489
|
toolNames: requestedToolNames,
|
|
479
490
|
env: deps.env,
|
|
480
491
|
});
|
|
492
|
+
runtimeToolExecutionLimits = resolveLocalWorkspaceExecutorOptionsFromPolicy(
|
|
493
|
+
resolveCurrentRunRuntimeToolPolicy(agentConfig),
|
|
494
|
+
);
|
|
481
495
|
await activateAgentWorkspaceSession({ agentRef, agentConfig });
|
|
496
|
+
localToolExecutors = buildLocalToolExecutors({
|
|
497
|
+
workspaceRoot: workspaceSession.workspaceRoot,
|
|
498
|
+
env: deps.env,
|
|
499
|
+
fetchImpl,
|
|
500
|
+
localToolExecutors: deps.localToolExecutors,
|
|
501
|
+
...runtimeToolExecutionLimits,
|
|
502
|
+
});
|
|
482
503
|
return agentConfig;
|
|
483
504
|
},
|
|
484
505
|
loadDialogHistory: async (dialogId) => readDialogMessages({
|
package/machineWsRunDispatch.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runLocalAgentTurn, type LocalAgentToolEvent } from "./agent-runtime/localLoop";
|
|
2
|
+
import { resolveLocalRuntimeEnvFromPolicy } from "./agent-runtime/runtimeToolPolicy";
|
|
2
3
|
import {
|
|
3
4
|
assertMachineRunAllowed as defaultAssertMachineRunAllowed,
|
|
4
5
|
resolveMachineRunPermissionPolicy as defaultResolveMachineRunPermissionPolicy,
|
|
@@ -225,11 +226,8 @@ function mergeToolNames(...values: unknown[]) {
|
|
|
225
226
|
return names;
|
|
226
227
|
}
|
|
227
228
|
|
|
228
|
-
function localRuntimeEnvFromPolicy(runtimeEnv: EnvLike, policy?: ConnectorRuntimePolicy): EnvLike {
|
|
229
|
-
return
|
|
230
|
-
...runtimeEnv,
|
|
231
|
-
...(policy?.workspace?.cwd ? { NOLO_LOCAL_WORKTREE: policy.workspace.cwd } : {}),
|
|
232
|
-
};
|
|
229
|
+
export function localRuntimeEnvFromPolicy(runtimeEnv: EnvLike, policy?: ConnectorRuntimePolicy): EnvLike {
|
|
230
|
+
return resolveLocalRuntimeEnvFromPolicy(runtimeEnv, policy);
|
|
233
231
|
}
|
|
234
232
|
|
|
235
233
|
function forwardedUserAuthToken(parsed: any): string {
|