import { pl_curated_proposal } from "std/agent/pattern_knowledge_curated"
import {
pl_memory_options,
pl_migration_marker,
pl_namespace,
pl_now,
pl_observation_file,
pl_observation_from_input,
pl_observation_from_legacy,
pl_pending_file,
pl_project_collection,
pl_project_root,
pl_project_value,
pl_read_jsonl_payloads,
pl_record_values,
pl_replace_state,
pl_sanitize_tool_sequence,
pl_state_file,
pl_store_observation,
pl_store_record,
pl_unique_record_id,
} from "std/agent/pattern_knowledge_persistence"
import {
PATTERN_NAMESPACE,
PATTERN_SCHEMA,
pl_short_hash,
pl_slugify,
pl_text,
pl_validate_skill_name,
} from "std/agent/pattern_knowledge_values"
// std/agent/pattern_knowledge.harn
//
// Cross-session repeated-work knowledge for agent pattern recall. The durable
// source of truth is Harn `std/memory`; hosts only provide project-root facts
// and render review UI.
import { write_json } from "std/fs"
import { memory_forget } from "std/memory"
import {
SessionStoreListOptions,
SessionStoreOptions,
session_store_events,
session_store_list,
} from "std/session-store"
const OBSERVATION_CAP: int = 500
const DEFAULT_SUPPORT_THRESHOLD: int = 5
const DEFAULT_WINDOW_DAYS: int = 14
const DEFAULT_SUPPRESSION_DAYS: int = 14
const DEFAULT_SESSION_IMPORT_LIMIT: int = 100
const STOP_WORDS = [
"about",
"after",
"again",
"all",
"and",
"any",
"are",
"can",
"could",
"for",
"from",
"has",
"have",
"how",
"into",
"just",
"make",
"more",
"not",
"now",
"off",
"the",
"this",
"that",
"then",
"there",
"these",
"those",
"through",
"use",
"using",
"was",
"what",
"when",
"where",
"with",
"would",
"you",
"your",
]
fn pl_normalize_words(text: string) -> list {
const cleaned = regex_replace(r"[^a-z0-9]+", " ", (text ?? "").lower()) ?? ""
return cleaned.split(" ").filter({ word -> word != "" }).to_list()
}
fn pl_significant_words(text: string) -> list {
let seen = []
let out = []
for word in pl_normalize_words(text) {
if len(word) < 3 || STOP_WORDS.contains(word) || seen.contains(word) {
continue
}
seen = seen + [word]
out = out + [word]
}
return out
}
/**
* Return the current pattern-learning state for the selected project memory namespace.
*
* @effects: [store.read]
* @errors: []
* @api_stability: experimental
*/
pub fn pattern_learning_state(harness: Harness, options = nil) -> dict {
pattern_learning_ensure_migrated(harness, options)
let best = nil
for value in pl_record_values(harness.fs, harness.memory, "state", options) {
if best == nil || pl_text(value?.updated_at) >= pl_text(best?.updated_at) {
best = value
}
}
return best ?? {schema: PATTERN_SCHEMA, record_kind: "state", enabled: true, suppressed_until: {}}
}
/**
* Enable or disable observation capture for the selected project memory namespace.
*
* @effects: [store.read, store.write]
* @errors: []
* @api_stability: experimental
*/
pub fn pattern_learning_set_enabled(harness: Harness, enabled: bool, options = nil) -> dict {
const state = pattern_learning_state(harness, options)
return pl_replace_state(harness, state + {enabled: enabled}, options)
}
fn pl_session_store_options(options = nil) -> SessionStoreOptions {
const root = pl_text(options?.session_store_root)
return root == "" ? {} : {root: root}
}
fn pl_session_store_list_options(limit: int, options = nil) -> SessionStoreListOptions {
const root = pl_text(options?.session_store_root)
return root == "" ? {limit: limit, sort_by: "updated_at", order: "descending"} : {
root: root,
limit: limit,
sort_by: "updated_at",
order: "descending",
}
}
fn pl_session_event_kind(event: dict) -> string {
return pl_text(event?.kind?.type ?? event?.kind?.kind)
}
fn pl_session_transcript_event(event: dict) -> dict {
return event?.payload?.transcript_event ?? {}
}
fn pl_session_prompt(events: list) -> string {
for event in events ?? [] {
const transcript_event = pl_session_transcript_event(event)
if pl_text(transcript_event?.kind) != "message"
|| pl_text(transcript_event?.role) != "user"
|| pl_text(transcript_event?.visibility) != "public" {
continue
}
const prompt = pl_text(transcript_event?.text)
if prompt != "" {
return prompt
}
}
return ""
}
fn pl_session_tool_sequence(events: list) -> list {
let out = []
for event in events ?? [] {
const transcript_event = pl_session_transcript_event(event)
if pl_session_event_kind(event) != "tool_call"
&& pl_text(transcript_event?.kind) != "tool_call" {
continue
}
const name = pl_text(transcript_event?.metadata?.tool_name)
if name != "" {
out = out + [name]
}
}
return pl_sanitize_tool_sequence(out)
}
fn pl_session_run_id(event: dict) -> string {
return pl_text(event?.headers?.run_id)
}
fn pl_session_run_key(session_id: string, run_id: string) -> string {
return session_id + "::" + run_id
}
fn pl_session_runs(events: list) -> list {
let ids = []
let grouped = {}
for event in events ?? [] {
const run_id = pl_session_run_id(event)
if run_id == "" {
continue
}
if !ids.contains(run_id) {
ids = ids + [run_id]
}
grouped = grouped + {[run_id]: (grouped[run_id] ?? []) + [event]}
}
return ids.map({ run_id -> {run_id: run_id, events: grouped[run_id] ?? []} }).to_list()
}
fn pl_run_has_eligible_natural_terminal(events: list) -> bool {
for event in events ?? [] {
const transcript_event = pl_session_transcript_event(event)
const headers = event?.headers ?? {}
if pl_session_event_kind(event) != "agent_run_terminal"
&& pl_text(transcript_event?.kind)
!= "agent_run_terminal" {
continue
}
if pl_text(transcript_event?.metadata?.terminal?.kind) == "natural"
&& pl_text(
headers["harn.workflow_learning.eligibility"],
)
== "eligible" {
return true
}
}
return false
}
/**
* Corpus provenance belongs to the producer-owned terminal event for one run,
* not an inference over its prompt, session id, or tool name. This lets one
* conversation contain distinct ask, coding, curation, and evaluation turns
* without allowing a control turn to taint the reusable-workflow corpus.
*/
fn pl_observed_evidence_keys(fs: HarnessFs, memory: HarnessMemory, options = nil) -> list {
let ids = []
for observation in pl_record_values(fs, memory, "observation", options) {
const source_id = pl_text(observation?.source_id)
const key = source_id == "" ? "legacy_session:" + pl_text(observation?.session_id) : source_id
if key != "" && !ids.contains(key) {
ids = ids + [key]
}
}
return ids
}
fn pl_run_observed_at(events: list, fallback: string) -> string {
if len(events) == 0 {
return fallback
}
const observed_at = pl_text(events[len(events) - 1]?.ts)
return observed_at == "" ? fallback : observed_at
}
/**
* Backfill explicitly eligible, natural-terminal Harn runs into pattern memory
* before an agentic curation pass. The canonical session store remains the
* evidence owner; this projection copies a bounded public prompt and tool
* sequence once. Runs without producer-owned terminal eligibility are
* excluded.
*
* @effects: [store.read, store.write, fs.read, fs.write]
* @errors: []
* @api_stability: experimental
*/
pub fn pattern_learning_import_session_records(harness: Harness, options = nil) -> dict {
pattern_learning_ensure_migrated(harness, options)
const session_options = pl_session_store_options(options)
const limit = min(
OBSERVATION_CAP,
max(1, options?.session_import_limit ?? DEFAULT_SESSION_IMPORT_LIMIT),
)
const sessions = session_store_list(harness.agent, pl_session_store_list_options(limit, options))
let observed_evidence_keys = pl_observed_evidence_keys(harness.fs, harness.memory, options)
let imported = 0
let skipped_existing = 0
let skipped_ineligible = 0
let scanned_runs = 0
for session in sessions {
const session_id = pl_text(session.id)
if session_id == "" {
skipped_ineligible = skipped_ineligible + 1
continue
}
const events = session_store_events(harness.agent, session_id, session_options)
const runs = pl_session_runs(events)
if len(runs) == 0 {
skipped_ineligible = skipped_ineligible + 1
continue
}
for run in runs {
scanned_runs = scanned_runs + 1
const run_id = pl_text(run?.run_id)
const evidence_key = pl_session_run_key(session_id, run_id)
if observed_evidence_keys.contains(evidence_key)
|| observed_evidence_keys.contains(
"legacy_session:" + session_id,
) {
skipped_existing = skipped_existing + 1
continue
}
const run_events = run?.events ?? []
if !pl_run_has_eligible_natural_terminal(run_events) {
skipped_ineligible = skipped_ineligible + 1
continue
}
const prompt = pl_session_prompt(run_events)
if prompt == "" {
skipped_ineligible = skipped_ineligible + 1
continue
}
pl_store_observation(
harness,
pl_observation_from_input(
harness.clock,
harness.random,
{
source_id: evidence_key,
session_id: session_id,
run_id: run_id,
prompt: prompt,
tool_sequence: pl_session_tool_sequence(run_events),
observed_at: pl_run_observed_at(
run_events,
pl_text(session.updated_at) == "" ? pl_now(harness.clock, options) : session
.updated_at,
),
},
options,
),
options,
)
observed_evidence_keys = observed_evidence_keys + [evidence_key]
imported = imported + 1
}
}
pl_cap_observations(harness, options)
return {
scanned_count: scanned_runs,
scanned_session_count: len(sessions),
imported_count: imported,
skipped_existing_count: skipped_existing,
skipped_ineligible_count: skipped_ineligible,
observation_count: len(pattern_learning_curator_evidence(harness, options)),
}
}
/**
* Return active pattern-learning observations sorted by observation time.
*
* @effects: [store.read]
* @errors: []
* @api_stability: experimental
*/
pub fn pattern_learning_observations(harness: Harness, options = nil) -> list {
pattern_learning_ensure_migrated(harness, options)
return pl_record_values(harness.fs, harness.memory, "observation", options).sorted_by(
{ item -> pl_text(item?.observed_at) },
)
}
fn pl_is_run_provenance_evidence(item: dict) -> bool {
return pl_text(item?.source_id) != ""
&& pl_text(item?.session_id) != ""
&& pl_text(item?.run_id) != ""
}
/**
* Return observations that are safe to provide to a curator or deterministic
* proposal refresher. Imported run evidence is the default. Callers that
* intentionally author manual evidence must opt in per invocation; historical
* records without either form of provenance remain inspectable but cannot
* become reusable capability proposals.
*
* @effects: [store.read]
* @errors: []
* @api_stability: experimental
*/
pub fn pattern_learning_curator_evidence(harness: Harness, options = nil) -> list {
const allow_manual = options?.allow_manual_evidence ?? false
return pattern_learning_observations(harness, options).filter(
{ item ->
return pl_is_run_provenance_evidence(item)
|| (allow_manual
&& pl_text(item?.evidence_kind) == "manual")
},
)
.to_list()
}
fn pl_forget_observation(fs: HarnessFs, memory: HarnessMemory, id: string, options = nil) -> nil {
memory_forget(memory, pl_namespace(options), {id: id}, pl_memory_options(fs, options))
}
fn pl_cap_observations(harness: Harness, options = nil) -> nil {
const observations = pattern_learning_observations(harness, options)
const overflow = len(observations) - OBSERVATION_CAP
if overflow <= 0 {
return
}
for observation in observations[0:overflow] {
pl_forget_observation(harness.fs, harness.memory, observation.id, options)
}
}
fn pl_prompt_cluster_key(prompt: string) -> string {
const words = pl_significant_words(prompt)
if len(words) < 3 {
return ""
}
return join(words[0:min(4, len(words))], "-")
}
fn pl_tool_sequence_key(sequence: list) -> string {
const names = pl_sanitize_tool_sequence(sequence)
if len(names) < 2 {
return ""
}
return join(names[0:min(8, len(names))], "-")
}
fn pl_push_group(groups: dict, key: string, observation: dict) -> dict {
if key == "" {
return groups
}
const current = groups[key] ?? []
return groups + {[key]: current + [observation]}
}
fn pl_recent_observations(observations: list, now_iso: string, window_days: int) -> list {
const now = date_parse(now_iso)
const max_age = window_days * 86400
let out = []
for observation in observations {
const observed = try {
date_parse(pl_text(observation?.observed_at))
}
if is_err(observed) {
continue
}
const age_seconds = duration_to_seconds(date_diff(now, unwrap(observed)))
if age_seconds >= 0 && age_seconds <= max_age {
out = out + [observation]
}
}
return out
}
fn pl_sample_prompts(observations: list) -> list {
let seen = []
let out = []
for observation in observations.sorted_by({ item -> pl_text(item?.observed_at) }) {
const prompt = pl_text(observation?.prompt)
if prompt == "" || seen.contains(prompt) {
continue
}
seen = seen + [prompt]
out = out + [prompt]
if len(out) >= 3 {
break
}
}
return out
}
fn pl_representative_tool_sequence(observations: list) -> list {
let best = []
for observation in observations {
const sequence = observation?.tool_sequence ?? []
if len(sequence) > len(best) {
best = sequence
}
}
return best
}
fn pl_title_from_words(words: list) -> string {
return join(words.map({ word -> word[0:1].upper() + word[1:] }), " ")
}
fn pl_skill_name(base: string) -> string {
const slug = pl_slugify(base)
if slug == "" {
return "learned-pattern"
}
let trimmed = slug[0:min(80, len(slug))]
while trimmed.ends_with("-") {
trimmed = trimmed[0:len(trimmed) - 1]
}
return trimmed == "" ? "learned-pattern" : trimmed
}
fn pl_render_skill_body(
title: string,
description: string,
when_to_use: string,
support: int,
samples: list,
sequence: list,
source: string,
) -> string {
let lines = [
"# " + title,
"",
description,
"",
"This skill was drafted from " + to_string(support) + " observed " + source + " matches.",
"",
"## When To Use",
"",
when_to_use,
"",
"## Guidance",
"",
"- Start by checking whether the current request really matches the observed examples.",
"- Reuse the project conventions already loaded in context before introducing a new workflow.",
"- Keep verification scoped to the files and commands touched by the task.",
"",
"## Observed Examples",
]
for sample in samples {
lines = lines + ["- " + pl_text(sample)]
}
if len(sequence) > 0 {
lines = lines + ["", "## Observed Tool Sequence", "", join(sequence, " -> ")]
}
return join(lines + [""], "\n")
}
fn pl_make_prompt_proposal(
id: string,
key: string,
observations: list,
existing: dict,
now: string,
) -> dict {
const words = key.split("-")
const title = pl_title_from_words(words)
const description = "Reusable guidance learned from " + to_string(len(observations))
+ " similar prompts."
const when_to_use = "Use when the request matches these recurring prompt terms: "
+ key.replace(
"-",
", ",
)
+ "."
const sequence = pl_representative_tool_sequence(observations)
return {
id: id,
kind: "skill",
source: "prompt_cluster",
name: existing?.name ?? pl_skill_name("learned-" + pl_slugify(title)),
title: title,
description: description,
when_to_use: when_to_use,
body: pl_render_skill_body(
title,
description,
when_to_use,
len(observations),
pl_sample_prompts(observations),
sequence,
"prompt cluster",
),
support: len(observations),
sample_prompts: pl_sample_prompts(observations),
tool_sequence: sequence,
created_at: existing?.created_at ?? now,
last_seen_at: now,
}
}
fn pl_make_tool_proposal(
id: string,
key: string,
observations: list,
existing: dict,
now: string,
) -> dict {
const sequence = key.split("-")
const title = "Repeated " + join(sequence, " -> ") + " workflow"
const description = "Reusable guidance learned from " + to_string(len(observations))
+ " runs with the same tool sequence."
const when_to_use = "Use when the task is likely to follow this tool sequence: "
+ join(
sequence,
" -> ",
)
+ "."
return {
id: id,
kind: "skill",
source: "tool_sequence",
name: existing?.name ?? pl_skill_name("learned-" + join(sequence, "-")),
title: title,
description: description,
when_to_use: when_to_use,
body: pl_render_skill_body(
title,
description,
when_to_use,
len(observations),
pl_sample_prompts(observations),
sequence,
"tool sequence",
),
support: len(observations),
sample_prompts: pl_sample_prompts(observations),
tool_sequence: sequence,
created_at: existing?.created_at ?? now,
last_seen_at: now,
}
}
fn pl_skill_roots(fs: HarnessFs, options = nil) -> list {
const root = pl_project_root(fs, options)
const namespace = pl_namespace(options)
if namespace == PATTERN_NAMESPACE {
return [path_join(root, ".claude", "skills"), path_join(root, ".burin", "skills")]
}
const namespace_dir = pl_skill_namespace_dir(namespace)
return [
path_join(root, ".claude", "skills", namespace_dir),
path_join(root, ".burin", "skills", namespace_dir),
]
}
fn pl_skill_namespace_dir(namespace: string) -> string {
const slug = pl_slugify(namespace)
return slug == "" ? "custom" : slug
}
/**
* Return the project skill root where accepted learned skills are written.
*
* @effects: []
* @errors: []
* @api_stability: experimental
*/
pub fn pattern_learning_skill_root(fs: HarnessFs, options = nil) -> string {
const root = path_join(pl_project_root(fs, options), ".claude", "skills")
const namespace = pl_namespace(options)
if namespace == PATTERN_NAMESPACE {
return root
}
return path_join(root, pl_skill_namespace_dir(namespace))
}
fn pl_frontmatter_value(body: string, key: string) -> string {
const prefix = key + ":"
for line in (body ?? "").split("\n") {
const trimmed = to_string(line ?? "").trim()
if trimmed.starts_with(prefix) {
let value = trimmed[len(prefix):].trim()
if len(value) >= 2
&& ((value.starts_with("\"") && value.ends_with("\""))
|| (value.starts_with("'")
&& value
.ends_with("'"))) {
value = value[1:len(value) - 1]
}
return value.replace("\\\"", "\"").replace("\\\\", "\\")
}
}
return ""
}
fn pl_load_skill_file(fs: HarnessFs, name: string, path: string) -> dict {
const body = fs.read_text(path)
return {
name: name,
description: pl_frontmatter_value(body, "description"),
when_to_use: pl_frontmatter_value(body, "when-to-use"),
body: body,
}
}
/**
* Load accepted learned skills from the project skill roots.
*
* @effects: [fs.read]
* @errors: []
* @api_stability: experimental
*/
pub fn pattern_learning_accepted_skills(fs: HarnessFs, options = nil) -> list {
let seen = []
let out = []
for root in pl_skill_roots(fs, options) {
if !fs.exists(root) {
continue
}
for name in (fs.list_dir(root) ?? []).sorted() {
const skill_file = path_join(root, name, "SKILL.md")
if seen.contains(name) || !fs.exists(skill_file) {
continue
}
seen = seen + [name]
out = out + [pl_load_skill_file(fs, name, skill_file)]
}
}
return out
}
fn pl_skill_exists(fs: HarnessFs, name: string, options = nil) -> bool {
for root in pl_skill_roots(fs, options) {
if fs.exists(path_join(root, name, "SKILL.md")) {
return true
}
}
return false
}
fn pl_existing_pending_by_id(harness: Harness, options = nil) -> dict {
let out = {}
for proposal in pattern_learning_pending(harness, options) {
out = out + {[proposal.id]: proposal}
}
return out
}
fn pl_is_suppressed(id: string, state: dict, now: string) -> bool {
const until = pl_text((state?.suppressed_until ?? state?.suppressedUntil ?? {})[id])
return until != "" && until > now
}
fn pl_sorted_proposal_insert(proposals: list, proposal: dict) -> list {
let out = []
let inserted = false
for existing in proposals {
const before = proposal.support > (existing?.support ?? 0)
|| (proposal.support
== (existing?.support ?? 0)
&& proposal.id < existing.id)
if before && !inserted {
out = out + [proposal]
inserted = true
}
out = out + [existing]
}
if !inserted {
out = out + [proposal]
}
return out
}
/**
* Recompute reviewable proposals from recent observations and store them as pending records.
*
* @effects: [store.read, store.write, fs.read]
* @errors: []
* @api_stability: experimental
*/
pub fn pattern_learning_refresh(harness: Harness, options = nil) -> list {
pattern_learning_ensure_migrated(harness, options)
const state = pattern_learning_state(harness, options)
if !(state?.enabled ?? true) {
return pattern_learning_pending(harness, options)
}
const now = pl_now(harness.clock, options)
const support_threshold = options?.support_threshold ?? DEFAULT_SUPPORT_THRESHOLD
const window_days = options?.window_days ?? DEFAULT_WINDOW_DAYS
const observations = pl_recent_observations(
pattern_learning_curator_evidence(harness, options),
now,
window_days,
)
const existing = pl_existing_pending_by_id(harness, options)
let prompt_groups = {}
let tool_groups = {}
for observation in observations {
prompt_groups = pl_push_group(
prompt_groups,
pl_prompt_cluster_key(observation.prompt),
observation,
)
tool_groups = pl_push_group(
tool_groups,
pl_tool_sequence_key(observation.tool_sequence),
observation,
)
}
let proposals = []
for key in keys(prompt_groups).sorted() {
const group = prompt_groups[key] ?? []
if len(group) < support_threshold {
continue
}
const id = "prompt-" + pl_short_hash(key)
if pl_is_suppressed(id, state, now) {
continue
}
const proposal = pl_make_prompt_proposal(id, key, group, existing[id] ?? {}, now)
if !pl_skill_exists(harness.fs, proposal.name, options) {
proposals = pl_sorted_proposal_insert(proposals, proposal)
}
}
for key in keys(tool_groups).sorted() {
const group = tool_groups[key] ?? []
if len(group) < support_threshold {
continue
}
const id = "tools-" + pl_short_hash(key)
if pl_is_suppressed(id, state, now) {
continue
}
const proposal = pl_make_tool_proposal(id, key, group, existing[id] ?? {}, now)
if !pl_skill_exists(harness.fs, proposal.name, options) {
proposals = pl_sorted_proposal_insert(proposals, proposal)
}
}
memory_forget(
harness.memory,
pl_namespace(options),
{tag: "pattern_learning:pending"},
pl_memory_options(harness.fs, options),
)
for proposal in proposals {
pl_store_record(
harness,
"pending",
"pending:" + proposal.id,
proposal,
options
+ {
id: pl_unique_record_id(
harness.clock,
harness.random,
"pending_" + proposal.id.replace("-", "_"),
options,
),
},
)
}
return proposals
}
/**
* Return active pending pattern-learning proposals, ordered by support.
*
* @effects: [store.read]
* @errors: []
* @api_stability: experimental
*/
pub fn pattern_learning_pending(harness: Harness, options = nil) -> list {
pattern_learning_ensure_migrated(harness, options)
return pl_record_values(harness.fs, harness.memory, "pending", options).sorted_by(
{ item -> to_string(999999 - (item?.support ?? 0)) + ":" + item.id },
)
}
/**
* Validate and persist the terminal proposal batch from a crystallization
* curator agent. The curator owns semantic synthesis; this boundary owns the
* closed shape, evidence membership, and durable pending-review state.
*
* @effects: [store.read, store.write]
* @errors: [HARN-PATTERN-003]
* @api_stability: experimental
*/
pub fn pattern_learning_record_curated_batch(
harness: Harness,
batch: dict,
evidence: list,
options = nil,
) -> dict {
const raw = batch?.proposals ?? []
if type_of(raw) != "list" || len(raw) > 3 {
throw "HARN-PATTERN-003: curator batch must contain between zero and three proposals"
}
let observations_by_id = {}
let allowed_ids = []
for observation in evidence ?? [] {
const id = pl_text(observation?.id)
if id == "" {
throw "HARN-PATTERN-003: curator evidence entries require id"
}
observations_by_id = observations_by_id + {[id]: observation}
allowed_ids = allowed_ids + [id]
}
const now = pl_now(harness.clock, options)
let accepted = []
let names = []
for proposal in raw {
const normalized = pl_curated_proposal(
harness.agent,
proposal,
observations_by_id,
allowed_ids,
now,
)
if names.contains(normalized.name) {
throw "HARN-PATTERN-003: curator batch contains duplicate name `" + normalized.name + "`"
}
names = names + [normalized.name]
pl_store_record(
harness,
"pending",
"pending:" + normalized.id,
normalized,
options
+ {
id: pl_unique_record_id(
harness.clock,
harness.random,
"pending_" + normalized.id.replace("-", "_"),
options,
),
},
)
accepted = accepted + [normalized]
}
return {
accepted: true,
decision: len(accepted) == 0 ? "no_candidate" : "proposed",
proposal_count: len(accepted),
proposals: accepted,
}
}
/**
* Store one completed-run observation and refresh pending proposals when learning is enabled.
*
* @effects: [store.read, store.write, fs.read]
* @errors: []
* @api_stability: experimental
*/
pub fn pattern_learning_observe(
harness: Harness,
session_id: string,
message: string,
tool_sequence: list = [],
options = nil,
) -> dict {
return try {
pattern_learning_observe_inner(harness, session_id, message, tool_sequence, options)
} catch (e) {
{
observed: false,
enabled: false,
unavailable: true,
error: to_string(e),
observation_count: 0,
pending_count: 0,
proposals: [],
}
}
}
fn pattern_learning_observe_inner(
harness: Harness,
session_id: string,
message: string,
tool_sequence: list = [],
options = nil,
) -> dict {
pattern_learning_ensure_migrated(harness, options)
const state = pattern_learning_state(harness, options)
if !(state?.enabled ?? true) {
return {
observed: false,
enabled: false,
observation_count: len(pattern_learning_observations(harness, options)),
pending_count: len(pattern_learning_pending(harness, options)),
proposals: pattern_learning_pending(harness, options),
}
}
const observation = pl_store_observation(
harness,
pl_observation_from_input(
harness.clock,
harness.random,
{
session_id: session_id,
evidence_kind: "manual",
prompt: message,
tool_sequence: tool_sequence,
observed_at: pl_now(harness.clock, options),
},
options,
),
options,
)
pl_cap_observations(harness, options)
// Observation capture is intentionally semantic-free. The optional legacy
// miner remains available to callers that explicitly request it, but the
// default path waits for the bounded curator agent instead of mistaking
// lexical similarity or an argv sequence for reusable intent.
const proposals = if options?.heuristic_refresh ?? false {
pattern_learning_refresh(harness, options)
} else {
pattern_learning_pending(harness, options)
}
return {
observed: true,
enabled: true,
observation: observation,
observation_count: len(pattern_learning_observations(harness, options)),
pending_count: len(proposals),
proposals: proposals,
}
}
fn pl_score_skill(skill_entry: dict, query_words: list) -> dict {
if len(query_words) == 0 {
return {learned_skill: skill_entry, score: 0.0, reason: "project learned skill"}
}
const haystack = join(
[skill_entry.name, skill_entry.description, skill_entry.when_to_use, skill_entry.body],
" ",
)
const skill_words = pl_significant_words(haystack)
let overlap = []
for word in query_words {
if skill_words.contains(word) && !overlap.contains(word) {
overlap = overlap + [word]
}
}
if len(overlap) == 0 {
return {}
}
return {
learned_skill: skill_entry,
score: (len(overlap) + 0.0) / (max(1, len(query_words)) + 0.0),
reason: "matched " + join(overlap.sorted()[0:min(4, len(overlap))], ", "),
}
}
fn pl_insert_match(matches: list, item: dict) -> list {
if item?.learned_skill == nil {
return matches
}
let out = []
let inserted = false
for existing in matches {
const before = item.score > existing.score
|| (item.score == existing.score
&& item.learned_skill.name
< existing
.learned_skill.name)
if before && !inserted {
out = out + [item]
inserted = true
}
out = out + [existing]
}
if !inserted {
out = out + [item]
}
return out
}
/**
* Rank accepted learned skills against a query string using deterministic lexical overlap.
*
* @effects: [fs.read]
* @errors: []
* @api_stability: experimental
*/
pub fn pattern_learning_skill_matches(
fs: HarnessFs,
query: string,
limit: int = 3,
options = nil,
) -> list {
const query_words = pl_significant_words(query)
let matches = []
for skill_entry in pattern_learning_accepted_skills(fs, options) {
matches = pl_insert_match(matches, pl_score_skill(skill_entry, query_words))
}
return matches[0:min(max(0, limit), len(matches))]
}
/**
* Build the learned-context block and provenance banner for an agent turn.
*
* @effects: [store.read, fs.read]
* @errors: []
* @api_stability: experimental
*/
pub fn pattern_learning_context(
harness: Harness,
session_id: string,
message: string,
limit: int = 3,
options = nil,
) -> dict {
return try {
pattern_learning_context_inner(harness, session_id, message, limit, options)
} catch (e) {
{body: "", matches: [], pending_count: 0, banner: "", unavailable: true, error: to_string(e)}
}
}
fn pattern_learning_context_inner(
harness: Harness,
session_id: string,
message: string,
limit: int = 3,
options = nil,
) -> dict {
const read_options = (options ?? {}) + {skip_migration: true}
const pending = pattern_learning_pending(harness, read_options)
const matches = pattern_learning_skill_matches(harness.fs, message, limit, read_options)
if len(matches) == 0 && len(pending) == 0 {
return {body: "", matches: [], pending_count: 0, banner: ""}
}
let lines = ["<learned_context>"]
if len(matches) > 0 {
const loaded = join(
matches.map(
{ candidate -> candidate.learned_skill.name + " (" + to_string(candidate.score) + ")" },
),
", ",
)
lines = lines
+ [
"<provenance_chip>Loaded learned skills: " + loaded + "</provenance_chip>",
"<learned_skills>",
]
for candidate in matches {
lines = lines
+ [
"- `" + candidate.learned_skill.name + "`: " + candidate.learned_skill.description,
" why: " + candidate.reason,
]
if pl_text(candidate.learned_skill.when_to_use) != "" {
lines = lines + [" when: " + candidate.learned_skill.when_to_use]
}
}
lines = lines + ["</learned_skills>"]
}
if len(pending) > 0 {
const suffix = len(pending) == 1 ? "" : "s"
lines = lines
+ [
"<learning_review>" + to_string(len(pending)) + " proposal" + suffix
+ " pending; run `/learn list` to review.</learning_review>",
]
}
const names = join(matches.map({ candidate -> "`" + candidate.learned_skill.name + "`" }), ", ")
const match_suffix = len(matches) == 1 ? "" : "s"
const banner = if len(matches) > 0 {
"loaded " + to_string(len(matches)) + " learned skill" + match_suffix + ": " + names
} else {
""
}
return {
body: join(lines + ["</learned_context>"], "\n"),
banner: banner,
matches: matches,
pending_count: len(pending),
}
}
fn pl_yaml_scalar(value: string) -> string {
return "\"" + (value ?? "").replace("\\", "\\\\").replace("\"", "\\\"") + "\""
}
fn pl_write_skill(fs: HarnessFs, proposal: dict, options = nil) -> string {
const name = pl_text(proposal?.name)
pl_validate_skill_name(name)
const root = pattern_learning_skill_root(fs, options)
const dir = path_join(root, name)
const path = path_join(dir, "SKILL.md")
fs.mkdir(dir)
const body = join(
[
"---",
"name: " + name,
"description: " + pl_yaml_scalar(proposal.description),
"when-to-use: " + pl_yaml_scalar(proposal.when_to_use),
"user-invocable: true",
"disable-model-invocation: false",
"allowed-tools: []",
"category: Command",
"tags: [learned, " + proposal.source.replace("_", "-") + "]",
"---",
"",
to_string(proposal.body ?? "").trim(),
"",
],
"\n",
)
fs.write_text(path, body)
return path
}
fn pl_write_candidate(
capabilities: {clock: HarnessClock, fs: HarnessFs},
proposal: dict,
options = nil,
) -> dict {
const project = pl_project_root(capabilities.fs, options)
const candidate_dir = path_join(
path_join(project, ".burin", "crystallization"),
pl_text(proposal.id),
)
const artifact = proposal?.artifact ?? {}
const entrypoint = pl_text(artifact?.entrypoint)
capabilities.fs.mkdir(candidate_dir)
capabilities.fs.write_text(path_join(candidate_dir, entrypoint), to_string(artifact.source))
capabilities.fs.write_text(path_join(candidate_dir, "test.harn"), to_string(artifact.test_source))
// Keep the human guidance beside the executable artifact. It is deliberately
// not installed yet: acceptance approves staging; compiler + test gates own
// activation so an agent-authored source blob can never become callable by
// merely crossing the model boundary.
const skill_path = pl_write_skill(
capabilities.fs,
proposal,
options + {project_root: candidate_dir},
)
const manifest = proposal
+ {
schema: "harn.crystallization.authored-candidate.v1",
status: "staged",
candidate_dir: candidate_dir,
staged_at: pl_now(capabilities.clock, options),
}
capabilities.fs.write_text(path_join(candidate_dir, "proposal.json"), json_stringify(manifest))
return {
staged: true,
candidate_dir: candidate_dir,
entrypoint: path_join(candidate_dir, entrypoint),
test_path: path_join(candidate_dir, "test.harn"),
skill_path: skill_path,
}
}
fn pl_find_pending(harness: Harness, id: string, options = nil) -> dict {
for proposal in pattern_learning_pending(harness, options) {
if proposal.id == id {
return proposal
}
}
for proposal in pattern_learning_refresh(harness, options) {
if proposal.id == id {
return proposal
}
}
return {}
}
/**
* Promote a pending proposal into a project skill and remove it from review.
*
* @effects: [store.read, store.write, fs.write]
* @errors: [HARN-PATTERN-002]
* @api_stability: experimental
*/
pub fn pattern_learning_accept(harness: Harness, id: string, options = nil) -> dict {
const proposal = pl_find_pending(harness, pl_text(id), options)
if proposal?.id == nil {
return {accepted: false, is_error: true, error: "proposal not found: " + pl_text(id)}
}
const executable = pl_text(proposal?.kind) != "" && pl_text(proposal?.kind) != "skill"
const staged = if executable {
pl_write_candidate({clock: harness.clock, fs: harness.fs}, proposal, options)
} else {
{staged: false, skill_path: pl_write_skill(harness.fs, proposal, options)}
}
memory_forget(
harness.memory,
pl_namespace(options),
{key: "pending:" + proposal.id},
pl_memory_options(harness.fs, options),
)
return {accepted: true, proposal: proposal} + staged
}
fn pl_add_days_iso(now: string, days: int) -> string {
return date_to_zone(date_add(date_parse(now), duration_days(days)), "UTC")
}
/**
* Reject a pending proposal and suppress regeneration for the configured interval.
*
* @effects: [store.read, store.write]
* @errors: []
* @api_stability: experimental
*/
pub fn pattern_learning_reject(harness: Harness, id: string, options = nil) -> dict {
const clean = pl_text(id)
const proposal = pl_find_pending(harness, clean, options)
if proposal?.id == nil {
return {rejected: false, is_error: true, error: "proposal not found: " + clean}
}
memory_forget(
harness.memory,
pl_namespace(options),
{key: "pending:" + clean},
pl_memory_options(harness.fs, options),
)
const state = pattern_learning_state(harness, options)
const suppressed = (state?.suppressed_until ?? {})
+ {
[clean]: pl_add_days_iso(
pl_now(harness.clock, options),
options?.suppression_days ?? DEFAULT_SUPPRESSION_DAYS,
),
}
pl_replace_state(harness, state + {suppressed_until: suppressed}, options)
return {rejected: true}
}
/**
* Return enablement, observation, pending, cap, and skill-root status.
*
* @effects: [store.read]
* @errors: []
* @api_stability: experimental
*/
pub fn pattern_learning_status(harness: Harness, options = nil) -> dict {
const state = pattern_learning_state(harness, options)
const pending = pattern_learning_pending(harness, options)
const observations = pattern_learning_observations(harness, options)
const curator_evidence = pattern_learning_curator_evidence(harness, options)
return {
enabled: state?.enabled ?? true,
observation_count: len(curator_evidence),
untrusted_observation_count: len(observations) - len(curator_evidence),
pending_count: len(pending),
observation_cap: OBSERVATION_CAP,
skill_root: pattern_learning_skill_root(harness.fs, options),
}
}
fn pl_migrate_observations(harness: Harness, options = nil) -> nil {
for legacy in pl_project_collection(
pl_read_jsonl_payloads(harness.fs, pl_observation_file(harness.fs, options)),
) {
const converted = try {
pl_observation_from_legacy(harness.clock, harness.random, legacy, options)
}
if is_ok(converted) {
pl_store_observation(harness, unwrap(converted), options)
}
}
}
fn pl_migrate_pending(harness: Harness, options = nil) -> nil {
const pending = pl_project_value(
pl_read_jsonl_payloads(harness.fs, pl_pending_file(harness.fs, options)),
{generatedAt: "", proposals: []},
)
for proposal in pending?.proposals ?? [] {
const normalized = {
id: pl_text(proposal?.id),
kind: pl_text(proposal?.kind) == "" ? "skill" : pl_text(proposal.kind),
source: pl_text(proposal?.source),
name: pl_text(proposal?.name),
title: pl_text(proposal?.title),
description: pl_text(proposal?.description),
when_to_use: pl_text(proposal?.whenToUse ?? proposal?.when_to_use),
body: to_string(proposal?.body ?? ""),
support: proposal?.support ?? 0,
sample_prompts: proposal?.samplePrompts ?? proposal?.sample_prompts ?? [],
tool_sequence: proposal?.toolSequence ?? proposal?.tool_sequence ?? [],
created_at: pl_text(
proposal?.createdAt ?? proposal?.created_at ?? pl_now(harness.clock, options),
),
last_seen_at: pl_text(
proposal?.lastSeenAt ?? proposal?.last_seen_at ?? pl_now(harness.clock, options),
),
}
if normalized.id != "" {
pl_store_record(
harness,
"pending",
"pending:" + normalized.id,
normalized,
options
+ {
id: pl_unique_record_id(
harness.clock,
harness.random,
"pending_" + normalized.id.replace("-", "_"),
options,
),
},
)
}
}
}
fn pl_migrate_state(harness: Harness, options = nil) -> nil {
const state = pl_project_value(
pl_read_jsonl_payloads(harness.fs, pl_state_file(harness.fs, options)),
{enabled: true, suppressedUntil: {}},
)
pl_replace_state(
harness,
{
enabled: state?.enabled ?? true,
suppressed_until: state?.suppressedUntil ?? state?.suppressed_until ?? {},
},
options,
)
}
/**
* Lazily import legacy Burin session-store pattern-learning records into memory.
*
* @effects: [store.read, store.write, fs.read, fs.write]
* @errors: []
* @api_stability: experimental
*/
pub fn pattern_learning_ensure_migrated(harness: Harness, options = nil) -> dict {
if options?.skip_migration ?? false {
return {migrated: false, skipped: true}
}
const marker = pl_migration_marker(harness.fs, options)
if harness.fs.exists(marker) {
return {migrated: false, already_done: true}
}
pl_migrate_observations(harness, options)
pl_migrate_pending(harness, options)
pl_migrate_state(harness, options)
write_json(
harness.fs,
marker,
{schema: PATTERN_SCHEMA, migrated_at: pl_now(harness.clock, options)},
{pretty: true},
)
return {migrated: true}
}