@relayflows/sdk 2.0.22 → 2.0.23
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/dist/agent-artifacts.d.ts +18 -9
- package/dist/agent-artifacts.d.ts.map +1 -1
- package/dist/agent-artifacts.js +27 -11
- package/dist/agent-artifacts.js.map +1 -1
- package/dist/authored-flow-error.d.ts +33 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js +20 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +4 -1
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-node-entry.js +5 -1
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts +11 -0
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +30 -1
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-root.js +3 -1
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-source-authority.js +13 -0
- package/dist/authored-source-authority.js.map +1 -1
- package/dist/authored-step-index.d.ts +82 -0
- package/dist/authored-step-index.d.ts.map +1 -0
- package/dist/authored-step-index.js +120 -0
- package/dist/authored-step-index.js.map +1 -0
- package/dist/authored-step-output.d.ts +23 -2
- package/dist/authored-step-output.d.ts.map +1 -1
- package/dist/authored-step-output.js +47 -5
- package/dist/authored-step-output.js.map +1 -1
- package/dist/authored-worker-step.d.ts +10 -2
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +81 -7
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +67 -12
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cloud-read.d.ts +31 -0
- package/dist/cli/cloud-read.d.ts.map +1 -0
- package/dist/cli/cloud-read.js +482 -0
- package/dist/cli/cloud-read.js.map +1 -0
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +53 -25
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/status.d.ts +9 -0
- package/dist/cli/status.d.ts.map +1 -1
- package/dist/cli/status.js +14 -0
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/step-failure.d.ts +26 -0
- package/dist/cli/step-failure.d.ts.map +1 -1
- package/dist/cli/step-failure.js +68 -3
- package/dist/cli/step-failure.js.map +1 -1
- package/dist/cli-commands.d.ts +29 -1
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +23 -1
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +19 -2
- package/dist/cli.js.map +1 -1
- package/dist/cloud-http.d.ts +12 -0
- package/dist/cloud-http.d.ts.map +1 -1
- package/dist/cloud-http.js +14 -6
- package/dist/cloud-http.js.map +1 -1
- package/dist/cloud-read.d.ts +154 -0
- package/dist/cloud-read.d.ts.map +1 -0
- package/dist/cloud-read.js +373 -0
- package/dist/cloud-read.js.map +1 -0
- package/dist/cloud-transcript.d.ts +94 -0
- package/dist/cloud-transcript.d.ts.map +1 -0
- package/dist/cloud-transcript.js +342 -0
- package/dist/cloud-transcript.js.map +1 -0
- package/dist/communication/environment.d.ts +3 -0
- package/dist/communication/environment.d.ts.map +1 -0
- package/dist/communication/environment.js +34 -0
- package/dist/communication/environment.js.map +1 -0
- package/dist/communication/history.d.ts +6 -0
- package/dist/communication/history.d.ts.map +1 -0
- package/dist/communication/history.js +38 -0
- package/dist/communication/history.js.map +1 -0
- package/dist/communication/local.d.ts +6 -0
- package/dist/communication/local.d.ts.map +1 -0
- package/dist/communication/local.js +56 -0
- package/dist/communication/local.js.map +1 -0
- package/dist/communication/preflight.d.ts +6 -0
- package/dist/communication/preflight.d.ts.map +1 -0
- package/dist/communication/preflight.js +37 -0
- package/dist/communication/preflight.js.map +1 -0
- package/dist/communication/projection.d.ts +8 -0
- package/dist/communication/projection.d.ts.map +1 -0
- package/dist/communication/projection.js +23 -0
- package/dist/communication/projection.js.map +1 -0
- package/dist/communication/relay.d.ts +78 -0
- package/dist/communication/relay.d.ts.map +1 -0
- package/dist/communication/relay.js +89 -0
- package/dist/communication/relay.js.map +1 -0
- package/dist/communication/session.d.ts +24 -0
- package/dist/communication/session.d.ts.map +1 -0
- package/dist/communication/session.js +100 -0
- package/dist/communication/session.js.map +1 -0
- package/dist/communication/spec.d.ts +20 -0
- package/dist/communication/spec.d.ts.map +1 -0
- package/dist/communication/spec.js +113 -0
- package/dist/communication/spec.js.map +1 -0
- package/dist/communication/tool.d.ts +3 -0
- package/dist/communication/tool.d.ts.map +1 -0
- package/dist/communication/tool.js +23 -0
- package/dist/communication/tool.js.map +1 -0
- package/dist/communication/tools.d.ts +11 -0
- package/dist/communication/tools.d.ts.map +1 -0
- package/dist/communication/tools.js +65 -0
- package/dist/communication/tools.js.map +1 -0
- package/dist/communication/worker.d.ts +6 -0
- package/dist/communication/worker.d.ts.map +1 -0
- package/dist/communication/worker.js +120 -0
- package/dist/communication/worker.js.map +1 -0
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +8 -1
- package/dist/compile.js.map +1 -1
- package/dist/failure-kinds.d.ts +16 -1
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +1 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/journal-client.d.ts +4 -1
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +11 -2
- package/dist/journal-client.js.map +1 -1
- package/dist/preflight.d.ts +13 -2
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +23 -6
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +39 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/spec.d.ts +8 -0
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +29 -50
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker.d.ts +1 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +10 -1
- package/dist/worker.js.map +1 -1
- package/dist/wrapper-session.d.ts +8 -0
- package/dist/wrapper-session.d.ts.map +1 -1
- package/dist/wrapper-session.js +156 -23
- package/dist/wrapper-session.js.map +1 -1
- package/package.json +14 -2
- package/src/agent-artifacts.ts +27 -11
- package/src/authored-flow-error.ts +17 -0
- package/src/authored-flow-executor.ts +4 -1
- package/src/authored-node-entry.ts +5 -1
- package/src/authored-node-runner.ts +33 -1
- package/src/authored-root.ts +2 -1
- package/src/authored-source-authority.ts +13 -0
- package/src/authored-step-index.ts +157 -0
- package/src/authored-step-output.ts +66 -14
- package/src/authored-worker-step.ts +88 -15
- package/src/cli/check.ts +69 -11
- package/src/cli/cloud-read.ts +518 -0
- package/src/cli/run.ts +43 -25
- package/src/cli/status.ts +20 -0
- package/src/cli/step-failure.ts +68 -3
- package/src/cli-commands.ts +23 -1
- package/src/cli.ts +20 -1
- package/src/cloud-http.ts +32 -6
- package/src/cloud-read.ts +515 -0
- package/src/cloud-transcript.ts +422 -0
- package/src/communication/environment.ts +33 -0
- package/src/communication/history.ts +38 -0
- package/src/communication/local.ts +41 -0
- package/src/communication/preflight.ts +34 -0
- package/src/communication/projection.ts +23 -0
- package/src/communication/relay.ts +102 -0
- package/src/communication/session.ts +84 -0
- package/src/communication/spec.ts +106 -0
- package/src/communication/tool.ts +22 -0
- package/src/communication/tools.ts +50 -0
- package/src/communication/worker.ts +95 -0
- package/src/compile.ts +4 -1
- package/src/failure-kinds.ts +16 -0
- package/src/index.ts +10 -1
- package/src/journal-client.ts +12 -2
- package/src/preflight.ts +36 -7
- package/src/protocol.ts +15 -0
- package/src/spec.ts +2 -0
- package/src/worker-cli.ts +28 -47
- package/src/worker.ts +10 -0
- package/src/wrapper-session.ts +159 -24
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
// Reading a hosted run: the four Cloud GETs behind `flows runs`, `flows logs`
|
|
2
|
+
// and `flows status --cloud`.
|
|
3
|
+
//
|
|
4
|
+
// Nothing here writes. Every call goes through `cloudFetch`, so the credential
|
|
5
|
+
// is resolved exactly once and in exactly one place (cloud-http.ts: the `token`
|
|
6
|
+
// option, then `FLOWS_CLOUD_TOKEN`, then the `agent-relay cloud login` store,
|
|
7
|
+
// with the URL binding and the expiry refusal) -- an agent inspecting its own
|
|
8
|
+
// runs holds the same credential it would use to start one, and there is no
|
|
9
|
+
// second path to get it wrong.
|
|
10
|
+
//
|
|
11
|
+
// The routes answer Cloud's own field names in camelCase. This module is where
|
|
12
|
+
// they become the snake_case view the SDK renders, so the shape a reader sees
|
|
13
|
+
// is the SDK's and a Cloud response field that changes name breaks here rather
|
|
14
|
+
// than in three renderers. Every field is read defensively: a route that adds a
|
|
15
|
+
// key is ignored, a route that drops one yields `null`, never a throw.
|
|
16
|
+
|
|
17
|
+
import { CloudFlowError, cloudFetch, cloudRunId, isCloudRecord, type CloudConnectionOptions } from './cloud-http.js';
|
|
18
|
+
|
|
19
|
+
/** One row of `GET /api/v1/workflows/runs`. */
|
|
20
|
+
export interface CloudRunSummary {
|
|
21
|
+
run_id: string;
|
|
22
|
+
/** The flow's declared name, read out of the bounded `workflow` projection. */
|
|
23
|
+
name: string;
|
|
24
|
+
status: string;
|
|
25
|
+
/** Present on a parked or finished run; the list extracts it from `result`. */
|
|
26
|
+
completion_reason: string | null;
|
|
27
|
+
created_at: string | null;
|
|
28
|
+
updated_at: string | null;
|
|
29
|
+
/** Only a literal GitHub pull request URL survives; anything else is dropped. */
|
|
30
|
+
pull_request_url: string | null;
|
|
31
|
+
/** The bounded error preview the list carries, redacted by the caller. */
|
|
32
|
+
error: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** `GET /api/v1/workflows/runs/<runId>` -- the run record itself. */
|
|
36
|
+
export interface CloudRunDetail {
|
|
37
|
+
run_id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
status: string;
|
|
40
|
+
completion_reason: string | null;
|
|
41
|
+
created_at: string | null;
|
|
42
|
+
updated_at: string | null;
|
|
43
|
+
pull_request_url: string | null;
|
|
44
|
+
error: string | null;
|
|
45
|
+
completed_steps: number | null;
|
|
46
|
+
/** What Cloud pinned to execute this run; provenance, not state. */
|
|
47
|
+
authority: {
|
|
48
|
+
surface_version: string | null;
|
|
49
|
+
source_sha256: string | null;
|
|
50
|
+
artifact_sha256: string | null;
|
|
51
|
+
source_commit: string | null;
|
|
52
|
+
} | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One entry of a step's `detail.attempts`. */
|
|
56
|
+
export interface CloudStepAttempt {
|
|
57
|
+
attempt: number | null;
|
|
58
|
+
disposition: string | null;
|
|
59
|
+
completion_reason: string | null;
|
|
60
|
+
at_ms: number | null;
|
|
61
|
+
transcript_bytes: number | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** One tool the agent used, from the digest's `tools.counts`. */
|
|
65
|
+
export interface CloudToolCount {
|
|
66
|
+
name: string;
|
|
67
|
+
calls: number | null;
|
|
68
|
+
errors: number | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** One of the digest's `tools.last_calls`. */
|
|
72
|
+
export interface CloudToolCall {
|
|
73
|
+
seq: number | null;
|
|
74
|
+
name: string;
|
|
75
|
+
result_bytes: number | null;
|
|
76
|
+
input_excerpt: string | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The transcript digest flows 2.0.22 ships on each agent step
|
|
81
|
+
* (`detail.transcript`). The same facts the local `flows status` prints from
|
|
82
|
+
* the journal's `trajectory_tail.transcript`, plus the tool roster Cloud keeps.
|
|
83
|
+
*/
|
|
84
|
+
export interface CloudStepTranscript {
|
|
85
|
+
attempt: number | null;
|
|
86
|
+
model: string | null;
|
|
87
|
+
num_turns: number | null;
|
|
88
|
+
total_cost_usd: number | null;
|
|
89
|
+
tokens_in: number | null;
|
|
90
|
+
tokens_out: number | null;
|
|
91
|
+
cache_read: number | null;
|
|
92
|
+
cache_creation: number | null;
|
|
93
|
+
duration_ms: number | null;
|
|
94
|
+
subtype: string | null;
|
|
95
|
+
is_error: boolean;
|
|
96
|
+
exit_code: number | null;
|
|
97
|
+
bytes_kept: number | null;
|
|
98
|
+
bytes_total: number | null;
|
|
99
|
+
frames_kept: number | null;
|
|
100
|
+
frames_total: number | null;
|
|
101
|
+
truncated: boolean;
|
|
102
|
+
total_calls: number | null;
|
|
103
|
+
tools: CloudToolCount[];
|
|
104
|
+
last_calls: CloudToolCall[];
|
|
105
|
+
artifacts: string[];
|
|
106
|
+
artifact_count: number | null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** One row of `GET /api/v1/workflows/runs/<runId>/steps`. */
|
|
110
|
+
export interface CloudStep {
|
|
111
|
+
step_name: string;
|
|
112
|
+
step_type: string;
|
|
113
|
+
status: string;
|
|
114
|
+
completion_reason: string | null;
|
|
115
|
+
cli: string | null;
|
|
116
|
+
model: string | null;
|
|
117
|
+
/** The log selector: `flows logs <run> --step <sandbox_id>`. Empty for a non-agent step. */
|
|
118
|
+
sandbox_id: string;
|
|
119
|
+
started_at: string | null;
|
|
120
|
+
ended_at: string | null;
|
|
121
|
+
duration_ms: number | null;
|
|
122
|
+
exit_code: number | null;
|
|
123
|
+
retry_count: number | null;
|
|
124
|
+
tokens_in: number | null;
|
|
125
|
+
tokens_out: number | null;
|
|
126
|
+
cost_usd: number | null;
|
|
127
|
+
output_summary: string | null;
|
|
128
|
+
gate: { gate: string; verdict: string; detail: string } | null;
|
|
129
|
+
attempts: CloudStepAttempt[];
|
|
130
|
+
transcript: CloudStepTranscript | null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** `GET /api/v1/workflows/runs/<runId>/logs` -- the route's own envelope. */
|
|
134
|
+
export interface CloudRunLog {
|
|
135
|
+
content: string;
|
|
136
|
+
/** Byte offset the content ends at; the route serves from `?offset=`. */
|
|
137
|
+
offset: number;
|
|
138
|
+
total_size: number;
|
|
139
|
+
/** False while the run is still producing; the content so far is still whole. */
|
|
140
|
+
done: boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** What `listCloudRuns` returns: the page, and whether `limit` cut it short. */
|
|
144
|
+
export interface CloudRunList {
|
|
145
|
+
runs: CloudRunSummary[];
|
|
146
|
+
/** True when Cloud had more rows than `limit` asked for. */
|
|
147
|
+
more: boolean;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A step name that can safely ride in a query string, and is worth asking about. */
|
|
151
|
+
const STEP_NAME = /^[^\u0000-\u001F\u007F]{1,200}$/u;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* How many run-list pages one `flows runs` may fetch.
|
|
155
|
+
*
|
|
156
|
+
* `limit` bounds the rows, but only a *cooperative* server bounds the pages:
|
|
157
|
+
* an empty page carrying a cursor advances neither, and a cursor that repeats
|
|
158
|
+
* advances nothing at all. Neither is caught by the per-request timeout, which
|
|
159
|
+
* bounds one request and not the command. Twenty pages is 2,000 rows at the
|
|
160
|
+
* route's page size — past any `limit` this client accepts — so reaching it
|
|
161
|
+
* means the listing is not progressing, not that the caller asked for a lot.
|
|
162
|
+
*/
|
|
163
|
+
const RUN_LIST_MAX_PAGES = 20;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A run id as the *caller* supplied it.
|
|
167
|
+
*
|
|
168
|
+
* `cloudRunId` applies the same shape rule to an id that came back from Cloud,
|
|
169
|
+
* where a violation means Cloud answered something this client cannot use, and
|
|
170
|
+
* `invalid_response` is right. Here the id is an argument someone typed: the
|
|
171
|
+
* same violation is a bad invocation, and the CLI has to exit 2 saying so
|
|
172
|
+
* rather than exit 1 blaming Cloud for a request it never sent.
|
|
173
|
+
*/
|
|
174
|
+
function callerRunId(value: string): string {
|
|
175
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/u.test(value)) {
|
|
176
|
+
throw new CloudFlowError('invalid_input',
|
|
177
|
+
'A run id may contain only letters, digits, underscores and hyphens, and at most 128 of them. '
|
|
178
|
+
+ '`flows runs` lists the ones this credential can read.');
|
|
179
|
+
}
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function get(record: Record<string, unknown>, key: string): unknown {
|
|
184
|
+
return record[key];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function str(value: unknown): string | null {
|
|
188
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function num(value: unknown): number | null {
|
|
192
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function record(value: unknown): Record<string, unknown> | null {
|
|
196
|
+
return isCloudRecord(value) ? value : null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function array(value: unknown): unknown[] {
|
|
200
|
+
return Array.isArray(value) ? value : [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The flow's name out of the bounded `workflow` projection.
|
|
205
|
+
*
|
|
206
|
+
* The list route ships `workflow` as a JSON *string* -- either a two-field
|
|
207
|
+
* object the SQL built (`{"name": ..., "description": ...}`) or, for a
|
|
208
|
+
* non-JSON source, a 4 KiB prefix of the source itself. Parsing is therefore
|
|
209
|
+
* allowed to fail, and a failure is not an error: the row is still a run, it
|
|
210
|
+
* just has no declared name to show.
|
|
211
|
+
*/
|
|
212
|
+
function workflowName(value: unknown): string {
|
|
213
|
+
const raw = str(value);
|
|
214
|
+
if (raw === null) return 'unnamed';
|
|
215
|
+
let parsed: unknown;
|
|
216
|
+
try {
|
|
217
|
+
parsed = JSON.parse(raw);
|
|
218
|
+
} catch {
|
|
219
|
+
return 'unnamed';
|
|
220
|
+
}
|
|
221
|
+
const object = record(parsed);
|
|
222
|
+
return object === null ? 'unnamed' : str(get(object, 'name')) ?? 'unnamed';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Only a literal GitHub pull request URL is surfaced.
|
|
227
|
+
*
|
|
228
|
+
* `pullRequestUrl` is whatever the run's callback put in `result`, which is
|
|
229
|
+
* flow-authored text. Cloud's own render layer applies this same constraint
|
|
230
|
+
* (`runPullRequestUrl` in lib/workflows/parked-run.ts); a CLI that printed the
|
|
231
|
+
* raw field would be a clickable line an authored flow controls.
|
|
232
|
+
*/
|
|
233
|
+
function pullRequestUrl(value: unknown): string | null {
|
|
234
|
+
const raw = str(value);
|
|
235
|
+
if (raw === null) return null;
|
|
236
|
+
return /^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/\d+$/u.test(raw) ? raw : null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* One list row.
|
|
241
|
+
*
|
|
242
|
+
* A row with no `runId` is not a row this client can do anything with, and
|
|
243
|
+
* dropping it quietly would make `runs 3` mean "three of the four Cloud sent".
|
|
244
|
+
* A count a reader cannot trust is worse than a refusal, so it refuses.
|
|
245
|
+
*/
|
|
246
|
+
function runSummary(row: Record<string, unknown>): CloudRunSummary {
|
|
247
|
+
const runId = str(get(row, 'runId'));
|
|
248
|
+
if (runId === null) throw new CloudFlowError('invalid_response', 'Cloud run list carried a row with no runId.');
|
|
249
|
+
return {
|
|
250
|
+
run_id: runId,
|
|
251
|
+
name: workflowName(get(row, 'workflow')),
|
|
252
|
+
status: str(get(row, 'status')) ?? 'unknown',
|
|
253
|
+
completion_reason: str(get(row, 'completionReason')),
|
|
254
|
+
created_at: str(get(row, 'createdAt')),
|
|
255
|
+
updated_at: str(get(row, 'updatedAt')),
|
|
256
|
+
pull_request_url: pullRequestUrl(get(row, 'pullRequestUrl')),
|
|
257
|
+
error: str(get(row, 'error')),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Recent runs the token can see, newest first.
|
|
263
|
+
*
|
|
264
|
+
* The route pages by opaque cursor and has no `limit` parameter (a page is
|
|
265
|
+
* `RUN_LIST_PAGE_SIZE` = 100 rows), so `limit` is honoured here: pages are
|
|
266
|
+
* followed only while fewer than `limit` rows are in hand, and `more` reports
|
|
267
|
+
* whether Cloud had further rows. A caller asking for 10 makes exactly one
|
|
268
|
+
* request.
|
|
269
|
+
*/
|
|
270
|
+
export async function listCloudRuns(
|
|
271
|
+
limit: number,
|
|
272
|
+
options: CloudConnectionOptions = {},
|
|
273
|
+
): Promise<CloudRunList> {
|
|
274
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000) {
|
|
275
|
+
throw new CloudFlowError('invalid_input', 'runs --limit must be an integer between 1 and 1000.');
|
|
276
|
+
}
|
|
277
|
+
const runs: CloudRunSummary[] = [];
|
|
278
|
+
let cursor: string | null = null;
|
|
279
|
+
let more = false;
|
|
280
|
+
// Two independent bounds, because `limit` alone is not one: a page that
|
|
281
|
+
// yields no rows leaves `runs.length` where it was, so the loop's own exit
|
|
282
|
+
// condition can never be reached by fetching more.
|
|
283
|
+
const requested = new Set<string>();
|
|
284
|
+
for (let page = 1; ; page += 1) {
|
|
285
|
+
if (page > RUN_LIST_MAX_PAGES) {
|
|
286
|
+
throw new CloudFlowError('invalid_response',
|
|
287
|
+
`Cloud served ${RUN_LIST_MAX_PAGES} run-list pages without reaching the requested limit; the listing is not advancing.`);
|
|
288
|
+
}
|
|
289
|
+
if (cursor !== null) {
|
|
290
|
+
if (requested.has(cursor)) {
|
|
291
|
+
throw new CloudFlowError('invalid_response', 'Cloud repeated a run-list cursor; the listing is not advancing.');
|
|
292
|
+
}
|
|
293
|
+
requested.add(cursor);
|
|
294
|
+
}
|
|
295
|
+
const query = cursor === null ? '' : `?cursor=${encodeURIComponent(cursor)}`;
|
|
296
|
+
const body = record(await cloudFetch(`/api/v1/workflows/runs${query}`, options, { method: 'GET', detail: true }));
|
|
297
|
+
if (body === null) throw new CloudFlowError('invalid_response', 'Cloud returned no run list.');
|
|
298
|
+
// A missing or non-array `runs` is not an empty page. Reading it as one
|
|
299
|
+
// would print `runs 0` — a claim about the workspace — for a response this
|
|
300
|
+
// client did not understand.
|
|
301
|
+
const rows = get(body, 'runs');
|
|
302
|
+
if (!Array.isArray(rows)) throw new CloudFlowError('invalid_response', 'Cloud run list carried no `runs` array.');
|
|
303
|
+
for (const row of rows) {
|
|
304
|
+
const object = record(row);
|
|
305
|
+
if (object === null) throw new CloudFlowError('invalid_response', 'Cloud run list carried a row that is not an object.');
|
|
306
|
+
const parsed = runSummary(object);
|
|
307
|
+
if (runs.length === limit) { more = true; break; }
|
|
308
|
+
runs.push(parsed);
|
|
309
|
+
}
|
|
310
|
+
cursor = str(get(body, 'nextCursor'));
|
|
311
|
+
if (runs.length >= limit) { more = more || cursor !== null; break; }
|
|
312
|
+
if (cursor === null) break;
|
|
313
|
+
}
|
|
314
|
+
return { runs, more };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function authority(value: unknown): CloudRunDetail['authority'] {
|
|
318
|
+
const object = record(value);
|
|
319
|
+
if (object === null) return null;
|
|
320
|
+
const source = record(get(object, 'source'));
|
|
321
|
+
const artifact = record(get(object, 'artifact'));
|
|
322
|
+
const surface = source === null ? null : record(get(source, 'surface'));
|
|
323
|
+
return {
|
|
324
|
+
surface_version: surface === null ? null : str(get(surface, 'version')),
|
|
325
|
+
source_sha256: source === null ? null : str(get(source, 'sha256')),
|
|
326
|
+
artifact_sha256: artifact === null ? null : str(get(artifact, 'sha256')),
|
|
327
|
+
source_commit: artifact === null ? null : str(get(artifact, 'sourceCommit')),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** One hosted run's record. */
|
|
332
|
+
export async function getCloudRunDetail(
|
|
333
|
+
runId: string,
|
|
334
|
+
options: CloudConnectionOptions = {},
|
|
335
|
+
): Promise<CloudRunDetail> {
|
|
336
|
+
const asked = callerRunId(runId);
|
|
337
|
+
const body = record(await cloudFetch(`/api/v1/workflows/runs/${asked}`, options,
|
|
338
|
+
{ method: 'GET', detail: true }));
|
|
339
|
+
if (body === null) throw new CloudFlowError('invalid_response', 'Cloud returned no run record.');
|
|
340
|
+
// The record has to be the record that was asked for, and it has to say what
|
|
341
|
+
// the run is doing. `getCloudFlowRun` (cloud-run.ts) already refuses a
|
|
342
|
+
// mismatched id for the same reason: a view headed `RUN <some other id>`, or
|
|
343
|
+
// one that invented `unknown` for an absent status, states a fact nobody
|
|
344
|
+
// established. What is *not* checked is the status vocabulary — Cloud owns
|
|
345
|
+
// that list and adds to it, and a CLI that refused an unfamiliar status would
|
|
346
|
+
// go blind on the next one it ships.
|
|
347
|
+
const answered = str(get(body, 'runId'));
|
|
348
|
+
if (answered === null) throw new CloudFlowError('invalid_response', 'Cloud run record carried no runId.');
|
|
349
|
+
if (answered !== asked) {
|
|
350
|
+
throw new CloudFlowError('invalid_response', `Cloud answered a different run than the one requested (${asked}).`);
|
|
351
|
+
}
|
|
352
|
+
const status = str(get(body, 'status'));
|
|
353
|
+
if (status === null) throw new CloudFlowError('invalid_response', 'Cloud run record carried no status.');
|
|
354
|
+
const result = record(get(body, 'result'));
|
|
355
|
+
return {
|
|
356
|
+
run_id: answered,
|
|
357
|
+
name: workflowName(get(body, 'workflow')),
|
|
358
|
+
status,
|
|
359
|
+
// On the detail route the reason is inside the run's `result`, not lifted
|
|
360
|
+
// into a column the way the list's SQL lifts it.
|
|
361
|
+
completion_reason: result === null ? null : str(get(result, 'completionReason')),
|
|
362
|
+
created_at: str(get(body, 'createdAt')),
|
|
363
|
+
updated_at: str(get(body, 'updatedAt')),
|
|
364
|
+
pull_request_url: result === null ? null : pullRequestUrl(get(result, 'pullRequestUrl')),
|
|
365
|
+
error: str(get(body, 'error')),
|
|
366
|
+
completed_steps: result === null ? null : num(get(result, 'completedSteps')),
|
|
367
|
+
authority: authority(get(body, 'relayflowV2Authority')),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function transcript(value: unknown): CloudStepTranscript | null {
|
|
372
|
+
const object = record(value);
|
|
373
|
+
if (object === null) return null;
|
|
374
|
+
const file = record(get(object, 'file'));
|
|
375
|
+
const tools = record(get(object, 'tools'));
|
|
376
|
+
const result = record(get(object, 'result'));
|
|
377
|
+
const usage = result === null ? null : record(get(result, 'usage'));
|
|
378
|
+
const artifacts = record(get(object, 'artifacts'));
|
|
379
|
+
return {
|
|
380
|
+
attempt: num(get(object, 'attempt')),
|
|
381
|
+
model: result === null ? null : str(get(result, 'model')),
|
|
382
|
+
num_turns: result === null ? null : num(get(result, 'num_turns')),
|
|
383
|
+
total_cost_usd: result === null ? null : num(get(result, 'total_cost_usd')),
|
|
384
|
+
tokens_in: usage === null ? null : num(get(usage, 'input')),
|
|
385
|
+
tokens_out: usage === null ? null : num(get(usage, 'output')),
|
|
386
|
+
cache_read: usage === null ? null : num(get(usage, 'cache_read')),
|
|
387
|
+
cache_creation: usage === null ? null : num(get(usage, 'cache_creation')),
|
|
388
|
+
duration_ms: result === null ? null : num(get(result, 'duration_ms')),
|
|
389
|
+
subtype: result === null ? null : str(get(result, 'subtype')),
|
|
390
|
+
is_error: result !== null && get(result, 'is_error') === true,
|
|
391
|
+
exit_code: num(get(object, 'exit_code')),
|
|
392
|
+
bytes_kept: file === null ? null : num(get(file, 'bytes_kept')),
|
|
393
|
+
bytes_total: file === null ? null : num(get(file, 'bytes_total')),
|
|
394
|
+
frames_kept: file === null ? null : num(get(file, 'frames_kept')),
|
|
395
|
+
frames_total: file === null ? null : num(get(file, 'frames_total')),
|
|
396
|
+
truncated: file !== null && get(file, 'truncated') === true,
|
|
397
|
+
total_calls: tools === null ? null : num(get(tools, 'total_calls')),
|
|
398
|
+
tools: tools === null ? [] : array(get(tools, 'counts')).flatMap((entry) => {
|
|
399
|
+
const count = record(entry);
|
|
400
|
+
const name = count === null ? null : str(get(count, 'name'));
|
|
401
|
+
return name === null ? [] : [{ name, calls: num(get(count!, 'calls')), errors: num(get(count!, 'errors')) }];
|
|
402
|
+
}),
|
|
403
|
+
last_calls: tools === null ? [] : array(get(tools, 'last_calls')).flatMap((entry) => {
|
|
404
|
+
const call = record(entry);
|
|
405
|
+
const name = call === null ? null : str(get(call, 'name'));
|
|
406
|
+
return name === null ? [] : [{
|
|
407
|
+
name,
|
|
408
|
+
seq: num(get(call!, 'seq')),
|
|
409
|
+
result_bytes: num(get(call!, 'result_bytes')),
|
|
410
|
+
input_excerpt: str(get(call!, 'input_excerpt')),
|
|
411
|
+
}];
|
|
412
|
+
}),
|
|
413
|
+
artifacts: artifacts === null ? [] : array(get(artifacts, 'paths')).flatMap((path) => {
|
|
414
|
+
const text = str(path);
|
|
415
|
+
return text === null ? [] : [text];
|
|
416
|
+
}),
|
|
417
|
+
artifact_count: artifacts === null ? null : num(get(artifacts, 'count')),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** One step row. Nameless is unusable, and dropping it would shorten the run. */
|
|
422
|
+
function step(row: Record<string, unknown>): CloudStep {
|
|
423
|
+
const name = str(get(row, 'stepName'));
|
|
424
|
+
if (name === null) throw new CloudFlowError('invalid_response', 'Cloud step list carried a row with no stepName.');
|
|
425
|
+
const detail = record(get(row, 'detail'));
|
|
426
|
+
const verification = detail === null ? null : record(get(detail, 'verification'));
|
|
427
|
+
return {
|
|
428
|
+
step_name: name,
|
|
429
|
+
step_type: str(get(row, 'stepType')) ?? 'unknown',
|
|
430
|
+
status: str(get(row, 'status')) ?? 'unknown',
|
|
431
|
+
completion_reason: str(get(row, 'completionReason')),
|
|
432
|
+
cli: str(get(row, 'cli')),
|
|
433
|
+
model: str(get(row, 'model')),
|
|
434
|
+
sandbox_id: str(get(row, 'sandboxId')) ?? '',
|
|
435
|
+
started_at: str(get(row, 'startTime')),
|
|
436
|
+
ended_at: str(get(row, 'endTime')),
|
|
437
|
+
// `wallclockMs` is the kernel's own measure; `durationMs` is Cloud's
|
|
438
|
+
// subtraction of the two timestamps. Prefer the kernel's when it is there.
|
|
439
|
+
duration_ms: (detail === null ? null : num(get(detail, 'wallclockMs'))) ?? num(get(row, 'durationMs')),
|
|
440
|
+
exit_code: num(get(row, 'exitCode')),
|
|
441
|
+
retry_count: num(get(row, 'retryCount')),
|
|
442
|
+
tokens_in: num(get(row, 'tokensInput')),
|
|
443
|
+
tokens_out: num(get(row, 'tokensOutput')),
|
|
444
|
+
cost_usd: num(get(row, 'costUsd')),
|
|
445
|
+
output_summary: str(get(row, 'outputSummary')),
|
|
446
|
+
gate: verification === null ? null : {
|
|
447
|
+
gate: str(get(verification, 'gate')) ?? 'unknown',
|
|
448
|
+
verdict: str(get(verification, 'verdict')) ?? 'unknown',
|
|
449
|
+
detail: str(get(verification, 'detail')) ?? '',
|
|
450
|
+
},
|
|
451
|
+
attempts: detail === null ? [] : array(get(detail, 'attempts')).flatMap((entry) => {
|
|
452
|
+
const attempt = record(entry);
|
|
453
|
+
return attempt === null ? [] : [{
|
|
454
|
+
attempt: num(get(attempt, 'attempt')),
|
|
455
|
+
disposition: str(get(attempt, 'disposition')),
|
|
456
|
+
completion_reason: str(get(attempt, 'completionReason')),
|
|
457
|
+
at_ms: num(get(attempt, 'atMs')),
|
|
458
|
+
transcript_bytes: num(get(attempt, 'transcriptBytes')),
|
|
459
|
+
}];
|
|
460
|
+
}),
|
|
461
|
+
transcript: detail === null ? null : transcript(get(detail, 'transcript')),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** One hosted run's per-step rows, in execution order. */
|
|
466
|
+
export async function getCloudRunSteps(
|
|
467
|
+
runId: string,
|
|
468
|
+
options: CloudConnectionOptions = {},
|
|
469
|
+
): Promise<CloudStep[]> {
|
|
470
|
+
const body = record(await cloudFetch(`/api/v1/workflows/runs/${callerRunId(runId)}/steps`, options,
|
|
471
|
+
{ method: 'GET', detail: true }));
|
|
472
|
+
if (body === null) throw new CloudFlowError('invalid_response', 'Cloud returned no step list.');
|
|
473
|
+
// As with the run list: a step silently dropped is a run that looks shorter
|
|
474
|
+
// than it was, and `flows logs --step` would then name the wrong candidates.
|
|
475
|
+
const rows = get(body, 'steps');
|
|
476
|
+
if (!Array.isArray(rows)) throw new CloudFlowError('invalid_response', 'Cloud step list carried no `steps` array.');
|
|
477
|
+
return rows.map((row) => {
|
|
478
|
+
const object = record(row);
|
|
479
|
+
if (object === null) throw new CloudFlowError('invalid_response', 'Cloud step list carried a row that is not an object.');
|
|
480
|
+
return step(object);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* The runner log, or one step's agent transcript.
|
|
486
|
+
*
|
|
487
|
+
* With no `step` this is the sandbox's own runner output. With one it is that
|
|
488
|
+
* step's transcript as JSONL -- the route keys it by the step's `sandboxId`,
|
|
489
|
+
* which for an agent step is the step name.
|
|
490
|
+
*
|
|
491
|
+
* One request: the route serves from `offset` to the end with no chunk cap, so
|
|
492
|
+
* there is no loop here and nothing polls. `done: false` means the run is
|
|
493
|
+
* still producing, and the caller is told rather than made to wait.
|
|
494
|
+
*/
|
|
495
|
+
export async function getCloudRunLog(
|
|
496
|
+
runId: string,
|
|
497
|
+
step: string | undefined,
|
|
498
|
+
options: CloudConnectionOptions = {},
|
|
499
|
+
): Promise<CloudRunLog> {
|
|
500
|
+
if (step !== undefined && !STEP_NAME.test(step)) {
|
|
501
|
+
throw new CloudFlowError('invalid_input', 'A step name must be 1-200 characters and carry no control characters.');
|
|
502
|
+
}
|
|
503
|
+
const query = step === undefined ? '' : `?sandboxId=${encodeURIComponent(step)}`;
|
|
504
|
+
const body = record(await cloudFetch(`/api/v1/workflows/runs/${callerRunId(runId)}/logs${query}`, options,
|
|
505
|
+
{ method: 'GET', detail: true }));
|
|
506
|
+
if (body === null) throw new CloudFlowError('invalid_response', 'Cloud returned no log envelope.');
|
|
507
|
+
const content = get(body, 'content');
|
|
508
|
+
if (typeof content !== 'string') throw new CloudFlowError('invalid_response', 'Cloud log envelope carried no content.');
|
|
509
|
+
return {
|
|
510
|
+
content,
|
|
511
|
+
offset: num(get(body, 'offset')) ?? content.length,
|
|
512
|
+
total_size: num(get(body, 'totalSize')) ?? content.length,
|
|
513
|
+
done: get(body, 'done') === true,
|
|
514
|
+
};
|
|
515
|
+
}
|