Skip to main content

everruns_runtime/
builders.rs

1// Embedder-facing builders for core seed models.
2// Decision: keep these in everruns-runtime so core domain structs stay literal
3// data models while runtime owns the public embedding ergonomics.
4
5use std::collections::HashMap;
6
7use chrono::{DateTime, Utc};
8pub use everruns_core::driver_registry::{
9    OPENROUTER_HTTP_REFERER_METADATA_KEY, OPENROUTER_X_TITLE_METADATA_KEY,
10};
11use everruns_core::network_access::NetworkAccessList;
12use everruns_core::{
13    Agent, AgentCapabilityConfig, AgentId, AgentStatus, DEFAULT_ORG_PUBLIC_ID, Harness, HarnessId,
14    HarnessStatus, ModelId, PrincipalId, ScopedMcpServers, Session, SessionId, SessionStatus,
15    ToolDefinition, plugin_capability_id,
16};
17use uuid::Uuid;
18
19/// Builds a [`Harness`] with runtime-friendly defaults.
20///
21/// Use this when embedding Everruns directly and seeding a harness in code.
22/// IDs and timestamps are generated at [`build`](Self::build) time unless
23/// explicitly set.
24#[derive(Debug, Clone)]
25pub struct HarnessBuilder {
26    id: HarnessId,
27    name: String,
28    display_name: Option<String>,
29    description: Option<String>,
30    system_prompt: String,
31    parent_harness_id: Option<HarnessId>,
32    default_model_id: Option<ModelId>,
33    tags: Vec<String>,
34    capabilities: Vec<AgentCapabilityConfig>,
35    initial_files: Vec<everruns_core::InitialFile>,
36    network_access: Option<NetworkAccessList>,
37    parallel_tool_calls: Option<bool>,
38    mcp_servers: ScopedMcpServers,
39    embedder_metadata: HashMap<String, String>,
40    is_built_in: bool,
41    status: HarnessStatus,
42    created_at: Option<DateTime<Utc>>,
43    updated_at: Option<DateTime<Utc>>,
44}
45
46impl HarnessBuilder {
47    /// Create a harness builder from the required embedder-facing fields.
48    pub fn new(name: impl Into<String>, system_prompt: impl Into<String>) -> Self {
49        Self {
50            id: HarnessId::new(),
51            name: name.into(),
52            display_name: None,
53            description: None,
54            system_prompt: system_prompt.into(),
55            parent_harness_id: None,
56            default_model_id: None,
57            tags: Vec::new(),
58            capabilities: Vec::new(),
59            initial_files: Vec::new(),
60            network_access: None,
61            parallel_tool_calls: None,
62            mcp_servers: ScopedMcpServers::default(),
63            embedder_metadata: HashMap::new(),
64            is_built_in: false,
65            status: HarnessStatus::Active,
66            created_at: None,
67            updated_at: None,
68        }
69    }
70
71    /// Set a stable harness id instead of generating one.
72    pub fn id(mut self, id: HarnessId) -> Self {
73        self.id = id;
74        self
75    }
76
77    /// Return the id currently assigned to this builder.
78    pub fn harness_id(&self) -> HarnessId {
79        self.id
80    }
81
82    pub fn name(mut self, name: impl Into<String>) -> Self {
83        self.name = name.into();
84        self
85    }
86
87    pub fn display_name(mut self, display_name: impl Into<String>) -> Self {
88        self.display_name = Some(display_name.into());
89        self
90    }
91
92    pub fn description(mut self, description: impl Into<String>) -> Self {
93        self.description = Some(description.into());
94        self
95    }
96
97    pub fn system_prompt(mut self, system_prompt: impl Into<String>) -> Self {
98        self.system_prompt = system_prompt.into();
99        self
100    }
101
102    pub fn parent_harness_id(mut self, parent_harness_id: HarnessId) -> Self {
103        self.parent_harness_id = Some(parent_harness_id);
104        self
105    }
106
107    pub fn default_model_id(mut self, default_model_id: ModelId) -> Self {
108        self.default_model_id = Some(default_model_id);
109        self
110    }
111
112    pub fn tag(mut self, tag: impl Into<String>) -> Self {
113        self.tags.push(tag.into());
114        self
115    }
116
117    pub fn tags<I, S>(mut self, tags: I) -> Self
118    where
119        I: IntoIterator<Item = S>,
120        S: Into<String>,
121    {
122        self.tags.extend(tags.into_iter().map(Into::into));
123        self
124    }
125
126    pub fn capability(mut self, capability: impl Into<AgentCapabilityConfig>) -> Self {
127        self.capabilities.push(capability.into());
128        self
129    }
130
131    pub fn with_capability(self, capability: impl Into<AgentCapabilityConfig>) -> Self {
132        self.capability(capability)
133    }
134
135    pub fn capabilities<I, C>(mut self, capabilities: I) -> Self
136    where
137        I: IntoIterator<Item = C>,
138        C: Into<AgentCapabilityConfig>,
139    {
140        self.capabilities
141            .extend(capabilities.into_iter().map(Into::into));
142        self
143    }
144
145    pub fn initial_file(mut self, file: everruns_core::InitialFile) -> Self {
146        self.initial_files.push(file);
147        self
148    }
149
150    pub fn network_access(mut self, network_access: NetworkAccessList) -> Self {
151        self.network_access = Some(network_access);
152        self
153    }
154
155    /// Set the request-level parallel tool calling preference (EVE-598).
156    pub fn parallel_tool_calls(mut self, parallel_tool_calls: bool) -> Self {
157        self.parallel_tool_calls = Some(parallel_tool_calls);
158        self
159    }
160
161    pub fn mcp_servers(mut self, mcp_servers: ScopedMcpServers) -> Self {
162        self.mcp_servers = mcp_servers;
163        self
164    }
165
166    pub fn metadata_entry(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
167        self.embedder_metadata.insert(key.into(), value.into());
168        self
169    }
170
171    pub fn metadata_entries<I, K, V>(mut self, entries: I) -> Self
172    where
173        I: IntoIterator<Item = (K, V)>,
174        K: Into<String>,
175        V: Into<String>,
176    {
177        self.embedder_metadata
178            .extend(entries.into_iter().map(|(k, v)| (k.into(), v.into())));
179        self
180    }
181
182    /// Set OpenRouter attribution headers for LLM calls made by this harness.
183    ///
184    /// The values flow through harness embedder metadata and are sent by the
185    /// OpenRouter driver as `HTTP-Referer` and `X-Title`.
186    pub fn openrouter_attribution(
187        mut self,
188        http_referer: impl Into<String>,
189        title: impl Into<String>,
190    ) -> Self {
191        self.embedder_metadata.insert(
192            OPENROUTER_HTTP_REFERER_METADATA_KEY.to_string(),
193            http_referer.into(),
194        );
195        self.embedder_metadata
196            .insert(OPENROUTER_X_TITLE_METADATA_KEY.to_string(), title.into());
197        self
198    }
199
200    pub fn is_built_in(mut self, is_built_in: bool) -> Self {
201        self.is_built_in = is_built_in;
202        self
203    }
204
205    pub fn status(mut self, status: HarnessStatus) -> Self {
206        self.status = status;
207        self
208    }
209
210    pub fn created_at(mut self, created_at: DateTime<Utc>) -> Self {
211        self.created_at = Some(created_at);
212        self
213    }
214
215    pub fn updated_at(mut self, updated_at: DateTime<Utc>) -> Self {
216        self.updated_at = Some(updated_at);
217        self
218    }
219
220    /// Build the harness. Builders do not validate domain invariants.
221    pub fn build(self) -> Harness {
222        let created_at = self.created_at.unwrap_or_else(Utc::now);
223        let updated_at = self.updated_at.unwrap_or(created_at);
224
225        Harness {
226            id: self.id,
227            name: self.name,
228            display_name: self.display_name,
229            description: self.description,
230            // Empty/whitespace-only builder prompt means the harness
231            // contributes no base prompt.
232            system_prompt: (!self.system_prompt.trim().is_empty()).then_some(self.system_prompt),
233            parent_harness_id: self.parent_harness_id,
234            default_model_id: self.default_model_id,
235            tags: self.tags,
236            capabilities: self.capabilities,
237            initial_files: self.initial_files,
238            network_access: self.network_access,
239            parallel_tool_calls: self.parallel_tool_calls,
240            mcp_servers: self.mcp_servers,
241            embedder_metadata: self.embedder_metadata,
242            is_built_in: self.is_built_in,
243            status: self.status,
244            created_at,
245            updated_at,
246            archived_at: None,
247            deleted_at: None,
248        }
249    }
250}
251
252/// Builds an [`Agent`] with runtime-friendly defaults.
253#[derive(Debug, Clone)]
254pub struct AgentBuilder {
255    id: AgentId,
256    name: String,
257    display_name: Option<String>,
258    description: Option<String>,
259    system_prompt: String,
260    default_model_id: Option<ModelId>,
261    tags: Vec<String>,
262    capabilities: Vec<AgentCapabilityConfig>,
263    initial_files: Vec<everruns_core::InitialFile>,
264    network_access: Option<NetworkAccessList>,
265    max_iterations: Option<usize>,
266    parallel_tool_calls: Option<bool>,
267    tools: Vec<ToolDefinition>,
268    mcp_servers: ScopedMcpServers,
269    status: AgentStatus,
270    created_at: Option<DateTime<Utc>>,
271    updated_at: Option<DateTime<Utc>>,
272}
273
274impl AgentBuilder {
275    /// Create an agent builder from the required embedder-facing fields.
276    pub fn new(name: impl Into<String>, system_prompt: impl Into<String>) -> Self {
277        Self {
278            id: AgentId::new(),
279            name: name.into(),
280            display_name: None,
281            description: None,
282            system_prompt: system_prompt.into(),
283            default_model_id: None,
284            tags: Vec::new(),
285            capabilities: Vec::new(),
286            initial_files: Vec::new(),
287            network_access: None,
288            max_iterations: None,
289            parallel_tool_calls: None,
290            tools: Vec::new(),
291            mcp_servers: ScopedMcpServers::default(),
292            status: AgentStatus::Active,
293            created_at: None,
294            updated_at: None,
295        }
296    }
297
298    /// Set a stable agent id instead of generating one.
299    pub fn id(mut self, id: AgentId) -> Self {
300        self.id = id;
301        self
302    }
303
304    /// Return the id currently assigned to this builder.
305    pub fn agent_id(&self) -> AgentId {
306        self.id
307    }
308
309    pub fn name(mut self, name: impl Into<String>) -> Self {
310        self.name = name.into();
311        self
312    }
313
314    pub fn display_name(mut self, display_name: impl Into<String>) -> Self {
315        self.display_name = Some(display_name.into());
316        self
317    }
318
319    pub fn description(mut self, description: impl Into<String>) -> Self {
320        self.description = Some(description.into());
321        self
322    }
323
324    pub fn system_prompt(mut self, system_prompt: impl Into<String>) -> Self {
325        self.system_prompt = system_prompt.into();
326        self
327    }
328
329    pub fn default_model_id(mut self, default_model_id: ModelId) -> Self {
330        self.default_model_id = Some(default_model_id);
331        self
332    }
333
334    pub fn tag(mut self, tag: impl Into<String>) -> Self {
335        self.tags.push(tag.into());
336        self
337    }
338
339    pub fn tags<I, S>(mut self, tags: I) -> Self
340    where
341        I: IntoIterator<Item = S>,
342        S: Into<String>,
343    {
344        self.tags.extend(tags.into_iter().map(Into::into));
345        self
346    }
347
348    pub fn capability(mut self, capability: impl Into<AgentCapabilityConfig>) -> Self {
349        self.capabilities.push(capability.into());
350        self
351    }
352
353    pub fn with_capability(self, capability: impl Into<AgentCapabilityConfig>) -> Self {
354        self.capability(capability)
355    }
356
357    pub fn capabilities<I, C>(mut self, capabilities: I) -> Self
358    where
359        I: IntoIterator<Item = C>,
360        C: Into<AgentCapabilityConfig>,
361    {
362        self.capabilities
363            .extend(capabilities.into_iter().map(Into::into));
364        self
365    }
366
367    pub fn initial_file(mut self, file: everruns_core::InitialFile) -> Self {
368        self.initial_files.push(file);
369        self
370    }
371
372    pub fn network_access(mut self, network_access: NetworkAccessList) -> Self {
373        self.network_access = Some(network_access);
374        self
375    }
376
377    pub fn max_iterations(mut self, max_iterations: usize) -> Self {
378        self.max_iterations = Some(max_iterations);
379        self
380    }
381
382    /// Set the request-level parallel tool calling preference (EVE-598).
383    pub fn parallel_tool_calls(mut self, parallel_tool_calls: bool) -> Self {
384        self.parallel_tool_calls = Some(parallel_tool_calls);
385        self
386    }
387
388    pub fn tool(mut self, tool: ToolDefinition) -> Self {
389        self.tools.push(tool);
390        self
391    }
392
393    pub fn tools<I>(mut self, tools: I) -> Self
394    where
395        I: IntoIterator<Item = ToolDefinition>,
396    {
397        self.tools.extend(tools);
398        self
399    }
400
401    pub fn mcp_servers(mut self, mcp_servers: ScopedMcpServers) -> Self {
402        self.mcp_servers = mcp_servers;
403        self
404    }
405
406    pub fn status(mut self, status: AgentStatus) -> Self {
407        self.status = status;
408        self
409    }
410
411    pub fn created_at(mut self, created_at: DateTime<Utc>) -> Self {
412        self.created_at = Some(created_at);
413        self
414    }
415
416    pub fn updated_at(mut self, updated_at: DateTime<Utc>) -> Self {
417        self.updated_at = Some(updated_at);
418        self
419    }
420
421    /// Build the agent. Builders do not validate domain invariants.
422    pub fn build(self) -> Agent {
423        let created_at = self.created_at.unwrap_or_else(Utc::now);
424        let updated_at = self.updated_at.unwrap_or(created_at);
425
426        Agent {
427            public_id: self.id,
428            internal_id: Uuid::nil(),
429            name: self.name,
430            display_name: self.display_name,
431            description: self.description,
432            system_prompt: self.system_prompt,
433            default_model_id: self.default_model_id,
434            default_version_id: None,
435            forked_from_agent_id: None,
436            forked_from_version_id: None,
437            root_agent_id: None,
438            tags: self.tags,
439            capabilities: self.capabilities,
440            initial_files: self.initial_files,
441            network_access: self.network_access,
442            max_iterations: self.max_iterations,
443            parallel_tool_calls: self.parallel_tool_calls,
444            tools: self.tools,
445            mcp_servers: self.mcp_servers,
446            status: self.status,
447            created_at,
448            updated_at,
449            archived_at: None,
450            deleted_at: None,
451            usage: None,
452        }
453    }
454}
455
456/// Builds a [`Session`] with runtime-friendly defaults.
457#[derive(Debug, Clone)]
458pub struct SessionBuilder {
459    id: SessionId,
460    organization_id: String,
461    harness_id: HarnessId,
462    agent_id: Option<AgentId>,
463    owner_principal_id: PrincipalId,
464    title: Option<String>,
465    locale: Option<String>,
466    tags: Vec<String>,
467    model_id: Option<ModelId>,
468    capabilities: Vec<AgentCapabilityConfig>,
469    tools: Vec<ToolDefinition>,
470    mcp_servers: ScopedMcpServers,
471    system_prompt: Option<String>,
472    initial_files: Vec<everruns_core::InitialFile>,
473    network_access: Option<NetworkAccessList>,
474    max_iterations: Option<usize>,
475    parallel_tool_calls: Option<bool>,
476    status: SessionStatus,
477    created_at: Option<DateTime<Utc>>,
478    updated_at: Option<DateTime<Utc>>,
479}
480
481impl SessionBuilder {
482    /// Create a session builder for the required harness id.
483    pub fn new(harness_id: HarnessId) -> Self {
484        Self {
485            id: SessionId::new(),
486            organization_id: DEFAULT_ORG_PUBLIC_ID.to_string(),
487            harness_id,
488            agent_id: None,
489            owner_principal_id: PrincipalId::from_seed(1),
490            title: None,
491            locale: None,
492            tags: Vec::new(),
493            model_id: None,
494            capabilities: Vec::new(),
495            tools: Vec::new(),
496            mcp_servers: ScopedMcpServers::default(),
497            system_prompt: None,
498            initial_files: Vec::new(),
499            network_access: None,
500            max_iterations: None,
501            parallel_tool_calls: None,
502            status: SessionStatus::Started,
503            created_at: None,
504            updated_at: None,
505        }
506    }
507
508    /// Set a stable session id instead of generating one.
509    pub fn id(mut self, id: SessionId) -> Self {
510        self.id = id;
511        self
512    }
513
514    /// Return the id currently assigned to this builder.
515    pub fn session_id(&self) -> SessionId {
516        self.id
517    }
518
519    pub fn organization_id(mut self, organization_id: impl Into<String>) -> Self {
520        self.organization_id = organization_id.into();
521        self
522    }
523
524    pub fn harness(mut self, harness_id: HarnessId) -> Self {
525        self.harness_id = harness_id;
526        self
527    }
528
529    pub fn agent(mut self, agent_id: AgentId) -> Self {
530        self.agent_id = Some(agent_id);
531        self
532    }
533
534    pub fn owner_principal_id(mut self, owner_principal_id: PrincipalId) -> Self {
535        self.owner_principal_id = owner_principal_id;
536        self
537    }
538
539    pub fn title(mut self, title: impl Into<String>) -> Self {
540        self.title = Some(title.into());
541        self
542    }
543
544    pub fn locale(mut self, locale: impl Into<String>) -> Self {
545        self.locale = Some(locale.into());
546        self
547    }
548
549    pub fn tag(mut self, tag: impl Into<String>) -> Self {
550        self.tags.push(tag.into());
551        self
552    }
553
554    pub fn tags<I, S>(mut self, tags: I) -> Self
555    where
556        I: IntoIterator<Item = S>,
557        S: Into<String>,
558    {
559        self.tags.extend(tags.into_iter().map(Into::into));
560        self
561    }
562
563    pub fn model_id(mut self, model_id: ModelId) -> Self {
564        self.model_id = Some(model_id);
565        self
566    }
567
568    pub fn capability(mut self, capability: impl Into<AgentCapabilityConfig>) -> Self {
569        self.capabilities.push(capability.into());
570        self
571    }
572
573    pub fn with_capability(self, capability: impl Into<AgentCapabilityConfig>) -> Self {
574        self.capability(capability)
575    }
576
577    pub fn capabilities<I, C>(mut self, capabilities: I) -> Self
578    where
579        I: IntoIterator<Item = C>,
580        C: Into<AgentCapabilityConfig>,
581    {
582        self.capabilities
583            .extend(capabilities.into_iter().map(Into::into));
584        self
585    }
586
587    pub fn tool(mut self, tool: ToolDefinition) -> Self {
588        self.tools.push(tool);
589        self
590    }
591
592    pub fn tools<I>(mut self, tools: I) -> Self
593    where
594        I: IntoIterator<Item = ToolDefinition>,
595    {
596        self.tools.extend(tools);
597        self
598    }
599
600    pub fn mcp_servers(mut self, mcp_servers: ScopedMcpServers) -> Self {
601        self.mcp_servers = mcp_servers;
602        self
603    }
604
605    pub fn system_prompt(mut self, system_prompt: impl Into<String>) -> Self {
606        self.system_prompt = Some(system_prompt.into());
607        self
608    }
609
610    pub fn initial_file(mut self, file: everruns_core::InitialFile) -> Self {
611        self.initial_files.push(file);
612        self
613    }
614
615    pub fn network_access(mut self, network_access: NetworkAccessList) -> Self {
616        self.network_access = Some(network_access);
617        self
618    }
619
620    pub fn max_iterations(mut self, max_iterations: usize) -> Self {
621        self.max_iterations = Some(max_iterations);
622        self
623    }
624
625    /// Set the request-level parallel tool calling preference (EVE-598).
626    pub fn parallel_tool_calls(mut self, parallel_tool_calls: bool) -> Self {
627        self.parallel_tool_calls = Some(parallel_tool_calls);
628        self
629    }
630
631    pub fn status(mut self, status: SessionStatus) -> Self {
632        self.status = status;
633        self
634    }
635
636    pub fn created_at(mut self, created_at: DateTime<Utc>) -> Self {
637        self.created_at = Some(created_at);
638        self
639    }
640
641    pub fn updated_at(mut self, updated_at: DateTime<Utc>) -> Self {
642        self.updated_at = Some(updated_at);
643        self
644    }
645
646    /// Build the session. Builders do not validate domain invariants.
647    pub fn build(self) -> Session {
648        let created_at = self.created_at.unwrap_or_else(Utc::now);
649        let updated_at = self.updated_at.unwrap_or(created_at);
650
651        Session {
652            id: self.id,
653            workspace_id: everruns_core::WorkspaceId::from_uuid((self.id).uuid()),
654            organization_id: self.organization_id,
655            harness_id: self.harness_id,
656            agent_id: self.agent_id,
657            agent_version_id: None,
658            agent_identity_id: None,
659            owner_principal_id: self.owner_principal_id,
660            resolved_owner_user_id: None,
661            owner: None,
662            effective_owner: None,
663            title: self.title,
664            locale: self.locale,
665            preview: None,
666            output_preview: None,
667            tags: self.tags,
668            model_id: self.model_id,
669            capabilities: self.capabilities,
670            tools: self.tools,
671            mcp_servers: self.mcp_servers,
672            system_prompt: self.system_prompt,
673            initial_files: self.initial_files,
674            hints: None,
675            network_access: self.network_access,
676            max_iterations: self.max_iterations,
677            parallel_tool_calls: self.parallel_tool_calls,
678            status: self.status,
679            created_at,
680            updated_at,
681            started_at: None,
682            finished_at: None,
683            usage: None,
684            is_pinned: None,
685            active_schedule_count: None,
686            features: Vec::new(),
687            parent_session_id: None,
688            blueprint_id: None,
689            blueprint_config: None,
690        }
691    }
692}
693
694/// High-level builder for seeding one harness, one agent, and one session.
695///
696/// This is the compact path used by embedders that want a runnable runtime
697/// without constructing each core model separately.
698#[derive(Debug, Clone)]
699pub struct SingleSessionBuilder {
700    harness: HarnessBuilder,
701    agent: AgentBuilder,
702    session: SessionBuilder,
703}
704
705impl Default for SingleSessionBuilder {
706    fn default() -> Self {
707        let harness = HarnessBuilder::new("embedded-harness", "");
708        let agent = AgentBuilder::new("embedded-agent", "");
709        let session = SessionBuilder::new(harness.harness_id()).agent(agent.agent_id());
710        Self {
711            harness,
712            agent,
713            session,
714        }
715    }
716}
717
718impl SingleSessionBuilder {
719    /// Configure the seeded harness. Mutates the existing `HarnessBuilder`
720    /// in place so previously configured fields (e.g. `network_access`) are
721    /// preserved regardless of call order.
722    pub fn harness(mut self, name: impl Into<String>, system_prompt: impl Into<String>) -> Self {
723        let harness_id = self.harness.harness_id();
724        self.harness = self.harness.name(name).system_prompt(system_prompt);
725        self.session = self.session.harness(harness_id);
726        self
727    }
728
729    /// Configure the seeded agent. Mutates the existing `AgentBuilder` in
730    /// place so previously configured fields (e.g. `network_access`) are
731    /// preserved regardless of call order.
732    pub fn agent(mut self, name: impl Into<String>, system_prompt: impl Into<String>) -> Self {
733        let agent_id = self.agent.agent_id();
734        self.agent = self.agent.name(name).system_prompt(system_prompt);
735        self.session = self.session.agent(agent_id);
736        self
737    }
738
739    /// Add a harness-level capability.
740    pub fn with_capability(self, capability: impl Into<AgentCapabilityConfig>) -> Self {
741        self.harness_capability(capability)
742    }
743
744    /// Add a harness-level capability.
745    pub fn harness_capability(mut self, capability: impl Into<AgentCapabilityConfig>) -> Self {
746        self.harness = self.harness.capability(capability);
747        self
748    }
749
750    /// Add an agent-level capability.
751    pub fn agent_capability(mut self, capability: impl Into<AgentCapabilityConfig>) -> Self {
752        self.agent = self.agent.capability(capability);
753        self
754    }
755
756    /// Enable a previously loaded plugin on the seeded agent.
757    ///
758    /// Adds a `plugin:{name}` capability ref to the agent with an empty config.
759    /// The hydrated definition must be supplied separately via
760    /// [`InProcessRuntimeBuilder::with_plugin_dir`] — this method only records
761    /// the capability ref on the agent so the capability is active for this
762    /// session. The builder looks up the hydrated config at build time from the
763    /// `plugin_capability_configs` accumulated by `with_plugin_dir` calls.
764    ///
765    /// If you need to pass the fully hydrated `AgentCapabilityConfig` (e.g.
766    /// from [`InProcessRuntimeBuilder::plugin_capability`]), use
767    /// [`Self::agent_capability`] directly.
768    pub fn agent_plugin(mut self, name: &str) -> Self {
769        self.agent = self.agent.capability(plugin_capability_id(name));
770        self
771    }
772
773    /// Add a session-level capability.
774    pub fn session_capability(mut self, capability: impl Into<AgentCapabilityConfig>) -> Self {
775        self.session = self.session.capability(capability);
776        self
777    }
778
779    /// Configure session-scoped MCP servers (specs/runtime-mcp.md). Discovered
780    /// and executed by the runtime alongside built-in tools.
781    pub fn session_mcp_servers(mut self, mcp_servers: ScopedMcpServers) -> Self {
782        self.session = self.session.mcp_servers(mcp_servers);
783        self
784    }
785
786    pub fn harness_display_name(mut self, display_name: impl Into<String>) -> Self {
787        self.harness = self.harness.display_name(display_name);
788        self
789    }
790
791    pub fn agent_display_name(mut self, display_name: impl Into<String>) -> Self {
792        self.agent = self.agent.display_name(display_name);
793        self
794    }
795
796    pub fn harness_description(mut self, description: impl Into<String>) -> Self {
797        self.harness = self.harness.description(description);
798        self
799    }
800
801    pub fn openrouter_attribution(
802        mut self,
803        http_referer: impl Into<String>,
804        title: impl Into<String>,
805    ) -> Self {
806        self.harness = self.harness.openrouter_attribution(http_referer, title);
807        self
808    }
809
810    pub fn agent_description(mut self, description: impl Into<String>) -> Self {
811        self.agent = self.agent.description(description);
812        self
813    }
814
815    pub fn session_title(mut self, title: impl Into<String>) -> Self {
816        self.session = self.session.title(title);
817        self
818    }
819
820    pub fn locale(mut self, locale: impl Into<String>) -> Self {
821        self.session = self.session.locale(locale);
822        self
823    }
824
825    pub fn tag(mut self, tag: impl Into<String>) -> Self {
826        let tag = tag.into();
827        self.harness = self.harness.tag(tag.clone());
828        self.agent = self.agent.tag(tag.clone());
829        self.session = self.session.tag(tag);
830        self
831    }
832
833    pub fn session_model_id(mut self, model_id: ModelId) -> Self {
834        self.session = self.session.model_id(model_id);
835        self
836    }
837
838    pub fn harness_default_model_id(mut self, model_id: ModelId) -> Self {
839        self.harness = self.harness.default_model_id(model_id);
840        self
841    }
842
843    pub fn agent_default_model_id(mut self, model_id: ModelId) -> Self {
844        self.agent = self.agent.default_model_id(model_id);
845        self
846    }
847
848    pub fn agent_max_iterations(mut self, max_iterations: usize) -> Self {
849        self.agent = self.agent.max_iterations(max_iterations);
850        self
851    }
852
853    pub fn session_max_iterations(mut self, max_iterations: usize) -> Self {
854        self.session = self.session.max_iterations(max_iterations);
855        self
856    }
857
858    pub fn agent_tool(mut self, tool: ToolDefinition) -> Self {
859        self.agent = self.agent.tool(tool);
860        self
861    }
862
863    pub fn session_tool(mut self, tool: ToolDefinition) -> Self {
864        self.session = self.session.tool(tool);
865        self
866    }
867
868    pub fn harness_initial_file(mut self, file: everruns_core::InitialFile) -> Self {
869        self.harness = self.harness.initial_file(file);
870        self
871    }
872
873    pub fn agent_initial_file(mut self, file: everruns_core::InitialFile) -> Self {
874        self.agent = self.agent.initial_file(file);
875        self
876    }
877
878    pub fn session_initial_file(mut self, file: everruns_core::InitialFile) -> Self {
879        self.session = self.session.initial_file(file);
880        self
881    }
882
883    pub fn harness_network_access(mut self, network_access: NetworkAccessList) -> Self {
884        self.harness = self.harness.network_access(network_access);
885        self
886    }
887
888    pub fn agent_network_access(mut self, network_access: NetworkAccessList) -> Self {
889        self.agent = self.agent.network_access(network_access);
890        self
891    }
892
893    pub fn session_network_access(mut self, network_access: NetworkAccessList) -> Self {
894        self.session = self.session.network_access(network_access);
895        self
896    }
897
898    pub fn harness_id(&self) -> HarnessId {
899        self.harness.harness_id()
900    }
901
902    pub fn agent_id(&self) -> AgentId {
903        self.agent.agent_id()
904    }
905
906    /// Pin the seeded session's id. When unset, the underlying
907    /// `SessionBuilder` generates a fresh `SessionId` at build time.
908    ///
909    /// Useful for embedders that need the id ahead of build — e.g. the
910    /// `examples/coding-cli` JSONL session log uses `<id>.jsonl` as the
911    /// filename and must open the file before the runtime exists.
912    pub fn session_id(mut self, id: SessionId) -> Self {
913        self.session = self.session.id(id);
914        self
915    }
916
917    pub(crate) fn build(self) -> (Harness, Agent, Session, SessionId) {
918        let session_id = self.session.session_id();
919        (
920            self.harness.build(),
921            self.agent.build(),
922            self.session.build(),
923            session_id,
924        )
925    }
926}
927
928#[cfg(test)]
929mod tests {
930    use super::*;
931
932    #[test]
933    fn harness_builder_openrouter_attribution_adds_metadata_keys() {
934        let harness = HarnessBuilder::new("app", "prompt")
935            .openrouter_attribution("https://app.example", "Example App")
936            .build();
937
938        assert_eq!(
939            harness
940                .embedder_metadata
941                .get(OPENROUTER_HTTP_REFERER_METADATA_KEY)
942                .map(String::as_str),
943            Some("https://app.example")
944        );
945        assert_eq!(
946            harness
947                .embedder_metadata
948                .get(OPENROUTER_X_TITLE_METADATA_KEY)
949                .map(String::as_str),
950            Some("Example App")
951        );
952    }
953
954    #[test]
955    fn single_session_builder_openrouter_attribution_configures_harness() {
956        let (harness, _agent, _session, _session_id) = SingleSessionBuilder::default()
957            .openrouter_attribution("https://single.example", "Single App")
958            .build();
959
960        assert_eq!(
961            harness
962                .embedder_metadata
963                .get(OPENROUTER_HTTP_REFERER_METADATA_KEY)
964                .map(String::as_str),
965            Some("https://single.example")
966        );
967        assert_eq!(
968            harness
969                .embedder_metadata
970                .get(OPENROUTER_X_TITLE_METADATA_KEY)
971                .map(String::as_str),
972            Some("Single App")
973        );
974    }
975}