import { filter_nil } from "std/collections"
import { memory_forget, memory_recall, memory_store } from "std/memory"
type FactKind = "observation" | "claim" | "hypothesis" | "decision" | "constraint"
type FactEvidenceKind = "trace_ref" | "file_range" | "tool_output" | "user_input"
type FactEvidence = {kind: FactEvidenceKind, ref: string, snippet?: string}
type FactValidUntil = {event: any, asserted_at: string}
type Fact = {
schema: "harn.fact.v1",
id: string,
kind: FactKind,
claim: string,
evidence: list<FactEvidence>,
confidence: float,
provenance: dict,
valid_until?: FactValidUntil,
asserted_at: string,
}
let FACT_SCHEMA = "harn.fact.v1"
let FACT_VALID_KINDS = ["observation", "claim", "hypothesis", "decision", "constraint"]
let FACT_VALID_EVIDENCE_KINDS = ["trace_ref", "file_range", "tool_output", "user_input"]
fn __fact_error(code, message) {
throw code + ": std/agent/fact: " + message
}
fn __fact_text(value) -> string {
if value == nil {
return ""
}
if type_of(value) == "string" {
return trim(value)
}
return trim(to_string(value))
}
fn __fact_in(needle, allowed) -> bool {
for candidate in allowed {
if candidate == needle {
return true
}
}
return false
}
fn __fact_kind(value) -> FactKind {
let raw = __fact_text(value)
let normalized = lowercase(replace(raw, "-", "_"))
if normalized == "" {
__fact_error("HARN-FACT-002", "kind is required")
}
if !__fact_in(normalized, FACT_VALID_KINDS) {
__fact_error("HARN-FACT-002", "unknown kind `" + raw + "`")
}
return normalized
}
fn __fact_evidence_kind(value) -> FactEvidenceKind {
let raw = __fact_text(value)
let normalized = lowercase(replace(raw, "-", "_"))
if normalized == "traceref" {
return "trace_ref"
}
if normalized == "filerange" {
return "file_range"
}
if normalized == "tooloutput" {
return "tool_output"
}
if normalized == "userinput" {
return "user_input"
}
if !__fact_in(normalized, FACT_VALID_EVIDENCE_KINDS) {
__fact_error("HARN-FACT-005", "unknown evidence.kind `" + raw + "`")
}
return normalized
}
fn __fact_confidence(value) -> float {
if value == nil {
__fact_error("HARN-FACT-004", "confidence is required")
}
let kind = type_of(value)
if kind != "int" && kind != "float" {
__fact_error("HARN-FACT-004", "confidence must be a number in [0, 1]")
}
let confidence = value + 0.0
if confidence < 0.0 || confidence > 1.0 {
__fact_error("HARN-FACT-004", "confidence must be in [0, 1]")
}
return confidence
}
fn __fact_string_list(value) -> list<string> {
if value == nil {
return []
}
let raw = if type_of(value) == "list" {
value
} else {
[value]
}
var out = []
for entry in raw {
let text = __fact_text(entry)
if text != "" && !contains(out, text) {
out = out.push(text)
}
}
return out
}
fn __fact_push_tag(tags, tag) -> list<string> {
let text = __fact_text(tag)
if text == "" || contains(tags, text) {
return tags
}
return tags.push(text)
}
fn __fact_evidence_entry(entry) -> FactEvidence {
if type_of(entry) != "dict" {
__fact_error("HARN-FACT-005", "evidence entries must be dicts")
}
let kind = __fact_evidence_kind(entry?.kind)
let ref = __fact_text(entry?.ref)
if ref == "" {
__fact_error("HARN-FACT-005", "evidence.ref is required")
}
let snippet = __fact_text(entry?.snippet)
return filter_nil({kind: kind, ref: ref, snippet: snippet})
}
fn __fact_evidence(value) -> list<FactEvidence> {
if value == nil {
return []
}
if type_of(value) != "list" {
__fact_error("HARN-FACT-005", "evidence must be a list")
}
var out = []
for entry in value {
out = out.push(__fact_evidence_entry(entry))
}
return out
}
fn __fact_evidence_tags(fact_kind, evidence) -> list<string> {
var out = []
for item in evidence {
out = __fact_push_tag(out, "fact:evidence:" + item.kind)
out = __fact_push_tag(out, "fact:evidence_ref:" + item.ref)
out = __fact_push_tag(out, "fact:evidence:" + item.kind + ":" + item.ref)
out = __fact_push_tag(out, "fact:" + fact_kind + ":evidence:" + item.kind)
out = __fact_push_tag(out, "fact:" + fact_kind + ":evidence_ref:" + item.ref)
out = __fact_push_tag(out, "fact:" + fact_kind + ":evidence:" + item.kind + ":" + item.ref)
}
return out
}
fn __fact_provenance(value) -> dict {
if value == nil {
return {}
}
if type_of(value) != "dict" {
__fact_error("HARN-FACT-006", "provenance must be a dict")
}
return value
}
fn __fact_valid_until(value) -> FactValidUntil? {
if value == nil {
return nil
}
if type_of(value) != "dict" {
__fact_error("HARN-FACT-007", "valid_until must be a dict")
}
if value?.event == nil {
__fact_error("HARN-FACT-007", "valid_until.event is required")
}
let asserted_at = __fact_text(value?.asserted_at)
if asserted_at == "" {
__fact_error("HARN-FACT-007", "valid_until.asserted_at is required")
}
return {event: value.event, asserted_at: asserted_at}
}
fn __fact_options(options, caller) -> dict {
if options == nil {
return {}
}
if type_of(options) != "dict" {
__fact_error("HARN-FACT-001", caller + " options must be a dict")
}
return options
}
/**
* Return the canonical memory namespace for a fact scope.
*
* The default namespace is project-scoped because the VM-native memory root is
* already project-local unless a caller provides `options.root`.
*
* @effects: []
* @allocation: heap
* @errors: []
* @api_stability: experimental
* @example: fact_namespace(scope)
*/
pub fn fact_namespace(scope = nil) -> string {
let raw = lowercase(__fact_text(scope))
if raw == "" || raw == "project" {
return "project/facts"
}
if raw == "workspace" {
return "workspace/facts"
}
if raw == "user" {
return "user/facts"
}
return __fact_text(scope)
}
/**
* Build a stable fact id from the normalized schema-bearing fields.
*
* @effects: []
* @allocation: heap
* @errors: []
* @api_stability: experimental
* @example: fact_id(kind, claim, evidence, provenance)
*/
pub fn fact_id(kind, claim, evidence = nil, provenance = nil) -> string {
let seed = to_string(__fact_kind(kind))
+ "\n"
+ __fact_text(claim)
+ "\n"
+ json_stringify(__fact_evidence(evidence))
+ "\n"
+ json_stringify(__fact_provenance(provenance))
return "fact_" + substring(sha256(seed), 0, 32)
}
/**
* Return the reserved memory key for a fact.
*
* @effects: []
* @allocation: heap
* @errors: []
* @api_stability: experimental
* @example: fact_key(fact)
*/
pub fn fact_key(fact_value) -> string {
let fact = fact_validate(fact_value)
return "fact:" + fact.kind + ":" + fact.id
}
/**
* Return the canonical memory tags for a fact.
*
* @effects: []
* @allocation: heap
* @errors: []
* @api_stability: experimental
* @example: fact_tags(fact, tags)
*/
pub fn fact_tags(fact_value, tags = nil) -> list<string> {
let fact = fact_validate(fact_value)
var out = ["fact", "fact:" + fact.kind, "schema:" + FACT_SCHEMA]
for tag in __fact_evidence_tags(fact.kind, fact.evidence) {
out = __fact_push_tag(out, tag)
}
for tag in __fact_string_list(tags) {
out = __fact_push_tag(out, tag)
}
return out
}
/**
* Normalize and validate a `harn.fact.v1` envelope.
*
* `kind` accepts the issue shorthand (`Claim`, `FileRange`, etc.) but stores
* lower snake-case strings so facts compose with the rest of the stdlib.
*
* @effects: []
* @allocation: heap
* @errors: [HARN-FACT-001, HARN-FACT-002, HARN-FACT-003, HARN-FACT-004, HARN-FACT-005, HARN-FACT-006, HARN-FACT-007]
* @api_stability: experimental
* @example: fact(input, options)
*/
pub fn fact(input, options = nil) -> Fact {
if input == nil || type_of(input) != "dict" {
__fact_error("HARN-FACT-001", "fact input must be a dict")
}
if input?.schema != nil && input.schema != FACT_SCHEMA {
__fact_error("HARN-FACT-001", "unsupported schema `" + __fact_text(input.schema) + "`")
}
let opts = __fact_options(options, "fact")
let kind = __fact_kind(opts?.kind ?? input?.kind)
let claim = __fact_text(opts?.claim ?? input?.claim)
if claim == "" {
__fact_error("HARN-FACT-003", "claim is required")
}
let confidence = __fact_confidence(opts?.confidence ?? input?.confidence)
let evidence = __fact_evidence(opts?.evidence ?? input?.evidence)
let provenance = __fact_provenance(opts?.provenance ?? input?.provenance)
let valid_until = __fact_valid_until(opts?.valid_until ?? input?.valid_until)
let asserted_at = __fact_text(opts?.asserted_at ?? input?.asserted_at ?? opts?.now)
if opts?.require_asserted_at ?? false && asserted_at == "" {
__fact_error("HARN-FACT-009", "asserted_at is required")
}
let raw_id = __fact_text(opts?.id ?? input?.id)
let id = raw_id == "" ? fact_id(kind, claim, evidence, provenance) : raw_id
return filter_nil(
{
schema: FACT_SCHEMA,
id: id,
kind: kind,
claim: claim,
evidence: evidence,
confidence: confidence,
provenance: provenance,
valid_until: valid_until,
asserted_at: asserted_at == "" ? date_now_iso() : asserted_at,
},
)
}
/**
* Validate and return a normalized `harn.fact.v1` envelope.
*
* @effects: []
* @allocation: heap
* @errors: [HARN-FACT-001, HARN-FACT-002, HARN-FACT-003, HARN-FACT-004, HARN-FACT-005, HARN-FACT-006, HARN-FACT-007, HARN-FACT-009]
* @api_stability: experimental
* @example: fact_validate(input)
*/
pub fn fact_validate(input) -> Fact {
return fact(input, {require_asserted_at: true})
}
/**
* Store a typed fact as `MemoryRecord.value` under a reserved `fact:<kind>:<id>` key.
*
* Options are forwarded to `memory_store`; `options.namespace` overrides the
* fact namespace, and `options.scope` maps through `fact_namespace`.
*
* @effects: [store.write]
* @allocation: heap
* @errors: [HARN-FACT-001, HARN-FACT-002, HARN-FACT-003, HARN-FACT-004, HARN-FACT-005, HARN-FACT-006, HARN-FACT-007]
* @api_stability: experimental
* @example: store_fact(input, options)
*/
pub fn store_fact(input, options = nil) {
let opts = __fact_options(options, "store_fact")
let fact_value = fact(input, opts)
let namespace = opts?.namespace ?? fact_namespace(opts?.scope)
let key = opts?.key ?? fact_key(fact_value)
let tags = fact_tags(fact_value, opts?.tags)
return memory_store(
namespace,
key,
fact_value,
tags,
opts + {id: fact_value.id, provenance: fact_value.provenance},
)
}
fn __fact_scope_options(scope) -> dict {
if scope == nil {
return {}
}
if type_of(scope) == "dict" {
return scope
}
return {scope: scope}
}
fn __fact_positive_int(value, fallback) {
if type_of(value) == "int" && value > 0 {
return value
}
return fallback
}
fn __fact_recall_hit(record) {
let fact_value = fact_validate(record.value)
return filter_nil(
fact_value
+ {
score: record?.score,
memory_record_id: record?.id,
memory_key: record?.key,
memory_namespace: record?.namespace,
stored_at: record?.stored_at,
},
)
}
/**
* Recall typed facts from memory, filtering by kind and minimum confidence.
*
* `scope` may be a scope string (`"project"`, `"workspace"`, `"user"`) or an
* options dict containing `namespace`, `root`, `limit`, `recall_limit`, and
* normal `memory_recall` options.
*
* @effects: [store.read]
* @allocation: heap
* @errors: [HARN-FACT-001, HARN-FACT-002, HARN-FACT-004]
* @api_stability: experimental
* @example: recall_facts(query, kind, min_confidence, scope)
*/
pub fn recall_facts(query, kind = nil, min_confidence = nil, scope = nil) -> list {
let opts = __fact_scope_options(scope)
let namespace = opts?.namespace ?? fact_namespace(opts?.scope)
let resolved_kind = kind == nil ? nil : __fact_kind(kind)
let min = min_confidence == nil ? 0.0 : __fact_confidence(min_confidence)
let limit = __fact_positive_int(opts?.limit ?? opts?.k, 5)
let recall_limit = __fact_positive_int(opts?.recall_limit, limit * 4)
let records = memory_recall(namespace, query, recall_limit < limit ? limit : recall_limit, opts)
var out = []
for record in records {
if record?.value?.schema != FACT_SCHEMA {
continue
}
let hit_result = try {
__fact_recall_hit(record)
}
if is_err(hit_result) {
continue
}
let hit = unwrap(hit_result)
if resolved_kind != nil && hit.kind != resolved_kind {
continue
}
if hit.confidence < min {
continue
}
out = out.push(hit)
if len(out) >= limit {
break
}
}
return out
}
fn __fact_forget_predicate(predicate) -> dict {
if predicate == nil {
__fact_error("HARN-FACT-008", "invalidate_facts predicate is required")
}
if type_of(predicate) == "string" {
let text = __fact_text(predicate)
if text == "" {
__fact_error("HARN-FACT-008", "invalidate_facts predicate must be non-empty")
}
if starts_with(text, "fact_") {
return {id: text}
}
return {query: text}
}
if type_of(predicate) != "dict" {
__fact_error("HARN-FACT-008", "invalidate_facts predicate must be a string or dict")
}
var out = {}
if predicate?.id != nil {
out.id = predicate.id
}
if predicate?.key != nil {
out.key = predicate.key
}
let fact_kind = predicate?.kind == nil ? nil : __fact_kind(predicate.kind)
let evidence_tags = __fact_forget_evidence_tags(predicate, fact_kind)
if len(evidence_tags) > 0 {
out.tags = evidence_tags
} else if fact_kind != nil {
out.tags = ["fact:" + to_string(fact_kind)]
} else if predicate?.tag != nil || predicate?.tags != nil {
out.tags = __fact_string_list(predicate?.tag ?? predicate?.tags)
}
if predicate?.claim != nil {
out.query = predicate.claim
} else if predicate?.query != nil {
out.query = predicate.query
}
if len(out.keys()) == 0 {
__fact_error("HARN-FACT-008", "invalidate_facts predicate matched no supported fields")
}
return out
}
fn __fact_forget_evidence_tags(predicate, fact_kind = nil) -> list<string> {
var out = []
if predicate?.evidence_ref != nil {
out = __fact_push_tag(out, __fact_evidence_ref_tag(fact_kind, predicate.evidence_ref))
}
if predicate?.evidence == nil {
return out
}
let evidence = predicate.evidence
if type_of(evidence) == "string" {
return __fact_push_tag(out, __fact_evidence_ref_tag(fact_kind, evidence))
}
if type_of(evidence) == "dict" {
return __fact_forget_evidence_entry(out, fact_kind, evidence)
}
if type_of(evidence) != "list" {
__fact_error("HARN-FACT-008", "invalidate_facts evidence must be a string, dict, or list")
}
for entry in evidence {
if type_of(entry) == "string" {
out = __fact_push_tag(out, __fact_evidence_ref_tag(fact_kind, entry))
} else if type_of(entry) == "dict" {
out = __fact_forget_evidence_entry(out, fact_kind, entry)
} else {
__fact_error("HARN-FACT-008", "invalidate_facts evidence entries must be strings or dicts")
}
}
return out
}
fn __fact_evidence_ref_tag(fact_kind, ref) -> string {
let text = __fact_text(ref)
if fact_kind == nil {
return "fact:evidence_ref:" + text
}
return "fact:" + to_string(fact_kind) + ":evidence_ref:" + text
}
fn __fact_evidence_kind_tag(fact_kind, kind) -> string {
let text = to_string(kind)
if fact_kind == nil {
return "fact:evidence:" + text
}
return "fact:" + to_string(fact_kind) + ":evidence:" + text
}
fn __fact_evidence_exact_tag(fact_kind, kind, ref) -> string {
let text = to_string(kind) + ":" + __fact_text(ref)
if fact_kind == nil {
return "fact:evidence:" + text
}
return "fact:" + to_string(fact_kind) + ":evidence:" + text
}
fn __fact_forget_evidence_entry(out, fact_kind, entry) -> list<string> {
let kind = entry?.kind == nil ? nil : __fact_evidence_kind(entry.kind)
let ref = __fact_text(entry?.ref)
if kind != nil && ref != "" {
return __fact_push_tag(out, __fact_evidence_exact_tag(fact_kind, kind, ref))
}
if ref != "" {
return __fact_push_tag(out, __fact_evidence_ref_tag(fact_kind, ref))
}
if kind != nil {
return __fact_push_tag(out, __fact_evidence_kind_tag(fact_kind, kind))
}
__fact_error("HARN-FACT-008", "invalidate_facts evidence requires kind or ref")
}
/**
* Invalidate matching facts by appending a memory tombstone.
*
* A string predicate invalidates an exact fact id when it starts with
* `fact_`; otherwise it is treated as a query. Dict predicates accept `id`,
* `key`, `kind`, `claim`, `query`, `tag`, `tags`, `evidence_ref`, and
* `evidence`.
*
* @effects: [store.write]
* @allocation: heap
* @errors: [HARN-FACT-002, HARN-FACT-008]
* @api_stability: experimental
* @example: invalidate_facts(predicate, scope)
*/
pub fn invalidate_facts(predicate, scope = nil) {
let opts = __fact_scope_options(scope)
let namespace = opts?.namespace ?? fact_namespace(opts?.scope)
return memory_forget(namespace, __fact_forget_predicate(predicate), opts)
}