dsh-interconnect 0.6.4 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -13
- package/lib/index.js +315 -253
- package/lib/interconnect/index.js +315 -253
- package/lib/interconnect/index.js.map +3 -3
- package/lib/tool-interconnect/index.js +93 -10
- package/lib/tool-interconnect/index.js.map +2 -2
- package/lib/types/interconnect/index.d.ts +74 -28
- package/lib/types/interconnect/index.d.ts.map +1 -1
- package/lib/types/interconnect/types.d.ts +137 -13
- package/lib/types/interconnect/types.d.ts.map +1 -1
- package/lib/types/tool-interconnect/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -7,10 +7,10 @@ function apply(ctx) {
|
|
|
7
7
|
name: "interconnect_send",
|
|
8
8
|
description: 'Deliver one text message to a live session on another DSH instance (same machine, another machine, or another session), over a shared-secret-authenticated channel. Returns whether the peer instance received it and which instance answered. Only a session with a running agent can receive a message; when none is running the result reports reason "session-not-live", and interconnect_list shows which sessions are live there.',
|
|
9
9
|
parameters: {
|
|
10
|
-
|
|
10
|
+
instanceId: {
|
|
11
11
|
type: "string",
|
|
12
12
|
required: true,
|
|
13
|
-
description: "
|
|
13
|
+
description: "The peer instance id to deliver to, as configured under this instance's interconnect peers map. Deliveries go over the persistent link to that instance."
|
|
14
14
|
},
|
|
15
15
|
sessionId: {
|
|
16
16
|
type: "string",
|
|
@@ -67,11 +67,16 @@ function apply(ctx) {
|
|
|
67
67
|
return [{ type: "text", text }];
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
|
-
async execute(args) {
|
|
70
|
+
async execute(args, exec) {
|
|
71
|
+
const sessionId = exec.agent?.session.id;
|
|
72
|
+
const self = interconnect.selfSender(sessionId === void 0 ? "" : String(sessionId));
|
|
71
73
|
const result = await interconnect.send({
|
|
72
|
-
|
|
74
|
+
instanceId: args.instanceId,
|
|
73
75
|
sessionId: args.sessionId,
|
|
74
76
|
text: args.text,
|
|
77
|
+
// Attribute this instance as the sender so the peer can reply back. The
|
|
78
|
+
// calling session id comes from the executing agent (the session sending).
|
|
79
|
+
sender: self,
|
|
75
80
|
...args.delivery === void 0 ? {} : { delivery: args.delivery },
|
|
76
81
|
...args.resume === void 0 ? {} : { resume: args.resume }
|
|
77
82
|
});
|
|
@@ -87,10 +92,10 @@ function apply(ctx) {
|
|
|
87
92
|
name: "interconnect_ping",
|
|
88
93
|
description: "Probe a peer DSH instance for liveness and identity over the shared-secret channel. Returns the peer instance id when reachable, or null on transport/auth failure.",
|
|
89
94
|
parameters: {
|
|
90
|
-
|
|
95
|
+
instanceId: {
|
|
91
96
|
type: "string",
|
|
92
97
|
required: true,
|
|
93
|
-
description: "
|
|
98
|
+
description: "The peer instance id to probe, as configured under this instance's interconnect peers map."
|
|
94
99
|
}
|
|
95
100
|
},
|
|
96
101
|
output: {
|
|
@@ -108,7 +113,7 @@ function apply(ctx) {
|
|
|
108
113
|
}]
|
|
109
114
|
},
|
|
110
115
|
async execute(args) {
|
|
111
|
-
const result = await interconnect.ping(args.
|
|
116
|
+
const result = await interconnect.ping(args.instanceId);
|
|
112
117
|
if (result === void 0) return { reachable: false };
|
|
113
118
|
return { reachable: true, instance: result.instance };
|
|
114
119
|
}
|
|
@@ -117,10 +122,10 @@ function apply(ctx) {
|
|
|
117
122
|
name: "interconnect_list",
|
|
118
123
|
description: "List the live sessions on a peer DSH instance, so a message can be addressed without knowing a session id in advance. Every returned sessionId is a valid interconnect_send target at the time of the call. Only live sessions appear: a session that exists on the peer but has no running agent is not listed and cannot receive a message.",
|
|
119
124
|
parameters: {
|
|
120
|
-
|
|
125
|
+
instanceId: {
|
|
121
126
|
type: "string",
|
|
122
127
|
required: true,
|
|
123
|
-
description: "
|
|
128
|
+
description: "The peer instance id to list, as configured under this instance's interconnect peers map."
|
|
124
129
|
}
|
|
125
130
|
},
|
|
126
131
|
output: {
|
|
@@ -159,7 +164,7 @@ function apply(ctx) {
|
|
|
159
164
|
}
|
|
160
165
|
},
|
|
161
166
|
async execute(args) {
|
|
162
|
-
const result = await interconnect.list(args.
|
|
167
|
+
const result = await interconnect.list(args.instanceId);
|
|
163
168
|
if (result === void 0) return { reachable: false };
|
|
164
169
|
return {
|
|
165
170
|
reachable: true,
|
|
@@ -174,6 +179,84 @@ function apply(ctx) {
|
|
|
174
179
|
};
|
|
175
180
|
}
|
|
176
181
|
}));
|
|
182
|
+
ctx.tools.register(defineTool({
|
|
183
|
+
name: "interconnect_reply",
|
|
184
|
+
description: 'Deliver one text message back to the peer that this session last received an interconnect message from. Only the LOCAL session id and the reply text are needed: the outbound target (the peer origin and its session id) is recalled from the message this session received, so you do not ask for an address twice. A session that never received a message through interconnect \u2014 or received one without a sender identity \u2014 reports reason "no-sender-known".',
|
|
185
|
+
parameters: {
|
|
186
|
+
sessionId: {
|
|
187
|
+
type: "string",
|
|
188
|
+
required: true,
|
|
189
|
+
description: "The LOCAL session id that received the message being replied to."
|
|
190
|
+
},
|
|
191
|
+
text: {
|
|
192
|
+
type: "string",
|
|
193
|
+
required: true,
|
|
194
|
+
description: "Message text delivered back to the recorded sender session."
|
|
195
|
+
},
|
|
196
|
+
delivery: {
|
|
197
|
+
type: "string",
|
|
198
|
+
enum: ["followup", "steer", "inject"],
|
|
199
|
+
description: "How the reply reaches the target agent, as in interconnect_send. Omit to use the receiver's configured default."
|
|
200
|
+
},
|
|
201
|
+
resume: {
|
|
202
|
+
type: "boolean",
|
|
203
|
+
description: "Wake the target session if it is persisted but has no running agent, as in interconnect_send. Off by default; the receiver may refuse."
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
output: {
|
|
207
|
+
schema: {
|
|
208
|
+
type: "object",
|
|
209
|
+
additionalProperties: false,
|
|
210
|
+
properties: {
|
|
211
|
+
delivered: { type: "boolean", required: true },
|
|
212
|
+
instance: { type: "string", required: true },
|
|
213
|
+
delivery: { type: "string" },
|
|
214
|
+
reason: { type: "string" }
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
render: (_args, value) => {
|
|
218
|
+
if (value.delivered) {
|
|
219
|
+
return [{
|
|
220
|
+
type: "text",
|
|
221
|
+
text: `replied to ${value.instance}${value.delivery === void 0 ? "" : ` via ${value.delivery}`}`
|
|
222
|
+
}];
|
|
223
|
+
}
|
|
224
|
+
const text = (() => {
|
|
225
|
+
if (value.reason === "no-sender-known") {
|
|
226
|
+
return `not delivered: no sender recorded for "${_args.sessionId}" \u2014 this session never received an interconnect message with a sender identity`;
|
|
227
|
+
}
|
|
228
|
+
if (value.reason === "session-owned-by-subagent") {
|
|
229
|
+
return `not delivered: "${_args.sessionId}" is a subagent's session \u2014 its parent agent owns delivery`;
|
|
230
|
+
}
|
|
231
|
+
if (value.reason === "unreachable") {
|
|
232
|
+
return `not delivered: the recorded sender did not answer (unreachable or unauthorized)`;
|
|
233
|
+
}
|
|
234
|
+
if (value.reason === "resume-refused") {
|
|
235
|
+
return `not delivered: the recorded sender does not allow waking persisted sessions`;
|
|
236
|
+
}
|
|
237
|
+
if (value.reason === "resume-failed") {
|
|
238
|
+
return `not delivered: could not wake the recorded sender's session`;
|
|
239
|
+
}
|
|
240
|
+
return `not delivered: the recorded sender's session is not live`;
|
|
241
|
+
})();
|
|
242
|
+
return [{ type: "text", text }];
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
async execute(args) {
|
|
246
|
+
const result = await interconnect.reply({
|
|
247
|
+
sessionId: args.sessionId,
|
|
248
|
+
text: args.text,
|
|
249
|
+
...args.delivery === void 0 ? {} : { delivery: args.delivery },
|
|
250
|
+
...args.resume === void 0 ? {} : { resume: args.resume }
|
|
251
|
+
});
|
|
252
|
+
return {
|
|
253
|
+
delivered: result.delivered,
|
|
254
|
+
instance: result.instance,
|
|
255
|
+
...result.delivery === void 0 ? {} : { delivery: result.delivery },
|
|
256
|
+
...result.reason === void 0 ? {} : { reason: result.reason }
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}));
|
|
177
260
|
}
|
|
178
261
|
export {
|
|
179
262
|
apply,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/tool-interconnect/index.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Model-facing tools for cross-instance message handoff: `interconnect_send`\n * delivers one text message to a live session on a peer DSH instance, and\n * `interconnect_ping` probes a peer's liveness and identity.\n *\n * The tools consume the host-plane `interconnect` service and publish nothing\n * themselves, so this row sits as an ordinary tool plugin in a preset while\n * the service it reaches stays host-side (the same split `tool-goal` uses\n * against `goals`).\n * @module @deepseek-ai/dsh-tool-interconnect\n */\n\nimport { Context } from '@deepseek-ai/cordis'\nimport { defineTool } from '@deepseek-ai/dsh-tools'\n// Activates the `Context.interconnect` merge declared by the interconnect service plugin.\nimport type {} from '../interconnect/index.ts'\n\n/** Services required before the tools can register. */\nexport const inject = ['interconnect', 'tools']\n\n/**\n * Register the two tool surfaces. Registration is idempotent per fiber; the\n * tools unregister with the owning fiber.\n * @param ctx - connection context carrying the interconnect service and the tool registry.\n */\nexport function apply(ctx: Context): void {\n const interconnect = ctx.interconnect\n\n ctx.tools.register(defineTool({\n name: 'interconnect_send',\n description: 'Deliver one text message to a live session on another DSH instance (same machine, '\n + 'another machine, or another session), over a shared-secret-authenticated channel. '\n + 'Returns whether the peer instance received it and which instance answered. '\n + 'Only a session with a running agent can receive a message; when none is running the result '\n + 'reports reason \"session-not-live\", and interconnect_list shows which sessions are live there.',\n parameters: {\n baseUrl: {\n type: 'string',\n required: true,\n description: 'Receiver instance origin, e.g. http://127.0.0.1:3080 or http://peer-host:9001.',\n },\n sessionId: {\n type: 'string',\n required: true,\n description: 'Target session id on the receiving instance to deliver the message to.',\n },\n text: {\n type: 'string',\n required: true,\n description: 'Message text delivered to the peer session.',\n },\n delivery: {\n type: 'string',\n enum: ['followup', 'steer', 'inject'],\n description: 'How the message reaches the target agent. `followup` queues it as its own turn '\n + 'behind whatever that agent is doing now. `steer` cuts into the nearest step boundary of a '\n + 'running turn, so an urgent message does not wait for that turn to end. `inject` seeds '\n + 'context without waking an idle agent, so it may sit unread. Omit to use the receiver\\'s '\n + 'configured default.',\n },\n resume: {\n type: 'boolean',\n description: 'Wake the target session if it is persisted but has no running agent. Off by '\n + 'default because delivery to a woken session starts a real agent turn \u2014 a billed model call '\n + 'with that session\\'s full toolset \u2014 in a conversation nobody is watching. Prefer '\n + 'interconnect_list and an already-live target; set this only when that specific session must '\n + 'be reached. The receiver may refuse, answering reason \"resume-refused\".',\n },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n delivered: { type: 'boolean', required: true },\n instance: { type: 'string', required: true },\n delivery: { type: 'string' },\n reason: { type: 'string' },\n },\n },\n render: (_args, value) => {\n if (value.delivered) {\n return [{\n type: 'text',\n text: `delivered to ${value.instance}${value.delivery === undefined ? '' : ` via ${value.delivery}`}`,\n }]\n }\n // Each failure gets the response it actually needs: a not-live target is\n // the caller's to re-choose, while an unreachable peer may just be worth\n // retrying. The old single line claimed \"no live session\" even when the\n // peer never answered, which pointed at the wrong thing entirely.\n const text = ((): string => {\n switch (value.reason) {\n case 'unreachable':\n return `not delivered: ${value.instance} did not answer (unreachable or unauthorized)`\n case 'resume-refused':\n return `not delivered: ${value.instance} does not allow waking persisted sessions`\n case 'resume-failed':\n return `not delivered: could not wake \"${_args.sessionId}\" on ${value.instance}`\n + ' (no such persisted session, or another owner holds it)'\n case 'session-owned-by-subagent':\n return `not delivered: \"${_args.sessionId}\" is a subagent's session on ${value.instance}`\n + ' \u2014 its parent agent owns delivery, so reach it through that parent'\n default:\n return `not delivered: no live session \"${_args.sessionId}\" on ${value.instance}`\n + ' \u2014 use interconnect_list to see which sessions are live there,'\n + ' or set resume to wake this one'\n }\n })()\n return [{ type: 'text', text }]\n },\n },\n async execute(args) {\n const result = await interconnect.send({\n baseUrl: args.baseUrl,\n sessionId: args.sessionId,\n text: args.text,\n ...(args.delivery === undefined ? {} : { delivery: args.delivery }),\n ...(args.resume === undefined ? {} : { resume: args.resume }),\n })\n return {\n delivered: result.delivered,\n instance: result.instance,\n ...(result.delivery === undefined ? {} : { delivery: result.delivery }),\n ...(result.reason === undefined ? {} : { reason: result.reason }),\n }\n },\n }))\n\n ctx.tools.register(defineTool({\n name: 'interconnect_ping',\n description: 'Probe a peer DSH instance for liveness and identity over the shared-secret channel. '\n + 'Returns the peer instance id when reachable, or null on transport/auth failure.',\n parameters: {\n baseUrl: {\n type: 'string',\n required: true,\n description: 'Peer instance origin, e.g. http://127.0.0.1:3080.',\n },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n reachable: { type: 'boolean', required: true },\n instance: { type: 'string' },\n },\n },\n render: (_args, value) => [{\n type: 'text',\n text: value.reachable\n ? `reachable: ${value.instance ?? '(unknown instance)'}`\n : 'unreachable or unauthorized',\n }],\n },\n async execute(args) {\n const result = await interconnect.ping(args.baseUrl)\n if (result === undefined) return { reachable: false }\n return { reachable: true, instance: result.instance }\n },\n }))\n\n ctx.tools.register(defineTool({\n name: 'interconnect_list',\n description: 'List the live sessions on a peer DSH instance, so a message can be addressed without '\n + 'knowing a session id in advance. Every returned sessionId is a valid interconnect_send target at '\n + 'the time of the call. Only live sessions appear: a session that exists on the peer but has no '\n + 'running agent is not listed and cannot receive a message.',\n parameters: {\n baseUrl: {\n type: 'string',\n required: true,\n description: 'Peer instance origin, e.g. http://127.0.0.1:3080.',\n },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n reachable: { type: 'boolean', required: true },\n instance: { type: 'string' },\n sessions: {\n type: 'array',\n items: {\n type: 'object',\n additionalProperties: false,\n properties: {\n sessionId: { type: 'string', required: true },\n title: { type: 'string' },\n status: { type: 'string' },\n },\n },\n },\n },\n },\n render: (_args, value) => {\n if (!value.reachable) return [{ type: 'text', text: 'unreachable or unauthorized' }]\n const sessions = value.sessions ?? []\n if (sessions.length === 0) {\n return [{ type: 'text', text: `no live sessions on ${value.instance ?? '(unknown instance)'}` }]\n }\n const lines = sessions.map((session) => {\n const title = session.title === undefined ? '' : ` ${session.title}`\n const status = session.status === undefined ? '' : ` [${session.status}]`\n return `${session.sessionId}${title}${status}`\n })\n return [{ type: 'text', text: lines.join('\\n') }]\n },\n },\n async execute(args) {\n const result = await interconnect.list(args.baseUrl)\n if (result === undefined) return { reachable: false }\n return {\n reachable: true,\n instance: result.instance,\n // Rebuild each row so an optional key stays absent rather than\n // becoming an explicit undefined the wire schema would reject.\n sessions: result.sessions.map(session => ({\n sessionId: session.sessionId,\n ...(session.title === undefined ? {} : { title: session.title }),\n ...(session.status === undefined ? {} : { status: session.status }),\n })),\n }\n },\n }))\n}\n"],
|
|
5
|
-
"mappings": ";AAaA,SAAS,kBAAkB;AAKpB,IAAM,SAAS,CAAC,gBAAgB,OAAO;AAOvC,SAAS,MAAM,KAAoB;AACxC,QAAM,eAAe,IAAI;AAEzB,MAAI,MAAM,SAAS,WAAW;AAAA,IAC5B,MAAM;AAAA,IACN,aAAa;AAAA,IAKb,YAAY;AAAA,MACV,
|
|
4
|
+
"sourcesContent": ["/**\n * Model-facing tools for cross-instance message handoff: `interconnect_send`\n * delivers one text message to a live session on a peer DSH instance, and\n * `interconnect_ping` probes a peer's liveness and identity.\n *\n * The tools consume the host-plane `interconnect` service and publish nothing\n * themselves, so this row sits as an ordinary tool plugin in a preset while\n * the service it reaches stays host-side (the same split `tool-goal` uses\n * against `goals`).\n * @module @deepseek-ai/dsh-tool-interconnect\n */\n\nimport { Context } from '@deepseek-ai/cordis'\nimport { defineTool } from '@deepseek-ai/dsh-tools'\n// Activates the `Context.interconnect` merge declared by the interconnect service plugin.\nimport type {} from '../interconnect/index.ts'\n\n/** Services required before the tools can register. */\nexport const inject = ['interconnect', 'tools']\n\n/**\n * Register the two tool surfaces. Registration is idempotent per fiber; the\n * tools unregister with the owning fiber.\n * @param ctx - connection context carrying the interconnect service and the tool registry.\n */\nexport function apply(ctx: Context): void {\n const interconnect = ctx.interconnect\n\n ctx.tools.register(defineTool({\n name: 'interconnect_send',\n description: 'Deliver one text message to a live session on another DSH instance (same machine, '\n + 'another machine, or another session), over a shared-secret-authenticated channel. '\n + 'Returns whether the peer instance received it and which instance answered. '\n + 'Only a session with a running agent can receive a message; when none is running the result '\n + 'reports reason \"session-not-live\", and interconnect_list shows which sessions are live there.',\n parameters: {\n instanceId: {\n type: 'string',\n required: true,\n description: 'The peer instance id to deliver to, as configured under this instance\\'s '\n + 'interconnect peers map. Deliveries go over the persistent link to that instance.',\n },\n sessionId: {\n type: 'string',\n required: true,\n description: 'Target session id on the receiving instance to deliver the message to.',\n },\n text: {\n type: 'string',\n required: true,\n description: 'Message text delivered to the peer session.',\n },\n delivery: {\n type: 'string',\n enum: ['followup', 'steer', 'inject'],\n description: 'How the message reaches the target agent. `followup` queues it as its own turn '\n + 'behind whatever that agent is doing now. `steer` cuts into the nearest step boundary of a '\n + 'running turn, so an urgent message does not wait for that turn to end. `inject` seeds '\n + 'context without waking an idle agent, so it may sit unread. Omit to use the receiver\\'s '\n + 'configured default.',\n },\n resume: {\n type: 'boolean',\n description: 'Wake the target session if it is persisted but has no running agent. Off by '\n + 'default because delivery to a woken session starts a real agent turn \u2014 a billed model call '\n + 'with that session\\'s full toolset \u2014 in a conversation nobody is watching. Prefer '\n + 'interconnect_list and an already-live target; set this only when that specific session must '\n + 'be reached. The receiver may refuse, answering reason \"resume-refused\".',\n },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n delivered: { type: 'boolean', required: true },\n instance: { type: 'string', required: true },\n delivery: { type: 'string' },\n reason: { type: 'string' },\n },\n },\n render: (_args, value) => {\n if (value.delivered) {\n return [{\n type: 'text',\n text: `delivered to ${value.instance}${value.delivery === undefined ? '' : ` via ${value.delivery}`}`,\n }]\n }\n // Each failure gets the response it actually needs: a not-live target is\n // the caller's to re-choose, while an unreachable peer may just be worth\n // retrying. The old single line claimed \"no live session\" even when the\n // peer never answered, which pointed at the wrong thing entirely.\n const text = ((): string => {\n switch (value.reason) {\n case 'unreachable':\n return `not delivered: ${value.instance} did not answer (unreachable or unauthorized)`\n case 'resume-refused':\n return `not delivered: ${value.instance} does not allow waking persisted sessions`\n case 'resume-failed':\n return `not delivered: could not wake \"${_args.sessionId}\" on ${value.instance}`\n + ' (no such persisted session, or another owner holds it)'\n case 'session-owned-by-subagent':\n return `not delivered: \"${_args.sessionId}\" is a subagent's session on ${value.instance}`\n + ' \u2014 its parent agent owns delivery, so reach it through that parent'\n default:\n return `not delivered: no live session \"${_args.sessionId}\" on ${value.instance}`\n + ' \u2014 use interconnect_list to see which sessions are live there,'\n + ' or set resume to wake this one'\n }\n })()\n return [{ type: 'text', text }]\n },\n },\n async execute(args, exec) {\n const sessionId = exec.agent?.session.id\n const self = interconnect.selfSender(sessionId === undefined ? '' : String(sessionId))\n const result = await interconnect.send({\n instanceId: args.instanceId,\n sessionId: args.sessionId,\n text: args.text,\n // Attribute this instance as the sender so the peer can reply back. The\n // calling session id comes from the executing agent (the session sending).\n sender: self,\n ...(args.delivery === undefined ? {} : { delivery: args.delivery }),\n ...(args.resume === undefined ? {} : { resume: args.resume }),\n })\n return {\n delivered: result.delivered,\n instance: result.instance,\n ...(result.delivery === undefined ? {} : { delivery: result.delivery }),\n ...(result.reason === undefined ? {} : { reason: result.reason }),\n }\n },\n }))\n\n ctx.tools.register(defineTool({\n name: 'interconnect_ping',\n description: 'Probe a peer DSH instance for liveness and identity over the shared-secret channel. '\n + 'Returns the peer instance id when reachable, or null on transport/auth failure.',\n parameters: {\n instanceId: {\n type: 'string',\n required: true,\n description: 'The peer instance id to probe, as configured under this instance\\'s '\n + 'interconnect peers map.',\n },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n reachable: { type: 'boolean', required: true },\n instance: { type: 'string' },\n },\n },\n render: (_args, value) => [{\n type: 'text',\n text: value.reachable\n ? `reachable: ${value.instance ?? '(unknown instance)'}`\n : 'unreachable or unauthorized',\n }],\n },\n async execute(args) {\n const result = await interconnect.ping(args.instanceId)\n if (result === undefined) return { reachable: false }\n return { reachable: true, instance: result.instance }\n },\n }))\n\n ctx.tools.register(defineTool({\n name: 'interconnect_list',\n description: 'List the live sessions on a peer DSH instance, so a message can be addressed without '\n + 'knowing a session id in advance. Every returned sessionId is a valid interconnect_send target at '\n + 'the time of the call. Only live sessions appear: a session that exists on the peer but has no '\n + 'running agent is not listed and cannot receive a message.',\n parameters: {\n instanceId: {\n type: 'string',\n required: true,\n description: 'The peer instance id to list, as configured under this instance\\'s '\n + 'interconnect peers map.',\n },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n reachable: { type: 'boolean', required: true },\n instance: { type: 'string' },\n sessions: {\n type: 'array',\n items: {\n type: 'object',\n additionalProperties: false,\n properties: {\n sessionId: { type: 'string', required: true },\n title: { type: 'string' },\n status: { type: 'string' },\n },\n },\n },\n },\n },\n render: (_args, value) => {\n if (!value.reachable) return [{ type: 'text', text: 'unreachable or unauthorized' }]\n const sessions = value.sessions ?? []\n if (sessions.length === 0) {\n return [{ type: 'text', text: `no live sessions on ${value.instance ?? '(unknown instance)'}` }]\n }\n const lines = sessions.map((session) => {\n const title = session.title === undefined ? '' : ` ${session.title}`\n const status = session.status === undefined ? '' : ` [${session.status}]`\n return `${session.sessionId}${title}${status}`\n })\n return [{ type: 'text', text: lines.join('\\n') }]\n },\n },\n async execute(args) {\n const result = await interconnect.list(args.instanceId)\n if (result === undefined) return { reachable: false }\n return {\n reachable: true,\n instance: result.instance,\n // Rebuild each row so an optional key stays absent rather than\n // becoming an explicit undefined the wire schema would reject.\n sessions: result.sessions.map(session => ({\n sessionId: session.sessionId,\n ...(session.title === undefined ? {} : { title: session.title }),\n ...(session.status === undefined ? {} : { status: session.status }),\n })),\n }\n },\n }))\n\n ctx.tools.register(defineTool({\n name: 'interconnect_reply',\n description: 'Deliver one text message back to the peer that this session last received an '\n + 'interconnect message from. Only the LOCAL session id and the reply text are needed: the '\n + 'outbound target (the peer origin and its session id) is recalled from the message this '\n + 'session received, so you do not ask for an address twice. A session that never received a '\n + 'message through interconnect \u2014 or received one without a sender identity \u2014 reports reason '\n + '\"no-sender-known\".',\n parameters: {\n sessionId: {\n type: 'string',\n required: true,\n description: 'The LOCAL session id that received the message being replied to.',\n },\n text: {\n type: 'string',\n required: true,\n description: 'Message text delivered back to the recorded sender session.',\n },\n delivery: {\n type: 'string',\n enum: ['followup', 'steer', 'inject'],\n description: 'How the reply reaches the target agent, as in interconnect_send. Omit to use '\n + \"the receiver's configured default.\",\n },\n resume: {\n type: 'boolean',\n description: 'Wake the target session if it is persisted but has no running agent, as in '\n + 'interconnect_send. Off by default; the receiver may refuse.',\n },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n delivered: { type: 'boolean', required: true },\n instance: { type: 'string', required: true },\n delivery: { type: 'string' },\n reason: { type: 'string' },\n },\n },\n render: (_args, value) => {\n if (value.delivered) {\n return [{\n type: 'text',\n text: `replied to ${value.instance}${value.delivery === undefined ? '' : ` via ${value.delivery}`}`,\n }]\n }\n const text = ((): string => {\n if (value.reason === 'no-sender-known') {\n return `not delivered: no sender recorded for \"${_args.sessionId}\"`\n + ' \u2014 this session never received an interconnect message with a sender identity'\n }\n if (value.reason === 'session-owned-by-subagent') {\n return `not delivered: \"${_args.sessionId}\" is a subagent's session \u2014 its parent agent owns delivery`\n }\n if (value.reason === 'unreachable') {\n return `not delivered: the recorded sender did not answer (unreachable or unauthorized)`\n }\n if (value.reason === 'resume-refused') {\n return `not delivered: the recorded sender does not allow waking persisted sessions`\n }\n if (value.reason === 'resume-failed') {\n return `not delivered: could not wake the recorded sender's session`\n }\n return `not delivered: the recorded sender's session is not live`\n })()\n return [{ type: 'text', text }]\n },\n },\n async execute(args) {\n const result = await interconnect.reply({\n sessionId: args.sessionId,\n text: args.text,\n ...(args.delivery === undefined ? {} : { delivery: args.delivery }),\n ...(args.resume === undefined ? {} : { resume: args.resume }),\n })\n return {\n delivered: result.delivered,\n instance: result.instance,\n ...(result.delivery === undefined ? {} : { delivery: result.delivery }),\n ...(result.reason === undefined ? {} : { reason: result.reason }),\n }\n },\n }))\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
15
15
|
import z from '@deepseek-ai/schemastery';
|
|
16
16
|
import WebSocket from 'ws';
|
|
17
|
-
import { type Config, type EventNotification, type ListResult, type PingResult, type SendResult, type SendRequest, type WebSocketLinkHandle } from './types.ts';
|
|
17
|
+
import { type Config, type EventNotification, type ListResult, type PingResult, type ReplyRequest, type SendResult, type SendRequest, type SenderIdentity, type WebSocketLinkHandle } from './types.ts';
|
|
18
18
|
export type * from './types.ts';
|
|
19
|
-
export {
|
|
19
|
+
export { INTERCONNECT_TOKEN_REF } from './types.ts';
|
|
20
20
|
declare module '@deepseek-ai/cordis' {
|
|
21
21
|
interface Context {
|
|
22
22
|
interconnect: InterconnectService;
|
|
@@ -42,52 +42,92 @@ export declare class InterconnectService extends Service {
|
|
|
42
42
|
private readonly requestTimeoutMs;
|
|
43
43
|
private readonly delivery;
|
|
44
44
|
private readonly allowResume;
|
|
45
|
-
private readonly peers;
|
|
46
45
|
private readonly subscriptions;
|
|
47
46
|
private readonly server;
|
|
48
47
|
private readonly sockets;
|
|
48
|
+
/** Outbound peer links keyed by the peer's `instanceId`. */
|
|
49
49
|
private readonly linkStates;
|
|
50
50
|
private heartbeatTimer;
|
|
51
51
|
/** Peer identity each live socket announced via its `hello` frame, if any. */
|
|
52
52
|
private readonly peerOf;
|
|
53
|
+
/** Sender each local session last received a send from, keyed by local session id. */
|
|
54
|
+
private readonly senders;
|
|
55
|
+
/** In-flight frames sent over a peer link, keyed by `reqId`, awaiting a correlated result. */
|
|
56
|
+
private readonly pendingMessages;
|
|
57
|
+
private reqIdCounter;
|
|
53
58
|
constructor(ctx: Context, config: Config);
|
|
59
|
+
/**
|
|
60
|
+
* This instance's identity to attach to outbound messages. Always present:
|
|
61
|
+
* it needs no address, only this instance's id and the calling session.
|
|
62
|
+
* @param sessionId - the local session that is sending, used as the reply target.
|
|
63
|
+
*/
|
|
64
|
+
selfSender(sessionId: string): SenderIdentity;
|
|
54
65
|
send(request: SendRequest): Promise<SendResult>;
|
|
55
|
-
ping(baseUrl: string): Promise<PingResult | undefined>;
|
|
56
66
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
67
|
+
* Deliver one text message back to the peer that a local session last
|
|
68
|
+
* received a send from. `sessionId` names the LOCAL replying session; the
|
|
69
|
+
* outbound target is the `sender` that session recorded, addressed through
|
|
70
|
+
* this instance's own link to the sender's instance.
|
|
71
|
+
*/
|
|
72
|
+
reply(request: ReplyRequest): Promise<SendResult>;
|
|
73
|
+
/**
|
|
74
|
+
* Probe a peer instance for liveness and identity over its persistent link.
|
|
75
|
+
* Returns the peer identity when reachable, or undefined when the link is
|
|
76
|
+
* not up or no answer arrives.
|
|
77
|
+
*/
|
|
78
|
+
ping(instanceId: string): Promise<PingResult | undefined>;
|
|
79
|
+
/**
|
|
80
|
+
* List a peer instance's live sessions over its persistent link. Undefined on
|
|
81
|
+
* transport failure, matching `ping`.
|
|
59
82
|
*/
|
|
60
|
-
list(
|
|
83
|
+
list(instanceId: string): Promise<ListResult | undefined>;
|
|
84
|
+
/** The origin this instance dials to reach a configured peer, or undefined. */
|
|
85
|
+
private originOf;
|
|
61
86
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
87
|
+
* Send a `msg` frame over the live link to a peer and resolve with its
|
|
88
|
+
* correlated `msg-result`. Undefined when the peer has no live link here
|
|
89
|
+
* (not configured, or the link is down) — sends can only address configured+
|
|
90
|
+
* connected peers by design.
|
|
66
91
|
*/
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
|
|
92
|
+
private msgRequest;
|
|
93
|
+
/** Send a `query` frame over the live link to a peer and resolve its result. */
|
|
94
|
+
private queryRequest;
|
|
95
|
+
/** Settle a `reqId` frame against its `*-result`, timing out at `requestTimeoutMs`. */
|
|
96
|
+
private waitForResult;
|
|
97
|
+
/**
|
|
98
|
+
* Add a peer route at runtime. Returns a disposer that removes it. Re-adding
|
|
99
|
+
* an existing instanceId re-routes it to the new origin.
|
|
100
|
+
* @param instanceId - the peer's `instanceId`.
|
|
101
|
+
* @param origin - origin this instance dials to reach that peer.
|
|
102
|
+
* @returns disposer removing the peer route.
|
|
103
|
+
*/
|
|
104
|
+
subscribe(instanceId: string, origin: string): () => void;
|
|
105
|
+
/** Remove a peer route, closing its outbound link. */
|
|
106
|
+
unsubscribe(instanceId: string): void;
|
|
70
107
|
/**
|
|
71
108
|
* Open (and, on drop, re-open) a persistent WebSocket link to a peer. Local
|
|
72
109
|
* events stream over the link in real time, and events the peer pushes are
|
|
73
|
-
* surfaced as `interconnect/event`. Repeating for the same
|
|
74
|
-
*
|
|
75
|
-
* @param
|
|
110
|
+
* surfaced as `interconnect/event`. Repeating for the same instanceId
|
|
111
|
+
* re-routes the link to the new origin.
|
|
112
|
+
* @param instanceId - the peer's `instanceId`.
|
|
113
|
+
* @param origin - receiver origin this instance dials, e.g. `http://127.0.0.1:13080`.
|
|
76
114
|
* @returns a handle closing the link and cancelling reconnection.
|
|
77
115
|
*/
|
|
78
|
-
link(
|
|
116
|
+
link(instanceId: string, origin: string): WebSocketLinkHandle;
|
|
79
117
|
/** Remove a closed outbound link's state so a later `link` re-dials fresh. */
|
|
80
|
-
forgetLink(
|
|
81
|
-
/**
|
|
118
|
+
forgetLink(instanceId: string): void;
|
|
119
|
+
/** Push one serialized lifecycle fact out to every linked peer over WS. */
|
|
82
120
|
private fanout;
|
|
83
121
|
/** Push one fact over every live WebSocket link, dropping closed sockets. */
|
|
84
122
|
private broadcast;
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
private
|
|
123
|
+
/**
|
|
124
|
+
* Dispatch one inbound `msg` frame: deliver a `send`/`reply` and answer the
|
|
125
|
+
* correlated `msg-result` on the same socket. Errors are surfaced as the
|
|
126
|
+
* result so a throwing handler cannot escape the socket's message callback.
|
|
127
|
+
*/
|
|
128
|
+
private handleMsgFrame;
|
|
129
|
+
/** Dispatch one inbound `query` frame (ping/list/event) and answer on the socket. */
|
|
130
|
+
private handleQueryFrame;
|
|
91
131
|
/** Surface one remote notification to local listeners and the log. */
|
|
92
132
|
private receiveEvent;
|
|
93
133
|
/**
|
|
@@ -120,8 +160,12 @@ export declare class InterconnectService extends Service {
|
|
|
120
160
|
* when the sender asked and this receiver allows it.
|
|
121
161
|
*/
|
|
122
162
|
private deliver;
|
|
123
|
-
/**
|
|
124
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Inbound `reply` dispatch: forward a request to {link reply}, resolving the
|
|
165
|
+
* outbound sender for the named local session. Kept separate from {link
|
|
166
|
+
* deliver} so the two wire endpoints stay distinct in the request pipeline.
|
|
167
|
+
*/
|
|
168
|
+
private replyForSession;
|
|
125
169
|
private resolveToken;
|
|
126
170
|
/** Resolve the shared token for an outbound dial (same source as inbound). */
|
|
127
171
|
resolveTokenForDial(): Promise<string | undefined>;
|
|
@@ -135,6 +179,8 @@ export declare class InterconnectService extends Service {
|
|
|
135
179
|
attachSocket(websocket: WebSocket): void;
|
|
136
180
|
/** Parse and route one inbound link frame, attributing events to the socket's announced peer. */
|
|
137
181
|
private handleFrame;
|
|
182
|
+
/** Write one frame to a socket, returning false when the socket cannot take it. */
|
|
183
|
+
private sendFrame;
|
|
138
184
|
}
|
|
139
185
|
export default InterconnectService;
|
|
140
186
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/interconnect/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/interconnect/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -29,6 +29,22 @@ export declare const INTERCONNECT_TOKEN_REF = "DSH_INTERCONNECT_TOKEN";
|
|
|
29
29
|
* an idle agent, so it can sit unread until something else wakes it.
|
|
30
30
|
*/
|
|
31
31
|
export type DeliveryMode = 'followup' | 'steer' | 'inject';
|
|
32
|
+
/**
|
|
33
|
+
* Self-reported identity of the peer that sent a message, carried for reply
|
|
34
|
+
* attribution and echoed `sessionId` of the sending instance. Address-free on
|
|
35
|
+
* purpose: every delivery goes over an already-established WebSocket link, so
|
|
36
|
+
* the receiver addresses a `reply` by recalling the sender's `instanceId` and
|
|
37
|
+
* using its OWN outbound link to that instance — no origin crosses the wire.
|
|
38
|
+
* Both fields are sender-supplied and self-reported; the receiver stores them
|
|
39
|
+
* only to address a `reply`, never as an auth or routing authority (the shared
|
|
40
|
+
* secret still authenticates the connection itself).
|
|
41
|
+
*/
|
|
42
|
+
export interface SenderIdentity {
|
|
43
|
+
/** The sender's self-reported instance id (diagnostic, as in `ping`). */
|
|
44
|
+
readonly instanceId: string;
|
|
45
|
+
/** Session id on the sender's instance to which a `reply` is addressed. */
|
|
46
|
+
readonly sessionId: string;
|
|
47
|
+
}
|
|
32
48
|
/**
|
|
33
49
|
* Business payload for the `send` endpoint: deliver one text message to one
|
|
34
50
|
* live session of the receiving instance.
|
|
@@ -38,6 +54,12 @@ export interface SendPayload {
|
|
|
38
54
|
readonly sessionId: string;
|
|
39
55
|
/** Message text delivered to the peer session's inbox. */
|
|
40
56
|
readonly text: string;
|
|
57
|
+
/**
|
|
58
|
+
* The sender's identity, so the receiver can attribute the message and
|
|
59
|
+
* address a reply back. Optional for backward compatibility: a peer running
|
|
60
|
+
* an older version omits it, and the receiver simply has nothing to reply to.
|
|
61
|
+
*/
|
|
62
|
+
readonly sender?: SenderIdentity;
|
|
41
63
|
/**
|
|
42
64
|
* Per-message override of the receiver's configured delivery mode. Urgency is
|
|
43
65
|
* a property of one message, not of the link, so a sender may ask to cut into
|
|
@@ -72,8 +94,12 @@ export interface SendPayload {
|
|
|
72
94
|
* - `session-owned-by-subagent` — the session is reserved to subagent routing,
|
|
73
95
|
* so its parent agent owns delivery. Injecting here would race that parent;
|
|
74
96
|
* the sender must reach the child through its parent instead.
|
|
97
|
+
* - `no-sender-known` — a `reply` was addressed to a local session that never
|
|
98
|
+
* recorded a sender, either because that session did not receive a message
|
|
99
|
+
* through this service or the incoming message carried no `sender` identity
|
|
100
|
+
* (an older peer omitted it).
|
|
75
101
|
*/
|
|
76
|
-
export type SendFailure = 'session-not-live' | 'unreachable' | 'resume-refused' | 'resume-failed' | 'session-owned-by-subagent';
|
|
102
|
+
export type SendFailure = 'session-not-live' | 'unreachable' | 'resume-refused' | 'resume-failed' | 'session-owned-by-subagent' | 'no-sender-known';
|
|
77
103
|
/** Business result of a `send` endpoint call. */
|
|
78
104
|
export interface SendResult {
|
|
79
105
|
/** True when the target session is live on the receiving instance and the message was delivered. */
|
|
@@ -123,22 +149,57 @@ export interface ListResult {
|
|
|
123
149
|
/** Echoed receiver instance id (diagnostic; never trusted for routing). */
|
|
124
150
|
readonly instance: string;
|
|
125
151
|
}
|
|
126
|
-
/** Outbound listing request:
|
|
152
|
+
/** Outbound listing request: which peer instance to list. */
|
|
127
153
|
export interface ListRequest {
|
|
128
|
-
/**
|
|
129
|
-
readonly
|
|
154
|
+
/** Peers' `instanceId` as configured under {@link Config.peers}. */
|
|
155
|
+
readonly instanceId: string;
|
|
130
156
|
}
|
|
131
|
-
/** Outbound delivery request:
|
|
157
|
+
/** Outbound delivery request: which peer instance plus a `send` business payload. */
|
|
132
158
|
export interface SendRequest {
|
|
133
|
-
/**
|
|
134
|
-
|
|
159
|
+
/**
|
|
160
|
+
* The peer instance to deliver to, as configured under {@link Config.peers}.
|
|
161
|
+
* The origin used to reach it comes from that instance's own link.
|
|
162
|
+
*/
|
|
163
|
+
readonly instanceId: string;
|
|
135
164
|
readonly sessionId: string;
|
|
136
165
|
readonly text: string;
|
|
166
|
+
/**
|
|
167
|
+
* This sender's identity, forwarded so the receiver can attribute the
|
|
168
|
+
* message and address a reply back.
|
|
169
|
+
*/
|
|
170
|
+
readonly sender?: SenderIdentity;
|
|
137
171
|
/** Per-message delivery override forwarded to the receiver. */
|
|
138
172
|
readonly delivery?: DeliveryMode;
|
|
139
173
|
/** Ask the receiver to wake a persisted session; see {@link SendPayload.resume}. */
|
|
140
174
|
readonly resume?: boolean;
|
|
141
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Business payload for the `reply` endpoint: deliver one text message back to
|
|
178
|
+
* the peer that a given local session most recently received a message from.
|
|
179
|
+
* `sessionId` names the LOCAL session; the outbound target is the sender this
|
|
180
|
+
* session recorded, so the caller does not need to know the peer's origin or
|
|
181
|
+
* session id again.
|
|
182
|
+
*/
|
|
183
|
+
export interface ReplyPayload {
|
|
184
|
+
/** Local session id that received the message being replied to. */
|
|
185
|
+
readonly sessionId: string;
|
|
186
|
+
/** Message text delivered back to the recorded sender's session. */
|
|
187
|
+
readonly text: string;
|
|
188
|
+
/** Per-message delivery override forwarded to the recalled sender. */
|
|
189
|
+
readonly delivery?: DeliveryMode;
|
|
190
|
+
/** Ask the recalled sender's receiver to wake a persisted session. */
|
|
191
|
+
readonly resume?: boolean;
|
|
192
|
+
}
|
|
193
|
+
/** Outbound reply request: which local session is replying, plus the text. */
|
|
194
|
+
export interface ReplyRequest {
|
|
195
|
+
/** Local session id that received the message being replied to. */
|
|
196
|
+
readonly sessionId: string;
|
|
197
|
+
readonly text: string;
|
|
198
|
+
/** Per-message delivery override forwarded to the recalled sender. */
|
|
199
|
+
readonly delivery?: DeliveryMode;
|
|
200
|
+
/** Ask the recalled sender's receiver to wake a persisted session. */
|
|
201
|
+
readonly resume?: boolean;
|
|
202
|
+
}
|
|
142
203
|
/**
|
|
143
204
|
* One serializable lifecycle fact pushed from an emitting instance to its
|
|
144
205
|
* peers. `kind` discriminates the union; the remaining fields are the compact
|
|
@@ -180,8 +241,10 @@ export interface EventPayload {
|
|
|
180
241
|
/**
|
|
181
242
|
* One text frame exchanged over a persistent WebSocket peer link. `hello`
|
|
182
243
|
* opens a link with the dialing side's identity; `event` pushes one
|
|
183
|
-
* notification in either direction
|
|
184
|
-
*
|
|
244
|
+
* notification in either direction; `msg`/`msg-result` carry a
|
|
245
|
+
* request/response message delivery over the same long-lived link (correlated
|
|
246
|
+
* by `reqId`). Native ws ping/pong carries the heartbeat, so no application
|
|
247
|
+
* keepalive frame exists.
|
|
185
248
|
*/
|
|
186
249
|
export type LinkFrame = {
|
|
187
250
|
readonly type: 'hello';
|
|
@@ -189,6 +252,59 @@ export type LinkFrame = {
|
|
|
189
252
|
} | {
|
|
190
253
|
readonly type: 'event';
|
|
191
254
|
readonly notification: EventNotification;
|
|
255
|
+
} | {
|
|
256
|
+
readonly type: 'msg';
|
|
257
|
+
readonly reqId: string;
|
|
258
|
+
readonly message: LinkMessage;
|
|
259
|
+
} | {
|
|
260
|
+
readonly type: 'msg-result';
|
|
261
|
+
readonly reqId: string;
|
|
262
|
+
readonly result: SendResult;
|
|
263
|
+
} | {
|
|
264
|
+
readonly type: 'query';
|
|
265
|
+
readonly reqId: string;
|
|
266
|
+
readonly query: QueryMessage;
|
|
267
|
+
} | {
|
|
268
|
+
readonly type: 'query-result';
|
|
269
|
+
readonly reqId: string;
|
|
270
|
+
readonly result: unknown;
|
|
271
|
+
};
|
|
272
|
+
/**
|
|
273
|
+
* A request/response query carried by `query`/`query-result` frames, used for
|
|
274
|
+
* the discovery endpoints (`ping`, `list`) over the persistent link. `ping`
|
|
275
|
+
* answers the peer's identity; `list` answers the peer's live session rows.
|
|
276
|
+
*/
|
|
277
|
+
export type QueryMessage = {
|
|
278
|
+
readonly kind: 'ping';
|
|
279
|
+
} | {
|
|
280
|
+
readonly kind: 'list';
|
|
281
|
+
} | {
|
|
282
|
+
readonly kind: 'event';
|
|
283
|
+
readonly notification: EventNotification;
|
|
284
|
+
};
|
|
285
|
+
/**
|
|
286
|
+
* The message-shaped business payload carried by a `msg` link frame. `kind`
|
|
287
|
+
* picks the delivery semantics: `send` targets the named remote session,
|
|
288
|
+
* `reply` targets the sender a local session recorded. One tagged union keeps
|
|
289
|
+
* the frame count down while the receiver dispatches on `kind`.
|
|
290
|
+
*/
|
|
291
|
+
export type LinkMessage = {
|
|
292
|
+
readonly kind: 'send';
|
|
293
|
+
readonly sessionId: string;
|
|
294
|
+
readonly text: string;
|
|
295
|
+
/** Delivered and attributed upstream exactly as in `SendPayload`. */
|
|
296
|
+
readonly sender?: SenderIdentity;
|
|
297
|
+
readonly delivery?: DeliveryMode;
|
|
298
|
+
readonly resume?: boolean;
|
|
299
|
+
} | {
|
|
300
|
+
readonly kind: 'reply';
|
|
301
|
+
/** LOCAL session id that received the message being replied to. */
|
|
302
|
+
readonly sessionId: string;
|
|
303
|
+
readonly text: string;
|
|
304
|
+
/** Attribution for the replying instance, so the peer can reply back. */
|
|
305
|
+
readonly sender?: SenderIdentity;
|
|
306
|
+
readonly delivery?: DeliveryMode;
|
|
307
|
+
readonly resume?: boolean;
|
|
192
308
|
};
|
|
193
309
|
/**
|
|
194
310
|
* Handle to one established outbound WebSocket peer link. `close` tears the
|
|
@@ -207,11 +323,19 @@ export interface Config {
|
|
|
207
323
|
/** Request timeout for outbound deliveries, in milliseconds. */
|
|
208
324
|
readonly requestTimeoutMs: number;
|
|
209
325
|
/**
|
|
210
|
-
* Peer instance
|
|
211
|
-
*
|
|
212
|
-
*
|
|
326
|
+
* Peer instance routes, keyed by the peer's `instanceId`, valued by the
|
|
327
|
+
* origin this instance dials to reach that peer (e.g. a tunnel endpoint
|
|
328
|
+
* `http://127.0.0.1:13080`). At activation each peer is linked over a
|
|
329
|
+
* persistent WebSocket (heartbeat + reconnect), and every outbound
|
|
330
|
+
* `send`/`reply`/`ping`/`list` is addressed by `instanceId` through that
|
|
331
|
+
* instance's own link. An origin is the ONLY routing authority — `instanceId`
|
|
332
|
+
* is never used to derive an address.
|
|
333
|
+
*
|
|
334
|
+
* Fan-out of local lifecycle events also goes to every peer in this map.
|
|
335
|
+
* A runtime `subscribe(instanceId, origin)` can extend the map without
|
|
336
|
+
* restart; retuning the origin of an existing peer re-routes it.
|
|
213
337
|
*/
|
|
214
|
-
readonly peers?: string
|
|
338
|
+
readonly peers?: Record<string, string>;
|
|
215
339
|
/**
|
|
216
340
|
* Default mode for inbound `send` messages that carry no per-message
|
|
217
341
|
* override. See {@link DeliveryMode} for what each mode does.
|