1mod composer;
11mod continuation;
12mod run;
13mod tools;
14
15pub use composer::Composer;
16pub use tools::{Tool, ToolContext, ToolFuture, ToolRegistry};
17
18pub use continuation::{should_continue, with_continuation, with_external_results};
19
20use crate::abort::AbortController;
21use crate::adapter::{ChatModelAdapter, ModelContext};
22use crate::repository::{ExportedRepository, MessageChanges, MessageEntry, MessageRepository};
23use crate::runtime::{
24 ComposerHost, RunFuture, RuntimeCapabilities, RuntimeError, StartRunConfig, ThreadRuntime,
25 ToolResult,
26};
27use crate::subscribe::{Subscribers, Subscription};
28use crate::types::{
29 AppendContent, AppendMessage, AssistantMessage, AssistantMetadata, MessageId, MessageMetadata,
30 MessageStatus, RequiresActionReason, RunConfig, SystemMessage, SystemMessagePart, TextPart,
31 ThreadMessage, Timestamp, UserMessage,
32};
33use std::cell::{Cell, RefCell};
34use std::collections::HashMap;
35use std::rc::{Rc, Weak};
36use std::sync::Arc;
37
38#[derive(Clone)]
40pub struct LocalRuntimeOptions {
41 pub max_steps: usize,
45 pub human_tool_names: Option<Vec<String>>,
48 pub thread_id: Option<String>,
50 pub clock: Rc<dyn Fn() -> Timestamp>,
52 pub ids: Rc<dyn Fn() -> MessageId>,
54}
55
56impl LocalRuntimeOptions {
57 pub fn with_clock(clock: Rc<dyn Fn() -> Timestamp>) -> Self {
59 let prefix = format!("{:x}", clock().0);
60 let counter = Cell::new(0u64);
61 Self {
62 max_steps: 2,
63 human_tool_names: None,
64 thread_id: None,
65 clock,
66 ids: Rc::new(move || {
67 counter.set(counter.get() + 1);
68 MessageId(format!("{prefix}-{}", counter.get()))
69 }),
70 }
71 }
72}
73
74#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
77impl Default for LocalRuntimeOptions {
78 fn default() -> Self {
79 Self::with_clock(Rc::new(|| {
80 let now = std::time::SystemTime::now()
81 .duration_since(std::time::UNIX_EPOCH)
82 .unwrap_or_default();
83 Timestamp(now.as_millis() as u64)
84 }))
85 }
86}
87
88struct ActiveRun {
89 id: u64,
90 controller: Option<AbortController>,
92 cancelled: bool,
94}
95
96struct State {
97 repository: MessageRepository,
98 active: Option<ActiveRun>,
99 next_run: u64,
100 model_context: ModelContext,
101 tools: Rc<ToolRegistry>,
102}
103
104pub(crate) struct Shared {
105 me: Weak<Shared>,
106 state: RefCell<State>,
107 adapter: Rc<dyn ChatModelAdapter>,
108 options: LocalRuntimeOptions,
109 subscribers: Rc<Subscribers>,
110 thread_composer: RefCell<Option<Composer>>,
111 edits: RefCell<HashMap<MessageId, Composer>>,
112}
113
114impl Shared {
115 fn notify(&self) {
118 self.subscribers.notify();
119 let composers: Vec<Composer> = self
120 .thread_composer
121 .borrow()
122 .iter()
123 .cloned()
124 .chain(self.edits.borrow().values().cloned())
125 .collect();
126 for composer in composers {
127 composer.notify();
128 }
129 }
130 fn is_active(&self, run: u64) -> bool {
131 self.state
132 .borrow()
133 .active
134 .as_ref()
135 .is_some_and(|a| a.id == run)
136 }
137 fn fresh_id(&self, repository: &MessageRepository) -> MessageId {
138 loop {
139 let id = (self.options.ids)();
140 if !repository.contains(&id) {
141 return id;
142 }
143 }
144 }
145}
146
147#[derive(Clone)]
149pub struct LocalThreadRuntime {
150 shared: Rc<Shared>,
151}
152
153impl LocalThreadRuntime {
154 pub fn new(adapter: impl ChatModelAdapter + 'static, options: LocalRuntimeOptions) -> Self {
156 Self {
157 shared: Rc::new_cyclic(|me| Shared {
158 me: me.clone(),
159 state: RefCell::new(State {
160 repository: MessageRepository::new(),
161 active: None,
162 next_run: 0,
163 model_context: ModelContext::default(),
164 tools: Rc::default(),
165 }),
166 adapter: Rc::new(adapter),
167 options,
168 subscribers: Rc::default(),
169 thread_composer: RefCell::new(None),
170 edits: RefCell::new(HashMap::new()),
171 }),
172 }
173 }
174
175 pub fn subscribe(&self, callback: impl Fn() + 'static) -> Subscription {
177 self.shared.subscribers.subscribe(callback)
178 }
179
180 pub fn messages(&self) -> Arc<[Arc<ThreadMessage>]> {
182 self.shared.state.borrow().repository.messages()
183 }
184 pub fn is_running(&self) -> bool {
186 self.shared.state.borrow().active.is_some()
187 }
188 pub fn message(&self, id: &MessageId) -> Result<MessageEntry, RuntimeError> {
190 Ok(self.shared.state.borrow().repository.get(id)?)
191 }
192 pub fn branches(&self, id: &MessageId) -> Result<Vec<MessageId>, RuntimeError> {
194 Ok(self.shared.state.borrow().repository.branches(id)?.to_vec())
195 }
196 pub fn switch_to_branch(&self, id: &MessageId) -> Result<(), RuntimeError> {
198 self.shared
199 .state
200 .borrow_mut()
201 .repository
202 .switch_to_branch(id)?;
203 self.shared.notify();
204 Ok(())
205 }
206 pub fn composer(&self) -> Composer {
208 self.shared
209 .thread_composer
210 .borrow_mut()
211 .get_or_insert_with(|| {
212 Composer::thread(Rc::downgrade(&self.shared) as Weak<dyn ComposerHost>)
213 })
214 .clone()
215 }
216
217 pub fn begin_edit(&self, id: &MessageId) -> Result<Composer, RuntimeError> {
220 if self.shared.edits.borrow().contains_key(id) {
221 return Err(RuntimeError::EditInProgress(id.clone()));
222 }
223 let entry = self.message(id)?;
224 let composer = Composer::edit(
225 Rc::downgrade(&self.shared) as Weak<dyn ComposerHost>,
226 entry.parent_id,
227 &entry.message,
228 );
229 self.shared
230 .edits
231 .borrow_mut()
232 .insert(id.clone(), composer.clone());
233 self.shared.notify();
234 Ok(composer)
235 }
236
237 pub fn edit_composer(&self, id: &MessageId) -> Option<Composer> {
239 self.shared.edits.borrow().get(id).cloned()
240 }
241
242 pub fn set_tools(&self, tools: ToolRegistry) {
246 let mut state = self.shared.state.borrow_mut();
247 state.model_context.tools = tools.definitions();
248 state.tools = Rc::new(tools);
249 }
250
251 pub fn respond_to_tool_approval(
256 &self,
257 message_id: &MessageId,
258 approval_id: &str,
259 approved: bool,
260 reason: Option<String>,
261 ) -> Result<Option<RunFuture>, RuntimeError> {
262 let (resume, parent) = {
263 let mut state = self.shared.state.borrow_mut();
264 let entry = state.repository.get(message_id)?;
265 let Some(assistant) = entry.message.as_assistant() else {
266 return Err(RuntimeError::NotAssistantMessage(message_id.clone()));
267 };
268 let mut updated = assistant.clone();
269 let approval = updated
270 .content
271 .iter_mut()
272 .filter_map(|p| match p {
273 crate::types::AssistantMessagePart::ToolCall(c) => c.approval.as_mut(),
274 _ => None,
275 })
276 .find(|a| a.id == approval_id && a.is_pending())
277 .ok_or_else(|| RuntimeError::ApprovalNotFound(approval_id.to_owned()))?;
278 approval.approved = Some(approved);
279 approval.reason = reason;
280 let paused = matches!(
281 updated.status,
282 MessageStatus::RequiresAction {
283 reason: RequiresActionReason::ToolCalls
284 }
285 );
286 let ready =
287 paused && state.active.is_none() && !state.repository.has_children(message_id);
288 state
289 .repository
290 .add_or_update(entry.parent_id.as_ref(), ThreadMessage::Assistant(updated))?;
291 let resume = ready.then(|| Self::activate(&mut state));
292 (resume, entry.parent_id)
293 };
294 self.shared.notify();
295 Ok(resume.map(|run| -> RunFuture {
296 Box::pin(run::run_loop(
297 self.shared.clone(),
298 run,
299 parent,
300 message_id.clone(),
301 RunConfig::default(),
302 true,
303 ))
304 }))
305 }
306
307 pub fn set_model_context(&self, context: ModelContext) {
309 self.shared.state.borrow_mut().model_context = context;
310 }
311
312 pub fn submit_feedback(
315 &self,
316 id: &MessageId,
317 feedback: Option<crate::types::SubmittedFeedback>,
318 ) -> Result<(), RuntimeError> {
319 {
320 let mut state = self.shared.state.borrow_mut();
321 let entry = state.repository.get(id)?;
322 let Some(assistant) = entry.message.as_assistant() else {
323 return Err(RuntimeError::NotAssistantMessage(id.clone()));
324 };
325 let mut updated = assistant.clone();
326 updated.metadata.submitted_feedback = feedback;
327 state
328 .repository
329 .add_or_update(entry.parent_id.as_ref(), ThreadMessage::Assistant(updated))?;
330 }
331 self.shared.notify();
332 Ok(())
333 }
334
335 pub fn export(&self) -> ExportedRepository {
337 self.shared.state.borrow().repository.export()
338 }
339 pub fn import(&self, exported: ExportedRepository) -> Result<(), RuntimeError> {
341 {
342 let mut state = self.shared.state.borrow_mut();
343 if state.active.is_some() {
344 return Err(RuntimeError::RunInProgress);
345 }
346 state.repository.import(exported)?;
347 }
348 self.shared.notify();
349 Ok(())
350 }
351
352 pub fn append(&self, message: AppendMessage) -> Result<RunFuture, RuntimeError> {
356 let start = message.starts_run();
357 let id = {
358 let mut state = self.shared.state.borrow_mut();
359 let id = self.shared.fresh_id(&state.repository);
360 let created_at = (self.shared.options.clock)();
361 let new = match message.content {
362 AppendContent::User {
363 content,
364 attachments,
365 custom,
366 } => ThreadMessage::User(UserMessage {
367 id: id.clone(),
368 created_at,
369 content,
370 attachments,
371 metadata: MessageMetadata {
372 custom,
373 ..Default::default()
374 },
375 }),
376 AppendContent::Assistant { content, custom } => {
377 ThreadMessage::Assistant(AssistantMessage {
378 id: id.clone(),
379 created_at,
380 content,
381 status: MessageStatus::complete(),
382 metadata: AssistantMetadata {
383 custom,
384 ..Default::default()
385 },
386 })
387 }
388 AppendContent::System { text } => ThreadMessage::System(SystemMessage {
389 id: id.clone(),
390 created_at,
391 content: vec![SystemMessagePart::Text(TextPart::new(text))],
392 metadata: MessageMetadata::default(),
393 }),
394 };
395 state
396 .repository
397 .add_or_update(message.parent_id.as_ref(), new)?;
398 if !start {
399 state.repository.switch_to_branch(&id)?;
400 }
401 id
402 };
403 if !start {
404 self.shared.notify();
405 return Ok(Box::pin(async { Ok(()) }));
406 }
407 self.start_run(StartRunConfig {
408 parent_id: Some(id),
409 source_id: message.source_id,
410 run_config: message.run_config,
411 })
412 }
413
414 pub fn start_run(&self, config: StartRunConfig) -> Result<RunFuture, RuntimeError> {
417 let (run, message_id) = {
418 let mut state = self.shared.state.borrow_mut();
419 let id = self.shared.fresh_id(&state.repository);
420 let message = AssistantMessage {
421 id: id.clone(),
422 created_at: (self.shared.options.clock)(),
423 content: Vec::new(),
424 status: MessageStatus::Running,
425 metadata: AssistantMetadata::default(),
426 };
427 state
428 .repository
429 .add_or_update(config.parent_id.as_ref(), ThreadMessage::Assistant(message))?;
430 state.repository.switch_to_branch(&id)?;
431 (Self::activate(&mut state), id)
432 };
433 self.shared.notify();
434 Ok(Box::pin(run::run_loop(
435 self.shared.clone(),
436 run,
437 config.parent_id,
438 message_id,
439 config.run_config,
440 false,
441 )))
442 }
443
444 pub fn cancel_run(&self) {
446 let controller = self
447 .shared
448 .state
449 .borrow_mut()
450 .active
451 .as_mut()
452 .and_then(|a| {
453 a.cancelled = true;
454 a.controller.take()
455 });
456 if let Some(controller) = controller {
457 controller.abort();
458 }
459 }
460
461 pub fn add_tool_result(&self, result: ToolResult) -> Result<Option<RunFuture>, RuntimeError> {
464 let (resume, parent) = {
465 let mut state = self.shared.state.borrow_mut();
466 let entry = state.repository.get(&result.message_id)?;
467 let Some(assistant) = entry.message.as_assistant() else {
468 return Err(RuntimeError::NotAssistantMessage(result.message_id));
469 };
470 let mut updated = assistant.clone();
471 let mut added = false;
472 let call = updated
473 .content
474 .iter_mut()
475 .filter_map(|p| match p {
476 crate::types::AssistantMessagePart::ToolCall(c) => Some(c),
477 _ => None,
478 })
479 .find(|c| c.tool_call_id == result.tool_call_id)
480 .ok_or_else(|| RuntimeError::ToolCallNotFound {
481 message_id: result.message_id.clone(),
482 tool_call_id: result.tool_call_id.clone(),
483 })?;
484 added |= call.is_open();
485 call.result = Some(result.result);
486 call.is_error = Some(result.is_error);
487 call.is_preliminary = None;
488 if result.artifact.is_some() {
489 call.artifact = result.artifact;
490 }
491 let ready = added
492 && state.active.is_none()
493 && should_continue(&updated, self.shared.options.human_tool_names.as_deref())
494 && !state.repository.has_children(&result.message_id);
495 state
496 .repository
497 .add_or_update(entry.parent_id.as_ref(), ThreadMessage::Assistant(updated))?;
498 let resume = ready.then(|| Self::activate(&mut state));
499 (resume, entry.parent_id)
500 };
501 self.shared.notify();
502 Ok(resume.map(|run| -> RunFuture {
503 Box::pin(run::run_loop(
504 self.shared.clone(),
505 run,
506 parent,
507 result.message_id,
508 RunConfig::default(),
509 false,
510 ))
511 }))
512 }
513
514 fn activate(state: &mut State) -> u64 {
516 if let Some(previous) = state.active.take() {
517 if let Some(controller) = previous.controller {
518 controller.abort();
519 }
520 }
521 let id = state.next_run;
522 state.next_run += 1;
523 state.active = Some(ActiveRun {
524 id,
525 controller: None,
526 cancelled: false,
527 });
528 id
529 }
530}
531
532impl ComposerHost for Shared {
533 fn last_message_id(&self) -> Option<MessageId> {
534 self.state
535 .borrow()
536 .repository
537 .messages()
538 .last()
539 .map(|m| m.id().clone())
540 }
541 fn append(&self, message: AppendMessage) -> Result<RunFuture, RuntimeError> {
542 let shared = self.me.upgrade().ok_or(RuntimeError::Disposed)?;
543 LocalThreadRuntime { shared }.append(message)
544 }
545 fn cancel_run(&self) {
546 if let Some(shared) = self.me.upgrade() {
547 LocalThreadRuntime { shared }.cancel_run();
548 }
549 }
550 fn is_running(&self) -> bool {
551 self.state.borrow().active.is_some()
552 }
553 fn end_edit(&self, id: &MessageId) {
554 self.edits.borrow_mut().remove(id);
555 self.notify();
556 }
557}
558
559impl ThreadRuntime for LocalThreadRuntime {
560 fn messages(&self) -> Arc<[Arc<ThreadMessage>]> {
561 Self::messages(self)
562 }
563 fn message(&self, id: &MessageId) -> Result<MessageEntry, RuntimeError> {
564 Self::message(self, id)
565 }
566 fn is_running(&self) -> bool {
567 Self::is_running(self)
568 }
569 fn capabilities(&self) -> RuntimeCapabilities {
570 RuntimeCapabilities::ALL
571 }
572 fn branches(&self, id: &MessageId) -> Result<Vec<MessageId>, RuntimeError> {
573 Self::branches(self, id)
574 }
575 fn branch_position(&self, id: &MessageId) -> Result<(usize, usize), RuntimeError> {
576 Ok(self.shared.state.borrow().repository.branch_position(id)?)
577 }
578 fn structure_version(&self) -> Option<u64> {
579 Some(self.shared.state.borrow().repository.shape_version())
580 }
581 fn message_changes_since(&self, version: Option<u64>) -> Option<MessageChanges> {
582 Some(
583 self.shared
584 .state
585 .borrow()
586 .repository
587 .message_changes_since(version),
588 )
589 }
590 fn switch_to_branch(&self, id: &MessageId) -> Result<(), RuntimeError> {
591 Self::switch_to_branch(self, id)
592 }
593 fn append(&self, message: AppendMessage) -> Result<RunFuture, RuntimeError> {
594 Self::append(self, message)
595 }
596 fn reload(&self, config: StartRunConfig) -> Result<RunFuture, RuntimeError> {
597 self.start_run(config)
598 }
599 fn cancel_run(&self) {
600 Self::cancel_run(self)
601 }
602 fn add_tool_result(&self, result: ToolResult) -> Result<Option<RunFuture>, RuntimeError> {
603 Self::add_tool_result(self, result)
604 }
605 fn respond_to_tool_approval(
606 &self,
607 message_id: &MessageId,
608 approval_id: &str,
609 approved: bool,
610 reason: Option<String>,
611 ) -> Result<Option<RunFuture>, RuntimeError> {
612 Self::respond_to_tool_approval(self, message_id, approval_id, approved, reason)
613 }
614 fn submit_feedback(
615 &self,
616 id: &MessageId,
617 feedback: Option<crate::types::SubmittedFeedback>,
618 ) -> Result<(), RuntimeError> {
619 Self::submit_feedback(self, id, feedback)
620 }
621 fn composer(&self) -> Composer {
622 Self::composer(self)
623 }
624 fn begin_edit(&self, id: &MessageId) -> Result<Composer, RuntimeError> {
625 Self::begin_edit(self, id)
626 }
627 fn edit_composer(&self, id: &MessageId) -> Option<Composer> {
628 Self::edit_composer(self, id)
629 }
630 fn subscribe_dyn(&self, callback: Box<dyn Fn()>) -> Subscription {
631 self.shared.subscribers.subscribe(callback)
632 }
633 fn export(&self) -> ExportedRepository {
634 Self::export(self)
635 }
636 fn import(&self, exported: ExportedRepository) -> Result<(), RuntimeError> {
637 Self::import(self, exported)
638 }
639}