mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
Move agent state (todos, subagents, skills, MCP) onto StageProjection (#364)
## Summary
The `GET /runs/{id}` response now carries per-stage agent state — todos,
subagents, skills, and MCP server status — directly on each
`StageProjection`, unblocking the agent-stage sidebar without any new
endpoints.
## What changed and why
**`RunProjection.todos_by_list` removed.** The run-level map was the
only home for todos, but every list was already 1:1 with a stage (keyed
by `openai_plan:<session>` or `anthropic_tasks:<root_session>`). Moving
todos onto `StageProjection.todos: Option<TodoListProjection>`
eliminates the awkward cross-stage indirection with no loss of
expressiveness.
**Four new fields on `StageProjection`** (`todos`, `subagents`,
`skills`, `mcp_servers`) plus five new projection-side types
(`SubAgentProjection`, `SubAgentStatus`, `SkillsProjection`,
`ActivatedSkill`, `McpServerProjection`, `McpServerStatus`). All
colocated in `run_projection.rs`; no new modules. Four existing
event-payload types are reused directly (`TodoListProjection`,
`AgentSkillSummary`, `AgentSkillActivationSource`,
`AgentMcpToolSummary`) via `with_replacement(...)` in
`fabro-api/build.rs` so progenitor doesn't generate parallel `ApiFoo`
aliases.
**Reducer rerouting** (`run_state.rs`): `TodoCreated/Updated/Deleted`
now resolve the owning stage via the existing
`stage_at_stored_or_current_visit` helper and mutate `stage.todos`
directly. Eight new event arms handle
`AgentSubSpawned/Completed/Failed/Closed`,
`AgentSkillsDiscovered/Activated`, and `AgentMcpReady/Failed` using the
same `stage_at_stored_or_visit` pattern already used for other agent
events.
**Design decisions called out in the plan:**
- `SkillActivated` appends every activation (event-sourced replay
semantics); the UI can collapse if needed.
- `upsert_mcp_server` replaces by `server_name`, so a server that
recovers after a failure shows its final state.
- `SubAgentStatus` and `McpServerStatus` are projection-side enums,
intentionally distinct from the runtime per-process status types in
`fabro-agent`.
**OpenAPI + TS client** updated in lockstep: new schemas added,
`todos_by_list` removed from `RunProjection`, `bun run generate`
regenerated 20+ new model files.
### Plan Summary
- Extend `StageProjection` with `todos`, `subagents`, `skills`,
`mcp_servers` + supporting types in `run_projection.rs`
- Remove `RunProjection.todos_by_list` (no cross-stage use case)
- Reroute todo reducer handlers; add 8 new event arms for
subagent/skill/MCP events
- Wire reused types through `fabro-api/build.rs` `with_replacement`;
regenerate TS client
- Update all existing todo reducer tests to read from `stage.todos`; add
new test modules for subagent, skill, and MCP event families
- Update stale comment in `run-events.ts`
### Fabro Details
<details>
<summary>Ran 9 stages in 38m 52s for $13.29</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 16s | – | 0 |
| implement | 16m 8s | $7.80 | 0 |
| simplify_opus | 10m 34s | $3.91 | 0 |
| simplify_gpt | 3m 23s | $1.58 | 0 |
| verify | 3m 51s | – | 0 |
| fmt | 2s | – | 0 |
| **Total** | **38m 52s** | **$13.29** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
This commit is contained in:
parent
7e6052bec3
commit
f81c5b96b1
29 changed files with 1436 additions and 112 deletions
|
|
@ -96,8 +96,8 @@ const STEERING_EVENTS = new Set([
|
|||
"agent.steer.buffered",
|
||||
"agent.steer.dropped",
|
||||
]);
|
||||
// Todo / task mutation events refresh `getRunState` consumers (so the
|
||||
// projected `todos_by_list` map updates live) and the run events list.
|
||||
// Todo / task mutation events refresh `getRunState` consumers (so per-stage
|
||||
// todo projections update live) and the run events list.
|
||||
const TODO_EVENTS = new Set([
|
||||
"todo.created",
|
||||
"todo.updated",
|
||||
|
|
@ -252,4 +252,4 @@ export function useRunEvents(runId: string | undefined) {
|
|||
if (!runId) return;
|
||||
return subscribeToRunEvents(runId, mutate as MutateFn);
|
||||
}, [mutate, runId]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7758,10 +7758,218 @@ components:
|
|||
oneOf:
|
||||
- $ref: "#/components/schemas/BillingModelRef"
|
||||
- type: "null"
|
||||
todos:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/TodoListProjection"
|
||||
- type: "null"
|
||||
description: Projected todo / task list for this stage.
|
||||
subagents:
|
||||
type: array
|
||||
description: Subagents spawned by this stage, in replay/insertion order.
|
||||
items:
|
||||
$ref: "#/components/schemas/SubAgentProjection"
|
||||
skills:
|
||||
$ref: "#/components/schemas/SkillsProjection"
|
||||
description: Agent skills discovered and activated during this stage.
|
||||
mcp_servers:
|
||||
type: array
|
||||
description: MCP servers observed by this stage.
|
||||
items:
|
||||
$ref: "#/components/schemas/McpServerProjection"
|
||||
state:
|
||||
$ref: "#/components/schemas/StageState"
|
||||
description: Lifecycle state of the stage projection.
|
||||
|
||||
SubAgentProjection:
|
||||
description: Current projected state for one subagent spawned by an agent stage.
|
||||
type: object
|
||||
required:
|
||||
- agent_id
|
||||
- depth
|
||||
- task
|
||||
- status
|
||||
properties:
|
||||
agent_id:
|
||||
type: string
|
||||
depth:
|
||||
type: integer
|
||||
minimum: 0
|
||||
task:
|
||||
type: string
|
||||
status:
|
||||
$ref: "#/components/schemas/SubAgentStatus"
|
||||
|
||||
SubAgentStatus:
|
||||
description: Projected lifecycle status for a subagent.
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/SubAgentStatusRunning"
|
||||
- $ref: "#/components/schemas/SubAgentStatusCompleted"
|
||||
- $ref: "#/components/schemas/SubAgentStatusFailed"
|
||||
- $ref: "#/components/schemas/SubAgentStatusClosed"
|
||||
discriminator:
|
||||
propertyName: kind
|
||||
mapping:
|
||||
running: "#/components/schemas/SubAgentStatusRunning"
|
||||
completed: "#/components/schemas/SubAgentStatusCompleted"
|
||||
failed: "#/components/schemas/SubAgentStatusFailed"
|
||||
closed: "#/components/schemas/SubAgentStatusClosed"
|
||||
|
||||
SubAgentStatusRunning:
|
||||
type: object
|
||||
required:
|
||||
- kind
|
||||
properties:
|
||||
kind:
|
||||
type: string
|
||||
enum: [running]
|
||||
|
||||
SubAgentStatusCompleted:
|
||||
type: object
|
||||
required:
|
||||
- kind
|
||||
- success
|
||||
- turns_used
|
||||
properties:
|
||||
kind:
|
||||
type: string
|
||||
enum: [completed]
|
||||
success:
|
||||
type: boolean
|
||||
turns_used:
|
||||
type: integer
|
||||
minimum: 0
|
||||
|
||||
SubAgentStatusFailed:
|
||||
type: object
|
||||
required:
|
||||
- kind
|
||||
- error
|
||||
properties:
|
||||
kind:
|
||||
type: string
|
||||
enum: [failed]
|
||||
error:
|
||||
description: Provider/tool error payload captured by the subagent event.
|
||||
|
||||
SubAgentStatusClosed:
|
||||
type: object
|
||||
required:
|
||||
- kind
|
||||
properties:
|
||||
kind:
|
||||
type: string
|
||||
enum: [closed]
|
||||
|
||||
SkillsProjection:
|
||||
description: Agent skills discovered and activated during a stage.
|
||||
type: object
|
||||
required:
|
||||
- available
|
||||
- activated
|
||||
properties:
|
||||
available:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/AgentSkillSummary"
|
||||
activated:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/ActivatedSkill"
|
||||
|
||||
AgentSkillSummary:
|
||||
description: Summary of an available agent skill.
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- description
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
|
||||
ActivatedSkill:
|
||||
description: One observed agent skill activation.
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- source
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
source:
|
||||
$ref: "#/components/schemas/AgentSkillActivationSource"
|
||||
|
||||
AgentSkillActivationSource:
|
||||
description: Source that activated an agent skill.
|
||||
type: string
|
||||
enum: [slash, tool]
|
||||
|
||||
McpServerProjection:
|
||||
description: Projected state for one MCP server observed by an agent stage.
|
||||
type: object
|
||||
required:
|
||||
- server_name
|
||||
- tool_count
|
||||
- status
|
||||
properties:
|
||||
server_name:
|
||||
type: string
|
||||
tool_count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
status:
|
||||
$ref: "#/components/schemas/McpServerStatus"
|
||||
|
||||
McpServerStatus:
|
||||
description: Projected MCP server readiness status.
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/McpServerStatusReady"
|
||||
- $ref: "#/components/schemas/McpServerStatusFailed"
|
||||
discriminator:
|
||||
propertyName: kind
|
||||
mapping:
|
||||
ready: "#/components/schemas/McpServerStatusReady"
|
||||
failed: "#/components/schemas/McpServerStatusFailed"
|
||||
|
||||
McpServerStatusReady:
|
||||
type: object
|
||||
required:
|
||||
- kind
|
||||
- tools
|
||||
properties:
|
||||
kind:
|
||||
type: string
|
||||
enum: [ready]
|
||||
tools:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/AgentMcpToolSummary"
|
||||
|
||||
McpServerStatusFailed:
|
||||
type: object
|
||||
required:
|
||||
- kind
|
||||
- error
|
||||
properties:
|
||||
kind:
|
||||
type: string
|
||||
enum: [failed]
|
||||
error:
|
||||
type: string
|
||||
|
||||
AgentMcpToolSummary:
|
||||
description: Summary of one tool exposed by an MCP server.
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- original_name
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
original_name:
|
||||
type: string
|
||||
|
||||
StageModelUsage:
|
||||
description: Provider, model, and request-control metadata recorded for a stage attempt.
|
||||
type: object
|
||||
|
|
@ -8069,15 +8277,6 @@ components:
|
|||
type: object
|
||||
additionalProperties:
|
||||
$ref: "#/components/schemas/PendingInterviewRecord"
|
||||
todos_by_list:
|
||||
type: object
|
||||
description: |-
|
||||
Projected todo / task lists, keyed by `list_id`
|
||||
(`openai_plan:<session_id>` or
|
||||
`anthropic_tasks:<root_session_id>`). Built by replaying
|
||||
`todo.created`, `todo.updated`, and `todo.deleted` events.
|
||||
additionalProperties:
|
||||
$ref: "#/components/schemas/TodoListProjection"
|
||||
stages:
|
||||
type: object
|
||||
description: Map from StageId (`node_id@visit`) to stage projection data.
|
||||
|
|
|
|||
|
|
@ -344,6 +344,28 @@ fn main() {
|
|||
("CommandTermination", "fabro_types::CommandTermination", &[]),
|
||||
("StageModelUsage", "fabro_types::StageModelUsage", &[]),
|
||||
("StageProjection", "fabro_types::StageProjection", &[]),
|
||||
("TodoListProjection", "fabro_types::TodoListProjection", &[]),
|
||||
("SubAgentProjection", "fabro_types::SubAgentProjection", &[]),
|
||||
("SubAgentStatus", "fabro_types::SubAgentStatus", &[]),
|
||||
("SkillsProjection", "fabro_types::SkillsProjection", &[]),
|
||||
("ActivatedSkill", "fabro_types::ActivatedSkill", &[]),
|
||||
("AgentSkillSummary", "fabro_types::AgentSkillSummary", &[]),
|
||||
(
|
||||
"AgentSkillActivationSource",
|
||||
"fabro_types::AgentSkillActivationSource",
|
||||
&[],
|
||||
),
|
||||
(
|
||||
"McpServerProjection",
|
||||
"fabro_types::McpServerProjection",
|
||||
&[],
|
||||
),
|
||||
("McpServerStatus", "fabro_types::McpServerStatus", &[]),
|
||||
(
|
||||
"AgentMcpToolSummary",
|
||||
"fabro_types::AgentMcpToolSummary",
|
||||
&[],
|
||||
),
|
||||
("SecretMetadata", "fabro_types::SecretMetadata", &[]),
|
||||
("InterviewOption", "fabro_types::InterviewOption", &[]),
|
||||
(
|
||||
|
|
|
|||
|
|
@ -32,22 +32,24 @@ pub mod types {
|
|||
BlockedReason, FailureReason, RunControlAction, RunStatus, SuccessReason,
|
||||
};
|
||||
pub use fabro_types::{
|
||||
ActivatedSkill, AgentMcpToolSummary, AgentSkillActivationSource, AgentSkillSummary,
|
||||
AskFabro, AuthMethod, BilledTokenCounts, CommandTermination, Conclusion, DiffStats,
|
||||
DiffSummary, DirtyStatus, EventEnvelope, ExecOutputTail, FailureCategory, FailureDetail,
|
||||
FailureSignature, GitContext, IdpIdentity, InterviewOption, InterviewQuestionRecord,
|
||||
PairId, PairMessageId, PairMessageRecord, PairMessageRequest, PairRecord, PairStartRequest,
|
||||
PairStatus, PairTarget, PairTranscriptEntry, PairTranscriptResponse,
|
||||
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,
|
||||
McpServerProjection, McpServerStatus, PairId, PairMessageId, PairMessageRecord,
|
||||
PairMessageRequest, PairRecord, PairStartRequest, PairStatus, PairTarget,
|
||||
PairTranscriptEntry, PairTranscriptResponse, 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, SessionDetail, SessionId, SessionMessage,
|
||||
SessionRecord, SessionStatus, SessionSummary, SessionTurn, StageCompletion, StageHandler,
|
||||
StageModelUsage, StageOutcome, StageProjection, StageState, SystemActorKind, TurnId,
|
||||
SessionRecord, SessionStatus, SessionSummary, SessionTurn, SkillsProjection,
|
||||
StageCompletion, StageHandler, StageModelUsage, StageOutcome, StageProjection, StageState,
|
||||
SubAgentProjection, SubAgentStatus, SystemActorKind, TodoListProjection, TurnId,
|
||||
UserPrincipal, WorkflowSettings,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
use std::any::{TypeId, type_name};
|
||||
|
||||
use fabro_api::types::StageProjection as ApiStageProjection;
|
||||
use fabro_types::StageProjection;
|
||||
use fabro_api::types::{
|
||||
ActivatedSkill as ApiActivatedSkill, AgentMcpToolSummary as ApiAgentMcpToolSummary,
|
||||
AgentSkillActivationSource as ApiAgentSkillActivationSource,
|
||||
AgentSkillSummary as ApiAgentSkillSummary, McpServerProjection as ApiMcpServerProjection,
|
||||
McpServerStatus as ApiMcpServerStatus, SkillsProjection as ApiSkillsProjection,
|
||||
StageProjection as ApiStageProjection, SubAgentProjection as ApiSubAgentProjection,
|
||||
SubAgentStatus as ApiSubAgentStatus, TodoListProjection as ApiTodoListProjection,
|
||||
};
|
||||
use fabro_types::{
|
||||
ActivatedSkill, AgentMcpToolSummary, AgentSkillActivationSource, AgentSkillSummary,
|
||||
McpServerProjection, McpServerStatus, SkillsProjection, StageProjection, SubAgentProjection,
|
||||
SubAgentStatus, TodoListKind, TodoListProjection,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
|
|
@ -9,6 +20,20 @@ fn stage_projection_reuses_canonical_type() {
|
|||
assert_same_type::<ApiStageProjection, StageProjection>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stage_projection_reuses_nested_agent_state_types() {
|
||||
assert_same_type::<ApiTodoListProjection, TodoListProjection>();
|
||||
assert_same_type::<ApiSubAgentProjection, SubAgentProjection>();
|
||||
assert_same_type::<ApiSubAgentStatus, SubAgentStatus>();
|
||||
assert_same_type::<ApiSkillsProjection, SkillsProjection>();
|
||||
assert_same_type::<ApiActivatedSkill, ActivatedSkill>();
|
||||
assert_same_type::<ApiAgentSkillSummary, AgentSkillSummary>();
|
||||
assert_same_type::<ApiAgentSkillActivationSource, AgentSkillActivationSource>();
|
||||
assert_same_type::<ApiMcpServerProjection, McpServerProjection>();
|
||||
assert_same_type::<ApiMcpServerStatus, McpServerStatus>();
|
||||
assert_same_type::<ApiAgentMcpToolSummary, AgentMcpToolSummary>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stage_projection_round_trips_representative_json() {
|
||||
let value = json!({
|
||||
|
|
@ -49,6 +74,60 @@ fn stage_projection_round_trips_representative_json() {
|
|||
"cache_read_tokens": 0,
|
||||
"cache_write_tokens": 0
|
||||
},
|
||||
"todos": {
|
||||
"kind": "openai_plan",
|
||||
"list_id": "openai_plan:ses_root",
|
||||
"items": [
|
||||
{
|
||||
"id": "todo-1",
|
||||
"status": "in_progress",
|
||||
"order": 0,
|
||||
"subject": "Write tests",
|
||||
"active_form": "Writing tests"
|
||||
}
|
||||
]
|
||||
},
|
||||
"subagents": [
|
||||
{
|
||||
"agent_id": "sub-1",
|
||||
"depth": 1,
|
||||
"task": "Investigate failing test",
|
||||
"status": {
|
||||
"kind": "completed",
|
||||
"success": true,
|
||||
"turns_used": 3
|
||||
}
|
||||
}
|
||||
],
|
||||
"skills": {
|
||||
"available": [
|
||||
{
|
||||
"name": "rust",
|
||||
"description": "Rust workflow help"
|
||||
}
|
||||
],
|
||||
"activated": [
|
||||
{
|
||||
"name": "rust",
|
||||
"source": "slash"
|
||||
}
|
||||
]
|
||||
},
|
||||
"mcp_servers": [
|
||||
{
|
||||
"server_name": "filesystem",
|
||||
"tool_count": 1,
|
||||
"status": {
|
||||
"kind": "ready",
|
||||
"tools": [
|
||||
{
|
||||
"name": "read_file",
|
||||
"original_name": "read_file"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"state": "succeeded"
|
||||
});
|
||||
|
||||
|
|
@ -56,6 +135,138 @@ fn stage_projection_round_trips_representative_json() {
|
|||
assert_eq!(serde_json::to_value(state).unwrap(), value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_agent_state_types_match_openapi_json_shape() {
|
||||
let todo_list = TodoListProjection::new(TodoListKind::OpenAiPlan, "openai_plan:ses_root");
|
||||
let todo_json = serde_json::to_value(&todo_list).unwrap();
|
||||
assert_eq!(
|
||||
todo_json,
|
||||
json!({
|
||||
"kind": "openai_plan",
|
||||
"list_id": "openai_plan:ses_root",
|
||||
"items": []
|
||||
})
|
||||
);
|
||||
let api_todo_list: ApiTodoListProjection = serde_json::from_value(todo_json).unwrap();
|
||||
assert_eq!(api_todo_list, todo_list);
|
||||
|
||||
let subagent = SubAgentProjection {
|
||||
agent_id: "sub-1".to_string(),
|
||||
depth: 1,
|
||||
task: "Investigate failing test".to_string(),
|
||||
status: SubAgentStatus::Completed {
|
||||
success: true,
|
||||
turns_used: 3,
|
||||
},
|
||||
};
|
||||
let subagent_json = serde_json::to_value(&subagent).unwrap();
|
||||
assert_eq!(
|
||||
subagent_json,
|
||||
json!({
|
||||
"agent_id": "sub-1",
|
||||
"depth": 1,
|
||||
"task": "Investigate failing test",
|
||||
"status": {
|
||||
"kind": "completed",
|
||||
"success": true,
|
||||
"turns_used": 3
|
||||
}
|
||||
})
|
||||
);
|
||||
let api_subagent: ApiSubAgentProjection = serde_json::from_value(subagent_json).unwrap();
|
||||
assert_eq!(api_subagent, subagent);
|
||||
|
||||
let skill = AgentSkillSummary {
|
||||
name: "rust".to_string(),
|
||||
description: "Rust workflow help".to_string(),
|
||||
};
|
||||
let skill_json = serde_json::to_value(&skill).unwrap();
|
||||
assert_eq!(
|
||||
skill_json,
|
||||
json!({
|
||||
"name": "rust",
|
||||
"description": "Rust workflow help"
|
||||
})
|
||||
);
|
||||
let api_skill: ApiAgentSkillSummary = serde_json::from_value(skill_json).unwrap();
|
||||
assert_eq!(api_skill, skill);
|
||||
|
||||
let source_json = serde_json::to_value(AgentSkillActivationSource::Slash).unwrap();
|
||||
assert_eq!(source_json, json!("slash"));
|
||||
let api_source: ApiAgentSkillActivationSource = serde_json::from_value(source_json).unwrap();
|
||||
assert_eq!(api_source, AgentSkillActivationSource::Slash);
|
||||
|
||||
let activated = ActivatedSkill {
|
||||
name: "rust".to_string(),
|
||||
source: AgentSkillActivationSource::Slash,
|
||||
};
|
||||
let skills = SkillsProjection {
|
||||
available: vec![skill],
|
||||
activated: vec![activated],
|
||||
};
|
||||
let skills_json = serde_json::to_value(&skills).unwrap();
|
||||
assert_eq!(
|
||||
skills_json,
|
||||
json!({
|
||||
"available": [
|
||||
{
|
||||
"name": "rust",
|
||||
"description": "Rust workflow help"
|
||||
}
|
||||
],
|
||||
"activated": [
|
||||
{
|
||||
"name": "rust",
|
||||
"source": "slash"
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
let api_skills: ApiSkillsProjection = serde_json::from_value(skills_json).unwrap();
|
||||
assert_eq!(api_skills, skills);
|
||||
|
||||
let tool = AgentMcpToolSummary {
|
||||
name: "read_file".to_string(),
|
||||
original_name: "read_file".to_string(),
|
||||
};
|
||||
let tool_json = serde_json::to_value(&tool).unwrap();
|
||||
assert_eq!(
|
||||
tool_json,
|
||||
json!({
|
||||
"name": "read_file",
|
||||
"original_name": "read_file"
|
||||
})
|
||||
);
|
||||
let api_tool: ApiAgentMcpToolSummary = serde_json::from_value(tool_json).unwrap();
|
||||
assert_eq!(api_tool, tool);
|
||||
|
||||
let mcp_server = McpServerProjection {
|
||||
server_name: "filesystem".to_string(),
|
||||
tool_count: 1,
|
||||
status: McpServerStatus::Ready { tools: vec![tool] },
|
||||
};
|
||||
let mcp_json = serde_json::to_value(&mcp_server).unwrap();
|
||||
assert_eq!(
|
||||
mcp_json,
|
||||
json!({
|
||||
"server_name": "filesystem",
|
||||
"tool_count": 1,
|
||||
"status": {
|
||||
"kind": "ready",
|
||||
"tools": [
|
||||
{
|
||||
"name": "read_file",
|
||||
"original_name": "read_file"
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
);
|
||||
let api_mcp: ApiMcpServerProjection = serde_json::from_value(mcp_json).unwrap();
|
||||
assert_eq!(api_mcp, mcp_server);
|
||||
assert_eq!(mcp_server.tool_count, 1);
|
||||
}
|
||||
|
||||
fn assert_same_type<T: 'static, U: 'static>() {
|
||||
assert_eq!(
|
||||
TypeId::of::<T>(),
|
||||
|
|
|
|||
|
|
@ -8,12 +8,13 @@ use fabro_types::run_event::{
|
|||
};
|
||||
use fabro_types::settings::run::{EnvironmentProvider, RunEnvironmentSettings};
|
||||
use fabro_types::{
|
||||
AskFabro, BilledModelUsage, Checkpoint, CheckpointRecord, CommandTermination, Conclusion,
|
||||
EventBody, FailureSignature, InterviewQuestionRecord, Outcome, PendingInterviewRecord,
|
||||
PullRequestLink, RepositoryRef, Run, RunBillingSummary, RunControlAction, RunDiff, RunEvent,
|
||||
RunId, RunLifecycle, RunLinks, RunModel, RunOrigin, RunProjection, RunSandbox,
|
||||
RunSandboxRuntime, RunSpec, RunStatus, RunTimestamps, SandboxProvider, StageCompletion,
|
||||
StageHandler, StageId, StageModelUsage, StageOutcome, StageProjection, StageState, StartRecord,
|
||||
ActivatedSkill, AskFabro, BilledModelUsage, Checkpoint, CheckpointRecord, CommandTermination,
|
||||
Conclusion, EventBody, FailureSignature, InterviewQuestionRecord, McpServerProjection,
|
||||
McpServerStatus, Outcome, PendingInterviewRecord, PullRequestLink, RepositoryRef, Run,
|
||||
RunBillingSummary, RunControlAction, RunDiff, RunEvent, RunId, RunLifecycle, RunLinks,
|
||||
RunModel, RunOrigin, RunProjection, RunSandbox, RunSandboxRuntime, RunSpec, RunStatus,
|
||||
RunTimestamps, SandboxProvider, StageCompletion, StageHandler, StageId, StageModelUsage,
|
||||
StageOutcome, StageProjection, StageState, StartRecord, SubAgentProjection, SubAgentStatus,
|
||||
TodoListProjection, TodoProjection, WorkflowRef, first_event_seq,
|
||||
};
|
||||
use fabro_util::error::render_compact_with_causes;
|
||||
|
|
@ -449,13 +450,109 @@ impl RunProjectionReducer for RunProjection {
|
|||
stage.parallel_results = Some(parallel_results);
|
||||
}
|
||||
EventBody::TodoCreated(props) => {
|
||||
apply_todo_created(self, props);
|
||||
let Some(stage) = stage_at_stored_or_current_visit(self, stored, event.seq) else {
|
||||
return Ok(());
|
||||
};
|
||||
apply_todo_created(stage, props);
|
||||
}
|
||||
EventBody::TodoUpdated(props) => {
|
||||
apply_todo_updated(self, props);
|
||||
let Some(stage) = stage_at_stored_or_current_visit(self, stored, event.seq) else {
|
||||
return Ok(());
|
||||
};
|
||||
apply_todo_updated(stage, props);
|
||||
}
|
||||
EventBody::TodoDeleted(props) => {
|
||||
apply_todo_deleted(self, props);
|
||||
let Some(stage) = stage_at_stored_or_current_visit(self, stored, event.seq) else {
|
||||
return Ok(());
|
||||
};
|
||||
apply_todo_deleted(stage, props);
|
||||
}
|
||||
EventBody::AgentSubSpawned(props) => {
|
||||
let Some(stage) = stage_at_stored_or_visit(self, stored, props.visit, event.seq)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
stage.subagents.push(SubAgentProjection {
|
||||
agent_id: props.agent_id.clone(),
|
||||
depth: props.depth,
|
||||
task: props.task.clone(),
|
||||
status: SubAgentStatus::Running,
|
||||
});
|
||||
}
|
||||
EventBody::AgentSubCompleted(props) => {
|
||||
let Some(stage) = stage_at_stored_or_visit(self, stored, props.visit, event.seq)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
if let Some(subagent) = subagent_mut(stage, &props.agent_id) {
|
||||
subagent.status = SubAgentStatus::Completed {
|
||||
success: props.success,
|
||||
turns_used: props.turns_used,
|
||||
};
|
||||
}
|
||||
}
|
||||
EventBody::AgentSubFailed(props) => {
|
||||
let Some(stage) = stage_at_stored_or_visit(self, stored, props.visit, event.seq)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
if let Some(subagent) = subagent_mut(stage, &props.agent_id) {
|
||||
subagent.status = SubAgentStatus::Failed {
|
||||
error: props.error.clone(),
|
||||
};
|
||||
}
|
||||
}
|
||||
EventBody::AgentSubClosed(props) => {
|
||||
let Some(stage) = stage_at_stored_or_visit(self, stored, props.visit, event.seq)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
if let Some(subagent) = subagent_mut(stage, &props.agent_id) {
|
||||
subagent.status = SubAgentStatus::Closed;
|
||||
}
|
||||
}
|
||||
EventBody::AgentSkillsDiscovered(props) => {
|
||||
let Some(stage) = stage_at_stored_or_visit(self, stored, props.visit, event.seq)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
stage.skills.available.clone_from(&props.skills);
|
||||
}
|
||||
EventBody::AgentSkillActivated(props) => {
|
||||
let Some(stage) = stage_at_stored_or_visit(self, stored, props.visit, event.seq)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
stage.skills.activated.push(ActivatedSkill {
|
||||
name: props.skill_name.clone(),
|
||||
source: props.source,
|
||||
});
|
||||
}
|
||||
EventBody::AgentMcpReady(props) => {
|
||||
let Some(stage) = stage_at_stored_or_visit(self, stored, props.visit, event.seq)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
upsert_mcp_server(stage, McpServerProjection {
|
||||
server_name: props.server_name.clone(),
|
||||
tool_count: props.tool_count,
|
||||
status: McpServerStatus::Ready {
|
||||
tools: props.tools.clone(),
|
||||
},
|
||||
});
|
||||
}
|
||||
EventBody::AgentMcpFailed(props) => {
|
||||
let Some(stage) = stage_at_stored_or_visit(self, stored, props.visit, event.seq)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
upsert_mcp_server(stage, McpServerProjection {
|
||||
server_name: props.server_name.clone(),
|
||||
tool_count: 0,
|
||||
status: McpServerStatus::Failed {
|
||||
error: props.error.clone(),
|
||||
},
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -464,11 +561,18 @@ impl RunProjectionReducer for RunProjection {
|
|||
}
|
||||
}
|
||||
|
||||
fn apply_todo_created(state: &mut RunProjection, props: &TodoCreatedProps) {
|
||||
let list = state
|
||||
.todos_by_list
|
||||
.entry(props.list_id.clone())
|
||||
.or_insert_with(|| TodoListProjection::new(props.list_kind, props.list_id.clone()));
|
||||
fn apply_todo_created(stage: &mut StageProjection, props: &TodoCreatedProps) {
|
||||
if stage
|
||||
.todos
|
||||
.as_ref()
|
||||
.is_none_or(|list| list.list_id != props.list_id || list.kind != props.list_kind)
|
||||
{
|
||||
stage.todos = Some(TodoListProjection::new(
|
||||
props.list_kind,
|
||||
props.list_id.clone(),
|
||||
));
|
||||
}
|
||||
let list = stage.todos.as_mut().expect("todo list was just inserted");
|
||||
list.upsert(TodoProjection {
|
||||
id: props.todo_id.clone(),
|
||||
status: props.status,
|
||||
|
|
@ -483,18 +587,49 @@ fn apply_todo_created(state: &mut RunProjection, props: &TodoCreatedProps) {
|
|||
});
|
||||
}
|
||||
|
||||
fn apply_todo_updated(state: &mut RunProjection, props: &TodoUpdatedProps) {
|
||||
if let Some(list) = state.todos_by_list.get_mut(&props.list_id) {
|
||||
fn apply_todo_updated(stage: &mut StageProjection, props: &TodoUpdatedProps) {
|
||||
if let Some(list) = stage
|
||||
.todos
|
||||
.as_mut()
|
||||
.filter(|list| list.list_id == props.list_id)
|
||||
{
|
||||
list.apply_patch(&props.todo_id, &fabro_types::TodoPatch::from_props(props));
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_todo_deleted(state: &mut RunProjection, props: &TodoDeletedProps) {
|
||||
if let Some(list) = state.todos_by_list.get_mut(&props.list_id) {
|
||||
list.remove(&props.todo_id);
|
||||
if list.items.is_empty() {
|
||||
state.todos_by_list.remove(&props.list_id);
|
||||
}
|
||||
fn apply_todo_deleted(stage: &mut StageProjection, props: &TodoDeletedProps) {
|
||||
let Some(list) = stage
|
||||
.todos
|
||||
.as_mut()
|
||||
.filter(|list| list.list_id == props.list_id)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
list.remove(&props.todo_id);
|
||||
if list.items.is_empty() {
|
||||
stage.todos = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn subagent_mut<'a>(
|
||||
stage: &'a mut StageProjection,
|
||||
agent_id: &str,
|
||||
) -> Option<&'a mut SubAgentProjection> {
|
||||
stage
|
||||
.subagents
|
||||
.iter_mut()
|
||||
.find(|subagent| subagent.agent_id == agent_id)
|
||||
}
|
||||
|
||||
fn upsert_mcp_server(stage: &mut StageProjection, server: McpServerProjection) {
|
||||
if let Some(existing) = stage
|
||||
.mcp_servers
|
||||
.iter_mut()
|
||||
.find(|existing| existing.server_name == server.server_name)
|
||||
{
|
||||
*existing = server;
|
||||
} else {
|
||||
stage.mcp_servers.push(server);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -892,8 +1027,11 @@ mod tests {
|
|||
use fabro_types::run_event::run::RunFailedProps;
|
||||
use fabro_types::run_event::{
|
||||
AgentAcpCancelledProps, AgentAcpCompletedProps, AgentAcpStartedProps,
|
||||
AgentAcpTimedOutProps, AgentMessageProps, AgentSessionActivatedProps,
|
||||
AgentSessionEndedProps, AgentSessionStartedProps, CheckpointCompletedProps,
|
||||
AgentAcpTimedOutProps, AgentMcpFailedProps, AgentMcpReadyProps, AgentMcpToolSummary,
|
||||
AgentMessageProps, AgentSessionActivatedProps, AgentSessionEndedProps,
|
||||
AgentSessionStartedProps, AgentSkillActivatedProps, AgentSkillActivationSource,
|
||||
AgentSkillSummary, AgentSkillsDiscoveredProps, AgentSubClosedProps, AgentSubCompletedProps,
|
||||
AgentSubFailedProps, AgentSubSpawnedProps, CheckpointCompletedProps,
|
||||
InterviewCompletedProps, InterviewOption, InterviewStartedProps, RunControlEffectProps,
|
||||
StageCompletedProps, StageFailedProps, StagePromptProps, StageRetryingProps,
|
||||
StageStartedProps,
|
||||
|
|
@ -901,9 +1039,10 @@ mod tests {
|
|||
use fabro_types::{
|
||||
AgentBackend, BilledModelUsage, BilledTokenCounts, BlockedReason, Checkpoint,
|
||||
CheckpointRecord, CommandTermination, EventBody, FailureCategory, FailureDetail,
|
||||
FailureReason, Graph, Outcome, PullRequestLink, QuestionType, ReasoningEffort, RunBlobId,
|
||||
RunControlAction, RunDiff, RunEvent, RunSpec, RunStatus, Speed, StageModelUsage,
|
||||
StageOutcome, StageState, SuccessReason, WorkflowSettings, first_event_seq, fixtures,
|
||||
FailureReason, Graph, McpServerStatus, Outcome, PullRequestLink, QuestionType,
|
||||
ReasoningEffort, RunBlobId, RunControlAction, RunDiff, RunEvent, RunSpec, RunStatus, Speed,
|
||||
StageModelUsage, StageOutcome, StageState, SubAgentStatus, SuccessReason, WorkflowSettings,
|
||||
first_event_seq, fixtures,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
|
|
@ -3104,10 +3243,21 @@ mod tests {
|
|||
|
||||
mod todo_reducer {
|
||||
use fabro_types::run_event::{TodoCreatedProps, TodoDeletedProps, TodoUpdatedProps};
|
||||
use fabro_types::{TodoListKind, TodoStatus};
|
||||
use fabro_types::{TodoListKind, TodoListProjection, TodoStatus};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn stage_id() -> StageId {
|
||||
StageId::new("code", 1)
|
||||
}
|
||||
|
||||
fn stage_todos<'a>(state: &'a RunProjection, stage_id: &StageId) -> &'a TodoListProjection {
|
||||
state
|
||||
.stage(stage_id)
|
||||
.and_then(|stage| stage.todos.as_ref())
|
||||
.expect("stage todos present")
|
||||
}
|
||||
|
||||
fn created(
|
||||
list: &str,
|
||||
list_kind: TodoListKind,
|
||||
|
|
@ -3164,30 +3314,32 @@ mod tests {
|
|||
#[test]
|
||||
fn replay_reconstructs_current_list() {
|
||||
let mut state = initialized_projection();
|
||||
let stage_id = stage_id();
|
||||
let list = "openai_plan:ses_a";
|
||||
state
|
||||
.apply_event(&test_event(
|
||||
.apply_event(&test_stage_event(
|
||||
1,
|
||||
created(list, TodoListKind::OpenAiPlan, "a", 0, "first"),
|
||||
None,
|
||||
stage_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
state
|
||||
.apply_event(&test_event(
|
||||
.apply_event(&test_stage_event(
|
||||
2,
|
||||
created(list, TodoListKind::OpenAiPlan, "b", 1, "second"),
|
||||
None,
|
||||
stage_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
state
|
||||
.apply_event(&test_event(
|
||||
.apply_event(&test_stage_event(
|
||||
3,
|
||||
updated_status(list, TodoListKind::OpenAiPlan, "a", TodoStatus::InProgress),
|
||||
None,
|
||||
stage_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let projection = state.todos_by_list.get(list).expect("list present");
|
||||
let projection = stage_todos(&state, &stage_id);
|
||||
assert_eq!(projection.list_id, list);
|
||||
assert_eq!(projection.items.len(), 2);
|
||||
assert_eq!(projection.items[0].id, "a");
|
||||
assert_eq!(projection.items[0].status, TodoStatus::InProgress);
|
||||
|
|
@ -3197,53 +3349,57 @@ mod tests {
|
|||
#[test]
|
||||
fn deleted_todos_are_absent() {
|
||||
let mut state = initialized_projection();
|
||||
let stage_id = stage_id();
|
||||
let list = "openai_plan:ses_a";
|
||||
state
|
||||
.apply_event(&test_event(
|
||||
.apply_event(&test_stage_event(
|
||||
1,
|
||||
created(list, TodoListKind::OpenAiPlan, "a", 0, "first"),
|
||||
None,
|
||||
stage_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
state
|
||||
.apply_event(&test_event(
|
||||
.apply_event(&test_stage_event(
|
||||
2,
|
||||
created(list, TodoListKind::OpenAiPlan, "b", 1, "second"),
|
||||
None,
|
||||
stage_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
state
|
||||
.apply_event(&test_event(
|
||||
.apply_event(&test_stage_event(
|
||||
3,
|
||||
deleted(list, TodoListKind::OpenAiPlan, "a"),
|
||||
None,
|
||||
stage_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let projection = state.todos_by_list.get(list).expect("list present");
|
||||
let projection = stage_todos(&state, &stage_id);
|
||||
assert_eq!(projection.items.len(), 1);
|
||||
assert_eq!(projection.items[0].id, "b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_lists_stay_isolated() {
|
||||
fn stage_todo_lists_stay_isolated() {
|
||||
let mut state = initialized_projection();
|
||||
let plan_one = StageId::new("plan_one", 1);
|
||||
let plan_two = StageId::new("plan_two", 1);
|
||||
let claude = StageId::new("claude", 1);
|
||||
state
|
||||
.apply_event(&test_event(
|
||||
.apply_event(&test_stage_event(
|
||||
1,
|
||||
created("openai_plan:s1", TodoListKind::OpenAiPlan, "a", 0, "p1"),
|
||||
None,
|
||||
plan_one.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
state
|
||||
.apply_event(&test_event(
|
||||
.apply_event(&test_stage_event(
|
||||
2,
|
||||
created("openai_plan:s2", TodoListKind::OpenAiPlan, "a", 0, "p2"),
|
||||
None,
|
||||
plan_two.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
state
|
||||
.apply_event(&test_event(
|
||||
.apply_event(&test_stage_event(
|
||||
3,
|
||||
created(
|
||||
"anthropic_tasks:s_root",
|
||||
|
|
@ -3252,35 +3408,32 @@ mod tests {
|
|||
0,
|
||||
"claude task",
|
||||
),
|
||||
None,
|
||||
claude.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.todos_by_list.len(), 3);
|
||||
assert_eq!(state.todos_by_list["openai_plan:s1"].items[0].subject, "p1");
|
||||
assert_eq!(state.todos_by_list["openai_plan:s2"].items[0].subject, "p2");
|
||||
assert_eq!(
|
||||
state.todos_by_list["anthropic_tasks:s_root"].items[0].subject,
|
||||
"claude task"
|
||||
);
|
||||
assert_eq!(stage_todos(&state, &plan_one).items[0].subject, "p1");
|
||||
assert_eq!(stage_todos(&state, &plan_two).items[0].subject, "p2");
|
||||
assert_eq!(stage_todos(&state, &claude).items[0].subject, "claude task");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_patch_merges_and_null_deletes() {
|
||||
let mut state = initialized_projection();
|
||||
let stage_id = stage_id();
|
||||
let list = "anthropic_tasks:r";
|
||||
state
|
||||
.apply_event(&test_event(
|
||||
.apply_event(&test_stage_event(
|
||||
1,
|
||||
created(list, TodoListKind::AnthropicTasks, "1", 0, "t"),
|
||||
None,
|
||||
stage_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
let mut meta = BTreeMap::new();
|
||||
meta.insert("k1".to_string(), serde_json::json!("v1"));
|
||||
meta.insert("k2".to_string(), serde_json::json!("v2"));
|
||||
state
|
||||
.apply_event(&test_event(
|
||||
.apply_event(&test_stage_event(
|
||||
2,
|
||||
EventBody::TodoUpdated(TodoUpdatedProps {
|
||||
list_id: list.to_string(),
|
||||
|
|
@ -3296,13 +3449,13 @@ mod tests {
|
|||
add_blocked_by: None,
|
||||
metadata_patch: meta,
|
||||
}),
|
||||
None,
|
||||
stage_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
let mut delete = BTreeMap::new();
|
||||
delete.insert("k1".to_string(), serde_json::Value::Null);
|
||||
state
|
||||
.apply_event(&test_event(
|
||||
.apply_event(&test_stage_event(
|
||||
3,
|
||||
EventBody::TodoUpdated(TodoUpdatedProps {
|
||||
list_id: list.to_string(),
|
||||
|
|
@ -3318,13 +3471,242 @@ mod tests {
|
|||
add_blocked_by: None,
|
||||
metadata_patch: delete,
|
||||
}),
|
||||
None,
|
||||
stage_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let todo = &state.todos_by_list[list].items[0];
|
||||
let todo = &stage_todos(&state, &stage_id).items[0];
|
||||
assert!(!todo.metadata.contains_key("k1"));
|
||||
assert_eq!(todo.metadata.get("k2"), Some(&serde_json::json!("v2")));
|
||||
}
|
||||
}
|
||||
|
||||
mod agent_state_reducer {
|
||||
use super::*;
|
||||
|
||||
fn stage_id() -> StageId {
|
||||
StageId::new("code", 1)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subagent_events_update_stage_projection() {
|
||||
let mut state = initialized_projection();
|
||||
let stage_id = stage_id();
|
||||
|
||||
state
|
||||
.apply_event(&test_stage_event(
|
||||
1,
|
||||
EventBody::AgentSubSpawned(AgentSubSpawnedProps {
|
||||
agent_id: "sub-1".to_string(),
|
||||
depth: 1,
|
||||
task: "write tests".to_string(),
|
||||
visit: 1,
|
||||
}),
|
||||
stage_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
let stage = state.stage(&stage_id).unwrap();
|
||||
assert_eq!(stage.subagents.len(), 1);
|
||||
assert_eq!(stage.subagents[0].agent_id, "sub-1");
|
||||
assert_eq!(stage.subagents[0].depth, 1);
|
||||
assert_eq!(stage.subagents[0].task, "write tests");
|
||||
assert_eq!(stage.subagents[0].status, SubAgentStatus::Running);
|
||||
|
||||
state
|
||||
.apply_event(&test_stage_event(
|
||||
2,
|
||||
EventBody::AgentSubCompleted(AgentSubCompletedProps {
|
||||
agent_id: "sub-1".to_string(),
|
||||
depth: 1,
|
||||
success: true,
|
||||
turns_used: 3,
|
||||
visit: 1,
|
||||
}),
|
||||
stage_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
let stage = state.stage(&stage_id).unwrap();
|
||||
assert_eq!(stage.subagents[0].status, SubAgentStatus::Completed {
|
||||
success: true,
|
||||
turns_used: 3,
|
||||
});
|
||||
|
||||
state
|
||||
.apply_event(&test_stage_event(
|
||||
3,
|
||||
EventBody::AgentSubSpawned(AgentSubSpawnedProps {
|
||||
agent_id: "sub-2".to_string(),
|
||||
depth: 2,
|
||||
task: "debug failure".to_string(),
|
||||
visit: 1,
|
||||
}),
|
||||
stage_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
state
|
||||
.apply_event(&test_stage_event(
|
||||
4,
|
||||
EventBody::AgentSubFailed(AgentSubFailedProps {
|
||||
agent_id: "sub-2".to_string(),
|
||||
depth: 2,
|
||||
error: json!({ "message": "boom" }),
|
||||
visit: 1,
|
||||
}),
|
||||
stage_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
let stage = state.stage(&stage_id).unwrap();
|
||||
assert_eq!(stage.subagents[1].status, SubAgentStatus::Failed {
|
||||
error: json!({ "message": "boom" }),
|
||||
});
|
||||
|
||||
state
|
||||
.apply_event(&test_stage_event(
|
||||
5,
|
||||
EventBody::AgentSubClosed(AgentSubClosedProps {
|
||||
agent_id: "sub-2".to_string(),
|
||||
depth: 2,
|
||||
visit: 1,
|
||||
}),
|
||||
stage_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
let stage = state.stage(&stage_id).unwrap();
|
||||
assert_eq!(stage.subagents[1].status, SubAgentStatus::Closed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skill_events_update_stage_projection() {
|
||||
let mut state = initialized_projection();
|
||||
let stage_id = stage_id();
|
||||
|
||||
state
|
||||
.apply_event(&test_stage_event(
|
||||
1,
|
||||
EventBody::AgentSkillsDiscovered(AgentSkillsDiscoveredProps {
|
||||
provider_profile: "claude".to_string(),
|
||||
source_dirs: vec![".claude/skills".to_string()],
|
||||
skills: vec![
|
||||
AgentSkillSummary {
|
||||
name: "rust".to_string(),
|
||||
description: "Rust help".to_string(),
|
||||
},
|
||||
AgentSkillSummary {
|
||||
name: "docs".to_string(),
|
||||
description: "Docs help".to_string(),
|
||||
},
|
||||
],
|
||||
visit: 1,
|
||||
}),
|
||||
stage_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
state
|
||||
.apply_event(&test_stage_event(
|
||||
2,
|
||||
EventBody::AgentSkillActivated(AgentSkillActivatedProps {
|
||||
skill_name: "rust".to_string(),
|
||||
source: AgentSkillActivationSource::Slash,
|
||||
visit: 1,
|
||||
}),
|
||||
stage_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
state
|
||||
.apply_event(&test_stage_event(
|
||||
3,
|
||||
EventBody::AgentSkillActivated(AgentSkillActivatedProps {
|
||||
skill_name: "rust".to_string(),
|
||||
source: AgentSkillActivationSource::Tool,
|
||||
visit: 1,
|
||||
}),
|
||||
stage_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let stage = state.stage(&stage_id).unwrap();
|
||||
assert_eq!(stage.skills.available.len(), 2);
|
||||
assert_eq!(stage.skills.available[0].name, "rust");
|
||||
assert_eq!(stage.skills.activated.len(), 2);
|
||||
assert_eq!(stage.skills.activated[0].name, "rust");
|
||||
assert_eq!(
|
||||
stage.skills.activated[0].source,
|
||||
AgentSkillActivationSource::Slash
|
||||
);
|
||||
assert_eq!(
|
||||
stage.skills.activated[1].source,
|
||||
AgentSkillActivationSource::Tool
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_server_events_update_stage_projection() {
|
||||
let mut state = initialized_projection();
|
||||
let stage_id = stage_id();
|
||||
|
||||
state
|
||||
.apply_event(&test_stage_event(
|
||||
1,
|
||||
EventBody::AgentMcpReady(AgentMcpReadyProps {
|
||||
server_name: "filesystem".to_string(),
|
||||
tool_count: 2,
|
||||
tools: vec![
|
||||
AgentMcpToolSummary {
|
||||
name: "read_file".to_string(),
|
||||
original_name: "read_file".to_string(),
|
||||
},
|
||||
AgentMcpToolSummary {
|
||||
name: "write_file".to_string(),
|
||||
original_name: "write_file".to_string(),
|
||||
},
|
||||
],
|
||||
visit: 1,
|
||||
}),
|
||||
stage_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
state
|
||||
.apply_event(&test_stage_event(
|
||||
2,
|
||||
EventBody::AgentMcpFailed(AgentMcpFailedProps {
|
||||
server_name: "github".to_string(),
|
||||
error: "missing token".to_string(),
|
||||
visit: 1,
|
||||
}),
|
||||
stage_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
state
|
||||
.apply_event(&test_stage_event(
|
||||
3,
|
||||
EventBody::AgentMcpReady(AgentMcpReadyProps {
|
||||
server_name: "filesystem".to_string(),
|
||||
tool_count: 1,
|
||||
tools: vec![AgentMcpToolSummary {
|
||||
name: "read_file".to_string(),
|
||||
original_name: "read_file".to_string(),
|
||||
}],
|
||||
visit: 1,
|
||||
}),
|
||||
stage_id.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let stage = state.stage(&stage_id).unwrap();
|
||||
assert_eq!(stage.mcp_servers.len(), 2);
|
||||
assert_eq!(stage.mcp_servers[0].server_name, "filesystem");
|
||||
assert_eq!(stage.mcp_servers[0].tool_count, 1);
|
||||
assert_eq!(stage.mcp_servers[0].status, McpServerStatus::Ready {
|
||||
tools: vec![AgentMcpToolSummary {
|
||||
name: "read_file".to_string(),
|
||||
original_name: "read_file".to_string(),
|
||||
}],
|
||||
});
|
||||
assert_eq!(stage.mcp_servers[1].server_name, "github");
|
||||
assert_eq!(stage.mcp_servers[1].tool_count, 0);
|
||||
assert_eq!(stage.mcp_servers[1].status, McpServerStatus::Failed {
|
||||
error: "missing token".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,8 +103,9 @@ pub use run_event::{
|
|||
pub use run_failure::RunFailure;
|
||||
pub use run_id::{RunId, fixtures};
|
||||
pub use run_projection::{
|
||||
CheckpointRecord, PendingInterviewRecord, RunProjection, StageModelUsage, StageProjection,
|
||||
first_event_seq,
|
||||
ActivatedSkill, CheckpointRecord, McpServerProjection, McpServerStatus, PendingInterviewRecord,
|
||||
RunProjection, SkillsProjection, StageModelUsage, StageProjection, SubAgentProjection,
|
||||
SubAgentStatus, first_event_seq,
|
||||
};
|
||||
pub use run_sandbox::{RunSandbox, RunSandboxRuntime};
|
||||
pub use run_summary::{
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ use fabro_model::{ReasoningEffort, Speed};
|
|||
|
||||
use crate::run_event::{AgentSessionActivatedProps, StagePromptProps};
|
||||
use crate::{
|
||||
AgentBackend, BilledTokenCounts, Checkpoint, Conclusion, InterviewQuestionRecord,
|
||||
InvalidTransition, ModelRef, PullRequestLink, RunControlAction, RunDiff, RunId, RunSandbox,
|
||||
RunSpec, RunStatus, StageCompletion, StageHandler, StageId, StageState, StageTiming,
|
||||
StartRecord, TodoListProjection,
|
||||
AgentBackend, AgentMcpToolSummary, AgentSkillActivationSource, AgentSkillSummary,
|
||||
BilledTokenCounts, Checkpoint, Conclusion, InterviewQuestionRecord, InvalidTransition,
|
||||
ModelRef, PullRequestLink, RunControlAction, RunDiff, RunId, RunSandbox, RunSpec, RunStatus,
|
||||
StageCompletion, StageHandler, StageId, StageState, StageTiming, StartRecord,
|
||||
TodoListProjection,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
|
|
@ -35,11 +36,6 @@ pub struct RunProjection {
|
|||
pub pull_request: Option<PullRequestLink>,
|
||||
pub superseded_by: Option<RunId>,
|
||||
pub pending_interviews: BTreeMap<String, PendingInterviewRecord>,
|
||||
/// Projected todo / task lists, keyed by `list_id` (`openai_plan:<session>`
|
||||
/// or `anthropic_tasks:<root_session>`). Maintained by replaying
|
||||
/// `todo.created`, `todo.updated`, and `todo.deleted` events.
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub todos_by_list: BTreeMap<String, TodoListProjection>,
|
||||
stages: HashMap<StageId, StageProjection>,
|
||||
}
|
||||
|
||||
|
|
@ -150,9 +146,67 @@ pub struct StageProjection {
|
|||
pub usage: BilledTokenCounts,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<ModelRef>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub todos: Option<TodoListProjection>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub subagents: Vec<SubAgentProjection>,
|
||||
#[serde(default, skip_serializing_if = "SkillsProjection::is_empty")]
|
||||
pub skills: SkillsProjection,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub mcp_servers: Vec<McpServerProjection>,
|
||||
pub state: StageState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SubAgentProjection {
|
||||
pub agent_id: String,
|
||||
pub depth: usize,
|
||||
pub task: String,
|
||||
pub status: SubAgentStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum SubAgentStatus {
|
||||
Running,
|
||||
Completed { success: bool, turns_used: usize },
|
||||
Failed { error: serde_json::Value },
|
||||
Closed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SkillsProjection {
|
||||
pub available: Vec<AgentSkillSummary>,
|
||||
pub activated: Vec<ActivatedSkill>,
|
||||
}
|
||||
|
||||
impl SkillsProjection {
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.available.is_empty() && self.activated.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ActivatedSkill {
|
||||
pub name: String,
|
||||
pub source: AgentSkillActivationSource,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct McpServerProjection {
|
||||
pub server_name: String,
|
||||
pub tool_count: usize,
|
||||
pub status: McpServerStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum McpServerStatus {
|
||||
Ready { tools: Vec<AgentMcpToolSummary> },
|
||||
Failed { error: String },
|
||||
}
|
||||
|
||||
/// Convert a 1-based event sequence number into the `NonZeroU32` form used for
|
||||
/// `StageProjection::first_event_seq`. Run event seqs always start at 1.
|
||||
#[must_use]
|
||||
|
|
@ -171,6 +225,10 @@ impl StageProjection {
|
|||
timing: None,
|
||||
usage: BilledTokenCounts::default(),
|
||||
model: None,
|
||||
todos: None,
|
||||
subagents: Vec::new(),
|
||||
skills: SkillsProjection::default(),
|
||||
mcp_servers: Vec::new(),
|
||||
provider_used: None,
|
||||
diff: None,
|
||||
script_invocation: None,
|
||||
|
|
@ -247,7 +305,6 @@ impl RunProjection {
|
|||
pull_request: None,
|
||||
superseded_by: None,
|
||||
pending_interviews: BTreeMap::new(),
|
||||
todos_by_list: BTreeMap::new(),
|
||||
stages: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@ base.ts
|
|||
common.ts
|
||||
configuration.ts
|
||||
index.ts
|
||||
models/activated-skill.ts
|
||||
models/agent-mcp-tool-summary.ts
|
||||
models/agent-permissions.ts
|
||||
models/agent-skill-activation-source.ts
|
||||
models/agent-skill-summary.ts
|
||||
models/aggregate-billing-totals.ts
|
||||
models/aggregate-billing.ts
|
||||
models/api-question-option.ts
|
||||
|
|
@ -162,7 +166,11 @@ models/manifest-goal.ts
|
|||
models/manifest-target.ts
|
||||
models/manifest-workflow-config.ts
|
||||
models/manifest-workflow.ts
|
||||
models/mcp-server-projection.ts
|
||||
models/mcp-server-settings.ts
|
||||
models/mcp-server-status-failed.ts
|
||||
models/mcp-server-status-ready.ts
|
||||
models/mcp-server-status.ts
|
||||
models/mcp-transport-http.ts
|
||||
models/mcp-transport-sandbox.ts
|
||||
models/mcp-transport-stdio.ts
|
||||
|
|
@ -253,6 +261,7 @@ models/pull-request-user.ts
|
|||
models/pull-request.ts
|
||||
models/question-type.ts
|
||||
models/reasoning-effort-feature.ts
|
||||
models/reasoning-effort.ts
|
||||
models/related-workflow-diagnostic.ts
|
||||
models/render-workflow-graph-direction.ts
|
||||
models/render-workflow-graph-format.ts
|
||||
|
|
@ -377,11 +386,13 @@ models/session-record.ts
|
|||
models/session-status.ts
|
||||
models/session-summary.ts
|
||||
models/session-turn.ts
|
||||
models/skills-projection.ts
|
||||
models/slack-integration-settings.ts
|
||||
models/ssh-access-request.ts
|
||||
models/ssh-access-response.ts
|
||||
models/stage-completion.ts
|
||||
models/stage-handler.ts
|
||||
models/stage-model-usage.ts
|
||||
models/stage-outcome.ts
|
||||
models/stage-projection.ts
|
||||
models/stage-state.ts
|
||||
|
|
@ -390,6 +401,12 @@ models/stage-timing.ts
|
|||
models/start-record.ts
|
||||
models/start-run-request.ts
|
||||
models/steer-run-request.ts
|
||||
models/sub-agent-projection.ts
|
||||
models/sub-agent-status-closed.ts
|
||||
models/sub-agent-status-completed.ts
|
||||
models/sub-agent-status-failed.ts
|
||||
models/sub-agent-status-running.ts
|
||||
models/sub-agent-status.ts
|
||||
models/submit-answer-multi-selected-request.ts
|
||||
models/submit-answer-no-request.ts
|
||||
models/submit-answer-request.ts
|
||||
|
|
|
|||
26
lib/packages/fabro-api-client/src/models/activated-skill.ts
generated
Normal file
26
lib/packages/fabro-api-client/src/models/activated-skill.ts
generated
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { AgentSkillActivationSource } from './agent-skill-activation-source';
|
||||
|
||||
/**
|
||||
* One observed agent skill activation.
|
||||
*/
|
||||
export interface ActivatedSkill {
|
||||
'name': string;
|
||||
'source': AgentSkillActivationSource;
|
||||
}
|
||||
23
lib/packages/fabro-api-client/src/models/agent-mcp-tool-summary.ts
generated
Normal file
23
lib/packages/fabro-api-client/src/models/agent-mcp-tool-summary.ts
generated
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Summary of one tool exposed by an MCP server.
|
||||
*/
|
||||
export interface AgentMcpToolSummary {
|
||||
'name': string;
|
||||
'original_name': string;
|
||||
}
|
||||
26
lib/packages/fabro-api-client/src/models/agent-skill-activation-source.ts
generated
Normal file
26
lib/packages/fabro-api-client/src/models/agent-skill-activation-source.ts
generated
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Source that activated an agent skill.
|
||||
*/
|
||||
|
||||
export const AgentSkillActivationSource = {
|
||||
SLASH: 'slash',
|
||||
TOOL: 'tool'
|
||||
} as const;
|
||||
|
||||
export type AgentSkillActivationSource = typeof AgentSkillActivationSource[keyof typeof AgentSkillActivationSource];
|
||||
23
lib/packages/fabro-api-client/src/models/agent-skill-summary.ts
generated
Normal file
23
lib/packages/fabro-api-client/src/models/agent-skill-summary.ts
generated
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Summary of an available agent skill.
|
||||
*/
|
||||
export interface AgentSkillSummary {
|
||||
'name': string;
|
||||
'description': string;
|
||||
}
|
||||
15
lib/packages/fabro-api-client/src/models/index.ts
generated
15
lib/packages/fabro-api-client/src/models/index.ts
generated
|
|
@ -1,4 +1,8 @@
|
|||
export * from './activated-skill';
|
||||
export * from './agent-mcp-tool-summary';
|
||||
export * from './agent-permissions';
|
||||
export * from './agent-skill-activation-source';
|
||||
export * from './agent-skill-summary';
|
||||
export * from './aggregate-billing';
|
||||
export * from './aggregate-billing-totals';
|
||||
export * from './api-question';
|
||||
|
|
@ -138,7 +142,11 @@ export * from './manifest-goal';
|
|||
export * from './manifest-target';
|
||||
export * from './manifest-workflow';
|
||||
export * from './manifest-workflow-config';
|
||||
export * from './mcp-server-projection';
|
||||
export * from './mcp-server-settings';
|
||||
export * from './mcp-server-status';
|
||||
export * from './mcp-server-status-failed';
|
||||
export * from './mcp-server-status-ready';
|
||||
export * from './mcp-transport';
|
||||
export * from './mcp-transport-http';
|
||||
export * from './mcp-transport-sandbox';
|
||||
|
|
@ -354,6 +362,7 @@ export * from './session-record';
|
|||
export * from './session-status';
|
||||
export * from './session-summary';
|
||||
export * from './session-turn';
|
||||
export * from './skills-projection';
|
||||
export * from './slack-integration-settings';
|
||||
export * from './ssh-access-request';
|
||||
export * from './ssh-access-response';
|
||||
|
|
@ -368,6 +377,12 @@ export * from './stage-timing';
|
|||
export * from './start-record';
|
||||
export * from './start-run-request';
|
||||
export * from './steer-run-request';
|
||||
export * from './sub-agent-projection';
|
||||
export * from './sub-agent-status';
|
||||
export * from './sub-agent-status-closed';
|
||||
export * from './sub-agent-status-completed';
|
||||
export * from './sub-agent-status-failed';
|
||||
export * from './sub-agent-status-running';
|
||||
export * from './submit-answer-multi-selected-request';
|
||||
export * from './submit-answer-no-request';
|
||||
export * from './submit-answer-request';
|
||||
|
|
|
|||
27
lib/packages/fabro-api-client/src/models/mcp-server-projection.ts
generated
Normal file
27
lib/packages/fabro-api-client/src/models/mcp-server-projection.ts
generated
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/* 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 { McpServerStatus } from './mcp-server-status';
|
||||
|
||||
/**
|
||||
* Projected state for one MCP server observed by an agent stage.
|
||||
*/
|
||||
export interface McpServerProjection {
|
||||
'server_name': string;
|
||||
'tool_count': number;
|
||||
'status': McpServerStatus;
|
||||
}
|
||||
26
lib/packages/fabro-api-client/src/models/mcp-server-status-failed.ts
generated
Normal file
26
lib/packages/fabro-api-client/src/models/mcp-server-status-failed.ts
generated
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export interface McpServerStatusFailed {
|
||||
'kind': McpServerStatusFailedKindEnum;
|
||||
'error': string;
|
||||
}
|
||||
|
||||
export const McpServerStatusFailedKindEnum = {
|
||||
FAILED: 'failed'
|
||||
} as const;
|
||||
|
||||
export type McpServerStatusFailedKindEnum = typeof McpServerStatusFailedKindEnum[keyof typeof McpServerStatusFailedKindEnum];
|
||||
29
lib/packages/fabro-api-client/src/models/mcp-server-status-ready.ts
generated
Normal file
29
lib/packages/fabro-api-client/src/models/mcp-server-status-ready.ts
generated
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/* 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 { AgentMcpToolSummary } from './agent-mcp-tool-summary';
|
||||
|
||||
export interface McpServerStatusReady {
|
||||
'kind': McpServerStatusReadyKindEnum;
|
||||
'tools': Array<AgentMcpToolSummary>;
|
||||
}
|
||||
|
||||
export const McpServerStatusReadyKindEnum = {
|
||||
READY: 'ready'
|
||||
} as const;
|
||||
|
||||
export type McpServerStatusReadyKindEnum = typeof McpServerStatusReadyKindEnum[keyof typeof McpServerStatusReadyKindEnum];
|
||||
30
lib/packages/fabro-api-client/src/models/mcp-server-status.ts
generated
Normal file
30
lib/packages/fabro-api-client/src/models/mcp-server-status.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 { AgentMcpToolSummary } from './agent-mcp-tool-summary';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { McpServerStatusFailed } from './mcp-server-status-failed';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { McpServerStatusReady } from './mcp-server-status-ready';
|
||||
|
||||
/**
|
||||
* @type McpServerStatus
|
||||
* Projected MCP server readiness status.
|
||||
*/
|
||||
export type McpServerStatus = { kind: 'failed' } & McpServerStatusFailed | { kind: 'ready' } & McpServerStatusReady;
|
||||
|
|
@ -43,9 +43,6 @@ import type { StageProjection } from './stage-projection';
|
|||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { StartRecord } from './start-record';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { TodoListProjection } from './todo-list-projection';
|
||||
|
||||
/**
|
||||
* Raw internal run projection derived from the event log.
|
||||
|
|
@ -79,10 +76,6 @@ export interface RunProjection {
|
|||
'pull_request'?: PullRequestLink | null;
|
||||
'superseded_by'?: string | null;
|
||||
'pending_interviews': { [key: string]: PendingInterviewRecord; };
|
||||
/**
|
||||
* Projected todo / task lists, keyed by `list_id` (`openai_plan:<session_id>` or `anthropic_tasks:<root_session_id>`). Built by replaying `todo.created`, `todo.updated`, and `todo.deleted` events.
|
||||
*/
|
||||
'todos_by_list'?: { [key: string]: TodoListProjection; };
|
||||
/**
|
||||
* Map from StageId (`node_id@visit`) to stage projection data.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -49,9 +49,6 @@ export interface RunStage {
|
|||
* 1-based visit count; bumped each time the workflow re-enters this node.
|
||||
*/
|
||||
'visit': number;
|
||||
/**
|
||||
* Provider, model, and request controls recorded for the latest stage attempt.
|
||||
*/
|
||||
'provider_used'?: StageModelUsage | null;
|
||||
/**
|
||||
* Wall-clock time the latest attempt of this stage started, if known.
|
||||
|
|
|
|||
29
lib/packages/fabro-api-client/src/models/skills-projection.ts
generated
Normal file
29
lib/packages/fabro-api-client/src/models/skills-projection.ts
generated
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/* 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 { ActivatedSkill } from './activated-skill';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { AgentSkillSummary } from './agent-skill-summary';
|
||||
|
||||
/**
|
||||
* Agent skills discovered and activated during a stage.
|
||||
*/
|
||||
export interface SkillsProjection {
|
||||
'available': Array<AgentSkillSummary>;
|
||||
'activated': Array<ActivatedSkill>;
|
||||
}
|
||||
|
|
@ -24,6 +24,9 @@ import type { ReasoningEffort } from './reasoning-effort';
|
|||
* Provider, model, and request-control metadata recorded for a stage attempt.
|
||||
*/
|
||||
export interface StageModelUsage {
|
||||
/**
|
||||
* Source of the stage\'s model usage metadata.
|
||||
*/
|
||||
'mode': string;
|
||||
'provider'?: string | null;
|
||||
'model'?: string | null;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,12 @@ import type { BillingModelRef } from './billing-model-ref';
|
|||
import type { CommandTermination } from './command-termination';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { McpServerProjection } from './mcp-server-projection';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { SkillsProjection } from './skills-projection';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { StageCompletion } from './stage-completion';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
|
|
@ -34,6 +40,12 @@ import type { StageState } from './stage-state';
|
|||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { StageTiming } from './stage-timing';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { SubAgentProjection } from './sub-agent-projection';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { TodoListProjection } from './todo-list-projection';
|
||||
|
||||
/**
|
||||
* Observable projection data for one workflow stage execution.
|
||||
|
|
@ -43,9 +55,6 @@ export interface StageProjection {
|
|||
'prompt'?: string | null;
|
||||
'response'?: string | null;
|
||||
'completion'?: StageCompletion | null;
|
||||
/**
|
||||
* Provider and model metadata recorded for the stage attempt.
|
||||
*/
|
||||
'provider_used'?: StageModelUsage | null;
|
||||
'diff'?: string | null;
|
||||
/**
|
||||
|
|
@ -71,6 +80,19 @@ export interface StageProjection {
|
|||
'timing'?: StageTiming | null;
|
||||
'usage': BilledTokenCounts;
|
||||
'model'?: BillingModelRef | null;
|
||||
'todos'?: TodoListProjection | null;
|
||||
/**
|
||||
* Subagents spawned by this stage, in replay/insertion order.
|
||||
*/
|
||||
'subagents'?: Array<SubAgentProjection>;
|
||||
/**
|
||||
* Agent skills discovered and activated during this stage.
|
||||
*/
|
||||
'skills'?: SkillsProjection;
|
||||
/**
|
||||
* MCP servers observed by this stage.
|
||||
*/
|
||||
'mcp_servers'?: Array<McpServerProjection>;
|
||||
/**
|
||||
* Lifecycle state of the stage projection.
|
||||
*/
|
||||
|
|
|
|||
28
lib/packages/fabro-api-client/src/models/sub-agent-projection.ts
generated
Normal file
28
lib/packages/fabro-api-client/src/models/sub-agent-projection.ts
generated
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { SubAgentStatus } from './sub-agent-status';
|
||||
|
||||
/**
|
||||
* Current projected state for one subagent spawned by an agent stage.
|
||||
*/
|
||||
export interface SubAgentProjection {
|
||||
'agent_id': string;
|
||||
'depth': number;
|
||||
'task': string;
|
||||
'status': SubAgentStatus;
|
||||
}
|
||||
25
lib/packages/fabro-api-client/src/models/sub-agent-status-closed.ts
generated
Normal file
25
lib/packages/fabro-api-client/src/models/sub-agent-status-closed.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 SubAgentStatusClosed {
|
||||
'kind': SubAgentStatusClosedKindEnum;
|
||||
}
|
||||
|
||||
export const SubAgentStatusClosedKindEnum = {
|
||||
CLOSED: 'closed'
|
||||
} as const;
|
||||
|
||||
export type SubAgentStatusClosedKindEnum = typeof SubAgentStatusClosedKindEnum[keyof typeof SubAgentStatusClosedKindEnum];
|
||||
27
lib/packages/fabro-api-client/src/models/sub-agent-status-completed.ts
generated
Normal file
27
lib/packages/fabro-api-client/src/models/sub-agent-status-completed.ts
generated
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/* 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 SubAgentStatusCompleted {
|
||||
'kind': SubAgentStatusCompletedKindEnum;
|
||||
'success': boolean;
|
||||
'turns_used': number;
|
||||
}
|
||||
|
||||
export const SubAgentStatusCompletedKindEnum = {
|
||||
COMPLETED: 'completed'
|
||||
} as const;
|
||||
|
||||
export type SubAgentStatusCompletedKindEnum = typeof SubAgentStatusCompletedKindEnum[keyof typeof SubAgentStatusCompletedKindEnum];
|
||||
26
lib/packages/fabro-api-client/src/models/sub-agent-status-failed.ts
generated
Normal file
26
lib/packages/fabro-api-client/src/models/sub-agent-status-failed.ts
generated
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Fabro Run API
|
||||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export interface SubAgentStatusFailed {
|
||||
'kind': SubAgentStatusFailedKindEnum;
|
||||
'error': any;
|
||||
}
|
||||
|
||||
export const SubAgentStatusFailedKindEnum = {
|
||||
FAILED: 'failed'
|
||||
} as const;
|
||||
|
||||
export type SubAgentStatusFailedKindEnum = typeof SubAgentStatusFailedKindEnum[keyof typeof SubAgentStatusFailedKindEnum];
|
||||
25
lib/packages/fabro-api-client/src/models/sub-agent-status-running.ts
generated
Normal file
25
lib/packages/fabro-api-client/src/models/sub-agent-status-running.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 SubAgentStatusRunning {
|
||||
'kind': SubAgentStatusRunningKindEnum;
|
||||
}
|
||||
|
||||
export const SubAgentStatusRunningKindEnum = {
|
||||
RUNNING: 'running'
|
||||
} as const;
|
||||
|
||||
export type SubAgentStatusRunningKindEnum = typeof SubAgentStatusRunningKindEnum[keyof typeof SubAgentStatusRunningKindEnum];
|
||||
33
lib/packages/fabro-api-client/src/models/sub-agent-status.ts
generated
Normal file
33
lib/packages/fabro-api-client/src/models/sub-agent-status.ts
generated
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/* 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 { SubAgentStatusClosed } from './sub-agent-status-closed';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { SubAgentStatusCompleted } from './sub-agent-status-completed';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { SubAgentStatusFailed } from './sub-agent-status-failed';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import type { SubAgentStatusRunning } from './sub-agent-status-running';
|
||||
|
||||
/**
|
||||
* @type SubAgentStatus
|
||||
* Projected lifecycle status for a subagent.
|
||||
*/
|
||||
export type SubAgentStatus = { kind: 'closed' } & SubAgentStatusClosed | { kind: 'completed' } & SubAgentStatusCompleted | { kind: 'failed' } & SubAgentStatusFailed | { kind: 'running' } & SubAgentStatusRunning;
|
||||
Loading…
Add table
Reference in a new issue