@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.
Files changed (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +412 -0
  3. package/bin/openclawbrain.js +15 -0
  4. package/docs/END_STATE.md +244 -0
  5. package/docs/EVIDENCE.md +128 -0
  6. package/docs/RELEASE_CONTRACT.md +91 -0
  7. package/docs/agent-tools.md +106 -0
  8. package/docs/architecture.md +224 -0
  9. package/docs/configuration.md +178 -0
  10. package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/status.json +87 -0
  11. package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/summary.md +16 -0
  12. package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/trace.json +273 -0
  13. package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/validation-report.json +652 -0
  14. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/channels-status.txt +31 -0
  15. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/config-snapshot.json +66 -0
  16. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/doctor.json +14 -0
  17. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/gateway-probe.txt +34 -0
  18. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/gateway-status.txt +41 -0
  19. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/logs.txt +428 -0
  20. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/status-all.txt +60 -0
  21. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/status.json +223 -0
  22. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/summary.md +13 -0
  23. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/trace.json +4 -0
  24. package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/validation-report.json +334 -0
  25. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/channels-status.txt +25 -0
  26. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/config-snapshot.json +91 -0
  27. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/doctor.json +14 -0
  28. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/gateway-probe.txt +36 -0
  29. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/gateway-status.txt +44 -0
  30. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/logs.txt +428 -0
  31. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-doctor.json +10 -0
  32. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-sdk-probe.json +11 -0
  33. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-setup-only.json +12 -0
  34. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/summary.md +30 -0
  35. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/validation-report.json +72 -0
  36. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/status-all.txt +63 -0
  37. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/status.json +200 -0
  38. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/summary.md +13 -0
  39. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/trace.json +4 -0
  40. package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/validation-report.json +311 -0
  41. package/docs/evidence/README.md +16 -0
  42. package/docs/fts5.md +161 -0
  43. package/docs/tui.md +506 -0
  44. package/index.ts +1372 -0
  45. package/openclaw.plugin.json +136 -0
  46. package/package.json +66 -0
  47. package/src/assembler.ts +804 -0
  48. package/src/brain-cli.ts +316 -0
  49. package/src/brain-core/decay.ts +35 -0
  50. package/src/brain-core/episode.ts +82 -0
  51. package/src/brain-core/graph.ts +321 -0
  52. package/src/brain-core/health.ts +116 -0
  53. package/src/brain-core/mutator.ts +281 -0
  54. package/src/brain-core/pack.ts +117 -0
  55. package/src/brain-core/policy.ts +153 -0
  56. package/src/brain-core/replay.ts +1 -0
  57. package/src/brain-core/teacher.ts +105 -0
  58. package/src/brain-core/trace.ts +40 -0
  59. package/src/brain-core/traverse.ts +230 -0
  60. package/src/brain-core/types.ts +405 -0
  61. package/src/brain-core/update.ts +123 -0
  62. package/src/brain-harvest/human.ts +46 -0
  63. package/src/brain-harvest/scanner.ts +98 -0
  64. package/src/brain-harvest/self.ts +147 -0
  65. package/src/brain-runtime/assembler-extension.ts +230 -0
  66. package/src/brain-runtime/evidence-detectors.ts +68 -0
  67. package/src/brain-runtime/graph-io.ts +72 -0
  68. package/src/brain-runtime/harvester-extension.ts +98 -0
  69. package/src/brain-runtime/service.ts +659 -0
  70. package/src/brain-runtime/tools.ts +109 -0
  71. package/src/brain-runtime/worker-state.ts +106 -0
  72. package/src/brain-runtime/worker-supervisor.ts +169 -0
  73. package/src/brain-store/embedding.ts +179 -0
  74. package/src/brain-store/init.ts +347 -0
  75. package/src/brain-store/migrations.ts +188 -0
  76. package/src/brain-store/store.ts +816 -0
  77. package/src/brain-worker/child-runner.ts +321 -0
  78. package/src/brain-worker/jobs.ts +12 -0
  79. package/src/brain-worker/mutation-job.ts +5 -0
  80. package/src/brain-worker/promotion-job.ts +5 -0
  81. package/src/brain-worker/protocol.ts +79 -0
  82. package/src/brain-worker/teacher-job.ts +5 -0
  83. package/src/brain-worker/update-job.ts +5 -0
  84. package/src/brain-worker/worker.ts +422 -0
  85. package/src/compaction.ts +1332 -0
  86. package/src/db/config.ts +265 -0
  87. package/src/db/connection.ts +72 -0
  88. package/src/db/features.ts +42 -0
  89. package/src/db/migration.ts +561 -0
  90. package/src/engine.ts +1995 -0
  91. package/src/expansion-auth.ts +351 -0
  92. package/src/expansion-policy.ts +303 -0
  93. package/src/expansion.ts +383 -0
  94. package/src/integrity.ts +600 -0
  95. package/src/large-files.ts +527 -0
  96. package/src/openclaw-bridge.ts +22 -0
  97. package/src/retrieval.ts +357 -0
  98. package/src/store/conversation-store.ts +748 -0
  99. package/src/store/fts5-sanitize.ts +29 -0
  100. package/src/store/full-text-fallback.ts +74 -0
  101. package/src/store/index.ts +29 -0
  102. package/src/store/summary-store.ts +918 -0
  103. package/src/summarize.ts +847 -0
  104. package/src/tools/common.ts +53 -0
  105. package/src/tools/lcm-conversation-scope.ts +76 -0
  106. package/src/tools/lcm-describe-tool.ts +234 -0
  107. package/src/tools/lcm-expand-query-tool.ts +594 -0
  108. package/src/tools/lcm-expand-tool.delegation.ts +556 -0
  109. package/src/tools/lcm-expand-tool.ts +448 -0
  110. package/src/tools/lcm-expansion-recursion-guard.ts +286 -0
  111. package/src/tools/lcm-grep-tool.ts +200 -0
  112. package/src/transcript-repair.ts +301 -0
  113. package/src/types.ts +149 -0
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Agent tools: brain_teach, brain_status, brain_trace.
3
+ *
4
+ * Follows lossless-claw's tool pattern:
5
+ * - Typebox schemas for input validation
6
+ * - jsonResult() for output formatting
7
+ * - createX() factory functions
8
+ */
9
+
10
+ import { Type } from "@sinclair/typebox";
11
+
12
+ // Use LCM's existing tool helpers
13
+ const jsonResult = (payload: unknown) => ({
14
+ content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }],
15
+ details: payload,
16
+ });
17
+
18
+ // ─── Types ───
19
+
20
+ interface BrainToolDeps {
21
+ teach: (
22
+ instruction: string,
23
+ kind?: string,
24
+ tags?: string[],
25
+ ) => Promise<{ nodeId: string; packVersion?: number | null }>;
26
+ status: () => Promise<Record<string, unknown>>;
27
+ getTrace: (traceId?: string) => Promise<Record<string, unknown> | null>;
28
+ }
29
+
30
+ // ─── brain_teach ───
31
+
32
+ const BrainTeachSchema = Type.Object({
33
+ instruction: Type.String({
34
+ description: "What to remember or correct. Examples: 'use gh pr create, not hub', 'for deployment errors, always check CI logs first'",
35
+ }),
36
+ kind: Type.Optional(Type.String({
37
+ description: 'Node kind: "correction", "toolcard", or "workflow". Default: "correction".',
38
+ enum: ["correction", "toolcard", "workflow"],
39
+ })),
40
+ tags: Type.Optional(Type.Array(Type.String(), {
41
+ description: "Tags for filtering and retrieval",
42
+ })),
43
+ });
44
+
45
+ export function createBrainTeachTool(deps: BrainToolDeps) {
46
+ return {
47
+ name: "brain_teach",
48
+ label: "Brain Teach",
49
+ description: "Teach the brain a correction, pattern, or preference. Creates a high-trust human node that will be surfaced in future context.",
50
+ parameters: BrainTeachSchema,
51
+ async execute(_toolCallId: string, params: Record<string, unknown>) {
52
+ const instruction = params.instruction as string;
53
+ const kind = (params.kind as string) ?? "correction";
54
+ const tags = (params.tags as string[]) ?? [];
55
+
56
+ const result = await deps.teach(instruction, kind, tags);
57
+ return jsonResult({
58
+ success: true,
59
+ nodeId: result.nodeId,
60
+ packVersion: result.packVersion ?? null,
61
+ message: `Brain will remember: "${instruction.slice(0, 80)}${instruction.length > 80 ? "..." : ""}"`,
62
+ });
63
+ },
64
+ };
65
+ }
66
+
67
+ // ─── brain_status ───
68
+
69
+ const BrainStatusSchema = Type.Object({});
70
+
71
+ export function createBrainStatusTool(deps: BrainToolDeps) {
72
+ return {
73
+ name: "brain_status",
74
+ label: "Brain Status",
75
+ description: "Show brain health: node/edge counts, pack version, learning stats, recent traces.",
76
+ parameters: BrainStatusSchema,
77
+ async execute() {
78
+ const status = await deps.status();
79
+ return jsonResult(status);
80
+ },
81
+ };
82
+ }
83
+
84
+ // ─── brain_trace ───
85
+
86
+ const BrainTraceSchema = Type.Object({
87
+ traceId: Type.Optional(Type.String({
88
+ description: "Specific trace ID (bt_...). Default: most recent trace.",
89
+ })),
90
+ });
91
+
92
+ export function createBrainTraceTool(deps: BrainToolDeps) {
93
+ return {
94
+ name: "brain_trace",
95
+ label: "Brain Trace",
96
+ description: "Show the detailed decision trace for the most recent (or specified) brain query. Shows seed ranking, traversal steps, candidate probabilities, fired nodes, and vetoed nodes.",
97
+ parameters: BrainTraceSchema,
98
+ async execute(_toolCallId: string, params: Record<string, unknown>) {
99
+ const traceId = params.traceId as string | undefined;
100
+ const trace = await deps.getTrace(traceId);
101
+
102
+ if (!trace) {
103
+ return jsonResult({ error: "No trace found", traceId });
104
+ }
105
+
106
+ return jsonResult(trace);
107
+ },
108
+ };
109
+ }
@@ -0,0 +1,106 @@
1
+ import type { BrainConfig } from "../brain-core/types.js";
2
+ import type { OpenClawBrainRuntimeConfig } from "../db/config.js";
3
+ import type { BrainStore } from "../brain-store/store.js";
4
+
5
+ export type WorkerLastExit = {
6
+ code: number | null;
7
+ signal: NodeJS.Signals | null;
8
+ at: number;
9
+ };
10
+
11
+ export type WorkerRuntimeState = {
12
+ workerMode: BrainConfig["workerMode"];
13
+ workerModeDevOnly: boolean;
14
+ workerModeWarning: string | null;
15
+ workerPid: number | null;
16
+ workerStatus: string | null;
17
+ workerLastHeartbeatAt: number | null;
18
+ workerLastReadyAt: number | null;
19
+ workerHealthy: boolean;
20
+ workerLastExit: WorkerLastExit | null;
21
+ workerRestartCount: number;
22
+ workerLastRestartAt: number | null;
23
+ workerStartedAt: number | null;
24
+ workerLastReloadRequestedAt: number | null;
25
+ workerLastReloadAckAt: number | null;
26
+ workerLastTickAt: number | null;
27
+ workerLastTickResultAt: number | null;
28
+ workerLastTickOk: boolean | null;
29
+ workerLastTickError: string | null;
30
+ workerLastFatalError: string | null;
31
+ };
32
+
33
+ function readInt(store: BrainStore, key: string): number | null {
34
+ const raw = store.getTrainingState(key)?.trim();
35
+ if (!raw) {
36
+ return null;
37
+ }
38
+ const value = Number.parseInt(raw, 10);
39
+ return Number.isFinite(value) && value > 0 ? value : null;
40
+ }
41
+
42
+ function readString(store: BrainStore, key: string): string | null {
43
+ const raw = store.getTrainingState(key)?.trim() ?? "";
44
+ return raw.length > 0 ? raw : null;
45
+ }
46
+
47
+ function readBoolean(store: BrainStore, key: string): boolean | null {
48
+ const raw = readString(store, key);
49
+ if (raw === "true") {
50
+ return true;
51
+ }
52
+ if (raw === "false") {
53
+ return false;
54
+ }
55
+ return null;
56
+ }
57
+
58
+ type WorkerStateConfig = Pick<BrainConfig, "workerMode" | "workerHeartbeatTimeoutMs">
59
+ | Pick<OpenClawBrainRuntimeConfig, "workerMode" | "workerHeartbeatTimeoutMs">;
60
+
61
+ export function readWorkerRuntimeState(
62
+ store: BrainStore,
63
+ config: WorkerStateConfig,
64
+ ): WorkerRuntimeState {
65
+ const workerMode = config.workerMode ?? "child";
66
+ const workerHeartbeatTimeoutMs = config.workerHeartbeatTimeoutMs ?? 90_000;
67
+ const workerPid = readInt(store, "worker_pid");
68
+ const workerLastHeartbeatAt = readInt(store, "worker_last_heartbeat_at");
69
+ const workerLastReadyAt = readInt(store, "worker_last_ready_at");
70
+ const workerLastExitAt = readInt(store, "worker_last_exit_at");
71
+ const workerLastExitCodeRaw = store.getTrainingState("worker_last_exit_code")?.trim() ?? "";
72
+ const workerLastExitSignal = readString(store, "worker_last_exit_signal");
73
+ const workerLastExit = workerLastExitAt
74
+ ? {
75
+ code: workerLastExitCodeRaw.length > 0 ? Number.parseInt(workerLastExitCodeRaw, 10) : null,
76
+ signal: workerLastExitSignal as NodeJS.Signals | null,
77
+ at: workerLastExitAt,
78
+ }
79
+ : null;
80
+
81
+ return {
82
+ workerMode,
83
+ workerModeDevOnly: workerMode === "in_process",
84
+ workerModeWarning: workerMode === "in_process"
85
+ ? "in_process worker mode is dev-only; use child mode for production operator truth"
86
+ : null,
87
+ workerPid,
88
+ workerStatus: readString(store, "worker_status") ?? (workerMode === "child" ? "unknown" : "running"),
89
+ workerLastHeartbeatAt,
90
+ workerLastReadyAt,
91
+ workerHealthy: workerMode === "child"
92
+ ? Boolean(workerLastHeartbeatAt && (Date.now() - workerLastHeartbeatAt) < workerHeartbeatTimeoutMs)
93
+ : true,
94
+ workerLastExit,
95
+ workerRestartCount: readInt(store, "worker_restart_count") ?? 0,
96
+ workerLastRestartAt: readInt(store, "worker_last_restart_at"),
97
+ workerStartedAt: readInt(store, "worker_started_at"),
98
+ workerLastReloadRequestedAt: readInt(store, "worker_last_reload_requested_at"),
99
+ workerLastReloadAckAt: readInt(store, "worker_last_reload_ack_at"),
100
+ workerLastTickAt: readInt(store, "worker_last_tick_at"),
101
+ workerLastTickResultAt: readInt(store, "worker_last_tick_result_at"),
102
+ workerLastTickOk: readBoolean(store, "worker_last_tick_ok"),
103
+ workerLastTickError: readString(store, "worker_last_tick_error"),
104
+ workerLastFatalError: readString(store, "worker_last_fatal_error"),
105
+ };
106
+ }
@@ -0,0 +1,169 @@
1
+ import { fork, type ChildProcess } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+ import type { BrainConfig } from "../brain-core/types.js";
4
+ import type { BrainStore } from "../brain-store/store.js";
5
+ import type {
6
+ ChildToParentMessage,
7
+ ParentTeacherCompleteResultMessage,
8
+ ParentToChildMessage,
9
+ WorkerTeacherCompleteRequestMessage,
10
+ } from "../brain-worker/protocol.js";
11
+
12
+ export class WorkerSupervisor {
13
+ private child: ChildProcess | null = null;
14
+ private shouldRun = false;
15
+ private restartTimer: ReturnType<typeof setTimeout> | null = null;
16
+
17
+ constructor(
18
+ private params: {
19
+ config: BrainConfig;
20
+ store: BrainStore;
21
+ log: { info: (msg: string) => void; warn: (msg: string) => void; error: (msg: string) => void };
22
+ teacherModel: { provider: string; model: string } | null;
23
+ isEnabled: () => boolean;
24
+ onPackPromoted: () => void;
25
+ onTeacherComplete: (
26
+ message: WorkerTeacherCompleteRequestMessage,
27
+ teacherModel: { provider: string; model: string } | null,
28
+ ) => Promise<ParentTeacherCompleteResultMessage>;
29
+ },
30
+ ) {}
31
+
32
+ start(): void {
33
+ if (!this.params.isEnabled()) {
34
+ return;
35
+ }
36
+ this.shouldRun = true;
37
+ this.ensureChildWorker(false);
38
+ }
39
+
40
+ stop(): void {
41
+ this.shouldRun = false;
42
+ if (this.restartTimer) {
43
+ clearTimeout(this.restartTimer);
44
+ this.restartTimer = null;
45
+ }
46
+ if (this.child) {
47
+ this.params.store.setTrainingState("worker_status", "stopping");
48
+ this.send({ type: "shutdown" });
49
+ const child = this.child;
50
+ setTimeout(() => {
51
+ if (this.child === child) {
52
+ this.child.kill("SIGTERM");
53
+ }
54
+ }, 2_000);
55
+ }
56
+ }
57
+
58
+ requestGraphReload(): void {
59
+ this.params.store.setTrainingState("worker_last_reload_requested_at", Date.now());
60
+ this.send({ type: "reload-graph" });
61
+ }
62
+
63
+ private ensureChildWorker(isRestart: boolean): void {
64
+ if (this.child || !this.params.isEnabled()) {
65
+ return;
66
+ }
67
+
68
+ const status = isRestart ? "restarting" : "starting";
69
+ this.params.store.setTrainingState("worker_mode", "child");
70
+ this.params.store.setTrainingState("worker_status", status);
71
+ if (isRestart) {
72
+ const nextCount = (Number.parseInt(this.params.store.getTrainingState("worker_restart_count") ?? "0", 10) || 0) + 1;
73
+ const restartedAt = Date.now();
74
+ this.params.store.setTrainingState("worker_restart_count", nextCount);
75
+ this.params.store.setTrainingState("worker_last_restart_at", restartedAt);
76
+ }
77
+
78
+ const child = fork(
79
+ fileURLToPath(new URL("../brain-worker/child-runner.ts", import.meta.url)),
80
+ [],
81
+ {
82
+ execArgv: ["--import", "tsx/esm"],
83
+ stdio: ["ignore", "pipe", "pipe", "ipc"],
84
+ env: {
85
+ ...process.env,
86
+ OPENCLAWBRAIN_CHILD_CONFIG_JSON: JSON.stringify(this.params.config),
87
+ OPENCLAWBRAIN_CHILD_TEACHER_MODEL_JSON: this.params.teacherModel
88
+ ? JSON.stringify(this.params.teacherModel)
89
+ : "",
90
+ },
91
+ },
92
+ );
93
+
94
+ this.child = child;
95
+
96
+ child.stdout?.on("data", (chunk) => {
97
+ const text = String(chunk).trim();
98
+ if (text) {
99
+ this.params.log.info(text);
100
+ }
101
+ });
102
+ child.stderr?.on("data", (chunk) => {
103
+ const text = String(chunk).trim();
104
+ if (text) {
105
+ this.params.log.warn(text);
106
+ }
107
+ });
108
+ child.on("message", (message) => {
109
+ void this.handleMessage(message as ChildToParentMessage, child);
110
+ });
111
+ child.on("exit", (code, signal) => {
112
+ const exitedAt = Date.now();
113
+ this.params.store.setTrainingState("worker_pid", "");
114
+ this.params.store.setTrainingState("worker_last_exit_at", exitedAt);
115
+ this.params.store.setTrainingState("worker_last_exit_code", code === null ? "" : String(code));
116
+ this.params.store.setTrainingState("worker_last_exit_signal", signal ?? "");
117
+ if (this.child === child) {
118
+ this.child = null;
119
+ }
120
+ const nextStatus = this.shouldRun && this.params.isEnabled() ? "restarting" : "stopped";
121
+ this.params.store.setTrainingState("worker_status", nextStatus);
122
+ if (this.shouldRun && this.params.isEnabled()) {
123
+ this.restartTimer = setTimeout(() => {
124
+ this.restartTimer = null;
125
+ this.ensureChildWorker(true);
126
+ }, this.params.config.workerRestartDelayMs);
127
+ }
128
+ });
129
+ }
130
+
131
+ private send(message: ParentToChildMessage): void {
132
+ this.child?.send(message);
133
+ }
134
+
135
+ private async handleMessage(message: ChildToParentMessage, child: ChildProcess): Promise<void> {
136
+ switch (message.type) {
137
+ case "ready": {
138
+ this.params.store.setTrainingState("worker_last_ready_at", message.at);
139
+ this.params.store.setTrainingState("worker_last_fatal_error", "");
140
+ return;
141
+ }
142
+ case "heartbeat": {
143
+ return;
144
+ }
145
+ case "reload-graph-ack": {
146
+ return;
147
+ }
148
+ case "tick-result": {
149
+ return;
150
+ }
151
+ case "pack-promoted": {
152
+ this.params.onPackPromoted();
153
+ return;
154
+ }
155
+ case "teacher-complete": {
156
+ const result = await this.params.onTeacherComplete(message, this.params.teacherModel);
157
+ child.send?.(result);
158
+ return;
159
+ }
160
+ case "fatal-error": {
161
+ this.params.store.setTrainingState("worker_last_fatal_error", message.error);
162
+ this.params.log.error(`[brain] child worker fatal error: ${message.error}`);
163
+ return;
164
+ }
165
+ default:
166
+ return;
167
+ }
168
+ }
169
+ }
@@ -0,0 +1,179 @@
1
+ import type { OpenClawBrainRuntimeConfig } from "../db/config.js";
2
+
3
+ export type BrainEmbeddingFn = (text: string) => Promise<Float32Array>;
4
+
5
+ export type BrainEmbeddingOptions = {
6
+ config: OpenClawBrainRuntimeConfig;
7
+ getApiKey?: (provider: string, model: string) => Promise<string | undefined>;
8
+ log?: {
9
+ debug?: (msg: string) => void;
10
+ warn?: (msg: string) => void;
11
+ };
12
+ };
13
+
14
+ export type EmbeddingAuthMode = "none" | "api_key";
15
+
16
+ export type EmbeddingConfigSummary = {
17
+ baseUrl: string;
18
+ authMode: EmbeddingAuthMode | "unknown";
19
+ error: string | null;
20
+ };
21
+
22
+ function normalizeProvider(provider: string): string {
23
+ return provider.trim().toLowerCase();
24
+ }
25
+
26
+ function trimTrailingSlashes(value: string): string {
27
+ return value.replace(/\/+$/, "");
28
+ }
29
+
30
+ function resolveExplicitBaseUrl(config: OpenClawBrainRuntimeConfig): string | null {
31
+ const explicit = config.embeddingBaseUrl.trim();
32
+ return explicit ? trimTrailingSlashes(explicit) : null;
33
+ }
34
+
35
+ export function resolveEmbeddingBaseUrl(config: OpenClawBrainRuntimeConfig): string {
36
+ const explicit = resolveExplicitBaseUrl(config);
37
+ if (explicit) {
38
+ return explicit;
39
+ }
40
+
41
+ const provider = normalizeProvider(config.embeddingProvider);
42
+ if (provider === "openai" || provider === "openai-resp") {
43
+ return "https://api.openai.com/v1";
44
+ }
45
+ if (provider === "ollama") {
46
+ return "http://127.0.0.1:11434/v1";
47
+ }
48
+
49
+ throw new Error(`Unsupported embedding provider "${config.embeddingProvider}"`);
50
+ }
51
+
52
+ function isLoopbackHostname(hostname: string): boolean {
53
+ const normalized = hostname.trim().toLowerCase();
54
+ return normalized === "localhost"
55
+ || normalized === "127.0.0.1"
56
+ || normalized === "::1"
57
+ || normalized === "[::1]";
58
+ }
59
+
60
+ export function isLocalEmbeddingBaseUrl(baseUrl: string): boolean {
61
+ try {
62
+ const parsed = new URL(baseUrl);
63
+ return isLoopbackHostname(parsed.hostname);
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+
69
+ export function resolveEmbeddingAuthMode(config: OpenClawBrainRuntimeConfig): EmbeddingAuthMode {
70
+ const explicitApiKey = process.env.OPENCLAWBRAIN_EMBEDDING_API_KEY?.trim();
71
+ if (explicitApiKey) {
72
+ return "api_key";
73
+ }
74
+
75
+ const provider = normalizeProvider(config.embeddingProvider);
76
+ if (provider === "ollama") {
77
+ return "none";
78
+ }
79
+
80
+ const baseUrl = resolveEmbeddingBaseUrl(config);
81
+ if (isLocalEmbeddingBaseUrl(baseUrl)) {
82
+ return "none";
83
+ }
84
+
85
+ return "api_key";
86
+ }
87
+
88
+ export function describeEmbeddingConfig(config: OpenClawBrainRuntimeConfig): EmbeddingConfigSummary {
89
+ try {
90
+ return {
91
+ baseUrl: resolveEmbeddingBaseUrl(config),
92
+ authMode: resolveEmbeddingAuthMode(config),
93
+ error: null,
94
+ };
95
+ } catch (error) {
96
+ return {
97
+ baseUrl: config.embeddingBaseUrl.trim(),
98
+ authMode: "unknown",
99
+ error: (error as Error).message,
100
+ };
101
+ }
102
+ }
103
+
104
+ async function resolveApiKey(
105
+ config: OpenClawBrainRuntimeConfig,
106
+ getApiKey?: (provider: string, model: string) => Promise<string | undefined>,
107
+ ): Promise<string | undefined> {
108
+ const explicitEmbeddingKey = process.env.OPENCLAWBRAIN_EMBEDDING_API_KEY?.trim();
109
+ if (explicitEmbeddingKey) {
110
+ return explicitEmbeddingKey;
111
+ }
112
+
113
+ if (resolveEmbeddingAuthMode(config) === "none") {
114
+ return undefined;
115
+ }
116
+
117
+ if (getApiKey) {
118
+ const key = await getApiKey(config.embeddingProvider, config.embeddingModel);
119
+ if (key) {
120
+ return key;
121
+ }
122
+ }
123
+
124
+ if (normalizeProvider(config.embeddingProvider) === "openai" || normalizeProvider(config.embeddingProvider) === "openai-resp") {
125
+ const envKey = process.env.OPENAI_API_KEY?.trim();
126
+ if (envKey) {
127
+ return envKey;
128
+ }
129
+ }
130
+
131
+ throw new Error(`Missing API key for embedding provider "${config.embeddingProvider}"`);
132
+ }
133
+
134
+ export function hasEmbeddingConfiguration(config: OpenClawBrainRuntimeConfig): boolean {
135
+ return config.embeddingModel.trim().length > 0;
136
+ }
137
+
138
+ export function createEmbeddingClient(options: BrainEmbeddingOptions): BrainEmbeddingFn | null {
139
+ const { config, getApiKey, log } = options;
140
+ if (!hasEmbeddingConfiguration(config)) {
141
+ log?.warn?.("[brain] Embedding model is unset; learned retrieval is disabled until init/configuration is complete");
142
+ return null;
143
+ }
144
+
145
+ return async (text: string): Promise<Float32Array> => {
146
+ const apiKey = await resolveApiKey(config, getApiKey);
147
+ const baseUrl = resolveEmbeddingBaseUrl(config);
148
+ const headers: Record<string, string> = {
149
+ "content-type": "application/json",
150
+ };
151
+ if (apiKey) {
152
+ headers.authorization = `Bearer ${apiKey}`;
153
+ }
154
+
155
+ const response = await fetch(`${baseUrl}/embeddings`, {
156
+ method: "POST",
157
+ headers,
158
+ body: JSON.stringify({
159
+ model: config.embeddingModel,
160
+ input: text,
161
+ }),
162
+ });
163
+
164
+ if (!response.ok) {
165
+ const body = await response.text();
166
+ throw new Error(`Embedding request failed (${response.status}): ${body.slice(0, 200)}`);
167
+ }
168
+
169
+ const payload = await response.json() as {
170
+ data?: Array<{ embedding?: number[] }>;
171
+ };
172
+ const embedding = payload.data?.[0]?.embedding;
173
+ if (!Array.isArray(embedding) || embedding.length === 0) {
174
+ throw new Error("Embedding response did not include a vector");
175
+ }
176
+
177
+ return new Float32Array(embedding.map((value) => Number(value)));
178
+ };
179
+ }