dsh-tdai-memory 0.2.9 → 0.2.11

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 (72) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +130 -130
  3. package/README.zh.md +72 -72
  4. package/THIRD-PARTY-NOTICES.md +20 -20
  5. package/client.js +349 -311
  6. package/cordis.patch.yml +8 -8
  7. package/index.js +421 -421
  8. package/package.json +8 -5
  9. package/recall-inject.js +86 -86
  10. package/vendor/dsh-settings-expose.js +75 -75
  11. package/vendor/tdai/adapters/index.js +15 -15
  12. package/vendor/tdai/adapters/openclaw/host-adapter.js +83 -83
  13. package/vendor/tdai/adapters/openclaw/index.js +5 -5
  14. package/vendor/tdai/adapters/openclaw/llm-runner.js +69 -69
  15. package/vendor/tdai/adapters/standalone/host-adapter.js +58 -58
  16. package/vendor/tdai/adapters/standalone/index.js +5 -5
  17. package/vendor/tdai/adapters/standalone/llm-runner.js +246 -246
  18. package/vendor/tdai/config.js +363 -363
  19. package/vendor/tdai/core/conversation/l0-recorder.js +420 -420
  20. package/vendor/tdai/core/hooks/auto-capture.js +236 -236
  21. package/vendor/tdai/core/hooks/auto-recall.js +640 -640
  22. package/vendor/tdai/core/index.js +8 -8
  23. package/vendor/tdai/core/persona/persona-generator.js +184 -184
  24. package/vendor/tdai/core/persona/persona-trigger.js +94 -94
  25. package/vendor/tdai/core/profile/profile-sync.js +200 -200
  26. package/vendor/tdai/core/prompts/l1-dedup.js +72 -72
  27. package/vendor/tdai/core/prompts/l1-extraction.js +34 -34
  28. package/vendor/tdai/core/prompts/persona-generation.js +35 -35
  29. package/vendor/tdai/core/prompts/scene-extraction.js +45 -45
  30. package/vendor/tdai/core/record/l1-dedup.js +318 -318
  31. package/vendor/tdai/core/record/l1-extractor.js +379 -379
  32. package/vendor/tdai/core/record/l1-reader.js +178 -178
  33. package/vendor/tdai/core/record/l1-writer.js +141 -141
  34. package/vendor/tdai/core/report/reporter.js +86 -86
  35. package/vendor/tdai/core/scene/filename-normalizer.js +161 -161
  36. package/vendor/tdai/core/scene/scene-extractor.js +410 -410
  37. package/vendor/tdai/core/scene/scene-format.js +53 -53
  38. package/vendor/tdai/core/scene/scene-index.js +82 -82
  39. package/vendor/tdai/core/scene/scene-navigation.js +57 -57
  40. package/vendor/tdai/core/seed/input.js +404 -404
  41. package/vendor/tdai/core/seed/seed-runtime.js +314 -314
  42. package/vendor/tdai/core/seed/types.js +9 -9
  43. package/vendor/tdai/core/store/bm25-client.js +117 -117
  44. package/vendor/tdai/core/store/bm25-local.js +68 -68
  45. package/vendor/tdai/core/store/embedding.js +632 -632
  46. package/vendor/tdai/core/store/factory.js +95 -95
  47. package/vendor/tdai/core/store/search-utils.js +54 -54
  48. package/vendor/tdai/core/store/sqlite.js +1632 -1632
  49. package/vendor/tdai/core/store/tcvdb-client.js +221 -221
  50. package/vendor/tdai/core/store/tcvdb.js +1047 -1047
  51. package/vendor/tdai/core/store/types.js +17 -17
  52. package/vendor/tdai/core/tdai-core.js +446 -446
  53. package/vendor/tdai/core/tools/conversation-search.js +199 -199
  54. package/vendor/tdai/core/tools/memory-search.js +207 -207
  55. package/vendor/tdai/core/types.js +12 -12
  56. package/vendor/tdai/utils/backup.js +189 -189
  57. package/vendor/tdai/utils/checkpoint.js +360 -360
  58. package/vendor/tdai/utils/clean-context-runner.js +472 -472
  59. package/vendor/tdai/utils/ensure-hook-policy.js +213 -213
  60. package/vendor/tdai/utils/env.js +13 -13
  61. package/vendor/tdai/utils/managed-timer.js +127 -127
  62. package/vendor/tdai/utils/manifest.js +91 -91
  63. package/vendor/tdai/utils/memory-cleaner.js +300 -300
  64. package/vendor/tdai/utils/no-think-fetch.js +109 -109
  65. package/vendor/tdai/utils/openclaw-state-dir.js +26 -26
  66. package/vendor/tdai/utils/pipeline-factory.js +517 -517
  67. package/vendor/tdai/utils/pipeline-manager.js +892 -892
  68. package/vendor/tdai/utils/sanitize.js +360 -360
  69. package/vendor/tdai/utils/serial-queue.js +103 -103
  70. package/vendor/tdai/utils/session-filter.js +88 -88
  71. package/vendor/tdai/utils/text-utils.js +29 -29
  72. 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
+ }