@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.
@@ -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
- isRateLimitError,
7
- validateAgentTools,
8
- normalizeTimeout,
9
- resolveSafeCwd,
10
- MAX_INSTRUCTIONS_LENGTH,
11
- READ_ONLY_TOOLS,
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
- 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;
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
- | { id: string; ok: true; result: SubAgentResult }
32
- | { id: string; ok: false; error: string };
31
+ | { id: string; ok: true; result: SubAgentResult }
32
+ | { id: string; ok: false; error: string };
33
33
 
34
34
  export async function runNamedAgent(options: {
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;
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
- 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"})`);
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
- const modelRegistry = options.ctx.modelRegistry;
49
- const modelRuntime = (modelRegistry as any).runtime;
50
- const authStorage = (modelRegistry as any).authStorage;
48
+ const modelRegistry = options.ctx.modelRegistry;
49
+ const modelRuntime = (modelRegistry as any).runtime;
50
+ const authStorage = (modelRegistry as any).authStorage;
51
51
 
52
- // Security: validate and normalise timeout.
53
- const effectiveTimeoutMs = normalizeTimeout({ requested: options.timeout }).timeoutMs;
52
+ // Security: validate and normalise timeout.
53
+ const effectiveTimeoutMs = normalizeTimeout({ requested: options.timeout }).timeoutMs;
54
54
 
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
- }
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
- // 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
- }
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
- const contract = options.instructions?.slice(0, MAX_INSTRUCTIONS_LENGTH);
74
+ const contract = options.instructions?.slice(0, MAX_INSTRUCTIONS_LENGTH);
75
75
 
76
- // Retry loop: rate-limit model fallback
77
- const candidates = getModelCandidates(options.agent);
78
- const triedModels: string[] = [];
76
+ // Retry loop: rate-limit model fallback
77
+ const candidates = getModelCandidates(options.agent);
78
+ const triedModels: string[] = [];
79
79
 
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
- }
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
- 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
- });
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
- 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
- };
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
- try {
138
- return await tryWithFallback();
139
- } finally {
140
- // No manual timeout handling needed — runSubAgent handles timeouts internally.
141
- }
137
+ try {
138
+ return await tryWithFallback();
139
+ } finally {
140
+ // No manual timeout handling needed — runSubAgent handles timeouts internally.
141
+ }
142
142
  }