@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,518 @@
|
|
|
1
|
+
// `flows runs`, `flows logs` and `flows status --cloud`: reading a hosted run.
|
|
2
|
+
//
|
|
3
|
+
// The local `flows status` (cli/status.ts) answers "what is this run doing"
|
|
4
|
+
// from a journal on the same disk. These three answer the same question about
|
|
5
|
+
// a run that executed in Cloud, for an agent holding its own user's Cloud
|
|
6
|
+
// credential -- so it does not have to hand-roll HTTP, learn four route
|
|
7
|
+
// shapes, or decide for itself what a 404 means.
|
|
8
|
+
//
|
|
9
|
+
// Presentation only. `cloud-read.ts` owns the requests and the field names;
|
|
10
|
+
// `cloud-transcript.ts` owns the JSONL vocabulary; this module owns the page
|
|
11
|
+
// and the refusals, and deliberately renders in the grammar cli/status.ts
|
|
12
|
+
// already uses (`RUN` / `steps N` / one glyph-led line per step, indented
|
|
13
|
+
// facts beneath it) so a reader who knows one knows the other.
|
|
14
|
+
|
|
15
|
+
import { canonicalize } from '../canonical.js';
|
|
16
|
+
import { CloudFlowError, type CloudConnectionOptions } from '../cloud-http.js';
|
|
17
|
+
import {
|
|
18
|
+
getCloudRunDetail, getCloudRunLog, getCloudRunSteps, listCloudRuns,
|
|
19
|
+
type CloudRunDetail, type CloudStep,
|
|
20
|
+
} from '../cloud-read.js';
|
|
21
|
+
import { parseAgentTranscript, renderAgentTranscript } from '../cloud-transcript.js';
|
|
22
|
+
import { redact } from '../redact.js';
|
|
23
|
+
import { formatDuration } from './status.js';
|
|
24
|
+
import type { CliIo } from '../cli.js';
|
|
25
|
+
|
|
26
|
+
export interface RunsArgs {
|
|
27
|
+
command: 'runs';
|
|
28
|
+
limit: number;
|
|
29
|
+
json: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface LogsArgs {
|
|
33
|
+
command: 'logs';
|
|
34
|
+
runId: string;
|
|
35
|
+
step: string | undefined;
|
|
36
|
+
raw: boolean;
|
|
37
|
+
json: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Enough runs to recognise the one you mean; `--limit` raises it. */
|
|
41
|
+
export const DEFAULT_RUN_LIMIT = 20;
|
|
42
|
+
|
|
43
|
+
/** Injected by tests; production takes the ambient clock and environment. */
|
|
44
|
+
export interface CloudReadOptions extends CloudConnectionOptions {
|
|
45
|
+
env?: NodeJS.ProcessEnv;
|
|
46
|
+
now?: () => number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseRunsArgs(args: readonly string[]): RunsArgs | undefined {
|
|
50
|
+
let limit: number | undefined;
|
|
51
|
+
let json = false;
|
|
52
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
53
|
+
const argument = args[index]!;
|
|
54
|
+
if (argument === '--json') {
|
|
55
|
+
if (json) return undefined;
|
|
56
|
+
json = true;
|
|
57
|
+
} else if (argument === '--limit') {
|
|
58
|
+
const value = args[++index];
|
|
59
|
+
if (limit !== undefined || value === undefined || !/^\d{1,4}$/u.test(value) || Number(value) < 1) return undefined;
|
|
60
|
+
limit = Number(value);
|
|
61
|
+
} else {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { command: 'runs', limit: limit ?? DEFAULT_RUN_LIMIT, json };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function parseLogsArgs(args: readonly string[]): LogsArgs | undefined {
|
|
69
|
+
let step: string | undefined;
|
|
70
|
+
let raw = false;
|
|
71
|
+
let json = false;
|
|
72
|
+
const positionals: string[] = [];
|
|
73
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
74
|
+
const argument = args[index]!;
|
|
75
|
+
if (argument === '--json') {
|
|
76
|
+
if (json) return undefined;
|
|
77
|
+
json = true;
|
|
78
|
+
} else if (argument === '--raw') {
|
|
79
|
+
if (raw) return undefined;
|
|
80
|
+
raw = true;
|
|
81
|
+
} else if (argument === '--step') {
|
|
82
|
+
const value = args[++index];
|
|
83
|
+
if (step !== undefined || value === undefined || value.length === 0 || value.startsWith('-')) return undefined;
|
|
84
|
+
step = value;
|
|
85
|
+
} else if (argument.startsWith('-')) {
|
|
86
|
+
return undefined;
|
|
87
|
+
} else {
|
|
88
|
+
positionals.push(argument);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (positionals.length !== 1) return undefined;
|
|
92
|
+
return { command: 'logs', runId: positionals[0]!, step, raw, json };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Every way these verbs can refuse, as a code and a sentence.
|
|
97
|
+
*
|
|
98
|
+
* Each one names the thing to do next. A missing credential names
|
|
99
|
+
* `agent-relay cloud login` because that is the command that fixes it; a 404
|
|
100
|
+
* names `flows runs` because the usual cause is a run id from another
|
|
101
|
+
* workspace, and the list is how you find yours.
|
|
102
|
+
*/
|
|
103
|
+
interface Refusal { code: string; message: string; exit: 1 | 2 }
|
|
104
|
+
|
|
105
|
+
function refusalFor(error: unknown, subject: string, env: NodeJS.ProcessEnv): Refusal {
|
|
106
|
+
const clean = (message: string): string => redact(message, env);
|
|
107
|
+
if (error instanceof CloudFlowError) {
|
|
108
|
+
if (error.code === 'configuration') {
|
|
109
|
+
switch (error.reason) {
|
|
110
|
+
case 'auth_missing':
|
|
111
|
+
return {
|
|
112
|
+
code: 'cloud_auth_missing', exit: 2,
|
|
113
|
+
message: 'No Cloud credential. Sign in with `agent-relay cloud login`, or set FLOWS_CLOUD_TOKEN to a '
|
|
114
|
+
+ 'Cloud API token with workflow:runs:read (and workflow:logs:read for `flows logs`).',
|
|
115
|
+
};
|
|
116
|
+
case 'auth_expired':
|
|
117
|
+
return { code: 'cloud_auth_expired', exit: 2, message: clean(error.message) };
|
|
118
|
+
case 'url_mismatch':
|
|
119
|
+
return { code: 'cloud_url_mismatch', exit: 2, message: clean(error.message) };
|
|
120
|
+
default:
|
|
121
|
+
return { code: 'cloud_configuration', exit: 2, message: clean(error.message) };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (error.code === 'http_error') {
|
|
125
|
+
if (error.status === 401) {
|
|
126
|
+
return {
|
|
127
|
+
code: 'cloud_auth_rejected', exit: 2,
|
|
128
|
+
message: 'Cloud rejected the credential (HTTP 401). The token is unknown or revoked; '
|
|
129
|
+
+ 'sign in again with `agent-relay cloud login`.',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (error.status === 403) {
|
|
133
|
+
return {
|
|
134
|
+
code: 'cloud_forbidden', exit: 2,
|
|
135
|
+
message: `The credential is valid but not allowed to read ${subject} (HTTP 403). `
|
|
136
|
+
+ 'A run-scoped sandbox token may only read its own run; a workspace token needs '
|
|
137
|
+
+ 'workflow:runs:read, and workflow:logs:read or workflow:invoke:read for logs.',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (error.status === 404) {
|
|
141
|
+
return {
|
|
142
|
+
code: 'cloud_run_not_found', exit: 2,
|
|
143
|
+
message: `Cloud has no ${subject} visible to this credential (HTTP 404). `
|
|
144
|
+
+ 'It may belong to another workspace; `flows runs` lists the ones this credential can read.',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return { code: 'cloud_http_error', exit: 1, message: clean(error.message) };
|
|
148
|
+
}
|
|
149
|
+
if (error.code === 'invalid_input') return { code: 'invalid_invocation', exit: 2, message: clean(error.message) };
|
|
150
|
+
if (error.code === 'transient_error' || error.code === 'transport_error') {
|
|
151
|
+
return { code: error.code === 'transient_error' ? 'cloud_unreachable' : 'cloud_transport_failed', exit: 1, message: clean(error.message) };
|
|
152
|
+
}
|
|
153
|
+
return { code: 'cloud_invalid_response', exit: 1, message: clean(error.message) };
|
|
154
|
+
}
|
|
155
|
+
return { code: 'cloud_read_failed', exit: 1, message: clean(error instanceof Error ? error.message : String(error)) };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function fail(refusal: Refusal, json: boolean, io: CliIo): 1 | 2 {
|
|
159
|
+
if (json) io.stdout(canonicalize({ v: 1, ok: false, code: refusal.code, message: refusal.message }));
|
|
160
|
+
else io.stderr(`REFUSED [${refusal.code}] ${refusal.message}`);
|
|
161
|
+
return refusal.exit;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function thousands(value: number): string {
|
|
165
|
+
return String(value).replace(/\B(?=(\d{3})+(?!\d))/gu, ',');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function dollars(value: number): string {
|
|
169
|
+
return `$${value.toFixed(6).replace(/0+$/u, '').replace(/\.$/u, '')}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Agent- and flow-authored names cannot inject terminal control sequences. */
|
|
173
|
+
function safe(text: string): string {
|
|
174
|
+
return text.replace(/[\u0000-\u001F\u007F-\u009F]/gu, '?');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Render a run's `error` as readable lines rather than one control-char smear.
|
|
179
|
+
*
|
|
180
|
+
* Cloud stores the runner's terminal output in this field, newlines and all, so
|
|
181
|
+
* passing it through `safe()` alone turns a 200-line tail into a single line of
|
|
182
|
+
* `?` separators. Long runs are dominated by lease renewals — one line every
|
|
183
|
+
* 10s for the life of every agent step, differing only in the deadline — which
|
|
184
|
+
* are worth counting, not reading.
|
|
185
|
+
*
|
|
186
|
+
* Collapsing is deliberately narrow: two adjacent lines merge only when they
|
|
187
|
+
* are character-for-character identical once a trailing integer is masked. A
|
|
188
|
+
* shared prefix is NOT line identity — runner lines put the step name, reason
|
|
189
|
+
* or message after a long fixed prefix, so collapsing on a prefix would hide
|
|
190
|
+
* distinct diagnostics behind a similarity count. The final line never
|
|
191
|
+
* collapses into an earlier one, because that is where the failure is.
|
|
192
|
+
*/
|
|
193
|
+
const TRAILING_NUMBER = /\d+(?=\D{0,2}$)/u;
|
|
194
|
+
|
|
195
|
+
export function errorLines(text: string, indent: string): string[] {
|
|
196
|
+
const raw = text.split(/\r\n|\r|\n/u).map((line) => line.trimEnd()).filter((line) => line !== '');
|
|
197
|
+
if (raw.length === 0) return [];
|
|
198
|
+
|
|
199
|
+
const key = (line: string): string => line.replace(TRAILING_NUMBER, '#');
|
|
200
|
+
const collapsed: { line: string; count: number }[] = [];
|
|
201
|
+
raw.forEach((line, index) => {
|
|
202
|
+
const previous = collapsed.at(-1);
|
|
203
|
+
const isLast = index === raw.length - 1;
|
|
204
|
+
if (previous !== undefined && !isLast && key(previous.line) === key(line)) previous.count += 1;
|
|
205
|
+
else collapsed.push({ line, count: 1 });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const rendered = collapsed.map(({ line, count }) =>
|
|
209
|
+
count === 1 ? safe(line) : `${safe(line)} (${count} times, differing only in a number)`);
|
|
210
|
+
|
|
211
|
+
const HEAD = 2;
|
|
212
|
+
const TAIL = 12;
|
|
213
|
+
if (rendered.length <= HEAD + TAIL + 1) return rendered.map((line) => `${indent}${line}`);
|
|
214
|
+
const elided = rendered.length - HEAD - TAIL;
|
|
215
|
+
return [
|
|
216
|
+
...rendered.slice(0, HEAD),
|
|
217
|
+
`… ${elided} more line${elided === 1 ? '' : 's'} (full text: --json)`,
|
|
218
|
+
...rendered.slice(-TAIL),
|
|
219
|
+
].map((line) => `${indent}${line}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** ISO-8601 to the second: a list column, not a timestamp to do arithmetic on. */
|
|
223
|
+
function instant(value: string | null): string {
|
|
224
|
+
if (value === null) return 'unknown';
|
|
225
|
+
const parsed = Date.parse(value);
|
|
226
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString().replace(/\.\d{3}Z$/u, 'Z') : 'unknown';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function ago(value: string | null, now: number): string {
|
|
230
|
+
if (value === null) return 'unknown';
|
|
231
|
+
const parsed = Date.parse(value);
|
|
232
|
+
return Number.isFinite(parsed) ? `${formatDuration(now - parsed)} ago` : 'unknown';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---------------------------------------------------------------- flows runs
|
|
236
|
+
|
|
237
|
+
export async function runCloudRunsCli(
|
|
238
|
+
args: RunsArgs,
|
|
239
|
+
io: CliIo,
|
|
240
|
+
options: CloudReadOptions = {},
|
|
241
|
+
): Promise<0 | 1 | 2> {
|
|
242
|
+
const env = options.env ?? process.env;
|
|
243
|
+
let page;
|
|
244
|
+
try {
|
|
245
|
+
page = await listCloudRuns(args.limit, options);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
return fail(refusalFor(error, 'the run list', env), args.json, io);
|
|
248
|
+
}
|
|
249
|
+
const runs = page.runs.map((run) => ({
|
|
250
|
+
...run,
|
|
251
|
+
name: redact(run.name, env),
|
|
252
|
+
error: run.error === null ? null : redact(run.error, env),
|
|
253
|
+
}));
|
|
254
|
+
if (args.json) {
|
|
255
|
+
io.stdout(canonicalize({ v: 1, ok: true, runs, more: page.more }));
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
io.stdout(`runs ${runs.length}${page.more ? ` (more available; pass --limit)` : ''}`);
|
|
259
|
+
if (runs.length === 0) {
|
|
260
|
+
io.stdout(' no runs visible to this credential');
|
|
261
|
+
return 0;
|
|
262
|
+
}
|
|
263
|
+
const nameWidth = Math.max(4, ...runs.map((run) => Math.min(32, run.name.length)));
|
|
264
|
+
const statusWidth = Math.max(6, ...runs.map((run) => run.status.length));
|
|
265
|
+
for (const run of runs) {
|
|
266
|
+
const cells = [
|
|
267
|
+
run.run_id,
|
|
268
|
+
safe(run.name).slice(0, 32).padEnd(nameWidth),
|
|
269
|
+
run.status.padEnd(statusWidth),
|
|
270
|
+
run.completion_reason === null ? null : safe(run.completion_reason),
|
|
271
|
+
`started ${instant(run.created_at)}`,
|
|
272
|
+
`updated ${instant(run.updated_at)}`,
|
|
273
|
+
].filter((cell): cell is string => cell !== null);
|
|
274
|
+
io.stdout(`RUN ${cells.join(' ')}`);
|
|
275
|
+
if (run.pull_request_url !== null) io.stdout(` pr ${run.pull_request_url}`);
|
|
276
|
+
if (run.error !== null) {
|
|
277
|
+
io.stdout(' error');
|
|
278
|
+
for (const line of errorLines(run.error, ' ')) io.stdout(line);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ---------------------------------------------------------------- flows logs
|
|
285
|
+
|
|
286
|
+
export async function runCloudLogsCli(
|
|
287
|
+
args: LogsArgs,
|
|
288
|
+
io: CliIo,
|
|
289
|
+
options: CloudReadOptions = {},
|
|
290
|
+
): Promise<0 | 1 | 2> {
|
|
291
|
+
const env = options.env ?? process.env;
|
|
292
|
+
const subject = args.step === undefined ? `run ${args.runId}` : `step "${args.step}" of run ${args.runId}`;
|
|
293
|
+
let log;
|
|
294
|
+
try {
|
|
295
|
+
log = await getCloudRunLog(args.runId, args.step, options);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
return fail(refusalFor(error, subject, env), args.json, io);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// The logs route answers 200 with an empty body for a sandbox id it does not
|
|
301
|
+
// know, so an unknown `--step` is indistinguishable from a step that has
|
|
302
|
+
// written nothing -- from this response alone. The step list distinguishes
|
|
303
|
+
// them, and is only fetched on this path, so the happy path stays one request.
|
|
304
|
+
if (args.step !== undefined && log.content.length === 0) {
|
|
305
|
+
let steps: CloudStep[];
|
|
306
|
+
try {
|
|
307
|
+
steps = await getCloudRunSteps(args.runId, options);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
// The step list is the only evidence for either message, so without it
|
|
310
|
+
// there is no finding to report. A 403 or a timed-out read must reach
|
|
311
|
+
// the caller as what it is — retryable, or a scope to fix — and never
|
|
312
|
+
// as `cloud_step_no_transcript`, which says the invocation was wrong.
|
|
313
|
+
return fail(refusalFor(error, `the steps of run ${args.runId}`, env), args.json, io);
|
|
314
|
+
}
|
|
315
|
+
const named = steps.some((step) => step.sandbox_id === args.step || step.step_name === args.step);
|
|
316
|
+
const withLogs = steps.filter((step) => step.sandbox_id.length > 0).map((step) => step.sandbox_id);
|
|
317
|
+
const available = withLogs.length === 0
|
|
318
|
+
? 'this run has no step with its own transcript'
|
|
319
|
+
: `steps with a transcript: ${withLogs.map(safe).join(', ')}`;
|
|
320
|
+
return fail({
|
|
321
|
+
code: 'cloud_step_no_transcript', exit: 2,
|
|
322
|
+
message: named
|
|
323
|
+
? `Step "${safe(args.step)}" of run ${args.runId} has no transcript${log.done ? '' : ' yet'}. `
|
|
324
|
+
+ 'Only an agent step writes one; a deterministic step has none.'
|
|
325
|
+
: `Run ${args.runId} has no step "${safe(args.step)}" — ${available}.`,
|
|
326
|
+
}, args.json, io);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const content = redact(log.content, env);
|
|
330
|
+
if (args.json) {
|
|
331
|
+
const parsed = args.step === undefined || args.raw ? undefined : parseAgentTranscript(log.content, env);
|
|
332
|
+
io.stdout(canonicalize({
|
|
333
|
+
v: 1, ok: true, run_id: args.runId, step: args.step ?? null,
|
|
334
|
+
bytes: log.offset, total_bytes: log.total_size, done: log.done,
|
|
335
|
+
...(parsed === undefined ? { content } : { entries: parsed.entries as unknown }),
|
|
336
|
+
}));
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const where = args.step === undefined ? 'runner' : `step ${safe(args.step)}`;
|
|
341
|
+
io.stdout(`LOG ${args.runId} ${where} ${thousands(log.total_size)} bytes ${log.done ? 'complete' : 'still running'}`);
|
|
342
|
+
if (content.length === 0) {
|
|
343
|
+
io.stdout(' (empty)');
|
|
344
|
+
return 0;
|
|
345
|
+
}
|
|
346
|
+
// `--raw` means unrendered, not unredacted: a credential an agent echoed
|
|
347
|
+
// into its transcript must not reach a terminal because a flag was passed.
|
|
348
|
+
if (args.raw || args.step === undefined) {
|
|
349
|
+
for (const line of content.split('\n')) io.stdout(line);
|
|
350
|
+
return 0;
|
|
351
|
+
}
|
|
352
|
+
const parsed = parseAgentTranscript(log.content, env);
|
|
353
|
+
if (!parsed.stream_json) {
|
|
354
|
+
io.stdout(' not a stream-json transcript; printed as written');
|
|
355
|
+
for (const line of content.split('\n')) io.stdout(line);
|
|
356
|
+
return 0;
|
|
357
|
+
}
|
|
358
|
+
for (const line of renderAgentTranscript(parsed)) io.stdout(line);
|
|
359
|
+
return 0;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// -------------------------------------------------------- flows status --cloud
|
|
363
|
+
|
|
364
|
+
const GLYPH: Record<string, string> = {
|
|
365
|
+
completed: '✓', succeeded: '✓', failed: '✗', cancelled: '✗', running: '↻', pending: '○', queued: '○',
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
function glyph(step: CloudStep): string {
|
|
369
|
+
if (step.status === 'completed' && step.completion_reason !== null && step.completion_reason !== 'success') return '✗';
|
|
370
|
+
return GLYPH[step.status] ?? '○';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** The `steps N: 3 done · 1 running` line, counted off whatever Cloud called each status. */
|
|
374
|
+
function stepCounts(steps: readonly CloudStep[]): string {
|
|
375
|
+
const counts = new Map<string, number>();
|
|
376
|
+
for (const step of steps) counts.set(step.status, (counts.get(step.status) ?? 0) + 1);
|
|
377
|
+
const parts = [...counts].map(([status, count]) => `${count} ${status}`);
|
|
378
|
+
return `steps ${steps.length}${parts.length === 0 ? '' : `: ${parts.join(' · ')}`}`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function renderStep(step: CloudStep, runId: string, idWidth: number): string[] {
|
|
382
|
+
const cells = [
|
|
383
|
+
safe(step.step_name).slice(0, 24).padEnd(idWidth),
|
|
384
|
+
step.step_type.padEnd(13),
|
|
385
|
+
step.status.padEnd(11),
|
|
386
|
+
];
|
|
387
|
+
const attempts = step.attempts.length;
|
|
388
|
+
if (attempts > 0) cells.push(`${attempts} attempt${attempts === 1 ? '' : 's'}`);
|
|
389
|
+
if (step.duration_ms !== null) cells.push(formatDuration(step.duration_ms));
|
|
390
|
+
if (step.completion_reason !== null) cells.push(safe(step.completion_reason));
|
|
391
|
+
if (step.gate !== null) cells.push(`gate: ${safe(step.gate.gate)} ${safe(step.gate.verdict)}`);
|
|
392
|
+
const lines = [` ${glyph(step)} ${cells.join(' ')}`.trimEnd()];
|
|
393
|
+
|
|
394
|
+
if (step.gate !== null && step.gate.verdict !== 'pass' && step.gate.detail.length > 0) {
|
|
395
|
+
lines.push(` gate: ${safe(step.gate.gate)} ${step.gate.verdict.toUpperCase()} — ${JSON.stringify(safe(step.gate.detail))}`);
|
|
396
|
+
}
|
|
397
|
+
const transcript = step.transcript;
|
|
398
|
+
if (transcript !== null) {
|
|
399
|
+
const facts = [
|
|
400
|
+
transcript.model === null ? null : safe(transcript.model),
|
|
401
|
+
transcript.num_turns === null ? null : `${transcript.num_turns} turn${transcript.num_turns === 1 ? '' : 's'}`,
|
|
402
|
+
transcript.total_calls === null ? null : `${transcript.total_calls} tool call${transcript.total_calls === 1 ? '' : 's'}`,
|
|
403
|
+
transcript.total_cost_usd === null ? null : dollars(transcript.total_cost_usd),
|
|
404
|
+
].filter((fact): fact is string => fact !== null);
|
|
405
|
+
lines.push(` transcript (attempt ${transcript.attempt ?? step.attempts.length})${facts.length === 0 ? '' : `: ${facts.join(' · ')}`}`);
|
|
406
|
+
if (transcript.frames_total !== null || transcript.bytes_total !== null) {
|
|
407
|
+
const frames = transcript.frames_total === null ? ''
|
|
408
|
+
: `${thousands(transcript.frames_kept ?? transcript.frames_total)} of ${thousands(transcript.frames_total)} frames`;
|
|
409
|
+
const bytes = transcript.bytes_total === null ? '' : `${thousands(transcript.bytes_total)} bytes`;
|
|
410
|
+
const cut = transcript.truncated ? ', truncated' : '';
|
|
411
|
+
lines.push(` ${[frames, bytes].filter((part) => part.length > 0).join(', ')}${cut}`);
|
|
412
|
+
}
|
|
413
|
+
if (transcript.tokens_in !== null || transcript.tokens_out !== null) {
|
|
414
|
+
const cache = [
|
|
415
|
+
transcript.cache_read === null ? null : `${thousands(transcript.cache_read)} cache read`,
|
|
416
|
+
transcript.cache_creation === null ? null : `${thousands(transcript.cache_creation)} cache write`,
|
|
417
|
+
].filter((part): part is string => part !== null);
|
|
418
|
+
lines.push(` ${thousands(transcript.tokens_in ?? 0)} in / ${thousands(transcript.tokens_out ?? 0)} out`
|
|
419
|
+
+ `${cache.length === 0 ? '' : ` · ${cache.join(' · ')}`}`);
|
|
420
|
+
}
|
|
421
|
+
if (transcript.tools.length > 0) {
|
|
422
|
+
lines.push(` tools: ${transcript.tools.map((tool) =>
|
|
423
|
+
`${safe(tool.name)} ×${tool.calls ?? 0}${tool.errors ? ` (${tool.errors} error${tool.errors === 1 ? '' : 's'})` : ''}`).join(', ')}`);
|
|
424
|
+
}
|
|
425
|
+
for (const call of transcript.last_calls) {
|
|
426
|
+
const excerpt = call.input_excerpt === null ? '' : ` ${safe(call.input_excerpt).slice(0, 120)}`;
|
|
427
|
+
lines.push(` call ${call.seq ?? '?'} ${safe(call.name)}${excerpt} → ${thousands(call.result_bytes ?? 0)} bytes`);
|
|
428
|
+
}
|
|
429
|
+
lines.push(` artifacts: ${transcript.artifacts.length === 0
|
|
430
|
+
? 'none' : transcript.artifacts.slice(0, 10).map(safe).join(', ')}`);
|
|
431
|
+
}
|
|
432
|
+
if (step.sandbox_id.length > 0) lines.push(` logs: flows logs ${runId} --step ${safe(step.sandbox_id)}`);
|
|
433
|
+
return lines;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function renderCloudStatus(run: CloudRunDetail, steps: readonly CloudStep[], now: number): string[] {
|
|
437
|
+
let tokensIn = 0;
|
|
438
|
+
let tokensOut = 0;
|
|
439
|
+
let cost = 0;
|
|
440
|
+
for (const step of steps) {
|
|
441
|
+
tokensIn += step.tokens_in ?? 0;
|
|
442
|
+
tokensOut += step.tokens_out ?? 0;
|
|
443
|
+
cost += step.cost_usd ?? 0;
|
|
444
|
+
}
|
|
445
|
+
// Summed from the step rows, because the run record carries no total. Cloud
|
|
446
|
+
// stores `costUsd` as a float, so this is a float sum — not the exact decimal
|
|
447
|
+
// addition the local journal fold does (run-state.ts `addDollars`). Shown to
|
|
448
|
+
// six places and labelled `spend`, the same word the local view uses.
|
|
449
|
+
const spend = `${thousands(tokensIn)} in / ${thousands(tokensOut)} out / ${dollars(cost)}`;
|
|
450
|
+
const finished = run.completion_reason === null ? '' : ` finished ${safe(run.completion_reason)}`;
|
|
451
|
+
const lines = [
|
|
452
|
+
`RUN ${run.run_id} ${safe(run.name)} ${run.status} started ${ago(run.created_at, now)}${finished} spend ${spend}`,
|
|
453
|
+
stepCounts(steps),
|
|
454
|
+
];
|
|
455
|
+
if (run.authority !== null) {
|
|
456
|
+
const facts = [
|
|
457
|
+
run.authority.surface_version === null ? null : `surface ${run.authority.surface_version}`,
|
|
458
|
+
run.authority.artifact_sha256 === null ? null : `artifact ${run.authority.artifact_sha256.slice(0, 12)}`,
|
|
459
|
+
run.authority.source_commit === null ? null : `commit ${run.authority.source_commit.slice(0, 12)}`,
|
|
460
|
+
].filter((fact): fact is string => fact !== null);
|
|
461
|
+
if (facts.length > 0) lines.push(`authority ${facts.join(' · ')}`);
|
|
462
|
+
}
|
|
463
|
+
if (run.pull_request_url !== null) lines.push(`pr ${run.pull_request_url}`);
|
|
464
|
+
if (run.error !== null) lines.push('error', ...errorLines(run.error, ' '));
|
|
465
|
+
lines.push('');
|
|
466
|
+
const idWidth = Math.max(4, ...steps.map((step) => Math.min(24, step.step_name.length)));
|
|
467
|
+
for (const step of steps) lines.push(...renderStep(step, run.run_id, idWidth));
|
|
468
|
+
return lines;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export async function runCloudStatusCli(
|
|
472
|
+
args: { runId?: string; json: boolean },
|
|
473
|
+
io: CliIo,
|
|
474
|
+
options: CloudReadOptions = {},
|
|
475
|
+
): Promise<0 | 1 | 2> {
|
|
476
|
+
const env = options.env ?? process.env;
|
|
477
|
+
const now = (options.now ?? Date.now)();
|
|
478
|
+
if (args.runId === undefined || args.runId.length === 0) {
|
|
479
|
+
return fail({
|
|
480
|
+
code: 'run_unknown', exit: 2,
|
|
481
|
+
message: '`flows status --cloud` needs the hosted run id; `flows runs` lists them. '
|
|
482
|
+
+ '(Without --cloud the run id defaults to RELAYFLOW_RUN_ID inside a step.)',
|
|
483
|
+
}, args.json, io);
|
|
484
|
+
}
|
|
485
|
+
let run: CloudRunDetail;
|
|
486
|
+
let steps: CloudStep[];
|
|
487
|
+
try {
|
|
488
|
+
run = await getCloudRunDetail(args.runId, options);
|
|
489
|
+
steps = await getCloudRunSteps(args.runId, options);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
return fail(refusalFor(error, `run ${args.runId}`, env), args.json, io);
|
|
492
|
+
}
|
|
493
|
+
const scrubbed: CloudRunDetail = {
|
|
494
|
+
...run,
|
|
495
|
+
name: redact(run.name, env),
|
|
496
|
+
error: run.error === null ? null : redact(run.error, env),
|
|
497
|
+
};
|
|
498
|
+
const scrubbedSteps = steps.map((step) => ({
|
|
499
|
+
...step,
|
|
500
|
+
output_summary: step.output_summary === null ? null : redact(step.output_summary, env),
|
|
501
|
+
gate: step.gate === null ? null : { ...step.gate, detail: redact(step.gate.detail, env) },
|
|
502
|
+
transcript: step.transcript === null ? null : {
|
|
503
|
+
...step.transcript,
|
|
504
|
+
model: step.transcript.model === null ? null : redact(step.transcript.model, env),
|
|
505
|
+
last_calls: step.transcript.last_calls.map((call) => ({
|
|
506
|
+
...call,
|
|
507
|
+
input_excerpt: call.input_excerpt === null ? null : redact(call.input_excerpt, env),
|
|
508
|
+
})),
|
|
509
|
+
artifacts: step.transcript.artifacts.map((path) => redact(path, env)),
|
|
510
|
+
},
|
|
511
|
+
}));
|
|
512
|
+
if (args.json) {
|
|
513
|
+
io.stdout(canonicalize({ v: 1, ok: true, run: scrubbed, steps: scrubbedSteps, now_ms: now }));
|
|
514
|
+
return 0;
|
|
515
|
+
}
|
|
516
|
+
for (const line of renderCloudStatus(scrubbed, scrubbedSteps, now)) io.stdout(line);
|
|
517
|
+
return 0;
|
|
518
|
+
}
|