mirror of
https://github.com/TencentCloud/TencentDB-Agent-Memory
synced 2026-07-11 04:44:29 +00:00
feat: release v0.3.3 — Hermes adapter, context offload, core refactor
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* TDAI Adapters — barrel re-export for all host adapter implementations.
|
||||
*
|
||||
* Each adapter translates a specific host environment's API into
|
||||
* the host-neutral HostAdapter interface consumed by TdaiCore.
|
||||
*
|
||||
* Directory structure:
|
||||
* adapters/
|
||||
* ├── openclaw/ — OpenClaw plugin host (in-process, runEmbeddedPiAgent)
|
||||
* └── standalone/ — Gateway / Hermes sidecar (HTTP, OpenAI-compatible API)
|
||||
*/
|
||||
|
||||
// OpenClaw adapter
|
||||
export { OpenClawHostAdapter, OpenClawLLMRunner, OpenClawLLMRunnerFactory } from "./openclaw/index.js";
|
||||
export type { OpenClawHostAdapterOptions, OpenClawLLMRunnerFactoryOptions } from "./openclaw/index.js";
|
||||
|
||||
// Standalone adapter
|
||||
export { StandaloneHostAdapter, StandaloneLLMRunner, StandaloneLLMRunnerFactory } from "./standalone/index.js";
|
||||
export type { StandaloneHostAdapterOptions, StandaloneLLMConfig, StandaloneLLMRunnerFactoryOptions } from "./standalone/index.js";
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* OpenClawHostAdapter — translates OpenClaw's plugin API into TDAI Core's
|
||||
* unified HostAdapter interface.
|
||||
*
|
||||
* This is the "thin shell" that keeps OpenClaw-specific dependencies
|
||||
* (OpenClawPluginApi, pluginConfig, resolveStateDir, event system)
|
||||
* confined to the adapter layer while TDAI Core remains host-neutral.
|
||||
*
|
||||
* Usage (in index.ts):
|
||||
* const adapter = new OpenClawHostAdapter({ api, pluginDataDir, config });
|
||||
* const core = new TdaiCore({ hostAdapter: adapter, config: parsedConfig });
|
||||
*/
|
||||
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import { OpenClawLLMRunnerFactory } from "./llm-runner.js";
|
||||
import type {
|
||||
HostAdapter,
|
||||
RuntimeContext,
|
||||
Logger,
|
||||
LLMRunnerFactory,
|
||||
} from "../../core/types.js";
|
||||
|
||||
// ============================
|
||||
// Options
|
||||
// ============================
|
||||
|
||||
export interface OpenClawHostAdapterOptions {
|
||||
/** OpenClaw plugin API instance. */
|
||||
api: OpenClawPluginApi;
|
||||
/** Resolved plugin data directory (e.g. ~/.openclaw/state/memory-tdai). */
|
||||
pluginDataDir: string;
|
||||
/** Parsed OpenClaw config (for LLM model resolution). */
|
||||
openclawConfig: unknown;
|
||||
}
|
||||
|
||||
// ============================
|
||||
// OpenClawHostAdapter
|
||||
// ============================
|
||||
|
||||
export class OpenClawHostAdapter implements HostAdapter {
|
||||
readonly hostType = "openclaw" as const;
|
||||
|
||||
private api: OpenClawPluginApi;
|
||||
private pluginDataDir: string;
|
||||
private openclawConfig: unknown;
|
||||
private runnerFactory: OpenClawLLMRunnerFactory;
|
||||
|
||||
constructor(opts: OpenClawHostAdapterOptions) {
|
||||
this.api = opts.api;
|
||||
this.pluginDataDir = opts.pluginDataDir;
|
||||
this.openclawConfig = opts.openclawConfig;
|
||||
|
||||
this.runnerFactory = new OpenClawLLMRunnerFactory({
|
||||
config: opts.openclawConfig,
|
||||
agentRuntime: opts.api.runtime.agent,
|
||||
logger: opts.api.logger,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a RuntimeContext from the current OpenClaw session.
|
||||
*
|
||||
* In OpenClaw, sessionKey and sessionId come from the event/ctx objects
|
||||
* passed to hooks. This method returns a context with sensible defaults;
|
||||
* callers can override sessionKey/sessionId per-hook invocation using
|
||||
* `buildRuntimeContextForSession()`.
|
||||
*/
|
||||
getRuntimeContext(): RuntimeContext {
|
||||
return {
|
||||
userId: "default_user",
|
||||
sessionId: "",
|
||||
sessionKey: "",
|
||||
platform: "openclaw",
|
||||
workspaceDir: process.cwd(),
|
||||
dataDir: this.pluginDataDir,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a RuntimeContext for a specific session (used per-hook).
|
||||
*
|
||||
* This is an OpenClaw-specific convenience that merges session-level
|
||||
* identifiers from hook ctx into the base context.
|
||||
*/
|
||||
buildRuntimeContextForSession(sessionKey: string, sessionId?: string): RuntimeContext {
|
||||
return {
|
||||
...this.getRuntimeContext(),
|
||||
sessionKey,
|
||||
sessionId: sessionId ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
getLogger(): Logger {
|
||||
return this.api.logger;
|
||||
}
|
||||
|
||||
getLLMRunnerFactory(): LLMRunnerFactory {
|
||||
return this.runnerFactory;
|
||||
}
|
||||
|
||||
// -- OpenClaw-specific accessors (for index.ts bridge) --------------------
|
||||
|
||||
/** Get the raw OpenClaw plugin API (for legacy callers during migration). */
|
||||
getPluginApi(): OpenClawPluginApi {
|
||||
return this.api;
|
||||
}
|
||||
|
||||
/** Get the OpenClaw config object (for legacy callers during migration). */
|
||||
getOpenClawConfig(): unknown {
|
||||
return this.openclawConfig;
|
||||
}
|
||||
|
||||
/** Get the resolved plugin data directory. */
|
||||
getPluginDataDir(): string {
|
||||
return this.pluginDataDir;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* OpenClaw adapter — barrel exports.
|
||||
*/
|
||||
export { OpenClawHostAdapter } from "./host-adapter.js";
|
||||
export type { OpenClawHostAdapterOptions } from "./host-adapter.js";
|
||||
export { OpenClawLLMRunner, OpenClawLLMRunnerFactory } from "./llm-runner.js";
|
||||
export type { OpenClawLLMRunnerFactoryOptions } from "./llm-runner.js";
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* OpenClawLLMRunner — wraps the existing CleanContextRunner as a host-neutral LLMRunner.
|
||||
*
|
||||
* This is a compatibility bridge: TDAI Core modules (L1 extractor, L2 scene extractor,
|
||||
* L3 persona generator, L1 dedup) can depend on the `LLMRunner` interface, while
|
||||
* OpenClaw continues to use its native `runEmbeddedPiAgent` mechanism under the hood.
|
||||
*
|
||||
* Usage:
|
||||
* const factory = new OpenClawLLMRunnerFactory({ config, agentRuntime, logger });
|
||||
* const runner = factory.createRunner({ modelRef: "openai/gpt-4o", enableTools: true });
|
||||
* const result = await runner.run({ prompt: "...", taskId: "l1-extraction" });
|
||||
*/
|
||||
|
||||
import { CleanContextRunner } from "../../utils/clean-context-runner.js";
|
||||
import type { EmbeddedAgentRuntimeLike } from "../../utils/clean-context-runner.js";
|
||||
import type {
|
||||
LLMRunner,
|
||||
LLMRunParams,
|
||||
LLMRunnerFactory,
|
||||
LLMRunnerCreateOptions,
|
||||
Logger,
|
||||
} from "../../core/types.js";
|
||||
|
||||
const TAG = "[memory-tdai] [openclaw-runner]";
|
||||
|
||||
// ============================
|
||||
// OpenClawLLMRunner
|
||||
// ============================
|
||||
|
||||
/**
|
||||
* LLMRunner implementation backed by CleanContextRunner.
|
||||
*
|
||||
* Each instance is configured with a fixed model + tools setting.
|
||||
* Create via `OpenClawLLMRunnerFactory.createRunner()`.
|
||||
*/
|
||||
export class OpenClawLLMRunner implements LLMRunner {
|
||||
private runner: CleanContextRunner;
|
||||
|
||||
constructor(runner: CleanContextRunner) {
|
||||
this.runner = runner;
|
||||
}
|
||||
|
||||
async run(params: LLMRunParams): Promise<string> {
|
||||
return this.runner.run({
|
||||
prompt: params.prompt,
|
||||
systemPrompt: params.systemPrompt,
|
||||
taskId: params.taskId,
|
||||
timeoutMs: params.timeoutMs,
|
||||
maxTokens: params.maxTokens,
|
||||
workspaceDir: params.workspaceDir,
|
||||
instanceId: params.instanceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================
|
||||
// OpenClawLLMRunnerFactory
|
||||
// ============================
|
||||
|
||||
export interface OpenClawLLMRunnerFactoryOptions {
|
||||
/** OpenClaw config object (passed to CleanContextRunner). */
|
||||
config: unknown;
|
||||
/** Preferred embedded agent runtime (host-injected). */
|
||||
agentRuntime?: EmbeddedAgentRuntimeLike;
|
||||
/** Logger for runner tracing. */
|
||||
logger?: Logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory that creates OpenClawLLMRunner instances.
|
||||
*
|
||||
* Encapsulates the OpenClaw-specific dependencies (config, agentRuntime)
|
||||
* so that callers only need to specify model + tools.
|
||||
*/
|
||||
export class OpenClawLLMRunnerFactory implements LLMRunnerFactory {
|
||||
private config: unknown;
|
||||
private agentRuntime?: EmbeddedAgentRuntimeLike;
|
||||
private logger?: Logger;
|
||||
|
||||
constructor(opts: OpenClawLLMRunnerFactoryOptions) {
|
||||
this.config = opts.config;
|
||||
this.agentRuntime = opts.agentRuntime;
|
||||
this.logger = opts.logger;
|
||||
}
|
||||
|
||||
createRunner(opts?: LLMRunnerCreateOptions): LLMRunner {
|
||||
const enableTools = opts?.enableTools ?? false;
|
||||
const modelRef = opts?.modelRef;
|
||||
|
||||
this.logger?.debug?.(
|
||||
`${TAG} Creating OpenClawLLMRunner: model=${modelRef ?? "(default)"}, tools=${enableTools}`,
|
||||
);
|
||||
|
||||
const cleanRunner = new CleanContextRunner({
|
||||
config: this.config,
|
||||
modelRef,
|
||||
enableTools,
|
||||
agentRuntime: this.agentRuntime,
|
||||
logger: this.logger,
|
||||
});
|
||||
|
||||
return new OpenClawLLMRunner(cleanRunner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* StandaloneHostAdapter — HostAdapter for the TDAI Gateway (Hermes sidecar).
|
||||
*
|
||||
* Does NOT depend on OpenClaw. Context is constructed from Gateway config
|
||||
* and per-request parameters (session_id, user_id, etc.).
|
||||
*/
|
||||
|
||||
import { StandaloneLLMRunnerFactory } from "./llm-runner.js";
|
||||
import type { StandaloneLLMConfig } from "./llm-runner.js";
|
||||
import type {
|
||||
HostAdapter,
|
||||
RuntimeContext,
|
||||
Logger,
|
||||
LLMRunnerFactory,
|
||||
} from "../../core/types.js";
|
||||
|
||||
// ============================
|
||||
// Options
|
||||
// ============================
|
||||
|
||||
export interface StandaloneHostAdapterOptions {
|
||||
/** Base data directory for TDAI storage. */
|
||||
dataDir: string;
|
||||
/** LLM configuration for model calls. */
|
||||
llmConfig: StandaloneLLMConfig;
|
||||
/** Logger instance. */
|
||||
logger: Logger;
|
||||
/** Default user ID (can be overridden per-request). */
|
||||
defaultUserId?: string;
|
||||
/** Platform identifier. */
|
||||
platform?: string;
|
||||
}
|
||||
|
||||
// ============================
|
||||
// StandaloneHostAdapter
|
||||
// ============================
|
||||
|
||||
export class StandaloneHostAdapter implements HostAdapter {
|
||||
readonly hostType = "standalone" as const;
|
||||
|
||||
private dataDir: string;
|
||||
private logger: Logger;
|
||||
private runnerFactory: StandaloneLLMRunnerFactory;
|
||||
private defaultUserId: string;
|
||||
private platform: string;
|
||||
|
||||
constructor(opts: StandaloneHostAdapterOptions) {
|
||||
this.dataDir = opts.dataDir;
|
||||
this.logger = opts.logger;
|
||||
this.defaultUserId = opts.defaultUserId ?? "default_user";
|
||||
this.platform = opts.platform ?? "gateway";
|
||||
|
||||
this.runnerFactory = new StandaloneLLMRunnerFactory({
|
||||
config: opts.llmConfig,
|
||||
logger: opts.logger,
|
||||
});
|
||||
}
|
||||
|
||||
getRuntimeContext(): RuntimeContext {
|
||||
return {
|
||||
userId: this.defaultUserId,
|
||||
sessionId: "",
|
||||
sessionKey: "",
|
||||
platform: this.platform,
|
||||
workspaceDir: this.dataDir,
|
||||
dataDir: this.dataDir,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a RuntimeContext for a specific request.
|
||||
* Used by Gateway route handlers to scope each request to the correct user/session.
|
||||
*/
|
||||
buildRuntimeContextForRequest(params: {
|
||||
userId?: string;
|
||||
sessionId?: string;
|
||||
sessionKey?: string;
|
||||
platform?: string;
|
||||
}): RuntimeContext {
|
||||
return {
|
||||
userId: params.userId ?? this.defaultUserId,
|
||||
sessionId: params.sessionId ?? "",
|
||||
sessionKey: params.sessionKey ?? params.sessionId ?? "",
|
||||
platform: params.platform ?? this.platform,
|
||||
workspaceDir: this.dataDir,
|
||||
dataDir: this.dataDir,
|
||||
};
|
||||
}
|
||||
|
||||
getLogger(): Logger {
|
||||
return this.logger;
|
||||
}
|
||||
|
||||
getLLMRunnerFactory(): LLMRunnerFactory {
|
||||
return this.runnerFactory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Standalone adapter — barrel exports.
|
||||
*/
|
||||
export { StandaloneHostAdapter } from "./host-adapter.js";
|
||||
export type { StandaloneHostAdapterOptions } from "./host-adapter.js";
|
||||
export { StandaloneLLMRunner, StandaloneLLMRunnerFactory } from "./llm-runner.js";
|
||||
export type { StandaloneLLMConfig, StandaloneLLMRunnerFactoryOptions } from "./llm-runner.js";
|
||||
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* StandaloneLLMRunner — powered by Vercel AI SDK (`ai` + `@ai-sdk/openai`).
|
||||
*
|
||||
* This runner does NOT depend on OpenClaw's `runEmbeddedPiAgent`. It is designed
|
||||
* for the Hermes Gateway scenario where TDAI runs as an independent Node.js sidecar
|
||||
* without the OpenClaw host.
|
||||
*
|
||||
* Capabilities:
|
||||
* - `enableTools: false`: pure text output (L1 extraction, L1 dedup)
|
||||
* - `enableTools: true`: automatic tool-call loop with local file operations
|
||||
* (L2 scene, L3 persona) via AI SDK's `maxSteps`
|
||||
*
|
||||
* Tool sandbox:
|
||||
* When tools are enabled, three basic file operations are exposed:
|
||||
* `read_file`, `write_to_file`, `replace_in_file`.
|
||||
* All file paths are resolved relative to `workspaceDir`, enforcing sandbox boundaries.
|
||||
*/
|
||||
|
||||
import fsPromises from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { generateText, tool, stepCountIs, jsonSchema } from "ai";
|
||||
import { createOpenAI } from "@ai-sdk/openai";
|
||||
import { report } from "../../core/report/reporter.js";
|
||||
import type {
|
||||
LLMRunner,
|
||||
LLMRunParams,
|
||||
LLMRunnerFactory,
|
||||
LLMRunnerCreateOptions,
|
||||
Logger,
|
||||
} from "../../core/types.js";
|
||||
|
||||
const TAG = "[memory-tdai] [standalone-runner]";
|
||||
|
||||
// Max iterations in the tool-call loop to prevent infinite loops
|
||||
const MAX_TOOL_ITERATIONS = 20;
|
||||
|
||||
// ============================
|
||||
// Configuration
|
||||
// ============================
|
||||
|
||||
export interface StandaloneLLMConfig {
|
||||
/** OpenAI-compatible API base URL (e.g. "https://api.openai.com/v1"). */
|
||||
baseUrl: string;
|
||||
/** API key for authentication. */
|
||||
apiKey: string;
|
||||
/** Default model name (e.g. "gpt-4o"). */
|
||||
model: string;
|
||||
/** Default max output tokens. */
|
||||
maxTokens?: number;
|
||||
/** Request timeout in milliseconds (default: 120_000). */
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
// ============================
|
||||
// Sandboxed tool execution helpers
|
||||
// ============================
|
||||
|
||||
function resolveSandboxedPath(workspaceDir: string, relativePath: string): string | null {
|
||||
const resolved = path.resolve(workspaceDir, relativePath);
|
||||
if (!resolved.startsWith(path.resolve(workspaceDir))) {
|
||||
return null;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// ============================
|
||||
// Tool definitions (Vercel AI SDK `tool()` format)
|
||||
// ============================
|
||||
|
||||
function createSandboxedTools(workspaceDir: string, logger?: Logger) {
|
||||
return {
|
||||
read_file: tool({
|
||||
description: "Read the contents of a file at the given relative path.",
|
||||
inputSchema: jsonSchema<{ path: string }>({
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string", description: "Relative file path to read." },
|
||||
},
|
||||
required: ["path"],
|
||||
}),
|
||||
execute: (async (args: { path: string }) => {
|
||||
const resolved = resolveSandboxedPath(workspaceDir, args.path);
|
||||
if (!resolved) return JSON.stringify({ error: `Path "${args.path}" escapes workspace boundary.` });
|
||||
try {
|
||||
return await fsPromises.readFile(resolved, "utf-8");
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logger?.warn?.(`${TAG} read_file failed: ${msg}`);
|
||||
return JSON.stringify({ error: msg });
|
||||
}
|
||||
}) as any,
|
||||
}),
|
||||
|
||||
write_to_file: tool({
|
||||
description: "Write content to a file at the given relative path. Creates or overwrites.",
|
||||
inputSchema: jsonSchema<{ path: string; content: string }>({
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string", description: "Relative file path to write." },
|
||||
content: { type: "string", description: "Content to write." },
|
||||
},
|
||||
required: ["path", "content"],
|
||||
}),
|
||||
execute: (async (args: { path: string; content: string }) => {
|
||||
const resolved = resolveSandboxedPath(workspaceDir, args.path);
|
||||
if (!resolved) return JSON.stringify({ error: `Path "${args.path}" escapes workspace boundary.` });
|
||||
try {
|
||||
await fsPromises.mkdir(path.dirname(resolved), { recursive: true });
|
||||
await fsPromises.writeFile(resolved, args.content, "utf-8");
|
||||
return JSON.stringify({ success: true });
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logger?.warn?.(`${TAG} write_to_file failed: ${msg}`);
|
||||
return JSON.stringify({ error: msg });
|
||||
}
|
||||
}) as any,
|
||||
}),
|
||||
|
||||
replace_in_file: tool({
|
||||
description: "Replace an exact substring in a file with new content.",
|
||||
inputSchema: jsonSchema<{ path: string; old_str: string; new_str: string }>({
|
||||
type: "object",
|
||||
properties: {
|
||||
path: { type: "string", description: "Relative file path." },
|
||||
old_str: { type: "string", description: "Exact string to find and replace." },
|
||||
new_str: { type: "string", description: "Replacement string." },
|
||||
},
|
||||
required: ["path", "old_str", "new_str"],
|
||||
}),
|
||||
execute: (async (args: { path: string; old_str: string; new_str: string }) => {
|
||||
const resolved = resolveSandboxedPath(workspaceDir, args.path);
|
||||
if (!resolved) return JSON.stringify({ error: `Path "${args.path}" escapes workspace boundary.` });
|
||||
if (!args.old_str) return JSON.stringify({ error: "old_str cannot be empty." });
|
||||
try {
|
||||
const existing = await fsPromises.readFile(resolved, "utf-8");
|
||||
if (!existing.includes(args.old_str)) {
|
||||
return JSON.stringify({ error: `old_str not found in file "${args.path}".` });
|
||||
}
|
||||
const updated = existing.replace(args.old_str, args.new_str);
|
||||
await fsPromises.writeFile(resolved, updated, "utf-8");
|
||||
return JSON.stringify({ success: true });
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logger?.warn?.(`${TAG} replace_in_file failed: ${msg}`);
|
||||
return JSON.stringify({ error: msg });
|
||||
}
|
||||
}) as any,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Read-only tool subset — used when enableTools=false to avoid empty tools rejection. */
|
||||
function createReadOnlyTools(workspaceDir: string, logger?: Logger) {
|
||||
const all = createSandboxedTools(workspaceDir, logger);
|
||||
return { read_file: all.read_file };
|
||||
}
|
||||
|
||||
// ============================
|
||||
// StandaloneLLMRunner
|
||||
// ============================
|
||||
|
||||
export class StandaloneLLMRunner implements LLMRunner {
|
||||
private config: StandaloneLLMConfig;
|
||||
private model: string;
|
||||
private enableTools: boolean;
|
||||
private logger?: Logger;
|
||||
|
||||
constructor(opts: {
|
||||
config: StandaloneLLMConfig;
|
||||
model?: string;
|
||||
enableTools?: boolean;
|
||||
logger?: Logger;
|
||||
}) {
|
||||
this.config = opts.config;
|
||||
this.model = opts.model ?? opts.config.model;
|
||||
this.enableTools = opts.enableTools ?? false;
|
||||
this.logger = opts.logger;
|
||||
}
|
||||
|
||||
async run(params: LLMRunParams): Promise<string> {
|
||||
const runStartMs = Date.now();
|
||||
const timeoutMs = params.timeoutMs ?? this.config.timeoutMs ?? 120_000;
|
||||
const maxTokens = params.maxTokens ?? this.config.maxTokens ?? 4096;
|
||||
const workspaceDir = params.workspaceDir ?? process.cwd();
|
||||
|
||||
this.logger?.debug?.(
|
||||
`${TAG} run() start: taskId=${params.taskId}, model=${this.model}, ` +
|
||||
`tools=${this.enableTools}, timeout=${timeoutMs}ms`,
|
||||
);
|
||||
|
||||
// Create OpenAI-compatible provider via AI SDK
|
||||
// Use "compatible" mode to call /chat/completions (not Responses API),
|
||||
// which works with all OpenAI-compatible backends (DeepSeek, Qwen, etc.)
|
||||
const provider = createOpenAI({
|
||||
baseURL: this.config.baseUrl,
|
||||
apiKey: this.config.apiKey,
|
||||
compatibility: "compatible",
|
||||
});
|
||||
|
||||
// Select tools based on mode
|
||||
const tools = this.enableTools
|
||||
? createSandboxedTools(workspaceDir, this.logger)
|
||||
: createReadOnlyTools(workspaceDir, this.logger);
|
||||
|
||||
try {
|
||||
const result = await generateText({
|
||||
model: provider.chat(this.model),
|
||||
system: params.systemPrompt,
|
||||
prompt: params.prompt,
|
||||
tools,
|
||||
stopWhen: stepCountIs(this.enableTools ? MAX_TOOL_ITERATIONS : 1),
|
||||
maxOutputTokens: maxTokens,
|
||||
abortSignal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
|
||||
const text = result.text.trim();
|
||||
const totalMs = Date.now() - runStartMs;
|
||||
|
||||
this.logger?.debug?.(
|
||||
`${TAG} run() completed: ${totalMs}ms, steps=${result.steps.length}, output=${text.length} chars`,
|
||||
);
|
||||
|
||||
// Log tool usage if any
|
||||
if (result.steps.length > 1) {
|
||||
const toolCalls = result.steps.flatMap((s) => s.toolCalls ?? []);
|
||||
this.logger?.debug?.(
|
||||
`${TAG} Tool calls: ${toolCalls.map((tc) => tc.toolName).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Metric
|
||||
if (params.instanceId) {
|
||||
report("llm_call", {
|
||||
taskId: params.taskId,
|
||||
provider: "standalone",
|
||||
model: this.model,
|
||||
inputLength: params.prompt.length,
|
||||
outputLength: text.length,
|
||||
totalDurationMs: totalMs,
|
||||
success: true,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
|
||||
return text;
|
||||
} catch (err) {
|
||||
const totalMs = Date.now() - runStartMs;
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
this.logger?.error(`${TAG} run() failed after ${totalMs}ms: ${errMsg}`);
|
||||
|
||||
if (params.instanceId) {
|
||||
report("llm_call", {
|
||||
taskId: params.taskId,
|
||||
provider: "standalone",
|
||||
model: this.model,
|
||||
inputLength: params.prompt.length,
|
||||
outputLength: 0,
|
||||
totalDurationMs: totalMs,
|
||||
success: false,
|
||||
error: errMsg,
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================
|
||||
// StandaloneLLMRunnerFactory
|
||||
// ============================
|
||||
|
||||
export interface StandaloneLLMRunnerFactoryOptions {
|
||||
/** LLM API configuration. */
|
||||
config: StandaloneLLMConfig;
|
||||
/** Logger instance. */
|
||||
logger?: Logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory that creates StandaloneLLMRunner instances.
|
||||
*
|
||||
* Used by the Gateway and Hermes host adapters.
|
||||
*/
|
||||
export class StandaloneLLMRunnerFactory implements LLMRunnerFactory {
|
||||
private config: StandaloneLLMConfig;
|
||||
private logger?: Logger;
|
||||
|
||||
constructor(opts: StandaloneLLMRunnerFactoryOptions) {
|
||||
this.config = opts.config;
|
||||
this.logger = opts.logger;
|
||||
}
|
||||
|
||||
createRunner(opts?: LLMRunnerCreateOptions): LLMRunner {
|
||||
const enableTools = opts?.enableTools ?? false;
|
||||
const modelRef = opts?.modelRef;
|
||||
|
||||
// Parse "provider/model" → just use the model part for OpenAI-compatible API
|
||||
let model = this.config.model;
|
||||
if (modelRef) {
|
||||
const slashIdx = modelRef.indexOf("/");
|
||||
model = slashIdx > 0 ? modelRef.slice(slashIdx + 1) : modelRef;
|
||||
}
|
||||
|
||||
this.logger?.debug?.(
|
||||
`${TAG} Creating StandaloneLLMRunner: model=${model}, tools=${enableTools}`,
|
||||
);
|
||||
|
||||
return new StandaloneLLMRunner({
|
||||
config: this.config,
|
||||
model,
|
||||
enableTools,
|
||||
logger: this.logger,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user