@jonathangu/openclawbrain 0.3.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/LICENSE +21 -0
- package/README.md +412 -0
- package/bin/openclawbrain.js +15 -0
- package/docs/END_STATE.md +244 -0
- package/docs/EVIDENCE.md +128 -0
- package/docs/RELEASE_CONTRACT.md +91 -0
- package/docs/agent-tools.md +106 -0
- package/docs/architecture.md +224 -0
- package/docs/configuration.md +178 -0
- package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/status.json +87 -0
- package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/summary.md +16 -0
- package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/trace.json +273 -0
- package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/validation-report.json +652 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/channels-status.txt +31 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/config-snapshot.json +66 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/doctor.json +14 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/gateway-probe.txt +34 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/gateway-status.txt +41 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/logs.txt +428 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/status-all.txt +60 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/status.json +223 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/summary.md +13 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/trace.json +4 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/validation-report.json +334 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/channels-status.txt +25 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/config-snapshot.json +91 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/doctor.json +14 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/gateway-probe.txt +36 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/gateway-status.txt +44 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/logs.txt +428 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-doctor.json +10 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-sdk-probe.json +11 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-setup-only.json +12 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/summary.md +30 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/validation-report.json +72 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/status-all.txt +63 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/status.json +200 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/summary.md +13 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/trace.json +4 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/validation-report.json +311 -0
- package/docs/evidence/README.md +16 -0
- package/docs/fts5.md +161 -0
- package/docs/tui.md +506 -0
- package/index.ts +1372 -0
- package/openclaw.plugin.json +136 -0
- package/package.json +66 -0
- package/src/assembler.ts +804 -0
- package/src/brain-cli.ts +316 -0
- package/src/brain-core/decay.ts +35 -0
- package/src/brain-core/episode.ts +82 -0
- package/src/brain-core/graph.ts +321 -0
- package/src/brain-core/health.ts +116 -0
- package/src/brain-core/mutator.ts +281 -0
- package/src/brain-core/pack.ts +117 -0
- package/src/brain-core/policy.ts +153 -0
- package/src/brain-core/replay.ts +1 -0
- package/src/brain-core/teacher.ts +105 -0
- package/src/brain-core/trace.ts +40 -0
- package/src/brain-core/traverse.ts +230 -0
- package/src/brain-core/types.ts +405 -0
- package/src/brain-core/update.ts +123 -0
- package/src/brain-harvest/human.ts +46 -0
- package/src/brain-harvest/scanner.ts +98 -0
- package/src/brain-harvest/self.ts +147 -0
- package/src/brain-runtime/assembler-extension.ts +230 -0
- package/src/brain-runtime/evidence-detectors.ts +68 -0
- package/src/brain-runtime/graph-io.ts +72 -0
- package/src/brain-runtime/harvester-extension.ts +98 -0
- package/src/brain-runtime/service.ts +659 -0
- package/src/brain-runtime/tools.ts +109 -0
- package/src/brain-runtime/worker-state.ts +106 -0
- package/src/brain-runtime/worker-supervisor.ts +169 -0
- package/src/brain-store/embedding.ts +179 -0
- package/src/brain-store/init.ts +347 -0
- package/src/brain-store/migrations.ts +188 -0
- package/src/brain-store/store.ts +816 -0
- package/src/brain-worker/child-runner.ts +321 -0
- package/src/brain-worker/jobs.ts +12 -0
- package/src/brain-worker/mutation-job.ts +5 -0
- package/src/brain-worker/promotion-job.ts +5 -0
- package/src/brain-worker/protocol.ts +79 -0
- package/src/brain-worker/teacher-job.ts +5 -0
- package/src/brain-worker/update-job.ts +5 -0
- package/src/brain-worker/worker.ts +422 -0
- package/src/compaction.ts +1332 -0
- package/src/db/config.ts +265 -0
- package/src/db/connection.ts +72 -0
- package/src/db/features.ts +42 -0
- package/src/db/migration.ts +561 -0
- package/src/engine.ts +1995 -0
- package/src/expansion-auth.ts +351 -0
- package/src/expansion-policy.ts +303 -0
- package/src/expansion.ts +383 -0
- package/src/integrity.ts +600 -0
- package/src/large-files.ts +527 -0
- package/src/openclaw-bridge.ts +22 -0
- package/src/retrieval.ts +357 -0
- package/src/store/conversation-store.ts +748 -0
- package/src/store/fts5-sanitize.ts +29 -0
- package/src/store/full-text-fallback.ts +74 -0
- package/src/store/index.ts +29 -0
- package/src/store/summary-store.ts +918 -0
- package/src/summarize.ts +847 -0
- package/src/tools/common.ts +53 -0
- package/src/tools/lcm-conversation-scope.ts +76 -0
- package/src/tools/lcm-describe-tool.ts +234 -0
- package/src/tools/lcm-expand-query-tool.ts +594 -0
- package/src/tools/lcm-expand-tool.delegation.ts +556 -0
- package/src/tools/lcm-expand-tool.ts +448 -0
- package/src/tools/lcm-expansion-recursion-guard.ts +286 -0
- package/src/tools/lcm-grep-tool.ts +200 -0
- package/src/transcript-repair.ts +301 -0
- package/src/types.ts +149 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { HarvestMessagePart, HarvestResult } from "../brain-runtime/evidence-detectors.js";
|
|
2
|
+
|
|
3
|
+
const TOOL_FAILURE_PATTERNS = [
|
|
4
|
+
/\berror\b/i,
|
|
5
|
+
/\bfailed\b/i,
|
|
6
|
+
/\bexception\b/i,
|
|
7
|
+
/stack\s*trace/i,
|
|
8
|
+
/\bEINVAL\b/,
|
|
9
|
+
/\bENOENT\b/,
|
|
10
|
+
/\bEACCES\b/,
|
|
11
|
+
/exit\s+code\s+[1-9]/i,
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const TOOL_SUCCESS_PATTERNS = [
|
|
15
|
+
/\bsuccess(ful|fully)?\b/i,
|
|
16
|
+
/\bpassed\b/i,
|
|
17
|
+
/\bdeployed\b/i,
|
|
18
|
+
/\bcreated\s+(commit|pr|branch)\b/i,
|
|
19
|
+
/\b\d+\s+pass(ed|ing)\b/i,
|
|
20
|
+
/\bfixed\b/i,
|
|
21
|
+
/\bresolved\b/i,
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function parseJson(value: string | null | undefined): unknown {
|
|
25
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(value);
|
|
30
|
+
} catch {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readPartMetadata(part: HarvestMessagePart): Record<string, unknown> {
|
|
36
|
+
const parsed = parseJson(part.metadata);
|
|
37
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
38
|
+
? parsed as Record<string, unknown>
|
|
39
|
+
: {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isStructuredToolResultPart(part: HarvestMessagePart): boolean {
|
|
43
|
+
if (part.partType !== "tool") {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const metadata = readPartMetadata(part);
|
|
47
|
+
const rawType = typeof metadata.rawType === "string" ? metadata.rawType : "";
|
|
48
|
+
const originalRole = typeof metadata.originalRole === "string" ? metadata.originalRole : "";
|
|
49
|
+
return originalRole === "toolResult"
|
|
50
|
+
|| rawType === "tool_result"
|
|
51
|
+
|| rawType === "toolResult"
|
|
52
|
+
|| rawType === "function_call_output";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function classifyStructuredToolOutput(output: unknown): { ok: boolean; reason: string } | null {
|
|
56
|
+
if (!output || typeof output !== "object" || Array.isArray(output)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const record = output as Record<string, unknown>;
|
|
60
|
+
|
|
61
|
+
if (record.isError === true || record.error !== undefined || record.errors !== undefined) {
|
|
62
|
+
return { ok: false, reason: "structured tool output indicates error" };
|
|
63
|
+
}
|
|
64
|
+
if (record.ok === false || record.success === false || record.passed === false || record.failed === true) {
|
|
65
|
+
return { ok: false, reason: "structured tool output indicates failure" };
|
|
66
|
+
}
|
|
67
|
+
if (typeof record.exitCode === "number" && record.exitCode > 0) {
|
|
68
|
+
return { ok: false, reason: `structured tool output exitCode=${record.exitCode}` };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (record.ok === true || record.success === true || record.passed === true) {
|
|
72
|
+
return { ok: true, reason: "structured tool output indicates success" };
|
|
73
|
+
}
|
|
74
|
+
if (typeof record.exitCode === "number" && record.exitCode === 0) {
|
|
75
|
+
return { ok: true, reason: "structured tool output exitCode=0" };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function detectStructuredSelfEvidence(messageParts?: HarvestMessagePart[]): HarvestResult | null {
|
|
82
|
+
if (!messageParts || messageParts.length === 0) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const part of messageParts) {
|
|
87
|
+
if (!isStructuredToolResultPart(part)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const metadata = readPartMetadata(part);
|
|
92
|
+
if (metadata.isError === true) {
|
|
93
|
+
return {
|
|
94
|
+
value: -0.5,
|
|
95
|
+
source: "self",
|
|
96
|
+
reason: "structured tool result marked isError=true",
|
|
97
|
+
confidence: 0.9,
|
|
98
|
+
kind: "self_result",
|
|
99
|
+
extractor: "structured_tool_result",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const classified = classifyStructuredToolOutput(parseJson(part.toolOutput));
|
|
104
|
+
if (!classified) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
value: classified.ok ? 0.5 : -0.5,
|
|
110
|
+
source: "self",
|
|
111
|
+
reason: classified.reason,
|
|
112
|
+
confidence: 0.9,
|
|
113
|
+
kind: "self_result",
|
|
114
|
+
extractor: "structured_tool_result",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function detectSelfEvidence(content: string): HarvestResult | null {
|
|
122
|
+
for (const pattern of TOOL_FAILURE_PATTERNS) {
|
|
123
|
+
if (pattern.test(content)) {
|
|
124
|
+
return {
|
|
125
|
+
value: -0.5,
|
|
126
|
+
source: "self",
|
|
127
|
+
reason: `tool failure: ${pattern.source}`,
|
|
128
|
+
confidence: 0.7,
|
|
129
|
+
kind: "self_result",
|
|
130
|
+
extractor: "self_pattern",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for (const pattern of TOOL_SUCCESS_PATTERNS) {
|
|
135
|
+
if (pattern.test(content)) {
|
|
136
|
+
return {
|
|
137
|
+
value: 0.5,
|
|
138
|
+
source: "self",
|
|
139
|
+
reason: `tool success: ${pattern.source}`,
|
|
140
|
+
confidence: 0.7,
|
|
141
|
+
kind: "self_result",
|
|
142
|
+
extractor: "self_pattern",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { AssembleContextResult } from "../assembler.js";
|
|
2
|
+
import type { ContextEngine } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { TraversalResult } from "../brain-core/types.js";
|
|
4
|
+
import type { BrainService } from "./service.js";
|
|
5
|
+
|
|
6
|
+
type AgentMessage = Parameters<ContextEngine["ingest"]>[0]["message"];
|
|
7
|
+
export type BrainAssemblyDecisionMode =
|
|
8
|
+
| "use_brain"
|
|
9
|
+
| "shadow"
|
|
10
|
+
| "skip_no_query"
|
|
11
|
+
| "skip_short_static_lookup"
|
|
12
|
+
| "skip_no_embedding"
|
|
13
|
+
| "skip_uninitialized"
|
|
14
|
+
| "skip_budget_too_small";
|
|
15
|
+
|
|
16
|
+
export type BrainAssemblyDecision = {
|
|
17
|
+
mode: BrainAssemblyDecisionMode;
|
|
18
|
+
queryText: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type BrainAssembledContextResult = AssembleContextResult & {
|
|
22
|
+
brainDecision?: {
|
|
23
|
+
mode: BrainAssemblyDecisionMode;
|
|
24
|
+
episodeId?: string | null;
|
|
25
|
+
traceId?: string | null;
|
|
26
|
+
footer?: string | null;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function decisionFooter(mode: BrainAssemblyDecisionMode): string {
|
|
31
|
+
switch (mode) {
|
|
32
|
+
case "use_brain":
|
|
33
|
+
return "[brain] used graph retrieval for this turn.";
|
|
34
|
+
case "shadow":
|
|
35
|
+
return "[brain shadow] recorded routing without injecting learned context.";
|
|
36
|
+
case "skip_no_query":
|
|
37
|
+
return "[brain] bypassed: no user query text.";
|
|
38
|
+
case "skip_short_static_lookup":
|
|
39
|
+
return "[brain] bypassed: short static lookup.";
|
|
40
|
+
case "skip_no_embedding":
|
|
41
|
+
return "[brain] bypassed: embeddings unavailable.";
|
|
42
|
+
case "skip_uninitialized":
|
|
43
|
+
return "[brain] bypassed: brain uninitialized or disabled.";
|
|
44
|
+
case "skip_budget_too_small":
|
|
45
|
+
return "[brain] bypassed: token budget too small.";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function estimateTokens(text: string): number {
|
|
50
|
+
return Math.ceil(text.length / 4);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function extractText(content: unknown): string {
|
|
54
|
+
if (typeof content === "string") {
|
|
55
|
+
return content;
|
|
56
|
+
}
|
|
57
|
+
if (!Array.isArray(content)) {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
return content
|
|
61
|
+
.filter((item): item is { type?: unknown; text?: unknown } => !!item && typeof item === "object")
|
|
62
|
+
.map((item) => (item.type === "text" && typeof item.text === "string" ? item.text : ""))
|
|
63
|
+
.join("\n")
|
|
64
|
+
.trim();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildBrainContextBlock(result: TraversalResult): string {
|
|
68
|
+
const corrections = result.fired.filter((node) => node.kind === "correction");
|
|
69
|
+
const playbooks = result.fired.filter((node) => node.kind === "workflow" || node.kind === "toolcard");
|
|
70
|
+
const evidence = result.fired.filter((node) => node.kind !== "correction" && node.kind !== "workflow" && node.kind !== "toolcard");
|
|
71
|
+
|
|
72
|
+
const sections = [
|
|
73
|
+
"OpenClawBrain retrieved context. Prefer correction cards over conflicting heuristics when directly relevant.",
|
|
74
|
+
"",
|
|
75
|
+
"## Correction Cards",
|
|
76
|
+
corrections.length > 0 ? corrections.map((node) => `- ${node.content}`).join("\n") : "- none",
|
|
77
|
+
"",
|
|
78
|
+
"## Route-Selected Evidence",
|
|
79
|
+
evidence.length > 0 ? evidence.map((node) => `- [${node.kind}] ${node.content}`).join("\n") : "- none",
|
|
80
|
+
"",
|
|
81
|
+
"## Toolcards And Playbooks",
|
|
82
|
+
playbooks.length > 0 ? playbooks.map((node) => `- [${node.kind}] ${node.content}`).join("\n") : "- none",
|
|
83
|
+
"",
|
|
84
|
+
"## Transcript Support",
|
|
85
|
+
"- Use the LCM transcript and summary context below for chronology and grounding.",
|
|
86
|
+
"",
|
|
87
|
+
`Trace: ${result.trace.footer}`,
|
|
88
|
+
];
|
|
89
|
+
return sections.join("\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export class BrainAssemblerExtension {
|
|
93
|
+
constructor(private brain: BrainService) {}
|
|
94
|
+
|
|
95
|
+
decide(params: {
|
|
96
|
+
tokenBudget: number;
|
|
97
|
+
liveMessages: AgentMessage[];
|
|
98
|
+
}): BrainAssemblyDecision {
|
|
99
|
+
const latestUserMessage = [...params.liveMessages]
|
|
100
|
+
.reverse()
|
|
101
|
+
.find((message) => message.role === "user");
|
|
102
|
+
const queryText = latestUserMessage ? extractText(latestUserMessage.content) : "";
|
|
103
|
+
|
|
104
|
+
if (!this.brain.isEnabled() || !this.brain.isInitialized()) {
|
|
105
|
+
return { mode: "skip_uninitialized", queryText };
|
|
106
|
+
}
|
|
107
|
+
if (!this.brain.isEmbeddingConfigured()) {
|
|
108
|
+
return { mode: "skip_no_embedding", queryText };
|
|
109
|
+
}
|
|
110
|
+
if (params.tokenBudget < 512) {
|
|
111
|
+
return { mode: "skip_budget_too_small", queryText };
|
|
112
|
+
}
|
|
113
|
+
if (!queryText) {
|
|
114
|
+
return { mode: "skip_no_query", queryText };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const normalized = queryText.toLowerCase();
|
|
118
|
+
const looksStaticLookup =
|
|
119
|
+
queryText.length < 72
|
|
120
|
+
&& (normalized.startsWith("read ")
|
|
121
|
+
|| normalized.startsWith("show ")
|
|
122
|
+
|| normalized.startsWith("open ")
|
|
123
|
+
|| normalized.startsWith("cat ")
|
|
124
|
+
|| normalized.startsWith("grep ")
|
|
125
|
+
|| normalized.includes(".ts")
|
|
126
|
+
|| normalized.includes(".md")
|
|
127
|
+
|| normalized.includes(".json")
|
|
128
|
+
|| normalized.includes("/"));
|
|
129
|
+
if (looksStaticLookup) {
|
|
130
|
+
return { mode: "skip_short_static_lookup", queryText };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { mode: this.brain.isShadowMode() ? "shadow" : "use_brain", queryText };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async augmentAssembly(params: {
|
|
137
|
+
conversationId: number;
|
|
138
|
+
tokenBudget: number;
|
|
139
|
+
assembled: AssembleContextResult;
|
|
140
|
+
liveMessages: AgentMessage[];
|
|
141
|
+
}): Promise<BrainAssembledContextResult> {
|
|
142
|
+
const decision = this.decide({
|
|
143
|
+
tokenBudget: params.tokenBudget,
|
|
144
|
+
liveMessages: params.liveMessages,
|
|
145
|
+
});
|
|
146
|
+
if (decision.mode !== "use_brain" && decision.mode !== "shadow") {
|
|
147
|
+
this.brain.noteAssemblyDecision({
|
|
148
|
+
mode: decision.mode,
|
|
149
|
+
conversationId: params.conversationId,
|
|
150
|
+
footer: decisionFooter(decision.mode),
|
|
151
|
+
});
|
|
152
|
+
return {
|
|
153
|
+
...params.assembled,
|
|
154
|
+
brainDecision: {
|
|
155
|
+
mode: decision.mode,
|
|
156
|
+
footer: decisionFooter(decision.mode),
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const result = await this.brain.query({
|
|
162
|
+
conversationId: params.conversationId,
|
|
163
|
+
queryText: decision.queryText,
|
|
164
|
+
budgetChars: Math.max(256, Math.floor(params.tokenBudget * 4 * 0.3)),
|
|
165
|
+
});
|
|
166
|
+
if (!result) {
|
|
167
|
+
this.brain.noteAssemblyDecision({
|
|
168
|
+
mode: decision.mode,
|
|
169
|
+
conversationId: params.conversationId,
|
|
170
|
+
footer: decisionFooter(decision.mode),
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
...params.assembled,
|
|
174
|
+
brainDecision: {
|
|
175
|
+
mode: decision.mode,
|
|
176
|
+
footer: decisionFooter(decision.mode),
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const brainMessage: AgentMessage = {
|
|
182
|
+
role: "user",
|
|
183
|
+
content: buildBrainContextBlock(result),
|
|
184
|
+
} as AgentMessage;
|
|
185
|
+
if (decision.mode === "shadow") {
|
|
186
|
+
this.brain.noteAssemblyDecision({
|
|
187
|
+
mode: "shadow",
|
|
188
|
+
conversationId: params.conversationId,
|
|
189
|
+
episodeId: result.episode.id,
|
|
190
|
+
traceId: result.trace.id,
|
|
191
|
+
footer: decisionFooter("shadow"),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
...params.assembled,
|
|
196
|
+
brainDecision: {
|
|
197
|
+
mode: "shadow",
|
|
198
|
+
episodeId: result.episode.id,
|
|
199
|
+
traceId: result.trace.id,
|
|
200
|
+
footer: decisionFooter("shadow"),
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
this.brain.noteAssemblyDecision({
|
|
205
|
+
mode: "use_brain",
|
|
206
|
+
conversationId: params.conversationId,
|
|
207
|
+
episodeId: result.episode.id,
|
|
208
|
+
traceId: result.trace.id,
|
|
209
|
+
footer: result.trace.footer,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
...params.assembled,
|
|
214
|
+
messages: [brainMessage, ...params.assembled.messages],
|
|
215
|
+
estimatedTokens: params.assembled.estimatedTokens + estimateTokens(extractText(brainMessage.content)),
|
|
216
|
+
systemPromptAddition: [
|
|
217
|
+
params.assembled.systemPromptAddition,
|
|
218
|
+
"OpenClawBrain sections are ranked by trust: correction cards, evidence, playbooks, then transcript support.",
|
|
219
|
+
]
|
|
220
|
+
.filter(Boolean)
|
|
221
|
+
.join("\n\n"),
|
|
222
|
+
brainDecision: {
|
|
223
|
+
mode: "use_brain",
|
|
224
|
+
episodeId: result.episode.id,
|
|
225
|
+
traceId: result.trace.id,
|
|
226
|
+
footer: result.trace.footer,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { BrainEvidenceKind, RewardSource } from "../brain-core/types.js";
|
|
2
|
+
import { detectHumanEvidence } from "../brain-harvest/human.js";
|
|
3
|
+
import { detectScannerEvidence } from "../brain-harvest/scanner.js";
|
|
4
|
+
import { detectSelfEvidence, detectStructuredSelfEvidence } from "../brain-harvest/self.js";
|
|
5
|
+
|
|
6
|
+
export type HarvestMessagePart = {
|
|
7
|
+
partType: string;
|
|
8
|
+
textContent?: string | null;
|
|
9
|
+
toolCallId?: string | null;
|
|
10
|
+
toolName?: string | null;
|
|
11
|
+
toolInput?: string | null;
|
|
12
|
+
toolOutput?: string | null;
|
|
13
|
+
metadata?: string | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export interface HarvestResult {
|
|
17
|
+
value: number;
|
|
18
|
+
source: RewardSource;
|
|
19
|
+
reason: string;
|
|
20
|
+
confidence?: number;
|
|
21
|
+
kind: BrainEvidenceKind;
|
|
22
|
+
extractor?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function detectEvidenceBatch(
|
|
26
|
+
role: string,
|
|
27
|
+
content: string,
|
|
28
|
+
messageParts?: HarvestMessagePart[],
|
|
29
|
+
): HarvestResult[] {
|
|
30
|
+
if (role === "user") {
|
|
31
|
+
const human = detectHumanEvidence(content);
|
|
32
|
+
return human ? [human] : [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (role === "tool" || role === "assistant") {
|
|
36
|
+
const structuredSelf = detectStructuredSelfEvidence(messageParts);
|
|
37
|
+
const self = structuredSelf ?? detectSelfEvidence(content);
|
|
38
|
+
const results = [
|
|
39
|
+
self,
|
|
40
|
+
detectScannerEvidence(content),
|
|
41
|
+
].filter((result): result is HarvestResult => result !== null);
|
|
42
|
+
|
|
43
|
+
const deduped = new Map<string, HarvestResult>();
|
|
44
|
+
for (const result of results) {
|
|
45
|
+
const key = [
|
|
46
|
+
result.source,
|
|
47
|
+
result.kind,
|
|
48
|
+
result.value,
|
|
49
|
+
result.reason,
|
|
50
|
+
result.extractor ?? "",
|
|
51
|
+
].join("::");
|
|
52
|
+
if (!deduped.has(key)) {
|
|
53
|
+
deduped.set(key, result);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return Array.from(deduped.values());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function detectEvidence(
|
|
63
|
+
role: string,
|
|
64
|
+
content: string,
|
|
65
|
+
messageParts?: HarvestMessagePart[],
|
|
66
|
+
): HarvestResult | null {
|
|
67
|
+
return detectEvidenceBatch(role, content, messageParts)[0] ?? null;
|
|
68
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { computeHealth } from "../brain-core/health.js";
|
|
2
|
+
import type { BrainConfig, BrainNode, SeedWeight } from "../brain-core/types.js";
|
|
3
|
+
import type { BrainGraph } from "../brain-core/graph.js";
|
|
4
|
+
import { PackManager } from "../brain-core/pack.js";
|
|
5
|
+
import type { BrainStore } from "../brain-store/store.js";
|
|
6
|
+
|
|
7
|
+
export function flattenEdges(graph: BrainGraph) {
|
|
8
|
+
return graph.getAllEdges();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function flattenSeedWeights(graph: BrainGraph): SeedWeight[] {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
return graph.getAllSeedWeights().map((seedWeight) => ({
|
|
14
|
+
nodeId: seedWeight.nodeId,
|
|
15
|
+
weight: seedWeight.weight,
|
|
16
|
+
updatedAt: now,
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function populateGraph(
|
|
21
|
+
graph: BrainGraph,
|
|
22
|
+
nodes: BrainNode[],
|
|
23
|
+
edges: ReturnType<typeof flattenEdges>,
|
|
24
|
+
seedWeights: SeedWeight[] = [],
|
|
25
|
+
): void {
|
|
26
|
+
graph.clear();
|
|
27
|
+
for (const node of nodes) {
|
|
28
|
+
graph.addNode(node);
|
|
29
|
+
}
|
|
30
|
+
for (const edge of edges) {
|
|
31
|
+
graph.addEdge(edge);
|
|
32
|
+
}
|
|
33
|
+
for (const seedWeight of seedWeights) {
|
|
34
|
+
graph.setSeedWeight(seedWeight.nodeId, seedWeight.weight);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function reloadGraphFromStore(store: BrainStore, graph: BrainGraph): void {
|
|
39
|
+
populateGraph(graph, store.getAllNodes(), store.loadAllEdges(), store.loadAllSeedWeights());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function promoteGraphSnapshot(params: {
|
|
43
|
+
store: BrainStore;
|
|
44
|
+
graph: BrainGraph;
|
|
45
|
+
packManager: PackManager;
|
|
46
|
+
config: BrainConfig;
|
|
47
|
+
reason: string;
|
|
48
|
+
metadata: Record<string, unknown>;
|
|
49
|
+
}): number | null {
|
|
50
|
+
if (params.graph.nodeCount() === 0) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const health = computeHealth(
|
|
55
|
+
params.graph,
|
|
56
|
+
params.store.getRecentEpisodes(params.config.replayEpisodeCount),
|
|
57
|
+
params.store.getCurrentPackVersion() ?? 0,
|
|
58
|
+
);
|
|
59
|
+
const pack = params.packManager.buildCandidate(health);
|
|
60
|
+
params.store.writePackSnapshot({
|
|
61
|
+
version: pack.version,
|
|
62
|
+
nodes: params.graph.getAllNodes(),
|
|
63
|
+
edges: flattenEdges(params.graph),
|
|
64
|
+
seedWeights: flattenSeedWeights(params.graph),
|
|
65
|
+
metadata: {
|
|
66
|
+
reason: params.reason,
|
|
67
|
+
...params.metadata,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
params.packManager.promote(pack.version);
|
|
71
|
+
return pack.version;
|
|
72
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Label harvesting from ingested messages.
|
|
3
|
+
*
|
|
4
|
+
* Structured evidence flow:
|
|
5
|
+
* - detect human/self/scanner evidence separately
|
|
6
|
+
* - persist raw evidence first
|
|
7
|
+
* - let the worker resolve evidence into labels with trust ordering
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { BrainStore } from "../brain-store/store.js";
|
|
11
|
+
import {
|
|
12
|
+
detectEvidence,
|
|
13
|
+
detectEvidenceBatch,
|
|
14
|
+
type HarvestMessagePart,
|
|
15
|
+
type HarvestResult,
|
|
16
|
+
} from "./evidence-detectors.js";
|
|
17
|
+
|
|
18
|
+
export class LabelHarvester {
|
|
19
|
+
constructor(
|
|
20
|
+
private store: BrainStore,
|
|
21
|
+
private log: { info: (msg: string) => void; warn: (msg: string) => void },
|
|
22
|
+
private resolveEpisodeIdForConversation?: (conversationId: number) => string | null | undefined,
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Called from engine.ts after message ingestion.
|
|
27
|
+
* Detects evidence from message content and attaches it to the matching episode.
|
|
28
|
+
*/
|
|
29
|
+
async harvestFromMessage(params: {
|
|
30
|
+
conversationId: number;
|
|
31
|
+
episodeId?: string;
|
|
32
|
+
role: string;
|
|
33
|
+
content: string;
|
|
34
|
+
messageParts?: HarvestMessagePart[];
|
|
35
|
+
}): Promise<void> {
|
|
36
|
+
const results = this.detectLabels(params.role, params.content, params.messageParts);
|
|
37
|
+
if (results.length === 0) return;
|
|
38
|
+
|
|
39
|
+
const explicitEpisodeId = params.episodeId ?? null;
|
|
40
|
+
const resolvedEpisodeId = explicitEpisodeId
|
|
41
|
+
?? this.resolveEpisodeIdForConversation?.(params.conversationId)
|
|
42
|
+
?? null;
|
|
43
|
+
const matchedResolvedEpisode = (() => {
|
|
44
|
+
if (!resolvedEpisodeId) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const episode = this.store.getEpisode(resolvedEpisodeId);
|
|
48
|
+
return episode?.conversationId === params.conversationId ? episode : null;
|
|
49
|
+
})();
|
|
50
|
+
const matchingEpisode = matchedResolvedEpisode
|
|
51
|
+
?? this.store.getRecentEpisodesForConversation(params.conversationId, 5)[0]
|
|
52
|
+
?? null;
|
|
53
|
+
|
|
54
|
+
if (!matchingEpisode) return;
|
|
55
|
+
|
|
56
|
+
const attributionMode = matchedResolvedEpisode
|
|
57
|
+
? explicitEpisodeId
|
|
58
|
+
? "explicit"
|
|
59
|
+
: "resolver"
|
|
60
|
+
: "recent_conversation_fallback";
|
|
61
|
+
|
|
62
|
+
for (const [index, result] of results.entries()) {
|
|
63
|
+
this.store.insertEvidence({
|
|
64
|
+
episodeId: matchingEpisode.id,
|
|
65
|
+
conversationId: params.conversationId,
|
|
66
|
+
source: result.source,
|
|
67
|
+
kind: result.kind,
|
|
68
|
+
value: result.value,
|
|
69
|
+
confidence: result.confidence,
|
|
70
|
+
reason: result.reason,
|
|
71
|
+
contentSnippet: params.content.slice(0, 240),
|
|
72
|
+
metadata: {
|
|
73
|
+
harvestedFromRole: params.role,
|
|
74
|
+
explicitEpisodeId,
|
|
75
|
+
resolvedEpisodeId,
|
|
76
|
+
matchedEpisodeId: matchingEpisode.id,
|
|
77
|
+
attributionMode,
|
|
78
|
+
extractor: result.extractor ?? null,
|
|
79
|
+
evidenceIndex: index,
|
|
80
|
+
evidenceCount: results.length,
|
|
81
|
+
messagePartCount: params.messageParts?.length ?? 0,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
this.log.info(
|
|
86
|
+
`[brain] Harvested ${result.source} evidence: ${result.value.toFixed(2)} for episode ${matchingEpisode.id} (${result.reason})`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
detectLabel(role: string, content: string, messageParts?: HarvestMessagePart[]): HarvestResult | null {
|
|
92
|
+
return detectEvidence(role, content, messageParts);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
detectLabels(role: string, content: string, messageParts?: HarvestMessagePart[]): HarvestResult[] {
|
|
96
|
+
return detectEvidenceBatch(role, content, messageParts);
|
|
97
|
+
}
|
|
98
|
+
}
|