@lunora/testing 1.0.0-alpha.4 → 1.0.0-alpha.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,5 +1,115 @@
1
+ import { AgentMessageRow, AgentRunInput, AgentGenerateResult, AgentRunResult, AgentDefinition } from '@lunora/agent';
1
2
  import { ScheduledJob, RegisteredAction, RegisteredMutation, RegisteredQuery, ArgsValidator, InferArgs, ActionCtx, MutationCtx, QueryCtx, TableDefinition, Schema } from '@lunora/server';
2
3
  export { type InboxOptions, type WaitForMailOptions, extractLink, listCapturedMail, waitForMail } from '@lunora/mail/testing';
4
+ /** A thread message as the in-memory runtime stores it (superset of {@link AgentMessageRow}). */
5
+ interface HarnessMessage extends AgentMessageRow {
6
+ messageKey: string;
7
+ threadKey: string;
8
+ }
9
+ /** A thread record as the in-memory runtime stores it. */
10
+ interface HarnessThread {
11
+ agent: string;
12
+ error?: string;
13
+ instanceId?: string;
14
+ key: string;
15
+ messageCount: number;
16
+ owner?: string;
17
+ status: string;
18
+ title?: string;
19
+ usage?: unknown;
20
+ }
21
+ /** One recorded function dispatch the loop made through `ctx.run`. */
22
+ interface HarnessDispatch {
23
+ args: Record<string, unknown> | undefined;
24
+ path: string;
25
+ }
26
+ /** Extra function-dispatch handler layered over the built-in `agents:*` runtime double. */
27
+ type HarnessFunction = (args?: Record<string, unknown>) => unknown;
28
+ interface AgentHarnessOptions {
29
+ /**
30
+ * Worker `env` bindings visible to tool `execute` and a dynamic
31
+ * `instructions` thunk. Default `{ LUNORA_TEST: true }`.
32
+ */
33
+ env?: Record<string, unknown>;
34
+ /**
35
+ * The agent's `lunora/agents.ts` export name — used for thread attribution
36
+ * and to derive the child-agent `AGENT_*` bindings a sub-agent tool targets.
37
+ * Default `"agent"`.
38
+ */
39
+ exportName?: string;
40
+ /**
41
+ * Stub the app functions a tool's `execute` (or the agent's `memory.source`)
42
+ * dispatches through `ctx.run`, keyed by function path (`"weather:lookup"`).
43
+ * A handler layered here shadows the built-in `agents:*` runtime double for
44
+ * that path. An unstubbed non-`agents:*` dispatch throws, so a missing stub
45
+ * fails loudly rather than silently returning `undefined`.
46
+ */
47
+ functions?: Record<string, HarnessFunction>;
48
+ /**
49
+ * Scripted model decisions, one {@link AgentGenerateResult} per LLM turn —
50
+ * the default script for {@link AgentHarness.run}. A terminal turn has an
51
+ * empty `toolCalls`; a tool-calling turn lists the calls the loop then runs
52
+ * against the agent's own tools. Override per run via `run(..., { script })`.
53
+ */
54
+ script: AgentGenerateResult[];
55
+ }
56
+ /** Per-run overrides for {@link AgentHarness.run}. */
57
+ interface AgentRunOverrides {
58
+ /** The workflow instance id for this run (default: an auto-incrementing `wf-N`). */
59
+ instanceId?: string;
60
+ /** The model script for THIS run, replacing the harness default. */
61
+ script?: AgentGenerateResult[];
62
+ }
63
+ interface AgentHarness {
64
+ /**
65
+ * Every function dispatch the loop has made through `ctx.run`, in order —
66
+ * both the `agents:*` runtime calls and the tool/memory app dispatches. Use
67
+ * it to assert a tool called the function you expected with the args you
68
+ * expected.
69
+ */
70
+ readonly dispatches: ReadonlyArray<HarnessDispatch>;
71
+ /** The persisted messages of a thread, ordered by `seq`. */
72
+ messages: (threadKey: string) => AgentMessageRow[];
73
+ /**
74
+ * Drive one durable agent run to completion against the in-memory journal +
75
+ * runtime double, returning the loop's {@link AgentRunResult}. Reuse the same
76
+ * `threadKey` across calls to continue a conversation (the persisted history
77
+ * carries over); each call runs under a fresh instance id, modelling a
78
+ * distinct workflow instance.
79
+ */
80
+ run: (params: AgentRunInput, overrides?: AgentRunOverrides) => Promise<AgentRunResult>;
81
+ /** The stored thread record (status, error, instanceId, usage, …), or `undefined` before its first run. */
82
+ thread: (threadKey: string) => HarnessThread | undefined;
83
+ }
84
+ /**
85
+ * A unit-test harness for a `defineAgent` tool-loop that runs WITHOUT a real
86
+ * model or network. It drives {@link runAgentLoop} over the agent's own
87
+ * `AgentGenerate` seam — the model is a script of per-turn decisions, tool calls
88
+ * run the agent's real `execute` functions inside an in-memory durable-step
89
+ * journal, and thread/message persistence goes through an in-memory `agents:*`
90
+ * runtime double. Mock any function a tool (or the agent's memory) dispatches
91
+ * through the `functions` option.
92
+ *
93
+ * ```ts
94
+ * const harness = agentHarness(support, {
95
+ * script: [toolCallTurn("c1", "lookup", { id: "o_1" }), finalTurn("It shipped.")],
96
+ * functions: { "orders:lookup": () => ({ status: "shipped" }) },
97
+ * });
98
+ * const result = await harness.run({ input: "where is my order?", threadKey: "t1" });
99
+ * expect(result.text).toBe("It shipped.");
100
+ * expect(harness.messages("t1").at(-1)?.content).toBe("It shipped.");
101
+ * ```
102
+ *
103
+ * The harness deliberately does NOT model the concurrency guard or a
104
+ * human-in-the-loop approval pause — it is for exercising an agent's tools and
105
+ * turn logic, not the durable orchestration itself (that is covered inside
106
+ * `@lunora/agent`).
107
+ */
108
+ declare const agentHarness: (agent: AgentDefinition, options: AgentHarnessOptions) => AgentHarness;
109
+ /** A terminal (final-answer) turn — no tool calls. */
110
+ declare const finalTurn: (text: string, extra?: Partial<AgentGenerateResult>) => AgentGenerateResult;
111
+ /** A single-tool-call turn: the loop runs the named tool with `input`. */
112
+ declare const toolCallTurn: (id: string, name: string, input: unknown, text?: string) => AgentGenerateResult;
3
113
  /** A pending job entry in the fake scheduler queue. */
