mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-20 00:11:34 +00:00
refactor(storage): unify scratch paths and key schema
- centralize FABRO_HOME and storage path resolution in fabro-config - rename store types, extract ArtifactStore, and simplify run key layout - switch run scratch to scratch/, remove RuntimeState, and refresh docs/clients
This commit is contained in:
parent
beec4c8dff
commit
35593d0a11
87 changed files with 2011 additions and 1210 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
|
@ -1494,6 +1494,7 @@ dependencies = [
|
|||
"prettyplease",
|
||||
"progenitor",
|
||||
"progenitor-client",
|
||||
"regress",
|
||||
"reqwest 0.13.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
@ -1599,6 +1600,7 @@ name = "fabro-config"
|
|||
version = "0.176.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"dirs",
|
||||
"fabro-types",
|
||||
|
|
@ -1937,6 +1939,7 @@ dependencies = [
|
|||
"fabro-types",
|
||||
"futures",
|
||||
"object_store",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"slatedb",
|
||||
|
|
@ -2011,11 +2014,11 @@ dependencies = [
|
|||
"clap",
|
||||
"dirs",
|
||||
"fabro-macros",
|
||||
"hex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"ulid",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ exec = "0.3"
|
|||
slatedb = "0.11.2"
|
||||
object_store = "0.12.5"
|
||||
rust-embed = "8"
|
||||
percent-encoding = "2"
|
||||
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "deny"
|
||||
|
|
|
|||
|
|
@ -1,298 +1,81 @@
|
|||
# Run Directory Keys
|
||||
# Run Scratch Files
|
||||
|
||||
All keys that may be written to the run store during a workflow execution, with event source mappings.
|
||||
This document maps the files written under a run scratch directory to their event sources.
|
||||
|
||||
## 1. `_init.json`
|
||||
Scope:
|
||||
- Scratch root: `~/.fabro/scratch/YYYYMMDD-{run_id}/`
|
||||
- This covers local run files only
|
||||
- Persistent store keys live in `lib/crates/fabro-store/src/keys.rs`
|
||||
- Artifact object-store keys live in `lib/crates/fabro-store/src/artifact_store.rs`
|
||||
|
||||
Store initialization metadata. Written when the store is created.
|
||||
There is no `_init.json` anymore. Run existence in the database is determined by stored run events, and local scratch directories are managed separately under `scratch/`.
|
||||
|
||||
No event source — written directly at store creation time.
|
||||
## Root-Level Files
|
||||
|
||||
| Field | Description | Event Source |
|
||||
|-------|-------------|--------------|
|
||||
| `run_id` | ULID string | — |
|
||||
| `created_at` | RFC 3339 timestamp | — |
|
||||
| `db_prefix` | SlateDB key prefix | — |
|
||||
| `run_dir` | path to run directory (optional) | — |
|
||||
| File | Purpose | Event source |
|
||||
|---|---|---|
|
||||
| `run.json` | Run metadata snapshot: run id, resolved settings, graph, workflow slug, working directory, repo info, labels | Primarily `run.created`, plus request-time inputs persisted during create |
|
||||
| `start.json` | Start timestamp and git context | `run.started` |
|
||||
| `workflow.fabro` | Original dot source when available | `run.created.properties.workflow_source` |
|
||||
| `workflow.toml` | Original workflow TOML when available | `run.created.properties.workflow_config` |
|
||||
| `progress.jsonl` | Append-only event log | Every emitted run event |
|
||||
| `live.json` | Latest live state snapshot | Derived continuously from emitted events |
|
||||
| `checkpoint.json` | Crash-recovery snapshot | Derived from accumulated stage/checkpoint events plus engine state |
|
||||
| `conclusion.json` | Final outcome summary | `run.completed` and `run.failed` |
|
||||
| `retro.json` | Post-run retro output | `retro.completed` |
|
||||
| `final.patch` | Final git diff for checkpointed runs | Local git state at finalize time, not a direct event payload |
|
||||
| `cli.log` | Per-run tracing log | Local tracing output, not event-derived |
|
||||
| `run.pid` | Legacy detached-run pid file from older runs | Legacy only; current flows do not rely on it |
|
||||
|
||||
## 2. `run.json`
|
||||
## Local-Only Directories
|
||||
|
||||
Run configuration snapshot. Written at run creation.
|
||||
These paths are local runtime state, not canonical event projections.
|
||||
|
||||
No single event carries this data. The `run.started` event has a subset (`name`, `run_id`, `base_branch`, `base_sha`, `run_branch`, `goal`) but not `settings`, `graph`, or `labels`.
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `worktree/` | Git worktree used by checkpointed runs |
|
||||
| `runtime/interview_request.json` | Detached-run interview request IPC file |
|
||||
| `runtime/interview_response.json` | Detached-run interview response IPC file |
|
||||
| `runtime/interview_request.claim` | Detached-run interview claim lock |
|
||||
| `cache/artifacts/values/` | Large context values spilled to the filesystem |
|
||||
| `cache/artifacts/files/` | Captured artifact files organized by node and retry |
|
||||
|
||||
| Field | Description | Event Source |
|
||||
|-------|-------------|--------------|
|
||||
| `run_id` | ULID string | `run.started` → `envelope.run_id` |
|
||||
| `created_at` | RFC 3339 timestamp | — |
|
||||
| `settings` | full Settings object | — |
|
||||
| `graph` | parsed workflow graph | — |
|
||||
| `workflow_slug` | workflow slug (optional) | — |
|
||||
| `working_directory` | path string | — |
|
||||
| `host_repo_path` | original host repo path (optional) | — |
|
||||
| `base_branch` | base git branch (optional) | `run.started` → `properties.base_branch` |
|
||||
| `labels` | string key-value map (optional) | — |
|
||||
## Node Directories
|
||||
|
||||
## 3. `start.json`
|
||||
Per-node outputs are written under:
|
||||
- `nodes/{node_id}/`
|
||||
- `nodes/{node_id}-visit_{N}/` for retries, where `N` starts at `2`
|
||||
|
||||
Start timestamp and git context. Written when execution begins.
|
||||
### Agent and prompt nodes
|
||||
|
||||
| Field | Description | Event Source |
|
||||
|-------|-------------|--------------|
|
||||
| `run_id` | ULID string | `run.started` → `envelope.run_id` |
|
||||
| `start_time` | RFC 3339 timestamp | `run.started` → `envelope.ts` |
|
||||
| `run_branch` | git branch for the run (optional) | `run.started` → `properties.run_branch` |
|
||||
| `base_sha` | base commit SHA (optional) | `run.started` → `properties.base_sha` |
|
||||
| File | Purpose | Event source |
|
||||
|---|---|---|
|
||||
| `prompt.md` | Rendered prompt text | `stage.prompt.properties.text` is the closest event source |
|
||||
| `response.md` | Final model response text | Reconstructable from message events, but written as a local convenience file |
|
||||
| `status.json` | Final node status, notes, failure reason, timestamp | `stage.completed` |
|
||||
|
||||
## 4. `checkpoint.json`
|
||||
### Command nodes
|
||||
|
||||
Latest execution state. Updated after each node completes.
|
||||
| File | Purpose | Event source |
|
||||
|---|---|---|
|
||||
| `script_invocation.json` | Command metadata: command, language, timeout | Partly from node config; not fully represented by a single event |
|
||||
| `stdout.log` | Captured stdout | Local process output |
|
||||
| `stderr.log` | Captured stderr | Local process output |
|
||||
| `script_timing.json` | Duration, exit code, timeout result | Partly `stage.completed.properties.duration_ms`, otherwise local process state |
|
||||
| `status.json` | Final node status | `stage.completed` |
|
||||
|
||||
Checkpoint is an accumulated snapshot built from multiple events over time. Individual fields map to specific events, but the full object is never in a single event.
|
||||
### Nodes with git checkpointing
|
||||
|
||||
| Field | Description | Event Source |
|
||||
|-------|-------------|--------------|
|
||||
| `timestamp` | RFC 3339 timestamp | — (written at checkpoint time) |
|
||||
| `current_node` | node being executed | `stage.started` → `envelope.node_id` |
|
||||
| `completed_nodes` | list of completed node ids | accumulated from `stage.completed` → `envelope.node_id` |
|
||||
| `node_retries` | map of node id → retry count | accumulated from `stage.retrying` → `envelope.node_id` + `properties.attempt` |
|
||||
| `context_values` | map of context key → JSON value | — (internal engine state) |
|
||||
| `node_outcomes` | map of node id → outcome | see below |
|
||||
| `next_node_id` | pre-selected next node (optional) | `edge.selected` → `properties.to_node` |
|
||||
| `git_commit_sha` | current HEAD SHA (optional) | `checkpoint.completed` → `properties.git_commit_sha` |
|
||||
| `loop_failure_signatures` | failure signature → count (optional) | — (internal engine state) |
|
||||
| `restart_failure_signatures` | failure signature → count (optional) | — (internal engine state) |
|
||||
| `node_visits` | node id → visit count (optional) | — (internal engine state) |
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `diff.patch` | Per-node git diff captured at checkpoint time |
|
||||
|
||||
**`node_outcomes[node_id]`** — each outcome maps to `stage.completed`:
|
||||
### Manager / child workflow nodes
|
||||
|
||||
| Field | Description | Event Source |
|
||||
|-------|-------------|--------------|
|
||||
| `status` | `"success"` / `"fail"` / `"skipped"` / `"partial_success"` / `"retry"` | `stage.completed` → `properties.status` |
|
||||
| `preferred_label` | edge label hint (optional) | `stage.completed` → `properties.preferred_label` |
|
||||
| `suggested_next_ids` | successor node ids (optional) | `stage.completed` → `properties.suggested_next_ids` |
|
||||
| `context_updates` | context key → JSON value (optional) | — (not in event) |
|
||||
| `jump_to_node` | non-edge jump target (optional) | — (not in event) |
|
||||
| `notes` | free-text notes (optional) | `stage.completed` → `properties.notes` |
|
||||
| `failure.message` | error description | `stage.completed` → `properties.error` (flattened) |
|
||||
| `failure.failure_class` | failure category | `stage.completed` → `properties.failure_class` (flattened) |
|
||||
| `failure.failure_signature` | dedup key (optional) | `stage.completed` → `properties.failure_signature` (flattened) |
|
||||
| `usage.model` | model identifier | `stage.completed` → `properties.usage.model` |
|
||||
| `usage.input_tokens` | input token count | `stage.completed` → `properties.usage.input_tokens` |
|
||||
| `usage.output_tokens` | output token count | `stage.completed` → `properties.usage.output_tokens` |
|
||||
| `usage.cache_read_tokens` | cache read tokens (optional) | `stage.completed` → `properties.usage.cache_read_tokens` |
|
||||
| `usage.cache_write_tokens` | cache write tokens (optional) | `stage.completed` → `properties.usage.cache_write_tokens` |
|
||||
| `usage.reasoning_tokens` | reasoning tokens (optional) | `stage.completed` → `properties.usage.reasoning_tokens` |
|
||||
| `usage.speed` | speed tier (optional) | `stage.completed` → `properties.usage.speed` |
|
||||
| `usage.cost` | estimated cost in USD (optional) | `stage.completed` → `properties.usage.cost` |
|
||||
| `files_touched` | file paths modified (optional) | `stage.completed` → `properties.files_touched` |
|
||||
| `duration_ms` | stage duration (optional) | `stage.completed` → `properties.duration_ms` |
|
||||
Manager nodes may create a nested `child/` directory containing a full run scratch structure for the child workflow.
|
||||
|
||||
## 5. `conclusion.json`
|
||||
## Notes
|
||||
|
||||
Final run summary. Written when the run finishes.
|
||||
|
||||
| Field | Description | Event Source |
|
||||
|-------|-------------|--------------|
|
||||
| `timestamp` | RFC 3339 timestamp | — (written at conclusion time) |
|
||||
| `status` | final status | `run.completed` → `properties.status` |
|
||||
| `duration_ms` | total run duration | `run.completed` → `properties.duration_ms` |
|
||||
| `failure_reason` | error message (optional) | `run.failed` → `properties.error` |
|
||||
| `final_git_commit_sha` | final HEAD SHA (optional) | `run.completed` → `properties.final_git_commit_sha` |
|
||||
| `stages` | list of stage summaries (optional) | — (aggregated, not in events) |
|
||||
| `stages[].stage_id` | node id | `stage.completed` → `envelope.node_id` |
|
||||
| `stages[].stage_label` | display label | `stage.completed` → `envelope.node_label` |
|
||||
| `stages[].duration_ms` | stage duration | `stage.completed` → `properties.duration_ms` |
|
||||
| `stages[].cost` | cost in USD (optional) | `stage.completed` → `properties.usage.cost` |
|
||||
| `stages[].retries` | retry count | accumulated from `stage.retrying` events |
|
||||
| `total_cost` | aggregate cost (optional) | `run.completed` → `properties.total_cost` |
|
||||
| `total_retries` | aggregate retries | — (aggregated from stage events) |
|
||||
| `total_input_tokens` | aggregate input tokens | `run.completed` → `properties.usage.input_tokens` |
|
||||
| `total_output_tokens` | aggregate output tokens | `run.completed` → `properties.usage.output_tokens` |
|
||||
| `total_cache_read_tokens` | aggregate cache read tokens | `run.completed` → `properties.usage.cache_read_tokens` |
|
||||
| `total_cache_write_tokens` | aggregate cache write tokens | `run.completed` → `properties.usage.cache_write_tokens` |
|
||||
| `total_reasoning_tokens` | aggregate reasoning tokens | `run.completed` → `properties.usage.reasoning_tokens` |
|
||||
| `has_pricing` | whether cost data is available | — (derived from `total_cost`) |
|
||||
|
||||
## 6. `retro.json`
|
||||
|
||||
Retrospective analysis. Written after the retro agent completes.
|
||||
|
||||
| Field | Description | Event Source |
|
||||
|-------|-------------|--------------|
|
||||
| `run_id` | ULID string | `retro.completed` → `envelope.run_id` |
|
||||
| `workflow_name` | workflow name | `run.started` → `properties.name` |
|
||||
| `goal` | workflow goal text | `run.started` → `properties.goal` |
|
||||
| `timestamp` | RFC 3339 timestamp | `retro.completed` → `envelope.ts` |
|
||||
| `smoothness` | rating (optional) | `retro.completed` → `properties.retro.smoothness` |
|
||||
| `stages` | list of stage retro objects | `retro.completed` → `properties.retro.stages` |
|
||||
| `stats` | aggregate stats object | `retro.completed` → `properties.retro.stats` |
|
||||
| `intent` | what the run intended to do (optional) | `retro.completed` → `properties.retro.intent` |
|
||||
| `outcome` | what actually happened (optional) | `retro.completed` → `properties.retro.outcome` |
|
||||
| `learnings` | list of learnings (optional) | `retro.completed` → `properties.retro.learnings` |
|
||||
| `friction_points` | list of friction points (optional) | `retro.completed` → `properties.retro.friction_points` |
|
||||
| `open_items` | list of open items (optional) | `retro.completed` → `properties.retro.open_items` |
|
||||
|
||||
## 7. `sandbox.json`
|
||||
|
||||
Sandbox environment details. Written when the sandbox is ready.
|
||||
|
||||
| Field | Description | Event Source |
|
||||
|-------|-------------|--------------|
|
||||
| `provider` | provider name | `sandbox.initialized` → `properties.provider` |
|
||||
| `working_directory` | working directory in sandbox | `sandbox.initialized` → `properties.working_directory` |
|
||||
| `identifier` | instance identifier (optional) | `sandbox.initialized` → `properties.identifier` |
|
||||
| `host_working_directory` | host-side path (optional) | `sandbox.initialized` → `properties.host_working_directory` |
|
||||
| `container_mount_point` | container mount point (optional) | `sandbox.initialized` → `properties.container_mount_point` |
|
||||
|
||||
## 8. `workflow.fabro`
|
||||
|
||||
Raw Graphviz dot source for the workflow graph. Plain text, not JSON.
|
||||
|
||||
Event source: `run.created` → `properties.workflow_source`
|
||||
|
||||
## 9. `workflow.toml`
|
||||
|
||||
Workflow configuration in TOML format. Same schema as `settings` in `run.json`.
|
||||
|
||||
Event source: `run.created` → `properties.workflow_config`
|
||||
|
||||
## 10. `checkpoints/{seq:04}-{epoch_ms}.json`
|
||||
|
||||
Checkpoint history snapshots. Same schema as `checkpoint.json` (#4).
|
||||
|
||||
Each snapshot is written on `checkpoint.completed` events.
|
||||
|
||||
## 11. `nodes/{node_id}/prompt.md`
|
||||
|
||||
Prompt sent to the LLM for agent or prompt nodes. Plain text/markdown, not JSON.
|
||||
|
||||
Partial event source: `stage.prompt` → `properties.text` carries the rendered prompt text.
|
||||
|
||||
## 12. `nodes/{node_id}/response.md`
|
||||
|
||||
Response received from the LLM. Plain text/markdown, not JSON.
|
||||
|
||||
Reconstructable from `agent.message` → `properties.text` events (one per LLM turn), but the file contains only the final response.
|
||||
|
||||
## 13. `nodes/{node_id}/status.json`
|
||||
|
||||
Node execution status. Written when a node completes.
|
||||
|
||||
| Field | Description | Event Source |
|
||||
|-------|-------------|--------------|
|
||||
| `status` | stage status | `stage.completed` → `properties.status` |
|
||||
| `notes` | free-text notes (optional) | `stage.completed` → `properties.notes` |
|
||||
| `failure_reason` | error message (optional) | `stage.completed` → `properties.error` (flattened from failure) |
|
||||
| `timestamp` | RFC 3339 timestamp | `stage.completed` → `envelope.ts` |
|
||||
|
||||
## 14. `nodes/{node_id}/stdout.log`
|
||||
|
||||
Standard output from command nodes. Plain text, not JSON.
|
||||
|
||||
No event source — captured from sandbox exec, not emitted as events.
|
||||
|
||||
## 15. `nodes/{node_id}/stderr.log`
|
||||
|
||||
Standard error from command nodes. Plain text, not JSON.
|
||||
|
||||
No event source — captured from sandbox exec, not emitted as events.
|
||||
|
||||
## 16. `nodes/{node_id}/cli_stdout.log`
|
||||
|
||||
Standard output from CLI-backend LLM invocations. Plain text, not JSON.
|
||||
|
||||
No event source — captured from CLI subprocess, not emitted as events.
|
||||
|
||||
## 17. `nodes/{node_id}/cli_stderr.log`
|
||||
|
||||
Standard error from CLI-backend LLM invocations. Plain text, not JSON.
|
||||
|
||||
No event source — captured from CLI subprocess, not emitted as events.
|
||||
|
||||
## 18. `nodes/{node_id}/diff.patch`
|
||||
|
||||
Git diff of sandbox changes made by the node. Plain text unified diff, not JSON.
|
||||
|
||||
No event source — generated from git at checkpoint time.
|
||||
|
||||
## 19. `nodes/{node_id}/provider_used.json`
|
||||
|
||||
LLM provider metadata. Written for agent, prompt, and CLI-backend nodes.
|
||||
|
||||
No direct event. Closest: `agent.failover` carries `from_provider`/`to_provider`/`from_model`/`to_model`, but only on failover. The initial provider choice is not emitted as an event.
|
||||
|
||||
| Field | Description | Event Source |
|
||||
|-------|-------------|--------------|
|
||||
| `mode` | `"agent"` / `"prompt"` / `"cli"` | — |
|
||||
| `provider` | provider name | — |
|
||||
| `model` | model identifier | `stage.completed` → `properties.usage.model` (indirect) |
|
||||
| `command` | CLI command (only when mode=cli) | — |
|
||||
|
||||
## 20. `nodes/{node_id}/script_invocation.json`
|
||||
|
||||
Command node invocation details. Written before the command runs.
|
||||
|
||||
| Field | Description | Event Source |
|
||||
|-------|-------------|--------------|
|
||||
| `command` | shell command or script body | `stage.started` → `properties.script` (when handler_type is command) |
|
||||
| `language` | `"shell"` / `"python"` | — |
|
||||
| `timeout_ms` | timeout in milliseconds (null if none) | — |
|
||||
|
||||
## 21. `nodes/{node_id}/script_timing.json`
|
||||
|
||||
Command node execution timing. Written after the command completes.
|
||||
|
||||
| Field | Description | Event Source |
|
||||
|-------|-------------|--------------|
|
||||
| `duration_ms` | execution duration | `stage.completed` → `properties.duration_ms` |
|
||||
| `exit_code` | process exit code (null if timed out) | — |
|
||||
| `timed_out` | whether command was killed by timeout | — |
|
||||
|
||||
## 22. `nodes/{node_id}/parallel_results.json`
|
||||
|
||||
Results from parallel branch execution. Written by the parallel handler.
|
||||
|
||||
Array of objects:
|
||||
|
||||
| Field | Description | Event Source |
|
||||
|-------|-------------|--------------|
|
||||
| `id` | branch node id | `parallel.branch.completed` → `envelope.node_id` |
|
||||
| `status` | status string | `parallel.branch.completed` → `properties.status` |
|
||||
| `head_sha` | git HEAD SHA (optional) | `parallel.branch.completed` → `properties.head_sha` |
|
||||
|
||||
## 23. `retro/prompt.md`
|
||||
|
||||
Prompt sent to the retro agent. Plain text/markdown, not JSON.
|
||||
|
||||
Event source: `retro.started` → `properties.prompt`
|
||||
|
||||
## 24. `retro/response.md`
|
||||
|
||||
Response received from the retro agent. Plain text/markdown, not JSON.
|
||||
|
||||
Event source: `retro.completed` → `properties.response`
|
||||
|
||||
## 25. `retro/status.json`
|
||||
|
||||
Retro agent execution status.
|
||||
|
||||
| Field | Description | Event Source |
|
||||
|-------|-------------|--------------|
|
||||
| `outcome` | `"success"` / `"failure"` | `retro.completed` or `retro.failed` (inferred from which event fires) |
|
||||
| `failure_reason` | error message (null on success) | `retro.failed` → `properties.error` |
|
||||
| `timestamp` | RFC 3339 timestamp | `retro.completed` → `envelope.ts` or `retro.failed` → `envelope.ts` |
|
||||
|
||||
## 26. `retro/provider_used.json`
|
||||
|
||||
Retro agent LLM provider metadata.
|
||||
|
||||
| Field | Description | Event Source |
|
||||
|-------|-------------|--------------|
|
||||
| `mode` | execution mode | constant `"agent"` plus `retro.started` context |
|
||||
| `provider` | LLM provider | `retro.started` → `properties.provider` |
|
||||
| `model` | model identifier | `retro.started` → `properties.model` |
|
||||
|
||||
---
|
||||
|
||||
**Node visit directories:** The first visit writes to `nodes/{node_id}/`. Subsequent visits write to `nodes/{node_id}-visit_{N}/` where N is the visit number.
|
||||
- `progress.jsonl` is the event log; many other files are denormalized convenience snapshots derived from that stream plus local runtime state.
|
||||
- `checkpoint.json` and `live.json` are projections, not single-event payloads.
|
||||
- Artifact binaries are no longer stored in the SlateDB keyspace. They live in `ArtifactStore`; the run scratch tree only contains local cached copies when a workflow stage writes them to disk.
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ Values under 100KB remain in the context as-is.
|
|||
Offloaded artifacts are written to the run's directory:
|
||||
|
||||
```
|
||||
~/.fabro/runs/{run_id}/
|
||||
~/.fabro/scratch/{run_id}/
|
||||
cache/
|
||||
artifacts/
|
||||
values/
|
||||
|
|
@ -207,7 +207,7 @@ For each pointer in the context updates:
|
|||
|
||||
```
|
||||
# Before sync (host path)
|
||||
file:///home/user/.fabro/runs/01JK.../cache/artifacts/values/response.plan.json
|
||||
file:///home/user/.fabro/scratch/01JK.../cache/artifacts/values/response.plan.json
|
||||
|
||||
# After sync (sandbox path)
|
||||
file:///workspace/.fabro/artifacts/response.plan.json
|
||||
|
|
@ -256,7 +256,7 @@ Tool caches and dependency directories (`node_modules`, `.cache/ms-playwright`,
|
|||
Collected assets are written to the run's directory, organized by node and retry attempt:
|
||||
|
||||
```
|
||||
~/.fabro/runs/{run_id}/
|
||||
~/.fabro/scratch/{run_id}/
|
||||
cache/
|
||||
artifacts/
|
||||
assets/
|
||||
|
|
|
|||
|
|
@ -1919,14 +1919,14 @@ components:
|
|||
description: Content-addressed blob identifier.
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
example: 550e8400-e29b-41d4-a716-446655440000
|
||||
pattern: '^[0-9a-f]{64}$'
|
||||
example: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
|
||||
|
||||
ArtifactFilename:
|
||||
name: filename
|
||||
in: query
|
||||
required: true
|
||||
description: Artifact filename. May contain path separators.
|
||||
description: Relative artifact path. `/` is allowed as a path separator. Backslash, empty segments, and traversal segments (`.` and `..`) are invalid.
|
||||
schema:
|
||||
type: string
|
||||
example: src/lib.rs
|
||||
|
|
|
|||
|
|
@ -71,15 +71,15 @@ Because event payload lives in `properties`, most shell queries should look ther
|
|||
```bash
|
||||
# Count tool calls in a run
|
||||
jq -r 'select(.event == "agent.tool.started") | .properties.tool_name' \
|
||||
~/.fabro/runs/01JKXYZ.../progress.jsonl | wc -l
|
||||
~/.fabro/scratch/01JKXYZ.../progress.jsonl | wc -l
|
||||
|
||||
# Find stage failures
|
||||
jq 'select(.event == "stage.failed")' \
|
||||
~/.fabro/runs/01JKXYZ.../progress.jsonl
|
||||
~/.fabro/scratch/01JKXYZ.../progress.jsonl
|
||||
|
||||
# See which edges were taken
|
||||
jq '{from: .properties.from_node, to: .properties.to_node, label: .properties.label}' \
|
||||
~/.fabro/runs/01JKXYZ.../progress.jsonl | head
|
||||
~/.fabro/scratch/01JKXYZ.../progress.jsonl | head
|
||||
```
|
||||
|
||||
`live.json` is still a pretty-printed copy of the most recent event envelope.
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ description: "Structure of Fabro's per-run directory"
|
|||
The run directory structure and file formats described here are internal implementation details and subject to change without notice. Do not build tooling that relies on them.
|
||||
</Warning>
|
||||
|
||||
Each `fabro run` invocation creates a timestamped directory under `~/.fabro/runs/`:
|
||||
Each `fabro run` invocation creates a timestamped directory under `~/.fabro/scratch/`:
|
||||
|
||||
```
|
||||
~/.fabro/runs/20260307-01JQXYZ123ABC456DEF789/
|
||||
~/.fabro/scratch/20260307-01JQXYZ123ABC456DEF789/
|
||||
```
|
||||
|
||||
The naming format is `YYYYMMDD-{run_id}`, where `run_id` is the ULID assigned to the run. You can override the base storage directory with the global `--storage-dir` flag (the runs directory will be `<storage-dir>/runs/`).
|
||||
The naming format is `YYYYMMDD-{run_id}`, where `run_id` is the ULID assigned to the run. You can override the base storage directory with the global `--storage-dir` flag (the scratch directory will be `<storage-dir>/scratch/`).
|
||||
|
||||
## Root-level files
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ Manager nodes that run sub-workflows write a nested `child/` directory containin
|
|||
|
||||
## Browsing runs
|
||||
|
||||
Use `fabro ps` to scan the runs directory and display a table of all runs with their status, workflow name, and timestamps. Pass `--json` for machine-readable output.
|
||||
Use `fabro ps` to scan the scratch directory and display a table of all runs with their status, workflow name, and timestamps. Pass `--json` for machine-readable output.
|
||||
|
||||
```bash
|
||||
fabro ps
|
||||
|
|
@ -89,7 +89,7 @@ fabro ps --filter workflow=my-workflow
|
|||
## Full directory tree
|
||||
|
||||
```
|
||||
~/.fabro/runs/
|
||||
~/.fabro/scratch/
|
||||
├── 20260307-01JQXYZ123ABC456DEF789/ # One directory per run
|
||||
│ ├── run.json
|
||||
│ ├── start.json
|
||||
|
|
@ -112,7 +112,7 @@ fabro ps --filter workflow=my-workflow
|
|||
│ │ ├── values/
|
||||
│ │ │ ├── response.plan.json
|
||||
│ │ │ └── command.output.json
|
||||
│ │ └── assets/
|
||||
│ │ └── files/
|
||||
│ │ └── test/
|
||||
│ │ └── retry_1/
|
||||
│ │ ├── test-results/
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ wildcard_imports = "warn"
|
|||
[dependencies]
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
progenitor-client = "0.13"
|
||||
regress = "0.10"
|
||||
reqwest.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
|
|
|||
|
|
@ -382,9 +382,9 @@ fn restore_empty_run_properties(value: &mut serde_json::Value) {
|
|||
|
||||
#[cfg(test)]
|
||||
fn infer_storage_dir(run_dir: &Path) -> Option<PathBuf> {
|
||||
let runs_dir = run_dir.parent()?;
|
||||
let storage_dir = runs_dir.parent()?;
|
||||
(runs_dir.file_name()? == "runs").then(|| storage_dir.to_path_buf())
|
||||
let scratch_dir = run_dir.parent()?;
|
||||
let storage_dir = scratch_dir.parent()?;
|
||||
(scratch_dir.file_name()? == "scratch").then(|| storage_dir.to_path_buf())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -482,7 +482,7 @@ mod tests {
|
|||
let run_dir = dir
|
||||
.path()
|
||||
.join("storage")
|
||||
.join("runs")
|
||||
.join("scratch")
|
||||
.join("20260401-test");
|
||||
std::fs::create_dir_all(&run_dir).unwrap();
|
||||
|
||||
|
|
@ -496,7 +496,7 @@ mod tests {
|
|||
fn infer_run_id_reads_id_txt() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let storage_dir = dir.path().join("storage");
|
||||
let run_dir = storage_dir.join("runs").join("20260401-test");
|
||||
let run_dir = storage_dir.join("scratch").join("20260401-test");
|
||||
std::fs::create_dir_all(&run_dir).unwrap();
|
||||
std::fs::write(
|
||||
run_dir.join("id.txt"),
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ use std::path::PathBuf;
|
|||
|
||||
use crate::args::RunArgs;
|
||||
use fabro_config::ConfigLayer;
|
||||
use fabro_config::Storage;
|
||||
use fabro_types::{RunId, Settings};
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_workflow::operations::make_run_dir;
|
||||
|
||||
use super::output::{api_diagnostics_to_local, print_preflight_workflow_summary};
|
||||
use crate::manifest_builder::{ManifestBuildInput, build_run_manifest, run_manifest_args};
|
||||
|
|
@ -66,9 +66,12 @@ pub(crate) async fn create_run(
|
|||
|
||||
let created_run_id = client.create_run_from_manifest(built.manifest).await?;
|
||||
let local_run_dir = match &connection {
|
||||
ServerConnection::Local { storage_dir } => {
|
||||
Some(make_run_dir(&storage_dir.join("runs"), &created_run_id))
|
||||
}
|
||||
ServerConnection::Local { storage_dir } => Some(
|
||||
Storage::new(storage_dir)
|
||||
.run_scratch(&created_run_id)
|
||||
.root()
|
||||
.to_path_buf(),
|
||||
),
|
||||
ServerConnection::Target(_) => None,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::time::Duration;
|
|||
|
||||
use anyhow::Result;
|
||||
use fabro_api::types;
|
||||
use fabro_store::RuntimeState;
|
||||
use fabro_config::RunScratch;
|
||||
use fabro_types::PullRequestRecord;
|
||||
use fabro_util::check_report::{CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus};
|
||||
use fabro_util::terminal::Styles;
|
||||
|
|
@ -264,8 +264,8 @@ pub(crate) fn print_final_output(checkpoint: Option<&fabro_types::Checkpoint>, s
|
|||
}
|
||||
|
||||
pub(crate) fn print_assets(run_dir: &Path, styles: &Styles) {
|
||||
let runtime_state = RuntimeState::new(run_dir);
|
||||
let paths = collect_artifact_paths(&runtime_state.artifacts_dir());
|
||||
let run_scratch = RunScratch::new(run_dir);
|
||||
let paths = collect_artifact_paths(&run_scratch.artifact_files_dir());
|
||||
if paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use std::path::{Path, PathBuf};
|
|||
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use fabro_config::Storage;
|
||||
use fabro_server::bind::Bind;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
|
@ -13,18 +14,6 @@ pub(crate) struct ServerRecord {
|
|||
pub started_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub(crate) fn server_record_path(storage_dir: &Path) -> PathBuf {
|
||||
storage_dir.join("server.json")
|
||||
}
|
||||
|
||||
pub(crate) fn server_lock_path(storage_dir: &Path) -> PathBuf {
|
||||
storage_dir.join("server.lock")
|
||||
}
|
||||
|
||||
pub(crate) fn server_log_path(storage_dir: &Path) -> PathBuf {
|
||||
storage_dir.join("server.log")
|
||||
}
|
||||
|
||||
pub(crate) fn write_server_record(path: &Path, record: &ServerRecord) -> Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
|
|
@ -47,7 +36,7 @@ pub(crate) fn server_record_is_running(record: &ServerRecord) -> bool {
|
|||
}
|
||||
|
||||
pub(crate) fn active_server_record(storage_dir: &Path) -> Option<ServerRecord> {
|
||||
let path = server_record_path(storage_dir);
|
||||
let path = Storage::new(storage_dir).server_state().record_path();
|
||||
let record = read_server_record(&path)?;
|
||||
if server_record_is_running(&record) {
|
||||
Some(record)
|
||||
|
|
@ -91,7 +80,7 @@ mod tests {
|
|||
#[test]
|
||||
fn write_and_read_round_trip() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = server_record_path(dir.path());
|
||||
let path = Storage::new(dir.path()).server_state().record_path();
|
||||
let record = test_record(Bind::Tcp("127.0.0.1:3000".parse().unwrap()));
|
||||
write_server_record(&path, &record).unwrap();
|
||||
|
||||
|
|
@ -109,7 +98,7 @@ mod tests {
|
|||
#[test]
|
||||
fn active_server_record_cleans_stale_dead_pid() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = server_record_path(dir.path());
|
||||
let path = Storage::new(dir.path()).server_state().record_path();
|
||||
let mut record = test_record(Bind::Tcp("127.0.0.1:3000".parse().unwrap()));
|
||||
record.pid = u32::MAX; // definitely not alive
|
||||
write_server_record(&path, &record).unwrap();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use std::time::Duration;
|
|||
|
||||
use anyhow::{Result, bail};
|
||||
use chrono::Utc;
|
||||
use fabro_config::Storage;
|
||||
use fabro_server::bind::Bind;
|
||||
use fabro_server::serve;
|
||||
use fabro_server::serve::ServeArgs;
|
||||
|
|
@ -83,13 +84,14 @@ async fn execute_foreground(
|
|||
);
|
||||
}
|
||||
|
||||
let record_path = record::server_record_path(&storage_dir);
|
||||
let server_state = Storage::new(&storage_dir).server_state();
|
||||
let record_path = server_state.record_path();
|
||||
record::write_server_record(
|
||||
&record_path,
|
||||
&record::ServerRecord {
|
||||
pid: std::process::id(),
|
||||
bind: bind.clone(),
|
||||
log_path: record::server_log_path(&storage_dir),
|
||||
log_path: server_state.log_path(),
|
||||
started_at: Utc::now(),
|
||||
},
|
||||
)?;
|
||||
|
|
@ -135,11 +137,12 @@ fn execute_daemon(
|
|||
}
|
||||
|
||||
// Rotate logs
|
||||
let log_path = record::server_log_path(storage_dir);
|
||||
let server_state = Storage::new(storage_dir).server_state();
|
||||
let log_path = server_state.log_path();
|
||||
let prev_path = log_path.with_extension("log.prev");
|
||||
let _ = std::fs::rename(&log_path, &prev_path);
|
||||
|
||||
let record_path = record::server_record_path(storage_dir);
|
||||
let record_path = server_state.record_path();
|
||||
let log_file = std::fs::File::create(&log_path)?;
|
||||
let stdout_log = log_file.try_clone()?;
|
||||
let exe = std::env::current_exe()?;
|
||||
|
|
@ -250,7 +253,7 @@ fn execute_daemon(
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn acquire_lock(storage_dir: &Path) -> Result<std::fs::File> {
|
||||
let lock_path = record::server_lock_path(storage_dir);
|
||||
let lock_path = Storage::new(storage_dir).server_state().lock_path();
|
||||
if let Some(parent) = lock_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use std::path::Path;
|
|||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use fabro_config::Storage;
|
||||
use fabro_server::bind::Bind;
|
||||
|
||||
use super::record;
|
||||
|
|
@ -29,7 +30,7 @@ pub(crate) fn execute(storage_dir: &Path, timeout: Duration) {
|
|||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
let record_path = record::server_record_path(storage_dir);
|
||||
let record_path = Storage::new(storage_dir).server_state().record_path();
|
||||
record::remove_server_record(&record_path);
|
||||
|
||||
if let Bind::Unix(ref path) = record.bind {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
use anyhow::{Context, Result};
|
||||
use fabro_config::Storage;
|
||||
#[cfg(test)]
|
||||
use fabro_store::StageId;
|
||||
use fabro_store::{RunProjection, SlateRunStore};
|
||||
use fabro_store::{ArtifactStore, RunDatabase, RunProjection};
|
||||
use fabro_workflow::run_dump::RunDump;
|
||||
#[cfg(test)]
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::args::{GlobalArgs, StoreDumpArgs};
|
||||
use crate::commands::store::rebuild::rebuild_run_store;
|
||||
use crate::server_runs::ServerRunLookup;
|
||||
use crate::shared::{absolute_or_current, print_json_pretty};
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
use object_store::{ObjectStore, local::LocalFileSystem};
|
||||
|
||||
pub(crate) async fn dump_command(args: &StoreDumpArgs, globals: &GlobalArgs) -> Result<()> {
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
|
|
@ -21,8 +24,12 @@ pub(crate) async fn dump_command(args: &StoreDumpArgs, globals: &GlobalArgs) ->
|
|||
let run_id = run.run_id();
|
||||
let events = lookup.client().list_run_events(&run_id, None, None).await?;
|
||||
let run_store = rebuild_run_store(&run_id, &events).await?;
|
||||
let artifact_object_store: Arc<dyn ObjectStore> = Arc::new(LocalFileSystem::new_with_prefix(
|
||||
Storage::new(cli_settings.storage_dir()).store_dir(),
|
||||
)?);
|
||||
let artifact_store = ArtifactStore::new(artifact_object_store, "artifacts");
|
||||
|
||||
let file_count = export_run(&run_store, &args.output).await?;
|
||||
let file_count = export_run(&run_store, &artifact_store, &args.output).await?;
|
||||
if globals.json {
|
||||
print_json_pretty(&serde_json::json!({
|
||||
"run_id": run_id,
|
||||
|
|
@ -39,7 +46,11 @@ pub(crate) async fn dump_command(args: &StoreDumpArgs, globals: &GlobalArgs) ->
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn export_run(run_store: &SlateRunStore, output_dir: &Path) -> Result<usize> {
|
||||
pub(crate) async fn export_run(
|
||||
run_store: &RunDatabase,
|
||||
artifact_store: &ArtifactStore,
|
||||
output_dir: &Path,
|
||||
) -> Result<usize> {
|
||||
let state = run_store.state().await?;
|
||||
anyhow::ensure!(state.run.is_some(), "run has no data in the store");
|
||||
|
||||
|
|
@ -59,7 +70,7 @@ pub(crate) async fn export_run(run_store: &SlateRunStore, output_dir: &Path) ->
|
|||
})?;
|
||||
let staging_path = staging_dir.path().to_path_buf();
|
||||
|
||||
let file_count = export_run_to_dir(run_store, &state, &staging_path).await?;
|
||||
let file_count = export_run_to_dir(run_store, artifact_store, &state, &staging_path).await?;
|
||||
|
||||
if matches!(output_state, OutputDirState::ExistingEmpty) {
|
||||
std::fs::remove_dir(output_dir)
|
||||
|
|
@ -78,11 +89,12 @@ pub(crate) async fn export_run(run_store: &SlateRunStore, output_dir: &Path) ->
|
|||
}
|
||||
|
||||
async fn export_run_to_dir(
|
||||
run_store: &SlateRunStore,
|
||||
run_store: &RunDatabase,
|
||||
artifact_store: &ArtifactStore,
|
||||
state: &RunProjection,
|
||||
output_dir: &Path,
|
||||
) -> Result<usize> {
|
||||
let dump = RunDump::store_export(run_store, state).await?;
|
||||
let dump = RunDump::store_export(run_store, artifact_store, state).await?;
|
||||
dump.write_to_dir(output_dir)
|
||||
}
|
||||
|
||||
|
|
@ -132,11 +144,10 @@ mod tests {
|
|||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use fabro_store::{EventEnvelope, EventPayload, SlateStore};
|
||||
use fabro_store::{Database, EventEnvelope, EventPayload};
|
||||
use fabro_types::{
|
||||
AggregateStats, AttrValue, Checkpoint, Conclusion, Graph, NodeStatusRecord, Retro, RunId,
|
||||
RunRecord, RunStatus, RunStatusRecord, SandboxRecord, Settings, StageStatus, StartRecord,
|
||||
|
|
@ -155,12 +166,15 @@ mod tests {
|
|||
fixtures::RUN_1
|
||||
}
|
||||
|
||||
fn test_store() -> Arc<SlateStore> {
|
||||
Arc::new(SlateStore::new(
|
||||
Arc::new(InMemory::new()),
|
||||
fn test_store_bundle() -> (Arc<Database>, ArtifactStore) {
|
||||
let object_store: Arc<dyn ObjectStore> = Arc::new(InMemory::new());
|
||||
let store = Arc::new(Database::new(
|
||||
Arc::clone(&object_store),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
))
|
||||
));
|
||||
let artifact_store = ArtifactStore::new(object_store, "artifacts");
|
||||
(store, artifact_store)
|
||||
}
|
||||
|
||||
fn sample_run_record(run_id: RunId, _created_at: DateTime<Utc>) -> RunRecord {
|
||||
|
|
@ -280,7 +294,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn export_run_writes_expected_directory_tree() {
|
||||
let store = test_store();
|
||||
let (store, artifact_store) = test_store_bundle();
|
||||
let created_at = dt("2026-03-27T12:00:00Z");
|
||||
let run_id = test_run_id();
|
||||
let run = store.create_run(&run_id).await.unwrap();
|
||||
|
|
@ -537,17 +551,21 @@ mod tests {
|
|||
.unwrap();
|
||||
let summary_blob = run.write_blob(br#"{"done":true}"#).await.unwrap();
|
||||
let plan_blob = run.write_blob(br#"{"steps":3}"#).await.unwrap();
|
||||
run.put_artifact(&node, "src/lib.rs", b"fn main() {}")
|
||||
artifact_store
|
||||
.put(&run_id, &node, "src/lib.rs", b"fn main() {}")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let artifact_only_node = StageId::new("artifact-only", 7);
|
||||
run.put_artifact(&artifact_only_node, "logs/output.txt", b"hello")
|
||||
artifact_store
|
||||
.put(&run_id, &artifact_only_node, "logs/output.txt", b"hello")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let output = tempfile::tempdir().unwrap();
|
||||
let file_count = export_run(&run, output.path()).await.unwrap();
|
||||
let file_count = export_run(&run, &artifact_store, output.path())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(file_count, 22);
|
||||
|
||||
let exported_run: RunRecord = read_json(&output.path().join("run.json"));
|
||||
|
|
@ -639,45 +657,6 @@ mod tests {
|
|||
assert!(!output.path().join("nodes/artifact-only").exists());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn export_run_rejects_path_traversal_and_leaves_no_partial_output() {
|
||||
let store = test_store();
|
||||
let created_at = dt("2026-03-27T12:00:00Z");
|
||||
let run_id = test_run_id();
|
||||
let run = store.create_run(&run_id).await.unwrap();
|
||||
let run_record = sample_run_record(run_id, created_at);
|
||||
append_event(
|
||||
&run,
|
||||
&run_id,
|
||||
&Event::RunCreated {
|
||||
run_id,
|
||||
settings: serde_json::to_value(&run_record.settings).unwrap(),
|
||||
graph: serde_json::to_value(&run_record.graph).unwrap(),
|
||||
workflow_source: Some("digraph night_sky {}".to_string()),
|
||||
workflow_config: None,
|
||||
labels: run_record.labels.clone().into_iter().collect(),
|
||||
run_dir: "/tmp/night-sky-run".to_string(),
|
||||
working_directory: run_record.working_directory.display().to_string(),
|
||||
host_repo_path: run_record.host_repo_path.clone(),
|
||||
repo_origin_url: run_record.repo_origin_url.clone(),
|
||||
base_branch: run_record.base_branch.clone(),
|
||||
workflow_slug: run_record.workflow_slug.clone(),
|
||||
db_prefix: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
run.put_artifact(&StageId::new("code", 1), "../escape.txt", b"boom")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let output = temp.path().join("dump");
|
||||
let err = export_run(&run, &output).await.unwrap_err();
|
||||
assert!(err.to_string().contains("artifact filename"));
|
||||
assert!(!output.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inspect_output_dir_rejects_non_empty_directory() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ use std::sync::Arc;
|
|||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use fabro_store::{EventEnvelope, EventPayload, SlateRunStore, SlateStore};
|
||||
use fabro_store::{Database, EventEnvelope, EventPayload, RunDatabase};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
pub(crate) async fn rebuild_run_store(
|
||||
run_id: &fabro_types::RunId,
|
||||
events: &[EventEnvelope],
|
||||
) -> Result<SlateRunStore> {
|
||||
let store = Arc::new(SlateStore::new(
|
||||
) -> Result<RunDatabase> {
|
||||
let store = Arc::new(Database::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ use cli_table::format::{Border, Justify, Separator};
|
|||
use cli_table::{Cell, CellStruct, Style, Table};
|
||||
use serde::Serialize;
|
||||
|
||||
use fabro_workflow::run_lookup::{logs_base, runs_base, scan_runs_with_summaries};
|
||||
use fabro_config::Storage;
|
||||
use fabro_workflow::run_lookup::{scan_runs_with_summaries, scratch_base};
|
||||
use fabro_workflow::run_status::RunStatus;
|
||||
|
||||
use crate::args::{DfArgs, GlobalArgs};
|
||||
|
|
@ -47,14 +48,14 @@ struct DfOutput {
|
|||
pub(super) async fn df_command(args: &DfArgs, globals: &GlobalArgs) -> Result<()> {
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let data_dir = cli_settings.storage_dir();
|
||||
let runs_base_dir = runs_base(&data_dir);
|
||||
let logs_base_dir = logs_base(&data_dir);
|
||||
let scratch_base_dir = scratch_base(&data_dir);
|
||||
let logs_base_dir = Storage::new(&data_dir).logs_dir();
|
||||
let lookup = ServerRunLookup::connect(&data_dir).await?;
|
||||
df_from(
|
||||
args,
|
||||
lookup.summaries(),
|
||||
&data_dir,
|
||||
&runs_base_dir,
|
||||
&scratch_base_dir,
|
||||
&logs_base_dir,
|
||||
globals,
|
||||
)
|
||||
|
|
@ -65,7 +66,7 @@ fn df_from(
|
|||
args: &DfArgs,
|
||||
summaries: &[fabro_store::RunSummary],
|
||||
data_dir: &Path,
|
||||
runs_base: &Path,
|
||||
scratch_base: &Path,
|
||||
logs_base: &Path,
|
||||
globals: &GlobalArgs,
|
||||
) -> Result<()> {
|
||||
|
|
@ -78,7 +79,7 @@ fn df_from(
|
|||
size: u64,
|
||||
}
|
||||
|
||||
let runs = scan_runs_with_summaries(summaries, runs_base)?;
|
||||
let runs = scan_runs_with_summaries(summaries, scratch_base)?;
|
||||
let mut active_count = 0u64;
|
||||
let mut total_run_size = 0u64;
|
||||
let mut reclaimable_run_size = 0u64;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ use chrono::Utc;
|
|||
use serde::Serialize;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use fabro_workflow::run_lookup::{StatusFilter, filter_runs, runs_base, scan_runs_with_summaries};
|
||||
use fabro_workflow::run_lookup::{
|
||||
StatusFilter, filter_runs, scan_runs_with_summaries, scratch_base,
|
||||
};
|
||||
|
||||
use crate::args::{GlobalArgs, RunsPruneArgs};
|
||||
use crate::commands::runs::rm::remove_run_with_cleanup;
|
||||
|
|
@ -24,7 +26,7 @@ struct PruneRunRow {
|
|||
|
||||
pub(super) async fn prune_command(args: &RunsPruneArgs, globals: &GlobalArgs) -> Result<()> {
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
let base = scratch_base(&cli_settings.storage_dir());
|
||||
let lookup = ServerRunLookup::connect(&cli_settings.storage_dir()).await?;
|
||||
prune_from(args, lookup.client(), lookup.summaries(), &base, globals).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,29 +6,29 @@ use anyhow::{Result, bail};
|
|||
use chrono::{DateTime, Utc};
|
||||
use fabro_store::RunSummary;
|
||||
use fabro_types::{RunId, RunStatus, StatusReason};
|
||||
use fabro_workflow::run_lookup::{RunInfo, resolve_run_from_summaries, runs_base};
|
||||
use fabro_workflow::run_lookup::{RunInfo, resolve_run_from_summaries, scratch_base};
|
||||
|
||||
use crate::args::ServerTargetArgs;
|
||||
use crate::server_client::{self, ServerStoreClient};
|
||||
|
||||
pub(crate) struct ServerRunLookup {
|
||||
client: ServerStoreClient,
|
||||
runs_base: PathBuf,
|
||||
scratch_base: PathBuf,
|
||||
summaries: Vec<RunSummary>,
|
||||
}
|
||||
|
||||
impl ServerRunLookup {
|
||||
pub(crate) async fn connect(storage_dir: &Path) -> Result<Self> {
|
||||
Self::connect_from_runs_base(&runs_base(storage_dir)).await
|
||||
Self::connect_from_scratch_base(&scratch_base(storage_dir)).await
|
||||
}
|
||||
|
||||
pub(crate) async fn connect_from_runs_base(runs_base: &Path) -> Result<Self> {
|
||||
let storage_dir = runs_base.parent().unwrap_or(runs_base);
|
||||
pub(crate) async fn connect_from_scratch_base(scratch_base: &Path) -> Result<Self> {
|
||||
let storage_dir = scratch_base.parent().unwrap_or(scratch_base);
|
||||
let client = server_client::connect_server(storage_dir).await?;
|
||||
let summaries = client.list_store_runs().await?;
|
||||
Ok(Self {
|
||||
client,
|
||||
runs_base: runs_base.to_path_buf(),
|
||||
scratch_base: scratch_base.to_path_buf(),
|
||||
summaries,
|
||||
})
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ impl ServerRunLookup {
|
|||
}
|
||||
|
||||
pub(crate) fn resolve(&self, selector: &str) -> Result<RunInfo> {
|
||||
resolve_run_from_summaries(&self.summaries, &self.runs_base, selector)
|
||||
resolve_run_from_summaries(&self.summaries, &self.scratch_base, selector)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -426,7 +426,7 @@ fn create_explicit_workflow_path_uses_project_config_relative_to_workflow() {
|
|||
.assert()
|
||||
.success();
|
||||
|
||||
let runs_dir = storage_dir.join("runs");
|
||||
let runs_dir = storage_dir.join("scratch");
|
||||
let run_dir = std::fs::read_dir(&runs_dir)
|
||||
.unwrap()
|
||||
.flatten()
|
||||
|
|
@ -575,6 +575,42 @@ shared = "legacy"
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_uses_fabro_home_for_home_config_resolution() {
|
||||
let context = test_context!();
|
||||
let fabro_home = tempfile::tempdir().unwrap();
|
||||
|
||||
std::fs::write(
|
||||
fabro_home.path().join("settings.toml"),
|
||||
r#"
|
||||
verbose = true
|
||||
|
||||
[llm]
|
||||
model = "from-fabro-home"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let output = context
|
||||
.settings()
|
||||
.arg("--json")
|
||||
.env("FABRO_HOME", fabro_home.path())
|
||||
.env_remove("FABRO_STORAGE_DIR")
|
||||
.output()
|
||||
.expect("command should execute");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"settings command failed:\nstdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
|
||||
let cfg: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
|
||||
assert_eq!(cfg["verbose"].as_bool(), Some(true));
|
||||
assert_eq!(cfg["llm"]["model"].as_str(), Some("from-fabro-home"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_rejects_server_url_flag() {
|
||||
let context = test_context!();
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ fn logs_completed_run_reads_store_without_progress_jsonl() {
|
|||
r#""id":"[EVENT_ID]""#.to_string(),
|
||||
));
|
||||
filters.push((
|
||||
r#""run_dir":"(?:\[DRY_RUN_DIR\]|\[STORAGE_DIR\]/runs/REDACTED)""#.to_string(),
|
||||
r#""run_dir":"(?:\[DRY_RUN_DIR\]|\[STORAGE_DIR\]/scratch/REDACTED)""#.to_string(),
|
||||
r#""run_dir":"[RUN_DIR]""#.to_string(),
|
||||
));
|
||||
|
||||
|
|
@ -159,7 +159,7 @@ fn logs_tail_limits_output() {
|
|||
r#""id":"[EVENT_ID]""#.to_string(),
|
||||
));
|
||||
filters.push((
|
||||
r#""run_dir":"(?:\[DRY_RUN_DIR\]|\[STORAGE_DIR\]/runs/REDACTED)""#.to_string(),
|
||||
r#""run_dir":"(?:\[DRY_RUN_DIR\]|\[STORAGE_DIR\]/scratch/REDACTED)""#.to_string(),
|
||||
r#""run_dir":"[RUN_DIR]""#.to_string(),
|
||||
));
|
||||
let mut cmd = context.command();
|
||||
|
|
|
|||
|
|
@ -528,7 +528,7 @@ pub(crate) fn wait_for_status(run_dir: &Path, expected: &[&str]) -> String {
|
|||
|
||||
pub(crate) fn only_run(context: &TestContext) -> RunSetup {
|
||||
let entries = run_dirs_for_test_case(context);
|
||||
let runs_dir = context.storage_dir.join("runs");
|
||||
let runs_dir = context.storage_dir.join("scratch");
|
||||
assert_eq!(
|
||||
entries.len(),
|
||||
1,
|
||||
|
|
@ -599,7 +599,7 @@ pub(crate) fn resolve_run(context: &TestContext, run_id: &str) -> RunSetup {
|
|||
}
|
||||
|
||||
pub(crate) fn find_run_dir(storage_dir: &Path, run_id: &str) -> Option<PathBuf> {
|
||||
let runs_dir = storage_dir.join("runs");
|
||||
let runs_dir = storage_dir.join("scratch");
|
||||
let entries = std::fs::read_dir(&runs_dir).ok()?;
|
||||
entries
|
||||
.filter_map(Result::ok)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use crate::cmd::support::{read_text, setup_artifact_run, text_tree};
|
|||
fn artifact_filters(context: &fabro_test::TestContext) -> Vec<(String, String)> {
|
||||
let mut filters = context.filters();
|
||||
filters.push((
|
||||
r"\[STORAGE_DIR\]/runs/\d{8}-\[ULID\]".to_string(),
|
||||
r"\[STORAGE_DIR\]/scratch/\d{8}-\[ULID\]".to_string(),
|
||||
"[RUN_DIR]".to_string(),
|
||||
));
|
||||
filters
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ macro_rules! fabro_json_snapshot {
|
|||
r#""duration_ms": "[DURATION_MS]""#.to_string(),
|
||||
));
|
||||
filters.push((
|
||||
r#""run_dir":\s*"\[STORAGE_DIR\]/runs/\d{8}-\[ULID\]""#.to_string(),
|
||||
r#""run_dir":\s*"\[STORAGE_DIR\]/scratch/\d{8}-\[ULID\]""#.to_string(),
|
||||
r#""run_dir": "[RUN_DIR]""#.to_string(),
|
||||
));
|
||||
let filters: Vec<(&str, &str)> = filters
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ pub(super) fn store_dump_export(context: &TestContext, run_id: &str) -> PathBuf
|
|||
|
||||
/// Find the single run directory for this test context.
|
||||
pub(super) fn find_run_dir(context: &TestContext) -> PathBuf {
|
||||
let runs_base = context.storage_dir.join("runs");
|
||||
let runs_base = context.storage_dir.join("scratch");
|
||||
let runs: Vec<RunSummaryRecord> = block_on(get_server_json_for_storage(
|
||||
&context.storage_dir,
|
||||
"/api/v1/runs",
|
||||
|
|
@ -152,7 +152,7 @@ async fn get_server_json_for_storage<T: serde::de::DeserializeOwned>(
|
|||
}
|
||||
|
||||
fn find_run_dir_for_id(storage_dir: &Path, run_id: &str) -> Option<PathBuf> {
|
||||
let runs_dir = storage_dir.join("runs");
|
||||
let runs_dir = storage_dir.join("scratch");
|
||||
let entries = std::fs::read_dir(&runs_dir).ok()?;
|
||||
entries
|
||||
.filter_map(Result::ok)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ workspace = true
|
|||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clap = { workspace = true, optional = true }
|
||||
chrono.workspace = true
|
||||
fabro-types = { path = "../fabro-types" }
|
||||
fabro-util = { path = "../fabro-util" }
|
||||
dirs.workspace = true
|
||||
|
|
|
|||
78
lib/crates/fabro-config/src/home.rs
Normal file
78
lib/crates/fabro-config/src/home.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Home {
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
impl Home {
|
||||
#[must_use]
|
||||
pub fn new(root: impl Into<PathBuf>) -> Self {
|
||||
Self { root: root.into() }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn from_env() -> Self {
|
||||
if let Some(root) = std::env::var_os("FABRO_HOME") {
|
||||
return Self::new(root);
|
||||
}
|
||||
|
||||
let root = dirs::home_dir()
|
||||
.map(|home| home.join(".fabro"))
|
||||
.unwrap_or_else(|| PathBuf::from(".fabro"));
|
||||
Self::new(root)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn root(&self) -> &Path {
|
||||
&self.root
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn user_config(&self) -> PathBuf {
|
||||
self.root.join("settings.toml")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn server_config(&self) -> PathBuf {
|
||||
self.root.join("settings.toml")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn certs_dir(&self) -> PathBuf {
|
||||
self.root.join("certs")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn skills_dir(&self) -> PathBuf {
|
||||
self.root.join("skills")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Home;
|
||||
|
||||
#[test]
|
||||
fn accessors_are_relative_to_root() {
|
||||
let home = Home::new("/tmp/fabro-home");
|
||||
|
||||
assert_eq!(home.root(), std::path::Path::new("/tmp/fabro-home"));
|
||||
assert_eq!(
|
||||
home.user_config(),
|
||||
std::path::Path::new("/tmp/fabro-home/settings.toml")
|
||||
);
|
||||
assert_eq!(
|
||||
home.server_config(),
|
||||
std::path::Path::new("/tmp/fabro-home/settings.toml")
|
||||
);
|
||||
assert_eq!(
|
||||
home.certs_dir(),
|
||||
std::path::Path::new("/tmp/fabro-home/certs")
|
||||
);
|
||||
assert_eq!(
|
||||
home.skills_dir(),
|
||||
std::path::Path::new("/tmp/fabro-home/skills")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ extern crate self as fabro_config;
|
|||
|
||||
pub mod combine;
|
||||
pub mod config;
|
||||
pub mod home;
|
||||
pub mod hook;
|
||||
pub mod legacy_env;
|
||||
pub mod mcp;
|
||||
|
|
@ -10,11 +11,14 @@ pub mod run;
|
|||
pub mod sandbox;
|
||||
pub mod server;
|
||||
pub mod settings;
|
||||
pub mod storage;
|
||||
pub mod user;
|
||||
|
||||
pub use config::ConfigLayer;
|
||||
pub use fabro_types::Combine;
|
||||
pub use fabro_util::path::expand_tilde;
|
||||
pub use home::Home;
|
||||
pub use storage::{RunScratch, ServerState, Storage};
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
|
|
@ -34,11 +38,7 @@ where
|
|||
return Ok(toml::from_str(&contents)?);
|
||||
}
|
||||
|
||||
let Some(home) = dirs::home_dir() else {
|
||||
tracing::debug!("No home directory found, using default config");
|
||||
return Ok(T::default());
|
||||
};
|
||||
let default_path = home.join(".fabro").join(filename);
|
||||
let default_path = Home::from_env().root().join(filename);
|
||||
tracing::debug!(path = %default_path.display(), "Loading config");
|
||||
match std::fs::read_to_string(&default_path) {
|
||||
Ok(contents) => Ok(toml::from_str(&contents)?),
|
||||
|
|
|
|||
291
lib/crates/fabro-config/src/storage.rs
Normal file
291
lib/crates/fabro-config/src/storage.rs
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use chrono::Local;
|
||||
use fabro_types::RunId;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Storage {
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ServerState {
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RunScratch {
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
#[must_use]
|
||||
pub fn new(root: impl Into<PathBuf>) -> Self {
|
||||
Self { root: root.into() }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn root(&self) -> &Path {
|
||||
&self.root
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn logs_dir(&self) -> PathBuf {
|
||||
self.root.join("logs")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn secrets_path(&self) -> PathBuf {
|
||||
self.root.join("secrets.json")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn server_state(&self) -> ServerState {
|
||||
ServerState::new(self.root.clone())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn run_scratch(&self, run_id: &RunId) -> RunScratch {
|
||||
let local_dt = run_id.created_at().with_timezone(&Local);
|
||||
RunScratch::new(
|
||||
self.scratch_dir()
|
||||
.join(format!("{}-{run_id}", local_dt.format("%Y%m%d"))),
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn scratch_dir(&self) -> PathBuf {
|
||||
self.root.join("scratch")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn store_dir(&self) -> PathBuf {
|
||||
self.root.join("store")
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerState {
|
||||
#[must_use]
|
||||
pub fn new(root: impl Into<PathBuf>) -> Self {
|
||||
Self { root: root.into() }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn record_path(&self) -> PathBuf {
|
||||
self.root.join("server.json")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn lock_path(&self) -> PathBuf {
|
||||
self.root.join("server.lock")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn log_path(&self) -> PathBuf {
|
||||
self.root.join("server.log")
|
||||
}
|
||||
}
|
||||
|
||||
impl RunScratch {
|
||||
#[must_use]
|
||||
pub fn new(root: impl Into<PathBuf>) -> Self {
|
||||
Self { root: root.into() }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn root(&self) -> &Path {
|
||||
&self.root
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn worktree_dir(&self) -> PathBuf {
|
||||
self.root.join("worktree")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn runtime_dir(&self) -> PathBuf {
|
||||
self.root.join("runtime")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn artifact_cache_dir(&self) -> PathBuf {
|
||||
self.root.join("cache").join("artifacts")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn blob_cache_dir(&self) -> PathBuf {
|
||||
self.artifact_cache_dir().join("values")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn artifact_files_dir(&self) -> PathBuf {
|
||||
self.artifact_cache_dir().join("files")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn interview_request_path(&self) -> PathBuf {
|
||||
self.runtime_dir().join("interview_request.json")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn interview_response_path(&self) -> PathBuf {
|
||||
self.runtime_dir().join("interview_response.json")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn interview_claim_path(&self) -> PathBuf {
|
||||
self.runtime_dir().join("interview_request.claim")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn artifact_stage_dir(&self, node_slug: &str, attempt: u32) -> PathBuf {
|
||||
self.artifact_files_dir()
|
||||
.join(node_slug)
|
||||
.join(format!("retry_{attempt}"))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn final_patch(&self) -> PathBuf {
|
||||
self.root.join("final.patch")
|
||||
}
|
||||
|
||||
pub fn create(&self) -> std::io::Result<()> {
|
||||
std::fs::create_dir_all(self.worktree_dir())?;
|
||||
std::fs::create_dir_all(self.runtime_dir())?;
|
||||
std::fs::create_dir_all(self.blob_cache_dir())?;
|
||||
std::fs::create_dir_all(self.artifact_files_dir())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove(&self) -> std::io::Result<()> {
|
||||
match std::fs::remove_dir_all(&self.root) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::Local;
|
||||
|
||||
use super::{RunScratch, Storage};
|
||||
use fabro_types::RunId;
|
||||
|
||||
#[test]
|
||||
fn storage_accessors_are_relative_to_root() {
|
||||
let storage = Storage::new("/tmp/fabro-data");
|
||||
|
||||
assert_eq!(storage.root(), std::path::Path::new("/tmp/fabro-data"));
|
||||
assert_eq!(
|
||||
storage.logs_dir(),
|
||||
std::path::Path::new("/tmp/fabro-data/logs")
|
||||
);
|
||||
assert_eq!(
|
||||
storage.secrets_path(),
|
||||
std::path::Path::new("/tmp/fabro-data/secrets.json")
|
||||
);
|
||||
assert_eq!(
|
||||
storage.store_dir(),
|
||||
std::path::Path::new("/tmp/fabro-data/store")
|
||||
);
|
||||
assert_eq!(
|
||||
storage.server_state().record_path(),
|
||||
std::path::Path::new("/tmp/fabro-data/server.json")
|
||||
);
|
||||
assert_eq!(
|
||||
storage.server_state().lock_path(),
|
||||
std::path::Path::new("/tmp/fabro-data/server.lock")
|
||||
);
|
||||
assert_eq!(
|
||||
storage.server_state().log_path(),
|
||||
std::path::Path::new("/tmp/fabro-data/server.log")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_scratch_uses_run_id_local_date() {
|
||||
let storage = Storage::new("/tmp/fabro-data");
|
||||
let run_id: RunId = "01JT56VE4Z5NZ814GZN2JZD65A".parse().unwrap();
|
||||
|
||||
let expected_date = run_id
|
||||
.created_at()
|
||||
.with_timezone(&Local)
|
||||
.format("%Y%m%d")
|
||||
.to_string();
|
||||
|
||||
assert_eq!(
|
||||
storage.run_scratch(&run_id).root(),
|
||||
std::path::Path::new("/tmp/fabro-data/scratch")
|
||||
.join(format!("{expected_date}-{run_id}"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_scratch_accessors_and_lifecycle_are_relative_to_root() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let scratch = RunScratch::new(dir.path().join("20260327-01TEST"));
|
||||
|
||||
assert_eq!(scratch.worktree_dir(), scratch.root().join("worktree"));
|
||||
assert_eq!(scratch.runtime_dir(), scratch.root().join("runtime"));
|
||||
assert_eq!(
|
||||
scratch.artifact_cache_dir(),
|
||||
scratch.root().join("cache").join("artifacts")
|
||||
);
|
||||
assert_eq!(
|
||||
scratch.blob_cache_dir(),
|
||||
scratch
|
||||
.root()
|
||||
.join("cache")
|
||||
.join("artifacts")
|
||||
.join("values")
|
||||
);
|
||||
assert_eq!(
|
||||
scratch.artifact_files_dir(),
|
||||
scratch.root().join("cache").join("artifacts").join("files")
|
||||
);
|
||||
assert_eq!(
|
||||
scratch.interview_request_path(),
|
||||
scratch
|
||||
.root()
|
||||
.join("runtime")
|
||||
.join("interview_request.json")
|
||||
);
|
||||
assert_eq!(
|
||||
scratch.interview_response_path(),
|
||||
scratch
|
||||
.root()
|
||||
.join("runtime")
|
||||
.join("interview_response.json")
|
||||
);
|
||||
assert_eq!(
|
||||
scratch.interview_claim_path(),
|
||||
scratch
|
||||
.root()
|
||||
.join("runtime")
|
||||
.join("interview_request.claim")
|
||||
);
|
||||
assert_eq!(
|
||||
scratch.artifact_stage_dir("plan", 2),
|
||||
scratch
|
||||
.root()
|
||||
.join("cache")
|
||||
.join("artifacts")
|
||||
.join("files")
|
||||
.join("plan")
|
||||
.join("retry_2")
|
||||
);
|
||||
assert_eq!(scratch.final_patch(), scratch.root().join("final.patch"));
|
||||
|
||||
scratch.create().unwrap();
|
||||
assert!(scratch.root().exists());
|
||||
assert!(scratch.worktree_dir().exists());
|
||||
assert!(scratch.runtime_dir().exists());
|
||||
assert!(scratch.blob_cache_dir().exists());
|
||||
assert!(scratch.artifact_files_dir().exists());
|
||||
|
||||
scratch.remove().unwrap();
|
||||
assert!(!scratch.root().exists());
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ use anyhow::anyhow;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::ConfigLayer;
|
||||
use crate::home::Home;
|
||||
|
||||
pub use fabro_types::settings::user::{
|
||||
ClientTlsSettings, ExecSettings, OutputFormat, PermissionLevel, ServerSettings,
|
||||
|
|
@ -80,19 +81,23 @@ impl From<ExecConfig> for ExecSettings {
|
|||
}
|
||||
|
||||
pub fn default_settings_path() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|home| home.join(".fabro").join(SETTINGS_CONFIG_FILENAME))
|
||||
Some(Home::from_env().user_config())
|
||||
}
|
||||
|
||||
pub fn legacy_user_config_path() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|home| home.join(".fabro").join(LEGACY_USER_CONFIG_FILENAME))
|
||||
Some(Home::from_env().root().join(LEGACY_USER_CONFIG_FILENAME))
|
||||
}
|
||||
|
||||
pub fn legacy_old_user_config_path() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|home| home.join(".fabro").join(LEGACY_OLD_USER_CONFIG_FILENAME))
|
||||
Some(
|
||||
Home::from_env()
|
||||
.root()
|
||||
.join(LEGACY_OLD_USER_CONFIG_FILENAME),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn legacy_server_config_path() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|home| home.join(".fabro").join(LEGACY_SERVER_CONFIG_FILENAME))
|
||||
Some(Home::from_env().root().join(LEGACY_SERVER_CONFIG_FILENAME))
|
||||
}
|
||||
|
||||
fn warned_legacy_user_configs() -> &'static Mutex<HashSet<PathBuf>> {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use fabro_agent::{
|
|||
use fabro_llm::client::Client;
|
||||
use fabro_llm::provider::Provider;
|
||||
use fabro_llm::types::ToolDefinition;
|
||||
use fabro_store::SlateRunStore;
|
||||
use fabro_store::RunDatabase;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::retro::{RetroNarrative, SmoothnessRating};
|
||||
|
|
@ -135,7 +135,7 @@ pub fn build_retro_prompt(retro_data_dir: &str) -> String {
|
|||
/// files via tool access, then calls `submit_retro` with its analysis.
|
||||
pub async fn run_retro_agent(
|
||||
sandbox: &Arc<dyn Sandbox>,
|
||||
run_store: &SlateRunStore,
|
||||
run_store: &RunDatabase,
|
||||
run_dir: &Path,
|
||||
llm_client: &Client,
|
||||
provider: Provider,
|
||||
|
|
@ -292,7 +292,7 @@ fn build_profile(provider: Provider, model: &str) -> Box<dyn AgentProfile> {
|
|||
|
||||
async fn upload_data_files(
|
||||
sandbox: &Arc<dyn Sandbox>,
|
||||
run_store: &SlateRunStore,
|
||||
run_store: &RunDatabase,
|
||||
_run_dir: &Path,
|
||||
target_dir: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
|
|
|
|||
|
|
@ -748,7 +748,7 @@ impl Sandbox for DaytonaSandbox {
|
|||
key: &str,
|
||||
) -> String {
|
||||
format!(
|
||||
"{}/.fabro/runs/{}/parallel/{}/{}",
|
||||
"{}/.fabro/scratch/{}/parallel/{}/{}",
|
||||
self.working_directory(),
|
||||
run_id,
|
||||
node_id,
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ use std::path::{Path, PathBuf};
|
|||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use fabro_config::Storage;
|
||||
use fabro_config::server::resolve_storage_dir;
|
||||
use fabro_config::user::{default_settings_path, load_settings_config};
|
||||
use fabro_util::terminal::Styles;
|
||||
use object_store::ObjectStore;
|
||||
use object_store::local::LocalFileSystem;
|
||||
use tokio::net::{TcpListener, UnixListener};
|
||||
use tokio::time::interval;
|
||||
|
|
@ -107,7 +109,8 @@ pub async fn serve_command(
|
|||
let disk_settings = load_settings(config_path.as_deref())?;
|
||||
let active_config_path = resolved_config_path(config_path.as_deref());
|
||||
let data_dir = storage_dir_override.unwrap_or_else(|| resolve_storage_dir(&disk_settings));
|
||||
let secret_store_path = data_dir.join("secrets.json");
|
||||
let storage = Storage::new(&data_dir);
|
||||
let secret_store_path = storage.secrets_path();
|
||||
let secret_store = SecretStore::load(secret_store_path.clone())?;
|
||||
let secret_snapshot = secret_store.snapshot();
|
||||
|
||||
|
|
@ -167,19 +170,22 @@ pub async fn serve_command(
|
|||
(auth_mode, client_auth, max_concurrent_runs)
|
||||
};
|
||||
|
||||
let store_path = data_dir.join("store");
|
||||
let store_path = storage.store_dir();
|
||||
std::fs::create_dir_all(&store_path)?;
|
||||
let object_store = Arc::new(LocalFileSystem::new_with_prefix(&store_path)?);
|
||||
let store = Arc::new(fabro_store::SlateStore::new(
|
||||
object_store,
|
||||
let object_store: Arc<dyn ObjectStore> =
|
||||
Arc::new(LocalFileSystem::new_with_prefix(&store_path)?);
|
||||
let store = Arc::new(fabro_store::Database::new(
|
||||
Arc::clone(&object_store),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
));
|
||||
let artifact_store = fabro_store::ArtifactStore::new(object_store, "artifacts");
|
||||
let state = build_app_state_with_path(
|
||||
Arc::clone(&shared_settings),
|
||||
None,
|
||||
max_concurrent_runs,
|
||||
store,
|
||||
artifact_store,
|
||||
secret_store_path,
|
||||
active_config_path,
|
||||
matches!(&auth_mode, AuthMode::Disabled),
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ use axum_extra::extract::cookie::Key;
|
|||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use bytes::Bytes;
|
||||
use fabro_config::Storage;
|
||||
use fabro_llm::client::Client as LlmClient;
|
||||
use fabro_llm::generate::{GenerateParams, generate_object};
|
||||
use fabro_llm::model_test::{ModelTestMode, run_model_test_with_client};
|
||||
|
|
@ -25,7 +26,7 @@ use fabro_llm::types::{
|
|||
ContentPart, FinishReason, Message as LlmMessage, Request as LlmRequest,
|
||||
Response as LlmResponse, Role, StreamEvent, ToolChoice, ToolDefinition, Usage,
|
||||
};
|
||||
use fabro_store::{EventEnvelope, EventPayload, StageId, StoreHandle};
|
||||
use fabro_store::{ArtifactStore, Database, EventEnvelope, EventPayload, StageId};
|
||||
use fabro_types::{RunBlobId, RunEvent, RunId, Settings};
|
||||
use fabro_util::redact::redact_jsonl_line;
|
||||
use fabro_util::version::FABRO_VERSION;
|
||||
|
|
@ -223,7 +224,8 @@ type RegistryFactoryOverride = dyn Fn(Arc<dyn Interviewer>) -> HandlerRegistry +
|
|||
pub struct AppState {
|
||||
runs: Mutex<HashMap<RunId, ManagedRun>>,
|
||||
aggregate_usage: Mutex<UsageAccumulator>,
|
||||
store: StoreHandle,
|
||||
store: Arc<Database>,
|
||||
artifact_store: ArtifactStore,
|
||||
max_concurrent_runs: usize,
|
||||
scheduler_notify: Notify,
|
||||
pub sessions: SessionStore,
|
||||
|
|
@ -880,11 +882,13 @@ pub fn create_app_state_with_settings_and_registry_factory(
|
|||
settings: Settings,
|
||||
registry_factory_override: impl Fn(Arc<dyn Interviewer>) -> HandlerRegistry + Send + Sync + 'static,
|
||||
) -> Arc<AppState> {
|
||||
let (store, artifact_store) = test_store_bundle();
|
||||
build_app_state_with_path(
|
||||
Arc::new(RwLock::new(settings)),
|
||||
Some(Box::new(registry_factory_override)),
|
||||
5,
|
||||
test_store(),
|
||||
store,
|
||||
artifact_store,
|
||||
test_secret_store_path(),
|
||||
test_config_path(),
|
||||
false,
|
||||
|
|
@ -897,31 +901,38 @@ pub fn create_app_state_with_options(
|
|||
settings: Settings,
|
||||
max_concurrent_runs: usize,
|
||||
) -> Arc<AppState> {
|
||||
let (store, artifact_store) = test_store_bundle();
|
||||
create_app_state_with_store(
|
||||
Arc::new(RwLock::new(settings)),
|
||||
max_concurrent_runs,
|
||||
test_store(),
|
||||
store,
|
||||
artifact_store,
|
||||
)
|
||||
}
|
||||
|
||||
fn test_store() -> StoreHandle {
|
||||
Arc::new(fabro_store::SlateStore::new(
|
||||
Arc::new(MemoryObjectStore::new()),
|
||||
fn test_store_bundle() -> (Arc<Database>, ArtifactStore) {
|
||||
let object_store: Arc<dyn object_store::ObjectStore> = Arc::new(MemoryObjectStore::new());
|
||||
let store = Arc::new(fabro_store::Database::new(
|
||||
Arc::clone(&object_store),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
))
|
||||
));
|
||||
let artifact_store = ArtifactStore::new(object_store, "artifacts");
|
||||
(store, artifact_store)
|
||||
}
|
||||
|
||||
pub fn create_app_state_with_store(
|
||||
settings: Arc<RwLock<Settings>>,
|
||||
max_concurrent_runs: usize,
|
||||
store: StoreHandle,
|
||||
store: Arc<Database>,
|
||||
artifact_store: ArtifactStore,
|
||||
) -> Arc<AppState> {
|
||||
build_app_state_with_path(
|
||||
settings,
|
||||
None,
|
||||
max_concurrent_runs,
|
||||
store,
|
||||
artifact_store,
|
||||
test_secret_store_path(),
|
||||
test_config_path(),
|
||||
false,
|
||||
|
|
@ -933,7 +944,8 @@ pub(crate) fn build_app_state_with_path(
|
|||
settings: Arc<RwLock<Settings>>,
|
||||
registry_factory_override: Option<Box<RegistryFactoryOverride>>,
|
||||
max_concurrent_runs: usize,
|
||||
store: StoreHandle,
|
||||
store: Arc<Database>,
|
||||
artifact_store: ArtifactStore,
|
||||
secret_store_path: PathBuf,
|
||||
config_path: PathBuf,
|
||||
local_daemon_mode: bool,
|
||||
|
|
@ -943,6 +955,7 @@ pub(crate) fn build_app_state_with_path(
|
|||
runs: Mutex::new(HashMap::new()),
|
||||
aggregate_usage: Mutex::new(UsageAccumulator::default()),
|
||||
store,
|
||||
artifact_store,
|
||||
max_concurrent_runs,
|
||||
scheduler_notify: Notify::new(),
|
||||
sessions: new_session_store(),
|
||||
|
|
@ -1042,8 +1055,8 @@ async fn delete_run(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
let storage_dir = state.settings.read().unwrap().storage_dir();
|
||||
let run_dir = operations::make_run_dir(&storage_dir.join("runs"), &id);
|
||||
let storage = Storage::new(state.settings.read().unwrap().storage_dir());
|
||||
let run_dir = storage.run_scratch(&id).root().to_path_buf();
|
||||
if let Err(err) = remove_run_dir(&run_dir) {
|
||||
return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
|
||||
.into_response();
|
||||
|
|
@ -1051,7 +1064,12 @@ async fn delete_run(
|
|||
}
|
||||
|
||||
match state.store.delete_run(&id).await {
|
||||
Ok(()) => StatusCode::NO_CONTENT.into_response(),
|
||||
Ok(()) => match state.artifact_store.delete_for_run(&id).await {
|
||||
Ok(()) => StatusCode::NO_CONTENT.into_response(),
|
||||
Err(err) => {
|
||||
ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response()
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response()
|
||||
}
|
||||
|
|
@ -1128,8 +1146,9 @@ fn validate_relative_artifact_path(kind: &str, value: &str) -> Result<PathBuf, R
|
|||
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn run_artifacts_dir(run: &fabro_types::RunRecord, run_id: &RunId) -> PathBuf {
|
||||
operations::make_run_dir(&run.settings.storage_dir().join("runs"), run_id)
|
||||
.join("cache/artifacts/files")
|
||||
Storage::new(run.settings.storage_dir())
|
||||
.run_scratch(run_id)
|
||||
.artifact_files_dir()
|
||||
}
|
||||
|
||||
#[allow(clippy::result_large_err)]
|
||||
|
|
@ -1416,7 +1435,10 @@ async fn start_run(
|
|||
)
|
||||
.into_response();
|
||||
};
|
||||
let run_dir = operations::make_run_dir(&run_record.settings.storage_dir().join("runs"), &id);
|
||||
let run_dir = Storage::new(run_record.settings.storage_dir())
|
||||
.run_scratch(&id)
|
||||
.root()
|
||||
.to_path_buf();
|
||||
let dot_source = run_state.graph_source.unwrap_or_default();
|
||||
|
||||
{
|
||||
|
|
@ -2172,7 +2194,7 @@ async fn list_stage_artifacts(
|
|||
)
|
||||
.into_response();
|
||||
};
|
||||
match run_store.list_artifacts_for_stage(&stage_id).await {
|
||||
match state.artifact_store.list_for_node(&id, &stage_id).await {
|
||||
Ok(filenames) if !filenames.is_empty() => Json(ArtifactListResponse {
|
||||
data: filenames
|
||||
.into_iter()
|
||||
|
|
@ -2228,8 +2250,12 @@ async fn put_stage_artifact(
|
|||
Ok(filename) => filename,
|
||||
Err(response) => return response,
|
||||
};
|
||||
match state.store.open_run(&id).await {
|
||||
Ok(run_store) => match run_store.put_artifact(&stage_id, &filename, &body).await {
|
||||
match state.store.open_run_reader(&id).await {
|
||||
Ok(_) => match state
|
||||
.artifact_store
|
||||
.put(&id, &stage_id, &filename, &body)
|
||||
.await
|
||||
{
|
||||
Ok(()) => StatusCode::NO_CONTENT.into_response(),
|
||||
Err(err) => {
|
||||
ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response()
|
||||
|
|
@ -2267,7 +2293,7 @@ async fn get_stage_artifact(
|
|||
)
|
||||
.into_response();
|
||||
};
|
||||
match run_store.get_artifact(&stage_id, &filename).await {
|
||||
match state.artifact_store.get(&id, &stage_id, &filename).await {
|
||||
Ok(Some(bytes)) => octet_stream_response(bytes),
|
||||
Ok(None) => {
|
||||
let relative_path =
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ workspace = true
|
|||
fabro-types = { path = "../fabro-types" }
|
||||
slatedb.workspace = true
|
||||
object_store.workspace = true
|
||||
percent-encoding.workspace = true
|
||||
async-trait.workspace = true
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tokio-stream.workspace = true
|
||||
|
|
|
|||
313
lib/crates/fabro-store/src/artifact_store.rs
Normal file
313
lib/crates/fabro-store/src/artifact_store.rs
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures::StreamExt;
|
||||
use object_store::{ObjectStore, path::Path as ObjectPath};
|
||||
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, percent_decode_str, utf8_percent_encode};
|
||||
|
||||
use crate::{Result, StageId, StoreError};
|
||||
use fabro_types::RunId;
|
||||
|
||||
const ARTIFACT_SEGMENT_ENCODE_SET: &AsciiSet =
|
||||
&NON_ALPHANUMERIC.remove(b'.').remove(b'_').remove(b'-');
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct NodeArtifact {
|
||||
pub node: StageId,
|
||||
pub filename: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ArtifactStore {
|
||||
object_store: Arc<dyn ObjectStore>,
|
||||
prefix: ObjectPath,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ArtifactStore {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ArtifactStore")
|
||||
.field("prefix", &self.prefix)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl ArtifactStore {
|
||||
#[must_use]
|
||||
pub fn new(object_store: Arc<dyn ObjectStore>, prefix: impl AsRef<str>) -> Self {
|
||||
Self {
|
||||
object_store,
|
||||
prefix: ObjectPath::from(prefix.as_ref()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn put(
|
||||
&self,
|
||||
run_id: &RunId,
|
||||
node: &StageId,
|
||||
filename: &str,
|
||||
data: &[u8],
|
||||
) -> Result<()> {
|
||||
let path = self.artifact_path(run_id, node, filename)?;
|
||||
self.object_store
|
||||
.put(&path, Bytes::copy_from_slice(data).into())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get(
|
||||
&self,
|
||||
run_id: &RunId,
|
||||
node: &StageId,
|
||||
filename: &str,
|
||||
) -> Result<Option<Bytes>> {
|
||||
let path = self.artifact_path(run_id, node, filename)?;
|
||||
match self.object_store.get(&path).await {
|
||||
Ok(result) => Ok(Some(result.bytes().await?)),
|
||||
Err(object_store::Error::NotFound { .. }) => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_for_run(&self, run_id: &RunId) -> Result<Vec<NodeArtifact>> {
|
||||
let prefix = self.run_prefix(run_id)?;
|
||||
let mut stream = self.object_store.list(Some(&prefix));
|
||||
let mut artifacts = Vec::new();
|
||||
while let Some(meta) = stream.next().await.transpose()? {
|
||||
artifacts.push(decode_artifact_location(&prefix, &meta.location)?);
|
||||
}
|
||||
artifacts.sort();
|
||||
Ok(artifacts)
|
||||
}
|
||||
|
||||
pub async fn list_for_node(&self, run_id: &RunId, node: &StageId) -> Result<Vec<String>> {
|
||||
let prefix = self.node_prefix(run_id, node)?;
|
||||
let mut stream = self.object_store.list(Some(&prefix));
|
||||
let mut filenames = Vec::new();
|
||||
while let Some(meta) = stream.next().await.transpose()? {
|
||||
filenames.push(decode_filename(&prefix, &meta.location)?);
|
||||
}
|
||||
filenames.sort();
|
||||
Ok(filenames)
|
||||
}
|
||||
|
||||
pub async fn delete_for_run(&self, run_id: &RunId) -> Result<()> {
|
||||
let prefix = self.run_prefix(run_id)?;
|
||||
let mut stream = self.object_store.list(Some(&prefix));
|
||||
let mut locations = Vec::new();
|
||||
while let Some(meta) = stream.next().await.transpose()? {
|
||||
locations.push(meta.location);
|
||||
}
|
||||
for location in locations {
|
||||
self.object_store.delete(&location).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_prefix(&self, run_id: &RunId) -> Result<ObjectPath> {
|
||||
parse_object_path(self.prefixed_raw(&run_id.to_string()))
|
||||
}
|
||||
|
||||
fn node_prefix(&self, run_id: &RunId, node: &StageId) -> Result<ObjectPath> {
|
||||
let encoded_node = encode_path_segment(node.node_id());
|
||||
parse_object_path(
|
||||
self.prefixed_raw(&format!("{run_id}/{encoded_node}@{:04}", node.visit())),
|
||||
)
|
||||
}
|
||||
|
||||
fn artifact_path(&self, run_id: &RunId, node: &StageId, filename: &str) -> Result<ObjectPath> {
|
||||
let mut raw = self.node_prefix(run_id, node)?.to_string();
|
||||
for segment in validate_filename_segments(filename)? {
|
||||
raw.push('/');
|
||||
raw.push_str(&encode_path_segment(segment));
|
||||
}
|
||||
parse_object_path(raw)
|
||||
}
|
||||
|
||||
fn prefixed_raw(&self, suffix: &str) -> String {
|
||||
if self.prefix.as_ref().is_empty() {
|
||||
suffix.to_string()
|
||||
} else {
|
||||
format!("{}/{suffix}", self.prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_filename_segments(filename: &str) -> Result<Vec<&str>> {
|
||||
if filename.contains('\\') {
|
||||
return Err(StoreError::Other(
|
||||
"artifact filename must not contain backslashes".to_string(),
|
||||
));
|
||||
}
|
||||
let segments = filename.split('/').collect::<Vec<_>>();
|
||||
if segments.is_empty() || segments.iter().any(|segment| segment.is_empty()) {
|
||||
return Err(StoreError::Other(
|
||||
"artifact filename must be a non-empty relative path".to_string(),
|
||||
));
|
||||
}
|
||||
if segments
|
||||
.iter()
|
||||
.any(|segment| matches!(*segment, "." | ".."))
|
||||
{
|
||||
return Err(StoreError::Other(
|
||||
"artifact filename must not contain '.' or '..' segments".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(segments)
|
||||
}
|
||||
|
||||
fn encode_path_segment(segment: &str) -> String {
|
||||
utf8_percent_encode(segment, ARTIFACT_SEGMENT_ENCODE_SET).to_string()
|
||||
}
|
||||
|
||||
fn decode_path_segment(kind: &str, value: &str) -> Result<String> {
|
||||
percent_decode_str(value)
|
||||
.decode_utf8()
|
||||
.map(|decoded| decoded.into_owned())
|
||||
.map_err(|err| StoreError::Other(format!("invalid {kind}: {err}")))
|
||||
}
|
||||
|
||||
fn decode_artifact_location(prefix: &ObjectPath, location: &ObjectPath) -> Result<NodeArtifact> {
|
||||
let mut parts = location.prefix_match(prefix).ok_or_else(|| {
|
||||
StoreError::Other(format!(
|
||||
"artifact location {location} does not match expected prefix {prefix}"
|
||||
))
|
||||
})?;
|
||||
let stage_part = parts.next().ok_or_else(|| {
|
||||
StoreError::Other(format!(
|
||||
"artifact location {location} is missing a stage segment"
|
||||
))
|
||||
})?;
|
||||
let (encoded_node_id, visit) = stage_part.as_ref().rsplit_once('@').ok_or_else(|| {
|
||||
StoreError::Other(format!(
|
||||
"artifact location {location} has an invalid stage segment"
|
||||
))
|
||||
})?;
|
||||
let node_id = decode_path_segment("artifact node id", encoded_node_id)?;
|
||||
let visit = visit.parse::<u32>().map_err(|err| {
|
||||
StoreError::Other(format!(
|
||||
"artifact location {location} has an invalid visit number: {err}"
|
||||
))
|
||||
})?;
|
||||
let filename_segments = parts
|
||||
.map(|part| decode_path_segment("artifact filename segment", part.as_ref()))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
if filename_segments.is_empty() {
|
||||
return Err(StoreError::Other(format!(
|
||||
"artifact location {location} is missing a filename"
|
||||
)));
|
||||
}
|
||||
Ok(NodeArtifact {
|
||||
node: StageId::new(node_id, visit),
|
||||
filename: filename_segments.join("/"),
|
||||
})
|
||||
}
|
||||
|
||||
fn decode_filename(prefix: &ObjectPath, location: &ObjectPath) -> Result<String> {
|
||||
let mut parts = location.prefix_match(prefix).ok_or_else(|| {
|
||||
StoreError::Other(format!(
|
||||
"artifact location {location} does not match expected prefix {prefix}"
|
||||
))
|
||||
})?;
|
||||
let filename_segments = parts
|
||||
.by_ref()
|
||||
.map(|part| decode_path_segment("artifact filename segment", part.as_ref()))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
if filename_segments.is_empty() {
|
||||
return Err(StoreError::Other(format!(
|
||||
"artifact location {location} is missing a filename"
|
||||
)));
|
||||
}
|
||||
Ok(filename_segments.join("/"))
|
||||
}
|
||||
|
||||
fn parse_object_path(raw: String) -> Result<ObjectPath> {
|
||||
ObjectPath::parse(&raw)
|
||||
.map_err(|err| StoreError::Other(format!("invalid artifact object path {raw:?}: {err}")))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
use fabro_types::fixtures;
|
||||
|
||||
fn test_store() -> ArtifactStore {
|
||||
let object_store: Arc<dyn ObjectStore> = Arc::new(InMemory::new());
|
||||
ArtifactStore::new(object_store, "artifacts")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn round_trips_unicode_nodes_and_nested_filenames() {
|
||||
let store = test_store();
|
||||
let run_id = fixtures::RUN_1;
|
||||
let node = StageId::new("build/naive @ alpha/π", 12);
|
||||
let filename = "logs/unicode/naive file ☃.txt";
|
||||
|
||||
store.put(&run_id, &node, filename, b"hello").await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
store.get(&run_id, &node, filename).await.unwrap(),
|
||||
Some(Bytes::from_static(b"hello"))
|
||||
);
|
||||
assert_eq!(
|
||||
store.list_for_node(&run_id, &node).await.unwrap(),
|
||||
vec![filename.to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
store.list_for_run(&run_id).await.unwrap(),
|
||||
vec![NodeArtifact {
|
||||
node,
|
||||
filename: filename.to_string(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_invalid_relative_filenames() {
|
||||
let store = test_store();
|
||||
let run_id = fixtures::RUN_1;
|
||||
let node = StageId::new("build", 1);
|
||||
|
||||
for filename in [
|
||||
"",
|
||||
"../escape.txt",
|
||||
"logs//output.txt",
|
||||
"logs/./output.txt",
|
||||
r"logs\output.txt",
|
||||
] {
|
||||
let err = store
|
||||
.put(&run_id, &node, filename, b"boom")
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("artifact filename"));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_for_run_only_removes_selected_run() {
|
||||
let store = test_store();
|
||||
let run_id = fixtures::RUN_1;
|
||||
let other_run_id = fixtures::RUN_2;
|
||||
let node = StageId::new("build", 1);
|
||||
|
||||
store.put(&run_id, &node, "a.txt", b"a").await.unwrap();
|
||||
store
|
||||
.put(&run_id, &node, "nested/b.txt", b"b")
|
||||
.await
|
||||
.unwrap();
|
||||
store
|
||||
.put(&other_run_id, &node, "keep.txt", b"keep")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
store.delete_for_run(&run_id).await.unwrap();
|
||||
|
||||
assert!(store.list_for_run(&run_id).await.unwrap().is_empty());
|
||||
assert_eq!(
|
||||
store.list_for_node(&other_run_id, &node).await.unwrap(),
|
||||
vec!["keep.txt".to_string()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,177 +1,123 @@
|
|||
use crate::StageId;
|
||||
use fabro_types::{RunBlobId, RunId};
|
||||
|
||||
pub(crate) const RUNS_PREFIX: &str = "runs/";
|
||||
pub(crate) const CATALOG_BY_ID_PREFIX: &str = "_catalog/by-id/";
|
||||
pub(crate) const CATALOG_BY_START_PREFIX: &str = "_catalog/by-start/";
|
||||
pub(crate) const INIT_KEY: &str = "_init.json";
|
||||
pub(crate) const EVENTS_PREFIX: &str = "events#";
|
||||
pub(crate) const BLOBS_PREFIX: &str = "blobs#";
|
||||
pub(crate) const ARTIFACT_NODES_PREFIX: &str = "artifacts#nodes#";
|
||||
const RUNS_PREFIX: &str = "runs#";
|
||||
const RUNS_INDEX_BY_START_PREFIX: &str = "runs#_index#by-start#";
|
||||
const BLOBS_PREFIX: &str = "blobs#";
|
||||
|
||||
pub(crate) fn run_prefix(run_id: &RunId) -> String {
|
||||
format!("{RUNS_PREFIX}{run_id}/")
|
||||
pub(crate) fn runs_index_by_start_prefix() -> &'static str {
|
||||
RUNS_INDEX_BY_START_PREFIX
|
||||
}
|
||||
|
||||
pub(crate) fn init_key(run_id: &RunId) -> String {
|
||||
format!("{}{INIT_KEY}", run_prefix(run_id))
|
||||
pub(crate) fn runs_index_by_start_key(run_id: &RunId) -> String {
|
||||
format!(
|
||||
"{RUNS_INDEX_BY_START_PREFIX}{}#{run_id}",
|
||||
run_id.created_at().format("%Y-%m-%d")
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn events_prefix(run_id: &RunId) -> String {
|
||||
format!("{}{EVENTS_PREFIX}", run_prefix(run_id))
|
||||
pub(crate) fn run_data_prefix(run_id: &RunId) -> String {
|
||||
format!("{RUNS_PREFIX}{run_id}#")
|
||||
}
|
||||
|
||||
pub(crate) fn event_key(run_id: &RunId, seq: u32, epoch_ms: i64) -> String {
|
||||
format!("{}{seq:06}-{epoch_ms}.json", events_prefix(run_id))
|
||||
pub(crate) fn run_events_prefix(run_id: &RunId) -> String {
|
||||
format!("{}events#", run_data_prefix(run_id))
|
||||
}
|
||||
|
||||
pub(crate) fn run_event_key(run_id: &RunId, seq: u32, epoch_ms: i64) -> String {
|
||||
format!("{}{seq:06}-{epoch_ms}", run_events_prefix(run_id))
|
||||
}
|
||||
|
||||
pub(crate) fn blobs_prefix(run_id: &RunId) -> String {
|
||||
format!("{}{BLOBS_PREFIX}", run_prefix(run_id))
|
||||
format!("{BLOBS_PREFIX}{run_id}#")
|
||||
}
|
||||
|
||||
pub(crate) fn blob_key(run_id: &RunId, id: &RunBlobId) -> String {
|
||||
format!("{}{id}", blobs_prefix(run_id))
|
||||
}
|
||||
|
||||
pub(crate) fn node_artifact_prefix(run_id: &RunId, node: &StageId) -> String {
|
||||
format!(
|
||||
"{}{ARTIFACT_NODES_PREFIX}{}#visit-{}",
|
||||
run_prefix(run_id),
|
||||
node.node_id(),
|
||||
node.visit()
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn node_artifact(run_id: &RunId, node: &StageId, filename: &str) -> String {
|
||||
format!("{}#{filename}", node_artifact_prefix(run_id, node))
|
||||
}
|
||||
|
||||
pub(crate) fn catalog_by_id_key(run_id: &RunId) -> String {
|
||||
format!("{CATALOG_BY_ID_PREFIX}{run_id}.json")
|
||||
}
|
||||
|
||||
pub(crate) fn catalog_by_start_prefix() -> &'static str {
|
||||
CATALOG_BY_START_PREFIX
|
||||
}
|
||||
|
||||
pub(crate) fn catalog_by_start_key(run_id: &RunId) -> String {
|
||||
format!(
|
||||
"{CATALOG_BY_START_PREFIX}{}/{run_id}.json",
|
||||
run_id.created_at().format("%Y-%m-%d-%H-%M")
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_event_seq(key: &str) -> Option<u32> {
|
||||
parse_seq(key.rsplit('/').next()?, EVENTS_PREFIX)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_blob_id(key: &str) -> Option<RunBlobId> {
|
||||
key.rsplit('/')
|
||||
.next()?
|
||||
.strip_prefix(BLOBS_PREFIX)?
|
||||
key.rsplit_once("#events#")?
|
||||
.1
|
||||
.split_once('-')?
|
||||
.0
|
||||
.parse()
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub(crate) fn parse_node_artifact_key(key: &str) -> Option<(StageId, String)> {
|
||||
let artifact_start = key.find(ARTIFACT_NODES_PREFIX)?;
|
||||
parse_visit_scoped_key(&key[artifact_start..], ARTIFACT_NODES_PREFIX)
|
||||
pub(crate) fn parse_blob_id(key: &str) -> Option<RunBlobId> {
|
||||
let rest = key.strip_prefix(BLOBS_PREFIX)?;
|
||||
let (_, blob_id) = rest.split_once('#')?;
|
||||
blob_id.parse().ok()
|
||||
}
|
||||
|
||||
pub(crate) fn parse_run_id_from_catalog_key(key: &str) -> Option<RunId> {
|
||||
let filename = key.rsplit('/').next()?;
|
||||
let run_id = filename.strip_suffix(".json").unwrap_or(filename);
|
||||
pub(crate) fn parse_run_id_from_index_key(key: &str) -> Option<RunId> {
|
||||
let rest = key.strip_prefix(RUNS_INDEX_BY_START_PREFIX)?;
|
||||
let (_, run_id) = rest.split_once('#')?;
|
||||
run_id.parse().ok()
|
||||
}
|
||||
|
||||
fn parse_seq(key: &str, prefix: &str) -> Option<u32> {
|
||||
key.strip_prefix(prefix)?.split_once('-')?.0.parse().ok()
|
||||
}
|
||||
|
||||
fn parse_visit_scoped_key(key: &str, prefix: &str) -> Option<(StageId, String)> {
|
||||
let rest = key.strip_prefix(prefix)?;
|
||||
let (node_id, rest) = rest.split_once("#visit-")?;
|
||||
let (visit, file) = rest.split_once('#')?;
|
||||
Some((StageId::new(node_id, visit.parse().ok()?), file.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fabro_types::RunId;
|
||||
|
||||
#[test]
|
||||
fn top_level_keys_match_spec() {
|
||||
let run_id = "01JT56VE4Z5NZ814GZN2JZD65A".parse().unwrap();
|
||||
assert_eq!(INIT_KEY, "_init.json");
|
||||
let run_id: RunId = "01JT56VE4Z5NZ814GZN2JZD65A".parse().unwrap();
|
||||
assert_eq!(
|
||||
event_key(&run_id, 7, 123),
|
||||
"runs/01JT56VE4Z5NZ814GZN2JZD65A/events#000007-123.json"
|
||||
run_event_key(&run_id, 7, 123),
|
||||
"runs#01JT56VE4Z5NZ814GZN2JZD65A#events#000007-123"
|
||||
);
|
||||
assert_eq!(
|
||||
runs_index_by_start_key(&run_id),
|
||||
format!(
|
||||
"runs#_index#by-start#{}#{run_id}",
|
||||
run_id.created_at().format("%Y-%m-%d")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sequence_keys_are_zero_padded() {
|
||||
let run_id = "01JT56VE4Z5NZ814GZN2JZD65A".parse().unwrap();
|
||||
let run_id: RunId = "01JT56VE4Z5NZ814GZN2JZD65A".parse().unwrap();
|
||||
assert_eq!(
|
||||
event_key(&run_id, 7, 123),
|
||||
"runs/01JT56VE4Z5NZ814GZN2JZD65A/events#000007-123.json"
|
||||
run_event_key(&run_id, 7, 123),
|
||||
"runs#01JT56VE4Z5NZ814GZN2JZD65A#events#000007-123"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artifact_keys_match_spec() {
|
||||
let node = StageId::new("code", 2);
|
||||
let run_id = "01JT56VE4Z5NZ814GZN2JZD65A".parse().unwrap();
|
||||
let blob_id = RunBlobId::new(&run_id, b"summary");
|
||||
fn blob_keys_match_spec() {
|
||||
let run_id: RunId = "01JT56VE4Z5NZ814GZN2JZD65A".parse().unwrap();
|
||||
let blob_id = RunBlobId::new(b"summary");
|
||||
assert_eq!(
|
||||
blob_key(&run_id, &blob_id),
|
||||
format!("runs/{run_id}/blobs#{blob_id}")
|
||||
);
|
||||
assert_eq!(
|
||||
node_artifact(&run_id, &node, "src/main.rs"),
|
||||
"runs/01JT56VE4Z5NZ814GZN2JZD65A/artifacts#nodes#code#visit-2#src/main.rs"
|
||||
format!("blobs#{run_id}#{blob_id}")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_helpers_extract_sequences_and_node_visits() {
|
||||
fn parse_helpers_extract_sequences_and_blob_ids() {
|
||||
assert_eq!(
|
||||
parse_event_seq("runs/01JT56VE4Z5NZ814GZN2JZD65A/events#000007-123.json"),
|
||||
parse_event_seq("runs#01JT56VE4Z5NZ814GZN2JZD65A#events#000007-123"),
|
||||
Some(7)
|
||||
);
|
||||
let blob_id = RunBlobId::new(&"01JT56VE4Z5NZ814GZN2JZD65A".parse().unwrap(), b"summary");
|
||||
let blob_id = RunBlobId::new(b"summary");
|
||||
assert_eq!(
|
||||
parse_blob_id(&format!("runs/01JT56VE4Z5NZ814GZN2JZD65A/blobs#{blob_id}")),
|
||||
parse_blob_id(&format!("blobs#01JT56VE4Z5NZ814GZN2JZD65A#{blob_id}")),
|
||||
Some(blob_id)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_node_artifact_key(
|
||||
"runs/01JT56VE4Z5NZ814GZN2JZD65A/artifacts#nodes#code#visit-2#src/main.rs"
|
||||
parse_run_id_from_index_key(
|
||||
"runs#_index#by-start#2026-03-27#01JT56VE4Z5NZ814GZN2JZD65A"
|
||||
),
|
||||
Some((StageId::new("code", 2), "src/main.rs".to_string()))
|
||||
Some("01JT56VE4Z5NZ814GZN2JZD65A".parse().unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_helpers_reject_invalid_keys() {
|
||||
assert_eq!(parse_event_seq("events#not-a-seq.json"), None);
|
||||
assert_eq!(parse_event_seq("runs#not-a-run#events#not-a-seq"), None);
|
||||
assert_eq!(parse_blob_id("blobs#not-a-uuid"), None);
|
||||
assert_eq!(
|
||||
parse_node_artifact_key("artifacts#nodes#code#status.json"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn asset_filename_with_slashes_parses_correctly() {
|
||||
assert_eq!(
|
||||
parse_node_artifact_key(
|
||||
"runs/01JT56VE4Z5NZ814GZN2JZD65A/artifacts#nodes#build#visit-1#deep/nested/path/file.rs"
|
||||
),
|
||||
Some((
|
||||
StageId::new("build", 1),
|
||||
"deep/nested/path/file.rs".to_string()
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,19 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
mod artifact_store;
|
||||
mod error;
|
||||
mod keys;
|
||||
mod run_state;
|
||||
mod runtime;
|
||||
mod slate;
|
||||
mod types;
|
||||
|
||||
pub use artifact_store::{ArtifactStore, NodeArtifact};
|
||||
pub use error::{Result, StoreError};
|
||||
pub use fabro_types::{RunBlobId, StageId};
|
||||
pub use run_state::{NodeState, RunProjection};
|
||||
pub use runtime::RuntimeState;
|
||||
pub use slate::{NodeArtifact, SlateRunStore, SlateStore};
|
||||
pub use slate::{Database, RunDatabase, Runs};
|
||||
pub use types::{EventEnvelope, EventPayload, RunSummary};
|
||||
|
||||
pub type StoreHandle = Arc<SlateStore>;
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct ListRunsQuery {
|
||||
pub start: Option<DateTime<Utc>>,
|
||||
|
|
|
|||
|
|
@ -1,112 +0,0 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RuntimeState {
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
impl RuntimeState {
|
||||
#[must_use]
|
||||
pub fn new(run_dir: impl AsRef<Path>) -> Self {
|
||||
Self {
|
||||
root: run_dir.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn runtime_dir(&self) -> PathBuf {
|
||||
self.root.join("runtime")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn interview_request_path(&self) -> PathBuf {
|
||||
self.runtime_dir().join("interview_request.json")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn interview_response_path(&self) -> PathBuf {
|
||||
self.runtime_dir().join("interview_response.json")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn interview_claim_path(&self) -> PathBuf {
|
||||
self.runtime_dir().join("interview_request.claim")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn blob_cache_dir(&self) -> PathBuf {
|
||||
self.root.join("cache").join("artifacts").join("values")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn artifact_values_dir(&self) -> PathBuf {
|
||||
self.blob_cache_dir()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn artifact_value_path(&self, artifact_id: &str) -> PathBuf {
|
||||
self.blob_cache_dir().join(format!("{artifact_id}.json"))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn artifacts_dir(&self) -> PathBuf {
|
||||
self.root.join("cache").join("artifacts").join("files")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn artifact_stage_dir(&self, node_slug: &str, attempt: u32) -> PathBuf {
|
||||
self.artifacts_dir()
|
||||
.join(node_slug)
|
||||
.join(format!("retry_{attempt}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::RuntimeState;
|
||||
|
||||
#[test]
|
||||
fn computes_runtime_and_cache_paths() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let state = RuntimeState::new(dir.path());
|
||||
|
||||
assert_eq!(state.runtime_dir(), dir.path().join("runtime"));
|
||||
assert_eq!(
|
||||
state.interview_request_path(),
|
||||
dir.path().join("runtime").join("interview_request.json")
|
||||
);
|
||||
assert_eq!(
|
||||
state.interview_response_path(),
|
||||
dir.path().join("runtime").join("interview_response.json")
|
||||
);
|
||||
assert_eq!(
|
||||
state.interview_claim_path(),
|
||||
dir.path().join("runtime").join("interview_request.claim")
|
||||
);
|
||||
assert_eq!(
|
||||
state.blob_cache_dir(),
|
||||
dir.path().join("cache").join("artifacts").join("values")
|
||||
);
|
||||
assert_eq!(
|
||||
state.artifact_value_path("response.plan"),
|
||||
dir.path()
|
||||
.join("cache")
|
||||
.join("artifacts")
|
||||
.join("values")
|
||||
.join("response.plan.json")
|
||||
);
|
||||
assert_eq!(
|
||||
state.artifacts_dir(),
|
||||
dir.path().join("cache").join("artifacts").join("files")
|
||||
);
|
||||
assert_eq!(
|
||||
state.artifact_stage_dir("plan", 2),
|
||||
dir.path()
|
||||
.join("cache")
|
||||
.join("artifacts")
|
||||
.join("files")
|
||||
.join("plan")
|
||||
.join("retry_2")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,30 +5,24 @@ use crate::keys;
|
|||
use crate::{ListRunsQuery, Result};
|
||||
use fabro_types::RunId;
|
||||
|
||||
pub(crate) async fn write_catalog(db: &Db, run_id: &RunId) -> Result<()> {
|
||||
db.put(keys::catalog_by_id_key(run_id), []).await?;
|
||||
db.put(keys::catalog_by_start_key(run_id), []).await?;
|
||||
pub(crate) async fn write_index(db: &Db, run_id: &RunId) -> Result<()> {
|
||||
db.put(keys::runs_index_by_start_key(run_id), []).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn read_locator(db: &Db, run_id: &RunId) -> Result<bool> {
|
||||
Ok(db.get(keys::catalog_by_id_key(run_id)).await?.is_some())
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_catalog(db: &Db, run_id: &RunId) -> Result<()> {
|
||||
db.delete(keys::catalog_by_id_key(run_id)).await?;
|
||||
db.delete(keys::catalog_by_start_key(run_id)).await?;
|
||||
pub(crate) async fn delete_index(db: &Db, run_id: &RunId) -> Result<()> {
|
||||
db.delete(keys::runs_index_by_start_key(run_id)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn list_run_ids(db: &Db, query: &ListRunsQuery) -> Result<Vec<RunId>> {
|
||||
let mut iter = db.scan_prefix(keys::catalog_by_start_prefix()).await?;
|
||||
let mut iter = db.scan_prefix(keys::runs_index_by_start_prefix()).await?;
|
||||
let mut run_ids = Vec::new();
|
||||
while let Some(entry) = iter.next().await? {
|
||||
let key = String::from_utf8(entry.key.to_vec()).map_err(|err| {
|
||||
crate::StoreError::Other(format!("stored key is not valid UTF-8: {err}"))
|
||||
})?;
|
||||
let Some(run_id) = keys::parse_run_id_from_catalog_key(&key) else {
|
||||
let Some(run_id) = keys::parse_run_id_from_index_key(&key) else {
|
||||
continue;
|
||||
};
|
||||
let created_at = run_id.created_at();
|
||||
|
|
|
|||
|
|
@ -12,28 +12,28 @@ use tokio::sync::{Mutex, OnceCell};
|
|||
use crate::keys;
|
||||
use crate::{ListRunsQuery, Result, RunSummary, StoreError};
|
||||
use fabro_types::RunId;
|
||||
use run_store::SlateRunStoreInner;
|
||||
pub use run_store::{NodeArtifact, SlateRunStore};
|
||||
pub use run_store::RunDatabase;
|
||||
use run_store::RunDatabaseInner;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SlateStore {
|
||||
pub struct Database {
|
||||
object_store: Arc<dyn ObjectStore>,
|
||||
base_prefix: String,
|
||||
flush_interval: Duration,
|
||||
db: Arc<OnceCell<slatedb::Db>>,
|
||||
active_runs: Arc<Mutex<HashMap<RunId, Arc<SlateRunStoreInner>>>>,
|
||||
active_runs: Arc<Mutex<HashMap<RunId, Arc<RunDatabaseInner>>>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SlateStore {
|
||||
impl std::fmt::Debug for Database {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SlateStore")
|
||||
f.debug_struct("Database")
|
||||
.field("base_prefix", &self.base_prefix)
|
||||
.field("flush_interval", &self.flush_interval)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl SlateStore {
|
||||
impl Database {
|
||||
pub fn new(
|
||||
object_store: Arc<dyn ObjectStore>,
|
||||
base_prefix: impl Into<String>,
|
||||
|
|
@ -68,55 +68,52 @@ impl SlateStore {
|
|||
Ok(db.clone())
|
||||
}
|
||||
|
||||
async fn get_active_run(&self, run_id: &RunId) -> Option<SlateRunStore> {
|
||||
async fn get_active_run(&self, run_id: &RunId) -> Option<RunDatabase> {
|
||||
let active_runs = self.active_runs.lock().await;
|
||||
active_runs
|
||||
.get(run_id)
|
||||
.cloned()
|
||||
.map(SlateRunStore::from_inner)
|
||||
.map(RunDatabase::from_inner)
|
||||
}
|
||||
|
||||
async fn cache_active_run(&self, run_store: &SlateRunStore) {
|
||||
async fn cache_active_run(&self, run_store: &RunDatabase) {
|
||||
self.active_runs
|
||||
.lock()
|
||||
.await
|
||||
.insert(run_store.run_id(), run_store.inner_arc());
|
||||
}
|
||||
|
||||
async fn remove_active_run(&self, run_id: &RunId) -> Option<SlateRunStore> {
|
||||
async fn remove_active_run(&self, run_id: &RunId) -> Option<RunDatabase> {
|
||||
self.active_runs
|
||||
.lock()
|
||||
.await
|
||||
.remove(run_id)
|
||||
.map(SlateRunStore::from_inner)
|
||||
.map(RunDatabase::from_inner)
|
||||
}
|
||||
|
||||
pub async fn create_run(&self, run_id: &RunId) -> Result<SlateRunStore> {
|
||||
pub async fn create_run(&self, run_id: &RunId) -> Result<RunDatabase> {
|
||||
let db = self.open_db().await?;
|
||||
let locator_exists = catalog::read_locator(&db, run_id).await?;
|
||||
let run_exists = RunDatabase::has_any_events(&db, run_id).await?;
|
||||
|
||||
if let Some(active) = self.get_active_run(run_id).await {
|
||||
if locator_exists && !active.matches_run(run_id) {
|
||||
if run_exists && !active.matches_run(run_id) {
|
||||
return Err(StoreError::RunAlreadyExists(run_id.to_string()));
|
||||
}
|
||||
catalog::write_catalog(&db, run_id).await?;
|
||||
catalog::write_index(&db, run_id).await?;
|
||||
return Ok(active);
|
||||
}
|
||||
|
||||
if locator_exists {
|
||||
if run_exists {
|
||||
return Err(StoreError::RunAlreadyExists(run_id.to_string()));
|
||||
}
|
||||
|
||||
SlateRunStore::validate_init(&db, run_id).await?;
|
||||
db.put(keys::init_key(run_id), serde_json::to_vec(run_id)?)
|
||||
.await?;
|
||||
catalog::write_catalog(&db, run_id).await?;
|
||||
let run_store = SlateRunStore::open_writer(*run_id, db).await?;
|
||||
catalog::write_index(&db, run_id).await?;
|
||||
let run_store = RunDatabase::open_writer(*run_id, db).await?;
|
||||
self.cache_active_run(&run_store).await;
|
||||
Ok(run_store)
|
||||
}
|
||||
|
||||
pub async fn open_run(&self, run_id: &RunId) -> Result<SlateRunStore> {
|
||||
pub async fn open_run(&self, run_id: &RunId) -> Result<RunDatabase> {
|
||||
let db = self.open_db().await?;
|
||||
if let Some(active) = self.get_active_run(run_id).await {
|
||||
if !active.matches_run(run_id) {
|
||||
|
|
@ -126,18 +123,15 @@ impl SlateStore {
|
|||
}
|
||||
return Ok(active);
|
||||
}
|
||||
if !catalog::read_locator(&db, run_id).await? {
|
||||
if !RunDatabase::has_any_events(&db, run_id).await? {
|
||||
return Err(StoreError::RunNotFound(run_id.to_string()));
|
||||
}
|
||||
if !SlateRunStore::validate_init(&db, run_id).await? {
|
||||
return Err(StoreError::RunNotFound(run_id.to_string()));
|
||||
}
|
||||
let run_store = SlateRunStore::open_writer(*run_id, db).await?;
|
||||
let run_store = RunDatabase::open_writer(*run_id, db).await?;
|
||||
self.cache_active_run(&run_store).await;
|
||||
Ok(run_store)
|
||||
}
|
||||
|
||||
pub async fn open_run_reader(&self, run_id: &RunId) -> Result<SlateRunStore> {
|
||||
pub async fn open_run_reader(&self, run_id: &RunId) -> Result<RunDatabase> {
|
||||
let db = self.open_db().await?;
|
||||
if let Some(active) = self.get_active_run(run_id).await {
|
||||
if !active.matches_run(run_id) {
|
||||
|
|
@ -147,13 +141,10 @@ impl SlateStore {
|
|||
}
|
||||
return Ok(active.read_only_clone());
|
||||
}
|
||||
if !catalog::read_locator(&db, run_id).await? {
|
||||
if !RunDatabase::has_any_events(&db, run_id).await? {
|
||||
return Err(StoreError::RunNotFound(run_id.to_string()));
|
||||
}
|
||||
if !SlateRunStore::validate_init(&db, run_id).await? {
|
||||
return Err(StoreError::RunNotFound(run_id.to_string()));
|
||||
}
|
||||
SlateRunStore::open_reader(*run_id, db).await
|
||||
RunDatabase::open_reader(*run_id, db).await
|
||||
}
|
||||
|
||||
pub async fn list_runs(&self, query: &ListRunsQuery) -> Result<Vec<RunSummary>> {
|
||||
|
|
@ -165,10 +156,10 @@ impl SlateStore {
|
|||
summaries.push(active.state().await?.build_summary(&run_id));
|
||||
continue;
|
||||
}
|
||||
if !SlateRunStore::validate_init(&db, &run_id).await? {
|
||||
if !RunDatabase::has_any_events(&db, &run_id).await? {
|
||||
continue;
|
||||
}
|
||||
summaries.push(SlateRunStore::build_summary(&db, &run_id).await?);
|
||||
summaries.push(RunDatabase::build_summary(&db, &run_id).await?);
|
||||
}
|
||||
summaries.sort_by(|a, b| b.run_id.created_at().cmp(&a.run_id.created_at()));
|
||||
Ok(summaries)
|
||||
|
|
@ -181,20 +172,49 @@ impl SlateStore {
|
|||
}
|
||||
|
||||
let db = self.open_db().await?;
|
||||
let prefix = keys::run_prefix(run_id);
|
||||
let mut iter = db.scan_prefix(prefix.as_bytes()).await?;
|
||||
let mut keys_to_delete = Vec::new();
|
||||
while let Some(entry) = iter.next().await? {
|
||||
keys_to_delete.push(String::from_utf8(entry.key.to_vec()).map_err(|err| {
|
||||
StoreError::Other(format!("stored key is not valid UTF-8: {err}"))
|
||||
})?);
|
||||
for prefix in [keys::run_data_prefix(run_id), keys::blobs_prefix(run_id)] {
|
||||
let mut iter = db.scan_prefix(prefix.as_bytes()).await?;
|
||||
while let Some(entry) = iter.next().await? {
|
||||
keys_to_delete.push(String::from_utf8(entry.key.to_vec()).map_err(|err| {
|
||||
StoreError::Other(format!("stored key is not valid UTF-8: {err}"))
|
||||
})?);
|
||||
}
|
||||
}
|
||||
for key in keys_to_delete {
|
||||
db.delete(key).await?;
|
||||
}
|
||||
catalog::delete_catalog(&db, run_id).await?;
|
||||
catalog::delete_index(&db, run_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn runs(&self) -> Runs {
|
||||
Runs { db: self.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Runs {
|
||||
db: Database,
|
||||
}
|
||||
|
||||
impl Runs {
|
||||
pub async fn get(&self, run_id: &RunId) -> Result<RunDatabase> {
|
||||
self.db.open_run(run_id).await
|
||||
}
|
||||
|
||||
pub async fn find(&self, run_id: &RunId) -> Result<Option<RunSummary>> {
|
||||
match self.db.open_run_reader(run_id).await {
|
||||
Ok(run_db) => Ok(Some(run_db.state().await?.build_summary(run_id))),
|
||||
Err(StoreError::RunNotFound(_)) => Ok(None),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list(&self, query: &ListRunsQuery) -> Result<Vec<RunSummary>> {
|
||||
self.db.list_runs(query).await
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_base_prefix(prefix: String) -> String {
|
||||
|
|
@ -244,9 +264,9 @@ mod tests {
|
|||
RunId::from(ulid::Ulid::from_parts(timestamp_ms, random))
|
||||
}
|
||||
|
||||
fn make_store() -> (Arc<dyn ObjectStore>, SlateStore) {
|
||||
fn make_store() -> (Arc<dyn ObjectStore>, Database) {
|
||||
let object_store: Arc<dyn ObjectStore> = Arc::new(InMemory::new());
|
||||
let store = SlateStore::new(object_store.clone(), "runs/", Duration::from_millis(1));
|
||||
let store = Database::new(object_store.clone(), "runs/", Duration::from_millis(1));
|
||||
(object_store, store)
|
||||
}
|
||||
|
||||
|
|
@ -288,7 +308,7 @@ mod tests {
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
async fn append_created(run: &SlateRunStore, label: &str, created_at: DateTime<Utc>) {
|
||||
async fn append_created(run: &RunDatabase, label: &str, created_at: DateTime<Utc>) {
|
||||
let run_record = sample_run_record(label);
|
||||
run.append_event(&event_payload(
|
||||
label,
|
||||
|
|
@ -309,7 +329,7 @@ mod tests {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
async fn append_completed(run: &SlateRunStore, label: &str, created_at: DateTime<Utc>) {
|
||||
async fn append_completed(run: &RunDatabase, label: &str, created_at: DateTime<Utc>) {
|
||||
append_created(run, label, created_at).await;
|
||||
run.append_event(&event_payload(
|
||||
label,
|
||||
|
|
@ -422,7 +442,7 @@ mod tests {
|
|||
let run = store.create_run(&test_run_id("run-1")).await.unwrap();
|
||||
append_completed(&run, "run-1", dt("2026-03-27T12:00:00Z")).await;
|
||||
|
||||
let reopened = SlateStore::new(object_store, "runs", Duration::from_millis(1));
|
||||
let reopened = Database::new(object_store, "runs", Duration::from_millis(1));
|
||||
let summary = reopened.list_runs(&ListRunsQuery::default()).await.unwrap();
|
||||
assert_eq!(summary.len(), 1);
|
||||
assert_eq!(summary[0].run_id, test_run_id("run-1"));
|
||||
|
|
|
|||
|
|
@ -5,40 +5,33 @@ use std::sync::atomic::{AtomicU32, Ordering};
|
|||
use bytes::Bytes;
|
||||
use chrono::Utc;
|
||||
use futures::Stream;
|
||||
use serde::de::DeserializeOwned;
|
||||
use slatedb::{Db, DbRead};
|
||||
use tokio::sync::{Mutex, broadcast, mpsc};
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
use crate::keys;
|
||||
use crate::run_state::EventProjectionCache;
|
||||
use crate::{EventEnvelope, EventPayload, Result, RunProjection, RunSummary, StageId, StoreError};
|
||||
use crate::{EventEnvelope, EventPayload, Result, RunProjection, RunSummary, StoreError};
|
||||
use fabro_types::{RunBlobId, RunId};
|
||||
|
||||
const DEFAULT_EVENT_TAIL_LIMIT: usize = 1024;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct NodeArtifact {
|
||||
pub node: StageId,
|
||||
pub filename: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SlateRunStore {
|
||||
inner: Arc<SlateRunStoreInner>,
|
||||
pub struct RunDatabase {
|
||||
inner: Arc<RunDatabaseInner>,
|
||||
read_only: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SlateRunStore {
|
||||
impl std::fmt::Debug for RunDatabase {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SlateRunStore")
|
||||
f.debug_struct("RunDatabase")
|
||||
.field("run_id", &self.inner.run_id)
|
||||
.field("read_only", &self.read_only)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SlateRunStoreInner {
|
||||
pub(crate) struct RunDatabaseInner {
|
||||
run_id: RunId,
|
||||
db: Db,
|
||||
event_seq: AtomicU32,
|
||||
|
|
@ -50,13 +43,17 @@ pub(crate) struct SlateRunStoreInner {
|
|||
event_tx: broadcast::Sender<EventEnvelope>,
|
||||
}
|
||||
|
||||
impl SlateRunStore {
|
||||
impl RunDatabase {
|
||||
pub(crate) async fn open_writer(run_id: RunId, db: Db) -> Result<Self> {
|
||||
let event_seq =
|
||||
recover_next_seq(&db, &keys::events_prefix(&run_id), keys::parse_event_seq).await?;
|
||||
let event_seq = recover_next_seq(
|
||||
&db,
|
||||
&keys::run_events_prefix(&run_id),
|
||||
keys::parse_event_seq,
|
||||
)
|
||||
.await?;
|
||||
let (event_tx, _) = broadcast::channel(DEFAULT_EVENT_TAIL_LIMIT.max(16));
|
||||
Ok(Self {
|
||||
inner: Arc::new(SlateRunStoreInner {
|
||||
inner: Arc::new(RunDatabaseInner {
|
||||
run_id,
|
||||
db,
|
||||
event_seq: AtomicU32::new(event_seq),
|
||||
|
|
@ -72,11 +69,15 @@ impl SlateRunStore {
|
|||
}
|
||||
|
||||
pub(crate) async fn open_reader(run_id: RunId, db: Db) -> Result<Self> {
|
||||
let event_seq =
|
||||
recover_next_seq(&db, &keys::events_prefix(&run_id), keys::parse_event_seq).await?;
|
||||
let event_seq = recover_next_seq(
|
||||
&db,
|
||||
&keys::run_events_prefix(&run_id),
|
||||
keys::parse_event_seq,
|
||||
)
|
||||
.await?;
|
||||
let (event_tx, _) = broadcast::channel(DEFAULT_EVENT_TAIL_LIMIT.max(16));
|
||||
Ok(Self {
|
||||
inner: Arc::new(SlateRunStoreInner {
|
||||
inner: Arc::new(RunDatabaseInner {
|
||||
run_id,
|
||||
db,
|
||||
event_seq: AtomicU32::new(event_seq),
|
||||
|
|
@ -91,7 +92,7 @@ impl SlateRunStore {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn from_inner(inner: Arc<SlateRunStoreInner>) -> Self {
|
||||
pub(crate) fn from_inner(inner: Arc<RunDatabaseInner>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
read_only: false,
|
||||
|
|
@ -105,7 +106,7 @@ impl SlateRunStore {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn inner_arc(&self) -> Arc<SlateRunStoreInner> {
|
||||
pub(crate) fn inner_arc(&self) -> Arc<RunDatabaseInner> {
|
||||
Arc::clone(&self.inner)
|
||||
}
|
||||
|
||||
|
|
@ -122,17 +123,14 @@ impl SlateRunStore {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn validate_init<R>(db: &R, run_id: &RunId) -> Result<bool>
|
||||
pub(crate) async fn has_any_events<R>(db: &R, run_id: &RunId) -> Result<bool>
|
||||
where
|
||||
R: DbRead + Sync,
|
||||
{
|
||||
match get_json::<R, RunId>(db, &keys::init_key(run_id)).await? {
|
||||
Some(existing) if existing == *run_id => Ok(true),
|
||||
Some(existing) => Err(StoreError::Other(format!(
|
||||
"existing init record {existing:?} does not match requested run_id {run_id:?}"
|
||||
))),
|
||||
None => Ok(false),
|
||||
}
|
||||
let mut iter = db
|
||||
.scan_prefix(keys::run_events_prefix(run_id).as_bytes())
|
||||
.await?;
|
||||
Ok(iter.next().await?.is_some())
|
||||
}
|
||||
|
||||
pub(crate) async fn build_summary<R>(db: &R, run_id: &RunId) -> Result<RunSummary>
|
||||
|
|
@ -193,7 +191,7 @@ impl SlateRunStore {
|
|||
}
|
||||
}
|
||||
|
||||
impl SlateRunStore {
|
||||
impl RunDatabase {
|
||||
pub async fn append_event(&self, payload: &EventPayload) -> Result<u32> {
|
||||
if self.read_only {
|
||||
return Err(StoreError::ReadOnly);
|
||||
|
|
@ -208,7 +206,7 @@ impl SlateRunStore {
|
|||
self.inner
|
||||
.db
|
||||
.put(
|
||||
keys::event_key(&self.inner.run_id, seq, Utc::now().timestamp_millis()),
|
||||
keys::run_event_key(&self.inner.run_id, seq, Utc::now().timestamp_millis()),
|
||||
serde_json::to_vec(payload)?,
|
||||
)
|
||||
.await?;
|
||||
|
|
@ -272,7 +270,7 @@ impl SlateRunStore {
|
|||
if self.read_only {
|
||||
return Err(StoreError::ReadOnly);
|
||||
}
|
||||
let id = RunBlobId::new(&self.inner.run_id, data);
|
||||
let id = RunBlobId::new(data);
|
||||
self.inner
|
||||
.db
|
||||
.put(keys::blob_key(&self.inner.run_id, &id), data)
|
||||
|
|
@ -292,53 +290,11 @@ impl SlateRunStore {
|
|||
list_blobs(&self.inner.db, &self.inner.run_id).await
|
||||
}
|
||||
|
||||
pub async fn put_artifact(&self, node: &StageId, filename: &str, data: &[u8]) -> Result<()> {
|
||||
if self.read_only {
|
||||
return Err(StoreError::ReadOnly);
|
||||
}
|
||||
self.inner
|
||||
.db
|
||||
.put(
|
||||
keys::node_artifact(&self.inner.run_id, node, filename),
|
||||
data,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_artifact(&self, node: &StageId, filename: &str) -> Result<Option<Bytes>> {
|
||||
Ok(self
|
||||
.inner
|
||||
.db
|
||||
.get(keys::node_artifact(&self.inner.run_id, node, filename))
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn list_all_artifacts(&self) -> Result<Vec<NodeArtifact>> {
|
||||
list_all_artifacts(&self.inner.db, &self.inner.run_id).await
|
||||
}
|
||||
|
||||
pub async fn list_artifacts_for_stage(&self, stage_id: &StageId) -> Result<Vec<String>> {
|
||||
list_artifacts_for_stage(&self.inner.db, &self.inner.run_id, stage_id).await
|
||||
}
|
||||
|
||||
pub async fn state(&self) -> Result<RunProjection> {
|
||||
self.projected_state().await
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_json<R, T>(db: &R, key: &str) -> Result<Option<T>>
|
||||
where
|
||||
R: DbRead + Sync,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
db.get(key)
|
||||
.await?
|
||||
.map(|value| serde_json::from_slice(&value))
|
||||
.transpose()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn recover_next_seq<R>(db: &R, prefix: &str, parse: fn(&str) -> Option<u32>) -> Result<u32>
|
||||
where
|
||||
R: DbRead + Sync,
|
||||
|
|
@ -359,7 +315,7 @@ where
|
|||
R: DbRead + Sync,
|
||||
{
|
||||
let mut iter = db
|
||||
.scan_prefix(keys::events_prefix(run_id).as_bytes())
|
||||
.scan_prefix(keys::run_events_prefix(run_id).as_bytes())
|
||||
.await?;
|
||||
let mut events = Vec::new();
|
||||
while let Some(entry) = iter.next().await? {
|
||||
|
|
@ -412,47 +368,6 @@ where
|
|||
Ok(blob_ids)
|
||||
}
|
||||
|
||||
async fn list_all_artifacts<R>(db: &R, run_id: &RunId) -> Result<Vec<NodeArtifact>>
|
||||
where
|
||||
R: DbRead + Sync,
|
||||
{
|
||||
let mut iter = db.scan_prefix(keys::run_prefix(run_id).as_bytes()).await?;
|
||||
let mut assets = Vec::new();
|
||||
while let Some(entry) = iter.next().await? {
|
||||
let key = key_to_string(&entry.key)?;
|
||||
let Some((node, filename)) = keys::parse_node_artifact_key(&key) else {
|
||||
continue;
|
||||
};
|
||||
assets.push(NodeArtifact { node, filename });
|
||||
}
|
||||
assets.sort();
|
||||
Ok(assets)
|
||||
}
|
||||
|
||||
async fn list_artifacts_for_stage<R>(
|
||||
db: &R,
|
||||
run_id: &RunId,
|
||||
stage_id: &StageId,
|
||||
) -> Result<Vec<String>>
|
||||
where
|
||||
R: DbRead + Sync,
|
||||
{
|
||||
let prefix = keys::node_artifact_prefix(run_id, stage_id);
|
||||
let mut iter = db.scan_prefix(prefix.as_bytes()).await?;
|
||||
let mut filenames = Vec::new();
|
||||
while let Some(entry) = iter.next().await? {
|
||||
let key = key_to_string(&entry.key)?;
|
||||
let Some((node, filename)) = keys::parse_node_artifact_key(&key) else {
|
||||
continue;
|
||||
};
|
||||
if &node == stage_id {
|
||||
filenames.push(filename);
|
||||
}
|
||||
}
|
||||
filenames.sort();
|
||||
Ok(filenames)
|
||||
}
|
||||
|
||||
fn key_to_string(key: &Bytes) -> Result<String> {
|
||||
String::from_utf8(key.to_vec())
|
||||
.map_err(|err| StoreError::Other(format!("stored key is not valid UTF-8: {err}")))
|
||||
|
|
|
|||
|
|
@ -29,10 +29,10 @@ static INSTA_FILTERS: &[(&str, &str)] = &[
|
|||
(r"\b[0-9A-HJKMNP-TV-Z]{26}\b", "[ULID]"),
|
||||
(r"in \d+(\.\d+)?(ms|s)", "in [TIME]"),
|
||||
(
|
||||
r"\[STORAGE_DIR\]/runs/\d{8}-dry-run-\[ULID\]",
|
||||
r"\[STORAGE_DIR\]/scratch/\d{8}-dry-run-\[ULID\]",
|
||||
"[DRY_RUN_DIR]",
|
||||
),
|
||||
(r"\[STORAGE_DIR\]/runs/\d{8}-\[ULID\]", "[RUN_DIR]"),
|
||||
(r"\[STORAGE_DIR\]/scratch/\d{8}-\[ULID\]", "[RUN_DIR]"),
|
||||
(
|
||||
r"Duration:\s+\d+\s+(seconds?|minutes?|hours?)",
|
||||
"Duration: [DURATION]",
|
||||
|
|
@ -706,9 +706,9 @@ impl TestContext {
|
|||
|
||||
/// Find a run directory whose name ends with `run_id_suffix`.
|
||||
pub fn find_run_dir(&self, run_id_suffix: &str) -> PathBuf {
|
||||
let runs_dir = self.storage_dir.join("runs");
|
||||
std::fs::read_dir(&runs_dir)
|
||||
.expect("runs directory should exist")
|
||||
let scratch_dir = self.storage_dir.join("scratch");
|
||||
std::fs::read_dir(&scratch_dir)
|
||||
.expect("scratch directory should exist")
|
||||
.flatten()
|
||||
.map(|entry| entry.path())
|
||||
.find(|path| {
|
||||
|
|
@ -720,16 +720,16 @@ impl TestContext {
|
|||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"expected run directory for {run_id_suffix} under {}",
|
||||
runs_dir.display()
|
||||
scratch_dir.display()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the only run directory currently present under storage.
|
||||
pub fn single_run_dir(&self) -> PathBuf {
|
||||
let runs_dir = self.storage_dir.join("runs");
|
||||
let entries: Vec<_> = std::fs::read_dir(&runs_dir)
|
||||
.expect("runs directory should exist")
|
||||
let scratch_dir = self.storage_dir.join("scratch");
|
||||
let entries: Vec<_> = std::fs::read_dir(&scratch_dir)
|
||||
.expect("scratch directory should exist")
|
||||
.flatten()
|
||||
.map(|entry| entry.path())
|
||||
.filter(|path| path.is_dir())
|
||||
|
|
@ -755,7 +755,7 @@ impl TestContext {
|
|||
1,
|
||||
"expected exactly one run directory for fabro_test_case={} under {}",
|
||||
self.test_case_id(),
|
||||
runs_dir.display()
|
||||
scratch_dir.display()
|
||||
);
|
||||
entries.into_iter().next().unwrap()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ chrono = { workspace = true, features = ["serde"] }
|
|||
clap = { workspace = true, optional = true }
|
||||
dirs.workspace = true
|
||||
fabro-macros = { path = "../fabro-macros" }
|
||||
hex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
ulid.workspace = true
|
||||
uuid.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,44 +1,36 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use hex::FromHexError;
|
||||
use serde::de::Error as _;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use sha2::{Digest, Sha256};
|
||||
use ulid::Ulid;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::RunId;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct RunBlobId(Uuid);
|
||||
pub struct RunBlobId([u8; 32]);
|
||||
|
||||
impl RunBlobId {
|
||||
pub fn new(run_id: &RunId, content: &[u8]) -> Self {
|
||||
let ulid: Ulid = (*run_id).into();
|
||||
let ulid_bytes = ulid.to_bytes();
|
||||
pub fn new(content: &[u8]) -> Self {
|
||||
let hash = Sha256::digest(content);
|
||||
let mut buf = [0_u8; 16];
|
||||
buf[..8].copy_from_slice(&ulid_bytes[..8]);
|
||||
buf[8..].copy_from_slice(&hash[..8]);
|
||||
Self(Uuid::new_v8(buf))
|
||||
}
|
||||
|
||||
pub fn uuid(&self) -> &Uuid {
|
||||
&self.0
|
||||
let mut bytes = [0_u8; 32];
|
||||
bytes.copy_from_slice(&hash);
|
||||
Self(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RunBlobId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.hyphenated().fmt(f)
|
||||
f.write_str(&hex::encode(self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for RunBlobId {
|
||||
type Err = uuid::Error;
|
||||
type Err = FromHexError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Self(Uuid::parse_str(s)?))
|
||||
let mut bytes = [0_u8; 32];
|
||||
hex::decode_to_slice(s, &mut bytes)?;
|
||||
Ok(Self(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,46 +55,44 @@ impl<'de> Deserialize<'de> for RunBlobId {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{RunBlobId, RunId};
|
||||
use crate::RunBlobId;
|
||||
|
||||
#[test]
|
||||
fn same_content_and_run_id_produce_same_blob_id() {
|
||||
let run_id = RunId::new();
|
||||
assert_eq!(
|
||||
RunBlobId::new(&run_id, b"hello"),
|
||||
RunBlobId::new(&run_id, b"hello")
|
||||
);
|
||||
fn same_content_produces_same_blob_id() {
|
||||
assert_eq!(RunBlobId::new(b"hello"), RunBlobId::new(b"hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_run_ids_produce_different_blob_ids() {
|
||||
assert_ne!(
|
||||
RunBlobId::new(&RunId::new(), b"hello"),
|
||||
RunBlobId::new(&RunId::new(), b"hello")
|
||||
fn display_is_lowercase_sha256_hex() {
|
||||
assert_eq!(
|
||||
RunBlobId::new(b"hello").to_string(),
|
||||
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_content_produces_different_blob_ids() {
|
||||
let run_id = RunId::new();
|
||||
assert_ne!(
|
||||
RunBlobId::new(&run_id, b"hello"),
|
||||
RunBlobId::new(&run_id, b"world")
|
||||
);
|
||||
assert_ne!(RunBlobId::new(b"hello"), RunBlobId::new(b"world"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_and_parse_round_trip() {
|
||||
let blob_id = RunBlobId::new(&RunId::new(), b"hello");
|
||||
let blob_id = RunBlobId::new(b"hello");
|
||||
let parsed: RunBlobId = blob_id.to_string().parse().unwrap();
|
||||
assert_eq!(parsed, blob_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_round_trip() {
|
||||
let blob_id = RunBlobId::new(&RunId::new(), b"hello");
|
||||
let blob_id = RunBlobId::new(b"hello");
|
||||
let value = serde_json::to_value(blob_id).unwrap();
|
||||
let parsed: RunBlobId = serde_json::from_value(value).unwrap();
|
||||
assert_eq!(parsed, blob_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_non_hex_blob_ids() {
|
||||
let parsed = "not-a-blob-id".parse::<RunBlobId>();
|
||||
assert!(parsed.is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,9 +182,10 @@ impl Settings {
|
|||
|
||||
pub fn storage_dir(&self) -> PathBuf {
|
||||
self.storage_dir.clone().unwrap_or_else(|| {
|
||||
dirs::home_dir()
|
||||
.expect("could not determine home directory")
|
||||
.join(".fabro")
|
||||
std::env::var_os("FABRO_HOME")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| dirs::home_dir().map(|home| home.join(".fabro")))
|
||||
.unwrap_or_else(|| PathBuf::from(".fabro"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use std::path::Path;
|
|||
use serde_json::Value;
|
||||
|
||||
use fabro_agent::Sandbox;
|
||||
use fabro_store::SlateRunStore;
|
||||
use fabro_store::RunDatabase;
|
||||
|
||||
use crate::error::{FabroError, Result};
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ const ARTIFACT_POINTER_PREFIX: &str = "file://";
|
|||
/// Returns an error if blob persistence or cache materialization fails.
|
||||
pub async fn offload_large_values(
|
||||
updates: &mut HashMap<String, Value>,
|
||||
run_store: &SlateRunStore,
|
||||
run_store: &RunDatabase,
|
||||
cache_dir: &Path,
|
||||
) -> Result<()> {
|
||||
std::fs::create_dir_all(cache_dir)?;
|
||||
|
|
@ -133,7 +133,7 @@ mod tests {
|
|||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use fabro_store::SlateStore;
|
||||
use fabro_store::Database;
|
||||
use object_store::memory::InMemory;
|
||||
use ulid::Ulid;
|
||||
|
||||
|
|
@ -143,9 +143,9 @@ mod tests {
|
|||
fabro_types::RunId::from(Ulid(u128::from(hasher.finish())))
|
||||
}
|
||||
|
||||
async fn make_run_store(label: &str) -> fabro_store::SlateRunStore {
|
||||
async fn make_run_store(label: &str) -> fabro_store::RunDatabase {
|
||||
let object_store = Arc::new(InMemory::new());
|
||||
let store = SlateStore::new(object_store, "runs/", Duration::from_millis(1));
|
||||
let store = Database::new(object_store, "runs/", Duration::from_millis(1));
|
||||
store.create_run(&test_run_id(label)).await.unwrap()
|
||||
}
|
||||
|
||||
|
|
@ -156,8 +156,7 @@ mod tests {
|
|||
|
||||
let large_string = "x".repeat(BLOB_OFFLOAD_THRESHOLD + 1);
|
||||
let serialized = serde_json::to_vec(&serde_json::json!(large_string.clone())).unwrap();
|
||||
let expected_blob_id =
|
||||
fabro_types::RunBlobId::new(&test_run_id("artifact-offload"), &serialized);
|
||||
let expected_blob_id = fabro_types::RunBlobId::new(&serialized);
|
||||
|
||||
let mut updates = HashMap::new();
|
||||
updates.insert("response.plan".to_string(), serde_json::json!(large_string));
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use ::fabro_types::run_event as fabro_types;
|
|||
use ::fabro_types::{RunEvent, RunId, StageStatus, StatusReason};
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::Utc;
|
||||
use fabro_store::{EventPayload, SlateRunStore};
|
||||
use fabro_store::{EventPayload, RunDatabase};
|
||||
use fabro_util::json::normalize_json_value;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
|
@ -2301,7 +2301,7 @@ pub fn event_payload_from_redacted_json(line: &str, run_id: &RunId) -> Result<Ev
|
|||
EventPayload::new(value, run_id).map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
pub async fn append_event(run_store: &SlateRunStore, run_id: &RunId, event: &Event) -> Result<()> {
|
||||
pub async fn append_event(run_store: &RunDatabase, run_id: &RunId, event: &Event) -> Result<()> {
|
||||
let stored = to_run_event(run_id, event);
|
||||
let payload = build_redacted_event_payload(&stored, run_id)?;
|
||||
run_store
|
||||
|
|
@ -2323,7 +2323,7 @@ pub struct StoreProgressLogger {
|
|||
|
||||
impl StoreProgressLogger {
|
||||
#[must_use]
|
||||
pub fn new(run_store: SlateRunStore) -> Self {
|
||||
pub fn new(run_store: RunDatabase) -> Self {
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
|
||||
tokio::spawn(async move {
|
||||
|
|
@ -2697,7 +2697,7 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn append_event_writes_store_event_shape() {
|
||||
let store = fabro_store::SlateStore::new(
|
||||
let store = fabro_store::Database::new(
|
||||
std::sync::Arc::new(object_store::memory::InMemory::new()),
|
||||
"",
|
||||
std::time::Duration::from_millis(1),
|
||||
|
|
|
|||
|
|
@ -310,7 +310,7 @@ pub fn sanitize_ref_component(s: &str) -> String {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use crate::run_dump::RunDump;
|
||||
use fabro_store::SlateStore;
|
||||
use fabro_store::Database;
|
||||
use fabro_types::fixtures;
|
||||
use object_store::memory::InMemory;
|
||||
use std::fs;
|
||||
|
|
@ -340,8 +340,8 @@ mod tests {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
fn test_store() -> Arc<SlateStore> {
|
||||
Arc::new(SlateStore::new(
|
||||
fn test_store() -> Arc<Database> {
|
||||
Arc::new(Database::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
|
|
|
|||
|
|
@ -395,7 +395,7 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::event::Emitter;
|
||||
use fabro_graphviz::graph::AttrValue;
|
||||
use fabro_store::{SlateRunStore, SlateStore, StageId};
|
||||
use fabro_store::{Database, RunDatabase, StageId};
|
||||
use fabro_types::fixtures;
|
||||
use object_store::memory::InMemory;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -406,8 +406,8 @@ mod tests {
|
|||
EngineServices::test_default()
|
||||
}
|
||||
|
||||
fn test_store() -> Arc<SlateStore> {
|
||||
Arc::new(SlateStore::new(
|
||||
fn test_store() -> Arc<Database> {
|
||||
Arc::new(Database::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
|
|
@ -416,7 +416,7 @@ mod tests {
|
|||
|
||||
async fn make_services_with_run_store() -> (
|
||||
EngineServices,
|
||||
SlateRunStore,
|
||||
RunDatabase,
|
||||
crate::event::StoreProgressLogger,
|
||||
) {
|
||||
let store = test_store();
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::outcome::StageStatus;
|
||||
use fabro_graphviz::graph::AttrValue;
|
||||
use fabro_store::{SlateRunStore, SlateStore, StageId};
|
||||
use fabro_store::{Database, RunDatabase, StageId};
|
||||
use fabro_types::fixtures;
|
||||
use object_store::memory::InMemory;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -179,8 +179,8 @@ mod tests {
|
|||
EngineServices::test_default()
|
||||
}
|
||||
|
||||
fn test_store() -> Arc<SlateStore> {
|
||||
Arc::new(SlateStore::new(
|
||||
fn test_store() -> Arc<Database> {
|
||||
Arc::new(Database::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
|
|
@ -189,7 +189,7 @@ mod tests {
|
|||
|
||||
async fn make_services_with_run_store() -> (
|
||||
EngineServices,
|
||||
SlateRunStore,
|
||||
RunDatabase,
|
||||
crate::event::StoreProgressLogger,
|
||||
) {
|
||||
let store = test_store();
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ use crate::run_dir::visit_from_context;
|
|||
use crate::run_options::RunOptions;
|
||||
use async_trait::async_trait;
|
||||
use fabro_graphviz::graph::{AttrValue, Graph, Node};
|
||||
use fabro_store::SlateStore;
|
||||
use fabro_store::Database;
|
||||
use fabro_types::Settings;
|
||||
use object_store::memory::InMemory;
|
||||
use tokio::time::{sleep, timeout};
|
||||
|
|
@ -233,7 +233,7 @@ impl Handler for SubWorkflowHandler {
|
|||
let env = services.env.clone();
|
||||
let dry_run = services.dry_run;
|
||||
let workflow_bundle = services.workflow_bundle.clone();
|
||||
let store = Arc::new(SlateStore::new(
|
||||
let store = Arc::new(Database::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ use std::time::Duration;
|
|||
|
||||
use async_trait::async_trait;
|
||||
use fabro_agent::Sandbox;
|
||||
use fabro_store::SlateRunStore;
|
||||
#[cfg(test)]
|
||||
use fabro_store::SlateStore;
|
||||
use fabro_store::Database;
|
||||
use fabro_store::RunDatabase;
|
||||
#[cfg(test)]
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ pub struct EngineServices {
|
|||
pub registry: Arc<HandlerRegistry>,
|
||||
pub emitter: Arc<Emitter>,
|
||||
pub sandbox: Arc<dyn Sandbox>,
|
||||
pub run_store: SlateRunStore,
|
||||
pub run_store: RunDatabase,
|
||||
/// Git state for the current run. Set via `set_git_state` at the start of
|
||||
/// `run_via_core` and read by parallel/fan-in handlers.
|
||||
pub(crate) git_state: std::sync::RwLock<Option<Arc<GitState>>>,
|
||||
|
|
@ -80,7 +80,7 @@ impl EngineServices {
|
|||
/// Test-only default: empty registry, no hooks, local sandbox at cwd.
|
||||
#[cfg(test)]
|
||||
pub fn test_default() -> Self {
|
||||
let store = Arc::new(SlateStore::new(
|
||||
let store = Arc::new(Database::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
|
|
|
|||
|
|
@ -584,7 +584,7 @@ fn find_join_node(results: &[BranchResult], graph: &Graph) -> Option<String> {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use fabro_graphviz::graph::{AttrValue, Edge};
|
||||
use fabro_store::{SlateStore, StageId};
|
||||
use fabro_store::{Database, StageId};
|
||||
use fabro_types::fixtures;
|
||||
use object_store::memory::InMemory;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -594,8 +594,8 @@ mod tests {
|
|||
EngineServices::test_default()
|
||||
}
|
||||
|
||||
fn test_store() -> Arc<SlateStore> {
|
||||
Arc::new(SlateStore::new(
|
||||
fn test_store() -> Arc<Database> {
|
||||
Arc::new(Database::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ impl Handler for PromptHandler {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use fabro_graphviz::graph::AttrValue;
|
||||
use fabro_store::{SlateRunStore, SlateStore, StageId};
|
||||
use fabro_store::{Database, RunDatabase, StageId};
|
||||
use fabro_types::fixtures;
|
||||
use object_store::memory::InMemory;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -186,8 +186,8 @@ mod tests {
|
|||
EngineServices::test_default()
|
||||
}
|
||||
|
||||
fn test_store() -> Arc<SlateStore> {
|
||||
Arc::new(SlateStore::new(
|
||||
fn test_store() -> Arc<Database> {
|
||||
Arc::new(Database::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
|
|
@ -196,7 +196,7 @@ mod tests {
|
|||
|
||||
async fn make_services_with_run_store() -> (
|
||||
EngineServices,
|
||||
SlateRunStore,
|
||||
RunDatabase,
|
||||
crate::event::StoreProgressLogger,
|
||||
) {
|
||||
let store = test_store();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::sync::Arc;
|
|||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use fabro_store::SlateRunStore;
|
||||
use fabro_store::RunDatabase;
|
||||
|
||||
use fabro_core::graph::NodeSpec;
|
||||
use fabro_core::lifecycle::{AttemptContext, AttemptResultContext, RunLifecycle};
|
||||
|
|
@ -26,7 +26,7 @@ type WfNodeDecision = NodeDecision<Option<StageUsage>>;
|
|||
/// Sub-lifecycle responsible for artifact collection, offloading, and syncing.
|
||||
pub(crate) struct ArtifactLifecycle {
|
||||
pub sandbox: Arc<dyn fabro_sandbox::Sandbox>,
|
||||
pub run_store: SlateRunStore,
|
||||
pub run_store: RunDatabase,
|
||||
pub blob_cache_dir: PathBuf,
|
||||
pub emitter: Arc<Emitter>,
|
||||
pub artifacts_dir: PathBuf,
|
||||
|
|
@ -40,7 +40,7 @@ impl ArtifactLifecycle {
|
|||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn new(
|
||||
sandbox: Arc<dyn fabro_sandbox::Sandbox>,
|
||||
run_store: SlateRunStore,
|
||||
run_store: RunDatabase,
|
||||
blob_cache_dir: PathBuf,
|
||||
emitter: Arc<Emitter>,
|
||||
artifacts_dir: PathBuf,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::path::PathBuf;
|
|||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use fabro_store::SlateRunStore;
|
||||
use fabro_store::RunDatabase;
|
||||
use fabro_types::RunId;
|
||||
use tokio::fs;
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ pub(crate) struct GitLifecycle {
|
|||
pub emitter: Arc<Emitter>,
|
||||
pub run_dir: PathBuf,
|
||||
pub run_id: RunId,
|
||||
pub run_store: SlateRunStore,
|
||||
pub run_store: RunDatabase,
|
||||
pub run_options: Arc<RunOptions>,
|
||||
pub start_node_id: Option<String>,
|
||||
// Cross-lifecycle data (shared with EventLifecycle)
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ use std::sync::{Arc, Mutex};
|
|||
use std::time::Instant;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use fabro_store::RuntimeState;
|
||||
use fabro_store::SlateRunStore;
|
||||
use fabro_config::RunScratch;
|
||||
use fabro_store::RunDatabase;
|
||||
use fabro_types::RunId;
|
||||
|
||||
use fabro_core::error::Result as CoreResult;
|
||||
|
|
@ -81,12 +81,12 @@ impl WorkflowLifecycle {
|
|||
sandbox: &Arc<dyn Sandbox>,
|
||||
graph: Arc<GvGraph>,
|
||||
run_dir: &PathBuf,
|
||||
run_store: &SlateRunStore,
|
||||
run_store: &RunDatabase,
|
||||
run_options: &Arc<RunOptions>,
|
||||
is_resume: bool,
|
||||
on_node: crate::OnNodeCallback,
|
||||
) -> Self {
|
||||
let runtime_state = RuntimeState::new(run_dir);
|
||||
let run_scratch = RunScratch::new(run_dir);
|
||||
let restarted_from: Arc<Mutex<Option<(String, String)>>> = Arc::new(Mutex::new(None));
|
||||
let loop_restart_signature_limit = graph.loop_restart_signature_limit();
|
||||
let checkpoint_git_result: Arc<Mutex<Option<GitCheckpointResult>>> =
|
||||
|
|
@ -155,9 +155,9 @@ impl WorkflowLifecycle {
|
|||
let artifact = ArtifactLifecycle::new(
|
||||
Arc::clone(sandbox),
|
||||
run_store.clone(),
|
||||
runtime_state.blob_cache_dir(),
|
||||
run_scratch.blob_cache_dir(),
|
||||
Arc::clone(emitter),
|
||||
runtime_state.artifacts_dir(),
|
||||
run_scratch.artifact_files_dir(),
|
||||
run_options.artifact_globs().to_vec(),
|
||||
captured_artifact_count,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
use chrono::Local;
|
||||
use fabro_config::Storage;
|
||||
use fabro_graphviz::graph::{AttrValue, Graph};
|
||||
use fabro_model::{Catalog, Provider};
|
||||
use fabro_sandbox::SandboxProvider;
|
||||
use fabro_store::SlateStore;
|
||||
use fabro_store::Database;
|
||||
use fabro_types::{RunId, Settings};
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
|
|
@ -14,7 +15,7 @@ use crate::file_resolver::FileResolver;
|
|||
use crate::pipeline::types::PersistOptions;
|
||||
use crate::pipeline::{self, Persisted, TransformOptions, Validated};
|
||||
use crate::records::RunRecord;
|
||||
use crate::run_lookup::default_runs_base;
|
||||
use crate::run_lookup::default_scratch_base;
|
||||
use crate::transforms::{Transform, expand_vars};
|
||||
use crate::workflow_bundle::{StoredWorkflowBundle, WorkflowBundle};
|
||||
use fabro_sandbox::daytona::detect_repo_info;
|
||||
|
|
@ -58,7 +59,7 @@ struct PersistCreateOptions {
|
|||
}
|
||||
|
||||
/// Resolve workflow inputs, normalize settings, and persist a run directory.
|
||||
pub async fn create(store: &SlateStore, request: CreateRunInput) -> Result<CreatedRun, FabroError> {
|
||||
pub async fn create(store: &Database, request: CreateRunInput) -> Result<CreatedRun, FabroError> {
|
||||
let resolved = resolve_workflow(ResolveWorkflowInput {
|
||||
workflow: request.workflow,
|
||||
settings: request.settings,
|
||||
|
|
@ -85,8 +86,8 @@ pub async fn create(store: &SlateStore, request: CreateRunInput) -> Result<Creat
|
|||
|
||||
let settings = resolved.settings.clone();
|
||||
let run_id = run_id.unwrap_or_else(RunId::new);
|
||||
let storage_dir = settings.storage_dir();
|
||||
let run_dir = make_run_dir(&storage_dir.join("runs"), &run_id);
|
||||
let storage = Storage::new(settings.storage_dir());
|
||||
let run_dir = storage.run_scratch(&run_id).root().to_path_buf();
|
||||
let working_directory = resolved.working_directory.clone();
|
||||
let host_repo_path =
|
||||
host_repo_path.or_else(|| Some(working_directory.to_string_lossy().to_string()));
|
||||
|
|
@ -142,7 +143,7 @@ pub async fn create(store: &SlateStore, request: CreateRunInput) -> Result<Creat
|
|||
}
|
||||
|
||||
async fn persist_created_run(
|
||||
store: &SlateStore,
|
||||
store: &Database,
|
||||
persisted: &Persisted,
|
||||
workflow_source: &str,
|
||||
workflow_config: Option<String>,
|
||||
|
|
@ -388,12 +389,12 @@ pub(crate) fn resolve_run_settings(mut settings: Settings, graph: &Graph) -> Set
|
|||
}
|
||||
|
||||
pub(crate) fn default_run_dir(run_id: &RunId) -> PathBuf {
|
||||
make_run_dir(&default_runs_base(), run_id)
|
||||
make_run_dir(&default_scratch_base(), run_id)
|
||||
}
|
||||
|
||||
pub fn make_run_dir(runs_base: &Path, run_id: &RunId) -> PathBuf {
|
||||
pub fn make_run_dir(scratch_base: &Path, run_id: &RunId) -> PathBuf {
|
||||
let local_dt = run_id.created_at().with_timezone(&Local);
|
||||
runs_base.join(format!("{}-{run_id}", local_dt.format("%Y%m%d")))
|
||||
scratch_base.join(format!("{}-{run_id}", local_dt.format("%Y%m%d")))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -401,7 +402,7 @@ mod tests {
|
|||
use super::*;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use fabro_graphviz::graph::AttrValue;
|
||||
use fabro_store::{SlateStore, StoreHandle};
|
||||
use fabro_store::Database;
|
||||
use fabro_types::fixtures;
|
||||
use object_store::local::LocalFileSystem;
|
||||
use object_store::memory::InMemory;
|
||||
|
|
@ -410,8 +411,8 @@ mod tests {
|
|||
|
||||
use crate::operations::{ValidateInput, validate};
|
||||
use crate::workflow_bundle::BundledWorkflow;
|
||||
fn memory_store() -> StoreHandle {
|
||||
Arc::new(SlateStore::new(
|
||||
fn memory_store() -> Arc<Database> {
|
||||
Arc::new(Database::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
|
|
@ -470,7 +471,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn make_run_dir_uses_run_id_timestamp_in_local_time() {
|
||||
let runs_base = Path::new("/tmp/runs");
|
||||
let scratch_base = Path::new("/tmp/scratch");
|
||||
let run_id = RunId::from(ulid::Ulid::from_datetime(
|
||||
Utc.with_ymd_and_hms(2026, 3, 27, 12, 0, 0).unwrap().into(),
|
||||
));
|
||||
|
|
@ -481,8 +482,8 @@ mod tests {
|
|||
.to_string();
|
||||
|
||||
assert_eq!(
|
||||
make_run_dir(runs_base, &run_id),
|
||||
runs_base.join(format!("{expected_date}-{run_id}"))
|
||||
make_run_dir(scratch_base, &run_id),
|
||||
scratch_base.join(format!("{expected_date}-{run_id}"))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -874,11 +875,7 @@ mod tests {
|
|||
std::fs::create_dir_all(storage_dir.join("store")).unwrap();
|
||||
let object_store =
|
||||
Arc::new(LocalFileSystem::new_with_prefix(storage_dir.join("store")).unwrap());
|
||||
let store = StoreHandle::from(Arc::new(SlateStore::new(
|
||||
object_store,
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
)));
|
||||
let store = Arc::new(Database::new(object_store, "", Duration::from_millis(1)));
|
||||
let created = create(
|
||||
store.as_ref(),
|
||||
CreateRunInput {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use std::path::PathBuf;
|
|||
use anyhow::{Context, Result, bail};
|
||||
use fabro_checkpoint::branch::BranchStore;
|
||||
use fabro_checkpoint::git::Store as GitStore;
|
||||
use fabro_store::{SlateRunStore as DurableRunStore, SlateStore as DurableStore};
|
||||
use fabro_store::{Database as DurableStore, RunDatabase as DurableRunStore};
|
||||
use fabro_types::{RunId, StageId};
|
||||
use git2::{Repository, Signature};
|
||||
use ulid::Ulid;
|
||||
|
|
@ -334,7 +334,7 @@ fn resolve_prefix_matches(prefix: &str, matches: Vec<RunId>) -> Result<RunId> {
|
|||
mod tests {
|
||||
use chrono::{TimeZone, Utc};
|
||||
use fabro_graphviz::graph::Graph;
|
||||
use fabro_store::{SlateStore, StageId, StoreHandle};
|
||||
use fabro_store::{Database, StageId};
|
||||
use fabro_types::{RunId, RunRecord, SandboxRecord, Settings, StartRecord, fixtures};
|
||||
use object_store::memory::InMemory;
|
||||
use std::collections::HashMap;
|
||||
|
|
@ -359,8 +359,8 @@ mod tests {
|
|||
fixtures::RUN_1
|
||||
}
|
||||
|
||||
fn memory_store() -> StoreHandle {
|
||||
Arc::new(SlateStore::new(
|
||||
fn memory_store() -> Arc<Database> {
|
||||
Arc::new(Database::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
|
|
@ -428,7 +428,7 @@ mod tests {
|
|||
}
|
||||
|
||||
async fn create_run_store(
|
||||
store: &SlateStore,
|
||||
store: &Database,
|
||||
run_id: RunId,
|
||||
host_repo_path: Option<&str>,
|
||||
) -> DurableRunStore {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::path::Path;
|
||||
|
||||
use fabro_store::RuntimeState;
|
||||
use fabro_config::RunScratch;
|
||||
|
||||
use crate::error::FabroError;
|
||||
use crate::event::{Event, append_event};
|
||||
|
|
@ -52,11 +52,11 @@ pub async fn resume(run_dir: &Path, services: StartServices) -> Result<Started,
|
|||
}
|
||||
|
||||
fn cleanup_resume_artifacts(run_dir: &Path) {
|
||||
let runtime_state = RuntimeState::new(run_dir);
|
||||
let run_scratch = RunScratch::new(run_dir);
|
||||
for path in [
|
||||
runtime_state.interview_request_path(),
|
||||
runtime_state.interview_response_path(),
|
||||
runtime_state.interview_claim_path(),
|
||||
run_scratch.interview_request_path(),
|
||||
run_scratch.interview_response_path(),
|
||||
run_scratch.interview_claim_path(),
|
||||
] {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use fabro_config::{project as project_config, run as run_config, sandbox as sand
|
|||
use fabro_interview::{AutoApproveInterviewer, Interviewer};
|
||||
use fabro_model::{Catalog, FallbackTarget, Provider};
|
||||
use fabro_sandbox::{SandboxProvider, SandboxSpec};
|
||||
use fabro_store::SlateRunStore;
|
||||
use fabro_store::RunDatabase;
|
||||
use fabro_types::{RunId, Settings};
|
||||
|
||||
use crate::context::Context;
|
||||
|
|
@ -48,7 +48,7 @@ struct RunSession {
|
|||
sandbox_env: SandboxEnvSpec,
|
||||
devcontainer: Option<DevcontainerSpec>,
|
||||
seed_context: Option<Context>,
|
||||
run_store: SlateRunStore,
|
||||
run_store: RunDatabase,
|
||||
git: Option<GitCheckpointOptions>,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
worktree_mode: Option<WorktreeMode>,
|
||||
|
|
@ -68,7 +68,7 @@ pub struct StartServices {
|
|||
pub cancel_token: Option<Arc<AtomicBool>>,
|
||||
pub emitter: Arc<Emitter>,
|
||||
pub interviewer: Arc<dyn Interviewer>,
|
||||
pub run_store: SlateRunStore,
|
||||
pub run_store: RunDatabase,
|
||||
pub github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
pub on_node: crate::OnNodeCallback,
|
||||
pub registry_override: Option<Arc<HandlerRegistry>>,
|
||||
|
|
@ -209,7 +209,7 @@ pub(super) async fn execute_persisted_run(
|
|||
|
||||
async fn persist_terminal_engine_failure(
|
||||
run_id: RunId,
|
||||
run_store: &SlateRunStore,
|
||||
run_store: &RunDatabase,
|
||||
_run_dir: &Path,
|
||||
error: &FabroError,
|
||||
duration: Duration,
|
||||
|
|
@ -590,7 +590,7 @@ impl RunSession {
|
|||
|
||||
struct DetachedRunBootstrapGuard {
|
||||
run_id: RunId,
|
||||
run_store: SlateRunStore,
|
||||
run_store: RunDatabase,
|
||||
cancel_token: Option<Arc<AtomicBool>>,
|
||||
active: bool,
|
||||
}
|
||||
|
|
@ -599,7 +599,7 @@ impl DetachedRunBootstrapGuard {
|
|||
fn arm(
|
||||
run_id: RunId,
|
||||
_run_dir: &Path,
|
||||
run_store: SlateRunStore,
|
||||
run_store: RunDatabase,
|
||||
cancel_token: Option<Arc<AtomicBool>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
|
@ -652,14 +652,14 @@ const POSTRUN_ABORTED_MESSAGE: &str = "Run aborted before post-run finalization
|
|||
const POSTRUN_CANCELLED_MESSAGE: &str = "Run cancelled before post-run finalization completed.";
|
||||
|
||||
struct DetachedRunCompletionGuard {
|
||||
run_store: SlateRunStore,
|
||||
run_store: RunDatabase,
|
||||
run_id: RunId,
|
||||
cancel_token: Option<Arc<AtomicBool>>,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
impl DetachedRunCompletionGuard {
|
||||
fn arm(run_id: RunId, run_store: SlateRunStore, cancel_token: Option<Arc<AtomicBool>>) -> Self {
|
||||
fn arm(run_id: RunId, run_store: RunDatabase, cancel_token: Option<Arc<AtomicBool>>) -> Self {
|
||||
Self {
|
||||
run_store,
|
||||
run_id,
|
||||
|
|
@ -766,7 +766,7 @@ impl Drop for DetachedRunCompletionGuard {
|
|||
|
||||
async fn persist_detached_failure(
|
||||
run_id: RunId,
|
||||
run_store: &SlateRunStore,
|
||||
run_store: &RunDatabase,
|
||||
_run_dir: &Path,
|
||||
phase: &'static str,
|
||||
reason: StatusReason,
|
||||
|
|
@ -818,7 +818,7 @@ mod tests {
|
|||
use std::time::Duration;
|
||||
|
||||
use chrono::Utc;
|
||||
use fabro_store::{SlateStore, StoreHandle};
|
||||
use fabro_store::Database;
|
||||
use fabro_types::{Settings, fixtures};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
|
|
@ -838,15 +838,15 @@ mod tests {
|
|||
start -> exit
|
||||
}"#;
|
||||
|
||||
fn memory_store() -> StoreHandle {
|
||||
Arc::new(SlateStore::new(
|
||||
fn memory_store() -> Arc<Database> {
|
||||
Arc::new(Database::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
))
|
||||
}
|
||||
|
||||
async fn persisted_workflow(dot: &str, run_dir: &Path) -> (Persisted, StoreHandle) {
|
||||
async fn persisted_workflow(dot: &str, run_dir: &Path) -> (Persisted, Arc<Database>) {
|
||||
let store = memory_store();
|
||||
let created = crate::operations::create(
|
||||
&store,
|
||||
|
|
@ -885,7 +885,7 @@ mod tests {
|
|||
}
|
||||
|
||||
async fn test_start_services(
|
||||
store: &SlateStore,
|
||||
store: &Database,
|
||||
_run_dir: &Path,
|
||||
emitter: Arc<Emitter>,
|
||||
registry: Arc<HandlerRegistry>,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
|
|||
use fabro_hooks::HookSettings;
|
||||
use fabro_interview::AutoApproveInterviewer;
|
||||
use fabro_sandbox::SandboxSpec;
|
||||
use fabro_store::{SlateStore, StoreHandle};
|
||||
use fabro_store::Database;
|
||||
use fabro_types::{RunId, Settings, fixtures};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
|
|
@ -156,8 +156,8 @@ fn test_lifecycle(setup_commands: Vec<String>) -> LifecycleOptions {
|
|||
}
|
||||
}
|
||||
|
||||
async fn test_run_store(run_id: &RunId) -> fabro_store::SlateRunStore {
|
||||
let store: StoreHandle = Arc::new(SlateStore::new(
|
||||
async fn test_run_store(run_id: &RunId) -> fabro_store::RunDatabase {
|
||||
let store: Arc<Database> = Arc::new(Database::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use crate::run_options::RunOptions;
|
|||
use crate::run_status::{RunStatus, StatusReason};
|
||||
use crate::sandbox_git::git_push_host;
|
||||
use fabro_hooks::{HookContext, HookEvent, HookRunner};
|
||||
use fabro_store::SlateRunStore;
|
||||
use fabro_store::RunDatabase;
|
||||
|
||||
use super::types::{Concluded, FinalizeOptions, Retroed};
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ pub fn classify_engine_result(
|
|||
}
|
||||
|
||||
pub(crate) async fn build_conclusion_from_store(
|
||||
run_store: &SlateRunStore,
|
||||
run_store: &RunDatabase,
|
||||
status: StageStatus,
|
||||
failure_reason: Option<String>,
|
||||
run_duration_ms: u64,
|
||||
|
|
@ -169,7 +169,7 @@ fn build_conclusion_from_parts(
|
|||
///
|
||||
/// This captures the last diff.patch (written after the final checkpoint) and retro.json.
|
||||
/// Best-effort: errors are logged as warnings.
|
||||
pub async fn write_finalize_commit(run_options: &RunOptions, run_store: &SlateRunStore) {
|
||||
pub async fn write_finalize_commit(run_options: &RunOptions, run_store: &RunDatabase) {
|
||||
let (Some(meta_branch), Some(repo_path)) = (
|
||||
run_options
|
||||
.git
|
||||
|
|
@ -321,7 +321,7 @@ mod tests {
|
|||
use std::time::Duration;
|
||||
|
||||
use fabro_graphviz::graph::Graph;
|
||||
use fabro_store::SlateStore;
|
||||
use fabro_store::Database;
|
||||
use fabro_types::{RunId, Settings, fixtures};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
|
|
@ -350,8 +350,8 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn test_store() -> Arc<SlateStore> {
|
||||
Arc::new(SlateStore::new(
|
||||
fn test_store() -> Arc<Database> {
|
||||
Arc::new(Database::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
|
|
|
|||
|
|
@ -668,7 +668,7 @@ mod tests {
|
|||
use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
|
||||
use fabro_interview::AutoApproveInterviewer;
|
||||
use fabro_sandbox::SandboxSpec;
|
||||
use fabro_store::{SlateStore, StoreHandle};
|
||||
use fabro_store::Database;
|
||||
use fabro_types::{RunId, Settings, fixtures};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
|
|
@ -682,8 +682,8 @@ mod tests {
|
|||
fixtures::RUN_1
|
||||
}
|
||||
|
||||
fn memory_store() -> StoreHandle {
|
||||
Arc::new(SlateStore::new(
|
||||
fn memory_store() -> Arc<Database> {
|
||||
Arc::new(Database::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::path::Path;
|
||||
|
||||
use fabro_store::SlateRunStore;
|
||||
use fabro_store::RunDatabase;
|
||||
|
||||
use crate::error::FabroError;
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ pub(crate) fn persist(
|
|||
}
|
||||
|
||||
pub(crate) async fn load_from_store(
|
||||
run_store: &SlateRunStore,
|
||||
run_store: &RunDatabase,
|
||||
run_dir: &Path,
|
||||
) -> Result<Persisted, FabroError> {
|
||||
let state = run_store
|
||||
|
|
@ -54,7 +54,7 @@ mod tests {
|
|||
use std::path::PathBuf;
|
||||
|
||||
use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
|
||||
use fabro_store::{SlateRunStore, SlateStore, StoreHandle};
|
||||
use fabro_store::{Database, RunDatabase};
|
||||
use fabro_types::{Settings, fixtures};
|
||||
use object_store::memory::InMemory;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -64,8 +64,8 @@ mod tests {
|
|||
use crate::event::{Event, append_event};
|
||||
use crate::records::RunRecord;
|
||||
|
||||
fn memory_store() -> StoreHandle {
|
||||
Arc::new(SlateStore::new(
|
||||
fn memory_store() -> Arc<Database> {
|
||||
Arc::new(Database::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
|
|
@ -137,11 +137,7 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
async fn seeded_store(
|
||||
run_dir: &Path,
|
||||
record: &RunRecord,
|
||||
source: Option<&str>,
|
||||
) -> SlateRunStore {
|
||||
async fn seeded_store(run_dir: &Path, record: &RunRecord, source: Option<&str>) -> RunDatabase {
|
||||
let store = memory_store();
|
||||
let run_store = store.create_run(&record.run_id).await.unwrap();
|
||||
append_event(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use fabro_config::run::MergeStrategy;
|
||||
use fabro_store::{RunProjection, SlateRunStore};
|
||||
use fabro_store::{RunDatabase, RunProjection};
|
||||
use fabro_types::PullRequestRecord;
|
||||
use tracing::{debug, info};
|
||||
|
||||
|
|
@ -280,7 +280,7 @@ fn emit_run_notice(
|
|||
});
|
||||
}
|
||||
|
||||
async fn load_pull_request_diff(run_store: &SlateRunStore) -> String {
|
||||
async fn load_pull_request_diff(run_store: &RunDatabase) -> String {
|
||||
run_store
|
||||
.state()
|
||||
.await
|
||||
|
|
@ -298,7 +298,7 @@ pub async fn build_pr_body(
|
|||
diff: &str,
|
||||
goal: &str,
|
||||
model: &str,
|
||||
run_store: &SlateRunStore,
|
||||
run_store: &RunDatabase,
|
||||
conclusion: Option<&Conclusion>,
|
||||
) -> Result<String, String> {
|
||||
debug!("Building PR body");
|
||||
|
|
@ -406,7 +406,7 @@ pub async fn maybe_open_pull_request(
|
|||
model: &str,
|
||||
draft: bool,
|
||||
auto_merge: Option<AutoMergeOptions>,
|
||||
run_store: &SlateRunStore,
|
||||
run_store: &RunDatabase,
|
||||
conclusion: Option<&Conclusion>,
|
||||
) -> Result<Option<PullRequestRecord>, String> {
|
||||
if diff.is_empty() {
|
||||
|
|
@ -591,7 +591,7 @@ mod tests {
|
|||
use fabro_retro::retro::{
|
||||
AggregateStats, FrictionKind, FrictionPoint, OpenItem, OpenItemKind, StageRetro,
|
||||
};
|
||||
use fabro_store::SlateStore;
|
||||
use fabro_store::Database;
|
||||
use fabro_types::{RunRecord, Settings, fixtures};
|
||||
use futures::stream;
|
||||
use object_store::memory::InMemory;
|
||||
|
|
@ -668,8 +668,8 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn test_store() -> Arc<SlateStore> {
|
||||
Arc::new(SlateStore::new(
|
||||
fn test_store() -> Arc<Database> {
|
||||
Arc::new(Database::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ mod tests {
|
|||
use std::time::Duration;
|
||||
|
||||
use fabro_graphviz::graph::Graph;
|
||||
use fabro_store::SlateStore;
|
||||
use fabro_store::Database;
|
||||
use fabro_types::{RunId, Settings, fixtures};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
|
|
@ -203,8 +203,8 @@ mod tests {
|
|||
)
|
||||
}
|
||||
|
||||
fn test_store() -> Arc<SlateStore> {
|
||||
Arc::new(SlateStore::new(
|
||||
fn test_store() -> Arc<Database> {
|
||||
Arc::new(Database::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
|
|
@ -214,7 +214,7 @@ mod tests {
|
|||
async fn test_run_store(
|
||||
run_dir: &std::path::Path,
|
||||
checkpoint: &Checkpoint,
|
||||
) -> fabro_store::SlateRunStore {
|
||||
) -> fabro_store::RunDatabase {
|
||||
let inner = test_store().create_run(&test_run_id()).await.unwrap();
|
||||
let run_store = inner;
|
||||
let run_record = RunRecord {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use fabro_llm::Provider;
|
|||
use fabro_mcp::config::McpServerSettings;
|
||||
use fabro_model::FallbackTarget;
|
||||
use fabro_sandbox::SandboxSpec;
|
||||
use fabro_store::SlateRunStore;
|
||||
use fabro_store::RunDatabase;
|
||||
use fabro_types::RunId;
|
||||
use fabro_validate::Diagnostic;
|
||||
|
||||
|
|
@ -197,7 +197,7 @@ impl Persisted {
|
|||
}
|
||||
|
||||
pub async fn load_from_store(
|
||||
run_store: &SlateRunStore,
|
||||
run_store: &RunDatabase,
|
||||
run_dir: &Path,
|
||||
) -> Result<Self, FabroError> {
|
||||
super::persist::load_from_store(run_store, run_dir).await
|
||||
|
|
@ -229,7 +229,7 @@ pub struct DevcontainerSpec {
|
|||
|
||||
pub struct InitOptions {
|
||||
pub run_id: RunId,
|
||||
pub run_store: SlateRunStore,
|
||||
pub run_store: RunDatabase,
|
||||
pub dry_run: bool,
|
||||
pub emitter: Arc<Emitter>,
|
||||
pub sandbox: SandboxSpec,
|
||||
|
|
@ -257,7 +257,7 @@ pub struct Initialized {
|
|||
pub run_options: RunOptions,
|
||||
pub workflow_path: Option<PathBuf>,
|
||||
pub workflow_bundle: Option<Arc<WorkflowBundle>>,
|
||||
pub run_store: SlateRunStore,
|
||||
pub run_store: RunDatabase,
|
||||
pub(crate) checkpoint: Option<Checkpoint>,
|
||||
pub(crate) seed_context: Option<Context>,
|
||||
pub emitter: Arc<Emitter>,
|
||||
|
|
@ -278,7 +278,7 @@ pub struct Executed {
|
|||
pub graph: Graph,
|
||||
pub outcome: Result<Outcome, FabroError>,
|
||||
pub run_options: RunOptions,
|
||||
pub run_store: SlateRunStore,
|
||||
pub run_store: RunDatabase,
|
||||
pub hook_runner: Option<Arc<HookRunner>>,
|
||||
pub emitter: Arc<Emitter>,
|
||||
pub sandbox: Arc<dyn Sandbox>,
|
||||
|
|
@ -295,7 +295,7 @@ pub struct Retroed {
|
|||
pub graph: Graph,
|
||||
pub outcome: Result<Outcome, FabroError>,
|
||||
pub run_options: RunOptions,
|
||||
pub run_store: SlateRunStore,
|
||||
pub run_store: RunDatabase,
|
||||
pub hook_runner: Option<Arc<HookRunner>>,
|
||||
pub emitter: Arc<Emitter>,
|
||||
pub sandbox: Arc<dyn Sandbox>,
|
||||
|
|
@ -335,7 +335,7 @@ pub struct TransformOptions {
|
|||
/// Options for the RETRO phase.
|
||||
pub struct RetroOptions {
|
||||
pub run_id: RunId,
|
||||
pub run_store: SlateRunStore,
|
||||
pub run_store: RunDatabase,
|
||||
pub workflow_name: String,
|
||||
pub goal: String,
|
||||
pub run_dir: PathBuf,
|
||||
|
|
@ -353,7 +353,7 @@ pub struct RetroOptions {
|
|||
pub struct FinalizeOptions {
|
||||
pub run_dir: PathBuf,
|
||||
pub run_id: RunId,
|
||||
pub run_store: SlateRunStore,
|
||||
pub run_store: RunDatabase,
|
||||
pub workflow_name: String,
|
||||
pub hook_runner: Option<Arc<HookRunner>>,
|
||||
pub preserve_sandbox: bool,
|
||||
|
|
@ -363,7 +363,7 @@ pub struct FinalizeOptions {
|
|||
/// Options for the PULL_REQUEST phase.
|
||||
pub struct PullRequestOptions {
|
||||
pub run_dir: PathBuf,
|
||||
pub run_store: SlateRunStore,
|
||||
pub run_store: RunDatabase,
|
||||
pub pr_config: Option<PullRequestSettings>,
|
||||
pub github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
pub origin_url: Option<String>,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::io::Write;
|
|||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use fabro_store::{RunProjection, SlateRunStore};
|
||||
use fabro_store::{ArtifactStore, RunDatabase, RunProjection};
|
||||
|
||||
use crate::git::MetadataStore;
|
||||
|
||||
|
|
@ -116,10 +116,15 @@ impl RunDump {
|
|||
dump
|
||||
}
|
||||
|
||||
pub async fn store_export(run_store: &SlateRunStore, state: &RunProjection) -> Result<Self> {
|
||||
pub async fn store_export(
|
||||
run_store: &RunDatabase,
|
||||
artifact_store: &ArtifactStore,
|
||||
state: &RunProjection,
|
||||
) -> Result<Self> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
if let Some(record) = state.run.as_ref() {
|
||||
let run_record = state.run.as_ref();
|
||||
if let Some(record) = run_record {
|
||||
push_json_entry(&mut entries, "run.json", record);
|
||||
}
|
||||
if let Some(record) = state.start.as_ref() {
|
||||
|
|
@ -218,28 +223,31 @@ impl RunDump {
|
|||
));
|
||||
}
|
||||
|
||||
for asset in run_store.list_all_artifacts().await? {
|
||||
let node_id_segment = validate_single_path_segment("node id", asset.node.node_id())?;
|
||||
let filename_path = validate_relative_path("artifact filename", &asset.filename)?;
|
||||
let data = run_store
|
||||
.get_artifact(&asset.node, &asset.filename)
|
||||
.await?
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"asset {:?} for node {:?} visit {} is missing from the store",
|
||||
asset.filename,
|
||||
asset.node.node_id(),
|
||||
asset.node.visit()
|
||||
)
|
||||
})?;
|
||||
entries.push(RunDumpEntry::bytes_path(
|
||||
&PathBuf::from("artifacts")
|
||||
.join("nodes")
|
||||
.join(node_id_segment)
|
||||
.join(format!("visit-{}", asset.node.visit()))
|
||||
.join(filename_path),
|
||||
data.to_vec(),
|
||||
));
|
||||
if let Some(run_record) = run_record {
|
||||
for asset in artifact_store.list_for_run(&run_record.run_id).await? {
|
||||
let node_id_segment =
|
||||
validate_single_path_segment("node id", asset.node.node_id())?;
|
||||
let filename_path = validate_relative_path("artifact filename", &asset.filename)?;
|
||||
let data = artifact_store
|
||||
.get(&run_record.run_id, &asset.node, &asset.filename)
|
||||
.await?
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"asset {:?} for node {:?} visit {} is missing from the store",
|
||||
asset.filename,
|
||||
asset.node.node_id(),
|
||||
asset.node.visit()
|
||||
)
|
||||
})?;
|
||||
entries.push(RunDumpEntry::bytes_path(
|
||||
&PathBuf::from("artifacts")
|
||||
.join("nodes")
|
||||
.join(node_id_segment)
|
||||
.join(format!("visit-{}", asset.node.visit()))
|
||||
.join(filename_path),
|
||||
data.to_vec(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self { entries })
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ use std::path::{Path, PathBuf};
|
|||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use chrono::{DateTime, Utc};
|
||||
use fabro_store::{RunSummary, SlateStore};
|
||||
use fabro_config::{Home, Storage};
|
||||
use fabro_store::{Database, RunSummary};
|
||||
use fabro_types::RunId;
|
||||
use serde::Serialize;
|
||||
|
||||
|
|
@ -126,22 +127,12 @@ fn empty_labels() -> &'static HashMap<String, String> {
|
|||
EMPTY.get_or_init(HashMap::new)
|
||||
}
|
||||
|
||||
pub fn default_storage_dir() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.expect("could not determine home directory")
|
||||
.join(".fabro")
|
||||
pub fn scratch_base(storage_dir: &Path) -> PathBuf {
|
||||
Storage::new(storage_dir).scratch_dir()
|
||||
}
|
||||
|
||||
pub fn logs_base(storage_dir: &Path) -> PathBuf {
|
||||
storage_dir.join("logs")
|
||||
}
|
||||
|
||||
pub fn runs_base(storage_dir: &Path) -> PathBuf {
|
||||
storage_dir.join("runs")
|
||||
}
|
||||
|
||||
pub fn default_runs_base() -> PathBuf {
|
||||
runs_base(&default_storage_dir())
|
||||
pub fn default_scratch_base() -> PathBuf {
|
||||
scratch_base(Home::from_env().root())
|
||||
}
|
||||
|
||||
fn scan_orphan_runs(base: &Path) -> Result<Vec<RunInfo>> {
|
||||
|
|
@ -194,7 +185,7 @@ fn scan_orphan_runs(base: &Path) -> Result<Vec<RunInfo>> {
|
|||
Ok(runs)
|
||||
}
|
||||
|
||||
pub async fn scan_runs_combined(store: &SlateStore, base: &Path) -> Result<Vec<RunInfo>> {
|
||||
pub async fn scan_runs_combined(store: &Database, base: &Path) -> Result<Vec<RunInfo>> {
|
||||
let store_runs = store
|
||||
.list_runs(&fabro_store::ListRunsQuery::default())
|
||||
.await
|
||||
|
|
@ -232,8 +223,8 @@ pub fn scan_runs_with_summaries(summaries: &[RunSummary], base: &Path) -> Result
|
|||
Ok(runs)
|
||||
}
|
||||
|
||||
fn run_info_from_summary(summary: &RunSummary, runs_base: &Path) -> Option<RunInfo> {
|
||||
let path = make_run_dir(runs_base, &summary.run_id);
|
||||
fn run_info_from_summary(summary: &RunSummary, scratch_base: &Path) -> Option<RunInfo> {
|
||||
let path = make_run_dir(scratch_base, &summary.run_id);
|
||||
if !path.exists() {
|
||||
return None;
|
||||
}
|
||||
|
|
@ -305,7 +296,7 @@ pub fn filter_runs(
|
|||
}
|
||||
|
||||
pub async fn resolve_run_combined(
|
||||
store: &SlateStore,
|
||||
store: &Database,
|
||||
base: &Path,
|
||||
identifier: &str,
|
||||
) -> Result<RunInfo> {
|
||||
|
|
@ -399,7 +390,7 @@ mod tests {
|
|||
use std::time::Duration;
|
||||
|
||||
use fabro_graphviz::graph::Graph;
|
||||
use fabro_store::{SlateStore, StoreHandle};
|
||||
use fabro_store::Database;
|
||||
use fabro_types::{RunStatus, Settings, fixtures};
|
||||
use object_store::memory::InMemory;
|
||||
|
||||
|
|
@ -408,8 +399,8 @@ mod tests {
|
|||
use crate::operations::make_run_dir;
|
||||
use crate::records::RunRecord;
|
||||
|
||||
fn memory_store() -> StoreHandle {
|
||||
Arc::new(SlateStore::new(
|
||||
fn memory_store() -> Arc<Database> {
|
||||
Arc::new(Database::new(
|
||||
Arc::new(InMemory::new()),
|
||||
"",
|
||||
Duration::from_millis(1),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use std::time::Duration;
|
|||
|
||||
use fabro_agent::Sandbox;
|
||||
use fabro_graphviz::graph::Graph as GvGraph;
|
||||
use fabro_store::{RunProjection, SlateStore};
|
||||
use fabro_store::{Database, RunProjection};
|
||||
use object_store::local::LocalFileSystem;
|
||||
|
||||
use crate::error::{FabroError, Result};
|
||||
|
|
@ -51,7 +51,7 @@ async fn initialized(
|
|||
run_options.run_id.to_string(),
|
||||
)
|
||||
.expect("failed to write run id marker");
|
||||
let store = Arc::new(SlateStore::new(
|
||||
let store = Arc::new(Database::new(
|
||||
Arc::new(
|
||||
LocalFileSystem::new_with_prefix(run_options.run_dir.join("store"))
|
||||
.expect("failed to create local test run store"),
|
||||
|
|
|
|||
|
|
@ -18,10 +18,11 @@ use std::path::Path;
|
|||
use std::sync::Arc;
|
||||
|
||||
use fabro_agent::Sandbox;
|
||||
use fabro_config::RunScratch;
|
||||
use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
|
||||
use fabro_llm::provider::Provider;
|
||||
use fabro_sandbox::daytona::{DaytonaConfig, DaytonaSandbox, DaytonaSnapshotConfig};
|
||||
use fabro_store::{RuntimeState, SlateStore};
|
||||
use fabro_store::Database;
|
||||
use fabro_types::{RunId, Settings};
|
||||
use fabro_workflow::artifact::sync_artifacts_to_env;
|
||||
use fabro_workflow::context::Context;
|
||||
|
|
@ -70,7 +71,7 @@ fn load_checkpoint(path: &Path) -> Result<Checkpoint, Box<dyn std::error::Error>
|
|||
(storage_dir.join("store"), run_id)
|
||||
};
|
||||
let object_store = Arc::new(LocalFileSystem::new_with_prefix(store_dir)?);
|
||||
let store = Arc::new(SlateStore::new(
|
||||
let store = Arc::new(Database::new(
|
||||
object_store,
|
||||
"",
|
||||
std::time::Duration::from_millis(1),
|
||||
|
|
@ -1346,7 +1347,7 @@ async fn daytona_asset_collection() {
|
|||
.expect("pipeline should succeed");
|
||||
assert_eq!(outcome.status, StageStatus::Success);
|
||||
|
||||
let artifacts_dir = RuntimeState::new(dir.path()).artifact_stage_dir("create_assets", 1);
|
||||
let artifacts_dir = RunScratch::new(dir.path()).artifact_stage_dir("create_assets", 1);
|
||||
|
||||
let report_path = artifacts_dir.join("test-results/report.xml");
|
||||
assert!(
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ use std::path::Path;
|
|||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use fabro_config::RunScratch;
|
||||
use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
|
||||
use fabro_graphviz::parser::parse;
|
||||
use fabro_interview::{
|
||||
|
|
@ -24,7 +25,7 @@ use fabro_interview::{
|
|||
QueueInterviewer, RecordingInterviewer,
|
||||
};
|
||||
use fabro_llm::provider::Provider;
|
||||
use fabro_store::{RuntimeState, SlateStore};
|
||||
use fabro_store::Database;
|
||||
use fabro_types::{RunEvent, RunId, Settings};
|
||||
use fabro_validate::{Severity, validate, validate_or_raise};
|
||||
use fabro_workflow::context::Context;
|
||||
|
|
@ -90,7 +91,7 @@ fn load_checkpoint(path: &Path) -> Result<Checkpoint, Box<dyn std::error::Error>
|
|||
(storage_dir.join("store"), run_id)
|
||||
};
|
||||
let object_store = Arc::new(LocalFileSystem::new_with_prefix(store_dir)?);
|
||||
let store = Arc::new(SlateStore::new(object_store, "", Duration::from_millis(1)));
|
||||
let store = Arc::new(Database::new(object_store, "", Duration::from_millis(1)));
|
||||
let state = if tokio::runtime::Handle::try_current().is_ok() {
|
||||
std::thread::spawn(
|
||||
move || -> Result<_, Box<dyn std::error::Error + Send + Sync>> {
|
||||
|
|
@ -8599,14 +8600,13 @@ async fn large_context_values_are_offloaded_to_artifact_store() {
|
|||
);
|
||||
|
||||
let expected_blob_id = fabro_types::RunBlobId::new(
|
||||
&run_options.run_id,
|
||||
&serde_json::to_vec(&serde_json::json!("x".repeat(150 * 1024)))
|
||||
.expect("large value should serialize"),
|
||||
);
|
||||
|
||||
// The artifact file should exist on disk
|
||||
let artifact_file = RuntimeState::new(dir.path())
|
||||
.artifact_values_dir()
|
||||
let artifact_file = RunScratch::new(dir.path())
|
||||
.blob_cache_dir()
|
||||
.join(format!("{expected_blob_id}.json"));
|
||||
assert!(
|
||||
artifact_file.exists(),
|
||||
|
|
@ -12453,7 +12453,7 @@ async fn asset_collection_local_sandbox_success() {
|
|||
assert_eq!(outcome.status, StageStatus::Success);
|
||||
|
||||
// Check that artifact files were collected into the stage directory
|
||||
let artifacts_dir = RuntimeState::new(run_dir.path()).artifact_stage_dir("create_assets", 1);
|
||||
let artifacts_dir = RunScratch::new(run_dir.path()).artifact_stage_dir("create_assets", 1);
|
||||
|
||||
let report_path = artifacts_dir.join("test-results/report.xml");
|
||||
assert!(
|
||||
|
|
@ -12573,7 +12573,7 @@ async fn asset_collection_local_sandbox_on_failure() {
|
|||
// Assets should still be collected regardless of intermediate node failures.
|
||||
assert_eq!(outcome.status, StageStatus::Success);
|
||||
|
||||
let artifacts_dir = RuntimeState::new(run_dir.path()).artifact_stage_dir("create_assets", 1);
|
||||
let artifacts_dir = RunScratch::new(run_dir.path()).artifact_stage_dir("create_assets", 1);
|
||||
|
||||
let report_path = artifacts_dir.join("test-results/report.xml");
|
||||
assert!(
|
||||
|
|
@ -12662,7 +12662,7 @@ async fn asset_collection_docker_sandbox() {
|
|||
.expect("pipeline should succeed");
|
||||
assert_eq!(outcome.status, StageStatus::Success);
|
||||
|
||||
let artifacts_dir = RuntimeState::new(run_dir.path()).artifact_stage_dir("create_assets", 1);
|
||||
let artifacts_dir = RunScratch::new(run_dir.path()).artifact_stage_dir("create_assets", 1);
|
||||
|
||||
let report_path = artifacts_dir.join("test-results/report.xml");
|
||||
assert!(
|
||||
|
|
|
|||
|
|
@ -146,6 +146,8 @@ models/retro-list-item.ts
|
|||
models/retro-stats.ts
|
||||
models/root-response-urls.ts
|
||||
models/root-response.ts
|
||||
models/run-artifact-entry.ts
|
||||
models/run-artifact-list-response.ts
|
||||
models/run-checkpoint.ts
|
||||
models/run-error.ts
|
||||
models/run-event.ts
|
||||
|
|
@ -166,6 +168,8 @@ models/run-timings.ts
|
|||
models/run-usage.ts
|
||||
models/run-verification-control.ts
|
||||
models/run-verification.ts
|
||||
models/sandbox-file-entry.ts
|
||||
models/sandbox-file-list-response.ts
|
||||
models/sandbox-resources.ts
|
||||
models/sandbox-settings.ts
|
||||
models/save-query-request.ts
|
||||
|
|
@ -184,6 +188,8 @@ models/sibling-control.ts
|
|||
models/signoff-status.ts
|
||||
models/signoff.ts
|
||||
models/smoothness-rating.ts
|
||||
models/ssh-access-request.ts
|
||||
models/ssh-access-response.ts
|
||||
models/stage-retro.ts
|
||||
models/stage-status.ts
|
||||
models/stage-turn.ts
|
||||
|
|
|
|||
|
|
@ -30,6 +30,12 @@ import type { PreviewUrlRequest } from '../models';
|
|||
// @ts-ignore
|
||||
import type { PreviewUrlResponse } from '../models';
|
||||
// @ts-ignore
|
||||
import type { SandboxFileListResponse } from '../models';
|
||||
// @ts-ignore
|
||||
import type { SshAccessRequest } from '../models';
|
||||
// @ts-ignore
|
||||
import type { SshAccessResponse } from '../models';
|
||||
// @ts-ignore
|
||||
import type { SteerRequest } from '../models';
|
||||
// @ts-ignore
|
||||
import type { SubmitAnswerRequest } from '../models';
|
||||
|
|
@ -39,7 +45,53 @@ import type { SubmitAnswerRequest } from '../models';
|
|||
export const HumanInTheLoopApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
* Generates a time-limited preview URL for a port exposed by the run\'s sandbox environment.
|
||||
* Creates a time-limited SSH command for the run\'s sandbox environment.
|
||||
* @summary SSH Access
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {SshAccessRequest} sshAccessRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createRunSshAccess: async (id: string, sshAccessRequest: SshAccessRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('createRunSshAccess', 'id', id)
|
||||
// verify required parameter 'sshAccessRequest' is not null or undefined
|
||||
assertParamExists('createRunSshAccess', 'sshAccessRequest', sshAccessRequest)
|
||||
const localVarPath = `/api/v1/runs/{id}/ssh`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication mTLS required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "X-mTLS-Client-CN", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(sshAccessRequest, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Generates a preview URL for a port exposed by the run\'s sandbox environment.
|
||||
* @summary Preview URL
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {PreviewUrlRequest} previewUrlRequest
|
||||
|
|
@ -84,6 +136,54 @@ export const HumanInTheLoopApiAxiosParamCreator = function (configuration?: Conf
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Downloads a file from the run\'s sandbox environment.
|
||||
* @summary Download Sandbox File
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} path
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getSandboxFile: async (id: string, path: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('getSandboxFile', 'id', id)
|
||||
// verify required parameter 'path' is not null or undefined
|
||||
assertParamExists('getSandboxFile', 'path', path)
|
||||
const localVarPath = `/api/v1/runs/{id}/sandbox/file`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication mTLS required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "X-mTLS-Client-CN", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (path !== undefined) {
|
||||
localVarQueryParameter['path'] = path;
|
||||
}
|
||||
|
||||
localVarHeaderParameter['Accept'] = 'application/octet-stream,application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Returns pending human-in-the-loop questions for a run. Questions are generated when the workflow needs user input to proceed.
|
||||
* @summary List Run Questions
|
||||
|
|
@ -135,6 +235,112 @@ export const HumanInTheLoopApiAxiosParamCreator = function (configuration?: Conf
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Lists directory entries from the run\'s sandbox environment.
|
||||
* @summary List Sandbox Files
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} path
|
||||
* @param {number} [depth]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listSandboxFiles: async (id: string, path: string, depth?: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('listSandboxFiles', 'id', id)
|
||||
// verify required parameter 'path' is not null or undefined
|
||||
assertParamExists('listSandboxFiles', 'path', path)
|
||||
const localVarPath = `/api/v1/runs/{id}/sandbox/files`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication mTLS required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "X-mTLS-Client-CN", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (path !== undefined) {
|
||||
localVarQueryParameter['path'] = path;
|
||||
}
|
||||
|
||||
if (depth !== undefined) {
|
||||
localVarQueryParameter['depth'] = depth;
|
||||
}
|
||||
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Uploads a file into the run\'s sandbox environment.
|
||||
* @summary Upload Sandbox File
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} path
|
||||
* @param {File} body
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
putSandboxFile: async (id: string, path: string, body: File, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('putSandboxFile', 'id', id)
|
||||
// verify required parameter 'path' is not null or undefined
|
||||
assertParamExists('putSandboxFile', 'path', path)
|
||||
// verify required parameter 'body' is not null or undefined
|
||||
assertParamExists('putSandboxFile', 'body', body)
|
||||
const localVarPath = `/api/v1/runs/{id}/sandbox/file`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication mTLS required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "X-mTLS-Client-CN", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (path !== undefined) {
|
||||
localVarQueryParameter['path'] = path;
|
||||
}
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/octet-stream';
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(body, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Sends inline guidance to a running agent, targeting a specific file and line. The guidance is delivered asynchronously.
|
||||
* @summary Steer Run
|
||||
|
|
@ -241,7 +447,21 @@ export const HumanInTheLoopApiFp = function(configuration?: Configuration) {
|
|||
const localVarAxiosParamCreator = HumanInTheLoopApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
* Generates a time-limited preview URL for a port exposed by the run\'s sandbox environment.
|
||||
* Creates a time-limited SSH command for the run\'s sandbox environment.
|
||||
* @summary SSH Access
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {SshAccessRequest} sshAccessRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async createRunSshAccess(id: string, sshAccessRequest: SshAccessRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SshAccessResponse>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.createRunSshAccess(id, sshAccessRequest, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['HumanInTheLoopApi.createRunSshAccess']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Generates a preview URL for a port exposed by the run\'s sandbox environment.
|
||||
* @summary Preview URL
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {PreviewUrlRequest} previewUrlRequest
|
||||
|
|
@ -254,6 +474,20 @@ export const HumanInTheLoopApiFp = function(configuration?: Configuration) {
|
|||
const localVarOperationServerBasePath = operationServerMap['HumanInTheLoopApi.generatePreviewUrl']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Downloads a file from the run\'s sandbox environment.
|
||||
* @summary Download Sandbox File
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} path
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getSandboxFile(id: string, path: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<File>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getSandboxFile(id, path, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['HumanInTheLoopApi.getSandboxFile']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Returns pending human-in-the-loop questions for a run. Questions are generated when the workflow needs user input to proceed.
|
||||
* @summary List Run Questions
|
||||
|
|
@ -269,6 +503,36 @@ export const HumanInTheLoopApiFp = function(configuration?: Configuration) {
|
|||
const localVarOperationServerBasePath = operationServerMap['HumanInTheLoopApi.listRunQuestions']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Lists directory entries from the run\'s sandbox environment.
|
||||
* @summary List Sandbox Files
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} path
|
||||
* @param {number} [depth]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listSandboxFiles(id: string, path: string, depth?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SandboxFileListResponse>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listSandboxFiles(id, path, depth, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['HumanInTheLoopApi.listSandboxFiles']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Uploads a file into the run\'s sandbox environment.
|
||||
* @summary Upload Sandbox File
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} path
|
||||
* @param {File} body
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async putSandboxFile(id: string, path: string, body: File, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.putSandboxFile(id, path, body, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['HumanInTheLoopApi.putSandboxFile']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Sends inline guidance to a running agent, targeting a specific file and line. The guidance is delivered asynchronously.
|
||||
* @summary Steer Run
|
||||
|
|
@ -308,7 +572,18 @@ export const HumanInTheLoopApiFactory = function (configuration?: Configuration,
|
|||
const localVarFp = HumanInTheLoopApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
* Generates a time-limited preview URL for a port exposed by the run\'s sandbox environment.
|
||||
* Creates a time-limited SSH command for the run\'s sandbox environment.
|
||||
* @summary SSH Access
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {SshAccessRequest} sshAccessRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createRunSshAccess(id: string, sshAccessRequest: SshAccessRequest, options?: RawAxiosRequestConfig): AxiosPromise<SshAccessResponse> {
|
||||
return localVarFp.createRunSshAccess(id, sshAccessRequest, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Generates a preview URL for a port exposed by the run\'s sandbox environment.
|
||||
* @summary Preview URL
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {PreviewUrlRequest} previewUrlRequest
|
||||
|
|
@ -318,6 +593,17 @@ export const HumanInTheLoopApiFactory = function (configuration?: Configuration,
|
|||
generatePreviewUrl(id: string, previewUrlRequest: PreviewUrlRequest, options?: RawAxiosRequestConfig): AxiosPromise<PreviewUrlResponse> {
|
||||
return localVarFp.generatePreviewUrl(id, previewUrlRequest, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Downloads a file from the run\'s sandbox environment.
|
||||
* @summary Download Sandbox File
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} path
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getSandboxFile(id: string, path: string, options?: RawAxiosRequestConfig): AxiosPromise<File> {
|
||||
return localVarFp.getSandboxFile(id, path, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Returns pending human-in-the-loop questions for a run. Questions are generated when the workflow needs user input to proceed.
|
||||
* @summary List Run Questions
|
||||
|
|
@ -330,6 +616,30 @@ export const HumanInTheLoopApiFactory = function (configuration?: Configuration,
|
|||
listRunQuestions(id: string, pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedApiQuestionList> {
|
||||
return localVarFp.listRunQuestions(id, pageLimit, pageOffset, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Lists directory entries from the run\'s sandbox environment.
|
||||
* @summary List Sandbox Files
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} path
|
||||
* @param {number} [depth]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listSandboxFiles(id: string, path: string, depth?: number, options?: RawAxiosRequestConfig): AxiosPromise<SandboxFileListResponse> {
|
||||
return localVarFp.listSandboxFiles(id, path, depth, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Uploads a file into the run\'s sandbox environment.
|
||||
* @summary Upload Sandbox File
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} path
|
||||
* @param {File} body
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
putSandboxFile(id: string, path: string, body: File, options?: RawAxiosRequestConfig): AxiosPromise<void> {
|
||||
return localVarFp.putSandboxFile(id, path, body, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Sends inline guidance to a running agent, targeting a specific file and line. The guidance is delivered asynchronously.
|
||||
* @summary Steer Run
|
||||
|
|
@ -361,7 +671,19 @@ export const HumanInTheLoopApiFactory = function (configuration?: Configuration,
|
|||
*/
|
||||
export class HumanInTheLoopApi extends BaseAPI {
|
||||
/**
|
||||
* Generates a time-limited preview URL for a port exposed by the run\'s sandbox environment.
|
||||
* Creates a time-limited SSH command for the run\'s sandbox environment.
|
||||
* @summary SSH Access
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {SshAccessRequest} sshAccessRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public createRunSshAccess(id: string, sshAccessRequest: SshAccessRequest, options?: RawAxiosRequestConfig) {
|
||||
return HumanInTheLoopApiFp(this.configuration).createRunSshAccess(id, sshAccessRequest, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a preview URL for a port exposed by the run\'s sandbox environment.
|
||||
* @summary Preview URL
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {PreviewUrlRequest} previewUrlRequest
|
||||
|
|
@ -372,6 +694,18 @@ export class HumanInTheLoopApi extends BaseAPI {
|
|||
return HumanInTheLoopApiFp(this.configuration).generatePreviewUrl(id, previewUrlRequest, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a file from the run\'s sandbox environment.
|
||||
* @summary Download Sandbox File
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} path
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public getSandboxFile(id: string, path: string, options?: RawAxiosRequestConfig) {
|
||||
return HumanInTheLoopApiFp(this.configuration).getSandboxFile(id, path, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns pending human-in-the-loop questions for a run. Questions are generated when the workflow needs user input to proceed.
|
||||
* @summary List Run Questions
|
||||
|
|
@ -385,6 +719,32 @@ export class HumanInTheLoopApi extends BaseAPI {
|
|||
return HumanInTheLoopApiFp(this.configuration).listRunQuestions(id, pageLimit, pageOffset, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists directory entries from the run\'s sandbox environment.
|
||||
* @summary List Sandbox Files
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} path
|
||||
* @param {number} [depth]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public listSandboxFiles(id: string, path: string, depth?: number, options?: RawAxiosRequestConfig) {
|
||||
return HumanInTheLoopApiFp(this.configuration).listSandboxFiles(id, path, depth, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file into the run\'s sandbox environment.
|
||||
* @summary Upload Sandbox File
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} path
|
||||
* @param {File} body
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public putSandboxFile(id: string, path: string, body: File, options?: RawAxiosRequestConfig) {
|
||||
return HumanInTheLoopApiFp(this.configuration).putSandboxFile(id, path, body, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends inline guidance to a running agent, targeting a specific file and line. The guidance is delivered asynchronously.
|
||||
* @summary Steer Run
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ import type { PaginatedRunStageList } from '../models';
|
|||
// @ts-ignore
|
||||
import type { PaginatedStageTurnList } from '../models';
|
||||
// @ts-ignore
|
||||
import type { RunArtifactListResponse } from '../models';
|
||||
// @ts-ignore
|
||||
import type { RunCheckpoint } from '../models';
|
||||
// @ts-ignore
|
||||
import type { RunEvent } from '../models';
|
||||
|
|
@ -186,7 +188,7 @@ export const RunInternalsApiAxiosParamCreator = function (configuration?: Config
|
|||
* @summary Get Stage Artifact
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} stageId Identifier of a stage within a run\'s workflow graph, serialized as `node_id@visit`.
|
||||
* @param {string} filename Artifact filename. May contain path separators.
|
||||
* @param {string} filename Relative artifact path. `/` is allowed as a path separator. Backslash, empty segments, and traversal segments (`.` and `..`) are invalid.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
|
|
@ -233,6 +235,47 @@ export const RunInternalsApiAxiosParamCreator = function (configuration?: Config
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Lists captured artifact files for a run.
|
||||
* @summary List Run Artifacts
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listRunArtifacts: async (id: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('listRunArtifacts', 'id', id)
|
||||
const localVarPath = `/api/v1/runs/{id}/artifacts`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication mTLS required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "X-mTLS-Client-CN", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Returns a paginated JSON list of stored run events.
|
||||
* @summary List Run Events
|
||||
|
|
@ -440,7 +483,7 @@ export const RunInternalsApiAxiosParamCreator = function (configuration?: Config
|
|||
* @summary Put Stage Artifact
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} stageId Identifier of a stage within a run\'s workflow graph, serialized as `node_id@visit`.
|
||||
* @param {string} filename Artifact filename. May contain path separators.
|
||||
* @param {string} filename Relative artifact path. `/` is allowed as a path separator. Backslash, empty segments, and traversal segments (`.` and `..`) are invalid.
|
||||
* @param {File} body
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
|
|
@ -720,7 +763,7 @@ export const RunInternalsApiFp = function(configuration?: Configuration) {
|
|||
* @summary Get Stage Artifact
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} stageId Identifier of a stage within a run\'s workflow graph, serialized as `node_id@visit`.
|
||||
* @param {string} filename Artifact filename. May contain path separators.
|
||||
* @param {string} filename Relative artifact path. `/` is allowed as a path separator. Backslash, empty segments, and traversal segments (`.` and `..`) are invalid.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
|
|
@ -730,6 +773,19 @@ export const RunInternalsApiFp = function(configuration?: Configuration) {
|
|||
const localVarOperationServerBasePath = operationServerMap['RunInternalsApi.getStageArtifact']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Lists captured artifact files for a run.
|
||||
* @summary List Run Artifacts
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listRunArtifacts(id: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RunArtifactListResponse>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listRunArtifacts(id, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['RunInternalsApi.listRunArtifacts']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Returns a paginated JSON list of stored run events.
|
||||
* @summary List Run Events
|
||||
|
|
@ -795,7 +851,7 @@ export const RunInternalsApiFp = function(configuration?: Configuration) {
|
|||
* @summary Put Stage Artifact
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} stageId Identifier of a stage within a run\'s workflow graph, serialized as `node_id@visit`.
|
||||
* @param {string} filename Artifact filename. May contain path separators.
|
||||
* @param {string} filename Relative artifact path. `/` is allowed as a path separator. Backslash, empty segments, and traversal segments (`.` and `..`) are invalid.
|
||||
* @param {File} body
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
|
|
@ -906,13 +962,23 @@ export const RunInternalsApiFactory = function (configuration?: Configuration, b
|
|||
* @summary Get Stage Artifact
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} stageId Identifier of a stage within a run\'s workflow graph, serialized as `node_id@visit`.
|
||||
* @param {string} filename Artifact filename. May contain path separators.
|
||||
* @param {string} filename Relative artifact path. `/` is allowed as a path separator. Backslash, empty segments, and traversal segments (`.` and `..`) are invalid.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getStageArtifact(id: string, stageId: string, filename: string, options?: RawAxiosRequestConfig): AxiosPromise<File> {
|
||||
return localVarFp.getStageArtifact(id, stageId, filename, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Lists captured artifact files for a run.
|
||||
* @summary List Run Artifacts
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listRunArtifacts(id: string, options?: RawAxiosRequestConfig): AxiosPromise<RunArtifactListResponse> {
|
||||
return localVarFp.listRunArtifacts(id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Returns a paginated JSON list of stored run events.
|
||||
* @summary List Run Events
|
||||
|
|
@ -966,7 +1032,7 @@ export const RunInternalsApiFactory = function (configuration?: Configuration, b
|
|||
* @summary Put Stage Artifact
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} stageId Identifier of a stage within a run\'s workflow graph, serialized as `node_id@visit`.
|
||||
* @param {string} filename Artifact filename. May contain path separators.
|
||||
* @param {string} filename Relative artifact path. `/` is allowed as a path separator. Backslash, empty segments, and traversal segments (`.` and `..`) are invalid.
|
||||
* @param {File} body
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
|
|
@ -1063,7 +1129,7 @@ export class RunInternalsApi extends BaseAPI {
|
|||
* @summary Get Stage Artifact
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} stageId Identifier of a stage within a run\'s workflow graph, serialized as `node_id@visit`.
|
||||
* @param {string} filename Artifact filename. May contain path separators.
|
||||
* @param {string} filename Relative artifact path. `/` is allowed as a path separator. Backslash, empty segments, and traversal segments (`.` and `..`) are invalid.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
|
|
@ -1071,6 +1137,17 @@ export class RunInternalsApi extends BaseAPI {
|
|||
return RunInternalsApiFp(this.configuration).getStageArtifact(id, stageId, filename, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists captured artifact files for a run.
|
||||
* @summary List Run Artifacts
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public listRunArtifacts(id: string, options?: RawAxiosRequestConfig) {
|
||||
return RunInternalsApiFp(this.configuration).listRunArtifacts(id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a paginated JSON list of stored run events.
|
||||
* @summary List Run Events
|
||||
|
|
@ -1128,7 +1205,7 @@ export class RunInternalsApi extends BaseAPI {
|
|||
* @summary Put Stage Artifact
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} stageId Identifier of a stage within a run\'s workflow graph, serialized as `node_id@visit`.
|
||||
* @param {string} filename Artifact filename. May contain path separators.
|
||||
* @param {string} filename Relative artifact path. `/` is allowed as a path separator. Backslash, empty segments, and traversal segments (`.` and `..`) are invalid.
|
||||
* @param {File} body
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError
|
|||
// @ts-ignore
|
||||
import type { ErrorResponse } from '../models';
|
||||
// @ts-ignore
|
||||
import type { PaginatedRunFileList } from '../models';
|
||||
// @ts-ignore
|
||||
import type { PaginatedRunVerificationList } from '../models';
|
||||
// @ts-ignore
|
||||
import type { RunUsage } from '../models';
|
||||
|
|
@ -34,62 +32,6 @@ import type { RunUsage } from '../models';
|
|||
*/
|
||||
export const RunOutputsApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
* Returns a paginated list of file-level diffs produced by the run, optionally filtered to a specific checkpoint.
|
||||
* @summary Retrieve Run Files
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} [checkpoint] Filter to a specific checkpoint ID. Omit to include all changes.
|
||||
* @param {number} [pageLimit] Maximum number of items to return per page.
|
||||
* @param {number} [pageOffset] Number of items to skip before returning results.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
retrieveRunFiles: async (id: string, checkpoint?: string, pageLimit?: number, pageOffset?: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('retrieveRunFiles', 'id', id)
|
||||
const localVarPath = `/api/v1/runs/{id}/files`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication mTLS required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "X-mTLS-Client-CN", configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (checkpoint !== undefined) {
|
||||
localVarQueryParameter['checkpoint'] = checkpoint;
|
||||
}
|
||||
|
||||
if (pageLimit !== undefined) {
|
||||
localVarQueryParameter['page[limit]'] = pageLimit;
|
||||
}
|
||||
|
||||
if (pageOffset !== undefined) {
|
||||
localVarQueryParameter['page[offset]'] = pageOffset;
|
||||
}
|
||||
|
||||
localVarHeaderParameter['Accept'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Returns token and cost usage broken down by stage and model for a specific run.
|
||||
* @summary Retrieve Run Usage
|
||||
|
|
@ -191,22 +133,6 @@ export const RunOutputsApiAxiosParamCreator = function (configuration?: Configur
|
|||
export const RunOutputsApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = RunOutputsApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
* Returns a paginated list of file-level diffs produced by the run, optionally filtered to a specific checkpoint.
|
||||
* @summary Retrieve Run Files
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} [checkpoint] Filter to a specific checkpoint ID. Omit to include all changes.
|
||||
* @param {number} [pageLimit] Maximum number of items to return per page.
|
||||
* @param {number} [pageOffset] Number of items to skip before returning results.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async retrieveRunFiles(id: string, checkpoint?: string, pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedRunFileList>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.retrieveRunFiles(id, checkpoint, pageLimit, pageOffset, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['RunOutputsApi.retrieveRunFiles']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Returns token and cost usage broken down by stage and model for a specific run.
|
||||
* @summary Retrieve Run Usage
|
||||
|
|
@ -244,19 +170,6 @@ export const RunOutputsApiFp = function(configuration?: Configuration) {
|
|||
export const RunOutputsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = RunOutputsApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
* Returns a paginated list of file-level diffs produced by the run, optionally filtered to a specific checkpoint.
|
||||
* @summary Retrieve Run Files
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} [checkpoint] Filter to a specific checkpoint ID. Omit to include all changes.
|
||||
* @param {number} [pageLimit] Maximum number of items to return per page.
|
||||
* @param {number} [pageOffset] Number of items to skip before returning results.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
retrieveRunFiles(id: string, checkpoint?: string, pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedRunFileList> {
|
||||
return localVarFp.retrieveRunFiles(id, checkpoint, pageLimit, pageOffset, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Returns token and cost usage broken down by stage and model for a specific run.
|
||||
* @summary Retrieve Run Usage
|
||||
|
|
@ -286,20 +199,6 @@ export const RunOutputsApiFactory = function (configuration?: Configuration, bas
|
|||
* RunOutputsApi - object-oriented interface
|
||||
*/
|
||||
export class RunOutputsApi extends BaseAPI {
|
||||
/**
|
||||
* Returns a paginated list of file-level diffs produced by the run, optionally filtered to a specific checkpoint.
|
||||
* @summary Retrieve Run Files
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} [checkpoint] Filter to a specific checkpoint ID. Omit to include all changes.
|
||||
* @param {number} [pageLimit] Maximum number of items to return per page.
|
||||
* @param {number} [pageOffset] Number of items to skip before returning results.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public retrieveRunFiles(id: string, checkpoint?: string, pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig) {
|
||||
return RunOutputsApiFp(this.configuration).retrieveRunFiles(id, checkpoint, pageLimit, pageOffset, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns token and cost usage broken down by stage and model for a specific run.
|
||||
* @summary Retrieve Run Usage
|
||||
|
|
|
|||
|
|
@ -124,6 +124,8 @@ export * from './retro-list-item';
|
|||
export * from './retro-stats';
|
||||
export * from './root-response';
|
||||
export * from './root-response-urls';
|
||||
export * from './run-artifact-entry';
|
||||
export * from './run-artifact-list-response';
|
||||
export * from './run-checkpoint';
|
||||
export * from './run-error';
|
||||
export * from './run-event';
|
||||
|
|
@ -144,6 +146,8 @@ export * from './run-timings';
|
|||
export * from './run-usage';
|
||||
export * from './run-verification';
|
||||
export * from './run-verification-control';
|
||||
export * from './sandbox-file-entry';
|
||||
export * from './sandbox-file-list-response';
|
||||
export * from './sandbox-resources';
|
||||
export * from './sandbox-settings';
|
||||
export * from './save-query-request';
|
||||
|
|
@ -162,6 +166,8 @@ export * from './sibling-control';
|
|||
export * from './signoff';
|
||||
export * from './signoff-status';
|
||||
export * from './smoothness-rating';
|
||||
export * from './ssh-access-request';
|
||||
export * from './ssh-access-response';
|
||||
export * from './stage-retro';
|
||||
export * from './stage-status';
|
||||
export * from './stage-turn';
|
||||
|
|
|
|||
|
|
@ -26,5 +26,9 @@ export interface PreviewUrlRequest {
|
|||
* Time-to-live for the preview URL in seconds.
|
||||
*/
|
||||
'expires_in_secs': number;
|
||||
/**
|
||||
* When true, return a signed URL that does not require a preview token header.
|
||||
*/
|
||||
'signed'?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,12 @@
|
|||
*/
|
||||
export interface PreviewUrlResponse {
|
||||
/**
|
||||
* Time-limited preview URL.
|
||||
* Preview URL.
|
||||
*/
|
||||
'url': string;
|
||||
/**
|
||||
* Preview token header value for unsigned preview URLs.
|
||||
*/
|
||||
'token'?: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A captured artifact file for a run.
|
||||
*/
|
||||
export interface RunArtifactEntry {
|
||||
/**
|
||||
* Stage ID in `node@visit` form.
|
||||
*/
|
||||
'stage_id': string;
|
||||
/**
|
||||
* Node slug that produced the artifact.
|
||||
*/
|
||||
'node_slug': string;
|
||||
/**
|
||||
* Retry attempt number.
|
||||
*/
|
||||
'retry': number;
|
||||
/**
|
||||
* Artifact path relative to the stage artifact capture directory.
|
||||
*/
|
||||
'relative_path': string;
|
||||
/**
|
||||
* Artifact size in bytes.
|
||||
*/
|
||||
'size': number;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { RunArtifactEntry } from './run-artifact-entry';
|
||||
|
||||
/**
|
||||
* List of captured artifact files for a run.
|
||||
*/
|
||||
export interface RunArtifactListResponse {
|
||||
'data': Array<RunArtifactEntry>;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A directory entry in a run sandbox.
|
||||
*/
|
||||
export interface SandboxFileEntry {
|
||||
/**
|
||||
* Basename of the entry.
|
||||
*/
|
||||
'name': string;
|
||||
/**
|
||||
* Whether the entry is a directory.
|
||||
*/
|
||||
'is_dir': boolean;
|
||||
/**
|
||||
* File size in bytes when known.
|
||||
*/
|
||||
'size'?: number;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { SandboxFileEntry } from './sandbox-file-entry';
|
||||
|
||||
/**
|
||||
* Non-paginated list of sandbox directory entries.
|
||||
*/
|
||||
export interface SandboxFileListResponse {
|
||||
'data': Array<SandboxFileEntry>;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Request body for creating SSH access for a sandbox-backed run.
|
||||
*/
|
||||
export interface SshAccessRequest {
|
||||
/**
|
||||
* Time-to-live for the SSH command in minutes.
|
||||
*/
|
||||
'ttl_minutes': number;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Response containing an SSH command for the sandbox.
|
||||
*/
|
||||
export interface SshAccessResponse {
|
||||
/**
|
||||
* SSH command to connect to the sandbox.
|
||||
*/
|
||||
'command': string;
|
||||
}
|
||||
|
||||
Loading…
Add table
Reference in a new issue