@agentprojectcontext/apx 1.16.0 → 1.17.0
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": "@agentprojectcontext/apx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
},
|
|
36
36
|
"packageManager": "pnpm@10.25.0",
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"@langchain/anthropic": "^0.3.34",
|
|
39
|
+
"@langchain/community": "^0.3.59",
|
|
40
|
+
"@langchain/core": "^0.3.80",
|
|
41
|
+
"@langchain/ollama": "^0.2.4",
|
|
38
42
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
39
43
|
"@opentui/core": "^0.2.8",
|
|
40
44
|
"@opentui/keymap": "^0.2.8",
|
|
@@ -52,6 +56,7 @@
|
|
|
52
56
|
"express": "^4.21.0",
|
|
53
57
|
"fuzzysort": "^3.1.0",
|
|
54
58
|
"jsonc-parser": "^3.3.1",
|
|
59
|
+
"langchain": "^0.3.37",
|
|
55
60
|
"node-fetch": "^3.3.2",
|
|
56
61
|
"open": "^11.0.0",
|
|
57
62
|
"opentui-spinner": "^0.0.6",
|
|
@@ -60,7 +65,8 @@
|
|
|
60
65
|
"semver": "^7.8.0",
|
|
61
66
|
"solid-js": "^1.9.12",
|
|
62
67
|
"strip-ansi": "^7.2.0",
|
|
63
|
-
"yargs": "^18.0.0"
|
|
68
|
+
"yargs": "^18.0.0",
|
|
69
|
+
"zod": "^3.25.76"
|
|
64
70
|
},
|
|
65
71
|
"optionalDependencies": {
|
|
66
72
|
"fast-glob": "^3.3.2",
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// LangChain adapter for the APX super-agent.
|
|
2
|
+
//
|
|
3
|
+
// Lives alongside the native loop in super-agent.js. Selected via
|
|
4
|
+
// config.super_agent.engine === "langchain" (default: "native"). The two
|
|
5
|
+
// implementations expose the same shape:
|
|
6
|
+
//
|
|
7
|
+
// { text, usage, name, trace } ← return value
|
|
8
|
+
// { globalConfig, projects, plugins, registries, prompt,
|
|
9
|
+
// previousMessages, contextNote, onEvent, onToken, signal } ← input
|
|
10
|
+
//
|
|
11
|
+
// Why a toggle and not a replacement: the native loop carries APX-specific
|
|
12
|
+
// features (pseudo-tool fallback for Ollama 500, ghost-response detection,
|
|
13
|
+
// permission_mode gates wired through tool handlers, identity-block injection,
|
|
14
|
+
// ACK_ONLY_TOOLS streak guard). Re-implementing all of those inside LangChain
|
|
15
|
+
// is a large refactor; meanwhile the toggle lets us A/B both paths and pick
|
|
16
|
+
// the one that actually behaves better with gemma4-class models on the
|
|
17
|
+
// user's hardware.
|
|
18
|
+
//
|
|
19
|
+
// LangChain version compat: written against @langchain/core ^0.3 +
|
|
20
|
+
// langchain ^0.3 + @langchain/anthropic ^0.3 + @langchain/ollama ^0.2.
|
|
21
|
+
//
|
|
22
|
+
// Limitations vs native loop (acknowledged in v1):
|
|
23
|
+
// - permission_mode confirmations are still enforced inside each tool
|
|
24
|
+
// handler (they return {error: "requires_confirmation: ..."}), but
|
|
25
|
+
// the loop has no UI to ask the user mid-run, so confirmable tools
|
|
26
|
+
// just fail-fast as they do today.
|
|
27
|
+
// - Pseudo-tool fallback (for Ollama 500 on structured tools) is NOT
|
|
28
|
+
// implemented here — if the underlying engine fails, the call
|
|
29
|
+
// propagates. Use engine === "native" for that case.
|
|
30
|
+
|
|
31
|
+
import { AgentExecutor, createToolCallingAgent } from "langchain/agents";
|
|
32
|
+
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
|
|
33
|
+
import { DynamicStructuredTool } from "@langchain/core/tools";
|
|
34
|
+
import { HumanMessage, AIMessage, ToolMessage } from "@langchain/core/messages";
|
|
35
|
+
import { z } from "zod";
|
|
36
|
+
|
|
37
|
+
import { TOOL_SCHEMAS, makeToolHandlers } from "./super-agent-tools.js";
|
|
38
|
+
import { readIdentity } from "../core/identity.js";
|
|
39
|
+
import { logInfo, logWarn, logError } from "../core/logging.js";
|
|
40
|
+
|
|
41
|
+
const MAX_ITER_DEFAULT = 15;
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// JSON-Schema → Zod converter
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// LangChain's DynamicStructuredTool wants a Zod schema. APX's tools ship JSON
|
|
47
|
+
// Schema (in the OpenAI function-calling shape). We translate just enough for
|
|
48
|
+
// the parameter types APX actually uses: string, number, boolean, object,
|
|
49
|
+
// array, enum, optional/required. Anything more exotic falls back to z.any().
|
|
50
|
+
function jsonSchemaToZod(schema) {
|
|
51
|
+
if (!schema || typeof schema !== "object") return z.any();
|
|
52
|
+
// OpenAI function shape: { type: "function", function: { parameters: {...} } }
|
|
53
|
+
const root = schema.function?.parameters || schema.parameters || schema;
|
|
54
|
+
return objectToZod(root);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function objectToZod(obj) {
|
|
58
|
+
if (!obj || obj.type !== "object" || !obj.properties) {
|
|
59
|
+
return z.object({}).passthrough();
|
|
60
|
+
}
|
|
61
|
+
const required = new Set(obj.required || []);
|
|
62
|
+
const shape = {};
|
|
63
|
+
for (const [key, prop] of Object.entries(obj.properties)) {
|
|
64
|
+
let s = propToZod(prop);
|
|
65
|
+
if (!required.has(key)) s = s.optional();
|
|
66
|
+
if (prop?.description) s = s.describe(prop.description);
|
|
67
|
+
shape[key] = s;
|
|
68
|
+
}
|
|
69
|
+
return z.object(shape);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function propToZod(prop) {
|
|
73
|
+
if (!prop || typeof prop !== "object") return z.any();
|
|
74
|
+
if (Array.isArray(prop.enum) && prop.enum.length > 0) {
|
|
75
|
+
return z.enum(prop.enum);
|
|
76
|
+
}
|
|
77
|
+
switch (prop.type) {
|
|
78
|
+
case "string": return z.string();
|
|
79
|
+
case "number": return z.number();
|
|
80
|
+
case "integer": return z.number().int();
|
|
81
|
+
case "boolean": return z.boolean();
|
|
82
|
+
case "array": return z.array(prop.items ? propToZod(prop.items) : z.any());
|
|
83
|
+
case "object": return objectToZod(prop);
|
|
84
|
+
default: return z.any();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// APX tool → LangChain DynamicStructuredTool
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
function buildLangChainTools(handlers, schemas, { trace, onEvent }) {
|
|
92
|
+
return schemas.map((s) => {
|
|
93
|
+
const name = s.function.name;
|
|
94
|
+
const handler = handlers[name];
|
|
95
|
+
if (!handler) {
|
|
96
|
+
logWarn("super-agent-lc", `no handler for tool ${name} — skipping`);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return new DynamicStructuredTool({
|
|
100
|
+
name,
|
|
101
|
+
description: s.function.description || "",
|
|
102
|
+
schema: jsonSchemaToZod(s),
|
|
103
|
+
func: async (args) => {
|
|
104
|
+
const traceId = `lc:${trace.length + 1}`;
|
|
105
|
+
if (typeof onEvent === "function") {
|
|
106
|
+
try {
|
|
107
|
+
await onEvent({
|
|
108
|
+
type: "tool_start",
|
|
109
|
+
trace: { id: traceId, tool: name, args, pending: true },
|
|
110
|
+
});
|
|
111
|
+
} catch {}
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const result = await handler(args || {});
|
|
115
|
+
trace.push({ id: traceId, tool: name, args, result });
|
|
116
|
+
if (typeof onEvent === "function") {
|
|
117
|
+
try { await onEvent({ type: "tool_result", trace: { id: traceId, tool: name, args, result } }); } catch {}
|
|
118
|
+
}
|
|
119
|
+
return typeof result === "string" ? result : JSON.stringify(result);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
const errObj = { error: e.message };
|
|
122
|
+
trace.push({ id: traceId, tool: name, args, result: errObj });
|
|
123
|
+
if (typeof onEvent === "function") {
|
|
124
|
+
try { await onEvent({ type: "tool_result", trace: { id: traceId, tool: name, args, result: errObj } }); } catch {}
|
|
125
|
+
}
|
|
126
|
+
return JSON.stringify(errObj);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}).filter(Boolean);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Engine factory — picks an @langchain ChatModel based on modelId
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
async function makeLangChainModel(modelId, config) {
|
|
137
|
+
// modelId grammar matches engines/index.js: "<provider>:<model>" or
|
|
138
|
+
// an inferable bare model id ("claude-…" → anthropic, etc).
|
|
139
|
+
const [providerRaw, ...rest] = String(modelId || "").split(":");
|
|
140
|
+
let provider = providerRaw.toLowerCase();
|
|
141
|
+
let model = rest.join(":");
|
|
142
|
+
if (!model) {
|
|
143
|
+
// bare id — infer like engines/index.js
|
|
144
|
+
if (/^claude/i.test(providerRaw)) { provider = "anthropic"; model = providerRaw; }
|
|
145
|
+
else if (/^gpt|^o[134]/i.test(providerRaw)) { provider = "openai"; model = providerRaw; }
|
|
146
|
+
else if (/^gemini/i.test(providerRaw)) { provider = "gemini"; model = providerRaw; }
|
|
147
|
+
else { provider = "ollama"; model = providerRaw; }
|
|
148
|
+
}
|
|
149
|
+
const providerCfg = (config && config.engines && config.engines[provider]) || {};
|
|
150
|
+
|
|
151
|
+
if (provider === "anthropic") {
|
|
152
|
+
const { ChatAnthropic } = await import("@langchain/anthropic");
|
|
153
|
+
const apiKey = providerCfg.api_key || process.env.ANTHROPIC_API_KEY;
|
|
154
|
+
if (!apiKey) throw new Error("anthropic: no api_key set");
|
|
155
|
+
return new ChatAnthropic({ apiKey, model, temperature: 1.0, maxTokens: 1024 });
|
|
156
|
+
}
|
|
157
|
+
if (provider === "ollama") {
|
|
158
|
+
const { ChatOllama } = await import("@langchain/ollama");
|
|
159
|
+
const baseUrl = providerCfg.base_url || process.env.OLLAMA_HOST || "http://localhost:11434";
|
|
160
|
+
return new ChatOllama({ baseUrl, model, temperature: 0.7 });
|
|
161
|
+
}
|
|
162
|
+
if (provider === "openai") {
|
|
163
|
+
// Lazy import — only required if the user picks openai.
|
|
164
|
+
const { ChatOpenAI } = await import("@langchain/openai").catch(() => ({}));
|
|
165
|
+
if (!ChatOpenAI) throw new Error("openai: install @langchain/openai to use this provider with the langchain engine");
|
|
166
|
+
const apiKey = providerCfg.api_key || process.env.OPENAI_API_KEY;
|
|
167
|
+
if (!apiKey) throw new Error("openai: no api_key set");
|
|
168
|
+
return new ChatOpenAI({ apiKey, model, temperature: 1.0 });
|
|
169
|
+
}
|
|
170
|
+
throw new Error(`langchain engine: unknown provider "${provider}" (modelId="${modelId}")`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Convert APX "previousMessages" rows ({role, content}) → LangChain messages
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
function toLangChainHistory(previousMessages) {
|
|
177
|
+
return (previousMessages || []).map((m) => {
|
|
178
|
+
if (m.role === "user") return new HumanMessage(m.content || "");
|
|
179
|
+
if (m.role === "assistant") return new AIMessage(m.content || "");
|
|
180
|
+
if (m.role === "tool") {
|
|
181
|
+
// LangChain ToolMessage requires a tool_call_id; APX doesn't track ids
|
|
182
|
+
// in the FS history, so we use a synthetic one. The agent only sees
|
|
183
|
+
// the content anyway.
|
|
184
|
+
return new ToolMessage({ content: m.content || "", tool_call_id: m.tool_name || "tool" });
|
|
185
|
+
}
|
|
186
|
+
return new HumanMessage(m.content || "");
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Public entry — same contract as runSuperAgent in super-agent.js
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
export async function runSuperAgentLangChain({
|
|
194
|
+
globalConfig,
|
|
195
|
+
projects,
|
|
196
|
+
plugins,
|
|
197
|
+
registries,
|
|
198
|
+
prompt,
|
|
199
|
+
previousMessages = [],
|
|
200
|
+
contextNote = "",
|
|
201
|
+
systemOverride = null,
|
|
202
|
+
onEvent,
|
|
203
|
+
onToken,
|
|
204
|
+
signal,
|
|
205
|
+
}) {
|
|
206
|
+
const sa = globalConfig?.super_agent || {};
|
|
207
|
+
if (!sa.model) throw new Error("super-agent (langchain): no model configured");
|
|
208
|
+
|
|
209
|
+
const identity = (() => { try { return readIdentity(); } catch { return null; } })();
|
|
210
|
+
const userLang = globalConfig?.user?.language || "en";
|
|
211
|
+
|
|
212
|
+
// System prompt — we reuse the native module's DEFAULT_SYSTEM unless the
|
|
213
|
+
// caller passes systemOverride. This keeps the personality / language /
|
|
214
|
+
// hard-rules consistent across both engines.
|
|
215
|
+
const { DEFAULT_SYSTEM, buildIdentityBlock } = await import("./super-agent.js");
|
|
216
|
+
const identityBlock = buildIdentityBlock(identity, userLang);
|
|
217
|
+
const systemPieces = [
|
|
218
|
+
systemOverride || sa.system || DEFAULT_SYSTEM,
|
|
219
|
+
identityBlock,
|
|
220
|
+
contextNote,
|
|
221
|
+
].filter(Boolean);
|
|
222
|
+
// LangChain ChatPromptTemplate uses f-string formatting and will try to
|
|
223
|
+
// resolve any `{name}` it finds in the system text as an input variable.
|
|
224
|
+
// The APX prompt naturally contains literal `{path: <CWD>}` examples and
|
|
225
|
+
// JSON-like snippets, so we double every `{` and `}` to escape them.
|
|
226
|
+
const systemText = systemPieces.join("\n\n").replace(/[{}]/g, (c) => c + c);
|
|
227
|
+
|
|
228
|
+
const trace = [];
|
|
229
|
+
const handlers = makeToolHandlers({
|
|
230
|
+
projects, plugins, registries, globalConfig,
|
|
231
|
+
implicitConfirmation: false,
|
|
232
|
+
});
|
|
233
|
+
const tools = buildLangChainTools(handlers, TOOL_SCHEMAS, { trace, onEvent });
|
|
234
|
+
|
|
235
|
+
logInfo("super-agent-lc", "starting AgentExecutor", {
|
|
236
|
+
model: sa.model, tools: tools.length, prev: previousMessages.length,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const llm = await makeLangChainModel(sa.model, globalConfig);
|
|
240
|
+
|
|
241
|
+
const promptTemplate = ChatPromptTemplate.fromMessages([
|
|
242
|
+
["system", systemText],
|
|
243
|
+
new MessagesPlaceholder("chat_history"),
|
|
244
|
+
["human", "{input}"],
|
|
245
|
+
new MessagesPlaceholder("agent_scratchpad"),
|
|
246
|
+
]);
|
|
247
|
+
|
|
248
|
+
const agent = await createToolCallingAgent({ llm, tools, prompt: promptTemplate });
|
|
249
|
+
|
|
250
|
+
const executor = new AgentExecutor({
|
|
251
|
+
agent,
|
|
252
|
+
tools,
|
|
253
|
+
maxIterations: Number(sa.max_iterations) > 0 ? Number(sa.max_iterations) : MAX_ITER_DEFAULT,
|
|
254
|
+
returnIntermediateSteps: true,
|
|
255
|
+
handleParsingErrors: true,
|
|
256
|
+
// verbose is noisy; we already log via core/logging.js
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const t0 = Date.now();
|
|
260
|
+
let result;
|
|
261
|
+
try {
|
|
262
|
+
if (typeof onEvent === "function") {
|
|
263
|
+
try { await onEvent({ type: "model_start", iteration: 1 }); } catch {}
|
|
264
|
+
}
|
|
265
|
+
result = await executor.invoke({
|
|
266
|
+
input: prompt,
|
|
267
|
+
chat_history: toLangChainHistory(previousMessages),
|
|
268
|
+
}, { signal });
|
|
269
|
+
} catch (e) {
|
|
270
|
+
logError("super-agent-lc", `executor failed in ${Date.now() - t0}ms`, { error: e.message });
|
|
271
|
+
throw e;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// result.output is the final text. result.intermediateSteps is an array
|
|
275
|
+
// of { action, observation }; we already pushed each into `trace` from the
|
|
276
|
+
// DynamicStructuredTool wrappers, so we don't double-record them here.
|
|
277
|
+
const text = String(result.output || "");
|
|
278
|
+
logInfo("super-agent-lc", `done in ${Date.now() - t0}ms`, {
|
|
279
|
+
text_len: text.length,
|
|
280
|
+
tool_calls: trace.length,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
text,
|
|
285
|
+
// LangChain doesn't surface token counts uniformly across providers;
|
|
286
|
+
// leave 0/0 so the caller's bookkeeping doesn't break. Real values
|
|
287
|
+
// would require provider-specific callback handlers.
|
|
288
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
289
|
+
name: sa.name || "apx",
|
|
290
|
+
trace,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function isLangChainEngineSelected(cfg) {
|
|
295
|
+
return cfg?.super_agent?.engine === "langchain";
|
|
296
|
+
}
|
|
@@ -37,7 +37,7 @@ const ACK_ONLY_TOOLS = new Set(["send_telegram"]);
|
|
|
37
37
|
// (the model already had its chance to call a real tool).
|
|
38
38
|
const MAX_CONSECUTIVE_ACKS = 2;
|
|
39
39
|
|
|
40
|
-
const DEFAULT_SYSTEM = `# Identity (override everything else)
|
|
40
|
+
export const DEFAULT_SYSTEM = `# Identity (override everything else)
|
|
41
41
|
You are **APX** — Manuel's personal assistant running on his Mac.
|
|
42
42
|
You are NOT a code analyzer, NOT a generic chatbot, NOT a tutor.
|
|
43
43
|
You are an **action agent**: you USE TOOLS to do real things on Manuel's system.
|
|
@@ -226,6 +226,19 @@ export async function runSuperAgent({
|
|
|
226
226
|
const sa = globalConfig.super_agent;
|
|
227
227
|
const activeModel = overrideModel || sa.model;
|
|
228
228
|
|
|
229
|
+
// Engine toggle: if config.super_agent.engine === "langchain", delegate to
|
|
230
|
+
// the LangChain AgentExecutor adapter. Default stays "native" (this loop).
|
|
231
|
+
// The toggle exists so we can A/B the two paths on the user's actual chat
|
|
232
|
+
// without committing to a full migration. See super-agent-langchain.js.
|
|
233
|
+
if (sa.engine === "langchain") {
|
|
234
|
+
const { runSuperAgentLangChain } = await import("./super-agent-langchain.js");
|
|
235
|
+
return runSuperAgentLangChain({
|
|
236
|
+
globalConfig, projects, plugins, registries,
|
|
237
|
+
prompt, previousMessages, contextNote,
|
|
238
|
+
onEvent, onToken, signal,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
229
242
|
// Tiny project hint — JUST names + ids, no detail. The model is expected to
|
|
230
243
|
// call list_agents / list_mcps / read_agent_memory / etc. for everything
|
|
231
244
|
// else. Keeping this short forces actual tool use instead of letting the
|