harn-stdlib 0.9.13

Embedded Harn standard library source catalog
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
import {
  __completion_gate_apply_budget,
  __completion_gate_classify_writes,
  __completion_gate_combine_verify,
  __completion_gate_ladder,
  __judge_apply_llm_overrides,
  __judge_classify_verdict,
} from "std/agent/judge_internals"
import { agent_typed_output_checkpoint } from "std/agent/primitives"
import { completion_judge_user_prompt } from "std/agent/prompts"
import {
  agent_emit_event,
  agent_session_inject_feedback,
  agent_session_messages,
} from "std/agent/state"

// Conservative default cap on how many times the completion-judge LLM may
// veto a proposed completion within one session. Each veto is a paid model
// call plus an injected feedback message, so a weak model that never satisfies
// the judge can otherwise burn calls up to `max_verify_attempts` (default 20)
// with no structured signal. Callers raise it via
// `verify_completion_judge.max_invocations` (or `max_feedback`), or disable the
// cap entirely with `0`.
const __VERIFY_COMPLETION_JUDGE_DEFAULT_CAP = 5

fn __unique_names(names) {
  var unique = []
  for name in names {
    if name != "" && !contains(unique, name) {
      unique = unique.push(name)
    }
  }
  return unique
}

fn __session_tool_names(messages) {
  var names = []
  for message in messages {
    if message?.role == "tool" {
      names = names.push(message?.name ?? "")
    }
  }
  return __unique_names(names)
}

fn __judge_payload(session, opts, stop_reason, text, iteration) {
  let messages = agent_session_messages(session.session_id)
  let tool_names = __session_tool_names(messages)
  return {
    session_id: session.session_id,
    task: session?.task ?? "",
    stop_reason: stop_reason,
    text: text,
    visible_text: text,
    last_text: text,
    transcript: json_stringify(messages),
    all_tools_used: join(tool_names, ", "),
    successful_tools_used: join(tool_names, ", "),
    iteration: iteration,
  }
}

fn __judge_invoke_closure(verify_completion, payload) {
  let result = verify_completion(payload)
  if result == nil || result == "" {
    return {vetoed: false, confirm: true, trigger: "verify_completion"}
  }
  if type_of(result) == "bool" {
    return {vetoed: !result, confirm: result, trigger: "verify_completion"}
  }
  if type_of(result) == "string" {
    return {vetoed: true, feedback: result, confirm: false, trigger: "verify_completion"}
  }
  if type_of(result) == "dict" {
    let confirm = result?.confirm ?? false
    let message = result?.message ?? result?.feedback
    return {
      vetoed: !confirm,
      feedback: message,
      confirm: confirm,
      reason: result?.reason,
      converted_from: result?.converted_from,
      trigger: result?.trigger ?? "verify_completion",
    }
  }
  return {vetoed: false, confirm: true, trigger: "verify_completion"}
}

fn __judge_invoke_structured(harness: Harness, judge_cfg, opts, payload) {
  let system = judge_cfg?.system ?? ""
  let user = completion_judge_user_prompt(payload)
  let schema = {
    type: "object",
    properties: {
      verdict: {type: "string", description: "Use `done` or `continue`."},
      reasoning: {type: "string"},
      next_step: {type: "string"},
    },
    required: ["verdict"],
  }
  let base = opts?.llm_options ?? {}
  let llm_opts = __judge_apply_llm_overrides(
    base
      + {
      model: judge_cfg?.model ?? opts?.model,
      provider: judge_cfg?.provider ?? opts?.provider,
      output_schema: schema,
      session_id: payload.session_id,
      system: system,
    },
    judge_cfg,
  )
  let started = harness.clock.monotonic_ms()
  let checkpoint = agent_typed_output_checkpoint("agent.completion_judge", user, schema, llm_opts)
  let duration_ms = harness.clock.monotonic_ms() - started
  if !checkpoint.ok {
    if judge_cfg?.fail_open_on_error ?? judge_cfg?.fail_open ?? false {
      return {
        vetoed: false,
        verdict: "done",
        reasoning: checkpoint.error,
        next_step: "",
        judge_duration_ms: duration_ms,
        typed_checkpoint: checkpoint,
      }
    }
    return {
      vetoed: true,
      feedback: checkpoint.error,
      verdict: "continue",
      reasoning: checkpoint.error,
      next_step: checkpoint.error,
      judge_duration_ms: duration_ms,
      typed_checkpoint: checkpoint,
    }
  }
  let result = checkpoint.data
  let {reasoning = "", next_step = ""} = result ?? {}
  // The done-judge prompt only ever asks for `done` or `continue`. A reflexive
  // `"yes"` from a cheap model usually means "yes, keep going" (continue), and
  // a bare `"true"` is just as ambiguous — classifying either as DONE
  // terminates incomplete work. Both are dropped from the allow-list.
  let done_verdicts = ["done", "pass", "passed", "safe", "yield", "yield_to_user", "complete", "completed"]
  let feedback_default = judge_cfg?.feedback_fallback ?? ""
  let outcome = __judge_classify_verdict(
    result?.verdict ?? "continue",
    done_verdicts,
    [next_step, reasoning],
    feedback_default,
  )
  return outcome
    + {
    reasoning: reasoning,
    next_step: next_step,
    judge_duration_ms: duration_ms,
    typed_checkpoint: checkpoint,
  }
}

