@relayflows/sdk 2.0.21 → 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/agent-transcript.d.ts +186 -0
- package/dist/agent-transcript.d.ts.map +1 -0
- package/dist/agent-transcript.js +585 -0
- package/dist/agent-transcript.js.map +1 -0
- 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 -24
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/status.d.ts +43 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +321 -0
- package/dist/cli/status.js.map +1 -0
- 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 +99 -10
- package/dist/cli/step-failure.js.map +1 -1
- package/dist/cli-commands.d.ts +41 -0
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +33 -0
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli.d.ts +4 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +26 -0
- 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 +30 -2
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +9 -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/journal-reader.d.ts +1 -1
- package/dist/journal-reader.d.ts.map +1 -1
- package/dist/journal-reader.js +3 -1
- package/dist/journal-reader.js.map +1 -1
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +12 -1
- package/dist/llm-worker.js.map +1 -1
- package/dist/permissions-preflight.d.ts +22 -0
- package/dist/permissions-preflight.d.ts.map +1 -0
- package/dist/permissions-preflight.js +49 -0
- package/dist/permissions-preflight.js.map +1 -0
- package/dist/preflight.d.ts +13 -2
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +29 -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/pty-sidechannel.d.ts +7 -0
- package/dist/pty-sidechannel.d.ts.map +1 -1
- package/dist/pty-sidechannel.js.map +1 -1
- package/dist/redact.d.ts +16 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +104 -0
- package/dist/redact.js.map +1 -0
- package/dist/run-state.d.ts +124 -0
- package/dist/run-state.d.ts.map +1 -0
- package/dist/run-state.js +368 -0
- package/dist/run-state.js.map +1 -0
- package/dist/spec.d.ts +24 -1
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/step-env.d.ts +25 -0
- package/dist/step-env.d.ts.map +1 -0
- package/dist/step-env.js +34 -0
- package/dist/step-env.js.map +1 -0
- package/dist/transcript-tail.d.ts +54 -0
- package/dist/transcript-tail.d.ts.map +1 -0
- package/dist/transcript-tail.js +176 -0
- package/dist/transcript-tail.js.map +1 -0
- package/dist/worker-cli.d.ts +10 -0
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +138 -17
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-usage.d.ts +7 -2
- package/dist/worker-usage.d.ts.map +1 -1
- package/dist/worker-usage.js +21 -3
- package/dist/worker-usage.js.map +1 -1
- package/dist/worker.d.ts +1 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +41 -7
- package/dist/worker.js.map +1 -1
- package/dist/wrapper-runtime.d.ts +7 -1
- package/dist/wrapper-runtime.d.ts.map +1 -1
- package/dist/wrapper-runtime.js +7 -1
- package/dist/wrapper-runtime.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/agent-transcript.ts +651 -0
- 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 -24
- package/src/cli/status.ts +349 -0
- package/src/cli/step-failure.ts +100 -10
- package/src/cli-commands.ts +33 -0
- package/src/cli.ts +27 -0
- 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 +31 -1
- package/src/index.ts +10 -1
- package/src/journal-client.ts +12 -2
- package/src/journal-reader.ts +4 -2
- package/src/llm-worker.ts +12 -1
- package/src/permissions-preflight.ts +53 -0
- package/src/preflight.ts +42 -7
- package/src/protocol.ts +15 -0
- package/src/pty-sidechannel.ts +7 -0
- package/src/redact.ts +106 -0
- package/src/run-state.ts +468 -0
- package/src/spec.ts +18 -1
- package/src/step-env.ts +47 -0
- package/src/transcript-tail.ts +193 -0
- package/src/worker-cli.ts +139 -18
- package/src/worker-usage.ts +26 -3
- package/src/worker.ts +42 -6
- package/src/wrapper-runtime.ts +7 -1
- package/src/wrapper-session.ts +159 -24
package/src/cli/check.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { communicationInstruction } from '../communication/spec.js';
|
|
2
|
+
import { checkCommunicationEnvironment } from '../communication/preflight.js';
|
|
3
|
+
import { agentEnvironment, brokerEnvironment } from '../communication/environment.js';
|
|
1
4
|
import { accessSync, constants, readFileSync } from 'node:fs';
|
|
2
5
|
import { dirname, isAbsolute, join, parse as parsePath, resolve } from 'node:path';
|
|
3
6
|
import { spawnSync } from 'node:child_process';
|
|
@@ -159,6 +162,16 @@ export function checkAuthoredFlow(authoring: FlowSpec, path: string, projectConf
|
|
|
159
162
|
config.directory,
|
|
160
163
|
)
|
|
161
164
|
: undefined;
|
|
165
|
+
if (flow?.steps.some(step => step.type === 'agent' && communicationInstruction(step.instruction))) {
|
|
166
|
+
try { checkCommunicationEnvironment(flow); }
|
|
167
|
+
catch (error) {
|
|
168
|
+
result.ok = false;
|
|
169
|
+
result.diagnostics.push({ severity: 'refusal', kind: 'probe_failed',
|
|
170
|
+
message: error instanceof Error ? error.message : 'Communication environment could not be checked.' });
|
|
171
|
+
}
|
|
172
|
+
result.diagnostics.push({ severity: 'warning', kind: 'budget_unmetered',
|
|
173
|
+
message: 'Managed communication sessions do not report token or dollar usage. Budget ceilings cannot bound their spend; communication.timeoutMs bounds their duration.' });
|
|
174
|
+
}
|
|
162
175
|
return {
|
|
163
176
|
report: {
|
|
164
177
|
ok: result.ok,
|
|
@@ -169,7 +182,7 @@ export function checkAuthoredFlow(authoring: FlowSpec, path: string, projectConf
|
|
|
169
182
|
diagnostics: result.diagnostics,
|
|
170
183
|
requirements: safeRequirements(authoring, config.cli),
|
|
171
184
|
},
|
|
172
|
-
...(flow !== undefined ? { flow } : {}),
|
|
185
|
+
...(result.ok && flow !== undefined ? { flow } : {}),
|
|
173
186
|
};
|
|
174
187
|
} catch (error) {
|
|
175
188
|
const failure = error instanceof CheckFailure
|
|
@@ -375,7 +388,7 @@ function findConfig(start: string): string | undefined {
|
|
|
375
388
|
function systemProbes(flowDirectory: string, config: ProjectConfig): PreflightProbes {
|
|
376
389
|
return {
|
|
377
390
|
helper: helperReady,
|
|
378
|
-
cli: (cli, source, model) => probeCli(cli, source === 'project' ? config.directory : flowDirectory, model),
|
|
391
|
+
cli: (cli, source, model, execution) => probeCli(cli, source === 'project' ? config.directory : flowDirectory, model, execution),
|
|
379
392
|
executor: (trigger) => config.executors.includes(trigger.executor),
|
|
380
393
|
// A deterministic step runs in the daemon's working directory — the
|
|
381
394
|
// directory `flows run` was invoked from, or Cloud's code mount — not in
|
|
@@ -416,12 +429,22 @@ function probeCli(
|
|
|
416
429
|
cli: string,
|
|
417
430
|
directory: string,
|
|
418
431
|
model?: string,
|
|
432
|
+
execution?: 'managed',
|
|
419
433
|
): CliProbeResult {
|
|
420
434
|
const executable = resolveExecutable(cli, directory);
|
|
421
435
|
if (executable === undefined) return { exists: false, authenticated: false };
|
|
422
436
|
const kind = cliAdapterKind(executable);
|
|
437
|
+
// Relay owns interactive CLI launch/injection. Its generic PTY path is not
|
|
438
|
+
// the headless wrapper protocol; do not demand that protocol from Gemini,
|
|
439
|
+
// Cursor, OpenCode, or other interactive tools. Never invent an auth pass.
|
|
440
|
+
if (execution === 'managed' && kind === 'relayflows-wrapper-v1') {
|
|
441
|
+
return { exists: true, supported: true, authenticated: 'unverified' };
|
|
442
|
+
}
|
|
443
|
+
const environment = execution === 'managed'
|
|
444
|
+
? { ...brokerEnvironment(process.env), ...agentEnvironment(executable) } : process.env;
|
|
445
|
+
const probe = (invocation: CliInvocation) => runProbe(executable, directory, invocation, environment);
|
|
423
446
|
const identification = adapterIdentification(kind);
|
|
424
|
-
const identified =
|
|
447
|
+
const identified = probe(identification.invocation);
|
|
425
448
|
if (
|
|
426
449
|
identified.status !== 0
|
|
427
450
|
|| (identification.expectedStdout !== undefined
|
|
@@ -435,7 +458,7 @@ function probeCli(
|
|
|
435
458
|
return {
|
|
436
459
|
exists: true,
|
|
437
460
|
supported: true,
|
|
438
|
-
authenticated:
|
|
461
|
+
authenticated: probe(auth).status === 0,
|
|
439
462
|
authCommand,
|
|
440
463
|
};
|
|
441
464
|
}
|
|
@@ -445,7 +468,7 @@ function probeCli(
|
|
|
445
468
|
// A successful real provider round trip (or identified wrapper probe)
|
|
446
469
|
// proves both auth and exact-model access. On failure, run the adapter's
|
|
447
470
|
// actual auth command solely to classify auth vs model access truthfully.
|
|
448
|
-
if (
|
|
471
|
+
if (probe(scoped).status === 0) {
|
|
449
472
|
return {
|
|
450
473
|
exists: true,
|
|
451
474
|
supported: true,
|
|
@@ -455,35 +478,70 @@ function probeCli(
|
|
|
455
478
|
modelCommand,
|
|
456
479
|
};
|
|
457
480
|
}
|
|
458
|
-
const
|
|
481
|
+
const authProbe = probe(auth);
|
|
482
|
+
const authenticated = authProbe.status === 0;
|
|
459
483
|
return {
|
|
460
484
|
exists: true,
|
|
461
485
|
supported: true,
|
|
462
|
-
authenticated
|
|
486
|
+
authenticated,
|
|
463
487
|
modelAvailable: false,
|
|
464
488
|
authCommand,
|
|
465
489
|
modelCommand,
|
|
490
|
+
// Only on failure: on success there is nothing to explain, and the output
|
|
491
|
+
// is the most identity-bearing thing this function touches.
|
|
492
|
+
...(authenticated
|
|
493
|
+
? {}
|
|
494
|
+
: {
|
|
495
|
+
authExitCode: authProbe.status,
|
|
496
|
+
authFailureDetail: redactProbeOutput(
|
|
497
|
+
`${authProbe.stderr}${authProbe.stdout}`,
|
|
498
|
+
).trim().slice(0, 500),
|
|
499
|
+
}),
|
|
466
500
|
};
|
|
467
501
|
}
|
|
468
502
|
|
|
503
|
+
/**
|
|
504
|
+
* Redact anything that looks like a credential or an account identifier.
|
|
505
|
+
*
|
|
506
|
+
* `auth status` output is diagnostic, but it is also the one place an account
|
|
507
|
+
* email, org id or token fragment can appear. The point of surfacing it is to
|
|
508
|
+
* say WHY a probe failed, which survives redaction; leaking an identity into a
|
|
509
|
+
* refusal message that gets pasted into issues does not.
|
|
510
|
+
*/
|
|
511
|
+
function redactProbeOutput(text: string): string {
|
|
512
|
+
return text
|
|
513
|
+
.replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, '<redacted-email>')
|
|
514
|
+
.replace(/\b(sk|pk|oat|rt)[-_][A-Za-z0-9._-]{8,}/gi, '<redacted-token>')
|
|
515
|
+
.replace(/\b[A-Fa-f0-9]{32,}\b/g, '<redacted-hex>');
|
|
516
|
+
}
|
|
517
|
+
|
|
469
518
|
function runProbe(
|
|
470
519
|
executable: string,
|
|
471
520
|
directory: string,
|
|
472
521
|
invocation: CliInvocation,
|
|
473
|
-
|
|
474
|
-
|
|
522
|
+
environment: NodeJS.ProcessEnv = process.env,
|
|
523
|
+
): { status: number | null; stdout: string; stderr: string } {
|
|
524
|
+
const env = { ...environment };
|
|
475
525
|
delete env[MODEL_ENV];
|
|
476
526
|
if (invocation.modelEnv !== undefined) env[MODEL_ENV] = invocation.modelEnv;
|
|
477
527
|
const result = spawnSync(executable, invocation.args, {
|
|
478
528
|
cwd: directory,
|
|
479
529
|
encoding: 'utf8',
|
|
480
|
-
|
|
530
|
+
// stderr was 'ignore'. A failing `auth status` writes its reason there, so
|
|
531
|
+
// discarding it made every authentication refusal structurally
|
|
532
|
+
// undiagnosable: the refusal could say a probe exited non-zero and never
|
|
533
|
+
// what it said. Captured, then redacted at the point of use.
|
|
534
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
481
535
|
timeout: invocation.timeoutMs,
|
|
482
536
|
env,
|
|
483
537
|
});
|
|
484
538
|
const failure = classifySpawnFailure(result.error, result.signal, invocation.timeoutMs);
|
|
485
539
|
if (failure !== undefined) throw failure;
|
|
486
|
-
return {
|
|
540
|
+
return {
|
|
541
|
+
status: result.status,
|
|
542
|
+
stdout: result.stdout,
|
|
543
|
+
stderr: result.stderr ?? '',
|
|
544
|
+
};
|
|
487
545
|
}
|
|
488
546
|
|
|
489
547
|
function resolveExecutable(command: string, directory: string): string | undefined {
|
|
@@ -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
|
+
}
|