@outcrawl/sdk 0.1.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 +198 -0
- package/dist/index.js +2493 -0
- package/dist/types/_deps/core/agent-alias.d.ts +219 -0
- package/dist/types/_deps/core/brand.d.ts +62 -0
- package/dist/types/_deps/core/certificate.d.ts +100 -0
- package/dist/types/_deps/core/cron.d.ts +116 -0
- package/dist/types/_deps/core/errors.d.ts +340 -0
- package/dist/types/_deps/core/index.d.ts +17 -0
- package/dist/types/_deps/core/money.d.ts +108 -0
- package/dist/types/_deps/core/registry.d.ts +466 -0
- package/dist/types/_deps/core/rules.d.ts +237 -0
- package/dist/types/_deps/core/secrets.d.ts +278 -0
- package/dist/types/_deps/core/types.d.ts +1677 -0
- package/dist/types/_deps/integrations/connector.d.ts +153 -0
- package/dist/types/_deps/replay/events.d.ts +1004 -0
- package/dist/types/agent.d.ts +313 -0
- package/dist/types/availability.d.ts +86 -0
- package/dist/types/browser.d.ts +107 -0
- package/dist/types/client.d.ts +128 -0
- package/dist/types/hands.d.ts +259 -0
- package/dist/types/index.d.ts +111 -0
- package/dist/types/integrations.d.ts +63 -0
- package/dist/types/monitors.d.ts +24 -0
- package/dist/types/page.d.ts +96 -0
- package/dist/types/profiles.d.ts +26 -0
- package/dist/types/result.d.ts +90 -0
- package/dist/types/rules.d.ts +41 -0
- package/dist/types/scrape.d.ts +68 -0
- package/dist/types/secrets.d.ts +36 -0
- package/dist/types/sessions.d.ts +124 -0
- package/dist/types/transport.d.ts +250 -0
- package/package.json +70 -0
|
@@ -0,0 +1,1677 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outcrawl wire contract.
|
|
3
|
+
*
|
|
4
|
+
* Every surface — SDK, REST API, MCP, CLI — speaks these shapes. The API is the
|
|
5
|
+
* truth; the other three are transports. See `registry.ts` for the capability
|
|
6
|
+
* list they are all generated or checked against.
|
|
7
|
+
*
|
|
8
|
+
* Three rules from the product surface spec are enforced *by omission* here,
|
|
9
|
+
* and all three are load-bearing:
|
|
10
|
+
*
|
|
11
|
+
* 1. The caller never chooses identity, seed, fingerprint, timezone,
|
|
12
|
+
* languages or webrtcIp. We mint them. Anything configurable is
|
|
13
|
+
* eventually configured wrong, and an incoherent identity is what gets
|
|
14
|
+
* caught — not any single value.
|
|
15
|
+
* 2. The caller never *sees* identity either. `Profile` and `SessionQuery`
|
|
16
|
+
* deliberately have no `identity` field. Customers see outcomes; we see
|
|
17
|
+
* which identity produced them.
|
|
18
|
+
* 3. The caller never chooses a model, and never learns which one ran.
|
|
19
|
+
* `AgentRequest` has no `model` field: it names a harness — `scout-1` or
|
|
20
|
+
* `voyager-1`, see `agent-alias.ts` — and which models that harness runs,
|
|
21
|
+
* including on a mid-run failover, is ours. A published per-minute price
|
|
22
|
+
* and a caller-chosen model cannot coexist; measured on one real
|
|
23
|
+
* production run the cheapest and dearest credible drivers differ by 126x,
|
|
24
|
+
* and whoever picks absorbs that. So the caller picks the capability. No
|
|
25
|
+
* response type, no usage row, no cache reason and no error message in
|
|
26
|
+
* this file carries a model id.
|
|
27
|
+
*/
|
|
28
|
+
import { type AgentAlias } from './agent-alias.js';
|
|
29
|
+
/**
|
|
30
|
+
* Which browser an execution presents as. Minted by us at
|
|
31
|
+
* `Target.createBrowserContext`, pinned for the life of a profile, and NEVER
|
|
32
|
+
* exposed to customers — it appears in no request and no response type in this
|
|
33
|
+
* file. Operator-side only.
|
|
34
|
+
*/
|
|
35
|
+
export type Identity = 'chrome' | 'brave';
|
|
36
|
+
/**
|
|
37
|
+
* Proxy targeting is country/region/city ONLY. No ASN, no ZIP, no ISP —
|
|
38
|
+
* geography is a legitimate product choice, the rest is a way to build an
|
|
39
|
+
* identity that cannot exist.
|
|
40
|
+
*/
|
|
41
|
+
export interface ExitTarget {
|
|
42
|
+
country: string;
|
|
43
|
+
region?: string;
|
|
44
|
+
city?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A concrete exit, resolved at session open. Session-scoped, not
|
|
48
|
+
* profile-scoped: a profile pins the city, the IP rotates underneath it.
|
|
49
|
+
*/
|
|
50
|
+
export interface ResolvedExit {
|
|
51
|
+
ip: string;
|
|
52
|
+
country: string;
|
|
53
|
+
region: string;
|
|
54
|
+
city: string;
|
|
55
|
+
/** IANA zone, from the city table. Never client-supplied. */
|
|
56
|
+
timezone: string;
|
|
57
|
+
/** Single tag for chrome identity, e.g. "en-US". */
|
|
58
|
+
languages: string;
|
|
59
|
+
lat: number;
|
|
60
|
+
lon: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* An exit may be named as a structured target or as a proxy URL of the form
|
|
64
|
+
* `outcrawl://<country>[/<region>[/<city>]]`, e.g. `outcrawl://us/texas/austin`.
|
|
65
|
+
*/
|
|
66
|
+
export type ExitSpec = ExitTarget | string;
|
|
67
|
+
/**
|
|
68
|
+
* Counters, all integers. These are safe as `number`: token counts and
|
|
69
|
+
* millisecond timings sit well inside binary64's exact-integer range.
|
|
70
|
+
*
|
|
71
|
+
* The meter does not. `UsageEventRow.quantity` in `@outcrawl/persistence` is an
|
|
72
|
+
* exact decimal string, and a surface that aggregates usage_events into this
|
|
73
|
+
* shape is crossing string -> number. That direction is fine. **The reverse
|
|
74
|
+
* never is**: append the string the meter produced, never a number that has
|
|
75
|
+
* been through binary64 and back. Money and meters are not floats, and a
|
|
76
|
+
* rounding error found in a reconciliation months later is unattributable.
|
|
77
|
+
*/
|
|
78
|
+
export interface Usage {
|
|
79
|
+
inputTokens?: number;
|
|
80
|
+
outputTokens?: number;
|
|
81
|
+
reasoningTokens?: number;
|
|
82
|
+
cachedInputTokens?: number;
|
|
83
|
+
inferenceTimeMs?: number;
|
|
84
|
+
cache?: {
|
|
85
|
+
status: 'HIT' | 'MISS' | 'DISABLED';
|
|
86
|
+
reason?: string;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Resources that actually appear on the bill. Charged, not estimated. */
|
|
90
|
+
export interface BilledResources {
|
|
91
|
+
browserMinutes: number;
|
|
92
|
+
bandwidthBytes: number;
|
|
93
|
+
/** Decimal string, e.g. "2.00". Never a float — money is not binary64. */
|
|
94
|
+
credits: string;
|
|
95
|
+
}
|
|
96
|
+
/** A JSON Schema object. Drives `json` extraction and agent success detection. */
|
|
97
|
+
export type JsonSchema = {
|
|
98
|
+
readonly [key: string]: unknown;
|
|
99
|
+
};
|
|
100
|
+
/** ISO 8601 instant, e.g. "2026-08-29T11:04:00.000Z". */
|
|
101
|
+
export type Timestamp = string;
|
|
102
|
+
/**
|
|
103
|
+
* Frame-qualified node reference: `<frameOrdinal>-<backendNodeId>`.
|
|
104
|
+
*
|
|
105
|
+
* Backend node ids come from a per-renderer-process static map, and
|
|
106
|
+
* cross-origin iframes run in different renderer processes under Site
|
|
107
|
+
* Isolation, so a bare backendNodeId can collide across frames.
|
|
108
|
+
*/
|
|
109
|
+
export type NodeRef = `${number}-${number}`;
|
|
110
|
+
/**
|
|
111
|
+
* The de facto vocabulary. `html` is post-transform, `rawHtml` is
|
|
112
|
+
* pre-transform, and `json` cascades — it needs markdown first. That ordering
|
|
113
|
+
* is why this is one pipeline and not five.
|
|
114
|
+
*/
|
|
115
|
+
export declare const SCRAPE_FORMATS: readonly ["markdown", "html", "rawHtml", "links", "screenshot", "screenshot@fullPage", "json", "changeTracking"];
|
|
116
|
+
export type ScrapeFormat = (typeof SCRAPE_FORMATS)[number];
|
|
117
|
+
export declare function isScrapeFormat(value: unknown): value is ScrapeFormat;
|
|
118
|
+
export interface ScrapeRequest {
|
|
119
|
+
url: string;
|
|
120
|
+
/** Defaults to `['markdown']`. */
|
|
121
|
+
formats?: readonly ScrapeFormat[];
|
|
122
|
+
/** Defaults to true — strip nav, chrome and boilerplate. */
|
|
123
|
+
onlyMainContent?: boolean;
|
|
124
|
+
/** Required when `formats` includes `json`. */
|
|
125
|
+
schema?: JsonSchema;
|
|
126
|
+
/** Pinned identity and logged-in state. */
|
|
127
|
+
profile?: string;
|
|
128
|
+
/** Geography only. Ignored when `profile` is given — a profile pins its exit. */
|
|
129
|
+
exit?: ExitSpec;
|
|
130
|
+
/** Extra settle time after the content classifier fires, in milliseconds. */
|
|
131
|
+
waitFor?: number;
|
|
132
|
+
timeoutMs?: number;
|
|
133
|
+
/** Compared against a previous snapshot when `changeTracking` is requested. */
|
|
134
|
+
changeTrackingTag?: string;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* How a captcha was resolved on a page.
|
|
138
|
+
*
|
|
139
|
+
* `abandoned` is the load-bearing value: the solve outran its budget, so the
|
|
140
|
+
* document in hand is the interstitial and NOT the page. A caller that treats
|
|
141
|
+
* it as content is parsing a challenge screen. `not-attempted` means solving
|
|
142
|
+
* was deliberately never started because the request's own timeout could not
|
|
143
|
+
* accommodate one — buying a token and then discarding it is worse than not
|
|
144
|
+
* buying it.
|
|
145
|
+
*/
|
|
146
|
+
export type PageChallengeOutcome = 'solved' | 'unsolved' | 'not-attempted' | 'abandoned';
|
|
147
|
+
export interface PageChallenge {
|
|
148
|
+
readonly outcome: PageChallengeOutcome;
|
|
149
|
+
/** Vendor as the browser named it, e.g. `recaptcha`. */
|
|
150
|
+
readonly detector: string;
|
|
151
|
+
readonly elapsedMs: number;
|
|
152
|
+
/** Solve budget this page reserved. 0 means solving was not armed. */
|
|
153
|
+
readonly budgetMs: number;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Whether this page was WATCHED for a challenge at all, which is what makes an
|
|
157
|
+
* absent {@link PageMetadata.challenge} interpretable.
|
|
158
|
+
*
|
|
159
|
+
* IT EXISTS BECAUSE THE ABSENCE WAS AMBIGUOUS AND SILENT. `challenge` is
|
|
160
|
+
* populated only from an armed solve window, and `@outcrawl/render`'s
|
|
161
|
+
* `unarmedWindow` settles `null` on purpose — with no policy in force the
|
|
162
|
+
* browser inspects nothing, so reporting a challenge would be inventing an
|
|
163
|
+
* observation. Correct, and it left every unwatched page indistinguishable
|
|
164
|
+
* from a page that genuinely had no captcha: HTTP 200, a full charge, the
|
|
165
|
+
* interstitial as `markdown`, and no field saying so.
|
|
166
|
+
*
|
|
167
|
+
* `PipelineTrace` already carried the numbers that resolve it and already
|
|
168
|
+
* argued the case — "without this number a page that was deliberately left
|
|
169
|
+
* unsolvable and a page that simply had no captcha are indistinguishable to an
|
|
170
|
+
* operator". That is the same ambiguity, and it was closed for an OPERATOR
|
|
171
|
+
* while the CUSTOMER kept it. This field is that fact on the surface the
|
|
172
|
+
* customer reads.
|
|
173
|
+
*
|
|
174
|
+
* `armed` — a solve window was open. An absent `challenge` then means there
|
|
175
|
+
* was no challenge, and that is a statement rather than a silence.
|
|
176
|
+
*
|
|
177
|
+
* `unarmed` — no window. Either this worker is not solving at all
|
|
178
|
+
* (`--challenge-mode=ignore`, or no provider key), or the request's own
|
|
179
|
+
* `timeoutMs` could not accommodate a solve. An absent `challenge` here means
|
|
180
|
+
* WE DO NOT KNOW, and a challenged page is exactly what it looks like.
|
|
181
|
+
*
|
|
182
|
+
* `refused` — a window was asked for and the browser declined the policy. Our
|
|
183
|
+
* fleet's fault rather than the caller's, distinguished from `unarmed` because
|
|
184
|
+
* "we chose not to look" and "we tried to look and could not" are different
|
|
185
|
+
* facts and only one of them is actionable by the customer.
|
|
186
|
+
*/
|
|
187
|
+
export type ChallengeWatch = 'armed' | 'unarmed' | 'refused';
|
|
188
|
+
export interface PageMetadata {
|
|
189
|
+
title?: string;
|
|
190
|
+
description?: string;
|
|
191
|
+
/** Content language as declared by the page, not by us. */
|
|
192
|
+
language?: string;
|
|
193
|
+
contentType?: string;
|
|
194
|
+
scrapedAt: Timestamp;
|
|
195
|
+
/**
|
|
196
|
+
* True when the cheap stop-early path was not enough and the page was given
|
|
197
|
+
* time to run its scripts. Every request leaves through the browser's own
|
|
198
|
+
* network stack either way — the saving is in stopping early, never in
|
|
199
|
+
* skipping the browser.
|
|
200
|
+
*/
|
|
201
|
+
escalated: boolean;
|
|
202
|
+
/**
|
|
203
|
+
* How a captcha on this page was resolved. Absent when none was OBSERVED,
|
|
204
|
+
* which is not the same as none having been raised — read
|
|
205
|
+
* {@link PageMetadata.challengeWatch} to tell those apart.
|
|
206
|
+
*/
|
|
207
|
+
challenge?: PageChallenge;
|
|
208
|
+
/**
|
|
209
|
+
* Whether a challenge could have been observed on this page at all.
|
|
210
|
+
*
|
|
211
|
+
* **Required, with no `?`.** An optional field here would reintroduce the
|
|
212
|
+
* exact defect it closes: a consumer cannot tell an absent watch state from
|
|
213
|
+
* an unwatched page, and this repository has already shipped six seams whose
|
|
214
|
+
* omission was read as a decision.
|
|
215
|
+
*/
|
|
216
|
+
challengeWatch: ChallengeWatch;
|
|
217
|
+
}
|
|
218
|
+
export type ChangeStatus = 'same' | 'changed' | 'new' | 'removed';
|
|
219
|
+
export interface ChangeTracking {
|
|
220
|
+
status: ChangeStatus;
|
|
221
|
+
previousScrapedAt?: Timestamp;
|
|
222
|
+
/** Unified diff over the normalised DOM, present only when status is `changed`. */
|
|
223
|
+
diff?: string;
|
|
224
|
+
/** Model summary of the diff — the diff, never the whole page. */
|
|
225
|
+
summary?: string;
|
|
226
|
+
}
|
|
227
|
+
export interface ScrapeResult {
|
|
228
|
+
/** Final URL after redirects. */
|
|
229
|
+
url: string;
|
|
230
|
+
statusCode: number;
|
|
231
|
+
markdown?: string;
|
|
232
|
+
html?: string;
|
|
233
|
+
rawHtml?: string;
|
|
234
|
+
links?: string[];
|
|
235
|
+
/**
|
|
236
|
+
* The capture itself, base64 PNG, as `Page.captureScreenshot` returned it.
|
|
237
|
+
* Both `screenshot` and `screenshot@fullPage` land here.
|
|
238
|
+
*
|
|
239
|
+
* NOT A URL, and this comment said "Fetchable URL" until it was measured.
|
|
240
|
+
* Live `POST /v1/scrape` against prod on 2026-09-06 returned 610,368 bytes
|
|
241
|
+
* of PNG in this field — `89 50 4e 47`, 1200x841, 8-bit RGB — because
|
|
242
|
+
* `renderFormats` puts `PipelineDeps.screenshot`'s reply here unchanged and
|
|
243
|
+
* nothing between it and the wire uploads anything. A client that wrote
|
|
244
|
+
* `fetch(result.screenshot)` on the strength of the old sentence would send
|
|
245
|
+
* a 610 kB request line.
|
|
246
|
+
*/
|
|
247
|
+
screenshot?: string;
|
|
248
|
+
/** Present when `json` was requested; shaped by the supplied schema. */
|
|
249
|
+
json?: unknown;
|
|
250
|
+
changeTracking?: ChangeTracking;
|
|
251
|
+
metadata: PageMetadata;
|
|
252
|
+
usage: Usage;
|
|
253
|
+
}
|
|
254
|
+
export interface CrawlRequest {
|
|
255
|
+
url: string;
|
|
256
|
+
/** Hard ceiling on pages fetched. */
|
|
257
|
+
limit?: number;
|
|
258
|
+
maxDepth?: number;
|
|
259
|
+
includePaths?: readonly string[];
|
|
260
|
+
excludePaths?: readonly string[];
|
|
261
|
+
allowSubdomains?: boolean;
|
|
262
|
+
allowExternalLinks?: boolean;
|
|
263
|
+
/** Skip sitemap.xml discovery and crawl the link graph only. */
|
|
264
|
+
ignoreSitemap?: boolean;
|
|
265
|
+
formats?: readonly ScrapeFormat[];
|
|
266
|
+
onlyMainContent?: boolean;
|
|
267
|
+
schema?: JsonSchema;
|
|
268
|
+
/** One warm context for the whole crawl: one coherent identity, not a thousand. */
|
|
269
|
+
profile?: string;
|
|
270
|
+
exit?: ExitSpec;
|
|
271
|
+
/** Delivered per page as the crawl streams; results are never batched. */
|
|
272
|
+
webhook?: string;
|
|
273
|
+
}
|
|
274
|
+
/** A crawled page: a scrape result plus its position in the frontier. */
|
|
275
|
+
export interface CrawlPage extends ScrapeResult {
|
|
276
|
+
depth: number;
|
|
277
|
+
/** The page whose link graph produced this URL; absent for seeds and sitemap entries. */
|
|
278
|
+
discoveredFrom?: string;
|
|
279
|
+
}
|
|
280
|
+
export type JobStatus = 'queued' | 'running' | 'completed' | 'failed' | 'cancelled';
|
|
281
|
+
/**
|
|
282
|
+
* One URL a crawl could not fetch. Produced by the scheduler in
|
|
283
|
+
* `@outcrawl/render`, which imports this rather than declaring its own — the
|
|
284
|
+
* producer owns the behaviour, core owns the wire shape.
|
|
285
|
+
*/
|
|
286
|
+
export interface CrawlFailure {
|
|
287
|
+
url: string;
|
|
288
|
+
depth: number;
|
|
289
|
+
message: string;
|
|
290
|
+
}
|
|
291
|
+
export interface CrawlJob {
|
|
292
|
+
id: string;
|
|
293
|
+
status: JobStatus;
|
|
294
|
+
url: string;
|
|
295
|
+
/** Pages completed so far. Crawls stream, so this moves. */
|
|
296
|
+
completed: number;
|
|
297
|
+
/** Pages currently known to the frontier — an estimate until the crawl ends. */
|
|
298
|
+
discovered: number;
|
|
299
|
+
createdAt: Timestamp;
|
|
300
|
+
/**
|
|
301
|
+
* Per-URL failures. Present and populated on runs that otherwise succeeded —
|
|
302
|
+
* a crawl that fetched 88 of 100 pages is a success with twelve failures, not
|
|
303
|
+
* a failed crawl, so an empty check here is not a success check.
|
|
304
|
+
*
|
|
305
|
+
* Declared because without it the information is simply lost at the wire: the
|
|
306
|
+
* scheduler collects the list, the closing frame is a `CrawlJob`, and a
|
|
307
|
+
* customer who hit twelve dead links would receive `completed: 88` with no
|
|
308
|
+
* way to learn which twelve.
|
|
309
|
+
*/
|
|
310
|
+
failures?: readonly CrawlFailure[];
|
|
311
|
+
usage: Usage;
|
|
312
|
+
}
|
|
313
|
+
export interface SearchRequest {
|
|
314
|
+
query: string;
|
|
315
|
+
limit?: number;
|
|
316
|
+
/** Render every hit through our pipeline. Without it this is a pass-through. */
|
|
317
|
+
scrape?: boolean;
|
|
318
|
+
formats?: readonly ScrapeFormat[];
|
|
319
|
+
/** Two-letter country bias for the index. */
|
|
320
|
+
country?: string;
|
|
321
|
+
lang?: string;
|
|
322
|
+
}
|
|
323
|
+
export interface SearchHit {
|
|
324
|
+
url: string;
|
|
325
|
+
title: string;
|
|
326
|
+
snippet: string;
|
|
327
|
+
/** 1-based position in the result set. */
|
|
328
|
+
rank: number;
|
|
329
|
+
/** Populated only when `scrape` was requested. */
|
|
330
|
+
markdown?: string;
|
|
331
|
+
html?: string;
|
|
332
|
+
rawHtml?: string;
|
|
333
|
+
links?: string[];
|
|
334
|
+
json?: unknown;
|
|
335
|
+
}
|
|
336
|
+
export interface SearchResponse {
|
|
337
|
+
hits: SearchHit[];
|
|
338
|
+
usage: Usage;
|
|
339
|
+
}
|
|
340
|
+
export declare const NOTIFY_ADAPTERS: readonly ["discord", "slack", "webhook"];
|
|
341
|
+
export type NotifyAdapter = (typeof NOTIFY_ADAPTERS)[number];
|
|
342
|
+
export declare function isNotifyAdapter(value: unknown): value is NotifyAdapter;
|
|
343
|
+
interface NotifyCommon {
|
|
344
|
+
/** Shapes the payload the adapter emits. Discord embed and Slack Block Kit otherwise. */
|
|
345
|
+
schema?: JsonSchema;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Exactly one adapter per monitor. A union rather than a bag of optionals, so
|
|
349
|
+
* a monitor that notifies nowhere cannot be constructed.
|
|
350
|
+
*/
|
|
351
|
+
export type MonitorNotify = (NotifyCommon & {
|
|
352
|
+
discord: string;
|
|
353
|
+
}) | (NotifyCommon & {
|
|
354
|
+
slack: string;
|
|
355
|
+
}) | (NotifyCommon & {
|
|
356
|
+
webhook: string;
|
|
357
|
+
});
|
|
358
|
+
/**
|
|
359
|
+
* Normalisation is where these products live or die. A monitor that fires on a
|
|
360
|
+
* rotating ad or a timestamp is worthless, so the defaults are deliberately
|
|
361
|
+
* lossy.
|
|
362
|
+
*/
|
|
363
|
+
export interface MonitorNormalisation {
|
|
364
|
+
/** Default true. */
|
|
365
|
+
stripTimestamps?: boolean;
|
|
366
|
+
/** Default true. CSRF tokens, cache-busters, build hashes. */
|
|
367
|
+
stripNonces?: boolean;
|
|
368
|
+
/** Default true. */
|
|
369
|
+
stripAds?: boolean;
|
|
370
|
+
/** Default true. */
|
|
371
|
+
stripSessionIds?: boolean;
|
|
372
|
+
/** Default true — sibling reordering is not a change. */
|
|
373
|
+
ignoreOrdering?: boolean;
|
|
374
|
+
/** Subtrees excluded from the diff entirely. */
|
|
375
|
+
ignoreSelectors?: readonly string[];
|
|
376
|
+
}
|
|
377
|
+
export interface MonitorSpec {
|
|
378
|
+
url: string;
|
|
379
|
+
/** Cron expression, e.g. "0 9 * * *". */
|
|
380
|
+
schedule: string;
|
|
381
|
+
notify: MonitorNotify;
|
|
382
|
+
/** Optional human label; the URL is the identity. */
|
|
383
|
+
label?: string;
|
|
384
|
+
formats?: readonly ScrapeFormat[];
|
|
385
|
+
profile?: string;
|
|
386
|
+
exit?: ExitSpec;
|
|
387
|
+
/** Run a model over the diff — the diff, not the page. Default false. */
|
|
388
|
+
judge?: boolean;
|
|
389
|
+
normalise?: MonitorNormalisation;
|
|
390
|
+
/** Default true. */
|
|
391
|
+
enabled?: boolean;
|
|
392
|
+
}
|
|
393
|
+
export interface Monitor extends MonitorSpec {
|
|
394
|
+
id: string;
|
|
395
|
+
createdAt: Timestamp;
|
|
396
|
+
lastCheckedAt?: Timestamp;
|
|
397
|
+
lastStatus?: ChangeStatus;
|
|
398
|
+
}
|
|
399
|
+
export interface MonitorCheck {
|
|
400
|
+
monitorId: string;
|
|
401
|
+
checkedAt: Timestamp;
|
|
402
|
+
status: ChangeStatus;
|
|
403
|
+
/** DOM tree diff, not a text diff. Present only when status is `changed`. */
|
|
404
|
+
diff?: string;
|
|
405
|
+
summary?: string;
|
|
406
|
+
/** The replay covering this check. */
|
|
407
|
+
replayUrl?: string;
|
|
408
|
+
usage: Usage;
|
|
409
|
+
}
|
|
410
|
+
export interface MonitorQuery {
|
|
411
|
+
url?: string;
|
|
412
|
+
/** Glob, e.g. "pricing-*". */
|
|
413
|
+
label?: string;
|
|
414
|
+
enabled?: boolean;
|
|
415
|
+
limit?: number;
|
|
416
|
+
cursor?: string;
|
|
417
|
+
}
|
|
418
|
+
export declare const CAP_NAMES: readonly ["steps", "budget", "duration"];
|
|
419
|
+
export type CapName = (typeof CAP_NAMES)[number];
|
|
420
|
+
/**
|
|
421
|
+
* Required on every agent run. The customer sets a ceiling before spending
|
|
422
|
+
* rather than disputing a bill afterwards, and since failures cost more than
|
|
423
|
+
* successes the same cap protects our margin.
|
|
424
|
+
*/
|
|
425
|
+
export interface AgentCaps {
|
|
426
|
+
/** Hard stop; the task then fails cleanly with partial data attached. */
|
|
427
|
+
steps: number;
|
|
428
|
+
/** Credits as a decimal string, e.g. "2.00". Not steps, not tokens. */
|
|
429
|
+
budget: string;
|
|
430
|
+
/**
|
|
431
|
+
* Wall clock: `<n>ms` | `<n>s` | `<n>m` | `<n>h`, e.g. "5m".
|
|
432
|
+
*
|
|
433
|
+
* **There is no ceiling above this and there is deliberately no longer one.**
|
|
434
|
+
* A run is a job rather than a held request, so nothing in the platform cuts
|
|
435
|
+
* it off at a number the caller did not choose: `caps` is the customer's dial
|
|
436
|
+
* and hours are normal. The `h` unit stopped being decorative on the day the
|
|
437
|
+
* job model landed.
|
|
438
|
+
*
|
|
439
|
+
* What used to live here was `MAX_RUN_DURATION_MS`, a 15-minute refusal that
|
|
440
|
+
* read as a product limit and was nothing of the kind — it was the edge's
|
|
441
|
+
* `AbortSignal.timeout` on one forwarded `fetch`, quoted back to customers as
|
|
442
|
+
* policy. Deleting the held request deleted the ceiling with it; it was never
|
|
443
|
+
* tuned, because there was nothing to tune.
|
|
444
|
+
*/
|
|
445
|
+
duration: string;
|
|
446
|
+
}
|
|
447
|
+
export interface AgentRequest {
|
|
448
|
+
task: string;
|
|
449
|
+
/** Required, not optional. See `AgentCaps`. */
|
|
450
|
+
caps: AgentCaps;
|
|
451
|
+
/** Supplies identity and logged-in state. */
|
|
452
|
+
profile?: string;
|
|
453
|
+
/**
|
|
454
|
+
* When supplied, schema satisfaction *is* the success signal, and
|
|
455
|
+
* {@link AgentResult.data} comes back schema-shaped.
|
|
456
|
+
*
|
|
457
|
+
* Optional, and omitting it no longer costs you the answer: a schema-less run
|
|
458
|
+
* returns the agent's own closing summary in `data` instead. It is still the
|
|
459
|
+
* better input when you need named fields, because a schema is also what the
|
|
460
|
+
* run is judged and held against.
|
|
461
|
+
*/
|
|
462
|
+
schema?: JsonSchema;
|
|
463
|
+
/**
|
|
464
|
+
* Which harness runs this task. Absent means {@link DEFAULT_AGENT_ALIAS}.
|
|
465
|
+
*
|
|
466
|
+
* Replaces a `model?: string` field that documented itself as "`'default'`
|
|
467
|
+
* for the model we host, or a BYOK model id" and was consulted by exactly one
|
|
468
|
+
* function, whose only behaviour was to refuse every value but `'default'`.
|
|
469
|
+
* BYOK was never implemented and there is no route for a customer credential
|
|
470
|
+
* to reach a worker, so the field's whole effect was to publish a choice
|
|
471
|
+
* nobody had.
|
|
472
|
+
*
|
|
473
|
+
* The two names are capabilities, not model tiers: `scout-1` does about an
|
|
474
|
+
* hour of work on the web; `voyager-1` reasons harder over hours or days. See
|
|
475
|
+
* `agent-alias.ts` for what each runs and why the caller is not told.
|
|
476
|
+
*/
|
|
477
|
+
agent?: AgentAlias;
|
|
478
|
+
/**
|
|
479
|
+
* Where to POST this run's terminal event, so nothing has to poll.
|
|
480
|
+
*
|
|
481
|
+
* Held at the EDGE for the life of the run and never handed to the machine
|
|
482
|
+
* running it: a fleet Mac is the least-trusted tier in this system, and a
|
|
483
|
+
* customer's webhook URL is a credential — whoever holds it can post a
|
|
484
|
+
* convincing "your run finished" into whatever the customer built on top of
|
|
485
|
+
* it. `server/src/notify.ts` renders the body, the same builder monitors
|
|
486
|
+
* already use.
|
|
487
|
+
*
|
|
488
|
+
* The field predates the job model by months and was read by NOTHING: it
|
|
489
|
+
* documented itself as "async delivery; the call returns an id immediately"
|
|
490
|
+
* on a route that returned an `AgentResult` fifteen minutes later. It is real
|
|
491
|
+
* now.
|
|
492
|
+
*/
|
|
493
|
+
webhook?: string;
|
|
494
|
+
/**
|
|
495
|
+
* How much to push, and optionally where.
|
|
496
|
+
*
|
|
497
|
+
* The level is the addition: {@link webhook} above says WHERE and has never
|
|
498
|
+
* said HOW MUCH, so the only thing it could ever deliver was the terminal
|
|
499
|
+
* outcome. `notify.level` is what turns one POST at the end into the run's
|
|
500
|
+
* log as it happens — `step` for what the agent did, `debug` for its
|
|
501
|
+
* reasoning too, `silent` for a run that must not push at all even though a
|
|
502
|
+
* webhook is configured.
|
|
503
|
+
*
|
|
504
|
+
* `notify.webhook` overrides {@link webhook} when both are present, being the
|
|
505
|
+
* more specific statement. Neither is forwarded to the machine running the
|
|
506
|
+
* run and neither is ever the machine's to deliver: the push is a projection
|
|
507
|
+
* of durable `run_records` rows made at the CONTROL PLANE, which is what
|
|
508
|
+
* makes a failed POST retryable from the row and a dying browser unable to
|
|
509
|
+
* lose an event. See {@link NOTIFY_KINDS}.
|
|
510
|
+
*/
|
|
511
|
+
notify?: NotifyPolicy;
|
|
512
|
+
exit?: ExitSpec;
|
|
513
|
+
/**
|
|
514
|
+
* Action cache, on by default. Key is URL + instruction + AX-tree hash +
|
|
515
|
+
* options; a hit replays with zero model tokens.
|
|
516
|
+
*/
|
|
517
|
+
cache?: boolean;
|
|
518
|
+
/**
|
|
519
|
+
* Set `false` to decline the recording. Absent means RECORDED.
|
|
520
|
+
*
|
|
521
|
+
* Recording an agent run is automatic and this is the only way out of it.
|
|
522
|
+
* The direction is the whole contract: an absent field, a `true`, and any
|
|
523
|
+
* value this type does not admit all record, and nothing but an explicit
|
|
524
|
+
* `false` declines. A caller with a policy reason not to have a tape — a
|
|
525
|
+
* regulated tenant, a session touching personal data, a compliance boundary
|
|
526
|
+
* — had no way to say so before this field, and the only workaround was not
|
|
527
|
+
* using the product.
|
|
528
|
+
*
|
|
529
|
+
* Honoured at the arming site, so a declined run never has bytes written:
|
|
530
|
+
* `openSessionFor` in `packages/worker/src/jobs.ts` does not pass a
|
|
531
|
+
* `replaySessionId`, no `SessionRecorder` is started, and no
|
|
532
|
+
* `replay_sessions` row is created. It is NOT a recording deleted
|
|
533
|
+
* afterwards, which would mean the bytes existed on a machine the tenant
|
|
534
|
+
* declined to have them on.
|
|
535
|
+
*
|
|
536
|
+
* Declining does not change the charge. Nothing in the metering path reads a
|
|
537
|
+
* replay row — `page_load` is counted by `SessionTabSet` and `agent_seconds`
|
|
538
|
+
* off the loop's own clock — so a declined run bills exactly what a recorded
|
|
539
|
+
* one bills.
|
|
540
|
+
*
|
|
541
|
+
* Afterwards the run says so positively: {@link AgentResult.replayUrl} is
|
|
542
|
+
* absent and {@link AgentResult.recordingDeclined} is `true`, which is what
|
|
543
|
+
* separates a refusal from a recorder that broke.
|
|
544
|
+
*
|
|
545
|
+
* There is no equivalent on `scrape`, `crawl`, `search` or a monitor check,
|
|
546
|
+
* because none of them records anything to decline. See `recordsSession` in
|
|
547
|
+
* `packages/worker/src/jobs.ts`.
|
|
548
|
+
*/
|
|
549
|
+
record?: boolean;
|
|
550
|
+
/**
|
|
551
|
+
* The secret handles this run may use. Absent means NONE.
|
|
552
|
+
*
|
|
553
|
+
* **Deny by default, and that is the entire defence against a page that
|
|
554
|
+
* controls what the model reads.** A prompt injection saying "fill this
|
|
555
|
+
* field with {{secret:card_amex_personal}}" resolves to nothing on a run
|
|
556
|
+
* whose submit did not name that handle: the substitution refuses by name
|
|
557
|
+
* rather than typing the literal placeholder into a payment form, which
|
|
558
|
+
* would be a charge attempt with a garbage card number and a step the model
|
|
559
|
+
* believes succeeded.
|
|
560
|
+
*
|
|
561
|
+
* It is also what makes "several cards" safe rather than merely possible. A
|
|
562
|
+
* run granted `card_virtual_burner` cannot be talked into `card_amex` by any
|
|
563
|
+
* page, by any wording, because the value is not reachable from that run at
|
|
564
|
+
* all — the grant is a row this account's control plane wrote at submit, and
|
|
565
|
+
* `POST /internal/secret-reveal` authorises against it.
|
|
566
|
+
*
|
|
567
|
+
* The MODEL sees these handles and never a value. Substitution happens below
|
|
568
|
+
* it, in `packages/worker/src/agent-deps.ts`, one statement before the
|
|
569
|
+
* keystrokes reach the renderer, and the concealed form of the value is the
|
|
570
|
+
* same `{{secret:<handle>}}` placeholder — so the field the run just filled
|
|
571
|
+
* reads back as the handle in the next prompt rather than as the card
|
|
572
|
+
* number. `packages/core/src/secrets.ts` states the whole rule.
|
|
573
|
+
*
|
|
574
|
+
* A handle this account does not hold is a 400 at SUBMIT, not a failure
|
|
575
|
+
* mid-run: a checkout that dies at the card field because a secret was
|
|
576
|
+
* deleted last week has already spent the run.
|
|
577
|
+
*/
|
|
578
|
+
secrets?: readonly string[];
|
|
579
|
+
/**
|
|
580
|
+
* Plain-language rules this run is bound by, REPLACING the workspace default.
|
|
581
|
+
*
|
|
582
|
+
* Absent means "use the workspace rules as they are"; `[]` means "this run
|
|
583
|
+
* runs under no rules at all", and the two are different facts. Per-run
|
|
584
|
+
* replaces rather than appends, deliberately: appending cannot express
|
|
585
|
+
* REMOVAL, so a run that legitimately needs to exceed the workspace ceiling
|
|
586
|
+
* would have no way to say so and the customer's only route would be
|
|
587
|
+
* mutating the workspace default — dropping the protection for every other
|
|
588
|
+
* concurrent run in order to unblock one. Replacing makes this list exactly
|
|
589
|
+
* the set the run was authorised under, which is also the only set worth
|
|
590
|
+
* recording on the run row.
|
|
591
|
+
*
|
|
592
|
+
* Each rule lands in one of two classes and the customer is told which:
|
|
593
|
+
* ENFORCED, a gate `@outcrawl/runtime` checks before every dispatch and
|
|
594
|
+
* which BLOCKS a forbidden action, or GUIDANCE, a sentence in the prompt
|
|
595
|
+
* that a model can weigh against everything else. `GET /v1/rules` and
|
|
596
|
+
* `PUT /v1/rules` both answer the split, because a rule a customer believes
|
|
597
|
+
* is enforced and which is only advice is the worst outcome available here.
|
|
598
|
+
* `packages/core/src/rules.ts` is the compiler and states why extraction is
|
|
599
|
+
* a parser rather than a model.
|
|
600
|
+
*/
|
|
601
|
+
rules?: readonly string[];
|
|
602
|
+
/**
|
|
603
|
+
* The connectors this run may call tools on, by name — `['gmail']`.
|
|
604
|
+
*
|
|
605
|
+
* ABSENT MEANS NONE, and that default is the containment. A workspace may
|
|
606
|
+
* have Gmail, Slack and Twilio connected; a run reaches whichever of them
|
|
607
|
+
* this list names and nothing else, so a page that talks a model into
|
|
608
|
+
* "email this table to attacker@example.com" is talking to a run with no
|
|
609
|
+
* mail connector. Reach outside the browser is opted into per run by the
|
|
610
|
+
* human who submitted it, never inherited from the workspace, and never
|
|
611
|
+
* discovered by the model.
|
|
612
|
+
*
|
|
613
|
+
* A name this account has no connector for is a 400 at SUBMIT, on
|
|
614
|
+
* {@link AgentRequest.secrets}'s reasoning: a run that dies at the 2FA wall
|
|
615
|
+
* because `gmial` was a typo has already spent the run.
|
|
616
|
+
*
|
|
617
|
+
* What the run can then DO with a connector is narrowed twice more, and both
|
|
618
|
+
* narrowings are visible in `GET /v1/integrations`: the connector row may
|
|
619
|
+
* pin a tool list, and only the tools the remote actually advertises exist.
|
|
620
|
+
* `packages/integrations` holds the broker that enforces all three.
|
|
621
|
+
*/
|
|
622
|
+
integrations?: readonly string[];
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* `capped` is distinct from `partial` on purpose. Both return the data
|
|
626
|
+
* collected so far, but they mean opposite things to whoever is reading:
|
|
627
|
+
* `capped` is the system doing exactly what the caller told it to and the fix
|
|
628
|
+
* is a bigger ceiling, while `partial` is the run genuinely failing to finish
|
|
629
|
+
* and the fix is somewhere in the task or the site. Collapsing them would hide
|
|
630
|
+
* the one distinction a customer acts on, and `SessionOutcome` already carries
|
|
631
|
+
* `capped` for the same reason.
|
|
632
|
+
*/
|
|
633
|
+
export type AgentStatus = 'completed' | 'partial' | 'capped' | 'failed';
|
|
634
|
+
/**
|
|
635
|
+
* Why the run stopped. `max-*` are the caps firing; a capped run is a normal
|
|
636
|
+
* outcome, not an incident.
|
|
637
|
+
*
|
|
638
|
+
* `gave-up` is the MODEL's verdict and `error` is OURS, and they are separate
|
|
639
|
+
* members for the same reason {@link AgentStatus} keeps `capped` apart from
|
|
640
|
+
* `failed`: collapsing them hides the one distinction a customer acts on. A run
|
|
641
|
+
* that ends `gave-up` chose `{"type":"fail","reason":"..."}` — an action the
|
|
642
|
+
* agent loop advertises on every prompt — because the task could not be done on
|
|
643
|
+
* the page in front of it. A run that ends `error` hit something that went
|
|
644
|
+
* wrong on our side: a lost action-lock, a reference naming no node, a step
|
|
645
|
+
* that threw. One means look at the task, the other means look at us, and while
|
|
646
|
+
* they shared a string neither a dashboard nor a retry loop could tell which
|
|
647
|
+
* had happened — so a caller retried, forever, something that will never
|
|
648
|
+
* succeed.
|
|
649
|
+
*
|
|
650
|
+
* Neither is a status: {@link AgentStatus} still answers "did the caller get
|
|
651
|
+
* what they asked for" (`failed` with no data, `partial` with some), and this
|
|
652
|
+
* answers why not.
|
|
653
|
+
*
|
|
654
|
+
* `cancelled` and `irreversible` are the two that are neither ours nor the
|
|
655
|
+
* model's. `cancelled` is the CUSTOMER's: somebody called
|
|
656
|
+
* `POST /v1/agent/{id}/cancel` and the run settled where it stood. It is not
|
|
657
|
+
* `gave-up` (the model never said anything) and not `error` (nothing of ours
|
|
658
|
+
* broke), and a retry loop must be able to tell it from both — retrying a run
|
|
659
|
+
* a human just stopped is the one retry that is certainly wrong.
|
|
660
|
+
*
|
|
661
|
+
* `irreversible` is the HARNESS refusing rather than failing. A run is
|
|
662
|
+
* resumable until its first irreversible action — payment submitted, order
|
|
663
|
+
* placed, message sent, file handed to a third party, account created — and
|
|
664
|
+
* permanently un-resumable after it, because nothing can distinguish "the order
|
|
665
|
+
* went through and the confirmation page died with the browser" from "the order
|
|
666
|
+
* never went through". A resume past that point REPORTS what the run knew and
|
|
667
|
+
* does not act: no browser is opened, no step is dispatched, no credit is
|
|
668
|
+
* spent. Not `error` (nothing of ours broke) and not `gave-up` (the model never
|
|
669
|
+
* said anything) — the two it would otherwise be collapsed into, and a caller's
|
|
670
|
+
* retry loop must be able to tell "never retry this" from both.
|
|
671
|
+
*/
|
|
672
|
+
export type AgentStopReason = 'schema-satisfied' | 'task-complete' | 'max-steps' | 'max-budget' | 'max-duration' | 'loop-detected' | 'gave-up' | 'irreversible' | 'cancelled' | 'error';
|
|
673
|
+
/** An action a page could take, resolved but not executed. Also the cache unit. */
|
|
674
|
+
export interface ObservedAction {
|
|
675
|
+
ref: NodeRef;
|
|
676
|
+
description: string;
|
|
677
|
+
/** e.g. "click", "fill", "selectOption". */
|
|
678
|
+
method: string;
|
|
679
|
+
args?: readonly string[];
|
|
680
|
+
/** Heal path: survives a node being genuinely recreated, which the ref cannot. */
|
|
681
|
+
xpath: string;
|
|
682
|
+
}
|
|
683
|
+
export interface AgentStep {
|
|
684
|
+
/** 0-based. */
|
|
685
|
+
index: number;
|
|
686
|
+
action: ObservedAction;
|
|
687
|
+
/** The model's stated reason for the action, when the model produced one. */
|
|
688
|
+
reasoning?: string;
|
|
689
|
+
at: Timestamp;
|
|
690
|
+
/**
|
|
691
|
+
* The tab this step ran on, on a multi-tab run. Absent means single-tab.
|
|
692
|
+
*
|
|
693
|
+
* On the wire because two consumers outside the loop cannot do their job
|
|
694
|
+
* without it. A replay player has nothing else to tell tab B's mutations from
|
|
695
|
+
* tab A's — every tab's renderer claims main-frame ordinal 0, so an
|
|
696
|
+
* un-namespaced recording applies one tab's changes to another's tree and
|
|
697
|
+
* plays back a run that never happened. And a scorer reading these frames
|
|
698
|
+
* infers which origin a step acted on from the last URL it saw; once focus can
|
|
699
|
+
* move between tabs, that inference is wrong without this field, and wrong
|
|
700
|
+
* quietly, which is worse than absent.
|
|
701
|
+
*/
|
|
702
|
+
tab?: string;
|
|
703
|
+
/**
|
|
704
|
+
* The sub-goal this step was working, on a run that stated a plan. Absent
|
|
705
|
+
* when the run has no plan, or for the steps before one was stated.
|
|
706
|
+
*
|
|
707
|
+
* On the wire for the reason `tab` above is: a consumer scoring a run's
|
|
708
|
+
* progress cannot attribute a step to an outcome without it, and the runtime
|
|
709
|
+
* recorded it on `StepTrace.goal` all along while this projection dropped it.
|
|
710
|
+
*/
|
|
711
|
+
goal?: string;
|
|
712
|
+
usage: Usage;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* A sub-goal of a run, and what became of it.
|
|
716
|
+
*
|
|
717
|
+
* On the wire because a five-part task is five completions and one report, and
|
|
718
|
+
* `status` carries only the report: a run that did four of five errands and a
|
|
719
|
+
* run that did one are both `partial`, and the difference is the thing the
|
|
720
|
+
* customer is paying for.
|
|
721
|
+
*
|
|
722
|
+
* It is also the only way anything outside the loop can measure long-horizon
|
|
723
|
+
* behaviour. The runtime held all of this on `AgentResult.goals` and
|
|
724
|
+
* `StepTrace.goal` and the projection dropped both, so a plan-driven run and a
|
|
725
|
+
* planless one were byte-identical on the wire — which meant the harness that
|
|
726
|
+
* exists to score long-horizon runs scored them the same, and an absence read
|
|
727
|
+
* off that output could not distinguish "no plan was stated" from "plans do not
|
|
728
|
+
* appear here". One such absence was reported as evidence during this work.
|
|
729
|
+
*
|
|
730
|
+
* Optional, and absent rather than `[]` when the run never stated a plan: an
|
|
731
|
+
* empty array reads as "asked and answered nothing", and absent reads as "this
|
|
732
|
+
* run had no plan", which is the true statement.
|
|
733
|
+
*/
|
|
734
|
+
export interface AgentGoal {
|
|
735
|
+
id: string;
|
|
736
|
+
goal: string;
|
|
737
|
+
/** Origin hint, when the plan named one. */
|
|
738
|
+
site?: string;
|
|
739
|
+
status: 'pending' | 'active' | 'done' | 'failed';
|
|
740
|
+
/** What closed it: the confirmation on `done`, the reason on `failed`. */
|
|
741
|
+
summary?: string;
|
|
742
|
+
/** The step it closed on. Absent while still open. */
|
|
743
|
+
closedAtStep?: number;
|
|
744
|
+
}
|
|
745
|
+
export interface AgentResult {
|
|
746
|
+
id: string;
|
|
747
|
+
/** Published success rates are 42–71%, so `partial` is normal, not an error. */
|
|
748
|
+
status: AgentStatus;
|
|
749
|
+
/**
|
|
750
|
+
* The run's answer. Partial results are returned, never discarded.
|
|
751
|
+
*
|
|
752
|
+
* WITH a schema this is schema-shaped, and a partially satisfied schema still
|
|
753
|
+
* comes back. WITHOUT one it is the agent's closing summary of what it found,
|
|
754
|
+
* so a question asked without a schema is still answered.
|
|
755
|
+
*
|
|
756
|
+
* OPTIONAL, and absence is not an error: it means the run had nothing to
|
|
757
|
+
* report — "click the unsubscribe link" asks no question — and such a run is
|
|
758
|
+
* still `completed`. Absent rather than `null` deliberately: `null` is a
|
|
759
|
+
* value a schema'd extraction can legitimately produce, and overloading it
|
|
760
|
+
* would make "the answer is null" and "there was no answer" the same reading.
|
|
761
|
+
*
|
|
762
|
+
* This field was declared required while the API shipped responses without
|
|
763
|
+
* it, because nothing populated it on a schema-less run and `JSON.stringify`
|
|
764
|
+
* erases an `undefined` value.
|
|
765
|
+
*/
|
|
766
|
+
data?: unknown;
|
|
767
|
+
usage: Usage;
|
|
768
|
+
/**
|
|
769
|
+
* How long the run took, from the loop's own monotonic clock.
|
|
770
|
+
*
|
|
771
|
+
* **This is a billed quantity, and that is why it is on the wire.** Agent is
|
|
772
|
+
* sold at +5 credits per minute; the API meters `agent_seconds` from exactly
|
|
773
|
+
* this number, and billing a customer per minute while refusing to tell them
|
|
774
|
+
* the minutes is indefensible. It is also the reading `AgentCaps.duration`
|
|
775
|
+
* was enforced against, so a `capped` run with `stopReason: 'max-duration'`
|
|
776
|
+
* can be checked against the ceiling that stopped it.
|
|
777
|
+
*
|
|
778
|
+
* Not optional. `SessionSummary.durationMs` is required for the same reason,
|
|
779
|
+
* and this field existed on the runtime's own result all along —
|
|
780
|
+
* `toWireResult` simply dropped it, which is how the published per-minute
|
|
781
|
+
* price came to have no minute anywhere on the customer's side of the wire.
|
|
782
|
+
*/
|
|
783
|
+
durationMs: number;
|
|
784
|
+
/**
|
|
785
|
+
* Watch exactly what the money bought. This is how a charge is disputed.
|
|
786
|
+
*
|
|
787
|
+
* OPTIONAL, and the absence is a statement rather than a gap: this run was
|
|
788
|
+
* not recorded anywhere durable. It was required until now and it lied —
|
|
789
|
+
* every agent run this product completed returned a link of the shape
|
|
790
|
+
* `https://outcrawl.ai/replay/r/<id>`, and `replay_sessions` held zero rows
|
|
791
|
+
* from the day the table was created, because a fleet Mac has no database
|
|
792
|
+
* credential and so bound an in-memory replay store that was freed when the
|
|
793
|
+
* process exited.
|
|
794
|
+
*
|
|
795
|
+
* A host that cannot honour a link now answers `null` from
|
|
796
|
+
* `ReplayStore.replayUrl` and this field is spread away — see
|
|
797
|
+
* `packages/worker/src/main.ts` `replayStore()`, whose memory branch says why
|
|
798
|
+
* refusing to BOOT over a missing replay credential would be an outage caused
|
|
799
|
+
* by replay. An absent field reads as "this run was not recorded"; a dead
|
|
800
|
+
* link reads as "your product is broken", and a customer chasing the second
|
|
801
|
+
* one opens a support ticket about our routing.
|
|
802
|
+
*
|
|
803
|
+
* `MonitorCheck.replayUrl` above has been optional since it was written, for
|
|
804
|
+
* exactly this reason and left deliberately unpopulated. That is the shape
|
|
805
|
+
* this codebase already chose once.
|
|
806
|
+
*
|
|
807
|
+
* PRESENT means a recording EXISTS, including one still sealing. The link is
|
|
808
|
+
* gated on `sessions.replay_key`, which is written the moment the recorder
|
|
809
|
+
* arms and never on `replay_sessions.state`, so a run whose bytes are still
|
|
810
|
+
* being flushed carries its url — that is what makes a live follow work.
|
|
811
|
+
* ABSENT therefore means no recording, and {@link recordingDeclined} is what
|
|
812
|
+
* says which of the two ways it came to have none.
|
|
813
|
+
*/
|
|
814
|
+
replayUrl?: string;
|
|
815
|
+
/**
|
|
816
|
+
* The CALLER declined the recording, with `AgentRequest.record: false`.
|
|
817
|
+
*
|
|
818
|
+
* Present and `true` only on a refusal; absent otherwise. It exists because
|
|
819
|
+
* an absent {@link replayUrl} had two causes and no way to tell them apart:
|
|
820
|
+
* a tenant who refused a tape, and a recorder that was asked for one and
|
|
821
|
+
* could not deliver — a sink that declined, a browser that would not arm, a
|
|
822
|
+
* host booted with no replay credential. Those are a satisfied request and
|
|
823
|
+
* an incident, and a customer disputing a charge, an auditor checking a
|
|
824
|
+
* compliance boundary and an operator reading a session with no tape all
|
|
825
|
+
* need the difference. This repo has shipped that same silence before, where
|
|
826
|
+
* a `replayUrl` derived from the row id alone made "recorded" and "never
|
|
827
|
+
* recorded" identical on the wire.
|
|
828
|
+
*
|
|
829
|
+
* So the three states are: `replayUrl` present — there is a recording;
|
|
830
|
+
* `replayUrl` absent with this `true` — the caller refused one and no bytes
|
|
831
|
+
* were ever written; `replayUrl` absent with this field absent — one was
|
|
832
|
+
* wanted and not obtained, which is ours to fix.
|
|
833
|
+
*
|
|
834
|
+
* TYPED `true` AND NOT `boolean`. A `recordingDeclined: false` on the wire
|
|
835
|
+
* would be a claim about a decision on every result that ever returns, where
|
|
836
|
+
* the absence already says everything — the argument {@link replayUrl} makes
|
|
837
|
+
* for being spread rather than assigned. It also means the default cannot be
|
|
838
|
+
* spelled: there is no `false` to write, so nothing can express "declined
|
|
839
|
+
* unless told otherwise".
|
|
840
|
+
*/
|
|
841
|
+
recordingDeclined?: true;
|
|
842
|
+
stepsUsed: number;
|
|
843
|
+
/**
|
|
844
|
+
* Navigations committed over the run. A billed quantity, metered as
|
|
845
|
+
* `page_load` at the same per-page rate a scrape pays.
|
|
846
|
+
*
|
|
847
|
+
* **Required, with no `?`.** The rate card sells agent minutes on top of the
|
|
848
|
+
* run's pages and browser time, and with no page count on this object the API
|
|
849
|
+
* metered a page residual of zero against runs that demonstrably navigated —
|
|
850
|
+
* the same defect `durationMs` above had, and an optional field would have
|
|
851
|
+
* reproduced it silently, because zero pages reads as a legal invoice rather
|
|
852
|
+
* than a missing one.
|
|
853
|
+
*
|
|
854
|
+
* Counted in `@outcrawl/runtime` from observed per-tab URL changes, so a click
|
|
855
|
+
* that navigates counts and switching back to an already-loaded tab does not.
|
|
856
|
+
* A revisit counts again: five sites with one revisit is six pages, which is
|
|
857
|
+
* the only reading that does not price a multi-site agent run below six
|
|
858
|
+
* scrapes of the same six pages.
|
|
859
|
+
*/
|
|
860
|
+
pages: number;
|
|
861
|
+
stopReason: AgentStopReason;
|
|
862
|
+
/** The plan and its outcomes. Absent when the run never stated one. See {@link AgentGoal}. */
|
|
863
|
+
goals?: readonly AgentGoal[];
|
|
864
|
+
/** Present when status is `failed`. */
|
|
865
|
+
error?: string;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Where a run is, at the instant it was read.
|
|
869
|
+
*
|
|
870
|
+
* Exactly {@link AgentStatus} plus the three states a job has and a request
|
|
871
|
+
* never did. `queued` is accepted-and-not-yet-placed; `running` is on a
|
|
872
|
+
* machine; `paused` is on a machine and WAITING ON A HUMAN. Everything else is
|
|
873
|
+
* terminal and is the same vocabulary the finished result reports, so a poller
|
|
874
|
+
* and a webhook consumer read one union rather than two.
|
|
875
|
+
*
|
|
876
|
+
* **There is no `cancelled` here, deliberately.** A cancel is not an outcome:
|
|
877
|
+
* a run that had already found the answer when somebody stopped it is
|
|
878
|
+
* `completed` and its data is real. What a human asked for is
|
|
879
|
+
* {@link AgentRun.cancelRequestedAt} — an instant, written once — and why the
|
|
880
|
+
* run stopped is `stopReason: 'cancelled'`. Folding the request into the status
|
|
881
|
+
* would throw away a delivered answer to record a button press.
|
|
882
|
+
*
|
|
883
|
+
* **`paused` is ONE state for two doors, and it is not terminal.** A run parks
|
|
884
|
+
* either because a human took the wheel (`POST /v1/agent/{id}/control`) or
|
|
885
|
+
* because the agent asked a question (`ask`, answered by
|
|
886
|
+
* `POST /v1/agent/{id}/answer`), and which of the two it was lives on
|
|
887
|
+
* {@link AgentRun.pause} rather than in a second status: everything else about
|
|
888
|
+
* the state is identical, and a second status would double every predicate in
|
|
889
|
+
* the system in order to record which button was pressed. It is an OPEN state
|
|
890
|
+
* on the same terms as `queued` and `running` — no `endedAt`, no `stopReason`
|
|
891
|
+
* — because a park always has an exit: the answer, the handback, or the park's
|
|
892
|
+
* own deadline, after which the run resumes and is told nobody replied. A run
|
|
893
|
+
* cannot end `paused`, so no consumer needs to treat it as an outcome.
|
|
894
|
+
*/
|
|
895
|
+
export type AgentRunStatus = 'queued' | 'running' | 'paused' | AgentStatus;
|
|
896
|
+
/**
|
|
897
|
+
* Whether this run can still do anything, as ONE predicate.
|
|
898
|
+
*
|
|
899
|
+
* It was spelled `status !== 'queued' && status !== 'running'` in three places
|
|
900
|
+
* — the event stream's `done` frame, the upload route's refusal, and the SDK's
|
|
901
|
+
* `settled()` poll — and every one of them means "has this run finished". A
|
|
902
|
+
* fourth state that three of those copies had never heard of is how a PARKED
|
|
903
|
+
* run comes to report `done: true` on its event stream, be refused an upload
|
|
904
|
+
* for having "ended", and settle an SDK poll with a run that is still waiting
|
|
905
|
+
* for the answer that poll was about to deliver.
|
|
906
|
+
*
|
|
907
|
+
* `paused` counts as OPEN, which is the whole reason this exists: a park is a
|
|
908
|
+
* run in the middle of its life, holding a warm browser, that is going to take
|
|
909
|
+
* more steps.
|
|
910
|
+
*/
|
|
911
|
+
export declare function isRunOpen(status: AgentRunStatus): boolean;
|
|
912
|
+
/**
|
|
913
|
+
* What a run appended, and why.
|
|
914
|
+
*
|
|
915
|
+
* ONE log, one sequence space. `result` is the deliverable; the rest is how the
|
|
916
|
+
* run got there. They share a table and a `seq` because the alternative —
|
|
917
|
+
* results in one stream and progress in another — needs the two reconciled in
|
|
918
|
+
* front of a customer, and a step IS a record of what happened.
|
|
919
|
+
*
|
|
920
|
+
* `irreversible` is the one a resume reads: past it a run may report and may
|
|
921
|
+
* not act. See {@link AgentStopReason}. `question` is the one a PERSON reads:
|
|
922
|
+
* it is the whole discovery path for a parked run, which is why a question is
|
|
923
|
+
* a durable record rather than a field on the row — a customer who was not
|
|
924
|
+
* watching finds it in the same log they read for everything else, and it is
|
|
925
|
+
* pushed to their webhook by the same projection.
|
|
926
|
+
*/
|
|
927
|
+
export declare const AGENT_RECORD_KINDS: readonly ["result", "finding", "ruled-out", "goal", "step", "irreversible", "question"];
|
|
928
|
+
export type AgentRecordKind = (typeof AGENT_RECORD_KINDS)[number];
|
|
929
|
+
/**
|
|
930
|
+
* How much of the log a subscriber wants.
|
|
931
|
+
*
|
|
932
|
+
* Three levels and not a kind list, because the caller who wants "just tell me
|
|
933
|
+
* what is happening" should not have to know the vocabulary — and because a
|
|
934
|
+
* level is stable across kinds we add later, where a hand-passed list silently
|
|
935
|
+
* stops including them.
|
|
936
|
+
*/
|
|
937
|
+
export declare const AGENT_EVENT_LEVELS: readonly ["status", "step", "debug"];
|
|
938
|
+
export type AgentEventLevel = (typeof AGENT_EVENT_LEVELS)[number];
|
|
939
|
+
/** The default when `level` is not asked for: progress without the reasoning. */
|
|
940
|
+
export declare const DEFAULT_AGENT_EVENT_LEVEL: AgentEventLevel;
|
|
941
|
+
/**
|
|
942
|
+
* Which kinds each level carries. Declared HERE and read by both ends, because
|
|
943
|
+
* the edge decides what to select and the customer decides what to ask for, and
|
|
944
|
+
* a level that means two different things at those two places is a stream with
|
|
945
|
+
* holes in it that nothing reports.
|
|
946
|
+
*/
|
|
947
|
+
export declare const AGENT_EVENT_KINDS: Readonly<Record<AgentEventLevel, readonly AgentRecordKind[]>>;
|
|
948
|
+
/**
|
|
949
|
+
* How much of a run is PUSHED to a customer's endpoint, as against how much a
|
|
950
|
+
* subscriber READS.
|
|
951
|
+
*
|
|
952
|
+
* ── Why this is not `AgentEventLevel`, and what is shared anyway ─────────────
|
|
953
|
+
*
|
|
954
|
+
* {@link AGENT_EVENT_LEVELS} is `status | step | debug` and spells exactly TWO
|
|
955
|
+
* of the four names below. `silent` has no meaning for a reader — a subscriber
|
|
956
|
+
* asking for nothing simply does not subscribe — and `error` has no meaning for
|
|
957
|
+
* a cursor tail, which is a query and not a policy. Going the other way,
|
|
958
|
+
* `status` is not a delivery level: it selects three kinds, and what a customer
|
|
959
|
+
* wants pushed at the quiet end is not three kinds, it is the ONE terminal fact
|
|
960
|
+
* that the run is over.
|
|
961
|
+
*
|
|
962
|
+
* So this is a second list of NAMES. It is deliberately not a second list of
|
|
963
|
+
* KINDS: {@link NOTIFY_KINDS} takes the `step` and `debug` sets by REFERENCE
|
|
964
|
+
* from {@link AGENT_EVENT_KINDS}, so a kind added to what `step` carries when
|
|
965
|
+
* read is carried by what `step` pushes in the same edit. Two overlapping
|
|
966
|
+
* vocabularies is where a drift bug lives; the overlap is therefore not
|
|
967
|
+
* retyped, it is shared.
|
|
968
|
+
*/
|
|
969
|
+
export declare const NOTIFY_LEVELS: readonly ["silent", "error", "step", "debug"];
|
|
970
|
+
export type NotifyLevel = (typeof NOTIFY_LEVELS)[number];
|
|
971
|
+
export declare function isNotifyLevel(value: unknown): value is NotifyLevel;
|
|
972
|
+
/**
|
|
973
|
+
* The default when a run names no policy, and it is `error` for a compatibility
|
|
974
|
+
* reason rather than a taste one: {@link AgentRequest.webhook} already exists
|
|
975
|
+
* and a bare one has always meant "tell me when it is over". A field that
|
|
976
|
+
* already exists must not change meaning under the callers who already set it.
|
|
977
|
+
*/
|
|
978
|
+
export declare const DEFAULT_NOTIFY_LEVEL: NotifyLevel;
|
|
979
|
+
/**
|
|
980
|
+
* Which record kinds each level pushes AS THEY BECOME DURABLE.
|
|
981
|
+
*
|
|
982
|
+
* `silent` and `error` push none: the difference between those two is the
|
|
983
|
+
* terminal outcome, which is not a record — see {@link pushesTerminal}.
|
|
984
|
+
*/
|
|
985
|
+
export declare const NOTIFY_KINDS: Readonly<Record<NotifyLevel, readonly AgentRecordKind[]>>;
|
|
986
|
+
/**
|
|
987
|
+
* Whether the run's terminal outcome is pushed at this level.
|
|
988
|
+
*
|
|
989
|
+
* Everything but `silent`, and `error` in particular. A webhook that is silent
|
|
990
|
+
* on success was the other candidate reading of that word and it is refused
|
|
991
|
+
* here: it makes a working endpoint indistinguishable from a broken one, so the
|
|
992
|
+
* customer cannot tell "still running" from "finished fine" — which is the
|
|
993
|
+
* failing-state-renders-as-the-passing-state defect this whole field exists to
|
|
994
|
+
* remove. `error` means "one message, and what I act on is its `status`".
|
|
995
|
+
*/
|
|
996
|
+
export declare function pushesTerminal(level: NotifyLevel): boolean;
|
|
997
|
+
/**
|
|
998
|
+
* Where a run's events go, and how many of them.
|
|
999
|
+
*
|
|
1000
|
+
* `level` is REQUIRED, so `notify: {}` is a 400 rather than a policy nobody
|
|
1001
|
+
* chose — the same rule {@link AgentCaps} follows and the same rule
|
|
1002
|
+
* `mcpTool: string | null` follows in the registry. `webhook` is optional
|
|
1003
|
+
* because {@link AgentRequest.webhook} may already carry it; when both are
|
|
1004
|
+
* present this one wins, being the more specific statement.
|
|
1005
|
+
*/
|
|
1006
|
+
export interface NotifyPolicy {
|
|
1007
|
+
level: NotifyLevel;
|
|
1008
|
+
webhook?: string;
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* One appended row.
|
|
1012
|
+
*
|
|
1013
|
+
* `seq` is allocated by the store, is gap-free per run and is the cursor: a
|
|
1014
|
+
* client that has seen `seq` asks for everything `after` it and cannot miss a
|
|
1015
|
+
* row, however many machines, retries or reconnections happened in between.
|
|
1016
|
+
* That property is the whole reason a killed browser does not cost a customer
|
|
1017
|
+
* their output.
|
|
1018
|
+
*/
|
|
1019
|
+
export interface AgentRecord {
|
|
1020
|
+
seq: number;
|
|
1021
|
+
kind: AgentRecordKind;
|
|
1022
|
+
at: Timestamp;
|
|
1023
|
+
/**
|
|
1024
|
+
* The payload, shaped by `kind`: the extracted record for `result`, the
|
|
1025
|
+
* {@link AgentStep} for `step`, the {@link AgentGoal} for `goal`, and a
|
|
1026
|
+
* stated sentence for `finding`, `ruled-out` and `irreversible`.
|
|
1027
|
+
*/
|
|
1028
|
+
value: unknown;
|
|
1029
|
+
/** The step this was produced at, when a step produced it. */
|
|
1030
|
+
step?: number;
|
|
1031
|
+
/** The sub-goal it belongs to, when the run had stated a plan. */
|
|
1032
|
+
goal?: string;
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* One run, as `GET /v1/agent/{id}` answers it: status, plan, findings so far,
|
|
1036
|
+
* cost so far.
|
|
1037
|
+
*
|
|
1038
|
+
* Every quantity on it is CURRENT rather than final — `usage`, `credits`,
|
|
1039
|
+
* `stepsUsed`, `pages` and `records` all move while the run is live — because
|
|
1040
|
+
* every run bills for work performed and a customer must be able to watch the
|
|
1041
|
+
* meter rather than discover it.
|
|
1042
|
+
*/
|
|
1043
|
+
export interface AgentRun {
|
|
1044
|
+
id: string;
|
|
1045
|
+
status: AgentRunStatus;
|
|
1046
|
+
task: string;
|
|
1047
|
+
/** The harness the run was placed on. Never the model behind it. */
|
|
1048
|
+
agent: AgentAlias;
|
|
1049
|
+
caps: AgentCaps;
|
|
1050
|
+
createdAt: Timestamp;
|
|
1051
|
+
/** Absent while `queued`: nothing has picked the run up yet. */
|
|
1052
|
+
startedAt?: Timestamp;
|
|
1053
|
+
/** Absent until terminal. */
|
|
1054
|
+
endedAt?: Timestamp;
|
|
1055
|
+
/** So far, from the loop's own monotonic clock. The billed quantity. */
|
|
1056
|
+
durationMs: number;
|
|
1057
|
+
stepsUsed: number;
|
|
1058
|
+
pages: number;
|
|
1059
|
+
/** Records of kind `result` appended so far. The count beside the output. */
|
|
1060
|
+
records: number;
|
|
1061
|
+
/** The high-water `seq` over EVERY kind — where to resume the event tail. */
|
|
1062
|
+
lastEventSeq: number;
|
|
1063
|
+
/** The plan as it currently stands. Absent when the run never stated one. */
|
|
1064
|
+
goals?: readonly AgentGoal[];
|
|
1065
|
+
usage: Usage;
|
|
1066
|
+
/** Credits spent so far, as a decimal string. What the bill is argued from. */
|
|
1067
|
+
credits: string;
|
|
1068
|
+
/**
|
|
1069
|
+
* The answer, inline, once there is one and it is small enough to be inline.
|
|
1070
|
+
* Above {@link INLINE_RESULT_BYTES} it is absent and {@link outputUrl} plus
|
|
1071
|
+
* {@link records} is the answer — asking for one title must never paginate,
|
|
1072
|
+
* and returning a megabyte on a status poll must never happen.
|
|
1073
|
+
*/
|
|
1074
|
+
data?: unknown;
|
|
1075
|
+
/** Where the whole output is fetchable, when it did not fit inline. */
|
|
1076
|
+
outputUrl?: string;
|
|
1077
|
+
stopReason?: AgentStopReason;
|
|
1078
|
+
replayUrl?: string;
|
|
1079
|
+
recordingDeclined?: true;
|
|
1080
|
+
/** Present when `status` is `failed`. */
|
|
1081
|
+
error?: string;
|
|
1082
|
+
/**
|
|
1083
|
+
* When a human asked for this run to stop. Write-once: a second cancel cannot
|
|
1084
|
+
* move the instant, so "who stopped it and when" survives a retried request.
|
|
1085
|
+
* Present while the run is still settling as well as after — a cancel is
|
|
1086
|
+
* asked for, not applied instantly.
|
|
1087
|
+
*/
|
|
1088
|
+
cancelRequestedAt?: Timestamp;
|
|
1089
|
+
/**
|
|
1090
|
+
* WHY THIS RUN IS PARKED, present exactly while `status` is `paused`.
|
|
1091
|
+
*
|
|
1092
|
+
* The one place the two doors are told apart. Everything else about a park
|
|
1093
|
+
* is identical — the row, the warm browser, the resume — so `reason` is a
|
|
1094
|
+
* field here rather than a second status, and a consumer that only cares
|
|
1095
|
+
* that the run is waiting can read `status` and stop.
|
|
1096
|
+
*
|
|
1097
|
+
* `expiresAt` is on it because a park a customer cannot see the end of is a
|
|
1098
|
+
* park they cannot plan around: it is when the run gives up waiting and
|
|
1099
|
+
* carries on, not when the run dies.
|
|
1100
|
+
*/
|
|
1101
|
+
pause?: AgentPause;
|
|
1102
|
+
/**
|
|
1103
|
+
* When a human asked for the wheel, still unanswered by the machine.
|
|
1104
|
+
*
|
|
1105
|
+
* The same shape and the same reason as {@link cancelRequestedAt}: a park is
|
|
1106
|
+
* ASKED FOR at the edge and applied by the machine on its next report, so
|
|
1107
|
+
* there is an instant where the request exists and the park does not. Naming
|
|
1108
|
+
* that instant is what lets `POST /control` be idempotent and lets a
|
|
1109
|
+
* customer see that their request landed before the run has noticed it.
|
|
1110
|
+
*/
|
|
1111
|
+
controlRequestedAt?: Timestamp;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* A park, as every surface reads it.
|
|
1115
|
+
*
|
|
1116
|
+
* ## Why a parked run is a ROW and not a held connection
|
|
1117
|
+
*
|
|
1118
|
+
* Because the thing being waited on is a person, and people are slower than
|
|
1119
|
+
* every timeout in this system. A park expressed as an open request would tie
|
|
1120
|
+
* the answer to one socket, one edge isolate and one machine staying up; a
|
|
1121
|
+
* park expressed as a row is discoverable by a customer who was asleep when it
|
|
1122
|
+
* happened, survives the process, and is answerable from a different device an
|
|
1123
|
+
* hour later. It is the same argument the job model already made for a run.
|
|
1124
|
+
*/
|
|
1125
|
+
export interface AgentPause {
|
|
1126
|
+
/**
|
|
1127
|
+
* Which door the run went through.
|
|
1128
|
+
*
|
|
1129
|
+
* `control` — a human asked for the wheel; the browser is live and drivable
|
|
1130
|
+
* at {@link viewerUrl}, and the run resumes when they hand it back.
|
|
1131
|
+
* `question` — the agent asked something only the customer can answer, on
|
|
1132
|
+
* `POST /v1/agent/{id}/answer`.
|
|
1133
|
+
*/
|
|
1134
|
+
reason: 'control' | 'question';
|
|
1135
|
+
at: Timestamp;
|
|
1136
|
+
/**
|
|
1137
|
+
* When the run stops waiting and carries on — NOT when it dies.
|
|
1138
|
+
*
|
|
1139
|
+
* A park has to expire, because it holds a warm browser and a machine slot
|
|
1140
|
+
* for somebody who may never reply. What expiry does is resume the run: an
|
|
1141
|
+
* unanswered question is answered "nobody replied" and the model decides,
|
|
1142
|
+
* and an unreturned wheel is taken back. So there is no state here a run can
|
|
1143
|
+
* be stuck in, and no stop reason that means "abandoned".
|
|
1144
|
+
*/
|
|
1145
|
+
expiresAt: Timestamp;
|
|
1146
|
+
/** What the agent asked. Present exactly when `reason` is `question`. */
|
|
1147
|
+
question?: string;
|
|
1148
|
+
/** The replies the agent offered. Advisory: any answer is accepted. */
|
|
1149
|
+
options?: readonly string[];
|
|
1150
|
+
/** Who is driving. Present exactly when `reason` is `control`. */
|
|
1151
|
+
driver?: string;
|
|
1152
|
+
/**
|
|
1153
|
+
* Where a human goes to watch and drive.
|
|
1154
|
+
*
|
|
1155
|
+
* The durable `/play/{sessionId}` address and never a short-lived signed
|
|
1156
|
+
* token: this URL is put in front of a person, who will paste it into a chat
|
|
1157
|
+
* and open it twenty minutes later.
|
|
1158
|
+
*/
|
|
1159
|
+
viewerUrl?: string;
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* `POST /v1/agent/{id}/control` — take the wheel, or hand it back.
|
|
1163
|
+
*
|
|
1164
|
+
* ONE route and not two, because "take" and "release" are the same
|
|
1165
|
+
* transition read in opposite directions and a customer holding the wheel
|
|
1166
|
+
* needs exactly one place to look. `action` is required for the reason every
|
|
1167
|
+
* required-and-explicit field in this file is: a default here would be a
|
|
1168
|
+
* guess about whether a person wants control, and both guesses are bad.
|
|
1169
|
+
*/
|
|
1170
|
+
export interface AgentControlRequest {
|
|
1171
|
+
action: 'take' | 'release';
|
|
1172
|
+
/**
|
|
1173
|
+
* Who is driving, for the audit trail and for the refusal message a second
|
|
1174
|
+
* driver gets. Defaulted to the calling API key's account when absent,
|
|
1175
|
+
* because the honest answer is never nobody.
|
|
1176
|
+
*/
|
|
1177
|
+
who?: string;
|
|
1178
|
+
}
|
|
1179
|
+
export declare const AGENT_CONTROL_FIELDS: readonly ["action", "who"];
|
|
1180
|
+
/**
|
|
1181
|
+
* `POST /v1/agent/{id}/answer` — the reply to a question a run parked on.
|
|
1182
|
+
*
|
|
1183
|
+
* Free text, and deliberately not constrained to {@link AgentPause.options}: a
|
|
1184
|
+
* human who was offered two choices and needs a third has information the
|
|
1185
|
+
* model did not have, and the one seam that exists to bring a person's
|
|
1186
|
+
* judgment into a run must not refuse the judgment.
|
|
1187
|
+
*/
|
|
1188
|
+
export interface AgentAnswerRequest {
|
|
1189
|
+
answer: string;
|
|
1190
|
+
answeredBy?: string;
|
|
1191
|
+
}
|
|
1192
|
+
export declare const AGENT_ANSWER_FIELDS: readonly ["answer", "answeredBy"];
|
|
1193
|
+
/**
|
|
1194
|
+
* Checks a control request. Whole-request validators live here, beside the
|
|
1195
|
+
* types and beside `validateAgentRequest`, so the edge and the machine cannot
|
|
1196
|
+
* disagree about what a valid body is.
|
|
1197
|
+
*/
|
|
1198
|
+
export declare function validateAgentControlRequest(input: unknown): ValidationResult<AgentControlRequest>;
|
|
1199
|
+
/** Checks an answer. An empty answer is refused: it is a park that never ended. */
|
|
1200
|
+
export declare function validateAgentAnswerRequest(input: unknown): ValidationResult<AgentAnswerRequest>;
|
|
1201
|
+
/**
|
|
1202
|
+
* Above this, the answer does not ride inline on the run row.
|
|
1203
|
+
*
|
|
1204
|
+
* One megabyte, and it is a product decision rather than a database one: under
|
|
1205
|
+
* it a caller who asked for one title gets it in `data` with no second request,
|
|
1206
|
+
* and over it the records are in object storage and `data` would be a megabyte
|
|
1207
|
+
* on every status poll of a two-hour run.
|
|
1208
|
+
*/
|
|
1209
|
+
export declare const INLINE_RESULT_BYTES = 1048576;
|
|
1210
|
+
/** What `POST /v1/agent` answers, in milliseconds. */
|
|
1211
|
+
export interface AgentSubmission {
|
|
1212
|
+
id: string;
|
|
1213
|
+
/** `queued`, always. Stated rather than implied so a client can assert it. */
|
|
1214
|
+
status: AgentRunStatus;
|
|
1215
|
+
createdAt: Timestamp;
|
|
1216
|
+
}
|
|
1217
|
+
/** `GET /v1/agent/{id}/events`. */
|
|
1218
|
+
export interface AgentEventQuery {
|
|
1219
|
+
/** Resume: everything with a greater `seq`. Absent starts at the beginning. */
|
|
1220
|
+
after?: number;
|
|
1221
|
+
level?: AgentEventLevel;
|
|
1222
|
+
limit?: number;
|
|
1223
|
+
}
|
|
1224
|
+
/** `GET /v1/agent/{id}/results`. */
|
|
1225
|
+
export interface AgentResultQuery {
|
|
1226
|
+
cursor?: string;
|
|
1227
|
+
limit?: number;
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* A frame on the event channel.
|
|
1231
|
+
*
|
|
1232
|
+
* ── THE STREAM IS A PROJECTION OF DURABLE STATE, NOT A LIVE PIPE ─────────────
|
|
1233
|
+
*
|
|
1234
|
+
* Every frame here was a row before it was a frame. The channel reads them from
|
|
1235
|
+
* a cursor and ends; it is never attached to the browser, the machine or the
|
|
1236
|
+
* loop. Three things fall out of that and all three are load-bearing:
|
|
1237
|
+
*
|
|
1238
|
+
* * a stateless Worker can serve it, because there is nothing to hold;
|
|
1239
|
+
* * a dropped connection loses nothing — resume at `cursor`;
|
|
1240
|
+
* * **the browser can die and the stream cannot**, because the stream was
|
|
1241
|
+
* never attached to it. `docs/AGENTS.md` states that as a property of the
|
|
1242
|
+
* product; here it is a property of the transport.
|
|
1243
|
+
*
|
|
1244
|
+
* The next reader will want to "upgrade" this to a WebSocket. That would take
|
|
1245
|
+
* all three away in exchange for latency nobody asked for on a run measured in
|
|
1246
|
+
* hours.
|
|
1247
|
+
*
|
|
1248
|
+
* Tagged with `frame` rather than dispatched on shape. `crawl` sniffs its
|
|
1249
|
+
* header frame apart from its page frames and gets away with it because the two
|
|
1250
|
+
* cannot be confused; here a `record` carrying an arbitrary `value` could
|
|
1251
|
+
* legitimately look like anything, including a header.
|
|
1252
|
+
*/
|
|
1253
|
+
export type AgentEventFrame =
|
|
1254
|
+
/**
|
|
1255
|
+
* First, always, before any record and even when there are none — so a client
|
|
1256
|
+
* that subscribed before the run produced anything receives the run and an
|
|
1257
|
+
* end rather than silence.
|
|
1258
|
+
*/
|
|
1259
|
+
{
|
|
1260
|
+
readonly frame: 'run';
|
|
1261
|
+
readonly run: AgentRun;
|
|
1262
|
+
readonly level: AgentEventLevel;
|
|
1263
|
+
} | {
|
|
1264
|
+
readonly frame: 'record';
|
|
1265
|
+
readonly record: AgentRecord;
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Last, always. `cursor` is where to resume; `done` is whether resuming would
|
|
1269
|
+
* be pointless because the run is terminal and this was the end of its log.
|
|
1270
|
+
*/
|
|
1271
|
+
| {
|
|
1272
|
+
readonly frame: 'end';
|
|
1273
|
+
readonly cursor: number;
|
|
1274
|
+
readonly done: boolean;
|
|
1275
|
+
};
|
|
1276
|
+
/**
|
|
1277
|
+
* A durable identity: minted seed, minted chrome-or-brave, pinned exit city
|
|
1278
|
+
* and its storage. Everything about it is minted and read-only — the caller
|
|
1279
|
+
* names it and nothing else.
|
|
1280
|
+
*
|
|
1281
|
+
* There is deliberately NO `identity` field, and there never will be.
|
|
1282
|
+
* Chrome-vs-brave is minted by us, pinned for the profile's life, and invisible
|
|
1283
|
+
* to customers; `seed`, `fingerprint`, `timezone`, `languages` and `webrtcIp`
|
|
1284
|
+
* are absent for the same reason. Every one of them is a way to build an
|
|
1285
|
+
* identity that cannot exist. The operator side runs the identity loop; the
|
|
1286
|
+
* customer side sees outcomes.
|
|
1287
|
+
*/
|
|
1288
|
+
export interface Profile {
|
|
1289
|
+
id: string;
|
|
1290
|
+
label: string;
|
|
1291
|
+
/** Pinned exit affinity: country/region/city. The IP rotates underneath it. */
|
|
1292
|
+
exit: ExitTarget;
|
|
1293
|
+
createdAt: Timestamp;
|
|
1294
|
+
}
|
|
1295
|
+
export interface ProfileCreateRequest {
|
|
1296
|
+
label: string;
|
|
1297
|
+
/** Optional geography. Everything else about the profile is minted. */
|
|
1298
|
+
exit?: ExitTarget;
|
|
1299
|
+
}
|
|
1300
|
+
export interface ProfileQuery {
|
|
1301
|
+
/** Glob, e.g. "acme-*". */
|
|
1302
|
+
label?: string;
|
|
1303
|
+
limit?: number;
|
|
1304
|
+
cursor?: string;
|
|
1305
|
+
}
|
|
1306
|
+
export type SessionStatus = 'running' | 'completed' | 'partial' | 'failed';
|
|
1307
|
+
/** What actually happened, as opposed to how the call returned. */
|
|
1308
|
+
export type SessionOutcome = 'ok' | 'blocked' | 'timeout' | 'capped' | 'error';
|
|
1309
|
+
export interface SessionSummary {
|
|
1310
|
+
id: string;
|
|
1311
|
+
status: SessionStatus;
|
|
1312
|
+
outcome?: SessionOutcome;
|
|
1313
|
+
/** Absent for ephemeral sessions. */
|
|
1314
|
+
profileId?: string;
|
|
1315
|
+
/** The agent task, when the session was an agent run. */
|
|
1316
|
+
task?: string;
|
|
1317
|
+
/** Where this session actually came out. Session-scoped, not profile-scoped. */
|
|
1318
|
+
exit?: ResolvedExit;
|
|
1319
|
+
startedAt: Timestamp;
|
|
1320
|
+
endedAt?: Timestamp;
|
|
1321
|
+
durationMs: number;
|
|
1322
|
+
pages: number;
|
|
1323
|
+
/**
|
|
1324
|
+
* Hosted player. First-class, not a URL buried in an agent result.
|
|
1325
|
+
*
|
|
1326
|
+
* Optional for the reason {@link WireAgentResult.replayUrl} gives at length:
|
|
1327
|
+
* a session served by a host with no durable replay storage has no link that
|
|
1328
|
+
* will still resolve after that process exits, and an absent field is the
|
|
1329
|
+
* honest way to say so.
|
|
1330
|
+
*/
|
|
1331
|
+
replayUrl?: string;
|
|
1332
|
+
/**
|
|
1333
|
+
* The caller declined a recording for this session. See
|
|
1334
|
+
* {@link AgentResult.recordingDeclined}, which is the same fact on the
|
|
1335
|
+
* result of the run that produced the row, argued at length.
|
|
1336
|
+
*
|
|
1337
|
+
* Read from `sessions.recording_declined`, which is written from the request
|
|
1338
|
+
* that opened the session and never moved afterwards. It is the reason
|
|
1339
|
+
* {@link replayUrl}'s absence is readable: a session with no url and this
|
|
1340
|
+
* `true` was refused, and one with no url and no field was wanted and not
|
|
1341
|
+
* obtained.
|
|
1342
|
+
*
|
|
1343
|
+
* Set for a `/connect` session and an `agent` run, the two capabilities that
|
|
1344
|
+
* record. Absent on a scrape, crawl, search or monitor check, which record
|
|
1345
|
+
* nothing and so have nothing to decline.
|
|
1346
|
+
*/
|
|
1347
|
+
recordingDeclined?: true;
|
|
1348
|
+
usage: Usage;
|
|
1349
|
+
billed?: BilledResources;
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Note there is no `identity` filter here, by design. The equivalent internal
|
|
1353
|
+
* query — "every brave block this week" — lives on the operator side, where the
|
|
1354
|
+
* identity loop is run. Customers see outcomes; we see which identity produced
|
|
1355
|
+
* them.
|
|
1356
|
+
*/
|
|
1357
|
+
export interface SessionQuery {
|
|
1358
|
+
status?: SessionStatus;
|
|
1359
|
+
outcome?: SessionOutcome;
|
|
1360
|
+
/** Glob against the agent task, e.g. "*checkout*". */
|
|
1361
|
+
task?: string;
|
|
1362
|
+
/** Relative (`24h`, `7d`) or an ISO instant. */
|
|
1363
|
+
since?: string;
|
|
1364
|
+
until?: string;
|
|
1365
|
+
profile?: string;
|
|
1366
|
+
limit?: number;
|
|
1367
|
+
cursor?: string;
|
|
1368
|
+
}
|
|
1369
|
+
export interface SessionExportRequest {
|
|
1370
|
+
sessionId: string;
|
|
1371
|
+
}
|
|
1372
|
+
export interface SessionExport {
|
|
1373
|
+
sessionId: string;
|
|
1374
|
+
/** Portable `.outcrawl` bundle; plays offline. */
|
|
1375
|
+
url: string;
|
|
1376
|
+
bytes: number;
|
|
1377
|
+
expiresAt: Timestamp;
|
|
1378
|
+
}
|
|
1379
|
+
export type UsageGrouping = 'hour' | 'day' | 'month' | 'capability';
|
|
1380
|
+
export interface UsageQuery {
|
|
1381
|
+
/** ISO date or instant. */
|
|
1382
|
+
from: string;
|
|
1383
|
+
to: string;
|
|
1384
|
+
/** Defaults to `day`. */
|
|
1385
|
+
groupBy?: UsageGrouping;
|
|
1386
|
+
}
|
|
1387
|
+
export interface UsageBucket {
|
|
1388
|
+
/** The day, hour, month or capability name this bucket covers. */
|
|
1389
|
+
key: string;
|
|
1390
|
+
usage: Usage;
|
|
1391
|
+
billed: BilledResources;
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Same numbers as the per-call `usage` fields, aggregated. Nothing appears here
|
|
1395
|
+
* that was not already visible on the call that caused it.
|
|
1396
|
+
*/
|
|
1397
|
+
export interface UsageReport {
|
|
1398
|
+
from: string;
|
|
1399
|
+
to: string;
|
|
1400
|
+
groupBy: UsageGrouping;
|
|
1401
|
+
buckets: UsageBucket[];
|
|
1402
|
+
total: BilledResources;
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Why credits moved, in the vocabulary a customer is owed an answer in.
|
|
1406
|
+
*
|
|
1407
|
+
* Three values, and every one of them is reachable today: a student grant, a
|
|
1408
|
+
* refund for a run we charged for and should not have, and an operator
|
|
1409
|
+
* correction. There is deliberately no `expiry` and no `spend`.
|
|
1410
|
+
*
|
|
1411
|
+
* No `expiry`, because expiry is the entry's own `expires_at` — a column the
|
|
1412
|
+
* balance reader honours at read time — and not a row somebody has to remember
|
|
1413
|
+
* to write. A sweep that writes expiry rows is a sweep that can fail to run,
|
|
1414
|
+
* and the failure mode is a balance that stays spendable months after it
|
|
1415
|
+
* lapsed.
|
|
1416
|
+
*
|
|
1417
|
+
* No `spend`, because spend is metered usage and writing it here as well would
|
|
1418
|
+
* be two numbers for one fact with nothing holding them equal. `CREDIT_BALANCE`
|
|
1419
|
+
* in `@outcrawl/persistence` carries the whole argument.
|
|
1420
|
+
*
|
|
1421
|
+
* HERE rather than in `@outcrawl/persistence`, where it was, because three
|
|
1422
|
+
* packages now have to agree about it and none of them may hold a copy: the
|
|
1423
|
+
* ledger's `credit_ledger_reason_known` check constraint renders from it, the
|
|
1424
|
+
* router validates `POST /operator/credits` against it, and the CLI's
|
|
1425
|
+
* `--reason` flag offers it. A second list in any of the three is a value one
|
|
1426
|
+
* of them accepts and another refuses.
|
|
1427
|
+
*/
|
|
1428
|
+
export type CreditReason = 'grant' | 'refund' | 'adjustment';
|
|
1429
|
+
export declare const CREDIT_REASONS: readonly ["grant", "refund", "adjustment"];
|
|
1430
|
+
/**
|
|
1431
|
+
* What authorised the entry, so a row can be traced back to the thing that
|
|
1432
|
+
* caused it rather than to whoever happened to run the insert.
|
|
1433
|
+
*
|
|
1434
|
+
* Paired with the entry's `reference_id` under a unique constraint, which is
|
|
1435
|
+
* what makes granting itself idempotent: a redeem code presented twice is
|
|
1436
|
+
* `('student_programme', <code id>)` twice and the second insert is a no-op,
|
|
1437
|
+
* not a second 10,000 credits.
|
|
1438
|
+
*
|
|
1439
|
+
* Narrow on purpose. `stripe` is absent because nothing in this tree talks to
|
|
1440
|
+
* Stripe, and a value that cannot be produced is not a source — it is an
|
|
1441
|
+
* unbilled intention every reader of the vocabulary has to rule out, which is
|
|
1442
|
+
* the exact defect `0007_usage_kinds.sql` removed from `UsageKind`.
|
|
1443
|
+
*/
|
|
1444
|
+
export type CreditSource = 'student_programme' | 'promotion' | 'operator';
|
|
1445
|
+
export declare const CREDIT_SOURCES: readonly ["student_programme", "promotion", "operator"];
|
|
1446
|
+
/**
|
|
1447
|
+
* What an account still OWNS, as `GET /v1/credits` answers it.
|
|
1448
|
+
*
|
|
1449
|
+
* A different question from {@link UsageReport} and deliberately a different
|
|
1450
|
+
* route, because folding one into the other would break both. A usage report is
|
|
1451
|
+
* WINDOWED — it takes `from` and `to` and answers "what did this account
|
|
1452
|
+
* consume between these instants". This is a POOL: granted once, drawn down by
|
|
1453
|
+
* every credit ever spent against it, and not reset by a month boundary. A
|
|
1454
|
+
* balance shown inside a windowed report would read as that window's balance,
|
|
1455
|
+
* which is the misreading `CREDIT_BALANCE` in `@outcrawl/persistence` warns
|
|
1456
|
+
* about at length.
|
|
1457
|
+
*
|
|
1458
|
+
* It is also NOT the plan's monthly allowance. A paid tier's 5,000 credits a
|
|
1459
|
+
* month are an allowance that resets; these are credits the account was given
|
|
1460
|
+
* and keeps until they are spent or they lapse. The two are enforced and
|
|
1461
|
+
* reported by different machinery on purpose: a student's 10,000-credit grant
|
|
1462
|
+
* folded into the monthly window would silently lapse every month, which is
|
|
1463
|
+
* the opposite of what the promotion promises.
|
|
1464
|
+
*
|
|
1465
|
+
* Every figure is an exact decimal STRING, summed and subtracted by Postgres.
|
|
1466
|
+
* These are credits, and a binary64 that has been through a reconciler cannot
|
|
1467
|
+
* be reconciled back.
|
|
1468
|
+
*/
|
|
1469
|
+
export interface CreditBalance {
|
|
1470
|
+
/** Unlapsed grants, refunds and adjustments, summed. */
|
|
1471
|
+
granted: string;
|
|
1472
|
+
/**
|
|
1473
|
+
* Entries whose expiry has passed, summed SEPARATELY rather than dropped.
|
|
1474
|
+
*
|
|
1475
|
+
* This field is the answer to "where did my 4,000 credits go". Without it a
|
|
1476
|
+
* reader can only show a smaller number, and the customer is told that the
|
|
1477
|
+
* database says so.
|
|
1478
|
+
*/
|
|
1479
|
+
expired: string;
|
|
1480
|
+
/** Priced usage, all time, off the append-only meter. */
|
|
1481
|
+
spent: string;
|
|
1482
|
+
/** `granted - spent`. Negative means the pool was overspent. */
|
|
1483
|
+
balance: string;
|
|
1484
|
+
/**
|
|
1485
|
+
* `balance <= 0`: nothing left in the granted pool.
|
|
1486
|
+
*
|
|
1487
|
+
* REPORTED, NOT ENFORCED. Nothing in the control plane refuses work because
|
|
1488
|
+
* this is true: admission is decided against the plan's monthly allowance,
|
|
1489
|
+
* which a grant deliberately does not move. So an exhausted pool costs an
|
|
1490
|
+
* account nothing today, and a fresh grant buys it nothing either. That is a
|
|
1491
|
+
* product gap, it is named here so no reader mistakes this field for a gate,
|
|
1492
|
+
* and closing it is a pricing decision rather than a wiring one.
|
|
1493
|
+
*/
|
|
1494
|
+
exhausted: boolean;
|
|
1495
|
+
/**
|
|
1496
|
+
* Usage rows carrying a price nothing can sum.
|
|
1497
|
+
*
|
|
1498
|
+
* Non-zero means somebody wrote a `credits` value to the meter out of band,
|
|
1499
|
+
* so `spent` is a lower bound and `balance` an upper one — every other figure
|
|
1500
|
+
* here is approximate while this is not zero, and saying so is the point.
|
|
1501
|
+
*/
|
|
1502
|
+
malformed: number;
|
|
1503
|
+
}
|
|
1504
|
+
export declare const PROFILE_KEYS: readonly ["id", "label", "exit", "createdAt"];
|
|
1505
|
+
export declare const SESSION_QUERY_KEYS: readonly ["status", "outcome", "task", "since", "until", "profile", "limit", "cursor"];
|
|
1506
|
+
/**
|
|
1507
|
+
* The wire form of every list capability: `profiles.list`, `sessions.list`,
|
|
1508
|
+
* `monitors.list`.
|
|
1509
|
+
*
|
|
1510
|
+
* A bare array cannot be the wire form, because every list query carries a
|
|
1511
|
+
* `cursor` and a bare array has nowhere to return the next one. Making the
|
|
1512
|
+
* envelope a type rather than a convention is what stops four surfaces
|
|
1513
|
+
* agreeing today and drifting apart at the first one that forgets.
|
|
1514
|
+
*
|
|
1515
|
+
* `cursor` absent means the last page. It is deliberately not `null`: absence
|
|
1516
|
+
* is the end of the list, and a `null` that means the same thing gives callers
|
|
1517
|
+
* two ways to spell one state.
|
|
1518
|
+
*
|
|
1519
|
+
* `usage` is mandatory here as it is everywhere else. Usage rides on the result
|
|
1520
|
+
* that caused it rather than living in a metrics endpoint, so that the number
|
|
1521
|
+
* on the bill is always attached to the call that produced it. A list is a
|
|
1522
|
+
* billable call like any other.
|
|
1523
|
+
*/
|
|
1524
|
+
export interface ListResponse<T> {
|
|
1525
|
+
data: T[];
|
|
1526
|
+
cursor?: string;
|
|
1527
|
+
usage: Usage;
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* The body of a non-upgrade `GET /connect`: what the router hands a client so
|
|
1531
|
+
* it can reach a browser.
|
|
1532
|
+
*
|
|
1533
|
+
* This lives in core because it has two ends — the router writes it, the SDK
|
|
1534
|
+
* reads it — and neither owns it. Defining it in the server would make a
|
|
1535
|
+
* published client package depend on the control plane to learn a wire shape,
|
|
1536
|
+
* and copying it into both is the drift that having one contract package is
|
|
1537
|
+
* meant to prevent.
|
|
1538
|
+
*/
|
|
1539
|
+
export interface SessionAllocation {
|
|
1540
|
+
/** Minted at allocation. The `sessions` row the worker will create. */
|
|
1541
|
+
sessionId: string;
|
|
1542
|
+
/** Hand this straight to `connectOverCDP`. */
|
|
1543
|
+
wsEndpoint: string;
|
|
1544
|
+
/** When the ticket in `wsEndpoint` stops being redeemable. */
|
|
1545
|
+
expiresAt: Timestamp;
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* The time seam. Every wait and every deadline goes through this, so tests
|
|
1549
|
+
* drive a virtual clock instead of sleeping.
|
|
1550
|
+
*
|
|
1551
|
+
* `now` is a monotonic millisecond reading for measuring elapsed time. It is
|
|
1552
|
+
* **not** a wall-clock timestamp and must never be persisted or compared across
|
|
1553
|
+
* processes — use `Timestamp` for anything that outlives the process.
|
|
1554
|
+
*
|
|
1555
|
+
* This lives in core rather than in the first package that needed it because
|
|
1556
|
+
* three now need one independently — the render pipeline, the agent loop's
|
|
1557
|
+
* duration cap, and the supervisor's heartbeat — and none of them should take a
|
|
1558
|
+
* dependency on another's package to obtain a two-method interface.
|
|
1559
|
+
*/
|
|
1560
|
+
export interface Clock {
|
|
1561
|
+
now(): number;
|
|
1562
|
+
sleep(ms: number): Promise<void>;
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* The real one. `performance.now()` rather than `Date.now()`, because a
|
|
1566
|
+
* deadline must not move when NTP steps the wall clock.
|
|
1567
|
+
*/
|
|
1568
|
+
export declare const systemClock: Clock;
|
|
1569
|
+
/**
|
|
1570
|
+
* Wall time, injected. Epoch milliseconds, for values that are **stored** or
|
|
1571
|
+
* compared across processes: when a cache entry was resolved, when a lease was
|
|
1572
|
+
* taken, when a profile was last seen.
|
|
1573
|
+
*
|
|
1574
|
+
* Deliberately a separate name from {@link Clock}, and deliberately not spelled
|
|
1575
|
+
* `clock`. The two have the same shape and opposite contracts, so anything that
|
|
1576
|
+
* unifies them by pattern-matching the word is a bug that does not announce
|
|
1577
|
+
* itself: `new Date(performance.now())` is a valid Date somewhere in 1970, and
|
|
1578
|
+
* every persisted timestamp is quietly wrong while every test still passes.
|
|
1579
|
+
*
|
|
1580
|
+
* Choosing between them: does the value outlive the process? Wall. Is it the
|
|
1581
|
+
* distance between two moments? Monotonic — and then it must be {@link Clock},
|
|
1582
|
+
* because wall time steps backwards when NTP corrects it.
|
|
1583
|
+
*/
|
|
1584
|
+
export interface WallClock {
|
|
1585
|
+
/** Epoch milliseconds. Subject to NTP steps; never use it for a duration. */
|
|
1586
|
+
now(): number;
|
|
1587
|
+
}
|
|
1588
|
+
export declare const systemWallClock: WallClock;
|
|
1589
|
+
export type ValidationResult<T> = {
|
|
1590
|
+
ok: true;
|
|
1591
|
+
value: T;
|
|
1592
|
+
} | {
|
|
1593
|
+
ok: false;
|
|
1594
|
+
errors: string[];
|
|
1595
|
+
};
|
|
1596
|
+
/**
|
|
1597
|
+
* `"5m"` -> 300000. Throws on anything {@link DURATION_PATTERN} rejects.
|
|
1598
|
+
*
|
|
1599
|
+
* Canonical here rather than in `@outcrawl/runtime`, which is where it used to
|
|
1600
|
+
* live, because the grammar has to be enforced at BOTH trust boundaries — the
|
|
1601
|
+
* edge validator and the loop's own `validateCaps` — and a second parser beside
|
|
1602
|
+
* a second copy of the pattern is how the two come to disagree about what
|
|
1603
|
+
* `"90m"` means. `@outcrawl/runtime` re-exports this one.
|
|
1604
|
+
*/
|
|
1605
|
+
export declare function parseDuration(spec: string): number;
|
|
1606
|
+
/**
|
|
1607
|
+
* Checks one `caps.duration` against the grammar, returning the error to report
|
|
1608
|
+
* or `undefined`.
|
|
1609
|
+
*
|
|
1610
|
+
* ── THERE IS NO CEILING HERE ANY MORE, AND THAT IS THE POINT ─────────────────
|
|
1611
|
+
*
|
|
1612
|
+
* This function used to refuse anything over `MAX_RUN_DURATION_MS` — 900,000,
|
|
1613
|
+
* spelled `15m` to customers — and the refusal was honest about its own
|
|
1614
|
+
* reasoning: the edge forwarded one `fetch` per REST call under an
|
|
1615
|
+
* `AbortSignal.timeout`, that signal bounded the whole call, and a run that
|
|
1616
|
+
* outlived it had its socket cut mid-stream with no terminal frame. So the
|
|
1617
|
+
* platform could not deliver a longer run and said so.
|
|
1618
|
+
*
|
|
1619
|
+
* A job has no held request. Nothing forwards, nothing times out, and the
|
|
1620
|
+
* number that used to be quoted as a product limit was never one — it was the
|
|
1621
|
+
* shape of the transport, wearing policy's clothes. `docs/AGENTS.md`: "the
|
|
1622
|
+
* refusal currently quoting it is a patch over that and is deleted when this
|
|
1623
|
+
* lands, not tuned." Deleted, not tuned. `caps` is the customer's dial, hours
|
|
1624
|
+
* are normal, and `"12h"` now means twelve hours.
|
|
1625
|
+
*
|
|
1626
|
+
* The function survives the ceiling because its OTHER reason outlives it:
|
|
1627
|
+
* `validateAgentRequest` here and `validateCaps` in `@outcrawl/runtime` must
|
|
1628
|
+
* refuse a malformed duration with the same sentence, so a caller who hits the
|
|
1629
|
+
* loop directly gets the same message as one who came through HTTP. Returns
|
|
1630
|
+
* rather than pushes, for that.
|
|
1631
|
+
*/
|
|
1632
|
+
export declare function capDurationError(value: unknown): string | undefined;
|
|
1633
|
+
/**
|
|
1634
|
+
* The one canonical object guard for this package. `@outcrawl/core` has no
|
|
1635
|
+
* dependencies — it is the contract every other package imports, so it carries
|
|
1636
|
+
* no schema validator — and this guard is defined here once rather than
|
|
1637
|
+
* recreated at each boundary. It proves an object and nothing more; fields stay
|
|
1638
|
+
* `unknown` and are checked individually below.
|
|
1639
|
+
*/
|
|
1640
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
1641
|
+
/**
|
|
1642
|
+
* Every field a submit accepts. This is part of the REQUEST CONTRACT, so it
|
|
1643
|
+
* lives beside the type and the validator rather than in whichever package
|
|
1644
|
+
* happens to parse a body first.
|
|
1645
|
+
*
|
|
1646
|
+
* It moved here from `@outcrawl/api`'s `AGENT_FIELDS` for a measured reason.
|
|
1647
|
+
* A run is a row, and the edge writes that row before it forwards. While the
|
|
1648
|
+
* accept-list was private to the api package the EDGE could not consult it, so
|
|
1649
|
+
* a submit carrying an unknown field was recorded, forwarded, refused a hop
|
|
1650
|
+
* later, and settled `failed` — a permanent run in the customer's history for
|
|
1651
|
+
* work they never successfully submitted. Measured 2026-09-08: one bad field,
|
|
1652
|
+
* `http=400`, `agent_runs` +1.
|
|
1653
|
+
*
|
|
1654
|
+
* `satisfies readonly (keyof AgentRequest)[]` is half its job; the other half
|
|
1655
|
+
* is `_requestFieldsExhaustive` in `@outcrawl/api`, which still fails to
|
|
1656
|
+
* compile if `AgentRequest` grows a field this list does not name. Both halves
|
|
1657
|
+
* now read ONE array, so the edge and the machine cannot disagree about what a
|
|
1658
|
+
* submit may carry.
|
|
1659
|
+
*/
|
|
1660
|
+
export declare const AGENT_REQUEST_FIELDS: readonly ["task", "caps", "profile", "schema", "agent", "webhook", "notify", "exit", "cache", "record", "secrets", "rules", "integrations"];
|
|
1661
|
+
/**
|
|
1662
|
+
* The names in `input` that no submit accepts, in the order given. Empty means
|
|
1663
|
+
* every field is known — it does NOT mean the request is valid, which is
|
|
1664
|
+
* `validateAgentRequest`'s question.
|
|
1665
|
+
*/
|
|
1666
|
+
export declare function unexpectedAgentFields(input: Record<string, unknown>): readonly string[];
|
|
1667
|
+
/**
|
|
1668
|
+
* Validates an agent request at the trust boundary — HTTP body, MCP tool call,
|
|
1669
|
+
* CLI flags. `caps` is required: an agent run with no ceiling is how a bill
|
|
1670
|
+
* becomes a story, so the request is rejected before a browser is allocated
|
|
1671
|
+
* rather than capped later.
|
|
1672
|
+
*
|
|
1673
|
+
* MCP supplies a conservative default when the caller omits `caps`; it must do
|
|
1674
|
+
* so *before* calling this.
|
|
1675
|
+
*/
|
|
1676
|
+
export declare function validateAgentRequest(input: unknown): ValidationResult<AgentRequest>;
|
|
1677
|
+
export {};
|