/**
 * Resolve the per-session veto cap for the completion judge. Returns nil when
 * the cap is disabled (`max_invocations`/`max_feedback` set to 0), otherwise a
 * positive integer ceiling on judge invocations.
 */
fn __verify_completion_judge_cap(judge_cfg) {
  let configured = if type_of(judge_cfg) == "dict" {
    judge_cfg?.max_invocations ?? judge_cfg?.max_feedback
  } else {
    nil
  }
  let cap = configured ?? __VERIFY_COMPLETION_JUDGE_DEFAULT_CAP
  if cap <= 0 {
    return nil
  }
  return cap
}

/**
 * Resolve the optional top-level cap for `done_judge`. Unlike
 * `done_judge.cadence.max_invocations`, this is a terminal veto-loop cap: the
 * judge may fire up to the cap, then the loop finalizes instead of silently
 * continuing until the iteration budget expires.
 */
fn __done_judge_cap(judge_cfg) {
  if type_of(judge_cfg) != "dict" {
    return nil
  }
  let cap = judge_cfg?.max_invocations ?? judge_cfg?.max_feedback
  if cap == nil || cap <= 0 {
    return nil
  }
  return cap
}

/**
 * agent_verify_completion_judge_cap.
 *
 * Resolved completion-judge veto cap for a `verify_completion_judge` config,
 * for surfacing in run records. Returns nil when the cap is disabled.
 *
 * @effects: []
 * @errors: []
 * @api_stability: experimental
 * @example: agent_verify_completion_judge_cap(opts?.verify_completion_judge)
 */
pub fn agent_verify_completion_judge_cap(judge_cfg) {
  return __verify_completion_judge_cap(judge_cfg)
}

/**
 * agent_done_judge_cap.
 *
 * Resolved terminal veto cap for a `done_judge` config. Returns nil when the
 * cap is not configured or is disabled with 0.
 *
 * @effects: []
 * @errors: []
 * @api_stability: experimental
 * @example: agent_done_judge_cap(opts?.done_judge)
 */
pub fn agent_done_judge_cap(judge_cfg) {
  return __done_judge_cap(judge_cfg)
}

fn __emit_judge_decision(session_id, iteration, verdict) {
  agent_emit_event(
    session_id,
    "judge_decision",
    {
      iteration: iteration,
      verdict: verdict?.verdict
        ?? if verdict.vetoed {
        "continue"
      } else {
        "done"
      },
      reasoning: verdict?.reasoning ?? "",
      next_step: verdict?.next_step ?? "",
      judge_duration_ms: verdict?.judge_duration_ms ?? 0,
      trigger: verdict?.trigger ?? nil,
      reason: verdict?.reason ?? nil,
      confirm: verdict?.confirm ?? !verdict.vetoed,
      converted_from: verdict?.converted_from ?? nil,
    },
  )
}

/**
 * agent_verify_or_continue.
 *
 * @effects: []
 * @errors: []
 * @api_stability: experimental
 */
