@ctliz/agent-intercom-pi 0.12.2 → 0.14.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 +80 -10
- package/bin/intercom-send.mjs +4 -0
- package/broker/client.ts +7 -1
- package/broker/framing.ts +4 -1
- package/cli-send.ts +39 -0
- package/index.ts +180 -72
- package/named-team-membership.ts +124 -0
- package/named-teams.ts +12 -0
- package/package.json +12 -2
- package/reply-tracker.ts +36 -13
- package/skills/pi-intercom/SKILL.md +74 -8
- package/tool-result.ts +26 -0
- package/types.ts +2 -0
- package/ui/inline-message.ts +2 -1
package/README.md
CHANGED
|
@@ -17,6 +17,76 @@
|
|
|
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 1.0 compatibility and codemode
|
|
21
|
+
|
|
22
|
+
Version 0.14.0 is tested with Pi 1.0.0. The preceding 0.13.0 release was tested with Pi 0.99.1. The development test baseline now uses Pi 1.0.0; host-provided Pi modules remain peer dependencies, not runtime dependencies. The existing protocol v4 broker, durable queues, acknowledgements, and cross-harness routing are unchanged.
|
|
23
|
+
|
|
24
|
+
Pi 1.0 defaults to fullscreen; set `tuiMode` to `"regular"` or launch with `--tui-mode regular` to retain terminal scrollback. Codemode's shorter declarations preserve Intercom's structured `{ ok, text, data }` results and team guidelines. To check tool existence in a script, use `"intercom_send" in tools`, not `typeof tools.intercom_send`, because unknown members now throw. Restart existing Pi processes to use a newly installed Pi version; `/reload` only reloads resources inside the running version.
|
|
25
|
+
|
|
26
|
+
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.
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
const sessions = await tools.intercom_list({});
|
|
30
|
+
if (!sessions.ok) throw new Error(sessions.text);
|
|
31
|
+
const target = sessions.data.sessions.find(s => s.name === "worker");
|
|
32
|
+
if (!target) throw new Error("Worker is offline");
|
|
33
|
+
const result = await tools.intercom_send({ to: target.id, message: "Tests passed." });
|
|
34
|
+
return { ok: result.ok, accepted: result.data.accepted, delivered: result.data.delivered };
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Concurrent sends and independent asks are supported; do not create two unresolved asks to the same recipient. Team joins execute sequentially; named task-team updates are also locked across independently launched Pi processes. Named joins append membership without changing the caller's broker 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.
|
|
38
|
+
|
|
39
|
+
### Send from a shell or release script
|
|
40
|
+
|
|
41
|
+
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:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm exec --yes --package=@ctliz/pi-intercom@0.14.0 -- intercom-send worker 'Tests passed.'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
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.
|
|
48
|
+
|
|
49
|
+
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.
|
|
50
|
+
|
|
51
|
+
## Pi-local task teams
|
|
52
|
+
|
|
53
|
+
Open Pi normally in separate terminals; no startup team, session name for the
|
|
54
|
+
coordinator, or terminal multiplexer is required. When you ask an agent to work
|
|
55
|
+
with `front` and `writer`, its tool guidelines tell it to ask once whether to form
|
|
56
|
+
a team. After approval (or an explicit request to form a team), it discovers the
|
|
57
|
+
sessions and creates the task group itself:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
intercom_join({ name: "launch", create: true, members: ["front", "writer"], work: "Build the product page" })
|
|
61
|
+
intercom_send({ team: "launch", to: "front", message: "Implement the UI." })
|
|
62
|
+
intercom_send({ team: "launch", to: "writer", message: "Write the copy." })
|
|
63
|
+
intercom_team({}) // all your task teams
|
|
64
|
+
intercom_team({ team: "launch" }) // one team's manager and members
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Joining another named team appends membership, never replaces it. The manager can
|
|
68
|
+
append approved peers with `members`; any session can join itself. Missing or
|
|
69
|
+
ambiguous targets fail before membership is written. Team updates are persistent
|
|
70
|
+
and serialized across Pi processes. A different task can use a different team
|
|
71
|
+
with the same participants.
|
|
72
|
+
|
|
73
|
+
Every task message carries a public team name, displayed as `[Team: launch]`.
|
|
74
|
+
When two sessions share multiple teams, sends/asks require `team`. Replies inherit
|
|
75
|
+
the exact original message's team: use its receiver-local `contextId` reply hint,
|
|
76
|
+
or an `askId` from `intercom_pending`. Mixed-team batches retain every message's
|
|
77
|
+
label; ambiguous replies fail rather than guessing.
|
|
78
|
+
|
|
79
|
+
This feature changes only Pi's named teams and prompt guidelines. It does not
|
|
80
|
+
change the shared v4 broker or other adapters, does not add a security boundary,
|
|
81
|
+
and does not isolate model history per team. Without a shared team, omitting
|
|
82
|
+
`team` sends an ungrouped direct message even when either session belongs to
|
|
83
|
+
unrelated teams, allowing initial contact before forming a team. Explicit team
|
|
84
|
+
messages still require both sessions to be members; multiple shared teams still
|
|
85
|
+
require selecting `team`. Consent is an agent instruction, not a broker-enforced approval record.
|
|
86
|
+
Managed-team and workspace integration paths are unchanged. All participating Pi
|
|
87
|
+
sessions need the 0.14.0 Pi adapter and `/reload`. This is not a cross-adapter
|
|
88
|
+
protocol rollout.
|
|
89
|
+
|
|
20
90
|
## Grok Build and AGY support
|
|
21
91
|
|
|
22
92
|
Grok Build and AGY are supported as first-class protocol peers through two dedicated npm packages:
|
|
@@ -93,7 +163,7 @@ Each pi session that has `pi-intercom` loaded and enabled connects to a tiny loc
|
|
|
93
163
|
## Install
|
|
94
164
|
|
|
95
165
|
```bash
|
|
96
|
-
pi install
|
|
166
|
+
pi install npm:@ctliz/pi-intercom@0.14.0
|
|
97
167
|
```
|
|
98
168
|
|
|
99
169
|
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.
|
|
@@ -292,7 +362,7 @@ If you never set `/name`, Intercom still exposes a runtime-only fallback alias s
|
|
|
292
362
|
|
|
293
363
|
### How `intercom_team` chooses a team
|
|
294
364
|
|
|
295
|
-
`intercom_team({})`
|
|
365
|
+
With Pi-local task teams, `intercom_team({})` first shows all named teams containing this session; `intercom_team({ team: "billing" })` selects one. If there are no named task teams, the existing managed-team resolution below applies. Boss mode keeps its existing resolution and does not accept named task teams.
|
|
296
366
|
|
|
297
367
|
1. **Orchestrator** — `~/.pi/agent/intercom/orchestrator/workers.json` has an owned record for this session (`AGENT_INTERCOM_WORKER_ID`) or this session is the current manager of live owned coworkers.
|
|
298
368
|
2. **TmuxDeck manifest** — `AGENT_INTERCOM_TEAM_MANIFEST` points at a valid team file. The Lead is `leadId`; workers are the other members. An invalid or empty manifest fails closed and does not fall through to the live roster.
|
|
@@ -321,7 +391,7 @@ Manifest, live-roster, and standalone teams cannot inspect another session's inb
|
|
|
321
391
|
|
|
322
392
|
### Create or join a team without tmux
|
|
323
393
|
|
|
324
|
-
Named teams live in the local Intercom directory. Creating one
|
|
394
|
+
Named teams live in the local Intercom directory. Creating one makes this session the task-team manager and stores explicit member session IDs. Joining appends membership without switching broker scope or replacing previous memberships. Legacy records retain their manager; peers must join again to record their membership. Tmux and TmuxDeck are not required.
|
|
325
395
|
|
|
326
396
|
```text
|
|
327
397
|
/intercom-create billing # create a named team and join as manager
|
|
@@ -601,11 +671,11 @@ The supervisor can reply with plain JSON or a fenced `json` block. If the reply
|
|
|
601
671
|
|
|
602
672
|
| Tool | Parameters | Description |
|
|
603
673
|
|------|------------|-------------|
|
|
604
|
-
| `intercom_send` | required `to`,
|
|
605
|
-
| `intercom_ask` | required `to`,
|
|
606
|
-
| `intercom_reply` | required `message
|
|
607
|
-
| `intercom_team` |
|
|
608
|
-
| `intercom_join` | optional `name`,
|
|
674
|
+
| `intercom_send` | required `to`, `message`; optional `team`, `attachments` | Fire-and-forget delivery in a task team |
|
|
675
|
+
| `intercom_ask` | required `to`, `message`; optional `team`, `attachments` | Ask and wait briefly for a reply |
|
|
676
|
+
| `intercom_reply` | required `message`; optional `contextId`, `askId`, `team`, `to`, `which` | Select an exact inbound context and inherit its team |
|
|
677
|
+
| `intercom_team` | optional `team` | Show your named task teams, or fall back to managed-team discovery |
|
|
678
|
+
| `intercom_join` | optional `name`, `create`, `members`, `work` | Append membership; a manager can add connected peers, and `work` describes a newly created task |
|
|
609
679
|
| `intercom_list` | none | List connected sessions in your scope |
|
|
610
680
|
| `intercom_pending` | optional `askId`, `session` | List unresolved inbound asks with stable IDs; `askId` retrieves the full untruncated body, and managers may use `session` for an owned coworker |
|
|
611
681
|
| `intercom_status` | none | Show connection and queue status |
|
|
@@ -630,7 +700,7 @@ Only registered in sessions where `pi-subagents` supplied the required child bri
|
|
|
630
700
|
|
|
631
701
|
### Tool behavior
|
|
632
702
|
|
|
633
|
-
**`intercom_team`** reads orchestrator ownership dynamically and returns the current manager plus live same-manager coworkers. After adoption it follows the new manager without restarting the worker; `AGENT_INTERCOM_MANAGER_TARGET` is only a startup fallback.
|
|
703
|
+
**`intercom_team`** first shows your named Pi task teams. Without named memberships it reads orchestrator ownership dynamically and returns the current manager plus live same-manager coworkers. After adoption it follows the new manager without restarting the worker; `AGENT_INTERCOM_MANAGER_TARGET` is only a startup fallback.
|
|
634
704
|
|
|
635
705
|
**`intercom_join`** lists named teams and TmuxDeck workspaces, joins one by name, or creates a named team with `create: true`. Creating a team does not require tmux. Listing never prints the raw scope.
|
|
636
706
|
|
|
@@ -640,7 +710,7 @@ Only registered in sessions where `pi-subagents` supplied the required child bri
|
|
|
640
710
|
|
|
641
711
|
**`intercom_ask`** waits up to 30 seconds for a prompt reply, then returns a successful pending result while keeping the request open for a late reply. Different recipients may wait concurrently; the same recipient may have only one unresolved ask. `PI_INTERCOM_ASK_WAIT_MS` changes the blocking window.
|
|
642
712
|
|
|
643
|
-
**`intercom_reply`** resolves the active or pending inbound context internally.
|
|
713
|
+
**`intercom_reply`** resolves the active or pending inbound context internally and inherits its original team. Use the `contextId` in a team message's reply hint for an exact ordinary message, or pass the stable `askId` returned by `intercom_pending` to select one exact ask. A `team` selector must match the original message, never override it. Optional `to` and `which: "oldest" | "latest"` remain available for compatibility. None of these values exposes the protocol thread ID.
|
|
644
714
|
|
|
645
715
|
The broker refuses a second unresolved `intercom_ask` from one session to the same recipient. Wait for the first answer or use `intercom_send` for a non-blocking follow-up.
|
|
646
716
|
|
package/broker/client.ts
CHANGED
|
@@ -38,6 +38,7 @@ import type {
|
|
|
38
38
|
|
|
39
39
|
export interface SendOptions {
|
|
40
40
|
text: string;
|
|
41
|
+
team?: string;
|
|
41
42
|
attachments?: Attachment[];
|
|
42
43
|
control?: IntercomCommonControlEnvelope;
|
|
43
44
|
replyTo?: string;
|
|
@@ -125,6 +126,8 @@ function isMessage(value: unknown): value is Message {
|
|
|
125
126
|
return false;
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
if (content.team !== undefined && (typeof content.team !== "string" || !/^[A-Za-z][A-Za-z0-9_-]{0,31}$/.test(content.team))) return false;
|
|
130
|
+
|
|
128
131
|
const attachmentsValid = content.attachments === undefined
|
|
129
132
|
|| (Array.isArray(content.attachments) && content.attachments.every(isAttachment));
|
|
130
133
|
const controlValid = content.control === undefined || isIntercomCommonControlEnvelope(content.control);
|
|
@@ -376,7 +379,9 @@ export class IntercomClient extends EventEmitter {
|
|
|
376
379
|
};
|
|
377
380
|
|
|
378
381
|
const onReaderError = (error: Error) => {
|
|
379
|
-
const protocolError = new Error(`Intercom protocol error: ${error.message}`, { cause: error });
|
|
382
|
+
const protocolError = new Error(`Intercom protocol error: ${error.message}`, { cause: error }) as Error & { code?: string };
|
|
383
|
+
const code = (error as Error & { code?: string }).code;
|
|
384
|
+
if (code) protocolError.code = code;
|
|
380
385
|
if (!connectionEstablished) {
|
|
381
386
|
onError(protocolError);
|
|
382
387
|
return;
|
|
@@ -809,6 +814,7 @@ export class IntercomClient extends EventEmitter {
|
|
|
809
814
|
expectsReply: options.expectsReply,
|
|
810
815
|
content: {
|
|
811
816
|
text: options.text,
|
|
817
|
+
...(options.team === undefined ? {} : { team: options.team }),
|
|
812
818
|
attachments: options.attachments,
|
|
813
819
|
control: options.control,
|
|
814
820
|
},
|
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
|
+
});
|