@agentchatme/openclaw 0.7.8 → 0.7.82
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/CHANGELOG.md +232 -176
- package/LICENSE +21 -21
- package/README.md +308 -297
- package/RUNBOOK.md +134 -134
- package/SECURITY.md +104 -104
- package/dist/binding/agents-anchor.cjs.map +1 -1
- package/dist/binding/agents-anchor.d.cts +1 -1
- package/dist/binding/agents-anchor.d.ts +1 -1
- package/dist/binding/agents-anchor.js.map +1 -1
- package/dist/configured-state.cjs.map +1 -1
- package/dist/configured-state.js.map +1 -1
- package/dist/credentials/read-env.cjs.map +1 -1
- package/dist/credentials/read-env.js.map +1 -1
- package/dist/index.cjs +858 -133
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +840 -134
- package/dist/index.js.map +1 -1
- package/dist/setup-entry.cjs +856 -131
- package/dist/setup-entry.cjs.map +1 -1
- package/dist/setup-entry.js +838 -132
- package/dist/setup-entry.js.map +1 -1
- package/icon.svg +5 -5
- package/openclaw.plugin.json +1 -1
- package/package.json +20 -16
- package/skills/agentchat/SKILL.md +331 -331
package/dist/index.cjs
CHANGED
|
@@ -10,15 +10,40 @@ var zod = require('zod');
|
|
|
10
10
|
var channelLifecycle = require('openclaw/plugin-sdk/channel-lifecycle');
|
|
11
11
|
var pino = require('pino');
|
|
12
12
|
var ws = require('ws');
|
|
13
|
-
var directDm = require('openclaw/plugin-sdk/direct-dm');
|
|
14
13
|
var inboundEnvelope = require('openclaw/plugin-sdk/inbound-envelope');
|
|
15
14
|
var inboundReplyDispatch = require('openclaw/plugin-sdk/inbound-reply-dispatch');
|
|
15
|
+
var fs2 = require('fs');
|
|
16
|
+
var os = require('os');
|
|
17
|
+
var path2 = require('path');
|
|
18
|
+
var readEnv_js$1 = require('./credentials/read-env.cjs');
|
|
16
19
|
var agentchatme = require('agentchatme');
|
|
20
|
+
var simpleCompletionRuntime = require('openclaw/plugin-sdk/simple-completion-runtime');
|
|
17
21
|
var typebox = require('@sinclair/typebox');
|
|
18
22
|
|
|
19
23
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
20
24
|
|
|
25
|
+
function _interopNamespace(e) {
|
|
26
|
+
if (e && e.__esModule) return e;
|
|
27
|
+
var n = Object.create(null);
|
|
28
|
+
if (e) {
|
|
29
|
+
Object.keys(e).forEach(function (k) {
|
|
30
|
+
if (k !== 'default') {
|
|
31
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
32
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
33
|
+
enumerable: true,
|
|
34
|
+
get: function () { return e[k]; }
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
n.default = e;
|
|
40
|
+
return Object.freeze(n);
|
|
41
|
+
}
|
|
42
|
+
|
|
21
43
|
var pino__default = /*#__PURE__*/_interopDefault(pino);
|
|
44
|
+
var fs2__default = /*#__PURE__*/_interopDefault(fs2);
|
|
45
|
+
var os__namespace = /*#__PURE__*/_interopNamespace(os);
|
|
46
|
+
var path2__namespace = /*#__PURE__*/_interopNamespace(path2);
|
|
22
47
|
|
|
23
48
|
// src/channel.ts
|
|
24
49
|
|
|
@@ -271,9 +296,9 @@ async function registerAgentVerify(input, opts = {}) {
|
|
|
271
296
|
}
|
|
272
297
|
return { ok: false, reason: "server-error", status: res.status, message };
|
|
273
298
|
}
|
|
274
|
-
async function post(
|
|
299
|
+
async function post(path3, body, opts) {
|
|
275
300
|
const base = (opts.apiBase ?? DEFAULT_API_BASE).replace(/\/+$/, "");
|
|
276
|
-
const url = `${base}${
|
|
301
|
+
const url = `${base}${path3}`;
|
|
277
302
|
const controller = new AbortController();
|
|
278
303
|
const fetchImpl = opts.fetch ?? fetch;
|
|
279
304
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
@@ -916,9 +941,9 @@ function createLogger(options) {
|
|
|
916
941
|
function buildRedactPaths(keys) {
|
|
917
942
|
const prefixes = ["", "config.", "headers.", "context.", "req.headers.", "res.headers."];
|
|
918
943
|
const paths = [];
|
|
919
|
-
for (const
|
|
944
|
+
for (const key2 of keys) {
|
|
920
945
|
for (const prefix of prefixes) {
|
|
921
|
-
paths.push(`${prefix}${
|
|
946
|
+
paths.push(`${prefix}${key2}`);
|
|
922
947
|
}
|
|
923
948
|
}
|
|
924
949
|
paths.push(...keys.map((k) => `*.${k}`));
|
|
@@ -1525,6 +1550,19 @@ var messageContentSchema = zod.z.object({
|
|
|
1525
1550
|
data: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
|
|
1526
1551
|
attachment_id: zod.z.string().optional()
|
|
1527
1552
|
}).passthrough();
|
|
1553
|
+
var messageContextSchema = zod.z.object({
|
|
1554
|
+
sender: zod.z.object({
|
|
1555
|
+
handle: zod.z.string(),
|
|
1556
|
+
display_name: zod.z.string().nullable().optional(),
|
|
1557
|
+
kind: zod.z.enum(["agent", "system"]).optional()
|
|
1558
|
+
}).passthrough().optional(),
|
|
1559
|
+
conversation: zod.z.object({
|
|
1560
|
+
type: zod.z.enum(["direct", "group"]).optional(),
|
|
1561
|
+
group_name: zod.z.string().nullable().optional(),
|
|
1562
|
+
member_count: zod.z.number().int().nullable().optional()
|
|
1563
|
+
}).passthrough().optional(),
|
|
1564
|
+
mentions: zod.z.array(zod.z.string()).optional()
|
|
1565
|
+
}).passthrough();
|
|
1528
1566
|
var messageSchema = zod.z.object({
|
|
1529
1567
|
id: zod.z.string(),
|
|
1530
1568
|
conversation_id: zod.z.string(),
|
|
@@ -1534,6 +1572,7 @@ var messageSchema = zod.z.object({
|
|
|
1534
1572
|
type: zod.z.enum(["text", "structured", "file", "system"]),
|
|
1535
1573
|
content: messageContentSchema,
|
|
1536
1574
|
metadata: zod.z.record(zod.z.string(), zod.z.unknown()).default({}),
|
|
1575
|
+
context: messageContextSchema.optional(),
|
|
1537
1576
|
// Per-recipient delivery state lives in `message_deliveries` since
|
|
1538
1577
|
// migration 011 — the `messages` row no longer carries `status`,
|
|
1539
1578
|
// `delivered_at`, or `read_at`. Fresh-send envelopes over WS omit
|
|
@@ -1655,7 +1694,12 @@ function normalizeMessageNew(frame) {
|
|
|
1655
1694
|
createdAt: msg.created_at,
|
|
1656
1695
|
deliveredAt: msg.delivered_at ?? null,
|
|
1657
1696
|
readAt: msg.read_at ?? null,
|
|
1658
|
-
receivedAt: frame.receivedAt
|
|
1697
|
+
receivedAt: frame.receivedAt,
|
|
1698
|
+
senderDisplayName: msg.context?.sender?.display_name ?? null,
|
|
1699
|
+
senderKind: msg.context?.sender?.kind === "system" ? "system" : "agent",
|
|
1700
|
+
groupName: msg.context?.conversation?.group_name ?? null,
|
|
1701
|
+
memberCount: msg.context?.conversation?.member_count ?? null,
|
|
1702
|
+
mentions: (msg.context?.mentions ?? []).map((m) => m.toLowerCase())
|
|
1659
1703
|
};
|
|
1660
1704
|
}
|
|
1661
1705
|
function normalizeMessageRead(frame) {
|
|
@@ -1734,7 +1778,7 @@ function normalizeGroupDeleted(frame) {
|
|
|
1734
1778
|
|
|
1735
1779
|
// src/retry.ts
|
|
1736
1780
|
function defaultSleep(ms) {
|
|
1737
|
-
return new Promise((
|
|
1781
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1738
1782
|
}
|
|
1739
1783
|
async function retryWithPolicy(fn, policy) {
|
|
1740
1784
|
const random = policy.random ?? Math.random;
|
|
@@ -1862,7 +1906,7 @@ var CircuitBreaker = class {
|
|
|
1862
1906
|
};
|
|
1863
1907
|
|
|
1864
1908
|
// src/version.ts
|
|
1865
|
-
var PACKAGE_VERSION = "0.7.
|
|
1909
|
+
var PACKAGE_VERSION = "0.7.82";
|
|
1866
1910
|
|
|
1867
1911
|
// src/outbound.ts
|
|
1868
1912
|
var DEFAULT_RETRY_POLICY = {
|
|
@@ -2078,11 +2122,11 @@ var OutboundAdapter = class {
|
|
|
2078
2122
|
`outbound queue full (${this.queue.length}) \u2014 shedding load`
|
|
2079
2123
|
);
|
|
2080
2124
|
}
|
|
2081
|
-
return new Promise((
|
|
2125
|
+
return new Promise((resolve2) => {
|
|
2082
2126
|
this.queue.push(() => {
|
|
2083
2127
|
this.inFlight++;
|
|
2084
2128
|
this.metrics.setInFlightDepth(this.inFlight);
|
|
2085
|
-
|
|
2129
|
+
resolve2();
|
|
2086
2130
|
});
|
|
2087
2131
|
});
|
|
2088
2132
|
}
|
|
@@ -2158,10 +2202,10 @@ var AgentchatChannelRuntime = class {
|
|
|
2158
2202
|
stop(deadlineMs) {
|
|
2159
2203
|
if (this.stopPromise) return this.stopPromise;
|
|
2160
2204
|
const deadline = deadlineMs ?? this.now() + 5e3;
|
|
2161
|
-
this.stopPromise = new Promise((
|
|
2205
|
+
this.stopPromise = new Promise((resolve2) => {
|
|
2162
2206
|
const off = this.ws.on("closed", () => {
|
|
2163
2207
|
off();
|
|
2164
|
-
|
|
2208
|
+
resolve2();
|
|
2165
2209
|
});
|
|
2166
2210
|
this.ws.stop(deadline);
|
|
2167
2211
|
});
|
|
@@ -2391,6 +2435,476 @@ function unregisterRuntime(accountId, deadlineMs = Date.now() + 5e3) {
|
|
|
2391
2435
|
function getRuntime(accountId) {
|
|
2392
2436
|
return registry.get(accountId)?.runtime;
|
|
2393
2437
|
}
|
|
2438
|
+
function resolveWorkspaceDir(cfg) {
|
|
2439
|
+
const configured = cfg?.agents?.defaults?.workspace;
|
|
2440
|
+
if (typeof configured === "string" && configured.trim().length > 0) {
|
|
2441
|
+
return path2__namespace.resolve(configured);
|
|
2442
|
+
}
|
|
2443
|
+
const profile = readEnv_js$1.readOpenClawProfileFromEnv();
|
|
2444
|
+
if (profile) {
|
|
2445
|
+
return path2__namespace.join(os__namespace.homedir(), ".openclaw", `workspace-${profile}`);
|
|
2446
|
+
}
|
|
2447
|
+
return path2__namespace.join(os__namespace.homedir(), ".openclaw", "workspace");
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// src/binding/thread-closures.ts
|
|
2451
|
+
var instances = /* @__PURE__ */ new Map();
|
|
2452
|
+
function fallbackWorkspaceDir() {
|
|
2453
|
+
const profile = process.env.OPENCLAW_PROFILE?.trim();
|
|
2454
|
+
if (profile && profile.toLowerCase() !== "default") {
|
|
2455
|
+
return path2__namespace.default.join(os__namespace.default.homedir(), ".openclaw", `workspace-${profile}`);
|
|
2456
|
+
}
|
|
2457
|
+
return path2__namespace.default.join(os__namespace.default.homedir(), ".openclaw", "workspace");
|
|
2458
|
+
}
|
|
2459
|
+
function resolveStatePath(cfg, accountId) {
|
|
2460
|
+
const workspaceDir = typeof cfg === "object" && cfg !== null ? resolveWorkspaceDir(cfg) : fallbackWorkspaceDir();
|
|
2461
|
+
return path2__namespace.default.join(workspaceDir, `.agentchat-closed-threads-${accountId}.json`);
|
|
2462
|
+
}
|
|
2463
|
+
var ThreadClosures = class {
|
|
2464
|
+
filePath;
|
|
2465
|
+
closed = /* @__PURE__ */ new Map();
|
|
2466
|
+
constructor(filePath) {
|
|
2467
|
+
this.filePath = filePath;
|
|
2468
|
+
this.load();
|
|
2469
|
+
}
|
|
2470
|
+
isClosed(conversationId) {
|
|
2471
|
+
return this.closed.has(conversationId);
|
|
2472
|
+
}
|
|
2473
|
+
close(conversationId, reason) {
|
|
2474
|
+
const record = {
|
|
2475
|
+
conversationId,
|
|
2476
|
+
closedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2477
|
+
reason: reason?.trim() ? reason.trim() : null
|
|
2478
|
+
};
|
|
2479
|
+
this.closed.set(conversationId, record);
|
|
2480
|
+
this.save();
|
|
2481
|
+
return record;
|
|
2482
|
+
}
|
|
2483
|
+
reopen(conversationId) {
|
|
2484
|
+
const existed = this.closed.delete(conversationId);
|
|
2485
|
+
if (existed) this.save();
|
|
2486
|
+
return existed;
|
|
2487
|
+
}
|
|
2488
|
+
list() {
|
|
2489
|
+
return [...this.closed.values()].sort((a, b) => b.closedAt.localeCompare(a.closedAt));
|
|
2490
|
+
}
|
|
2491
|
+
load() {
|
|
2492
|
+
if (!fs2__default.default.existsSync(this.filePath)) return;
|
|
2493
|
+
try {
|
|
2494
|
+
const raw = JSON.parse(fs2__default.default.readFileSync(this.filePath, "utf8"));
|
|
2495
|
+
if (!Array.isArray(raw)) return;
|
|
2496
|
+
for (const entry of raw) {
|
|
2497
|
+
if (!entry || typeof entry !== "object") continue;
|
|
2498
|
+
const conversationId = entry.conversationId;
|
|
2499
|
+
const closedAt = entry.closedAt;
|
|
2500
|
+
const reason = entry.reason;
|
|
2501
|
+
if (typeof conversationId !== "string" || typeof closedAt !== "string") continue;
|
|
2502
|
+
this.closed.set(conversationId, {
|
|
2503
|
+
conversationId,
|
|
2504
|
+
closedAt,
|
|
2505
|
+
reason: typeof reason === "string" ? reason : null
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
} catch {
|
|
2509
|
+
this.closed = /* @__PURE__ */ new Map();
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
save() {
|
|
2513
|
+
fs2__default.default.mkdirSync(path2__namespace.default.dirname(this.filePath), { recursive: true });
|
|
2514
|
+
const payload = JSON.stringify(this.list(), null, 2);
|
|
2515
|
+
const tempPath = `${this.filePath}.tmp`;
|
|
2516
|
+
fs2__default.default.writeFileSync(tempPath, payload, "utf8");
|
|
2517
|
+
fs2__default.default.renameSync(tempPath, this.filePath);
|
|
2518
|
+
}
|
|
2519
|
+
};
|
|
2520
|
+
function getThreadClosures(cfg, accountId) {
|
|
2521
|
+
const filePath = resolveStatePath(cfg, accountId);
|
|
2522
|
+
const existing = instances.get(filePath);
|
|
2523
|
+
if (existing) return existing;
|
|
2524
|
+
const created = new ThreadClosures(filePath);
|
|
2525
|
+
instances.set(filePath, created);
|
|
2526
|
+
return created;
|
|
2527
|
+
}
|
|
2528
|
+
var cache = /* @__PURE__ */ new Map();
|
|
2529
|
+
function getClient({ accountId, config, options }) {
|
|
2530
|
+
const existing = cache.get(accountId);
|
|
2531
|
+
if (existing && existing.apiKey === config.apiKey && existing.apiBase === config.apiBase) {
|
|
2532
|
+
return existing.client;
|
|
2533
|
+
}
|
|
2534
|
+
const client = new agentchatme.AgentChatClient({
|
|
2535
|
+
apiKey: config.apiKey,
|
|
2536
|
+
baseUrl: config.apiBase,
|
|
2537
|
+
...options
|
|
2538
|
+
});
|
|
2539
|
+
cache.set(accountId, {
|
|
2540
|
+
client,
|
|
2541
|
+
apiKey: config.apiKey,
|
|
2542
|
+
apiBase: config.apiBase
|
|
2543
|
+
});
|
|
2544
|
+
return client;
|
|
2545
|
+
}
|
|
2546
|
+
function disposeClient(accountId) {
|
|
2547
|
+
cache.delete(accountId);
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
// src/binding/send-tracker.ts
|
|
2551
|
+
var lastSendByKey = /* @__PURE__ */ new Map();
|
|
2552
|
+
var MAX_TRACKED = 512;
|
|
2553
|
+
function key(accountId, conversationId) {
|
|
2554
|
+
return `${accountId}\0${conversationId}`;
|
|
2555
|
+
}
|
|
2556
|
+
function recordAgentSend(accountId, conversationId, atMs = Date.now()) {
|
|
2557
|
+
if (!conversationId) return;
|
|
2558
|
+
const k = key(accountId, conversationId);
|
|
2559
|
+
lastSendByKey.delete(k);
|
|
2560
|
+
lastSendByKey.set(k, atMs);
|
|
2561
|
+
if (lastSendByKey.size > MAX_TRACKED) {
|
|
2562
|
+
const oldest = lastSendByKey.keys().next().value;
|
|
2563
|
+
if (oldest !== void 0) lastSendByKey.delete(oldest);
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
function hasAgentSendSince(accountId, conversationId, sinceMs) {
|
|
2567
|
+
const at = lastSendByKey.get(key(accountId, conversationId));
|
|
2568
|
+
return at !== void 0 && at >= sinceMs;
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
// src/binding/reply-gate.ts
|
|
2572
|
+
var DEFAULT_GATE_MAX_TOKENS = 256;
|
|
2573
|
+
var MAX_HISTORY_TURNS = 12;
|
|
2574
|
+
var CADENCE_WINDOW_SECONDS = 60;
|
|
2575
|
+
var VALID_CATEGORIES = /* @__PURE__ */ new Set([
|
|
2576
|
+
"open_request",
|
|
2577
|
+
"new_info",
|
|
2578
|
+
"goal_followup",
|
|
2579
|
+
"closing",
|
|
2580
|
+
"acknowledgement",
|
|
2581
|
+
"not_addressed",
|
|
2582
|
+
"no_action_needed",
|
|
2583
|
+
"spam",
|
|
2584
|
+
"other"
|
|
2585
|
+
]);
|
|
2586
|
+
var REPLY_TOKENS = /* @__PURE__ */ new Set(["reply", "yes", "true", "respond"]);
|
|
2587
|
+
var NO_REPLY_TOKENS = /* @__PURE__ */ new Set([
|
|
2588
|
+
"no_reply",
|
|
2589
|
+
"no-reply",
|
|
2590
|
+
"noreply",
|
|
2591
|
+
"no",
|
|
2592
|
+
"false",
|
|
2593
|
+
"silent",
|
|
2594
|
+
"skip",
|
|
2595
|
+
"none"
|
|
2596
|
+
]);
|
|
2597
|
+
var FENCE_RE = /```(?:json)?\s*([\s\S]+?)```/i;
|
|
2598
|
+
function computeConversationSignals(messages, params) {
|
|
2599
|
+
const windowSeconds = params.windowSeconds ?? CADENCE_WINDOW_SECONDS;
|
|
2600
|
+
const own = normalizeHandle(params.ownHandle);
|
|
2601
|
+
const prior = messages.filter(
|
|
2602
|
+
(m) => isRecord(m) && m.id !== params.triggerMessageId
|
|
2603
|
+
);
|
|
2604
|
+
const firstContact = prior.length === 0;
|
|
2605
|
+
const youHaveSpoken = prior.some((m) => messageIsOwn(m, own));
|
|
2606
|
+
const timestamps = [];
|
|
2607
|
+
for (const m of prior) {
|
|
2608
|
+
const ts = parseTimestamp(m.created_at);
|
|
2609
|
+
if (ts !== null) timestamps.push(ts);
|
|
2610
|
+
}
|
|
2611
|
+
const cutoff = params.nowMs - windowSeconds * 1e3;
|
|
2612
|
+
const recentInWindow = timestamps.filter((ts) => ts >= cutoff).length;
|
|
2613
|
+
let secondsSincePrevious = null;
|
|
2614
|
+
if (timestamps.length > 0) {
|
|
2615
|
+
const delta = (params.nowMs - Math.max(...timestamps)) / 1e3;
|
|
2616
|
+
secondsSincePrevious = delta > 0 ? delta : 0;
|
|
2617
|
+
}
|
|
2618
|
+
return {
|
|
2619
|
+
firstContact,
|
|
2620
|
+
youHaveSpoken,
|
|
2621
|
+
messagesLastWindow: recentInWindow + 1,
|
|
2622
|
+
// +1 for the new message
|
|
2623
|
+
secondsSincePrevious
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2626
|
+
function messageIsOwn(msg, ownHandleNorm) {
|
|
2627
|
+
if (typeof msg.is_own === "boolean") return msg.is_own;
|
|
2628
|
+
const sender = msg.sender ?? msg.from ?? msg.sender_handle ?? "";
|
|
2629
|
+
return normalizeHandle(String(sender)) === ownHandleNorm;
|
|
2630
|
+
}
|
|
2631
|
+
function parseTimestamp(raw) {
|
|
2632
|
+
if (typeof raw !== "string" || !raw) return null;
|
|
2633
|
+
const t = Date.parse(raw);
|
|
2634
|
+
return Number.isNaN(t) ? null : t;
|
|
2635
|
+
}
|
|
2636
|
+
function normalizeHandle(handle) {
|
|
2637
|
+
return handle.replace(/^@/, "").toLowerCase();
|
|
2638
|
+
}
|
|
2639
|
+
function isRecord(value) {
|
|
2640
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2641
|
+
}
|
|
2642
|
+
function systemTemplate(handle) {
|
|
2643
|
+
const h = `@${handle}`;
|
|
2644
|
+
return `You are the reply gate for ${h}, an autonomous agent on AgentChat (a peer-to-peer messaging network for AI agents). A message just arrived. Your only job is to decide whether ${h} should reply to it now. You do NOT write the reply \u2014 you output one decision.
|
|
2645
|
+
|
|
2646
|
+
"no_reply" is a SUCCESS, not a failure. Most healthy conversations are SUPPOSED to end; going quiet is the normal, correct outcome and is never rude here \u2014 on this network silence IS the acknowledgement.
|
|
2647
|
+
|
|
2648
|
+
Judge DONE-NESS, not how interesting another message could be:
|
|
2649
|
+
|
|
2650
|
+
Choose "reply" only if a further message accomplishes a concrete, still-OPEN purpose:
|
|
2651
|
+
- answer a substantive pending question or supply specifically requested information
|
|
2652
|
+
- make or respond to a decision, or unblock the peer on a real task
|
|
2653
|
+
- ${h} started this thread toward a goal and the peer's reply needs a substantive follow-up to reach it
|
|
2654
|
+
- new information genuinely requires ${h}'s input
|
|
2655
|
+
|
|
2656
|
+
Choose "no_reply" when the exchange has reached its natural end \u2014 even if another clever or friendly message is easily possible:
|
|
2657
|
+
- it is trading pleasantries, mutual appreciation, agreement, or open-ended tangents / "riffing" with no open objective
|
|
2658
|
+
- a courtesy question that merely mirrors the exchange back ("and you?", "what are you working on?", "what tools are you using?") after the substantive part has run its course is part of the pleasantry, not an open task \u2014 it does not oblige a reply
|
|
2659
|
+
- the other side is acknowledging or closing out (thanks / ok / got it / sounds good / \u{1F44D} / bye) and replying would only prolong it
|
|
2660
|
+
- ${h} already answered what was asked and nothing new is on the table
|
|
2661
|
+
- in a group, the message is not addressed to ${h} and does not need it. (Groups only \u2014 in a direct conversation every message is addressed to ${h} by definition, so "not_addressed" never applies there.)
|
|
2662
|
+
|
|
2663
|
+
"I could add something" is NOT a reason to reply. "Something concrete is unresolved and my reply resolves it" IS. Two agents keeping a chat alive by each politely asking the next question is the exact failure you exist to prevent \u2014 the thread being pleasant does not make it open. If the Pace line shows messages flying back and forth with each turn only restating, appreciating, or re-asking a mirrored question, that IS the loop \u2014 choose "no_reply". When unsure, prefer "no_reply".
|
|
2664
|
+
|
|
2665
|
+
Respond with ONLY a JSON object \u2014 no prose, no markdown fences:
|
|
2666
|
+
{"decision": "reply" or "no_reply", "reason": "<one short sentence>", "category": "<one of: open_request, new_info, goal_followup, closing, acknowledgement, not_addressed, no_action_needed, spam, other>"}`;
|
|
2667
|
+
}
|
|
2668
|
+
function buildDecisionMessages(params) {
|
|
2669
|
+
return [
|
|
2670
|
+
{ role: "system", content: systemTemplate(params.handle) },
|
|
2671
|
+
{
|
|
2672
|
+
role: "user",
|
|
2673
|
+
content: buildUserContent({
|
|
2674
|
+
handle: params.handle,
|
|
2675
|
+
event: params.event,
|
|
2676
|
+
history: params.history,
|
|
2677
|
+
signals: params.signals ?? null,
|
|
2678
|
+
maxHistory: params.maxHistory ?? MAX_HISTORY_TURNS
|
|
2679
|
+
})
|
|
2680
|
+
}
|
|
2681
|
+
];
|
|
2682
|
+
}
|
|
2683
|
+
function formatReceivedAt(ms) {
|
|
2684
|
+
if (!Number.isFinite(ms)) return "an unknown time";
|
|
2685
|
+
const iso = new Date(ms).toISOString();
|
|
2686
|
+
return `${iso.slice(0, 10)} ${iso.slice(11, 16)} UTC`;
|
|
2687
|
+
}
|
|
2688
|
+
function formatConversationContext(params) {
|
|
2689
|
+
const { handle, event, signals, priorCount } = params;
|
|
2690
|
+
const lines = [`Conversation type: ${formatConversationLabel(event)}`];
|
|
2691
|
+
if (signals) {
|
|
2692
|
+
lines.push(`Relationship: ${relationshipPhrase(signals)}`);
|
|
2693
|
+
if (signals.secondsSincePrevious !== null) {
|
|
2694
|
+
lines.push(
|
|
2695
|
+
`Pace: ${signals.messagesLastWindow} message(s) in the last ${CADENCE_WINDOW_SECONDS}s; ${formatGap(signals.secondsSincePrevious)} since the previous message`
|
|
2696
|
+
);
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
lines.push(`Prior messages in this thread: ${priorCount}`);
|
|
2700
|
+
if (event.conversationKind === "group" && handle && (event.mentions ?? []).includes(handle.toLowerCase())) {
|
|
2701
|
+
lines.push("You were @-mentioned in this message.");
|
|
2702
|
+
}
|
|
2703
|
+
return lines;
|
|
2704
|
+
}
|
|
2705
|
+
function formatConversationLabel(event) {
|
|
2706
|
+
if (event.conversationKind !== "group") return "direct";
|
|
2707
|
+
let label = event.groupName ? `group "${event.groupName}"` : "group";
|
|
2708
|
+
if (event.memberCount != null) {
|
|
2709
|
+
label += ` (${event.memberCount} member${event.memberCount === 1 ? "" : "s"})`;
|
|
2710
|
+
}
|
|
2711
|
+
return label;
|
|
2712
|
+
}
|
|
2713
|
+
function buildUserContent(params) {
|
|
2714
|
+
const { handle, event, history, signals, maxHistory } = params;
|
|
2715
|
+
const lines = formatConversationContext({
|
|
2716
|
+
handle,
|
|
2717
|
+
event,
|
|
2718
|
+
signals,
|
|
2719
|
+
priorCount: history.length
|
|
2720
|
+
});
|
|
2721
|
+
lines.push("");
|
|
2722
|
+
const rendered = renderHistory(history, maxHistory);
|
|
2723
|
+
if (rendered.length > 0) {
|
|
2724
|
+
lines.push("Recent conversation (oldest first):");
|
|
2725
|
+
lines.push(...rendered);
|
|
2726
|
+
} else {
|
|
2727
|
+
lines.push("Recent conversation: (none \u2014 this is first contact)");
|
|
2728
|
+
}
|
|
2729
|
+
let newText = collapseWhitespace(event.contentText || "");
|
|
2730
|
+
if (newText.length > 2e3) newText = `${newText.slice(0, 2e3)}\u2026`;
|
|
2731
|
+
lines.push("");
|
|
2732
|
+
lines.push(`New message from @${event.senderHandle}: ${newText}`);
|
|
2733
|
+
lines.push("");
|
|
2734
|
+
lines.push("Decide now: reply or no_reply?");
|
|
2735
|
+
return lines.join("\n");
|
|
2736
|
+
}
|
|
2737
|
+
function renderHistory(history, maxHistory) {
|
|
2738
|
+
const recent = history.length > maxHistory ? history.slice(history.length - maxHistory) : history;
|
|
2739
|
+
const out = [];
|
|
2740
|
+
for (const turn of recent) {
|
|
2741
|
+
if (typeof turn.content !== "string" || !turn.content.trim()) continue;
|
|
2742
|
+
const speaker = turn.role === "assistant" ? "you" : "peer";
|
|
2743
|
+
let text = collapseWhitespace(turn.content);
|
|
2744
|
+
if (text.length > 400) text = `${text.slice(0, 400)}\u2026`;
|
|
2745
|
+
out.push(`${speaker}: ${text}`);
|
|
2746
|
+
}
|
|
2747
|
+
return out;
|
|
2748
|
+
}
|
|
2749
|
+
function relationshipPhrase(signals) {
|
|
2750
|
+
if (signals.firstContact) return "first contact \u2014 no prior messages in this thread";
|
|
2751
|
+
if (signals.youHaveSpoken) return "established \u2014 you have already replied in this thread";
|
|
2752
|
+
return "this peer is messaging you, but you have not replied yet";
|
|
2753
|
+
}
|
|
2754
|
+
function formatGap(seconds) {
|
|
2755
|
+
if (seconds < 90) return `${Math.round(seconds)}s`;
|
|
2756
|
+
if (seconds < 5400) return `${Math.round(seconds / 60)}m`;
|
|
2757
|
+
return `${Math.round(seconds / 3600)}h`;
|
|
2758
|
+
}
|
|
2759
|
+
function collapseWhitespace(text) {
|
|
2760
|
+
return text.split(/\s+/).filter(Boolean).join(" ");
|
|
2761
|
+
}
|
|
2762
|
+
function parseDecision(text, opts = {}) {
|
|
2763
|
+
const raw = extractJson(text);
|
|
2764
|
+
if (raw === null) return null;
|
|
2765
|
+
let obj;
|
|
2766
|
+
try {
|
|
2767
|
+
obj = JSON.parse(raw);
|
|
2768
|
+
} catch {
|
|
2769
|
+
return null;
|
|
2770
|
+
}
|
|
2771
|
+
if (!isRecord(obj)) return null;
|
|
2772
|
+
const decisionRaw = obj.decision;
|
|
2773
|
+
if (typeof decisionRaw !== "string") return null;
|
|
2774
|
+
const token = decisionRaw.trim().toLowerCase();
|
|
2775
|
+
let reply;
|
|
2776
|
+
if (REPLY_TOKENS.has(token)) reply = true;
|
|
2777
|
+
else if (NO_REPLY_TOKENS.has(token)) reply = false;
|
|
2778
|
+
else return null;
|
|
2779
|
+
const reasonRaw = obj.reason;
|
|
2780
|
+
let reason = typeof reasonRaw === "string" ? reasonRaw.trim() : "";
|
|
2781
|
+
if (reason.length > 280) reason = reason.slice(0, 280);
|
|
2782
|
+
const categoryRaw = obj.category;
|
|
2783
|
+
let category = typeof categoryRaw === "string" ? categoryRaw.trim().toLowerCase() : "other";
|
|
2784
|
+
if (!VALID_CATEGORIES.has(category)) category = "other";
|
|
2785
|
+
return {
|
|
2786
|
+
reply,
|
|
2787
|
+
reason,
|
|
2788
|
+
category,
|
|
2789
|
+
source: opts.source ?? "llm",
|
|
2790
|
+
latencyMs: opts.latencyMs ?? 0
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
function extractJson(text) {
|
|
2794
|
+
if (!text || !text.trim()) return null;
|
|
2795
|
+
let s = text.trim();
|
|
2796
|
+
const fence = FENCE_RE.exec(s);
|
|
2797
|
+
if (fence?.[1]) s = fence[1].trim();
|
|
2798
|
+
const start = s.indexOf("{");
|
|
2799
|
+
const end = s.lastIndexOf("}");
|
|
2800
|
+
if (start === -1 || end === -1 || end <= start) return null;
|
|
2801
|
+
return s.slice(start, end + 1);
|
|
2802
|
+
}
|
|
2803
|
+
function gateFallback(failOpen, reason, latencyMs) {
|
|
2804
|
+
return {
|
|
2805
|
+
reply: failOpen,
|
|
2806
|
+
reason,
|
|
2807
|
+
category: "fallback",
|
|
2808
|
+
source: failOpen ? "fail_open" : "fail_closed",
|
|
2809
|
+
latencyMs
|
|
2810
|
+
};
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
// src/binding/gate.ts
|
|
2814
|
+
var DEFAULT_GATE_TIMEOUT_MS = 2e4;
|
|
2815
|
+
var GATE_REASONING_LEVEL = "off";
|
|
2816
|
+
var GateTimeoutError = class extends Error {
|
|
2817
|
+
};
|
|
2818
|
+
function withTimeout(promise, ms) {
|
|
2819
|
+
if (!Number.isFinite(ms) || ms <= 0) return promise;
|
|
2820
|
+
return new Promise((resolve2, reject) => {
|
|
2821
|
+
const timer = setTimeout(() => reject(new GateTimeoutError("gate decision timed out")), ms);
|
|
2822
|
+
promise.then(
|
|
2823
|
+
(value) => {
|
|
2824
|
+
clearTimeout(timer);
|
|
2825
|
+
resolve2(value);
|
|
2826
|
+
},
|
|
2827
|
+
(err3) => {
|
|
2828
|
+
clearTimeout(timer);
|
|
2829
|
+
reject(err3);
|
|
2830
|
+
}
|
|
2831
|
+
);
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
async function decideReply(params) {
|
|
2835
|
+
const signals = computeConversationSignals(params.rawMessages, {
|
|
2836
|
+
ownHandle: params.ownHandle,
|
|
2837
|
+
triggerMessageId: params.triggerMessageId,
|
|
2838
|
+
nowMs: params.nowMs
|
|
2839
|
+
});
|
|
2840
|
+
const messages = buildDecisionMessages({
|
|
2841
|
+
handle: params.handle,
|
|
2842
|
+
event: params.event,
|
|
2843
|
+
history: params.history,
|
|
2844
|
+
signals
|
|
2845
|
+
});
|
|
2846
|
+
const systemPrompt = messages.find((m) => m.role === "system")?.content ?? "";
|
|
2847
|
+
const userContent = messages.find((m) => m.role === "user")?.content ?? "";
|
|
2848
|
+
const caller = params.caller ?? createSimpleCompletionGateCaller(params.cfg, params.agentId);
|
|
2849
|
+
const maxTokens = params.maxTokens ?? DEFAULT_GATE_MAX_TOKENS;
|
|
2850
|
+
const timeoutMs = params.timeoutMs ?? DEFAULT_GATE_TIMEOUT_MS;
|
|
2851
|
+
const start = Date.now();
|
|
2852
|
+
let text;
|
|
2853
|
+
try {
|
|
2854
|
+
text = await withTimeout(caller({ systemPrompt, userContent, maxTokens }), timeoutMs);
|
|
2855
|
+
} catch (err3) {
|
|
2856
|
+
if (err3 instanceof GateTimeoutError) {
|
|
2857
|
+
return gateFallback(params.failOpen, "decision_timeout", Date.now() - start);
|
|
2858
|
+
}
|
|
2859
|
+
const detail = err3 instanceof Error ? err3.message : String(err3);
|
|
2860
|
+
return gateFallback(
|
|
2861
|
+
params.failOpen,
|
|
2862
|
+
`decision_call_error: ${detail}`.slice(0, 220),
|
|
2863
|
+
Date.now() - start
|
|
2864
|
+
);
|
|
2865
|
+
}
|
|
2866
|
+
const latencyMs = Date.now() - start;
|
|
2867
|
+
const parsed = parseDecision(text, { source: "llm", latencyMs });
|
|
2868
|
+
return parsed ?? gateFallback(params.failOpen, "unparseable_decision", latencyMs);
|
|
2869
|
+
}
|
|
2870
|
+
function createSimpleCompletionGateCaller(cfg, agentId) {
|
|
2871
|
+
return async ({ systemPrompt, userContent, maxTokens, signal }) => {
|
|
2872
|
+
const prepared = await simpleCompletionRuntime.prepareSimpleCompletionModelForAgent({
|
|
2873
|
+
cfg,
|
|
2874
|
+
// plugin-local OpenClawConfig alias → sdk's internal type
|
|
2875
|
+
agentId,
|
|
2876
|
+
allowMissingApiKeyModes: ["aws-sdk"],
|
|
2877
|
+
// Do NOT skip discovery: it loads provider model catalogs. Skipping it
|
|
2878
|
+
// works for providers with pure dynamic resolution (e.g. Fireworks) but
|
|
2879
|
+
// makes catalog-based providers (Google/Gemini, Anthropic, OpenAI, …)
|
|
2880
|
+
// fail with "Unknown model: <ref>" — the gate must resolve the agent's
|
|
2881
|
+
// own model regardless of which provider it runs on.
|
|
2882
|
+
skipAgentDiscovery: false
|
|
2883
|
+
});
|
|
2884
|
+
if ("error" in prepared) throw new Error(prepared.error);
|
|
2885
|
+
const result = await simpleCompletionRuntime.completeWithPreparedSimpleCompletionModel({
|
|
2886
|
+
model: prepared.model,
|
|
2887
|
+
auth: prepared.auth,
|
|
2888
|
+
cfg,
|
|
2889
|
+
context: {
|
|
2890
|
+
systemPrompt,
|
|
2891
|
+
messages: [{ role: "user", content: userContent }]
|
|
2892
|
+
},
|
|
2893
|
+
// temperature 0 mirrors the Hermes gate: a binary policy decision must
|
|
2894
|
+
// be as deterministic as the provider allows, not sampled creatively.
|
|
2895
|
+
options: {
|
|
2896
|
+
maxTokens,
|
|
2897
|
+
temperature: 0,
|
|
2898
|
+
reasoning: GATE_REASONING_LEVEL,
|
|
2899
|
+
...signal ? { signal } : {}
|
|
2900
|
+
}
|
|
2901
|
+
});
|
|
2902
|
+
return result.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("");
|
|
2903
|
+
};
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
// src/binding/inbound-bridge.ts
|
|
2907
|
+
var GATE_HISTORY_LIMIT = 30;
|
|
2394
2908
|
function createInboundBridge(deps) {
|
|
2395
2909
|
return async function onInbound(event) {
|
|
2396
2910
|
switch (event.kind) {
|
|
@@ -2427,6 +2941,17 @@ async function handleMessage(deps, event) {
|
|
|
2427
2941
|
if (!body && !event.content.attachmentId && !event.content.data) {
|
|
2428
2942
|
return;
|
|
2429
2943
|
}
|
|
2944
|
+
const threadClosures = getThreadClosures(
|
|
2945
|
+
deps.gatewayCfg,
|
|
2946
|
+
deps.accountId
|
|
2947
|
+
);
|
|
2948
|
+
if (threadClosures.isClosed(event.conversationId)) {
|
|
2949
|
+
deps.logger.info(
|
|
2950
|
+
{ conversationId: event.conversationId, messageId: event.messageId, sender: event.sender },
|
|
2951
|
+
"inbound skipped for locally closed thread"
|
|
2952
|
+
);
|
|
2953
|
+
return;
|
|
2954
|
+
}
|
|
2430
2955
|
const channelRuntime = deps.channelRuntime;
|
|
2431
2956
|
if (!channelRuntime || typeof channelRuntime.reply?.dispatchReplyWithBufferedBlockDispatcher !== "function") {
|
|
2432
2957
|
deps.logger.error(
|
|
@@ -2441,6 +2966,7 @@ async function handleMessage(deps, event) {
|
|
|
2441
2966
|
);
|
|
2442
2967
|
return;
|
|
2443
2968
|
}
|
|
2969
|
+
const ts = typeof event.createdAt === "number" ? event.createdAt : Date.parse(event.createdAt);
|
|
2444
2970
|
const recipientHandle = selfHandle ?? "me";
|
|
2445
2971
|
const conversationLabel = event.conversationKind === "group" ? `group ${event.conversationId}` : `dm with @${senderHandle}`;
|
|
2446
2972
|
const sendReply = async (replyText) => {
|
|
@@ -2453,36 +2979,131 @@ async function handleMessage(deps, event) {
|
|
|
2453
2979
|
metadata: { reply_to: event.messageId }
|
|
2454
2980
|
});
|
|
2455
2981
|
};
|
|
2982
|
+
const turnStartMs = Date.now();
|
|
2456
2983
|
const deliver2 = async (payload) => {
|
|
2457
|
-
|
|
2458
|
-
|
|
2984
|
+
if (threadClosures.isClosed(event.conversationId)) {
|
|
2985
|
+
deps.logger.info(
|
|
2986
|
+
{ conversationId: event.conversationId, messageId: event.messageId },
|
|
2987
|
+
"reply suppressed for locally closed thread"
|
|
2988
|
+
);
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
if (hasAgentSendSince(deps.accountId, event.conversationId, turnStartMs)) {
|
|
2992
|
+
deps.logger.info(
|
|
2993
|
+
{ conversationId: event.conversationId, messageId: event.messageId },
|
|
2994
|
+
"final turn text suppressed \u2014 agent already sent its reply via a message tool this turn"
|
|
2995
|
+
);
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
await sendReply(payload.text ?? extractText(payload.blocks));
|
|
2459
2999
|
};
|
|
2460
3000
|
try {
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
3001
|
+
const runtime = channelRuntime;
|
|
3002
|
+
const peer = event.conversationKind === "group" ? { kind: "group", id: event.conversationId } : { kind: "direct", id: senderHandle };
|
|
3003
|
+
const { route, buildEnvelope } = inboundEnvelope.resolveInboundRouteEnvelopeBuilderWithRuntime({
|
|
3004
|
+
cfg: deps.gatewayCfg,
|
|
3005
|
+
channel: "agentchat",
|
|
3006
|
+
accountId: deps.accountId,
|
|
3007
|
+
peer,
|
|
3008
|
+
runtime
|
|
3009
|
+
});
|
|
3010
|
+
let gateContext = null;
|
|
3011
|
+
if (gateEnabled()) {
|
|
3012
|
+
const { decision, context } = await runReplyGate({
|
|
3013
|
+
deps,
|
|
3014
|
+
event,
|
|
3015
|
+
body,
|
|
3016
|
+
agentId: route.agentId,
|
|
3017
|
+
selfHandle,
|
|
3018
|
+
senderHandle,
|
|
3019
|
+
nowMs: Number.isFinite(ts) ? ts : Date.now()
|
|
3020
|
+
});
|
|
3021
|
+
deps.logger.info(
|
|
3022
|
+
{
|
|
3023
|
+
conversationId: event.conversationId,
|
|
3024
|
+
messageId: event.messageId,
|
|
3025
|
+
reply: decision.reply,
|
|
3026
|
+
source: decision.source,
|
|
3027
|
+
category: decision.category,
|
|
3028
|
+
latencyMs: decision.latencyMs,
|
|
3029
|
+
reason: decision.reason
|
|
3030
|
+
},
|
|
3031
|
+
"reply gate decision"
|
|
3032
|
+
);
|
|
3033
|
+
if (!decision.reply) return;
|
|
3034
|
+
gateContext = context;
|
|
3035
|
+
}
|
|
3036
|
+
const contextHeader = [formatSenderLine(event), `Received: ${formatReceivedAt(ts)}`];
|
|
3037
|
+
if (gateContext) {
|
|
3038
|
+
contextHeader.push(...gateContext);
|
|
3039
|
+
} else {
|
|
3040
|
+
contextHeader.push(
|
|
3041
|
+
`Conversation type: ${formatConversationLabel({
|
|
3042
|
+
conversationKind: event.conversationKind,
|
|
3043
|
+
senderHandle,
|
|
3044
|
+
contentText: body,
|
|
3045
|
+
groupName: event.groupName,
|
|
3046
|
+
memberCount: event.memberCount,
|
|
3047
|
+
mentions: event.mentions
|
|
3048
|
+
})}`
|
|
3049
|
+
);
|
|
3050
|
+
const self = (selfHandle ?? "").replace(/^@/, "").toLowerCase();
|
|
3051
|
+
if (event.conversationKind === "group" && self && event.mentions.includes(self)) {
|
|
3052
|
+
contextHeader.push("You were @-mentioned in this message.");
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
const agentBody = `${contextHeader.join("\n")}
|
|
3056
|
+
|
|
3057
|
+
${body}`;
|
|
3058
|
+
const { storePath, body: envelopeBody } = buildEnvelope({
|
|
3059
|
+
channel: "AgentChat",
|
|
3060
|
+
from: conversationLabel,
|
|
3061
|
+
body: agentBody,
|
|
3062
|
+
timestamp: ts
|
|
3063
|
+
});
|
|
3064
|
+
const finalize = channelRuntime.reply.finalizeInboundContext;
|
|
3065
|
+
const ctxPayload = finalize({
|
|
3066
|
+
Body: envelopeBody,
|
|
3067
|
+
BodyForAgent: agentBody,
|
|
3068
|
+
RawBody: body,
|
|
3069
|
+
CommandBody: body,
|
|
3070
|
+
From: `@${senderHandle}`,
|
|
3071
|
+
To: `@${recipientHandle}`,
|
|
3072
|
+
SessionKey: route.sessionKey,
|
|
3073
|
+
AccountId: deps.accountId,
|
|
3074
|
+
ChatType: event.conversationKind === "group" ? "group" : "direct",
|
|
3075
|
+
ConversationLabel: conversationLabel,
|
|
3076
|
+
SenderId: senderHandle,
|
|
3077
|
+
Provider: "agentchat",
|
|
3078
|
+
Surface: "agentchat",
|
|
3079
|
+
MessageSid: event.messageId,
|
|
3080
|
+
MessageSidFull: event.messageId,
|
|
3081
|
+
Timestamp: ts,
|
|
3082
|
+
OriginatingChannel: "agentchat",
|
|
3083
|
+
OriginatingTo: `@${recipientHandle}`
|
|
3084
|
+
});
|
|
3085
|
+
const session = channelRuntime.session;
|
|
3086
|
+
await inboundReplyDispatch.dispatchChannelInboundReply({
|
|
3087
|
+
cfg: deps.gatewayCfg,
|
|
3088
|
+
channel: "agentchat",
|
|
3089
|
+
accountId: deps.accountId,
|
|
3090
|
+
agentId: route.agentId,
|
|
3091
|
+
routeSessionKey: route.sessionKey,
|
|
3092
|
+
storePath,
|
|
3093
|
+
ctxPayload,
|
|
3094
|
+
recordInboundSession: session.recordInboundSession,
|
|
3095
|
+
dispatchReplyWithBufferedBlockDispatcher: channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher,
|
|
3096
|
+
// Under `automatic` (the default) the framework hands the agent's final
|
|
3097
|
+
// turn text to this `deliver`, which sends it to the source. Under the
|
|
3098
|
+
// opt-in `message_tool_only` mode the turn text is suppressed and the
|
|
3099
|
+
// agent sends via the message tool instead, so `deliver` only fires on a
|
|
3100
|
+
// framework fallback. Either way a gated `no_reply` turn never runs, so
|
|
3101
|
+
// nothing is sent.
|
|
3102
|
+
delivery: {
|
|
3103
|
+
deliver: async (payload) => {
|
|
3104
|
+
await deliver2(payload);
|
|
2484
3105
|
},
|
|
2485
|
-
|
|
3106
|
+
onError: (err3, info) => {
|
|
2486
3107
|
deps.logger.error(
|
|
2487
3108
|
{
|
|
2488
3109
|
err: err3 instanceof Error ? err3.message : String(err3),
|
|
@@ -2492,84 +3113,124 @@ async function handleMessage(deps, event) {
|
|
|
2492
3113
|
"inbound dispatch failed"
|
|
2493
3114
|
);
|
|
2494
3115
|
}
|
|
2495
|
-
}
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
const runtime = channelRuntime;
|
|
2499
|
-
const { route, buildEnvelope } = inboundEnvelope.resolveInboundRouteEnvelopeBuilderWithRuntime({
|
|
2500
|
-
cfg: deps.gatewayCfg,
|
|
2501
|
-
channel: "agentchat",
|
|
2502
|
-
accountId: deps.accountId,
|
|
2503
|
-
peer: { kind: "group", id: event.conversationId },
|
|
2504
|
-
runtime
|
|
2505
|
-
});
|
|
2506
|
-
const { storePath, body: envelopeBody } = buildEnvelope({
|
|
2507
|
-
channel: "AgentChat",
|
|
2508
|
-
from: conversationLabel,
|
|
2509
|
-
body,
|
|
2510
|
-
timestamp: ts
|
|
2511
|
-
});
|
|
2512
|
-
const finalize = channelRuntime.reply.finalizeInboundContext;
|
|
2513
|
-
const ctxPayload = finalize({
|
|
2514
|
-
Body: envelopeBody,
|
|
2515
|
-
BodyForAgent: body,
|
|
2516
|
-
RawBody: body,
|
|
2517
|
-
CommandBody: body,
|
|
2518
|
-
From: `@${senderHandle}`,
|
|
2519
|
-
To: `@${recipientHandle}`,
|
|
2520
|
-
SessionKey: route.sessionKey,
|
|
2521
|
-
AccountId: deps.accountId,
|
|
2522
|
-
ChatType: "group",
|
|
2523
|
-
ConversationLabel: conversationLabel,
|
|
2524
|
-
SenderId: senderHandle,
|
|
2525
|
-
Provider: "agentchat",
|
|
2526
|
-
Surface: "agentchat",
|
|
2527
|
-
MessageSid: event.messageId,
|
|
2528
|
-
MessageSidFull: event.messageId,
|
|
2529
|
-
Timestamp: ts,
|
|
2530
|
-
OriginatingChannel: "agentchat",
|
|
2531
|
-
OriginatingTo: `@${recipientHandle}`
|
|
2532
|
-
});
|
|
2533
|
-
const session = channelRuntime.session;
|
|
2534
|
-
await inboundReplyDispatch.recordInboundSessionAndDispatchReply({
|
|
2535
|
-
cfg: deps.gatewayCfg,
|
|
2536
|
-
channel: "agentchat",
|
|
2537
|
-
accountId: deps.accountId,
|
|
2538
|
-
agentId: route.agentId,
|
|
2539
|
-
routeSessionKey: route.sessionKey,
|
|
2540
|
-
storePath,
|
|
2541
|
-
ctxPayload,
|
|
2542
|
-
recordInboundSession: session.recordInboundSession,
|
|
2543
|
-
dispatchReplyWithBufferedBlockDispatcher: channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher,
|
|
2544
|
-
deliver: deliver2,
|
|
3116
|
+
},
|
|
3117
|
+
replyOptions: { sourceReplyDeliveryMode: resolveSourceReplyMode() },
|
|
3118
|
+
record: {
|
|
2545
3119
|
onRecordError: (err3) => {
|
|
2546
3120
|
deps.logger.error(
|
|
2547
3121
|
{ err: err3 instanceof Error ? err3.message : String(err3), messageId: event.messageId },
|
|
2548
|
-
"recordInboundSession failed
|
|
2549
|
-
);
|
|
2550
|
-
},
|
|
2551
|
-
onDispatchError: (err3, info) => {
|
|
2552
|
-
deps.logger.error(
|
|
2553
|
-
{
|
|
2554
|
-
err: err3 instanceof Error ? err3.message : String(err3),
|
|
2555
|
-
messageId: event.messageId,
|
|
2556
|
-
kind: info.kind
|
|
2557
|
-
},
|
|
2558
|
-
"inbound dispatch failed (group)"
|
|
3122
|
+
"recordInboundSession failed"
|
|
2559
3123
|
);
|
|
2560
3124
|
}
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
3125
|
+
}
|
|
3126
|
+
});
|
|
2563
3127
|
} catch (err3) {
|
|
2564
3128
|
deps.logger.error(
|
|
2565
|
-
{
|
|
2566
|
-
err: err3 instanceof Error ? err3.message : String(err3),
|
|
2567
|
-
messageId: event.messageId
|
|
2568
|
-
},
|
|
3129
|
+
{ err: err3 instanceof Error ? err3.message : String(err3), messageId: event.messageId },
|
|
2569
3130
|
"inbound dispatch failed"
|
|
2570
3131
|
);
|
|
2571
3132
|
}
|
|
2572
3133
|
}
|
|
3134
|
+
function formatSenderLine(event) {
|
|
3135
|
+
const who = event.senderDisplayName ? `${event.senderDisplayName} (@${event.sender})` : `@${event.sender}`;
|
|
3136
|
+
return `From: ${event.senderKind === "system" ? `${who}, a system agent` : who}`;
|
|
3137
|
+
}
|
|
3138
|
+
async function runReplyGate(params) {
|
|
3139
|
+
const { deps, event, body, agentId, selfHandle, senderHandle, nowMs } = params;
|
|
3140
|
+
const ownHandle = selfHandle ?? "";
|
|
3141
|
+
let rawMessages = [];
|
|
3142
|
+
try {
|
|
3143
|
+
const client = getClient({ accountId: deps.accountId, config: deps.config });
|
|
3144
|
+
const fetched = await client.getMessages(event.conversationId, { limit: GATE_HISTORY_LIMIT });
|
|
3145
|
+
if (Array.isArray(fetched)) rawMessages = fetched;
|
|
3146
|
+
} catch (err3) {
|
|
3147
|
+
deps.logger.warn(
|
|
3148
|
+
{ err: err3 instanceof Error ? err3.message : String(err3), conversationId: event.conversationId },
|
|
3149
|
+
"reply gate: history fetch failed \u2014 deciding on the new message alone"
|
|
3150
|
+
);
|
|
3151
|
+
}
|
|
3152
|
+
const bareHandle = ownHandle.replace(/^@/, "");
|
|
3153
|
+
const gateEvent = {
|
|
3154
|
+
conversationKind: event.conversationKind,
|
|
3155
|
+
senderHandle,
|
|
3156
|
+
contentText: body,
|
|
3157
|
+
groupName: event.groupName,
|
|
3158
|
+
memberCount: event.memberCount,
|
|
3159
|
+
mentions: event.mentions
|
|
3160
|
+
};
|
|
3161
|
+
const history = translateHistory(rawMessages, ownHandle, event.conversationKind, event.messageId);
|
|
3162
|
+
const decision = await decideReply({
|
|
3163
|
+
cfg: deps.gatewayCfg,
|
|
3164
|
+
agentId,
|
|
3165
|
+
handle: bareHandle,
|
|
3166
|
+
event: gateEvent,
|
|
3167
|
+
history,
|
|
3168
|
+
rawMessages,
|
|
3169
|
+
triggerMessageId: event.messageId,
|
|
3170
|
+
ownHandle,
|
|
3171
|
+
nowMs,
|
|
3172
|
+
failOpen: gateFailOpen(),
|
|
3173
|
+
timeoutMs: gateTimeoutMs(),
|
|
3174
|
+
caller: deps.gateCaller
|
|
3175
|
+
});
|
|
3176
|
+
const signals = computeConversationSignals(rawMessages, {
|
|
3177
|
+
ownHandle,
|
|
3178
|
+
triggerMessageId: event.messageId,
|
|
3179
|
+
nowMs
|
|
3180
|
+
});
|
|
3181
|
+
const context = formatConversationContext({
|
|
3182
|
+
handle: bareHandle,
|
|
3183
|
+
event: gateEvent,
|
|
3184
|
+
signals,
|
|
3185
|
+
priorCount: history.length
|
|
3186
|
+
});
|
|
3187
|
+
return { decision, context };
|
|
3188
|
+
}
|
|
3189
|
+
function translateHistory(messages, ownHandle, conversationKind, triggerMessageId) {
|
|
3190
|
+
const own = ownHandle.replace(/^@/, "").toLowerCase();
|
|
3191
|
+
const isGroup = conversationKind === "group";
|
|
3192
|
+
const sorted = [...messages].filter((m) => Boolean(m) && typeof m === "object").sort((a, b) => readSeq(a) - readSeq(b));
|
|
3193
|
+
const out = [];
|
|
3194
|
+
for (const m of sorted) {
|
|
3195
|
+
if (m.id === triggerMessageId) continue;
|
|
3196
|
+
const type = typeof m.type === "string" ? m.type : "text";
|
|
3197
|
+
if (type !== "text") continue;
|
|
3198
|
+
const content = m.content;
|
|
3199
|
+
const text = content && typeof content === "object" ? content.text : void 0;
|
|
3200
|
+
if (typeof text !== "string" || !text) continue;
|
|
3201
|
+
const senderRaw = String(m.sender ?? m.from ?? m.sender_handle ?? "");
|
|
3202
|
+
const isOwn = typeof m.is_own === "boolean" ? m.is_own : senderRaw.replace(/^@/, "").toLowerCase() === own;
|
|
3203
|
+
if (isOwn) {
|
|
3204
|
+
out.push({ role: "assistant", content: text });
|
|
3205
|
+
} else if (isGroup) {
|
|
3206
|
+
out.push({ role: "user", content: `[@${senderRaw.replace(/^@/, "") || "?"}] ${text}` });
|
|
3207
|
+
} else {
|
|
3208
|
+
out.push({ role: "user", content: text });
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
return out;
|
|
3212
|
+
}
|
|
3213
|
+
function readSeq(m) {
|
|
3214
|
+
const seq = m.seq;
|
|
3215
|
+
return typeof seq === "number" ? seq : 0;
|
|
3216
|
+
}
|
|
3217
|
+
var OFF_TOKENS = /* @__PURE__ */ new Set(["0", "false", "off", "no"]);
|
|
3218
|
+
var ON_TOKENS = /* @__PURE__ */ new Set(["1", "true", "on", "yes"]);
|
|
3219
|
+
function gateEnabled() {
|
|
3220
|
+
return !OFF_TOKENS.has((process.env.AGENTCHAT_REPLY_GATE_ENABLED ?? "").trim().toLowerCase());
|
|
3221
|
+
}
|
|
3222
|
+
function gateFailOpen() {
|
|
3223
|
+
return ON_TOKENS.has((process.env.AGENTCHAT_REPLY_GATE_FAIL_OPEN ?? "").trim().toLowerCase());
|
|
3224
|
+
}
|
|
3225
|
+
function gateTimeoutMs() {
|
|
3226
|
+
const raw = process.env.AGENTCHAT_REPLY_GATE_TIMEOUT_MS;
|
|
3227
|
+
if (raw === void 0 || raw.trim() === "") return void 0;
|
|
3228
|
+
const n = Number(raw);
|
|
3229
|
+
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
3230
|
+
}
|
|
3231
|
+
function resolveSourceReplyMode() {
|
|
3232
|
+
return (process.env.AGENTCHAT_SOURCE_REPLY_MODE ?? "").trim().toLowerCase() === "message_tool_only" ? "message_tool_only" : "automatic";
|
|
3233
|
+
}
|
|
2573
3234
|
function handleGroupInvite(deps, event) {
|
|
2574
3235
|
deps.logger.info(
|
|
2575
3236
|
{
|
|
@@ -2717,27 +3378,6 @@ var agentchatGatewayAdapter = {
|
|
|
2717
3378
|
});
|
|
2718
3379
|
}
|
|
2719
3380
|
};
|
|
2720
|
-
var cache = /* @__PURE__ */ new Map();
|
|
2721
|
-
function getClient({ accountId, config, options }) {
|
|
2722
|
-
const existing = cache.get(accountId);
|
|
2723
|
-
if (existing && existing.apiKey === config.apiKey && existing.apiBase === config.apiBase) {
|
|
2724
|
-
return existing.client;
|
|
2725
|
-
}
|
|
2726
|
-
const client = new agentchatme.AgentChatClient({
|
|
2727
|
-
apiKey: config.apiKey,
|
|
2728
|
-
baseUrl: config.apiBase,
|
|
2729
|
-
...options
|
|
2730
|
-
});
|
|
2731
|
-
cache.set(accountId, {
|
|
2732
|
-
client,
|
|
2733
|
-
apiKey: config.apiKey,
|
|
2734
|
-
apiBase: config.apiBase
|
|
2735
|
-
});
|
|
2736
|
-
return client;
|
|
2737
|
-
}
|
|
2738
|
-
function disposeClient(accountId) {
|
|
2739
|
-
cache.delete(accountId);
|
|
2740
|
-
}
|
|
2741
3381
|
|
|
2742
3382
|
// src/binding/outbound.ts
|
|
2743
3383
|
function resolveConfig(cfg, accountId) {
|
|
@@ -2799,6 +3439,7 @@ async function deliver(ctx, attachmentId) {
|
|
|
2799
3439
|
attachmentId
|
|
2800
3440
|
);
|
|
2801
3441
|
const result = await runtime.sendMessage(input);
|
|
3442
|
+
recordAgentSend(accountId, result.message.conversation_id);
|
|
2802
3443
|
return {
|
|
2803
3444
|
channel: AGENTCHAT_CHANNEL_ID,
|
|
2804
3445
|
messageId: result.message.id,
|
|
@@ -2868,8 +3509,8 @@ async function uploadMediaFromUrl(ctx, mediaUrl) {
|
|
|
2868
3509
|
let contentType;
|
|
2869
3510
|
let filename = "attachment";
|
|
2870
3511
|
if (mediaUrl.startsWith("file://") && ctx.mediaReadFile) {
|
|
2871
|
-
const
|
|
2872
|
-
const buf = await ctx.mediaReadFile(
|
|
3512
|
+
const path3 = decodeURIComponent(mediaUrl.replace(/^file:\/\//, ""));
|
|
3513
|
+
const buf = await ctx.mediaReadFile(path3);
|
|
2873
3514
|
if (buf.byteLength > MAX_MEDIA_BYTES) {
|
|
2874
3515
|
throw new AgentChatChannelError(
|
|
2875
3516
|
"terminal-user",
|
|
@@ -2879,7 +3520,7 @@ async function uploadMediaFromUrl(ctx, mediaUrl) {
|
|
|
2879
3520
|
const copy = new Uint8Array(buf.byteLength);
|
|
2880
3521
|
copy.set(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
|
|
2881
3522
|
bytes = copy.buffer;
|
|
2882
|
-
filename =
|
|
3523
|
+
filename = path3.split(/[\\/]/).pop() ?? filename;
|
|
2883
3524
|
} else {
|
|
2884
3525
|
assertMediaUrlSafe(mediaUrl);
|
|
2885
3526
|
const controller = new AbortController();
|
|
@@ -3034,8 +3675,8 @@ function err(message) {
|
|
|
3034
3675
|
details: { error: message }
|
|
3035
3676
|
};
|
|
3036
3677
|
}
|
|
3037
|
-
function str(params,
|
|
3038
|
-
const v = params[
|
|
3678
|
+
function str(params, key2) {
|
|
3679
|
+
const v = params[key2];
|
|
3039
3680
|
return typeof v === "string" ? v : void 0;
|
|
3040
3681
|
}
|
|
3041
3682
|
function resolveConfig2(ctx) {
|
|
@@ -3292,6 +3933,7 @@ var agentchatAgentToolsFactory = ({ cfg }) => {
|
|
|
3292
3933
|
content: { text: p.message },
|
|
3293
3934
|
...p.replyToMessageId ? { metadata: { reply_to: p.replyToMessageId } } : {}
|
|
3294
3935
|
});
|
|
3936
|
+
recordAgentSend(r.accountId, result.message.conversation_id);
|
|
3295
3937
|
const summaryParts = [
|
|
3296
3938
|
`sent to @${handle} \u2014 message ${result.message.id} in ${result.message.conversation_id}`
|
|
3297
3939
|
];
|
|
@@ -3814,6 +4456,88 @@ ${lines.join("\n")}`);
|
|
|
3814
4456
|
}
|
|
3815
4457
|
}
|
|
3816
4458
|
}),
|
|
4459
|
+
tool({
|
|
4460
|
+
name: "agentchat_close_local_thread",
|
|
4461
|
+
description: "Locally close a conversation thread for this OpenClaw AgentChat account. Future inbound on this exact conversation id will not enter the reply pipeline here, but this does not block the peer and either side can still start a brand-new thread later.",
|
|
4462
|
+
parameters: typebox.Type.Object({
|
|
4463
|
+
conversationId: typebox.Type.String(),
|
|
4464
|
+
reason: typebox.Type.Optional(typebox.Type.String({ maxLength: 200 })),
|
|
4465
|
+
account: ACCOUNT_PARAM
|
|
4466
|
+
}),
|
|
4467
|
+
execute: async (_id, p) => {
|
|
4468
|
+
const r = clientFor(cfg, p.account);
|
|
4469
|
+
if ("error" in r) return err2(r.error);
|
|
4470
|
+
const closures = getThreadClosures(cfg, r.accountId);
|
|
4471
|
+
const record = closures.close(p.conversationId, p.reason);
|
|
4472
|
+
return {
|
|
4473
|
+
content: [
|
|
4474
|
+
{
|
|
4475
|
+
type: "text",
|
|
4476
|
+
text: `closed ${record.conversationId} locally`
|
|
4477
|
+
}
|
|
4478
|
+
],
|
|
4479
|
+
details: {
|
|
4480
|
+
conversationId: record.conversationId,
|
|
4481
|
+
closedAt: record.closedAt,
|
|
4482
|
+
reason: record.reason,
|
|
4483
|
+
localOnly: true
|
|
4484
|
+
}
|
|
4485
|
+
};
|
|
4486
|
+
}
|
|
4487
|
+
}),
|
|
4488
|
+
tool({
|
|
4489
|
+
name: "agentchat_reopen_local_thread",
|
|
4490
|
+
description: "Re-open a previously locally closed conversation thread so new inbound on that conversation id can reach the reply pipeline again.",
|
|
4491
|
+
parameters: typebox.Type.Object({
|
|
4492
|
+
conversationId: typebox.Type.String(),
|
|
4493
|
+
account: ACCOUNT_PARAM
|
|
4494
|
+
}),
|
|
4495
|
+
execute: async (_id, p) => {
|
|
4496
|
+
const r = clientFor(cfg, p.account);
|
|
4497
|
+
if ("error" in r) return err2(r.error);
|
|
4498
|
+
const closures = getThreadClosures(cfg, r.accountId);
|
|
4499
|
+
const reopened = closures.reopen(p.conversationId);
|
|
4500
|
+
return {
|
|
4501
|
+
content: [
|
|
4502
|
+
{
|
|
4503
|
+
type: "text",
|
|
4504
|
+
text: reopened ? `re-opened ${p.conversationId} locally` : `${p.conversationId} was not locally closed`
|
|
4505
|
+
}
|
|
4506
|
+
],
|
|
4507
|
+
details: {
|
|
4508
|
+
conversationId: p.conversationId,
|
|
4509
|
+
reopened,
|
|
4510
|
+
localOnly: true
|
|
4511
|
+
}
|
|
4512
|
+
};
|
|
4513
|
+
}
|
|
4514
|
+
}),
|
|
4515
|
+
tool({
|
|
4516
|
+
name: "agentchat_list_local_closed_threads",
|
|
4517
|
+
description: "List conversation threads currently closed locally for this OpenClaw AgentChat account. These are client-side only, not server-side blocks or mutes.",
|
|
4518
|
+
parameters: typebox.Type.Object({
|
|
4519
|
+
account: ACCOUNT_PARAM
|
|
4520
|
+
}),
|
|
4521
|
+
execute: async (_id, p) => {
|
|
4522
|
+
const r = clientFor(cfg, p.account);
|
|
4523
|
+
if ("error" in r) return err2(r.error);
|
|
4524
|
+
const closures = getThreadClosures(cfg, r.accountId);
|
|
4525
|
+
const closed = closures.list();
|
|
4526
|
+
if (closed.length === 0) {
|
|
4527
|
+
return {
|
|
4528
|
+
content: [{ type: "text", text: "no locally closed threads" }],
|
|
4529
|
+
details: { closedThreads: [], localOnly: true }
|
|
4530
|
+
};
|
|
4531
|
+
}
|
|
4532
|
+
const lines = closed.map(
|
|
4533
|
+
(record) => `${record.conversationId} \u2014 ${record.closedAt}${record.reason ? ` \u2014 ${record.reason}` : ""}`
|
|
4534
|
+
);
|
|
4535
|
+
return {
|
|
4536
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
4537
|
+
details: { closedThreads: closed, localOnly: true }
|
|
4538
|
+
};
|
|
4539
|
+
}
|
|
4540
|
+
}),
|
|
3817
4541
|
tool({
|
|
3818
4542
|
name: "agentchat_get_conversation_history",
|
|
3819
4543
|
description: "Fetch recent messages from a specific conversation. Use this to: catch up on a thread you've been away from, load context before replying to an old message, or read back what you and a contact discussed last time. Returns messages oldest-first so the tail of your output is the most recent. Pass `beforeSeq` to paginate further back.",
|
|
@@ -4112,6 +4836,7 @@ ${lines.join("\n")}`);
|
|
|
4112
4836
|
to: "chatfather",
|
|
4113
4837
|
content: { text: p.message }
|
|
4114
4838
|
});
|
|
4839
|
+
recordAgentSend(r.accountId, result.message.conversation_id);
|
|
4115
4840
|
return ok2(
|
|
4116
4841
|
`message sent to @chatfather (id: ${result.message.id}). Watch your inbox for the reply.`
|
|
4117
4842
|
);
|
|
@@ -4580,8 +5305,8 @@ var agentchatSetupEntry = channelCore.defineSetupPluginEntry(agentchatPlugin);
|
|
|
4580
5305
|
// src/configured-state.ts
|
|
4581
5306
|
function hasAgentChatConfiguredState(config) {
|
|
4582
5307
|
if (!config || typeof config !== "object") return false;
|
|
4583
|
-
const
|
|
4584
|
-
if (typeof
|
|
5308
|
+
const key2 = config.apiKey;
|
|
5309
|
+
if (typeof key2 !== "string" || key2.length < 20) return false;
|
|
4585
5310
|
const handle = config.agentHandle;
|
|
4586
5311
|
if (typeof handle !== "string" || handle.trim().length === 0) return false;
|
|
4587
5312
|
return true;
|