@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/security.ts
CHANGED
|
@@ -14,13 +14,13 @@ import * as path from "node:path";
|
|
|
14
14
|
|
|
15
15
|
/** Tools that child agents may use. The subagent tool is never included. */
|
|
16
16
|
export const ALLOWED_CHILD_TOOLS = [
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
"read",
|
|
18
|
+
"grep",
|
|
19
|
+
"find",
|
|
20
|
+
"ls",
|
|
21
|
+
"bash",
|
|
22
|
+
"edit",
|
|
23
|
+
"write",
|
|
24
24
|
] as const;
|
|
25
25
|
|
|
26
26
|
export const READ_ONLY_TOOLS: readonly string[] = ["read", "grep", "find", "ls"];
|
|
@@ -53,11 +53,11 @@ const PARTIAL_REASONS = new Set(["length", "max_tokens", "context_limit"]);
|
|
|
53
53
|
|
|
54
54
|
/** Pi SDK stop reasons that indicate a provider or tool error. */
|
|
55
55
|
const ERROR_REASONS = new Set([
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
"error",
|
|
57
|
+
"tool_error",
|
|
58
|
+
"authentication_error",
|
|
59
|
+
"provider_error",
|
|
60
|
+
"content_filter",
|
|
61
61
|
]);
|
|
62
62
|
|
|
63
63
|
/**
|
|
@@ -68,18 +68,18 @@ const ERROR_REASONS = new Set([
|
|
|
68
68
|
* from their own abort controllers.
|
|
69
69
|
*/
|
|
70
70
|
export function classifyStopReason(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
reason: string | undefined,
|
|
72
|
+
isAborted: boolean,
|
|
73
|
+
isTimeout: boolean,
|
|
74
74
|
): SubagentStatus {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
if (isAborted) return "aborted";
|
|
76
|
+
if (isTimeout) return "timeout";
|
|
77
|
+
if (!reason) return "success";
|
|
78
|
+
if (SUCCESS_REASONS.has(reason)) return "success";
|
|
79
|
+
if (PARTIAL_REASONS.has(reason)) return "partial";
|
|
80
|
+
if (ERROR_REASONS.has(reason)) return "error";
|
|
81
|
+
// Unknown stop reason — classify conservatively.
|
|
82
|
+
return "error";
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// ---------------------------------------------------------------------------
|
|
@@ -87,20 +87,20 @@ export function classifyStopReason(
|
|
|
87
87
|
// ---------------------------------------------------------------------------
|
|
88
88
|
|
|
89
89
|
export interface SafeCwdOptions {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
/** The workspace root (parent session's cwd). Must be an absolute path. */
|
|
91
|
+
workspaceRoot: string;
|
|
92
|
+
/** The child's requested working directory, if any. */
|
|
93
|
+
childCwd?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Trusted user setting that allows child cwd outside the workspace.
|
|
96
|
+
* Must never come from the tool-calling model.
|
|
97
|
+
*/
|
|
98
|
+
allowExternalCwd?: boolean;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
export interface SafeCwdResult {
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
path: string;
|
|
103
|
+
error?: string;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/**
|
|
@@ -117,51 +117,51 @@ export interface SafeCwdResult {
|
|
|
117
117
|
* - Paths that are files instead of directories are rejected.
|
|
118
118
|
*/
|
|
119
119
|
export function resolveSafeCwd(options: SafeCwdOptions): SafeCwdResult {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
120
|
+
const { workspaceRoot, childCwd, allowExternalCwd } = options;
|
|
121
|
+
|
|
122
|
+
// Normalise workspace root to an absolute canonical path.
|
|
123
|
+
const resolvedRoot = resolveCanonical(workspaceRoot);
|
|
124
|
+
if (!resolvedRoot) {
|
|
125
|
+
return { path: "", error: `Workspace root does not exist: ${workspaceRoot}` };
|
|
126
|
+
}
|
|
127
|
+
if (!isDirectorySync(resolvedRoot)) {
|
|
128
|
+
return { path: "", error: `Workspace root is not a directory: ${workspaceRoot}` };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// No child cwd → use workspace root.
|
|
132
|
+
if (!childCwd) {
|
|
133
|
+
return { path: resolvedRoot };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Resolve the child path.
|
|
137
|
+
const absPath = path.resolve(resolvedRoot, childCwd);
|
|
138
|
+
|
|
139
|
+
// If the resolved path escapes the workspace via `..`, the resolved path
|
|
140
|
+
// will differ from the canonical workspace root prefix. We check by
|
|
141
|
+
// resolving the canonical absolute path.
|
|
142
|
+
const canonicalPath = resolveCanonical(absPath);
|
|
143
|
+
if (!canonicalPath) {
|
|
144
|
+
return { path: "", error: `Child working directory does not exist: ${childCwd}` };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!isDirectorySync(canonicalPath)) {
|
|
148
|
+
return { path: "", error: `Child working directory is a file, not a directory: ${childCwd}` };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check if the child path is inside the workspace.
|
|
152
|
+
if (!isPathInside(canonicalPath, resolvedRoot)) {
|
|
153
|
+
if (allowExternalCwd) {
|
|
154
|
+
return { path: canonicalPath };
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
path: "",
|
|
158
|
+
error:
|
|
159
|
+
`Child working directory "${childCwd}" is outside the workspace root "${workspaceRoot}". ` +
|
|
160
|
+
`Paths outside the workspace are rejected by default.`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { path: canonicalPath };
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
/**
|
|
@@ -169,34 +169,34 @@ export function resolveSafeCwd(options: SafeCwdOptions): SafeCwdResult {
|
|
|
169
169
|
* to canonical paths. Rejects `..` traversal that escapes `parent`.
|
|
170
170
|
*/
|
|
171
171
|
function isPathInside(child: string, parent: string): boolean {
|
|
172
|
-
|
|
173
|
-
|
|
172
|
+
// Both must be absolute.
|
|
173
|
+
if (!path.isAbsolute(child) || !path.isAbsolute(parent)) return false;
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
const relative = path.relative(parent, child);
|
|
176
|
+
// relative must not start with ".." and must not be an absolute path.
|
|
177
|
+
return !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
/**
|
|
181
181
|
* Resolve a path to its canonical real path, or return null if it doesn't exist.
|
|
182
182
|
*/
|
|
183
183
|
function resolveCanonical(p: string): string | null {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
184
|
+
try {
|
|
185
|
+
return fs.realpathSync(p);
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
/**
|
|
192
192
|
* Synchronously check if a path is a directory.
|
|
193
193
|
*/
|
|
194
194
|
function isDirectorySync(p: string): boolean {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
195
|
+
try {
|
|
196
|
+
return fs.statSync(p).isDirectory();
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
// ---------------------------------------------------------------------------
|
|
@@ -204,17 +204,17 @@ function isDirectorySync(p: string): boolean {
|
|
|
204
204
|
// ---------------------------------------------------------------------------
|
|
205
205
|
|
|
206
206
|
export interface ValidateToolsOptions {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
207
|
+
/** Tool names from the agent definition or service override. */
|
|
208
|
+
tools: string[];
|
|
209
|
+
/** When true, only read-only tools are permitted. Mutation/execution tools are rejected. */
|
|
210
|
+
readOnly?: boolean;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
export interface ValidateToolsResult {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
214
|
+
/** Deduplicated and validated tool names. */
|
|
215
|
+
tools: string[];
|
|
216
|
+
/** Validation errors, if any. Empty array means valid. */
|
|
217
|
+
errors: string[];
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
/**
|
|
@@ -225,43 +225,43 @@ export interface ValidateToolsResult {
|
|
|
225
225
|
* When `readOnly` is true, only READ_ONLY_TOOLS are permitted.
|
|
226
226
|
*/
|
|
227
227
|
export function validateAgentTools(options: ValidateToolsOptions): ValidateToolsResult {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
228
|
+
const { readOnly = false } = options;
|
|
229
|
+
const seen = new Set<string>();
|
|
230
|
+
const tools: string[] = [];
|
|
231
|
+
const errors: string[] = [];
|
|
232
|
+
|
|
233
|
+
for (const raw of options.tools) {
|
|
234
|
+
const tool = raw.trim();
|
|
235
|
+
if (!tool) continue;
|
|
236
|
+
|
|
237
|
+
// Reject subagent regardless of casing (Pi tool names are case-sensitive
|
|
238
|
+
// but "subagent" should never pass through).
|
|
239
|
+
if (tool.toLowerCase() === "subagent") {
|
|
240
|
+
errors.push(`Tool "subagent" is not allowed in child agents (recursive delegation is prevented).`);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Check against allowlist
|
|
245
|
+
if (!ALLOWED_CHILD_TOOLS.includes(tool as any)) {
|
|
246
|
+
errors.push(`Unknown tool "${tool}". Allowed tools: ${ALLOWED_CHILD_TOOLS.join(", ")}.`);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Check read-only constraint
|
|
251
|
+
if (readOnly && !READ_ONLY_TOOLS.includes(tool)) {
|
|
252
|
+
errors.push(
|
|
253
|
+
`Tool "${tool}" is not allowed in read-only mode. Read-only tools: ${READ_ONLY_TOOLS.join(", ")}.`,
|
|
254
|
+
);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Deduplicate
|
|
259
|
+
if (seen.has(tool)) continue;
|
|
260
|
+
seen.add(tool);
|
|
261
|
+
tools.push(tool);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { tools, errors };
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
// ---------------------------------------------------------------------------
|
|
@@ -269,19 +269,19 @@ export function validateAgentTools(options: ValidateToolsOptions): ValidateTools
|
|
|
269
269
|
// ---------------------------------------------------------------------------
|
|
270
270
|
|
|
271
271
|
export interface NormalizeTimeoutOptions {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
272
|
+
/** Requested timeout value from tool params, if any. */
|
|
273
|
+
requested?: number;
|
|
274
|
+
/** Global default when no timeout is specified. */
|
|
275
|
+
defaultValue?: number;
|
|
276
|
+
/** Absolute maximum allowed value. */
|
|
277
|
+
maxValue?: number;
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
export interface NormalizeTimeoutResult {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
281
|
+
/** The validated timeout in milliseconds, or undefined if none was set and no default applies. */
|
|
282
|
+
timeoutMs: number | undefined;
|
|
283
|
+
/** Error message if the value is invalid. */
|
|
284
|
+
error?: string;
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
/**
|
|
@@ -295,33 +295,33 @@ export interface NormalizeTimeoutResult {
|
|
|
295
295
|
* - Returns undefined only when no value and no default are configured.
|
|
296
296
|
*/
|
|
297
297
|
export function normalizeTimeout(options: NormalizeTimeoutOptions): NormalizeTimeoutResult {
|
|
298
|
-
|
|
298
|
+
const { requested, defaultValue = DEFAULT_TIMEOUT_MS, maxValue = MAX_TIMEOUT_MS } = options;
|
|
299
299
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
300
|
+
if (requested === undefined || requested === null) {
|
|
301
|
+
// No explicit timeout — apply default.
|
|
302
|
+
return { timeoutMs: defaultValue };
|
|
303
|
+
}
|
|
304
304
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
305
|
+
if (typeof requested !== "number" || !Number.isFinite(requested)) {
|
|
306
|
+
return { timeoutMs: undefined, error: "Timeout must be a finite number." };
|
|
307
|
+
}
|
|
308
308
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
309
|
+
if (!Number.isInteger(requested)) {
|
|
310
|
+
return { timeoutMs: undefined, error: "Timeout must be an integer (milliseconds)." };
|
|
311
|
+
}
|
|
312
312
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
313
|
+
if (requested <= 0) {
|
|
314
|
+
return { timeoutMs: undefined, error: "Timeout must be a positive integer." };
|
|
315
|
+
}
|
|
316
316
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
317
|
+
if (requested > maxValue) {
|
|
318
|
+
return {
|
|
319
|
+
timeoutMs: undefined,
|
|
320
|
+
error: `Timeout ${requested}ms exceeds maximum allowed ${maxValue}ms (${maxValue / 60_000} minutes).`,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
323
|
|
|
324
|
-
|
|
324
|
+
return { timeoutMs: requested };
|
|
325
325
|
}
|
|
326
326
|
|
|
327
327
|
// ---------------------------------------------------------------------------
|
|
@@ -339,57 +339,57 @@ export function normalizeTimeout(options: NormalizeTimeoutOptions): NormalizeTim
|
|
|
339
339
|
* Callers MUST call `cleanup()` in a `finally` block.
|
|
340
340
|
*/
|
|
341
341
|
export function createCombinedAbortSignal(
|
|
342
|
-
|
|
342
|
+
signals: (AbortSignal | undefined | null | false)[],
|
|
343
343
|
): { signal: AbortSignal; cleanup: () => void } {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
344
|
+
const valid = signals.filter(Boolean) as AbortSignal[];
|
|
345
|
+
|
|
346
|
+
if (valid.length === 0) {
|
|
347
|
+
const controller = new AbortController();
|
|
348
|
+
return { signal: controller.signal, cleanup: () => {} };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (valid.length === 1) {
|
|
352
|
+
return { signal: valid[0], cleanup: () => {} };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Check if any is already aborted.
|
|
356
|
+
for (const sig of valid) {
|
|
357
|
+
if (sig.aborted) {
|
|
358
|
+
const controller = new AbortController();
|
|
359
|
+
controller.abort(sig.reason);
|
|
360
|
+
return { signal: controller.signal, cleanup: () => {} };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Use native AbortSignal.any when available.
|
|
365
|
+
if (typeof (AbortSignal as any).any === "function") {
|
|
366
|
+
const combined = (AbortSignal as any).any(valid);
|
|
367
|
+
return { signal: combined, cleanup: () => {} };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Manual fallback: create a controller and forward all signals.
|
|
371
|
+
const controller = new AbortController();
|
|
372
|
+
const listeners: Array<() => void> = [];
|
|
373
|
+
|
|
374
|
+
for (const sig of valid) {
|
|
375
|
+
const handler = () => {
|
|
376
|
+
controller.abort(sig.reason);
|
|
377
|
+
};
|
|
378
|
+
sig.addEventListener("abort", handler, { once: true });
|
|
379
|
+
listeners.push(() => sig.removeEventListener("abort", handler));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const cleanup = () => {
|
|
383
|
+
for (const remove of listeners) {
|
|
384
|
+
try {
|
|
385
|
+
remove();
|
|
386
|
+
} catch {
|
|
387
|
+
// Best-effort cleanup.
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
return { signal: controller.signal, cleanup };
|
|
393
393
|
}
|
|
394
394
|
|
|
395
395
|
// ---------------------------------------------------------------------------
|
|
@@ -403,16 +403,16 @@ export const PER_TASK_OUTPUT_CAP = 50 * 1024; // 50 KB
|
|
|
403
403
|
export const MAX_INSTRUCTIONS_LENGTH = 16 * 1024; // 16 KB
|
|
404
404
|
|
|
405
405
|
export interface ValidateExecutionRequestOptions {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
406
|
+
agentName?: string;
|
|
407
|
+
task?: string;
|
|
408
|
+
tasks?: unknown[];
|
|
409
|
+
chain?: unknown[];
|
|
410
|
+
timeout?: number;
|
|
411
411
|
}
|
|
412
412
|
|
|
413
413
|
export interface ValidationError {
|
|
414
|
-
|
|
415
|
-
|
|
414
|
+
field: string;
|
|
415
|
+
message: string;
|
|
416
416
|
}
|
|
417
417
|
|
|
418
418
|
/**
|
|
@@ -421,85 +421,85 @@ export interface ValidationError {
|
|
|
421
421
|
* internal callers that bypass the public tool schema.
|
|
422
422
|
*/
|
|
423
423
|
export function validateExecutionRequest(
|
|
424
|
-
|
|
424
|
+
options: ValidateExecutionRequestOptions,
|
|
425
425
|
): ValidationError[] {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
426
|
+
const errors: ValidationError[] = [];
|
|
427
|
+
|
|
428
|
+
// Agent name
|
|
429
|
+
if (options.agentName !== undefined) {
|
|
430
|
+
if (typeof options.agentName !== "string" || options.agentName.trim().length === 0) {
|
|
431
|
+
errors.push({ field: "agent", message: "Agent name must be a non-empty string." });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Task
|
|
436
|
+
if (options.task !== undefined) {
|
|
437
|
+
if (typeof options.task !== "string") {
|
|
438
|
+
errors.push({ field: "task", message: "Task must be a string." });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Parallel tasks
|
|
443
|
+
if (options.tasks !== undefined) {
|
|
444
|
+
if (!Array.isArray(options.tasks)) {
|
|
445
|
+
errors.push({ field: "tasks", message: "Tasks must be an array." });
|
|
446
|
+
} else {
|
|
447
|
+
if (options.tasks.length > MAX_PARALLEL_TASKS) {
|
|
448
|
+
errors.push({
|
|
449
|
+
field: "tasks",
|
|
450
|
+
message: `Too many parallel tasks (${options.tasks.length}). Maximum is ${MAX_PARALLEL_TASKS}.`,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
for (let i = 0; i < options.tasks.length; i++) {
|
|
454
|
+
const t = options.tasks[i] as Record<string, unknown>;
|
|
455
|
+
if (!t || typeof t.agent !== "string" || !t.agent.trim()) {
|
|
456
|
+
errors.push({ field: `tasks[${i}].agent`, message: "Agent name must be a non-empty string." });
|
|
457
|
+
}
|
|
458
|
+
if (typeof t.task !== "string") {
|
|
459
|
+
errors.push({ field: `tasks[${i}].task`, message: "Task must be a string." });
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Chain
|
|
466
|
+
if (options.chain !== undefined) {
|
|
467
|
+
if (!Array.isArray(options.chain)) {
|
|
468
|
+
errors.push({ field: "chain", message: "Chain must be an array." });
|
|
469
|
+
} else {
|
|
470
|
+
if (options.chain.length > MAX_CHAIN_LENGTH) {
|
|
471
|
+
errors.push({
|
|
472
|
+
field: "chain",
|
|
473
|
+
message: `Too many chain steps (${options.chain.length}). Maximum is ${MAX_CHAIN_LENGTH}.`,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
for (let i = 0; i < options.chain.length; i++) {
|
|
477
|
+
const s = options.chain[i] as Record<string, unknown>;
|
|
478
|
+
if (!s || typeof s.agent !== "string" || !s.agent.trim()) {
|
|
479
|
+
errors.push({ field: `chain[${i}].agent`, message: "Agent name must be a non-empty string." });
|
|
480
|
+
}
|
|
481
|
+
if (typeof s.task !== "string") {
|
|
482
|
+
errors.push({ field: `chain[${i}].task`, message: "Task must be a string." });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return errors;
|
|
489
489
|
}
|
|
490
490
|
|
|
491
491
|
/**
|
|
492
492
|
* Truncate parallel task output to the per-task cap.
|
|
493
493
|
*/
|
|
494
494
|
export function truncateParallelOutput(output: string): string {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
495
|
+
const byteLength = Buffer.byteLength(output, "utf8");
|
|
496
|
+
if (byteLength <= PER_TASK_OUTPUT_CAP) return output;
|
|
497
|
+
|
|
498
|
+
let truncated = output.slice(0, PER_TASK_OUTPUT_CAP);
|
|
499
|
+
while (Buffer.byteLength(truncated, "utf8") > PER_TASK_OUTPUT_CAP) {
|
|
500
|
+
truncated = truncated.slice(0, -1);
|
|
501
|
+
}
|
|
502
|
+
return `${truncated}\n\n[Output truncated: ${byteLength - Buffer.byteLength(truncated, "utf8")} bytes omitted.]`;
|
|
503
503
|
}
|
|
504
504
|
|
|
505
505
|
// ---------------------------------------------------------------------------
|
|
@@ -507,19 +507,19 @@ export function truncateParallelOutput(output: string): string {
|
|
|
507
507
|
// ---------------------------------------------------------------------------
|
|
508
508
|
|
|
509
509
|
const RATE_LIMIT_PATTERNS = [
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
510
|
+
/\b429\b/,
|
|
511
|
+
/\b529\b/,
|
|
512
|
+
/rate[\s_]limit/i,
|
|
513
|
+
/ratelimit/i,
|
|
514
|
+
/too[\s_]many[\s_]requests/i,
|
|
515
|
+
/quota[\s_]exhausted/i,
|
|
516
|
+
/quota[\s_]exceeded/i,
|
|
517
|
+
/exceeded[\s_](?:your[\s_])?(?:current[\s_])?quota/i,
|
|
518
|
+
/insufficient_quota/i,
|
|
519
|
+
/resource[\s_]exhausted/i,
|
|
520
|
+
/capacity[\s_]exceeded/i,
|
|
521
|
+
/usage[\s_]limit/i,
|
|
522
|
+
/overloaded/i,
|
|
523
523
|
];
|
|
524
524
|
|
|
525
525
|
/**
|
|
@@ -529,5 +529,5 @@ const RATE_LIMIT_PATTERNS = [
|
|
|
529
529
|
* primary model candidate hits a 429 or similar server-side capacity error.
|
|
530
530
|
*/
|
|
531
531
|
export function isRateLimitError(message: string): boolean {
|
|
532
|
-
|
|
532
|
+
return RATE_LIMIT_PATTERNS.some(p => p.test(message));
|
|
533
533
|
}
|