@trim21/personal-pi-extensions 0.0.201 → 0.0.203
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 +6 -6
- package/package.json +4 -1
- package/src/talk/core.ts +12 -12
- package/src/talk/format.ts +12 -8
- package/src/talk/index.ts +12 -0
- package/src/talk/skills/multi-agent-dev/SKILL.md +94 -0
package/README.md
CHANGED
|
@@ -230,12 +230,12 @@ index.ts —— pi adapter:把 core 接到 pi 的 sendMessage / 生命周
|
|
|
230
230
|
|
|
231
231
|
### 工具(LLM 可见)
|
|
232
232
|
|
|
233
|
-
| 工具 | 作用
|
|
234
|
-
| -------------------- |
|
|
235
|
-
| `talk-list-sessions` |
|
|
236
|
-
| `talk-ask` | 向某个 session 提问并阻塞等待回复(默认 30 分钟超时)
|
|
237
|
-
| `talk-send` | 发送纯文本消息(`to: "*"` 广播所有,`to: "cwd"` 广播同 cwd)
|
|
238
|
-
| `talk-reply` | 回复一个 ask(`replyTo` 为 ask id,显式关联、不推断)
|
|
233
|
+
| 工具 | 作用 |
|
|
234
|
+
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
235
|
+
| `talk-list-sessions` | 列出会话,返回 JSON 数组(`status` / `work_dir` / `id` / `name`,自己带 `self: true`);默认只列有心跳的,`includeOffline: true` 列全部 |
|
|
236
|
+
| `talk-ask` | 向某个 session 提问并阻塞等待回复(默认 30 分钟超时) |
|
|
237
|
+
| `talk-send` | 发送纯文本消息(`to: "*"` 广播所有,`to: "cwd"` 广播同 cwd) |
|
|
238
|
+
| `talk-reply` | 回复一个 ask(`replyTo` 为 ask id,显式关联、不推断) |
|
|
239
239
|
|
|
240
240
|
对端消息自动投递(无需主动拉取):投递方式由 `talk.deliver` 配置,`steer` 在模型工作过程中打断/唤醒,`queue` 排队到 session 下一轮自然 turn 时注入。
|
|
241
241
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trim21/personal-pi-extensions",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.203",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
|
|
6
6
|
"keywords": [
|
|
@@ -67,6 +67,9 @@
|
|
|
67
67
|
"src/opencode-todo.ts",
|
|
68
68
|
"src/question.ts",
|
|
69
69
|
"src/talk/index.ts"
|
|
70
|
+
],
|
|
71
|
+
"skills": [
|
|
72
|
+
"src/talk/skills"
|
|
70
73
|
]
|
|
71
74
|
},
|
|
72
75
|
"lint-staged": {
|
package/src/talk/core.ts
CHANGED
|
@@ -449,17 +449,18 @@ export class TalkCore {
|
|
|
449
449
|
// ── Tool actions ───────────────────────────────────────────────────────
|
|
450
450
|
|
|
451
451
|
/**
|
|
452
|
-
* JSON listing of visible
|
|
453
|
-
* heartbeat is fresh (within LIST_ACTIVE_MS);
|
|
454
|
-
* every visible peer regardless of last contact.
|
|
452
|
+
* JSON listing of visible sessions, including self (marked `self: true`).
|
|
453
|
+
* Defaults to sessions whose heartbeat is fresh (within LIST_ACTIVE_MS);
|
|
454
|
+
* pass includeOffline to show every visible peer regardless of last contact.
|
|
455
455
|
*/
|
|
456
456
|
async list(includeOffline = false): Promise<string> {
|
|
457
457
|
const self = this.requireSelf();
|
|
458
458
|
const now = this.now();
|
|
459
459
|
const all = await listRecords(this.storage);
|
|
460
|
-
const records = all.filter(
|
|
461
|
-
(r
|
|
462
|
-
|
|
460
|
+
const records = all.filter((r) => {
|
|
461
|
+
if (r.addr === self.addr) return !this.dead;
|
|
462
|
+
return this.isPeerVisible(r.cwd) && (includeOffline || now - r.lastSeenAt < LIST_ACTIVE_MS);
|
|
463
|
+
});
|
|
463
464
|
return formatListing(records, self.addr, (r) => presenceOf(r, now));
|
|
464
465
|
}
|
|
465
466
|
|
|
@@ -468,12 +469,11 @@ export class TalkCore {
|
|
|
468
469
|
const self = this.requireSelf();
|
|
469
470
|
const now = this.now();
|
|
470
471
|
const records = await listRecords(this.storage);
|
|
471
|
-
const filtered = records.filter(
|
|
472
|
-
(r)
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
);
|
|
472
|
+
const filtered = records.filter((r) => {
|
|
473
|
+
if (r.cwd !== cwd) return false;
|
|
474
|
+
if (r.addr === self.addr) return !this.dead;
|
|
475
|
+
return this.isPeerVisible(r.cwd) && (includeOffline || now - r.lastSeenAt < LIST_ACTIVE_MS);
|
|
476
|
+
});
|
|
477
477
|
return formatListing(filtered, self.addr, (r) => presenceOf(r, now));
|
|
478
478
|
}
|
|
479
479
|
|
package/src/talk/format.ts
CHANGED
|
@@ -23,34 +23,38 @@ export function age(ts: number, now: number = Date.now()): string {
|
|
|
23
23
|
|
|
24
24
|
/** Delivery text injected into the receiving session's LLM context. */
|
|
25
25
|
export function formatDelivery(letter: Letter, now: number = Date.now()): string {
|
|
26
|
-
const
|
|
26
|
+
const from = letter.from;
|
|
27
|
+
const header = `From pi session ${from.sessionId} (${from.cwd}) — "${from.name}"`;
|
|
27
28
|
const meta = `_id ${letter.id} · ${letter.kind} · sent ${age(letter.ts, now)}_`;
|
|
28
29
|
const hint =
|
|
29
30
|
letter.kind === "ask" ? `\n\nReply with the talk-reply tool, replyTo: "${letter.id}"` : "";
|
|
30
31
|
return `${BOUNDARY_PREAMBLE}\n\n${header}:\n\n${letter.body}\n\n${meta}${hint}`;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
/** One
|
|
34
|
-
*
|
|
34
|
+
/** One session as the model sees it in a listing. `id` is the stable pi
|
|
35
|
+
* session uuid; `name` is the display name when one was set; `self` marks
|
|
36
|
+
* the calling session itself. */
|
|
35
37
|
export interface SessionListItem {
|
|
36
38
|
status: string;
|
|
37
39
|
work_dir: string;
|
|
38
40
|
id: string;
|
|
39
41
|
name?: string;
|
|
42
|
+
self?: boolean;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
/** Machine-readable JSON listing of
|
|
45
|
+
/** Machine-readable JSON listing of sessions (what the model sees). */
|
|
43
46
|
export function formatListing(
|
|
44
47
|
records: SessionRecord[],
|
|
45
48
|
selfAddr: string,
|
|
46
49
|
presence: (r: SessionRecord) => Presence,
|
|
47
50
|
): string {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const items: SessionListItem[] = others.map((r) => {
|
|
51
|
+
if (records.length === 0) return "[]";
|
|
52
|
+
const items: SessionListItem[] = records.map((r) => {
|
|
51
53
|
const p = presence(r);
|
|
52
54
|
const status = p === "live" ? r.status : p === "stalled" ? "not responding" : "offline";
|
|
53
|
-
|
|
55
|
+
const item: SessionListItem = { status, work_dir: r.cwd, id: r.sessionId, name: r.name };
|
|
56
|
+
if (r.addr === selfAddr) item.self = true;
|
|
57
|
+
return item;
|
|
54
58
|
});
|
|
55
59
|
return JSON.stringify(items, null, 2);
|
|
56
60
|
}
|
package/src/talk/index.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as os from "node:os";
|
|
13
13
|
import * as path from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
14
15
|
|
|
15
16
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
17
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
@@ -31,6 +32,9 @@ const NOTIFY_TYPE = "talk:notify";
|
|
|
31
32
|
|
|
32
33
|
const ASK_TIMEOUT_MS = 30 * 60 * 1000;
|
|
33
34
|
|
|
35
|
+
/** Guide the model to the multi-agent workflow skill shipped with this package. */
|
|
36
|
+
const SKILL_PATH = fileURLToPath(new URL("skills/multi-agent-dev/SKILL.md", import.meta.url));
|
|
37
|
+
|
|
34
38
|
function toolResult(text: string) {
|
|
35
39
|
return { content: [{ type: "text" as const, text }], details: {} };
|
|
36
40
|
}
|
|
@@ -200,6 +204,14 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
200
204
|
pi.on("agent_start", () => core.setWorking());
|
|
201
205
|
pi.on("agent_end", () => core.setIdle());
|
|
202
206
|
pi.on("agent_settled", () => core.setIdle());
|
|
207
|
+
pi.on("before_agent_start", (event) => {
|
|
208
|
+
// One-line nudge: before coordinating with other pi sessions, read the
|
|
209
|
+
// shipped workflow skill. Skipped when the skill file is absent.
|
|
210
|
+
if (!fs.existsSync(SKILL_PATH)) return;
|
|
211
|
+
return {
|
|
212
|
+
systemPrompt: `${event.systemPrompt}\n\nBefore multi-session collaboration, read ${SKILL_PATH} to understand the talk workflow.`,
|
|
213
|
+
};
|
|
214
|
+
});
|
|
203
215
|
pi.on("session_info_changed", () => {
|
|
204
216
|
if (self) core.setSessionName(pi.getSessionName() ?? self.name);
|
|
205
217
|
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: multi-agent-dev
|
|
3
|
+
description: Coordinate multi-agent development across pi sessions using the talk extension. Explains how sessions discover and address each other, when to use talk-send vs talk-ask, and how to split work, exchange information, and review between agents. Use whenever you need to collaborate with other pi sessions or agents on the same machine.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Multi-Agent Development with Talk
|
|
7
|
+
|
|
8
|
+
## Concept
|
|
9
|
+
|
|
10
|
+
Multi-agent development runs several independent pi sessions in parallel and coordinates them over **talk**. Every session is a complete agent workspace — its own cwd, conversation history, and context. Talk lets sessions discover each other, exchange messages, ask questions, and sync progress.
|
|
11
|
+
|
|
12
|
+
The core rule: **a peer only knows what you tell it.** Messages must be self-contained — background, goal, and constraints — because the receiving session has none of your context.
|
|
13
|
+
|
|
14
|
+
## Session model
|
|
15
|
+
|
|
16
|
+
### Discovery and addressing
|
|
17
|
+
|
|
18
|
+
- `talk-list-sessions` returns sessions as JSON — **your own session is included and marked `self: true`** (also where you learn your own id):
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
[
|
|
22
|
+
{
|
|
23
|
+
"status": "idle",
|
|
24
|
+
"work_dir": "/path/to/cwd",
|
|
25
|
+
"id": "0193a2f5-...",
|
|
26
|
+
"name": "...",
|
|
27
|
+
"self": true
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- Addressing is **by session id only**: `talk-send` / `talk-ask` take the full `id` (pi session uuid). Names, paths, and prefixes are not accepted.
|
|
33
|
+
- An unknown or invisible target is refused with `Unknown session id` — always list before sending.
|
|
34
|
+
|
|
35
|
+
### Status
|
|
36
|
+
|
|
37
|
+
- `idle` / `working` (agent actively running) / `waiting-talk-message` (blocked in `talk-ask` waiting for a reply)
|
|
38
|
+
- `not responding` (heartbeat stale, process alive but unresponsive) / `offline` (process exited or marked dead)
|
|
39
|
+
- The default listing shows only sessions with a heartbeat in the last 15 minutes; pass `includeOffline: true` for everything.
|
|
40
|
+
|
|
41
|
+
### Visibility
|
|
42
|
+
|
|
43
|
+
- Each workspace controls what it can see via `allowed` in `<cwd>/.pi/talk.json` (path prefixes). Sessions outside the prefixes are neither listed nor addressable.
|
|
44
|
+
- Visibility is one-way: you seeing a session does not mean it sees you.
|
|
45
|
+
|
|
46
|
+
## Tools
|
|
47
|
+
|
|
48
|
+
| Tool | Purpose |
|
|
49
|
+
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
50
|
+
| `talk-list-sessions` | List visible sessions (`id` / `status` / `work_dir` / `name`) |
|
|
51
|
+
| `talk-send` | Send a plain message (async — the main collaboration primitive). `to: "*"` broadcasts, `to: "cwd"` broadcasts to the same directory |
|
|
52
|
+
| `talk-ask` | Ask a question and block for the reply (default 30 min timeout) |
|
|
53
|
+
| `talk-reply` | Reply to a received ask; `replyTo` is the ask id shown in the delivered message |
|
|
54
|
+
|
|
55
|
+
In the TUI: `/talk` lists sessions, `/talk-dead` marks a session as dead (removes it from listings and sweeps it).
|
|
56
|
+
|
|
57
|
+
## Collaboration workflows
|
|
58
|
+
|
|
59
|
+
### Split work between sessions
|
|
60
|
+
|
|
61
|
+
1. `talk-list-sessions` first: see which sessions exist, their `work_dir`, and status.
|
|
62
|
+
2. Assign work by module/files with `talk-send` — state the scope, boundaries, and expected output.
|
|
63
|
+
3. Each session completes its slice, then sends the result or a review request.
|
|
64
|
+
4. Sync progress periodically to avoid overlapping edits.
|
|
65
|
+
|
|
66
|
+
### Synchronous question/answer (need the answer to continue)
|
|
67
|
+
|
|
68
|
+
- Use `talk-ask` when the next step depends on the peer's information and the peer is reachable.
|
|
69
|
+
- On receiving an ask, reply with `talk-reply` using the `replyTo` id from the delivered message.
|
|
70
|
+
- If two sessions ask each other simultaneously: the later asker yields — answer the peer's ask first, then re-ask.
|
|
71
|
+
|
|
72
|
+
### Async notifications
|
|
73
|
+
|
|
74
|
+
- Use `talk-send` for heads-ups that do not block: send and keep working.
|
|
75
|
+
- Messages deliver on the next natural turn by default (`queue`); `steer` interrupts the peer immediately — behavior depends on the `talk.deliver` setting.
|
|
76
|
+
|
|
77
|
+
### Cross-session review
|
|
78
|
+
|
|
79
|
+
- Ask another session to review your changes: `talk-send` the file paths plus a diff summary, request a review, and let it reply.
|
|
80
|
+
- Send paths and summaries, not whole file contents — the peer can `read` them itself.
|
|
81
|
+
|
|
82
|
+
## Message style
|
|
83
|
+
|
|
84
|
+
- Plain text, ≤32KB. Send a summary and paths, never the full file or large code blocks.
|
|
85
|
+
- Self-contained: background, goal, constraints — the peer has none of your context.
|
|
86
|
+
- Make the ask explicit: "please review", "please implement", or "FYI only".
|
|
87
|
+
- One topic per message so the reply stays focused.
|
|
88
|
+
|
|
89
|
+
## Pitfalls
|
|
90
|
+
|
|
91
|
+
- **Avoid message loops**: if the peer sent you something or is asking you, answer it before sending new ones. Two agents pinging each other deadlock.
|
|
92
|
+
- **Address from known ids**: only run `talk-list-sessions` to discover sessions or verify an id. If you already hold a valid id (e.g. from an incoming message or a previous listing), send directly — an unknown or invisible id is refused with `Unknown session id`.
|
|
93
|
+
- **Respect status**: asking an offline/not-responding session blocks up to 30 min. Prefer `talk-send` there — the message queues on disk and the peer receives it when it resumes.
|
|
94
|
+
- **Visibility boundary**: you can only collaborate with sessions you can see; invisible sessions are unreachable by design.
|