Skip to main content

ainl_runtime/adapters/
graph_patch.rs

1//! Reference [`GraphPatchAdapter`] — structured GraphPatch dispatch without executing AINL IR in Rust.
2//!
3//! ## What this is (and is not)
4//!
5//! - **Is:** normalizes active procedural patch rows (same shape as Python `memory.patch` / graph
6//!   store patch records) into a JSON **dispatch envelope** for a host, or returns that envelope
7//!   directly when no host hook is installed.
8//! - **Is not:** an AINL compiler, an IR interpreter, or parity with Python `RuntimeEngine` GraphPatch.
9//!
10//! ## Payload shape
11//!
12//! The envelope includes `patch_label` (procedural `label` or `pattern_name`), `patch_node_id`,
13//! `patch_version`, `declared_reads`, `compiled_graph_byte_len`, optional UTF-8 preview of
14//! `compiled_graph` when valid UTF-8, `tool_sequence`, `trace_id`, and `frame_keys`. Hosts should
15//! treat unknown fields as forward-compatible.
16
17use std::collections::HashMap;
18use std::sync::Arc;
19
20use serde_json::{json, Value};
21
22use super::PatchAdapter;
23use crate::engine::PatchDispatchContext;
24use ainl_memory::AinlNodeType;
25
26/// Optional host hook: receives the normalized GraphPatch envelope (see module docs).
27pub trait GraphPatchHostDispatch: Send + Sync {
28    fn on_patch_dispatch(&self, envelope: Value) -> Result<Value, String>;
29}
30
31/// Reference adapter registered as [`Self::NAME`]. Used as a **fallback** when no label-specific
32/// [`PatchAdapter`] is registered for the procedural patch label.
33pub struct GraphPatchAdapter {
34    host: Option<Arc<dyn GraphPatchHostDispatch>>,
35}
36
37impl GraphPatchAdapter {
38    pub const NAME: &'static str = "graph_patch";
39
40    pub fn new() -> Self {
41        Self { host: None }
42    }
43
44    pub fn with_host(host: Arc<dyn GraphPatchHostDispatch>) -> Self {
45        Self { host: Some(host) }
46    }
47}
48
49impl Default for GraphPatchAdapter {
50    fn default() -> Self {
51        Self::new()
52    }
53}
54
55fn build_envelope(ctx: &PatchDispatchContext<'_>) -> Result<Value, String> {
56    let proc = match &ctx.node.node_type {
57        AinlNodeType::Procedural { procedural } => procedural,
58        _ => {
59            return Err("graph_patch: PatchDispatchContext.node is not procedural".to_string());
60        }
61    };
62    let utf8_preview = std::str::from_utf8(&proc.compiled_graph)
63        .ok()
64        .map(|s| s.chars().take(256).collect::<String>());
65    Ok(json!({
66        "kind": "graph_patch_dispatch",
67        "patch_label": ctx.patch_label,
68        "patch_node_id": ctx.node.id.to_string(),
69        "pattern_name": proc.pattern_name,
70        "patch_version": proc.patch_version,
71        "procedure_type": format!("{:?}", proc.procedure_type),
72        "declared_reads": proc.declared_reads,
73        "compiled_graph_byte_len": proc.compiled_graph.len(),
74        "compiled_graph_utf8_preview": utf8_preview,
75        "tool_sequence": proc.tool_sequence,
76        "trace_id": proc.trace_id,
77        "frame_keys": ctx.frame.keys().cloned().collect::<Vec<String>>(),
78    }))
79}
80
81impl PatchAdapter for GraphPatchAdapter {
82    fn name(&self) -> &str {
83        Self::NAME
84    }
85
86    fn execute(
87        &self,
88        _label: &str,
89        _frame: &HashMap<String, serde_json::Value>,
90    ) -> Result<Value, String> {
91        Err(
92            "graph_patch: dispatch uses execute_patch with PatchDispatchContext; register via \
93             AinlRuntime::register_adapter(GraphPatchAdapter::new()) or register_default_patch_adapters"
94                .to_string(),
95        )
96    }
97
98    fn execute_patch(&self, ctx: &PatchDispatchContext<'_>) -> Result<Value, String> {
99        let envelope = build_envelope(ctx)?;
100        if let Some(h) = &self.host {
101            h.on_patch_dispatch(envelope)
102        } else {
103            Ok(envelope)
104        }
105    }
106}