minimal-agent 0.18.10 → 0.18.12
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/advisor/advisorCoordinator.js +30 -6
- package/src/advisor/advisorCoordinator.ts +23 -6
- package/src/advisor/deliveryReview.js +23 -0
- package/src/advisor/deliveryReview.ts +22 -0
- package/src/advisor/presentation.js +3 -1
- package/src/advisor/presentation.ts +4 -1
- package/src/loop/README.md +44 -0
- package/src/loop/deliveryIntent.js +22 -0
- package/src/loop/deliveryIntent.ts +39 -0
- package/src/loop/messages.js +146 -0
- package/src/loop/messages.ts +166 -0
- package/src/loop/options.js +1 -0
- package/src/loop/options.ts +155 -0
- package/src/loop/progressStream.js +40 -0
- package/src/loop/progressStream.ts +44 -0
- package/src/loop/publication.js +94 -0
- package/src/loop/publication.ts +124 -0
- package/src/loop/requestPreparation.js +180 -0
- package/src/loop/requestPreparation.ts +226 -0
- package/src/loop/toolBatch.js +99 -0
- package/src/loop/toolBatch.ts +123 -0
- package/src/loop/toolExecutor.js +304 -0
- package/src/loop/toolExecutor.ts +363 -0
- package/src/loop/toolPresentation.js +135 -0
- package/src/loop/toolPresentation.ts +134 -0
- package/src/loop/toolSemantics.js +45 -0
- package/src/loop/toolSemantics.ts +51 -0
- package/src/loop.js +369 -1114
- package/src/loop.ts +480 -1380
- package/src/tools/advisor/advisor.js +5 -2
- package/src/tools/advisor/advisor.ts +6 -2
- package/src/tools/bash/semantics.js +33 -1
- package/src/tools/bash/semantics.ts +27 -1
- package/src/tools/toolPromptGuides.js +1 -1
- package/src/tools/toolPromptGuides.ts +1 -1
- package/src/tools/webfetch/pageSnapshot.js +56 -0
- package/src/tools/webfetch/pageSnapshot.ts +51 -0
- package/src/tools/webfetch/webfetch.js +77 -116
- package/src/tools/webfetch/webfetch.ts +70 -136
- package/src/types.ts +8 -0
|
@@ -6,27 +6,29 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 输入:
|
|
8
8
|
* url 必填,要获取内容的 URL
|
|
9
|
+
* snapshot_id / offset / max_chars / query 用于稳定续读与搜索
|
|
9
10
|
* prompt 可选,旧版兼容字段,运行时忽略
|
|
10
11
|
*
|
|
11
12
|
* 输出:
|
|
12
|
-
*
|
|
13
|
+
* 完整快照引用、字符范围、下一段游标与本次正文窗口
|
|
13
14
|
*
|
|
14
15
|
* 流程:
|
|
15
16
|
* 1. URL 验证(合法格式、2000字符以内、无凭据)
|
|
16
17
|
* 2. fetch(保持 HTTP(S) 协议,超时 60s)
|
|
17
18
|
* 3. 检测跨 host 重定向,返回提示而非自动跟随
|
|
18
19
|
* 4. HTML → Markdown(turndown);非 HTML 原样返回
|
|
19
|
-
* 5.
|
|
20
|
+
* 5. 完整正文保存为本机会话快照,按字符游标读取或搜索
|
|
20
21
|
* ============================================================
|
|
21
22
|
*/
|
|
22
23
|
|
|
23
24
|
import { z } from 'zod';
|
|
24
25
|
|
|
25
|
-
import type { Tool, ToolResult } from '../../types.js';
|
|
26
|
+
import type { Tool, ToolResult, ToolCallContext } from '../../types.js';
|
|
26
27
|
import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
|
|
27
28
|
import { toToolParameters } from '../../utils/zodToJson.js';
|
|
28
29
|
import { isPreapprovedDomain } from './preapproved.js';
|
|
29
30
|
import { readHttpBody } from '../shared/httpBody.js';
|
|
31
|
+
import { loadPageSnapshot, savePageSnapshot, pageSnapshotPath, pageWindow, type PageSnapshot } from './pageSnapshot.js';
|
|
30
32
|
|
|
31
33
|
// ---------------- 1. 常量 ----------------
|
|
32
34
|
const MAX_URL_LENGTH = 2000;
|
|
@@ -76,16 +78,18 @@ function cleanCache(): void {
|
|
|
76
78
|
// ---------------- 3. Zod 输入 schema ----------------
|
|
77
79
|
const inputSchema = z.object({
|
|
78
80
|
url: z.string().describe('要获取内容的 URL'),
|
|
81
|
+
snapshot_id: z.string().regex(/^[a-f0-9]{64}$/).optional().describe('续读时使用上次返回的快照 ID,保证读取同一版本。'),
|
|
82
|
+
offset: z.number().int().nonnegative().optional().describe('0-based UTF-16 字符位置;使用 next_offset 续读,不是行号。'),
|
|
83
|
+
max_chars: z.number().int().min(1000).max(1_000_000).optional().describe('本次正文字符预算,Runtime 按剩余上下文收紧。'),
|
|
84
|
+
query: z.string().min(1).max(500).optional().describe('在完整快照中从 offset 开始查找精确文本(区分大小写,非正则),返回命中处正文。'),
|
|
79
85
|
/** @deprecated Accepted for callers compiled against the legacy contract. */
|
|
80
86
|
prompt: z.string().optional(),
|
|
81
87
|
});
|
|
82
88
|
type WebFetchInput = z.infer<typeof inputSchema>;
|
|
83
89
|
|
|
84
|
-
// Model-visible
|
|
90
|
+
// Model-visible snapshot contract. WebFetch returns the page body and does not run a
|
|
85
91
|
// hidden extraction model; the runtime schema still accepts legacy `prompt`.
|
|
86
|
-
const parameters = toToolParameters(
|
|
87
|
-
url: z.string().describe('要读取正文的已知 URL'),
|
|
88
|
-
}));
|
|
92
|
+
const parameters = toToolParameters(inputSchema.omit({ prompt: true }));
|
|
89
93
|
|
|
90
94
|
// ---------------- 4. Description ----------------
|
|
91
95
|
const description = `Reads the full text content of a known URL without opening a browser.
|
|
@@ -95,8 +99,11 @@ const description = `Reads the full text content of a known URL without opening
|
|
|
95
99
|
- It does not discover sources, authenticate, click, upload, or operate an interactive page.
|
|
96
100
|
- HTTP and HTTPS URLs are fetched as supplied, including local development servers.
|
|
97
101
|
- When a URL redirects to a different host, returns a warning with the redirect URL.
|
|
98
|
-
-
|
|
99
|
-
-
|
|
102
|
+
- Read-only with respect to the source/workspace; complete extracted text is saved in a private local runtime snapshot.
|
|
103
|
+
- Results are windows, not the whole page. Continue with the same url + snapshot_id + next_offset until next_offset=null. Use query for exact-text search anywhere in the snapshot.
|
|
104
|
+
- max_chars can request larger windows for large-context models; the runtime may reduce the per-call budget.
|
|
105
|
+
- A complete snapshot means the fetched/extracted body, not authenticated or JavaScript-rendered content. Do not claim unread ranges were inspected.
|
|
106
|
+
- Conversion failures return clearly labeled raw HTML. If dynamic content is missing, use available browser tools; do not repeat the same window or invent missing content.
|
|
100
107
|
- ⚠️ IMPORTANT: This tool WILL FAIL for authenticated or private URLs.
|
|
101
108
|
Do not claim access to private content when the current tool catalog does not
|
|
102
109
|
contain an authenticated integration for that service.
|
|
@@ -234,140 +241,66 @@ async function htmlToMarkdown(html: string): Promise<string> {
|
|
|
234
241
|
async function call(
|
|
235
242
|
input: WebFetchInput,
|
|
236
243
|
signal?: AbortSignal,
|
|
244
|
+
context: ToolCallContext = {},
|
|
237
245
|
): Promise<ToolResult> {
|
|
238
246
|
if (signal?.aborted) return { ok: false, error: '请求被中断' };
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
247
|
+
const valid = validateURL(input.url);
|
|
248
|
+
if (!valid.ok) return { ok: false, error: valid.error };
|
|
249
|
+
if ((input.offset ?? 0) > 0 && !input.snapshot_id)
|
|
250
|
+
return { ok: false, error: '续读需要 snapshot_id;先读取 URL 获取稳定快照,避免拼接不同版本。' };
|
|
251
|
+
const budget = context.reserveReadChars?.(input.max_chars) ?? input.max_chars ?? DEFAULT_MAX_RESULT_SIZE_CHARS;
|
|
252
|
+
if (budget < 1000) return { ok: false, error: '本批读取预算已用完,请处理已有结果或压缩上下文后续读。' };
|
|
244
253
|
try {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
} catch (e) {
|
|
261
|
-
console.warn(`turndown 失败: ${(e as Error).message}`);
|
|
254
|
+
let page: PageSnapshot;
|
|
255
|
+
let status = '';
|
|
256
|
+
if (input.snapshot_id) {
|
|
257
|
+
page = await loadPageSnapshot(input.snapshot_id, input.url, context);
|
|
258
|
+
} else {
|
|
259
|
+
const cached = URL_CACHE.get(input.url);
|
|
260
|
+
const hit = cached && Date.now() - cached.fetchedAt <= CACHE_TTL_MS;
|
|
261
|
+
const fetched = hit ? { type: 'success' as const, ...cached } : await fetchURL(input.url, signal ?? new AbortController().signal);
|
|
262
|
+
if (fetched.type === 'error') return { ok: false, error: fetched.error };
|
|
263
|
+
if (fetched.type === 'redirect') return { ok: true, content:
|
|
264
|
+
`【重定向检测】\n原始 URL: ${fetched.originalUrl}\n重定向到: ${fetched.redirectUrl}\n状态: ${fetched.statusCode}\n请使用重定向后的 URL 再次调用 WebFetch 工具。` };
|
|
265
|
+
const fetchedAt = hit ? cached.fetchedAt : Date.now();
|
|
266
|
+
if (!hit) {
|
|
267
|
+
URL_CACHE.set(input.url, { ...fetched, fetchedAt });
|
|
268
|
+
cleanCache();
|
|
262
269
|
}
|
|
270
|
+
let text = fetched.content;
|
|
271
|
+
let format = 'text';
|
|
272
|
+
if (fetched.contentType.includes('text/html')) {
|
|
273
|
+
try { text = await htmlToMarkdown(text); format = 'markdown'; }
|
|
274
|
+
catch { format = 'raw-html (正文转换失败,保留原始 HTML)'; }
|
|
275
|
+
}
|
|
276
|
+
page = { url: input.url, text, format, fetchedAt };
|
|
277
|
+
status = `状态: ${fetched.code} ${fetched.codeText}${hit ? ' (缓存命中)' : ''}\n`;
|
|
263
278
|
}
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
?
|
|
267
|
-
:
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const domainNote = preapproved
|
|
279
|
+
signal?.throwIfAborted();
|
|
280
|
+
const snapshot = input.snapshot_id
|
|
281
|
+
? { id: input.snapshot_id, path: pageSnapshotPath(input.snapshot_id, context) }
|
|
282
|
+
: await savePageSnapshot(page, context);
|
|
283
|
+
signal?.throwIfAborted();
|
|
284
|
+
const window = pageWindow(page.text, input.offset ?? 0, budget, input.query);
|
|
285
|
+
const domain = isPreapprovedDomain(valid.parsed.hostname)
|
|
272
286
|
? '[网络访问:预批准域名]'
|
|
273
287
|
: '[网络访问:普通域名;请按来源本身评估证据质量]';
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
URL: ${url}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
${
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
${
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return { ok: true, content: final };
|
|
288
|
+
return { ok: true, policy: { highValue: true, reclaimable: false, boundedWindow: true,
|
|
289
|
+
archiveRef: snapshot.path, originalChars: page.text.length }, content: `【WebFetch 结果】
|
|
290
|
+
URL: ${page.url}
|
|
291
|
+
${status}${domain}
|
|
292
|
+
snapshot_id: ${snapshot.id}
|
|
293
|
+
snapshot_complete: true (完整抓取正文,不代表动态页面内容完整)
|
|
294
|
+
format: ${page.format}
|
|
295
|
+
total_chars: ${page.text.length}
|
|
296
|
+
range: [${window.start}, ${window.end})
|
|
297
|
+
next_offset: ${window.next ?? 'null'}
|
|
298
|
+
完整正文文件: ${snapshot.path}
|
|
299
|
+
${window.found ? '' : '未找到指定文本(精确匹配,区分大小写)。\n'}--- 内容 ---
|
|
300
|
+
${window.text}` };
|
|
301
|
+
} catch (error) {
|
|
302
|
+
return { ok: false, error: `网页快照读取失败:${(error as Error).message}。未返回不完整正文;请重试原 URL 或检查本地存储。` };
|
|
292
303
|
}
|
|
293
|
-
|
|
294
|
-
// 1. fetch
|
|
295
|
-
const fetched = await fetchURL(url, signal ?? new AbortController().signal);
|
|
296
|
-
const durationMs = Date.now() - start;
|
|
297
|
-
|
|
298
|
-
if (fetched.type === 'redirect') {
|
|
299
|
-
const statusText =
|
|
300
|
-
fetched.statusCode === 301 ? 'Moved Permanently' :
|
|
301
|
-
fetched.statusCode === 308 ? 'Permanent Redirect' :
|
|
302
|
-
fetched.statusCode === 307 ? 'Temporary Redirect' : 'Found';
|
|
303
|
-
|
|
304
|
-
return {
|
|
305
|
-
ok: true,
|
|
306
|
-
content: `【重定向检测】
|
|
307
|
-
|
|
308
|
-
原始 URL: ${fetched.originalUrl}
|
|
309
|
-
重定向到: ${fetched.redirectUrl}
|
|
310
|
-
状态: ${fetched.statusCode} ${statusText}
|
|
311
|
-
|
|
312
|
-
请使用重定向后的 URL 再次调用 WebFetch 工具。`,
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (fetched.type === 'error') {
|
|
317
|
-
return { ok: false, error: fetched.error };
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
let { content, bytes, code, codeText, contentType } = fetched;
|
|
321
|
-
|
|
322
|
-
// 写入缓存
|
|
323
|
-
URL_CACHE.set(cacheKey, {
|
|
324
|
-
bytes,
|
|
325
|
-
code,
|
|
326
|
-
codeText,
|
|
327
|
-
content,
|
|
328
|
-
contentType,
|
|
329
|
-
fetchedAt: Date.now(),
|
|
330
|
-
});
|
|
331
|
-
cleanCache();
|
|
332
|
-
|
|
333
|
-
// 2. HTML → Markdown
|
|
334
|
-
if (contentType.includes('text/html')) {
|
|
335
|
-
try {
|
|
336
|
-
content = await htmlToMarkdown(content);
|
|
337
|
-
} catch (e) {
|
|
338
|
-
console.warn(`turndown 失败: ${(e as Error).message}`);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// 3. 内容直接返回(超长内容在这里直接截断)
|
|
343
|
-
// 4. 追加 metadata
|
|
344
|
-
const formattedSize = bytes < 1024
|
|
345
|
-
? `${bytes} B`
|
|
346
|
-
: bytes < 1024 * 1024
|
|
347
|
-
? `${(bytes / 1024).toFixed(1)} KB`
|
|
348
|
-
: `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
349
|
-
|
|
350
|
-
const domainNote = preapproved
|
|
351
|
-
? '[网络访问:预批准域名]'
|
|
352
|
-
: '[网络访问:普通域名;请按来源本身评估证据质量]';
|
|
353
|
-
|
|
354
|
-
const output = `【WebFetch 结果】
|
|
355
|
-
URL: ${url}
|
|
356
|
-
状态: ${code} ${codeText}
|
|
357
|
-
大小: ${formattedSize}
|
|
358
|
-
耗时: ${(durationMs / 1000).toFixed(2)}s
|
|
359
|
-
${domainNote}
|
|
360
|
-
|
|
361
|
-
--- 内容 ---
|
|
362
|
-
${content}`;
|
|
363
|
-
|
|
364
|
-
// 5. 截断
|
|
365
|
-
let final = output;
|
|
366
|
-
if (final.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
|
|
367
|
-
final = final.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS) + `\n\n... (输出超过 ${DEFAULT_MAX_RESULT_SIZE_CHARS} 字符,已截断)`;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
return { ok: true, content: final };
|
|
371
304
|
}
|
|
372
305
|
|
|
373
306
|
// ---------------- 7. 导出 ----------------
|
|
@@ -378,7 +311,8 @@ export const webfetchTool: Tool<WebFetchInput> = {
|
|
|
378
311
|
parameters,
|
|
379
312
|
isReadOnly: true,
|
|
380
313
|
isConcurrencySafe: true,
|
|
381
|
-
maxResultSizeChars:
|
|
314
|
+
maxResultSizeChars: 1_010_000,
|
|
382
315
|
interruptBehavior: () => 'cancel',
|
|
383
316
|
call,
|
|
317
|
+
callWithContext: (input, signal, context) => call(input, signal, context),
|
|
384
318
|
};
|
package/src/types.ts
CHANGED
|
@@ -397,6 +397,9 @@ export interface AdvisorCheckpointMetadata extends AdvisorVersions {
|
|
|
397
397
|
|
|
398
398
|
export interface AdvisorConsultationMetadata extends AdvisorVersions {
|
|
399
399
|
kind: 'advisor_consultation';
|
|
400
|
+
/** Host-recorded trigger, distinct from who emitted the tool call. Absent in old history. */
|
|
401
|
+
triggerSource?: 'checkpoint' | 'model';
|
|
402
|
+
triggerReason?: string;
|
|
400
403
|
consultationId: string;
|
|
401
404
|
checkpoint: AdvisorCheckpoint;
|
|
402
405
|
checkpointKey: string;
|
|
@@ -477,6 +480,9 @@ export type ToolResultMetadata =
|
|
|
477
480
|
export interface ToolResultPolicy {
|
|
478
481
|
reclaimable: boolean;
|
|
479
482
|
highValue: boolean;
|
|
483
|
+
/** Tool already budgeted a window backed by a complete local snapshot.
|
|
484
|
+
* Keep this window inline: re-archiving would invalidate its visible cursor. */
|
|
485
|
+
boundedWindow?: boolean;
|
|
480
486
|
archiveRef?: string;
|
|
481
487
|
originalChars?: number;
|
|
482
488
|
}
|
|
@@ -570,6 +576,8 @@ export type ToolResult = { problem?: ToolProblemOutcome; observation?: ReadObser
|
|
|
570
576
|
* 避免按工具名写特判分支。
|
|
571
577
|
*/
|
|
572
578
|
export interface ToolCallContext {
|
|
579
|
+
/** Run-local host initialization shared with Advisor; never a model argument. */
|
|
580
|
+
advisorConsultation?: AdvisorConsultationMetadata;
|
|
573
581
|
/** Shared per-model-response read allowance; reservations happen before IO. */
|
|
574
582
|
reserveReadChars?: (requested?: number) => number;
|
|
575
583
|
/** Host presentation only; cannot grant tools to a delegated agent. */
|