@thincoder/core 0.10.4 → 0.10.6
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/CHANGELOG.md +39 -1
- package/README.md +1 -1
- package/advisor/compaction.mjs +11 -6
- package/advisor/run.mjs +4 -4
- package/agent/assemble.mjs +1 -1
- package/agent/chat-call.mjs +1 -1
- package/agent/child-marks.mjs +2 -2
- package/agent/digest-account.mjs +5 -4
- package/agent/dispatch-gates.mjs +8 -0
- package/agent/dispatch.mjs +39 -18
- package/agent/family-tools.mjs +22 -11
- package/agent/helpers.mjs +4 -3
- package/agent/light-round.mjs +66 -0
- package/agent/post-turn.mjs +8 -4
- package/agent/run-stages.mjs +5 -3
- package/agent/run-start.mjs +3 -2
- package/agent/setup-reminders.mjs +1 -1
- package/agent/setup.mjs +18 -3
- package/agent/subagent-event.mjs +1 -1
- package/agent/suspension.mjs +8 -5
- package/agent/timers.mjs +56 -5
- package/agent/turn-loop.mjs +5 -3
- package/agent/write-gate.mjs +7 -7
- package/agent-tools/advisor-async.mjs +2 -2
- package/agent-tools/advisor-settle.mjs +2 -2
- package/agent-tools/advisor.mjs +1 -1
- package/agent-tools/async-discard.mjs +34 -0
- package/agent-tools/async-settle.mjs +3 -2
- package/agent-tools/audit-block.mjs +1 -1
- package/agent-tools/bash-async.mjs +1 -1
- package/agent-tools/batch-lifecycle.mjs +35 -14
- package/agent-tools/batch-paths.mjs +4 -4
- package/agent-tools/batch-skeleton.mjs +29 -11
- package/agent-tools/batch.mjs +28 -15
- package/agent-tools/browser-async.mjs +198 -0
- package/agent-tools/checkpoint.mjs +1 -1
- package/agent-tools/design-slots.mjs +232 -0
- package/agent-tools/digest-budget.mjs +1 -1
- package/agent-tools/manifest.mjs +193 -0
- package/agent-tools/parent-channel.mjs +7 -4
- package/agent-tools/review-facts.mjs +13 -13
- package/agent-tools/settings.mjs +1 -1
- package/agent-tools/skill.mjs +1 -1
- package/agent-tools/subagent-async.mjs +21 -12
- package/agent-tools/subagent-scheduler.mjs +8 -5
- package/agent-tools/subagent-spawn.mjs +29 -55
- package/agent-tools/subagent.mjs +21 -8
- package/agent-tools/timer.mjs +186 -12
- package/agent-tools/zero-write-watch.mjs +2 -2
- package/agent-tools.mjs +2 -1
- package/agent.mjs +1 -1
- package/attachments.mjs +1 -1
- package/browser/actions.mjs +163 -0
- package/browser/cdp.mjs +161 -0
- package/browser/clipboard.mjs +142 -0
- package/browser/input-actions.mjs +197 -0
- package/browser/input.mjs +256 -0
- package/browser/launch.mjs +143 -0
- package/browser/queue.mjs +171 -0
- package/browser/session.mjs +494 -0
- package/browser/snapshot.mjs +270 -0
- package/config-io.mjs +18 -13
- package/config-migrate.mjs +13 -29
- package/config-presets.mjs +33 -29
- package/config.mjs +68 -25
- package/context-degrade.mjs +1 -1
- package/context-echo.mjs +1 -1
- package/context-push.mjs +2 -2
- package/context.mjs +1 -1
- package/conventions.mjs +2 -2
- package/declaration.mjs +18 -2
- package/file-links.mjs +1 -1
- package/file-refs.mjs +1 -1
- package/history-window.mjs +1 -1
- package/i18n.mjs +2 -0
- package/ledger-cmd.mjs +62 -16
- package/ledger-db.mjs +6 -0
- package/ledger-surface.mjs +49 -5
- package/ledger-tools.mjs +16 -8
- package/ledger.mjs +9 -16
- package/log.mjs +1 -1
- package/manifest-discovery.mjs +1 -1
- package/manifest-schema.mjs +1 -1
- package/mcp/transport-stdio.mjs +1 -1
- package/mcp.mjs +1 -1
- package/memory/code-index.mjs +1 -1
- package/memory/code-search.mjs +1 -1
- package/memory/code-sync.mjs +1 -1
- package/memory/docs.mjs +6 -2
- package/memory/file-list.mjs +1 -1
- package/memory/file-walk.mjs +17 -5
- package/memory/maintain.mjs +410 -0
- package/memory/memory-tool.mjs +21 -6
- package/memory/schema.mjs +19 -7
- package/memory/sweep.mjs +8 -5
- package/memory/sync-tail.mjs +16 -10
- package/model-ref.mjs +16 -13
- package/model-specs.mjs +21 -9
- package/package.json +2 -1
- package/peer-instances.mjs +1 -1
- package/permission.mjs +33 -0
- package/process-probe.mjs +2 -2
- package/prompts/advisor-design.md +4 -2
- package/prompts/advisor-round1.md +1 -1
- package/prompts/advisor-round2.md +1 -1
- package/prompts/advisor-round3.md +1 -1
- package/prompts/common.md +36 -2
- package/prompts/consult-base.md +1 -1
- package/prompts/discipline-engineering.md +32 -15
- package/prompts/discipline-normal.md +46 -2
- package/prompts/persona-eng-designer.md +6 -6
- package/prompts/persona-engineering.md +14 -6
- package/prompts/persona-normal.md +2 -0
- package/prompts/subagent-base.md +3 -0
- package/provider/core.mjs +6 -0
- package/provider/errors.mjs +5 -3
- package/provider/google.mjs +9 -8
- package/provider/list-models.mjs +3 -2
- package/provider/sse.mjs +3 -0
- package/provider-flows.mjs +14 -15
- package/proxy-chunked.mjs +102 -0
- package/proxy-target.mjs +54 -0
- package/proxy-transport.mjs +298 -0
- package/proxy.mjs +17 -248
- package/session-gc.mjs +2 -2
- package/session-lifecycle.mjs +18 -14
- package/session-slot-claims.mjs +2 -2
- package/session-slots-manifest.mjs +1 -1
- package/session-slots.mjs +3 -3
- package/team-ledger.mjs +282 -0
- package/team.mjs +223 -0
- package/tool-docs/bash.md +1 -0
- package/tool-docs/batch.md +1 -1
- package/tool-docs/browser.md +48 -0
- package/tool-docs/ledger.md +3 -3
- package/tool-docs/manifest.md +16 -0
- package/tool-docs/memory.md +1 -0
- package/tool-docs/notify_parent.md +5 -0
- package/tool-docs/process.md +4 -4
- package/tool-docs/question.md +1 -0
- package/tool-docs/subagent.md +4 -3
- package/tool-docs/team_ledger.md +9 -0
- package/tool-docs/timer.md +6 -0
- package/tool-docs/wait_for.md +1 -0
- package/tools/browser.mjs +161 -0
- package/tools/git.mjs +1 -1
- package/tools/index.mjs +3 -0
- package/tools/ops.mjs +26 -10
- package/tools/repomap.mjs +1 -1
- package/tools/write-path.mjs +2 -2
- package/traces/trace-cleanup.mjs +1 -1
- package/traces/trace-store.mjs +1 -1
- package/vision-reader.mjs +10 -16
package/agent-tools/subagent.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* subagent.mjs — subagent tool(ONE tool,
|
|
2
|
+
* subagent.mjs — subagent tool(ONE tool, NINE actions + spawn 路径驱动器)。
|
|
3
3
|
* 2026-09-07 token 链终消费制:+action: consume-design(ENGINEERING-MODE.md §2.6 F1——
|
|
4
4
|
* 父侧链终核销消费 designId 槽——执行器 executeConsumeDesignAction 在 subagent-spawn.mjs)。
|
|
5
|
+
* 2026-10-07 批 ledger-tool(#927 并入):+action: design-slots(只读清点面——
|
|
6
|
+
* DESIGN-TOKEN-SETTLEMENT.md §10 F-SL1)+ consume-design 选择器扩展(designIds / expired /
|
|
7
|
+
* olderThanDays)——实现在 `design-slots.mjs`(subagent-spawn.mjs re-export 保 import 面)。
|
|
5
8
|
*
|
|
6
9
|
* 2026-09-03 拆分轮: subagent.mjs 超 500 硬顶——async 常量、共享 post-spawn 管线
|
|
7
10
|
*(runChildPipeline)与队列/注入/并账机械迁至 ./subagent-async.mjs。execute
|
|
@@ -31,6 +34,7 @@ import {
|
|
|
31
34
|
import { buildChildSignal } from "./async-settle.mjs"
|
|
32
35
|
import { executeStatusAction, executeEscalateAction, executePanelAction, executeObserveAction, executeSendAction } from "./subagent-actions.mjs"
|
|
33
36
|
import { prepareScheduling, buildSpawnChild, executeConsumeDesignAction } from "./subagent-spawn.mjs"
|
|
37
|
+
import { executeDesignSlotsAction } from "./design-slots.mjs"
|
|
34
38
|
import { executeAsyncSpawn } from "./subagent-run.mjs"
|
|
35
39
|
import { ROUND_VALUES } from "./spawn-gates.mjs"
|
|
36
40
|
// #15 描述外置:描述文本单点 = tool-docs/subagent.md(DESC 单解析面,缺档抛错语义不变)
|
|
@@ -102,14 +106,16 @@ export function buildSyncStoppedReport(role, capturedOutput, designId) {
|
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
/**
|
|
105
|
-
* subagent tool — ONE tool,
|
|
109
|
+
* subagent tool — ONE tool, NINE actions (AGENT-LOOP-SUBAGENT.md §6.7 +
|
|
106
110
|
* SUBAGENT-OBSERVE-SEND): spawn (default) / status (non-blocking pool query) / observe
|
|
107
111
|
* (inspect a running/queued/done async child's recent activity + current tool — docs/cli/design/TUI.md §6.8) /
|
|
108
112
|
* send (inject a direction into a RUNNING async child — consumed at its next turn
|
|
109
113
|
* boundary as an ordinary instruction — §6.8) / escalate (飞刀 — hand implementation to
|
|
110
114
|
* a stronger model) / cancel (stop ONE background subagent — AGENT-LOOP-SUBAGENT.md §6.7.2) / panel (view + fix
|
|
111
115
|
* the live subagent panel — AGENT-LOOP-SUBAGENT.md §6.7.2) / consume-design (parent-side chain-terminal token
|
|
112
|
-
* consumption — ENGINEERING-MODE.md §2.6, 2026-09-07
|
|
116
|
+
* consumption — ENGINEERING-MODE.md §2.6, 2026-09-07; selectors designId / designIds / expired /
|
|
117
|
+
* olderThanDays — DESIGN-TOKEN-SETTLEMENT.md §10 F-SL2) / design-slots (read-only slot inventory —
|
|
118
|
+
* DESIGN-TOKEN-SETTLEMENT.md §10 F-SL1, 2026-10-07). The check
|
|
113
119
|
* action was deleted (AGENT-LOOP-SUBAGENT.md §6.7.5): async results reach the model only via the auto channel.
|
|
114
120
|
* - action:"spawn" roles: "explore" — read-only tools, search/read/analyze
|
|
115
121
|
* (suitable for codebase exploration); "coder" — full tool set, self-contained
|
|
@@ -130,7 +136,7 @@ export const subagentTool = {
|
|
|
130
136
|
parameters: {
|
|
131
137
|
type: "object",
|
|
132
138
|
properties: {
|
|
133
|
-
action: { type: "string", enum: ["spawn", "status", "escalate", "cancel", "panel", "consume-design", "observe", "send"], description: "Which action — spawn (default) / status / observe / send / escalate / cancel / panel / consume-design; semantics per action in the tool description." },
|
|
139
|
+
action: { type: "string", enum: ["spawn", "status", "escalate", "cancel", "panel", "consume-design", "observe", "send", "design-slots"], description: "Which action — spawn (default) / status / observe / send / escalate / cancel / panel / consume-design / design-slots; semantics per action in the tool description." },
|
|
134
140
|
view: { type: "boolean", description: "action:'panel' only: true (default) = the live panel blocks as the user sees them; false without freeze = error. Mutually exclusive with freeze (freeze wins)." },
|
|
135
141
|
freeze: { type: "string", description: "action:'panel' only: block key of a digested-stuck block — reclaims it into the conversation; refused for running/done/unknown blocks or a still-pending report; works on the CLI TUI panel and on the desktop renderer panel (live readback of the blocks as the user sees them)." },
|
|
136
142
|
task: { type: "string", description: "Required for action:'spawn' (the self-contained task brief) and action:'escalate' (goal, constraints, entry files, acceptance criteria)." },
|
|
@@ -138,8 +144,11 @@ export const subagentTool = {
|
|
|
138
144
|
role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder", "eng-designer"], description: "The sub-agent role — see the role matrix in the tool description. Exact spelling required. action:'spawn' only (escalate spawns its own expert internally)." },
|
|
139
145
|
model: { type: "string", description: "action:'spawn': provider/model override ('provider:model', a provider, or a model name on the parent's provider) — defaults config.agent.subagentModels[role] → config.agent.subagentModel → the parent's provider; \"default\" inherits. action:'escalate': a consult candidate (default = the first)." },
|
|
140
146
|
designToken: { type: "string", description: "Required when role='eng-coder': the token from advisor(type='design') after the review passed — without it, eng-coder cannot modify files." },
|
|
141
|
-
designId: { type: "string", description: "role='eng-coder': the designId echoed with the approved token — required to pick between concurrent designs (optional for one). action:'consume-design': the slot
|
|
142
|
-
|
|
147
|
+
designId: { type: "string", description: "role='eng-coder': the designId echoed with the approved token — required to pick between concurrent designs (optional for one). action:'consume-design': the single-slot selector (required to pick between several; the gate refuses to guess); see designIds / expired / olderThanDays for batch consumption." },
|
|
148
|
+
designIds: { type: "array", items: { type: "string" }, description: "action:'consume-design' (batch): an explicit non-empty list of design ids to close out — pass exactly ONE selector per call (designId / designIds / expired / olderThanDays)." },
|
|
149
|
+
expired: { type: "boolean", description: "action:'consume-design' (batch): true = close out every expired slot (exactly ONE selector per call)." },
|
|
150
|
+
olderThanDays: { type: "number", description: "action:'consume-design' (batch): close out every slot older than N days (age > N; exactly ONE selector per call)." },
|
|
151
|
+
batchDoc: { type: "string", description: "REQUIRED for role='eng-coder'/'eng-designer': the batch record path (e.g. <repo>/docs/batches/<batch>-<topic>.md; relative paths resolve against the session cwd first, then candidate project roots — or absolute) — the task book this spawn implements/writes. Refused when absent or when the path does not resolve to a readable file; the content is never validated. role='coder' (normal mode): optional — binds this spawn's §5 record-write channel (unbound ⇒ the main agent records §5 on its behalf); when provided the path must resolve to a readable file." },
|
|
143
152
|
round: { type: "string", enum: ROUND_VALUES, description: "REQUIRED for role='eng-coder'/'eng-designer': initial (first round for this design) or fix (a correction round reusing the same designId+designToken; docs FIRST). No default — missing = refused." },
|
|
144
153
|
async: { type: "boolean", description: "action:'spawn'/'escalate': default at depth 0 = true (async; the report arrives automatically); depth>0 always sync. async:false forces the blocking flight." },
|
|
145
154
|
files: { type: "array", items: { type: "string" }, description: "action:'spawn' only: the file write-domain this task declares — file-level paths only (cwd-relative or absolute; directories rejected — they bypass the conflict detector). Overlapping-file tasks serialize automatically. Omit = no conflict detection." },
|
|
@@ -165,7 +174,7 @@ export const subagentTool = {
|
|
|
165
174
|
// coder+WRITE child (violates explore-only intent) and status/panel/observe/send
|
|
166
175
|
// have no async pool / panel mirror to query in a child context.
|
|
167
176
|
if ((ctx.depth ?? 0) > 0 && (ctx.agent?._role === "eng-coder" || ctx.agent?._role === "eng-designer")) {
|
|
168
|
-
throw new Error(`only action:'spawn' (sync explore children) is available inside an ${ctx.agent._role} — escalate/status/cancel/panel/consume-design/observe/send are not`)
|
|
177
|
+
throw new Error(`only action:'spawn' (sync explore children) is available inside an ${ctx.agent._role} — escalate/status/cancel/panel/consume-design/design-slots/observe/send are not`)
|
|
169
178
|
}
|
|
170
179
|
// AGENT-LOOP-ASYNC-POOL.md §6.8 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn —
|
|
171
180
|
// async OR blocking — the digest must stay organize-only. The escalate
|
|
@@ -183,6 +192,10 @@ export const subagentTool = {
|
|
|
183
192
|
// 非只读控制动作——depth-0 + 工程模式限定(本分流已过受限变体门;工程模式门在
|
|
184
193
|
// 执行器内)——planMode 拒绝(dispatch 不豁免)——不入批审批分组(dispatch 免审)。
|
|
185
194
|
if (action === "consume-design") return executeConsumeDesignAction(args, ctx)
|
|
195
|
+
// 2026-10-07 批 ledger-tool(#927 并入——DESIGN-TOKEN-SETTLEMENT.md §10 F-SL1):design-slots =
|
|
196
|
+
// 只读清点(工程模式父侧限定——门在执行器内);dispatch 分类 = readonly(planMode 放行 /
|
|
197
|
+
// 免审批 / digest 放行——同 status / observe)。
|
|
198
|
+
if (action === "design-slots") return executeDesignSlotsAction(args, ctx)
|
|
186
199
|
// AGENT-LOOP-SUBAGENT.md §6.7.2 panel 动作:view(readonly 面——digest 内放行——自省类)与 freeze
|
|
187
200
|
// (控制类——同 cancel——digest 内放行)。深度/门控检查在 executePanelAction 内。
|
|
188
201
|
// CLI-ACTIVITY-DEBLOAT F-3(2026-09-10)接线:executePanelAction 经 ctx.state
|
|
@@ -195,7 +208,7 @@ export const subagentTool = {
|
|
|
195
208
|
// send = 控制类豁免(同 cancel——父回合内显式调用即授权)。深度门在各自执行器内。
|
|
196
209
|
if (action === "observe") return executeObserveAction(args, ctx)
|
|
197
210
|
if (action === "send") return executeSendAction(args, ctx)
|
|
198
|
-
throw new Error(`Unknown subagent action: ${JSON.stringify(action)}. Valid actions: spawn, status, escalate, cancel, panel, consume-design, observe, send.`)
|
|
211
|
+
throw new Error(`Unknown subagent action: ${JSON.stringify(action)}. Valid actions: spawn, status, escalate, cancel, panel, consume-design, observe, send, design-slots.`)
|
|
199
212
|
}
|
|
200
213
|
|
|
201
214
|
const parent = ctx.agent
|
package/agent-tools/timer.mjs
CHANGED
|
@@ -1,10 +1,90 @@
|
|
|
1
|
-
import { TIMER_MAX_PENDING } from "../agent/timers.mjs"
|
|
1
|
+
import { TIMER_MAX_PENDING, TIMER_MIN_EVERY_MS, nextFireAt } from "../agent/timers.mjs"
|
|
2
2
|
import { DESC } from "../tools/shared.mjs"
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* timer tool: set a time budget for thinking before the agent insists on action.
|
|
6
|
-
* 描述面 = `tool-docs/timer.md`(DESC() 加载;机制 = AGENT-LOOP-ASYNC-POOL.md §6.30.3 D-TW8
|
|
6
|
+
* 描述面 = `tool-docs/timer.md`(DESC() 加载;机制 = AGENT-LOOP-ASYNC-POOL.md §6.30.3 D-TW8 ∥ §6.30.18
|
|
7
|
+
* 四式 + `cancel`/`list`)。取时单点 = 时钟缝 `ctx.now ?? Date.now()`(§6.30.18——生产 `toolCtx`
|
|
8
|
+
* (`agent/dispatch-run.mjs`)不携 `now` ⇒ 缺省回落 = 现行为零变;单元测试档以假 ctx 直驱)。
|
|
7
9
|
*/
|
|
10
|
+
|
|
11
|
+
/** `at` = 本地刻 `YYYY-MM-DDTHH:MM[:SS]`(严格正则+日历回读校验——如 `2026-02-30` 拒)。 */
|
|
12
|
+
const AT_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/
|
|
13
|
+
/** `daily` = 本地 `HH:MM`(与核 `DAILY_RE` 同形——工具面 = 唯一生产者,核守卫为防御面)。 */
|
|
14
|
+
const DAILY_RE = /^([01]\d|2[0-3]):([0-5]\d)$/
|
|
15
|
+
|
|
16
|
+
/** 默认 `message` 槽尾(逐式槽 = §6.30.18 默认表;一次性槽逐字零变)。 */
|
|
17
|
+
const DEFAULT_TAIL = ". Have you tried running the code, adding a console.log, or checking the output? Thinking more without data is guessing."
|
|
18
|
+
|
|
19
|
+
/** 本地刻 `YYYY-MM-DD HH:MM`(宿主本地时区——回执 ∥ 默认文案槽共用)。 */
|
|
20
|
+
function localText(ms) {
|
|
21
|
+
const d = new Date(ms)
|
|
22
|
+
const p = (n) => String(n).padStart(2, "0")
|
|
23
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `at` 解算(宿主本地时区):形不合 ∥ 日历回读不符(`2026-02-30` ∥ 时位越界等)⇒ `null`。 */
|
|
27
|
+
function atEpochMs(raw) {
|
|
28
|
+
const m = AT_RE.exec(raw)
|
|
29
|
+
if (m === null) return null
|
|
30
|
+
const [y, mo, d, hh, mi, ss] = [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4]), Number(m[5]), Number(m[6] ?? 0)]
|
|
31
|
+
const date = new Date(y, mo - 1, d, hh, mi, ss, 0)
|
|
32
|
+
const back = date.getFullYear() === y && date.getMonth() === mo - 1 && date.getDate() === d
|
|
33
|
+
&& date.getHours() === hh && date.getMinutes() === mi && date.getSeconds() === ss
|
|
34
|
+
return back ? date.getTime() : null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 时长形(§6.30.18):`<mm:ss>`(< 1h)∥ `<h:mm:ss>`(≥ 1h——h 不补零;与 `/timers` 行同判)。 */
|
|
38
|
+
function durText(ms) {
|
|
39
|
+
const sec = Math.max(0, Math.ceil(ms / 1000))
|
|
40
|
+
const h = Math.floor(sec / 3600)
|
|
41
|
+
const mm = String(Math.floor((sec % 3600) / 60)).padStart(2, "0")
|
|
42
|
+
const ss = String(sec % 60).padStart(2, "0")
|
|
43
|
+
return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** kind(`list` 行——§6.30.18):`every <N>s` ∕ `daily <HH:MM>` ∕ `at <刻>`(`at: true` 来源标记位——
|
|
47
|
+
* 父侧裁定 2026-10-12);余(一次性)⇒ `one-shot`。 */
|
|
48
|
+
function kindText(t) {
|
|
49
|
+
const everyMs = t?.everyMs
|
|
50
|
+
if (typeof everyMs === "number" && Number.isFinite(everyMs) && everyMs > 0) return `every ${everyMs / 1000}s`
|
|
51
|
+
const daily = t?.daily
|
|
52
|
+
if (typeof daily === "string" && DAILY_RE.test(daily)) return `daily ${daily}`
|
|
53
|
+
if (t?.at === true) return `at ${localText(t.expiresAt)}`
|
|
54
|
+
return "one-shot"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** `list` 行第三槽:到期未投 ⇒ `due now`;否则 `next <dur>`(剩余现算)。 */
|
|
58
|
+
function nextText(t, now) {
|
|
59
|
+
const at = Number(t?.expiresAt)
|
|
60
|
+
return at > now ? `next ${durText(at - now)}` : "due now"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `message` 首行(`list` 行第四槽——逐字原样,不截断)。 */
|
|
64
|
+
function firstLine(message) {
|
|
65
|
+
const text = String(message ?? "")
|
|
66
|
+
const i = text.indexOf("\n")
|
|
67
|
+
return i < 0 ? text : text.slice(0, i)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** `cancel`(§6.30.18):按 id 移除(`String` 宽松比对);零匹配 ⇒ 显式抛错(文案含 `list` 指引)。 */
|
|
71
|
+
function cancelAction(agent, wanted) {
|
|
72
|
+
const pending = agent?._pendingTimers ?? []
|
|
73
|
+
const idx = pending.findIndex((t) => String(t?.id) === String(wanted))
|
|
74
|
+
if (idx < 0) {
|
|
75
|
+
throw new Error(`timer cancel: no pending timer with id ${String(wanted)} — use list: true to see pending timers`)
|
|
76
|
+
}
|
|
77
|
+
const [hit] = pending.splice(idx, 1)
|
|
78
|
+
return `Timer cancelled (id ${String(hit.id)}).`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** `list`(只读枚举——剩余现算;到期未投 ⇒ `due now` 态照出)。 */
|
|
82
|
+
function listAction(agent, now) {
|
|
83
|
+
const pending = agent?._pendingTimers ?? []
|
|
84
|
+
if (pending.length === 0) return "No pending timers."
|
|
85
|
+
return pending.map((t) => `${String(t.id)} · ${kindText(t)} · ${nextText(t, now)} · ${firstLine(t.message)}`).join("\n")
|
|
86
|
+
}
|
|
87
|
+
|
|
8
88
|
export const timerTool = {
|
|
9
89
|
name: "timer",
|
|
10
90
|
description: DESC("timer"),
|
|
@@ -15,6 +95,26 @@ export const timerTool = {
|
|
|
15
95
|
type: "number",
|
|
16
96
|
description: "Thinking budget in seconds (default 180). Longer for complex reasoning, shorter for simple tasks.",
|
|
17
97
|
},
|
|
98
|
+
at: {
|
|
99
|
+
type: "string",
|
|
100
|
+
description: 'Fire once at a local clock time, e.g. "2026-10-13T09:00" (YYYY-MM-DDTHH:MM[:SS]). A time already past fires immediately.',
|
|
101
|
+
},
|
|
102
|
+
every: {
|
|
103
|
+
type: "number",
|
|
104
|
+
description: "Fixed interval in seconds (minimum 60): the first reminder comes N seconds after setting, then every N seconds until cancelled.",
|
|
105
|
+
},
|
|
106
|
+
daily: {
|
|
107
|
+
type: "string",
|
|
108
|
+
description: 'Local "HH:MM" — repeats every day at that time (a time already past today fires tomorrow), until cancelled.',
|
|
109
|
+
},
|
|
110
|
+
cancel: {
|
|
111
|
+
type: "string",
|
|
112
|
+
description: "Id of a pending timer to stop (from the set confirmation; see also list).",
|
|
113
|
+
},
|
|
114
|
+
list: {
|
|
115
|
+
type: "boolean",
|
|
116
|
+
description: "List pending timers with their next due time (read-only).",
|
|
117
|
+
},
|
|
18
118
|
message: {
|
|
19
119
|
type: "string",
|
|
20
120
|
description: "Custom reminder message to show when time is up. Default: a suggestion to add debug logs or run the code.",
|
|
@@ -25,22 +125,96 @@ export const timerTool = {
|
|
|
25
125
|
readonly: true,
|
|
26
126
|
sideEffectExempt: true,
|
|
27
127
|
execute(args, ctx) {
|
|
28
|
-
//
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
128
|
+
const now = ctx.now ?? Date.now() // 时钟缝单点(§6.30.18):at 过时判定 ∥ every/daily 首投换算 ∥ id 撞号基准
|
|
129
|
+
const agent = ctx.agent
|
|
130
|
+
|
|
131
|
+
// 动作解析 + 互斥(至多一式;cancel ∥ list 与一切设置式互斥、与彼此互斥)——违例显式拒。
|
|
132
|
+
const hasSeconds = args.seconds !== undefined && args.seconds !== null
|
|
133
|
+
const hasAt = args.at !== undefined && args.at !== null
|
|
134
|
+
const hasEvery = args.every !== undefined && args.every !== null
|
|
135
|
+
const hasDaily = args.daily !== undefined && args.daily !== null
|
|
136
|
+
const hasCancel = args.cancel !== undefined && args.cancel !== null
|
|
137
|
+
const wantsList = args.list === true
|
|
138
|
+
const forms = [hasSeconds && "seconds", hasAt && "at", hasEvery && "every", hasDaily && "daily"].filter((v) => v !== false)
|
|
139
|
+
if (forms.length > 1 || (forms.length > 0 && (hasCancel || wantsList)) || (hasCancel && wantsList)) {
|
|
140
|
+
const given = [...forms, hasCancel && "cancel", wantsList && "list"].filter((v) => v !== false).join(" / ")
|
|
141
|
+
throw new Error(
|
|
142
|
+
`timer: pass at most one action per call — got ${given}. Forms: { seconds } one-shot ∥ { at: "YYYY-MM-DDTHH:MM[:SS]" } absolute ∥ { every: <seconds ≥ 60> } interval ∥ { daily: "HH:MM" } daily ∥ { cancel: <id> } stop ∥ { list: true } list`,
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
if (hasCancel) return cancelAction(agent, args.cancel)
|
|
146
|
+
if (wantsList) return listAction(agent, now)
|
|
147
|
+
|
|
148
|
+
// 逐式解算(校验逐条显式抛错——不静默钳制;拒点时在途表零变动)
|
|
149
|
+
let form = "seconds"
|
|
150
|
+
let expiresAt = null
|
|
151
|
+
let message = null
|
|
152
|
+
let everySec = null
|
|
153
|
+
let everyMs = null
|
|
154
|
+
let oneShotSec = null
|
|
155
|
+
if (hasAt) {
|
|
156
|
+
form = "at"
|
|
157
|
+
const raw = args.at
|
|
158
|
+
const ts = typeof raw === "string" ? atEpochMs(raw) : null
|
|
159
|
+
if (ts === null) {
|
|
160
|
+
throw new Error(`timer at must be a local clock time "YYYY-MM-DDTHH:MM[:SS]" (e.g. "2026-10-13T09:00"), got: ${String(raw)}`)
|
|
161
|
+
}
|
|
162
|
+
expiresAt = ts // 过时点 ⇒ 所给刻(delay ≤ 0 ⇒ 即时投递——AC-45① 口径;不重排)
|
|
163
|
+
message = `⏰ Time's up (at ${localText(ts)})${DEFAULT_TAIL}`
|
|
164
|
+
} else if (hasEvery) {
|
|
165
|
+
form = "every"
|
|
166
|
+
everySec = Number(args.every)
|
|
167
|
+
if (!Number.isFinite(everySec) || everySec * 1000 < TIMER_MIN_EVERY_MS) {
|
|
168
|
+
throw new Error(`timer every must be a number of seconds ≥ ${TIMER_MIN_EVERY_MS / 1000} (got: ${String(args.every)})`)
|
|
169
|
+
}
|
|
170
|
+
everyMs = everySec * 1000
|
|
171
|
+
expiresAt = nextFireAt({ everyMs, expiresAt: now }, now) // 首投 = 设时 + N(非立即——防「设置即投」双投感)
|
|
172
|
+
message = `⏰ Time's up (every ${everySec} seconds)${DEFAULT_TAIL}`
|
|
173
|
+
} else if (hasDaily) {
|
|
174
|
+
form = "daily"
|
|
175
|
+
if (typeof args.daily !== "string" || !DAILY_RE.test(args.daily)) {
|
|
176
|
+
throw new Error(`timer daily must be a local "HH:MM" time (24-hour, e.g. "09:00"), got: ${String(args.daily)}`)
|
|
177
|
+
}
|
|
178
|
+
expiresAt = nextFireAt({ daily: args.daily }, now) // 今日该刻未过 ⇒ 今日;已过 ⇒ 明日(非即时)
|
|
179
|
+
message = `⏰ Time's up (daily at ${args.daily})${DEFAULT_TAIL}`
|
|
180
|
+
} else {
|
|
181
|
+
// D15.3#1:默认 180;非法值(非数字/非正数)显式报错——静默 NaN 定时器永不触发的坑
|
|
182
|
+
const seconds = Number(args.seconds ?? 180)
|
|
183
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
184
|
+
throw new Error(`timer seconds must be a positive number, got: ${args.seconds}`)
|
|
185
|
+
}
|
|
186
|
+
expiresAt = now + seconds * 1000
|
|
187
|
+
message = `⏰ Time's up (${seconds}s)${DEFAULT_TAIL}`
|
|
188
|
+
oneShotSec = seconds
|
|
32
189
|
}
|
|
33
|
-
const expiresAt = Date.now() + seconds * 1000
|
|
34
|
-
const message = args.message || `⏰ Time's up (${seconds}s). Have you tried running the code, adding a console.log, or checking the output? Thinking more without data is guessing.`
|
|
35
190
|
|
|
36
191
|
// §6.30.3 门三件②(D-TW6):在途帽 8——超限显式拒(不静默丢 / 不静默清;形态先例 = 队满
|
|
37
|
-
// 「拒 + 提示 +
|
|
38
|
-
const pending = (
|
|
192
|
+
// 「拒 + 提示 + 保留」)。周期项同占坑(1 条 1 坑——§6.30.18 D-TW15;重排净零不触帽)。
|
|
193
|
+
const pending = (agent._pendingTimers = agent._pendingTimers ?? [])
|
|
39
194
|
if (pending.length >= TIMER_MAX_PENDING) {
|
|
40
195
|
throw new Error(`timer: ${pending.length} timers already pending (max ${TIMER_MAX_PENDING}) — wait for one to fire before setting another`)
|
|
41
196
|
}
|
|
42
|
-
|
|
197
|
+
// id 唯一化(取消可寻址·§6.30.18):基准读时钟缝单点,与在途撞号 ⇒ `+1` 递增至空闲(零新载体字段)。
|
|
198
|
+
let id = now
|
|
199
|
+
while (pending.some((t) => String(t?.id) === String(id))) id += 1
|
|
200
|
+
const entry = { id, expiresAt, message: args.message || message }
|
|
201
|
+
if (everyMs !== null) entry.everyMs = everyMs
|
|
202
|
+
if (form === "daily") entry.daily = args.daily
|
|
203
|
+
if (form === "at") entry.at = true // 来源标记位(非载重——时刻单源 = `expiresAt`;父侧裁定 2026-10-12)
|
|
204
|
+
pending.push(entry)
|
|
43
205
|
|
|
44
|
-
|
|
206
|
+
if (form === "at") {
|
|
207
|
+
const when = localText(expiresAt)
|
|
208
|
+
return expiresAt > now
|
|
209
|
+
? `Timer set (id ${id}) for ${when} local. Stop it with cancel id ${id}.`
|
|
210
|
+
: `Timer set (id ${id}) for ${when} local — that time has passed, so the reminder appears immediately.`
|
|
211
|
+
}
|
|
212
|
+
if (form === "every") {
|
|
213
|
+
return `Repeating timer set (id ${id}): first reminder in ${everySec} seconds, then every ${everySec} seconds. Stop it with cancel id ${id}.`
|
|
214
|
+
}
|
|
215
|
+
if (form === "daily") {
|
|
216
|
+
return `Daily timer set (id ${id}): next reminder ${localText(expiresAt)} local, then every day at ${args.daily}. Stop it with cancel id ${id}.`
|
|
217
|
+
}
|
|
218
|
+
return `Timer set for ${oneShotSec} seconds. A reminder will appear when time is up.`
|
|
45
219
|
},
|
|
46
220
|
}
|
|
@@ -36,7 +36,7 @@ export function advisorAlertText(from, rounds) {
|
|
|
36
36
|
* 命中 ⇒ `pushChildUpstream({parent, from: _upstream.label, kind: "note", message})` + 置闩。
|
|
37
37
|
* **恒零抛**(兜底捕获兑现——自动源无模型可报错;失败面静默跳过)。
|
|
38
38
|
* @param {object} child 子 agent(eng-coder / eng-designer 形态;`_upstream` 由 spawn 站点装配)。
|
|
39
|
-
* @returns {boolean}
|
|
39
|
+
* @returns {boolean} 是否命中推送(测试)。
|
|
40
40
|
*/
|
|
41
41
|
export function maybeZeroWriteAlert(child) {
|
|
42
42
|
try {
|
|
@@ -65,7 +65,7 @@ export function maybeZeroWriteAlert(child) {
|
|
|
65
65
|
* @param {object} parent 父 agent(接收方——`pushChildUpstream` 载体吸收同款)。
|
|
66
66
|
* @param {string} from 来源标签(`advisor#<entry id>`)。
|
|
67
67
|
* @param {number} rounds 连续无产出轮数。
|
|
68
|
-
* @returns {boolean}
|
|
68
|
+
* @returns {boolean} 是否推送成功(测试)。
|
|
69
69
|
*/
|
|
70
70
|
export function pushAdvisorAlert(parent, from, rounds) {
|
|
71
71
|
try {
|
package/agent-tools.mjs
CHANGED
|
@@ -13,13 +13,14 @@ export { recentChangesTool } from "./agent-tools/recent-changes.mjs"
|
|
|
13
13
|
export { timerTool } from "./agent-tools/timer.mjs"
|
|
14
14
|
export { advisorTool } from "./agent-tools/advisor.mjs"
|
|
15
15
|
export { engTool } from "./agent-tools/eng.mjs"
|
|
16
|
+
export { manifestTool } from "./agent-tools/manifest.mjs"
|
|
16
17
|
export { readHistoryTool } from "./agent-tools/read-history.mjs"
|
|
17
18
|
// 模型主动整理上下文(CONTEXT-COMPACTION.md §6.16 · F-CC5):单工具三操作 stats / prune / compact
|
|
18
19
|
// ——depth-0 段挂载(子代理面裁见 §6.16.6 `family-tools.mjs` `depthOnly`)。
|
|
19
20
|
export { contextTool } from "./agent-tools/context.mjs"
|
|
20
21
|
export { batchTool } from "./agent-tools/batch.mjs"
|
|
21
22
|
// 批次档生命周期工具(BATCH-RECORD §4.3 挂载表——主名 `batch` 单工具四 action:depth-0 主 agent
|
|
22
|
-
// create/close + append §1/§4/§6
|
|
23
|
+
// create/close + append/status 域随模式(工程面 = append §1/§4/§6 ∥ status §1;普通面 = append §1/§2/§4/§5/§6 ∥ status §1/§2/§5)(轮 2 裁定②:§4/§6 状态面走普通文档写)(D-BR18);
|
|
23
24
|
// eng 子代理 eng 分支绑定段写入;评审 §3。
|
|
24
25
|
// CORE-UNIFICATION TOOLS #83(统一登记册——VSC `agent-tools/index.mjs:15` 含 consult 家族;
|
|
25
26
|
// CLI 原把 consult 另挂 `agent/setup.mjs:173` ⇒ 归位:登记册即单一来源,装配方只读本档)。
|
package/agent.mjs
CHANGED
|
@@ -91,7 +91,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
91
91
|
try {
|
|
92
92
|
return await runTurnLoop(agent, {
|
|
93
93
|
maxTurns, threshold, toolSchemas, toolByName, systemPrompt,
|
|
94
|
-
depth, signal, autoTurn, streamOutput, consumeInjected, consumeQueuedInput, callbacks, distillSignal,
|
|
94
|
+
depth, signal, autoTurn, suspDriven, streamOutput, consumeInjected, consumeQueuedInput, callbacks, distillSignal,
|
|
95
95
|
})
|
|
96
96
|
} catch (e) {
|
|
97
97
|
thrownError = e
|
package/attachments.mjs
CHANGED
|
@@ -64,7 +64,7 @@ export function isNonVisionModel(model) {
|
|
|
64
64
|
*/
|
|
65
65
|
export function savePastedImages(dataUrls, cwd, { fs } = {}) {
|
|
66
66
|
const list = Array.isArray(dataUrls) ? dataUrls : []
|
|
67
|
-
if (list.length === 0) return { paths: [], dropped: 0 } //
|
|
67
|
+
if (list.length === 0) return { paths: [], dropped: 0 } // 空表先于缝校验早退(零判据 —— 缝不检)
|
|
68
68
|
if (typeof fs?.mkdirSync !== "function" || typeof fs?.writeFileSync !== "function") {
|
|
69
69
|
throw new TypeError("savePastedImages: fs seam missing — inject { mkdirSync, writeFileSync } (the write face must not silently drop every image)")
|
|
70
70
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browser/actions.mjs — 基线八动作实现(navigate / snapshot / click / type / evaluate / wait / screenshot / close)
|
|
3
|
+
* (BROWSER-TOOL.md §2.2 ∥ §2.4;KD-10:click / type 内部选型**不迁** Input 域——对外语义零回归)。
|
|
4
|
+
*
|
|
5
|
+
* 会话能力一律经句柄取(`h.call` / `h.evalRaw` / `h.takeSnapshot` / `h.ensureSession` / `h.pageError` /
|
|
6
|
+
* `h.step` / `h.sleep` …)——本档不 import `session.mjs`(DAG 单向,§5 拆分决定)。
|
|
7
|
+
* 硬超时(§2.9):wait 的谓词帽(`timeoutMs`:缺省 30s · 上限 120s)语义零变——动作预算 = 谓词帽 + 15s
|
|
8
|
+
* 余量(`queue.mjs` 预算表);轮询循环标 `wait poll` 步名(卡点可诊断——N-BT10)。
|
|
9
|
+
*/
|
|
10
|
+
import { DEFAULT_MAX, clickExpression, typeExpression, truncateText, waitExpression } from "./snapshot.mjs"
|
|
11
|
+
|
|
12
|
+
export const WAIT_TIMEOUT_DEFAULT = 30_000
|
|
13
|
+
export const WAIT_TIMEOUT_MAX = 120_000
|
|
14
|
+
export const WAIT_POLL_MS = 150
|
|
15
|
+
export const NETWORK_QUIET_MS = 500
|
|
16
|
+
export const MAX_EVAL_CHARS = 8_000
|
|
17
|
+
|
|
18
|
+
function normalizeHttpUrl(value) {
|
|
19
|
+
const raw = String(value ?? "")
|
|
20
|
+
let url
|
|
21
|
+
try { url = new URL(raw) } catch { url = null }
|
|
22
|
+
if (!url || (url.protocol !== "http:" && url.protocol !== "https:")) {
|
|
23
|
+
throw new Error(`navigate requires an http/https url — got "${raw}"`)
|
|
24
|
+
}
|
|
25
|
+
return url.toString()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function jsonOf(value) {
|
|
29
|
+
if (value === undefined) return "undefined"
|
|
30
|
+
try { return JSON.stringify(value) ?? String(value) }
|
|
31
|
+
catch { return String(value) }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** navigate(§2.2):http/https 限;回执 = `[navigate] <final-url>` + 内联新页快照(省一轮 snapshot)。 */
|
|
35
|
+
async function actNavigate(args, ctx, h) {
|
|
36
|
+
const url = normalizeHttpUrl(args.url)
|
|
37
|
+
h.assertHostAllowed(url, ctx) // 静态参数面先检——被拦的 host 不得起浏览器(§3.2「不执行」)
|
|
38
|
+
await h.ensureSession(args)
|
|
39
|
+
await h.call("Page.navigate", { url })
|
|
40
|
+
await h.waitForReady()
|
|
41
|
+
const snap = await h.takeSnapshot({ max: DEFAULT_MAX })
|
|
42
|
+
return `[navigate] ${snap.url}\n${snap.text}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** snapshot(§2.2):引用清单(生成 / 复用 / 标记——§2.3)。 */
|
|
46
|
+
async function actSnapshot(args, ctx, h) {
|
|
47
|
+
await h.ensureSession(args)
|
|
48
|
+
const snap = await h.takeSnapshot({ selector: args.selector ?? null, max: args.max ?? DEFAULT_MAX })
|
|
49
|
+
return snap.text
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** click(§2.2):DOM 级语义激活(`el.click()`——KD-10);导航判据 = URL 变化 ⇒ `[navigated]` / `[no navigation]`。 */
|
|
53
|
+
async function actClick(args, ctx, h) {
|
|
54
|
+
await h.ensureSession(args)
|
|
55
|
+
const info = await h.pageInfo()
|
|
56
|
+
h.assertHostAllowed(info.url, ctx)
|
|
57
|
+
const entry = h.resolveRef(args.ref)
|
|
58
|
+
const res = await h.evalRaw(clickExpression(entry.selector, entry.tag))
|
|
59
|
+
if (!res?.found || res.tag) throw h.pageError(`ref ${args.ref} is stale (was "${entry.name}") — run snapshot again`)
|
|
60
|
+
if (res.disabled) throw h.pageError(`ref ${args.ref} is disabled ("${entry.name}")`)
|
|
61
|
+
const out = [`[click] ${args.ref} "${entry.name}"`]
|
|
62
|
+
const landed = await h.waitForUrlChange(info.url)
|
|
63
|
+
if (landed) {
|
|
64
|
+
await h.waitForReady()
|
|
65
|
+
const snap = await h.takeSnapshot({ max: DEFAULT_MAX })
|
|
66
|
+
out.push(`[navigated] ${snap.url}`, snap.text)
|
|
67
|
+
} else {
|
|
68
|
+
out.push("[no navigation]")
|
|
69
|
+
}
|
|
70
|
+
return out.join("\n")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** type(§2.2):写值(原生 value setter + input/change);password 目标 ⇒ 值不回显(N-BT4)。 */
|
|
74
|
+
async function actType(args, ctx, h) {
|
|
75
|
+
await h.ensureSession(args)
|
|
76
|
+
const info = await h.pageInfo()
|
|
77
|
+
h.assertHostAllowed(info.url, ctx)
|
|
78
|
+
const entry = h.resolveRef(args.ref)
|
|
79
|
+
const text = String(args.text)
|
|
80
|
+
const res = await h.evalRaw(typeExpression(entry.selector, entry.tag, text, args.clear === true))
|
|
81
|
+
if (!res?.found || res.tag) throw h.pageError(`ref ${args.ref} is stale (was "${entry.name}") — run snapshot again`)
|
|
82
|
+
if (res.fillable === false) {
|
|
83
|
+
throw h.pageError(`ref ${args.ref} is not a fillable field ("${entry.name}", <${entry.tag}>) — use snapshot to pick an input/textarea`)
|
|
84
|
+
}
|
|
85
|
+
if (res.disabled) throw h.pageError(`ref ${args.ref} is disabled ("${entry.name}")`)
|
|
86
|
+
return `[type] ${args.ref} "${entry.name}" ← ${text.length} chars${res.password ? " (hidden)" : ""}`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** evaluate(§2.2):页面上下文 JS;异常 ⇒ `Error: evaluate failed: <message>`;回执受 8000 字符上限(N-BT3)。 */
|
|
90
|
+
async function actEvaluate(args, ctx, h) {
|
|
91
|
+
await h.ensureSession(args)
|
|
92
|
+
const info = await h.pageInfo()
|
|
93
|
+
h.assertHostAllowed(info.url, ctx)
|
|
94
|
+
let value
|
|
95
|
+
try {
|
|
96
|
+
value = await h.evalRaw(String(args.expression))
|
|
97
|
+
} catch (e) {
|
|
98
|
+
if (e?.browserAbort || e?.sessionLost) throw e // 预算中止 / 外部关闭 ⇒ 原样上报(不裹进页面异常句)
|
|
99
|
+
throw h.pageError(`evaluate failed: ${e?.message ?? e}`)
|
|
100
|
+
}
|
|
101
|
+
return `[evaluate @ ${info.url}] ${truncateText(jsonOf(value), MAX_EVAL_CHARS, "narrow the expression")}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function waitPredicate(args) {
|
|
105
|
+
if (args.networkIdle === true) return { kind: "networkIdle", value: null, label: "networkIdle" }
|
|
106
|
+
for (const kind of ["selector", "text", "url"]) {
|
|
107
|
+
const value = args[kind]
|
|
108
|
+
if (typeof value === "string" && value !== "") return { kind, value, label: `${kind} ${JSON.stringify(value)}` }
|
|
109
|
+
}
|
|
110
|
+
// 工具层已校验互斥(§2.2)——此处兜底(直调会话面时同样不悬挂)
|
|
111
|
+
throw new Error("wait requires exactly one of selector|text|url|networkIdle")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** wait(§2.2):四选一谓词 + 150ms 轮询;networkIdle = 事件在飞归零后静默 500ms。 */
|
|
115
|
+
async function actWait(args, ctx, h) {
|
|
116
|
+
await h.ensureSession(args)
|
|
117
|
+
const pred = waitPredicate(args)
|
|
118
|
+
const wanted = Number(args.timeoutMs)
|
|
119
|
+
const timeoutMs = Math.max(1, Math.min(Number.isFinite(wanted) && wanted > 0 ? wanted : WAIT_TIMEOUT_DEFAULT, WAIT_TIMEOUT_MAX))
|
|
120
|
+
const started = Date.now()
|
|
121
|
+
return h.step("wait poll", async () => {
|
|
122
|
+
for (;;) {
|
|
123
|
+
if (pred.kind === "networkIdle") {
|
|
124
|
+
const net = h.networkState()
|
|
125
|
+
if (net.inflight.size === 0 && Date.now() - net.lastEventAt >= NETWORK_QUIET_MS) break
|
|
126
|
+
} else if (await h.evalRaw(waitExpression(pred.kind, pred.value)) === true) break
|
|
127
|
+
if (Date.now() - started >= timeoutMs) throw h.pageError(`wait timed out after ${timeoutMs}ms (${pred.label})`)
|
|
128
|
+
await h.sleep(WAIT_POLL_MS)
|
|
129
|
+
}
|
|
130
|
+
return `[wait] ${pred.label} — ok after ${Date.now() - started}ms`
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** screenshot(§2.2):PNG 落盘返路径(KD-6——模型经 read_image 查看)。 */
|
|
135
|
+
async function actScreenshot(args, ctx, h) {
|
|
136
|
+
await h.ensureSession(args)
|
|
137
|
+
const shot = await h.call("Page.captureScreenshot", {
|
|
138
|
+
format: "png",
|
|
139
|
+
...(args.fullPage === true ? { captureBeyondViewport: true } : {}),
|
|
140
|
+
})
|
|
141
|
+
if (typeof shot?.data !== "string") throw new Error("screenshot failed: the browser returned no image data")
|
|
142
|
+
const saved = await h.saveShot(Buffer.from(shot.data, "base64"))
|
|
143
|
+
return `[screenshot] ${saved.path} (${saved.bytes} bytes)`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** close(§2.2):幂等——无会话 ⇒ `[close] no browser session`(成功态)。 */
|
|
147
|
+
async function actClose(args, ctx, h) {
|
|
148
|
+
if (!h.connection()) return "[close] no browser session"
|
|
149
|
+
await h.closeSession()
|
|
150
|
+
return "[close] browser session closed"
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 基线八动作分发表(会话档并入总表——§5)。 */
|
|
154
|
+
export const ACTIONS = {
|
|
155
|
+
navigate: actNavigate,
|
|
156
|
+
snapshot: actSnapshot,
|
|
157
|
+
click: actClick,
|
|
158
|
+
type: actType,
|
|
159
|
+
evaluate: actEvaluate,
|
|
160
|
+
wait: actWait,
|
|
161
|
+
screenshot: actScreenshot,
|
|
162
|
+
close: actClose,
|
|
163
|
+
}
|