pub fn agent_verify_or_continue(session, opts, stop_reason, text, iteration = 0) {
  let payload = __judge_payload(session, opts, stop_reason, text, iteration)
  var verdict = {vetoed: false}
  if opts?.verify_completion != nil {
    verdict = __judge_invoke_closure(opts.verify_completion, payload)
    __emit_judge_decision(session.session_id, iteration, verdict)
  }
  var completion_judge_invoked = false
  var completion_judge_cap_reached = false
  let prior_judge_invocations = opts?._verify_completion_judge_invocations ?? 0
  if !verdict.vetoed && opts?.verify_completion_judge != nil {
    let cap = __verify_completion_judge_cap(opts.verify_completion_judge)
    if cap != nil && prior_judge_invocations >= cap {
      // Cap reached: stop firing the judge (and injecting its feedback). The
      // loop surfaces this as stop_reason `completion_judge_cap_reached` plus a
      // structured `completion_judge` block in the run record. The prior
      // `judge_decision` events already record each veto for loop counting, so
      // no extra event type is needed here.
      completion_judge_cap_reached = true
    } else {
      verdict = __judge_invoke_structured(harness, opts.verify_completion_judge, opts, payload)
      completion_judge_invoked = true
      __emit_judge_decision(session.session_id, iteration, verdict)
    }
  }
  let done_judge_due = opts?._done_judge_due ?? true
  let done_judge_applies = opts?.done_judge != nil
    && (stop_reason == "sentinel" || stop_reason == "natural" || stop_reason == "stalled")
    && done_judge_due
  var done_judge_cap_reached = false
  let prior_done_judge_invocations = opts?._done_judge_invocations ?? 0
  if !verdict.vetoed && done_judge_applies {
    let cap = __done_judge_cap(opts.done_judge)
    if cap != nil && prior_done_judge_invocations >= cap {
      done_judge_cap_reached = true
    } else {
      verdict = __judge_invoke_structured(harness, opts.done_judge, opts, payload)
      verdict = verdict + {done_judge_invoked: true, trigger: opts?._done_judge_trigger ?? nil}
      __emit_judge_decision(session.session_id, iteration, verdict)
    }
  }
  if verdict.vetoed && verdict?.feedback != nil && verdict.feedback != "" {
    agent_session_inject_feedback(session.session_id, "verify_completion", verdict.feedback)
  }
  return verdict
    + {
    verify_completion_judge_invoked: completion_judge_invoked,
    verify_completion_judge_cap_reached: completion_judge_cap_reached,
    done_judge_cap_reached: done_judge_cap_reached,
  }
}

// -------------------------------------------------------------------------------------------------

// completion_gate — a configured done-time gate built by composing the existing
// `verify_completion` (deterministic) + `verify_completion_judge` / `done_judge`
// (bounded LLM) seams. It generalizes burin-code's completion-verification
// machinery (`lib/runtime/completion-judge.harn` — the veto arithmetic, the
// source-vs-cosmetic write gate ⛔#5, the bounded judge budget) while keeping
// every DOMAIN FACT (write classification, verifier verdict) a host CALLBACK.
// The deterministic ladder rides `verify_completion` because that is the seam the
// loop consults for a closure at done-time; the optional LLM judge rides the
// existing capped `verify_completion_judge`/`done_judge` seam. No new loop seam
// is added. Never keys any decision on a done-sentinel string (ledger ⛔#3).

// -------------------------------------------------------------------------------------------------

/**
 * A host write-classification label. `"source"` and `"cosmetic"` are load-bearing; other kinds are treated as non-source.
 */
pub type WriteKind = string

/** One host-supplied write fact. */
pub type CompletionWriteFact = {path?: string, diff?: string, kind?: WriteKind}

/** A host-supplied verifier verdict. `findings` is optional red-detail text. */
pub type CompletionVerifyVerdict = {ok?: bool, findings?: string}

/**
 * Facts a host supplies (via the `facts` callback) for one gate evaluation.
 * All fields optional: absent write facts disable the evidence gate for that
 * turn (never a fabricated pass), absent `verify` falls back to the
 * `verify_command` callback. Rust twin: none — stdlib-owned.
 */
