@reticlehq/server 3.8.0 → 3.8.2
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 +2 -2
- package/dist/command/cli/answers/gate-hook-message.d.ts +2 -0
- package/dist/command/cli/answers/gate-hook-message.js +5 -1
- package/dist/command/cli/auth/cloud-session.d.ts +1 -19
- package/dist/command/cli/auth/device-flow.d.ts +3 -37
- package/dist/command/cli/cli-doctor.js +30 -23
- package/dist/command/cli/cli-flow-commands.js +7 -1
- package/dist/command/cli/cli-parse-serve.d.ts +24 -0
- package/dist/command/cli/cli-parse-serve.js +124 -0
- package/dist/command/cli/cli-parse.d.ts +1 -10
- package/dist/command/cli/cli-parse.js +62 -121
- package/dist/command/cli/cli-usage.js +5 -2
- package/dist/command/cli/cloud-link.d.ts +1 -20
- package/dist/command/cli/doctor/doctor-agent-link.d.ts +23 -0
- package/dist/command/cli/doctor/doctor-agent-link.js +28 -0
- package/dist/command/cli/ports/port-holder.js +6 -1
- package/dist/command/cli/project-name.d.ts +1 -7
- package/dist/command/cli/project-name.js +1 -0
- package/dist/command/daemon/daemon.d.ts +6 -0
- package/dist/command/daemon/daemon.js +52 -2
- package/dist/command/daemon/win32-escape.d.ts +32 -0
- package/dist/command/daemon/win32-escape.js +119 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +1 -9
- package/dist/command/setup/bringup/dev-server-wait.js +5 -1
- package/dist/command/version/version-skew.d.ts +2 -0
- package/dist/command/version/version-skew.js +7 -3
- package/dist/features/crawl/crawl-tools.js +2 -2
- package/dist/features/exhaust/ledger.d.ts +15 -96
- package/dist/features/exhaust/ledger.js +3 -3
- package/dist/features/license/license.d.ts +2 -14
- package/dist/hooks/hook-commands.js +19 -9
- package/dist/judgement/capsule/capsule-store.d.ts +2 -39
- package/dist/language/flows/annotate-tools.js +1 -1
- package/dist/language/flows/fields/flow-secret-field.js +1 -1
- package/dist/language/flows/flow-expect-grammar.js +14 -6
- package/dist/language/flows/flow-lease.d.ts +27 -0
- package/dist/language/flows/flow-lease.js +59 -0
- package/dist/language/flows/flow-replay-run.d.ts +1 -87
- package/dist/language/flows/flow-replay-run.js +88 -283
- package/dist/language/flows/flow-replayable.d.ts +1 -1
- package/dist/language/flows/flow-start-path.d.ts +108 -0
- package/dist/language/flows/flow-start-path.js +313 -0
- package/dist/language/flows/flow-tools.d.ts +2 -23
- package/dist/language/flows/flow-tools.js +34 -66
- package/dist/language/flows/recording/flake.d.ts +3 -27
- package/dist/language/flows/recording/flake.js +1 -1
- package/dist/language/flows/stores/assertion-tiers-store.js +1 -1
- package/dist/machine/git-tracks.js +1 -0
- package/dist/memory/cloud/cloud-sync.d.ts +2 -24
- package/dist/memory/intent/intent-shard.d.ts +41 -266
- package/dist/memory/journal/envelope-store.js +1 -1
- package/dist/memory/journal/envelope.d.ts +6 -154
- package/dist/memory/journal/on-disk/ambient-file.d.ts +1 -7
- package/dist/memory/journal/on-disk/ambient-file.js +1 -1
- package/dist/memory/journal/self-observation.js +3 -1
- package/dist/memory/journal/session-journal.js +39 -20
- package/dist/portal/input/drive-error.d.ts +6 -0
- package/dist/portal/input/drive-error.js +9 -0
- package/dist/portal/input/real-input-press.d.ts +34 -0
- package/dist/portal/input/real-input-press.js +150 -0
- package/dist/portal/input/real-input.d.ts +6 -23
- package/dist/portal/input/real-input.js +95 -192
- package/dist/portal/input/viewport-tools.d.ts +19 -15
- package/dist/portal/pool/browser-pool.d.ts +5 -55
- package/dist/portal/pool/browser-pool.js +121 -100
- package/dist/portal/pool/pool-lease.d.ts +128 -0
- package/dist/portal/pool/pool-lease.js +49 -0
- package/dist/portal/production/production-activation.d.ts +5 -27
- package/dist/portal/session/control-envelope.d.ts +1 -1
- package/dist/portal/session/zero-install-first-call.d.ts +3 -0
- package/dist/portal/session/zero-install-first-call.js +33 -12
- package/dist/surface/mcp/mcp.d.ts +3 -3
- package/dist/surface/mcp/mcp.js +4 -6
- package/dist/surface/tools/act/act-danger.js +32 -17
- package/dist/surface/tools/act/durable.js +1 -1
- package/dist/surface/tools/act-sequence-tool.js +3 -3
- package/dist/surface/tools/act-tools.js +15 -16
- package/dist/surface/tools/agent/compact-envelope.d.ts +2 -2
- package/dist/surface/tools/coverage-tools.js +1 -1
- package/dist/surface/tools/dynamic-tools.d.ts +8 -0
- package/dist/surface/tools/dynamic-tools.js +36 -16
- package/dist/surface/tools/empty-session-list.d.ts +5 -29
- package/dist/surface/tools/feedback-tools.js +1 -1
- package/dist/surface/tools/gap-novelty.d.ts +1 -1
- package/dist/surface/tools/gaps/capability-absences.js +4 -3
- package/dist/surface/tools/harness-gate.d.ts +1 -1
- package/dist/surface/tools/harness-toolset.js +3 -4
- package/dist/surface/tools/lease-permissions.d.ts +1 -1
- package/dist/surface/tools/lease-readiness.d.ts +23 -1
- package/dist/surface/tools/lease-readiness.js +75 -9
- package/dist/surface/tools/lease-tools.d.ts +1 -52
- package/dist/surface/tools/lease-tools.js +79 -142
- package/dist/surface/tools/observe-tools.js +3 -1
- package/dist/surface/tools/read-tools.js +5 -5
- package/dist/surface/tools/real-input-attempt.js +70 -31
- package/dist/surface/tools/session-ports.d.ts +57 -0
- package/dist/surface/tools/session-ports.js +102 -0
- package/dist/surface/tools/tool-kit.d.ts +5 -3
- package/dist/surface/tools/tools.js +1 -1
- package/dist/telemetry/daemon-telemetry.d.ts +3 -1
- package/dist/telemetry/daemon-telemetry.js +9 -2
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +2 -0
- package/docs/agent-cheatsheet.md +12 -0
- package/docs/cli/gate.mdx +1 -1
- package/docs/cli/verify.mdx +8 -1
- package/docs/first-drive.md +7 -0
- package/docs/multi-agent-testing.md +2 -0
- package/docs/packages/react.mdx +14 -2
- package/docs/packages/vite-plugin.mdx +1 -0
- package/docs/predicates.mdx +6 -0
- package/docs/skill.md +3 -3
- package/docs/troubleshooting.mdx +10 -0
- package/docs/usage.md +1 -1
- package/package.json +8 -9
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pool's lease handle and lease record, its options, and its limits: the types and constants
|
|
3
|
+
* `BrowserPool` is built from.
|
|
4
|
+
*/
|
|
5
|
+
/** Default lease time-to-live: a lease untouched for this long is presumed orphaned and reclaimed. */
|
|
6
|
+
export const DEFAULT_LEASE_TTL_MS = 5 * 60_000;
|
|
7
|
+
/**
|
|
8
|
+
* How long a lease outlives the MCP connection that took it. Long enough for an agent whose
|
|
9
|
+
* connection only blipped to touch it again; short enough that a gone agent's window closes soon.
|
|
10
|
+
*/
|
|
11
|
+
export const ORPHANED_LEASE_GRACE_MS = 30_000;
|
|
12
|
+
/** Default cap on how long a lease's initial navigation may take before it fails (frees the slot). */
|
|
13
|
+
export const DEFAULT_NAV_TIMEOUT_MS = 30_000;
|
|
14
|
+
/**
|
|
15
|
+
* How many reaped lease ids to remember. Only the most recent departure is ever asked about, so
|
|
16
|
+
* this is slack for a burst sweep, not a history.
|
|
17
|
+
*/
|
|
18
|
+
export const MAX_REMEMBERED_REAPED_LEASES = 64;
|
|
19
|
+
/** What a headed lease is refused with where no window can be opened. */
|
|
20
|
+
export const NO_HEADED_BROWSER = 'no headed browser can be opened here';
|
|
21
|
+
/**
|
|
22
|
+
* An acquire that asked to be refused rather than queued found every slot taken (#1535). Queued,
|
|
23
|
+
* it waited behind the cap with no deadline and no status, and `reticle drive` reported the 60 s
|
|
24
|
+
* timeout as a daemon shutting down. The message names the cap and the one override for it.
|
|
25
|
+
*/
|
|
26
|
+
export class PoolAtCapacityError extends Error {
|
|
27
|
+
max;
|
|
28
|
+
constructor(max) {
|
|
29
|
+
super(`all ${String(max)} lease slots are in use. Release one (reticle_lease{action:"release"}, or ` +
|
|
30
|
+
'stop a `reticle drive`), or raise RETICLE_MAX_CONTEXTS.');
|
|
31
|
+
this.max = max;
|
|
32
|
+
this.name = 'PoolAtCapacityError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* What an acquire that asked for an init script is refused with when the page cannot run one.
|
|
37
|
+
*
|
|
38
|
+
* A refusal rather than a lease: the caller asked for a stub of the bindings a desktop shell
|
|
39
|
+
* injects, and a tab opened without it would throw in the app's view and read as a broken app.
|
|
40
|
+
*/
|
|
41
|
+
export const INIT_SCRIPT_UNSUPPORTED = 'Init script failed: this page does not support init scripts, so the requested stub would not be there';
|
|
42
|
+
/** Whether the page's document has not finished loading. A page that cannot be asked counts as loaded. */
|
|
43
|
+
export async function pageLoading(page) {
|
|
44
|
+
if (page.evaluate === undefined) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const state = await page.evaluate('document.readyState');
|
|
48
|
+
return 'string' === typeof state && state !== 'complete';
|
|
49
|
+
}
|
|
@@ -19,20 +19,12 @@ export declare const PRODUCTION_MODE_NOT_LINKED = "production mode needs a linke
|
|
|
19
19
|
/** The `production` argument of `reticle_lease { action: "acquire" }`. */
|
|
20
20
|
export declare const ProductionLeaseArgSchema: z.ZodObject<{
|
|
21
21
|
workspaceId: z.ZodString;
|
|
22
|
-
scope: z.
|
|
22
|
+
scope: z.ZodEnum<{
|
|
23
23
|
readonly OBSERVE: "observe";
|
|
24
24
|
readonly ACT: "act";
|
|
25
25
|
}>;
|
|
26
26
|
intent: z.ZodOptional<z.ZodString>;
|
|
27
|
-
},
|
|
28
|
-
workspaceId: string;
|
|
29
|
-
scope: "observe" | "act";
|
|
30
|
-
intent?: string | undefined;
|
|
31
|
-
}, {
|
|
32
|
-
workspaceId: string;
|
|
33
|
-
scope: "observe" | "act";
|
|
34
|
-
intent?: string | undefined;
|
|
35
|
-
}>;
|
|
27
|
+
}, z.core.$strip>;
|
|
36
28
|
export type TicketFetch = (url: string, init: {
|
|
37
29
|
method: string;
|
|
38
30
|
headers: Record<string, string>;
|
|
@@ -86,31 +78,17 @@ export declare function leaseWithoutInjection<L extends {
|
|
|
86
78
|
/** `reticle_lease`'s `production` argument, described for the agent. */
|
|
87
79
|
export declare const PRODUCTION_LEASE_ARG: z.ZodOptional<z.ZodObject<{
|
|
88
80
|
workspaceId: z.ZodString;
|
|
89
|
-
scope: z.
|
|
81
|
+
scope: z.ZodEnum<{
|
|
90
82
|
readonly OBSERVE: "observe";
|
|
91
83
|
readonly ACT: "act";
|
|
92
84
|
}>;
|
|
93
85
|
intent: z.ZodOptional<z.ZodString>;
|
|
94
|
-
},
|
|
95
|
-
workspaceId: string;
|
|
96
|
-
scope: "observe" | "act";
|
|
97
|
-
intent?: string | undefined;
|
|
98
|
-
}, {
|
|
99
|
-
workspaceId: string;
|
|
100
|
-
scope: "observe" | "act";
|
|
101
|
-
intent?: string | undefined;
|
|
102
|
-
}>>;
|
|
86
|
+
}, z.core.$strip>>;
|
|
103
87
|
/** What a production lease reports back. */
|
|
104
88
|
export declare const PRODUCTION_LEASE_OUTPUT: z.ZodOptional<z.ZodObject<{
|
|
105
89
|
scope: z.ZodString;
|
|
106
90
|
expiresAt: z.ZodNumber;
|
|
107
|
-
},
|
|
108
|
-
scope: string;
|
|
109
|
-
expiresAt: number;
|
|
110
|
-
}, {
|
|
111
|
-
scope: string;
|
|
112
|
-
expiresAt: number;
|
|
113
|
-
}>>;
|
|
91
|
+
}, z.core.$strip>>;
|
|
114
92
|
/** The lease's ports activation reads. */
|
|
115
93
|
interface ActivationPorts {
|
|
116
94
|
nonces?: NonceMinter | undefined;
|
|
@@ -30,7 +30,7 @@ interface PausedResult {
|
|
|
30
30
|
* human's message entirely (not merely hides it). Declared here beside PausedResult so the schema and
|
|
31
31
|
* the runtime shape are the same source; spread into each act tool's outputSchema.
|
|
32
32
|
*/
|
|
33
|
-
export declare const pausedOutputShape: z.
|
|
33
|
+
export declare const pausedOutputShape: Record<string, z.ZodType>;
|
|
34
34
|
/** The optional `control` key `withControl` may add — keeps callers' return types honest. */
|
|
35
35
|
type ControlSpread = {
|
|
36
36
|
control?: ControlEnvelope;
|
|
@@ -18,6 +18,8 @@ export interface FirstCallOpened {
|
|
|
18
18
|
}
|
|
19
19
|
/** Compared both as written and with links resolved: either spelling may be the one lsof used. */
|
|
20
20
|
export declare function within(child: string | undefined, parent: string): boolean;
|
|
21
|
+
/** The caller's running app, for a lease that names no url. */
|
|
22
|
+
export declare function projectAppUrlFinder(io: Pick<FirstCallIo, 'project' | 'serving' | 'servingDirectory'>): () => Promise<string | undefined>;
|
|
21
23
|
/** One lease per project for the daemon's life: a failure is reported once, never retried. */
|
|
22
24
|
export declare function firstCallOpener(io: FirstCallIo): () => Promise<FirstCallOpened | undefined>;
|
|
23
25
|
/**
|
|
@@ -30,4 +32,5 @@ export declare function agentWiring(sessions: {
|
|
|
30
32
|
}, lease: (url: string) => Promise<unknown>): {
|
|
31
33
|
firstRun: FirstRunWiring;
|
|
32
34
|
openUnwiredApp: () => Promise<FirstCallOpened | undefined>;
|
|
35
|
+
projectAppUrl: () => Promise<string | undefined>;
|
|
33
36
|
};
|
|
@@ -47,6 +47,28 @@ function openedNotice(url, init) {
|
|
|
47
47
|
`file:line). To wire it for good: ${wiringConsent(init)} Then restart the dev server and ` +
|
|
48
48
|
'reload the page.');
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The one dev server that runs in this project's folder, or undefined when there is none or more
|
|
52
|
+
* than one. Two servers in one project is a choice the caller makes, not one Reticle guesses.
|
|
53
|
+
*/
|
|
54
|
+
async function projectAppUrlIn(io, dir) {
|
|
55
|
+
const pinned = detectDevCommandInProject(dir)?.port;
|
|
56
|
+
const candidates = new Set(DEV_SERVER_PORTS);
|
|
57
|
+
if (pinned !== undefined) {
|
|
58
|
+
candidates.add(pinned);
|
|
59
|
+
}
|
|
60
|
+
const own = (await io.serving([...candidates]))
|
|
61
|
+
.map((port) => `${LOCALHOST}:${String(port)}`)
|
|
62
|
+
.filter((url) => within(io.servingDirectory(url), dir));
|
|
63
|
+
return 1 === own.length ? own[0] : undefined;
|
|
64
|
+
}
|
|
65
|
+
/** The caller's running app, for a lease that names no url. */
|
|
66
|
+
export function projectAppUrlFinder(io) {
|
|
67
|
+
return async () => {
|
|
68
|
+
const dir = io.project();
|
|
69
|
+
return dir === undefined ? undefined : projectAppUrlIn(io, dir);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
50
72
|
/** One lease per project for the daemon's life: a failure is reported once, never retried. */
|
|
51
73
|
export function firstCallOpener(io) {
|
|
52
74
|
const tried = new Set();
|
|
@@ -54,16 +76,9 @@ export function firstCallOpener(io) {
|
|
|
54
76
|
const dir = io.project();
|
|
55
77
|
if (dir === undefined || tried.has(dir))
|
|
56
78
|
return undefined;
|
|
57
|
-
const
|
|
58
|
-
const candidates = new Set(DEV_SERVER_PORTS);
|
|
59
|
-
if (pinned !== undefined)
|
|
60
|
-
candidates.add(pinned);
|
|
61
|
-
const own = (await io.serving([...candidates]))
|
|
62
|
-
.map((port) => `${LOCALHOST}:${String(port)}`)
|
|
63
|
-
.filter((url) => within(io.servingDirectory(url), dir));
|
|
64
|
-
const [url] = own;
|
|
79
|
+
const url = await projectAppUrlIn(io, dir);
|
|
65
80
|
// ponytail: nothing serving yet is not an attempt, so a later call looks again.
|
|
66
|
-
if (url === undefined
|
|
81
|
+
if (url === undefined)
|
|
67
82
|
return undefined;
|
|
68
83
|
tried.add(dir);
|
|
69
84
|
await io.lease(url).catch(() => undefined);
|
|
@@ -79,12 +94,18 @@ export function firstCallOpener(io) {
|
|
|
79
94
|
*/
|
|
80
95
|
export function agentWiring(sessions, lease) {
|
|
81
96
|
const firstRun = firstRunWiring();
|
|
82
|
-
const
|
|
97
|
+
const io = {
|
|
83
98
|
project: () => firstRun.projectDirectory(callingClientDirectory() ?? process.cwd()),
|
|
84
99
|
serving: async (ports) => (await probeDevServerStates(ports)).serving,
|
|
85
100
|
servingDirectory: (url) => servingDirectoryOf(url),
|
|
86
101
|
lease,
|
|
87
102
|
connected: () => 0 < sessions.list().length,
|
|
88
|
-
}
|
|
89
|
-
|
|
103
|
+
};
|
|
104
|
+
// Wired or not: a lease that names no url wants the app this caller works in either way.
|
|
105
|
+
const callerFolder = () => callingClientDirectory() ?? process.cwd();
|
|
106
|
+
return {
|
|
107
|
+
firstRun,
|
|
108
|
+
openUnwiredApp: firstCallOpener(io),
|
|
109
|
+
projectAppUrl: projectAppUrlFinder({ ...io, project: callerFolder }),
|
|
110
|
+
};
|
|
90
111
|
}
|
|
@@ -18,7 +18,7 @@ export declare const CALL_TOOL_METHOD = "tools/call";
|
|
|
18
18
|
* tool with a strict schema (`reticle_context`) failed the whole call on a validating client the
|
|
19
19
|
* first time one of them rode on it.
|
|
20
20
|
*/
|
|
21
|
-
export declare function withSessionEnvelope(_name: string, outputSchema: z.
|
|
21
|
+
export declare function withSessionEnvelope(_name: string, outputSchema: Record<string, z.ZodType> | undefined): Record<string, z.ZodType> | undefined;
|
|
22
22
|
/**
|
|
23
23
|
* Abbreviations whose internal period is NOT a sentence end.
|
|
24
24
|
*
|
|
@@ -93,8 +93,8 @@ tools?: readonly ToolDef[]): ToolDef[];
|
|
|
93
93
|
*/
|
|
94
94
|
export declare function advertisedConfig(tool: ToolDef, advertised: readonly ToolDef[], profile: ToolSurface): {
|
|
95
95
|
description: string;
|
|
96
|
-
inputSchema: z.
|
|
97
|
-
outputSchema?: z.
|
|
96
|
+
inputSchema: Record<string, z.ZodType>;
|
|
97
|
+
outputSchema?: Record<string, z.ZodType>;
|
|
98
98
|
annotations: ToolAnnotations;
|
|
99
99
|
};
|
|
100
100
|
export declare function installFriendlyArgErrors(server: McpServer, examples: Map<string, string>, allowedKeys?: Map<string, ReadonlySet<string>>): void;
|
package/dist/surface/mcp/mcp.js
CHANGED
|
@@ -8,7 +8,7 @@ import { tableForSurface } from '../tools/tools.js';
|
|
|
8
8
|
import { filterTools, describeToolSurface, TOOL_SURFACE, toolAnnotations, } from '../tools/tool-surface.js';
|
|
9
9
|
import { ReticleTool } from '@reticlehq/core';
|
|
10
10
|
import { sharedParamShort } from './shared-params.js';
|
|
11
|
-
import { buildDynamicTools } from '../tools/dynamic-tools.js';
|
|
11
|
+
import { buildDynamicTools, descriptionOf } from '../tools/dynamic-tools.js';
|
|
12
12
|
import { runTool } from '../tools/invoke-tool.js';
|
|
13
13
|
import { sessionEnvelopeShape, newSnapshotCache } from '../tools/tool-kit.js';
|
|
14
14
|
import { buildErrorPayload, codedRefusalOf } from '../tools/error-recovery.js';
|
|
@@ -150,11 +150,9 @@ function leanZodShape(shape, advertised, predicateAnchor) {
|
|
|
150
150
|
for (const [key, schema] of Object.entries(shape)) {
|
|
151
151
|
if (isPredicateParam(schema)) {
|
|
152
152
|
// Every tool keeps the kinds; only the navigation hint is anchored.
|
|
153
|
-
const compact = z
|
|
154
|
-
|
|
155
|
-
.describe(hintSpent ? PREDICATE_KINDS : COMPACT_PREDICATE_DESCRIPTION);
|
|
153
|
+
const compact = z.record(z.string(), z.unknown());
|
|
154
|
+
out[key] = (schema.isOptional() ? compact.optional() : compact).describe(hintSpent ? PREDICATE_KINDS : COMPACT_PREDICATE_DESCRIPTION);
|
|
156
155
|
hintSpent = true;
|
|
157
|
-
out[key] = schema.isOptional() ? compact.optional() : compact;
|
|
158
156
|
continue;
|
|
159
157
|
}
|
|
160
158
|
// Shared boilerplate is documented once in the initialize instructions instead of on every
|
|
@@ -169,7 +167,7 @@ function leanZodShape(shape, advertised, predicateAnchor) {
|
|
|
169
167
|
}
|
|
170
168
|
// Through the same rewrite as tool descriptions: a parameter that says "a ref from
|
|
171
169
|
// reticle_snapshot" names a tool this surface does not have.
|
|
172
|
-
const desc = schema
|
|
170
|
+
const desc = descriptionOf(schema);
|
|
173
171
|
out[key] =
|
|
174
172
|
'string' === typeof desc
|
|
175
173
|
? schema.describe(liveCallText(firstSentence(desc), advertised))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActionType, DANGEROUS_ACTION_CONFIRM_ARG, NATIVE_INPUT_ARG, OutwardAction, isDangerousActionText, isDangerousNavigation, isInertHref, isPaymentProviderUrl, outwardActionOf, outwardRefusal, } from '@reticlehq/core';
|
|
1
|
+
import { ActionType, DANGEROUS_ACTION_CONFIRM_ARG, NATIVE_INPUT_ARG, OutwardAction, isDangerousActionText, isDangerousNavigation, isInertHref, isPaymentProviderUrl, outwardActionOf, outwardRefusal, withoutFilenameTokens, } from '@reticlehq/core';
|
|
2
2
|
import { currentDrivenBy, harnessOutwardAllowed } from '../../../hooks/driven-by.js';
|
|
3
3
|
import { asRecord, asString } from '@reticlehq/core';
|
|
4
4
|
/**
|
|
@@ -14,10 +14,12 @@ import { asRecord, asString } from '@reticlehq/core';
|
|
|
14
14
|
*/
|
|
15
15
|
function descriptorText(value) {
|
|
16
16
|
const descriptor = asRecord(value);
|
|
17
|
+
// Filename tokens are stripped from the name, the text and the value only: an href or a
|
|
18
|
+
// formAction is a target, not words on the control, and its destination must be read whole.
|
|
17
19
|
return [
|
|
18
|
-
asString(descriptor['name']) ?? '',
|
|
19
|
-
asString(descriptor['text']) ?? '',
|
|
20
|
-
asString(descriptor['value']) ?? '',
|
|
20
|
+
withoutFilenameTokens(asString(descriptor['name']) ?? ''),
|
|
21
|
+
withoutFilenameTokens(asString(descriptor['text']) ?? ''),
|
|
22
|
+
withoutFilenameTokens(asString(descriptor['value']) ?? ''),
|
|
21
23
|
asString(descriptor['href']) ?? '',
|
|
22
24
|
asString(descriptor['formAction']) ?? '',
|
|
23
25
|
asString(descriptor['formText']) ?? '',
|
|
@@ -30,37 +32,45 @@ function descriptorRole(value) {
|
|
|
30
32
|
function isDestructiveDescriptor(value) {
|
|
31
33
|
// A real link is a navigation, judged by where it goes: the same rule as the SDK path.
|
|
32
34
|
const href = asString(asRecord(value)['href']);
|
|
33
|
-
if (href !== undefined && !isInertHref(href))
|
|
35
|
+
if (href !== undefined && !isInertHref(href)) {
|
|
34
36
|
return isDangerousNavigation(href);
|
|
37
|
+
}
|
|
35
38
|
return isDangerousActionText(descriptorText(value), descriptorRole(value));
|
|
36
39
|
}
|
|
37
40
|
/** A Harness call presses an outward control only when its journey asked (`harnessOutwardAllowed`). */
|
|
38
41
|
function assertOutwardAllowed(...descriptors) {
|
|
39
42
|
const allowed = harnessOutwardAllowed();
|
|
40
|
-
if (allowed === undefined)
|
|
43
|
+
if (allowed === undefined) {
|
|
41
44
|
return;
|
|
45
|
+
}
|
|
42
46
|
for (const descriptor of descriptors) {
|
|
43
47
|
const kind = outwardActionOf(descriptorText(descriptor), descriptorRole(descriptor));
|
|
44
|
-
if (kind !== undefined && !allowed.includes(kind))
|
|
48
|
+
if (kind !== undefined && !allowed.includes(kind)) {
|
|
45
49
|
throw new Error(outwardRefusal(kind));
|
|
50
|
+
}
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
53
|
/** A Harness call goes to a payment provider's origin only when its journey asked to pay. */
|
|
49
54
|
export function assertNotPaymentNavigation(url) {
|
|
50
55
|
const allowed = harnessOutwardAllowed();
|
|
51
|
-
if (allowed === undefined || allowed.includes(OutwardAction.PAY))
|
|
56
|
+
if (allowed === undefined || allowed.includes(OutwardAction.PAY)) {
|
|
52
57
|
return;
|
|
53
|
-
|
|
58
|
+
}
|
|
59
|
+
if (isPaymentProviderUrl(url)) {
|
|
54
60
|
throw new Error(outwardRefusal(OutwardAction.PAY));
|
|
61
|
+
}
|
|
55
62
|
}
|
|
56
63
|
export function assertNotDestructive(action, innerArgs, inspected) {
|
|
57
|
-
if (action !== ActionType.CLICK && action !== ActionType.DBLCLICK)
|
|
64
|
+
if (action !== ActionType.CLICK && action !== ActionType.DBLCLICK) {
|
|
58
65
|
return;
|
|
66
|
+
}
|
|
59
67
|
assertOutwardAllowed(inspected);
|
|
60
|
-
if (true === innerArgs[DANGEROUS_ACTION_CONFIRM_ARG])
|
|
68
|
+
if (true === innerArgs[DANGEROUS_ACTION_CONFIRM_ARG]) {
|
|
61
69
|
return;
|
|
62
|
-
|
|
70
|
+
}
|
|
71
|
+
if (!isDestructiveDescriptor(inspected)) {
|
|
63
72
|
return;
|
|
73
|
+
}
|
|
64
74
|
throw new Error(`potentially destructive native action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
65
75
|
}
|
|
66
76
|
/**
|
|
@@ -70,10 +80,12 @@ export function assertNotDestructive(action, innerArgs, inspected) {
|
|
|
70
80
|
*/
|
|
71
81
|
export function assertDragNotDestructive(innerArgs, from, to) {
|
|
72
82
|
assertOutwardAllowed(from, to);
|
|
73
|
-
if (true === innerArgs[DANGEROUS_ACTION_CONFIRM_ARG])
|
|
83
|
+
if (true === innerArgs[DANGEROUS_ACTION_CONFIRM_ARG]) {
|
|
74
84
|
return;
|
|
75
|
-
|
|
85
|
+
}
|
|
86
|
+
if (!isDestructiveDescriptor(from) && !isDestructiveDescriptor(to)) {
|
|
76
87
|
return;
|
|
88
|
+
}
|
|
77
89
|
throw new Error(`potentially destructive native action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
78
90
|
}
|
|
79
91
|
/**
|
|
@@ -90,8 +102,9 @@ export const NATIVE_INPUT_UNSUPPORTED = `reticle_act_and_wait cannot drive nativ
|
|
|
90
102
|
'consequence with reticle_assert / reticle_observe using the `since` cursor it returns.';
|
|
91
103
|
/** Refuse rather than silently drop a native-input request the act-then-wait path cannot honour. */
|
|
92
104
|
export function assertNativeInputSupported(innerArgs) {
|
|
93
|
-
if (true === innerArgs[NATIVE_INPUT_ARG])
|
|
105
|
+
if (true === innerArgs[NATIVE_INPUT_ARG]) {
|
|
94
106
|
throw new Error(NATIVE_INPUT_UNSUPPORTED);
|
|
107
|
+
}
|
|
95
108
|
}
|
|
96
109
|
/**
|
|
97
110
|
* The agent's own declaration counts as the confirmation, for that one control.
|
|
@@ -104,10 +117,12 @@ export function assertNativeInputSupported(innerArgs) {
|
|
|
104
117
|
* guard decides alone.
|
|
105
118
|
*/
|
|
106
119
|
export function confirmedByDeclaration(innerArgs, target, declared) {
|
|
107
|
-
if (!declared || true === innerArgs[DANGEROUS_ACTION_CONFIRM_ARG])
|
|
120
|
+
if (!declared || true === innerArgs[DANGEROUS_ACTION_CONFIRM_ARG]) {
|
|
108
121
|
return innerArgs;
|
|
109
|
-
|
|
122
|
+
}
|
|
123
|
+
if (currentDrivenBy() !== undefined) {
|
|
110
124
|
return innerArgs;
|
|
125
|
+
}
|
|
111
126
|
const named = asRecord(target);
|
|
112
127
|
// The words must themselves be the dangerous ones: naming "Continue" on a button whose form posts
|
|
113
128
|
// to /delete-account consents to nothing, and the guard still reads the whole control.
|
|
@@ -20,7 +20,7 @@ export const durableArg = z
|
|
|
20
20
|
.describe('After a yes, reload and require the element/text/state part of `until` to hold again.');
|
|
21
21
|
// Loose and undescribed on purpose: the output surface is re-sent every turn. `because` carries
|
|
22
22
|
// the answer; this is the detail — { held?, observed?, skipped? }.
|
|
23
|
-
export const durableOutput = z.record(z.unknown()).optional();
|
|
23
|
+
export const durableOutput = z.record(z.string(), z.unknown()).optional();
|
|
24
24
|
const RE_READABLE = new Set([
|
|
25
25
|
PredicateKind.ELEMENT,
|
|
26
26
|
PredicateKind.TEXT,
|
|
@@ -122,7 +122,7 @@ export const ACT_SEQUENCE_TOOL = {
|
|
|
122
122
|
description: "Drive a WHOLE journey in ONE round-trip, with NO snapshot first. Name each step's element by {testid} | {label} | {role,name} | {text} in `target` and Reticle resolves it AT THE MOMENT THAT STEP RUNS — so a control that does not exist yet (a modal an earlier step opens, a row it creates) is still addressable, and waits up to timeout_ms to appear. This is the fast path: snapshot -> act -> snapshot -> act costs a model turn per step and is the slowest thing an agent can do. Give each step an `expect` and the sequence stops at the first one that does not hold, returning the un-run `tail` verbatim so you re-plan only the failure. Returns per-step effects[] (see reticle_act).",
|
|
123
123
|
inputSchema: {
|
|
124
124
|
steps: z
|
|
125
|
-
.array(z.record(z.unknown()))
|
|
125
|
+
.array(z.record(z.string(), z.unknown()))
|
|
126
126
|
.describe('Ordered list of { ref | target, action, args?, expect? } objects. Each step is equivalent to one reticle_act call, and takes its arguments either way: `{ ref, action: "fill", value: "…" }` flat, as reticle_act does, or nested as `args: { value }`. Give `ref` from a snapshot/query, or `target` ({ testid } | { label } | { role, name } | { text }) to resolve in this call. Put confirmDangerous:true in a destructive step args object. `expect` is the same predicate shape as reticle_act_and_wait `until`, and it is what makes a step PROVE something: a step without one is driven, not verified, and the result says so. Naming the consequence is also FASTER — a named consequence is detected the instant it fires, where waiting for the page to settle can only conclude by waiting for silence.'),
|
|
127
127
|
// A plain number, not boolean|number: a zod union serialises to a JSON-Schema `anyOf` that
|
|
128
128
|
// cost 174 B on a surface re-sent every turn, against 81 B of headroom. One spelling is also
|
|
@@ -163,14 +163,14 @@ export const ACT_SEQUENCE_TOOL = {
|
|
|
163
163
|
/** How many steps declared a consequence, of how many were driven. Never averaged away. */
|
|
164
164
|
coverage: z.object({ declared: z.number(), total: z.number() }).optional(),
|
|
165
165
|
/** Steps never run, verbatim, so a caller re-plans the failure rather than the whole journey. */
|
|
166
|
-
tail: z.array(z.record(z.unknown())).optional(),
|
|
166
|
+
tail: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
167
167
|
/**
|
|
168
168
|
* Each step carries the same effect record a REPLAYED step carries: `window` (the
|
|
169
169
|
* `reticle_observe { since, until }` drill address), `digest` (what the app did, as counts) and
|
|
170
170
|
* `contradictions` (channels that disagree, present even when the step passed). Declared here
|
|
171
171
|
* because an undeclared field is stripped from structuredContent and would be silently lost.
|
|
172
172
|
*/
|
|
173
|
-
steps: z.array(z.record(z.unknown())).optional(),
|
|
173
|
+
steps: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
174
174
|
result: z.unknown().optional(),
|
|
175
175
|
session: z
|
|
176
176
|
.object({ lastSeenMs: z.number(), throttled: z.boolean(), focused: z.boolean() })
|
|
@@ -116,14 +116,14 @@ export const ACT_TOOLS = [
|
|
|
116
116
|
.optional()
|
|
117
117
|
.describe(`Element ref (e.g. 'e42') from reticle_snapshot/reticle_query — stable until the element leaves the DOM, so no re-snapshot between actions. Give this OR \`target\`. A press of Escape, Tab, or a modifier shortcut (Cmd+K) is a document key and needs neither.`),
|
|
118
118
|
target: z
|
|
119
|
-
.record(z.unknown())
|
|
119
|
+
.record(z.string(), z.unknown())
|
|
120
120
|
.optional()
|
|
121
121
|
.describe('Find the element and act on it in ONE call, instead of a reticle_query round trip first: { testid } | { text } | { role, name } | { label }. Refuses if it matches more than one, rather than guessing.'),
|
|
122
122
|
action: actionTypeEnum.describe('Action to perform.'),
|
|
123
123
|
args: z
|
|
124
|
-
.record(z.unknown())
|
|
124
|
+
.record(z.string(), z.unknown())
|
|
125
125
|
.optional()
|
|
126
|
-
.describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt — a Cmd+K), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { native: true } to force a trusted native click, { holdMs: N } to keep the pointer or the key DOWN for N ms (hold-to-confirm controls; effect.heldMs reports what was achieved), { confirmDangerous: true } to allow a potentially destructive control — a permission gate, NOT a duration. For press: { keys: ["Control","k"] } holds several keys down TOGETHER (pressed in order, released in reverse) and { code } names the physical key — both stay on the in-page dispatcher, the only path that can express them. For upload: { path } is a path on disk (absolute or relative to the project root; the daemon reads the file and delivers real bytes to the file picker — this is the way to verify document-ingestion flows); or { name, content?, type? } to supply inline bytes directly.'),
|
|
126
|
+
.describe('Action-specific arguments: { value } for fill/select (select also drives an ARIA combobox: it opens it and clicks the option by its label), { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt — a Cmd+K) or a click/dblclick (Shift+click range select), { button: "right" | "middle" } for a click (right sends contextmenu), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { native: true } to force a trusted native click, { holdMs: N } to keep the pointer or the key DOWN for N ms (hold-to-confirm controls; effect.heldMs reports what was achieved), { confirmDangerous: true } to allow a potentially destructive control — a permission gate, NOT a duration. For press: { keys: ["Control","k"] } holds several keys down TOGETHER (pressed in order, released in reverse) and { code } names the physical key — both stay on the in-page dispatcher, the only path that can express them. For upload: { path } is a path on disk (absolute or relative to the project root; the daemon reads the file and delivers real bytes to the file picker — this is the way to verify document-ingestion flows); or { name, content?, type? } to supply inline bytes directly.'),
|
|
127
127
|
refuseWhenThrottled: z
|
|
128
128
|
.boolean()
|
|
129
129
|
.optional()
|
|
@@ -264,14 +264,14 @@ export const ACT_TOOLS = [
|
|
|
264
264
|
.optional()
|
|
265
265
|
.describe(`Element ref (e.g. 'e42') from reticle_snapshot/reticle_query — stable until the element leaves the DOM, so no re-snapshot between actions. Give this OR \`target\`. A press of Escape, Tab, or a modifier shortcut (Cmd+K) is a document key and needs neither.`),
|
|
266
266
|
target: z
|
|
267
|
-
.record(z.unknown())
|
|
267
|
+
.record(z.string(), z.unknown())
|
|
268
268
|
.optional()
|
|
269
269
|
.describe('Find the element and act on it in ONE call, instead of a reticle_query round trip first: { testid } | { text } | { role, name } | { label }. Refuses if it matches more than one, rather than guessing.'),
|
|
270
270
|
action: actionTypeEnum.describe('Action to perform.'),
|
|
271
271
|
args: z
|
|
272
|
-
.record(z.unknown())
|
|
272
|
+
.record(z.string(), z.unknown())
|
|
273
273
|
.optional()
|
|
274
|
-
.describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { confirmDangerous: true } for a potentially destructive control. For upload: { path } is a path on disk (absolute or relative to project root; daemon reads real bytes) or { name, content?, type? } for inline bytes.'),
|
|
274
|
+
.describe('Action-specific arguments: { value } for fill/select (select also drives an ARIA combobox: it opens it and clicks the option by its label), { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut or a click (Meta/Control/Shift/Alt), { button: "right" | "middle" } for a click, { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { confirmDangerous: true } for a potentially destructive control. For upload: { path } is a path on disk (absolute or relative to project root; daemon reads real bytes) or { name, content?, type? } for inline bytes.'),
|
|
275
275
|
predicate: PredicateSchema.optional().describe('Alias for `until`.'),
|
|
276
276
|
until: PredicateSchema.optional().describe('Predicate to wait for after the action completes (same shape as reticle_assert). OMIT to wait for the page to SETTLE — network + DOM idle — the deterministic default instead of a sleep. To assert a consequence AND settle, allOf them: { kind: "allOf", predicates: [<your predicate>, { kind: "settled" }] }.'),
|
|
277
277
|
timeout_ms: timeoutMsSchema
|
|
@@ -289,6 +289,7 @@ export const ACT_TOOLS = [
|
|
|
289
289
|
durable: durableOutput,
|
|
290
290
|
effect: z
|
|
291
291
|
.unknown()
|
|
292
|
+
.optional()
|
|
292
293
|
.describe('The reticle_act result (dispatched, settled, inputMode, etc.).'),
|
|
293
294
|
verdict: z.object({
|
|
294
295
|
pass: z.boolean(),
|
|
@@ -316,9 +317,11 @@ export const ACT_TOOLS = [
|
|
|
316
317
|
}),
|
|
317
318
|
trace: z
|
|
318
319
|
.unknown()
|
|
320
|
+
.optional()
|
|
319
321
|
.describe('Reaction digest: { window_ms, summary } of what the app did (DOM/network/route/console/signal counts). The full per-event timeline is one reticle_observe { since } away.'),
|
|
320
322
|
summary: z
|
|
321
323
|
.unknown()
|
|
324
|
+
.optional()
|
|
322
325
|
.describe('Bounded causal summary: net {total,errors,headline}, consoleErrors, statePathsChanged, storageKeysChanged, stateDiffs [{path,from,to}], storageDiffs [{key,from,to}], route, signals, layoutShift, longTasks — real before→after diffs (capped), not just readings. THIS IS THE ANSWER TO "what did that click do": console, network, storage and state for this action are all here, so do NOT follow an act with reticle_console + reticle_network + reticle_state to find out — call them only to go deeper into something this block already pointed at. Every list is length-capped so the verdict can never be the field a client truncates; `elided` says which lists lost entries and how many. `stateUnwatched: true` means NO subscribable store is registered, so an empty stateDiffs means unwatched, NOT unchanged — no state conclusion is available until one is registered.'),
|
|
323
326
|
// Promoted out of `effect` on RED only — the file:line the failure came from, the first thing a
|
|
324
327
|
// repair wants. Undeclared, it was stripped on the validating profile exactly like the structured
|
|
@@ -354,6 +357,7 @@ export const ACT_TOOLS = [
|
|
|
354
357
|
engineBlindSpots: engineBlindSpotsOutput,
|
|
355
358
|
honesty: z
|
|
356
359
|
.unknown()
|
|
360
|
+
.optional()
|
|
357
361
|
.describe('The verdict trust block { grade, attribution, coverage, integrity, envelope? } — a green never looks stronger than this. Gate on grade ≥ net AND integrity.clean. Fields that were not measured are OMITTED rather than reported as zero, so treat an absent `envelope` as "not sampled", never as a failure.'),
|
|
358
362
|
since: z
|
|
359
363
|
.number()
|
|
@@ -505,17 +509,12 @@ export const ACT_TOOLS = [
|
|
|
505
509
|
? undefined
|
|
506
510
|
: buildDivergenceCapsule(links, windowEvents, bufferLost);
|
|
507
511
|
/*
|
|
508
|
-
* The blast radius
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
* somewhere I never mentioned" is exactly the finding a passing verdict buries. A click that
|
|
513
|
-
* works and also fires a DELETE is a green that did extra damage.
|
|
512
|
+
* The blast radius goes on a GREEN too. On a red the capsule carries it. On a green, a click
|
|
513
|
+
* that works and also sends a DELETE did extra damage, and no other field shows that.
|
|
514
|
+
* It is attached only when it is not empty, because its presence is the signal. The full
|
|
515
|
+
* capsule stays red-only: its divergence walk and causal summary answer a red's question.
|
|
514
516
|
*
|
|
515
|
-
* One pass over a window already in hand
|
|
516
|
-
* that is always present teaches a reader to skim it, and the whole value here is that its
|
|
517
|
-
* presence is the signal. The full capsule stays red-only — the divergence walk and the
|
|
518
|
-
* causal summary are the expensive halves and answer a question a green does not have.
|
|
517
|
+
* One pass over a window that is already in hand.
|
|
519
518
|
*/
|
|
520
519
|
const greenRadius = verdict.pass ? blastRadius(links, windowEvents) : [];
|
|
521
520
|
// Grade from what the verdict PROVED, not what it declared. A green anyOf holds on one branch, so
|
|
@@ -75,6 +75,6 @@ export declare function shapeForAgent(tool: string, args: Record<string, unknown
|
|
|
75
75
|
*/
|
|
76
76
|
export declare function withCompactEnvelope<T extends {
|
|
77
77
|
name: string;
|
|
78
|
-
inputSchema: z.
|
|
79
|
-
outputSchema?: z.
|
|
78
|
+
inputSchema: Record<string, z.ZodType>;
|
|
79
|
+
outputSchema?: Record<string, z.ZodType>;
|
|
80
80
|
}>(tools: T[]): T[];
|
|
@@ -69,7 +69,7 @@ export function buildCoverageTools() {
|
|
|
69
69
|
outputSchema: {
|
|
70
70
|
// The app-wide ledger — see features/exhaust/ledger.ts. Loose: run-only, and the levels
|
|
71
71
|
// carry their own shape.
|
|
72
|
-
app: z.record(z.unknown()).optional(),
|
|
72
|
+
app: z.record(z.string(), z.unknown()).optional(),
|
|
73
73
|
total: z.number(),
|
|
74
74
|
exercised: z.number(),
|
|
75
75
|
untouched: z.array(z.object({ ref: z.string(), label: z.string() })),
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import type { ToolDef } from './tool-kit.js';
|
|
2
3
|
import { type ToolSurfaceOrigin } from './tool-surface.js';
|
|
4
|
+
/**
|
|
5
|
+
* A parameter's description, wherever in its wrappers it was written.
|
|
6
|
+
*
|
|
7
|
+
* zod 4 keeps a description on the schema `.describe()` was called on. `z.string().describe(d)
|
|
8
|
+
* .optional()` has none on the outside, although the JSON Schema an agent reads still carries `d`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function descriptionOf(schema: z.ZodTypeAny): string | undefined;
|
|
3
11
|
/**
|
|
4
12
|
* Build the two dynamic meta-tools over the full tool table. `reticle_run` dispatches through the same
|
|
5
13
|
* `runTool` chokepoint as a direct call, so session-health splicing and every other invariant hold.
|
|
@@ -8,25 +8,43 @@ import { ReticleTool } from '@reticlehq/core';
|
|
|
8
8
|
import { ADVERTISE_ALL_ENV } from './tool-surface.js';
|
|
9
9
|
import { getSessionMetrics } from '../../telemetry/session-metrics.js';
|
|
10
10
|
import { isPredicateParam, predicateGrammar, } from '@reticlehq/engine/question/predicate/predicate-eval.js';
|
|
11
|
+
/** One wrapper that does not change the value a schema accepts, peeled off; undefined at the core. */
|
|
12
|
+
function unwrapOnce(schema) {
|
|
13
|
+
if (schema instanceof z.ZodOptional || schema instanceof z.ZodNullable) {
|
|
14
|
+
return schema.unwrap();
|
|
15
|
+
}
|
|
16
|
+
if (schema instanceof z.ZodDefault) {
|
|
17
|
+
return schema.removeDefault();
|
|
18
|
+
}
|
|
19
|
+
if (schema instanceof z.ZodPipe) {
|
|
20
|
+
// A transform pipes the schema into the transform; a preprocess pipes the other way round.
|
|
21
|
+
return (schema.in instanceof z.ZodTransform ? schema.out : schema.in);
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
11
25
|
/** The schema without the wrappers that do not change the value it accepts. */
|
|
12
26
|
function unwrappedOf(schema) {
|
|
13
27
|
let inner = schema;
|
|
14
|
-
for (
|
|
15
|
-
|
|
16
|
-
inner = inner.unwrap();
|
|
17
|
-
}
|
|
18
|
-
else if (inner instanceof z.ZodDefault) {
|
|
19
|
-
inner = inner.removeDefault();
|
|
20
|
-
}
|
|
21
|
-
else if (inner instanceof z.ZodEffects) {
|
|
22
|
-
inner = inner.innerType();
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
break;
|
|
26
|
-
}
|
|
28
|
+
for (let next = unwrapOnce(inner); next !== undefined; next = unwrapOnce(inner)) {
|
|
29
|
+
inner = next;
|
|
27
30
|
}
|
|
28
31
|
return inner;
|
|
29
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* A parameter's description, wherever in its wrappers it was written.
|
|
35
|
+
*
|
|
36
|
+
* zod 4 keeps a description on the schema `.describe()` was called on. `z.string().describe(d)
|
|
37
|
+
* .optional()` has none on the outside, although the JSON Schema an agent reads still carries `d`.
|
|
38
|
+
*/
|
|
39
|
+
export function descriptionOf(schema) {
|
|
40
|
+
for (let current = schema; current !== undefined;) {
|
|
41
|
+
if (current.description !== undefined) {
|
|
42
|
+
return current.description;
|
|
43
|
+
}
|
|
44
|
+
current = unwrapOnce(current);
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
30
48
|
/**
|
|
31
49
|
* A numeric parameter's enforced bounds, read off the schema that enforces them (#1537).
|
|
32
50
|
*
|
|
@@ -39,7 +57,9 @@ function boundsOf(schema) {
|
|
|
39
57
|
if (!(inner instanceof z.ZodNumber)) {
|
|
40
58
|
return {};
|
|
41
59
|
}
|
|
42
|
-
|
|
60
|
+
// `.int()` reports the safe-integer range as its bounds. That range bounds nothing an agent sends.
|
|
61
|
+
const min = Number.MIN_SAFE_INTEGER < (inner.minValue ?? Number.MIN_SAFE_INTEGER) ? inner.minValue : null;
|
|
62
|
+
const max = Number.MAX_SAFE_INTEGER > (inner.maxValue ?? Number.MAX_SAFE_INTEGER) ? inner.maxValue : null;
|
|
43
63
|
return { ...(null === min ? {} : { min }), ...(null === max ? {} : { max }) };
|
|
44
64
|
}
|
|
45
65
|
/**
|
|
@@ -68,7 +88,7 @@ function paramInfo(shape) {
|
|
|
68
88
|
return {
|
|
69
89
|
name,
|
|
70
90
|
required: !schema.isOptional(),
|
|
71
|
-
description: schema
|
|
91
|
+
description: descriptionOf(schema) ?? '',
|
|
72
92
|
...(values === undefined ? {} : { values }),
|
|
73
93
|
...boundsOf(schema),
|
|
74
94
|
};
|
|
@@ -260,7 +280,7 @@ callable) {
|
|
|
260
280
|
};
|
|
261
281
|
const runShape = {
|
|
262
282
|
tool: z.string().describe('Tool name to invoke, e.g. reticle_clock.'),
|
|
263
|
-
args: z.record(z.unknown()).optional().describe('Arguments object for that tool.'),
|
|
283
|
+
args: z.record(z.string(), z.unknown()).optional().describe('Arguments object for that tool.'),
|
|
264
284
|
// Accepted AND FORWARDED, not merely tolerated. reticle_run is the only way to reach an
|
|
265
285
|
// unadvertised tool, so on a machine running several projects it has to be aimable — and
|
|
266
286
|
// `sessionId` is the shape an agent already uses on every other tool. Reported from the field
|