@signalridge/pi-workflows 1.4.0 → 1.6.0

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/src/rpc-client.ts CHANGED
@@ -1,15 +1,14 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type {
3
+ ManagedRoutingPolicy,
3
4
  ManagedSpawnResponse as ProtocolManagedSpawnResponse,
4
5
  ManagedTerminalSnapshot as ProtocolManagedTerminalSnapshot,
5
- WorkflowTier,
6
6
  } from "@signalridge/pi-subagents-protocol";
7
7
  import {
8
8
  PROTOCOL_VERSION,
9
9
  parseManagedSpawnResponse,
10
10
  parseProtocolPing,
11
11
  requiredCapabilitiesMatch,
12
- workflowTierCapabilityMatch,
13
12
  } from "@signalridge/pi-subagents-protocol";
14
13
  import type { WorkflowOwner } from "./journal.js";
15
14
 
@@ -18,11 +17,8 @@ export interface DispatchTask {
18
17
  subagent_type: string;
19
18
  prompt: string;
20
19
  description: string;
21
- tier?: WorkflowTier;
22
- /** Exact provider/model reference; resolution remains inside pi-subagents. */
23
- model?: string;
24
- /** Optional thinking suffix/override resolved by pi-subagents. */
25
- thinking?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | "off";
20
+ /** Agent-tier key, from the host's own catalogue. The only model policy here. */
21
+ tier?: string;
26
22
  /** Per-call worktree request owned by pi-subagents. */
27
23
  isolation?: "worktree";
28
24
  /** Forwarded toolset hint (e.g. "web-research") for pi-subagents to augment tools. */
@@ -105,9 +101,11 @@ function callRpc<T>(
105
101
 
106
102
  export const REQUIRED_SUBAGENTS_PROTOCOL = PROTOCOL_VERSION;
107
103
  export const PROTOCOL_DIAGNOSTIC =
108
- "@signalridge/pi-workflows requires @signalridge/pi-subagents protocol v3 with managedSpawn, lifecycleOwner, ownedStop, ownedQuiescence, workflowTiers, and managedPolicy capabilities. " +
104
+ `@signalridge/pi-workflows requires @signalridge/pi-subagents protocol v${PROTOCOL_VERSION} with managedSpawn, lifecycleOwner, ownedStop, childContext, ownedQuiescence, agentTiers, and managedPolicy, plus its Agent-tier routing policy. ` +
109
105
  "Install @signalridge/pi-subagents and @signalridge/pi-workflows exactly once; Pi loads both from their configured pi.extensions manifests.";
110
106
 
107
+ const CAPABILITY_DIAGNOSTIC = `subagents protocol v${PROTOCOL_VERSION} is required, with every capability and an Agent-tier routing policy`;
108
+
111
109
  export const CHILD_CONTEXT_QUERY_TIMEOUT_MS = 250;
112
110
 
113
111
  /**
@@ -169,7 +167,17 @@ export function queryChildSessionContextImmediate(events: WorkflowEventBus): boo
169
167
  }
170
168
 
171
169
  /** Verify the separately configured pi-subagents package before workflow execution. */
172
- export async function checkManagedSpawnProtocol(events: WorkflowEventBus, signal?: AbortSignal): Promise<void> {
170
+ export interface ManagedProtocolCheck {
171
+ /** The host's Agent-tier catalogue, as of this check. */
172
+ routingPolicy: ManagedRoutingPolicy;
173
+ /** Whole-catalogue identity; used only where a per-tier identity cannot be. */
174
+ routingPolicyFingerprint: string;
175
+ }
176
+
177
+ export async function checkManagedSpawnProtocol(
178
+ events: WorkflowEventBus,
179
+ signal?: AbortSignal,
180
+ ): Promise<ManagedProtocolCheck> {
173
181
  const id = requestId();
174
182
  const reply = await new Promise<unknown>((resolve, reject) => {
175
183
  const replyChannel = `subagents:rpc:ping:reply:${id}`;
@@ -228,57 +236,40 @@ export async function checkManagedSpawnProtocol(events: WorkflowEventBus, signal
228
236
  let ping: ReturnType<typeof parseProtocolPing>;
229
237
  try {
230
238
  ping = parseProtocolPing(reply);
231
- } catch {
232
- if (
233
- !isRecord(reply) ||
234
- typeof reply.version !== "number" ||
235
- !isRecord(reply.capabilities) ||
236
- reply.version < REQUIRED_SUBAGENTS_PROTOCOL ||
237
- reply.capabilities.managedSpawn !== true ||
238
- reply.capabilities.lifecycleOwner !== true ||
239
- reply.capabilities.ownedStop !== true ||
240
- reply.capabilities.ownedQuiescence !== true ||
241
- reply.capabilities.managedPolicy !== true
242
- ) {
243
- throw new Error(
244
- `subagents protocol v${REQUIRED_SUBAGENTS_PROTOCOL} with managed spawning, owned stop, owned quiescence, and managed policy capabilities is required`,
245
- );
239
+ } catch (error: unknown) {
240
+ // Anything shaped like a ping is a peer that is present but not v4-complete,
241
+ // and the actionable answer is the same in every such case: install a
242
+ // matching pair. A reply that is not a ping at all gets its own message,
243
+ // because "upgrade the peer" would be the wrong thing to go do.
244
+ if (!isRecord(reply) || typeof reply.version !== "number") {
245
+ throw new Error("subagents protocol ping did not return a valid version/capability envelope");
246
246
  }
247
- throw new Error("subagents protocol ping did not return a valid version/capability envelope");
247
+ throw new Error(`${CAPABILITY_DIAGNOSTIC} (${error instanceof Error ? error.message : String(error)})`);
248
248
  }
249
+ // Every v4 capability is required, so one check covers them all. The parser
250
+ // has already rejected a ping without a well-formed routing policy.
249
251
  if (ping.version < REQUIRED_SUBAGENTS_PROTOCOL || !requiredCapabilitiesMatch(ping.capabilities)) {
250
- throw new Error(
251
- `subagents protocol v${REQUIRED_SUBAGENTS_PROTOCOL} with managed spawning, owned stop, and owned quiescence capability is required`,
252
- );
253
- }
254
- if (!workflowTierCapabilityMatch(ping.capabilities)) {
255
- throw new Error("subagents workflow tier capability is required before tiered managed spawning");
256
- }
257
- if (ping.capabilities.managedPolicy !== true) {
258
- throw new Error("subagents managed policy capability is required before policy-bearing managed spawning");
252
+ throw new Error(CAPABILITY_DIAGNOSTIC);
259
253
  }
254
+ return {
255
+ routingPolicy: ping.routingPolicy.policy,
256
+ routingPolicyFingerprint: ping.routingPolicy.fingerprint,
257
+ };
260
258
  }
261
259
 
262
260
  export type ManagedTerminalSnapshot = ProtocolManagedTerminalSnapshot;
263
261
  export type ManagedSpawnResponse = ProtocolManagedSpawnResponse;
264
262
 
265
263
  export interface ManagedSpawnClient {
266
- /** String remains accepted for deterministic legacy fixtures; protocol v3 returns ManagedSpawnResponse. */
267
- /**
268
- * The fourth argument is the current attempt for protocol-v3 clients. The
269
- * optional fifth argument is retained only for old in-process test/fixture
270
- * clients that used the former context, attemptId shape; it is never a
271
- * runtime context and is not sent over RPC.
272
- */
264
+ /** Managed spawn returns a structured response. */
273
265
  spawn(
274
266
  task: DispatchTask,
275
267
  runId: string,
276
268
  nodeId: string,
277
269
  attemptId?: string,
278
- legacyAttemptId?: string,
279
270
  signal?: AbortSignal,
280
271
  ): Promise<ManagedSpawnResponse | string>;
281
- /** Unscoped v2 stop, retained for existing callers. */
272
+ /** Unscoped stop for non-managed callers. */
282
273
  stop(agentId: string): Promise<void>;
283
274
  /** Owner-scoped v3 stop used by workflow lifecycle operations. */
284
275
  stopOwned?(agentId: string, owner: WorkflowOwner): Promise<void>;
@@ -290,8 +281,8 @@ export interface ManagedSpawnClient {
290
281
  timeoutMs?: number,
291
282
  owners?: WorkflowOwner[],
292
283
  ): Promise<{ settled: boolean; pending: string[] }>;
293
- /** Optional startup capability check; workflow execution remains event-bus-only. */
294
- checkProtocol?: () => Promise<void>;
284
+ /** Fresh capability and routing-policy check; workflow execution remains event-bus-only. */
285
+ checkProtocol?: () => Promise<ManagedProtocolCheck>;
295
286
  }
296
287
 
297
288
  /** Event-bus-only client for the additive pi-subagents managed protocol. */
@@ -320,7 +311,7 @@ export function createManagedSpawnClient(
320
311
  * error for the same key with a different fingerprint. Same key + same
321
312
  * fingerprint still reuses the persisted tombstone (per-call cache).
322
313
  */
323
- async spawn(task, runId, nodeId, attemptId, _legacyAttemptId, signal) {
314
+ async spawn(task, runId, nodeId, attemptId, signal) {
324
315
  const effectiveAttemptId = attemptId ?? `attempt-1`;
325
316
  const spawnKey = `${runId}/${nodeId}/${effectiveAttemptId}`;
326
317
  const requestSignal =
@@ -339,8 +330,6 @@ export function createManagedSpawnClient(
339
330
  ...(task.toolset === undefined ? {} : { toolset: task.toolset }),
340
331
  ...(task.thread === undefined ? {} : { thread: task.thread }),
341
332
  ...(task.excludeTools === undefined ? {} : { excludeTools: task.excludeTools }),
342
- ...(task.model === undefined ? {} : { model: task.model }),
343
- ...(task.thinking === undefined ? {} : { thinking: task.thinking }),
344
333
  ...(task.isolation === undefined ? {} : { isolation: task.isolation }),
345
334
  owner: { extension: "pi-workflows", runId, nodeId, attemptId: effectiveAttemptId },
346
335
  },