1use std::collections::HashMap;
6
7use everruns_capability::plugin_capability_id;
8use everruns_core::network_access::NetworkAccessList;
9use everruns_core::{
10 AgentDefinition, DEFAULT_ORG_PUBLIC_ID, ExecutionSession, HarnessDefinition, ScopedMcpServers,
11 SessionExecutionState,
12};
13const OPENROUTER_HTTP_REFERER_METADATA_KEY: &str = "openrouter.http_referer";
17const OPENROUTER_X_TITLE_METADATA_KEY: &str = "openrouter.x_title";
18use everruns_provider::tool_types::ToolDefinition;
19use everruns_provider::typed_id::{AgentId, HarnessId, ModelId, SessionId, WorkspaceId};
20
21#[derive(Debug, Clone)]
27pub struct SeededHarness {
28 pub id: HarnessId,
30 pub definition: HarnessDefinition,
32}
33
34#[derive(Debug, Clone)]
42pub struct HarnessBuilder {
43 id: HarnessId,
44 name: String,
45 system_prompt: String,
46 default_model_id: Option<ModelId>,
47 capabilities: Vec<everruns_capability::CapabilityRef>,
48 initial_files: Vec<everruns_core::InitialFile>,
49 network_access: Option<NetworkAccessList>,
50 parallel_tool_calls: Option<bool>,
51 mcp_servers: ScopedMcpServers,
52 embedder_metadata: HashMap<String, String>,
53}
54
55impl HarnessBuilder {
56 pub fn new(name: impl Into<String>, system_prompt: impl Into<String>) -> Self {
58 Self {
59 id: HarnessId::new(),
60 name: name.into(),
61 system_prompt: system_prompt.into(),
62 default_model_id: None,
63 capabilities: Vec::new(),
64 initial_files: Vec::new(),
65 network_access: None,
66 parallel_tool_calls: None,
67 mcp_servers: ScopedMcpServers::default(),
68 embedder_metadata: HashMap::new(),
69 }
70 }
71
72 pub fn id(mut self, id: HarnessId) -> Self {
74 self.id = id;
75 self
76 }
77
78 pub fn harness_id(&self) -> HarnessId {
80 self.id
81 }
82
83 pub fn name(mut self, name: impl Into<String>) -> Self {
84 self.name = name.into();
85 self
86 }
87
88 pub fn system_prompt(mut self, system_prompt: impl Into<String>) -> Self {
89 self.system_prompt = system_prompt.into();
90 self
91 }
92
93 pub fn default_model_id(mut self, default_model_id: ModelId) -> Self {
94 self.default_model_id = Some(default_model_id);
95 self
96 }
97
98 pub fn capability(mut self, capability: impl Into<everruns_capability::CapabilityRef>) -> Self {
99 self.capabilities.push(capability.into());
100 self
101 }
102
103 pub fn with_capability(
104 self,
105 capability: impl Into<everruns_capability::CapabilityRef>,
106 ) -> Self {
107 self.capability(capability)
108 }
109
110 pub fn capabilities<I, C>(mut self, capabilities: I) -> Self
111 where
112 I: IntoIterator<Item = C>,
113 C: Into<everruns_capability::CapabilityRef>,
114 {
115 self.capabilities
116 .extend(capabilities.into_iter().map(Into::into));
117 self
118 }
119
120 pub fn initial_file(mut self, file: everruns_core::InitialFile) -> Self {
121 self.initial_files.push(file);
122 self
123 }
124
125 pub fn network_access(mut self, network_access: NetworkAccessList) -> Self {
126 self.network_access = Some(network_access);
127 self
128 }
129
130 pub fn parallel_tool_calls(mut self, parallel_tool_calls: bool) -> Self {
132 self.parallel_tool_calls = Some(parallel_tool_calls);
133 self
134 }
135
136 pub fn mcp_servers(mut self, mcp_servers: ScopedMcpServers) -> Self {
137 self.mcp_servers = mcp_servers;
138 self
139 }
140
141 pub fn metadata_entry(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
142 self.embedder_metadata.insert(key.into(), value.into());
143 self
144 }
145
146 pub fn metadata_entries<I, K, V>(mut self, entries: I) -> Self
147 where
148 I: IntoIterator<Item = (K, V)>,
149 K: Into<String>,
150 V: Into<String>,
151 {
152 self.embedder_metadata
153 .extend(entries.into_iter().map(|(k, v)| (k.into(), v.into())));
154 self
155 }
156
157 pub fn openrouter_attribution(
162 mut self,
163 http_referer: impl Into<String>,
164 title: impl Into<String>,
165 ) -> Self {
166 self.embedder_metadata.insert(
167 OPENROUTER_HTTP_REFERER_METADATA_KEY.to_string(),
168 http_referer.into(),
169 );
170 self.embedder_metadata
171 .insert(OPENROUTER_X_TITLE_METADATA_KEY.to_string(), title.into());
172 self
173 }
174
175 pub fn build(self) -> SeededHarness {
177 SeededHarness {
178 id: self.id,
179 definition: HarnessDefinition {
180 name: self.name,
181 system_prompt: (!self.system_prompt.trim().is_empty())
184 .then_some(self.system_prompt),
185 default_model_id: self.default_model_id,
186 capabilities: self.capabilities,
187 initial_files: self.initial_files,
188 network_access: self.network_access,
189 parallel_tool_calls: self.parallel_tool_calls,
190 mcp_servers: self.mcp_servers,
191 embedder_metadata: self.embedder_metadata,
192 },
193 }
194 }
195}
196
197#[derive(Debug, Clone)]
203pub struct AgentBuilder {
204 id: AgentId,
205 name: String,
206 display_name: Option<String>,
207 description: Option<String>,
208 system_prompt: String,
209 default_model_id: Option<ModelId>,
210 capabilities: Vec<everruns_capability::CapabilityRef>,
211 initial_files: Vec<everruns_core::InitialFile>,
212 network_access: Option<NetworkAccessList>,
213 max_iterations: Option<usize>,
214 parallel_tool_calls: Option<bool>,
215 tools: Vec<ToolDefinition>,
216 mcp_servers: ScopedMcpServers,
217}
218
219impl AgentBuilder {
220 pub fn new(name: impl Into<String>, system_prompt: impl Into<String>) -> Self {
222 Self {
223 id: AgentId::new(),
224 name: name.into(),
225 display_name: None,
226 description: None,
227 system_prompt: system_prompt.into(),
228 default_model_id: None,
229 capabilities: Vec::new(),
230 initial_files: Vec::new(),
231 network_access: None,
232 max_iterations: None,
233 parallel_tool_calls: None,
234 tools: Vec::new(),
235 mcp_servers: ScopedMcpServers::default(),
236 }
237 }
238
239 pub fn id(mut self, id: AgentId) -> Self {
241 self.id = id;
242 self
243 }
244
245 pub fn agent_id(&self) -> AgentId {
247 self.id
248 }
249
250 pub fn name(mut self, name: impl Into<String>) -> Self {
251 self.name = name.into();
252 self
253 }
254
255 pub fn display_name(mut self, display_name: impl Into<String>) -> Self {
256 self.display_name = Some(display_name.into());
257 self
258 }
259
260 pub fn description(mut self, description: impl Into<String>) -> Self {
261 self.description = Some(description.into());
262 self
263 }
264
265 pub fn system_prompt(mut self, system_prompt: impl Into<String>) -> Self {
266 self.system_prompt = system_prompt.into();
267 self
268 }
269
270 pub fn default_model_id(mut self, default_model_id: ModelId) -> Self {
271 self.default_model_id = Some(default_model_id);
272 self
273 }
274
275 pub fn capability(mut self, capability: impl Into<everruns_capability::CapabilityRef>) -> Self {
276 self.capabilities.push(capability.into());
277 self
278 }
279
280 pub fn with_capability(
281 self,
282 capability: impl Into<everruns_capability::CapabilityRef>,
283 ) -> Self {
284 self.capability(capability)
285 }
286
287 pub fn capabilities<I, C>(mut self, capabilities: I) -> Self
288 where
289 I: IntoIterator<Item = C>,
290 C: Into<everruns_capability::CapabilityRef>,
291 {
292 self.capabilities
293 .extend(capabilities.into_iter().map(Into::into));
294 self
295 }
296
297 pub fn initial_file(mut self, file: everruns_core::InitialFile) -> Self {
298 self.initial_files.push(file);
299 self
300 }
301
302 pub fn network_access(mut self, network_access: NetworkAccessList) -> Self {
303 self.network_access = Some(network_access);
304 self
305 }
306
307 pub fn max_iterations(mut self, max_iterations: usize) -> Self {
308 self.max_iterations = Some(max_iterations);
309 self
310 }
311
312 pub fn parallel_tool_calls(mut self, parallel_tool_calls: bool) -> Self {
314 self.parallel_tool_calls = Some(parallel_tool_calls);
315 self
316 }
317
318 pub fn tool(mut self, tool: ToolDefinition) -> Self {
319 self.tools.push(tool);
320 self
321 }
322
323 pub fn tools<I>(mut self, tools: I) -> Self
324 where
325 I: IntoIterator<Item = ToolDefinition>,
326 {
327 self.tools.extend(tools);
328 self
329 }
330
331 pub fn mcp_servers(mut self, mcp_servers: ScopedMcpServers) -> Self {
332 self.mcp_servers = mcp_servers;
333 self
334 }
335
336 pub fn build(self) -> AgentDefinition {
338 AgentDefinition {
339 id: self.id,
340 name: self.name,
341 display_name: self.display_name,
342 description: self.description,
343 system_prompt: self.system_prompt,
344 default_model_id: self.default_model_id,
345 capabilities: self.capabilities,
346 initial_files: self.initial_files,
347 network_access: self.network_access,
348 max_iterations: self.max_iterations,
349 parallel_tool_calls: self.parallel_tool_calls,
350 tools: self.tools,
351 mcp_servers: self.mcp_servers,
352 }
353 }
354}
355
356#[derive(Debug, Clone)]
363pub struct SessionBuilder {
364 id: SessionId,
365 workspace_id: Option<WorkspaceId>,
366 organization_id: String,
367 harness_id: HarnessId,
368 agent_id: Option<AgentId>,
369 title: Option<String>,
370 goal: Option<String>,
371 locale: Option<String>,
372 tags: Vec<String>,
373 model_id: Option<ModelId>,
374 capabilities: Vec<everruns_capability::CapabilityRef>,
375 tools: Vec<ToolDefinition>,
376 mcp_servers: ScopedMcpServers,
377 system_prompt: Option<String>,
378 initial_files: Vec<everruns_core::InitialFile>,
379 network_access: Option<NetworkAccessList>,
380 max_iterations: Option<usize>,
381 parallel_tool_calls: Option<bool>,
382 status: SessionExecutionState,
383}
384
385impl SessionBuilder {
386 pub fn new(harness_id: HarnessId) -> Self {
388 Self {
389 id: SessionId::new(),
390 workspace_id: None,
391 organization_id: DEFAULT_ORG_PUBLIC_ID.to_string(),
392 harness_id,
393 agent_id: None,
394 title: None,
395 goal: None,
396 locale: None,
397 tags: Vec::new(),
398 model_id: None,
399 capabilities: Vec::new(),
400 tools: Vec::new(),
401 mcp_servers: ScopedMcpServers::default(),
402 system_prompt: None,
403 initial_files: Vec::new(),
404 network_access: None,
405 max_iterations: None,
406 parallel_tool_calls: None,
407 status: SessionExecutionState::Started,
408 }
409 }
410
411 pub fn id(mut self, id: SessionId) -> Self {
413 self.id = id;
414 self
415 }
416
417 pub fn session_id(&self) -> SessionId {
419 self.id
420 }
421
422 pub fn workspace(mut self, workspace_id: WorkspaceId) -> Self {
424 self.workspace_id = Some(workspace_id);
425 self
426 }
427
428 pub fn organization_id(mut self, organization_id: impl Into<String>) -> Self {
429 self.organization_id = organization_id.into();
430 self
431 }
432
433 pub fn harness(mut self, harness_id: HarnessId) -> Self {
434 self.harness_id = harness_id;
435 self
436 }
437
438 pub fn agent(mut self, agent_id: AgentId) -> Self {
439 self.agent_id = Some(agent_id);
440 self
441 }
442
443 pub fn title(mut self, title: impl Into<String>) -> Self {
444 self.title = Some(title.into());
445 self
446 }
447
448 pub fn goal(mut self, goal: impl Into<String>) -> Self {
449 self.goal = Some(goal.into());
450 self
451 }
452
453 pub fn locale(mut self, locale: impl Into<String>) -> Self {
454 self.locale = Some(locale.into());
455 self
456 }
457
458 pub fn tag(mut self, tag: impl Into<String>) -> Self {
459 self.tags.push(tag.into());
460 self
461 }
462
463 pub fn tags<I, S>(mut self, tags: I) -> Self
464 where
465 I: IntoIterator<Item = S>,
466 S: Into<String>,
467 {
468 self.tags.extend(tags.into_iter().map(Into::into));
469 self
470 }
471
472 pub fn model_id(mut self, model_id: ModelId) -> Self {
473 self.model_id = Some(model_id);
474 self
475 }
476
477 pub fn capability(mut self, capability: impl Into<everruns_capability::CapabilityRef>) -> Self {
478 self.capabilities.push(capability.into());
479 self
480 }
481
482 pub fn with_capability(
483 self,
484 capability: impl Into<everruns_capability::CapabilityRef>,
485 ) -> Self {
486 self.capability(capability)
487 }
488
489 pub fn capabilities<I, C>(mut self, capabilities: I) -> Self
490 where
491 I: IntoIterator<Item = C>,
492 C: Into<everruns_capability::CapabilityRef>,
493 {
494 self.capabilities
495 .extend(capabilities.into_iter().map(Into::into));
496 self
497 }
498
499 pub fn tool(mut self, tool: ToolDefinition) -> Self {
500 self.tools.push(tool);
501 self
502 }
503
504 pub fn tools<I>(mut self, tools: I) -> Self
505 where
506 I: IntoIterator<Item = ToolDefinition>,
507 {
508 self.tools.extend(tools);
509 self
510 }
511
512 pub fn mcp_servers(mut self, mcp_servers: ScopedMcpServers) -> Self {
513 self.mcp_servers = mcp_servers;
514 self
515 }
516
517 pub fn system_prompt(mut self, system_prompt: impl Into<String>) -> Self {
518 self.system_prompt = Some(system_prompt.into());
519 self
520 }
521
522 pub fn initial_file(mut self, file: everruns_core::InitialFile) -> Self {
523 self.initial_files.push(file);
524 self
525 }
526
527 pub fn network_access(mut self, network_access: NetworkAccessList) -> Self {
528 self.network_access = Some(network_access);
529 self
530 }
531
532 pub fn max_iterations(mut self, max_iterations: usize) -> Self {
533 self.max_iterations = Some(max_iterations);
534 self
535 }
536
537 pub fn parallel_tool_calls(mut self, parallel_tool_calls: bool) -> Self {
539 self.parallel_tool_calls = Some(parallel_tool_calls);
540 self
541 }
542
543 pub fn status(mut self, status: SessionExecutionState) -> Self {
544 self.status = status;
545 self
546 }
547
548 pub fn build(self) -> ExecutionSession {
550 ExecutionSession {
551 id: self.id,
552 workspace_id: self
553 .workspace_id
554 .unwrap_or_else(|| WorkspaceId::from_uuid((self.id).uuid())),
555 organization_id: self.organization_id,
556 harness_id: self.harness_id,
557 agent_id: self.agent_id,
558 title: self.title,
559 goal: self.goal,
560 locale: self.locale,
561 tags: self.tags,
562 model_id: self.model_id,
563 capabilities: self.capabilities,
564 tools: self.tools,
565 mcp_servers: self.mcp_servers,
566 system_prompt: self.system_prompt,
567 initial_files: self.initial_files,
568 hints: None,
569 network_access: self.network_access,
570 max_iterations: self.max_iterations,
571 parallel_tool_calls: self.parallel_tool_calls,
572 status: self.status,
573 usage: None,
574 parent_session_id: None,
575 forked_from_session_id: None,
576 blueprint_id: None,
577 blueprint_config: None,
578 }
579 }
580}
581
582#[derive(Debug, Clone)]
587pub struct SingleSessionBuilder {
588 harness: HarnessBuilder,
589 agent: AgentBuilder,
590 session: SessionBuilder,
591}
592
593impl Default for SingleSessionBuilder {
594 fn default() -> Self {
595 let harness = HarnessBuilder::new("embedded-harness", "");
596 let agent = AgentBuilder::new("embedded-agent", "");
597 let session = SessionBuilder::new(harness.harness_id()).agent(agent.agent_id());
598 Self {
599 harness,
600 agent,
601 session,
602 }
603 }
604}
605
606impl SingleSessionBuilder {
607 pub fn harness(mut self, name: impl Into<String>, system_prompt: impl Into<String>) -> Self {
611 let harness_id = self.harness.harness_id();
612 self.harness = self.harness.name(name).system_prompt(system_prompt);
613 self.session = self.session.harness(harness_id);
614 self
615 }
616
617 pub fn agent(mut self, name: impl Into<String>, system_prompt: impl Into<String>) -> Self {
621 let agent_id = self.agent.agent_id();
622 self.agent = self.agent.name(name).system_prompt(system_prompt);
623 self.session = self.session.agent(agent_id);
624 self
625 }
626
627 pub fn with_capability(
629 self,
630 capability: impl Into<everruns_capability::CapabilityRef>,
631 ) -> Self {
632 self.harness_capability(capability)
633 }
634
635 pub fn harness_capability(
637 mut self,
638 capability: impl Into<everruns_capability::CapabilityRef>,
639 ) -> Self {
640 self.harness = self.harness.capability(capability);
641 self
642 }
643
644 pub fn agent_capability(
646 mut self,
647 capability: impl Into<everruns_capability::CapabilityRef>,
648 ) -> Self {
649 self.agent = self.agent.capability(capability);
650 self
651 }
652
653 pub fn agent_plugin(mut self, name: &str) -> Self {
666 self.agent = self.agent.capability(plugin_capability_id(name));
667 self
668 }
669
670 pub fn session_capability(
672 mut self,
673 capability: impl Into<everruns_capability::CapabilityRef>,
674 ) -> Self {
675 self.session = self.session.capability(capability);
676 self
677 }
678
679 pub fn session_mcp_servers(mut self, mcp_servers: ScopedMcpServers) -> Self {
682 self.session = self.session.mcp_servers(mcp_servers);
683 self
684 }
685
686 pub fn agent_display_name(mut self, display_name: impl Into<String>) -> Self {
687 self.agent = self.agent.display_name(display_name);
688 self
689 }
690
691 pub fn openrouter_attribution(
692 mut self,
693 http_referer: impl Into<String>,
694 title: impl Into<String>,
695 ) -> Self {
696 self.harness = self.harness.openrouter_attribution(http_referer, title);
697 self
698 }
699
700 pub fn agent_description(mut self, description: impl Into<String>) -> Self {
701 self.agent = self.agent.description(description);
702 self
703 }
704
705 pub fn session_title(mut self, title: impl Into<String>) -> Self {
706 self.session = self.session.title(title);
707 self
708 }
709
710 pub fn locale(mut self, locale: impl Into<String>) -> Self {
711 self.session = self.session.locale(locale);
712 self
713 }
714
715 pub fn tag(mut self, tag: impl Into<String>) -> Self {
716 self.session = self.session.tag(tag);
719 self
720 }
721
722 pub fn session_model_id(mut self, model_id: ModelId) -> Self {
723 self.session = self.session.model_id(model_id);
724 self
725 }
726
727 pub fn harness_default_model_id(mut self, model_id: ModelId) -> Self {
728 self.harness = self.harness.default_model_id(model_id);
729 self
730 }
731
732 pub fn agent_default_model_id(mut self, model_id: ModelId) -> Self {
733 self.agent = self.agent.default_model_id(model_id);
734 self
735 }
736
737 pub fn agent_max_iterations(mut self, max_iterations: usize) -> Self {
738 self.agent = self.agent.max_iterations(max_iterations);
739 self
740 }
741
742 pub fn session_max_iterations(mut self, max_iterations: usize) -> Self {
743 self.session = self.session.max_iterations(max_iterations);
744 self
745 }
746
747 pub fn agent_tool(mut self, tool: ToolDefinition) -> Self {
748 self.agent = self.agent.tool(tool);
749 self
750 }
751
752 pub fn session_tool(mut self, tool: ToolDefinition) -> Self {
753 self.session = self.session.tool(tool);
754 self
755 }
756
757 pub fn harness_initial_file(mut self, file: everruns_core::InitialFile) -> Self {
758 self.harness = self.harness.initial_file(file);
759 self
760 }
761
762 pub fn agent_initial_file(mut self, file: everruns_core::InitialFile) -> Self {
763 self.agent = self.agent.initial_file(file);
764 self
765 }
766
767 pub fn session_initial_file(mut self, file: everruns_core::InitialFile) -> Self {
768 self.session = self.session.initial_file(file);
769 self
770 }
771
772 pub fn harness_network_access(mut self, network_access: NetworkAccessList) -> Self {
773 self.harness = self.harness.network_access(network_access);
774 self
775 }
776
777 pub fn agent_network_access(mut self, network_access: NetworkAccessList) -> Self {
778 self.agent = self.agent.network_access(network_access);
779 self
780 }
781
782 pub fn session_network_access(mut self, network_access: NetworkAccessList) -> Self {
783 self.session = self.session.network_access(network_access);
784 self
785 }
786
787 pub fn harness_id(&self) -> HarnessId {
788 self.harness.harness_id()
789 }
790
791 pub fn agent_id(&self) -> AgentId {
792 self.agent.agent_id()
793 }
794
795 pub fn session_id(mut self, id: SessionId) -> Self {
802 self.session = self.session.id(id);
803 self
804 }
805
806 pub(crate) fn build(self) -> (SeededHarness, AgentDefinition, ExecutionSession, SessionId) {
807 let session_id = self.session.session_id();
808 (
809 self.harness.build(),
810 self.agent.build(),
811 self.session.build(),
812 session_id,
813 )
814 }
815}
816
817#[cfg(test)]
818mod tests {
819 use super::*;
820
821 #[test]
822 fn harness_builder_openrouter_attribution_adds_metadata_keys() {
823 let harness = HarnessBuilder::new("app", "prompt")
824 .openrouter_attribution("https://app.example", "Example App")
825 .build();
826
827 assert_eq!(
828 harness
829 .definition
830 .embedder_metadata
831 .get(OPENROUTER_HTTP_REFERER_METADATA_KEY)
832 .map(String::as_str),
833 Some("https://app.example")
834 );
835 assert_eq!(
836 harness
837 .definition
838 .embedder_metadata
839 .get(OPENROUTER_X_TITLE_METADATA_KEY)
840 .map(String::as_str),
841 Some("Example App")
842 );
843 }
844
845 #[test]
846 fn single_session_builder_openrouter_attribution_configures_harness() {
847 let (harness, _agent, _session, _session_id) = SingleSessionBuilder::default()
848 .openrouter_attribution("https://single.example", "Single App")
849 .build();
850
851 assert_eq!(
852 harness
853 .definition
854 .embedder_metadata
855 .get(OPENROUTER_HTTP_REFERER_METADATA_KEY)
856 .map(String::as_str),
857 Some("https://single.example")
858 );
859 assert_eq!(
860 harness
861 .definition
862 .embedder_metadata
863 .get(OPENROUTER_X_TITLE_METADATA_KEY)
864 .map(String::as_str),
865 Some("Single App")
866 );
867 }
868}