pi-taskflow 0.0.22 → 0.0.24
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/CHANGELOG.md +114 -0
- package/README.md +175 -46
- package/README.zh-CN.md +1 -1
- package/extensions/compile.ts +371 -0
- package/extensions/context-store.ts +447 -0
- package/extensions/index.ts +207 -4
- package/extensions/runner.ts +96 -3
- package/extensions/runtime.ts +310 -13
- package/extensions/schema.ts +34 -6
- package/extensions/store.ts +17 -4
- package/extensions/workspace.ts +206 -0
- package/package.json +6 -2
- package/skills/taskflow/SKILL.md +106 -2
package/extensions/index.ts
CHANGED
|
@@ -44,6 +44,16 @@ import {
|
|
|
44
44
|
} from "./store.ts";
|
|
45
45
|
import { CacheStore } from "./cache.ts";
|
|
46
46
|
import { safeParse } from "./interpolate.ts";
|
|
47
|
+
import {
|
|
48
|
+
isValidKey,
|
|
49
|
+
queueSpawn,
|
|
50
|
+
readVisibleFindings,
|
|
51
|
+
readTree,
|
|
52
|
+
nodeDepth,
|
|
53
|
+
writeFinding,
|
|
54
|
+
writeReport,
|
|
55
|
+
} from "./context-store.ts";
|
|
56
|
+
import { MAX_DYNAMIC_NESTING } from "./schema.ts";
|
|
47
57
|
|
|
48
58
|
interface TaskflowDetails {
|
|
49
59
|
state?: RunState;
|
|
@@ -73,8 +83,8 @@ const ShorthandStep = Type.Object(
|
|
|
73
83
|
);
|
|
74
84
|
|
|
75
85
|
const TaskflowParams = Type.Object({
|
|
76
|
-
action: StringEnum(["run", "save", "resume", "list", "agents", "init", "verify", "cache-clear"] as const, {
|
|
77
|
-
description: "What to do: run a flow, save a definition, resume a paused run, list saved flows, list available agents, init model role configuration, or clear the cross-run memoization cache",
|
|
86
|
+
action: StringEnum(["run", "save", "resume", "list", "agents", "init", "verify", "compile", "cache-clear"] as const, {
|
|
87
|
+
description: "What to do: run a flow, save a definition, resume a paused run, list saved flows, list available agents, init model role configuration, verify the DAG, compile the DAG to a Mermaid diagram + verification report, or clear the cross-run memoization cache",
|
|
78
88
|
default: "run",
|
|
79
89
|
}),
|
|
80
90
|
name: Type.Optional(Type.String({ description: "Name of a saved flow (for run/save without inline define)" })),
|
|
@@ -292,6 +302,21 @@ async function runFlow(
|
|
|
292
302
|
}
|
|
293
303
|
|
|
294
304
|
export default function (pi: ExtensionAPI) {
|
|
305
|
+
// ---- Dual identity ----------------------------------------------------
|
|
306
|
+
// When this extension is loaded INSIDE a subagent process that the taskflow
|
|
307
|
+
// runtime spawned with Shared Context Tree enabled, PI_TASKFLOW_CTX_DIR +
|
|
308
|
+
// PI_TASKFLOW_NODE_ID are present. In that case we register the ctx_* tools
|
|
309
|
+
// (the blackboard + supervision API) instead of the host `taskflow` tool —
|
|
310
|
+
// a subagent has no business orchestrating its own taskflows, and the host
|
|
311
|
+
// tool's heavy machinery is irrelevant there. When the env is absent we are
|
|
312
|
+
// the host: register `taskflow` + `/tf` exactly as before (zero change).
|
|
313
|
+
const ctxDir = process.env.PI_TASKFLOW_CTX_DIR;
|
|
314
|
+
const nodeId = process.env.PI_TASKFLOW_NODE_ID;
|
|
315
|
+
if (ctxDir && nodeId) {
|
|
316
|
+
registerCtxTools(pi, ctxDir, nodeId);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
295
320
|
// ---- Register per-saved-flow shortcut commands on session start ----
|
|
296
321
|
const registerSavedFlowCommands = (ctx: ExtensionContext) => {
|
|
297
322
|
const flows = listFlows(ctx.cwd);
|
|
@@ -377,6 +402,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
377
402
|
"Every delegation is tracked (runId), resumable across sessions, and saveable as /tf:<name> via action=save.",
|
|
378
403
|
"Use action=agents to list the 18 built-in agents (executor, scout, planner, analyst, critic, reviewer, risk-reviewer, security-reviewer, plan-arbiter, final-arbiter, test-engineer, doc-writer, executor-code, executor-fast, executor-ui, recover, verifier, visual-explorer). Do NOT invent agent names.",
|
|
379
404
|
"Phase types: agent, parallel (static branches), map (dynamic fan-out over array), gate (VERDICT: PASS/BLOCK), reduce (aggregate from N), approval (human-in-the-loop), flow (run saved sub-flow), loop (iterate until condition/convergence/cap), tournament (N variants, judge picks best/aggregate).",
|
|
405
|
+
"Use action=compile to generate a Mermaid diagram + verification report from a saved or inline flow — 0 tokens.",
|
|
380
406
|
"Interpolation: {args.X}, {steps.ID.output}, {steps.ID.json}, {item} (map), {previous.output}.",
|
|
381
407
|
].join(" "),
|
|
382
408
|
parameters: TaskflowParams,
|
|
@@ -545,6 +571,46 @@ export default function (pi: ExtensionAPI) {
|
|
|
545
571
|
return { content: [{ type: "text", text: lines.join("\n") }], details: { action } satisfies TaskflowDetails };
|
|
546
572
|
}
|
|
547
573
|
|
|
574
|
+
if (action === "compile") {
|
|
575
|
+
const { compileTaskflow } = await import("./compile.ts");
|
|
576
|
+
// Resolve definition: inline define (object or JSON/fenced string) then saved name.
|
|
577
|
+
let def: Taskflow | undefined;
|
|
578
|
+
let resolvedDefine: unknown = params.define;
|
|
579
|
+
if (typeof resolvedDefine === "string") {
|
|
580
|
+
const parsed = safeParse(resolvedDefine);
|
|
581
|
+
if (parsed && typeof parsed === "object") resolvedDefine = parsed;
|
|
582
|
+
}
|
|
583
|
+
if (resolvedDefine) {
|
|
584
|
+
const d = resolvedDefine as Record<string, unknown>;
|
|
585
|
+
if (typeof d === "object" && d !== null && Array.isArray(d.phases)) {
|
|
586
|
+
def = d as unknown as Taskflow;
|
|
587
|
+
} else if (isShorthand(resolvedDefine)) {
|
|
588
|
+
try {
|
|
589
|
+
def = desugar(resolvedDefine) as Taskflow;
|
|
590
|
+
} catch (e) {
|
|
591
|
+
return errorResult(action, `Invalid shorthand: ${e instanceof Error ? e.message : String(e)}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
} else if (params.name) {
|
|
595
|
+
const saved = getFlow(ctx.cwd, params.name);
|
|
596
|
+
if (saved) def = saved.def;
|
|
597
|
+
}
|
|
598
|
+
if (!def) {
|
|
599
|
+
return errorResult(action, "Provide 'define' (DSL) or 'name' (saved flow) to compile.");
|
|
600
|
+
}
|
|
601
|
+
// Schema validation first so a malformed graph gives a clean error
|
|
602
|
+
// rather than a half-rendered diagram.
|
|
603
|
+
const vr = validateTaskflow(def, { cwd: ctx.cwd ? String(ctx.cwd) : undefined });
|
|
604
|
+
if (!vr.ok) {
|
|
605
|
+
return errorResult(action, `Schema validation failed:\n${vr.errors.join("\n")}`);
|
|
606
|
+
}
|
|
607
|
+
const compiled = compileTaskflow(def);
|
|
608
|
+
return {
|
|
609
|
+
content: [{ type: "text", text: compiled.markdown }],
|
|
610
|
+
details: { action } satisfies TaskflowDetails,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
548
614
|
if (action === "cache-clear") {
|
|
549
615
|
const removed = new CacheStore(ctx.cwd).clear();
|
|
550
616
|
return {
|
|
@@ -754,9 +820,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
754
820
|
|
|
755
821
|
// ---- The /tf user command ----
|
|
756
822
|
pi.registerCommand("tf", {
|
|
757
|
-
description: "Taskflow: list | run <name> | show <name> | runs | init",
|
|
823
|
+
description: "Taskflow: list | run <name> | show <name> | compile <name> | runs | init",
|
|
758
824
|
getArgumentCompletions: (prefix) => {
|
|
759
|
-
const subs = ["list", "run", "show", "runs", "resume", "init", "save", "verify"];
|
|
825
|
+
const subs = ["list", "run", "show", "runs", "resume", "init", "save", "verify", "compile"];
|
|
760
826
|
const items = subs.map((s) => ({ value: s, label: s }));
|
|
761
827
|
const filtered = items.filter((i) => i.value.startsWith(prefix));
|
|
762
828
|
return filtered.length > 0 ? filtered : null;
|
|
@@ -785,6 +851,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
785
851
|
return;
|
|
786
852
|
}
|
|
787
853
|
|
|
854
|
+
if (sub === "compile") {
|
|
855
|
+
if (!arg) {
|
|
856
|
+
ctx.ui.notify("Usage: /tf compile <name> [lr|td]", "warning");
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
// `arg` may carry an optional direction suffix: "<name> lr" / "<name> td".
|
|
860
|
+
const parts = arg.trim().split(/\s+/);
|
|
861
|
+
const flowName = parts[0];
|
|
862
|
+
const direction = parts[1]?.toLowerCase() === "lr" ? "LR" : "TD";
|
|
863
|
+
const flow = getFlow(ctx.cwd, flowName);
|
|
864
|
+
if (!flow) {
|
|
865
|
+
ctx.ui.notify(`Flow not found: ${flowName}`, "error");
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
// Schema-validate before compiling so a malformed saved flow yields a
|
|
869
|
+
// clean error rather than a half-rendered diagram (mirrors the tool action).
|
|
870
|
+
const vr = validateTaskflow(flow.def, { cwd: ctx.cwd ? String(ctx.cwd) : undefined });
|
|
871
|
+
if (!vr.ok) {
|
|
872
|
+
ctx.ui.notify(`Schema validation failed:\n${vr.errors.join("\n")}`, "error");
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
const { compileTaskflow } = await import("./compile.ts");
|
|
876
|
+
const compiled = compileTaskflow(flow.def, { direction });
|
|
877
|
+
ctx.ui.notify(compiled.markdown, compiled.verification.ok ? "info" : "warning");
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
|
|
788
881
|
if (sub === "runs") {
|
|
789
882
|
const runs = listRuns(ctx.cwd, 50);
|
|
790
883
|
if (runs.length === 0) {
|
|
@@ -912,6 +1005,116 @@ export default function (pi: ExtensionAPI) {
|
|
|
912
1005
|
|
|
913
1006
|
// --- helpers ---
|
|
914
1007
|
|
|
1008
|
+
/**
|
|
1009
|
+
* Register the Shared Context Tree tools inside a subagent process. These read
|
|
1010
|
+
* & write the per-run blackboard at `ctxDir` on behalf of node `nodeId`.
|
|
1011
|
+
*
|
|
1012
|
+
* - ctx_read : read findings visible to this node (own + ancestors + completed others)
|
|
1013
|
+
* - ctx_write : write a finding (last-write-wins per key) so siblings can reuse it
|
|
1014
|
+
* - ctx_report : report a result upward to the parent
|
|
1015
|
+
* - ctx_spawn : queue child tasks the runtime picks up after this node finishes
|
|
1016
|
+
*/
|
|
1017
|
+
function registerCtxTools(pi: ExtensionAPI, ctxDir: string, nodeId: string) {
|
|
1018
|
+
const textResult = (text: string, isError = false): ToolResult => ({
|
|
1019
|
+
content: [{ type: "text", text }],
|
|
1020
|
+
details: { action: "ctx" },
|
|
1021
|
+
...(isError ? { isError: true } : {}),
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
pi.registerTool({
|
|
1025
|
+
name: "ctx_read",
|
|
1026
|
+
label: "Context Read",
|
|
1027
|
+
description:
|
|
1028
|
+
"Read shared findings from the taskflow blackboard (what sibling/ancestor agents already discovered). Pass a key to read one value, or omit to list all visible findings. Use this BEFORE re-reading files another agent may have already mapped.",
|
|
1029
|
+
parameters: Type.Object({
|
|
1030
|
+
key: Type.Optional(Type.String({ description: "Specific finding key to read; omit to get all visible findings." })),
|
|
1031
|
+
}),
|
|
1032
|
+
async execute(_id, params) {
|
|
1033
|
+
try {
|
|
1034
|
+
const out = readVisibleFindings(ctxDir, nodeId, params.key);
|
|
1035
|
+
return textResult(typeof out === "string" ? out : JSON.stringify(out ?? null, null, 2));
|
|
1036
|
+
} catch (e) {
|
|
1037
|
+
return textResult(`ctx_read failed: ${e instanceof Error ? e.message : String(e)}`, true);
|
|
1038
|
+
}
|
|
1039
|
+
},
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
pi.registerTool({
|
|
1043
|
+
name: "ctx_write",
|
|
1044
|
+
label: "Context Write",
|
|
1045
|
+
description:
|
|
1046
|
+
"Write a finding to the shared taskflow blackboard so sibling/descendant agents can reuse it without re-reading files. Key must be [A-Za-z0-9._-] (<=128 chars). Value is any JSON. Last write wins per key.",
|
|
1047
|
+
parameters: Type.Object({
|
|
1048
|
+
key: Type.String({ description: "Finding key, e.g. 'endpoints' or 'auth.summary'." }),
|
|
1049
|
+
value: Type.Unknown({ description: "The value to store (string, number, object, or array)." }),
|
|
1050
|
+
}),
|
|
1051
|
+
async execute(_id, params) {
|
|
1052
|
+
if (!isValidKey(params.key)) {
|
|
1053
|
+
return textResult(`ctx_write rejected: invalid key '${params.key}'.`, true);
|
|
1054
|
+
}
|
|
1055
|
+
try {
|
|
1056
|
+
writeFinding(ctxDir, nodeId, params.key, params.value);
|
|
1057
|
+
return textResult(`Stored finding '${params.key}'.`);
|
|
1058
|
+
} catch (e) {
|
|
1059
|
+
return textResult(`ctx_write failed: ${e instanceof Error ? e.message : String(e)}`, true);
|
|
1060
|
+
}
|
|
1061
|
+
},
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
pi.registerTool({
|
|
1065
|
+
name: "ctx_report",
|
|
1066
|
+
label: "Context Report",
|
|
1067
|
+
description:
|
|
1068
|
+
"Report your result upward to the parent task. Provide a concise summary and optional structured JSON. The parent (and downstream phases) will see this report.",
|
|
1069
|
+
parameters: Type.Object({
|
|
1070
|
+
summary: Type.String({ description: "Concise summary of what you accomplished / found." }),
|
|
1071
|
+
structured: Type.Optional(Type.Unknown({ description: "Optional structured result (JSON)." })),
|
|
1072
|
+
}),
|
|
1073
|
+
async execute(_id, params) {
|
|
1074
|
+
try {
|
|
1075
|
+
writeReport(ctxDir, nodeId, params.summary, params.structured);
|
|
1076
|
+
return textResult("Report recorded.");
|
|
1077
|
+
} catch (e) {
|
|
1078
|
+
return textResult(`ctx_report failed: ${e instanceof Error ? e.message : String(e)}`, true);
|
|
1079
|
+
}
|
|
1080
|
+
},
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
pi.registerTool({
|
|
1084
|
+
name: "ctx_spawn",
|
|
1085
|
+
label: "Context Spawn",
|
|
1086
|
+
description:
|
|
1087
|
+
"Delegate sub-tasks to NEW child agents. After you finish, the runtime runs each child (isolated context) and folds their reports back into your output. Use when you discover the work needs to fan out. Each assignment is EITHER {task, agent?} for one flat task, OR {subflow, defaultAgent?} where subflow is an inline plan {phases:[...]} (a dependency-bearing DAG: phases can use dependsOn / map / gate / reduce). Use a subflow when the delegated work itself has multiple coordinated steps.",
|
|
1088
|
+
parameters: Type.Object({
|
|
1089
|
+
assignments: Type.Array(
|
|
1090
|
+
Type.Object({
|
|
1091
|
+
task: Type.Optional(Type.String({ description: "A single child task prompt (use this OR subflow, not both)." })),
|
|
1092
|
+
agent: Type.Optional(Type.String({ description: "Agent name for a flat task (optional)." })),
|
|
1093
|
+
subflow: Type.Optional(Type.Unknown({ description: "An inline Taskflow plan {phases:[...]} or a bare phases array, run as a nested validated sub-flow." })),
|
|
1094
|
+
defaultAgent: Type.Optional(Type.String({ description: "Fallback agent for subflow phases that don't name their own (optional)." })),
|
|
1095
|
+
}),
|
|
1096
|
+
{ description: "Child tasks to spawn (1..16). Each is a flat {task} or a {subflow} DAG." },
|
|
1097
|
+
),
|
|
1098
|
+
}),
|
|
1099
|
+
async execute(_id, params) {
|
|
1100
|
+
// Depth cap: walk the parent chain in the tree to find this node's depth.
|
|
1101
|
+
try {
|
|
1102
|
+
const depth = nodeDepth(readTree(ctxDir), nodeId);
|
|
1103
|
+
if (depth >= MAX_DYNAMIC_NESTING) {
|
|
1104
|
+
return textResult(
|
|
1105
|
+
`ctx_spawn rejected: depth ${depth} >= MAX_DYNAMIC_NESTING (${MAX_DYNAMIC_NESTING}). Do the work yourself.`,
|
|
1106
|
+
true,
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
const n = queueSpawn(ctxDir, nodeId, params.assignments);
|
|
1110
|
+
return textResult(`Queued ${n} child task(s); they will run after you finish and their reports will be appended to your output.`);
|
|
1111
|
+
} catch (e) {
|
|
1112
|
+
return textResult(`ctx_spawn failed: ${e instanceof Error ? e.message : String(e)}`, true);
|
|
1113
|
+
}
|
|
1114
|
+
},
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
|
|
915
1118
|
function errorResult(action: string, message: string): ToolResult {
|
|
916
1119
|
return {
|
|
917
1120
|
content: [{ type: "text", text: message }],
|
package/extensions/runner.ts
CHANGED
|
@@ -60,6 +60,14 @@ export interface RunOptions {
|
|
|
60
60
|
* the prior behaviour (no idle timeout). Defaults to DEFAULT_IDLE_TIMEOUT_MS.
|
|
61
61
|
*/
|
|
62
62
|
idleTimeoutMs?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Shared Context Tree (opt-in). When set, the spawned subagent receives
|
|
65
|
+
* PI_TASKFLOW_CTX_DIR + PI_TASKFLOW_NODE_ID in its environment and is loaded
|
|
66
|
+
* with this extension via `--extension`, so it can register the ctx_* tools
|
|
67
|
+
* (read/write/report/spawn) that read & write the per-run blackboard.
|
|
68
|
+
*/
|
|
69
|
+
ctxDir?: string;
|
|
70
|
+
nodeId?: string;
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
/**
|
|
@@ -71,6 +79,41 @@ export interface RunOptions {
|
|
|
71
79
|
*/
|
|
72
80
|
const DEFAULT_IDLE_TIMEOUT_MS = 5 * 60_000;
|
|
73
81
|
|
|
82
|
+
/** The Shared Context Tree tool names a subagent may call when sharing is on. */
|
|
83
|
+
export const CTX_TOOL_NAMES = ["ctx_read", "ctx_write", "ctx_report", "ctx_spawn"] as const;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Guidance appended to a subagent's system prompt when the Shared Context Tree
|
|
87
|
+
* is enabled for its phase. Registering the ctx_* tools makes them AVAILABLE;
|
|
88
|
+
* this block is what makes the model actually USE them with the right discipline
|
|
89
|
+
* (read-before-you-explore; publish reusable findings; report up; delegate when
|
|
90
|
+
* work fans out). Kept short and imperative on purpose.
|
|
91
|
+
*/
|
|
92
|
+
export const CTX_TOOLS_GUIDANCE = [
|
|
93
|
+
"## Shared Context Tree (you are part of a coordinated team of agents)",
|
|
94
|
+
"",
|
|
95
|
+
"You are one agent in a tree working a shared goal, with a shared blackboard",
|
|
96
|
+
"and an upward report channel. Use these tools deliberately \u2014 they save tokens",
|
|
97
|
+
"and prevent the team from duplicating work:",
|
|
98
|
+
"",
|
|
99
|
+
"- ctx_read(key?): BEFORE exploring the codebase or re-reading files, call",
|
|
100
|
+
" ctx_read with no arguments to see what teammates already discovered. If a",
|
|
101
|
+
" finding you need already exists, REUSE it instead of re-deriving it.",
|
|
102
|
+
"- ctx_write(key, value): when you discover something other agents will likely",
|
|
103
|
+
" need (a file map, an endpoint list, an interface, a config value), publish it",
|
|
104
|
+
" under a short key (e.g. 'endpoints', 'db.schema'). Keep values concise and",
|
|
105
|
+
" structured (JSON) so others can consume them directly.",
|
|
106
|
+
"- ctx_report(summary, structured?): when you finish, report your result upward",
|
|
107
|
+
" so the parent task and downstream steps can see it. Lead with the outcome.",
|
|
108
|
+
"- ctx_spawn(assignments[]): if you discover the work should fan out into",
|
|
109
|
+
" independent sub-tasks, delegate them as child agents. They run after you",
|
|
110
|
+
" finish and their reports are folded back into your output. Only spawn when it",
|
|
111
|
+
" genuinely parallelizes \u2014 otherwise just do the work yourself.",
|
|
112
|
+
"",
|
|
113
|
+
"Default habit: ctx_read first, do the work (reusing shared findings), ctx_write",
|
|
114
|
+
"anything reusable, then ctx_report your result.",
|
|
115
|
+
].join("\n");
|
|
116
|
+
|
|
74
117
|
export function isFailed(r: RunResult): boolean {
|
|
75
118
|
return r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted";
|
|
76
119
|
}
|
|
@@ -281,6 +324,25 @@ function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
|
281
324
|
return { command: "pi", args };
|
|
282
325
|
}
|
|
283
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Resolve the path to this extension's entry file, so a spawned subagent can be
|
|
329
|
+
* launched with `--extension <path>` and register the ctx_* tools. Returns
|
|
330
|
+
* undefined if it cannot be resolved (the subagent then simply runs without the
|
|
331
|
+
* ctx tools — fail-open: context sharing degrades to "no sharing").
|
|
332
|
+
*/
|
|
333
|
+
export function ctxExtensionPath(): string | undefined {
|
|
334
|
+
const override = process.env.PI_TASKFLOW_EXT_PATH;
|
|
335
|
+
if (override) return override;
|
|
336
|
+
try {
|
|
337
|
+
const here = path.dirname(new URL(import.meta.url).pathname);
|
|
338
|
+
const entry = path.join(here, "index.ts");
|
|
339
|
+
if (fs.existsSync(entry)) return entry;
|
|
340
|
+
} catch {
|
|
341
|
+
/* fall through */
|
|
342
|
+
}
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
|
|
284
346
|
/**
|
|
285
347
|
* Run a single subagent task. Resolves the agent from `agents` by name and
|
|
286
348
|
* spawns an isolated pi process, returning structured output + usage.
|
|
@@ -310,7 +372,15 @@ export async function runAgentTask(
|
|
|
310
372
|
|
|
311
373
|
const model = opts.model ?? agent.model;
|
|
312
374
|
const thinking = opts.thinking ?? agent.thinking ?? globalThinking;
|
|
313
|
-
const
|
|
375
|
+
const ctxEnabledEarly = Boolean(opts.ctxDir && opts.nodeId);
|
|
376
|
+
let tools = opts.tools ?? agent.tools;
|
|
377
|
+
// If the agent restricts tools to a whitelist, the ctx_* tools we register
|
|
378
|
+
// would be filtered out by `--tools` even though they're registered. When
|
|
379
|
+
// context sharing is on, extend the whitelist so the subagent can actually
|
|
380
|
+
// call them. (No whitelist = all tools available = nothing to do.)
|
|
381
|
+
if (ctxEnabledEarly && tools && tools.length > 0) {
|
|
382
|
+
tools = [...new Set([...tools, ...CTX_TOOL_NAMES])];
|
|
383
|
+
}
|
|
314
384
|
|
|
315
385
|
const args: string[] = ["--mode", "json", "-p", "--no-session"];
|
|
316
386
|
if (model) args.push("--model", model);
|
|
@@ -332,18 +402,40 @@ export async function runAgentTask(
|
|
|
332
402
|
};
|
|
333
403
|
|
|
334
404
|
try {
|
|
335
|
-
|
|
405
|
+
const ctxEnabled = Boolean(opts.ctxDir && opts.nodeId);
|
|
406
|
+
// Build the appended system prompt = the agent's own prompt PLUS, when the
|
|
407
|
+
// Shared Context Tree is enabled for this phase, a guidance block that tells
|
|
408
|
+
// the subagent the ctx_* tools exist and the discipline for using them.
|
|
409
|
+
// Without this the model only sees terse tool descriptions and rarely uses
|
|
410
|
+
// them proactively (capability != usage).
|
|
411
|
+
const appendedPrompt = [agent.systemPrompt.trim(), ctxEnabled ? CTX_TOOLS_GUIDANCE : ""]
|
|
412
|
+
.filter(Boolean)
|
|
413
|
+
.join("\n\n");
|
|
414
|
+
if (appendedPrompt) {
|
|
336
415
|
// Allocate the temp dir + path BEFORE any fallible I/O so that if
|
|
337
416
|
// writeFile throws, tmpPromptDir/tmpPromptPath are already set and
|
|
338
417
|
// the finally block can clean up the directory (F-004).
|
|
339
418
|
tmpPromptDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-taskflow-"));
|
|
340
419
|
const safeName = agent.name.replace(/[^\w.-]+/g, "_");
|
|
341
420
|
tmpPromptPath = path.join(tmpPromptDir, `prompt-${safeName}.md`);
|
|
342
|
-
await writePromptToTempFile(tmpPromptPath,
|
|
421
|
+
await writePromptToTempFile(tmpPromptPath, appendedPrompt);
|
|
343
422
|
args.push("--append-system-prompt", tmpPromptPath);
|
|
344
423
|
}
|
|
345
424
|
args.push(`Task: ${task}`);
|
|
346
425
|
|
|
426
|
+
// Shared Context Tree opt-in: load THIS extension into the subagent so it
|
|
427
|
+
// can register the ctx_* tools, and pass the blackboard dir + node id via
|
|
428
|
+
// env. `--extension` is the explicit, self-documenting fallback that does
|
|
429
|
+
// not rely on the subagent auto-discovering user/project extensions in
|
|
430
|
+
// `-p` mode. The env vars drive the dual-identity branch in index.ts.
|
|
431
|
+
const ctxEnv: Record<string, string> = {};
|
|
432
|
+
if (opts.ctxDir && opts.nodeId) {
|
|
433
|
+
const selfPath = ctxExtensionPath();
|
|
434
|
+
if (selfPath) args.push("--extension", selfPath);
|
|
435
|
+
ctxEnv.PI_TASKFLOW_CTX_DIR = opts.ctxDir;
|
|
436
|
+
ctxEnv.PI_TASKFLOW_NODE_ID = opts.nodeId;
|
|
437
|
+
}
|
|
438
|
+
|
|
347
439
|
let wasAborted = false;
|
|
348
440
|
let idleTimedOut = false;
|
|
349
441
|
let killedBySignal: string | undefined;
|
|
@@ -353,6 +445,7 @@ export async function runAgentTask(
|
|
|
353
445
|
cwd: opts.cwd ?? defaultCwd,
|
|
354
446
|
shell: false,
|
|
355
447
|
stdio: ["ignore", "pipe", "pipe"],
|
|
448
|
+
env: { ...process.env, ...ctxEnv },
|
|
356
449
|
});
|
|
357
450
|
if (proc.pid) activeChildren.add(proc.pid);
|
|
358
451
|
let buffer = "";
|