@spoons-and-mirrors/iam 0.1.4 → 0.1.6
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 +84 -26
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +459 -226
- package/dist/logger.d.ts +7 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/prompt.d.ts +10 -18
- package/dist/prompt.d.ts.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,42 +1,100 @@
|
|
|
1
|
-
# IAM
|
|
1
|
+
# IAM (Inter-Agent Messaging)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Enable parallel agents communication for opencode
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
When you spawn multiple agents with the `task` tool, they can:
|
|
8
|
-
- **Announce** what they're working on (and see all parallel agents)
|
|
9
|
-
- **Broadcast** messages to one, some, or all agents
|
|
10
|
-
- Get **notified** when new messages arrive
|
|
5
|
+
`@spoons-and-mirrors/iam@latest`
|
|
11
6
|
|
|
12
7
|
## How It Works
|
|
13
8
|
|
|
14
|
-
|
|
9
|
+
Parallel agents they can send messages to each other using the `broadcast` tool. Messages are relayed to the proper agent's context.
|
|
10
|
+
|
|
11
|
+
```mermaid
|
|
12
|
+
sequenceDiagram
|
|
13
|
+
participant Parent as Parent Session
|
|
14
|
+
participant A as AgentA
|
|
15
|
+
participant B as AgentB
|
|
16
|
+
|
|
17
|
+
Parent->>A: spawn task
|
|
18
|
+
Parent->>B: spawn task
|
|
19
|
+
|
|
20
|
+
Note over A,B: Both agents auto-register on first LLM call
|
|
21
|
+
|
|
22
|
+
A->>B: broadcast(recipient="agentB", message="Question?")
|
|
23
|
+
|
|
24
|
+
Note over B: Receives message
|
|
25
|
+
Note over B: Responds
|
|
26
|
+
|
|
27
|
+
B->>A: broadcast(recipient="agentA", reply_to=[1], message="Answer!")
|
|
28
|
+
|
|
29
|
+
Note over A: Receives reply
|
|
30
|
+
Note over B: Tool output shows both reply and handled message
|
|
31
|
+
Note over B: Remove message from inbox
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## The `broadcast` Tool
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
broadcast(message="...") # Send to all agents
|
|
38
|
+
broadcast(recipient="agentB", message="...") # Send to specific agent
|
|
39
|
+
broadcast(reply_to=[1, 2], message="...") # Mark messages as handled
|
|
40
|
+
broadcast(recipient="agentA", reply_to=[1], message="...") # Reply and mark handled
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Parameters
|
|
44
|
+
|
|
45
|
+
| Parameter | Required | Description |
|
|
46
|
+
| ----------- | -------- | -------------------------------------------------------------------- |
|
|
47
|
+
| `message` | Yes | Your message content |
|
|
48
|
+
| `recipient` | No | Target agent(s), comma-separated. Omit to send to all |
|
|
49
|
+
| `reply_to` | No | Array of message IDs to mark as handled (e.g., `[1]` or `[1, 2, 3]`) |
|
|
50
|
+
|
|
51
|
+
## Receiving Messages
|
|
52
|
+
|
|
53
|
+
Messages appear as a `broadcast` tool result with structured data:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"messages": [
|
|
58
|
+
{ "id": 1, "from": "agentA", "body": "What's the status on the API?" },
|
|
59
|
+
{ "id": 2, "from": "agentA", "body": "Also, can you check the tests?" }
|
|
60
|
+
],
|
|
61
|
+
"agents": ["agentA", "agentC: Working on backend"]
|
|
62
|
+
}
|
|
63
|
+
```
|
|
15
64
|
|
|
16
|
-
|
|
65
|
+
The `agents` array always shows available agents to message. This is injected even when there are no incoming messages.
|
|
17
66
|
|
|
18
|
-
|
|
67
|
+
Messages persist in the inbox until the agent marks them as handled using `reply_to`.
|
|
19
68
|
|
|
20
|
-
|
|
69
|
+
## Installation
|
|
21
70
|
|
|
22
|
-
|
|
71
|
+
Add to your OpenCode config:
|
|
23
72
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
| `read` | Read your inbox (marks messages as read). |
|
|
28
|
-
| `broadcast` | Send a message. Use `to` for specific agent(s), or omit for all. |
|
|
73
|
+
```
|
|
74
|
+
"plugin": ["@spoons-and-mirrors/iam@latest"]
|
|
75
|
+
```
|
|
29
76
|
|
|
30
|
-
##
|
|
77
|
+
## Example Workflow
|
|
31
78
|
|
|
32
79
|
```
|
|
33
|
-
#
|
|
34
|
-
action="announce", message="Refactoring the auth module"
|
|
80
|
+
# Parent spawns two agents to work on different parts of a feature
|
|
35
81
|
|
|
36
|
-
|
|
37
|
-
|
|
82
|
+
AgentA (working on frontend):
|
|
83
|
+
→ broadcast(message="Starting frontend work")
|
|
84
|
+
→ ... does work ...
|
|
85
|
+
→ broadcast(recipient="agentB", message="Need the API schema")
|
|
38
86
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
87
|
+
AgentB (working on backend):
|
|
88
|
+
→ broadcast(message="Starting backend work")
|
|
89
|
+
→ ... sees AgentA's question in inbox ...
|
|
90
|
+
→ broadcast(recipient="agentA", reply_to=[1], message="Here's the schema: {...}")
|
|
91
|
+
|
|
92
|
+
AgentA:
|
|
93
|
+
→ ... sees AgentB's response in inbox ...
|
|
94
|
+
→ broadcast(reply_to=[1], message="Got it, thanks!")
|
|
42
95
|
```
|
|
96
|
+
|
|
97
|
+
## Notes
|
|
98
|
+
|
|
99
|
+
- Agents are assigned aliases automatically: `agentA`, `agentB`, `agentC`, etc.
|
|
100
|
+
- Logs are written to `.logs/iam.log` for debugging
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAijBlD,QAAA,MAAM,MAAM,EAAE,MAwTb,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,147 +2,122 @@
|
|
|
2
2
|
import { tool } from "@opencode-ai/plugin";
|
|
3
3
|
|
|
4
4
|
// prompt.ts
|
|
5
|
-
var
|
|
6
|
-
|
|
7
|
-
Actions:
|
|
8
|
-
- "announce": Announce what you're working on (do this first!). Shows all parallel agents. You can re-announce to update your status.
|
|
9
|
-
- "read": Read your messages (marks them as read)
|
|
10
|
-
- "broadcast": Send a message (requires 'message', optional 'to')`;
|
|
11
|
-
var ARG_DESCRIPTIONS = {
|
|
12
|
-
action: "Action to perform",
|
|
13
|
-
to: "Recipient(s): 'agentA', 'agentA,agentC', or 'all' (default: all)",
|
|
14
|
-
message: "Your announcement or message content"
|
|
15
|
-
};
|
|
16
|
-
function formatAgentList(agents) {
|
|
5
|
+
var BROADCAST_DESCRIPTION = `Communicate with other parallel agents. Use 'recipient' for specific agent(s), or omit to message all. Use 'reply_to' to mark messages as handled.`;
|
|
6
|
+
function broadcastResult(alias, recipients, parallelAgents, handledMessages) {
|
|
17
7
|
const lines = [];
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
8
|
+
lines.push(`You are: ${alias}`);
|
|
9
|
+
lines.push(``);
|
|
10
|
+
if (parallelAgents.length > 0) {
|
|
11
|
+
lines.push(`Available agents to message:`);
|
|
12
|
+
for (const agent of parallelAgents) {
|
|
13
|
+
if (agent.description) {
|
|
14
|
+
lines.push(` - ${agent.alias}: ${agent.description}`);
|
|
15
|
+
} else {
|
|
16
|
+
lines.push(` - ${agent.alias}`);
|
|
17
|
+
}
|
|
23
18
|
}
|
|
24
|
-
}
|
|
25
|
-
return lines;
|
|
26
|
-
}
|
|
27
|
-
function readResult(alias, messages, unreadCount, hasAnnounced) {
|
|
28
|
-
const lines = [`You are: ${alias}`, ``];
|
|
29
|
-
if (messages.length === 0) {
|
|
30
|
-
lines.push(`No messages in your inbox.`);
|
|
31
19
|
} else {
|
|
32
|
-
lines.push(`
|
|
33
|
-
for (const msg of messages) {
|
|
34
|
-
const time = new Date(msg.timestamp).toISOString();
|
|
35
|
-
const status = msg.read ? "" : " [NEW]";
|
|
36
|
-
lines.push(`[${time}] From: ${msg.from}${status}`);
|
|
37
|
-
lines.push(msg.body);
|
|
38
|
-
lines.push(`---`);
|
|
39
|
-
}
|
|
40
|
-
lines.push(``);
|
|
41
|
-
lines.push(`To reply: use action="broadcast" with to="<sender>" and message="..."`);
|
|
20
|
+
lines.push(`No other agents available yet.`);
|
|
42
21
|
}
|
|
43
|
-
if (
|
|
22
|
+
if (recipients.length > 0) {
|
|
44
23
|
lines.push(``);
|
|
45
|
-
|
|
24
|
+
const recipientStr = recipients.length === 1 ? recipients[0] : recipients.join(", ");
|
|
25
|
+
lines.push(`Message sent to: ${recipientStr}`);
|
|
46
26
|
}
|
|
47
|
-
|
|
48
|
-
`);
|
|
49
|
-
}
|
|
50
|
-
function announceResult(alias, parallelAgents) {
|
|
51
|
-
const lines = [
|
|
52
|
-
`Announced! Other agents will see your description when they call announce.`,
|
|
53
|
-
``,
|
|
54
|
-
`You are: ${alias}`
|
|
55
|
-
];
|
|
56
|
-
if (parallelAgents.length > 0) {
|
|
27
|
+
if (handledMessages.length > 0) {
|
|
57
28
|
lines.push(``);
|
|
58
|
-
lines.push(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
lines.push(``);
|
|
64
|
-
lines.push(`No other agents running yet.`);
|
|
29
|
+
lines.push(`Marked as handled: ${handledMessages.length} message(s)`);
|
|
30
|
+
for (const msg of handledMessages) {
|
|
31
|
+
const preview = msg.body.length > 80 ? msg.body.substring(0, 80) + "..." : msg.body;
|
|
32
|
+
lines.push(` #${msg.id} from ${msg.from}: "${preview}"`);
|
|
33
|
+
}
|
|
65
34
|
}
|
|
66
35
|
return lines.join(`
|
|
67
36
|
`);
|
|
68
37
|
}
|
|
69
|
-
var BROADCAST_MISSING_MESSAGE = `Error: 'message' parameter is required
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return `Error:
|
|
73
|
-
}
|
|
74
|
-
function broadcastResult(recipients, messageId) {
|
|
75
|
-
const recipientStr = recipients.length === 1 ? recipients[0] : recipients.join(", ");
|
|
76
|
-
return `Message sent!
|
|
77
|
-
|
|
78
|
-
To: ${recipientStr}
|
|
79
|
-
Message ID: ${messageId}
|
|
80
|
-
|
|
81
|
-
Recipients will be notified.`;
|
|
38
|
+
var BROADCAST_MISSING_MESSAGE = `Error: 'message' parameter is required.`;
|
|
39
|
+
var BROADCAST_SELF_MESSAGE = `Warning: You cannot send a message to yourself. The target alias is your own alias. Choose a different recipient.`;
|
|
40
|
+
function broadcastMessageTooLong(length, maxLength) {
|
|
41
|
+
return `Error: Message too long (${length} chars). Maximum allowed: ${maxLength} chars.`;
|
|
82
42
|
}
|
|
83
|
-
function
|
|
84
|
-
|
|
43
|
+
function broadcastUnknownRecipient(recipient, known) {
|
|
44
|
+
const list = known.length > 0 ? `Known agents: ${known.join(", ")}` : "No agents available yet.";
|
|
45
|
+
return `Error: Unknown recipient "${recipient}". ${list}`;
|
|
85
46
|
}
|
|
86
47
|
var SYSTEM_PROMPT = `
|
|
87
48
|
<instructions tool="iam">
|
|
88
49
|
# Inter-Agent Messaging
|
|
89
50
|
|
|
90
|
-
|
|
51
|
+
Use \`broadcast\` to communicate with other parallel agents.
|
|
91
52
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
- action="read" - Read your messages
|
|
95
|
-
- action="broadcast", message="..." - Message all agents
|
|
96
|
-
- action="broadcast", to="agentA", message="..." - Message specific agent(s)
|
|
53
|
+
## IMPORTANT: Broadcast Immediately on Start
|
|
54
|
+
Call \`broadcast(message="...")\` as your FIRST action to announce yourself and discover other agents. The tool result will show all available agents you can message.
|
|
97
55
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
56
|
+
## Sending Messages
|
|
57
|
+
- \`broadcast(message="...")\` → send to all agents
|
|
58
|
+
- \`broadcast(recipient="agentB", message="...")\` → send to specific agent
|
|
101
59
|
|
|
102
|
-
|
|
60
|
+
## Receiving Messages
|
|
61
|
+
Incoming messages appear as \`broadcast\` tool results with a \`messages\` array:
|
|
62
|
+
\`\`\`
|
|
63
|
+
{ messages: [{ id: 1, from: "agentA", body: "..." }, ...] }
|
|
64
|
+
\`\`\`
|
|
65
|
+
|
|
66
|
+
Use \`reply_to\` to mark messages as handled (they persist until you do):
|
|
67
|
+
- \`broadcast(recipient="agentA", reply_to=[1], message="...")\`
|
|
68
|
+
- \`broadcast(recipient="agentA", reply_to=[1, 2, 3], message="...")\`
|
|
103
69
|
</instructions>
|
|
104
70
|
`;
|
|
105
|
-
function urgentNotification(unreadCount) {
|
|
106
|
-
return `<system-reminder priority="critical">
|
|
107
|
-
URGENT: You have ${unreadCount} unread message(s) in your IAM inbox.
|
|
108
|
-
Use the iam tool with action="read" NOW to check your messages before continuing other work.
|
|
109
|
-
</system-reminder>`;
|
|
110
|
-
}
|
|
111
71
|
|
|
112
72
|
// logger.ts
|
|
113
73
|
import * as fs from "fs";
|
|
114
74
|
import * as path from "path";
|
|
115
|
-
var __dirname = "/home/spoon/.config/opencode/plugin/iam";
|
|
116
|
-
var IS_DEV = fs.existsSync(path.join(__dirname, "..", ".git"));
|
|
117
75
|
var LOG_DIR = path.join(process.cwd(), ".logs");
|
|
118
76
|
var LOG_FILE = path.join(LOG_DIR, "iam.log");
|
|
119
|
-
|
|
77
|
+
var WRITE_INTERVAL_MS = 100;
|
|
78
|
+
var logBuffer = [];
|
|
79
|
+
var writeScheduled = false;
|
|
80
|
+
try {
|
|
81
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
82
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
fs.writeFileSync(LOG_FILE, "");
|
|
85
|
+
} catch {}
|
|
86
|
+
function formatTimestamp() {
|
|
87
|
+
return new Date().toISOString();
|
|
88
|
+
}
|
|
89
|
+
async function flushLogs() {
|
|
90
|
+
if (logBuffer.length === 0) {
|
|
91
|
+
writeScheduled = false;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const toWrite = logBuffer.join("");
|
|
95
|
+
logBuffer = [];
|
|
96
|
+
writeScheduled = false;
|
|
120
97
|
try {
|
|
121
|
-
|
|
122
|
-
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
123
|
-
}
|
|
124
|
-
fs.writeFileSync(LOG_FILE, "");
|
|
98
|
+
await fs.promises.appendFile(LOG_FILE, toWrite);
|
|
125
99
|
} catch {}
|
|
126
100
|
}
|
|
127
|
-
function
|
|
128
|
-
|
|
101
|
+
function scheduleFlush() {
|
|
102
|
+
if (!writeScheduled) {
|
|
103
|
+
writeScheduled = true;
|
|
104
|
+
setTimeout(flushLogs, WRITE_INTERVAL_MS);
|
|
105
|
+
}
|
|
129
106
|
}
|
|
130
107
|
function writeLog(level, category, message, data) {
|
|
131
|
-
if (!IS_DEV)
|
|
132
|
-
return;
|
|
133
108
|
const timestamp = formatTimestamp();
|
|
134
109
|
const dataStr = data !== undefined ? ` | ${JSON.stringify(data)}` : "";
|
|
135
110
|
const logLine = `[${timestamp}] [${level}] [${category}] ${message}${dataStr}
|
|
136
111
|
`;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
} catch {}
|
|
112
|
+
logBuffer.push(logLine);
|
|
113
|
+
scheduleFlush();
|
|
140
114
|
}
|
|
141
115
|
var log = {
|
|
142
116
|
debug: (category, message, data) => writeLog("DEBUG", category, message, data),
|
|
143
117
|
info: (category, message, data) => writeLog("INFO", category, message, data),
|
|
144
118
|
warn: (category, message, data) => writeLog("WARN", category, message, data),
|
|
145
|
-
error: (category, message, data) => writeLog("ERROR", category, message, data)
|
|
119
|
+
error: (category, message, data) => writeLog("ERROR", category, message, data),
|
|
120
|
+
flush: flushLogs
|
|
146
121
|
};
|
|
147
122
|
var LOG = {
|
|
148
123
|
TOOL: "TOOL",
|
|
@@ -153,18 +128,76 @@ var LOG = {
|
|
|
153
128
|
};
|
|
154
129
|
|
|
155
130
|
// index.ts
|
|
131
|
+
var CHAR_CODE_A = 65;
|
|
132
|
+
var ALPHABET_SIZE = 26;
|
|
133
|
+
var MAX_DESCRIPTION_LENGTH = 100;
|
|
134
|
+
var MESSAGE_TTL_MS = 30 * 60 * 1000;
|
|
135
|
+
var UNHANDLED_TTL_MS = 2 * 60 * 60 * 1000;
|
|
136
|
+
var MAX_INBOX_SIZE = 100;
|
|
137
|
+
var PARENT_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
138
|
+
var CLEANUP_INTERVAL_MS = 60 * 1000;
|
|
139
|
+
var DEFAULT_MODEL_ID = "gpt-4o-2024-08-06";
|
|
140
|
+
var DEFAULT_PROVIDER_ID = "openai";
|
|
141
|
+
var MAX_MESSAGE_LENGTH = 1e4;
|
|
156
142
|
var inboxes = new Map;
|
|
143
|
+
var sessionMsgCounter = new Map;
|
|
157
144
|
var activeSessions = new Set;
|
|
158
145
|
var sessionToAlias = new Map;
|
|
159
146
|
var aliasToSession = new Map;
|
|
160
147
|
var agentDescriptions = new Map;
|
|
161
148
|
var nextAgentIndex = 0;
|
|
162
|
-
var
|
|
149
|
+
var registeringSessionsLock = new Set;
|
|
163
150
|
var sessionParentCache = new Map;
|
|
151
|
+
function cleanupExpiredMessages() {
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
let totalRemoved = 0;
|
|
154
|
+
for (const [sessionId, messages] of inboxes) {
|
|
155
|
+
const before = messages.length;
|
|
156
|
+
const filtered = messages.filter((m) => {
|
|
157
|
+
if (m.handled) {
|
|
158
|
+
return now - m.timestamp < MESSAGE_TTL_MS;
|
|
159
|
+
}
|
|
160
|
+
return now - m.timestamp < UNHANDLED_TTL_MS;
|
|
161
|
+
});
|
|
162
|
+
if (filtered.length > MAX_INBOX_SIZE) {
|
|
163
|
+
const unhandled = filtered.filter((m) => !m.handled);
|
|
164
|
+
const handled = filtered.filter((m) => m.handled);
|
|
165
|
+
handled.sort((a, b) => b.timestamp - a.timestamp);
|
|
166
|
+
if (unhandled.length > MAX_INBOX_SIZE) {
|
|
167
|
+
unhandled.sort((a, b) => b.timestamp - a.timestamp);
|
|
168
|
+
inboxes.set(sessionId, unhandled.slice(0, MAX_INBOX_SIZE));
|
|
169
|
+
totalRemoved += before - MAX_INBOX_SIZE;
|
|
170
|
+
} else {
|
|
171
|
+
const kept = [
|
|
172
|
+
...unhandled,
|
|
173
|
+
...handled.slice(0, MAX_INBOX_SIZE - unhandled.length)
|
|
174
|
+
];
|
|
175
|
+
inboxes.set(sessionId, kept);
|
|
176
|
+
totalRemoved += before - kept.length;
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
inboxes.set(sessionId, filtered);
|
|
180
|
+
totalRemoved += before - filtered.length;
|
|
181
|
+
}
|
|
182
|
+
if (inboxes.get(sessionId).length === 0) {
|
|
183
|
+
inboxes.delete(sessionId);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (const [sessionId, cached] of sessionParentCache) {
|
|
187
|
+
if (now - cached.cachedAt > PARENT_CACHE_TTL_MS) {
|
|
188
|
+
sessionParentCache.delete(sessionId);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (totalRemoved > 0) {
|
|
192
|
+
log.debug(LOG.MESSAGE, `Cleanup removed ${totalRemoved} expired messages`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
setInterval(cleanupExpiredMessages, CLEANUP_INTERVAL_MS);
|
|
164
196
|
function getNextAlias() {
|
|
165
|
-
const
|
|
166
|
-
const suffix = nextAgentIndex >= 26 ? Math.floor(nextAgentIndex / 26).toString() : "";
|
|
197
|
+
const index = nextAgentIndex;
|
|
167
198
|
nextAgentIndex++;
|
|
199
|
+
const letter = String.fromCharCode(CHAR_CODE_A + index % ALPHABET_SIZE);
|
|
200
|
+
const suffix = index >= ALPHABET_SIZE ? Math.floor(index / ALPHABET_SIZE).toString() : "";
|
|
168
201
|
return `agent${letter}${suffix}`;
|
|
169
202
|
}
|
|
170
203
|
function getAlias(sessionId) {
|
|
@@ -172,22 +205,28 @@ function getAlias(sessionId) {
|
|
|
172
205
|
}
|
|
173
206
|
function setDescription(sessionId, description) {
|
|
174
207
|
const alias = getAlias(sessionId);
|
|
175
|
-
|
|
176
|
-
|
|
208
|
+
const truncated = description.substring(0, MAX_DESCRIPTION_LENGTH);
|
|
209
|
+
agentDescriptions.set(alias, truncated);
|
|
210
|
+
log.info(LOG.SESSION, `Agent announced`, { alias, description: truncated });
|
|
177
211
|
}
|
|
178
212
|
function getDescription(alias) {
|
|
179
213
|
return agentDescriptions.get(alias);
|
|
180
214
|
}
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
function resolveAlias(aliasOrSessionId) {
|
|
215
|
+
function resolveAlias(aliasOrSessionId, parentId) {
|
|
216
|
+
if (aliasOrSessionId === "parent" && parentId) {
|
|
217
|
+
return parentId;
|
|
218
|
+
}
|
|
186
219
|
return aliasToSession.get(aliasOrSessionId) || (activeSessions.has(aliasOrSessionId) ? aliasOrSessionId : undefined);
|
|
187
220
|
}
|
|
188
221
|
function generateId() {
|
|
189
222
|
return Math.random().toString(36).substring(2, 10);
|
|
190
223
|
}
|
|
224
|
+
function getNextMsgIndex(sessionId) {
|
|
225
|
+
const current = sessionMsgCounter.get(sessionId) || 0;
|
|
226
|
+
const next = current + 1;
|
|
227
|
+
sessionMsgCounter.set(sessionId, next);
|
|
228
|
+
return next;
|
|
229
|
+
}
|
|
191
230
|
function getInbox(sessionId) {
|
|
192
231
|
if (!inboxes.has(sessionId)) {
|
|
193
232
|
inboxes.set(sessionId, []);
|
|
@@ -197,152 +236,326 @@ function getInbox(sessionId) {
|
|
|
197
236
|
function sendMessage(from, to, body) {
|
|
198
237
|
const message = {
|
|
199
238
|
id: generateId(),
|
|
239
|
+
msgIndex: getNextMsgIndex(to),
|
|
200
240
|
from,
|
|
201
241
|
to,
|
|
202
242
|
body,
|
|
203
243
|
timestamp: Date.now(),
|
|
204
|
-
|
|
244
|
+
handled: false
|
|
205
245
|
};
|
|
206
|
-
getInbox(to)
|
|
207
|
-
|
|
246
|
+
const queue = getInbox(to);
|
|
247
|
+
if (queue.length >= MAX_INBOX_SIZE) {
|
|
248
|
+
const handledIndex = queue.findIndex((m) => m.handled);
|
|
249
|
+
if (handledIndex !== -1) {
|
|
250
|
+
queue.splice(handledIndex, 1);
|
|
251
|
+
} else {
|
|
252
|
+
queue.shift();
|
|
253
|
+
}
|
|
254
|
+
log.warn(LOG.MESSAGE, `Queue full, removed oldest message`, { to });
|
|
255
|
+
}
|
|
256
|
+
queue.push(message);
|
|
257
|
+
log.info(LOG.MESSAGE, `Message sent`, {
|
|
258
|
+
id: message.id,
|
|
259
|
+
msgIndex: message.msgIndex,
|
|
260
|
+
from,
|
|
261
|
+
to,
|
|
262
|
+
bodyLength: body.length
|
|
263
|
+
});
|
|
208
264
|
return message;
|
|
209
265
|
}
|
|
210
|
-
function
|
|
211
|
-
return getInbox(sessionId).filter((m) => !m.
|
|
212
|
-
}
|
|
213
|
-
function getAllMessages(sessionId) {
|
|
214
|
-
return getInbox(sessionId);
|
|
266
|
+
function getUnhandledMessages(sessionId) {
|
|
267
|
+
return getInbox(sessionId).filter((m) => !m.handled);
|
|
215
268
|
}
|
|
216
|
-
function
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
for (const msg of
|
|
220
|
-
msg.
|
|
269
|
+
function markMessagesAsHandled(sessionId, msgIndices) {
|
|
270
|
+
const queue = getInbox(sessionId);
|
|
271
|
+
const handled = [];
|
|
272
|
+
for (const msg of queue) {
|
|
273
|
+
if (msgIndices.includes(msg.msgIndex) && !msg.handled) {
|
|
274
|
+
msg.handled = true;
|
|
275
|
+
handled.push({
|
|
276
|
+
id: msg.msgIndex,
|
|
277
|
+
from: msg.from,
|
|
278
|
+
body: msg.body
|
|
279
|
+
});
|
|
280
|
+
log.info(LOG.MESSAGE, `Message marked as handled`, {
|
|
281
|
+
sessionId,
|
|
282
|
+
msgIndex: msg.msgIndex,
|
|
283
|
+
from: msg.from
|
|
284
|
+
});
|
|
285
|
+
}
|
|
221
286
|
}
|
|
222
|
-
|
|
287
|
+
return handled;
|
|
223
288
|
}
|
|
224
|
-
function
|
|
289
|
+
function getKnownAliases(sessionId) {
|
|
290
|
+
const selfAlias = sessionToAlias.get(sessionId);
|
|
225
291
|
const agents = [];
|
|
226
|
-
for (const
|
|
227
|
-
if (
|
|
228
|
-
agents.push(
|
|
292
|
+
for (const alias of aliasToSession.keys()) {
|
|
293
|
+
if (alias !== selfAlias) {
|
|
294
|
+
agents.push(alias);
|
|
229
295
|
}
|
|
230
296
|
}
|
|
231
297
|
return agents;
|
|
232
298
|
}
|
|
233
299
|
function getParallelAgents(sessionId) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
300
|
+
const selfAlias = sessionToAlias.get(sessionId);
|
|
301
|
+
const agents = [];
|
|
302
|
+
for (const [alias, sessId] of aliasToSession.entries()) {
|
|
303
|
+
if (alias !== selfAlias) {
|
|
304
|
+
agents.push({
|
|
305
|
+
alias,
|
|
306
|
+
description: getDescription(alias)
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return agents;
|
|
238
311
|
}
|
|
239
312
|
function registerSession(sessionId) {
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
313
|
+
if (activeSessions.has(sessionId)) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (registeringSessionsLock.has(sessionId)) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
registeringSessionsLock.add(sessionId);
|
|
320
|
+
try {
|
|
321
|
+
if (!activeSessions.has(sessionId)) {
|
|
322
|
+
activeSessions.add(sessionId);
|
|
323
|
+
const alias = getNextAlias();
|
|
324
|
+
sessionToAlias.set(sessionId, alias);
|
|
325
|
+
aliasToSession.set(alias, sessionId);
|
|
326
|
+
log.info(LOG.SESSION, `Session registered`, {
|
|
327
|
+
sessionId,
|
|
328
|
+
alias,
|
|
329
|
+
totalSessions: activeSessions.size
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
} finally {
|
|
333
|
+
registeringSessionsLock.delete(sessionId);
|
|
246
334
|
}
|
|
247
335
|
}
|
|
248
336
|
async function getParentId(client, sessionId) {
|
|
249
|
-
|
|
250
|
-
|
|
337
|
+
const now = Date.now();
|
|
338
|
+
const cached = sessionParentCache.get(sessionId);
|
|
339
|
+
if (cached && now - cached.cachedAt < PARENT_CACHE_TTL_MS) {
|
|
340
|
+
return cached.value;
|
|
251
341
|
}
|
|
252
342
|
try {
|
|
253
343
|
const response = await client.session.get({ path: { id: sessionId } });
|
|
254
344
|
const parentId = response.data?.parentID || null;
|
|
255
|
-
sessionParentCache.set(sessionId, parentId);
|
|
345
|
+
sessionParentCache.set(sessionId, { value: parentId, cachedAt: now });
|
|
256
346
|
log.debug(LOG.SESSION, `Looked up parentID`, { sessionId, parentId });
|
|
257
347
|
return parentId;
|
|
258
348
|
} catch (e) {
|
|
259
|
-
log.warn(LOG.SESSION, `Failed to get session info`, {
|
|
260
|
-
|
|
349
|
+
log.warn(LOG.SESSION, `Failed to get session info`, {
|
|
350
|
+
sessionId,
|
|
351
|
+
error: String(e)
|
|
352
|
+
});
|
|
353
|
+
sessionParentCache.set(sessionId, {
|
|
354
|
+
value: null,
|
|
355
|
+
cachedAt: now - PARENT_CACHE_TTL_MS + 60000
|
|
356
|
+
});
|
|
261
357
|
return null;
|
|
262
358
|
}
|
|
263
359
|
}
|
|
360
|
+
function createInboxMessage(sessionId, messages, baseUserMessage) {
|
|
361
|
+
const now = Date.now();
|
|
362
|
+
const userInfo = baseUserMessage.info;
|
|
363
|
+
const inboxMsgs = messages.map((m) => ({
|
|
364
|
+
id: m.msgIndex,
|
|
365
|
+
from: m.from,
|
|
366
|
+
body: m.body
|
|
367
|
+
}));
|
|
368
|
+
const assistantMessageId = `msg_broadcast_${now}`;
|
|
369
|
+
const partId = `prt_broadcast_${now}`;
|
|
370
|
+
const callId = `call_broadcast_${now}`;
|
|
371
|
+
log.debug(LOG.MESSAGE, `Creating bundled inbox message`, {
|
|
372
|
+
sessionId,
|
|
373
|
+
messageCount: messages.length,
|
|
374
|
+
msgIndices: messages.map((m) => m.msgIndex)
|
|
375
|
+
});
|
|
376
|
+
const result = {
|
|
377
|
+
info: {
|
|
378
|
+
id: assistantMessageId,
|
|
379
|
+
sessionID: sessionId,
|
|
380
|
+
role: "assistant",
|
|
381
|
+
agent: userInfo.agent || "code",
|
|
382
|
+
parentID: userInfo.id,
|
|
383
|
+
modelID: userInfo.model?.modelID || DEFAULT_MODEL_ID,
|
|
384
|
+
providerID: userInfo.model?.providerID || DEFAULT_PROVIDER_ID,
|
|
385
|
+
mode: "default",
|
|
386
|
+
path: { cwd: "/", root: "/" },
|
|
387
|
+
time: { created: now, completed: now },
|
|
388
|
+
cost: 0,
|
|
389
|
+
tokens: {
|
|
390
|
+
input: 0,
|
|
391
|
+
output: 0,
|
|
392
|
+
reasoning: 0,
|
|
393
|
+
cache: { read: 0, write: 0 }
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
parts: [
|
|
397
|
+
{
|
|
398
|
+
id: partId,
|
|
399
|
+
sessionID: sessionId,
|
|
400
|
+
messageID: assistantMessageId,
|
|
401
|
+
type: "tool",
|
|
402
|
+
callID: callId,
|
|
403
|
+
tool: "broadcast",
|
|
404
|
+
state: {
|
|
405
|
+
status: "completed",
|
|
406
|
+
input: { messages: inboxMsgs },
|
|
407
|
+
output: `${messages.length} message(s)`,
|
|
408
|
+
title: `Inbox (${messages.length} messages)`,
|
|
409
|
+
metadata: {
|
|
410
|
+
incoming_message: true,
|
|
411
|
+
message_count: messages.length
|
|
412
|
+
},
|
|
413
|
+
time: { start: now, end: now }
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
]
|
|
417
|
+
};
|
|
418
|
+
if (userInfo.variant !== undefined) {
|
|
419
|
+
result.info.variant = userInfo.variant;
|
|
420
|
+
}
|
|
421
|
+
return result;
|
|
422
|
+
}
|
|
264
423
|
var plugin = async (ctx) => {
|
|
265
424
|
log.info(LOG.HOOK, "Plugin initialized");
|
|
266
425
|
const client = ctx.client;
|
|
267
426
|
return {
|
|
268
427
|
tool: {
|
|
269
|
-
|
|
270
|
-
description:
|
|
428
|
+
broadcast: tool({
|
|
429
|
+
description: BROADCAST_DESCRIPTION,
|
|
271
430
|
args: {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
431
|
+
recipient: tool.schema.string().optional().describe("Target agent(s), comma-separated. Omit to send to all."),
|
|
432
|
+
message: tool.schema.string().describe("Your message"),
|
|
433
|
+
reply_to: tool.schema.array(tool.schema.number()).optional().describe("Message IDs to mark as handled (e.g., [1, 2, 3])")
|
|
275
434
|
},
|
|
276
435
|
async execute(args, context) {
|
|
277
436
|
const sessionId = context.sessionID;
|
|
437
|
+
const isFirstCall = !activeSessions.has(sessionId);
|
|
278
438
|
registerSession(sessionId);
|
|
279
439
|
const alias = getAlias(sessionId);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
440
|
+
if (!args.message) {
|
|
441
|
+
log.warn(LOG.TOOL, `broadcast missing 'message'`, { alias });
|
|
442
|
+
return BROADCAST_MISSING_MESSAGE;
|
|
443
|
+
}
|
|
444
|
+
if (args.message.length > MAX_MESSAGE_LENGTH) {
|
|
445
|
+
log.warn(LOG.TOOL, `broadcast message too long`, {
|
|
446
|
+
alias,
|
|
447
|
+
length: args.message.length
|
|
448
|
+
});
|
|
449
|
+
return broadcastMessageTooLong(args.message.length, MAX_MESSAGE_LENGTH);
|
|
450
|
+
}
|
|
451
|
+
if (isFirstCall) {
|
|
452
|
+
setDescription(sessionId, args.message);
|
|
453
|
+
}
|
|
454
|
+
log.debug(LOG.TOOL, `broadcast called`, {
|
|
455
|
+
sessionId,
|
|
456
|
+
alias,
|
|
457
|
+
recipient: args.recipient,
|
|
458
|
+
reply_to: args.reply_to,
|
|
459
|
+
messageLength: args.message.length,
|
|
460
|
+
isFirstCall
|
|
461
|
+
});
|
|
462
|
+
let handledMessages = [];
|
|
463
|
+
if (args.reply_to && args.reply_to.length > 0) {
|
|
464
|
+
handledMessages = markMessagesAsHandled(sessionId, args.reply_to);
|
|
465
|
+
log.info(LOG.TOOL, `Handled messages via reply_to`, {
|
|
466
|
+
alias,
|
|
467
|
+
requested: args.reply_to,
|
|
468
|
+
actuallyHandled: handledMessages.length
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
const knownAgents = getKnownAliases(sessionId);
|
|
472
|
+
const parallelAgents = getParallelAgents(sessionId);
|
|
473
|
+
let targetAliases;
|
|
474
|
+
if (!args.recipient) {
|
|
475
|
+
targetAliases = knownAgents;
|
|
476
|
+
} else {
|
|
477
|
+
targetAliases = args.recipient.split(",").map((s) => s.trim()).filter(Boolean);
|
|
478
|
+
}
|
|
479
|
+
if (targetAliases.length === 0) {
|
|
480
|
+
log.info(LOG.TOOL, `No recipients, returning agent info`, {
|
|
481
|
+
alias
|
|
482
|
+
});
|
|
483
|
+
return broadcastResult(alias, [], parallelAgents, handledMessages);
|
|
484
|
+
}
|
|
485
|
+
const parentId = await getParentId(client, sessionId);
|
|
486
|
+
const recipientSessions = [];
|
|
487
|
+
const validTargets = [];
|
|
488
|
+
for (const targetAlias of targetAliases) {
|
|
489
|
+
const recipientSessionId = resolveAlias(targetAlias, parentId);
|
|
490
|
+
if (!recipientSessionId) {
|
|
491
|
+
log.warn(LOG.TOOL, `broadcast unknown recipient`, {
|
|
492
|
+
alias,
|
|
493
|
+
to: targetAlias
|
|
494
|
+
});
|
|
495
|
+
return broadcastUnknownRecipient(targetAlias, knownAgents);
|
|
289
496
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (!args.to || args.to.toLowerCase() === "all") {
|
|
298
|
-
targetAliases = knownAgents;
|
|
299
|
-
} else {
|
|
300
|
-
targetAliases = args.to.split(",").map((s) => s.trim()).filter(Boolean);
|
|
497
|
+
if (recipientSessionId === sessionId) {
|
|
498
|
+
log.warn(LOG.TOOL, `Skipping self-message`, {
|
|
499
|
+
alias,
|
|
500
|
+
targetAlias
|
|
501
|
+
});
|
|
502
|
+
if (targetAliases.length === 1) {
|
|
503
|
+
return BROADCAST_SELF_MESSAGE;
|
|
301
504
|
}
|
|
302
|
-
|
|
303
|
-
return `No agents to broadcast to. Use action="announce" to see parallel agents.`;
|
|
304
|
-
}
|
|
305
|
-
const recipientSessions = [];
|
|
306
|
-
for (const targetAlias of targetAliases) {
|
|
307
|
-
const recipientSessionId = resolveAlias(targetAlias);
|
|
308
|
-
if (!recipientSessionId) {
|
|
309
|
-
log.warn(LOG.TOOL, `broadcast unknown recipient`, { alias, to: targetAlias });
|
|
310
|
-
return broadcastUnknownRecipient(targetAlias, knownAgents);
|
|
311
|
-
}
|
|
312
|
-
recipientSessions.push(recipientSessionId);
|
|
313
|
-
}
|
|
314
|
-
let messageId = "";
|
|
315
|
-
for (const recipientSessionId of recipientSessions) {
|
|
316
|
-
const msg = sendMessage(alias, recipientSessionId, args.message);
|
|
317
|
-
messageId = msg.id;
|
|
318
|
-
}
|
|
319
|
-
return broadcastResult(targetAliases, messageId);
|
|
505
|
+
continue;
|
|
320
506
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
507
|
+
recipientSessions.push(recipientSessionId);
|
|
508
|
+
validTargets.push(targetAlias);
|
|
509
|
+
}
|
|
510
|
+
if (recipientSessions.length === 0) {
|
|
511
|
+
log.info(LOG.TOOL, `No valid recipients after filtering`, {
|
|
512
|
+
alias
|
|
513
|
+
});
|
|
514
|
+
return broadcastResult(alias, [], parallelAgents, handledMessages);
|
|
515
|
+
}
|
|
516
|
+
const isTargetingParent = parentId && recipientSessions.includes(parentId);
|
|
517
|
+
for (const recipientSessionId of recipientSessions) {
|
|
518
|
+
sendMessage(alias, recipientSessionId, args.message);
|
|
519
|
+
}
|
|
520
|
+
if (isTargetingParent) {
|
|
521
|
+
log.info(LOG.MESSAGE, `Broadcasting to parent session, calling notify_once`, { sessionId, parentId });
|
|
522
|
+
try {
|
|
523
|
+
const internalClient = client._client;
|
|
524
|
+
if (internalClient?.post) {
|
|
525
|
+
await internalClient.post({
|
|
526
|
+
url: `/session/${parentId}/notify_once`,
|
|
527
|
+
body: {
|
|
528
|
+
text: `[IAM] Message from ${alias}: ${args.message}`
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
log.info(LOG.MESSAGE, `Parent session notified successfully`, {
|
|
532
|
+
parentId
|
|
533
|
+
});
|
|
325
534
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
535
|
+
} catch (e) {
|
|
536
|
+
log.warn(LOG.MESSAGE, `Failed to notify parent session`, {
|
|
537
|
+
parentId,
|
|
538
|
+
error: String(e)
|
|
539
|
+
});
|
|
329
540
|
}
|
|
330
|
-
default:
|
|
331
|
-
return unknownAction(args.action);
|
|
332
541
|
}
|
|
542
|
+
return broadcastResult(alias, validTargets, parallelAgents, handledMessages);
|
|
333
543
|
}
|
|
334
544
|
})
|
|
335
545
|
},
|
|
336
546
|
"tool.execute.after": async (input, output) => {
|
|
337
|
-
log.debug(LOG.HOOK, `tool.execute.after fired`, {
|
|
547
|
+
log.debug(LOG.HOOK, `tool.execute.after fired`, {
|
|
548
|
+
tool: input.tool,
|
|
549
|
+
sessionID: input.sessionID,
|
|
550
|
+
hasMetadata: !!output.metadata
|
|
551
|
+
});
|
|
338
552
|
if (input.tool === "task") {
|
|
339
|
-
log.debug(LOG.HOOK, `task metadata`, { metadata: output.metadata, output: output.output?.substring?.(0, 200) });
|
|
340
553
|
const newSessionId = output.metadata?.sessionId || output.metadata?.session_id;
|
|
341
554
|
if (newSessionId) {
|
|
342
|
-
log.info(LOG.HOOK, `task completed,
|
|
555
|
+
log.info(LOG.HOOK, `task completed, ensuring session registered`, {
|
|
556
|
+
newSessionId
|
|
557
|
+
});
|
|
343
558
|
registerSession(newSessionId);
|
|
344
|
-
} else {
|
|
345
|
-
log.warn(LOG.HOOK, `task completed but no session_id in metadata`);
|
|
346
559
|
}
|
|
347
560
|
}
|
|
348
561
|
},
|
|
@@ -352,43 +565,63 @@ var plugin = async (ctx) => {
|
|
|
352
565
|
log.debug(LOG.INJECT, `No sessionID in system.transform input, skipping`);
|
|
353
566
|
return;
|
|
354
567
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
568
|
+
try {
|
|
569
|
+
const result = await client.session.get({ path: { id: sessionId } });
|
|
570
|
+
if (!result.data?.parentID) {
|
|
571
|
+
log.debug(LOG.INJECT, `Session has no parentID (main session), skipping IAM`, {
|
|
572
|
+
sessionId
|
|
573
|
+
});
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
} catch (e) {
|
|
577
|
+
log.debug(LOG.INJECT, `Failed to get session info, skipping IAM`, {
|
|
578
|
+
sessionId,
|
|
579
|
+
error: String(e)
|
|
580
|
+
});
|
|
358
581
|
return;
|
|
359
582
|
}
|
|
583
|
+
registerSession(sessionId);
|
|
360
584
|
output.system.push(SYSTEM_PROMPT);
|
|
361
|
-
log.info(LOG.INJECT, `
|
|
585
|
+
log.info(LOG.INJECT, `Registered session and injected IAM system prompt`, {
|
|
586
|
+
sessionId,
|
|
587
|
+
alias: getAlias(sessionId)
|
|
588
|
+
});
|
|
362
589
|
},
|
|
363
590
|
"experimental.chat.messages.transform": async (_input, output) => {
|
|
364
591
|
const lastUserMsg = [...output.messages].reverse().find((m) => m.info.role === "user");
|
|
365
|
-
if (!lastUserMsg)
|
|
592
|
+
if (!lastUserMsg) {
|
|
593
|
+
log.debug(LOG.INJECT, `No user message found in transform, skipping IAM injection`);
|
|
366
594
|
return;
|
|
595
|
+
}
|
|
367
596
|
const sessionId = lastUserMsg.info.sessionID;
|
|
368
|
-
const
|
|
369
|
-
|
|
597
|
+
const unhandled = getUnhandledMessages(sessionId);
|
|
598
|
+
log.debug(LOG.INJECT, `Checking for messages in transform`, {
|
|
599
|
+
sessionId,
|
|
600
|
+
unhandledCount: unhandled.length
|
|
601
|
+
});
|
|
602
|
+
if (unhandled.length === 0) {
|
|
370
603
|
return;
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
]
|
|
604
|
+
}
|
|
605
|
+
log.info(LOG.INJECT, `Injecting bundled inbox message`, {
|
|
606
|
+
sessionId,
|
|
607
|
+
unhandledCount: unhandled.length,
|
|
608
|
+
msgIndices: unhandled.map((m) => m.msgIndex)
|
|
609
|
+
});
|
|
610
|
+
const inboxMsg = createInboxMessage(sessionId, unhandled, lastUserMsg);
|
|
611
|
+
output.messages.push(inboxMsg);
|
|
612
|
+
log.info(LOG.INJECT, `Injected inbox at end of message chain`, {
|
|
613
|
+
sessionId,
|
|
614
|
+
messageCount: unhandled.length
|
|
615
|
+
});
|
|
616
|
+
},
|
|
617
|
+
"experimental.config.transform": async (_input, output) => {
|
|
618
|
+
const experimental = output.experimental ?? {};
|
|
619
|
+
const existingSubagentTools = experimental.subagent_tools ?? [];
|
|
620
|
+
output.experimental = {
|
|
621
|
+
...experimental,
|
|
622
|
+
subagent_tools: [...existingSubagentTools, "broadcast"]
|
|
390
623
|
};
|
|
391
|
-
|
|
624
|
+
log.info(LOG.HOOK, `Added 'broadcast' to experimental.subagent_tools`);
|
|
392
625
|
}
|
|
393
626
|
};
|
|
394
627
|
};
|
package/dist/logger.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flush buffered logs to file asynchronously
|
|
3
|
+
*/
|
|
4
|
+
declare function flushLogs(): Promise<void>;
|
|
1
5
|
export declare const log: {
|
|
2
6
|
debug: (category: string, message: string, data?: unknown) => void;
|
|
3
7
|
info: (category: string, message: string, data?: unknown) => void;
|
|
4
8
|
warn: (category: string, message: string, data?: unknown) => void;
|
|
5
9
|
error: (category: string, message: string, data?: unknown) => void;
|
|
10
|
+
/** Force immediate flush (useful before process exit) */
|
|
11
|
+
flush: typeof flushLogs;
|
|
6
12
|
};
|
|
7
13
|
export declare const LOG: {
|
|
8
14
|
readonly TOOL: "TOOL";
|
|
@@ -11,4 +17,5 @@ export declare const LOG: {
|
|
|
11
17
|
readonly HOOK: "HOOK";
|
|
12
18
|
readonly INJECT: "INJECT";
|
|
13
19
|
};
|
|
20
|
+
export {};
|
|
14
21
|
//# sourceMappingURL=logger.d.ts.map
|
package/dist/logger.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../logger.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../logger.ts"],"names":[],"mappings":"AAkCA;;GAEG;AACH,iBAAe,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAexC;AA0BD,eAAO,MAAM,GAAG;sBACI,MAAM,WAAW,MAAM,SAAS,OAAO;qBAGxC,MAAM,WAAW,MAAM,SAAS,OAAO;qBAGvC,MAAM,WAAW,MAAM,SAAS,OAAO;sBAGtC,MAAM,WAAW,MAAM,SAAS,OAAO;IAGzD,yDAAyD;;CAE1D,CAAC;AAGF,eAAO,MAAM,GAAG;;;;;;CAMN,CAAC"}
|
package/dist/prompt.d.ts
CHANGED
|
@@ -1,25 +1,17 @@
|
|
|
1
|
-
export declare const
|
|
2
|
-
export declare const ARG_DESCRIPTIONS: {
|
|
3
|
-
readonly action: "Action to perform";
|
|
4
|
-
readonly to: "Recipient(s): 'agentA', 'agentA,agentC', or 'all' (default: all)";
|
|
5
|
-
readonly message: "Your announcement or message content";
|
|
6
|
-
};
|
|
1
|
+
export declare const BROADCAST_DESCRIPTION = "Communicate with other parallel agents. Use 'recipient' for specific agent(s), or omit to message all. Use 'reply_to' to mark messages as handled.";
|
|
7
2
|
export interface ParallelAgent {
|
|
8
3
|
alias: string;
|
|
9
4
|
description?: string;
|
|
10
5
|
}
|
|
11
|
-
export
|
|
12
|
-
|
|
6
|
+
export interface HandledMessage {
|
|
7
|
+
id: number;
|
|
13
8
|
from: string;
|
|
14
9
|
body: string;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export declare
|
|
19
|
-
export declare
|
|
20
|
-
export declare function broadcastUnknownRecipient(
|
|
21
|
-
export declare
|
|
22
|
-
export declare function unknownAction(action: string): string;
|
|
23
|
-
export declare const SYSTEM_PROMPT = "\n<instructions tool=\"iam\">\n# Inter-Agent Messaging\n\nYou have access to an `iam` tool for communicating with other parallel agents.\n\nUsage:\n- action=\"announce\", message=\"...\" - Announce what you're working on (do this first!)\n- action=\"read\" - Read your messages\n- action=\"broadcast\", message=\"...\" - Message all agents\n- action=\"broadcast\", to=\"agentA\", message=\"...\" - Message specific agent(s)\n\nAt the start of your task, use announce to let other agents know what you're doing.\nYou can re-announce to update your status as your task evolves.\nCheck your inbox when notified about new messages.\n\nWhen you complete your task, broadcast to all: \"Done. Here's what I found/did: ...\"\n</instructions>\n";
|
|
24
|
-
export declare function urgentNotification(unreadCount: number): string;
|
|
10
|
+
}
|
|
11
|
+
export declare function broadcastResult(alias: string, recipients: string[], parallelAgents: ParallelAgent[], handledMessages: HandledMessage[]): string;
|
|
12
|
+
export declare const BROADCAST_MISSING_MESSAGE = "Error: 'message' parameter is required.";
|
|
13
|
+
export declare const BROADCAST_SELF_MESSAGE = "Warning: You cannot send a message to yourself. The target alias is your own alias. Choose a different recipient.";
|
|
14
|
+
export declare function broadcastMessageTooLong(length: number, maxLength: number): string;
|
|
15
|
+
export declare function broadcastUnknownRecipient(recipient: string, known: string[]): string;
|
|
16
|
+
export declare const SYSTEM_PROMPT = "\n<instructions tool=\"iam\">\n# Inter-Agent Messaging\n\nUse `broadcast` to communicate with other parallel agents.\n\n## IMPORTANT: Broadcast Immediately on Start\nCall `broadcast(message=\"...\")` as your FIRST action to announce yourself and discover other agents. The tool result will show all available agents you can message.\n\n## Sending Messages\n- `broadcast(message=\"...\")` \u2192 send to all agents\n- `broadcast(recipient=\"agentB\", message=\"...\")` \u2192 send to specific agent\n\n## Receiving Messages\nIncoming messages appear as `broadcast` tool results with a `messages` array:\n```\n{ messages: [{ id: 1, from: \"agentA\", body: \"...\" }, ...] }\n```\n\nUse `reply_to` to mark messages as handled (they persist until you do):\n- `broadcast(recipient=\"agentA\", reply_to=[1], message=\"...\")`\n- `broadcast(recipient=\"agentA\", reply_to=[1, 2, 3], message=\"...\")`\n</instructions>\n";
|
|
25
17
|
//# sourceMappingURL=prompt.d.ts.map
|
package/dist/prompt.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../prompt.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,
|
|
1
|
+
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../prompt.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,qBAAqB,uJAAuJ,CAAC;AAM1L,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAMD,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,EAAE,EACpB,cAAc,EAAE,aAAa,EAAE,EAC/B,eAAe,EAAE,cAAc,EAAE,GAChC,MAAM,CAyCR;AAED,eAAO,MAAM,yBAAyB,4CAA4C,CAAC;AAEnF,eAAO,MAAM,sBAAsB,sHAAsH,CAAC;AAE1J,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,MAAM,CAER;AAED,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EAAE,GACd,MAAM,CAMR;AAMD,eAAO,MAAM,aAAa,q5BAuBzB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spoons-and-mirrors/iam",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Inter-agent messaging for OpenCode parallel subagents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,12 +29,13 @@
|
|
|
29
29
|
],
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@opencode-ai/plugin": "
|
|
32
|
+
"@opencode-ai/plugin": "^1.0.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@opencode-ai/plugin": "^1.0.143",
|
|
36
36
|
"@types/node": "^24.10.1",
|
|
37
37
|
"bun-types": "^1.3.5",
|
|
38
|
+
"prettier": "^3.7.4",
|
|
38
39
|
"typescript": "^5.9.3"
|
|
39
40
|
}
|
|
40
41
|
}
|