pi-workflow-engine 0.8.0 → 0.9.0
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/.pi/extensions/pi-workflow-engine/index.ts +2 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +36 -8
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +222 -0
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +1 -0
- package/.pi/extensions/pi-workflow-engine/src/types.ts +7 -0
- package/README.md +1 -0
- package/USAGE.md +3 -0
- package/package.json +1 -1
|
@@ -302,6 +302,7 @@ Use the workflow tool with a script argument, not a saved workflow name.
|
|
|
302
302
|
The script must start with export const meta = { ... } and default-export an async workflow function.
|
|
303
303
|
Use the injected Type object for schemas. Do not import anything or use dynamic import().
|
|
304
304
|
Set thinkingLevel explicitly on each agent() call.
|
|
305
|
+
Subagents receive no skills by default. When the brief asks for a skill or a stage clearly benefits from one, pass \`skills: ["skill-name"]\` on that agent call only.
|
|
305
306
|
Do not edit files unless the user explicitly requested edits.`;
|
|
306
307
|
}
|
|
307
308
|
|
|
@@ -487,6 +488,7 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
487
488
|
"Use workflow with `script` for a new one-off inline workflow; the script must start with `export const meta = { ... }` and default-export an async workflow function.",
|
|
488
489
|
"Inline workflow scripts must use the injected `Type` object for schemas and must not contain imports or dynamic import().",
|
|
489
490
|
"Inline scripts may compose registered workflows in-process via `api.workflow(\"<name>\", args)` (e.g. `await api.workflow(\"code-review\", \"HEAD~3\")`); it returns the sub-workflow's result and nests one level only.",
|
|
491
|
+
"Subagents receive no skills by default. In inline workflows, pass `skills: [\"skill-name\"]` per `agent()` call when the user asks for a skill or a stage should use one; grant only the needed skills.",
|
|
490
492
|
"Every workflow tool call must provide exactly one of `name` or `script`, never both.",
|
|
491
493
|
],
|
|
492
494
|
parameters: Type.Object({
|
|
@@ -6,6 +6,7 @@ import type { Semaphore } from "./concurrency.ts";
|
|
|
6
6
|
import type { PerfSink } from "./perf.ts";
|
|
7
7
|
import type { WorkflowUsageSink } from "./usage.ts";
|
|
8
8
|
import { throwIfAborted } from "./cancellation.ts";
|
|
9
|
+
import { appendSkillReminder, createAgentSkillResourceLoader, extractSkillSelectorsFromText } from "./agent-skills.ts";
|
|
9
10
|
|
|
10
11
|
/** Name of the synthetic terminating tool that carries structured output. */
|
|
11
12
|
const FINAL_TOOL = "final_answer";
|
|
@@ -156,6 +157,20 @@ function linkSessionAbort(signal: AbortSignal | undefined, session: AgentRunnerS
|
|
|
156
157
|
return () => signal.removeEventListener("abort", onAbort);
|
|
157
158
|
}
|
|
158
159
|
|
|
160
|
+
function buildToolAllowlist(opts: AgentOptions, skillsEnabled: boolean): string[] | undefined {
|
|
161
|
+
if (!opts.tools) return undefined;
|
|
162
|
+
const allow = [...opts.tools];
|
|
163
|
+
if (skillsEnabled && !allow.includes("read")) allow.push("read");
|
|
164
|
+
if (opts.schema && !allow.includes(FINAL_TOOL)) allow.push(FINAL_TOOL);
|
|
165
|
+
return allow;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function shouldCreateSkillResourceLoader(rc: RunContext, prompt: string, opts: AgentOptions): boolean {
|
|
169
|
+
if (!rc.createSession) return true;
|
|
170
|
+
if (opts.skills !== undefined) return true;
|
|
171
|
+
return extractSkillSelectorsFromText(prompt).length > 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
159
174
|
/**
|
|
160
175
|
* Run one subagent to completion in an isolated in-memory session.
|
|
161
176
|
*
|
|
@@ -209,14 +224,25 @@ export async function runAgent(rc: RunContext, prompt: string, opts: AgentOption
|
|
|
209
224
|
: [];
|
|
210
225
|
|
|
211
226
|
try {
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
?
|
|
215
|
-
|
|
216
|
-
|
|
227
|
+
const { model } = resolveAgentModel(opts.model, rc.modelRegistry, rc.hostModel);
|
|
228
|
+
const skillSetup = shouldCreateSkillResourceLoader(rc, prompt, opts)
|
|
229
|
+
? await rc.perf.time(
|
|
230
|
+
"agent.skills_ms",
|
|
231
|
+
() =>
|
|
232
|
+
createAgentSkillResourceLoader({
|
|
233
|
+
cwd: rc.cwd,
|
|
234
|
+
prompt,
|
|
235
|
+
skills: opts.skills,
|
|
236
|
+
log: (message) => rc.progress.log(`${label}: ${message}`),
|
|
237
|
+
}),
|
|
238
|
+
tags,
|
|
239
|
+
)
|
|
217
240
|
: undefined;
|
|
241
|
+
const selectedSkills = skillSetup?.selectedSkills ?? [];
|
|
242
|
+
// When the workflow restricts built-in tools, keep the terminating tool visible.
|
|
243
|
+
// Skill opt-ins also need read so the subagent can load SKILL.md on demand.
|
|
244
|
+
const allow = buildToolAllowlist(opts, selectedSkills.length > 0);
|
|
218
245
|
|
|
219
|
-
const { model } = resolveAgentModel(opts.model, rc.modelRegistry, rc.hostModel);
|
|
220
246
|
const createSessionForRun = rc.createSession ?? defaultCreateSession;
|
|
221
247
|
|
|
222
248
|
throwIfAborted(rc.signal);
|
|
@@ -230,6 +256,7 @@ export async function runAgent(rc: RunContext, prompt: string, opts: AgentOption
|
|
|
230
256
|
thinkingLevel: opts.thinkingLevel,
|
|
231
257
|
tools: allow,
|
|
232
258
|
customTools,
|
|
259
|
+
resourceLoader: skillSetup?.resourceLoader,
|
|
233
260
|
sessionManager: SessionManager.inMemory(rc.cwd),
|
|
234
261
|
}),
|
|
235
262
|
tags,
|
|
@@ -244,9 +271,10 @@ export async function runAgent(rc: RunContext, prompt: string, opts: AgentOption
|
|
|
244
271
|
}
|
|
245
272
|
});
|
|
246
273
|
|
|
274
|
+
const promptedWithSkills = appendSkillReminder(prompt, selectedSkills);
|
|
247
275
|
const finalPrompt = opts.schema
|
|
248
|
-
? `${
|
|
249
|
-
:
|
|
276
|
+
? `${promptedWithSkills}\n\nWhen finished, return your result by calling the ${FINAL_TOOL} tool.`
|
|
277
|
+
: promptedWithSkills;
|
|
250
278
|
throwIfAborted(rc.signal);
|
|
251
279
|
const unlinkPromptAbort = linkSessionAbort(rc.signal, activeSession);
|
|
252
280
|
try {
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { DefaultResourceLoader, getAgentDir, type ResourceDiagnostic, type Skill } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export type AgentSkillOption = readonly string[] | undefined;
|
|
4
|
+
export type AgentSkillRequestSource = "explicit" | "prompt";
|
|
5
|
+
|
|
6
|
+
export interface AgentSkillRequest {
|
|
7
|
+
readonly selectors: readonly string[];
|
|
8
|
+
readonly source: AgentSkillRequestSource;
|
|
9
|
+
readonly strict: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface AgentSkillResolution {
|
|
13
|
+
readonly selected: readonly Skill[];
|
|
14
|
+
readonly unmatched: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AgentSkillResourceSetup {
|
|
18
|
+
readonly resourceLoader: DefaultResourceLoader;
|
|
19
|
+
readonly selectedSkills: readonly Skill[];
|
|
20
|
+
readonly unmatchedSelectors: readonly string[];
|
|
21
|
+
readonly diagnostics: readonly ResourceDiagnostic[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SLASH_SKILL_PATTERN = /\/skill:([a-z0-9][a-z0-9-]{0,63})/gi;
|
|
25
|
+
const SKILLS_FIELD_PATTERN = /(?<!\/)\bskills?\s*[:=]\s*([^\n.;]+)/gi;
|
|
26
|
+
const VERB_SKILL_LIST_PATTERN = /\b(?:include|use|enable|load|provide|allow|grant|with)\s+(?:the\s+|this\s+|these\s+)?skills?\s+([^\n.;]+)/gi;
|
|
27
|
+
const VERB_NAMED_SKILL_PATTERN = /\b(?:include|use|enable|load|provide|allow|grant|with)\s+(?:the\s+|this\s+|these\s+)?(["'`]?)([a-z0-9][a-z0-9 _-]{0,80}?)\1\s+skills?\b/gi;
|
|
28
|
+
const PURPOSE_BOUNDARY_PATTERN = /\s+\b(?:for|to|when|while|so|because|in\s+order\s+to)\b.*$/i;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve which subagent skills were requested.
|
|
32
|
+
*
|
|
33
|
+
* Subagents are skillless by default. Explicit `opts.skills` wins and suppresses prompt inference;
|
|
34
|
+
* when it is omitted we only infer from clear natural-language opt-ins such as `/skill:name`,
|
|
35
|
+
* `include skill diagnose`, `include this Skill diagnose`, or `use the diagnose skill`.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveAgentSkillRequest(prompt: string, explicitSkills: unknown): AgentSkillRequest {
|
|
38
|
+
if (explicitSkills !== undefined) {
|
|
39
|
+
return { selectors: normalizeExplicitSkillSelectors(explicitSkills), source: "explicit", strict: true };
|
|
40
|
+
}
|
|
41
|
+
return { selectors: extractSkillSelectorsFromText(prompt), source: "prompt", strict: false };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function extractSkillSelectorsFromText(text: string): string[] {
|
|
45
|
+
const selectors: string[] = [];
|
|
46
|
+
collectPatternMatches(text, SLASH_SKILL_PATTERN, selectors, 1);
|
|
47
|
+
collectListPatternMatches(text, SKILLS_FIELD_PATTERN, selectors);
|
|
48
|
+
collectListPatternMatches(text, VERB_SKILL_LIST_PATTERN, selectors);
|
|
49
|
+
collectPatternMatches(text, VERB_NAMED_SKILL_PATTERN, selectors, 2);
|
|
50
|
+
return uniqueSelectors(selectors);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function normalizeSkillSelector(value: string): string {
|
|
54
|
+
return value
|
|
55
|
+
.trim()
|
|
56
|
+
.replace(/^\/skill:/i, "")
|
|
57
|
+
.replace(/^skills?\s*[:=]?\s*/i, "")
|
|
58
|
+
.replace(/^(?:named|called)\s+/i, "")
|
|
59
|
+
.replace(/^(?:the|this|these)\s+/i, "")
|
|
60
|
+
.replace(/\s+skills?$/i, "")
|
|
61
|
+
.replace(/^[`"']+|[`"']+$/g, "")
|
|
62
|
+
.trim()
|
|
63
|
+
.toLowerCase()
|
|
64
|
+
.replace(/[\s_]+/g, "-")
|
|
65
|
+
.replace(/[^a-z0-9-]/g, "")
|
|
66
|
+
.replace(/-+/g, "-")
|
|
67
|
+
.replace(/^-|-$/g, "");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function selectAgentSkills(skills: readonly Skill[], selectors: readonly string[]): AgentSkillResolution {
|
|
71
|
+
const selected = new Map<string, Skill>();
|
|
72
|
+
const unmatched: string[] = [];
|
|
73
|
+
|
|
74
|
+
for (const selector of selectors) {
|
|
75
|
+
const match = matchSkillSelector(skills, selector);
|
|
76
|
+
if (!match) {
|
|
77
|
+
unmatched.push(selector);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
selected.set(match.name, makeSkillModelInvocable(match));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { selected: [...selected.values()], unmatched };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function createAgentSkillResourceLoader(options: {
|
|
87
|
+
readonly cwd: string;
|
|
88
|
+
readonly prompt: string;
|
|
89
|
+
readonly skills: unknown;
|
|
90
|
+
readonly log?: (message: string) => void;
|
|
91
|
+
}): Promise<AgentSkillResourceSetup> {
|
|
92
|
+
const request = resolveAgentSkillRequest(options.prompt, options.skills);
|
|
93
|
+
let resolution: AgentSkillResolution = { selected: [], unmatched: [] };
|
|
94
|
+
let availableSkillNames: readonly string[] = [];
|
|
95
|
+
|
|
96
|
+
const loader = new DefaultResourceLoader({
|
|
97
|
+
cwd: options.cwd,
|
|
98
|
+
agentDir: getAgentDir(),
|
|
99
|
+
noSkills: request.selectors.length === 0,
|
|
100
|
+
skillsOverride:
|
|
101
|
+
request.selectors.length === 0
|
|
102
|
+
? undefined
|
|
103
|
+
: (current) => {
|
|
104
|
+
availableSkillNames = current.skills.map((skill) => skill.name).sort();
|
|
105
|
+
resolution = selectAgentSkills(current.skills, request.selectors);
|
|
106
|
+
return { skills: [...resolution.selected], diagnostics: current.diagnostics };
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
await loader.reload();
|
|
110
|
+
const { diagnostics } = loader.getSkills();
|
|
111
|
+
logSkillDiagnostics(options.log, diagnostics, request.selectors.length > 0);
|
|
112
|
+
|
|
113
|
+
if (request.strict && resolution.unmatched.length > 0) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Unknown subagent skill${resolution.unmatched.length === 1 ? "" : "s"}: ${resolution.unmatched.join(", ")}. Available: ${
|
|
116
|
+
availableSkillNames.join(", ") || "(none)"
|
|
117
|
+
}.`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!request.strict && resolution.unmatched.length > 0) {
|
|
122
|
+
options.log?.(`Ignoring unrecognized subagent skill mention${resolution.unmatched.length === 1 ? "" : "s"}: ${resolution.unmatched.join(", ")}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { resourceLoader: loader, selectedSkills: resolution.selected, unmatchedSelectors: resolution.unmatched, diagnostics };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function appendSkillReminder(prompt: string, skills: readonly Pick<Skill, "name" | "filePath">[]): string {
|
|
129
|
+
if (skills.length === 0) return prompt;
|
|
130
|
+
const skillList = skills.map((skill) => `${skill.name} (${skill.filePath})`).join(", ");
|
|
131
|
+
return `${prompt}\n\nWorkflow subagent skills enabled: ${skillList}. If you rely on an enabled skill, first read its SKILL.md with the read tool and follow its instructions. No other skills are available in this subagent.`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function collectPatternMatches(text: string, pattern: RegExp, selectors: string[], captureIndex: number): void {
|
|
135
|
+
pattern.lastIndex = 0;
|
|
136
|
+
for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {
|
|
137
|
+
const value = match[captureIndex];
|
|
138
|
+
if (value !== undefined) selectors.push(value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function collectListPatternMatches(text: string, pattern: RegExp, selectors: string[]): void {
|
|
143
|
+
pattern.lastIndex = 0;
|
|
144
|
+
for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {
|
|
145
|
+
const value = match[1];
|
|
146
|
+
if (value === undefined) continue;
|
|
147
|
+
selectors.push(...parseSkillList(value));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseSkillList(fragment: string): string[] {
|
|
152
|
+
const scoped = fragment.replace(PURPOSE_BOUNDARY_PATTERN, "");
|
|
153
|
+
return scoped
|
|
154
|
+
.split(/\s*(?:,|\s+\band\b\s+|&|\+)\s*/i)
|
|
155
|
+
.map((part) => part.trim())
|
|
156
|
+
.filter((part) => part.length > 0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function normalizeExplicitSkillSelectors(value: unknown): string[] {
|
|
160
|
+
if (!Array.isArray(value)) {
|
|
161
|
+
throw new Error("Invalid subagent skills option: expected an array of skill names.");
|
|
162
|
+
}
|
|
163
|
+
const seen = new Set<string>();
|
|
164
|
+
const result: string[] = [];
|
|
165
|
+
for (const item of value) {
|
|
166
|
+
if (typeof item !== "string") {
|
|
167
|
+
throw new Error("Invalid subagent skills option: every skill name must be a string.");
|
|
168
|
+
}
|
|
169
|
+
const normalized = normalizeSkillSelector(item);
|
|
170
|
+
if (!normalized || isGenericSkillWord(normalized)) {
|
|
171
|
+
throw new Error(`Invalid subagent skills option: \"${item}\" is not a valid skill name.`);
|
|
172
|
+
}
|
|
173
|
+
if (seen.has(normalized)) continue;
|
|
174
|
+
seen.add(normalized);
|
|
175
|
+
result.push(normalized);
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function logSkillDiagnostics(log: ((message: string) => void) | undefined, diagnostics: readonly ResourceDiagnostic[], requested: boolean): void {
|
|
181
|
+
if (!requested || !log) return;
|
|
182
|
+
for (const diagnostic of diagnostics) {
|
|
183
|
+
const where = diagnostic.path ? ` (${diagnostic.path})` : "";
|
|
184
|
+
log(`skill ${diagnostic.type}: ${diagnostic.message}${where}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function uniqueSelectors(values: Iterable<string>): string[] {
|
|
189
|
+
const seen = new Set<string>();
|
|
190
|
+
const result: string[] = [];
|
|
191
|
+
for (const value of values) {
|
|
192
|
+
const normalized = normalizeSkillSelector(value);
|
|
193
|
+
if (!normalized || isGenericSkillWord(normalized) || seen.has(normalized)) continue;
|
|
194
|
+
seen.add(normalized);
|
|
195
|
+
result.push(normalized);
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isGenericSkillWord(value: string): boolean {
|
|
201
|
+
return value === "skill" || value === "skills" || value === "the" || value === "this" || value === "these" || value === "that";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function matchSkillSelector(skills: readonly Skill[], selector: string): Skill | undefined {
|
|
205
|
+
const normalized = normalizeSkillSelector(selector);
|
|
206
|
+
if (!normalized) return undefined;
|
|
207
|
+
|
|
208
|
+
const exact = skills.find((skill) => skill.name.toLowerCase() === normalized);
|
|
209
|
+
if (exact) return exact;
|
|
210
|
+
|
|
211
|
+
const selectorWords = normalized.replace(/-/g, " ");
|
|
212
|
+
const fuzzy = skills.filter((skill) => {
|
|
213
|
+
const name = skill.name.toLowerCase();
|
|
214
|
+
const words = name.replace(/-/g, " ");
|
|
215
|
+
return name.includes(normalized) || words.includes(selectorWords);
|
|
216
|
+
});
|
|
217
|
+
return fuzzy.length === 1 ? fuzzy[0] : undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function makeSkillModelInvocable(skill: Skill): Skill {
|
|
221
|
+
return skill.disableModelInvocation ? { ...skill, disableModelInvocation: false } : skill;
|
|
222
|
+
}
|
|
@@ -32,6 +32,7 @@ Inline workflow rules:
|
|
|
32
32
|
- Use the injected Type object for schemas, for example Type.Object({ ok: Type.Boolean() }); do not import typebox.
|
|
33
33
|
- Do not use import statements or dynamic import() in inline workflow scripts.
|
|
34
34
|
- Set thinkingLevel explicitly on each agent() call so fan-out remains bounded.
|
|
35
|
+
- Subagents receive no skills by default. Add \`skills: ["skill-name"]\` per agent only when that stage should load that skill; grant the smallest useful set.
|
|
35
36
|
- Provide exactly one of workflow.name or workflow.script.
|
|
36
37
|
`;
|
|
37
38
|
|
|
@@ -79,6 +79,13 @@ export interface AgentOptions<S extends TSchema = TSchema> {
|
|
|
79
79
|
thinkingLevel?: ThinkingLevel;
|
|
80
80
|
/** Allowlist of built-in tool names the agent may use (e.g. ["read", "bash"]). */
|
|
81
81
|
tools?: string[];
|
|
82
|
+
/**
|
|
83
|
+
* Skill names to expose to this subagent. Subagents receive no skills by default.
|
|
84
|
+
* When omitted, clear prompt text such as `/skill:name`, `include skill name`, or
|
|
85
|
+
* `use the name skill` is treated as a natural-language opt-in. Pass [] to suppress
|
|
86
|
+
* prompt inference for prompts that mention skills only as subject matter.
|
|
87
|
+
*/
|
|
88
|
+
skills?: readonly string[];
|
|
82
89
|
/** typebox schema. When set, the agent is forced to return matching structured data. */
|
|
83
90
|
schema?: S;
|
|
84
91
|
}
|
package/README.md
CHANGED
|
@@ -205,6 +205,7 @@ Inline workflows are passed to the `workflow` tool as a script string. They are
|
|
|
205
205
|
- A single run-level semaphore caps concurrent agents, so nested `parallel`, `pipeline`, and `workflow()` calls stay bounded.
|
|
206
206
|
- Built-in workflows stay statically imported so they share pi's bundled `typebox` identity.
|
|
207
207
|
- Set `thinkingLevel` per fan-out agent. Otherwise many subagents can inherit an expensive global reasoning level.
|
|
208
|
+
- Subagents receive no skills by default. Opt in per agent with `skills: ["skill-name"]`; clear prompt text like `include skill name` also works when `skills` is omitted.
|
|
208
209
|
- Set `model` per agent only when needed: bare ids are Anthropic shorthand, and `provider/id` targets other providers.
|
|
209
210
|
Omitted models inherit the host/session default; malformed or unknown explicit refs fail fast.
|
|
210
211
|
|
package/USAGE.md
CHANGED
|
@@ -146,6 +146,8 @@ Core primitives:
|
|
|
146
146
|
|
|
147
147
|
Set `thinkingLevel` on fan-out agents. Otherwise many subagents can inherit an expensive global reasoning level.
|
|
148
148
|
|
|
149
|
+
Subagents receive no skills by default. Opt in per agent with `skills: ["skill-name"]`; if `tools` is also restricted, the engine automatically keeps `read` available so the subagent can load the selected `SKILL.md`. When `skills` is omitted, clear prompt text such as `/skill:name`, `include skill name`, or `use the name skill` is also treated as an opt-in. Pass `skills: []` to suppress that inference.
|
|
150
|
+
|
|
149
151
|
Set `model` only when a subagent should use a specific model. Bare ids keep the Anthropic shorthand; `provider/id` targets built-in, custom, or local providers. Omitted models inherit the host/session default; malformed or unknown explicit refs fail fast.
|
|
150
152
|
|
|
151
153
|
### Compose workflows
|
|
@@ -203,6 +205,7 @@ Inline rules:
|
|
|
203
205
|
- default-export an async workflow function;
|
|
204
206
|
- no imports or dynamic `import()`;
|
|
205
207
|
- use the injected `Type` object for schemas;
|
|
208
|
+
- add `skills: ["skill-name"]` per `agent()` call when a subagent should use a pi skill; no skills are exposed by default;
|
|
206
209
|
- no extra exports or code after the default export.
|
|
207
210
|
|
|
208
211
|
Inline workflows run in-process with extension permissions, so treat them as trusted task automation.
|