harn-stdlib 0.10.125

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
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
import { agent_emit_event } from "std/agent/state"

type AgentLoopCommandAction = "none" | "extend" | "stop"

type AgentLoopCommand = {
  action: AgentLoopCommandAction,
  by: int,
  until: int,
  reason: string,
  status: string,
}

pub type AgentLoopBudget = {
  mode: string,
  initial: int,
  max: int,
  extend_by: int,
  progress_window: int,
  expose_decisions: bool,
  wall_clock_ms: int?,
  total_cost_usd: float?,
  consecutive_failures: dict?,
}

pub type AgentLoopBudgetDecision = {
  iteration: int,
  action: string,
  old_limit: int,
  new_limit: int,
  reason: string,
  status: string,
}

type AgentLoopCommandApplication = {
  stop: bool,
  final_status: string,
  stop_reason: string?,
  current_max: int,
  extensions_used: int,
  decisions: list<AgentLoopBudgetDecision>,
}

type AgentLoopState = {
  iteration: int,
  budget: dict,
  turn: dict,
  session: dict,
  completion: dict,
  progress: dict,
}

fn __agent_loop_state(args: dict) -> AgentLoopState {
  const current_limit = args.current_limit
  const iteration = args.iteration
  const remaining = if iteration >= current_limit {
    0
  } else {
    current_limit - iteration
  }
  const missing = args.missing_required_tools
  return {
    iteration: iteration,
    budget: {
      current_limit: current_limit,
      max: args.budget_max,
      remaining: remaining,
      extension_count: args.extensions_used,
      wall_clock_ms: args.wall_clock_ms ?? 0,
      wall_clock_limit_ms: args.wall_clock_limit_ms,
      cost_usd: args.cost_usd ?? 0.0,
      total_cost_limit_usd: args.total_cost_limit_usd,
      consecutive_failures: args.consecutive_failures ?? 0,
      consecutive_failure_limit: args.consecutive_failure_limit,
    },
    turn: {
      tool_call_count: args.turn_tool_count,
      successful_tool_names: args.turn_successful,
      rejected_tool_names: args.turn_rejected,
      text_chars: args.turn_text_chars,
      native_fallback_used: args.turn_native_fallback_used,
    },
    session: {
      successful_tool_names: args.session_successful,
      rejected_tool_names: args.session_rejected,
      required_tools_satisfied: len(missing) == 0,
      required_tools_missing: missing,
    },
    completion: {
      proposed: args.completion_proposed,
      vetoed: args.completion_vetoed,
      verdict: args.completion_verdict,
      feedback: args.completion_feedback,
    },
    progress: {
      changed: args.progress_changed,
      summary: args.progress_summary,
      no_net_advance: args.progress_no_net_advance ?? false,
      no_information_gain: args.progress_no_information_gain ?? false,
      turns_since_progressing: args.progress_turns_since_progressing ?? 0,
    },
  }
}

/**
 * Single source of truth for "the agent is still advancing the task."
 * `progress.changed` (computed once in `agent_loop_snapshot_state`) covers tool
 * calls, successful tool results, AND visible text — so a turn that is pure
 * planning/narration toward the next edit (no tool call *this* turn) is still
 * forward progress. This is deliberately NOT re-gated on a tool call here: an
 * earlier inline `progress.changed && tool_call_count > 0` denied the extension
 * whenever the budget boundary happened to land on a planning turn, cutting a
 * productive multi-file refactor off mid-work (a methodical model editing across
 * ~6 files stopped at its initial cap because one interleaved planning turn was
 * treated as "no progress"). Degenerate narration that never acts is bounded
 * independently by the iteration max and the stall detector.
 *
 * Structural outcome evidence can veto activity credit. `progress.changed` is
 * an ACTIVITY signal — it fires merely because the model issued tool calls —
 * which makes a thrashing run
 * (lots of edits/verifies, the SAME compile/test failure recurring turn after
 * turn, no error draining toward a green build) read as "progressing" every
 * turn and lets the loop extend its own budget without bound (observed
 * 24->32->...->72 on a run that should have been cut). `no_net_advance` is set
 * ONLY on the verify-bearing failing path when the verification OUTCOME is not
 * advancing (the same failure signature has recurred past a threshold, i.e. the
 * stall detector's same-diagnostic streak). It is the runtime-side twin of the
 * source policy's
 * no-net-progress ripcord. Crucially it is OUTCOME-AWARE, not a second activity
 * notion: a productive edit changes the error signature (streak resets => still
 * progress) or a test passes (failure model clears => still progress), and
 * read-only / explore turns with no failing verification never set it.
 * `progress.no_information_gain` covers that disjoint path: it is set only
 * when every completed, explicitly read-only observation in the turn is an
 * exact `(tool, args, result)` recurrence. A changed or mixed novel result
 * leaves it false. These typed outcome facts meet here so extension policy has
 * one definition of progress.
 */