pub type CompletionFacts = {
  source_write_count?: int,
  cosmetic_write_count?: int,
  writes?: list<CompletionWriteFact>,
  verify?: CompletionVerifyVerdict | list<CompletionVerifyVerdict>,
  requires_write?: bool,
}

/**
 * Options for `completion_gate`. Domain facts enter as callbacks; policy knobs
 * are plain data. Rust twin: none — stdlib-owned.
 *
 * Host-fact callbacks (all optional):
 * - `facts(ctx) -> CompletionFacts` — the primary fact supplier. `ctx` carries
 *   `{session_id, task, stop_reason, text, messages}`.
 * - `classify_write(path, diff?) -> WriteKind` — classifies a single write when
 *   `facts` returns a `writes` list without counts.
 * - `verify_command() -> CompletionVerifyVerdict` — runs the verifier oracle when
 *   `facts` does not carry a `verify` verdict.
 *
 * Policy knobs:
 * - `require_source_write` (default true) — enforce the evidence gate ⛔#5.
 * - `requires_write` — override the per-task "needs a source change" fact.
 * - `max_vetoes` (default 3, burin `COMPLETION_GATE_MAX_VETOES`) — per-session
 *   soft-veto budget; 0 disables.
 * - `veto_combine(verdicts) -> CompletionVerifyVerdict` — override the default
 *   AND-of-oracles arithmetic for combining multiple verifier verdicts.
 * - `judge` (default off) — attach a bounded LLM completion judge (`true` or a
 *   JudgeConfig-shaped dict); defaults its cap to burin's 5.
 * - `judge_seam` (default `"verify_completion_judge"`) — which capped LLM seam
 *   the judge rides (`"verify_completion_judge"` or `"done_judge"`).
 */
pub type CompletionGateOptions = {
  facts?: any,
  classify_write?: any,
  verify_command?: any,
  require_source_write?: bool,
  requires_write?: bool,
  max_vetoes?: int,
  veto_combine?: any,
  judge?: any,
  judge_seam?: string,
}

/** Session-store key for the completion-gate per-session veto counter. */
fn __completion_gate_veto_key(session_id) {
  return "harn.completion_gate." + session_id + ".veto_count"
}

/** Per-session vetoes charged so far (0 without a session id). */
fn __completion_gate_vetoes_used(session_id) {
  if session_id == "" {
    return 0
  }
  return to_int(store_get(__completion_gate_veto_key(session_id)) ?? 0) ?? 0
}

/**
 * Derive `{source_write_count?, cosmetic_write_count?, source_known}` from host
 * facts. `source_known` is false only when the host supplied neither counts nor a
 * `writes` list — in which case the evidence gate abstains for that turn.
 */
fn __completion_gate_write_counts(raw, classify_write) {
  if raw?.source_write_count != nil {
    return {
      source_write_count: to_int(raw.source_write_count) ?? 0,
      cosmetic_write_count: to_int(raw?.cosmetic_write_count ?? 0) ?? 0,
      source_known: true,
    }
  }
  if raw?.writes != nil {
    let counts = __completion_gate_classify_writes(raw.writes, classify_write)
    return counts + {source_known: true}
  }
  return {source_write_count: 0, cosmetic_write_count: 0, source_known: false}
}

/**
 * The deterministic gate closure body: turn a `__judge_payload` payload into a
 * `verify_completion` verdict (`{confirm, message?, reason}` — nil-safe for the
 * `__judge_invoke_closure` contract). Degrades to judge-only (abstain), surfacing
 * the degraded mode via the bundle's `_completion_gate.facts_available = false`
 * flag and a `facts_unavailable` verdict reason, when the host supplied no facts
 * at all (never a silent fabricated pass).
 */
