@easynet/agent-runtime 1.0.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/.github/workflows/ci.yml +80 -0
- package/.github/workflows/release.yml +82 -0
- package/.releaserc.cjs +26 -0
- package/dist/cli.d.ts +43 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +617 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +86 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +84 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +104 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +111 -0
- package/dist/context.js.map +1 -0
- package/dist/deep-agent.d.ts +29 -0
- package/dist/deep-agent.d.ts.map +1 -0
- package/dist/deep-agent.js +77 -0
- package/dist/deep-agent.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/malformed-tool-call-middleware.d.ts +8 -0
- package/dist/malformed-tool-call-middleware.d.ts.map +1 -0
- package/dist/malformed-tool-call-middleware.js +191 -0
- package/dist/malformed-tool-call-middleware.js.map +1 -0
- package/dist/react-agent.d.ts +38 -0
- package/dist/react-agent.d.ts.map +1 -0
- package/dist/react-agent.js +465 -0
- package/dist/react-agent.js.map +1 -0
- package/dist/sub-agent.d.ts +34 -0
- package/dist/sub-agent.d.ts.map +1 -0
- package/dist/sub-agent.js +53 -0
- package/dist/sub-agent.js.map +1 -0
- package/example/basic-usage.ts +49 -0
- package/package.json +53 -0
- package/src/cli.ts +745 -0
- package/src/config.ts +177 -0
- package/src/context.ts +247 -0
- package/src/deep-agent.ts +104 -0
- package/src/index.ts +53 -0
- package/src/malformed-tool-call-middleware.ts +239 -0
- package/src/markdown-it-terminal.d.ts +4 -0
- package/src/marked-terminal.d.ts +16 -0
- package/src/react-agent.ts +576 -0
- package/src/sub-agent.ts +82 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware that recovers from malformed tool call errors produced by OSS LLMs.
|
|
3
|
+
*
|
|
4
|
+
* When models like Ollama-hosted open-source LLMs mix chain-of-thought reasoning
|
|
5
|
+
* into tool call JSON arguments, the server returns a 500 parsing error.
|
|
6
|
+
* This middleware catches those errors, extracts the valid JSON from the raw text,
|
|
7
|
+
* and constructs a proper AIMessage so the agent loop can continue.
|
|
8
|
+
*/
|
|
9
|
+
import { createMiddleware } from "langchain";
|
|
10
|
+
import { AIMessage } from "@langchain/core/messages";
|
|
11
|
+
|
|
12
|
+
export interface MalformedToolCallMiddlewareConfig {
|
|
13
|
+
/** Maximum retry attempts after JSON extraction fails. Default: 1 */
|
|
14
|
+
maxRetries?: number;
|
|
15
|
+
/** Log recovery attempts to stderr. Default: true */
|
|
16
|
+
verbose?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface OllamaToolCallError {
|
|
20
|
+
rawText: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ExtractedToolCall {
|
|
24
|
+
toolName: string;
|
|
25
|
+
args: Record<string, unknown>;
|
|
26
|
+
id?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Helpers
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Detect Ollama "500 error parsing tool call" errors and extract the raw text.
|
|
35
|
+
* Error format: `500 error parsing tool call: raw='<raw_text>', err=invalid character...`
|
|
36
|
+
*/
|
|
37
|
+
function parseOllamaToolCallError(err: Error): OllamaToolCallError | null {
|
|
38
|
+
const msg = err.message;
|
|
39
|
+
const rawMatch = msg.match(/error parsing tool call.*?raw='(.+?)',\s*err=/s);
|
|
40
|
+
if (!rawMatch) return null;
|
|
41
|
+
return { rawText: rawMatch[1] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract valid JSON object from a string that may contain leading non-JSON text.
|
|
46
|
+
* Uses brace-depth counting to find matching `{...}` blocks.
|
|
47
|
+
*/
|
|
48
|
+
function extractJsonObject(text: string): Record<string, unknown> | null {
|
|
49
|
+
for (let i = 0; i < text.length; i++) {
|
|
50
|
+
if (text[i] !== "{") continue;
|
|
51
|
+
|
|
52
|
+
let depth = 0;
|
|
53
|
+
let inString = false;
|
|
54
|
+
let escape = false;
|
|
55
|
+
let end = -1;
|
|
56
|
+
|
|
57
|
+
for (let j = i; j < text.length; j++) {
|
|
58
|
+
const ch = text[j];
|
|
59
|
+
if (escape) {
|
|
60
|
+
escape = false;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (ch === "\\") {
|
|
64
|
+
escape = true;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (ch === '"') {
|
|
68
|
+
inString = !inString;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (inString) continue;
|
|
72
|
+
if (ch === "{") depth++;
|
|
73
|
+
else if (ch === "}") {
|
|
74
|
+
depth--;
|
|
75
|
+
if (depth === 0) {
|
|
76
|
+
end = j;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (end === -1) continue;
|
|
82
|
+
|
|
83
|
+
const candidate = text.slice(i, end + 1);
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(candidate) as unknown;
|
|
86
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
87
|
+
return parsed as Record<string, unknown>;
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// not valid JSON from this position, try next '{'
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Match extracted JSON keys against available tool schemas to infer the tool name.
|
|
98
|
+
*/
|
|
99
|
+
function matchToolByArgs(
|
|
100
|
+
args: Record<string, unknown>,
|
|
101
|
+
tools: ReadonlyArray<{ name: string; schema?: unknown }>,
|
|
102
|
+
): string | null {
|
|
103
|
+
const argKeys = new Set(Object.keys(args));
|
|
104
|
+
let bestMatch: string | null = null;
|
|
105
|
+
let bestScore = 0;
|
|
106
|
+
|
|
107
|
+
for (const tool of tools) {
|
|
108
|
+
const schema = tool.schema as
|
|
109
|
+
| { properties?: Record<string, unknown> }
|
|
110
|
+
| { shape?: Record<string, unknown> }
|
|
111
|
+
| null
|
|
112
|
+
| undefined;
|
|
113
|
+
const propKeys =
|
|
114
|
+
(schema && "properties" in schema && schema.properties
|
|
115
|
+
? Object.keys(schema.properties)
|
|
116
|
+
: schema && "shape" in schema && schema.shape
|
|
117
|
+
? Object.keys(schema.shape)
|
|
118
|
+
: null);
|
|
119
|
+
if (!propKeys) continue;
|
|
120
|
+
|
|
121
|
+
let score = 0;
|
|
122
|
+
for (const key of argKeys) {
|
|
123
|
+
if (propKeys.includes(key)) score++;
|
|
124
|
+
}
|
|
125
|
+
if (score > bestScore) {
|
|
126
|
+
bestScore = score;
|
|
127
|
+
bestMatch = tool.name;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return bestMatch;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function generateToolCallId(): string {
|
|
134
|
+
return `call_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Middleware
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
export function malformedToolCallMiddleware(
|
|
142
|
+
config?: MalformedToolCallMiddlewareConfig,
|
|
143
|
+
) {
|
|
144
|
+
const maxRetries = config?.maxRetries ?? 1;
|
|
145
|
+
const verbose = config?.verbose ?? true;
|
|
146
|
+
|
|
147
|
+
const log = verbose
|
|
148
|
+
? (msg: string) => console.error(`[malformedToolCallRecovery] ${msg}`)
|
|
149
|
+
: () => {};
|
|
150
|
+
|
|
151
|
+
function tryExtract(
|
|
152
|
+
rawText: string,
|
|
153
|
+
tools: ReadonlyArray<{ name: string; schema?: unknown }>,
|
|
154
|
+
): ExtractedToolCall | null {
|
|
155
|
+
const args = extractJsonObject(rawText);
|
|
156
|
+
if (!args) return null;
|
|
157
|
+
|
|
158
|
+
const toolName =
|
|
159
|
+
matchToolByArgs(args, tools) ?? tools[0]?.name ?? "unknown_tool";
|
|
160
|
+
return { toolName, args, id: generateToolCallId() };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function buildAIMessage(tc: ExtractedToolCall): AIMessage {
|
|
164
|
+
return new AIMessage({
|
|
165
|
+
content: "",
|
|
166
|
+
tool_calls: [{ name: tc.toolName, args: tc.args, id: tc.id }],
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return createMiddleware({
|
|
171
|
+
name: "malformedToolCallRecovery",
|
|
172
|
+
|
|
173
|
+
wrapModelCall: async (request, handler) => {
|
|
174
|
+
// --- Normal call ---
|
|
175
|
+
try {
|
|
176
|
+
return await handler(request);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
179
|
+
const parsed = parseOllamaToolCallError(err);
|
|
180
|
+
if (!parsed) throw err; // not our error type
|
|
181
|
+
|
|
182
|
+
log("Caught malformed tool call error. Attempting JSON extraction...");
|
|
183
|
+
|
|
184
|
+
// Cast tools to access name/schema for matching
|
|
185
|
+
const req = request as unknown as Record<string, unknown>;
|
|
186
|
+
const tools = (req.tools ?? []) as ReadonlyArray<{
|
|
187
|
+
name: string;
|
|
188
|
+
schema?: unknown;
|
|
189
|
+
}>;
|
|
190
|
+
const systemPrompt = req.systemPrompt as string | undefined;
|
|
191
|
+
|
|
192
|
+
// --- Strategy A: extract JSON directly from the error text ---
|
|
193
|
+
const extracted = tryExtract(parsed.rawText, tools);
|
|
194
|
+
if (extracted) {
|
|
195
|
+
log(`Extracted JSON for tool "${extracted.toolName}".`);
|
|
196
|
+
return buildAIMessage(extracted);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// --- Strategy B: retry with enhanced prompt ---
|
|
200
|
+
log(`JSON extraction failed. Retrying (${maxRetries} attempts)...`);
|
|
201
|
+
|
|
202
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
203
|
+
try {
|
|
204
|
+
const retryRequest = {
|
|
205
|
+
...request,
|
|
206
|
+
systemPrompt:
|
|
207
|
+
(systemPrompt ?? "") +
|
|
208
|
+
"\n\nCRITICAL: Tool call arguments MUST be a single valid JSON object. " +
|
|
209
|
+
"Do NOT include any reasoning text before or after the JSON.",
|
|
210
|
+
};
|
|
211
|
+
return await handler(retryRequest);
|
|
212
|
+
} catch (retryError) {
|
|
213
|
+
const retryErr =
|
|
214
|
+
retryError instanceof Error
|
|
215
|
+
? retryError
|
|
216
|
+
: new Error(String(retryError));
|
|
217
|
+
const retryParsed = parseOllamaToolCallError(retryErr);
|
|
218
|
+
if (retryParsed) {
|
|
219
|
+
const retryExtracted = tryExtract(retryParsed.rawText, tools);
|
|
220
|
+
if (retryExtracted) {
|
|
221
|
+
log(`Extracted JSON on retry ${attempt + 1} for tool "${retryExtracted.toolName}".`);
|
|
222
|
+
return buildAIMessage(retryExtracted);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
log(`Retry ${attempt + 1}/${maxRetries} failed.`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// --- All strategies exhausted ---
|
|
230
|
+
log("All recovery attempts failed. Returning error to agent loop.");
|
|
231
|
+
return new AIMessage({
|
|
232
|
+
content:
|
|
233
|
+
"Tool call failed due to malformed output from the model. " +
|
|
234
|
+
"Please try a simpler command or rephrase your request.",
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
declare module "marked-terminal" {
|
|
2
|
+
import type { MarkedExtension } from "marked";
|
|
3
|
+
|
|
4
|
+
export interface MarkedTerminalOptions {
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface HighlightOptions {
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function markedTerminal(
|
|
13
|
+
options?: MarkedTerminalOptions,
|
|
14
|
+
highlightOptions?: HighlightOptions,
|
|
15
|
+
): MarkedExtension;
|
|
16
|
+
}
|