Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions crates/ironclaw_product_workflow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ pub use fakes::{FakeConversationBindingService, FakeIdempotencyLedger, FakeInbou
pub use inbound_turn::{DefaultInboundTurnService, InboundTurnOutcome, InboundTurnService};
pub use ledger::{IdempotencyDecision, IdempotencyLedger};
pub use reborn_services::{
RebornCancelRunResponse, RebornCreateThreadResponse, RebornResolveGateResponse,
RebornResumeGateResponse, RebornServices, RebornServicesApi, RebornServicesError,
RebornServicesErrorCode, RebornStreamEventsRequest, RebornStreamEventsResponse,
RebornSubmitTurnResponse, RebornTimelineRequest, RebornTimelineResponse,
RebornCancelRunResponse, RebornCreateThreadResponse, RebornGetRunStateRequest,
RebornGetRunStateResponse, RebornResolveGateResponse, RebornResumeGateResponse, RebornServices,
RebornServicesApi, RebornServicesError, RebornServicesErrorCode, RebornStreamEventsRequest,
RebornStreamEventsResponse, RebornSubmitTurnResponse, RebornTimelineRequest,
RebornTimelineResponse,
};
pub use webui_inbound::{
WebUiAuthenticatedCaller, WebUiCancelReason, WebUiCancelRunRequest, WebUiCreateThreadRequest,
Expand Down
49 changes: 46 additions & 3 deletions crates/ironclaw_product_workflow/src/reborn_services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ mod types;

pub use error::{RebornServicesError, RebornServicesErrorCode};
pub use types::{
RebornCancelRunResponse, RebornCreateThreadResponse, RebornResolveGateResponse,
RebornResumeGateResponse, RebornStreamEventsRequest, RebornStreamEventsResponse,
RebornSubmitTurnResponse, RebornTimelineRequest, RebornTimelineResponse,
RebornCancelRunResponse, RebornCreateThreadResponse, RebornGetRunStateRequest,
RebornGetRunStateResponse, RebornResolveGateResponse, RebornResumeGateResponse,
RebornStreamEventsRequest, RebornStreamEventsResponse, RebornSubmitTurnResponse,
RebornTimelineRequest, RebornTimelineResponse,
};

/// Stable WebUI-facing facade surface for beta Reborn routes.
Expand Down Expand Up @@ -72,6 +73,12 @@ pub trait RebornServicesApi: Send + Sync {
caller: WebUiAuthenticatedCaller,
request: WebUiResolveGateRequest,
) -> Result<RebornResolveGateResponse, RebornServicesError>;

async fn get_run_state(
&self,
caller: WebUiAuthenticatedCaller,
request: RebornGetRunStateRequest,
) -> Result<RebornGetRunStateResponse, RebornServicesError>;
}

/// Default facade implementation composed at the WebUI boundary.
Expand Down Expand Up @@ -412,6 +419,28 @@ impl RebornServicesApi for RebornServices {
}
}
}

async fn get_run_state(
&self,
caller: WebUiAuthenticatedCaller,
request: RebornGetRunStateRequest,
) -> Result<RebornGetRunStateResponse, RebornServicesError> {
let thread_id = parse_thread_id_field("thread_id", request.thread_id)?;
let run_id = parse_run_id_field("run_id", request.run_id)?;
let scope = caller.turn_scope(thread_id);
let actor = caller.actor();
// TurnScope has no owner_user_id, so without this gate any caller
// sharing the (tenant, agent, project) scope could read another user's
// run state by guessing thread_id and run_id. Mirrors the ownership
// probe `cancel_run` and `resolve_gate` already perform.
assert_thread_owned_by(self.thread_service.as_ref(), &scope, &actor).await?;
let state = self
.turn_coordinator
.get_run_state(GetRunStateRequest { scope, run_id })
.await
.map_err(map_turn_error)?;
Ok(state.into())
}
Comment thread
italic-jinxin marked this conversation as resolved.
}

struct AcceptedWebUiMessage {
Expand Down Expand Up @@ -557,6 +586,20 @@ fn parse_thread_id_field(
})
}

fn parse_run_id_field(
field: &'static str,
value: String,
) -> Result<TurnRunId, RebornServicesError> {
Uuid::parse_str(&value)
.map(TurnRunId::from_uuid)
.map_err(|_| {
RebornServicesError::validation(WebUiInboundValidationError::new(
field,
WebUiInboundValidationCode::InvalidId,
))
})
}

fn accepted_message_ref(message_id: String) -> Result<AcceptedMessageRef, RebornServicesError> {
AcceptedMessageRef::new(format!("msg:{message_id}")).map_err(|_| {
RebornServicesError::from_status(RebornServicesErrorCode::Internal, 500, false)
Expand Down
57 changes: 56 additions & 1 deletion crates/ironclaw_product_workflow/src/reborn_services/types.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use chrono::{DateTime, Utc};
use ironclaw_host_api::ThreadId;
use ironclaw_product_adapters::{ProductOutboundEnvelope, ProjectionCursor};
use ironclaw_threads::{SessionThreadRecord, SummaryArtifact, ThreadMessageRecord};
use ironclaw_turns::{
AcceptedMessageRef, CancelRunResponse, EventCursor, ResumeTurnResponse, TurnRunId, TurnStatus,
AcceptedMessageRef, CancelRunResponse, EventCursor, GateRef, ResumeTurnResponse,
SanitizedFailure, TurnCheckpointId, TurnRunId, TurnRunState, TurnStatus,
};
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -106,3 +108,56 @@ pub enum RebornResolveGateResponse {
Resumed(RebornResumeGateResponse),
Cancelled(RebornCancelRunResponse),
}

/// Browser body for the WebUI run-state read.
///
/// Pure read — no idempotency key. Caller authority is supplied separately by
/// `WebUiAuthenticatedCaller` and combined with `thread_id` to produce the
/// canonical [`ironclaw_turns::TurnScope`] inside the facade.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RebornGetRunStateRequest {
pub thread_id: String,
pub run_id: String,
}

/// Stable run-state projection returned to WebUI route handlers.
///
/// Deliberately omits M3-internal fields carried on [`TurnRunState`]:
/// `scope`, `source_binding_ref`, `reply_target_binding_ref`, and
/// `resolved_model_route`. Route handlers and downstream M5 consumers must
/// build their views from this surface only.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RebornGetRunStateResponse {
pub turn_id: String,
pub run_id: TurnRunId,
pub status: TurnStatus,
pub event_cursor: EventCursor,
pub accepted_message_ref: AcceptedMessageRef,
pub resolved_run_profile_id: String,
pub resolved_run_profile_version: u64,
pub received_at: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub checkpoint_id: Option<TurnCheckpointId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gate_ref: Option<GateRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub failure: Option<SanitizedFailure>,
}
Comment thread
italic-jinxin marked this conversation as resolved.

impl From<TurnRunState> for RebornGetRunStateResponse {
fn from(value: TurnRunState) -> Self {
Self {
turn_id: value.turn_id.to_string(),
run_id: value.run_id,
status: value.status,
event_cursor: value.event_cursor,
accepted_message_ref: value.accepted_message_ref,
resolved_run_profile_id: value.resolved_run_profile_id.as_str().to_string(),
resolved_run_profile_version: value.resolved_run_profile_version.as_u64(),
received_at: value.received_at,
checkpoint_id: value.checkpoint_id,
gate_ref: value.gate_ref,
failure: value.failure,
}
}
}
Loading
Loading