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