fn agent_loop_is_progressing(state: AgentLoopState) -> bool {
  return state.progress.changed
    && !(state.progress?.no_net_advance ?? false)
    && !(state.progress?.no_information_gain ?? false)
}

/**
 * Advance the extension window counter: how many turns have elapsed since the
 * last turn that satisfied `agent_loop_is_progressing`. Zero means the turn
 * just observed satisfied it.
 *
 * This exists so the extension rule reads a WINDOW of recent turns rather than
 * the single turn the budget boundary happens to land on. It deliberately
 * re-uses `agent_loop_is_progressing` instead of recomputing the three progress
 * facts, so there remains exactly one definition of "advancing the task".
 */
fn __agent_loop_advance_progress_window(state: AgentLoopState, previous: int) -> AgentLoopState {
  const count = if agent_loop_is_progressing(state) {
    0
  } else {
    previous + 1
  }
  return state + {progress: state.progress + {turns_since_progressing: count}}
}

/**
 * How far back the extension rule looks for a progressing turn, in turns.
 * Defaults to `budget.extend_by` — the loop is willing to look back over as
 * many turns as one extension would buy — and `iteration_budget.progress_window`
 * overrides it. Falls back to 1 (the boundary turn alone, i.e. the pre-window
 * behavior) when neither is positive, so a fixed-mode or misconfigured budget
 * cannot silently widen the window.
 */
fn __agent_loop_progress_window(budget: AgentLoopBudget) -> int {
  const configured = budget.progress_window
  const window = if configured > 0 {
    configured
  } else {
    budget.extend_by
  }
  if window > 0 {
    return window
  }
  return 1
}

/**
 * Did the agent progress anywhere inside the recent window?
 *
 * Deciding extension from the boundary turn ALONE reads one sample of a noisy
 * signal: a run that had made nine edits and one failed verification, then
 * spent its last turns reading files back to repair, was stopped at exactly its
 * initial cap because that one boundary turn was suppressed by
 * `no_information_gain`. The repair was in flight and the evidence for it was
 * three turns old.
 *
 * The window does not weaken the thrash bound that
 * `no_net_advance`/`no_information_gain` exist to enforce. A run that fails the
 * predicate on EVERY turn drives the counter monotonically upward, so each
 * granted extension moves the next boundary `extend_by` turns further into a
 * counter that has grown by the same amount. At the default window
 * (`progress_window == extend_by`) that means a sustained thrash buys at most
 * one extension and then stops; a deliberately wider window buys proportionally
 * more, never unbounded. `budget.max` remains the outer bound in every case
 * (`__agent_apply_extension` clamps to it).
 */
fn __agent_loop_progressed_within_window(state: AgentLoopState, budget: AgentLoopBudget) -> bool {
  if agent_loop_is_progressing(state) {
    return true
  }
  const since = state.progress?.turns_since_progressing ?? 0
  // A state whose own turn is NOT progressing but whose counter still reads 0
  // has never been advanced — a hand-built state, or a caller that did not
  // thread the count through `agent_loop_snapshot_state`. Absence of window
  // evidence must not read as the strongest possible evidence, so fall back to
  // the boundary-turn-only answer instead of extending on a missing field.
  if since <= 0 {
    return false
  }
  return since < __agent_loop_progress_window(budget)
}

/**
 * Ordered extension policy for the default loop control: each entry pairs a
 * named, eagerly-evaluated condition with the reason recorded on the resulting
 * budget decision, and the first satisfied entry wins. Expressing the policy as
 * an explicit table — rather than a chain of inline compound `if`s — keeps every
 * extension trigger auditable in one place and forces each signal through a
 * single definition (there is nowhere to silently AND an extra condition onto a
 * rule, which is exactly the class of bug this replaced).
 */
fn __agent_loop_extension_rules(state: AgentLoopState, budget: AgentLoopBudget) {
  return [
    {when: state.completion.vetoed, reason: "completion gate vetoed"},
    {when: !state.session.required_tools_satisfied, reason: "required tools missing"},
    {when: __agent_loop_progressed_within_window(state, budget), reason: "progress within window"},
  ]
}

