botinabox 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -6
- package/dist/index.d.ts +771 -1
- package/dist/index.js +1529 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -774,6 +774,147 @@ var MessagePipeline = class {
|
|
|
774
774
|
}
|
|
775
775
|
};
|
|
776
776
|
|
|
777
|
+
// src/core/chat/triage-router.ts
|
|
778
|
+
var TriageRouter = class {
|
|
779
|
+
constructor(db, hooks, config) {
|
|
780
|
+
this.db = db;
|
|
781
|
+
this.hooks = hooks;
|
|
782
|
+
this.rules = config.rules;
|
|
783
|
+
this.fallbackAgent = config.fallbackAgent;
|
|
784
|
+
this.llmFallback = config.llmFallback ?? true;
|
|
785
|
+
this.persist = config.persist ?? true;
|
|
786
|
+
this.compiledRules = this.rules.sort((a, b) => (a.priority ?? 50) - (b.priority ?? 50)).map((rule) => ({
|
|
787
|
+
rule,
|
|
788
|
+
regexes: (rule.patterns ?? []).map((p) => new RegExp(p, "i")),
|
|
789
|
+
keywordSet: new Set((rule.keywords ?? []).map((k) => k.toLowerCase()))
|
|
790
|
+
}));
|
|
791
|
+
}
|
|
792
|
+
db;
|
|
793
|
+
hooks;
|
|
794
|
+
rules;
|
|
795
|
+
fallbackAgent;
|
|
796
|
+
llmFallback;
|
|
797
|
+
persist;
|
|
798
|
+
compiledRules;
|
|
799
|
+
/**
|
|
800
|
+
* Route an inbound message to the best agent.
|
|
801
|
+
* Returns the agent slug and the routing decision.
|
|
802
|
+
*/
|
|
803
|
+
async route(msg) {
|
|
804
|
+
const body = msg.body.toLowerCase();
|
|
805
|
+
const words = new Set(body.split(/\s+/));
|
|
806
|
+
for (const { rule, regexes, keywordSet } of this.compiledRules) {
|
|
807
|
+
for (const keyword of keywordSet) {
|
|
808
|
+
if (words.has(keyword) || body.includes(keyword)) {
|
|
809
|
+
const decision2 = this.buildDecision(
|
|
810
|
+
rule.agentSlug,
|
|
811
|
+
`keyword: '${keyword}'`,
|
|
812
|
+
"deterministic",
|
|
813
|
+
msg
|
|
814
|
+
);
|
|
815
|
+
await this.logDecision(decision2);
|
|
816
|
+
return { agentSlug: rule.agentSlug, decision: decision2 };
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
for (const regex of regexes) {
|
|
820
|
+
if (regex.test(msg.body)) {
|
|
821
|
+
const decision2 = this.buildDecision(
|
|
822
|
+
rule.agentSlug,
|
|
823
|
+
`pattern: ${regex.source}`,
|
|
824
|
+
"deterministic",
|
|
825
|
+
msg
|
|
826
|
+
);
|
|
827
|
+
await this.logDecision(decision2);
|
|
828
|
+
return { agentSlug: rule.agentSlug, decision: decision2 };
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (this.llmFallback) {
|
|
833
|
+
const agentSlugs = this.rules.map((r) => r.agentSlug);
|
|
834
|
+
const classified = await this.classifyWithLLM(msg, agentSlugs);
|
|
835
|
+
if (classified) {
|
|
836
|
+
const decision2 = this.buildDecision(
|
|
837
|
+
classified.agentSlug,
|
|
838
|
+
`llm: ${classified.reason}`,
|
|
839
|
+
"llm",
|
|
840
|
+
msg
|
|
841
|
+
);
|
|
842
|
+
await this.logDecision(decision2);
|
|
843
|
+
return { agentSlug: classified.agentSlug, decision: decision2 };
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
const decision = this.buildDecision(
|
|
847
|
+
this.fallbackAgent,
|
|
848
|
+
"fallback: no rule matched",
|
|
849
|
+
"deterministic",
|
|
850
|
+
msg
|
|
851
|
+
);
|
|
852
|
+
await this.logDecision(decision);
|
|
853
|
+
return { agentSlug: this.fallbackAgent, decision };
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Query the ownership chain for a given message or channel.
|
|
857
|
+
*/
|
|
858
|
+
async getDecisionHistory(filter) {
|
|
859
|
+
const rows = await this.db.query("activity_log", {
|
|
860
|
+
where: { event_type: "triage_decision" }
|
|
861
|
+
});
|
|
862
|
+
let decisions = rows.map((r) => {
|
|
863
|
+
try {
|
|
864
|
+
return JSON.parse(r["payload"]);
|
|
865
|
+
} catch {
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
}).filter((d) => d !== null);
|
|
869
|
+
if (filter?.channel) {
|
|
870
|
+
decisions = decisions.filter((d) => d.channel === filter.channel);
|
|
871
|
+
}
|
|
872
|
+
decisions.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
873
|
+
if (filter?.limit) {
|
|
874
|
+
decisions = decisions.slice(0, filter.limit);
|
|
875
|
+
}
|
|
876
|
+
return decisions;
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* LLM classification — emits a hook for external LLM integration.
|
|
880
|
+
* Returns agent slug + reason, or undefined if LLM is unavailable.
|
|
881
|
+
*/
|
|
882
|
+
async classifyWithLLM(msg, agentSlugs) {
|
|
883
|
+
const result = {};
|
|
884
|
+
await this.hooks.emit("triage.classify", {
|
|
885
|
+
message: msg,
|
|
886
|
+
candidates: agentSlugs,
|
|
887
|
+
respond: (slug, reason) => {
|
|
888
|
+
result.agentSlug = slug;
|
|
889
|
+
result.reason = reason;
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
if (result.agentSlug && result.reason) {
|
|
893
|
+
return { agentSlug: result.agentSlug, reason: result.reason };
|
|
894
|
+
}
|
|
895
|
+
return void 0;
|
|
896
|
+
}
|
|
897
|
+
buildDecision(target, reason, method, msg) {
|
|
898
|
+
return {
|
|
899
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
900
|
+
source: "triage",
|
|
901
|
+
target: target ?? "none",
|
|
902
|
+
reason,
|
|
903
|
+
method,
|
|
904
|
+
messageId: msg.id,
|
|
905
|
+
channel: msg.channel
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
async logDecision(decision) {
|
|
909
|
+
if (!this.persist) return;
|
|
910
|
+
await this.db.insert("activity_log", {
|
|
911
|
+
event_type: "triage_decision",
|
|
912
|
+
payload: JSON.stringify(decision)
|
|
913
|
+
});
|
|
914
|
+
await this.hooks.emit("triage.routed", { decision });
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
|
|
777
918
|
// src/core/chat/session-key.ts
|
|
778
919
|
var SessionKey = class _SessionKey {
|
|
779
920
|
constructor(agentId, channel, scope) {
|
|
@@ -1018,6 +1159,441 @@ var NotificationQueue = class {
|
|
|
1018
1159
|
}
|
|
1019
1160
|
};
|
|
1020
1161
|
|
|
1162
|
+
// src/core/chat/message-store.ts
|
|
1163
|
+
var MessageStore = class {
|
|
1164
|
+
constructor(db, hooks) {
|
|
1165
|
+
this.db = db;
|
|
1166
|
+
this.hooks = hooks;
|
|
1167
|
+
}
|
|
1168
|
+
db;
|
|
1169
|
+
hooks;
|
|
1170
|
+
/**
|
|
1171
|
+
* Store an inbound message and its attachments.
|
|
1172
|
+
* Must complete successfully before any bot response is generated.
|
|
1173
|
+
*/
|
|
1174
|
+
async storeInbound(msg) {
|
|
1175
|
+
const row = await this.db.insert("messages", {
|
|
1176
|
+
channel: msg.channel,
|
|
1177
|
+
direction: "inbound",
|
|
1178
|
+
from_user: msg.from,
|
|
1179
|
+
user_id: msg.userId,
|
|
1180
|
+
body: msg.body,
|
|
1181
|
+
thread_id: msg.threadId
|
|
1182
|
+
});
|
|
1183
|
+
const messageId = row["id"];
|
|
1184
|
+
const attachmentIds = [];
|
|
1185
|
+
if (msg.attachments && msg.attachments.length > 0) {
|
|
1186
|
+
for (const att of msg.attachments) {
|
|
1187
|
+
const attId = await this.storeAttachment(messageId, {
|
|
1188
|
+
fileType: att.type,
|
|
1189
|
+
filename: att.filename,
|
|
1190
|
+
mimeType: att.mimeType,
|
|
1191
|
+
sizeBytes: att.size,
|
|
1192
|
+
url: att.url
|
|
1193
|
+
});
|
|
1194
|
+
attachmentIds.push(attId);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
await this.hooks.emit("message.stored", {
|
|
1198
|
+
messageId,
|
|
1199
|
+
direction: "inbound",
|
|
1200
|
+
channel: msg.channel,
|
|
1201
|
+
from: msg.from,
|
|
1202
|
+
attachmentCount: attachmentIds.length
|
|
1203
|
+
});
|
|
1204
|
+
return { messageId, attachmentIds };
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Store an outbound message BEFORE sending it.
|
|
1208
|
+
* Returns the message ID for confirmation tracking.
|
|
1209
|
+
*/
|
|
1210
|
+
async storeOutbound(opts) {
|
|
1211
|
+
const row = await this.db.insert("messages", {
|
|
1212
|
+
channel: opts.channel,
|
|
1213
|
+
direction: "outbound",
|
|
1214
|
+
from_agent: opts.agentSlug,
|
|
1215
|
+
agent_id: opts.agentId,
|
|
1216
|
+
body: opts.text,
|
|
1217
|
+
thread_id: opts.threadId,
|
|
1218
|
+
task_id: opts.taskId
|
|
1219
|
+
});
|
|
1220
|
+
const messageId = row["id"];
|
|
1221
|
+
await this.hooks.emit("message.stored", {
|
|
1222
|
+
messageId,
|
|
1223
|
+
direction: "outbound",
|
|
1224
|
+
channel: opts.channel
|
|
1225
|
+
});
|
|
1226
|
+
return messageId;
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Store an attachment linked to a message.
|
|
1230
|
+
*/
|
|
1231
|
+
async storeAttachment(messageId, att) {
|
|
1232
|
+
const row = await this.db.insert("message_attachments", {
|
|
1233
|
+
message_id: messageId,
|
|
1234
|
+
file_type: att.fileType,
|
|
1235
|
+
filename: att.filename,
|
|
1236
|
+
mime_type: att.mimeType,
|
|
1237
|
+
size_bytes: att.sizeBytes,
|
|
1238
|
+
contents: att.contents,
|
|
1239
|
+
summary: att.summary,
|
|
1240
|
+
url: att.url
|
|
1241
|
+
});
|
|
1242
|
+
return row["id"];
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Get recent messages in a thread for context building.
|
|
1246
|
+
*/
|
|
1247
|
+
async getThreadHistory(threadId, limit = 20) {
|
|
1248
|
+
const messages = await this.db.query("messages", {
|
|
1249
|
+
where: { thread_id: threadId },
|
|
1250
|
+
orderBy: "created_at",
|
|
1251
|
+
limit
|
|
1252
|
+
});
|
|
1253
|
+
return messages;
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Get recent outbound messages in a thread for redundancy checking.
|
|
1257
|
+
*/
|
|
1258
|
+
async getRecentOutbound(threadId, limit = 10) {
|
|
1259
|
+
const messages = await this.db.query("messages", {
|
|
1260
|
+
where: { thread_id: threadId, direction: "outbound" },
|
|
1261
|
+
orderBy: "created_at",
|
|
1262
|
+
limit
|
|
1263
|
+
});
|
|
1264
|
+
return messages;
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Get attachments for a message.
|
|
1268
|
+
*/
|
|
1269
|
+
async getAttachments(messageId) {
|
|
1270
|
+
return this.db.query("message_attachments", {
|
|
1271
|
+
where: { message_id: messageId }
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1276
|
+
// src/core/chat/chat-responder.ts
|
|
1277
|
+
var DEFAULT_SYSTEM_PROMPT = `You are a helpful, enthusiastic AI digital assistant. Your job is to:
|
|
1278
|
+
- Acknowledge the user's message and relay what you understand
|
|
1279
|
+
- Let them know what will happen next (if a task is being worked on)
|
|
1280
|
+
- Answer conversational questions directly
|
|
1281
|
+
- Be honest when you need to look something up \u2014 say "let me find out"
|
|
1282
|
+
- Keep responses concise and friendly
|
|
1283
|
+
|
|
1284
|
+
You are aware of tools and capabilities in the system but you do NOT execute anything.
|
|
1285
|
+
You cannot run code, search databases, or take actions. You are purely conversational.
|
|
1286
|
+
If the user asks you to DO something, acknowledge it and say it will be handled.`;
|
|
1287
|
+
var DEFAULT_CONTEXT_TOKENS = 4e3;
|
|
1288
|
+
var DEFAULT_REDUNDANCY_WINDOW = 10;
|
|
1289
|
+
var APPROX_CHARS_PER_TOKEN = 4;
|
|
1290
|
+
var ChatResponder = class {
|
|
1291
|
+
constructor(db, hooks, messageStore, config) {
|
|
1292
|
+
this.db = db;
|
|
1293
|
+
this.hooks = hooks;
|
|
1294
|
+
this.messageStore = messageStore;
|
|
1295
|
+
this.systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
1296
|
+
this.contextWindowTokens = config.contextWindowTokens ?? DEFAULT_CONTEXT_TOKENS;
|
|
1297
|
+
this.redundancyWindow = config.redundancyWindow ?? DEFAULT_REDUNDANCY_WINDOW;
|
|
1298
|
+
this.model = config.model ?? "fast";
|
|
1299
|
+
this.llmCall = config.llmCall;
|
|
1300
|
+
}
|
|
1301
|
+
db;
|
|
1302
|
+
hooks;
|
|
1303
|
+
messageStore;
|
|
1304
|
+
systemPrompt;
|
|
1305
|
+
contextWindowTokens;
|
|
1306
|
+
redundancyWindow;
|
|
1307
|
+
model;
|
|
1308
|
+
llmCall;
|
|
1309
|
+
/**
|
|
1310
|
+
* Generate a fast conversational response to an inbound message.
|
|
1311
|
+
* Uses rolling context window from thread history.
|
|
1312
|
+
*/
|
|
1313
|
+
async respond(opts) {
|
|
1314
|
+
const history = await this.messageStore.getThreadHistory(
|
|
1315
|
+
opts.threadId,
|
|
1316
|
+
50
|
|
1317
|
+
// get more, trim by tokens
|
|
1318
|
+
);
|
|
1319
|
+
const messages = this.buildContextWindow(history, opts.messageBody);
|
|
1320
|
+
let system = this.systemPrompt;
|
|
1321
|
+
if (opts.capabilities) {
|
|
1322
|
+
system += `
|
|
1323
|
+
|
|
1324
|
+
System capabilities you are aware of:
|
|
1325
|
+
${opts.capabilities}`;
|
|
1326
|
+
}
|
|
1327
|
+
if (opts.userName) {
|
|
1328
|
+
system += `
|
|
1329
|
+
|
|
1330
|
+
You are talking to: ${opts.userName}`;
|
|
1331
|
+
}
|
|
1332
|
+
const result = await this.llmCall({
|
|
1333
|
+
model: this.model,
|
|
1334
|
+
messages,
|
|
1335
|
+
system,
|
|
1336
|
+
maxTokens: 500
|
|
1337
|
+
});
|
|
1338
|
+
return result.content;
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Filter any outbound message through the LLM for human readability.
|
|
1342
|
+
* This is the single funnel ALL responses pass through.
|
|
1343
|
+
*/
|
|
1344
|
+
async filterResponse(text, context) {
|
|
1345
|
+
if (text.length < 100) return text;
|
|
1346
|
+
const result = await this.llmCall({
|
|
1347
|
+
model: this.model,
|
|
1348
|
+
messages: [
|
|
1349
|
+
{
|
|
1350
|
+
role: "user",
|
|
1351
|
+
content: `Rewrite this agent/system message to be human-friendly and conversational. Keep the substance, remove jargon, make it feel like a helpful assistant talking to a person. If it's already readable, return it as-is. Do not add preamble like "Here's the rewritten version". Just output the rewritten text.
|
|
1352
|
+
|
|
1353
|
+
---
|
|
1354
|
+
${text}`
|
|
1355
|
+
}
|
|
1356
|
+
],
|
|
1357
|
+
maxTokens: 1e3
|
|
1358
|
+
});
|
|
1359
|
+
return result.content;
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Check if a candidate outbound message is redundant with recent messages.
|
|
1363
|
+
* Returns true if the message should be suppressed.
|
|
1364
|
+
*/
|
|
1365
|
+
async isRedundant(text, threadId) {
|
|
1366
|
+
const recent = await this.messageStore.getRecentOutbound(threadId, this.redundancyWindow);
|
|
1367
|
+
if (recent.length === 0) return false;
|
|
1368
|
+
const recentTexts = recent.map((m) => m["body"] ?? "").filter((t) => t.length > 0).slice(-5).join("\n---\n");
|
|
1369
|
+
const result = await this.llmCall({
|
|
1370
|
+
model: this.model,
|
|
1371
|
+
messages: [
|
|
1372
|
+
{
|
|
1373
|
+
role: "user",
|
|
1374
|
+
content: `Does this NEW message duplicate or substantially overlap the RECENT messages already sent? Answer with just "redundant" or "not redundant". If the new message has meaningful new information or updates, it is NOT redundant.
|
|
1375
|
+
|
|
1376
|
+
RECENT MESSAGES:
|
|
1377
|
+
${recentTexts}
|
|
1378
|
+
|
|
1379
|
+
NEW MESSAGE:
|
|
1380
|
+
${text}`
|
|
1381
|
+
}
|
|
1382
|
+
],
|
|
1383
|
+
maxTokens: 10
|
|
1384
|
+
});
|
|
1385
|
+
return result.content.toLowerCase().includes("redundant") && !result.content.toLowerCase().includes("not redundant");
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Full send pipeline: check redundancy → filter → store → deliver.
|
|
1389
|
+
* Returns the message ID, or undefined if suppressed as redundant.
|
|
1390
|
+
*/
|
|
1391
|
+
async sendResponse(opts) {
|
|
1392
|
+
if (!opts.skipRedundancyCheck) {
|
|
1393
|
+
const redundant = await this.isRedundant(opts.text, opts.threadId);
|
|
1394
|
+
if (redundant) {
|
|
1395
|
+
await this.hooks.emit("response.suppressed", {
|
|
1396
|
+
channel: opts.channel,
|
|
1397
|
+
threadId: opts.threadId,
|
|
1398
|
+
reason: "redundant"
|
|
1399
|
+
});
|
|
1400
|
+
return void 0;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
const filtered = opts.skipFilter ? opts.text : await this.filterResponse(opts.text, {
|
|
1404
|
+
channel: opts.channel,
|
|
1405
|
+
threadId: opts.threadId,
|
|
1406
|
+
source: opts.source
|
|
1407
|
+
});
|
|
1408
|
+
const messageId = await this.messageStore.storeOutbound({
|
|
1409
|
+
channel: opts.channel,
|
|
1410
|
+
text: filtered,
|
|
1411
|
+
threadId: opts.threadId,
|
|
1412
|
+
agentId: opts.agentId,
|
|
1413
|
+
agentSlug: opts.agentSlug,
|
|
1414
|
+
taskId: opts.taskId
|
|
1415
|
+
});
|
|
1416
|
+
await this.hooks.emit("response.ready", {
|
|
1417
|
+
messageId,
|
|
1418
|
+
channel: opts.channel,
|
|
1419
|
+
threadId: opts.threadId,
|
|
1420
|
+
text: filtered,
|
|
1421
|
+
taskId: opts.taskId
|
|
1422
|
+
});
|
|
1423
|
+
return messageId;
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Build a context window from thread history, trimmed to token limit.
|
|
1427
|
+
*/
|
|
1428
|
+
buildContextWindow(history, currentMessage) {
|
|
1429
|
+
const maxChars = this.contextWindowTokens * APPROX_CHARS_PER_TOKEN;
|
|
1430
|
+
let charCount = currentMessage.length;
|
|
1431
|
+
const messages = [];
|
|
1432
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
1433
|
+
const msg = history[i];
|
|
1434
|
+
const body = msg["body"] ?? "";
|
|
1435
|
+
const direction = msg["direction"];
|
|
1436
|
+
if (charCount + body.length > maxChars) break;
|
|
1437
|
+
messages.unshift({
|
|
1438
|
+
role: direction === "inbound" ? "user" : "assistant",
|
|
1439
|
+
content: body
|
|
1440
|
+
});
|
|
1441
|
+
charCount += body.length;
|
|
1442
|
+
}
|
|
1443
|
+
messages.push({ role: "user", content: currentMessage });
|
|
1444
|
+
return messages;
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
|
|
1448
|
+
// src/core/chat/message-interpreter.ts
|
|
1449
|
+
var INTERPRET_SYSTEM = `You are a message parser. Extract structured data from the user's message.
|
|
1450
|
+
|
|
1451
|
+
Return a JSON object with these fields:
|
|
1452
|
+
{
|
|
1453
|
+
"tasks": [{ "title": "...", "description": "...", "priority": 1-10 }],
|
|
1454
|
+
"memories": [{ "summary": "one-line", "contents": "full text", "tags": ["tag1"], "category": "..." }],
|
|
1455
|
+
"user_context": [{ "trait": "...", "value": "..." }],
|
|
1456
|
+
"is_task_request": true/false
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
Rules:
|
|
1460
|
+
- "tasks": actionable requests the user wants done. NOT conversational messages.
|
|
1461
|
+
- "memories": information, notes, random thoughts to remember. Parse thematically \u2014 one message can have multiple memories.
|
|
1462
|
+
- "user_context": personality traits, preferences, or learnings about the user.
|
|
1463
|
+
- "is_task_request": true if the message contains at least one actionable task.
|
|
1464
|
+
- If the message is just a greeting or conversation, return empty arrays and is_task_request: false.
|
|
1465
|
+
- Return ONLY valid JSON, no markdown or explanation.`;
|
|
1466
|
+
var MessageInterpreter = class {
|
|
1467
|
+
constructor(db, hooks, config) {
|
|
1468
|
+
this.db = db;
|
|
1469
|
+
this.hooks = hooks;
|
|
1470
|
+
this.extractors = config.extractors ?? [];
|
|
1471
|
+
this.model = config.model ?? "fast";
|
|
1472
|
+
this.llmCall = config.llmCall;
|
|
1473
|
+
this.autoCreateTasks = config.autoCreateTasks ?? false;
|
|
1474
|
+
}
|
|
1475
|
+
db;
|
|
1476
|
+
hooks;
|
|
1477
|
+
extractors;
|
|
1478
|
+
model;
|
|
1479
|
+
llmCall;
|
|
1480
|
+
autoCreateTasks;
|
|
1481
|
+
/**
|
|
1482
|
+
* Interpret a stored message asynchronously.
|
|
1483
|
+
* Extracts tasks, memories, files, user context, and custom types.
|
|
1484
|
+
*/
|
|
1485
|
+
async interpret(messageId) {
|
|
1486
|
+
const message = await this.db.get("messages", { id: messageId });
|
|
1487
|
+
if (!message) throw new Error(`Message not found: ${messageId}`);
|
|
1488
|
+
const body = message["body"];
|
|
1489
|
+
const userId = message["user_id"];
|
|
1490
|
+
const attachments = await this.db.query("message_attachments", {
|
|
1491
|
+
where: { message_id: messageId }
|
|
1492
|
+
});
|
|
1493
|
+
const parsed = await this.extractWithLLM(body);
|
|
1494
|
+
for (const memory of parsed.memories) {
|
|
1495
|
+
await this.db.insert("memories", {
|
|
1496
|
+
message_id: messageId,
|
|
1497
|
+
user_id: userId,
|
|
1498
|
+
summary: memory.summary,
|
|
1499
|
+
contents: memory.contents,
|
|
1500
|
+
tags: JSON.stringify(memory.tags ?? []),
|
|
1501
|
+
category: memory.category
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
for (const ctx of parsed.userContext) {
|
|
1505
|
+
await this.db.insert("memories", {
|
|
1506
|
+
message_id: messageId,
|
|
1507
|
+
user_id: userId,
|
|
1508
|
+
summary: ctx.trait,
|
|
1509
|
+
contents: ctx.value,
|
|
1510
|
+
tags: JSON.stringify(["user_context"]),
|
|
1511
|
+
category: "user_context"
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
const files = [];
|
|
1515
|
+
for (const att of attachments) {
|
|
1516
|
+
if (att["contents"]) {
|
|
1517
|
+
files.push({
|
|
1518
|
+
filename: att["filename"] ?? "unknown",
|
|
1519
|
+
fileType: att["file_type"] ?? "file",
|
|
1520
|
+
contents: att["contents"],
|
|
1521
|
+
summary: att["summary"] ?? ""
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
const custom = {};
|
|
1526
|
+
for (const extractor of this.extractors) {
|
|
1527
|
+
try {
|
|
1528
|
+
const results = await extractor.extract(
|
|
1529
|
+
{ body, attachments },
|
|
1530
|
+
this.llmCall
|
|
1531
|
+
);
|
|
1532
|
+
if (results.length > 0) {
|
|
1533
|
+
custom[extractor.type] = results;
|
|
1534
|
+
}
|
|
1535
|
+
} catch {
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
const result = {
|
|
1539
|
+
messageId,
|
|
1540
|
+
tasks: parsed.tasks,
|
|
1541
|
+
memories: parsed.memories,
|
|
1542
|
+
files,
|
|
1543
|
+
userContext: parsed.userContext,
|
|
1544
|
+
custom,
|
|
1545
|
+
isTaskRequest: parsed.isTaskRequest
|
|
1546
|
+
};
|
|
1547
|
+
await this.hooks.emit("interpretation.completed", {
|
|
1548
|
+
messageId,
|
|
1549
|
+
taskCount: result.tasks.length,
|
|
1550
|
+
memoryCount: result.memories.length,
|
|
1551
|
+
fileCount: result.files.length,
|
|
1552
|
+
isTaskRequest: result.isTaskRequest
|
|
1553
|
+
});
|
|
1554
|
+
return result;
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* Extract structured data from message text using LLM.
|
|
1558
|
+
*/
|
|
1559
|
+
async extractWithLLM(body) {
|
|
1560
|
+
try {
|
|
1561
|
+
const result = await this.llmCall({
|
|
1562
|
+
model: this.model,
|
|
1563
|
+
messages: [{ role: "user", content: body }],
|
|
1564
|
+
system: INTERPRET_SYSTEM,
|
|
1565
|
+
maxTokens: 1e3
|
|
1566
|
+
});
|
|
1567
|
+
const parsed = JSON.parse(result.content);
|
|
1568
|
+
return {
|
|
1569
|
+
tasks: (parsed.tasks ?? []).map((t) => ({
|
|
1570
|
+
title: t.title,
|
|
1571
|
+
description: t.description,
|
|
1572
|
+
priority: t.priority
|
|
1573
|
+
})),
|
|
1574
|
+
memories: (parsed.memories ?? []).map((m) => ({
|
|
1575
|
+
summary: m.summary,
|
|
1576
|
+
contents: m.contents,
|
|
1577
|
+
tags: m.tags,
|
|
1578
|
+
category: m.category
|
|
1579
|
+
})),
|
|
1580
|
+
userContext: (parsed.user_context ?? []).map((u) => ({
|
|
1581
|
+
trait: u.trait,
|
|
1582
|
+
value: u.value
|
|
1583
|
+
})),
|
|
1584
|
+
isTaskRequest: parsed.is_task_request ?? false
|
|
1585
|
+
};
|
|
1586
|
+
} catch {
|
|
1587
|
+
return {
|
|
1588
|
+
tasks: [],
|
|
1589
|
+
memories: [],
|
|
1590
|
+
userContext: [],
|
|
1591
|
+
isTaskRequest: false
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
|
|
1021
1597
|
// src/core/chat/text-chunker.ts
|
|
1022
1598
|
function chunkText(text, maxLen) {
|
|
1023
1599
|
if (maxLen <= 0) throw new Error("maxLen must be > 0");
|
|
@@ -1639,6 +2215,88 @@ function defineCoreTables(db) {
|
|
|
1639
2215
|
"CREATE UNIQUE INDEX IF NOT EXISTS idx_secrets_name_env ON secrets(name, environment, org_id) WHERE deleted_at IS NULL"
|
|
1640
2216
|
]
|
|
1641
2217
|
});
|
|
2218
|
+
db.define("feedback", {
|
|
2219
|
+
columns: {
|
|
2220
|
+
id: "TEXT PRIMARY KEY",
|
|
2221
|
+
agent_id: "TEXT NOT NULL",
|
|
2222
|
+
task_id: "TEXT",
|
|
2223
|
+
issue: "TEXT NOT NULL",
|
|
2224
|
+
root_cause: "TEXT",
|
|
2225
|
+
severity: "TEXT NOT NULL DEFAULT 'medium'",
|
|
2226
|
+
repeatable: "INTEGER NOT NULL DEFAULT 0",
|
|
2227
|
+
accuracy_score: "REAL",
|
|
2228
|
+
efficiency_score: "REAL",
|
|
2229
|
+
tags: "TEXT NOT NULL DEFAULT '[]'",
|
|
2230
|
+
created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
|
|
2231
|
+
},
|
|
2232
|
+
tableConstraints: [
|
|
2233
|
+
"CREATE INDEX IF NOT EXISTS idx_feedback_agent ON feedback(agent_id, created_at)",
|
|
2234
|
+
"CREATE INDEX IF NOT EXISTS idx_feedback_issue ON feedback(issue)"
|
|
2235
|
+
]
|
|
2236
|
+
});
|
|
2237
|
+
db.define("playbooks", {
|
|
2238
|
+
columns: {
|
|
2239
|
+
id: "TEXT PRIMARY KEY",
|
|
2240
|
+
pattern: "TEXT NOT NULL",
|
|
2241
|
+
rule: "TEXT NOT NULL",
|
|
2242
|
+
feedback_ids: "TEXT NOT NULL DEFAULT '[]'",
|
|
2243
|
+
project_scoped: "INTEGER NOT NULL DEFAULT 1",
|
|
2244
|
+
created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
|
|
2245
|
+
updated_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
|
|
2246
|
+
deleted_at: "TEXT"
|
|
2247
|
+
},
|
|
2248
|
+
tableConstraints: [
|
|
2249
|
+
"CREATE INDEX IF NOT EXISTS idx_playbooks_pattern ON playbooks(pattern)"
|
|
2250
|
+
]
|
|
2251
|
+
});
|
|
2252
|
+
db.define("agent_playbooks", {
|
|
2253
|
+
columns: {
|
|
2254
|
+
agent_id: "TEXT NOT NULL",
|
|
2255
|
+
playbook_id: "TEXT NOT NULL",
|
|
2256
|
+
assigned_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
|
|
2257
|
+
},
|
|
2258
|
+
primaryKey: ["agent_id", "playbook_id"],
|
|
2259
|
+
tableConstraints: [
|
|
2260
|
+
"FOREIGN KEY (agent_id) REFERENCES agents(id)",
|
|
2261
|
+
"FOREIGN KEY (playbook_id) REFERENCES playbooks(id)"
|
|
2262
|
+
]
|
|
2263
|
+
});
|
|
2264
|
+
db.define("message_attachments", {
|
|
2265
|
+
columns: {
|
|
2266
|
+
id: "TEXT PRIMARY KEY",
|
|
2267
|
+
message_id: "TEXT NOT NULL",
|
|
2268
|
+
file_type: "TEXT NOT NULL DEFAULT 'file'",
|
|
2269
|
+
filename: "TEXT",
|
|
2270
|
+
mime_type: "TEXT",
|
|
2271
|
+
size_bytes: "INTEGER",
|
|
2272
|
+
contents: "TEXT",
|
|
2273
|
+
summary: "TEXT",
|
|
2274
|
+
url: "TEXT",
|
|
2275
|
+
created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
|
|
2276
|
+
},
|
|
2277
|
+
tableConstraints: [
|
|
2278
|
+
"CREATE INDEX IF NOT EXISTS idx_message_attachments_message ON message_attachments(message_id)",
|
|
2279
|
+
"FOREIGN KEY (message_id) REFERENCES messages(id)"
|
|
2280
|
+
]
|
|
2281
|
+
});
|
|
2282
|
+
db.define("memories", {
|
|
2283
|
+
columns: {
|
|
2284
|
+
id: "TEXT PRIMARY KEY",
|
|
2285
|
+
message_id: "TEXT",
|
|
2286
|
+
user_id: "TEXT",
|
|
2287
|
+
summary: "TEXT NOT NULL",
|
|
2288
|
+
contents: "TEXT NOT NULL",
|
|
2289
|
+
tags: "TEXT NOT NULL DEFAULT '[]'",
|
|
2290
|
+
category: "TEXT",
|
|
2291
|
+
created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
|
|
2292
|
+
updated_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
|
|
2293
|
+
deleted_at: "TEXT"
|
|
2294
|
+
},
|
|
2295
|
+
tableConstraints: [
|
|
2296
|
+
"CREATE INDEX IF NOT EXISTS idx_memories_user ON memories(user_id, created_at)",
|
|
2297
|
+
"CREATE INDEX IF NOT EXISTS idx_memories_message ON memories(message_id)"
|
|
2298
|
+
]
|
|
2299
|
+
});
|
|
1642
2300
|
}
|
|
1643
2301
|
|
|
1644
2302
|
// src/core/data/core-migrations.ts
|
|
@@ -1704,12 +2362,55 @@ function defineCoreEntityContexts(db) {
|
|
|
1704
2362
|
`# ${a.name}`,
|
|
1705
2363
|
"",
|
|
1706
2364
|
a.role ? `**Role:** ${a.role}` : null,
|
|
2365
|
+
a.adapter ? `**Adapter:** ${a.adapter}` : null,
|
|
1707
2366
|
a.status ? `**Status:** ${a.status}` : null,
|
|
1708
2367
|
a.cwd ? `**Working Directory:** ${a.cwd}` : null,
|
|
1709
2368
|
a.reports_to ? `**Reports To:** ${a.reports_to}` : null,
|
|
1710
2369
|
""
|
|
1711
2370
|
].filter(Boolean).join("\n");
|
|
1712
2371
|
}
|
|
2372
|
+
},
|
|
2373
|
+
"SKILLS.md": {
|
|
2374
|
+
source: {
|
|
2375
|
+
type: "manyToMany",
|
|
2376
|
+
junctionTable: "agent_skills",
|
|
2377
|
+
localKey: "agent_id",
|
|
2378
|
+
remoteKey: "skill_id",
|
|
2379
|
+
remoteTable: "skills"
|
|
2380
|
+
},
|
|
2381
|
+
omitIfEmpty: true,
|
|
2382
|
+
render: (rows) => {
|
|
2383
|
+
if (!rows.length) return "";
|
|
2384
|
+
const lines = [`# Skills (${rows.length})`, ""];
|
|
2385
|
+
for (const s of rows) {
|
|
2386
|
+
lines.push(`## ${s.name}`);
|
|
2387
|
+
if (s.category) lines.push(`**Category:** ${s.category}`);
|
|
2388
|
+
if (s.description) lines.push("", s.description);
|
|
2389
|
+
if (s.definition) lines.push("", "```", s.definition, "```");
|
|
2390
|
+
lines.push("");
|
|
2391
|
+
}
|
|
2392
|
+
return lines.join("\n");
|
|
2393
|
+
}
|
|
2394
|
+
},
|
|
2395
|
+
"PLAYBOOKS.md": {
|
|
2396
|
+
source: {
|
|
2397
|
+
type: "manyToMany",
|
|
2398
|
+
junctionTable: "agent_playbooks",
|
|
2399
|
+
localKey: "agent_id",
|
|
2400
|
+
remoteKey: "playbook_id",
|
|
2401
|
+
remoteTable: "playbooks"
|
|
2402
|
+
},
|
|
2403
|
+
omitIfEmpty: true,
|
|
2404
|
+
render: (rows) => {
|
|
2405
|
+
if (!rows.length) return "";
|
|
2406
|
+
const lines = [`# Playbooks (${rows.length})`, ""];
|
|
2407
|
+
for (const pb of rows) {
|
|
2408
|
+
lines.push(`## ${pb.pattern ?? pb.name ?? "Unnamed"}`);
|
|
2409
|
+
if (pb.rule) lines.push("", pb.rule);
|
|
2410
|
+
lines.push("");
|
|
2411
|
+
}
|
|
2412
|
+
return lines.join("\n");
|
|
2413
|
+
}
|
|
1713
2414
|
}
|
|
1714
2415
|
}
|
|
1715
2416
|
});
|
|
@@ -3097,6 +3798,13 @@ var RunManager = class {
|
|
|
3097
3798
|
// agentId → runId
|
|
3098
3799
|
orphanTimer = null;
|
|
3099
3800
|
staleThresholdMs;
|
|
3801
|
+
circuitBreaker;
|
|
3802
|
+
/**
|
|
3803
|
+
* Attach a CircuitBreaker to prevent retries on broken agents.
|
|
3804
|
+
*/
|
|
3805
|
+
setCircuitBreaker(cb) {
|
|
3806
|
+
this.circuitBreaker = cb;
|
|
3807
|
+
}
|
|
3100
3808
|
isLocked(agentId) {
|
|
3101
3809
|
return this.locks.has(agentId);
|
|
3102
3810
|
}
|
|
@@ -3134,11 +3842,15 @@ var RunManager = class {
|
|
|
3134
3842
|
this.locks.delete(agentId);
|
|
3135
3843
|
const taskId = run["task_id"];
|
|
3136
3844
|
if (!succeeded) {
|
|
3845
|
+
if (this.circuitBreaker) {
|
|
3846
|
+
await this.circuitBreaker.recordFailure(agentId, result.output);
|
|
3847
|
+
}
|
|
3137
3848
|
const task = await this.db.get("tasks", { id: taskId });
|
|
3138
3849
|
if (task) {
|
|
3139
3850
|
const retryCount = task["retry_count"] ?? 0;
|
|
3140
3851
|
const maxRetries = task["max_retries"] ?? 0;
|
|
3141
|
-
|
|
3852
|
+
const circuitOpen = this.circuitBreaker ? !this.circuitBreaker.canExecute(agentId) : false;
|
|
3853
|
+
if (retryCount < maxRetries && !circuitOpen) {
|
|
3142
3854
|
const maxBackoff = this.config?.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
|
|
3143
3855
|
const backoffMs = Math.min(BASE_BACKOFF_MS * Math.pow(2, retryCount), maxBackoff);
|
|
3144
3856
|
const nextRetryAt = new Date(Date.now() + backoffMs).toISOString();
|
|
@@ -3157,6 +3869,9 @@ var RunManager = class {
|
|
|
3157
3869
|
}
|
|
3158
3870
|
}
|
|
3159
3871
|
} else {
|
|
3872
|
+
if (this.circuitBreaker) {
|
|
3873
|
+
await this.circuitBreaker.recordSuccess(agentId);
|
|
3874
|
+
}
|
|
3160
3875
|
await this.db.update("tasks", { id: taskId }, {
|
|
3161
3876
|
status: "done",
|
|
3162
3877
|
result: result.output,
|
|
@@ -4096,6 +4811,803 @@ var CliExecutionAdapter = class {
|
|
|
4096
4811
|
}
|
|
4097
4812
|
};
|
|
4098
4813
|
|
|
4814
|
+
// src/core/orchestrator/adapters/deterministic-adapter.ts
|
|
4815
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
4816
|
+
var DeterministicAdapter = class {
|
|
4817
|
+
type = "deterministic";
|
|
4818
|
+
async execute(ctx) {
|
|
4819
|
+
const cwd = ctx.agent.cwd ?? process.cwd();
|
|
4820
|
+
let config = { command: "echo" };
|
|
4821
|
+
if (ctx.agent.adapter_config) {
|
|
4822
|
+
try {
|
|
4823
|
+
config = JSON.parse(ctx.agent.adapter_config);
|
|
4824
|
+
} catch {
|
|
4825
|
+
throw new Error("Invalid adapter_config for deterministic adapter");
|
|
4826
|
+
}
|
|
4827
|
+
}
|
|
4828
|
+
if (!config.command) {
|
|
4829
|
+
throw new Error("Deterministic adapter requires a command");
|
|
4830
|
+
}
|
|
4831
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
4832
|
+
const payload = JSON.stringify({
|
|
4833
|
+
taskId: ctx.task.title,
|
|
4834
|
+
description: ctx.task.description ?? "",
|
|
4835
|
+
context: ctx.task.context ?? ""
|
|
4836
|
+
});
|
|
4837
|
+
const args = [...config.args ?? []];
|
|
4838
|
+
if (config.inputMode === "arg") {
|
|
4839
|
+
args.push(payload);
|
|
4840
|
+
}
|
|
4841
|
+
const child = spawnProcess(config.command, args, {
|
|
4842
|
+
cwd,
|
|
4843
|
+
extraEnv: config.env
|
|
4844
|
+
});
|
|
4845
|
+
if (config.inputMode !== "arg" && child.stdin) {
|
|
4846
|
+
child.stdin.write(payload);
|
|
4847
|
+
child.stdin.end();
|
|
4848
|
+
}
|
|
4849
|
+
const stdoutChunks = [];
|
|
4850
|
+
child.stdout?.on("data", (chunk) => {
|
|
4851
|
+
stdoutChunks.push(chunk);
|
|
4852
|
+
ctx.onLog?.("stdout", chunk.toString("utf8"));
|
|
4853
|
+
});
|
|
4854
|
+
child.stderr?.on("data", (chunk) => {
|
|
4855
|
+
ctx.onLog?.("stderr", chunk.toString("utf8"));
|
|
4856
|
+
});
|
|
4857
|
+
const abortHandler = () => {
|
|
4858
|
+
if (child.pid != null) killProcessGroup(child.pid);
|
|
4859
|
+
};
|
|
4860
|
+
ctx.abortSignal?.addEventListener("abort", abortHandler);
|
|
4861
|
+
const timeout = setTimeout(() => {
|
|
4862
|
+
if (child.pid != null) killProcessGroup(child.pid);
|
|
4863
|
+
}, timeoutMs);
|
|
4864
|
+
const exitCode = await new Promise((resolve) => {
|
|
4865
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
4866
|
+
child.on("error", () => resolve(1));
|
|
4867
|
+
});
|
|
4868
|
+
clearTimeout(timeout);
|
|
4869
|
+
ctx.abortSignal?.removeEventListener("abort", abortHandler);
|
|
4870
|
+
const output = Buffer.concat(stdoutChunks).toString("utf8");
|
|
4871
|
+
return { output, exitCode };
|
|
4872
|
+
}
|
|
4873
|
+
};
|
|
4874
|
+
|
|
4875
|
+
// src/core/orchestrator/loop-detector.ts
|
|
4876
|
+
var LoopType = /* @__PURE__ */ ((LoopType2) => {
|
|
4877
|
+
LoopType2["SELF_LOOP"] = "self_loop";
|
|
4878
|
+
LoopType2["PING_PONG"] = "ping_pong";
|
|
4879
|
+
LoopType2["BLOCKED_REENTRY"] = "blocked_reentry";
|
|
4880
|
+
return LoopType2;
|
|
4881
|
+
})(LoopType || {});
|
|
4882
|
+
var DEFAULT_WINDOW = 10;
|
|
4883
|
+
var DEFAULT_PING_PONG_THRESHOLD = 2;
|
|
4884
|
+
var LoopDetector = class {
|
|
4885
|
+
constructor(db, config) {
|
|
4886
|
+
this.db = db;
|
|
4887
|
+
this.windowSize = config?.windowSize ?? DEFAULT_WINDOW;
|
|
4888
|
+
this.pingPongThreshold = config?.pingPongThreshold ?? DEFAULT_PING_PONG_THRESHOLD;
|
|
4889
|
+
}
|
|
4890
|
+
db;
|
|
4891
|
+
windowSize;
|
|
4892
|
+
pingPongThreshold;
|
|
4893
|
+
/**
|
|
4894
|
+
* Check for loops before creating a followup task.
|
|
4895
|
+
* Returns a LoopDetection if a loop pattern is found, undefined otherwise.
|
|
4896
|
+
*/
|
|
4897
|
+
async check(sourceAgentId, targetAgentId, taskId, chainOriginId) {
|
|
4898
|
+
const selfLoop = this.checkSelfLoop(sourceAgentId, targetAgentId, taskId);
|
|
4899
|
+
if (selfLoop) return selfLoop;
|
|
4900
|
+
const blocked = await this.checkBlockedReentry(targetAgentId, taskId, chainOriginId);
|
|
4901
|
+
if (blocked) return blocked;
|
|
4902
|
+
const pingPong = await this.checkPingPong(sourceAgentId, targetAgentId, chainOriginId);
|
|
4903
|
+
if (pingPong) return pingPong;
|
|
4904
|
+
return void 0;
|
|
4905
|
+
}
|
|
4906
|
+
/**
|
|
4907
|
+
* Check if an agent is routing to itself.
|
|
4908
|
+
*/
|
|
4909
|
+
checkSelfLoop(sourceAgentId, targetAgentId, taskId) {
|
|
4910
|
+
if (sourceAgentId === targetAgentId) {
|
|
4911
|
+
return {
|
|
4912
|
+
type: "self_loop" /* SELF_LOOP */,
|
|
4913
|
+
agents: [sourceAgentId],
|
|
4914
|
+
taskId,
|
|
4915
|
+
message: `Self-loop detected: agent ${sourceAgentId} is routing to itself`
|
|
4916
|
+
};
|
|
4917
|
+
}
|
|
4918
|
+
return void 0;
|
|
4919
|
+
}
|
|
4920
|
+
/**
|
|
4921
|
+
* Check if a previously blocked task is being re-entered.
|
|
4922
|
+
*/
|
|
4923
|
+
async checkBlockedReentry(targetAgentId, taskId, chainOriginId) {
|
|
4924
|
+
const originId = chainOriginId ?? taskId;
|
|
4925
|
+
const chainTasks = await this.db.query("tasks", {
|
|
4926
|
+
where: { chain_origin_id: originId }
|
|
4927
|
+
});
|
|
4928
|
+
const blockedInChain = chainTasks.filter(
|
|
4929
|
+
(t) => (t["status"] === "blocked" || t["status"] === "failed") && t["assignee_id"] === targetAgentId
|
|
4930
|
+
);
|
|
4931
|
+
if (blockedInChain.length > 0) {
|
|
4932
|
+
return {
|
|
4933
|
+
type: "blocked_reentry" /* BLOCKED_REENTRY */,
|
|
4934
|
+
agents: [targetAgentId],
|
|
4935
|
+
taskId,
|
|
4936
|
+
chainOriginId: originId,
|
|
4937
|
+
message: `Blocked re-entry: agent ${targetAgentId} already has a blocked/failed task in chain ${originId}`
|
|
4938
|
+
};
|
|
4939
|
+
}
|
|
4940
|
+
return void 0;
|
|
4941
|
+
}
|
|
4942
|
+
/**
|
|
4943
|
+
* Check for A→B→A→B ping-pong by scanning recent tasks in the chain.
|
|
4944
|
+
*/
|
|
4945
|
+
async checkPingPong(sourceAgentId, targetAgentId, chainOriginId) {
|
|
4946
|
+
if (!chainOriginId) return void 0;
|
|
4947
|
+
const chainTasks = await this.db.query("tasks", {
|
|
4948
|
+
where: { chain_origin_id: chainOriginId }
|
|
4949
|
+
});
|
|
4950
|
+
const sorted = chainTasks.sort((a, b) => {
|
|
4951
|
+
const depthDiff = (a["chain_depth"] ?? 0) - (b["chain_depth"] ?? 0);
|
|
4952
|
+
if (depthDiff !== 0) return depthDiff;
|
|
4953
|
+
return (a["created_at"] ?? "").localeCompare(b["created_at"] ?? "");
|
|
4954
|
+
}).slice(-this.windowSize);
|
|
4955
|
+
const agentSequence = sorted.map((t) => t["assignee_id"]).filter(Boolean);
|
|
4956
|
+
agentSequence.push(targetAgentId);
|
|
4957
|
+
if (agentSequence.length >= this.pingPongThreshold * 2) {
|
|
4958
|
+
const tail = agentSequence.slice(-this.pingPongThreshold * 2);
|
|
4959
|
+
const a = tail[0];
|
|
4960
|
+
const b = tail[1];
|
|
4961
|
+
if (a && b && a !== b) {
|
|
4962
|
+
let isPingPong = true;
|
|
4963
|
+
for (let i = 0; i < tail.length; i++) {
|
|
4964
|
+
if (tail[i] !== (i % 2 === 0 ? a : b)) {
|
|
4965
|
+
isPingPong = false;
|
|
4966
|
+
break;
|
|
4967
|
+
}
|
|
4968
|
+
}
|
|
4969
|
+
if (isPingPong) {
|
|
4970
|
+
return {
|
|
4971
|
+
type: "ping_pong" /* PING_PONG */,
|
|
4972
|
+
agents: [a, b],
|
|
4973
|
+
taskId: sorted[sorted.length - 1]?.["id"] ?? "",
|
|
4974
|
+
chainOriginId,
|
|
4975
|
+
message: `Ping-pong detected: agents ${a} and ${b} are bouncing tasks in chain ${chainOriginId}`
|
|
4976
|
+
};
|
|
4977
|
+
}
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
return void 0;
|
|
4981
|
+
}
|
|
4982
|
+
};
|
|
4983
|
+
|
|
4984
|
+
// src/core/orchestrator/circuit-breaker.ts
|
|
4985
|
+
var BreakerState = /* @__PURE__ */ ((BreakerState2) => {
|
|
4986
|
+
BreakerState2["CLOSED"] = "closed";
|
|
4987
|
+
BreakerState2["OPEN"] = "open";
|
|
4988
|
+
BreakerState2["HALF_OPEN"] = "half_open";
|
|
4989
|
+
return BreakerState2;
|
|
4990
|
+
})(BreakerState || {});
|
|
4991
|
+
var DEFAULT_FAILURE_THRESHOLD = 3;
|
|
4992
|
+
var DEFAULT_RESET_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
4993
|
+
var CircuitBreaker = class {
|
|
4994
|
+
constructor(db, hooks, config) {
|
|
4995
|
+
this.db = db;
|
|
4996
|
+
this.hooks = hooks;
|
|
4997
|
+
this.failureThreshold = config?.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD;
|
|
4998
|
+
this.resetTimeoutMs = config?.resetTimeoutMs ?? DEFAULT_RESET_TIMEOUT_MS;
|
|
4999
|
+
this.persist = config?.persist ?? true;
|
|
5000
|
+
}
|
|
5001
|
+
db;
|
|
5002
|
+
hooks;
|
|
5003
|
+
breakers = /* @__PURE__ */ new Map();
|
|
5004
|
+
failureThreshold;
|
|
5005
|
+
resetTimeoutMs;
|
|
5006
|
+
persist;
|
|
5007
|
+
/**
|
|
5008
|
+
* Check if an agent is allowed to execute.
|
|
5009
|
+
* Returns true if execution is allowed, false if circuit is open.
|
|
5010
|
+
*/
|
|
5011
|
+
canExecute(agentId) {
|
|
5012
|
+
const breaker = this.breakers.get(agentId);
|
|
5013
|
+
if (!breaker) return true;
|
|
5014
|
+
switch (breaker.state) {
|
|
5015
|
+
case "closed" /* CLOSED */:
|
|
5016
|
+
return true;
|
|
5017
|
+
case "open" /* OPEN */: {
|
|
5018
|
+
const elapsed = Date.now() - (breaker.trippedAt ?? 0);
|
|
5019
|
+
if (elapsed >= this.resetTimeoutMs) {
|
|
5020
|
+
breaker.state = "half_open" /* HALF_OPEN */;
|
|
5021
|
+
return true;
|
|
5022
|
+
}
|
|
5023
|
+
return false;
|
|
5024
|
+
}
|
|
5025
|
+
case "half_open" /* HALF_OPEN */:
|
|
5026
|
+
return true;
|
|
5027
|
+
}
|
|
5028
|
+
}
|
|
5029
|
+
/**
|
|
5030
|
+
* Record a successful execution. Resets the breaker to CLOSED.
|
|
5031
|
+
*/
|
|
5032
|
+
async recordSuccess(agentId) {
|
|
5033
|
+
const breaker = this.breakers.get(agentId);
|
|
5034
|
+
if (!breaker) return;
|
|
5035
|
+
const previousState = breaker.state;
|
|
5036
|
+
breaker.state = "closed" /* CLOSED */;
|
|
5037
|
+
breaker.failureCount = 0;
|
|
5038
|
+
if (previousState === "half_open" /* HALF_OPEN */) {
|
|
5039
|
+
await this.logEvent(agentId, "circuit_recovered", {
|
|
5040
|
+
previousState
|
|
5041
|
+
});
|
|
5042
|
+
await this.hooks.emit("circuit_breaker.recovered", {
|
|
5043
|
+
agentId,
|
|
5044
|
+
previousState
|
|
5045
|
+
});
|
|
5046
|
+
}
|
|
5047
|
+
}
|
|
5048
|
+
/**
|
|
5049
|
+
* Record a failed execution. Increments failure count and may trip breaker.
|
|
5050
|
+
*/
|
|
5051
|
+
async recordFailure(agentId, reason) {
|
|
5052
|
+
let breaker = this.breakers.get(agentId);
|
|
5053
|
+
if (!breaker) {
|
|
5054
|
+
breaker = {
|
|
5055
|
+
state: "closed" /* CLOSED */,
|
|
5056
|
+
failureCount: 0,
|
|
5057
|
+
lastFailureAt: Date.now()
|
|
5058
|
+
};
|
|
5059
|
+
this.breakers.set(agentId, breaker);
|
|
5060
|
+
}
|
|
5061
|
+
breaker.failureCount++;
|
|
5062
|
+
breaker.lastFailureAt = Date.now();
|
|
5063
|
+
if (breaker.state === "half_open" /* HALF_OPEN */) {
|
|
5064
|
+
await this.trip(agentId, reason ?? "Probe execution failed during half-open state");
|
|
5065
|
+
return;
|
|
5066
|
+
}
|
|
5067
|
+
if (breaker.failureCount >= this.failureThreshold) {
|
|
5068
|
+
await this.trip(agentId, reason ?? `Failure threshold reached (${this.failureThreshold})`);
|
|
5069
|
+
}
|
|
5070
|
+
}
|
|
5071
|
+
/**
|
|
5072
|
+
* Trip the breaker to OPEN state and escalate to human.
|
|
5073
|
+
*/
|
|
5074
|
+
async trip(agentId, reason) {
|
|
5075
|
+
let breaker = this.breakers.get(agentId);
|
|
5076
|
+
if (!breaker) {
|
|
5077
|
+
breaker = {
|
|
5078
|
+
state: "closed" /* CLOSED */,
|
|
5079
|
+
failureCount: 0,
|
|
5080
|
+
lastFailureAt: Date.now()
|
|
5081
|
+
};
|
|
5082
|
+
this.breakers.set(agentId, breaker);
|
|
5083
|
+
}
|
|
5084
|
+
breaker.state = "open" /* OPEN */;
|
|
5085
|
+
breaker.trippedAt = Date.now();
|
|
5086
|
+
await this.logEvent(agentId, "circuit_tripped", {
|
|
5087
|
+
reason,
|
|
5088
|
+
failureCount: breaker.failureCount
|
|
5089
|
+
});
|
|
5090
|
+
await this.hooks.emit("circuit_breaker.tripped", {
|
|
5091
|
+
agentId,
|
|
5092
|
+
reason,
|
|
5093
|
+
failureCount: breaker.failureCount,
|
|
5094
|
+
action: "escalate_to_human"
|
|
5095
|
+
});
|
|
5096
|
+
}
|
|
5097
|
+
/**
|
|
5098
|
+
* Manually reset a breaker (e.g. after human review).
|
|
5099
|
+
*/
|
|
5100
|
+
async reset(agentId) {
|
|
5101
|
+
this.breakers.delete(agentId);
|
|
5102
|
+
await this.logEvent(agentId, "circuit_reset", {});
|
|
5103
|
+
await this.hooks.emit("circuit_breaker.reset", { agentId });
|
|
5104
|
+
}
|
|
5105
|
+
/**
|
|
5106
|
+
* Get the current state of a breaker.
|
|
5107
|
+
*/
|
|
5108
|
+
getState(agentId) {
|
|
5109
|
+
return this.breakers.get(agentId)?.state ?? "closed" /* CLOSED */;
|
|
5110
|
+
}
|
|
5111
|
+
/**
|
|
5112
|
+
* Get failure count for an agent.
|
|
5113
|
+
*/
|
|
5114
|
+
getFailureCount(agentId) {
|
|
5115
|
+
return this.breakers.get(agentId)?.failureCount ?? 0;
|
|
5116
|
+
}
|
|
5117
|
+
async logEvent(agentId, eventType, payload) {
|
|
5118
|
+
if (!this.persist) return;
|
|
5119
|
+
await this.db.insert("activity_log", {
|
|
5120
|
+
agent_id: agentId,
|
|
5121
|
+
event_type: eventType,
|
|
5122
|
+
payload: JSON.stringify(payload)
|
|
5123
|
+
});
|
|
5124
|
+
}
|
|
5125
|
+
};
|
|
5126
|
+
|
|
5127
|
+
// src/core/orchestrator/learning-pipeline.ts
|
|
5128
|
+
var DEFAULT_PLAYBOOK_THRESHOLD = 3;
|
|
5129
|
+
var DEFAULT_SKILL_THRESHOLD = 3;
|
|
5130
|
+
var LearningPipeline = class {
|
|
5131
|
+
constructor(db, hooks, config) {
|
|
5132
|
+
this.db = db;
|
|
5133
|
+
this.hooks = hooks;
|
|
5134
|
+
this.playbookThreshold = config?.playbookThreshold ?? DEFAULT_PLAYBOOK_THRESHOLD;
|
|
5135
|
+
this.skillThreshold = config?.skillThreshold ?? DEFAULT_SKILL_THRESHOLD;
|
|
5136
|
+
this.autoPromote = config?.autoPromote ?? false;
|
|
5137
|
+
}
|
|
5138
|
+
db;
|
|
5139
|
+
hooks;
|
|
5140
|
+
playbookThreshold;
|
|
5141
|
+
skillThreshold;
|
|
5142
|
+
autoPromote;
|
|
5143
|
+
// --- Feedback Layer ---
|
|
5144
|
+
/**
|
|
5145
|
+
* Capture a structured feedback record from an execution.
|
|
5146
|
+
*/
|
|
5147
|
+
async captureFeedback(entry) {
|
|
5148
|
+
const row = await this.db.insert("feedback", {
|
|
5149
|
+
agent_id: entry.agentId,
|
|
5150
|
+
task_id: entry.taskId,
|
|
5151
|
+
issue: entry.issue,
|
|
5152
|
+
root_cause: entry.rootCause,
|
|
5153
|
+
severity: entry.severity,
|
|
5154
|
+
repeatable: entry.repeatable ? 1 : 0,
|
|
5155
|
+
accuracy_score: entry.accuracyScore,
|
|
5156
|
+
efficiency_score: entry.efficiencyScore,
|
|
5157
|
+
tags: JSON.stringify(entry.tags ?? [])
|
|
5158
|
+
});
|
|
5159
|
+
const feedbackId = row["id"];
|
|
5160
|
+
await this.hooks.emit("learning.feedback_captured", {
|
|
5161
|
+
feedbackId,
|
|
5162
|
+
agentId: entry.agentId,
|
|
5163
|
+
issue: entry.issue,
|
|
5164
|
+
severity: entry.severity
|
|
5165
|
+
});
|
|
5166
|
+
if (this.autoPromote) {
|
|
5167
|
+
await this.checkPlaybookPromotion(entry.issue);
|
|
5168
|
+
}
|
|
5169
|
+
return feedbackId;
|
|
5170
|
+
}
|
|
5171
|
+
/**
|
|
5172
|
+
* Get all feedback records, optionally filtered.
|
|
5173
|
+
*/
|
|
5174
|
+
async listFeedback(filter) {
|
|
5175
|
+
const where = {};
|
|
5176
|
+
if (filter?.agentId) where["agent_id"] = filter.agentId;
|
|
5177
|
+
if (filter?.severity) where["severity"] = filter.severity;
|
|
5178
|
+
if (filter?.repeatable !== void 0) where["repeatable"] = filter.repeatable ? 1 : 0;
|
|
5179
|
+
return this.db.query("feedback", Object.keys(where).length ? { where } : void 0);
|
|
5180
|
+
}
|
|
5181
|
+
// --- Playbook Layer ---
|
|
5182
|
+
/**
|
|
5183
|
+
* Check if feedback records with similar issues should be promoted to a playbook.
|
|
5184
|
+
* Groups by issue text similarity (exact match for now).
|
|
5185
|
+
*/
|
|
5186
|
+
async checkPlaybookPromotion(issue) {
|
|
5187
|
+
const allFeedback = await this.db.query("feedback", {
|
|
5188
|
+
where: { issue }
|
|
5189
|
+
});
|
|
5190
|
+
if (allFeedback.length < this.playbookThreshold) {
|
|
5191
|
+
return void 0;
|
|
5192
|
+
}
|
|
5193
|
+
const existingPlaybooks = await this.db.query("playbooks", {
|
|
5194
|
+
where: { pattern: issue }
|
|
5195
|
+
});
|
|
5196
|
+
if (existingPlaybooks.length > 0) {
|
|
5197
|
+
return existingPlaybooks[0]["id"];
|
|
5198
|
+
}
|
|
5199
|
+
const feedbackIds = allFeedback.map((f) => f["id"]);
|
|
5200
|
+
const rootCauses = allFeedback.map((f) => f["root_cause"]).filter(Boolean);
|
|
5201
|
+
const rule = rootCauses.length > 0 ? `When encountering "${issue}": ${rootCauses[0]}` : `Pattern detected: "${issue}" \u2014 review and add specific guidance.`;
|
|
5202
|
+
const playbookId = await this.promoteToPlaybook({
|
|
5203
|
+
pattern: issue,
|
|
5204
|
+
rule,
|
|
5205
|
+
feedbackIds,
|
|
5206
|
+
projectScoped: true
|
|
5207
|
+
});
|
|
5208
|
+
return playbookId;
|
|
5209
|
+
}
|
|
5210
|
+
/**
|
|
5211
|
+
* Manually create a playbook from a set of feedback records.
|
|
5212
|
+
*/
|
|
5213
|
+
async promoteToPlaybook(entry) {
|
|
5214
|
+
const row = await this.db.insert("playbooks", {
|
|
5215
|
+
pattern: entry.pattern,
|
|
5216
|
+
rule: entry.rule,
|
|
5217
|
+
feedback_ids: JSON.stringify(entry.feedbackIds),
|
|
5218
|
+
project_scoped: entry.projectScoped ? 1 : 0
|
|
5219
|
+
});
|
|
5220
|
+
const playbookId = row["id"];
|
|
5221
|
+
if (entry.agentIds) {
|
|
5222
|
+
for (const agentId of entry.agentIds) {
|
|
5223
|
+
await this.db.insert("agent_playbooks", {
|
|
5224
|
+
agent_id: agentId,
|
|
5225
|
+
playbook_id: playbookId
|
|
5226
|
+
});
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
5229
|
+
await this.hooks.emit("learning.playbook_promoted", {
|
|
5230
|
+
playbookId,
|
|
5231
|
+
pattern: entry.pattern,
|
|
5232
|
+
feedbackCount: entry.feedbackIds.length
|
|
5233
|
+
});
|
|
5234
|
+
return playbookId;
|
|
5235
|
+
}
|
|
5236
|
+
/**
|
|
5237
|
+
* List playbooks, optionally filtered.
|
|
5238
|
+
*/
|
|
5239
|
+
async listPlaybooks(filter) {
|
|
5240
|
+
const where = {};
|
|
5241
|
+
if (filter?.projectScoped !== void 0) {
|
|
5242
|
+
where["project_scoped"] = filter.projectScoped ? 1 : 0;
|
|
5243
|
+
}
|
|
5244
|
+
return this.db.query("playbooks", Object.keys(where).length ? { where } : void 0);
|
|
5245
|
+
}
|
|
5246
|
+
// --- Skill Layer ---
|
|
5247
|
+
/**
|
|
5248
|
+
* Check if a playbook should be promoted to a skill.
|
|
5249
|
+
* A playbook becomes a skill when it works across multiple projects
|
|
5250
|
+
* (indicated by being referenced by agents in different contexts).
|
|
5251
|
+
*/
|
|
5252
|
+
async checkSkillPromotion(playbookId) {
|
|
5253
|
+
const playbook = await this.db.get("playbooks", { id: playbookId });
|
|
5254
|
+
if (!playbook) return void 0;
|
|
5255
|
+
const links = await this.db.query("agent_playbooks", {
|
|
5256
|
+
where: { playbook_id: playbookId }
|
|
5257
|
+
});
|
|
5258
|
+
if (links.length < this.skillThreshold) {
|
|
5259
|
+
return void 0;
|
|
5260
|
+
}
|
|
5261
|
+
const pattern = playbook["pattern"];
|
|
5262
|
+
const existingSkills = await this.db.query("skills", {
|
|
5263
|
+
where: { name: pattern }
|
|
5264
|
+
});
|
|
5265
|
+
if (existingSkills.length > 0) {
|
|
5266
|
+
return existingSkills[0]["id"];
|
|
5267
|
+
}
|
|
5268
|
+
const slug = pattern.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 64);
|
|
5269
|
+
const skillId = await this.promoteToSkill({
|
|
5270
|
+
name: pattern,
|
|
5271
|
+
slug,
|
|
5272
|
+
description: `Auto-promoted from playbook: ${pattern}`,
|
|
5273
|
+
behavior: playbook["rule"],
|
|
5274
|
+
sourcePlaybookIds: [playbookId]
|
|
5275
|
+
});
|
|
5276
|
+
return skillId;
|
|
5277
|
+
}
|
|
5278
|
+
/**
|
|
5279
|
+
* Manually promote a playbook to a reusable skill.
|
|
5280
|
+
*/
|
|
5281
|
+
async promoteToSkill(entry) {
|
|
5282
|
+
const row = await this.db.insert("skills", {
|
|
5283
|
+
name: entry.name,
|
|
5284
|
+
slug: entry.slug,
|
|
5285
|
+
description: entry.description,
|
|
5286
|
+
category: entry.category ?? "learned",
|
|
5287
|
+
definition: JSON.stringify({
|
|
5288
|
+
behavior: entry.behavior,
|
|
5289
|
+
source_playbook_ids: entry.sourcePlaybookIds
|
|
5290
|
+
})
|
|
5291
|
+
});
|
|
5292
|
+
const skillId = row["id"];
|
|
5293
|
+
await this.hooks.emit("learning.skill_promoted", {
|
|
5294
|
+
skillId,
|
|
5295
|
+
name: entry.name,
|
|
5296
|
+
slug: entry.slug,
|
|
5297
|
+
sourcePlaybookCount: entry.sourcePlaybookIds.length
|
|
5298
|
+
});
|
|
5299
|
+
return skillId;
|
|
5300
|
+
}
|
|
5301
|
+
/**
|
|
5302
|
+
* Assign a skill to an agent.
|
|
5303
|
+
*/
|
|
5304
|
+
async assignSkill(agentId, skillId) {
|
|
5305
|
+
await this.db.link("agent_skills", {
|
|
5306
|
+
agent_id: agentId,
|
|
5307
|
+
skill_id: skillId
|
|
5308
|
+
});
|
|
5309
|
+
await this.hooks.emit("learning.skill_assigned", { agentId, skillId });
|
|
5310
|
+
}
|
|
5311
|
+
/**
|
|
5312
|
+
* Get learning metrics for an agent.
|
|
5313
|
+
*/
|
|
5314
|
+
async getMetrics(agentId) {
|
|
5315
|
+
const feedback = await this.db.query("feedback", { where: { agent_id: agentId } });
|
|
5316
|
+
const accuracyScores = feedback.map((f) => f["accuracy_score"]).filter((s) => s !== null && s !== void 0);
|
|
5317
|
+
const efficiencyScores = feedback.map((f) => f["efficiency_score"]).filter((s) => s !== null && s !== void 0);
|
|
5318
|
+
let playbookCount = 0;
|
|
5319
|
+
try {
|
|
5320
|
+
const links = await this.db.query("agent_playbooks", { where: { agent_id: agentId } });
|
|
5321
|
+
playbookCount = links.length;
|
|
5322
|
+
} catch {
|
|
5323
|
+
}
|
|
5324
|
+
const skillLinks = await this.db.query("agent_skills", { where: { agent_id: agentId } });
|
|
5325
|
+
return {
|
|
5326
|
+
feedbackCount: feedback.length,
|
|
5327
|
+
avgAccuracy: accuracyScores.length > 0 ? accuracyScores.reduce((a, b) => a + b, 0) / accuracyScores.length : null,
|
|
5328
|
+
avgEfficiency: efficiencyScores.length > 0 ? efficiencyScores.reduce((a, b) => a + b, 0) / efficiencyScores.length : null,
|
|
5329
|
+
playbookCount,
|
|
5330
|
+
skillCount: skillLinks.length
|
|
5331
|
+
};
|
|
5332
|
+
}
|
|
5333
|
+
};
|
|
5334
|
+
|
|
5335
|
+
// src/core/orchestrator/permission-relay.ts
|
|
5336
|
+
var DEFAULT_POLL_INTERVAL_MS = 5e3;
|
|
5337
|
+
var DEFAULT_TIMEOUT_MS2 = 5 * 60 * 1e3;
|
|
5338
|
+
var PermissionRelay = class {
|
|
5339
|
+
constructor(hooks, config) {
|
|
5340
|
+
this.hooks = hooks;
|
|
5341
|
+
this.providers = config.providers;
|
|
5342
|
+
this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
5343
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
5344
|
+
}
|
|
5345
|
+
hooks;
|
|
5346
|
+
providers;
|
|
5347
|
+
pollIntervalMs;
|
|
5348
|
+
timeoutMs;
|
|
5349
|
+
pending = /* @__PURE__ */ new Map();
|
|
5350
|
+
/**
|
|
5351
|
+
* Request approval from all configured providers.
|
|
5352
|
+
* Returns when the first provider responds (approve or deny).
|
|
5353
|
+
*/
|
|
5354
|
+
async requestApproval(prompt) {
|
|
5355
|
+
const expiresAt = new Date(Date.now() + this.timeoutMs).toISOString();
|
|
5356
|
+
const promptWithExpiry = { ...prompt, expiresAt };
|
|
5357
|
+
await this.hooks.emit("permission.requested", {
|
|
5358
|
+
promptId: prompt.id,
|
|
5359
|
+
agentId: prompt.agentId,
|
|
5360
|
+
action: prompt.action
|
|
5361
|
+
});
|
|
5362
|
+
const handles = /* @__PURE__ */ new Map();
|
|
5363
|
+
for (const provider of this.providers) {
|
|
5364
|
+
try {
|
|
5365
|
+
const handle = await provider.sendPrompt(promptWithExpiry);
|
|
5366
|
+
handles.set(provider.id, handle);
|
|
5367
|
+
} catch {
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
if (handles.size === 0) {
|
|
5371
|
+
throw new Error("No permission providers available");
|
|
5372
|
+
}
|
|
5373
|
+
return new Promise((resolve, reject) => {
|
|
5374
|
+
const entry = {
|
|
5375
|
+
prompt: promptWithExpiry,
|
|
5376
|
+
handles,
|
|
5377
|
+
resolve,
|
|
5378
|
+
reject
|
|
5379
|
+
};
|
|
5380
|
+
this.pending.set(prompt.id, entry);
|
|
5381
|
+
const pollTimer = setInterval(async () => {
|
|
5382
|
+
for (const [providerId, handle] of handles) {
|
|
5383
|
+
const provider = this.providers.find((p) => p.id === providerId);
|
|
5384
|
+
if (!provider) continue;
|
|
5385
|
+
try {
|
|
5386
|
+
const response = await provider.pollResponse(handle);
|
|
5387
|
+
if (response) {
|
|
5388
|
+
clearInterval(pollTimer);
|
|
5389
|
+
clearTimeout(timeoutTimer);
|
|
5390
|
+
this.pending.delete(prompt.id);
|
|
5391
|
+
await this.cancelOtherProviders(handles, providerId);
|
|
5392
|
+
await this.hooks.emit("permission.responded", {
|
|
5393
|
+
promptId: prompt.id,
|
|
5394
|
+
status: response.status,
|
|
5395
|
+
respondedBy: response.respondedBy
|
|
5396
|
+
});
|
|
5397
|
+
resolve(response);
|
|
5398
|
+
return;
|
|
5399
|
+
}
|
|
5400
|
+
} catch {
|
|
5401
|
+
}
|
|
5402
|
+
}
|
|
5403
|
+
}, this.pollIntervalMs);
|
|
5404
|
+
const timeoutTimer = setTimeout(async () => {
|
|
5405
|
+
clearInterval(pollTimer);
|
|
5406
|
+
this.pending.delete(prompt.id);
|
|
5407
|
+
for (const [providerId, handle] of handles) {
|
|
5408
|
+
const provider = this.providers.find((p) => p.id === providerId);
|
|
5409
|
+
if (provider) {
|
|
5410
|
+
try {
|
|
5411
|
+
await provider.cancelPrompt(handle);
|
|
5412
|
+
} catch {
|
|
5413
|
+
}
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
await this.hooks.emit("permission.expired", {
|
|
5417
|
+
promptId: prompt.id,
|
|
5418
|
+
agentId: prompt.agentId
|
|
5419
|
+
});
|
|
5420
|
+
reject(new Error(`Permission request expired after ${this.timeoutMs}ms`));
|
|
5421
|
+
}, this.timeoutMs);
|
|
5422
|
+
});
|
|
5423
|
+
}
|
|
5424
|
+
/**
|
|
5425
|
+
* Provide a local approval (from terminal).
|
|
5426
|
+
* Resolves the pending request and cancels remote providers.
|
|
5427
|
+
*/
|
|
5428
|
+
async approveLocally(promptId, approved) {
|
|
5429
|
+
const entry = this.pending.get(promptId);
|
|
5430
|
+
if (!entry) return;
|
|
5431
|
+
const response = {
|
|
5432
|
+
promptId,
|
|
5433
|
+
status: approved ? "approved" : "denied",
|
|
5434
|
+
respondedBy: "local",
|
|
5435
|
+
respondedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5436
|
+
};
|
|
5437
|
+
this.pending.delete(promptId);
|
|
5438
|
+
for (const [providerId, handle] of entry.handles) {
|
|
5439
|
+
const provider = this.providers.find((p) => p.id === providerId);
|
|
5440
|
+
if (provider) {
|
|
5441
|
+
try {
|
|
5442
|
+
await provider.cancelPrompt(handle);
|
|
5443
|
+
} catch {
|
|
5444
|
+
}
|
|
5445
|
+
}
|
|
5446
|
+
}
|
|
5447
|
+
await this.hooks.emit("permission.responded", {
|
|
5448
|
+
promptId,
|
|
5449
|
+
status: response.status,
|
|
5450
|
+
respondedBy: "local"
|
|
5451
|
+
});
|
|
5452
|
+
entry.resolve(response);
|
|
5453
|
+
}
|
|
5454
|
+
/**
|
|
5455
|
+
* Get all pending approval requests.
|
|
5456
|
+
*/
|
|
5457
|
+
getPending() {
|
|
5458
|
+
return Array.from(this.pending.values()).map((e) => e.prompt);
|
|
5459
|
+
}
|
|
5460
|
+
async cancelOtherProviders(handles, excludeProviderId) {
|
|
5461
|
+
for (const [providerId, handle] of handles) {
|
|
5462
|
+
if (providerId === excludeProviderId) continue;
|
|
5463
|
+
const provider = this.providers.find((p) => p.id === providerId);
|
|
5464
|
+
if (provider) {
|
|
5465
|
+
try {
|
|
5466
|
+
await provider.cancelPrompt(handle);
|
|
5467
|
+
} catch {
|
|
5468
|
+
}
|
|
5469
|
+
}
|
|
5470
|
+
}
|
|
5471
|
+
}
|
|
5472
|
+
};
|
|
5473
|
+
|
|
5474
|
+
// src/core/orchestrator/governance-gate.ts
|
|
5475
|
+
var GovernanceGate = class {
|
|
5476
|
+
};
|
|
5477
|
+
var QAGate = class extends GovernanceGate {
|
|
5478
|
+
constructor(validators = []) {
|
|
5479
|
+
super();
|
|
5480
|
+
this.validators = validators;
|
|
5481
|
+
}
|
|
5482
|
+
validators;
|
|
5483
|
+
id = "qa";
|
|
5484
|
+
name = "Quality Assurance";
|
|
5485
|
+
dimension = "data_correctness";
|
|
5486
|
+
async check(input) {
|
|
5487
|
+
const start = Date.now();
|
|
5488
|
+
const findings = [];
|
|
5489
|
+
for (const validator of this.validators) {
|
|
5490
|
+
const results = validator.validate(input.output, input.metadata);
|
|
5491
|
+
findings.push(...results);
|
|
5492
|
+
}
|
|
5493
|
+
const hasErrors = findings.some((f) => f.severity === "error" || f.severity === "critical");
|
|
5494
|
+
const hasWarnings = findings.some((f) => f.severity === "warning");
|
|
5495
|
+
return {
|
|
5496
|
+
gateId: this.id,
|
|
5497
|
+
verdict: hasErrors ? "fail" : hasWarnings ? "warn" : "pass",
|
|
5498
|
+
findings,
|
|
5499
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5500
|
+
durationMs: Date.now() - start
|
|
5501
|
+
};
|
|
5502
|
+
}
|
|
5503
|
+
};
|
|
5504
|
+
var QualityGate = class extends GovernanceGate {
|
|
5505
|
+
constructor(checks = []) {
|
|
5506
|
+
super();
|
|
5507
|
+
this.checks = checks;
|
|
5508
|
+
}
|
|
5509
|
+
checks;
|
|
5510
|
+
id = "quality";
|
|
5511
|
+
name = "Code Quality";
|
|
5512
|
+
dimension = "code_quality";
|
|
5513
|
+
async check(input) {
|
|
5514
|
+
const start = Date.now();
|
|
5515
|
+
const findings = [];
|
|
5516
|
+
for (const chk of this.checks) {
|
|
5517
|
+
const results = await chk.check(input.output, input.metadata);
|
|
5518
|
+
findings.push(...results);
|
|
5519
|
+
}
|
|
5520
|
+
const hasErrors = findings.some((f) => f.severity === "error" || f.severity === "critical");
|
|
5521
|
+
const hasWarnings = findings.some((f) => f.severity === "warning");
|
|
5522
|
+
return {
|
|
5523
|
+
gateId: this.id,
|
|
5524
|
+
verdict: hasErrors ? "fail" : hasWarnings ? "warn" : "pass",
|
|
5525
|
+
findings,
|
|
5526
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5527
|
+
durationMs: Date.now() - start
|
|
5528
|
+
};
|
|
5529
|
+
}
|
|
5530
|
+
};
|
|
5531
|
+
var DriftGate = class extends GovernanceGate {
|
|
5532
|
+
constructor(rules = []) {
|
|
5533
|
+
super();
|
|
5534
|
+
this.rules = rules;
|
|
5535
|
+
}
|
|
5536
|
+
rules;
|
|
5537
|
+
id = "drift";
|
|
5538
|
+
name = "Architectural Drift";
|
|
5539
|
+
dimension = "architecture";
|
|
5540
|
+
async check(input) {
|
|
5541
|
+
const start = Date.now();
|
|
5542
|
+
const findings = [];
|
|
5543
|
+
for (const rule of this.rules) {
|
|
5544
|
+
const results = rule.detect(input.output, input.metadata);
|
|
5545
|
+
findings.push(...results);
|
|
5546
|
+
}
|
|
5547
|
+
const hasErrors = findings.some((f) => f.severity === "error" || f.severity === "critical");
|
|
5548
|
+
const hasWarnings = findings.some((f) => f.severity === "warning");
|
|
5549
|
+
return {
|
|
5550
|
+
gateId: this.id,
|
|
5551
|
+
verdict: hasErrors ? "fail" : hasWarnings ? "warn" : "pass",
|
|
5552
|
+
findings,
|
|
5553
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5554
|
+
durationMs: Date.now() - start
|
|
5555
|
+
};
|
|
5556
|
+
}
|
|
5557
|
+
};
|
|
5558
|
+
var GateRunner = class {
|
|
5559
|
+
constructor(gates, hooks) {
|
|
5560
|
+
this.gates = gates;
|
|
5561
|
+
this.hooks = hooks;
|
|
5562
|
+
}
|
|
5563
|
+
gates;
|
|
5564
|
+
hooks;
|
|
5565
|
+
/**
|
|
5566
|
+
* Run all gates on the given input.
|
|
5567
|
+
* Gates run independently — one failure doesn't block others.
|
|
5568
|
+
*/
|
|
5569
|
+
async runAll(input) {
|
|
5570
|
+
const results = [];
|
|
5571
|
+
for (const gate of this.gates) {
|
|
5572
|
+
try {
|
|
5573
|
+
const result = await gate.check(input);
|
|
5574
|
+
results.push(result);
|
|
5575
|
+
await this.hooks.emit("governance.gate_completed", {
|
|
5576
|
+
gateId: gate.id,
|
|
5577
|
+
gateName: gate.name,
|
|
5578
|
+
verdict: result.verdict,
|
|
5579
|
+
findingCount: result.findings.length,
|
|
5580
|
+
agentId: input.agentId,
|
|
5581
|
+
taskId: input.taskId
|
|
5582
|
+
});
|
|
5583
|
+
} catch (err) {
|
|
5584
|
+
results.push({
|
|
5585
|
+
gateId: gate.id,
|
|
5586
|
+
verdict: "fail",
|
|
5587
|
+
findings: [{
|
|
5588
|
+
severity: "error",
|
|
5589
|
+
message: `Gate error: ${err instanceof Error ? err.message : String(err)}`
|
|
5590
|
+
}],
|
|
5591
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5592
|
+
durationMs: 0
|
|
5593
|
+
});
|
|
5594
|
+
}
|
|
5595
|
+
}
|
|
5596
|
+
const passed = results.every((r) => r.verdict !== "fail");
|
|
5597
|
+
await this.hooks.emit("governance.review_completed", {
|
|
5598
|
+
passed,
|
|
5599
|
+
agentId: input.agentId,
|
|
5600
|
+
taskId: input.taskId,
|
|
5601
|
+
results: results.map((r) => ({
|
|
5602
|
+
gateId: r.gateId,
|
|
5603
|
+
verdict: r.verdict,
|
|
5604
|
+
findingCount: r.findings.length
|
|
5605
|
+
}))
|
|
5606
|
+
});
|
|
5607
|
+
return { passed, results };
|
|
5608
|
+
}
|
|
5609
|
+
};
|
|
5610
|
+
|
|
4099
5611
|
// src/core/orchestrator/user-registry.ts
|
|
4100
5612
|
import { v4 as uuidv4 } from "uuid";
|
|
4101
5613
|
var UserRegistry = class {
|
|
@@ -4427,25 +5939,40 @@ export {
|
|
|
4427
5939
|
ApiExecutionAdapter,
|
|
4428
5940
|
AuditEmitter,
|
|
4429
5941
|
BackupManager,
|
|
5942
|
+
BreakerState,
|
|
4430
5943
|
BudgetController,
|
|
4431
5944
|
CORE_MIGRATIONS,
|
|
4432
5945
|
ChannelRegistry,
|
|
4433
5946
|
ChannelRegistryError,
|
|
5947
|
+
ChatResponder,
|
|
4434
5948
|
ChatSessionManager,
|
|
5949
|
+
CircuitBreaker,
|
|
4435
5950
|
CliExecutionAdapter,
|
|
4436
5951
|
ColumnValidatorImpl,
|
|
4437
5952
|
DEFAULTS,
|
|
4438
5953
|
DEFAULT_CONFIG,
|
|
4439
5954
|
DataStore,
|
|
4440
5955
|
DataStoreError,
|
|
5956
|
+
DeterministicAdapter,
|
|
5957
|
+
DriftGate,
|
|
4441
5958
|
EVENTS,
|
|
5959
|
+
GateRunner,
|
|
5960
|
+
GovernanceGate,
|
|
4442
5961
|
HookBus,
|
|
5962
|
+
LearningPipeline,
|
|
5963
|
+
LoopDetector,
|
|
5964
|
+
LoopType,
|
|
4443
5965
|
MAX_CHAIN_DEPTH,
|
|
5966
|
+
MessageInterpreter,
|
|
4444
5967
|
MessagePipeline,
|
|
5968
|
+
MessageStore,
|
|
4445
5969
|
ModelRouter,
|
|
4446
5970
|
NdjsonLogger,
|
|
4447
5971
|
NotificationQueue,
|
|
5972
|
+
PermissionRelay,
|
|
4448
5973
|
ProviderRegistry,
|
|
5974
|
+
QAGate,
|
|
5975
|
+
QualityGate,
|
|
4449
5976
|
RUN_STATUSES,
|
|
4450
5977
|
RunManager,
|
|
4451
5978
|
Scheduler,
|
|
@@ -4454,6 +5981,7 @@ export {
|
|
|
4454
5981
|
SessionManager,
|
|
4455
5982
|
TASK_STATUSES,
|
|
4456
5983
|
TaskQueue,
|
|
5984
|
+
TriageRouter,
|
|
4457
5985
|
UpdateChecker,
|
|
4458
5986
|
UpdateManager,
|
|
4459
5987
|
UserRegistry,
|