@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fradser/pi-agent-teams",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "Multi-agent team system for Pi — resident teammates, shared task board, and peer-to-peer messaging",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -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 { Teammate } from "./types.ts";
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 = Type.Union([
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 = Type.Union([
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 = Type.Union([
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"),