///|
/// Built-in memory surface: the fixed lead line for the session-opening
/// memory message, the inbound collector behind the Agent's injection step,
/// and the `memory_search` / `memory_add` tool provider built over the
/// aggregated `MemoryPort` sources. Like the other built-in adapters, these
/// live in the root package (not `src/port/`) — concrete adapters, not
/// extension contracts.
///|
/// Fixed lead line core puts in front of the injected memory message — the
/// one piece of memory-specific text core ever produces. Everything past
/// this line is provider content, untouched by core.
pub const MEMORY_INBOUND_LEAD : String =
#| ## Memory
#|
#| The following is memory recalled for this session, for your information. It may be stale or incomplete — verify against the current conversation before relying on it.
///|
/// One provider's inbound read with an optional per-call timeout. Any
/// failure — provider raise or timeout — is reported via `on_failure`
/// (cancellation is not a defect and is not reported) and contributes
/// nothing (`None`).
async fn memory_inbound_one(
source : String,
port : &@port.MemoryPort,
session_id : String,
request : String,
timeout_ms : Int?,
on_failure : (String) -> Unit,
) -> String? {
let call = async fn() -> String? {
match timeout_ms {
Some(n) =>
@async.with_timeout(n, () => port.inbound(session_id~, request~))
None => port.inbound(session_id~, request~)
}
}
call() catch {
@async.TimeoutError => {
match timeout_ms {
Some(n) => on_failure("\{source}: timeout after \{n}ms")
None => ()
}
None
}
err => {
if !@async.is_cancellation_error(err) {
on_failure("\{source}: \{err.to_string()}")
}
None
}
}
}
///|
/// Fetch inbound memory bodies for every provider concurrently. Failures and
/// timeouts are reported via `on_failure` and swallowed — the session
/// continues without that provider's content. `noraise`: every
/// `MemoryPort::inbound` raise is caught locally, so the injection step it
/// feeds never widens its raise set. Results stay in provider-registration
/// order because `@async.all` returns values in submission order.
async fn collect_memory_inbound(
sources : Array[(String, &@port.MemoryPort)],
session_id : String,
request : String,
timeout_ms : Int?,
on_failure : (String) -> Unit,
) -> Array[String] noraise {
let attempted : Result[Array[String?], Error] = Ok(
@async.all(
sources.map(fn(pair) {
let (source, port) = pair
() => {
memory_inbound_one(
source, port, session_id, request, timeout_ms, on_failure,
)
}
}),
),
) catch {
error => Err(error)
}
let out : Array[String] = []
match attempted {
Ok(results) =>
for result in results {
match result {
Some(body) if body.trim() != "" => out.push(body)
_ => ()
}
}
Err(_) => () // cancellation: turn is aborting, skip injection
}
out
}
///|
/// Assemble the single frozen user message: core's lead line followed by the
/// provider bodies in registration order. Placed before the real first user
/// input; never rewritten, never re-read.
fn memory_inbound_message(bodies : Array[String]) -> @kernel.Message {
@kernel.UserMessage(content=[
@kernel.Text(MEMORY_INBOUND_LEAD + "\n\n" + bodies.join("\n\n")),
])
}
///|
/// JSON-schema fragment for one tool argument: `{"type": }`.
fn memory_arg_schema(ty : String) -> Json {
Json::object(Map::from_array([("type", Json::string(ty))]))
}
///|
fn memory_search_schema() -> Json {
Json::object(
Map::from_array([
("type", Json::string("object")),
(
"properties",
Json::object(
Map::from_array([
("query", memory_arg_schema("string")),
("top_k", memory_arg_schema("integer")),
]),
),
),
("required", Json::array([Json::string("query")])),
]),
)
}
///|
fn memory_add_schema() -> Json {
Json::object(
Map::from_array([
("type", Json::string("object")),
(
"properties",
Json::object(
Map::from_array([
("content", memory_arg_schema("string")),
("source", memory_arg_schema("string")),
]),
),
),
("required", Json::array([Json::string("content")])),
]),
)
}
///|
/// Read a required string argument from a tool call's JSON object args.
/// `Err` carries the model-facing diagnostic.
fn memory_required_string_arg(
args : Map[String, Json],
tool : String,
key : String,
) -> Result[String, String] {
match args.get(key) {
Some(Json::String(s)) => Ok(s)
Some(_) => Err("\{tool}: argument '\{key}' must be a string")
None => Err("\{tool}: missing required argument '\{key}'")
}
}
///|
/// Built-in ToolProvider over the aggregated memory sources: exposes
/// `memory_search` / `memory_add` to the model and fans each call out to
/// every matching provider. Provider failures are reported via `on_failure`
/// (the Agent wires it to the `secondary_failure` observer event) and never
/// abort the calling turn.
priv struct MemoryToolProvider {
sources : Array[(String, &@port.MemoryPort)]
on_failure : (String) -> Unit
}
///|
fn MemoryToolProvider::MemoryToolProvider(
sources~ : Array[(String, &@port.MemoryPort)],
on_failure~ : (String) -> Unit,
) -> MemoryToolProvider {
{ sources, on_failure, }
}
///|
impl @port.ToolProvider for MemoryToolProvider with fn list_tools(
self : MemoryToolProvider,
) -> Array[@kernel.ToolDef] {
let valid_sources : Array[String] = []
for pair in self.sources {
let (id, _) = pair
valid_sources.push(id)
}
[
@kernel.ToolDef(
name=@kernel.ToolName::unchecked("memory_search"),
description="Search accumulated memory (past work, settled decisions, SOPs) before answering questions it may have settled, and before saving anything new.",
input_schema=memory_search_schema(),
owner=@kernel.OwnerId::unchecked("placeholder"),
policy=@kernel.ExecutionPolicy::Sequential,
provenance=Some("posoco.core"),
),
@kernel.ToolDef(
name=@kernel.ToolName::unchecked("memory_add"),
description="Save a durable memory for future sessions. Omit source to save to every connected memory provider; valid sources: \{valid_sources.join(", ")}",
input_schema=memory_add_schema(),
owner=@kernel.OwnerId::unchecked("placeholder"),
policy=@kernel.ExecutionPolicy::Sequential,
provenance=Some("posoco.core"),
),
]
}
///|
async fn MemoryToolProvider::execute_memory_search(
self : MemoryToolProvider,
args : Map[String, Json],
) -> @kernel.ToolOutcome noraise {
let query = match memory_required_string_arg(args, "memory_search", "query") {
Ok(q) => q
Err(msg) => return @kernel.ToolReportedError(content=msg, structured=None)
}
let top_k : Int? = match args.get("top_k") {
Some(Json::Number(n, ..)) => Some(n.to_int())
Some(_) =>
return @kernel.ToolReportedError(
content="memory_search: argument 'top_k' must be an integer",
structured=None,
)
None => None
}
let searched : Array[(String, String?)] = @async.all(
self.sources.map(fn(pair) {
let (source, port) = pair
() => {
let text : String? = port.search(query~, top_k?) catch {
err => {
if !@async.is_cancellation_error(err) {
(self.on_failure)("memory_search \{source}: \{err.to_string()}")
}
None
}
}
(source, text)
}
}),
) catch {
_ => [] // cancellation: the calling turn is aborting; contribute nothing
}
let hits : Array[String] = []
for pair in searched {
let (_, text) = pair
match text {
Some(body) if body.trim() != "" => hits.push(body)
_ => ()
}
}
if hits.is_empty() {
@kernel.Success(content="No matching memory.", structured=None)
} else {
@kernel.Success(content=hits.join("\n\n"), structured=None)
}
}
///|
async fn MemoryToolProvider::execute_memory_add(
self : MemoryToolProvider,
args : Map[String, Json],
) -> @kernel.ToolOutcome noraise {
let content = match
memory_required_string_arg(args, "memory_add", "content") {
Ok(c) => c
Err(msg) => return @kernel.ToolReportedError(content=msg, structured=None)
}
if content.trim() == "" {
return @kernel.ToolReportedError(
content="memory_add: argument 'content' must be non-empty",
structured=None,
)
}
let source = match args.get("source") {
Some(Json::String(s)) => Some(s)
Some(_) =>
return @kernel.ToolReportedError(
content="memory_add: argument 'source' must be a string",
structured=None,
)
None => None
}
let targets : Array[(String, &@port.MemoryPort)] = match source {
Some(id) => {
let matched : Array[(String, &@port.MemoryPort)] = []
for pair in self.sources {
let (mid, port) = pair
if mid == id {
matched.push((mid, port))
}
}
if matched.is_empty() {
let valid : Array[String] = []
for pair in self.sources {
let (mid, _) = pair
valid.push(mid)
}
return @kernel.ToolReportedError(
content="memory_add: unknown source '\{id}'; valid sources: \{valid.join(", ")}",
structured=None,
)
}
matched
}
None => self.sources
}
// Concurrent writes; receipt lines stay in registration order because
// `@async.all` returns values in submission order.
let receipts : Array[(String, Result[String, String])] = @async.all(
targets.map(fn(pair) {
let (source, port) = pair
() => {
let outcome : Result[String, String] = Ok(
port.store(content~, metadata=Map::from_array([])),
) catch {
err => Err(err.to_string())
}
(source, outcome)
}
}),
) catch {
_ => [] // cancellation: the calling turn is aborting
}
let lines : Array[String] = []
let mut failures = 0
for pair in receipts {
let (source, outcome) = pair
match outcome {
Ok(ticket) => lines.push("\{source}: \{ticket}")
Err(reason) => {
failures = failures + 1
lines.push("\{source}: failed (\{reason})")
}
}
}
if failures == targets.length() {
@kernel.ToolReportedError(content=lines.join("\n"), structured=None)
} else {
@kernel.Success(content=lines.join("\n"), structured=None)
}
}
///|
impl @port.ToolProvider for MemoryToolProvider with fn execute(
self : MemoryToolProvider,
name : String,
call : @kernel.ToolCall,
) -> @kernel.ToolOutcome raise @error.RuntimeError {
let args : Map[String, Json] = match call.arguments {
Json::Object(fields) => fields
_ =>
return @kernel.ToolReportedError(
content="\{name}: arguments must be a JSON object",
structured=None,
)
}
match name {
"memory_search" => self.execute_memory_search(args)
"memory_add" => self.execute_memory_add(args)
_ =>
raise @error.RuntimeError::UnknownTool(
"No executor registered for tool '\{name}'",
)
}
}