harn-stdlib 0.8.49

Embedded Harn standard library source catalog
Documentation
/**
 * std/timing — scoped duration spans for Harn scripts.
 *
 * Promotes the hand-rolled `let started = harness.clock.now_ms(); work();
 * let dur = harness.clock.now_ms() - started` pattern into a first-class
 * observability primitive. Callback form (`timed(name, attrs, callback)`)
 * is the preferred shape because it cannot forget to close; imperative
 * pairs (`start_timing` / `end_timing` / `timing_event`) are still
 * available for flows that cross callbacks, branches, or async-ish
 * lifecycle boundaries.
 *
 * Timing segments are recorded as `user_timing` VM spans regardless of
 * whether `enable_tracing(true)` was called — scripts always get
 * `duration_ms` back. Duration is measured against the monotonic clock,
 * while `started_at_ms` / `ended_at_ms` capture wall-clock millis for
 * external correlation. When tracing is enabled, the segments appear in
 * `trace_spans()` and `harn run --profile-json` under
 * `kind = "user_timing"` so OTel exporters route them as INTERNAL spans
 * rather than as GenAI inference or tool spans.
 *
 * Pick a timing span when duration matters, a metric when you want a
 * pre-aggregated numeric series, and a span attribute when the value
 * belongs on the current span instead of a new one.
 */
type TimingSegment = dict

type TimingHandle = dict

/**
 * timed runs `callback` inside a timing span named `name` and returns
 * `{result, timing}`. The span closes automatically on return or throw,
 * so this is the preferred shape for measuring a discrete piece of work.
 *
 * @effects: [host]
 * @allocation: heap
 * @errors: [invalid_argument]
 * @api_stability: experimental
 * @example: let r = timed("benchmark.agent_loop", {case_id: case_id}, { -> work() })
 */
pub fn timed(name: string, attrs: dict = {}, callback = nil) -> dict {
  if callback == nil {
    throw "timed: missing callback (use start_timing for imperative flows)"
  }
  let handle = __timing_start(name, attrs)
  try {
    let value = callback()
    let timing = __timing_end(handle, {status: "ok"})
    return {result: value, timing: timing}
  } catch (e) {
    let timing = __timing_end(handle, {status: "error", error: to_string(e)})
    throw e
  }
}

/**
 * start_timing opens an imperative timing span and returns a handle.
 * Pair every call with `end_timing` once the work finishes. Prefer
 * `timed(name, attrs, callback)` when the work fits in a single callback.
 *
 * @effects: [host]
 * @allocation: heap
 * @errors: [invalid_argument]
 * @api_stability: experimental
 * @example: let h = start_timing("provider.preflight", {provider: provider})
 */
pub fn start_timing(name: string, attrs: dict = {}) -> TimingHandle {
  return __timing_start(name, attrs)
}

/**
 * timing_event records a sub-phase annotation on the open span without
 * allocating a child span for every marker. Returns `true` when the
 * event was attached, `false` if the handle's span was already closed.
 *
 * @effects: [host]
 * @allocation: heap
 * @errors: [invalid_argument]
 * @api_stability: experimental
 * @example: timing_event(timing, "credentials.checked", {available: true})
 */
pub fn timing_event(handle: TimingHandle, name: string, attrs: dict = {}) -> bool {
  return __timing_event(handle, name, attrs)
}

/**
 * end_timing finalizes a timing handle and returns its `TimingSegment`.
 * Final attributes (`status`, `error`, additional metadata) merge into
 * the span before close. Calling `end_timing` twice on the same handle
 * is idempotent — the second call returns the cached segment instead of
 * corrupting the active span stack.
 *
 * @effects: [host]
 * @allocation: heap
 * @errors: [invalid_argument]
 * @api_stability: experimental
 * @example: let timing = end_timing(handle, {status: "ok"})
 */
pub fn end_timing(handle: TimingHandle, final_attrs: dict = {}) -> TimingSegment {
  return __timing_end(handle, final_attrs)
}

/**
 * elapsed_ms reads the monotonic milliseconds elapsed since the handle
 * was opened, without closing the span. Useful for soft deadlines that
 * want to keep the span open for sub-phase annotations after the check.
 *
 * @effects: []
 * @allocation: heap
 * @errors: [invalid_argument]
 * @api_stability: experimental
 * @example: if elapsed_ms(handle) > 5000 { abort("deadline") }
 */
pub fn elapsed_ms(handle: TimingHandle) -> int {
  let started = handle?.monotonic_started_ms ?? 0
  return __timing_now_monotonic_ms() - started
}