runtime: remove worker transcript projection
This commit is contained in:
@@ -170,10 +170,10 @@ pub struct WorkingDirectoryStatus {
|
||||
/// Browser/product launch semantics are resolved by a backend before this
|
||||
/// request is built. The request contains only durable Runtime identity inputs:
|
||||
/// a backend-decided profile selector, the Decodal profile source archive source
|
||||
/// used to resolve that selector, optional initial user input committed with the
|
||||
/// Worker catalog/transcript persistence, and an optional Runtime-owned working
|
||||
/// directory binding. Browser-facing status for materialized working directories
|
||||
/// is summarized without exposing raw host paths.
|
||||
/// used to resolve that selector, optional initial user input committed as a
|
||||
/// protocol observation event, and an optional Runtime-owned working directory
|
||||
/// binding. Browser-facing status for materialized working directories is
|
||||
/// summarized without exposing raw host paths.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CreateWorkerRequest {
|
||||
pub profile: ProfileSelector,
|
||||
@@ -215,7 +215,6 @@ pub struct WorkerSummary {
|
||||
pub profile_source: ProfileSourceArchiveRef,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub config_bundle: Option<ConfigBundleRef>,
|
||||
pub transcript_len: usize,
|
||||
pub last_event_id: u64,
|
||||
}
|
||||
|
||||
@@ -231,7 +230,6 @@ pub struct WorkerDetail {
|
||||
pub profile_source: ProfileSourceArchiveRef,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub config_bundle: Option<ConfigBundleRef>,
|
||||
pub transcript_len: usize,
|
||||
pub last_event_id: u64,
|
||||
}
|
||||
|
||||
|
||||
@@ -305,7 +305,7 @@ pub enum WorkerExecutionSpawnResult {
|
||||
|
||||
/// Backend boundary for Worker execution.
|
||||
///
|
||||
/// Runtime owns Worker catalog, transcript, observation, and lifecycle state. A
|
||||
/// Runtime owns Worker catalog, protocol observation, and lifecycle state. A
|
||||
/// backend owns concrete execution. The default Runtime has no backend, so input
|
||||
/// to those Workers is rejected instead of producing providerless responses.
|
||||
pub trait WorkerExecutionBackend: Send + Sync + 'static {
|
||||
|
||||
@@ -5,10 +5,9 @@ use crate::error::RuntimeError;
|
||||
use crate::execution::WorkerExecutionStatus;
|
||||
use crate::identity::{RuntimeId, WorkerId, WorkerRef};
|
||||
use crate::management::{RuntimeBackendKind, RuntimeLimits, RuntimeStatus};
|
||||
use crate::observation::{
|
||||
EventCursor, RuntimeEvent, RuntimeEventBatch, TranscriptEntry, TranscriptProjection,
|
||||
TranscriptQuery,
|
||||
};
|
||||
#[cfg(feature = "ws-server")]
|
||||
use crate::observation::WorkerObservationEvent;
|
||||
use crate::observation::{EventCursor, RuntimeEvent, RuntimeEventBatch};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
@@ -22,7 +21,8 @@ const RUNTIME_FILE: &str = "runtime.json";
|
||||
const EVENTS_FILE: &str = "events.jsonl";
|
||||
const WORKERS_DIR: &str = "workers";
|
||||
const WORKER_FILE: &str = "worker.json";
|
||||
const TRANSCRIPT_FILE: &str = "transcript.jsonl";
|
||||
#[cfg(feature = "ws-server")]
|
||||
const OBSERVATIONS_FILE: &str = "observations.jsonl";
|
||||
|
||||
static NEXT_TMP_SEQUENCE: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
@@ -119,42 +119,6 @@ impl FsRuntimeStore {
|
||||
})
|
||||
}
|
||||
|
||||
/// Read a persisted Worker transcript directly from its Worker-scoped log.
|
||||
pub fn read_transcript(
|
||||
&self,
|
||||
worker_ref: &WorkerRef,
|
||||
query: TranscriptQuery,
|
||||
max_limit: usize,
|
||||
) -> Result<TranscriptProjection, RuntimeError> {
|
||||
self.ensure_worker_ref(worker_ref)?;
|
||||
if query.limit > max_limit {
|
||||
return Err(RuntimeError::LimitTooLarge {
|
||||
requested: query.limit,
|
||||
max: max_limit,
|
||||
});
|
||||
}
|
||||
|
||||
let path = self.transcript_path(&worker_ref.worker_id);
|
||||
let entries = read_json_lines::<TranscriptEntry>(&path, "read transcript")?;
|
||||
let total_items = entries.len();
|
||||
let end = query.start.saturating_add(query.limit).min(total_items);
|
||||
let items = if query.start >= total_items {
|
||||
Vec::new()
|
||||
} else {
|
||||
entries[query.start..end].to_vec()
|
||||
};
|
||||
let next_start = (end < total_items).then_some(end);
|
||||
|
||||
Ok(TranscriptProjection {
|
||||
worker_ref: worker_ref.clone(),
|
||||
start: query.start,
|
||||
limit: query.limit,
|
||||
total_items,
|
||||
items,
|
||||
next_start,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn open_or_create(
|
||||
root: PathBuf,
|
||||
runtime_id: RuntimeId,
|
||||
@@ -220,7 +184,12 @@ impl FsRuntimeStore {
|
||||
&WorkerSnapshot::from_persisted(worker),
|
||||
"write worker snapshot",
|
||||
)?;
|
||||
ensure_file_exists(&worker_dir.join(TRANSCRIPT_FILE), "create transcript log")
|
||||
#[cfg(feature = "ws-server")]
|
||||
ensure_file_exists(
|
||||
&worker_dir.join(OBSERVATIONS_FILE),
|
||||
"create observations log",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn append_event(&self, event: &RuntimeEvent) -> Result<(), RuntimeError> {
|
||||
@@ -230,15 +199,16 @@ impl FsRuntimeStore {
|
||||
append_json_line(&self.events_path(), event, "append event")
|
||||
}
|
||||
|
||||
pub(crate) fn append_transcript_entry(
|
||||
#[cfg(feature = "ws-server")]
|
||||
pub(crate) fn append_worker_observation_event(
|
||||
&self,
|
||||
entry: &TranscriptEntry,
|
||||
event: &WorkerObservationEvent,
|
||||
) -> Result<(), RuntimeError> {
|
||||
self.ensure_worker_ref(&entry.worker_ref)?;
|
||||
self.ensure_worker_ref(&event.worker_ref)?;
|
||||
append_json_line(
|
||||
&self.transcript_path(&entry.worker_ref.worker_id),
|
||||
entry,
|
||||
"append transcript",
|
||||
&self.observations_path(&event.worker_ref.worker_id),
|
||||
event,
|
||||
"append worker observation",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -278,6 +248,9 @@ impl FsRuntimeStore {
|
||||
})?;
|
||||
worker_dirs.sort_by_key(|entry| entry.path());
|
||||
|
||||
#[cfg(feature = "ws-server")]
|
||||
let mut observation_events = Vec::new();
|
||||
|
||||
for entry in worker_dirs {
|
||||
let path = entry.path();
|
||||
if !path.is_dir() {
|
||||
@@ -312,38 +285,44 @@ impl FsRuntimeStore {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let transcript = match read_json_lines::<TranscriptEntry>(
|
||||
&path.join(TRANSCRIPT_FILE),
|
||||
"read transcript",
|
||||
#[cfg(feature = "ws-server")]
|
||||
let worker_observations = match read_json_lines::<WorkerObservationEvent>(
|
||||
&path.join(OBSERVATIONS_FILE),
|
||||
"read worker observations",
|
||||
) {
|
||||
Ok(transcript) => transcript,
|
||||
Ok(events) => events,
|
||||
Err(_error) => {
|
||||
record_worker_load_diagnostic(
|
||||
&mut snapshot,
|
||||
Some(worker_snapshot.worker_ref.clone()),
|
||||
"ignored worker with unreadable transcript while loading runtime store",
|
||||
"ignored worker with unreadable observations while loading runtime store",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let mut transcript_valid = true;
|
||||
for entry in &transcript {
|
||||
if self.ensure_worker_ref(&entry.worker_ref).is_err()
|
||||
|| entry.worker_ref.worker_id != worker_snapshot.worker_id
|
||||
#[cfg(feature = "ws-server")]
|
||||
let mut observations_valid = true;
|
||||
#[cfg(feature = "ws-server")]
|
||||
for event in &worker_observations {
|
||||
if self.ensure_worker_ref(&event.worker_ref).is_err()
|
||||
|| event.worker_ref.worker_id != worker_snapshot.worker_id
|
||||
{
|
||||
transcript_valid = false;
|
||||
observations_valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !transcript_valid {
|
||||
#[cfg(feature = "ws-server")]
|
||||
if !observations_valid {
|
||||
record_worker_load_diagnostic(
|
||||
&mut snapshot,
|
||||
Some(worker_snapshot.worker_ref.clone()),
|
||||
"ignored worker with invalid transcript while loading runtime store",
|
||||
"ignored worker with invalid observations while loading runtime store",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let worker = worker_snapshot.into_persisted(transcript);
|
||||
#[cfg(feature = "ws-server")]
|
||||
observation_events.extend(worker_observations);
|
||||
let worker = worker_snapshot.into_persisted();
|
||||
if workers.insert(worker.worker_id.clone(), worker).is_some() {
|
||||
record_worker_load_diagnostic(
|
||||
&mut snapshot,
|
||||
@@ -353,7 +332,15 @@ impl FsRuntimeStore {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(snapshot.into_persisted(events, workers))
|
||||
#[cfg(feature = "ws-server")]
|
||||
observation_events.sort_by_key(|event| event.sequence);
|
||||
|
||||
Ok(snapshot.into_persisted(
|
||||
events,
|
||||
workers,
|
||||
#[cfg(feature = "ws-server")]
|
||||
observation_events,
|
||||
))
|
||||
}
|
||||
|
||||
fn ensure_worker_ref(&self, worker_ref: &WorkerRef) -> Result<(), RuntimeError> {
|
||||
@@ -382,8 +369,9 @@ impl FsRuntimeStore {
|
||||
.join(encoded_component(worker_id.as_str()))
|
||||
}
|
||||
|
||||
fn transcript_path(&self, worker_id: &WorkerId) -> PathBuf {
|
||||
self.worker_dir(worker_id).join(TRANSCRIPT_FILE)
|
||||
#[cfg(feature = "ws-server")]
|
||||
fn observations_path(&self, worker_id: &WorkerId) -> PathBuf {
|
||||
self.worker_dir(worker_id).join(OBSERVATIONS_FILE)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,6 +393,8 @@ pub(crate) struct PersistedRuntimeState {
|
||||
pub(crate) workers: BTreeMap<WorkerId, PersistedWorkerRecord>,
|
||||
pub(crate) config_bundles: BTreeMap<String, ConfigBundle>,
|
||||
pub(crate) events: Vec<RuntimeEvent>,
|
||||
#[cfg(feature = "ws-server")]
|
||||
pub(crate) observation_events: Vec<WorkerObservationEvent>,
|
||||
pub(crate) diagnostics: Vec<RuntimeDiagnostic>,
|
||||
}
|
||||
|
||||
@@ -415,8 +405,6 @@ pub(crate) struct PersistedWorkerRecord {
|
||||
pub(crate) status: WorkerStatus,
|
||||
pub(crate) request: CreateWorkerRequest,
|
||||
pub(crate) execution: WorkerExecutionStatus,
|
||||
pub(crate) transcript: Vec<TranscriptEntry>,
|
||||
pub(crate) next_transcript_sequence: u64,
|
||||
pub(crate) last_event_id: u64,
|
||||
}
|
||||
|
||||
@@ -504,6 +492,7 @@ impl RuntimeSnapshot {
|
||||
self,
|
||||
events: Vec<RuntimeEvent>,
|
||||
workers: BTreeMap<WorkerId, PersistedWorkerRecord>,
|
||||
#[cfg(feature = "ws-server")] observation_events: Vec<WorkerObservationEvent>,
|
||||
) -> PersistedRuntimeState {
|
||||
PersistedRuntimeState {
|
||||
runtime_id: self.runtime_id,
|
||||
@@ -516,6 +505,8 @@ impl RuntimeSnapshot {
|
||||
workers,
|
||||
config_bundles: self.config_bundles,
|
||||
events,
|
||||
#[cfg(feature = "ws-server")]
|
||||
observation_events,
|
||||
diagnostics: self.diagnostics,
|
||||
}
|
||||
}
|
||||
@@ -530,7 +521,6 @@ struct WorkerSnapshot {
|
||||
request: CreateWorkerRequest,
|
||||
#[serde(default = "WorkerExecutionStatus::unconnected")]
|
||||
execution: WorkerExecutionStatus,
|
||||
next_transcript_sequence: u64,
|
||||
last_event_id: u64,
|
||||
}
|
||||
|
||||
@@ -543,7 +533,6 @@ impl WorkerSnapshot {
|
||||
status: worker.status,
|
||||
request: worker.request.clone(),
|
||||
execution: worker.execution.clone(),
|
||||
next_transcript_sequence: worker.next_transcript_sequence,
|
||||
last_event_id: worker.last_event_id,
|
||||
}
|
||||
}
|
||||
@@ -582,15 +571,13 @@ impl WorkerSnapshot {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn into_persisted(self, transcript: Vec<TranscriptEntry>) -> PersistedWorkerRecord {
|
||||
fn into_persisted(self) -> PersistedWorkerRecord {
|
||||
PersistedWorkerRecord {
|
||||
worker_ref: self.worker_ref,
|
||||
worker_id: self.worker_id,
|
||||
status: self.status,
|
||||
request: self.request,
|
||||
execution: self.execution,
|
||||
transcript,
|
||||
next_transcript_sequence: self.next_transcript_sequence,
|
||||
last_event_id: self.last_event_id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ use crate::interaction::{WorkerInput, WorkerInteractionAck};
|
||||
use crate::management::{RuntimeLimits, RuntimeSummary};
|
||||
#[cfg(feature = "ws-server")]
|
||||
use crate::observation::WorkerObservationCursor;
|
||||
use crate::observation::{TranscriptProjection, TranscriptQuery};
|
||||
use axum::body::{Body, Bytes};
|
||||
use axum::extract::rejection::{JsonRejection, QueryRejection};
|
||||
#[cfg(feature = "ws-server")]
|
||||
@@ -151,11 +150,7 @@ pub fn runtime_http_router(runtime: Runtime, local_token: Option<String>) -> Rou
|
||||
.route("/v1/workers/{worker_id}", get(get_worker))
|
||||
.route("/v1/workers/{worker_id}/input", post(send_worker_input))
|
||||
.route("/v1/workers/{worker_id}/stop", post(stop_worker))
|
||||
.route("/v1/workers/{worker_id}/cancel", post(cancel_worker))
|
||||
.route(
|
||||
"/v1/workers/{worker_id}/transcript",
|
||||
get(get_worker_transcript),
|
||||
);
|
||||
.route("/v1/workers/{worker_id}/cancel", post(cancel_worker));
|
||||
|
||||
#[cfg(feature = "ws-server")]
|
||||
let router = router.route("/v1/workers/{worker_id}/events/ws", get(worker_events_ws));
|
||||
@@ -243,12 +238,6 @@ pub struct RuntimeHttpWorkerLifecycleResponse {
|
||||
pub ack: WorkerLifecycleAck,
|
||||
}
|
||||
|
||||
/// `GET /v1/workers/{worker_id}/transcript` response.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeHttpTranscriptResponse {
|
||||
pub transcript: TranscriptProjection,
|
||||
}
|
||||
|
||||
/// Typed REST error response.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeHttpErrorResponse {
|
||||
@@ -299,18 +288,6 @@ struct RuntimeWorkerEventsWsQuery {
|
||||
cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
struct RuntimeHttpTranscriptQuery {
|
||||
#[serde(default)]
|
||||
start: usize,
|
||||
#[serde(default = "default_transcript_limit")]
|
||||
limit: usize,
|
||||
}
|
||||
|
||||
fn default_transcript_limit() -> usize {
|
||||
256
|
||||
}
|
||||
|
||||
#[cfg(feature = "ws-server")]
|
||||
impl RuntimeWorkerEventWsFrame {
|
||||
fn event(
|
||||
@@ -714,20 +691,6 @@ async fn cancel_worker(
|
||||
Ok(Json(RuntimeHttpWorkerLifecycleResponse { ack }))
|
||||
}
|
||||
|
||||
async fn get_worker_transcript(
|
||||
State(state): State<RuntimeHttpState>,
|
||||
Path(worker_id): Path<String>,
|
||||
query: Result<Query<RuntimeHttpTranscriptQuery>, QueryRejection>,
|
||||
) -> RestResult<RuntimeHttpTranscriptResponse> {
|
||||
let worker_ref = worker_ref_for(&state.runtime, worker_id)?;
|
||||
let Query(query) = query.map_err(RuntimeHttpRestError::query_rejection)?;
|
||||
let transcript = state
|
||||
.runtime
|
||||
.transcript_projection(&worker_ref, TranscriptQuery::new(query.start, query.limit))
|
||||
.map_err(RuntimeHttpRestError::runtime)?;
|
||||
Ok(Json(RuntimeHttpTranscriptResponse { transcript }))
|
||||
}
|
||||
|
||||
fn worker_ref_for(runtime: &Runtime, worker_id: String) -> Result<WorkerRef, RuntimeHttpRestError> {
|
||||
let worker_id = WorkerId::new(worker_id).ok_or_else(|| {
|
||||
RuntimeHttpRestError::new(
|
||||
@@ -1067,8 +1030,7 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let input_ack: RuntimeHttpWorkerInputResponse = read_json(response).await;
|
||||
assert_eq!(input_ack.ack.transcript_sequence, 1);
|
||||
let _input_ack: RuntimeHttpWorkerInputResponse = read_json(response).await;
|
||||
|
||||
let response = empty_request(
|
||||
app.clone(),
|
||||
@@ -1077,21 +1039,15 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let detail: RuntimeHttpWorkerResponse = read_json(response).await;
|
||||
assert_eq!(detail.worker.transcript_len, 1);
|
||||
let _detail: RuntimeHttpWorkerResponse = read_json(response).await;
|
||||
|
||||
let response = empty_request(
|
||||
app.clone(),
|
||||
Method::GET,
|
||||
&format!(
|
||||
"/v1/workers/{}/transcript?start=0&limit=1",
|
||||
created.worker.worker_id
|
||||
),
|
||||
&format!("/v1/workers/{}/transcript", created.worker.worker_id),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let transcript: RuntimeHttpTranscriptResponse = read_json(response).await;
|
||||
assert_eq!(transcript.transcript.items[0].content, "hello from backend");
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let response = empty_request(
|
||||
app.clone(),
|
||||
@@ -1117,7 +1073,6 @@ mod tests {
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let workers: RuntimeHttpWorkersResponse = read_json(response).await;
|
||||
assert_eq!(workers.workers.len(), 1);
|
||||
assert_eq!(workers.workers[0].transcript_len, 1);
|
||||
|
||||
let response = empty_request(app, Method::GET, "/v1/runtime").await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
@@ -10,8 +10,7 @@ pub enum WorkerInputKind {
|
||||
System,
|
||||
}
|
||||
|
||||
/// Worker input request. v0 stores the input in an in-memory transcript and
|
||||
/// does not execute providers/tools.
|
||||
/// Worker input request accepted by a Runtime Worker.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct WorkerInput {
|
||||
pub kind: WorkerInputKind,
|
||||
@@ -34,11 +33,10 @@ impl WorkerInput {
|
||||
}
|
||||
}
|
||||
|
||||
/// Acknowledgement returned after input is accepted into the in-memory Worker.
|
||||
/// Acknowledgement returned after input is accepted into the Worker.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct WorkerInteractionAck {
|
||||
pub worker_ref: WorkerRef,
|
||||
pub status: WorkerStatus,
|
||||
pub transcript_sequence: u64,
|
||||
pub event_id: u64,
|
||||
}
|
||||
|
||||
@@ -238,10 +238,6 @@ where
|
||||
}
|
||||
config.http.local_token = Some(value);
|
||||
}
|
||||
"--max-transcript-projection-items" => {
|
||||
config.http.limits.max_transcript_projection_items =
|
||||
parse_usize_flag(&flag, take_value(&flag, inline_value, &mut args)?)?;
|
||||
}
|
||||
"--max-event-batch-items" => {
|
||||
config.http.limits.max_event_batch_items =
|
||||
parse_usize_flag(&flag, take_value(&flag, inline_value, &mut args)?)?;
|
||||
@@ -436,7 +432,6 @@ Options:\n\
|
||||
--fs-root <PATH> Runtime catalog filesystem store root\n\
|
||||
--local-token <TOKEN> Minimal local bearer token placeholder\n\
|
||||
--local-token-env <ENV> Read local bearer token placeholder from env\n\
|
||||
--max-transcript-projection-items <N> Override transcript projection limit\n\
|
||||
--max-event-batch-items <N> Override event batch limit\n\
|
||||
-h, --help Show this help"
|
||||
}
|
||||
|
||||
@@ -18,17 +18,15 @@ pub enum RuntimeStatus {
|
||||
Stopped,
|
||||
}
|
||||
|
||||
/// Guardrails for bounded observation/projection APIs.
|
||||
/// Guardrails for bounded Runtime APIs.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeLimits {
|
||||
pub max_transcript_projection_items: usize,
|
||||
pub max_event_batch_items: usize,
|
||||
}
|
||||
|
||||
impl Default for RuntimeLimits {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_transcript_projection_items: 256,
|
||||
max_event_batch_items: 256,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,6 @@
|
||||
use crate::identity::{RuntimeId, WorkerRef};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Transcript role used by bounded projection.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TranscriptRole {
|
||||
User,
|
||||
Assistant,
|
||||
System,
|
||||
}
|
||||
|
||||
/// One projected transcript item.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TranscriptEntry {
|
||||
pub sequence: u64,
|
||||
pub worker_ref: WorkerRef,
|
||||
pub role: TranscriptRole,
|
||||
pub content: String,
|
||||
pub event_id: u64,
|
||||
}
|
||||
|
||||
/// Bounded transcript query.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TranscriptQuery {
|
||||
pub start: usize,
|
||||
pub limit: usize,
|
||||
}
|
||||
|
||||
impl TranscriptQuery {
|
||||
pub fn new(start: usize, limit: usize) -> Self {
|
||||
Self { start, limit }
|
||||
}
|
||||
}
|
||||
|
||||
/// Bounded transcript projection response.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TranscriptProjection {
|
||||
pub worker_ref: WorkerRef,
|
||||
pub start: usize,
|
||||
pub limit: usize,
|
||||
pub total_items: usize,
|
||||
pub items: Vec<TranscriptEntry>,
|
||||
pub next_start: Option<usize>,
|
||||
}
|
||||
|
||||
/// Event cursor. `next_event_id` is the first event id that should be returned
|
||||
/// by the next poll.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
@@ -28,7 +28,7 @@ use crate::management::{
|
||||
};
|
||||
use crate::observation::{
|
||||
EventCursor, EventSubscription, EventSubscriptionMode, RuntimeEvent, RuntimeEventBatch,
|
||||
RuntimeEventKind, TranscriptEntry, TranscriptProjection, TranscriptQuery, TranscriptRole,
|
||||
RuntimeEventKind,
|
||||
};
|
||||
#[cfg(feature = "ws-server")]
|
||||
use crate::observation::{WorkerObservationCursor, WorkerObservationEvent};
|
||||
@@ -333,19 +333,6 @@ impl Runtime {
|
||||
format!("worker {worker_id} created"),
|
||||
);
|
||||
|
||||
let mut transcript = Vec::new();
|
||||
let mut next_transcript_sequence = 1;
|
||||
if let Some(input) = request.initial_input.clone() {
|
||||
transcript.push(TranscriptEntry {
|
||||
sequence: next_transcript_sequence,
|
||||
worker_ref: worker_ref.clone(),
|
||||
role: TranscriptRole::User,
|
||||
content: input.content,
|
||||
event_id,
|
||||
});
|
||||
next_transcript_sequence += 1;
|
||||
}
|
||||
|
||||
let record = WorkerRecord {
|
||||
worker_ref: worker_ref.clone(),
|
||||
worker_id: worker_id.clone(),
|
||||
@@ -353,8 +340,6 @@ impl Runtime {
|
||||
request: request.clone(),
|
||||
execution: WorkerExecutionStatus::unconnected(),
|
||||
execution_handle: None,
|
||||
transcript,
|
||||
next_transcript_sequence,
|
||||
last_event_id: event_id,
|
||||
};
|
||||
state.workers.insert(worker_id, record);
|
||||
@@ -392,7 +377,7 @@ impl Runtime {
|
||||
let state = self.lock()?;
|
||||
state.worker(&worker_ref)?.request.initial_input.clone()
|
||||
} {
|
||||
let dispatch_result = backend.dispatch_input(&handle, initial_input);
|
||||
let dispatch_result = backend.dispatch_input(&handle, initial_input.clone());
|
||||
if !dispatch_result.is_accepted() {
|
||||
let _ = backend.stop_worker(&handle);
|
||||
self.rollback_failed_create(&worker_ref)?;
|
||||
@@ -404,7 +389,7 @@ impl Runtime {
|
||||
result: dispatch_result,
|
||||
});
|
||||
}
|
||||
self.commit_created_worker(
|
||||
let detail = self.commit_created_worker(
|
||||
&worker_ref,
|
||||
handle,
|
||||
WorkerExecutionRunState::Busy,
|
||||
@@ -413,7 +398,9 @@ impl Runtime {
|
||||
WorkerExecutionOperation::Input,
|
||||
WorkerExecutionRunState::Busy,
|
||||
),
|
||||
)
|
||||
)?;
|
||||
self.record_input_observation(&worker_ref, initial_input)?;
|
||||
Ok(detail)
|
||||
} else {
|
||||
self.commit_created_worker(
|
||||
&worker_ref,
|
||||
@@ -442,7 +429,7 @@ impl Runtime {
|
||||
Ok(worker.detail(&state.runtime_id))
|
||||
}
|
||||
|
||||
/// Accept input into a Worker transcript.
|
||||
/// Accept input into a Worker.
|
||||
pub fn send_input(
|
||||
&self,
|
||||
worker_ref: &WorkerRef,
|
||||
@@ -502,14 +489,6 @@ impl Runtime {
|
||||
"worker input accepted",
|
||||
);
|
||||
let worker = state.worker_mut(worker_ref)?;
|
||||
|
||||
let input_content = input.content;
|
||||
let role = match input.kind {
|
||||
WorkerInputKind::User => TranscriptRole::User,
|
||||
WorkerInputKind::System => TranscriptRole::System,
|
||||
};
|
||||
let transcript_sequence = worker.next_transcript_sequence;
|
||||
worker.next_transcript_sequence += 1;
|
||||
worker.last_event_id = event_id;
|
||||
worker.execution = WorkerExecutionStatus {
|
||||
backend: WorkerExecutionBackendKind::Connected,
|
||||
@@ -518,44 +497,24 @@ impl Runtime {
|
||||
working_directory: worker.execution.working_directory.clone(),
|
||||
last_result: Some(dispatch_result),
|
||||
};
|
||||
worker.transcript.push(TranscriptEntry {
|
||||
sequence: transcript_sequence,
|
||||
worker_ref: worker_ref.clone(),
|
||||
role,
|
||||
content: input_content.clone(),
|
||||
event_id,
|
||||
});
|
||||
|
||||
let status = worker.status;
|
||||
#[cfg(feature = "ws-server")]
|
||||
{
|
||||
let payload = match role {
|
||||
TranscriptRole::User => protocol::Event::UserMessage {
|
||||
segments: vec![protocol::Segment::Text {
|
||||
content: input_content.clone(),
|
||||
}],
|
||||
},
|
||||
TranscriptRole::Assistant => protocol::Event::TextDone {
|
||||
text: input_content.clone(),
|
||||
},
|
||||
TranscriptRole::System => protocol::Event::SystemItem {
|
||||
item: serde_json::json!({
|
||||
"kind": "embedded_worker_system_input",
|
||||
"content": input_content.clone(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
state.push_worker_observation_event(worker_ref.clone(), payload);
|
||||
}
|
||||
let observation = {
|
||||
let payload = input_protocol_event(&input);
|
||||
Some(state.push_worker_observation_event(worker_ref.clone(), payload))
|
||||
};
|
||||
state.persist_runtime_snapshot()?;
|
||||
state.persist_worker(&worker_ref.worker_id)?;
|
||||
state.persist_event_by_id(event_id)?;
|
||||
state.persist_transcript_entry(&worker_ref.worker_id, transcript_sequence)?;
|
||||
#[cfg(feature = "ws-server")]
|
||||
if let Some(observation) = observation.as_ref() {
|
||||
state.persist_worker_observation_event(observation)?;
|
||||
}
|
||||
|
||||
Ok(WorkerInteractionAck {
|
||||
worker_ref: worker_ref.clone(),
|
||||
status,
|
||||
transcript_sequence,
|
||||
event_id,
|
||||
})
|
||||
}
|
||||
@@ -583,10 +542,6 @@ impl Runtime {
|
||||
};
|
||||
state.persist_runtime_snapshot()?;
|
||||
state.persist_worker(&worker_ref.worker_id)?;
|
||||
let worker = state.worker(worker_ref)?;
|
||||
for entry in &worker.transcript {
|
||||
state.persist_transcript_entry(&worker_ref.worker_id, entry.sequence)?;
|
||||
}
|
||||
state.persist_event_by_id(detail.last_event_id)?;
|
||||
Ok(detail)
|
||||
}
|
||||
@@ -691,38 +646,6 @@ impl Runtime {
|
||||
)
|
||||
}
|
||||
|
||||
/// Bounded transcript projection for a Worker.
|
||||
pub fn transcript_projection(
|
||||
&self,
|
||||
worker_ref: &WorkerRef,
|
||||
query: TranscriptQuery,
|
||||
) -> Result<TranscriptProjection, RuntimeError> {
|
||||
let state = self.lock()?;
|
||||
if query.limit > state.limits.max_transcript_projection_items {
|
||||
return Err(RuntimeError::LimitTooLarge {
|
||||
requested: query.limit,
|
||||
max: state.limits.max_transcript_projection_items,
|
||||
});
|
||||
}
|
||||
let worker = state.worker(worker_ref)?;
|
||||
let total_items = worker.transcript.len();
|
||||
let end = query.start.saturating_add(query.limit).min(total_items);
|
||||
let items = if query.start >= total_items {
|
||||
Vec::new()
|
||||
} else {
|
||||
worker.transcript[query.start..end].to_vec()
|
||||
};
|
||||
let next_start = (end < total_items).then_some(end);
|
||||
Ok(TranscriptProjection {
|
||||
worker_ref: worker_ref.clone(),
|
||||
start: query.start,
|
||||
limit: query.limit,
|
||||
total_items,
|
||||
items,
|
||||
next_start,
|
||||
})
|
||||
}
|
||||
|
||||
/// Cursor pointing to the beginning of Runtime events.
|
||||
pub fn event_cursor_from_start(&self) -> Result<EventCursor, RuntimeError> {
|
||||
let state = self.lock()?;
|
||||
@@ -880,16 +803,13 @@ impl Runtime {
|
||||
) -> Result<WorkerObservationEvent, RuntimeError> {
|
||||
let mut state = self.lock()?;
|
||||
state.ensure_worker_ref(worker_ref)?;
|
||||
let transcript_sequence = state.project_protocol_event_to_transcript(worker_ref, &payload);
|
||||
let execution_state_changed =
|
||||
state.project_protocol_event_to_execution(worker_ref, &payload);
|
||||
let event = state.push_worker_observation_event(worker_ref.clone(), payload);
|
||||
if transcript_sequence.is_some() || execution_state_changed {
|
||||
if execution_state_changed {
|
||||
state.persist_worker(&worker_ref.worker_id)?;
|
||||
}
|
||||
if let Some(sequence) = transcript_sequence {
|
||||
state.persist_transcript_entry(&worker_ref.worker_id, sequence)?;
|
||||
}
|
||||
state.persist_worker_observation_event(&event)?;
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
@@ -898,6 +818,29 @@ impl Runtime {
|
||||
Ok(self.lock()?.diagnostics.clone())
|
||||
}
|
||||
|
||||
#[cfg(feature = "ws-server")]
|
||||
fn record_input_observation(
|
||||
&self,
|
||||
worker_ref: &WorkerRef,
|
||||
input: WorkerInput,
|
||||
) -> Result<(), RuntimeError> {
|
||||
let mut state = self.lock()?;
|
||||
state.ensure_worker_ref(worker_ref)?;
|
||||
let event =
|
||||
state.push_worker_observation_event(worker_ref.clone(), input_protocol_event(&input));
|
||||
state.persist_worker_observation_event(&event)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ws-server"))]
|
||||
fn record_input_observation(
|
||||
&self,
|
||||
_worker_ref: &WorkerRef,
|
||||
_input: WorkerInput,
|
||||
) -> Result<(), RuntimeError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn transition_worker(
|
||||
&self,
|
||||
worker_ref: &WorkerRef,
|
||||
@@ -1106,13 +1049,22 @@ impl RuntimeState {
|
||||
request: worker.request,
|
||||
execution,
|
||||
execution_handle: None,
|
||||
transcript: worker.transcript,
|
||||
next_transcript_sequence: worker.next_transcript_sequence,
|
||||
last_event_id: worker.last_event_id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ws-server")]
|
||||
let next_observation_sequence = persisted
|
||||
.observation_events
|
||||
.iter()
|
||||
.map(|event| event.sequence)
|
||||
.max()
|
||||
.map(|sequence| sequence.saturating_add(1))
|
||||
.unwrap_or(1);
|
||||
#[cfg(feature = "ws-server")]
|
||||
let observation_events = persisted.observation_events.into_iter().collect();
|
||||
|
||||
Ok(Self {
|
||||
runtime_id: persisted.runtime_id,
|
||||
display_name: persisted.display_name,
|
||||
@@ -1129,9 +1081,9 @@ impl RuntimeState {
|
||||
events: persisted.events,
|
||||
diagnostics,
|
||||
#[cfg(feature = "ws-server")]
|
||||
next_observation_sequence: 1,
|
||||
next_observation_sequence,
|
||||
#[cfg(feature = "ws-server")]
|
||||
observation_events: VecDeque::new(),
|
||||
observation_events,
|
||||
#[cfg(feature = "ws-server")]
|
||||
observation_tx: broadcast::channel(256).0,
|
||||
})
|
||||
@@ -1154,6 +1106,8 @@ impl RuntimeState {
|
||||
.collect(),
|
||||
config_bundles: self.config_bundles.clone(),
|
||||
events: self.events.clone(),
|
||||
#[cfg(feature = "ws-server")]
|
||||
observation_events: Vec::new(),
|
||||
diagnostics: self.diagnostics.clone(),
|
||||
}
|
||||
}
|
||||
@@ -1206,36 +1160,25 @@ impl RuntimeState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "fs-store")]
|
||||
fn persist_transcript_entry(
|
||||
#[cfg(all(feature = "fs-store", feature = "ws-server"))]
|
||||
fn persist_worker_observation_event(
|
||||
&self,
|
||||
worker_id: &WorkerId,
|
||||
sequence: u64,
|
||||
event: &WorkerObservationEvent,
|
||||
) -> Result<(), RuntimeError> {
|
||||
if let Some(store) = self.fs_store() {
|
||||
let worker =
|
||||
self.workers
|
||||
.get(worker_id)
|
||||
.ok_or_else(|| RuntimeError::WorkerNotFound {
|
||||
runtime_id: self.runtime_id.clone(),
|
||||
worker_id: worker_id.clone(),
|
||||
})?;
|
||||
let entry = worker
|
||||
.transcript
|
||||
.iter()
|
||||
.find(|entry| entry.sequence == sequence)
|
||||
.ok_or_else(|| RuntimeError::StoreCorrupt {
|
||||
operation: "persist transcript",
|
||||
path: store.runtime_dir().to_path_buf(),
|
||||
message: format!(
|
||||
"transcript sequence {sequence} is missing from worker {worker_id}"
|
||||
),
|
||||
})?;
|
||||
store.append_transcript_entry(entry)?;
|
||||
store.append_worker_observation_event(event)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(not(feature = "fs-store"), feature = "ws-server"))]
|
||||
fn persist_worker_observation_event(
|
||||
&self,
|
||||
_event: &WorkerObservationEvent,
|
||||
) -> Result<(), RuntimeError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "fs-store")]
|
||||
fn persist_workers(&self) -> Result<(), RuntimeError> {
|
||||
if self.fs_store().is_some() {
|
||||
@@ -1261,15 +1204,6 @@ impl RuntimeState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "fs-store"))]
|
||||
fn persist_transcript_entry(
|
||||
&self,
|
||||
_worker_id: &WorkerId,
|
||||
_sequence: u64,
|
||||
) -> Result<(), RuntimeError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "fs-store"))]
|
||||
fn persist_workers(&self) -> Result<(), RuntimeError> {
|
||||
Ok(())
|
||||
@@ -1421,66 +1355,6 @@ impl RuntimeState {
|
||||
event
|
||||
}
|
||||
|
||||
#[cfg(feature = "ws-server")]
|
||||
fn append_worker_transcript_entry(
|
||||
&mut self,
|
||||
worker_ref: &WorkerRef,
|
||||
role: TranscriptRole,
|
||||
content: impl Into<String>,
|
||||
) -> Option<u64> {
|
||||
let content = content.into();
|
||||
if content.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
let event_id = self.last_event_id();
|
||||
let worker = self.workers.get_mut(&worker_ref.worker_id)?;
|
||||
let sequence = worker.next_transcript_sequence;
|
||||
worker.next_transcript_sequence += 1;
|
||||
worker.transcript.push(TranscriptEntry {
|
||||
sequence,
|
||||
worker_ref: worker_ref.clone(),
|
||||
role,
|
||||
content,
|
||||
event_id,
|
||||
});
|
||||
Some(sequence)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ws-server")]
|
||||
fn project_protocol_event_to_transcript(
|
||||
&mut self,
|
||||
worker_ref: &WorkerRef,
|
||||
event: &protocol::Event,
|
||||
) -> Option<u64> {
|
||||
match event {
|
||||
protocol::Event::TextDone { text, .. } => self.append_worker_transcript_entry(
|
||||
worker_ref,
|
||||
TranscriptRole::Assistant,
|
||||
text.clone(),
|
||||
),
|
||||
protocol::Event::Error { message, .. } => self.append_worker_transcript_entry(
|
||||
worker_ref,
|
||||
TranscriptRole::System,
|
||||
format!("error: {message}"),
|
||||
),
|
||||
protocol::Event::ToolResult {
|
||||
id,
|
||||
summary,
|
||||
is_error,
|
||||
..
|
||||
} => self.append_worker_transcript_entry(
|
||||
worker_ref,
|
||||
TranscriptRole::System,
|
||||
format!(
|
||||
"tool result {id}: {}{}",
|
||||
if *is_error { "error: " } else { "" },
|
||||
summary
|
||||
),
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ws-server")]
|
||||
fn project_protocol_event_to_execution(
|
||||
&mut self,
|
||||
@@ -1535,8 +1409,6 @@ struct WorkerRecord {
|
||||
request: CreateWorkerRequest,
|
||||
execution: WorkerExecutionStatus,
|
||||
execution_handle: Option<WorkerExecutionHandle>,
|
||||
transcript: Vec<TranscriptEntry>,
|
||||
next_transcript_sequence: u64,
|
||||
last_event_id: u64,
|
||||
}
|
||||
|
||||
@@ -1551,7 +1423,6 @@ impl WorkerRecord {
|
||||
profile: self.request.profile.clone(),
|
||||
profile_source: self.request.profile_source.reference(),
|
||||
config_bundle: self.request.config_bundle.clone(),
|
||||
transcript_len: self.transcript.len(),
|
||||
last_event_id: self.last_event_id,
|
||||
}
|
||||
}
|
||||
@@ -1566,7 +1437,6 @@ impl WorkerRecord {
|
||||
profile: self.request.profile.clone(),
|
||||
profile_source: self.request.profile_source.reference(),
|
||||
config_bundle: self.request.config_bundle.clone(),
|
||||
transcript_len: self.transcript.len(),
|
||||
last_event_id: self.last_event_id,
|
||||
}
|
||||
}
|
||||
@@ -1579,8 +1449,6 @@ impl WorkerRecord {
|
||||
status: self.status,
|
||||
request: self.request.clone(),
|
||||
execution: self.execution.clone(),
|
||||
transcript: self.transcript.clone(),
|
||||
next_transcript_sequence: self.next_transcript_sequence,
|
||||
last_event_id: self.last_event_id,
|
||||
}
|
||||
}
|
||||
@@ -1630,6 +1498,23 @@ fn validate_worker_input(input: &WorkerInput) -> Result<(), RuntimeError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "ws-server")]
|
||||
fn input_protocol_event(input: &WorkerInput) -> protocol::Event {
|
||||
match input.kind {
|
||||
WorkerInputKind::User => protocol::Event::UserMessage {
|
||||
segments: vec![protocol::Segment::Text {
|
||||
content: input.content.clone(),
|
||||
}],
|
||||
},
|
||||
WorkerInputKind::System => protocol::Event::SystemItem {
|
||||
item: serde_json::json!({
|
||||
"kind": "embedded_worker_system_input",
|
||||
"content": input.content.clone(),
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1965,12 +1850,12 @@ mod tests {
|
||||
));
|
||||
let refreshed = runtime.worker_detail(&detail.worker_ref).unwrap();
|
||||
assert_eq!(refreshed.execution.run_state, WorkerExecutionRunState::Busy);
|
||||
assert_eq!(
|
||||
#[cfg(feature = "ws-server")]
|
||||
assert!(
|
||||
runtime
|
||||
.transcript_projection(&detail.worker_ref, TranscriptQuery::new(0, 1))
|
||||
.read_worker_observation_events(&detail.worker_ref, WorkerObservationCursor::zero())
|
||||
.unwrap()
|
||||
.total_items,
|
||||
0
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2049,12 +1934,12 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ws-server")]
|
||||
#[test]
|
||||
fn send_input_and_project_bounded_transcript() {
|
||||
fn send_input_records_protocol_observations() {
|
||||
let runtime = Runtime::with_execution_backend(
|
||||
RuntimeOptions {
|
||||
limits: RuntimeLimits {
|
||||
max_transcript_projection_items: 2,
|
||||
max_event_batch_items: 16,
|
||||
},
|
||||
..RuntimeOptions::default()
|
||||
@@ -2068,27 +1953,23 @@ mod tests {
|
||||
let first = runtime
|
||||
.send_input(&detail.worker_ref, WorkerInput::user("hello"))
|
||||
.unwrap();
|
||||
assert_eq!(first.transcript_sequence, 1);
|
||||
runtime
|
||||
.send_input(&detail.worker_ref, WorkerInput::system("note"))
|
||||
.unwrap();
|
||||
runtime
|
||||
.send_input(&detail.worker_ref, WorkerInput::user("again"))
|
||||
.unwrap();
|
||||
|
||||
let projection = runtime
|
||||
.transcript_projection(&detail.worker_ref, TranscriptQuery::new(0, 2))
|
||||
let observations = runtime
|
||||
.read_worker_observation_events(&detail.worker_ref, WorkerObservationCursor::zero())
|
||||
.unwrap();
|
||||
assert_eq!(projection.total_items, 3);
|
||||
assert_eq!(projection.items.len(), 2);
|
||||
assert_eq!(projection.items[0].content, "hello");
|
||||
assert_eq!(projection.items[1].role, TranscriptRole::System);
|
||||
assert_eq!(projection.next_start, Some(2));
|
||||
|
||||
let err = runtime
|
||||
.transcript_projection(&detail.worker_ref, TranscriptQuery::new(0, 3))
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, RuntimeError::LimitTooLarge { .. }));
|
||||
assert_eq!(first.event_id, 3);
|
||||
assert_eq!(observations.len(), 2);
|
||||
assert!(matches!(
|
||||
observations[0].payload,
|
||||
protocol::Event::UserMessage { .. }
|
||||
));
|
||||
assert!(matches!(
|
||||
observations[1].payload,
|
||||
protocol::Event::SystemItem { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2253,7 +2134,7 @@ mod tests {
|
||||
|
||||
#[cfg(feature = "fs-store")]
|
||||
#[test]
|
||||
fn fs_store_restores_workers_events_and_transcripts() {
|
||||
fn fs_store_restores_workers_events_and_protocol_observations() {
|
||||
let root = fs_store_root("restore");
|
||||
let runtime_id = RuntimeId::new("runtime-fs-authority").unwrap();
|
||||
let runtime = Runtime::with_fs_store_and_execution_backend(
|
||||
@@ -2262,7 +2143,6 @@ mod tests {
|
||||
runtime_id: Some(runtime_id.clone()),
|
||||
display_name: Some("filesystem runtime".to_string()),
|
||||
limits: RuntimeLimits {
|
||||
max_transcript_projection_items: 2,
|
||||
max_event_batch_items: 2,
|
||||
},
|
||||
},
|
||||
@@ -2319,14 +2199,21 @@ mod tests {
|
||||
&& diagnostic.worker_ref.as_ref() == Some(&worker.worker_ref)
|
||||
)
|
||||
);
|
||||
assert_eq!(restored_worker.transcript_len, 2);
|
||||
|
||||
let projection = restored
|
||||
.transcript_projection(&worker.worker_ref, TranscriptQuery::new(0, 1))
|
||||
.unwrap();
|
||||
assert_eq!(projection.total_items, 2);
|
||||
assert_eq!(projection.items[0].content, "first");
|
||||
assert_eq!(projection.next_start, Some(1));
|
||||
#[cfg(feature = "ws-server")]
|
||||
{
|
||||
let observations = restored
|
||||
.read_worker_observation_events(&worker.worker_ref, WorkerObservationCursor::zero())
|
||||
.unwrap();
|
||||
assert_eq!(observations.len(), 2);
|
||||
assert!(matches!(
|
||||
observations[0].payload,
|
||||
protocol::Event::UserMessage { .. }
|
||||
));
|
||||
assert!(matches!(
|
||||
observations[1].payload,
|
||||
protocol::Event::SystemItem { .. }
|
||||
));
|
||||
}
|
||||
|
||||
let cursor = restored.event_cursor_from_start().unwrap();
|
||||
let batch = restored.read_events(&cursor, 2).unwrap();
|
||||
@@ -2337,11 +2224,6 @@ mod tests {
|
||||
|
||||
let direct_events = store.read_events(&cursor, 2, 2).unwrap();
|
||||
assert_eq!(direct_events.events, batch.events);
|
||||
let direct_transcript = store
|
||||
.read_transcript(&worker.worker_ref, TranscriptQuery::new(1, 1), 2)
|
||||
.unwrap();
|
||||
assert_eq!(direct_transcript.items[0].content, "second");
|
||||
|
||||
#[cfg(feature = "ws-server")]
|
||||
{
|
||||
let observation = restored
|
||||
@@ -2352,12 +2234,12 @@ mod tests {
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(observation.sequence, 1);
|
||||
assert_eq!(observation.sequence, 3);
|
||||
let observations = restored
|
||||
.read_worker_observation_events(&worker.worker_ref, WorkerObservationCursor::zero())
|
||||
.unwrap();
|
||||
assert_eq!(observations.len(), 1);
|
||||
assert_eq!(observations[0].cursor, observation.cursor);
|
||||
assert_eq!(observations.len(), 3);
|
||||
assert_eq!(observations[2].cursor, observation.cursor);
|
||||
}
|
||||
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
|
||||
@@ -820,7 +820,7 @@ mod tests {
|
||||
use crate::execution::WorkerExecutionContext;
|
||||
use crate::identity::RuntimeId;
|
||||
use crate::management::RuntimeOptions;
|
||||
use crate::observation::{TranscriptQuery, TranscriptRole};
|
||||
use crate::observation::WorkerObservationCursor;
|
||||
use crate::working_directory::LocalGitWorktreeMaterializer;
|
||||
use async_trait::async_trait;
|
||||
use futures::Stream;
|
||||
@@ -1128,27 +1128,27 @@ mod tests {
|
||||
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(5);
|
||||
loop {
|
||||
let projection = runtime
|
||||
.transcript_projection(&detail.worker_ref, TranscriptQuery::new(0, 10))
|
||||
let observations = runtime
|
||||
.read_worker_observation_events(&detail.worker_ref, WorkerObservationCursor::zero())
|
||||
.unwrap();
|
||||
if projection.items.iter().any(|item| {
|
||||
item.role == TranscriptRole::Assistant && item.content == "hello from worker"
|
||||
if observations.iter().any(|event| {
|
||||
matches!(
|
||||
&event.payload,
|
||||
protocol::Event::TextDone { text } if text == "hello from worker"
|
||||
)
|
||||
}) {
|
||||
break;
|
||||
}
|
||||
assert!(
|
||||
std::time::Instant::now() < deadline,
|
||||
"timed out waiting for assistant transcript projection"
|
||||
"timed out waiting for assistant protocol observation"
|
||||
);
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
|
||||
assert_eq!(client.captured.lock().unwrap().len(), 1);
|
||||
let observations = runtime
|
||||
.read_worker_observation_events(
|
||||
&detail.worker_ref,
|
||||
crate::observation::WorkerObservationCursor::zero(),
|
||||
)
|
||||
.read_worker_observation_events(&detail.worker_ref, WorkerObservationCursor::zero())
|
||||
.unwrap();
|
||||
assert!(
|
||||
observations
|
||||
|
||||
Reference in New Issue
Block a user