4
114
  interface FakeScheduledJob extends ScheduledJob {
5
115
  /** The args the job was scheduled with. */
@@ -141,6 +251,19 @@ type FunctionRegistry = Record<string, RegisteredAction<any, any> | RegisteredMu
141
251
  * clearly-throwing stubs for unsupported surfaces.
142
252
  */
143
253
  interface LunoraTestOptions {
254
+ /**
255
+ * Injectable `ctx.env` for every context (query / mutation / action). When
256
+ * provided, handlers that read `ctx.env.SOME_KEY` (the validated `defineEnv`
257
+ * surface) see this object. Left unset it stays `undefined` — matching the
258
+ * optional `ctx.env?` field, so graceful `ctx.env?.KEY` access still yields
259
+ * `undefined` rather than throwing. Not a throwing stub for exactly that
260
+ * reason: `env` is designed to be legitimately absent.
261
+ * @example
262
+ * ```ts
263
+ * const t = lunoraTest(schema, { env: { STRIPE_KEY: "sk_test_…" } });
264
+ * ```
265
+ */
266
+ env?: Record<string, unknown>;
144
267
  /**
145
268
  * Injectable `fetch` implementation for action contexts. When provided,
146
269
  * `ctx.fetch` in every `action` (and `withIdentity` views) resolves to this
@@ -171,6 +294,16 @@ interface LunoraTestOptions {
171
294
  * ```
172
295
  */
173
296
  functions?: FunctionRegistry;
297
+ /**
298
+ * Fixed value for `ctx.now` (epoch ms) in every context. Production captures
299
+ * `Date.now()` once per execution; in tests a fixed `now` makes time-dependent
300
+ * handlers deterministic. Defaults to the wall clock at harness creation.
301
+ * @example
302
+ * ```ts
303
+ * const t = lunoraTest(schema, { now: 1_700_000_000_000 });
304
+ * ```
305
+ */
306
+ now?: number;
174
307
  }
175
308
  /**
176
309
  * The in-memory test harness returned by {@link lunoraTest}. Mirrors the first
@@ -253,6 +386,8 @@ interface TestHarness {
253
386
  *
254
387
  * **v1 surfaces now supported:**
255
388
  *
389
+ * - `ctx.env` (all contexts): inject the validated env via `options.env`; unset it
390
+ * stays `undefined`, matching the optional `ctx.env?` field.
256
391
  * - `ctx.fetch` (actions): inject a custom `fetch` via `options.fetch`.
257
392
  * - `ctx.scheduler` (mutations + actions): fully functional fake with virtual clock;
258
393
  * control via `harness.scheduler.advance(ms)` / `runPending()` / `list()`.
@@ -262,4 +397,76 @@ interface TestHarness {
262
397
  * These are clearly documented follow-ups.
263
398
  */
264
399
  declare const lunoraTest: (schema: TestSchema, options?: LunoraTestOptions) => TestHarness;
265
- export { type FakeScheduledJob, type FakeSchedulerControls, type FunctionRegistry, type LunoraTestOptions, type ScheduledJobFailure, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, lunoraTest };
400
+ /** One sample handed to a {@link Scorer}. */
401
+ interface ScorerSample {
402
+ /** The reference/gold answer, when the scorer compares against one. */
403
+ expected?: string;
404
+ /** The prompt/input that produced the output (for LLM-judge context). */
405
+ input?: string;
406
+ /** Arbitrary per-sample metadata carried through. */
407
+ metadata?: Record<string, unknown>;
408
+ /** The model/agent output under test. */
409
+ output: string;
410
+ }
411
+ /** A scorer's verdict: a `[0, 1]` score and an optional human-readable reason. */
412
+ interface ScoreResult {
413
+ reason?: string;
414
+ score: number;
415
+ }
416
+ /** A named scorer — returns a `[0, 1]` score (or a {@link ScoreResult}) for a sample. */
417
+ interface Scorer {
418
+ name: string;
419
+ score: (sample: ScorerSample) => Promise<ScoreResult | number> | ScoreResult | number;
420
+ }
421
+ /** One dataset case: an input and its optional gold answer/metadata. */
422
+ interface EvalCase {
423
+ expected?: string;
424
+ input: string;
425
+ metadata?: Record<string, unknown>;
426
+ }
427
+ /** The per-case eval result: the produced output plus each scorer's verdict and the mean. */
428
+ interface EvalItemResult {
429
+ average: number;
430
+ input: string;
431
+ output: string;
432
+ scores: Record<string, ScoreResult>;
433
+ }
434
+ /** The whole eval run: per-case results plus the mean of their averages. */
435
+ interface EvalResult {
436
+ average: number;
437
+ items: EvalItemResult[];
438
+ }
439
+ /** Score 1 if the output contains `needle` (case-insensitive unless `caseSensitive`). */
440
+ declare const containsScorer: (needle: string, options?: {
441
+ caseSensitive?: boolean;
442
+ }) => Scorer;
443
+ /** Score 1 when `pattern` matches the output. */
444
+ declare const regexScorer: (pattern: RegExp, name?: string) => Scorer;
445
+ /** Score 1 when the trimmed output exactly equals the trimmed `expected`. */
446
+ declare const exactMatchScorer: () => Scorer;
447
+ /** Score the fraction of `keywords` (case-insensitive) present in the output. */
448
+ declare const keywordScorer: (keywords: ReadonlyArray<string>) => Scorer;
449
+ /**
450
+ * An LLM-as-judge scorer. `judge` is INJECTED — a `(prompt) => Promise&lt;string>`
451
+ * you wire to your model (e.g. via `ctx.ai` / `generateText`), so this module
452
+ * stays model-agnostic and the judge is mockable in tests. It returns the model's
453
+ * numeric verdict (`[0, 1]`) plus its reason.
454
+ */
455
+ declare const llmScorer: (options: {
456
+ criteria: string;
457
+ judge: (prompt: string) => Promise<string>;
458
+ name?: string;
459
+ }) => Scorer;
460
+ /** Run every scorer over one sample (concurrently) → each verdict keyed by name + their mean. */
461
+ declare const scoreSample: (sample: ScorerSample, scorers: ReadonlyArray<Scorer>) => Promise<{
462
+ average: number;
463
+ scores: Record<string, ScoreResult>;
464
+ }>;
465
+ /**
466
+ * Run a dataset through `produce` (e.g. an `agentHarness.run` wrapper returning
467
+ * the output text) and score each output with `scorers`. Cases run concurrently;
468
+ * give each its own thread/key inside `produce` if the producer is stateful.
469
+ * Returns per-case results plus the mean of their averages.
470
+ */
471
+ declare const evaluate: (cases: ReadonlyArray<EvalCase>, produce: (input: string) => Promise<string> | string, scorers: ReadonlyArray<Scorer>) => Promise<EvalResult>;
472
+ export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type EvalCase, type EvalItemResult, type EvalResult, type FakeScheduledJob, type FakeSchedulerControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, evaluate, exactMatchScorer, finalTurn, keywordScorer, llmScorer, lunoraTest, regexScorer, scoreSample, toolCallTurn };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,115 @@
1
+ import { AgentMessageRow, AgentRunInput, AgentGenerateResult, AgentRunResult, AgentDefinition } from '@lunora/agent';
1
2
  import { ScheduledJob, RegisteredAction, RegisteredMutation, RegisteredQuery, ArgsValidator, InferArgs, ActionCtx, MutationCtx, QueryCtx, TableDefinition, Schema } from '@lunora/server';
