Skip to main content

everruns_host/
builders.rs

1// Host-facing builders for core seed models.
2// Decision: keep these in everruns-host so core domain structs stay literal
3// data models while the host owns low-level embedding ergonomics.
4
5use 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};
13/// Metadata keys consumed by the OpenRouter driver as `HTTP-Referer` / `X-Title`.
14/// Owned by `everruns_openrouter::options`; duplicated here as plain strings so
15/// the host harness keeps zero type-level coupling to any leaf driver crate.
16const 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/// A portable harness definition seeded under an embedder-chosen id.
22///
23/// EVE-881: the Framework host carries no stored Harness persistence records.
24/// The definition itself is id-free neutral configuration; the id exists only
25/// to key the association with sessions (`Session::harness_id`).
26#[derive(Debug, Clone)]
27pub struct SeededHarness {
28    /// Id sessions reference via `harness_id`.
29    pub id: HarnessId,
30    /// Portable execution configuration seeded under `id`.
31    pub definition: HarnessDefinition,
32}
33
34/// Builds a portable [`HarnessDefinition`] with runtime-friendly defaults,
35/// paired with the id it is seeded under.
36///
37/// EVE-881: the embedded host seeds neutral execution configuration only.
38/// Stored Harness persistence records (lifecycle status, hierarchy, display
39/// metadata, timestamps) live in `everruns-platform` and are a hosted
40/// control-plane concern.
41#[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    /// Create a harness builder from the required embedder-facing fields.
57    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    /// Set a stable harness id instead of generating one.
73    pub fn id(mut self, id: HarnessId) -> Self {
74        self.id = id;
75        self
76    }
77
78    /// Return the id currently assigned to this builder.
79    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    /// Set the request-level parallel tool calling preference (EVE-598).
131    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    /// Set OpenRouter attribution headers for LLM calls made by this harness.
158    ///
159    /// The values flow through harness embedder metadata and are sent by the
160    /// OpenRouter driver as `HTTP-Referer` and `X-Title`.
161    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    /// Build the seeded harness. Builders do not validate domain invariants.
176    pub fn build(self) -> SeededHarness {
177        SeededHarness {
178            id: self.id,
179            definition: HarnessDefinition {
180                name: self.name,
181                // Empty/whitespace-only builder prompt means the harness
182                // contributes no base prompt.
183                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/// Builds a portable [`AgentDefinition`] with runtime-friendly defaults.
198///
199/// EVE-877: the embedded host seeds authored execution configuration only.
200/// Stored Agent persistence records (lifecycle status, versioning, timestamps)
201/// live in `everruns-platform` and are a hosted-control-plane concern.
202#[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    /// Create an agent builder from the required embedder-facing fields.
221    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    /// Set a stable agent id instead of generating one.
240    pub fn id(mut self, id: AgentId) -> Self {
241        self.id = id;
242        self
243    }
244
245    /// Return the id currently assigned to this builder.
246    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    /// Set the request-level parallel tool calling preference (EVE-598).
313    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    /// Build the agent definition. Builders do not validate domain invariants.
337    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/// Builds a portable [`ExecutionSession`] with runtime-friendly defaults.
357///
358/// EVE-882: the embedded host seeds the neutral execution view only. The
359/// stored Session persistence record (source facets, participants, ownership
360/// summaries, timestamps, UI metadata) lives in `everruns-platform` and is a
361/// hosted control-plane concern.
362#[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    /// Create a session builder for the required harness id.
387    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    /// Set a stable session id instead of generating one.
412    pub fn id(mut self, id: SessionId) -> Self {
413        self.id = id;
414        self
415    }
416
417    /// Return the id currently assigned to this builder.
418    pub fn session_id(&self) -> SessionId {
419        self.id
420    }
421
422    /// Bind execution filesystem scoping to an explicit logical workspace.
423    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    /// Set the request-level parallel tool calling preference (EVE-598).
538    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    /// Build the session. Builders do not validate domain invariants.
549    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/// High-level builder for seeding one harness, one agent, and one session.
583///
584/// This is the compact path used by embedders that want a runnable runtime
585/// without constructing each core model separately.
586#[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    /// Configure the seeded harness. Mutates the existing `HarnessBuilder`
608    /// in place so previously configured fields (e.g. `network_access`) are
609    /// preserved regardless of call order.
610    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    /// Configure the seeded agent. Mutates the existing `AgentBuilder` in
618    /// place so previously configured fields (e.g. `network_access`) are
619    /// preserved regardless of call order.
620    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    /// Add a harness-level capability.
628    pub fn with_capability(
629        self,
630        capability: impl Into<everruns_capability::CapabilityRef>,
631    ) -> Self {
632        self.harness_capability(capability)
633    }
634
635    /// Add a harness-level capability.
636    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    /// Add an agent-level capability.
645    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    /// Enable a previously loaded plugin on the seeded agent.
654    ///
655    /// Adds a `plugin:{name}` capability ref to the agent with an empty config.
656    /// The hydrated definition must be supplied separately via
657    /// [`crate::InProcessRuntimeBuilder::with_plugin_dir`] — this method only records
658    /// the capability ref on the agent so the capability is active for this
659    /// session. The builder looks up the hydrated config at build time from the
660    /// `plugin_capability_configs` accumulated by `with_plugin_dir` calls.
661    ///
662    /// If you need to pass the fully hydrated `everruns_capability::CapabilityRef` (e.g.
663    /// from [`crate::InProcessRuntimeBuilder::plugin_capability`]), use
664    /// [`Self::agent_capability`] directly.
665    pub fn agent_plugin(mut self, name: &str) -> Self {
666        self.agent = self.agent.capability(plugin_capability_id(name));
667        self
668    }
669
670    /// Add a session-level capability.
671    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    /// Configure session-scoped MCP servers (knowledge/integrations/runtime-mcp.md). Discovered
680    /// and executed by the runtime alongside built-in tools.
681    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        // Harness/agent definitions carry no tags (EVE-877, EVE-881); session
717        // tags drive execution metadata.
718        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    /// Pin the seeded session's id. When unset, the underlying
796    /// `SessionBuilder` generates a fresh `SessionId` at build time.
797    ///
798    /// Useful for embedders that need the id ahead of build — e.g. the
799    /// `examples/coding-cli` JSONL session log uses `<id>.jsonl` as the
800    /// filename and must open the file before the runtime exists.
801    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}