fn __completion_gate_evaluate(payload, opts, facts_available) {
  let session_id = to_string(payload?.session_id ?? "")
  let classify_write = opts?.classify_write
  let verify_command = opts?.verify_command
  let facts = opts?.facts
  let max_vetoes = to_int(opts?.max_vetoes ?? 3) ?? 3
  if !facts_available {
    // No host facts: the deterministic gate cannot assert anything, so it
    // abstains (allow). Any configured LLM judge still runs — judge-only mode.
    // The degraded mode is NOT silent: the bundle carries
    // `_completion_gate.facts_available = false` and the verdict names the
    // `facts_unavailable` reason, so a harness never mistakes it for a real pass.
    return {confirm: true, reason: "facts_unavailable"}
  }
  let messages = json_parse(to_string(payload?.transcript ?? "[]")) ?? []
  let ctx = {
    session_id: session_id,
    task: to_string(payload?.task ?? ""),
    stop_reason: to_string(payload?.stop_reason ?? ""),
    text: to_string(payload?.text ?? ""),
    messages: messages,
  }
  let raw = if facts != nil {
    facts(ctx) ?? {}
  } else {
    {}
  }
  let counts = __completion_gate_write_counts(raw, classify_write)
  let verify_raw = if raw?.verify != nil {
    raw.verify
  } else if verify_command != nil {
    verify_command()
  } else {
    nil
  }
  let verify = __completion_gate_combine_verify(verify_raw, opts?.veto_combine)
  let requires_write = if opts?.requires_write != nil {
    opts.requires_write
  } else if raw?.requires_write != nil {
    raw.requires_write
  } else {
    opts?.require_source_write ?? true
  }
  let derived = {
    source_known: counts.source_known,
    source_write_count: counts.source_write_count,
    cosmetic_write_count: counts.cosmetic_write_count,
    requires_write: requires_write,
    verify: verify,
    oracle_expected: verify_command != nil || raw?.verify != nil,
  }
  let verdict = __completion_gate_ladder(derived)
  let vetoes_used = __completion_gate_vetoes_used(session_id)
  let budgeted = __completion_gate_apply_budget(verdict, vetoes_used, max_vetoes)
  if budgeted.charge && session_id != "" {
    store_set(__completion_gate_veto_key(session_id), vetoes_used + 1)
  }
  let resolved = budgeted.verdict
  if resolved?.ok ?? false {
    return {confirm: true, reason: resolved?.reason ?? "", converted_from: resolved?.converted_from ?? nil}
  }
  return {confirm: false, message: to_string(resolved?.feedback ?? ""), reason: resolved?.reason ?? ""}
}

/**
 * agent_completion_gate.
 *
 * Build a completion-gate option bundle to spread into `agent_loop` options. The
 * returned dict configures the done-time gate: a deterministic `verify_completion`
 * closure implementing the ported veto arithmetic + source-write evidence
 * requirement + per-session veto budget, plus (when `judge` is set) a bounded LLM
 * judge on the existing capped seam. Compose it with base options:
 *
 * ```harn,ignore
 * agent_loop(task, system, base_opts + agent_completion_gate({
 *   facts: fn(ctx) { return host_completion_facts(ctx.session_id) },
 *   verify_command: fn() { return host_run_verify() },
 * }))
 * ```
 *
 * With no host-fact callbacks the gate degrades to judge-only mode and surfaces
 * the degraded state on the returned bundle (`_completion_gate.facts_available =
 * false`) rather than fabricating a pass.
 *
 * @effects: []
 * @errors: []
 * @api_stability: experimental
 * @example: agent_completion_gate({facts: host_facts, verify_command: host_verify})
 */
pub fn agent_completion_gate(options: CompletionGateOptions = {}) {
  let opts = options ?? {}
  let facts_available = opts.facts != nil || opts.verify_command != nil || opts.classify_write != nil
  let gate = fn(payload) { return __completion_gate_evaluate(payload, opts, facts_available) }
  var out = {
    verify_completion: gate,
    _completion_gate: {
      facts_available: facts_available,
      max_vetoes: to_int(opts.max_vetoes ?? 3) ?? 3,
      require_source_write: opts.require_source_write ?? true,
    },
  }
  let judge = opts.judge
  if judge != nil && judge {
    let judge_cfg = if type_of(judge) == "dict" {
      judge
    } else {
      {}
    }
    let seam = opts.judge_seam ?? "verify_completion_judge"
    let cap = judge_cfg?.max_invocations ?? judge_cfg?.max_feedback ?? __VERIFY_COMPLETION_JUDGE_DEFAULT_CAP
    out = out + {[seam]: judge_cfg + {max_invocations: cap}}
  }
  return out
}