@reticlehq/server 2.6.0 → 2.8.0
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/README.md +14 -5
- package/SKILL.md +251 -33
- package/dist/bridge/bridge.d.ts +6 -1
- package/dist/bridge/bridge.js +27 -4
- package/dist/capsule/causal-summary.d.ts +20 -0
- package/dist/capsule/causal-summary.js +31 -6
- package/dist/capsule/divergence.js +7 -1
- package/dist/cli/chromium-hint.d.ts +68 -0
- package/dist/cli/chromium-hint.js +145 -0
- package/dist/cli/cli-doctor.js +31 -36
- package/dist/cli/cli-kill.d.ts +58 -0
- package/dist/cli/cli-kill.js +162 -0
- package/dist/cli/cli-launch.d.ts +18 -0
- package/dist/cli/cli-launch.js +32 -1
- package/dist/cli/cli-parse.d.ts +16 -1
- package/dist/cli/cli-parse.js +32 -9
- package/dist/cli/cli-port.d.ts +8 -0
- package/dist/cli/cli-port.js +36 -1
- package/dist/cli/cli-verify.js +2 -2
- package/dist/cli/doctor-daemon-line.js +10 -1
- package/dist/cli/doctor-sessions-line.d.ts +12 -0
- package/dist/cli/doctor-sessions-line.js +66 -0
- package/dist/cli/port-holder.d.ts +10 -0
- package/dist/cli/port-holder.js +26 -2
- package/dist/cli.js +139 -23
- package/dist/crawl/crawl-tools.js +12 -2
- package/dist/crawl/crawl.d.ts +13 -0
- package/dist/crawl/crawl.js +50 -2
- package/dist/daemon/daemon-resilience.js +2 -2
- package/dist/daemon/loopback-alias.d.ts +32 -0
- package/dist/daemon/loopback-alias.js +80 -0
- package/dist/events/contradictions.d.ts +14 -0
- package/dist/events/contradictions.js +26 -17
- package/dist/events/predicate-eval.d.ts +26 -3
- package/dist/events/predicate-eval.js +284 -95
- package/dist/events/predicate-parse.d.ts +1 -1
- package/dist/events/predicate-parse.js +18 -4
- package/dist/events/predicate.js +170 -18
- package/dist/events/ring-buffer.d.ts +8 -0
- package/dist/events/ring-buffer.js +34 -1
- package/dist/flows/flow-classify.js +1 -1
- package/dist/flows/flow-tools.js +12 -4
- package/dist/flows/heal-run.js +1 -1
- package/dist/honesty/blind-spots.js +6 -2
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +9 -1
- package/dist/honesty/unsettled.d.ts +40 -0
- package/dist/honesty/unsettled.js +78 -0
- package/dist/honesty/verified.d.ts +7 -0
- package/dist/honesty/verified.js +27 -4
- package/dist/http-server.js +11 -1
- package/dist/index.js +29 -2
- package/dist/init/agent-rules.d.ts +16 -0
- package/dist/init/agent-rules.js +96 -17
- package/dist/init/astro-patch.d.ts +2 -1
- package/dist/init/astro-patch.js +11 -5
- package/dist/init/capabilities.d.ts +22 -0
- package/dist/init/capabilities.js +76 -0
- package/dist/init/confirm.d.ts +96 -0
- package/dist/init/confirm.js +138 -0
- package/dist/init/cra.js +3 -2
- package/dist/init/detect.d.ts +8 -0
- package/dist/init/detect.js +15 -0
- package/dist/init/non-js-project.d.ts +27 -0
- package/dist/init/non-js-project.js +84 -0
- package/dist/init/plan-framework.d.ts +9 -0
- package/dist/init/plan-framework.js +72 -16
- package/dist/init/plan.d.ts +40 -4
- package/dist/init/plan.js +183 -43
- package/dist/init/run.d.ts +24 -2
- package/dist/init/run.js +181 -46
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +46 -6
- package/dist/init/snippets.d.ts +64 -5
- package/dist/init/snippets.js +220 -31
- package/dist/init/vite-config.js +29 -3
- package/dist/input/network-mock-tools.d.ts +19 -0
- package/dist/input/network-mock-tools.js +29 -4
- package/dist/license/license.js +1 -1
- package/dist/mcp/mcp-proxy.d.ts +69 -3
- package/dist/mcp/mcp-proxy.js +215 -23
- package/dist/mcp/mcp.js +58 -7
- package/dist/mcp/proxy-lifecycle.d.ts +20 -1
- package/dist/mcp/proxy-lifecycle.js +22 -1
- package/dist/pool/browser-pool.d.ts +22 -2
- package/dist/pool/browser-pool.js +44 -4
- package/dist/pool/playwright-launcher.js +10 -2
- package/dist/project/learned-routes.d.ts +24 -0
- package/dist/project/learned-routes.js +63 -0
- package/dist/project/project-store.d.ts +6 -0
- package/dist/project/project-store.js +25 -1
- package/dist/session/live-control-tools.js +39 -2
- package/dist/session/live-control.d.ts +7 -9
- package/dist/session/live-control.js +34 -1
- package/dist/session/no-session-diagnosis.d.ts +23 -4
- package/dist/session/no-session-diagnosis.js +128 -44
- package/dist/session/no-session-watch.js +12 -0
- package/dist/session/ref-provenance.d.ts +54 -0
- package/dist/session/ref-provenance.js +61 -0
- package/dist/session/review-store.d.ts +19 -0
- package/dist/session/review-store.js +34 -5
- package/dist/session/session-lease.js +1 -1
- package/dist/session/session-manager.d.ts +9 -0
- package/dist/session/session-manager.js +11 -0
- package/dist/session/session.d.ts +8 -0
- package/dist/session/session.js +12 -0
- package/dist/status-payload.d.ts +13 -1
- package/dist/status-payload.js +6 -1
- package/dist/telemetry/app-instrumented.d.ts +37 -0
- package/dist/telemetry/app-instrumented.js +54 -0
- package/dist/telemetry/automation-hint.d.ts +17 -0
- package/dist/telemetry/automation-hint.js +71 -0
- package/dist/telemetry/bug-found.d.ts +0 -20
- package/dist/telemetry/bug-found.js +38 -21
- package/dist/telemetry/cli-telemetry.js +7 -1
- package/dist/telemetry/daemon-telemetry.d.ts +4 -4
- package/dist/telemetry/daemon-telemetry.js +8 -4
- package/dist/telemetry/feedback.js +1 -1
- package/dist/telemetry/init-telemetry.js +9 -1
- package/dist/telemetry/install-source.d.ts +36 -0
- package/dist/telemetry/install-source.js +40 -0
- package/dist/telemetry/run-telemetry.js +4 -1
- package/dist/telemetry/session-metrics.d.ts +3 -3
- package/dist/telemetry/session-metrics.js +9 -4
- package/dist/telemetry/telemetry.d.ts +7 -1
- package/dist/telemetry/telemetry.js +17 -2
- package/dist/telemetry/tool-refused.d.ts +19 -0
- package/dist/telemetry/tool-refused.js +55 -0
- package/dist/telemetry/verification-of.js +72 -8
- package/dist/temp-dir.d.ts +27 -0
- package/dist/temp-dir.js +46 -0
- package/dist/tools/act-tools.js +77 -11
- package/dist/tools/assert-grade.d.ts +1 -1
- package/dist/tools/assert-grade.js +27 -5
- package/dist/tools/assert-verdict.js +25 -1
- package/dist/tools/contract-tools.js +4 -0
- package/dist/tools/dynamic-tools.js +1 -1
- package/dist/tools/error-recovery.d.ts +10 -0
- package/dist/tools/error-recovery.js +70 -5
- package/dist/tools/feedback-invite.js +1 -1
- package/dist/tools/invoke-tool.js +149 -32
- package/dist/tools/lease-tools.d.ts +29 -1
- package/dist/tools/lease-tools.js +58 -5
- package/dist/tools/navigate-result.js +1 -1
- package/dist/tools/observe-tools.js +1 -1
- package/dist/tools/observed-nothing.js +1 -1
- package/dist/tools/react-grace.d.ts +48 -0
- package/dist/tools/react-grace.js +87 -0
- package/dist/tools/read-tools.js +20 -4
- package/dist/tools/settle-in-flight.d.ts +79 -0
- package/dist/tools/settle-in-flight.js +106 -0
- package/dist/tools/snapshot-delta.d.ts +7 -0
- package/dist/tools/snapshot-delta.js +130 -6
- package/dist/tools/tool-kit.d.ts +52 -5
- package/dist/tools/tool-kit.js +55 -24
- package/dist/tools/tool-surface.js +28 -9
- package/dist/tools/tools.js +38 -4
- package/dist/visual/capture-cleanup.d.ts +16 -0
- package/dist/visual/capture-cleanup.js +39 -0
- package/dist/visual/visual-tools.d.ts +18 -0
- package/dist/visual/visual-tools.js +24 -5
- package/docs/README.md +56 -11
- package/docs/actions.mdx +87 -0
- package/docs/agent-cheatsheet.md +102 -26
- package/docs/architecture.md +46 -35
- package/docs/benchmarks.md +76 -72
- package/docs/best-practices.mdx +211 -0
- package/docs/capabilities.mdx +387 -0
- package/docs/cli/affected.mdx +51 -0
- package/docs/cli/capsules.mdx +69 -0
- package/docs/cli/cloud.mdx +127 -0
- package/docs/cli/doctor.mdx +76 -0
- package/docs/cli/drive.mdx +55 -0
- package/docs/cli/feedback.mdx +67 -0
- package/docs/cli/gate.mdx +64 -0
- package/docs/cli/help.mdx +92 -0
- package/docs/cli/hunt.mdx +57 -0
- package/docs/cli/identify.mdx +62 -0
- package/docs/cli/init.mdx +94 -0
- package/docs/cli/kill.mdx +91 -0
- package/docs/cli/license.mdx +48 -0
- package/docs/cli/mcp.mdx +61 -0
- package/docs/cli/open.mdx +68 -0
- package/docs/cli/restart.mdx +60 -0
- package/docs/cli/rollback.mdx +35 -0
- package/docs/cli/serve.mdx +62 -0
- package/docs/cli/status.mdx +64 -0
- package/docs/cli/stop.mdx +56 -0
- package/docs/cli/telemetry.mdx +57 -0
- package/docs/cli/update.mdx +52 -0
- package/docs/cli/verify.mdx +57 -0
- package/docs/cli/version.mdx +39 -0
- package/docs/cli/watch.mdx +55 -0
- package/docs/cli.mdx +219 -0
- package/docs/debugging.md +13 -9
- package/docs/deploy-checks.md +15 -11
- package/docs/desktop-apps.md +49 -40
- package/docs/desktop.mdx +188 -0
- package/docs/docs.json +363 -0
- package/docs/enterprise.md +20 -16
- package/docs/faq.mdx +88 -0
- package/docs/fixtures.md +11 -7
- package/docs/flows.md +26 -22
- package/docs/for-agents.md +71 -0
- package/docs/frameworks.mdx +199 -0
- package/docs/gate-plan.md +45 -41
- package/docs/gates.md +23 -19
- package/docs/getting-started.md +76 -65
- package/docs/human-control.md +22 -18
- package/docs/index.mdx +101 -0
- package/docs/install-agentic.mdx +215 -0
- package/docs/install-manual.mdx +194 -0
- package/docs/instrumentation.mdx +232 -0
- package/docs/integration-patterns.md +38 -34
- package/docs/local-registry.md +16 -12
- package/docs/multi-agent-testing.md +16 -12
- package/docs/packages/babel-plugin.mdx +70 -0
- package/docs/packages/browser.mdx +111 -0
- package/docs/packages/core.mdx +106 -0
- package/docs/packages/electron.mdx +87 -0
- package/docs/packages/eslint-plugin.mdx +84 -0
- package/docs/packages/next.mdx +91 -0
- package/docs/packages/react.mdx +91 -0
- package/docs/packages/server.mdx +98 -0
- package/docs/packages/tauri.mdx +63 -0
- package/docs/packages/test.mdx +108 -0
- package/docs/packages/vite-plugin.mdx +96 -0
- package/docs/packages.mdx +217 -0
- package/docs/platform-integration.md +33 -26
- package/docs/predicates.mdx +179 -0
- package/docs/quickstart.mdx +211 -0
- package/docs/recipes.mdx +242 -0
- package/docs/skill-file.mdx +105 -0
- package/docs/state-management.mdx +197 -0
- package/docs/style.css +14 -0
- package/docs/system-map.md +47 -32
- package/docs/telemetry-contract.md +101 -46
- package/docs/telemetry-events.mdx +430 -0
- package/docs/telemetry.md +63 -50
- package/docs/testing.md +10 -6
- package/docs/token-efficiency.md +30 -26
- package/docs/tools-act-and-wait.mdx +123 -0
- package/docs/tools-act-sequence.mdx +120 -0
- package/docs/tools-act.mdx +82 -0
- package/docs/tools-assert.mdx +70 -0
- package/docs/tools-console.mdx +59 -0
- package/docs/tools-inspect.mdx +85 -0
- package/docs/tools-navigate.mdx +63 -0
- package/docs/tools-network.mdx +86 -0
- package/docs/tools-observe.mdx +89 -0
- package/docs/tools-overview.mdx +157 -0
- package/docs/tools-query.mdx +82 -0
- package/docs/tools-session-and-feedback.mdx +91 -0
- package/docs/tools-sessions.mdx +65 -0
- package/docs/tools-snapshot.mdx +75 -0
- package/docs/tools-state.mdx +81 -0
- package/docs/tools-tools-and-run.mdx +125 -0
- package/docs/tools-wait-for.mdx +66 -0
- package/docs/troubleshooting.mdx +193 -0
- package/docs/usage.md +175 -153
- package/docs/vs-chrome-devtools-mcp.mdx +82 -0
- package/docs/vs-playwright-mcp.mdx +85 -0
- package/docs/vs-screenshots.mdx +96 -0
- package/docs/what-is-reticle.mdx +63 -0
- package/docs/why-reticle.mdx +107 -0
- package/package.json +2 -2
- package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
- package/docs/matrix/2.5.0/codex-machine.json +0 -20
- package/docs/matrix/2.5.0/cursor-machine.json +0 -22
- package/docs/matrix/2.5.0/gemini-machine.json +0 -22
- package/docs/matrix/2.5.0/opencode-machine.json +0 -22
- package/docs/matrix/2.5.0/vscode-machine.json +0 -22
- package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
- package/docs/matrix/MATRIX.md +0 -26
- package/docs/matrix/README.md +0 -59
- package/docs/mint.json +0 -61
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ElementQuerySchema, ElementState, EventType, PredicateKind, StreamDirection, isDevToolingUrl, } from '@reticlehq/core';
|
|
1
|
+
import { ElementQuerySchema, ElementState, EventType, PredicateKind, QueryBy, StreamDirection, isDevToolingUrl, } from '@reticlehq/core';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { describeObserved } from './observed-in-window.js';
|
|
4
4
|
/**
|
|
@@ -9,6 +9,11 @@ import { describeObserved } from './observed-in-window.js';
|
|
|
9
9
|
* the only key supplied left a predicate that asserts nothing and passes on anything.
|
|
10
10
|
*/
|
|
11
11
|
const PREDICATE_ALIASES = {
|
|
12
|
+
// `text`/`value` on a `text` predicate: the kind is called "text", so `text:` is the first thing
|
|
13
|
+
// anyone writes for it, and `value` follows from the element query's own `value` field. Both were
|
|
14
|
+
// hard rejections, and a rejected predicate produces NO verdict at all — the drive ends with
|
|
15
|
+
// nothing rather than with a failure, which is the worst outcome of the three.
|
|
16
|
+
[PredicateKind.TEXT]: { text: 'contains', value: 'contains' },
|
|
12
17
|
// `urlContains`/`url` reported from the field: an agent that had just written
|
|
13
18
|
// `net { urlContains }` applied the same word to `route`, which spells it `contains`, and got
|
|
14
19
|
// `unrecognized_keys` with no list of what would have worked. The parallel it assumed is a fair
|
|
@@ -19,111 +24,265 @@ const PREDICATE_ALIASES = {
|
|
|
19
24
|
[PredicateKind.NET]: { url: 'urlContains' },
|
|
20
25
|
[PredicateKind.SIGNAL]: { data: 'dataMatches' },
|
|
21
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* Element-query fields an agent writes FLAT on an `element` predicate instead of nested under
|
|
29
|
+
* `query`. `reticle_query` takes exactly these at the top level, so an agent that has just located
|
|
30
|
+
* something writes the same words again when it asserts on it — and got `query: Required` plus an
|
|
31
|
+
* `unknown field` list for its trouble. Lifting them is unambiguous: they have no other meaning on
|
|
32
|
+
* this kind.
|
|
33
|
+
*/
|
|
34
|
+
const ELEMENT_QUERY_FIELDS = [
|
|
35
|
+
'by',
|
|
36
|
+
'value',
|
|
37
|
+
'role',
|
|
38
|
+
'name',
|
|
39
|
+
'text',
|
|
40
|
+
'label',
|
|
41
|
+
'placeholder',
|
|
42
|
+
'testid',
|
|
43
|
+
'alt',
|
|
44
|
+
'component',
|
|
45
|
+
];
|
|
46
|
+
/**
|
|
47
|
+
* The locator fields the browser actually CONSUMES for a given query — mirrors the precedence in
|
|
48
|
+
* `findIn` (packages/browser/src/dom/query.ts).
|
|
49
|
+
*
|
|
50
|
+
* An element query is not a conjunction. It is a first-match dispatch: `by`+`value` wins, then the
|
|
51
|
+
* component/source anchor, then `role` (which alone also consumes `name`), then the first of
|
|
52
|
+
* text/label/placeholder/testid/alt that is present. Every OTHER field the caller wrote is dropped on
|
|
53
|
+
* the floor, silently, and the match reported as if the whole query had been honoured.
|
|
54
|
+
*
|
|
55
|
+
* Duplicated here on purpose. The alternative is to send the question to the browser, and the browser
|
|
56
|
+
* cannot answer it: by the time a match comes back, the fields it ignored are indistinguishable from
|
|
57
|
+
* the fields it used.
|
|
58
|
+
*/
|
|
59
|
+
function usedQueryFields(query) {
|
|
60
|
+
const used = new Set();
|
|
61
|
+
if (query.by !== undefined && query.value !== undefined) {
|
|
62
|
+
used.add('by').add('value');
|
|
63
|
+
if (QueryBy.ROLE === query.by)
|
|
64
|
+
used.add('name');
|
|
65
|
+
if (QueryBy.COMPONENT === query.by)
|
|
66
|
+
used.add('component');
|
|
67
|
+
return used;
|
|
68
|
+
}
|
|
69
|
+
if (query.component !== undefined || query.source !== undefined)
|
|
70
|
+
return used.add('component');
|
|
71
|
+
if (query.role !== undefined)
|
|
72
|
+
return used.add('role').add('name');
|
|
73
|
+
for (const field of ['text', 'label', 'placeholder', 'testid', 'alt']) {
|
|
74
|
+
if (query[field] !== undefined)
|
|
75
|
+
return used.add(field);
|
|
76
|
+
}
|
|
77
|
+
return used;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* How a dropped field is checked back on the server, against the descriptor the match returned.
|
|
81
|
+
*
|
|
82
|
+
* `value` is the field this exists for: `{ role: "textbox", name: "GST amount", value: "274.58" }`
|
|
83
|
+
* read as a locator has no `by`, so the value half was discarded and the predicate collapsed to "a
|
|
84
|
+
* textbox named GST amount exists" — trivially true against an EMPTY field. Comparison is TRIMMED and
|
|
85
|
+
* exact: an input's value is a value, not prose, and a trailing space in either the app or the
|
|
86
|
+
* predicate is not a finding anybody wants. `""` asserts the field is empty, which describe() reports
|
|
87
|
+
* by omitting the field entirely.
|
|
88
|
+
*
|
|
89
|
+
* `role`/`name`/`value` compare against exactly what `reticle_query` REPORTS, so the words an agent
|
|
90
|
+
* copies out of a query result are the words that match here. `text` is a substring match, matching
|
|
91
|
+
* Testing Library's `exact: false`, and falls back to the name because describe() omits `text` when it
|
|
92
|
+
* equals the accessible name.
|
|
93
|
+
*/
|
|
94
|
+
const RESIDUAL_CHECKS = {
|
|
95
|
+
value: (element, want) => (element.value ?? '').trim() === want.trim(),
|
|
96
|
+
role: (element, want) => element.role === want,
|
|
97
|
+
name: (element, want) => element.name.trim() === want.trim(),
|
|
98
|
+
text: (element, want) => (element.text ?? element.name).includes(want),
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Split a query's dropped fields into the ones the server can still enforce and the ones it cannot.
|
|
102
|
+
*
|
|
103
|
+
* The alternative — refuse every dropped field — breaks calls that work today and that our own
|
|
104
|
+
* cheatsheet advertises (`{ role: "button", text: "Save" }`), and breaks them into no verdict at all.
|
|
105
|
+
* Enforcing what we can and refusing only the rest keeps those calls working AS WRITTEN, which is the
|
|
106
|
+
* outcome the caller was already assuming.
|
|
107
|
+
*/
|
|
108
|
+
export function residualQueryChecks(query) {
|
|
109
|
+
const used = usedQueryFields(query);
|
|
110
|
+
const checks = [];
|
|
111
|
+
const unusable = [];
|
|
112
|
+
for (const field of ELEMENT_QUERY_FIELDS) {
|
|
113
|
+
const want = query[field];
|
|
114
|
+
if (want === undefined || used.has(field))
|
|
115
|
+
continue;
|
|
116
|
+
if ('string' === typeof want && RESIDUAL_CHECKS[field] !== undefined)
|
|
117
|
+
checks.push([field, want]);
|
|
118
|
+
else
|
|
119
|
+
unusable.push(field);
|
|
120
|
+
}
|
|
121
|
+
return { checks, unusable };
|
|
122
|
+
}
|
|
123
|
+
/** Does this element satisfy every field the locator dropped? */
|
|
124
|
+
export function satisfiesResiduals(element, checks) {
|
|
125
|
+
return checks.every(([field, want]) => true === RESIDUAL_CHECKS[field]?.(element, want));
|
|
126
|
+
}
|
|
127
|
+
/** How the element's own reading of a dropped field should be REPORTED back on a failure. */
|
|
128
|
+
export function describeResidual(element, field) {
|
|
129
|
+
const reading = 'value' === field
|
|
130
|
+
? (element.value ?? '')
|
|
131
|
+
: 'text' === field
|
|
132
|
+
? (element.text ?? element.name)
|
|
133
|
+
: 'role' === field
|
|
134
|
+
? element.role
|
|
135
|
+
: element.name;
|
|
136
|
+
return `${element.role} "${element.name}" ${field}=${JSON.stringify(reading)}`;
|
|
137
|
+
}
|
|
22
138
|
/** Rename known aliases before parse; an explicit canonical key always wins. */
|
|
23
139
|
function applyPredicateAliases(input) {
|
|
24
140
|
if (typeof input !== 'object' || null === input || Array.isArray(input))
|
|
25
141
|
return input;
|
|
26
142
|
const obj = input;
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
143
|
+
const kind = 'string' === typeof obj['kind'] ? obj['kind'] : '';
|
|
144
|
+
const aliases = PREDICATE_ALIASES[kind];
|
|
145
|
+
let out = obj;
|
|
146
|
+
if (aliases !== undefined) {
|
|
147
|
+
out = { ...obj };
|
|
148
|
+
for (const [from, to] of Object.entries(aliases)) {
|
|
149
|
+
if (out[from] === undefined)
|
|
150
|
+
continue;
|
|
151
|
+
if (out[to] === undefined)
|
|
152
|
+
out[to] = out[from];
|
|
153
|
+
delete out[from];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return PredicateKind.ELEMENT === kind ? liftElementQuery(out) : out;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Fold flat query fields into `query`. An explicit `query` wins outright — a caller that supplied
|
|
160
|
+
* both told us which one it meant, and merging the two would invent a locator neither side wrote.
|
|
161
|
+
*/
|
|
162
|
+
function liftElementQuery(obj) {
|
|
163
|
+
const loose = ELEMENT_QUERY_FIELDS.filter((field) => obj[field] !== undefined);
|
|
164
|
+
if (0 === loose.length)
|
|
165
|
+
return obj;
|
|
30
166
|
const out = { ...obj };
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
out[to] = out[from];
|
|
36
|
-
delete out[from];
|
|
167
|
+
const query = {};
|
|
168
|
+
for (const field of loose) {
|
|
169
|
+
query[field] = out[field];
|
|
170
|
+
delete out[field];
|
|
37
171
|
}
|
|
172
|
+
if (out['query'] === undefined)
|
|
173
|
+
out['query'] = query;
|
|
38
174
|
return out;
|
|
39
175
|
}
|
|
40
176
|
/**
|
|
41
177
|
* Strict on every branch. A key that is nobody's spelling is now a schema error naming it, instead of
|
|
42
178
|
* a stripped field and a green — see predicate-strict.test.ts for the MCP session that found this.
|
|
179
|
+
*
|
|
180
|
+
* Built on demand rather than at module scope: the `allOf`/`anyOf`/`not` branches reference
|
|
181
|
+
* `PredicateSchema` itself, which does not exist yet while this module is initialising. Calling it
|
|
182
|
+
* after init is what makes the union introspectable — see `predicateFieldsFor`.
|
|
43
183
|
*/
|
|
44
|
-
|
|
45
|
-
z
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
184
|
+
function predicateUnion() {
|
|
185
|
+
return z.discriminatedUnion('kind', [
|
|
186
|
+
z
|
|
187
|
+
.object({
|
|
188
|
+
kind: z.literal(PredicateKind.ELEMENT),
|
|
189
|
+
query: ElementQuerySchema,
|
|
190
|
+
state: z.nativeEnum(ElementState).optional(),
|
|
191
|
+
absent: z.boolean().optional(),
|
|
192
|
+
})
|
|
193
|
+
.strict(),
|
|
194
|
+
z
|
|
195
|
+
.object({
|
|
196
|
+
kind: z.literal(PredicateKind.TEXT),
|
|
197
|
+
contains: z.string(),
|
|
198
|
+
visible: z.boolean().optional(),
|
|
199
|
+
absent: z.boolean().optional(),
|
|
200
|
+
})
|
|
201
|
+
.strict(),
|
|
202
|
+
z
|
|
203
|
+
.object({
|
|
204
|
+
kind: z.literal(PredicateKind.NET),
|
|
205
|
+
method: z.string().optional(),
|
|
206
|
+
urlContains: z.string().optional(),
|
|
207
|
+
status: z.number().optional(),
|
|
208
|
+
ok: z.boolean().optional(),
|
|
209
|
+
since: z.number().optional(),
|
|
210
|
+
count: z.number().int().nonnegative().optional(),
|
|
211
|
+
})
|
|
212
|
+
.strict(),
|
|
213
|
+
z
|
|
214
|
+
.object({
|
|
215
|
+
kind: z.literal(PredicateKind.ROUTE),
|
|
216
|
+
pathname: z.string().optional(),
|
|
217
|
+
contains: z.string().optional(),
|
|
218
|
+
since: z.number().optional(),
|
|
219
|
+
})
|
|
220
|
+
.strict(),
|
|
221
|
+
z
|
|
222
|
+
.object({
|
|
223
|
+
kind: z.literal(PredicateKind.CONSOLE),
|
|
224
|
+
level: z.string().optional(),
|
|
225
|
+
absent: z.boolean().optional(),
|
|
226
|
+
since: z.number().optional(),
|
|
227
|
+
})
|
|
228
|
+
.strict(),
|
|
229
|
+
z
|
|
230
|
+
.object({
|
|
231
|
+
kind: z.literal(PredicateKind.ANIMATION),
|
|
232
|
+
name: z.string().optional(),
|
|
233
|
+
target: z.string().optional(),
|
|
234
|
+
completed: z.boolean().optional(),
|
|
235
|
+
since: z.number().optional(),
|
|
236
|
+
})
|
|
237
|
+
.strict(),
|
|
238
|
+
z
|
|
239
|
+
.object({
|
|
240
|
+
kind: z.literal(PredicateKind.SIGNAL),
|
|
241
|
+
name: z.string().optional(),
|
|
242
|
+
dataMatches: z.record(z.unknown()).optional(),
|
|
243
|
+
since: z.number().optional(),
|
|
244
|
+
})
|
|
245
|
+
.strict(),
|
|
246
|
+
z
|
|
247
|
+
.object({
|
|
248
|
+
kind: z.literal(PredicateKind.STATE),
|
|
249
|
+
store: z.string().optional(),
|
|
250
|
+
path: z.string(),
|
|
251
|
+
equals: z.unknown().optional(),
|
|
252
|
+
})
|
|
253
|
+
.strict(),
|
|
254
|
+
z
|
|
255
|
+
.object({
|
|
256
|
+
kind: z.literal(PredicateKind.SETTLED),
|
|
257
|
+
quietMs: z.number().positive().optional(),
|
|
258
|
+
})
|
|
259
|
+
.strict(),
|
|
260
|
+
z
|
|
261
|
+
.object({ kind: z.literal(PredicateKind.ALL_OF), predicates: z.array(PredicateSchema) })
|
|
262
|
+
.strict(),
|
|
263
|
+
z
|
|
264
|
+
.object({ kind: z.literal(PredicateKind.ANY_OF), predicates: z.array(PredicateSchema) })
|
|
265
|
+
.strict(),
|
|
266
|
+
z.object({ kind: z.literal(PredicateKind.NOT), predicate: PredicateSchema }).strict(),
|
|
267
|
+
]);
|
|
268
|
+
}
|
|
269
|
+
export const PredicateSchema = z.lazy(() => z.preprocess(applyPredicateAliases, predicateUnion()));
|
|
270
|
+
/**
|
|
271
|
+
* The fields a given predicate kind accepts, read off the schema itself.
|
|
272
|
+
*
|
|
273
|
+
* Derived rather than listed so the two can never disagree: a rejection message that names a stale
|
|
274
|
+
* field set is worse than one that names none, because the agent trusts it and retries into the same
|
|
275
|
+
* wall. Empty for a kind that is not in the union.
|
|
276
|
+
*/
|
|
277
|
+
export function predicateFieldsFor(kind) {
|
|
278
|
+
for (const option of predicateUnion().options) {
|
|
279
|
+
const literal = option.shape['kind'];
|
|
280
|
+
if (literal instanceof z.ZodLiteral && literal.value === kind) {
|
|
281
|
+
return Object.keys(option.shape).filter((field) => 'kind' !== field);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
127
286
|
function str(value) {
|
|
128
287
|
return 'string' === typeof value ? value : undefined;
|
|
129
288
|
}
|
|
@@ -189,7 +348,37 @@ export function matchValue(got, want) {
|
|
|
189
348
|
}
|
|
190
349
|
return true;
|
|
191
350
|
}
|
|
192
|
-
return got
|
|
351
|
+
return structurallyEqual(got, want);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Value equality for the leaf comparison, because `===` could never match a literal.
|
|
355
|
+
*
|
|
356
|
+
* The expected side of a predicate is parsed out of the agent's JSON, so it is a fresh object every
|
|
357
|
+
* time — reference equality made `equals: ["a", "b"]` false against a store holding exactly
|
|
358
|
+
* `["a", "b"]`, and there was no value of the app's state that could have made it true. The mirror of
|
|
359
|
+
* a false green: an assertion nobody could satisfy, on the commonest thing there is to assert about.
|
|
360
|
+
*
|
|
361
|
+
* Deliberately strict about shape. An array is not an object, an extra key is a difference, and order
|
|
362
|
+
* is part of a list's value — `equals` means equals; `dataMatches` is the field-by-field one.
|
|
363
|
+
*/
|
|
364
|
+
function structurallyEqual(got, want) {
|
|
365
|
+
if (got === want)
|
|
366
|
+
return true;
|
|
367
|
+
if (null === got || null === want)
|
|
368
|
+
return false;
|
|
369
|
+
if ('object' !== typeof got || 'object' !== typeof want)
|
|
370
|
+
return false;
|
|
371
|
+
if (Array.isArray(got) !== Array.isArray(want))
|
|
372
|
+
return false;
|
|
373
|
+
if (Array.isArray(got) && Array.isArray(want)) {
|
|
374
|
+
return got.length === want.length && got.every((v, i) => structurallyEqual(v, want[i]));
|
|
375
|
+
}
|
|
376
|
+
const a = got;
|
|
377
|
+
const b = want;
|
|
378
|
+
const keys = Object.keys(a);
|
|
379
|
+
if (keys.length !== Object.keys(b).length)
|
|
380
|
+
return false;
|
|
381
|
+
return keys.every((k) => k in b && structurallyEqual(a[k], b[k]));
|
|
193
382
|
}
|
|
194
383
|
/** Shallow JSON pattern match: each key in `pattern` must match (see matchValue). */
|
|
195
384
|
function dataMatches(actual, pattern) {
|
|
@@ -419,7 +608,7 @@ export function evalSignal(events, p) {
|
|
|
419
608
|
? `signal '${p.name ?? '(any)'}' fired ${String(sameName.length)}x, payload: ${JSON.stringify(sameName[0])}`
|
|
420
609
|
: // Name what DID fire: a typo'd signal name and a genuinely dead action produce the same
|
|
421
610
|
// sentence otherwise, and the agent cannot tell them apart. See observed-in-window.ts.
|
|
422
|
-
`signal '${p.name ?? '(any)'}' never fired
|
|
611
|
+
`signal '${p.name ?? '(any)'}' never fired; ${describeObserved('signals', events.filter((e) => e.type === EventType.SIGNAL).map((e) => str(e.data['name']) ?? ''))}`,
|
|
423
612
|
expected: p.dataMatches === undefined
|
|
424
613
|
? `signal '${p.name ?? '(any)'}' to fire`
|
|
425
614
|
: `signal '${p.name ?? '(any)'}' with payload matching ${JSON.stringify(p.dataMatches)}`,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* One readable sentence when a predicate does not parse — never the zod array.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* In the field a meaningful share of all tool errors were **a serialized zod issue array**, all on
|
|
5
5
|
* `reticle_act_and_wait`, `reticle_wait_for` and `reticle_assert` — the three tools that produced
|
|
6
6
|
* every action-derived finding in the dataset. The least readable error we emit was landing on the
|
|
7
7
|
* highest-value path, and an agent had to `JSON.parse` an error string to learn which field it got
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* One readable sentence when a predicate does not parse — never the zod array.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* In the field a meaningful share of all tool errors were **a serialized zod issue array**, all on
|
|
5
5
|
* `reticle_act_and_wait`, `reticle_wait_for` and `reticle_assert` — the three tools that produced
|
|
6
6
|
* every action-derived finding in the dataset. The least readable error we emit was landing on the
|
|
7
7
|
* highest-value path, and an agent had to `JSON.parse` an error string to learn which field it got
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* show a valid call.
|
|
14
14
|
*/
|
|
15
15
|
import { z } from 'zod';
|
|
16
|
-
import {
|
|
16
|
+
import { PredicateKind } from '@reticlehq/core';
|
|
17
|
+
import { PredicateSchema, predicateFieldsFor } from './predicate-eval.js';
|
|
17
18
|
/** A valid call, short enough to sit inside an error without becoming the error. */
|
|
18
19
|
const EXAMPLE = '{ kind: "signal", name: "todos:loaded" }';
|
|
19
20
|
/** `path: ["net","urlContains"]` → `net.urlContains`; an empty path means the object itself. */
|
|
@@ -49,6 +50,19 @@ export function parsePredicate(input) {
|
|
|
49
50
|
// back is how the zod array became unreadable in the first place.
|
|
50
51
|
const issues = parsed.error.issues.slice(0, 3).map(describeIssue).join('; ');
|
|
51
52
|
throw new Error(`that predicate did not parse (kind "${kind}"): ${issues}. Nothing ran — the predicate was ` +
|
|
52
|
-
`not evaluated, so no verdict was produced.
|
|
53
|
-
`
|
|
53
|
+
`not evaluated, so no verdict was produced. ${accepted(kind)} ` +
|
|
54
|
+
`A valid call looks like: ${EXAMPLE}`);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* What WOULD have worked, in the same breath as what did not.
|
|
58
|
+
*
|
|
59
|
+
* Telling an agent only which field is wrong leaves it guessing again, and each guess costs another
|
|
60
|
+
* round trip on the one call path that produces verdicts. Naming the accepted fields — or, when the
|
|
61
|
+
* kind itself is the mistake, the accepted kinds — makes the retry informed instead.
|
|
62
|
+
*/
|
|
63
|
+
function accepted(kind) {
|
|
64
|
+
const fields = predicateFieldsFor(kind);
|
|
65
|
+
if (0 < fields.length)
|
|
66
|
+
return `${kind} accepts: ${fields.join(', ')}.`;
|
|
67
|
+
return `"${kind}" is not a predicate kind — use one of: ${Object.values(PredicateKind).join(', ')}.`;
|
|
54
68
|
}
|