2
3
  export { type InboxOptions, type WaitForMailOptions, extractLink, listCapturedMail, waitForMail } from '@lunora/mail/testing';
4
+ /** A thread message as the in-memory runtime stores it (superset of {@link AgentMessageRow}). */
5
+ interface HarnessMessage extends AgentMessageRow {
6
+ messageKey: string;
7
+ threadKey: string;
8
+ }
9
+ /** A thread record as the in-memory runtime stores it. */
10
+ interface HarnessThread {
11
+ agent: string;
12
+ error?: string;
13
+ instanceId?: string;
14
+ key: string;
15
+ messageCount: number;
16
+ owner?: string;
17
+ status: string;
18
+ title?: string;
19
+ usage?: unknown;
20
+ }
21
+ /** One recorded function dispatch the loop made through `ctx.run`. */
22
+ interface HarnessDispatch {
23
+ args: Record<string, unknown> | undefined;
24
+ path: string;
25
+ }
26
+ /** Extra function-dispatch handler layered over the built-in `agents:*` runtime double. */
27
+ type HarnessFunction = (args?: Record<string, unknown>) => unknown;
28
+ interface AgentHarnessOptions {
29
+ /**
30
+ * Worker `env` bindings visible to tool `execute` and a dynamic
31
+ * `instructions` thunk. Default `{ LUNORA_TEST: true }`.
32
+ */
33
+ env?: Record<string, unknown>;
34
+ /**
35
+ * The agent's `lunora/agents.ts` export name — used for thread attribution
36
+ * and to derive the child-agent `AGENT_*` bindings a sub-agent tool targets.
37
+ * Default `"agent"`.
38
+ */
39
+ exportName?: string;
40
+ /**
41
+ * Stub the app functions a tool's `execute` (or the agent's `memory.source`)
42
+ * dispatches through `ctx.run`, keyed by function path (`"weather:lookup"`).
43
+ * A handler layered here shadows the built-in `agents:*` runtime double for
44
+ * that path. An unstubbed non-`agents:*` dispatch throws, so a missing stub
45
+ * fails loudly rather than silently returning `undefined`.
46
+ */
47
+ functions?: Record<string, HarnessFunction>;
48
+ /**
49
+ * Scripted model decisions, one {@link AgentGenerateResult} per LLM turn —
50
+ * the default script for {@link AgentHarness.run}. A terminal turn has an
51
+ * empty `toolCalls`; a tool-calling turn lists the calls the loop then runs
52
+ * against the agent's own tools. Override per run via `run(..., { script })`.
53
+ */
54
+ script: AgentGenerateResult[];
55
+ }
56
+ /** Per-run overrides for {@link AgentHarness.run}. */
57
+ interface AgentRunOverrides {
58
+ /** The workflow instance id for this run (default: an auto-incrementing `wf-N`). */
59
+ instanceId?: string;
60
+ /** The model script for THIS run, replacing the harness default. */
61
+ script?: AgentGenerateResult[];
62
+ }
63
+ interface AgentHarness {
64
+ /**
65
+ * Every function dispatch the loop has made through `ctx.run`, in order —
66
+ * both the `agents:*` runtime calls and the tool/memory app dispatches. Use
67
+ * it to assert a tool called the function you expected with the args you
68
+ * expected.
69
+ */
70
+ readonly dispatches: ReadonlyArray<HarnessDispatch>;
71
+ /** The persisted messages of a thread, ordered by `seq`. */
72
+ messages: (threadKey: string) => AgentMessageRow[];
73
+ /**
74
+ * Drive one durable agent run to completion against the in-memory journal +
75
+ * runtime double, returning the loop's {@link AgentRunResult}. Reuse the same
76
+ * `threadKey` across calls to continue a conversation (the persisted history
77
+ * carries over); each call runs under a fresh instance id, modelling a
78
+ * distinct workflow instance.
79
+ */
80
+ run: (params: AgentRunInput, overrides?: AgentRunOverrides) => Promise<AgentRunResult>;
81
+ /** The stored thread record (status, error, instanceId, usage, …), or `undefined` before its first run. */
82
+ thread: (threadKey: string) => HarnessThread | undefined;
83
+ }
84
+ /**
85
+ * A unit-test harness for a `defineAgent` tool-loop that runs WITHOUT a real
86
+ * model or network. It drives {@link runAgentLoop} over the agent's own
87
+ * `AgentGenerate` seam — the model is a script of per-turn decisions, tool calls
88
+ * run the agent's real `execute` functions inside an in-memory durable-step
89
+ * journal, and thread/message persistence goes through an in-memory `agents:*`
90
+ * runtime double. Mock any function a tool (or the agent's memory) dispatches
91
+ * through the `functions` option.
92
+ *
93
+ * ```ts
94
+ * const harness = agentHarness(support, {
95
+ * script: [toolCallTurn("c1", "lookup", { id: "o_1" }), finalTurn("It shipped.")],
96
+ * functions: { "orders:lookup": () => ({ status: "shipped" }) },
97
+ * });
98
+ * const result = await harness.run({ input: "where is my order?", threadKey: "t1" });
99
+ * expect(result.text).toBe("It shipped.");
100
+ * expect(harness.messages("t1").at(-1)?.content).toBe("It shipped.");
101
+ * ```
102
+ *
103
+ * The harness deliberately does NOT model the concurrency guard or a
104
+ * human-in-the-loop approval pause — it is for exercising an agent's tools and
105
+ * turn logic, not the durable orchestration itself (that is covered inside
106
+ * `@lunora/agent`).
107
+ */
108
+ declare const agentHarness: (agent: AgentDefinition, options: AgentHarnessOptions) => AgentHarness;
109
+ /** A terminal (final-answer) turn — no tool calls. */
110
+ declare const finalTurn: (text: string, extra?: Partial<AgentGenerateResult>) => AgentGenerateResult;
111
+ /** A single-tool-call turn: the loop runs the named tool with `input`. */
112
+ declare const toolCallTurn: (id: string, name: string, input: unknown, text?: string) => AgentGenerateResult;
3
113
  /** A pending job entry in the fake scheduler queue. */
