mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
feat: Replace duration/elapsed fields with wall_time_ms and StageTiming (#343)
## Summary
Replaces the ambiguous `runtime_secs`, `elapsed_secs`, and `duration_ms`
timing fields on run/stage public API surfaces with explicit
`wall_time_ms` (elapsed clock time) and a `StageTiming` value object
that also carries `inference_time_ms`, `tool_time_ms`, and
`active_time_ms`.
This is a greenfield breaking change — no compatibility shims are
preserved.
### What changed
**API shape**
- `RunBillingStage.runtime_secs` → `RunBillingStage.timing: StageTiming`
- `RunBillingTotals.runtime_secs` → `RunBillingTotals.timing:
StageTiming`
- `RunSummary.timestamps.duration_ms` / `elapsed_secs` removed; a
top-level `timing: StageTiming | null` field added
- Stage list item `duration_secs` → `wall_time_ms`
**Web app (`apps/fabro-web`)**
- `run-billing.tsx`: `liveRuntimeSecs` → `liveWallTimeMs`; live ticking
now returns milliseconds and the footer total sums `wallTimeMs` across
rows
- `stage-sidebar.ts`: `duration_secs` → `wall_time_ms` for the per-stage
duration display
- `runs.ts`: `elapsed_secs` lookup replaced with `timing.wall_time_ms`
- `formatElapsedSecs` / `formatDurationSecs` call sites replaced with
`formatDurationMs`
**Lockfile / tooling**
- `@openapitools/openapi-generator-cli@2.20.2` added as a dev dependency
to `@qltysh/fabro-api-client` to support regenerating the TypeScript
client after schema edits; several transitive deps pulled in alongside
it.
### Design notes
- **Units are now consistent**: every timing value on run/stage surfaces
is in milliseconds; the old API mixed seconds (`runtime_secs`,
`elapsed_secs`) with milliseconds (`duration_ms`).
- **Live ticking** still works correctly: the in-flight billing row
computes `now - startedAt` in ms and sums across rows for the footer,
avoiding a server round-trip during a running stage.
- **`StageTiming.active_time_ms = inference_time_ms + tool_time_ms`** —
parallel work is summed, so run active time can exceed wall time.
- Subsystem-internal `duration_ms` fields (sandbox setup, devcontainer
lifecycle, hooks) are intentionally left unchanged; only public
run/stage timing surfaces are affected.
### Fabro Details
<details>
<summary>Ran 9 stages in 115m 53s for $108.50</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 18s | – | 0 |
| implement | 80m 53s | $101.97 | 0 |
| simplify_opus | 21m 35s | $4.09 | 0 |
| simplify_gpt | 5m 1s | $2.44 | 0 |
| verify | 3m 11s | – | 0 |
| fmt | 3s | – | 0 |
| **Total** | **115m 53s** | **$108.50** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
This commit is contained in:
parent
178adf15ea
commit
54bc67017e
93 changed files with 1788 additions and 716 deletions
|
|
@ -36,8 +36,12 @@ function makeRun(overrides: Partial<Run> = {}): Run {
|
|||
started_at: "2026-04-08T12:00:00Z",
|
||||
last_event_at: null,
|
||||
completed_at: null,
|
||||
duration_ms: 65000,
|
||||
elapsed_secs: 65,
|
||||
},
|
||||
timing: {
|
||||
wall_time_ms: 65000,
|
||||
inference_time_ms: 0,
|
||||
tool_time_ms: 0,
|
||||
active_time_ms: 0,
|
||||
},
|
||||
billing: { total_usd_micros: 500000 },
|
||||
diff: null,
|
||||
|
|
@ -130,9 +134,8 @@ describe("mapRunToRunItem", () => {
|
|||
started_at: null,
|
||||
last_event_at: null,
|
||||
completed_at: null,
|
||||
duration_ms: null,
|
||||
elapsed_secs: null,
|
||||
},
|
||||
timing: null,
|
||||
billing: null,
|
||||
});
|
||||
const item = mapRunToRunItem(summary);
|
||||
|
|
@ -177,4 +180,4 @@ describe("columnForStatus", () => {
|
|||
test("returns null for lifecycle states that do not map to a board column", () => {
|
||||
expect(columnForStatus("removing")).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { formatElapsedSecs, formatDurationSecs } from "../lib/format";
|
||||
import { formatDurationMs } from "../lib/format";
|
||||
import {
|
||||
BoardColumn,
|
||||
type Run,
|
||||
|
|
@ -91,7 +91,7 @@ export function mapRunListItem(item: Run): RunItem {
|
|||
lifecycleStatusLabel: lifecycleStatusLabel(item.lifecycle.status, item.lifecycle.archived),
|
||||
number: item.pull_request?.number,
|
||||
pullRequestUrl: item.pull_request?.html_url,
|
||||
elapsed: item.timestamps.elapsed_secs != null ? formatElapsedSecs(item.timestamps.elapsed_secs) : undefined,
|
||||
elapsed: item.timing != null ? formatDurationMs(item.timing.wall_time_ms) : undefined,
|
||||
resources: undefined,
|
||||
question: item.current_question?.text,
|
||||
sandboxId: runtime?.id ?? undefined,
|
||||
|
|
@ -202,4 +202,4 @@ export const ciConfig: Record<CiStatus, { label: string; dot: string; text: stri
|
|||
passing: { label: "Passing", dot: "bg-mint", text: "text-mint" },
|
||||
failing: { label: "Changes needed", dot: "bg-coral", text: "text-coral" },
|
||||
pending: { label: "Pending", dot: "bg-amber", text: "text-amber" },
|
||||
};
|
||||
};
|
||||
|
|
@ -26,7 +26,7 @@ describe("mapRunStagesToSidebarStages", () => {
|
|||
name: "Apply Changes",
|
||||
handler: "command",
|
||||
status: "succeeded",
|
||||
duration_secs: 12.5,
|
||||
wall_time_ms: 12500,
|
||||
node_id: "apply",
|
||||
visit: 1,
|
||||
},
|
||||
|
|
@ -197,4 +197,4 @@ describe("aggregateGraphNodeStatus", () => {
|
|||
latestStageId: "apply@1",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -3,7 +3,7 @@ import type { PaginatedRunStageList } from "@qltysh/fabro-api-client";
|
|||
|
||||
import type { Stage } from "../components/stage-sidebar";
|
||||
import { isVisibleStage } from "../data/runs";
|
||||
import { formatDurationSecs } from "./format";
|
||||
import { formatDurationMs } from "./format";
|
||||
|
||||
export const ACTIVE_STAGE_STATES: ReadonlySet<StageState> = new Set([
|
||||
StageState.RUNNING,
|
||||
|
|
@ -70,8 +70,8 @@ export function mapRunStagesToSidebarStages(
|
|||
nodeId: stage.node_id,
|
||||
visit: stage.visit,
|
||||
status: stage.status,
|
||||
duration: stage.duration_secs != null
|
||||
? formatDurationSecs(stage.duration_secs)
|
||||
duration: stage.wall_time_ms != null
|
||||
? formatDurationMs(stage.wall_time_ms)
|
||||
: "--",
|
||||
startedAt: stage.started_at ?? null,
|
||||
}));
|
||||
|
|
@ -112,4 +112,4 @@ export function aggregateGraphNodeStatus(stages: readonly Stage[]): Map<
|
|||
result.set(nodeId, { displayStatus: display.status, latestStageId: latestStage.id });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,20 @@
|
|||
import { afterEach, describe, expect, mock, test } from "bun:test";
|
||||
import TestRenderer from "react-test-renderer";
|
||||
|
||||
import type { BilledTokenCounts, RunBilling } from "@qltysh/fabro-api-client";
|
||||
import type {
|
||||
BilledTokenCounts,
|
||||
RunBilling,
|
||||
StageTiming,
|
||||
} from "@qltysh/fabro-api-client";
|
||||
|
||||
function stageTiming(wall_time_ms = 0, inference_time_ms = 0, tool_time_ms = 0): StageTiming {
|
||||
return {
|
||||
wall_time_ms,
|
||||
inference_time_ms,
|
||||
tool_time_ms,
|
||||
active_time_ms: inference_time_ms + tool_time_ms,
|
||||
};
|
||||
}
|
||||
|
||||
let currentBilling: RunBilling | undefined;
|
||||
|
||||
|
|
@ -28,7 +41,7 @@ function billing(overrides: Partial<RunBilling> = {}): RunBilling {
|
|||
return {
|
||||
stages: [],
|
||||
totals: {
|
||||
runtime_secs: 0,
|
||||
timing: stageTiming(),
|
||||
...zeroBilling(),
|
||||
},
|
||||
by_model: [],
|
||||
|
|
@ -74,19 +87,19 @@ describe("RunBilling", () => {
|
|||
stage: { id: "start", name: "start" },
|
||||
model: null,
|
||||
billing: zeroBilling(),
|
||||
runtime_secs: 0,
|
||||
timing: stageTiming(),
|
||||
state: "succeeded",
|
||||
},
|
||||
{
|
||||
stage: { id: "command", name: "command" },
|
||||
model: null,
|
||||
billing: zeroBilling(),
|
||||
runtime_secs: 61,
|
||||
timing: stageTiming(61000),
|
||||
state: "succeeded",
|
||||
},
|
||||
],
|
||||
totals: {
|
||||
runtime_secs: 61,
|
||||
timing: stageTiming(61000),
|
||||
...zeroBilling(),
|
||||
},
|
||||
}),
|
||||
|
|
@ -109,7 +122,7 @@ describe("RunBilling", () => {
|
|||
stage: { id: "start", name: "start" },
|
||||
model: null,
|
||||
billing: zeroBilling(),
|
||||
runtime_secs: 0,
|
||||
timing: stageTiming(),
|
||||
state: "succeeded",
|
||||
},
|
||||
{
|
||||
|
|
@ -124,12 +137,12 @@ describe("RunBilling", () => {
|
|||
total_tokens: 1500,
|
||||
total_usd_micros: 240000,
|
||||
}),
|
||||
runtime_secs: 42,
|
||||
timing: stageTiming(42000),
|
||||
state: "succeeded",
|
||||
},
|
||||
],
|
||||
totals: {
|
||||
runtime_secs: 42,
|
||||
timing: stageTiming(42000),
|
||||
...zeroBilling({
|
||||
input_tokens: 1200,
|
||||
output_tokens: 300,
|
||||
|
|
@ -197,13 +210,13 @@ describe("RunBilling", () => {
|
|||
total_tokens: 1500,
|
||||
total_usd_micros: 240000,
|
||||
}),
|
||||
runtime_secs: 0,
|
||||
timing: stageTiming(),
|
||||
started_at: startedAt,
|
||||
state: "running",
|
||||
},
|
||||
],
|
||||
totals: {
|
||||
runtime_secs: 0,
|
||||
timing: stageTiming(),
|
||||
...zeroBilling({
|
||||
input_tokens: 1200,
|
||||
output_tokens: 300,
|
||||
|
|
@ -255,4 +268,4 @@ describe("RunBilling", () => {
|
|||
Date.now = originalNow;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -3,7 +3,7 @@ import { Fragment, useMemo } from "react";
|
|||
import { EmptyState } from "../components/state";
|
||||
import { Tooltip } from "../components/ui";
|
||||
import {
|
||||
formatDurationSecs,
|
||||
formatDurationMs,
|
||||
formatTokenCount,
|
||||
formatUsdMicros,
|
||||
} from "../lib/format";
|
||||
|
|
@ -53,24 +53,24 @@ interface MappedStageRow {
|
|||
outputTokens: number | null;
|
||||
cacheReadTokens: number | null;
|
||||
cacheWriteTokens: number | null;
|
||||
runtimeSecs: number;
|
||||
wallTimeMs: number;
|
||||
totalUsdMicros: number | null | undefined;
|
||||
inFlight: boolean;
|
||||
}
|
||||
|
||||
function liveRuntimeSecs(stage: RunBillingStage, now: number): number {
|
||||
function liveWallTimeMs(stage: RunBillingStage, now: number): number {
|
||||
if (stage.started_at) {
|
||||
const startedMs = new Date(stage.started_at).getTime();
|
||||
if (Number.isFinite(startedMs)) {
|
||||
return Math.max(0, (now - startedMs) / 1000);
|
||||
return Math.max(0, now - startedMs);
|
||||
}
|
||||
}
|
||||
return stage.runtime_secs;
|
||||
return stage.timing.wall_time_ms;
|
||||
}
|
||||
|
||||
export const handle = { wide: true };
|
||||
|
||||
function mapStageRow(stage: RunBillingStage, runtimeSecs: number): MappedStageRow {
|
||||
function mapStageRow(stage: RunBillingStage, wallTimeMs: number): MappedStageRow {
|
||||
const hasModel = stage.model != null;
|
||||
return {
|
||||
stage: stage.stage.name,
|
||||
|
|
@ -81,7 +81,7 @@ function mapStageRow(stage: RunBillingStage, runtimeSecs: number): MappedStageRo
|
|||
: null,
|
||||
cacheReadTokens: hasModel ? stage.billing.cache_read_tokens : null,
|
||||
cacheWriteTokens: hasModel ? stage.billing.cache_write_tokens : null,
|
||||
runtimeSecs,
|
||||
wallTimeMs,
|
||||
totalUsdMicros: stage.billing.total_usd_micros,
|
||||
inFlight: isInFlight(stage),
|
||||
};
|
||||
|
|
@ -181,7 +181,7 @@ export default function RunBilling({ params }: { params: { id: string } }) {
|
|||
// don't reallocate them every tick.
|
||||
const completedRows = useMemo<MappedStageRow[]>(() => {
|
||||
if (!billing) return [];
|
||||
return billing.stages.map((stage) => mapStageRow(stage, stage.runtime_secs));
|
||||
return billing.stages.map((stage) => mapStageRow(stage, stage.timing.wall_time_ms));
|
||||
}, [billing]);
|
||||
|
||||
// The model breakdown is server-derived and stable across ticks too.
|
||||
|
|
@ -206,16 +206,16 @@ export default function RunBilling({ params }: { params: { id: string } }) {
|
|||
if (!hasInFlight) return completedRows;
|
||||
return billing.stages.map((stage, idx) =>
|
||||
isInFlight(stage)
|
||||
? mapStageRow(stage, liveRuntimeSecs(stage, now))
|
||||
? mapStageRow(stage, liveWallTimeMs(stage, now))
|
||||
: completedRows[idx],
|
||||
);
|
||||
}, [billing, completedRows, hasInFlight, now]);
|
||||
|
||||
// While ticking, sum the displayed row runtimes so the footer updates in
|
||||
// lock-step. Otherwise trust the server's authoritative total.
|
||||
const totalRuntimeSecs = hasInFlight
|
||||
? rows.reduce((sum, row) => sum + row.runtimeSecs, 0)
|
||||
: (billing?.totals.runtime_secs ?? 0);
|
||||
const totalWallTimeMs = hasInFlight
|
||||
? rows.reduce((sum, row) => sum + row.wallTimeMs, 0)
|
||||
: (billing?.totals.timing.wall_time_ms ?? 0);
|
||||
|
||||
const hasLlmStages = (billing?.by_model.length ?? 0) > 0;
|
||||
const totalInput = hasLlmStages ? (billing?.totals.input_tokens ?? null) : null;
|
||||
|
|
@ -276,7 +276,7 @@ export default function RunBilling({ params }: { params: { id: string } }) {
|
|||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs text-fg-3">
|
||||
{formatDurationSecs(row.runtimeSecs)}
|
||||
{formatDurationMs(row.wallTimeMs)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs text-fg-3">
|
||||
{formatUsdMicrosOrDash(row.totalUsdMicros)}
|
||||
|
|
@ -297,7 +297,7 @@ export default function RunBilling({ params }: { params: { id: string } }) {
|
|||
/>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs font-medium text-fg">
|
||||
{formatDurationSecs(totalRuntimeSecs)}
|
||||
{formatDurationMs(totalWallTimeMs)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs font-medium text-fg">
|
||||
{formatUsdMicrosOrDash(totalUsdMicros)}
|
||||
|
|
|
|||
|
|
@ -104,9 +104,8 @@ function makeRunSummary(
|
|||
started_at: null,
|
||||
last_event_at: null,
|
||||
completed_at: null,
|
||||
duration_ms: null,
|
||||
elapsed_secs: null,
|
||||
},
|
||||
timing: null,
|
||||
billing: null,
|
||||
diff: diffSummary,
|
||||
pull_request: pullRequest,
|
||||
|
|
@ -548,4 +547,4 @@ describe("RunDetail full-height child routes", () => {
|
|||
);
|
||||
expect(outletWrappers).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
267
bun.lock
267
bun.lock
|
|
@ -77,6 +77,7 @@
|
|||
"axios": "^1.7.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openapitools/openapi-generator-cli": "2.20.2",
|
||||
"typescript": "^5.9.2",
|
||||
},
|
||||
},
|
||||
|
|
@ -146,6 +147,8 @@
|
|||
|
||||
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="],
|
||||
|
||||
"@capsizecss/unpack": ["@capsizecss/unpack@4.0.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA=="],
|
||||
|
||||
"@clack/core": ["@clack/core@1.2.0", "", { "dependencies": { "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg=="],
|
||||
|
|
@ -294,6 +297,8 @@
|
|||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@lukeed/csprng": ["@lukeed/csprng@1.1.0", "", {}, "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA=="],
|
||||
|
||||
"@mediabunny/aac-encoder": ["@mediabunny/aac-encoder@1.39.2", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-KD6KADVzAnW7tqhRFGBOX4uaiHbd0Yxvg0lfthj3wJLAEEgEBAvi43w+ZXWeEn54X/jpabrLe4bW/eYFFvlbUA=="],
|
||||
|
||||
"@mediabunny/flac-encoder": ["@mediabunny/flac-encoder@1.39.2", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-VwBr3AzZTPEEPvt4aladZiXwOf3W293eq213zDupGQi/taS8WWNqDd3eBdf8FfvlbXATfbRiycXDKyQ0HlOZaQ=="],
|
||||
|
|
@ -314,6 +319,18 @@
|
|||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="],
|
||||
|
||||
"@nestjs/axios": ["@nestjs/axios@4.0.0", "", { "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "axios": "^1.3.1", "rxjs": "^7.0.0" } }, "sha512-1cB+Jyltu/uUPNQrpUimRHEQHrnQrpLzVj6dU3dgn6iDDDdahr10TgHFGTmw5VuJ9GzKZsCLDL78VSwJAs/9JQ=="],
|
||||
|
||||
"@nestjs/common": ["@nestjs/common@11.1.1", "", { "dependencies": { "file-type": "20.5.0", "iterare": "1.2.1", "load-esm": "1.0.2", "tslib": "2.8.1", "uid": "2.0.2" }, "peerDependencies": { "class-transformer": ">=0.4.1", "class-validator": ">=0.13.2", "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "optionalPeers": ["class-transformer", "class-validator"] }, "sha512-crzp+1qeZ5EGL0nFTPy9NrVMAaUWewV5AwtQyv6SQ9yQPXwRl9W9hm1pt0nAtUu5QbYMbSuo7lYcF81EjM+nCA=="],
|
||||
|
||||
"@nestjs/core": ["@nestjs/core@11.1.1", "", { "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", "path-to-regexp": "8.2.0", "tslib": "2.8.1", "uid": "2.0.2" }, "peerDependencies": { "@nestjs/common": "^11.0.0", "@nestjs/microservices": "^11.0.0", "@nestjs/platform-express": "^11.0.0", "@nestjs/websockets": "^11.0.0", "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "optionalPeers": ["@nestjs/microservices", "@nestjs/platform-express", "@nestjs/websockets"] }, "sha512-UFoUAgLKFT+RwHTANJdr0dF7p0qS9QjkaUPjg8aafnjM/qxxxrUVDB49nVvyMlk+Hr1+vvcNaOHbWWQBxoZcHA=="],
|
||||
|
||||
"@nuxt/opencollective": ["@nuxt/opencollective@0.4.1", "", { "dependencies": { "consola": "^3.2.3" }, "bin": { "opencollective": "bin/opencollective.js" } }, "sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ=="],
|
||||
|
||||
"@nuxtjs/opencollective": ["@nuxtjs/opencollective@0.3.2", "", { "dependencies": { "chalk": "^4.1.0", "consola": "^2.15.0", "node-fetch": "^2.6.1" }, "bin": { "opencollective": "bin/opencollective.js" } }, "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA=="],
|
||||
|
||||
"@openapitools/openapi-generator-cli": ["@openapitools/openapi-generator-cli@2.20.2", "", { "dependencies": { "@nestjs/axios": "4.0.0", "@nestjs/common": "11.1.1", "@nestjs/core": "11.1.1", "@nuxtjs/opencollective": "0.3.2", "axios": "1.9.0", "chalk": "4.1.2", "commander": "8.3.0", "compare-versions": "4.1.4", "concurrently": "6.5.1", "console.table": "0.10.0", "fs-extra": "11.3.0", "glob": "9.3.5", "inquirer": "8.2.6", "lodash": "4.17.21", "proxy-agent": "6.5.0", "reflect-metadata": "0.2.2", "rxjs": "7.8.2", "tslib": "2.8.1" }, "bin": { "openapi-generator-cli": "main.js" } }, "sha512-dNFwQcQu6+rmEWSJj4KUx468+p6Co7nfpVgi5QEfVhzKj7wBytz9GEhCN2qmVgtg3ZX8H6nxbXI8cjh7hAxAqg=="],
|
||||
|
||||
"@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
|
||||
|
||||
"@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="],
|
||||
|
|
@ -668,6 +685,12 @@
|
|||
|
||||
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.19", "", {}, "sha512-/BMP7kNhzKOd7wnDeB8NrIRNLwkf5AhCYCvtfZV2GXWbBieFm/el0n6LOAXlTi6ZwHICSNnQcIxRCWHrLzDY+g=="],
|
||||
|
||||
"@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
|
||||
|
||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||
|
||||
"@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
|
@ -760,12 +783,20 @@
|
|||
|
||||
"acorn-import-phases": ["acorn-import-phases@1.0.4", "", { "peerDependencies": { "acorn": "^8.14.0" } }, "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ=="],
|
||||
|
||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
|
||||
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
|
||||
|
||||
"ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="],
|
||||
|
||||
"ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
|
@ -792,14 +823,26 @@
|
|||
|
||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
|
||||
|
||||
"basic-ftp": ["basic-ftp@5.3.1", "", {}, "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw=="],
|
||||
|
||||
"big.js": ["big.js@5.2.2", "", {}, "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ=="],
|
||||
|
||||
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
|
||||
|
||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||
|
||||
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||
|
||||
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
|
@ -810,6 +853,8 @@
|
|||
|
||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||
|
||||
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
||||
|
|
@ -818,6 +863,8 @@
|
|||
|
||||
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
|
||||
|
||||
"chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="],
|
||||
|
||||
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
"chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="],
|
||||
|
|
@ -828,16 +875,38 @@
|
|||
|
||||
"classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
|
||||
|
||||
"cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
|
||||
|
||||
"cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
|
||||
|
||||
"cli-width": ["cli-width@3.0.0", "", {}, "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw=="],
|
||||
|
||||
"cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="],
|
||||
|
||||
"clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||
|
||||
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||
"commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
|
||||
"common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="],
|
||||
|
||||
"compare-versions": ["compare-versions@4.1.4", "", {}, "sha512-FemMreK9xNyL8gQevsdRMrvO4lFCkQP7qbuktn1q8ndcNk1+0mz7lgE7b/sNvbhVgY4w6tMN1FDp6aADjqw2rw=="],
|
||||
|
||||
"concurrently": ["concurrently@6.5.1", "", { "dependencies": { "chalk": "^4.1.0", "date-fns": "^2.16.1", "lodash": "^4.17.21", "rxjs": "^6.6.3", "spawn-command": "^0.0.2-1", "supports-color": "^8.1.0", "tree-kill": "^1.2.2", "yargs": "^16.2.0" }, "bin": { "concurrently": "bin/concurrently.js" } }, "sha512-FlSwNpGjWQfRwPLXvJ/OgysbBxPkWpiVjy1042b0U7on7S7qwwMIILRj7WTN1mTgqa582bG6NFuScOoh6Zgdag=="],
|
||||
|
||||
"consola": ["consola@2.15.3", "", {}, "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw=="],
|
||||
|
||||
"console.table": ["console.table@0.10.0", "", { "dependencies": { "easy-table": "1.1.0" } }, "sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
|
@ -862,14 +931,22 @@
|
|||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
|
||||
|
||||
"date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
||||
|
||||
"defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="],
|
||||
|
||||
"define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
|
||||
|
||||
"defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="],
|
||||
|
||||
"degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
|
@ -902,8 +979,12 @@
|
|||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"easy-table": ["easy-table@1.1.0", "", { "optionalDependencies": { "wcwidth": ">=1.0.1" } }, "sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"emojis-list": ["emojis-list@3.0.0", "", {}, "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="],
|
||||
|
||||
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||
|
|
@ -928,7 +1009,9 @@
|
|||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
"escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
||||
|
||||
"escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
|
||||
|
||||
|
|
@ -942,6 +1025,8 @@
|
|||
|
||||
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
|
||||
|
||||
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
|
||||
|
|
@ -950,6 +1035,8 @@
|
|||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="],
|
||||
|
||||
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
|
||||
|
||||
"fabro-remotion": ["fabro-remotion@workspace:apps/remotion"],
|
||||
|
|
@ -960,6 +1047,8 @@
|
|||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="],
|
||||
|
||||
"fast-string-truncated-width": ["fast-string-truncated-width@1.2.1", "", {}, "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow=="],
|
||||
|
||||
"fast-string-width": ["fast-string-width@1.1.0", "", { "dependencies": { "fast-string-truncated-width": "^1.2.0" } }, "sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ=="],
|
||||
|
|
@ -972,6 +1061,12 @@
|
|||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="],
|
||||
|
||||
"figures": ["figures@3.2.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg=="],
|
||||
|
||||
"file-type": ["file-type@20.5.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="],
|
||||
|
||||
"flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
||||
|
|
@ -982,14 +1077,20 @@
|
|||
|
||||
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||
|
||||
"fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="],
|
||||
|
||||
"fs-monkey": ["fs-monkey@1.0.3", "", {}, "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="],
|
||||
|
||||
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||
|
|
@ -998,8 +1099,12 @@
|
|||
|
||||
"get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||
|
||||
"get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="],
|
||||
|
||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||
|
||||
"glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
|
||||
|
||||
"glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
|
@ -1048,12 +1153,26 @@
|
|||
|
||||
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
|
||||
|
||||
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
|
||||
"icss-utils": ["icss-utils@5.1.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
||||
|
||||
"inquirer": ["inquirer@8.2.6", "", { "dependencies": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", "ora": "^5.4.1", "run-async": "^2.4.0", "rxjs": "^7.5.5", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", "wrap-ansi": "^6.0.1" } }, "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg=="],
|
||||
|
||||
"ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="],
|
||||
|
||||
"iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
|
||||
|
||||
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
|
||||
|
|
@ -1066,20 +1185,28 @@
|
|||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
||||
|
||||
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
|
||||
|
||||
"is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
||||
"is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
|
||||
|
||||
"is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"iterare": ["iterare@1.2.1", "", {}, "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q=="],
|
||||
|
||||
"jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="],
|
||||
|
||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
|
@ -1096,6 +1223,8 @@
|
|||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
|
||||
|
||||
"kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
|
@ -1122,15 +1251,21 @@
|
|||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
|
||||
"load-esm": ["load-esm@1.0.2", "", {}, "sha512-nVAvWk/jeyrWyXEAs84mpQCYccxRqgKY4OznLuJhJCa0XsPSfdOIr2zvBZEj3IHEHbX97jjscKRRV539bW0Gpw=="],
|
||||
|
||||
"loader-runner": ["loader-runner@4.3.1", "", {}, "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q=="],
|
||||
|
||||
"loader-utils": ["loader-utils@2.0.4", "", { "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^2.1.2" } }, "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw=="],
|
||||
|
||||
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="],
|
||||
|
||||
"log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="],
|
||||
|
||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="],
|
||||
"lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
|
||||
|
||||
"lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="],
|
||||
|
||||
|
|
@ -1250,24 +1385,34 @@
|
|||
|
||||
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
"minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="],
|
||||
|
||||
"minimist": ["minimist@1.2.6", "", {}, "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="],
|
||||
|
||||
"minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
|
||||
|
||||
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||
|
||||
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"mute-stream": ["mute-stream@0.0.8", "", {}, "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="],
|
||||
|
||||
"nanoid": ["nanoid@5.1.11", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="],
|
||||
|
||||
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
|
||||
|
||||
"neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="],
|
||||
|
||||
"netmask": ["netmask@2.1.1", "", {}, "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA=="],
|
||||
|
||||
"nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
||||
|
||||
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
|
||||
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||
|
||||
"node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="],
|
||||
|
|
@ -1296,12 +1441,20 @@
|
|||
|
||||
"open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
|
||||
|
||||
"ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
|
||||
|
||||
"os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="],
|
||||
|
||||
"p-limit": ["p-limit@7.3.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw=="],
|
||||
|
||||
"p-queue": ["p-queue@9.1.2", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^7.0.0" } }, "sha512-ktsDOALzTYTWWF1PbkNVg2rOt+HaOaMWJMUnt7T3qf5tvZ1L8dBW3tObzprBcXNMKkwj+yFSLqHso0x+UFcJXw=="],
|
||||
|
||||
"p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="],
|
||||
|
||||
"pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="],
|
||||
|
||||
"pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="],
|
||||
|
||||
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
|
||||
|
||||
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
||||
|
|
@ -1312,6 +1465,10 @@
|
|||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
|
||||
|
||||
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
|
||||
|
||||
"piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="],
|
||||
|
|
@ -1346,6 +1503,8 @@
|
|||
|
||||
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||
|
||||
"proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
|
||||
|
|
@ -1378,10 +1537,14 @@
|
|||
|
||||
"react-textarea-autosize": ["react-textarea-autosize@8.5.9", "", { "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
|
||||
"recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
|
||||
|
||||
"reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="],
|
||||
|
||||
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
|
||||
|
||||
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
|
||||
|
|
@ -1408,8 +1571,12 @@
|
|||
|
||||
"remotion": ["remotion@4.0.437", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-mQHiYZwt3HoMngJeTGyytVFdobf/mgsPTiQSUfPP43kA7bEpn4OdaF4hWoMfJhjfTC1ZdkTIRX/s3OKto0aWzg=="],
|
||||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
|
||||
|
||||
"retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="],
|
||||
|
||||
"retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="],
|
||||
|
|
@ -1420,8 +1587,16 @@
|
|||
|
||||
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
|
||||
|
||||
"run-async": ["run-async@2.4.1", "", {}, "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ=="],
|
||||
|
||||
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safe-content-frame": ["safe-content-frame@0.0.19", "", {}, "sha512-+R0IHHjvghT5O8bc8itf9AoS9MvzhUcD0p+hNINLgyEuFQJug3wt3ZuhLFZFG3bUzHi8UfQED4p6J3/Ft9oCtg=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
|
@ -1446,8 +1621,14 @@
|
|||
|
||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||
|
||||
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
|
||||
|
||||
"smol-toml": ["smol-toml@1.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="],
|
||||
|
||||
"socks": ["socks@2.8.9", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw=="],
|
||||
|
||||
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
|
||||
|
||||
"source-map": ["source-map@0.7.3", "", {}, "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
|
@ -1456,19 +1637,29 @@
|
|||
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"spawn-command": ["spawn-command@0.0.2", "", {}, "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ=="],
|
||||
|
||||
"stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="],
|
||||
|
||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
|
||||
|
||||
"strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="],
|
||||
|
||||
"style-loader": ["style-loader@4.0.0", "", { "peerDependencies": { "webpack": "^5.27.0" } }, "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA=="],
|
||||
|
||||
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
|
||||
|
||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||
|
||||
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"svgo": ["svgo@4.0.1", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w=="],
|
||||
|
||||
|
|
@ -1484,6 +1675,8 @@
|
|||
|
||||
"terser-webpack-plugin": ["terser-webpack-plugin@5.4.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g=="],
|
||||
|
||||
"through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="],
|
||||
|
||||
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
||||
|
||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||
|
|
@ -1494,7 +1687,13 @@
|
|||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="],
|
||||
"tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="],
|
||||
|
||||
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
|
||||
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
|
||||
|
||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||
|
||||
|
|
@ -1504,10 +1703,16 @@
|
|||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
||||
|
||||
"uid": ["uid@2.0.2", "", { "dependencies": { "@lukeed/csprng": "^1.0.0" } }, "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g=="],
|
||||
|
||||
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
||||
|
||||
"ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="],
|
||||
|
||||
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||
|
|
@ -1536,6 +1741,8 @@
|
|||
|
||||
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
||||
|
||||
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
||||
|
||||
"unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
|
@ -1570,28 +1777,36 @@
|
|||
|
||||
"watchpack": ["watchpack@2.5.1", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg=="],
|
||||
|
||||
"wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="],
|
||||
|
||||
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="],
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"webpack": ["webpack@5.105.0", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.19.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.16", "watchpack": "^2.5.1", "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw=="],
|
||||
|
||||
"webpack-sources": ["webpack-sources@3.3.4", "", {}, "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="],
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="],
|
||||
|
||||
"xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||
|
||||
"yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
|
||||
|
||||
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
|
||||
|
|
@ -1620,6 +1835,10 @@
|
|||
|
||||
"@babel/traverse/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||
|
||||
"@nuxt/opencollective/consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||
|
||||
"@openapitools/openapi-generator-cli/axios": ["axios@1.9.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg=="],
|
||||
|
||||
"@parcel/watcher/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"@radix-ui/react-accordion/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||
|
|
@ -1816,24 +2035,44 @@
|
|||
|
||||
"astro/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
|
||||
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"concurrently/rxjs": ["rxjs@6.6.7", "", { "dependencies": { "tslib": "^1.9.0" } }, "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ=="],
|
||||
|
||||
"concurrently/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||
|
||||
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
|
||||
|
||||
"degenerator/ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="],
|
||||
|
||||
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"escodegen/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"extract-zip/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
|
||||
|
||||
"is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||
|
||||
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||
|
||||
"magicast/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||
|
||||
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
|
||||
|
||||
"open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
||||
|
||||
"postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"postcss-modules-local-by-default/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
|
||||
|
|
@ -1852,14 +2091,20 @@
|
|||
|
||||
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||
|
||||
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
|
||||
"terser-webpack-plugin/schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="],
|
||||
|
||||
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"unstorage/lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="],
|
||||
|
||||
"webpack/schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="],
|
||||
|
||||
"yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
|
||||
|
||||
"@astrojs/markdown-remark/shiki/@shikijs/core": ["@shikijs/core@4.0.2", "", { "dependencies": { "@shikijs/primitive": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw=="],
|
||||
|
||||
"@astrojs/markdown-remark/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag=="],
|
||||
|
|
@ -1986,6 +2231,8 @@
|
|||
|
||||
"@remotion/bundler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ=="],
|
||||
|
||||
"@remotion/renderer/source-map/whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="],
|
||||
|
||||
"@remotion/studio-server/semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||
|
||||
"@remotion/studio/semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||
|
|
@ -2030,6 +2277,8 @@
|
|||
|
||||
"astro/shiki/@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="],
|
||||
|
||||
"concurrently/rxjs/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
|
||||
|
||||
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
|
||||
|
||||
"terser-webpack-plugin/schema-utils/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
|
@ -2040,6 +2289,10 @@
|
|||
|
||||
"webpack/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
|
||||
|
||||
"@remotion/renderer/source-map/whatwg-url/tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="],
|
||||
|
||||
"@remotion/renderer/source-map/whatwg-url/webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="],
|
||||
|
||||
"@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
|
||||
|
||||
"@tailwindcss/vite/@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
|
||||
|
|
|
|||
|
|
@ -7583,11 +7583,13 @@ components:
|
|||
type: ["string", "null"]
|
||||
format: date-time
|
||||
description: Wall-clock time the latest attempt of this stage started, if known.
|
||||
duration_ms:
|
||||
type: ["integer", "null"]
|
||||
format: uint64
|
||||
minimum: 0
|
||||
description: Wall-clock duration of the stage's latest terminal attempt, if known.
|
||||
timing:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/StageTiming"
|
||||
- type: "null"
|
||||
description: |
|
||||
Per-attempt timing breakdown for the latest terminal attempt:
|
||||
wall time plus the active inference/tool breakdown.
|
||||
usage:
|
||||
$ref: "#/components/schemas/BilledTokenCounts"
|
||||
model:
|
||||
|
|
@ -7743,17 +7745,15 @@ components:
|
|||
required:
|
||||
- stage_id
|
||||
- stage_label
|
||||
- duration_ms
|
||||
- timing
|
||||
- retries
|
||||
properties:
|
||||
stage_id:
|
||||
type: string
|
||||
stage_label:
|
||||
type: string
|
||||
duration_ms:
|
||||
type: integer
|
||||
format: uint64
|
||||
minimum: 0
|
||||
timing:
|
||||
$ref: "#/components/schemas/StageTiming"
|
||||
billing_usd_micros:
|
||||
type: ["integer", "null"]
|
||||
format: int64
|
||||
|
|
@ -7768,7 +7768,7 @@ components:
|
|||
required:
|
||||
- timestamp
|
||||
- status
|
||||
- duration_ms
|
||||
- timing
|
||||
- stages
|
||||
- total_retries
|
||||
- diff
|
||||
|
|
@ -7778,10 +7778,8 @@ components:
|
|||
format: date-time
|
||||
status:
|
||||
$ref: "#/components/schemas/StageOutcome"
|
||||
duration_ms:
|
||||
type: integer
|
||||
format: uint64
|
||||
minimum: 0
|
||||
timing:
|
||||
$ref: "#/components/schemas/RunTiming"
|
||||
failure:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/RunFailure"
|
||||
|
|
@ -7908,6 +7906,7 @@ components:
|
|||
- models
|
||||
- source_directory
|
||||
- timestamps
|
||||
- timing
|
||||
- billing
|
||||
- diff
|
||||
- pull_request
|
||||
|
|
@ -7964,6 +7963,13 @@ components:
|
|||
type: ["string", "null"]
|
||||
timestamps:
|
||||
$ref: "#/components/schemas/RunTimestamps"
|
||||
timing:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/RunTiming"
|
||||
- type: "null"
|
||||
description: |
|
||||
Run-level timing rollup. Wall time is the run's clock duration;
|
||||
active timing sums work across stage visits.
|
||||
billing:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/RunBillingSummary"
|
||||
|
|
@ -8068,11 +8074,6 @@ components:
|
|||
completed_at:
|
||||
type: ["string", "null"]
|
||||
format: date-time
|
||||
duration_ms:
|
||||
type: ["integer", "null"]
|
||||
format: int64
|
||||
elapsed_secs:
|
||||
type: ["number", "null"]
|
||||
|
||||
RunBillingSummary:
|
||||
type: object
|
||||
|
|
@ -8212,10 +8213,12 @@ components:
|
|||
example: unit-tests
|
||||
status:
|
||||
$ref: "#/components/schemas/CheckRunStatus"
|
||||
duration_secs:
|
||||
type: number
|
||||
description: Duration of the check run in seconds.
|
||||
example: 154.0
|
||||
wall_time_ms:
|
||||
type: integer
|
||||
format: uint64
|
||||
minimum: 0
|
||||
description: Wall-clock duration of the check run in milliseconds.
|
||||
example: 154000
|
||||
|
||||
# ── Reusable Sub-Schemas ───────────────────────────────────────────
|
||||
|
||||
|
|
@ -8611,20 +8614,75 @@ components:
|
|||
format: uri
|
||||
example: https://github.com/fabro-sh/fabro/pull/123
|
||||
|
||||
RunTimings:
|
||||
description: Timing information for a run.
|
||||
StageTiming:
|
||||
description: |
|
||||
Timing breakdown for one stage visit. Fields are all milliseconds.
|
||||
`wall_time_ms` is elapsed clock time; `inference_time_ms` is Fabro-
|
||||
observed LLM request/stream elapsed time; `tool_time_ms` is tool or
|
||||
command execution elapsed time; `active_time_ms` equals
|
||||
`inference_time_ms + tool_time_ms`.
|
||||
type: object
|
||||
required:
|
||||
- elapsed_secs
|
||||
- wall_time_ms
|
||||
- active_time_ms
|
||||
properties:
|
||||
elapsed_secs:
|
||||
type: number
|
||||
description: Wall-clock time elapsed in seconds.
|
||||
example: 420.0
|
||||
elapsed_warning:
|
||||
type: boolean
|
||||
description: Whether the elapsed time exceeds the expected threshold.
|
||||
example: false
|
||||
wall_time_ms:
|
||||
type: integer
|
||||
format: uint64
|
||||
minimum: 0
|
||||
example: 1500
|
||||
inference_time_ms:
|
||||
type: integer
|
||||
format: uint64
|
||||
minimum: 0
|
||||
default: 0
|
||||
example: 900
|
||||
tool_time_ms:
|
||||
type: integer
|
||||
format: uint64
|
||||
minimum: 0
|
||||
default: 0
|
||||
example: 200
|
||||
active_time_ms:
|
||||
type: integer
|
||||
format: uint64
|
||||
minimum: 0
|
||||
description: Equals `inference_time_ms + tool_time_ms`.
|
||||
example: 1100
|
||||
|
||||
RunTiming:
|
||||
description: |
|
||||
Timing rollup for an entire run. Active fields sum work across stage
|
||||
visits, so `active_time_ms` can exceed `wall_time_ms` when parallel
|
||||
branches run concurrently.
|
||||
type: object
|
||||
required:
|
||||
- wall_time_ms
|
||||
- active_time_ms
|
||||
properties:
|
||||
wall_time_ms:
|
||||
type: integer
|
||||
format: uint64
|
||||
minimum: 0
|
||||
example: 420000
|
||||
inference_time_ms:
|
||||
type: integer
|
||||
format: uint64
|
||||
minimum: 0
|
||||
default: 0
|
||||
example: 120000
|
||||
tool_time_ms:
|
||||
type: integer
|
||||
format: uint64
|
||||
minimum: 0
|
||||
default: 0
|
||||
example: 60000
|
||||
active_time_ms:
|
||||
type: integer
|
||||
format: uint64
|
||||
minimum: 0
|
||||
description: Equals `inference_time_ms + tool_time_ms`.
|
||||
example: 180000
|
||||
|
||||
SandboxProvider:
|
||||
description: Sandbox execution provider.
|
||||
|
|
@ -8704,7 +8762,7 @@ components:
|
|||
- reasoning_tokens
|
||||
- cache_read_tokens
|
||||
- cache_write_tokens
|
||||
- runtime_secs
|
||||
- timing
|
||||
properties:
|
||||
runs:
|
||||
type: integer
|
||||
|
|
@ -8739,10 +8797,12 @@ components:
|
|||
format: int64
|
||||
description: Total billed USD amount in micros.
|
||||
example: 20340000
|
||||
runtime_secs:
|
||||
type: number
|
||||
description: Total runtime in seconds.
|
||||
example: 3501.0
|
||||
timing:
|
||||
$ref: "#/components/schemas/RunTiming"
|
||||
description: |
|
||||
Aggregate timing rollup across every completed run. Active timing
|
||||
sums work across stage visits, so `active_time_ms` can exceed
|
||||
`wall_time_ms`.
|
||||
|
||||
BillingStageRef:
|
||||
description: Reference to a workflow node in a billing stage row.
|
||||
|
|
@ -8864,10 +8924,12 @@ components:
|
|||
$ref: "#/components/schemas/StageHandler"
|
||||
status:
|
||||
$ref: "#/components/schemas/StageState"
|
||||
duration_secs:
|
||||
type: number
|
||||
description: Time spent in this stage, in seconds.
|
||||
example: 154.0
|
||||
wall_time_ms:
|
||||
type: integer
|
||||
format: uint64
|
||||
minimum: 0
|
||||
description: Wall-clock time the latest attempt spent in this stage, in milliseconds.
|
||||
example: 154000
|
||||
node_id:
|
||||
type: string
|
||||
description: Node id in the workflow graph; multiple stages with different visits share the same node_id.
|
||||
|
|
@ -9220,13 +9282,13 @@ components:
|
|||
# ── Billing Schemas ──────────────────────────────────────────────────
|
||||
|
||||
RunBillingStage:
|
||||
description: Token counts and billed totals for one workflow node within a run. Rows are grouped by node; billing and runtime sum every visit of that node.
|
||||
description: Token counts and billed totals for one workflow node within a run. Rows are grouped by node; billing and timing sum every visit of that node.
|
||||
type: object
|
||||
required:
|
||||
- stage
|
||||
- model
|
||||
- billing
|
||||
- runtime_secs
|
||||
- timing
|
||||
properties:
|
||||
stage:
|
||||
$ref: "#/components/schemas/BillingStageRef"
|
||||
|
|
@ -9237,10 +9299,11 @@ components:
|
|||
- type: "null"
|
||||
billing:
|
||||
$ref: "#/components/schemas/BilledTokenCounts"
|
||||
runtime_secs:
|
||||
type: number
|
||||
description: Wall-clock runtime in seconds, summed across every visit of this node.
|
||||
example: 154.0
|
||||
timing:
|
||||
$ref: "#/components/schemas/StageTiming"
|
||||
description: |
|
||||
Per-node timing summed across every visit. `wall_time_ms` is the
|
||||
sum of visit wall times; the active breakdown sums work timing.
|
||||
started_at:
|
||||
type: ["string", "null"]
|
||||
format: date-time
|
||||
|
|
@ -9256,7 +9319,7 @@ components:
|
|||
description: Aggregate billing totals across all stages of a run.
|
||||
type: object
|
||||
required:
|
||||
- runtime_secs
|
||||
- timing
|
||||
- input_tokens
|
||||
- output_tokens
|
||||
- total_tokens
|
||||
|
|
@ -9264,10 +9327,11 @@ components:
|
|||
- cache_read_tokens
|
||||
- cache_write_tokens
|
||||
properties:
|
||||
runtime_secs:
|
||||
type: number
|
||||
description: Total wall-clock runtime in seconds.
|
||||
example: 389.0
|
||||
timing:
|
||||
$ref: "#/components/schemas/RunTiming"
|
||||
description: |
|
||||
Run-level timing rollup. `wall_time_ms` is summed across stage
|
||||
visits; active timing sums work across visits.
|
||||
input_tokens:
|
||||
type: integer
|
||||
description: Total input tokens consumed.
|
||||
|
|
@ -11350,4 +11414,4 @@ components:
|
|||
login:
|
||||
type: string
|
||||
description: User's login identifier (e.g. GitHub username).
|
||||
example: octocat
|
||||
example: octocat
|
||||
|
|
@ -364,6 +364,8 @@ fn main() {
|
|||
("BillingModelRef", "fabro_model::ModelRef", &[]),
|
||||
("BillingSpeed", "fabro_model::Speed", &[]),
|
||||
("ExecOutputTail", "fabro_types::ExecOutputTail", &[]),
|
||||
("StageTiming", "fabro_types::StageTiming", &[]),
|
||||
("RunTiming", "fabro_types::RunTiming", &[]),
|
||||
("ProviderId", "fabro_model::ProviderId", &[]),
|
||||
("Model", "fabro_model::Model", &[]),
|
||||
("Provider", "fabro_model::Provider", &[]),
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ fn run_billing_stage_model_accepts_required_null() {
|
|||
"cache_read_tokens": 0,
|
||||
"cache_write_tokens": 0
|
||||
},
|
||||
"runtime_secs": 0.0
|
||||
"timing": {"wall_time_ms": 0, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0}
|
||||
});
|
||||
|
||||
let stage: RunBillingStage =
|
||||
|
|
@ -69,7 +69,7 @@ fn run_billing_stage_round_trips_terminal_row_with_started_at_and_state() {
|
|||
"cache_read_tokens": 0,
|
||||
"cache_write_tokens": 0
|
||||
},
|
||||
"runtime_secs": 5.5,
|
||||
"timing": {"wall_time_ms": 5500, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0},
|
||||
"started_at": "2026-04-29T12:34:56Z",
|
||||
"state": "succeeded"
|
||||
});
|
||||
|
|
@ -122,7 +122,7 @@ fn run_billing_stage_round_trips_in_flight_row() {
|
|||
"cache_read_tokens": 0,
|
||||
"cache_write_tokens": 0
|
||||
},
|
||||
"runtime_secs": 1.25,
|
||||
"timing": {"wall_time_ms": 1250, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0},
|
||||
"started_at": "2026-04-29T12:34:56Z",
|
||||
"state": "running"
|
||||
});
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ fn conclusion_json_uses_failure_object() {
|
|||
status: StageOutcome::Failed {
|
||||
retry_requested: false,
|
||||
},
|
||||
duration_ms: 42,
|
||||
timing: fabro_types::RunTiming::wall_only(42),
|
||||
failure: Some(RunFailure {
|
||||
reason: FailureReason::WorkflowError,
|
||||
detail: FailureDetail::new("boom", FailureCategory::Deterministic),
|
||||
|
|
@ -84,7 +84,12 @@ fn conclusion_json_uses_failure_object() {
|
|||
json!({
|
||||
"timestamp": "2026-05-13T12:00:00Z",
|
||||
"status": "failed",
|
||||
"duration_ms": 42,
|
||||
"timing": {
|
||||
"wall_time_ms": 42,
|
||||
"inference_time_ms": 0,
|
||||
"tool_time_ms": 0,
|
||||
"active_time_ms": 0
|
||||
},
|
||||
"failure": {
|
||||
"reason": "workflow_error",
|
||||
"detail": {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use fabro_api::types::{RepositoryRef as ApiRepositoryRef, Run as ApiRun};
|
|||
use fabro_types::status::{RunStatus, SuccessReason};
|
||||
use fabro_types::{
|
||||
DiffSummary, PullRequestLink, RepositoryProvider, RepositoryRef, Run, RunBillingSummary, RunId,
|
||||
RunLifecycle, RunLinks, RunOrigin, RunTimestamps, WorkflowRef,
|
||||
RunLifecycle, RunLinks, RunOrigin, RunTimestamps, RunTiming, WorkflowRef,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
|
|
@ -62,9 +62,8 @@ fn run_summary_json_matches_openapi_shape() {
|
|||
started_at: Some(created_at),
|
||||
last_event_at: Some(last_event_at),
|
||||
completed_at: None,
|
||||
duration_ms: Some(42_000),
|
||||
elapsed_secs: Some(42.0),
|
||||
},
|
||||
timing: Some(RunTiming::new(42_000, 12_000, 30_000)),
|
||||
billing: Some(RunBillingSummary {
|
||||
total_usd_micros: Some(123),
|
||||
}),
|
||||
|
|
@ -128,9 +127,13 @@ fn run_summary_json_matches_openapi_shape() {
|
|||
"created_at": "2026-04-20T12:00:00Z",
|
||||
"started_at": "2026-04-20T12:00:00Z",
|
||||
"last_event_at": "2026-04-20T12:00:42Z",
|
||||
"completed_at": null,
|
||||
"duration_ms": 42000,
|
||||
"elapsed_secs": 42.0
|
||||
"completed_at": null
|
||||
},
|
||||
"timing": {
|
||||
"wall_time_ms": 42000,
|
||||
"inference_time_ms": 12000,
|
||||
"tool_time_ms": 30000,
|
||||
"active_time_ms": 42000
|
||||
},
|
||||
"billing": {
|
||||
"total_usd_micros": 123
|
||||
|
|
@ -220,8 +223,7 @@ fn run_summary_deserializes_when_optional_fields_are_absent() {
|
|||
assert_eq!(summary.timestamps.last_event_at, None);
|
||||
assert_eq!(summary.lifecycle.status, RunStatus::Running);
|
||||
assert_eq!(summary.lifecycle.pending_control, None);
|
||||
assert_eq!(summary.timestamps.duration_ms, None);
|
||||
assert_eq!(summary.timestamps.elapsed_secs, None);
|
||||
assert_eq!(summary.timing.map(|t| t.wall_time_ms), None);
|
||||
assert_eq!(summary.billing, None);
|
||||
assert_eq!(summary.superseded_by, None);
|
||||
assert_eq!(summary.diff, None);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,12 @@ fn stage_projection_round_trips_representative_json() {
|
|||
"output": "ok",
|
||||
"termination": "exited",
|
||||
"started_at": "2026-04-29T12:34:00Z",
|
||||
"duration_ms": 56000,
|
||||
"timing": {
|
||||
"wall_time_ms": 56000,
|
||||
"inference_time_ms": 0,
|
||||
"tool_time_ms": 0,
|
||||
"active_time_ms": 0
|
||||
},
|
||||
"usage": {
|
||||
"input_tokens": 0,
|
||||
"output_tokens": 0,
|
||||
|
|
|
|||
|
|
@ -372,7 +372,7 @@ fn format_event_pretty_value(envelope: &serde_json::Value, styles: &Styles) -> O
|
|||
}
|
||||
}
|
||||
"run.completed" => {
|
||||
let duration = format_duration_ms(prop_field(envelope, "duration_ms"));
|
||||
let duration = format_duration_ms(timing_wall_field(envelope));
|
||||
let status_str = match prop_str_field(envelope, "status") {
|
||||
Some(status) if !status.is_empty() => status,
|
||||
_ => "succeeded",
|
||||
|
|
@ -526,7 +526,7 @@ fn format_event_pretty_value(envelope: &serde_json::Value, styles: &Styles) -> O
|
|||
}
|
||||
"stage.completed" => {
|
||||
let label = str_field(envelope, "node_label").unwrap_or("?");
|
||||
let duration = format_duration_ms(prop_field(envelope, "duration_ms"));
|
||||
let duration = format_duration_ms(timing_wall_field(envelope));
|
||||
let billing = prop_field(envelope, "billing").or_else(|| prop_field(envelope, "usage"));
|
||||
let cost = format_cost(
|
||||
billing
|
||||
|
|
@ -809,6 +809,13 @@ fn prop_str_field<'a>(value: &'a serde_json::Value, key: &str) -> Option<&'a str
|
|||
prop_field(value, key)?.as_str()
|
||||
}
|
||||
|
||||
/// Read `properties.timing.wall_time_ms` from a stage/run terminal event
|
||||
/// envelope. Returns `None` when timing is absent, which falls back to a
|
||||
/// blank display via `format_duration_ms`.
|
||||
fn timing_wall_field(envelope: &serde_json::Value) -> Option<&serde_json::Value> {
|
||||
prop_field(envelope, "timing")?.get("wall_time_ms")
|
||||
}
|
||||
|
||||
fn failure_message(failure: &serde_json::Value) -> Option<&serde_json::Value> {
|
||||
failure
|
||||
.get("detail")
|
||||
|
|
@ -1027,7 +1034,7 @@ mod tests {
|
|||
#[test]
|
||||
fn pretty_stage_completed() {
|
||||
let styles = no_color_styles();
|
||||
let line = r#"{"ts":"2026-01-01T14:23:15Z","event":"stage.completed","node_label":"plan","properties":{"duration_ms":8000,"status":"succeeded","usage":{"cost":0.12,"input_tokens":10000,"output_tokens":5200}}}"#;
|
||||
let line = r#"{"ts":"2026-01-01T14:23:15Z","event":"stage.completed","node_label":"plan","properties":{"timing":{"wall_time_ms":8000,"inference_time_ms":0,"tool_time_ms":0,"active_time_ms":0},"status":"succeeded","usage":{"cost":0.12,"input_tokens":10000,"output_tokens":5200}}}"#;
|
||||
let result = format_event_pretty(line, &styles).unwrap();
|
||||
assert!(result.contains("plan"), "got: {result}");
|
||||
assert!(result.contains("$0.12"), "got: {result}");
|
||||
|
|
@ -1106,7 +1113,7 @@ mod tests {
|
|||
#[test]
|
||||
fn pretty_workflow_run_completed() {
|
||||
let styles = no_color_styles();
|
||||
let line = r#"{"ts":"2026-01-01T14:23:32Z","run_id":"abc123","event":"run.completed","properties":{"duration_ms":25000,"status":"succeeded","total_usd_micros":570000,"billing":{"input_tokens":5000,"output_tokens":2000,"total_tokens":7000,"cache_read_tokens":3000,"cache_write_tokens":500,"reasoning_tokens":800}}}"#;
|
||||
let line = r#"{"ts":"2026-01-01T14:23:32Z","run_id":"abc123","event":"run.completed","properties":{"timing":{"wall_time_ms":25000,"inference_time_ms":0,"tool_time_ms":0,"active_time_ms":0},"status":"succeeded","total_usd_micros":570000,"billing":{"input_tokens":5000,"output_tokens":2000,"total_tokens":7000,"cache_read_tokens":3000,"cache_write_tokens":500,"reasoning_tokens":800}}}"#;
|
||||
let result = format_event_pretty(line, &styles).unwrap();
|
||||
assert!(result.contains("SUCCEEDED"), "got: {result}");
|
||||
assert!(result.contains("25s"), "got: {result}");
|
||||
|
|
@ -1120,7 +1127,7 @@ mod tests {
|
|||
#[test]
|
||||
fn pretty_workflow_run_completed_backward_compat() {
|
||||
let styles = no_color_styles();
|
||||
let line = r#"{"ts":"2026-01-01T14:23:32Z","run_id":"abc123","event":"run.completed","properties":{"duration_ms":25000,"total_cost":0.57}}"#;
|
||||
let line = r#"{"ts":"2026-01-01T14:23:32Z","run_id":"abc123","event":"run.completed","properties":{"timing":{"wall_time_ms":25000,"inference_time_ms":0,"tool_time_ms":0,"active_time_ms":0},"total_cost":0.57}}"#;
|
||||
let result = format_event_pretty(line, &styles).unwrap();
|
||||
assert!(result.contains("SUCCEEDED"), "got: {result}");
|
||||
assert!(result.contains("25s"), "got: {result}");
|
||||
|
|
@ -1131,7 +1138,7 @@ mod tests {
|
|||
#[test]
|
||||
fn pretty_workflow_run_completed_fail_status() {
|
||||
let styles = no_color_styles();
|
||||
let line = r#"{"ts":"2026-01-01T14:23:32Z","event":"run.completed","properties":{"duration_ms":25000,"status":"failed"}}"#;
|
||||
let line = r#"{"ts":"2026-01-01T14:23:32Z","event":"run.completed","properties":{"timing":{"wall_time_ms":25000,"inference_time_ms":0,"tool_time_ms":0,"active_time_ms":0},"status":"failed"}}"#;
|
||||
let result = format_event_pretty(line, &styles).unwrap();
|
||||
assert!(result.contains("FAIL"), "got: {result}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ pub(crate) fn print_run_conclusion(
|
|||
fabro_util::printerr!(
|
||||
printer,
|
||||
"Duration: {}",
|
||||
HumanDuration(Duration::from_millis(conclusion.duration_ms))
|
||||
HumanDuration(Duration::from_millis(conclusion.timing.wall_time_ms))
|
||||
);
|
||||
|
||||
if let Some(billing) = conclusion.billing.as_ref() {
|
||||
|
|
|
|||
|
|
@ -132,11 +132,11 @@ pub(super) enum ProgressEvent {
|
|||
script: Option<String>,
|
||||
},
|
||||
StageCompleted {
|
||||
node_id: String,
|
||||
name: String,
|
||||
duration_ms: u64,
|
||||
status: String,
|
||||
usage: Option<ProgressUsage>,
|
||||
node_id: String,
|
||||
name: String,
|
||||
timing: fabro_types::StageTiming,
|
||||
status: String,
|
||||
usage: Option<ProgressUsage>,
|
||||
},
|
||||
StageFailed {
|
||||
node_id: String,
|
||||
|
|
@ -350,7 +350,7 @@ pub(super) fn from_run_event(stored: &RunEvent) -> Option<ProgressEvent> {
|
|||
EventBody::StageCompleted(props) => Some(ProgressEvent::StageCompleted {
|
||||
node_id,
|
||||
name: node_label,
|
||||
duration_ms: props.duration_ms,
|
||||
timing: props.timing,
|
||||
status: props.status.to_string(),
|
||||
usage: props
|
||||
.billing
|
||||
|
|
@ -550,7 +550,7 @@ mod tests {
|
|||
node_id: "plan".into(),
|
||||
name: "Plan".into(),
|
||||
index: 0,
|
||||
duration_ms: 5000,
|
||||
timing: fabro_types::StageTiming::wall_only(5000),
|
||||
status: "succeeded".into(),
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
|
|
@ -576,9 +576,9 @@ mod tests {
|
|||
ProgressEvent::StageCompleted {
|
||||
node_id,
|
||||
name,
|
||||
duration_ms,
|
||||
timing,
|
||||
..
|
||||
} if node_id == "plan" && name == "Plan" && duration_ms == 5000
|
||||
} if node_id == "plan" && name == "Plan" && timing.wall_time_ms == 5000
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ impl ProgressUI {
|
|||
ProgressEvent::StageCompleted {
|
||||
node_id,
|
||||
name,
|
||||
duration_ms,
|
||||
timing,
|
||||
status,
|
||||
usage,
|
||||
} => {
|
||||
|
|
@ -263,7 +263,7 @@ impl ProgressUI {
|
|||
renderer,
|
||||
&node_id,
|
||||
&name,
|
||||
duration_ms,
|
||||
timing.wall_time_ms,
|
||||
&status,
|
||||
usage.as_ref(),
|
||||
);
|
||||
|
|
@ -585,7 +585,7 @@ mod tests {
|
|||
node_id: node_id.into(),
|
||||
name: name.into(),
|
||||
index: 0,
|
||||
duration_ms: 5000,
|
||||
timing: fabro_types::StageTiming::wall_only(5000),
|
||||
status: "succeeded".into(),
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -850,7 +850,7 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
worker_title_phase_for_event(&EventBody::RunCompleted(RunCompletedProps {
|
||||
duration_ms: 10,
|
||||
timing: fabro_types::RunTiming::wall_only(10),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
@ -868,7 +868,7 @@ mod tests {
|
|||
reason: FailureReason::Cancelled,
|
||||
detail: FailureDetail::new("cancelled", FailureCategory::Canceled),
|
||||
},
|
||||
duration_ms: 10,
|
||||
timing: fabro_types::RunTiming::wall_only(10),
|
||||
final_git_commit_sha: None,
|
||||
final_patch: None,
|
||||
diff_summary: None,
|
||||
|
|
@ -882,7 +882,7 @@ mod tests {
|
|||
reason: FailureReason::Terminated,
|
||||
detail: FailureDetail::new("boom", FailureCategory::Deterministic),
|
||||
},
|
||||
duration_ms: 10,
|
||||
timing: fabro_types::RunTiming::wall_only(10),
|
||||
final_git_commit_sha: None,
|
||||
final_patch: None,
|
||||
diff_summary: None,
|
||||
|
|
|
|||
|
|
@ -82,7 +82,8 @@ fn build_json_output(
|
|||
"status": run_status_kind(status),
|
||||
});
|
||||
if let Some(c) = conclusion {
|
||||
value["duration_ms"] = c.duration_ms.into();
|
||||
value["timing"] =
|
||||
serde_json::to_value(c.timing).unwrap_or_else(|_| serde_json::Value::Null);
|
||||
if let Some(total_usd_micros) = c
|
||||
.billing
|
||||
.as_ref()
|
||||
|
|
@ -112,7 +113,7 @@ fn print_human_output(
|
|||
|
||||
let details = match conclusion {
|
||||
Some(c) => {
|
||||
let duration = format_duration_ms(c.duration_ms);
|
||||
let duration = format_duration_ms(c.timing.wall_time_ms);
|
||||
let cost = c
|
||||
.billing
|
||||
.as_ref()
|
||||
|
|
@ -152,7 +153,7 @@ mod tests {
|
|||
let conclusion = Conclusion {
|
||||
timestamp: chrono::Utc::now(),
|
||||
status: StageOutcome::Succeeded,
|
||||
duration_ms: 12345,
|
||||
timing: fabro_types::RunTiming::wall_only(12345),
|
||||
failure: None,
|
||||
final_git_commit_sha: None,
|
||||
stages: vec![],
|
||||
|
|
@ -177,7 +178,7 @@ mod tests {
|
|||
);
|
||||
assert_eq!(json["run_id"], run_id.to_string());
|
||||
assert_eq!(json["status"], "succeeded");
|
||||
assert_eq!(json["duration_ms"], 12345);
|
||||
assert_eq!(json["timing"]["wall_time_ms"], 12345);
|
||||
assert_eq!(json["total_usd_micros"], 420_000);
|
||||
}
|
||||
|
||||
|
|
@ -193,7 +194,7 @@ mod tests {
|
|||
);
|
||||
assert_eq!(json["run_id"], run_id.to_string());
|
||||
assert_eq!(json["status"], "failed");
|
||||
assert!(json.get("duration_ms").is_none());
|
||||
assert!(json.get("timing").is_none());
|
||||
assert!(json.get("total_usd_micros").is_none());
|
||||
}
|
||||
|
||||
|
|
@ -211,7 +212,7 @@ mod tests {
|
|||
status: StageOutcome::Failed {
|
||||
retry_requested: false,
|
||||
},
|
||||
duration_ms: 500,
|
||||
timing: fabro_types::RunTiming::wall_only(500),
|
||||
failure: Some(RunFailure {
|
||||
reason: FailureReason::WorkflowError,
|
||||
detail: FailureDetail::new("error", FailureCategory::Deterministic),
|
||||
|
|
@ -230,7 +231,7 @@ mod tests {
|
|||
Some(&conclusion),
|
||||
);
|
||||
assert!(json.get("total_usd_micros").is_none());
|
||||
assert_eq!(json["duration_ms"], 500);
|
||||
assert_eq!(json["timing"]["wall_time_ms"], 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -240,7 +241,7 @@ mod tests {
|
|||
let conclusion = Conclusion {
|
||||
timestamp: chrono::Utc::now(),
|
||||
status: StageOutcome::Succeeded,
|
||||
duration_ms: 8000,
|
||||
timing: fabro_types::RunTiming::wall_only(8000),
|
||||
failure: None,
|
||||
final_git_commit_sha: None,
|
||||
stages: vec![],
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ pub(crate) async fn list_command(
|
|||
"status": run.status(),
|
||||
"start_time": run.start_time(),
|
||||
"labels": run.labels(),
|
||||
"duration_ms": run.duration_ms(),
|
||||
"wall_time_ms": run.wall_time_ms(),
|
||||
"total_usd_micros": run.total_usd_micros(),
|
||||
"source_directory": run.source_directory(),
|
||||
"repo_origin_url": run.repo_origin_url(),
|
||||
|
|
@ -107,7 +107,7 @@ pub(crate) async fn list_command(
|
|||
let rows: Vec<Vec<CellStruct>> = display_runs
|
||||
.iter()
|
||||
.map(|run| {
|
||||
let duration_display = match run.duration_ms() {
|
||||
let duration_display = match run.wall_time_ms() {
|
||||
Some(ms) => format_duration_ms(ms),
|
||||
None => match run.start_time_dt() {
|
||||
Some(start) => {
|
||||
|
|
|
|||
|
|
@ -77,8 +77,8 @@ impl ServerRunInfo {
|
|||
&self.run.labels
|
||||
}
|
||||
|
||||
pub(crate) fn duration_ms(&self) -> Option<u64> {
|
||||
self.run.timestamps.duration_ms
|
||||
pub(crate) fn wall_time_ms(&self) -> Option<u64> {
|
||||
self.run.timing.as_ref().map(|t| t.wall_time_ms)
|
||||
}
|
||||
|
||||
pub(crate) fn total_usd_micros(&self) -> Option<i64> {
|
||||
|
|
|
|||
|
|
@ -1147,13 +1147,18 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"internal.run_id": "[ULID]",
|
||||
"internal.thread_id": null
|
||||
},
|
||||
"duration_ms": "[DURATION_MS]",
|
||||
"index": 0,
|
||||
"max_attempts": 1,
|
||||
"node_visits": {
|
||||
"start": 1
|
||||
},
|
||||
"status": "succeeded"
|
||||
"status": "succeeded",
|
||||
"timing": {
|
||||
"active_time_ms": 0,
|
||||
"inference_time_ms": 0,
|
||||
"tool_time_ms": 0,
|
||||
"wall_time_ms": 0
|
||||
}
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"stage_id": "start@1",
|
||||
|
|
|
|||
|
|
@ -356,7 +356,7 @@ fn inspect_completed_run_shows_run_start_conclusion_checkpoint() {
|
|||
},
|
||||
"conclusion": {
|
||||
"status": "succeeded",
|
||||
"duration_ms": "[DURATION_MS]",
|
||||
"timing": "[TIMING]",
|
||||
"stage_count": null
|
||||
},
|
||||
"checkpoint": {
|
||||
|
|
@ -428,7 +428,7 @@ fn inspect_completed_run_reads_store_without_disk_metadata_files() {
|
|||
},
|
||||
"conclusion": {
|
||||
"status": "succeeded",
|
||||
"duration_ms": "[DURATION_MS]",
|
||||
"timing": "[TIMING]",
|
||||
"stage_count": null
|
||||
},
|
||||
"checkpoint": {
|
||||
|
|
@ -487,7 +487,7 @@ fn inspect_git_backed_run_exposes_checkpoint_and_sandbox_state() {
|
|||
},
|
||||
"conclusion": {
|
||||
"status": "succeeded",
|
||||
"duration_ms": "[DURATION_MS]",
|
||||
"timing": "[TIMING]",
|
||||
"final_git_commit_sha": "[SHA]",
|
||||
"stage_count": null
|
||||
},
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ fn remote_run_state_response(run_id: &str) -> serde_json::Value {
|
|||
state["conclusion"] = serde_json::json!({
|
||||
"timestamp": "2026-04-05T12:00:01Z",
|
||||
"status": "succeeded",
|
||||
"duration_ms": 12,
|
||||
"timing": {"wall_time_ms": 12, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0},
|
||||
"stages": [],
|
||||
"billing": null,
|
||||
"total_retries": 0,
|
||||
|
|
@ -74,7 +74,7 @@ fn run_completed_event(run_id: &str) -> serde_json::Value {
|
|||
"run_id": run_id,
|
||||
"ts": "2026-04-05T12:00:01Z",
|
||||
"properties": {
|
||||
"duration_ms": 12,
|
||||
"timing": {"wall_time_ms": 12, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0},
|
||||
"artifact_count": 0,
|
||||
"status": "succeeded",
|
||||
"reason": "completed"
|
||||
|
|
|
|||
|
|
@ -174,10 +174,9 @@ pub(crate) fn remote_run_summary_json(
|
|||
"created_at": timestamp,
|
||||
"started_at": timestamp,
|
||||
"last_event_at": null,
|
||||
"completed_at": null,
|
||||
"duration_ms": null,
|
||||
"elapsed_secs": null
|
||||
"completed_at": null
|
||||
},
|
||||
"timing": null,
|
||||
"billing": null,
|
||||
"diff": null,
|
||||
"pull_request": null,
|
||||
|
|
@ -1108,7 +1107,7 @@ async fn append_seeded_simple_completion_events(
|
|||
None,
|
||||
"run.completed",
|
||||
serde_json::json!({
|
||||
"duration_ms": 123,
|
||||
"timing": {"wall_time_ms": 123, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0},
|
||||
"artifact_count": 0,
|
||||
"status": "succeeded",
|
||||
"reason": "completed",
|
||||
|
|
@ -1274,7 +1273,7 @@ async fn append_seeded_git_completion_events(
|
|||
None,
|
||||
"run.completed",
|
||||
serde_json::json!({
|
||||
"duration_ms": 456,
|
||||
"timing": {"wall_time_ms": 456, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0},
|
||||
"artifact_count": 0,
|
||||
"status": "succeeded",
|
||||
"reason": "completed",
|
||||
|
|
@ -1335,7 +1334,7 @@ async fn append_seeded_git_noop_events(
|
|||
None,
|
||||
"run.completed",
|
||||
serde_json::json!({
|
||||
"duration_ms": 123,
|
||||
"timing": {"wall_time_ms": 123, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0},
|
||||
"artifact_count": 0,
|
||||
"status": "succeeded",
|
||||
"reason": "completed",
|
||||
|
|
@ -1395,7 +1394,7 @@ async fn append_seeded_artifact_run_events(
|
|||
None,
|
||||
"run.completed",
|
||||
serde_json::json!({
|
||||
"duration_ms": 123,
|
||||
"timing": {"wall_time_ms": 123, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0},
|
||||
"artifact_count": 6,
|
||||
"status": "succeeded",
|
||||
"reason": "completed",
|
||||
|
|
@ -1544,7 +1543,7 @@ fn test_labels(context: &TestContext) -> Vec<String> {
|
|||
fn stage_completed_properties(index: usize, response: Option<&str>) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"index": index,
|
||||
"duration_ms": 1,
|
||||
"timing": {"wall_time_ms": 1, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0},
|
||||
"status": "succeeded",
|
||||
"preferred_label": null,
|
||||
"suggested_next_ids": [],
|
||||
|
|
@ -1741,7 +1740,7 @@ pub(crate) fn compact_inspect(output: &Output) -> Value {
|
|||
"conclusion": conclusion.as_object().map(|_| {
|
||||
serde_json::json!({
|
||||
"status": conclusion["status"],
|
||||
"duration_ms": "[DURATION_MS]",
|
||||
"timing": "[TIMING]",
|
||||
"stage_count": conclusion["stages"].as_array().map(|stages| stages.len()),
|
||||
})
|
||||
}),
|
||||
|
|
@ -1802,7 +1801,7 @@ pub(crate) fn compact_git_inspect(output: &Output) -> Value {
|
|||
"conclusion": conclusion.as_object().map(|_| {
|
||||
serde_json::json!({
|
||||
"status": conclusion["status"],
|
||||
"duration_ms": "[DURATION_MS]",
|
||||
"timing": "[TIMING]",
|
||||
"final_git_commit_sha": "[SHA]",
|
||||
"stage_count": conclusion["stages"].as_array().map(|stages| stages.len()),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -96,8 +96,20 @@ fn wait_completed_run_json_outputs_status_and_duration() {
|
|||
let run = setup_seeded_completed_dry_run(&context);
|
||||
let mut filters = context.filters();
|
||||
filters.push((
|
||||
r#""duration_ms":\s*\d+"#.to_string(),
|
||||
r#""duration_ms": [DURATION_MS]"#.to_string(),
|
||||
r#""wall_time_ms":\s*\d+"#.to_string(),
|
||||
r#""wall_time_ms": [WALL_TIME_MS]"#.to_string(),
|
||||
));
|
||||
filters.push((
|
||||
r#""inference_time_ms":\s*\d+"#.to_string(),
|
||||
r#""inference_time_ms": [INFERENCE_TIME_MS]"#.to_string(),
|
||||
));
|
||||
filters.push((
|
||||
r#""tool_time_ms":\s*\d+"#.to_string(),
|
||||
r#""tool_time_ms": [TOOL_TIME_MS]"#.to_string(),
|
||||
));
|
||||
filters.push((
|
||||
r#""active_time_ms":\s*\d+"#.to_string(),
|
||||
r#""active_time_ms": [ACTIVE_TIME_MS]"#.to_string(),
|
||||
));
|
||||
let mut cmd = context.command();
|
||||
cmd.args(["wait", "--json", &run.run_id]);
|
||||
|
|
@ -109,7 +121,12 @@ fn wait_completed_run_json_outputs_status_and_duration() {
|
|||
{
|
||||
"run_id": "[ULID]",
|
||||
"status": "succeeded",
|
||||
"duration_ms": [DURATION_MS]
|
||||
"timing": {
|
||||
"wall_time_ms": [WALL_TIME_MS],
|
||||
"inference_time_ms": [INFERENCE_TIME_MS],
|
||||
"tool_time_ms": [TOOL_TIME_MS],
|
||||
"active_time_ms": [ACTIVE_TIME_MS]
|
||||
}
|
||||
}
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ fn run_sse_body(run_id: &str) -> String {
|
|||
"run_id": run_id,
|
||||
"ts": "2026-04-05T12:00:01Z",
|
||||
"properties": {
|
||||
"duration_ms": 12,
|
||||
"timing": {"wall_time_ms": 12, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0},
|
||||
"artifact_count": 0,
|
||||
"status": "succeeded",
|
||||
"reason": "completed"
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ fn scenario_full_stack(sandbox: &str) {
|
|||
"conclusion: {conclusion}"
|
||||
);
|
||||
assert!(
|
||||
conclusion["duration_ms"].as_u64().unwrap_or(0) > 0,
|
||||
"duration_ms should be > 0"
|
||||
conclusion["timing"]["wall_time_ms"].as_u64().unwrap_or(0) > 0,
|
||||
"timing.wall_time_ms should be > 0"
|
||||
);
|
||||
|
||||
// RunSpec should have key fields
|
||||
|
|
|
|||
|
|
@ -14,9 +14,37 @@ use crate::lifecycle::{
|
|||
AttemptContext, AttemptResultContext, EdgeContext, EdgeDecision, NodeDecision, NoopLifecycle,
|
||||
RunLifecycle,
|
||||
};
|
||||
use crate::outcome::{NodeResult, NodeResultExt, Outcome};
|
||||
use crate::outcome::{NodeResult, NodeResultExt, Outcome, OutcomeMeta};
|
||||
use crate::state::ExecutionState;
|
||||
|
||||
/// Build a [`NodeResult`] from an attempt outcome, pulling the inference and
|
||||
/// tool breakdown from `outcome.timing` when handlers populated it. The wall
|
||||
/// time comes from the executor's stopwatch since that is the source of
|
||||
/// authoritative per-attempt clock time.
|
||||
fn node_result_from_outcome<M: OutcomeMeta>(
|
||||
outcome: Outcome<M>,
|
||||
wall_time: std::time::Duration,
|
||||
attempts: u32,
|
||||
max_attempts: u32,
|
||||
) -> NodeResult<M> {
|
||||
let inference_time = outcome
|
||||
.timing
|
||||
.map(|t| std::time::Duration::from_millis(t.inference_time_ms))
|
||||
.unwrap_or_default();
|
||||
let tool_time = outcome
|
||||
.timing
|
||||
.map(|t| std::time::Duration::from_millis(t.tool_time_ms))
|
||||
.unwrap_or_default();
|
||||
NodeResult::new(
|
||||
outcome,
|
||||
wall_time,
|
||||
inference_time,
|
||||
tool_time,
|
||||
attempts,
|
||||
max_attempts,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ExecutorOptions {
|
||||
pub cancel_token: Option<CancellationToken>,
|
||||
|
|
@ -269,9 +297,9 @@ impl<G: Graph + 'static> Executor<G> {
|
|||
graph: &G,
|
||||
) -> Result<NodeResult<G::Meta>> {
|
||||
let policy = self.handler.retry_policy(node, graph);
|
||||
let start = Instant::now();
|
||||
|
||||
for attempt in 1..=policy.max_attempts {
|
||||
let attempt_start = Instant::now();
|
||||
let attempt_ctx = AttemptContext {
|
||||
node,
|
||||
attempt,
|
||||
|
|
@ -288,8 +316,12 @@ impl<G: Graph + 'static> Executor<G> {
|
|||
match self.handler.execute(node, &state.context, graph).await {
|
||||
Ok(outcome) if outcome.status.retry_requested() && can_retry => {
|
||||
let delay = policy.backoff.delay_for_attempt(attempt);
|
||||
let result =
|
||||
NodeResult::new(outcome, start.elapsed(), attempt, policy.max_attempts);
|
||||
let result = node_result_from_outcome(
|
||||
outcome,
|
||||
attempt_start.elapsed(),
|
||||
attempt,
|
||||
policy.max_attempts,
|
||||
);
|
||||
let ctx = AttemptResultContext {
|
||||
node,
|
||||
result: &result,
|
||||
|
|
@ -302,9 +334,9 @@ impl<G: Graph + 'static> Executor<G> {
|
|||
}
|
||||
Ok(outcome) if outcome.status.retry_requested() => {
|
||||
let final_outcome = self.handler.on_retries_exhausted(node, outcome);
|
||||
let result = NodeResult::new(
|
||||
let result = node_result_from_outcome(
|
||||
final_outcome,
|
||||
start.elapsed(),
|
||||
attempt_start.elapsed(),
|
||||
attempt,
|
||||
policy.max_attempts,
|
||||
);
|
||||
|
|
@ -319,8 +351,12 @@ impl<G: Graph + 'static> Executor<G> {
|
|||
return Ok(result);
|
||||
}
|
||||
Ok(outcome) => {
|
||||
let result =
|
||||
NodeResult::new(outcome, start.elapsed(), attempt, policy.max_attempts);
|
||||
let result = node_result_from_outcome(
|
||||
outcome,
|
||||
attempt_start.elapsed(),
|
||||
attempt,
|
||||
policy.max_attempts,
|
||||
);
|
||||
let ctx = AttemptResultContext {
|
||||
node,
|
||||
result: &result,
|
||||
|
|
@ -333,8 +369,12 @@ impl<G: Graph + 'static> Executor<G> {
|
|||
}
|
||||
Err(e) if can_retry && e.is_retryable() => {
|
||||
let delay = policy.backoff.delay_for_attempt(attempt);
|
||||
let fail_result =
|
||||
NodeResult::from_error(&e, start.elapsed(), attempt, policy.max_attempts);
|
||||
let fail_result = NodeResult::from_error(
|
||||
&e,
|
||||
attempt_start.elapsed(),
|
||||
attempt,
|
||||
policy.max_attempts,
|
||||
);
|
||||
let ctx = AttemptResultContext {
|
||||
node,
|
||||
result: &fail_result,
|
||||
|
|
@ -348,8 +388,12 @@ impl<G: Graph + 'static> Executor<G> {
|
|||
Err(e @ Error::Handler { .. }) => {
|
||||
// Convert handler failures to fail outcomes so routing continues.
|
||||
let outcome = e.to_fail_outcome();
|
||||
let result =
|
||||
NodeResult::new(outcome, start.elapsed(), attempt, policy.max_attempts);
|
||||
let result = node_result_from_outcome(
|
||||
outcome,
|
||||
attempt_start.elapsed(),
|
||||
attempt,
|
||||
policy.max_attempts,
|
||||
);
|
||||
let ctx = AttemptResultContext {
|
||||
node,
|
||||
result: &result,
|
||||
|
|
@ -1289,6 +1333,80 @@ mod tests {
|
|||
assert_eq!(log, vec![(1, true), (2, false)]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn executor_retry_attempt_wall_time_excludes_prior_attempts_and_backoff() {
|
||||
let wall_times = Arc::new(Mutex::new(Vec::<Duration>::new()));
|
||||
|
||||
struct WallTimeTracker(Arc<Mutex<Vec<Duration>>>);
|
||||
#[async_trait]
|
||||
impl RunLifecycle<TestGraph> for WallTimeTracker {
|
||||
async fn after_attempt(
|
||||
&self,
|
||||
ctx: &AttemptResultContext<'_, TestGraph>,
|
||||
_s: &ExecutionState,
|
||||
) -> Result<()> {
|
||||
self.0.lock().unwrap().push(ctx.result.wall_time);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct SlowRetryThenSuccess(AtomicU32);
|
||||
#[async_trait]
|
||||
impl NodeHandler<TestGraph> for SlowRetryThenSuccess {
|
||||
async fn execute(
|
||||
&self,
|
||||
_n: &TestNode,
|
||||
_c: &Context,
|
||||
_g: &TestGraph,
|
||||
) -> Result<Outcome> {
|
||||
sleep(Duration::from_millis(5)).await;
|
||||
let call = self.0.fetch_add(1, Ordering::Relaxed);
|
||||
if call == 0 {
|
||||
Ok(Outcome {
|
||||
status: StageOutcome::Failed {
|
||||
retry_requested: true,
|
||||
},
|
||||
..Outcome::default()
|
||||
})
|
||||
} else {
|
||||
Ok(Outcome::success())
|
||||
}
|
||||
}
|
||||
|
||||
fn retry_policy(&self, _n: &TestNode, _g: &TestGraph) -> RetryPolicy {
|
||||
RetryPolicy {
|
||||
max_attempts: 2,
|
||||
backoff: BackoffPolicy {
|
||||
initial_delay: Duration::from_millis(500),
|
||||
factor: 1.0,
|
||||
max_delay: Duration::from_millis(500),
|
||||
jitter: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let g = linear_graph(&["start", "end"]);
|
||||
let state = ExecutionState::new(&g).unwrap();
|
||||
let executor =
|
||||
ExecutorBuilder::new(Arc::new(SlowRetryThenSuccess(AtomicU32::new(0)))
|
||||
as Arc<dyn NodeHandler<TestGraph>>)
|
||||
.lifecycle(Box::new(WallTimeTracker(Arc::clone(&wall_times))))
|
||||
.build();
|
||||
|
||||
executor.run(&g, state).await.unwrap();
|
||||
|
||||
let wall_times = wall_times.lock().unwrap().clone();
|
||||
assert_eq!(wall_times.len(), 2);
|
||||
for wall_time in wall_times {
|
||||
assert!(wall_time >= Duration::from_millis(5));
|
||||
assert!(
|
||||
wall_time < Duration::from_millis(300),
|
||||
"attempt wall time should not include retry backoff or prior attempts: {wall_time:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn executor_retry_lifecycle_before_attempt_skip_stops_retry() {
|
||||
let call_count = Arc::new(std::sync::atomic::AtomicU32::new(0));
|
||||
|
|
|
|||
|
|
@ -570,7 +570,14 @@ mod tests {
|
|||
let g = linear_graph(&["start", "end"]);
|
||||
let state = ExecutionState::new(&g).unwrap();
|
||||
let node = g.get_node("start").unwrap();
|
||||
let result = NodeResult::new(Outcome::success(), Duration::ZERO, 1, 1);
|
||||
let result = NodeResult::new(
|
||||
Outcome::success(),
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
let ctx = AttemptResultContext {
|
||||
node: &node,
|
||||
result: &result,
|
||||
|
|
@ -667,7 +674,14 @@ mod tests {
|
|||
let g = linear_graph(&["start", "end"]);
|
||||
let state = ExecutionState::new(&g).unwrap();
|
||||
let node = g.get_node("start").unwrap();
|
||||
let mut result = NodeResult::new(Outcome::success(), Duration::ZERO, 1, 1);
|
||||
let mut result = NodeResult::new(
|
||||
Outcome::success(),
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
lc.after_node(&node, &mut result, &state).await.unwrap();
|
||||
let calls = log.lock().unwrap().clone();
|
||||
assert_eq!(calls, vec!["a:after_node", "b:after_node"]);
|
||||
|
|
@ -683,7 +697,14 @@ mod tests {
|
|||
let g = linear_graph(&["start", "end"]);
|
||||
let state = ExecutionState::new(&g).unwrap();
|
||||
let node = g.get_node("start").unwrap();
|
||||
let result = NodeResult::new(Outcome::success(), Duration::ZERO, 1, 1);
|
||||
let result = NodeResult::new(
|
||||
Outcome::success(),
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
lc.after_record(&node, &result, &state).await.unwrap();
|
||||
let calls = log.lock().unwrap().clone();
|
||||
assert_eq!(calls, vec!["a:after_record", "b:after_record"]);
|
||||
|
|
|
|||
|
|
@ -7,14 +7,16 @@ pub use fabro_types::outcome::{
|
|||
use crate::error::Error;
|
||||
|
||||
pub trait NodeResultExt<M: OutcomeMeta = ()> {
|
||||
fn from_error(error: &Error, duration: Duration, attempts: u32, max_attempts: u32) -> Self;
|
||||
fn from_error(error: &Error, wall_time: Duration, attempts: u32, max_attempts: u32) -> Self;
|
||||
}
|
||||
|
||||
impl<M: OutcomeMeta> NodeResultExt<M> for NodeResult<M> {
|
||||
fn from_error(error: &Error, duration: Duration, attempts: u32, max_attempts: u32) -> Self {
|
||||
fn from_error(error: &Error, wall_time: Duration, attempts: u32, max_attempts: u32) -> Self {
|
||||
Self {
|
||||
outcome: error.to_fail_outcome(),
|
||||
duration,
|
||||
wall_time,
|
||||
inference_time: Duration::ZERO,
|
||||
tool_time: Duration::ZERO,
|
||||
attempts,
|
||||
max_attempts,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,14 @@ mod tests {
|
|||
fn run_state_record_updates_all_fields() {
|
||||
let g = linear_graph(&["start", "end"]);
|
||||
let mut state = ExecutionState::<()>::new(&g).unwrap();
|
||||
let result = NodeResult::new(Outcome::success(), Duration::from_millis(50), 2, 3);
|
||||
let result = NodeResult::new(
|
||||
Outcome::success(),
|
||||
Duration::from_millis(50),
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
2,
|
||||
3,
|
||||
);
|
||||
state.record("start", &result);
|
||||
|
||||
assert_eq!(state.completed_nodes, vec!["start"]);
|
||||
|
|
@ -126,7 +133,14 @@ mod tests {
|
|||
let mut state = ExecutionState::<()>::new(&g).unwrap();
|
||||
let mut outcome = Outcome::success();
|
||||
outcome.context_updates.insert("key".into(), json!("value"));
|
||||
let result = NodeResult::new(outcome, Duration::ZERO, 1, 1);
|
||||
let result = NodeResult::new(
|
||||
outcome,
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
state.record("start", &result);
|
||||
assert_eq!(state.context.get("key"), Some(json!("value")));
|
||||
}
|
||||
|
|
@ -155,7 +169,14 @@ mod tests {
|
|||
state.increment_visits("work");
|
||||
state.record(
|
||||
"start",
|
||||
&NodeResult::new(Outcome::success(), Duration::ZERO, 1, 1),
|
||||
&NodeResult::new(
|
||||
Outcome::success(),
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
1,
|
||||
1,
|
||||
),
|
||||
);
|
||||
state.advance("work");
|
||||
|
||||
|
|
|
|||
|
|
@ -548,7 +548,7 @@ mod tests {
|
|||
.single()
|
||||
.unwrap(),
|
||||
status: StageOutcome::Succeeded,
|
||||
duration_ms: 5,
|
||||
timing: fabro_types::RunTiming::wall_only(5),
|
||||
failure: None,
|
||||
final_git_commit_sha: Some("abc123".to_string()),
|
||||
stages: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -1106,7 +1106,8 @@ mod runs {
|
|||
let run_id = RunId::with_timestamp(created_at, sequence);
|
||||
let source_directory = Some(format!("/demo/{repo_name}"));
|
||||
let repo_origin_url = Some(format!("https://github.com/demo/{repo_name}.git"));
|
||||
let duration_ms = elapsed_secs.and_then(duration_ms_from_secs);
|
||||
let wall_time_ms = elapsed_secs.and_then(duration_ms_from_secs);
|
||||
let timing = wall_time_ms.map(fabro_types::RunTiming::wall_only);
|
||||
Run {
|
||||
id: run_id,
|
||||
parent_id: None,
|
||||
|
|
@ -1145,9 +1146,8 @@ mod runs {
|
|||
started_at: Some(created_at),
|
||||
last_event_at: Some(created_at),
|
||||
completed_at: Some(created_at),
|
||||
duration_ms,
|
||||
elapsed_secs,
|
||||
},
|
||||
timing,
|
||||
billing: total_usd_micros.map(|total_usd_micros| RunBillingSummary {
|
||||
total_usd_micros: Some(total_usd_micros),
|
||||
}),
|
||||
|
|
@ -1350,7 +1350,7 @@ mod runs {
|
|||
&StageId::new("detect-drift", 1),
|
||||
"Detect Drift",
|
||||
StageState::Succeeded,
|
||||
Some(72.0),
|
||||
Some(72_000),
|
||||
None,
|
||||
StageHandler::Command,
|
||||
),
|
||||
|
|
@ -1358,7 +1358,7 @@ mod runs {
|
|||
&StageId::new("propose-changes", 1),
|
||||
"Propose Changes",
|
||||
StageState::Succeeded,
|
||||
Some(154.0),
|
||||
Some(154_000),
|
||||
None,
|
||||
StageHandler::Agent,
|
||||
),
|
||||
|
|
@ -1366,7 +1366,7 @@ mod runs {
|
|||
&StageId::new("review-changes", 1),
|
||||
"Review Changes",
|
||||
StageState::Succeeded,
|
||||
Some(45.0),
|
||||
Some(45_000),
|
||||
None,
|
||||
StageHandler::Agent,
|
||||
),
|
||||
|
|
@ -1374,7 +1374,7 @@ mod runs {
|
|||
&StageId::new("apply-changes", 1),
|
||||
"Apply Changes",
|
||||
StageState::Succeeded,
|
||||
Some(118.0),
|
||||
Some(118_000),
|
||||
None,
|
||||
StageHandler::Command,
|
||||
),
|
||||
|
|
@ -1512,15 +1512,15 @@ mod runs {
|
|||
RunBilling {
|
||||
stages: vec![
|
||||
RunBillingStage {
|
||||
stage: BillingStageRef {
|
||||
stage: BillingStageRef {
|
||||
id: "detect-drift".into(),
|
||||
name: "Detect Drift".into(),
|
||||
},
|
||||
model: Some(billing_model(
|
||||
model: Some(billing_model(
|
||||
fabro_model::ProviderId::anthropic(),
|
||||
"claude-opus-4-6",
|
||||
)),
|
||||
billing: BilledTokenCounts {
|
||||
billing: BilledTokenCounts {
|
||||
cache_read_tokens: 0,
|
||||
cache_write_tokens: 0,
|
||||
input_tokens: 12480,
|
||||
|
|
@ -1529,20 +1529,20 @@ mod runs {
|
|||
total_tokens: 15690,
|
||||
total_usd_micros: Some(480_000),
|
||||
},
|
||||
runtime_secs: 72.0,
|
||||
started_at: None,
|
||||
state: Some(StageState::Succeeded),
|
||||
timing: fabro_types::StageTiming::wall_only(72_000),
|
||||
started_at: None,
|
||||
state: Some(StageState::Succeeded),
|
||||
},
|
||||
RunBillingStage {
|
||||
stage: BillingStageRef {
|
||||
stage: BillingStageRef {
|
||||
id: "propose-changes".into(),
|
||||
name: "Propose Changes".into(),
|
||||
},
|
||||
model: Some(billing_model(
|
||||
model: Some(billing_model(
|
||||
fabro_model::ProviderId::gemini(),
|
||||
"gemini-3.1-pro-preview",
|
||||
)),
|
||||
billing: BilledTokenCounts {
|
||||
billing: BilledTokenCounts {
|
||||
cache_read_tokens: 0,
|
||||
cache_write_tokens: 0,
|
||||
input_tokens: 28640,
|
||||
|
|
@ -1551,20 +1551,20 @@ mod runs {
|
|||
total_tokens: 37390,
|
||||
total_usd_micros: Some(720_000),
|
||||
},
|
||||
runtime_secs: 154.0,
|
||||
started_at: None,
|
||||
state: Some(StageState::Succeeded),
|
||||
timing: fabro_types::StageTiming::wall_only(154_000),
|
||||
started_at: None,
|
||||
state: Some(StageState::Succeeded),
|
||||
},
|
||||
RunBillingStage {
|
||||
stage: BillingStageRef {
|
||||
stage: BillingStageRef {
|
||||
id: "review-changes".into(),
|
||||
name: "Review Changes".into(),
|
||||
},
|
||||
model: Some(billing_model(
|
||||
model: Some(billing_model(
|
||||
fabro_model::ProviderId::openai(),
|
||||
"gpt-5.3-codex",
|
||||
)),
|
||||
billing: BilledTokenCounts {
|
||||
billing: BilledTokenCounts {
|
||||
cache_read_tokens: 0,
|
||||
cache_write_tokens: 0,
|
||||
input_tokens: 9120,
|
||||
|
|
@ -1573,20 +1573,20 @@ mod runs {
|
|||
total_tokens: 11760,
|
||||
total_usd_micros: Some(190_000),
|
||||
},
|
||||
runtime_secs: 45.0,
|
||||
started_at: None,
|
||||
state: Some(StageState::Succeeded),
|
||||
timing: fabro_types::StageTiming::wall_only(45_000),
|
||||
started_at: None,
|
||||
state: Some(StageState::Succeeded),
|
||||
},
|
||||
RunBillingStage {
|
||||
stage: BillingStageRef {
|
||||
stage: BillingStageRef {
|
||||
id: "apply-changes".into(),
|
||||
name: "Apply Changes".into(),
|
||||
},
|
||||
model: Some(billing_model(
|
||||
model: Some(billing_model(
|
||||
fabro_model::ProviderId::anthropic(),
|
||||
"claude-opus-4-6",
|
||||
)),
|
||||
billing: BilledTokenCounts {
|
||||
billing: BilledTokenCounts {
|
||||
cache_read_tokens: 0,
|
||||
cache_write_tokens: 0,
|
||||
input_tokens: 21300,
|
||||
|
|
@ -1595,15 +1595,15 @@ mod runs {
|
|||
total_tokens: 27780,
|
||||
total_usd_micros: Some(870_000),
|
||||
},
|
||||
runtime_secs: 118.0,
|
||||
started_at: None,
|
||||
state: Some(StageState::Running),
|
||||
timing: fabro_types::StageTiming::wall_only(118_000),
|
||||
started_at: None,
|
||||
state: Some(StageState::Running),
|
||||
},
|
||||
],
|
||||
totals: RunBillingTotals {
|
||||
cache_read_tokens: 0,
|
||||
cache_write_tokens: 0,
|
||||
runtime_secs: 389.0,
|
||||
timing: fabro_types::RunTiming::wall_only(389_000),
|
||||
input_tokens: 71540,
|
||||
output_tokens: 21080,
|
||||
reasoning_tokens: 0,
|
||||
|
|
@ -1987,7 +1987,7 @@ mod billing {
|
|||
input_tokens: 643_860,
|
||||
output_tokens: 189_720,
|
||||
reasoning_tokens: 0,
|
||||
runtime_secs: 3_501.0,
|
||||
timing: fabro_types::RunTiming::wall_only(3_501_000),
|
||||
total_tokens: 833_580,
|
||||
total_usd_micros: Some(20_340_000),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2390,7 +2390,7 @@ index 1111111..2222222 160000
|
|||
projection.conclusion = Some(fabro_types::Conclusion {
|
||||
timestamp: chrono::Utc::now(),
|
||||
status: fabro_types::StageOutcome::Succeeded,
|
||||
duration_ms: 1,
|
||||
timing: fabro_types::RunTiming::wall_only(1),
|
||||
failure: None,
|
||||
final_git_commit_sha: None,
|
||||
stages: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -252,9 +252,9 @@ struct ModelBillingTotals {
|
|||
/// In-memory aggregate billing counters, reset on server restart.
|
||||
#[derive(Default)]
|
||||
struct BillingAccumulator {
|
||||
total_runs: i64,
|
||||
total_runtime_secs: f64,
|
||||
by_model: HashMap<ModelRef, ModelBillingTotals>,
|
||||
total_runs: i64,
|
||||
total_timing: fabro_types::RunTiming,
|
||||
by_model: HashMap<ModelRef, ModelBillingTotals>,
|
||||
}
|
||||
|
||||
pub(crate) type RegistryFactoryOverride =
|
||||
|
|
@ -703,7 +703,7 @@ fn accumulate_billing_rollup(
|
|||
rollup: &fabro_workflow::ProjectionBillingRollup,
|
||||
) {
|
||||
accumulator.total_runs += 1;
|
||||
accumulator.total_runtime_secs += rollup.runtime_ms as f64 / 1000.0;
|
||||
accumulator.total_timing = accumulator.total_timing.saturating_add(&rollup.timing);
|
||||
for model in &rollup.by_model {
|
||||
let entry = accumulator.by_model.entry(model.model.clone()).or_default();
|
||||
entry.stages += model.stages;
|
||||
|
|
@ -715,7 +715,7 @@ pub(crate) fn run_stage_from_stage_id(
|
|||
stage_id: &StageId,
|
||||
name: impl Into<String>,
|
||||
status: StageState,
|
||||
duration_secs: Option<f64>,
|
||||
wall_time_ms: Option<u64>,
|
||||
started_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
handler: StageHandler,
|
||||
) -> RunStage {
|
||||
|
|
@ -724,7 +724,7 @@ pub(crate) fn run_stage_from_stage_id(
|
|||
name: name.into(),
|
||||
handler,
|
||||
status,
|
||||
duration_secs,
|
||||
wall_time_ms,
|
||||
node_id: stage_id.node_id().to_string(),
|
||||
visit: std::num::NonZeroU32::new(stage_id.visit())
|
||||
.expect("StageId stores a non-zero visit"),
|
||||
|
|
@ -2237,7 +2237,13 @@ pub(crate) async fn reconcile_incomplete_runs_on_startup(
|
|||
"Fabro server restarted before the run reached a terminal state.".to_string(),
|
||||
);
|
||||
let failure_event = workflow_event::Event::workflow_run_failed_from_error(
|
||||
&error, 0, reason, None, None, None, None,
|
||||
&error,
|
||||
fabro_types::RunTiming::default(),
|
||||
reason,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
workflow_event::append_event(&run_store, &summary.id, &failure_event).await?;
|
||||
reconciled += 1;
|
||||
|
|
@ -2282,7 +2288,13 @@ async fn persist_shutdown_run_failures(
|
|||
"Fabro server shut down before the run reached a terminal state.".to_string(),
|
||||
);
|
||||
let failure_event = workflow_event::Event::workflow_run_failed_from_error(
|
||||
&error, 0, reason, None, None, None, None,
|
||||
&error,
|
||||
fabro_types::RunTiming::default(),
|
||||
reason,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
workflow_event::append_event(&run_store, &run_id, &failure_event).await?;
|
||||
}
|
||||
|
|
@ -2354,7 +2366,7 @@ async fn persist_cancelled_run_status(state: &AppState, run_id: RunId) -> anyhow
|
|||
|
||||
let failure_event = workflow_event::Event::workflow_run_failed_from_error(
|
||||
&WorkflowError::Cancelled,
|
||||
0,
|
||||
fabro_types::RunTiming::default(),
|
||||
FailureReason::Cancelled,
|
||||
None,
|
||||
None,
|
||||
|
|
@ -2390,7 +2402,7 @@ async fn fail_run_before_execution(
|
|||
Ok(run_store) => {
|
||||
let failure_event = workflow_event::Event::workflow_run_failed_from_error(
|
||||
&WorkflowError::engine(message.clone()),
|
||||
0,
|
||||
fabro_types::RunTiming::default(),
|
||||
reason,
|
||||
None,
|
||||
None,
|
||||
|
|
@ -2704,7 +2716,13 @@ async fn append_worker_exit_failure(
|
|||
format!("Worker exited before emitting a terminal run event: {wait_status}"),
|
||||
);
|
||||
let failure_event = workflow_event::Event::workflow_run_failed_from_error(
|
||||
&error, 0, reason, None, None, None, None,
|
||||
&error,
|
||||
fabro_types::RunTiming::default(),
|
||||
reason,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
if let Err(err) = workflow_event::append_event(run_store, &run_id, &failure_event).await {
|
||||
|
|
@ -3365,7 +3383,7 @@ async fn execute_run_subprocess(state: Arc<AppState>, run_id: RunId) {
|
|||
let message = format!("Failed to spawn worker: {err}");
|
||||
let failure_event = workflow_event::Event::workflow_run_failed_from_error(
|
||||
&WorkflowError::engine_with_anyhow("Failed to spawn worker", err),
|
||||
0,
|
||||
fabro_types::RunTiming::default(),
|
||||
FailureReason::LaunchFailed,
|
||||
None,
|
||||
None,
|
||||
|
|
@ -3385,7 +3403,7 @@ async fn execute_run_subprocess(state: Arc<AppState>, run_id: RunId) {
|
|||
let _ = child.start_kill();
|
||||
let failure_event = workflow_event::Event::workflow_run_failed_from_error(
|
||||
&WorkflowError::engine(message.clone()),
|
||||
0,
|
||||
fabro_types::RunTiming::default(),
|
||||
FailureReason::LaunchFailed,
|
||||
None,
|
||||
None,
|
||||
|
|
@ -3413,7 +3431,7 @@ async fn execute_run_subprocess(state: Arc<AppState>, run_id: RunId) {
|
|||
let _ = child.start_kill();
|
||||
let failure_event = workflow_event::Event::workflow_run_failed_from_error(
|
||||
&WorkflowError::engine(message.clone()),
|
||||
0,
|
||||
fabro_types::RunTiming::default(),
|
||||
FailureReason::LaunchFailed,
|
||||
None,
|
||||
None,
|
||||
|
|
@ -3432,7 +3450,7 @@ async fn execute_run_subprocess(state: Arc<AppState>, run_id: RunId) {
|
|||
let _ = child.start_kill();
|
||||
let failure_event = workflow_event::Event::workflow_run_failed_from_error(
|
||||
&WorkflowError::engine(message.clone()),
|
||||
0,
|
||||
fabro_types::RunTiming::default(),
|
||||
FailureReason::LaunchFailed,
|
||||
None,
|
||||
None,
|
||||
|
|
@ -3464,7 +3482,7 @@ async fn execute_run_subprocess(state: Arc<AppState>, run_id: RunId) {
|
|||
let _ = child.start_kill();
|
||||
let failure_event = workflow_event::Event::workflow_run_failed_from_error(
|
||||
&WorkflowError::engine_with_source("Worker wait failed", err),
|
||||
0,
|
||||
fabro_types::RunTiming::default(),
|
||||
FailureReason::Terminated,
|
||||
None,
|
||||
None,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::collections::HashMap;
|
|||
use std::sync::Arc;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use fabro_types::{RunProjection, StageHandler, StageProjection, StageState};
|
||||
use fabro_types::{RunProjection, StageHandler, StageProjection, StageState, StageTiming};
|
||||
|
||||
use super::super::{
|
||||
ApiError, AppState, BillingByModel, BillingStageRef, IntoResponse, Json, ListResponse,
|
||||
|
|
@ -54,7 +54,7 @@ async fn list_run_stages(
|
|||
stage_id,
|
||||
stage_id.node_id().to_string(),
|
||||
stage.effective_state(),
|
||||
stage.runtime_secs(now),
|
||||
stage.live_wall_time_ms(now),
|
||||
stage.started_at,
|
||||
handler,
|
||||
)
|
||||
|
|
@ -97,23 +97,25 @@ async fn get_run_billing(
|
|||
.map(|stage| (stage.node_id.as_str(), stage))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let live_rows = live_billing_rows(&projection, Utc::now());
|
||||
let runtime_secs = live_rows.iter().map(|row| row.runtime_secs).sum::<f64>();
|
||||
let totals_timing = live_rows.iter().fold(StageTiming::default(), |acc, row| {
|
||||
acc.saturating_add(&row.timing)
|
||||
});
|
||||
let stages = live_rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
let rollup_stage = rollup_by_node.get(row.node_id.as_str());
|
||||
RunBillingStage {
|
||||
billing: rollup_stage
|
||||
billing: rollup_stage
|
||||
.map(|stage| stage.billing.clone())
|
||||
.unwrap_or_default(),
|
||||
model: rollup_stage.and_then(|stage| stage.model.as_ref()).cloned(),
|
||||
runtime_secs: row.runtime_secs,
|
||||
stage: BillingStageRef {
|
||||
model: rollup_stage.and_then(|stage| stage.model.as_ref()).cloned(),
|
||||
timing: row.timing,
|
||||
stage: BillingStageRef {
|
||||
id: row.node_id.clone(),
|
||||
name: row.node_id,
|
||||
},
|
||||
started_at: row.started_at,
|
||||
state: row.state,
|
||||
started_at: row.started_at,
|
||||
state: row.state,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
|
@ -122,14 +124,14 @@ async fn get_run_billing(
|
|||
by_model,
|
||||
stages,
|
||||
totals: RunBillingTotals {
|
||||
cache_read_tokens: rollup.totals.cache_read_tokens,
|
||||
cache_read_tokens: rollup.totals.cache_read_tokens,
|
||||
cache_write_tokens: rollup.totals.cache_write_tokens,
|
||||
input_tokens: rollup.totals.input_tokens,
|
||||
output_tokens: rollup.totals.output_tokens,
|
||||
reasoning_tokens: rollup.totals.reasoning_tokens,
|
||||
runtime_secs,
|
||||
total_tokens: rollup.totals.total_tokens,
|
||||
total_usd_micros: rollup.totals.total_usd_micros,
|
||||
input_tokens: rollup.totals.input_tokens,
|
||||
output_tokens: rollup.totals.output_tokens,
|
||||
reasoning_tokens: rollup.totals.reasoning_tokens,
|
||||
timing: totals_timing.into(),
|
||||
total_tokens: rollup.totals.total_tokens,
|
||||
total_usd_micros: rollup.totals.total_usd_micros,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -138,7 +140,7 @@ async fn get_run_billing(
|
|||
|
||||
struct LiveBillingRow {
|
||||
node_id: String,
|
||||
runtime_secs: f64,
|
||||
timing: StageTiming,
|
||||
started_at: Option<DateTime<Utc>>,
|
||||
state: Option<StageState>,
|
||||
latest_visit: u32,
|
||||
|
|
@ -158,7 +160,7 @@ fn live_billing_rows(projection: &RunProjection, now: DateTime<Utc>) -> Vec<Live
|
|||
let index = rows.len();
|
||||
rows.push(LiveBillingRow {
|
||||
node_id: node_id.to_string(),
|
||||
runtime_secs: 0.0,
|
||||
timing: StageTiming::default(),
|
||||
started_at: None,
|
||||
state: None,
|
||||
latest_visit: 0,
|
||||
|
|
@ -166,7 +168,8 @@ fn live_billing_rows(projection: &RunProjection, now: DateTime<Utc>) -> Vec<Live
|
|||
index
|
||||
});
|
||||
let row = &mut rows[index];
|
||||
row.runtime_secs += billing_runtime_secs(stage, now).unwrap_or(0.0);
|
||||
let stage_timing = billing_stage_timing(stage, now);
|
||||
row.timing = row.timing.saturating_add(&stage_timing);
|
||||
|
||||
if stage_id.visit() >= row.latest_visit {
|
||||
row.latest_visit = stage_id.visit();
|
||||
|
|
@ -178,16 +181,23 @@ fn live_billing_rows(projection: &RunProjection, now: DateTime<Utc>) -> Vec<Live
|
|||
rows
|
||||
}
|
||||
|
||||
fn billing_runtime_secs(stage: &StageProjection, now: DateTime<Utc>) -> Option<f64> {
|
||||
stage
|
||||
.duration_ms
|
||||
.map(|ms| ms as f64 / 1000.0)
|
||||
.or_else(|| stage.runtime_secs(now))
|
||||
/// Per-visit timing for a stage. For terminal visits, the stored breakdown is
|
||||
/// used directly. For in-flight visits, fall back to the live wall-clock since
|
||||
/// `started_at` (no active breakdown yet — that is only finalized at terminal
|
||||
/// event time in v1).
|
||||
fn billing_stage_timing(stage: &StageProjection, now: DateTime<Utc>) -> StageTiming {
|
||||
if let Some(timing) = stage.timing {
|
||||
return timing;
|
||||
}
|
||||
if let Some(live_wall) = stage.live_wall_time_ms(now) {
|
||||
return StageTiming::wall_only(live_wall);
|
||||
}
|
||||
StageTiming::default()
|
||||
}
|
||||
|
||||
fn stage_has_billing_row(stage: &StageProjection) -> bool {
|
||||
stage.completion.is_some()
|
||||
|| stage.duration_ms.is_some()
|
||||
|| stage.timing.is_some()
|
||||
|| !stage.usage.is_zero()
|
||||
|| stage.started_at.is_some()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -541,7 +541,7 @@ async fn get_aggregate_billing(
|
|||
output_tokens: total_billing.output_tokens,
|
||||
reasoning_tokens: total_billing.reasoning_tokens,
|
||||
runs: agg.total_runs,
|
||||
runtime_secs: agg.total_runtime_secs,
|
||||
timing: agg.total_timing,
|
||||
total_tokens: total_billing.total_tokens,
|
||||
total_usd_micros: total_billing.total_usd_micros,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2743,7 +2743,7 @@ async fn persist_cancelled_run_status_ignores_already_terminal_runs() {
|
|||
let run_id = fixtures::RUN_1;
|
||||
create_durable_run_with_events(&state, run_id, &[
|
||||
workflow_event::Event::WorkflowRunCompleted {
|
||||
duration_ms: 1000,
|
||||
timing: fabro_types::RunTiming::wall_only(1000),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
@ -2779,7 +2779,7 @@ async fn delete_terminal_managed_run_does_not_send_cancel_signal() {
|
|||
let run_id = fixtures::RUN_1;
|
||||
create_durable_run_with_events(&state, run_id, &[
|
||||
workflow_event::Event::WorkflowRunCompleted {
|
||||
duration_ms: 1000,
|
||||
timing: fabro_types::RunTiming::wall_only(1000),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
@ -2889,7 +2889,7 @@ async fn list_run_stages_projects_retrying_until_completion() {
|
|||
node_id: "setup".to_string(),
|
||||
name: "Setup".to_string(),
|
||||
index: 0,
|
||||
duration_ms: 5,
|
||||
timing: fabro_types::StageTiming::wall_only(5),
|
||||
status: "succeeded".to_string(),
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
|
|
@ -2930,14 +2930,14 @@ async fn list_run_stages_projects_retrying_until_completion() {
|
|||
"work",
|
||||
1,
|
||||
&workflow_event::Event::StageFailed {
|
||||
node_id: "work".to_string(),
|
||||
name: "Work".to_string(),
|
||||
index: 1,
|
||||
failure: FailureDetail::new("try again", FailureCategory::TransientInfra),
|
||||
will_retry: true,
|
||||
duration_ms: 10,
|
||||
billing: None,
|
||||
actor: None,
|
||||
node_id: "work".to_string(),
|
||||
name: "Work".to_string(),
|
||||
index: 1,
|
||||
failure: FailureDetail::new("try again", FailureCategory::TransientInfra),
|
||||
will_retry: true,
|
||||
timing: fabro_types::StageTiming::wall_only(10),
|
||||
billing: None,
|
||||
actor: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
|
@ -2981,7 +2981,7 @@ async fn list_run_stages_projects_retrying_until_completion() {
|
|||
node_id: "work".to_string(),
|
||||
name: "Work".to_string(),
|
||||
index: 1,
|
||||
duration_ms: 25,
|
||||
timing: fabro_types::StageTiming::wall_only(25),
|
||||
status: "partially_succeeded".to_string(),
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
|
|
@ -3111,7 +3111,7 @@ async fn list_run_stages_distinguishes_visits() {
|
|||
node_id: "verify".to_string(),
|
||||
name: "Verify".to_string(),
|
||||
index: 1,
|
||||
duration_ms: 1500,
|
||||
timing: fabro_types::StageTiming::wall_only(1500),
|
||||
status: "failed".to_string(),
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
|
|
@ -3171,7 +3171,7 @@ async fn list_run_stages_distinguishes_visits() {
|
|||
assert_eq!(first["visit"], 1);
|
||||
assert_eq!(first["handler"], "command");
|
||||
assert_eq!(first["status"], "failed");
|
||||
assert_eq!(first["duration_secs"], 1.5);
|
||||
assert_eq!(first["wall_time_ms"], 1500);
|
||||
|
||||
let second = stage_entry(&body, "verify@2");
|
||||
assert_eq!(second["node_id"], "verify");
|
||||
|
|
@ -3211,7 +3211,7 @@ async fn run_billing_dedups_retried_nodes_and_sums_their_durations() {
|
|||
node_id: "verify".to_string(),
|
||||
name: "Verify".to_string(),
|
||||
index: 1,
|
||||
duration_ms: 1500,
|
||||
timing: fabro_types::StageTiming::wall_only(1500),
|
||||
status: "failed".to_string(),
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
|
|
@ -3242,7 +3242,7 @@ async fn run_billing_dedups_retried_nodes_and_sums_their_durations() {
|
|||
node_id: "verify".to_string(),
|
||||
name: "Verify".to_string(),
|
||||
index: 1,
|
||||
duration_ms: 800,
|
||||
timing: fabro_types::StageTiming::wall_only(800),
|
||||
status: "succeeded".to_string(),
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
|
|
@ -3314,16 +3314,16 @@ async fn run_billing_dedups_retried_nodes_and_sums_their_durations() {
|
|||
assert_eq!(stages[0]["stage"]["id"], "verify");
|
||||
// Duration on the row is the sum across visits (1.5s + 0.8s = 2.3s).
|
||||
assert!(
|
||||
(stages[0]["runtime_secs"].as_f64().unwrap() - 2.3).abs() < f64::EPSILON,
|
||||
stages[0]["timing"]["wall_time_ms"].as_u64().unwrap() == 2300,
|
||||
"row runtime_secs should sum visits, got {}",
|
||||
stages[0]["runtime_secs"]
|
||||
stages[0]["timing"]["wall_time_ms"]
|
||||
);
|
||||
|
||||
// Totals must not double-count: a single 2.3s, not 4.6s.
|
||||
assert!(
|
||||
(body["totals"]["runtime_secs"].as_f64().unwrap() - 2.3).abs() < f64::EPSILON,
|
||||
body["totals"]["timing"]["wall_time_ms"].as_u64().unwrap() == 2300,
|
||||
"totals.runtime_secs should sum visits exactly once, got {}",
|
||||
body["totals"]["runtime_secs"]
|
||||
body["totals"]["timing"]["wall_time_ms"]
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -3350,14 +3350,14 @@ async fn run_billing_sums_usage_across_retry_visits_and_uses_latest_model() {
|
|||
"verify",
|
||||
1,
|
||||
&workflow_event::Event::StageFailed {
|
||||
node_id: "verify".to_string(),
|
||||
name: "Verify".to_string(),
|
||||
index: 1,
|
||||
failure: FailureDetail::new("try again", FailureCategory::TransientInfra),
|
||||
will_retry: true,
|
||||
duration_ms: 1200,
|
||||
billing: Some(failed_usage),
|
||||
actor: None,
|
||||
node_id: "verify".to_string(),
|
||||
name: "Verify".to_string(),
|
||||
index: 1,
|
||||
failure: FailureDetail::new("try again", FailureCategory::TransientInfra),
|
||||
will_retry: true,
|
||||
timing: fabro_types::StageTiming::wall_only(1200),
|
||||
billing: Some(failed_usage),
|
||||
actor: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
|
@ -3370,7 +3370,7 @@ async fn run_billing_sums_usage_across_retry_visits_and_uses_latest_model() {
|
|||
node_id: "verify".to_string(),
|
||||
name: "Verify".to_string(),
|
||||
index: 1,
|
||||
duration_ms: 800,
|
||||
timing: fabro_types::StageTiming::wall_only(800),
|
||||
status: "succeeded".to_string(),
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
|
|
@ -3393,7 +3393,7 @@ async fn run_billing_sums_usage_across_retry_visits_and_uses_latest_model() {
|
|||
|
||||
let mut latest_outcome: Outcome<Option<fabro_model::BilledModelUsage>> = Outcome::success();
|
||||
latest_outcome.usage = Some(success_usage);
|
||||
latest_outcome.duration_ms = Some(800);
|
||||
latest_outcome.timing = Some(fabro_types::StageTiming::wall_only(800));
|
||||
let run_store = state.store.open_run(&run_id).await.unwrap();
|
||||
workflow_event::append_event(
|
||||
&run_store,
|
||||
|
|
@ -3442,12 +3442,12 @@ async fn run_billing_sums_usage_across_retry_visits_and_uses_latest_model() {
|
|||
assert_eq!(stages[0]["billing"]["input_tokens"], 300);
|
||||
assert_eq!(stages[0]["billing"]["output_tokens"], 30);
|
||||
assert_eq!(stages[0]["billing"]["total_usd_micros"], 330);
|
||||
assert!((stages[0]["runtime_secs"].as_f64().unwrap() - 2.0).abs() < f64::EPSILON);
|
||||
assert!(stages[0]["timing"]["wall_time_ms"].as_u64().unwrap() == 2000);
|
||||
|
||||
assert_eq!(body["totals"]["input_tokens"], 300);
|
||||
assert_eq!(body["totals"]["output_tokens"], 30);
|
||||
assert_eq!(body["totals"]["total_usd_micros"], 330);
|
||||
assert!((body["totals"]["runtime_secs"].as_f64().unwrap() - 2.0).abs() < f64::EPSILON);
|
||||
assert!(body["totals"]["timing"]["wall_time_ms"].as_u64().unwrap() == 2000);
|
||||
|
||||
let by_model = body["by_model"].as_array().unwrap();
|
||||
assert_eq!(by_model.len(), 2);
|
||||
|
|
@ -3503,14 +3503,14 @@ async fn list_run_stages_shows_retrying_after_failed_event() {
|
|||
"work",
|
||||
1,
|
||||
&workflow_event::Event::StageFailed {
|
||||
node_id: "work".to_string(),
|
||||
name: "Work".to_string(),
|
||||
index: 0,
|
||||
failure: FailureDetail::new("flake", FailureCategory::TransientInfra),
|
||||
will_retry: true,
|
||||
duration_ms: 5,
|
||||
billing: None,
|
||||
actor: None,
|
||||
node_id: "work".to_string(),
|
||||
name: "Work".to_string(),
|
||||
index: 0,
|
||||
failure: FailureDetail::new("flake", FailureCategory::TransientInfra),
|
||||
will_retry: true,
|
||||
timing: fabro_types::StageTiming::wall_only(5),
|
||||
billing: None,
|
||||
actor: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
|
@ -3583,14 +3583,14 @@ async fn list_run_stages_shows_retrying_when_failed_will_retry() {
|
|||
"work",
|
||||
1,
|
||||
&workflow_event::Event::StageFailed {
|
||||
node_id: "work".to_string(),
|
||||
name: "Work".to_string(),
|
||||
index: 0,
|
||||
failure: FailureDetail::new("flake", FailureCategory::TransientInfra),
|
||||
will_retry: true,
|
||||
duration_ms: 5,
|
||||
billing: None,
|
||||
actor: None,
|
||||
node_id: "work".to_string(),
|
||||
name: "Work".to_string(),
|
||||
index: 0,
|
||||
failure: FailureDetail::new("flake", FailureCategory::TransientInfra),
|
||||
will_retry: true,
|
||||
timing: fabro_types::StageTiming::wall_only(5),
|
||||
billing: None,
|
||||
actor: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
|
@ -3631,14 +3631,14 @@ async fn run_billing_retried_node_then_succeeded_emits_one_row_with_final_attemp
|
|||
max_attempts: 3,
|
||||
},
|
||||
workflow_event::Event::StageFailed {
|
||||
node_id: "work".to_string(),
|
||||
name: "Work".to_string(),
|
||||
index: 0,
|
||||
failure: FailureDetail::new("transient", FailureCategory::TransientInfra),
|
||||
will_retry: true,
|
||||
duration_ms: 10,
|
||||
billing: None,
|
||||
actor: None,
|
||||
node_id: "work".to_string(),
|
||||
name: "Work".to_string(),
|
||||
index: 0,
|
||||
failure: FailureDetail::new("transient", FailureCategory::TransientInfra),
|
||||
will_retry: true,
|
||||
timing: fabro_types::StageTiming::wall_only(10),
|
||||
billing: None,
|
||||
actor: None,
|
||||
},
|
||||
workflow_event::Event::StageRetrying {
|
||||
node_id: "work".to_string(),
|
||||
|
|
@ -3660,7 +3660,7 @@ async fn run_billing_retried_node_then_succeeded_emits_one_row_with_final_attemp
|
|||
node_id: "work".to_string(),
|
||||
name: "Work".to_string(),
|
||||
index: 0,
|
||||
duration_ms: 25,
|
||||
timing: fabro_types::StageTiming::wall_only(25),
|
||||
status: "succeeded".to_string(),
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
|
|
@ -3700,9 +3700,9 @@ async fn run_billing_retried_node_then_succeeded_emits_one_row_with_final_attemp
|
|||
row["state"], "succeeded",
|
||||
"final state mirrors the latest StageCompleted"
|
||||
);
|
||||
let runtime = row["runtime_secs"].as_f64().unwrap();
|
||||
assert!(
|
||||
(runtime - 0.025).abs() < f64::EPSILON,
|
||||
let runtime = row["timing"]["wall_time_ms"].as_u64().unwrap();
|
||||
assert_eq!(
|
||||
runtime, 25,
|
||||
"runtime should equal final attempt's 25ms, got {runtime}"
|
||||
);
|
||||
}
|
||||
|
|
@ -3729,7 +3729,7 @@ fn revisit_test_completed_with_visit(
|
|||
node_id: node_id.to_string(),
|
||||
name: node_id.to_string(),
|
||||
index: 0,
|
||||
duration_ms,
|
||||
timing: fabro_types::StageTiming::wall_only(duration_ms),
|
||||
status: "succeeded".to_string(),
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
|
|
@ -3790,14 +3790,14 @@ async fn run_billing_revisited_node_collapses_to_two_rows_with_summed_visit_dura
|
|||
"A appeared first → A's row first"
|
||||
);
|
||||
assert_eq!(stages[1]["stage"]["id"], "b");
|
||||
let a_runtime = stages[0]["runtime_secs"].as_f64().unwrap();
|
||||
assert!(
|
||||
(a_runtime - 0.1).abs() < f64::EPSILON,
|
||||
let a_runtime = stages[0]["timing"]["wall_time_ms"].as_u64().unwrap();
|
||||
assert_eq!(
|
||||
a_runtime, 100,
|
||||
"A should sum both visit durations (1ms + 99ms), got {a_runtime}"
|
||||
);
|
||||
let b_runtime = stages[1]["runtime_secs"].as_f64().unwrap();
|
||||
assert!(
|
||||
(b_runtime - 0.002).abs() < f64::EPSILON,
|
||||
let b_runtime = stages[1]["timing"]["wall_time_ms"].as_u64().unwrap();
|
||||
assert_eq!(
|
||||
b_runtime, 2,
|
||||
"B should carry its single visit's duration (2ms), got {b_runtime}"
|
||||
);
|
||||
}
|
||||
|
|
@ -3843,7 +3843,12 @@ async fn create_unreadable_durable_run(state: &Arc<AppState>, run_id: RunId) {
|
|||
"run_id": run_id,
|
||||
"event": "run.completed",
|
||||
"properties": {
|
||||
"duration_ms": 1,
|
||||
"timing": {
|
||||
"wall_time_ms": 1,
|
||||
"inference_time_ms": 0,
|
||||
"tool_time_ms": 0,
|
||||
"active_time_ms": 0
|
||||
},
|
||||
"artifact_count": 0,
|
||||
"status": "legacy-status",
|
||||
"reason": "completed",
|
||||
|
|
@ -4086,7 +4091,7 @@ async fn create_completed_run_ready_for_pull_request(
|
|||
goal: Some("Ship the server-side PR".to_string()),
|
||||
},
|
||||
workflow_event::Event::WorkflowRunCompleted {
|
||||
duration_ms: 1,
|
||||
timing: fabro_types::RunTiming::wall_only(1),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
@ -7785,7 +7790,7 @@ async fn patch_run_title_updates_active_and_archived_runs() {
|
|||
&run_store,
|
||||
&run_id,
|
||||
&workflow_event::Event::WorkflowRunCompleted {
|
||||
duration_ms: 1,
|
||||
timing: fabro_types::RunTiming::wall_only(1),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
@ -7933,7 +7938,7 @@ async fn cancel_terminal_durable_run_returns_conflict() {
|
|||
let run_id = fixtures::RUN_1;
|
||||
create_durable_run_with_events(&state, run_id, &[
|
||||
workflow_event::Event::WorkflowRunCompleted {
|
||||
duration_ms: 1000,
|
||||
timing: fabro_types::RunTiming::wall_only(1000),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
@ -7983,7 +7988,7 @@ async fn steer_terminal_durable_run_returns_run_not_steerable() {
|
|||
let run_id = fixtures::RUN_1;
|
||||
create_durable_run_with_events(&state, run_id, &[
|
||||
workflow_event::Event::WorkflowRunCompleted {
|
||||
duration_ms: 1000,
|
||||
timing: fabro_types::RunTiming::wall_only(1000),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
@ -8402,7 +8407,7 @@ async fn active_acp_steerable_marker_clears_on_terminal_paths() {
|
|||
node_id: "agent".to_string(),
|
||||
name: "agent".to_string(),
|
||||
index: 0,
|
||||
duration_ms: 1,
|
||||
timing: fabro_types::StageTiming::wall_only(1),
|
||||
status: "success".to_string(),
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
|
|
@ -8421,14 +8426,14 @@ async fn active_acp_steerable_marker_clears_on_terminal_paths() {
|
|||
max_attempts: 1,
|
||||
},
|
||||
workflow_event::Event::StageFailed {
|
||||
node_id: "agent".to_string(),
|
||||
name: "agent".to_string(),
|
||||
index: 0,
|
||||
failure: FailureDetail::new("failed", FailureCategory::Deterministic),
|
||||
will_retry: false,
|
||||
duration_ms: 1,
|
||||
billing: None,
|
||||
actor: None,
|
||||
node_id: "agent".to_string(),
|
||||
name: "agent".to_string(),
|
||||
index: 0,
|
||||
failure: FailureDetail::new("failed", FailureCategory::Deterministic),
|
||||
will_retry: false,
|
||||
timing: fabro_types::StageTiming::wall_only(1),
|
||||
billing: None,
|
||||
actor: None,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -8837,7 +8842,7 @@ async fn archive_and_unarchive_updates_listing_visibility() {
|
|||
workflow_event::Event::RunStarting,
|
||||
workflow_event::Event::RunRunning,
|
||||
workflow_event::Event::WorkflowRunCompleted {
|
||||
duration_ms: 1000,
|
||||
timing: fabro_types::RunTiming::wall_only(1000),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
@ -9171,7 +9176,7 @@ async fn delete_run_retry_after_missing_provider_resource_removes_metadata() {
|
|||
primary_repo_link: None,
|
||||
},
|
||||
workflow_event::Event::WorkflowRunCompleted {
|
||||
duration_ms: 1,
|
||||
timing: fabro_types::RunTiming::wall_only(1),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
@ -9299,7 +9304,10 @@ async fn get_aggregate_billing_returns_zeros_initially() {
|
|||
assert_eq!(body["totals"]["runs"].as_i64().unwrap(), 0);
|
||||
assert_eq!(body["totals"]["input_tokens"].as_i64().unwrap(), 0);
|
||||
assert_eq!(body["totals"]["output_tokens"].as_i64().unwrap(), 0);
|
||||
assert_eq!(body["totals"]["runtime_secs"].as_f64().unwrap(), 0.0);
|
||||
assert_eq!(
|
||||
body["totals"]["timing"]["wall_time_ms"].as_u64().unwrap(),
|
||||
0
|
||||
);
|
||||
assert!(body["totals"]["total_usd_micros"].is_null());
|
||||
assert!(body["by_model"].as_array().unwrap().is_empty());
|
||||
}
|
||||
|
|
@ -9434,14 +9442,14 @@ fn aggregate_billing_counts_projection_rollup_usage_visits() {
|
|||
},
|
||||
},
|
||||
],
|
||||
runtime_ms: 2000,
|
||||
timing: fabro_types::RunTiming::wall_only(2000),
|
||||
billed_visit_count: 2,
|
||||
};
|
||||
|
||||
accumulate_billing_rollup(&mut accumulator, &rollup);
|
||||
|
||||
assert_eq!(accumulator.total_runs, 1);
|
||||
assert_eq!(accumulator.total_runtime_secs, 2.0);
|
||||
assert_eq!(accumulator.total_timing.wall_time_ms, 2000);
|
||||
assert_eq!(accumulator.by_model.len(), 2);
|
||||
assert_eq!(
|
||||
accumulator.by_model[&ModelRef {
|
||||
|
|
@ -10671,7 +10679,7 @@ async fn boards_runs_excludes_archived_by_default() {
|
|||
workflow_event::Event::RunStarting,
|
||||
workflow_event::Event::RunRunning,
|
||||
workflow_event::Event::WorkflowRunCompleted {
|
||||
duration_ms: 1000,
|
||||
timing: fabro_types::RunTiming::wall_only(1000),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
@ -10720,7 +10728,7 @@ async fn boards_runs_includes_archived_when_flag_set() {
|
|||
workflow_event::Event::RunStarting,
|
||||
workflow_event::Event::RunRunning,
|
||||
workflow_event::Event::WorkflowRunCompleted {
|
||||
duration_ms: 1000,
|
||||
timing: fabro_types::RunTiming::wall_only(1000),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
@ -10740,7 +10748,7 @@ async fn boards_runs_includes_archived_when_flag_set() {
|
|||
workflow_event::Event::RunStarting,
|
||||
workflow_event::Event::RunRunning,
|
||||
workflow_event::Event::WorkflowRunCompleted {
|
||||
duration_ms: 1000,
|
||||
timing: fabro_types::RunTiming::wall_only(1000),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
@ -10813,7 +10821,7 @@ async fn get_run_exposes_canonical_operator_statuses() {
|
|||
workflow_event::Event::RunStarting,
|
||||
workflow_event::Event::RunRunning,
|
||||
workflow_event::Event::WorkflowRunCompleted {
|
||||
duration_ms: 1000,
|
||||
timing: fabro_types::RunTiming::wall_only(1000),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
@ -10898,7 +10906,7 @@ async fn boards_runs_maps_statuses_to_columns() {
|
|||
workflow_event::Event::RunStarting,
|
||||
workflow_event::Event::RunRunning,
|
||||
workflow_event::Event::WorkflowRunCompleted {
|
||||
duration_ms: 1000,
|
||||
timing: fabro_types::RunTiming::wall_only(1000),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ async fn append_completed_run_with_final_patch(
|
|||
&run_store,
|
||||
run_id,
|
||||
&workflow_event::Event::WorkflowRunCompleted {
|
||||
duration_ms: 1,
|
||||
timing: fabro_types::RunTiming::wall_only(1),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
|
|||
|
|
@ -133,12 +133,12 @@ fn assert_non_llm_billing(billing: &serde_json::Value, expected_stage_ids: &[&st
|
|||
"every non-LLM stage should have null model and zero token counts: {stages:?}"
|
||||
);
|
||||
|
||||
let runtime_secs: f64 = stages
|
||||
let stage_wall_sum: u64 = stages
|
||||
.iter()
|
||||
.map(|stage| {
|
||||
stage["runtime_secs"]
|
||||
.as_f64()
|
||||
.expect("stage should include runtime_secs")
|
||||
stage["timing"]["wall_time_ms"]
|
||||
.as_u64()
|
||||
.expect("stage should include timing.wall_time_ms")
|
||||
})
|
||||
.sum();
|
||||
|
||||
|
|
@ -153,11 +153,11 @@ fn assert_non_llm_billing(billing: &serde_json::Value, expected_stage_ids: &[&st
|
|||
assert_eq!(billing["totals"]["output_tokens"], 0);
|
||||
assert!(billing["totals"]["total_usd_micros"].is_null());
|
||||
|
||||
let total_runtime_secs = billing["totals"]["runtime_secs"]
|
||||
.as_f64()
|
||||
.expect("totals should include runtime_secs");
|
||||
assert!(
|
||||
(total_runtime_secs - runtime_secs).abs() < f64::EPSILON,
|
||||
"total runtime {total_runtime_secs} should equal summed stage runtime {runtime_secs}"
|
||||
let total_wall_time_ms = billing["totals"]["timing"]["wall_time_ms"]
|
||||
.as_u64()
|
||||
.expect("totals should include timing.wall_time_ms");
|
||||
assert_eq!(
|
||||
total_wall_time_ms, stage_wall_sum,
|
||||
"total wall_time_ms {total_wall_time_ms} should equal summed stage wall_time_ms {stage_wall_sum}"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -333,7 +333,7 @@ impl RunProjectionReducer for RunProjection {
|
|||
};
|
||||
stage.response = response;
|
||||
stage.completion = Some(completion);
|
||||
stage.duration_ms = Some(props.duration_ms);
|
||||
stage.timing = Some(props.timing);
|
||||
if let Some(billing) = &props.billing {
|
||||
stage.usage.replace_with_billed_usage(billing);
|
||||
stage.model = Some(billing.model().clone());
|
||||
|
|
@ -354,7 +354,7 @@ impl RunProjectionReducer for RunProjection {
|
|||
failure_reason,
|
||||
timestamp: ts,
|
||||
});
|
||||
stage.duration_ms = Some(props.duration_ms);
|
||||
stage.timing = Some(props.timing);
|
||||
if let Some(billing) = &props.billing {
|
||||
stage.usage.replace_with_billed_usage(billing);
|
||||
stage.model = Some(billing.model().clone());
|
||||
|
|
@ -620,10 +620,10 @@ pub(crate) fn build_summary(state: &RunProjection, run_id: &RunId) -> Run {
|
|||
.conclusion
|
||||
.as_ref()
|
||||
.map(|conclusion| conclusion.timestamp);
|
||||
let duration_ms = state
|
||||
let run_timing = state
|
||||
.conclusion
|
||||
.as_ref()
|
||||
.map(|conclusion| conclusion.duration_ms);
|
||||
.map(|conclusion| conclusion.timing);
|
||||
let total_usd_micros = state
|
||||
.conclusion
|
||||
.as_ref()
|
||||
|
|
@ -669,9 +669,8 @@ pub(crate) fn build_summary(state: &RunProjection, run_id: &RunId) -> Run {
|
|||
started_at: start_time,
|
||||
last_event_at: Some(state.last_event_at),
|
||||
completed_at,
|
||||
duration_ms,
|
||||
elapsed_secs: elapsed_secs(duration_ms),
|
||||
},
|
||||
timing: run_timing,
|
||||
billing: total_usd_micros.map(|total_usd_micros| RunBillingSummary {
|
||||
total_usd_micros: Some(total_usd_micros),
|
||||
}),
|
||||
|
|
@ -703,10 +702,6 @@ fn run_models(state: &RunProjection) -> Vec<RunModel> {
|
|||
models
|
||||
}
|
||||
|
||||
fn elapsed_secs(duration_ms: Option<u64>) -> Option<f64> {
|
||||
duration_ms.map(|ms| ms as f64 / 1000.0)
|
||||
}
|
||||
|
||||
fn checkpoint_from_props(props: &CheckpointCompletedProps, timestamp: DateTime<Utc>) -> Checkpoint {
|
||||
let loop_failure_signatures = props
|
||||
.loop_failure_signatures
|
||||
|
|
@ -751,7 +746,7 @@ fn conclusion_from_completed(
|
|||
timestamp,
|
||||
status: StageOutcome::from_str(&props.status)
|
||||
.map_err(|err| Error::InvalidEvent(format!("invalid completed stage status: {err}")))?,
|
||||
duration_ms: props.duration_ms,
|
||||
timing: props.timing,
|
||||
failure: None,
|
||||
final_git_commit_sha: props.final_git_commit_sha.clone(),
|
||||
stages: Vec::new(),
|
||||
|
|
@ -770,7 +765,7 @@ fn conclusion_from_failed(props: &RunFailedProps, timestamp: DateTime<Utc>) -> C
|
|||
status: StageOutcome::Failed {
|
||||
retry_requested: false,
|
||||
},
|
||||
duration_ms: props.duration_ms,
|
||||
timing: props.timing,
|
||||
failure: Some(props.failure.clone()),
|
||||
final_git_commit_sha: props.final_git_commit_sha.clone(),
|
||||
stages: Vec::new(),
|
||||
|
|
@ -811,7 +806,7 @@ fn stage_outcome_from_props(props: &StageCompletedProps) -> Outcome<Option<Bille
|
|||
failure: props.failure.clone(),
|
||||
usage: props.billing.clone(),
|
||||
files_touched: props.files_touched.clone(),
|
||||
duration_ms: Some(props.duration_ms),
|
||||
timing: Some(props.timing),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1467,7 +1462,7 @@ mod tests {
|
|||
3,
|
||||
EventBody::StageCompleted(StageCompletedProps {
|
||||
index: 0,
|
||||
duration_ms: 789,
|
||||
timing: fabro_types::StageTiming::wall_only(789),
|
||||
status: StageOutcome::Succeeded,
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
|
|
@ -1490,7 +1485,7 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
let stage = state.stage(&StageId::new("build", 1)).unwrap();
|
||||
assert_eq!(stage.duration_ms, Some(789));
|
||||
assert_eq!(stage.timing.map(|t| t.wall_time_ms), Some(789));
|
||||
assert_eq!(stage.usage, usage_counts(&usage));
|
||||
assert_eq!(stage.model.as_ref(), Some(usage.model()));
|
||||
}
|
||||
|
|
@ -1524,7 +1519,7 @@ mod tests {
|
|||
"category": "transient_infra"
|
||||
},
|
||||
"will_retry": false,
|
||||
"duration_ms": 654,
|
||||
"timing": {"wall_time_ms": 654, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0},
|
||||
"billing": usage_json(&usage)
|
||||
}),
|
||||
Some("build"),
|
||||
|
|
@ -1532,7 +1527,7 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
let stage = state.stage(&stage_id).unwrap();
|
||||
assert_eq!(stage.duration_ms, Some(654));
|
||||
assert_eq!(stage.timing.map(|t| t.wall_time_ms), Some(654));
|
||||
assert_eq!(stage.usage, usage_counts(&usage));
|
||||
assert_eq!(stage.model.as_ref(), Some(usage.model()));
|
||||
}
|
||||
|
|
@ -1552,7 +1547,7 @@ mod tests {
|
|||
seq,
|
||||
EventBody::StageCompleted(StageCompletedProps {
|
||||
index: 0,
|
||||
duration_ms,
|
||||
timing: fabro_types::StageTiming::wall_only(duration_ms),
|
||||
status: StageOutcome::Succeeded,
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
|
|
@ -1577,10 +1572,10 @@ mod tests {
|
|||
|
||||
let first_stage = state.stage(&StageId::new("build", 1)).unwrap();
|
||||
let second_stage = state.stage(&StageId::new("build", 2)).unwrap();
|
||||
assert_eq!(first_stage.duration_ms, Some(111));
|
||||
assert_eq!(first_stage.timing.map(|t| t.wall_time_ms), Some(111));
|
||||
assert_eq!(first_stage.usage, usage_counts(&first_usage));
|
||||
assert_eq!(first_stage.model.as_ref(), Some(first_usage.model()));
|
||||
assert_eq!(second_stage.duration_ms, Some(222));
|
||||
assert_eq!(second_stage.timing.map(|t| t.wall_time_ms), Some(222));
|
||||
assert_eq!(second_stage.usage, usage_counts(&second_usage));
|
||||
assert_eq!(second_stage.model.as_ref(), Some(second_usage.model()));
|
||||
}
|
||||
|
|
@ -1596,7 +1591,7 @@ mod tests {
|
|||
3,
|
||||
EventBody::StageCompleted(StageCompletedProps {
|
||||
index: 0,
|
||||
duration_ms: 333,
|
||||
timing: fabro_types::StageTiming::wall_only(333),
|
||||
status: StageOutcome::Succeeded,
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
|
|
@ -1623,7 +1618,7 @@ mod tests {
|
|||
"legacy node_visits must not override stored stage_id"
|
||||
);
|
||||
let stage = state.stage(&scoped_stage_id).unwrap();
|
||||
assert_eq!(stage.duration_ms, Some(333));
|
||||
assert_eq!(stage.timing.map(|t| t.wall_time_ms), Some(333));
|
||||
assert_eq!(stage.usage, usage_counts(&usage));
|
||||
assert_eq!(stage.model.as_ref(), Some(usage.model()));
|
||||
assert_eq!(stage.response.as_deref(), Some("done"));
|
||||
|
|
@ -1639,14 +1634,14 @@ mod tests {
|
|||
.apply_event(&test_stage_event(
|
||||
3,
|
||||
EventBody::StageFailed(StageFailedProps {
|
||||
index: 0,
|
||||
failure: Some(fabro_types::FailureDetail::new(
|
||||
index: 0,
|
||||
failure: Some(fabro_types::FailureDetail::new(
|
||||
"try again",
|
||||
fabro_types::FailureCategory::TransientInfra,
|
||||
)),
|
||||
will_retry: true,
|
||||
duration_ms: 444,
|
||||
billing: Some(usage.clone()),
|
||||
will_retry: true,
|
||||
timing: fabro_types::StageTiming::wall_only(444),
|
||||
billing: Some(usage.clone()),
|
||||
}),
|
||||
scoped_stage_id.clone(),
|
||||
))
|
||||
|
|
@ -1657,7 +1652,7 @@ mod tests {
|
|||
"current-visit fallback must not override stored stage_id"
|
||||
);
|
||||
let stage = state.stage(&scoped_stage_id).unwrap();
|
||||
assert_eq!(stage.duration_ms, Some(444));
|
||||
assert_eq!(stage.timing.map(|t| t.wall_time_ms), Some(444));
|
||||
assert_eq!(stage.usage, usage_counts(&usage));
|
||||
assert_eq!(stage.model.as_ref(), Some(usage.model()));
|
||||
let completion = stage.completion.as_ref().unwrap();
|
||||
|
|
@ -2145,7 +2140,7 @@ mod tests {
|
|||
reason: FailureReason::WorkflowError,
|
||||
detail: FailureDetail::new("boom", FailureCategory::Deterministic),
|
||||
},
|
||||
duration_ms: 42,
|
||||
timing: fabro_types::RunTiming::wall_only(42),
|
||||
final_git_commit_sha: Some("abc123".to_string()),
|
||||
final_patch: Some(patch.to_string()),
|
||||
diff_summary: None,
|
||||
|
|
@ -2222,7 +2217,7 @@ mod tests {
|
|||
5,
|
||||
"run.completed",
|
||||
&json!({
|
||||
"duration_ms": 42,
|
||||
"timing": {"wall_time_ms": 42, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0},
|
||||
"artifact_count": 0,
|
||||
"status": "succeeded",
|
||||
"reason": "completed",
|
||||
|
|
@ -2263,7 +2258,7 @@ mod tests {
|
|||
"category": "deterministic"
|
||||
}
|
||||
},
|
||||
"duration_ms": 42,
|
||||
"timing": {"wall_time_ms": 42, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0},
|
||||
"diff_summary": {
|
||||
"files_changed": 5,
|
||||
"additions": 20,
|
||||
|
|
@ -2304,7 +2299,7 @@ mod tests {
|
|||
detail
|
||||
},
|
||||
},
|
||||
duration_ms: 42,
|
||||
timing: fabro_types::RunTiming::wall_only(42),
|
||||
final_git_commit_sha: None,
|
||||
final_patch: None,
|
||||
diff_summary: None,
|
||||
|
|
@ -2345,7 +2340,7 @@ mod tests {
|
|||
1,
|
||||
EventBody::RunFailed(RunFailedProps {
|
||||
failure: failure.clone(),
|
||||
duration_ms: 42,
|
||||
timing: fabro_types::RunTiming::wall_only(42),
|
||||
final_git_commit_sha: Some("abc123".to_string()),
|
||||
final_patch: None,
|
||||
diff_summary: None,
|
||||
|
|
@ -2372,7 +2367,7 @@ mod tests {
|
|||
.apply_event(&test_event(
|
||||
1,
|
||||
EventBody::RunCompleted(RunCompletedProps {
|
||||
duration_ms: 10,
|
||||
timing: fabro_types::RunTiming::wall_only(10),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
@ -2538,7 +2533,7 @@ mod tests {
|
|||
.apply_event(&test_event(
|
||||
1,
|
||||
EventBody::RunCompleted(RunCompletedProps {
|
||||
duration_ms: 10,
|
||||
timing: fabro_types::RunTiming::wall_only(10),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::PartialSuccess,
|
||||
|
|
@ -2678,7 +2673,7 @@ mod tests {
|
|||
.apply_event(&test_event(
|
||||
1,
|
||||
EventBody::RunCompleted(RunCompletedProps {
|
||||
duration_ms: 10,
|
||||
timing: fabro_types::RunTiming::wall_only(10),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
@ -2721,7 +2716,7 @@ mod tests {
|
|||
index: 0,
|
||||
failure: Some(FailureDetail::new("boom", FailureCategory::TransientInfra)),
|
||||
will_retry,
|
||||
duration_ms,
|
||||
timing: fabro_types::StageTiming::wall_only(duration_ms),
|
||||
billing: None,
|
||||
}
|
||||
}
|
||||
|
|
@ -2738,7 +2733,7 @@ mod tests {
|
|||
fn completed_props(duration_ms: u64, status: StageOutcome) -> StageCompletedProps {
|
||||
StageCompletedProps {
|
||||
index: 0,
|
||||
duration_ms,
|
||||
timing: fabro_types::StageTiming::wall_only(duration_ms),
|
||||
status,
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
|
|
@ -3015,7 +3010,7 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
let stage = state.stage(&stage_id).unwrap();
|
||||
assert_eq!(stage.duration_ms, Some(42));
|
||||
assert_eq!(stage.timing.map(|t| t.wall_time_ms), Some(42));
|
||||
assert_eq!(stage.usage, usage_counts(&usage));
|
||||
assert_eq!(stage.model.as_ref(), Some(usage.model()));
|
||||
assert_eq!(stage.state, StageState::Succeeded);
|
||||
|
|
@ -3043,7 +3038,7 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
let stage = state.stage(&stage_id).unwrap();
|
||||
assert_eq!(stage.duration_ms, Some(10));
|
||||
assert_eq!(stage.timing.map(|t| t.wall_time_ms), Some(10));
|
||||
assert_eq!(stage.state, StageState::Failed);
|
||||
}
|
||||
|
||||
|
|
@ -3116,6 +3111,6 @@ mod tests {
|
|||
assert_eq!(stage.state, StageState::Running);
|
||||
// Prior attempt's terminal data must not leak into the new attempt.
|
||||
assert!(stage.completion.is_none());
|
||||
assert_eq!(stage.duration_ms, None);
|
||||
assert_eq!(stage.timing.map(|t| t.wall_time_ms), None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -619,7 +619,7 @@ mod tests {
|
|||
"2026-03-27T12:00:03Z",
|
||||
"run.completed",
|
||||
&serde_json::json!({
|
||||
"duration_ms": 3210,
|
||||
"timing": {"wall_time_ms": 3210, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0},
|
||||
"artifact_count": 1,
|
||||
"status": "succeeded",
|
||||
"reason": "completed",
|
||||
|
|
@ -993,7 +993,7 @@ mod tests {
|
|||
"category": "canceled"
|
||||
}
|
||||
},
|
||||
"duration_ms": 1,
|
||||
"timing": {"wall_time_ms": 1, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0},
|
||||
}),
|
||||
))
|
||||
.await
|
||||
|
|
@ -1038,7 +1038,7 @@ mod tests {
|
|||
"2026-03-27T12:00:04Z",
|
||||
"run.completed",
|
||||
&serde_json::json!({
|
||||
"duration_ms": 3210,
|
||||
"timing": {"wall_time_ms": 3210, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0},
|
||||
"artifact_count": 1,
|
||||
"status": "succeeded",
|
||||
"reason": "completed",
|
||||
|
|
@ -1397,7 +1397,7 @@ mod tests {
|
|||
"category": "deterministic"
|
||||
}
|
||||
},
|
||||
"duration_ms": 1,
|
||||
"timing": {"wall_time_ms": 1, "inference_time_ms": 0, "tool_time_ms": 0, "active_time_ms": 0},
|
||||
}),
|
||||
))
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ fn serializable_projection_round_trips_and_trims_bulky_node_fields() {
|
|||
stage.script_invocation = Some(json!({ "command": "cargo test" }));
|
||||
stage.script_timing = Some(json!({ "duration_ms": 10 }));
|
||||
stage.parallel_results = Some(json!([{ "stage": "fanout@1" }]));
|
||||
stage.duration_ms = Some(1234);
|
||||
stage.timing = Some(fabro_types::StageTiming::wall_only(1234));
|
||||
let usage = sample_usage();
|
||||
let usage_counts = BilledTokenCounts::from_billed_usage(std::slice::from_ref(&usage));
|
||||
stage.usage = usage_counts.clone();
|
||||
|
|
@ -186,7 +186,7 @@ fn serializable_projection_round_trips_and_trims_bulky_node_fields() {
|
|||
node.parallel_results,
|
||||
Some(json!([{ "stage": "fanout@1" }]))
|
||||
);
|
||||
assert_eq!(node.duration_ms, Some(1234));
|
||||
assert_eq!(node.timing.map(|t| t.wall_time_ms), Some(1234));
|
||||
assert_eq!(node.usage, usage_counts);
|
||||
assert_eq!(node.model.as_ref(), Some(usage.model()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -294,9 +294,8 @@ mod tests {
|
|||
started_at: None,
|
||||
last_event_at: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
elapsed_secs: None,
|
||||
},
|
||||
timing: None,
|
||||
billing: None,
|
||||
diff: None,
|
||||
pull_request: None,
|
||||
|
|
|
|||
|
|
@ -510,9 +510,8 @@ mod tests {
|
|||
started_at: None,
|
||||
last_event_at: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
elapsed_secs: None,
|
||||
},
|
||||
timing: None,
|
||||
billing: None,
|
||||
diff: None,
|
||||
pull_request: None,
|
||||
|
|
|
|||
|
|
@ -463,9 +463,8 @@ mod tests {
|
|||
started_at: None,
|
||||
last_event_at: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
elapsed_secs: None,
|
||||
},
|
||||
timing: None,
|
||||
billing: None,
|
||||
diff: None,
|
||||
pull_request: None,
|
||||
|
|
|
|||
|
|
@ -2,13 +2,15 @@ use chrono::{DateTime, Utc};
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::outcome::StageOutcome;
|
||||
use crate::{BilledTokenCounts, RunDiff, RunFailure};
|
||||
use crate::{BilledTokenCounts, RunDiff, RunFailure, RunTiming, StageTiming};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StageSummary {
|
||||
pub stage_id: String,
|
||||
pub stage_label: String,
|
||||
pub duration_ms: u64,
|
||||
/// Per-node timing summed across every visit of the node within this
|
||||
/// conclusion. `wall_time_ms` is the sum of visit wall times.
|
||||
pub timing: StageTiming,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub billing_usd_micros: Option<i64>,
|
||||
pub retries: u32,
|
||||
|
|
@ -18,7 +20,9 @@ pub struct StageSummary {
|
|||
pub struct Conclusion {
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub status: StageOutcome,
|
||||
pub duration_ms: u64,
|
||||
/// Run-level timing. `wall_time_ms` is the run's clock duration; active
|
||||
/// fields sum work across stage visits and can exceed wall time.
|
||||
pub timing: RunTiming,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub failure: Option<RunFailure>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ mod tests {
|
|||
use super::EventEnvelope;
|
||||
use crate::run_event::RunCompletedProps;
|
||||
use crate::{
|
||||
EventBody, ParallelBranchId, Principal, RunEvent, StageId, SuccessReason, fixtures,
|
||||
EventBody, ParallelBranchId, Principal, RunEvent, RunTiming, StageId, SuccessReason,
|
||||
fixtures,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
|
@ -35,7 +36,7 @@ mod tests {
|
|||
tool_call_id: None,
|
||||
actor: None,
|
||||
body: EventBody::RunCompleted(RunCompletedProps {
|
||||
duration_ms: 42,
|
||||
timing: RunTiming::wall_only(42),
|
||||
artifact_count: 0,
|
||||
status: "success".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
@ -79,7 +80,7 @@ mod tests {
|
|||
model: Some("claude-sonnet".to_string()),
|
||||
}),
|
||||
body: EventBody::RunCompleted(RunCompletedProps {
|
||||
duration_ms: 100,
|
||||
timing: RunTiming::wall_only(100),
|
||||
artifact_count: 1,
|
||||
status: "success".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ pub mod stage_id;
|
|||
pub mod start;
|
||||
pub mod status;
|
||||
pub mod steering;
|
||||
pub mod timing;
|
||||
|
||||
pub use artifact::ArtifactUpload;
|
||||
pub use auth::{IdpIdentity, IdpIdentityError};
|
||||
|
|
@ -131,3 +132,4 @@ pub use status::{
|
|||
TerminalStatus,
|
||||
};
|
||||
pub use steering::SteeringMessage;
|
||||
pub use timing::{RunTiming, StageTiming};
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
|||
use serde_json::Value;
|
||||
use strum::{Display, EnumString, IntoStaticStr};
|
||||
|
||||
use crate::{ExecOutputTail, FailureSignature, SystemActorKind};
|
||||
use crate::{ExecOutputTail, FailureSignature, StageTiming, SystemActorKind};
|
||||
|
||||
pub trait OutcomeMeta:
|
||||
Default + Clone + Send + Sync + fmt::Debug + Serialize + DeserializeOwned + 'static
|
||||
|
|
@ -274,8 +274,13 @@ pub struct Outcome<M: OutcomeMeta = ()> {
|
|||
pub usage: M,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub files_touched: Vec<String>,
|
||||
/// Stage timing breakdown captured by the workflow engine.
|
||||
///
|
||||
/// `None` until the stage produces a terminal outcome with a measured
|
||||
/// wall time. Stage handlers that perform no inference or tool work
|
||||
/// populate this with [`StageTiming::wall_only`].
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub duration_ms: Option<u64>,
|
||||
pub timing: Option<StageTiming>,
|
||||
}
|
||||
|
||||
impl<M: OutcomeMeta> Default for Outcome<M> {
|
||||
|
|
@ -290,7 +295,7 @@ impl<M: OutcomeMeta> Default for Outcome<M> {
|
|||
failure: None,
|
||||
usage: M::default(),
|
||||
files_touched: Vec::new(),
|
||||
duration_ms: None,
|
||||
timing: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -402,17 +407,35 @@ mod tests {
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NodeResult<M: OutcomeMeta = ()> {
|
||||
pub outcome: Outcome<M>,
|
||||
pub duration: Duration,
|
||||
pub attempts: u32,
|
||||
pub max_attempts: u32,
|
||||
pub outcome: Outcome<M>,
|
||||
/// Wall-clock time spent executing this node attempt (including handler
|
||||
/// internal waits). Independent of the `inference_time` / `tool_time`
|
||||
/// breakdown — those are work-only measurements.
|
||||
pub wall_time: Duration,
|
||||
/// Sum of LLM request/stream elapsed time across this node attempt. Zero
|
||||
/// for non-LLM handlers.
|
||||
pub inference_time: Duration,
|
||||
/// Sum of tool/command execution elapsed time across this node attempt.
|
||||
/// Zero for handlers that do not invoke tools or commands.
|
||||
pub tool_time: Duration,
|
||||
pub attempts: u32,
|
||||
pub max_attempts: u32,
|
||||
}
|
||||
|
||||
impl<M: OutcomeMeta> NodeResult<M> {
|
||||
pub fn new(outcome: Outcome<M>, duration: Duration, attempts: u32, max_attempts: u32) -> Self {
|
||||
pub fn new(
|
||||
outcome: Outcome<M>,
|
||||
wall_time: Duration,
|
||||
inference_time: Duration,
|
||||
tool_time: Duration,
|
||||
attempts: u32,
|
||||
max_attempts: u32,
|
||||
) -> Self {
|
||||
Self {
|
||||
outcome,
|
||||
duration,
|
||||
wall_time,
|
||||
inference_time,
|
||||
tool_time,
|
||||
attempts,
|
||||
max_attempts,
|
||||
}
|
||||
|
|
@ -421,7 +444,9 @@ impl<M: OutcomeMeta> NodeResult<M> {
|
|||
pub fn from_skip(outcome: Outcome<M>) -> Self {
|
||||
Self {
|
||||
outcome,
|
||||
duration: Duration::ZERO,
|
||||
wall_time: Duration::ZERO,
|
||||
inference_time: Duration::ZERO,
|
||||
tool_time: Duration::ZERO,
|
||||
attempts: 0,
|
||||
max_attempts: 0,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -940,7 +940,7 @@ mod tests {
|
|||
actor: None,
|
||||
body: EventBody::StageCompleted(StageCompletedProps {
|
||||
index: 1,
|
||||
duration_ms: 1234,
|
||||
timing: crate::StageTiming::wall_only(1234),
|
||||
status: crate::StageOutcome::Succeeded,
|
||||
preferred_label: None,
|
||||
suggested_next_ids: vec!["next".to_string()],
|
||||
|
|
@ -1135,7 +1135,12 @@ mod tests {
|
|||
(
|
||||
"run.completed",
|
||||
json!({
|
||||
"duration_ms": 42,
|
||||
"timing": {
|
||||
"wall_time_ms": 42,
|
||||
"inference_time_ms": 0,
|
||||
"tool_time_ms": 0,
|
||||
"active_time_ms": 0
|
||||
},
|
||||
"artifact_count": 0,
|
||||
"status": "succeeded",
|
||||
"reason": "completed",
|
||||
|
|
@ -1156,7 +1161,12 @@ mod tests {
|
|||
"category": "deterministic"
|
||||
}
|
||||
},
|
||||
"duration_ms": 42,
|
||||
"timing": {
|
||||
"wall_time_ms": 42,
|
||||
"inference_time_ms": 0,
|
||||
"tool_time_ms": 0,
|
||||
"active_time_ms": 0
|
||||
},
|
||||
"diff_summary": {
|
||||
"files_changed": 2,
|
||||
"additions": 10,
|
||||
|
|
@ -1214,7 +1224,7 @@ mod tests {
|
|||
fn event_body_event_name_matches_wire_name() {
|
||||
let body = EventBody::StageCompleted(StageCompletedProps {
|
||||
index: 1,
|
||||
duration_ms: 1234,
|
||||
timing: crate::StageTiming::wall_only(1234),
|
||||
status: crate::StageOutcome::Succeeded,
|
||||
preferred_label: None,
|
||||
suggested_next_ids: vec!["next".to_string()],
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use super::{BilledTokenCounts, ExecOutputTail, RunNoticeLevel};
|
|||
use crate::status::{BlockedReason, SuccessReason};
|
||||
use crate::{
|
||||
DiffSummary, ForkSourceRef, GitContext, Graph, PairId, PairTarget, RunBlobId, RunControlAction,
|
||||
RunFailure, RunId, RunProvenance, WorkflowSettings,
|
||||
RunFailure, RunId, RunProvenance, RunTiming, WorkflowSettings,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -183,7 +183,8 @@ pub struct RunUnarchivedProps {}
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunCompletedProps {
|
||||
pub duration_ms: u64,
|
||||
/// Run wall-clock time, with active timing breakdown for the run rollup.
|
||||
pub timing: RunTiming,
|
||||
pub artifact_count: usize,
|
||||
pub status: String,
|
||||
pub reason: SuccessReason,
|
||||
|
|
@ -202,7 +203,8 @@ pub struct RunCompletedProps {
|
|||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunFailedProps {
|
||||
pub failure: RunFailure,
|
||||
pub duration_ms: u64,
|
||||
/// Run wall-clock time at failure, with active timing breakdown.
|
||||
pub timing: RunTiming,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub final_git_commit_sha: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
|||
use serde_json::Value;
|
||||
|
||||
use super::ExecOutputTail;
|
||||
use crate::{BilledModelUsage, DiffSummary, FailureDetail, Outcome, StageOutcome};
|
||||
use crate::{BilledModelUsage, DiffSummary, FailureDetail, Outcome, StageOutcome, StageTiming};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct StageStartedProps {
|
||||
|
|
@ -17,7 +17,8 @@ pub struct StageStartedProps {
|
|||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct StageCompletedProps {
|
||||
pub index: usize,
|
||||
pub duration_ms: u64,
|
||||
/// Per-attempt timing breakdown for this stage visit.
|
||||
pub timing: StageTiming,
|
||||
pub status: StageOutcome,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub preferred_label: Option<String>,
|
||||
|
|
@ -51,14 +52,15 @@ pub struct StageCompletedProps {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct StageFailedProps {
|
||||
pub index: usize,
|
||||
pub index: usize,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub failure: Option<FailureDetail>,
|
||||
pub will_retry: bool,
|
||||
pub failure: Option<FailureDetail>,
|
||||
pub will_retry: bool,
|
||||
/// Per-attempt timing breakdown for this stage visit.
|
||||
#[serde(default)]
|
||||
pub duration_ms: u64,
|
||||
pub timing: StageTiming,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub billing: Option<BilledModelUsage>,
|
||||
pub billing: Option<BilledModelUsage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use chrono::{DateTime, Utc};
|
|||
use crate::{
|
||||
BilledTokenCounts, Checkpoint, Conclusion, InterviewQuestionRecord, InvalidTransition,
|
||||
ModelRef, PullRequestLink, RunControlAction, RunDiff, RunId, RunSandbox, RunSpec, RunStatus,
|
||||
StageCompletion, StageHandler, StageId, StageState, StartRecord,
|
||||
StageCompletion, StageHandler, StageId, StageState, StageTiming, StartRecord,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
|
|
@ -71,8 +71,13 @@ pub struct StageProjection {
|
|||
pub started_at: Option<DateTime<Utc>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub handler: Option<StageHandler>,
|
||||
/// Per-attempt timing breakdown for the latest terminal attempt.
|
||||
///
|
||||
/// `None` for stages still in flight (`started_at` is set but no terminal
|
||||
/// event has been observed yet). For live wall-time ticking, the UI uses
|
||||
/// `started_at`; once terminal this carries the finalized breakdown.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub duration_ms: Option<u64>,
|
||||
pub timing: Option<StageTiming>,
|
||||
#[serde(default)]
|
||||
pub usage: BilledTokenCounts,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
|
@ -95,7 +100,7 @@ impl StageProjection {
|
|||
prompt: None,
|
||||
response: None,
|
||||
completion: None,
|
||||
duration_ms: None,
|
||||
timing: None,
|
||||
usage: BilledTokenCounts::default(),
|
||||
model: None,
|
||||
provider_used: None,
|
||||
|
|
@ -119,26 +124,27 @@ impl StageProjection {
|
|||
self.state
|
||||
}
|
||||
|
||||
/// Live wall-clock runtime in seconds.
|
||||
/// Live wall-clock time in milliseconds.
|
||||
///
|
||||
/// While the stage is non-terminal (`Pending`, `Running`, or `Retrying`),
|
||||
/// this returns the elapsed time since `started_at` so the UI can tick
|
||||
/// client-side. Once terminal, the stored `duration_ms` is returned. This
|
||||
/// also handles retries safely: a new `StageStarted` resets the state
|
||||
/// back to `Running` and keeps the live computation correct even if a
|
||||
/// previous attempt left a stale `duration_ms`.
|
||||
/// client-side. Once terminal, the stored `timing.wall_time_ms` is
|
||||
/// returned. This also handles retries safely: a new `StageStarted` resets
|
||||
/// the state back to `Running` and keeps the live computation correct
|
||||
/// even if a previous attempt left stale timing.
|
||||
#[must_use]
|
||||
pub fn runtime_secs(&self, now: DateTime<Utc>) -> Option<f64> {
|
||||
pub fn live_wall_time_ms(&self, now: DateTime<Utc>) -> Option<u64> {
|
||||
let state = self.effective_state();
|
||||
if matches!(
|
||||
state,
|
||||
StageState::Running | StageState::Retrying | StageState::Pending
|
||||
) {
|
||||
return self.started_at.map(|started| {
|
||||
now.signed_duration_since(started).num_milliseconds().max(0) as f64 / 1000.0
|
||||
u64::try_from(now.signed_duration_since(started).num_milliseconds().max(0))
|
||||
.unwrap_or(0)
|
||||
});
|
||||
}
|
||||
self.duration_ms.map(|ms| ms as f64 / 1000.0)
|
||||
self.timing.map(|timing| timing.wall_time_ms)
|
||||
}
|
||||
|
||||
/// Begin a new attempt (or visit) for this stage: clear every
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
use crate::{
|
||||
DiffSummary, InterviewQuestionRecord, Principal, PullRequestLink, RepositoryRef,
|
||||
RunControlAction, RunId, RunSandbox, RunStatus,
|
||||
RunControlAction, RunId, RunSandbox, RunStatus, RunTiming,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -33,6 +33,10 @@ pub struct Run {
|
|||
#[serde(default)]
|
||||
pub source_directory: Option<String>,
|
||||
pub timestamps: RunTimestamps,
|
||||
/// Run-level timing rollup. `None` until the run has measurable timing
|
||||
/// data; populated once a terminal event or partial rollup is available.
|
||||
#[serde(default)]
|
||||
pub timing: Option<RunTiming>,
|
||||
#[serde(default)]
|
||||
pub billing: Option<RunBillingSummary>,
|
||||
#[serde(default)]
|
||||
|
|
@ -123,10 +127,6 @@ pub struct RunTimestamps {
|
|||
pub last_event_at: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
pub duration_ms: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub elapsed_secs: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
|
|||
252
lib/crates/fabro-types/src/timing.rs
Normal file
252
lib/crates/fabro-types/src/timing.rs
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
//! Wall and active timing primitives shared across stages and runs.
|
||||
//!
|
||||
//! Two value objects: [`StageTiming`] for one stage visit, [`RunTiming`] for a
|
||||
//! run-level rollup. Both expose the same four fields:
|
||||
//!
|
||||
//! - `wall_time_ms`: elapsed clock time from start to finish.
|
||||
//! - `inference_time_ms`: Fabro-observed LLM request/stream elapsed time.
|
||||
//! - `tool_time_ms`: tool or command execution elapsed time.
|
||||
//! - `active_time_ms`: `inference_time_ms + tool_time_ms`.
|
||||
//!
|
||||
//! `active_time_ms` is precomputed and serialized so API consumers do not need
|
||||
//! to redo the addition. Use the `new` constructors to enforce the invariant.
|
||||
//!
|
||||
//! For parallel container stages the container reports `active = 0` and the
|
||||
//! child branches carry their own work timing; run-level active time sums work
|
||||
//! across stage visits and can exceed run wall time when work runs in parallel.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Timing breakdown for one stage visit.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct StageTiming {
|
||||
/// Wall-clock time from stage start to terminal event.
|
||||
pub wall_time_ms: u64,
|
||||
/// Fabro-observed LLM request/stream elapsed time.
|
||||
#[serde(default)]
|
||||
pub inference_time_ms: u64,
|
||||
/// Tool or command execution elapsed time.
|
||||
#[serde(default)]
|
||||
pub tool_time_ms: u64,
|
||||
/// `inference_time_ms + tool_time_ms`.
|
||||
pub active_time_ms: u64,
|
||||
}
|
||||
|
||||
impl StageTiming {
|
||||
/// Construct a [`StageTiming`] with `active_time_ms` derived from the
|
||||
/// breakdown.
|
||||
#[must_use]
|
||||
pub fn new(wall_time_ms: u64, inference_time_ms: u64, tool_time_ms: u64) -> Self {
|
||||
let active_time_ms = inference_time_ms.saturating_add(tool_time_ms);
|
||||
Self {
|
||||
wall_time_ms,
|
||||
inference_time_ms,
|
||||
tool_time_ms,
|
||||
active_time_ms,
|
||||
}
|
||||
}
|
||||
|
||||
/// Stages with no inference/tool work (human, wait, conditional, fan-in,
|
||||
/// start, exit, parallel container) report wall time only.
|
||||
#[must_use]
|
||||
pub fn wall_only(wall_time_ms: u64) -> Self {
|
||||
Self::new(wall_time_ms, 0, 0)
|
||||
}
|
||||
|
||||
/// Sum two timings field-by-field. Used to aggregate visits of one node
|
||||
/// and to accumulate run-level rollups.
|
||||
#[must_use]
|
||||
pub fn saturating_add(&self, other: &Self) -> Self {
|
||||
Self::new(
|
||||
self.wall_time_ms.saturating_add(other.wall_time_ms),
|
||||
self.inference_time_ms
|
||||
.saturating_add(other.inference_time_ms),
|
||||
self.tool_time_ms.saturating_add(other.tool_time_ms),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Timing rollup for an entire run.
|
||||
///
|
||||
/// `wall_time_ms` is the run's clock duration from start to terminal event.
|
||||
/// The other three fields sum work across stage visits, so `active_time_ms`
|
||||
/// can exceed `wall_time_ms` when parallel branches run concurrently.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RunTiming {
|
||||
/// Wall-clock time from run start to terminal event.
|
||||
pub wall_time_ms: u64,
|
||||
/// Sum of inference time across every stage visit.
|
||||
#[serde(default)]
|
||||
pub inference_time_ms: u64,
|
||||
/// Sum of tool time across every stage visit.
|
||||
#[serde(default)]
|
||||
pub tool_time_ms: u64,
|
||||
/// `inference_time_ms + tool_time_ms`.
|
||||
pub active_time_ms: u64,
|
||||
}
|
||||
|
||||
impl RunTiming {
|
||||
/// Construct a [`RunTiming`] with `active_time_ms` derived from the
|
||||
/// breakdown.
|
||||
#[must_use]
|
||||
pub fn new(wall_time_ms: u64, inference_time_ms: u64, tool_time_ms: u64) -> Self {
|
||||
let active_time_ms = inference_time_ms.saturating_add(tool_time_ms);
|
||||
Self {
|
||||
wall_time_ms,
|
||||
inference_time_ms,
|
||||
tool_time_ms,
|
||||
active_time_ms,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run-level rollups for runs with no inference/tool work yet recorded.
|
||||
#[must_use]
|
||||
pub fn wall_only(wall_time_ms: u64) -> Self {
|
||||
Self::new(wall_time_ms, 0, 0)
|
||||
}
|
||||
|
||||
/// Sum two run timings field-by-field. Used to accumulate aggregate
|
||||
/// billing totals across completed runs.
|
||||
#[must_use]
|
||||
pub fn saturating_add(&self, other: &Self) -> Self {
|
||||
Self::new(
|
||||
self.wall_time_ms.saturating_add(other.wall_time_ms),
|
||||
self.inference_time_ms
|
||||
.saturating_add(other.inference_time_ms),
|
||||
self.tool_time_ms.saturating_add(other.tool_time_ms),
|
||||
)
|
||||
}
|
||||
|
||||
/// Return a copy with `wall_time_ms` replaced. Useful at finalize time
|
||||
/// when the active breakdown is summed across stage visits but the run
|
||||
/// wall time is the executor's clock duration.
|
||||
#[must_use]
|
||||
pub fn with_wall_time(self, wall_time_ms: u64) -> Self {
|
||||
Self {
|
||||
wall_time_ms,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StageTiming> for RunTiming {
|
||||
fn from(stage: StageTiming) -> Self {
|
||||
Self {
|
||||
wall_time_ms: stage.wall_time_ms,
|
||||
inference_time_ms: stage.inference_time_ms,
|
||||
tool_time_ms: stage.tool_time_ms,
|
||||
active_time_ms: stage.active_time_ms,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{RunTiming, StageTiming};
|
||||
|
||||
#[test]
|
||||
fn stage_timing_new_derives_active_as_sum_of_inference_and_tool() {
|
||||
let timing = StageTiming::new(1000, 200, 300);
|
||||
assert_eq!(timing.wall_time_ms, 1000);
|
||||
assert_eq!(timing.inference_time_ms, 200);
|
||||
assert_eq!(timing.tool_time_ms, 300);
|
||||
assert_eq!(timing.active_time_ms, 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stage_timing_wall_only_zeroes_breakdown_and_active() {
|
||||
let timing = StageTiming::wall_only(750);
|
||||
assert_eq!(timing.wall_time_ms, 750);
|
||||
assert_eq!(timing.inference_time_ms, 0);
|
||||
assert_eq!(timing.tool_time_ms, 0);
|
||||
assert_eq!(timing.active_time_ms, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stage_timing_saturating_add_sums_all_breakdown_fields() {
|
||||
let a = StageTiming::new(100, 30, 70);
|
||||
let b = StageTiming::new(200, 50, 25);
|
||||
let sum = a.saturating_add(&b);
|
||||
assert_eq!(sum.wall_time_ms, 300);
|
||||
assert_eq!(sum.inference_time_ms, 80);
|
||||
assert_eq!(sum.tool_time_ms, 95);
|
||||
assert_eq!(sum.active_time_ms, 175);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_timing_wall_only_zeroes_breakdown_and_active() {
|
||||
let timing = RunTiming::wall_only(1500);
|
||||
assert_eq!(timing.wall_time_ms, 1500);
|
||||
assert_eq!(timing.inference_time_ms, 0);
|
||||
assert_eq!(timing.tool_time_ms, 0);
|
||||
assert_eq!(timing.active_time_ms, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_timing_saturating_add_sums_all_breakdown_fields() {
|
||||
let a = RunTiming::new(1000, 100, 50);
|
||||
let b = RunTiming::new(500, 200, 150);
|
||||
let sum = a.saturating_add(&b);
|
||||
assert_eq!(sum.wall_time_ms, 1500);
|
||||
assert_eq!(sum.inference_time_ms, 300);
|
||||
assert_eq!(sum.tool_time_ms, 200);
|
||||
assert_eq!(sum.active_time_ms, 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_timing_from_stage_timing_copies_all_fields() {
|
||||
let stage = StageTiming::new(900, 200, 300);
|
||||
let run: RunTiming = stage.into();
|
||||
assert_eq!(run.wall_time_ms, 900);
|
||||
assert_eq!(run.inference_time_ms, 200);
|
||||
assert_eq!(run.tool_time_ms, 300);
|
||||
assert_eq!(run.active_time_ms, 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_timing_with_wall_time_replaces_wall_and_preserves_breakdown() {
|
||||
let original = RunTiming::new(1000, 200, 300);
|
||||
let updated = original.with_wall_time(5000);
|
||||
assert_eq!(updated.wall_time_ms, 5000);
|
||||
assert_eq!(updated.inference_time_ms, 200);
|
||||
assert_eq!(updated.tool_time_ms, 300);
|
||||
assert_eq!(updated.active_time_ms, 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stage_timing_round_trips_json_with_serialized_active_time() {
|
||||
let original = StageTiming::new(900, 250, 350);
|
||||
let json = serde_json::to_value(original).unwrap();
|
||||
assert_eq!(json["wall_time_ms"], 900);
|
||||
assert_eq!(json["inference_time_ms"], 250);
|
||||
assert_eq!(json["tool_time_ms"], 350);
|
||||
assert_eq!(json["active_time_ms"], 600);
|
||||
let parsed: StageTiming = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(parsed, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_timing_round_trips_json_with_serialized_active_time() {
|
||||
let original = RunTiming::new(2000, 600, 400);
|
||||
let json = serde_json::to_value(original).unwrap();
|
||||
assert_eq!(json["wall_time_ms"], 2000);
|
||||
assert_eq!(json["inference_time_ms"], 600);
|
||||
assert_eq!(json["tool_time_ms"], 400);
|
||||
assert_eq!(json["active_time_ms"], 1000);
|
||||
let parsed: RunTiming = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(parsed, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stage_timing_breakdown_fields_default_when_missing_from_json() {
|
||||
let json = serde_json::json!({
|
||||
"wall_time_ms": 500,
|
||||
"active_time_ms": 0
|
||||
});
|
||||
let parsed: StageTiming = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(parsed.wall_time_ms, 500);
|
||||
assert_eq!(parsed.inference_time_ms, 0);
|
||||
assert_eq!(parsed.tool_time_ms, 0);
|
||||
assert_eq!(parsed.active_time_ms, 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
use fabro_types::run_event::run::RunFailedProps;
|
||||
use fabro_types::{
|
||||
Conclusion, EventBody, ExecOutputTail, FailureCategory, FailureDetail, FailureReason,
|
||||
FailureSignature, RunDiff, RunFailure, StageOutcome, SystemActorKind,
|
||||
FailureSignature, RunDiff, RunFailure, RunTiming, StageOutcome, SystemActorKind,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ fn run_failed_serializes_nested_failure_contract() {
|
|||
detail
|
||||
},
|
||||
},
|
||||
duration_ms: 42,
|
||||
timing: RunTiming::wall_only(42),
|
||||
final_git_commit_sha: Some("abc123".to_string()),
|
||||
final_patch: Some("diff --git a/file b/file".to_string()),
|
||||
diff_summary: None,
|
||||
|
|
@ -63,7 +63,12 @@ fn run_failed_serializes_nested_failure_contract() {
|
|||
}
|
||||
}
|
||||
},
|
||||
"duration_ms": 42,
|
||||
"timing": {
|
||||
"wall_time_ms": 42,
|
||||
"inference_time_ms": 0,
|
||||
"tool_time_ms": 0,
|
||||
"active_time_ms": 0
|
||||
},
|
||||
"final_git_commit_sha": "abc123",
|
||||
"final_patch": "diff --git a/file b/file"
|
||||
})
|
||||
|
|
@ -81,7 +86,7 @@ fn run_failed_omits_empty_failure_optional_fields() {
|
|||
reason: FailureReason::WorkflowError,
|
||||
detail: FailureDetail::new("boom", FailureCategory::Deterministic),
|
||||
},
|
||||
duration_ms: 1,
|
||||
timing: RunTiming::wall_only(1),
|
||||
final_git_commit_sha: None,
|
||||
final_patch: None,
|
||||
diff_summary: None,
|
||||
|
|
@ -100,7 +105,12 @@ fn run_failed_omits_empty_failure_optional_fields() {
|
|||
"category": "deterministic"
|
||||
}
|
||||
},
|
||||
"duration_ms": 1
|
||||
"timing": {
|
||||
"wall_time_ms": 1,
|
||||
"inference_time_ms": 0,
|
||||
"tool_time_ms": 0,
|
||||
"active_time_ms": 0
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -114,7 +124,7 @@ fn conclusion_serializes_rich_failure() {
|
|||
status: StageOutcome::Failed {
|
||||
retry_requested: false,
|
||||
},
|
||||
duration_ms: 42,
|
||||
timing: RunTiming::wall_only(42),
|
||||
failure: Some(RunFailure {
|
||||
reason: FailureReason::WorkflowError,
|
||||
detail: {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ use std::borrow::Cow;
|
|||
use std::collections::HashMap;
|
||||
|
||||
use fabro_model::Catalog;
|
||||
use fabro_types::{BilledTokenCounts, ModelRef, RunProjection, StageProjection};
|
||||
use fabro_types::{
|
||||
BilledTokenCounts, ModelRef, RunProjection, RunTiming, StageProjection, StageTiming,
|
||||
};
|
||||
|
||||
fn stage_usage_with_cost<'a>(
|
||||
catalog: Option<&Catalog>,
|
||||
|
|
@ -28,10 +30,13 @@ fn stage_usage_with_cost<'a>(
|
|||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ProjectionBillingStage {
|
||||
pub node_id: String,
|
||||
pub billing: BilledTokenCounts,
|
||||
pub duration_ms: u64,
|
||||
pub model: Option<ModelRef>,
|
||||
pub node_id: String,
|
||||
pub billing: BilledTokenCounts,
|
||||
/// Per-node timing summed across every visit of that node within this
|
||||
/// projection. `wall_time_ms`, `inference_time_ms`, `tool_time_ms`, and
|
||||
/// `active_time_ms` are all summed in lockstep.
|
||||
pub timing: StageTiming,
|
||||
pub model: Option<ModelRef>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
@ -46,7 +51,9 @@ pub struct ProjectionBillingRollup {
|
|||
pub stages: Vec<ProjectionBillingStage>,
|
||||
pub totals: BilledTokenCounts,
|
||||
pub by_model: Vec<ProjectionBillingByModel>,
|
||||
pub runtime_ms: u64,
|
||||
/// Run-level timing summed across every stage visit. `wall_time_ms` is
|
||||
/// the sum of stage visit wall times (not the run clock duration).
|
||||
pub timing: RunTiming,
|
||||
pub billed_visit_count: usize,
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +73,7 @@ pub fn billing_rollup_from_projection(
|
|||
let mut stages = Vec::<ProjectionBillingStage>::new();
|
||||
let mut by_model = HashMap::<ModelRef, ProjectionBillingByModel>::new();
|
||||
let mut totals = BilledTokenCounts::default();
|
||||
let mut runtime_ms = 0_u64;
|
||||
let mut run_timing = RunTiming::default();
|
||||
let mut billed_visit_count = 0_usize;
|
||||
|
||||
for (stage_id, stage) in projection.iter_stages() {
|
||||
|
|
@ -75,7 +82,7 @@ pub fn billing_rollup_from_projection(
|
|||
}
|
||||
let usage = stage_usage_with_cost(catalog, stage);
|
||||
let usage = usage.as_ref();
|
||||
if stage.completion.is_none() && stage.duration_ms.is_none() && usage.is_zero() {
|
||||
if stage.completion.is_none() && stage.timing.is_none() && usage.is_zero() {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -83,18 +90,18 @@ pub fn billing_rollup_from_projection(
|
|||
let index = *stage_indices.entry(node_id.to_string()).or_insert_with(|| {
|
||||
let index = stages.len();
|
||||
stages.push(ProjectionBillingStage {
|
||||
node_id: node_id.to_string(),
|
||||
billing: BilledTokenCounts::default(),
|
||||
duration_ms: 0,
|
||||
model: None,
|
||||
node_id: node_id.to_string(),
|
||||
billing: BilledTokenCounts::default(),
|
||||
timing: StageTiming::default(),
|
||||
model: None,
|
||||
});
|
||||
index
|
||||
});
|
||||
let row = &mut stages[index];
|
||||
|
||||
if let Some(duration_ms) = stage.duration_ms {
|
||||
row.duration_ms = row.duration_ms.saturating_add(duration_ms);
|
||||
runtime_ms = runtime_ms.saturating_add(duration_ms);
|
||||
if let Some(timing) = stage.timing {
|
||||
row.timing = row.timing.saturating_add(&timing);
|
||||
run_timing = run_timing.saturating_add(&RunTiming::from(timing));
|
||||
}
|
||||
|
||||
if !usage.is_zero() {
|
||||
|
|
@ -137,7 +144,7 @@ pub fn billing_rollup_from_projection(
|
|||
stages,
|
||||
totals,
|
||||
by_model,
|
||||
runtime_ms,
|
||||
timing: run_timing,
|
||||
billed_visit_count,
|
||||
}
|
||||
}
|
||||
|
|
@ -198,7 +205,7 @@ mod tests {
|
|||
let failed_usage = test_usage("gpt-old", 100, 10);
|
||||
let success_usage = test_usage("gpt-new", 200, 20);
|
||||
let first = projection.stage_entry("verify", 1, first_event_seq(1));
|
||||
first.duration_ms = Some(1200);
|
||||
first.timing = Some(fabro_types::StageTiming::wall_only(1200));
|
||||
first.usage = BilledTokenCounts::from_billed_usage(std::slice::from_ref(&failed_usage));
|
||||
first.model = Some(failed_usage.model().clone());
|
||||
first.completion = Some(StageCompletion {
|
||||
|
|
@ -210,7 +217,7 @@ mod tests {
|
|||
timestamp: chrono::Utc::now(),
|
||||
});
|
||||
let second = projection.stage_entry("verify", 2, first_event_seq(2));
|
||||
second.duration_ms = Some(800);
|
||||
second.timing = Some(fabro_types::StageTiming::wall_only(800));
|
||||
second.usage = BilledTokenCounts::from_billed_usage(std::slice::from_ref(&success_usage));
|
||||
second.model = Some(success_usage.model().clone());
|
||||
second.completion = Some(StageCompletion {
|
||||
|
|
@ -231,12 +238,12 @@ mod tests {
|
|||
.map(|model| model.model_id.as_str()),
|
||||
Some("gpt-new")
|
||||
);
|
||||
assert_eq!(rollup.stages[0].duration_ms, 2000);
|
||||
assert_eq!(rollup.stages[0].timing.wall_time_ms, 2000);
|
||||
assert_eq!(rollup.stages[0].billing.input_tokens, 300);
|
||||
assert_eq!(rollup.stages[0].billing.output_tokens, 30);
|
||||
assert_eq!(rollup.stages[0].billing.total_usd_micros, Some(330));
|
||||
|
||||
assert_eq!(rollup.runtime_ms, 2000);
|
||||
assert_eq!(rollup.timing.wall_time_ms, 2000);
|
||||
assert_eq!(rollup.totals.input_tokens, 300);
|
||||
assert_eq!(rollup.totals.output_tokens, 30);
|
||||
assert_eq!(rollup.totals.total_usd_micros, Some(330));
|
||||
|
|
@ -255,7 +262,7 @@ mod tests {
|
|||
fn rollup_includes_completed_non_llm_stage_rows_with_zero_billing() {
|
||||
let mut projection = test_projection();
|
||||
let stage = projection.stage_entry("build", 1, first_event_seq(1));
|
||||
stage.duration_ms = Some(25);
|
||||
stage.timing = Some(fabro_types::StageTiming::wall_only(25));
|
||||
stage.completion = Some(StageCompletion {
|
||||
outcome: StageOutcome::Succeeded,
|
||||
notes: None,
|
||||
|
|
@ -267,10 +274,10 @@ mod tests {
|
|||
|
||||
assert_eq!(rollup.stages.len(), 1);
|
||||
assert_eq!(rollup.stages[0].node_id, "build");
|
||||
assert_eq!(rollup.stages[0].duration_ms, 25);
|
||||
assert_eq!(rollup.stages[0].timing.wall_time_ms, 25);
|
||||
assert!(rollup.stages[0].model.is_none());
|
||||
assert_eq!(rollup.stages[0].billing.input_tokens, 0);
|
||||
assert_eq!(rollup.runtime_ms, 25);
|
||||
assert_eq!(rollup.timing.wall_time_ms, 25);
|
||||
assert!(rollup.by_model.is_empty());
|
||||
assert!(rollup.billing_if_present().is_none());
|
||||
}
|
||||
|
|
@ -280,7 +287,7 @@ mod tests {
|
|||
let mut projection = test_projection();
|
||||
projection.spec = run_spec_with_boundary_nodes();
|
||||
let start = projection.stage_entry("start", 1, first_event_seq(1));
|
||||
start.duration_ms = Some(25);
|
||||
start.timing = Some(fabro_types::StageTiming::wall_only(25));
|
||||
start.completion = Some(StageCompletion {
|
||||
outcome: StageOutcome::Succeeded,
|
||||
notes: None,
|
||||
|
|
@ -288,7 +295,7 @@ mod tests {
|
|||
timestamp: chrono::Utc::now(),
|
||||
});
|
||||
let exit = projection.stage_entry("exit", 1, first_event_seq(2));
|
||||
exit.duration_ms = Some(7);
|
||||
exit.timing = Some(fabro_types::StageTiming::wall_only(7));
|
||||
exit.completion = Some(StageCompletion {
|
||||
outcome: StageOutcome::Succeeded,
|
||||
notes: None,
|
||||
|
|
@ -299,7 +306,7 @@ mod tests {
|
|||
let rollup = billing_rollup_from_projection(&projection, None);
|
||||
|
||||
assert_eq!(rollup.stages.len(), 0);
|
||||
assert_eq!(rollup.runtime_ms, 0);
|
||||
assert_eq!(rollup.timing.wall_time_ms, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -2037,14 +2037,14 @@ mod tests {
|
|||
// 3. Outcome → StageFailed event
|
||||
let failure = outcome.failure.clone().unwrap();
|
||||
let event = Event::StageFailed {
|
||||
node_id: "code".into(),
|
||||
name: "code".into(),
|
||||
index: 0,
|
||||
failure: failure.clone(),
|
||||
will_retry: false,
|
||||
duration_ms: 0,
|
||||
billing: None,
|
||||
actor: None,
|
||||
node_id: "code".into(),
|
||||
name: "code".into(),
|
||||
index: 0,
|
||||
failure: failure.clone(),
|
||||
will_retry: false,
|
||||
timing: fabro_types::StageTiming::wall_only(0),
|
||||
billing: None,
|
||||
actor: None,
|
||||
};
|
||||
|
||||
// 4. Verify classification survived all the way through
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ fn event_body_from_event(event: &Event) -> EventBody {
|
|||
previous_parent_id: *previous_parent_id,
|
||||
}),
|
||||
Event::WorkflowRunCompleted {
|
||||
duration_ms,
|
||||
timing,
|
||||
artifact_count,
|
||||
status,
|
||||
reason,
|
||||
|
|
@ -191,7 +191,7 @@ fn event_body_from_event(event: &Event) -> EventBody {
|
|||
diff_summary,
|
||||
billing,
|
||||
} => EventBody::RunCompleted(fabro_types::RunCompletedProps {
|
||||
duration_ms: *duration_ms,
|
||||
timing: *timing,
|
||||
artifact_count: *artifact_count,
|
||||
status: status.clone(),
|
||||
reason: *reason,
|
||||
|
|
@ -203,14 +203,14 @@ fn event_body_from_event(event: &Event) -> EventBody {
|
|||
}),
|
||||
Event::WorkflowRunFailed {
|
||||
failure,
|
||||
duration_ms,
|
||||
timing,
|
||||
final_git_commit_sha,
|
||||
final_patch,
|
||||
diff_summary,
|
||||
billing,
|
||||
} => EventBody::RunFailed(fabro_types::RunFailedProps {
|
||||
failure: failure.clone(),
|
||||
duration_ms: *duration_ms,
|
||||
timing: *timing,
|
||||
final_git_commit_sha: final_git_commit_sha.clone(),
|
||||
final_patch: final_patch.clone(),
|
||||
diff_summary: *diff_summary,
|
||||
|
|
@ -285,7 +285,7 @@ fn event_body_from_event(event: &Event) -> EventBody {
|
|||
}),
|
||||
Event::StageCompleted {
|
||||
index,
|
||||
duration_ms,
|
||||
timing,
|
||||
status,
|
||||
preferred_label,
|
||||
suggested_next_ids,
|
||||
|
|
@ -305,7 +305,7 @@ fn event_body_from_event(event: &Event) -> EventBody {
|
|||
..
|
||||
} => EventBody::StageCompleted(fabro_types::StageCompletedProps {
|
||||
index: *index,
|
||||
duration_ms: *duration_ms,
|
||||
timing: *timing,
|
||||
status: stage_status_from_string(status),
|
||||
preferred_label: preferred_label.clone(),
|
||||
suggested_next_ids: suggested_next_ids.clone(),
|
||||
|
|
@ -327,15 +327,15 @@ fn event_body_from_event(event: &Event) -> EventBody {
|
|||
index,
|
||||
failure,
|
||||
will_retry,
|
||||
duration_ms,
|
||||
timing,
|
||||
billing,
|
||||
..
|
||||
} => EventBody::StageFailed(fabro_types::StageFailedProps {
|
||||
index: *index,
|
||||
failure: Some(failure.clone()),
|
||||
will_retry: *will_retry,
|
||||
duration_ms: *duration_ms,
|
||||
billing: billing.clone(),
|
||||
index: *index,
|
||||
failure: Some(failure.clone()),
|
||||
will_retry: *will_retry,
|
||||
timing: *timing,
|
||||
billing: billing.clone(),
|
||||
}),
|
||||
Event::StageRetrying {
|
||||
index,
|
||||
|
|
@ -1366,7 +1366,7 @@ mod tests {
|
|||
node_id: "plan".to_string(),
|
||||
name: "Plan".to_string(),
|
||||
index: 0,
|
||||
duration_ms: 5000,
|
||||
timing: ::fabro_types::StageTiming::wall_only(5000),
|
||||
status: "succeeded".to_string(),
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
|
|
@ -1399,7 +1399,8 @@ mod tests {
|
|||
assert_eq!(stored.node_label.as_deref(), Some("Plan"));
|
||||
assert_eq!(stored.stage_id, Some(StageId::new("plan", 1)));
|
||||
let properties = stored.properties().unwrap();
|
||||
assert_eq!(properties["duration_ms"], 5000);
|
||||
assert_eq!(properties["timing"]["wall_time_ms"], 5000);
|
||||
assert_eq!(properties["timing"]["active_time_ms"], 0);
|
||||
assert_eq!(properties["status"], "succeeded");
|
||||
assert!(stored.session_id.is_none());
|
||||
}
|
||||
|
|
@ -1410,7 +1411,7 @@ mod tests {
|
|||
node_id: "plan".to_string(),
|
||||
name: "Plan".to_string(),
|
||||
index: 0,
|
||||
duration_ms: 5000,
|
||||
timing: ::fabro_types::StageTiming::wall_only(5000),
|
||||
status: "succeeded".to_string(),
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
|
|
@ -1439,17 +1440,17 @@ mod tests {
|
|||
fn run_event_stage_failure_keeps_failure_detail() {
|
||||
let usage = test_usage("gpt-5.2", 321, 54);
|
||||
let stored = to_run_event(&fixtures::RUN_3, &Event::StageFailed {
|
||||
node_id: "code".to_string(),
|
||||
name: "Code".to_string(),
|
||||
index: 1,
|
||||
failure: FailureDetail::new(
|
||||
node_id: "code".to_string(),
|
||||
name: "Code".to_string(),
|
||||
index: 1,
|
||||
failure: FailureDetail::new(
|
||||
"lint failed",
|
||||
crate::outcome::FailureCategory::Deterministic,
|
||||
),
|
||||
will_retry: true,
|
||||
duration_ms: 5000,
|
||||
billing: Some(usage.clone()),
|
||||
actor: None,
|
||||
will_retry: true,
|
||||
timing: ::fabro_types::StageTiming::wall_only(5000),
|
||||
billing: Some(usage.clone()),
|
||||
actor: None,
|
||||
});
|
||||
|
||||
assert_eq!(stored.event_name(), "stage.failed");
|
||||
|
|
@ -1552,7 +1553,7 @@ mod tests {
|
|||
fn run_event_workflow_failure_uses_display_error() {
|
||||
let event = Event::workflow_run_failed_from_error(
|
||||
&Error::handler("boom"),
|
||||
900,
|
||||
::fabro_types::RunTiming::wall_only(900),
|
||||
FailureReason::WorkflowError,
|
||||
Some("abc123".to_string()),
|
||||
None,
|
||||
|
|
@ -1564,7 +1565,7 @@ mod tests {
|
|||
assert_eq!(stored.event_name(), "run.failed");
|
||||
let properties = stored.properties().unwrap();
|
||||
assert_eq!(properties["failure"]["detail"]["message"], "boom");
|
||||
assert_eq!(properties["duration_ms"], 900);
|
||||
assert_eq!(properties["timing"]["wall_time_ms"], 900);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1572,7 +1573,7 @@ mod tests {
|
|||
let source = EventTestCause;
|
||||
let event = Event::workflow_run_failed_from_error(
|
||||
&Error::engine_with_source("Failed to initialize sandbox", source),
|
||||
900,
|
||||
::fabro_types::RunTiming::wall_only(900),
|
||||
FailureReason::WorkflowError,
|
||||
None,
|
||||
None,
|
||||
|
|
@ -1597,7 +1598,7 @@ mod tests {
|
|||
let source = EventTestCause;
|
||||
let event = Event::workflow_run_failed_from_error(
|
||||
&Error::engine_with_source("Failed to initialize sandbox", source),
|
||||
900,
|
||||
::fabro_types::RunTiming::wall_only(900),
|
||||
FailureReason::SandboxInitFailed,
|
||||
Some("abc123".to_string()),
|
||||
None,
|
||||
|
|
@ -1621,7 +1622,7 @@ mod tests {
|
|||
properties["failure"]["detail"]["category"],
|
||||
"transient_infra"
|
||||
);
|
||||
assert_eq!(properties["duration_ms"], 900);
|
||||
assert_eq!(properties["timing"]["wall_time_ms"], 900);
|
||||
assert_eq!(properties["final_git_commit_sha"], "abc123");
|
||||
assert!(properties.get("error").is_none());
|
||||
assert!(properties.get("causes").is_none());
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ use ::fabro_types::{
|
|||
BilledTokenCounts, BlockedReason, CommandTermination, DiffSummary, FailureReason,
|
||||
ForkSourceRef, GitContext, PairId, PairMessageId, PairSystemMessageKind, PairTarget,
|
||||
ParallelBranchId, Principal, PullRequestLink, RunBlobId, RunFailure, RunId, RunNoticeLevel,
|
||||
RunPairEndedReason, RunPairFailedReason, RunProvenance, SandboxProvider, StageId,
|
||||
SuccessReason, run_event as fabro_types,
|
||||
RunPairEndedReason, RunPairFailedReason, RunProvenance, RunTiming, SandboxProvider, StageId,
|
||||
StageTiming, SuccessReason, run_event as fabro_types,
|
||||
};
|
||||
use fabro_agent::{AgentEvent, SandboxEvent};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -150,7 +150,7 @@ pub enum Event {
|
|||
actor: Option<Principal>,
|
||||
},
|
||||
WorkflowRunCompleted {
|
||||
duration_ms: u64,
|
||||
timing: RunTiming,
|
||||
artifact_count: usize,
|
||||
#[serde(default)]
|
||||
status: String,
|
||||
|
|
@ -168,7 +168,7 @@ pub enum Event {
|
|||
},
|
||||
WorkflowRunFailed {
|
||||
failure: RunFailure,
|
||||
duration_ms: u64,
|
||||
timing: RunTiming,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
final_git_commit_sha: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
|
@ -226,7 +226,7 @@ pub enum Event {
|
|||
node_id: String,
|
||||
name: String,
|
||||
index: usize,
|
||||
duration_ms: u64,
|
||||
timing: StageTiming,
|
||||
status: String,
|
||||
preferred_label: Option<String>,
|
||||
suggested_next_ids: Vec<String>,
|
||||
|
|
@ -253,15 +253,15 @@ pub enum Event {
|
|||
max_attempts: usize,
|
||||
},
|
||||
StageFailed {
|
||||
node_id: String,
|
||||
name: String,
|
||||
index: usize,
|
||||
failure: FailureDetail,
|
||||
will_retry: bool,
|
||||
duration_ms: u64,
|
||||
billing: Option<BilledModelUsage>,
|
||||
node_id: String,
|
||||
name: String,
|
||||
index: usize,
|
||||
failure: FailureDetail,
|
||||
will_retry: bool,
|
||||
timing: StageTiming,
|
||||
billing: Option<BilledModelUsage>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
actor: Option<Principal>,
|
||||
actor: Option<Principal>,
|
||||
},
|
||||
StageRetrying {
|
||||
node_id: String,
|
||||
|
|
@ -724,7 +724,7 @@ impl Event {
|
|||
#[must_use]
|
||||
pub fn workflow_run_failed_from_error(
|
||||
error: &Error,
|
||||
duration_ms: u64,
|
||||
timing: RunTiming,
|
||||
reason: FailureReason,
|
||||
final_git_commit_sha: Option<String>,
|
||||
final_patch: Option<String>,
|
||||
|
|
@ -733,7 +733,7 @@ impl Event {
|
|||
) -> Self {
|
||||
Self::WorkflowRunFailed {
|
||||
failure: run_failure_from_error(error, reason),
|
||||
duration_ms,
|
||||
timing,
|
||||
final_git_commit_sha,
|
||||
final_patch,
|
||||
diff_summary,
|
||||
|
|
@ -868,20 +868,23 @@ impl Event {
|
|||
info!(%previous_parent_id, ?actor, "Run parent unlinked");
|
||||
}
|
||||
Self::WorkflowRunCompleted {
|
||||
duration_ms,
|
||||
timing,
|
||||
artifact_count,
|
||||
status,
|
||||
..
|
||||
} => {
|
||||
info!(
|
||||
duration_ms,
|
||||
artifact_count, status, "Workflow run completed"
|
||||
wall_time_ms = timing.wall_time_ms,
|
||||
active_time_ms = timing.active_time_ms,
|
||||
inference_time_ms = timing.inference_time_ms,
|
||||
tool_time_ms = timing.tool_time_ms,
|
||||
artifact_count,
|
||||
status,
|
||||
"Workflow run completed"
|
||||
);
|
||||
}
|
||||
Self::WorkflowRunFailed {
|
||||
failure,
|
||||
duration_ms,
|
||||
..
|
||||
failure, timing, ..
|
||||
} => {
|
||||
let detail = &failure.detail;
|
||||
let tail =
|
||||
|
|
@ -898,7 +901,8 @@ impl Event {
|
|||
exec_stderr_tail_bytes = tail.stderr_bytes,
|
||||
exec_stdout_truncated = tail.stdout_truncated,
|
||||
exec_stderr_truncated = tail.stderr_truncated,
|
||||
duration_ms,
|
||||
wall_time_ms = timing.wall_time_ms,
|
||||
active_time_ms = timing.active_time_ms,
|
||||
"Workflow run failed"
|
||||
);
|
||||
}
|
||||
|
|
@ -1009,7 +1013,7 @@ impl Event {
|
|||
node_id,
|
||||
name,
|
||||
index,
|
||||
duration_ms,
|
||||
timing,
|
||||
status,
|
||||
attempt,
|
||||
max_attempts,
|
||||
|
|
@ -1019,7 +1023,10 @@ impl Event {
|
|||
node_id,
|
||||
stage = name.as_str(),
|
||||
index,
|
||||
duration_ms,
|
||||
wall_time_ms = timing.wall_time_ms,
|
||||
active_time_ms = timing.active_time_ms,
|
||||
inference_time_ms = timing.inference_time_ms,
|
||||
tool_time_ms = timing.tool_time_ms,
|
||||
status,
|
||||
attempt,
|
||||
max_attempts,
|
||||
|
|
|
|||
|
|
@ -500,7 +500,7 @@ mod tests {
|
|||
node_id: "work".into(),
|
||||
name: "Work".into(),
|
||||
index: 2,
|
||||
duration_ms: 100,
|
||||
timing: fabro_types::StageTiming::wall_only(100),
|
||||
status: "succeeded".into(),
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -1635,9 +1635,8 @@ mod tests {
|
|||
started_at: None,
|
||||
last_event_at: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
elapsed_secs: None,
|
||||
},
|
||||
timing: None,
|
||||
billing: None,
|
||||
diff: None,
|
||||
pull_request: None,
|
||||
|
|
|
|||
|
|
@ -29,57 +29,67 @@ pub(crate) fn millis_u64(d: std::time::Duration) -> u64 {
|
|||
u64::try_from(d.as_millis()).unwrap_or(u64::MAX)
|
||||
}
|
||||
|
||||
/// Extract the `duration_ms` from a `stage.completed` / `stage.failed`
|
||||
/// Extract the timing breakdown from a `stage.completed` / `stage.failed`
|
||||
/// event body, or `None` for any other variant.
|
||||
fn stage_completion_duration_ms(body: &EventBody) -> Option<u64> {
|
||||
fn stage_completion_timing(body: &EventBody) -> Option<fabro_types::StageTiming> {
|
||||
match body {
|
||||
EventBody::StageCompleted(props) => Some(props.duration_ms),
|
||||
EventBody::StageFailed(props) => Some(props.duration_ms),
|
||||
EventBody::StageCompleted(props) => Some(props.timing),
|
||||
EventBody::StageFailed(props) => Some(props.timing),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract per-stage (node_id, visit) durations from `stage.completed` /
|
||||
/// Extract per-stage (node_id, visit) timing from `stage.completed` /
|
||||
/// `stage.failed` events. Keys on the full [`StageId`] so multi-visit stages
|
||||
/// (e.g. a looped `verify` node) keep distinct durations.
|
||||
/// (e.g. a looped `verify` node) keep distinct timings.
|
||||
///
|
||||
/// This is the canonical primitive; [`total_stage_duration_by_node`] and
|
||||
/// [`latest_stage_duration_by_node`] are explicit rollups built on top of it.
|
||||
pub fn extract_stage_durations_by_stage_id(events: &[EventEnvelope]) -> HashMap<StageId, u64> {
|
||||
let mut durations = HashMap::new();
|
||||
/// This is the canonical primitive; [`total_stage_timing_by_node`] and
|
||||
/// [`latest_stage_timing_by_node`] are explicit rollups built on top of it.
|
||||
pub fn extract_stage_timings_by_stage_id(
|
||||
events: &[EventEnvelope],
|
||||
) -> HashMap<StageId, fabro_types::StageTiming> {
|
||||
let mut timings = HashMap::new();
|
||||
for envelope in events {
|
||||
let Some(duration_ms) = stage_completion_duration_ms(&envelope.event.body) else {
|
||||
let Some(timing) = stage_completion_timing(&envelope.event.body) else {
|
||||
continue;
|
||||
};
|
||||
let Some(stage_id) = envelope.event.stage_id.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
durations.insert(stage_id.clone(), duration_ms);
|
||||
timings.insert(stage_id.clone(), timing);
|
||||
}
|
||||
durations
|
||||
timings
|
||||
}
|
||||
|
||||
/// Total duration spent in each node, summed across every visit. Use for
|
||||
/// billing/usage where a retried node should count its full time.
|
||||
pub fn total_stage_duration_by_node(events: &[EventEnvelope]) -> HashMap<String, u64> {
|
||||
let mut totals: HashMap<String, u64> = HashMap::new();
|
||||
for (stage_id, duration_ms) in extract_stage_durations_by_stage_id(events) {
|
||||
*totals.entry(stage_id.node_id().to_string()).or_default() += duration_ms;
|
||||
/// Sum of timing in each node across every visit. Use for billing/usage
|
||||
/// where a retried node should count its full time. `wall_time_ms`,
|
||||
/// `inference_time_ms`, `tool_time_ms`, and `active_time_ms` are all summed
|
||||
/// per node.
|
||||
pub fn total_stage_timing_by_node(
|
||||
events: &[EventEnvelope],
|
||||
) -> HashMap<String, fabro_types::StageTiming> {
|
||||
let mut totals: HashMap<String, fabro_types::StageTiming> = HashMap::new();
|
||||
for (stage_id, timing) in extract_stage_timings_by_stage_id(events) {
|
||||
let entry = totals.entry(stage_id.node_id().to_string()).or_default();
|
||||
*entry = entry.saturating_add(&timing);
|
||||
}
|
||||
totals
|
||||
}
|
||||
|
||||
/// Duration of each node's most recent visit (the highest visit number). Use
|
||||
/// Timing of each node's most recent visit (the highest visit number). Use
|
||||
/// for run summaries where the table shows one row per node and "the last
|
||||
/// attempt" is the right representative.
|
||||
pub fn latest_stage_duration_by_node(events: &[EventEnvelope]) -> HashMap<String, u64> {
|
||||
let mut entries: Vec<(StageId, u64)> = extract_stage_durations_by_stage_id(events)
|
||||
.into_iter()
|
||||
.collect();
|
||||
pub fn latest_stage_timing_by_node(
|
||||
events: &[EventEnvelope],
|
||||
) -> HashMap<String, fabro_types::StageTiming> {
|
||||
let mut entries: Vec<(StageId, fabro_types::StageTiming)> =
|
||||
extract_stage_timings_by_stage_id(events)
|
||||
.into_iter()
|
||||
.collect();
|
||||
entries.sort_by_key(|(stage_id, _)| stage_id.visit());
|
||||
let mut latest = HashMap::new();
|
||||
for (stage_id, duration_ms) in entries {
|
||||
latest.insert(stage_id.node_id().to_string(), duration_ms);
|
||||
for (stage_id, timing) in entries {
|
||||
latest.insert(stage_id.node_id().to_string(), timing);
|
||||
}
|
||||
latest
|
||||
}
|
||||
|
|
@ -89,14 +99,13 @@ mod duration_tests {
|
|||
use chrono::{TimeZone, Utc};
|
||||
use fabro_store::EventEnvelope;
|
||||
use fabro_types::run_event::{StageCompletedProps, StageFailedProps};
|
||||
use fabro_types::{EventBody, RunEvent, StageId, StageOutcome, fixtures};
|
||||
use fabro_types::{EventBody, RunEvent, StageId, StageOutcome, StageTiming, fixtures};
|
||||
|
||||
use super::{
|
||||
extract_stage_durations_by_stage_id, latest_stage_duration_by_node,
|
||||
total_stage_duration_by_node,
|
||||
extract_stage_timings_by_stage_id, latest_stage_timing_by_node, total_stage_timing_by_node,
|
||||
};
|
||||
|
||||
fn completed_event(seq: u32, node: &str, visit: u32, duration_ms: u64) -> EventEnvelope {
|
||||
fn completed_event(seq: u32, node: &str, visit: u32, wall_time_ms: u64) -> EventEnvelope {
|
||||
let event = RunEvent {
|
||||
id: format!("evt_{seq}"),
|
||||
ts: Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap(),
|
||||
|
|
@ -112,7 +121,7 @@ mod duration_tests {
|
|||
actor: None,
|
||||
body: EventBody::StageCompleted(StageCompletedProps {
|
||||
index: 0,
|
||||
duration_ms,
|
||||
timing: StageTiming::wall_only(wall_time_ms),
|
||||
status: StageOutcome::Succeeded,
|
||||
preferred_label: None,
|
||||
suggested_next_ids: vec![],
|
||||
|
|
@ -134,7 +143,7 @@ mod duration_tests {
|
|||
EventEnvelope { seq, event }
|
||||
}
|
||||
|
||||
fn failed_event(seq: u32, node: &str, visit: u32, duration_ms: u64) -> EventEnvelope {
|
||||
fn failed_event(seq: u32, node: &str, visit: u32, wall_time_ms: u64) -> EventEnvelope {
|
||||
let event = RunEvent {
|
||||
id: format!("evt_{seq}"),
|
||||
ts: Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap(),
|
||||
|
|
@ -149,62 +158,126 @@ mod duration_tests {
|
|||
tool_call_id: None,
|
||||
actor: None,
|
||||
body: EventBody::StageFailed(StageFailedProps {
|
||||
index: 0,
|
||||
failure: None,
|
||||
index: 0,
|
||||
failure: None,
|
||||
will_retry: true,
|
||||
duration_ms,
|
||||
billing: None,
|
||||
timing: StageTiming::wall_only(wall_time_ms),
|
||||
billing: None,
|
||||
}),
|
||||
};
|
||||
EventEnvelope { seq, event }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_keys_durations_by_full_stage_id() {
|
||||
fn extract_keys_timings_by_full_stage_id() {
|
||||
let events = vec![
|
||||
completed_event(1, "verify", 1, 100),
|
||||
completed_event(2, "verify", 2, 200),
|
||||
];
|
||||
let durations = extract_stage_durations_by_stage_id(&events);
|
||||
let timings = extract_stage_timings_by_stage_id(&events);
|
||||
assert_eq!(
|
||||
durations.get(&StageId::new("verify", 1)).copied(),
|
||||
timings
|
||||
.get(&StageId::new("verify", 1))
|
||||
.map(|t| t.wall_time_ms),
|
||||
Some(100)
|
||||
);
|
||||
assert_eq!(
|
||||
durations.get(&StageId::new("verify", 2)).copied(),
|
||||
timings
|
||||
.get(&StageId::new("verify", 2))
|
||||
.map(|t| t.wall_time_ms),
|
||||
Some(200)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn total_sums_across_visits_per_node() {
|
||||
fn total_sums_wall_time_across_visits_per_node() {
|
||||
let events = vec![
|
||||
completed_event(1, "verify", 1, 100),
|
||||
completed_event(2, "verify", 2, 200),
|
||||
completed_event(3, "build", 1, 50),
|
||||
];
|
||||
let totals = total_stage_duration_by_node(&events);
|
||||
assert_eq!(totals.get("verify").copied(), Some(300));
|
||||
assert_eq!(totals.get("build").copied(), Some(50));
|
||||
let totals = total_stage_timing_by_node(&events);
|
||||
assert_eq!(totals.get("verify").map(|t| t.wall_time_ms), Some(300));
|
||||
assert_eq!(totals.get("build").map(|t| t.wall_time_ms), Some(50));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latest_picks_highest_visit_regardless_of_input_order() {
|
||||
// Visit 2 appears in the events vector before visit 1; the result
|
||||
// must still reflect visit 2's duration (the latest visit).
|
||||
// must still reflect visit 2's timing (the latest visit).
|
||||
let events = vec![
|
||||
completed_event(1, "verify", 2, 999),
|
||||
completed_event(2, "verify", 1, 100),
|
||||
];
|
||||
let latest = latest_stage_duration_by_node(&events);
|
||||
assert_eq!(latest.get("verify").copied(), Some(999));
|
||||
let latest = latest_stage_timing_by_node(&events);
|
||||
assert_eq!(latest.get("verify").map(|t| t.wall_time_ms), Some(999));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stage_failed_durations_are_included() {
|
||||
fn stage_failed_timings_are_included() {
|
||||
let events = vec![failed_event(1, "verify", 1, 75)];
|
||||
let durations = extract_stage_durations_by_stage_id(&events);
|
||||
assert_eq!(durations.get(&StageId::new("verify", 1)).copied(), Some(75));
|
||||
let timings = extract_stage_timings_by_stage_id(&events);
|
||||
assert_eq!(
|
||||
timings
|
||||
.get(&StageId::new("verify", 1))
|
||||
.map(|t| t.wall_time_ms),
|
||||
Some(75)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn total_sums_active_breakdown_across_visits() {
|
||||
// Same node visited twice with different inference/tool breakdowns:
|
||||
// the rollup must add inference, tool, and active fields, not just
|
||||
// wall time. This guards against accidentally summing wall only.
|
||||
fn timed_completed(seq: u32, visit: u32, timing: StageTiming) -> EventEnvelope {
|
||||
let event = RunEvent {
|
||||
id: format!("evt_{seq}"),
|
||||
ts: Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap(),
|
||||
run_id: fixtures::RUN_1,
|
||||
node_id: Some("agent".to_string()),
|
||||
node_label: None,
|
||||
stage_id: Some(StageId::new("agent", visit)),
|
||||
parallel_group_id: None,
|
||||
parallel_branch_id: None,
|
||||
session_id: None,
|
||||
parent_session_id: None,
|
||||
tool_call_id: None,
|
||||
actor: None,
|
||||
body: EventBody::StageCompleted(StageCompletedProps {
|
||||
index: 0,
|
||||
timing,
|
||||
status: StageOutcome::Succeeded,
|
||||
preferred_label: None,
|
||||
suggested_next_ids: vec![],
|
||||
billing: None,
|
||||
failure: None,
|
||||
notes: None,
|
||||
files_touched: vec![],
|
||||
context_updates: None,
|
||||
jump_to_node: None,
|
||||
context_values: None,
|
||||
node_visits: None,
|
||||
loop_failure_signatures: None,
|
||||
restart_failure_signatures: None,
|
||||
response: None,
|
||||
attempt: 1,
|
||||
max_attempts: 1,
|
||||
}),
|
||||
};
|
||||
EventEnvelope { seq, event }
|
||||
}
|
||||
|
||||
let events = vec![
|
||||
timed_completed(1, 1, StageTiming::new(1000, 600, 300)),
|
||||
timed_completed(2, 2, StageTiming::new(700, 400, 200)),
|
||||
];
|
||||
let totals = total_stage_timing_by_node(&events);
|
||||
let agent = totals.get("agent").copied().unwrap();
|
||||
assert_eq!(agent.wall_time_ms, 1700);
|
||||
assert_eq!(agent.inference_time_ms, 1000);
|
||||
assert_eq!(agent.tool_time_ms, 500);
|
||||
assert_eq!(agent.active_time_ms, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use fabro_core::lifecycle::{
|
|||
};
|
||||
use fabro_core::outcome::NodeResult;
|
||||
use fabro_core::state::ExecutionState;
|
||||
use fabro_types::{Principal, RunId};
|
||||
use fabro_types::{Principal, RunId, StageTiming};
|
||||
|
||||
use super::circuit_breaker::CircuitBreakerLifecycle;
|
||||
use super::git::GitCheckpointResult;
|
||||
|
|
@ -73,6 +73,18 @@ fn actor_for_stage_failure(failure: &FailureDetail) -> Option<Principal> {
|
|||
.map(|system_kind| Principal::System { system_kind })
|
||||
}
|
||||
|
||||
/// Build a [`StageTiming`] from a [`WfNodeResult`]. Inference and tool time
|
||||
/// flow from the executor's `NodeResult` fields, which are populated from
|
||||
/// `outcome.timing` by [`fabro_core`]. Handlers without an active-time
|
||||
/// breakdown produce a wall-only timing.
|
||||
fn node_result_timing(result: &WfNodeResult) -> StageTiming {
|
||||
StageTiming::new(
|
||||
crate::millis_u64(result.wall_time),
|
||||
crate::millis_u64(result.inference_time),
|
||||
crate::millis_u64(result.tool_time),
|
||||
)
|
||||
}
|
||||
|
||||
fn response_from_outcome(node_id: &str, outcome: &Outcome) -> Option<String> {
|
||||
outcome
|
||||
.context_updates
|
||||
|
|
@ -154,7 +166,7 @@ impl RunLifecycle<WorkflowGraph> for EventLifecycle {
|
|||
node_id: gv.id.clone(),
|
||||
name: gv.label().to_string(),
|
||||
index: stage_index,
|
||||
duration_ms: 0,
|
||||
timing: StageTiming::wall_only(0),
|
||||
status: StageOutcome::Succeeded.to_string(),
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
|
|
@ -211,7 +223,7 @@ impl RunLifecycle<WorkflowGraph> for EventLifecycle {
|
|||
let stage_index = state.stage_index;
|
||||
let scope = stage_scope_for(state, &gv.id);
|
||||
|
||||
let duration_ms = crate::millis_u64(ctx.result.duration);
|
||||
let timing = node_result_timing(ctx.result);
|
||||
let failure = outcome.failure.clone().unwrap_or_else(|| {
|
||||
FailureDetail::new("handler failed", FailureCategory::TransientInfra)
|
||||
});
|
||||
|
|
@ -223,7 +235,7 @@ impl RunLifecycle<WorkflowGraph> for EventLifecycle {
|
|||
index: stage_index,
|
||||
failure,
|
||||
will_retry: true,
|
||||
duration_ms,
|
||||
timing,
|
||||
billing: outcome.usage.clone(),
|
||||
actor,
|
||||
},
|
||||
|
|
@ -259,7 +271,7 @@ impl RunLifecycle<WorkflowGraph> for EventLifecycle {
|
|||
let gv = node.inner();
|
||||
let stage_index = state.stage_index;
|
||||
let scope = stage_scope_for(state, &gv.id);
|
||||
let duration_ms = crate::millis_u64(result.duration);
|
||||
let timing = node_result_timing(result);
|
||||
let (loop_failure_signatures, restart_failure_signatures) =
|
||||
snapshot_failure_signatures(&self.circuit_breaker);
|
||||
|
||||
|
|
@ -275,7 +287,7 @@ impl RunLifecycle<WorkflowGraph> for EventLifecycle {
|
|||
index: stage_index,
|
||||
failure,
|
||||
will_retry: false,
|
||||
duration_ms,
|
||||
timing,
|
||||
billing: outcome.usage.clone(),
|
||||
actor,
|
||||
},
|
||||
|
|
@ -287,7 +299,7 @@ impl RunLifecycle<WorkflowGraph> for EventLifecycle {
|
|||
node_id: gv.id.clone(),
|
||||
name: gv.label().to_string(),
|
||||
index: stage_index,
|
||||
duration_ms,
|
||||
timing,
|
||||
status: outcome.status.to_string(),
|
||||
preferred_label: outcome.preferred_label.clone(),
|
||||
suggested_next_ids: outcome.suggested_next_ids.clone(),
|
||||
|
|
|
|||
|
|
@ -956,7 +956,14 @@ mod tests {
|
|||
let node = graph.get_node("build").unwrap();
|
||||
let mut state = ExecutionState::new(&graph).unwrap();
|
||||
state.increment_visits("build");
|
||||
let result = WfNodeResult::new(Outcome::success(), Duration::from_millis(10), 1, 1);
|
||||
let result = WfNodeResult::new(
|
||||
Outcome::success(),
|
||||
Duration::from_millis(10),
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
|
||||
lifecycle
|
||||
.on_checkpoint(&node, &result, Some("exit"), &state)
|
||||
|
|
@ -999,7 +1006,14 @@ mod tests {
|
|||
let node = graph.get_node("build").unwrap();
|
||||
let mut state = ExecutionState::new(&graph).unwrap();
|
||||
state.increment_visits("build");
|
||||
let result = WfNodeResult::new(Outcome::success(), Duration::from_millis(10), 1, 1);
|
||||
let result = WfNodeResult::new(
|
||||
Outcome::success(),
|
||||
Duration::from_millis(10),
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
|
||||
lifecycle
|
||||
.on_checkpoint(&node, &result, Some("exit"), &state)
|
||||
|
|
@ -1060,7 +1074,14 @@ mod tests {
|
|||
let node = graph.get_node("build").unwrap();
|
||||
let mut state = ExecutionState::new(&graph).unwrap();
|
||||
state.increment_visits("build");
|
||||
let result = WfNodeResult::new(Outcome::success(), Duration::from_millis(10), 1, 1);
|
||||
let result = WfNodeResult::new(
|
||||
Outcome::success(),
|
||||
Duration::from_millis(10),
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
|
||||
lifecycle
|
||||
.on_checkpoint(&node, &result, Some("exit"), &state)
|
||||
|
|
@ -1127,7 +1148,14 @@ mod tests {
|
|||
let node = graph.get_node("build").unwrap();
|
||||
let mut state = ExecutionState::new(&graph).unwrap();
|
||||
state.increment_visits("build");
|
||||
let result = WfNodeResult::new(Outcome::success(), Duration::from_millis(10), 1, 1);
|
||||
let result = WfNodeResult::new(
|
||||
Outcome::success(),
|
||||
Duration::from_millis(10),
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
|
||||
lifecycle
|
||||
.on_checkpoint(&node, &result, Some("exit"), &state)
|
||||
|
|
@ -1189,7 +1217,14 @@ mod tests {
|
|||
let node = graph.get_node("build").unwrap();
|
||||
let mut checkpoint_state = ExecutionState::new(&graph).unwrap();
|
||||
checkpoint_state.increment_visits("build");
|
||||
let result = WfNodeResult::new(Outcome::success(), Duration::from_millis(10), 1, 1);
|
||||
let result = WfNodeResult::new(
|
||||
Outcome::success(),
|
||||
Duration::from_millis(10),
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
lifecycle
|
||||
.on_checkpoint(&node, &result, Some("exit"), &checkpoint_state)
|
||||
.await
|
||||
|
|
@ -1220,7 +1255,7 @@ mod tests {
|
|||
let conclusion = Conclusion {
|
||||
timestamp: chrono::Utc::now(),
|
||||
status: StageOutcome::Succeeded,
|
||||
duration_ms: 10,
|
||||
timing: fabro_types::RunTiming::wall_only(10),
|
||||
failure: None,
|
||||
final_git_commit_sha: None,
|
||||
stages: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
event::append_event(&run_store, run_id, &Event::WorkflowRunCompleted {
|
||||
duration_ms: 10,
|
||||
timing: fabro_types::RunTiming::wall_only(10),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
@ -185,7 +185,7 @@ mod tests {
|
|||
.unwrap();
|
||||
let failure_event = Event::workflow_run_failed_from_error(
|
||||
&crate::error::Error::engine("boom"),
|
||||
10,
|
||||
fabro_types::RunTiming::wall_only(10),
|
||||
FailureReason::WorkflowError,
|
||||
None,
|
||||
None,
|
||||
|
|
|
|||
|
|
@ -392,7 +392,7 @@ mod tests {
|
|||
node_id: "work".to_string(),
|
||||
name: "Work".to_string(),
|
||||
index: 1,
|
||||
duration_ms: 10,
|
||||
timing: fabro_types::StageTiming::wall_only(10),
|
||||
status: "succeeded".to_string(),
|
||||
preferred_label: None,
|
||||
suggested_next_ids: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ async fn persist_terminal_engine_failure(
|
|||
};
|
||||
let failure_event = Event::workflow_run_failed_from_error(
|
||||
error,
|
||||
crate::millis_u64(duration),
|
||||
fabro_types::RunTiming::wall_only(crate::millis_u64(duration)),
|
||||
reason,
|
||||
None,
|
||||
None,
|
||||
|
|
@ -1003,7 +1003,7 @@ impl Drop for DetachedRunBootstrapGuard {
|
|||
handle.spawn(async move {
|
||||
let failure_event = Event::workflow_run_failed_from_error(
|
||||
&Error::engine(reason.to_string()),
|
||||
0,
|
||||
fabro_types::RunTiming::default(),
|
||||
reason,
|
||||
None,
|
||||
None,
|
||||
|
|
@ -1070,7 +1070,7 @@ impl Drop for DetachedRunCompletionGuard {
|
|||
handle.spawn(async move {
|
||||
let failure_event = Event::workflow_run_failed_from_error(
|
||||
&Error::engine(message.to_string()),
|
||||
0,
|
||||
fabro_types::RunTiming::default(),
|
||||
reason,
|
||||
None,
|
||||
None,
|
||||
|
|
@ -1100,8 +1100,15 @@ async fn persist_detached_failure(
|
|||
) -> Result<(), Error> {
|
||||
let message = error.to_string();
|
||||
|
||||
let failure_event =
|
||||
Event::workflow_run_failed_from_error(error, 0, reason, None, None, None, None);
|
||||
let failure_event = Event::workflow_run_failed_from_error(
|
||||
error,
|
||||
fabro_types::RunTiming::default(),
|
||||
reason,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
if let Err(err) = append_event_to_sink(event_sink, &run_id, &failure_event).await {
|
||||
tracing::warn!(error = %err, "Failed to append detached failure event");
|
||||
}
|
||||
|
|
@ -1719,7 +1726,7 @@ reasoning = false
|
|||
let conclusion = crate::records::Conclusion {
|
||||
timestamp: Utc::now(),
|
||||
status: StageOutcome::Succeeded,
|
||||
duration_ms: 1,
|
||||
timing: fabro_types::RunTiming::wall_only(1),
|
||||
failure: None,
|
||||
final_git_commit_sha: None,
|
||||
stages: vec![],
|
||||
|
|
@ -1761,7 +1768,7 @@ reasoning = false
|
|||
.await
|
||||
.unwrap();
|
||||
crate::event::append_event(&run_store, &fixtures::RUN_1, &Event::WorkflowRunCompleted {
|
||||
duration_ms: conclusion.duration_ms,
|
||||
timing: conclusion.timing,
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: crate::run_status::SuccessReason::Completed,
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ pub async fn execute(init: Initialized) -> Executed {
|
|||
graph,
|
||||
outcome: Err(err),
|
||||
run_options,
|
||||
duration_ms: crate::millis_u64(start.elapsed()),
|
||||
wall_time_ms: crate::millis_u64(start.elapsed()),
|
||||
final_context: seed_context_from_checkpoint(checkpoint.as_ref()),
|
||||
engine,
|
||||
model,
|
||||
|
|
@ -163,7 +163,7 @@ pub async fn execute(init: Initialized) -> Executed {
|
|||
graph,
|
||||
outcome: Err(err),
|
||||
run_options,
|
||||
duration_ms: crate::millis_u64(start.elapsed()),
|
||||
wall_time_ms: crate::millis_u64(start.elapsed()),
|
||||
final_context: seed,
|
||||
engine,
|
||||
model,
|
||||
|
|
@ -178,7 +178,7 @@ pub async fn execute(init: Initialized) -> Executed {
|
|||
graph,
|
||||
outcome: Err(err),
|
||||
run_options,
|
||||
duration_ms: crate::millis_u64(start.elapsed()),
|
||||
wall_time_ms: crate::millis_u64(start.elapsed()),
|
||||
final_context: Context::new(),
|
||||
engine,
|
||||
model,
|
||||
|
|
@ -294,13 +294,13 @@ pub async fn execute(init: Initialized) -> Executed {
|
|||
|
||||
engine.registry.shutdown_all(&engine.run.emitter).await;
|
||||
|
||||
let duration_ms = crate::millis_u64(start.elapsed());
|
||||
let wall_time_ms = crate::millis_u64(start.elapsed());
|
||||
|
||||
Executed {
|
||||
graph,
|
||||
outcome,
|
||||
run_options,
|
||||
duration_ms,
|
||||
wall_time_ms,
|
||||
final_context,
|
||||
engine,
|
||||
model,
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ pub(crate) async fn build_conclusion_from_store(
|
|||
run_store: &RunStoreHandle,
|
||||
status: StageOutcome,
|
||||
failure: Option<RunFailure>,
|
||||
run_duration_ms: u64,
|
||||
run_wall_time_ms: u64,
|
||||
final_git_commit_sha: Option<String>,
|
||||
) -> Conclusion {
|
||||
let projection = run_store.state().await.ok();
|
||||
|
|
@ -94,7 +94,7 @@ pub(crate) async fn build_conclusion_from_store(
|
|||
&projection_order,
|
||||
status,
|
||||
failure,
|
||||
run_duration_ms,
|
||||
run_wall_time_ms,
|
||||
final_git_commit_sha,
|
||||
)
|
||||
}
|
||||
|
|
@ -105,7 +105,7 @@ fn build_conclusion_from_parts(
|
|||
projection_order: &HashMap<String, u32>,
|
||||
status: StageOutcome,
|
||||
failure: Option<RunFailure>,
|
||||
run_duration_ms: u64,
|
||||
run_wall_time_ms: u64,
|
||||
final_git_commit_sha: Option<String>,
|
||||
) -> Conclusion {
|
||||
// Looping workflows revisit nodes; `completed_nodes` accumulates duplicates
|
||||
|
|
@ -154,7 +154,8 @@ fn build_conclusion_from_parts(
|
|||
let summary = StageSummary {
|
||||
stage_id: node_id.to_string(),
|
||||
stage_label: node_id.to_string(),
|
||||
duration_ms: billing.map_or(0, |stage| stage.duration_ms),
|
||||
timing: billing
|
||||
.map_or_else(fabro_types::StageTiming::default, |stage| stage.timing),
|
||||
billing_usd_micros: billing.and_then(|stage| stage.billing.total_usd_micros),
|
||||
retries,
|
||||
};
|
||||
|
|
@ -182,7 +183,7 @@ fn build_conclusion_from_parts(
|
|||
Conclusion {
|
||||
timestamp: chrono::Utc::now(),
|
||||
status,
|
||||
duration_ms: run_duration_ms,
|
||||
timing: projection_billing.timing.with_wall_time(run_wall_time_ms),
|
||||
failure,
|
||||
final_git_commit_sha,
|
||||
stages,
|
||||
|
|
@ -443,7 +444,7 @@ pub(crate) fn billing_from_projection(projection: &RunProjection) -> Option<Bill
|
|||
|
||||
pub(crate) fn build_terminal_event(
|
||||
outcome: &Result<Outcome, Error>,
|
||||
duration_ms: u64,
|
||||
timing: fabro_types::RunTiming,
|
||||
artifact_count: usize,
|
||||
final_git_commit_sha: Option<String>,
|
||||
final_patch: Option<String>,
|
||||
|
|
@ -462,7 +463,7 @@ pub(crate) fn build_terminal_event(
|
|||
{
|
||||
let total_usd_micros = billing.as_ref().and_then(|b| b.total_usd_micros);
|
||||
return Event::WorkflowRunCompleted {
|
||||
duration_ms,
|
||||
timing,
|
||||
artifact_count,
|
||||
status: outcome_status.to_string(),
|
||||
reason: match outcome_status {
|
||||
|
|
@ -493,7 +494,7 @@ pub(crate) fn build_terminal_event(
|
|||
};
|
||||
Event::WorkflowRunFailed {
|
||||
failure,
|
||||
duration_ms,
|
||||
timing,
|
||||
final_git_commit_sha,
|
||||
final_patch,
|
||||
diff_summary,
|
||||
|
|
@ -533,7 +534,7 @@ pub async fn finalize(executed: Executed, options: &FinalizeOptions) -> Result<C
|
|||
graph,
|
||||
outcome,
|
||||
run_options,
|
||||
duration_ms,
|
||||
wall_time_ms,
|
||||
final_context: _,
|
||||
engine,
|
||||
model: _,
|
||||
|
|
@ -565,7 +566,7 @@ pub async fn finalize(executed: Executed, options: &FinalizeOptions) -> Result<C
|
|||
&projection_order,
|
||||
final_status,
|
||||
failure_reason,
|
||||
duration_ms,
|
||||
wall_time_ms,
|
||||
options.last_git_sha.clone(),
|
||||
);
|
||||
|
||||
|
|
@ -584,7 +585,7 @@ pub async fn finalize(executed: Executed, options: &FinalizeOptions) -> Result<C
|
|||
|
||||
let terminal_event = build_terminal_event(
|
||||
&outcome,
|
||||
duration_ms,
|
||||
conclusion.timing,
|
||||
artifact_count,
|
||||
options.last_git_sha.clone(),
|
||||
final_patch,
|
||||
|
|
@ -698,7 +699,7 @@ mod tests {
|
|||
graph: Graph,
|
||||
outcome: Result<Outcome, Error>,
|
||||
run_options: RunOptions,
|
||||
duration_ms: u64,
|
||||
wall_time_ms: u64,
|
||||
services: Arc<RunServices>,
|
||||
) -> Executed {
|
||||
let mut engine = EngineServices::test_default();
|
||||
|
|
@ -707,7 +708,7 @@ mod tests {
|
|||
graph,
|
||||
outcome,
|
||||
run_options,
|
||||
duration_ms,
|
||||
wall_time_ms,
|
||||
final_context: Context::new(),
|
||||
engine: Arc::new(engine),
|
||||
model: "test-model".to_string(),
|
||||
|
|
@ -955,7 +956,7 @@ mod tests {
|
|||
let failed_usage = test_usage("gpt-old", 100, 10);
|
||||
let success_usage = test_usage("gpt-new", 200, 20);
|
||||
let failed = projection.stage_entry("verify", 1, first_event_seq(1));
|
||||
failed.duration_ms = Some(1200);
|
||||
failed.timing = Some(fabro_types::StageTiming::wall_only(1200));
|
||||
failed.usage = BilledTokenCounts::from_billed_usage(std::slice::from_ref(&failed_usage));
|
||||
failed.model = Some(failed_usage.model().clone());
|
||||
failed.completion = Some(StageCompletion {
|
||||
|
|
@ -967,7 +968,7 @@ mod tests {
|
|||
timestamp: chrono::Utc::now(),
|
||||
});
|
||||
let succeeded = projection.stage_entry("verify", 2, first_event_seq(2));
|
||||
succeeded.duration_ms = Some(800);
|
||||
succeeded.timing = Some(fabro_types::StageTiming::wall_only(800));
|
||||
succeeded.usage =
|
||||
BilledTokenCounts::from_billed_usage(std::slice::from_ref(&success_usage));
|
||||
succeeded.model = Some(success_usage.model().clone());
|
||||
|
|
@ -982,7 +983,7 @@ mod tests {
|
|||
let projection_billing = billing_rollup_from_projection(&projection, None);
|
||||
let mut latest_outcome = Outcome::success();
|
||||
latest_outcome.usage = Some(success_usage);
|
||||
latest_outcome.duration_ms = Some(800);
|
||||
latest_outcome.timing = Some(fabro_types::StageTiming::wall_only(800));
|
||||
let mut checkpoint = checkpoint_with(
|
||||
vec!["verify", "verify"],
|
||||
HashMap::from([("verify".to_string(), latest_outcome)]),
|
||||
|
|
@ -1007,7 +1008,7 @@ mod tests {
|
|||
);
|
||||
assert_eq!(conclusion.stages.len(), 1);
|
||||
assert_eq!(conclusion.stages[0].stage_id, "verify");
|
||||
assert_eq!(conclusion.stages[0].duration_ms, 2000);
|
||||
assert_eq!(conclusion.stages[0].timing.wall_time_ms, 2000);
|
||||
assert_eq!(conclusion.stages[0].billing_usd_micros, Some(330));
|
||||
assert_eq!(conclusion.stages[0].retries, 1);
|
||||
}
|
||||
|
|
@ -1104,7 +1105,7 @@ mod tests {
|
|||
let conclusion = Conclusion {
|
||||
timestamp: chrono::Utc::now(),
|
||||
status: StageOutcome::Succeeded,
|
||||
duration_ms: 10,
|
||||
timing: fabro_types::RunTiming::wall_only(10),
|
||||
failure: None,
|
||||
final_git_commit_sha: None,
|
||||
stages: Vec::new(),
|
||||
|
|
@ -1167,7 +1168,7 @@ mod tests {
|
|||
let conclusion = Conclusion {
|
||||
timestamp: chrono::Utc::now(),
|
||||
status: StageOutcome::Succeeded,
|
||||
duration_ms: 10,
|
||||
timing: fabro_types::RunTiming::wall_only(10),
|
||||
failure: None,
|
||||
final_git_commit_sha: None,
|
||||
stages: Vec::new(),
|
||||
|
|
@ -1219,7 +1220,7 @@ mod tests {
|
|||
let conclusion = Conclusion {
|
||||
timestamp: chrono::Utc::now(),
|
||||
status: StageOutcome::Succeeded,
|
||||
duration_ms: 10,
|
||||
timing: fabro_types::RunTiming::wall_only(10),
|
||||
failure: None,
|
||||
final_git_commit_sha: None,
|
||||
stages: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ fn format_arc_details_section(
|
|||
parts.push(String::new());
|
||||
|
||||
// Cost table
|
||||
let total_duration = format_duration_ms(conclusion.duration_ms);
|
||||
let total_duration = format_duration_ms(conclusion.timing.wall_time_ms);
|
||||
let total_cost_str = format_cost(conclusion.billing.as_ref().and_then(|b| b.total_usd_micros));
|
||||
let stage_count = conclusion.stages.len();
|
||||
parts.push(format!(
|
||||
|
|
@ -184,7 +184,7 @@ fn format_arc_details_section(
|
|||
parts.push("| Stage | Duration | Cost | Retries |".to_string());
|
||||
parts.push("|---|---|---|---|".to_string());
|
||||
for stage in &conclusion.stages {
|
||||
let dur = format_duration_ms(stage.duration_ms);
|
||||
let dur = format_duration_ms(stage.timing.wall_time_ms);
|
||||
let cost = format_cost(stage.billing_usd_micros);
|
||||
parts.push(format!(
|
||||
"| {} | {} | {} | {} |",
|
||||
|
|
@ -870,28 +870,28 @@ mod tests {
|
|||
Conclusion {
|
||||
timestamp: Utc::now(),
|
||||
status: crate::outcome::StageOutcome::Succeeded,
|
||||
duration_ms: 150_000,
|
||||
timing: fabro_types::RunTiming::wall_only(150_000),
|
||||
failure: None,
|
||||
final_git_commit_sha: None,
|
||||
stages: vec![
|
||||
StageSummary {
|
||||
stage_id: "plan".to_string(),
|
||||
stage_label: "plan".to_string(),
|
||||
duration_ms: 45_000,
|
||||
timing: fabro_types::StageTiming::wall_only(45_000),
|
||||
billing_usd_micros: Some(120_000),
|
||||
retries: 0,
|
||||
},
|
||||
StageSummary {
|
||||
stage_id: "implement".to_string(),
|
||||
stage_label: "implement".to_string(),
|
||||
duration_ms: 90_000,
|
||||
timing: fabro_types::StageTiming::wall_only(90_000),
|
||||
billing_usd_micros: Some(250_000),
|
||||
retries: 0,
|
||||
},
|
||||
StageSummary {
|
||||
stage_id: "simplify".to_string(),
|
||||
stage_label: "simplify".to_string(),
|
||||
duration_ms: 15_000,
|
||||
timing: fabro_types::StageTiming::wall_only(15_000),
|
||||
billing_usd_micros: Some(50_000),
|
||||
retries: 0,
|
||||
},
|
||||
|
|
@ -1244,7 +1244,7 @@ mod tests {
|
|||
node_id: "plan".to_string(),
|
||||
name: "plan".to_string(),
|
||||
index: 0,
|
||||
duration_ms: 1,
|
||||
timing: fabro_types::StageTiming::wall_only(1),
|
||||
status: "succeeded".to_string(),
|
||||
preferred_label: None,
|
||||
suggested_next_ids: vec![],
|
||||
|
|
@ -1600,7 +1600,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
append_event(&run_store, &fixtures::RUN_1, &Event::WorkflowRunCompleted {
|
||||
duration_ms: 1,
|
||||
timing: fabro_types::RunTiming::wall_only(1),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
@ -1717,7 +1717,7 @@ mod tests {
|
|||
node_id: "plan".to_string(),
|
||||
name: "plan".to_string(),
|
||||
index: 0,
|
||||
duration_ms: 1,
|
||||
timing: fabro_types::StageTiming::wall_only(1),
|
||||
status: "succeeded".to_string(),
|
||||
preferred_label: None,
|
||||
suggested_next_ids: vec![],
|
||||
|
|
@ -1888,7 +1888,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
append_event(&run_store, &fixtures::RUN_1, &Event::WorkflowRunCompleted {
|
||||
duration_ms: 1,
|
||||
timing: fabro_types::RunTiming::wall_only(1),
|
||||
artifact_count: 0,
|
||||
status: "succeeded".to_string(),
|
||||
reason: SuccessReason::Completed,
|
||||
|
|
|
|||
|
|
@ -299,7 +299,8 @@ pub struct Executed {
|
|||
pub graph: Graph,
|
||||
pub outcome: Result<Outcome, Error>,
|
||||
pub run_options: RunOptions,
|
||||
pub duration_ms: u64,
|
||||
/// Run wall-clock time in milliseconds from EXECUTE start to outcome.
|
||||
pub wall_time_ms: u64,
|
||||
pub final_context: Context,
|
||||
pub engine: Arc<EngineServices>,
|
||||
pub model: String,
|
||||
|
|
|
|||
|
|
@ -140,10 +140,10 @@ impl RunInfo {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn duration_ms(&self) -> Option<u64> {
|
||||
pub fn wall_time_ms(&self) -> Option<u64> {
|
||||
self.summary
|
||||
.as_ref()
|
||||
.and_then(|summary| summary.timestamps.duration_ms)
|
||||
.and_then(|summary| summary.timing.as_ref().map(|t| t.wall_time_ms))
|
||||
}
|
||||
|
||||
pub fn total_cost(&self) -> Option<f64> {
|
||||
|
|
@ -284,8 +284,11 @@ fn run_info_from_summary(summary: &Run, scratch_base: &Path) -> Option<RunInfo>
|
|||
let dir_name = path.file_name()?.to_string_lossy().to_string();
|
||||
let start_time_dt = summary.id.created_at();
|
||||
let end_time = if summary.lifecycle.status.is_terminal() {
|
||||
summary.timestamps.duration_ms.and_then(|duration_ms| {
|
||||
Some(start_time_dt + chrono::Duration::milliseconds(i64::try_from(duration_ms).ok()?))
|
||||
summary.timing.as_ref().and_then(|timing| {
|
||||
Some(
|
||||
start_time_dt
|
||||
+ chrono::Duration::milliseconds(i64::try_from(timing.wall_time_ms).ok()?),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ async fn execute_and_emit_terminal(initialized: InitializedState) -> Executed {
|
|||
let billing = state.as_ref().and_then(billing_from_projection);
|
||||
let event = build_terminal_event(
|
||||
&executed.outcome,
|
||||
executed.duration_ms,
|
||||
fabro_types::RunTiming::wall_only(executed.wall_time_ms),
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
|
|
|
|||
|
|
@ -7130,7 +7130,7 @@ async fn workflow_run_with_vault_only_openai_codex_builds_pr_body() {
|
|||
Some(&Conclusion {
|
||||
timestamp: Utc::now(),
|
||||
status: StageOutcome::Succeeded,
|
||||
duration_ms: 1,
|
||||
timing: fabro_types::RunTiming::wall_only(1),
|
||||
failure: None,
|
||||
final_git_commit_sha: None,
|
||||
stages: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
"typecheck": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openapitools/openapi-generator-cli": "2.20.2",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ models/run-status-succeeded.ts
|
|||
models/run-status.ts
|
||||
models/run-superseded-by-props.ts
|
||||
models/run-timestamps.ts
|
||||
models/run-timings.ts
|
||||
models/run-timing.ts
|
||||
models/run.ts
|
||||
models/sandbox-details.ts
|
||||
models/sandbox-file-entry.ts
|
||||
|
|
@ -384,6 +384,7 @@ models/stage-outcome.ts
|
|||
models/stage-projection.ts
|
||||
models/stage-state.ts
|
||||
models/stage-summary.ts
|
||||
models/stage-timing.ts
|
||||
models/start-record.ts
|
||||
models/start-run-request.ts
|
||||
models/steer-run-request.ts
|
||||
|
|
@ -425,4 +426,4 @@ models/workflow-ref.ts
|
|||
models/workflow-reference.ts
|
||||
models/workflow-schedule-summary.ts
|
||||
models/workflow-settings.ts
|
||||
models/write-blob-response.ts
|
||||
models/write-blob-response.ts
|
||||
|
|
@ -13,6 +13,9 @@
|
|||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunTiming } from './run-timing';
|
||||
|
||||
/**
|
||||
* Aggregate billing totals across all runs.
|
||||
|
|
@ -51,7 +54,7 @@ export interface AggregateBillingTotals {
|
|||
*/
|
||||
'total_usd_micros'?: number | null;
|
||||
/**
|
||||
* Total runtime in seconds.
|
||||
* Aggregate timing rollup across every completed run. Active timing sums work across stage visits, so `active_time_ms` can exceed `wall_time_ms`.
|
||||
*/
|
||||
'runtime_secs': number;
|
||||
'timing': RunTiming;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export interface CheckRun {
|
|||
'name': string;
|
||||
'status': CheckRunStatus;
|
||||
/**
|
||||
* Duration of the check run in seconds.
|
||||
* Wall-clock duration of the check run in milliseconds.
|
||||
*/
|
||||
'duration_secs'?: number;
|
||||
'wall_time_ms'?: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ import type { RunDiff } from './run-diff';
|
|||
import type { RunFailure } from './run-failure';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunTiming } from './run-timing';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { StageOutcome } from './stage-outcome';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
|
|
@ -35,7 +38,7 @@ import type { StageSummary } from './stage-summary';
|
|||
export interface Conclusion {
|
||||
'timestamp': string;
|
||||
'status': StageOutcome;
|
||||
'duration_ms': number;
|
||||
'timing': RunTiming;
|
||||
'failure'?: RunFailure | null;
|
||||
'final_git_commit_sha'?: string | null;
|
||||
'stages': Array<StageSummary>;
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ export * from './run-status-submitted';
|
|||
export * from './run-status-succeeded';
|
||||
export * from './run-superseded-by-props';
|
||||
export * from './run-timestamps';
|
||||
export * from './run-timings';
|
||||
export * from './run-timing';
|
||||
export * from './sandbox-details';
|
||||
export * from './sandbox-file-entry';
|
||||
export * from './sandbox-file-list-response';
|
||||
|
|
@ -360,6 +360,7 @@ export * from './stage-outcome';
|
|||
export * from './stage-projection';
|
||||
export * from './stage-state';
|
||||
export * from './stage-summary';
|
||||
export * from './stage-timing';
|
||||
export * from './start-record';
|
||||
export * from './start-run-request';
|
||||
export * from './steer-run-request';
|
||||
|
|
@ -401,4 +402,4 @@ export * from './workflow-ref';
|
|||
export * from './workflow-reference';
|
||||
export * from './workflow-schedule-summary';
|
||||
export * from './workflow-settings';
|
||||
export * from './write-blob-response';
|
||||
export * from './write-blob-response';
|
||||
|
|
@ -25,18 +25,21 @@ import type { BillingStageRef } from './billing-stage-ref';
|
|||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { StageState } from './stage-state';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { StageTiming } from './stage-timing';
|
||||
|
||||
/**
|
||||
* Token counts and billed totals for one workflow node within a run. Rows are grouped by node; billing and runtime sum every visit of that node.
|
||||
* Token counts and billed totals for one workflow node within a run. Rows are grouped by node; billing and timing sum every visit of that node.
|
||||
*/
|
||||
export interface RunBillingStage {
|
||||
'stage': BillingStageRef;
|
||||
'model': BillingModelRef | null;
|
||||
'billing': BilledTokenCounts;
|
||||
/**
|
||||
* Wall-clock runtime in seconds, summed across every visit of this node.
|
||||
* Per-node timing summed across every visit. `wall_time_ms` is the sum of visit wall times; the active breakdown sums work timing.
|
||||
*/
|
||||
'runtime_secs': number;
|
||||
'timing': StageTiming;
|
||||
/**
|
||||
* Wall-clock time the latest attempt of this stage started, if known.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -13,15 +13,18 @@
|
|||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunTiming } from './run-timing';
|
||||
|
||||
/**
|
||||
* Aggregate billing totals across all stages of a run.
|
||||
*/
|
||||
export interface RunBillingTotals {
|
||||
/**
|
||||
* Total wall-clock runtime in seconds.
|
||||
* Run-level timing rollup. `wall_time_ms` is summed across stage visits; active timing sums work across visits.
|
||||
*/
|
||||
'runtime_secs': number;
|
||||
'timing': RunTiming;
|
||||
/**
|
||||
* Total input tokens consumed.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ export interface RunStage {
|
|||
'handler': StageHandler;
|
||||
'status': StageState;
|
||||
/**
|
||||
* Time spent in this stage, in seconds.
|
||||
* Wall-clock time the latest attempt spent in this stage, in milliseconds.
|
||||
*/
|
||||
'duration_secs'?: number;
|
||||
'wall_time_ms'?: number;
|
||||
/**
|
||||
* Node id in the workflow graph; multiple stages with different visits share the same node_id.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -19,6 +19,4 @@ export interface RunTimestamps {
|
|||
'started_at': string | null;
|
||||
'last_event_at': string | null;
|
||||
'completed_at': string | null;
|
||||
'duration_ms'?: number | null;
|
||||
'elapsed_secs'?: number | null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,15 +15,14 @@
|
|||
|
||||
|
||||
/**
|
||||
* Timing information for a run.
|
||||
* Timing rollup for an entire run. Active fields sum work across stage visits, so `active_time_ms` can exceed `wall_time_ms` when parallel branches run concurrently.
|
||||
*/
|
||||
export interface RunTimings {
|
||||
export interface RunTiming {
|
||||
'wall_time_ms': number;
|
||||
'inference_time_ms'?: number;
|
||||
'tool_time_ms'?: number;
|
||||
/**
|
||||
* Wall-clock time elapsed in seconds.
|
||||
* Equals `inference_time_ms + tool_time_ms`.
|
||||
*/
|
||||
'elapsed_secs': number;
|
||||
/**
|
||||
* Whether the elapsed time exceeds the expected threshold.
|
||||
*/
|
||||
'elapsed_warning'?: boolean;
|
||||
'active_time_ms': number;
|
||||
}
|
||||
4
lib/packages/fabro-api-client/src/models/run.ts
generated
4
lib/packages/fabro-api-client/src/models/run.ts
generated
|
|
@ -54,6 +54,9 @@ import type { RunSandbox } from './run-sandbox';
|
|||
import type { RunTimestamps } from './run-timestamps';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunTiming } from './run-timing';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { WorkflowRef } from './workflow-ref';
|
||||
|
||||
/**
|
||||
|
|
@ -82,6 +85,7 @@ export interface Run {
|
|||
'models': Array<RunModel>;
|
||||
'source_directory': string | null;
|
||||
'timestamps': RunTimestamps;
|
||||
'timing': RunTiming | null;
|
||||
'billing': RunBillingSummary | null;
|
||||
'diff': DiffSummary | null;
|
||||
'pull_request': PullRequestLink | null;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ import type { StageCompletion } from './stage-completion';
|
|||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { StageState } from './stage-state';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { StageTiming } from './stage-timing';
|
||||
|
||||
/**
|
||||
* Observable projection data for one workflow stage execution.
|
||||
|
|
@ -62,10 +65,7 @@ export interface StageProjection {
|
|||
* Wall-clock time the latest attempt of this stage started, if known.
|
||||
*/
|
||||
'started_at'?: string | null;
|
||||
/**
|
||||
* Wall-clock duration of the stage\'s latest terminal attempt, if known.
|
||||
*/
|
||||
'duration_ms'?: number | null;
|
||||
'timing'?: StageTiming | null;
|
||||
'usage': BilledTokenCounts;
|
||||
'model'?: BillingModelRef | null;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@
|
|||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { StageTiming } from './stage-timing';
|
||||
|
||||
/**
|
||||
* Terminal summary for one stage in a run conclusion.
|
||||
|
|
@ -20,7 +23,7 @@
|
|||
export interface StageSummary {
|
||||
'stage_id': string;
|
||||
'stage_label': string;
|
||||
'duration_ms': number;
|
||||
'timing': StageTiming;
|
||||
'billing_usd_micros'?: number | null;
|
||||
'retries': number;
|
||||
}
|
||||
|
|
|
|||
28
lib/packages/fabro-api-client/src/models/stage-timing.ts
generated
Normal file
28
lib/packages/fabro-api-client/src/models/stage-timing.ts
generated
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Timing breakdown for one stage visit. Fields are all milliseconds. `wall_time_ms` is elapsed clock time; `inference_time_ms` is Fabro- observed LLM request/stream elapsed time; `tool_time_ms` is tool or command execution elapsed time; `active_time_ms` equals `inference_time_ms + tool_time_ms`.
|
||||
*/
|
||||
export interface StageTiming {
|
||||
'wall_time_ms': number;
|
||||
'inference_time_ms'?: number;
|
||||
'tool_time_ms'?: number;
|
||||
/**
|
||||
* Equals `inference_time_ms + tool_time_ms`.
|
||||
*/
|
||||
'active_time_ms': number;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue