@perkos/perkos-a2a 0.12.19 → 0.12.27
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 +60 -0
- package/dist/agent.js +3 -2
- package/dist/agent.js.map +1 -1
- package/dist/bridge-agent.js +25 -56
- package/dist/bridge-agent.js.map +1 -1
- package/dist/bridge-chat-reply.d.ts +14 -1
- package/dist/bridge-chat-reply.d.ts.map +1 -1
- package/dist/bridge-chat-reply.js +71 -5
- package/dist/bridge-chat-reply.js.map +1 -1
- package/dist/gateway-health.d.ts +5 -11
- package/dist/gateway-health.d.ts.map +1 -1
- package/dist/gateway-health.js +53 -15
- package/dist/gateway-health.js.map +1 -1
- package/dist/hermes-plugin.d.ts +2 -0
- package/dist/hermes-plugin.d.ts.map +1 -1
- package/dist/hermes-plugin.js +4 -2
- package/dist/hermes-plugin.js.map +1 -1
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +407 -19
- package/dist/index.js.map +3 -3
- package/dist/platform-heartbeat.d.ts +26 -0
- package/dist/platform-heartbeat.d.ts.map +1 -0
- package/dist/platform-heartbeat.js +72 -0
- package/dist/platform-heartbeat.js.map +1 -0
- package/dist/project-chat-context.d.ts +3 -0
- package/dist/project-chat-context.d.ts.map +1 -0
- package/dist/project-chat-context.js +16 -0
- package/dist/project-chat-context.js.map +1 -0
- package/dist/relay-client.d.ts +21 -2
- package/dist/relay-client.d.ts.map +1 -1
- package/dist/relay-client.js +82 -17
- package/dist/relay-client.js.map +1 -1
- package/dist/repair-cli.d.ts +3 -0
- package/dist/repair-cli.d.ts.map +1 -0
- package/dist/repair-cli.js +31 -0
- package/dist/repair-cli.js.map +1 -0
- package/dist/repair.d.ts +53 -0
- package/dist/repair.d.ts.map +1 -0
- package/dist/repair.js +357 -0
- package/dist/repair.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +5 -2
- package/dist/server.js.map +1 -1
- package/dist/task-context.d.ts +18 -0
- package/dist/task-context.d.ts.map +1 -0
- package/dist/task-context.js +35 -0
- package/dist/task-context.js.map +1 -0
- package/dist/types.d.ts +20 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/openclaw.plugin.json +28 -1
- package/package.json +9 -5
package/dist/index.js
CHANGED
|
@@ -26252,6 +26252,8 @@ import { randomUUID } from "crypto";
|
|
|
26252
26252
|
var MIN_RECONNECT_MS = 1e3;
|
|
26253
26253
|
var MAX_RECONNECT_MS = 6e4;
|
|
26254
26254
|
var HEARTBEAT_INTERVAL_MS = 25e3;
|
|
26255
|
+
var ORPHAN_TTL_MS = 10 * 6e4;
|
|
26256
|
+
var ORPHAN_MAX = 256;
|
|
26255
26257
|
var RelayClient = class {
|
|
26256
26258
|
ws = null;
|
|
26257
26259
|
options;
|
|
@@ -26262,6 +26264,8 @@ var RelayClient = class {
|
|
|
26262
26264
|
connected = false;
|
|
26263
26265
|
stopped = false;
|
|
26264
26266
|
pendingCallbacks = /* @__PURE__ */ new Map();
|
|
26267
|
+
/** Responses that arrived with nobody waiting, claimable by a retry. */
|
|
26268
|
+
orphanResponses = /* @__PURE__ */ new Map();
|
|
26265
26269
|
constructor(options) {
|
|
26266
26270
|
this.options = options;
|
|
26267
26271
|
this.logger = options.logger || { info: console.log, error: console.error };
|
|
@@ -26282,12 +26286,25 @@ var RelayClient = class {
|
|
|
26282
26286
|
}
|
|
26283
26287
|
this.connected = false;
|
|
26284
26288
|
}
|
|
26285
|
-
/**
|
|
26286
|
-
|
|
26289
|
+
/**
|
|
26290
|
+
* Send a task to another agent via the relay.
|
|
26291
|
+
*
|
|
26292
|
+
* Pass `taskId` to retry a task that already went out: the relay dedups on
|
|
26293
|
+
* the id and will not run the work twice, and if the first attempt's response
|
|
26294
|
+
* arrived after we gave up it is claimed from the orphan buffer instead of
|
|
26295
|
+
* waiting again. Omit it for genuinely new work.
|
|
26296
|
+
*/
|
|
26297
|
+
async sendTask(targetAgent, payload, taskId) {
|
|
26298
|
+
const id = taskId ?? randomUUID();
|
|
26299
|
+
const alreadyAnswered = this.claimOrphan(id);
|
|
26300
|
+
if (alreadyAnswered) {
|
|
26301
|
+
this.logger.info(`[perkos-a2a] Reusing buffered response for task ${id}`);
|
|
26302
|
+
return alreadyAnswered;
|
|
26303
|
+
}
|
|
26287
26304
|
return this.sendAndWait({
|
|
26288
26305
|
type: "task",
|
|
26289
26306
|
to: targetAgent,
|
|
26290
|
-
id
|
|
26307
|
+
id,
|
|
26291
26308
|
from: this.options.agentName,
|
|
26292
26309
|
payload,
|
|
26293
26310
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -26372,10 +26389,10 @@ var RelayClient = class {
|
|
|
26372
26389
|
this.send(msg);
|
|
26373
26390
|
}
|
|
26374
26391
|
handleMessage(msg) {
|
|
26375
|
-
const
|
|
26376
|
-
if (
|
|
26392
|
+
const pending = this.pendingCallbacks.get(msg.id);
|
|
26393
|
+
if (pending && (msg.type === pending.expectedType || msg.type === "error")) {
|
|
26377
26394
|
this.pendingCallbacks.delete(msg.id);
|
|
26378
|
-
|
|
26395
|
+
pending.settle(msg);
|
|
26379
26396
|
return;
|
|
26380
26397
|
}
|
|
26381
26398
|
switch (msg.type) {
|
|
@@ -26385,8 +26402,15 @@ var RelayClient = class {
|
|
|
26385
26402
|
case "task":
|
|
26386
26403
|
this.options.onTask(msg);
|
|
26387
26404
|
break;
|
|
26405
|
+
case "task_ack":
|
|
26406
|
+
if (msg.payload?.deduplicated) {
|
|
26407
|
+
this.logger.info(`[perkos-a2a] Relay deduplicated task ${msg.id}; awaiting the original response`);
|
|
26408
|
+
}
|
|
26409
|
+
break;
|
|
26388
26410
|
case "task_response":
|
|
26389
|
-
this.
|
|
26411
|
+
this.bufferOrphan(msg);
|
|
26412
|
+
this.logger.info(`[perkos-a2a] Buffered unmatched task response: ${msg.id}`);
|
|
26413
|
+
this.options.onOrphanTaskResponse?.(msg);
|
|
26390
26414
|
break;
|
|
26391
26415
|
case "discover_response":
|
|
26392
26416
|
if (this.options.onDiscoverResponse) {
|
|
@@ -26407,18 +26431,39 @@ var RelayClient = class {
|
|
|
26407
26431
|
this.ws.send(JSON.stringify(msg));
|
|
26408
26432
|
}
|
|
26409
26433
|
}
|
|
26434
|
+
/** Take a buffered response for `id`, if one arrived while nobody waited. */
|
|
26435
|
+
claimOrphan(id) {
|
|
26436
|
+
const entry = this.orphanResponses.get(id);
|
|
26437
|
+
if (!entry) return null;
|
|
26438
|
+
this.orphanResponses.delete(id);
|
|
26439
|
+
if (entry.expiresAt <= Date.now()) return null;
|
|
26440
|
+
return entry.msg;
|
|
26441
|
+
}
|
|
26442
|
+
bufferOrphan(msg) {
|
|
26443
|
+
const now = Date.now();
|
|
26444
|
+
for (const [id, entry] of this.orphanResponses) {
|
|
26445
|
+
if (entry.expiresAt <= now) this.orphanResponses.delete(id);
|
|
26446
|
+
}
|
|
26447
|
+
this.orphanResponses.set(msg.id, { msg, expiresAt: now + ORPHAN_TTL_MS });
|
|
26448
|
+
while (this.orphanResponses.size > ORPHAN_MAX) {
|
|
26449
|
+
this.orphanResponses.delete(this.orphanResponses.keys().next().value);
|
|
26450
|
+
}
|
|
26451
|
+
}
|
|
26410
26452
|
async sendAndWait(msg, expectedType, timeoutMs) {
|
|
26411
26453
|
return new Promise((resolve, reject) => {
|
|
26412
26454
|
const timer = setTimeout(() => {
|
|
26413
26455
|
this.pendingCallbacks.delete(msg.id);
|
|
26414
26456
|
reject(new Error(`Relay request timed out after ${timeoutMs}ms`));
|
|
26415
26457
|
}, timeoutMs);
|
|
26416
|
-
this.pendingCallbacks.set(msg.id,
|
|
26417
|
-
|
|
26418
|
-
|
|
26419
|
-
|
|
26420
|
-
|
|
26421
|
-
|
|
26458
|
+
this.pendingCallbacks.set(msg.id, {
|
|
26459
|
+
expectedType,
|
|
26460
|
+
settle: (response) => {
|
|
26461
|
+
clearTimeout(timer);
|
|
26462
|
+
if (response.type === "error") {
|
|
26463
|
+
reject(new Error(`Relay error: ${response.payload.message}`));
|
|
26464
|
+
} else {
|
|
26465
|
+
resolve(response);
|
|
26466
|
+
}
|
|
26422
26467
|
}
|
|
26423
26468
|
});
|
|
26424
26469
|
this.send(msg);
|
|
@@ -27034,6 +27079,7 @@ var A2AServer = class {
|
|
|
27034
27079
|
messages: [message],
|
|
27035
27080
|
artifacts: [],
|
|
27036
27081
|
metadata: {
|
|
27082
|
+
...message?.metadata,
|
|
27037
27083
|
fromAgent: message?.metadata?.fromAgent || "unknown"
|
|
27038
27084
|
},
|
|
27039
27085
|
sessionKeyHint: "agent:main"
|
|
@@ -27281,9 +27327,9 @@ var A2AServer = class {
|
|
|
27281
27327
|
const payload = msg.payload;
|
|
27282
27328
|
const params = payload.params || payload;
|
|
27283
27329
|
const rpcId = payload.id || msg.id;
|
|
27284
|
-
const acceptQueued = params?.message?.metadata?.acceptQueued === true;
|
|
27285
27330
|
this.handleSendMessage(params, rpcId).then(async (response) => {
|
|
27286
27331
|
const task = response.result;
|
|
27332
|
+
const acceptQueued = task?.messages?.[0]?.metadata?.acceptQueued === true;
|
|
27287
27333
|
if (task?.id && !isTerminalTaskState(task.status?.state)) {
|
|
27288
27334
|
if (acceptQueued) {
|
|
27289
27335
|
this.relayClient?.sendTaskResponse(
|
|
@@ -27301,7 +27347,12 @@ var A2AServer = class {
|
|
|
27301
27347
|
return;
|
|
27302
27348
|
}
|
|
27303
27349
|
}
|
|
27304
|
-
|
|
27350
|
+
if (!acceptQueued) {
|
|
27351
|
+
this.relayClient?.sendTaskResponse(
|
|
27352
|
+
msg,
|
|
27353
|
+
response
|
|
27354
|
+
);
|
|
27355
|
+
}
|
|
27305
27356
|
}).catch((err) => {
|
|
27306
27357
|
const errMsg2 = err instanceof Error ? err.message : String(err);
|
|
27307
27358
|
this.logger.error(`[perkos-a2a] Failed to process relay task: ${errMsg2}`);
|
|
@@ -28412,6 +28463,69 @@ function errMsg(err) {
|
|
|
28412
28463
|
return err instanceof Error ? err.message : String(err);
|
|
28413
28464
|
}
|
|
28414
28465
|
|
|
28466
|
+
// src/platform-heartbeat.ts
|
|
28467
|
+
var DEFAULT_INTERVAL_MS = 6e4;
|
|
28468
|
+
var MIN_INTERVAL_MS = 1e4;
|
|
28469
|
+
function startPlatformHeartbeat(opts) {
|
|
28470
|
+
const env = opts.env ?? process.env;
|
|
28471
|
+
const fetcher = opts.fetcher ?? fetch;
|
|
28472
|
+
const url = opts.url?.trim() || env.PERKOS_HEARTBEAT_URL?.trim();
|
|
28473
|
+
const relayKey = opts.relayKey?.trim() || env.A2A_RELAY_API_KEY?.trim();
|
|
28474
|
+
if (!url || !relayKey) return { stop: () => void 0 };
|
|
28475
|
+
const configuredInterval = Number(
|
|
28476
|
+
opts.intervalMs ?? env.PERKOS_HEARTBEAT_INTERVAL_MS ?? DEFAULT_INTERVAL_MS
|
|
28477
|
+
);
|
|
28478
|
+
const intervalMs = Number.isFinite(configuredInterval) ? Math.max(MIN_INTERVAL_MS, configuredInterval) : DEFAULT_INTERVAL_MS;
|
|
28479
|
+
const a2aRuntime = env.A2A_RUNTIME?.trim() || "hermes-api";
|
|
28480
|
+
const runtimeKind = opts.runtimeKind ?? (a2aRuntime === "openclaw" ? "openclaw" : a2aRuntime === "custom" ? "custom" : "hermes");
|
|
28481
|
+
let stopped = false;
|
|
28482
|
+
let firstSuccess = true;
|
|
28483
|
+
const report = async () => {
|
|
28484
|
+
if (stopped) return;
|
|
28485
|
+
try {
|
|
28486
|
+
const res = await fetcher(url, {
|
|
28487
|
+
method: "POST",
|
|
28488
|
+
headers: {
|
|
28489
|
+
"content-type": "application/json",
|
|
28490
|
+
authorization: `Bearer ${relayKey}`,
|
|
28491
|
+
"x-relay-key": relayKey
|
|
28492
|
+
},
|
|
28493
|
+
body: JSON.stringify({
|
|
28494
|
+
runtimeKind,
|
|
28495
|
+
version: opts.version?.trim() || env.PERKOS_A2A_VERSION?.trim() || "0.12.26",
|
|
28496
|
+
ts: Date.now()
|
|
28497
|
+
})
|
|
28498
|
+
});
|
|
28499
|
+
if (res.ok) {
|
|
28500
|
+
if (firstSuccess) {
|
|
28501
|
+
opts.logger.info(
|
|
28502
|
+
`[perkos-heartbeat] reported online to ${url} (status=${res.status}, every=${intervalMs}ms)`
|
|
28503
|
+
);
|
|
28504
|
+
firstSuccess = false;
|
|
28505
|
+
}
|
|
28506
|
+
} else {
|
|
28507
|
+
opts.logger.warn(
|
|
28508
|
+
`[perkos-heartbeat] ${res.status} from ${url} (continuing)`
|
|
28509
|
+
);
|
|
28510
|
+
}
|
|
28511
|
+
} catch (err) {
|
|
28512
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
28513
|
+
opts.logger.warn(
|
|
28514
|
+
`[perkos-heartbeat] POST failed to ${url}: ${msg} (continuing)`
|
|
28515
|
+
);
|
|
28516
|
+
}
|
|
28517
|
+
};
|
|
28518
|
+
void report();
|
|
28519
|
+
const timer = setInterval(() => void report(), intervalMs);
|
|
28520
|
+
timer.unref?.();
|
|
28521
|
+
return {
|
|
28522
|
+
stop: () => {
|
|
28523
|
+
stopped = true;
|
|
28524
|
+
clearInterval(timer);
|
|
28525
|
+
}
|
|
28526
|
+
};
|
|
28527
|
+
}
|
|
28528
|
+
|
|
28415
28529
|
// src/types.ts
|
|
28416
28530
|
function isWebhookEvent(msg) {
|
|
28417
28531
|
return msg.type === "webhook_event";
|
|
@@ -28444,11 +28558,204 @@ function openclawChatSessionKey(config, convId) {
|
|
|
28444
28558
|
const safe = convId.replace(/[^a-zA-Z0-9_.-]/g, "-").slice(0, 120) || "unknown";
|
|
28445
28559
|
return `${configured}:perkos-chat-${safe}`;
|
|
28446
28560
|
}
|
|
28561
|
+
var MISSING_AGENT_HARNESS_RE = /Requested agent harness "([^"]+)" is not registered\./u;
|
|
28562
|
+
function missingAgentHarnessId(error) {
|
|
28563
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
28564
|
+
return message.match(MISSING_AGENT_HARNESS_RE)?.[1]?.trim() || null;
|
|
28565
|
+
}
|
|
28566
|
+
function omitUnavailableAgentHarness(value, unavailableHarnessId) {
|
|
28567
|
+
if (Array.isArray(value)) {
|
|
28568
|
+
return value.map((entry) => omitUnavailableAgentHarness(entry, unavailableHarnessId));
|
|
28569
|
+
}
|
|
28570
|
+
if (!value || typeof value !== "object") return value;
|
|
28571
|
+
const source = value;
|
|
28572
|
+
const next = {};
|
|
28573
|
+
for (const [key, entry] of Object.entries(source)) {
|
|
28574
|
+
if (key === "agentRuntime" && entry && typeof entry === "object" && String(entry.id ?? "").trim() === unavailableHarnessId) {
|
|
28575
|
+
continue;
|
|
28576
|
+
}
|
|
28577
|
+
if ((key === "embeddedHarness" || key === "agentRuntimeOverride" || key === "agentHarnessId") && typeof entry === "string" && entry.trim() === unavailableHarnessId) {
|
|
28578
|
+
continue;
|
|
28579
|
+
}
|
|
28580
|
+
next[key] = omitUnavailableAgentHarness(entry, unavailableHarnessId);
|
|
28581
|
+
}
|
|
28582
|
+
return next;
|
|
28583
|
+
}
|
|
28584
|
+
function alignOpenClawChatSessionModel(entry, configuredModel) {
|
|
28585
|
+
const next = { ...entry };
|
|
28586
|
+
for (const key of [
|
|
28587
|
+
"providerOverride",
|
|
28588
|
+
"modelOverride",
|
|
28589
|
+
"modelOverrideSource",
|
|
28590
|
+
"modelOverrideFallbackOriginProvider",
|
|
28591
|
+
"modelOverrideFallbackOriginModel",
|
|
28592
|
+
"agentRuntimeOverride",
|
|
28593
|
+
"agentHarnessId",
|
|
28594
|
+
"modelProvider",
|
|
28595
|
+
"model",
|
|
28596
|
+
"fallbackNoticeSelectedModel",
|
|
28597
|
+
"fallbackNoticeActiveModel",
|
|
28598
|
+
"fallbackNoticeReason"
|
|
28599
|
+
]) {
|
|
28600
|
+
delete next[key];
|
|
28601
|
+
}
|
|
28602
|
+
const modelRef = configuredModel?.trim();
|
|
28603
|
+
const separator = modelRef?.indexOf("/") ?? -1;
|
|
28604
|
+
if (modelRef && separator > 0 && separator < modelRef.length - 1) {
|
|
28605
|
+
const provider = modelRef.slice(0, separator);
|
|
28606
|
+
const model = modelRef.slice(separator + 1);
|
|
28607
|
+
next.providerOverride = provider;
|
|
28608
|
+
next.modelOverride = model;
|
|
28609
|
+
next.modelOverrideSource = "user";
|
|
28610
|
+
next.modelProvider = provider;
|
|
28611
|
+
next.model = model;
|
|
28612
|
+
}
|
|
28613
|
+
return next;
|
|
28614
|
+
}
|
|
28615
|
+
async function prepareOpenClawChatSession(api, config, sessionKey, logger) {
|
|
28616
|
+
const patchSessionEntry = api.runtime?.agent?.session?.patchSessionEntry;
|
|
28617
|
+
if (typeof patchSessionEntry !== "function") {
|
|
28618
|
+
logger.info("[perkos-chat] OpenClaw session metadata API unavailable \u2014 using gateway session defaults");
|
|
28619
|
+
return;
|
|
28620
|
+
}
|
|
28621
|
+
try {
|
|
28622
|
+
await patchSessionEntry({
|
|
28623
|
+
sessionKey,
|
|
28624
|
+
fallbackEntry: { sessionId: randomUUID6(), updatedAt: Date.now() },
|
|
28625
|
+
preserveActivity: true,
|
|
28626
|
+
replaceEntry: true,
|
|
28627
|
+
update: (entry) => alignOpenClawChatSessionModel(entry, config.runtime?.model)
|
|
28628
|
+
});
|
|
28629
|
+
logger.info(
|
|
28630
|
+
config.runtime?.model ? `[perkos-chat] session ${sessionKey} pinned to ${config.runtime.model}` : `[perkos-chat] session ${sessionKey} aligned to the gateway's active default model`
|
|
28631
|
+
);
|
|
28632
|
+
} catch (err) {
|
|
28633
|
+
logger.error(
|
|
28634
|
+
`[perkos-chat] failed to align OpenClaw session model: ${err instanceof Error ? err.message : String(err)}`
|
|
28635
|
+
);
|
|
28636
|
+
}
|
|
28637
|
+
}
|
|
28638
|
+
async function forceNativeOpenClawChatRuntime(api, config, sessionKey, logger) {
|
|
28639
|
+
const patchSessionEntry = api.runtime?.agent?.session?.patchSessionEntry;
|
|
28640
|
+
if (typeof patchSessionEntry !== "function") return;
|
|
28641
|
+
try {
|
|
28642
|
+
await patchSessionEntry({
|
|
28643
|
+
sessionKey,
|
|
28644
|
+
fallbackEntry: { sessionId: randomUUID6(), updatedAt: Date.now() },
|
|
28645
|
+
preserveActivity: true,
|
|
28646
|
+
replaceEntry: true,
|
|
28647
|
+
update: (entry) => ({
|
|
28648
|
+
...alignOpenClawChatSessionModel(entry, config.runtime?.model),
|
|
28649
|
+
// OpenClaw <= 2026.5.x calls its built-in runtime "pi". Newer
|
|
28650
|
+
// gateways normalize the alias to their native "openclaw" harness.
|
|
28651
|
+
agentRuntimeOverride: "pi",
|
|
28652
|
+
agentHarnessId: "pi"
|
|
28653
|
+
})
|
|
28654
|
+
});
|
|
28655
|
+
logger.info(`[perkos-chat] session ${sessionKey} retrying with the native OpenClaw harness`);
|
|
28656
|
+
} catch (err) {
|
|
28657
|
+
logger.error(
|
|
28658
|
+
`[perkos-chat] failed to select native OpenClaw harness: ${err instanceof Error ? err.message : String(err)}`
|
|
28659
|
+
);
|
|
28660
|
+
}
|
|
28661
|
+
}
|
|
28662
|
+
function extractOpenClawChatReply(result) {
|
|
28663
|
+
const finalText = result.meta?.finalAssistantVisibleText?.trim() || result.meta?.finalAssistantRawText?.trim();
|
|
28664
|
+
if (finalText) return finalText;
|
|
28665
|
+
const payloadText = result.payloads?.filter((payload) => !payload.isError && !payload.isReasoning && !payload.isCommentary).map((payload) => payload.text?.trim()).filter((text) => Boolean(text)).join("\n\n").trim();
|
|
28666
|
+
return payloadText || null;
|
|
28667
|
+
}
|
|
28668
|
+
function openclawAgentId(config) {
|
|
28669
|
+
const sessionKey = config.runtime?.sessionKey || "agent:main";
|
|
28670
|
+
const parts = sessionKey.split(":");
|
|
28671
|
+
return parts[0] === "agent" && parts[1] ? parts[1] : "main";
|
|
28672
|
+
}
|
|
28673
|
+
async function runOpenClawChatTurn(api, config, sessionKey, prompt, transcriptPrompt, logger) {
|
|
28674
|
+
const runEmbeddedAgent = api.runtime?.agent?.runEmbeddedAgent;
|
|
28675
|
+
const currentConfig = api.runtime?.config?.current;
|
|
28676
|
+
const resolveAgentWorkspaceDir = api.runtime?.agent?.resolveAgentWorkspaceDir;
|
|
28677
|
+
if (typeof runEmbeddedAgent !== "function" || typeof currentConfig !== "function" || typeof resolveAgentWorkspaceDir !== "function") {
|
|
28678
|
+
logger.info("[perkos-chat] embedded OpenClaw runner unavailable \u2014 using gateway wake fallback");
|
|
28679
|
+
return null;
|
|
28680
|
+
}
|
|
28681
|
+
const agentId = openclawAgentId(config);
|
|
28682
|
+
const runtimeConfig = currentConfig();
|
|
28683
|
+
const workspaceDir = resolveAgentWorkspaceDir(runtimeConfig, agentId);
|
|
28684
|
+
const entry = api.runtime?.agent?.session?.getSessionEntry?.({ agentId, sessionKey });
|
|
28685
|
+
const sessionId = typeof entry?.sessionId === "string" && entry.sessionId.trim() ? entry.sessionId : randomUUID6();
|
|
28686
|
+
const configuredTimeout = api.runtime?.agent?.resolveAgentTimeoutMs?.(runtimeConfig);
|
|
28687
|
+
const timeoutMs = Math.min(
|
|
28688
|
+
75e3,
|
|
28689
|
+
typeof configuredTimeout === "number" && configuredTimeout > 0 ? configuredTimeout : 75e3
|
|
28690
|
+
);
|
|
28691
|
+
const run = async (configOverride, retryNative = false) => runEmbeddedAgent({
|
|
28692
|
+
sessionId,
|
|
28693
|
+
sessionKey,
|
|
28694
|
+
agentId,
|
|
28695
|
+
workspaceDir,
|
|
28696
|
+
config: configOverride,
|
|
28697
|
+
prompt,
|
|
28698
|
+
transcriptPrompt,
|
|
28699
|
+
timeoutMs,
|
|
28700
|
+
runId: randomUUID6(),
|
|
28701
|
+
trigger: "user",
|
|
28702
|
+
messageChannel: "perkos-chat",
|
|
28703
|
+
disableTools: true,
|
|
28704
|
+
disableMessageTool: true,
|
|
28705
|
+
terminalReplyExpectation: "required",
|
|
28706
|
+
suppressLiveStreamOutput: true,
|
|
28707
|
+
cleanupBundleMcpOnRunEnd: true,
|
|
28708
|
+
...retryNative ? {
|
|
28709
|
+
// Cross-version compatibility: 2026.5.x recognizes `pi`, while
|
|
28710
|
+
// current gateways recognize the explicit native runtime override.
|
|
28711
|
+
agentHarnessId: "pi",
|
|
28712
|
+
agentHarnessRuntimeOverride: "openclaw",
|
|
28713
|
+
suppressNextUserMessagePersistence: true
|
|
28714
|
+
} : {}
|
|
28715
|
+
});
|
|
28716
|
+
try {
|
|
28717
|
+
let result;
|
|
28718
|
+
try {
|
|
28719
|
+
result = await run(runtimeConfig);
|
|
28720
|
+
} catch (err) {
|
|
28721
|
+
const unavailableHarnessId = missingAgentHarnessId(err);
|
|
28722
|
+
if (!unavailableHarnessId) throw err;
|
|
28723
|
+
logger.info(
|
|
28724
|
+
`[perkos-chat] configured harness ${unavailableHarnessId} is unavailable; retrying with the native OpenClaw runtime and the same gateway-selected model`
|
|
28725
|
+
);
|
|
28726
|
+
await forceNativeOpenClawChatRuntime(api, config, sessionKey, logger);
|
|
28727
|
+
result = await run(
|
|
28728
|
+
omitUnavailableAgentHarness(runtimeConfig, unavailableHarnessId),
|
|
28729
|
+
true
|
|
28730
|
+
);
|
|
28731
|
+
}
|
|
28732
|
+
const reply = extractOpenClawChatReply(result);
|
|
28733
|
+
if (!reply) {
|
|
28734
|
+
logger.error(`[perkos-chat] embedded OpenClaw run completed without reply text for ${sessionKey}`);
|
|
28735
|
+
return null;
|
|
28736
|
+
}
|
|
28737
|
+
logger.info(`[perkos-chat] embedded OpenClaw run completed for ${sessionKey}`);
|
|
28738
|
+
return reply;
|
|
28739
|
+
} catch (err) {
|
|
28740
|
+
logger.error(
|
|
28741
|
+
`[perkos-chat] embedded OpenClaw run failed for ${sessionKey}: ${err instanceof Error ? err.message : String(err)}`
|
|
28742
|
+
);
|
|
28743
|
+
return null;
|
|
28744
|
+
}
|
|
28745
|
+
}
|
|
28447
28746
|
function parseWalletFromIdentity(identity) {
|
|
28448
28747
|
if (typeof identity !== "string" || !identity.startsWith("user:")) return null;
|
|
28449
28748
|
const addr = identity.slice("user:".length);
|
|
28450
28749
|
return addr.startsWith("0x") ? addr.toLowerCase() : null;
|
|
28451
28750
|
}
|
|
28751
|
+
var CHAT_CONTEXT_MESSAGE_LIMIT = 12;
|
|
28752
|
+
var CHAT_CONTEXT_CHAR_LIMIT = 6e3;
|
|
28753
|
+
function formatRecentChatContext(messages, currentMessageId) {
|
|
28754
|
+
const prior = messages.filter((message) => message.id !== currentMessageId).slice(-CHAT_CONTEXT_MESSAGE_LIMIT).map((message) => `${message.from}: ${message.text.trim()}`).filter((line) => line.length > 0);
|
|
28755
|
+
if (prior.length === 0) return "(No earlier messages in this conversation.)";
|
|
28756
|
+
const transcript = prior.join("\n");
|
|
28757
|
+
return transcript.length <= CHAT_CONTEXT_CHAR_LIMIT ? transcript : `\u2026${transcript.slice(-CHAT_CONTEXT_CHAR_LIMIT)}`;
|
|
28758
|
+
}
|
|
28452
28759
|
async function deliverToHermes(config, message, logger) {
|
|
28453
28760
|
const runtime = config.runtime || {};
|
|
28454
28761
|
const baseUrl = (runtime.hermesUrl || "http://127.0.0.1:8642").replace(/\/+$/, "");
|
|
@@ -28463,7 +28770,8 @@ async function deliverToHermes(config, message, logger) {
|
|
|
28463
28770
|
};
|
|
28464
28771
|
if (token) headers.authorization = `Bearer ${token}`;
|
|
28465
28772
|
const normalizedEndpoint = endpoint.replace(/^\/?/, "/");
|
|
28466
|
-
const
|
|
28773
|
+
const model = runtime.model?.trim();
|
|
28774
|
+
const body = normalizedEndpoint.startsWith("/v1/chat/completions") ? { ...model ? { model } : {}, messages: [{ role: "user", content: message }], stream: false } : normalizedEndpoint.startsWith("/v1/runs") ? { input: message, session_id: sessionKey } : normalizedEndpoint.startsWith("/v1/responses") ? { ...model ? { model } : {}, input: message, store: true } : { sessionKey, message };
|
|
28467
28775
|
const response = await fetch(url, {
|
|
28468
28776
|
method: "POST",
|
|
28469
28777
|
headers,
|
|
@@ -28681,6 +28989,13 @@ function register(api) {
|
|
|
28681
28989
|
onChatDeliver: async (frame) => {
|
|
28682
28990
|
const walletAddress = parseWalletFromIdentity(frame.from);
|
|
28683
28991
|
const marker = `[PERKOS_CHAT:${frame.convId}]`;
|
|
28992
|
+
const recentPage = await chatClient?.getStore().readPage(frame.convId, {
|
|
28993
|
+
limit: CHAT_CONTEXT_MESSAGE_LIMIT + 1
|
|
28994
|
+
});
|
|
28995
|
+
const recentContext = formatRecentChatContext(
|
|
28996
|
+
recentPage?.messages ?? [],
|
|
28997
|
+
frame.id
|
|
28998
|
+
);
|
|
28684
28999
|
const eventText = [
|
|
28685
29000
|
marker,
|
|
28686
29001
|
`From: ${frame.from}`,
|
|
@@ -28694,6 +29009,27 @@ function register(api) {
|
|
|
28694
29009
|
walletAddress ? ` walletAddress: "${walletAddress}"` : ` walletAddress: <derived from From>`,
|
|
28695
29010
|
` text: <your reply>`,
|
|
28696
29011
|
"",
|
|
29012
|
+
"Recent conversation context (oldest to newest):",
|
|
29013
|
+
recentContext,
|
|
29014
|
+
"",
|
|
29015
|
+
"Use the recent context. Do not ask again for information the user already provided.",
|
|
29016
|
+
"",
|
|
29017
|
+
"Message body:",
|
|
29018
|
+
frame.text
|
|
29019
|
+
].filter(Boolean).join("\n");
|
|
29020
|
+
const directRunPrompt = [
|
|
29021
|
+
marker,
|
|
29022
|
+
`From: ${frame.from}`,
|
|
29023
|
+
`Conversation: ${frame.convId}`,
|
|
29024
|
+
frame.projectId ? `Canonical projectId: ${frame.projectId}` : "",
|
|
29025
|
+
"",
|
|
29026
|
+
"Reply to this PerkOS chat message using your current runtime configuration.",
|
|
29027
|
+
"Return only the reply text. Do not call messaging tools; the PerkOS plugin delivers the returned text.",
|
|
29028
|
+
"Use the recent context and do not ask again for information already provided.",
|
|
29029
|
+
"",
|
|
29030
|
+
"Recent conversation context (oldest to newest):",
|
|
29031
|
+
recentContext,
|
|
29032
|
+
"",
|
|
28697
29033
|
"Message body:",
|
|
28698
29034
|
frame.text
|
|
28699
29035
|
].filter(Boolean).join("\n");
|
|
@@ -28710,14 +29046,38 @@ function register(api) {
|
|
|
28710
29046
|
logger.info(`[perkos-chat] dropped chat_deliver for ${frame.convId} (runtime=none)`);
|
|
28711
29047
|
return;
|
|
28712
29048
|
}
|
|
29049
|
+
const chatSessionKey = openclawChatSessionKey(pluginConfig, frame.convId);
|
|
29050
|
+
await prepareOpenClawChatSession(api, pluginConfig, chatSessionKey, logger);
|
|
29051
|
+
if (walletAddress) {
|
|
29052
|
+
const reply = await runOpenClawChatTurn(
|
|
29053
|
+
api,
|
|
29054
|
+
pluginConfig,
|
|
29055
|
+
chatSessionKey,
|
|
29056
|
+
directRunPrompt,
|
|
29057
|
+
frame.text,
|
|
29058
|
+
logger
|
|
29059
|
+
);
|
|
29060
|
+
if (reply) {
|
|
29061
|
+
const result = await chatClient?.sendReply({
|
|
29062
|
+
convId: frame.convId,
|
|
29063
|
+
walletAddress,
|
|
29064
|
+
text: reply,
|
|
29065
|
+
replyTo: frame.id
|
|
29066
|
+
});
|
|
29067
|
+
logger.info(
|
|
29068
|
+
`[perkos-chat] embedded reply ${result?.delivered ? "delivered" : "queued (offline)"} for ${frame.convId}`
|
|
29069
|
+
);
|
|
29070
|
+
return;
|
|
29071
|
+
}
|
|
29072
|
+
}
|
|
28713
29073
|
if (enqueueSystemEvent) {
|
|
28714
|
-
enqueueSystemEvent(eventText, { sessionKey:
|
|
29074
|
+
enqueueSystemEvent(eventText, { sessionKey: chatSessionKey });
|
|
28715
29075
|
}
|
|
28716
29076
|
wakeGatewayAgent(
|
|
28717
29077
|
requestHeartbeatNow,
|
|
28718
29078
|
`[PerkOS-Chat] inbound message in ${frame.convId}`,
|
|
28719
29079
|
logger,
|
|
28720
|
-
|
|
29080
|
+
chatSessionKey
|
|
28721
29081
|
);
|
|
28722
29082
|
},
|
|
28723
29083
|
onChannelJoin: async (frame) => {
|
|
@@ -28741,6 +29101,29 @@ function register(api) {
|
|
|
28741
29101
|
}
|
|
28742
29102
|
});
|
|
28743
29103
|
}
|
|
29104
|
+
if (pluginConfig.platform?.heartbeatUrl) {
|
|
29105
|
+
let platformHeartbeat = null;
|
|
29106
|
+
api.registerService({
|
|
29107
|
+
id: "perkos-a2a-platform-heartbeat",
|
|
29108
|
+
start: () => {
|
|
29109
|
+
platformHeartbeat = startPlatformHeartbeat({
|
|
29110
|
+
logger: {
|
|
29111
|
+
info: (message) => logger.info(message),
|
|
29112
|
+
warn: (message) => (logger.warn || logger.info).call(logger, message)
|
|
29113
|
+
},
|
|
29114
|
+
url: pluginConfig.platform?.heartbeatUrl,
|
|
29115
|
+
relayKey: pluginConfig.relay?.apiKey,
|
|
29116
|
+
runtimeKind: "openclaw",
|
|
29117
|
+
version: "0.12.26",
|
|
29118
|
+
intervalMs: pluginConfig.platform?.heartbeatIntervalMs
|
|
29119
|
+
});
|
|
29120
|
+
},
|
|
29121
|
+
stop: () => {
|
|
29122
|
+
platformHeartbeat?.stop();
|
|
29123
|
+
platformHeartbeat = null;
|
|
29124
|
+
}
|
|
29125
|
+
});
|
|
29126
|
+
}
|
|
28744
29127
|
api.registerTool({
|
|
28745
29128
|
name: "perkos_a2a_send",
|
|
28746
29129
|
description: "Send a task to another agent via PerkOS A2A. Use this to delegate work, ask a peer to research, or route a chain/circular workflow. The message may name additional agents and a final notification target; the bridge preserves the route and prevents loops.",
|
|
@@ -29122,10 +29505,15 @@ export {
|
|
|
29122
29505
|
ChatStore,
|
|
29123
29506
|
RelayClient,
|
|
29124
29507
|
RelayHub,
|
|
29508
|
+
alignOpenClawChatSessionModel,
|
|
29125
29509
|
register as default,
|
|
29126
29510
|
detectNetworking,
|
|
29511
|
+
extractOpenClawChatReply,
|
|
29512
|
+
formatRecentChatContext,
|
|
29127
29513
|
isWebhookEvent,
|
|
29128
|
-
|
|
29514
|
+
omitUnavailableAgentHarness,
|
|
29515
|
+
openclawChatSessionKey,
|
|
29516
|
+
runOpenClawChatTurn
|
|
29129
29517
|
};
|
|
29130
29518
|
/*! Bundled license information:
|
|
29131
29519
|
|