botinabox 1.4.2 → 1.6.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 +10 -6
- package/dist/index.d.ts +533 -1
- package/dist/index.js +1077 -2
- 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) {
|
|
@@ -1639,6 +1780,52 @@ function defineCoreTables(db) {
|
|
|
1639
1780
|
"CREATE UNIQUE INDEX IF NOT EXISTS idx_secrets_name_env ON secrets(name, environment, org_id) WHERE deleted_at IS NULL"
|
|
1640
1781
|
]
|
|
1641
1782
|
});
|
|
1783
|
+
db.define("feedback", {
|
|
1784
|
+
columns: {
|
|
1785
|
+
id: "TEXT PRIMARY KEY",
|
|
1786
|
+
agent_id: "TEXT NOT NULL",
|
|
1787
|
+
task_id: "TEXT",
|
|
1788
|
+
issue: "TEXT NOT NULL",
|
|
1789
|
+
root_cause: "TEXT",
|
|
1790
|
+
severity: "TEXT NOT NULL DEFAULT 'medium'",
|
|
1791
|
+
repeatable: "INTEGER NOT NULL DEFAULT 0",
|
|
1792
|
+
accuracy_score: "REAL",
|
|
1793
|
+
efficiency_score: "REAL",
|
|
1794
|
+
tags: "TEXT NOT NULL DEFAULT '[]'",
|
|
1795
|
+
created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
|
|
1796
|
+
},
|
|
1797
|
+
tableConstraints: [
|
|
1798
|
+
"CREATE INDEX IF NOT EXISTS idx_feedback_agent ON feedback(agent_id, created_at)",
|
|
1799
|
+
"CREATE INDEX IF NOT EXISTS idx_feedback_issue ON feedback(issue)"
|
|
1800
|
+
]
|
|
1801
|
+
});
|
|
1802
|
+
db.define("playbooks", {
|
|
1803
|
+
columns: {
|
|
1804
|
+
id: "TEXT PRIMARY KEY",
|
|
1805
|
+
pattern: "TEXT NOT NULL",
|
|
1806
|
+
rule: "TEXT NOT NULL",
|
|
1807
|
+
feedback_ids: "TEXT NOT NULL DEFAULT '[]'",
|
|
1808
|
+
project_scoped: "INTEGER NOT NULL DEFAULT 1",
|
|
1809
|
+
created_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
|
|
1810
|
+
updated_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
|
|
1811
|
+
deleted_at: "TEXT"
|
|
1812
|
+
},
|
|
1813
|
+
tableConstraints: [
|
|
1814
|
+
"CREATE INDEX IF NOT EXISTS idx_playbooks_pattern ON playbooks(pattern)"
|
|
1815
|
+
]
|
|
1816
|
+
});
|
|
1817
|
+
db.define("agent_playbooks", {
|
|
1818
|
+
columns: {
|
|
1819
|
+
agent_id: "TEXT NOT NULL",
|
|
1820
|
+
playbook_id: "TEXT NOT NULL",
|
|
1821
|
+
assigned_at: "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP"
|
|
1822
|
+
},
|
|
1823
|
+
primaryKey: ["agent_id", "playbook_id"],
|
|
1824
|
+
tableConstraints: [
|
|
1825
|
+
"FOREIGN KEY (agent_id) REFERENCES agents(id)",
|
|
1826
|
+
"FOREIGN KEY (playbook_id) REFERENCES playbooks(id)"
|
|
1827
|
+
]
|
|
1828
|
+
});
|
|
1642
1829
|
}
|
|
1643
1830
|
|
|
1644
1831
|
// src/core/data/core-migrations.ts
|
|
@@ -1704,12 +1891,55 @@ function defineCoreEntityContexts(db) {
|
|
|
1704
1891
|
`# ${a.name}`,
|
|
1705
1892
|
"",
|
|
1706
1893
|
a.role ? `**Role:** ${a.role}` : null,
|
|
1894
|
+
a.adapter ? `**Adapter:** ${a.adapter}` : null,
|
|
1707
1895
|
a.status ? `**Status:** ${a.status}` : null,
|
|
1708
1896
|
a.cwd ? `**Working Directory:** ${a.cwd}` : null,
|
|
1709
1897
|
a.reports_to ? `**Reports To:** ${a.reports_to}` : null,
|
|
1710
1898
|
""
|
|
1711
1899
|
].filter(Boolean).join("\n");
|
|
1712
1900
|
}
|
|
1901
|
+
},
|
|
1902
|
+
"SKILLS.md": {
|
|
1903
|
+
source: {
|
|
1904
|
+
type: "manyToMany",
|
|
1905
|
+
junctionTable: "agent_skills",
|
|
1906
|
+
localKey: "agent_id",
|
|
1907
|
+
remoteKey: "skill_id",
|
|
1908
|
+
remoteTable: "skills"
|
|
1909
|
+
},
|
|
1910
|
+
omitIfEmpty: true,
|
|
1911
|
+
render: (rows) => {
|
|
1912
|
+
if (!rows.length) return "";
|
|
1913
|
+
const lines = [`# Skills (${rows.length})`, ""];
|
|
1914
|
+
for (const s of rows) {
|
|
1915
|
+
lines.push(`## ${s.name}`);
|
|
1916
|
+
if (s.category) lines.push(`**Category:** ${s.category}`);
|
|
1917
|
+
if (s.description) lines.push("", s.description);
|
|
1918
|
+
if (s.definition) lines.push("", "```", s.definition, "```");
|
|
1919
|
+
lines.push("");
|
|
1920
|
+
}
|
|
1921
|
+
return lines.join("\n");
|
|
1922
|
+
}
|
|
1923
|
+
},
|
|
1924
|
+
"PLAYBOOKS.md": {
|
|
1925
|
+
source: {
|
|
1926
|
+
type: "manyToMany",
|
|
1927
|
+
junctionTable: "agent_playbooks",
|
|
1928
|
+
localKey: "agent_id",
|
|
1929
|
+
remoteKey: "playbook_id",
|
|
1930
|
+
remoteTable: "playbooks"
|
|
1931
|
+
},
|
|
1932
|
+
omitIfEmpty: true,
|
|
1933
|
+
render: (rows) => {
|
|
1934
|
+
if (!rows.length) return "";
|
|
1935
|
+
const lines = [`# Playbooks (${rows.length})`, ""];
|
|
1936
|
+
for (const pb of rows) {
|
|
1937
|
+
lines.push(`## ${pb.pattern ?? pb.name ?? "Unnamed"}`);
|
|
1938
|
+
if (pb.rule) lines.push("", pb.rule);
|
|
1939
|
+
lines.push("");
|
|
1940
|
+
}
|
|
1941
|
+
return lines.join("\n");
|
|
1942
|
+
}
|
|
1713
1943
|
}
|
|
1714
1944
|
}
|
|
1715
1945
|
});
|
|
@@ -2222,6 +2452,26 @@ ${r.rule_text}`
|
|
|
2222
2452
|
return `# Project Rules
|
|
2223
2453
|
|
|
2224
2454
|
${lines.join("\n\n")}
|
|
2455
|
+
`;
|
|
2456
|
+
},
|
|
2457
|
+
omitIfEmpty: true
|
|
2458
|
+
}
|
|
2459
|
+
} : {},
|
|
2460
|
+
...opts.files ? {
|
|
2461
|
+
"FILES.md": {
|
|
2462
|
+
source: {
|
|
2463
|
+
type: "hasMany",
|
|
2464
|
+
table: "file",
|
|
2465
|
+
foreignKey: "project_id"
|
|
2466
|
+
},
|
|
2467
|
+
render: (rows) => {
|
|
2468
|
+
if (!rows.length) return "";
|
|
2469
|
+
const lines = rows.map(
|
|
2470
|
+
(r) => `- [${r.name}](files/${r.name}/)${r.mime_type ? ` (${r.mime_type})` : ""}`
|
|
2471
|
+
);
|
|
2472
|
+
return `# Files
|
|
2473
|
+
|
|
2474
|
+
${lines.join("\n")}
|
|
2225
2475
|
`;
|
|
2226
2476
|
},
|
|
2227
2477
|
omitIfEmpty: true
|
|
@@ -2243,7 +2493,8 @@ ${lines.join("\n\n")}
|
|
|
2243
2493
|
const agent = r.from_agent ? ` [${r.from_agent}]` : "";
|
|
2244
2494
|
const body = r.body ?? "";
|
|
2245
2495
|
const preview = truncateAtWord(body, 150);
|
|
2246
|
-
|
|
2496
|
+
const link = `[${ts}](messages/${r.id}/)`;
|
|
2497
|
+
return `- ${dir} **${link}**${agent} ${preview}`;
|
|
2247
2498
|
});
|
|
2248
2499
|
return `# Messages
|
|
2249
2500
|
|
|
@@ -3076,6 +3327,13 @@ var RunManager = class {
|
|
|
3076
3327
|
// agentId → runId
|
|
3077
3328
|
orphanTimer = null;
|
|
3078
3329
|
staleThresholdMs;
|
|
3330
|
+
circuitBreaker;
|
|
3331
|
+
/**
|
|
3332
|
+
* Attach a CircuitBreaker to prevent retries on broken agents.
|
|
3333
|
+
*/
|
|
3334
|
+
setCircuitBreaker(cb) {
|
|
3335
|
+
this.circuitBreaker = cb;
|
|
3336
|
+
}
|
|
3079
3337
|
isLocked(agentId) {
|
|
3080
3338
|
return this.locks.has(agentId);
|
|
3081
3339
|
}
|
|
@@ -3113,11 +3371,15 @@ var RunManager = class {
|
|
|
3113
3371
|
this.locks.delete(agentId);
|
|
3114
3372
|
const taskId = run["task_id"];
|
|
3115
3373
|
if (!succeeded) {
|
|
3374
|
+
if (this.circuitBreaker) {
|
|
3375
|
+
await this.circuitBreaker.recordFailure(agentId, result.output);
|
|
3376
|
+
}
|
|
3116
3377
|
const task = await this.db.get("tasks", { id: taskId });
|
|
3117
3378
|
if (task) {
|
|
3118
3379
|
const retryCount = task["retry_count"] ?? 0;
|
|
3119
3380
|
const maxRetries = task["max_retries"] ?? 0;
|
|
3120
|
-
|
|
3381
|
+
const circuitOpen = this.circuitBreaker ? !this.circuitBreaker.canExecute(agentId) : false;
|
|
3382
|
+
if (retryCount < maxRetries && !circuitOpen) {
|
|
3121
3383
|
const maxBackoff = this.config?.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
|
|
3122
3384
|
const backoffMs = Math.min(BASE_BACKOFF_MS * Math.pow(2, retryCount), maxBackoff);
|
|
3123
3385
|
const nextRetryAt = new Date(Date.now() + backoffMs).toISOString();
|
|
@@ -3136,6 +3398,9 @@ var RunManager = class {
|
|
|
3136
3398
|
}
|
|
3137
3399
|
}
|
|
3138
3400
|
} else {
|
|
3401
|
+
if (this.circuitBreaker) {
|
|
3402
|
+
await this.circuitBreaker.recordSuccess(agentId);
|
|
3403
|
+
}
|
|
3139
3404
|
await this.db.update("tasks", { id: taskId }, {
|
|
3140
3405
|
status: "done",
|
|
3141
3406
|
result: result.output,
|
|
@@ -4075,6 +4340,803 @@ var CliExecutionAdapter = class {
|
|
|
4075
4340
|
}
|
|
4076
4341
|
};
|
|
4077
4342
|
|
|
4343
|
+
// src/core/orchestrator/adapters/deterministic-adapter.ts
|
|
4344
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
4345
|
+
var DeterministicAdapter = class {
|
|
4346
|
+
type = "deterministic";
|
|
4347
|
+
async execute(ctx) {
|
|
4348
|
+
const cwd = ctx.agent.cwd ?? process.cwd();
|
|
4349
|
+
let config = { command: "echo" };
|
|
4350
|
+
if (ctx.agent.adapter_config) {
|
|
4351
|
+
try {
|
|
4352
|
+
config = JSON.parse(ctx.agent.adapter_config);
|
|
4353
|
+
} catch {
|
|
4354
|
+
throw new Error("Invalid adapter_config for deterministic adapter");
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
if (!config.command) {
|
|
4358
|
+
throw new Error("Deterministic adapter requires a command");
|
|
4359
|
+
}
|
|
4360
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
4361
|
+
const payload = JSON.stringify({
|
|
4362
|
+
taskId: ctx.task.title,
|
|
4363
|
+
description: ctx.task.description ?? "",
|
|
4364
|
+
context: ctx.task.context ?? ""
|
|
4365
|
+
});
|
|
4366
|
+
const args = [...config.args ?? []];
|
|
4367
|
+
if (config.inputMode === "arg") {
|
|
4368
|
+
args.push(payload);
|
|
4369
|
+
}
|
|
4370
|
+
const child = spawnProcess(config.command, args, {
|
|
4371
|
+
cwd,
|
|
4372
|
+
extraEnv: config.env
|
|
4373
|
+
});
|
|
4374
|
+
if (config.inputMode !== "arg" && child.stdin) {
|
|
4375
|
+
child.stdin.write(payload);
|
|
4376
|
+
child.stdin.end();
|
|
4377
|
+
}
|
|
4378
|
+
const stdoutChunks = [];
|
|
4379
|
+
child.stdout?.on("data", (chunk) => {
|
|
4380
|
+
stdoutChunks.push(chunk);
|
|
4381
|
+
ctx.onLog?.("stdout", chunk.toString("utf8"));
|
|
4382
|
+
});
|
|
4383
|
+
child.stderr?.on("data", (chunk) => {
|
|
4384
|
+
ctx.onLog?.("stderr", chunk.toString("utf8"));
|
|
4385
|
+
});
|
|
4386
|
+
const abortHandler = () => {
|
|
4387
|
+
if (child.pid != null) killProcessGroup(child.pid);
|
|
4388
|
+
};
|
|
4389
|
+
ctx.abortSignal?.addEventListener("abort", abortHandler);
|
|
4390
|
+
const timeout = setTimeout(() => {
|
|
4391
|
+
if (child.pid != null) killProcessGroup(child.pid);
|
|
4392
|
+
}, timeoutMs);
|
|
4393
|
+
const exitCode = await new Promise((resolve) => {
|
|
4394
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
4395
|
+
child.on("error", () => resolve(1));
|
|
4396
|
+
});
|
|
4397
|
+
clearTimeout(timeout);
|
|
4398
|
+
ctx.abortSignal?.removeEventListener("abort", abortHandler);
|
|
4399
|
+
const output = Buffer.concat(stdoutChunks).toString("utf8");
|
|
4400
|
+
return { output, exitCode };
|
|
4401
|
+
}
|
|
4402
|
+
};
|
|
4403
|
+
|
|
4404
|
+
// src/core/orchestrator/loop-detector.ts
|
|
4405
|
+
var LoopType = /* @__PURE__ */ ((LoopType2) => {
|
|
4406
|
+
LoopType2["SELF_LOOP"] = "self_loop";
|
|
4407
|
+
LoopType2["PING_PONG"] = "ping_pong";
|
|
4408
|
+
LoopType2["BLOCKED_REENTRY"] = "blocked_reentry";
|
|
4409
|
+
return LoopType2;
|
|
4410
|
+
})(LoopType || {});
|
|
4411
|
+
var DEFAULT_WINDOW = 10;
|
|
4412
|
+
var DEFAULT_PING_PONG_THRESHOLD = 2;
|
|
4413
|
+
var LoopDetector = class {
|
|
4414
|
+
constructor(db, config) {
|
|
4415
|
+
this.db = db;
|
|
4416
|
+
this.windowSize = config?.windowSize ?? DEFAULT_WINDOW;
|
|
4417
|
+
this.pingPongThreshold = config?.pingPongThreshold ?? DEFAULT_PING_PONG_THRESHOLD;
|
|
4418
|
+
}
|
|
4419
|
+
db;
|
|
4420
|
+
windowSize;
|
|
4421
|
+
pingPongThreshold;
|
|
4422
|
+
/**
|
|
4423
|
+
* Check for loops before creating a followup task.
|
|
4424
|
+
* Returns a LoopDetection if a loop pattern is found, undefined otherwise.
|
|
4425
|
+
*/
|
|
4426
|
+
async check(sourceAgentId, targetAgentId, taskId, chainOriginId) {
|
|
4427
|
+
const selfLoop = this.checkSelfLoop(sourceAgentId, targetAgentId, taskId);
|
|
4428
|
+
if (selfLoop) return selfLoop;
|
|
4429
|
+
const blocked = await this.checkBlockedReentry(targetAgentId, taskId, chainOriginId);
|
|
4430
|
+
if (blocked) return blocked;
|
|
4431
|
+
const pingPong = await this.checkPingPong(sourceAgentId, targetAgentId, chainOriginId);
|
|
4432
|
+
if (pingPong) return pingPong;
|
|
4433
|
+
return void 0;
|
|
4434
|
+
}
|
|
4435
|
+
/**
|
|
4436
|
+
* Check if an agent is routing to itself.
|
|
4437
|
+
*/
|
|
4438
|
+
checkSelfLoop(sourceAgentId, targetAgentId, taskId) {
|
|
4439
|
+
if (sourceAgentId === targetAgentId) {
|
|
4440
|
+
return {
|
|
4441
|
+
type: "self_loop" /* SELF_LOOP */,
|
|
4442
|
+
agents: [sourceAgentId],
|
|
4443
|
+
taskId,
|
|
4444
|
+
message: `Self-loop detected: agent ${sourceAgentId} is routing to itself`
|
|
4445
|
+
};
|
|
4446
|
+
}
|
|
4447
|
+
return void 0;
|
|
4448
|
+
}
|
|
4449
|
+
/**
|
|
4450
|
+
* Check if a previously blocked task is being re-entered.
|
|
4451
|
+
*/
|
|
4452
|
+
async checkBlockedReentry(targetAgentId, taskId, chainOriginId) {
|
|
4453
|
+
const originId = chainOriginId ?? taskId;
|
|
4454
|
+
const chainTasks = await this.db.query("tasks", {
|
|
4455
|
+
where: { chain_origin_id: originId }
|
|
4456
|
+
});
|
|
4457
|
+
const blockedInChain = chainTasks.filter(
|
|
4458
|
+
(t) => (t["status"] === "blocked" || t["status"] === "failed") && t["assignee_id"] === targetAgentId
|
|
4459
|
+
);
|
|
4460
|
+
if (blockedInChain.length > 0) {
|
|
4461
|
+
return {
|
|
4462
|
+
type: "blocked_reentry" /* BLOCKED_REENTRY */,
|
|
4463
|
+
agents: [targetAgentId],
|
|
4464
|
+
taskId,
|
|
4465
|
+
chainOriginId: originId,
|
|
4466
|
+
message: `Blocked re-entry: agent ${targetAgentId} already has a blocked/failed task in chain ${originId}`
|
|
4467
|
+
};
|
|
4468
|
+
}
|
|
4469
|
+
return void 0;
|
|
4470
|
+
}
|
|
4471
|
+
/**
|
|
4472
|
+
* Check for A→B→A→B ping-pong by scanning recent tasks in the chain.
|
|
4473
|
+
*/
|
|
4474
|
+
async checkPingPong(sourceAgentId, targetAgentId, chainOriginId) {
|
|
4475
|
+
if (!chainOriginId) return void 0;
|
|
4476
|
+
const chainTasks = await this.db.query("tasks", {
|
|
4477
|
+
where: { chain_origin_id: chainOriginId }
|
|
4478
|
+
});
|
|
4479
|
+
const sorted = chainTasks.sort((a, b) => {
|
|
4480
|
+
const depthDiff = (a["chain_depth"] ?? 0) - (b["chain_depth"] ?? 0);
|
|
4481
|
+
if (depthDiff !== 0) return depthDiff;
|
|
4482
|
+
return (a["created_at"] ?? "").localeCompare(b["created_at"] ?? "");
|
|
4483
|
+
}).slice(-this.windowSize);
|
|
4484
|
+
const agentSequence = sorted.map((t) => t["assignee_id"]).filter(Boolean);
|
|
4485
|
+
agentSequence.push(targetAgentId);
|
|
4486
|
+
if (agentSequence.length >= this.pingPongThreshold * 2) {
|
|
4487
|
+
const tail = agentSequence.slice(-this.pingPongThreshold * 2);
|
|
4488
|
+
const a = tail[0];
|
|
4489
|
+
const b = tail[1];
|
|
4490
|
+
if (a && b && a !== b) {
|
|
4491
|
+
let isPingPong = true;
|
|
4492
|
+
for (let i = 0; i < tail.length; i++) {
|
|
4493
|
+
if (tail[i] !== (i % 2 === 0 ? a : b)) {
|
|
4494
|
+
isPingPong = false;
|
|
4495
|
+
break;
|
|
4496
|
+
}
|
|
4497
|
+
}
|
|
4498
|
+
if (isPingPong) {
|
|
4499
|
+
return {
|
|
4500
|
+
type: "ping_pong" /* PING_PONG */,
|
|
4501
|
+
agents: [a, b],
|
|
4502
|
+
taskId: sorted[sorted.length - 1]?.["id"] ?? "",
|
|
4503
|
+
chainOriginId,
|
|
4504
|
+
message: `Ping-pong detected: agents ${a} and ${b} are bouncing tasks in chain ${chainOriginId}`
|
|
4505
|
+
};
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4509
|
+
return void 0;
|
|
4510
|
+
}
|
|
4511
|
+
};
|
|
4512
|
+
|
|
4513
|
+
// src/core/orchestrator/circuit-breaker.ts
|
|
4514
|
+
var BreakerState = /* @__PURE__ */ ((BreakerState2) => {
|
|
4515
|
+
BreakerState2["CLOSED"] = "closed";
|
|
4516
|
+
BreakerState2["OPEN"] = "open";
|
|
4517
|
+
BreakerState2["HALF_OPEN"] = "half_open";
|
|
4518
|
+
return BreakerState2;
|
|
4519
|
+
})(BreakerState || {});
|
|
4520
|
+
var DEFAULT_FAILURE_THRESHOLD = 3;
|
|
4521
|
+
var DEFAULT_RESET_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
4522
|
+
var CircuitBreaker = class {
|
|
4523
|
+
constructor(db, hooks, config) {
|
|
4524
|
+
this.db = db;
|
|
4525
|
+
this.hooks = hooks;
|
|
4526
|
+
this.failureThreshold = config?.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD;
|
|
4527
|
+
this.resetTimeoutMs = config?.resetTimeoutMs ?? DEFAULT_RESET_TIMEOUT_MS;
|
|
4528
|
+
this.persist = config?.persist ?? true;
|
|
4529
|
+
}
|
|
4530
|
+
db;
|
|
4531
|
+
hooks;
|
|
4532
|
+
breakers = /* @__PURE__ */ new Map();
|
|
4533
|
+
failureThreshold;
|
|
4534
|
+
resetTimeoutMs;
|
|
4535
|
+
persist;
|
|
4536
|
+
/**
|
|
4537
|
+
* Check if an agent is allowed to execute.
|
|
4538
|
+
* Returns true if execution is allowed, false if circuit is open.
|
|
4539
|
+
*/
|
|
4540
|
+
canExecute(agentId) {
|
|
4541
|
+
const breaker = this.breakers.get(agentId);
|
|
4542
|
+
if (!breaker) return true;
|
|
4543
|
+
switch (breaker.state) {
|
|
4544
|
+
case "closed" /* CLOSED */:
|
|
4545
|
+
return true;
|
|
4546
|
+
case "open" /* OPEN */: {
|
|
4547
|
+
const elapsed = Date.now() - (breaker.trippedAt ?? 0);
|
|
4548
|
+
if (elapsed >= this.resetTimeoutMs) {
|
|
4549
|
+
breaker.state = "half_open" /* HALF_OPEN */;
|
|
4550
|
+
return true;
|
|
4551
|
+
}
|
|
4552
|
+
return false;
|
|
4553
|
+
}
|
|
4554
|
+
case "half_open" /* HALF_OPEN */:
|
|
4555
|
+
return true;
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
/**
|
|
4559
|
+
* Record a successful execution. Resets the breaker to CLOSED.
|
|
4560
|
+
*/
|
|
4561
|
+
async recordSuccess(agentId) {
|
|
4562
|
+
const breaker = this.breakers.get(agentId);
|
|
4563
|
+
if (!breaker) return;
|
|
4564
|
+
const previousState = breaker.state;
|
|
4565
|
+
breaker.state = "closed" /* CLOSED */;
|
|
4566
|
+
breaker.failureCount = 0;
|
|
4567
|
+
if (previousState === "half_open" /* HALF_OPEN */) {
|
|
4568
|
+
await this.logEvent(agentId, "circuit_recovered", {
|
|
4569
|
+
previousState
|
|
4570
|
+
});
|
|
4571
|
+
await this.hooks.emit("circuit_breaker.recovered", {
|
|
4572
|
+
agentId,
|
|
4573
|
+
previousState
|
|
4574
|
+
});
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
/**
|
|
4578
|
+
* Record a failed execution. Increments failure count and may trip breaker.
|
|
4579
|
+
*/
|
|
4580
|
+
async recordFailure(agentId, reason) {
|
|
4581
|
+
let breaker = this.breakers.get(agentId);
|
|
4582
|
+
if (!breaker) {
|
|
4583
|
+
breaker = {
|
|
4584
|
+
state: "closed" /* CLOSED */,
|
|
4585
|
+
failureCount: 0,
|
|
4586
|
+
lastFailureAt: Date.now()
|
|
4587
|
+
};
|
|
4588
|
+
this.breakers.set(agentId, breaker);
|
|
4589
|
+
}
|
|
4590
|
+
breaker.failureCount++;
|
|
4591
|
+
breaker.lastFailureAt = Date.now();
|
|
4592
|
+
if (breaker.state === "half_open" /* HALF_OPEN */) {
|
|
4593
|
+
await this.trip(agentId, reason ?? "Probe execution failed during half-open state");
|
|
4594
|
+
return;
|
|
4595
|
+
}
|
|
4596
|
+
if (breaker.failureCount >= this.failureThreshold) {
|
|
4597
|
+
await this.trip(agentId, reason ?? `Failure threshold reached (${this.failureThreshold})`);
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4600
|
+
/**
|
|
4601
|
+
* Trip the breaker to OPEN state and escalate to human.
|
|
4602
|
+
*/
|
|
4603
|
+
async trip(agentId, reason) {
|
|
4604
|
+
let breaker = this.breakers.get(agentId);
|
|
4605
|
+
if (!breaker) {
|
|
4606
|
+
breaker = {
|
|
4607
|
+
state: "closed" /* CLOSED */,
|
|
4608
|
+
failureCount: 0,
|
|
4609
|
+
lastFailureAt: Date.now()
|
|
4610
|
+
};
|
|
4611
|
+
this.breakers.set(agentId, breaker);
|
|
4612
|
+
}
|
|
4613
|
+
breaker.state = "open" /* OPEN */;
|
|
4614
|
+
breaker.trippedAt = Date.now();
|
|
4615
|
+
await this.logEvent(agentId, "circuit_tripped", {
|
|
4616
|
+
reason,
|
|
4617
|
+
failureCount: breaker.failureCount
|
|
4618
|
+
});
|
|
4619
|
+
await this.hooks.emit("circuit_breaker.tripped", {
|
|
4620
|
+
agentId,
|
|
4621
|
+
reason,
|
|
4622
|
+
failureCount: breaker.failureCount,
|
|
4623
|
+
action: "escalate_to_human"
|
|
4624
|
+
});
|
|
4625
|
+
}
|
|
4626
|
+
/**
|
|
4627
|
+
* Manually reset a breaker (e.g. after human review).
|
|
4628
|
+
*/
|
|
4629
|
+
async reset(agentId) {
|
|
4630
|
+
this.breakers.delete(agentId);
|
|
4631
|
+
await this.logEvent(agentId, "circuit_reset", {});
|
|
4632
|
+
await this.hooks.emit("circuit_breaker.reset", { agentId });
|
|
4633
|
+
}
|
|
4634
|
+
/**
|
|
4635
|
+
* Get the current state of a breaker.
|
|
4636
|
+
*/
|
|
4637
|
+
getState(agentId) {
|
|
4638
|
+
return this.breakers.get(agentId)?.state ?? "closed" /* CLOSED */;
|
|
4639
|
+
}
|
|
4640
|
+
/**
|
|
4641
|
+
* Get failure count for an agent.
|
|
4642
|
+
*/
|
|
4643
|
+
getFailureCount(agentId) {
|
|
4644
|
+
return this.breakers.get(agentId)?.failureCount ?? 0;
|
|
4645
|
+
}
|
|
4646
|
+
async logEvent(agentId, eventType, payload) {
|
|
4647
|
+
if (!this.persist) return;
|
|
4648
|
+
await this.db.insert("activity_log", {
|
|
4649
|
+
agent_id: agentId,
|
|
4650
|
+
event_type: eventType,
|
|
4651
|
+
payload: JSON.stringify(payload)
|
|
4652
|
+
});
|
|
4653
|
+
}
|
|
4654
|
+
};
|
|
4655
|
+
|
|
4656
|
+
// src/core/orchestrator/learning-pipeline.ts
|
|
4657
|
+
var DEFAULT_PLAYBOOK_THRESHOLD = 3;
|
|
4658
|
+
var DEFAULT_SKILL_THRESHOLD = 3;
|
|
4659
|
+
var LearningPipeline = class {
|
|
4660
|
+
constructor(db, hooks, config) {
|
|
4661
|
+
this.db = db;
|
|
4662
|
+
this.hooks = hooks;
|
|
4663
|
+
this.playbookThreshold = config?.playbookThreshold ?? DEFAULT_PLAYBOOK_THRESHOLD;
|
|
4664
|
+
this.skillThreshold = config?.skillThreshold ?? DEFAULT_SKILL_THRESHOLD;
|
|
4665
|
+
this.autoPromote = config?.autoPromote ?? false;
|
|
4666
|
+
}
|
|
4667
|
+
db;
|
|
4668
|
+
hooks;
|
|
4669
|
+
playbookThreshold;
|
|
4670
|
+
skillThreshold;
|
|
4671
|
+
autoPromote;
|
|
4672
|
+
// --- Feedback Layer ---
|
|
4673
|
+
/**
|
|
4674
|
+
* Capture a structured feedback record from an execution.
|
|
4675
|
+
*/
|
|
4676
|
+
async captureFeedback(entry) {
|
|
4677
|
+
const row = await this.db.insert("feedback", {
|
|
4678
|
+
agent_id: entry.agentId,
|
|
4679
|
+
task_id: entry.taskId,
|
|
4680
|
+
issue: entry.issue,
|
|
4681
|
+
root_cause: entry.rootCause,
|
|
4682
|
+
severity: entry.severity,
|
|
4683
|
+
repeatable: entry.repeatable ? 1 : 0,
|
|
4684
|
+
accuracy_score: entry.accuracyScore,
|
|
4685
|
+
efficiency_score: entry.efficiencyScore,
|
|
4686
|
+
tags: JSON.stringify(entry.tags ?? [])
|
|
4687
|
+
});
|
|
4688
|
+
const feedbackId = row["id"];
|
|
4689
|
+
await this.hooks.emit("learning.feedback_captured", {
|
|
4690
|
+
feedbackId,
|
|
4691
|
+
agentId: entry.agentId,
|
|
4692
|
+
issue: entry.issue,
|
|
4693
|
+
severity: entry.severity
|
|
4694
|
+
});
|
|
4695
|
+
if (this.autoPromote) {
|
|
4696
|
+
await this.checkPlaybookPromotion(entry.issue);
|
|
4697
|
+
}
|
|
4698
|
+
return feedbackId;
|
|
4699
|
+
}
|
|
4700
|
+
/**
|
|
4701
|
+
* Get all feedback records, optionally filtered.
|
|
4702
|
+
*/
|
|
4703
|
+
async listFeedback(filter) {
|
|
4704
|
+
const where = {};
|
|
4705
|
+
if (filter?.agentId) where["agent_id"] = filter.agentId;
|
|
4706
|
+
if (filter?.severity) where["severity"] = filter.severity;
|
|
4707
|
+
if (filter?.repeatable !== void 0) where["repeatable"] = filter.repeatable ? 1 : 0;
|
|
4708
|
+
return this.db.query("feedback", Object.keys(where).length ? { where } : void 0);
|
|
4709
|
+
}
|
|
4710
|
+
// --- Playbook Layer ---
|
|
4711
|
+
/**
|
|
4712
|
+
* Check if feedback records with similar issues should be promoted to a playbook.
|
|
4713
|
+
* Groups by issue text similarity (exact match for now).
|
|
4714
|
+
*/
|
|
4715
|
+
async checkPlaybookPromotion(issue) {
|
|
4716
|
+
const allFeedback = await this.db.query("feedback", {
|
|
4717
|
+
where: { issue }
|
|
4718
|
+
});
|
|
4719
|
+
if (allFeedback.length < this.playbookThreshold) {
|
|
4720
|
+
return void 0;
|
|
4721
|
+
}
|
|
4722
|
+
const existingPlaybooks = await this.db.query("playbooks", {
|
|
4723
|
+
where: { pattern: issue }
|
|
4724
|
+
});
|
|
4725
|
+
if (existingPlaybooks.length > 0) {
|
|
4726
|
+
return existingPlaybooks[0]["id"];
|
|
4727
|
+
}
|
|
4728
|
+
const feedbackIds = allFeedback.map((f) => f["id"]);
|
|
4729
|
+
const rootCauses = allFeedback.map((f) => f["root_cause"]).filter(Boolean);
|
|
4730
|
+
const rule = rootCauses.length > 0 ? `When encountering "${issue}": ${rootCauses[0]}` : `Pattern detected: "${issue}" \u2014 review and add specific guidance.`;
|
|
4731
|
+
const playbookId = await this.promoteToPlaybook({
|
|
4732
|
+
pattern: issue,
|
|
4733
|
+
rule,
|
|
4734
|
+
feedbackIds,
|
|
4735
|
+
projectScoped: true
|
|
4736
|
+
});
|
|
4737
|
+
return playbookId;
|
|
4738
|
+
}
|
|
4739
|
+
/**
|
|
4740
|
+
* Manually create a playbook from a set of feedback records.
|
|
4741
|
+
*/
|
|
4742
|
+
async promoteToPlaybook(entry) {
|
|
4743
|
+
const row = await this.db.insert("playbooks", {
|
|
4744
|
+
pattern: entry.pattern,
|
|
4745
|
+
rule: entry.rule,
|
|
4746
|
+
feedback_ids: JSON.stringify(entry.feedbackIds),
|
|
4747
|
+
project_scoped: entry.projectScoped ? 1 : 0
|
|
4748
|
+
});
|
|
4749
|
+
const playbookId = row["id"];
|
|
4750
|
+
if (entry.agentIds) {
|
|
4751
|
+
for (const agentId of entry.agentIds) {
|
|
4752
|
+
await this.db.insert("agent_playbooks", {
|
|
4753
|
+
agent_id: agentId,
|
|
4754
|
+
playbook_id: playbookId
|
|
4755
|
+
});
|
|
4756
|
+
}
|
|
4757
|
+
}
|
|
4758
|
+
await this.hooks.emit("learning.playbook_promoted", {
|
|
4759
|
+
playbookId,
|
|
4760
|
+
pattern: entry.pattern,
|
|
4761
|
+
feedbackCount: entry.feedbackIds.length
|
|
4762
|
+
});
|
|
4763
|
+
return playbookId;
|
|
4764
|
+
}
|
|
4765
|
+
/**
|
|
4766
|
+
* List playbooks, optionally filtered.
|
|
4767
|
+
*/
|
|
4768
|
+
async listPlaybooks(filter) {
|
|
4769
|
+
const where = {};
|
|
4770
|
+
if (filter?.projectScoped !== void 0) {
|
|
4771
|
+
where["project_scoped"] = filter.projectScoped ? 1 : 0;
|
|
4772
|
+
}
|
|
4773
|
+
return this.db.query("playbooks", Object.keys(where).length ? { where } : void 0);
|
|
4774
|
+
}
|
|
4775
|
+
// --- Skill Layer ---
|
|
4776
|
+
/**
|
|
4777
|
+
* Check if a playbook should be promoted to a skill.
|
|
4778
|
+
* A playbook becomes a skill when it works across multiple projects
|
|
4779
|
+
* (indicated by being referenced by agents in different contexts).
|
|
4780
|
+
*/
|
|
4781
|
+
async checkSkillPromotion(playbookId) {
|
|
4782
|
+
const playbook = await this.db.get("playbooks", { id: playbookId });
|
|
4783
|
+
if (!playbook) return void 0;
|
|
4784
|
+
const links = await this.db.query("agent_playbooks", {
|
|
4785
|
+
where: { playbook_id: playbookId }
|
|
4786
|
+
});
|
|
4787
|
+
if (links.length < this.skillThreshold) {
|
|
4788
|
+
return void 0;
|
|
4789
|
+
}
|
|
4790
|
+
const pattern = playbook["pattern"];
|
|
4791
|
+
const existingSkills = await this.db.query("skills", {
|
|
4792
|
+
where: { name: pattern }
|
|
4793
|
+
});
|
|
4794
|
+
if (existingSkills.length > 0) {
|
|
4795
|
+
return existingSkills[0]["id"];
|
|
4796
|
+
}
|
|
4797
|
+
const slug = pattern.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 64);
|
|
4798
|
+
const skillId = await this.promoteToSkill({
|
|
4799
|
+
name: pattern,
|
|
4800
|
+
slug,
|
|
4801
|
+
description: `Auto-promoted from playbook: ${pattern}`,
|
|
4802
|
+
behavior: playbook["rule"],
|
|
4803
|
+
sourcePlaybookIds: [playbookId]
|
|
4804
|
+
});
|
|
4805
|
+
return skillId;
|
|
4806
|
+
}
|
|
4807
|
+
/**
|
|
4808
|
+
* Manually promote a playbook to a reusable skill.
|
|
4809
|
+
*/
|
|
4810
|
+
async promoteToSkill(entry) {
|
|
4811
|
+
const row = await this.db.insert("skills", {
|
|
4812
|
+
name: entry.name,
|
|
4813
|
+
slug: entry.slug,
|
|
4814
|
+
description: entry.description,
|
|
4815
|
+
category: entry.category ?? "learned",
|
|
4816
|
+
definition: JSON.stringify({
|
|
4817
|
+
behavior: entry.behavior,
|
|
4818
|
+
source_playbook_ids: entry.sourcePlaybookIds
|
|
4819
|
+
})
|
|
4820
|
+
});
|
|
4821
|
+
const skillId = row["id"];
|
|
4822
|
+
await this.hooks.emit("learning.skill_promoted", {
|
|
4823
|
+
skillId,
|
|
4824
|
+
name: entry.name,
|
|
4825
|
+
slug: entry.slug,
|
|
4826
|
+
sourcePlaybookCount: entry.sourcePlaybookIds.length
|
|
4827
|
+
});
|
|
4828
|
+
return skillId;
|
|
4829
|
+
}
|
|
4830
|
+
/**
|
|
4831
|
+
* Assign a skill to an agent.
|
|
4832
|
+
*/
|
|
4833
|
+
async assignSkill(agentId, skillId) {
|
|
4834
|
+
await this.db.link("agent_skills", {
|
|
4835
|
+
agent_id: agentId,
|
|
4836
|
+
skill_id: skillId
|
|
4837
|
+
});
|
|
4838
|
+
await this.hooks.emit("learning.skill_assigned", { agentId, skillId });
|
|
4839
|
+
}
|
|
4840
|
+
/**
|
|
4841
|
+
* Get learning metrics for an agent.
|
|
4842
|
+
*/
|
|
4843
|
+
async getMetrics(agentId) {
|
|
4844
|
+
const feedback = await this.db.query("feedback", { where: { agent_id: agentId } });
|
|
4845
|
+
const accuracyScores = feedback.map((f) => f["accuracy_score"]).filter((s) => s !== null && s !== void 0);
|
|
4846
|
+
const efficiencyScores = feedback.map((f) => f["efficiency_score"]).filter((s) => s !== null && s !== void 0);
|
|
4847
|
+
let playbookCount = 0;
|
|
4848
|
+
try {
|
|
4849
|
+
const links = await this.db.query("agent_playbooks", { where: { agent_id: agentId } });
|
|
4850
|
+
playbookCount = links.length;
|
|
4851
|
+
} catch {
|
|
4852
|
+
}
|
|
4853
|
+
const skillLinks = await this.db.query("agent_skills", { where: { agent_id: agentId } });
|
|
4854
|
+
return {
|
|
4855
|
+
feedbackCount: feedback.length,
|
|
4856
|
+
avgAccuracy: accuracyScores.length > 0 ? accuracyScores.reduce((a, b) => a + b, 0) / accuracyScores.length : null,
|
|
4857
|
+
avgEfficiency: efficiencyScores.length > 0 ? efficiencyScores.reduce((a, b) => a + b, 0) / efficiencyScores.length : null,
|
|
4858
|
+
playbookCount,
|
|
4859
|
+
skillCount: skillLinks.length
|
|
4860
|
+
};
|
|
4861
|
+
}
|
|
4862
|
+
};
|
|
4863
|
+
|
|
4864
|
+
// src/core/orchestrator/permission-relay.ts
|
|
4865
|
+
var DEFAULT_POLL_INTERVAL_MS = 5e3;
|
|
4866
|
+
var DEFAULT_TIMEOUT_MS2 = 5 * 60 * 1e3;
|
|
4867
|
+
var PermissionRelay = class {
|
|
4868
|
+
constructor(hooks, config) {
|
|
4869
|
+
this.hooks = hooks;
|
|
4870
|
+
this.providers = config.providers;
|
|
4871
|
+
this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
4872
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
4873
|
+
}
|
|
4874
|
+
hooks;
|
|
4875
|
+
providers;
|
|
4876
|
+
pollIntervalMs;
|
|
4877
|
+
timeoutMs;
|
|
4878
|
+
pending = /* @__PURE__ */ new Map();
|
|
4879
|
+
/**
|
|
4880
|
+
* Request approval from all configured providers.
|
|
4881
|
+
* Returns when the first provider responds (approve or deny).
|
|
4882
|
+
*/
|
|
4883
|
+
async requestApproval(prompt) {
|
|
4884
|
+
const expiresAt = new Date(Date.now() + this.timeoutMs).toISOString();
|
|
4885
|
+
const promptWithExpiry = { ...prompt, expiresAt };
|
|
4886
|
+
await this.hooks.emit("permission.requested", {
|
|
4887
|
+
promptId: prompt.id,
|
|
4888
|
+
agentId: prompt.agentId,
|
|
4889
|
+
action: prompt.action
|
|
4890
|
+
});
|
|
4891
|
+
const handles = /* @__PURE__ */ new Map();
|
|
4892
|
+
for (const provider of this.providers) {
|
|
4893
|
+
try {
|
|
4894
|
+
const handle = await provider.sendPrompt(promptWithExpiry);
|
|
4895
|
+
handles.set(provider.id, handle);
|
|
4896
|
+
} catch {
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4899
|
+
if (handles.size === 0) {
|
|
4900
|
+
throw new Error("No permission providers available");
|
|
4901
|
+
}
|
|
4902
|
+
return new Promise((resolve, reject) => {
|
|
4903
|
+
const entry = {
|
|
4904
|
+
prompt: promptWithExpiry,
|
|
4905
|
+
handles,
|
|
4906
|
+
resolve,
|
|
4907
|
+
reject
|
|
4908
|
+
};
|
|
4909
|
+
this.pending.set(prompt.id, entry);
|
|
4910
|
+
const pollTimer = setInterval(async () => {
|
|
4911
|
+
for (const [providerId, handle] of handles) {
|
|
4912
|
+
const provider = this.providers.find((p) => p.id === providerId);
|
|
4913
|
+
if (!provider) continue;
|
|
4914
|
+
try {
|
|
4915
|
+
const response = await provider.pollResponse(handle);
|
|
4916
|
+
if (response) {
|
|
4917
|
+
clearInterval(pollTimer);
|
|
4918
|
+
clearTimeout(timeoutTimer);
|
|
4919
|
+
this.pending.delete(prompt.id);
|
|
4920
|
+
await this.cancelOtherProviders(handles, providerId);
|
|
4921
|
+
await this.hooks.emit("permission.responded", {
|
|
4922
|
+
promptId: prompt.id,
|
|
4923
|
+
status: response.status,
|
|
4924
|
+
respondedBy: response.respondedBy
|
|
4925
|
+
});
|
|
4926
|
+
resolve(response);
|
|
4927
|
+
return;
|
|
4928
|
+
}
|
|
4929
|
+
} catch {
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
}, this.pollIntervalMs);
|
|
4933
|
+
const timeoutTimer = setTimeout(async () => {
|
|
4934
|
+
clearInterval(pollTimer);
|
|
4935
|
+
this.pending.delete(prompt.id);
|
|
4936
|
+
for (const [providerId, handle] of handles) {
|
|
4937
|
+
const provider = this.providers.find((p) => p.id === providerId);
|
|
4938
|
+
if (provider) {
|
|
4939
|
+
try {
|
|
4940
|
+
await provider.cancelPrompt(handle);
|
|
4941
|
+
} catch {
|
|
4942
|
+
}
|
|
4943
|
+
}
|
|
4944
|
+
}
|
|
4945
|
+
await this.hooks.emit("permission.expired", {
|
|
4946
|
+
promptId: prompt.id,
|
|
4947
|
+
agentId: prompt.agentId
|
|
4948
|
+
});
|
|
4949
|
+
reject(new Error(`Permission request expired after ${this.timeoutMs}ms`));
|
|
4950
|
+
}, this.timeoutMs);
|
|
4951
|
+
});
|
|
4952
|
+
}
|
|
4953
|
+
/**
|
|
4954
|
+
* Provide a local approval (from terminal).
|
|
4955
|
+
* Resolves the pending request and cancels remote providers.
|
|
4956
|
+
*/
|
|
4957
|
+
async approveLocally(promptId, approved) {
|
|
4958
|
+
const entry = this.pending.get(promptId);
|
|
4959
|
+
if (!entry) return;
|
|
4960
|
+
const response = {
|
|
4961
|
+
promptId,
|
|
4962
|
+
status: approved ? "approved" : "denied",
|
|
4963
|
+
respondedBy: "local",
|
|
4964
|
+
respondedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4965
|
+
};
|
|
4966
|
+
this.pending.delete(promptId);
|
|
4967
|
+
for (const [providerId, handle] of entry.handles) {
|
|
4968
|
+
const provider = this.providers.find((p) => p.id === providerId);
|
|
4969
|
+
if (provider) {
|
|
4970
|
+
try {
|
|
4971
|
+
await provider.cancelPrompt(handle);
|
|
4972
|
+
} catch {
|
|
4973
|
+
}
|
|
4974
|
+
}
|
|
4975
|
+
}
|
|
4976
|
+
await this.hooks.emit("permission.responded", {
|
|
4977
|
+
promptId,
|
|
4978
|
+
status: response.status,
|
|
4979
|
+
respondedBy: "local"
|
|
4980
|
+
});
|
|
4981
|
+
entry.resolve(response);
|
|
4982
|
+
}
|
|
4983
|
+
/**
|
|
4984
|
+
* Get all pending approval requests.
|
|
4985
|
+
*/
|
|
4986
|
+
getPending() {
|
|
4987
|
+
return Array.from(this.pending.values()).map((e) => e.prompt);
|
|
4988
|
+
}
|
|
4989
|
+
async cancelOtherProviders(handles, excludeProviderId) {
|
|
4990
|
+
for (const [providerId, handle] of handles) {
|
|
4991
|
+
if (providerId === excludeProviderId) continue;
|
|
4992
|
+
const provider = this.providers.find((p) => p.id === providerId);
|
|
4993
|
+
if (provider) {
|
|
4994
|
+
try {
|
|
4995
|
+
await provider.cancelPrompt(handle);
|
|
4996
|
+
} catch {
|
|
4997
|
+
}
|
|
4998
|
+
}
|
|
4999
|
+
}
|
|
5000
|
+
}
|
|
5001
|
+
};
|
|
5002
|
+
|
|
5003
|
+
// src/core/orchestrator/governance-gate.ts
|
|
5004
|
+
var GovernanceGate = class {
|
|
5005
|
+
};
|
|
5006
|
+
var QAGate = class extends GovernanceGate {
|
|
5007
|
+
constructor(validators = []) {
|
|
5008
|
+
super();
|
|
5009
|
+
this.validators = validators;
|
|
5010
|
+
}
|
|
5011
|
+
validators;
|
|
5012
|
+
id = "qa";
|
|
5013
|
+
name = "Quality Assurance";
|
|
5014
|
+
dimension = "data_correctness";
|
|
5015
|
+
async check(input) {
|
|
5016
|
+
const start = Date.now();
|
|
5017
|
+
const findings = [];
|
|
5018
|
+
for (const validator of this.validators) {
|
|
5019
|
+
const results = validator.validate(input.output, input.metadata);
|
|
5020
|
+
findings.push(...results);
|
|
5021
|
+
}
|
|
5022
|
+
const hasErrors = findings.some((f) => f.severity === "error" || f.severity === "critical");
|
|
5023
|
+
const hasWarnings = findings.some((f) => f.severity === "warning");
|
|
5024
|
+
return {
|
|
5025
|
+
gateId: this.id,
|
|
5026
|
+
verdict: hasErrors ? "fail" : hasWarnings ? "warn" : "pass",
|
|
5027
|
+
findings,
|
|
5028
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5029
|
+
durationMs: Date.now() - start
|
|
5030
|
+
};
|
|
5031
|
+
}
|
|
5032
|
+
};
|
|
5033
|
+
var QualityGate = class extends GovernanceGate {
|
|
5034
|
+
constructor(checks = []) {
|
|
5035
|
+
super();
|
|
5036
|
+
this.checks = checks;
|
|
5037
|
+
}
|
|
5038
|
+
checks;
|
|
5039
|
+
id = "quality";
|
|
5040
|
+
name = "Code Quality";
|
|
5041
|
+
dimension = "code_quality";
|
|
5042
|
+
async check(input) {
|
|
5043
|
+
const start = Date.now();
|
|
5044
|
+
const findings = [];
|
|
5045
|
+
for (const chk of this.checks) {
|
|
5046
|
+
const results = await chk.check(input.output, input.metadata);
|
|
5047
|
+
findings.push(...results);
|
|
5048
|
+
}
|
|
5049
|
+
const hasErrors = findings.some((f) => f.severity === "error" || f.severity === "critical");
|
|
5050
|
+
const hasWarnings = findings.some((f) => f.severity === "warning");
|
|
5051
|
+
return {
|
|
5052
|
+
gateId: this.id,
|
|
5053
|
+
verdict: hasErrors ? "fail" : hasWarnings ? "warn" : "pass",
|
|
5054
|
+
findings,
|
|
5055
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5056
|
+
durationMs: Date.now() - start
|
|
5057
|
+
};
|
|
5058
|
+
}
|
|
5059
|
+
};
|
|
5060
|
+
var DriftGate = class extends GovernanceGate {
|
|
5061
|
+
constructor(rules = []) {
|
|
5062
|
+
super();
|
|
5063
|
+
this.rules = rules;
|
|
5064
|
+
}
|
|
5065
|
+
rules;
|
|
5066
|
+
id = "drift";
|
|
5067
|
+
name = "Architectural Drift";
|
|
5068
|
+
dimension = "architecture";
|
|
5069
|
+
async check(input) {
|
|
5070
|
+
const start = Date.now();
|
|
5071
|
+
const findings = [];
|
|
5072
|
+
for (const rule of this.rules) {
|
|
5073
|
+
const results = rule.detect(input.output, input.metadata);
|
|
5074
|
+
findings.push(...results);
|
|
5075
|
+
}
|
|
5076
|
+
const hasErrors = findings.some((f) => f.severity === "error" || f.severity === "critical");
|
|
5077
|
+
const hasWarnings = findings.some((f) => f.severity === "warning");
|
|
5078
|
+
return {
|
|
5079
|
+
gateId: this.id,
|
|
5080
|
+
verdict: hasErrors ? "fail" : hasWarnings ? "warn" : "pass",
|
|
5081
|
+
findings,
|
|
5082
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5083
|
+
durationMs: Date.now() - start
|
|
5084
|
+
};
|
|
5085
|
+
}
|
|
5086
|
+
};
|
|
5087
|
+
var GateRunner = class {
|
|
5088
|
+
constructor(gates, hooks) {
|
|
5089
|
+
this.gates = gates;
|
|
5090
|
+
this.hooks = hooks;
|
|
5091
|
+
}
|
|
5092
|
+
gates;
|
|
5093
|
+
hooks;
|
|
5094
|
+
/**
|
|
5095
|
+
* Run all gates on the given input.
|
|
5096
|
+
* Gates run independently — one failure doesn't block others.
|
|
5097
|
+
*/
|
|
5098
|
+
async runAll(input) {
|
|
5099
|
+
const results = [];
|
|
5100
|
+
for (const gate of this.gates) {
|
|
5101
|
+
try {
|
|
5102
|
+
const result = await gate.check(input);
|
|
5103
|
+
results.push(result);
|
|
5104
|
+
await this.hooks.emit("governance.gate_completed", {
|
|
5105
|
+
gateId: gate.id,
|
|
5106
|
+
gateName: gate.name,
|
|
5107
|
+
verdict: result.verdict,
|
|
5108
|
+
findingCount: result.findings.length,
|
|
5109
|
+
agentId: input.agentId,
|
|
5110
|
+
taskId: input.taskId
|
|
5111
|
+
});
|
|
5112
|
+
} catch (err) {
|
|
5113
|
+
results.push({
|
|
5114
|
+
gateId: gate.id,
|
|
5115
|
+
verdict: "fail",
|
|
5116
|
+
findings: [{
|
|
5117
|
+
severity: "error",
|
|
5118
|
+
message: `Gate error: ${err instanceof Error ? err.message : String(err)}`
|
|
5119
|
+
}],
|
|
5120
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5121
|
+
durationMs: 0
|
|
5122
|
+
});
|
|
5123
|
+
}
|
|
5124
|
+
}
|
|
5125
|
+
const passed = results.every((r) => r.verdict !== "fail");
|
|
5126
|
+
await this.hooks.emit("governance.review_completed", {
|
|
5127
|
+
passed,
|
|
5128
|
+
agentId: input.agentId,
|
|
5129
|
+
taskId: input.taskId,
|
|
5130
|
+
results: results.map((r) => ({
|
|
5131
|
+
gateId: r.gateId,
|
|
5132
|
+
verdict: r.verdict,
|
|
5133
|
+
findingCount: r.findings.length
|
|
5134
|
+
}))
|
|
5135
|
+
});
|
|
5136
|
+
return { passed, results };
|
|
5137
|
+
}
|
|
5138
|
+
};
|
|
5139
|
+
|
|
4078
5140
|
// src/core/orchestrator/user-registry.ts
|
|
4079
5141
|
import { v4 as uuidv4 } from "uuid";
|
|
4080
5142
|
var UserRegistry = class {
|
|
@@ -4406,25 +5468,37 @@ export {
|
|
|
4406
5468
|
ApiExecutionAdapter,
|
|
4407
5469
|
AuditEmitter,
|
|
4408
5470
|
BackupManager,
|
|
5471
|
+
BreakerState,
|
|
4409
5472
|
BudgetController,
|
|
4410
5473
|
CORE_MIGRATIONS,
|
|
4411
5474
|
ChannelRegistry,
|
|
4412
5475
|
ChannelRegistryError,
|
|
4413
5476
|
ChatSessionManager,
|
|
5477
|
+
CircuitBreaker,
|
|
4414
5478
|
CliExecutionAdapter,
|
|
4415
5479
|
ColumnValidatorImpl,
|
|
4416
5480
|
DEFAULTS,
|
|
4417
5481
|
DEFAULT_CONFIG,
|
|
4418
5482
|
DataStore,
|
|
4419
5483
|
DataStoreError,
|
|
5484
|
+
DeterministicAdapter,
|
|
5485
|
+
DriftGate,
|
|
4420
5486
|
EVENTS,
|
|
5487
|
+
GateRunner,
|
|
5488
|
+
GovernanceGate,
|
|
4421
5489
|
HookBus,
|
|
5490
|
+
LearningPipeline,
|
|
5491
|
+
LoopDetector,
|
|
5492
|
+
LoopType,
|
|
4422
5493
|
MAX_CHAIN_DEPTH,
|
|
4423
5494
|
MessagePipeline,
|
|
4424
5495
|
ModelRouter,
|
|
4425
5496
|
NdjsonLogger,
|
|
4426
5497
|
NotificationQueue,
|
|
5498
|
+
PermissionRelay,
|
|
4427
5499
|
ProviderRegistry,
|
|
5500
|
+
QAGate,
|
|
5501
|
+
QualityGate,
|
|
4428
5502
|
RUN_STATUSES,
|
|
4429
5503
|
RunManager,
|
|
4430
5504
|
Scheduler,
|
|
@@ -4433,6 +5507,7 @@ export {
|
|
|
4433
5507
|
SessionManager,
|
|
4434
5508
|
TASK_STATUSES,
|
|
4435
5509
|
TaskQueue,
|
|
5510
|
+
TriageRouter,
|
|
4436
5511
|
UpdateChecker,
|
|
4437
5512
|
UpdateManager,
|
|
4438
5513
|
UserRegistry,
|