@bermudi/pi-delegate 0.1.1 → 0.1.3
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 +3 -2
- package/agents.ts +163 -17
- package/concurrency.ts +90 -10
- package/config.ts +61 -0
- package/delegate.ts +14 -0
- package/dispatch.ts +126 -21
- package/extension.ts +309 -62
- package/file-tracking.ts +27 -5
- package/format.ts +46 -7
- package/leaf.ts +48 -0
- package/lifecycle.ts +129 -35
- package/manual.ts +38 -10
- package/package.json +4 -1
- package/patches/@marcfargas%2Fpi-test-harness@0.6.1.patch +13 -0
- package/render-branches.ts +31 -13
- package/render-result.ts +12 -0
- package/runner.ts +237 -42
- package/schema.ts +204 -47
- package/status.ts +68 -2
- package/task-resolution.ts +101 -65
- package/telemetry.ts +738 -0
- package/tickets.ts +196 -61
- package/tools.ts +16 -15
- package/types.ts +71 -13
- package/usage.ts +19 -0
package/task-resolution.ts
CHANGED
|
@@ -37,40 +37,32 @@ const PROJECT_CONTEXT_END = "\n</project_context>\n";
|
|
|
37
37
|
*
|
|
38
38
|
* The AGENTS.md/CLAUDE.md content is inserted verbatim inside
|
|
39
39
|
* `<project_instructions>...</project_instructions>` blocks. A file that
|
|
40
|
-
* itself contains `\n</project_context>\n`
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
40
|
+
* itself contains `\n</project_context>\n` can therefore forge an early
|
|
41
|
+
* closing marker. It could also include a forged `</project_instructions>` to
|
|
42
|
+
* balance the open tag, making the fake `</project_context>` look like the
|
|
43
|
+
* section end. A content-controlled marker can only ever appear *inside* the
|
|
44
|
+
* generated `<project_context>` block, before the real `</project_context>`
|
|
45
|
+
* that Pi appends after the last file. Skills escape angle brackets, the
|
|
46
|
+
* `appendSystemPrompt` text is added before this section, and the trailing
|
|
47
|
+
* `Current working directory:` line contains no such marker, so the real
|
|
48
|
+
* closing marker is the final raw `\n</project_context>\n` in the prompt.
|
|
49
|
+
* We therefore match the Pi-generated opening marker to the final matching
|
|
50
|
+
* closing marker, deterministically stripping the whole generated section.
|
|
50
51
|
*/
|
|
51
52
|
export function stripInheritedProjectContext(
|
|
52
53
|
prompt: string | undefined,
|
|
53
54
|
): string | undefined {
|
|
54
55
|
if (!prompt) return prompt;
|
|
56
|
+
|
|
55
57
|
const start = prompt.indexOf(PROJECT_CONTEXT_START);
|
|
56
58
|
if (start < 0) return prompt;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const closeCount = (before.match(/<\/project_instructions>/g) || []).length;
|
|
65
|
-
if (openCount > closeCount) {
|
|
66
|
-
// Inside a file block — embedded fake, skip it.
|
|
67
|
-
searchFrom = candidate + PROJECT_CONTEXT_END.length;
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
end = candidate;
|
|
71
|
-
break; // first valid outside block is the true closing
|
|
72
|
-
}
|
|
73
|
-
if (end < 0) return prompt;
|
|
59
|
+
|
|
60
|
+
// The real closing marker is the final occurrence: child content cannot
|
|
61
|
+
// place a `</project_context>` after the one Pi generates to close the
|
|
62
|
+
// section.
|
|
63
|
+
const end = prompt.lastIndexOf(PROJECT_CONTEXT_END);
|
|
64
|
+
if (end < start) return prompt;
|
|
65
|
+
|
|
74
66
|
return `${prompt.slice(0, start)}${prompt.slice(end + PROJECT_CONTEXT_END.length)}`;
|
|
75
67
|
}
|
|
76
68
|
|
|
@@ -86,6 +78,11 @@ function noticeResult(
|
|
|
86
78
|
};
|
|
87
79
|
}
|
|
88
80
|
|
|
81
|
+
/** Format a task reference for error messages: one-based index with an optional caller id. */
|
|
82
|
+
function formatTaskRef(index: number, id: string | undefined): string {
|
|
83
|
+
return `Task ${index + 1}${id ? `#${id}` : ""}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
89
86
|
/** Pre-dispatch validation: duplicate sessionIds, sessions busy with an async
|
|
90
87
|
* ticket, and unknown agent names. Returns an error result to short-circuit
|
|
91
88
|
* the call, or null when all checks pass. */
|
|
@@ -121,6 +118,24 @@ export function validateTasks(
|
|
|
121
118
|
);
|
|
122
119
|
}
|
|
123
120
|
|
|
121
|
+
// Disallow duplicate caller-provided task ids within one dispatch.
|
|
122
|
+
const seenIds = new Map<string, number>();
|
|
123
|
+
const duplicateIds: string[] = [];
|
|
124
|
+
for (const [index, task] of tasks.entries()) {
|
|
125
|
+
if (task.id) {
|
|
126
|
+
if (seenIds.has(task.id)) {
|
|
127
|
+
duplicateIds.push(
|
|
128
|
+
`task ${index + 1}: duplicate id '${task.id}' — ids must be unique within one dispatch.`,
|
|
129
|
+
);
|
|
130
|
+
} else {
|
|
131
|
+
seenIds.set(task.id, index);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (duplicateIds.length) {
|
|
136
|
+
return noticeResult(duplicateIds.join(" "), tasks, parentModelId);
|
|
137
|
+
}
|
|
138
|
+
|
|
124
139
|
const unknown: string[] = [];
|
|
125
140
|
for (const t of tasks) {
|
|
126
141
|
if (t.agent && t.agent !== DEFAULT_AGENT_NAME && !agents.has(t.agent)) {
|
|
@@ -187,17 +202,47 @@ export function resolveTasks(
|
|
|
187
202
|
// it. The assembled parent project-context section was stripped above;
|
|
188
203
|
// the child ResourceLoader supplies context for this task's cwd.
|
|
189
204
|
const pooledConfig = t.sessionId ? configFor(t.sessionId) : undefined;
|
|
205
|
+
const isPoolHit = pooledConfig !== undefined;
|
|
206
|
+
const parentNativeTools = parentDefaults.tools.filter((name) =>
|
|
207
|
+
Object.hasOwn(TOOL_FACTORIES, name),
|
|
208
|
+
);
|
|
209
|
+
let tools: string[] = [];
|
|
210
|
+
const warnings: string[] = [];
|
|
190
211
|
|
|
191
212
|
// Prompt is required for fresh tasks. ResumeFrom provides context already.
|
|
192
213
|
if (
|
|
193
|
-
t.
|
|
194
|
-
t.
|
|
214
|
+
t.sessionAction !== "close" &&
|
|
215
|
+
t.sessionAction !== "list" &&
|
|
195
216
|
!t.resumeFrom &&
|
|
196
217
|
!t.prompt?.trim()
|
|
197
218
|
) {
|
|
198
219
|
throw new Error(
|
|
199
|
-
|
|
220
|
+
`${formatTaskRef(i, t.id)}: prompt is required unless sessionAction is 'close'/'list' or resumeFrom is set.`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Resolve tools — warn about unknown tool names.
|
|
225
|
+
// For active pooled sessions, fall back to the frozen pooled config so
|
|
226
|
+
// "continue with only sessionId" works without re-supplying tools.
|
|
227
|
+
// Explicit overrides that don't match get rejected by acquireAgentSession.
|
|
228
|
+
if (t.sessionAction !== "close" && t.sessionAction !== "list") {
|
|
229
|
+
tools = resolveToolGroups(
|
|
230
|
+
t.tools ??
|
|
231
|
+
agentOverride?.tools ??
|
|
232
|
+
agent?.tools ??
|
|
233
|
+
(isDefaultAgent ? parentNativeTools : undefined) ??
|
|
234
|
+
(isPoolHit ? pooledConfig?.tools : undefined) ??
|
|
235
|
+
DEFAULT_TOOLS,
|
|
236
|
+
);
|
|
237
|
+
const unknownTools = tools.filter(
|
|
238
|
+
(name) => !Object.hasOwn(TOOL_FACTORIES, name),
|
|
200
239
|
);
|
|
240
|
+
if (unknownTools.length) {
|
|
241
|
+
warnings.push(
|
|
242
|
+
`Unknown tool(s) ignored: ${unknownTools.join(", ")}. Available: ${Object.keys(TOOL_FACTORIES).join(", ")}`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
tools = tools.filter((name) => Object.hasOwn(TOOL_FACTORIES, name));
|
|
201
246
|
}
|
|
202
247
|
|
|
203
248
|
// System prompt resolution. AgentSession's resource loader owns
|
|
@@ -206,21 +251,34 @@ export function resolveTasks(
|
|
|
206
251
|
// explicit task prompt → named agent body → sanitized parent prompt →
|
|
207
252
|
// default. The resolved base is passed as the loader's customPrompt (see
|
|
208
253
|
// buildDelegateSession).
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
254
|
+
//
|
|
255
|
+
// The *requested* system prompt is what the reuse check compares against the
|
|
256
|
+
// frozen pooled value. A bare `{ prompt, sessionId }` omits it so the pool
|
|
257
|
+
// can keep using the frozen prompt even if the parent prompt changed. An
|
|
258
|
+
// explicit task/profile prompt is used as-is. The built-in `default` profile
|
|
259
|
+
// intentionally mirrors the live parent, so its requested prompt is the
|
|
260
|
+
// *sanitized* parent prompt — the same form that would be stored as the
|
|
261
|
+
// frozen base, avoiding a false mismatch when the inherited default prompt
|
|
262
|
+
// gets its stale tool inventory stripped.
|
|
263
|
+
const resolvedBasePrompt = buildSubagentSystemPrompt({
|
|
264
|
+
taskSystemPrompt: t.systemPrompt,
|
|
265
|
+
agentSystemPrompt: agent?.systemPrompt,
|
|
266
|
+
parentSystemPrompt,
|
|
267
|
+
tools,
|
|
268
|
+
});
|
|
212
269
|
const requestedSystemPrompt = t.systemPrompt?.trim()
|
|
213
270
|
? t.systemPrompt
|
|
214
271
|
: agent?.systemPrompt?.trim()
|
|
215
272
|
? agent.systemPrompt
|
|
216
273
|
: isDefaultAgent
|
|
217
|
-
?
|
|
274
|
+
? resolvedBasePrompt
|
|
218
275
|
: undefined;
|
|
219
276
|
const systemPrompt = buildSubagentSystemPrompt({
|
|
220
277
|
taskSystemPrompt: t.systemPrompt,
|
|
221
278
|
agentSystemPrompt: agent?.systemPrompt,
|
|
222
279
|
parentSystemPrompt,
|
|
223
280
|
pooledSystemPrompt: pooledConfig?.systemPrompt,
|
|
281
|
+
tools,
|
|
224
282
|
});
|
|
225
283
|
|
|
226
284
|
// Build prompt — wrap with parent context if using with-parent-transcript
|
|
@@ -252,11 +310,9 @@ export function resolveTasks(
|
|
|
252
310
|
let model: Model<Api> | undefined;
|
|
253
311
|
let requestedModel: Model<Api> | undefined;
|
|
254
312
|
let modelSuffix: ThinkingLevel | undefined;
|
|
255
|
-
let tools: string[] = [];
|
|
256
313
|
let thinking: ThinkingLevel = "off";
|
|
257
|
-
const warnings: string[] = [];
|
|
258
314
|
|
|
259
|
-
if (t.
|
|
315
|
+
if (t.sessionAction !== "close" && t.sessionAction !== "list") {
|
|
260
316
|
// A pool hit always runs its frozen model, but an explicitly requested
|
|
261
317
|
// task/profile model still has to be resolved so checkout can reject a
|
|
262
318
|
// contradictory request rather than silently discarding it. Naming the
|
|
@@ -278,7 +334,7 @@ export function resolveTasks(
|
|
|
278
334
|
modelSuffix = requested.strippedSuffix;
|
|
279
335
|
if (!requestedModel) {
|
|
280
336
|
throw new Error(
|
|
281
|
-
|
|
337
|
+
`${formatTaskRef(i, t.id)}: requested model '${requestedModelSpec}' is not available. Check provider config or remove the model field to continue the pooled session.`,
|
|
282
338
|
);
|
|
283
339
|
}
|
|
284
340
|
} else if (isDefaultAgent) {
|
|
@@ -305,11 +361,13 @@ export function resolveTasks(
|
|
|
305
361
|
// it is honored only as a last-resort thinking default (see below).
|
|
306
362
|
modelSuffix = resolvedRequest?.strippedSuffix;
|
|
307
363
|
|
|
308
|
-
//
|
|
309
|
-
|
|
364
|
+
// The selected model spec is explicit regardless of whether it came
|
|
365
|
+
// from the task, settings, or named-agent frontmatter. If it cannot
|
|
366
|
+
// resolve, fail loudly instead of silently falling back to the parent.
|
|
367
|
+
const explicitRequest = modelSpec;
|
|
310
368
|
if (explicitRequest && !resolvedModel) {
|
|
311
369
|
throw new Error(
|
|
312
|
-
|
|
370
|
+
`${formatTaskRef(i, t.id)}: requested model '${explicitRequest}' is not available. Check provider config or remove the model field to use the parent model.`,
|
|
313
371
|
);
|
|
314
372
|
}
|
|
315
373
|
|
|
@@ -322,30 +380,7 @@ export function resolveTasks(
|
|
|
322
380
|
|
|
323
381
|
if (!model) {
|
|
324
382
|
throw new Error(
|
|
325
|
-
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Resolve tools — warn about unknown tool names.
|
|
330
|
-
// For active pooled sessions, fall back to the frozen pooled config so
|
|
331
|
-
// "continue with only sessionId" works without re-supplying tools.
|
|
332
|
-
// Explicit overrides that don't match get rejected by acquireAgentSession.
|
|
333
|
-
const isPoolHit = pooledConfig !== undefined;
|
|
334
|
-
const parentNativeTools = parentDefaults.tools.filter(
|
|
335
|
-
(name) => name in TOOL_FACTORIES,
|
|
336
|
-
);
|
|
337
|
-
tools = resolveToolGroups(
|
|
338
|
-
t.tools ??
|
|
339
|
-
agentOverride?.tools ??
|
|
340
|
-
agent?.tools ??
|
|
341
|
-
(isDefaultAgent ? parentNativeTools : undefined) ??
|
|
342
|
-
(isPoolHit ? pooledConfig?.tools : undefined) ??
|
|
343
|
-
DEFAULT_TOOLS,
|
|
344
|
-
);
|
|
345
|
-
const unknownTools = tools.filter((name) => !(name in TOOL_FACTORIES));
|
|
346
|
-
if (unknownTools.length) {
|
|
347
|
-
warnings.push(
|
|
348
|
-
`Unknown tool(s) ignored: ${unknownTools.join(", ")}. Available: ${Object.keys(TOOL_FACTORIES).join(", ")}`,
|
|
383
|
+
`${formatTaskRef(i, t.id)}: no model available — parent session has no model set.`,
|
|
349
384
|
);
|
|
350
385
|
}
|
|
351
386
|
|
|
@@ -381,6 +416,7 @@ export function resolveTasks(
|
|
|
381
416
|
}
|
|
382
417
|
return {
|
|
383
418
|
...t,
|
|
419
|
+
id: t.id,
|
|
384
420
|
cwd,
|
|
385
421
|
systemPrompt,
|
|
386
422
|
model: model!,
|