pi-taskflow 0.0.8 → 0.0.9

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.
@@ -50,8 +50,8 @@ const ShorthandStep = Type.Object(
50
50
  );
51
51
 
52
52
  const TaskflowParams = Type.Object({
53
- action: StringEnum(["run", "save", "resume", "list"] as const, {
54
- description: "What to do: run a flow, save a definition, resume a paused run, or list saved flows",
53
+ action: StringEnum(["run", "save", "resume", "list", "agents"] as const, {
54
+ description: "What to do: run a flow, save a definition, resume a paused run, list saved flows, or list available agents you can use in phases",
55
55
  default: "run",
56
56
  }),
57
57
  name: Type.Optional(Type.String({ description: "Name of a saved flow (for run/save without inline define)" })),
@@ -219,7 +219,7 @@ export default function (pi: ExtensionAPI) {
219
219
  "Phases (agent, parallel, map, gate, reduce, approval, flow) form a DAG; intermediate outputs stay out of your context — only the final phase output is returned.",
220
220
  "Use action=run with an inline `define` (you write the DSL) or a saved `name`.",
221
221
  "For simple non-DAG delegations (like the subagent tool) skip the DSL: pass `task` (+optional `agent`) for one task, `tasks:[{task,agent?}]` to run in parallel, or `chain:[{task,agent?}]` to run sequentially (reference the prior step with {previous.output}).",
222
- "Use action=save to persist a definition as a reusable /tf:<name> command. action=resume continues a paused run. action=list shows saved flows.",
222
+ "Use action=save to persist a definition as a reusable /tf:<name> command. action=resume continues a paused run. action=list shows saved flows. Use action=agents to list available agents — do NOT invent agent names; either use an agent from that list or omit the 'agent' field to auto-select the default agent.",
223
223
  "DSL: {name, args?, concurrency?, budget?:{maxUSD,maxTokens}, phases:[{id, type, agent, task, dependsOn?, join?:'all'|'any', when?, retry?:{max,backoffMs,factor}, over?(map), as?(map), branches?(parallel), from?(reduce), use?(flow), with?(flow), output?:'json', final?}]}.",
224
224
  "Phase types: agent (one subagent), parallel (static branches), map (dynamic fan-out over an array), gate (VERDICT: PASS/BLOCK quality gate), reduce (aggregate from N phases), approval (human-in-the-loop pause), flow (run a saved sub-flow). join:'any' is an OR-join; when is a conditional guard; retry adds backoff; budget caps run cost.",
225
225
  "Interpolation: {args.X}, {steps.ID.output}, {steps.ID.json}, {item} (map), {previous.output}.",
@@ -235,6 +235,21 @@ export default function (pi: ExtensionAPI) {
235
235
  async execute(_id, params, signal, onUpdate, ctx) {
236
236
  const action = params.action ?? "run";
237
237
 
238
+ // agents — list available agents the LLM can use in phase definitions
239
+ if (action === "agents") {
240
+ const scope = params.scope ?? "both";
241
+ const { agents } = discoverAgents(ctx.cwd, scope as AgentScope, undefined);
242
+ const text = agents.length
243
+ ? agents
244
+ .map(
245
+ (a) =>
246
+ `- ${a.name} (${a.source}): ${a.description}${a.model ? ` [model: ${a.model}]` : ""}${a.tools?.length ? ` [tools: ${a.tools.join(", ")}]` : ""}`,
247
+ )
248
+ .join("\n")
249
+ : "No agents found. Use the default agent by omitting the 'agent' field in phases.";
250
+ return { content: [{ type: "text", text }], details: { action } satisfies TaskflowDetails };
251
+ }
252
+
238
253
  // list
239
254
  if (action === "list") {
240
255
  const flows = listFlows(ctx.cwd);
@@ -414,11 +414,12 @@ async function executePhase(
414
414
  if (type === "agent" || type === "gate" || type === "reduce") {
415
415
  const { text } = interpolate(phase.task ?? "", ctx);
416
416
  const fullTask = preRead + text;
417
- const inputHash = hashInput(phase.id, phase.agent ?? "", fullTask);
417
+ const agentName = resolveAgent(phase.agent, deps, state);
418
+ const inputHash = hashInput(phase.id, agentName, fullTask);
418
419
  const cached = cachedPhase(prior, inputHash);
419
420
  if (cached) return cached;
420
421
 
421
- const r = await runOne(phase.agent ?? defaultAgent(deps), fullTask, liveSink(state, phase.id, emitProgress));
422
+ const r = await runOne(agentName, fullTask, liveSink(state, phase.id, emitProgress));
422
423
  const ps = resultToPhaseState(phase.id, r, inputHash, parseJson);
423
424
  if (type === "gate" && ps.status === "done") ps.gate = parseGateVerdict(r.output);
424
425
  return ps;
@@ -428,7 +429,7 @@ async function executePhase(
428
429
  const branches = (phase.branches ?? []).map((b) => {
429
430
  const r = interpolate(b.task, ctx);
430
431
  return {
431
- agent: b.agent ?? phase.agent ?? defaultAgent(deps),
432
+ agent: resolveAgent(b.agent ?? phase.agent, deps, state),
432
433
  task: preRead + r.text,
433
434
  };
434
435
  });
@@ -458,7 +459,7 @@ async function executePhase(
458
459
  const tasks = arr.map((item) => {
459
460
  const localCtx = buildInterpolationContext(state, previousOutput, { [loopVar]: item });
460
461
  return {
461
- agent: phase.agent ?? defaultAgent(deps),
462
+ agent: resolveAgent(phase.agent, deps, state),
462
463
  task: preRead + interpolate(phase.task ?? "", localCtx).text,
463
464
  };
464
465
  });
@@ -641,6 +642,27 @@ function cachedPhase(prior: PhaseState | undefined, inputHash: string): PhaseSta
641
642
  return null;
642
643
  }
643
644
 
645
+ /**
646
+ * Resolve an agent name against available agents. Falls back to the default
647
+ * agent if the requested agent isn't found, logging a warning via safeEmit.
648
+ */
649
+ function resolveAgent(name: string | undefined, deps: RuntimeDeps, state: RunState): string {
650
+ const resolved = name ?? defaultAgent(deps);
651
+ if (name && !deps.agents.some((a) => a.name === name)) {
652
+ const fallback = defaultAgent(deps);
653
+ // Log only once per run to avoid noise.
654
+ if (!(state as any).__unknownAgentWarned) {
655
+ (state as any).__unknownAgentWarned = new Set<string>();
656
+ }
657
+ if (!(state as any).__unknownAgentWarned.has(name)) {
658
+ (state as any).__unknownAgentWarned.add(name);
659
+ console.warn(`[taskflow] Unknown agent "${name}", falling back to "${fallback}". Use action=agents to list available agents.`);
660
+ }
661
+ return fallback;
662
+ }
663
+ return resolved;
664
+ }
665
+
644
666
  function defaultAgent(deps: RuntimeDeps): string {
645
667
  return deps.agents[0]?.name ?? "default";
646
668
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-taskflow",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Lightweight workflow orchestration for the Pi coding agent — declarative multi-phase taskflows with dynamic fan-out, isolated subagent context, resumable runs, and saveable commands.",
5
5
  "keywords": [
6
6
  "pi-package",