1use kernel::event::record::{ModelReturned, RunStarted, SteerReceived, ToolAnswer, ToolResult};
19use kernel::model::content_from_message;
20use kernel::{AxCode, AxError, ChatMessage, ContentBlock, EventKind, EventRecord, RunId, Seq};
21
22use crate::compaction::Exchange;
23use crate::conversation::{Conversation, Opening};
24use crate::replay::VerifiedLedger;
25
26mod lineage;
27
28pub use lineage::fork_draft;
29use lineage::forked_from;
30
31pub fn prefix(mother: &VerifiedLedger, at_seq: Seq) -> Result<Vec<Vec<u8>>, AxError> {
34 let refuse = || {
35 AxError::failure(
36 AxCode::InvalidArgs,
37 "fork",
38 format!("at_seq {}", at_seq.value()),
39 )
40 .with_recovery(match mother.tail_seq() {
41 Some(tail) => format!("the mother sequence ends at seq {}", tail.value()),
42 None => "the mother sequence is empty".to_string(),
43 })
44 };
45 let tail = mother.tail_seq().ok_or_else(refuse)?;
46 if at_seq > tail {
47 return Err(refuse());
48 }
49 let index = usize::try_from(at_seq.value()).map_err(|_| refuse())?;
50 let end = index.checked_add(1).ok_or_else(refuse)?;
51 mother
52 .raw_lines()
53 .get(..end)
54 .map(<[Vec<u8>]>::to_vec)
55 .ok_or_else(refuse)
56}
57
58#[derive(Debug, Clone, PartialEq)]
61pub struct Inherited {
62 pub messages: Vec<ChatMessage>,
65 pub at: Seq,
72}
73
74fn no_start(owner: RunId) -> AxError {
76 AxError::failure(
77 AxCode::InvalidArgs,
78 "fork",
79 format!("{owner} has no run_started in this ledger"),
80 )
81 .with_recovery("name a run this history holds, or start a session instead")
82}
83
84fn fold_run<'a>(
88 mut conversation: Conversation,
89 records: impl Iterator<Item = &'a EventRecord>,
90) -> Result<Inherited, AxError> {
91 let mut at = Seq::FIRST;
92 let mut open: Option<Wave> = None;
93 for record in records {
94 match record.kind() {
95 EventKind::RunStarted => {
96 let started = record.data().read::<RunStarted>()?;
97 conversation.push_task_lines(&started.task, &started.goal, rebuilt(&started));
98 at = record.seq();
99 }
100 EventKind::ModelReturned => {
101 commit(&mut conversation, open.take())?;
107 let returned = record.data().read::<ModelReturned>()?;
108 let assistant = content_from_message(&returned.message)?;
109 let mut exchange = Exchange::new();
110 exchange.push_assistant(assistant);
111 open = Some(Wave {
112 exchange,
113 expect: usize::try_from(returned.calls).unwrap_or(usize::MAX),
114 });
115 if returned.calls == 0 {
116 commit(&mut conversation, open.take())?;
117 at = record.seq();
118 }
119 }
120 EventKind::ToolResult => {
121 let result = record.data().read::<ToolResult>()?;
122 if let Some(wave) = open.as_mut() {
123 wave.exchange.push_result(result_block(&result)?);
124 if wave.exchange.results().len() >= wave.expect {
125 commit(&mut conversation, open.take())?;
126 at = record.seq();
127 }
128 }
129 }
130 EventKind::PromptShapeCompared => conversation.mark_sent(),
135 EventKind::SteerReceived => {
136 let steer = record.data().read::<SteerReceived>()?;
137 conversation.push_steer(&steer.source, &steer.text);
138 }
139 EventKind::CityInitialized
146 | EventKind::BuildingCreated
147 | EventKind::BuildingConfigured
148 | EventKind::BuildingRemoved
149 | EventKind::SessionOpened
150 | EventKind::RunForked
151 | EventKind::PromptAssembled
152 | EventKind::ModelCalled
153 | EventKind::ToolCalled
154 | EventKind::ResultOffloaded
155 | EventKind::GateChecked
156 | EventKind::GateDenied
157 | EventKind::CheckpointCommitted
158 | EventKind::HandoffWritten
159 | EventKind::CancelReceived
160 | EventKind::WatchdogFired
161 | EventKind::BudgetLimit
162 | EventKind::RunFrozen
163 | EventKind::LogTruncated
164 | EventKind::SignalEnqueued
165 | EventKind::SignalConsumed
166 | EventKind::DraftHeld
167 | EventKind::DraftResolved
168 | EventKind::GoalRegistered
169 | EventKind::GoalConflict
170 | EventKind::ArbitrationVerdict
171 | EventKind::RepairStarted
172 | EventKind::RepairReused
173 | EventKind::WorktreeOpened
174 | EventKind::PrOpened
175 | EventKind::PrMerged
176 | EventKind::PrRejected
177 | EventKind::RoadmapClaimed
178 | EventKind::RoadmapFinished
179 | EventKind::RoadmapReleased
180 | EventKind::RoadmapSplit
181 | EventKind::RoadmapBlocked
182 | EventKind::PursuitChanged
183 | EventKind::ApprovalRequested
184 | EventKind::ApprovalResolved
185 | EventKind::PolicyCreated
186 | EventKind::PolicyRevoked
187 | EventKind::TaintPromoted
188 | EventKind::CrossBuildingTransfer
189 | EventKind::CityHalted
190 | EventKind::BackpressureShed
191 | EventKind::DigestInvalidated
192 | EventKind::EndpointAttached
193 | EventKind::EndpointProbed
194 | EventKind::EndpointLost
195 | EventKind::ModelSelected
196 | EventKind::ProviderDegraded
197 | EventKind::LoginStarted
198 | EventKind::EvalRun
199 | EventKind::AssetArchived
200 | EventKind::CredentialLent
201 | EventKind::SecretCaptured
202 | EventKind::SecretEgressBlocked
203 | EventKind::FileDiscarded
204 | EventKind::DiscardRestored
205 | EventKind::AutonomyChanged
206 | EventKind::WentBack
207 | EventKind::FileRestored
208 | EventKind::GovernedDocumentWritten
209 | EventKind::SpineDocumentWritten
210 | EventKind::RulesChanged
211 | EventKind::ToolkitLinkOpened
212 | EventKind::EmbeddingCalled
213 | EventKind::RerankCalled
214 | EventKind::AdviserAsked
215 | EventKind::AdviserAnswered
216 | EventKind::AdviserFellBack
217 | EventKind::CacheRenewed
218 | EventKind::HarnessReported
221 | EventKind::HarnessAnswered
222 | EventKind::RemoteOpened
224 | EventKind::RemoteClosed
225 | EventKind::DevicePaired
226 | EventKind::DeviceRevoked
227 | EventKind::RemoteSessionStarted
228 | EventKind::DocumentWritten
231 | EventKind::ProposalOffered
232 | EventKind::ProposalDecided
233 | EventKind::ProposalWithdrawn => {}
234 }
235 }
236 open.take();
240 Ok(Inherited {
241 messages: conversation.messages().to_vec(),
242 at,
243 })
244}
245
246fn rebuilt(started: &RunStarted) -> Opening {
261 match started.opening {
262 Some(Opening::FromJob) => Opening::Inherited,
263 Some(spoken @ (Opening::Inherited | Opening::WithPerson)) => spoken,
264 None if started.job.is_some() => Opening::Inherited,
265 None => Opening::WithPerson,
266 }
267}
268
269struct Wave {
274 exchange: Exchange,
275 expect: usize,
276}
277
278fn commit(conversation: &mut Conversation, wave: Option<Wave>) -> Result<(), AxError> {
283 let Some(mut wave) = wave else {
284 return Ok(());
285 };
286 wave.exchange.compact()?;
287 conversation.push_assistant(wave.exchange.assistant().to_vec());
288 conversation.push_tool_results(wave.exchange.results().to_vec());
289 Ok(())
290}
291
292fn result_block(result: &ToolResult) -> Result<ContentBlock, AxError> {
301 let (payload, is_error) = match &result.answer {
302 ToolAnswer::Answered { result } => (result, false),
303 ToolAnswer::Failed { error } => (error, true),
304 };
305 Ok(ContentBlock::ToolResult {
306 tool_use_id: result.tool_use_id.clone(),
307 content: serde_json::to_string(payload).map_err(|err| {
308 AxError::failure(
309 AxCode::InvalidArgs,
310 "rebuild a tool result for a fork",
311 err.to_string(),
312 )
313 .with_recovery(
314 "report this against runtime::fork: a payload refused JSON after the \
315 record that holds it was read",
316 )
317 })?,
318 is_error,
319 attachments: Vec::new(),
320 })
321}
322
323mod indexed;
324pub use indexed::inherited_indexed;
325
326#[cfg(test)]
327mod tests;
328
329#[cfg(test)]
330mod compaction_tests;
331
332#[cfg(test)]
333mod request_tests;