dsh-tdai-memory 0.3.0 → 0.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-tdai-memory",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "DeepSeek Harness 的 TencentDB Agent Memory 移植:L0 对话捕获 → L1 结构化记忆提取 → L2 场景/L3 画像,自动召回注入 + 记忆/对话搜索工具;复用现有 ~/.memory-tencentdb/memory-tdai 数据;附 Web UI 设置栏。",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -63,8 +63,8 @@
63
63
  },
64
64
  "peerDependencies": {
65
65
  "react": "^18.2.0",
66
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.7 || ^0.1.1-rc.1 || ^0.1.2-rc.1",
67
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.7 || ^0.1.1-rc.1 || ^0.1.2-rc.1",
66
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.7 || ^0.1.1-rc.1 || ^0.1.2-rc.1 || ^0.1.5-rc.1",
67
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.7 || ^0.1.1-rc.1 || ^0.1.2-rc.1 || ^0.1.5-rc.1",
68
68
  "node-llama-cpp": "^3.19.1"
69
69
  },
70
70
  "peerDependenciesMeta": {
@@ -1,274 +1,274 @@
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
- import { resolvePersistentSessionId, sessionHeaderOf } from "../../utils/session-header-fetch.js";
25
- const TAG = "[memory-tdai] [standalone-runner]";
26
- // Max iterations in the tool-call loop to prevent infinite loops
27
- const MAX_TOOL_ITERATIONS = 20;
28
- // ============================
29
- // Sandboxed tool execution helpers
30
- // ============================
31
- function resolveSandboxedPath(workspaceDir, relativePath) {
32
- const resolved = path.resolve(workspaceDir, relativePath);
33
- if (!resolved.startsWith(path.resolve(workspaceDir))) {
34
- return null;
35
- }
36
- return resolved;
37
- }
38
- // ============================
39
- // Tool definitions (Vercel AI SDK `tool()` format)
40
- // ============================
41
- function createSandboxedTools(workspaceDir, logger) {
42
- return {
43
- read_file: tool({
44
- description: "Read the contents of a file at the given relative path.",
45
- inputSchema: jsonSchema({
46
- type: "object",
47
- properties: {
48
- path: { type: "string", description: "Relative file path to read." },
49
- },
50
- required: ["path"],
51
- }),
52
- execute: (async (args) => {
53
- const resolved = resolveSandboxedPath(workspaceDir, args.path);
54
- if (!resolved)
55
- return JSON.stringify({ error: `Path "${args.path}" escapes workspace boundary.` });
56
- try {
57
- return await fsPromises.readFile(resolved, "utf-8");
58
- }
59
- catch (err) {
60
- const msg = err instanceof Error ? err.message : String(err);
61
- logger?.warn?.(`${TAG} read_file failed: ${msg}`);
62
- return JSON.stringify({ error: msg });
63
- }
64
- }),
65
- }),
66
- write_to_file: tool({
67
- description: "Write content to a file at the given relative path. Creates or overwrites.",
68
- inputSchema: jsonSchema({
69
- type: "object",
70
- properties: {
71
- path: { type: "string", description: "Relative file path to write." },
72
- content: { type: "string", description: "Content to write." },
73
- },
74
- required: ["path", "content"],
75
- }),
76
- execute: (async (args) => {
77
- const resolved = resolveSandboxedPath(workspaceDir, args.path);
78
- if (!resolved)
79
- return JSON.stringify({ error: `Path "${args.path}" escapes workspace boundary.` });
80
- try {
81
- await fsPromises.mkdir(path.dirname(resolved), { recursive: true });
82
- await fsPromises.writeFile(resolved, args.content, "utf-8");
83
- return JSON.stringify({ success: true });
84
- }
85
- catch (err) {
86
- const msg = err instanceof Error ? err.message : String(err);
87
- logger?.warn?.(`${TAG} write_to_file failed: ${msg}`);
88
- return JSON.stringify({ error: msg });
89
- }
90
- }),
91
- }),
92
- replace_in_file: tool({
93
- description: "Replace an exact substring in a file with new content.",
94
- inputSchema: jsonSchema({
95
- type: "object",
96
- properties: {
97
- path: { type: "string", description: "Relative file path." },
98
- old_str: { type: "string", description: "Exact string to find and replace." },
99
- new_str: { type: "string", description: "Replacement string." },
100
- },
101
- required: ["path", "old_str", "new_str"],
102
- }),
103
- execute: (async (args) => {
104
- const resolved = resolveSandboxedPath(workspaceDir, args.path);
105
- if (!resolved)
106
- return JSON.stringify({ error: `Path "${args.path}" escapes workspace boundary.` });
107
- if (!args.old_str)
108
- return JSON.stringify({ error: "old_str cannot be empty." });
109
- try {
110
- const existing = await fsPromises.readFile(resolved, "utf-8");
111
- if (!existing.includes(args.old_str)) {
112
- return JSON.stringify({ error: `old_str not found in file "${args.path}".` });
113
- }
114
- const updated = existing.replace(args.old_str, args.new_str);
115
- await fsPromises.writeFile(resolved, updated, "utf-8");
116
- return JSON.stringify({ success: true });
117
- }
118
- catch (err) {
119
- const msg = err instanceof Error ? err.message : String(err);
120
- logger?.warn?.(`${TAG} replace_in_file failed: ${msg}`);
121
- return JSON.stringify({ error: msg });
122
- }
123
- }),
124
- }),
125
- };
126
- }
127
- // ============================
128
- // StandaloneLLMRunner
129
- // ============================
130
- export class StandaloneLLMRunner {
131
- config;
132
- model;
133
- enableTools;
134
- logger;
135
- customFetch;
136
- sessionIdPromise;
137
- constructor(opts) {
138
- this.config = opts.config;
139
- this.model = opts.model ?? opts.config.model;
140
- this.enableTools = opts.enableTools ?? false;
141
- this.logger = opts.logger;
142
- this.customFetch = opts.config.disableThinking
143
- ? createNoThinkFetch(opts.config.disableThinking)
144
- : undefined;
145
- // Resolve the persistent session id lazily (async, cached). Only needed
146
- // when the session header is enabled and no fixed sessionId is set.
147
- const { sendSessionHeader, sessionId, dataDir } = opts.config;
148
- if (sendSessionHeader === false || (sessionId && String(sessionId).trim())) {
149
- this.sessionIdPromise = Promise.resolve(String(sessionId ?? "").trim());
150
- }
151
- else {
152
- this.sessionIdPromise = resolvePersistentSessionId(dataDir);
153
- }
154
- }
155
- async buildProvider() {
156
- const providerOptions = {
157
- baseURL: this.config.baseUrl,
158
- apiKey: this.config.apiKey,
159
- compatibility: "compatible",
160
- ...(this.customFetch ? { fetch: this.customFetch } : {}),
161
- };
162
- // x-opencode-session: stable per-conversation id header.
163
- if (this.config.sendSessionHeader !== false) {
164
- const persistentId = await this.sessionIdPromise;
165
- const header = sessionHeaderOf({
166
- send: this.config.sendSessionHeader,
167
- headerName: this.config.sessionHeaderName,
168
- sessionId: this.config.sessionId,
169
- persistentId,
170
- });
171
- if (header) {
172
- providerOptions.headers = { ...(providerOptions.headers ?? {}), ...header };
173
- }
174
- }
175
- return createOpenAI(providerOptions);
176
- }
177
- async run(params) {
178
- const runStartMs = Date.now();
179
- const timeoutMs = params.timeoutMs ?? this.config.timeoutMs ?? 120_000;
180
- const maxTokens = params.maxTokens ?? this.config.maxTokens ?? 4096;
181
- const workspaceDir = params.workspaceDir ?? process.cwd();
182
- this.logger?.debug?.(`${TAG} run() start: taskId=${params.taskId}, model=${this.model}, ` +
183
- `tools=${this.enableTools}, timeout=${timeoutMs}ms`);
184
- // Create OpenAI-compatible provider via AI SDK
185
- // Use "compatible" mode to call /chat/completions (not Responses API),
186
- // which works with all OpenAI-compatible backends (DeepSeek, Qwen, etc.)
187
- const provider = await this.buildProvider();
188
- // For pure text tasks like L1 extraction, avoid exposing any tools.
189
- const tools = this.enableTools
190
- ? createSandboxedTools(workspaceDir, this.logger)
191
- : undefined;
192
- try {
193
- const result = await generateText({
194
- model: provider.chat(this.model),
195
- system: params.systemPrompt,
196
- prompt: params.prompt,
197
- ...(tools ? { tools } : {}),
198
- stopWhen: stepCountIs(this.enableTools ? MAX_TOOL_ITERATIONS : 1),
199
- maxOutputTokens: maxTokens,
200
- abortSignal: AbortSignal.timeout(timeoutMs),
201
- });
202
- const text = result.text.trim();
203
- const totalMs = Date.now() - runStartMs;
204
- this.logger?.debug?.(`${TAG} run() completed: ${totalMs}ms, steps=${result.steps.length}, output=${text.length} chars`);
205
- // Log tool usage if any
206
- if (result.steps.length > 1) {
207
- const toolCalls = result.steps.flatMap((s) => s.toolCalls ?? []);
208
- this.logger?.debug?.(`${TAG} Tool calls: ${toolCalls.map((tc) => tc.toolName).join(", ")}`);
209
- }
210
- // Metric
211
- if (params.instanceId) {
212
- report("llm_call", {
213
- taskId: params.taskId,
214
- provider: "standalone",
215
- model: this.model,
216
- inputLength: params.prompt.length,
217
- outputLength: text.length,
218
- totalDurationMs: totalMs,
219
- success: true,
220
- error: null,
221
- });
222
- }
223
- return text;
224
- }
225
- catch (err) {
226
- const totalMs = Date.now() - runStartMs;
227
- const errMsg = err instanceof Error ? err.message : String(err);
228
- this.logger?.error(`${TAG} run() failed after ${totalMs}ms: ${errMsg}`);
229
- if (params.instanceId) {
230
- report("llm_call", {
231
- taskId: params.taskId,
232
- provider: "standalone",
233
- model: this.model,
234
- inputLength: params.prompt.length,
235
- outputLength: 0,
236
- totalDurationMs: totalMs,
237
- success: false,
238
- error: errMsg,
239
- });
240
- }
241
- throw err;
242
- }
243
- }
244
- }
245
- /**
246
- * Factory that creates StandaloneLLMRunner instances.
247
- *
248
- * Used by the Gateway and Hermes host adapters.
249
- */
250
- export class StandaloneLLMRunnerFactory {
251
- config;
252
- logger;
253
- constructor(opts) {
254
- this.config = opts.config;
255
- this.logger = opts.logger;
256
- }
257
- createRunner(opts) {
258
- const enableTools = opts?.enableTools ?? false;
259
- const modelRef = opts?.modelRef;
260
- // Parse "provider/model" → just use the model part for OpenAI-compatible API
261
- let model = this.config.model;
262
- if (modelRef) {
263
- const slashIdx = modelRef.indexOf("/");
264
- model = slashIdx > 0 ? modelRef.slice(slashIdx + 1) : modelRef;
265
- }
266
- this.logger?.debug?.(`${TAG} Creating StandaloneLLMRunner: model=${model}, tools=${enableTools}`);
267
- return new StandaloneLLMRunner({
268
- config: this.config,
269
- model,
270
- enableTools,
271
- logger: this.logger,
272
- });
273
- }
274
- }
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
+ import { resolvePersistentSessionId, sessionHeaderOf } from "../../utils/session-header-fetch.js";
25
+ const TAG = "[memory-tdai] [standalone-runner]";
26
+ // Max iterations in the tool-call loop to prevent infinite loops
27
+ const MAX_TOOL_ITERATIONS = 20;
28
+ // ============================
29
+ // Sandboxed tool execution helpers
30
+ // ============================
31
+ function resolveSandboxedPath(workspaceDir, relativePath) {
32
+ const resolved = path.resolve(workspaceDir, relativePath);
33
+ if (!resolved.startsWith(path.resolve(workspaceDir))) {
34
+ return null;
35
+ }
36
+ return resolved;
37
+ }
38
+ // ============================
39
+ // Tool definitions (Vercel AI SDK `tool()` format)
40
+ // ============================
41
+ function createSandboxedTools(workspaceDir, logger) {
42
+ return {
43
+ read_file: tool({
44
+ description: "Read the contents of a file at the given relative path.",
45
+ inputSchema: jsonSchema({
46
+ type: "object",
47
+ properties: {
48
+ path: { type: "string", description: "Relative file path to read." },
49
+ },
50
+ required: ["path"],
51
+ }),
52
+ execute: (async (args) => {
53
+ const resolved = resolveSandboxedPath(workspaceDir, args.path);
54
+ if (!resolved)
55
+ return JSON.stringify({ error: `Path "${args.path}" escapes workspace boundary.` });
56
+ try {
57
+ return await fsPromises.readFile(resolved, "utf-8");
58
+ }
59
+ catch (err) {
60
+ const msg = err instanceof Error ? err.message : String(err);
61
+ logger?.warn?.(`${TAG} read_file failed: ${msg}`);
62
+ return JSON.stringify({ error: msg });
63
+ }
64
+ }),
65
+ }),
66
+ write_to_file: tool({
67
+ description: "Write content to a file at the given relative path. Creates or overwrites.",
68
+ inputSchema: jsonSchema({
69
+ type: "object",
70
+ properties: {
71
+ path: { type: "string", description: "Relative file path to write." },
72
+ content: { type: "string", description: "Content to write." },
73
+ },
74
+ required: ["path", "content"],
75
+ }),
76
+ execute: (async (args) => {
77
+ const resolved = resolveSandboxedPath(workspaceDir, args.path);
78
+ if (!resolved)
79
+ return JSON.stringify({ error: `Path "${args.path}" escapes workspace boundary.` });
80
+ try {
81
+ await fsPromises.mkdir(path.dirname(resolved), { recursive: true });
82
+ await fsPromises.writeFile(resolved, args.content, "utf-8");
83
+ return JSON.stringify({ success: true });
84
+ }
85
+ catch (err) {
86
+ const msg = err instanceof Error ? err.message : String(err);
87
+ logger?.warn?.(`${TAG} write_to_file failed: ${msg}`);
88
+ return JSON.stringify({ error: msg });
89
+ }
90
+ }),
91
+ }),
92
+ replace_in_file: tool({
93
+ description: "Replace an exact substring in a file with new content.",
94
+ inputSchema: jsonSchema({
95
+ type: "object",
96
+ properties: {
97
+ path: { type: "string", description: "Relative file path." },
98
+ old_str: { type: "string", description: "Exact string to find and replace." },
99
+ new_str: { type: "string", description: "Replacement string." },
100
+ },
101
+ required: ["path", "old_str", "new_str"],
102
+ }),
103
+ execute: (async (args) => {
104
+ const resolved = resolveSandboxedPath(workspaceDir, args.path);
105
+ if (!resolved)
106
+ return JSON.stringify({ error: `Path "${args.path}" escapes workspace boundary.` });
107
+ if (!args.old_str)
108
+ return JSON.stringify({ error: "old_str cannot be empty." });
109
+ try {
110
+ const existing = await fsPromises.readFile(resolved, "utf-8");
111
+ if (!existing.includes(args.old_str)) {
112
+ return JSON.stringify({ error: `old_str not found in file "${args.path}".` });
113
+ }
114
+ const updated = existing.replace(args.old_str, args.new_str);
115
+ await fsPromises.writeFile(resolved, updated, "utf-8");
116
+ return JSON.stringify({ success: true });
117
+ }
118
+ catch (err) {
119
+ const msg = err instanceof Error ? err.message : String(err);
120
+ logger?.warn?.(`${TAG} replace_in_file failed: ${msg}`);
121
+ return JSON.stringify({ error: msg });
122
+ }
123
+ }),
124
+ }),
125
+ };
126
+ }
127
+ // ============================
128
+ // StandaloneLLMRunner
129
+ // ============================
130
+ export class StandaloneLLMRunner {
131
+ config;
132
+ model;
133
+ enableTools;
134
+ logger;
135
+ customFetch;
136
+ sessionIdPromise;
137
+ constructor(opts) {
138
+ this.config = opts.config;
139
+ this.model = opts.model ?? opts.config.model;
140
+ this.enableTools = opts.enableTools ?? false;
141
+ this.logger = opts.logger;
142
+ this.customFetch = opts.config.disableThinking
143
+ ? createNoThinkFetch(opts.config.disableThinking)
144
+ : undefined;
145
+ // Resolve the persistent session id lazily (async, cached). Only needed
146
+ // when the session header is enabled and no fixed sessionId is set.
147
+ const { sendSessionHeader, sessionId, dataDir } = opts.config;
148
+ if (sendSessionHeader === false || (sessionId && String(sessionId).trim())) {
149
+ this.sessionIdPromise = Promise.resolve(String(sessionId ?? "").trim());
150
+ }
151
+ else {
152
+ this.sessionIdPromise = resolvePersistentSessionId(dataDir);
153
+ }
154
+ }
155
+ async buildProvider() {
156
+ const providerOptions = {
157
+ baseURL: this.config.baseUrl,
158
+ apiKey: this.config.apiKey,
159
+ compatibility: "compatible",
160
+ ...(this.customFetch ? { fetch: this.customFetch } : {}),
161
+ };
162
+ // x-opencode-session: stable per-conversation id header.
163
+ if (this.config.sendSessionHeader !== false) {
164
+ const persistentId = await this.sessionIdPromise;
165
+ const header = sessionHeaderOf({
166
+ send: this.config.sendSessionHeader,
167
+ headerName: this.config.sessionHeaderName,
168
+ sessionId: this.config.sessionId,
169
+ persistentId,
170
+ });
171
+ if (header) {
172
+ providerOptions.headers = { ...(providerOptions.headers ?? {}), ...header };
173
+ }
174
+ }
175
+ return createOpenAI(providerOptions);
176
+ }
177
+ async run(params) {
178
+ const runStartMs = Date.now();
179
+ const timeoutMs = params.timeoutMs ?? this.config.timeoutMs ?? 120_000;
180
+ const maxTokens = params.maxTokens ?? this.config.maxTokens ?? 4096;
181
+ const workspaceDir = params.workspaceDir ?? process.cwd();
182
+ this.logger?.debug?.(`${TAG} run() start: taskId=${params.taskId}, model=${this.model}, ` +
183
+ `tools=${this.enableTools}, timeout=${timeoutMs}ms`);
184
+ // Create OpenAI-compatible provider via AI SDK
185
+ // Use "compatible" mode to call /chat/completions (not Responses API),
186
+ // which works with all OpenAI-compatible backends (DeepSeek, Qwen, etc.)
187
+ const provider = await this.buildProvider();
188
+ // For pure text tasks like L1 extraction, avoid exposing any tools.
189
+ const tools = this.enableTools
190
+ ? createSandboxedTools(workspaceDir, this.logger)
191
+ : undefined;
192
+ try {
193
+ const result = await generateText({
194
+ model: provider.chat(this.model),
195
+ system: params.systemPrompt,
196
+ prompt: params.prompt,
197
+ ...(tools ? { tools } : {}),
198
+ stopWhen: stepCountIs(this.enableTools ? MAX_TOOL_ITERATIONS : 1),
199
+ maxOutputTokens: maxTokens,
200
+ abortSignal: AbortSignal.timeout(timeoutMs),
201
+ });
202
+ const text = result.text.trim();
203
+ const totalMs = Date.now() - runStartMs;
204
+ this.logger?.debug?.(`${TAG} run() completed: ${totalMs}ms, steps=${result.steps.length}, output=${text.length} chars`);
205
+ // Log tool usage if any
206
+ if (result.steps.length > 1) {
207
+ const toolCalls = result.steps.flatMap((s) => s.toolCalls ?? []);
208
+ this.logger?.debug?.(`${TAG} Tool calls: ${toolCalls.map((tc) => tc.toolName).join(", ")}`);
209
+ }
210
+ // Metric
211
+ if (params.instanceId) {
212
+ report("llm_call", {
213
+ taskId: params.taskId,
214
+ provider: "standalone",
215
+ model: this.model,
216
+ inputLength: params.prompt.length,
217
+ outputLength: text.length,
218
+ totalDurationMs: totalMs,
219
+ success: true,
220
+ error: null,
221
+ });
222
+ }
223
+ return text;
224
+ }
225
+ catch (err) {
226
+ const totalMs = Date.now() - runStartMs;
227
+ const errMsg = err instanceof Error ? err.message : String(err);
228
+ this.logger?.error(`${TAG} run() failed after ${totalMs}ms: ${errMsg}`);
229
+ if (params.instanceId) {
230
+ report("llm_call", {
231
+ taskId: params.taskId,
232
+ provider: "standalone",
233
+ model: this.model,
234
+ inputLength: params.prompt.length,
235
+ outputLength: 0,
236
+ totalDurationMs: totalMs,
237
+ success: false,
238
+ error: errMsg,
239
+ });
240
+ }
241
+ throw err;
242
+ }
243
+ }
244
+ }
245
+ /**
246
+ * Factory that creates StandaloneLLMRunner instances.
247
+ *
248
+ * Used by the Gateway and Hermes host adapters.
249
+ */
250
+ export class StandaloneLLMRunnerFactory {
251
+ config;
252
+ logger;
253
+ constructor(opts) {
254
+ this.config = opts.config;
255
+ this.logger = opts.logger;
256
+ }
257
+ createRunner(opts) {
258
+ const enableTools = opts?.enableTools ?? false;
259
+ const modelRef = opts?.modelRef;
260
+ // Parse "provider/model" → just use the model part for OpenAI-compatible API
261
+ let model = this.config.model;
262
+ if (modelRef) {
263
+ const slashIdx = modelRef.indexOf("/");
264
+ model = slashIdx > 0 ? modelRef.slice(slashIdx + 1) : modelRef;
265
+ }
266
+ this.logger?.debug?.(`${TAG} Creating StandaloneLLMRunner: model=${model}, tools=${enableTools}`);
267
+ return new StandaloneLLMRunner({
268
+ config: this.config,
269
+ model,
270
+ enableTools,
271
+ logger: this.logger,
272
+ });
273
+ }
274
+ }