@pi-ohm/subagents 0.6.3 → 0.6.4-dev.22132458683.1.8646f56
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 +107 -0
- package/package.json +15 -3
- package/src/catalog.test.ts +71 -0
- package/src/catalog.ts +5 -0
- package/src/errors.test.ts +72 -0
- package/src/errors.ts +146 -0
- package/src/extension.test.ts +137 -0
- package/src/extension.ts +117 -55
- package/src/policy.test.ts +173 -0
- package/src/policy.ts +114 -0
- package/src/runtime/backend.test.ts +310 -0
- package/src/runtime/backend.ts +462 -0
- package/src/runtime/tasks.test.ts +459 -0
- package/src/runtime/tasks.ts +955 -0
- package/src/runtime/ui.test.ts +130 -0
- package/src/runtime/ui.ts +118 -0
- package/src/schema.test.ts +460 -0
- package/src/schema.ts +446 -0
- package/src/tools/primary.test.ts +292 -0
- package/src/tools/primary.ts +217 -0
- package/src/tools/task.test.ts +1101 -0
- package/src/tools/task.ts +1705 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import type { OhmRuntimeConfig, OhmSubagentBackend } from "@pi-ohm/config";
|
|
4
|
+
import { Result } from "better-result";
|
|
5
|
+
import type { OhmSubagentDefinition } from "../catalog";
|
|
6
|
+
import {
|
|
7
|
+
createDefaultTaskExecutionBackend,
|
|
8
|
+
PiCliTaskExecutionBackend,
|
|
9
|
+
ScaffoldTaskExecutionBackend,
|
|
10
|
+
type PiCliRunner,
|
|
11
|
+
} from "./backend";
|
|
12
|
+
|
|
13
|
+
function defineTest(name: string, run: () => void | Promise<void>): void {
|
|
14
|
+
void test(name, run);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function makeConfig(subagentBackend: OhmSubagentBackend): OhmRuntimeConfig {
|
|
18
|
+
return {
|
|
19
|
+
defaultMode: "smart",
|
|
20
|
+
subagentBackend,
|
|
21
|
+
features: {
|
|
22
|
+
handoff: true,
|
|
23
|
+
subagents: true,
|
|
24
|
+
sessionThreadSearch: true,
|
|
25
|
+
handoffVisualizer: true,
|
|
26
|
+
painterImagegen: false,
|
|
27
|
+
},
|
|
28
|
+
painter: {
|
|
29
|
+
googleNanoBanana: {
|
|
30
|
+
enabled: false,
|
|
31
|
+
model: "",
|
|
32
|
+
},
|
|
33
|
+
openai: {
|
|
34
|
+
enabled: false,
|
|
35
|
+
model: "",
|
|
36
|
+
},
|
|
37
|
+
azureOpenai: {
|
|
38
|
+
enabled: false,
|
|
39
|
+
deployment: "",
|
|
40
|
+
endpoint: "",
|
|
41
|
+
apiVersion: "",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
subagents: {
|
|
45
|
+
taskMaxConcurrency: 3,
|
|
46
|
+
taskRetentionMs: 1000,
|
|
47
|
+
permissions: {
|
|
48
|
+
default: "allow",
|
|
49
|
+
subagents: {},
|
|
50
|
+
allowInternalRouting: false,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const subagentFixture: OhmSubagentDefinition = {
|
|
57
|
+
id: "finder",
|
|
58
|
+
name: "Finder",
|
|
59
|
+
summary: "Search specialist",
|
|
60
|
+
whenToUse: ["search"],
|
|
61
|
+
scaffoldPrompt: "search prompt",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
defineTest("ScaffoldTaskExecutionBackend returns deterministic summary/output", async () => {
|
|
65
|
+
const backend = new ScaffoldTaskExecutionBackend();
|
|
66
|
+
|
|
67
|
+
const result = await backend.executeStart({
|
|
68
|
+
taskId: "task_1",
|
|
69
|
+
subagent: subagentFixture,
|
|
70
|
+
description: "Auth flow scan",
|
|
71
|
+
prompt: "Find auth validation path and refresh flow",
|
|
72
|
+
cwd: "/tmp/project",
|
|
73
|
+
config: makeConfig("none"),
|
|
74
|
+
signal: undefined,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
assert.equal(Result.isOk(result), true);
|
|
78
|
+
if (Result.isError(result)) {
|
|
79
|
+
assert.fail("Expected scaffold backend execution to succeed");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
assert.match(result.value.summary, /Finder/);
|
|
83
|
+
assert.match(result.value.summary, /Auth flow scan/);
|
|
84
|
+
assert.match(result.value.output, /subagent: finder/);
|
|
85
|
+
assert.match(result.value.output, /backend: scaffold/);
|
|
86
|
+
assert.match(result.value.output, /mode: smart/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
defineTest("ScaffoldTaskExecutionBackend returns deterministic follow-up output", async () => {
|
|
90
|
+
const backend = new ScaffoldTaskExecutionBackend();
|
|
91
|
+
|
|
92
|
+
const result = await backend.executeSend({
|
|
93
|
+
taskId: "task_1",
|
|
94
|
+
subagent: subagentFixture,
|
|
95
|
+
description: "Auth flow scan",
|
|
96
|
+
initialPrompt: "Find auth validation path",
|
|
97
|
+
followUpPrompts: ["Now include tests"],
|
|
98
|
+
prompt: "Now include edge-case tests",
|
|
99
|
+
cwd: "/tmp/project",
|
|
100
|
+
config: makeConfig("none"),
|
|
101
|
+
signal: undefined,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
assert.equal(Result.isOk(result), true);
|
|
105
|
+
if (Result.isError(result)) {
|
|
106
|
+
assert.fail("Expected scaffold follow-up execution to succeed");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
assert.match(result.value.summary, /follow-up/);
|
|
110
|
+
assert.match(result.value.output, /initial_prompt:/);
|
|
111
|
+
assert.match(result.value.output, /follow_up_prompt:/);
|
|
112
|
+
assert.match(result.value.output, /follow_up_count: 1/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
defineTest("ScaffoldTaskExecutionBackend maps aborted signal to runtime error", async () => {
|
|
116
|
+
const backend = new ScaffoldTaskExecutionBackend();
|
|
117
|
+
const controller = new AbortController();
|
|
118
|
+
controller.abort();
|
|
119
|
+
|
|
120
|
+
const result = await backend.executeStart({
|
|
121
|
+
taskId: "task_2",
|
|
122
|
+
subagent: subagentFixture,
|
|
123
|
+
description: "Auth flow scan",
|
|
124
|
+
prompt: "prompt",
|
|
125
|
+
cwd: "/tmp/project",
|
|
126
|
+
config: makeConfig("none"),
|
|
127
|
+
signal: controller.signal,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
assert.equal(Result.isError(result), true);
|
|
131
|
+
if (Result.isOk(result)) {
|
|
132
|
+
assert.fail("Expected aborted signal to produce runtime error");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
assert.equal(result.error.code, "task_aborted");
|
|
136
|
+
assert.equal(result.error.stage, "execute_start");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
defineTest("ScaffoldTaskExecutionBackend maps aborted send signal to runtime error", async () => {
|
|
140
|
+
const backend = new ScaffoldTaskExecutionBackend();
|
|
141
|
+
const controller = new AbortController();
|
|
142
|
+
controller.abort();
|
|
143
|
+
|
|
144
|
+
const result = await backend.executeSend({
|
|
145
|
+
taskId: "task_3",
|
|
146
|
+
subagent: subagentFixture,
|
|
147
|
+
description: "Auth flow scan",
|
|
148
|
+
initialPrompt: "initial",
|
|
149
|
+
followUpPrompts: [],
|
|
150
|
+
prompt: "prompt",
|
|
151
|
+
cwd: "/tmp/project",
|
|
152
|
+
config: makeConfig("none"),
|
|
153
|
+
signal: controller.signal,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
assert.equal(Result.isError(result), true);
|
|
157
|
+
if (Result.isOk(result)) {
|
|
158
|
+
assert.fail("Expected aborted signal to produce runtime error");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
assert.equal(result.error.code, "task_aborted");
|
|
162
|
+
assert.equal(result.error.stage, "execute_send");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
defineTest(
|
|
166
|
+
"PiCliTaskExecutionBackend executes real backend runner for interactive-shell",
|
|
167
|
+
async () => {
|
|
168
|
+
const prompts: string[] = [];
|
|
169
|
+
|
|
170
|
+
const runner: PiCliRunner = async (input) => {
|
|
171
|
+
prompts.push(input.prompt);
|
|
172
|
+
return {
|
|
173
|
+
exitCode: 0,
|
|
174
|
+
stdout: "finder online",
|
|
175
|
+
stderr: "",
|
|
176
|
+
timedOut: false,
|
|
177
|
+
aborted: false,
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const backend = new PiCliTaskExecutionBackend(runner, 1_000);
|
|
182
|
+
const result = await backend.executeStart({
|
|
183
|
+
taskId: "task_4",
|
|
184
|
+
subagent: subagentFixture,
|
|
185
|
+
description: "Auth flow scan",
|
|
186
|
+
prompt: "Find auth validation path and refresh flow",
|
|
187
|
+
cwd: "/tmp/project",
|
|
188
|
+
config: makeConfig("interactive-shell"),
|
|
189
|
+
signal: undefined,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
assert.equal(Result.isOk(result), true);
|
|
193
|
+
if (Result.isError(result)) {
|
|
194
|
+
assert.fail("Expected interactive-shell backend execution to succeed");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
assert.equal(prompts.length, 1);
|
|
198
|
+
assert.match(prompts[0] ?? "", /You are the Finder subagent in Pi OHM/);
|
|
199
|
+
assert.equal(result.value.output, "finder online");
|
|
200
|
+
assert.match(result.value.summary, /Finder: finder online/);
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
defineTest(
|
|
205
|
+
"PiCliTaskExecutionBackend falls back to scaffold mode when backend is none",
|
|
206
|
+
async () => {
|
|
207
|
+
let called = false;
|
|
208
|
+
|
|
209
|
+
const runner: PiCliRunner = async () => {
|
|
210
|
+
called = true;
|
|
211
|
+
return {
|
|
212
|
+
exitCode: 0,
|
|
213
|
+
stdout: "should not run",
|
|
214
|
+
stderr: "",
|
|
215
|
+
timedOut: false,
|
|
216
|
+
aborted: false,
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const backend = new PiCliTaskExecutionBackend(runner, 1_000);
|
|
221
|
+
const result = await backend.executeStart({
|
|
222
|
+
taskId: "task_5",
|
|
223
|
+
subagent: subagentFixture,
|
|
224
|
+
description: "Auth flow scan",
|
|
225
|
+
prompt: "Find auth validation path and refresh flow",
|
|
226
|
+
cwd: "/tmp/project",
|
|
227
|
+
config: makeConfig("none"),
|
|
228
|
+
signal: undefined,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
assert.equal(called, false);
|
|
232
|
+
assert.equal(Result.isOk(result), true);
|
|
233
|
+
if (Result.isError(result)) {
|
|
234
|
+
assert.fail("Expected scaffold fallback execution to succeed");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
assert.match(result.value.output, /backend: scaffold/);
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
defineTest("PiCliTaskExecutionBackend reports timeout and backend execution failures", async () => {
|
|
242
|
+
const timeoutRunner: PiCliRunner = async () => ({
|
|
243
|
+
exitCode: 124,
|
|
244
|
+
stdout: "",
|
|
245
|
+
stderr: "",
|
|
246
|
+
timedOut: true,
|
|
247
|
+
aborted: false,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const timeoutBackend = new PiCliTaskExecutionBackend(timeoutRunner, 1_000);
|
|
251
|
+
const timedOut = await timeoutBackend.executeStart({
|
|
252
|
+
taskId: "task_6",
|
|
253
|
+
subagent: subagentFixture,
|
|
254
|
+
description: "Auth flow scan",
|
|
255
|
+
prompt: "prompt",
|
|
256
|
+
cwd: "/tmp/project",
|
|
257
|
+
config: makeConfig("interactive-shell"),
|
|
258
|
+
signal: undefined,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
assert.equal(Result.isError(timedOut), true);
|
|
262
|
+
if (Result.isOk(timedOut)) {
|
|
263
|
+
assert.fail("Expected timeout error");
|
|
264
|
+
}
|
|
265
|
+
assert.equal(timedOut.error.code, "task_backend_timeout");
|
|
266
|
+
|
|
267
|
+
const failureRunner: PiCliRunner = async () => ({
|
|
268
|
+
exitCode: 1,
|
|
269
|
+
stdout: "",
|
|
270
|
+
stderr: "runner exploded",
|
|
271
|
+
timedOut: false,
|
|
272
|
+
aborted: false,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const failureBackend = new PiCliTaskExecutionBackend(failureRunner, 1_000);
|
|
276
|
+
const failed = await failureBackend.executeStart({
|
|
277
|
+
taskId: "task_7",
|
|
278
|
+
subagent: subagentFixture,
|
|
279
|
+
description: "Auth flow scan",
|
|
280
|
+
prompt: "prompt",
|
|
281
|
+
cwd: "/tmp/project",
|
|
282
|
+
config: makeConfig("interactive-shell"),
|
|
283
|
+
signal: undefined,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
assert.equal(Result.isError(failed), true);
|
|
287
|
+
if (Result.isOk(failed)) {
|
|
288
|
+
assert.fail("Expected backend execution failure");
|
|
289
|
+
}
|
|
290
|
+
assert.equal(failed.error.code, "task_backend_execution_failed");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
defineTest("PiCliTaskExecutionBackend resolves backend IDs from runtime config", () => {
|
|
294
|
+
const backend = new PiCliTaskExecutionBackend(async () => ({
|
|
295
|
+
exitCode: 0,
|
|
296
|
+
stdout: "ok",
|
|
297
|
+
stderr: "",
|
|
298
|
+
timedOut: false,
|
|
299
|
+
aborted: false,
|
|
300
|
+
}));
|
|
301
|
+
|
|
302
|
+
assert.equal(backend.resolveBackendId(makeConfig("interactive-shell")), "interactive-shell");
|
|
303
|
+
assert.equal(backend.resolveBackendId(makeConfig("none")), "scaffold");
|
|
304
|
+
assert.equal(backend.resolveBackendId(makeConfig("custom-plugin")), "custom-plugin");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
defineTest("createDefaultTaskExecutionBackend defaults to interactive-shell backend", () => {
|
|
308
|
+
const backend = createDefaultTaskExecutionBackend();
|
|
309
|
+
assert.equal(backend.id, "interactive-shell");
|
|
310
|
+
});
|