import { agent_session_inject_feedback, agent_session_inject_reminder } from "std/agent/state"
import { find_files, read_json, read_toml } from "std/fs"
import { ext, is_absolute } from "std/path"
fn __canon_text(value) -> string {
return trim(to_string(value ?? ""))
}
fn __canon_path(root, rel) -> string {
let raw = __canon_text(rel)
if raw == "" {
return ""
}
if is_absolute(raw) || root == nil || __canon_text(root) == "" {
return raw
}
return path_join(__canon_text(root), raw)
}
fn __canon_manifest_path(root) -> string {
let text = __canon_text(root)
if text == "" {
return ""
}
return path_join(text, "canon-packs.json")
}
fn __canon_root_with_manifest(root) -> string {
let text = __canon_text(root)
if text == "" {
return ""
}
if harness.fs.exists(__canon_manifest_path(text)) {
return text
}
return ""
}
fn __canon_push_unique(items, value) {
let text = __canon_text(value)
if text == "" || contains(items, text) {
return items
}
return items.push(text)
}
fn __canon_manifest_has_routing(manifest) -> bool {
for pack in manifest?.packs ?? [] {
if len(pack?.extensions ?? []) > 0 || len(pack?.file_names ?? []) > 0 {
return true
}
}
return false
}
fn __canon_selector_matches(values, wanted) -> bool {
let target = lowercase(__canon_text(wanted))
if target == "" {
return false
}
for value in values ?? [] {
if lowercase(__canon_text(value)) == target {
return true
}
}
return false
}
fn __canon_pack_ids_for_path_from_manifest(path, manifest) -> list<string> {
let raw = __canon_text(path)
if raw == "" {
return []
}
let extension = lowercase(ext(raw))
let name = lowercase(basename(raw))
var ids = []
for pack in manifest?.packs ?? [] {
let id = __canon_text(pack?.id)
if id == "" {
continue
}
let matches_extension = __canon_selector_matches(pack?.extensions, extension)
let matches_name = __canon_selector_matches(pack?.file_names, name)
if matches_extension || matches_name {
ids = __canon_push_unique(ids, id)
}
}
return ids.sort()
}
/**
* Resolve the harn-canon root for callers that did not pass one explicitly.
*
* Resolution order:
* 1. `options.canon_root`, returned as-is so invalid explicit roots still fail
* when the caller loads `canon-packs.json`;
* 2. `HARN_CANON_ROOT`, when it contains `canon-packs.json`;
* 3. workspace-local `.harn/canon` or `.harn/harn-canon`, when present.
*
* @effects: [fs.read, env.read]
* @errors: []
* @api_stability: experimental
*/
pub fn canon_resolve_root(options = nil) -> string {
let opts = options ?? {}
let explicit = __canon_text(opts?.canon_root)
if explicit != "" {
return explicit
}
let env_root = __canon_root_with_manifest(harness.env.get_or("HARN_CANON_ROOT", ""))
if env_root != "" {
return env_root
}
let root = __canon_text(opts?.project_root ?? project_root() ?? cwd() ?? "")
if root == "" {
return ""
}
for candidate in [path_join(root, ".harn", "canon"), path_join(root, ".harn", "harn-canon")] {
let found = __canon_root_with_manifest(candidate)
if found != "" {
return found
}
}
return ""
}
/**
* Infer harn-canon pack ids for one file path from a parsed manifest.
*
* @effects: []
* @errors: []
* @api_stability: experimental
*/
pub fn canon_pack_ids_for_path(path, manifest = nil) -> list<string> {
let raw = __canon_text(path)
if raw == "" {
return []
}
if __canon_manifest_has_routing(manifest) {
return __canon_pack_ids_for_path_from_manifest(raw, manifest)
}
return []
}
/**
* Infer harn-canon pack ids for a list of `{path, text?}` file records.
*
* The parsed `canon-packs.json` manifest owns routing selectors. Callers that
* have not loaded harn-canon metadata should pass explicit pack ids instead of
* relying on a second extension table.
*
* @effects: []
* @errors: []
* @api_stability: experimental
*/
pub fn canon_pack_ids_for_files(files, manifest = nil) -> list<string> {
var ids = []
for file in files ?? [] {
let path = __canon_text(file?.path ?? file?.name)
for id in canon_pack_ids_for_path(path, manifest) {
ids = __canon_push_unique(ids, id)
}
}
return ids.sort()
}
/**
* Build the Flow `slice` shape used by harn-canon packs from file records.
*
* @effects: []
* @errors: []
* @api_stability: experimental
*/
pub fn canon_slice_from_files(files, metadata = nil) -> dict {
var normalized = []
for file in files ?? [] {
let path = __canon_text(file?.path ?? file?.name)
if path != "" {
normalized = normalized
.push({path: path, text: to_string(file?.text ?? file?.content ?? "")})
}
}
return {files: normalized, metadata: metadata ?? {}}
}
/**
* Build a canon slice by reading changed file paths from a workspace.
*
* Options:
* root | workspace_root: base directory for relative paths
* include_missing: include missing paths with empty text and `missing: true`
* metadata: slice metadata
*
* @effects: [fs.read]
* @errors: [runtime]
* @api_stability: experimental
*/
pub fn canon_slice_from_paths(paths, options = nil) -> dict {
let opts = options ?? {}
let root = opts?.root ?? opts?.workspace_root
var files = []
for item in paths ?? [] {
let path = __canon_text(item?.path ?? item?.name ?? item)
if path == "" {
continue
}
if item?.text != nil || item?.content != nil {
files = files.push({path: path, text: to_string(item?.text ?? item?.content ?? "")})
continue
}
let read_path = __canon_path(root, path)
if harness.fs.exists(read_path) {
files = files.push({path: path, text: harness.fs.read_text(read_path)})
} else if opts?.include_missing ?? false {
files = files.push({path: path, text: "", missing: true})
}
}
return {files: files, metadata: opts?.metadata ?? {}}
}
/**
* Load `canon-packs.json` from a configured harn-canon root.
*
* @effects: [fs.read]
* @errors: [runtime]
* @api_stability: experimental
*/
pub fn canon_load_manifest(canon_root: string) -> dict {
let root = __canon_text(canon_root)
if root == "" {
throw "canon_load_manifest: canon_root is required"
}
let path = __canon_manifest_path(root)
let manifest = read_json(path, nil)
if manifest == nil {
throw "canon_load_manifest: failed to read " + path
}
return manifest
}
fn __canon_manifest_pack(manifest, id) {
let wanted = __canon_text(id)
for pack in manifest?.packs ?? [] {
if __canon_text(pack?.id) == wanted {
return pack
}
}
return nil
}
fn __canon_requested_needs_manifest(items) -> bool {
for item in items ?? [] {
if type_of(item) == "string" {
return true
}
}
return false
}
fn __canon_item_file_names(item) {
return item?.file_names ?? item?.fileNames ?? item?["file-names"] ?? []
}
fn __canon_append_manifest_packs(packs, base_dir, manifest) {
if manifest == nil {
return packs
}
var out = packs
for pack in manifest?.packs ?? [] {
let id = __canon_text(pack?.id)
let invariants = __canon_text(pack?.invariants ?? pack?.path ?? pack?.module_path)
if id == "" || invariants == "" {
continue
}
out = out
.push(
{
id: id,
title: __canon_text(pack?.title ?? id),
invariants: __canon_path(base_dir, invariants),
extensions: pack?.extensions ?? [],
file_names: __canon_item_file_names(pack),
predicates: pack?.predicates,
},
)
}
return out
}
fn __canon_manifest_path_for_contribution(package_dir, contribution) -> string {
let explicit = __canon_text(contribution?.manifest ?? contribution?.canon_packs ?? contribution?.canonPacks)
if explicit != "" {
return __canon_path(package_dir, explicit)
}
let raw_path = __canon_text(contribution?.path)
if raw_path == "" {
return ""
}
let manifest_path = path_join(__canon_path(package_dir, raw_path), "canon-packs.json")
if harness.fs.exists(manifest_path) {
return manifest_path
}
return ""
}
fn __canon_manifest_from_installed_packages(project_root_value) -> dict? {
let root = __canon_text(project_root_value)
if root == "" {
return nil
}
let packages_dir = path_join(root, ".harn", "packages")
if !harness.fs.exists(packages_dir) {
return nil
}
var packs = []
for manifest_path in find_files(root, ".harn/packages/*/harn.toml").sort() {
let manifest = read_toml(manifest_path, nil)
if manifest == nil {
continue
}
let package_dir = dirname(manifest_path)
for contribution in manifest?.contributes ?? [] {
if __canon_text(contribution?.kind) != "harn.canon" {
continue
}
let contribution_manifest_path = __canon_manifest_path_for_contribution(package_dir, contribution)
if contribution_manifest_path != "" {
packs = __canon_append_manifest_packs(
packs,
dirname(contribution_manifest_path),
read_json(contribution_manifest_path, nil),
)
continue
}
let raw_path = __canon_text(contribution?.path ?? contribution?.invariants ?? contribution?.file)
if raw_path == "" {
continue
}
let contribution_path = __canon_path(package_dir, raw_path)
let invariants = if contribution_path.ends_with(".harn") {
contribution_path
} else {
path_join(contribution_path, "invariants.harn")
}
let id = __canon_text(contribution?.id ?? basename(contribution_path))
if id == "" {
continue
}
packs = packs
.push(
{
id: id,
title: __canon_text(contribution?.title ?? id),
invariants: invariants,
extensions: contribution?.extensions ?? [],
file_names: __canon_item_file_names(contribution),
predicates: contribution?.predicates,
},
)
}
}
if len(packs) == 0 {
return nil
}
return {schema_version: 1, packs: packs.sort_by({ pack -> pack.id })}
}
fn __canon_merge_manifest_packs(primary, secondary) -> dict? {
if primary == nil {
return secondary
}
if secondary == nil {
return primary
}
var packs = []
for pack in primary?.packs ?? [] {
packs = packs.push(pack)
}
for pack in secondary?.packs ?? [] {
let id = __canon_text(pack?.id)
if id == "" || __canon_manifest_pack({packs: packs}, id) != nil {
continue
}
packs = packs.push(pack)
}
return {schema_version: primary?.schema_version ?? secondary?.schema_version ?? 1, packs: packs}
}
fn __canon_pack_from_item(item, manifest, canon_root) {
if type_of(item) == "string" {
let id = __canon_text(item)
let found = __canon_manifest_pack(manifest, id)
if found == nil {
return {id: id, title: id, invariants: __canon_path(canon_root, path_join(id, "invariants.harn"))}
}
return {
id: id,
title: __canon_text(found?.title ?? id),
invariants: __canon_path(canon_root, found?.invariants),
}
}
if type_of(item) != "dict" {
throw "canon_resolve_packs: pack entries must be strings or dicts"
}
let id = __canon_text(item?.id ?? item?.pack_id)
let title = __canon_text(item?.title ?? id)
let invariants = __canon_text(item?.invariants ?? item?.path ?? item?.module_path)
if id == "" || invariants == "" {
throw "canon_resolve_packs: dict pack entries need `id` and `invariants`"
}
return {
id: id,
title: if title == "" {
id
} else {
title
},
invariants: __canon_path(canon_root, invariants),
predicates: item?.predicates,
}
}
/**
* Resolve harn-canon pack config from explicit pack ids, dict entries, or files.
*
* Options:
* canon_root: directory containing `canon-packs.json`
* manifest: parsed manifest override
* packs | pack_ids: strings or `{id, invariants, title?, predicates?}` dicts
* files: file records used for language inference when packs are omitted
*
* Default project-local roots are merged with installed `.harn/packages`
* contributions of kind `harn.canon`. Explicit `canon_root`, `HARN_CANON_ROOT`,
* and `manifest` values stay authoritative.
*
* @effects: [fs.read]
* @errors: [runtime]
* @api_stability: experimental
*/
pub fn canon_resolve_packs(options = nil) -> list<dict> {
let opts = options ?? {}
let project = __canon_text(opts?.project_root ?? project_root() ?? cwd() ?? "")
let explicit_root = __canon_text(opts?.canon_root)
let env_root = if explicit_root == "" {
__canon_root_with_manifest(harness.env.get_or("HARN_CANON_ROOT", ""))
} else {
""
}
let configured_root = explicit_root != "" || env_root != ""
let canon_root = if explicit_root != "" {
explicit_root
} else if env_root != "" {
env_root
} else {
canon_resolve_root(opts)
}
var requested = opts?.packs ?? opts?.pack_ids ?? []
if type_of(requested) == "string" {
requested = [requested]
}
let needs_manifest = len(requested) == 0 || __canon_requested_needs_manifest(requested)
let manifest = opts?.manifest
?? if needs_manifest && canon_root != nil && __canon_text(canon_root) != "" {
let root_manifest = canon_load_manifest(canon_root)
if configured_root {
root_manifest
} else {
__canon_merge_manifest_packs(root_manifest, __canon_manifest_from_installed_packages(project))
}
} else if needs_manifest {
__canon_manifest_from_installed_packages(project)
} else {
nil
}
if len(requested) == 0 {
requested = canon_pack_ids_for_files(opts?.files ?? opts?.slice?.files ?? [], manifest)
}
var resolved = []
for item in requested {
resolved = resolved.push(__canon_pack_from_item(item, manifest, canon_root))
}
return resolved
}
/**
* Evaluate a Flow slice against selected harn-canon packs.
*
* Returns `{ok, status, packs, selected_pack_ids}` where each pack result carries
* the raw Flow report and a bounded feedback string.
*
* @effects: [fs.read]
* @errors: [runtime]
* @api_stability: experimental
*/
pub fn canon_evaluate_slice(slice, options = nil) -> dict {
let opts = options ?? {}
let packs = canon_resolve_packs(opts + {slice: slice})
var reports = []
var selected = []
var ok = true
for pack in packs {
selected = selected.push(pack.id)
let report = flow_evaluate_invariants(
"",
slice,
{
path: pack.invariants,
predicates: pack?.predicates ?? opts?.predicates,
include_semantic: opts?.include_semantic ?? false,
budget_ms: opts?.budget_ms ?? 50,
ctx: opts?.ctx ?? opts?.predicate_ctx ?? {},
repo_at_base: opts?.repo_at_base,
},
)
ok = ok && (report?.ok ?? false)
reports = reports
.push(
{
pack_id: pack.id,
title: pack.title,
invariants: pack.invariants,
report: report,
feedback: flow_invariant_feedback(report, opts?.feedback ?? {}),
},
)
}
let status = if len(packs) == 0 {
"no_packs"
} else if ok {
"pass"
} else {
"fail"
}
return {ok: ok, status: status, packs: reports, selected_pack_ids: selected}
}
/**
* Render compact agent-facing feedback from `canon_evaluate_slice`.
*
* @effects: []
* @errors: []
* @api_stability: experimental
*/
pub fn canon_feedback_text(result, options = nil) -> string {
let opts = options ?? {}
var sections = []
for pack in result?.packs ?? [] {
let body = trim(to_string(pack?.feedback ?? ""))
if body != "" {
sections = sections.push("[" + pack.pack_id + "] " + pack.title + "\n" + body)
}
}
if len(sections) == 0 {
return ""
}
return __canon_text(opts?.header ?? "harn-canon feedback") + "\n\n" + join(sections, "\n\n")
}
/**
* Evaluate harn-canon packs and inject bounded feedback into an agent session.
*
* Options:
* inject: "feedback" | "reminder" | "none" (default "feedback")
* feedback_kind: feedback kind when `inject == "feedback"` (default "harn_canon")
* reminder: reminder options merged with the generated body when `inject == "reminder"`
*
* @effects: [fs.read, host]
* @errors: [runtime]
* @api_stability: experimental
*/
pub fn canon_inject_feedback(session_id: string, slice, options = nil) -> dict {
let opts = options ?? {}
let result = canon_evaluate_slice(slice, opts)
let feedback = canon_feedback_text(result, opts)
let mode = lowercase(__canon_text(opts?.inject ?? "feedback"))
var reminder_id = nil
if feedback != "" && mode == "feedback" {
agent_session_inject_feedback(session_id, opts?.feedback_kind ?? "harn_canon", feedback)
} else if feedback != "" && mode == "reminder" {
let reminder = (opts?.reminder ?? {})
+ {
body: feedback,
tags: opts?.reminder?.tags ?? ["harn-canon"],
dedupe_key: opts?.reminder?.dedupe_key ?? ("harn-canon/" + join(result.selected_pack_ids, ",")),
ttl_turns: opts?.reminder?.ttl_turns ?? 1,
preserve_on_compact: opts?.reminder?.preserve_on_compact ?? true,
}
reminder_id = agent_session_inject_reminder(session_id, reminder)
} else if mode != "none" && mode != "feedback" && mode != "reminder" {
throw "canon_inject_feedback: inject must be feedback|reminder|none"
}
return result
+ {feedback_text: feedback, injected: feedback != "" && mode != "none", reminder_id: reminder_id}
}
/**
* Evaluate harn-canon packs for changed paths and inject bounded feedback.
*
* This is the agent-loop seam for hosts that already know which files changed:
* the host or pipeline passes paths, Harn reads the current workspace content,
* harn-canon remains the predicate source of truth, and delivery still goes
* through `canon_inject_feedback`.
*
* @effects: [fs.read, host]
* @errors: [runtime]
* @api_stability: experimental
*/
pub fn canon_inject_paths_feedback(session_id: string, paths, options = nil) -> dict {
let opts = options ?? {}
let slice = canon_slice_from_paths(paths, opts)
return canon_inject_feedback(session_id, slice, opts + {files: slice.files})
}
fn __canon_event_path(item) -> string {
let kind = type_of(item)
if kind == "string" {
return __canon_text(item)
}
if kind != "dict" {
return ""
}
return __canon_text(
item?.path
?? item?.name
?? item?.file
?? item?.file_path
?? item?.filePath
?? item?.target_path
?? item?.targetPath
?? item?.target,
)
}
fn __canon_event_push_path(items, value) {
let path = __canon_event_path(value)
if path == "" || contains(items, path) {
return items
}
return items.push(path)
}
fn __canon_event_push_path_bucket(items, bucket) {
if bucket == nil {
return items
}
if type_of(bucket) != "list" {
return __canon_event_push_path(items, bucket)
}
var out = items
for item in bucket ?? [] {
out = __canon_event_push_path(out, item)
}
return out
}
fn __canon_event_strip_path_token(raw) -> string {
var token = __canon_text(raw)
while starts_with(token, "`") || starts_with(token, "'") || starts_with(token, "\"")
|| starts_with(token, "(")
|| starts_with(token, "[") {
token = substring(token, 1)
}
while ends_with(token, "`") || ends_with(token, "'") || ends_with(token, "\"")
|| ends_with(token, ")")
|| ends_with(token, "]")
|| ends_with(token, ":")
|| ends_with(token, ",")
|| ends_with(token, ".") {
token = substring(token, 0, len(token) - 1)
}
return token
}
/**
* Extract a likely changed path from a tool-result record.
*
* Tool hosts should prefer structured `changed_paths` / `files_written` fields;
* this helper exists for older edit-result records that only preserve call
* arguments or a terse first-line observation.
*
* @effects: []
* @errors: []
* @api_stability: experimental
*/
pub fn canon_event_write_path_from_result(result) -> string {
if type_of(result) != "dict" {
return ""
}
for carrier in [
result?.params,
result?.args,
result?.arguments,
result?.tool_args,
result?.input,
result?.raw_input,
result?.function?.arguments,
] {
if type_of(carrier) == "dict" {
let path = __canon_text(
carrier?.path
?? carrier?.file
?? carrier?.file_path
?? carrier?.filePath
?? carrier?.target
?? carrier?.target_path
?? carrier?.targetPath
?? carrier?.folder,
)
if path != "" {
return path
}
}
}
let body = __canon_text(result?.observation ?? result?.result ?? result?.rendered_result ?? result?.raw_output)
let first_line = to_string((body.split("\n") ?? [""])[0] ?? "")
for raw_token in first_line.split(" ") {
let token = __canon_event_strip_path_token(raw_token)
if token == "" {
continue
}
let extension = lowercase(ext(token))
if contains(token, "/") || extension != "" {
return token
}
}
return ""
}
fn __canon_event_result_ok(result) -> bool {
if result?.ok != nil {
return result.ok ? true : false
}
if result?.success != nil {
return result.success ? true : false
}
let status = lowercase(__canon_text(result?.status))
if status == "error" || status == "failed" || status == "failure" {
return false
}
return !(result?.rejected ?? false)
}
fn __canon_event_push_tool_result_paths(items, result) {
if type_of(result) != "dict" {
return items
}
var out = items
for bucket in [
result?.changed_paths ?? result?.changedPaths,
result?.files_written ?? result?.filesWritten,
result?.result?.changed_paths ?? result?.result?.changedPaths,
result?.result?.files_written ?? result?.result?.filesWritten,
result?.raw_output?.changed_paths ?? result?.raw_output?.changedPaths,
result?.raw_output?.files_written ?? result?.raw_output?.filesWritten,
] {
out = __canon_event_push_path_bucket(out, bucket)
}
let tool_name = lowercase(__canon_text(result?.tool_name ?? result?.name))
if contains(["edit", "scaffold", "run_codemod", "write", "write_file"], tool_name)
&& __canon_event_result_ok(result) {
out = __canon_event_push_path(out, canon_event_write_path_from_result(result))
}
return out
}
fn __canon_event_push_tool_results_paths(items, bucket) {
if type_of(bucket) != "list" {
return __canon_event_push_tool_result_paths(items, bucket)
}
var out = items
for result in bucket ?? [] {
out = __canon_event_push_tool_result_paths(out, result)
}
return out
}
fn __canon_event_observation_field(item, key) -> string {
if type_of(item) != "dict" {
return ""
}
return __canon_text(item[key])
}
fn __canon_event_observation_explicit(item) -> bool {
if type_of(item) != "dict" {
return false
}
let schema = __canon_event_observation_field(item, "schema")
let kind = lowercase(__canon_event_observation_field(item, "kind"))
let event_type = lowercase(__canon_event_observation_field(item, "type"))
let mode = lowercase(__canon_event_observation_field(item, "mode"))
return schema == "harn.probe.v1"
|| kind == "observe_output"
|| kind == "observed_output"
|| kind == "observation"
|| event_type == "observe_output"
|| event_type == "observed_output"
|| event_type == "observation"
|| mode == "observe_output"
}
fn __canon_event_symbol(value) -> string {
let kind = type_of(value)
if kind == "string" {
return __canon_text(value)
}
if kind != "dict" {
return ""
}
for key in [
"symbol",
"observed_symbol",
"observedSymbol",
"target_symbol",
"targetSymbol",
"callee",
"function_name",
"functionName",
] {
let field = __canon_event_observation_field(value, key)
if field != "" {
return field
}
}
if __canon_event_observation_explicit(value) {
return __canon_event_observation_field(value, "name")
}
return ""
}
fn __canon_event_push_symbol(items, value) {
let symbol = __canon_event_symbol(value)
if symbol == "" || contains(items, symbol) {
return items
}
return items.push(symbol)
}
fn __canon_event_push_symbol_bucket(items, bucket) {
if bucket == nil {
return items
}
if type_of(bucket) != "list" {
return __canon_event_push_symbol(items, bucket)
}
var out = items
for item in bucket ?? [] {
out = __canon_event_push_symbol(out, item)
}
return out
}
fn __canon_event_push_tool_result_symbols(items, bucket) {
if type_of(bucket) != "list" {
return items
}
var out = items
for result in bucket ?? [] {
out = __canon_event_push_symbol(out, result)
out = __canon_event_push_symbol(out, result?.result)
out = __canon_event_push_symbol(out, result?.raw_output)
out = __canon_event_push_symbol_bucket(out, result?.observed_symbols ?? result?.observedSymbols)
out = __canon_event_push_symbol_bucket(
out,
result?.result?.observed_symbols ?? result?.result?.observedSymbols,
)
out = __canon_event_push_symbol_bucket(
out,
result?.raw_output?.observed_symbols ?? result?.raw_output?.observedSymbols,
)
}
return out
}
/**
* Extract changed paths already carried on an agent event.
*
* This is intentionally host-neutral: it reads event payloads and tool-result
* records only. Callers that need a host fallback should pass one to
* `canon_event_router`.
*
* @effects: []
* @errors: []
* @api_stability: experimental
*/
pub fn canon_event_paths(event) -> list<string> {
var paths = []
for bucket in [
event?.changed_paths ?? [],
event?.changedPaths ?? [],
event?.files_written ?? [],
event?.filesWritten ?? [],
event?.paths ?? [],
event?.iteration_info?.changed_paths ?? [],
event?.iteration_info?.changedPaths ?? [],
event?.iteration_info?.files_written ?? [],
event?.iteration_info?.filesWritten ?? [],
event?.iterationInfo?.changed_paths ?? [],
event?.iterationInfo?.changedPaths ?? [],
event?.iterationInfo?.files_written ?? [],
event?.iterationInfo?.filesWritten ?? [],
] {
paths = __canon_event_push_path_bucket(paths, bucket)
}
return __canon_event_push_tool_results_paths(paths, event?.tool_results ?? event?.toolResults)
}
/**
* Extract explicit producer-output observations from an agent event.
*
* harn-canon predicates can consume these under `ctx.observed_symbols` to
* distinguish assertions grounded in observed output from guessed literals.
*
* @effects: []
* @errors: []
* @api_stability: experimental
*/
pub fn canon_event_observed_symbols(event) -> list<string> {
var symbols = []
for bucket in [
event?.observed_symbols ?? event?.observedSymbols,
event?.observed_symbol ?? event?.observedSymbol,
event?.observation,
event?.observations,
event?.probe,
event?.probes,
event?.iteration_info?.observed_symbols ?? event?.iteration_info?.observedSymbols,
event?.iterationInfo?.observed_symbols ?? event?.iterationInfo?.observedSymbols,
] {
symbols = __canon_event_push_symbol_bucket(symbols, bucket)
}
return __canon_event_push_tool_result_symbols(symbols, event?.tool_results ?? event?.toolResults)
}
fn __canon_event_state_key(session_id: string, suffix: string, options = nil) -> string {
let opts = options ?? {}
let prefix = __canon_text(opts?.state_key_prefix ?? "harn.canon_feedback")
return prefix + "." + __canon_text(session_id) + "." + suffix
}
fn __canon_event_stored_observed_symbols(session_id: string, options = nil) -> list {
let raw = store_get(__canon_event_state_key(session_id, "observed_symbols", options))
if type_of(raw) == "list" {
return raw
}
return []
}
/**
* Merge event observations into the per-session harn-canon state.
*
* @effects: [store]
* @errors: []
* @api_stability: experimental
*/
pub fn canon_event_record_observed_symbols(session_id: string, event, options = nil) -> list<string> {
let sid = __canon_text(session_id)
let event_symbols = canon_event_observed_symbols(event ?? {})
if sid == "" {
return event_symbols
}
var merged = __canon_event_stored_observed_symbols(sid, options)
for symbol in event_symbols {
merged = __canon_event_push_symbol(merged, symbol)
}
store_set(__canon_event_state_key(sid, "observed_symbols", options), merged)
return merged
}
fn __canon_event_provider_paths(provider) {
if type_of(provider) == "closure" {
return provider()
}
return provider
}
fn __canon_event_fallback_paths(options = nil) -> list {
let opts = options ?? {}
let provider = opts?.changed_paths_provider ?? opts?.changedPathsProvider ?? opts?.changed_paths
?? opts?.changedPaths
var paths = []
for item in __canon_event_provider_paths(provider) ?? [] {
paths = __canon_event_push_path(paths, item)
}
return paths
}
fn __canon_event_options_with_ctx(options, observed_symbols) {
let opts = options ?? {}
let existing = opts?.ctx ?? opts?.predicate_ctx ?? {}
let ctx = if type_of(existing) == "dict" {
existing
} else {
{}
}
return opts + {ctx: ctx + {observed_symbols: observed_symbols ?? []}}
}
fn __canon_event_is_iteration_end(event) -> bool {
let event_type = lowercase(__canon_text(event?.type ?? event?.kind))
return event_type == "iteration_end"
|| event_type == "iteration-end"
|| event_type == "iterationend"
}
/**
* Route one agent event into harn-canon feedback when it carries changed paths.
*
* Options:
* enabled | active: false short-circuits without reading store or paths
* root | workspace_root | project_root: workspace root for path reads
* canon_root: optional harn-canon root; otherwise project-local package/root
* discovery is used by `canon_resolve_packs`
* inject: "reminder" | "feedback" | "none" (default "reminder")
* changed_paths_provider: optional list or closure fallback when the event
* carries no changed paths
* state_key_prefix: store namespace for observed symbols / last result
*
* @effects: [fs.read, store, host]
* @errors: []
* @api_stability: experimental
*/
pub fn canon_event_router(session_id: string, event, options = nil) {
let opts = options ?? {}
if !(opts?.enabled ?? opts?.active ?? true) {
return nil
}
let observed_symbols = canon_event_record_observed_symbols(session_id, event ?? {}, opts)
if !__canon_event_is_iteration_end(event ?? {}) {
return nil
}
let event_paths = canon_event_paths(event ?? {})
let paths = if len(event_paths) > 0 {
event_paths
} else {
__canon_event_fallback_paths(opts)
}
if len(paths) == 0 {
return nil
}
let workspace_root = __canon_text(opts?.root ?? opts?.workspace_root ?? opts?.project_root ?? project_root() ?? cwd())
let inject_options = __canon_event_options_with_ctx(opts, observed_symbols)
+ {root: workspace_root, project_root: workspace_root, inject: opts?.inject ?? "reminder"}
let injected = try {
canon_inject_paths_feedback(session_id, paths, inject_options)
}
if !is_ok(injected) {
let message = to_string(injected)
store_set(__canon_event_state_key(session_id, "last_error", opts), message)
return {ok: false, error: message, paths: paths}
}
let result = unwrap(injected) + {paths: paths}
store_set(__canon_event_state_key(session_id, "last_result", opts), result)
return result
}
/**
* Subscribe `canon_event_router` to an agent session once.
*
* @effects: [store, host]
* @errors: [runtime]
* @api_stability: experimental
*/
pub fn canon_install_feedback_sink(session_id: string, options = nil) -> nil {
let opts = options ?? {}
if !(opts?.enabled ?? opts?.active ?? true) {
return nil
}
let key = __canon_event_state_key(session_id, "installed", opts)
if store_get(key) {
return nil
}
store_set(key, true)
let handler = { event -> canon_event_router(session_id, event, opts) }
agent_subscribe(session_id, handler)
return nil
}