@yuzc-001/grasp 0.6.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/LICENSE +21 -0
- package/README.md +327 -0
- package/README.zh-CN.md +324 -0
- package/examples/README.md +31 -0
- package/examples/claude-desktop.json +8 -0
- package/examples/codex-config.toml +4 -0
- package/grasp.skill +0 -0
- package/index.js +87 -0
- package/package.json +48 -0
- package/scripts/grasp_openclaw_ctl.sh +122 -0
- package/scripts/run-search-benchmark.mjs +287 -0
- package/scripts/update-star-history.mjs +274 -0
- package/skill/SKILL.md +61 -0
- package/skill/references/tools.md +306 -0
- package/src/cli/auto-configure.js +116 -0
- package/src/cli/cmd-connect.js +148 -0
- package/src/cli/cmd-explain.js +42 -0
- package/src/cli/cmd-logs.js +55 -0
- package/src/cli/cmd-status.js +119 -0
- package/src/cli/config.js +27 -0
- package/src/cli/detect-chrome.js +58 -0
- package/src/grasp/handoff/events.js +67 -0
- package/src/grasp/handoff/persist.js +48 -0
- package/src/grasp/handoff/state.js +28 -0
- package/src/grasp/page/capture.js +34 -0
- package/src/grasp/page/state.js +273 -0
- package/src/grasp/verify/evidence.js +40 -0
- package/src/grasp/verify/pipeline.js +52 -0
- package/src/layer1-bridge/chrome.js +416 -0
- package/src/layer1-bridge/webmcp.js +143 -0
- package/src/layer2-perception/hints.js +284 -0
- package/src/layer3-action/actions.js +400 -0
- package/src/runtime/browser-instance.js +65 -0
- package/src/runtime/truth/model.js +94 -0
- package/src/runtime/truth/snapshot.js +51 -0
- package/src/server/affordances.js +47 -0
- package/src/server/audit.js +122 -0
- package/src/server/boss-fast-path.js +164 -0
- package/src/server/boundary-guard.js +53 -0
- package/src/server/content.js +97 -0
- package/src/server/continuity.js +256 -0
- package/src/server/engine-selection.js +29 -0
- package/src/server/entry-orchestrator.js +115 -0
- package/src/server/error-codes.js +7 -0
- package/src/server/explain-share-card.js +113 -0
- package/src/server/fast-path-router.js +134 -0
- package/src/server/form-runtime.js +602 -0
- package/src/server/form-tasks.js +254 -0
- package/src/server/gateway-response.js +62 -0
- package/src/server/index.js +22 -0
- package/src/server/observe.js +52 -0
- package/src/server/page-projection.js +31 -0
- package/src/server/page-state.js +27 -0
- package/src/server/postconditions.js +128 -0
- package/src/server/prompt-assembly.js +148 -0
- package/src/server/responses.js +44 -0
- package/src/server/route-boundary.js +174 -0
- package/src/server/route-policy.js +168 -0
- package/src/server/runtime-confirmation.js +87 -0
- package/src/server/runtime-status.js +7 -0
- package/src/server/share-artifacts.js +284 -0
- package/src/server/state.js +132 -0
- package/src/server/structured-extraction.js +131 -0
- package/src/server/surface-prompts.js +166 -0
- package/src/server/task-frame.js +11 -0
- package/src/server/tasks/search-task.js +321 -0
- package/src/server/tools.actions.js +1361 -0
- package/src/server/tools.form.js +526 -0
- package/src/server/tools.gateway.js +757 -0
- package/src/server/tools.handoff.js +210 -0
- package/src/server/tools.js +20 -0
- package/src/server/tools.legacy.js +983 -0
- package/src/server/tools.strategy.js +250 -0
- package/src/server/tools.task-surface.js +66 -0
- package/src/server/tools.workspace.js +873 -0
- package/src/server/workspace-runtime.js +1138 -0
- package/src/server/workspace-tasks.js +735 -0
- package/start-chrome.bat +84 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const BOUNDARY_PROMPT_PACKS = {
|
|
2
|
+
public_read: {
|
|
3
|
+
id: 'public_read',
|
|
4
|
+
instructions: [
|
|
5
|
+
'Operate as a read-first agent inside the current Grasp runtime boundary.',
|
|
6
|
+
'Keep the flow on inspect/extract style tools until the runtime evidence shows a different surface.',
|
|
7
|
+
'Do not mutate the page or jump into form/workspace actions from a readable public page.',
|
|
8
|
+
],
|
|
9
|
+
},
|
|
10
|
+
live_session: {
|
|
11
|
+
id: 'live_session',
|
|
12
|
+
instructions: [
|
|
13
|
+
'Operate inside the current authenticated browser session instead of replaying from scratch.',
|
|
14
|
+
'Inspect the live page first and wait for clear evidence before switching into a specialized surface.',
|
|
15
|
+
'Reuse the current session state and avoid speculative deep actions.',
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
session_warmup: {
|
|
19
|
+
id: 'session_warmup',
|
|
20
|
+
instructions: [
|
|
21
|
+
'Treat this as a trust-warmup path, not a direct action path.',
|
|
22
|
+
'Preheat the session, then re-enter once the runtime trust improves.',
|
|
23
|
+
'Avoid blind retries that keep re-triggering the same low-trust entry.',
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
form_runtime: {
|
|
27
|
+
id: 'form_runtime',
|
|
28
|
+
instructions: [
|
|
29
|
+
'Operate as a guarded form agent.',
|
|
30
|
+
'Inspect first, fill safe text fields conservatively, and keep review-tier controls explicit.',
|
|
31
|
+
'Submit only through safe_submit with the required confirmation gate.',
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
workspace_runtime: {
|
|
35
|
+
id: 'workspace_runtime',
|
|
36
|
+
instructions: [
|
|
37
|
+
'Operate as a guarded workspace agent on the current live item.',
|
|
38
|
+
'Keep selection stable, draft before execute, and verify outcome after every execution.',
|
|
39
|
+
'Never bypass the guarded workspace tools with raw send-like actions.',
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
handoff: {
|
|
43
|
+
id: 'handoff',
|
|
44
|
+
instructions: [
|
|
45
|
+
'Operate as a handoff coordinator rather than a direct actor.',
|
|
46
|
+
'Stop blind retries, persist the blocked step, and wait for the human recovery action.',
|
|
47
|
+
'Resume only after the runtime evidence shows the page has been reacquired.',
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const SURFACE_PROMPT_PACKS = {
|
|
53
|
+
public_content: {
|
|
54
|
+
id: 'public_content',
|
|
55
|
+
instructions: [
|
|
56
|
+
'Surface: readable public content.',
|
|
57
|
+
'Prefer inspect, extract, extract_structured, extract_batch, share_page, and continue.',
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
public_search: {
|
|
61
|
+
id: 'public_search',
|
|
62
|
+
instructions: [
|
|
63
|
+
'Surface: public search or navigation-heavy content.',
|
|
64
|
+
'Stay conservative and extract the visible result state before deeper actions.',
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
live_auth_session: {
|
|
68
|
+
id: 'live_auth_session',
|
|
69
|
+
instructions: [
|
|
70
|
+
'Surface: authenticated session shell without a specialized task surface yet.',
|
|
71
|
+
'Use inspect, continue, and explain_route until form or workspace evidence becomes explicit.',
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
live_runtime_surface: {
|
|
75
|
+
id: 'live_runtime_surface',
|
|
76
|
+
instructions: [
|
|
77
|
+
'Surface: generic live runtime page.',
|
|
78
|
+
'Preserve the current session context and avoid premature specialization.',
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
session_reentry: {
|
|
82
|
+
id: 'session_reentry',
|
|
83
|
+
instructions: [
|
|
84
|
+
'Surface: warmup and re-entry.',
|
|
85
|
+
'Focus on trust recovery and re-entry rather than content extraction or page mutation.',
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
form_surface: {
|
|
89
|
+
id: 'form_surface',
|
|
90
|
+
instructions: [
|
|
91
|
+
'Surface: visible form.',
|
|
92
|
+
'Match labels conservatively, avoid blind writes, and keep sensitive fields out of automatic writes.',
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
form_review_required: {
|
|
96
|
+
id: 'form_review_required',
|
|
97
|
+
instructions: [
|
|
98
|
+
'Surface: form still requires review.',
|
|
99
|
+
'Resolve blockers and review-tier fields first; do not confirm submit while the form still needs review.',
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
form_ready_to_submit: {
|
|
103
|
+
id: 'form_ready_to_submit',
|
|
104
|
+
instructions: [
|
|
105
|
+
'Surface: form appears ready to submit.',
|
|
106
|
+
'Keep the final step explicit and use safe_submit with the required confirmation string.',
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
workspace_surface: {
|
|
110
|
+
id: 'workspace_surface',
|
|
111
|
+
instructions: [
|
|
112
|
+
'Surface: authenticated workspace.',
|
|
113
|
+
'Inspect the visible live items and composer state before taking guarded actions.',
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
workspace_list: {
|
|
117
|
+
id: 'workspace_list',
|
|
118
|
+
instructions: [
|
|
119
|
+
'Surface: workspace list.',
|
|
120
|
+
'Select the live item first; do not draft or execute until the target item is stable.',
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
workspace_detail: {
|
|
124
|
+
id: 'workspace_detail',
|
|
125
|
+
instructions: [
|
|
126
|
+
'Surface: workspace detail panel.',
|
|
127
|
+
'Keep the selected item aligned with the detail view before drafting or executing.',
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
workspace_thread: {
|
|
131
|
+
id: 'workspace_thread',
|
|
132
|
+
instructions: [
|
|
133
|
+
'Surface: workspace thread.',
|
|
134
|
+
'Draft in the active composer, keep the active item stable, and execute only through execute_action.',
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
workspace_composer: {
|
|
138
|
+
id: 'workspace_composer',
|
|
139
|
+
instructions: [
|
|
140
|
+
'Surface: focused composer.',
|
|
141
|
+
'Draft safely, keep send guarded, and verify the result after any execution.',
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
workspace_loading_shell: {
|
|
145
|
+
id: 'workspace_loading_shell',
|
|
146
|
+
instructions: [
|
|
147
|
+
'Surface: loading shell.',
|
|
148
|
+
'Re-inspect after the workspace stabilizes and avoid acting while the shell is still loading.',
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
checkpoint_handoff: {
|
|
152
|
+
id: 'checkpoint_handoff',
|
|
153
|
+
instructions: [
|
|
154
|
+
'Surface: gated checkpoint or blocker.',
|
|
155
|
+
'Request handoff, stop direct action loops, and wait for the human recovery step.',
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export function getBoundaryPromptPack(key) {
|
|
161
|
+
return key ? BOUNDARY_PROMPT_PACKS[key] ?? null : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function getSurfacePromptPack(key) {
|
|
165
|
+
return key ? SURFACE_PROMPT_PACKS[key] ?? null : null;
|
|
166
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { getActivePage } from '../../layer1-bridge/chrome.js';
|
|
2
|
+
import { typeByHintId, clickByHintId, pressKey } from '../../layer3-action/actions.js';
|
|
3
|
+
import { extractMainContent, waitUntilStable } from '../content.js';
|
|
4
|
+
import { observeSearchSnapshot } from '../observe.js';
|
|
5
|
+
import { createTaskFrame } from '../task-frame.js';
|
|
6
|
+
import { syncPageState } from '../state.js';
|
|
7
|
+
import { rebindHintCandidate } from '../../layer2-perception/hints.js';
|
|
8
|
+
import { NO_EFFECT, LOADING_PENDING, EXECUTION_FAILED } from '../error-codes.js';
|
|
9
|
+
|
|
10
|
+
const INPUT_HINT_TYPES = new Set(['searchbox', 'textbox', 'combobox', 'input', 'textarea']);
|
|
11
|
+
|
|
12
|
+
function isInputLikeHint(hint = {}) {
|
|
13
|
+
if (INPUT_HINT_TYPES.has(hint.type)) return true;
|
|
14
|
+
if (hint.meta?.contenteditable === true || hint.meta?.contenteditable === 'true') return true;
|
|
15
|
+
if (hint.meta?.tag === 'input' || hint.meta?.tag === 'textarea') return true;
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function hydrateHintCandidate(candidate, snapshot) {
|
|
20
|
+
if (!candidate?.id) return candidate ?? null;
|
|
21
|
+
const fullHint = (snapshot?.hints ?? []).find((hint) => hint.id === candidate.id);
|
|
22
|
+
return fullHint ? { ...fullHint, ...candidate, meta: { ...(fullHint.meta ?? {}), ...(candidate.meta ?? {}) } } : candidate;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveSearchHint(snapshot, frame) {
|
|
26
|
+
const ranked = snapshot?.ranking?.search_input ?? [];
|
|
27
|
+
const rankedCandidate = ranked
|
|
28
|
+
.map((hint) => hydrateHintCandidate(hint, snapshot))
|
|
29
|
+
.find(isInputLikeHint);
|
|
30
|
+
if (rankedCandidate) return rankedCandidate;
|
|
31
|
+
|
|
32
|
+
const boundId = frame.semanticBindings.get('search_input');
|
|
33
|
+
const boundHint = (snapshot?.hints ?? []).find((hint) => hint.id === boundId);
|
|
34
|
+
if (isInputLikeHint(boundHint)) return boundHint;
|
|
35
|
+
|
|
36
|
+
return (snapshot?.hints ?? []).find(isInputLikeHint) ?? null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function chooseSearchPlan(snapshot, frame) {
|
|
40
|
+
const searchHint = resolveSearchHint(snapshot, frame);
|
|
41
|
+
const submitHint = snapshot?.submitCandidate;
|
|
42
|
+
let mode = 'primary_submit';
|
|
43
|
+
if (frame.nextRecovery === 'alternate_submit') {
|
|
44
|
+
mode = 'alternate_submit';
|
|
45
|
+
} else if (frame.nextRecovery === 'reobserve') {
|
|
46
|
+
mode = 'reobserve';
|
|
47
|
+
}
|
|
48
|
+
frame.nextRecovery = null;
|
|
49
|
+
const plan = {
|
|
50
|
+
query: snapshot?.query ?? '',
|
|
51
|
+
mode,
|
|
52
|
+
searchInputHintId: searchHint?.id ?? frame.semanticBindings.get('search_input') ?? null,
|
|
53
|
+
submitHintId: submitHint?.id ?? frame.semanticBindings.get('submit_control') ?? null,
|
|
54
|
+
};
|
|
55
|
+
if (plan.searchInputHintId) {
|
|
56
|
+
frame.semanticBindings.set('search_input', plan.searchInputHintId);
|
|
57
|
+
}
|
|
58
|
+
if (plan.submitHintId) {
|
|
59
|
+
frame.semanticBindings.set('submit_control', plan.submitHintId);
|
|
60
|
+
}
|
|
61
|
+
return plan;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function applyRecovery(frame, verdict) {
|
|
65
|
+
if (!verdict?.error_code) return;
|
|
66
|
+
if (verdict.error_code === NO_EFFECT) {
|
|
67
|
+
frame.nextRecovery = 'alternate_submit';
|
|
68
|
+
} else if (verdict.error_code === LOADING_PENDING) {
|
|
69
|
+
frame.nextRecovery = 'wait_then_reverify';
|
|
70
|
+
} else {
|
|
71
|
+
frame.nextRecovery = 'reobserve';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function countExecutionToolCalls(execution) {
|
|
76
|
+
if (!execution) return 0;
|
|
77
|
+
if (typeof execution.toolCalls === 'number') {
|
|
78
|
+
return execution.toolCalls;
|
|
79
|
+
}
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function finalizeResult(frame, status, plan, verdict) {
|
|
84
|
+
const attempts = status === 'failed' ? frame.attempts : frame.attempts + 1;
|
|
85
|
+
const toolCalls = frame.history.reduce(
|
|
86
|
+
(sum, entry) => sum + countExecutionToolCalls(entry.execution),
|
|
87
|
+
0
|
|
88
|
+
);
|
|
89
|
+
const recovered = status === 'completed' && frame.history.slice(0, -1).some((entry) => entry.verdict && !entry.verdict.ok);
|
|
90
|
+
return {
|
|
91
|
+
taskId: frame.taskId,
|
|
92
|
+
status,
|
|
93
|
+
attempts,
|
|
94
|
+
toolCalls,
|
|
95
|
+
retries: Math.max(attempts - 1, 0),
|
|
96
|
+
plan,
|
|
97
|
+
verdict,
|
|
98
|
+
frame,
|
|
99
|
+
recovered,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function stripTrace(result) {
|
|
104
|
+
const { frame, ...publicResult } = result;
|
|
105
|
+
return publicResult;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function createRebuildHints(page, state) {
|
|
109
|
+
return async (hintId) => {
|
|
110
|
+
const previousHint = state.hintMap.find((hint) => hint.id === hintId);
|
|
111
|
+
await syncPageState(page, state, { force: true });
|
|
112
|
+
if (!previousHint) return null;
|
|
113
|
+
return rebindHintCandidate(previousHint, state.hintMap);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function verifySearchOutcome({
|
|
118
|
+
page,
|
|
119
|
+
state,
|
|
120
|
+
plan,
|
|
121
|
+
snapshot,
|
|
122
|
+
deps = {},
|
|
123
|
+
}) {
|
|
124
|
+
const prevDomRevision = snapshot?.domRevision ?? 0;
|
|
125
|
+
const prevUrl = snapshot?.url ?? (await page.url());
|
|
126
|
+
const prevContent = snapshot?.content?.text ?? '';
|
|
127
|
+
const syncState = deps.syncState ?? syncPageState;
|
|
128
|
+
await syncState(page, state, { force: true });
|
|
129
|
+
const currentDomRevision = state.pageState.domRevision;
|
|
130
|
+
const currentUrl = page.url();
|
|
131
|
+
const readyState = await page.evaluate(() => document.readyState);
|
|
132
|
+
const extractContent = deps.extractContent ?? extractMainContent;
|
|
133
|
+
const newContent = (await extractContent(page)).text;
|
|
134
|
+
const evidence = {
|
|
135
|
+
domRevision: currentDomRevision,
|
|
136
|
+
url: currentUrl,
|
|
137
|
+
content: newContent,
|
|
138
|
+
readyState,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (
|
|
142
|
+
currentDomRevision !== prevDomRevision ||
|
|
143
|
+
currentUrl !== prevUrl ||
|
|
144
|
+
newContent !== prevContent
|
|
145
|
+
) {
|
|
146
|
+
return { ok: true, evidence };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (readyState !== 'complete') {
|
|
150
|
+
return { ok: false, error_code: LOADING_PENDING, evidence };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { ok: false, error_code: NO_EFFECT, evidence };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function executeSearchTask({
|
|
157
|
+
query,
|
|
158
|
+
observer,
|
|
159
|
+
executor,
|
|
160
|
+
verifier,
|
|
161
|
+
waitThenReverify,
|
|
162
|
+
maxAttempts = 3,
|
|
163
|
+
taskId,
|
|
164
|
+
}) {
|
|
165
|
+
const frame = createTaskFrame({
|
|
166
|
+
taskId: taskId ?? `search-${Date.now()}`,
|
|
167
|
+
kind: 'search_task',
|
|
168
|
+
maxAttempts,
|
|
169
|
+
});
|
|
170
|
+
const waitFn = typeof waitThenReverify === 'function'
|
|
171
|
+
? waitThenReverify
|
|
172
|
+
: async () => undefined;
|
|
173
|
+
|
|
174
|
+
for (; frame.attempts < frame.maxAttempts; frame.attempts += 1) {
|
|
175
|
+
const observerResult = await observer({ query, frame });
|
|
176
|
+
const snapshot = observerResult?.snapshot ?? observerResult;
|
|
177
|
+
const plan = chooseSearchPlan(snapshot, frame);
|
|
178
|
+
const execution = await executor(plan);
|
|
179
|
+
const verdict = await verifier({ plan, execution, snapshot, frame });
|
|
180
|
+
frame.history.push({ snapshot, plan, execution, verdict });
|
|
181
|
+
|
|
182
|
+
if (verdict.ok) {
|
|
183
|
+
return finalizeResult(frame, 'completed', plan, verdict);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (verdict.error_code === LOADING_PENDING) {
|
|
187
|
+
await waitFn({ plan, snapshot, frame, query });
|
|
188
|
+
const retryVerdict = await verifier({ plan, execution, snapshot, frame, retry: true });
|
|
189
|
+
frame.history.push({ phase: 'wait_then_reverify', plan, verdict: retryVerdict });
|
|
190
|
+
if (retryVerdict.ok) {
|
|
191
|
+
return finalizeResult(frame, 'completed', plan, retryVerdict);
|
|
192
|
+
}
|
|
193
|
+
applyRecovery(frame, retryVerdict);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
applyRecovery(frame, verdict);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return finalizeResult(frame, 'failed', null, null);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function runSearchTask(options) {
|
|
204
|
+
const result = await executeSearchTask(options);
|
|
205
|
+
return stripTrace(result);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function runSearchTaskTool({
|
|
209
|
+
state,
|
|
210
|
+
query,
|
|
211
|
+
max_attempts = 3,
|
|
212
|
+
deps = {},
|
|
213
|
+
}) {
|
|
214
|
+
const {
|
|
215
|
+
getActivePage: getActivePageOverride,
|
|
216
|
+
observer: observerOverride,
|
|
217
|
+
executor: executorOverride,
|
|
218
|
+
verifier: verifierOverride,
|
|
219
|
+
waitThenReverify: waitThenReverifyOverride,
|
|
220
|
+
waitStableAction: waitStableActionOverride,
|
|
221
|
+
extractContentAction: extractContentActionOverride,
|
|
222
|
+
typeAction: typeActionOverride,
|
|
223
|
+
clickAction: clickActionOverride,
|
|
224
|
+
pressKeyAction: pressKeyActionOverride,
|
|
225
|
+
syncStateAction: syncStateActionOverride,
|
|
226
|
+
} = deps;
|
|
227
|
+
|
|
228
|
+
const getPage = getActivePageOverride ?? getActivePage;
|
|
229
|
+
const waitStableAction = waitStableActionOverride ?? waitUntilStable;
|
|
230
|
+
const extractContentAction = extractContentActionOverride ?? extractMainContent;
|
|
231
|
+
const typeAction = typeActionOverride ?? typeByHintId;
|
|
232
|
+
const clickAction = clickActionOverride ?? clickByHintId;
|
|
233
|
+
const pressKeyAction = pressKeyActionOverride ?? pressKey;
|
|
234
|
+
const syncStateAction = syncStateActionOverride ?? syncPageState;
|
|
235
|
+
|
|
236
|
+
const page = await getPage();
|
|
237
|
+
const rebuildHints = createRebuildHints(page, state);
|
|
238
|
+
let observer = observerOverride;
|
|
239
|
+
if (!observer) {
|
|
240
|
+
observer = ({ frame }) => observeSearchSnapshot({
|
|
241
|
+
page,
|
|
242
|
+
state,
|
|
243
|
+
query,
|
|
244
|
+
frame,
|
|
245
|
+
deps: { waitStable: waitStableAction, extractContent: extractContentAction },
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let executor = executorOverride;
|
|
250
|
+
if (!executor) {
|
|
251
|
+
executor = async (plan) => {
|
|
252
|
+
if (!plan.searchInputHintId) {
|
|
253
|
+
return { ok: false, error_code: EXECUTION_FAILED };
|
|
254
|
+
}
|
|
255
|
+
let actionCount = 0;
|
|
256
|
+
try {
|
|
257
|
+
if (plan.mode === 'primary_submit') {
|
|
258
|
+
await typeAction(page, plan.searchInputHintId, query, true, { rebuildHints });
|
|
259
|
+
actionCount += 1;
|
|
260
|
+
} else if (plan.mode === 'alternate_submit') {
|
|
261
|
+
await typeAction(page, plan.searchInputHintId, query, false, { rebuildHints });
|
|
262
|
+
actionCount += 1;
|
|
263
|
+
if (plan.submitHintId) {
|
|
264
|
+
await clickAction(page, plan.submitHintId, { rebuildHints });
|
|
265
|
+
actionCount += 1;
|
|
266
|
+
} else {
|
|
267
|
+
await pressKeyAction(page, 'Enter');
|
|
268
|
+
actionCount += 1;
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
await pressKeyAction(page, 'Enter');
|
|
272
|
+
actionCount += 1;
|
|
273
|
+
}
|
|
274
|
+
await syncStateAction(page, state, { force: true });
|
|
275
|
+
return { ok: true, toolCalls: actionCount };
|
|
276
|
+
} catch (err) {
|
|
277
|
+
return {
|
|
278
|
+
ok: false,
|
|
279
|
+
error_code: EXECUTION_FAILED,
|
|
280
|
+
toolCalls: actionCount,
|
|
281
|
+
evidence: { message: err.message },
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let verifier = verifierOverride;
|
|
288
|
+
if (!verifier) {
|
|
289
|
+
verifier = ({ plan, snapshot }) =>
|
|
290
|
+
verifySearchOutcome({
|
|
291
|
+
page,
|
|
292
|
+
state,
|
|
293
|
+
plan,
|
|
294
|
+
snapshot,
|
|
295
|
+
deps: {
|
|
296
|
+
extractContent: extractContentAction,
|
|
297
|
+
syncState: syncStateAction,
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let waitThenReverify = waitThenReverifyOverride;
|
|
303
|
+
if (!waitThenReverify) {
|
|
304
|
+
waitThenReverify = async () => {
|
|
305
|
+
await waitStableAction(page, { stableChecks: 2, interval: 120, timeout: 2000 });
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const result = await executeSearchTask({
|
|
310
|
+
query,
|
|
311
|
+
observer,
|
|
312
|
+
executor,
|
|
313
|
+
verifier,
|
|
314
|
+
waitThenReverify,
|
|
315
|
+
maxAttempts: max_attempts,
|
|
316
|
+
taskId: `search-tool-${Date.now()}`,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
state.taskFrames.set(result.frame.taskId, result.frame);
|
|
320
|
+
return stripTrace(result);
|
|
321
|
+
}
|