@rohaquinlop/pi-subagents 0.3.0 → 0.5.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.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Error-message helpers for subagent tool returns.
3
+ *
4
+ * pi-agent-core drops the `isError` flag on normal tool returns, so `content`
5
+ * is the only reliable channel to communicate failure context back to the main
6
+ * LLM. These helpers build clear, actionable messages so the LLM can retry,
7
+ * redirect, or report.
8
+ */
9
+
10
+ /** Minimal shape the helper needs from a subagent result. */
11
+ interface SubagentErrorResult {
12
+ agent: string;
13
+ exitCode: number;
14
+ output: string;
15
+ progress: {
16
+ error?: string;
17
+ lastMessage?: string;
18
+ };
19
+ }
20
+
21
+ /**
22
+ * Build a clear, actionable error message for the main LLM when a single
23
+ * subagent fails. The `content` channel is the only reliable one (pi-agent-core
24
+ * drops a returned `isError` flag on normal tool returns), so this must carry
25
+ * enough context for the LLM to retry, redirect, or report.
26
+ */
27
+ export function buildSubagentErrorContent(result: SubagentErrorResult): string {
28
+ const parts: string[] = [];
29
+ parts.push(`[Subagent error] Agent "${result.agent}" failed (exit code ${result.exitCode}).`);
30
+ if (result.progress.error) {
31
+ parts.push(`Error: ${result.progress.error}`);
32
+ }
33
+ if (result.progress.lastMessage) {
34
+ parts.push(`Last message: ${result.progress.lastMessage}`);
35
+ }
36
+ if (result.output && result.output !== "(no output)") {
37
+ parts.push(`Output:\n${result.output}`);
38
+ }
39
+ return parts.join("\n");
40
+ }
41
+
42
+ export function buildPipelineErrorContent(
43
+ stepIndex: number,
44
+ stepAgent: string,
45
+ result: { exitCode: number; output: string; progress: { error?: string } },
46
+ ): string {
47
+ return [
48
+ `Pipeline failed at step ${stepIndex + 1} (agent: ${stepAgent}).`,
49
+ `Exit code: ${result.exitCode}`,
50
+ result.progress.error ? `Error: ${result.progress.error}` : null,
51
+ result.output ? `Output:\n${result.output}` : "(no output)",
52
+ ].filter(Boolean).join("\n");
53
+ }
54
+
55
+ export function buildLoopErrorContent(
56
+ iteration: number,
57
+ agentName: string,
58
+ result: { exitCode: number; output: string; progress: { error?: string } },
59
+ ): string {
60
+ return [
61
+ `Loop failed at iteration ${iteration + 1} (agent: ${agentName}).`,
62
+ `Exit code: ${result.exitCode}`,
63
+ result.progress.error ? `Error: ${result.progress.error}` : null,
64
+ result.output ? `Output:\n${result.output}` : "(no output)",
65
+ ].filter(Boolean).join("\n");
66
+ }
package/lib/helpers.ts CHANGED
@@ -50,8 +50,11 @@ export function parseAgentMd(content: string, filePath: string): AgentConfig | n
50
50
  const subagentAgents = fields.subagent_agents
51
51
  ? normalizeTools(fields.subagent_agents)
52
52
  : undefined;
53
+ const connector = fields.connector
54
+ ? fields.connector.replace(/^"|"$/g, "")
55
+ : undefined;
53
56
 
54
- return { name, description, tools, model, thinking, systemPrompt, filePath, subagentAgents };
57
+ return { name, description, tools, model, thinking, systemPrompt, filePath, subagentAgents, connector };
55
58
  }
56
59
 
