@cr1ms0n/pi-subagent 0.8.7 → 0.8.9
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/CHANGELOG.md +6 -0
- package/docs/ARCHITECTURE.md +125 -125
- package/docs/SECURITY.md +88 -88
- package/package.json +1 -1
- package/skills/subagent/SKILL.md +113 -113
- package/src/agents.ts +282 -282
- package/src/orchestrator.ts +247 -247
- package/src/policy.ts +531 -531
- package/src/schema.ts +189 -189
package/src/schema.ts
CHANGED
|
@@ -1,189 +1,189 @@
|
|
|
1
|
-
import { Type, type Static } from "typebox";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Remove TypeBox's internal compositor metadata before a schema crosses into
|
|
5
|
-
* Pi's provider-facing tool catalog. The original schema is kept intact for
|
|
6
|
-
* local Value.Check/Value.Errors validation.
|
|
7
|
-
*/
|
|
8
|
-
export function sanitizeProviderSchema<T>(value: T): T {
|
|
9
|
-
if (Array.isArray(value)) {
|
|
10
|
-
return value.map((item) => sanitizeProviderSchema(item)) as T;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
if (value && typeof value === "object") {
|
|
14
|
-
const record = value as Record<string, unknown>;
|
|
15
|
-
return Object.fromEntries(
|
|
16
|
-
Object.entries(record)
|
|
17
|
-
.filter(([key]) => !key.startsWith("~"))
|
|
18
|
-
.map(([key, entry]) => [key, sanitizeProviderSchema(entry)] as const),
|
|
19
|
-
) as T;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return value;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const ThinkingLevel = Type.String({
|
|
26
|
-
minLength: 1,
|
|
27
|
-
maxLength: 64,
|
|
28
|
-
description: "Opaque Pi thinking level passed through unchanged. Pi/model-specific values such as max are allowed; the active Pi process decides support.",
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
const OutputMode = Type.Union([Type.Literal("inline"), Type.Literal("file-only")]);
|
|
32
|
-
const Profile = Type.Union([Type.Literal("explore"), Type.Literal("review"), Type.Literal("general")]);
|
|
33
|
-
const Isolation = Type.Union([Type.Literal("shared"), Type.Literal("worktree")]);
|
|
34
|
-
const Backend = Type.Union([Type.Literal("pi"), Type.Literal("codex"), Type.Literal("claude")]);
|
|
35
|
-
const Action = Type.Union([
|
|
36
|
-
Type.Literal("status"),
|
|
37
|
-
Type.Literal("wait"),
|
|
38
|
-
Type.Literal("cancel"),
|
|
39
|
-
Type.Literal("steer"),
|
|
40
|
-
Type.Literal("diff"),
|
|
41
|
-
Type.Literal("apply"),
|
|
42
|
-
Type.Literal("discard"),
|
|
43
|
-
Type.Literal("plan"),
|
|
44
|
-
]);
|
|
45
|
-
|
|
46
|
-
/** Shared optional task configuration fields. */
|
|
47
|
-
export const TaskFields = {
|
|
48
|
-
backend: Type.Optional({ ...Backend, description: "Agent CLI powering the child. pi (default) supports every feature. codex uses codex exec with an OS-level read-only sandbox, but reports no cost (max_cost is refused) and cannot be steered. claude uses the claude CLI, reports cost, and cannot be steered. Unsupported combinations are refused with an explanation rather than silently ignored." }),
|
|
49
|
-
agent: Type.Optional(Type.String({ minLength: 1, description: "Named agent to use (from .pi/agents/<name>.md). Supplies persona system prompt and defaults; explicit params still override." })),
|
|
50
|
-
description: Type.Optional(Type.String({ description: "Short human label (3-5 words) shown in UIs and result indexes." })),
|
|
51
|
-
system_prompt: Type.Optional(Type.String({ description: "Extra system prompt appended to the child's prompt (does not replace it)." })),
|
|
52
|
-
model: Type.Optional(Type.String({ description: "**REQUIRED for every spawn call (task/tasks).** Exact model id from modelPolicy, in provider/model-id form. Calls without an explicit model are rejected; agent-file model, taskDefaults, and parent-session inheritance are ignored. Management actions (status/wait/cancel/steer/diff/apply/discard) do not need it." })),
|
|
53
|
-
thinking: Type.Optional({ ...ThinkingLevel, description: "Opaque Pi thinking level for the child. Values such as max are passed through unchanged; Pi/model support decides validity. Defaults to agent thinking, profile taskDefaults.thinking, modelPolicy route thinking, then the parent's level." }),
|
|
54
|
-
tools: Type.Optional(Type.Array(Type.String(), { description: "Optional tool allowlist. explore/review profiles reject project-writing tools; Pi context-management tools remain available for context continuity." })),
|
|
55
|
-
profile: Type.Optional({ ...Profile, description: "Capability profile: explore/review cannot write project files but retain Pi context-management tools; general inherits the parent's active tools and may write." }),
|
|
56
|
-
cwd: Type.Optional(Type.String({ description: "Working directory for the child process." })),
|
|
57
|
-
timeout_ms: Type.Optional(Type.Number({ minimum: 1, maximum: 24 * 60 * 60_000, description: "Total budget in milliseconds including queue time. Timed-out runs report which phase timed out." })),
|
|
58
|
-
max_turns: Type.Optional(Type.Number({ minimum: 1, maximum: 500, description: "Budget: at this many turns the child is steered to wrap up and given grace turns for a final answer; ends as 'partial' with output preserved." })),
|
|
59
|
-
max_cost: Type.Optional(Type.Number({ minimum: 0, description: "Soft cost ceiling in dollars; checked after each turn, triggers the same wrap-up flow as max_turns." })),
|
|
60
|
-
grace_turns: Type.Optional(Type.Number({ minimum: 0, maximum: 20, description: "Wrap-up turns allowed after a budget breach before hard stop. 0 = immediate stop. Default from config (2)." })),
|
|
61
|
-
fallback_models: Type.Optional(Type.Array(Type.String(), { maxItems: 5, description: "Ordered backup models tried automatically on transient failures (provider error, stall, queue timeout)." })),
|
|
62
|
-
max_retries: Type.Optional(Type.Number({ minimum: 0, maximum: 5, description: "Extra attempts on transient failures. Defaults to config maxRetries (1). Task-quality failures never retry." })),
|
|
63
|
-
context: Type.Optional(
|
|
64
|
-
Type.Union([Type.Literal("fresh"), Type.Literal("fork")], {
|
|
65
|
-
description: "fork starts the child from a branched copy of the parent conversation (needs a persisted parent session); fresh (default) starts clean. Fork is single-task only.",
|
|
66
|
-
}),
|
|
67
|
-
),
|
|
68
|
-
output: Type.Optional(Type.String({ description: "File path to write final output." })),
|
|
69
|
-
output_schema: Type.Optional(
|
|
70
|
-
Type.Unsafe<Record<string, unknown>>(
|
|
71
|
-
Type.Object({}, {
|
|
72
|
-
additionalProperties: true,
|
|
73
|
-
description: "JSON Schema the child's final result must satisfy. The child ends with a fenced json:result block; validation failures get one steer-based repair round, then end 'partial' with the errors reported.",
|
|
74
|
-
}),
|
|
75
|
-
),
|
|
76
|
-
),
|
|
77
|
-
output_mode: Type.Optional({ ...OutputMode, description: "file-only returns a pointer instead of inline text; use for large reports." }),
|
|
78
|
-
resume: Type.Optional(Type.String({ description: "Child session id to continue." })),
|
|
79
|
-
fork_resume: Type.Optional(Type.Boolean({ description: "Fork the resumed session instead of direct resume." })),
|
|
80
|
-
isolation: Type.Optional({ ...Isolation, description: "worktree runs the task in an isolated git worktree; changed work is preserved on a branch." }),
|
|
81
|
-
include_wip: Type.Optional(
|
|
82
|
-
Type.Boolean({ description: "Seed a worktree with the parent checkout's uncommitted changes (staged + unstaged + untracked). Only valid with isolation:'worktree'." }),
|
|
83
|
-
),
|
|
84
|
-
allow_shared_writes: Type.Optional(
|
|
85
|
-
Type.Boolean({ description: "Unsafe opt-in for parallel writers sharing one checkout." }),
|
|
86
|
-
),
|
|
87
|
-
keep_background: Type.Optional(
|
|
88
|
-
Type.Boolean({ description: "Keep processes the child backgrounded (e.g. dev servers) alive after a clean exit." }),
|
|
89
|
-
),
|
|
90
|
-
} as const;
|
|
91
|
-
|
|
92
|
-
export const ParallelTaskItem = Type.Object(
|
|
93
|
-
{
|
|
94
|
-
task: Type.String({ minLength: 1, description: "Task text for one parallel worker." }),
|
|
95
|
-
...TaskFields,
|
|
96
|
-
},
|
|
97
|
-
{ additionalProperties: false },
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Provider-facing tool parameters.
|
|
102
|
-
*
|
|
103
|
-
* IMPORTANT: LLM tool APIs (OpenAI-compatible / OpenRouter / Anthropic via many
|
|
104
|
-
* providers) require the top-level parameters schema to be JSON Schema
|
|
105
|
-
* `type: "object"`. A Type.Union serializes as `anyOf` without `type: "object"`,
|
|
106
|
-
* which surfaces as:
|
|
107
|
-
* Invalid schema for function 'subagent': schema must be a JSON Schema of
|
|
108
|
-
* 'type: "object"', got 'type: "None"'.
|
|
109
|
-
*
|
|
110
|
-
* Mode exclusivity (action vs task vs tasks) is enforced in policy validation,
|
|
111
|
-
* not at the JSON Schema layer.
|
|
112
|
-
*/
|
|
113
|
-
export const SubagentParamsSchema = Type.Object(
|
|
114
|
-
{
|
|
115
|
-
// Management actions
|
|
116
|
-
action: Type.Optional({ ...Action, description: "Management action on an existing run: status, wait, cancel, steer (inject guidance into a running child), diff/apply/discard (worktree results). action:\"plan\" dry-runs validation + preflight with task/tasks — no spawn." }),
|
|
117
|
-
id: Type.Optional(Type.String({ minLength: 1, description: "Run id (or unique prefix) for management actions." })),
|
|
118
|
-
message: Type.Optional(Type.String({ minLength: 1, description: "Steering message injected into the running child (action: steer)." })),
|
|
119
|
-
index: Type.Optional(Type.Number({ minimum: 0, description: "Task index within a parallel run for steer/diff/apply/discard. Defaults to the only eligible task." })),
|
|
120
|
-
|
|
121
|
-
// Single-task mode
|
|
122
|
-
task: Type.Optional(Type.String({ minLength: 1, description: "Task to delegate (single mode)." })),
|
|
123
|
-
...TaskFields,
|
|
124
|
-
async: Type.Optional(Type.Boolean({ description: "Run in the background and return a handle immediately." })),
|
|
125
|
-
|
|
126
|
-
// Parallel mode
|
|
127
|
-
tasks: Type.Optional(
|
|
128
|
-
Type.Array(ParallelTaskItem, {
|
|
129
|
-
minItems: 1,
|
|
130
|
-
maxItems: 8,
|
|
131
|
-
description: "Array of independent tasks for parallel mode. Parallel tasks default to the read-only explore profile; parallel writers need isolation:'worktree'.",
|
|
132
|
-
}),
|
|
133
|
-
),
|
|
134
|
-
synthesis: Type.Optional(
|
|
135
|
-
Type.String({
|
|
136
|
-
minLength: 1,
|
|
137
|
-
description: "Optional synthesis prompt for parallel mode: after all tasks finish, one read-only child folds their outputs using this instruction and its result is delivered first.",
|
|
138
|
-
}),
|
|
139
|
-
),
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
additionalProperties: false,
|
|
143
|
-
description: "Subagent request: single, parallel, status, wait, or cancel.",
|
|
144
|
-
},
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
export type SubagentParams = Static<typeof SubagentParamsSchema>;
|
|
148
|
-
export type ParallelTaskInput = Static<typeof ParallelTaskItem>;
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Dedicated blocking-wait tool.
|
|
152
|
-
*
|
|
153
|
-
* `subagent` already exposes wait via `action: "wait"`, but collecting a
|
|
154
|
-
* background run is the one management step a model reaches for reflexively
|
|
155
|
-
* mid-flow, and burying it behind an action union costs a discovery step.
|
|
156
|
-
* This mirrors the highest-adoption package in the ecosystem (`pi-subagents`
|
|
157
|
-
* ships `subagent` + `subagent_wait` for the same reason). It is a thin
|
|
158
|
-
* front-end over the identical handler — no second delivery path.
|
|
159
|
-
*/
|
|
160
|
-
export const SubagentWaitParamsSchema = Type.Object(
|
|
161
|
-
{
|
|
162
|
-
id: Type.String({ minLength: 1, description: "Run id (or unique prefix) of the background run to collect." }),
|
|
163
|
-
timeout_ms: Type.Optional(
|
|
164
|
-
Type.Number({
|
|
165
|
-
minimum: 1,
|
|
166
|
-
maximum: 24 * 60 * 60_000,
|
|
167
|
-
description: "Give up waiting after this long and return a still-running notice. The run is NOT cancelled; collect it later with subagent_wait or action:'status'. Omit to wait until the run settles.",
|
|
168
|
-
}),
|
|
169
|
-
),
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
additionalProperties: false,
|
|
173
|
-
description: "Block until a background subagent run settles, then deliver its output.",
|
|
174
|
-
},
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
/** Provider-facing projections without TypeBox's internal `~...` metadata. */
|
|
178
|
-
export const ProviderSubagentParamsSchema = sanitizeProviderSchema(SubagentParamsSchema);
|
|
179
|
-
export const ProviderSubagentWaitParamsSchema = sanitizeProviderSchema(SubagentWaitParamsSchema);
|
|
180
|
-
|
|
181
|
-
export type SubagentWaitParams = Static<typeof SubagentWaitParamsSchema>;
|
|
182
|
-
|
|
183
|
-
/** Runtime guard used by tests/docs to assert provider compatibility. */
|
|
184
|
-
export function assertObjectToolSchema(schema: unknown): asserts schema is { type: "object" } {
|
|
185
|
-
if (!schema || typeof schema !== "object" || (schema as { type?: unknown }).type !== "object") {
|
|
186
|
-
const type = schema && typeof schema === "object" ? (schema as { type?: unknown }).type : typeof schema;
|
|
187
|
-
throw new Error(`Tool parameters must be JSON Schema type "object", got ${JSON.stringify(type ?? "None")}`);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
1
|
+
import { Type, type Static } from "typebox";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Remove TypeBox's internal compositor metadata before a schema crosses into
|
|
5
|
+
* Pi's provider-facing tool catalog. The original schema is kept intact for
|
|
6
|
+
* local Value.Check/Value.Errors validation.
|
|
7
|
+
*/
|
|
8
|
+
export function sanitizeProviderSchema<T>(value: T): T {
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
return value.map((item) => sanitizeProviderSchema(item)) as T;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (value && typeof value === "object") {
|
|
14
|
+
const record = value as Record<string, unknown>;
|
|
15
|
+
return Object.fromEntries(
|
|
16
|
+
Object.entries(record)
|
|
17
|
+
.filter(([key]) => !key.startsWith("~"))
|
|
18
|
+
.map(([key, entry]) => [key, sanitizeProviderSchema(entry)] as const),
|
|
19
|
+
) as T;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ThinkingLevel = Type.String({
|
|
26
|
+
minLength: 1,
|
|
27
|
+
maxLength: 64,
|
|
28
|
+
description: "Opaque Pi thinking level passed through unchanged. Pi/model-specific values such as max are allowed; the active Pi process decides support.",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const OutputMode = Type.Union([Type.Literal("inline"), Type.Literal("file-only")]);
|
|
32
|
+
const Profile = Type.Union([Type.Literal("explore"), Type.Literal("review"), Type.Literal("general")]);
|
|
33
|
+
const Isolation = Type.Union([Type.Literal("shared"), Type.Literal("worktree")]);
|
|
34
|
+
const Backend = Type.Union([Type.Literal("pi"), Type.Literal("codex"), Type.Literal("claude")]);
|
|
35
|
+
const Action = Type.Union([
|
|
36
|
+
Type.Literal("status"),
|
|
37
|
+
Type.Literal("wait"),
|
|
38
|
+
Type.Literal("cancel"),
|
|
39
|
+
Type.Literal("steer"),
|
|
40
|
+
Type.Literal("diff"),
|
|
41
|
+
Type.Literal("apply"),
|
|
42
|
+
Type.Literal("discard"),
|
|
43
|
+
Type.Literal("plan"),
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/** Shared optional task configuration fields. */
|
|
47
|
+
export const TaskFields = {
|
|
48
|
+
backend: Type.Optional({ ...Backend, description: "Agent CLI powering the child. pi (default) supports every feature. codex uses codex exec with an OS-level read-only sandbox, but reports no cost (max_cost is refused) and cannot be steered. claude uses the claude CLI, reports cost, and cannot be steered. Unsupported combinations are refused with an explanation rather than silently ignored." }),
|
|
49
|
+
agent: Type.Optional(Type.String({ minLength: 1, description: "Named agent to use (from .pi/agents/<name>.md). Supplies persona system prompt and defaults; explicit params still override." })),
|
|
50
|
+
description: Type.Optional(Type.String({ description: "Short human label (3-5 words) shown in UIs and result indexes." })),
|
|
51
|
+
system_prompt: Type.Optional(Type.String({ description: "Extra system prompt appended to the child's prompt (does not replace it)." })),
|
|
52
|
+
model: Type.Optional(Type.String({ description: "**REQUIRED for every spawn call (task/tasks).** Exact model id from modelPolicy, in provider/model-id form. Calls without an explicit model are rejected; agent-file model, taskDefaults, and parent-session inheritance are ignored. Management actions (status/wait/cancel/steer/diff/apply/discard) do not need it." })),
|
|
53
|
+
thinking: Type.Optional({ ...ThinkingLevel, description: "Opaque Pi thinking level for the child. Values such as max are passed through unchanged; Pi/model support decides validity. Defaults to agent thinking, profile taskDefaults.thinking, modelPolicy route thinking, then the parent's level." }),
|
|
54
|
+
tools: Type.Optional(Type.Array(Type.String(), { description: "Optional tool allowlist. explore/review profiles reject project-writing tools; Pi context-management tools remain available for context continuity." })),
|
|
55
|
+
profile: Type.Optional({ ...Profile, description: "Capability profile: explore/review cannot write project files but retain Pi context-management tools; general inherits the parent's active tools and may write." }),
|
|
56
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for the child process." })),
|
|
57
|
+
timeout_ms: Type.Optional(Type.Number({ minimum: 1, maximum: 24 * 60 * 60_000, description: "Total budget in milliseconds including queue time. Timed-out runs report which phase timed out." })),
|
|
58
|
+
max_turns: Type.Optional(Type.Number({ minimum: 1, maximum: 500, description: "Budget: at this many turns the child is steered to wrap up and given grace turns for a final answer; ends as 'partial' with output preserved." })),
|
|
59
|
+
max_cost: Type.Optional(Type.Number({ minimum: 0, description: "Soft cost ceiling in dollars; checked after each turn, triggers the same wrap-up flow as max_turns." })),
|
|
60
|
+
grace_turns: Type.Optional(Type.Number({ minimum: 0, maximum: 20, description: "Wrap-up turns allowed after a budget breach before hard stop. 0 = immediate stop. Default from config (2)." })),
|
|
61
|
+
fallback_models: Type.Optional(Type.Array(Type.String(), { maxItems: 5, description: "Ordered backup models tried automatically on transient failures (provider error, stall, queue timeout)." })),
|
|
62
|
+
max_retries: Type.Optional(Type.Number({ minimum: 0, maximum: 5, description: "Extra attempts on transient failures. Defaults to config maxRetries (1). Task-quality failures never retry." })),
|
|
63
|
+
context: Type.Optional(
|
|
64
|
+
Type.Union([Type.Literal("fresh"), Type.Literal("fork")], {
|
|
65
|
+
description: "fork starts the child from a branched copy of the parent conversation (needs a persisted parent session); fresh (default) starts clean. Fork is single-task only.",
|
|
66
|
+
}),
|
|
67
|
+
),
|
|
68
|
+
output: Type.Optional(Type.String({ description: "File path to write final output." })),
|
|
69
|
+
output_schema: Type.Optional(
|
|
70
|
+
Type.Unsafe<Record<string, unknown>>(
|
|
71
|
+
Type.Object({}, {
|
|
72
|
+
additionalProperties: true,
|
|
73
|
+
description: "JSON Schema the child's final result must satisfy. The child ends with a fenced json:result block; validation failures get one steer-based repair round, then end 'partial' with the errors reported.",
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
output_mode: Type.Optional({ ...OutputMode, description: "file-only returns a pointer instead of inline text; use for large reports." }),
|
|
78
|
+
resume: Type.Optional(Type.String({ description: "Child session id to continue." })),
|
|
79
|
+
fork_resume: Type.Optional(Type.Boolean({ description: "Fork the resumed session instead of direct resume." })),
|
|
80
|
+
isolation: Type.Optional({ ...Isolation, description: "worktree runs the task in an isolated git worktree; changed work is preserved on a branch." }),
|
|
81
|
+
include_wip: Type.Optional(
|
|
82
|
+
Type.Boolean({ description: "Seed a worktree with the parent checkout's uncommitted changes (staged + unstaged + untracked). Only valid with isolation:'worktree'." }),
|
|
83
|
+
),
|
|
84
|
+
allow_shared_writes: Type.Optional(
|
|
85
|
+
Type.Boolean({ description: "Unsafe opt-in for parallel writers sharing one checkout." }),
|
|
86
|
+
),
|
|
87
|
+
keep_background: Type.Optional(
|
|
88
|
+
Type.Boolean({ description: "Keep processes the child backgrounded (e.g. dev servers) alive after a clean exit." }),
|
|
89
|
+
),
|
|
90
|
+
} as const;
|
|
91
|
+
|
|
92
|
+
export const ParallelTaskItem = Type.Object(
|
|
93
|
+
{
|
|
94
|
+
task: Type.String({ minLength: 1, description: "Task text for one parallel worker." }),
|
|
95
|
+
...TaskFields,
|
|
96
|
+
},
|
|
97
|
+
{ additionalProperties: false },
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Provider-facing tool parameters.
|
|
102
|
+
*
|
|
103
|
+
* IMPORTANT: LLM tool APIs (OpenAI-compatible / OpenRouter / Anthropic via many
|
|
104
|
+
* providers) require the top-level parameters schema to be JSON Schema
|
|
105
|
+
* `type: "object"`. A Type.Union serializes as `anyOf` without `type: "object"`,
|
|
106
|
+
* which surfaces as:
|
|
107
|
+
* Invalid schema for function 'subagent': schema must be a JSON Schema of
|
|
108
|
+
* 'type: "object"', got 'type: "None"'.
|
|
109
|
+
*
|
|
110
|
+
* Mode exclusivity (action vs task vs tasks) is enforced in policy validation,
|
|
111
|
+
* not at the JSON Schema layer.
|
|
112
|
+
*/
|
|
113
|
+
export const SubagentParamsSchema = Type.Object(
|
|
114
|
+
{
|
|
115
|
+
// Management actions
|
|
116
|
+
action: Type.Optional({ ...Action, description: "Management action on an existing run: status, wait, cancel, steer (inject guidance into a running child), diff/apply/discard (worktree results). action:\"plan\" dry-runs validation + preflight with task/tasks — no spawn." }),
|
|
117
|
+
id: Type.Optional(Type.String({ minLength: 1, description: "Run id (or unique prefix) for management actions." })),
|
|
118
|
+
message: Type.Optional(Type.String({ minLength: 1, description: "Steering message injected into the running child (action: steer)." })),
|
|
119
|
+
index: Type.Optional(Type.Number({ minimum: 0, description: "Task index within a parallel run for steer/diff/apply/discard. Defaults to the only eligible task." })),
|
|
120
|
+
|
|
121
|
+
// Single-task mode
|
|
122
|
+
task: Type.Optional(Type.String({ minLength: 1, description: "Task to delegate (single mode)." })),
|
|
123
|
+
...TaskFields,
|
|
124
|
+
async: Type.Optional(Type.Boolean({ description: "Run in the background and return a handle immediately." })),
|
|
125
|
+
|
|
126
|
+
// Parallel mode
|
|
127
|
+
tasks: Type.Optional(
|
|
128
|
+
Type.Array(ParallelTaskItem, {
|
|
129
|
+
minItems: 1,
|
|
130
|
+
maxItems: 8,
|
|
131
|
+
description: "Array of independent tasks for parallel mode. Parallel tasks default to the read-only explore profile; parallel writers need isolation:'worktree'.",
|
|
132
|
+
}),
|
|
133
|
+
),
|
|
134
|
+
synthesis: Type.Optional(
|
|
135
|
+
Type.String({
|
|
136
|
+
minLength: 1,
|
|
137
|
+
description: "Optional synthesis prompt for parallel mode: after all tasks finish, one read-only child folds their outputs using this instruction and its result is delivered first.",
|
|
138
|
+
}),
|
|
139
|
+
),
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
additionalProperties: false,
|
|
143
|
+
description: "Subagent request: single, parallel, status, wait, or cancel.",
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
export type SubagentParams = Static<typeof SubagentParamsSchema>;
|
|
148
|
+
export type ParallelTaskInput = Static<typeof ParallelTaskItem>;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Dedicated blocking-wait tool.
|
|
152
|
+
*
|
|
153
|
+
* `subagent` already exposes wait via `action: "wait"`, but collecting a
|
|
154
|
+
* background run is the one management step a model reaches for reflexively
|
|
155
|
+
* mid-flow, and burying it behind an action union costs a discovery step.
|
|
156
|
+
* This mirrors the highest-adoption package in the ecosystem (`pi-subagents`
|
|
157
|
+
* ships `subagent` + `subagent_wait` for the same reason). It is a thin
|
|
158
|
+
* front-end over the identical handler — no second delivery path.
|
|
159
|
+
*/
|
|
160
|
+
export const SubagentWaitParamsSchema = Type.Object(
|
|
161
|
+
{
|
|
162
|
+
id: Type.String({ minLength: 1, description: "Run id (or unique prefix) of the background run to collect." }),
|
|
163
|
+
timeout_ms: Type.Optional(
|
|
164
|
+
Type.Number({
|
|
165
|
+
minimum: 1,
|
|
166
|
+
maximum: 24 * 60 * 60_000,
|
|
167
|
+
description: "Give up waiting after this long and return a still-running notice. The run is NOT cancelled; collect it later with subagent_wait or action:'status'. Omit to wait until the run settles.",
|
|
168
|
+
}),
|
|
169
|
+
),
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
additionalProperties: false,
|
|
173
|
+
description: "Block until a background subagent run settles, then deliver its output.",
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
/** Provider-facing projections without TypeBox's internal `~...` metadata. */
|
|
178
|
+
export const ProviderSubagentParamsSchema = sanitizeProviderSchema(SubagentParamsSchema);
|
|
179
|
+
export const ProviderSubagentWaitParamsSchema = sanitizeProviderSchema(SubagentWaitParamsSchema);
|
|
180
|
+
|
|
181
|
+
export type SubagentWaitParams = Static<typeof SubagentWaitParamsSchema>;
|
|
182
|
+
|
|
183
|
+
/** Runtime guard used by tests/docs to assert provider compatibility. */
|
|
184
|
+
export function assertObjectToolSchema(schema: unknown): asserts schema is { type: "object" } {
|
|
185
|
+
if (!schema || typeof schema !== "object" || (schema as { type?: unknown }).type !== "object") {
|
|
186
|
+
const type = schema && typeof schema === "object" ? (schema as { type?: unknown }).type : typeof schema;
|
|
187
|
+
throw new Error(`Tool parameters must be JSON Schema type "object", got ${JSON.stringify(type ?? "None")}`);
|
|
188
|
+
}
|
|
189
|
+
}
|