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    harness_id: HarnessId,
262    tags: Vec<String>,
263    capabilities: Vec<AgentCapabilityConfig>,
264    initial_files: Vec<everruns_core::InitialFile>,
265    network_access: Option<NetworkAccessList>,
266    max_iterations: Option<usize>,
267    parallel_tool_calls: Option<bool>,
268    tools: Vec<ToolDefinition>,
269    mcp_servers: ScopedMcpServers,
270    status: AgentStatus,
271    created_at: Option<DateTime<Utc>>,
272    updated_at: Option<DateTime<Utc>>,
273}
274
275impl AgentBuilder {
276    /// Create an agent builder from the required embedder-facing fields.
277    pub fn new(name: impl Into<String>, system_prompt: impl Into<String>) -> Self {
278        Self {
279            id: AgentId::new(),
280            name: name.into(),
281            display_name: None,
282            description: None,
283            system_prompt: system_prompt.into(),
284            default_model_id: None,
285            harness_id: HarnessId::new(),
286            tags: Vec::new(),
287            capabilities: Vec::new(),
288            initial_files: Vec::new(),
289            network_access: None,
290            max_iterations: None,
291            parallel_tool_calls: None,
292            tools: Vec::new(),
293            mcp_servers: ScopedMcpServers::default(),
294            status: AgentStatus::Active,
295            created_at: None,
296            updated_at: None,
297        }
298    }
299
300    /// Set a stable agent id instead of generating one.
301    pub fn id(mut self, id: AgentId) -> Self {
302        self.id = id;
303        self
304    }
305
306    /// Return the id currently assigned to this builder.
307    pub fn agent_id(&self) -> AgentId {
308        self.id
309    }
310
311    pub fn name(mut self, name: impl Into<String>) -> Self {
312        self.name = name.into();
313        self
314    }
315
316    pub fn display_name(mut self, display_name: impl Into<String>) -> Self {
317        self.display_name = Some(display_name.into());
318        self
319    }
320
321    pub fn description(mut self, description: impl Into<String>) -> Self {
322        self.description = Some(description.into());
323        self
324    }
325
326    pub fn system_prompt(mut self, system_prompt: impl Into<String>) -> Self {
327        self.system_prompt = system_prompt.into();
328        self
329    }
330
331    pub fn default_model_id(mut self, default_model_id: ModelId) -> Self {
332        self.default_model_id = Some(default_model_id);
333        self
334    }
335
336    pub fn harness_id(mut self, harness_id: HarnessId) -> Self {
337        self.harness_id = harness_id;
338        self
339    }
340
341    pub fn tag(mut self, tag: impl Into<String>) -> Self {
342        self.tags.push(tag.into());
343        self
344    }
345
346    pub fn tags<I, S>(mut self, tags: I) -> Self
347    where
348        I: IntoIterator<Item = S>,
349        S: Into<String>,
350    {
351        self.tags.extend(tags.into_iter().map(Into::into));
352        self
353    }
354
355    pub fn capability(mut self, capability: impl Into<AgentCapabilityConfig>) -> Self {
356        self.capabilities.push(capability.into());
357        self
358    }
359
360    pub fn with_capability(self, capability: impl Into<AgentCapabilityConfig>) -> Self {
361        self.capability(capability)
362    }
363
364    pub fn capabilities<I, C>(mut self, capabilities: I) -> Self
365    where
366        I: IntoIterator<Item = C>,
367        C: Into<AgentCapabilityConfig>,
368    {
369        self.capabilities
370            .extend(capabilities.into_iter().map(Into::into));
371        self
372    }
373
374    pub fn initial_file(mut self, file: everruns_core::InitialFile) -> Self {
375        self.initial_files.push(file);
376        self
377    }
378
379    pub fn network_access(mut self, network_access: NetworkAccessList) -> Self {
380        self.network_access = Some(network_access);
381        self
382    }
383
384    pub fn max_iterations(mut self, max_iterations: usize) -> Self {
385        self.max_iterations = Some(max_iterations);
386        self
387    }
388
389    /// Set the request-level parallel tool calling preference (EVE-598).
390    pub fn parallel_tool_calls(mut self, parallel_tool_calls: bool) -> Self {
391        self.parallel_tool_calls = Some(parallel_tool_calls);
392        self
393    }
394
395    pub fn tool(mut self, tool: ToolDefinition) -> Self {
396        self.tools.push(tool);
397        self
398    }
399
400    pub fn tools<I>(mut self, tools: I) -> Self
401    where
402        I: IntoIterator<Item = ToolDefinition>,
403    {
404        self.tools.extend(tools);
405        self
406    }
407
408    pub fn mcp_servers(mut self, mcp_servers: ScopedMcpServers) -> Self {
409        self.mcp_servers = mcp_servers;
410        self
411    }
412
413    pub fn status(mut self, status: AgentStatus) -> Self {
414        self.status = status;
415        self
416    }
417
418    pub fn created_at(mut self, created_at: DateTime<Utc>) -> Self {
419        self.created_at = Some(created_at);
420        self
421    }
422
423    pub fn updated_at(mut self, updated_at: DateTime<Utc>) -> Self {
424        self.updated_at = Some(updated_at);
425        self
426    }
427
428    /// Build the agent. Builders do not validate domain invariants.
429    pub fn build(self) -> Agent {
430        let created_at = self.created_at.unwrap_or_else(Utc::now);
431        let updated_at = self.updated_at.unwrap_or(created_at);
432
433        Agent {
434            public_id: self.id,
435            internal_id: Uuid::nil(),
436            name: self.name,
437            display_name: self.display_name,
438            description: self.description,
439            system_prompt: self.system_prompt,
440            default_model_id: self.default_model_id,
441            harness_id: self.harness_id,
442            default_version_id: None,
443            forked_from_agent_id: None,
444            forked_from_version_id: None,
445            root_agent_id: None,
446            tags: self.tags,
447            capabilities: self.capabilities,
448            initial_files: self.initial_files,
449            network_access: self.network_access,
450            max_iterations: self.max_iterations,
451            parallel_tool_calls: self.parallel_tool_calls,
452            tools: self.tools,
453            mcp_servers: self.mcp_servers,
454            status: self.status,
455            created_at,
456            updated_at,
457            archived_at: None,
458            deleted_at: None,
459            usage: None,
460        }
461    }
462}
463
464/// Builds a [`Session`] with runtime-friendly defaults.
465#[derive(Debug, Clone)]
466pub struct SessionBuilder {
467    id: SessionId,
468    organization_id: String,
469    harness_id: HarnessId,
470    agent_id: Option<AgentId>,
471    owner_principal_id: PrincipalId,
472    title: Option<String>,
473    locale: Option<String>,
474    tags: Vec<String>,
475    model_id: Option<ModelId>,
476    capabilities: Vec<AgentCapabilityConfig>,
477    tools: Vec<ToolDefinition>,
478    mcp_servers: ScopedMcpServers,
479    system_prompt: Option<String>,
480    initial_files: Vec<everruns_core::InitialFile>,
481    network_access: Option<NetworkAccessList>,
482    max_iterations: Option<usize>,
483    parallel_tool_calls: Option<bool>,
484    status: SessionStatus,
485    created_at: Option<DateTime<Utc>>,
486    updated_at: Option<DateTime<Utc>>,
487}
488
489impl SessionBuilder {
490    /// Create a session builder for the required harness id.
491    pub fn new(harness_id: HarnessId) -> Self {
492        Self {
493            id: SessionId::new(),
494            organization_id: DEFAULT_ORG_PUBLIC_ID.to_string(),
495            harness_id,
496            agent_id: None,
497            owner_principal_id: PrincipalId::from_seed(1),
498            title: None,
499            locale: None,
500            tags: Vec::new(),
501            model_id: None,
502            capabilities: Vec::new(),
503            tools: Vec::new(),
504            mcp_servers: ScopedMcpServers::default(),
505            system_prompt: None,
506            initial_files: Vec::new(),
507            network_access: None,
508            max_iterations: None,
509            parallel_tool_calls: None,
510            status: SessionStatus::Started,
511            created_at: None,
512            updated_at: None,
513        }
514    }
515
516    /// Set a stable session id instead of generating one.
517    pub fn id(mut self, id: SessionId) -> Self {
518        self.id = id;
519        self
520    }
521
522    /// Return the id currently assigned to this builder.
523    pub fn session_id(&self) -> SessionId {
524        self.id
525    }
526
527    pub fn organization_id(mut self, organization_id: impl Into<String>) -> Self {
528        self.organization_id = organization_id.into();
529        self
530    }
531
532    pub fn harness(mut self, harness_id: HarnessId) -> Self {
533        self.harness_id = harness_id;
534        self
535    }
536
537    pub fn agent(mut self, agent_id: AgentId) -> Self {
538        self.agent_id = Some(agent_id);
539        self
540    }
541
542    pub fn owner_principal_id(mut self, owner_principal_id: PrincipalId) -> Self {
543        self.owner_principal_id = owner_principal_id;
544        self
545    }
546
547    pub fn title(mut self, title: impl Into<String>) -> Self {
548        self.title = Some(title.into());
549        self
550    }
551
552    pub fn locale(mut self, locale: impl Into<String>) -> Self {
553        self.locale = Some(locale.into());
554        self
555    }
556
557    pub fn tag(mut self, tag: impl Into<String>) -> Self {
558        self.tags.push(tag.into());
559        self
560    }
561
562    pub fn tags<I, S>(mut self, tags: I) -> Self
563    where
564        I: IntoIterator<Item = S>,
565        S: Into<String>,
566    {
567        self.tags.extend(tags.into_iter().map(Into::into));
568        self
569    }
570
571    pub fn model_id(mut self, model_id: ModelId) -> Self {
572        self.model_id = Some(model_id);
573        self
574    }
575
576    pub fn capability(mut self, capability: impl Into<AgentCapabilityConfig>) -> Self {
577        self.capabilities.push(capability.into());
578        self
579    }
580
581    pub fn with_capability(self, capability: impl Into<AgentCapabilityConfig>) -> Self {
582        self.capability(capability)
583    }
584
585    pub fn capabilities<I, C>(mut self, capabilities: I) -> Self
586    where
587        I: IntoIterator<Item = C>,
588        C: Into<AgentCapabilityConfig>,
589    {
590        self.capabilities
591            .extend(capabilities.into_iter().map(Into::into));
592        self
593    }
594
595    pub fn tool(mut self, tool: ToolDefinition) -> Self {
596        self.tools.push(tool);
597        self
598    }
599
600    pub fn tools<I>(mut self, tools: I) -> Self
601    where
602        I: IntoIterator<Item = ToolDefinition>,
603    {
604        self.tools.extend(tools);
605        self
606    }
607
608    pub fn mcp_servers(mut self, mcp_servers: ScopedMcpServers) -> Self {
609        self.mcp_servers = mcp_servers;
610        self
611    }
612
613    pub fn system_prompt(mut self, system_prompt: impl Into<String>) -> Self {
614        self.system_prompt = Some(system_prompt.into());
615        self
616    }
617
618    pub fn initial_file(mut self, file: everruns_core::InitialFile) -> Self {
619        self.initial_files.push(file);
620        self
621    }
622
623    pub fn network_access(mut self, network_access: NetworkAccessList) -> Self {
624        self.network_access = Some(network_access);
625        self
626    }
627
628    pub fn max_iterations(mut self, max_iterations: usize) -> Self {
629        self.max_iterations = Some(max_iterations);
630        self
631    }
632
633    /// Set the request-level parallel tool calling preference (EVE-598).
634    pub fn parallel_tool_calls(mut self, parallel_tool_calls: bool) -> Self {
635        self.parallel_tool_calls = Some(parallel_tool_calls);
636        self
637    }
638
639    pub fn status(mut self, status: SessionStatus) -> Self {
640        self.status = status;
641        self
642    }
643
644    pub fn created_at(mut self, created_at: DateTime<Utc>) -> Self {
645        self.created_at = Some(created_at);
646        self
647    }
648
649    pub fn updated_at(mut self, updated_at: DateTime<Utc>) -> Self {
650        self.updated_at = Some(updated_at);
651        self
652    }
653
654    /// Build the session. Builders do not validate domain invariants.
655    pub fn build(self) -> Session {
656        let created_at = self.created_at.unwrap_or_else(Utc::now);
657        let updated_at = self.updated_at.unwrap_or(created_at);
658
659        Session {
660            id: self.id,
661            workspace_id: everruns_core::WorkspaceId::from_uuid((self.id).uuid()),
662            organization_id: self.organization_id,
663            harness_id: self.harness_id,
664            agent_id: self.agent_id,
665            agent_version_id: None,
666            agent_identity_id: None,
667            owner_principal_id: self.owner_principal_id,
668            resolved_owner_user_id: None,
669            owner: None,
670            effective_owner: None,
671            title: self.title,
672            locale: self.locale,
673            preview: None,
674            output_preview: None,
675            tags: self.tags,
676            model_id: self.model_id,
677            capabilities: self.capabilities,
678            tools: self.tools,
679            mcp_servers: self.mcp_servers,
680            system_prompt: self.system_prompt,
681            initial_files: self.initial_files,
682            hints: None,
683            network_access: self.network_access,
684            max_iterations: self.max_iterations,
685            parallel_tool_calls: self.parallel_tool_calls,
686            status: self.status,
687            created_at,
688            updated_at,
689            started_at: None,
690            finished_at: None,
691            usage: None,
692            is_pinned: None,
693            active_schedule_count: None,
694            features: Vec::new(),
695            parent_session_id: None,
696            forked_from_session_id: None,
697            forked_from_sequence: None,
698            blueprint_id: None,
699            blueprint_config: None,
700        }
701    }
702}
703
704/// High-level builder for seeding one harness, one agent, and one session.
705///
706/// This is the compact path used by embedders that want a runnable runtime
707/// without constructing each core model separately.
708#[derive(Debug, Clone)]
709pub struct SingleSessionBuilder {
710    harness: HarnessBuilder,
711    agent: AgentBuilder,
712    session: SessionBuilder,
713}
714
715impl Default for SingleSessionBuilder {
716    fn default() -> Self {
717        let harness = HarnessBuilder::new("embedded-harness", "");
718        let agent = AgentBuilder::new("embedded-agent", "");
719        let session = SessionBuilder::new(harness.harness_id()).agent(agent.agent_id());
720        Self {
721            harness,
722            agent,
723            session,
724        }
725    }
726}
727
728impl SingleSessionBuilder {
729    /// Configure the seeded harness. Mutates the existing `HarnessBuilder`
730    /// in place so previously configured fields (e.g. `network_access`) are
731    /// preserved regardless of call order.
732    pub fn harness(mut self, name: impl Into<String>, system_prompt: impl Into<String>) -> Self {
733        let harness_id = self.harness.harness_id();
734        self.harness = self.harness.name(name).system_prompt(system_prompt);
735        self.session = self.session.harness(harness_id);
736        self
737    }
738
739    /// Configure the seeded agent. Mutates the existing `AgentBuilder` in
740    /// place so previously configured fields (e.g. `network_access`) are
741    /// preserved regardless of call order.
742    pub fn agent(mut self, name: impl Into<String>, system_prompt: impl Into<String>) -> Self {
743        let agent_id = self.agent.agent_id();
744        self.agent = self.agent.name(name).system_prompt(system_prompt);
745        self.session = self.session.agent(agent_id);
746        self
747    }
748
749    /// Add a harness-level capability.
750    pub fn with_capability(self, capability: impl Into<AgentCapabilityConfig>) -> Self {
751        self.harness_capability(capability)
752    }
753
754    /// Add a harness-level capability.
755    pub fn harness_capability(mut self, capability: impl Into<AgentCapabilityConfig>) -> Self {
756        self.harness = self.harness.capability(capability);
757        self
758    }
759
760    /// Add an agent-level capability.
761    pub fn agent_capability(mut self, capability: impl Into<AgentCapabilityConfig>) -> Self {
762        self.agent = self.agent.capability(capability);
763        self
764    }
765
766    /// Enable a previously loaded plugin on the seeded agent.
767    ///
768    /// Adds a `plugin:{name}` capability ref to the agent with an empty config.
769    /// The hydrated definition must be supplied separately via
770    /// [`InProcessRuntimeBuilder::with_plugin_dir`] — this method only records
771    /// the capability ref on the agent so the capability is active for this
772    /// session. The builder looks up the hydrated config at build time from the
773    /// `plugin_capability_configs` accumulated by `with_plugin_dir` calls.
774    ///
775    /// If you need to pass the fully hydrated `AgentCapabilityConfig` (e.g.
776    /// from [`InProcessRuntimeBuilder::plugin_capability`]), use
777    /// [`Self::agent_capability`] directly.
778    pub fn agent_plugin(mut self, name: &str) -> Self {
779        self.agent = self.agent.capability(plugin_capability_id(name));
780        self
781    }
782
783    /// Add a session-level capability.
784    pub fn session_capability(mut self, capability: impl Into<AgentCapabilityConfig>) -> Self {
785        self.session = self.session.capability(capability);
786        self
787    }
788
789    /// Configure session-scoped MCP servers (specs/runtime-mcp.md). Discovered
790    /// and executed by the runtime alongside built-in tools.
791    pub fn session_mcp_servers(mut self, mcp_servers: ScopedMcpServers) -> Self {
792        self.session = self.session.mcp_servers(mcp_servers);
793        self
794    }
795
796    pub fn harness_display_name(mut self, display_name: impl Into<String>) -> Self {
797        self.harness = self.harness.display_name(display_name);
798        self
799    }
800
801    pub fn agent_display_name(mut self, display_name: impl Into<String>) -> Self {
802        self.agent = self.agent.display_name(display_name);
803        self
804    }
805
806    pub fn harness_description(mut self, description: impl Into<String>) -> Self {
807        self.harness = self.harness.description(description);
808        self
809    }
810
811    pub fn openrouter_attribution(
812        mut self,
813        http_referer: impl Into<String>,
814        title: impl Into<String>,
815    ) -> Self {
816        self.harness = self.harness.openrouter_attribution(http_referer, title);
817        self
818    }
819
820    pub fn agent_description(mut self, description: impl Into<String>) -> Self {
821        self.agent = self.agent.description(description);
822        self
823    }
824
825    pub fn session_title(mut self, title: impl Into<String>) -> Self {
826        self.session = self.session.title(title);
827        self
828    }
829
830    pub fn locale(mut self, locale: impl Into<String>) -> Self {
831        self.session = self.session.locale(locale);
832        self
833    }
834
835    pub fn tag(mut self, tag: impl Into<String>) -> Self {
836        let tag = tag.into();
837        self.harness = self.harness.tag(tag.clone());
838        self.agent = self.agent.tag(tag.clone());
839        self.session = self.session.tag(tag);
840        self
841    }
842
843    pub fn session_model_id(mut self, model_id: ModelId) -> Self {
844        self.session = self.session.model_id(model_id);
845        self
846    }
847
848    pub fn harness_default_model_id(mut self, model_id: ModelId) -> Self {
849        self.harness = self.harness.default_model_id(model_id);
850        self
851    }
852
853    pub fn agent_default_model_id(mut self, model_id: ModelId) -> Self {
854        self.agent = self.agent.default_model_id(model_id);
855        self
856    }
857
858    pub fn agent_max_iterations(mut self, max_iterations: usize) -> Self {
859        self.agent = self.agent.max_iterations(max_iterations);
860        self
861    }
862
863    pub fn session_max_iterations(mut self, max_iterations: usize) -> Self {
864        self.session = self.session.max_iterations(max_iterations);
865        self
866    }
867
868    pub fn agent_tool(mut self, tool: ToolDefinition) -> Self {
869        self.agent = self.agent.tool(tool);
870        self
871    }
872
873    pub fn session_tool(mut self, tool: ToolDefinition) -> Self {
874        self.session = self.session.tool(tool);
875        self
876    }
877
878    pub fn harness_initial_file(mut self, file: everruns_core::InitialFile) -> Self {
879        self.harness = self.harness.initial_file(file);
880        self
881    }
882
883    pub fn agent_initial_file(mut self, file: everruns_core::InitialFile) -> Self {
884        self.agent = self.agent.initial_file(file);
885        self
886    }
887
888    pub fn session_initial_file(mut self, file: everruns_core::InitialFile) -> Self {
889        self.session = self.session.initial_file(file);
890        self
891    }
892
893    pub fn harness_network_access(mut self, network_access: NetworkAccessList) -> Self {
894        self.harness = self.harness.network_access(network_access);
895        self
896    }
897
898    pub fn agent_network_access(mut self, network_access: NetworkAccessList) -> Self {
899        self.agent = self.agent.network_access(network_access);
900        self
901    }
902
903    pub fn session_network_access(mut self, network_access: NetworkAccessList) -> Self {
904        self.session = self.session.network_access(network_access);
905        self
906    }
907
908    pub fn harness_id(&self) -> HarnessId {
909        self.harness.harness_id()
910    }
911
912    pub fn agent_id(&self) -> AgentId {
913        self.agent.agent_id()
914    }
915
916    /// Pin the seeded session's id. When unset, the underlying
917    /// `SessionBuilder` generates a fresh `SessionId` at build time.
918    ///
919    /// Useful for embedders that need the id ahead of build — e.g. the
920    /// `examples/coding-cli` JSONL session log uses `<id>.jsonl` as the
921    /// filename and must open the file before the runtime exists.
922    pub fn session_id(mut self, id: SessionId) -> Self {
923        self.session = self.session.id(id);
924        self
925    }
926
927    pub(crate) fn build(self) -> (Harness, Agent, Session, SessionId) {
928        let session_id = self.session.session_id();
929        (
930            self.harness.build(),
931            self.agent.build(),
932            self.session.build(),
933            session_id,
934        )
935    }
936}
937
938#[cfg(test)]
939mod tests {
940    use super::*;
941
942    #[test]
943    fn harness_builder_openrouter_attribution_adds_metadata_keys() {
944        let harness = HarnessBuilder::new("app", "prompt")
945            .openrouter_attribution("https://app.example", "Example App")
946            .build();
947
948        assert_eq!(
949            harness
950                .embedder_metadata
951                .get(OPENROUTER_HTTP_REFERER_METADATA_KEY)
952                .map(String::as_str),
953            Some("https://app.example")
954        );
955        assert_eq!(
956            harness
957                .embedder_metadata
958                .get(OPENROUTER_X_TITLE_METADATA_KEY)
959                .map(String::as_str),
960            Some("Example App")
961        );
962    }
963
964    #[test]
965    fn single_session_builder_openrouter_attribution_configures_harness() {
966        let (harness, _agent, _session, _session_id) = SingleSessionBuilder::default()
967            .openrouter_attribution("https://single.example", "Single App")
968            .build();
969
970        assert_eq!(
971            harness
972                .embedder_metadata
973                .get(OPENROUTER_HTTP_REFERER_METADATA_KEY)
974                .map(String::as_str),
975            Some("https://single.example")
976        );
977        assert_eq!(
978            harness
979                .embedder_metadata
980                .get(OPENROUTER_X_TITLE_METADATA_KEY)
981                .map(String::as_str),
982            Some("Single App")
983        );
984    }
985}