fn __agent_default_loop_control(state: AgentLoopState, budget: AgentLoopBudget) {
  // Only decide at the budget boundary; below it, keep looping untouched.
  if state.budget.remaining > 0 {
    return nil
  }
  for rule in __agent_loop_extension_rules(state, budget) {
    if rule.when {
      return {action: "extend", reason: rule.reason}
    }
  }
  // No extension rule matched — let the loop stop at the current cap.
  return nil
}

fn __agent_interpret_loop_command(command: any) -> AgentLoopCommand {
  if command == nil {
    return {action: "none", by: 0, until: 0, reason: "", status: ""}
  }
  if type_of(command) != "dict" {
    throw "agent_loop: loop_control must return nil or a dict; got " + type_of(command)
  }
  const action = command?.action ?? "none"
  if action != "none" && action != "extend" && action != "stop" {
    throw "agent_loop: loop_control action must be \"none\", \"extend\", or \"stop\"; got "
      + to_string(action)
  }
  const by = command?.by ?? 0
  if type_of(by) != "int" {
    throw "agent_loop: loop_control `by` must be an integer; got " + type_of(by)
  }
  const until = command?.until ?? 0
  if type_of(until) != "int" {
    throw "agent_loop: loop_control `until` must be an integer; got " + type_of(until)
  }
  return {
    action: action,
    by: by,
    until: until,
    reason: command?.reason ?? "",
    status: command?.status ?? "",
  }
}

/**
 * agent_loop_control_invoke normalizes a custom or adaptive loop command.
 *
 * @effects: [state.mutate]
 * @errors: [agent_loop]
 * @api_stability: experimental
 */
pub fn agent_loop_control_invoke(
  opts: dict,
  budget: AgentLoopBudget,
  state: AgentLoopState,
) -> AgentLoopCommand {
  const policy = opts?.loop_control
  if policy != nil {
    return __agent_interpret_loop_command(policy(state))
  }
  if budget.mode == "adaptive" {
    return __agent_interpret_loop_command(__agent_default_loop_control(state, budget))
  }
  return __agent_interpret_loop_command(nil)
}

fn __agent_apply_extension(command: AgentLoopCommand, budget: AgentLoopBudget, current_max: int) {
  const cap = budget.max
  const target = if command.until > 0 {
    command.until
  } else {
    let by = if command.by > 0 {
      command.by
    } else {
      budget.extend_by
    }
    current_max + by
  }
  const bounded = if target > cap {
    cap
  } else {
    target
  }
  if bounded <= current_max {
    return {extended: false, new_limit: current_max, delta: 0}
  }
  return {extended: true, new_limit: bounded, delta: bounded - current_max}
}

fn __agent_record_budget_decision(
  decisions: list<AgentLoopBudgetDecision>,
  iteration: int,
  action: string,
  old_limit: int,
  new_limit: int,
  reason: string,
  status: string,
) -> list<AgentLoopBudgetDecision> {
  return decisions.appending(
    {
      iteration: iteration,
      action: action,
      old_limit: old_limit,
      new_limit: new_limit,
      reason: reason,
      status: status,
    },
  )
}

fn __agent_progress_summary_text(
  completion_vetoed: bool,
  tool_count: int,
  visible_text: string,
  no_net_advance: bool,
  no_information_gain: bool,
) -> string {
  if completion_vetoed {
    return "completion gate vetoed"
  }
  // Activity present but the verification outcome is not advancing (same failure
  // recurring): name the thrash so the recorded loop decision is auditable.
  if no_net_advance {
    return "tool activity without net verification progress"
  }
  if no_information_gain {
    return "repeated read-only observations returned no new information"
  }
  if tool_count > 0 {
    return "executed " + to_string(tool_count) + " tool call(s)"
  }
  if trim(visible_text) != "" {
    return "produced visible text"
  }
  return "no progress signal"
}

/**
 * agent_loop_snapshot_state builds the loop-control state callback payload.
 *
 * `progress.turns_since_progressing` is returned ADVANCED for the turn being
 * snapshotted: pass the caller's previous count as
 * `progress_turns_since_progressing` and store the returned value back. Zero
 * means this turn satisfied `agent_loop_is_progressing`.
 *
 * @effects: []
 * @errors: []
 * @api_stability: experimental
 */