4
114
  interface FakeScheduledJob extends ScheduledJob {
5
115
  /** The args the job was scheduled with. */
@@ -141,6 +251,19 @@ type FunctionRegistry = Record<string, RegisteredAction<any, any> | RegisteredMu
141
251
  * clearly-throwing stubs for unsupported surfaces.
142
252
  */
143
253
  interface LunoraTestOptions {
254
+ /**
255
+ * Injectable `ctx.env` for every context (query / mutation / action). When
256
+ * provided, handlers that read `ctx.env.SOME_KEY` (the validated `defineEnv`
257
+ * surface) see this object. Left unset it stays `undefined` — matching the
258
+ * optional `ctx.env?` field, so graceful `ctx.env?.KEY` access still yields
259
+ * `undefined` rather than throwing. Not a throwing stub for exactly that
260
+ * reason: `env` is designed to be legitimately absent.
261
+ * @example
262
+ * ```ts
263
+ * const t = lunoraTest(schema, { env: { STRIPE_KEY: "sk_test_…" } });
264
+ * ```
265
+ */
266
+ env?: Record<string, unknown>;
144
267
  /**
145
268
  * Injectable `fetch` implementation for action contexts. When provided,
146
269
  * `ctx.fetch` in every `action` (and `withIdentity` views) resolves to this
@@ -171,6 +294,16 @@ interface LunoraTestOptions {
171
294
  * ```
172
295
  */
173
296
  functions?: FunctionRegistry;
297
+ /**
298
+ * Fixed value for `ctx.now` (epoch ms) in every context. Production captures
299
+ * `Date.now()` once per execution; in tests a fixed `now` makes time-dependent
300
+ * handlers deterministic. Defaults to the wall clock at harness creation.
301
+ * @example
302
+ * ```ts
303
+ * const t = lunoraTest(schema, { now: 1_700_000_000_000 });
304
+ * ```
305
+ */
306
+ now?: number;
174
307
  }
175
308
  /**
176
309
  * The in-memory test harness returned by {@link lunoraTest}. Mirrors the first
@@ -253,6 +386,8 @@ interface TestHarness {
253
386
  *
254
387
  * **v1 surfaces now supported:**
255
388
  *
389
+ * - `ctx.env` (all contexts): inject the validated env via `options.env`; unset it
390
+ * stays `undefined`, matching the optional `ctx.env?` field.
256
391
  * - `ctx.fetch` (actions): inject a custom `fetch` via `options.fetch`.
257
392
  * - `ctx.scheduler` (mutations + actions): fully functional fake with virtual clock;
258
393
  * control via `harness.scheduler.advance(ms)` / `runPending()` / `list()`.
@@ -262,4 +397,76 @@ interface TestHarness {
262
397
  * These are clearly documented follow-ups.
263
398
  */
264
399
  declare const lunoraTest: (schema: TestSchema, options?: LunoraTestOptions) => TestHarness;
265
- export { type FakeScheduledJob, type FakeSchedulerControls, type FunctionRegistry, type LunoraTestOptions, type ScheduledJobFailure, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, lunoraTest };
400
+ /** One sample handed to a {@link Scorer}. */
401
+ interface ScorerSample {
402
+ /** The reference/gold answer, when the scorer compares against one. */
403
+ expected?: string;
404
+ /** The prompt/input that produced the output (for LLM-judge context). */
405
+ input?: string;
406
+ /** Arbitrary per-sample metadata carried through. */
407
+ metadata?: Record<string, unknown>;
408
+ /** The model/agent output under test. */
409
+ output: string;
410
+ }
411
+ /** A scorer's verdict: a `[0, 1]` score and an optional human-readable reason. */
412
+ interface ScoreResult {
413
+ reason?: string;
414
+ score: number;
415
+ }
416
+ /** A named scorer — returns a `[0, 1]` score (or a {@link ScoreResult}) for a sample. */
417
+ interface Scorer {
418
+ name: string;
419
+ score: (sample: ScorerSample) => Promise<ScoreResult | number> | ScoreResult | number;
420
+ }
421
+ /** One dataset case: an input and its optional gold answer/metadata. */
422
+ interface EvalCase {
423
+ expected?: string;
424
+ input: string;
425
+ metadata?: Record<string, unknown>;
426
+ }
427
+ /** The per-case eval result: the produced output plus each scorer's verdict and the mean. */
428
+ interface EvalItemResult {
429
+ average: number;
430
+ input: string;
431
+ output: string;
432
+ scores: Record<string, ScoreResult>;
433
+ }
434
+ /** The whole eval run: per-case results plus the mean of their averages. */
435
+ interface EvalResult {
436
+ average: number;
437
+ items: EvalItemResult[];
438
+ }
439
+ /** Score 1 if the output contains `needle` (case-insensitive unless `caseSensitive`). */
440
+ declare const containsScorer: (needle: string, options?: {
441
+ caseSensitive?: boolean;
442
+ }) => Scorer;
443
+ /** Score 1 when `pattern` matches the output. */
444
+ declare const regexScorer: (pattern: RegExp, name?: string) => Scorer;
445
+ /** Score 1 when the trimmed output exactly equals the trimmed `expected`. */
446
+ declare const exactMatchScorer: () => Scorer;
447
+ /** Score the fraction of `keywords` (case-insensitive) present in the output. */
448
+ declare const keywordScorer: (keywords: ReadonlyArray<string>) => Scorer;
449
+ /**
450
+ * An LLM-as-judge scorer. `judge` is INJECTED — a `(prompt) => Promise&lt;string>`
451
+ * you wire to your model (e.g. via `ctx.ai` / `generateText`), so this module
452
+ * stays model-agnostic and the judge is mockable in tests. It returns the model's
453
+ * numeric verdict (`[0, 1]`) plus its reason.
454
+ */
455
+ declare const llmScorer: (options: {
456
+ criteria: string;
457
+ judge: (prompt: string) => Promise<string>;
458
+ name?: string;
459
+ }) => Scorer;
460
+ /** Run every scorer over one sample (concurrently) → each verdict keyed by name + their mean. */
461
+ declare const scoreSample: (sample: ScorerSample, scorers: ReadonlyArray<Scorer>) => Promise<{
462
+ average: number;
463
+ scores: Record<string, ScoreResult>;
464
+ }>;
465
+ /**
466
+ * Run a dataset through `produce` (e.g. an `agentHarness.run` wrapper returning
467
+ * the output text) and score each output with `scorers`. Cases run concurrently;
468
+ * give each its own thread/key inside `produce` if the producer is stateful.
469
+ * Returns per-case results plus the mean of their averages.
470
+ */
471
+ declare const evaluate: (cases: ReadonlyArray<EvalCase>, produce: (input: string) => Promise<string> | string, scorers: ReadonlyArray<Scorer>) => Promise<EvalResult>;
472
+ export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type EvalCase, type EvalItemResult, type EvalResult, type FakeScheduledJob, type FakeSchedulerControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, evaluate, exactMatchScorer, finalTurn, keywordScorer, llmScorer, lunoraTest, regexScorer, scoreSample, toolCallTurn };
package/dist/index.mjs CHANGED
@@ -1,2 +1,4 @@
1
- export { lunoraTest } from './packem_shared/lunoraTest-BUg4Ebv8.mjs';
1
+ export { agentHarness, finalTurn, toolCallTurn } from './packem_shared/agentHarness-GTPhWdQ8.mjs';
2
+ export { lunoraTest } from './packem_shared/lunoraTest-DbzbUAGQ.mjs';
3
+ export { containsScorer, evaluate, exactMatchScorer, keywordScorer, llmScorer, regexScorer, scoreSample } from './packem_shared/containsScorer-DGwtvUNp.mjs';
2
4
  export { extractLink, listCapturedMail, waitForMail } from '@lunora/mail/testing';
