feat(terminal): wait_for needle 支持 regex,found 返回实际匹配文本

needle 以 JavaScript 正则优先(编译失败回退字面匹配,旧字面 needle 完全兼容);found 结果新增 match 字段回报实际匹配文本——单个 needle 可覆盖多结局场景(如 (BUILD_OK|BUILD_FAIL) 同步构建成功/失败,match 区分命中分支);工具 description / needle 参数 / output schema / render 同步更新并写明多重匹配场景;新增 3 个 pty 级测试(alternation 命中分支、非法正则回退、元字符匹配);全量测试失败集合与干净 main 基线完全一致(27 个失败均为预存在 win32 非回归)
This commit is contained in:
HuanLinOTO
2026-09-08 23:07:07 +08:00
parent eecf9dc536
commit 3e408dbe48
4 changed files with 124 additions and 20 deletions
+11 -5
View File
@@ -287,11 +287,14 @@ export function registerTools(
register(defineTool({
name: 'terminal_wait_for',
description:
'Block until a substring appears in a terminal\'s retained transcript, or until the timeout elapses, or until the terminal exits — whichever happens first. '
'Block until a pattern appears in a terminal\'s retained transcript, or until the timeout elapses, or until the terminal exits — whichever happens first. '
+ 'Use this to synchronize on command completion cues ( e.g. a shell prompt, "done", "Listening on", "Build successful" ) '
+ 'without busy-polling terminal_read. '
+ 'The needle is a JavaScript regular expression ( a pattern that fails to compile falls back to verbatim substring matching ). '
+ 'One needle may cover MULTIPLE outcomes — e.g. wait on `(BUILD_OK|BUILD_FAIL)` or `Build (succeeded|failed)` returns as soon as EITHER marker appears, '
+ 'and the found result\'s `match` field tells which alternative hit ( build success vs failure ). '
+ 'The wait scans the FULL retained transcript (up to ~1 MiB) on every poll, so a needle that scrolled past the most recent chunk is still a match. '
+ 'Returns `found` with the line/column of the first occurrence, `timeout` if the needle did not appear in time, or `exited` if the terminal process died before the needle appeared. '
+ 'Returns `found` with the line/column and the matched text, `timeout` if the needle did not appear in time, or `exited` if the terminal process died before the needle appeared. '
+ 'Default timeout is 10 seconds; raise it for long-running commands ( dev servers, test suites ). '
+ 'The wait is cooperative: a tool-call cancel ( or agent turn end ) aborts it immediately.',
parameters: {
@@ -303,7 +306,8 @@ export function registerTools(
needle: {
type: 'string',
required: true,
description: 'Substring to wait for (case-sensitive, verbatim). Must be non-empty.',
description: 'JavaScript regular expression to wait for (case-sensitive); a pattern that fails to compile falls back to verbatim substring matching. '
+ 'May cover several outcomes in one wait ( e.g. `(BUILD_OK|BUILD_FAIL)` for build success/failure ) — check `match` in the found result to see which one hit. Must be non-empty.',
},
timeout_ms: {
type: 'number',
@@ -321,6 +325,7 @@ export function registerTools(
needle: { type: 'string', required: true },
line: { type: 'integer', required: true, description: '0-based line index in the retained transcript where the needle first appeared.' },
column: { type: 'integer', required: true, description: '0-based column index within that line where the match starts.' },
match: { type: 'string', required: true, description: 'The text that actually matched — for multi-outcome patterns ( e.g. `(BUILD_OK|BUILD_FAIL)` ) this tells which alternative matched.' },
elapsedMs: { type: 'integer', required: true, description: 'Wall-clock milliseconds from wait start to match.' },
},
},
@@ -347,9 +352,10 @@ export function registerTools(
],
},
render: (_args, value) => {
const v = value as { kind: 'found' | 'timeout' | 'exited'; needle: string; elapsedMs?: number; timeoutMs?: number; line?: number; column?: number; exitCode?: number | null; exitSignal?: string | null }
const v = value as { kind: 'found' | 'timeout' | 'exited'; needle: string; elapsedMs?: number; timeoutMs?: number; line?: number; column?: number; match?: string; exitCode?: number | null; exitSignal?: string | null }
if (v.kind === 'found') {
return [{ type: 'text', text: `Found "${v.needle}" at line ${v.line}, column ${v.column} (after ${v.elapsedMs}ms).` }]
const matched = v.match !== undefined && v.match !== '' ? `, matched "${v.match}"` : ''
return [{ type: 'text', text: `Found "${v.needle}" at line ${v.line}, column ${v.column}${matched} (after ${v.elapsedMs}ms).` }]
}
if (v.kind === 'timeout') {
return [{ type: 'text', text: `Timed out after ${v.timeoutMs}ms waiting for "${v.needle}". Call terminal_read to inspect the transcript.` }]