dsh-tdai-memory 0.2.13 → 0.2.14

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 (69) hide show
  1. package/LICENSE +21 -21
  2. package/THIRD-PARTY-NOTICES.md +20 -20
  3. package/client.js +8 -4
  4. package/cordis.patch.yml +8 -8
  5. package/index.js +414 -414
  6. package/package.json +1 -1
  7. package/recall-inject.js +86 -86
  8. package/vendor/tdai/adapters/index.js +15 -15
  9. package/vendor/tdai/adapters/openclaw/host-adapter.js +83 -83
  10. package/vendor/tdai/adapters/openclaw/index.js +5 -5
  11. package/vendor/tdai/adapters/openclaw/llm-runner.js +69 -69
  12. package/vendor/tdai/adapters/standalone/host-adapter.js +58 -58
  13. package/vendor/tdai/adapters/standalone/index.js +5 -5
  14. package/vendor/tdai/adapters/standalone/llm-runner.js +246 -246
  15. package/vendor/tdai/config.js +363 -363
  16. package/vendor/tdai/core/conversation/l0-recorder.js +420 -420
  17. package/vendor/tdai/core/hooks/auto-capture.js +236 -236
  18. package/vendor/tdai/core/hooks/auto-recall.js +653 -653
  19. package/vendor/tdai/core/index.js +8 -8
  20. package/vendor/tdai/core/persona/persona-generator.js +184 -184
  21. package/vendor/tdai/core/persona/persona-trigger.js +94 -94
  22. package/vendor/tdai/core/profile/profile-sync.js +200 -200
  23. package/vendor/tdai/core/prompts/l1-dedup.js +136 -136
  24. package/vendor/tdai/core/prompts/l1-extraction.js +132 -132
  25. package/vendor/tdai/core/prompts/persona-generation.js +160 -160
  26. package/vendor/tdai/core/prompts/scene-extraction.js +269 -269
  27. package/vendor/tdai/core/record/l1-dedup.js +318 -318
  28. package/vendor/tdai/core/record/l1-extractor.js +379 -379
  29. package/vendor/tdai/core/record/l1-reader.js +178 -178
  30. package/vendor/tdai/core/record/l1-writer.js +141 -141
  31. package/vendor/tdai/core/report/reporter.js +86 -86
  32. package/vendor/tdai/core/scene/filename-normalizer.js +161 -161
  33. package/vendor/tdai/core/scene/scene-extractor.js +410 -410
  34. package/vendor/tdai/core/scene/scene-format.js +53 -53
  35. package/vendor/tdai/core/scene/scene-index.js +82 -82
  36. package/vendor/tdai/core/scene/scene-navigation.js +60 -60
  37. package/vendor/tdai/core/seed/input.js +404 -404
  38. package/vendor/tdai/core/seed/seed-runtime.js +314 -314
  39. package/vendor/tdai/core/seed/types.js +9 -9
  40. package/vendor/tdai/core/store/bm25-client.js +117 -117
  41. package/vendor/tdai/core/store/bm25-local.js +68 -68
  42. package/vendor/tdai/core/store/embedding.js +632 -632
  43. package/vendor/tdai/core/store/factory.js +95 -95
  44. package/vendor/tdai/core/store/search-utils.js +54 -54
  45. package/vendor/tdai/core/store/sqlite.js +1822 -1822
  46. package/vendor/tdai/core/store/tcvdb-client.js +221 -221
  47. package/vendor/tdai/core/store/tcvdb.js +1047 -1047
  48. package/vendor/tdai/core/store/types.js +17 -17
  49. package/vendor/tdai/core/tdai-core.js +446 -446
  50. package/vendor/tdai/core/tools/conversation-search.js +199 -199
  51. package/vendor/tdai/core/tools/memory-search.js +207 -207
  52. package/vendor/tdai/core/types.js +12 -12
  53. package/vendor/tdai/utils/backup.js +189 -189
  54. package/vendor/tdai/utils/checkpoint.js +360 -360
  55. package/vendor/tdai/utils/clean-context-runner.js +472 -472
  56. package/vendor/tdai/utils/ensure-hook-policy.js +213 -213
  57. package/vendor/tdai/utils/env.js +13 -13
  58. package/vendor/tdai/utils/managed-timer.js +127 -127
  59. package/vendor/tdai/utils/manifest.js +91 -91
  60. package/vendor/tdai/utils/memory-cleaner.js +300 -300
  61. package/vendor/tdai/utils/no-think-fetch.js +109 -109
  62. package/vendor/tdai/utils/openclaw-state-dir.js +26 -26
  63. package/vendor/tdai/utils/pipeline-factory.js +517 -517
  64. package/vendor/tdai/utils/pipeline-manager.js +892 -892
  65. package/vendor/tdai/utils/sanitize.js +360 -360
  66. package/vendor/tdai/utils/serial-queue.js +103 -103
  67. package/vendor/tdai/utils/session-filter.js +88 -88
  68. package/vendor/tdai/utils/text-utils.js +29 -29
  69. package/vendor/tdai/utils/time.js +252 -252
