/** * Predicate context construction for the v2 evaluator. * * Two concerns live here because they collaborate tightly: * * 1. `createExecCache` — memoizes `exec(cmd, args, opts)` by * `(cmd, args, cwd)` so every rule evaluated for ONE tool_call * sees the same result for the same query, without re-running the * underlying child process. A fresh cache is created per * tool_call; cross-call results are never shared. * 2. `createFindEntries` — wraps pi's `sessionManager.getEntries()` * into the {@link PredicateContext.findEntries} shape, filtering * to `type: "custom"` entries by `customType` and flattening to * `{ data, timestamp }` (timestamps normalized from ISO strings to * epoch ms, matching what observers producing entries can rely on). * * The evaluator itself assembles the final {@link PredicateContext} * from these closures plus per-candidate fields (cwd / tool / input / * agentLoopIndex) as an object literal — no helper needed once the shape * is shared across bash and write/edit code paths. * * Kept internal (under `evaluator-internals/`) so consumers can swap * the evaluator without inheriting its helper surface. The only * re-export is through `../evaluator.ts`. */ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import type { ExecOpts, PredicateContext } from "../schema.ts"; /** * Narrow host surface the evaluator needs from the pi runtime. Lets * tests pass a stub without building a full fake `ExtensionAPI`, and * keeps the evaluator decoupled from the unrelated parts of pi's API * (tool registration, slash commands, OAuth, …). */ export interface EvaluatorHost { /** See {@link ExtensionAPI.exec}. */ exec: ExtensionAPI["exec"]; /** See {@link ExtensionAPI.appendEntry}. */ appendEntry: ExtensionAPI["appendEntry"]; } /** * Create a tool_call-scoped exec function that memoizes by * `(cmd, args, cwd)`. Caches only the PROMISE so concurrent * predicate evaluations that hit the same key await the same * in-flight child process — not N redundant ones. * * `opts.cwd` defaults to `sessionCwd` (mirroring how predicates see * their "current cwd" via {@link PredicateContext.cwd}). `timeoutMs` * is forwarded as `timeout`. */ export declare function createExecCache(host: EvaluatorHost, sessionCwd: string): PredicateContext["exec"]; /** * Key under which the engine auto-injects the current `agentLoopIndex` * into every entry written via `PredicateContext.appendEntry` or * `ObserverContext.appendEntry`. Rules using * `when.happened: { in: "agent_loop" }` filter session entries by * comparing this key against `ctx.agentLoopIndex`. * * Part of the on-disk session-JSONL format, exposed as a public * module-level constant. Re-exported from the package root * so plugin authors who manually inspect entries via * `findEntries` can import the constant by name rather than * hardcoding the string — a future rename would then break at * import time instead of silently producing un-filtered entries. */ export declare const AGENT_LOOP_INDEX_KEY: "_agentLoopIndex"; /** * Narrow "is plain object" guard used to distinguish payloads that * are safe to merge into (spread) from payloads that must be wrapped * as `{ value, _agentLoopIndex }`. * * Anything that is NOT a plain object (arrays, Date, Map, Set, Error, * class instances, functions, null, undefined, primitives) falls into * the wrap branch. Direct `{...}` or `Object.create(null)` shapes fall * into the merge branch. * * Two-stage detection: * 1. `Object.prototype.toString.call(x)` returns `"[object Object]"` * only for plain objects and for class instances of user-defined * classes. It correctly excludes arrays, Date, Map, Set, Error, * etc. * 2. A prototype check then rejects user-class instances: plain * objects have `Object.prototype` (or `null` for * `Object.create(null)`) as their prototype; `new Box(...)` has * `Box.prototype`, which is neither. * * The explicit `Array.isArray` check is belt-and-suspenders — some * runtimes have historically misreported arrays via `toString`, and * the array case is the one most likely to hit this wrapper (an * observer appending a list of watched paths). Cheap to check twice. */ export declare function isPlainObject(x: unknown): x is Record; /** * Wrap a raw `host.appendEntry` so every write auto-injects the * current `agentLoopIndex` into the payload. Plain-object payloads * get the field merged in; everything else — primitives, arrays, * `Date`, `Map`, `Set`, `Error`, class instances, functions, null, * undefined — is wrapped as `{ value, _agentLoopIndex }` so * downstream consumers always see a consistent object shape. * * The "everything else" branch exists because the naive spread * (`{ ...data, ... }`) silently corrupts non-plain objects: arrays * become pseudo-objects with string-indexed keys, Date / Map / Set / * Error instances lose their internal state entirely, etc. Wrapping * under `value` preserves the original reference unchanged. * * The returned closure matches both {@link PredicateContext.appendEntry} * and {@link ObserverContext.appendEntry} so the evaluator and the * observer dispatcher share one wrapper. * * `findEntriesCache` (optional) is a cache map shared with a sibling * {@link createFindEntries} closure. When supplied, every `appendEntry` * call invalidates the cache entry for the written `customType` so * the next `findEntries(customType)` re-reads the session JSONL and * sees the newly-written entry (S2/E1; see ../INVARIANTS.md for the * S/E tag glossary). Omit the parameter to keep * the pre-S2 behaviour (no invalidation) — handy for tests or callers * that don't pair the two closures. */ export declare function createAppendEntry(host: EvaluatorHost, agentLoopIndex: number, findEntriesCache?: Map>): PredicateContext["appendEntry"]; /** * Adapt pi's `sessionManager.getEntries()` into the typed-and-filtered * view predicates (and observers) expect. * * Strategy: * - pick only `type: "custom"` entries (the shape `pi.appendEntry` * produces — see `CustomEntry` in pi's session-manager), * - filter by `customType`, * - project to `{ data, timestamp }` where `timestamp` is epoch-ms * (parsed from the entry's ISO string). Epoch-ms is what turn-state * checks want for chronological comparisons without having to * re-parse. * * Results are memoized PER invocation of `createFindEntries` by * customType. The evaluator rebuilds the closure on every tool_call; * the observer dispatcher rebuilds on every tool_result. So each phase * sees a consistent snapshot across reads. * * Cross-rule write visibility (S2/E1): when the same phase also uses * a paired {@link createAppendEntry} with the SAME cache map, a write * during rule A's `onFire` invalidates the cached read for that * customType so rule B's `when.happened` predicate sees the fresh * entry. Callers that want this consistency pass in a shared cache * via the optional `cache` parameter; callers that omit it get the * old per-closure snapshot behaviour (pre-S2), which is sound only * when the closure never interleaves reads with writes. * * The `ctx` argument is the pi `ExtensionContext` — we re-read * `getEntries()` only on a cache miss. Cache keys are per-closure (or * per shared cache) so cross-tool_call or cross-tool_result reads * always see the freshest state (a new closure = a new cache). */ export declare function createFindEntries(ctx: ExtensionContext, cache?: Map>): PredicateContext["findEntries"]; /** * Allocate a fresh session-entry cache shared between a paired * {@link createFindEntries} + {@link createAppendEntry} for the same * tool_call (evaluator) or tool_result (observer dispatcher) phase. * * Using a shared cache gives two guarantees the evaluator + dispatcher * rely on: * * 1. Consistent reads: N calls to `findEntries(type)` within one * phase materialize the entry list ONCE per type. * 2. Write-through-reads (S2/E1): a write via the paired * `appendEntry` invalidates that type's cached list, so the next * read re-scans the session JSONL and observes the write. Without * this, a rule's `onFire` appending X followed by a later rule's * `when.happened: { event: X }` would read a stale pre-write * snapshot. * * Consumers who don't need write-through-reads (tests, one-shot * `findEntries` calls) can pass a fresh cache or omit the parameter * on both constructors — the closures then each get their own cache * map and behave like the pre-S2 implementation. */ export declare function createSessionEntryCache(): Map>; export type { ExecOpts }; //# sourceMappingURL=context.d.ts.map