dsh-interconnect 0.11.24 → 0.15.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 +177 -31
- package/assets/dsh-interconnect.md +60 -24
- package/dsh.plugin.json +1 -1
- package/lib/index.js +85 -28
- package/lib/interconnect/index.js +85 -28
- package/lib/interconnect/index.js.map +3 -3
- package/lib/tool-interconnect/index.js +163 -42
- package/lib/tool-interconnect/index.js.map +3 -3
- package/lib/types/interconnect/index.d.ts +29 -7
- package/lib/types/interconnect/index.d.ts.map +1 -1
- package/lib/types/interconnect/types.d.ts +54 -14
- package/lib/types/interconnect/types.d.ts.map +1 -1
- package/lib/types/tool-interconnect/index.d.ts +1 -1
- package/lib/types/tool-interconnect/index.d.ts.map +1 -1
- package/package.json +1 -7
- package/lib/interconnect/invariant.js +0 -25
- package/lib/interconnect/invariant.js.map +0 -7
- package/lib/skill-interconnect/invariant.js +0 -13
- package/lib/skill-interconnect/invariant.js.map +0 -7
- package/lib/tool-interconnect/invariant.js +0 -13
- package/lib/tool-interconnect/invariant.js.map +0 -7
- package/lib/types/interconnect/invariant.d.ts +0 -16
- package/lib/types/interconnect/invariant.d.ts.map +0 -1
- package/lib/types/skill-interconnect/invariant.d.ts +0 -16
- package/lib/types/skill-interconnect/invariant.d.ts.map +0 -1
- package/lib/types/tool-interconnect/invariant.d.ts +0 -16
- package/lib/types/tool-interconnect/invariant.d.ts.map +0 -1
|
@@ -8,7 +8,7 @@ import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
|
8
8
|
import { remoteErrorOf } from "@deepseek-ai/dsh-typert-protocol";
|
|
9
9
|
import { boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
10
10
|
|
|
11
|
-
//
|
|
11
|
+
// ../dsh-wt-ic-merge/vendor/cosmokit/src/misc.ts
|
|
12
12
|
function isNullable(value) {
|
|
13
13
|
return value === null || value === void 0;
|
|
14
14
|
}
|
|
@@ -30,7 +30,7 @@ function pick(source, keys, forced) {
|
|
|
30
30
|
return result;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
//
|
|
33
|
+
// ../dsh-wt-ic-merge/vendor/cosmokit/src/types.ts
|
|
34
34
|
function is(type, value) {
|
|
35
35
|
if (arguments.length === 1) return (value2) => is(type, value2);
|
|
36
36
|
return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
|
|
@@ -139,7 +139,7 @@ function deepEqual(a, b, strict) {
|
|
|
139
139
|
}) ?? Object.keys({ ...a, ...b }).every((key) => deepEqual(a[key], b[key], strict));
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
//
|
|
142
|
+
// ../dsh-wt-ic-merge/vendor/cosmokit/src/time.ts
|
|
143
143
|
var Time;
|
|
144
144
|
((Time2) => {
|
|
145
145
|
Time2.millisecond = 1;
|
|
@@ -219,7 +219,7 @@ var Time;
|
|
|
219
219
|
Time2.template = template;
|
|
220
220
|
})(Time || (Time = {}));
|
|
221
221
|
|
|
222
|
-
//
|
|
222
|
+
// ../dsh-wt-ic-merge/vendor/schemastery/lib/index.mjs
|
|
223
223
|
var kSchema = Symbol.for("schemastery");
|
|
224
224
|
var kValidationError = Symbol.for("ValidationError");
|
|
225
225
|
globalThis.__schemastery_index__ ??= 0;
|
|
@@ -823,12 +823,8 @@ defineMethod("transform", [
|
|
|
823
823
|
|
|
824
824
|
// src/interconnect/index.ts
|
|
825
825
|
import WebSocket, { WebSocketServer } from "ws";
|
|
826
|
-
function isSessionOwnedBySubagent(
|
|
827
|
-
|
|
828
|
-
const parentId = session.header.parentSession;
|
|
829
|
-
if (parentId === void 0 || agent === void 0) return false;
|
|
830
|
-
const parent = ctx.agents.get(parentId);
|
|
831
|
-
return parent !== void 0 && ctx.agents.isOwnedBy(agent.id, parent);
|
|
826
|
+
function isSessionOwnedBySubagent(session) {
|
|
827
|
+
return session.header.origin === "subagent";
|
|
832
828
|
}
|
|
833
829
|
function assertNever(value) {
|
|
834
830
|
throw new Error(`interconnect: unhandled variant ${JSON.stringify(value)}`);
|
|
@@ -839,6 +835,7 @@ var DEFAULT_INSTANCE_ID = "dsh";
|
|
|
839
835
|
var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
|
|
840
836
|
var LINK_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
841
837
|
var LINK_CHANNEL = "/interconnect/link";
|
|
838
|
+
var SELF_PEER_ORIGIN = "self";
|
|
842
839
|
var MAX_INSTANCE_ID_CHARS = 256;
|
|
843
840
|
var MAX_REQUEST_ID_CHARS = 256;
|
|
844
841
|
var MAX_LISTED_SESSIONS = 100;
|
|
@@ -966,11 +963,19 @@ var InterconnectService = class extends Service {
|
|
|
966
963
|
this.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
967
964
|
this.delivery = config.delivery ?? "followup";
|
|
968
965
|
this.allowResume = config.allowResume ?? true;
|
|
969
|
-
for (const origin of Object.
|
|
966
|
+
for (const [peerInstanceId, origin] of Object.entries(config.peers ?? {})) {
|
|
967
|
+
if (origin === SELF_PEER_ORIGIN) {
|
|
968
|
+
if (peerInstanceId !== this.instanceId) {
|
|
969
|
+
throw new Error(
|
|
970
|
+
`interconnect: peer "${peerInstanceId}" is configured as self, but this instance announces "${this.instanceId}"; key a self peer by the instanceId`
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
970
975
|
linkUrl(trimBase(origin));
|
|
971
976
|
}
|
|
972
977
|
for (const [peerInstanceId, origin] of Object.entries(config.peers ?? {})) {
|
|
973
|
-
this.link(peerInstanceId, origin);
|
|
978
|
+
this.link(peerInstanceId, origin === SELF_PEER_ORIGIN ? () => this.selfOrigin() : origin);
|
|
974
979
|
}
|
|
975
980
|
const upgrade = {
|
|
976
981
|
path: LINK_CHANNEL,
|
|
@@ -1111,7 +1116,7 @@ var InterconnectService = class extends Service {
|
|
|
1111
1116
|
* Returns the peer identity when reachable, or undefined when the link is
|
|
1112
1117
|
* not up or no answer arrives.
|
|
1113
1118
|
* @param instanceId - the peer's `instanceId` as configured under {@link Config.peers}.
|
|
1114
|
-
* @returns the peer's identity, or undefined when
|
|
1119
|
+
* @returns the peer's identity, or undefined when there is no usable answer.
|
|
1115
1120
|
*/
|
|
1116
1121
|
async ping(instanceId) {
|
|
1117
1122
|
return this.queryRequest(instanceId, { kind: "ping" });
|
|
@@ -1125,16 +1130,33 @@ var InterconnectService = class extends Service {
|
|
|
1125
1130
|
async list(instanceId) {
|
|
1126
1131
|
return this.queryRequest(instanceId, { kind: "list" });
|
|
1127
1132
|
}
|
|
1133
|
+
/**
|
|
1134
|
+
* The peers this instance configures, each with the current state of the link
|
|
1135
|
+
* it dials, in configuration order. This is how a caller tells an id this
|
|
1136
|
+
* instance cannot reach at all (`peer-not-configured`) from one whose link is
|
|
1137
|
+
* merely down (`unreachable`), which the bare `undefined` of `ping`/`list`
|
|
1138
|
+
* cannot express. A configured peer whose link is `down` is still a valid
|
|
1139
|
+
* target: its reconnect runs on its own backoff, so it may answer later.
|
|
1140
|
+
* @returns one row per configured peer.
|
|
1141
|
+
*/
|
|
1142
|
+
peers() {
|
|
1143
|
+
return [...this.linkStates].map(([instanceId, state]) => ({
|
|
1144
|
+
instanceId,
|
|
1145
|
+
origin: state.peer,
|
|
1146
|
+
link: state.writable() ? "up" : "down"
|
|
1147
|
+
}));
|
|
1148
|
+
}
|
|
1128
1149
|
/**
|
|
1129
1150
|
* Send a `msg` frame over the live link to a peer and resolve with its
|
|
1130
|
-
* correlated `msg-result`.
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1151
|
+
* correlated `msg-result`. A `peer-not-configured` result when this instance
|
|
1152
|
+
* configures no route for that id, undefined when the route exists but its
|
|
1153
|
+
* link is down, and a `message-too-large` result when the encoded frame would
|
|
1154
|
+
* not fit the link.
|
|
1134
1155
|
*/
|
|
1135
1156
|
async msgRequest(instanceId, kind, payload) {
|
|
1136
1157
|
const state = this.linkStates.get(instanceId);
|
|
1137
|
-
if (state === void 0
|
|
1158
|
+
if (state === void 0) return { delivered: false, instance: instanceId, reason: "peer-not-configured" };
|
|
1159
|
+
if (!state.writable()) return void 0;
|
|
1138
1160
|
const reqId = `m${++this.reqIdCounter}-${randomUUID()}`;
|
|
1139
1161
|
const frame = {
|
|
1140
1162
|
type: "msg",
|
|
@@ -1196,7 +1218,8 @@ var InterconnectService = class extends Service {
|
|
|
1196
1218
|
* `Config.peers` entry; the resulting state owns reconnection for the life of
|
|
1197
1219
|
* the service fiber.
|
|
1198
1220
|
* @param instanceId - the peer's `instanceId`.
|
|
1199
|
-
* @param origin - receiver origin this instance dials
|
|
1221
|
+
* @param origin - receiver origin this instance dials (e.g.
|
|
1222
|
+
* `http://127.0.0.1:13080`), or a resolver for a `self` route.
|
|
1200
1223
|
*/
|
|
1201
1224
|
link(instanceId, origin) {
|
|
1202
1225
|
const state = new LinkState(
|
|
@@ -1206,11 +1229,28 @@ var InterconnectService = class extends Service {
|
|
|
1206
1229
|
this.ctx.logger,
|
|
1207
1230
|
() => this.resolveToken(),
|
|
1208
1231
|
instanceId,
|
|
1209
|
-
trimBase(origin)
|
|
1232
|
+
typeof origin === "string" ? trimBase(origin) : origin
|
|
1210
1233
|
);
|
|
1211
1234
|
this.linkStates.set(instanceId, state);
|
|
1212
1235
|
state.dial();
|
|
1213
1236
|
}
|
|
1237
|
+
/**
|
|
1238
|
+
* The origin of this instance's own host webserver, or undefined while no
|
|
1239
|
+
* listening port exists. Read on every dial rather than cached: the
|
|
1240
|
+
* webserver may activate after this service, its port is assigned only in
|
|
1241
|
+
* its listen callback (`--port 0` picks a fresh one per start), and a
|
|
1242
|
+
* replacement webserver would move it again. Loopback is used regardless of
|
|
1243
|
+
* the bind host: a server bound to `0.0.0.0` also accepts `127.0.0.1`, while
|
|
1244
|
+
* `0.0.0.0` itself is a bind address, not a dialable origin.
|
|
1245
|
+
* @returns `http://127.0.0.1:<port>`, or undefined without a listening server.
|
|
1246
|
+
*/
|
|
1247
|
+
selfOrigin() {
|
|
1248
|
+
const server = this.ctx.get("webServer");
|
|
1249
|
+
if (server === void 0) return void 0;
|
|
1250
|
+
const port = server.port;
|
|
1251
|
+
if (typeof port !== "number" || port <= 0) return void 0;
|
|
1252
|
+
return `http://127.0.0.1:${String(port)}`;
|
|
1253
|
+
}
|
|
1214
1254
|
/** Push one serialized lifecycle fact out to every linked peer over WS. */
|
|
1215
1255
|
fanout(notification) {
|
|
1216
1256
|
this.broadcast(notification);
|
|
@@ -1345,7 +1385,7 @@ var InterconnectService = class extends Service {
|
|
|
1345
1385
|
* @returns the bounded listing.
|
|
1346
1386
|
*/
|
|
1347
1387
|
listSessions(rowBudgetBytes) {
|
|
1348
|
-
const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(
|
|
1388
|
+
const reachable = this.ctx.agents.list().filter((agent) => !isSessionOwnedBySubagent(agent.session));
|
|
1349
1389
|
const sessions = [];
|
|
1350
1390
|
let used = 0;
|
|
1351
1391
|
for (const agent of reachable.slice(0, MAX_LISTED_SESSIONS)) {
|
|
@@ -1372,7 +1412,8 @@ var InterconnectService = class extends Service {
|
|
|
1372
1412
|
let title;
|
|
1373
1413
|
try {
|
|
1374
1414
|
const snapshot = this.ctx.get("sessionProjections")?.snapshot(agent.session);
|
|
1375
|
-
const
|
|
1415
|
+
const values = snapshot?.values;
|
|
1416
|
+
const value = values?.title;
|
|
1376
1417
|
if (typeof value === "string" && value !== "") title = value;
|
|
1377
1418
|
} catch {
|
|
1378
1419
|
title = void 0;
|
|
@@ -1430,7 +1471,7 @@ var InterconnectService = class extends Service {
|
|
|
1430
1471
|
}
|
|
1431
1472
|
agent = woken.agent;
|
|
1432
1473
|
}
|
|
1433
|
-
if (isSessionOwnedBySubagent(
|
|
1474
|
+
if (isSessionOwnedBySubagent(agent.session)) {
|
|
1434
1475
|
return { delivered: false, instance: this.instanceId, reason: "session-owned-by-subagent" };
|
|
1435
1476
|
}
|
|
1436
1477
|
const sender = (() => {
|
|
@@ -1639,24 +1680,40 @@ var LinkState = class {
|
|
|
1639
1680
|
this.logger = logger;
|
|
1640
1681
|
this.resolveToken = resolveToken;
|
|
1641
1682
|
this.instanceId = instanceId;
|
|
1642
|
-
this.
|
|
1683
|
+
this.origin = origin;
|
|
1643
1684
|
}
|
|
1644
1685
|
dialed;
|
|
1645
1686
|
closed = false;
|
|
1646
1687
|
retry = 0;
|
|
1647
1688
|
reconnectTimer;
|
|
1648
1689
|
warnedNoToken = false;
|
|
1649
|
-
|
|
1650
|
-
peer;
|
|
1690
|
+
warnedNoOrigin = false;
|
|
1651
1691
|
instanceId;
|
|
1692
|
+
origin;
|
|
1693
|
+
/** The origin this link dials now; for an unresolved `self` route, the sentinel is shown. */
|
|
1694
|
+
get peer() {
|
|
1695
|
+
return typeof this.origin === "string" ? this.origin : this.origin() ?? SELF_PEER_ORIGIN;
|
|
1696
|
+
}
|
|
1652
1697
|
/** Open the socket; reconnect is scheduled by the close handler. */
|
|
1653
1698
|
dial() {
|
|
1654
1699
|
if (this.closed) return;
|
|
1700
|
+
const origin = typeof this.origin === "string" ? this.origin : this.origin();
|
|
1701
|
+
if (origin === void 0) {
|
|
1702
|
+
if (this.retry >= 1 && !this.warnedNoOrigin) {
|
|
1703
|
+
this.warnedNoOrigin = true;
|
|
1704
|
+
this.logger.warn(
|
|
1705
|
+
`interconnect: peer ${this.instanceId} is configured as self but this instance has no listening webServer port yet; the link stays down and retries`
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
this.scheduleReconnect();
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
this.warnedNoOrigin = false;
|
|
1655
1712
|
let url;
|
|
1656
1713
|
try {
|
|
1657
|
-
url = linkUrl(
|
|
1714
|
+
url = linkUrl(origin);
|
|
1658
1715
|
} catch {
|
|
1659
|
-
this.logger.warn(`interconnect: invalid peer origin ${
|
|
1716
|
+
this.logger.warn(`interconnect: invalid peer origin ${origin}; link to ${this.instanceId} stays down`);
|
|
1660
1717
|
return;
|
|
1661
1718
|
}
|
|
1662
1719
|
void this.resolveToken().then((token) => {
|
|
@@ -1788,11 +1845,13 @@ var deliveryOutcome = (result) => ({
|
|
|
1788
1845
|
...result.delivery === void 0 ? {} : { delivery: result.delivery },
|
|
1789
1846
|
...result.reason === void 0 ? {} : { reason: result.reason }
|
|
1790
1847
|
});
|
|
1848
|
+
var configures = (interconnect, instanceId) => interconnect.peers().some((peer) => peer.instanceId === instanceId);
|
|
1849
|
+
var unansweredText = (instanceId, configured) => configured === false ? `not configured: this instance has no peer "${instanceId}" \u2014 add it to the interconnect peers mapping (interconnect_peers lists what is configured)` : `unreachable: peer "${instanceId}" gave no usable answer (its link is down, or the shared token differs); retrying may succeed`;
|
|
1791
1850
|
function apply(ctx) {
|
|
1792
1851
|
const interconnect = ctx.interconnect;
|
|
1793
1852
|
ctx.tools.register(defineTool({
|
|
1794
1853
|
name: "interconnect_send",
|
|
1795
|
-
description: 'Deliver one text message to a
|
|
1854
|
+
description: 'Deliver one text message to a session on another DSH instance (same machine, another machine, or another session). Returns whether the peer instance received it and which instance answered. A live session receives directly; a persisted session with no live agent is woken by default (resume), and when delivery fails the result reports the reason (for example "session-not-live" or "resume-refused"). The sending instance and session ids are attached automatically, so the receiver can reply with interconnect_reply; do not pass a sender parameter.',
|
|
1796
1855
|
parameters: {
|
|
1797
1856
|
instanceId: {
|
|
1798
1857
|
type: "string",
|
|
@@ -1816,7 +1875,7 @@ function apply(ctx) {
|
|
|
1816
1875
|
},
|
|
1817
1876
|
resume: {
|
|
1818
1877
|
type: "boolean",
|
|
1819
|
-
description: `Wake the target session if it is persisted but not live.
|
|
1878
|
+
description: `Wake the target session if it is persisted but not live. Defaults to true: waking starts a real agent turn \u2014 a billed model call with that session's full toolset \u2014 on the receiver, which is what makes an offline target reachable. Set false to leave it asleep and report reason "session-not-live". The receiver may still refuse, answering reason "resume-refused".`
|
|
1820
1879
|
}
|
|
1821
1880
|
},
|
|
1822
1881
|
output: {
|
|
@@ -1830,6 +1889,8 @@ function apply(ctx) {
|
|
|
1830
1889
|
}
|
|
1831
1890
|
const text = (() => {
|
|
1832
1891
|
switch (value.reason) {
|
|
1892
|
+
case "peer-not-configured":
|
|
1893
|
+
return `not delivered: this instance configures no peer "${value.instance}" \u2014 add it to the interconnect peers mapping (interconnect_peers lists what is configured)`;
|
|
1833
1894
|
case "unreachable":
|
|
1834
1895
|
return `not delivered: ${value.instance} did not answer (unreachable); retrying may succeed`;
|
|
1835
1896
|
case "resume-refused":
|
|
@@ -1841,7 +1902,7 @@ function apply(ctx) {
|
|
|
1841
1902
|
case "message-too-large":
|
|
1842
1903
|
return "not delivered: the message is too large for the peer link (limit 1 MiB); shorten the text and try again";
|
|
1843
1904
|
default:
|
|
1844
|
-
return `not delivered: no live session "${_args.sessionId}" on ${value.instance}` + (_args.resume ===
|
|
1905
|
+
return `not delivered: no live session "${_args.sessionId}" on ${value.instance}` + (_args.resume === false ? " \u2014 waking was disabled (resume: false); the message was not delivered to any other session" : " \u2014 it could not be woken there (the receiver has no resuming agent lookup); the message was not delivered to any other session");
|
|
1845
1906
|
}
|
|
1846
1907
|
})();
|
|
1847
1908
|
return [{ type: "text", text }];
|
|
@@ -1859,7 +1920,9 @@ function apply(ctx) {
|
|
|
1859
1920
|
// as a reply target.
|
|
1860
1921
|
...sessionId === void 0 ? {} : { sender: interconnect.selfSender(String(sessionId)) },
|
|
1861
1922
|
...args.delivery === void 0 ? {} : { delivery: args.delivery },
|
|
1862
|
-
|
|
1923
|
+
// Waking is the default: a persisted target with no live agent is
|
|
1924
|
+
// otherwise unreachable, and a caller that wants it left asleep passes false.
|
|
1925
|
+
resume: args.resume ?? true
|
|
1863
1926
|
}));
|
|
1864
1927
|
return deliveryOutcome(result);
|
|
1865
1928
|
}
|
|
@@ -1880,17 +1943,20 @@ function apply(ctx) {
|
|
|
1880
1943
|
additionalProperties: false,
|
|
1881
1944
|
properties: {
|
|
1882
1945
|
reachable: { type: "boolean", required: true },
|
|
1883
|
-
instance: { type: "string" }
|
|
1946
|
+
instance: { type: "string" },
|
|
1947
|
+
// Whether this instance configures that peer at all: absent/false means
|
|
1948
|
+
// the id cannot be reached by any retry, true means the link is down.
|
|
1949
|
+
configured: { type: "boolean" }
|
|
1884
1950
|
}
|
|
1885
1951
|
},
|
|
1886
|
-
render: (
|
|
1952
|
+
render: (args, value) => [{
|
|
1887
1953
|
type: "text",
|
|
1888
|
-
text: value.reachable ? `reachable: ${value.instance ?? "(unknown instance)"}` :
|
|
1954
|
+
text: value.reachable ? `reachable: ${value.instance ?? "(unknown instance)"}` : unansweredText(args.instanceId, value.configured)
|
|
1889
1955
|
}]
|
|
1890
1956
|
},
|
|
1891
1957
|
async execute(args, exec) {
|
|
1892
1958
|
const result = await raceSignal(exec.signal, interconnect.ping(args.instanceId));
|
|
1893
|
-
if (result === void 0) return { reachable: false };
|
|
1959
|
+
if (result === void 0) return { reachable: false, configured: configures(interconnect, args.instanceId) };
|
|
1894
1960
|
return { reachable: true, instance: result.instance };
|
|
1895
1961
|
}
|
|
1896
1962
|
}));
|
|
@@ -1911,6 +1977,8 @@ function apply(ctx) {
|
|
|
1911
1977
|
properties: {
|
|
1912
1978
|
reachable: { type: "boolean", required: true },
|
|
1913
1979
|
instance: { type: "string" },
|
|
1980
|
+
// Whether this instance configures that peer at all, as in interconnect_ping.
|
|
1981
|
+
configured: { type: "boolean" },
|
|
1914
1982
|
sessions: {
|
|
1915
1983
|
type: "array",
|
|
1916
1984
|
items: {
|
|
@@ -1925,8 +1993,8 @@ function apply(ctx) {
|
|
|
1925
1993
|
}
|
|
1926
1994
|
}
|
|
1927
1995
|
},
|
|
1928
|
-
render: (
|
|
1929
|
-
if (!value.reachable) return [{ type: "text", text:
|
|
1996
|
+
render: (args, value) => {
|
|
1997
|
+
if (!value.reachable) return [{ type: "text", text: unansweredText(args.instanceId, value.configured) }];
|
|
1930
1998
|
const sessions = value.sessions ?? [];
|
|
1931
1999
|
if (sessions.length === 0) {
|
|
1932
2000
|
return [{ type: "text", text: `no live sessions on ${value.instance ?? "(unknown instance)"}` }];
|
|
@@ -1944,7 +2012,7 @@ function apply(ctx) {
|
|
|
1944
2012
|
},
|
|
1945
2013
|
async execute(args, exec) {
|
|
1946
2014
|
const result = await raceSignal(exec.signal, interconnect.list(args.instanceId));
|
|
1947
|
-
if (result === void 0) return { reachable: false };
|
|
2015
|
+
if (result === void 0) return { reachable: false, configured: configures(interconnect, args.instanceId) };
|
|
1948
2016
|
return {
|
|
1949
2017
|
reachable: true,
|
|
1950
2018
|
instance: result.instance,
|
|
@@ -1958,6 +2026,55 @@ function apply(ctx) {
|
|
|
1958
2026
|
};
|
|
1959
2027
|
}
|
|
1960
2028
|
}));
|
|
2029
|
+
ctx.tools.register(defineTool({
|
|
2030
|
+
name: "interconnect_peers",
|
|
2031
|
+
description: `List the peer DSH instances this instance is configured to reach, with each peer's dialed link state. Call this first when you do not already know the peer ids available here: every listed instanceId is a valid target for interconnect_send, interconnect_ping, and interconnect_list. A peer listed with link "down" is configured but currently cannot carry frames, so it answers nothing until its reconnect succeeds; an id that is not listed at all cannot be reached and reports reason "peer-not-configured". A message that arrives from a peer this instance does not configure can be received but not replied to.`,
|
|
2032
|
+
parameters: {},
|
|
2033
|
+
output: {
|
|
2034
|
+
schema: {
|
|
2035
|
+
type: "object",
|
|
2036
|
+
additionalProperties: false,
|
|
2037
|
+
properties: {
|
|
2038
|
+
peers: {
|
|
2039
|
+
type: "array",
|
|
2040
|
+
items: {
|
|
2041
|
+
type: "object",
|
|
2042
|
+
additionalProperties: false,
|
|
2043
|
+
properties: {
|
|
2044
|
+
instanceId: { type: "string", required: true },
|
|
2045
|
+
origin: { type: "string", required: true },
|
|
2046
|
+
link: { type: "string", required: true }
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
},
|
|
2052
|
+
render: (_args, value) => {
|
|
2053
|
+
const peers = value.peers ?? [];
|
|
2054
|
+
if (peers.length === 0) {
|
|
2055
|
+
return [{
|
|
2056
|
+
type: "text",
|
|
2057
|
+
text: "no peers configured: this instance can receive messages but cannot send or reply. Add entries to the interconnect row's peers mapping (a peer id keyed to the origin this instance dials); the change applies when that row's config is re-activated."
|
|
2058
|
+
}];
|
|
2059
|
+
}
|
|
2060
|
+
return [{
|
|
2061
|
+
type: "text",
|
|
2062
|
+
text: peers.map((peer) => `${peer.instanceId} ${peer.origin} [${peer.link}]`).join("\n")
|
|
2063
|
+
}];
|
|
2064
|
+
}
|
|
2065
|
+
},
|
|
2066
|
+
execute(_args, _exec) {
|
|
2067
|
+
return Promise.resolve({
|
|
2068
|
+
peers: interconnect.peers().map((peer) => ({
|
|
2069
|
+
instanceId: peer.instanceId,
|
|
2070
|
+
// The origin is this instance's own dial target, read locally: it is
|
|
2071
|
+
// what a caller needs to diagnose a peer whose link will not come up.
|
|
2072
|
+
origin: peer.origin,
|
|
2073
|
+
link: peer.link
|
|
2074
|
+
}))
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
}));
|
|
1961
2078
|
ctx.tools.register(defineTool({
|
|
1962
2079
|
name: "interconnect_reply",
|
|
1963
2080
|
description: `Deliver one text message back to the peer that this session last received an interconnect message from. Only the reply text is needed: the replying session is this agent's own, and the outbound target (the sending instance id and session id) is recalled from the message this session received, so you do not ask for an address twice. The recalled target is the most recent sender only: a later message from another peer replaces it, and restarting this instance forgets it until a new message arrives, so name the intended recipient in the reply text when more than one peer may message this session. A session that never received a message through interconnect \u2014 or received one without a sender identity \u2014 reports reason "no-sender-known".`,
|
|
@@ -1974,7 +2091,7 @@ function apply(ctx) {
|
|
|
1974
2091
|
},
|
|
1975
2092
|
resume: {
|
|
1976
2093
|
type: "boolean",
|
|
1977
|
-
description:
|
|
2094
|
+
description: `Wake the recorded sender's session if it is persisted but not live. Defaults to true: a reply completes an interchange the sender started, so the session is woken by a real billed agent turn on the receiver. Set false to leave it asleep and report reason "session-not-live". The receiver may still refuse, answering "resume-refused".`
|
|
1978
2095
|
}
|
|
1979
2096
|
},
|
|
1980
2097
|
output: {
|
|
@@ -1996,6 +2113,9 @@ function apply(ctx) {
|
|
|
1996
2113
|
if (value.reason === "session-owned-by-subagent") {
|
|
1997
2114
|
return "not delivered: the recorded sender's session belongs to a subagent and its parent agent owns delivery";
|
|
1998
2115
|
}
|
|
2116
|
+
if (value.reason === "peer-not-configured") {
|
|
2117
|
+
return `not delivered: this instance configures no peer "${value.instance}" \u2014 the sender can be messaged, but this instance cannot dial it back until it is added to the interconnect peers mapping`;
|
|
2118
|
+
}
|
|
1999
2119
|
if (value.reason === "unreachable") {
|
|
2000
2120
|
return "not delivered: the recorded sender did not answer (unreachable); retrying may succeed";
|
|
2001
2121
|
}
|
|
@@ -2005,7 +2125,7 @@ function apply(ctx) {
|
|
|
2005
2125
|
if (value.reason === "resume-failed") {
|
|
2006
2126
|
return "not delivered: could not wake the recorded sender's session";
|
|
2007
2127
|
}
|
|
2008
|
-
return "not delivered: the recorded sender's session is not live";
|
|
2128
|
+
return "not delivered: the recorded sender's session is not live" + (_args.resume === false ? " \u2014 waking was disabled (resume: false); the reply was not sent to any other session" : " \u2014 it could not be woken there (the receiver has no resuming agent lookup); the reply was not sent to any other session");
|
|
2009
2129
|
})();
|
|
2010
2130
|
return [{ type: "text", text }];
|
|
2011
2131
|
}
|
|
@@ -2021,7 +2141,8 @@ function apply(ctx) {
|
|
|
2021
2141
|
sessionId: String(exec.agent.session.id),
|
|
2022
2142
|
text: args.text,
|
|
2023
2143
|
...args.delivery === void 0 ? {} : { delivery: args.delivery },
|
|
2024
|
-
|
|
2144
|
+
// Replying to a sender that has gone offline should still reach it.
|
|
2145
|
+
resume: args.resume ?? true
|
|
2025
2146
|
}));
|
|
2026
2147
|
return deliveryOutcome(result);
|
|
2027
2148
|
}
|