1#![cfg(any(test, feature = "test-utils"))]
13#![allow(missing_docs)] use crate::agent::AgentContext;
16use crate::bus::{
17 ClaimHandleImpl, ClaimedJob, Headers, Job, JobQueue, KvEntry, KvStore, Lease, LeaseImpl, Msg,
18 MsgStream, Pubsub, RequestReply, Revision,
19};
20use crate::error::{BusError, LlmError, MemoryError, ToolError};
21use crate::ids::{DurableName, FactId, JobId, RunId, ThreadId};
22use crate::llm::{
23 Capabilities, ChatChunk, ChatRequest, ChatResponse, ChunkStream, Embedding, FinishReason,
24 LlmClient, Message, ToolCall,
25};
26use crate::memory::{
27 Episode, EpisodicMemory, Fact, LongTermMemory, RunFilter, RunSummary, Scope, ShortTermMemory,
28};
29use crate::runtime::{run_steps, RunOptions};
30use crate::tool::{ToolCtx, ToolInvoker};
31use async_trait::async_trait;
32use bytes::Bytes;
33use chrono::Utc;
34use futures_core::Stream;
35use std::collections::HashMap;
36use std::pin::Pin;
37use std::sync::Arc;
38use std::task::{Context, Poll};
39use std::time::Duration;
40use tokio::sync::Mutex;
41use tokio_util::sync::CancellationToken;
42
43#[derive(Clone)]
47pub enum FakeLlmStep {
48 Text(String),
50 ToolCalls(Vec<ToolCall>),
52}
53
54pub enum FakeStreamStep {
56 Chunks(Vec<Result<ChatChunk, LlmError>>),
59 InitErr(Option<LlmError>),
63}
64
65impl FakeStreamStep {
66 pub fn init_err(e: LlmError) -> Self {
71 Self::InitErr(Some(e))
72 }
73}
74
75#[derive(Default)]
76pub struct FakeLlmClient {
77 name: String,
78 caps: Capabilities,
79 script: Mutex<std::collections::VecDeque<FakeLlmStep>>,
80 stream_script: Mutex<std::collections::VecDeque<FakeStreamStep>>,
81 stream_calls: std::sync::atomic::AtomicU32,
82}
83
84impl FakeLlmClient {
85 pub fn new(name: impl Into<String>) -> Self {
86 Self {
87 name: name.into(),
88 caps: Capabilities {
89 tool_calling: true,
90 streaming: false,
91 structured_output: false,
92 embeddings: false,
93 max_context_tokens: 32_000,
94 vision: false,
95 },
96 script: Mutex::new(std::collections::VecDeque::new()),
97 stream_script: Mutex::new(std::collections::VecDeque::new()),
98 stream_calls: std::sync::atomic::AtomicU32::new(0),
99 }
100 }
101
102 pub fn with_steps(self, steps: Vec<FakeLlmStep>) -> Self {
103 let mut new_q = std::collections::VecDeque::new();
104 new_q.extend(steps);
105 Self {
106 script: Mutex::new(new_q),
107 ..self
108 }
109 }
110
111 pub fn with_stream_steps(self, steps: Vec<FakeStreamStep>) -> Self {
114 let mut new_q = std::collections::VecDeque::new();
115 new_q.extend(steps);
116 Self {
117 stream_script: Mutex::new(new_q),
118 ..self
119 }
120 }
121
122 pub fn stream_call_count(&self) -> u32 {
124 self.stream_calls.load(std::sync::atomic::Ordering::SeqCst)
125 }
126}
127
128#[async_trait]
129impl LlmClient for FakeLlmClient {
130 fn name(&self) -> &str {
131 &self.name
132 }
133
134 fn capabilities(&self) -> &Capabilities {
135 &self.caps
136 }
137
138 async fn complete(&self, _req: ChatRequest) -> Result<ChatResponse, LlmError> {
139 let mut q = self.script.lock().await;
140 let step = q
141 .pop_front()
142 .ok_or_else(|| LlmError::BadRequest("FakeLlmClient: script exhausted".into()))?;
143 let (msg, finish) = match step {
144 FakeLlmStep::Text(s) => (
145 Message {
146 role: crate::llm::Role::Assistant,
147 content: s,
148 tool_calls: vec![],
149 tool_call_id: None,
150 },
151 FinishReason::Stop,
152 ),
153 FakeLlmStep::ToolCalls(calls) => (
154 Message {
155 role: crate::llm::Role::Assistant,
156 content: String::new(),
157 tool_calls: calls,
158 tool_call_id: None,
159 },
160 FinishReason::ToolCalls,
161 ),
162 };
163 Ok(ChatResponse::new(
164 msg,
165 Default::default(),
166 finish,
167 self.name.clone(),
168 ))
169 }
170
171 async fn stream(&self, _req: ChatRequest) -> Result<ChunkStream, LlmError> {
172 self.stream_calls
173 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
174 let mut q = self.stream_script.lock().await;
175 let step = q
176 .pop_front()
177 .ok_or_else(|| LlmError::Unsupported("streaming".into()))?;
178 match step {
179 FakeStreamStep::Chunks(items) => Ok(Box::pin(tokio_stream::iter(items))),
180 FakeStreamStep::InitErr(mut slot) => {
181 let e = slot
182 .take()
183 .unwrap_or_else(|| LlmError::Server("fake init err already taken".into()));
184 Err(e)
185 }
186 }
187 }
188
189 async fn embed(&self, _texts: &[String]) -> Result<Vec<Embedding>, LlmError> {
190 Err(LlmError::Unsupported("embeddings".into()))
191 }
192}
193
194pub struct ScriptedLlm {
201 capabilities: Capabilities,
202 replies: Vec<String>,
203 calls: std::sync::atomic::AtomicUsize,
204}
205
206impl ScriptedLlm {
207 pub fn new(replies: Vec<String>) -> Self {
209 Self {
210 capabilities: Capabilities::default(),
211 replies,
212 calls: std::sync::atomic::AtomicUsize::new(0),
213 }
214 }
215}
216
217#[async_trait]
218impl LlmClient for ScriptedLlm {
219 fn name(&self) -> &str {
220 "scripted"
221 }
222 fn capabilities(&self) -> &Capabilities {
223 &self.capabilities
224 }
225 async fn complete(&self, _req: ChatRequest) -> Result<ChatResponse, LlmError> {
226 let index = self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
227 let content = self
228 .replies
229 .get(index)
230 .or_else(|| self.replies.last())
231 .cloned()
232 .unwrap_or_default();
233 Ok(ChatResponse::new(
234 Message {
235 role: crate::llm::Role::Assistant,
236 content,
237 tool_calls: vec![],
238 tool_call_id: None,
239 },
240 Default::default(),
241 FinishReason::Stop,
242 "scripted",
243 ))
244 }
245 async fn stream(&self, _req: ChatRequest) -> Result<ChunkStream, LlmError> {
246 Err(LlmError::Unsupported(
247 "scripted llm has no streaming".into(),
248 ))
249 }
250 async fn embed(&self, _texts: &[String]) -> Result<Vec<Embedding>, LlmError> {
251 Err(LlmError::Unsupported(
252 "scripted llm has no embeddings".into(),
253 ))
254 }
255}
256
257type ToolHandler =
260 Arc<dyn Fn(serde_json::Value) -> Result<serde_json::Value, ToolError> + Send + Sync>;
261
262#[derive(Default)]
263pub struct FakeToolInvoker {
264 handlers: HashMap<String, ToolHandler>,
265 catalogue: Vec<crate::llm::ToolDef>,
266 idempotent: std::collections::HashSet<String>,
267 redacting: std::collections::HashSet<String>,
268}
269
270impl FakeToolInvoker {
271 pub fn new() -> Self {
272 Self::default()
273 }
274
275 pub fn with_tool<F>(mut self, name: &str, description: &str, handler: F) -> Self
276 where
277 F: Fn(serde_json::Value) -> Result<serde_json::Value, ToolError> + Send + Sync + 'static,
278 {
279 self.handlers.insert(name.to_string(), Arc::new(handler));
280 self.catalogue.push(crate::llm::ToolDef {
281 name: name.to_string(),
282 description: description.to_string(),
283 json_schema: serde_json::json!({"type": "object"}),
284 });
285 self
286 }
287
288 pub fn with_idempotent_tool<F>(mut self, name: &str, description: &str, handler: F) -> Self
291 where
292 F: Fn(serde_json::Value) -> Result<serde_json::Value, ToolError> + Send + Sync + 'static,
293 {
294 self.idempotent.insert(name.to_string());
295 self.with_tool(name, description, handler)
296 }
297
298 pub fn with_redacting_tool<F>(mut self, name: &str, description: &str, handler: F) -> Self
302 where
303 F: Fn(serde_json::Value) -> Result<serde_json::Value, ToolError> + Send + Sync + 'static,
304 {
305 self.redacting.insert(name.to_string());
306 self.with_tool(name, description, handler)
307 }
308}
309
310#[async_trait]
311impl ToolInvoker for FakeToolInvoker {
312 async fn invoke(
313 &self,
314 name: &str,
315 args: serde_json::Value,
316 _ctx: ToolCtx,
317 ) -> Result<serde_json::Value, ToolError> {
318 let h = self
319 .handlers
320 .get(name)
321 .ok_or_else(|| ToolError::UnknownTool(name.into()))?;
322 h(args)
323 }
324
325 fn catalogue(&self) -> Vec<crate::llm::ToolDef> {
326 self.catalogue.clone()
327 }
328
329 fn is_tool_idempotent(&self, name: &str) -> bool {
330 self.idempotent.contains(name)
331 }
332
333 fn tool_redacts_audit(&self, name: &str) -> bool {
334 self.redacting.contains(name)
335 }
336}
337
338#[derive(Default)]
341pub struct InMemoryShortTerm {
342 inner: Mutex<HashMap<String, Vec<Message>>>,
343}
344
345#[async_trait]
346impl ShortTermMemory for InMemoryShortTerm {
347 async fn append(&self, thread: ThreadId, msg: Message) -> Result<(), MemoryError> {
348 self.inner
349 .lock()
350 .await
351 .entry(thread.0)
352 .or_default()
353 .push(msg);
354 Ok(())
355 }
356
357 async fn load(
358 &self,
359 thread: ThreadId,
360 _max_tokens: usize,
361 ) -> Result<Vec<Message>, MemoryError> {
362 Ok(self
363 .inner
364 .lock()
365 .await
366 .get(&thread.0)
367 .cloned()
368 .unwrap_or_default())
369 }
370
371 async fn clear(&self, thread: ThreadId) -> Result<(), MemoryError> {
372 self.inner.lock().await.remove(&thread.0);
373 Ok(())
374 }
375}
376
377#[derive(Default)]
380pub struct InMemoryLongTerm {
381 inner: Mutex<Vec<(FactId, Scope, Fact)>>,
382 counter: Mutex<u64>,
383}
384
385#[async_trait]
386impl LongTermMemory for InMemoryLongTerm {
387 async fn remember(&self, scope: Scope, fact: Fact) -> Result<FactId, MemoryError> {
388 let mut c = self.counter.lock().await;
389 *c += 1;
390 let id = FactId(format!("fake-{}", *c));
391 self.inner.lock().await.push((id.clone(), scope, fact));
392 Ok(id)
393 }
394
395 async fn recall(&self, scope: Scope, query: &str, k: usize) -> Result<Vec<Fact>, MemoryError> {
396 let q = query.to_lowercase();
397 Ok(self
398 .inner
399 .lock()
400 .await
401 .iter()
402 .filter(|(_, s, _)| *s == scope)
403 .filter(|(_, _, f)| f.text.to_lowercase().contains(&q))
404 .take(k)
405 .map(|(_, _, f)| f.clone())
406 .collect())
407 }
408
409 async fn forget(&self, id: FactId) -> Result<(), MemoryError> {
410 self.inner.lock().await.retain(|(i, _, _)| i != &id);
411 Ok(())
412 }
413}
414
415#[derive(Default)]
418pub struct InMemoryEpisodic {
419 inner: Mutex<HashMap<RunId, Vec<Episode>>>,
420}
421
422#[async_trait]
423impl EpisodicMemory for InMemoryEpisodic {
424 async fn record(&self, run: RunId, event: Episode) -> Result<(), MemoryError> {
425 self.inner.lock().await.entry(run).or_default().push(event);
426 Ok(())
427 }
428
429 async fn replay(&self, run: RunId) -> Result<Vec<Episode>, MemoryError> {
430 Ok(self
431 .inner
432 .lock()
433 .await
434 .get(&run)
435 .cloned()
436 .unwrap_or_default())
437 }
438
439 async fn list_runs(&self, _filter: RunFilter) -> Result<Vec<RunSummary>, MemoryError> {
440 let g = self.inner.lock().await;
441 Ok(g.iter()
442 .map(|(id, eps)| RunSummary {
443 run_id: *id,
444 agent: String::new(),
445 started_at: Utc::now(),
446 finished_at: None,
447 episode_count: eps.len() as u32,
448 })
449 .collect())
450 }
451}
452
453struct EmptyStream;
457
458impl Stream for EmptyStream {
459 type Item = Result<Msg, BusError>;
460
461 fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
462 Poll::Ready(None)
463 }
464}
465
466pub struct NoopPubsub;
467
468#[async_trait]
469impl Pubsub for NoopPubsub {
470 async fn publish(
471 &self,
472 _subject: &str,
473 _payload: Bytes,
474 _headers: Headers,
475 ) -> Result<(), BusError> {
476 Ok(())
477 }
478
479 async fn subscribe(
480 &self,
481 _subject: &str,
482 _durable: DurableName,
483 ) -> Result<MsgStream, BusError> {
484 let s = EmptyStream;
485 Ok(Box::pin(s))
486 }
487}
488
489pub struct NoopRequestReply;
490
491#[async_trait]
492impl RequestReply for NoopRequestReply {
493 async fn request(
494 &self,
495 _subject: &str,
496 _payload: Bytes,
497 _timeout: Duration,
498 ) -> Result<Bytes, BusError> {
499 Err(BusError::NotFound("noop bus".into()))
500 }
501}
502
503pub struct NoopKv;
504
505struct NoopLease;
506#[async_trait]
507impl LeaseImpl for NoopLease {
508 async fn heartbeat(&self) -> Result<(), BusError> {
509 Ok(())
510 }
511}
512
513#[async_trait]
514impl KvStore for NoopKv {
515 async fn get(&self, _b: &str, _k: &str) -> Result<Option<KvEntry>, BusError> {
516 Ok(None)
517 }
518
519 async fn put(&self, _b: &str, _k: &str, _v: Bytes) -> Result<Revision, BusError> {
520 Ok(1)
521 }
522
523 async fn cas(
524 &self,
525 _b: &str,
526 _k: &str,
527 _v: Bytes,
528 _expected: Option<Revision>,
529 ) -> Result<Revision, BusError> {
530 Ok(1)
531 }
532
533 async fn delete(&self, _b: &str, _k: &str) -> Result<(), BusError> {
534 Ok(())
535 }
536
537 async fn lease(&self, _b: &str, _k: &str, _ttl: Duration) -> Result<Lease, BusError> {
538 Ok(Lease::new(Box::new(NoopLease)))
539 }
540}
541
542#[derive(Clone, Debug)]
545struct FakeKvEntry {
546 value: Bytes,
547 revision: Revision,
548}
549
550#[derive(Default)]
554pub struct FakeKvStore {
555 inner: Mutex<std::collections::HashMap<(String, String), FakeKvEntry>>,
556}
557
558impl FakeKvStore {
559 pub fn new() -> Self {
561 Self::default()
562 }
563}
564
565struct FakeKvLease;
566
567#[async_trait]
568impl LeaseImpl for FakeKvLease {
569 async fn heartbeat(&self) -> Result<(), BusError> {
570 Ok(())
571 }
572}
573
574#[async_trait]
575impl KvStore for FakeKvStore {
576 async fn get(&self, bucket: &str, key: &str) -> Result<Option<KvEntry>, BusError> {
577 let g = self.inner.lock().await;
578 Ok(g.get(&(bucket.into(), key.into())).map(|e| KvEntry {
579 value: e.value.clone(),
580 revision: e.revision,
581 }))
582 }
583
584 async fn put(&self, bucket: &str, key: &str, value: Bytes) -> Result<Revision, BusError> {
585 let mut g = self.inner.lock().await;
586 let map_key = (bucket.into(), key.into());
587 let next_rev = g.get(&map_key).map(|e| e.revision + 1).unwrap_or(1);
588 g.insert(
589 map_key,
590 FakeKvEntry {
591 value,
592 revision: next_rev,
593 },
594 );
595 Ok(next_rev)
596 }
597
598 async fn cas(
599 &self,
600 bucket: &str,
601 key: &str,
602 value: Bytes,
603 expected: Option<Revision>,
604 ) -> Result<Revision, BusError> {
605 let mut g = self.inner.lock().await;
606 let map_key: (String, String) = (bucket.into(), key.into());
607 let existing = g.get(&map_key);
608 match (expected, existing) {
609 (None, None) => {
610 g.insert(map_key, FakeKvEntry { value, revision: 1 });
611 Ok(1)
612 }
613 (None, Some(e)) => Err(BusError::CasConflict {
614 expected: 0,
615 actual: e.revision,
616 }),
617 (Some(want), Some(e)) if e.revision == want => {
618 let next = e.revision + 1;
619 g.insert(
620 map_key,
621 FakeKvEntry {
622 value,
623 revision: next,
624 },
625 );
626 Ok(next)
627 }
628 (Some(want), Some(e)) => Err(BusError::CasConflict {
629 expected: want,
630 actual: e.revision,
631 }),
632 (Some(want), None) => Err(BusError::CasConflict {
633 expected: want,
634 actual: 0,
635 }),
636 }
637 }
638
639 async fn delete(&self, bucket: &str, key: &str) -> Result<(), BusError> {
640 self.inner.lock().await.remove(&(bucket.into(), key.into()));
641 Ok(())
642 }
643
644 async fn lease(&self, _b: &str, _k: &str, _ttl: Duration) -> Result<Lease, BusError> {
645 Ok(Lease::new(Box::new(FakeKvLease)))
646 }
647
648 async fn keys(&self, bucket: &str) -> Result<Vec<String>, BusError> {
649 let g = self.inner.lock().await;
650 Ok(g.keys()
651 .filter(|(b, _)| b == bucket)
652 .map(|(_, k)| k.clone())
653 .collect())
654 }
655}
656
657pub fn fake_kv() -> Arc<dyn KvStore> {
660 Arc::new(FakeKvStore::new())
661}
662
663pub struct NoopJobQueue;
664
665#[allow(dead_code)]
666struct NoopClaim;
667
668#[async_trait]
669impl ClaimHandleImpl for NoopClaim {
670 async fn ack(self: Box<Self>) -> Result<(), BusError> {
671 Ok(())
672 }
673 async fn nak(self: Box<Self>, _delay: Duration) -> Result<(), BusError> {
674 Ok(())
675 }
676 async fn dead_letter(self: Box<Self>, _reason: &str) -> Result<(), BusError> {
677 Ok(())
678 }
679}
680
681#[async_trait]
682impl JobQueue for NoopJobQueue {
683 async fn enqueue(&self, _queue: &str, _job: Job) -> Result<JobId, BusError> {
684 Ok(JobId("noop-0".into()))
685 }
686
687 async fn claim(
688 &self,
689 _queue: &str,
690 _worker_id: &str,
691 _ttl: Duration,
692 ) -> Result<Option<ClaimedJob>, BusError> {
693 Ok(None)
694 }
695}
696
697pub type NoopBusHandles = (
699 Arc<dyn Pubsub>,
700 Arc<dyn RequestReply>,
701 Arc<dyn KvStore>,
702 Arc<dyn JobQueue>,
703);
704
705pub fn noop_bus() -> NoopBusHandles {
708 (
709 Arc::new(NoopPubsub),
710 Arc::new(NoopRequestReply),
711 Arc::new(NoopKv),
712 Arc::new(NoopJobQueue),
713 )
714}
715
716pub fn fake_context(agent_name: impl Into<String>) -> AgentContext {
743 let (pubsub, request_reply, kv, jobs) = noop_bus();
744 AgentContext {
745 llm: Arc::new(FakeLlmClient::new("fake")),
746 short_term: Arc::new(InMemoryShortTerm::default()),
747 long_term: Arc::new(InMemoryLongTerm::default()),
748 episodic: Arc::new(InMemoryEpisodic::default()),
749 pubsub,
750 kv,
751 request_reply,
752 jobs,
753 tools: Arc::new(FakeToolInvoker::new()),
754 run_id: RunId::new(),
755 cancel: CancellationToken::new(),
756 agent_name: agent_name.into(),
757 progress: None,
758 audit_redactor: None,
759 tenant_label: None,
760 parent_anchor: None,
761 }
762}
763
764pub struct TestContext {
787 llm: Arc<FakeLlmClient>,
788 tools: Arc<dyn ToolInvoker>,
789 short_term: Arc<dyn ShortTermMemory>,
790 long_term: Arc<dyn LongTermMemory>,
791 episodic: Arc<dyn EpisodicMemory>,
792 pubsub: Arc<dyn Pubsub>,
793 request_reply: Arc<dyn RequestReply>,
794 kv: Arc<dyn KvStore>,
795 jobs: Arc<dyn JobQueue>,
796 cancel: CancellationToken,
797 run_id: RunId,
798 agent_name: String,
799 system_prompt: String,
800 run_options: RunOptions,
801 seeded_history: Vec<Message>,
802 pending_tools: Vec<(String, String, ToolHandler)>,
807}
808
809impl Default for TestContext {
810 fn default() -> Self {
811 let (pubsub, request_reply, kv, jobs) = noop_bus();
812 Self {
813 llm: Arc::new(FakeLlmClient::new("test")),
814 tools: Arc::new(FakeToolInvoker::new()),
815 short_term: Arc::new(InMemoryShortTerm::default()),
816 long_term: Arc::new(InMemoryLongTerm::default()),
817 episodic: Arc::new(InMemoryEpisodic::default()),
818 pubsub,
819 request_reply,
820 kv,
821 jobs,
822 cancel: CancellationToken::new(),
823 run_id: RunId::new(),
824 agent_name: "test-agent".into(),
825 system_prompt: String::new(),
826 run_options: RunOptions::default(),
827 seeded_history: Vec::new(),
828 pending_tools: Vec::new(),
829 }
830 }
831}
832
833impl TestContext {
834 pub fn with_canned_llm_responses(mut self, steps: Vec<FakeLlmStep>) -> Self {
837 self.llm = Arc::new(FakeLlmClient::new(self.llm.name().to_string()).with_steps(steps));
838 self
839 }
840
841 pub fn with_canned_stream_responses(mut self, steps: Vec<FakeStreamStep>) -> Self {
844 self.llm =
845 Arc::new(FakeLlmClient::new(self.llm.name().to_string()).with_stream_steps(steps));
846 self
847 }
848
849 pub fn with_tool<F>(mut self, name: &str, description: &str, f: F) -> Self
852 where
853 F: Fn(serde_json::Value) -> Result<serde_json::Value, ToolError> + Send + Sync + 'static,
854 {
855 self.pending_tools
856 .push((name.into(), description.into(), Arc::new(f)));
857 self
858 }
859
860 pub fn with_short_term_history(mut self, messages: Vec<Message>) -> Self {
864 self.seeded_history = messages;
865 self
866 }
867
868 pub fn with_run_options(mut self, opts: RunOptions) -> Self {
870 self.run_options = opts;
871 self
872 }
873
874 pub fn with_system_prompt(mut self, prompt: &str) -> Self {
876 self.system_prompt = prompt.into();
877 self
878 }
879
880 pub fn with_agent_name(mut self, name: &str) -> Self {
882 self.agent_name = name.into();
883 self
884 }
885
886 pub fn cancel_token(&self) -> &CancellationToken {
888 &self.cancel
889 }
890
891 pub fn run_id(&self) -> RunId {
893 self.run_id
894 }
895
896 pub fn llm(&self) -> &FakeLlmClient {
899 &self.llm
900 }
901
902 pub async fn recorded_episodes(&self) -> Vec<Episode> {
904 self.episodic.replay(self.run_id).await.unwrap_or_default()
905 }
906
907 async fn materialize(&mut self) -> Result<(), crate::Error> {
910 if !self.pending_tools.is_empty() {
911 let mut inv = FakeToolInvoker::new();
915 for (name, desc, handler) in self.pending_tools.drain(..) {
916 inv = inv.with_tool(&name, &desc, move |args| handler(args));
917 }
918 self.tools = Arc::new(inv);
919 }
920 if !self.seeded_history.is_empty() {
921 let thread = ThreadId::new("test");
922 for msg in self.seeded_history.drain(..) {
923 self.short_term.append(thread.clone(), msg).await?;
924 }
925 }
926 Ok(())
927 }
928
929 fn build_agent_ctx(&self) -> AgentContext {
931 AgentContext {
932 llm: self.llm.clone() as Arc<dyn LlmClient>,
933 short_term: self.short_term.clone(),
934 long_term: self.long_term.clone(),
935 episodic: self.episodic.clone(),
936 pubsub: self.pubsub.clone(),
937 kv: self.kv.clone(),
938 request_reply: self.request_reply.clone(),
939 jobs: self.jobs.clone(),
940 tools: self.tools.clone(),
941 run_id: self.run_id,
942 cancel: self.cancel.clone(),
943 agent_name: self.agent_name.clone(),
944 progress: None,
945 audit_redactor: None,
946 tenant_label: None,
947 parent_anchor: None,
948 }
949 }
950
951 pub async fn run<A>(&mut self, agent: &A, input: A::Input) -> Result<A::Output, A::Error>
957 where
958 A: crate::Agent<Error = crate::Error>,
959 {
960 self.materialize().await?;
961 let ctx = self.build_agent_ctx();
962 agent.run(ctx, input).await
963 }
964
965 pub async fn run_steps_directly(&mut self) -> Result<String, crate::Error> {
970 self.materialize().await?;
971 let ctx = self.build_agent_ctx();
972 let thread = ThreadId::new("test");
973 run_steps(&ctx, &self.system_prompt, thread, self.run_options.clone()).await
974 }
975}