1use std::collections::BTreeMap;
2use std::sync::Arc;
3use std::time::{Duration, Instant};
4
5use chrono::Utc;
6use serde_json::{Value, json};
7
8use crate::budget::Budget;
9use crate::event::{Event, EventKind};
10use crate::journal::{InMemoryJournal, Journal, JournalError};
11use crate::message::{Content, Message, content_text};
12use futures::StreamExt;
13
14use crate::model::{Model, ModelError, ModelRequest, ModelResponse, StreamEvent, Usage};
15use crate::policy::{Decision, EffectPolicy, Policy};
16use crate::state::{PendingCall, RunState, Status};
17use crate::tool::{Tool, ToolCtx};
18
19#[derive(Debug, thiserror::Error)]
20pub enum AgentError {
21 #[error(transparent)]
22 Journal(#[from] JournalError),
23 #[error("no run with id `{0}`")]
24 UnknownRun(String),
25 #[error("a run with id `{0}` already exists")]
26 RunExists(String),
27 #[error("call `{call_id}` in run `{run_id}` is not awaiting approval")]
28 NotAwaitingApproval { run_id: String, call_id: String },
29 #[error("invalid agent configuration: {0}")]
30 Config(String),
31 #[error("run `{run_id}` is being driven by `{owner}` (lease until {expires_at})")]
32 Leased { run_id: String, owner: String, expires_at: chrono::DateTime<chrono::Utc> },
33 #[error("lost the lease on run `{run_id}`; another worker may have taken over")]
34 LeaseLost { run_id: String },
35}
36
37#[derive(Debug, Clone)]
39pub struct RunOutcome {
40 pub run_id: String,
41 pub status: Status,
42 pub steps: u32,
43 pub usage: Usage,
44 pub cost_usd: f64,
45 pub awaiting_approval: Vec<PendingCall>,
46 pub structured: Option<Value>,
48}
49
50impl RunOutcome {
51 pub fn output(&self) -> Option<&str> {
52 match &self.status {
53 Status::Completed { output } => Some(output),
54 _ => None,
55 }
56 }
57
58 fn from_state(state: &RunState) -> Self {
59 Self {
60 run_id: state.run_id.clone(),
61 status: state.status.clone(),
62 steps: state.steps,
63 usage: state.usage,
64 cost_usd: state.cost_usd,
65 awaiting_approval: state.awaiting_approval(),
66 structured: state.structured_output.clone(),
67 }
68 }
69
70 pub fn output_as<T: serde::de::DeserializeOwned>(&self) -> Option<Result<T, serde_json::Error>> {
72 self.structured.clone().map(serde_json::from_value)
73 }
74}
75
76pub struct Agent {
77 name: String,
78 model: Arc<dyn Model>,
79 system: Option<String>,
80 tools: Vec<Arc<dyn Tool>>,
81 budget: Budget,
82 policy: Arc<dyn Policy>,
83 journal: Arc<dyn Journal>,
84 max_tokens: u32,
85 temperature: Option<f32>,
86 provider_options: BTreeMap<String, Value>,
87 on_delta: Option<DeltaFn>,
88 tool_timeout: Option<Duration>,
89 finish: Option<FinishSchema>,
90 worker_id: String,
91 lease_ttl: Duration,
92}
93
94pub const DEFAULT_LEASE_TTL: Duration = Duration::from_secs(60);
96
97struct Lease {
99 lost: Arc<std::sync::atomic::AtomicBool>,
100 renewer: tokio::task::JoinHandle<()>,
101}
102
103impl Lease {
104 fn lost(&self) -> bool {
105 self.lost.load(std::sync::atomic::Ordering::SeqCst)
106 }
107}
108
109impl Drop for Lease {
110 fn drop(&mut self) {
111 self.renewer.abort();
112 }
113}
114
115struct FinishSchema {
117 schema: Value,
118 validator: jsonschema::Validator,
119 retries: u32,
120}
121
122impl FinishSchema {
123 fn check(&self, text: &str) -> Result<Value, String> {
125 let value = crate::output::extract_json(text).ok_or_else(|| "the answer is not valid JSON".to_owned())?;
126 let errors: Vec<String> = self
127 .validator
128 .iter_errors(&value)
129 .take(5)
130 .map(|e| {
131 let path = e.instance_path().to_string();
132 if path.is_empty() { e.to_string() } else { format!("{path}: {e}") }
133 })
134 .collect();
135 if errors.is_empty() { Ok(value) } else { Err(errors.join("; ")) }
136 }
137}
138
139pub const DEFAULT_TOOL_TIMEOUT: Duration = Duration::from_secs(120);
141
142type DeltaFn = Arc<dyn Fn(&StreamEvent) + Send + Sync>;
143
144impl Agent {
145 pub fn builder(name: impl Into<String>) -> AgentBuilder {
146 AgentBuilder {
147 name: name.into(),
148 model: None,
149 system: None,
150 tools: Vec::new(),
151 budget: Budget::default(),
152 policy: Arc::new(EffectPolicy::default()),
153 journal: Arc::new(InMemoryJournal::new()),
154 max_tokens: 4096,
155 temperature: None,
156 provider_options: BTreeMap::new(),
157 on_delta: None,
158 tool_timeout: Some(DEFAULT_TOOL_TIMEOUT),
159 finish_schema: None,
160 finish_retries: 1,
161 worker_id: None,
162 lease_ttl: DEFAULT_LEASE_TTL,
163 }
164 }
165
166 pub fn name(&self) -> &str {
167 &self.name
168 }
169
170 pub fn to_builder(&self) -> AgentBuilder {
173 AgentBuilder {
174 name: self.name.clone(),
175 model: Some(self.model.clone()),
176 system: self.system.clone(),
177 tools: self.tools.clone(),
178 budget: self.budget.clone(),
179 policy: self.policy.clone(),
180 journal: self.journal.clone(),
181 max_tokens: self.max_tokens,
182 temperature: self.temperature,
183 provider_options: self.provider_options.clone(),
184 on_delta: self.on_delta.clone(),
185 tool_timeout: self.tool_timeout,
186 finish_schema: self.finish.as_ref().map(|f| f.schema.clone()),
187 finish_retries: self.finish.as_ref().map_or(1, |f| f.retries),
188 worker_id: Some(self.worker_id.clone()),
189 lease_ttl: self.lease_ttl,
190 }
191 }
192
193 pub fn worker_id(&self) -> &str {
194 &self.worker_id
195 }
196
197 pub fn journal(&self) -> &Arc<dyn Journal> {
198 &self.journal
199 }
200
201 pub async fn run(&self, input: impl Into<String>) -> Result<RunOutcome, AgentError> {
203 self.run_message(Message::user(input)).await
204 }
205
206 pub async fn run_message(&self, input: Message) -> Result<RunOutcome, AgentError> {
207 self.start(uuid::Uuid::now_v7().to_string(), input).await
208 }
209
210 pub async fn run_with_id(&self, run_id: &str, input: impl Into<String>) -> Result<RunOutcome, AgentError> {
218 let plain = !run_id.is_empty()
221 && run_id.len() <= 128
222 && !run_id.starts_with('.')
223 && run_id.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'));
224 if !plain {
225 return Err(AgentError::Config(format!(
226 "run id `{run_id}` must be 1–128 characters of A–Z, a–z, 0–9, '-', '_' or '.', not starting with '.'"
227 )));
228 }
229 if !self.journal.load(run_id).await?.is_empty() {
230 return Err(AgentError::RunExists(run_id.to_owned()));
231 }
232 self.start(run_id.to_owned(), Message::user(input)).await
233 }
234
235 async fn start(&self, run_id: String, input: Message) -> Result<RunOutcome, AgentError> {
236 let first = Event {
237 run_id: run_id.clone(),
238 seq: 0,
239 at: Utc::now(),
240 kind: EventKind::RunStarted { agent: self.name.clone(), input },
241 };
242 match self.journal.append(&first).await {
245 Ok(()) => {}
246 Err(JournalError::Conflict { .. }) => return Err(AgentError::RunExists(run_id)),
247 Err(e) => return Err(e.into()),
248 }
249 let state = RunState::from_events(std::slice::from_ref(&first)).expect("one event");
250 tracing::info!(run_id = %run_id, agent = %self.name, "run started");
251 self.drive_leased(state).await
252 }
253
254 pub async fn resume(&self, run_id: &str) -> Result<RunOutcome, AgentError> {
261 let lease = self.take_lease(run_id).await?;
262 let result = match self.state(run_id).await {
264 Ok(state) => self.drive(state, &lease).await,
265 Err(e) => Err(e),
266 };
267 self.end_lease(run_id, lease).await;
268 result
269 }
270
271 async fn drive_leased(&self, state: RunState) -> Result<RunOutcome, AgentError> {
272 let run_id = state.run_id.clone();
273 let lease = self.take_lease(&run_id).await?;
274 let result = self.drive(state, &lease).await;
275 self.end_lease(&run_id, lease).await;
276 result
277 }
278
279 async fn take_lease(&self, run_id: &str) -> Result<Lease, AgentError> {
280 if let Err(holder) = self.journal.acquire_lease(run_id, &self.worker_id, self.lease_ttl).await? {
281 return Err(AgentError::Leased {
282 run_id: run_id.to_owned(),
283 owner: holder.owner,
284 expires_at: holder.expires_at,
285 });
286 }
287 let lost = Arc::new(std::sync::atomic::AtomicBool::new(false));
288 let (journal, owner, ttl, flag, id) =
289 (self.journal.clone(), self.worker_id.clone(), self.lease_ttl, lost.clone(), run_id.to_owned());
290 let renewer = tokio::spawn(async move {
291 let mut tick = tokio::time::interval((ttl / 3).max(Duration::from_millis(10)));
292 tick.tick().await;
293 loop {
294 tick.tick().await;
295 if !matches!(journal.renew_lease(&id, &owner, ttl).await, Ok(true)) {
296 tracing::warn!(run_id = %id, %owner, "lost run lease");
297 flag.store(true, std::sync::atomic::Ordering::SeqCst);
298 return;
299 }
300 }
301 });
302 Ok(Lease { lost, renewer })
303 }
304
305 async fn end_lease(&self, run_id: &str, lease: Lease) {
306 drop(lease);
307 if let Err(e) = self.journal.release_lease(run_id, &self.worker_id).await {
308 tracing::warn!(run_id, error = %e, "could not release run lease; it will expire");
309 }
310 }
311
312 pub async fn state(&self, run_id: &str) -> Result<RunState, AgentError> {
314 let events = self.journal.load(run_id).await?;
315 RunState::from_events(&events).ok_or_else(|| AgentError::UnknownRun(run_id.to_owned()))
316 }
317
318 pub async fn decide(
321 &self,
322 run_id: &str,
323 call_id: &str,
324 approved: bool,
325 by: Option<&str>,
326 note: Option<&str>,
327 ) -> Result<(), AgentError> {
328 let mut state = self.state(run_id).await?;
329 if !state.pending.iter().any(|p| p.id == call_id && p.awaiting_approval) {
330 return Err(AgentError::NotAwaitingApproval { run_id: run_id.to_owned(), call_id: call_id.to_owned() });
331 }
332 self.emit(
333 &mut state,
334 EventKind::ApprovalDecided {
335 call_id: call_id.to_owned(),
336 approved,
337 by: by.map(str::to_owned),
338 note: note.map(str::to_owned),
339 },
340 )
341 .await
342 }
343
344 async fn emit(&self, state: &mut RunState, kind: EventKind) -> Result<(), AgentError> {
345 let event = Event { run_id: state.run_id.clone(), seq: state.next_seq, at: Utc::now(), kind };
346 tracing::debug!(run_id = %event.run_id, seq = event.seq, event = event.kind.name(), "append");
347 self.journal.append(&event).await?;
348 state.apply(&event);
349 Ok(())
350 }
351
352 fn find_tool(&self, name: &str) -> Option<&Arc<dyn Tool>> {
353 self.tools.iter().find(|t| t.spec().name == name)
354 }
355
356 async fn drive(&self, mut state: RunState, lease: &Lease) -> Result<RunOutcome, AgentError> {
357 loop {
358 if state.status.is_terminal() {
359 return Ok(RunOutcome::from_state(&state));
360 }
361 if lease.lost() {
362 return Err(AgentError::LeaseLost { run_id: state.run_id.clone() });
363 }
364
365 if !state.pending.is_empty() {
366 self.handle_tool_calls(&mut state).await?;
367 if state.status == Status::AwaitingApproval {
368 return Ok(RunOutcome::from_state(&state));
369 }
370 continue;
371 }
372
373 if let Some(reason) = self.budget.exceeded(state.steps, state.usage.total(), state.cost_usd) {
374 self.emit(&mut state, EventKind::BudgetExceeded { reason }).await?;
375 continue;
376 }
377
378 self.model_turn(&mut state).await?;
379 }
380 }
381
382 async fn model_turn(&self, state: &mut RunState) -> Result<(), AgentError> {
383 let mut request = ModelRequest {
384 system: self.system_prompt(),
385 messages: state.messages.clone(),
386 tools: self.tools.iter().map(|t| t.spec().clone()).collect(),
387 max_tokens: self.max_tokens,
388 temperature: self.temperature,
389 provider_options: self.provider_options.clone(),
390 };
391 let est_input = estimate_input_tokens(&request);
394 match self.budget.output_cap(self.max_tokens, state.usage.total(), state.cost_usd, est_input, |i, o| {
395 self.model.max_call_cost(i, o)
396 }) {
397 Ok(cap) => request.max_tokens = cap,
398 Err(reason) => return self.emit(state, EventKind::BudgetExceeded { reason }).await,
399 }
400 let step = state.steps + 1;
401 let started = Instant::now();
402
403 let result = match &self.on_delta {
404 Some(on_delta) => self.stream_turn(&request, on_delta).await,
405 None => self.model.complete(&request).await,
406 };
407 match result {
408 Ok(response) => {
409 let cost_usd = self.model.cost_usd(&response);
410 let wants_tools = response.content.iter().any(|c| matches!(c, Content::ToolUse { .. }));
411 let text = content_text(&response.content);
412 let duration_ms = started.elapsed().as_millis() as u64;
413 self.emit(state, EventKind::ModelCalled { step, response, cost_usd, duration_ms }).await?;
414 if !wants_tools {
415 self.finish(state, step, text).await?;
416 }
417 }
418 Err(e) => {
419 let error = e.to_string();
420 let (usage, spent) = e.spent();
421 let cost_usd = spent.or_else(|| self.model.pricing().map(|p| p.cost(&usage)));
423 self.emit(state, EventKind::ModelFailed { step, error: error.clone(), usage, cost_usd }).await?;
424 self.emit(state, EventKind::RunFailed { error }).await?;
425 }
426 }
427 Ok(())
428 }
429
430 fn system_prompt(&self) -> Option<String> {
434 let Some(finish) = &self.finish else { return self.system.clone() };
435 let rule = format!(
436 "When you have finished, reply with only a JSON object (no other text) that matches this JSON Schema:\n{}",
437 finish.schema
438 );
439 Some(match &self.system {
440 Some(s) => format!("{s}\n\n{rule}"),
441 None => rule,
442 })
443 }
444
445 async fn finish(&self, state: &mut RunState, step: u32, text: String) -> Result<(), AgentError> {
447 let Some(finish) = &self.finish else {
448 return self.emit(state, EventKind::RunCompleted { output: text, structured: None }).await;
449 };
450 match finish.check(&text) {
451 Ok(value) => self.emit(state, EventKind::RunCompleted { output: text, structured: Some(value) }).await,
452 Err(reason) if state.finish_rejections < finish.retries => {
453 self.emit(state, EventKind::FinishRejected { step, reason }).await
454 }
455 Err(reason) => {
456 let error = format!(
457 "final answer did not match the finish schema after {} retr{}: {reason}",
458 finish.retries,
459 if finish.retries == 1 { "y" } else { "ies" }
460 );
461 self.emit(state, EventKind::RunFailed { error }).await
462 }
463 }
464 }
465
466 async fn stream_turn(&self, request: &ModelRequest, on_delta: &DeltaFn) -> Result<ModelResponse, ModelError> {
469 let mut stream = self.model.stream(request).await?;
470 while let Some(event) = stream.next().await {
471 match event? {
472 StreamEvent::Completed(response) => return Ok(response),
473 delta => on_delta(&delta),
474 }
475 }
476 Err(ModelError::Decode("stream ended before the response completed".into()))
477 }
478
479 async fn handle_tool_calls(&self, state: &mut RunState) -> Result<(), AgentError> {
480 for call in state.pending.clone() {
481 if call.awaiting_approval {
482 continue;
483 }
484 if let Some(&approved) = state.approvals.get(&call.id) {
485 if approved {
486 self.execute(state, &call).await?;
487 } else {
488 let kind = EventKind::ToolDenied {
489 call_id: call.id.clone(),
490 tool: call.name.clone(),
491 reason: "rejected by approver".into(),
492 };
493 self.emit(state, kind).await?;
494 }
495 continue;
496 }
497 let Some(tool) = self.find_tool(&call.name) else {
498 let kind = EventKind::ToolCompleted {
499 call_id: call.id.clone(),
500 tool: call.name.clone(),
501 output: json!({ "error": format!("unknown tool `{}`", call.name) }),
502 is_error: true,
503 duration_ms: 0,
504 };
505 self.emit(state, kind).await?;
506 continue;
507 };
508 let kind = match self.policy.decide(tool.spec(), &call.input) {
509 Decision::Allow => {
510 self.execute(state, &call).await?;
511 continue;
512 }
513 Decision::Deny { reason } => {
514 EventKind::ToolDenied { call_id: call.id.clone(), tool: call.name.clone(), reason }
515 }
516 Decision::RequireApproval { reason } => EventKind::ApprovalRequested {
517 call_id: call.id.clone(),
518 tool: call.name.clone(),
519 input: call.input.clone(),
520 reason,
521 },
522 };
523 self.emit(state, kind).await?;
524 }
525 Ok(())
526 }
527
528 async fn execute(&self, state: &mut RunState, call: &PendingCall) -> Result<(), AgentError> {
529 let tool = self.find_tool(&call.name).cloned();
530 let ctx = ToolCtx::new(&state.run_id, &call.id);
531 let started = Instant::now();
532 let (output, is_error) = match tool {
533 Some(tool) => {
534 let limit = tool.timeout().or(self.tool_timeout);
535 let fut = tool.call(call.input.clone(), &ctx);
536 let result = match limit {
537 Some(limit) => tokio::time::timeout(limit, fut).await.unwrap_or_else(|_| {
538 tracing::warn!(tool = %call.name, ?limit, "tool timed out");
539 Err(crate::tool::ToolError(format!(
540 "tool `{}` timed out after {:.1}s and was cancelled",
541 call.name,
542 limit.as_secs_f64()
543 )))
544 }),
545 None => fut.await,
546 };
547 match result {
548 Ok(v) => (v, false),
549 Err(e) => (json!({ "error": e.0 }), true),
550 }
551 }
552 None => (json!({ "error": format!("unknown tool `{}`", call.name) }), true),
553 };
554 let kind = EventKind::ToolCompleted {
555 call_id: call.id.clone(),
556 tool: call.name.clone(),
557 output,
558 is_error,
559 duration_ms: started.elapsed().as_millis() as u64,
560 };
561 self.emit(state, kind).await
562 }
563}
564
565fn estimate_input_tokens(request: &ModelRequest) -> u64 {
568 let chars = request.system.as_deref().map_or(0, str::len)
569 + serde_json::to_string(&request.messages).map_or(0, |s| s.len())
570 + serde_json::to_string(&request.tools).map_or(0, |s| s.len());
571 (chars as u64).div_ceil(3)
572}
573
574pub struct AgentBuilder {
575 name: String,
576 model: Option<Arc<dyn Model>>,
577 system: Option<String>,
578 tools: Vec<Arc<dyn Tool>>,
579 budget: Budget,
580 policy: Arc<dyn Policy>,
581 journal: Arc<dyn Journal>,
582 max_tokens: u32,
583 temperature: Option<f32>,
584 provider_options: BTreeMap<String, Value>,
585 on_delta: Option<DeltaFn>,
586 tool_timeout: Option<Duration>,
587 finish_schema: Option<Value>,
588 finish_retries: u32,
589 worker_id: Option<String>,
590 lease_ttl: Duration,
591}
592
593impl AgentBuilder {
594 pub fn model(mut self, model: impl Model + 'static) -> Self {
595 self.model = Some(Arc::new(model));
596 self
597 }
598
599 pub fn replace_tools(mut self, tools: Vec<Arc<dyn Tool>>) -> Self {
601 self.tools = tools;
602 self
603 }
604
605 pub(crate) fn tool_list(&self) -> &[Arc<dyn Tool>] {
606 &self.tools
607 }
608
609 pub fn without_on_delta(mut self) -> Self {
611 self.on_delta = None;
612 self
613 }
614
615 pub fn system(mut self, prompt: impl Into<String>) -> Self {
616 self.system = Some(prompt.into());
617 self
618 }
619
620 pub fn tool(mut self, tool: impl Tool + 'static) -> Self {
621 self.tools.push(Arc::new(tool));
622 self
623 }
624
625 pub fn tools<T: Tool + 'static>(mut self, tools: impl IntoIterator<Item = T>) -> Self {
627 self.tools.extend(tools.into_iter().map(|t| Arc::new(t) as Arc<dyn Tool>));
628 self
629 }
630
631 pub fn budget(mut self, budget: Budget) -> Self {
632 self.budget = budget;
633 self
634 }
635
636 pub fn policy(mut self, policy: impl Policy + 'static) -> Self {
637 self.policy = Arc::new(policy);
638 self
639 }
640
641 pub fn journal(mut self, journal: impl Journal + 'static) -> Self {
642 self.journal = Arc::new(journal);
643 self
644 }
645
646 pub fn shared_journal(mut self, journal: Arc<dyn Journal>) -> Self {
647 self.journal = journal;
648 self
649 }
650
651 pub fn max_tokens(mut self, n: u32) -> Self {
652 self.max_tokens = n;
653 self
654 }
655
656 pub fn temperature(mut self, t: f32) -> Self {
657 self.temperature = Some(t);
658 self
659 }
660
661 pub fn provider_option(mut self, provider: &str, options: Value) -> Self {
664 self.provider_options.insert(provider.to_owned(), options);
665 self
666 }
667
668 pub fn on_delta(mut self, f: impl Fn(&StreamEvent) + Send + Sync + 'static) -> Self {
671 self.on_delta = Some(Arc::new(f));
672 self
673 }
674
675 pub fn tool_timeout(mut self, limit: Duration) -> Self {
679 self.tool_timeout = Some(limit);
680 self
681 }
682
683 pub fn finish_schema(mut self, schema: Value) -> Self {
689 self.finish_schema = Some(schema);
690 self
691 }
692
693 pub fn finish_as<T: schemars::JsonSchema>(self) -> Self {
695 let mut schema = serde_json::to_value(schemars::schema_for!(T)).unwrap_or(Value::Null);
696 if let Value::Object(map) = &mut schema {
697 map.remove("$schema");
698 }
699 self.finish_schema(schema)
700 }
701
702 pub fn worker_id(mut self, id: impl Into<String>) -> Self {
704 self.worker_id = Some(id.into());
705 self
706 }
707
708 pub fn lease_ttl(mut self, ttl: Duration) -> Self {
711 self.lease_ttl = ttl;
712 self
713 }
714
715 pub fn finish_retries(mut self, n: u32) -> Self {
717 self.finish_retries = n;
718 self
719 }
720
721 pub fn without_tool_timeout(mut self) -> Self {
723 self.tool_timeout = None;
724 self
725 }
726
727 pub fn build(self) -> Result<Agent, AgentError> {
728 let model = self.model.ok_or_else(|| AgentError::Config("a model is required".into()))?;
729 if self.budget.max_usd.is_some() {
730 let unpriced = model.unpriced_models();
731 if !unpriced.is_empty() {
732 return Err(AgentError::Config(format!(
733 "max_usd is set but these models have no pricing, so spend could not be counted: {}. \
734 Set pricing (e.g. Priced::new(model, Pricing::new(input, output))); \
735 use Pricing::new(0.0, 0.0) for a model that is genuinely free",
736 unpriced.join(", ")
737 )));
738 }
739 }
740 let finish = match self.finish_schema {
741 Some(schema) => {
742 let validator = jsonschema::validator_for(&schema)
743 .map_err(|e| AgentError::Config(format!("invalid finish schema: {e}")))?;
744 Some(FinishSchema { schema, validator, retries: self.finish_retries })
745 }
746 None => None,
747 };
748 let mut seen = std::collections::HashSet::new();
749 for t in &self.tools {
750 if !seen.insert(t.spec().name.clone()) {
751 return Err(AgentError::Config(format!("duplicate tool name `{}`", t.spec().name)));
752 }
753 }
754 Ok(Agent {
755 name: self.name,
756 model,
757 system: self.system,
758 tools: self.tools,
759 budget: self.budget,
760 policy: self.policy,
761 journal: self.journal,
762 max_tokens: self.max_tokens,
763 temperature: self.temperature,
764 provider_options: self.provider_options,
765 on_delta: self.on_delta,
766 tool_timeout: self.tool_timeout,
767 finish,
768 worker_id: self.worker_id.unwrap_or_else(|| format!("worker-{}", uuid::Uuid::now_v7())),
769 lease_ttl: self.lease_ttl,
770 })
771 }
772}