@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
package/README.md
CHANGED
|
@@ -17,9 +17,110 @@ Scaffolded subagents:
|
|
|
17
17
|
Profiles can be marked with `primary: true` in config/catalog to indicate direct
|
|
18
18
|
invocation as a top-level tool entrypoint instead of task-tool-only invocation.
|
|
19
19
|
|
|
20
|
+
Primary profiles are registered as direct tools automatically. Tool names are derived
|
|
21
|
+
from profile IDs with deterministic collision handling.
|
|
22
|
+
|
|
23
|
+
`primary: true` is additive:
|
|
24
|
+
|
|
25
|
+
- profile gets a direct top-level tool
|
|
26
|
+
- profile stays available in `task` subagent roster (`subagent_type`)
|
|
27
|
+
- direct-tool execution and task-routed execution share the same runtime/result envelope
|
|
28
|
+
|
|
20
29
|
The orchestration tool name is **`task`**. Async orchestration lifecycle
|
|
21
30
|
operations (`start/status/wait/send/cancel`) are exposed through this tool.
|
|
22
31
|
|
|
32
|
+
## Task tool (current)
|
|
33
|
+
|
|
34
|
+
Current behavior:
|
|
35
|
+
|
|
36
|
+
- supports `op: "start"` for a single task payload (sync + `async:true`)
|
|
37
|
+
- supports batched `op: "start"` payloads via `tasks[]` with optional `parallel:true`
|
|
38
|
+
- supports lifecycle operations: `status`, `wait`, `send`, `cancel`
|
|
39
|
+
- compatibility aliases: `status`/`wait` accept `id` or `ids`; `op:"result"` is normalized to `status`
|
|
40
|
+
- returns `task_id`, status, and deterministic task details
|
|
41
|
+
- persists task registry snapshots to disk for resume/reload behavior
|
|
42
|
+
- enforces terminal-task retention expiry with explicit `task_expired` lookup errors
|
|
43
|
+
- validates all payloads with TypeBox boundary schema + typed Result errors
|
|
44
|
+
|
|
45
|
+
## Subagent backend behavior
|
|
46
|
+
|
|
47
|
+
Runtime backend is selected from `subagentBackend` config:
|
|
48
|
+
|
|
49
|
+
- `interactive-shell` (default): executes a real nested `pi` run for subagent prompts
|
|
50
|
+
using built-in tools (`read,bash,edit,write,grep,find,ls`)
|
|
51
|
+
- `none`: uses deterministic scaffold backend (echo-style debug output)
|
|
52
|
+
- `custom-plugin`: currently returns `unsupported_subagent_backend`
|
|
53
|
+
|
|
54
|
+
If output appears like prompt regurgitation, verify `subagentBackend` is not set to `none`.
|
|
55
|
+
|
|
56
|
+
Example payload:
|
|
57
|
+
|
|
58
|
+
```jsonc
|
|
59
|
+
{
|
|
60
|
+
"op": "start",
|
|
61
|
+
"subagent_type": "finder",
|
|
62
|
+
"description": "Auth flow scan",
|
|
63
|
+
"prompt": "Trace token validation + refresh paths",
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Batch execution notes:
|
|
68
|
+
|
|
69
|
+
- aggregate item order is deterministic (input order)
|
|
70
|
+
- bounded parallelism is enforced by `subagents.taskMaxConcurrency` (default `3`)
|
|
71
|
+
- task failures are isolated; one failed batch item does not abort siblings
|
|
72
|
+
|
|
73
|
+
## Task permission policy
|
|
74
|
+
|
|
75
|
+
Task orchestration enforces policy decisions from runtime config:
|
|
76
|
+
|
|
77
|
+
- `allow` — task execution proceeds
|
|
78
|
+
- `deny` — execution is blocked with `task_permission_denied`
|
|
79
|
+
|
|
80
|
+
Config shape:
|
|
81
|
+
|
|
82
|
+
```jsonc
|
|
83
|
+
{
|
|
84
|
+
"subagents": {
|
|
85
|
+
"permissions": {
|
|
86
|
+
"default": "allow",
|
|
87
|
+
"subagents": {
|
|
88
|
+
"finder": "deny",
|
|
89
|
+
},
|
|
90
|
+
"allowInternalRouting": false,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Additional hardening behaviors:
|
|
97
|
+
|
|
98
|
+
- internal profiles (`internal:true`) are hidden from task roster exposure unless
|
|
99
|
+
`allowInternalRouting` is enabled
|
|
100
|
+
- wait timeout/abort are explicit (`task_wait_timeout`, `task_wait_aborted`)
|
|
101
|
+
- tool error payloads include stable `error_category`
|
|
102
|
+
|
|
103
|
+
Persistence details:
|
|
104
|
+
|
|
105
|
+
- default snapshot path: `${PI_CONFIG_DIR|PI_CODING_AGENT_DIR|PI_AGENT_DIR|~/.pi/agent}/ohm.subagents.tasks.json`
|
|
106
|
+
- retention window is configurable via `OHM_SUBAGENTS_TASK_RETENTION_MS` (positive integer ms)
|
|
107
|
+
- corrupt snapshot files are auto-recovered to `*.corrupt-<epoch>` and runtime falls back to empty state
|
|
108
|
+
|
|
109
|
+
## Migration notes
|
|
110
|
+
|
|
111
|
+
Behavior has moved from scaffold-only single start calls to a lifecycle runtime,
|
|
112
|
+
with real backend execution as the default:
|
|
113
|
+
|
|
114
|
+
- orchestration now supports `start/status/wait/send/cancel`
|
|
115
|
+
- batched `start` supports deterministic ordering and bounded concurrency
|
|
116
|
+
- primary tools and task-routed calls share one execution/runtime contract
|
|
117
|
+
- policy-denied calls now fail deterministically instead of silently proceeding
|
|
118
|
+
|
|
119
|
+
Existing slash commands remain unchanged:
|
|
120
|
+
|
|
121
|
+
- `/ohm-subagents`
|
|
122
|
+
- `/ohm-subagent <id>`
|
|
123
|
+
|
|
23
124
|
## Live TUI feedback
|
|
24
125
|
|
|
25
126
|
`@pi-ohm/subagents` uses `@mariozechner/pi-tui` for task runtime visuals.
|
|
@@ -31,6 +132,12 @@ Baseline running-task display includes:
|
|
|
31
132
|
- in-flight tool call count
|
|
32
133
|
- elapsed time (`mm:ss`)
|
|
33
134
|
|
|
135
|
+
Runtime UI surfaces are synchronized from one task snapshot model:
|
|
136
|
+
|
|
137
|
+
- footer status (`setStatus`) with running/active counters
|
|
138
|
+
- widget task list (`setWidget`) using two-line per-task renderer
|
|
139
|
+
- headless fallback `onUpdate` text with equivalent description/tool-count/elapsed info
|
|
140
|
+
|
|
34
141
|
Example running block:
|
|
35
142
|
|
|
36
143
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-ohm/subagents",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4-dev.22132458683.1.8646f56",
|
|
4
4
|
"homepage": "https://github.com/pi-ohm/pi-ohm/tree/dev/packages/subagents#readme",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,10 +20,22 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@mariozechner/pi-coding-agent": "catalog:pi",
|
|
23
|
-
"@pi-ohm/config": "
|
|
23
|
+
"@pi-ohm/config": "0.6.4-dev.22132458683.1.8646f56",
|
|
24
|
+
"better-result": "catalog:",
|
|
25
|
+
"zod": "catalog:"
|
|
24
26
|
},
|
|
25
27
|
"peerDependencies": {
|
|
26
|
-
"@mariozechner/pi-coding-agent": "*"
|
|
28
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
29
|
+
"@mariozechner/pi-tui": "*",
|
|
30
|
+
"@sinclair/typebox": "*"
|
|
31
|
+
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"@mariozechner/pi-tui": {
|
|
34
|
+
"optional": true
|
|
35
|
+
},
|
|
36
|
+
"@sinclair/typebox": {
|
|
37
|
+
"optional": true
|
|
38
|
+
}
|
|
27
39
|
},
|
|
28
40
|
"pi": {
|
|
29
41
|
"extensions": [
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { getSubagentById, OHM_SUBAGENT_CATALOG } from "./catalog";
|
|
4
|
+
|
|
5
|
+
function defineTest(name: string, run: () => void | Promise<void>): void {
|
|
6
|
+
void test(name, run);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
defineTest("catalog ids are unique", () => {
|
|
10
|
+
const ids = OHM_SUBAGENT_CATALOG.map((agent) => agent.id);
|
|
11
|
+
const uniqueIds = new Set(ids);
|
|
12
|
+
|
|
13
|
+
assert.equal(uniqueIds.size, ids.length);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
defineTest("catalog includes expected default subagents", () => {
|
|
17
|
+
const ids = OHM_SUBAGENT_CATALOG.map((agent) => agent.id);
|
|
18
|
+
assert.deepEqual(ids, ["librarian", "oracle", "finder"]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
defineTest("librarian is primary by default", () => {
|
|
22
|
+
const librarian = getSubagentById("librarian");
|
|
23
|
+
assert.notEqual(librarian, undefined);
|
|
24
|
+
if (!librarian) {
|
|
25
|
+
assert.fail("Expected librarian profile in catalog");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
assert.equal(librarian.primary, true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
defineTest("finder and oracle are task-routed by default", () => {
|
|
32
|
+
const finder = getSubagentById("finder");
|
|
33
|
+
const oracle = getSubagentById("oracle");
|
|
34
|
+
|
|
35
|
+
assert.notEqual(finder, undefined);
|
|
36
|
+
assert.notEqual(oracle, undefined);
|
|
37
|
+
|
|
38
|
+
if (!finder || !oracle) {
|
|
39
|
+
assert.fail("Expected finder and oracle profiles in catalog");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
assert.equal(finder.primary, undefined);
|
|
43
|
+
assert.equal(oracle.primary, undefined);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
defineTest("getSubagentById is case-insensitive and trims input", () => {
|
|
47
|
+
const match = getSubagentById(" LiBrArIaN ");
|
|
48
|
+
assert.notEqual(match, undefined);
|
|
49
|
+
if (!match) {
|
|
50
|
+
assert.fail("Expected to find librarian profile");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
assert.equal(match.id, "librarian");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
defineTest("getSubagentById returns undefined for unknown ids", () => {
|
|
57
|
+
const match = getSubagentById("does-not-exist");
|
|
58
|
+
assert.equal(match, undefined);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
defineTest("catalog entries contain required non-empty scaffold fields", () => {
|
|
62
|
+
for (const agent of OHM_SUBAGENT_CATALOG) {
|
|
63
|
+
assert.ok(agent.name.trim().length > 0);
|
|
64
|
+
assert.ok(agent.summary.trim().length > 0);
|
|
65
|
+
assert.ok(agent.scaffoldPrompt.trim().length > 0);
|
|
66
|
+
assert.ok(agent.whenToUse.length > 0);
|
|
67
|
+
for (const condition of agent.whenToUse) {
|
|
68
|
+
assert.ok(condition.trim().length > 0);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
package/src/catalog.ts
CHANGED
|
@@ -9,6 +9,11 @@ export interface OhmSubagentDefinition {
|
|
|
9
9
|
* instead of requiring delegated Task-style invocation.
|
|
10
10
|
*/
|
|
11
11
|
primary?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Internal profiles are hidden from model-facing task roster exposure unless
|
|
14
|
+
* policy explicitly allows internal routing.
|
|
15
|
+
*/
|
|
16
|
+
internal?: boolean;
|
|
12
17
|
whenToUse: string[];
|
|
13
18
|
scaffoldPrompt: string;
|
|
14
19
|
requiresPackage?: string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
SubagentPersistenceError,
|
|
5
|
+
SubagentPolicyError,
|
|
6
|
+
SubagentRuntimeError,
|
|
7
|
+
SubagentValidationError,
|
|
8
|
+
isSubagentError,
|
|
9
|
+
} from "./errors";
|
|
10
|
+
|
|
11
|
+
function defineTest(name: string, run: () => void | Promise<void>): void {
|
|
12
|
+
void test(name, run);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
defineTest("SubagentValidationError derives message from cause when omitted", () => {
|
|
16
|
+
const error = new SubagentValidationError({
|
|
17
|
+
code: "invalid_payload",
|
|
18
|
+
cause: new Error("bad shape"),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
assert.equal(error._tag, "SubagentValidationError");
|
|
22
|
+
assert.equal(error.code, "invalid_payload");
|
|
23
|
+
assert.match(error.message, /Validation failed/);
|
|
24
|
+
assert.match(error.message, /bad shape/);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
defineTest("SubagentPolicyError keeps explicit message", () => {
|
|
28
|
+
const error = new SubagentPolicyError({
|
|
29
|
+
code: "policy_denied",
|
|
30
|
+
action: "start",
|
|
31
|
+
message: "Policy blocked task start",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
assert.equal(error._tag, "SubagentPolicyError");
|
|
35
|
+
assert.equal(error.action, "start");
|
|
36
|
+
assert.equal(error.message, "Policy blocked task start");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
defineTest("SubagentRuntimeError and SubagentPersistenceError expose typed tags", () => {
|
|
40
|
+
const runtimeError = new SubagentRuntimeError({
|
|
41
|
+
code: "runtime_unavailable",
|
|
42
|
+
stage: "execute",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const persistenceError = new SubagentPersistenceError({
|
|
46
|
+
code: "state_write_failed",
|
|
47
|
+
resource: "task-registry",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
assert.equal(runtimeError._tag, "SubagentRuntimeError");
|
|
51
|
+
assert.equal(runtimeError.stage, "execute");
|
|
52
|
+
assert.equal(persistenceError._tag, "SubagentPersistenceError");
|
|
53
|
+
assert.equal(persistenceError.resource, "task-registry");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
defineTest("isSubagentError narrows tagged errors", () => {
|
|
57
|
+
const validationError = new SubagentValidationError({
|
|
58
|
+
code: "invalid_id",
|
|
59
|
+
message: "id required",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const runtimeError = new SubagentRuntimeError({
|
|
63
|
+
code: "runtime_failure",
|
|
64
|
+
message: "executor unavailable",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const notError = { ok: true };
|
|
68
|
+
|
|
69
|
+
assert.equal(isSubagentError(validationError), true);
|
|
70
|
+
assert.equal(isSubagentError(runtimeError), true);
|
|
71
|
+
assert.equal(isSubagentError(notError), false);
|
|
72
|
+
});
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { TaggedError, type Result } from "better-result";
|
|
2
|
+
|
|
3
|
+
export type SubagentErrorMeta = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
function messageFromCause(cause: unknown): string {
|
|
6
|
+
if (cause instanceof Error && cause.message.trim().length > 0) return cause.message;
|
|
7
|
+
if (typeof cause === "string" && cause.trim().length > 0) return cause;
|
|
8
|
+
return String(cause);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class SubagentValidationError extends TaggedError("SubagentValidationError")<{
|
|
12
|
+
code: string;
|
|
13
|
+
message: string;
|
|
14
|
+
path?: string;
|
|
15
|
+
cause?: unknown;
|
|
16
|
+
meta?: SubagentErrorMeta;
|
|
17
|
+
}>() {
|
|
18
|
+
constructor(args: {
|
|
19
|
+
code: string;
|
|
20
|
+
path?: string;
|
|
21
|
+
message?: string;
|
|
22
|
+
cause?: unknown;
|
|
23
|
+
meta?: SubagentErrorMeta;
|
|
24
|
+
}) {
|
|
25
|
+
const derivedMessage =
|
|
26
|
+
args.message ??
|
|
27
|
+
(args.cause
|
|
28
|
+
? `Validation failed (${args.code}): ${messageFromCause(args.cause)}`
|
|
29
|
+
: `Validation failed (${args.code})`);
|
|
30
|
+
|
|
31
|
+
super({
|
|
32
|
+
code: args.code,
|
|
33
|
+
path: args.path,
|
|
34
|
+
cause: args.cause,
|
|
35
|
+
meta: args.meta,
|
|
36
|
+
message: derivedMessage,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class SubagentPolicyError extends TaggedError("SubagentPolicyError")<{
|
|
42
|
+
code: string;
|
|
43
|
+
message: string;
|
|
44
|
+
action?: string;
|
|
45
|
+
cause?: unknown;
|
|
46
|
+
meta?: SubagentErrorMeta;
|
|
47
|
+
}>() {
|
|
48
|
+
constructor(args: {
|
|
49
|
+
code: string;
|
|
50
|
+
action?: string;
|
|
51
|
+
message?: string;
|
|
52
|
+
cause?: unknown;
|
|
53
|
+
meta?: SubagentErrorMeta;
|
|
54
|
+
}) {
|
|
55
|
+
const derivedMessage =
|
|
56
|
+
args.message ??
|
|
57
|
+
(args.cause
|
|
58
|
+
? `Policy denied (${args.code}): ${messageFromCause(args.cause)}`
|
|
59
|
+
: `Policy denied (${args.code})`);
|
|
60
|
+
|
|
61
|
+
super({
|
|
62
|
+
code: args.code,
|
|
63
|
+
action: args.action,
|
|
64
|
+
cause: args.cause,
|
|
65
|
+
meta: args.meta,
|
|
66
|
+
message: derivedMessage,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class SubagentRuntimeError extends TaggedError("SubagentRuntimeError")<{
|
|
72
|
+
code: string;
|
|
73
|
+
message: string;
|
|
74
|
+
stage?: string;
|
|
75
|
+
cause?: unknown;
|
|
76
|
+
meta?: SubagentErrorMeta;
|
|
77
|
+
}>() {
|
|
78
|
+
constructor(args: {
|
|
79
|
+
code: string;
|
|
80
|
+
stage?: string;
|
|
81
|
+
message?: string;
|
|
82
|
+
cause?: unknown;
|
|
83
|
+
meta?: SubagentErrorMeta;
|
|
84
|
+
}) {
|
|
85
|
+
const derivedMessage =
|
|
86
|
+
args.message ??
|
|
87
|
+
(args.cause
|
|
88
|
+
? `Runtime failure (${args.code}): ${messageFromCause(args.cause)}`
|
|
89
|
+
: `Runtime failure (${args.code})`);
|
|
90
|
+
|
|
91
|
+
super({
|
|
92
|
+
code: args.code,
|
|
93
|
+
stage: args.stage,
|
|
94
|
+
cause: args.cause,
|
|
95
|
+
meta: args.meta,
|
|
96
|
+
message: derivedMessage,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class SubagentPersistenceError extends TaggedError("SubagentPersistenceError")<{
|
|
102
|
+
code: string;
|
|
103
|
+
message: string;
|
|
104
|
+
resource?: string;
|
|
105
|
+
cause?: unknown;
|
|
106
|
+
meta?: SubagentErrorMeta;
|
|
107
|
+
}>() {
|
|
108
|
+
constructor(args: {
|
|
109
|
+
code: string;
|
|
110
|
+
resource?: string;
|
|
111
|
+
message?: string;
|
|
112
|
+
cause?: unknown;
|
|
113
|
+
meta?: SubagentErrorMeta;
|
|
114
|
+
}) {
|
|
115
|
+
const derivedMessage =
|
|
116
|
+
args.message ??
|
|
117
|
+
(args.cause
|
|
118
|
+
? `Persistence failure (${args.code}): ${messageFromCause(args.cause)}`
|
|
119
|
+
: `Persistence failure (${args.code})`);
|
|
120
|
+
|
|
121
|
+
super({
|
|
122
|
+
code: args.code,
|
|
123
|
+
resource: args.resource,
|
|
124
|
+
cause: args.cause,
|
|
125
|
+
meta: args.meta,
|
|
126
|
+
message: derivedMessage,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type SubagentError =
|
|
132
|
+
| SubagentValidationError
|
|
133
|
+
| SubagentPolicyError
|
|
134
|
+
| SubagentRuntimeError
|
|
135
|
+
| SubagentPersistenceError;
|
|
136
|
+
|
|
137
|
+
export type SubagentResult<T, E extends SubagentError = SubagentError> = Result<T, E>;
|
|
138
|
+
|
|
139
|
+
export function isSubagentError(value: unknown): value is SubagentError {
|
|
140
|
+
return (
|
|
141
|
+
SubagentValidationError.is(value) ||
|
|
142
|
+
SubagentPolicyError.is(value) ||
|
|
143
|
+
SubagentRuntimeError.is(value) ||
|
|
144
|
+
SubagentPersistenceError.is(value)
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import type { OhmRuntimeConfig } from "@pi-ohm/config";
|
|
4
|
+
import { getSubagentById } from "./catalog";
|
|
5
|
+
import {
|
|
6
|
+
buildSubagentDetailText,
|
|
7
|
+
buildSubagentsOverviewText,
|
|
8
|
+
getSubagentInvocationMode,
|
|
9
|
+
normalizeCommandArgs,
|
|
10
|
+
registerSubagentTools,
|
|
11
|
+
} from "./extension";
|
|
12
|
+
|
|
13
|
+
function defineTest(name: string, run: () => void | Promise<void>): void {
|
|
14
|
+
void test(name, run);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const configFixture: OhmRuntimeConfig = {
|
|
18
|
+
defaultMode: "smart",
|
|
19
|
+
subagentBackend: "none",
|
|
20
|
+
features: {
|
|
21
|
+
handoff: true,
|
|
22
|
+
subagents: true,
|
|
23
|
+
sessionThreadSearch: true,
|
|
24
|
+
handoffVisualizer: true,
|
|
25
|
+
painterImagegen: false,
|
|
26
|
+
},
|
|
27
|
+
painter: {
|
|
28
|
+
googleNanoBanana: {
|
|
29
|
+
enabled: false,
|
|
30
|
+
model: "",
|
|
31
|
+
},
|
|
32
|
+
openai: {
|
|
33
|
+
enabled: false,
|
|
34
|
+
model: "",
|
|
35
|
+
},
|
|
36
|
+
azureOpenai: {
|
|
37
|
+
enabled: false,
|
|
38
|
+
deployment: "",
|
|
39
|
+
endpoint: "",
|
|
40
|
+
apiVersion: "",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
subagents: {
|
|
44
|
+
taskMaxConcurrency: 2,
|
|
45
|
+
taskRetentionMs: 1000,
|
|
46
|
+
permissions: {
|
|
47
|
+
default: "allow",
|
|
48
|
+
subagents: {},
|
|
49
|
+
allowInternalRouting: false,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
defineTest("normalizeCommandArgs supports array input", () => {
|
|
55
|
+
const parsed = normalizeCommandArgs(["finder", 42, "oracle", null]);
|
|
56
|
+
assert.deepEqual(parsed, ["finder", "oracle"]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
defineTest("normalizeCommandArgs splits raw string input", () => {
|
|
60
|
+
const parsed = normalizeCommandArgs(" finder oracle librarian ");
|
|
61
|
+
assert.deepEqual(parsed, ["finder", "oracle", "librarian"]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
defineTest("normalizeCommandArgs supports envelope args array", () => {
|
|
65
|
+
const parsed = normalizeCommandArgs({
|
|
66
|
+
args: ["finder", 1, "oracle"],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
assert.deepEqual(parsed, ["finder", "oracle"]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
defineTest("normalizeCommandArgs supports envelope raw string", () => {
|
|
73
|
+
const parsed = normalizeCommandArgs({
|
|
74
|
+
raw: "finder oracle",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
assert.deepEqual(parsed, ["finder", "oracle"]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
defineTest("normalizeCommandArgs returns empty list for unsupported payloads", () => {
|
|
81
|
+
assert.deepEqual(normalizeCommandArgs(undefined), []);
|
|
82
|
+
assert.deepEqual(normalizeCommandArgs(123), []);
|
|
83
|
+
assert.deepEqual(normalizeCommandArgs({}), []);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
defineTest("getSubagentInvocationMode returns primary-tool for primary profiles", () => {
|
|
87
|
+
assert.equal(getSubagentInvocationMode(true), "primary-tool");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
defineTest("getSubagentInvocationMode returns task-routed for non-primary profiles", () => {
|
|
91
|
+
assert.equal(getSubagentInvocationMode(false), "task-routed");
|
|
92
|
+
assert.equal(getSubagentInvocationMode(undefined), "task-routed");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
defineTest("registerSubagentTools registers task tool + primary tools", () => {
|
|
96
|
+
const registeredTools: string[] = [];
|
|
97
|
+
|
|
98
|
+
registerSubagentTools({
|
|
99
|
+
registerTool(definition) {
|
|
100
|
+
registeredTools.push(definition.name);
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
assert.equal(registeredTools.includes("task"), true);
|
|
105
|
+
assert.equal(registeredTools.includes("librarian"), true);
|
|
106
|
+
assert.equal(registeredTools.includes("finder"), false);
|
|
107
|
+
assert.equal(registeredTools.includes("oracle"), false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
defineTest("buildSubagentsOverviewText preserves command compatibility output", () => {
|
|
111
|
+
const text = buildSubagentsOverviewText({
|
|
112
|
+
config: configFixture,
|
|
113
|
+
loadedFrom: ["/tmp/global/ohm.json"],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
assert.match(text, /Pi OHM: subagents/);
|
|
117
|
+
assert.match(text, /Scaffolded subagents:/);
|
|
118
|
+
assert.match(text, /Use \/ohm-subagent <id> to inspect one profile\./);
|
|
119
|
+
assert.match(text, /loadedFrom: \/tmp\/global\/ohm.json/);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
defineTest("buildSubagentDetailText preserves detailed subagent view", () => {
|
|
123
|
+
const librarian = getSubagentById("librarian");
|
|
124
|
+
assert.notEqual(librarian, undefined);
|
|
125
|
+
if (!librarian) {
|
|
126
|
+
assert.fail("Expected librarian profile");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const text = buildSubagentDetailText({
|
|
130
|
+
config: configFixture,
|
|
131
|
+
subagent: librarian,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
assert.match(text, /Subagent: Librarian/);
|
|
135
|
+
assert.match(text, /When to use:/);
|
|
136
|
+
assert.match(text, /Scaffold prompt:/);
|
|
137
|
+
});
|