@unblocklabs/unblock-memory 0.3.15 → 0.3.17
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 +303 -15
- package/dist/src/config.d.ts +4 -0
- package/dist/src/config.js +8 -2
- package/dist/src/evidence-review.d.ts +9 -1
- package/dist/src/evidence-review.js +2 -1
- package/dist/src/manager.d.ts +5 -1
- package/dist/src/people-background.d.ts +3 -0
- package/dist/src/people-background.js +5 -0
- package/dist/src/people-dossier-review.d.ts +41 -0
- package/dist/src/people-dossier-review.js +47 -0
- package/dist/src/people-primer-config.d.ts +13 -0
- package/dist/src/people-primer-config.js +33 -0
- package/dist/src/people-primer-tool.d.ts +5 -0
- package/dist/src/people-primer-tool.js +78 -0
- package/dist/src/people-primer.d.ts +94 -0
- package/dist/src/people-primer.js +170 -0
- package/dist/src/people-store.d.ts +8 -1
- package/dist/src/people-store.js +60 -2
- package/dist/src/people-tools.d.ts +2 -1
- package/dist/src/people-tools.js +35 -11
- package/dist/src/plugin.js +5 -1
- package/dist/src/response-audit.d.ts +87 -0
- package/dist/src/response-audit.js +193 -0
- package/dist/src/response-config.d.ts +13 -0
- package/dist/src/response-config.js +43 -0
- package/dist/src/response-episodes.d.ts +68 -0
- package/dist/src/response-episodes.js +242 -0
- package/dist/src/response-identity.d.ts +15 -0
- package/dist/src/response-identity.js +34 -0
- package/dist/src/response-judge.d.ts +224 -0
- package/dist/src/response-judge.js +248 -0
- package/dist/src/response-memory.d.ts +8 -0
- package/dist/src/response-memory.js +25 -0
- package/dist/src/response-outcome.d.ts +30 -0
- package/dist/src/response-outcome.js +51 -0
- package/dist/src/response-reviews.d.ts +27 -0
- package/dist/src/response-reviews.js +116 -0
- package/dist/src/response-runtime.d.ts +3 -0
- package/dist/src/response-runtime.js +150 -0
- package/dist/src/response-stages.d.ts +184 -0
- package/dist/src/response-stages.js +38 -0
- package/dist/src/response-store.d.ts +180 -0
- package/dist/src/response-store.js +411 -0
- package/dist/src/response-text.d.ts +6 -0
- package/dist/src/response-text.js +37 -0
- package/dist/src/typesafe-review.d.ts +14 -1
- package/dist/src/typesafe-review.js +46 -8
- package/openclaw.plugin.json +37 -1
- package/package.json +1 -1
- package/skills/people-whisperer/SKILL.md +107 -100
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Reporting/composition changes do not require paid re-inference of the same rubric.
|
|
2
|
+
export const RESPONSE_REPORT_VERSION = "response-report-v4";
|
|
3
|
+
/** Compose observed evidence, not a factual-verification or causal agent grade.
|
|
4
|
+
* Narrow admissions take precedence over praise; missing evidence stays unknown. */
|
|
5
|
+
export function responseOutcome(result) {
|
|
6
|
+
const later = result.retrospective.judgment;
|
|
7
|
+
const basis = [];
|
|
8
|
+
const reasons = [];
|
|
9
|
+
const reasonDetails = [];
|
|
10
|
+
const addReason = (reason, strength, source, measure = "choice_confidence") => {
|
|
11
|
+
reasons.push(reason);
|
|
12
|
+
reasonDetails.push({ reason, strength, measure, source });
|
|
13
|
+
};
|
|
14
|
+
if (result.quality.underdelivery.noul >= 0.8) {
|
|
15
|
+
basis.push("visible_underdelivery");
|
|
16
|
+
const reason = result.quality.failureReason;
|
|
17
|
+
if (reason.confidence >= 0.8 && reason.choice !== "none_or_unclear")
|
|
18
|
+
addReason(reason.choice, reason.confidence, "visible_quality");
|
|
19
|
+
}
|
|
20
|
+
if (later) {
|
|
21
|
+
const correction = later.correction.noul >= 0.8;
|
|
22
|
+
const delivery = later.deliveryAdmission.noul >= 0.8;
|
|
23
|
+
const regression = later.regression.noul >= 0.8;
|
|
24
|
+
if (correction)
|
|
25
|
+
basis.push("later_correction");
|
|
26
|
+
if (delivery) {
|
|
27
|
+
basis.push("delivery_admission");
|
|
28
|
+
addReason("failed_delivery", later.deliveryAdmission.noul, "delivery_admission", "yes_probability");
|
|
29
|
+
}
|
|
30
|
+
if (regression) {
|
|
31
|
+
basis.push("regression_admission");
|
|
32
|
+
addReason("regression", later.regression.noul, "regression_admission", "yes_probability");
|
|
33
|
+
}
|
|
34
|
+
const reported = later.outcome.confidence >= 0.8 && later.outcome.choice === "reported_shortfall" && later.scopeClarification.noul <= 0.2;
|
|
35
|
+
if (reported)
|
|
36
|
+
basis.push("reported_shortfall");
|
|
37
|
+
// A narrow admission can establish the shortfall even when the broad outcome
|
|
38
|
+
// abstains. Its independently confident reason is still useful, not overridden
|
|
39
|
+
// by a hard-coded assumption that every correction means incorrect_claim.
|
|
40
|
+
if ((correction || delivery || regression || reported) && later.reason.confidence >= 0.8 && later.reason.choice !== "none_or_unclear") {
|
|
41
|
+
addReason(later.reason.choice, later.reason.confidence, "retrospective_reason");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (basis.length)
|
|
45
|
+
return { status: "reported_shortfall", basis, reasons: [...new Set(reasons)], reasonDetails,
|
|
46
|
+
reasonStatus: reasonDetails.length ? "classified" : "uncertain" };
|
|
47
|
+
if (later?.outcome.choice === "acknowledged_success" && later.outcome.confidence >= 0.8) {
|
|
48
|
+
return { status: "acknowledged_success", basis: ["human_acknowledgment"], reasons: [], reasonDetails: [], reasonStatus: "not_applicable" };
|
|
49
|
+
}
|
|
50
|
+
return { status: "unknown", basis: [], reasons: [], reasonDetails: [], reasonStatus: "not_applicable" };
|
|
51
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import type { ResponseResult } from "./response-store.js";
|
|
3
|
+
export declare const RESPONSE_REVIEW_POLICY = "response-review-v2";
|
|
4
|
+
export type ResponseReviewStatus = "pending" | "resolved" | "dismissed" | "deferred";
|
|
5
|
+
/** Operator-only tasks, intentionally not exposed as memory-curation agent tools. */
|
|
6
|
+
export declare class ResponseReviews {
|
|
7
|
+
private readonly db;
|
|
8
|
+
constructor(db: DatabaseSync);
|
|
9
|
+
sync(cohort: string, episodeId: string, inputHash: string, result: ResponseResult, now: number): void;
|
|
10
|
+
refresh(cohort: string, since: number): number;
|
|
11
|
+
reconcile(cohort: string): void;
|
|
12
|
+
list(cohort: string, id?: string): {
|
|
13
|
+
id: string;
|
|
14
|
+
episodeId: string;
|
|
15
|
+
family: string;
|
|
16
|
+
status: string;
|
|
17
|
+
evidenceStatus: string;
|
|
18
|
+
reviewerKind: import("node:sqlite").SQLOutputValue;
|
|
19
|
+
resolutionNote: import("node:sqlite").SQLOutputValue;
|
|
20
|
+
createdAt: import("node:sqlite").SQLOutputValue;
|
|
21
|
+
updatedAt: import("node:sqlite").SQLOutputValue;
|
|
22
|
+
evidence: unknown;
|
|
23
|
+
}[];
|
|
24
|
+
decide(cohort: string, id: string, status: ResponseReviewStatus, reviewer: "human" | "agent", note: string, now?: number): void;
|
|
25
|
+
annotate(occurredAt: number, kind: string, note: string): `${string}-${string}-${string}-${string}-${string}`;
|
|
26
|
+
annotations(since: number, until: number): Record<string, import("node:sqlite").SQLOutputValue>[];
|
|
27
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { responseOutcome } from "./response-outcome.js";
|
|
3
|
+
export const RESPONSE_REVIEW_POLICY = "response-review-v2";
|
|
4
|
+
/** Operator-only tasks, intentionally not exposed as memory-curation agent tools. */
|
|
5
|
+
export class ResponseReviews {
|
|
6
|
+
db;
|
|
7
|
+
constructor(db) {
|
|
8
|
+
this.db = db;
|
|
9
|
+
db.exec(`CREATE TABLE IF NOT EXISTS response_review_tasks (
|
|
10
|
+
id TEXT PRIMARY KEY, episode_id TEXT NOT NULL, family TEXT NOT NULL, human_key TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending',
|
|
11
|
+
reviewer_kind TEXT, resolution_note TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL,
|
|
12
|
+
UNIQUE(episode_id,family,human_key));
|
|
13
|
+
CREATE TABLE IF NOT EXISTS response_review_evidence (
|
|
14
|
+
task_id TEXT NOT NULL, cohort TEXT NOT NULL, input_hash TEXT NOT NULL, active INTEGER NOT NULL,
|
|
15
|
+
detail TEXT NOT NULL, PRIMARY KEY(task_id,cohort));
|
|
16
|
+
CREATE TABLE IF NOT EXISTS response_review_decisions (
|
|
17
|
+
id INTEGER PRIMARY KEY, task_id TEXT NOT NULL, status TEXT NOT NULL, reviewer_kind TEXT NOT NULL,
|
|
18
|
+
note TEXT NOT NULL, decided_at INTEGER NOT NULL);
|
|
19
|
+
CREATE TABLE IF NOT EXISTS response_review_versions (
|
|
20
|
+
cohort TEXT NOT NULL, episode_id TEXT NOT NULL, input_hash TEXT NOT NULL, policy TEXT NOT NULL,
|
|
21
|
+
PRIMARY KEY(cohort,episode_id));
|
|
22
|
+
CREATE TABLE IF NOT EXISTS response_annotations (
|
|
23
|
+
id TEXT PRIMARY KEY, occurred_at INTEGER NOT NULL, kind TEXT NOT NULL, note TEXT NOT NULL);`);
|
|
24
|
+
}
|
|
25
|
+
sync(cohort, episodeId, inputHash, result, now) {
|
|
26
|
+
this.db.prepare(`UPDATE response_review_evidence SET active=-1 WHERE cohort=? AND task_id IN
|
|
27
|
+
(SELECT id FROM response_review_tasks WHERE episode_id=?)`).run(cohort, episodeId);
|
|
28
|
+
const outcome = responseOutcome(result);
|
|
29
|
+
const f = result.feedback, intensity = f.dissatisfactionIntensity;
|
|
30
|
+
// Use probability of the relevant *group*, not the expected score or certainty
|
|
31
|
+
// of one fine-grained target. Ambiguity among agent-related targets is harmless.
|
|
32
|
+
const agentDirected = ["current_answer", "earlier_behavior", "delivery", "proactive_action", "mixed"];
|
|
33
|
+
const agentProbability = agentDirected.reduce((sum, key) => sum + f.target.probabilities[key], 0);
|
|
34
|
+
const signals = [
|
|
35
|
+
...(outcome.status === "reported_shortfall" ? [{ family: "delivery_quality", detail: outcome }] : []),
|
|
36
|
+
...(intensity && intensity.probabilities["2"] + intensity.probabilities["3"] >= 0.8 && agentProbability >= 0.8 &&
|
|
37
|
+
((f.annoyance?.noul ?? 0) >= 0.8 || (f.frustration?.noul ?? 0) >= 0.8) ?
|
|
38
|
+
[{ family: "human_experience", detail: { target: f.target, sentiment: f.sentiment, annoyance: f.annoyance,
|
|
39
|
+
frustration: f.frustration, intensity } }] : []),
|
|
40
|
+
];
|
|
41
|
+
for (const s of signals) {
|
|
42
|
+
const humanKey = result.human?.key ?? "unknown";
|
|
43
|
+
const id = createHash("sha256").update(JSON.stringify([episodeId, s.family, humanKey])).digest("hex");
|
|
44
|
+
this.db.prepare(`INSERT INTO response_review_tasks(id,episode_id,family,human_key,created_at,updated_at) VALUES(?,?,?,?,?,?)
|
|
45
|
+
ON CONFLICT(id) DO UPDATE SET updated_at=excluded.updated_at`).run(id, episodeId, s.family, humanKey, now, now);
|
|
46
|
+
this.db.prepare(`INSERT INTO response_review_evidence VALUES(?,?,?,1,?) ON CONFLICT(task_id,cohort)
|
|
47
|
+
DO UPDATE SET input_hash=excluded.input_hash,active=1,detail=excluded.detail`)
|
|
48
|
+
.run(id, cohort, inputHash, JSON.stringify({ policy: RESPONSE_REVIEW_POLICY, signals: s.detail, human: result.human ?? null, references: result.references }));
|
|
49
|
+
}
|
|
50
|
+
this.db.prepare(`INSERT INTO response_review_versions VALUES(?,?,?,?) ON CONFLICT(cohort,episode_id)
|
|
51
|
+
DO UPDATE SET input_hash=excluded.input_hash,policy=excluded.policy`).run(cohort, episodeId, inputHash, RESPONSE_REVIEW_POLICY);
|
|
52
|
+
}
|
|
53
|
+
refresh(cohort, since) {
|
|
54
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
55
|
+
try {
|
|
56
|
+
const rows = this.db.prepare(`SELECT r.id,r.input_hash,r.result FROM response_results r
|
|
57
|
+
LEFT JOIN response_review_versions v ON v.cohort=r.cohort AND v.episode_id=r.id
|
|
58
|
+
WHERE r.cohort=? AND r.active=1 AND r.status='ok' AND r.episode_at>=?
|
|
59
|
+
AND (v.policy IS NULL OR v.policy!=? OR v.input_hash!=r.input_hash) ORDER BY r.id LIMIT 1000`)
|
|
60
|
+
.all(cohort, since, RESPONSE_REVIEW_POLICY);
|
|
61
|
+
for (const row of rows)
|
|
62
|
+
this.sync(cohort, String(row.id), String(row.input_hash), JSON.parse(String(row.result)), Date.now());
|
|
63
|
+
this.db.exec("COMMIT");
|
|
64
|
+
return rows.length;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
this.db.exec("ROLLBACK");
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
reconcile(cohort) {
|
|
72
|
+
this.db.prepare(`UPDATE response_review_evidence AS e SET active=CASE WHEN EXISTS (
|
|
73
|
+
SELECT 1 FROM response_review_tasks t JOIN response_results r ON r.id=t.episode_id
|
|
74
|
+
WHERE t.id=e.task_id AND r.cohort=e.cohort AND r.input_hash=e.input_hash AND r.active=1 AND r.status='ok')
|
|
75
|
+
THEN 1 ELSE 0 END WHERE cohort=? AND active>=0`).run(cohort);
|
|
76
|
+
}
|
|
77
|
+
list(cohort, id) {
|
|
78
|
+
return this.db.prepare(`SELECT t.*,e.input_hash,e.active,e.detail FROM response_review_tasks t
|
|
79
|
+
JOIN response_review_evidence e ON e.task_id=t.id WHERE e.cohort=? ${id ? "AND t.id=?" : ""}
|
|
80
|
+
ORDER BY t.updated_at DESC,t.id LIMIT 1001`).all(cohort, ...(id ? [id] : [])).map(row => ({
|
|
81
|
+
id: String(row.id), episodeId: String(row.episode_id), family: String(row.family), status: String(row.status),
|
|
82
|
+
evidenceStatus: row.active === 1 ? "current" : row.active === -1 ? "superseded" : "stale", reviewerKind: row.reviewer_kind,
|
|
83
|
+
resolutionNote: row.resolution_note, createdAt: row.created_at, updatedAt: row.updated_at,
|
|
84
|
+
evidence: JSON.parse(String(row.detail)),
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
decide(cohort, id, status, reviewer, note, now = Date.now()) {
|
|
88
|
+
if (!["pending", "resolved", "dismissed", "deferred"].includes(status) || !["human", "agent"].includes(reviewer) ||
|
|
89
|
+
!note.trim() || note.length > 4000)
|
|
90
|
+
throw new Error("Invalid response review decision");
|
|
91
|
+
if (!this.list(cohort, id).length)
|
|
92
|
+
throw new Error("Unknown response review task in current cohort");
|
|
93
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
94
|
+
try {
|
|
95
|
+
this.db.prepare("UPDATE response_review_tasks SET status=?,reviewer_kind=?,resolution_note=?,updated_at=? WHERE id=?")
|
|
96
|
+
.run(status, reviewer, note.trim(), now, id);
|
|
97
|
+
this.db.prepare("INSERT INTO response_review_decisions(task_id,status,reviewer_kind,note,decided_at) VALUES(?,?,?,?,?)")
|
|
98
|
+
.run(id, status, reviewer, note.trim(), now);
|
|
99
|
+
this.db.exec("COMMIT");
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
this.db.exec("ROLLBACK");
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
annotate(occurredAt, kind, note) {
|
|
107
|
+
if (!Number.isFinite(occurredAt) || !["model", "prompt", "deployment", "other"].includes(kind) || !note.trim() || note.length > 4000)
|
|
108
|
+
throw new Error("Invalid response annotation");
|
|
109
|
+
const id = randomUUID();
|
|
110
|
+
this.db.prepare("INSERT INTO response_annotations VALUES(?,?,?,?)").run(id, occurredAt, kind, note.trim());
|
|
111
|
+
return id;
|
|
112
|
+
}
|
|
113
|
+
annotations(since, until) {
|
|
114
|
+
return this.db.prepare("SELECT * FROM response_annotations WHERE occurred_at>=? AND occurred_at<? ORDER BY occurred_at,id").all(since, until);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { resolveAgentDir, resolveAgentWorkspaceDir, resolveStateDir } from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
|
|
4
|
+
import { listAgentIds } from "openclaw/plugin-sdk/agent-runtime";
|
|
5
|
+
import { normalizeAgentIdStrict } from "openclaw/plugin-sdk/routing";
|
|
6
|
+
import { resolveSources } from "./sources.js";
|
|
7
|
+
import { auditResponses, responseCohort } from "./response-audit.js";
|
|
8
|
+
import { ResponseAuditStore } from "./response-store.js";
|
|
9
|
+
function dateOption(value, fallback) {
|
|
10
|
+
if (value === undefined)
|
|
11
|
+
return fallback;
|
|
12
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || !Number.isFinite(Date.parse(value)) || new Date(value).toISOString().slice(0, 10) !== value) {
|
|
13
|
+
throw new Error("Dates must be valid YYYY-MM-DD UTC dates");
|
|
14
|
+
}
|
|
15
|
+
return Date.parse(value);
|
|
16
|
+
}
|
|
17
|
+
export function registerResponseAudit(api, config) {
|
|
18
|
+
const options = (cfg, id) => {
|
|
19
|
+
const normalized = normalizeAgentIdStrict(id);
|
|
20
|
+
if (!normalized.ok || !listAgentIds(cfg).includes(normalized.value))
|
|
21
|
+
throw new Error("Unknown response-audit agent");
|
|
22
|
+
const agentId = normalized.value;
|
|
23
|
+
const state = join(resolveStateDir(), "agents", agentId, "unblock-memory");
|
|
24
|
+
return { agentId, config, databasePath: join(resolveAgentDir(cfg, agentId), "openclaw-agent.sqlite"),
|
|
25
|
+
storePath: join(state, "response-audit.sqlite"), indexPath: join(state, "index.sqlite"),
|
|
26
|
+
peoplePath: join(state, "people.sqlite"),
|
|
27
|
+
sources: resolveSources(resolveAgentWorkspaceDir(cfg, agentId), config.corpora.filter(c => c.kind === "files")
|
|
28
|
+
.filter(c => config.responseAudit.memoryCorpora.includes(c.name))) };
|
|
29
|
+
};
|
|
30
|
+
api.registerCli(({ program, config: cfg }) => {
|
|
31
|
+
const root = program.command("memory-responses").description("Operator-only response quality audits and evidence-linked trend reports");
|
|
32
|
+
root.command("audit").option("--agent <id>", "Agent id", "main").option("--dry-run", "Preview bounded coverage without inference or audit writes")
|
|
33
|
+
.action(async (opts) => {
|
|
34
|
+
const result = await auditResponses({ ...options(cfg, opts.agent), dryRun: opts.dryRun });
|
|
35
|
+
console.log(JSON.stringify(result, null, 2));
|
|
36
|
+
if (result.status === "unavailable")
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
});
|
|
39
|
+
root.command("report").option("--agent <id>", "Agent id", "main").option("--episode <id>", "Include one episode's judgments and source event references")
|
|
40
|
+
.option("--since <date>", "Inclusive UTC date YYYY-MM-DD").option("--until <date>", "Exclusive UTC date YYYY-MM-DD")
|
|
41
|
+
.option("--bucket <period>", "day or week", "week").option("--sender <id>", "Human sender ID; requires --account")
|
|
42
|
+
.option("--account <scope>", "Provider account scope; requires --sender").option("--person <id>", "Linked people-store person ID")
|
|
43
|
+
.option("--task-type <type>", "Task classification").option("--model <id>", "Agent model")
|
|
44
|
+
.action((opts) => {
|
|
45
|
+
if (!config.responseAudit.enabled) {
|
|
46
|
+
console.log(JSON.stringify({ status: "disabled" }));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const { storePath } = options(cfg, opts.agent);
|
|
50
|
+
if (!existsSync(storePath)) {
|
|
51
|
+
console.log(JSON.stringify({ status: "not_run" }));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const store = new ResponseAuditStore(storePath);
|
|
55
|
+
if (opts.bucket !== "day" && opts.bucket !== "week") {
|
|
56
|
+
store.close();
|
|
57
|
+
throw new Error("Bucket must be day or week");
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
console.log(JSON.stringify(store.report(responseCohort(config.responseAudit), dateOption(opts.since, Date.now() - config.responseAudit.lookbackDays * 86400_000), opts.episode, { until: dateOption(opts.until, Number.MAX_SAFE_INTEGER), bucket: opts.bucket, senderId: opts.sender,
|
|
61
|
+
accountScope: opts.account, personId: opts.person, taskType: opts.taskType, agentModel: opts.model }), null, 2));
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
store.close();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const withStore = (agent, fn) => {
|
|
68
|
+
if (!config.responseAudit.enabled)
|
|
69
|
+
throw new Error("Response audit is disabled");
|
|
70
|
+
const { storePath } = options(cfg, agent);
|
|
71
|
+
if (!existsSync(storePath))
|
|
72
|
+
throw new Error("Response audit has not run");
|
|
73
|
+
const store = new ResponseAuditStore(storePath);
|
|
74
|
+
try {
|
|
75
|
+
console.log(JSON.stringify(fn(store, responseCohort(config.responseAudit)), null, 2));
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
store.close();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
root.command("tasks").description("Operator-only response reviews; never automatically writes memories or preferences")
|
|
82
|
+
.option("--agent <id>", "Agent id", "main").option("--id <id>", "One task with evidence")
|
|
83
|
+
.action((opts) => withStore(opts.agent, (store, cohort) => {
|
|
84
|
+
const tasks = store.reviews.list(cohort, opts.id);
|
|
85
|
+
return { tasks: tasks.slice(0, 1000), capped: tasks.length > 1000 };
|
|
86
|
+
}));
|
|
87
|
+
root.command("review").requiredOption("--id <id>", "Task ID").requiredOption("--status <status>", "pending, resolved, dismissed or deferred")
|
|
88
|
+
.requiredOption("--reviewer <kind>", "human or agent: provenance, not an authorization grant")
|
|
89
|
+
.requiredOption("--note <text>", "Evidence-based decision; no automatic whisperer write")
|
|
90
|
+
.option("--agent <id>", "Agent id", "main")
|
|
91
|
+
.action((opts) => withStore(opts.agent, (store, cohort) => {
|
|
92
|
+
store.reviews.decide(cohort, opts.id, opts.status, opts.reviewer, opts.note);
|
|
93
|
+
return store.reviews.list(cohort, opts.id);
|
|
94
|
+
}));
|
|
95
|
+
root.command("annotate").option("--agent <id>", "Agent id", "main").requiredOption("--date <date>", "UTC date YYYY-MM-DD")
|
|
96
|
+
.requiredOption("--kind <kind>", "model, prompt, deployment or other").requiredOption("--note <text>", "Known change; correlation is not causation")
|
|
97
|
+
.action((opts) => withStore(opts.agent, store => ({ id: store.reviews.annotate(dateOption(opts.date, NaN), opts.kind, opts.note) })));
|
|
98
|
+
root.command("retry-failed").description("Reset failed stage retries without discarding successful judgments; next audit performs the work")
|
|
99
|
+
.option("--agent <id>", "Agent id", "main")
|
|
100
|
+
.action((opts) => withStore(opts.agent, (store, cohort) => store.retryFailed(cohort)));
|
|
101
|
+
}, { descriptors: [{ name: "memory-responses", description: "Audit human-agent exchanges and report quality signals", hasSubcommands: true }] });
|
|
102
|
+
if (api.registrationMode === "cli-metadata" || !config.responseAudit.enabled || !config.typesafe.enabled || !config.responseAudit.intervalMinutes)
|
|
103
|
+
return;
|
|
104
|
+
let timer, running, lifetime = new AbortController();
|
|
105
|
+
const tick = () => {
|
|
106
|
+
if (running || lifetime.signal.aborted)
|
|
107
|
+
return;
|
|
108
|
+
running = (async () => {
|
|
109
|
+
for (const id of listAgentIds(api.config)) {
|
|
110
|
+
if (lifetime.signal.aborted)
|
|
111
|
+
break;
|
|
112
|
+
try {
|
|
113
|
+
const params = options(api.config, id);
|
|
114
|
+
const store = new ResponseAuditStore(params.storePath);
|
|
115
|
+
let due;
|
|
116
|
+
try {
|
|
117
|
+
due = store.claimScheduled(Date.now(), config.responseAudit.intervalMinutes * 60_000);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
store.close();
|
|
121
|
+
}
|
|
122
|
+
if (!due)
|
|
123
|
+
continue;
|
|
124
|
+
const result = await auditResponses({ ...params, signal: lifetime.signal });
|
|
125
|
+
if (result.status === "unavailable")
|
|
126
|
+
api.logger.warn("unblock-memory response audit unavailable; inspect operator report");
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
api.logger.warn("unblock-memory response audit unavailable");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
})().catch(() => { api.logger.warn("unblock-memory response audit unavailable"); }).finally(() => { running = undefined; });
|
|
133
|
+
};
|
|
134
|
+
api.on("gateway_start", () => {
|
|
135
|
+
if (timer)
|
|
136
|
+
clearInterval(timer);
|
|
137
|
+
lifetime = new AbortController();
|
|
138
|
+
// A minute poll honors durable due times without relying on process uptime.
|
|
139
|
+
timer = setInterval(tick, 60_000);
|
|
140
|
+
timer.unref();
|
|
141
|
+
tick(); // New schedules wait one interval; overdue schedules get one bounded attempt.
|
|
142
|
+
});
|
|
143
|
+
api.on("gateway_stop", async () => {
|
|
144
|
+
if (timer)
|
|
145
|
+
clearInterval(timer);
|
|
146
|
+
timer = undefined;
|
|
147
|
+
lifetime.abort();
|
|
148
|
+
await running;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { ResponseEpisode } from "./response-episodes.js";
|
|
2
|
+
import type { ResponseAuditStore } from "./response-store.js";
|
|
3
|
+
export declare function assessResponseStages(store: ResponseAuditStore, cohort: string, e: ResponseEpisode, params: {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
timeoutMs: number;
|
|
6
|
+
signal: AbortSignal;
|
|
7
|
+
}, sentimentEnabled: boolean, usage: {
|
|
8
|
+
stageAttempts: number;
|
|
9
|
+
stageCacheHits: number;
|
|
10
|
+
}): Promise<{
|
|
11
|
+
retrospective: {
|
|
12
|
+
status: ResponseEpisode["followup"]["status"];
|
|
13
|
+
judgment: import("typebox").Static<import("typebox").TObject<{
|
|
14
|
+
answers: import("typebox").TObject<{
|
|
15
|
+
correction: import("typebox").TObject<{
|
|
16
|
+
type: import("typebox").TLiteral<"noul">;
|
|
17
|
+
noul: import("typebox").TNumber;
|
|
18
|
+
}>;
|
|
19
|
+
deliveryAdmission: import("typebox").TObject<{
|
|
20
|
+
type: import("typebox").TLiteral<"noul">;
|
|
21
|
+
noul: import("typebox").TNumber;
|
|
22
|
+
}>;
|
|
23
|
+
regression: import("typebox").TObject<{
|
|
24
|
+
type: import("typebox").TLiteral<"noul">;
|
|
25
|
+
noul: import("typebox").TNumber;
|
|
26
|
+
}>;
|
|
27
|
+
scopeClarification: import("typebox").TObject<{
|
|
28
|
+
type: import("typebox").TLiteral<"noul">;
|
|
29
|
+
noul: import("typebox").TNumber;
|
|
30
|
+
}>;
|
|
31
|
+
outcome: import("typebox").TObject<{
|
|
32
|
+
type: import("typebox").TLiteral<"choice">;
|
|
33
|
+
choice: import("typebox").TEnum<("unknown" | "reported_shortfall" | "acknowledged_success")[]>;
|
|
34
|
+
confidence: import("typebox").TNumber;
|
|
35
|
+
probabilities: import("typebox").TObject<{
|
|
36
|
+
[k: string]: import("typebox").TNumber;
|
|
37
|
+
}>;
|
|
38
|
+
}>;
|
|
39
|
+
reason: import("typebox").TObject<{
|
|
40
|
+
type: import("typebox").TLiteral<"choice">;
|
|
41
|
+
choice: import("typebox").TEnum<("none_or_unclear" | "missing_requested_work" | "unnecessary_deferral" | "regression" | "incorrect_claim" | "wrong_scope" | "failed_delivery")[]>;
|
|
42
|
+
confidence: import("typebox").TNumber;
|
|
43
|
+
probabilities: import("typebox").TObject<{
|
|
44
|
+
[k: string]: import("typebox").TNumber;
|
|
45
|
+
}>;
|
|
46
|
+
}>;
|
|
47
|
+
}>;
|
|
48
|
+
}>>["answers"] | null;
|
|
49
|
+
};
|
|
50
|
+
quality: {
|
|
51
|
+
taskType: {
|
|
52
|
+
type: "choice";
|
|
53
|
+
choice: "action" | "question" | "artifact" | "discussion" | "other";
|
|
54
|
+
confidence: number;
|
|
55
|
+
probabilities: {
|
|
56
|
+
[x: string]: number;
|
|
57
|
+
[x: number]: number;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
assessability: {
|
|
61
|
+
type: "choice";
|
|
62
|
+
choice: "assessable" | "not_assessable";
|
|
63
|
+
confidence: number;
|
|
64
|
+
probabilities: {
|
|
65
|
+
[x: string]: number;
|
|
66
|
+
[x: number]: number;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
fitAssessability: {
|
|
70
|
+
type: "choice";
|
|
71
|
+
choice: "assessable" | "not_assessable";
|
|
72
|
+
confidence: number;
|
|
73
|
+
probabilities: {
|
|
74
|
+
[x: string]: number;
|
|
75
|
+
[x: number]: number;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
underdelivery: {
|
|
79
|
+
type: "noul";
|
|
80
|
+
noul: number;
|
|
81
|
+
};
|
|
82
|
+
failureReason: {
|
|
83
|
+
type: "choice";
|
|
84
|
+
choice: "none_or_unclear" | "missing_requested_work" | "wrong_deliverable" | "missed_constraint" | "insufficient_answer_depth" | "unnecessary_deferral";
|
|
85
|
+
confidence: number;
|
|
86
|
+
probabilities: {
|
|
87
|
+
[x: string]: number;
|
|
88
|
+
[x: number]: number;
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
fulfillment: {
|
|
92
|
+
type: "score";
|
|
93
|
+
confidence: number;
|
|
94
|
+
probabilities: {
|
|
95
|
+
"0": number;
|
|
96
|
+
"1": number;
|
|
97
|
+
"2": number;
|
|
98
|
+
"3": number;
|
|
99
|
+
};
|
|
100
|
+
score: number;
|
|
101
|
+
};
|
|
102
|
+
deliverableFit: {
|
|
103
|
+
type: "score";
|
|
104
|
+
confidence: number;
|
|
105
|
+
probabilities: {
|
|
106
|
+
"0": number;
|
|
107
|
+
"1": number;
|
|
108
|
+
"2": number;
|
|
109
|
+
"3": number;
|
|
110
|
+
};
|
|
111
|
+
score: number;
|
|
112
|
+
};
|
|
113
|
+
consistency: {
|
|
114
|
+
type: "choice";
|
|
115
|
+
choice: "not_assessable" | "consistent" | "contradicted";
|
|
116
|
+
confidence: number;
|
|
117
|
+
probabilities: {
|
|
118
|
+
[x: string]: number;
|
|
119
|
+
[x: number]: number;
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
feedback: {
|
|
124
|
+
feedbackType: {
|
|
125
|
+
type: "choice";
|
|
126
|
+
choice: "mixed" | "unrelated" | "unclear" | "acceptance" | "correction" | "continuation";
|
|
127
|
+
confidence: number;
|
|
128
|
+
probabilities: {
|
|
129
|
+
[x: string]: number;
|
|
130
|
+
[x: number]: number;
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
target: {
|
|
134
|
+
type: "choice";
|
|
135
|
+
choice: "delivery" | "mixed" | "unclear" | "current_answer" | "earlier_behavior" | "proactive_action" | "external" | "new_work";
|
|
136
|
+
confidence: number;
|
|
137
|
+
probabilities: {
|
|
138
|
+
[x: string]: number;
|
|
139
|
+
[x: number]: number;
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
avoidableRework: {
|
|
143
|
+
type: "noul";
|
|
144
|
+
noul: number;
|
|
145
|
+
};
|
|
146
|
+
repeatedConstraint: {
|
|
147
|
+
type: "noul";
|
|
148
|
+
noul: number;
|
|
149
|
+
};
|
|
150
|
+
memoryGap: {
|
|
151
|
+
type: "noul";
|
|
152
|
+
noul: number;
|
|
153
|
+
};
|
|
154
|
+
} & Partial<{
|
|
155
|
+
sentiment: {
|
|
156
|
+
type: "choice";
|
|
157
|
+
choice: "satisfied" | "dissatisfied" | "mixed" | "neutral" | "unrelated" | "unclear";
|
|
158
|
+
confidence: number;
|
|
159
|
+
probabilities: {
|
|
160
|
+
[x: string]: number;
|
|
161
|
+
[x: number]: number;
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
annoyance: {
|
|
165
|
+
type: "noul";
|
|
166
|
+
noul: number;
|
|
167
|
+
};
|
|
168
|
+
frustration: {
|
|
169
|
+
type: "noul";
|
|
170
|
+
noul: number;
|
|
171
|
+
};
|
|
172
|
+
dissatisfactionIntensity: {
|
|
173
|
+
type: "score";
|
|
174
|
+
confidence: number;
|
|
175
|
+
probabilities: {
|
|
176
|
+
"0": number;
|
|
177
|
+
"1": number;
|
|
178
|
+
"2": number;
|
|
179
|
+
"3": number;
|
|
180
|
+
};
|
|
181
|
+
score: number;
|
|
182
|
+
};
|
|
183
|
+
}>;
|
|
184
|
+
}>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { RESPONSE_EXTRACTOR_VERSION } from "./response-episodes.js";
|
|
3
|
+
import { judgeResponse, judgeResponseFollowup, RESPONSE_STAGE_VERSIONS } from "./response-judge.js";
|
|
4
|
+
import { TYPESAFE_REVIEW_MODEL } from "./typesafe-review.js";
|
|
5
|
+
export async function assessResponseStages(store, cohort, e, params, sentimentEnabled, usage) {
|
|
6
|
+
const original = { before: e.before, request: e.request, answer: e.answer, contextLimited: e.contextLimited };
|
|
7
|
+
const feedback = { ...original, feedback: e.feedback };
|
|
8
|
+
const evidence = { quality: original, feedback, sentiment: feedback, retrospective: { ...feedback, followup: e.followup } };
|
|
9
|
+
const keys = Object.fromEntries(Object.keys(evidence).map(stage => [stage,
|
|
10
|
+
createHash("sha256").update(JSON.stringify([RESPONSE_EXTRACTOR_VERSION, TYPESAFE_REVIEW_MODEL, RESPONSE_STAGE_VERSIONS[stage],
|
|
11
|
+
e.id, e.session, e.senderId, e.thread, evidence[stage]])).digest("hex"),
|
|
12
|
+
]));
|
|
13
|
+
const cache = {
|
|
14
|
+
quality: store.stage(cohort, e, "quality", keys.quality),
|
|
15
|
+
feedback: store.stage(cohort, e, "feedback", keys.feedback),
|
|
16
|
+
sentiment: sentimentEnabled ? store.stage(cohort, e, "sentiment", keys.sentiment) : undefined,
|
|
17
|
+
begin(stages) {
|
|
18
|
+
params.signal.throwIfAborted();
|
|
19
|
+
store.stageBegin(stages.map(stage => keys[stage]), Date.now());
|
|
20
|
+
usage.stageAttempts += stages.length;
|
|
21
|
+
},
|
|
22
|
+
save(stage, result) {
|
|
23
|
+
params.signal.throwIfAborted();
|
|
24
|
+
store.stageSave(keys[stage], result, Date.now());
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
usage.stageCacheHits += Number(!!cache.quality) + Number(!!cache.feedback) + Number(!!cache.sentiment);
|
|
28
|
+
const judgment = await judgeResponse(e, params, sentimentEnabled, cache);
|
|
29
|
+
let retrospective = store.stage(cohort, e, "retrospective", keys.retrospective);
|
|
30
|
+
if (retrospective)
|
|
31
|
+
usage.stageCacheHits++;
|
|
32
|
+
if (!retrospective) {
|
|
33
|
+
cache.begin(["retrospective"]);
|
|
34
|
+
retrospective = await judgeResponseFollowup(e, params);
|
|
35
|
+
cache.save("retrospective", retrospective);
|
|
36
|
+
}
|
|
37
|
+
return { ...judgment, retrospective };
|
|
38
|
+
}
|