@yanlinglabs/winter-agent-sdk 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/index-51ysrfm8.js +614 -0
- package/dist/index-9e98bg1r.js +707 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +292 -741
- package/dist/messaging/adapter.d.ts +119 -0
- package/dist/messaging/addressing.d.ts +13 -0
- package/dist/messaging/attribution.d.ts +43 -0
- package/dist/messaging/idle.d.ts +54 -0
- package/dist/messaging/inbound.d.ts +39 -0
- package/dist/messaging/index.d.ts +15 -0
- package/dist/messaging/index.js +118 -0
- package/dist/messaging/outcomes.d.ts +25 -0
- package/dist/messaging/resolution.d.ts +39 -0
- package/dist/messaging/router.d.ts +100 -0
- package/dist/paths/project-key.d.ts +2 -0
- package/dist/protocol/messaging.d.ts +111 -0
- package/dist/query.d.ts +91 -0
- package/dist/tools/accept.d.ts +39 -0
- package/dist/tools/advisor.d.ts +106 -0
- package/dist/tools/definitions.d.ts +31 -0
- package/dist/tools/index.d.ts +14 -0
- package/dist/tools/index.js +438 -0
- package/dist/tools/messaging-handlers.d.ts +52 -0
- package/dist/tools/port.d.ts +39 -0
- package/dist/tools/schemas.d.ts +48 -0
- package/dist/version.d.ts +1 -0
- package/package.json +13 -5
package/README.md
CHANGED
|
@@ -38,6 +38,36 @@ files, `README.md` and `LICENSE`. It does **not** ship `src/`: the TypeScript so
|
|
|
38
38
|
<https://github.com/yanlingLabs/winter-agent-sdk>, which is where to read them, file an issue, or send
|
|
39
39
|
a patch.
|
|
40
40
|
|
|
41
|
+
## Subpaths
|
|
42
|
+
|
|
43
|
+
The main entry is the wrapper surface above. Two subpaths ship beside it, for hosts that compose
|
|
44
|
+
Winter rather than only call it:
|
|
45
|
+
|
|
46
|
+
**`@yanlinglabs/winter-agent-sdk/messaging`** (since 0.0.2) — the cross-runtime messaging contract
|
|
47
|
+
and its router core: addresses, listings, delivery outcomes, the adapter interface, inbound policy,
|
|
48
|
+
the `notify_when_idle` subscription store, and the three orchestration functions a host drives them
|
|
49
|
+
with. It ships no adapter and no process-level singleton; those are host composition.
|
|
50
|
+
|
|
51
|
+
**`@yanlinglabs/winter-agent-sdk/tools`** (since 0.0.3) — Winter's default tools, declared and
|
|
52
|
+
implemented once:
|
|
53
|
+
|
|
54
|
+
- `WINTER_DEFAULT_TOOL_DEFINITIONS` — `send_message`, `list_agents`, `read_notifications` and
|
|
55
|
+
`advisor`, each a `WinterToolDefinition` carrying a BARE `toolName`, the official built-in alias
|
|
56
|
+
key in `builtinName`, the description, the schemas and a permission class. No registry policy
|
|
57
|
+
fields: a host supplies its own.
|
|
58
|
+
- the native schemas and their bounds (`NATIVE_SEND_MESSAGE_SCHEMA`, `SEND_MESSAGE_TO_MAX` and the
|
|
59
|
+
rest), plus strict acceptors — unknown arguments are refused, `summary` is truncated rather than
|
|
60
|
+
rejected, and a refusal is returned as data the model can correct from, never thrown.
|
|
61
|
+
- `MessagingToolPort` + `messagingToolPortFromRuntimeDeps`, and `createMessagingToolHandlers`, which
|
|
62
|
+
turn a messaging world into the three tool handlers over it.
|
|
63
|
+
- `createAdvisorToolHandler` and `transcriptSourceForSessionKey`, with the reviewer left to the host
|
|
64
|
+
to resolve.
|
|
65
|
+
|
|
66
|
+
A host BINDS these under its own names: the Winter runtime registers them as its native built-ins
|
|
67
|
+
plus two canonical standing-server twins, and `@yanlinglabs/winter-runtime-sdk` binds the same
|
|
68
|
+
definitions under Claude's built-in names. Handlers return `{ text, isError? }` for each host to
|
|
69
|
+
wrap in its own result shape.
|
|
70
|
+
|
|
41
71
|
## License
|
|
42
72
|
|
|
43
73
|
MIT — see [`LICENSE`](./LICENSE), which ships in the published tarball.
|
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
// src/messaging/adapter.ts
|
|
2
|
+
function serializeRuntimeAddress2(addr) {
|
|
3
|
+
if (addr.objectKind === "session")
|
|
4
|
+
return `session:${addr.winterSessionId}`;
|
|
5
|
+
if (addr.childId === undefined) {
|
|
6
|
+
throw new Error("serializeRuntimeAddress: objectKind 'agent' requires childId");
|
|
7
|
+
}
|
|
8
|
+
const parent = addr.parentWinterSessionId ?? addr.winterSessionId;
|
|
9
|
+
return `agent:${parent}:${addr.childId}`;
|
|
10
|
+
}
|
|
11
|
+
function createFakeMessagingRouterSeam2() {
|
|
12
|
+
const outcomes = new Map;
|
|
13
|
+
const idsBySenderAndTool = new Map;
|
|
14
|
+
let children = [];
|
|
15
|
+
let counter = 0;
|
|
16
|
+
return {
|
|
17
|
+
outcomes,
|
|
18
|
+
setChildren(next) {
|
|
19
|
+
children = next;
|
|
20
|
+
},
|
|
21
|
+
allocateMessageId(senderSessionId, toolUseId) {
|
|
22
|
+
const key = `${senderSessionId}:${toolUseId}`;
|
|
23
|
+
const existing = idsBySenderAndTool.get(key);
|
|
24
|
+
if (existing !== undefined)
|
|
25
|
+
return existing;
|
|
26
|
+
const id = `msg-${++counter}`;
|
|
27
|
+
idsBySenderAndTool.set(key, id);
|
|
28
|
+
return id;
|
|
29
|
+
},
|
|
30
|
+
recordOutcome(messageId, outcome) {
|
|
31
|
+
outcomes.set(messageId, outcome);
|
|
32
|
+
},
|
|
33
|
+
lookupOutcome(messageId) {
|
|
34
|
+
return outcomes.get(messageId);
|
|
35
|
+
},
|
|
36
|
+
children() {
|
|
37
|
+
return children;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// src/messaging/addressing.ts
|
|
42
|
+
var REFERENCE_RUNTIME_KIND2 = "winter-agent";
|
|
43
|
+
var MAX_TO_LENGTH = 300;
|
|
44
|
+
function validateToField2(to) {
|
|
45
|
+
if (typeof to !== "string" || to.length === 0)
|
|
46
|
+
return { ok: false, message: "to must be a non-empty string" };
|
|
47
|
+
if (to.length > MAX_TO_LENGTH)
|
|
48
|
+
return { ok: false, message: `to must be at most ${MAX_TO_LENGTH} characters (got ${to.length})` };
|
|
49
|
+
if (to.includes(`
|
|
50
|
+
`) || to.includes("\r"))
|
|
51
|
+
return { ok: false, message: "to must not contain a newline" };
|
|
52
|
+
if (to.includes("*"))
|
|
53
|
+
return { ok: false, message: 'to must not contain "*" ("*" broadcast is forbidden, WS-10 §10.1)' };
|
|
54
|
+
return { ok: true };
|
|
55
|
+
}
|
|
56
|
+
function buildSessionAddress2(winterSessionId) {
|
|
57
|
+
return { objectKind: "session", runtimeKind: REFERENCE_RUNTIME_KIND2, winterSessionId };
|
|
58
|
+
}
|
|
59
|
+
function buildChildAddress2(parentWinterSessionId, childId) {
|
|
60
|
+
return { objectKind: "agent", runtimeKind: REFERENCE_RUNTIME_KIND2, winterSessionId: parentWinterSessionId, parentWinterSessionId, childId };
|
|
61
|
+
}
|
|
62
|
+
function parseRuntimeAddress2(serialized) {
|
|
63
|
+
if (serialized.startsWith("session:")) {
|
|
64
|
+
const id = serialized.slice("session:".length);
|
|
65
|
+
if (id.length === 0)
|
|
66
|
+
return;
|
|
67
|
+
return buildSessionAddress2(id);
|
|
68
|
+
}
|
|
69
|
+
if (serialized.startsWith("agent:")) {
|
|
70
|
+
const rest = serialized.slice("agent:".length);
|
|
71
|
+
const sep = rest.indexOf(":");
|
|
72
|
+
if (sep <= 0 || sep === rest.length - 1)
|
|
73
|
+
return;
|
|
74
|
+
const parent = rest.slice(0, sep);
|
|
75
|
+
const childId = rest.slice(sep + 1);
|
|
76
|
+
return buildChildAddress2(parent, childId);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
function sameAddress2(a, b) {
|
|
81
|
+
try {
|
|
82
|
+
return serializeRuntimeAddress2(a) === serializeRuntimeAddress2(b);
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// src/messaging/outcomes.ts
|
|
88
|
+
var MAX_GLOBAL_MESSAGE_SIZE2 = 1e6;
|
|
89
|
+
var DEFAULT_MESSAGE_TTL_MS2 = 24 * 60 * 60 * 1000;
|
|
90
|
+
var MAX_HOP_COUNT2 = 10;
|
|
91
|
+
var RAPID_REPEAT_WINDOW_MS2 = 5000;
|
|
92
|
+
var HELD_INBOX_CAP2 = 100;
|
|
93
|
+
var ACCEPTED_QUEUE_CAP2 = 50;
|
|
94
|
+
var DEFAULT_HOLD_EXPIRY_MS2 = 5 * 60 * 1000;
|
|
95
|
+
var NOTIFY_IDLE_EXPIRY_MS2 = 12 * 60 * 60 * 1000;
|
|
96
|
+
function messageExceedsMaxSize2(body) {
|
|
97
|
+
return body.length > MAX_GLOBAL_MESSAGE_SIZE2;
|
|
98
|
+
}
|
|
99
|
+
function hopCountExceeded2(hopCount) {
|
|
100
|
+
return hopCount >= MAX_HOP_COUNT2;
|
|
101
|
+
}
|
|
102
|
+
function delivered2(messageId) {
|
|
103
|
+
return { status: "delivered", messageId };
|
|
104
|
+
}
|
|
105
|
+
function queued2(messageId) {
|
|
106
|
+
return { status: "queued", messageId };
|
|
107
|
+
}
|
|
108
|
+
function resumedAndDelivered2(messageId) {
|
|
109
|
+
return { status: "resumed_and_delivered", messageId };
|
|
110
|
+
}
|
|
111
|
+
function held2(messageId, reason) {
|
|
112
|
+
return { status: "held", messageId, reason };
|
|
113
|
+
}
|
|
114
|
+
function subscribed2(messageId) {
|
|
115
|
+
return { status: "subscribed", messageId };
|
|
116
|
+
}
|
|
117
|
+
function deliveryUncertain2(messageId, reason) {
|
|
118
|
+
return { status: "delivery_uncertain", messageId, deliveryMayHaveOccurred: true, reason };
|
|
119
|
+
}
|
|
120
|
+
function refused2(messageId, reason) {
|
|
121
|
+
return { status: "refused", messageId, reason };
|
|
122
|
+
}
|
|
123
|
+
function ambiguous2(messageId, candidates) {
|
|
124
|
+
return { status: "ambiguous", messageId, candidates };
|
|
125
|
+
}
|
|
126
|
+
function notFound2(messageId, reason) {
|
|
127
|
+
return { status: "not_found", messageId, reason };
|
|
128
|
+
}
|
|
129
|
+
function unavailable2(messageId, retryable, reason) {
|
|
130
|
+
return { status: "unavailable", messageId, retryable, reason };
|
|
131
|
+
}
|
|
132
|
+
function createLoopGuard2() {
|
|
133
|
+
const lastSeen = new Map;
|
|
134
|
+
return {
|
|
135
|
+
check(from, to, body, now) {
|
|
136
|
+
const key = JSON.stringify([from, to, body]);
|
|
137
|
+
const prev = lastSeen.get(key);
|
|
138
|
+
lastSeen.set(key, now);
|
|
139
|
+
if (prev !== undefined && now - prev < RAPID_REPEAT_WINDOW_MS2)
|
|
140
|
+
return "duplicate";
|
|
141
|
+
return "ok";
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// src/messaging/resolution.ts
|
|
146
|
+
function childToListedRuntimeObject2(parentSessionId, child) {
|
|
147
|
+
const running = child.status() === "running";
|
|
148
|
+
return {
|
|
149
|
+
address: serializeRuntimeAddress2(buildChildAddress2(parentSessionId, child.record.id)),
|
|
150
|
+
...child.record.name !== undefined ? { name: child.record.name } : {},
|
|
151
|
+
objectKind: "agent",
|
|
152
|
+
runtimeKind: "winter-agent",
|
|
153
|
+
status: running ? "running" : "exited",
|
|
154
|
+
mode: child.record.permission.effectiveMode,
|
|
155
|
+
capabilities: {
|
|
156
|
+
message: running,
|
|
157
|
+
resume: !running,
|
|
158
|
+
notifyWhenIdle: false,
|
|
159
|
+
reply: running
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function resolveTarget2(input) {
|
|
164
|
+
const ownChildren = input.children.filter((c) => c.record.parentSessionId === input.callerParentSessionId);
|
|
165
|
+
const parsed = parseRuntimeAddress2(input.to);
|
|
166
|
+
if (parsed !== undefined) {
|
|
167
|
+
if (parsed.objectKind === "agent") {
|
|
168
|
+
const owningParent = parsed.parentWinterSessionId ?? parsed.winterSessionId;
|
|
169
|
+
if (owningParent !== input.callerParentSessionId) {
|
|
170
|
+
return { kind: "not_found", message: `"${input.to}" is not reachable from this session (a child agent is only addressable within its owning parent)` };
|
|
171
|
+
}
|
|
172
|
+
const child = ownChildren.find((c) => c.record.id === parsed.childId);
|
|
173
|
+
if (child === undefined)
|
|
174
|
+
return { kind: "not_found", message: `no child at canonical address "${input.to}"` };
|
|
175
|
+
return { kind: "resolved", address: parsed, child };
|
|
176
|
+
}
|
|
177
|
+
const peer = input.peers.find((p) => p.address === input.to);
|
|
178
|
+
if (peer === undefined)
|
|
179
|
+
return { kind: "not_found", message: `no live session at canonical address "${input.to}"` };
|
|
180
|
+
return { kind: "resolved", address: { ...parsed, runtimeKind: peer.runtimeKind } };
|
|
181
|
+
}
|
|
182
|
+
const byId = ownChildren.find((c) => c.record.id === input.to);
|
|
183
|
+
if (byId !== undefined) {
|
|
184
|
+
return { kind: "resolved", address: buildChildAddress2(input.callerParentSessionId, byId.record.id), child: byId };
|
|
185
|
+
}
|
|
186
|
+
const childrenNamed = ownChildren.filter((c) => c.record.name === input.to);
|
|
187
|
+
const distinctChildIds = new Set(childrenNamed.map((c) => c.record.id));
|
|
188
|
+
if (distinctChildIds.size > 1) {
|
|
189
|
+
return {
|
|
190
|
+
kind: "stale",
|
|
191
|
+
message: `"${input.to}" has been used by more than one agent in this conversation; address the one you mean by its canonical address from ListAgents`
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const peersNamed = input.peers.filter((p) => p.name === input.to);
|
|
195
|
+
const uniqueChild = distinctChildIds.size === 1 ? childrenNamed[0] : undefined;
|
|
196
|
+
const totalCandidates = (uniqueChild !== undefined ? 1 : 0) + peersNamed.length;
|
|
197
|
+
if (totalCandidates === 0) {
|
|
198
|
+
return { kind: "not_found", message: `no agent or session named "${input.to}" is currently reachable` };
|
|
199
|
+
}
|
|
200
|
+
if (totalCandidates > 1) {
|
|
201
|
+
const candidates = [...uniqueChild !== undefined ? [childToListedRuntimeObject2(input.callerParentSessionId, uniqueChild)] : [], ...peersNamed];
|
|
202
|
+
return { kind: "ambiguous", candidates };
|
|
203
|
+
}
|
|
204
|
+
if (uniqueChild !== undefined) {
|
|
205
|
+
return { kind: "resolved", address: buildChildAddress2(input.callerParentSessionId, uniqueChild.record.id), child: uniqueChild };
|
|
206
|
+
}
|
|
207
|
+
const peer = peersNamed[0];
|
|
208
|
+
if (peer === undefined) {
|
|
209
|
+
return { kind: "not_found", message: `no agent or session named "${input.to}" is currently reachable` };
|
|
210
|
+
}
|
|
211
|
+
const peerAddr = parseRuntimeAddress2(peer.address);
|
|
212
|
+
if (peerAddr === undefined) {
|
|
213
|
+
return { kind: "not_found", message: `internal: malformed peer address for "${input.to}"` };
|
|
214
|
+
}
|
|
215
|
+
return { kind: "resolved", address: { ...peerAddr, runtimeKind: peer.runtimeKind } };
|
|
216
|
+
}
|
|
217
|
+
// src/messaging/inbound.ts
|
|
218
|
+
var PROMPTING_MODES = new Set(["default", "acceptEdits", "dontAsk", "auto"]);
|
|
219
|
+
function classifyPermissionMode2(mode, opts) {
|
|
220
|
+
if (mode === "bypassPermissions")
|
|
221
|
+
return "bypasses";
|
|
222
|
+
if (mode === "plan")
|
|
223
|
+
return opts.bypassAvailable ? "bypasses" : "prompts";
|
|
224
|
+
if (PROMPTING_MODES.has(mode))
|
|
225
|
+
return "prompts";
|
|
226
|
+
return "prompts";
|
|
227
|
+
}
|
|
228
|
+
function mapFromModeToPermissionClass2(fromMode) {
|
|
229
|
+
if (fromMode === "bypass")
|
|
230
|
+
return "bypasses";
|
|
231
|
+
if (fromMode === "prompting")
|
|
232
|
+
return "prompts";
|
|
233
|
+
return "unknown";
|
|
234
|
+
}
|
|
235
|
+
function defaultInboundResult2(receiverClass, senderClass) {
|
|
236
|
+
if (receiverClass === "prompts")
|
|
237
|
+
return senderClass === "bypasses" ? "hold" : "accept";
|
|
238
|
+
return senderClass === "bypasses" ? "accept" : "hold";
|
|
239
|
+
}
|
|
240
|
+
function resolveInboundDecision2(params) {
|
|
241
|
+
if (!params.authenticated)
|
|
242
|
+
return "refuse";
|
|
243
|
+
if (params.explicitSetting !== undefined)
|
|
244
|
+
return params.explicitSetting;
|
|
245
|
+
return defaultInboundResult2(params.receiverClass, params.senderClass);
|
|
246
|
+
}
|
|
247
|
+
function createMailbox2() {
|
|
248
|
+
const boxes = new Map;
|
|
249
|
+
function boxFor(key) {
|
|
250
|
+
let box = boxes.get(key);
|
|
251
|
+
if (box === undefined) {
|
|
252
|
+
box = { held: [], acceptedCount: 0 };
|
|
253
|
+
boxes.set(key, box);
|
|
254
|
+
}
|
|
255
|
+
return box;
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
hold(receiverKey, entry) {
|
|
259
|
+
const box = boxFor(receiverKey);
|
|
260
|
+
if (box.held.length >= HELD_INBOX_CAP2)
|
|
261
|
+
return false;
|
|
262
|
+
box.held.push(entry);
|
|
263
|
+
return true;
|
|
264
|
+
},
|
|
265
|
+
accept(receiverKey) {
|
|
266
|
+
const box = boxFor(receiverKey);
|
|
267
|
+
if (box.acceptedCount >= ACCEPTED_QUEUE_CAP2)
|
|
268
|
+
return false;
|
|
269
|
+
box.acceptedCount += 1;
|
|
270
|
+
return true;
|
|
271
|
+
},
|
|
272
|
+
releaseAccepted(receiverKey, n = 1) {
|
|
273
|
+
const box = boxFor(receiverKey);
|
|
274
|
+
box.acceptedCount = Math.max(0, box.acceptedCount - n);
|
|
275
|
+
},
|
|
276
|
+
heldCount(receiverKey) {
|
|
277
|
+
return boxes.get(receiverKey)?.held.length ?? 0;
|
|
278
|
+
},
|
|
279
|
+
acceptedCount(receiverKey) {
|
|
280
|
+
return boxes.get(receiverKey)?.acceptedCount ?? 0;
|
|
281
|
+
},
|
|
282
|
+
listHeld(receiverKey) {
|
|
283
|
+
return boxes.get(receiverKey)?.held ?? [];
|
|
284
|
+
},
|
|
285
|
+
takeHeld(receiverKey, messageId) {
|
|
286
|
+
const box = boxes.get(receiverKey);
|
|
287
|
+
if (box === undefined)
|
|
288
|
+
return;
|
|
289
|
+
const idx = box.held.findIndex((e) => e.messageId === messageId);
|
|
290
|
+
if (idx === -1)
|
|
291
|
+
return;
|
|
292
|
+
const [removed] = box.held.splice(idx, 1);
|
|
293
|
+
return removed;
|
|
294
|
+
},
|
|
295
|
+
reevaluate(receiverKey, decide) {
|
|
296
|
+
const box = boxes.get(receiverKey);
|
|
297
|
+
if (box === undefined)
|
|
298
|
+
return [];
|
|
299
|
+
const promoted = [];
|
|
300
|
+
box.held = box.held.filter((entry) => {
|
|
301
|
+
if (entry.kind !== "default")
|
|
302
|
+
return true;
|
|
303
|
+
const next = decide(entry);
|
|
304
|
+
if (next === "hold")
|
|
305
|
+
return true;
|
|
306
|
+
promoted.push({ entry, next });
|
|
307
|
+
return false;
|
|
308
|
+
});
|
|
309
|
+
return promoted;
|
|
310
|
+
},
|
|
311
|
+
sweepExpired(receiverKey, now) {
|
|
312
|
+
const box = boxes.get(receiverKey);
|
|
313
|
+
if (box === undefined)
|
|
314
|
+
return [];
|
|
315
|
+
const expired = [];
|
|
316
|
+
box.held = box.held.filter((entry) => {
|
|
317
|
+
if (entry.kind === "default" && entry.expiresAt !== undefined && entry.expiresAt <= now) {
|
|
318
|
+
expired.push(entry);
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
});
|
|
323
|
+
return expired;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function buildDefaultHoldEntry2(messageId, reason, now) {
|
|
328
|
+
return { messageId, reason, kind: "default", heldAt: now, expiresAt: now + DEFAULT_HOLD_EXPIRY_MS2 };
|
|
329
|
+
}
|
|
330
|
+
function buildExplicitHoldEntry2(messageId, reason, now) {
|
|
331
|
+
return { messageId, reason, kind: "explicit", heldAt: now };
|
|
332
|
+
}
|
|
333
|
+
// src/messaging/idle.ts
|
|
334
|
+
function isIdleSubscribeSenderAllowed2(sender) {
|
|
335
|
+
return !sender.isChild;
|
|
336
|
+
}
|
|
337
|
+
function isIdleSubscribeTargetAllowed2(target) {
|
|
338
|
+
return target.objectKind === "session" && target.hasReliableIdleSignal;
|
|
339
|
+
}
|
|
340
|
+
function createNotificationQueue2() {
|
|
341
|
+
const byOwner = new Map;
|
|
342
|
+
let listeners = [];
|
|
343
|
+
let counter = 0;
|
|
344
|
+
return {
|
|
345
|
+
subscribe(listener) {
|
|
346
|
+
listeners.push(listener);
|
|
347
|
+
return () => {
|
|
348
|
+
listeners = listeners.filter((l) => l !== listener);
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
push(ownerKey, rec) {
|
|
352
|
+
const list = byOwner.get(ownerKey) ?? [];
|
|
353
|
+
const record = {
|
|
354
|
+
notification_id: `note-${++counter}`,
|
|
355
|
+
origin: rec.origin,
|
|
356
|
+
queued_at: new Date(rec.queuedAtMs).toISOString(),
|
|
357
|
+
content: rec.content
|
|
358
|
+
};
|
|
359
|
+
list.push(record);
|
|
360
|
+
byOwner.set(ownerKey, list);
|
|
361
|
+
for (const listener of listeners)
|
|
362
|
+
listener(ownerKey, record);
|
|
363
|
+
},
|
|
364
|
+
drain(ownerKey, max) {
|
|
365
|
+
const list = byOwner.get(ownerKey) ?? [];
|
|
366
|
+
const take = max === undefined ? list.length : Math.max(0, Math.min(max, list.length));
|
|
367
|
+
const notifications = list.splice(0, take);
|
|
368
|
+
const remaining = list.length;
|
|
369
|
+
if (list.length === 0)
|
|
370
|
+
byOwner.delete(ownerKey);
|
|
371
|
+
else
|
|
372
|
+
byOwner.set(ownerKey, list);
|
|
373
|
+
return { notifications, remaining };
|
|
374
|
+
},
|
|
375
|
+
pendingCount(ownerKey) {
|
|
376
|
+
return byOwner.get(ownerKey)?.length ?? 0;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function createIdleSubscriptionStore2() {
|
|
381
|
+
let pending = [];
|
|
382
|
+
return {
|
|
383
|
+
subscribe(sub, now) {
|
|
384
|
+
pending.push({ ...sub, createdAt: now, expiresAt: now + NOTIFY_IDLE_EXPIRY_MS2 });
|
|
385
|
+
},
|
|
386
|
+
fireIdle(targetKey, now, computeReducedStatus, queue, originLabel) {
|
|
387
|
+
const matches = pending.filter((p) => p.targetKey === targetKey && p.expiresAt > now);
|
|
388
|
+
pending = pending.filter((p) => p.targetKey !== targetKey);
|
|
389
|
+
for (const m of matches) {
|
|
390
|
+
const reducedStatus = computeReducedStatus(m.subscriberKey);
|
|
391
|
+
queue.push(m.subscriberKey, {
|
|
392
|
+
origin: originLabel,
|
|
393
|
+
content: reducedStatus ? `${originLabel} changed state (reduced-status notice: the subscribing session is currently holding cross-session messages from this sender's class)` : `${originLabel} is now idle`,
|
|
394
|
+
queuedAtMs: now
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return matches.length;
|
|
398
|
+
},
|
|
399
|
+
sweepExpired(now) {
|
|
400
|
+
pending = pending.filter((p) => p.expiresAt > now);
|
|
401
|
+
},
|
|
402
|
+
pendingCount(targetKey) {
|
|
403
|
+
return pending.filter((p) => p.targetKey === targetKey).length;
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
// src/messaging/attribution.ts
|
|
408
|
+
var AGENT_MESSAGE_TAG2 = "agent-message";
|
|
409
|
+
function escapeAttributionText2(value) {
|
|
410
|
+
return value.split(`</${AGENT_MESSAGE_TAG2}`).join(`</${AGENT_MESSAGE_TAG2}`).split(`<${AGENT_MESSAGE_TAG2}`).join(`<${AGENT_MESSAGE_TAG2}`);
|
|
411
|
+
}
|
|
412
|
+
function escapeAttributionAttribute2(value) {
|
|
413
|
+
return escapeAttributionText2(value).split('"').join(""");
|
|
414
|
+
}
|
|
415
|
+
var RESERVED_NOTIFICATION_KEY_PREFIX2 = "host:";
|
|
416
|
+
function facetNotificationKey2(sessionId) {
|
|
417
|
+
return `${RESERVED_NOTIFICATION_KEY_PREFIX2}${sessionId}`;
|
|
418
|
+
}
|
|
419
|
+
function isReservedNotificationKey2(key) {
|
|
420
|
+
return key.startsWith(RESERVED_NOTIFICATION_KEY_PREFIX2);
|
|
421
|
+
}
|
|
422
|
+
// src/messaging/router.ts
|
|
423
|
+
var MAX_TRACKED_MESSAGE_IDS2 = 1e4;
|
|
424
|
+
function rememberBounded2(map, key, value, cap = MAX_TRACKED_MESSAGE_IDS2) {
|
|
425
|
+
if (map.has(key))
|
|
426
|
+
map.delete(key);
|
|
427
|
+
map.set(key, value);
|
|
428
|
+
while (map.size > cap) {
|
|
429
|
+
const oldest = map.keys().next();
|
|
430
|
+
if (oldest.done === true)
|
|
431
|
+
break;
|
|
432
|
+
map.delete(oldest.value);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function createMessagingRouterSeam2() {
|
|
436
|
+
const outcomes = new Map;
|
|
437
|
+
const idsBySenderAndTool = new Map;
|
|
438
|
+
let sources = [];
|
|
439
|
+
let counter = 0;
|
|
440
|
+
return {
|
|
441
|
+
allocateMessageId(senderSessionId, toolUseId) {
|
|
442
|
+
const key = JSON.stringify([senderSessionId, toolUseId]);
|
|
443
|
+
const existing = idsBySenderAndTool.get(key);
|
|
444
|
+
if (existing !== undefined)
|
|
445
|
+
return existing;
|
|
446
|
+
const id = `msg-${++counter}`;
|
|
447
|
+
rememberBounded2(idsBySenderAndTool, key, id);
|
|
448
|
+
return id;
|
|
449
|
+
},
|
|
450
|
+
recordOutcome(messageId, outcome) {
|
|
451
|
+
rememberBounded2(outcomes, messageId, outcome);
|
|
452
|
+
},
|
|
453
|
+
lookupOutcome(messageId) {
|
|
454
|
+
return outcomes.get(messageId);
|
|
455
|
+
},
|
|
456
|
+
addChildRosterSource(getChildren) {
|
|
457
|
+
sources.push(getChildren);
|
|
458
|
+
return () => {
|
|
459
|
+
sources = sources.filter((s) => s !== getChildren);
|
|
460
|
+
};
|
|
461
|
+
},
|
|
462
|
+
children() {
|
|
463
|
+
return sources.flatMap((getChildren) => getChildren());
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function createSubscriberDirectory2() {
|
|
468
|
+
const map = new Map;
|
|
469
|
+
return {
|
|
470
|
+
remember(messageId, subscriberSessionId) {
|
|
471
|
+
rememberBounded2(map, messageId, subscriberSessionId);
|
|
472
|
+
},
|
|
473
|
+
lookup(messageId) {
|
|
474
|
+
return map.get(messageId);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function callerAddress2(caller) {
|
|
479
|
+
if (caller.agentId !== undefined)
|
|
480
|
+
return buildChildAddress2(caller.sessionId, caller.agentId);
|
|
481
|
+
return buildSessionAddress2(caller.sessionId);
|
|
482
|
+
}
|
|
483
|
+
var SUCCESS_CLASS_STATUSES = new Set(["delivered", "queued", "resumed_and_delivered"]);
|
|
484
|
+
function outcomeReason(outcome) {
|
|
485
|
+
return "reason" in outcome ? outcome.reason : outcome.status;
|
|
486
|
+
}
|
|
487
|
+
async function sendMessage2(deps, caller, input) {
|
|
488
|
+
const now = deps.now();
|
|
489
|
+
const messageId = deps.seam.allocateMessageId(caller.sessionId, caller.toolUseId);
|
|
490
|
+
const existing = deps.seam.lookupOutcome(messageId);
|
|
491
|
+
if (existing !== undefined)
|
|
492
|
+
return { outcome: existing };
|
|
493
|
+
function settle(outcome, notify) {
|
|
494
|
+
deps.seam.recordOutcome(messageId, outcome);
|
|
495
|
+
return notify !== undefined ? { outcome, notify } : { outcome };
|
|
496
|
+
}
|
|
497
|
+
const from = callerAddress2(caller);
|
|
498
|
+
const fromKey = serializeRuntimeAddress2(from);
|
|
499
|
+
const wantsIdle = input.notify_when_idle === true;
|
|
500
|
+
const isPureSubscription = input.message.length === 0;
|
|
501
|
+
if (wantsIdle && !isIdleSubscribeSenderAllowed2({ isChild: caller.agentId !== undefined })) {
|
|
502
|
+
return settle(refused2(messageId, "notify_when_idle: only a main conversation may subscribe (WS-10 §14)"));
|
|
503
|
+
}
|
|
504
|
+
if (!isPureSubscription) {
|
|
505
|
+
if (messageExceedsMaxSize2(input.message)) {
|
|
506
|
+
return settle(refused2(messageId, `message exceeds MAX_GLOBAL_MESSAGE_SIZE (${MAX_GLOBAL_MESSAGE_SIZE2} chars)`));
|
|
507
|
+
}
|
|
508
|
+
if (deps.loopGuard.check(fromKey, input.to, input.message, now) === "duplicate") {
|
|
509
|
+
return settle(refused2(messageId, "identical message to the same target was sent moments ago (rapid repeat suppressed, WS-10 §12)"));
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const reachable = await deps.adapter.listReachable({ parent: buildSessionAddress2(caller.sessionId) });
|
|
513
|
+
const peers = reachable.filter((r) => r.objectKind === "session");
|
|
514
|
+
const resolved = resolveTarget2({ to: input.to, callerParentSessionId: caller.sessionId, children: deps.seam.children(), peers });
|
|
515
|
+
if (resolved.kind === "not_found")
|
|
516
|
+
return settle(notFound2(messageId, resolved.message));
|
|
517
|
+
if (resolved.kind === "stale")
|
|
518
|
+
return settle(refused2(messageId, resolved.message));
|
|
519
|
+
if (resolved.kind === "ambiguous")
|
|
520
|
+
return settle(ambiguous2(messageId, resolved.candidates));
|
|
521
|
+
const to = resolved.address;
|
|
522
|
+
if (sameAddress2(to, from)) {
|
|
523
|
+
return settle(refused2(messageId, "cannot SendMessage to your own session (self-target, WS-10 §16)"));
|
|
524
|
+
}
|
|
525
|
+
const targetRow = to.objectKind === "agent" && resolved.child !== undefined ? childToListedRuntimeObject2(caller.sessionId, resolved.child) : reachable.find((r) => r.address === serializeRuntimeAddress2(to));
|
|
526
|
+
if (wantsIdle && (targetRow === undefined || !targetRow.capabilities.notifyWhenIdle)) {
|
|
527
|
+
return settle(refused2(messageId, "notify_when_idle: target does not support idle notification (subagents, teammates, remote peers, and adapters without a reliable idle signal refuse the WHOLE call, WS-10 §14)"));
|
|
528
|
+
}
|
|
529
|
+
if (wantsIdle) {
|
|
530
|
+
deps.subscribers.remember(messageId, caller.sessionId);
|
|
531
|
+
}
|
|
532
|
+
if (isPureSubscription) {
|
|
533
|
+
return settle(await deps.adapter.subscribeIdle(to, { messageId }));
|
|
534
|
+
}
|
|
535
|
+
const hopCount = 0;
|
|
536
|
+
if (hopCountExceeded2(hopCount))
|
|
537
|
+
return settle(refused2(messageId, "message exceeds MAX_HOP_COUNT"));
|
|
538
|
+
const senderPermissionClass = await deps.adapter.senderPermissionClass(from);
|
|
539
|
+
const envelope = {
|
|
540
|
+
messageId,
|
|
541
|
+
from,
|
|
542
|
+
fromGeneration: 0,
|
|
543
|
+
to,
|
|
544
|
+
toGeneration: 0,
|
|
545
|
+
body: input.message,
|
|
546
|
+
...input.summary !== undefined ? { summary: input.summary } : {},
|
|
547
|
+
notifyWhenIdle: wantsIdle,
|
|
548
|
+
createdAt: now,
|
|
549
|
+
expiresAt: now + DEFAULT_MESSAGE_TTL_MS2,
|
|
550
|
+
hopCount,
|
|
551
|
+
originToolCallId: caller.toolUseId,
|
|
552
|
+
senderPermissionClass
|
|
553
|
+
};
|
|
554
|
+
const bodyOutcome = await deliverEnvelope(deps, to, resolved.child, envelope, messageId);
|
|
555
|
+
if (!wantsIdle)
|
|
556
|
+
return settle(bodyOutcome);
|
|
557
|
+
if (!SUCCESS_CLASS_STATUSES.has(bodyOutcome.status)) {
|
|
558
|
+
return settle(bodyOutcome, { refused: `message was not delivered (status: ${bodyOutcome.status}); notify_when_idle was not attempted` });
|
|
559
|
+
}
|
|
560
|
+
const idleOutcome = await deps.adapter.subscribeIdle(to, { messageId });
|
|
561
|
+
return settle(bodyOutcome, idleOutcome.status === "subscribed" ? { subscribed: true } : { refused: outcomeReason(idleOutcome) });
|
|
562
|
+
}
|
|
563
|
+
function describeThrow(err) {
|
|
564
|
+
return err instanceof Error ? err.message : String(err);
|
|
565
|
+
}
|
|
566
|
+
function outcomeForThrow(deps, messageId, err) {
|
|
567
|
+
if (deps.classifyDeliveryError?.(err) === "refused")
|
|
568
|
+
return refused2(messageId, describeThrow(err));
|
|
569
|
+
return deliveryUncertain2(messageId, `unexpected error during delivery: ${describeThrow(err)}`);
|
|
570
|
+
}
|
|
571
|
+
async function deliverEnvelope(deps, to, child, envelope, messageId) {
|
|
572
|
+
if (to.objectKind === "agent") {
|
|
573
|
+
if (child === undefined)
|
|
574
|
+
return notFound2(messageId, "child no longer reachable");
|
|
575
|
+
try {
|
|
576
|
+
return child.status() === "running" ? await deps.adapter.steerChild(to, envelope) : await deps.adapter.resumeChild(to, envelope);
|
|
577
|
+
} catch (err) {
|
|
578
|
+
return outcomeForThrow(deps, messageId, err);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
try {
|
|
582
|
+
return await deps.adapter.deliverToSession(to, envelope);
|
|
583
|
+
} catch (err) {
|
|
584
|
+
return outcomeForThrow(deps, messageId, err);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
function formatListing(rows) {
|
|
588
|
+
if (rows.length === 0)
|
|
589
|
+
return "No agents or sessions are currently reachable.";
|
|
590
|
+
return rows.map((r) => {
|
|
591
|
+
const label = r.name !== undefined ? `${r.name} (${r.address})` : r.address;
|
|
592
|
+
return `- ${label} [${r.objectKind}/${r.runtimeKind}] status=${r.status} mode=${r.mode}`;
|
|
593
|
+
}).join(`
|
|
594
|
+
`);
|
|
595
|
+
}
|
|
596
|
+
async function listAgents2(deps, caller, _input) {
|
|
597
|
+
const selfAddr = buildSessionAddress2(caller.sessionId);
|
|
598
|
+
const selfKey = serializeRuntimeAddress2(selfAddr);
|
|
599
|
+
const reachable = await deps.adapter.listReachable({ parent: selfAddr });
|
|
600
|
+
const rows = reachable.filter((r) => r.address !== selfKey);
|
|
601
|
+
return { listing: formatListing(rows), rows };
|
|
602
|
+
}
|
|
603
|
+
function readNotifications2(deps, caller) {
|
|
604
|
+
return deps.notifications.drain(caller.sessionId);
|
|
605
|
+
}
|
|
606
|
+
function createMessagingRouter2(deps) {
|
|
607
|
+
return {
|
|
608
|
+
deps,
|
|
609
|
+
sendMessage: (caller, input) => sendMessage2(deps, caller, input),
|
|
610
|
+
listAgents: (caller, input) => listAgents2(deps, caller, input),
|
|
611
|
+
readNotifications: (caller) => readNotifications2(deps, caller)
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
export { serializeRuntimeAddress2, createFakeMessagingRouterSeam2, REFERENCE_RUNTIME_KIND2, validateToField2, buildSessionAddress2, buildChildAddress2, parseRuntimeAddress2, sameAddress2, MAX_GLOBAL_MESSAGE_SIZE2, DEFAULT_MESSAGE_TTL_MS2, MAX_HOP_COUNT2, RAPID_REPEAT_WINDOW_MS2, HELD_INBOX_CAP2, ACCEPTED_QUEUE_CAP2, DEFAULT_HOLD_EXPIRY_MS2, NOTIFY_IDLE_EXPIRY_MS2, messageExceedsMaxSize2, hopCountExceeded2, delivered2, queued2, resumedAndDelivered2, held2, subscribed2, deliveryUncertain2, refused2, ambiguous2, notFound2, unavailable2, createLoopGuard2, childToListedRuntimeObject2, resolveTarget2, classifyPermissionMode2, mapFromModeToPermissionClass2, defaultInboundResult2, resolveInboundDecision2, createMailbox2, buildDefaultHoldEntry2, buildExplicitHoldEntry2, isIdleSubscribeSenderAllowed2, isIdleSubscribeTargetAllowed2, createNotificationQueue2, createIdleSubscriptionStore2, AGENT_MESSAGE_TAG2, escapeAttributionText2, escapeAttributionAttribute2, RESERVED_NOTIFICATION_KEY_PREFIX2, facetNotificationKey2, isReservedNotificationKey2, MAX_TRACKED_MESSAGE_IDS2, rememberBounded2, createMessagingRouterSeam2, createSubscriberDirectory2, callerAddress2, sendMessage2, formatListing, listAgents2, readNotifications2, createMessagingRouter2 };
|