@@ -1,246 +1,246 @@
1
- /**
2
- * StandaloneLLMRunner — powered by Vercel AI SDK (`ai` + `@ai-sdk/openai`).
3
- *
4
- * This runner does NOT depend on OpenClaw's `runEmbeddedPiAgent`. It is designed
5
- * for the Hermes Gateway scenario where TDAI runs as an independent Node.js sidecar
6
- * without the OpenClaw host.
7
- *
8
- * Capabilities:
9
- * - `enableTools: false`: pure text output (L1 extraction, L1 dedup)
10
- * - `enableTools: true`: automatic tool-call loop with local file operations
11
- * (L2 scene, L3 persona) via AI SDK's `maxSteps`
12
- *
13
- * Tool sandbox:
14
- * When tools are enabled, three basic file operations are exposed:
15
- * `read_file`, `write_to_file`, `replace_in_file`.
16
- * All file paths are resolved relative to `workspaceDir`, enforcing sandbox boundaries.
17
- */
18
- import fsPromises from "node:fs/promises";
19
- import path from "node:path";
20
- import { generateText, tool, stepCountIs, jsonSchema } from "ai";
21
- import { createOpenAI } from "@ai-sdk/openai";
22
- import { report } from "../../core/report/reporter.js";
23
- import { createNoThinkFetch } from "../../utils/no-think-fetch.js";
24
- const TAG = "[memory-tdai] [standalone-runner]";
25
- // Max iterations in the tool-call loop to prevent infinite loops
26
- const MAX_TOOL_ITERATIONS = 20;
27
- // ============================
28
- // Sandboxed tool execution helpers
29
- // ============================
30
- function resolveSandboxedPath(workspaceDir, relativePath) {
31
- const resolved = path.resolve(workspaceDir, relativePath);
32
- if (!resolved.startsWith(path.resolve(workspaceDir))) {
33
- return null;
34
- }
35
- return resolved;
36
- }
37
- // ============================
38
- // Tool definitions (Vercel AI SDK `tool()` format)
39
- // ============================
40
- function createSandboxedTools(workspaceDir, logger) {
41
- return {
42
- read_file: tool({
43
- description: "Read the contents of a file at the given relative path.",
44
- inputSchema: jsonSchema({
45
- type: "object",
46
- properties: {
47
- path: { type: "string", description: "Relative file path to read." },
48
- },
49
- required: ["path"],
50
- }),
51
- execute: (async (args) => {
52
- const resolved = resolveSandboxedPath(workspaceDir, args.path);
53
- if (!resolved)
54
- return JSON.stringify({ error: `Path "${args.path}" escapes workspace boundary.` });
55
- try {
56
- return await fsPromises.readFile(resolved, "utf-8");
57
- }
58
- catch (err) {
59
- const msg = err instanceof Error ? err.message : String(err);
60
- logger?.warn?.(`${TAG} read_file failed: ${msg}`);
61
- return JSON.stringify({ error: msg });
62
- }
63
- }),
64
- }),
65
- write_to_file: tool({
66
- description: "Write content to a file at the given relative path. Creates or overwrites.",
67
- inputSchema: jsonSchema({
68
- type: "object",
69
- properties: {
70
- path: { type: "string", description: "Relative file path to write." },
71
- content: { type: "string", description: "Content to write." },
72
- },
73
- required: ["path", "content"],
74
- }),
75
- execute: (async (args) => {
76
- const resolved = resolveSandboxedPath(workspaceDir, args.path);
77
- if (!resolved)
78
- return JSON.stringify({ error: `Path "${args.path}" escapes workspace boundary.` });
79
- try {
80
- await fsPromises.mkdir(path.dirname(resolved), { recursive: true });
81
- await fsPromises.writeFile(resolved, args.content, "utf-8");
82
- return JSON.stringify({ success: true });
83
- }
84
- catch (err) {
85
- const msg = err instanceof Error ? err.message : String(err);
86
- logger?.warn?.(`${TAG} write_to_file failed: ${msg}`);
87
- return JSON.stringify({ error: msg });
88
- }
89
- }),
90
- }),
91
- replace_in_file: tool({
92
- description: "Replace an exact substring in a file with new content.",
93
- inputSchema: jsonSchema({
94
- type: "object",
95
- properties: {
96
- path: { type: "string", description: "Relative file path." },
97
- old_str: { type: "string", description: "Exact string to find and replace." },
98
- new_str: { type: "string", description: "Replacement string." },
99
- },
100
- required: ["path", "old_str", "new_str"],
101
- }),
102
- execute: (async (args) => {
103
- const resolved = resolveSandboxedPath(workspaceDir, args.path);
104
- if (!resolved)
105
- return JSON.stringify({ error: `Path "${args.path}" escapes workspace boundary.` });
106
- if (!args.old_str)
107
- return JSON.stringify({ error: "old_str cannot be empty." });
108
- try {
109
- const existing = await fsPromises.readFile(resolved, "utf-8");
110
- if (!existing.includes(args.old_str)) {
111
- return JSON.stringify({ error: `old_str not found in file "${args.path}".` });
112
- }
113
- const updated = existing.replace(args.old_str, args.new_str);
114
- await fsPromises.writeFile(resolved, updated, "utf-8");
115
- return JSON.stringify({ success: true });
116
- }
117
- catch (err) {
118
- const msg = err instanceof Error ? err.message : String(err);
119
- logger?.warn?.(`${TAG} replace_in_file failed: ${msg}`);
120
- return JSON.stringify({ error: msg });
121
- }
122
- }),
123
- }),
124
- };
125
- }
126
- // ============================
127
- // StandaloneLLMRunner
128
- // ============================
129
- export class StandaloneLLMRunner {
130
- config;
131
- model;
132
- enableTools;
133
- logger;
134
- customFetch;
135
- constructor(opts) {
136
- this.config = opts.config;
137
- this.model = opts.model ?? opts.config.model;
138
- this.enableTools = opts.enableTools ?? false;
139
- this.logger = opts.logger;
140
- this.customFetch = opts.config.disableThinking
141
- ? createNoThinkFetch(opts.config.disableThinking)
142
- : undefined;
143
- }
144
- async run(params) {
145
- const runStartMs = Date.now();
146
- const timeoutMs = params.timeoutMs ?? this.config.timeoutMs ?? 120_000;
147
- const maxTokens = params.maxTokens ?? this.config.maxTokens ?? 4096;
148
- const workspaceDir = params.workspaceDir ?? process.cwd();
149
- this.logger?.debug?.(`${TAG} run() start: taskId=${params.taskId}, model=${this.model}, ` +
150
- `tools=${this.enableTools}, timeout=${timeoutMs}ms`);
151
- // Create OpenAI-compatible provider via AI SDK
152
- // Use "compatible" mode to call /chat/completions (not Responses API),
153
- // which works with all OpenAI-compatible backends (DeepSeek, Qwen, etc.)
154
- const provider = createOpenAI({
155
- baseURL: this.config.baseUrl,
156
- apiKey: this.config.apiKey,
157
- compatibility: "compatible",
158
- ...(this.customFetch ? { fetch: this.customFetch } : {}),
159
- });
160
- // For pure text tasks like L1 extraction, avoid exposing any tools.
161
- const tools = this.enableTools
162
- ? createSandboxedTools(workspaceDir, this.logger)
163
- : undefined;
164
- try {
165
- const result = await generateText({
166
- model: provider.chat(this.model),
167
- system: params.systemPrompt,
168
- prompt: params.prompt,
169
- ...(tools ? { tools } : {}),
170
- stopWhen: stepCountIs(this.enableTools ? MAX_TOOL_ITERATIONS : 1),
171
- maxOutputTokens: maxTokens,
172
- abortSignal: AbortSignal.timeout(timeoutMs),
173
- });
174
- const text = result.text.trim();
175
- const totalMs = Date.now() - runStartMs;
176
- this.logger?.debug?.(`${TAG} run() completed: ${totalMs}ms, steps=${result.steps.length}, output=${text.length} chars`);
177
- // Log tool usage if any
178
- if (result.steps.length > 1) {
179
- const toolCalls = result.steps.flatMap((s) => s.toolCalls ?? []);
180
- this.logger?.debug?.(`${TAG} Tool calls: ${toolCalls.map((tc) => tc.toolName).join(", ")}`);
181
- }
182
- // Metric
183
- if (params.instanceId) {
184
- report("llm_call", {
185
- taskId: params.taskId,
186
- provider: "standalone",
187
- model: this.model,
188
- inputLength: params.prompt.length,
189
- outputLength: text.length,
190
- totalDurationMs: totalMs,
191
- success: true,
192
- error: null,
193
- });
194
- }
195
- return text;
196
- }
197
- catch (err) {
198
- const totalMs = Date.now() - runStartMs;
199
- const errMsg = err instanceof Error ? err.message : String(err);
200
- this.logger?.error(`${TAG} run() failed after ${totalMs}ms: ${errMsg}`);
201
- if (params.instanceId) {
202
- report("llm_call", {
203
- taskId: params.taskId,
204
- provider: "standalone",
205
- model: this.model,
206
- inputLength: params.prompt.length,
207
- outputLength: 0,
208
- totalDurationMs: totalMs,
209
- success: false,
210
- error: errMsg,
211
- });
212
- }
213
- throw err;
214
- }
215
- }
216
- }
217
- /**
218
- * Factory that creates StandaloneLLMRunner instances.
219
- *
220
- * Used by the Gateway and Hermes host adapters.
221
- */
222
- export class StandaloneLLMRunnerFactory {
223
- config;
224
- logger;
225
- constructor(opts) {
226
- this.config = opts.config;
227
- this.logger = opts.logger;
228
- }
229
- createRunner(opts) {
230
- const enableTools = opts?.enableTools ?? false;
231
- const modelRef = opts?.modelRef;
232
- // Parse "provider/model" → just use the model part for OpenAI-compatible API
233
- let model = this.config.model;
234
- if (modelRef) {
235
- const slashIdx = modelRef.indexOf("/");
236
- model = slashIdx > 0 ? modelRef.slice(slashIdx + 1) : modelRef;
237
- }
238
- this.logger?.debug?.(`${TAG} Creating StandaloneLLMRunner: model=${model}, tools=${enableTools}`);
239
- return new StandaloneLLMRunner({
240
- config: this.config,
241
- model,
242
- enableTools,
243
- logger: this.logger,
244
- });
245
- }
246
- }
1
+ /**
2
+ * StandaloneLLMRunner — powered by Vercel AI SDK (`ai` + `@ai-sdk/openai`).
3
+ *
4
+ * This runner does NOT depend on OpenClaw's `runEmbeddedPiAgent`. It is designed
5
+ * for the Hermes Gateway scenario where TDAI runs as an independent Node.js sidecar
6
+ * without the OpenClaw host.
7
+ *
8
+ * Capabilities:
9
+ * - `enableTools: false`: pure text output (L1 extraction, L1 dedup)
10
+ * - `enableTools: true`: automatic tool-call loop with local file operations
11
+ * (L2 scene, L3 persona) via AI SDK's `maxSteps`
12
+ *
13
+ * Tool sandbox:
14
+ * When tools are enabled, three basic file operations are exposed:
15
+ * `read_file`, `write_to_file`, `replace_in_file`.
16
+ * All file paths are resolved relative to `workspaceDir`, enforcing sandbox boundaries.
17
+ */
18
+ import fsPromises from "node:fs/promises";
19
+ import path from "node:path";
20
+ import { generateText, tool, stepCountIs, jsonSchema } from "ai";
21
+ import { createOpenAI } from "@ai-sdk/openai";
22
+ import { report } from "../../core/report/reporter.js";
23
+ import { createNoThinkFetch } from "../../utils/no-think-fetch.js";
24
+ const TAG = "[memory-tdai] [standalone-runner]";
25
+ // Max iterations in the tool-call loop to prevent infinite loops
26
+ const MAX_TOOL_ITERATIONS = 20;
27
+ // ============================
28
+ // Sandboxed tool execution helpers
29
+ // ============================
30
+ function resolveSandboxedPath(workspaceDir, relativePath) {
31
+ const resolved = path.resolve(workspaceDir, relativePath);
32
+ if (!resolved.startsWith(path.resolve(workspaceDir))) {
33
+ return null;
34
+ }
35
+ return resolved;
36
+ }
37
+ // ============================
38
+ // Tool definitions (Vercel AI SDK `tool()` format)
39
+ // ============================
40
+ function createSandboxedTools(workspaceDir, logger) {
41
+ return {
42
+ read_file: tool({
43
+ description: "Read the contents of a file at the given relative path.",
44
+ inputSchema: jsonSchema({
45
+ type: "object",
46
+ properties: {
47
+ path: { type: "string", description: "Relative file path to read." },
48
+ },
49
+ required: ["path"],
50
+ }),
51
+ execute: (async (args) => {
52
+ const resolved = resolveSandboxedPath(workspaceDir, args.path);
53
+ if (!resolved)
54
+ return JSON.stringify({ error: `Path "${args.path}" escapes workspace boundary.` });
55
+ try {
56
+ return await fsPromises.readFile(resolved, "utf-8");
57
+ }
58
+ catch (err) {
59
+ const msg = err instanceof Error ? err.message : String(err);
60
+ logger?.warn?.(`${TAG} read_file failed: ${msg}`);
61
+ return JSON.stringify({ error: msg });
62
+ }
63
+ }),
64
+ }),
65
+ write_to_file: tool({
66
+ description: "Write content to a file at the given relative path. Creates or overwrites.",
67
+ inputSchema: jsonSchema({
68
+ type: "object",
69
+ properties: {
70
+ path: { type: "string", description: "Relative file path to write." },
71
+ content: { type: "string", description: "Content to write." },
72
+ },
73
+ required: ["path", "content"],
74
+ }),
75
+ execute: (async (args) => {
76
+ const resolved = resolveSandboxedPath(workspaceDir, args.path);
77
+ if (!resolved)
78
+ return JSON.stringify({ error: `Path "${args.path}" escapes workspace boundary.` });
79
+ try {
80
+ await fsPromises.mkdir(path.dirname(resolved), { recursive: true });
81
+ await fsPromises.writeFile(resolved, args.content, "utf-8");
82
+ return JSON.stringify({ success: true });
83
+ }
84
+ catch (err) {
85
+ const msg = err instanceof Error ? err.message : String(err);
86
+ logger?.warn?.(`${TAG} write_to_file failed: ${msg}`);
87
+ return JSON.stringify({ error: msg });
88
+ }
89
+ }),
90
+ }),
91
+ replace_in_file: tool({
92
+ description: "Replace an exact substring in a file with new content.",
93
+ inputSchema: jsonSchema({
94
+ type: "object",
95
+ properties: {
96
+ path: { type: "string", description: "Relative file path." },
97
+ old_str: { type: "string", description: "Exact string to find and replace." },
98
+ new_str: { type: "string", description: "Replacement string." },
99
+ },
100
+ required: ["path", "old_str", "new_str"],
101
+ }),
102
+ execute: (async (args) => {
103
+ const resolved = resolveSandboxedPath(workspaceDir, args.path);
104
+ if (!resolved)
105
+ return JSON.stringify({ error: `Path "${args.path}" escapes workspace boundary.` });
106
+ if (!args.old_str)
107
+ return JSON.stringify({ error: "old_str cannot be empty." });
108
+ try {
109
+ const existing = await fsPromises.readFile(resolved, "utf-8");
110
+ if (!existing.includes(args.old_str)) {
111
+ return JSON.stringify({ error: `old_str not found in file "${args.path}".` });
112
+ }
113
+ const updated = existing.replace(args.old_str, args.new_str);
114
+ await fsPromises.writeFile(resolved, updated, "utf-8");
115
+ return JSON.stringify({ success: true });
116
+ }
117
+ catch (err) {
118
+ const msg = err instanceof Error ? err.message : String(err);
119
+ logger?.warn?.(`${TAG} replace_in_file failed: ${msg}`);
120
+ return JSON.stringify({ error: msg });
121
+ }
122
+ }),
123
+ }),
124
+ };
125
+ }
126
+ // ============================
127
+ // StandaloneLLMRunner
128
+ // ============================
129
+ export class StandaloneLLMRunner {
130
+ config;
131
+ model;
132
+ enableTools;
133
+ logger;
134
+ customFetch;
135
+ constructor(opts) {
136
+ this.config = opts.config;
137
+ this.model = opts.model ?? opts.config.model;
138
+ this.enableTools = opts.enableTools ?? false;
139
+ this.logger = opts.logger;
140
+ this.customFetch = opts.config.disableThinking
141
+ ? createNoThinkFetch(opts.config.disableThinking)
142
+ : undefined;
143
+ }
144
+ async run(params) {
145
+ const runStartMs = Date.now();
146
+ const timeoutMs = params.timeoutMs ?? this.config.timeoutMs ?? 120_000;
147
+ const maxTokens = params.maxTokens ?? this.config.maxTokens ?? 4096;
148
+ const workspaceDir = params.workspaceDir ?? process.cwd();
149
+ this.logger?.debug?.(`${TAG} run() start: taskId=${params.taskId}, model=${this.model}, ` +
150
+ `tools=${this.enableTools}, timeout=${timeoutMs}ms`);
151
+ // Create OpenAI-compatible provider via AI SDK
152
+ // Use "compatible" mode to call /chat/completions (not Responses API),
153
+ // which works with all OpenAI-compatible backends (DeepSeek, Qwen, etc.)
154
+ const provider = createOpenAI({
155
+ baseURL: this.config.baseUrl,
156
+ apiKey: this.config.apiKey,
157
+ compatibility: "compatible",
158
+ ...(this.customFetch ? { fetch: this.customFetch } : {}),
159
+ });
160
+ // For pure text tasks like L1 extraction, avoid exposing any tools.
161
+ const tools = this.enableTools
162
+ ? createSandboxedTools(workspaceDir, this.logger)
163
+ : undefined;
164
+ try {
165
+ const result = await generateText({
166
+ model: provider.chat(this.model),
167
+ system: params.systemPrompt,
168
+ prompt: params.prompt,
169
+ ...(tools ? { tools } : {}),
170
+ stopWhen: stepCountIs(this.enableTools ? MAX_TOOL_ITERATIONS : 1),
171
+ maxOutputTokens: maxTokens,
172
+ abortSignal: AbortSignal.timeout(timeoutMs),
173
+ });
174
+ const text = result.text.trim();
175
+ const totalMs = Date.now() - runStartMs;
176
+ this.logger?.debug?.(`${TAG} run() completed: ${totalMs}ms, steps=${result.steps.length}, output=${text.length} chars`);
177
+ // Log tool usage if any
178
+ if (result.steps.length > 1) {
179
+ const toolCalls = result.steps.flatMap((s) => s.toolCalls ?? []);
180
+ this.logger?.debug?.(`${TAG} Tool calls: ${toolCalls.map((tc) => tc.toolName).join(", ")}`);
181
+ }
182
+ // Metric
183
+ if (params.instanceId) {
184
+ report("llm_call", {
185
+ taskId: params.taskId,
186
+ provider: "standalone",
187
+ model: this.model,
188
+ inputLength: params.prompt.length,
189
+ outputLength: text.length,
190
+ totalDurationMs: totalMs,
191
+ success: true,
192
+ error: null,
193
+ });
194
+ }
195
+ return text;
196
+ }
197
+ catch (err) {
198
+ const totalMs = Date.now() - runStartMs;
199
+ const errMsg = err instanceof Error ? err.message : String(err);
200
+ this.logger?.error(`${TAG} run() failed after ${totalMs}ms: ${errMsg}`);
201
+ if (params.instanceId) {
202
+ report("llm_call", {
203
+ taskId: params.taskId,
204
+ provider: "standalone",
205
+ model: this.model,
206
+ inputLength: params.prompt.length,
207
+ outputLength: 0,
208
+ totalDurationMs: totalMs,
209
+ success: false,
210
+ error: errMsg,
211
+ });
212
+ }
213
+ throw err;
214
+ }
215
+ }
216
+ }
217
+ /**
218
+ * Factory that creates StandaloneLLMRunner instances.
219
+ *
220
+ * Used by the Gateway and Hermes host adapters.
221
+ */
222
+ export class StandaloneLLMRunnerFactory {
223
+ config;
224
+ logger;
225
+ constructor(opts) {
226
+ this.config = opts.config;
227
+ this.logger = opts.logger;
228
+ }
229
+ createRunner(opts) {
230
+ const enableTools = opts?.enableTools ?? false;
231
+ const modelRef = opts?.modelRef;
232
+ // Parse "provider/model" → just use the model part for OpenAI-compatible API
233
+ let model = this.config.model;
234
+ if (modelRef) {
235
+ const slashIdx = modelRef.indexOf("/");
236
+ model = slashIdx > 0 ? modelRef.slice(slashIdx + 1) : modelRef;
237
+ }
238
+ this.logger?.debug?.(`${TAG} Creating StandaloneLLMRunner: model=${model}, tools=${enableTools}`);
239
+ return new StandaloneLLMRunner({
240
+ config: this.config,
241
+ model,
242
+ enableTools,
243
+ logger: this.logger,
244
+ });
245
+ }
246
+ }