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