@botbotgo/agent-harness 0.0.271 → 0.0.273
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/README.md
CHANGED
|
@@ -704,6 +704,7 @@ Discovery rules:
|
|
|
704
704
|
Example workspaces:
|
|
705
705
|
|
|
706
706
|
- `examples/hello-skill-app/` keeps the smallest local tool + skill workspace
|
|
707
|
+
- `examples/local-scheduled-task-app/` runs recurring prompt-driven tasks with a local `node-llama-cpp` GGUF workspace and one local tool
|
|
707
708
|
- `examples/multimodal-app/` keeps the smallest image-plus-PDF example and sends both through one `request(...)` call
|
|
708
709
|
- `examples/plan-and-run-app/` keeps the smallest public-API planning example and prints both the plan and the observed execution steps
|
|
709
710
|
- `examples/runtime-flow-demo/` runs one real hosted-model request and exports a Mermaid flowchart from runtime plus upstream events
|
|
@@ -743,6 +744,13 @@ Practical guidance:
|
|
|
743
744
|
- use `backend: deepagent` for approvals, resume, multi-agent orchestration, rich memory flows, and heavier tool chains
|
|
744
745
|
- keep `backend: langchain-v1` for lighter direct-response or explicitly chosen V1 agent shapes while this upstream behavior settles
|
|
745
746
|
|
|
747
|
+
Local GGUF note:
|
|
748
|
+
|
|
749
|
+
- `provider: node-llama-cpp` now exposes a LangChain-style tool-binding shim, so local GGUF models can enter the standard tool-calling path without an app-owned model wrapper
|
|
750
|
+
- `backend: langchain-v1` is the straightforward local GGUF path and is the currently verified default for `node-llama-cpp` tool use
|
|
751
|
+
- `backend: deepagent` can also reach the same tool-calling path, but final reliability still depends on the selected model following upstream tool schemas correctly
|
|
752
|
+
- `agent-harness` does not try to normalize every model-specific argument drift or malformed tool payload; once the runtime hands a call to upstream tools, schema fidelity is a model responsibility
|
|
753
|
+
|
|
746
754
|
### `config/runtime/workspace.yaml`
|
|
747
755
|
|
|
748
756
|
Use this file for workspace-wide runtime policy.
|
|
@@ -1059,6 +1067,7 @@ ACP transport notes:
|
|
|
1059
1067
|
- `serveAcpStdio(runtime)` exposes newline-delimited JSON-RPC over stdio for local IDE, CLI, or subprocess clients.
|
|
1060
1068
|
- `serveAcpHttp(runtime)` exposes JSON-RPC over HTTP plus SSE runtime events so remote operator surfaces can connect without importing the runtime in-process.
|
|
1061
1069
|
- ACP transport validation now covers the reference-client core flow: capability discovery, request submit, session lookup, request lookup, invalid-JSON handling, notification calls without response ids, stdio JSON-RPC, and HTTP plus SSE runtime notifications.
|
|
1070
|
+
- Cross-protocol conformance now has an explicit regression gate as well: ACP submission, A2A task lookup and continuation, and runtime MCP inspection must all project the same persisted `sessionId` / `requestId` runtime records instead of drifting into surface-specific identifiers or side stores.
|
|
1062
1071
|
- For the thinnest editor or CLI starter, begin with `agent-harness acp serve --workspace . --transport stdio` and mirror the `examples/protocol-hello-world/app/acp-stdio-hello-world.mjs` wire shape. Applications that want an in-process reference client can use `createAcpStdioClient(...)` to issue JSON-RPC requests and route runtime notifications without hand-rolling line parsing.
|
|
1063
1072
|
- `serveA2aHttp(runtime)` exposes an A2A-compatible HTTP JSON-RPC bridge plus agent card discovery, mapping both existing methods such as `message/send` and A2A v1.0 PascalCase methods such as `SendMessage`, `SendStreamingMessage`, `GetTask`, `ListTasks`, `CancelTask`, `SubscribeToTask`, `GetAgentCard`, `GetExtendedAgentCard`, and task push-notification config methods onto the existing session/request runtime surface. The bridge now advertises both `1.0` and `0.3` JSON-RPC interfaces, answers `HEAD` / `OPTIONS` discovery on the agent-card path, sets supported-version discovery headers, can optionally expose registry URLs plus detached signed-card metadata for surrounding discovery systems, validates `A2A-Version`, records `A2A-Extensions` into runtime invocation metadata, publishes `TASK_STATE_*` statuses plus the `{ task }` `SendMessage` wrapper, streams an initial `{ task }` snapshot plus later `{ statusUpdate }` payloads over SSE for v1 streaming methods, and can send best-effort webhook task snapshots for configured push notification receivers.
|
|
1064
1073
|
- `serveAgUiHttp(runtime)` exposes an AG-UI-compatible HTTP SSE bridge that projects runtime lifecycle, text output, upstream thinking, step progress, and tool calls onto `RUN_*`, `TEXT_MESSAGE_*`, `THINKING_TEXT_MESSAGE_*`, `STEP_*`, and `TOOL_CALL_*` events for UI clients.
|
package/README.zh.md
CHANGED
|
@@ -667,6 +667,7 @@ await stop(runtime);
|
|
|
667
667
|
示例工作区:
|
|
668
668
|
|
|
669
669
|
- `examples/hello-skill-app/` 保留最小的本地 tool + skill 工作区
|
|
670
|
+
- `examples/local-scheduled-task-app/` 展示如何用本地 `node-llama-cpp` GGUF 工作区和一个本地 tool 执行周期性 prompt 任务
|
|
670
671
|
- `examples/multimodal-app/` 保留最小的图片 + PDF 示例,并通过一次 `request(...)` 调用发送
|
|
671
672
|
- `examples/plan-and-run-app/` 保留最小的公开 API 规划示例,并同时打印规划步骤和真实执行步骤
|
|
672
673
|
- `examples/runtime-flow-demo/` 会跑一次真实 hosted model 请求,并把 runtime 与 upstream events 导出为 Mermaid flowchart
|
|
@@ -700,6 +701,13 @@ await stop(runtime);
|
|
|
700
701
|
- approvals、resume、多 agent orchestration、复杂 memory 流、重工具链,优先使用 `backend: deepagent`
|
|
701
702
|
- `backend: langchain-v1` 先保留给轻量 direct-response 场景,或明确需要 V1 agent 语义的工作区
|
|
702
703
|
|
|
704
|
+
本地 GGUF 补充说明:
|
|
705
|
+
|
|
706
|
+
- `provider: node-llama-cpp` 现在带有一层 LangChain 风格的 tool-binding shim,因此本地 GGUF 模型可以进入标准 tool-calling 路径,而不需要应用自己包一层 model wrapper
|
|
707
|
+
- 对 `node-llama-cpp` 来说,`backend: langchain-v1` 仍然是更直接、当前已验证的本地 tool use 路径
|
|
708
|
+
- `backend: deepagent` 也可以走到同一条 tool-calling 路径,但最终稳定性仍取决于所选模型是否能正确遵守 upstream tool schema
|
|
709
|
+
- `agent-harness` 不会为每个模型的参数漂移或畸形 tool payload 做无限兼容;runtime 把调用交给 upstream tools 之后,schema fidelity 就属于模型责任
|
|
710
|
+
|
|
703
711
|
### `config/runtime/workspace.yaml`
|
|
704
712
|
|
|
705
713
|
用于工作区范围的运行时策略。
|
|
@@ -1016,6 +1024,7 @@ ACP transport 说明:
|
|
|
1016
1024
|
- `serveAcpStdio(runtime)` 提供基于 stdio 的 newline-delimited JSON-RPC,适合本地 IDE、CLI 或子进程客户端。
|
|
1017
1025
|
- `serveAcpHttp(runtime)` 提供基于 HTTP 的 JSON-RPC 与 SSE runtime events,适合远程界面或独立控制面接入。
|
|
1018
1026
|
- ACP transport 现已覆盖核心参考客户端流程验证:capability discovery、request submit、session lookup、request lookup、invalid JSON 处理、无 id notification 不返回响应,以及 stdio JSON-RPC 与 HTTP + SSE runtime notifications。
|
|
1027
|
+
- 现在还额外有一条跨协议一致性回归门:ACP 发起的 request、A2A 读取或继续的 task,以及 runtime MCP 暴露的检查结果,必须始终指向同一组持久化 `sessionId` / `requestId` 运行时记录,不能漂移成各协议各自维护的标识体系。
|
|
1019
1028
|
- 如果要从最薄的一层 editor / CLI starter 开始,优先用 `agent-harness acp serve --workspace . --transport stdio`,并直接参考 `examples/protocol-hello-world/app/acp-stdio-hello-world.mjs` 的 wire shape。需要在应用内使用 reference client 时,可直接用 `createAcpStdioClient(...)` 发起 JSON-RPC 请求并分流 runtime notifications,避免每个 sidecar 自己重写 line parsing。
|
|
1020
1029
|
- `serveA2aHttp(runtime)` 提供 A2A HTTP JSON-RPC bridge 与 agent card discovery,同时兼容 `message/send` 这类旧方法,以及 `SendMessage`、`SendStreamingMessage`、`GetTask`、`ListTasks`、`CancelTask`、`SubscribeToTask`、`GetAgentCard`、`GetExtendedAgentCard` 与 task push-notification config 这类 A2A v1.0 方法,并统一映射到现有 session/request 运行记录。bridge 现在会同时声明 `1.0` 与 `0.3` 两个 JSON-RPC interface、在 agent card 路径上响应 `HEAD` / `OPTIONS` discovery、写出支持版本的 discovery headers、可选暴露 registry URL 与 detached signed-card metadata 供外围发现系统使用、校验 `A2A-Version`、把 `A2A-Extensions` 记录进 runtime invocation metadata、发布 `TASK_STATE_*` 状态与 `SendMessage` 的 `{ task }` wrapper、在 v1 streaming 方法上先通过 SSE 输出 `{ task }` 初始快照,再输出 `{ statusUpdate }` 增量状态,并可向已配置的 webhook receiver 发送 best-effort task push notifications。
|
|
1021
1030
|
- `serveAgUiHttp(runtime)` 提供 AG-UI HTTP SSE bridge,把 runtime 生命周期、文本输出、upstream thinking、step 进度与 tool call 投影成 `RUN_*`、`TEXT_MESSAGE_*`、`THINKING_TEXT_MESSAGE_*`、`STEP_*` 与 `TOOL_CALL_*` 事件,便于 UI 客户端直接接入。
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const AGENT_HARNESS_VERSION = "0.0.
|
|
1
|
+
export declare const AGENT_HARNESS_VERSION = "0.0.272";
|
package/dist/package-version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AGENT_HARNESS_VERSION = "0.0.
|
|
1
|
+
export const AGENT_HARNESS_VERSION = "0.0.272";
|
|
@@ -2,8 +2,324 @@ import { ChatAnthropic } from "@langchain/anthropic";
|
|
|
2
2
|
import { ChatGoogle } from "@langchain/google";
|
|
3
3
|
import { ChatOllama } from "@langchain/ollama";
|
|
4
4
|
import { ChatOpenAI } from "@langchain/openai";
|
|
5
|
+
import { AIMessage } from "langchain";
|
|
5
6
|
import { initChatModel } from "langchain";
|
|
7
|
+
import { salvageToolArgs, tryParseJson } from "../../parsing/output-parsing.js";
|
|
8
|
+
import { normalizeModelFacingToolSchema } from "../tool/resolved-tool.js";
|
|
6
9
|
import { normalizeOpenAICompatibleInit } from "../compat/openai-compatible.js";
|
|
10
|
+
const NODE_LLAMA_CPP_TOOL_CALL_INSTRUCTION = [
|
|
11
|
+
"Available tools are listed below.",
|
|
12
|
+
"If you need a tool, respond with only one JSON object.",
|
|
13
|
+
'Use this exact shape: {"name":"tool_name","arguments":{"key":"value"}}',
|
|
14
|
+
"Do not add markdown, prose, or code fences unless the output is wrapped inside <tool_call>...</tool_call>.",
|
|
15
|
+
"If no tool is needed, answer normally.",
|
|
16
|
+
].join("\n");
|
|
17
|
+
function readModelText(value) {
|
|
18
|
+
if (typeof value === "string") {
|
|
19
|
+
return value.trim();
|
|
20
|
+
}
|
|
21
|
+
if (typeof value !== "object" || value === null) {
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
const typed = value;
|
|
25
|
+
if (typeof typed.content === "string") {
|
|
26
|
+
return typed.content.trim();
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(typed.content)) {
|
|
29
|
+
return typed.content
|
|
30
|
+
.map((item) => typeof item === "string"
|
|
31
|
+
? item
|
|
32
|
+
: typeof item === "object" && item !== null && typeof item.text === "string"
|
|
33
|
+
? item.text
|
|
34
|
+
: "")
|
|
35
|
+
.join("")
|
|
36
|
+
.trim();
|
|
37
|
+
}
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
function readPromptContent(value) {
|
|
41
|
+
if (typeof value === "string") {
|
|
42
|
+
return value.trim();
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
return value.map((item) => readPromptContent(item)).filter(Boolean).join("\n").trim();
|
|
46
|
+
}
|
|
47
|
+
if (typeof value !== "object" || value === null) {
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
if (typeof value.content === "string" || Array.isArray(value.content)) {
|
|
51
|
+
return readModelText(value);
|
|
52
|
+
}
|
|
53
|
+
if (typeof value.text === "string") {
|
|
54
|
+
return String(value.text).trim();
|
|
55
|
+
}
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
function readMessageType(value) {
|
|
59
|
+
if (typeof value !== "object" || value === null) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
if (typeof value._getType === "function") {
|
|
63
|
+
return String(value._getType() ?? "");
|
|
64
|
+
}
|
|
65
|
+
if (typeof value.getType === "function") {
|
|
66
|
+
return String(value.getType() ?? "");
|
|
67
|
+
}
|
|
68
|
+
const ids = Array.isArray(value.id)
|
|
69
|
+
? (value.id).filter((item) => typeof item === "string")
|
|
70
|
+
: [];
|
|
71
|
+
const typeName = ids.at(-1);
|
|
72
|
+
if (typeName === "HumanMessage")
|
|
73
|
+
return "human";
|
|
74
|
+
if (typeName === "SystemMessage")
|
|
75
|
+
return "system";
|
|
76
|
+
if (typeName === "AIMessage")
|
|
77
|
+
return "ai";
|
|
78
|
+
if (typeName === "ToolMessage")
|
|
79
|
+
return "tool";
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
function mapMessageRole(value) {
|
|
83
|
+
const directRole = typeof value?.role === "string"
|
|
84
|
+
? String(value.role).trim().toLowerCase()
|
|
85
|
+
: undefined;
|
|
86
|
+
if (directRole) {
|
|
87
|
+
if (directRole === "assistant")
|
|
88
|
+
return "ASSISTANT";
|
|
89
|
+
if (directRole === "tool")
|
|
90
|
+
return "TOOL";
|
|
91
|
+
return directRole.toUpperCase();
|
|
92
|
+
}
|
|
93
|
+
const messageType = readMessageType(value);
|
|
94
|
+
if (messageType === "system")
|
|
95
|
+
return "SYSTEM";
|
|
96
|
+
if (messageType === "human")
|
|
97
|
+
return "USER";
|
|
98
|
+
if (messageType === "ai")
|
|
99
|
+
return "ASSISTANT";
|
|
100
|
+
if (messageType === "tool")
|
|
101
|
+
return "TOOL";
|
|
102
|
+
return "USER";
|
|
103
|
+
}
|
|
104
|
+
function readToolCalls(value) {
|
|
105
|
+
if (typeof value !== "object" || value === null) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
if (Array.isArray(value.tool_calls)) {
|
|
109
|
+
return value.tool_calls;
|
|
110
|
+
}
|
|
111
|
+
if (typeof value.kwargs === "object" && value.kwargs !== null) {
|
|
112
|
+
const toolCalls = value.kwargs.tool_calls;
|
|
113
|
+
return Array.isArray(toolCalls) ? toolCalls : [];
|
|
114
|
+
}
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
function formatStructuredMessage(value) {
|
|
118
|
+
const role = mapMessageRole(value);
|
|
119
|
+
const content = readPromptContent(value);
|
|
120
|
+
if (role === "ASSISTANT") {
|
|
121
|
+
const toolCalls = readToolCalls(value);
|
|
122
|
+
if (toolCalls.length > 0) {
|
|
123
|
+
return [
|
|
124
|
+
"ASSISTANT_TOOL_CALLS:",
|
|
125
|
+
JSON.stringify(toolCalls),
|
|
126
|
+
].join("\n");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (role === "TOOL") {
|
|
130
|
+
const typed = value;
|
|
131
|
+
const name = typeof typed.name === "string"
|
|
132
|
+
? typed.name
|
|
133
|
+
: typeof typed.kwargs === "object" && typed.kwargs !== null && typeof typed.kwargs.name === "string"
|
|
134
|
+
? String(typed.kwargs.name)
|
|
135
|
+
: typeof typed.lc_kwargs === "object" && typed.lc_kwargs !== null && typeof typed.lc_kwargs.name === "string"
|
|
136
|
+
? String(typed.lc_kwargs.name)
|
|
137
|
+
: "";
|
|
138
|
+
const toolCallId = typeof typed.tool_call_id === "string"
|
|
139
|
+
? typed.tool_call_id
|
|
140
|
+
: typeof typed.kwargs === "object" && typed.kwargs !== null && typeof typed.kwargs.tool_call_id === "string"
|
|
141
|
+
? String(typed.kwargs.tool_call_id)
|
|
142
|
+
: typeof typed.lc_kwargs === "object" && typed.lc_kwargs !== null && typeof typed.lc_kwargs.tool_call_id === "string"
|
|
143
|
+
? String(typed.lc_kwargs.tool_call_id)
|
|
144
|
+
: "";
|
|
145
|
+
return [
|
|
146
|
+
"TOOL_RESULT:",
|
|
147
|
+
name ? `name=${name}` : "",
|
|
148
|
+
toolCallId ? `tool_call_id=${toolCallId}` : "",
|
|
149
|
+
content,
|
|
150
|
+
].filter(Boolean).join("\n");
|
|
151
|
+
}
|
|
152
|
+
return content ? `${role}:\n${content}` : "";
|
|
153
|
+
}
|
|
154
|
+
function stringifyNodeLlamaCppInput(input) {
|
|
155
|
+
if (typeof input === "string") {
|
|
156
|
+
return input;
|
|
157
|
+
}
|
|
158
|
+
if (Array.isArray(input)) {
|
|
159
|
+
return input
|
|
160
|
+
.map((message) => formatStructuredMessage(message))
|
|
161
|
+
.filter(Boolean)
|
|
162
|
+
.join("\n\n")
|
|
163
|
+
.trim();
|
|
164
|
+
}
|
|
165
|
+
if (typeof input === "object" && input !== null && Array.isArray(input.messages)) {
|
|
166
|
+
return stringifyNodeLlamaCppInput(input.messages);
|
|
167
|
+
}
|
|
168
|
+
return readPromptContent(input);
|
|
169
|
+
}
|
|
170
|
+
function extractToolCallPayload(text) {
|
|
171
|
+
const trimmed = text.trim();
|
|
172
|
+
if (!trimmed) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const direct = tryParseJson(trimmed);
|
|
176
|
+
if (direct) {
|
|
177
|
+
return direct;
|
|
178
|
+
}
|
|
179
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1]?.trim();
|
|
180
|
+
if (fenced) {
|
|
181
|
+
const parsed = tryParseJson(fenced);
|
|
182
|
+
if (parsed) {
|
|
183
|
+
return parsed;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const xml = trimmed.match(/<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/i)?.[1]?.trim();
|
|
187
|
+
if (xml) {
|
|
188
|
+
const parsed = tryParseJson(xml);
|
|
189
|
+
if (parsed) {
|
|
190
|
+
return parsed;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
function normalizeParsedToolCall(payload) {
|
|
196
|
+
if (typeof payload !== "object" || payload === null) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
if (Array.isArray(payload)) {
|
|
200
|
+
return normalizeParsedToolCall(payload[0]);
|
|
201
|
+
}
|
|
202
|
+
const typed = payload;
|
|
203
|
+
const functionPayload = typeof typed.function === "object" && typed.function !== null ? typed.function : undefined;
|
|
204
|
+
const nameCandidate = typed.name ?? typed.tool ?? functionPayload?.name;
|
|
205
|
+
const name = typeof nameCandidate === "string" ? nameCandidate.trim() : "";
|
|
206
|
+
if (!name) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const argsCandidate = typed.arguments ?? typed.args ?? typed.parameters ?? typed.input ?? functionPayload?.arguments ?? {};
|
|
210
|
+
const args = salvageToolArgs(argsCandidate) ?? {};
|
|
211
|
+
return { name, args };
|
|
212
|
+
}
|
|
213
|
+
function formatBoundToolInstruction(tool) {
|
|
214
|
+
if (typeof tool !== "object" || tool === null) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
const typed = tool;
|
|
218
|
+
const name = typeof typed.name === "string" ? typed.name.trim() : "";
|
|
219
|
+
if (!name) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
const description = typeof typed.description === "string" ? typed.description.trim() : "";
|
|
223
|
+
const schema = normalizeModelFacingToolSchema(typed);
|
|
224
|
+
return [
|
|
225
|
+
`Tool: ${name}`,
|
|
226
|
+
description ? `Description: ${description}` : "",
|
|
227
|
+
`Arguments JSON schema: ${JSON.stringify(schema)}`,
|
|
228
|
+
].filter(Boolean).join("\n");
|
|
229
|
+
}
|
|
230
|
+
function withNodeLlamaCppToolPrompt(input, tools) {
|
|
231
|
+
const toolInstructions = tools.map((tool) => formatBoundToolInstruction(tool)).filter((value) => Boolean(value));
|
|
232
|
+
if (toolInstructions.length === 0) {
|
|
233
|
+
return stringifyNodeLlamaCppInput(input);
|
|
234
|
+
}
|
|
235
|
+
const systemContent = `${NODE_LLAMA_CPP_TOOL_CALL_INSTRUCTION}\n\n${toolInstructions.join("\n\n")}`;
|
|
236
|
+
const prompt = stringifyNodeLlamaCppInput(input);
|
|
237
|
+
return [systemContent, prompt].filter(Boolean).join("\n\n");
|
|
238
|
+
}
|
|
239
|
+
function createNodeLlamaCppToolBindableModel(model, boundTools = []) {
|
|
240
|
+
return new Proxy(model, {
|
|
241
|
+
has(target, prop) {
|
|
242
|
+
if (prop === "bindTools" || prop === "invoke" || prop === "stream" || prop === "withConfig") {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
return prop in target;
|
|
246
|
+
},
|
|
247
|
+
get(target, prop, receiver) {
|
|
248
|
+
if (prop === "bindTools") {
|
|
249
|
+
return (tools) => createNodeLlamaCppToolBindableModel(target, tools);
|
|
250
|
+
}
|
|
251
|
+
if (prop === "invoke") {
|
|
252
|
+
return async (input, config) => {
|
|
253
|
+
const rawResult = await target.invoke(boundTools.length > 0 ? withNodeLlamaCppToolPrompt(input, boundTools) : input, config);
|
|
254
|
+
if (boundTools.length === 0) {
|
|
255
|
+
return rawResult;
|
|
256
|
+
}
|
|
257
|
+
const text = readModelText(rawResult);
|
|
258
|
+
const parsedToolCall = normalizeParsedToolCall(extractToolCallPayload(text));
|
|
259
|
+
if (!parsedToolCall) {
|
|
260
|
+
return rawResult;
|
|
261
|
+
}
|
|
262
|
+
return new AIMessage({
|
|
263
|
+
content: "",
|
|
264
|
+
tool_calls: [{
|
|
265
|
+
id: `tool-${Math.random().toString(36).slice(2, 10)}`,
|
|
266
|
+
name: parsedToolCall.name,
|
|
267
|
+
args: parsedToolCall.args,
|
|
268
|
+
type: "tool_call",
|
|
269
|
+
}],
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if (prop === "stream") {
|
|
274
|
+
return async (input, config) => {
|
|
275
|
+
const value = await receiver.invoke(input, config);
|
|
276
|
+
return (async function* () {
|
|
277
|
+
yield value;
|
|
278
|
+
})();
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (prop === "withConfig" && typeof target.withConfig === "function") {
|
|
282
|
+
return (config) => createNodeLlamaCppToolBindableModel(target.withConfig(config), boundTools);
|
|
283
|
+
}
|
|
284
|
+
const member = Reflect.get(target, prop, receiver);
|
|
285
|
+
return typeof member === "function" ? member.bind(target) : member;
|
|
286
|
+
},
|
|
287
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
288
|
+
if (prop === "bindTools" || prop === "invoke" || prop === "stream" || prop === "withConfig") {
|
|
289
|
+
return {
|
|
290
|
+
configurable: true,
|
|
291
|
+
enumerable: false,
|
|
292
|
+
writable: false,
|
|
293
|
+
value: this.get?.(target, prop, target),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function inferNodeLlamaCppModelPath(model) {
|
|
301
|
+
const modelPath = typeof model.init?.modelPath === "string" ? model.init.modelPath.trim() : "";
|
|
302
|
+
if (modelPath) {
|
|
303
|
+
return modelPath;
|
|
304
|
+
}
|
|
305
|
+
return model.model.includes("/") || model.model.endsWith(".gguf") ? model.model : undefined;
|
|
306
|
+
}
|
|
307
|
+
async function createNodeLlamaCppModel(model) {
|
|
308
|
+
const modelPath = inferNodeLlamaCppModelPath(model);
|
|
309
|
+
if (!modelPath) {
|
|
310
|
+
throw new Error(`Model ${model.id} with provider ${model.provider} must define a GGUF path via top-level modelPath or use model as the GGUF path.`);
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
const { ChatLlamaCpp } = await import("@langchain/community/chat_models/llama_cpp");
|
|
314
|
+
return createNodeLlamaCppToolBindableModel(await ChatLlamaCpp.initialize({
|
|
315
|
+
...model.init,
|
|
316
|
+
modelPath,
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
throw new Error(`Failed to initialize ${model.provider} model ${model.id}. Install node-llama-cpp in the application workspace and ensure the GGUF file exists at ${modelPath}.`, { cause: error });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
7
323
|
export async function createResolvedModel(model, modelResolver) {
|
|
8
324
|
if (modelResolver) {
|
|
9
325
|
return modelResolver(model.id);
|
|
@@ -23,5 +339,8 @@ export async function createResolvedModel(model, modelResolver) {
|
|
|
23
339
|
if (model.provider === "google" || model.provider === "google-genai" || model.provider === "gemini") {
|
|
24
340
|
return new ChatGoogle({ model: model.model, ...model.init });
|
|
25
341
|
}
|
|
342
|
+
if (model.provider === "node-llama-cpp" || model.provider === "llama-cpp") {
|
|
343
|
+
return createNodeLlamaCppModel(model);
|
|
344
|
+
}
|
|
26
345
|
return initChatModel(model.model, { modelProvider: model.provider, ...model.init });
|
|
27
346
|
}
|
|
@@ -46,6 +46,29 @@ function normalizeOpenAICompatibleInit(init) {
|
|
|
46
46
|
delete normalized.omitAuthHeader;
|
|
47
47
|
return normalized;
|
|
48
48
|
}
|
|
49
|
+
function inferNodeLlamaCppModelPath(embeddingModel) {
|
|
50
|
+
const modelPath = typeof embeddingModel.init?.modelPath === "string" ? embeddingModel.init.modelPath.trim() : "";
|
|
51
|
+
if (modelPath) {
|
|
52
|
+
return modelPath;
|
|
53
|
+
}
|
|
54
|
+
return embeddingModel.model.includes("/") || embeddingModel.model.endsWith(".gguf") ? embeddingModel.model : undefined;
|
|
55
|
+
}
|
|
56
|
+
async function createNodeLlamaCppEmbeddings(embeddingModel) {
|
|
57
|
+
const modelPath = inferNodeLlamaCppModelPath(embeddingModel);
|
|
58
|
+
if (!modelPath) {
|
|
59
|
+
throw new Error(`Embedding model ${embeddingModel.id} with provider ${embeddingModel.provider} must define a GGUF path via top-level modelPath or use model as the GGUF path.`);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const { LlamaCppEmbeddings } = await import("@langchain/community/embeddings/llama_cpp");
|
|
63
|
+
return await LlamaCppEmbeddings.initialize({
|
|
64
|
+
...embeddingModel.init,
|
|
65
|
+
modelPath,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
throw new Error(`Failed to initialize ${embeddingModel.provider} embedding model ${embeddingModel.id}. Install node-llama-cpp in the application workspace and ensure the GGUF file exists at ${modelPath}.`, { cause: error });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
49
72
|
export function resolveCompiledEmbeddingModelRef(workspace, embeddingModelRef) {
|
|
50
73
|
const resolvedId = embeddingModelRef ? resolveRefId(embeddingModelRef) : "default";
|
|
51
74
|
const embeddingModel = workspace.embeddings.get(resolvedId);
|
|
@@ -85,5 +108,8 @@ export async function resolveCompiledEmbeddingModel(embeddingModel, resolver) {
|
|
|
85
108
|
if (embeddingModel.provider === "llamaindex-ollama") {
|
|
86
109
|
return createLlamaIndexEmbeddingModel(embeddingModel);
|
|
87
110
|
}
|
|
88
|
-
|
|
111
|
+
if (embeddingModel.provider === "node-llama-cpp" || embeddingModel.provider === "llama-cpp") {
|
|
112
|
+
return createNodeLlamaCppEmbeddings(embeddingModel);
|
|
113
|
+
}
|
|
114
|
+
throw new Error(`Embedding model provider ${embeddingModel.provider} is not supported by the built-in runtime. Configure embeddingModelResolver or use openai-compatible/openai/ollama/llamaindex-ollama/node-llama-cpp.`);
|
|
89
115
|
}
|