Skip to main content

writerslogic_warm/core/memory_api/
json.rs

1// Copyright 2024-2026 WritersLogic Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! JSON dispatch: one request/response contract that the HTTP server and the Node binding
5//! both carry, so every surface exposes the same operations with the same shapes. Requests
6//! and responses are the camelCase serde forms of the types in [`super::types`]. Unknown
7//! request fields are rejected rather than ignored, so a misspelled option fails loudly.
8
9use anyhow::Result;
10use serde::de::DeserializeOwned;
11use serde_json::{json, Map, Value};
12
13use super::types::*;
14use super::{Memory, MemoryError};
15
16/// The operations [`Memory::call`] accepts.
17pub const OPERATIONS: &[&str] = &[
18    "capabilities",
19    "remember",
20    "recall",
21    "explain",
22    "forget",
23    "get",
24    "history",
25    "applyForgetting",
26    "consolidate",
27];
28
29fn parse<T: DeserializeOwned>(value: Value) -> Result<T> {
30    serde_json::from_value(value).map_err(|error| {
31        anyhow::Error::new(MemoryError::Invalid(format!("invalid request: {error}")))
32    })
33}
34
35fn object(value: Value) -> Result<Map<String, Value>> {
36    match value {
37        Value::Object(map) => Ok(map),
38        Value::Null => Ok(Map::new()),
39        _ => Err(MemoryError::Invalid("the request must be a JSON object".into()).into()),
40    }
41}
42
43fn take_string(map: &mut Map<String, Value>, key: &str) -> Result<String> {
44    match map.remove(key) {
45        Some(Value::String(value)) => Ok(value),
46        _ => Err(MemoryError::Invalid(format!("{key} is required and must be a string")).into()),
47    }
48}
49
50/// JavaScript has one number type, so a time arrives as `1735689600000.0`; integral floats
51/// become integers so integer fields accept them.
52fn integral_floats(value: &mut Value) {
53    match value {
54        Value::Number(number) if number.is_f64() => {
55            if let Some(float) = number.as_f64() {
56                if float.fract() == 0.0 && float.abs() < 9_007_199_254_740_992.0 {
57                    *number = (float as i64).into();
58                }
59            }
60        }
61        Value::Array(items) => items.iter_mut().for_each(integral_floats),
62        Value::Object(map) => map.values_mut().for_each(integral_floats),
63        _ => {}
64    }
65}
66
67/// Maintenance operations over JSON are dry runs unless the request says `"dryRun": false`.
68fn dry_run_by_default(request: Value) -> Result<Value> {
69    let mut map = object(request)?;
70    map.entry("dryRun").or_insert(Value::Bool(true));
71    Ok(Value::Object(map))
72}
73
74fn to_value<T: serde::Serialize>(value: &T) -> Result<Value> {
75    Ok(serde_json::to_value(value)?)
76}
77
78impl Memory {
79    pub fn capabilities(&self) -> MemoryCapabilities {
80        MemoryCapabilities {
81            contract_version: 1,
82            profile: "temporal-memory-v1".into(),
83            operations: OPERATIONS.iter().map(|op| (*op).to_owned()).collect(),
84            temporal_queries: true,
85            exact_metadata_filters: true,
86            subject_entity_filters: true,
87            document_pagination: false,
88            committed_error_code: "mutation_committed".into(),
89        }
90    }
91
92    /// Runs one operation from its JSON request and returns its JSON response.
93    ///
94    /// | op | request | response |
95    /// |---|---|---|
96    /// | `remember` | a [`RememberInput`], or `{"facts": [..]}` | `{"remembered": [Remembered]}` |
97    /// | `recall` | `{"query", ..RecallOptions}` | `{"results": [Recalled]}` |
98    /// | `explain` | `{"query", "id", ..RecallOptions}` | `{"explanation": Explanation}` |
99    /// | `forget` | `{"scope": ForgetScope, ..ForgetOptions}` | [`ForgetReport`] |
100    /// | `get` | `{"id"}` | `{"fact": FactRecord or null}` |
101    /// | `history` | `{"id"}` | `{"history": [FactRecord]}` |
102    /// | `applyForgetting` | [`ForgetOptions`] | [`ForgetReport`] |
103    /// | `consolidate` | [`ConsolidateOptions`] | [`ConsolidationReport`] |
104    ///
105    /// Errors that the caller can fix (bad fields, unknown ids, id conflicts) are
106    /// [`MemoryError`]s; anything else is an internal failure.
107    pub fn call(&self, op: &str, mut request: Value) -> Result<Value> {
108        integral_floats(&mut request);
109        match op {
110            "capabilities" => {
111                let request: CapabilitiesRequest = parse(request)?;
112                check!(
113                    request.contract_version.is_none_or(|version| version == 1),
114                    "unsupported memory contractVersion"
115                );
116                Ok(json!({"capabilities": self.capabilities()}))
117            }
118            "remember" => {
119                let mut map = object(request)?;
120                let inputs: Vec<RememberInput> = match map.remove("facts") {
121                    Some(facts) if map.is_empty() => parse(facts)?,
122                    Some(_) => {
123                        return Err(MemoryError::Invalid(
124                            "send either a fact or {\"facts\": [..]}, not both".into(),
125                        )
126                        .into())
127                    }
128                    None => vec![parse(Value::Object(map))?],
129                };
130                Ok(json!({ "remembered": self.remember_many(inputs)? }))
131            }
132            "recall" => {
133                let mut map = object(request)?;
134                let query = take_string(&mut map, "query")?;
135                let options: RecallOptions = parse(Value::Object(map))?;
136                Ok(json!({ "results": self.recall(&query, &options)? }))
137            }
138            "explain" => {
139                let mut map = object(request)?;
140                let query = take_string(&mut map, "query")?;
141                let id = take_string(&mut map, "id")?;
142                let options: RecallOptions = parse(Value::Object(map))?;
143                Ok(json!({ "explanation": self.explain(&query, &id, &options)? }))
144            }
145            "forget" => {
146                let mut map = object(request)?;
147                let scope: ForgetScope = parse(map.remove("scope").unwrap_or(Value::Null))?;
148                let options: ForgetOptions = parse(Value::Object(map))?;
149                to_value(&self.forget(&scope, &options)?)
150            }
151            "get" => {
152                let id = take_string(&mut object(request)?, "id")?;
153                Ok(json!({ "fact": self.get(&id)? }))
154            }
155            "history" => {
156                let id = take_string(&mut object(request)?, "id")?;
157                Ok(json!({ "history": self.history(&id)? }))
158            }
159            "applyForgetting" => {
160                let options: ForgetOptions = parse(dry_run_by_default(request)?)?;
161                to_value(&self.apply_forgetting(&options)?)
162            }
163            "consolidate" => {
164                let options: ConsolidateOptions = parse(dry_run_by_default(request)?)?;
165                to_value(&self.consolidate(&options)?)
166            }
167            other => Err(MemoryError::Invalid(format!(
168                "unknown memory operation {other:?}; expected one of {OPERATIONS:?}"
169            ))
170            .into()),
171        }
172    }
173}