@ctliz/agent-intercom-pi 0.12.0-connect.9 → 0.13.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.
- package/README.md +31 -2
- package/bin/intercom-send.mjs +4 -0
- package/broker/client.ts +3 -1
- package/broker/framing.ts +4 -1
- package/cli-send.ts +39 -0
- package/index.ts +57 -22
- package/package.json +8 -4
- package/skills/pi-intercom/SKILL.md +14 -1
- package/tool-result.ts +26 -0
package/README.md
CHANGED
|
@@ -17,6 +17,35 @@
|
|
|
17
17
|
| AGY | [`agent-intercom-agy`](https://github.com/ctliz/agent-intercom-agy) |
|
|
18
18
|
| Fleet lifecycle | [`agent-intercom-orchestrator`](https://github.com/ctliz/agent-intercom-orchestrator) |
|
|
19
19
|
|
|
20
|
+
## Pi 0.99 and codemode
|
|
21
|
+
|
|
22
|
+
Version 0.13.0 requires Pi 0.99.1 or newer in the 0.99 release line. It keeps the existing protocol v4 broker, durable queues, acknowledgements, and cross-harness routing unchanged.
|
|
23
|
+
|
|
24
|
+
All Intercom tools remain directly callable and are also available through Pi's built-in codemode when active. Scripts receive `{ ok, text, data }` rather than a display string. `data` contains the tool's structured details: delivery flags and message ID, session lists, team roster, pending asks, or connection status. Returned failures set `isError: true` and retain structured data; a deferred ask is successful with `data.pending === true`. Always check `ok` before continuing with dependent actions. Blocked calls, invalid arguments, and thrown exceptions can still reject, so use `try/catch` or `Promise.allSettled()` when appropriate.
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
const sessions = await tools.intercom_list({});
|
|
28
|
+
if (!sessions.ok) throw new Error(sessions.text);
|
|
29
|
+
const target = sessions.data.sessions.find(s => s.name === "worker");
|
|
30
|
+
if (!target) throw new Error("Worker is offline");
|
|
31
|
+
const result = await tools.intercom_send({ to: target.id, message: "Tests passed." });
|
|
32
|
+
return { ok: result.ok, accepted: result.data.accepted, delivered: result.data.delivered };
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Concurrent sends and independent asks are supported; do not create two unresolved asks to the same recipient. Team joins execute sequentially because they change the caller's routing scope. Receiving a delivery acknowledgement means the message is durably queued, not that the recipient finished its task. Busy sessions wait until `ctx.isIdle()`; `agent_settled` updates final idle status after automatic retries and compaction.
|
|
36
|
+
|
|
37
|
+
### Send from a shell or release script
|
|
38
|
+
|
|
39
|
+
The package includes an `intercom-send` executable. Install the alias globally for a shell command, or run it without relying on Pi's private installation path:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm exec --yes --package=@ctliz/pi-intercom@0.13.0 -- intercom-send worker 'Tests passed.'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
It prints one JSON result with `accepted`, `delivered`, `messageId`, and optional failure `code`/`reason`. Exit status is zero only for acknowledged delivery. It inherits the routing scope, but never inherits `PI_INTERCOM_SESSION_ID` or `AGENT_INTERCOM_SESSION_ID`: every invocation registers an independent sender, leaves running Pi sessions intact, and disconnects after sending. It is send-only; use the session tools for reply-tracked asks.
|
|
46
|
+
|
|
47
|
+
When a second runtime claims the same stable session ID, Intercom reports `SESSION_ID_IN_USE`, pauses automatic reconnect, and preserves the original owner. Switch to a different session, or release the duplicate owner and `/reload`. `intercom_status` exposes the conflict as structured data rather than silently treating it as a temporary outage. Updating this adapter does not require restarting a compatible v4 broker.
|
|
48
|
+
|
|
20
49
|
## Grok Build and AGY support
|
|
21
50
|
|
|
22
51
|
Grok Build and AGY are supported as first-class protocol peers through two dedicated npm packages:
|
|
@@ -93,7 +122,7 @@ Each pi session that has `pi-intercom` loaded and enabled connects to a tiny loc
|
|
|
93
122
|
## Install
|
|
94
123
|
|
|
95
124
|
```bash
|
|
96
|
-
pi install git:github.com/ctliz/agent-intercom-pi@v0.12.
|
|
125
|
+
pi install git:github.com/ctliz/agent-intercom-pi@v0.12.2
|
|
97
126
|
```
|
|
98
127
|
|
|
99
128
|
If you are coming from `connect.1`, read [Upgrading from `connect.1`](#upgrading-from-connect1-to-connect2) first — the package namespace changed and the two versions must not be installed side by side.
|
|
@@ -141,7 +170,7 @@ A session becomes intercom-connected when all of these are true:
|
|
|
141
170
|
|
|
142
171
|
The session list only shows intercom-connected sessions, not every open Pi process on the machine.
|
|
143
172
|
|
|
144
|
-
If you upgrade pi-intercom or the orchestrator while sessions are already open, run `/reload` in each open Pi session (and restart any companion `coi`, `cci`, or OpenCode adapter). Update the packages by reinstalling the exact release tags with `pi install git:github.com/ctliz/agent-intercom-pi@v0.12.
|
|
173
|
+
If you upgrade pi-intercom or the orchestrator while sessions are already open, run `/reload` in each open Pi session (and restart any companion `coi`, `cci`, or OpenCode adapter). Update the packages by reinstalling the exact release tags with `pi install git:github.com/ctliz/agent-intercom-pi@v0.12.2` and, only where Orchestrator is actually installed, `pi install git:github.com/ctliz/agent-intercom-orchestrator@v0.12.0-connect.5`. Extensions are loaded into the running host process, so an existing session cannot adopt new broker/discovery code until it reloads. This is especially important when upgrading from a release that allowed multiple broker processes to form separate session-list "islands": the broker ownership fix prevents new splits, but it cannot move clients that are still running the old code. After every host has reloaded once, they converge on the same broker automatically.
|
|
145
174
|
|
|
146
175
|
If `/intercom` still reports no peers, first confirm the other Pi windows have pi-intercom loaded and have also been reloaded. Open Pi processes without the extension, disabled sessions, and sessions using a different `PI_CODING_AGENT_DIR` intentionally do not appear in the same list.
|
|
147
176
|
|
package/broker/client.ts
CHANGED
|
@@ -376,7 +376,9 @@ export class IntercomClient extends EventEmitter {
|
|
|
376
376
|
};
|
|
377
377
|
|
|
378
378
|
const onReaderError = (error: Error) => {
|
|
379
|
-
const protocolError = new Error(`Intercom protocol error: ${error.message}`, { cause: error });
|
|
379
|
+
const protocolError = new Error(`Intercom protocol error: ${error.message}`, { cause: error }) as Error & { code?: string };
|
|
380
|
+
const code = (error as Error & { code?: string }).code;
|
|
381
|
+
if (code) protocolError.code = code;
|
|
380
382
|
if (!connectionEstablished) {
|
|
381
383
|
onError(protocolError);
|
|
382
384
|
return;
|
package/broker/framing.ts
CHANGED
|
@@ -41,7 +41,10 @@ export function createMessageReader(
|
|
|
41
41
|
return true;
|
|
42
42
|
} catch (error) {
|
|
43
43
|
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
-
|
|
44
|
+
const handlerError = new Error(`Failed to handle intercom message: ${message}`, { cause: error }) as Error & { code?: string };
|
|
45
|
+
const code = (error as { code?: unknown } | null)?.code;
|
|
46
|
+
if (typeof code === "string") handlerError.code = code;
|
|
47
|
+
onError(handlerError);
|
|
45
48
|
return false;
|
|
46
49
|
}
|
|
47
50
|
}
|
package/cli-send.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { IntercomClient } from "./broker/client.ts";
|
|
3
|
+
import { spawnBrokerIfNeeded } from "./broker/spawn.ts";
|
|
4
|
+
import { loadConfig } from "./config.ts";
|
|
5
|
+
|
|
6
|
+
async function main(): Promise<void> {
|
|
7
|
+
const [to, ...parts] = process.argv.slice(2);
|
|
8
|
+
const message = parts.join(" ");
|
|
9
|
+
if (!to?.trim() || !message.trim()) {
|
|
10
|
+
throw new Error("Usage: intercom-send <session-name-or-id> <message>");
|
|
11
|
+
}
|
|
12
|
+
const config = loadConfig();
|
|
13
|
+
if (!config.enabled) throw new Error("Intercom disabled");
|
|
14
|
+
await spawnBrokerIfNeeded(config.brokerCommand, config.brokerArgs);
|
|
15
|
+
const client = new IntercomClient();
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
try {
|
|
18
|
+
// Never inherit the calling Pi's session ID or take over its mailbox.
|
|
19
|
+
await client.connect({
|
|
20
|
+
name: `intercom-cli-${randomUUID()}`,
|
|
21
|
+
cwd: process.cwd(),
|
|
22
|
+
model: "cli-sender",
|
|
23
|
+
pid: process.pid,
|
|
24
|
+
startedAt: now,
|
|
25
|
+
lastActivity: now,
|
|
26
|
+
runtimeInstanceId: randomUUID(),
|
|
27
|
+
});
|
|
28
|
+
const result = await client.send(to, { text: message });
|
|
29
|
+
console.log(JSON.stringify({ messageId: result.id, ...result }));
|
|
30
|
+
if (!result.delivered) process.exitCode = 1;
|
|
31
|
+
} finally {
|
|
32
|
+
await client.disconnect();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
main().catch((error: Error & { code?: string }) => {
|
|
37
|
+
console.log(JSON.stringify({ accepted: false, delivered: false, reason: error.message, ...(error.code ? { code: error.code } : {}) }));
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
});
|
package/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { intercomOutputSchema, structuredIntercomResult } from "./tool-result.ts";
|
|
2
3
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
3
4
|
import { randomUUID } from "crypto";
|
|
4
5
|
import { spawn, spawnSync } from "child_process";
|
|
@@ -147,9 +148,8 @@ function toError(error: unknown): Error {
|
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
function toolErrorDetails(error: unknown): { error: true; code?: string } {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
: { error: true };
|
|
151
|
+
const code = (error as { code?: unknown } | null)?.code;
|
|
152
|
+
return { error: true, ...(typeof code === "string" ? { code } : {}) };
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
class AskWaitElapsedError extends Error {
|
|
@@ -680,6 +680,15 @@ function getNamePollMs(): number {
|
|
|
680
680
|
return 1000;
|
|
681
681
|
}
|
|
682
682
|
export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
683
|
+
function registerIntercomTool(tool: ToolDefinition<any, any>): void {
|
|
684
|
+
pi.registerTool({
|
|
685
|
+
...tool,
|
|
686
|
+
outputSchema: intercomOutputSchema,
|
|
687
|
+
async execute(...args) {
|
|
688
|
+
return structuredIntercomResult(await tool.execute(...args));
|
|
689
|
+
},
|
|
690
|
+
});
|
|
691
|
+
}
|
|
683
692
|
let runtimeScopeId = intercomScopeIdFromEnvForRegistration();
|
|
684
693
|
const initialHarnessSessionId = process.env[INTERCOM_SESSION_ID_ENV]?.trim();
|
|
685
694
|
const initialGenericSessionId = process.env.AGENT_INTERCOM_SESSION_ID?.trim();
|
|
@@ -706,6 +715,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
706
715
|
let reconnectPromiseGeneration: number | null = null;
|
|
707
716
|
let startupConnectTimer: NodeJS.Timeout | null = null;
|
|
708
717
|
let reconnectAttempt = 0;
|
|
718
|
+
let registrationConflict: Error | null = null;
|
|
709
719
|
let shuttingDown = false;
|
|
710
720
|
let disposed = true;
|
|
711
721
|
let runtimeStarted = false;
|
|
@@ -835,7 +845,8 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
835
845
|
}
|
|
836
846
|
function currentStatus(): string {
|
|
837
847
|
const activeToolName = activeTools.values().next().value;
|
|
838
|
-
const
|
|
848
|
+
const busy = agentRunning || getLiveContext()?.isIdle() === false;
|
|
849
|
+
const lifecycleStatus = activeToolName ? `tool:${activeToolName}` : busy ? "thinking" : "idle";
|
|
839
850
|
const queueStatus = inboundInbox?.size ? ` · inbox:${inboundInbox.size}` : "";
|
|
840
851
|
const outboxStatus = client?.outboxSize ? ` · outbox:${client.outboxSize}` : "";
|
|
841
852
|
return config.status ? `${lifecycleStatus}${queueStatus}${outboxStatus} · ${config.status}` : `${lifecycleStatus}${queueStatus}${outboxStatus}`;
|
|
@@ -1079,6 +1090,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1079
1090
|
inboundFlushTimer.unref?.();
|
|
1080
1091
|
}
|
|
1081
1092
|
function flushIdleMessages(generation = runtimeGeneration): void {
|
|
1093
|
+
if (registrationConflict) return;
|
|
1082
1094
|
if (!inboundInbox || inboundInbox.size === 0) {
|
|
1083
1095
|
return;
|
|
1084
1096
|
}
|
|
@@ -1086,6 +1098,10 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1086
1098
|
if (!ctx) {
|
|
1087
1099
|
return;
|
|
1088
1100
|
}
|
|
1101
|
+
if (!client?.isConnected()) {
|
|
1102
|
+
scheduleInboundFlush(INBOUND_IDLE_RETRY_MS);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1089
1105
|
|
|
1090
1106
|
let isIdle: boolean;
|
|
1091
1107
|
try {
|
|
@@ -1305,7 +1321,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1305
1321
|
});
|
|
1306
1322
|
}
|
|
1307
1323
|
function scheduleReconnect(): void {
|
|
1308
|
-
if (disposed || shuttingDown || reconnectTimer || reconnectPromise || !getLiveContext()) {
|
|
1324
|
+
if (disposed || shuttingDown || registrationConflict || reconnectTimer || reconnectPromise || !getLiveContext()) {
|
|
1309
1325
|
return;
|
|
1310
1326
|
}
|
|
1311
1327
|
const scheduledGeneration = runtimeGeneration;
|
|
@@ -1327,6 +1343,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1327
1343
|
if (disposed || shuttingDown) {
|
|
1328
1344
|
throw new Error("Intercom shutting down");
|
|
1329
1345
|
}
|
|
1346
|
+
if (registrationConflict) throw registrationConflict;
|
|
1330
1347
|
if (bossTeamScope.present) {
|
|
1331
1348
|
const selfError = currentSessionId ? bossSelfSessionError(bossTeamScope, currentSessionId) : "Boss session identity is unavailable";
|
|
1332
1349
|
if (selfError) throw new BossTeamScopeError("BOSS_TEAM_METADATA_INVALID", selfError);
|
|
@@ -1359,19 +1376,24 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1359
1376
|
}
|
|
1360
1377
|
client = nextClient;
|
|
1361
1378
|
reconnectAttempt = 0;
|
|
1379
|
+
scheduleInboundFlush(0);
|
|
1362
1380
|
return nextClient;
|
|
1363
1381
|
} catch (error) {
|
|
1364
1382
|
if (client === nextClient) {
|
|
1365
1383
|
client = null;
|
|
1366
1384
|
}
|
|
1367
|
-
if (
|
|
1368
|
-
|
|
1385
|
+
if ((error as { code?: string })?.code === "SESSION_ID_IN_USE" && getLiveContext(contextAtStart, generationAtStart)) {
|
|
1386
|
+
registrationConflict = toError(error);
|
|
1387
|
+
clearReconnectTimer();
|
|
1388
|
+
pi.appendEntry("intercom_registration_conflict", { sessionId: currentSessionId, code: "SESSION_ID_IN_USE", timestamp: Date.now() });
|
|
1389
|
+
notifyIfLive(contextAtStart, "Intercom session is owned by another runtime. Switch to a different session, or release the duplicate owner and /reload. The existing owner was not changed.", "warning", generationAtStart);
|
|
1369
1390
|
}
|
|
1370
1391
|
throw toError(error);
|
|
1371
1392
|
} finally {
|
|
1372
1393
|
if (reconnectPromise === nextReconnectPromise) {
|
|
1373
1394
|
reconnectPromise = null;
|
|
1374
1395
|
reconnectPromiseGeneration = null;
|
|
1396
|
+
if (reason === "background" && !client) scheduleReconnect();
|
|
1375
1397
|
}
|
|
1376
1398
|
}
|
|
1377
1399
|
})();
|
|
@@ -1490,6 +1512,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1490
1512
|
runtimeStarted = true;
|
|
1491
1513
|
runtimeGeneration += 1;
|
|
1492
1514
|
reconnectAttempt = 0;
|
|
1515
|
+
registrationConflict = null;
|
|
1493
1516
|
clearReconnectTimer();
|
|
1494
1517
|
clearStartupConnectTimer();
|
|
1495
1518
|
clearNamePollTimer();
|
|
@@ -1810,11 +1833,16 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1810
1833
|
activeTools.delete(event.toolCallId);
|
|
1811
1834
|
syncPresenceStatus();
|
|
1812
1835
|
});
|
|
1813
|
-
pi.on("agent_end", () => {
|
|
1814
|
-
if (!getLiveContext())
|
|
1815
|
-
return;
|
|
1816
|
-
}
|
|
1836
|
+
pi.on("agent_end", (_event, ctx) => {
|
|
1837
|
+
if (!getLiveContext(ctx)) return;
|
|
1817
1838
|
replyTracker.endTurn();
|
|
1839
|
+
agentRunning = !ctx.isIdle();
|
|
1840
|
+
activeTools.clear();
|
|
1841
|
+
syncPresenceStatus();
|
|
1842
|
+
scheduleInboundFlush(0);
|
|
1843
|
+
});
|
|
1844
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
1845
|
+
if (!getLiveContext(ctx)) return;
|
|
1818
1846
|
agentRunning = false;
|
|
1819
1847
|
activeTools.clear();
|
|
1820
1848
|
syncPresenceStatus();
|
|
@@ -1895,7 +1923,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1895
1923
|
|
|
1896
1924
|
const childOrchestratorMetadata = readChildOrchestratorMetadata();
|
|
1897
1925
|
if (childOrchestratorMetadata) {
|
|
1898
|
-
|
|
1926
|
+
registerIntercomTool({
|
|
1899
1927
|
name: "contact_supervisor",
|
|
1900
1928
|
label: "Contact Supervisor",
|
|
1901
1929
|
description: "Subagent-only tool for contacting the supervisor agent that delegated this task. Use need_decision when blocked, uncertain, needing approval, or facing a product/API/scope decision before continuing; this waits up to 30 seconds, then continues asynchronously if unanswered. Use interview_request when multiple structured questions need supervisor answers; it has the same soft wait. Use progress_update only for meaningful progress or unexpected discoveries that change the plan; this does not wait for a reply. Do not use for routine completion handoffs.",
|
|
@@ -2219,6 +2247,12 @@ Usage:
|
|
|
2219
2247
|
}),
|
|
2220
2248
|
|
|
2221
2249
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
2250
|
+
if (params.action === "status" && registrationConflict) {
|
|
2251
|
+
return {
|
|
2252
|
+
content: [{ type: "text", text: `**Intercom Status:**\nConnected: No\nSession ID: ${currentSessionId}\nState: SESSION_ID_IN_USE\nAnother runtime owns this session; automatic reconnect is paused. Release the duplicate owner and /reload, or switch to a different session.` }],
|
|
2253
|
+
details: { connected: false, sessionId: currentSessionId, code: "SESSION_ID_IN_USE", error: true },
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2222
2256
|
let connectedClient: IntercomClient;
|
|
2223
2257
|
try {
|
|
2224
2258
|
connectedClient = await ensureConnected("tool");
|
|
@@ -2272,7 +2306,7 @@ Usage:
|
|
|
2272
2306
|
|
|
2273
2307
|
return {
|
|
2274
2308
|
content: [{ type: "text", text: `${currentSection}\n\n${otherSection}` }],
|
|
2275
|
-
details: {},
|
|
2309
|
+
details: { sessionId: mySessionId, sessions },
|
|
2276
2310
|
};
|
|
2277
2311
|
} catch (error) {
|
|
2278
2312
|
return {
|
|
@@ -2632,7 +2666,7 @@ Usage:
|
|
|
2632
2666
|
type: "text",
|
|
2633
2667
|
text: `**Intercom Status:**\nConnected: Yes\nSession ID: ${mySessionId}\nActive sessions: ${sessions.length}\nQueued inbound messages: ${inboundInbox?.size ?? 0}\nQueued outbound messages: ${connectedClient.outboxSize}\nPending inbound asks: ${replyTracker.listPending().length}`,
|
|
2634
2668
|
}],
|
|
2635
|
-
details: {},
|
|
2669
|
+
details: { connected: true, sessionId: mySessionId, activeSessions: sessions.length, inboundMessages: inboundInbox?.size ?? 0, outboundMessages: connectedClient.outboxSize, pendingAsks: replyTracker.listPending().length },
|
|
2636
2670
|
};
|
|
2637
2671
|
} catch (error) {
|
|
2638
2672
|
return {
|
|
@@ -2705,7 +2739,7 @@ Usage:
|
|
|
2705
2739
|
legacyIntercomTool.renderCall({ ...args, action }, theme, context);
|
|
2706
2740
|
const renderSplitResult = legacyIntercomTool.renderResult;
|
|
2707
2741
|
|
|
2708
|
-
|
|
2742
|
+
registerIntercomTool({
|
|
2709
2743
|
name: "intercom_send",
|
|
2710
2744
|
label: "Intercom Send",
|
|
2711
2745
|
description: "Send a message to another local Pi session. Both the recipient and message are required.",
|
|
@@ -2721,7 +2755,7 @@ Usage:
|
|
|
2721
2755
|
renderResult: renderSplitResult,
|
|
2722
2756
|
} as any);
|
|
2723
2757
|
|
|
2724
|
-
|
|
2758
|
+
registerIntercomTool({
|
|
2725
2759
|
name: "intercom_ask",
|
|
2726
2760
|
label: "Intercom Ask",
|
|
2727
2761
|
description: "Ask another local Pi session a blocking question, waiting briefly before continuing asynchronously. Do not use this for progress or status checkpoints.",
|
|
@@ -2737,7 +2771,7 @@ Usage:
|
|
|
2737
2771
|
renderResult: renderSplitResult,
|
|
2738
2772
|
} as any);
|
|
2739
2773
|
|
|
2740
|
-
|
|
2774
|
+
registerIntercomTool({
|
|
2741
2775
|
name: "intercom_reply",
|
|
2742
2776
|
label: "Intercom Reply",
|
|
2743
2777
|
description: "Reply to an inbound intercom message or ask. Exact protocol threading is resolved internally.",
|
|
@@ -2754,7 +2788,7 @@ Usage:
|
|
|
2754
2788
|
renderResult: renderSplitResult,
|
|
2755
2789
|
} as any);
|
|
2756
2790
|
|
|
2757
|
-
|
|
2791
|
+
registerIntercomTool({
|
|
2758
2792
|
name: "intercom_team",
|
|
2759
2793
|
label: "Intercom Team",
|
|
2760
2794
|
description: "Show your current manager and the live coworkers owned by that manager. No arguments are required.",
|
|
@@ -2789,8 +2823,9 @@ Usage:
|
|
|
2789
2823
|
},
|
|
2790
2824
|
} as any);
|
|
2791
2825
|
|
|
2792
|
-
|
|
2826
|
+
registerIntercomTool({
|
|
2793
2827
|
name: "intercom_join",
|
|
2828
|
+
executionMode: "sequential",
|
|
2794
2829
|
label: "Intercom Join",
|
|
2795
2830
|
description: "List, join, or create a named intercom team without tmux. Omit name to list joinable teams. Set create=true to create a team and join as manager.",
|
|
2796
2831
|
promptSnippet: "Join or create a named intercom team so intercom_team works without tmux.",
|
|
@@ -2838,7 +2873,7 @@ Usage:
|
|
|
2838
2873
|
{ name: "intercom_status", label: "Intercom Status", action: "status", description: "Show this session's intercom connection status.", promptSnippet: "Show intercom connection status." },
|
|
2839
2874
|
] as const) {
|
|
2840
2875
|
if (definition.name === "intercom_list" && bossTeamScope.restricted) continue;
|
|
2841
|
-
|
|
2876
|
+
registerIntercomTool({
|
|
2842
2877
|
name: definition.name,
|
|
2843
2878
|
label: definition.label,
|
|
2844
2879
|
description: definition.description,
|
|
@@ -2856,7 +2891,7 @@ Usage:
|
|
|
2856
2891
|
}
|
|
2857
2892
|
|
|
2858
2893
|
if (config.legacyTool) {
|
|
2859
|
-
|
|
2894
|
+
registerIntercomTool(legacyIntercomTool);
|
|
2860
2895
|
}
|
|
2861
2896
|
|
|
2862
2897
|
async function resolveCurrentContact(ctx: ExtensionContext, generation = runtimeGeneration): Promise<{ target: string; name?: string; id: string; duplicateName: boolean } | undefined> {
|
package/package.json
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ctliz/agent-intercom-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Pi coding-agent intercom for local messaging with Codex, Claude Code, OpenCode, Grok Build, and AGY agents.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "index.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"intercom-send": "./bin/intercom-send.mjs"
|
|
10
|
+
},
|
|
8
11
|
"exports": {
|
|
9
12
|
".": "./index.ts"
|
|
10
13
|
},
|
|
11
14
|
"files": [
|
|
12
15
|
"banner.png",
|
|
16
|
+
"bin/*.mjs",
|
|
13
17
|
"*.ts",
|
|
14
18
|
"!*.test.ts",
|
|
15
19
|
"broker/**/*.ts",
|
|
@@ -54,9 +58,9 @@
|
|
|
54
58
|
},
|
|
55
59
|
"dependencies": {
|
|
56
60
|
"@ctliz/agent-intercom-core": "0.2.0",
|
|
57
|
-
"@earendil-works/pi-ai": "
|
|
58
|
-
"@earendil-works/pi-coding-agent": "
|
|
59
|
-
"@earendil-works/pi-tui": "
|
|
61
|
+
"@earendil-works/pi-ai": "^0.99.1",
|
|
62
|
+
"@earendil-works/pi-coding-agent": "^0.99.1",
|
|
63
|
+
"@earendil-works/pi-tui": "^0.99.1",
|
|
60
64
|
"tsx": "^4.20.0",
|
|
61
65
|
"typebox": "*"
|
|
62
66
|
},
|
|
@@ -24,6 +24,19 @@ This skill covers how to handle those orchestrator-side escalations.
|
|
|
24
24
|
- **Clarification loops**: Worker asks questions, planner answers, work continues
|
|
25
25
|
- **Multi-session workflows**: Coordinate between specialized sessions (frontend/backend, research/implementation)
|
|
26
26
|
|
|
27
|
+
## Codemode (Pi 0.99.1+)
|
|
28
|
+
|
|
29
|
+
Active Intercom tools can be called from codemode as `await tools.intercom_send({...})`, `await tools.intercom_team({})`, and so on. Every result is `{ ok, text, data }`: check `ok`, then read structured delivery flags, session lists, team roster, or pending asks from `data`. Returned errors retain this object; invalid arguments, blocked calls, and thrown exceptions can still reject. Use `try/catch` or `Promise.allSettled()` for independent operations.
|
|
30
|
+
|
|
31
|
+
```javascript
|
|
32
|
+
const result = await tools.intercom_send({ to: "worker", message: "Tests passed." });
|
|
33
|
+
return { ok: result.ok, delivered: result.data.delivered };
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
A deferred ask has `ok: true` and `data.pending: true`; it is not a failure. Do not proceed with dependent work until the actual answer arrives. Different recipients may be asked concurrently; never create a second unresolved ask to the same recipient. Delivery means durable queue acknowledgement, not task completion.
|
|
37
|
+
|
|
38
|
+
For shell notifications, use `intercom-send <session-name-or-id> <message>`. It registers an independent send-only identity and prints JSON; it does not take over the current Pi session or track replies.
|
|
39
|
+
|
|
27
40
|
## Core Patterns
|
|
28
41
|
|
|
29
42
|
### Pattern 1: Planner-Worker Delegation
|
|
@@ -467,7 +480,7 @@ if (result.details?.pending) {
|
|
|
467
480
|
|
|
468
481
|
### Session name flips or registration reports `SESSION_ID_IN_USE`
|
|
469
482
|
|
|
470
|
-
The same Pi session is open in more than one live runtime, such as a desktop terminal and a mobile/RPC host. Intercom keeps the first runtime authoritative instead of allowing the two clients to evict each other.
|
|
483
|
+
The same Pi session is open in more than one live runtime, such as a desktop terminal and a mobile/RPC host. Intercom keeps the first runtime authoritative instead of allowing the two clients to evict each other. The conflicting runtime pauses automatic reconnect; `intercom_status` returns `ok: false` with `data.code: "SESSION_ID_IN_USE"`. Switch to a different session, or release the duplicate owner and `/reload`. One transcript/session ID must have only one live owner; do not auto-generate a replacement identity for a running session.
|
|
471
484
|
|
|
472
485
|
### Message not delivered
|
|
473
486
|
|
package/tool-result.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
export const intercomOutputSchema = Type.Object({
|
|
5
|
+
ok: Type.Boolean(),
|
|
6
|
+
text: Type.String(),
|
|
7
|
+
data: Type.Record(Type.String(), Type.Unknown()),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
type ToolResult = Awaited<ReturnType<ToolDefinition<any, any>["execute"]>>;
|
|
11
|
+
|
|
12
|
+
/** Keep model/TUI content unchanged while giving codemode callers JSON data. */
|
|
13
|
+
export function structuredIntercomResult(result: ToolResult): ToolResult {
|
|
14
|
+
const details = result.details ?? {};
|
|
15
|
+
const isError = result.isError === true || details.error === true || details.delivered === false;
|
|
16
|
+
return {
|
|
17
|
+
...result,
|
|
18
|
+
details,
|
|
19
|
+
isError,
|
|
20
|
+
structuredContent: {
|
|
21
|
+
ok: !isError,
|
|
22
|
+
text: result.content.filter((item) => item.type === "text").map((item) => item.text).join("\n"),
|
|
23
|
+
data: JSON.parse(JSON.stringify(details)),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|