171 lines
5.2 KiB
Plaintext
171 lines
5.2 KiB
Plaintext
-- libertaria/context.jan
|
|
-- NCP (Nexus Context Protocol) implementation
|
|
-- Structured, hierarchical context management for agent conversations
|
|
|
|
module Context exposing
|
|
( Context
|
|
, create, fork, merge, close
|
|
, current_depth, max_depth
|
|
, add_message, get_messages
|
|
, subscribe, unsubscribe
|
|
, to_astdb_query
|
|
)
|
|
|
|
import message.{Message}
|
|
import memory.{VectorStore}
|
|
import time.{timestamp}
|
|
|
|
-- Context is a structured conversation container
|
|
type Context =
|
|
{ id: context_id.ContextId
|
|
, parent: ?context_id.ContextId -- Hierarchical nesting
|
|
, depth: int -- Nesting level (prevents infinite loops)
|
|
, created_at: timestamp.Timestamp
|
|
, messages: list.List(Message)
|
|
, metadata: metadata.ContextMetadata
|
|
, vector_store: ?VectorStore -- Semantic indexing
|
|
, subscribers: set.Set(fingerprint.Fingerprint)
|
|
, closed: bool
|
|
}
|
|
|
|
type ContextConfig =
|
|
{ max_depth: int = 100 -- Max nesting before forced flattening
|
|
, max_messages: int = 10000 -- Auto-archive older messages
|
|
, enable_vector_index: bool = true
|
|
, retention_policy: RetentionPolicy
|
|
}
|
|
|
|
type RetentionPolicy =
|
|
| Keep_Forever
|
|
| Auto_Archive_After(duration.Duration)
|
|
| Delete_After(duration.Duration)
|
|
|
|
-- Create root context
|
|
-- Top-level conversation container
|
|
fn create(config: ContextConfig) -> Context
|
|
let id = context_id.generate()
|
|
let now = timestamp.now()
|
|
let vs = if config.enable_vector_index
|
|
then some(memory.create_vector_store())
|
|
else null
|
|
|
|
{ id = id
|
|
, parent = null
|
|
, depth = 0
|
|
, created_at = now
|
|
, messages = list.empty()
|
|
, metadata = metadata.create(id)
|
|
, vector_store = vs
|
|
, subscribers = set.empty()
|
|
, closed = false
|
|
}
|
|
|
|
-- Fork child context from parent
|
|
-- Used for: sub-conversations, branching decisions, isolated experiments
|
|
fn fork(parent: Context, reason: string, config: ContextConfig) -> result.Result(Context, error.ForkError)
|
|
if parent.depth >= config.max_depth then
|
|
error.err(MaxDepthExceeded)
|
|
else if parent.closed then
|
|
error.err(ParentClosed)
|
|
else
|
|
let id = context_id.generate()
|
|
let now = timestamp.now()
|
|
let vs = if config.enable_vector_index
|
|
then some(memory.create_vector_store())
|
|
else null
|
|
|
|
let child =
|
|
{ id = id
|
|
, parent = some(parent.id)
|
|
, depth = parent.depth + 1
|
|
, created_at = now
|
|
, messages = list.empty()
|
|
, metadata = metadata.create(id)
|
|
|> metadata.set_parent(parent.id)
|
|
|> metadata.set_fork_reason(reason)
|
|
, vector_store = vs
|
|
, subscribers = set.empty()
|
|
, closed = false
|
|
}
|
|
|
|
ok(child)
|
|
|
|
-- Merge child context back into parent
|
|
-- Consolidates messages, preserves fork history
|
|
fn merge(child: Context, into parent: Context) -> result.Result(Context, error.MergeError)
|
|
if child.parent != some(parent.id) then
|
|
error.err(NotMyParent)
|
|
else if child.closed then
|
|
error.err(ChildClosed)
|
|
else
|
|
let merged_messages = parent.messages ++ child.messages
|
|
let merged_subs = set.union(parent.subscribers, child.subscribers)
|
|
|
|
let updated_parent =
|
|
{ parent with
|
|
messages = merged_messages
|
|
, subscribers = merged_subs
|
|
, metadata = parent.metadata
|
|
|> metadata.add_merge_history(child.id, child.messages.length())
|
|
}
|
|
|
|
ok(updated_parent)
|
|
|
|
-- Close context (final state)
|
|
-- No more messages, preserves history
|
|
fn close(ctx: Context) -> Context
|
|
{ ctx with closed = true }
|
|
|
|
-- Get current nesting depth
|
|
fn current_depth(ctx: Context) -> int
|
|
ctx.depth
|
|
|
|
-- Get max allowed depth (from config)
|
|
fn max_depth(ctx: Context) -> int
|
|
ctx.metadata.config.max_depth
|
|
|
|
-- Add message to context
|
|
-- Indexes in vector store if enabled
|
|
fn add_message(ctx: Context, msg: Message) -> result.Result(Context, error.AddError)
|
|
if ctx.closed then
|
|
error.err(ContextClosed)
|
|
else
|
|
let updated = { ctx with messages = ctx.messages ++ [msg] }
|
|
|
|
-- Index in vector store for semantic search
|
|
match ctx.vector_store with
|
|
| null -> ok(updated)
|
|
| some(vs) ->
|
|
let embedding = memory.embed(message.content(msg))
|
|
let indexed_vs = memory.store(vs, message.id(msg), embedding)
|
|
ok({ updated with vector_store = some(indexed_vs) })
|
|
|
|
-- Get all messages in context
|
|
fn get_messages(ctx: Context) -> list.List(Message)
|
|
ctx.messages
|
|
|
|
-- Subscribe agent to context updates
|
|
fn subscribe(ctx: Context, agent: fingerprint.Fingerprint) -> Context
|
|
{ ctx with subscribers = set.insert(ctx.subscribers, agent) }
|
|
|
|
-- Unsubscribe agent
|
|
fn unsubscribe(ctx: Context, agent: fingerprint.Fingerprint) -> Context
|
|
{ ctx with subscribers = set.remove(ctx.subscribers, agent) }
|
|
|
|
-- Convert to ASTDB query for semantic search
|
|
-- Enables: "Find similar contexts", "What did we discuss about X?"
|
|
fn to_astdb_query(ctx: Context) -> astdb.Query
|
|
let message_hashes = list.map(ctx.messages, message.hash)
|
|
let time_range =
|
|
{ start = ctx.created_at
|
|
, end = match list.last(ctx.messages) with
|
|
| null -> timestamp.now()
|
|
| some(last_msg) -> message.timestamp(last_msg)
|
|
}
|
|
|
|
astdb.query()
|
|
|> astdb.with_context_id(ctx.id)
|
|
|> astdb.with_message_hashes(message_hashes)
|
|
|> astdb.with_time_range(time_range)
|
|
|> astdb.with_depth(ctx.depth)
|