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    goal: Option<String>,
474    locale: Option<String>,
475    tags: Vec<String>,
476    model_id: Option<ModelId>,
477    capabilities: Vec<AgentCapabilityConfig>,
478    tools: Vec<ToolDefinition>,
479    mcp_servers: ScopedMcpServers,
480    system_prompt: Option<String>,
481    initial_files: Vec<everruns_core::InitialFile>,
482    network_access: Option<NetworkAccessList>,
483    max_iterations: Option<usize>,
484    parallel_tool_calls: Option<bool>,
485    status: SessionStatus,
486    created_at: Option<DateTime<Utc>>,
487    updated_at: Option<DateTime<Utc>>,
488}
489
490impl SessionBuilder {
491    /// Create a session builder for the required harness id.
492    pub fn new(harness_id: HarnessId) -> Self {
493        Self {
494            id: SessionId::new(),
495            organization_id: DEFAULT_ORG_PUBLIC_ID.to_string(),
496            harness_id,
497            agent_id: None,
498            owner_principal_id: PrincipalId::from_seed(1),
499            title: None,
500            goal: None,
501            locale: None,
502            tags: Vec::new(),
503            model_id: None,
504            capabilities: Vec::new(),
505            tools: Vec::new(),
506            mcp_servers: ScopedMcpServers::default(),
507            system_prompt: None,
508            initial_files: Vec::new(),
509            network_access: None,
510            max_iterations: None,
511            parallel_tool_calls: None,
512            status: SessionStatus::Started,
513            created_at: None,
514            updated_at: None,
515        }
516    }
517
518    /// Set a stable session id instead of generating one.
519    pub fn id(mut self, id: SessionId) -> Self {
520        self.id = id;
521        self
522    }
523
524    /// Return the id currently assigned to this builder.
525    pub fn session_id(&self) -> SessionId {
526        self.id
527    }
528
529    pub fn organization_id(mut self, organization_id: impl Into<String>) -> Self {
530        self.organization_id = organization_id.into();
531        self
532    }
533
534    pub fn harness(mut self, harness_id: HarnessId) -> Self {
535        self.harness_id = harness_id;
536        self
537    }
538
539    pub fn agent(mut self, agent_id: AgentId) -> Self {
540        self.agent_id = Some(agent_id);
541        self
542    }
543
544    pub fn owner_principal_id(mut self, owner_principal_id: PrincipalId) -> Self {
545        self.owner_principal_id = owner_principal_id;
546        self
547    }
548
549    pub fn title(mut self, title: impl Into<String>) -> Self {
550        self.title = Some(title.into());
551        self
552    }
553
554    pub fn goal(mut self, goal: impl Into<String>) -> Self {
555        self.goal = Some(goal.into());
556        self
557    }
558
559    pub fn locale(mut self, locale: impl Into<String>) -> Self {
560        self.locale = Some(locale.into());
561        self
562    }
563
564    pub fn tag(mut self, tag: impl Into<String>) -> Self {
565        self.tags.push(tag.into());
566        self
567    }
568
569    pub fn tags<I, S>(mut self, tags: I) -> Self
570    where
571        I: IntoIterator<Item = S>,
572        S: Into<String>,
573    {
574        self.tags.extend(tags.into_iter().map(Into::into));
575        self
576    }
577
578    pub fn model_id(mut self, model_id: ModelId) -> Self {
579        self.model_id = Some(model_id);
580        self
581    }
582
583    pub fn capability(mut self, capability: impl Into<AgentCapabilityConfig>) -> Self {
584        self.capabilities.push(capability.into());
585        self
586    }
587
588    pub fn with_capability(self, capability: impl Into<AgentCapabilityConfig>) -> Self {
589        self.capability(capability)
590    }
591
592    pub fn capabilities<I, C>(mut self, capabilities: I) -> Self
593    where
594        I: IntoIterator<Item = C>,
595        C: Into<AgentCapabilityConfig>,
596    {
597        self.capabilities
598            .extend(capabilities.into_iter().map(Into::into));
599        self
600    }
601
602    pub fn tool(mut self, tool: ToolDefinition) -> Self {
603        self.tools.push(tool);
604        self
605    }
606
607    pub fn tools<I>(mut self, tools: I) -> Self
608    where
609        I: IntoIterator<Item = ToolDefinition>,
610    {
611        self.tools.extend(tools);
612        self
613    }
614
615    pub fn mcp_servers(mut self, mcp_servers: ScopedMcpServers) -> Self {
616        self.mcp_servers = mcp_servers;
617        self
618    }
619
620    pub fn system_prompt(mut self, system_prompt: impl Into<String>) -> Self {
621        self.system_prompt = Some(system_prompt.into());
622        self
623    }
624
625    pub fn initial_file(mut self, file: everruns_core::InitialFile) -> Self {
626        self.initial_files.push(file);
627        self
628    }
629
630    pub fn network_access(mut self, network_access: NetworkAccessList) -> Self {
631        self.network_access = Some(network_access);
632        self
633    }
634
635    pub fn max_iterations(mut self, max_iterations: usize) -> Self {
636        self.max_iterations = Some(max_iterations);
637        self
638    }
639
640    /// Set the request-level parallel tool calling preference (EVE-598).
641    pub fn parallel_tool_calls(mut self, parallel_tool_calls: bool) -> Self {
642        self.parallel_tool_calls = Some(parallel_tool_calls);
643        self
644    }
645
646    pub fn status(mut self, status: SessionStatus) -> Self {
647        self.status = status;
648        self
649    }
650
651    pub fn created_at(mut self, created_at: DateTime<Utc>) -> Self {
652        self.created_at = Some(created_at);
653        self
654    }
655
656    pub fn updated_at(mut self, updated_at: DateTime<Utc>) -> Self {
657        self.updated_at = Some(updated_at);
658        self
659    }
660
661    /// Build the session. Builders do not validate domain invariants.
662    pub fn build(self) -> Session {
663        let created_at = self.created_at.unwrap_or_else(Utc::now);
664        let updated_at = self.updated_at.unwrap_or(created_at);
665
666        Session {
667            id: self.id,
668            workspace_id: everruns_core::WorkspaceId::from_uuid((self.id).uuid()),
669            organization_id: self.organization_id,
670            harness_id: self.harness_id,
671            agent_id: self.agent_id,
672            agent_version_id: None,
673            agent_identity_id: None,
674            owner_principal_id: self.owner_principal_id,
675            resolved_owner_user_id: None,
676            owner: None,
677            effective_owner: None,
678            title: self.title,
679            goal: self.goal,
680            locale: self.locale,
681            preview: None,
682            output_preview: None,
683            tags: self.tags,
684            model_id: self.model_id,
685            capabilities: self.capabilities,
686            tools: self.tools,
687            mcp_servers: self.mcp_servers,
688            system_prompt: self.system_prompt,
689            initial_files: self.initial_files,
690            hints: None,
691            network_access: self.network_access,
692            max_iterations: self.max_iterations,
693            parallel_tool_calls: self.parallel_tool_calls,
694            status: self.status,
695            created_at,
696            updated_at,
697            started_at: None,
698            finished_at: None,
699            usage: None,
700            is_pinned: None,
701            active_schedule_count: None,
702            features: Vec::new(),
703            parent_session_id: None,
704            forked_from_session_id: None,
705            forked_from_sequence: None,
706            blueprint_id: None,
707            blueprint_config: None,
708        }
709    }
710}
711
712/// High-level builder for seeding one harness, one agent, and one session.
713///
714/// This is the compact path used by embedders that want a runnable runtime
715/// without constructing each core model separately.
716#[derive(Debug, Clone)]
717pub struct SingleSessionBuilder {
718    harness: HarnessBuilder,
719    agent: AgentBuilder,
720    session: SessionBuilder,
721}
722
723impl Default for SingleSessionBuilder {
724    fn default() -> Self {
725        let harness = HarnessBuilder::new("embedded-harness", "");
726        let agent = AgentBuilder::new("embedded-agent", "");
727        let session = SessionBuilder::new(harness.harness_id()).agent(agent.agent_id());
728        Self {
729            harness,
730            agent,
731            session,
732        }
733    }
734}
735
736impl SingleSessionBuilder {
737    /// Configure the seeded harness. Mutates the existing `HarnessBuilder`
738    /// in place so previously configured fields (e.g. `network_access`) are
739    /// preserved regardless of call order.
740    pub fn harness(mut self, name: impl Into<String>, system_prompt: impl Into<String>) -> Self {
741        let harness_id = self.harness.harness_id();
742        self.harness = self.harness.name(name).system_prompt(system_prompt);
743        self.session = self.session.harness(harness_id);
744        self
745    }
746
747    /// Configure the seeded agent. Mutates the existing `AgentBuilder` in
748    /// place so previously configured fields (e.g. `network_access`) are
749    /// preserved regardless of call order.
750    pub fn agent(mut self, name: impl Into<String>, system_prompt: impl Into<String>) -> Self {
751        let agent_id = self.agent.agent_id();
752        self.agent = self.agent.name(name).system_prompt(system_prompt);
753        self.session = self.session.agent(agent_id);
754        self
755    }
756
757    /// Add a harness-level capability.
758    pub fn with_capability(self, capability: impl Into<AgentCapabilityConfig>) -> Self {
759        self.harness_capability(capability)
760    }
761
762    /// Add a harness-level capability.
763    pub fn harness_capability(mut self, capability: impl Into<AgentCapabilityConfig>) -> Self {
764        self.harness = self.harness.capability(capability);
765        self
766    }
767
768    /// Add an agent-level capability.
769    pub fn agent_capability(mut self, capability: impl Into<AgentCapabilityConfig>) -> Self {
770        self.agent = self.agent.capability(capability);
771        self
772    }
773
774    /// Enable a previously loaded plugin on the seeded agent.
775    ///
776    /// Adds a `plugin:{name}` capability ref to the agent with an empty config.
777    /// The hydrated definition must be supplied separately via
778    /// [`InProcessRuntimeBuilder::with_plugin_dir`] — this method only records
779    /// the capability ref on the agent so the capability is active for this
780    /// session. The builder looks up the hydrated config at build time from the
781    /// `plugin_capability_configs` accumulated by `with_plugin_dir` calls.
782    ///
783    /// If you need to pass the fully hydrated `AgentCapabilityConfig` (e.g.
784    /// from [`InProcessRuntimeBuilder::plugin_capability`]), use
785    /// [`Self::agent_capability`] directly.
786    pub fn agent_plugin(mut self, name: &str) -> Self {
787        self.agent = self.agent.capability(plugin_capability_id(name));
788        self
789    }
790
791    /// Add a session-level capability.
792    pub fn session_capability(mut self, capability: impl Into<AgentCapabilityConfig>) -> Self {
793        self.session = self.session.capability(capability);
794        self
795    }
796
797    /// Configure session-scoped MCP servers (specs/runtime-mcp.md). Discovered
798    /// and executed by the runtime alongside built-in tools.
799    pub fn session_mcp_servers(mut self, mcp_servers: ScopedMcpServers) -> Self {
800        self.session = self.session.mcp_servers(mcp_servers);
801        self
802    }
803
804    pub fn harness_display_name(mut self, display_name: impl Into<String>) -> Self {
805        self.harness = self.harness.display_name(display_name);
806        self
807    }
808
809    pub fn agent_display_name(mut self, display_name: impl Into<String>) -> Self {
810        self.agent = self.agent.display_name(display_name);
811        self
812    }
813
814    pub fn harness_description(mut self, description: impl Into<String>) -> Self {
815        self.harness = self.harness.description(description);
816        self
817    }
818
819    pub fn openrouter_attribution(
820        mut self,
821        http_referer: impl Into<String>,
822        title: impl Into<String>,
823    ) -> Self {
824        self.harness = self.harness.openrouter_attribution(http_referer, title);
825        self
826    }
827
828    pub fn agent_description(mut self, description: impl Into<String>) -> Self {
829        self.agent = self.agent.description(description);
830        self
831    }
832
833    pub fn session_title(mut self, title: impl Into<String>) -> Self {
834        self.session = self.session.title(title);
835        self
836    }
837
838    pub fn locale(mut self, locale: impl Into<String>) -> Self {
839        self.session = self.session.locale(locale);
840        self
841    }
842
843    pub fn tag(mut self, tag: impl Into<String>) -> Self {
844        let tag = tag.into();
845        self.harness = self.harness.tag(tag.clone());
846        self.agent = self.agent.tag(tag.clone());
847        self.session = self.session.tag(tag);
848        self
849    }
850
851    pub fn session_model_id(mut self, model_id: ModelId) -> Self {
852        self.session = self.session.model_id(model_id);
853        self
854    }
855
856    pub fn harness_default_model_id(mut self, model_id: ModelId) -> Self {
857        self.harness = self.harness.default_model_id(model_id);
858        self
859    }
860
861    pub fn agent_default_model_id(mut self, model_id: ModelId) -> Self {
862        self.agent = self.agent.default_model_id(model_id);
863        self
864    }
865
866    pub fn agent_max_iterations(mut self, max_iterations: usize) -> Self {
867        self.agent = self.agent.max_iterations(max_iterations);
868        self
869    }
870
871    pub fn session_max_iterations(mut self, max_iterations: usize) -> Self {
872        self.session = self.session.max_iterations(max_iterations);
873        self
874    }
875
876    pub fn agent_tool(mut self, tool: ToolDefinition) -> Self {
877        self.agent = self.agent.tool(tool);
878        self
879    }
880
881    pub fn session_tool(mut self, tool: ToolDefinition) -> Self {
882        self.session = self.session.tool(tool);
883        self
884    }
885
886    pub fn harness_initial_file(mut self, file: everruns_core::InitialFile) -> Self {
887        self.harness = self.harness.initial_file(file);
888        self
889    }
890
891    pub fn agent_initial_file(mut self, file: everruns_core::InitialFile) -> Self {
892        self.agent = self.agent.initial_file(file);
893        self
894    }
895
896    pub fn session_initial_file(mut self, file: everruns_core::InitialFile) -> Self {
897        self.session = self.session.initial_file(file);
898        self
899    }
900
901    pub fn harness_network_access(mut self, network_access: NetworkAccessList) -> Self {
902        self.harness = self.harness.network_access(network_access);
903        self
904    }
905
906    pub fn agent_network_access(mut self, network_access: NetworkAccessList) -> Self {
907        self.agent = self.agent.network_access(network_access);
908        self
909    }
910
911    pub fn session_network_access(mut self, network_access: NetworkAccessList) -> Self {
912        self.session = self.session.network_access(network_access);
913        self
914    }
915
916    pub fn harness_id(&self) -> HarnessId {
917        self.harness.harness_id()
918    }
919
920    pub fn agent_id(&self) -> AgentId {
921        self.agent.agent_id()
922    }
923
924    /// Pin the seeded session's id. When unset, the underlying
925    /// `SessionBuilder` generates a fresh `SessionId` at build time.
926    ///
927    /// Useful for embedders that need the id ahead of build — e.g. the
928    /// `examples/coding-cli` JSONL session log uses `<id>.jsonl` as the
929    /// filename and must open the file before the runtime exists.
930    pub fn session_id(mut self, id: SessionId) -> Self {
931        self.session = self.session.id(id);
932        self
933    }
934
935    pub(crate) fn build(self) -> (Harness, Agent, Session, SessionId) {
936        let session_id = self.session.session_id();
937        (
938            self.harness.build(),
939            self.agent.build(),
940            self.session.build(),
941            session_id,
942        )
943    }
944}
945
946#[cfg(test)]
947mod tests {
948    use super::*;
949
950    #[test]
951    fn harness_builder_openrouter_attribution_adds_metadata_keys() {
952        let harness = HarnessBuilder::new("app", "prompt")
953            .openrouter_attribution("https://app.example", "Example App")
954            .build();
955
956        assert_eq!(
957            harness
958                .embedder_metadata
959                .get(OPENROUTER_HTTP_REFERER_METADATA_KEY)
960                .map(String::as_str),
961            Some("https://app.example")
962        );
963        assert_eq!(
964            harness
965                .embedder_metadata
966                .get(OPENROUTER_X_TITLE_METADATA_KEY)
967                .map(String::as_str),
968            Some("Example App")
969        );
970    }
971
972    #[test]
973    fn single_session_builder_openrouter_attribution_configures_harness() {
974        let (harness, _agent, _session, _session_id) = SingleSessionBuilder::default()
975            .openrouter_attribution("https://single.example", "Single App")
976            .build();
977
978        assert_eq!(
979            harness
980                .embedder_metadata
981                .get(OPENROUTER_HTTP_REFERER_METADATA_KEY)
982                .map(String::as_str),
983            Some("https://single.example")
984        );
985        assert_eq!(
986            harness
987                .embedder_metadata
988                .get(OPENROUTER_X_TITLE_METADATA_KEY)
989                .map(String::as_str),
990            Some("Single App")
991        );
992    }
993}