pi-subagents 0.13.1 → 0.13.3
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 +15 -0
- package/README.md +22 -13
- package/execution.ts +76 -15
- package/index.ts +1 -1
- package/intercom-bridge.ts +75 -24
- package/package.json +1 -1
- package/render.ts +5 -3
- package/subagent-executor.ts +22 -4
- package/types.ts +21 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.13.3] - 2026-04-13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added `intercomBridge.instructionFile` so subagent intercom guidance can be overridden from a Markdown template with `{orchestratorTarget}` interpolation.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Intercom-enabled delegated runs now detach only after the child actually starts the `intercom` tool, preserving clean sync behavior until coordination is needed.
|
|
12
|
+
- Graceful intercom coordination no longer leaves detached child runs vulnerable to later parent abort listeners, and reply confirmation follow-ups avoid unnecessary orchestrator aborts.
|
|
13
|
+
- Child process spawn failures now preserve the original error message instead of collapsing to a generic failure.
|
|
14
|
+
|
|
15
|
+
## [0.13.2] - 2026-04-13
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- `intercomBridge` now defaults to `always` so intercom coordination instructions are injected for both `fresh` and `fork` delegated runs when `pi-intercom` is available.
|
|
19
|
+
|
|
5
20
|
## [0.13.1] - 2026-04-13
|
|
6
21
|
|
|
7
22
|
### Added
|
package/README.md
CHANGED
|
@@ -14,12 +14,6 @@ https://github.com/user-attachments/assets/702554ec-faaf-4635-80aa-fb5d6e292fd1
|
|
|
14
14
|
pi install npm:pi-subagents
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
To remove:
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
npx pi-subagents --remove
|
|
21
|
-
```
|
|
22
|
-
|
|
23
17
|
If you use [pi-prompt-template-model](https://github.com/nicobailon/pi-prompt-template-model), you can wrap subagent delegation in a slash command:
|
|
24
18
|
|
|
25
19
|
```markdown
|
|
@@ -331,7 +325,7 @@ Chains can be created from the Agents Manager template picker ("Blank Chain"), o
|
|
|
331
325
|
| Parallel | Yes | `{ tasks: [{agent, task}...] }` - via TUI toggle or converted to chain for async |
|
|
332
326
|
|
|
333
327
|
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: `
|
|
328
|
+
When `intercomBridge` is enabled (default: `always`) and `pi-intercom` is installed/enabled, delegated children get runtime instructions for contacting the orchestrator session via `intercom({ action: "ask"|"send", ... })`.
|
|
335
329
|
|
|
336
330
|
> **Note:** Intercom bridging requires the [pi-intercom](https://github.com/nicobailon/pi-intercom) extension. Install it with `pi install npm:pi-intercom`.
|
|
337
331
|
|
|
@@ -787,19 +781,34 @@ Controls whether subagents receive runtime intercom coordination instructions (a
|
|
|
787
781
|
|
|
788
782
|
```json
|
|
789
783
|
{
|
|
790
|
-
|
|
784
|
+
"intercomBridge": {
|
|
785
|
+
"mode": "always",
|
|
786
|
+
"instructionFile": "./intercom-bridge.md"
|
|
787
|
+
}
|
|
791
788
|
}
|
|
792
789
|
```
|
|
793
790
|
|
|
794
|
-
|
|
795
|
-
- `"
|
|
796
|
-
- `"
|
|
797
|
-
|
|
791
|
+
Fields:
|
|
792
|
+
- `"mode"` (default: `"always"`): inject bridge in both `fresh` and `fork`, only in `fork`, or disable it entirely
|
|
793
|
+
- `"instructionFile"` (optional): path to a Markdown template that replaces the default injected subagent intercom instructions. Supports `{orchestratorTarget}` placeholder interpolation. Relative paths resolve from `~/.pi/agent/extensions/subagent/`.
|
|
794
|
+
|
|
795
|
+
Example `instructionFile`:
|
|
796
|
+
|
|
797
|
+
```md
|
|
798
|
+
Intercom orchestration channel:
|
|
799
|
+
|
|
800
|
+
Use `intercom` only to coordinate with the orchestrator session `{orchestratorTarget}`.
|
|
801
|
+
|
|
802
|
+
- Need a decision or you're blocked: `intercom({ action: "ask", to: "{orchestratorTarget}", message: "<question>" })`
|
|
803
|
+
- Need to report progress or completion: `intercom({ action: "send", to: "{orchestratorTarget}", message: "DONE: <summary>" })`
|
|
804
|
+
|
|
805
|
+
If intercom is unavailable in this run, continue the task normally.
|
|
806
|
+
```
|
|
798
807
|
|
|
799
808
|
Bridge activation also requires all of the following:
|
|
800
809
|
- [pi-intercom](https://github.com/nicobailon/pi-intercom) is installed (`pi install npm:pi-intercom`)
|
|
801
810
|
- `~/.pi/agent/intercom/config.json` is not set to `"enabled": false`
|
|
802
|
-
- the current session has a name
|
|
811
|
+
- the current session has a target name (existing `/name`, or auto-assigned `session-<id>` when unnamed)
|
|
803
812
|
- if agent `extensions` is an explicit allowlist, it must include `pi-intercom`
|
|
804
813
|
|
|
805
814
|
### `worktreeSetupHook`
|
package/execution.ts
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
type SingleResult,
|
|
20
20
|
type Usage,
|
|
21
21
|
DEFAULT_MAX_OUTPUT,
|
|
22
|
+
INTERCOM_DETACH_REQUEST_EVENT,
|
|
23
|
+
INTERCOM_DETACH_RESPONSE_EVENT,
|
|
22
24
|
truncateOutput,
|
|
23
25
|
getSubagentDepthEnv,
|
|
24
26
|
} from "./types.ts";
|
|
@@ -123,7 +125,6 @@ async function runSingleAttempt(
|
|
|
123
125
|
const startTime = Date.now();
|
|
124
126
|
const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
|
|
125
127
|
|
|
126
|
-
let closeJsonlWriter: (() => Promise<void>) | undefined;
|
|
127
128
|
const exitCode = await new Promise<number>((resolve) => {
|
|
128
129
|
const spawnSpec = getPiSpawnCommand(args);
|
|
129
130
|
const proc = spawn(spawnSpec.command, spawnSpec.args, {
|
|
@@ -132,9 +133,46 @@ async function runSingleAttempt(
|
|
|
132
133
|
stdio: ["ignore", "pipe", "pipe"],
|
|
133
134
|
});
|
|
134
135
|
const jsonlWriter = createJsonlWriter(shared.jsonlPath, proc.stdout);
|
|
135
|
-
closeJsonlWriter = () => jsonlWriter.close();
|
|
136
136
|
let buf = "";
|
|
137
137
|
let processClosed = false;
|
|
138
|
+
let settled = false;
|
|
139
|
+
let detached = false;
|
|
140
|
+
let intercomStarted = false;
|
|
141
|
+
let removeAbortListener: (() => void) | undefined;
|
|
142
|
+
|
|
143
|
+
const detachForIntercom = () => {
|
|
144
|
+
detached = true;
|
|
145
|
+
processClosed = true;
|
|
146
|
+
result.detached = true;
|
|
147
|
+
result.detachedReason = "intercom coordination";
|
|
148
|
+
progress.status = "detached";
|
|
149
|
+
progress.durationMs = Date.now() - startTime;
|
|
150
|
+
result.progressSummary = {
|
|
151
|
+
toolCount: progress.toolCount,
|
|
152
|
+
tokens: progress.tokens,
|
|
153
|
+
durationMs: progress.durationMs,
|
|
154
|
+
};
|
|
155
|
+
finish(-2);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const unsubscribeIntercomDetach = options.intercomEvents?.on(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
|
|
159
|
+
if (!options.allowIntercomDetach || detached || processClosed) return;
|
|
160
|
+
if (!payload || typeof payload !== "object") return;
|
|
161
|
+
const requestId = (payload as { requestId?: unknown }).requestId;
|
|
162
|
+
if (typeof requestId !== "string" || requestId.length === 0) return;
|
|
163
|
+
const accepted = intercomStarted;
|
|
164
|
+
options.intercomEvents?.emit(INTERCOM_DETACH_RESPONSE_EVENT, { requestId, accepted });
|
|
165
|
+
if (!accepted) return;
|
|
166
|
+
detachForIntercom();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const finish = (code: number) => {
|
|
170
|
+
if (settled) return;
|
|
171
|
+
settled = true;
|
|
172
|
+
unsubscribeIntercomDetach?.();
|
|
173
|
+
removeAbortListener?.();
|
|
174
|
+
resolve(code);
|
|
175
|
+
};
|
|
138
176
|
|
|
139
177
|
const fireUpdate = () => {
|
|
140
178
|
if (!options.onUpdate || processClosed) return;
|
|
@@ -154,6 +192,9 @@ async function runSingleAttempt(
|
|
|
154
192
|
progress.durationMs = now - startTime;
|
|
155
193
|
|
|
156
194
|
if (evt.type === "tool_execution_start") {
|
|
195
|
+
if (options.allowIntercomDetach && evt.toolName === "intercom") {
|
|
196
|
+
intercomStarted = true;
|
|
197
|
+
}
|
|
157
198
|
progress.toolCount++;
|
|
158
199
|
progress.currentTool = evt.toolName;
|
|
159
200
|
progress.currentToolArgs = extractToolArgsPreview((evt.args || {}) as Record<string, unknown>);
|
|
@@ -215,35 +256,55 @@ async function runSingleAttempt(
|
|
|
215
256
|
stderrBuf += d.toString();
|
|
216
257
|
});
|
|
217
258
|
proc.on("close", (code) => {
|
|
259
|
+
void jsonlWriter.close().catch(() => {
|
|
260
|
+
// JSONL artifact flush is best effort.
|
|
261
|
+
});
|
|
262
|
+
cleanupTempDir(tempDir);
|
|
263
|
+
if (detached) {
|
|
264
|
+
finish(-2);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
218
267
|
processClosed = true;
|
|
219
268
|
if (buf.trim()) processLine(buf);
|
|
220
269
|
if (code !== 0 && stderrBuf.trim() && !result.error) {
|
|
221
270
|
result.error = stderrBuf.trim();
|
|
222
271
|
}
|
|
223
|
-
|
|
272
|
+
finish(code ?? 0);
|
|
273
|
+
});
|
|
274
|
+
proc.on("error", (error) => {
|
|
275
|
+
void jsonlWriter.close().catch(() => {
|
|
276
|
+
// JSONL artifact flush is best effort.
|
|
277
|
+
});
|
|
278
|
+
cleanupTempDir(tempDir);
|
|
279
|
+
if (!result.error) {
|
|
280
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
281
|
+
}
|
|
282
|
+
finish(1);
|
|
224
283
|
});
|
|
225
|
-
proc.on("error", () => resolve(1));
|
|
226
284
|
|
|
227
285
|
if (options.signal) {
|
|
228
286
|
const kill = () => {
|
|
287
|
+
if (processClosed || detached) return;
|
|
288
|
+
if (options.allowIntercomDetach && intercomStarted && !detached) {
|
|
289
|
+
detachForIntercom();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
229
292
|
proc.kill("SIGTERM");
|
|
230
293
|
setTimeout(() => !proc.killed && proc.kill("SIGKILL"), 3000);
|
|
231
294
|
};
|
|
232
295
|
if (options.signal.aborted) kill();
|
|
233
|
-
else
|
|
296
|
+
else {
|
|
297
|
+
options.signal.addEventListener("abort", kill, { once: true });
|
|
298
|
+
removeAbortListener = () => options.signal?.removeEventListener("abort", kill);
|
|
299
|
+
}
|
|
234
300
|
}
|
|
235
301
|
});
|
|
236
|
-
|
|
237
|
-
if (closeJsonlWriter) {
|
|
238
|
-
try {
|
|
239
|
-
await closeJsonlWriter();
|
|
240
|
-
} catch {
|
|
241
|
-
// JSONL artifact flush is best effort.
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
cleanupTempDir(tempDir);
|
|
246
302
|
result.exitCode = exitCode;
|
|
303
|
+
if (result.detached) {
|
|
304
|
+
result.exitCode = 0;
|
|
305
|
+
result.finalOutput = "Detached for intercom coordination.";
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
247
308
|
|
|
248
309
|
if (exitCode === 0 && !result.error) {
|
|
249
310
|
const errInfo = detectSubagentError(result.messages);
|
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, "intercomBridge": "
|
|
12
|
+
* { "asyncByDefault": true, "maxSubagentDepth": 1, "intercomBridge": { "mode": "always", "instructionFile": "./intercom-bridge.md" }, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import * as fs from "node:fs";
|
package/intercom-bridge.ts
CHANGED
|
@@ -2,32 +2,50 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AgentConfig } from "./agents.ts";
|
|
5
|
-
import type { ExtensionConfig } from "./types.ts";
|
|
5
|
+
import type { ExtensionConfig, IntercomBridgeConfig, IntercomBridgeMode } from "./types.ts";
|
|
6
6
|
|
|
7
7
|
const DEFAULT_INTERCOM_EXTENSION_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "pi-intercom");
|
|
8
8
|
const DEFAULT_INTERCOM_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "intercom", "config.json");
|
|
9
|
+
const DEFAULT_SUBAGENT_CONFIG_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent");
|
|
9
10
|
const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `Use intercom only for coordination with the orchestrator session:
|
|
12
|
+
- Need a decision or blocked: intercom({ action: "ask", to: "{orchestratorTarget}", message: "<question>" })
|
|
13
|
+
- Completion/update: intercom({ action: "send", to: "{orchestratorTarget}", message: "DONE: <summary>" })
|
|
14
|
+
If intercom is unavailable in this run, continue the task normally.`;
|
|
12
15
|
|
|
13
16
|
export interface IntercomBridgeState {
|
|
14
17
|
active: boolean;
|
|
15
18
|
mode: IntercomBridgeMode;
|
|
16
19
|
orchestratorTarget?: string;
|
|
17
20
|
extensionDir: string;
|
|
21
|
+
instruction: string;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
interface ResolveIntercomBridgeInput {
|
|
21
|
-
|
|
25
|
+
config: ExtensionConfig["intercomBridge"];
|
|
22
26
|
context: "fresh" | "fork" | undefined;
|
|
23
27
|
orchestratorTarget?: string;
|
|
24
28
|
extensionDir?: string;
|
|
25
29
|
configPath?: string;
|
|
30
|
+
settingsDir?: string;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
export function resolveIntercomBridgeMode(value: unknown): IntercomBridgeMode {
|
|
29
34
|
if (value === "off" || value === "always" || value === "fork-only") return value;
|
|
30
|
-
return "
|
|
35
|
+
return "always";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveIntercomBridgeConfig(value: ExtensionConfig["intercomBridge"]): Required<IntercomBridgeConfig> {
|
|
39
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
40
|
+
return {
|
|
41
|
+
mode: "always",
|
|
42
|
+
instructionFile: "",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
mode: resolveIntercomBridgeMode(value.mode),
|
|
47
|
+
instructionFile: typeof value.instructionFile === "string" ? value.instructionFile : "",
|
|
48
|
+
};
|
|
31
49
|
}
|
|
32
50
|
|
|
33
51
|
function intercomEnabled(configPath: string): boolean {
|
|
@@ -43,51 +61,84 @@ function intercomEnabled(configPath: string): boolean {
|
|
|
43
61
|
|
|
44
62
|
function extensionSandboxAllowsIntercom(extensions: string[] | undefined, extensionDir: string): boolean {
|
|
45
63
|
if (extensions === undefined) return true;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
|
|
65
|
+
const intercomDir = path.resolve(extensionDir).replaceAll("\\", "/").toLowerCase();
|
|
66
|
+
for (const entry of extensions) {
|
|
67
|
+
const normalized = entry.trim().replaceAll("\\", "/").toLowerCase();
|
|
68
|
+
if (normalized === "pi-intercom") return true;
|
|
69
|
+
if (normalized === intercomDir) return true;
|
|
70
|
+
if (normalized.startsWith(`${intercomDir}/`)) return true;
|
|
71
|
+
if (normalized.endsWith("/pi-intercom")) return true;
|
|
72
|
+
if (normalized.includes("/pi-intercom/")) return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
52
75
|
}
|
|
53
76
|
|
|
54
|
-
function
|
|
55
|
-
|
|
77
|
+
function expandTilde(filePath: string): string {
|
|
78
|
+
return filePath.startsWith("~/") ? path.join(os.homedir(), filePath.slice(2)) : filePath;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveInstructionTemplate(instructionFile: string, settingsDir: string): string {
|
|
82
|
+
if (!instructionFile) return DEFAULT_INTERCOM_BRIDGE_TEMPLATE;
|
|
83
|
+
const expandedPath = expandTilde(instructionFile);
|
|
84
|
+
const resolvedPath = path.isAbsolute(expandedPath)
|
|
85
|
+
? expandedPath
|
|
86
|
+
: path.resolve(settingsDir, expandedPath);
|
|
87
|
+
try {
|
|
88
|
+
return fs.readFileSync(resolvedPath, "utf-8");
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.warn(`Failed to read intercom bridge instructionFile at '${resolvedPath}'. Using default instructions.`, error);
|
|
91
|
+
return DEFAULT_INTERCOM_BRIDGE_TEMPLATE;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildIntercomBridgeInstruction(orchestratorTarget: string, template: string): string {
|
|
96
|
+
const instruction = template.replaceAll("{orchestratorTarget}", orchestratorTarget).trim();
|
|
97
|
+
if (instruction.startsWith(INTERCOM_BRIDGE_MARKER)) return instruction;
|
|
56
98
|
return `${INTERCOM_BRIDGE_MARKER}
|
|
57
|
-
|
|
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.`;
|
|
99
|
+
${instruction}`;
|
|
61
100
|
}
|
|
62
101
|
|
|
63
102
|
export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): IntercomBridgeState {
|
|
64
|
-
const
|
|
103
|
+
const config = resolveIntercomBridgeConfig(input.config);
|
|
104
|
+
const mode = config.mode;
|
|
65
105
|
const extensionDir = path.resolve(input.extensionDir ?? DEFAULT_INTERCOM_EXTENSION_DIR);
|
|
66
106
|
const orchestratorTarget = input.orchestratorTarget?.trim();
|
|
107
|
+
const settingsDir = path.resolve(input.settingsDir ?? DEFAULT_SUBAGENT_CONFIG_DIR);
|
|
108
|
+
const defaultInstruction = buildIntercomBridgeInstruction(
|
|
109
|
+
orchestratorTarget || "{orchestratorTarget}",
|
|
110
|
+
DEFAULT_INTERCOM_BRIDGE_TEMPLATE,
|
|
111
|
+
);
|
|
67
112
|
|
|
68
113
|
if (mode === "off") {
|
|
69
|
-
return { active: false, mode, extensionDir };
|
|
114
|
+
return { active: false, mode, extensionDir, instruction: defaultInstruction };
|
|
70
115
|
}
|
|
71
116
|
if (mode === "fork-only" && input.context !== "fork") {
|
|
72
|
-
return { active: false, mode, extensionDir };
|
|
117
|
+
return { active: false, mode, extensionDir, instruction: defaultInstruction };
|
|
73
118
|
}
|
|
74
119
|
if (!orchestratorTarget) {
|
|
75
|
-
return { active: false, mode, extensionDir };
|
|
120
|
+
return { active: false, mode, extensionDir, instruction: defaultInstruction };
|
|
76
121
|
}
|
|
77
122
|
if (!fs.existsSync(extensionDir)) {
|
|
78
|
-
return { active: false, mode, extensionDir };
|
|
123
|
+
return { active: false, mode, extensionDir, instruction: defaultInstruction };
|
|
79
124
|
}
|
|
80
125
|
|
|
81
126
|
const configPath = path.resolve(input.configPath ?? DEFAULT_INTERCOM_CONFIG_PATH);
|
|
82
127
|
if (!intercomEnabled(configPath)) {
|
|
83
|
-
return { active: false, mode, extensionDir };
|
|
128
|
+
return { active: false, mode, extensionDir, instruction: defaultInstruction };
|
|
84
129
|
}
|
|
85
130
|
|
|
131
|
+
const instruction = buildIntercomBridgeInstruction(
|
|
132
|
+
orchestratorTarget,
|
|
133
|
+
resolveInstructionTemplate(config.instructionFile, settingsDir),
|
|
134
|
+
);
|
|
135
|
+
|
|
86
136
|
return {
|
|
87
137
|
active: true,
|
|
88
138
|
mode,
|
|
89
139
|
orchestratorTarget,
|
|
90
140
|
extensionDir,
|
|
141
|
+
instruction,
|
|
91
142
|
};
|
|
92
143
|
}
|
|
93
144
|
|
|
@@ -98,7 +149,7 @@ export function applyIntercomBridgeToAgent(agent: AgentConfig, bridge: IntercomB
|
|
|
98
149
|
const tools = agent.tools && !agent.tools.includes("intercom")
|
|
99
150
|
? [...agent.tools, "intercom"]
|
|
100
151
|
: agent.tools;
|
|
101
|
-
const instruction =
|
|
152
|
+
const instruction = bridge.instruction;
|
|
102
153
|
const trimmedPrompt = agent.systemPrompt?.trim() || "";
|
|
103
154
|
const systemPrompt = trimmedPrompt.includes(INTERCOM_BRIDGE_MARKER)
|
|
104
155
|
? trimmedPrompt
|
package/package.json
CHANGED
package/render.ts
CHANGED
|
@@ -195,9 +195,11 @@ export function renderSubagentResult(
|
|
|
195
195
|
const isRunning = r.progress?.status === "running";
|
|
196
196
|
const icon = isRunning
|
|
197
197
|
? theme.fg("warning", "...")
|
|
198
|
-
: r.
|
|
199
|
-
? theme.fg("
|
|
200
|
-
:
|
|
198
|
+
: r.detached
|
|
199
|
+
? theme.fg("warning", "↗")
|
|
200
|
+
: r.exitCode === 0
|
|
201
|
+
? theme.fg("success", "ok")
|
|
202
|
+
: theme.fg("error", "X");
|
|
201
203
|
const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
|
|
202
204
|
const output = r.truncation?.text || getSingleResultOutput(r);
|
|
203
205
|
|
package/subagent-executor.ts
CHANGED
|
@@ -995,6 +995,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
995
995
|
const r = await runSync(ctx.cwd, agents, params.agent!, task, {
|
|
996
996
|
cwd: params.cwd,
|
|
997
997
|
signal,
|
|
998
|
+
allowIntercomDetach: agentConfig.systemPrompt?.includes("Intercom orchestration channel:") === true,
|
|
999
|
+
intercomEvents: deps.pi.events,
|
|
998
1000
|
runId,
|
|
999
1001
|
sessionDir: sessionDirForIndex(0),
|
|
1000
1002
|
sessionFile: sessionFileForIndex(0),
|
|
@@ -1024,6 +1026,19 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1024
1026
|
saveError: r.outputSaveError,
|
|
1025
1027
|
});
|
|
1026
1028
|
|
|
1029
|
+
if (r.detached) {
|
|
1030
|
+
return {
|
|
1031
|
+
content: [{ type: "text", text: `Detached for intercom coordination: ${params.agent}` }],
|
|
1032
|
+
details: {
|
|
1033
|
+
mode: "single",
|
|
1034
|
+
results: [r],
|
|
1035
|
+
progress: params.includeProgress ? allProgress : undefined,
|
|
1036
|
+
artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
|
|
1037
|
+
truncation: r.truncation,
|
|
1038
|
+
},
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1027
1042
|
if (r.exitCode !== 0)
|
|
1028
1043
|
return {
|
|
1029
1044
|
content: [{ type: "text", text: r.error || "Failed" }],
|
|
@@ -1102,12 +1117,15 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1102
1117
|
const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
|
|
1103
1118
|
deps.state.currentSessionId = parentSessionFile ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1104
1119
|
const discoveredAgents = deps.discoverAgents(ctx.cwd, scope).agents;
|
|
1105
|
-
|
|
1106
|
-
|
|
1120
|
+
let sessionName = deps.pi.getSessionName()?.trim();
|
|
1121
|
+
if (!sessionName) {
|
|
1122
|
+
sessionName = `session-${ctx.sessionManager.getSessionId().slice(0, 8)}`;
|
|
1123
|
+
deps.pi.setSessionName(sessionName);
|
|
1124
|
+
}
|
|
1107
1125
|
const intercomBridge = resolveIntercomBridge({
|
|
1108
|
-
|
|
1126
|
+
config: deps.config.intercomBridge,
|
|
1109
1127
|
context: normalizedParams.context,
|
|
1110
|
-
orchestratorTarget,
|
|
1128
|
+
orchestratorTarget: sessionName,
|
|
1111
1129
|
});
|
|
1112
1130
|
const agents = intercomBridge.active
|
|
1113
1131
|
? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge))
|
package/types.ts
CHANGED
|
@@ -58,7 +58,7 @@ export interface ResolvedSkill {
|
|
|
58
58
|
export interface AgentProgress {
|
|
59
59
|
index: number;
|
|
60
60
|
agent: string;
|
|
61
|
-
status: "pending" | "running" | "completed" | "failed";
|
|
61
|
+
status: "pending" | "running" | "completed" | "failed" | "detached";
|
|
62
62
|
task: string;
|
|
63
63
|
skills?: string[];
|
|
64
64
|
currentTool?: string;
|
|
@@ -94,6 +94,8 @@ export interface SingleResult {
|
|
|
94
94
|
agent: string;
|
|
95
95
|
task: string;
|
|
96
96
|
exitCode: number;
|
|
97
|
+
detached?: boolean;
|
|
98
|
+
detachedReason?: string;
|
|
97
99
|
messages: Message[];
|
|
98
100
|
usage: Usage;
|
|
99
101
|
model?: string;
|
|
@@ -237,6 +239,14 @@ export interface ErrorInfo {
|
|
|
237
239
|
details?: string;
|
|
238
240
|
}
|
|
239
241
|
|
|
242
|
+
export interface IntercomEventBus {
|
|
243
|
+
on(channel: string, handler: (data: unknown) => void): () => void;
|
|
244
|
+
emit(channel: string, data: unknown): void;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export const INTERCOM_DETACH_REQUEST_EVENT = "pi-intercom:detach-request";
|
|
248
|
+
export const INTERCOM_DETACH_RESPONSE_EVENT = "pi-intercom:detach-response";
|
|
249
|
+
|
|
240
250
|
// ============================================================================
|
|
241
251
|
// Execution Options
|
|
242
252
|
// ============================================================================
|
|
@@ -244,6 +254,8 @@ export interface ErrorInfo {
|
|
|
244
254
|
export interface RunSyncOptions {
|
|
245
255
|
cwd?: string;
|
|
246
256
|
signal?: AbortSignal;
|
|
257
|
+
allowIntercomDetach?: boolean;
|
|
258
|
+
intercomEvents?: IntercomEventBus;
|
|
247
259
|
onUpdate?: (r: import("@mariozechner/pi-agent-core").AgentToolResult<Details>) => void;
|
|
248
260
|
maxOutput?: MaxOutputConfig;
|
|
249
261
|
artifactsDir?: string;
|
|
@@ -263,13 +275,20 @@ export interface RunSyncOptions {
|
|
|
263
275
|
skills?: string[];
|
|
264
276
|
}
|
|
265
277
|
|
|
278
|
+
export type IntercomBridgeMode = "off" | "fork-only" | "always";
|
|
279
|
+
|
|
280
|
+
export interface IntercomBridgeConfig {
|
|
281
|
+
mode?: IntercomBridgeMode;
|
|
282
|
+
instructionFile?: string;
|
|
283
|
+
}
|
|
284
|
+
|
|
266
285
|
export interface ExtensionConfig {
|
|
267
286
|
asyncByDefault?: boolean;
|
|
268
287
|
defaultSessionDir?: string;
|
|
269
288
|
maxSubagentDepth?: number;
|
|
270
289
|
worktreeSetupHook?: string;
|
|
271
290
|
worktreeSetupHookTimeoutMs?: number;
|
|
272
|
-
intercomBridge?:
|
|
291
|
+
intercomBridge?: IntercomBridgeConfig;
|
|
273
292
|
}
|
|
274
293
|
|
|
275
294
|
// ============================================================================
|