Skip to main content

everruns_host/
builders.rs

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