chapterhouse 0.3.26 → 0.4.1
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/dist/api/server.js +12 -0
- package/dist/api/server.test.js +39 -0
- package/dist/config.js +70 -0
- package/dist/config.test.js +109 -0
- package/dist/copilot/agents.js +32 -6
- package/dist/copilot/agents.test.js +41 -0
- package/dist/copilot/oneshot.js +54 -0
- package/dist/copilot/orchestrator.js +224 -3
- package/dist/copilot/orchestrator.test.js +380 -0
- package/dist/copilot/prompt-date.js +8 -0
- package/dist/copilot/system-message.js +8 -0
- package/dist/copilot/system-message.test.js +58 -0
- package/dist/copilot/tools.agent.test.js +24 -0
- package/dist/copilot/tools.js +351 -4
- package/dist/copilot/tools.memory.test.js +297 -0
- package/dist/copilot/turn-event-log-env.test.js +19 -0
- package/dist/copilot/turn-event-log.js +22 -23
- package/dist/copilot/turn-event-log.test.js +61 -2
- package/dist/memory/active-scope.js +69 -0
- package/dist/memory/active-scope.test.js +76 -0
- package/dist/memory/checkpoint-prompt.js +71 -0
- package/dist/memory/checkpoint.js +257 -0
- package/dist/memory/checkpoint.test.js +255 -0
- package/dist/memory/decisions.js +53 -0
- package/dist/memory/decisions.test.js +92 -0
- package/dist/memory/entities.js +59 -0
- package/dist/memory/entities.test.js +65 -0
- package/dist/memory/eot.js +219 -0
- package/dist/memory/eot.test.js +263 -0
- package/dist/memory/hot-tier.js +187 -0
- package/dist/memory/hot-tier.test.js +197 -0
- package/dist/memory/housekeeping.js +352 -0
- package/dist/memory/housekeeping.test.js +280 -0
- package/dist/memory/inbox.js +73 -0
- package/dist/memory/index.js +11 -0
- package/dist/memory/observations.js +46 -0
- package/dist/memory/observations.test.js +86 -0
- package/dist/memory/recall.js +210 -0
- package/dist/memory/recall.test.js +238 -0
- package/dist/memory/scopes.js +89 -0
- package/dist/memory/scopes.test.js +201 -0
- package/dist/memory/tiering.js +193 -0
- package/dist/memory/types.js +2 -0
- package/dist/paths.js +7 -1
- package/dist/store/db.js +412 -8
- package/dist/store/db.test.js +83 -0
- package/dist/test/setup-env.js +16 -0
- package/dist/test/setup-env.test.js +4 -0
- package/package.json +1 -1
- package/web/dist/assets/{index-BRPJa1DK.js → index-DmYLALt0.js} +70 -70
- package/web/dist/assets/index-DmYLALt0.js.map +1 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-BRPJa1DK.js.map +0 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
const repoRoot = process.cwd();
|
|
6
|
+
const sandboxRoot = join(repoRoot, ".test-work", `memory-decisions-${process.pid}`);
|
|
7
|
+
const chapterhouseHome = join(sandboxRoot, ".chapterhouse");
|
|
8
|
+
process.env.CHAPTERHOUSE_HOME = sandboxRoot;
|
|
9
|
+
function resetSandbox() {
|
|
10
|
+
mkdirSync(join(repoRoot, ".test-work"), { recursive: true });
|
|
11
|
+
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
12
|
+
mkdirSync(chapterhouseHome, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
async function loadModules() {
|
|
15
|
+
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
16
|
+
const memoryModule = await import(new URL("./index.js", import.meta.url).href);
|
|
17
|
+
return { dbModule, memoryModule };
|
|
18
|
+
}
|
|
19
|
+
function getFunction(module, name) {
|
|
20
|
+
const value = module[name];
|
|
21
|
+
assert.equal(typeof value, "function", `expected ${name} to be exported`);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
test.beforeEach(async () => {
|
|
25
|
+
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
26
|
+
dbModule.closeDb();
|
|
27
|
+
resetSandbox();
|
|
28
|
+
});
|
|
29
|
+
test.after(async () => {
|
|
30
|
+
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
31
|
+
dbModule.closeDb();
|
|
32
|
+
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
test("recordDecision supports CRUD, supersession, and keeps the decision FTS index in sync", async () => {
|
|
35
|
+
const { dbModule, memoryModule } = await loadModules();
|
|
36
|
+
const db = dbModule.getDb();
|
|
37
|
+
const getScope = getFunction(memoryModule, "getScope");
|
|
38
|
+
const recordDecision = getFunction(memoryModule, "recordDecision");
|
|
39
|
+
const getDecision = getFunction(memoryModule, "getDecision");
|
|
40
|
+
const listDecisions = getFunction(memoryModule, "listDecisions");
|
|
41
|
+
const supersedeDecision = getFunction(memoryModule, "supersedeDecision");
|
|
42
|
+
const chapterhouse = getScope("chapterhouse");
|
|
43
|
+
assert.ok(chapterhouse, "chapterhouse scope should be seeded");
|
|
44
|
+
const original = recordDecision({
|
|
45
|
+
scope_id: chapterhouse.id,
|
|
46
|
+
title: "Use SQLite for scoped memory",
|
|
47
|
+
rationale: "SQLite FTS5 keeps memory recall local, fast, and dependency-free.",
|
|
48
|
+
decided_at: "2026-05-13",
|
|
49
|
+
});
|
|
50
|
+
const replacement = recordDecision({
|
|
51
|
+
scope_id: chapterhouse.id,
|
|
52
|
+
title: "Keep SQLite for agent memory v1",
|
|
53
|
+
rationale: "SQLite remains the only persistence layer for PR 2.",
|
|
54
|
+
decided_at: "2026-05-14",
|
|
55
|
+
});
|
|
56
|
+
assert.equal(listDecisions({ scope_id: chapterhouse.id }).length >= 2, true);
|
|
57
|
+
assert.deepEqual(getDecision(original.id)?.title, original.title);
|
|
58
|
+
const initialHits = db.prepare(`
|
|
59
|
+
SELECT rowid
|
|
60
|
+
FROM mem_decisions_fts
|
|
61
|
+
WHERE mem_decisions_fts MATCH 'dependency'
|
|
62
|
+
`).all();
|
|
63
|
+
assert.deepEqual(initialHits, [{ rowid: original.id }]);
|
|
64
|
+
db.prepare(`
|
|
65
|
+
UPDATE mem_decisions
|
|
66
|
+
SET rationale = ?
|
|
67
|
+
WHERE id = ?
|
|
68
|
+
`).run("FTS snippets should refresh when the rationale changes.", original.id);
|
|
69
|
+
const oldHits = db.prepare(`
|
|
70
|
+
SELECT rowid
|
|
71
|
+
FROM mem_decisions_fts
|
|
72
|
+
WHERE mem_decisions_fts MATCH 'dependency'
|
|
73
|
+
`).all();
|
|
74
|
+
const newHits = db.prepare(`
|
|
75
|
+
SELECT rowid
|
|
76
|
+
FROM mem_decisions_fts
|
|
77
|
+
WHERE mem_decisions_fts MATCH 'snippets'
|
|
78
|
+
`).all();
|
|
79
|
+
assert.deepEqual(oldHits, []);
|
|
80
|
+
assert.deepEqual(newHits, [{ rowid: original.id }]);
|
|
81
|
+
const superseded = supersedeDecision(original.id, replacement.id);
|
|
82
|
+
assert.equal(superseded.supersededBy, replacement.id);
|
|
83
|
+
assert.equal(getDecision(original.id)?.supersededBy, replacement.id);
|
|
84
|
+
db.prepare(`DELETE FROM mem_decisions WHERE id = ?`).run(original.id);
|
|
85
|
+
const deletedHits = db.prepare(`
|
|
86
|
+
SELECT rowid
|
|
87
|
+
FROM mem_decisions_fts
|
|
88
|
+
WHERE mem_decisions_fts MATCH 'snippets'
|
|
89
|
+
`).all();
|
|
90
|
+
assert.deepEqual(deletedHits, []);
|
|
91
|
+
});
|
|
92
|
+
//# sourceMappingURL=decisions.test.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { getDb } from "../store/db.js";
|
|
2
|
+
function toEntity(row) {
|
|
3
|
+
return {
|
|
4
|
+
id: row.id,
|
|
5
|
+
scopeId: row.scope_id,
|
|
6
|
+
kind: row.kind,
|
|
7
|
+
name: row.name,
|
|
8
|
+
summary: row.summary ?? undefined,
|
|
9
|
+
tier: row.tier,
|
|
10
|
+
confidence: row.confidence,
|
|
11
|
+
createdAt: row.created_at,
|
|
12
|
+
updatedAt: row.updated_at,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function getEntity(id) {
|
|
16
|
+
const row = getDb().prepare(`
|
|
17
|
+
SELECT id, scope_id, kind, name, summary, tier, confidence, created_at, updated_at
|
|
18
|
+
FROM mem_entities
|
|
19
|
+
WHERE id = ?
|
|
20
|
+
`).get(id);
|
|
21
|
+
return row ? toEntity(row) : undefined;
|
|
22
|
+
}
|
|
23
|
+
export function findEntityByName(scopeId, kind, name) {
|
|
24
|
+
const row = getDb().prepare(`
|
|
25
|
+
SELECT id, scope_id, kind, name, summary, tier, confidence, created_at, updated_at
|
|
26
|
+
FROM mem_entities
|
|
27
|
+
WHERE scope_id = ? AND kind = ? AND name = ?
|
|
28
|
+
`).get(scopeId, kind, name);
|
|
29
|
+
return row ? toEntity(row) : undefined;
|
|
30
|
+
}
|
|
31
|
+
export function upsertEntity(input) {
|
|
32
|
+
const db = getDb();
|
|
33
|
+
const existing = findEntityByName(input.scope_id, input.kind, input.name);
|
|
34
|
+
if (existing) {
|
|
35
|
+
db.prepare(`
|
|
36
|
+
UPDATE mem_entities
|
|
37
|
+
SET summary = ?, tier = ?, confidence = ?, updated_at = CURRENT_TIMESTAMP
|
|
38
|
+
WHERE id = ?
|
|
39
|
+
`).run(input.summary ?? existing.summary ?? null, input.tier ?? existing.tier, input.confidence ?? existing.confidence, existing.id);
|
|
40
|
+
return getEntity(existing.id);
|
|
41
|
+
}
|
|
42
|
+
const result = db.prepare(`
|
|
43
|
+
INSERT INTO mem_entities (scope_id, kind, name, summary, tier, confidence, created_at, updated_at)
|
|
44
|
+
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
45
|
+
`).run(input.scope_id, input.kind, input.name, input.summary ?? null, input.tier ?? "warm", input.confidence ?? 1.0);
|
|
46
|
+
return getEntity(Number(result.lastInsertRowid));
|
|
47
|
+
}
|
|
48
|
+
export function listEntities(input = {}) {
|
|
49
|
+
const rows = getDb().prepare(`
|
|
50
|
+
SELECT id, scope_id, kind, name, summary, tier, confidence, created_at, updated_at
|
|
51
|
+
FROM mem_entities
|
|
52
|
+
WHERE (? IS NULL OR scope_id = ?)
|
|
53
|
+
AND (? IS NULL OR kind = ?)
|
|
54
|
+
ORDER BY updated_at DESC, id DESC
|
|
55
|
+
LIMIT ? OFFSET ?
|
|
56
|
+
`).all(input.scope_id ?? null, input.scope_id ?? null, input.kind ?? null, input.kind ?? null, input.limit ?? 50, input.offset ?? 0);
|
|
57
|
+
return rows.map(toEntity);
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=entities.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
const repoRoot = process.cwd();
|
|
6
|
+
const sandboxRoot = join(repoRoot, ".test-work", `memory-entities-${process.pid}`);
|
|
7
|
+
const chapterhouseHome = join(sandboxRoot, ".chapterhouse");
|
|
8
|
+
process.env.CHAPTERHOUSE_HOME = sandboxRoot;
|
|
9
|
+
function resetSandbox() {
|
|
10
|
+
mkdirSync(join(repoRoot, ".test-work"), { recursive: true });
|
|
11
|
+
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
12
|
+
mkdirSync(chapterhouseHome, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
async function loadModules() {
|
|
15
|
+
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
16
|
+
const memoryModule = await import(new URL("./index.js", import.meta.url).href);
|
|
17
|
+
return { dbModule, memoryModule };
|
|
18
|
+
}
|
|
19
|
+
function getFunction(module, name) {
|
|
20
|
+
const value = module[name];
|
|
21
|
+
assert.equal(typeof value, "function", `expected ${name} to be exported`);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
test.beforeEach(async () => {
|
|
25
|
+
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
26
|
+
dbModule.closeDb();
|
|
27
|
+
resetSandbox();
|
|
28
|
+
});
|
|
29
|
+
test.after(async () => {
|
|
30
|
+
const dbModule = await import(new URL("../store/db.js", import.meta.url).href);
|
|
31
|
+
dbModule.closeDb();
|
|
32
|
+
rmSync(sandboxRoot, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
test("upsertEntity is idempotent on scope, kind, and name", async () => {
|
|
35
|
+
const { dbModule, memoryModule } = await loadModules();
|
|
36
|
+
dbModule.getDb();
|
|
37
|
+
const getScope = getFunction(memoryModule, "getScope");
|
|
38
|
+
const upsertEntity = getFunction(memoryModule, "upsertEntity");
|
|
39
|
+
const getEntity = getFunction(memoryModule, "getEntity");
|
|
40
|
+
const findEntityByName = getFunction(memoryModule, "findEntityByName");
|
|
41
|
+
const listEntities = getFunction(memoryModule, "listEntities");
|
|
42
|
+
const chapterhouse = getScope("chapterhouse");
|
|
43
|
+
assert.ok(chapterhouse, "chapterhouse scope should be seeded");
|
|
44
|
+
const first = upsertEntity({
|
|
45
|
+
scope_id: chapterhouse.id,
|
|
46
|
+
kind: "tool",
|
|
47
|
+
name: "better-sqlite3",
|
|
48
|
+
summary: "SQLite driver used for memory persistence.",
|
|
49
|
+
tier: "warm",
|
|
50
|
+
confidence: 0.7,
|
|
51
|
+
});
|
|
52
|
+
const second = upsertEntity({
|
|
53
|
+
scope_id: chapterhouse.id,
|
|
54
|
+
kind: "tool",
|
|
55
|
+
name: "better-sqlite3",
|
|
56
|
+
summary: "Sync SQLite driver used throughout the daemon.",
|
|
57
|
+
tier: "hot",
|
|
58
|
+
confidence: 0.95,
|
|
59
|
+
});
|
|
60
|
+
assert.equal(second.id, first.id);
|
|
61
|
+
assert.equal(listEntities({ scope_id: chapterhouse.id, kind: "tool" }).length, 1);
|
|
62
|
+
assert.equal(findEntityByName(chapterhouse.id, "tool", "better-sqlite3")?.id, first.id);
|
|
63
|
+
assert.deepEqual(getEntity(first.id), second);
|
|
64
|
+
});
|
|
65
|
+
//# sourceMappingURL=entities.test.js.map
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { config } from "../config.js";
|
|
2
|
+
import { runOneShotPrompt } from "../copilot/oneshot.js";
|
|
3
|
+
import { childLogger } from "../util/logger.js";
|
|
4
|
+
import { recordDecision } from "./decisions.js";
|
|
5
|
+
import { upsertEntity } from "./entities.js";
|
|
6
|
+
import { listPendingMemoryProposalsForTask, resolveInboxItem } from "./inbox.js";
|
|
7
|
+
import { recordObservation } from "./observations.js";
|
|
8
|
+
import { getScope } from "./scopes.js";
|
|
9
|
+
const log = childLogger("memory.eot");
|
|
10
|
+
function isEndOfTaskHookEnabled() {
|
|
11
|
+
const raw = process.env.CHAPTERHOUSE_MEMORY_EOT_HOOK_ENABLED?.trim();
|
|
12
|
+
if (raw === "0")
|
|
13
|
+
return false;
|
|
14
|
+
if (raw === "1")
|
|
15
|
+
return true;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
function isMemoryAutoAcceptEnabled() {
|
|
19
|
+
const raw = process.env.CHAPTERHOUSE_MEMORY_AUTO_ACCEPT?.trim();
|
|
20
|
+
if (raw === "0")
|
|
21
|
+
return false;
|
|
22
|
+
if (raw === "1")
|
|
23
|
+
return true;
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
function buildReviewerSystemPrompt() {
|
|
27
|
+
return [
|
|
28
|
+
"You review subagent memory proposals at end-of-task.",
|
|
29
|
+
"Decide accept or reject for each proposal id.",
|
|
30
|
+
"Optionally extract additional implicit durable memories from the task summary.",
|
|
31
|
+
"Return JSON only with keys: decisions, implicit_memories.",
|
|
32
|
+
"Each decision must include proposal_id, decision, reason.",
|
|
33
|
+
"Each implicit memory must include kind, scope_slug, payload, and may include confidence/reason.",
|
|
34
|
+
].join("\n");
|
|
35
|
+
}
|
|
36
|
+
function buildReviewerUserPrompt(finalResult, proposals) {
|
|
37
|
+
return JSON.stringify({
|
|
38
|
+
final_result: finalResult,
|
|
39
|
+
proposals: proposals.map((proposal) => ({
|
|
40
|
+
id: proposal.id,
|
|
41
|
+
source_agent: proposal.sourceAgent,
|
|
42
|
+
payload: proposal.payload,
|
|
43
|
+
})),
|
|
44
|
+
}, null, 2);
|
|
45
|
+
}
|
|
46
|
+
function parseEnvelope(raw) {
|
|
47
|
+
const parsed = JSON.parse(raw);
|
|
48
|
+
if (!parsed || typeof parsed !== "object") {
|
|
49
|
+
throw new Error("Invalid memory proposal payload.");
|
|
50
|
+
}
|
|
51
|
+
if (parsed.kind !== "observation" && parsed.kind !== "decision" && parsed.kind !== "entity") {
|
|
52
|
+
throw new Error("Invalid proposal kind.");
|
|
53
|
+
}
|
|
54
|
+
if (!parsed.payload || typeof parsed.payload !== "object") {
|
|
55
|
+
throw new Error("Invalid proposal payload.");
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
kind: parsed.kind,
|
|
59
|
+
scope_slug: typeof parsed.scope_slug === "string" ? parsed.scope_slug : undefined,
|
|
60
|
+
confidence: typeof parsed.confidence === "number" ? parsed.confidence : 0.5,
|
|
61
|
+
reason: typeof parsed.reason === "string" ? parsed.reason : undefined,
|
|
62
|
+
payload: parsed.payload,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function parseReviewerResponse(raw) {
|
|
66
|
+
const parsed = JSON.parse(raw);
|
|
67
|
+
return {
|
|
68
|
+
decisions: Array.isArray(parsed.decisions)
|
|
69
|
+
? parsed.decisions.flatMap((entry) => {
|
|
70
|
+
if (!entry || typeof entry !== "object") {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const candidate = entry;
|
|
74
|
+
if (typeof candidate.proposal_id !== "number") {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
if (candidate.decision !== "accept" && candidate.decision !== "reject") {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
if (typeof candidate.reason !== "string" || candidate.reason.trim().length === 0) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
return [{
|
|
84
|
+
proposal_id: candidate.proposal_id,
|
|
85
|
+
decision: candidate.decision,
|
|
86
|
+
reason: candidate.reason.trim(),
|
|
87
|
+
}];
|
|
88
|
+
})
|
|
89
|
+
: [],
|
|
90
|
+
implicit_memories: Array.isArray(parsed.implicit_memories)
|
|
91
|
+
? parsed.implicit_memories.flatMap((entry) => {
|
|
92
|
+
if (!entry || typeof entry !== "object") {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
const candidate = entry;
|
|
96
|
+
if (candidate.kind !== "observation" && candidate.kind !== "decision" && candidate.kind !== "entity") {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
if (typeof candidate.scope_slug !== "string" || !candidate.payload || typeof candidate.payload !== "object") {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
return [{
|
|
103
|
+
kind: candidate.kind,
|
|
104
|
+
scope_slug: candidate.scope_slug,
|
|
105
|
+
payload: candidate.payload,
|
|
106
|
+
confidence: typeof candidate.confidence === "number" ? candidate.confidence : undefined,
|
|
107
|
+
reason: typeof candidate.reason === "string" ? candidate.reason : undefined,
|
|
108
|
+
}];
|
|
109
|
+
})
|
|
110
|
+
: [],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function rememberAcceptedMemory(kind, scopeSlug, payload, source, confidence) {
|
|
114
|
+
const scope = getScope(scopeSlug);
|
|
115
|
+
if (!scope) {
|
|
116
|
+
throw new Error(`Unknown memory scope '${scopeSlug}'.`);
|
|
117
|
+
}
|
|
118
|
+
if (kind === "observation") {
|
|
119
|
+
const observation = payload;
|
|
120
|
+
recordObservation({
|
|
121
|
+
scope_id: scope.id,
|
|
122
|
+
entity_id: observation.entity_id,
|
|
123
|
+
content: observation.content,
|
|
124
|
+
source: observation.source ?? source,
|
|
125
|
+
confidence,
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (kind === "decision") {
|
|
130
|
+
const decision = payload;
|
|
131
|
+
recordDecision({
|
|
132
|
+
scope_id: scope.id,
|
|
133
|
+
title: decision.title,
|
|
134
|
+
rationale: decision.rationale ?? decision.title,
|
|
135
|
+
decided_at: decision.decided_at,
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const entity = payload;
|
|
140
|
+
upsertEntity({
|
|
141
|
+
scope_id: scope.id,
|
|
142
|
+
kind: entity.kind,
|
|
143
|
+
name: entity.name,
|
|
144
|
+
summary: entity.summary,
|
|
145
|
+
confidence,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
export async function runEndOfTaskMemoryHook(input) {
|
|
149
|
+
const autoAcceptEnabled = isMemoryAutoAcceptEnabled();
|
|
150
|
+
const summary = {
|
|
151
|
+
task_id: input.taskId,
|
|
152
|
+
proposals_total: 0,
|
|
153
|
+
accepted: 0,
|
|
154
|
+
rejected: 0,
|
|
155
|
+
implicit_extracted: 0,
|
|
156
|
+
auto_accept: autoAcceptEnabled,
|
|
157
|
+
};
|
|
158
|
+
if (!isEndOfTaskHookEnabled()) {
|
|
159
|
+
return summary;
|
|
160
|
+
}
|
|
161
|
+
const proposals = listPendingMemoryProposalsForTask(input.taskId);
|
|
162
|
+
summary.proposals_total = proposals.length;
|
|
163
|
+
const callLLM = input.callLLM ?? (async ({ system, user, model }) => {
|
|
164
|
+
const result = await runOneShotPrompt({
|
|
165
|
+
client: input.copilotClient,
|
|
166
|
+
model,
|
|
167
|
+
system,
|
|
168
|
+
user,
|
|
169
|
+
expectJson: true,
|
|
170
|
+
});
|
|
171
|
+
return result.content;
|
|
172
|
+
});
|
|
173
|
+
const review = parseReviewerResponse(await callLLM({
|
|
174
|
+
system: buildReviewerSystemPrompt(),
|
|
175
|
+
user: buildReviewerUserPrompt(input.finalResult, proposals),
|
|
176
|
+
model: input.model ?? config.copilotModel,
|
|
177
|
+
}));
|
|
178
|
+
const proposalsById = new Map(proposals.map((proposal) => [proposal.id, proposal]));
|
|
179
|
+
const reviewedProposalIds = new Set();
|
|
180
|
+
for (const decision of review.decisions) {
|
|
181
|
+
const proposal = proposalsById.get(decision.proposal_id);
|
|
182
|
+
if (!proposal) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
reviewedProposalIds.add(proposal.id);
|
|
186
|
+
if (decision.decision === "accept") {
|
|
187
|
+
summary.accepted++;
|
|
188
|
+
if (autoAcceptEnabled) {
|
|
189
|
+
const envelope = parseEnvelope(proposal.payload);
|
|
190
|
+
rememberAcceptedMemory(envelope.kind, envelope.scope_slug ?? getScope(proposal.scopeId).slug, envelope.payload, `agent:${proposal.sourceAgent}`, envelope.confidence);
|
|
191
|
+
resolveInboxItem(proposal.id, "accepted", decision.reason);
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
summary.rejected++;
|
|
196
|
+
if (autoAcceptEnabled) {
|
|
197
|
+
resolveInboxItem(proposal.id, "rejected", decision.reason);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
for (const proposal of proposals) {
|
|
201
|
+
if (reviewedProposalIds.has(proposal.id)) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
summary.rejected++;
|
|
205
|
+
if (autoAcceptEnabled) {
|
|
206
|
+
resolveInboxItem(proposal.id, "rejected", "Reviewer did not select this proposal for acceptance.");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (autoAcceptEnabled) {
|
|
210
|
+
for (const implicitMemory of review.implicit_memories) {
|
|
211
|
+
rememberAcceptedMemory(implicitMemory.kind, implicitMemory.scope_slug, implicitMemory.payload, "agent:eot", implicitMemory.confidence);
|
|
212
|
+
summary.implicit_extracted++;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
log.info(summary, "memory.eot.processed");
|
|
216
|
+
input.onProcessed?.(summary);
|
|
217
|
+
return summary;
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=eot.js.map
|