feat(api): bind sessions to runs (#336)

## Summary

Ask Fabro sessions are now run-bound instead of standalone. Sessions are
created under their owning run, then accessed by flat session ID routes,
with durable state projected from the run event stream rather than a
separate session store.

## Changes

- Move session creation/listing to `POST/GET /api/v1/runs/{id}/sessions`
while keeping flat session reads, turns, interrupts, and event streams
under `/api/v1/sessions/{id}/...`.
- Add typed `run.session.*` events, ULID-backed session/turn IDs,
read-only default permissions, and a rebuildable SlateDB `session_id ->
run_id` index.
- Remove the old file-backed session store and wire the server, runtime,
Rust client, generated API crates, and TypeScript client around run
event projections.
- Replace the old top-level CLI session command with `fabro run ask` for
chatting with a run.
- Regenerate the TypeScript API client; this also catches up existing
generated models for Pair/run event detail schemas already present in
the OpenAPI spec.

## Validation

- `cargo build -p fabro-api -p fabro-client -p fabro-server -p
fabro-cli`
- `cargo nextest run -p fabro-server --features test-support -E
'test(run_bound_session_is_created_as_run_event_and_resolves_by_flat_id)
| test(sessions_are_listed_only_under_their_owning_run)'`
- `cargo nextest run -p fabro-store
projection_rebuilds_runtime_context_from_run_events`
- `cargo +nightly-2026-04-14 clippy -p fabro-api -p fabro-client -p
fabro-store -p fabro-server -p fabro-cli --all-targets -- -D warnings`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cd lib/packages/fabro-api-client && bun run typecheck && cd
../../../apps/fabro-web && bun run typecheck`
- `git diff --check`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
Generated with GPT-5 via [Codex](https://openai.com/codex)
This commit is contained in:
Bryan Helmkamp 2026-05-21 10:53:41 -04:00 • committed by GitHub
parent 02fabb7b47
commit 86b1fbef7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 3297 additions and 3842 deletions

View file

@ -53,5 +53,5 @@ Demo mode implements every API endpoint. Read endpoints return static data repre
## Limitations
- **No state changes.** Write operations return a success response but nothing is persisted. Creating a run returns a fixed ID; it won't appear in subsequent list calls.
- **No SSE streaming.** Event stream endpoints (`/api/v1/runs/{id}/events`, `/api/v1/sessions/{id}/events`) return immediately rather than streaming.
- **No SSE streaming.** Event stream endpoints such as `/api/v1/runs/{id}/events` return immediately rather than streaming.
- **Static data only.** The same data is returned regardless of path parameters — requesting any run ID returns the same run detail.

View file

@ -16,7 +16,7 @@ tags:
- name: Runs
description: Run management operations
- name: Sessions
description: Persistent Fabro agent sessions
description: Ask Fabro sessions bound to runs
- name: Human-in-the-Loop
description: Questions, answers, and steering for runs
- name: Run Outputs
@ -815,28 +815,31 @@ paths:
# ── Sessions ──────────────────────────────────────────────────────────
/api/v1/sessions:
/api/v1/runs/{id}/sessions:
parameters:
- $ref: "#/components/parameters/RunId"
get:
operationId: listSessions
operationId: listRunSessions
tags: [Sessions]
summary: List sessions
summary: List run sessions
responses:
"200":
description: Durable session summaries
description: Ask Fabro sessions for the run
content:
application/json:
schema:
$ref: "#/components/schemas/PaginatedSessionList"
post:
operationId: createSession
operationId: createRunSession
tags: [Sessions]
summary: Create session
summary: Create run session
description: Creates a read-only Ask Fabro session bound to the run.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateSessionRequest"
$ref: "#/components/schemas/CreateRunSessionRequest"
responses:
"201":
description: Session created
@ -881,31 +884,6 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
patch:
operationId: updateSession
tags: [Sessions]
summary: Update session
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateSessionRequest"
responses:
"200":
description: Updated session record
content:
application/json:
schema:
$ref: "#/components/schemas/SessionRecord"
delete:
operationId: deleteSession
tags: [Sessions]
summary: Delete session
responses:
"204":
description: Session deleted
/api/v1/sessions/{id}/turns:
parameters:
- name: id
@ -913,17 +891,6 @@ paths:
required: true
schema:
$ref: "#/components/schemas/SessionId"
get:
operationId: listSessionTurns
tags: [Sessions]
summary: List session turns
responses:
"200":
description: Session turns
content:
application/json:
schema:
$ref: "#/components/schemas/PaginatedTurnList"
post:
operationId: submitSessionTurn
tags: [Sessions]
@ -970,39 +937,6 @@ paths:
schema:
$ref: "#/components/schemas/ErrorResponse"
/api/v1/sessions/{id}/turns/{turnId}:
parameters:
- name: id
in: path
required: true
schema:
$ref: "#/components/schemas/SessionId"
- name: turnId
in: path
required: true
schema:
$ref: "#/components/schemas/TurnId"
get:
operationId: getSessionTurn
tags: [Sessions]
summary: Get session turn
responses:
"200":
description: Turn record
content:
application/json:
schema:
$ref: "#/components/schemas/TurnRecord"
"404":
description: Turn not found
headers:
x-request-id:
$ref: "#/components/headers/XRequestId"
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/api/v1/sessions/{id}/turns/{turnId}/interrupt:
parameters:
- name: id
@ -1025,9 +959,9 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/SessionEventEnvelope"
$ref: "#/components/schemas/EventEnvelope"
"404":
description: Session turn not found
description: Session not found
headers:
x-request-id:
$ref: "#/components/headers/XRequestId"
@ -1036,7 +970,7 @@ paths:
schema:
$ref: "#/components/schemas/ErrorResponse"
"409":
description: Turn is already terminal or is not active for this session
description: Turn is not active for this session
headers:
x-request-id:
$ref: "#/components/headers/XRequestId"
@ -1045,36 +979,6 @@ paths:
schema:
$ref: "#/components/schemas/ErrorResponse"
/api/v1/sessions/{id}/events:
parameters:
- name: id
in: path
required: true
schema:
$ref: "#/components/schemas/SessionId"
get:
operationId: listSessionEvents
tags: [Sessions]
summary: List or stream session events
description: Returns JSON replay by default. When the `Accept` header includes `text/event-stream`, replays durable events from `since_seq` or `Last-Event-ID`, then stays attached for live events.
parameters:
- name: since_seq
in: query
required: false
schema:
type: integer
minimum: 1
responses:
"200":
description: Durable session events or live SSE stream
content:
application/json:
schema:
$ref: "#/components/schemas/PaginatedSessionEventList"
text/event-stream:
schema:
type: string
# ── Runs ──────────────────────────────────────────────────────────────
/api/v1/runs:
@ -5173,12 +5077,13 @@ components:
discriminator:
propertyName: kind
PairTranscriptBase:
PairTranscriptUserMessage:
type: object
required: [kind, seq, event_id, ts, pair_id, target]
required: [kind, seq, event_id, ts, pair_id, target, message_id, text]
properties:
kind:
type: string
enum: [user_message]
seq:
type: integer
minimum: 1
@ -5191,138 +5096,167 @@ components:
$ref: "#/components/schemas/PairId"
target:
$ref: "#/components/schemas/PairTarget"
PairTranscriptUserMessage:
allOf:
- $ref: "#/components/schemas/PairTranscriptBase"
- type: object
required: [message_id, text]
properties:
kind:
type: string
enum: [user_message]
message_id:
$ref: "#/components/schemas/PairMessageId"
client_message_id:
type: ["string", "null"]
text:
type: string
message_id:
$ref: "#/components/schemas/PairMessageId"
client_message_id:
type: ["string", "null"]
text:
type: string
PairTranscriptSystemMessage:
allOf:
- $ref: "#/components/schemas/PairTranscriptBase"
- type: object
required: [system_message_kind, text]
properties:
kind:
type: string
enum: [system_message]
system_message_kind:
type: string
enum: [human_joined, human_left]
text:
type: string
type: object
required: [kind, seq, event_id, ts, pair_id, target, system_message_kind, text]
properties:
kind:
type: string
enum: [system_message]
seq:
type: integer
minimum: 1
event_id:
type: string
ts:
type: string
format: date-time
pair_id:
$ref: "#/components/schemas/PairId"
target:
$ref: "#/components/schemas/PairTarget"
system_message_kind:
type: string
enum: [human_joined, human_left]
text:
type: string
PairTranscriptAssistantMessage:
allOf:
- $ref: "#/components/schemas/PairTranscriptBase"
- type: object
required: [text, model, tool_call_count]
type: object
required: [kind, seq, event_id, ts, pair_id, target, text, model, tool_call_count]
properties:
kind:
type: string
enum: [assistant_message]
seq:
type: integer
minimum: 1
event_id:
type: string
ts:
type: string
format: date-time
pair_id:
$ref: "#/components/schemas/PairId"
target:
$ref: "#/components/schemas/PairTarget"
text:
type: string
model:
type: object
required: [provider, model_id]
properties:
kind:
provider:
type: string
enum: [assistant_message]
text:
model_id:
type: string
model:
type: object
required: [provider, model_id]
properties:
provider:
type: string
model_id:
type: string
speed:
type: ["string", "null"]
tool_call_count:
type: integer
minimum: 0
speed:
type: ["string", "null"]
tool_call_count:
type: integer
minimum: 0
PairTranscriptToolCall:
allOf:
- $ref: "#/components/schemas/PairTranscriptBase"
- type: object
required: [tool_call_id, tool_name, status, summary, is_error, truncated, detail_ref]
properties:
kind:
type: string
enum: [tool_call]
tool_call_id:
type: string
tool_name:
type: string
status:
type: string
enum: [started, completed]
summary:
type: string
is_error:
type: boolean
truncated:
type: boolean
detail_ref:
type: object
required: [seq]
properties:
seq:
type: integer
minimum: 1
tool_call_id:
type: string
type: object
required: [kind, seq, event_id, ts, pair_id, target, tool_call_id, tool_name, status, summary, is_error, truncated, detail_ref]
properties:
kind:
type: string
enum: [tool_call]
seq:
type: integer
minimum: 1
event_id:
type: string
ts:
type: string
format: date-time
pair_id:
$ref: "#/components/schemas/PairId"
target:
$ref: "#/components/schemas/PairTarget"
tool_call_id:
type: string
tool_name:
type: string
status:
type: string
enum: [started, completed]
summary:
type: string
is_error:
type: boolean
truncated:
type: boolean
detail_ref:
$ref: "#/components/schemas/PairTranscriptDetailRef"
PairTranscriptError:
allOf:
- $ref: "#/components/schemas/PairTranscriptBase"
- type: object
required: [message, detail_ref]
properties:
kind:
type: string
enum: [error]
message:
type: string
detail_ref:
type: object
required: [seq]
properties:
seq:
type: integer
minimum: 1
tool_call_id:
type: string
type: object
required: [kind, seq, event_id, ts, pair_id, target, message, detail_ref]
properties:
kind:
type: string
enum: [error]
seq:
type: integer
minimum: 1
event_id:
type: string
ts:
type: string
format: date-time
pair_id:
$ref: "#/components/schemas/PairId"
target:
$ref: "#/components/schemas/PairTarget"
message:
type: string
detail_ref:
$ref: "#/components/schemas/PairTranscriptDetailRef"
PairTranscriptWarning:
allOf:
- $ref: "#/components/schemas/PairTranscriptBase"
- type: object
required: [warning_kind, message, detail_ref]
properties:
kind:
type: string
enum: [warning]
warning_kind:
type: string
message:
type: string
detail_ref:
type: object
required: [seq]
properties:
seq:
type: integer
minimum: 1
tool_call_id:
type: string
type: object
required: [kind, seq, event_id, ts, pair_id, target, warning_kind, message, detail_ref]
properties:
kind:
type: string
enum: [warning]
seq:
type: integer
minimum: 1
event_id:
type: string
ts:
type: string
format: date-time
pair_id:
$ref: "#/components/schemas/PairId"
target:
$ref: "#/components/schemas/PairTarget"
warning_kind:
type: string
message:
type: string
detail_ref:
$ref: "#/components/schemas/PairTranscriptDetailRef"
PairTranscriptDetailRef:
type: object
required: [seq]
properties:
seq:
type: integer
minimum: 1
tool_call_id:
type: string
RunEventDetailResponse:
type: object
@ -5402,11 +5336,7 @@ components:
SessionStatus:
type: string
enum: [idle, running, failed, closed, deleted]
TurnStatus:
type: string
enum: [running, succeeded, failed, interrupted]
enum: [idle, running, failed]
SessionMessage:
description: Persisted full-fidelity session transcript message.
@ -5437,71 +5367,50 @@ components:
items: {}
SessionRecord:
description: Durable session metadata and compactable runtime context.
description: Ask Fabro session metadata derived from the owning run event stream.
type: object
required:
- id
- run_id
- status
- permissions
- created_at
- updated_at
- runtime_context
properties:
id:
$ref: "#/components/schemas/SessionId"
run_id:
type: string
title:
type: ["string", "null"]
status:
$ref: "#/components/schemas/SessionStatus"
working_dir:
type: ["string", "null"]
provider:
type: ["string", "null"]
model:
type: ["string", "null"]
permissions:
$ref: "#/components/schemas/PermissionLevel"
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
deleted_at:
type: ["string", "null"]
format: date-time
runtime_context:
type: array
items:
$ref: "#/components/schemas/SessionMessage"
PermissionLevel:
description: Agent tool permission level applied to a session.
type: string
enum:
- read-only
- read-write
- full
SessionSummary:
description: List projection of a durable session.
description: List projection of an Ask Fabro session.
type: object
required:
- id
- run_id
- status
- created_at
- updated_at
properties:
id:
$ref: "#/components/schemas/SessionId"
run_id:
type: string
title:
type: ["string", "null"]
status:
$ref: "#/components/schemas/SessionStatus"
working_dir:
type: ["string", "null"]
provider:
type: ["string", "null"]
model:
type: ["string", "null"]
created_at:
@ -5511,87 +5420,14 @@ components:
type: string
format: date-time
TurnRecord:
description: One externally submitted input plus terminal output metadata. Full resumable context is stored on SessionRecord.runtime_context.
CreateRunSessionRequest:
type: object
required:
- id
- session_id
- input
- status
- created_at
- updated_at
properties:
id:
$ref: "#/components/schemas/TurnId"
session_id:
$ref: "#/components/schemas/SessionId"
input:
type: string
status:
$ref: "#/components/schemas/TurnStatus"
output:
type: ["string", "null"]
error:
type: ["string", "null"]
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
completed_at:
type: ["string", "null"]
format: date-time
SessionEventEnvelope:
description: Append-only durable session event envelope.
type: object
required:
- seq
- session_id
- event
- properties
- ts
properties:
seq:
type: integer
minimum: 0
session_id:
$ref: "#/components/schemas/SessionId"
turn_id:
oneOf:
- $ref: "#/components/schemas/TurnId"
- type: "null"
event:
type: string
example: turn.running
properties: {}
ts:
type: string
format: date-time
CreateSessionRequest:
type: object
required:
- permissions
properties:
title:
type: string
working_dir:
type: string
provider:
type: string
model:
type: string
permissions:
$ref: "#/components/schemas/PermissionLevel"
UpdateSessionRequest:
type: object
properties:
title:
type: string
description: Catalog model ID or alias, or provider-qualified provider/model reference. Stored as the canonical catalog model ID.
SubmitTurnRequest:
type: object
@ -5615,34 +5451,6 @@ components:
meta:
$ref: "#/components/schemas/PaginationMeta"
PaginatedTurnList:
description: Paginated list of turns.
type: object
required:
- data
- meta
properties:
data:
type: array
items:
$ref: "#/components/schemas/TurnRecord"
meta:
$ref: "#/components/schemas/PaginationMeta"
PaginatedSessionEventList:
description: Paginated list of session events.
type: object
required:
- data
- meta
properties:
data:
type: array
items:
$ref: "#/components/schemas/SessionEventEnvelope"
meta:
$ref: "#/components/schemas/PaginationMeta"
PaginatedBoardRunList:
description: Paginated list of board runs with shared canonical fields plus board metadata.
type: object

View file

@ -65,6 +65,7 @@ fabro [OPTIONS] [COMMAND]
| --- | --- |
| `fabro archive` | Mark terminal runs as archived (reviewed, no further action needed). Archived runs are hidden from default listings |
| `fabro artifact` | Inspect and copy run artifacts (screenshots, reports, traces) |
| `fabro ask` | Ask Fabro a read-only question about a run |
| `fabro attach` | Attach to a running or finished workflow run |
| `fabro auth` | Manage CLI authentication state |
| `fabro completion` | Generate shell completions |
@ -93,7 +94,6 @@ fabro [OPTIONS] [COMMAND]
| `fabro sandbox` | Sandbox operations (cp, ssh, preview) |
| `fabro secret` | Manage server-owned secrets |
| `fabro server` | Server operations |
| `fabro session` | Run a persistent Fabro agent session |
| `fabro settings` | Inspect effective settings |
| `fabro start` | Start a created workflow run on the server |
| `fabro steer` | Steer a running agent mid-execution |
@ -187,6 +187,28 @@ fabro artifact list [OPTIONS] <RUN_ID>
| `--retry <retry>` | Filter to artifacts from a specific retry attempt |
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
### `fabro ask`
Ask Fabro a read-only question about a run
```bash
fabro ask [OPTIONS] --prompt <PROMPT> <RUN>
```
#### Arguments
| Name | Description |
| --- | --- |
| `RUN` | Run ID prefix or workflow name |
#### Options
| Option | Description |
| --- | --- |
| `--model <model>` | Optional model name |
| `-p, --prompt <prompt>` | Question to ask |
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
### `fabro attach`
Attach to a running or finished workflow run
@ -1267,25 +1289,6 @@ fabro server stop [OPTIONS]
| `--storage-dir <storage_dir>` | Local storage directory (default: ~/.fabro/storage) |
| `--timeout <timeout>` | Seconds to wait for graceful shutdown before SIGKILL<br />Default: `10` |
### `fabro session`
Run a persistent Fabro agent session
```bash
fabro session [OPTIONS] --prompt <PROMPT>
```
#### Options
| Option | Description |
| --- | --- |
| `--model <model>` | Model name (defaults per provider) |
| `--permissions <level>` | Permission level for tool execution<br />Values: `read-only`, `read-write`, `full` |
| `-p, --prompt <prompt>` | Task prompt |
| `--provider <provider>` | LLM provider (anthropic, openai, gemini, kimi, zai, minimax, inception) |
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
| `--storage-dir <storage_dir>` | Local storage directory (default: ~/.fabro/storage) |
### `fabro settings`
Inspect effective settings

View file

@ -30,6 +30,7 @@ use tokio::sync::{Mutex as AsyncMutex, RwLock as AsyncRwLock};
use crate::config::{ToolApprovalAdapter, ToolApprovalFn, ToolHookCallback};
use crate::error::InterruptReason;
use crate::subagent::{SessionFactory, SubAgentManager};
use crate::tool_permissions::{is_auto_approved, tool_category};
use crate::tools::WebFetchSummarizer;
use crate::{
AgentEvent, AgentProfile, AnthropicProfile, GeminiProfile, LocalSandbox, Message,
@ -120,26 +121,6 @@ impl AgentArgs {
}
}
fn tool_category(name: &str) -> &'static str {
match name {
"read_file" | "read_many_files" | "grep" | "glob" | "list_dir" => "read",
"write_file" | "edit_file" | "apply_patch" => "write",
// subagent tools inherit parent permissions, always allowed
"spawn_agent" | "send_input" | "wait" | "close_agent" => "subagent",
// shell and unknown tools require highest permission
_ => "shell",
}
}
fn is_auto_approved(level: PermissionLevel, category: &str) -> bool {
matches!(
(level, category),
(_, "read" | "subagent")
| (PermissionLevel::ReadWrite | PermissionLevel::Full, "write")
| (PermissionLevel::Full, "shell")
)
}
#[allow(
clippy::print_stderr,
reason = "Interactive approval prompts belong on stderr, not assistant output."

View file

@ -20,6 +20,7 @@ pub mod session;
pub mod skills;
pub mod subagent;
pub mod tool_execution;
pub mod tool_permissions;
pub mod tool_registry;
pub mod tools;
pub mod truncation;

View file

@ -15,7 +15,7 @@ use fabro_llm::{Error as LlmError, retry};
use fabro_mcp::config::{McpServerSettings, McpTransport};
use fabro_mcp::connection_manager::McpConnectionManager;
use fabro_model::{AgentProfileKind, Catalog, ModelRef, Speed};
use fabro_types::{Principal, SessionRecord, SessionStatus, SteeringMessage};
use fabro_types::{Principal, SessionMessage, SessionRecord, SteeringMessage};
use futures::StreamExt;
use tokio::sync::{Mutex as AsyncMutex, Notify, broadcast};
use tokio::time;
@ -381,6 +381,7 @@ impl Session {
pub fn from_record(
record: &SessionRecord,
runtime_context: &[SessionMessage],
llm_client: Client,
provider_profile: Arc<dyn AgentProfile>,
sandbox: Arc<dyn Sandbox>,
@ -395,33 +396,13 @@ impl Session {
subagent_manager,
);
session.id = record.id.to_string();
session.history =
History::from_session_messages(&record.runtime_context).map_err(|err| {
Error::InvalidState(format!("invalid persisted session context: {err}"))
})?;
session.state = match record.status {
SessionStatus::Closed | SessionStatus::Deleted => SessionState::Closed,
SessionStatus::Running | SessionStatus::Failed | SessionStatus::Idle => {
SessionState::Idle
}
};
session.history = History::from_session_messages(runtime_context).map_err(|err| {
Error::InvalidState(format!("invalid persisted session context: {err}"))
})?;
session.state = SessionState::Idle;
Ok(session)
}
#[must_use]
pub fn to_record(&self, mut record: SessionRecord) -> SessionRecord {
record.status = match self.state {
SessionState::Closed => SessionStatus::Closed,
SessionState::Thinking | SessionState::Executing => SessionStatus::Running,
SessionState::Idle => SessionStatus::Idle,
};
record.provider = Some(self.provider_id().to_string());
record.model = Some(self.model().to_string());
record.runtime_context = self.history.to_session_messages();
record.updated_at = chrono::Utc::now();
record
}
pub fn set_tool_env_provider(&mut self, provider: Arc<dyn ToolEnvProvider>) {
self.tool_env_provider = Some(provider);
}

View file

@ -0,0 +1,23 @@
use fabro_types::PermissionLevel;
pub fn tool_category(name: &str) -> &'static str {
match name {
"read_file" | "read_many_files" | "grep" | "glob" | "list_dir" => "read",
"write_file" | "edit_file" | "apply_patch" => "write",
"spawn_agent" | "send_input" | "wait" | "close_agent" => "subagent",
_ => "shell",
}
}
pub fn is_auto_approved(level: PermissionLevel, category: &str) -> bool {
matches!(
(level, category),
(_, "read" | "subagent")
| (PermissionLevel::ReadWrite | PermissionLevel::Full, "write")
| (PermissionLevel::Full, "shell")
)
}
pub fn is_tool_auto_approved(level: PermissionLevel, tool_name: &str) -> bool {
is_auto_approved(level, tool_category(tool_name))
}

View file

@ -476,17 +476,9 @@ fn main() {
("SessionId", "fabro_types::SessionId", &[]),
("TurnId", "fabro_types::TurnId", &[]),
("SessionStatus", "fabro_types::SessionStatus", &[]),
("TurnStatus", "fabro_types::TurnStatus", &[]),
("PermissionLevel", "fabro_types::PermissionLevel", &[]),
("SessionMessage", "fabro_types::SessionMessage", &[]),
("SessionRecord", "fabro_types::SessionRecord", &[]),
("SessionSummary", "fabro_types::SessionSummary", &[]),
("TurnRecord", "fabro_types::TurnRecord", &[]),
(
"SessionEventEnvelope",
"fabro_types::SessionEventEnvelope",
&[],
),
];
for (name, path, impls) in replacements {
settings.with_replacement(*name, *path, impls.iter().copied());

View file

@ -37,18 +37,17 @@ pub mod types {
FailureSignature, GitContext, IdpIdentity, InterviewOption, InterviewQuestionRecord,
PairId, PairMessageId, PairMessageRecord, PairMessageRequest, PairRecord, PairStartRequest,
PairStatus, PairTarget, PairTargetSelector, PairTranscriptEntry, PairTranscriptResponse,
PendingInterviewRecord, PermissionLevel, PreRunPushOutcome, Principal, PullRequest,
PullRequestDetails, PullRequestDetailsStatus, PullRequestDetailsUnavailableReason,
PullRequestLink, PullRequestMeta, PullRequestResponse, QuestionType, RepositoryRef, Run,
PendingInterviewRecord, PreRunPushOutcome, Principal, PullRequest, PullRequestDetails,
PullRequestDetailsStatus, PullRequestDetailsUnavailableReason, PullRequestLink,
PullRequestMeta, PullRequestResponse, QuestionType, RepositoryRef, Run,
RunClientProvenance, RunEvent, RunEventDetailContentKind, RunEventDetailResponse,
RunFailure, RunPairStatusResponse, RunProjection, RunProvenance, RunSandbox,
RunSandboxRuntime, RunServerProvenance, SandboxDetails, SandboxNetwork,
SandboxNetworkPolicy, SandboxNetworkPolicyMode, SandboxProvider, SandboxResources,
SandboxService, SandboxServiceListResponse, SandboxState, SandboxTimestamps,
SecretMetadata, SecretType, ServerSettings, SessionEventEnvelope, SessionId,
SessionMessage, SessionRecord, SessionStatus, SessionSummary, StageCompletion,
StageHandler, StageOutcome, StageProjection, StageState, SystemActorKind, TurnId,
TurnRecord, TurnStatus, UserPrincipal, WorkflowSettings,
SecretMetadata, SecretType, ServerSettings, SessionId, SessionMessage, SessionRecord,
SessionStatus, SessionSummary, StageCompletion, StageHandler, StageOutcome,
StageProjection, StageState, SystemActorKind, TurnId, UserPrincipal, WorkflowSettings,
};
pub use crate::generated::types::*;

View file

@ -1,53 +0,0 @@
use std::any::{TypeId, type_name};
use fabro_api::types::PermissionLevel as ApiPermissionLevel;
use fabro_types::PermissionLevel;
use serde_json::json;
#[test]
fn permission_level_reuses_canonical_type() {
assert_same_type::<ApiPermissionLevel, PermissionLevel>();
}
#[test]
fn permission_level_serializes_as_kebab_case_strings() {
assert_eq!(
serde_json::to_value(PermissionLevel::ReadOnly).unwrap(),
json!("read-only")
);
assert_eq!(
serde_json::to_value(PermissionLevel::ReadWrite).unwrap(),
json!("read-write")
);
assert_eq!(
serde_json::to_value(PermissionLevel::Full).unwrap(),
json!("full")
);
}
#[test]
fn permission_level_deserializes_each_variant() {
let read_only: PermissionLevel = serde_json::from_value(json!("read-only")).unwrap();
assert_eq!(read_only, PermissionLevel::ReadOnly);
let read_write: PermissionLevel = serde_json::from_value(json!("read-write")).unwrap();
assert_eq!(read_write, PermissionLevel::ReadWrite);
let full: PermissionLevel = serde_json::from_value(json!("full")).unwrap();
assert_eq!(full, PermissionLevel::Full);
}
#[test]
fn permission_level_rejects_unknown_values() {
assert!(serde_json::from_value::<PermissionLevel>(json!("readonly")).is_err());
assert!(serde_json::from_value::<PermissionLevel>(json!("read_only")).is_err());
assert!(serde_json::from_value::<PermissionLevel>(json!("")).is_err());
}
fn assert_same_type<T: 'static, U: 'static>() {
assert_eq!(
TypeId::of::<T>(),
TypeId::of::<U>(),
"{} should be the same type as {}",
type_name::<T>(),
type_name::<U>()
);
}

View file

@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use clap::{Args, Parser, Subcommand, ValueEnum};
use fabro_agent::cli::{AgentArgs, PermissionLevel};
use fabro_agent::cli::AgentArgs;
use fabro_config::{CliLayer, CliLoggingLayer, CliOutputLayer, CliUpdatesLayer};
use fabro_server::serve::DEFAULT_TCP_PORT;
use fabro_static::EnvVars;
@ -759,6 +759,23 @@ pub(crate) struct SteerArgs {
pub(crate) interrupt: bool,
}
#[derive(Args)]
pub(crate) struct AskArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run ID prefix or workflow name
pub(crate) run: String,
/// Question to ask
#[arg(short = 'p', long = "prompt")]
pub(crate) prompt: String,
/// Optional model name
#[arg(long)]
pub(crate) model: Option<String>,
}
#[derive(Args)]
pub(crate) struct WorkflowListArgs;
@ -1054,28 +1071,6 @@ pub(crate) struct ExecArgs {
pub(crate) agent: AgentArgs,
}
#[derive(Args)]
pub(crate) struct SessionArgs {
#[command(flatten)]
pub(crate) connection: ServerConnectionArgs,
/// Task prompt
#[arg(short = 'p', long = "prompt")]
pub(crate) prompt: String,
/// LLM provider (anthropic, openai, gemini, kimi, zai, minimax, inception)
#[arg(long)]
pub(crate) provider: Option<String>,
/// Model name (defaults per provider)
#[arg(long)]
pub(crate) model: Option<String>,
/// Permission level for tool execution
#[arg(long = "permissions", value_name = "LEVEL", value_enum)]
pub(crate) permissions: Option<PermissionLevel>,
}
#[derive(Args)]
pub(crate) struct UpgradeArgs {
/// Target version (e.g. "0.5.0", "v0.5.0", or "v0.177.0-alpha.1")
@ -1125,6 +1120,8 @@ pub(crate) enum RunCommands {
Wait(WaitArgs),
/// Steer a running agent mid-execution
Steer(SteerArgs),
/// Ask Fabro a read-only question about a run
Ask(AskArgs),
}
impl RunCommands {
@ -1141,6 +1138,7 @@ impl RunCommands {
Self::Resume(_) => "resume",
Self::Rewind(_) => "rewind",
Self::Steer(_) => "steer",
Self::Ask(_) => "ask",
Self::Fork(_) => "fork",
Self::Wait(_) => "wait",
}
@ -1209,8 +1207,6 @@ pub(crate) enum Commands {
/// Run an agentic coding session
#[command(hide = true)]
Exec(ExecArgs),
/// Run a persistent Fabro agent session
Session(SessionArgs),
#[command(flatten)]
RunCmd(RunCommands),
/// Validate run configuration without executing
@ -1317,7 +1313,6 @@ impl Commands {
},
Self::Dump(_) => "dump",
Self::Exec(_) => "exec",
Self::Session(_) => "session",
Self::RunCmd(cmd) => cmd.name(),
Self::Preflight(_) => "preflight",
Self::Validate(_) => "validate",

View file

@ -21,7 +21,6 @@ pub(crate) mod runs;
pub(crate) mod sandbox;
pub(crate) mod secret;
pub(crate) mod server;
pub(crate) mod session;
pub(crate) mod system;
pub(crate) mod uninstall;
pub(crate) mod upgrade;

View file

@ -0,0 +1,96 @@
use anyhow::{Result, bail};
use fabro_api::types::CreateRunSessionRequest;
use fabro_store::EventEnvelope;
use crate::args::AskArgs;
use crate::command_context::CommandContext;
pub(crate) async fn run(args: AskArgs, base_ctx: &CommandContext) -> Result<()> {
let ctx = base_ctx.with_target(&args.server)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(&args.run).await?.id;
let session = client
.create_run_session(run_id, CreateRunSessionRequest {
title: Some(session_title(&args.prompt)),
model: args.model,
})
.await?;
let mut stream = client
.submit_session_turn_stream(session.id, args.prompt)
.await?;
let mut terminal_error = None;
let mut saw_terminal = false;
while let Some(event) = stream.next_event().await? {
render_event(&event, ctx.json_output())?;
match event.event.event_name() {
"run.session.turn.succeeded" | "run.session.turn.interrupted" => {
saw_terminal = true;
}
"run.session.turn.failed" => {
saw_terminal = true;
terminal_error = Some(
event
.event
.properties()?
.get("error")
.and_then(serde_json::Value::as_str)
.unwrap_or("session turn failed")
.to_string(),
);
}
_ => {}
}
}
if let Some(error) = terminal_error {
bail!(error);
}
if !saw_terminal {
bail!("session turn ended before a terminal event was received");
}
Ok(())
}
fn session_title(prompt: &str) -> String {
const MAX_CHARS: usize = 80;
let trimmed = prompt.trim();
if trimmed.chars().count() <= MAX_CHARS {
return trimmed.to_string();
}
let mut title = trimmed.chars().take(MAX_CHARS - 3).collect::<String>();
title.push_str("...");
title
}
#[allow(
clippy::print_stdout,
reason = "The ask command streams assistant output and JSON events to stdout."
)]
fn render_event(event: &EventEnvelope, json_output: bool) -> Result<()> {
if json_output {
println!("{}", serde_json::to_string(event)?);
return Ok(());
}
match event.event.event_name() {
"run.session.assistant_delta" => {
let properties = event.event.properties()?;
if let Some(delta) = properties.get("delta").and_then(serde_json::Value::as_str) {
print!("{delta}");
}
}
"run.session.assistant_message" => {
let properties = event.event.properties()?;
if let Some(text) = properties
.get("text")
.and_then(serde_json::Value::as_str)
.filter(|text| !text.is_empty())
{
println!("{text}");
}
}
_ => {}
}
Ok(())
}

View file

@ -8,6 +8,7 @@ use crate::shared::print_json_pretty;
#[cfg(feature = "sleep_inhibitor")]
use crate::sleep_inhibitor;
pub(crate) mod ask;
pub(crate) mod attach;
pub(crate) mod checkpoints;
pub(crate) mod command;
@ -127,5 +128,6 @@ pub(crate) async fn dispatch(
wait::run(&args, &styles, base_ctx).await
}
RunCommands::Steer(args) => steer::run(args, base_ctx).await,
RunCommands::Ask(args) => ask::run(args, base_ctx).await,
}
}

View file

@ -1,140 +0,0 @@
use anyhow::{Result, bail};
use fabro_api::types::CreateSessionRequest;
use fabro_types::settings::InterpString;
use fabro_types::settings::run::AgentPermissions;
use fabro_types::{PermissionLevel, SessionEventEnvelope};
use crate::args::SessionArgs;
use crate::command_context::CommandContext;
pub(crate) async fn execute(args: SessionArgs, base_ctx: &CommandContext) -> Result<()> {
let ctx = base_ctx.with_connection(&args.connection)?;
let client = ctx.server().await?;
let request = CreateSessionRequest {
title: Some(session_title(&args.prompt)),
working_dir: Some(ctx.cwd().to_string_lossy().into_owned()),
provider: session_provider(&args, &ctx),
model: session_model(&args, &ctx),
permissions: session_permissions(&args, &ctx),
};
let session = client.create_session(request).await?;
let mut stream = client
.submit_session_turn_stream(session.id, args.prompt)
.await?;
let mut terminal_error = None;
let mut saw_terminal = false;
while let Some(event) = stream.next_event().await? {
render_event(&event, ctx.json_output())?;
match event.event.as_str() {
"turn.succeeded" | "turn.interrupted" => saw_terminal = true,
"turn.failed" => {
saw_terminal = true;
terminal_error = Some(
event
.properties
.get("error")
.and_then(serde_json::Value::as_str)
.unwrap_or("session turn failed")
.to_string(),
);
}
_ => {}
}
}
if let Some(error) = terminal_error {
bail!(error);
}
if !saw_terminal {
bail!("session turn ended before a terminal event was received");
}
Ok(())
}
fn session_title(prompt: &str) -> String {
const MAX_CHARS: usize = 80;
let trimmed = prompt.trim();
if trimmed.chars().count() <= MAX_CHARS {
return trimmed.to_string();
}
let mut title = trimmed.chars().take(MAX_CHARS - 3).collect::<String>();
title.push_str("...");
title
}
fn session_provider(args: &SessionArgs, ctx: &CommandContext) -> Option<String> {
args.provider.clone().or_else(|| {
ctx.user_settings()
.cli
.exec
.model
.provider
.as_ref()
.map(InterpString::as_source)
})
}
fn session_model(args: &SessionArgs, ctx: &CommandContext) -> Option<String> {
args.model.clone().or_else(|| {
ctx.user_settings()
.cli
.exec
.model
.name
.as_ref()
.map(InterpString::as_source)
})
}
fn session_permissions(args: &SessionArgs, ctx: &CommandContext) -> PermissionLevel {
args.permissions
.or_else(|| {
ctx.user_settings()
.cli
.exec
.agent
.permissions
.map(|permissions| match permissions {
AgentPermissions::ReadOnly => PermissionLevel::ReadOnly,
AgentPermissions::ReadWrite => PermissionLevel::ReadWrite,
AgentPermissions::Full => PermissionLevel::Full,
})
})
.unwrap_or(PermissionLevel::ReadWrite)
}
#[allow(
clippy::print_stdout,
reason = "The session command streams assistant output and JSON events to stdout."
)]
fn render_event(event: &SessionEventEnvelope, json_output: bool) -> Result<()> {
if json_output {
println!("{}", serde_json::to_string(event)?);
return Ok(());
}
match event.event.as_str() {
"turn.text_delta" => {
if let Some(delta) = event
.properties
.get("delta")
.and_then(serde_json::Value::as_str)
{
print!("{delta}");
}
}
"turn.assistant_message" => {
if let Some(text) = event
.properties
.get("text")
.and_then(serde_json::Value::as_str)
.filter(|text| !text.is_empty())
{
println!("{text}");
}
}
_ => {}
}
Ok(())
}

View file

@ -263,7 +263,6 @@ async fn main_inner(worker_token: Option<String>) -> (String, Result<()>) {
command.as_ref(),
Commands::RunCmd(RunCommands::Run(_) | RunCommands::Create(_))
| Commands::Exec(_)
| Commands::Session(_)
| Commands::Repo(_)
| Commands::Install { .. }
) {
@ -277,9 +276,6 @@ async fn main_inner(worker_token: Option<String>) -> (String, Result<()>) {
Commands::Exec(args) => {
commands::exec::execute(args, &base_ctx).await?;
}
Commands::Session(args) => {
commands::session::execute(args, &base_ctx).await?;
}
Commands::RunCmd(cmd) => {
Box::pin(commands::run::dispatch(cmd, &base_ctx, worker_token)).await?;
}

View file

@ -12,7 +12,6 @@ fn help() {
Usage: fabro [OPTIONS] [COMMAND]
Commands:
session Run a persistent Fabro agent session
run Launch a workflow run
create Create a workflow run (allocate run dir, persist spec)
start Start a created workflow run on the server
@ -24,6 +23,7 @@ fn help() {
fork Fork a workflow run from an earlier checkpoint into a new run
wait Block until a workflow run completes
steer Steer a running agent mid-execution
ask Ask Fabro a read-only question about a run
preflight Validate run configuration without executing
validate Validate a workflow
graph Render a workflow graph as SVG

View file

@ -58,7 +58,6 @@ mod send_panic;
mod server_start;
mod server_status;
mod server_stop;
mod session;
mod start;
pub(crate) mod support;
mod system;

View file

@ -1,114 +0,0 @@
use fabro_test::{fabro_snapshot, test_context};
use httpmock::MockServer;
#[test]
fn help() {
let context = test_context!();
let mut cmd = context.command();
cmd.args(["session", "--help"]);
fabro_snapshot!(context.filters(), cmd, @"
success: true
exit_code: 0
----- stdout -----
Run a persistent Fabro agent session
Usage: fabro session [OPTIONS] --prompt <PROMPT>
Options:
--json Output as JSON [env: FABRO_JSON=]
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--server <SERVER> Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
-p, --prompt <PROMPT> Task prompt
--provider <PROVIDER> LLM provider (anthropic, openai, gemini, kimi, zai, minimax, inception)
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--model <MODEL> Model name (defaults per provider)
--verbose Enable verbose output [env: FABRO_VERBOSE=]
--permissions <LEVEL> Permission level for tool execution [possible values: read-only, read-write, full]
-h, --help Print help
----- stderr -----
");
}
#[test]
fn prompt_creates_session_and_streams_turn_events() {
let context = test_context!();
let server = MockServer::start();
let session_id = "01ARZ3NDEKTSV4RRFFQ69G5FAV";
let turn_id = "01BX5ZZKBKACTAV9WEVGEMMVRZ";
let create = server.mock(|when, then| {
when.method("POST").path("/api/v1/sessions");
then.status(201)
.header("Content-Type", "application/json")
.json_body(serde_json::json!({
"id": session_id,
"title": "say hello",
"status": "idle",
"working_dir": context.temp_dir.to_string_lossy(),
"provider": "openai",
"model": "gpt-5.4-mini",
"permissions": "read-write",
"created_at": "2026-04-05T12:00:00Z",
"updated_at": "2026-04-05T12:00:00Z",
"deleted_at": null,
"runtime_context": []
}));
});
let submit = server.mock(|when, then| {
when.method("POST")
.path(format!("/api/v1/sessions/{session_id}/turns"));
then.status(200)
.header("Content-Type", "text/event-stream")
.body(format!(
"data: {}\n\ndata: {}\n\n",
serde_json::json!({
"seq": 1,
"session_id": session_id,
"turn_id": turn_id,
"event": "turn.assistant_message",
"properties": {"text": "Hello from server"},
"ts": "2026-04-05T12:00:01Z"
}),
serde_json::json!({
"seq": 2,
"session_id": session_id,
"turn_id": turn_id,
"event": "turn.succeeded",
"properties": {"turn_id": turn_id},
"ts": "2026-04-05T12:00:02Z"
})
));
});
let output = context
.command()
.args([
"session",
"--server",
&format!("{}/api/v1", server.base_url()),
"--provider",
"openai",
"--model",
"gpt-5.4-mini",
"-p",
"say hello",
])
.output()
.expect("session command should execute");
create.assert();
submit.assert();
assert!(
output.status.success(),
"command failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"Hello from server\n"
);
assert_eq!(String::from_utf8_lossy(&output.stderr), "");
}

View file

@ -15,8 +15,8 @@ use fabro_types::settings::run::MergeStrategy;
use fabro_types::{
ArtifactUpload, EventEnvelope, PairId, PairMessageRecord, PairMessageRequest, PairRecord,
PairStartRequest, PairTargetSelector, PairTranscriptResponse, Run, RunBlobId, RunEvent,
RunEventDetailResponse, RunId, RunPairStatusResponse, RunProjection, SessionEventEnvelope,
SessionId, SessionRecord, StageId,
RunEventDetailResponse, RunId, RunPairStatusResponse, RunProjection, SessionId, SessionRecord,
StageId,
};
use fabro_util::exit::{ErrorExt, ExitClass};
use futures::future::BoxFuture;
@ -53,7 +53,7 @@ type HttpByteStream = Pin<Box<dyn Stream<Item = Result<Bytes>> + Send>>;
pub struct SessionEventStream {
stream: HttpByteStream,
pending_bytes: Vec<u8>,
buffered_events: VecDeque<SessionEventEnvelope>,
buffered_events: VecDeque<EventEnvelope>,
}
pub struct RewindRunResult {
@ -186,7 +186,7 @@ impl SessionEventStream {
}
}
pub async fn next_event(&mut self) -> Result<Option<SessionEventEnvelope>> {
pub async fn next_event(&mut self) -> Result<Option<EventEnvelope>> {
loop {
if let Some(event) = self.buffered_events.pop_front() {
return Ok(Some(event));
@ -613,11 +613,23 @@ impl Client {
.context("server returned invalid JSON for server settings")
}
pub async fn create_session(&self, body: types::CreateSessionRequest) -> Result<SessionRecord> {
pub async fn create_run_session(
&self,
run_id: RunId,
body: types::CreateRunSessionRequest,
) -> Result<SessionRecord> {
let response = self
.send_api(
|client| async move { client.create_session().body(body.clone()).send().await },
)
.send_api(|client| {
let body = body.clone();
async move {
client
.create_run_session()
.id(run_id.to_string())
.body(body)
.send()
.await
}
})
.await?;
Ok(response.into_inner())
}

View file

@ -72,7 +72,7 @@ use fabro_slack::{blocks as slack_blocks, connection as slack_connection};
use fabro_static::EnvVars;
use fabro_store::{
ArtifactKey, ArtifactStore, Database, EventEnvelope, EventPayload, NodeArtifact,
PendingInterviewRecord, SessionStore, StageArtifactEntry, StageId,
PendingInterviewRecord, StageArtifactEntry, StageId,
};
#[cfg(test)]
use fabro_types::BlockedReason;
@ -595,7 +595,6 @@ pub struct AppState {
runs: Mutex<HashMap<RunId, ManagedRun>>,
aggregate_billing: Mutex<BillingAccumulator>,
store: Arc<Database>,
session_store: SessionStore,
session_runtimes: SessionRuntimeManager,
artifact_store: ArtifactStore,
worker_tokens: WorkerTokenKeys,
@ -850,10 +849,6 @@ impl AppState {
&self.store
}
pub(crate) fn session_store(&self) -> &SessionStore {
&self.session_store
}
pub(crate) fn session_runtimes(&self) -> &SessionRuntimeManager {
&self.session_runtimes
}
@ -1678,15 +1673,6 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
));
let (global_event_tx, _) = broadcast::channel(4096);
let current_server_settings = Arc::new(resolved_settings.server_settings);
let session_store = SessionStore::new(
PathBuf::from(resolve_interp_string(
&current_server_settings.server.storage.root,
)?)
.join("sessions"),
);
session_store
.recover_stale_running_state(chrono::Utc::now())
.context("recovering stale session runtime state")?;
let current_manifest_run_defaults = Arc::new(resolved_settings.manifest_run_defaults);
let current_manifest_run_settings = resolved_settings.manifest_run_settings;
let current_catalog = Arc::new(
@ -1723,7 +1709,6 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
runs: Mutex::new(HashMap::new()),
aggregate_billing: Mutex::new(BillingAccumulator::default()),
store,
session_store,
session_runtimes: SessionRuntimeManager::new(),
artifact_store,
worker_tokens,

File diff suppressed because it is too large Load diff

View file

@ -2,12 +2,10 @@ use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use fabro_agent::Session;
use fabro_types::{SessionEventEnvelope, SessionId, TurnId};
use tokio::sync::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard, broadcast};
use fabro_types::{SessionId, TurnId};
use tokio::sync::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard};
use tokio_util::sync::CancellationToken;
const SESSION_EVENT_BROADCAST_CAPACITY: usize = 1024;
#[derive(Default)]
pub(crate) struct SessionRuntimeManager {
entries: Mutex<HashMap<SessionId, Arc<SessionRuntimeEntry>>>,
@ -22,11 +20,6 @@ impl SessionRuntimeManager {
self.entry(session_id)
}
pub(crate) fn has_active_turn(&self, session_id: SessionId) -> bool {
self.existing_entry(session_id)
.is_some_and(|entry| entry.has_active_turn())
}
pub(crate) fn reserve_turn(
&self,
session_id: SessionId,
@ -73,37 +66,6 @@ impl SessionRuntimeManager {
Ok(PendingTurnInterrupt { entry, turn_id })
}
pub(crate) fn subscribe_events(
&self,
session_id: SessionId,
) -> broadcast::Receiver<SessionEventEnvelope> {
self.load_or_create_runtime(session_id).live_tx.subscribe()
}
pub(crate) fn broadcast_event(&self, event: &SessionEventEnvelope) {
let Some(entry) = self.existing_entry(event.session_id) else {
return;
};
let _ = entry.live_tx.send(event.clone());
}
pub(crate) async fn unload_idle(&self, session_id: SessionId) -> bool {
let entry = {
let mut entries = self.entries.lock().expect("session runtime map poisoned");
let Some(entry) = entries.get(&session_id) else {
return true;
};
if entry.has_active_turn() {
return false;
}
entries
.remove(&session_id)
.expect("session runtime entry should exist")
};
entry.clear_session().await;
true
}
fn entry(&self, session_id: SessionId) -> Arc<SessionRuntimeEntry> {
let mut entries = self.entries.lock().expect("session runtime map poisoned");
Arc::clone(
@ -126,17 +88,14 @@ pub(crate) struct SessionRuntimeEntry {
session: AsyncMutex<Option<Session>>,
initialized: Mutex<bool>,
active_turn: Mutex<Option<ActiveTurn>>,
live_tx: broadcast::Sender<SessionEventEnvelope>,
}
impl SessionRuntimeEntry {
fn new() -> Self {
let (live_tx, _) = broadcast::channel(SESSION_EVENT_BROADCAST_CAPACITY);
Self {
session: AsyncMutex::new(None),
session: AsyncMutex::new(None),
initialized: Mutex::new(false),
active_turn: Mutex::new(None),
live_tx,
}
}
@ -165,13 +124,6 @@ impl SessionRuntimeEntry {
.lock()
.expect("session initialized lock poisoned") = false;
}
fn has_active_turn(&self) -> bool {
self.active_turn
.lock()
.expect("session active turn lock poisoned")
.is_some()
}
}
struct ActiveTurn {

View file

@ -1,5 +1,4 @@
use std::collections::HashMap;
use std::convert::Infallible;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
@ -9,7 +8,6 @@ use std::sync::{Arc as StdArc, Mutex as StdMutex};
use axum::body::Body;
use axum::http::{Method, Request, header};
use axum::response::sse::{Event as SseEvent, Sse};
use chrono::{Duration as ChronoDuration, Utc};
use fabro_config::ServerSettingsBuilder;
use fabro_config::bind::Bind;
@ -23,11 +21,9 @@ use fabro_types::settings::ServerAuthMethod;
use fabro_types::{
AgentBackend, AttrValue, AuthMethod, CommandTermination, FailureCategory, FailureDetail, Graph,
InterviewQuestionRecord, Node, Outcome, QuestionType, RunBlobId, RunId, RunSpec,
SandboxProvider, SessionMessage, SuccessReason, SystemActorKind, WorkflowSettings, fixtures,
SandboxProvider, SuccessReason, SystemActorKind, WorkflowSettings, fixtures,
};
use fabro_util::check_report::CheckStatus;
use futures_util::stream;
use http_body_util::BodyExt as _;
use httpmock::Method::{GET, POST};
use httpmock::MockServer;
use serde_json::json;
@ -133,36 +129,6 @@ async fn body_json(body: Body) -> serde_json::Value {
serde_json::from_slice(&bytes).unwrap()
}
async fn read_sse_until(body: &mut Body, expected_event: &str) -> String {
let mut sse_data = String::new();
for _ in 0..32 {
let frame = tokio::time::timeout(Duration::from_secs(2), body.frame())
.await
.unwrap_or_else(|_| panic!("timed out waiting for SSE event {expected_event}"))
.unwrap_or_else(|| panic!("SSE ended before event {expected_event}"))
.expect("SSE frame should be readable");
if let Some(data) = frame.data_ref() {
sse_data.push_str(&String::from_utf8_lossy(data));
if sse_events(&sse_data).iter().any(|event| {
event["event"]
.as_str()
.is_some_and(|name| name == expected_event)
}) {
return sse_data;
}
}
}
panic!("SSE event {expected_event} not found in {sse_data}");
}
fn sse_events(sse_data: &str) -> Vec<serde_json::Value> {
sse_data
.lines()
.filter_map(|line| line.strip_prefix("data:"))
.filter_map(|json| serde_json::from_str(json.trim()).ok())
.collect()
}
fn run_json_id(run: &serde_json::Value) -> Option<&str> {
run["id"].as_str().or_else(|| run["run_id"].as_str())
}
@ -269,39 +235,6 @@ fn openai_responses_payload(text: &str) -> serde_json::Value {
})
}
fn openai_stream_body(text: &str) -> String {
let created = json!({
"type": "response.created",
"response": {
"id": "resp_session",
"model": "gpt-5.4-mini"
}
});
let delta = json!({
"type": "response.output_text.delta",
"delta": text
});
let completed = json!({
"type": "response.completed",
"response": {
"id": "resp_session",
"model": "gpt-5.4-mini",
"status": "completed",
"usage": {
"input_tokens": 10,
"output_tokens": 5,
"input_tokens_details": { "cached_tokens": 0 },
"output_tokens_details": { "reasoning_tokens": 0 }
}
}
});
format!(
"event: response.created\ndata: {created}\n\n\
event: response.output_text.delta\ndata: {delta}\n\n\
event: response.completed\ndata: {completed}\n\n"
)
}
macro_rules! assert_status {
($response:expr, $expected:expr) => {
fabro_test::assert_axum_status($response, $expected, concat!(file!(), ":", line!()))
@ -2334,713 +2267,6 @@ async fn create_run(app: &Router, dot_source: &str) -> String {
body["id"].as_str().unwrap().to_string()
}
#[tokio::test]
async fn create_session_rejects_missing_permissions() {
let state = test_app_state_with_isolated_storage();
let app = crate::test_support::build_test_router(Arc::clone(&state));
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri(api("/sessions"))
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({"working_dir": "/tmp"}).to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
}
#[tokio::test]
async fn create_session_rejects_unknown_permission_value() {
let state = test_app_state_with_isolated_storage();
let app = crate::test_support::build_test_router(Arc::clone(&state));
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri(api("/sessions"))
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({"permissions": "readonly"}).to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
}
#[tokio::test]
async fn session_apis_create_list_replay_events_and_delete() {
let state = test_app_state_with_isolated_storage();
let app = crate::test_support::build_test_router(Arc::clone(&state));
let create_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri(api("/sessions"))
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"title": "Investigate failure",
"working_dir": "/tmp",
"provider": "openai",
"model": "gpt-5.4-mini",
"permissions": "read-only"
})
.to_string(),
))
.unwrap(),
)
.await
.unwrap();
let created = response_json!(create_response, StatusCode::CREATED).await;
let session_id = created["id"]
.as_str()
.expect("create session response should include id")
.to_string();
assert_eq!(created["status"], "idle");
assert_eq!(created["working_dir"], "/tmp");
let list_response = app
.clone()
.oneshot(
Request::builder()
.method("GET")
.uri(api("/sessions"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let list = response_json!(list_response, StatusCode::OK).await;
assert_eq!(list["data"].as_array().unwrap().len(), 1);
let turns_response = app
.clone()
.oneshot(
Request::builder()
.method("GET")
.uri(api(&format!("/sessions/{session_id}/turns")))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let turns = response_json!(turns_response, StatusCode::OK).await;
assert!(turns["data"].as_array().unwrap().is_empty());
let replay_response = app
.clone()
.oneshot(
Request::builder()
.method("GET")
.uri(api(&format!("/sessions/{session_id}/events?since_seq=1")))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let replay = response_json!(replay_response, StatusCode::OK).await;
assert_eq!(replay["data"][0]["seq"], 1);
assert_eq!(replay["data"][0]["event"], "session.created");
let generated_client_replay_response = app
.clone()
.oneshot(
Request::builder()
.method("GET")
.uri(api(&format!("/sessions/{session_id}/events?since_seq=1")))
.header(header::ACCEPT, "application/json,text/event-stream")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let generated_client_replay =
response_json!(generated_client_replay_response, StatusCode::OK).await;
assert_eq!(
generated_client_replay["data"][0]["event"],
"session.created"
);
let header_replay_response = app
.clone()
.oneshot(
Request::builder()
.method("GET")
.uri(api(&format!("/sessions/{session_id}/events")))
.header("last-event-id", "1")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let header_replay = response_json!(header_replay_response, StatusCode::OK).await;
assert!(
header_replay["data"]
.as_array()
.expect("events response should contain array")
.is_empty()
);
let delete_response = app
.clone()
.oneshot(
Request::builder()
.method("DELETE")
.uri(api(&format!("/sessions/{session_id}")))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
checked_response!(delete_response, StatusCode::NO_CONTENT).await;
let get_deleted_response = app
.oneshot(
Request::builder()
.method("GET")
.uri(api(&format!("/sessions/{session_id}")))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
checked_response!(get_deleted_response, StatusCode::NOT_FOUND).await;
}
#[tokio::test]
async fn streaming_session_turn_persists_terminal_failure_event() {
let state = test_app_state_with_isolated_storage();
let app = crate::test_support::build_test_router(Arc::clone(&state));
let create_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri(api("/sessions"))
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"working_dir": "/tmp",
"provider": "openai",
"model": "gpt-5.4-mini",
"permissions": "read-only"
})
.to_string(),
))
.unwrap(),
)
.await
.unwrap();
let created = response_json!(create_response, StatusCode::CREATED).await;
let session_id = created["id"].as_str().unwrap().to_string();
let stream_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri(api(&format!("/sessions/{session_id}/turns")))
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({"input": "hello"}).to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(stream_response.status(), StatusCode::OK);
let body = String::from_utf8(
to_bytes(stream_response.into_body(), usize::MAX)
.await
.unwrap()
.to_vec(),
)
.unwrap();
assert!(body.contains("turn.running"), "SSE body: {body}");
assert!(body.contains("turn.failed"), "SSE body: {body}");
assert!(
body.contains("LLM credentials not configured for provider 'openai'"),
"SSE body: {body}"
);
let turns_response = app
.clone()
.oneshot(
Request::builder()
.method("GET")
.uri(api(&format!("/sessions/{session_id}/turns")))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let turns = response_json!(turns_response, StatusCode::OK).await;
assert_eq!(turns["data"][0]["status"], "failed");
let events_response = app
.clone()
.oneshot(
Request::builder()
.method("GET")
.uri(api(&format!("/sessions/{session_id}/events")))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let events = response_json!(events_response, StatusCode::OK).await;
let event_names = events["data"]
.as_array()
.unwrap()
.iter()
.map(|event| event["event"].as_str().unwrap())
.collect::<Vec<_>>();
assert_eq!(event_names, vec![
"session.created",
"turn.running",
"turn.failed"
]);
let turn_id = turns["data"][0]["id"].as_str().unwrap();
let interrupt_response = app
.oneshot(
Request::builder()
.method("POST")
.uri(api(&format!(
"/sessions/{session_id}/turns/{turn_id}/interrupt"
)))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let interrupt_error = response_json!(interrupt_response, StatusCode::CONFLICT).await;
assert_eq!(
interrupt_error["errors"][0]["detail"],
"Turn is already terminal."
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn session_events_sse_replays_history_then_streams_live_events() {
let state = test_app_state_with_isolated_storage();
let app = crate::test_support::build_test_router(Arc::clone(&state));
let create_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri(api("/sessions"))
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"working_dir": "/tmp",
"provider": "openai",
"model": "gpt-5.4-mini",
"permissions": "read-only"
})
.to_string(),
))
.unwrap(),
)
.await
.unwrap();
let created = response_json!(create_response, StatusCode::CREATED).await;
let session_id = created["id"].as_str().unwrap().to_string();
let events_response = app
.clone()
.oneshot(
Request::builder()
.method("GET")
.uri(api(&format!("/sessions/{session_id}/events?since_seq=1")))
.header(header::ACCEPT, "text/event-stream")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(events_response.status(), StatusCode::OK);
assert!(
events_response
.headers()
.get(header::CONTENT_TYPE)
.unwrap()
.to_str()
.unwrap()
.contains("text/event-stream")
);
let mut events_body = events_response.into_body();
let replay = read_sse_until(&mut events_body, "session.created").await;
assert_eq!(sse_events(&replay)[0]["event"], "session.created");
let stream_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri(api(&format!("/sessions/{session_id}/turns")))
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({"input": "hello"}).to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(stream_response.status(), StatusCode::OK);
let _ = to_bytes(stream_response.into_body(), usize::MAX)
.await
.unwrap();
let live = read_sse_until(&mut events_body, "turn.failed").await;
let event_names = sse_events(&live)
.into_iter()
.map(|event| event["event"].as_str().unwrap().to_string())
.collect::<Vec<_>>();
assert!(event_names.contains(&"turn.running".to_string()));
assert!(event_names.contains(&"turn.failed".to_string()));
}
#[tokio::test]
async fn streaming_session_turn_updates_runtime_context_without_copying_prior_history_to_turn() {
let llm = MockServer::start_async().await;
let response_mock = llm
.mock_async(|when, then| {
when.method(POST)
.path("/v1/responses")
.header("authorization", "Bearer openai-key");
then.status(200)
.header("content-type", "text/event-stream")
.body(openai_stream_body("new answer"));
})
.await;
// Use an isolated storage root so parallel tests do not race on the
// shared default session storage directory. `session_store` writes
// `session.json` via `fs::write`, which truncates before writing; a
// concurrent reader can observe the empty file and fail to deserialize.
let storage_dir = std::env::temp_dir().join(format!("fabro-server-test-{}", Ulid::new()));
std::fs::create_dir_all(&storage_dir).expect("test storage dir should be creatable");
let server_settings = server_settings_from_toml(&format!(
r#"
_version = 1
[server.storage]
root = "{}"
[server.auth]
methods = ["dev-token"]
"#,
storage_dir.display()
));
let state = TestAppStateBuilder::new()
.runtime_settings(server_settings, RunLayer::default())
.max_concurrent_runs(5)
.provider_base_url("openai", llm.url("/v1"))
.build();
state
.vault
.write()
.await
.set("OPENAI_API_KEY", "openai-key", SecretType::Token, None)
.unwrap();
let app = crate::test_support::build_test_router(Arc::clone(&state));
let create_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri(api("/sessions"))
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"working_dir": "/tmp",
"provider": "openai",
"model": "gpt-5.4-mini",
"permissions": "read-only"
})
.to_string(),
))
.unwrap(),
)
.await
.unwrap();
let created = response_json!(create_response, StatusCode::CREATED).await;
let session_id = created["id"].as_str().unwrap().to_string();
let session_id_typed = session_id.parse().unwrap();
let now = Utc::now();
let mut record = state
.session_store()
.get_session(session_id_typed)
.await
.unwrap()
.unwrap();
record.runtime_context = vec![
SessionMessage::user("prior question", now),
SessionMessage::Assistant {
content: "prior answer".to_string(),
tool_calls: Vec::new(),
provider_parts: Vec::new(),
usage: json!({ "input_tokens": 0, "output_tokens": 0 }),
response_id: "prior_resp".to_string(),
timestamp: now,
},
];
state.session_store().update_session(record).await.unwrap();
let stream_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri(api(&format!("/sessions/{session_id}/turns")))
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({"input": "new question"}).to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(stream_response.status(), StatusCode::OK);
let body = String::from_utf8(
to_bytes(stream_response.into_body(), usize::MAX)
.await
.unwrap()
.to_vec(),
)
.unwrap();
assert!(body.contains("turn.succeeded"), "SSE body: {body}");
let session_response = app
.clone()
.oneshot(
Request::builder()
.method("GET")
.uri(api(&format!("/sessions/{session_id}")))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let session = response_json!(session_response, StatusCode::OK).await;
let runtime_context = session["runtime_context"].as_array().unwrap();
assert_eq!(runtime_context.len(), 4);
assert!(
runtime_context
.iter()
.any(|message| message["content"] == "prior question")
);
assert!(
runtime_context
.iter()
.any(|message| message["content"] == "new answer")
);
let turns_response = app
.oneshot(
Request::builder()
.method("GET")
.uri(api(&format!("/sessions/{session_id}/turns")))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let turns = response_json!(turns_response, StatusCode::OK).await;
let turn = &turns["data"][0];
assert_eq!(turn["input"], "new question");
assert_eq!(turn["output"], "new answer");
assert!(turn.get("messages").is_none());
response_mock.assert_async().await;
}
async fn hanging_openai_responses()
-> Sse<impl futures_util::Stream<Item = Result<SseEvent, Infallible>>> {
let created = json!({
"type": "response.created",
"response": {
"id": "resp_hanging",
"model": "gpt-5.4-mini"
}
});
let first = stream::once(async move {
Ok(SseEvent::default()
.event("response.created")
.data(created.to_string()))
});
Sse::new(first.chain(stream::pending()))
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn interrupt_active_session_turn_cancels_runtime_and_persists_interrupted() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let llm_addr = listener.local_addr().unwrap();
let llm_app = Router::new().route("/v1/responses", post(hanging_openai_responses));
let llm_handle = tokio::spawn(async move {
axum::serve(listener, llm_app).await.unwrap();
});
// Use an isolated storage root so that other tests' AppState startup does
// not run `recover_stale_running_state` against this test's session
// directory and mark its in-flight turn as Interrupted.
let storage_dir = std::env::temp_dir().join(format!("fabro-server-test-{}", Ulid::new()));
std::fs::create_dir_all(&storage_dir).expect("test storage dir should be creatable");
let server_settings = server_settings_from_toml(&format!(
r#"
_version = 1
[server.storage]
root = "{}"
[server.auth]
methods = ["dev-token"]
"#,
storage_dir.display()
));
let state = TestAppStateBuilder::new()
.runtime_settings(server_settings, RunLayer::default())
.max_concurrent_runs(5)
.provider_base_url("openai", format!("http://{llm_addr}/v1"))
.build();
state
.vault
.write()
.await
.set("OPENAI_API_KEY", "openai-key", SecretType::Token, None)
.unwrap();
let app = crate::test_support::build_test_router(Arc::clone(&state));
let create_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri(api("/sessions"))
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({
"working_dir": "/tmp",
"provider": "openai",
"model": "gpt-5.4-mini",
"permissions": "read-only"
})
.to_string(),
))
.unwrap(),
)
.await
.unwrap();
let created = response_json!(create_response, StatusCode::CREATED).await;
let session_id = created["id"].as_str().unwrap().to_string();
let stream_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri(api(&format!("/sessions/{session_id}/turns")))
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({"input": "please wait"}).to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(stream_response.status(), StatusCode::OK);
let mut stream_body = stream_response.into_body();
// Wait for `turn.assistant_text_start` so the agent is committed to the
// in-flight LLM call. Waiting only for `turn.running` (emitted before
// `build_agent_session` and `process_input`) leaves a window in which the
// turn can reach a terminal state before the interrupt request arrives,
// making the assertions below flaky under heavy CI load.
let running = read_sse_until(&mut stream_body, "turn.assistant_text_start").await;
let turn_id = sse_events(&running)
.into_iter()
.find_map(|event| {
(event["event"] == "turn.running")
.then(|| event["turn_id"].as_str().unwrap().to_string())
})
.expect("running event should include turn id");
let conflict_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri(api(&format!("/sessions/{session_id}/turns")))
.header("content-type", "application/json")
.body(Body::from(
serde_json::json!({"input": "second"}).to_string(),
))
.unwrap(),
)
.await
.unwrap();
checked_response!(conflict_response, StatusCode::CONFLICT).await;
let delete_response = app
.clone()
.oneshot(
Request::builder()
.method("DELETE")
.uri(api(&format!("/sessions/{session_id}")))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let delete_error = response_json!(delete_response, StatusCode::CONFLICT).await;
assert_eq!(
delete_error["errors"][0]["detail"],
"Session has an active turn."
);
let interrupt_response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri(api(&format!(
"/sessions/{session_id}/turns/{turn_id}/interrupt"
)))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let interrupt_event = response_json!(interrupt_response, StatusCode::ACCEPTED).await;
assert_eq!(interrupt_event["event"], "turn.interrupt_requested");
let interrupted = read_sse_until(&mut stream_body, "turn.interrupted").await;
assert!(
sse_events(&interrupted)
.iter()
.any(|event| event["event"] == "turn.interrupted")
);
let turn_response = app
.oneshot(
Request::builder()
.method("GET")
.uri(api(&format!("/sessions/{session_id}/turns/{turn_id}")))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let turn = response_json!(turn_response, StatusCode::OK).await;
assert_eq!(turn["status"], "interrupted");
llm_handle.abort();
}
#[tokio::test]
async fn create_run_response_includes_web_url_when_web_enabled() {
let state = test_app_state_with_options(

View file

@ -7,6 +7,7 @@ mod routing;
mod run_files;
mod runs;
mod sandbox_vnc;
mod sessions;
mod settings;
mod system;
mod tcp;

View file

@ -0,0 +1,395 @@
use axum::body::{Body, to_bytes};
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
use crate::helpers::{
MINIMAL_DOT, api, minimal_manifest_json, response_json, response_status, test_app_state,
test_app_with_no_providers,
};
async fn create_run(app: &axum::Router) -> String {
let request = Request::builder()
.method("POST")
.uri(api("/runs"))
.header("content-type", "application/json")
.body(Body::from(
serde_json::to_string(&minimal_manifest_json(MINIMAL_DOT))
.expect("manifest should serialize"),
))
.expect("create-run request should build");
let body = response_json(
app.clone().oneshot(request).await.unwrap(),
StatusCode::CREATED,
"POST /api/v1/runs",
)
.await;
body["id"]
.as_str()
.expect("create-run response should include an id")
.to_string()
}
async fn create_session(app: &axum::Router, run_id: &str, title: &str) -> serde_json::Value {
create_session_with_body(app, run_id, serde_json::json!({ "title": title })).await
}
async fn create_session_with_model(
app: &axum::Router,
run_id: &str,
title: &str,
model: &str,
) -> serde_json::Value {
create_session_with_body(
app,
run_id,
serde_json::json!({ "title": title, "model": model }),
)
.await
}
async fn create_session_with_body(
app: &axum::Router,
run_id: &str,
body: serde_json::Value,
) -> serde_json::Value {
response_json(
create_session_response(app, run_id, body).await,
StatusCode::CREATED,
format!("POST /api/v1/runs/{run_id}/sessions"),
)
.await
}
async fn create_session_response(
app: &axum::Router,
run_id: &str,
body: serde_json::Value,
) -> axum::response::Response {
let request = Request::builder()
.method("POST")
.uri(api(&format!("/runs/{run_id}/sessions")))
.header("content-type", "application/json")
.body(Body::from(
serde_json::to_string(&body).expect("session request should serialize"),
))
.expect("create-session request should build");
app.clone().oneshot(request).await.unwrap()
}
#[tokio::test]
async fn run_bound_session_is_created_as_run_event_and_resolves_by_flat_id() {
let app = fabro_server::test_support::build_test_router(test_app_state());
let run_id = create_run(&app).await;
let created = create_session(&app, &run_id, "Ask Fabro").await;
let session_id = created["id"]
.as_str()
.expect("session response should include an id");
assert_eq!(created["run_id"], run_id);
assert_eq!(created["title"], "Ask Fabro");
assert_session_metadata_only(&created);
assert!(session_id.parse::<fabro_types::SessionId>().is_ok());
let get_request = Request::builder()
.method("GET")
.uri(api(&format!("/sessions/{session_id}")))
.body(Body::empty())
.expect("get-session request should build");
let fetched = response_json(
app.clone().oneshot(get_request).await.unwrap(),
StatusCode::OK,
format!("GET /api/v1/sessions/{session_id}"),
)
.await;
assert_eq!(fetched["id"], session_id);
assert_eq!(fetched["run_id"], run_id);
assert_session_metadata_only(&fetched);
let events_request = Request::builder()
.method("GET")
.uri(api(&format!("/runs/{run_id}/events")))
.body(Body::empty())
.expect("run-events request should build");
let events = response_json(
app.clone().oneshot(events_request).await.unwrap(),
StatusCode::OK,
format!("GET /api/v1/runs/{run_id}/events"),
)
.await;
let session_events: Vec<_> = events["data"]
.as_array()
.expect("events response should include data")
.iter()
.filter(|event| event["session_id"] == session_id)
.collect();
assert_eq!(session_events.len(), 1);
assert_eq!(session_events[0]["event"], "run.session.created");
assert!(
session_events[0]["properties"].get("permissions").is_none(),
"run session creation event should not expose permissions"
);
}
#[tokio::test]
async fn sessions_are_listed_only_under_their_owning_run() {
let app = fabro_server::test_support::build_test_router(test_app_state());
let first_run_id = create_run(&app).await;
let second_run_id = create_run(&app).await;
let created = create_session(&app, &first_run_id, "First run chat").await;
let first_request = Request::builder()
.method("GET")
.uri(api(&format!("/runs/{first_run_id}/sessions")))
.body(Body::empty())
.expect("list sessions request should build");
let first = response_json(
app.clone().oneshot(first_request).await.unwrap(),
StatusCode::OK,
format!("GET /api/v1/runs/{first_run_id}/sessions"),
)
.await;
assert_eq!(first["data"].as_array().unwrap().len(), 1);
assert_eq!(first["data"][0]["id"], created["id"]);
assert_session_metadata_only(&first["data"][0]);
let second_request = Request::builder()
.method("GET")
.uri(api(&format!("/runs/{second_run_id}/sessions")))
.body(Body::empty())
.expect("list sessions request should build");
let second = response_json(
app.clone().oneshot(second_request).await.unwrap(),
StatusCode::OK,
format!("GET /api/v1/runs/{second_run_id}/sessions"),
)
.await;
assert!(second["data"].as_array().unwrap().is_empty());
}
#[tokio::test]
async fn supplied_session_model_alias_is_canonicalized() {
let app = fabro_server::test_support::build_test_router(test_app_state());
let run_id = create_run(&app).await;
let created = create_session_with_model(&app, &run_id, "Ask Fabro", "gpt54").await;
assert_eq!(created["model"], "gpt-5.4");
let events_request = Request::builder()
.method("GET")
.uri(api(&format!("/runs/{run_id}/events")))
.body(Body::empty())
.expect("run-events request should build");
let events = response_json(
app.clone().oneshot(events_request).await.unwrap(),
StatusCode::OK,
format!("GET /api/v1/runs/{run_id}/events"),
)
.await;
let created_event = events["data"]
.as_array()
.expect("events response should include data")
.iter()
.find(|event| event["event"] == "run.session.created")
.expect("session creation event should be recorded");
assert_eq!(created_event["properties"]["model"], "gpt-5.4");
}
#[tokio::test]
async fn provider_qualified_session_model_is_canonicalized() {
let app = fabro_server::test_support::build_test_router(test_app_state());
let run_id = create_run(&app).await;
let created = create_session_with_model(&app, &run_id, "Ask Fabro", "openai/gpt-5.4").await;
assert_eq!(created["model"], "gpt-5.4");
}
#[tokio::test]
async fn invalid_session_model_refs_are_rejected_at_creation() {
let app = fabro_server::test_support::build_test_router(test_app_state());
let run_id = create_run(&app).await;
for model in ["not-a-real-model", "openai", "openai/", "anthropic/gpt-5.4"] {
let response = create_session_response(
&app,
&run_id,
serde_json::json!({ "title": "Ask Fabro", "model": model }),
)
.await;
response_status(
response,
StatusCode::BAD_REQUEST,
format!("POST /api/v1/runs/{run_id}/sessions with model {model}"),
)
.await;
}
}
#[tokio::test]
async fn ambiguous_session_model_refs_are_rejected_at_creation() {
let mut catalog_settings = fabro_model::catalog::LlmCatalogSettings::default();
catalog_settings.providers.insert(
"openai".to_string(),
fabro_model::catalog::ProviderCatalogSettings {
aliases: Some(vec!["gpt54".to_string()]),
..fabro_model::catalog::ProviderCatalogSettings::default()
},
);
let state = fabro_server::test_support::TestAppStateBuilder::new()
.llm_catalog_settings(catalog_settings)
.build();
let app = fabro_server::test_support::build_test_router(state);
let run_id = create_run(&app).await;
let response = create_session_response(
&app,
&run_id,
serde_json::json!({ "title": "Ask Fabro", "model": "gpt54" }),
)
.await;
response_status(
response,
StatusCode::BAD_REQUEST,
format!("POST /api/v1/runs/{run_id}/sessions with ambiguous model gpt54"),
)
.await;
}
#[tokio::test]
async fn session_turn_fails_when_selected_model_provider_is_unconfigured() {
let app = test_app_with_no_providers();
let run_id = create_run(&app).await;
let created = create_session_with_model(&app, &run_id, "Ask Fabro", "gpt54").await;
let session_id = created["id"]
.as_str()
.expect("session response should include an id");
let request = Request::builder()
.method("POST")
.uri(api(&format!("/sessions/{session_id}/turns")))
.header("content-type", "application/json")
.body(Body::from(r#"{"input":"Which provider are you using?"}"#))
.expect("submit-turn request should build");
let response = app.clone().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let events = session_sse_events(response).await;
let failed = events
.iter()
.find(|event| event["event"] == "run.session.turn.failed")
.expect("selected provider failure should be streamed");
assert!(
failed["properties"]["error"]
.as_str()
.expect("failure event should include an error")
.contains("provider 'openai'"),
"failure should be for the selected model provider: {failed:?}"
);
}
#[tokio::test]
async fn session_metadata_patch_route_is_removed() {
let app = fabro_server::test_support::build_test_router(test_app_state());
let run_id = create_run(&app).await;
let created = create_session(&app, &run_id, "Ask Fabro").await;
let session_id = created["id"]
.as_str()
.expect("session response should include an id");
let request = Request::builder()
.method("PATCH")
.uri(api(&format!("/sessions/{session_id}")))
.header("content-type", "application/json")
.body(Body::from(r#"{"title":"Renamed"}"#))
.expect("patch-session request should build");
response_status(
app.clone().oneshot(request).await.unwrap(),
StatusCode::NOT_FOUND,
format!("PATCH /api/v1/sessions/{session_id}"),
)
.await;
}
#[tokio::test]
async fn derived_session_read_routes_are_removed() {
let app = fabro_server::test_support::build_test_router(test_app_state());
let run_id = create_run(&app).await;
let created = create_session(&app, &run_id, "Ask Fabro").await;
let session_id = created["id"]
.as_str()
.expect("session response should include an id");
let turn_id = fabro_types::TurnId::new();
for path in [
format!("/sessions/{session_id}/turns"),
format!("/sessions/{session_id}/turns/{turn_id}"),
format!("/sessions/{session_id}/events"),
] {
let request = Request::builder()
.method("GET")
.uri(api(&path))
.body(Body::empty())
.expect("removed session read request should build");
response_status(
app.clone().oneshot(request).await.unwrap(),
StatusCode::NOT_FOUND,
format!("GET /api/v1{path}"),
)
.await;
}
}
#[tokio::test]
async fn inactive_turn_interrupt_returns_conflict() {
let app = fabro_server::test_support::build_test_router(test_app_state());
let run_id = create_run(&app).await;
let created = create_session(&app, &run_id, "Ask Fabro").await;
let session_id = created["id"]
.as_str()
.expect("session response should include an id");
let turn_id = fabro_types::TurnId::new();
let request = Request::builder()
.method("POST")
.uri(api(&format!(
"/sessions/{session_id}/turns/{turn_id}/interrupt"
)))
.body(Body::empty())
.expect("interrupt request should build");
response_status(
app.clone().oneshot(request).await.unwrap(),
StatusCode::CONFLICT,
format!("POST /api/v1/sessions/{session_id}/turns/{turn_id}/interrupt"),
)
.await;
}
async fn session_sse_events(response: axum::response::Response) -> Vec<serde_json::Value> {
let bytes = to_bytes(response.into_body(), usize::MAX)
.await
.expect("session event stream body should be readable");
let body = String::from_utf8(bytes.to_vec()).expect("session event stream should be UTF-8");
body.lines()
.filter_map(|line| line.strip_prefix("data: "))
.map(|data| serde_json::from_str(data).expect("session event data should be JSON"))
.collect()
}
fn assert_session_metadata_only(value: &serde_json::Value) {
let object = value
.as_object()
.expect("session response should be a JSON object");
for field in [
"working_dir",
"provider",
"permissions",
"deleted_at",
"runtime_context",
] {
assert!(
!object.contains_key(field),
"session metadata should not expose {field}"
);
}
}

View file

@ -1,6 +1,6 @@
use std::fmt::{self, Write};
use fabro_types::{RunBlobId, RunId};
use fabro_types::{RunBlobId, RunId, SessionId};
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct SlateKey(String);
@ -70,6 +70,14 @@ pub(crate) fn blobs_prefix() -> SlateKey {
SlateKey::new("blobs").with("sha256").into_prefix()
}
pub(crate) fn sessions_by_id_prefix() -> SlateKey {
SlateKey::new("sessions").with("by-id").into_prefix()
}
pub(crate) fn session_by_id_key(session_id: &SessionId) -> SlateKey {
SlateKey::new("sessions").with("by-id").with(session_id)
}
// --- Parsing ---
pub(crate) fn parse_event_seq(key: &str) -> Option<u32> {

View file

@ -5,9 +5,9 @@ mod error;
mod keyed_mutex;
mod keys;
mod record;
mod run_sessions;
mod run_state;
mod serializable_projection;
mod session_store;
mod slate;
mod types;
@ -20,9 +20,12 @@ pub use fabro_types::{
EventEnvelope, PendingInterviewRecord, Run, RunBlobId, RunProjection, StageId, StageProjection,
};
pub(crate) use keyed_mutex::KeyedMutex;
pub use run_sessions::{
ProjectedRunSession, project_run_session, project_run_session_with_context,
project_run_sessions,
};
pub use run_state::RunProjectionReducer;
pub use serializable_projection::SerializableProjection;
pub use session_store::SessionStore;
pub use slate::{
AuthCode, AuthCodeStore, Blob, BlobStore, CachedRunProjection, ConsumeOutcome, Database,
RefreshToken, RefreshTokenStore, RunCatalogIndex, RunDatabase, Runs, UnreadableRun,
@ -35,191 +38,3 @@ pub struct ListRunsQuery {
pub end: Option<DateTime<Utc>>,
pub parent_id: Option<fabro_types::RunId>,
}
#[cfg(test)]
mod session_store_contract_tests {
use chrono::Utc;
use fabro_types::{
SessionEventEnvelope, SessionId, SessionRecord, SessionStatus, TurnId, TurnRecord,
TurnStatus,
};
use serde_json::json;
use crate::SessionStore;
#[tokio::test]
async fn create_list_read_delete_sessions_and_replay_events() {
let root = tempfile::tempdir().expect("temp store root should exist");
let store = SessionStore::new(root.path().join("sessions"));
let session_id = SessionId::new();
let turn_id = TurnId::new();
let now = Utc::now();
let mut session = SessionRecord::new(session_id, now);
session.title = Some("Investigate failure".to_string());
session.working_dir = Some("/tmp/project".to_string());
session.provider = Some("openai".to_string());
session.model = Some("gpt-5.4-mini".to_string());
store
.create_session(session.clone())
.await
.expect("session should persist");
store
.append_turn(TurnRecord {
id: turn_id,
session_id,
input: "hello".to_string(),
status: TurnStatus::Succeeded,
output: Some("world".to_string()),
error: None,
created_at: now,
updated_at: now,
completed_at: Some(now),
})
.await
.expect("turn should persist");
let first = store
.append_event(SessionEventEnvelope::new(
session_id,
Some(turn_id),
"session.created",
json!({"title": "Investigate failure"}),
now,
))
.await
.expect("first event should persist");
let second = store
.append_event(SessionEventEnvelope::new(
session_id,
Some(turn_id),
"turn.completed",
json!({"ok": true}),
now,
))
.await
.expect("second event should persist");
assert_eq!(first.seq, 1);
assert_eq!(second.seq, 2);
assert_eq!(
store
.get_session(session_id)
.await
.expect("session read should succeed")
.expect("session should exist")
.working_dir
.as_deref(),
Some("/tmp/project")
);
assert_eq!(
store
.list_sessions()
.await
.expect("session list should succeed")
.len(),
1
);
assert_eq!(
store
.list_turns(session_id)
.await
.expect("turn list should succeed")
.len(),
1
);
let replayed = store
.list_events(session_id, Some(2))
.await
.expect("event replay should succeed");
assert_eq!(
replayed
.into_iter()
.map(|event| event.event)
.collect::<Vec<_>>(),
vec!["turn.completed".to_string()]
);
store
.delete_session(session_id)
.await
.expect("delete should succeed");
assert!(
store
.get_session(session_id)
.await
.expect("session read should succeed")
.is_none()
);
assert!(
store
.list_sessions()
.await
.expect("session list should succeed")
.is_empty()
);
}
#[test]
fn session_and_turn_ids_reject_malformed_values() {
assert!("not-a-ulid".parse::<SessionId>().is_err());
assert!("not-a-ulid".parse::<TurnId>().is_err());
assert_eq!(SessionStatus::Idle.as_str(), "idle");
}
#[tokio::test]
async fn stale_running_sessions_and_turns_recover_to_idle_and_interrupted() {
let root = tempfile::tempdir().expect("temp store root should exist");
let store = SessionStore::new(root.path().join("sessions"));
let session_id = SessionId::new();
let running_turn_id = TurnId::new();
let now = Utc::now();
let mut session = SessionRecord::new(session_id, now);
session.status = SessionStatus::Running;
store
.create_session(session)
.await
.expect("session should persist");
store
.append_turn(TurnRecord {
id: running_turn_id,
session_id,
input: "hello".to_string(),
status: TurnStatus::Running,
output: None,
error: None,
created_at: now,
updated_at: now,
completed_at: None,
})
.await
.expect("turn should persist");
let recovered_at = now + chrono::Duration::seconds(5);
store
.recover_stale_running_state(recovered_at)
.expect("stale runtime state should recover");
let recovered = store
.get_session(session_id)
.await
.expect("session read should succeed")
.expect("session should exist");
assert_eq!(recovered.status, SessionStatus::Idle);
assert_eq!(recovered.updated_at, recovered_at);
let turns = store
.list_turns(session_id)
.await
.expect("turn list should succeed");
assert_eq!(turns.len(), 1);
for turn in turns {
assert_eq!(turn.status, TurnStatus::Interrupted);
assert_eq!(turn.completed_at, Some(recovered_at));
assert_eq!(
turn.error.as_deref(),
Some("Server restarted before the turn completed.")
);
}
}
}

View file

@ -0,0 +1,414 @@
use std::collections::BTreeMap;
use fabro_types::run_event::{RunSessionToolCallCompletedProps, RunSessionToolCallStartedProps};
use fabro_types::{
EventBody, EventEnvelope, RunId, SessionId, SessionMessage, SessionRecord, SessionStatus,
SessionSummary,
};
use serde_json::json;
#[derive(Debug, Clone, PartialEq)]
pub struct ProjectedRunSession {
pub record: SessionRecord,
pub runtime_context: Vec<SessionMessage>,
}
pub fn project_run_sessions(run_id: RunId, events: &[EventEnvelope]) -> Vec<SessionSummary> {
let mut projection = RunSessionProjection::metadata_only();
projection.apply(run_id, events);
projection
.sessions
.values()
.map(|session| SessionSummary::from(&session.record))
.collect()
}
pub fn project_run_session(
run_id: RunId,
session_id: SessionId,
events: &[EventEnvelope],
) -> Option<SessionRecord> {
project_run_session_with_context(run_id, session_id, events).map(|session| session.record)
}
pub fn project_run_session_with_context(
run_id: RunId,
session_id: SessionId,
events: &[EventEnvelope],
) -> Option<ProjectedRunSession> {
let mut projection = RunSessionProjection::with_context_for(session_id);
projection.apply(run_id, events);
projection.sessions.remove(&session_id)
}
struct RunSessionProjection {
sessions: BTreeMap<SessionId, ProjectedRunSession>,
context: RuntimeContextProjection,
}
enum RuntimeContextProjection {
None,
Session(SessionId),
}
impl RunSessionProjection {
fn metadata_only() -> Self {
Self {
sessions: BTreeMap::new(),
context: RuntimeContextProjection::None,
}
}
fn with_context_for(session_id: SessionId) -> Self {
Self {
sessions: BTreeMap::new(),
context: RuntimeContextProjection::Session(session_id),
}
}
fn apply(&mut self, run_id: RunId, events: &[EventEnvelope]) {
for envelope in events {
let Some(session_id) = event_session_id(envelope) else {
continue;
};
match &envelope.event.body {
EventBody::RunSessionCreated(props) => {
let mut record = SessionRecord::new(session_id, run_id, envelope.event.ts);
record.title.clone_from(&props.title);
record.model.clone_from(&props.model);
let projected = ProjectedRunSession {
record,
runtime_context: Vec::new(),
};
self.sessions.insert(session_id, projected);
}
EventBody::RunSessionTurnStarted(_) => {
if let Some(session) = self.sessions.get_mut(&session_id) {
session.record.status = SessionStatus::Running;
session.record.updated_at = envelope.event.ts;
}
}
EventBody::RunSessionUserMessage(props) => {
let project_context = self.should_project_context(session_id);
if let Some(session) = self.sessions.get_mut(&session_id) {
if project_context {
session
.runtime_context
.push(SessionMessage::user(props.text.clone(), envelope.event.ts));
}
session.record.updated_at = envelope.event.ts;
}
}
EventBody::RunSessionAssistantMessage(props) => {
let project_context = self.should_project_context(session_id);
if let Some(session) = self.sessions.get_mut(&session_id) {
if project_context {
session.runtime_context.push(SessionMessage::Assistant {
content: props.text.clone(),
tool_calls: Vec::new(),
provider_parts: Vec::new(),
usage: props.usage.clone(),
response_id: String::new(),
timestamp: envelope.event.ts,
});
}
session.record.updated_at = envelope.event.ts;
}
}
EventBody::RunSessionToolCallStarted(props) => {
let project_context = self.should_project_context(session_id);
if let Some(session) = self.sessions.get_mut(&session_id) {
if project_context {
append_tool_call(session, props);
}
session.record.updated_at = envelope.event.ts;
}
}
EventBody::RunSessionToolCallCompleted(props) => {
let project_context = self.should_project_context(session_id);
if let Some(session) = self.sessions.get_mut(&session_id) {
if project_context {
append_tool_result(session, props, envelope.event.ts);
}
session.record.updated_at = envelope.event.ts;
}
}
EventBody::RunSessionTurnFailed(_) => {
self.finish_turn(session_id, true, envelope.event.ts);
}
EventBody::RunSessionTurnSucceeded(_) | EventBody::RunSessionTurnInterrupted(_) => {
self.finish_turn(session_id, false, envelope.event.ts);
}
_ => {}
}
}
}
fn finish_turn(
&mut self,
session_id: SessionId,
failed: bool,
timestamp: chrono::DateTime<chrono::Utc>,
) {
if let Some(session) = self.sessions.get_mut(&session_id) {
session.record.status = if failed {
SessionStatus::Failed
} else {
SessionStatus::Idle
};
session.record.updated_at = timestamp;
}
}
fn should_project_context(&self, session_id: SessionId) -> bool {
match self.context {
RuntimeContextProjection::None => false,
RuntimeContextProjection::Session(target) => target == session_id,
}
}
}
fn event_session_id(envelope: &EventEnvelope) -> Option<SessionId> {
envelope
.event
.session_id
.as_deref()
.and_then(|id| id.parse().ok())
}
fn append_tool_call(session: &mut ProjectedRunSession, props: &RunSessionToolCallStartedProps) {
if let Some(SessionMessage::Assistant { tool_calls, .. }) = session
.runtime_context
.iter_mut()
.rev()
.find(|message| matches!(message, SessionMessage::Assistant { .. }))
{
tool_calls.push(json!({
"id": props.tool_call_id.clone(),
"name": props.tool_name.clone(),
"arguments": props.arguments.clone(),
}));
}
}
fn append_tool_result(
session: &mut ProjectedRunSession,
props: &RunSessionToolCallCompletedProps,
timestamp: chrono::DateTime<chrono::Utc>,
) {
let result = json!({
"tool_call_id": props.tool_call_id.clone(),
"content": props.output.clone(),
"is_error": props.is_error,
});
if let Some(SessionMessage::ToolResults { results, .. }) = session.runtime_context.last_mut() {
results.push(result);
} else {
session.runtime_context.push(SessionMessage::ToolResults {
results: vec![result],
timestamp,
});
}
}
#[cfg(test)]
mod tests {
use chrono::{TimeZone, Utc};
use fabro_types::run_event::{
RunSessionAssistantMessageProps, RunSessionCreatedProps, RunSessionToolCallCompletedProps,
RunSessionToolCallStartedProps, RunSessionTurnStartedProps, RunSessionTurnSucceededProps,
RunSessionUserMessageProps,
};
use fabro_types::{EventBody, EventEnvelope, RunEvent, SessionMessage, TurnId, fixtures};
use serde_json::json;
use super::{project_run_session, project_run_session_with_context};
#[test]
fn projection_rebuilds_runtime_context_from_run_events() {
let session_id = fabro_types::SessionId::new();
let turn_id = TurnId::new();
let events = vec![
event(
1,
session_id,
EventBody::RunSessionCreated(RunSessionCreatedProps {
title: Some("Ask".to_string()),
model: Some("test-model".to_string()),
}),
),
event(
2,
session_id,
EventBody::RunSessionTurnStarted(RunSessionTurnStartedProps {
turn_id,
input: "What happened?".to_string(),
}),
),
event(
3,
session_id,
EventBody::RunSessionUserMessage(RunSessionUserMessageProps {
turn_id,
text: "What happened?".to_string(),
}),
),
event(
4,
session_id,
EventBody::RunSessionAssistantMessage(RunSessionAssistantMessageProps {
turn_id,
text: "The run finished.".to_string(),
model: Some("test-model".to_string()),
usage: json!({ "output_tokens": 4 }),
}),
),
event(
5,
session_id,
EventBody::RunSessionTurnSucceeded(RunSessionTurnSucceededProps {
turn_id,
output: Some("The run finished.".to_string()),
}),
),
];
let session = project_run_session_with_context(fixtures::RUN_1, session_id, &events)
.expect("session should project from run events");
assert_eq!(session.runtime_context.len(), 2);
assert!(matches!(
&session.runtime_context[0],
SessionMessage::User { content, .. } if content == "What happened?"
));
assert!(matches!(
&session.runtime_context[1],
SessionMessage::Assistant { content, usage, .. }
if content == "The run finished." && usage == &json!({ "output_tokens": 4 })
));
}
#[test]
fn projection_rebuilds_tool_calls_and_results() {
let session_id = fabro_types::SessionId::new();
let turn_id = TurnId::new();
let events = vec![
event(
1,
session_id,
EventBody::RunSessionCreated(RunSessionCreatedProps {
title: None,
model: None,
}),
),
event(
2,
session_id,
EventBody::RunSessionAssistantMessage(RunSessionAssistantMessageProps {
turn_id,
text: String::new(),
model: Some("test-model".to_string()),
usage: json!({}),
}),
),
event(
3,
session_id,
EventBody::RunSessionToolCallStarted(RunSessionToolCallStartedProps {
turn_id,
tool_name: "read_file".to_string(),
tool_call_id: "call_1".to_string(),
arguments: json!({ "path": "README.md" }),
}),
),
event(
4,
session_id,
EventBody::RunSessionToolCallCompleted(RunSessionToolCallCompletedProps {
turn_id,
tool_name: "read_file".to_string(),
tool_call_id: "call_1".to_string(),
output: json!("contents"),
is_error: false,
}),
),
];
let session = project_run_session_with_context(fixtures::RUN_1, session_id, &events)
.expect("session should project from run events");
assert!(matches!(
&session.runtime_context[0],
SessionMessage::Assistant { tool_calls, .. }
if tool_calls == &vec![json!({
"id": "call_1",
"name": "read_file",
"arguments": { "path": "README.md" },
})]
));
assert!(matches!(
&session.runtime_context[1],
SessionMessage::ToolResults { results, .. }
if results == &vec![json!({
"tool_call_id": "call_1",
"content": "contents",
"is_error": false,
})]
));
}
#[test]
fn public_session_record_projection_omits_runtime_context() {
let session_id = fabro_types::SessionId::new();
let turn_id = TurnId::new();
let events = vec![
event(
1,
session_id,
EventBody::RunSessionCreated(RunSessionCreatedProps {
title: Some("Ask".to_string()),
model: Some("test-model".to_string()),
}),
),
event(
2,
session_id,
EventBody::RunSessionUserMessage(RunSessionUserMessageProps {
turn_id,
text: "What happened?".to_string(),
}),
),
];
let session = project_run_session(fixtures::RUN_1, session_id, &events)
.expect("session should project from run events");
assert_eq!(session.updated_at, events[1].event.ts);
let value = serde_json::to_value(session).expect("session should serialize");
assert!(value.get("runtime_context").is_none());
assert!(value.get("working_dir").is_none());
assert!(value.get("provider").is_none());
assert!(value.get("permissions").is_none());
assert!(value.get("deleted_at").is_none());
}
fn event(seq: u32, session_id: fabro_types::SessionId, body: EventBody) -> EventEnvelope {
let event = RunEvent {
id: format!("evt-{seq}"),
ts: Utc.with_ymd_and_hms(2026, 5, 20, 12, 0, seq).unwrap(),
run_id: fixtures::RUN_1,
node_id: None,
node_label: None,
stage_id: None,
parallel_group_id: None,
parallel_branch_id: None,
session_id: Some(session_id.to_string()),
parent_session_id: None,
tool_call_id: None,
actor: None,
body,
};
EventEnvelope { seq, event }
}
}

View file

@ -1,404 +0,0 @@
use std::fs as std_fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use chrono::{DateTime, Utc};
use fabro_types::{
SessionEventEnvelope, SessionId, SessionRecord, SessionStatus, SessionSummary, TurnId,
TurnRecord, TurnStatus,
};
use serde::Serialize;
use serde::de::DeserializeOwned;
use tokio::fs;
use tokio::io::AsyncWriteExt as _;
use tokio::sync::Mutex;
use crate::{Error, Result};
#[derive(Clone, Debug)]
pub struct SessionStore {
root: PathBuf,
write_lock: Arc<Mutex<()>>,
}
impl SessionStore {
#[must_use]
pub fn new(root: impl Into<PathBuf>) -> Self {
Self {
root: root.into(),
write_lock: Arc::new(Mutex::new(())),
}
}
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
pub async fn create_session(&self, mut record: SessionRecord) -> Result<SessionRecord> {
let _guard = self.write_lock.lock().await;
let dir = self.session_dir(record.id);
match fs::try_exists(&dir).await {
Ok(true) => return Err(Error::SessionAlreadyExists(record.id.to_string())),
Ok(false) => {}
Err(err) => return Err(err.into()),
}
fs::create_dir_all(dir.join("turns")).await?;
record.deleted_at = None;
write_json(&self.session_path(record.id), &record).await?;
Ok(record)
}
pub async fn list_sessions(&self) -> Result<Vec<SessionSummary>> {
let mut summaries = Vec::new();
let mut entries = match fs::read_dir(&self.root).await {
Ok(entries) => entries,
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(Vec::new()),
Err(err) => return Err(err.into()),
};
while let Some(entry) = entries.next_entry().await? {
let file_type = entry.file_type().await?;
if !file_type.is_dir() {
continue;
}
let Some(name) = entry.file_name().to_str().map(ToOwned::to_owned) else {
continue;
};
let Ok(session_id) = name.parse::<SessionId>() else {
continue;
};
let Some(record) = self.get_session_including_deleted(session_id).await? else {
continue;
};
if record.deleted_at.is_none() {
summaries.push(SessionSummary::from(&record));
}
}
summaries.sort_by(|a, b| {
b.updated_at
.cmp(&a.updated_at)
.then_with(|| a.id.cmp(&b.id))
});
Ok(summaries)
}
pub async fn get_session(&self, id: SessionId) -> Result<Option<SessionRecord>> {
let Some(record) = self.get_session_including_deleted(id).await? else {
return Ok(None);
};
Ok(record.deleted_at.is_none().then_some(record))
}
async fn get_session_including_deleted(&self, id: SessionId) -> Result<Option<SessionRecord>> {
read_optional_json(&self.session_path(id)).await
}
pub async fn update_session(&self, record: SessionRecord) -> Result<SessionRecord> {
let _guard = self.write_lock.lock().await;
if self
.get_session_including_deleted(record.id)
.await?
.is_none()
{
return Err(Error::SessionNotFound(record.id.to_string()));
}
write_json(&self.session_path(record.id), &record).await?;
Ok(record)
}
pub async fn delete_session(&self, id: SessionId) -> Result<()> {
let _guard = self.write_lock.lock().await;
let Some(mut record) = self.get_session_including_deleted(id).await? else {
return Ok(());
};
let now = Utc::now();
record.status = SessionStatus::Deleted;
record.updated_at = now;
record.deleted_at = Some(now);
write_json(&self.session_path(id), &record).await
}
#[expect(
clippy::disallowed_methods,
reason = "Server startup recovery runs from synchronous AppState construction before routes are served."
)]
pub fn recover_stale_running_state(&self, recovered_at: DateTime<Utc>) -> Result<()> {
let entries = match std_fs::read_dir(&self.root) {
Ok(entries) => entries,
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(()),
Err(err) => return Err(err.into()),
};
for entry in entries {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
let Some(name) = entry.file_name().to_str().map(ToOwned::to_owned) else {
continue;
};
let Ok(session_id) = name.parse::<SessionId>() else {
continue;
};
let session_path = self.session_path(session_id);
let mut session: SessionRecord = match read_json_sync(&session_path) {
Ok(session) => session,
Err(Error::Io(err)) if err.kind() == ErrorKind::NotFound => continue,
Err(Error::Serde(_)) => continue,
Err(err) => return Err(err),
};
if session.deleted_at.is_some() {
continue;
}
if session.status == SessionStatus::Running {
session.status = SessionStatus::Idle;
session.updated_at = recovered_at;
write_json_sync(&session_path, &session)?;
}
self.recover_stale_turns(session_id, recovered_at)?;
}
Ok(())
}
#[expect(
clippy::disallowed_methods,
reason = "Server startup recovery runs from synchronous AppState construction before routes are served."
)]
fn recover_stale_turns(
&self,
session_id: SessionId,
recovered_at: DateTime<Utc>,
) -> Result<()> {
let entries = match std_fs::read_dir(self.turns_dir(session_id)) {
Ok(entries) => entries,
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(()),
Err(err) => return Err(err.into()),
};
for entry in entries {
let entry = entry?;
if !entry.file_type()?.is_file() {
continue;
}
let path = entry.path();
if !path
.extension()
.is_some_and(|extension| extension.eq_ignore_ascii_case("json"))
{
continue;
}
let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) else {
continue;
};
if stem.parse::<TurnId>().is_err() {
continue;
}
let mut turn: TurnRecord = match read_json_sync(&path) {
Ok(turn) => turn,
Err(Error::Io(err)) if err.kind() == ErrorKind::NotFound => continue,
Err(Error::Serde(_)) => continue,
Err(err) => return Err(err),
};
if turn.status != TurnStatus::Running {
continue;
}
turn.status = TurnStatus::Interrupted;
turn.updated_at = recovered_at;
turn.completed_at = Some(recovered_at);
turn.error = Some("Server restarted before the turn completed.".to_string());
write_json_sync(&path, &turn)?;
}
Ok(())
}
pub async fn append_turn(&self, record: TurnRecord) -> Result<TurnRecord> {
let _guard = self.write_lock.lock().await;
if self.get_session(record.session_id).await?.is_none() {
return Err(Error::SessionNotFound(record.session_id.to_string()));
}
fs::create_dir_all(self.turns_dir(record.session_id)).await?;
write_json(&self.turn_path(record.session_id, record.id), &record).await?;
Ok(record)
}
pub async fn update_turn(&self, record: TurnRecord) -> Result<TurnRecord> {
let _guard = self.write_lock.lock().await;
if self.get_session(record.session_id).await?.is_none() {
return Err(Error::SessionNotFound(record.session_id.to_string()));
}
let path = self.turn_path(record.session_id, record.id);
match fs::try_exists(&path).await {
Ok(true) => {}
Ok(false) => return Err(Error::SessionNotFound(record.id.to_string())),
Err(err) => return Err(err.into()),
}
write_json(&path, &record).await?;
Ok(record)
}
pub async fn get_turn(
&self,
session_id: SessionId,
turn_id: TurnId,
) -> Result<Option<TurnRecord>> {
read_optional_json(&self.turn_path(session_id, turn_id)).await
}
pub async fn list_turns(&self, session_id: SessionId) -> Result<Vec<TurnRecord>> {
let mut turns: Vec<TurnRecord> = Vec::new();
let mut entries = match fs::read_dir(self.turns_dir(session_id)).await {
Ok(entries) => entries,
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(Vec::new()),
Err(err) => return Err(err.into()),
};
while let Some(entry) = entries.next_entry().await? {
let file_type = entry.file_type().await?;
if !file_type.is_file() {
continue;
}
let path = entry.path();
if !path
.extension()
.is_some_and(|extension| extension.eq_ignore_ascii_case("json"))
{
continue;
}
let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) else {
continue;
};
if stem.parse::<TurnId>().is_err() {
continue;
}
turns.push(read_json(&path).await?);
}
turns.sort_by_key(|turn| turn.created_at);
Ok(turns)
}
pub async fn append_event(
&self,
mut event: SessionEventEnvelope,
) -> Result<SessionEventEnvelope> {
let _guard = self.write_lock.lock().await;
if self.get_session(event.session_id).await?.is_none() {
return Err(Error::SessionNotFound(event.session_id.to_string()));
}
let path = self.events_path(event.session_id);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await?;
}
event.seq = next_event_seq(&path).await?;
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.await?;
let mut bytes = serde_json::to_vec(&event)?;
bytes.push(b'\n');
file.write_all(&bytes).await?;
file.flush().await?;
Ok(event)
}
pub async fn list_events(
&self,
session_id: SessionId,
since_seq: Option<u32>,
) -> Result<Vec<SessionEventEnvelope>> {
let start = since_seq.unwrap_or(1);
let path = self.events_path(session_id);
let contents = match fs::read_to_string(path).await {
Ok(contents) => contents,
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(Vec::new()),
Err(err) => return Err(err.into()),
};
let mut events = Vec::new();
for line in contents.lines().filter(|line| !line.trim().is_empty()) {
let event: SessionEventEnvelope = serde_json::from_str(line)?;
if event.seq >= start {
events.push(event);
}
}
events.sort_by_key(|event| event.seq);
Ok(events)
}
fn session_dir(&self, id: SessionId) -> PathBuf {
self.root.join(id.to_string())
}
fn session_path(&self, id: SessionId) -> PathBuf {
self.session_dir(id).join("session.json")
}
fn turns_dir(&self, session_id: SessionId) -> PathBuf {
self.session_dir(session_id).join("turns")
}
fn turn_path(&self, session_id: SessionId, turn_id: TurnId) -> PathBuf {
self.turns_dir(session_id).join(format!("{turn_id}.json"))
}
fn events_path(&self, session_id: SessionId) -> PathBuf {
self.session_dir(session_id).join("events.jsonl")
}
}
async fn next_event_seq(path: &Path) -> Result<u32> {
let contents = match fs::read_to_string(path).await {
Ok(contents) => contents,
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(1),
Err(err) => return Err(err.into()),
};
let mut max_seq = 0;
for line in contents.lines().filter(|line| !line.trim().is_empty()) {
let event: SessionEventEnvelope = serde_json::from_str(line)?;
max_seq = max_seq.max(event.seq);
}
Ok(max_seq.saturating_add(1).max(1))
}
async fn write_json<T: Serialize>(path: &Path, value: &T) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await?;
}
let mut bytes = serde_json::to_vec_pretty(value)?;
bytes.push(b'\n');
fs::write(path, bytes).await?;
Ok(())
}
async fn read_json<T: DeserializeOwned>(path: &Path) -> Result<T> {
let bytes = fs::read(path).await?;
Ok(serde_json::from_slice(&bytes)?)
}
async fn read_optional_json<T: DeserializeOwned>(path: &Path) -> Result<Option<T>> {
match fs::read(path).await {
Ok(bytes) => Ok(Some(serde_json::from_slice(&bytes)?)),
Err(err) if err.kind() == ErrorKind::NotFound => Ok(None),
Err(err) => Err(err.into()),
}
}
#[expect(
clippy::disallowed_methods,
reason = "Used only by synchronous server startup recovery before routes are served."
)]
fn write_json_sync<T: Serialize>(path: &Path, value: &T) -> Result<()> {
if let Some(parent) = path.parent() {
std_fs::create_dir_all(parent)?;
}
let mut bytes = serde_json::to_vec_pretty(value)?;
bytes.push(b'\n');
std_fs::write(path, bytes)?;
Ok(())
}
#[expect(
clippy::disallowed_methods,
reason = "Used only by synchronous server startup recovery before routes are served."
)]
fn read_json_sync<T: DeserializeOwned>(path: &Path) -> Result<T> {
let bytes = std_fs::read(path)?;
Ok(serde_json::from_slice(&bytes)?)
}

View file

@ -14,7 +14,7 @@ pub use auth_codes::{AuthCode, AuthCodeStore};
pub use auth_tokens::{ConsumeOutcome, RefreshToken, RefreshTokenStore};
pub use blob_store::{Blob, BlobStore};
use chrono::{DateTime, Utc};
use fabro_types::{Run, RunId};
use fabro_types::{Run, RunId, SessionId};
use object_store::ObjectStore;
pub use projection_cache::CachedRunProjection;
use projection_cache::RunProjectionCache;
@ -34,6 +34,11 @@ pub struct UnreadableRun {
pub error: String,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
struct SessionRunIndexEntry {
run_id: RunId,
}
#[derive(Clone)]
pub struct Database {
object_store: Arc<dyn ObjectStore>,
@ -292,6 +297,29 @@ impl Database {
Ok(self.projection_cache.get_summary(run_id).await)
}
pub async fn put_session_run_index(
&self,
session_id: &SessionId,
run_id: &RunId,
) -> Result<()> {
let db = self.open_db().await?;
db.put(
keys::session_by_id_key(session_id),
serde_json::to_vec(&SessionRunIndexEntry { run_id: *run_id })?,
)
.await?;
Ok(())
}
pub async fn get_session_run_id(&self, session_id: &SessionId) -> Result<Option<RunId>> {
let db = self.open_db().await?;
if let Some(bytes) = db.get(keys::session_by_id_key(session_id)).await? {
let entry: SessionRunIndexEntry = serde_json::from_slice(&bytes)?;
return Ok(Some(entry.run_id));
}
Ok(None)
}
pub(crate) async fn remove_cached_run(&self, run_id: &RunId) {
self.projection_cache.remove(run_id).await;
}
@ -315,11 +343,30 @@ impl Database {
for key in keys_to_delete {
db.delete(key).await?;
}
self.delete_session_indexes_for_run(run_id).await?;
self.catalog_index().await?.remove(run_id).await?;
self.remove_cached_run(run_id).await;
Ok(())
}
async fn delete_session_indexes_for_run(&self, run_id: &RunId) -> Result<()> {
let db = self.open_db().await?;
let mut keys_to_delete = Vec::new();
let mut iter = db.scan_prefix(keys::sessions_by_id_prefix()).await?;
while let Some(entry) = iter.next().await? {
let index: SessionRunIndexEntry = serde_json::from_slice(&entry.value)?;
if index.run_id == *run_id {
keys_to_delete.push(String::from_utf8(entry.key.to_vec()).map_err(|err| {
Error::Other(format!("stored key is not valid UTF-8: {err}"))
})?);
}
}
for key in keys_to_delete {
db.delete(key).await?;
}
Ok(())
}
pub async fn auth_codes(&self) -> Result<Arc<AuthCodeStore>> {
let store = self
.auth_codes

View file

@ -235,6 +235,10 @@ impl RunDatabase {
impl RunDatabase {
pub async fn append_event(&self, payload: &EventPayload) -> Result<u32> {
Ok(self.append_event_envelope(payload).await?.seq)
}
pub async fn append_event_envelope(&self, payload: &EventPayload) -> Result<EventEnvelope> {
if self.read_only {
return Err(Error::ReadOnly);
}
@ -262,7 +266,7 @@ impl RunDatabase {
match Self::build_cached_projection(&self.inner.db, &self.inner.run_id).await {
Ok(Some(entry)) => {
self.inner.shared_projection_cache.replace(entry).await;
return Ok(seq);
return Ok(event);
}
Ok(None) => {
self.inner
@ -289,7 +293,7 @@ impl RunDatabase {
);
return Err(err);
}
Ok(seq)
Ok(event)
}
pub async fn list_events(&self) -> Result<Vec<EventEnvelope>> {

View file

@ -118,8 +118,8 @@ pub use sandbox_services::{
};
pub use secret::{SecretMetadata, SecretType};
pub use session::{
PermissionLevel, SessionEventEnvelope, SessionId, SessionMessage, SessionRecord, SessionStatus,
SessionSummary, TurnId, TurnRecord, TurnStatus,
PermissionLevel, SessionId, SessionMessage, SessionRecord, SessionStatus, SessionSummary,
TurnId,
};
pub use stage_completion::StageCompletion;
pub use stage_handler::StageHandler;

View file

@ -2,6 +2,7 @@ pub mod agent;
pub mod infra;
pub mod misc;
pub mod run;
pub mod session;
pub mod stage;
pub use agent::*;
@ -14,6 +15,7 @@ use serde::de::Error as DeError;
use serde::ser::Error as SerError;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{Map, Value, json};
pub use session::*;
pub use stage::*;
use crate::{ParallelBranchId, Principal, RunId, StageId};
@ -96,6 +98,26 @@ pub enum EventBody {
RunUnarchived(RunUnarchivedProps),
#[serde(rename = "run.title.updated")]
RunTitleUpdated(RunTitleUpdatedProps),
#[serde(rename = "run.session.created")]
RunSessionCreated(RunSessionCreatedProps),
#[serde(rename = "run.session.turn.started")]
RunSessionTurnStarted(RunSessionTurnStartedProps),
#[serde(rename = "run.session.user_message")]
RunSessionUserMessage(RunSessionUserMessageProps),
#[serde(rename = "run.session.assistant_delta")]
RunSessionAssistantDelta(RunSessionAssistantDeltaProps),
#[serde(rename = "run.session.assistant_message")]
RunSessionAssistantMessage(RunSessionAssistantMessageProps),
#[serde(rename = "run.session.tool_call.started")]
RunSessionToolCallStarted(RunSessionToolCallStartedProps),
#[serde(rename = "run.session.tool_call.completed")]
RunSessionToolCallCompleted(RunSessionToolCallCompletedProps),
#[serde(rename = "run.session.turn.succeeded")]
RunSessionTurnSucceeded(RunSessionTurnSucceededProps),
#[serde(rename = "run.session.turn.failed")]
RunSessionTurnFailed(RunSessionTurnFailedProps),
#[serde(rename = "run.session.turn.interrupted")]
RunSessionTurnInterrupted(RunSessionTurnInterruptedProps),
#[serde(rename = "run.parent.linked")]
RunParentLinked(RunParentLinkedProps),
#[serde(rename = "run.parent.unlinked")]
@ -405,6 +427,16 @@ impl EventBody {
Self::RunArchived(_) => "run.archived",
Self::RunUnarchived(_) => "run.unarchived",
Self::RunTitleUpdated(_) => "run.title.updated",
Self::RunSessionCreated(_) => "run.session.created",
Self::RunSessionTurnStarted(_) => "run.session.turn.started",
Self::RunSessionUserMessage(_) => "run.session.user_message",
Self::RunSessionAssistantDelta(_) => "run.session.assistant_delta",
Self::RunSessionAssistantMessage(_) => "run.session.assistant_message",
Self::RunSessionToolCallStarted(_) => "run.session.tool_call.started",
Self::RunSessionToolCallCompleted(_) => "run.session.tool_call.completed",
Self::RunSessionTurnSucceeded(_) => "run.session.turn.succeeded",
Self::RunSessionTurnFailed(_) => "run.session.turn.failed",
Self::RunSessionTurnInterrupted(_) => "run.session.turn.interrupted",
Self::RunParentLinked(_) => "run.parent.linked",
Self::RunParentUnlinked(_) => "run.parent.unlinked",
Self::RunCompleted(_) => "run.completed",
@ -561,6 +593,17 @@ fn is_known_event_name(event: &str) -> bool {
| "run.superseded_by"
| "run.archived"
| "run.unarchived"
| "run.title.updated"
| "run.session.created"
| "run.session.turn.started"
| "run.session.user_message"
| "run.session.assistant_delta"
| "run.session.assistant_message"
| "run.session.tool_call.started"
| "run.session.tool_call.completed"
| "run.session.turn.succeeded"
| "run.session.turn.failed"
| "run.session.turn.interrupted"
| "run.parent.linked"
| "run.parent.unlinked"
| "run.completed"

View file

@ -0,0 +1,79 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::TurnId;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunSessionCreatedProps {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunSessionTurnStartedProps {
pub turn_id: TurnId,
pub input: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunSessionUserMessageProps {
pub turn_id: TurnId,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunSessionAssistantDeltaProps {
pub turn_id: TurnId,
pub delta: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunSessionAssistantMessageProps {
pub turn_id: TurnId,
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default)]
pub usage: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunSessionToolCallStartedProps {
pub turn_id: TurnId,
pub tool_name: String,
pub tool_call_id: String,
pub arguments: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunSessionToolCallCompletedProps {
pub turn_id: TurnId,
pub tool_name: String,
pub tool_call_id: String,
pub output: Value,
pub is_error: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunSessionTurnSucceededProps {
pub turn_id: TurnId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunSessionTurnFailedProps {
pub turn_id: TurnId,
pub error: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RunSessionTurnInterruptedProps {
pub turn_id: TurnId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}

View file

@ -2,6 +2,7 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString, IntoStaticStr};
use crate::RunId;
use crate::id::ulid_id;
ulid_id!(SessionId);
@ -39,8 +40,6 @@ pub enum SessionStatus {
Idle,
Running,
Failed,
Closed,
Deleted,
}
impl SessionStatus {
@ -49,134 +48,56 @@ impl SessionStatus {
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, EnumString, IntoStaticStr,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum TurnStatus {
Running,
Succeeded,
Failed,
Interrupted,
}
impl TurnStatus {
pub fn as_str(self) -> &'static str {
self.into()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SessionRecord {
pub id: SessionId,
pub title: Option<String>,
pub status: SessionStatus,
pub working_dir: Option<String>,
pub provider: Option<String>,
pub model: Option<String>,
pub permissions: PermissionLevel,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub deleted_at: Option<DateTime<Utc>>,
#[serde(default)]
pub runtime_context: Vec<SessionMessage>,
pub id: SessionId,
pub run_id: RunId,
pub title: Option<String>,
pub status: SessionStatus,
pub model: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl SessionRecord {
pub fn new(id: SessionId, now: DateTime<Utc>) -> Self {
pub fn new(id: SessionId, run_id: RunId, now: DateTime<Utc>) -> Self {
Self {
id,
run_id,
title: None,
status: SessionStatus::Idle,
working_dir: None,
provider: None,
model: None,
permissions: PermissionLevel::ReadWrite,
created_at: now,
updated_at: now,
deleted_at: None,
runtime_context: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SessionSummary {
pub id: SessionId,
pub title: Option<String>,
pub status: SessionStatus,
pub working_dir: Option<String>,
pub provider: Option<String>,
pub model: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub id: SessionId,
pub run_id: RunId,
pub title: Option<String>,
pub status: SessionStatus,
pub model: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl From<&SessionRecord> for SessionSummary {
fn from(record: &SessionRecord) -> Self {
Self {
id: record.id,
title: record.title.clone(),
status: record.status,
working_dir: record.working_dir.clone(),
provider: record.provider.clone(),
model: record.model.clone(),
created_at: record.created_at,
updated_at: record.updated_at,
id: record.id,
run_id: record.run_id,
title: record.title.clone(),
status: record.status,
model: record.model.clone(),
created_at: record.created_at,
updated_at: record.updated_at,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TurnRecord {
pub id: TurnId,
pub session_id: SessionId,
pub input: String,
pub status: TurnStatus,
pub output: Option<String>,
pub error: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SessionEventEnvelope {
pub seq: u32,
pub session_id: SessionId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turn_id: Option<TurnId>,
pub event: String,
pub properties: serde_json::Value,
pub ts: DateTime<Utc>,
}
impl SessionEventEnvelope {
pub fn new(
session_id: SessionId,
turn_id: Option<TurnId>,
event: impl Into<String>,
properties: serde_json::Value,
ts: DateTime<Utc>,
) -> Self {
Self {
seq: 0,
session_id,
turn_id,
event: event.into(),
properties,
ts,
}
}
#[must_use]
pub fn with_seq(mut self, seq: u32) -> Self {
self.seq = seq;
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SessionMessage {
@ -225,3 +146,16 @@ impl SessionMessage {
}
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::SessionStatus;
#[test]
fn session_status_rejects_removed_terminal_states() {
assert!(serde_json::from_value::<SessionStatus>(json!("closed")).is_err());
assert!(serde_json::from_value::<SessionStatus>(json!("deleted")).is_err());
}
}

View file

@ -64,8 +64,8 @@ models/completion-usage.ts
models/conclusion.ts
models/create-completion-request.ts
models/create-run-pull-request-request.ts
models/create-run-session-request.ts
models/create-secret-request.ts
models/create-session-request.ts
models/daytona-network-layer-one-of-allow-list.ts
models/daytona-network-layer-one-of.ts
models/daytona-network-layer.ts
@ -190,13 +190,28 @@ models/paginated-run-file-list.ts
models/paginated-run-list.ts
models/paginated-run-stage-list.ts
models/paginated-saved-query-list.ts
models/paginated-session-event-list.ts
models/paginated-session-list.ts
models/paginated-turn-list.ts
models/paginated-workflow-list-response.ts
models/pagination-meta.ts
models/pair-message-record.ts
models/pair-message-request.ts
models/pair-record.ts
models/pair-start-request.ts
models/pair-status.ts
models/pair-target-selector.ts
models/pair-target.ts
models/pair-transcript-assistant-message-model.ts
models/pair-transcript-assistant-message.ts
models/pair-transcript-detail-ref.ts
models/pair-transcript-entry.ts
models/pair-transcript-error.ts
models/pair-transcript-response-meta.ts
models/pair-transcript-response.ts
models/pair-transcript-system-message.ts
models/pair-transcript-tool-call.ts
models/pair-transcript-user-message.ts
models/pair-transcript-warning.ts
models/pending-interview-record.ts
models/permission-level.ts
models/pre-run-push-outcome-failed.ts
models/pre-run-push-outcome-not-attempted.ts
models/pre-run-push-outcome-skipped-no-remote.ts
@ -268,6 +283,9 @@ models/run-commits-meta.ts
models/run-control-action.ts
models/run-diff.ts
models/run-error.ts
models/run-event-detail-response-content.ts
models/run-event-detail-response-event.ts
models/run-event-detail-response.ts
models/run-event.ts
models/run-execution-settings.ts
models/run-failure.ts
@ -288,6 +306,7 @@ models/run-model-settings.ts
models/run-model.ts
models/run-namespace.ts
models/run-origin.ts
models/run-pair-status-response.ts
models/run-prepare-settings.ts
models/run-projection.ts
models/run-provenance.ts
@ -352,7 +371,6 @@ models/server-settings.ts
models/server-slate-db-settings.ts
models/server-storage-settings.ts
models/server-web-settings.ts
models/session-event-envelope.ts
models/session-message.ts
models/session-record.ts
models/session-status.ts
@ -392,11 +410,8 @@ models/system-resources-response.ts
models/system-run-counts.ts
models/timeline-entry-response.ts
models/tls-mode.ts
models/turn-record.ts
models/turn-status.ts
models/update-run-parent-request.ts
models/update-run-request.ts
models/update-session-request.ts
models/user-response.ts
models/validate-response.ts
models/vnc-preview-response.ts

View file

@ -26,10 +26,22 @@ import type { ErrorResponse } from '../models';
// @ts-ignore
import type { PaginatedApiQuestionList } from '../models';
// @ts-ignore
import type { PairMessageRecord } from '../models';
// @ts-ignore
import type { PairMessageRequest } from '../models';
// @ts-ignore
import type { PairRecord } from '../models';
// @ts-ignore
import type { PairStartRequest } from '../models';
// @ts-ignore
import type { PairTranscriptResponse } from '../models';
// @ts-ignore
import type { PreviewUrlRequest } from '../models';
// @ts-ignore
import type { PreviewUrlResponse } from '../models';
// @ts-ignore
import type { RunPairStatusResponse } from '../models';
// @ts-ignore
import type { SandboxDetails } from '../models';
// @ts-ignore
import type { SandboxFileListResponse } from '../models';
@ -135,6 +147,50 @@ export const HumanInTheLoopApiAxiosParamCreator = function (configuration?: Conf
options: localVarRequestOptions,
};
},
/**
*
* @summary End Run Pair
* @param {string} id Unique run identifier (ULID).
* @param {string} pairId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
endRunPair: async (id: string, pairId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('endRunPair', 'id', id)
// verify required parameter 'pairId' is not null or undefined
assertParamExists('endRunPair', 'pairId', pairId)
const localVarPath = `/api/v1/runs/{id}/pair/{pair_id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)))
.replace(`{${"pair_id"}}`, encodeURIComponent(String(pairId)));
// 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: 'DELETE', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication SessionCookie required
// 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,
};
},
/**
* Generates a preview URL for a port exposed by the run\'s sandbox environment.
* @summary Preview URL
@ -180,6 +236,144 @@ export const HumanInTheLoopApiAxiosParamCreator = function (configuration?: Conf
options: localVarRequestOptions,
};
},
/**
*
* @summary Get Run Pair
* @param {string} id Unique run identifier (ULID).
* @param {string} pairId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getRunPair: async (id: string, pairId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('getRunPair', 'id', id)
// verify required parameter 'pairId' is not null or undefined
assertParamExists('getRunPair', 'pairId', pairId)
const localVarPath = `/api/v1/runs/{id}/pair/{pair_id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)))
.replace(`{${"pair_id"}}`, encodeURIComponent(String(pairId)));
// 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 SessionCookie required
// 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,
};
},
/**
*
* @summary Get Run Pair Status
* @param {string} id Unique run identifier (ULID).
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getRunPairStatus: async (id: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('getRunPairStatus', 'id', id)
const localVarPath = `/api/v1/runs/{id}/pair`
.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 SessionCookie required
// 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,
};
},
/**
*
* @summary Get Run Pair Transcript
* @param {string} id Unique run identifier (ULID).
* @param {string} pairId
* @param {number} [sinceSeq] First event sequence number to include.
* @param {number} [limit] Maximum number of events to return.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getRunPairTranscript: async (id: string, pairId: string, sinceSeq?: number, limit?: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('getRunPairTranscript', 'id', id)
// verify required parameter 'pairId' is not null or undefined
assertParamExists('getRunPairTranscript', 'pairId', pairId)
const localVarPath = `/api/v1/runs/{id}/pair/{pair_id}/transcript`
.replace(`{${"id"}}`, encodeURIComponent(String(id)))
.replace(`{${"pair_id"}}`, encodeURIComponent(String(pairId)));
// 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 SessionCookie required
// authentication BearerAuth required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (sinceSeq !== undefined) {
localVarQueryParameter['since_seq'] = sinceSeq;
}
if (limit !== undefined) {
localVarQueryParameter['limit'] = limit;
}
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,
};
},
/**
* Downloads a file from the run\'s sandbox environment.
* @summary Download Sandbox File
@ -501,6 +695,100 @@ export const HumanInTheLoopApiAxiosParamCreator = function (configuration?: Conf
options: localVarRequestOptions,
};
},
/**
*
* @summary Send Run Pair Message
* @param {string} id Unique run identifier (ULID).
* @param {string} pairId
* @param {PairMessageRequest} pairMessageRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
sendRunPairMessage: async (id: string, pairId: string, pairMessageRequest: PairMessageRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('sendRunPairMessage', 'id', id)
// verify required parameter 'pairId' is not null or undefined
assertParamExists('sendRunPairMessage', 'pairId', pairId)
// verify required parameter 'pairMessageRequest' is not null or undefined
assertParamExists('sendRunPairMessage', 'pairMessageRequest', pairMessageRequest)
const localVarPath = `/api/v1/runs/{id}/pair/{pair_id}/messages`
.replace(`{${"id"}}`, encodeURIComponent(String(id)))
.replace(`{${"pair_id"}}`, encodeURIComponent(String(pairId)));
// 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 SessionCookie required
// 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(pairMessageRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
* Starts pairing with exactly one selected active API-mode agent target.
* @summary Start Run Pair
* @param {string} id Unique run identifier (ULID).
* @param {PairStartRequest} pairStartRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
startRunPair: async (id: string, pairStartRequest: PairStartRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('startRunPair', 'id', id)
// verify required parameter 'pairStartRequest' is not null or undefined
assertParamExists('startRunPair', 'pairStartRequest', pairStartRequest)
const localVarPath = `/api/v1/runs/{id}/pair`
.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 SessionCookie required
// 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(pairStartRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
* Send a mid-run steering message to the live agent session(s) of a running run. Set `interrupt=true` to atomically interrupt the active steerable agent round first, then deliver this message as the next user turn. Without `interrupt=true`, the message is appended to the steering queue and may buffer until the next steerable agent session.
* @summary Steer Run
@ -631,6 +919,20 @@ export const HumanInTheLoopApiFp = function(configuration?: Configuration) {
const localVarOperationServerBasePath = operationServerMap['HumanInTheLoopApi.createSandboxVncPreview']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @summary End Run Pair
* @param {string} id Unique run identifier (ULID).
* @param {string} pairId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async endRunPair(id: string, pairId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PairRecord>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.endRunPair(id, pairId, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['HumanInTheLoopApi.endRunPair']?.[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
@ -645,6 +947,49 @@ 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);
},
/**
*
* @summary Get Run Pair
* @param {string} id Unique run identifier (ULID).
* @param {string} pairId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getRunPair(id: string, pairId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PairRecord>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getRunPair(id, pairId, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['HumanInTheLoopApi.getRunPair']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @summary Get Run Pair Status
* @param {string} id Unique run identifier (ULID).
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getRunPairStatus(id: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RunPairStatusResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getRunPairStatus(id, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['HumanInTheLoopApi.getRunPairStatus']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @summary Get Run Pair Transcript
* @param {string} id Unique run identifier (ULID).
* @param {string} pairId
* @param {number} [sinceSeq] First event sequence number to include.
* @param {number} [limit] Maximum number of events to return.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getRunPairTranscript(id: string, pairId: string, sinceSeq?: number, limit?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PairTranscriptResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getRunPairTranscript(id, pairId, sinceSeq, limit, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['HumanInTheLoopApi.getRunPairTranscript']?.[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
@ -743,6 +1088,35 @@ export const HumanInTheLoopApiFp = function(configuration?: Configuration) {
const localVarOperationServerBasePath = operationServerMap['HumanInTheLoopApi.retrieveRunSandbox']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @summary Send Run Pair Message
* @param {string} id Unique run identifier (ULID).
* @param {string} pairId
* @param {PairMessageRequest} pairMessageRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async sendRunPairMessage(id: string, pairId: string, pairMessageRequest: PairMessageRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PairMessageRecord>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.sendRunPairMessage(id, pairId, pairMessageRequest, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['HumanInTheLoopApi.sendRunPairMessage']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
* Starts pairing with exactly one selected active API-mode agent target.
* @summary Start Run Pair
* @param {string} id Unique run identifier (ULID).
* @param {PairStartRequest} pairStartRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async startRunPair(id: string, pairStartRequest: PairStartRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PairRecord>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.startRunPair(id, pairStartRequest, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['HumanInTheLoopApi.startRunPair']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
* Send a mid-run steering message to the live agent session(s) of a running run. Set `interrupt=true` to atomically interrupt the active steerable agent round first, then deliver this message as the next user turn. Without `interrupt=true`, the message is appended to the steering queue and may buffer until the next steerable agent session.
* @summary Steer Run
@ -802,6 +1176,17 @@ export const HumanInTheLoopApiFactory = function (configuration?: Configuration,
createSandboxVncPreview(id: string, options?: RawAxiosRequestConfig): AxiosPromise<VncPreviewResponse> {
return localVarFp.createSandboxVncPreview(id, options).then((request) => request(axios, basePath));
},
/**
*
* @summary End Run Pair
* @param {string} id Unique run identifier (ULID).
* @param {string} pairId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
endRunPair(id: string, pairId: string, options?: RawAxiosRequestConfig): AxiosPromise<PairRecord> {
return localVarFp.endRunPair(id, pairId, options).then((request) => request(axios, basePath));
},
/**
* Generates a preview URL for a port exposed by the run\'s sandbox environment.
* @summary Preview URL
@ -813,6 +1198,40 @@ 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));
},
/**
*
* @summary Get Run Pair
* @param {string} id Unique run identifier (ULID).
* @param {string} pairId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getRunPair(id: string, pairId: string, options?: RawAxiosRequestConfig): AxiosPromise<PairRecord> {
return localVarFp.getRunPair(id, pairId, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Get Run Pair Status
* @param {string} id Unique run identifier (ULID).
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getRunPairStatus(id: string, options?: RawAxiosRequestConfig): AxiosPromise<RunPairStatusResponse> {
return localVarFp.getRunPairStatus(id, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Get Run Pair Transcript
* @param {string} id Unique run identifier (ULID).
* @param {string} pairId
* @param {number} [sinceSeq] First event sequence number to include.
* @param {number} [limit] Maximum number of events to return.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getRunPairTranscript(id: string, pairId: string, sinceSeq?: number, limit?: number, options?: RawAxiosRequestConfig): AxiosPromise<PairTranscriptResponse> {
return localVarFp.getRunPairTranscript(id, pairId, sinceSeq, limit, options).then((request) => request(axios, basePath));
},
/**
* Downloads a file from the run\'s sandbox environment.
* @summary Download Sandbox File
@ -890,6 +1309,29 @@ export const HumanInTheLoopApiFactory = function (configuration?: Configuration,
retrieveRunSandbox(id: string, options?: RawAxiosRequestConfig): AxiosPromise<SandboxDetails> {
return localVarFp.retrieveRunSandbox(id, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Send Run Pair Message
* @param {string} id Unique run identifier (ULID).
* @param {string} pairId
* @param {PairMessageRequest} pairMessageRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
sendRunPairMessage(id: string, pairId: string, pairMessageRequest: PairMessageRequest, options?: RawAxiosRequestConfig): AxiosPromise<PairMessageRecord> {
return localVarFp.sendRunPairMessage(id, pairId, pairMessageRequest, options).then((request) => request(axios, basePath));
},
/**
* Starts pairing with exactly one selected active API-mode agent target.
* @summary Start Run Pair
* @param {string} id Unique run identifier (ULID).
* @param {PairStartRequest} pairStartRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
startRunPair(id: string, pairStartRequest: PairStartRequest, options?: RawAxiosRequestConfig): AxiosPromise<PairRecord> {
return localVarFp.startRunPair(id, pairStartRequest, options).then((request) => request(axios, basePath));
},
/**
* Send a mid-run steering message to the live agent session(s) of a running run. Set `interrupt=true` to atomically interrupt the active steerable agent round first, then deliver this message as the next user turn. Without `interrupt=true`, the message is appended to the steering queue and may buffer until the next steerable agent session.
* @summary Steer Run
@ -943,6 +1385,18 @@ export class HumanInTheLoopApi extends BaseAPI {
return HumanInTheLoopApiFp(this.configuration).createSandboxVncPreview(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary End Run Pair
* @param {string} id Unique run identifier (ULID).
* @param {string} pairId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public endRunPair(id: string, pairId: string, options?: RawAxiosRequestConfig) {
return HumanInTheLoopApiFp(this.configuration).endRunPair(id, pairId, 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
@ -955,6 +1409,43 @@ export class HumanInTheLoopApi extends BaseAPI {
return HumanInTheLoopApiFp(this.configuration).generatePreviewUrl(id, previewUrlRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Get Run Pair
* @param {string} id Unique run identifier (ULID).
* @param {string} pairId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public getRunPair(id: string, pairId: string, options?: RawAxiosRequestConfig) {
return HumanInTheLoopApiFp(this.configuration).getRunPair(id, pairId, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Get Run Pair Status
* @param {string} id Unique run identifier (ULID).
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public getRunPairStatus(id: string, options?: RawAxiosRequestConfig) {
return HumanInTheLoopApiFp(this.configuration).getRunPairStatus(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Get Run Pair Transcript
* @param {string} id Unique run identifier (ULID).
* @param {string} pairId
* @param {number} [sinceSeq] First event sequence number to include.
* @param {number} [limit] Maximum number of events to return.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public getRunPairTranscript(id: string, pairId: string, sinceSeq?: number, limit?: number, options?: RawAxiosRequestConfig) {
return HumanInTheLoopApiFp(this.configuration).getRunPairTranscript(id, pairId, sinceSeq, limit, options).then((request) => request(this.axios, this.basePath));
}
/**
* Downloads a file from the run\'s sandbox environment.
* @summary Download Sandbox File
@ -1039,6 +1530,31 @@ export class HumanInTheLoopApi extends BaseAPI {
return HumanInTheLoopApiFp(this.configuration).retrieveRunSandbox(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Send Run Pair Message
* @param {string} id Unique run identifier (ULID).
* @param {string} pairId
* @param {PairMessageRequest} pairMessageRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public sendRunPairMessage(id: string, pairId: string, pairMessageRequest: PairMessageRequest, options?: RawAxiosRequestConfig) {
return HumanInTheLoopApiFp(this.configuration).sendRunPairMessage(id, pairId, pairMessageRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
* Starts pairing with exactly one selected active API-mode agent target.
* @summary Start Run Pair
* @param {string} id Unique run identifier (ULID).
* @param {PairStartRequest} pairStartRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public startRunPair(id: string, pairStartRequest: PairStartRequest, options?: RawAxiosRequestConfig) {
return HumanInTheLoopApiFp(this.configuration).startRunPair(id, pairStartRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
* Send a mid-run steering message to the live agent session(s) of a running run. Set `interrupt=true` to atomically interrupt the active steerable agent round first, then deliver this message as the next user turn. Without `interrupt=true`, the message is appended to the steering queue and may buffer until the next steerable agent session.
* @summary Steer Run
@ -1064,4 +1580,3 @@ export class HumanInTheLoopApi extends BaseAPI {
return HumanInTheLoopApiFp(this.configuration).submitRunAnswer(id, qid, submitAnswerRequest, options).then((request) => request(this.axios, this.basePath));
}
}

View file

@ -5,7 +5,7 @@
* 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
@ -40,6 +40,8 @@ import type { RunCheckpoint } from '../models';
// @ts-ignore
import type { RunEvent } from '../models';
// @ts-ignore
import type { RunEventDetailResponse } from '../models';
// @ts-ignore
import type { RunProjection } from '../models';
// @ts-ignore
import type { WorkflowSettings } from '../models';
@ -56,7 +58,7 @@ export const RunInternalsApiAxiosParamCreator = function (configuration?: Config
* Appends a validated event to the run event log. Intended for trusted internal callers.
* @summary Append Run Event
* @param {string} id Unique run identifier (ULID).
* @param {RunEvent} runEvent
* @param {RunEvent} runEvent
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -142,6 +144,55 @@ export const RunInternalsApiAxiosParamCreator = function (configuration?: Config
options: localVarRequestOptions,
};
},
/**
* Returns one stored run event by source event sequence with content fields separated and truncated.
* @summary Get Run Event Detail
* @param {string} id Unique run identifier (ULID).
* @param {number} seq
* @param {number} [maxContentLength]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getRunEventDetail: async (id: string, seq: number, maxContentLength?: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('getRunEventDetail', 'id', id)
// verify required parameter 'seq' is not null or undefined
assertParamExists('getRunEventDetail', 'seq', seq)
const localVarPath = `/api/v1/runs/{id}/events/{seq}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)))
.replace(`{${"seq"}}`, encodeURIComponent(String(seq)));
// 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 SessionCookie required
// authentication BearerAuth required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (maxContentLength !== undefined) {
localVarQueryParameter['max_content_length'] = maxContentLength;
}
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 the worker tracing log for a run when it is available.
* @summary Get Run Logs
@ -573,12 +624,12 @@ export const RunInternalsApiAxiosParamCreator = function (configuration?: Config
};
},
/**
* Uploads one or more artifacts for a stage. Intended for trusted internal callers. The server accepts both: - `application/octet-stream` for single-file uploads with the `filename` query parameter - strict manifest-first `multipart/form-data` uploads documented by `ArtifactBatchUploadManifest` The generated Rust client currently exposes the octet-stream variant because the OpenAPI code generator in this repo does not support multiple request media types on one operation.
* Uploads one or more artifacts for a stage. Intended for trusted internal callers. The server accepts both: - `application/octet-stream` for single-file uploads with the `filename` query parameter - strict manifest-first `multipart/form-data` uploads documented by `ArtifactBatchUploadManifest` The generated Rust client currently exposes the octet-stream variant because the OpenAPI code generator in this repo does not support multiple request media types on one operation.
* @summary Put Stage Artifact
* @param {string} id Unique run identifier (ULID).
* @param {string} stageId Identifier of a stage within a run\&#39;s workflow graph, serialized as &#x60;node_id@visit&#x60;.
* @param {number} retry Retry attempt number for the artifact.
* @param {File} body
* @param {File} body
* @param {string} [filename] Relative artifact path for &#x60;application/octet-stream&#x60; uploads. Ignored for multipart uploads.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@ -761,7 +812,7 @@ export const RunInternalsApiAxiosParamCreator = function (configuration?: Config
* Writes an opaque binary blob and returns its content-addressed blob identifier.
* @summary Write Run Blob
* @param {string} id Unique run identifier (ULID).
* @param {File} body
* @param {File} body
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -815,7 +866,7 @@ export const RunInternalsApiFp = function(configuration?: Configuration) {
* Appends a validated event to the run event log. Intended for trusted internal callers.
* @summary Append Run Event
* @param {string} id Unique run identifier (ULID).
* @param {RunEvent} runEvent
* @param {RunEvent} runEvent
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -839,6 +890,21 @@ export const RunInternalsApiFp = function(configuration?: Configuration) {
const localVarOperationServerBasePath = operationServerMap['RunInternalsApi.attachRunEvents']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
* Returns one stored run event by source event sequence with content fields separated and truncated.
* @summary Get Run Event Detail
* @param {string} id Unique run identifier (ULID).
* @param {number} seq
* @param {number} [maxContentLength]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getRunEventDetail(id: string, seq: number, maxContentLength?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RunEventDetailResponse>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getRunEventDetail(id, seq, maxContentLength, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['RunInternalsApi.getRunEventDetail']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
* Returns the worker tracing log for a run when it is available.
* @summary Get Run Logs
@ -971,12 +1037,12 @@ export const RunInternalsApiFp = function(configuration?: Configuration) {
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
* Uploads one or more artifacts for a stage. Intended for trusted internal callers. The server accepts both: - `application/octet-stream` for single-file uploads with the `filename` query parameter - strict manifest-first `multipart/form-data` uploads documented by `ArtifactBatchUploadManifest` The generated Rust client currently exposes the octet-stream variant because the OpenAPI code generator in this repo does not support multiple request media types on one operation.
* Uploads one or more artifacts for a stage. Intended for trusted internal callers. The server accepts both: - `application/octet-stream` for single-file uploads with the `filename` query parameter - strict manifest-first `multipart/form-data` uploads documented by `ArtifactBatchUploadManifest` The generated Rust client currently exposes the octet-stream variant because the OpenAPI code generator in this repo does not support multiple request media types on one operation.
* @summary Put Stage Artifact
* @param {string} id Unique run identifier (ULID).
* @param {string} stageId Identifier of a stage within a run\&#39;s workflow graph, serialized as &#x60;node_id@visit&#x60;.
* @param {number} retry Retry attempt number for the artifact.
* @param {File} body
* @param {File} body
* @param {string} [filename] Relative artifact path for &#x60;application/octet-stream&#x60; uploads. Ignored for multipart uploads.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@ -1031,7 +1097,7 @@ export const RunInternalsApiFp = function(configuration?: Configuration) {
* Writes an opaque binary blob and returns its content-addressed blob identifier.
* @summary Write Run Blob
* @param {string} id Unique run identifier (ULID).
* @param {File} body
* @param {File} body
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -1054,7 +1120,7 @@ export const RunInternalsApiFactory = function (configuration?: Configuration, b
* Appends a validated event to the run event log. Intended for trusted internal callers.
* @summary Append Run Event
* @param {string} id Unique run identifier (ULID).
* @param {RunEvent} runEvent
* @param {RunEvent} runEvent
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -1072,6 +1138,18 @@ export const RunInternalsApiFactory = function (configuration?: Configuration, b
attachRunEvents(id: string, sinceSeq?: number, options?: RawAxiosRequestConfig): AxiosPromise<string> {
return localVarFp.attachRunEvents(id, sinceSeq, options).then((request) => request(axios, basePath));
},
/**
* Returns one stored run event by source event sequence with content fields separated and truncated.
* @summary Get Run Event Detail
* @param {string} id Unique run identifier (ULID).
* @param {number} seq
* @param {number} [maxContentLength]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getRunEventDetail(id: string, seq: number, maxContentLength?: number, options?: RawAxiosRequestConfig): AxiosPromise<RunEventDetailResponse> {
return localVarFp.getRunEventDetail(id, seq, maxContentLength, options).then((request) => request(axios, basePath));
},
/**
* Returns the worker tracing log for a run when it is available.
* @summary Get Run Logs
@ -1177,12 +1255,12 @@ export const RunInternalsApiFactory = function (configuration?: Configuration, b
return localVarFp.listStageEvents(id, stageId, sinceSeq, limit, options).then((request) => request(axios, basePath));
},
/**
* Uploads one or more artifacts for a stage. Intended for trusted internal callers. The server accepts both: - `application/octet-stream` for single-file uploads with the `filename` query parameter - strict manifest-first `multipart/form-data` uploads documented by `ArtifactBatchUploadManifest` The generated Rust client currently exposes the octet-stream variant because the OpenAPI code generator in this repo does not support multiple request media types on one operation.
* Uploads one or more artifacts for a stage. Intended for trusted internal callers. The server accepts both: - `application/octet-stream` for single-file uploads with the `filename` query parameter - strict manifest-first `multipart/form-data` uploads documented by `ArtifactBatchUploadManifest` The generated Rust client currently exposes the octet-stream variant because the OpenAPI code generator in this repo does not support multiple request media types on one operation.
* @summary Put Stage Artifact
* @param {string} id Unique run identifier (ULID).
* @param {string} stageId Identifier of a stage within a run\&#39;s workflow graph, serialized as &#x60;node_id@visit&#x60;.
* @param {number} retry Retry attempt number for the artifact.
* @param {File} body
* @param {File} body
* @param {string} [filename] Relative artifact path for &#x60;application/octet-stream&#x60; uploads. Ignored for multipart uploads.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@ -1225,7 +1303,7 @@ export const RunInternalsApiFactory = function (configuration?: Configuration, b
* Writes an opaque binary blob and returns its content-addressed blob identifier.
* @summary Write Run Blob
* @param {string} id Unique run identifier (ULID).
* @param {File} body
* @param {File} body
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -1243,7 +1321,7 @@ export class RunInternalsApi extends BaseAPI {
* Appends a validated event to the run event log. Intended for trusted internal callers.
* @summary Append Run Event
* @param {string} id Unique run identifier (ULID).
* @param {RunEvent} runEvent
* @param {RunEvent} runEvent
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -1263,6 +1341,19 @@ export class RunInternalsApi extends BaseAPI {
return RunInternalsApiFp(this.configuration).attachRunEvents(id, sinceSeq, options).then((request) => request(this.axios, this.basePath));
}
/**
* Returns one stored run event by source event sequence with content fields separated and truncated.
* @summary Get Run Event Detail
* @param {string} id Unique run identifier (ULID).
* @param {number} seq
* @param {number} [maxContentLength]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public getRunEventDetail(id: string, seq: number, maxContentLength?: number, options?: RawAxiosRequestConfig) {
return RunInternalsApiFp(this.configuration).getRunEventDetail(id, seq, maxContentLength, options).then((request) => request(this.axios, this.basePath));
}
/**
* Returns the worker tracing log for a run when it is available.
* @summary Get Run Logs
@ -1377,12 +1468,12 @@ export class RunInternalsApi extends BaseAPI {
}
/**
* Uploads one or more artifacts for a stage. Intended for trusted internal callers. The server accepts both: - `application/octet-stream` for single-file uploads with the `filename` query parameter - strict manifest-first `multipart/form-data` uploads documented by `ArtifactBatchUploadManifest` The generated Rust client currently exposes the octet-stream variant because the OpenAPI code generator in this repo does not support multiple request media types on one operation.
* Uploads one or more artifacts for a stage. Intended for trusted internal callers. The server accepts both: - `application/octet-stream` for single-file uploads with the `filename` query parameter - strict manifest-first `multipart/form-data` uploads documented by `ArtifactBatchUploadManifest` The generated Rust client currently exposes the octet-stream variant because the OpenAPI code generator in this repo does not support multiple request media types on one operation.
* @summary Put Stage Artifact
* @param {string} id Unique run identifier (ULID).
* @param {string} stageId Identifier of a stage within a run\&#39;s workflow graph, serialized as &#x60;node_id@visit&#x60;.
* @param {number} retry Retry attempt number for the artifact.
* @param {File} body
* @param {File} body
* @param {string} [filename] Relative artifact path for &#x60;application/octet-stream&#x60; uploads. Ignored for multipart uploads.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@ -1429,7 +1520,7 @@ export class RunInternalsApi extends BaseAPI {
* Writes an opaque binary blob and returns its content-addressed blob identifier.
* @summary Write Run Blob
* @param {string} id Unique run identifier (ULID).
* @param {File} body
* @param {File} body
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -1437,4 +1528,3 @@ export class RunInternalsApi extends BaseAPI {
return RunInternalsApiFp(this.configuration).writeRunBlob(id, body, options).then((request) => request(this.axios, this.basePath));
}
}

View file

@ -5,7 +5,7 @@
* 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
@ -22,41 +22,37 @@ import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObj
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base';
// @ts-ignore
import type { CreateSessionRequest } from '../models';
import type { CreateRunSessionRequest } from '../models';
// @ts-ignore
import type { ErrorResponse } from '../models';
// @ts-ignore
import type { PaginatedSessionEventList } from '../models';
import type { EventEnvelope } from '../models';
// @ts-ignore
import type { PaginatedSessionList } from '../models';
// @ts-ignore
import type { PaginatedTurnList } from '../models';
// @ts-ignore
import type { SessionEventEnvelope } from '../models';
// @ts-ignore
import type { SessionRecord } from '../models';
// @ts-ignore
import type { SubmitTurnRequest } from '../models';
// @ts-ignore
import type { TurnRecord } from '../models';
// @ts-ignore
import type { UpdateSessionRequest } from '../models';
/**
* SessionsApi - axios parameter creator
*/
export const SessionsApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @summary Create session
* @param {CreateSessionRequest} createSessionRequest
* Creates a read-only Ask Fabro session bound to the run.
* @summary Create run session
* @param {string} id Unique run identifier (ULID).
* @param {CreateRunSessionRequest} createRunSessionRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createSession: async (createSessionRequest: CreateSessionRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'createSessionRequest' is not null or undefined
assertParamExists('createSession', 'createSessionRequest', createSessionRequest)
const localVarPath = `/api/v1/sessions`;
createRunSession: async (id: string, createRunSessionRequest: CreateRunSessionRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('createRunSession', 'id', id)
// verify required parameter 'createRunSessionRequest' is not null or undefined
assertParamExists('createRunSession', 'createRunSessionRequest', createRunSessionRequest)
const localVarPath = `/api/v1/runs/{id}/sessions`
.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;
@ -80,7 +76,7 @@ export const SessionsApiAxiosParamCreator = function (configuration?: Configurat
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(createSessionRequest, localVarRequestOptions, configuration)
localVarRequestOptions.data = serializeDataIfNeeded(createRunSessionRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
@ -88,48 +84,9 @@ export const SessionsApiAxiosParamCreator = function (configuration?: Configurat
};
},
/**
*
* @summary Delete session
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteSession: async (id: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('deleteSession', 'id', id)
const localVarPath = `/api/v1/sessions/{id}`
.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: 'DELETE', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication SessionCookie required
// authentication BearerAuth required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
*
* @summary Get session
* @param {string} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -167,54 +124,10 @@ export const SessionsApiAxiosParamCreator = function (configuration?: Configurat
};
},
/**
*
* @summary Get session turn
* @param {string} id
* @param {string} turnId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getSessionTurn: async (id: string, turnId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('getSessionTurn', 'id', id)
// verify required parameter 'turnId' is not null or undefined
assertParamExists('getSessionTurn', 'turnId', turnId)
const localVarPath = `/api/v1/sessions/{id}/turns/{turnId}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)))
.replace(`{${"turnId"}}`, encodeURIComponent(String(turnId)));
// 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 SessionCookie required
// 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,
};
},
/**
*
*
* @summary Interrupt a session turn
* @param {string} id
* @param {string} turnId
* @param {string} id
* @param {string} turnId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -255,17 +168,16 @@ export const SessionsApiAxiosParamCreator = function (configuration?: Configurat
};
},
/**
* Returns JSON replay by default. When the `Accept` header includes `text/event-stream`, replays durable events from `since_seq` or `Last-Event-ID`, then stays attached for live events.
* @summary List or stream session events
* @param {string} id
* @param {number} [sinceSeq]
*
* @summary List run sessions
* @param {string} id Unique run identifier (ULID).
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listSessionEvents: async (id: string, sinceSeq?: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
listRunSessions: async (id: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('listSessionEvents', 'id', id)
const localVarPath = `/api/v1/sessions/{id}/events`
assertParamExists('listRunSessions', 'id', id)
const localVarPath = `/api/v1/runs/{id}/sessions`
.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);
@ -284,86 +196,6 @@ export const SessionsApiAxiosParamCreator = function (configuration?: Configurat
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (sinceSeq !== undefined) {
localVarQueryParameter['since_seq'] = sinceSeq;
}
localVarHeaderParameter['Accept'] = 'application/json,text/event-stream';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary List session turns
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listSessionTurns: async (id: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('listSessionTurns', 'id', id)
const localVarPath = `/api/v1/sessions/{id}/turns`
.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 SessionCookie required
// 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,
};
},
/**
*
* @summary List sessions
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listSessions: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/v1/sessions`;
// 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 SessionCookie required
// authentication BearerAuth required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Accept'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
@ -378,8 +210,8 @@ export const SessionsApiAxiosParamCreator = function (configuration?: Configurat
/**
* Starts a streamed turn immediately. Background turns are not supported in this API version.
* @summary Submit a session turn
* @param {string} id
* @param {SubmitTurnRequest} submitTurnRequest
* @param {string} id
* @param {SubmitTurnRequest} submitTurnRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -415,51 +247,6 @@ export const SessionsApiAxiosParamCreator = function (configuration?: Configurat
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(submitTurnRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary Update session
* @param {string} id
* @param {UpdateSessionRequest} updateSessionRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateSession: async (id: string, updateSessionRequest: UpdateSessionRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('updateSession', 'id', id)
// verify required parameter 'updateSessionRequest' is not null or undefined
assertParamExists('updateSession', 'updateSessionRequest', updateSessionRequest)
const localVarPath = `/api/v1/sessions/{id}`
.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: 'PATCH', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication SessionCookie required
// 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(updateSessionRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
@ -475,35 +262,23 @@ export const SessionsApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = SessionsApiAxiosParamCreator(configuration)
return {
/**
*
* @summary Create session
* @param {CreateSessionRequest} createSessionRequest
* Creates a read-only Ask Fabro session bound to the run.
* @summary Create run session
* @param {string} id Unique run identifier (ULID).
* @param {CreateRunSessionRequest} createRunSessionRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createSession(createSessionRequest: CreateSessionRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SessionRecord>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createSession(createSessionRequest, options);
async createRunSession(id: string, createRunSessionRequest: CreateRunSessionRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SessionRecord>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createRunSession(id, createRunSessionRequest, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['SessionsApi.createSession']?.[localVarOperationServerIndex]?.url;
const localVarOperationServerBasePath = operationServerMap['SessionsApi.createRunSession']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @summary Delete session
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async deleteSession(id: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteSession(id, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['SessionsApi.deleteSession']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
*
* @summary Get session
* @param {string} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -514,77 +289,37 @@ export const SessionsApiFp = function(configuration?: Configuration) {
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @summary Get session turn
* @param {string} id
* @param {string} turnId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getSessionTurn(id: string, turnId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TurnRecord>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getSessionTurn(id, turnId, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['SessionsApi.getSessionTurn']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
*
* @summary Interrupt a session turn
* @param {string} id
* @param {string} turnId
* @param {string} id
* @param {string} turnId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async interruptSessionTurn(id: string, turnId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SessionEventEnvelope>> {
async interruptSessionTurn(id: string, turnId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<EventEnvelope>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.interruptSessionTurn(id, turnId, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['SessionsApi.interruptSessionTurn']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
* Returns JSON replay by default. When the `Accept` header includes `text/event-stream`, replays durable events from `since_seq` or `Last-Event-ID`, then stays attached for live events.
* @summary List or stream session events
* @param {string} id
* @param {number} [sinceSeq]
*
* @summary List run sessions
* @param {string} id Unique run identifier (ULID).
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listSessionEvents(id: string, sinceSeq?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedSessionEventList>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listSessionEvents(id, sinceSeq, options);
async listRunSessions(id: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedSessionList>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listRunSessions(id, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['SessionsApi.listSessionEvents']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @summary List session turns
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listSessionTurns(id: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedTurnList>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listSessionTurns(id, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['SessionsApi.listSessionTurns']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @summary List sessions
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async listSessions(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedSessionList>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.listSessions(options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['SessionsApi.listSessions']?.[localVarOperationServerIndex]?.url;
const localVarOperationServerBasePath = operationServerMap['SessionsApi.listRunSessions']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
* Starts a streamed turn immediately. Background turns are not supported in this API version.
* @summary Submit a session turn
* @param {string} id
* @param {SubmitTurnRequest} submitTurnRequest
* @param {string} id
* @param {SubmitTurnRequest} submitTurnRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -594,20 +329,6 @@ export const SessionsApiFp = function(configuration?: Configuration) {
const localVarOperationServerBasePath = operationServerMap['SessionsApi.submitSessionTurn']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @summary Update session
* @param {string} id
* @param {UpdateSessionRequest} updateSessionRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updateSession(id: string, updateSessionRequest: UpdateSessionRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SessionRecord>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateSession(id, updateSessionRequest, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['SessionsApi.updateSession']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
}
};
@ -618,29 +339,20 @@ export const SessionsApiFactory = function (configuration?: Configuration, baseP
const localVarFp = SessionsApiFp(configuration)
return {
/**
*
* @summary Create session
* @param {CreateSessionRequest} createSessionRequest
* Creates a read-only Ask Fabro session bound to the run.
* @summary Create run session
* @param {string} id Unique run identifier (ULID).
* @param {CreateRunSessionRequest} createRunSessionRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createSession(createSessionRequest: CreateSessionRequest, options?: RawAxiosRequestConfig): AxiosPromise<SessionRecord> {
return localVarFp.createSession(createSessionRequest, options).then((request) => request(axios, basePath));
createRunSession(id: string, createRunSessionRequest: CreateRunSessionRequest, options?: RawAxiosRequestConfig): AxiosPromise<SessionRecord> {
return localVarFp.createRunSession(id, createRunSessionRequest, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Delete session
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteSession(id: string, options?: RawAxiosRequestConfig): AxiosPromise<void> {
return localVarFp.deleteSession(id, options).then((request) => request(axios, basePath));
},
/**
*
*
* @summary Get session
* @param {string} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -648,79 +360,37 @@ export const SessionsApiFactory = function (configuration?: Configuration, baseP
return localVarFp.getSession(id, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Get session turn
* @param {string} id
* @param {string} turnId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getSessionTurn(id: string, turnId: string, options?: RawAxiosRequestConfig): AxiosPromise<TurnRecord> {
return localVarFp.getSessionTurn(id, turnId, options).then((request) => request(axios, basePath));
},
/**
*
*
* @summary Interrupt a session turn
* @param {string} id
* @param {string} turnId
* @param {string} id
* @param {string} turnId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
interruptSessionTurn(id: string, turnId: string, options?: RawAxiosRequestConfig): AxiosPromise<SessionEventEnvelope> {
interruptSessionTurn(id: string, turnId: string, options?: RawAxiosRequestConfig): AxiosPromise<EventEnvelope> {
return localVarFp.interruptSessionTurn(id, turnId, options).then((request) => request(axios, basePath));
},
/**
* Returns JSON replay by default. When the `Accept` header includes `text/event-stream`, replays durable events from `since_seq` or `Last-Event-ID`, then stays attached for live events.
* @summary List or stream session events
* @param {string} id
* @param {number} [sinceSeq]
*
* @summary List run sessions
* @param {string} id Unique run identifier (ULID).
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listSessionEvents(id: string, sinceSeq?: number, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedSessionEventList> {
return localVarFp.listSessionEvents(id, sinceSeq, options).then((request) => request(axios, basePath));
},
/**
*
* @summary List session turns
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listSessionTurns(id: string, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedTurnList> {
return localVarFp.listSessionTurns(id, options).then((request) => request(axios, basePath));
},
/**
*
* @summary List sessions
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
listSessions(options?: RawAxiosRequestConfig): AxiosPromise<PaginatedSessionList> {
return localVarFp.listSessions(options).then((request) => request(axios, basePath));
listRunSessions(id: string, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedSessionList> {
return localVarFp.listRunSessions(id, options).then((request) => request(axios, basePath));
},
/**
* Starts a streamed turn immediately. Background turns are not supported in this API version.
* @summary Submit a session turn
* @param {string} id
* @param {SubmitTurnRequest} submitTurnRequest
* @param {string} id
* @param {SubmitTurnRequest} submitTurnRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
submitSessionTurn(id: string, submitTurnRequest: SubmitTurnRequest, options?: RawAxiosRequestConfig): AxiosPromise<string> {
return localVarFp.submitSessionTurn(id, submitTurnRequest, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Update session
* @param {string} id
* @param {UpdateSessionRequest} updateSessionRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateSession(id: string, updateSessionRequest: UpdateSessionRequest, options?: RawAxiosRequestConfig): AxiosPromise<SessionRecord> {
return localVarFp.updateSession(id, updateSessionRequest, options).then((request) => request(axios, basePath));
},
};
};
@ -729,31 +399,21 @@ export const SessionsApiFactory = function (configuration?: Configuration, baseP
*/
export class SessionsApi extends BaseAPI {
/**
*
* @summary Create session
* @param {CreateSessionRequest} createSessionRequest
* Creates a read-only Ask Fabro session bound to the run.
* @summary Create run session
* @param {string} id Unique run identifier (ULID).
* @param {CreateRunSessionRequest} createRunSessionRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public createSession(createSessionRequest: CreateSessionRequest, options?: RawAxiosRequestConfig) {
return SessionsApiFp(this.configuration).createSession(createSessionRequest, options).then((request) => request(this.axios, this.basePath));
public createRunSession(id: string, createRunSessionRequest: CreateRunSessionRequest, options?: RawAxiosRequestConfig) {
return SessionsApiFp(this.configuration).createRunSession(id, createRunSessionRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Delete session
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public deleteSession(id: string, options?: RawAxiosRequestConfig) {
return SessionsApiFp(this.configuration).deleteSession(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
*
* @summary Get session
* @param {string} id
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -762,22 +422,10 @@ export class SessionsApi extends BaseAPI {
}
/**
*
* @summary Get session turn
* @param {string} id
* @param {string} turnId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public getSessionTurn(id: string, turnId: string, options?: RawAxiosRequestConfig) {
return SessionsApiFp(this.configuration).getSessionTurn(id, turnId, options).then((request) => request(this.axios, this.basePath));
}
/**
*
*
* @summary Interrupt a session turn
* @param {string} id
* @param {string} turnId
* @param {string} id
* @param {string} turnId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -786,60 +434,25 @@ export class SessionsApi extends BaseAPI {
}
/**
* Returns JSON replay by default. When the `Accept` header includes `text/event-stream`, replays durable events from `since_seq` or `Last-Event-ID`, then stays attached for live events.
* @summary List or stream session events
* @param {string} id
* @param {number} [sinceSeq]
*
* @summary List run sessions
* @param {string} id Unique run identifier (ULID).
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public listSessionEvents(id: string, sinceSeq?: number, options?: RawAxiosRequestConfig) {
return SessionsApiFp(this.configuration).listSessionEvents(id, sinceSeq, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary List session turns
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public listSessionTurns(id: string, options?: RawAxiosRequestConfig) {
return SessionsApiFp(this.configuration).listSessionTurns(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary List sessions
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public listSessions(options?: RawAxiosRequestConfig) {
return SessionsApiFp(this.configuration).listSessions(options).then((request) => request(this.axios, this.basePath));
public listRunSessions(id: string, options?: RawAxiosRequestConfig) {
return SessionsApiFp(this.configuration).listRunSessions(id, options).then((request) => request(this.axios, this.basePath));
}
/**
* Starts a streamed turn immediately. Background turns are not supported in this API version.
* @summary Submit a session turn
* @param {string} id
* @param {SubmitTurnRequest} submitTurnRequest
* @param {string} id
* @param {SubmitTurnRequest} submitTurnRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public submitSessionTurn(id: string, submitTurnRequest: SubmitTurnRequest, options?: RawAxiosRequestConfig) {
return SessionsApiFp(this.configuration).submitSessionTurn(id, submitTurnRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Update session
* @param {string} id
* @param {UpdateSessionRequest} updateSessionRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public updateSession(id: string, updateSessionRequest: UpdateSessionRequest, options?: RawAxiosRequestConfig) {
return SessionsApiFp(this.configuration).updateSession(id, updateSessionRequest, options).then((request) => request(this.axios, this.basePath));
}
}

View file

@ -5,7 +5,7 @@
* 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
@ -14,17 +14,10 @@
/**
* Agent tool permission level applied to a session.
*/
export const PermissionLevel = {
READ_ONLY: 'read-only',
READ_WRITE: 'read-write',
FULL: 'full'
} as const;
export type PermissionLevel = typeof PermissionLevel[keyof typeof PermissionLevel];
export interface CreateRunSessionRequest {
'title'?: string;
/**
* Catalog model ID or alias, or provider-qualified provider/model reference. Stored as the canonical catalog model ID.
*/
'model'?: string;
}

View file

@ -41,8 +41,8 @@ export * from './completion-usage';
export * from './conclusion';
export * from './create-completion-request';
export * from './create-run-pull-request-request';
export * from './create-run-session-request';
export * from './create-secret-request';
export * from './create-session-request';
export * from './daytona-network-layer';
export * from './daytona-network-layer-one-of';
export * from './daytona-network-layer-one-of-allow-list';
@ -166,13 +166,28 @@ export * from './paginated-run-file-list';
export * from './paginated-run-list';
export * from './paginated-run-stage-list';
export * from './paginated-saved-query-list';
export * from './paginated-session-event-list';
export * from './paginated-session-list';
export * from './paginated-turn-list';
export * from './paginated-workflow-list-response';
export * from './pagination-meta';
export * from './pair-message-record';
export * from './pair-message-request';
export * from './pair-record';
export * from './pair-start-request';
export * from './pair-status';
export * from './pair-target';
export * from './pair-target-selector';
export * from './pair-transcript-assistant-message';
export * from './pair-transcript-assistant-message-model';
export * from './pair-transcript-detail-ref';
export * from './pair-transcript-entry';
export * from './pair-transcript-error';
export * from './pair-transcript-response';
export * from './pair-transcript-response-meta';
export * from './pair-transcript-system-message';
export * from './pair-transcript-tool-call';
export * from './pair-transcript-user-message';
export * from './pair-transcript-warning';
export * from './pending-interview-record';
export * from './permission-level';
export * from './pre-run-push-outcome';
export * from './pre-run-push-outcome-failed';
export * from './pre-run-push-outcome-not-attempted';
@ -246,6 +261,9 @@ export * from './run-control-action';
export * from './run-diff';
export * from './run-error';
export * from './run-event';
export * from './run-event-detail-response';
export * from './run-event-detail-response-content';
export * from './run-event-detail-response-event';
export * from './run-execution-settings';
export * from './run-failure';
export * from './run-files-meta';
@ -265,6 +283,7 @@ export * from './run-model';
export * from './run-model-settings';
export * from './run-namespace';
export * from './run-origin';
export * from './run-pair-status-response';
export * from './run-prepare-settings';
export * from './run-projection';
export * from './run-provenance';
@ -328,7 +347,6 @@ export * from './server-settings';
export * from './server-slate-db-settings';
export * from './server-storage-settings';
export * from './server-web-settings';
export * from './session-event-envelope';
export * from './session-message';
export * from './session-record';
export * from './session-status';
@ -368,11 +386,8 @@ export * from './system-resources-response';
export * from './system-run-counts';
export * from './timeline-entry-response';
export * from './tls-mode';
export * from './turn-record';
export * from './turn-status';
export * from './update-run-parent-request';
export * from './update-run-request';
export * from './update-session-request';
export * from './user-response';
export * from './validate-response';
export * from './vnc-preview-response';

View file

@ -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.
*/
// May contain unused imports in some cases
// @ts-ignore
import type { PairTargetSelector } from './pair-target-selector';
export interface PairMessageRecord {
/**
* Durable pair message identifier.
*/
'message_id': string;
'client_message_id'?: string | null;
/**
* Durable run pair identifier.
*/
'pair_id': string;
'run_id': string;
'target': PairTargetSelector;
'text': string;
'accepted_at': string;
}

View file

@ -0,0 +1,20 @@
/* 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.
*/
export interface PairMessageRequest {
'text': string;
'client_message_id'?: string;
}

View file

@ -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.
*/
// May contain unused imports in some cases
// @ts-ignore
import type { PairStatus } from './pair-status';
// May contain unused imports in some cases
// @ts-ignore
import type { PairTarget } from './pair-target';
export interface PairRecord {
/**
* Durable run pair identifier.
*/
'pair_id': string;
'run_id': string;
'status': PairStatus;
'started_at': string;
'ended_at'?: string | null;
'failure_reason'?: string | null;
'target': PairTarget;
}

View file

@ -5,7 +5,7 @@
* 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
@ -15,15 +15,8 @@
// May contain unused imports in some cases
// @ts-ignore
import type { PermissionLevel } from './permission-level';
import type { PairTargetSelector } from './pair-target-selector';
export interface CreateSessionRequest {
'title'?: string;
'working_dir'?: string;
'provider'?: string;
'model'?: string;
'permissions': PermissionLevel;
export interface PairStartRequest {
'target': PairTargetSelector;
}

View file

@ -5,7 +5,7 @@
* 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
@ -15,14 +15,10 @@
export const TurnStatus = {
RUNNING: 'running',
SUCCEEDED: 'succeeded',
FAILED: 'failed',
INTERRUPTED: 'interrupted'
export const PairStatus = {
ACTIVE: 'active',
ENDED: 'ended',
FAILED: 'failed'
} as const;
export type TurnStatus = typeof TurnStatus[keyof typeof TurnStatus];
export type PairStatus = typeof PairStatus[keyof typeof PairStatus];

View file

@ -0,0 +1,20 @@
/* 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.
*/
export interface PairTargetSelector {
'stage_id': string;
'agent_session_id': string;
}

View file

@ -0,0 +1,25 @@
/* 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.
*/
export interface PairTarget {
'stage_id': string;
'node_id': string;
'node_label': string;
'visit': number;
'agent_session_id': string;
'provider'?: string;
'model'?: string;
}

View file

@ -0,0 +1,21 @@
/* 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.
*/
export interface PairTranscriptAssistantMessageModel {
'provider': string;
'model_id': string;
'speed'?: string | null;
}

View file

@ -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.
*/
// May contain unused imports in some cases
// @ts-ignore
import type { PairTarget } from './pair-target';
// May contain unused imports in some cases
// @ts-ignore
import type { PairTranscriptAssistantMessageModel } from './pair-transcript-assistant-message-model';
export interface PairTranscriptAssistantMessage {
'kind': PairTranscriptAssistantMessageKindEnum;
'seq': number;
'event_id': string;
'ts': string;
/**
* Durable run pair identifier.
*/
'pair_id': string;
'target': PairTarget;
'text': string;
'model': PairTranscriptAssistantMessageModel;
'tool_call_count': number;
}
export const PairTranscriptAssistantMessageKindEnum = {
ASSISTANT_MESSAGE: 'assistant_message'
} as const;
export type PairTranscriptAssistantMessageKindEnum = typeof PairTranscriptAssistantMessageKindEnum[keyof typeof PairTranscriptAssistantMessageKindEnum];

View file

@ -5,7 +5,7 @@
* 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
@ -14,7 +14,7 @@
export interface UpdateSessionRequest {
'title'?: string;
export interface PairTranscriptDetailRef {
'seq': number;
'tool_call_id'?: string;
}

View file

@ -0,0 +1,47 @@
/* 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 { PairTarget } from './pair-target';
// May contain unused imports in some cases
// @ts-ignore
import type { PairTranscriptAssistantMessage } from './pair-transcript-assistant-message';
// May contain unused imports in some cases
// @ts-ignore
import type { PairTranscriptAssistantMessageModel } from './pair-transcript-assistant-message-model';
// May contain unused imports in some cases
// @ts-ignore
import type { PairTranscriptDetailRef } from './pair-transcript-detail-ref';
// May contain unused imports in some cases
// @ts-ignore
import type { PairTranscriptError } from './pair-transcript-error';
// May contain unused imports in some cases
// @ts-ignore
import type { PairTranscriptSystemMessage } from './pair-transcript-system-message';
// May contain unused imports in some cases
// @ts-ignore
import type { PairTranscriptToolCall } from './pair-transcript-tool-call';
// May contain unused imports in some cases
// @ts-ignore
import type { PairTranscriptUserMessage } from './pair-transcript-user-message';
// May contain unused imports in some cases
// @ts-ignore
import type { PairTranscriptWarning } from './pair-transcript-warning';
/**
* @type PairTranscriptEntry
*/
export type PairTranscriptEntry = PairTranscriptAssistantMessage | PairTranscriptError | PairTranscriptSystemMessage | PairTranscriptToolCall | PairTranscriptUserMessage | PairTranscriptWarning;

View file

@ -0,0 +1,41 @@
/* 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 { PairTarget } from './pair-target';
// May contain unused imports in some cases
// @ts-ignore
import type { PairTranscriptDetailRef } from './pair-transcript-detail-ref';
export interface PairTranscriptError {
'kind': PairTranscriptErrorKindEnum;
'seq': number;
'event_id': string;
'ts': string;
/**
* Durable run pair identifier.
*/
'pair_id': string;
'target': PairTarget;
'message': string;
'detail_ref': PairTranscriptDetailRef;
}
export const PairTranscriptErrorKindEnum = {
ERROR: 'error'
} as const;
export type PairTranscriptErrorKindEnum = typeof PairTranscriptErrorKindEnum[keyof typeof PairTranscriptErrorKindEnum];

View file

@ -0,0 +1,20 @@
/* 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.
*/
export interface PairTranscriptResponseMeta {
'next_since_seq': number;
'has_more': boolean;
}

View file

@ -5,7 +5,7 @@
* 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
@ -15,16 +15,12 @@
// May contain unused imports in some cases
// @ts-ignore
import type { PaginationMeta } from './pagination-meta';
import type { PairTranscriptEntry } from './pair-transcript-entry';
// May contain unused imports in some cases
// @ts-ignore
import type { SessionEventEnvelope } from './session-event-envelope';
import type { PairTranscriptResponseMeta } from './pair-transcript-response-meta';
/**
* Paginated list of session events.
*/
export interface PaginatedSessionEventList {
'data': Array<SessionEventEnvelope>;
'meta': PaginationMeta;
export interface PairTranscriptResponse {
'data': Array<PairTranscriptEntry>;
'meta': PairTranscriptResponseMeta;
}

View file

@ -0,0 +1,44 @@
/* 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 { PairTarget } from './pair-target';
export interface PairTranscriptSystemMessage {
'kind': PairTranscriptSystemMessageKindEnum;
'seq': number;
'event_id': string;
'ts': string;
/**
* Durable run pair identifier.
*/
'pair_id': string;
'target': PairTarget;
'system_message_kind': PairTranscriptSystemMessageSystemMessageKindEnum;
'text': string;
}
export const PairTranscriptSystemMessageKindEnum = {
SYSTEM_MESSAGE: 'system_message'
} as const;
export type PairTranscriptSystemMessageKindEnum = typeof PairTranscriptSystemMessageKindEnum[keyof typeof PairTranscriptSystemMessageKindEnum];
export const PairTranscriptSystemMessageSystemMessageKindEnum = {
HUMAN_JOINED: 'human_joined',
HUMAN_LEFT: 'human_left'
} as const;
export type PairTranscriptSystemMessageSystemMessageKindEnum = typeof PairTranscriptSystemMessageSystemMessageKindEnum[keyof typeof PairTranscriptSystemMessageSystemMessageKindEnum];

View file

@ -0,0 +1,52 @@
/* 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 { PairTarget } from './pair-target';
// May contain unused imports in some cases
// @ts-ignore
import type { PairTranscriptDetailRef } from './pair-transcript-detail-ref';
export interface PairTranscriptToolCall {
'kind': PairTranscriptToolCallKindEnum;
'seq': number;
'event_id': string;
'ts': string;
/**
* Durable run pair identifier.
*/
'pair_id': string;
'target': PairTarget;
'tool_call_id': string;
'tool_name': string;
'status': PairTranscriptToolCallStatusEnum;
'summary': string;
'is_error': boolean;
'truncated': boolean;
'detail_ref': PairTranscriptDetailRef;
}
export const PairTranscriptToolCallKindEnum = {
TOOL_CALL: 'tool_call'
} as const;
export type PairTranscriptToolCallKindEnum = typeof PairTranscriptToolCallKindEnum[keyof typeof PairTranscriptToolCallKindEnum];
export const PairTranscriptToolCallStatusEnum = {
STARTED: 'started',
COMPLETED: 'completed'
} as const;
export type PairTranscriptToolCallStatusEnum = typeof PairTranscriptToolCallStatusEnum[keyof typeof PairTranscriptToolCallStatusEnum];

View file

@ -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.
*/
// May contain unused imports in some cases
// @ts-ignore
import type { PairTarget } from './pair-target';
export interface PairTranscriptUserMessage {
'kind': PairTranscriptUserMessageKindEnum;
'seq': number;
'event_id': string;
'ts': string;
/**
* Durable run pair identifier.
*/
'pair_id': string;
'target': PairTarget;
/**
* Durable pair message identifier.
*/
'message_id': string;
'client_message_id'?: string | null;
'text': string;
}
export const PairTranscriptUserMessageKindEnum = {
USER_MESSAGE: 'user_message'
} as const;
export type PairTranscriptUserMessageKindEnum = typeof PairTranscriptUserMessageKindEnum[keyof typeof PairTranscriptUserMessageKindEnum];

View file

@ -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.
*/
// May contain unused imports in some cases
// @ts-ignore
import type { PairTarget } from './pair-target';
// May contain unused imports in some cases
// @ts-ignore
import type { PairTranscriptDetailRef } from './pair-transcript-detail-ref';
export interface PairTranscriptWarning {
'kind': PairTranscriptWarningKindEnum;
'seq': number;
'event_id': string;
'ts': string;
/**
* Durable run pair identifier.
*/
'pair_id': string;
'target': PairTarget;
'warning_kind': string;
'message': string;
'detail_ref': PairTranscriptDetailRef;
}
export const PairTranscriptWarningKindEnum = {
WARNING: 'warning'
} as const;
export type PairTranscriptWarningKindEnum = typeof PairTranscriptWarningKindEnum[keyof typeof PairTranscriptWarningKindEnum];

View file

@ -0,0 +1,30 @@
/* 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.
*/
export interface RunEventDetailResponseContent {
'kind': RunEventDetailResponseContentKindEnum;
'value': string;
}
export const RunEventDetailResponseContentKindEnum = {
TEXT: 'text',
TOOL_OUTPUT: 'tool_output',
TOOL_ARGUMENTS: 'tool_arguments',
ERROR: 'error',
DETAILS: 'details'
} as const;
export type RunEventDetailResponseContentKindEnum = typeof RunEventDetailResponseContentKindEnum[keyof typeof RunEventDetailResponseContentKindEnum];

View file

@ -5,7 +5,7 @@
* 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
@ -13,22 +13,20 @@
*/
// May contain unused imports in some cases
// @ts-ignore
import type { Principal } from './principal';
/**
* Append-only durable session event envelope.
*/
export interface SessionEventEnvelope {
export interface RunEventDetailResponseEvent {
'seq': number;
/**
* Durable session identifier.
*/
'session_id': string;
/**
* Durable session turn identifier.
*/
'turn_id'?: string | null;
'event': string;
'properties': any;
'id': string;
'ts': string;
'run_id': string;
'event': string;
'actor'?: Principal;
'session_id'?: string;
'node_id'?: string;
'node_label'?: string;
'stage_id'?: string;
'tool_call_id'?: string;
}

View file

@ -0,0 +1,30 @@
/* 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 { RunEventDetailResponseContent } from './run-event-detail-response-content';
// May contain unused imports in some cases
// @ts-ignore
import type { RunEventDetailResponseEvent } from './run-event-detail-response-event';
export interface RunEventDetailResponse {
'event': RunEventDetailResponseEvent;
'properties': { [key: string]: any; };
'content'?: RunEventDetailResponseContent;
'truncated': boolean;
'redacted': boolean;
'max_content_length': number;
}

View file

@ -5,7 +5,7 @@
* 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
@ -15,16 +15,13 @@
// May contain unused imports in some cases
// @ts-ignore
import type { PaginationMeta } from './pagination-meta';
import type { PairRecord } from './pair-record';
// May contain unused imports in some cases
// @ts-ignore
import type { TurnRecord } from './turn-record';
import type { PairTarget } from './pair-target';
/**
* Paginated list of turns.
*/
export interface PaginatedTurnList {
'data': Array<TurnRecord>;
'meta': PaginationMeta;
export interface RunPairStatusResponse {
'run_id': string;
'current_pair'?: PairRecord | null;
'targets': Array<PairTarget>;
}

View file

@ -5,7 +5,7 @@
* 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
@ -21,5 +21,3 @@ import type { RunCheckpoint } from './run-checkpoint';
* @type RunProjectionCheckpointsInnerInner
*/
export type RunProjectionCheckpointsInnerInner = RunCheckpoint | number;

View file

@ -5,7 +5,7 @@
* 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
@ -13,35 +13,22 @@
*/
// May contain unused imports in some cases
// @ts-ignore
import type { PermissionLevel } from './permission-level';
// May contain unused imports in some cases
// @ts-ignore
import type { SessionMessage } from './session-message';
// May contain unused imports in some cases
// @ts-ignore
import type { SessionStatus } from './session-status';
/**
* Durable session metadata and compactable runtime context.
* Ask Fabro session metadata derived from the owning run event stream.
*/
export interface SessionRecord {
/**
* Durable session identifier.
*/
'id': string;
'run_id': string;
'title'?: string | null;
'status': SessionStatus;
'working_dir'?: string | null;
'provider'?: string | null;
'model'?: string | null;
'permissions': PermissionLevel;
'created_at': string;
'updated_at': string;
'deleted_at'?: string | null;
'runtime_context': Array<SessionMessage>;
}

View file

@ -5,7 +5,7 @@
* 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
@ -18,12 +18,7 @@
export const SessionStatus = {
IDLE: 'idle',
RUNNING: 'running',
FAILED: 'failed',
CLOSED: 'closed',
DELETED: 'deleted'
FAILED: 'failed'
} as const;
export type SessionStatus = typeof SessionStatus[keyof typeof SessionStatus];

View file

@ -5,7 +5,7 @@
* 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
@ -18,21 +18,17 @@
import type { SessionStatus } from './session-status';
/**
* List projection of a durable session.
* List projection of an Ask Fabro session.
*/
export interface SessionSummary {
/**
* Durable session identifier.
*/
'id': string;
'run_id': string;
'title'?: string | null;
'status': SessionStatus;
'working_dir'?: string | null;
'provider'?: string | null;
'model'?: string | null;
'created_at': string;
'updated_at': string;
}

View file

@ -1,42 +0,0 @@
/* 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 { TurnStatus } from './turn-status';
/**
* One externally submitted input plus terminal output metadata. Full resumable context is stored on SessionRecord.runtime_context.
*/
export interface TurnRecord {
/**
* Durable session turn identifier.
*/
'id': string;
/**
* Durable session identifier.
*/
'session_id': string;
'input': string;
'status': TurnStatus;
'output'?: string | null;
'error'?: string | null;
'created_at': string;
'updated_at': string;
'completed_at'?: string | null;
}