@@ -0,0 +1,144 @@
1
+ import { runAgentLoop, DEFAULT_AGENT_FUNCTION_PATHS } from '@lunora/agent';
2
+
3
+ const scriptedGenerate = (script) => {
4
+ const remaining = [...script];
5
+ return () => {
6
+ const next = remaining.shift();
7
+ if (!next) {
8
+ throw new Error("agentHarness: scripted generate exhausted — the run took more turns than the script provided.");
9
+ }
10
+ return Promise.resolve(next);
11
+ };
12
+ };
13
+ class DurableStepJournal {
14
+ invoked = [];
15
+ entries = /* @__PURE__ */ new Map();
16
+ async do(name, callback) {
17
+ const existing = this.entries.get(name);
18
+ if (existing) {
19
+ return existing.output;
20
+ }
21
+ this.invoked.push(name);
22
+ const output = await callback();
23
+ this.entries.set(name, { output });
24
+ return output;
25
+ }
26
+ // eslint-disable-next-line class-methods-use-this -- mirrors AgentStepLike.waitForEvent's host signature so the mock stays assignable
27
+ async waitForEvent(_name, _options) {
28
+ return new Promise(() => {
29
+ });
30
+ }
31
+ }
32
+ const createRuntime = (extra) => {
33
+ const threads = /* @__PURE__ */ new Map();
34
+ const messages = /* @__PURE__ */ new Map();
35
+ const dispatches = [];
36
+ const ensureThread = (args) => {
37
+ const key = args?.["key"];
38
+ const instanceId = args?.["instanceId"];
39
+ const existing = threads.get(key);
40
+ if (existing) {
41
+ existing.status = "running";
42
+ delete existing.error;
43
+ if (instanceId !== void 0) {
44
+ existing.instanceId = instanceId;
45
+ }
46
+ return { created: false };
47
+ }
48
+ threads.set(key, {
49
+ agent: args?.["agent"],
50
+ instanceId,
51
+ key,
52
+ messageCount: 0,
53
+ owner: args?.["owner"],
54
+ status: "running",
55
+ title: args?.["title"]
56
+ });
57
+ return { created: true };
58
+ };
59
+ const appendMessage = (args) => {
60
+ const threadKey = args?.["threadKey"];
61
+ const messageKey = args?.["messageKey"];
62
+ const id = `${threadKey}:${messageKey}`;
63
+ const existing = messages.get(id);
64
+ if (existing) {
65
+ return { seq: existing.seq };
66
+ }
67
+ const thread = threads.get(threadKey);
68
+ if (!thread) {
69
+ throw new Error(`agentHarness: append to unknown thread "${threadKey}".`);
70
+ }
71
+ const seq = thread.messageCount;
72
+ thread.messageCount += 1;
73
+ messages.set(id, { ...args, seq });
74
+ return { seq };
75
+ };
76
+ const listMessages = (args) => {
77
+ const key = args?.["key"];
78
+ return [...messages.values()].filter((message) => message.threadKey === key).toSorted((a, b) => a.seq - b.seq);
79
+ };
80
+ const patchThread = (args) => {
81
+ const thread = threads.get(args?.["key"]);
82
+ if (thread) {
83
+ for (const [field, value] of Object.entries(args ?? {})) {
84
+ if (field !== "key" && value !== void 0) {
85
+ thread[field] = value;
86
+ }
87
+ }
88
+ }
89
+ return void 0;
90
+ };
91
+ const handlers = new Map([
92
+ [DEFAULT_AGENT_FUNCTION_PATHS.appendMessage, appendMessage],
93
+ [DEFAULT_AGENT_FUNCTION_PATHS.ensureThread, ensureThread],
94
+ [DEFAULT_AGENT_FUNCTION_PATHS.listMessages, listMessages],
95
+ [DEFAULT_AGENT_FUNCTION_PATHS.patchThread, patchThread],
96
+ ...Object.entries(extra)
97
+ ]);
98
+ const run = (reference, args) => {
99
+ const path = reference["__lunoraRef"];
100
+ dispatches.push({ args, path });
101
+ const handler = handlers.get(path);
102
+ if (!handler) {
103
+ throw new Error(`agentHarness: unstubbed dispatch "${path}" — pass a \`functions\` handler for it.`);
104
+ }
105
+ return Promise.resolve(handler(args));
106
+ };
107
+ return { dispatches, messages, run, threads };
108
+ };
109
+ const agentHarness = (agent, options) => {
110
+ const exportName = options.exportName ?? "agent";
111
+ const env = options.env ?? { LUNORA_TEST: true };
112
+ const runtime = createRuntime(options.functions ?? {});
113
+ let runCount = 0;
114
+ const run = (params, overrides) => {
115
+ runCount += 1;
116
+ const instanceId = overrides?.instanceId ?? `wf-${String(runCount)}`;
117
+ const script = overrides?.script ?? options.script;
118
+ return runAgentLoop({
119
+ agent,
120
+ env,
121
+ exportName,
122
+ generate: scriptedGenerate(script),
123
+ instanceId,
124
+ params,
125
+ paths: DEFAULT_AGENT_FUNCTION_PATHS,
126
+ run: runtime.run,
127
+ step: new DurableStepJournal()
128
+ });
129
+ };
130
+ return {
131
+ dispatches: runtime.dispatches,
132
+ messages: (threadKey) => [...runtime.messages.values()].filter((message) => message.threadKey === threadKey).toSorted((a, b) => a.seq - b.seq).map(({ messageKey: _messageKey, threadKey: _threadKey, ...row }) => row),
133
+ run,
134
+ thread: (threadKey) => runtime.threads.get(threadKey)
135
+ };
136
+ };
137
+ const finalTurn = (text, extra) => {
138
+ return { text, toolCalls: [], ...extra };
139
+ };
140
+ const toolCallTurn = (id, name, input, text = "") => {
141
+ return { text, toolCalls: [{ id, input, name }] };
142
+ };
143
+
144
+ export { agentHarness, finalTurn, toolCallTurn };