mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-10 03:30:59 +00:00
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`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
Generated with GPT-5 via [Codex](https://openai.com/codex)
This commit is contained in:
parent
02fabb7b47
commit
86b1fbef7f
69 changed files with 3297 additions and 3842 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
23
lib/crates/fabro-agent/src/tool_permissions.rs
Normal file
23
lib/crates/fabro-agent/src/tool_permissions.rs
Normal 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))
|
||||
}
|
||||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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>()
|
||||
);
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
96
lib/crates/fabro-cli/src/commands/run/ask.rs
Normal file
96
lib/crates/fabro-cli/src/commands/run/ask.rs
Normal 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(())
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
@ -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?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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), "");
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
¤t_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
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ mod routing;
|
|||
mod run_files;
|
||||
mod runs;
|
||||
mod sandbox_vnc;
|
||||
mod sessions;
|
||||
mod settings;
|
||||
mod system;
|
||||
mod tcp;
|
||||
|
|
|
|||
395
lib/crates/fabro-server/tests/it/api/sessions.rs
Normal file
395
lib/crates/fabro-server/tests/it/api/sessions.rs
Normal 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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
414
lib/crates/fabro-store/src/run_sessions.rs
Normal file
414
lib/crates/fabro-store/src/run_sessions.rs
Normal 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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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)?)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>> {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
79
lib/crates/fabro-types/src/run_event/session.rs
Normal file
79
lib/crates/fabro-types/src/run_event/session.rs
Normal 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>,
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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\'s workflow graph, serialized as `node_id@visit`.
|
||||
* @param {number} retry Retry attempt number for the artifact.
|
||||
* @param {File} body
|
||||
* @param {File} body
|
||||
* @param {string} [filename] Relative artifact path for `application/octet-stream` 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\'s workflow graph, serialized as `node_id@visit`.
|
||||
* @param {number} retry Retry attempt number for the artifact.
|
||||
* @param {File} body
|
||||
* @param {File} body
|
||||
* @param {string} [filename] Relative artifact path for `application/octet-stream` 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\'s workflow graph, serialized as `node_id@visit`.
|
||||
* @param {number} retry Retry attempt number for the artifact.
|
||||
* @param {File} body
|
||||
* @param {File} body
|
||||
* @param {string} [filename] Relative artifact path for `application/octet-stream` 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\'s workflow graph, serialized as `node_id@visit`.
|
||||
* @param {number} retry Retry attempt number for the artifact.
|
||||
* @param {File} body
|
||||
* @param {File} body
|
||||
* @param {string} [filename] Relative artifact path for `application/octet-stream` 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
559
lib/packages/fabro-api-client/src/api/sessions-api.ts
generated
559
lib/packages/fabro-api-client/src/api/sessions-api.ts
generated
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
31
lib/packages/fabro-api-client/src/models/index.ts
generated
31
lib/packages/fabro-api-client/src/models/index.ts
generated
|
|
@ -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';
|
||||
|
|
|
|||
34
lib/packages/fabro-api-client/src/models/pair-message-record.ts
generated
Normal file
34
lib/packages/fabro-api-client/src/models/pair-message-record.ts
generated
Normal 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;
|
||||
}
|
||||
20
lib/packages/fabro-api-client/src/models/pair-message-request.ts
generated
Normal file
20
lib/packages/fabro-api-client/src/models/pair-message-request.ts
generated
Normal 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;
|
||||
}
|
||||
34
lib/packages/fabro-api-client/src/models/pair-record.ts
generated
Normal file
34
lib/packages/fabro-api-client/src/models/pair-record.ts
generated
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -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];
|
||||
20
lib/packages/fabro-api-client/src/models/pair-target-selector.ts
generated
Normal file
20
lib/packages/fabro-api-client/src/models/pair-target-selector.ts
generated
Normal 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;
|
||||
}
|
||||
25
lib/packages/fabro-api-client/src/models/pair-target.ts
generated
Normal file
25
lib/packages/fabro-api-client/src/models/pair-target.ts
generated
Normal 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;
|
||||
}
|
||||
21
lib/packages/fabro-api-client/src/models/pair-transcript-assistant-message-model.ts
generated
Normal file
21
lib/packages/fabro-api-client/src/models/pair-transcript-assistant-message-model.ts
generated
Normal 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;
|
||||
}
|
||||
42
lib/packages/fabro-api-client/src/models/pair-transcript-assistant-message.ts
generated
Normal file
42
lib/packages/fabro-api-client/src/models/pair-transcript-assistant-message.ts
generated
Normal 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];
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
47
lib/packages/fabro-api-client/src/models/pair-transcript-entry.ts
generated
Normal file
47
lib/packages/fabro-api-client/src/models/pair-transcript-entry.ts
generated
Normal 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;
|
||||
41
lib/packages/fabro-api-client/src/models/pair-transcript-error.ts
generated
Normal file
41
lib/packages/fabro-api-client/src/models/pair-transcript-error.ts
generated
Normal 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];
|
||||
20
lib/packages/fabro-api-client/src/models/pair-transcript-response-meta.ts
generated
Normal file
20
lib/packages/fabro-api-client/src/models/pair-transcript-response-meta.ts
generated
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
44
lib/packages/fabro-api-client/src/models/pair-transcript-system-message.ts
generated
Normal file
44
lib/packages/fabro-api-client/src/models/pair-transcript-system-message.ts
generated
Normal 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];
|
||||
52
lib/packages/fabro-api-client/src/models/pair-transcript-tool-call.ts
generated
Normal file
52
lib/packages/fabro-api-client/src/models/pair-transcript-tool-call.ts
generated
Normal 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];
|
||||
42
lib/packages/fabro-api-client/src/models/pair-transcript-user-message.ts
generated
Normal file
42
lib/packages/fabro-api-client/src/models/pair-transcript-user-message.ts
generated
Normal 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];
|
||||
42
lib/packages/fabro-api-client/src/models/pair-transcript-warning.ts
generated
Normal file
42
lib/packages/fabro-api-client/src/models/pair-transcript-warning.ts
generated
Normal 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];
|
||||
30
lib/packages/fabro-api-client/src/models/run-event-detail-response-content.ts
generated
Normal file
30
lib/packages/fabro-api-client/src/models/run-event-detail-response-content.ts
generated
Normal 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];
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
30
lib/packages/fabro-api-client/src/models/run-event-detail-response.ts
generated
Normal file
30
lib/packages/fabro-api-client/src/models/run-event-detail-response.ts
generated
Normal 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;
|
||||
}
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Loading…
Add table
Reference in a new issue