pub fn agent_loop_snapshot_state(args: dict) -> AgentLoopState {
  const verdict = args.verdict
  const completion_vetoed = verdict?.action == "continue"
  const completion_verdict = if verdict == nil {
    nil
  } else {
    verdict?.receipt?.outcome ?? verdict?.action
  }
  const completion_feedback = if verdict == nil {
    nil
  } else {
    verdict?.feedback
  }
  const visible_text = args.visible_text
  const tool_count = args.tool_count
  const turn_successful = args.turn_successful
  const progress_changed = tool_count > 0 || len(turn_successful) > 0
    || trim(visible_text) != ""
  // Outcome-aware override: the caller (the loop) passes `progress_no_net_advance`
  // when this turn is on the verify-bearing failing path AND the verification
  // outcome is not advancing (same-diagnostic streak past threshold). It only
  // suppresses the extension when there WAS activity to begin with — a thrash —
  // never when there is genuinely no activity (that case is already "no progress").
  const no_net_advance = progress_changed && (args.progress_no_net_advance ?? false)
  const no_information_gain = progress_changed
    && (args.progress_no_information_gain ?? false)
  const progress_summary = __agent_progress_summary_text(
    completion_vetoed,
    tool_count,
    visible_text,
    no_net_advance,
    no_information_gain,
  )
  const previous_turns_since_progressing = args.progress_turns_since_progressing ?? 0
  const base = __agent_loop_state(
    {
      iteration: args.iteration,
      current_limit: args.current_limit,
      budget_max: args.budget_max,
      extensions_used: args.extensions_used,
      wall_clock_ms: args.wall_clock_ms,
      wall_clock_limit_ms: args.wall_clock_limit_ms,
      cost_usd: args.cost_usd,
      total_cost_limit_usd: args.total_cost_limit_usd,
      consecutive_failures: args.consecutive_failures,
      consecutive_failure_limit: args.consecutive_failure_limit,
      turn_tool_count: tool_count,
      turn_successful: turn_successful,
      turn_rejected: args.turn_rejected,
      turn_text_chars: len(visible_text),
      turn_native_fallback_used: args.turn_native_fallback_used,
      session_successful: args.session_successful,
      session_rejected: args.session_rejected,
      missing_required_tools: args.missing_required_tools,
      completion_proposed: args.completion_proposed,
      completion_vetoed: completion_vetoed,
      completion_verdict: completion_verdict,
      completion_feedback: completion_feedback,
      progress_changed: progress_changed,
      progress_no_net_advance: no_net_advance,
      progress_no_information_gain: no_information_gain,
      progress_summary: progress_summary,
      progress_turns_since_progressing: previous_turns_since_progressing,
    },
  )
  return __agent_loop_advance_progress_window(base, previous_turns_since_progressing)
}

/**
 * agent_loop_apply_command applies a normalized command to the loop budget.
 *
 * @effects: [agent]
 * @errors: []
 * @api_stability: experimental
 */
pub fn agent_loop_apply_command(agent: HarnessAgent, state: dict) -> AgentLoopCommandApplication {
  const command = state.command
  const session_id = state.session_id
  const iteration = state.iteration
  const current_max = state.current_max
  const budget = state.budget
  if command.action == "stop" {
    const stop_status = if command.status != "" {
      command.status
    } else {
      "stopped"
    }
    const stop_reason = if command.reason != "" {
      command.reason
    } else {
      "loop_control"
    }
    const decisions = __agent_record_budget_decision(
      state.decisions,
      iteration,
      "stop",
      current_max,
      current_max,
      command.reason,
      stop_status,
    )
    agent_emit_event(
      agent,
      session_id,
      "loop_control_decision",
      {
        iteration: iteration,
        action: "stop",
        old_limit: current_max,
        new_limit: current_max,
        reason: command.reason,
        status: stop_status,
      },
    )
    return {
      stop: true,
      final_status: stop_status,
      stop_reason: stop_reason,
      current_max: current_max,
      extensions_used: state.extensions_used,
      decisions: decisions,
    }
  }
  if command.action == "extend" {
    const applied = __agent_apply_extension(command, budget, current_max)
    if applied.extended {
      const old_limit = current_max
      const new_limit = applied.new_limit
      const extensions_used = state.extensions_used + 1
      const decisions = __agent_record_budget_decision(
        state.decisions,
        iteration,
        "extend",
        old_limit,
        new_limit,
        command.reason,
        "",
      )
      agent_emit_event(
        agent,
        session_id,
        "loop_control_decision",
        {
          iteration: iteration,
          action: "extend",
          old_limit: old_limit,
          new_limit: new_limit,
          reason: command.reason,
          status: "",
        },
      )
      return {
        stop: false,
        final_status: "",
        stop_reason: nil,
        current_max: new_limit,
        extensions_used: extensions_used,
        decisions: decisions,
      }
    }
  }
  return {
    stop: false,
    final_status: "",
    stop_reason: nil,
    current_max: current_max,
    extensions_used: state.extensions_used,
    decisions: state.decisions,
  }
}