@bacnh85/pi-subagent 0.10.1 → 0.12.2
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 +20 -0
- package/README.md +11 -9
- package/agent-format.md +4 -2
- package/agents/general-purpose.md +4 -2
- package/agents/planner.md +2 -1
- package/agents/reviewer.md +2 -1
- package/agents/scout.md +3 -3
- package/agents/tester.md +3 -3
- package/agents/worker.md +4 -2
- package/extensions/agents.ts +310 -310
- package/extensions/index.ts +1357 -1357
- package/extensions/model.ts +41 -41
- package/extensions/render.ts +205 -205
- package/extensions/runner.ts +200 -200
- package/extensions/security.ts +313 -313
- package/extensions/service.ts +115 -115
- package/extensions/thread-viewer.ts +246 -246
- package/extensions/threads.ts +99 -99
- package/package.json +9 -9
package/extensions/service.ts
CHANGED
|
@@ -3,140 +3,140 @@ import { type AgentConfig, getModelCandidates } from "./agents.ts";
|
|
|
3
3
|
import { runSubAgent, type SubAgentProgress, type SubAgentResult } from "./runner.ts";
|
|
4
4
|
import { resolveModel } from "./model.ts";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
isRateLimitError,
|
|
7
|
+
validateAgentTools,
|
|
8
|
+
normalizeTimeout,
|
|
9
|
+
resolveSafeCwd,
|
|
10
|
+
MAX_INSTRUCTIONS_LENGTH,
|
|
11
|
+
READ_ONLY_TOOLS,
|
|
12
12
|
} from "./security.ts";
|
|
13
13
|
|
|
14
14
|
export const SUBAGENT_REQUEST_EVENT = "pi-subagent:run";
|
|
15
15
|
|
|
16
16
|
export interface SubagentRunRequest {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
17
|
+
id: string;
|
|
18
|
+
agent: string;
|
|
19
|
+
task: string;
|
|
20
|
+
cwd?: string;
|
|
21
|
+
timeout?: number;
|
|
22
|
+
instructions?: string;
|
|
23
|
+
readOnly?: boolean;
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
accept?: () => boolean;
|
|
26
|
+
respond: (response: SubagentRunResponse) => void;
|
|
27
|
+
onProgress?: (progress: SubAgentProgress) => void;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export type SubagentRunResponse =
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
| { id: string; ok: true; result: SubAgentResult }
|
|
32
|
+
| { id: string; ok: false; error: string };
|
|
33
33
|
|
|
34
34
|
export async function runNamedAgent(options: {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
agent: AgentConfig;
|
|
36
|
+
task: string;
|
|
37
|
+
cwd: string;
|
|
38
|
+
ctx: ExtensionContext;
|
|
39
|
+
timeout?: number;
|
|
40
|
+
instructions?: string;
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
onMessage?: (result: SubAgentResult) => void;
|
|
43
|
+
onProgress?: (progress: SubAgentProgress) => void;
|
|
44
44
|
}): Promise<SubAgentResult> {
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
const { model, attempted } = await resolveModel(getModelCandidates(options.agent), options.ctx.model, options.ctx.modelRegistry);
|
|
46
|
+
if (!model) throw new Error(`No model resolved for agent "${options.agent.name}" (tried: ${attempted.join(", ") || "none"})`);
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
const modelRegistry = options.ctx.modelRegistry;
|
|
49
|
+
const modelRuntime = (modelRegistry as any).runtime;
|
|
50
|
+
const authStorage = (modelRegistry as any).authStorage;
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
// Security: validate and normalise timeout.
|
|
53
|
+
const effectiveTimeoutMs = normalizeTimeout({ requested: options.timeout }).timeoutMs;
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
55
|
+
// Security: validate tools against allowlist.
|
|
56
|
+
let rawTools = options.agent.tools ?? ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
57
|
+
// Enforce read-only sandbox: strip mutating and execution tools
|
|
58
|
+
if (options.agent.sandbox === "read-only") {
|
|
59
|
+
rawTools = rawTools.filter(t => READ_ONLY_TOOLS.includes(t));
|
|
60
|
+
if (rawTools.length === 0) rawTools = [...READ_ONLY_TOOLS];
|
|
61
|
+
}
|
|
62
|
+
const toolValidation = validateAgentTools({ tools: rawTools, readOnly: options.agent.sandbox === "read-only" });
|
|
63
|
+
if (toolValidation.errors.length > 0) {
|
|
64
|
+
throw new Error(`Tool validation errors for agent "${options.agent.name}": ${toolValidation.errors.join("; ")}`);
|
|
65
|
+
}
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
// Security: validate cwd (service caller must provide valid cwd).
|
|
68
|
+
// The service path uses the same policy as the tool path.
|
|
69
|
+
const safeCwd = resolveSafeCwd({ workspaceRoot: options.ctx.cwd, childCwd: options.cwd });
|
|
70
|
+
if (safeCwd.error) {
|
|
71
|
+
throw new Error(safeCwd.error);
|
|
72
|
+
}
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
const contract = options.instructions?.slice(0, MAX_INSTRUCTIONS_LENGTH);
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
// Retry loop: rate-limit model fallback
|
|
77
|
+
const candidates = getModelCandidates(options.agent);
|
|
78
|
+
const triedModels: string[] = [];
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
80
|
+
const tryWithFallback = async (): Promise<SubAgentResult> => {
|
|
81
|
+
const remaining = candidates.filter(m => !triedModels.includes(m));
|
|
82
|
+
const isParentFallback = remaining.length === 0;
|
|
83
|
+
const fallbackResolved = await resolveModel(remaining, options.ctx.model, options.ctx.modelRegistry);
|
|
84
|
+
if (!fallbackResolved.model) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`All models rate-limited or unavailable. Tried: ${triedModels.join(" → ") || "(none)"}. ` +
|
|
87
|
+
`Remaining candidates: ${remaining.join(", ") || "none"}. ` +
|
|
88
|
+
`Parent: ${options.ctx.model?.provider}/${options.ctx.model?.id}.`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const triedName = `${fallbackResolved.model!.provider}/${fallbackResolved.model!.id}`;
|
|
92
|
+
if (triedModels.includes(triedName)) {
|
|
93
|
+
// Already tried this model (e.g., all candidates unavailable
|
|
94
|
+
// and parent fallback) — no further options.
|
|
95
|
+
throw new Error(
|
|
96
|
+
`All available models exhausted. Tried: ${triedModels.join(" → ")}.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
triedModels.push(triedName);
|
|
100
|
+
// Also track the raw candidate name so candidates.filter() can
|
|
101
|
+
// exclude it even when the agent uses unqualified names.
|
|
102
|
+
// Avoid duplicating when candidate name is already qualified (matchedCandidate === triedName).
|
|
103
|
+
if (fallbackResolved.matchedCandidate && fallbackResolved.matchedCandidate !== triedName) {
|
|
104
|
+
triedModels.push(fallbackResolved.matchedCandidate);
|
|
105
|
+
}
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
107
|
+
const result = await runSubAgent({
|
|
108
|
+
cwd: safeCwd.path,
|
|
109
|
+
systemPrompt: contract ? `${options.agent.systemPrompt}\n\n## Task Contract\n${contract}` : options.agent.systemPrompt,
|
|
110
|
+
task: options.task,
|
|
111
|
+
tools: toolValidation.tools,
|
|
112
|
+
model: fallbackResolved.model,
|
|
113
|
+
modelRuntime,
|
|
114
|
+
authStorage,
|
|
115
|
+
modelRegistry,
|
|
116
|
+
signal: options.signal,
|
|
117
|
+
timeoutMs: effectiveTimeoutMs,
|
|
118
|
+
agentName: options.agent.name,
|
|
119
|
+
thinkingLevel: options.agent.thinking,
|
|
120
|
+
onMessage: options.onMessage,
|
|
121
|
+
onProgress: options.onProgress,
|
|
122
|
+
});
|
|
123
123
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
124
|
+
if (result.errorMessage && isRateLimitError(result.errorMessage)) {
|
|
125
|
+
// If the model that just rate-limited was the parent fallback
|
|
126
|
+
// (no remaining candidates), stop — no further options.
|
|
127
|
+
if (isParentFallback) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`All available models exhausted. Tried: ${triedModels.join(" → ")}.`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return tryWithFallback();
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
};
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
try {
|
|
138
|
+
return await tryWithFallback();
|
|
139
|
+
} finally {
|
|
140
|
+
// No manual timeout handling needed — runSubAgent handles timeouts internally.
|
|
141
|
+
}
|
|
142
142
|
}
|