57
60
  /**
@@ -90,6 +93,34 @@ export function mergeAgents(builtIn: AgentConfig[], user: AgentConfig[]): AgentC
90
93
  return Array.from(byName.values());
91
94
  }
92
95
 
96
+ /**
97
+ * Replace {previous} placeholder in a task string with the prior step's output.
98
+ * Truncation happens here — this is the single truncation point.
99
+ */
100
+ export function substitutePlaceholders(
101
+ task: string,
102
+ previousOutput: string,
103
+ maxContextChars: number = 16000,
104
+ ): string {
105
+ const truncated = previousOutput.length > maxContextChars
106
+ ? previousOutput.slice(0, maxContextChars) + "\n\n[Context truncated for pipeline]"
107
+ : previousOutput;
108
+ return task.replace(/\{previous\}/g, truncated);
109
+ }
110
+
111
+ /**
112
+ * Format an agent's output using its connector template.
113
+ * Pure formatting function — does NOT truncate. Truncation is handled
114
+ * by substitutePlaceholders() before this is called.
115
+ */
116
+ export function formatConnectorContext(
117
+ output: string,
118
+ connectorTemplate?: string,
119
+ ): string {
120
+ if (!connectorTemplate) return output;
121
+ return connectorTemplate.replace(/\{output\}/g, output);
122
+ }
123
+
93
124
  /**
94
125
  * Parses PI_SUBAGENT_ALLOWED env var into a Set of agent names.
95
126
  * Returns null if the env var is not set or empty (meaning no restriction).
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Cycle detector for tool-call streams. Watches a sliding window of
3
+ * tool-call signatures for a sub-sequence of length P repeated 3 times
4
+ * (P in [2,8]). Catches an agent stuck in a tool-call loop without
5
+ * imposing any count cap on legitimate work. Purely parent-side.
6
+ */
7
+ const WINDOW_SIZE = 24;
8
+ const MIN_PATTERN_LEN = 2;
9
+ const MAX_PATTERN_LEN = 8;
10
+ const REPETITIONS = 3;
11
+
12
+ export interface CycleResult {
13
+ cycle: boolean;
14
+ pattern?: string[]; // the repeating sub-sequence of signatures (length P)
15
+ }
16
+
17
+ /**
18
+ * Detect whether `newSig` completes a 3× cycle. Pure: does NOT mutate `history`.
19
+ * The caller pushes `newSig` onto its own history AFTER calling this.
20
+ */
21
+ export function detectCycle(history: string[], newSig: string): CycleResult {
22
+ const window = [...history, newSig];
23
+ const start = Math.max(0, window.length - WINDOW_SIZE);
24
+ const w = window.slice(start);
25
+ for (let p = MIN_PATTERN_LEN; p <= MAX_PATTERN_LEN; p++) {
26
+ const needed = p * REPETITIONS;
27
+ if (w.length < needed) continue;
28
+ const tail = w.slice(-needed);
29
+ const pattern = tail.slice(0, p);
30
+ let isCycle = true;
31
+ for (let i = 0; i < p; i++) {
32
+ if (pattern[i] !== tail[p + i] || pattern[i] !== tail[2 * p + i]) {
33
+ isCycle = false;
34
+ break;
35
+ }
36
+ }
37
+ if (isCycle) return { cycle: true, pattern };
38
+ }
39
+ return { cycle: false };
40
+ }
@@ -0,0 +1,53 @@
1
+ import type { AgentConfig, AgentUsage } from "./types";
2
+
3
+ /**
4
+ * Create a zeroed-out AgentUsage object.
5
+ */
6
+ export function zeroUsage(): AgentUsage {
7
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
8
+ }
9
+
10
+ /**
11
+ * Accumulate usage from one step/iteration into the running total.
12
+ */
13
+ export function accumulateUsage(total: AgentUsage, step: AgentUsage): AgentUsage {
14
+ return {
15
+ input: total.input + step.input,
16
+ output: total.output + step.output,
17
+ cacheRead: total.cacheRead + step.cacheRead,
18
+ cacheWrite: total.cacheWrite + step.cacheWrite,
19
+ cost: total.cost + step.cost,
20
+ turns: total.turns + step.turns,
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Validate that all referenced agent names exist in the loaded agents array.
26
+ * Returns the first missing agent name, or null if all are valid.
27
+ */
28
+ export function validateAgents(
29
+ agentNames: string[],
30
+ agents: AgentConfig[],
31
+ ): string | null {
32
+ for (const name of agentNames) {
33
+ if (!agents.some((a) => a.name === name)) return name;
34
+ }
35
+ return null;
36
+ }
37
+
38
+ /**
39
+ * Maximum total characters for accumulated loop context (prior iteration outputs).
40
+ * When exceeded, oldest iterations are dropped first, keeping only the last 2–3.
41
+ */
42
+ export const MAX_LOOP_CONTEXT = 48000;
43
+
44
+ /**
45
+ * Parse a judge agent's response to determine if it signals satisfaction.
46
+ * Extracts the first non-empty line, strips markdown formatting, and checks
47
+ * for word-boundary YES match. Returns false on any parse failure.
48
+ */
49
+ export function parseJudgeVerdict(response: string): boolean {
50
+ const firstLine = response.split('\n').find(l => l.trim()) || '';
51
+ const cleaned = firstLine.replace(/[*_`#]/g, '').trim().toUpperCase();
52
+ return /\bYES\b/.test(cleaned);
53
+ }
package/lib/types.ts CHANGED
@@ -7,4 +7,57 @@ export interface AgentConfig {
7
7
  systemPrompt: string;
8
8
  filePath: string;
9
9
  subagentAgents?: string[];
10
+ connector?: string; // Single-line prompt template, e.g. "## Findings\n\n{output}"
11
+ }
12
+
13
+ export interface AgentUsage {
14
+ input: number;
15
+ output: number;
16
+ cacheRead: number;
17
+ cacheWrite: number;
18
+ cost: number;
19
+ turns: number;
20
+ }
21
+
22
+ export interface PipelineStep {
23
+ agent: string;
24
+ task: string; // May contain {previous} placeholder
25
+ connector?: string; // Override agent's default connector for this step
26
+ }
27
+
28
+ export interface PipelineStepResult {
29
+ agent: string;
30
+ task: string;
31
+ output: string;
32
+ exitCode: number;
33
+ usage: AgentUsage;
34
+ durationMs: number;
35
+ }
36
+
37
+ export interface PipelineResult {
38
+ steps: PipelineStepResult[];
39
+ currentStep?: number; // Present during live execution updates
40
+ finalOutput: string;
41
+ stoppedAt?: number; // 0-indexed step where pipeline stopped (on error)
42
+ error?: string; // Error message if pipeline failed
43
+ totalUsage: AgentUsage;
44
+ totalDurationMs: number;
45
+ }
46
+
47
+ export interface LoopIterationResult {
48
+ iteration: number;
49
+ output: string;
50
+ exitCode: number;
51
+ usage: AgentUsage;
52
+ durationMs: number;
53
+ judgeVerdict?: { satisfied: boolean; response: string }; // Present when judge is configured
54
+ }
55
+
56
+ export interface LoopResult {
57
+ iterations: LoopIterationResult[];
58
+ currentIteration?: number; // Present during live execution updates
59
+ finalOutput: string;
60
+ stoppedBecause: "max_iterations" | "judge_satisfied" | "error";
61
+ totalUsage: AgentUsage;
62
+ totalDurationMs: number;
10
63
  }
package/package.json CHANGED
@@ -1,42 +1,42 @@
1
1
  {
2
- "name": "@rohaquinlop/pi-subagents",
3
- "version": "0.3.0",
4
- "description": "Pi extension for delegating tasks to subagents — parallel execution, agent discovery, and TUI rendering",
5
- "keywords": [
6
- "pi-package",
7
- "subagent",
8
- "parallel",
9
- "coding-agent",
10
- "extension"
11
- ],
12
- "license": "MIT",
13
- "author": "rohaquinlop",
14
- "repository": {
15
- "type": "git",
16
- "url": "git+https://github.com/rohaquinlop/pi-subagents.git"
17
- },
18
- "files": [
19
- "index.ts",
20
- "agents/",
21
- "tools/",
22
- "lib/",
23
- "README.md"
24
- ],
25
- "pi": {
26
- "extensions": [
27
- "./index.ts"
28
- ]
29
- },
30
- "peerDependencies": {
31
- "@earendil-works/pi-coding-agent": "*",
32
- "@earendil-works/pi-tui": "*",
33
- "@sinclair/typebox": "*"
34
- },
35
- "scripts": {
36
- "test": "vitest run",
37
- "test:watch": "vitest"
38
- },
39
- "devDependencies": {
40
- "vitest": "^3.2.0"
41
- }
2
+ "name": "@rohaquinlop/pi-subagents",
3
+ "version": "0.5.0",
4
+ "description": "Pi extension for delegating tasks to subagents — parallel execution, agent discovery, and TUI rendering",
5
+ "keywords": [
6
+ "pi-package",
7
+ "subagent",
8
+ "parallel",
9
+ "coding-agent",
10
+ "extension"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "rohaquinlop",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/rohaquinlop/pi-subagents.git"
17
+ },
18
+ "files": [
19
+ "index.ts",
20
+ "agents/",
21
+ "tools/",
22
+ "lib/",
23
+ "README.md"
24
+ ],
25
+ "pi": {
26
+ "extensions": [
27
+ "./index.ts"
28
+ ]
29
+ },
30
+ "peerDependencies": {
31
+ "@earendil-works/pi-coding-agent": "*",
32
+ "@earendil-works/pi-tui": "*",
33
+ "@sinclair/typebox": "*"
34
+ },
35
+ "scripts": {
36
+ "test": "vitest run",
37
+ "test:watch": "vitest"
38
+ },
39
+ "devDependencies": {
40
+ "vitest": "^3.2.0"
41
+ }
42
42
  }
@@ -6,6 +6,8 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
6
  import { createBashTool } from "@earendil-works/pi-coding-agent";
7
7
  import { Type } from "@sinclair/typebox";
8
8
 
9
+ const DEFAULT_SAFE_BASH_TIMEOUT_S = 300; // 5 minutes — kills stuck commands instead of relying on the 10-min wall-clock
10
+
9
11
  const DANGEROUS_PATTERNS = [
10
12
  /\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?(-[a-zA-Z]*r[a-zA-Z]*\s+)?(\/|~\/?\s|~\/?\b)/,
11
13
  /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+)?(-[a-zA-Z]*f[a-zA-Z]*\s+)?(\/|~\/?\s|~\/?\b)/,
@@ -46,7 +48,7 @@ export default function (pi: ExtensionAPI) {
46
48
  parameters: Type.Object({
47
49
  command: Type.String({ description: "Bash command to execute" }),
48
50
  timeout: Type.Optional(
49
- Type.Number({ description: "Timeout in seconds (optional)" }),
51
+ Type.Number({ description: "Timeout in seconds (default: 300s/5min; pass a larger value for builds/tests/installs)" }),
50
52
  ),
51
53
  }),
52
54
  async execute(toolCallId, params, signal, onUpdate, ctx) {
@@ -54,7 +56,8 @@ export default function (pi: ExtensionAPI) {
54
56
  if (danger) {
55
57
  throw new Error(danger);
56
58
  }
57
- return bashTool.execute(toolCallId, params, signal, onUpdate);
59
+ const timeout = params.timeout ?? DEFAULT_SAFE_BASH_TIMEOUT_S;
60
+ return bashTool.execute(toolCallId, { ...params, timeout }, signal, onUpdate);
58
61
  },
59
62
  });
60
63
  }