@fradser/pi-agent-teams 0.9.1 → 0.9.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/package.json +1 -1
- package/src/agent-actions.ts +5 -2
- package/src/tools.ts +10 -8
- package/src/types.ts +92 -12
package/package.json
CHANGED
package/src/agent-actions.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
import { isValidTeammateName, listTeammates } from "./state.ts";
|
|
5
5
|
import { resolveAgent } from "./agents.ts";
|
|
6
6
|
import { shutdownTeammateExact, spawnTeammate } from "./team-machine.ts";
|
|
7
|
-
import type
|
|
7
|
+
import { normalizeCoordinationParams, requireParsedParams, type Teammate } from "./types.ts";
|
|
8
8
|
import { snapshotWorkContext } from "./work-context.ts";
|
|
9
9
|
import { exactSessionRoute, resolveExactSession } from "./recipient.ts";
|
|
10
10
|
|
|
@@ -48,6 +48,9 @@ export function runAgentAction(
|
|
|
48
48
|
runtime: AgentActionRuntime,
|
|
49
49
|
sessionManager?: ExtensionContext["sessionManager"],
|
|
50
50
|
) {
|
|
51
|
+
// Harnesses that cannot infer object types from union-root schemas deliver
|
|
52
|
+
// `definition`/`resources` as JSON strings; parse before branching on them.
|
|
53
|
+
params = requireParsedParams(normalizeCoordinationParams(params as Record<string, unknown>, ["definition", "resources"]), ["definition", "resources"]) as typeof params;
|
|
51
54
|
if (params.action === "inspect") {
|
|
52
55
|
if (!params.name) throw new Error("Agent inspect requires a name.");
|
|
53
56
|
requireName(params.name);
|
|
@@ -77,7 +80,7 @@ export function runAgentAction(
|
|
|
77
80
|
const result = runtime.spawnTeammate({
|
|
78
81
|
name: params.name,
|
|
79
82
|
agent: params.name,
|
|
80
|
-
...(params.action === "delegate" ? { workId: `work:${randomUUID()}`, prompt, resources: params.resources, verify: params.verify, context } : {}),
|
|
83
|
+
...(params.action === "delegate" ? { workId: `work:${randomUUID()}`, prompt, resources: params.resources as string[] | undefined, verify: params.verify, context } : {}),
|
|
81
84
|
...(params.model ? { model: params.model } : {}),
|
|
82
85
|
...(params.definition ? { definition: params.definition } : {}),
|
|
83
86
|
});
|
package/src/tools.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "./team-machine.ts";
|
|
13
13
|
import { listTasks, livingTeammates } from "./state.ts";
|
|
14
14
|
|
|
15
|
-
import { AgentActionParams, LEADER_RECIPIENT, AgentEventParams, WorkToolParams } from "./types.ts";
|
|
15
|
+
import { AgentActionParams, LEADER_RECIPIENT, AgentEventParams, WorkToolParams, normalizeCoordinationParams, requireParsedParams } from "./types.ts";
|
|
16
16
|
import { openTeamConsole, refreshTeamUI } from "./ui.ts";
|
|
17
17
|
import { discoverAgents } from "./agents.ts";
|
|
18
18
|
import { agentRow, leaderWorkRow, messageRow, type AgentRowArgs } from "./tool-copy.ts";
|
|
@@ -73,7 +73,7 @@ export function registerLeaderTools(pi: ExtensionAPI, runtime: AgentActionRuntim
|
|
|
73
73
|
);
|
|
74
74
|
},
|
|
75
75
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
76
|
-
const result = await runAgentAction(params, ctx.cwd, runtime, ctx.sessionManager);
|
|
76
|
+
const result = await runAgentAction(params as Parameters<typeof runAgentAction>[0], ctx.cwd, runtime, ctx.sessionManager);
|
|
77
77
|
if (params.action !== "inspect") {
|
|
78
78
|
refreshTeamUI(ctx);
|
|
79
79
|
}
|
|
@@ -129,13 +129,15 @@ export function registerLeaderTools(pi: ExtensionAPI, runtime: AgentActionRuntim
|
|
|
129
129
|
);
|
|
130
130
|
},
|
|
131
131
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
132
|
+
// Union-root schema: some harnesses deliver target/arrays as JSON strings.
|
|
133
|
+
params = requireParsedParams(normalizeCoordinationParams(params as Record<string, unknown>, ["target", "dependsOn", "resources", "supersedes"]), ["target", "dependsOn", "resources", "supersedes"]) as typeof params;
|
|
132
134
|
if (params.action === "supersede") {
|
|
133
135
|
const tasks = listTasks();
|
|
134
|
-
const referenced = [...(params.dependsOn ?? []), ...params.supersedes];
|
|
136
|
+
const referenced = [...((params.dependsOn as string[] | undefined) ?? []), ...(params.supersedes as string[])];
|
|
135
137
|
if (referenced.some((id) => !tasks.some((task) => task.id === id))) {
|
|
136
138
|
throw new Error(`Unknown work id in [${referenced.join(", ")}].`);
|
|
137
139
|
}
|
|
138
|
-
const created = createBoardTask(params);
|
|
140
|
+
const created = createBoardTask(params as Parameters<typeof createBoardTask>[0]);
|
|
139
141
|
if (!created.ok) throw new Error(created.error);
|
|
140
142
|
const work = listTasks().find((task) => task.id === created.id);
|
|
141
143
|
if (!work) throw new Error(`Replacement Work Item "${created.id}" is unavailable.`);
|
|
@@ -171,7 +173,7 @@ export function registerLeaderTools(pi: ExtensionAPI, runtime: AgentActionRuntim
|
|
|
171
173
|
};
|
|
172
174
|
}
|
|
173
175
|
if (params.action === "assign") {
|
|
174
|
-
const assigned = assignExistingWork(params.id, params.target.session);
|
|
176
|
+
const assigned = assignExistingWork(params.id, (params.target as { session: string }).session);
|
|
175
177
|
if (!assigned.ok) throw new Error(assigned.error);
|
|
176
178
|
const task = listTasks().find((entry) => entry.id === assigned.workId);
|
|
177
179
|
if (!task) throw new Error(`Assigned Work Item "${assigned.workId}" is unavailable.`);
|
|
@@ -182,7 +184,7 @@ export function registerLeaderTools(pi: ExtensionAPI, runtime: AgentActionRuntim
|
|
|
182
184
|
action: "assign", outcome: "assigned", state: task.status,
|
|
183
185
|
work: { id: task.id, subject: task.subject, resources: task.resources, state: task.status, claimedBy: task.claimedBy },
|
|
184
186
|
assignment: { id: assigned.assignmentId, owner: assigned.owner, kind: "direct" },
|
|
185
|
-
target: params.target, delivery: "fresh-session-pending",
|
|
187
|
+
target: params.target as { session: string }, delivery: "fresh-session-pending",
|
|
186
188
|
},
|
|
187
189
|
};
|
|
188
190
|
}
|
|
@@ -204,11 +206,11 @@ export function registerLeaderTools(pi: ExtensionAPI, runtime: AgentActionRuntim
|
|
|
204
206
|
};
|
|
205
207
|
}
|
|
206
208
|
const tasks = listTasks();
|
|
207
|
-
const referenced = params.dependsOn ?? [];
|
|
209
|
+
const referenced = (params.dependsOn as string[] | undefined) ?? [];
|
|
208
210
|
if (referenced.some((id) => !tasks.some((task) => task.id === id))) {
|
|
209
211
|
throw new Error(`Unknown work id in [${referenced.join(", ")}].`);
|
|
210
212
|
}
|
|
211
|
-
const created = createBoardTask(params);
|
|
213
|
+
const created = createBoardTask(params as Parameters<typeof createBoardTask>[0]);
|
|
212
214
|
if (!created.ok) throw new Error(created.error);
|
|
213
215
|
const work = listTasks().find((task) => task.id === created.id);
|
|
214
216
|
if (!work) throw new Error(`Created Work Item "${created.id}" is unavailable.`);
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Static } from "typebox";
|
|
1
|
+
import type { Static, TSchema, TString, TObject, TUnion } from "typebox";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import { WORKER_BUILTIN_TOOLS } from "./worker-tools.ts";
|
|
4
4
|
|
|
@@ -225,10 +225,90 @@ export const InlineAgentDefinitionParams = Type.Object({
|
|
|
225
225
|
persistScope: Type.Optional(Type.Union([Type.Literal("project"), Type.Literal("project-local")])),
|
|
226
226
|
}, { additionalProperties: false });
|
|
227
227
|
|
|
228
|
+
/** Some harnesses deliver object/array parameters as JSON strings and Pi
|
|
229
|
+
* validates arguments exactly as delivered, before any handler runs. Accept the
|
|
230
|
+
* string at the schema edge and parse it in the handler so a callable payload is
|
|
231
|
+
* never rejected by validation. */
|
|
232
|
+
function stringTolerant<T extends TSchema>(schema: T): TUnion<[T, TString]> {
|
|
233
|
+
return Type.Union([schema, Type.String()]);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Harnesses that stream tool parameters per name infer JSON parsing from the
|
|
237
|
+
* root schema `properties`. A bare Type.Union exposes only `anyOf`, so object
|
|
238
|
+
* and array parameters such as `definition`, `target`, or `dependsOn` reach
|
|
239
|
+
* validation as unparsed JSON strings and every branch rejects the call before
|
|
240
|
+
* the handler runs. Mirror every branch property onto the root as an optional
|
|
241
|
+
* property (unioned when branches disagree) so callers parse structured values,
|
|
242
|
+
* while each anyOf branch keeps its strict per-action contract. */
|
|
243
|
+
function alternativesOf(sub: any): any[] {
|
|
244
|
+
return Array.isArray(sub.anyOf) ? sub.anyOf : [sub];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function mergeBranchSubSchemas(pool: any[]): any {
|
|
248
|
+
if (pool.length === 1) return pool[0];
|
|
249
|
+
const types = new Set(pool.map((entry) => entry.type));
|
|
250
|
+
if (types.size === 1) {
|
|
251
|
+
const [type] = [...types];
|
|
252
|
+
// Same JSON type: collapse to the shared type so callers can still infer
|
|
253
|
+
// parsing; literal branches keep an exact enum at the root.
|
|
254
|
+
if (type === "string" && pool.every((entry) => entry.const !== undefined || Array.isArray(entry.enum))) {
|
|
255
|
+
const values = pool.flatMap((entry) => (entry.const !== undefined ? [entry.const] : entry.enum));
|
|
256
|
+
return { type, enum: [...new Set(values)] };
|
|
257
|
+
}
|
|
258
|
+
return { type };
|
|
259
|
+
}
|
|
260
|
+
return { anyOf: pool };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function actionUnion<T extends TObject[]>(branches: [...T], options?: Record<string, unknown>): TUnion<T> {
|
|
264
|
+
const pools: Record<string, any[]> = {};
|
|
265
|
+
for (const branch of branches) {
|
|
266
|
+
for (const [key, sub] of Object.entries((branch as { properties?: Record<string, any> }).properties ?? {})) {
|
|
267
|
+
const pool = (pools[key] ??= []);
|
|
268
|
+
for (const alt of alternativesOf(sub)) {
|
|
269
|
+
const json = JSON.stringify(alt);
|
|
270
|
+
if (!pool.some((entry) => JSON.stringify(entry) === json)) pool.push(alt);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const properties: Record<string, any> = {};
|
|
275
|
+
for (const [key, pool] of Object.entries(pools)) properties[key] = mergeBranchSubSchemas(pool);
|
|
276
|
+
return Type.Union(branches, { ...options, properties } as never) as TUnion<T>;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Tolerate harnesses that deliver object or array parameters as JSON strings. */
|
|
280
|
+
export function parseJsonParam<T>(value: T): T {
|
|
281
|
+
if (typeof value !== "string") return value;
|
|
282
|
+
const text = value.trim();
|
|
283
|
+
if (!text.startsWith("{") && !text.startsWith("[")) return value;
|
|
284
|
+
try {
|
|
285
|
+
return JSON.parse(text) as T;
|
|
286
|
+
} catch {
|
|
287
|
+
return value;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Parse stringified structured parameters before handlers branch on them. */
|
|
292
|
+
export function normalizeCoordinationParams<T extends Record<string, unknown>>(params: T, keys: readonly string[]): T {
|
|
293
|
+
const next: Record<string, unknown> = { ...params };
|
|
294
|
+
for (const key of keys) {
|
|
295
|
+
if (key in next) next[key] = parseJsonParam(next[key]);
|
|
296
|
+
}
|
|
297
|
+
return next as T;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Reject string parameters that could not be parsed back into structures. */
|
|
301
|
+
export function requireParsedParams<T extends Record<string, unknown>>(params: T, keys: readonly string[]): T {
|
|
302
|
+
for (const key of keys) {
|
|
303
|
+
if (typeof params[key] === "string") throw new Error(`Parameter "${key}" must be a JSON object or array, not a string.`);
|
|
304
|
+
}
|
|
305
|
+
return params;
|
|
306
|
+
}
|
|
307
|
+
|
|
228
308
|
/** Legacy overloaded Agent control; replaced by AgentActionParams at final cutover. */
|
|
229
|
-
export const AgentActionParams =
|
|
230
|
-
Type.Object({ action: Type.Literal("delegate"), name: Type.String({ minLength: 1 }), prompt: Type.String({ minLength: 1 }), definition: Type.Optional(InlineAgentDefinitionParams), resources: Type.Optional(Type.Array(Type.String({ minLength: 1 }))), verify: Type.Optional(Type.String()), model: Type.Optional(Type.String()), fork: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
|
|
231
|
-
Type.Object({ action: Type.Literal("start"), name: Type.String({ minLength: 1 }), definition: Type.Optional(InlineAgentDefinitionParams), model: Type.Optional(Type.String()) }, { additionalProperties: false }),
|
|
309
|
+
export const AgentActionParams = actionUnion([
|
|
310
|
+
Type.Object({ action: Type.Literal("delegate"), name: Type.String({ minLength: 1 }), prompt: Type.String({ minLength: 1 }), definition: Type.Optional(stringTolerant(InlineAgentDefinitionParams)), resources: Type.Optional(stringTolerant(Type.Array(Type.String({ minLength: 1 })))), verify: Type.Optional(Type.String()), model: Type.Optional(Type.String()), fork: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
|
|
311
|
+
Type.Object({ action: Type.Literal("start"), name: Type.String({ minLength: 1 }), definition: Type.Optional(stringTolerant(InlineAgentDefinitionParams)), model: Type.Optional(Type.String()) }, { additionalProperties: false }),
|
|
232
312
|
Type.Object({ action: Type.Literal("inspect"), name: Type.String({ minLength: 1 }), session: Type.String({ minLength: 1 }) }, { additionalProperties: false }),
|
|
233
313
|
Type.Object({ action: Type.Literal("stop"), session: Type.String({ minLength: 1 }) }, { additionalProperties: false }),
|
|
234
314
|
], { description: "Delegate, start, inspect, or stop an Agent session" });
|
|
@@ -237,20 +317,20 @@ export const AgentActionParams = Type.Union([
|
|
|
237
317
|
/** Create a board task (leader-only). */
|
|
238
318
|
/** First public Work interface slice. Work creation reuses the current
|
|
239
319
|
* single-writer task record until every acquisition path shares one lifecycle. */
|
|
240
|
-
export const WorkToolParams =
|
|
320
|
+
export const WorkToolParams = actionUnion([
|
|
241
321
|
Type.Object({
|
|
242
322
|
action: Type.Literal("create"),
|
|
243
323
|
subject: Type.String({ minLength: 1, description: "Work title" }),
|
|
244
324
|
description: Type.Optional(Type.String({ description: "Full Work description" })),
|
|
245
|
-
dependsOn: Type.Optional(Type.Array(Type.String({ minLength: 1 }), { description: "Work IDs that must complete first" })),
|
|
325
|
+
dependsOn: Type.Optional(stringTolerant(Type.Array(Type.String({ minLength: 1 }), { description: "Work IDs that must complete first" }))),
|
|
246
326
|
verify: Type.Optional(Type.String({ description: "Completion gate for this Work Item" })),
|
|
247
|
-
resources: Type.Optional(Type.Array(Type.String({ minLength: 1 }), { description: "Resource tags that cannot overlap active Work" })),
|
|
327
|
+
resources: Type.Optional(stringTolerant(Type.Array(Type.String({ minLength: 1 }), { description: "Resource tags that cannot overlap active Work" }))),
|
|
248
328
|
}, { additionalProperties: false }),
|
|
249
329
|
Type.Object({ action: Type.Literal("list") }, { additionalProperties: false }),
|
|
250
330
|
Type.Object({
|
|
251
331
|
action: Type.Literal("assign"),
|
|
252
332
|
id: Type.String({ minLength: 1, description: "Pending Work Item ID" }),
|
|
253
|
-
target: Type.Object({ session: Type.String({ minLength: 1, description: "Exact idle session route" }) }, { additionalProperties: false }),
|
|
333
|
+
target: stringTolerant(Type.Object({ session: Type.String({ minLength: 1, description: "Exact idle session route" }) }, { additionalProperties: false })),
|
|
254
334
|
}, { additionalProperties: false }),
|
|
255
335
|
Type.Object({
|
|
256
336
|
action: Type.Literal("release"),
|
|
@@ -266,10 +346,10 @@ export const WorkToolParams = Type.Union([
|
|
|
266
346
|
action: Type.Literal("supersede"),
|
|
267
347
|
subject: Type.String({ minLength: 1, description: "Replacement Work title" }),
|
|
268
348
|
description: Type.Optional(Type.String({ description: "Full replacement Work description" })),
|
|
269
|
-
dependsOn: Type.Optional(Type.Array(Type.String({ minLength: 1 }), { description: "Replacement Work dependencies" })),
|
|
349
|
+
dependsOn: Type.Optional(stringTolerant(Type.Array(Type.String({ minLength: 1 }), { description: "Replacement Work dependencies" }))),
|
|
270
350
|
verify: Type.Optional(Type.String({ description: "Replacement completion gate" })),
|
|
271
|
-
resources: Type.Optional(Type.Array(Type.String({ minLength: 1 }), { description: "Replacement resource tags" })),
|
|
272
|
-
supersedes: Type.Array(Type.String({ minLength: 1 }), { minItems: 1, description: "Obsolete Work IDs replaced atomically" }),
|
|
351
|
+
resources: Type.Optional(stringTolerant(Type.Array(Type.String({ minLength: 1 }), { description: "Replacement resource tags" }))),
|
|
352
|
+
supersedes: stringTolerant(Type.Array(Type.String({ minLength: 1 }), { minItems: 1, description: "Obsolete Work IDs replaced atomically" })),
|
|
273
353
|
}, { additionalProperties: false }),
|
|
274
354
|
], { description: "Create, list, assign, release, reopen, or supersede current Work Items" });
|
|
275
355
|
|
|
@@ -289,7 +369,7 @@ export const LEADER_RECIPIENT = "leader";
|
|
|
289
369
|
/** Self-claim a pending board task. */
|
|
290
370
|
/** Worker-only Work claim operation. It queues an intent; the single-writer
|
|
291
371
|
* harness remains the only authority that can grant ownership. */
|
|
292
|
-
export const WorkerWorkToolParams =
|
|
372
|
+
export const WorkerWorkToolParams = actionUnion([
|
|
293
373
|
Type.Object({ action: Type.Literal("list") }, { additionalProperties: false }),
|
|
294
374
|
Type.Object({
|
|
295
375
|
action: Type.Literal("claim"),
|