@bermudi/pi-delegate 0.1.9 → 0.1.11
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/README.md +22 -7
- package/agents.ts +140 -14
- package/delegate.ts +3 -2
- package/dispatch.ts +26 -27
- package/extension.ts +2 -4
- package/format.ts +2 -2
- package/host-cache.ts +70 -0
- package/host.ts +164 -811
- package/lifecycle.ts +653 -508
- package/manual.ts +40 -10
- package/package.json +1 -1
- package/pi-package-source.ts +293 -0
- package/provider-extensions.ts +528 -0
- package/quiescence.ts +262 -0
- package/runner.ts +32 -140
- package/schema.ts +77 -153
- package/task-resolution.ts +60 -16
- package/ticket-format.ts +323 -0
- package/tickets.ts +79 -248
- package/tools.ts +12 -0
- package/trusted-paths.ts +71 -0
- package/types.ts +13 -17
package/schema.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type { DelegateArguments } from "./types.ts";
|
|
|
7
7
|
// `Type.Union([Type.Literal…])` keeps the literals but serializes as `anyOf`,
|
|
8
8
|
// which some providers handle poorly. `Type.Unsafe` gives both: the wire
|
|
9
9
|
// format stays `{ type: "string", enum: [...] }` and the type stays narrow.
|
|
10
|
+
// (TypeBox 0.34's `Type.Enum` targets numeric TS enums, not string arrays, so
|
|
11
|
+
// it is not a drop-in replacement here.)
|
|
10
12
|
function StringEnum<const T extends readonly string[]>(
|
|
11
13
|
values: T,
|
|
12
14
|
options?: SchemaOptions,
|
|
@@ -112,9 +114,8 @@ export const delegateTaskSchema = Type.Object({
|
|
|
112
114
|
});
|
|
113
115
|
|
|
114
116
|
// Single source of truth for registration and generated help. The exported
|
|
115
|
-
// argument types in types.ts project this canonical schema
|
|
116
|
-
//
|
|
117
|
-
// those legacy fields in this schema.
|
|
117
|
+
// argument types in types.ts project this canonical schema; providers see
|
|
118
|
+
// only these fields.
|
|
118
119
|
export const delegateArgumentsSchema = Type.Object({
|
|
119
120
|
ticketAction: Type.Optional(
|
|
120
121
|
StringEnum(["poll", "cancel", "wait"], {
|
|
@@ -157,8 +158,8 @@ export const delegateArgumentsSchema = Type.Object({
|
|
|
157
158
|
),
|
|
158
159
|
});
|
|
159
160
|
|
|
160
|
-
/** Fields that belong to a task entry. Models sometimes place these at the
|
|
161
|
-
*
|
|
161
|
+
/** Fields that belong to a task entry. Models sometimes place these at the top
|
|
162
|
+
* level of the arguments; the shim folds them back into a single task. */
|
|
162
163
|
const TASK_FIELD_NAMES = [
|
|
163
164
|
"id",
|
|
164
165
|
"prompt",
|
|
@@ -181,52 +182,26 @@ const TASK_FIELD_NAMES = [
|
|
|
181
182
|
* corrective message instead of being silently ignored (observed in the
|
|
182
183
|
* wild: a task-level `async: true` the caller believed had backgrounded
|
|
183
184
|
* the work while the call in fact ran synchronously). */
|
|
184
|
-
const VALID_TASK_KEYS = new Set<string>(
|
|
185
|
-
|
|
186
|
-
/** Top-level ticket actions the legacy `action` field may map to. */
|
|
187
|
-
const TICKET_ACTIONS = new Set(["poll", "cancel", "wait"]);
|
|
188
|
-
|
|
189
|
-
/** Session actions that are valid at the task level. A flat `action` at the
|
|
190
|
-
* top level may also fold into a wrapped task's `sessionAction`. */
|
|
191
|
-
const TASK_ACTIONS = new Set(["prompt", "close", "list"]);
|
|
192
|
-
|
|
193
|
-
/** Every value the legacy `action` field can carry before it is normalized to
|
|
194
|
-
* `ticketAction` or `sessionAction`. */
|
|
195
|
-
const LEGACY_ACTIONS = new Set([...TICKET_ACTIONS, ...TASK_ACTIONS]);
|
|
185
|
+
const VALID_TASK_KEYS = new Set<string>(TASK_FIELD_NAMES);
|
|
196
186
|
|
|
197
187
|
/** Validate the three operation modes after compatibility reshaping. */
|
|
198
188
|
export function validateDelegateOperation(
|
|
199
189
|
params: DelegateArguments,
|
|
200
190
|
): string | undefined {
|
|
201
191
|
const rawParams = params as Record<string, unknown>;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (hasLegacyAction) {
|
|
208
|
-
if (!LEGACY_ACTIONS.has(rawParams.action as string)) {
|
|
209
|
-
return `unknown action '${rawParams.action}'; valid ticket actions are poll/cancel/wait, valid session actions are prompt/close/list.`;
|
|
210
|
-
}
|
|
211
|
-
if (hasTicketAction) {
|
|
212
|
-
return "ambiguous: supply only ticketAction (or only legacy action), not both.";
|
|
213
|
-
}
|
|
214
|
-
if (TASK_ACTIONS.has(rawParams.action as string) && tasks.length > 0) {
|
|
215
|
-
return `legacy top-level action '${rawParams.action}' cannot be combined with an explicit tasks array; move it into the task's sessionAction or remove tasks.`;
|
|
216
|
-
}
|
|
192
|
+
if ("action" in rawParams) {
|
|
193
|
+
return (
|
|
194
|
+
"unsupported field 'action'; use 'ticketAction' for poll/cancel/wait " +
|
|
195
|
+
"or 'sessionAction' for prompt/close/list."
|
|
196
|
+
);
|
|
217
197
|
}
|
|
198
|
+
const tasks = params.tasks ?? [];
|
|
218
199
|
|
|
219
|
-
const ticketAction
|
|
220
|
-
params.ticketAction ??
|
|
221
|
-
(hasLegacyAction && TICKET_ACTIONS.has(rawParams.action as string)
|
|
222
|
-
? (rawParams.action as string)
|
|
223
|
-
: undefined);
|
|
224
|
-
|
|
200
|
+
const ticketAction = params.ticketAction;
|
|
225
201
|
const isTicketControl = ticketAction !== undefined;
|
|
226
202
|
|
|
227
203
|
if (isTicketControl) {
|
|
228
|
-
const
|
|
229
|
-
const taskIntentFields = topLevelTaskIntentFields.filter(
|
|
204
|
+
const taskIntentFields = ([...TASK_FIELD_NAMES, "tasks"] as const).filter(
|
|
230
205
|
(field) => rawParams[field] !== undefined,
|
|
231
206
|
);
|
|
232
207
|
if (taskIntentFields.length) {
|
|
@@ -268,9 +243,9 @@ export function validateDelegateOperation(
|
|
|
268
243
|
// there is no tasks array, so a mixed call silently lets tasks win —
|
|
269
244
|
// a model mistake that should fail loudly.
|
|
270
245
|
if (tasks.length > 0) {
|
|
271
|
-
const flatTaskFields =
|
|
272
|
-
|
|
273
|
-
|
|
246
|
+
const flatTaskFields = TASK_FIELD_NAMES.filter(
|
|
247
|
+
(field) => rawParams[field] !== undefined,
|
|
248
|
+
);
|
|
274
249
|
if (flatTaskFields.length) {
|
|
275
250
|
return `cannot mix top-level task field(s) ${flatTaskFields
|
|
276
251
|
.map((field) => `'${field}'`)
|
|
@@ -282,29 +257,11 @@ export function validateDelegateOperation(
|
|
|
282
257
|
|
|
283
258
|
for (const [index, task] of tasks.entries()) {
|
|
284
259
|
const rawTask = task as Record<string, unknown>;
|
|
285
|
-
const
|
|
286
|
-
const hasSessionAction = task.sessionAction !== undefined;
|
|
287
|
-
|
|
288
|
-
if (hasLegacyTaskAction) {
|
|
289
|
-
if (!TASK_ACTIONS.has(rawTask.action as string)) {
|
|
290
|
-
return `task ${index + 1}: unknown action '${rawTask.action}'; valid session actions are prompt/close/list.`;
|
|
291
|
-
}
|
|
292
|
-
if (hasSessionAction) {
|
|
293
|
-
return `task ${index + 1}: ambiguous: supply only sessionAction (or only legacy action), not both.`;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const sessionAction: string | undefined =
|
|
298
|
-
task.sessionAction ??
|
|
299
|
-
(hasLegacyTaskAction && TASK_ACTIONS.has(rawTask.action as string)
|
|
300
|
-
? (rawTask.action as string)
|
|
301
|
-
: undefined);
|
|
260
|
+
const sessionAction = task.sessionAction;
|
|
302
261
|
|
|
303
|
-
const unknownKeys = Object.keys(rawTask).filter(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return true;
|
|
307
|
-
});
|
|
262
|
+
const unknownKeys = Object.keys(rawTask).filter(
|
|
263
|
+
(key) => !VALID_TASK_KEYS.has(key),
|
|
264
|
+
);
|
|
308
265
|
if (unknownKeys.length) {
|
|
309
266
|
const asyncHint = unknownKeys.includes("async")
|
|
310
267
|
? " 'async' is a top-level flag; move it out of the task entry."
|
|
@@ -336,11 +293,7 @@ export function validateDelegateOperation(
|
|
|
336
293
|
return `task ${index + 1}: sessionAction 'close' requires sessionId.`;
|
|
337
294
|
}
|
|
338
295
|
const extras = Object.keys(rawTask).filter(
|
|
339
|
-
(key) =>
|
|
340
|
-
key !== "sessionAction" &&
|
|
341
|
-
key !== "sessionId" &&
|
|
342
|
-
key !== "action" &&
|
|
343
|
-
key !== "id",
|
|
296
|
+
(key) => key !== "sessionAction" && key !== "sessionId" && key !== "id",
|
|
344
297
|
);
|
|
345
298
|
if (extras.length) {
|
|
346
299
|
return `task ${index + 1}: sessionAction 'close' accepts only sessionAction and sessionId.`;
|
|
@@ -348,7 +301,7 @@ export function validateDelegateOperation(
|
|
|
348
301
|
}
|
|
349
302
|
if (sessionAction === "list") {
|
|
350
303
|
const extras = Object.keys(rawTask).filter(
|
|
351
|
-
(key) => key !== "sessionAction" && key !== "
|
|
304
|
+
(key) => key !== "sessionAction" && key !== "id",
|
|
352
305
|
);
|
|
353
306
|
if (extras.length) {
|
|
354
307
|
return `task ${index + 1}: sessionAction 'list' accepts only sessionAction.`;
|
|
@@ -378,6 +331,52 @@ function normalizeToolsField(value: string): unknown {
|
|
|
378
331
|
return trimmed && !/[\s,]/.test(trimmed) ? [trimmed] : value;
|
|
379
332
|
}
|
|
380
333
|
|
|
334
|
+
/** True when `record` carries a top-level ticket-control intent that makes a
|
|
335
|
+
* flat task-field wrap illegitimate: an explicit `ticketAction`, or a bare
|
|
336
|
+
* `ticket` id (which only makes sense with poll/cancel/wait). */
|
|
337
|
+
function hasTicketControlIntent(record: Record<string, unknown>): boolean {
|
|
338
|
+
return (
|
|
339
|
+
record.ticketAction === "poll" ||
|
|
340
|
+
record.ticketAction === "cancel" ||
|
|
341
|
+
record.ticketAction === "wait" ||
|
|
342
|
+
record.ticket !== undefined
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Fold top-level task fields into a single `tasks` entry. Only fires when
|
|
347
|
+
* there is no usable tasks array and no ticket-control intent — those calls
|
|
348
|
+
* are legitimately taskless. `sessionAction` is part of `TASK_FIELD_NAMES`,
|
|
349
|
+
* so a top-level `sessionAction` rides along into the wrapped task. */
|
|
350
|
+
function wrapFlatTaskFields(record: Record<string, unknown>): void {
|
|
351
|
+
const hasTasks = Array.isArray(record.tasks) && record.tasks.length > 0;
|
|
352
|
+
if (hasTasks || hasTicketControlIntent(record)) return;
|
|
353
|
+
const task: Record<string, unknown> = {};
|
|
354
|
+
for (const key of TASK_FIELD_NAMES) {
|
|
355
|
+
if (record[key] !== undefined) {
|
|
356
|
+
task[key] = record[key];
|
|
357
|
+
delete record[key];
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (Object.keys(task).length > 0) record.tasks = [task];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Per-entry recovery for one task: stringified (or bare-token) `tools` → a
|
|
364
|
+
* real array, and `agent: ""` → omitted (ad-hoc). Other malformed input is
|
|
365
|
+
* left for schema validation to reject loudly. */
|
|
366
|
+
function normalizeTaskEntry(entry: unknown): unknown {
|
|
367
|
+
if (!entry || typeof entry !== "object") return entry;
|
|
368
|
+
const e = entry as Record<string, unknown>;
|
|
369
|
+
const rawTools = e.tools;
|
|
370
|
+
const fixAgent = e.agent === "";
|
|
371
|
+
if (typeof rawTools !== "string" && !fixAgent) return entry;
|
|
372
|
+
const out = { ...e };
|
|
373
|
+
if (typeof rawTools === "string") {
|
|
374
|
+
out.tools = normalizeToolsField(rawTools);
|
|
375
|
+
}
|
|
376
|
+
if (fixAgent) delete out.agent;
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
|
|
381
380
|
/** Compatibility shim run by pi before schema validation. Recovers the
|
|
382
381
|
* malformed shapes weaker models emit, instead of letting them silently
|
|
383
382
|
* degrade to the help response (an empty `tasks` returns the manual, which
|
|
@@ -386,13 +385,9 @@ function normalizeToolsField(value: string): unknown {
|
|
|
386
385
|
* - task fields (`prompt`, `systemPrompt`, `tools`, ...) placed at the top
|
|
387
386
|
* level instead of inside a `tasks` entry — wrapped into a single task;
|
|
388
387
|
* - `tools` as a JSON string (or bare token) inside a task entry;
|
|
389
|
-
* - `agent: ""` inside a task entry — treated as omitted (ad-hoc)
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
* Skipped when a ticket action is in play. Conflicts between the legacy
|
|
393
|
-
* `action` field and its canonical replacement are left for
|
|
394
|
-
* `validateDelegateOperation` to report. All other invalid input is left for
|
|
395
|
-
* normal schema validation to reject loudly.
|
|
388
|
+
* - `agent: ""` inside a task entry — treated as omitted (ad-hoc).
|
|
389
|
+
* All other invalid input is left for normal schema validation to reject
|
|
390
|
+
* loudly.
|
|
396
391
|
*
|
|
397
392
|
* Silent by design: these rewrites are lossless re-shaping, so unlike the
|
|
398
393
|
* model-suffix warning in task-resolution (which fires because thinking
|
|
@@ -409,83 +404,12 @@ export function normalizeDelegateArguments(args: unknown): DelegateArguments {
|
|
|
409
404
|
if (parsed) record.tasks = parsed;
|
|
410
405
|
}
|
|
411
406
|
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
if (
|
|
415
|
-
typeof record.action === "string" &&
|
|
416
|
-
["poll", "cancel", "wait"].includes(record.action)
|
|
417
|
-
) {
|
|
418
|
-
if (record.ticketAction === undefined) {
|
|
419
|
-
record.ticketAction = record.action;
|
|
420
|
-
delete record.action;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Flat task fields at the top level → wrap into a single task. Only fires
|
|
425
|
-
// when there is no usable tasks array and no ticket action (`ticket`,
|
|
426
|
-
// poll/cancel/wait) — those calls are legitimately taskless.
|
|
427
|
-
const hasTasks = Array.isArray(record.tasks) && record.tasks.length > 0;
|
|
428
|
-
const isTicketAction =
|
|
429
|
-
record.ticketAction === "poll" ||
|
|
430
|
-
record.ticketAction === "cancel" ||
|
|
431
|
-
record.ticketAction === "wait" ||
|
|
432
|
-
record.action === "poll" ||
|
|
433
|
-
record.action === "cancel" ||
|
|
434
|
-
record.action === "wait";
|
|
435
|
-
if (!hasTasks && !isTicketAction && record.ticket === undefined) {
|
|
436
|
-
const task: Record<string, unknown> = {};
|
|
437
|
-
for (const key of TASK_FIELD_NAMES) {
|
|
438
|
-
if (record[key] !== undefined) {
|
|
439
|
-
task[key] = record[key];
|
|
440
|
-
delete record[key];
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
// Canonical `sessionAction` at the top level folds into the wrapped task.
|
|
444
|
-
if (typeof record.sessionAction === "string") {
|
|
445
|
-
if (task.sessionAction === undefined) {
|
|
446
|
-
task.sessionAction = record.sessionAction;
|
|
447
|
-
}
|
|
448
|
-
delete record.sessionAction;
|
|
449
|
-
}
|
|
450
|
-
// Legacy top-level session `action` folds into the wrapped task's
|
|
451
|
-
// `sessionAction`. A conflict with an explicit `sessionAction` is left
|
|
452
|
-
// for validateDelegateOperation to report.
|
|
453
|
-
if (typeof record.action === "string" && TASK_ACTIONS.has(record.action)) {
|
|
454
|
-
if (task.sessionAction === undefined) {
|
|
455
|
-
task.sessionAction = record.action;
|
|
456
|
-
} else {
|
|
457
|
-
task.action = record.action;
|
|
458
|
-
}
|
|
459
|
-
delete record.action;
|
|
460
|
-
}
|
|
461
|
-
if (Object.keys(task).length > 0) record.tasks = [task];
|
|
462
|
-
}
|
|
407
|
+
// Flat task fields at the top level → wrap into a single task.
|
|
408
|
+
wrapFlatTaskFields(record);
|
|
463
409
|
|
|
464
|
-
// Per-entry recovery: stringified
|
|
465
|
-
// `agent: ""` → omitted, and legacy `action` → `sessionAction`.
|
|
410
|
+
// Per-entry recovery: stringified/bare-token `tools` and `agent: ""`.
|
|
466
411
|
if (Array.isArray(record.tasks)) {
|
|
467
|
-
record.tasks = record.tasks.map(
|
|
468
|
-
if (!entry || typeof entry !== "object") return entry;
|
|
469
|
-
const e = entry as Record<string, unknown>;
|
|
470
|
-
const rawTools = e.tools;
|
|
471
|
-
const fixAgent = e.agent === "";
|
|
472
|
-
const needsActionNorm =
|
|
473
|
-
typeof e.action === "string" &&
|
|
474
|
-
TASK_ACTIONS.has(e.action) &&
|
|
475
|
-
e.sessionAction === undefined;
|
|
476
|
-
if (typeof rawTools !== "string" && !fixAgent && !needsActionNorm)
|
|
477
|
-
return entry;
|
|
478
|
-
const out = { ...e };
|
|
479
|
-
if (typeof rawTools === "string") {
|
|
480
|
-
out.tools = normalizeToolsField(rawTools);
|
|
481
|
-
}
|
|
482
|
-
if (fixAgent) delete out.agent;
|
|
483
|
-
if (needsActionNorm) {
|
|
484
|
-
out.sessionAction = out.action;
|
|
485
|
-
delete out.action;
|
|
486
|
-
}
|
|
487
|
-
return out;
|
|
488
|
-
});
|
|
412
|
+
record.tasks = record.tasks.map(normalizeTaskEntry);
|
|
489
413
|
}
|
|
490
414
|
|
|
491
415
|
return record as DelegateArguments;
|
package/task-resolution.ts
CHANGED
|
@@ -6,7 +6,11 @@ import {
|
|
|
6
6
|
DEFAULT_TOOLS,
|
|
7
7
|
VALID_THINKING,
|
|
8
8
|
} from "./constants.ts";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
TOOL_FACTORIES,
|
|
11
|
+
availableToolNames,
|
|
12
|
+
resolveToolGroups,
|
|
13
|
+
} from "./tools.ts";
|
|
10
14
|
import { configFor } from "./pool.ts";
|
|
11
15
|
import { isSessionBusy } from "./tickets.ts";
|
|
12
16
|
import { BUILTIN_AGENT_CONFIGS, buildSubagentSystemPrompt } from "./agents.ts";
|
|
@@ -120,7 +124,7 @@ export function validateTasks(
|
|
|
120
124
|
? (agents.get(task.agent) ?? BUILTIN_AGENT_CONFIGS[task.agent])
|
|
121
125
|
: undefined;
|
|
122
126
|
const workspace = task.workspace ?? agent?.workspace ?? "shared";
|
|
123
|
-
const sessionAction = task.sessionAction
|
|
127
|
+
const sessionAction = task.sessionAction;
|
|
124
128
|
if (
|
|
125
129
|
workspace === "scratch" &&
|
|
126
130
|
(task.sessionId || task.resumeFrom || sessionAction !== undefined)
|
|
@@ -274,25 +278,32 @@ export function resolveTasks(
|
|
|
274
278
|
// "continue with only sessionId" works without re-supplying tools.
|
|
275
279
|
// Explicit overrides that don't match get rejected by acquireAgentSession.
|
|
276
280
|
if (t.sessionAction !== "close" && t.sessionAction !== "list") {
|
|
281
|
+
// For `default` a deny-only override (no explicit allowlist) is not
|
|
282
|
+
// materialized at discovery; apply its denylist to the parent's actual
|
|
283
|
+
// tools here so a read-only parent stays read-only.
|
|
284
|
+
let effectiveParentTools = parentNativeTools;
|
|
285
|
+
if (
|
|
286
|
+
isDefaultAgent &&
|
|
287
|
+
agent?.deniedTools?.length &&
|
|
288
|
+
!agent?.explicitTools
|
|
289
|
+
) {
|
|
290
|
+
const denied = new Set(agent.deniedTools);
|
|
291
|
+
effectiveParentTools = parentNativeTools.filter((t) => !denied.has(t));
|
|
292
|
+
}
|
|
277
293
|
tools = resolveToolGroups(
|
|
278
294
|
t.tools ??
|
|
279
295
|
parentModelOverride?.tools ??
|
|
280
296
|
agentOverride?.tools ??
|
|
281
|
-
(isDefaultAgent
|
|
297
|
+
(isDefaultAgent
|
|
298
|
+
? agent?.explicitTools
|
|
299
|
+
? agent.tools
|
|
300
|
+
: effectiveParentTools
|
|
301
|
+
: undefined) ??
|
|
282
302
|
(isBuiltinAgent ? agent?.tools : undefined) ??
|
|
283
303
|
agent?.tools ??
|
|
284
304
|
(isPoolHit ? pooledConfig?.tools : undefined) ??
|
|
285
305
|
DEFAULT_TOOLS,
|
|
286
306
|
);
|
|
287
|
-
const unknownTools = tools.filter(
|
|
288
|
-
(name) => !Object.hasOwn(TOOL_FACTORIES, name),
|
|
289
|
-
);
|
|
290
|
-
if (unknownTools.length) {
|
|
291
|
-
warnings.push(
|
|
292
|
-
`Unknown tool(s) ignored: ${unknownTools.join(", ")}. Available: ${Object.keys(TOOL_FACTORIES).join(", ")}`,
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
tools = tools.filter((name) => Object.hasOwn(TOOL_FACTORIES, name));
|
|
296
307
|
}
|
|
297
308
|
|
|
298
309
|
// System prompt resolution. AgentSession's resource loader owns
|
|
@@ -316,14 +327,14 @@ export function resolveTasks(
|
|
|
316
327
|
parentSystemPrompt,
|
|
317
328
|
tools,
|
|
318
329
|
});
|
|
319
|
-
|
|
330
|
+
let requestedSystemPrompt = t.systemPrompt?.trim()
|
|
320
331
|
? t.systemPrompt
|
|
321
332
|
: agent?.systemPrompt?.trim()
|
|
322
333
|
? agent.systemPrompt
|
|
323
334
|
: isDefaultAgent
|
|
324
335
|
? resolvedBasePrompt
|
|
325
336
|
: undefined;
|
|
326
|
-
|
|
337
|
+
let systemPrompt = buildSubagentSystemPrompt({
|
|
327
338
|
taskSystemPrompt: t.systemPrompt,
|
|
328
339
|
agentSystemPrompt: agent?.systemPrompt,
|
|
329
340
|
parentSystemPrompt,
|
|
@@ -369,10 +380,16 @@ export function resolveTasks(
|
|
|
369
380
|
// task and settings.json model overrides, but deliberately ignore the
|
|
370
381
|
// legacy delegate.json agent model map so they inherit the parent unless
|
|
371
382
|
// an explicit modern override wins.
|
|
383
|
+
// Overridden built-ins can still provide an explicit `model` in their
|
|
384
|
+
// Markdown frontmatter – when `explicitModel` is set, honor it instead
|
|
385
|
+
// of silently ignoring it (which would contradict the Markdown contract).
|
|
372
386
|
const modelSpec = isDefaultAgent
|
|
373
|
-
? t.model
|
|
387
|
+
? (t.model ?? (agent?.explicitModel ? agent.model : undefined))
|
|
374
388
|
: isBuiltinAgent
|
|
375
|
-
? (t.model ??
|
|
389
|
+
? (t.model ??
|
|
390
|
+
parentModelOverride?.model ??
|
|
391
|
+
agentOverride?.model ??
|
|
392
|
+
(agent?.explicitModel ? agent.model : undefined))
|
|
376
393
|
: resolveModelSpec({
|
|
377
394
|
taskModel:
|
|
378
395
|
t.model ?? parentModelOverride?.model ?? agentOverride?.model,
|
|
@@ -449,6 +466,7 @@ export function resolveTasks(
|
|
|
449
466
|
? (t.thinking ??
|
|
450
467
|
parentModelOverride?.thinking ??
|
|
451
468
|
agentOverride?.thinking ??
|
|
469
|
+
(agent?.explicitThinking ? agent.thinking : undefined) ??
|
|
452
470
|
modelSuffix ??
|
|
453
471
|
parentDefaults.thinking ??
|
|
454
472
|
(isPoolHit ? pooledConfig?.thinking : undefined) ??
|
|
@@ -456,6 +474,7 @@ export function resolveTasks(
|
|
|
456
474
|
: (t.thinking ??
|
|
457
475
|
parentModelOverride?.thinking ??
|
|
458
476
|
agentOverride?.thinking ??
|
|
477
|
+
(agent?.explicitThinking ? agent.thinking : undefined) ??
|
|
459
478
|
(isPoolHit ? pooledConfig?.thinking : undefined) ??
|
|
460
479
|
modelSuffix ??
|
|
461
480
|
parentDefaults.thinking ??
|
|
@@ -478,6 +497,31 @@ export function resolveTasks(
|
|
|
478
497
|
);
|
|
479
498
|
}
|
|
480
499
|
}
|
|
500
|
+
|
|
501
|
+
const availableTools = availableToolNames(model?.provider);
|
|
502
|
+
const availableToolSet = new Set(availableTools);
|
|
503
|
+
const unknownTools = tools.filter((name) => !availableToolSet.has(name));
|
|
504
|
+
if (unknownTools.length) {
|
|
505
|
+
warnings.push(
|
|
506
|
+
`Unknown tool(s) ignored: ${unknownTools.join(", ")}. Available: ${availableTools.join(", ")}`,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
tools = tools.filter((name) => availableToolSet.has(name));
|
|
510
|
+
systemPrompt = buildSubagentSystemPrompt({
|
|
511
|
+
taskSystemPrompt: t.systemPrompt,
|
|
512
|
+
agentSystemPrompt: agent?.systemPrompt,
|
|
513
|
+
parentSystemPrompt,
|
|
514
|
+
pooledSystemPrompt: pooledConfig?.systemPrompt,
|
|
515
|
+
tools,
|
|
516
|
+
});
|
|
517
|
+
if (
|
|
518
|
+
isDefaultAgent &&
|
|
519
|
+
!t.systemPrompt?.trim() &&
|
|
520
|
+
!agent?.systemPrompt?.trim()
|
|
521
|
+
) {
|
|
522
|
+
requestedSystemPrompt = systemPrompt;
|
|
523
|
+
}
|
|
524
|
+
|
|
481
525
|
return {
|
|
482
526
|
...t,
|
|
483
527
|
id: t.id,
|