feat: expose worker model and context metadata
This commit is contained in:
@@ -766,6 +766,10 @@ pub struct InternalWorkerSnapshot {
|
||||
pub worker: InternalWorkerRef,
|
||||
pub revision: u64,
|
||||
pub session: SessionSnapshot,
|
||||
/// Public execution metadata for this child. Older snapshots and service
|
||||
/// workers may omit it; clients must never substitute the parent's values.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub greeting: Option<Greeting>,
|
||||
#[serde(default)]
|
||||
pub status: WorkerStatus,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -1667,11 +1671,45 @@ impl InFlightToolCallState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Worker self-description rendered by the TUI when a session starts empty.
|
||||
/// Public reasoning configuration resolved for the model client.
|
||||
///
|
||||
/// Built once in the Worker controller from the resolved manifest and
|
||||
/// transmitted alongside `Event::Snapshot` so clients don't need
|
||||
/// their own view of the manifest.
|
||||
/// This deliberately exposes only the effective provider control, not the
|
||||
/// Worker manifest or provider credentials. The tagged shape prevents token
|
||||
/// budgets from being presented as effort labels.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum ReasoningConfig {
|
||||
Effort { effort: String },
|
||||
BudgetTokens { budget_tokens: i32 },
|
||||
}
|
||||
|
||||
/// Provenance of the current-context token count in a [`Greeting`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ContextTokenSource {
|
||||
/// Provider-reported input occupancy from the most recent LLM request.
|
||||
Measured,
|
||||
/// Current history projected from the Worker's token accounting records.
|
||||
Estimated,
|
||||
}
|
||||
|
||||
/// Current prompt-context occupancy, distinct from cumulative run traffic.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub struct ContextUsage {
|
||||
pub tokens: u64,
|
||||
pub source: ContextTokenSource,
|
||||
}
|
||||
|
||||
/// Worker self-description rendered by clients when a session starts empty.
|
||||
///
|
||||
/// Snapshot metadata contains only resolved public execution information so
|
||||
/// clients do not need their own copy of the manifest.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
@@ -1680,14 +1718,22 @@ pub struct Greeting {
|
||||
pub cwd: String,
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
/// Effective reasoning control. Absent means unspecified or unavailable.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub reasoning: Option<ReasoningConfig>,
|
||||
pub scope_summary: String,
|
||||
pub tools: Vec<String>,
|
||||
/// Model context window in tokens. Always filled by the Worker greeting.
|
||||
/// Resolved effective model context window in tokens. Legacy producers use
|
||||
/// zero when this is unavailable; clients must treat that as unknown.
|
||||
#[serde(default)]
|
||||
pub context_window: u64,
|
||||
/// Estimated current session context tokens at connect time.
|
||||
/// Legacy numeric mirror retained for mixed-version clients.
|
||||
#[serde(default)]
|
||||
pub context_tokens: u64,
|
||||
/// Typed current-context value. Older snapshots omit it; clients may fall
|
||||
/// back to `context_tokens` only when `context_window` is known.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub context_usage: Option<ContextUsage>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -2354,6 +2400,11 @@ mod tests {
|
||||
tools: vec!["Read".into()],
|
||||
context_window: 200_000,
|
||||
context_tokens: 42_000,
|
||||
reasoning: Some(ReasoningConfig::BudgetTokens { budget_tokens: -1 }),
|
||||
context_usage: Some(ContextUsage {
|
||||
tokens: 42_000,
|
||||
source: ContextTokenSource::Measured,
|
||||
}),
|
||||
},
|
||||
state: WorkerStatus::Paused.into(),
|
||||
in_flight: InFlightSnapshot::default(),
|
||||
@@ -2372,6 +2423,19 @@ mod tests {
|
||||
assert_eq!(parsed["data"]["greeting"]["tools"][0], "Read");
|
||||
assert_eq!(parsed["data"]["greeting"]["context_window"], 200_000);
|
||||
assert_eq!(parsed["data"]["greeting"]["context_tokens"], 42_000);
|
||||
assert_eq!(
|
||||
parsed["data"]["greeting"]["reasoning"]["kind"],
|
||||
"budget_tokens"
|
||||
);
|
||||
assert_eq!(parsed["data"]["greeting"]["reasoning"]["budget_tokens"], -1);
|
||||
assert_eq!(
|
||||
parsed["data"]["greeting"]["context_usage"]["tokens"],
|
||||
42_000
|
||||
);
|
||||
assert_eq!(
|
||||
parsed["data"]["greeting"]["context_usage"]["source"],
|
||||
"measured"
|
||||
);
|
||||
assert_eq!(parsed["data"]["state"]["state"]["kind"], "busy");
|
||||
assert_eq!(parsed["data"]["state"]["state"]["state"]["state"], "paused");
|
||||
}
|
||||
@@ -2381,7 +2445,17 @@ mod tests {
|
||||
let inbound = r#"{"event":"snapshot","data":{"session":{"entries":[]},"greeting":{"worker_name":"test","cwd":"/tmp","provider":"p","model":"m","scope_summary":"s","tools":[]},"state":{"last_command_id":0,"state":{"kind":"busy","state":{"kind":"run","state":"running"}}}}}"#;
|
||||
let decoded: Event = serde_json::from_str(inbound).unwrap();
|
||||
match decoded {
|
||||
Event::Snapshot { in_flight, .. } => assert!(in_flight.is_empty()),
|
||||
Event::Snapshot {
|
||||
greeting,
|
||||
in_flight,
|
||||
..
|
||||
} => {
|
||||
assert!(in_flight.is_empty());
|
||||
assert_eq!(greeting.reasoning, None);
|
||||
assert_eq!(greeting.context_usage, None);
|
||||
assert_eq!(greeting.context_window, 0);
|
||||
assert_eq!(greeting.context_tokens, 0);
|
||||
}
|
||||
other => panic!("expected Snapshot, got {other:?}"),
|
||||
}
|
||||
|
||||
@@ -2399,6 +2473,8 @@ mod tests {
|
||||
tools: Vec::new(),
|
||||
context_window: 0,
|
||||
context_tokens: 0,
|
||||
reasoning: None,
|
||||
context_usage: None,
|
||||
},
|
||||
state: WorkerStatus::Running.into(),
|
||||
in_flight: InFlightSnapshot {
|
||||
|
||||
@@ -6,17 +6,18 @@ use ts_rs::{Config, TS};
|
||||
use crate::{
|
||||
Alert, AlertLevel, AlertSource, CommandEvent, CommandSnapshot, CommandStatus, CommandStream,
|
||||
CommandStreamSlice, CompactionLifecycle, CompactionLifecycleState, CompactionPhase,
|
||||
CompactionTrigger, CompletionEntry, CompletionKind, ErrorCode, Event, Greeting, InFlightBlock,
|
||||
InFlightCompaction, InFlightSnapshot, InFlightToolCallState, InternalWorkerKind,
|
||||
InternalWorkerRef, InternalWorkerSnapshot, InvokeKind, MemoryWorkerEvent, Method,
|
||||
PasteArtifactAvailability, PasteArtifactMediaType, PasteArtifactRef, PendingSubmissionSummary,
|
||||
PendingSubmissionsSnapshot, Permission, RewindSummary, RewindTarget, RewindTargetId,
|
||||
RunFailureKind, RunResult, RunResumeSource, RunYieldReason, ScopeRule, Segment,
|
||||
SessionContentPart, SessionConversationTurn, SessionEntryProvenance,
|
||||
SessionHistoryLineageBoundary, SessionHistoryPage, SessionMessageRole, SessionSnapshot,
|
||||
SessionSnapshotEntry, SessionSnapshotEntryData, SessionToolAttachment, SubmissionDisposition,
|
||||
SymlinkPolicy, ToolResultDisposition, TurnResult, UploadedFileAvailability, UploadedFileRef,
|
||||
WorkerBusyState, WorkerCommandAcknowledgement, WorkerCommandDisposition, WorkerCommandEnvelope,
|
||||
CompactionTrigger, CompletionEntry, CompletionKind, ContextTokenSource, ContextUsage,
|
||||
ErrorCode, Event, Greeting, InFlightBlock, InFlightCompaction, InFlightSnapshot,
|
||||
InFlightToolCallState, InternalWorkerKind, InternalWorkerRef, InternalWorkerSnapshot,
|
||||
InvokeKind, MemoryWorkerEvent, Method, PasteArtifactAvailability, PasteArtifactMediaType,
|
||||
PasteArtifactRef, PendingSubmissionSummary, PendingSubmissionsSnapshot, Permission,
|
||||
ReasoningConfig, RewindSummary, RewindTarget, RewindTargetId, RunFailureKind, RunResult,
|
||||
RunResumeSource, RunYieldReason, ScopeRule, Segment, SessionContentPart,
|
||||
SessionConversationTurn, SessionEntryProvenance, SessionHistoryLineageBoundary,
|
||||
SessionHistoryPage, SessionMessageRole, SessionSnapshot, SessionSnapshotEntry,
|
||||
SessionSnapshotEntryData, SessionToolAttachment, SubmissionDisposition, SymlinkPolicy,
|
||||
ToolResultDisposition, TurnResult, UploadedFileAvailability, UploadedFileRef, WorkerBusyState,
|
||||
WorkerCommandAcknowledgement, WorkerCommandDisposition, WorkerCommandEnvelope,
|
||||
WorkerCommandKind, WorkerEvent, WorkerMaintenanceState, WorkerRunState, WorkerState,
|
||||
WorkerStateSnapshot, WorkerStatus,
|
||||
subscription::{
|
||||
@@ -116,10 +117,13 @@ pub fn generated_protocol_types() -> String {
|
||||
push_decl::<PendingSubmissionsSnapshot>(&cfg, &mut output);
|
||||
push_decl::<SubmissionDisposition>(&cfg, &mut output);
|
||||
push_decl::<SessionSnapshot>(&cfg, &mut output);
|
||||
push_decl::<ReasoningConfig>(&cfg, &mut output);
|
||||
push_decl::<ContextTokenSource>(&cfg, &mut output);
|
||||
push_decl::<ContextUsage>(&cfg, &mut output);
|
||||
push_decl::<Greeting>(&cfg, &mut output);
|
||||
push_decl::<InternalWorkerKind>(&cfg, &mut output);
|
||||
push_decl::<InternalWorkerRef>(&cfg, &mut output);
|
||||
push_decl::<InternalWorkerSnapshot>(&cfg, &mut output);
|
||||
push_decl::<Greeting>(&cfg, &mut output);
|
||||
push_decl::<Alert>(&cfg, &mut output);
|
||||
push_decl::<MemoryWorkerEvent>(&cfg, &mut output);
|
||||
push_decl::<PasteArtifactMediaType>(&cfg, &mut output);
|
||||
@@ -256,20 +260,71 @@ fn rust_serialized_subscription_frame_fixtures() -> Vec<SubscriptionFrame> {
|
||||
));
|
||||
let pending =
|
||||
SubscriptionFrame::new(SubscriptionFramePayload::Event(SubscriptionEvent::Event {
|
||||
subscription_id,
|
||||
subscription_id: subscription_id.clone(),
|
||||
payload: SubscriptionEventPayload::WorkerProtocol {
|
||||
worker_id,
|
||||
worker_id: worker_id.clone(),
|
||||
event: Event::PendingSubmissionsChanged {
|
||||
pending: PendingSubmissionsSnapshot::default(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
for fixture in [&subscribed, &pending] {
|
||||
let greeting = Greeting {
|
||||
worker_name: "fixture-worker".into(),
|
||||
cwd: "/fixture".into(),
|
||||
provider: "openai_responses".into(),
|
||||
model: "gpt-6-astra".into(),
|
||||
reasoning: Some(ReasoningConfig::Effort {
|
||||
effort: "high".into(),
|
||||
}),
|
||||
scope_summary: "fixture scope".into(),
|
||||
tools: vec!["Read".into()],
|
||||
context_window: 272_000,
|
||||
context_tokens: 142_000,
|
||||
context_usage: Some(ContextUsage {
|
||||
tokens: 142_000,
|
||||
source: ContextTokenSource::Measured,
|
||||
}),
|
||||
};
|
||||
let snapshot =
|
||||
SubscriptionFrame::new(SubscriptionFramePayload::Event(SubscriptionEvent::Event {
|
||||
subscription_id,
|
||||
payload: SubscriptionEventPayload::WorkerProtocol {
|
||||
worker_id,
|
||||
event: Event::Snapshot {
|
||||
session: SessionSnapshot {
|
||||
pending_submissions: PendingSubmissionsSnapshot::default(),
|
||||
entries: Vec::new(),
|
||||
},
|
||||
greeting: greeting.clone(),
|
||||
state: WorkerStateSnapshot::initial(),
|
||||
in_flight: InFlightSnapshot::default(),
|
||||
internal_workers: vec![InternalWorkerSnapshot {
|
||||
worker: InternalWorkerRef {
|
||||
session_id: "fixture-child-session".into(),
|
||||
name: "fixture-child".into(),
|
||||
parent_session_id: Some("fixture-parent-session".into()),
|
||||
kind: InternalWorkerKind::SubWorker,
|
||||
},
|
||||
revision: 1,
|
||||
session: SessionSnapshot {
|
||||
pending_submissions: PendingSubmissionsSnapshot::default(),
|
||||
entries: Vec::new(),
|
||||
},
|
||||
greeting: Some(greeting),
|
||||
status: WorkerStatus::Idle,
|
||||
error: None,
|
||||
in_flight: InFlightSnapshot::default(),
|
||||
internal_workers: Vec::new(),
|
||||
}],
|
||||
},
|
||||
},
|
||||
}));
|
||||
for fixture in [&subscribed, &pending, &snapshot] {
|
||||
fixture
|
||||
.validate()
|
||||
.expect("Rust-serialized Browser compatibility fixture must validate");
|
||||
}
|
||||
vec![subscribed, pending]
|
||||
vec![subscribed, pending, snapshot]
|
||||
}
|
||||
|
||||
fn close_object_shapes(schema: &mut Value) {
|
||||
|
||||
Reference in New Issue
Block a user