@perkos/perkos-a2a 0.12.30 → 0.12.31
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 +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +277 -36
- package/dist/index.js.map +3 -3
- package/dist/relay-client.d.ts +7 -0
- package/dist/relay-client.d.ts.map +1 -1
- package/dist/relay-client.js +89 -18
- package/dist/relay-client.js.map +1 -1
- package/dist/relay.d.ts +1 -1
- package/dist/relay.d.ts.map +1 -1
- package/dist/relay.js +21 -3
- package/dist/relay.js.map +1 -1
- package/dist/repair.d.ts +7 -2
- package/dist/repair.d.ts.map +1 -1
- package/dist/repair.js +76 -10
- package/dist/repair.js.map +1 -1
- package/dist/server.d.ts +16 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +160 -13
- package/dist/server.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -170,7 +170,7 @@ openclaw gateway restart
|
|
|
170
170
|
|
|
171
171
|
#### Official idempotent connection
|
|
172
172
|
|
|
173
|
-
Version 0.12.
|
|
173
|
+
Version 0.12.31+ ships the machine-readable `perkos-a2a connect` CLI. The
|
|
174
174
|
official PerkOS invitation supplies the artifact URL, SHA-256, agent name, and
|
|
175
175
|
agent id; it invokes the exact npm version with `npx`. The same command performs
|
|
176
176
|
a first install or repairs the matching existing identity, even when an invalid
|
|
@@ -203,7 +203,7 @@ values are never printed.
|
|
|
203
203
|
|
|
204
204
|
```bash
|
|
205
205
|
A2A_RELAY_API_KEY='<credential-from-invitation>' \
|
|
206
|
-
npx --yes @perkos/perkos-a2a@0.12.
|
|
206
|
+
npx --yes @perkos/perkos-a2a@0.12.31 connect \
|
|
207
207
|
--artifact ./perkos-a2a.tgz \
|
|
208
208
|
--sha256 '<sha256-from-the-official-invitation>' \
|
|
209
209
|
--agent-name '<agent-name>' \
|
package/dist/index.d.ts
CHANGED
|
@@ -51,5 +51,7 @@ export declare function runOpenClawChatTurn(api: any, config: A2APluginConfig, s
|
|
|
51
51
|
}): Promise<string | null>;
|
|
52
52
|
/** Build a bounded, agent-local transcript so every runtime turn has continuity. */
|
|
53
53
|
export declare function formatRecentChatContext(messages: ChatMessage[], currentMessageId: string): string;
|
|
54
|
+
/** The identity the relay treats as exclusive; names are display metadata. */
|
|
55
|
+
export declare function a2aLifecycleKey(config: A2APluginConfig): string;
|
|
54
56
|
export default function register(api: any): void;
|
|
55
57
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAYH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,KAAK,EAGV,WAAW,EAEZ,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,cAAc,YAAY,CAAC;AA0C3B,kEAAkE;AAClE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAItF;AAED,KAAK,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AASpD;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,OAAO,EACd,oBAAoB,EAAE,MAAM,GAC3B,OAAO,CA2BT;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,oBAAoB,EAC3B,eAAe,CAAC,EAAE,MAAM,GACvB,oBAAoB,CA+BtB;AAiED,KAAK,qBAAqB,GAAG;IAC3B,QAAQ,CAAC,EAAE,KAAK,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,YAAY,CAAC,EAAE,OAAO,CAAC;KACxB,CAAC,CAAC;IACH,IAAI,CAAC,EAAE;QACL,yBAAyB,CAAC,EAAE,MAAM,CAAC;QACnC,qBAAqB,CAAC,EAAE,MAAM,CAAC;KAChC,CAAC;CACH,CAAC;AAEF,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,GAAG,IAAI,CAYrF;AAQD,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,eAAe,EACvB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;CAAE,GAClF,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAuFxB;AAYD,oFAAoF;AACpF,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,WAAW,EAAE,EACvB,gBAAgB,EAAE,MAAM,GACvB,MAAM,CAWR;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAYH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,KAAK,EAGV,WAAW,EAEZ,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,cAAc,YAAY,CAAC;AA0C3B,kEAAkE;AAClE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAItF;AAED,KAAK,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AASpD;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,OAAO,EACd,oBAAoB,EAAE,MAAM,GAC3B,OAAO,CA2BT;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,oBAAoB,EAC3B,eAAe,CAAC,EAAE,MAAM,GACvB,oBAAoB,CA+BtB;AAiED,KAAK,qBAAqB,GAAG;IAC3B,QAAQ,CAAC,EAAE,KAAK,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,YAAY,CAAC,EAAE,OAAO,CAAC;KACxB,CAAC,CAAC;IACH,IAAI,CAAC,EAAE;QACL,yBAAyB,CAAC,EAAE,MAAM,CAAC;QACnC,qBAAqB,CAAC,EAAE,MAAM,CAAC;KAChC,CAAC;CACH,CAAC;AAEF,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,GAAG,IAAI,CAYrF;AAQD,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,eAAe,EACvB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;CAAE,GAClF,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAuFxB;AAYD,oFAAoF;AACpF,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,WAAW,EAAE,EACvB,gBAAgB,EAAE,MAAM,GACvB,MAAM,CAWR;AAoDD,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAe/D;AAED,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,GAAG,EAAE,GAAG,QAi2BxC"}
|
package/dist/index.js
CHANGED
|
@@ -26263,6 +26263,7 @@ var RelayClient = class {
|
|
|
26263
26263
|
heartbeatTimer = null;
|
|
26264
26264
|
connected = false;
|
|
26265
26265
|
stopped = false;
|
|
26266
|
+
generation = 0;
|
|
26266
26267
|
pendingCallbacks = /* @__PURE__ */ new Map();
|
|
26267
26268
|
/** Responses that arrived with nobody waiting, claimable by a retry. */
|
|
26268
26269
|
orphanResponses = /* @__PURE__ */ new Map();
|
|
@@ -26274,17 +26275,46 @@ var RelayClient = class {
|
|
|
26274
26275
|
return this.connected;
|
|
26275
26276
|
}
|
|
26276
26277
|
start() {
|
|
26278
|
+
if (!this.stopped && this.ws) return;
|
|
26277
26279
|
this.stopped = false;
|
|
26278
|
-
this.
|
|
26280
|
+
const generation = ++this.generation;
|
|
26281
|
+
this.connect(generation);
|
|
26279
26282
|
}
|
|
26280
26283
|
stop() {
|
|
26281
|
-
|
|
26282
|
-
|
|
26283
|
-
|
|
26284
|
-
|
|
26285
|
-
this.ws = null;
|
|
26284
|
+
const socket = this.beginStop();
|
|
26285
|
+
try {
|
|
26286
|
+
socket?.close(1e3, "Client shutting down");
|
|
26287
|
+
} catch {
|
|
26286
26288
|
}
|
|
26287
|
-
|
|
26289
|
+
}
|
|
26290
|
+
/** Stop reconnect activity and wait a bounded time for the socket to close. */
|
|
26291
|
+
async stopAndWait(timeoutMs = 5e3) {
|
|
26292
|
+
const socket = this.beginStop();
|
|
26293
|
+
if (!socket || socket.readyState === wrapper_default.CLOSED) return { closeTimedOut: false };
|
|
26294
|
+
return new Promise((resolve3) => {
|
|
26295
|
+
let settled = false;
|
|
26296
|
+
const finish = (closeTimedOut) => {
|
|
26297
|
+
if (settled) return;
|
|
26298
|
+
settled = true;
|
|
26299
|
+
clearTimeout(timer);
|
|
26300
|
+
resolve3({ closeTimedOut });
|
|
26301
|
+
};
|
|
26302
|
+
const timer = setTimeout(() => {
|
|
26303
|
+
finish(true);
|
|
26304
|
+
try {
|
|
26305
|
+
socket.terminate();
|
|
26306
|
+
} catch {
|
|
26307
|
+
}
|
|
26308
|
+
}, timeoutMs);
|
|
26309
|
+
timer.unref?.();
|
|
26310
|
+
socket.once("close", () => finish(false));
|
|
26311
|
+
socket.once("error", () => finish(false));
|
|
26312
|
+
try {
|
|
26313
|
+
socket.close(1e3, "Client shutting down");
|
|
26314
|
+
} catch {
|
|
26315
|
+
finish(false);
|
|
26316
|
+
}
|
|
26317
|
+
});
|
|
26288
26318
|
}
|
|
26289
26319
|
/**
|
|
26290
26320
|
* Send a task to another agent via the relay.
|
|
@@ -26333,8 +26363,8 @@ var RelayClient = class {
|
|
|
26333
26363
|
};
|
|
26334
26364
|
this.send(response);
|
|
26335
26365
|
}
|
|
26336
|
-
connect() {
|
|
26337
|
-
if (this.
|
|
26366
|
+
connect(generation) {
|
|
26367
|
+
if (!this.isCurrent(generation)) return;
|
|
26338
26368
|
const url = this.options.relay.url;
|
|
26339
26369
|
this.logger.info(`[perkos-a2a] Connecting to relay: ${url}`);
|
|
26340
26370
|
try {
|
|
@@ -26342,17 +26372,23 @@ var RelayClient = class {
|
|
|
26342
26372
|
} catch (err) {
|
|
26343
26373
|
const msg = err instanceof Error ? err.message : String(err);
|
|
26344
26374
|
this.logger.error(`[perkos-a2a] Failed to create WebSocket: ${msg}`);
|
|
26345
|
-
this.scheduleReconnect();
|
|
26375
|
+
this.scheduleReconnect(generation);
|
|
26346
26376
|
return;
|
|
26347
26377
|
}
|
|
26348
|
-
this.ws
|
|
26378
|
+
const socket = this.ws;
|
|
26379
|
+
socket.on("open", () => {
|
|
26380
|
+
if (!this.isCurrent(generation, socket)) {
|
|
26381
|
+
socket.close(1e3, "Stale relay generation");
|
|
26382
|
+
return;
|
|
26383
|
+
}
|
|
26349
26384
|
this.connected = true;
|
|
26350
26385
|
this.reconnectMs = MIN_RECONNECT_MS;
|
|
26351
26386
|
this.logger.info("[perkos-a2a] Connected to relay hub");
|
|
26352
26387
|
this.register();
|
|
26353
26388
|
this.startHeartbeat();
|
|
26354
26389
|
});
|
|
26355
|
-
|
|
26390
|
+
socket.on("message", (data) => {
|
|
26391
|
+
if (!this.isCurrent(generation, socket)) return;
|
|
26356
26392
|
let msg;
|
|
26357
26393
|
try {
|
|
26358
26394
|
msg = JSON.parse(data.toString());
|
|
@@ -26362,15 +26398,17 @@ var RelayClient = class {
|
|
|
26362
26398
|
}
|
|
26363
26399
|
this.handleMessage(msg);
|
|
26364
26400
|
});
|
|
26365
|
-
|
|
26401
|
+
socket.on("close", (code, reason) => {
|
|
26402
|
+
if (!this.isCurrent(generation, socket)) return;
|
|
26366
26403
|
this.connected = false;
|
|
26367
26404
|
this.clearTimers();
|
|
26368
26405
|
if (!this.stopped) {
|
|
26369
26406
|
this.logger.info(`[perkos-a2a] Relay connection closed (${code}: ${reason || "no reason"}). Reconnecting...`);
|
|
26370
|
-
this.scheduleReconnect();
|
|
26407
|
+
this.scheduleReconnect(generation);
|
|
26371
26408
|
}
|
|
26372
26409
|
});
|
|
26373
|
-
|
|
26410
|
+
socket.on("error", (err) => {
|
|
26411
|
+
if (!this.isCurrent(generation, socket)) return;
|
|
26374
26412
|
this.logger.error(`[perkos-a2a] Relay WebSocket error: ${err.message}`);
|
|
26375
26413
|
});
|
|
26376
26414
|
}
|
|
@@ -26464,6 +26502,10 @@ var RelayClient = class {
|
|
|
26464
26502
|
} else {
|
|
26465
26503
|
resolve3(response);
|
|
26466
26504
|
}
|
|
26505
|
+
},
|
|
26506
|
+
cancel: (error) => {
|
|
26507
|
+
clearTimeout(timer);
|
|
26508
|
+
reject(error);
|
|
26467
26509
|
}
|
|
26468
26510
|
});
|
|
26469
26511
|
this.send(msg);
|
|
@@ -26481,10 +26523,10 @@ var RelayClient = class {
|
|
|
26481
26523
|
this.send(msg);
|
|
26482
26524
|
}, HEARTBEAT_INTERVAL_MS);
|
|
26483
26525
|
}
|
|
26484
|
-
scheduleReconnect() {
|
|
26485
|
-
if (this.
|
|
26526
|
+
scheduleReconnect(generation) {
|
|
26527
|
+
if (!this.isCurrent(generation)) return;
|
|
26486
26528
|
this.reconnectTimer = setTimeout(() => {
|
|
26487
|
-
this.connect();
|
|
26529
|
+
if (this.isCurrent(generation)) this.connect(generation);
|
|
26488
26530
|
}, this.reconnectMs);
|
|
26489
26531
|
this.reconnectMs = Math.min(this.reconnectMs * 2, MAX_RECONNECT_MS);
|
|
26490
26532
|
}
|
|
@@ -26498,6 +26540,23 @@ var RelayClient = class {
|
|
|
26498
26540
|
this.reconnectTimer = null;
|
|
26499
26541
|
}
|
|
26500
26542
|
}
|
|
26543
|
+
isCurrent(generation, socket) {
|
|
26544
|
+
return !this.stopped && generation === this.generation && (!socket || socket === this.ws);
|
|
26545
|
+
}
|
|
26546
|
+
beginStop() {
|
|
26547
|
+
if (this.stopped && !this.ws) return null;
|
|
26548
|
+
this.stopped = true;
|
|
26549
|
+
this.generation += 1;
|
|
26550
|
+
this.clearTimers();
|
|
26551
|
+
const error = new Error("CLIENT_STOPPED");
|
|
26552
|
+
for (const pending of this.pendingCallbacks.values()) pending.cancel(error);
|
|
26553
|
+
this.pendingCallbacks.clear();
|
|
26554
|
+
this.orphanResponses.clear();
|
|
26555
|
+
const socket = this.ws;
|
|
26556
|
+
this.ws = null;
|
|
26557
|
+
this.connected = false;
|
|
26558
|
+
return socket;
|
|
26559
|
+
}
|
|
26501
26560
|
};
|
|
26502
26561
|
|
|
26503
26562
|
// src/relay.ts
|
|
@@ -26533,17 +26592,33 @@ var RelayHub = class {
|
|
|
26533
26592
|
});
|
|
26534
26593
|
this.heartbeatTimer = setInterval(() => this.checkHeartbeats(), this.config.heartbeatIntervalMs);
|
|
26535
26594
|
}
|
|
26536
|
-
stop() {
|
|
26595
|
+
async stop(timeoutMs = 5e3) {
|
|
26537
26596
|
if (this.heartbeatTimer) {
|
|
26538
26597
|
clearInterval(this.heartbeatTimer);
|
|
26539
26598
|
this.heartbeatTimer = null;
|
|
26540
26599
|
}
|
|
26541
26600
|
if (this.wss) {
|
|
26601
|
+
const wss = this.wss;
|
|
26602
|
+
this.wss = null;
|
|
26542
26603
|
for (const agent of this.agents.values()) {
|
|
26543
26604
|
agent.ws.close(1001, "Relay shutting down");
|
|
26544
26605
|
}
|
|
26545
|
-
|
|
26546
|
-
|
|
26606
|
+
await new Promise((resolve3) => {
|
|
26607
|
+
let settled = false;
|
|
26608
|
+
const done = () => {
|
|
26609
|
+
if (settled) return;
|
|
26610
|
+
settled = true;
|
|
26611
|
+
clearTimeout(timer);
|
|
26612
|
+
resolve3();
|
|
26613
|
+
};
|
|
26614
|
+
const timer = setTimeout(() => {
|
|
26615
|
+
for (const client of wss.clients) client.terminate();
|
|
26616
|
+
done();
|
|
26617
|
+
}, timeoutMs);
|
|
26618
|
+
timer.unref?.();
|
|
26619
|
+
wss.close(() => done());
|
|
26620
|
+
});
|
|
26621
|
+
this.agents.clear();
|
|
26547
26622
|
this.logger.info("[perkos-a2a] Relay hub stopped");
|
|
26548
26623
|
}
|
|
26549
26624
|
}
|
|
@@ -26938,6 +27013,12 @@ var A2AServer = class {
|
|
|
26938
27013
|
taskResultHandler = null;
|
|
26939
27014
|
taskFailureHandler = null;
|
|
26940
27015
|
lastRuntimeDelivery = null;
|
|
27016
|
+
httpServer = null;
|
|
27017
|
+
lifecycleState = "new";
|
|
27018
|
+
generation = 0;
|
|
27019
|
+
startPromise = null;
|
|
27020
|
+
stopPromise = null;
|
|
27021
|
+
operations = /* @__PURE__ */ new Set();
|
|
26941
27022
|
constructor(config, logger) {
|
|
26942
27023
|
this.config = config;
|
|
26943
27024
|
this.logger = logger || { info: console.log, error: console.error };
|
|
@@ -27068,6 +27149,9 @@ var A2AServer = class {
|
|
|
27068
27149
|
});
|
|
27069
27150
|
}
|
|
27070
27151
|
async handleSendMessage(params, rpcId) {
|
|
27152
|
+
if (this.lifecycleState === "stopping" || this.lifecycleState === "stopped") {
|
|
27153
|
+
return this.error(rpcId, 503, "SERVER_RELOADING");
|
|
27154
|
+
}
|
|
27071
27155
|
const message = params.message;
|
|
27072
27156
|
const taskId = randomUUID4();
|
|
27073
27157
|
const contextId = message?.contextId || randomUUID4();
|
|
@@ -27088,10 +27172,11 @@ var A2AServer = class {
|
|
|
27088
27172
|
this.logger.info(
|
|
27089
27173
|
`[perkos-a2a] Task ${taskId} received from ${task.metadata?.fromAgent}`
|
|
27090
27174
|
);
|
|
27091
|
-
|
|
27175
|
+
const generation = this.generation;
|
|
27176
|
+
this.trackOperation(this.processTask(task, generation));
|
|
27092
27177
|
return this.success(rpcId, task);
|
|
27093
27178
|
}
|
|
27094
|
-
async processTask(task) {
|
|
27179
|
+
async processTask(task, generation) {
|
|
27095
27180
|
task.status = { state: "working", timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
27096
27181
|
this.tasks.set(task.id, structuredClone(task));
|
|
27097
27182
|
const textParts = task.messages.flatMap((m) => m.parts || []).filter((p) => p.kind === "text").map((p) => p.text).join("\n");
|
|
@@ -27135,9 +27220,11 @@ var A2AServer = class {
|
|
|
27135
27220
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
27136
27221
|
};
|
|
27137
27222
|
}
|
|
27223
|
+
if (!this.isGenerationActive(generation)) return;
|
|
27138
27224
|
this.tasks.set(task.id, structuredClone(task));
|
|
27139
27225
|
this.logger.info(`[perkos-a2a] Task ${task.id} completed`);
|
|
27140
27226
|
} catch (err) {
|
|
27227
|
+
if (!this.isGenerationActive(generation)) return;
|
|
27141
27228
|
const msg = err instanceof Error ? err.message : String(err);
|
|
27142
27229
|
if (this.taskFailureHandler) {
|
|
27143
27230
|
await this.taskFailureHandler(task, msg);
|
|
@@ -27324,10 +27411,17 @@ var A2AServer = class {
|
|
|
27324
27411
|
}
|
|
27325
27412
|
/** Handle an inbound task received via the relay */
|
|
27326
27413
|
handleRelayTask(msg) {
|
|
27414
|
+
if (this.lifecycleState !== "running") {
|
|
27415
|
+
this.relayClient?.sendTaskResponse(
|
|
27416
|
+
msg,
|
|
27417
|
+
this.error(String(msg.payload?.id ?? msg.id), 503, "SERVER_RELOADING")
|
|
27418
|
+
);
|
|
27419
|
+
return;
|
|
27420
|
+
}
|
|
27327
27421
|
const payload = msg.payload;
|
|
27328
27422
|
const params = payload.params || payload;
|
|
27329
27423
|
const rpcId = payload.id || msg.id;
|
|
27330
|
-
this.handleSendMessage(params, rpcId).then(async (response) => {
|
|
27424
|
+
const operation = this.handleSendMessage(params, rpcId).then(async (response) => {
|
|
27331
27425
|
const task = response.result;
|
|
27332
27426
|
const acceptQueued = task?.messages?.[0]?.metadata?.acceptQueued === true;
|
|
27333
27427
|
if (task?.id && !isTerminalTaskState(task.status?.state)) {
|
|
@@ -27358,6 +27452,7 @@ var A2AServer = class {
|
|
|
27358
27452
|
this.logger.error(`[perkos-a2a] Failed to process relay task: ${errMsg2}`);
|
|
27359
27453
|
this.relayClient?.sendTaskResponse(msg, this.error(rpcId, -32603, errMsg2));
|
|
27360
27454
|
});
|
|
27455
|
+
this.trackOperation(operation);
|
|
27361
27456
|
}
|
|
27362
27457
|
startRelayClient() {
|
|
27363
27458
|
const relay = this.config.relay;
|
|
@@ -27389,13 +27484,30 @@ var A2AServer = class {
|
|
|
27389
27484
|
this.relayHub.start();
|
|
27390
27485
|
}
|
|
27391
27486
|
async start() {
|
|
27487
|
+
if (this.lifecycleState === "running") return;
|
|
27488
|
+
if (this.lifecycleState === "starting" && this.startPromise) return this.startPromise;
|
|
27489
|
+
if (this.lifecycleState === "stopping" && this.stopPromise) await this.stopPromise;
|
|
27490
|
+
const generation = ++this.generation;
|
|
27491
|
+
this.lifecycleState = "starting";
|
|
27492
|
+
const startPromise = this.startGeneration(generation);
|
|
27493
|
+
this.startPromise = startPromise;
|
|
27494
|
+
try {
|
|
27495
|
+
await startPromise;
|
|
27496
|
+
if (this.isGenerationActive(generation, "starting")) this.lifecycleState = "running";
|
|
27497
|
+
} finally {
|
|
27498
|
+
if (this.startPromise === startPromise) this.startPromise = null;
|
|
27499
|
+
}
|
|
27500
|
+
}
|
|
27501
|
+
async startGeneration(generation) {
|
|
27392
27502
|
const mode = this.config.mode || "auto";
|
|
27393
27503
|
if (mode === "relay") {
|
|
27504
|
+
if (!this.isGenerationActive(generation, "starting")) return;
|
|
27394
27505
|
this.clientOnly = true;
|
|
27395
27506
|
this.startRelayHub();
|
|
27396
27507
|
return;
|
|
27397
27508
|
}
|
|
27398
27509
|
if (mode === "client-only") {
|
|
27510
|
+
if (!this.isGenerationActive(generation, "starting")) return;
|
|
27399
27511
|
this.clientOnly = true;
|
|
27400
27512
|
this.startRelayClient();
|
|
27401
27513
|
if (!this.config.relay?.enabled) {
|
|
@@ -27406,12 +27518,14 @@ var A2AServer = class {
|
|
|
27406
27518
|
return;
|
|
27407
27519
|
}
|
|
27408
27520
|
if (mode === "full") {
|
|
27409
|
-
this.tryListen(this.config.port);
|
|
27521
|
+
await this.tryListen(this.config.port, 1, generation);
|
|
27522
|
+
if (!this.isGenerationActive(generation, "starting")) return;
|
|
27410
27523
|
this.startRelayClient();
|
|
27411
27524
|
return;
|
|
27412
27525
|
}
|
|
27413
27526
|
try {
|
|
27414
27527
|
const net = await detectNetworking();
|
|
27528
|
+
if (!this.isGenerationActive(generation, "starting")) return;
|
|
27415
27529
|
if (net.isBehindNat && !net.hasTailscale) {
|
|
27416
27530
|
if (this.config.relay?.enabled) {
|
|
27417
27531
|
this.logger.info(
|
|
@@ -27434,37 +27548,129 @@ var A2AServer = class {
|
|
|
27434
27548
|
}
|
|
27435
27549
|
} catch {
|
|
27436
27550
|
}
|
|
27437
|
-
this.tryListen(this.config.port);
|
|
27551
|
+
await this.tryListen(this.config.port, 1, generation);
|
|
27552
|
+
if (!this.isGenerationActive(generation, "starting")) return;
|
|
27438
27553
|
this.startRelayClient();
|
|
27439
27554
|
}
|
|
27440
|
-
|
|
27555
|
+
stop(options = {}) {
|
|
27556
|
+
if (this.lifecycleState === "stopping" && this.stopPromise) return this.stopPromise;
|
|
27557
|
+
if (this.lifecycleState === "stopped" || this.lifecycleState === "new") {
|
|
27558
|
+
this.lifecycleState = "stopped";
|
|
27559
|
+
this.generation += 1;
|
|
27560
|
+
return Promise.resolve();
|
|
27561
|
+
}
|
|
27562
|
+
this.lifecycleState = "stopping";
|
|
27563
|
+
this.generation += 1;
|
|
27564
|
+
const priorStart = this.startPromise;
|
|
27565
|
+
let stopPromise;
|
|
27566
|
+
stopPromise = (async () => {
|
|
27567
|
+
try {
|
|
27568
|
+
const timeoutMs = options.timeoutMs ?? 5e3;
|
|
27569
|
+
const drainMs = Math.min(options.drainMs ?? 3e3, timeoutMs);
|
|
27570
|
+
await this.teardownResources(timeoutMs);
|
|
27571
|
+
await priorStart?.catch(() => void 0);
|
|
27572
|
+
await this.teardownResources(timeoutMs);
|
|
27573
|
+
await this.drainOperations(drainMs);
|
|
27574
|
+
for (const task of this.tasks.values()) {
|
|
27575
|
+
if (!isTerminalTaskState(task.status.state)) {
|
|
27576
|
+
task.status = {
|
|
27577
|
+
state: "failed",
|
|
27578
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
27579
|
+
message: { role: "agent", parts: [{ kind: "text", text: "interrupted_by_reload" }] }
|
|
27580
|
+
};
|
|
27581
|
+
}
|
|
27582
|
+
}
|
|
27583
|
+
this.lifecycleState = "stopped";
|
|
27584
|
+
this.logger.info(`[perkos-a2a] A2A server stopped${options.reason ? ` (${options.reason})` : ""}`);
|
|
27585
|
+
} finally {
|
|
27586
|
+
if (this.stopPromise === stopPromise) this.stopPromise = null;
|
|
27587
|
+
}
|
|
27588
|
+
})();
|
|
27589
|
+
this.stopPromise = stopPromise;
|
|
27590
|
+
return stopPromise;
|
|
27591
|
+
}
|
|
27592
|
+
tryListen(port, attempt, generation) {
|
|
27441
27593
|
const maxAttempts = 3;
|
|
27442
|
-
|
|
27594
|
+
return new Promise((resolveStart) => {
|
|
27595
|
+
if (!this.isGenerationActive(generation, "starting")) {
|
|
27596
|
+
resolveStart();
|
|
27597
|
+
return;
|
|
27598
|
+
}
|
|
27443
27599
|
const bindHost = this.config.bindHost || "0.0.0.0";
|
|
27444
27600
|
const srv = this.app.listen(port, bindHost, () => {
|
|
27601
|
+
if (!this.isGenerationActive(generation, "starting")) {
|
|
27602
|
+
srv.close(() => resolveStart());
|
|
27603
|
+
return;
|
|
27604
|
+
}
|
|
27605
|
+
this.httpServer = srv;
|
|
27445
27606
|
this.logger.info(
|
|
27446
27607
|
`[perkos-a2a] ${this.config.agentName} server on ${bindHost}:${port}`
|
|
27447
27608
|
);
|
|
27448
27609
|
this.agentCard.url = `${(this.config.publicUrl || `http://localhost:${port}`).replace(/\/+$/, "")}/a2a/jsonrpc`;
|
|
27610
|
+
resolveStart();
|
|
27449
27611
|
});
|
|
27450
27612
|
srv.on("error", (err) => {
|
|
27451
27613
|
if (err.code === "EADDRINUSE" && attempt < maxAttempts) {
|
|
27452
27614
|
const nextPort = port + 1;
|
|
27453
27615
|
this.logger.info(`[perkos-a2a] Port ${port} in use, trying ${nextPort}`);
|
|
27454
|
-
this.tryListen(nextPort, attempt + 1);
|
|
27616
|
+
void this.tryListen(nextPort, attempt + 1, generation).then(resolveStart);
|
|
27455
27617
|
} else if (err.code === "EADDRINUSE") {
|
|
27456
27618
|
this.logger.info(
|
|
27457
27619
|
`[perkos-a2a] Ports ${this.config.port}-${port} all in use. Falling back to client-only mode.`
|
|
27458
27620
|
);
|
|
27459
27621
|
this.clientOnly = true;
|
|
27622
|
+
resolveStart();
|
|
27460
27623
|
} else {
|
|
27461
27624
|
this.logger.error(`[perkos-a2a] Server error: ${err.message}`);
|
|
27625
|
+
resolveStart();
|
|
27462
27626
|
}
|
|
27463
27627
|
});
|
|
27464
|
-
}
|
|
27465
|
-
|
|
27466
|
-
|
|
27467
|
-
|
|
27628
|
+
});
|
|
27629
|
+
}
|
|
27630
|
+
isGenerationActive(generation, expectedState) {
|
|
27631
|
+
return generation === this.generation && (expectedState ? this.lifecycleState === expectedState : this.lifecycleState !== "stopping" && this.lifecycleState !== "stopped");
|
|
27632
|
+
}
|
|
27633
|
+
trackOperation(operation) {
|
|
27634
|
+
this.operations.add(operation);
|
|
27635
|
+
void operation.finally(() => this.operations.delete(operation)).catch(() => void 0);
|
|
27636
|
+
}
|
|
27637
|
+
async drainOperations(timeoutMs) {
|
|
27638
|
+
if (this.operations.size === 0 || timeoutMs <= 0) return;
|
|
27639
|
+
await Promise.race([
|
|
27640
|
+
Promise.allSettled([...this.operations]),
|
|
27641
|
+
new Promise((resolveDrain) => {
|
|
27642
|
+
const timer = setTimeout(resolveDrain, timeoutMs);
|
|
27643
|
+
timer.unref?.();
|
|
27644
|
+
})
|
|
27645
|
+
]);
|
|
27646
|
+
}
|
|
27647
|
+
async teardownResources(timeoutMs) {
|
|
27648
|
+
const deadline = Date.now() + timeoutMs;
|
|
27649
|
+
const remaining = () => Math.max(1, deadline - Date.now());
|
|
27650
|
+
const relayClient = this.relayClient;
|
|
27651
|
+
this.relayClient = null;
|
|
27652
|
+
await relayClient?.stopAndWait(remaining());
|
|
27653
|
+
const relayHub = this.relayHub;
|
|
27654
|
+
this.relayHub = null;
|
|
27655
|
+
await relayHub?.stop(remaining());
|
|
27656
|
+
const httpServer = this.httpServer;
|
|
27657
|
+
this.httpServer = null;
|
|
27658
|
+
if (!httpServer) return;
|
|
27659
|
+
await new Promise((resolveClose) => {
|
|
27660
|
+
let settled = false;
|
|
27661
|
+
const done = () => {
|
|
27662
|
+
if (settled) return;
|
|
27663
|
+
settled = true;
|
|
27664
|
+
clearTimeout(timer);
|
|
27665
|
+
resolveClose();
|
|
27666
|
+
};
|
|
27667
|
+
const timer = setTimeout(() => {
|
|
27668
|
+
httpServer.closeAllConnections?.();
|
|
27669
|
+
done();
|
|
27670
|
+
}, remaining());
|
|
27671
|
+
timer.unref?.();
|
|
27672
|
+
httpServer.close(() => done());
|
|
27673
|
+
});
|
|
27468
27674
|
}
|
|
27469
27675
|
getExpressApp() {
|
|
27470
27676
|
return this.app;
|
|
@@ -28871,6 +29077,28 @@ async function deliverToHermes(config, message, logger) {
|
|
|
28871
29077
|
}
|
|
28872
29078
|
logger.info(`[perkos-a2a] Delivered inbound A2A task to Hermes API session ${sessionKey} via ${url}`);
|
|
28873
29079
|
}
|
|
29080
|
+
var A2A_LIFECYCLE_REGISTRY = /* @__PURE__ */ Symbol.for("@perkos/perkos-a2a:lifecycle-registry");
|
|
29081
|
+
function lifecycleRegistry() {
|
|
29082
|
+
const root = globalThis;
|
|
29083
|
+
return root[A2A_LIFECYCLE_REGISTRY] ??= /* @__PURE__ */ new Map();
|
|
29084
|
+
}
|
|
29085
|
+
function a2aLifecycleKey(config) {
|
|
29086
|
+
const rawRelayUrl = (config.relay?.url || "no-relay").trim();
|
|
29087
|
+
let relayUrl = rawRelayUrl.replace(/[?#].*$/u, "").replace(/\/+$/u, "").toLowerCase();
|
|
29088
|
+
try {
|
|
29089
|
+
const parsed = new URL(rawRelayUrl);
|
|
29090
|
+
parsed.username = "";
|
|
29091
|
+
parsed.password = "";
|
|
29092
|
+
parsed.search = "";
|
|
29093
|
+
parsed.hash = "";
|
|
29094
|
+
relayUrl = parsed.toString().replace(/\/+$/u, "").toLowerCase();
|
|
29095
|
+
} catch {
|
|
29096
|
+
}
|
|
29097
|
+
const heartbeatUrl = config.platform?.heartbeatUrl || "";
|
|
29098
|
+
const idFromHeartbeat = heartbeatUrl.match(/\/agents\/([^/]+)\/heartbeat(?:\?|$)/u)?.[1];
|
|
29099
|
+
const agentId = config.platform?.agentId || idFromHeartbeat || config.agentName.trim().toLowerCase();
|
|
29100
|
+
return `${relayUrl}|${agentId}`;
|
|
29101
|
+
}
|
|
28874
29102
|
function register(api) {
|
|
28875
29103
|
const pluginConfig = api.config?.plugins?.entries?.["perkos-a2a"]?.config || {
|
|
28876
29104
|
agentName: "agent",
|
|
@@ -28887,6 +29115,8 @@ function register(api) {
|
|
|
28887
29115
|
logger
|
|
28888
29116
|
});
|
|
28889
29117
|
const server = new A2AServer(pluginConfig, logger);
|
|
29118
|
+
const lifecycleKey = a2aLifecycleKey(pluginConfig);
|
|
29119
|
+
const lifecycleInstanceId = randomUUID6();
|
|
28890
29120
|
const pendingTasks = [];
|
|
28891
29121
|
const enqueueSystemEvent = api.runtime?.system?.enqueueSystemEvent;
|
|
28892
29122
|
const requestHeartbeatNow = api.runtime?.system?.requestHeartbeatNow;
|
|
@@ -29058,12 +29288,22 @@ function register(api) {
|
|
|
29058
29288
|
});
|
|
29059
29289
|
api.registerService({
|
|
29060
29290
|
id: "perkos-a2a",
|
|
29061
|
-
start: () => {
|
|
29062
|
-
|
|
29291
|
+
start: async () => {
|
|
29292
|
+
const registry = lifecycleRegistry();
|
|
29293
|
+
const previous = registry.get(lifecycleKey);
|
|
29294
|
+
if (previous && previous.instanceId !== lifecycleInstanceId) {
|
|
29295
|
+
logger.info(`[perkos-a2a] Waiting for previous relay generation to stop for ${pluginConfig.agentName}`);
|
|
29296
|
+
await previous.server.stop({ reason: "replaced", timeoutMs: 5e3, drainMs: 3e3 });
|
|
29297
|
+
}
|
|
29298
|
+
registry.set(lifecycleKey, { instanceId: lifecycleInstanceId, server });
|
|
29299
|
+
await server.start();
|
|
29063
29300
|
logger.info(`[perkos-a2a] A2A server started for ${pluginConfig.agentName}`);
|
|
29064
29301
|
},
|
|
29065
|
-
stop: () => {
|
|
29302
|
+
stop: async () => {
|
|
29066
29303
|
logger.info("[perkos-a2a] A2A server stopping");
|
|
29304
|
+
await server.stop({ reason: "openclaw-service-stop", timeoutMs: 5e3, drainMs: 3e3 });
|
|
29305
|
+
const registry = lifecycleRegistry();
|
|
29306
|
+
if (registry.get(lifecycleKey)?.instanceId === lifecycleInstanceId) registry.delete(lifecycleKey);
|
|
29067
29307
|
}
|
|
29068
29308
|
});
|
|
29069
29309
|
const chatApiKey = pluginConfig.chat?.apiKey || pluginConfig.relay?.apiKey;
|
|
@@ -29600,6 +29840,7 @@ export {
|
|
|
29600
29840
|
ChatStore,
|
|
29601
29841
|
RelayClient,
|
|
29602
29842
|
RelayHub,
|
|
29843
|
+
a2aLifecycleKey,
|
|
29603
29844
|
alignOpenClawChatSessionModel,
|
|
29604
29845
|
register as default,
|
|
29605
29846
|
detectNetworking,
|