@unblocklabs/unblock-memory 0.3.27 → 0.3.28
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/src/cluster-review.js +1 -1
- package/dist/src/config.d.ts +4 -0
- package/dist/src/config.js +14 -1
- package/dist/src/diagnostics.d.ts +13 -4
- package/dist/src/diagnostics.js +5 -0
- package/dist/src/manager.d.ts +3 -1
- package/dist/src/manager.js +31 -0
- package/dist/src/memory-whisperer.d.ts +2 -1
- package/dist/src/memory-whisperer.js +240 -112
- package/dist/src/mlx-query.d.ts +22 -0
- package/dist/src/mlx-query.js +155 -0
- package/dist/src/people-dossier-review.js +1 -1
- package/dist/src/people-hooks.d.ts +1 -1
- package/dist/src/people-hooks.js +13 -7
- package/dist/src/people-primer-tool.js +1 -1
- package/dist/src/people-primer.js +3 -2
- package/dist/src/plugin.js +10 -5
- package/dist/src/quality-audit.js +86 -81
- package/dist/src/response-audit.js +1 -1
- package/dist/src/response-judge.d.ts +1 -1
- package/dist/src/response-judge.js +18 -15
- package/dist/src/response-stages.d.ts +8 -8
- package/dist/src/response-text.d.ts +5 -0
- package/dist/src/response-text.js +23 -1
- package/dist/src/retrieval-telemetry.d.ts +2 -2
- package/dist/src/retrieval-telemetry.js +1 -1
- package/dist/src/review-tools.js +1 -1
- package/dist/src/skill-whisperer.d.ts +1 -1
- package/dist/src/skill-whisperer.js +76 -63
- package/dist/src/training-candidates.d.ts +9 -1
- package/dist/src/training-candidates.js +12 -7
- package/dist/src/training-gate.js +3 -3
- package/dist/src/training-input.js +2 -23
- package/dist/src/training-judge.js +3 -3
- package/dist/src/training-queries.js +5 -4
- package/dist/src/training.js +5 -4
- package/dist/src/typesafe-client.d.ts +15 -0
- package/dist/src/typesafe-client.js +75 -0
- package/dist/src/typesafe-review.d.ts +2 -6
- package/dist/src/typesafe-review.js +49 -55
- package/dist/src/typesafe.d.ts +3 -7
- package/dist/src/typesafe.js +108 -163
- package/dist/src/whisperer-prompt.d.ts +3 -0
- package/dist/src/whisperer-prompt.js +21 -0
- package/docs/configuration.md +12 -2
- package/docs/peoplesql.md +3 -1
- package/docs/response-audit.md +4 -1
- package/docs/retrieval.md +128 -20
- package/openclaw.plugin.json +13 -4
- package/package.json +2 -1
- package/src/mlx-query-worker.py +104 -0
- package/dist/src/typesafe-transport.d.ts +0 -10
- package/dist/src/typesafe-transport.js +0 -26
|
@@ -50,8 +50,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
|
|
|
50
50
|
quality: {
|
|
51
51
|
taskType: {
|
|
52
52
|
type: "choice";
|
|
53
|
-
choice: "action" | "question" | "artifact" | "discussion" | "other";
|
|
54
53
|
confidence: number;
|
|
54
|
+
choice: "action" | "question" | "artifact" | "discussion" | "other";
|
|
55
55
|
probabilities: {
|
|
56
56
|
[x: string]: number;
|
|
57
57
|
[x: number]: number;
|
|
@@ -59,8 +59,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
|
|
|
59
59
|
};
|
|
60
60
|
assessability: {
|
|
61
61
|
type: "choice";
|
|
62
|
-
choice: "assessable" | "not_assessable";
|
|
63
62
|
confidence: number;
|
|
63
|
+
choice: "assessable" | "not_assessable";
|
|
64
64
|
probabilities: {
|
|
65
65
|
[x: string]: number;
|
|
66
66
|
[x: number]: number;
|
|
@@ -68,8 +68,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
|
|
|
68
68
|
};
|
|
69
69
|
fitAssessability: {
|
|
70
70
|
type: "choice";
|
|
71
|
-
choice: "assessable" | "not_assessable";
|
|
72
71
|
confidence: number;
|
|
72
|
+
choice: "assessable" | "not_assessable";
|
|
73
73
|
probabilities: {
|
|
74
74
|
[x: string]: number;
|
|
75
75
|
[x: number]: number;
|
|
@@ -81,8 +81,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
|
|
|
81
81
|
};
|
|
82
82
|
failureReason: {
|
|
83
83
|
type: "choice";
|
|
84
|
-
choice: "none_or_unclear" | "missing_requested_work" | "wrong_deliverable" | "missed_constraint" | "insufficient_answer_depth" | "unnecessary_deferral";
|
|
85
84
|
confidence: number;
|
|
85
|
+
choice: "none_or_unclear" | "missing_requested_work" | "wrong_deliverable" | "missed_constraint" | "insufficient_answer_depth" | "unnecessary_deferral";
|
|
86
86
|
probabilities: {
|
|
87
87
|
[x: string]: number;
|
|
88
88
|
[x: number]: number;
|
|
@@ -112,8 +112,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
|
|
|
112
112
|
};
|
|
113
113
|
consistency: {
|
|
114
114
|
type: "choice";
|
|
115
|
-
choice: "not_assessable" | "consistent" | "contradicted";
|
|
116
115
|
confidence: number;
|
|
116
|
+
choice: "not_assessable" | "consistent" | "contradicted";
|
|
117
117
|
probabilities: {
|
|
118
118
|
[x: string]: number;
|
|
119
119
|
[x: number]: number;
|
|
@@ -123,8 +123,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
|
|
|
123
123
|
feedback: {
|
|
124
124
|
feedbackType: {
|
|
125
125
|
type: "choice";
|
|
126
|
-
choice: "mixed" | "unrelated" | "unclear" | "acceptance" | "correction" | "continuation";
|
|
127
126
|
confidence: number;
|
|
127
|
+
choice: "mixed" | "unrelated" | "unclear" | "acceptance" | "correction" | "continuation";
|
|
128
128
|
probabilities: {
|
|
129
129
|
[x: string]: number;
|
|
130
130
|
[x: number]: number;
|
|
@@ -132,8 +132,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
|
|
|
132
132
|
};
|
|
133
133
|
target: {
|
|
134
134
|
type: "choice";
|
|
135
|
-
choice: "delivery" | "mixed" | "unclear" | "current_answer" | "earlier_behavior" | "proactive_action" | "external" | "new_work";
|
|
136
135
|
confidence: number;
|
|
136
|
+
choice: "delivery" | "mixed" | "unclear" | "current_answer" | "earlier_behavior" | "proactive_action" | "external" | "new_work";
|
|
137
137
|
probabilities: {
|
|
138
138
|
[x: string]: number;
|
|
139
139
|
[x: number]: number;
|
|
@@ -154,8 +154,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
|
|
|
154
154
|
} & Partial<{
|
|
155
155
|
sentiment: {
|
|
156
156
|
type: "choice";
|
|
157
|
-
choice: "satisfied" | "dissatisfied" | "mixed" | "neutral" | "unrelated" | "unclear";
|
|
158
157
|
confidence: number;
|
|
158
|
+
choice: "satisfied" | "dissatisfied" | "mixed" | "neutral" | "unrelated" | "unclear";
|
|
159
159
|
probabilities: {
|
|
160
160
|
[x: string]: number;
|
|
161
161
|
[x: number]: number;
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/** Infer legacy transport sender metadata only to unwrap visible conversation text. */
|
|
2
|
+
export declare function conversationUserText(raw: string, sender?: unknown): {
|
|
3
|
+
text: string;
|
|
4
|
+
contextLimited: boolean;
|
|
5
|
+
} | undefined;
|
|
1
6
|
/** Parse only recognized transport envelopes. Ambiguous wrappers are excluded,
|
|
2
7
|
* never flattened into a human's request or used to approve embedded speakers. */
|
|
3
8
|
export declare function responseUserText(input: string, senderId: string): {
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/** Infer legacy transport sender metadata only to unwrap visible conversation text. */
|
|
2
|
+
export function conversationUserText(raw, sender) {
|
|
3
|
+
let senderId = typeof sender === "string" ? sender : undefined;
|
|
4
|
+
if (senderId === undefined) {
|
|
5
|
+
const header = /^Conversation info: ⟦openclaw:ctx⟧\r?\n```json\r?\n([^]*?)\r?\n```\r?\n/.exec(raw.trim());
|
|
6
|
+
if (header) {
|
|
7
|
+
let metadata;
|
|
8
|
+
try {
|
|
9
|
+
metadata = JSON.parse(header[1]);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (metadata && typeof metadata === "object" && "sender" in metadata && metadata.sender &&
|
|
15
|
+
typeof metadata.sender === "object" && "id" in metadata.sender && typeof metadata.sender.id === "string") {
|
|
16
|
+
senderId = metadata.sender.id;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
senderId ??= /^From: [^\r\n]+ \(([^()\r\n]+)\)\r?\n/.exec(raw.trim())?.[1];
|
|
20
|
+
}
|
|
21
|
+
return responseUserText(raw, senderId ?? "");
|
|
22
|
+
}
|
|
1
23
|
/** Parse only recognized transport envelopes. Ambiguous wrappers are excluded,
|
|
2
24
|
* never flattened into a human's request or used to approve embedded speakers. */
|
|
3
25
|
export function responseUserText(input, senderId) {
|
|
@@ -15,7 +37,7 @@ export function responseUserText(input, senderId) {
|
|
|
15
37
|
if (!meta || meta.sender?.id !== senderId || typeof meta.sender.name !== "string")
|
|
16
38
|
return undefined;
|
|
17
39
|
const rest = text.slice(header[0].length);
|
|
18
|
-
const markers = [...rest.matchAll(/^System: \[[^\]\r\n]+\] Slack message in [^\r\n]
|
|
40
|
+
const markers = [...rest.matchAll(/^System: \[[^\]\r\n]+\] Slack (?:message in [^\r\n]+|DM) from ([^\r\n]+)\r?\n\r?\n/gm)];
|
|
19
41
|
if (markers.length !== 1 || markers[0][1] !== meta.sender.name)
|
|
20
42
|
return undefined;
|
|
21
43
|
const marker = markers[0];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
type RetrievalOperation = "vector" | "lexical" | "memoryWhisperer";
|
|
2
2
|
type RetrievalOutcome = "ok" | "empty" | "failed" | "cancelled" | "timed_out" | "skipped";
|
|
3
|
-
declare const fields: readonly ["elapsedMs", "retrievalMs", "judgeMs", "candidates", "eligible", "results", "contextChars"];
|
|
3
|
+
declare const fields: readonly ["elapsedMs", "retrievalMs", "judgeMs", "gateMs", "generationMs", "candidates", "eligible", "results", "contextChars"];
|
|
4
4
|
type Field = typeof fields[number];
|
|
5
5
|
export type RetrievalObservation = {
|
|
6
6
|
outcome: RetrievalOutcome;
|
|
@@ -16,11 +16,11 @@ export declare class RetrievalTelemetry {
|
|
|
16
16
|
[k: string]: {
|
|
17
17
|
calls: number;
|
|
18
18
|
outcomes: {
|
|
19
|
+
cancelled?: number | undefined;
|
|
19
20
|
ok?: number | undefined;
|
|
20
21
|
skipped?: number | undefined;
|
|
21
22
|
failed?: number | undefined;
|
|
22
23
|
empty?: number | undefined;
|
|
23
|
-
cancelled?: number | undefined;
|
|
24
24
|
timed_out?: number | undefined;
|
|
25
25
|
};
|
|
26
26
|
measurements: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const fields = ["elapsedMs", "retrievalMs", "judgeMs", "candidates", "eligible", "results", "contextChars"];
|
|
1
|
+
const fields = ["elapsedMs", "retrievalMs", "judgeMs", "gateMs", "generationMs", "candidates", "eligible", "results", "contextChars"];
|
|
2
2
|
const recentLimit = 256;
|
|
3
3
|
const boundedAdd = (left, right) => Math.min(Number.MAX_SAFE_INTEGER, left + right);
|
|
4
4
|
/** Only fixed operation/outcome names and nonnegative numbers enter this store. */
|
package/dist/src/review-tools.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Type } from "typebox";
|
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
import { jsonResult } from "openclaw/plugin-sdk/agent-runtime";
|
|
4
4
|
import { getContext } from "./tool-context.js";
|
|
5
|
-
import { resolveTypeSafeApiKey } from "./typesafe.js";
|
|
5
|
+
import { resolveTypeSafeApiKey } from "./typesafe-client.js";
|
|
6
6
|
import { abortable } from "./abortable.js";
|
|
7
7
|
const claimParameters = Type.Object({
|
|
8
8
|
claim: Type.String({ pattern: "\\S", maxLength: 2000 }),
|
|
@@ -13,5 +13,5 @@ type SkillWhispererRuntime = {
|
|
|
13
13
|
}, path: string): string | undefined;
|
|
14
14
|
};
|
|
15
15
|
export declare function buildSkillWhispererQuery(prompt: string, messages: readonly unknown[], historyMessages: number): string;
|
|
16
|
-
export declare function registerSkillWhisperer(api: OpenClawPluginApi, runtime: SkillWhispererRuntime, config: UnblockMemoryConfig["skillWhisperer"], typesafe: UnblockMemoryConfig["typesafe"], diagnostics?: WhispererDiagnostics):
|
|
16
|
+
export declare function registerSkillWhisperer(api: OpenClawPluginApi, runtime: SkillWhispererRuntime, config: UnblockMemoryConfig["skillWhisperer"], typesafe: UnblockMemoryConfig["typesafe"], diagnostics?: WhispererDiagnostics): Parameters<typeof api.on<"before_prompt_build">>[1] | undefined;
|
|
17
17
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { selectTypeSafeSkill } from "./typesafe.js";
|
|
3
|
+
import { resolveTypeSafeApiKey, TypeSafeRequestError } from "./typesafe-client.js";
|
|
3
4
|
import { messageText } from "./whisperer-context.js";
|
|
4
5
|
const CANDIDATE_LIMIT = 10;
|
|
5
6
|
const MAX_QUERY_CHARS = 12_000;
|
|
@@ -55,78 +56,89 @@ export function registerSkillWhisperer(api, runtime, config, typesafe, diagnosti
|
|
|
55
56
|
return state;
|
|
56
57
|
};
|
|
57
58
|
const active = (agentId) => ({ cfg: api.config, agentId });
|
|
58
|
-
|
|
59
|
+
const beforePrompt = async (event, context) => {
|
|
60
|
+
const { agentId, runId } = context;
|
|
59
61
|
const scope = sessionScope(context);
|
|
60
|
-
if (context.trigger !== "user" || !scope || !
|
|
62
|
+
if (context.trigger !== "user" || !scope || !runId || !agentId)
|
|
61
63
|
return;
|
|
62
64
|
const state = stateFor(scope);
|
|
63
|
-
if (state.lastRunId ===
|
|
64
|
-
return;
|
|
65
|
-
state.lastRunId =
|
|
65
|
+
if (state.lastRunId === runId)
|
|
66
|
+
return state.result;
|
|
67
|
+
state.lastRunId = runId;
|
|
66
68
|
state.turn += 1;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (!resolved) {
|
|
79
|
-
diagnostics?.record(context.agentId, "skill", "no_candidates");
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
if (apiKey) {
|
|
83
|
-
const shortlist = resolvedCandidates.slice(0, TYPESAFE_CANDIDATE_LIMIT);
|
|
84
|
-
const selectedIndex = await selectTypeSafeSkill({
|
|
85
|
-
apiKey, timeoutMs: typesafe.timeoutMs,
|
|
86
|
-
...typeSafeConversation(event.prompt, event.messages, config.historyMessages),
|
|
87
|
-
candidates: shortlist.map(({ candidate }) => candidate),
|
|
69
|
+
// Reuse even an in-flight result when the host rebuilds this run's prompt.
|
|
70
|
+
const run = async () => {
|
|
71
|
+
try {
|
|
72
|
+
const runtimeParams = active(agentId);
|
|
73
|
+
const apiKey = await resolveTypeSafeApiKey(typesafe);
|
|
74
|
+
if (!apiKey)
|
|
75
|
+
diagnostics?.record(agentId, "skill", typesafe.enabled ? "missing_key" : "typesafe_disabled");
|
|
76
|
+
const candidates = await runtime.searchSkills(runtimeParams, buildSkillWhispererQuery(event.prompt, event.messages, config.historyMessages), apiKey ? -1 : config.minScore, CANDIDATE_LIMIT);
|
|
77
|
+
const resolvedCandidates = candidates.flatMap((candidate) => {
|
|
78
|
+
const canonicalPath = runtime.resolveSkillPath(runtimeParams, candidate.path);
|
|
79
|
+
return canonicalPath ? [{ candidate, canonicalPath }] : [];
|
|
88
80
|
});
|
|
89
|
-
|
|
90
|
-
|
|
81
|
+
let resolved = resolvedCandidates[0];
|
|
82
|
+
if (!resolved) {
|
|
83
|
+
diagnostics?.record(agentId, "skill", "no_candidates");
|
|
91
84
|
return;
|
|
92
85
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
86
|
+
if (apiKey) {
|
|
87
|
+
const shortlist = resolvedCandidates.slice(0, TYPESAFE_CANDIDATE_LIMIT);
|
|
88
|
+
const selectedIndex = await selectTypeSafeSkill({
|
|
89
|
+
apiKey, timeoutMs: typesafe.timeoutMs,
|
|
90
|
+
...typeSafeConversation(event.prompt, event.messages, config.historyMessages),
|
|
91
|
+
candidates: shortlist.map(({ candidate }) => candidate),
|
|
92
|
+
onCandidateFailure: (candidateIndex, error) => {
|
|
93
|
+
diagnostics?.record(agentId, "skill", "judge_candidate_failed");
|
|
94
|
+
api.logger.warn("unblock-memory skill_whisperer " + JSON.stringify({ event: "candidate_failed",
|
|
95
|
+
agentId, runId, candidateIndex, errorCode: error instanceof TypeSafeRequestError ? error.code : "unexpected",
|
|
96
|
+
httpStatus: error instanceof TypeSafeRequestError ? error.status : undefined }));
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
if (selectedIndex === undefined) {
|
|
100
|
+
diagnostics?.record(agentId, "skill", "rejected");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
resolved = shortlist[selectedIndex];
|
|
104
|
+
}
|
|
105
|
+
else if (resolved && resolved.candidate.score < config.minScore) {
|
|
106
|
+
diagnostics?.record(agentId, "skill", "rejected");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!resolved)
|
|
110
|
+
return;
|
|
111
|
+
// A selection completing after session teardown must not resurrect its hint.
|
|
112
|
+
if (sessions.get(scope) !== state || state.lastRunId !== runId) {
|
|
113
|
+
diagnostics?.record(agentId, "skill", "cancelled");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const { candidate: selected, canonicalPath } = resolved;
|
|
117
|
+
if (apiKey && runtime.resolveSkillPath(runtimeParams, selected.path) !== canonicalPath)
|
|
118
|
+
return;
|
|
119
|
+
const previous = state.skills.get(canonicalPath);
|
|
120
|
+
const lastSeen = Math.max(previous?.suggested ?? -Infinity, previous?.opened ?? -Infinity);
|
|
121
|
+
if (state.turn - lastSeen <= config.cooldownTurns) {
|
|
122
|
+
diagnostics?.record(agentId, "skill", "cooldown");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const history = state.skills.get(canonicalPath) ?? {};
|
|
126
|
+
history.suggested = state.turn;
|
|
127
|
+
state.skills.set(canonicalPath, history);
|
|
128
|
+
diagnostics?.record(agentId, "skill", "emitted");
|
|
129
|
+
return {
|
|
130
|
+
appendContext: `<skill>This skill may be relevant: ${JSON.stringify(selected.path).replace(/</gu, "\\u003c").replace(/>/gu, "\\u003e")}</skill>`,
|
|
131
|
+
};
|
|
105
132
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const previous = state.skills.get(canonicalPath);
|
|
110
|
-
const lastSeen = Math.max(previous?.suggested ?? -Infinity, previous?.opened ?? -Infinity);
|
|
111
|
-
if (state.turn - lastSeen <= config.cooldownTurns) {
|
|
112
|
-
diagnostics?.record(context.agentId, "skill", "cooldown");
|
|
133
|
+
catch (error) {
|
|
134
|
+
diagnostics?.record(agentId, "skill", error instanceof TypeSafeRequestError && error.code === "timeout" ? "timed_out" : "failed");
|
|
135
|
+
api.logger.warn("unblock-memory skill whisperer failed; no hint emitted");
|
|
113
136
|
return;
|
|
114
137
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
prependContext: `A potentially relevant skill is available: ${JSON.stringify(selected.name)} ` +
|
|
121
|
-
`at ${JSON.stringify(selected.path)}. Check it before proceeding if applicable.`,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
125
|
-
diagnostics?.record(context.agentId, "skill", error instanceof Error && error.message === "TypeSafe selection timed out" ? "timed_out" : "failed");
|
|
126
|
-
api.logger.warn("unblock-memory skill whisperer failed; no hint emitted");
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
});
|
|
138
|
+
};
|
|
139
|
+
state.result = run();
|
|
140
|
+
return state.result;
|
|
141
|
+
};
|
|
130
142
|
api.on("after_tool_call", (event, context) => {
|
|
131
143
|
if (event.toolName !== "read" || event.error ||
|
|
132
144
|
(isRecord(event.result) && event.result.isError === true) || !context.agentId)
|
|
@@ -155,4 +167,5 @@ export function registerSkillWhisperer(api, runtime, config, typesafe, diagnosti
|
|
|
155
167
|
if (context.sessionKey)
|
|
156
168
|
sessions.delete(context.sessionKey);
|
|
157
169
|
});
|
|
170
|
+
return beforePrompt;
|
|
158
171
|
}
|
|
@@ -8,6 +8,14 @@ type Candidate = {
|
|
|
8
8
|
explain: {
|
|
9
9
|
methods: string[];
|
|
10
10
|
};
|
|
11
|
+
vector?: {
|
|
12
|
+
score: number;
|
|
13
|
+
rank: number;
|
|
14
|
+
};
|
|
15
|
+
bm25?: {
|
|
16
|
+
score: number;
|
|
17
|
+
rank: number;
|
|
18
|
+
};
|
|
11
19
|
};
|
|
12
|
-
export declare function trainingCandidates(qmd: QMDStore, query: string, collection: string, intent: string): Promise<Candidate[]>;
|
|
20
|
+
export declare function trainingCandidates(qmd: QMDStore, query: string, collection: string | string[], intent: string, signal?: AbortSignal): Promise<Candidate[]>;
|
|
13
21
|
export {};
|
|
@@ -26,14 +26,15 @@ function lexicalChunk(chunks, body, highlighted, marker, intent) {
|
|
|
26
26
|
return { chunk, matches, intentMatches: intentTerms.filter(term => lower.includes(term)).length };
|
|
27
27
|
}).sort((a, b) => b.matches - a.matches || b.intentMatches - a.intentMatches || a.chunk.pos - b.chunk.pos)[0]?.chunk;
|
|
28
28
|
}
|
|
29
|
-
export async function trainingCandidates(qmd, query, collection, intent) {
|
|
29
|
+
export async function trainingCandidates(qmd, query, collection, intent, signal) {
|
|
30
|
+
signal?.throwIfAborted();
|
|
30
31
|
if (!query.trim() || query.length > 12_000)
|
|
31
32
|
throw new Error("Invalid training query");
|
|
32
33
|
// QMD exposes its store but not these chunk helpers at the package root.
|
|
33
34
|
// Resolve relative to its installed SDK, never a global QMD or modified copy.
|
|
34
35
|
const chunksApi = await import(new URL("./store.js", import.meta.resolve("@unblocklabs/qmd")).href);
|
|
35
36
|
const candidates = new Map();
|
|
36
|
-
const add = (hit, method, rank) => {
|
|
37
|
+
const add = (hit, method, rank, rawScore) => {
|
|
37
38
|
if (!hit.bestChunk.trim() || hit.bestChunk.length > 12_000)
|
|
38
39
|
return;
|
|
39
40
|
const key = JSON.stringify([hit.file, hit.bestChunk.trim()]), existing = candidates.get(key);
|
|
@@ -41,16 +42,19 @@ export async function trainingCandidates(qmd, query, collection, intent) {
|
|
|
41
42
|
if (!existing.explain.methods.includes(method))
|
|
42
43
|
existing.explain.methods.push(method);
|
|
43
44
|
existing.score = Math.max(existing.score, 1 / (rank + 1));
|
|
45
|
+
existing[method] ??= { score: rawScore, rank: rank + 1 };
|
|
44
46
|
}
|
|
45
47
|
else
|
|
46
|
-
candidates.set(key, { ...hit, score: 1 / (rank + 1), explain: { methods: [method] }
|
|
48
|
+
candidates.set(key, { ...hit, score: 1 / (rank + 1), explain: { methods: [method] },
|
|
49
|
+
[method]: { score: rawScore, rank: rank + 1 } });
|
|
47
50
|
};
|
|
48
51
|
const vectors = await qmd.searchVector(query, { limit: 10, collection });
|
|
52
|
+
signal?.throwIfAborted();
|
|
49
53
|
for (const [rank, hit] of vectors.entries()) {
|
|
50
54
|
const pos = hit.chunkPos, len = hit.chunkLen, body = hit.body ?? "";
|
|
51
55
|
if (pos === undefined || len === undefined || pos < 0 || len <= 0 || pos + len > body.length)
|
|
52
56
|
continue;
|
|
53
|
-
add({ file: hit.filepath, body, bestChunk: body.slice(pos, pos + len), bestChunkPos: pos }, "vector", rank);
|
|
57
|
+
add({ file: hit.filepath, body, bestChunk: body.slice(pos, pos + len), bestChunkPos: pos }, "vector", rank, hit.score);
|
|
54
58
|
}
|
|
55
59
|
const expression = queryTerms(query).map(term => `"${chunksApi.normalizeCjkForFTS(term).trim()}"`).join(" OR ");
|
|
56
60
|
if (expression) {
|
|
@@ -58,9 +62,10 @@ export async function trainingCandidates(qmd, query, collection, intent) {
|
|
|
58
62
|
const rows = qmd.internal.db.prepare(`SELECT d.collection,d.path,d.hash,c.doc,
|
|
59
63
|
bm25(documents_fts,1.5,4.0,1.0) AS rank, highlight(documents_fts,2,?,?) AS highlighted
|
|
60
64
|
FROM documents_fts JOIN documents d ON d.id=documents_fts.rowid JOIN content c ON c.hash=d.hash
|
|
61
|
-
WHERE documents_fts MATCH ? AND d.active=1 AND d.collection
|
|
62
|
-
ORDER BY rank,d.collection,d.path LIMIT 10`).all(marker, marker, expression, collection);
|
|
65
|
+
WHERE documents_fts MATCH ? AND d.active=1 AND d.collection IN (SELECT value FROM json_each(?))
|
|
66
|
+
ORDER BY rank,d.collection,d.path LIMIT 10`).all(marker, marker, expression, JSON.stringify(typeof collection === "string" ? [collection] : collection));
|
|
63
67
|
for (const [rank, row] of rows.entries()) {
|
|
68
|
+
signal?.throwIfAborted();
|
|
64
69
|
const file = `qmd://${row.collection}/${row.path}`;
|
|
65
70
|
const stored = chunksApi.getStoredChunkSpans(qmd.internal.db, row.hash)
|
|
66
71
|
.filter(span => span.pos >= 0 && span.chunk_len > 0 && span.pos + span.chunk_len <= row.doc.length)
|
|
@@ -68,7 +73,7 @@ export async function trainingCandidates(qmd, query, collection, intent) {
|
|
|
68
73
|
const chunks = stored.length ? stored : await chunksApi.chunkDocumentAsync(row.doc, undefined, undefined, undefined, file);
|
|
69
74
|
const selected = lexicalChunk(chunks, row.doc, row.highlighted, marker, intent);
|
|
70
75
|
if (selected)
|
|
71
|
-
add({ file, body: row.doc, bestChunk: selected.text, bestChunkPos: selected.pos }, "bm25", rank);
|
|
76
|
+
add({ file, body: row.doc, bestChunk: selected.text, bestChunkPos: selected.pos }, "bm25", rank, row.rank);
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
79
|
return [...candidates.values()].sort((a, b) => b.score - a.score);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
|
-
import {
|
|
3
|
+
import { requestTypeSafe, TYPESAFE_MODEL, TypeSafeRequestError } from "./typesafe-client.js";
|
|
4
4
|
export const TRAINING_GATE_VERSION = "historical-recall-v1";
|
|
5
5
|
export const TRAINING_GATE_MODEL = TYPESAFE_MODEL;
|
|
6
6
|
export const TRAINING_GATE_THRESHOLD = 0.7;
|
|
@@ -25,9 +25,9 @@ const resultSchema = Type.Object({
|
|
|
25
25
|
usage: Type.Object({ input_tokens: Type.Integer({ minimum: 0 }), output_tokens: Type.Integer({ minimum: 0 }) }),
|
|
26
26
|
});
|
|
27
27
|
export async function judgeTrainingInput(input, apiKey, signal) {
|
|
28
|
-
const result = await
|
|
28
|
+
const result = await requestTypeSafe({ apiKey, signal }, input, TRAINING_GATE_QUESTIONS);
|
|
29
29
|
if (!Value.Check(resultSchema, result) || !Number.isFinite(result.answers.recall_needed.noul)) {
|
|
30
|
-
throw new
|
|
30
|
+
throw new TypeSafeRequestError("Invalid training gate response", "invalid_response");
|
|
31
31
|
}
|
|
32
32
|
return { probability: result.answers.recall_needed.noul, model: result.model, usage: result.usage };
|
|
33
33
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { DatabaseSync } from "node:sqlite";
|
|
3
3
|
import { messageText } from "./whisperer-context.js";
|
|
4
|
-
import {
|
|
4
|
+
import { conversationUserText } from "./response-text.js";
|
|
5
5
|
// Identical serialized inputs keep their checkpoints when eligibility broadens.
|
|
6
6
|
export const TRAINING_PREPARATION = "visible-history-v1";
|
|
7
7
|
// A deliberately conservative byte budget, NOT a tokenizer or a 32k-token target.
|
|
@@ -10,27 +10,6 @@ export const trainingHash = (value) => createHash("sha256").update(JSON.stringif
|
|
|
10
10
|
function record(value) {
|
|
11
11
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
12
12
|
}
|
|
13
|
-
/** Legacy sender IDs are useful for envelope cleanup, not an admission requirement. */
|
|
14
|
-
function userText(raw, sender) {
|
|
15
|
-
let senderId = typeof sender === "string" ? sender : undefined;
|
|
16
|
-
if (senderId === undefined) {
|
|
17
|
-
const header = /^Conversation info: ⟦openclaw:ctx⟧\r?\n```json\r?\n([^]*?)\r?\n```\r?\n/.exec(raw.trim());
|
|
18
|
-
if (header) {
|
|
19
|
-
let metadata;
|
|
20
|
-
try {
|
|
21
|
-
metadata = JSON.parse(header[1]);
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
const id = record(record(metadata)?.sender)?.id;
|
|
27
|
-
if (typeof id === "string")
|
|
28
|
-
senderId = id;
|
|
29
|
-
}
|
|
30
|
-
senderId ??= /^From: [^\r\n]+ \(([^()\r\n]+)\)\r?\n/.exec(raw.trim())?.[1];
|
|
31
|
-
}
|
|
32
|
-
return responseUserText(raw, senderId ?? "");
|
|
33
|
-
}
|
|
34
13
|
/** The following answer establishes eligibility, but is never part of that example's input. */
|
|
35
14
|
export function trainingExamples(rows) {
|
|
36
15
|
const examples = [];
|
|
@@ -88,7 +67,7 @@ export function trainingExamples(rows) {
|
|
|
88
67
|
continue;
|
|
89
68
|
}
|
|
90
69
|
const raw = typeof meta?.upstreamUserText === "string" ? meta.upstreamUserText : messageText(message)?.text;
|
|
91
|
-
const visible = raw ?
|
|
70
|
+
const visible = raw ? conversationUserText(raw, meta?.senderId ?? message.senderId) : undefined;
|
|
92
71
|
if (!visible || /^(?:\[OpenClaw heartbeat poll\]|\[Queued messages while agent was busy\]|\[Subagent Context\]|<relevant-memories>)/.test(visible.text)) {
|
|
93
72
|
coverage.filtered++;
|
|
94
73
|
boundary();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Type } from 'typebox';
|
|
2
2
|
import { Value } from 'typebox/value';
|
|
3
|
-
import {
|
|
3
|
+
import { requestTypeSafe, TYPESAFE_MODEL } from './typesafe-client.js';
|
|
4
4
|
export const CONTEXT_JUDGE_VERSION = 'conversation-context-usefulness-v1';
|
|
5
|
-
const CONTEXT_JUDGE_MODEL =
|
|
5
|
+
const CONTEXT_JUDGE_MODEL = TYPESAFE_MODEL;
|
|
6
6
|
const CONTEXT_QUESTIONS = {
|
|
7
7
|
usefulness: {
|
|
8
8
|
type: 'score',
|
|
@@ -53,5 +53,5 @@ export function parseContextJudgment(payload) {
|
|
|
53
53
|
return { score: answer.score / 3, answer, model: payload.model, usage: payload.usage ?? null };
|
|
54
54
|
}
|
|
55
55
|
export async function judgeTrainingPassage(request, apiKey) {
|
|
56
|
-
return parseContextJudgment(await
|
|
56
|
+
return parseContextJudgment(await requestTypeSafe({ apiKey, timeoutMs: 30_000 }, request.state, request.questions));
|
|
57
57
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { collectTraining } from "./training.js";
|
|
2
2
|
import { trainingHash } from "./training-input.js";
|
|
3
3
|
import { TRAINING_GATE_THRESHOLD } from "./training-gate.js";
|
|
4
|
-
import { resolveTypeSafeApiKey } from "./typesafe.js";
|
|
4
|
+
import { resolveTypeSafeApiKey, TypeSafeRequestError } from "./typesafe-client.js";
|
|
5
5
|
import { trainingTeacher, trainingTeacherMessage, TRAINING_TEACHER_MODEL, TRAINING_TEACHER_PROMPT, TRAINING_TEACHER_VERSION } from "./training-models.js";
|
|
6
6
|
import { historicalTrainingSearch, TRAINING_RETRIEVAL_VERSION, TRAINING_SEARCH_OPTIONS } from "./training-retrieval.js";
|
|
7
7
|
import { contextJudgeRequest, judgeTrainingPassage, CONTEXT_JUDGE_VERSION } from "./training-judge.js";
|
|
8
|
-
import { TypeSafeHttpError } from "./typesafe-transport.js";
|
|
9
8
|
const SELECTION_VERSION = "conversation-utility-top5-sum-v2";
|
|
10
9
|
export const TRAINING_EVALUATION_CONCURRENCY = 4;
|
|
11
10
|
function bounds(options) {
|
|
@@ -41,9 +40,11 @@ async function checkpoint(store, step, result, operation) {
|
|
|
41
40
|
const hostCode = error && typeof error === "object" && "code" in error && typeof error.code === "string" &&
|
|
42
41
|
/^LLM_[A-Z_]+$/u.test(error.code) ? error.code : undefined;
|
|
43
42
|
const status = hostCode === "LLM_COMPLETION_NOT_AUTHORIZED" ||
|
|
44
|
-
(error instanceof
|
|
43
|
+
(error instanceof TypeSafeRequestError && error.code === "http_error" &&
|
|
44
|
+
error.status !== undefined && error.status >= 400 && error.status < 500) ? "failed" : "ambiguous";
|
|
45
45
|
store.finishStep(step.stage, step.id, attempt, { status,
|
|
46
|
-
error: error instanceof
|
|
46
|
+
error: error instanceof TypeSafeRequestError && error.code === "http_error" ?
|
|
47
|
+
`http_${error.status}` : hostCode ?? "request_or_response_uncertain" });
|
|
47
48
|
result[status]++;
|
|
48
49
|
return;
|
|
49
50
|
}
|
package/dist/src/training.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolveTypeSafeApiKey } from "./typesafe.js";
|
|
1
|
+
import { TypeSafeRequestError, resolveTypeSafeApiKey } from "./typesafe-client.js";
|
|
3
2
|
import { TrainingTranscriptReader } from "./training-input.js";
|
|
4
3
|
import { judgeTrainingInput, TRAINING_GATE_QUESTIONS } from "./training-gate.js";
|
|
5
4
|
/** A full active-branch rescan is cheap/local; only changed exact inputs need inference. */
|
|
@@ -78,9 +77,11 @@ export async function runTraining(source, store, config, options) {
|
|
|
78
77
|
judgment = await judgeTrainingInput(input, key, AbortSignal.timeout(30_000));
|
|
79
78
|
}
|
|
80
79
|
catch (error) {
|
|
81
|
-
const definite = error instanceof
|
|
80
|
+
const definite = error instanceof TypeSafeRequestError && error.code === "http_error" &&
|
|
81
|
+
error.status !== undefined && error.status >= 400 && error.status < 500;
|
|
82
82
|
const status = definite ? "failed" : "ambiguous";
|
|
83
|
-
store.finish(job.id, attempt, { status, error: error instanceof
|
|
83
|
+
store.finish(job.id, attempt, { status, error: error instanceof TypeSafeRequestError && error.code === "http_error" ?
|
|
84
|
+
`http_${error.status}` : "request_or_response_uncertain" });
|
|
84
85
|
result[status]++;
|
|
85
86
|
// Stop on the first failure rather than spending the rest of the budget during an outage.
|
|
86
87
|
return;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { UnblockMemoryConfig } from "./config.js";
|
|
2
|
+
export declare const TYPESAFE_MODEL = "jev-1.13.0";
|
|
3
|
+
export declare class TypeSafeRequestError extends Error {
|
|
4
|
+
readonly code: "timeout" | "cancelled" | "http_error" | "invalid_response" | "network_error";
|
|
5
|
+
readonly status?: number | undefined;
|
|
6
|
+
constructor(message: string, code: "timeout" | "cancelled" | "http_error" | "invalid_response" | "network_error", status?: number | undefined);
|
|
7
|
+
}
|
|
8
|
+
/** Explicit credentials take precedence; a missing explicit file never selects another key. */
|
|
9
|
+
export declare function resolveTypeSafeApiKey(config: UnblockMemoryConfig["typesafe"]): Promise<string | undefined>;
|
|
10
|
+
/** One TypeSafe request. The caller owns the response schema and failure policy. */
|
|
11
|
+
export declare function requestTypeSafe(params: {
|
|
12
|
+
apiKey: string;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}, state: unknown, questions: unknown): Promise<unknown>;
|