pi-subagents 0.13.0 → 0.13.1
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 +13 -0
- package/README.md +24 -0
- package/index.ts +1 -1
- package/intercom-bridge.ts +115 -0
- package/package.json +1 -1
- package/subagent-executor.ts +29 -18
- package/types.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.13.1] - 2026-04-13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added optional intercom orchestration bridge for delegated runs. When enabled via `intercomBridge` (default `fork-only`) and `pi-intercom` is available, child subagents get runtime coordination instructions for contacting the orchestrator session via `intercom`, and `intercom` is auto-added to the child tool allowlist when needed.
|
|
9
|
+
- Added unit coverage for intercom bridge activation, config handling, and extension allowlist behavior.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Normalized `subagent-executor.ts` relative imports to `.ts` specifiers to match direct TypeScript runtime loading.
|
|
13
|
+
- Documented `pi-intercom` installation and activation requirements in README.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Tightened intercom extension allowlist matching to avoid false positives from similarly named extension paths.
|
|
17
|
+
|
|
5
18
|
## [0.13.0] - 2026-04-11
|
|
6
19
|
|
|
7
20
|
### Added
|
package/README.md
CHANGED
|
@@ -331,6 +331,9 @@ Chains can be created from the Agents Manager template picker ("Blank Chain"), o
|
|
|
331
331
|
| Parallel | Yes | `{ tasks: [{agent, task}...] }` - via TUI toggle or converted to chain for async |
|
|
332
332
|
|
|
333
333
|
Execution context defaults to `context: "fresh"`, which starts each child run from a clean session. Set `context: "fork"` to start each child from a real branched session created from the parent's current leaf.
|
|
334
|
+
When `intercomBridge` is enabled (default: `fork-only`) and `pi-intercom` is installed/enabled, forked children get runtime instructions for contacting the orchestrator session via `intercom({ action: "ask"|"send", ... })`.
|
|
335
|
+
|
|
336
|
+
> **Note:** Intercom bridging requires the [pi-intercom](https://github.com/nicobailon/pi-intercom) extension. Install it with `pi install npm:pi-intercom`.
|
|
334
337
|
|
|
335
338
|
All modes support foreground and background execution. Foreground is the default (the call waits and streams progress). For programmatic background launch, use `clarify: false, async: true`. For interactive background launch, use `clarify: true` and press `b` in the TUI before running. Chains with parallel steps (`{ parallel: [...] }`) run concurrently with configurable `concurrency` and `failFast` options.
|
|
336
339
|
|
|
@@ -778,6 +781,27 @@ Sessions are always enabled — every subagent run gets a session directory for
|
|
|
778
781
|
|
|
779
782
|
Per-agent `maxSubagentDepth` can tighten that limit further for child runs, but it does not relax an already inherited stricter limit.
|
|
780
783
|
|
|
784
|
+
### `intercomBridge`
|
|
785
|
+
|
|
786
|
+
Controls whether subagents receive runtime intercom coordination instructions (and `intercom` is auto-added to their tool allowlist when needed).
|
|
787
|
+
|
|
788
|
+
```json
|
|
789
|
+
{
|
|
790
|
+
"intercomBridge": "fork-only"
|
|
791
|
+
}
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
Values:
|
|
795
|
+
- `"fork-only"` (default): inject intercom bridge only when `context: "fork"`
|
|
796
|
+
- `"always"`: inject bridge in both `fresh` and `fork`
|
|
797
|
+
- `"off"`: disable bridge entirely
|
|
798
|
+
|
|
799
|
+
Bridge activation also requires all of the following:
|
|
800
|
+
- [pi-intercom](https://github.com/nicobailon/pi-intercom) is installed (`pi install npm:pi-intercom`)
|
|
801
|
+
- `~/.pi/agent/intercom/config.json` is not set to `"enabled": false`
|
|
802
|
+
- the current session has a name to target as orchestrator
|
|
803
|
+
- if agent `extensions` is an explicit allowlist, it must include `pi-intercom`
|
|
804
|
+
|
|
781
805
|
### `worktreeSetupHook`
|
|
782
806
|
|
|
783
807
|
`worktreeSetupHook` configures an optional setup hook for worktree-isolated parallel runs. The hook runs once per created worktree, after `git worktree add` succeeds and before the agent starts.
|
package/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Toggle: async parameter (default: false, configurable via config.json)
|
|
10
10
|
*
|
|
11
11
|
* Config file: ~/.pi/agent/extensions/subagent/config.json
|
|
12
|
-
* { "asyncByDefault": true, "maxSubagentDepth": 1, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
|
|
12
|
+
* { "asyncByDefault": true, "maxSubagentDepth": 1, "intercomBridge": "fork-only", "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import * as fs from "node:fs";
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { AgentConfig } from "./agents.ts";
|
|
5
|
+
import type { ExtensionConfig } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_INTERCOM_EXTENSION_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "pi-intercom");
|
|
8
|
+
const DEFAULT_INTERCOM_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "intercom", "config.json");
|
|
9
|
+
const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
|
|
10
|
+
|
|
11
|
+
export type IntercomBridgeMode = NonNullable<ExtensionConfig["intercomBridge"]>;
|
|
12
|
+
|
|
13
|
+
export interface IntercomBridgeState {
|
|
14
|
+
active: boolean;
|
|
15
|
+
mode: IntercomBridgeMode;
|
|
16
|
+
orchestratorTarget?: string;
|
|
17
|
+
extensionDir: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ResolveIntercomBridgeInput {
|
|
21
|
+
mode: unknown;
|
|
22
|
+
context: "fresh" | "fork" | undefined;
|
|
23
|
+
orchestratorTarget?: string;
|
|
24
|
+
extensionDir?: string;
|
|
25
|
+
configPath?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolveIntercomBridgeMode(value: unknown): IntercomBridgeMode {
|
|
29
|
+
if (value === "off" || value === "always" || value === "fork-only") return value;
|
|
30
|
+
return "fork-only";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function intercomEnabled(configPath: string): boolean {
|
|
34
|
+
if (!fs.existsSync(configPath)) return true;
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8")) as { enabled?: unknown };
|
|
37
|
+
return parsed.enabled !== false;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.warn(`Failed to parse intercom config at '${configPath}'. Assuming enabled.`, error);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function extensionSandboxAllowsIntercom(extensions: string[] | undefined, extensionDir: string): boolean {
|
|
45
|
+
if (extensions === undefined) return true;
|
|
46
|
+
const lowerExtensionDir = extensionDir.toLowerCase();
|
|
47
|
+
return extensions.some((entry) => {
|
|
48
|
+
const normalized = entry.toLowerCase();
|
|
49
|
+
if (normalized.includes(lowerExtensionDir)) return true;
|
|
50
|
+
return /(^|[\\/])pi-intercom([\\/]|$)/i.test(normalized);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildIntercomBridgeInstruction(orchestratorTarget: string): string {
|
|
55
|
+
const escapedTarget = JSON.stringify(orchestratorTarget);
|
|
56
|
+
return `${INTERCOM_BRIDGE_MARKER}
|
|
57
|
+
Use intercom only for coordination with the orchestrator session:
|
|
58
|
+
- Need a decision or blocked: intercom({ action: "ask", to: ${escapedTarget}, message: "<question>" })
|
|
59
|
+
- Completion/update: intercom({ action: "send", to: ${escapedTarget}, message: "DONE: <summary>" })
|
|
60
|
+
If intercom is unavailable in this run, continue the task normally.`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): IntercomBridgeState {
|
|
64
|
+
const mode = resolveIntercomBridgeMode(input.mode);
|
|
65
|
+
const extensionDir = path.resolve(input.extensionDir ?? DEFAULT_INTERCOM_EXTENSION_DIR);
|
|
66
|
+
const orchestratorTarget = input.orchestratorTarget?.trim();
|
|
67
|
+
|
|
68
|
+
if (mode === "off") {
|
|
69
|
+
return { active: false, mode, extensionDir };
|
|
70
|
+
}
|
|
71
|
+
if (mode === "fork-only" && input.context !== "fork") {
|
|
72
|
+
return { active: false, mode, extensionDir };
|
|
73
|
+
}
|
|
74
|
+
if (!orchestratorTarget) {
|
|
75
|
+
return { active: false, mode, extensionDir };
|
|
76
|
+
}
|
|
77
|
+
if (!fs.existsSync(extensionDir)) {
|
|
78
|
+
return { active: false, mode, extensionDir };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const configPath = path.resolve(input.configPath ?? DEFAULT_INTERCOM_CONFIG_PATH);
|
|
82
|
+
if (!intercomEnabled(configPath)) {
|
|
83
|
+
return { active: false, mode, extensionDir };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
active: true,
|
|
88
|
+
mode,
|
|
89
|
+
orchestratorTarget,
|
|
90
|
+
extensionDir,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function applyIntercomBridgeToAgent(agent: AgentConfig, bridge: IntercomBridgeState): AgentConfig {
|
|
95
|
+
if (!bridge.active || !bridge.orchestratorTarget) return agent;
|
|
96
|
+
if (!extensionSandboxAllowsIntercom(agent.extensions, bridge.extensionDir)) return agent;
|
|
97
|
+
|
|
98
|
+
const tools = agent.tools && !agent.tools.includes("intercom")
|
|
99
|
+
? [...agent.tools, "intercom"]
|
|
100
|
+
: agent.tools;
|
|
101
|
+
const instruction = buildIntercomBridgeInstruction(bridge.orchestratorTarget);
|
|
102
|
+
const trimmedPrompt = agent.systemPrompt?.trim() || "";
|
|
103
|
+
const systemPrompt = trimmedPrompt.includes(INTERCOM_BRIDGE_MARKER)
|
|
104
|
+
? trimmedPrompt
|
|
105
|
+
: trimmedPrompt
|
|
106
|
+
? `${trimmedPrompt}\n\n${instruction}`
|
|
107
|
+
: instruction;
|
|
108
|
+
|
|
109
|
+
if (tools === agent.tools && systemPrompt === agent.systemPrompt) return agent;
|
|
110
|
+
return {
|
|
111
|
+
...agent,
|
|
112
|
+
tools,
|
|
113
|
+
systemPrompt,
|
|
114
|
+
};
|
|
115
|
+
}
|
package/package.json
CHANGED
package/subagent-executor.ts
CHANGED
|
@@ -3,27 +3,28 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
5
5
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
6
|
-
import { type AgentConfig, type AgentScope } from "./agents.
|
|
7
|
-
import { getArtifactsDir } from "./artifacts.
|
|
8
|
-
import { ChainClarifyComponent, type ChainClarifyResult, type ModelInfo } from "./chain-clarify.
|
|
9
|
-
import { executeChain } from "./chain-execution.
|
|
10
|
-
import { resolveExecutionAgentScope } from "./agent-scope.
|
|
11
|
-
import { handleManagementAction } from "./agent-management.
|
|
12
|
-
import { runSync } from "./execution.
|
|
13
|
-
import { aggregateParallelOutputs } from "./parallel-utils.
|
|
14
|
-
import { recordRun } from "./run-history.
|
|
6
|
+
import { type AgentConfig, type AgentScope } from "./agents.ts";
|
|
7
|
+
import { getArtifactsDir } from "./artifacts.ts";
|
|
8
|
+
import { ChainClarifyComponent, type ChainClarifyResult, type ModelInfo } from "./chain-clarify.ts";
|
|
9
|
+
import { executeChain } from "./chain-execution.ts";
|
|
10
|
+
import { resolveExecutionAgentScope } from "./agent-scope.ts";
|
|
11
|
+
import { handleManagementAction } from "./agent-management.ts";
|
|
12
|
+
import { runSync } from "./execution.ts";
|
|
13
|
+
import { aggregateParallelOutputs } from "./parallel-utils.ts";
|
|
14
|
+
import { recordRun } from "./run-history.ts";
|
|
15
15
|
import {
|
|
16
16
|
getStepAgents,
|
|
17
17
|
isParallelStep,
|
|
18
18
|
resolveStepBehavior,
|
|
19
19
|
type ChainStep,
|
|
20
20
|
type SequentialStep,
|
|
21
|
-
} from "./settings.
|
|
22
|
-
import { discoverAvailableSkills, normalizeSkillInput } from "./skills.
|
|
23
|
-
import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "./async-execution.
|
|
24
|
-
import { createForkContextResolver } from "./fork-context.
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
21
|
+
} from "./settings.ts";
|
|
22
|
+
import { discoverAvailableSkills, normalizeSkillInput } from "./skills.ts";
|
|
23
|
+
import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "./async-execution.ts";
|
|
24
|
+
import { createForkContextResolver } from "./fork-context.ts";
|
|
25
|
+
import { applyIntercomBridgeToAgent, resolveIntercomBridge } from "./intercom-bridge.ts";
|
|
26
|
+
import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.ts";
|
|
27
|
+
import { getSingleResultOutput, mapConcurrent } from "./utils.ts";
|
|
27
28
|
import {
|
|
28
29
|
cleanupWorktrees,
|
|
29
30
|
createWorktrees,
|
|
@@ -32,7 +33,7 @@ import {
|
|
|
32
33
|
formatWorktreeDiffSummary,
|
|
33
34
|
formatWorktreeTaskCwdConflict,
|
|
34
35
|
type WorktreeSetup,
|
|
35
|
-
} from "./worktree.
|
|
36
|
+
} from "./worktree.ts";
|
|
36
37
|
import {
|
|
37
38
|
type AgentProgress,
|
|
38
39
|
type ArtifactConfig,
|
|
@@ -49,7 +50,7 @@ import {
|
|
|
49
50
|
resolveChildMaxSubagentDepth,
|
|
50
51
|
resolveCurrentMaxSubagentDepth,
|
|
51
52
|
wrapForkTask,
|
|
52
|
-
} from "./types.
|
|
53
|
+
} from "./types.ts";
|
|
53
54
|
|
|
54
55
|
interface TaskParam {
|
|
55
56
|
agent: string;
|
|
@@ -1100,7 +1101,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1100
1101
|
const scope: AgentScope = resolveExecutionAgentScope(normalizedParams.agentScope);
|
|
1101
1102
|
const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
|
|
1102
1103
|
deps.state.currentSessionId = parentSessionFile ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1103
|
-
const
|
|
1104
|
+
const discoveredAgents = deps.discoverAgents(ctx.cwd, scope).agents;
|
|
1105
|
+
const piWithSessionName = deps.pi as ExtensionAPI & { getSessionName?: () => string | undefined };
|
|
1106
|
+
const orchestratorTarget = piWithSessionName.getSessionName?.();
|
|
1107
|
+
const intercomBridge = resolveIntercomBridge({
|
|
1108
|
+
mode: deps.config.intercomBridge,
|
|
1109
|
+
context: normalizedParams.context,
|
|
1110
|
+
orchestratorTarget,
|
|
1111
|
+
});
|
|
1112
|
+
const agents = intercomBridge.active
|
|
1113
|
+
? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge))
|
|
1114
|
+
: discoveredAgents;
|
|
1104
1115
|
const runId = randomUUID().slice(0, 8);
|
|
1105
1116
|
const shareEnabled = normalizedParams.share === true;
|
|
1106
1117
|
const hasChain = (normalizedParams.chain?.length ?? 0) > 0;
|
package/types.ts
CHANGED
|
@@ -269,6 +269,7 @@ export interface ExtensionConfig {
|
|
|
269
269
|
maxSubagentDepth?: number;
|
|
270
270
|
worktreeSetupHook?: string;
|
|
271
271
|
worktreeSetupHookTimeoutMs?: number;
|
|
272
|
+
intercomBridge?: "off" | "fork-only" | "always";
|
|
272
273
|
}
|
|
273
274
|
|
|
274
275
|
// ============================================================================
|