@mercuryo-ai/agentbrowse 0.2.61 → 0.2.63
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 +33 -1
- package/README.md +102 -9
- package/dist/browser-session-state.d.ts +2 -11
- package/dist/browser-session-state.d.ts.map +1 -1
- package/dist/browser-session-state.js +0 -4
- package/dist/commands/act.d.ts.map +1 -1
- package/dist/commands/act.js +14 -5
- package/dist/commands/attach.d.ts +1 -3
- package/dist/commands/attach.d.ts.map +1 -1
- package/dist/commands/attach.js +0 -2
- package/dist/commands/browser-status.d.ts +0 -2
- package/dist/commands/browser-status.d.ts.map +1 -1
- package/dist/commands/browser-status.js +1 -7
- package/dist/commands/interaction-kernel.d.ts +1 -1
- package/dist/commands/interaction-kernel.d.ts.map +1 -1
- package/dist/commands/interaction-kernel.js +1 -1
- package/dist/commands/launch.d.ts +0 -1
- package/dist/commands/launch.d.ts.map +1 -1
- package/dist/commands/launch.js +0 -4
- package/dist/commands/observe-accessibility.d.ts.map +1 -1
- package/dist/commands/observe-accessibility.js +36 -2
- package/dist/commands/observe-inventory.d.ts +49 -7
- package/dist/commands/observe-inventory.d.ts.map +1 -1
- package/dist/commands/observe-inventory.js +807 -96
- package/dist/commands/observe-persistence.d.ts.map +1 -1
- package/dist/commands/observe-persistence.js +49 -6
- package/dist/commands/observe-projection.d.ts +6 -2
- package/dist/commands/observe-projection.d.ts.map +1 -1
- package/dist/commands/observe-projection.js +251 -27
- package/dist/commands/observe-semantics.d.ts +1 -0
- package/dist/commands/observe-semantics.d.ts.map +1 -1
- package/dist/commands/observe-semantics.js +541 -135
- package/dist/commands/observe-signals.d.ts +4 -4
- package/dist/commands/observe-signals.d.ts.map +1 -1
- package/dist/commands/observe-signals.js +2 -2
- package/dist/commands/observe-surfaces.d.ts +2 -1
- package/dist/commands/observe-surfaces.d.ts.map +1 -1
- package/dist/commands/observe-surfaces.js +143 -45
- package/dist/commands/observe.d.ts +5 -1
- package/dist/commands/observe.d.ts.map +1 -1
- package/dist/commands/observe.js +15 -11
- package/dist/commands/semantic-observe.d.ts.map +1 -1
- package/dist/commands/semantic-observe.js +43 -0
- package/dist/library.d.ts +2 -1
- package/dist/library.d.ts.map +1 -1
- package/dist/library.js +2 -1
- package/dist/match-resolve-fill.d.ts +196 -0
- package/dist/match-resolve-fill.d.ts.map +1 -0
- package/dist/match-resolve-fill.js +700 -0
- package/dist/match-resolve-fill.test-support.d.ts +34 -0
- package/dist/match-resolve-fill.test-support.d.ts.map +1 -0
- package/dist/match-resolve-fill.test-support.js +81 -0
- package/dist/runtime-protected-state.d.ts.map +1 -1
- package/dist/runtime-protected-state.js +12 -0
- package/dist/runtime-state.d.ts +6 -0
- package/dist/runtime-state.d.ts.map +1 -1
- package/dist/runtime-state.js +6 -0
- package/dist/secrets/form-matcher.d.ts.map +1 -1
- package/dist/secrets/form-matcher.js +76 -27
- package/dist/secrets/protected-exact-value-redaction.d.ts.map +1 -1
- package/dist/secrets/protected-exact-value-redaction.js +6 -0
- package/dist/secrets/protected-fill.js +3 -3
- package/dist/session.d.ts +3 -3
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +2 -2
- package/dist/solver/browser-launcher.d.ts.map +1 -1
- package/dist/solver/browser-launcher.js +2 -1
- package/dist/testing.d.ts +1 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +1 -0
- package/docs/README.md +28 -11
- package/docs/api-reference.md +311 -19
- package/docs/assistive-runtime.md +41 -16
- package/docs/getting-started.md +45 -1
- package/docs/integration-checklist.md +32 -3
- package/docs/match-resolve-fill.md +699 -0
- package/docs/protected-fill.md +373 -91
- package/docs/testing.md +147 -15
- package/docs/troubleshooting.md +5 -0
- package/examples/README.md +7 -0
- package/examples/match-resolve-fill.ts +107 -0
- package/package.json +4 -2
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
# Match / Resolve / Fill Guide
|
|
2
|
+
|
|
3
|
+
`match`, `resolve`, and `fill` are the data-plane primitives on top of the
|
|
4
|
+
AgentBrowse browser runtime. They exist to answer one deceptively simple
|
|
5
|
+
question: **you have some key–value pairs in your runtime, and you want
|
|
6
|
+
them to end up in the right fields of a form on the page — how do you do
|
|
7
|
+
that cleanly?**
|
|
8
|
+
|
|
9
|
+
This guide builds the model from first principles. It assumes you have
|
|
10
|
+
already seen [`observe(...)`](./api-reference.md#observesession-goal) and
|
|
11
|
+
[`act(...)`](./api-reference.md#actsession-targetref-action-value); if
|
|
12
|
+
not, read [Getting Started](./getting-started.md) first.
|
|
13
|
+
|
|
14
|
+
## The Problem
|
|
15
|
+
|
|
16
|
+
You run `observe(session)` and you get back a page inventory: some
|
|
17
|
+
observed targets (inputs, buttons), some fillable forms. In parallel,
|
|
18
|
+
your runtime holds candidate values — from a profile, a vault, the
|
|
19
|
+
current task context. Somehow you have to decide which value goes in
|
|
20
|
+
which field, and then write it into the browser.
|
|
21
|
+
|
|
22
|
+
The low-level primitive for writing is
|
|
23
|
+
`act(session, targetRef, 'fill', value)`. It takes exactly one target
|
|
24
|
+
ref and exactly one value string, and it fills the input. That's it. No
|
|
25
|
+
decision logic, no lookup logic.
|
|
26
|
+
|
|
27
|
+
If all you ever need is "I already know this target takes this value",
|
|
28
|
+
`act(...)` is fine. Real integrations hit three concerns it does not
|
|
29
|
+
help with:
|
|
30
|
+
|
|
31
|
+
1. **Which candidate wins?** Your runtime has several values that could
|
|
32
|
+
plausibly belong in an observed field. Picking one by hand is fine
|
|
33
|
+
when there is a single correspondence (`email → email input`) but
|
|
34
|
+
becomes brittle when there are multiple candidates, host-specific
|
|
35
|
+
applicability rules, or protected targets that must be excluded from
|
|
36
|
+
open-data filling.
|
|
37
|
+
2. **What if the value is not in hand yet?** Passwords, payment cards,
|
|
38
|
+
and anything behind user approval do not live in your process memory.
|
|
39
|
+
Something has to go fetch them — and ideally your fetch logic should
|
|
40
|
+
not be duplicated across every field that needs it.
|
|
41
|
+
3. **What about values that must not touch the LLM prompt?** In agent
|
|
42
|
+
stacks, the orchestrating model sees everything in the prompt and in
|
|
43
|
+
the result objects it receives. Passing raw passwords through those
|
|
44
|
+
objects defeats the whole «kept out of the model» property that makes
|
|
45
|
+
vaults like MagicPay useful.
|
|
46
|
+
|
|
47
|
+
The three primitives in this guide address those three concerns in
|
|
48
|
+
order: **`match`** decides, **`resolve`** fetches, **`fill`** applies.
|
|
49
|
+
|
|
50
|
+
## The Three Primitives
|
|
51
|
+
|
|
52
|
+
Read them top-to-bottom — that is also the order you call them.
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
observe() ──▶ target / fillableForm
|
|
56
|
+
│
|
|
57
|
+
▼
|
|
58
|
+
match(subject, { from })
|
|
59
|
+
│
|
|
60
|
+
├── { kind: 'ready', valueRef } ────────┐
|
|
61
|
+
├── { kind: 'needs_resolution', plan } ─┤
|
|
62
|
+
└── ambiguous / no_match / group-variants
|
|
63
|
+
│ │
|
|
64
|
+
▼ │
|
|
65
|
+
resolve(plan, { with }) │
|
|
66
|
+
│ │
|
|
67
|
+
└── { kind: 'ready', valueRef }┤
|
|
68
|
+
│
|
|
69
|
+
▼
|
|
70
|
+
fill(session, subject, plan)
|
|
71
|
+
│
|
|
72
|
+
▼
|
|
73
|
+
act(...) on the browser
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- **`match(subject, { from })`** — takes an observed target or fillable
|
|
77
|
+
form plus a source of candidate values, returns a structured outcome:
|
|
78
|
+
a ready reference (when the value is already in hand), a plan (when
|
|
79
|
+
external resolution is needed), or an explicit ambiguity / no-match
|
|
80
|
+
signal. Pure, local, no network.
|
|
81
|
+
- **`resolve(plan, { with })`** — takes a plan (or an array of plans)
|
|
82
|
+
and an adapter, and walks each plan through your
|
|
83
|
+
`AgentbrowseMatchResolver` to produce a ready reference. This is the
|
|
84
|
+
one step where *your* code talks to a vault, approval UI, secret
|
|
85
|
+
manager, or any other external source of truth.
|
|
86
|
+
- **`fill(session, subject, plan, { resolver? })`** — applies a ready
|
|
87
|
+
plan to the browser. It's the only step that dereferences the value
|
|
88
|
+
(more on that below) and the only step that drives a DOM action.
|
|
89
|
+
|
|
90
|
+
If you want one sentence to remember: **match is pure; resolve owns the
|
|
91
|
+
adapter; fill owns the browser.**
|
|
92
|
+
|
|
93
|
+
## Walk-through 1 — value in hand
|
|
94
|
+
|
|
95
|
+
The simplest case: you already have the value in memory, and you want to
|
|
96
|
+
fill one observed input with it.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { match, resolve, fill } from '@mercuryo-ai/agentbrowse';
|
|
100
|
+
|
|
101
|
+
const observed = await observe(session);
|
|
102
|
+
if (!observed.success) throw new Error(observed.reason ?? observed.message);
|
|
103
|
+
|
|
104
|
+
const emailTarget = observed.targets.find((t) => t.inputType === 'email');
|
|
105
|
+
if (!emailTarget) return;
|
|
106
|
+
|
|
107
|
+
const matched = await match(emailTarget, {
|
|
108
|
+
from: { email: 'traveler@example.com' },
|
|
109
|
+
});
|
|
110
|
+
// matched is { kind: 'ready', targetRef, fieldKey: 'email', valueRef, confidence }.
|
|
111
|
+
|
|
112
|
+
await fill(session, emailTarget, matched);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
What happened, step by step:
|
|
116
|
+
|
|
117
|
+
- `match` saw one observed target (the email input) and one candidate
|
|
118
|
+
coming from `{ email: '…' }`. AgentBrowse scored the candidate against
|
|
119
|
+
the target (label, `inputType`, `autocomplete`, etc.). High confidence
|
|
120
|
+
and no ambiguity, so it returned `kind: 'ready'`.
|
|
121
|
+
- The ready result carries a `valueRef` — a stable opaque string, not
|
|
122
|
+
the email address itself. `JSON.stringify(matched)` does not contain
|
|
123
|
+
`traveler@example.com`. The value is held inside a non-enumerable
|
|
124
|
+
accessor on the result object.
|
|
125
|
+
- `fill` used that accessor internally to dereference the value and
|
|
126
|
+
called `act(session, emailTarget.ref, 'fill', 'traveler@example.com')`.
|
|
127
|
+
- The final result is the regular `ActResult` shape (same contract as a
|
|
128
|
+
direct `act(...)` call), so you can branch on `success` / `error` the
|
|
129
|
+
usual way.
|
|
130
|
+
|
|
131
|
+
If `match` had returned something other than `ready` (for example,
|
|
132
|
+
`ambiguous` because two candidates competed), `fill` would have
|
|
133
|
+
short-circuited with a typed contract failure instead of guessing. See
|
|
134
|
+
[Failure Contract](#failure-contract) below.
|
|
135
|
+
|
|
136
|
+
### Candidate sources
|
|
137
|
+
|
|
138
|
+
The `from` option is the same shape in every walk-through. It accepts
|
|
139
|
+
four forms; pick whichever fits how your runtime already holds values:
|
|
140
|
+
|
|
141
|
+
| Source | Shape | When to use |
|
|
142
|
+
| --- | --- | --- |
|
|
143
|
+
| Plain record | `{ [fieldKey]: value }` | You have a handful of open values in memory — fastest path. |
|
|
144
|
+
| Candidate array | `ReadonlyArray<AgentbrowseMatchCandidate>` | You want full control: `applicability` rules, `semanticTags`, or a `resolve` plan per entry. |
|
|
145
|
+
| `AgentbrowseMatchStore` | `{ entries(), read(candidateRef) }` | You do not want the value to sit inside the candidate list — the store lists metadata and only dereferences values through `read(candidateRef)`. |
|
|
146
|
+
| Group variants | array or store of `AgentbrowseGroupMatchCandidate` | The subject is a fillable form instead of a single target. See [Walk-through 5](#walk-through-5--grouped-protected-forms). |
|
|
147
|
+
|
|
148
|
+
The first three all produce single-field matches. Mix them as you like
|
|
149
|
+
across calls; the result shape is always the same tagged union.
|
|
150
|
+
|
|
151
|
+
### How `match` decides
|
|
152
|
+
|
|
153
|
+
You just saw a successful single-field match. It is useful to understand
|
|
154
|
+
at a conceptual level how the matcher picked the candidate, because the
|
|
155
|
+
inputs you feed it — candidate metadata, applicability rules, target
|
|
156
|
+
metadata — directly shape the result. You do not compute scores by
|
|
157
|
+
hand; what you control is how much signal you hand the matcher.
|
|
158
|
+
|
|
159
|
+
The matcher runs in two phases: a **filter** phase that drops
|
|
160
|
+
candidates that cannot possibly apply, and a **score** phase that picks
|
|
161
|
+
a winner from what remains.
|
|
162
|
+
|
|
163
|
+
#### Filter phase
|
|
164
|
+
|
|
165
|
+
Every one of these filters is hard — if a candidate fails the check,
|
|
166
|
+
it's out. The matcher returns `no_match` with the corresponding reason
|
|
167
|
+
when all candidates drop.
|
|
168
|
+
|
|
169
|
+
- **Protected-target guard.** If the caller passed
|
|
170
|
+
`protectedTargetRefs` and the target's ref is in that set, the matcher
|
|
171
|
+
refuses everything. This is how open-data flows avoid writing values
|
|
172
|
+
into a password or card input. Reason: `protected_target`.
|
|
173
|
+
- **At least one candidate.** Empty source → reason: `no_candidate`.
|
|
174
|
+
- **Applicability.** A candidate declares
|
|
175
|
+
`applicability: { target: 'global' }` (applies anywhere) or
|
|
176
|
+
`{ target: 'host', value: 'example.com' }` (applies only when
|
|
177
|
+
`options.host` matches). Host-scoped candidates that do not match the
|
|
178
|
+
current host drop. Reason when zero remain: `scope_ineligible`.
|
|
179
|
+
- **Shape compatibility.** The candidate's value type must be
|
|
180
|
+
compatible with the target's inferred type. An email input only
|
|
181
|
+
accepts candidates whose value looks like an email (or whose declared
|
|
182
|
+
`type` is `email` / `text`). Dates need ISO `YYYY-MM-DD`, numbers
|
|
183
|
+
need numeric values, URLs need `http(s)://` prefixes. **Secret-typed
|
|
184
|
+
candidates (`type: 'secret'`) are always rejected** — secrets must
|
|
185
|
+
flow through the `resolve` plan path in Walk-through 2, never through
|
|
186
|
+
open-data matching. Reason when zero remain: `incompatible_shape`.
|
|
187
|
+
|
|
188
|
+
#### Score phase
|
|
189
|
+
|
|
190
|
+
From the candidates that survived filtering, the matcher ranks by an
|
|
191
|
+
evidence score combining signal strength on both sides:
|
|
192
|
+
|
|
193
|
+
- **On the target side:** `label`, `displayLabel`, `placeholder`,
|
|
194
|
+
`inputName`, `inputType`, `autocomplete`, and other accessibility
|
|
195
|
+
signals collected by `observe(...)`. The matcher looks at direct
|
|
196
|
+
signals first (high weight) and broader context signals with a
|
|
197
|
+
small penalty.
|
|
198
|
+
- **On the candidate side:** `fieldKey` (expanded into an evidence
|
|
199
|
+
vocabulary — for example, `fieldKey: 'email'` also scores against
|
|
200
|
+
terms like `"e-mail"`, `"email address"`, etc.), `label`, and any
|
|
201
|
+
`semanticTags` you provide.
|
|
202
|
+
|
|
203
|
+
Richer candidates win ambiguous cases. The most impactful thing you
|
|
204
|
+
control here is the candidate's metadata — providing `label` and
|
|
205
|
+
`semanticTags` alongside `fieldKey` gives the matcher more handles.
|
|
206
|
+
|
|
207
|
+
On top of the evidence score, two priority boosts break ties:
|
|
208
|
+
|
|
209
|
+
- **Host-scoped candidates** (`applicability: { target: 'host', value: <host> }`)
|
|
210
|
+
get a small boost over global candidates at the same evidence score.
|
|
211
|
+
Use this when the same field has different values on different sites.
|
|
212
|
+
- **Session-local values** (candidates with `source: 'session_open_value'`
|
|
213
|
+
— only relevant when you construct `ObservedFieldCandidate` objects
|
|
214
|
+
via the lower-level API) beat profile-fact ties. Use this to prefer
|
|
215
|
+
values a user just entered over generic profile defaults.
|
|
216
|
+
|
|
217
|
+
#### Confidence, ambiguity, and low-confidence — three distinct outcomes
|
|
218
|
+
|
|
219
|
+
These three adjacent concepts are easy to confuse. They each mean a
|
|
220
|
+
different thing and need a different fix.
|
|
221
|
+
|
|
222
|
+
- **`confidence: 'high'` / `'medium'` on a `ready` result.** The matcher
|
|
223
|
+
picked a clear winner; the field only reports whether it was a strong
|
|
224
|
+
match or a marginal one. Both are safe to fill. Use the field only
|
|
225
|
+
if your policy wants an extra guard (for example, auto-fill on
|
|
226
|
+
`'high'` and ask the user on `'medium'`).
|
|
227
|
+
- **`kind: 'no_match'` with `reason: 'low_confidence'`.** No candidate
|
|
228
|
+
scored high enough to be plausible. Fix: add metadata to your
|
|
229
|
+
candidates (richer `label`, explicit `type`, `semanticTags`) or
|
|
230
|
+
narrow the source.
|
|
231
|
+
- **`kind: 'ambiguous'`.** Several candidates scored within a small
|
|
232
|
+
threshold of the top. The matcher refuses to guess. Fix: disambiguate
|
|
233
|
+
by passing a narrower source for this target, or pick one of
|
|
234
|
+
`result.candidates` explicitly in your wrapper.
|
|
235
|
+
|
|
236
|
+
#### Grouped matching is simpler
|
|
237
|
+
|
|
238
|
+
When the subject is a fillable form (not a single target), the matcher
|
|
239
|
+
does not run signal-based evidence scoring. It ranks group candidates
|
|
240
|
+
by two things only:
|
|
241
|
+
|
|
242
|
+
1. **Overlap.** How many of the form's `fieldKeys` the candidate
|
|
243
|
+
claims to cover.
|
|
244
|
+
2. **Declared confidence** on the candidate itself (`'high'` >
|
|
245
|
+
`'medium'` > undefined).
|
|
246
|
+
|
|
247
|
+
The winner gets `confidence: 'high'` when it covers every field the
|
|
248
|
+
form declared, otherwise `'medium'`. Ties on `overlap + confidence`
|
|
249
|
+
surface as `ambiguous_group` rather than being resolved by list
|
|
250
|
+
position — that refusal is a deterministic-contract property of the
|
|
251
|
+
grouped matcher.
|
|
252
|
+
|
|
253
|
+
## Walk-through 2 — value behind a lookup
|
|
254
|
+
|
|
255
|
+
Some values cannot live in memory: a password behind approval, a payment
|
|
256
|
+
card stored in a vault, a secret issued per request. For these you
|
|
257
|
+
describe the candidate as *needing resolution* and let AgentBrowse hand
|
|
258
|
+
the plan to a caller-supplied adapter.
|
|
259
|
+
|
|
260
|
+
The key word is **resolve**, and it has a specific meaning here. It is
|
|
261
|
+
not «resolve a Promise» and not «resolve a conflict». It means:
|
|
262
|
+
|
|
263
|
+
> Turn a match result that says *«this candidate requires external
|
|
264
|
+
> data»* into a match result that says *«the value is ready — here is
|
|
265
|
+
> the ref»*, by running a caller-supplied adapter.
|
|
266
|
+
|
|
267
|
+
The adapter is yours. AgentBrowse has no idea what a vault is.
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
const passwordTarget = observed.targets.find((t) => t.inputType === 'password');
|
|
271
|
+
|
|
272
|
+
const pending = await match(passwordTarget!, {
|
|
273
|
+
from: [
|
|
274
|
+
{
|
|
275
|
+
fieldKey: 'password',
|
|
276
|
+
type: 'secret',
|
|
277
|
+
resolve: { kind: 'vault_lookup', key: 'login.password' },
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
});
|
|
281
|
+
// pending is { kind: 'needs_resolution', targetRef, fieldKey, candidateRef, confidence, plan }.
|
|
282
|
+
|
|
283
|
+
const vaultResolver: AgentbrowseMatchResolver = {
|
|
284
|
+
async resolve(plan) {
|
|
285
|
+
if ('fillRef' in plan) {
|
|
286
|
+
throw new Error('This adapter only handles single-field plans.');
|
|
287
|
+
}
|
|
288
|
+
// Your real logic goes here — MagicPay request, KMS call, env var, etc.
|
|
289
|
+
const value = await readFromMyVault(plan.resolve.key!);
|
|
290
|
+
return { kind: 'value', value };
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const ready = await resolve(pending, { with: vaultResolver });
|
|
295
|
+
// ready is { kind: 'ready', ...same shape as in Walk-through 1 }.
|
|
296
|
+
|
|
297
|
+
await fill(session, passwordTarget!, ready);
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Anatomy of the resolve step:
|
|
301
|
+
|
|
302
|
+
- `pending.plan` is a plain serialisable object:
|
|
303
|
+
`{ targetRef, candidateRef, fieldKey, type, resolve: { kind, key? , params? } }`.
|
|
304
|
+
It fully describes the work without carrying any value. You can log it,
|
|
305
|
+
pass it across a process boundary, store it in a job queue — nothing
|
|
306
|
+
sensitive is in there.
|
|
307
|
+
- `vaultResolver.resolve(plan)` returns a typed resource:
|
|
308
|
+
`{ kind: 'value', value }` for single fields, or
|
|
309
|
+
`{ kind: 'artifact', artifact, ...metadata }` for grouped plans (see
|
|
310
|
+
Walk-through 5). AgentBrowse wraps the result into a fresh ready match
|
|
311
|
+
with a stable `valueRef`.
|
|
312
|
+
- The whole adapter contract is two methods (see
|
|
313
|
+
[Resolver Interface](#resolver-interface)); you do not subclass
|
|
314
|
+
anything or register with a runtime. A handwritten object literal is a
|
|
315
|
+
perfectly valid resolver.
|
|
316
|
+
|
|
317
|
+
Why bother separating `resolve` from `fill`? Three reasons:
|
|
318
|
+
|
|
319
|
+
- Your adapter may run slower than the browser step (approval UIs,
|
|
320
|
+
polling, retries). Keeping them separate lets you log a checkpoint
|
|
321
|
+
between resolution and application.
|
|
322
|
+
- `resolve` can operate on batches (next walk-through), so several
|
|
323
|
+
fields can share one lookup round-trip.
|
|
324
|
+
- `resolve` is where all domain-specific behaviour lives. `fill` stays a
|
|
325
|
+
deterministic one-shot apply. That separation is deliberate — see
|
|
326
|
+
[Design Rules](#design-rules).
|
|
327
|
+
|
|
328
|
+
## Walk-through 3 — collapsing to one call
|
|
329
|
+
|
|
330
|
+
Often you do not need the intermediate ready result. `fill` accepts an
|
|
331
|
+
optional `resolver`; if the plan still needs resolution, `fill` runs it
|
|
332
|
+
inline and applies the result in the same call.
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
await fill(session, passwordTarget!, pending, { resolver: vaultResolver });
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
This is equivalent to `fill(session, passwordTarget!, await resolve(pending, { with: vaultResolver }))`.
|
|
339
|
+
Use the two-call form when you want to inspect the ready result,
|
|
340
|
+
correlate traces, or batch multiple resolves together. Use the inline
|
|
341
|
+
form for one-shot integrations.
|
|
342
|
+
|
|
343
|
+
## Walk-through 4 — resolving a batch
|
|
344
|
+
|
|
345
|
+
`resolve` also accepts an array of match results. Entries that are
|
|
346
|
+
already `ready` pass through untouched; only `needs_resolution` entries
|
|
347
|
+
hit the adapter. When more than one entry needs resolution and your
|
|
348
|
+
adapter implements `resolveBatch`, AgentBrowse calls it once with all of
|
|
349
|
+
the plans.
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
const [emailReady, passwordReady, cardReady] = await resolve(
|
|
353
|
+
[emailMatch, passwordMatch, cardMatch],
|
|
354
|
+
{ with: vaultResolver },
|
|
355
|
+
);
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Two rules matter:
|
|
359
|
+
|
|
360
|
+
- **Order is preserved.** The output array has the same length and same
|
|
361
|
+
positions as the input. Your code can rebuild the `target → ready`
|
|
362
|
+
mapping by index without extra bookkeeping.
|
|
363
|
+
- **Batch is an optimisation, not a requirement.** If your adapter only
|
|
364
|
+
implements `resolve` (singular), AgentBrowse falls back to
|
|
365
|
+
`Promise.all` over the single-plan method. Add `resolveBatch` only when
|
|
366
|
+
one network round-trip can fulfil many plans (typical for vault
|
|
367
|
+
lookups on one login form).
|
|
368
|
+
|
|
369
|
+
## Walk-through 5 — grouped protected forms
|
|
370
|
+
|
|
371
|
+
Up to this point every walk-through filled one field at a time. Some
|
|
372
|
+
forms cannot be filled that way: a protected login form, a payment-card
|
|
373
|
+
form, an identity-document form. Either you fill the whole form atomically
|
|
374
|
+
(so the page's own validation and submit logic sees a consistent state),
|
|
375
|
+
or the caller's protected-fill guardrails apply to the whole form rather
|
|
376
|
+
than to individual inputs.
|
|
377
|
+
|
|
378
|
+
For these, `match` takes a `fillableForm` (one of the entries in
|
|
379
|
+
`observeResult.fillableForms`) as the subject, and the source is a list
|
|
380
|
+
of group candidates describing whole-form plans:
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
const fillableForm = observed.fillableForms?.find((f) => f.purpose === 'login');
|
|
384
|
+
if (!fillableForm) return;
|
|
385
|
+
|
|
386
|
+
const matched = await match(fillableForm, {
|
|
387
|
+
from: [
|
|
388
|
+
{
|
|
389
|
+
candidateRef: 'vault_login_1',
|
|
390
|
+
itemRef: 'vault_login_1',
|
|
391
|
+
fieldKeys: ['username', 'password'],
|
|
392
|
+
confidence: 'high',
|
|
393
|
+
resolve: { kind: 'magicpay_observed_form', key: 'vault_login_1' },
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
});
|
|
397
|
+
// matched is { kind: 'needs_resolution_group', fillRef, purpose, candidateRef, itemRef, fieldKeys, confidence, plan }.
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Two new things show up here:
|
|
401
|
+
|
|
402
|
+
- The resolver's `resolve(plan)` is expected to return an **artifact**,
|
|
403
|
+
not a single value. In the MagicPay case that artifact has shape
|
|
404
|
+
`{ kind: 'values', values: { username, password } }` plus metadata
|
|
405
|
+
(`requestId`, `resolutionPath`, `claimedAt`).
|
|
406
|
+
- A fresh capability, `resolver.fill`, takes over the apply step. It
|
|
407
|
+
receives the session, the form subject, and the ready artifact, and is
|
|
408
|
+
responsible for actually writing the values into the browser — usually
|
|
409
|
+
by delegating to `fillProtectedForm(...)` so all the stale-binding,
|
|
410
|
+
validation, and field-policy guards still apply.
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
const magicpayResolver: AgentbrowseMatchResolver = {
|
|
414
|
+
async resolve(plan) {
|
|
415
|
+
if (!('fillRef' in plan)) throw new Error('Expected a grouped plan.');
|
|
416
|
+
const result = await magicpay.data.resolve(sessionId, buildResolveInput(plan));
|
|
417
|
+
const final = await magicpay.data.waitForResult(sessionId, result);
|
|
418
|
+
if (!final.ok || final.artifact.kind !== 'values') throw new Error('…');
|
|
419
|
+
return {
|
|
420
|
+
kind: 'artifact',
|
|
421
|
+
artifact: final.artifact,
|
|
422
|
+
requestId: final.requestId,
|
|
423
|
+
resolutionPath: final.resolutionPath,
|
|
424
|
+
claimedAt: new Date().toISOString(),
|
|
425
|
+
};
|
|
426
|
+
},
|
|
427
|
+
async fill(session, form, ready) {
|
|
428
|
+
const artifact = ready.artifact as { kind: 'values'; values: Record<string, string> };
|
|
429
|
+
return fillProtectedForm({
|
|
430
|
+
session,
|
|
431
|
+
fillableForm: form,
|
|
432
|
+
protectedValues: artifact.values,
|
|
433
|
+
});
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const result = await fill(session, fillableForm, matched, {
|
|
438
|
+
resolver: magicpayResolver,
|
|
439
|
+
});
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
The whole flow is still `match → resolve → fill`. The only change is
|
|
443
|
+
that the subject is a form, the candidates describe whole-form plans,
|
|
444
|
+
and the resolver carries a `fill` capability alongside `resolve`.
|
|
445
|
+
|
|
446
|
+
## Resolver Interface
|
|
447
|
+
|
|
448
|
+
Two exported interfaces cover what you hand to `resolve(...)` and
|
|
449
|
+
`fill(...)`:
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
// Main adapter — used by resolve() and by fill() when a plan needs
|
|
453
|
+
// external data or when grouped fill is part of the same adapter.
|
|
454
|
+
interface AgentbrowseMatchResolver {
|
|
455
|
+
resolve(plan): Promise<AgentbrowseResolvedResource>;
|
|
456
|
+
resolveBatch?(plans): Promise<ReadonlyArray<AgentbrowseResolvedResource>>;
|
|
457
|
+
fill?(session, form, ready): Promise<Record<string, unknown> & { success: boolean }>;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Narrow handler — used by fill() when the caller only applies ready
|
|
461
|
+
// grouped artifacts and never fetches external data.
|
|
462
|
+
interface AgentbrowseGroupFillHandler {
|
|
463
|
+
fill(session, form, ready): Promise<Record<string, unknown> & { success: boolean }>;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
type AgentbrowseResolvedResource =
|
|
467
|
+
| { kind: 'value'; value: string | number }
|
|
468
|
+
| {
|
|
469
|
+
kind: 'artifact';
|
|
470
|
+
artifact: unknown;
|
|
471
|
+
itemRef?: string;
|
|
472
|
+
requestId?: string;
|
|
473
|
+
resolutionPath?: string;
|
|
474
|
+
claimedAt?: string;
|
|
475
|
+
};
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
One parameter slot, two accepted shapes. `fill(session, subject, plan,
|
|
479
|
+
{ resolver })` accepts either interface via a union, and picks which
|
|
480
|
+
capability to call based on the plan's `kind` (`needs_resolution` needs
|
|
481
|
+
`.resolve`, `ready_group` needs `.fill`). `resolve(plan, { with })`
|
|
482
|
+
always requires the main `AgentbrowseMatchResolver` — the `.resolve`
|
|
483
|
+
method has to be there at compile time.
|
|
484
|
+
|
|
485
|
+
### `AgentbrowseMatchResolver` capabilities
|
|
486
|
+
|
|
487
|
+
| Capability | Required? | Purpose |
|
|
488
|
+
| --- | --- | --- |
|
|
489
|
+
| `resolve` | Yes, on this interface. | Turn one plan into one ready resource. |
|
|
490
|
+
| `resolveBatch` | Optional — add when several plans share one external call. | Turn many plans into many resources in one call. Must preserve input order. |
|
|
491
|
+
| `fill` | Optional — add when the same adapter also applies grouped protected artifacts. | Apply a ready grouped artifact to the fillable form — usually a thin wrapper over `fillProtectedForm(...)`. |
|
|
492
|
+
|
|
493
|
+
Implement just `resolve` when your integration only fetches external
|
|
494
|
+
values (single-field flows). Add `fill` alongside when the same adapter
|
|
495
|
+
both fetches the artifact and applies it (the usual shape for a
|
|
496
|
+
MagicPay bridge around a single workflow).
|
|
497
|
+
|
|
498
|
+
### `AgentbrowseGroupFillHandler` — narrow handler
|
|
499
|
+
|
|
500
|
+
Use this shape when the caller already has the grouped artifact in
|
|
501
|
+
hand (for example, after a separate `magicpay data.waitForResult(...)`
|
|
502
|
+
outside the AgentBrowse call) and only needs to apply it:
|
|
503
|
+
|
|
504
|
+
```ts
|
|
505
|
+
import { fill, type AgentbrowseGroupFillHandler } from '@mercuryo-ai/agentbrowse';
|
|
506
|
+
|
|
507
|
+
const applier: AgentbrowseGroupFillHandler = {
|
|
508
|
+
async fill(session, form, ready) {
|
|
509
|
+
return fillProtectedForm({
|
|
510
|
+
session,
|
|
511
|
+
fillableForm: form,
|
|
512
|
+
protectedValues: (ready.artifact as { values: Record<string, string> }).values,
|
|
513
|
+
});
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
await fill(session, fillableForm, readyGroupMatch, { resolver: applier });
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
Because `AgentbrowseGroupFillHandler` has no `resolve` method,
|
|
521
|
+
`resolve(plan, { with: applier })` would not type-check — that is
|
|
522
|
+
intentional. The narrow shape is only meaningful at the `fill(...)`
|
|
523
|
+
boundary, where the plan is already `ready_group` and no fetching is
|
|
524
|
+
needed.
|
|
525
|
+
|
|
526
|
+
### Common rules
|
|
527
|
+
|
|
528
|
+
Both shapes follow the same behavioural contract:
|
|
529
|
+
|
|
530
|
+
- The adapter does not mutate its input plan — it returns a fresh
|
|
531
|
+
resource, and AgentBrowse wraps the result.
|
|
532
|
+
- Adapters must throw — not return silently — when a resource cannot be
|
|
533
|
+
produced for a plan the caller believed was valid. Thrown errors
|
|
534
|
+
propagate through `resolve(...)` / `fill(...)` to your call site.
|
|
535
|
+
- When you pass a handler that does not carry a capability the plan
|
|
536
|
+
needs (a `ready_group` plan with a handler lacking `.fill`, or a
|
|
537
|
+
`needs_resolution_group` plan with a handler lacking `.resolve`),
|
|
538
|
+
`fill(...)` returns a typed `match_resolver_required` failure instead
|
|
539
|
+
of throwing. See the [failure contract](#failure-contract) above.
|
|
540
|
+
|
|
541
|
+
## Design Rules
|
|
542
|
+
|
|
543
|
+
These rules are public contract, not implementation detail. Rely on
|
|
544
|
+
them.
|
|
545
|
+
|
|
546
|
+
### No raw values in public match results
|
|
547
|
+
|
|
548
|
+
Ready match results carry `valueRef` / `artifactRef` strings. The actual
|
|
549
|
+
value or artifact lives in a non-enumerable symbol accessor attached to
|
|
550
|
+
the same object. Only `fill(...)` looks through it.
|
|
551
|
+
|
|
552
|
+
This has visible consequences:
|
|
553
|
+
|
|
554
|
+
- `JSON.stringify(ready)` does not contain the value. It is safe to log,
|
|
555
|
+
snapshot-test, or forward to trace pipelines.
|
|
556
|
+
- `structuredClone(ready)` drops the accessor; the clone is a
|
|
557
|
+
diagnostic-only snapshot and `fill(...)` will reject it with
|
|
558
|
+
`match_value_unavailable`.
|
|
559
|
+
- Passing a ready result across a process boundary (a queue, a worker, a
|
|
560
|
+
subprocess, IPC) drops the accessor for the same reason.
|
|
561
|
+
|
|
562
|
+
The serialisable shape of the pipeline is the `needs_resolution` plan.
|
|
563
|
+
If your runtime needs to hand work to another process, ship the plan
|
|
564
|
+
there and let the downstream side run `resolve → fill` in one shot.
|
|
565
|
+
|
|
566
|
+
### Stable resolved refs
|
|
567
|
+
|
|
568
|
+
After `resolve(plan)` succeeds, the ready result carries a stable ref of
|
|
569
|
+
the form `value:${candidateRef}:resolved` or
|
|
570
|
+
`artifact:${candidateRef}:resolved`. Repeated resolves of the same plan
|
|
571
|
+
produce the same ref.
|
|
572
|
+
|
|
573
|
+
Use this property when:
|
|
574
|
+
|
|
575
|
+
- writing snapshot tests that compare match results across runs;
|
|
576
|
+
- correlating traces between a resolve and a later fill;
|
|
577
|
+
- deduplicating resolves by ref before calling fill.
|
|
578
|
+
|
|
579
|
+
The refs before `resolve` (fresh from a `match` with an inline source)
|
|
580
|
+
also keep the property that they are stable for a given input array —
|
|
581
|
+
they are seeded by `candidateRef` and the input index, not by wall time.
|
|
582
|
+
|
|
583
|
+
### AgentBrowse makes no network calls
|
|
584
|
+
|
|
585
|
+
`match` and `fill` are local-only. Only your adapter's `.resolve` /
|
|
586
|
+
`.resolveBatch` / `.fill` methods make network or I/O calls. This is
|
|
587
|
+
the «adapter boundary» — AgentBrowse core has zero knowledge of
|
|
588
|
+
MagicPay, vaults, approval UIs, or any specific transport.
|
|
589
|
+
|
|
590
|
+
## Result Kinds
|
|
591
|
+
|
|
592
|
+
`match` returns a tagged union. Always branch on `kind`; never
|
|
593
|
+
infer from the presence of individual fields.
|
|
594
|
+
|
|
595
|
+
| `kind` | Meaning | Next step |
|
|
596
|
+
| --- | --- | --- |
|
|
597
|
+
| `ready` | Unique candidate; value is in memory. | `fill(...)`. |
|
|
598
|
+
| `needs_resolution` | Unique candidate; value needs external lookup. | `resolve(...)` or `fill(..., { resolver })`. |
|
|
599
|
+
| `ambiguous` | Several candidates tied for this target. | Ask the caller to disambiguate; do not fill speculatively. |
|
|
600
|
+
| `no_match` | No candidate applied. `reason` explains why: `protected_target`, `no_candidate`, `scope_ineligible`, `incompatible_shape`, `low_confidence`. | Skip the target. |
|
|
601
|
+
| `ready_group` | Grouped plan is ready for a fillable form. | `fill(session, form, matched, { resolver })` with `resolver.fill` implemented. |
|
|
602
|
+
| `needs_resolution_group` | Grouped plan still needs an artifact. | `resolve(...)` or `fill(..., { resolver })`. |
|
|
603
|
+
| `ambiguous_group` | Several group candidates tied on overlap + confidence. | Pick one candidate explicitly (e.g. via a wrapper option); do not fill. |
|
|
604
|
+
| `no_match_group` | No group candidate applied. | Skip the form. |
|
|
605
|
+
|
|
606
|
+
`ambiguous_group` is a deliberate deterministic-contract signal. Before
|
|
607
|
+
the current surface, group matching would silently pick the first
|
|
608
|
+
candidate in list order on a tie; now it raises the tie explicitly and
|
|
609
|
+
forces the caller to decide.
|
|
610
|
+
|
|
611
|
+
## Failure Contract
|
|
612
|
+
|
|
613
|
+
When `fill` cannot proceed because of a contract problem, it returns a
|
|
614
|
+
structured failure instead of throwing:
|
|
615
|
+
|
|
616
|
+
```ts
|
|
617
|
+
interface AgentbrowseFillFailureResult {
|
|
618
|
+
success: false;
|
|
619
|
+
failureSurface: 'contract';
|
|
620
|
+
error:
|
|
621
|
+
| 'match_no_match'
|
|
622
|
+
| 'match_ambiguous'
|
|
623
|
+
| 'match_resolver_required'
|
|
624
|
+
| 'match_value_unavailable'
|
|
625
|
+
| 'match_artifact_unavailable';
|
|
626
|
+
outcomeType: 'blocked' | 'unsupported';
|
|
627
|
+
message: string;
|
|
628
|
+
reason: string;
|
|
629
|
+
targetRef?: string;
|
|
630
|
+
fillRef?: string;
|
|
631
|
+
action: 'fill';
|
|
632
|
+
}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
Branch on `error`:
|
|
636
|
+
|
|
637
|
+
| `error` | Root cause | Action |
|
|
638
|
+
| --- | --- | --- |
|
|
639
|
+
| `match_no_match` | `fill` received a `no_match` / `no_match_group` plan, or a subject-shape mismatch (field plan on a form, form plan on a field). | Do not retry with the same input. Check the match result first or re-observe. |
|
|
640
|
+
| `match_ambiguous` | `fill` received an `ambiguous` / `ambiguous_group` plan. | Ask the caller to disambiguate. |
|
|
641
|
+
| `match_resolver_required` | Plan needs external resolution and no `resolver` was passed. For grouped plans, also raised when `resolver.fill` is missing. | Supply the resolver adapter (with `fill` implemented when grouped). |
|
|
642
|
+
| `match_value_unavailable` | Internal accessor is gone. Almost always means a ready result was serialised across a process boundary and the non-enumerable accessor was lost. | Run `resolve` → `fill` inside the same process, or ship the `needs_resolution` plan instead. |
|
|
643
|
+
| `match_artifact_unavailable` | Same failure mode as above, for grouped ready plans. | Same fix. |
|
|
644
|
+
|
|
645
|
+
Browser-level failures (stale refs, page-level validation, connection)
|
|
646
|
+
surface through the underlying `ActResult` or `resolver.fill` result —
|
|
647
|
+
see [api-reference.md → Error codes by command](./api-reference.md#error-codes-by-command)
|
|
648
|
+
and [protected-fill.md](./protected-fill.md).
|
|
649
|
+
|
|
650
|
+
## Relation To `act(...)` And `fillProtectedForm(...)`
|
|
651
|
+
|
|
652
|
+
Three doors into the browser, increasing level of abstraction:
|
|
653
|
+
|
|
654
|
+
| API | Owns | Use it when |
|
|
655
|
+
| --- | --- | --- |
|
|
656
|
+
| `act(session, ref, 'fill', value)` | One raw target, one raw value. | You already hold the value, you already know the target, you do not need a match decision. |
|
|
657
|
+
| `fillProtectedForm(...)` — from `@mercuryo-ai/agentbrowse/protected-fill` | Applying a ready set of protected values to a fillable form with stale-binding, validation, and field-policy guards. | You already have the values artifact (for example from a MagicPay `artifact.kind === 'values'`) and you do not want `match` / `resolve` orchestration on top. |
|
|
658
|
+
| `fill(session, subject, plan, { resolver? })` | End-to-end `match → resolve → apply`. Grouped subjects route through `resolver.fill`, which typically wraps `fillProtectedForm(...)`. | You want one deterministic surface for deciding, resolving, and applying. Also the right shape when your runtime composes several fields or forms in batch. |
|
|
659
|
+
|
|
660
|
+
`fillProtectedForm(...)` did not go away — it is still the right call
|
|
661
|
+
when the artifact is in hand and no match/resolve plan is needed. See
|
|
662
|
+
[protected-fill.md](./protected-fill.md) for its full contract.
|
|
663
|
+
|
|
664
|
+
## Testing Wrappers Around These Primitives
|
|
665
|
+
|
|
666
|
+
Wrapper packages should exercise the primitives against the published
|
|
667
|
+
`/testing` subpath instead of fabricating result objects by hand:
|
|
668
|
+
|
|
669
|
+
```ts
|
|
670
|
+
import {
|
|
671
|
+
createFixtureMatchStore,
|
|
672
|
+
createFixtureGroupStore,
|
|
673
|
+
createFixtureResolver,
|
|
674
|
+
fixtureResolvedValue,
|
|
675
|
+
fixtureResolvedArtifact,
|
|
676
|
+
} from '@mercuryo-ai/agentbrowse/testing';
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
Available helpers:
|
|
680
|
+
|
|
681
|
+
- `createFixtureMatchStore(entries)` — opaque store for single-field
|
|
682
|
+
matching. Implements `AgentbrowseMatchStore` with a backing
|
|
683
|
+
in-memory map.
|
|
684
|
+
- `createFixtureGroupStore(entries)` — opaque store for grouped matching.
|
|
685
|
+
Implements `AgentbrowseGroupMatchStore`.
|
|
686
|
+
- `createFixtureResolver(resourcesByCandidateRef, { enableBatch? })` —
|
|
687
|
+
adapter that satisfies `AgentbrowseMatchResolver` and records
|
|
688
|
+
`resolveCalls` / `resolveBatchCalls` for assertions.
|
|
689
|
+
- `fixtureResolvedValue(value)` / `fixtureResolvedArtifact(artifact)` —
|
|
690
|
+
typed builders for the resolved-resource shape.
|
|
691
|
+
|
|
692
|
+
See [testing.md](./testing.md) for integration patterns.
|
|
693
|
+
|
|
694
|
+
## Further Reading
|
|
695
|
+
|
|
696
|
+
- [API Reference — Match / Resolve / Fill](./api-reference.md)
|
|
697
|
+
- [Protected Fill Guide](./protected-fill.md)
|
|
698
|
+
- [Testing Guide](./testing.md)
|
|
699
|
+
- [Integration Checklist](./integration-checklist.md)
|