⏳ Temporal Semantics in TelaMentis
TelaMentis provides robust support for temporal data, allowing AI agents to reason about information not just as it is, but also when it was true and when it was known. This capability is crucial for building sophisticated agents that can understand context, track changes, and learn from history.
1. Why Temporal Awareness?
AI agents operating in dynamic environments need to:
- Understand Context: The relevance of information often depends on its timeliness. "What is the current weather?" vs. "What was the weather last Tuesday?"
- Reason About Change: Track how entities and relationships evolve. "When did John join Acme Corp?" and "When did he leave?"
- Perform "As-Of" Analysis: Query the state of the world as it was at a specific point in the past. "What did our knowledge graph show about Alice's relationships on January 1, 2023?"
- Audit and Trace Data Lineage: Understand when information was recorded or modified, crucial for debugging, compliance, and reproducibility.
- Manage Versioning: Handle multiple versions of facts or entities over time.
- Implement Regret/Rollback: If a fact is later found to be incorrect, the system should be ableable to identify and potentially invalidate decisions made based on that fact.
2. Bitemporal Data Model
TelaMentis implements full bitemporality for its edges (relationships) as of Phase 2. This means it tracks two distinct time dimensions:
- Valid Time: The time period during which a fact or relationship is true in the modeled world (the "real world" or the domain of interest). This is controlled by the user or the data source.
valid_from
: The timestamp when the fact/relationship started being true.valid_to
: The timestamp when the fact/relationship stopped being true. An open-endedvalid_to
(e.g.,None
or a special far-future date) indicates the fact is "currently true" or true indefinitely.
- Transaction Time (or Ingestion Time): The time period during which a fact was recorded and present in the database. This is typically system-managed.
transaction_start
: The timestamp when the fact/relationship was added to (or became current in) the database.transaction_end
: The timestamp when the fact/relationship was superseded by a new version or logically deleted from the database. An open-endedtransaction_end
indicates it's the current version in the database.
Current Implementation Status:
- ✅ Valid Time: Fully implemented with
valid_from
/valid_to
fields - ✅ Transaction Time: Fully implemented as of Phase 2 with
transaction_start_time
/transaction_end_time
fields
The TimeEdge
Structure
The core of TelaMentis's temporal model is the TimeEdge
:
// From TelaMentis-core/src/types.rs (Phase 2 implementation)
use chrono::{DateTime, Utc};
use uuid::Uuid;
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeEdge<P = serde_json::Value> {
pub from_node_id: Uuid, // UUID of the source node
pub to_node_id: Uuid, // UUID of the target node
pub kind: String, // Type of the relationship (e.g., "WORKS_FOR")
// Valid Time
pub valid_from: DateTime<Utc>, // When the relationship became true in the modeled world
pub valid_to: Option<DateTime<Utc>>,// When it ceased to be true (None = still valid / indefinitely)
// Transaction Time (Phase 2)
pub transaction_start_time: DateTime<Utc>, // When this version was recorded
pub transaction_end_time: Option<DateTime<Utc>>, // When this version was superseded
pub props: P, // Properties of the relationship
}
Key Points about TimeEdge
:
- Immutability: Once a
TimeEdge
is written with its temporal attributes, it is generally considered immutable. - Automatic Transaction Time: The
transaction_start_time
is automatically set to the current time when creating a new edge. - Updates as New Versions: When a relationship changes (e.g., its properties or validity period are modified) as of Phase 2:
- The existing
TimeEdge
representing the old state has itstransaction_end_time
(system-managed) set, and/or itsvalid_to
(user-managed) updated if the change affects its real-world validity. - A new
TimeEdge
is created with the updated information, its ownvalid_from
,valid_to
, and a newtransaction_start_time
.
- The existing
- This creates a full history of changes, enabling rich temporal queries.
3. Temporal Queries
TelaMentis's GraphStore
trait and its adapters are designed to support various types of temporal queries.
a. "As-Of" Queries (Valid Time)
These queries ask about the state of the modeled world at a specific point in valid time. "What relationships were true for User X on March 1, 2024?"
Conceptual GraphQuery
parameters:
as_of_valid_time: DateTime<Utc>
The storage adapter translates this into a condition where:
edge.valid_from <= as_of_valid_time AND (edge.valid_to IS NULL OR edge.valid_to > as_of_valid_time)
And, importantly, it should also only consider edges that are currently part of the database history (i.e. their transaction_end_time
is open).
Example Cypher (for Neo4j adapter):
// What did the user believe (what relationships were valid) on 2025-04-01?
MATCH (u:User {id_alias: $uid})-[r]->(n)
WHERE r.valid_from <= date('2025-04-01')
AND (r.valid_to IS NULL OR r.valid_to > date('2025-04-01'))
// Assuming Neo4j bitemporal modeling might also add transaction time checks if supported directly
// OR that the query engine only sees current transaction time versions by default.
RETURN u, r, n;
b. "As-At" Queries (Transaction Time)
These queries ask what the database knew at a specific point in transaction time. "What did our graph state about User X's relationships as of system time April 15, 2024, 10:00 UTC?"
Phase 2 Implementation:
The Neo4j adapter now stores and indexes transaction_start_time
and transaction_end_time
for each edge version.
Conceptual GraphQuery
parameters:
as_at_transaction_time: DateTime<Utc>
The storage adapter translates this into a condition where:
edge.transaction_start_time <= as_at_transaction_time AND (edge.transaction_end_time IS NULL OR edge.transaction_end_time > as_at_transaction_time)
Example Neo4j Query:
// What relationships existed in the database on April 15, 2024?
MATCH (u:User {id_alias: $uid})-[r]->(n)
WHERE r.transaction_start_time <= datetime('2024-04-15T10:00:00Z')
AND (r.transaction_end_time IS NULL OR r.transaction_end_time > datetime('2024-04-15T10:00:00Z'))
RETURN u, r, n;
c. Bitemporal Queries (Combined Valid and Transaction Time)
These are the most powerful, asking what the database knew at tx_time
about what was true at valid_time
.
"Show me what our graph recorded on April 15th about relationships that were valid on March 1st."
Conceptual GraphQuery
parameters:
as_of_valid_time: DateTime<Utc>
as_at_transaction_time: DateTime<Utc>
This combines both sets of temporal conditions.
d. Interval Queries (Valid Time Range)
Queries that retrieve all relationships that were valid at any point within a given valid time interval. "Which employees worked at Acme Corp between January 1, 2023, and December 31, 2023?"
Conceptual GraphQuery
parameters:
Current Implementation Example:
use telamentis_core::prelude::*;
use chrono::Utc;
// Create a temporal relationship
let employment = TimeEdge::new(
alice_id,
company_id,
"WORKS_FOR",
"2023-01-15T09:00:00Z".parse::<DateTime<Utc>>()?,
serde_json::json!({"role": "Senior Engineer"})
).with_valid_to("2024-01-15T17:00:00Z".parse()?);
// The transaction time is automatically set
println!("Transaction start: {}", employment.transaction_start_time);
println!("Current version: {}", employment.is_current_version());
Conceptual GraphQuery
parameters:
valid_time_range_start: DateTime<Utc>
valid_time_range_end: DateTime<Utc>
The condition checks for overlapping intervals:
edge.valid_from < valid_time_range_end AND (edge.valid_to IS NULL OR edge.valid_to > valid_time_range_start)
4. Use Cases for AI Agents
Temporal capabilities unlock advanced reasoning for AI agents:
- Contextual Memory Retrieval: An agent can retrieve information relevant to a specific past event or conversation by querying the graph "as-of" that time.
- Change Detection & Analysis: Agents can identify when key relationships or properties changed, enabling them to understand trends or react to significant events.
- Example: "When did
Product_A
's status change fromIN_STOCK
toOUT_OF_STOCK
?"
- Example: "When did
- Causal Reasoning (Simplified): By observing the sequence of
valid_from
timestamps, agents can infer potential causal links (though true causality is more complex).- Example: Order Placed (T1) -> Payment Confirmed (T2) -> Item Shipped (T3).
- Temporal Pattern Recognition: Agents can be trained to recognize patterns that unfold over time.
- Maintaining Historical Accuracy: Even if current information changes, the agent can always refer back to what was true or known at previous times.
- Supporting "Undo" or "Regret": If an LLM extraction or data feed introduces an error that is later corrected, the bitemporal history allows the agent to:
- Identify the incorrect information (
TimeEdge
with specificvalid_from
/valid_to
andtransaction_time
). - "Logically delete" or supersede it by creating a new version or setting
valid_to
. - Potentially re-evaluate decisions made based on that incorrect information.
- Identify the incorrect information (
5. Storage and Indexing Implications
Supporting efficient temporal queries has implications for the storage adapter. Phase 2 Implementation:
- Indexing: The Neo4j adapter automatically creates indexes for all temporal fields:
CREATE INDEX valid_from_idx FOR ()-[r]-() ON (r.valid_from);
CREATE INDEX valid_to_idx FOR ()-[r]-() ON (r.valid_to);
CREATE INDEX transaction_start_idx FOR ()-[r]-() ON (r.transaction_start_time);
CREATE INDEX transaction_end_idx FOR ()-[r]-() ON (r.transaction_end_time); - Storage Adapters:
- ✅ Neo4j: Full bitemporal support with automatic indexing
- ✅ In-Memory: Basic temporal support (valid time only for performance)
- 🔄 Future Adapters: Will implement full bitemporal support
- Data Volume: Storing full history can lead to increased data volume compared to systems that only keep current state. Strategies for archiving or summarizing old data might be needed for very long-lived systems.
- Query Complexity: The
GraphStore
trait abstracts temporal complexity, but storage adapters must handle efficient temporal query execution.
6. Handling "Current Time"
Often, queries will relate to "now" in valid time. Current Implementation:
// Query for currently valid relationships
let current_query = GraphQuery::FindRelationships {
from_node_id: Some(alice_id),
to_node_id: None,
relationship_types: vec!["WORKS_FOR".to_string()],
valid_at: Some(Utc::now()), // Current time
limit: None,
};
// Check if a specific edge is currently valid
let is_current = time_edge.is_currently_valid();
let is_current_version = time_edge.is_current_version();
The storage adapters handle "current time" queries by checking for valid_to IS NULL
or valid_to > current_timestamp_utc
.
7. Roadmap Tie-In for Temporal Features
- ✅ Phase 1 (Completed): Core
TimeEdge
structure withvalid_from
andvalid_to
. Basic "as-of" queries supported by Neo4j adapter. - ✅ Phase 2 (Current): Full bitemporal support with explicit
transaction_start_time
andtransaction_end_time
. Enhanced temporal indexing. In-memory adapter with basic temporal support. - Future Phases:
- More sophisticated temporal query operators in a core
GraphQuery
DSL. - Allen's Interval Algebra query support.
- Complex event processing (CEP) capabilities.
- Potential for helper functions for common temporal patterns (e.g., "show changes between T1 and T2").
- Tooling/UI for visualizing temporal evolution of graph segments.
- More sophisticated temporal query operators in a core
8. Working with the Current Implementation
Creating Temporal Relationships
use telamentis_core::prelude::*;
// Create a relationship with automatic transaction time
let edge = TimeEdge::new(
from_id,
to_id,
"WORKS_FOR",
"2023-01-15T09:00:00Z".parse()?, // valid_from
serde_json::json!({"role": "Engineer"})
);
// transaction_start_time is automatically set to now()
// Create a completed relationship
let completed_edge = TimeEdge::new(
from_id,
to_id,
"WORKED_FOR",
"2022-01-01T09:00:00Z".parse()?,
serde_json::json!({"role": "Junior Developer"})
).with_valid_to("2022-12-31T17:00:00Z".parse()?);
Querying with Temporal Constraints
// Find current relationships
let current_query = GraphQuery::FindRelationships {
from_node_id: Some(person_id),
to_node_id: None,
relationship_types: vec!["WORKS_FOR".to_string()],
valid_at: Some(Utc::now()),
limit: None,
};
// Find historical relationships
let historical_query = GraphQuery::FindRelationships {
from_node_id: Some(person_id),
to_node_id: None,
relationship_types: vec!["WORKS_FOR".to_string()],
valid_at: Some("2023-06-01T00:00:00Z".parse()?),
limit: None,
};
// As-of query
let as_of_query = GraphQuery::AsOfQuery {
base_query: Box::new(historical_query),
as_of_time: "2023-06-01T00:00:00Z".parse()?,
};
By implementing full bitemporal semantics in Phase 2, TelaMentis provides a powerful foundation for AI agents that need to understand and interact with a world that is constantly changing, while maintaining a complete audit trail of all data changes.