1use std::collections::BTreeMap;
16
17use ingot_ir::{AgentIr, Decision, Node, NodeKind, RefScope, TemplatePart, Value as IrValue};
18use serde_json::{json, Value};
19
20use crate::events::{Artifact, EventSink, RunEvent, VerifyOutcome};
21use crate::price::{parse_micros, Pricing, Spend};
22use crate::provider::{CompletionRequest, ModelProvider, ModelSelection, ProviderError, Usage};
23use crate::schema;
24use crate::snapshot::{self, Resumption};
25use crate::tools::{ApprovalMode, ApprovalRequest, ToolError, ToolHost, ToolInvocation};
26use crate::{RunError, RunReport};
27
28const NON_STREAMING_CEILING: u32 = 16_000;
35
36const STREAMING_CEILING: u32 = 64_000;
43
44pub struct RunOptions {
45 pub inputs: BTreeMap<String, Value>,
47 pub approval: ApprovalMode,
48 pub max_steps: u32,
51 pub memory: BTreeMap<String, Value>,
55 pub stop_at: Option<String>,
61 pub resume: Option<Resumption>,
66 pub pricing: Pricing,
73}
74
75impl Default for RunOptions {
76 fn default() -> Self {
77 RunOptions {
78 inputs: BTreeMap::new(),
79 approval: ApprovalMode::Deny,
80 max_steps: 1_000,
81 memory: BTreeMap::new(),
82 stop_at: None,
83 resume: None,
84 pricing: Pricing::default(),
85 }
86 }
87}
88
89pub type AgentRegistry = BTreeMap<String, AgentIr>;
91
92struct Interp<'a> {
93 ir: &'a AgentIr,
94 registry: &'a AgentRegistry,
95 provider: &'a mut dyn ModelProvider,
96 tools: &'a mut dyn ToolHost,
97 sink: &'a mut dyn EventSink,
98 approval: &'a mut ApprovalMode,
99
100 bindings: BTreeMap<String, Value>,
101 state: BTreeMap<String, Value>,
102 memory: BTreeMap<String, Value>,
105 outputs: BTreeMap<String, Artifact>,
106
107 steps: u32,
108 max_steps: u32,
109 usage: Usage,
110 pricing: Pricing,
111 spend: Spend,
112
113 stop_at: Option<String>,
115 stopped: Option<(String, String)>,
121 run_inputs: BTreeMap<String, Value>,
123 model_calls: u32,
125 tool_calls: u32,
126}
127
128pub fn run(
130 ir: &AgentIr,
131 registry: &AgentRegistry,
132 provider: &mut dyn ModelProvider,
133 tools: &mut dyn ToolHost,
134 sink: &mut dyn EventSink,
135 options: RunOptions,
136) -> Result<RunReport, RunError> {
137 let RunOptions {
138 inputs,
139 mut approval,
140 max_steps,
141 memory,
142 stop_at,
143 resume,
144 pricing,
145 } = options;
146 run_nested(
147 ir,
148 registry,
149 provider,
150 tools,
151 sink,
152 inputs,
153 &mut approval,
154 max_steps,
155 memory,
156 Interruption { stop_at, resume },
157 &pricing,
158 )
159}
160
161#[allow(clippy::too_many_arguments)]
169fn run_nested(
170 ir: &AgentIr,
171 registry: &AgentRegistry,
172 provider: &mut dyn ModelProvider,
173 tools: &mut dyn ToolHost,
174 sink: &mut dyn EventSink,
175 inputs: BTreeMap<String, Value>,
176 approval: &mut ApprovalMode,
177 step_ceiling: u32,
178 stored: BTreeMap<String, Value>,
182 interruption: Interruption,
185 pricing: &Pricing,
188) -> Result<RunReport, RunError> {
189 check_ir_version(ir)?;
190
191 let inputs = match &interruption.resume {
196 Some(snapshot) => {
197 if !inputs.is_empty() && inputs != snapshot.inputs {
198 return Err(RunError::InputsAfterResume);
199 }
200 snapshot.inputs.clone()
201 }
202 None => inputs,
203 };
204
205 let mut bindings = BTreeMap::new();
206 for (name, declared_type) in &ir.inputs {
207 let Some(value) = inputs.get(name) else {
208 return Err(RunError::MissingInput {
209 name: name.clone(),
210 ty: declared_type.clone(),
211 });
212 };
213 schema::validate(value, declared_type, &ir.types).map_err(|reason| {
214 RunError::InvalidInput {
215 name: name.clone(),
216 reason,
217 }
218 })?;
219 bindings.insert(name.clone(), value.clone());
220 }
221 for name in inputs.keys() {
222 if !ir.inputs.contains_key(name) {
223 return Err(RunError::UnknownInput {
224 name: name.clone(),
225 expected: ir.inputs.keys().cloned().collect(),
226 });
227 }
228 }
229
230 let mut memory = BTreeMap::new();
236 for (name, field) in &ir.persistent {
237 let value = stored
238 .get(name)
239 .cloned()
240 .unwrap_or_else(|| field.initial.clone());
241 schema::validate(&value, &field.ty, &ir.types).map_err(|reason| {
242 RunError::InvalidMemory {
243 field: name.clone(),
244 reason,
245 }
246 })?;
247 memory.insert(name.clone(), value);
248 }
249 for name in stored.keys() {
250 if !ir.persistent.contains_key(name) {
251 return Err(RunError::UnknownMemoryField {
252 field: name.clone(),
253 expected: ir.persistent.keys().cloned().collect(),
254 });
255 }
256 }
257
258 let Interruption { stop_at, resume } = interruption;
262 if let Some(label) = &stop_at {
263 check_stop_label(ir, label)?;
264 }
265 if let Some(snapshot) = &resume {
266 snapshot.check(ir).map_err(RunError::Snapshot)?;
267 }
268
269 let entry = match &resume {
273 Some(snapshot) => {
274 bindings = snapshot.bindings.clone();
275 Some(snapshot.resume_at.clone())
276 }
277 None => ir.entry.clone(),
278 };
279
280 sink.emit(RunEvent::RunStarted {
281 agent: ir.agent.clone(),
282 provider: provider.name().to_string(),
283 });
284
285 let max_steps = match ir.budget.steps {
286 Some(limit) if limit >= 0 => (limit as u32).min(step_ceiling),
287 _ => step_ceiling,
288 };
289
290 let mut interp = Interp {
291 ir,
292 registry,
293 provider,
294 tools,
295 sink,
296 approval,
297 bindings,
298 state: resume
302 .as_ref()
303 .map(|snapshot| snapshot.state.clone())
304 .unwrap_or_default(),
305 memory,
306 outputs: resume
307 .as_ref()
308 .map(|snapshot| snapshot.outputs.clone())
309 .unwrap_or_default(),
310 steps: resume.as_ref().map(|snapshot| snapshot.steps).unwrap_or(0),
311 max_steps,
312 usage: resume
313 .as_ref()
314 .map(|snapshot| snapshot.usage)
315 .unwrap_or_default(),
316 pricing: pricing.clone(),
317 spend: resume
318 .as_ref()
319 .map(|snapshot| snapshot.spend.clone())
320 .unwrap_or_default(),
321 stop_at,
322 stopped: None,
323 run_inputs: match &resume {
324 Some(snapshot) => snapshot.inputs.clone(),
325 None => inputs,
326 },
327 model_calls: resume
328 .as_ref()
329 .map(|snapshot| snapshot.model_calls)
330 .unwrap_or(0),
331 tool_calls: resume
332 .as_ref()
333 .map(|snapshot| snapshot.tool_calls)
334 .unwrap_or(0),
335 };
336
337 let result = interp.run_region(entry.as_deref());
338
339 match result {
340 Ok(()) => {
341 let stopped = interp.take_snapshot();
342 let report = RunReport {
343 agent: ir.agent.clone(),
344 outputs: interp.outputs,
345 memory: interp.memory,
346 stopped,
347 usage: interp.usage,
348 steps: interp.steps,
349 spend: interp.spend.clone(),
350 };
351 if let Some(snapshot) = &report.stopped {
352 sink.emit(RunEvent::RunStopped {
353 node: snapshot.stopped_at.clone(),
354 label: snapshot.label.clone(),
355 });
356 return Ok(report);
361 }
362 sink.emit(RunEvent::RunFinished {
363 steps: report.steps,
364 usage: report.usage,
365 });
366 for name in ir.outputs.keys() {
367 if !report.outputs.contains_key(name) {
368 return Err(RunError::OutputNotProduced { name: name.clone() });
369 }
370 }
371 Ok(report)
372 }
373 Err(error) => {
374 sink.emit(RunEvent::RunFailed {
375 reason: error.to_string(),
376 });
377 Err(error)
378 }
379 }
380}
381
382#[derive(Default)]
385struct Interruption {
386 stop_at: Option<String>,
387 resume: Option<Resumption>,
388}
389
390fn check_stop_label(ir: &AgentIr, label: &str) -> Result<(), RunError> {
395 if snapshot::resumable_labels(ir).iter().any(|it| it == label) {
396 return Ok(());
397 }
398 let nested = snapshot::all_checkpoint_labels(ir)
399 .iter()
400 .any(|it| it == label);
401 Err(RunError::NotResumable {
402 label: label.to_string(),
403 nested,
404 available: snapshot::resumable_labels(ir),
405 })
406}
407
408fn check_ir_version(ir: &AgentIr) -> Result<(), RunError> {
409 let major = ir.ir_version.split('.').next().unwrap_or_default();
410 if major != "0" {
411 return Err(RunError::UnsupportedIrVersion {
412 found: ir.ir_version.clone(),
413 supported: ingot_ir::IR_VERSION.to_string(),
414 });
415 }
416 Ok(())
417}
418
419impl Interp<'_> {
420 fn node(&self, id: &str) -> Result<&Node, RunError> {
421 self.ir
422 .node(id)
423 .ok_or_else(|| RunError::MalformedIr(format!("node `{id}` does not exist")))
424 }
425
426 fn take_snapshot(&self) -> Option<Resumption> {
428 let (node, label) = self.stopped.clone()?;
429 let resume_at = self.ir.node(&node).and_then(|node| node.next.clone())?;
433 Some(Resumption {
434 ingot_snapshot: snapshot::SNAPSHOT_VERSION.to_string(),
435 kind: snapshot::KIND.to_string(),
436 agent: self.ir.agent.clone(),
437 artifact: snapshot::artifact_digest(self.ir),
438 label,
439 stopped_at: node,
440 resume_at,
441 inputs: self.run_inputs.clone(),
442 bindings: self.bindings.clone(),
443 state: self.state.clone(),
444 outputs: self.outputs.clone(),
445 steps: self.steps,
446 usage: self.usage,
447 spend: self.spend.clone(),
448 model_calls: self.model_calls,
449 tool_calls: self.tool_calls,
450 })
451 }
452
453 fn run_region(&mut self, entry: Option<&str>) -> Result<(), RunError> {
455 let mut current = entry.map(str::to_string);
456 while let Some(id) = current {
457 if self.stopped.is_some() {
458 return Ok(());
459 }
460 let node = self.node(&id)?.clone();
461 self.sink.emit(RunEvent::NodeStarted {
462 node: node.id.clone(),
463 kind: node.kind.as_str().to_string(),
464 });
465 self.run_node(&node)?;
466 current = node.next.clone();
467 }
468 Ok(())
469 }
470
471 fn run_node(&mut self, node: &Node) -> Result<(), RunError> {
472 match node.kind {
473 NodeKind::LlmCall => self.run_llm_call(node),
474 NodeKind::ToolCall => self.run_tool_call(node),
475 NodeKind::AgentCall => self.run_agent_call(node),
476 NodeKind::Branch => self.run_branch(node),
477 NodeKind::Parallel => self.run_parallel(node),
478 NodeKind::Loop => self.run_loop(node),
479 NodeKind::Approval => self.run_approval(node),
480 NodeKind::Verify => self.run_verify(node),
481 NodeKind::StateRead => self.run_state_read(node),
482 NodeKind::StateWrite => self.run_state_write(node),
483 NodeKind::ArtifactEmit => self.run_emit(node),
484 NodeKind::Checkpoint => {
485 let label = node.label.clone().unwrap_or_default();
486 self.sink.emit(RunEvent::Checkpoint {
487 node: node.id.clone(),
488 label: label.clone(),
489 });
490 if node.resumable && self.stop_at.as_deref() == Some(label.as_str()) {
494 self.stopped = Some((node.id.clone(), label));
495 }
496 Ok(())
497 }
498 }
499 }
500
501 fn charge_step(&mut self, node: &Node) -> Result<(), RunError> {
504 self.steps += 1;
505 if self.steps > self.max_steps {
506 return Err(RunError::BudgetExceeded {
507 budget: "steps".to_string(),
508 limit: self.max_steps.to_string(),
509 node: node.id.clone(),
510 });
511 }
512 Ok(())
513 }
514
515 fn charge_cost(&mut self, node: &Node, model: &str, usage: Usage) -> Result<(), RunError> {
522 let Some(budget) = self.ir.budget.cost.clone() else {
523 return Ok(());
526 };
527 self.spend
528 .add(model, self.pricing.charge(model, usage, &budget.currency));
529
530 let Some(limit) = parse_micros(&budget.amount) else {
531 return Ok(());
532 };
533 if self.spend.is_complete() && self.spend.micros() > limit {
534 return Err(RunError::BudgetExceeded {
535 budget: "cost".to_string(),
536 limit: format!("{} {}", budget.amount, budget.currency.to_ascii_uppercase()),
537 node: node.id.clone(),
538 });
539 }
540 Ok(())
541 }
542
543 fn charge_tokens(&mut self, node: &Node, usage: Usage) -> Result<(), RunError> {
544 self.usage.add(usage);
545 if let Some(limit) = self.ir.budget.tokens {
546 if limit >= 0 && self.usage.total() > limit as u64 {
547 return Err(RunError::BudgetExceeded {
548 budget: "tokens".to_string(),
549 limit: limit.to_string(),
550 node: node.id.clone(),
551 });
552 }
553 }
554 Ok(())
555 }
556
557 fn check_effects(&mut self, node: &Node, effects: &[String]) -> Result<bool, RunError> {
564 let mut needs_approval = Vec::new();
565 for effect in effects {
566 if effect == "model_access" {
567 continue;
568 }
569 let subject = subject_for_effect(effect);
570 match self.ir.policy.get(subject) {
571 Some(rule) => match rule.decision {
572 Decision::Allow => {}
573 Decision::RequireApproval => needs_approval.push(effect.clone()),
574 Decision::Deny => {
575 return Err(RunError::CapabilityDenied {
576 node: node.id.clone(),
577 effect: effect.clone(),
578 explicit: true,
579 })
580 }
581 },
582 None => {
583 return Err(RunError::CapabilityDenied {
584 node: node.id.clone(),
585 effect: effect.clone(),
586 explicit: false,
587 })
588 }
589 }
590 }
591 Ok(!needs_approval.is_empty())
592 }
593
594 fn run_llm_call(&mut self, node: &Node) -> Result<(), RunError> {
597 self.charge_step(node)?;
598 self.model_calls += 1;
602
603 let response_type = node
604 .response_type
605 .clone()
606 .ok_or_else(|| RunError::MalformedIr(format!("`{}` has no responseType", node.id)))?;
607 let shape = schema::response_shape(&response_type, &self.ir.types).map_err(|error| {
608 RunError::UnsupportedResponseType {
609 node: node.id.clone(),
610 ty: error.ty,
611 reason: error.reason,
612 }
613 })?;
614
615 let prompt_value = node
616 .prompt
617 .as_ref()
618 .ok_or_else(|| RunError::MalformedIr(format!("`{}` has no prompt", node.id)))?;
619 let prompt = self.render_prompt(prompt_value)?;
620
621 let mut system = None;
622 let mut context = Vec::new();
623 for argument in &node.args {
624 let value = self.eval(&argument.value)?;
625 match argument.name.as_str() {
626 "system" => system = value.as_str().map(str::to_string),
627 "temperature" | "max_tokens" => {}
632 name => context.push((name.to_string(), value)),
633 }
634 }
635
636 let request = CompletionRequest {
637 node: node.id.clone(),
638 model: self.model_selection(),
639 system,
640 prompt,
641 context,
642 response_type: response_type.clone(),
643 shape,
644 max_tokens: self.max_output_tokens(),
645 };
646
647 let node_id = node.id.clone();
652 let mut shown = false;
653 let attempt = {
654 let sink = &mut *self.sink;
655 self.provider.complete_streaming(&request, &mut |text| {
656 shown = true;
657 sink.delta(&node_id, text);
658 })
659 };
660
661 let response = match attempt {
666 Ok(response) => response,
667 Err(error) => {
668 if shown {
669 self.sink.settled(&node_id, false);
670 }
671 return Err(RunError::Provider {
672 node: node.id.clone(),
673 source: error,
674 });
675 }
676 };
677
678 if let Err(reason) = schema::validate(&response.value, &response_type, &self.ir.types) {
679 if shown {
680 self.sink.settled(&node_id, false);
681 }
682 return Err(RunError::Provider {
683 node: node.id.clone(),
684 source: ProviderError::InvalidResponse(reason),
685 });
686 }
687 if shown {
688 self.sink.settled(&node_id, true);
689 }
690
691 self.sink.emit(RunEvent::ModelCall {
692 node: node.id.clone(),
693 model: response.model.clone(),
694 response_type,
695 usage: response.usage,
696 });
697 self.charge_tokens(node, response.usage)?;
698 self.charge_cost(node, &response.model, response.usage)?;
699 self.bind(node, response.value);
700 Ok(())
701 }
702
703 fn run_tool_call(&mut self, node: &Node) -> Result<(), RunError> {
704 self.tool_calls += 1;
705 let reference = node
706 .tool
707 .clone()
708 .ok_or_else(|| RunError::MalformedIr(format!("`{}` names no tool", node.id)))?;
709 let binding = self
710 .ir
711 .tools
712 .iter()
713 .find(|tool| tool.reference == reference)
714 .ok_or_else(|| {
715 RunError::MalformedIr(format!(
716 "`{}` calls `{reference}`, which the artifact does not grant",
717 node.id
718 ))
719 })?
720 .clone();
721
722 self.check_effects(node, &node.effects)?;
723 self.charge_step(node)?;
724
725 if !self.tools.provides(&binding.name) {
726 return Err(RunError::Tool {
727 node: node.id.clone(),
728 source: ToolError::NotAvailable(binding.name.clone()),
729 });
730 }
731
732 let mut arguments = BTreeMap::new();
733 for argument in &node.args {
734 arguments.insert(argument.name.clone(), self.eval(&argument.value)?);
735 }
736
737 let invocation = ToolInvocation {
738 node: node.id.clone(),
739 agent: self.ir.agent.clone(),
740 reference: binding.reference.clone(),
741 name: binding.name.clone(),
742 transport: binding.transport.clone(),
743 arguments,
744 effects: node.effects.clone(),
745 result_type: binding.signature.result.clone(),
746 };
747
748 let result = self
749 .tools
750 .call(&invocation)
751 .map_err(|source| RunError::Tool {
752 node: node.id.clone(),
753 source,
754 })?;
755
756 schema::validate(&result, &binding.signature.result, &self.ir.types).map_err(|reason| {
757 RunError::Tool {
758 node: node.id.clone(),
759 source: ToolError::InvalidResult(reason),
760 }
761 })?;
762
763 self.sink.emit(RunEvent::ToolCall {
764 node: node.id.clone(),
765 tool: binding.reference,
766 effects: node.effects.clone(),
767 });
768 self.bind(node, result);
769 Ok(())
770 }
771
772 fn run_agent_call(&mut self, node: &Node) -> Result<(), RunError> {
773 let name = node
774 .agent
775 .clone()
776 .ok_or_else(|| RunError::MalformedIr(format!("`{}` names no agent", node.id)))?;
777 let sub = self
778 .registry
779 .get(&name)
780 .ok_or_else(|| RunError::AgentNotAvailable {
781 node: node.id.clone(),
782 agent: name.clone(),
783 })?;
784
785 self.check_effects(node, &node.effects)?;
786 self.charge_step(node)?;
787 self.sink.emit(RunEvent::AgentCall {
788 node: node.id.clone(),
789 agent: name.clone(),
790 });
791
792 let mut inputs = BTreeMap::new();
793 for argument in &node.args {
794 inputs.insert(argument.name.clone(), self.eval(&argument.value)?);
795 }
796
797 let sub = sub.clone();
805 let report = run_nested(
806 &sub,
807 self.registry,
808 self.provider,
809 self.tools,
810 self.sink,
811 inputs,
812 &mut *self.approval,
813 self.max_steps.saturating_sub(self.steps).max(1),
814 BTreeMap::new(),
818 Interruption::default(),
822 &self.pricing,
823 )
824 .map_err(|error| RunError::SubAgent {
825 node: node.id.clone(),
826 agent: name.clone(),
827 source: Box::new(error),
828 })?;
829
830 self.steps += report.steps;
831 self.charge_tokens(node, report.usage)?;
832
833 let value = report
834 .outputs
835 .values()
836 .next()
837 .map(|artifact| artifact.value.clone())
838 .unwrap_or(Value::Null);
839 self.bind(node, value);
840 Ok(())
841 }
842
843 fn run_branch(&mut self, node: &Node) -> Result<(), RunError> {
844 let condition = node
845 .condition
846 .as_ref()
847 .ok_or_else(|| RunError::MalformedIr(format!("`{}` has no condition", node.id)))?;
848 let taken = self.eval(condition)?.as_bool().ok_or_else(|| {
849 RunError::MalformedIr(format!(
850 "`{}` condition did not evaluate to a boolean",
851 node.id
852 ))
853 })?;
854
855 self.sink.emit(RunEvent::BranchTaken {
856 node: node.id.clone(),
857 arm: if taken { "then".into() } else { "else".into() },
858 });
859 let arm = if taken {
860 node.then.as_deref()
861 } else {
862 node.otherwise.as_deref()
863 };
864 self.run_region(arm)
865 }
866
867 fn run_parallel(&mut self, node: &Node) -> Result<(), RunError> {
872 let source = node
873 .source
874 .as_ref()
875 .ok_or_else(|| RunError::MalformedIr(format!("`{}` has no source", node.id)))?;
876 let binder = node
877 .binder
878 .clone()
879 .ok_or_else(|| RunError::MalformedIr(format!("`{}` has no binder", node.id)))?;
880 let items = self.eval(source)?;
881 let items = items.as_array().cloned().ok_or_else(|| {
882 RunError::MalformedIr(format!("`{}` source did not evaluate to a list", node.id))
883 })?;
884
885 let last_body_node = node
886 .body
887 .as_deref()
888 .map(|entry| self.last_of_region(entry))
889 .transpose()?
890 .flatten();
891
892 let total = items.len();
893 let mut collected = Vec::with_capacity(total);
894 let shadowed = self.bindings.remove(&binder);
895
896 for (index, item) in items.into_iter().enumerate() {
897 self.sink.emit(RunEvent::MapIteration {
898 node: node.id.clone(),
899 index,
900 total,
901 });
902 self.bindings.insert(binder.clone(), item);
903 self.run_region(node.body.as_deref())?;
904 if self.stopped.is_some() {
905 break;
906 }
907
908 let value = match &last_body_node {
917 Some(id) => {
918 let last = self.node(id)?.clone();
919 match &last.binding {
920 Some(name) => self.bindings.get(name).cloned().unwrap_or(Value::Null),
921 None => {
922 return Err(RunError::MalformedIr(format!(
923 "`{}` ends its body at `{id}`, which binds nothing, so an iteration has no value to collect",
924 node.id
925 )))
926 }
927 }
928 }
929 None => Value::Null,
930 };
931 collected.push(value);
932 }
933
934 self.bindings.remove(&binder);
935 if let Some(shadowed) = shadowed {
936 self.bindings.insert(binder, shadowed);
937 }
938 self.bind(node, Value::Array(collected));
939 Ok(())
940 }
941
942 fn run_loop(&mut self, node: &Node) -> Result<(), RunError> {
943 let max = node.max_iterations.unwrap_or(0).max(0) as u32;
946 for iteration in 0..max {
947 if let Some(guard) = &node.guard {
948 let keep_going = self.eval(guard)?.as_bool().ok_or_else(|| {
949 RunError::MalformedIr(format!(
950 "`{}` guard did not evaluate to a boolean",
951 node.id
952 ))
953 })?;
954 if !keep_going {
955 break;
956 }
957 }
958 self.sink.emit(RunEvent::LoopIteration {
959 node: node.id.clone(),
960 iteration: iteration + 1,
961 });
962 self.run_region(node.body.as_deref())?;
963 if self.stopped.is_some() {
967 break;
968 }
969 }
970 Ok(())
971 }
972
973 fn run_approval(&mut self, node: &Node) -> Result<(), RunError> {
974 let reason = node
975 .label
976 .clone()
977 .unwrap_or_else(|| "approval required".to_string());
978 self.sink.emit(RunEvent::ApprovalRequested {
979 node: node.id.clone(),
980 effects: node.effects.clone(),
981 reason: reason.clone(),
982 });
983
984 let allowed = match self.approval {
985 ApprovalMode::AssumeYes => true,
986 ApprovalMode::Deny => false,
987 ApprovalMode::Ask(handler) => handler.approve(&ApprovalRequest {
988 node: node.id.clone(),
989 effects: node.effects.clone(),
990 reason: reason.clone(),
991 }),
992 };
993
994 self.sink.emit(RunEvent::ApprovalDecided {
995 node: node.id.clone(),
996 allowed,
997 });
998 if allowed {
999 Ok(())
1000 } else {
1001 Err(RunError::ApprovalDenied {
1002 node: node.id.clone(),
1003 reason,
1004 })
1005 }
1006 }
1007
1008 fn run_verify(&mut self, node: &Node) -> Result<(), RunError> {
1009 let verifier = node
1010 .verifier
1011 .clone()
1012 .ok_or_else(|| RunError::MalformedIr(format!("`{}` names no verifier", node.id)))?;
1013 for argument in &node.args {
1014 self.eval(&argument.value)?;
1015 }
1016
1017 let Some(condition) = &node.condition else {
1022 self.sink.emit(RunEvent::Verified {
1023 node: node.id.clone(),
1024 verifier: verifier.clone(),
1025 outcome: VerifyOutcome::NotPerformed,
1026 });
1027 return Ok(());
1028 };
1029
1030 let held = self.eval(condition)?.as_bool().ok_or_else(|| {
1031 RunError::MalformedIr(format!(
1032 "`{}` condition did not evaluate to a boolean",
1033 node.id
1034 ))
1035 })?;
1036
1037 self.sink.emit(RunEvent::Verified {
1038 node: node.id.clone(),
1039 verifier: verifier.clone(),
1040 outcome: if held {
1041 VerifyOutcome::Passed
1042 } else {
1043 VerifyOutcome::Failed
1044 },
1045 });
1046
1047 if held {
1051 Ok(())
1052 } else {
1053 Err(RunError::VerificationFailed {
1054 node: node.id.clone(),
1055 verifier: verifier.clone(),
1056 })
1057 }
1058 }
1059
1060 fn run_state_read(&mut self, node: &Node) -> Result<(), RunError> {
1061 let field = node
1062 .field
1063 .clone()
1064 .ok_or_else(|| RunError::MalformedIr(format!("`{}` names no state field", node.id)))?;
1065 let value = match node.scope {
1066 Some(RefScope::Memory) => self.memory.get(&field).cloned().ok_or_else(|| {
1067 RunError::MalformedIr(format!(
1068 "`{}` reads `memory.{field}`, which is not declared",
1069 node.id
1070 ))
1071 })?,
1072 _ => self
1073 .state
1074 .get(&field)
1075 .cloned()
1076 .ok_or_else(|| RunError::StateNotSet {
1077 node: node.id.clone(),
1078 field: field.clone(),
1079 })?,
1080 };
1081 self.bind(node, value);
1082 Ok(())
1083 }
1084
1085 fn run_state_write(&mut self, node: &Node) -> Result<(), RunError> {
1086 let field = node
1087 .field
1088 .clone()
1089 .ok_or_else(|| RunError::MalformedIr(format!("`{}` names no state field", node.id)))?;
1090 let value = node
1091 .value
1092 .as_ref()
1093 .ok_or_else(|| RunError::MalformedIr(format!("`{}` has no value", node.id)))?;
1094 let value = self.eval(value)?;
1095 let persistent = matches!(node.scope, Some(RefScope::Memory));
1096 let declared = if persistent {
1097 self.ir.persistent.get(&field).map(|field| field.ty.clone())
1098 } else {
1099 self.ir.state.get(&field).cloned()
1100 };
1101 if let Some(declared) = declared {
1102 let root = if persistent { "memory" } else { "state" };
1103 schema::validate(&value, &declared, &self.ir.types).map_err(|reason| {
1104 RunError::TypeMismatch {
1105 node: node.id.clone(),
1106 what: format!("{root}.{field}"),
1107 reason,
1108 }
1109 })?;
1110 }
1111 if persistent {
1112 self.memory.insert(field.clone(), value);
1113 } else {
1114 self.state.insert(field.clone(), value);
1115 }
1116 self.sink.emit(RunEvent::StateWritten {
1117 node: node.id.clone(),
1118 field,
1119 });
1120 Ok(())
1121 }
1122
1123 fn run_emit(&mut self, node: &Node) -> Result<(), RunError> {
1124 let output = node
1125 .output
1126 .clone()
1127 .ok_or_else(|| RunError::MalformedIr(format!("`{}` names no output", node.id)))?;
1128 let value = node
1129 .value
1130 .as_ref()
1131 .ok_or_else(|| RunError::MalformedIr(format!("`{}` has no value", node.id)))?;
1132 let value = self.eval(value)?;
1133
1134 let declared = self.ir.outputs.get(&output).cloned().ok_or_else(|| {
1135 RunError::MalformedIr(format!(
1136 "`{}` emits `{output}`, which is not declared",
1137 node.id
1138 ))
1139 })?;
1140 let content_type = declared
1141 .strip_prefix("artifact<")
1142 .and_then(|rest| rest.strip_suffix('>'))
1143 .unwrap_or(&declared)
1144 .to_string();
1145 schema::validate(&value, &content_type, &self.ir.types).map_err(|reason| {
1146 RunError::TypeMismatch {
1147 node: node.id.clone(),
1148 what: format!("output `{output}`"),
1149 reason,
1150 }
1151 })?;
1152
1153 self.outputs.insert(
1154 output.clone(),
1155 Artifact {
1156 name: output.clone(),
1157 content_type,
1158 value,
1159 },
1160 );
1161 self.sink.emit(RunEvent::Emitted {
1162 node: node.id.clone(),
1163 output,
1164 });
1165 Ok(())
1166 }
1167
1168 fn bind(&mut self, node: &Node, value: Value) {
1171 if let Some(name) = &node.binding {
1172 self.bindings.insert(name.clone(), value);
1173 }
1174 }
1175
1176 fn last_of_region(&self, entry: &str) -> Result<Option<String>, RunError> {
1178 let mut current = Some(entry.to_string());
1179 let mut last = None;
1180 let mut visited = 0usize;
1181 while let Some(id) = current {
1182 let node = self.node(&id)?;
1183 last = Some(node.id.clone());
1184 current = node.next.clone();
1185 visited += 1;
1186 if visited > self.ir.nodes.len() {
1187 return Err(RunError::MalformedIr(
1188 "the node graph contains a cycle".to_string(),
1189 ));
1190 }
1191 }
1192 Ok(last)
1193 }
1194
1195 fn model_selection(&self) -> ModelSelection {
1196 match &self.ir.requirements.model {
1197 ingot_ir::ModelRequirement::Exact { reference } => {
1198 ModelSelection::Exact(reference.clone())
1199 }
1200 ingot_ir::ModelRequirement::Capabilities {
1201 capabilities,
1202 context_tokens,
1203 } => ModelSelection::Capabilities {
1204 capabilities: capabilities.clone(),
1205 min_context_tokens: context_tokens.as_ref().map(|tokens| tokens.min),
1206 },
1207 ingot_ir::ModelRequirement::Unspecified => ModelSelection::Default,
1208 }
1209 }
1210
1211 fn max_output_tokens(&self) -> u32 {
1219 let ceiling = if self.provider.streams() {
1220 STREAMING_CEILING
1221 } else {
1222 NON_STREAMING_CEILING
1223 };
1224 let remaining = match self.ir.budget.tokens {
1225 Some(limit) if limit >= 0 => (limit as u64)
1226 .saturating_sub(self.usage.total())
1227 .min(u32::MAX as u64) as u32,
1228 _ => ceiling,
1229 };
1230 remaining.clamp(1, ceiling)
1231 }
1232
1233 fn render_prompt(&mut self, value: &IrValue) -> Result<String, RunError> {
1234 match value {
1235 IrValue::Literal {
1236 value: Value::String(text),
1237 ..
1238 } => Ok(text.clone()),
1239 IrValue::Template { parts } => {
1240 let mut out = String::new();
1241 for part in parts {
1242 match part {
1243 TemplatePart::Text { value } => out.push_str(value),
1244 TemplatePart::Value { value, ty } => {
1245 let resolved = self.eval(value)?;
1246 out.push_str(&render_value(&resolved, ty));
1247 }
1248 }
1249 }
1250 Ok(out)
1251 }
1252 other => {
1253 let resolved = self.eval(other)?;
1254 Ok(render_value(&resolved, "string"))
1255 }
1256 }
1257 }
1258
1259 fn eval(&mut self, value: &IrValue) -> Result<Value, RunError> {
1261 match value {
1262 IrValue::Literal { value, .. } => Ok(value.clone()),
1263 IrValue::Ref { scope, path } => self.eval_ref(*scope, path),
1264 IrValue::List { items } => {
1265 let mut out = Vec::with_capacity(items.len());
1266 for item in items {
1267 out.push(self.eval(item)?);
1268 }
1269 Ok(Value::Array(out))
1270 }
1271 IrValue::Template { .. } => Ok(Value::String(self.render_prompt(value)?)),
1272 IrValue::Unary { op, operand } => {
1273 let operand = self.eval(operand)?;
1274 match op.as_str() {
1275 "!" => Ok(json!(!truthy(&operand))),
1276 "-" => match operand.as_i64() {
1277 Some(int) => Ok(json!(-int)),
1278 None => Ok(json!(-operand.as_f64().unwrap_or_default())),
1279 },
1280 other => Err(RunError::MalformedIr(format!(
1281 "unknown unary operator `{other}`"
1282 ))),
1283 }
1284 }
1285 IrValue::Binary { op, lhs, rhs } => {
1286 let lhs = self.eval(lhs)?;
1287 let rhs = self.eval(rhs)?;
1288 eval_binary(op, &lhs, &rhs)
1289 }
1290 IrValue::Builtin { name, args } => {
1291 let mut evaluated = Vec::with_capacity(args.len());
1292 for arg in args {
1293 evaluated.push(self.eval(arg)?);
1294 }
1295 eval_builtin(name, &evaluated)
1296 }
1297 IrValue::Unknown => Err(RunError::MalformedIr(
1298 "the artifact contains an unresolved value; it was not built from a clean compile"
1299 .to_string(),
1300 )),
1301 }
1302 }
1303
1304 fn eval_ref(&mut self, scope: RefScope, path: &[String]) -> Result<Value, RunError> {
1305 let Some((root, fields)) = path.split_first() else {
1306 return Err(RunError::MalformedIr(
1307 "a reference has an empty path".to_string(),
1308 ));
1309 };
1310 let mut current = match scope {
1311 RefScope::Input | RefScope::Binding => {
1312 self.bindings.get(root).cloned().ok_or_else(|| {
1313 RunError::MalformedIr(format!("`{root}` is not bound at this point"))
1314 })?
1315 }
1316 RefScope::State => {
1317 self.state
1318 .get(root)
1319 .cloned()
1320 .ok_or_else(|| RunError::StateNotSet {
1321 node: String::new(),
1322 field: root.clone(),
1323 })?
1324 }
1325 RefScope::Memory => self.memory.get(root).cloned().ok_or_else(|| {
1329 RunError::MalformedIr(format!("`memory.{root}` is not a declared field"))
1330 })?,
1331 };
1332 for field in fields {
1333 current = current
1334 .get(field)
1335 .cloned()
1336 .ok_or_else(|| RunError::MalformedIr(format!("no field `{field}` on the value")))?;
1337 }
1338 Ok(current)
1339 }
1340}
1341
1342fn render_value(value: &Value, ty: &str) -> String {
1343 match value {
1344 Value::String(text) => text.clone(),
1347 Value::Null => String::new(),
1348 other => match ty {
1349 "json" => serde_json::to_string_pretty(other).unwrap_or_else(|_| other.to_string()),
1350 _ => other.to_string(),
1351 },
1352 }
1353}
1354
1355fn truthy(value: &Value) -> bool {
1356 value.as_bool().unwrap_or(false)
1357}
1358
1359fn eval_binary(op: &str, lhs: &Value, rhs: &Value) -> Result<Value, RunError> {
1360 let result = match op {
1361 "==" => json!(lhs == rhs),
1362 "!=" => json!(lhs != rhs),
1363 "&&" => json!(truthy(lhs) && truthy(rhs)),
1364 "||" => json!(truthy(lhs) || truthy(rhs)),
1365 "<" | "<=" | ">" | ">=" => {
1366 let (a, b) = numeric_pair(lhs, rhs, op)?;
1367 json!(match op {
1368 "<" => a < b,
1369 "<=" => a <= b,
1370 ">" => a > b,
1371 _ => a >= b,
1372 })
1373 }
1374 "+" | "-" => {
1375 if let (Some(a), Some(b)) = (lhs.as_i64(), rhs.as_i64()) {
1376 json!(if op == "+" { a + b } else { a - b })
1377 } else {
1378 let (a, b) = numeric_pair(lhs, rhs, op)?;
1379 json!(if op == "+" { a + b } else { a - b })
1380 }
1381 }
1382 other => {
1383 return Err(RunError::MalformedIr(format!(
1384 "unknown binary operator `{other}`"
1385 )));
1386 }
1387 };
1388 Ok(result)
1389}
1390
1391fn numeric_pair(lhs: &Value, rhs: &Value, op: &str) -> Result<(f64, f64), RunError> {
1392 match (lhs.as_f64(), rhs.as_f64()) {
1393 (Some(a), Some(b)) => Ok((a, b)),
1394 _ => Err(RunError::MalformedIr(format!(
1395 "`{op}` needs two numbers but was given {lhs} and {rhs}"
1396 ))),
1397 }
1398}
1399
1400fn eval_builtin(name: &str, args: &[Value]) -> Result<Value, RunError> {
1401 match name {
1402 "len" => {
1403 let Some(value) = args.first() else {
1404 return Err(RunError::MalformedIr(
1405 "`len` takes one argument".to_string(),
1406 ));
1407 };
1408 let length = match value {
1409 Value::Array(items) => items.len(),
1410 Value::String(text) => text.chars().count(),
1411 Value::Object(map) => map.len(),
1412 other => {
1413 return Err(RunError::MalformedIr(format!(
1414 "`len` cannot measure {other}"
1415 )))
1416 }
1417 };
1418 Ok(json!(length))
1419 }
1420 other => Err(RunError::MalformedIr(format!("unknown builtin `{other}`"))),
1421 }
1422}
1423
1424fn subject_for_effect(effect: &str) -> &str {
1429 match effect {
1430 "secret_access" => "secrets",
1431 other => other,
1432 }
1433}
1434
1435#[cfg(test)]
1436mod tests {
1437 use super::*;
1438
1439 #[test]
1440 fn secret_access_maps_to_the_secrets_subject() {
1441 assert_eq!(subject_for_effect("secret_access"), "secrets");
1442 assert_eq!(subject_for_effect("network"), "network");
1443 }
1444
1445 #[test]
1446 fn len_measures_lists_strings_and_objects() {
1447 assert_eq!(eval_builtin("len", &[json!([1, 2, 3])]).unwrap(), json!(3));
1448 assert_eq!(eval_builtin("len", &[json!("abc")]).unwrap(), json!(3));
1449 assert_eq!(eval_builtin("len", &[json!({"a": 1})]).unwrap(), json!(1));
1450 }
1451
1452 #[test]
1453 fn comparison_operators_work_on_numbers() {
1454 assert_eq!(eval_binary(">", &json!(3), &json!(1)).unwrap(), json!(true));
1455 assert_eq!(
1456 eval_binary("<=", &json!(3), &json!(3)).unwrap(),
1457 json!(true)
1458 );
1459 }
1460
1461 #[test]
1462 fn equality_works_on_any_value() {
1463 assert_eq!(
1464 eval_binary("==", &json!("a"), &json!("a")).unwrap(),
1465 json!(true)
1466 );
1467 assert_eq!(
1468 eval_binary("!=", &json!([1]), &json!([2])).unwrap(),
1469 json!(true)
1470 );
1471 }
1472
1473 #[test]
1474 fn integer_arithmetic_stays_integral() {
1475 assert_eq!(eval_binary("+", &json!(2), &json!(3)).unwrap(), json!(5));
1476 }
1477
1478 #[test]
1479 fn strings_render_without_json_quoting() {
1480 assert_eq!(render_value(&json!("hello"), "string"), "hello");
1481 assert_eq!(render_value(&json!(3), "int"), "3");
1482 }
1483}