membot 0.5.1 → 0.5.2
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 +1 -1
- package/src/ingest/agent-fetcher.ts +80 -5
- package/src/ingest/fetcher.ts +7 -2
- package/src/ingest/ingest.ts +1 -0
package/package.json
CHANGED
|
@@ -58,6 +58,13 @@ export interface AgentFetchOptions {
|
|
|
58
58
|
mcpx: AgentMcpxAdapter;
|
|
59
59
|
llm: LlmConfig;
|
|
60
60
|
hint?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Optional sublabel callback. Receives compact, human-readable strings
|
|
63
|
+
* describing what the agent is doing each turn (e.g. "mcp_exec
|
|
64
|
+
* linear/list_comments (turn 2)"). Wired to the spinner suffix in TTY
|
|
65
|
+
* mode so users see live progress without `--verbose`.
|
|
66
|
+
*/
|
|
67
|
+
onProgress?: (sublabel: string) => void;
|
|
61
68
|
/** Test seam: inject a pre-built Anthropic client. */
|
|
62
69
|
_testClient?: Anthropic;
|
|
63
70
|
}
|
|
@@ -238,7 +245,14 @@ export async function agentFetch(opts: AgentFetchOptions): Promise<AgentFetchOut
|
|
|
238
245
|
|
|
239
246
|
const captured = new Map<string, CapturedExec>();
|
|
240
247
|
|
|
248
|
+
opts.onProgress?.(`fetching via mcpx agent (turn 1)`);
|
|
249
|
+
|
|
241
250
|
for (let turn = 0; turn < MAX_TURNS; turn++) {
|
|
251
|
+
if (turn > 0) {
|
|
252
|
+
logger.info(`[fetcher] turn ${turn + 1}/${MAX_TURNS}`);
|
|
253
|
+
opts.onProgress?.(`fetching via mcpx agent (turn ${turn + 1})`);
|
|
254
|
+
}
|
|
255
|
+
|
|
242
256
|
const response = await client.messages.create({
|
|
243
257
|
model: opts.llm.converter_model,
|
|
244
258
|
max_tokens: MAX_RESPONSE_TOKENS,
|
|
@@ -249,7 +263,7 @@ export async function agentFetch(opts: AgentFetchOptions): Promise<AgentFetchOut
|
|
|
249
263
|
|
|
250
264
|
for (const block of response.content) {
|
|
251
265
|
if (block.type === "text" && block.text.trim()) {
|
|
252
|
-
logger.debug(`
|
|
266
|
+
logger.debug(`[fetcher] turn ${turn + 1} reasoning: ${block.text.trim()}`);
|
|
253
267
|
}
|
|
254
268
|
}
|
|
255
269
|
|
|
@@ -263,12 +277,19 @@ export async function agentFetch(opts: AgentFetchOptions): Promise<AgentFetchOut
|
|
|
263
277
|
|
|
264
278
|
const toolUseBlocks = response.content.filter((b): b is ToolUseBlock => b.type === "tool_use");
|
|
265
279
|
if (toolUseBlocks.length === 0) {
|
|
266
|
-
logger.
|
|
280
|
+
logger.info(`[fetcher] turn ${turn + 1}: no tool calls — falling back to HTTP`);
|
|
267
281
|
return { kind: "fallback", reason: "agent stopped without selecting an outcome" };
|
|
268
282
|
}
|
|
269
283
|
|
|
270
284
|
messages.push({ role: "assistant", content: response.content });
|
|
271
285
|
|
|
286
|
+
// Log selected tools at info-level so users see what the agent is doing
|
|
287
|
+
// without enabling --verbose. Discovery (search/info/list) stays quiet
|
|
288
|
+
// at info; the actual mcp_exec calls are the high-signal events.
|
|
289
|
+
for (const tu of toolUseBlocks) {
|
|
290
|
+
logToolSelection(tu, turn + 1, opts.onProgress);
|
|
291
|
+
}
|
|
292
|
+
|
|
272
293
|
// Terminal tools — checked in priority order.
|
|
273
294
|
const failureCall = toolUseBlocks.find((b) => b.name === "report_failure");
|
|
274
295
|
if (failureCall) {
|
|
@@ -277,6 +298,7 @@ export async function agentFetch(opts: AgentFetchOptions): Promise<AgentFetchOut
|
|
|
277
298
|
typeof input.message === "string" && input.message.trim()
|
|
278
299
|
? input.message.trim()
|
|
279
300
|
: "Fetch failed but the agent did not provide a message.";
|
|
301
|
+
logger.info(`[fetcher] turn ${turn + 1}: report_failure: ${message}`);
|
|
280
302
|
throw new HelpfulError({
|
|
281
303
|
kind: "input_error",
|
|
282
304
|
message: `Fetcher agent reported failure for ${opts.url}: ${message}`,
|
|
@@ -286,7 +308,7 @@ export async function agentFetch(opts: AgentFetchOptions): Promise<AgentFetchOut
|
|
|
286
308
|
|
|
287
309
|
const fallbackCall = toolUseBlocks.find((b) => b.name === "request_http_fallback");
|
|
288
310
|
if (fallbackCall) {
|
|
289
|
-
logger.
|
|
311
|
+
logger.info(`[fetcher] turn ${turn + 1}: agent requested HTTP fallback`);
|
|
290
312
|
return { kind: "fallback", reason: "agent requested HTTP fallback" };
|
|
291
313
|
}
|
|
292
314
|
|
|
@@ -325,6 +347,10 @@ export async function agentFetch(opts: AgentFetchOptions): Promise<AgentFetchOut
|
|
|
325
347
|
}
|
|
326
348
|
const claimedMime = (input.mime_type ?? cached.mimeType ?? "text/markdown").trim() || "text/markdown";
|
|
327
349
|
const bytes = new TextEncoder().encode(cached.content);
|
|
350
|
+
logger.info(`[fetcher] accepted: ${cached.server}/${cached.tool} (${bytes.byteLength} bytes, ${claimedMime})`);
|
|
351
|
+
logger.debug(`[fetcher] accepted args: ${truncateJson(cached.args, 500)}`);
|
|
352
|
+
logger.debug(`[fetcher] accepted preview: ${truncate(cached.content, 200)}`);
|
|
353
|
+
opts.onProgress?.(`accepted ${cached.server}/${cached.tool}`);
|
|
328
354
|
return {
|
|
329
355
|
kind: "accepted",
|
|
330
356
|
result: {
|
|
@@ -347,10 +373,52 @@ export async function agentFetch(opts: AgentFetchOptions): Promise<AgentFetchOut
|
|
|
347
373
|
messages.push({ role: "user", content: toolResults });
|
|
348
374
|
}
|
|
349
375
|
|
|
350
|
-
logger.
|
|
376
|
+
logger.info(`[fetcher] max turns (${MAX_TURNS}) exceeded — falling back to HTTP`);
|
|
351
377
|
return { kind: "fallback", reason: `agent exceeded MAX_TURNS=${MAX_TURNS}` };
|
|
352
378
|
}
|
|
353
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Emit a per-turn line about which tool the agent is about to invoke. mcp_exec
|
|
382
|
+
* is the high-signal event that surfaces *which provider was chosen*, so it
|
|
383
|
+
* goes to info; discovery (search / list / info) stays at debug.
|
|
384
|
+
*/
|
|
385
|
+
function logToolSelection(tu: ToolUseBlock, turn: number, onProgress?: (s: string) => void): void {
|
|
386
|
+
if (tu.name === "mcp_exec") {
|
|
387
|
+
const i = tu.input as Partial<{ server: string; tool: string; args: Record<string, unknown> }>;
|
|
388
|
+
const server = i.server ?? "?";
|
|
389
|
+
const tool = i.tool ?? "?";
|
|
390
|
+
logger.info(`[fetcher] turn ${turn}: mcp_exec ${server}/${tool}`);
|
|
391
|
+
logger.debug(`[fetcher] turn ${turn}: mcp_exec args: ${truncateJson(i.args ?? {}, 500)}`);
|
|
392
|
+
onProgress?.(`mcp_exec ${server}/${tool} (turn ${turn})`);
|
|
393
|
+
} else if (tu.name === "mcp_search") {
|
|
394
|
+
const i = tu.input as Partial<{ query: string }>;
|
|
395
|
+
logger.debug(`[fetcher] turn ${turn}: mcp_search "${i.query ?? ""}"`);
|
|
396
|
+
} else if (tu.name === "mcp_info") {
|
|
397
|
+
const i = tu.input as Partial<{ server: string; tool: string }>;
|
|
398
|
+
logger.debug(`[fetcher] turn ${turn}: mcp_info ${i.server ?? "?"}/${i.tool ?? "?"}`);
|
|
399
|
+
} else if (tu.name === "mcp_list_tools") {
|
|
400
|
+
const i = tu.input as Partial<{ server: string }>;
|
|
401
|
+
logger.debug(`[fetcher] turn ${turn}: mcp_list_tools${i.server ? ` ${i.server}` : ""}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** JSON-stringify with a length cap so a giant args payload doesn't bloat logs. */
|
|
406
|
+
function truncateJson(value: unknown, max: number): string {
|
|
407
|
+
let s: string;
|
|
408
|
+
try {
|
|
409
|
+
s = JSON.stringify(value);
|
|
410
|
+
} catch {
|
|
411
|
+
s = String(value);
|
|
412
|
+
}
|
|
413
|
+
return s.length > max ? `${s.slice(0, max)}… (+${s.length - max} chars)` : s;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Single-line truncation for debug previews; collapses whitespace. */
|
|
417
|
+
function truncate(s: string, max: number): string {
|
|
418
|
+
const oneLine = s.replace(/\s+/g, " ").trim();
|
|
419
|
+
return oneLine.length > max ? `${oneLine.slice(0, max)}… (+${oneLine.length - max} chars)` : oneLine;
|
|
420
|
+
}
|
|
421
|
+
|
|
354
422
|
/** Execute one agent tool call and produce the tool_result block fed back to Claude. */
|
|
355
423
|
async function dispatchAgentTool(
|
|
356
424
|
toolUse: ToolUseBlock,
|
|
@@ -392,6 +460,8 @@ async function runMcpSearch(toolUse: ToolUseBlock, mcpx: AgentMcpxAdapter): Prom
|
|
|
392
460
|
}
|
|
393
461
|
try {
|
|
394
462
|
const results = await mcpx.search(input.query);
|
|
463
|
+
const top = results.slice(0, 3).map((r) => `${r.server}/${r.tool}${r.score ? ` (${r.score.toFixed(2)})` : ""}`);
|
|
464
|
+
logger.debug(`[fetcher] mcp_search "${input.query}" → ${top.length ? top.join(", ") : "(no hits)"}`);
|
|
395
465
|
return {
|
|
396
466
|
type: "tool_result",
|
|
397
467
|
tool_use_id: toolUse.id,
|
|
@@ -498,10 +568,12 @@ async function runMcpExec(
|
|
|
498
568
|
try {
|
|
499
569
|
result = await mcpx.exec(input.server, input.tool, args);
|
|
500
570
|
} catch (err) {
|
|
571
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
572
|
+
logger.info(`[fetcher] → ${input.server}/${input.tool} threw: ${truncate(msg, 200)}`);
|
|
501
573
|
return {
|
|
502
574
|
type: "tool_result",
|
|
503
575
|
tool_use_id: toolUse.id,
|
|
504
|
-
content: `mcp_exec ${input.server}/${input.tool} threw: ${
|
|
576
|
+
content: `mcp_exec ${input.server}/${input.tool} threw: ${msg}. Use mcp_info to verify the schema, then retry — or pivot to a different tool.`,
|
|
505
577
|
is_error: true,
|
|
506
578
|
};
|
|
507
579
|
}
|
|
@@ -509,6 +581,7 @@ async function runMcpExec(
|
|
|
509
581
|
const text = extractText(result);
|
|
510
582
|
|
|
511
583
|
if (result.isError === true) {
|
|
584
|
+
logger.info(`[fetcher] → ${input.server}/${input.tool} error: ${truncate(text, 200)}`);
|
|
512
585
|
return {
|
|
513
586
|
type: "tool_result",
|
|
514
587
|
tool_use_id: toolUse.id,
|
|
@@ -518,6 +591,7 @@ async function runMcpExec(
|
|
|
518
591
|
}
|
|
519
592
|
|
|
520
593
|
if (!text?.trim()) {
|
|
594
|
+
logger.info(`[fetcher] → ${input.server}/${input.tool} empty result`);
|
|
521
595
|
return {
|
|
522
596
|
type: "tool_result",
|
|
523
597
|
tool_use_id: toolUse.id,
|
|
@@ -526,6 +600,7 @@ async function runMcpExec(
|
|
|
526
600
|
};
|
|
527
601
|
}
|
|
528
602
|
|
|
603
|
+
logger.info(`[fetcher] → ${input.server}/${input.tool} ok (${text.length} chars)`);
|
|
529
604
|
captured.set(toolUse.id, { server: input.server, tool: input.tool, args, content: text, mimeType: "text/markdown" });
|
|
530
605
|
const preview =
|
|
531
606
|
text.length > PREVIEW_CHARS
|
package/src/ingest/fetcher.ts
CHANGED
|
@@ -32,6 +32,11 @@ export interface FetchOptions {
|
|
|
32
32
|
* mcpx path is skipped and we fall back to plain HTTP.
|
|
33
33
|
*/
|
|
34
34
|
llm?: LlmConfig;
|
|
35
|
+
/**
|
|
36
|
+
* Forwarded to the agent loop so callers (e.g. the ingest progress
|
|
37
|
+
* reporter) can drive a spinner sublabel from per-turn agent activity.
|
|
38
|
+
*/
|
|
39
|
+
onProgress?: (sublabel: string) => void;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
/**
|
|
@@ -79,7 +84,7 @@ export async function fetchRemote(url: string, options: FetchOptions = {}): Prom
|
|
|
79
84
|
|
|
80
85
|
let outcome: Awaited<ReturnType<typeof agentFetch>>;
|
|
81
86
|
try {
|
|
82
|
-
outcome = await agentFetch({ url, mcpx, llm: options.llm!, hint });
|
|
87
|
+
outcome = await agentFetch({ url, mcpx, llm: options.llm!, hint, onProgress: options.onProgress });
|
|
83
88
|
} catch (err) {
|
|
84
89
|
if (err instanceof HelpfulError) throw err;
|
|
85
90
|
logger.warn(`agent-fetch failed (${err instanceof Error ? err.message : String(err)}) — falling back to HTTP`);
|
|
@@ -98,7 +103,7 @@ export async function fetchRemote(url: string, options: FetchOptions = {}): Prom
|
|
|
98
103
|
sourceUrl: url,
|
|
99
104
|
};
|
|
100
105
|
}
|
|
101
|
-
logger.
|
|
106
|
+
logger.info(`[fetcher] falling back to HTTP: ${outcome.reason}`);
|
|
102
107
|
return httpFetch(url);
|
|
103
108
|
}
|
|
104
109
|
|
package/src/ingest/ingest.ts
CHANGED
|
@@ -237,6 +237,7 @@ async function ingestUrl(
|
|
|
237
237
|
hint: input.fetcher_hint,
|
|
238
238
|
mcpx: mcpxAdapter,
|
|
239
239
|
llm: ctx.config.llm,
|
|
240
|
+
onProgress: (sublabel) => callbacks?.onEntryProgress?.(url, sublabel),
|
|
240
241
|
});
|
|
241
242
|
result.mime_type = fetched.mimeType;
|
|
242
243
|
result.size_bytes = fetched.bytes.byteLength;
|