@reticlehq/server 2.5.0 → 2.7.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/SKILL.md +226 -26
- package/dist/bridge/bridge.d.ts +18 -1
- package/dist/bridge/bridge.js +59 -6
- package/dist/bridge/bridge.test-harness.d.ts +8 -0
- package/dist/bridge/bridge.test-harness.js +9 -1
- package/dist/cli/chromium-hint.d.ts +40 -0
- package/dist/cli/chromium-hint.js +75 -0
- package/dist/cli/cli-doctor.d.ts +0 -5
- package/dist/cli/cli-doctor.js +74 -14
- package/dist/cli/cli-launch.d.ts +1 -0
- package/dist/cli/cli-launch.js +4 -1
- package/dist/cli/cli-parse-feedback.d.ts +31 -0
- package/dist/cli/cli-parse-feedback.js +56 -0
- package/dist/cli/cli-parse.d.ts +5 -9
- package/dist/cli/cli-parse.js +7 -23
- package/dist/cli/cli-port.js +20 -1
- package/dist/cli/doctor-daemon-line.d.ts +37 -0
- package/dist/cli/doctor-daemon-line.js +44 -0
- package/dist/cli/port-holder.d.ts +43 -0
- package/dist/cli/port-holder.js +99 -0
- package/dist/cli.js +170 -19
- package/dist/daemon/daemon-resilience.d.ts +24 -0
- package/dist/daemon/daemon-resilience.js +105 -17
- package/dist/daemon/daemon.js +26 -1
- package/dist/daemon/heartbeat.d.ts +115 -0
- package/dist/daemon/heartbeat.js +141 -0
- package/dist/daemon/idle-shutdown.d.ts +13 -5
- package/dist/daemon/idle-shutdown.js +14 -6
- package/dist/daemon/loopback-alias.d.ts +32 -0
- package/dist/daemon/loopback-alias.js +80 -0
- package/dist/daemon/port-presence.d.ts +76 -0
- package/dist/daemon/port-presence.js +93 -0
- package/dist/daemon/state-dir.d.ts +7 -0
- package/dist/daemon/state-dir.js +59 -0
- package/dist/daemon/unclean-predecessor.d.ts +23 -0
- package/dist/daemon/unclean-predecessor.js +27 -0
- package/dist/events/predicate-eval.d.ts +17 -3
- package/dist/events/predicate-eval.js +183 -96
- package/dist/events/predicate-parse.d.ts +23 -0
- package/dist/events/predicate-parse.js +68 -0
- package/dist/events/predicate.d.ts +5 -0
- package/dist/events/predicate.js +17 -3
- package/dist/events/ring-buffer.d.ts +8 -0
- package/dist/events/ring-buffer.js +35 -17
- package/dist/flows/flow-replay.d.ts +1 -1
- package/dist/flows/flow-replay.js +19 -2
- package/dist/flows/flow-step-runners.js +2 -3
- package/dist/flows/flow-tools.js +10 -14
- package/dist/flows/role-drift-reason.d.ts +20 -0
- package/dist/flows/role-drift-reason.js +39 -0
- package/dist/flows/suite-flakes.d.ts +33 -0
- package/dist/flows/suite-flakes.js +32 -0
- package/dist/honesty/blind-spots.d.ts +19 -0
- package/dist/honesty/blind-spots.js +33 -0
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +9 -1
- package/dist/honesty/verified.d.ts +10 -1
- package/dist/honesty/verified.js +42 -3
- package/dist/http-server.js +20 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +36 -3
- package/dist/init/agent-rules.d.ts +18 -0
- package/dist/init/agent-rules.js +28 -5
- package/dist/init/cra.d.ts +13 -0
- package/dist/init/cra.js +16 -0
- package/dist/init/detect.d.ts +14 -0
- package/dist/init/detect.js +23 -0
- package/dist/init/mcp-clients.d.ts +81 -0
- package/dist/init/mcp-clients.js +248 -0
- package/dist/init/node-io.js +8 -2
- package/dist/init/non-js-project.d.ts +27 -0
- package/dist/init/non-js-project.js +60 -0
- package/dist/init/plan-framework.d.ts +9 -0
- package/dist/init/plan-framework.js +55 -3
- package/dist/init/plan.d.ts +14 -0
- package/dist/init/plan.js +93 -12
- package/dist/init/run.d.ts +18 -1
- package/dist/init/run.js +169 -23
- package/dist/init/snippets.d.ts +20 -1
- package/dist/init/snippets.js +71 -6
- package/dist/init/windows-quote.d.ts +38 -0
- package/dist/init/windows-quote.js +70 -0
- package/dist/mcp/mcp-outage.d.ts +1 -0
- package/dist/mcp/mcp-outage.js +8 -1
- package/dist/mcp/mcp-proxy.d.ts +11 -1
- package/dist/mcp/mcp-proxy.js +39 -9
- package/dist/mcp/mcp.js +64 -2
- package/dist/mcp/proxy-lifecycle.d.ts +19 -0
- package/dist/mcp/proxy-lifecycle.js +21 -0
- package/dist/pool/browser-pool.d.ts +9 -0
- package/dist/pool/browser-pool.js +38 -9
- package/dist/pool/playwright-launcher.js +10 -2
- package/dist/session/attachment-history.d.ts +46 -0
- package/dist/session/attachment-history.js +65 -0
- 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 +11 -4
- package/dist/session/no-session-diagnosis.js +95 -14
- package/dist/session/no-session-watch.d.ts +3 -1
- package/dist/session/no-session-watch.js +10 -2
- package/dist/session/pending-commands.js +1 -0
- package/dist/session/review-store.d.ts +19 -0
- package/dist/session/review-store.js +34 -5
- package/dist/session/session-info.d.ts +17 -1
- package/dist/session/session-info.js +5 -1
- package/dist/session/session-manager.d.ts +12 -0
- package/dist/session/session-manager.js +83 -3
- package/dist/session/session.d.ts +9 -0
- package/dist/session/session.js +20 -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/crash-cause.d.ts +47 -0
- package/dist/telemetry/crash-cause.js +104 -0
- package/dist/telemetry/daemon-telemetry.d.ts +25 -7
- package/dist/telemetry/daemon-telemetry.js +32 -13
- package/dist/telemetry/error-class.d.ts +26 -0
- package/dist/telemetry/error-class.js +52 -0
- package/dist/telemetry/feedback-cli.d.ts +7 -2
- package/dist/telemetry/feedback-cli.js +49 -5
- package/dist/telemetry/feedback-context.d.ts +1 -6
- package/dist/telemetry/feedback-context.js +64 -4
- package/dist/telemetry/git-facts.d.ts +7 -0
- package/dist/telemetry/git-facts.js +4 -4
- package/dist/telemetry/project-profile.js +2 -1
- package/dist/telemetry/session-metrics.d.ts +45 -3
- package/dist/telemetry/session-metrics.js +247 -9
- package/dist/telemetry/telemetry.d.ts +21 -1
- package/dist/telemetry/telemetry.js +34 -9
- package/dist/telemetry/verification-of.js +26 -1
- package/dist/tools/act-tools.js +63 -14
- package/dist/tools/assert-verdict.d.ts +7 -1
- package/dist/tools/assert-verdict.js +22 -3
- package/dist/tools/browser-tools.js +8 -2
- package/dist/tools/contract-tools.d.ts +0 -5
- package/dist/tools/contract-tools.js +26 -0
- package/dist/tools/dynamic-tools.js +6 -1
- package/dist/tools/error-recovery.d.ts +14 -0
- package/dist/tools/error-recovery.js +89 -8
- package/dist/tools/feedback-invite.d.ts +48 -0
- package/dist/tools/feedback-invite.js +71 -0
- package/dist/tools/invoke-tool.d.ts +0 -6
- package/dist/tools/invoke-tool.js +76 -7
- package/dist/tools/navigate-arrival.d.ts +26 -0
- package/dist/tools/navigate-arrival.js +60 -0
- package/dist/tools/navigate-result.d.ts +15 -1
- package/dist/tools/navigate-result.js +17 -10
- package/dist/tools/observe-tools.js +22 -4
- package/dist/tools/real-input-attempt.js +10 -5
- package/dist/tools/tool-kit.d.ts +60 -5
- package/dist/tools/tool-kit.js +55 -24
- package/dist/tools/tool-surface.js +13 -0
- package/dist/tools/tools.js +22 -0
- 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 +55 -0
- package/docs/agent-cheatsheet.md +15 -3
- package/docs/benchmarks.md +1 -1
- package/docs/fixtures-dispatch-receiver.yml +76 -0
- package/docs/fixtures.md +17 -0
- package/docs/gate-plan.md +98 -0
- package/docs/gates.md +117 -0
- package/docs/local-registry.md +1 -1
- package/docs/matrix/2.5.0/claude-code-machine.json +19 -0
- package/docs/matrix/2.5.0/codex-machine.json +20 -0
- package/docs/matrix/2.5.0/cursor-machine.json +22 -0
- package/docs/matrix/2.5.0/gemini-machine.json +22 -0
- package/docs/matrix/2.5.0/opencode-machine.json +22 -0
- package/docs/matrix/2.5.0/vscode-machine.json +22 -0
- package/docs/matrix/2.5.0/windsurf-machine.json +22 -0
- package/docs/matrix/MATRIX.md +26 -0
- package/docs/matrix/README.md +59 -0
- package/docs/mint.json +1 -1
- package/docs/system-map.md +152 -0
- package/docs/telemetry-contract.md +15 -3
- package/docs/telemetry.md +16 -1
- package/docs/token-efficiency.md +2 -2
- package/package.json +2 -2
|
@@ -7,10 +7,4 @@ export declare const SESSION_BOUND_TOOLS: ReadonlySet<string>;
|
|
|
7
7
|
* `sessionId`-bearing tool to be classified into exactly one set (bound XOR exempt).
|
|
8
8
|
*/
|
|
9
9
|
export declare const SESSION_EXEMPT_TOOLS: ReadonlySet<string>;
|
|
10
|
-
/**
|
|
11
|
-
* The single entry point both the MCP server and the programmatic invoker call instead of
|
|
12
|
-
* `tool.handler` directly. Runs the handler, then — for a live-session tool returning a plain
|
|
13
|
-
* object that did not already include `session` — splices the health envelope on. Idempotent
|
|
14
|
-
* (handlers that already add health are left untouched) and never alters non-object results.
|
|
15
|
-
*/
|
|
16
10
|
export declare function runTool(tool: ToolDef, deps: ToolDeps, args: Record<string, unknown>): Promise<unknown>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { healthEnvelope } from '../session/session-health.js';
|
|
2
|
-
import { TelemetryActor, TelemetryEventKind } from '@reticlehq/core';
|
|
2
|
+
import { TelemetryActor, TelemetryEventKind, TRANSPORT_LIMITS, } from '@reticlehq/core';
|
|
3
3
|
import { getSessionMetrics } from '../telemetry/session-metrics.js';
|
|
4
4
|
import { getTelemetry } from '../telemetry/telemetry.js';
|
|
5
5
|
import { takeUpdateNudge } from '../update/update-nudge.js';
|
|
@@ -8,10 +8,12 @@ import { noteToolCall } from '../daemon/daemon-usefulness.js';
|
|
|
8
8
|
import { bugsInResult } from '../telemetry/bug-found.js';
|
|
9
9
|
import { verificationOf } from '../telemetry/verification-of.js';
|
|
10
10
|
import { asString } from './tools-helpers.js';
|
|
11
|
+
import { EnvelopeKey } from './tool-kit.js';
|
|
11
12
|
import { ReticleTool } from './tool-names.js';
|
|
12
13
|
import { takeFeedbackPrompt } from './feedback-tools.js';
|
|
13
14
|
import { takeFeedbackUndelivered } from '../telemetry/feedback-delivery.js';
|
|
14
15
|
import { span } from '../trace.js';
|
|
16
|
+
import { frictionOf, inviteFor } from './feedback-invite.js';
|
|
15
17
|
/**
|
|
16
18
|
* The live-session tools whose result MUST carry the
|
|
17
19
|
* session-health envelope. Owned in ONE place — not retrofitted per handler — so a throttled tab
|
|
@@ -138,6 +140,27 @@ function reportBugsFound(toolName, result) {
|
|
|
138
140
|
* object that did not already include `session` — splices the health envelope on. Idempotent
|
|
139
141
|
* (handlers that already add health are left untouched) and never alters non-object results.
|
|
140
142
|
*/
|
|
143
|
+
/**
|
|
144
|
+
* The name of the first argument whose string value exceeds the transport bound, if any.
|
|
145
|
+
*
|
|
146
|
+
* One level of nesting is searched because that is where callers put payloads — `reticle_act`'s
|
|
147
|
+
* `args.value`, and the fuzz's own `huge-string` shape. Deeper recursion would cost more than it
|
|
148
|
+
* buys: the failure is a single huge string, not a deeply nested structure of small ones.
|
|
149
|
+
*/
|
|
150
|
+
function firstOversizedArg(args) {
|
|
151
|
+
const tooLong = (value) => 'string' === typeof value && value.length > TRANSPORT_LIMITS.MAX_STRING_LENGTH;
|
|
152
|
+
for (const [key, value] of Object.entries(args)) {
|
|
153
|
+
if (tooLong(value))
|
|
154
|
+
return key;
|
|
155
|
+
if ('object' === typeof value && null !== value && !Array.isArray(value)) {
|
|
156
|
+
for (const [inner, nested] of Object.entries(value)) {
|
|
157
|
+
if (tooLong(nested))
|
|
158
|
+
return `${key}.${inner}`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
141
164
|
export async function runTool(tool, deps, args) {
|
|
142
165
|
// Both dispatch paths (MCP + programmatic) pass through here — the one place "which tool is mostly
|
|
143
166
|
// used" can be counted. This used to EMIT an event per call; it now increments an in-process counter
|
|
@@ -150,6 +173,28 @@ export async function runTool(tool, deps, args) {
|
|
|
150
173
|
// closure carries this call's own identity, which is also what makes peak-concurrency measurable.
|
|
151
174
|
// A daemon that has served even one tool call is doing a job for somebody; see daemon-usefulness.
|
|
152
175
|
noteToolCall();
|
|
176
|
+
// An oversized argument is refused BEFORE the handler deserialises it.
|
|
177
|
+
//
|
|
178
|
+
// tool-fuzz failed CI on the invariant that matters most — `every tool answers every hostile call`
|
|
179
|
+
// — for a ~100KB string. Seen twice with different tools; the earlier one came back
|
|
180
|
+
// `-32001 sse_aborted` with the tool surface dropping 48→17, which is a RESTARTED daemon. The
|
|
181
|
+
// shape is: oversized argument → the daemon dies → the proxy answers the in-flight call with
|
|
182
|
+
// transport loss, and the next assertion is talking to a different daemon.
|
|
183
|
+
//
|
|
184
|
+
// `TRANSPORT_LIMITS.MAX_STRING_LENGTH` already bounds what crosses the bridge; a tool argument
|
|
185
|
+
// arrives over MCP stdio and never met it. Here, because this is the one place every invocation
|
|
186
|
+
// routes through, so a tool added later inherits the bound instead of having to remember it.
|
|
187
|
+
//
|
|
188
|
+
// A refusal is a normal answer. An UNANSWERED call is a hung agent, and that is the failure this
|
|
189
|
+
// prevents: the agent learns its argument was too big, which is something it can fix.
|
|
190
|
+
const oversized = firstOversizedArg(args);
|
|
191
|
+
if (oversized !== undefined) {
|
|
192
|
+
return {
|
|
193
|
+
error: `argument '${oversized}' is larger than ${String(TRANSPORT_LIMITS.MAX_STRING_LENGTH)} ` +
|
|
194
|
+
'characters, which is the most Reticle moves in one call. Nothing ran. Send a smaller ' +
|
|
195
|
+
'value — a selector or an id rather than a document, or a slice of the text you need.',
|
|
196
|
+
};
|
|
197
|
+
}
|
|
153
198
|
// An ACTION is what gets abandoned. Counted here, against verifications, so "the agent drove the
|
|
154
199
|
// page and then wandered off" becomes a number instead of an impression.
|
|
155
200
|
if (ACTION_TOOLS.has(tool.name))
|
|
@@ -218,12 +263,12 @@ export async function runTool(tool, deps, args) {
|
|
|
218
263
|
? raw
|
|
219
264
|
: {
|
|
220
265
|
...raw,
|
|
221
|
-
...(prompt !== undefined ? {
|
|
222
|
-
...(update !== undefined ? {
|
|
223
|
-
...(skew !== undefined ? {
|
|
266
|
+
...(prompt !== undefined ? { [EnvelopeKey.FEEDBACK_PROMPT]: prompt } : {}),
|
|
267
|
+
...(update !== undefined ? { [EnvelopeKey.UPDATE_AVAILABLE]: update } : {}),
|
|
268
|
+
...(skew !== undefined ? { [EnvelopeKey.VERSION_SKEW]: skew } : {}),
|
|
224
269
|
...(undelivered !== undefined
|
|
225
270
|
? {
|
|
226
|
-
|
|
271
|
+
[EnvelopeKey.FEEDBACK_UNDELIVERED]: `your earlier report did NOT send: ${undelivered}. Tell the human what you found so it is not lost.`,
|
|
227
272
|
}
|
|
228
273
|
: {}),
|
|
229
274
|
};
|
|
@@ -242,9 +287,33 @@ export async function runTool(tool, deps, args) {
|
|
|
242
287
|
// one-time pool-lease reminder or the age cleanup nudge. Splice them regardless.
|
|
243
288
|
const lease = resolved.takeSessionLease();
|
|
244
289
|
if (lease !== undefined)
|
|
245
|
-
envelope[
|
|
290
|
+
envelope[EnvelopeKey.SESSION_LEASE] = lease;
|
|
246
291
|
const warning = resolved.ageWarning();
|
|
247
292
|
if (warning !== undefined)
|
|
248
|
-
envelope[
|
|
293
|
+
envelope[EnvelopeKey.SESSION_AGE_WARNING] = warning;
|
|
294
|
+
// Ask for a verdict when the agent has driven the page and not asked for one. Almost every
|
|
295
|
+
// verdict-less session in the field never called a verdict-producing tool ONCE;
|
|
296
|
+
// the counter behind this already existed and was reported only to us. One-shot per abandoned
|
|
297
|
+
// run, same discipline as the pool lease — a hint on every call is noise that gets tuned out.
|
|
298
|
+
const unverified = getSessionMetrics().takeUnverifiedNudge();
|
|
299
|
+
if (unverified !== undefined)
|
|
300
|
+
envelope[EnvelopeKey.VERIFY_NEXT] = unverified;
|
|
301
|
+
// Invite feedback at the moment of friction, worded for what just happened. Feedback produced
|
|
302
|
+
// ~11 of this release's fixes from 14 reports; the handshake instruction is what got those 14,
|
|
303
|
+
// and this closes the gap it leaves — the agent mid-problem, not thinking about feedback tooling.
|
|
304
|
+
// Counted so `feedback_submitted / feedbackPrompted` can tell us whether it works at all.
|
|
305
|
+
const friction = frictionOf({
|
|
306
|
+
unknownTool: false,
|
|
307
|
+
verifiedUnknown: 'unknown' === result.verified,
|
|
308
|
+
repeatRun: getSessionMetrics().currentRun,
|
|
309
|
+
errored: 'error' in result,
|
|
310
|
+
// A verdict is progress. Without this, three successful act_and_wait calls in a row — the exact
|
|
311
|
+
// loop this release exists to encourage — get told they are stuck.
|
|
312
|
+
producedVerdict: 'verified' in result,
|
|
313
|
+
});
|
|
314
|
+
if (friction !== undefined) {
|
|
315
|
+
envelope[EnvelopeKey.FEEDBACK_INVITE] = inviteFor(friction);
|
|
316
|
+
getSessionMetrics().recordFeedbackPrompt();
|
|
317
|
+
}
|
|
249
318
|
return Object.keys(envelope).length > 0 ? { ...result, ...envelope } : result;
|
|
250
319
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wait, briefly, for the SDK to reconnect after a navigation — so `confirmed` can mean something.
|
|
3
|
+
*
|
|
4
|
+
* `window.location.assign(url)` destroys the SDK that would report on the new document, so the
|
|
5
|
+
* navigation itself cannot be confirmed from inside the page. But the SDK reconnects TO THE DAEMON,
|
|
6
|
+
* which makes the daemon the only party that can see arrival happen. Before this, it did not look,
|
|
7
|
+
* and told the agent to poll `reticle_sessions` instead — a tool call on every navigation, on the
|
|
8
|
+
* least reliable tool we ship.
|
|
9
|
+
*
|
|
10
|
+
* Bounded and best-effort by construction: a navigation to a page that is not instrumented, or not
|
|
11
|
+
* there at all, must still return promptly with `confirmed:false` rather than hanging. The clock and
|
|
12
|
+
* the sleep are injected so this is testable without waiting on a real one.
|
|
13
|
+
*/
|
|
14
|
+
import type { SessionManager } from '../session/session-manager.js';
|
|
15
|
+
import type { NavigateArrival } from './navigate-result.js';
|
|
16
|
+
/** Long enough for a dev server to serve a page and the SDK to dial back; short enough to not hang. */
|
|
17
|
+
export declare const ARRIVAL_TIMEOUT_MS = 5000;
|
|
18
|
+
export interface ArrivalClock {
|
|
19
|
+
now: () => number;
|
|
20
|
+
sleep: (ms: number) => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Poll until a session is present at `target`, or the budget runs out. Returns `null` on timeout —
|
|
24
|
+
* never throws, because a failure to confirm is a legitimate answer, not an error.
|
|
25
|
+
*/
|
|
26
|
+
export declare function awaitArrival(sessions: SessionManager, target: string, timeoutMs?: number, clock?: ArrivalClock): Promise<NavigateArrival | null>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wait, briefly, for the SDK to reconnect after a navigation — so `confirmed` can mean something.
|
|
3
|
+
*
|
|
4
|
+
* `window.location.assign(url)` destroys the SDK that would report on the new document, so the
|
|
5
|
+
* navigation itself cannot be confirmed from inside the page. But the SDK reconnects TO THE DAEMON,
|
|
6
|
+
* which makes the daemon the only party that can see arrival happen. Before this, it did not look,
|
|
7
|
+
* and told the agent to poll `reticle_sessions` instead — a tool call on every navigation, on the
|
|
8
|
+
* least reliable tool we ship.
|
|
9
|
+
*
|
|
10
|
+
* Bounded and best-effort by construction: a navigation to a page that is not instrumented, or not
|
|
11
|
+
* there at all, must still return promptly with `confirmed:false` rather than hanging. The clock and
|
|
12
|
+
* the sleep are injected so this is testable without waiting on a real one.
|
|
13
|
+
*/
|
|
14
|
+
/** Long enough for a dev server to serve a page and the SDK to dial back; short enough to not hang. */
|
|
15
|
+
export const ARRIVAL_TIMEOUT_MS = 5_000;
|
|
16
|
+
const POLL_MS = 100;
|
|
17
|
+
/**
|
|
18
|
+
* Compare only origin + pathname.
|
|
19
|
+
*
|
|
20
|
+
* The app is entitled to add or rewrite the query and hash on arrival — a redirect to
|
|
21
|
+
* `?redirect=%2F`, a router normalising a trailing slash, an auth guard appending a reason. Matching
|
|
22
|
+
* the whole URL string would report `confirmed:false` for a navigation that plainly succeeded, which
|
|
23
|
+
* is the same lie in the opposite direction.
|
|
24
|
+
*/
|
|
25
|
+
function samePage(a, b) {
|
|
26
|
+
try {
|
|
27
|
+
const x = new URL(a);
|
|
28
|
+
const y = new URL(b);
|
|
29
|
+
return x.origin === y.origin && x.pathname.replace(/\/$/, '') === y.pathname.replace(/\/$/, '');
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function findArrival(sessions, target) {
|
|
36
|
+
for (const s of sessions.all()) {
|
|
37
|
+
if (samePage(s.url, target))
|
|
38
|
+
return { sessionId: s.id };
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const REAL_CLOCK = {
|
|
43
|
+
now: () => Date.now(),
|
|
44
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Poll until a session is present at `target`, or the budget runs out. Returns `null` on timeout —
|
|
48
|
+
* never throws, because a failure to confirm is a legitimate answer, not an error.
|
|
49
|
+
*/
|
|
50
|
+
export async function awaitArrival(sessions, target, timeoutMs = ARRIVAL_TIMEOUT_MS, clock = REAL_CLOCK) {
|
|
51
|
+
const deadline = clock.now() + timeoutMs;
|
|
52
|
+
for (;;) {
|
|
53
|
+
const found = findArrival(sessions, target);
|
|
54
|
+
if (found !== null)
|
|
55
|
+
return found;
|
|
56
|
+
if (clock.now() >= deadline)
|
|
57
|
+
return null;
|
|
58
|
+
await clock.sleep(POLL_MS);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -10,8 +10,22 @@
|
|
|
10
10
|
* `reticle_act` already draws this line — `dispatched` (sent) versus `settled` (a frame flushed).
|
|
11
11
|
* This gives navigate the same honesty without changing what it does.
|
|
12
12
|
*/
|
|
13
|
+
/** The session the SDK reconnected as after the page moved, when one was observed in the window. */
|
|
14
|
+
export interface NavigateArrival {
|
|
15
|
+
sessionId: string;
|
|
16
|
+
}
|
|
13
17
|
export declare function navigateResult(result: {
|
|
14
18
|
ok?: unknown;
|
|
15
19
|
url?: unknown;
|
|
16
20
|
reason?: unknown;
|
|
17
|
-
}
|
|
21
|
+
},
|
|
22
|
+
/**
|
|
23
|
+
* Arrival, if the daemon saw the SDK come back. `null` keeps the original honest envelope.
|
|
24
|
+
*
|
|
25
|
+
* Measured on three fixtures: `confirmed` was `false` on every navigation on every app, because
|
|
26
|
+
* nothing ever set it. A boolean that never varies teaches an agent nothing, and an agent that
|
|
27
|
+
* learns to ignore it will keep ignoring it when it starts meaning something. The daemon is the
|
|
28
|
+
* one place that CAN answer this — the SDK reconnects to it — so making it answer removes a
|
|
29
|
+
* `reticle_sessions` poll from every single navigation.
|
|
30
|
+
*/
|
|
31
|
+
arrival?: NavigateArrival | null): Record<string, unknown>;
|
|
@@ -1,16 +1,14 @@
|
|
|
1
|
+
export function navigateResult(result,
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
+
* Arrival, if the daemon saw the SDK come back. `null` keeps the original honest envelope.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `reticle_act` already draws this line — `dispatched` (sent) versus `settled` (a frame flushed).
|
|
11
|
-
* This gives navigate the same honesty without changing what it does.
|
|
5
|
+
* Measured on three fixtures: `confirmed` was `false` on every navigation on every app, because
|
|
6
|
+
* nothing ever set it. A boolean that never varies teaches an agent nothing, and an agent that
|
|
7
|
+
* learns to ignore it will keep ignoring it when it starts meaning something. The daemon is the
|
|
8
|
+
* one place that CAN answer this — the SDK reconnects to it — so making it answer removes a
|
|
9
|
+
* `reticle_sessions` poll from every single navigation.
|
|
12
10
|
*/
|
|
13
|
-
|
|
11
|
+
arrival = null) {
|
|
14
12
|
const ok = true === result.ok;
|
|
15
13
|
const base = {
|
|
16
14
|
ok,
|
|
@@ -20,6 +18,15 @@ export function navigateResult(result) {
|
|
|
20
18
|
// A refusal is conclusive: the page never moved, so there is nothing unconfirmed to report.
|
|
21
19
|
if (!ok)
|
|
22
20
|
return base;
|
|
21
|
+
if (arrival !== null) {
|
|
22
|
+
return {
|
|
23
|
+
...base,
|
|
24
|
+
confirmed: true,
|
|
25
|
+
// The SDK that reconnects after a navigation is a NEW session with a new id. Returning it here
|
|
26
|
+
// is the difference between a confirmation and a confirmation the agent can act on.
|
|
27
|
+
sessionId: arrival.sessionId,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
23
30
|
return {
|
|
24
31
|
...base,
|
|
25
32
|
confirmed: false,
|
|
@@ -10,6 +10,7 @@ import { ReticleTool } from './tool-names.js';
|
|
|
10
10
|
import { buildReactionReport } from '../events/reaction.js';
|
|
11
11
|
import { findContradictions } from '../events/contradictions.js';
|
|
12
12
|
import { evaluatePredicate, waitForPredicate, PredicateSchema } from '../events/predicate.js';
|
|
13
|
+
import { parsePredicate } from '../events/predicate-parse.js';
|
|
13
14
|
import { matchNet, matchConsole, isConsoleEvent, eventMatchesFilters, netEmptyHint, consoleEmptyHint, reconcileNet, projectNetCall, projectConsoleLog, } from '../events/event-filters.js';
|
|
14
15
|
import { applyEventBudget, costHint, withSizeCost, DEFAULT_QUERY_LIMIT, } from '../session/output-budget.js';
|
|
15
16
|
import { healthEnvelope, bufferEnvelope } from '../session/session-health.js';
|
|
@@ -209,6 +210,10 @@ export const OBSERVE_TOOLS = [
|
|
|
209
210
|
pass: z.boolean(),
|
|
210
211
|
evidence: z.unknown().optional(),
|
|
211
212
|
failureReason: z.string().optional(),
|
|
213
|
+
observationLost: z
|
|
214
|
+
.boolean()
|
|
215
|
+
.optional()
|
|
216
|
+
.describe('The tab disconnected mid-wait, so this was never observed — the verdict is UNKNOWN, not a failure of the app.'),
|
|
212
217
|
session: z
|
|
213
218
|
.object({ lastSeenMs: z.number(), throttled: z.boolean(), focused: z.boolean() })
|
|
214
219
|
.optional(),
|
|
@@ -233,7 +238,7 @@ export const OBSERVE_TOOLS = [
|
|
|
233
238
|
handler: async (deps, args) => {
|
|
234
239
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
235
240
|
// `until` is act_and_wait's name for this — see alias-args.ts.
|
|
236
|
-
const predicate =
|
|
241
|
+
const predicate = parsePredicate(aliasParam(args, 'predicate', ['until'])['predicate']);
|
|
237
242
|
// Honesty: explicit since wins; else default to the last act's cursor; else the whole buffer.
|
|
238
243
|
const since = asNumber(args['since']) ?? session.lastAct.cursor() ?? 0;
|
|
239
244
|
const verdict = await waitForPredicate(session, predicate, asNumber(args['timeout_ms']) ?? DEFAULT_ASSERT_TIMEOUT_MS, since);
|
|
@@ -252,7 +257,16 @@ export const OBSERVE_TOOLS = [
|
|
|
252
257
|
example: { predicate: { kind: PredicateKind.SIGNAL, name: 'todos:loaded' } },
|
|
253
258
|
description: 'Evaluate a predicate (optionally waiting up to timeout_ms). Returns { pass, evidence, failureReason? }. The end of every verify loop. Prefer a { signal } or { net } consequence over { element }/{ text } presence — a passing presence-only assertion returns `advice` because a wrong/healed element can fake it. By default it only counts events since your last act, so a stale buffered signal can never fake a pass; pass `since` (an observe/act cursor) to set the window explicitly.',
|
|
254
259
|
inputSchema: {
|
|
255
|
-
predicate: PredicateSchema.optional().describe(
|
|
260
|
+
predicate: PredicateSchema.optional().describe(
|
|
261
|
+
// Every kind, each with the field that carries its argument. Five of the nine were
|
|
262
|
+
// undocumented here, including `route` — and "did submitting the login form navigate away"
|
|
263
|
+
// is the most common thing an agent wants to assert. A field report reached us from an agent
|
|
264
|
+
// that guessed `urlContains` on route (net's spelling) and got unrecognized_keys.
|
|
265
|
+
'Predicate to evaluate. Kinds: { signal, name } { net, urlContains|method|status|count } ' +
|
|
266
|
+
'{ state, path|equals } { route, pathname (exact) | contains (path+query+hash) } ' +
|
|
267
|
+
'{ element, testid|role|text } { text } { console, level|absent } { animation, name } ' +
|
|
268
|
+
'{ settled } — combine with { allOf | anyOf | not }. Prefer a signal/net/state consequence ' +
|
|
269
|
+
'over element/text presence.'),
|
|
256
270
|
until: PredicateSchema.optional().describe("Alias for `predicate` (act_and_wait's name)."),
|
|
257
271
|
timeout_ms: z
|
|
258
272
|
.number()
|
|
@@ -268,6 +282,10 @@ export const OBSERVE_TOOLS = [
|
|
|
268
282
|
pass: z.boolean(),
|
|
269
283
|
evidence: z.unknown().optional(),
|
|
270
284
|
failureReason: z.string().optional(),
|
|
285
|
+
observationLost: z
|
|
286
|
+
.boolean()
|
|
287
|
+
.optional()
|
|
288
|
+
.describe('The tab disconnected mid-wait, so this was never observed — the verdict is UNKNOWN, not a failure of the app.'),
|
|
271
289
|
advice: z
|
|
272
290
|
.string()
|
|
273
291
|
.optional()
|
|
@@ -304,7 +322,7 @@ export const OBSERVE_TOOLS = [
|
|
|
304
322
|
handler: async (deps, args) => {
|
|
305
323
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
306
324
|
// `until` is act_and_wait's name for this — see alias-args.ts.
|
|
307
|
-
const predicate =
|
|
325
|
+
const predicate = parsePredicate(aliasParam(args, 'predicate', ['until'])['predicate']);
|
|
308
326
|
const timeout = asNumber(args['timeout_ms']) ?? 0;
|
|
309
327
|
// Honesty: explicit since wins; else default to the last act's cursor; else the whole buffer.
|
|
310
328
|
const since = asNumber(args['since']) ?? session.lastAct.cursor() ?? 0;
|
|
@@ -323,7 +341,7 @@ export const OBSERVE_TOOLS = [
|
|
|
323
341
|
: assertsDerivedIpcStatus(predicate)
|
|
324
342
|
? { advice: DERIVED_IPC_STATUS_ADVICE }
|
|
325
343
|
: {};
|
|
326
|
-
const { decision, contradictions, coverage } = await assertVerdict(session, predicate, verdict.pass, since, verdict.inconclusive);
|
|
344
|
+
const { decision, contradictions, coverage } = await assertVerdict(session, predicate, verdict.pass, since, verdict.inconclusive, verdict.observationLost);
|
|
327
345
|
return withControl(session, {
|
|
328
346
|
...decision,
|
|
329
347
|
...verdict,
|
|
@@ -27,17 +27,22 @@ function synthetic(reason) {
|
|
|
27
27
|
*/
|
|
28
28
|
export async function tryRealInput(deps, session, ref, action, args) {
|
|
29
29
|
const provider = deps.realInput;
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const inner = asRecord(args['args']);
|
|
31
|
+
const askedForNative = true === inner[NATIVE_INPUT_ARG];
|
|
32
|
+
if (provider === undefined) {
|
|
33
|
+
// Silent by default: with no provider EVERY action is synthetic, and a reason on all of them is
|
|
34
|
+
// noise on the most-used tool in the product. But an agent that passed native:true asked a
|
|
35
|
+
// question and got the opposite answer — reported from the field as a silent downgrade that cost
|
|
36
|
+
// real debugging time, because the tool description promises a reason is "never silent".
|
|
37
|
+
return askedForNative ? synthetic(InputModeReason.NOT_CONFIGURED) : synthetic();
|
|
38
|
+
}
|
|
32
39
|
if (!isPointerAction(action))
|
|
33
40
|
return synthetic(InputModeReason.NOT_POINTER); // fill/type stay synthetic
|
|
34
|
-
const inner = asRecord(args['args']);
|
|
35
41
|
// "Don't click, run the code": a click/dblclick runs the occlusion-honest SYNTHETIC path by default
|
|
36
42
|
// even with a provider configured — no coordinate gesture to be intercepted by the HUD or missed
|
|
37
43
|
// off-screen. Opt into a trusted native click with args.native:true (file pickers, clipboard,
|
|
38
44
|
// isTrusted-gated handlers). hover/drag genuinely need native pointer state, so they stay real.
|
|
39
|
-
if ((action === ActionType.CLICK || action === ActionType.DBLCLICK) &&
|
|
40
|
-
inner[NATIVE_INPUT_ARG] !== true) {
|
|
45
|
+
if ((action === ActionType.CLICK || action === ActionType.DBLCLICK) && !askedForNative) {
|
|
41
46
|
return synthetic(InputModeReason.SYNTHETIC_CLICK_PREFERRED);
|
|
42
47
|
}
|
|
43
48
|
if (!(await provider.isAvailableFor(session.url)))
|
package/dist/tools/tool-kit.d.ts
CHANGED
|
@@ -33,6 +33,14 @@ export interface ToolDeps {
|
|
|
33
33
|
fs: FileSystemPort;
|
|
34
34
|
/** absolute.reticle path (index.ts computes cwd/.reticle). */
|
|
35
35
|
reticleRoot: string;
|
|
36
|
+
/**
|
|
37
|
+
* This daemon's OWN project id, derived from the directory it was started in.
|
|
38
|
+
*
|
|
39
|
+
* Optional because a daemon above an uninitialised directory has none, and because every existing
|
|
40
|
+
* test construction of ToolDeps predates it. Absence means "cannot tell", which must never be
|
|
41
|
+
* treated as a mismatch.
|
|
42
|
+
*/
|
|
43
|
+
projectId?: string;
|
|
36
44
|
/** injected clock for the contract's generatedAt stamp. */
|
|
37
45
|
now: () => number;
|
|
38
46
|
/**
|
|
@@ -76,11 +84,58 @@ export declare const sessionIdShape: {
|
|
|
76
84
|
sessionId: z.ZodOptional<z.ZodString>;
|
|
77
85
|
};
|
|
78
86
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* envelope
|
|
87
|
+
* Every key `runTool` splices onto a session-bound result, as a closed list.
|
|
88
|
+
*
|
|
89
|
+
* These are wire strings and were inlined at the splice sites, which is exactly how four of them
|
|
90
|
+
* came to be spliced and never declared. `sessionEnvelopeShape` is derived from this object and
|
|
91
|
+
* `envelope-keys-declared.test.ts` fails if a member is missing from it, so the two cannot drift.
|
|
92
|
+
*
|
|
93
|
+
* Each of these is a channel that exists BECAUSE the agent had no other way to learn the fact.
|
|
94
|
+
* Undeclared, a schema-strict client validating `structuredContent` drops it, and the channel is
|
|
95
|
+
* indistinguishable from never having been built.
|
|
96
|
+
*/
|
|
97
|
+
export declare const EnvelopeKey: {
|
|
98
|
+
/** Session health — which tab, how fresh. */
|
|
99
|
+
readonly SESSION: "session";
|
|
100
|
+
/** One-shot pool-lease reminder. */
|
|
101
|
+
readonly SESSION_LEASE: "session_lease";
|
|
102
|
+
/** One-shot "these sessions are old, clean them up". */
|
|
103
|
+
readonly SESSION_AGE_WARNING: "session_age_warning";
|
|
104
|
+
/** The delivered-once human-in-the-loop guidance envelope. Losing it is a safety failure. */
|
|
105
|
+
readonly CONTROL: "control";
|
|
106
|
+
/**
|
|
107
|
+
* Rides alongside `session` whenever the tab is THROTTLED. It was declared on `reticle_act`'s
|
|
108
|
+
* schema and nowhere else, so on a validating profile every other session-bound tool silently
|
|
109
|
+
* dropped it — a throttled tab, where drives can no-op, returned a healthy-looking result.
|
|
110
|
+
*/
|
|
111
|
+
readonly WARNING: "warning";
|
|
112
|
+
/** One-shot "ask the human how this went". */
|
|
113
|
+
readonly FEEDBACK_PROMPT: "feedback_prompt";
|
|
114
|
+
/** One-shot "a newer Reticle exists". */
|
|
115
|
+
readonly UPDATE_AVAILABLE: "update_available";
|
|
116
|
+
/**
|
|
117
|
+
* "You have driven this page N times without asking for a verdict."
|
|
118
|
+
*
|
|
119
|
+
* The largest known lever on the metric this product is judged by: verdict-less sessions
|
|
120
|
+
* overwhelmingly never call a verdict-producing tool at all. It was spliced undeclared, so on a
|
|
121
|
+
* validating client the nudge was built, fired, and thrown away before any agent saw it.
|
|
122
|
+
*/
|
|
123
|
+
readonly VERIFY_NEXT: "verify_next";
|
|
124
|
+
/** The contextual "tell us what just went wrong" invitation. Same story, same release. */
|
|
125
|
+
readonly FEEDBACK_INVITE: "feedback_invite";
|
|
126
|
+
/** SDK/daemon version skew — often the one fact that explains everything else in the session. */
|
|
127
|
+
readonly VERSION_SKEW: "version_skew";
|
|
128
|
+
/** A feedback report that was accepted and then failed to send. Only the reporter can act on it. */
|
|
129
|
+
readonly FEEDBACK_UNDELIVERED: "feedback_undelivered";
|
|
130
|
+
};
|
|
131
|
+
export type EnvelopeKey = (typeof EnvelopeKey)[keyof typeof EnvelopeKey];
|
|
132
|
+
/**
|
|
133
|
+
* Fields that `runTool` / `withControl` splice onto EVERY session-bound tool result at runtime.
|
|
134
|
+
* Merged into each session-bound tool's outputSchema so a schema-strict client (structuredContent
|
|
135
|
+
* validation) keeps them instead of silently dropping them.
|
|
136
|
+
*
|
|
137
|
+
* Derived from `EnvelopeKey` rather than re-listed: this shape and the splice sites disagreed for a
|
|
138
|
+
* whole release, and every key that went missing was a channel nobody could see was missing.
|
|
84
139
|
*/
|
|
85
140
|
export declare const sessionEnvelopeShape: z.ZodRawShape;
|
|
86
141
|
/** Unwrap a browser command result or throw its error so the agent sees a clean failure. */
|
package/dist/tools/tool-kit.js
CHANGED
|
@@ -14,32 +14,63 @@ export const sessionIdShape = {
|
|
|
14
14
|
.describe('OMIT THIS unless you mean a specific tab — Reticle scopes to your project, prefers the active one, and refuses rather than guesses when ambiguous. Pass an id from reticle_sessions only to target a particular tab. The previous wording ("omit when only ONE session is open") was wrong and cost real turns: with three sessions connected — an app plus two pool leases — resolution is still unambiguous, but that sentence sent the agent off to list and filter sessions by hand before every single call.'),
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* envelope
|
|
17
|
+
* Every key `runTool` splices onto a session-bound result, as a closed list.
|
|
18
|
+
*
|
|
19
|
+
* These are wire strings and were inlined at the splice sites, which is exactly how four of them
|
|
20
|
+
* came to be spliced and never declared. `sessionEnvelopeShape` is derived from this object and
|
|
21
|
+
* `envelope-keys-declared.test.ts` fails if a member is missing from it, so the two cannot drift.
|
|
22
|
+
*
|
|
23
|
+
* Each of these is a channel that exists BECAUSE the agent had no other way to learn the fact.
|
|
24
|
+
* Undeclared, a schema-strict client validating `structuredContent` drops it, and the channel is
|
|
25
|
+
* indistinguishable from never having been built.
|
|
22
26
|
*/
|
|
23
|
-
export const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
27
|
+
export const EnvelopeKey = {
|
|
28
|
+
/** Session health — which tab, how fresh. */
|
|
29
|
+
SESSION: 'session',
|
|
30
|
+
/** One-shot pool-lease reminder. */
|
|
31
|
+
SESSION_LEASE: 'session_lease',
|
|
32
|
+
/** One-shot "these sessions are old, clean them up". */
|
|
33
|
+
SESSION_AGE_WARNING: 'session_age_warning',
|
|
34
|
+
/** The delivered-once human-in-the-loop guidance envelope. Losing it is a safety failure. */
|
|
35
|
+
CONTROL: 'control',
|
|
36
|
+
/**
|
|
37
|
+
* Rides alongside `session` whenever the tab is THROTTLED. It was declared on `reticle_act`'s
|
|
38
|
+
* schema and nowhere else, so on a validating profile every other session-bound tool silently
|
|
39
|
+
* dropped it — a throttled tab, where drives can no-op, returned a healthy-looking result.
|
|
40
|
+
*/
|
|
41
|
+
WARNING: 'warning',
|
|
42
|
+
/** One-shot "ask the human how this went". */
|
|
43
|
+
FEEDBACK_PROMPT: 'feedback_prompt',
|
|
44
|
+
/** One-shot "a newer Reticle exists". */
|
|
45
|
+
UPDATE_AVAILABLE: 'update_available',
|
|
46
|
+
/**
|
|
47
|
+
* "You have driven this page N times without asking for a verdict."
|
|
48
|
+
*
|
|
49
|
+
* The largest known lever on the metric this product is judged by: verdict-less sessions
|
|
50
|
+
* overwhelmingly never call a verdict-producing tool at all. It was spliced undeclared, so on a
|
|
51
|
+
* validating client the nudge was built, fired, and thrown away before any agent saw it.
|
|
52
|
+
*/
|
|
53
|
+
VERIFY_NEXT: 'verify_next',
|
|
54
|
+
/** The contextual "tell us what just went wrong" invitation. Same story, same release. */
|
|
55
|
+
FEEDBACK_INVITE: 'feedback_invite',
|
|
56
|
+
/** SDK/daemon version skew — often the one fact that explains everything else in the session. */
|
|
57
|
+
VERSION_SKEW: 'version_skew',
|
|
58
|
+
/** A feedback report that was accepted and then failed to send. Only the reporter can act on it. */
|
|
59
|
+
FEEDBACK_UNDELIVERED: 'feedback_undelivered',
|
|
42
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* Fields that `runTool` / `withControl` splice onto EVERY session-bound tool result at runtime.
|
|
63
|
+
* Merged into each session-bound tool's outputSchema so a schema-strict client (structuredContent
|
|
64
|
+
* validation) keeps them instead of silently dropping them.
|
|
65
|
+
*
|
|
66
|
+
* Derived from `EnvelopeKey` rather than re-listed: this shape and the splice sites disagreed for a
|
|
67
|
+
* whole release, and every key that went missing was a channel nobody could see was missing.
|
|
68
|
+
*/
|
|
69
|
+
export const sessionEnvelopeShape = Object.fromEntries(Object.values(EnvelopeKey).map((key) => [
|
|
70
|
+
key,
|
|
71
|
+
// `warning` is the one with a narrower type; the rest are opaque envelopes the client forwards.
|
|
72
|
+
EnvelopeKey.WARNING === key ? z.string().optional() : z.unknown().optional(),
|
|
73
|
+
]));
|
|
43
74
|
/** Unwrap a browser command result or throw its error so the agent sees a clean failure. */
|
|
44
75
|
export async function commandOrThrow(deps, sessionId,
|
|
45
76
|
// ReticleCommand, not string: the union already exists and every caller passes a member. Typed as
|
|
@@ -98,6 +98,19 @@ export const CORE_TOOL_NAMES = new Set([
|
|
|
98
98
|
ReticleTool.QUERY,
|
|
99
99
|
ReticleTool.ACT,
|
|
100
100
|
ReticleTool.ACT_AND_WAIT,
|
|
101
|
+
// ACT_SEQUENCE is here because its absence was measurably CAUSING the biggest loop in the data.
|
|
102
|
+
//
|
|
103
|
+
// In the field `reticle_act` is called overwhelmingly more often than `reticle_act_sequence`, and
|
|
104
|
+
// `act` leads the repeat table by a wide margin — inside those sessions the repeated calls are
|
|
105
|
+
// clicks and fills, a login form driven one round trip at a time, which is exactly the antipattern
|
|
106
|
+
// SKILL.md warns about and exactly what this tool exists to collapse.
|
|
107
|
+
//
|
|
108
|
+
// The repeats are NOT retries: looping sessions have a LOWER error rate than non-looping ones. The
|
|
109
|
+
// calls succeed and get repeated, because the batching tool was reachable only through
|
|
110
|
+
// `reticle_run` — so an agent had to already know it existed to use it, and essentially nobody
|
|
111
|
+
// did. A tool an agent must already know about is a tool that never gets called; the same argument
|
|
112
|
+
// that put INSPECT and FEEDBACK here.
|
|
113
|
+
ReticleTool.ACT_SEQUENCE,
|
|
101
114
|
ReticleTool.OBSERVE,
|
|
102
115
|
ReticleTool.NETWORK,
|
|
103
116
|
ReticleTool.CONSOLE,
|