@mcpmesh/sdk 2.2.4 → 2.4.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/dist/__tests__/jobs.spec.js +231 -1
- package/dist/__tests__/jobs.spec.js.map +1 -1
- package/dist/__tests__/llm-agent-model-params.test.js +83 -0
- package/dist/__tests__/llm-agent-model-params.test.js.map +1 -1
- package/dist/__tests__/llm-max-iterations.test.d.ts +20 -0
- package/dist/__tests__/llm-max-iterations.test.d.ts.map +1 -0
- package/dist/__tests__/llm-max-iterations.test.js +250 -0
- package/dist/__tests__/llm-max-iterations.test.js.map +1 -0
- package/dist/__tests__/llm-mesh-error-mapping.test.d.ts +16 -0
- package/dist/__tests__/llm-mesh-error-mapping.test.d.ts.map +1 -0
- package/dist/__tests__/llm-mesh-error-mapping.test.js +135 -0
- package/dist/__tests__/llm-mesh-error-mapping.test.js.map +1 -0
- package/dist/__tests__/llm-provider-output-mode.test.d.ts +21 -0
- package/dist/__tests__/llm-provider-output-mode.test.d.ts.map +1 -0
- package/dist/__tests__/llm-provider-output-mode.test.js +115 -0
- package/dist/__tests__/llm-provider-output-mode.test.js.map +1 -0
- package/dist/__tests__/llm-provider-system-synthesis.test.d.ts +20 -0
- package/dist/__tests__/llm-provider-system-synthesis.test.d.ts.map +1 -0
- package/dist/__tests__/llm-provider-system-synthesis.test.js +167 -0
- package/dist/__tests__/llm-provider-system-synthesis.test.js.map +1 -0
- package/dist/__tests__/llm-response-model.test.d.ts +10 -0
- package/dist/__tests__/llm-response-model.test.d.ts.map +1 -0
- package/dist/__tests__/llm-response-model.test.js +92 -0
- package/dist/__tests__/llm-response-model.test.js.map +1 -0
- package/dist/__tests__/proxy-timeout-guard.test.d.ts +12 -0
- package/dist/__tests__/proxy-timeout-guard.test.d.ts.map +1 -0
- package/dist/__tests__/proxy-timeout-guard.test.js +85 -0
- package/dist/__tests__/proxy-timeout-guard.test.js.map +1 -0
- package/dist/__tests__/registry-disconnect-retains-deps.spec.d.ts +2 -0
- package/dist/__tests__/registry-disconnect-retains-deps.spec.d.ts.map +1 -0
- package/dist/__tests__/registry-disconnect-retains-deps.spec.js +101 -0
- package/dist/__tests__/registry-disconnect-retains-deps.spec.js.map +1 -0
- package/dist/__tests__/response-parser.test.js +29 -0
- package/dist/__tests__/response-parser.test.js.map +1 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +4 -0
- package/dist/agent.js.map +1 -1
- package/dist/api-runtime.d.ts.map +1 -1
- package/dist/api-runtime.js +8 -1
- package/dist/api-runtime.js.map +1 -1
- package/dist/express.d.ts.map +1 -1
- package/dist/express.js +8 -1
- package/dist/express.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/jobs.d.ts +191 -0
- package/dist/jobs.d.ts.map +1 -1
- package/dist/jobs.js +198 -1
- package/dist/jobs.js.map +1 -1
- package/dist/llm-agent.d.ts +34 -0
- package/dist/llm-agent.d.ts.map +1 -1
- package/dist/llm-agent.js +239 -434
- package/dist/llm-agent.js.map +1 -1
- package/dist/llm-provider.d.ts +33 -4
- package/dist/llm-provider.d.ts.map +1 -1
- package/dist/llm-provider.js +91 -4
- package/dist/llm-provider.js.map +1 -1
- package/dist/llm.d.ts +1 -1
- package/dist/llm.d.ts.map +1 -1
- package/dist/llm.js +8 -5
- package/dist/llm.js.map +1 -1
- package/dist/provider-handlers/gemini-handler.d.ts.map +1 -1
- package/dist/provider-handlers/gemini-handler.js +2 -14
- package/dist/provider-handlers/gemini-handler.js.map +1 -1
- package/dist/provider-handlers/openai-handler.d.ts.map +1 -1
- package/dist/provider-handlers/openai-handler.js +2 -15
- package/dist/provider-handlers/openai-handler.js.map +1 -1
- package/dist/provider-handlers/provider-handler.d.ts +12 -0
- package/dist/provider-handlers/provider-handler.d.ts.map +1 -1
- package/dist/provider-handlers/provider-handler.js +24 -0
- package/dist/provider-handlers/provider-handler.js.map +1 -1
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +189 -254
- package/dist/proxy.js.map +1 -1
- package/dist/response-parser.d.ts +10 -0
- package/dist/response-parser.d.ts.map +1 -1
- package/dist/response-parser.js +55 -0
- package/dist/response-parser.js.map +1 -1
- package/dist/tracing.d.ts +12 -0
- package/dist/tracing.d.ts.map +1 -1
- package/dist/tracing.js +37 -0
- package/dist/tracing.js.map +1 -1
- package/dist/types.d.ts +10 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/llm-agent.js
CHANGED
|
@@ -33,12 +33,10 @@ import { zodToJsonSchema } from "zod-to-json-schema";
|
|
|
33
33
|
import { renderTemplate } from "./template.js";
|
|
34
34
|
import { ResponseParser } from "./response-parser.js";
|
|
35
35
|
import { MaxIterationsError, LLMAPIError, ToolExecutionError, } from "./errors.js";
|
|
36
|
-
import { parseSSEResponse } from "./sse.js";
|
|
37
36
|
import { resolveMediaInputs } from "./media/index.js";
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import { getDispatcher } from "./http-pool.js";
|
|
37
|
+
import { callMcpTool, streamMcpTool, DEFAULT_CALL_OPTIONS, } from "./proxy.js";
|
|
38
|
+
import { isTimeoutError } from "./timeout-utils.js";
|
|
39
|
+
import { envMaxIterations, sanitizeMaxIterations } from "./llm-provider.js";
|
|
42
40
|
/**
|
|
43
41
|
* Mesh provider that delegates to an LLM provider discovered via mesh.
|
|
44
42
|
*/
|
|
@@ -51,8 +49,16 @@ export class MeshDelegatedProvider {
|
|
|
51
49
|
this.functionName = functionName;
|
|
52
50
|
this.parallelToolCalls = parallelToolCalls;
|
|
53
51
|
}
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Build the MeshLlmRequest body shared by complete() and streamComplete().
|
|
54
|
+
*
|
|
55
|
+
* Assembles model_params (with the escape-hatch merge + typed overrides),
|
|
56
|
+
* wraps messages/tools into the MeshLlmRequest, and returns it pre-wrapped
|
|
57
|
+
* in the ``{ request }`` arguments object. Callers inject trace context /
|
|
58
|
+
* propagated headers into ``args`` afterward (per-caller — complete() uses
|
|
59
|
+
* injectTraceAndHeaders, streamComplete() lets streamMcpTool() handle it).
|
|
60
|
+
*/
|
|
61
|
+
buildMeshLlmRequest(model, messages, tools, options) {
|
|
56
62
|
const modelParams = {};
|
|
57
63
|
// Escape-hatch merge: callers can pass vendor-specific kwargs
|
|
58
64
|
// (e.g., thinking_config, output_config) via options.modelParams.
|
|
@@ -84,6 +90,18 @@ export class MeshDelegatedProvider {
|
|
|
84
90
|
if (this.parallelToolCalls) {
|
|
85
91
|
modelParams.parallel_tool_calls = true;
|
|
86
92
|
}
|
|
93
|
+
// Issue #1116: forward the provider-managed loop cap. Typed field, so it
|
|
94
|
+
// takes precedence over any escape-hatch modelParams.max_iterations above.
|
|
95
|
+
if (options?.maxIterations !== undefined) {
|
|
96
|
+
modelParams.max_iterations = options.maxIterations;
|
|
97
|
+
}
|
|
98
|
+
// Issue #1112: forward the consumer-supplied output_mode override ONLY when
|
|
99
|
+
// the user explicitly set it (undefined = auto = provider's per-vendor
|
|
100
|
+
// selection; omitting keeps the provider byte-identical to today). Typed
|
|
101
|
+
// field, so it takes precedence over any escape-hatch modelParams.output_mode.
|
|
102
|
+
if (options?.outputMode !== undefined) {
|
|
103
|
+
modelParams.output_mode = options.outputMode;
|
|
104
|
+
}
|
|
87
105
|
const request = {
|
|
88
106
|
messages,
|
|
89
107
|
};
|
|
@@ -96,152 +114,82 @@ export class MeshDelegatedProvider {
|
|
|
96
114
|
}
|
|
97
115
|
// Wrap in "request" parameter as expected by Python claude_provider
|
|
98
116
|
const args = { request };
|
|
117
|
+
return { request, args };
|
|
118
|
+
}
|
|
119
|
+
async complete(model, messages, tools, options) {
|
|
120
|
+
// Build MeshLlmRequest structure (matches Python claude_provider schema).
|
|
121
|
+
// The {request} wrapper stays; callMcpTool injects trace context internally.
|
|
122
|
+
const { args } = this.buildMeshLlmRequest(model, messages, tools, options);
|
|
99
123
|
// Set up timeout (default 300s to match Python SDK's stream_timeout)
|
|
100
124
|
const timeoutMs = parseInt(process.env.MESH_PROVIDER_TIMEOUT_MS || "300000", 10);
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
// Fallback to manual injection
|
|
117
|
-
args._trace_id = traceCtx.traceId;
|
|
118
|
-
args._parent_span = traceSpanId;
|
|
119
|
-
if (Object.keys(delegatedPropHeaders).length > 0) {
|
|
120
|
-
args._mesh_headers = { ...delegatedPropHeaders };
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
else if (Object.keys(delegatedPropHeaders).length > 0) {
|
|
125
|
-
args._mesh_headers = { ...delegatedPropHeaders };
|
|
126
|
-
}
|
|
127
|
-
let traceSuccess = true;
|
|
128
|
-
let traceError = null;
|
|
125
|
+
// Route through the shared callMcpTool (trace injection, span publish,
|
|
126
|
+
// dispatcher pooling, job-cancel wiring all handled internally). LLM calls
|
|
127
|
+
// are expensive / non-idempotent so disable retries; LLM responses can
|
|
128
|
+
// exceed the default 10 MiB cap so raise it to effectively unbounded.
|
|
129
|
+
const callOptions = {
|
|
130
|
+
...DEFAULT_CALL_OPTIONS,
|
|
131
|
+
timeout: timeoutMs,
|
|
132
|
+
maxAttempts: 1,
|
|
133
|
+
maxResponseSize: Number.MAX_SAFE_INTEGER,
|
|
134
|
+
};
|
|
135
|
+
// Call the mesh provider via MCP. callMcpTool throws a plain Error on
|
|
136
|
+
// failure — re-wrap into LLMAPIError to preserve the public error type.
|
|
137
|
+
let content;
|
|
129
138
|
try {
|
|
130
|
-
|
|
131
|
-
let response;
|
|
132
|
-
try {
|
|
133
|
-
response = await fetchWithTimeout(`${this.endpoint}/mcp`, {
|
|
134
|
-
method: "POST",
|
|
135
|
-
headers: {
|
|
136
|
-
"Content-Type": "application/json",
|
|
137
|
-
"Accept": "application/json, text/event-stream",
|
|
138
|
-
...(traceCtx && traceSpanId ? createTraceHeaders(traceCtx.traceId, traceSpanId) : {}),
|
|
139
|
-
...getCurrentPropagatedHeaders(),
|
|
140
|
-
},
|
|
141
|
-
body: JSON.stringify({
|
|
142
|
-
jsonrpc: "2.0",
|
|
143
|
-
id: Date.now(),
|
|
144
|
-
method: "tools/call",
|
|
145
|
-
params: {
|
|
146
|
-
name: this.functionName,
|
|
147
|
-
arguments: args,
|
|
148
|
-
},
|
|
149
|
-
}),
|
|
150
|
-
timeout: timeoutMs,
|
|
151
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
152
|
-
dispatcher: getDispatcher(`${this.endpoint}/mcp`),
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
catch (error) {
|
|
156
|
-
if (isTimeoutError(error)) {
|
|
157
|
-
throw new LLMAPIError(408, `Request timed out after ${timeoutMs}ms`, `mesh:${this.endpoint}`);
|
|
158
|
-
}
|
|
159
|
-
throw new LLMAPIError(0, `Fetch failed: ${error instanceof Error ? error.message : String(error)}`, `mesh:${this.endpoint}`);
|
|
160
|
-
}
|
|
161
|
-
if (!response.ok) {
|
|
162
|
-
const error = await response.text();
|
|
163
|
-
throw new LLMAPIError(response.status, error, `mesh:${this.endpoint}`);
|
|
164
|
-
}
|
|
165
|
-
// Handle SSE response from FastMCP stateless HTTP stream
|
|
166
|
-
const responseText = await response.text();
|
|
167
|
-
const result = parseSSEResponse(responseText);
|
|
168
|
-
if (result.error) {
|
|
169
|
-
throw new Error(`Mesh provider RPC error: ${result.error.message}`);
|
|
170
|
-
}
|
|
171
|
-
// Parse the MCP result content
|
|
172
|
-
const content = result.result?.content?.[0];
|
|
173
|
-
if (!content || content.type !== "text") {
|
|
174
|
-
throw new Error("Invalid response from mesh provider");
|
|
175
|
-
}
|
|
176
|
-
// Check for MCP tool execution error (isError flag in result)
|
|
177
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
178
|
-
if (result.result?.isError) {
|
|
179
|
-
throw new Error(`Mesh provider tool error: ${content.text}`);
|
|
180
|
-
}
|
|
181
|
-
// Parse the LLM provider response
|
|
182
|
-
// Format: { role, content, tool_calls?, _mesh_usage? }
|
|
183
|
-
const meshResponse = JSON.parse(content.text);
|
|
184
|
-
// Validate role - LLM responses should always be "assistant"
|
|
185
|
-
let validatedRole = "assistant";
|
|
186
|
-
if (meshResponse.role !== "assistant") {
|
|
187
|
-
console.warn(`[mesh.llm] Unexpected role "${meshResponse.role}" from mesh provider, defaulting to "assistant"`);
|
|
188
|
-
}
|
|
189
|
-
// Convert to OpenAI format expected by MeshLlmAgent
|
|
190
|
-
const openAiResponse = {
|
|
191
|
-
id: `mesh-${Date.now()}`,
|
|
192
|
-
object: "chat.completion",
|
|
193
|
-
created: Math.floor(Date.now() / 1000),
|
|
194
|
-
model: "mesh-delegated",
|
|
195
|
-
choices: [
|
|
196
|
-
{
|
|
197
|
-
index: 0,
|
|
198
|
-
message: {
|
|
199
|
-
role: validatedRole,
|
|
200
|
-
content: meshResponse.content,
|
|
201
|
-
tool_calls: meshResponse.tool_calls,
|
|
202
|
-
},
|
|
203
|
-
finish_reason: meshResponse.tool_calls ? "tool_calls" : "stop",
|
|
204
|
-
},
|
|
205
|
-
],
|
|
206
|
-
usage: meshResponse._mesh_usage
|
|
207
|
-
? {
|
|
208
|
-
prompt_tokens: meshResponse._mesh_usage.prompt_tokens,
|
|
209
|
-
completion_tokens: meshResponse._mesh_usage.completion_tokens,
|
|
210
|
-
total_tokens: meshResponse._mesh_usage.prompt_tokens +
|
|
211
|
-
meshResponse._mesh_usage.completion_tokens,
|
|
212
|
-
}
|
|
213
|
-
: undefined,
|
|
214
|
-
};
|
|
215
|
-
return openAiResponse;
|
|
139
|
+
content = await callMcpTool(this.endpoint, this.functionName, args, callOptions, "mesh-llm");
|
|
216
140
|
}
|
|
217
141
|
catch (err) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
142
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
143
|
+
// callMcpTool catches the AbortError and re-throws a plain Error whose
|
|
144
|
+
// message is "MCP call timed out after <N>ms", so isTimeoutError (name ===
|
|
145
|
+
// "AbortError") no longer matches here — also detect the message.
|
|
146
|
+
const isTimeout = isTimeoutError(err) ||
|
|
147
|
+
(err instanceof Error && /timed out/i.test(err.message));
|
|
148
|
+
if (isTimeout) {
|
|
149
|
+
throw new LLMAPIError(408, message, `mesh:${this.endpoint}`);
|
|
150
|
+
}
|
|
151
|
+
throw new LLMAPIError(0, message, `mesh:${this.endpoint}`);
|
|
152
|
+
}
|
|
153
|
+
// The mesh provider returns a single text content item whose text is the
|
|
154
|
+
// LLM provider response JSON. callMcpTool joins/extracts it to a string.
|
|
155
|
+
if (typeof content !== "string") {
|
|
156
|
+
throw new Error("Invalid response from mesh provider");
|
|
157
|
+
}
|
|
158
|
+
// Parse the LLM provider response
|
|
159
|
+
// Format: { role, content, tool_calls?, _mesh_usage? }
|
|
160
|
+
const meshResponse = JSON.parse(content);
|
|
161
|
+
// Validate role - LLM responses should always be "assistant"
|
|
162
|
+
let validatedRole = "assistant";
|
|
163
|
+
if (meshResponse.role !== "assistant") {
|
|
164
|
+
console.warn(`[mesh.llm] Unexpected role "${meshResponse.role}" from mesh provider, defaulting to "assistant"`);
|
|
165
|
+
}
|
|
166
|
+
// Convert to OpenAI format expected by MeshLlmAgent
|
|
167
|
+
const openAiResponse = {
|
|
168
|
+
id: `mesh-${Date.now()}`,
|
|
169
|
+
object: "chat.completion",
|
|
170
|
+
created: Math.floor(Date.now() / 1000),
|
|
171
|
+
model: "mesh-delegated",
|
|
172
|
+
choices: [
|
|
173
|
+
{
|
|
174
|
+
index: 0,
|
|
175
|
+
message: {
|
|
176
|
+
role: validatedRole,
|
|
177
|
+
content: meshResponse.content,
|
|
178
|
+
tool_calls: meshResponse.tool_calls,
|
|
179
|
+
},
|
|
180
|
+
finish_reason: meshResponse.tool_calls ? "tool_calls" : "stop",
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
usage: meshResponse._mesh_usage
|
|
184
|
+
? {
|
|
185
|
+
prompt_tokens: meshResponse._mesh_usage.prompt_tokens,
|
|
186
|
+
completion_tokens: meshResponse._mesh_usage.completion_tokens,
|
|
187
|
+
total_tokens: meshResponse._mesh_usage.prompt_tokens +
|
|
188
|
+
meshResponse._mesh_usage.completion_tokens,
|
|
189
|
+
}
|
|
190
|
+
: undefined,
|
|
191
|
+
};
|
|
192
|
+
return openAiResponse;
|
|
245
193
|
}
|
|
246
194
|
/**
|
|
247
195
|
* Stream chunks from the mesh-delegated provider's streaming variant.
|
|
@@ -257,40 +205,8 @@ export class MeshDelegatedProvider {
|
|
|
257
205
|
* ``ai.mcpmesh.stream`` tag opt-in (see ``MeshLlmAgent.stream()``).
|
|
258
206
|
*/
|
|
259
207
|
async *streamComplete(model, messages, tools, options) {
|
|
260
|
-
// Build MeshLlmRequest body — same shape as complete()
|
|
261
|
-
const
|
|
262
|
-
// Escape-hatch merge: callers can pass vendor-specific kwargs
|
|
263
|
-
// (e.g., thinking_config, output_config) via options.modelParams.
|
|
264
|
-
// Merged FIRST so typed fields below take precedence on collision.
|
|
265
|
-
if (options?.modelParams) {
|
|
266
|
-
Object.assign(modelParams, options.modelParams);
|
|
267
|
-
}
|
|
268
|
-
if (model && model !== "default") {
|
|
269
|
-
modelParams.model = model;
|
|
270
|
-
}
|
|
271
|
-
if (options?.maxOutputTokens)
|
|
272
|
-
modelParams.max_tokens = options.maxOutputTokens;
|
|
273
|
-
if (options?.temperature !== undefined)
|
|
274
|
-
modelParams.temperature = options.temperature;
|
|
275
|
-
if (options?.topP !== undefined)
|
|
276
|
-
modelParams.top_p = options.topP;
|
|
277
|
-
if (options?.stop)
|
|
278
|
-
modelParams.stop = options.stop;
|
|
279
|
-
if (options?.outputSchema) {
|
|
280
|
-
modelParams.output_schema = options.outputSchema.schema;
|
|
281
|
-
modelParams.output_type_name = options.outputSchema.name;
|
|
282
|
-
}
|
|
283
|
-
if (this.parallelToolCalls) {
|
|
284
|
-
modelParams.parallel_tool_calls = true;
|
|
285
|
-
}
|
|
286
|
-
const request = { messages };
|
|
287
|
-
if (Object.keys(modelParams).length > 0) {
|
|
288
|
-
request.model_params = modelParams;
|
|
289
|
-
}
|
|
290
|
-
if (tools && tools.length > 0) {
|
|
291
|
-
request.tools = tools;
|
|
292
|
-
}
|
|
293
|
-
const args = { request };
|
|
208
|
+
// Build MeshLlmRequest body — same shape as complete().
|
|
209
|
+
const { args } = this.buildMeshLlmRequest(model, messages, tools, options);
|
|
294
210
|
// streamMcpTool() handles trace context injection / propagated headers /
|
|
295
211
|
// dispatcher pooling internally — same path as createProxy().stream().
|
|
296
212
|
// Match complete()'s env-backed timeout (MESH_PROVIDER_TIMEOUT_MS) so
|
|
@@ -314,10 +230,36 @@ export class MeshLlmAgent {
|
|
|
314
230
|
_meta = null;
|
|
315
231
|
_systemPromptOverride = null;
|
|
316
232
|
_parallelLogEmitted = false;
|
|
233
|
+
// Cached output schema derived from the immutable returnSchema (Issue #459).
|
|
234
|
+
// Computed once: `null` means "not yet computed", an object holds the result
|
|
235
|
+
// (which may itself be `undefined` when conversion failed).
|
|
236
|
+
_outputSchema = null;
|
|
237
|
+
_outputSchemaSection = null;
|
|
317
238
|
constructor(config) {
|
|
318
239
|
this.config = config;
|
|
319
240
|
this.responseParser = new ResponseParser(config.returnSchema);
|
|
320
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Build (once) the provider output schema from the immutable returnSchema.
|
|
244
|
+
* Returns `undefined` when there is no schema or conversion failed.
|
|
245
|
+
*/
|
|
246
|
+
getOutputSchema() {
|
|
247
|
+
if (this._outputSchema !== null)
|
|
248
|
+
return this._outputSchema;
|
|
249
|
+
let result;
|
|
250
|
+
if (this.config.returnSchema) {
|
|
251
|
+
try {
|
|
252
|
+
const jsonSchema = zodToJsonSchema(this.config.returnSchema);
|
|
253
|
+
const schemaName = jsonSchema.title ?? "Response";
|
|
254
|
+
result = { schema: jsonSchema, name: schemaName };
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// If schema conversion fails, skip
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
this._outputSchema = result;
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
321
263
|
/**
|
|
322
264
|
* Get metadata from the last run.
|
|
323
265
|
*/
|
|
@@ -337,28 +279,20 @@ export class MeshLlmAgent {
|
|
|
337
279
|
return this._systemPromptOverride ?? this.config.systemPrompt;
|
|
338
280
|
}
|
|
339
281
|
/**
|
|
340
|
-
*
|
|
282
|
+
* Build the initial LlmMessage[] shared by run() and stream():
|
|
283
|
+
* render the system prompt (+ tool schema injection), optionally append the
|
|
284
|
+
* output-schema hint, resolve media inputs, and unwind multi-turn history
|
|
285
|
+
* (attaching resolved media to the last user message).
|
|
341
286
|
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
287
|
+
* The ONLY behavioral knob is opts.includeOutputSchemaHint:
|
|
288
|
+
* - run() passes `!meshDelegated` (consumer-side schema hint when not delegated).
|
|
289
|
+
* - stream() passes `false` (always mesh-delegated; provider applies formatting).
|
|
345
290
|
*/
|
|
346
|
-
async
|
|
347
|
-
if (this.config.parallelToolCalls && !this._parallelLogEmitted) {
|
|
348
|
-
console.log("[mesh.llm] parallel tool calls enabled — tools will execute concurrently via Promise.all()");
|
|
349
|
-
this._parallelLogEmitted = true;
|
|
350
|
-
}
|
|
351
|
-
const startTime = Date.now();
|
|
352
|
-
const toolCalls = [];
|
|
353
|
-
let totalInputTokens = 0;
|
|
354
|
-
let totalOutputTokens = 0;
|
|
355
|
-
// Resolve provider
|
|
356
|
-
const provider = this.resolveProvider(context);
|
|
357
|
-
// Build initial messages
|
|
291
|
+
async buildAgentMessages(messageInput, context, opts) {
|
|
358
292
|
const messages = [];
|
|
359
|
-
// Build tool definitions first (needed for schema injection)
|
|
293
|
+
// Build tool definitions first (needed for schema injection).
|
|
360
294
|
// When using mesh delegation, enrich tools with endpoint URLs
|
|
361
|
-
// so the provider can execute tools directly via MCP proxies
|
|
295
|
+
// so the provider can execute tools directly via MCP proxies.
|
|
362
296
|
const isMeshDelegated = !!context.meshProvider;
|
|
363
297
|
const toolDefs = this.buildToolDefinitions(context.tools, isMeshDelegated);
|
|
364
298
|
// Add system prompt if configured
|
|
@@ -375,7 +309,7 @@ export class MeshLlmAgent {
|
|
|
375
309
|
// formatting via output_schema in model_params. Consumer doesn't know
|
|
376
310
|
// the provider's vendor, so it must not add vendor-agnostic schema instructions.
|
|
377
311
|
const outputMode = this.config.outputMode ?? "hint";
|
|
378
|
-
if (
|
|
312
|
+
if (opts.includeOutputSchemaHint && outputMode !== "text" && this.config.returnSchema) {
|
|
379
313
|
const outputSchemaSection = this.buildOutputSchemaSection();
|
|
380
314
|
systemContent += outputSchemaSection;
|
|
381
315
|
}
|
|
@@ -426,11 +360,41 @@ export class MeshLlmAgent {
|
|
|
426
360
|
}
|
|
427
361
|
}
|
|
428
362
|
}
|
|
363
|
+
return messages;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Run the agentic loop.
|
|
367
|
+
*
|
|
368
|
+
* @param messageInput - User message string or multi-turn message array
|
|
369
|
+
* @param context - Runtime context with tools and options
|
|
370
|
+
* @returns Parsed response (validated if schema provided)
|
|
371
|
+
*/
|
|
372
|
+
async run(messageInput, context) {
|
|
373
|
+
if (this.config.parallelToolCalls && !this._parallelLogEmitted) {
|
|
374
|
+
console.log("[mesh.llm] parallel tool calls enabled — tools will execute concurrently via Promise.all()");
|
|
375
|
+
this._parallelLogEmitted = true;
|
|
376
|
+
}
|
|
377
|
+
const startTime = Date.now();
|
|
378
|
+
const toolCalls = [];
|
|
379
|
+
let totalInputTokens = 0;
|
|
380
|
+
let totalOutputTokens = 0;
|
|
381
|
+
// Resolve provider
|
|
382
|
+
const provider = this.resolveProvider(context);
|
|
383
|
+
// Build tool definitions (needed for schema injection + the agentic loop).
|
|
384
|
+
// When using mesh delegation, enrich tools with endpoint URLs
|
|
385
|
+
// so the provider can execute tools directly via MCP proxies.
|
|
386
|
+
const isMeshDelegated = !!context.meshProvider;
|
|
387
|
+
const toolDefs = this.buildToolDefinitions(context.tools, isMeshDelegated);
|
|
388
|
+
// Build initial messages (system prompt + tool schema + output-schema hint
|
|
389
|
+
// + resolved media + multi-turn unwinding). run() includes the output-schema
|
|
390
|
+
// hint only when NOT mesh-delegated.
|
|
391
|
+
const messages = await this.buildAgentMessages(messageInput, context, {
|
|
392
|
+
includeOutputSchemaHint: !isMeshDelegated,
|
|
393
|
+
});
|
|
429
394
|
// Get effective options (runtime options > MESH_LLM_* env > config)
|
|
430
|
-
const maxIterations = context.options?.maxIterations ??
|
|
431
|
-
(
|
|
432
|
-
|
|
433
|
-
: this.config.maxIterations);
|
|
395
|
+
const maxIterations = sanitizeMaxIterations(context.options?.maxIterations) ??
|
|
396
|
+
envMaxIterations() ??
|
|
397
|
+
this.config.maxIterations;
|
|
434
398
|
const maxTokens = context.options?.maxOutputTokens ?? this.config.maxOutputTokens;
|
|
435
399
|
const temperature = context.options?.temperature ?? this.config.temperature;
|
|
436
400
|
// Determine model (mesh provider > MESH_LLM_MODEL env > config > default)
|
|
@@ -438,19 +402,8 @@ export class MeshLlmAgent {
|
|
|
438
402
|
process.env.MESH_LLM_MODEL ??
|
|
439
403
|
this.config.model ??
|
|
440
404
|
this.getDefaultModel();
|
|
441
|
-
// Build output schema for provider (Issue #459) - computed once
|
|
442
|
-
|
|
443
|
-
if (this.config.returnSchema) {
|
|
444
|
-
try {
|
|
445
|
-
const jsonSchema = zodToJsonSchema(this.config.returnSchema);
|
|
446
|
-
// Extract schema name from title or use generic name
|
|
447
|
-
const schemaName = jsonSchema.title ?? "Response";
|
|
448
|
-
outputSchema = { schema: jsonSchema, name: schemaName };
|
|
449
|
-
}
|
|
450
|
-
catch {
|
|
451
|
-
// If schema conversion fails, skip
|
|
452
|
-
}
|
|
453
|
-
}
|
|
405
|
+
// Build output schema for provider (Issue #459) - computed once, cached
|
|
406
|
+
const outputSchema = this.getOutputSchema();
|
|
454
407
|
// Agentic loop
|
|
455
408
|
let iteration = 0;
|
|
456
409
|
let finalContent = "";
|
|
@@ -465,6 +418,11 @@ export class MeshLlmAgent {
|
|
|
465
418
|
outputSchema,
|
|
466
419
|
// Issue #1019: forward caller-supplied escape-hatch kwargs
|
|
467
420
|
modelParams: context.options?.modelParams,
|
|
421
|
+
// Issue #1116: forward the resolved provider-managed loop cap.
|
|
422
|
+
maxIterations,
|
|
423
|
+
// Issue #1112: forward the RAW (possibly-undefined) output_mode so the
|
|
424
|
+
// provider honors an explicit override; unset stays auto.
|
|
425
|
+
outputMode: this.config.outputMode,
|
|
468
426
|
});
|
|
469
427
|
// Track tokens
|
|
470
428
|
if (response.usage) {
|
|
@@ -597,79 +555,25 @@ export class MeshLlmAgent {
|
|
|
597
555
|
// loop. The mesh-delegated streaming provider runs its own loop on the
|
|
598
556
|
// server side and emits text chunks via notifications/progress; the
|
|
599
557
|
// consumer just yields each one.
|
|
600
|
-
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
let systemContent = await renderTemplate(systemPromptTemplate, context.templateContext ?? {});
|
|
609
|
-
if (toolDefs.length > 0) {
|
|
610
|
-
systemContent += this.buildToolSchemaSection(toolDefs);
|
|
611
|
-
}
|
|
612
|
-
messages.push({ role: "system", content: systemContent });
|
|
613
|
-
}
|
|
614
|
-
// Resolve media items to OpenAI-compatible image_url parts
|
|
615
|
-
const mediaItems = context.options?.media;
|
|
616
|
-
let mediaParts = null;
|
|
617
|
-
if (mediaItems && mediaItems.length > 0) {
|
|
618
|
-
mediaParts = await resolveMediaInputs(mediaItems);
|
|
619
|
-
}
|
|
620
|
-
if (typeof messageInput === "string") {
|
|
621
|
-
if (mediaParts && mediaParts.length > 0) {
|
|
622
|
-
messages.push({
|
|
623
|
-
role: "user",
|
|
624
|
-
content: [
|
|
625
|
-
{ type: "text", text: messageInput },
|
|
626
|
-
...mediaParts,
|
|
627
|
-
],
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
else {
|
|
631
|
-
messages.push({ role: "user", content: messageInput });
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
else {
|
|
635
|
-
for (let i = 0; i < messageInput.length; i++) {
|
|
636
|
-
const msg = messageInput[i];
|
|
637
|
-
const isLastUser = mediaParts &&
|
|
638
|
-
mediaParts.length > 0 &&
|
|
639
|
-
msg.role === "user" &&
|
|
640
|
-
i === messageInput.length - 1;
|
|
641
|
-
if (isLastUser) {
|
|
642
|
-
messages.push({
|
|
643
|
-
role: "user",
|
|
644
|
-
content: [
|
|
645
|
-
{ type: "text", text: msg.content },
|
|
646
|
-
...mediaParts,
|
|
647
|
-
],
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
else {
|
|
651
|
-
messages.push({ role: msg.role, content: msg.content });
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
}
|
|
558
|
+
// Mesh-delegated by definition (we required meshProvider above).
|
|
559
|
+
const toolDefs = this.buildToolDefinitions(context.tools, true);
|
|
560
|
+
// Build initial messages (system prompt + tool schema + resolved media +
|
|
561
|
+
// multi-turn unwinding). stream() NEVER includes the output-schema hint —
|
|
562
|
+
// the provider applies vendor-specific output formatting.
|
|
563
|
+
const messages = await this.buildAgentMessages(messageInput, context, {
|
|
564
|
+
includeOutputSchemaHint: false,
|
|
565
|
+
});
|
|
655
566
|
// Effective options (runtime > env > config)
|
|
567
|
+
const maxIterations = sanitizeMaxIterations(context.options?.maxIterations) ??
|
|
568
|
+
envMaxIterations() ??
|
|
569
|
+
this.config.maxIterations;
|
|
656
570
|
const maxTokens = context.options?.maxOutputTokens ?? this.config.maxOutputTokens;
|
|
657
571
|
const temperature = context.options?.temperature ?? this.config.temperature;
|
|
658
572
|
const model = context.meshProvider?.model ??
|
|
659
573
|
process.env.MESH_LLM_MODEL ??
|
|
660
574
|
this.config.model ??
|
|
661
575
|
this.getDefaultModel();
|
|
662
|
-
|
|
663
|
-
if (this.config.returnSchema) {
|
|
664
|
-
try {
|
|
665
|
-
const jsonSchema = zodToJsonSchema(this.config.returnSchema);
|
|
666
|
-
const schemaName = jsonSchema.title ?? "Response";
|
|
667
|
-
outputSchema = { schema: jsonSchema, name: schemaName };
|
|
668
|
-
}
|
|
669
|
-
catch {
|
|
670
|
-
// skip
|
|
671
|
-
}
|
|
672
|
-
}
|
|
576
|
+
const outputSchema = this.getOutputSchema();
|
|
673
577
|
const provider = new MeshDelegatedProvider(context.meshProvider.endpoint, context.meshProvider.functionName, this.config.parallelToolCalls ?? false);
|
|
674
578
|
yield* provider.streamComplete(model, messages, toolDefs.length > 0 ? toolDefs : undefined, {
|
|
675
579
|
maxOutputTokens: maxTokens,
|
|
@@ -679,6 +583,11 @@ export class MeshLlmAgent {
|
|
|
679
583
|
outputSchema,
|
|
680
584
|
// Issue #1019: forward caller-supplied escape-hatch kwargs
|
|
681
585
|
modelParams: context.options?.modelParams,
|
|
586
|
+
// Issue #1116: forward the resolved provider-managed loop cap.
|
|
587
|
+
maxIterations,
|
|
588
|
+
// Issue #1112: forward the RAW (possibly-undefined) output_mode so the
|
|
589
|
+
// provider honors an explicit override; unset stays auto.
|
|
590
|
+
outputMode: this.config.outputMode,
|
|
682
591
|
});
|
|
683
592
|
}
|
|
684
593
|
/**
|
|
@@ -686,8 +595,9 @@ export class MeshLlmAgent {
|
|
|
686
595
|
*/
|
|
687
596
|
createCallable(context) {
|
|
688
597
|
const agent = this;
|
|
689
|
-
|
|
690
|
-
|
|
598
|
+
// Shared "context merge vs replace" semantics used by both the buffered
|
|
599
|
+
// callable and the stream method below.
|
|
600
|
+
const mergeRunContext = (options) => {
|
|
691
601
|
const contextMode = options?.contextMode ?? "merge";
|
|
692
602
|
let mergedTemplateContext;
|
|
693
603
|
if (contextMode === "replace" && options?.context) {
|
|
@@ -702,13 +612,14 @@ export class MeshLlmAgent {
|
|
|
702
612
|
// No runtime context - use base context
|
|
703
613
|
mergedTemplateContext = context.templateContext ?? {};
|
|
704
614
|
}
|
|
705
|
-
|
|
706
|
-
const mergedContext = {
|
|
615
|
+
return {
|
|
707
616
|
...context,
|
|
708
617
|
options: options ? { ...context.options, ...options } : context.options,
|
|
709
618
|
templateContext: mergedTemplateContext,
|
|
710
619
|
};
|
|
711
|
-
|
|
620
|
+
};
|
|
621
|
+
const callable = async (message, options) => {
|
|
622
|
+
return agent.run(message, mergeRunContext(options));
|
|
712
623
|
};
|
|
713
624
|
// Attach meta property
|
|
714
625
|
Object.defineProperty(callable, "meta", {
|
|
@@ -727,23 +638,7 @@ export class MeshLlmAgent {
|
|
|
727
638
|
// "context merge vs replace" behavior as the buffered call.
|
|
728
639
|
Object.defineProperty(callable, "stream", {
|
|
729
640
|
value: (message, options) => {
|
|
730
|
-
|
|
731
|
-
let mergedTemplateContext;
|
|
732
|
-
if (contextMode === "replace" && options?.context) {
|
|
733
|
-
mergedTemplateContext = options.context;
|
|
734
|
-
}
|
|
735
|
-
else if (options?.context) {
|
|
736
|
-
mergedTemplateContext = { ...context.templateContext, ...options.context };
|
|
737
|
-
}
|
|
738
|
-
else {
|
|
739
|
-
mergedTemplateContext = context.templateContext ?? {};
|
|
740
|
-
}
|
|
741
|
-
const mergedContext = {
|
|
742
|
-
...context,
|
|
743
|
-
options: options ? { ...context.options, ...options } : context.options,
|
|
744
|
-
templateContext: mergedTemplateContext,
|
|
745
|
-
};
|
|
746
|
-
return agent.stream(message, mergedContext);
|
|
641
|
+
return agent.stream(message, mergeRunContext(options));
|
|
747
642
|
},
|
|
748
643
|
});
|
|
749
644
|
return callable;
|
|
@@ -819,17 +714,16 @@ export class MeshLlmAgent {
|
|
|
819
714
|
* Guides the LLM to produce structured output matching the schema.
|
|
820
715
|
*/
|
|
821
716
|
buildOutputSchemaSection() {
|
|
822
|
-
if (
|
|
823
|
-
return
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
return `\n\n## Output Format\n\nYour response MUST be valid JSON matching this schema:\n\n\`\`\`json\n${schemaStr}\n\`\`\`\n\nRespond ONLY with the JSON object, no additional text.`;
|
|
828
|
-
}
|
|
829
|
-
catch {
|
|
830
|
-
// If schema conversion fails, skip injection
|
|
717
|
+
if (this._outputSchemaSection !== null)
|
|
718
|
+
return this._outputSchemaSection;
|
|
719
|
+
const cached = this.getOutputSchema();
|
|
720
|
+
if (!cached) {
|
|
721
|
+
this._outputSchemaSection = "";
|
|
831
722
|
return "";
|
|
832
723
|
}
|
|
724
|
+
const schemaStr = JSON.stringify(cached.schema, null, 2);
|
|
725
|
+
this._outputSchemaSection = `\n\n## Output Format\n\nYour response MUST be valid JSON matching this schema:\n\n\`\`\`json\n${schemaStr}\n\`\`\`\n\nRespond ONLY with the JSON object, no additional text.`;
|
|
726
|
+
return this._outputSchemaSection;
|
|
833
727
|
}
|
|
834
728
|
/**
|
|
835
729
|
* Execute a tool call and record metadata.
|
|
@@ -876,129 +770,40 @@ export function createLlmToolProxy(toolInfo, description) {
|
|
|
876
770
|
const proxy = async (args) => {
|
|
877
771
|
// Set up timeout (default 30s for tool calls)
|
|
878
772
|
const timeoutMs = parseInt(process.env.MESH_TOOL_TIMEOUT_MS || "30000", 10);
|
|
879
|
-
//
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
const headersJson = Object.keys(toolPropHeaders).length > 0 ? JSON.stringify(toolPropHeaders) : undefined;
|
|
893
|
-
const injectedJson = injectTraceContext(argsJson, traceCtx.traceId, traceSpanId, headersJson);
|
|
894
|
-
toolArgsWithTrace = JSON.parse(injectedJson);
|
|
895
|
-
}
|
|
896
|
-
catch {
|
|
897
|
-
// Fallback to manual injection
|
|
898
|
-
toolArgsWithTrace = {
|
|
899
|
-
...args,
|
|
900
|
-
_trace_id: traceCtx.traceId,
|
|
901
|
-
_parent_span: traceSpanId,
|
|
902
|
-
...(Object.keys(toolPropHeaders).length > 0 ? { _mesh_headers: toolPropHeaders } : {}),
|
|
903
|
-
};
|
|
904
|
-
}
|
|
773
|
+
// Route through the shared callMcpTool (trace injection, span publish,
|
|
774
|
+
// dispatcher pooling, job-cancel wiring all handled internally). Tools may
|
|
775
|
+
// be non-idempotent so disable retries; LLM tool results can exceed the
|
|
776
|
+
// default 10 MiB cap so raise it to effectively unbounded.
|
|
777
|
+
const callOptions = {
|
|
778
|
+
...DEFAULT_CALL_OPTIONS,
|
|
779
|
+
timeout: timeoutMs,
|
|
780
|
+
maxAttempts: 1,
|
|
781
|
+
maxResponseSize: Number.MAX_SAFE_INTEGER,
|
|
782
|
+
};
|
|
783
|
+
let result;
|
|
784
|
+
try {
|
|
785
|
+
result = await callMcpTool(toolInfo.endpoint, toolInfo.functionName, args, callOptions, "mesh-tool");
|
|
905
786
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
};
|
|
787
|
+
catch (error) {
|
|
788
|
+
// callMcpTool throws a plain Error on failure — re-wrap into the
|
|
789
|
+
// ToolExecutionError shape the agentic loop expects.
|
|
790
|
+
throw new ToolExecutionError(toolInfo.functionName, error instanceof Error ? error : new Error(String(error)));
|
|
911
791
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
try {
|
|
916
|
-
response = await fetchWithTimeout(`${toolInfo.endpoint}/mcp`, {
|
|
917
|
-
method: "POST",
|
|
918
|
-
headers: {
|
|
919
|
-
"Content-Type": "application/json",
|
|
920
|
-
"Accept": "application/json, text/event-stream",
|
|
921
|
-
...(traceCtx && traceSpanId ? createTraceHeaders(traceCtx.traceId, traceSpanId) : {}),
|
|
922
|
-
...toolPropHeaders,
|
|
923
|
-
},
|
|
924
|
-
body: JSON.stringify({
|
|
925
|
-
jsonrpc: "2.0",
|
|
926
|
-
id: Date.now(),
|
|
927
|
-
method: "tools/call",
|
|
928
|
-
params: {
|
|
929
|
-
name: toolInfo.functionName,
|
|
930
|
-
arguments: toolArgsWithTrace,
|
|
931
|
-
},
|
|
932
|
-
}),
|
|
933
|
-
timeout: timeoutMs,
|
|
934
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
935
|
-
dispatcher: getDispatcher(`${toolInfo.endpoint}/mcp`),
|
|
936
|
-
});
|
|
937
|
-
}
|
|
938
|
-
catch (error) {
|
|
939
|
-
if (isTimeoutError(error)) {
|
|
940
|
-
throw new ToolExecutionError(toolInfo.functionName, new Error(`Tool call timed out after ${timeoutMs}ms (endpoint: ${toolInfo.endpoint})`));
|
|
941
|
-
}
|
|
942
|
-
throw new ToolExecutionError(toolInfo.functionName, error instanceof Error ? error : new Error(String(error)));
|
|
943
|
-
}
|
|
944
|
-
if (!response.ok) {
|
|
945
|
-
const errorBody = await response.text();
|
|
946
|
-
throw new ToolExecutionError(toolInfo.functionName, new Error(`Tool call failed: ${response.status} ${errorBody}`));
|
|
947
|
-
}
|
|
948
|
-
// Handle SSE response from FastMCP stateless HTTP stream
|
|
949
|
-
const responseText = await response.text();
|
|
950
|
-
const result = parseSSEResponse(responseText);
|
|
951
|
-
if (result.error) {
|
|
952
|
-
throw new Error(`Tool error: ${result.error.message}`);
|
|
953
|
-
}
|
|
954
|
-
// Parse result content
|
|
955
|
-
const content = result.result?.content?.[0];
|
|
956
|
-
if (!content) {
|
|
957
|
-
resultType = "null";
|
|
958
|
-
return null;
|
|
959
|
-
}
|
|
960
|
-
if (content.type === "text" && content.text) {
|
|
961
|
-
// Try to parse as JSON
|
|
962
|
-
try {
|
|
963
|
-
const parsed = JSON.parse(content.text);
|
|
964
|
-
resultType = typeof parsed;
|
|
965
|
-
return parsed;
|
|
966
|
-
}
|
|
967
|
-
catch {
|
|
968
|
-
resultType = "string";
|
|
969
|
-
return content.text;
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
resultType = typeof content;
|
|
973
|
-
return content;
|
|
792
|
+
// Multi-content results are returned as structured objects as-is.
|
|
793
|
+
if (typeof result === "object") {
|
|
794
|
+
return result;
|
|
974
795
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
796
|
+
// Empty content signals "tool returned nothing" — preserve the null result
|
|
797
|
+
// the hand-rolled path produced rather than an empty string.
|
|
798
|
+
if (result === "") {
|
|
799
|
+
return null;
|
|
979
800
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
spanId: traceSpanId,
|
|
987
|
-
parentSpan: traceCtx.parentSpanId,
|
|
988
|
-
functionName: "proxy_call_wrapper",
|
|
989
|
-
startTime: traceStartTime,
|
|
990
|
-
endTime: traceEndTime,
|
|
991
|
-
durationMs: traceDurationMs,
|
|
992
|
-
success: traceSuccess,
|
|
993
|
-
error: traceError,
|
|
994
|
-
resultType: traceSuccess ? resultType : "error",
|
|
995
|
-
argsCount: 0,
|
|
996
|
-
kwargsCount: 0,
|
|
997
|
-
dependencies: [toolInfo.endpoint],
|
|
998
|
-
injectedDependencies: 0,
|
|
999
|
-
meshPositions: [],
|
|
1000
|
-
}).catch(() => { });
|
|
1001
|
-
}
|
|
801
|
+
// Parse JSON if possible, otherwise return the raw string.
|
|
802
|
+
try {
|
|
803
|
+
return JSON.parse(result);
|
|
804
|
+
}
|
|
805
|
+
catch {
|
|
806
|
+
return result;
|
|
1002
807
|
}
|
|
1003
808
|
};
|
|
1004
809
|
// Safely parse inputSchema - don't let malformed JSON break proxy creation
|