@lunora/testing 1.0.0-alpha.286 → 1.0.0-alpha.288
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 +119 -8
- package/dist/index.d.ts +119 -8
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/agentHarness-BJFCbBx-.mjs +1 -0
- package/dist/packem_shared/createFakeSecretValues-BTNOscqQ.mjs +1 -0
- package/dist/packem_shared/createStreamGate-Geg-yvx6.mjs +1 -0
- package/package.json +11 -9
- package/dist/packem_shared/agentHarness-DA9onGVO.mjs +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { AgentMessageRow, AgentRunInput, AgentGenerateResult, AgentRunResult, AgentDefinition } from '@lunora/agent';
|
|
1
|
+
import { AgentMessageRow, AgentRunInput, AgentTokenDelta, AgentGenerateResult, AgentRunResult, AgentDefinition } from '@lunora/agent';
|
|
2
|
+
import { SecretValueHost } from '@lunora/platform';
|
|
3
|
+
import { LanguageModel } from 'ai';
|
|
2
4
|
import { ArtifactsBindingLike, ArtifactsErrorCode, ArtifactsCommitMetadata, ArtifactsTreeEntry } from '@lunora/bindings/artifacts';
|
|
3
5
|
import { ScheduledJob, LogFields, RegisteredAction, RegisteredMutation, RegisteredQuery, ArgsValidator, InferArgs, ActionCtx, MutationCtx, QueryCtx, TableDefinition, Schema } from '@lunora/server';
|
|
4
6
|
import { NotifyDefinition } from '@lunora/notify';
|
|
5
7
|
import { ChannelType, ChannelPayloadMap } from '@visulima/notification';
|
|
6
8
|
import { QueueContentType } from '@lunora/queue';
|
|
9
|
+
import { MockLanguageModelV4 } from 'ai/test';
|
|
7
10
|
export {
|
|
8
11
|
/**
|
|
9
12
|
* `@lunora/testing` — the user-facing toolkit for end-to-end testing a Lunora app.
|
|
@@ -83,18 +86,53 @@ interface AgentHarnessOptions {
|
|
|
83
86
|
* Scripted model decisions, one {@link AgentGenerateResult} per LLM turn —
|
|
84
87
|
* the default script for {@link AgentHarness.run}. A terminal turn has an
|
|
85
88
|
* empty `toolCalls`; a tool-calling turn lists the calls the loop then runs
|
|
86
|
-
* against the agent's own tools. Override per run via `run(..., { script })
|
|
89
|
+
* against the agent's own tools. Override per run via `run(..., { script })`,
|
|
90
|
+
* or replace it per run with a streamed `model` via `run(..., { model })`.
|
|
87
91
|
*/
|
|
88
|
-
script
|
|
92
|
+
script?: AgentGenerateResult[];
|
|
93
|
+
/**
|
|
94
|
+
* The secret store the agent's `requestSecretTool` values live in, shared by
|
|
95
|
+
* every run of this harness. Default: an in-memory store whose clock only moves
|
|
96
|
+
* through its `advanceClock`.
|
|
97
|
+
*/
|
|
98
|
+
secrets?: SecretValueHost;
|
|
89
99
|
}
|
|
90
100
|
/** Per-run overrides for {@link AgentHarness.run}. */
|
|
91
101
|
interface AgentRunOverrides {
|
|
92
102
|
/** The workflow instance id for this run (default: an auto-incrementing `wf-N`). */
|
|
93
103
|
instanceId?: string;
|
|
104
|
+
/**
|
|
105
|
+
* The model for THIS run, replacing the scripted decisions. Each turn runs
|
|
106
|
+
* through the production seam over this model: `createStreamGenerate` (the
|
|
107
|
+
* `streamText` path, so a recorded stream from `recordedModel` is consumed
|
|
108
|
+
* part by part) with `onTokenDelta` as its live sink.
|
|
109
|
+
*/
|
|
110
|
+
model?: LanguageModel;
|
|
111
|
+
/**
|
|
112
|
+
* Live token-delta sink for a `model` run — receives each text delta as the
|
|
113
|
+
* recorded stream produces it. Ignored for scripted runs.
|
|
114
|
+
*/
|
|
115
|
+
onTokenDelta?: (delta: AgentTokenDelta) => void;
|
|
94
116
|
/** The model script for THIS run, replacing the harness default. */
|
|
95
117
|
script?: AgentGenerateResult[];
|
|
96
118
|
}
|
|
97
119
|
interface AgentHarness {
|
|
120
|
+
/**
|
|
121
|
+
* Resolve once the run `instanceId` has reached the wait named `name` (e.g.
|
|
122
|
+
* `secret:0:call_1`, for the first turn's call `call_1`), so a test can act on the run at a known point instead of
|
|
123
|
+
* after a delay.
|
|
124
|
+
*/
|
|
125
|
+
awaitWait: (instanceId: string, name: string) => Promise<void>;
|
|
126
|
+
/**
|
|
127
|
+
* Decline the `requestSecretTool` call `toolCallId` on `threadKey` and wake its
|
|
128
|
+
* wait, as the owner's decline does.
|
|
129
|
+
*/
|
|
130
|
+
decline: (threadKey: string, toolCallId: string) => void;
|
|
131
|
+
/**
|
|
132
|
+
* Deliver the event the run `instanceId` is waiting on. Delivered before that
|
|
133
|
+
* wait is reached, it is held for it.
|
|
134
|
+
*/
|
|
135
|
+
deliver: (instanceId: string, name: string, payload: unknown) => void;
|
|
98
136
|
/**
|
|
99
137
|
* Every function dispatch the loop has made through `ctx.run`, in order —
|
|
100
138
|
* both the `agents:*` runtime calls and the tool/memory app dispatches. Use
|
|
@@ -102,6 +140,15 @@ interface AgentHarness {
|
|
|
102
140
|
* expected.
|
|
103
141
|
*/
|
|
104
142
|
readonly dispatches: ReadonlyArray<HarnessDispatch>;
|
|
143
|
+
/** Fail the run `instanceId`'s pending wait the way an elapsed host timeout does. */
|
|
144
|
+
expire: (instanceId: string, name: string) => void;
|
|
145
|
+
/**
|
|
146
|
+
* Every journal entry the runs have written, in the order they were written:
|
|
147
|
+
* each completed step's output (`{ kind: "step", name, output }`) and each
|
|
148
|
+
* delivered wait payload (`{ kind: "wait", name, type, payload }`). A test
|
|
149
|
+
* asserts a secret is absent from this, as it must be from the durable journal.
|
|
150
|
+
*/
|
|
151
|
+
journalEntries: () => unknown[];
|
|
105
152
|
/** The persisted messages of a thread, ordered by `seq`. */
|
|
106
153
|
messages: (threadKey: string) => AgentMessageRow[];
|
|
107
154
|
/**
|
|
@@ -112,6 +159,14 @@ interface AgentHarness {
|
|
|
112
159
|
* distinct workflow instance.
|
|
113
160
|
*/
|
|
114
161
|
run: (params: AgentRunInput, overrides?: AgentRunOverrides) => Promise<AgentRunResult>;
|
|
162
|
+
/** The secret store the runs use (see {@link AgentHarnessOptions.secrets}). */
|
|
163
|
+
readonly secrets: SecretValueHost;
|
|
164
|
+
/**
|
|
165
|
+
* Supply `value` for the `requestSecretTool` call `toolCallId` on `threadKey` and
|
|
166
|
+
* wake its wait, as the owner's supply does. The value goes to the secret store
|
|
167
|
+
* and only the request's handle reaches the run.
|
|
168
|
+
*/
|
|
169
|
+
supply: (threadKey: string, toolCallId: string, value: string) => Promise<void>;
|
|
115
170
|
/** The stored thread record (status, error, instanceId, usage, …), or `undefined` before its first run. */
|
|
116
171
|
thread: (threadKey: string) => HarnessThread | undefined;
|
|
117
172
|
}
|
|
@@ -134,10 +189,12 @@ interface AgentHarness {
|
|
|
134
189
|
* expect(harness.messages("t1").at(-1)?.content).toBe("It shipped.");
|
|
135
190
|
* ```
|
|
136
191
|
*
|
|
137
|
-
* The harness deliberately does NOT model the concurrency guard
|
|
138
|
-
* human-in-the-loop
|
|
139
|
-
*
|
|
140
|
-
*
|
|
192
|
+
* The harness deliberately does NOT model the concurrency guard. A
|
|
193
|
+
* human-in-the-loop pause (an approval, or a `requestSecretTool` request) stops
|
|
194
|
+
* the run at its wait; the test resolves it with `deliver` or `expire`, and the
|
|
195
|
+
* harness does not route a decision through the owner-gated mutations. The
|
|
196
|
+
* harness is for exercising an agent's tools and turn logic, not the durable
|
|
197
|
+
* orchestration itself (that is covered inside `@lunora/agent`).
|
|
141
198
|
*/
|
|
142
199
|
declare const agentHarness: (agent: AgentDefinition, options: AgentHarnessOptions) => AgentHarness;
|
|
143
200
|
/** A terminal (final-answer) turn — no tool calls. */
|
|
@@ -272,6 +329,18 @@ interface ArtifactsFake {
|
|
|
272
329
|
declare const createArtifactsFake: (options?: {
|
|
273
330
|
namespace?: string;
|
|
274
331
|
}) => ArtifactsFake;
|
|
332
|
+
/** A {@link SecretValueHost} whose clock the test moves forward by hand. */
|
|
333
|
+
interface FakeSecretValues extends SecretValueHost {
|
|
334
|
+
/** Move the store's clock forward, so a value put earlier can expire. */
|
|
335
|
+
advanceClock: (ms: number) => void;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* In-memory {@link SecretValueHost} for harness suites. Follows the contract in
|
|
339
|
+
* `@lunora/platform` (unguessable refs, scope, one claimant, expiry refuses
|
|
340
|
+
* reads) with a clock that starts at zero, so a test controls every expiry
|
|
341
|
+
* through {@link FakeSecretValues.advanceClock}.
|
|
342
|
+
*/
|
|
343
|
+
declare const createFakeSecretValues: () => FakeSecretValues;
|
|
275
344
|
/** A pending job entry in the fake scheduler queue. */
|
|
276
345
|
interface FakeScheduledJob extends ScheduledJob {
|
|
277
346
|
/** The args the job was scheduled with. */
|
|
@@ -791,6 +860,48 @@ interface TestHarness {
|
|
|
791
860
|
* These are clearly documented follow-ups.
|
|
792
861
|
*/
|
|
793
862
|
declare const lunoraTest: (schema: TestSchema, options?: LunoraTestOptions) => TestHarness;
|
|
863
|
+
/** One provider stream part as a model's `doStream` emits it (`text-delta`, `tool-call`, `finish`, …). */
|
|
864
|
+
type ModelStreamResult = Awaited<ReturnType<InstanceType<typeof MockLanguageModelV4>["doStream"]>>;
|
|
865
|
+
type RecordedStreamPart = ModelStreamResult["stream"] extends ReadableStream<infer Part> ? Part : never;
|
|
866
|
+
/**
|
|
867
|
+
* A pause point in a recorded turn. The stream emits `after` parts, then stops
|
|
868
|
+
* until {@link StreamGate.release} is called — a test observes or cancels the
|
|
869
|
+
* run while it is mid-stream without sleeping.
|
|
870
|
+
*/
|
|
871
|
+
interface StreamGate {
|
|
872
|
+
/** Resolves once the stream has reached the gate and is waiting on {@link StreamGate.release}. */
|
|
873
|
+
readonly reached: Promise<void>;
|
|
874
|
+
/** Let the paused stream continue. Idempotent. */
|
|
875
|
+
release: () => void;
|
|
876
|
+
/** Called by the recorded stream when it reaches the gate; the stream resumes after `release`. */
|
|
877
|
+
wait: () => Promise<void>;
|
|
878
|
+
}
|
|
879
|
+
/** One recorded model call: the stream parts it emits, plus optional pause and failure points. */
|
|
880
|
+
interface RecordedTurn {
|
|
881
|
+
/** Fail the stream with `cause` once `after` parts have been emitted (a mid-stream provider error). */
|
|
882
|
+
error?: {
|
|
883
|
+
after: number;
|
|
884
|
+
cause: unknown;
|
|
885
|
+
};
|
|
886
|
+
/** The provider stream parts for one model call, in emission order. */
|
|
887
|
+
parts: ReadonlyArray<RecordedStreamPart>;
|
|
888
|
+
/** Pause after `after` parts until `gate` is released. */
|
|
889
|
+
pause?: {
|
|
890
|
+
after: number;
|
|
891
|
+
gate: StreamGate;
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
/** Create a {@link StreamGate} to pause a recorded turn on. */
|
|
895
|
+
declare const createStreamGate: () => StreamGate;
|
|
896
|
+
/**
|
|
897
|
+
* A language model that replays recorded stream parts, one {@link RecordedTurn}
|
|
898
|
+
* per model call, in order. Pass it as an agent's `model` (or to
|
|
899
|
+
* `agentHarness(...).run(..., { model })`) to drive the production streaming
|
|
900
|
+
* seam against a recorded stream. Each call pops the next turn; a call past the
|
|
901
|
+
* last recorded turn fails the stream loudly rather than inventing an answer.
|
|
902
|
+
* `doStreamCalls` records the call options each turn was requested with.
|
|
903
|
+
*/
|
|
904
|
+
declare const recordedModel: (turns: ReadonlyArray<RecordedTurn>) => MockLanguageModelV4;
|
|
794
905
|
/** One sample handed to a {@link Scorer}. */
|
|
795
906
|
interface ScorerSample {
|
|
796
907
|
/** The reference/gold answer, when the scorer compares against one. */
|
|
@@ -942,4 +1053,4 @@ declare const groundednessScorer: (options: {
|
|
|
942
1053
|
judge: (prompt: string) => Promise<string>;
|
|
943
1054
|
name?: string;
|
|
944
1055
|
}) => Scorer;
|
|
945
|
-
export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type ArtifactsFake, type EvalCase, type EvalItemResult, type EvalResult, type EvaluationAttributeValue, type EvaluationMetrics, type EvaluationSpanHandle, type FakeNotifyControls, type FakeQueueControls, type FakeScheduledJob, type FakeSchedulerControls, type FakeTopicControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type NotifyChannel, type ProducedOutput, type PublishedTopicMessage, type RecordEvaluationInput, type RetrievalScorerOptions, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SentNotification, type SentQueueMessage, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, createArtifactsFake, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, groundednessScorer, keywordScorer, llmScorer, lunoraTest, mrrScorer, ndcgAtK, precisionAtK, recallAtK, recordEvaluation, regexScorer, scoreSample, toolCallTurn };
|
|
1056
|
+
export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type ArtifactsFake, type EvalCase, type EvalItemResult, type EvalResult, type EvaluationAttributeValue, type EvaluationMetrics, type EvaluationSpanHandle, type FakeNotifyControls, type FakeQueueControls, type FakeScheduledJob, type FakeSchedulerControls, type FakeSecretValues, type FakeTopicControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type NotifyChannel, type ProducedOutput, type PublishedTopicMessage, type RecordEvaluationInput, type RecordedStreamPart, type RecordedTurn, type RetrievalScorerOptions, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SentNotification, type SentQueueMessage, type StreamGate, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, createArtifactsFake, createFakeSecretValues, createStreamGate, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, groundednessScorer, keywordScorer, llmScorer, lunoraTest, mrrScorer, ndcgAtK, precisionAtK, recallAtK, recordEvaluation, recordedModel, regexScorer, scoreSample, toolCallTurn };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { AgentMessageRow, AgentRunInput, AgentGenerateResult, AgentRunResult, AgentDefinition } from '@lunora/agent';
|
|
1
|
+
import { AgentMessageRow, AgentRunInput, AgentTokenDelta, AgentGenerateResult, AgentRunResult, AgentDefinition } from '@lunora/agent';
|
|
2
|
+
import { SecretValueHost } from '@lunora/platform';
|
|
3
|
+
import { LanguageModel } from 'ai';
|
|
2
4
|
import { ArtifactsBindingLike, ArtifactsErrorCode, ArtifactsCommitMetadata, ArtifactsTreeEntry } from '@lunora/bindings/artifacts';
|
|
3
5
|
import { ScheduledJob, LogFields, RegisteredAction, RegisteredMutation, RegisteredQuery, ArgsValidator, InferArgs, ActionCtx, MutationCtx, QueryCtx, TableDefinition, Schema } from '@lunora/server';
|
|
4
6
|
import { NotifyDefinition } from '@lunora/notify';
|
|
5
7
|
import { ChannelType, ChannelPayloadMap } from '@visulima/notification';
|
|
6
8
|
import { QueueContentType } from '@lunora/queue';
|
|
9
|
+
import { MockLanguageModelV4 } from 'ai/test';
|
|
7
10
|
export {
|
|
8
11
|
/**
|
|
9
12
|
* `@lunora/testing` — the user-facing toolkit for end-to-end testing a Lunora app.
|
|
@@ -83,18 +86,53 @@ interface AgentHarnessOptions {
|
|
|
83
86
|
* Scripted model decisions, one {@link AgentGenerateResult} per LLM turn —
|
|
84
87
|
* the default script for {@link AgentHarness.run}. A terminal turn has an
|
|
85
88
|
* empty `toolCalls`; a tool-calling turn lists the calls the loop then runs
|
|
86
|
-
* against the agent's own tools. Override per run via `run(..., { script })
|
|
89
|
+
* against the agent's own tools. Override per run via `run(..., { script })`,
|
|
90
|
+
* or replace it per run with a streamed `model` via `run(..., { model })`.
|
|
87
91
|
*/
|
|
88
|
-
script
|
|
92
|
+
script?: AgentGenerateResult[];
|
|
93
|
+
/**
|
|
94
|
+
* The secret store the agent's `requestSecretTool` values live in, shared by
|
|
95
|
+
* every run of this harness. Default: an in-memory store whose clock only moves
|
|
96
|
+
* through its `advanceClock`.
|
|
97
|
+
*/
|
|
98
|
+
secrets?: SecretValueHost;
|
|
89
99
|
}
|
|
90
100
|
/** Per-run overrides for {@link AgentHarness.run}. */
|
|
91
101
|
interface AgentRunOverrides {
|
|
92
102
|
/** The workflow instance id for this run (default: an auto-incrementing `wf-N`). */
|
|
93
103
|
instanceId?: string;
|
|
104
|
+
/**
|
|
105
|
+
* The model for THIS run, replacing the scripted decisions. Each turn runs
|
|
106
|
+
* through the production seam over this model: `createStreamGenerate` (the
|
|
107
|
+
* `streamText` path, so a recorded stream from `recordedModel` is consumed
|
|
108
|
+
* part by part) with `onTokenDelta` as its live sink.
|
|
109
|
+
*/
|
|
110
|
+
model?: LanguageModel;
|
|
111
|
+
/**
|
|
112
|
+
* Live token-delta sink for a `model` run — receives each text delta as the
|
|
113
|
+
* recorded stream produces it. Ignored for scripted runs.
|
|
114
|
+
*/
|
|
115
|
+
onTokenDelta?: (delta: AgentTokenDelta) => void;
|
|
94
116
|
/** The model script for THIS run, replacing the harness default. */
|
|
95
117
|
script?: AgentGenerateResult[];
|
|
96
118
|
}
|
|
97
119
|
interface AgentHarness {
|
|
120
|
+
/**
|
|
121
|
+
* Resolve once the run `instanceId` has reached the wait named `name` (e.g.
|
|
122
|
+
* `secret:0:call_1`, for the first turn's call `call_1`), so a test can act on the run at a known point instead of
|
|
123
|
+
* after a delay.
|
|
124
|
+
*/
|
|
125
|
+
awaitWait: (instanceId: string, name: string) => Promise<void>;
|
|
126
|
+
/**
|
|
127
|
+
* Decline the `requestSecretTool` call `toolCallId` on `threadKey` and wake its
|
|
128
|
+
* wait, as the owner's decline does.
|
|
129
|
+
*/
|
|
130
|
+
decline: (threadKey: string, toolCallId: string) => void;
|
|
131
|
+
/**
|
|
132
|
+
* Deliver the event the run `instanceId` is waiting on. Delivered before that
|
|
133
|
+
* wait is reached, it is held for it.
|
|
134
|
+
*/
|
|
135
|
+
deliver: (instanceId: string, name: string, payload: unknown) => void;
|
|
98
136
|
/**
|
|
99
137
|
* Every function dispatch the loop has made through `ctx.run`, in order —
|
|
100
138
|
* both the `agents:*` runtime calls and the tool/memory app dispatches. Use
|
|
@@ -102,6 +140,15 @@ interface AgentHarness {
|
|
|
102
140
|
* expected.
|
|
103
141
|
*/
|
|
104
142
|
readonly dispatches: ReadonlyArray<HarnessDispatch>;
|
|
143
|
+
/** Fail the run `instanceId`'s pending wait the way an elapsed host timeout does. */
|
|
144
|
+
expire: (instanceId: string, name: string) => void;
|
|
145
|
+
/**
|
|
146
|
+
* Every journal entry the runs have written, in the order they were written:
|
|
147
|
+
* each completed step's output (`{ kind: "step", name, output }`) and each
|
|
148
|
+
* delivered wait payload (`{ kind: "wait", name, type, payload }`). A test
|
|
149
|
+
* asserts a secret is absent from this, as it must be from the durable journal.
|
|
150
|
+
*/
|
|
151
|
+
journalEntries: () => unknown[];
|
|
105
152
|
/** The persisted messages of a thread, ordered by `seq`. */
|
|
106
153
|
messages: (threadKey: string) => AgentMessageRow[];
|
|
107
154
|
/**
|
|
@@ -112,6 +159,14 @@ interface AgentHarness {
|
|
|
112
159
|
* distinct workflow instance.
|
|
113
160
|
*/
|
|
114
161
|
run: (params: AgentRunInput, overrides?: AgentRunOverrides) => Promise<AgentRunResult>;
|
|
162
|
+
/** The secret store the runs use (see {@link AgentHarnessOptions.secrets}). */
|
|
163
|
+
readonly secrets: SecretValueHost;
|
|
164
|
+
/**
|
|
165
|
+
* Supply `value` for the `requestSecretTool` call `toolCallId` on `threadKey` and
|
|
166
|
+
* wake its wait, as the owner's supply does. The value goes to the secret store
|
|
167
|
+
* and only the request's handle reaches the run.
|
|
168
|
+
*/
|
|
169
|
+
supply: (threadKey: string, toolCallId: string, value: string) => Promise<void>;
|
|
115
170
|
/** The stored thread record (status, error, instanceId, usage, …), or `undefined` before its first run. */
|
|
116
171
|
thread: (threadKey: string) => HarnessThread | undefined;
|
|
117
172
|
}
|
|
@@ -134,10 +189,12 @@ interface AgentHarness {
|
|
|
134
189
|
* expect(harness.messages("t1").at(-1)?.content).toBe("It shipped.");
|
|
135
190
|
* ```
|
|
136
191
|
*
|
|
137
|
-
* The harness deliberately does NOT model the concurrency guard
|
|
138
|
-
* human-in-the-loop
|
|
139
|
-
*
|
|
140
|
-
*
|
|
192
|
+
* The harness deliberately does NOT model the concurrency guard. A
|
|
193
|
+
* human-in-the-loop pause (an approval, or a `requestSecretTool` request) stops
|
|
194
|
+
* the run at its wait; the test resolves it with `deliver` or `expire`, and the
|
|
195
|
+
* harness does not route a decision through the owner-gated mutations. The
|
|
196
|
+
* harness is for exercising an agent's tools and turn logic, not the durable
|
|
197
|
+
* orchestration itself (that is covered inside `@lunora/agent`).
|
|
141
198
|
*/
|
|
142
199
|
declare const agentHarness: (agent: AgentDefinition, options: AgentHarnessOptions) => AgentHarness;
|
|
143
200
|
/** A terminal (final-answer) turn — no tool calls. */
|
|
@@ -272,6 +329,18 @@ interface ArtifactsFake {
|
|
|
272
329
|
declare const createArtifactsFake: (options?: {
|
|
273
330
|
namespace?: string;
|
|
274
331
|
}) => ArtifactsFake;
|
|
332
|
+
/** A {@link SecretValueHost} whose clock the test moves forward by hand. */
|
|
333
|
+
interface FakeSecretValues extends SecretValueHost {
|
|
334
|
+
/** Move the store's clock forward, so a value put earlier can expire. */
|
|
335
|
+
advanceClock: (ms: number) => void;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* In-memory {@link SecretValueHost} for harness suites. Follows the contract in
|
|
339
|
+
* `@lunora/platform` (unguessable refs, scope, one claimant, expiry refuses
|
|
340
|
+
* reads) with a clock that starts at zero, so a test controls every expiry
|
|
341
|
+
* through {@link FakeSecretValues.advanceClock}.
|
|
342
|
+
*/
|
|
343
|
+
declare const createFakeSecretValues: () => FakeSecretValues;
|
|
275
344
|
/** A pending job entry in the fake scheduler queue. */
|
|
276
345
|
interface FakeScheduledJob extends ScheduledJob {
|
|
277
346
|
/** The args the job was scheduled with. */
|
|
@@ -791,6 +860,48 @@ interface TestHarness {
|
|
|
791
860
|
* These are clearly documented follow-ups.
|
|
792
861
|
*/
|
|
793
862
|
declare const lunoraTest: (schema: TestSchema, options?: LunoraTestOptions) => TestHarness;
|
|
863
|
+
/** One provider stream part as a model's `doStream` emits it (`text-delta`, `tool-call`, `finish`, …). */
|
|
864
|
+
type ModelStreamResult = Awaited<ReturnType<InstanceType<typeof MockLanguageModelV4>["doStream"]>>;
|
|
865
|
+
type RecordedStreamPart = ModelStreamResult["stream"] extends ReadableStream<infer Part> ? Part : never;
|
|
866
|
+
/**
|
|
867
|
+
* A pause point in a recorded turn. The stream emits `after` parts, then stops
|
|
868
|
+
* until {@link StreamGate.release} is called — a test observes or cancels the
|
|
869
|
+
* run while it is mid-stream without sleeping.
|
|
870
|
+
*/
|
|
871
|
+
interface StreamGate {
|
|
872
|
+
/** Resolves once the stream has reached the gate and is waiting on {@link StreamGate.release}. */
|
|
873
|
+
readonly reached: Promise<void>;
|
|
874
|
+
/** Let the paused stream continue. Idempotent. */
|
|
875
|
+
release: () => void;
|
|
876
|
+
/** Called by the recorded stream when it reaches the gate; the stream resumes after `release`. */
|
|
877
|
+
wait: () => Promise<void>;
|
|
878
|
+
}
|
|
879
|
+
/** One recorded model call: the stream parts it emits, plus optional pause and failure points. */
|
|
880
|
+
interface RecordedTurn {
|
|
881
|
+
/** Fail the stream with `cause` once `after` parts have been emitted (a mid-stream provider error). */
|
|
882
|
+
error?: {
|
|
883
|
+
after: number;
|
|
884
|
+
cause: unknown;
|
|
885
|
+
};
|
|
886
|
+
/** The provider stream parts for one model call, in emission order. */
|
|
887
|
+
parts: ReadonlyArray<RecordedStreamPart>;
|
|
888
|
+
/** Pause after `after` parts until `gate` is released. */
|
|
889
|
+
pause?: {
|
|
890
|
+
after: number;
|
|
891
|
+
gate: StreamGate;
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
/** Create a {@link StreamGate} to pause a recorded turn on. */
|
|
895
|
+
declare const createStreamGate: () => StreamGate;
|
|
896
|
+
/**
|
|
897
|
+
* A language model that replays recorded stream parts, one {@link RecordedTurn}
|
|
898
|
+
* per model call, in order. Pass it as an agent's `model` (or to
|
|
899
|
+
* `agentHarness(...).run(..., { model })`) to drive the production streaming
|
|
900
|
+
* seam against a recorded stream. Each call pops the next turn; a call past the
|
|
901
|
+
* last recorded turn fails the stream loudly rather than inventing an answer.
|
|
902
|
+
* `doStreamCalls` records the call options each turn was requested with.
|
|
903
|
+
*/
|
|
904
|
+
declare const recordedModel: (turns: ReadonlyArray<RecordedTurn>) => MockLanguageModelV4;
|
|
794
905
|
/** One sample handed to a {@link Scorer}. */
|
|
795
906
|
interface ScorerSample {
|
|
796
907
|
/** The reference/gold answer, when the scorer compares against one. */
|
|
@@ -942,4 +1053,4 @@ declare const groundednessScorer: (options: {
|
|
|
942
1053
|
judge: (prompt: string) => Promise<string>;
|
|
943
1054
|
name?: string;
|
|
944
1055
|
}) => Scorer;
|
|
945
|
-
export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type ArtifactsFake, type EvalCase, type EvalItemResult, type EvalResult, type EvaluationAttributeValue, type EvaluationMetrics, type EvaluationSpanHandle, type FakeNotifyControls, type FakeQueueControls, type FakeScheduledJob, type FakeSchedulerControls, type FakeTopicControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type NotifyChannel, type ProducedOutput, type PublishedTopicMessage, type RecordEvaluationInput, type RetrievalScorerOptions, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SentNotification, type SentQueueMessage, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, createArtifactsFake, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, groundednessScorer, keywordScorer, llmScorer, lunoraTest, mrrScorer, ndcgAtK, precisionAtK, recallAtK, recordEvaluation, regexScorer, scoreSample, toolCallTurn };
|
|
1056
|
+
export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type ArtifactsFake, type EvalCase, type EvalItemResult, type EvalResult, type EvaluationAttributeValue, type EvaluationMetrics, type EvaluationSpanHandle, type FakeNotifyControls, type FakeQueueControls, type FakeScheduledJob, type FakeSchedulerControls, type FakeSecretValues, type FakeTopicControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type NotifyChannel, type ProducedOutput, type PublishedTopicMessage, type RecordEvaluationInput, type RecordedStreamPart, type RecordedTurn, type RetrievalScorerOptions, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SentNotification, type SentQueueMessage, type StreamGate, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, createArtifactsFake, createFakeSecretValues, createStreamGate, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, groundednessScorer, keywordScorer, llmScorer, lunoraTest, mrrScorer, ndcgAtK, precisionAtK, recallAtK, recordEvaluation, recordedModel, regexScorer, scoreSample, toolCallTurn };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{agentHarness as o,finalTurn as t,toolCallTurn as a}from"./packem_shared/agentHarness-
|
|
1
|
+
import{agentHarness as o,finalTurn as t,toolCallTurn as a}from"./packem_shared/agentHarness-BJFCbBx-.mjs";import{evaluationAttributes as l,recordEvaluation as n}from"./packem_shared/evaluationAttributes-CYLgTP1P.mjs";import{createArtifactsFake as m}from"./packem_shared/createArtifactsFake-L0tjki11.mjs";import{createFakeSecretValues as s}from"./packem_shared/createFakeSecretValues-BTNOscqQ.mjs";import{lunoraTest as f}from"./packem_shared/lunoraTest-VecnFIun.mjs";import{createStreamGate as S,recordedModel as d}from"./packem_shared/createStreamGate-Geg-yvx6.mjs";import{groundednessScorer as g,mrrScorer as k,ndcgAtK as M,precisionAtK as v,recallAtK as F}from"./packem_shared/groundednessScorer-kgXJCZDg.mjs";import{containsScorer as T,evaluate as w,exactMatchScorer as C,keywordScorer as b,llmScorer as h,regexScorer as y,scoreSample as E}from"./packem_shared/containsScorer-DoLfFOs6.mjs";import{extractLink as H,listCapturedMail as L,waitForMail as V}from"@lunora/mail/testing";export{o as agentHarness,T as containsScorer,m as createArtifactsFake,s as createFakeSecretValues,S as createStreamGate,w as evaluate,l as evaluationAttributes,C as exactMatchScorer,H as extractLink,t as finalTurn,g as groundednessScorer,b as keywordScorer,L as listCapturedMail,h as llmScorer,f as lunoraTest,k as mrrScorer,M as ndcgAtK,v as precisionAtK,F as recallAtK,n as recordEvaluation,d as recordedModel,y as regexScorer,E as scoreSample,a as toolCallTurn,V as waitForMail};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{DEFAULT_AGENT_FUNCTION_PATHS as h,runAgentLoop as E,createStreamGenerate as _,createAgentGenerate as j}from"@lunora/agent";import{createFakeSecretValues as O}from"./createFakeSecretValues-BTNOscqQ.mjs";const A="WorkflowTimeoutError",I=(p,s)=>JSON.stringify([p,s]);class H{constructor(s){this.journal=s}journal;awaited=new Map;delivered=new Map;expired=new Set;pending=new Map;reached=new Map;started=new Map;wait(s,r,n){const i=I(s,r),u=(this.started.get(i)??0)+1;if(this.started.set(i,u),this.expired.delete(i),this.delivered.has(i)){const g=this.delivered.get(i);return this.delivered.delete(i),this.notifyReached(i,u),Promise.resolve(this.record(r,n,g))}return new Promise((g,f)=>{if(this.pending.has(i)){f(new Error(`agentHarness: run "${s}" is already waiting on "${r}"`));return}this.pending.set(i,{reject:f,resolve:v=>{g(this.record(r,n,v))}}),this.notifyReached(i,u)})}deliver(s,r,n){const i=I(s,r),u=this.pending.get(i);if(u===void 0){this.expired.has(i)||this.delivered.set(i,n);return}this.pending.delete(i),u.resolve(n)}expire(s,r){const n=I(s,r),i=this.pending.get(n);if(i===void 0)throw new Error(`agentHarness: no pending wait "${r}" on run "${s}" to expire`);this.pending.delete(n),this.expired.add(n);const u=new Error(`agentHarness: wait "${r}" timed out`);u.name=A,i.reject(u)}awaitWait(s,r){const n=I(s,r),i=(this.awaited.get(n)??0)+1;return this.awaited.set(n,i),(this.started.get(n)??0)>=i?Promise.resolve():new Promise(u=>{this.reached.set(n,[...this.reached.get(n)??[],{resolve:u,target:i}])})}notifyReached(s,r){const n=this.reached.get(s)??[];this.reached.set(s,n.filter(i=>i.target>r));for(const i of n)i.target<=r&&i.resolve()}record(s,r,n){return this.journal.push({kind:"wait",name:s,payload:n,type:r}),n}}const N=900*1e3,P=p=>{const s=[...p];return()=>{const r=s.shift();if(!r)throw new Error("agentHarness: scripted generate exhausted — the run took more turns than the script provided.");return Promise.resolve(r)}};class W{constructor(s,r,n){this.waits=s,this.instanceId=r,this.journal=n}waits;instanceId;journal;invoked=[];entries=new Map;async do(s,r){const n=this.entries.get(s);if(n)return n.output;this.invoked.push(s);const i=await r();return this.entries.set(s,{output:i}),this.journal.push({kind:"step",name:s,output:i}),i}async waitForEvent(s,r){const n=this.entries.get(`wait:${s}`);if(n)return{payload:n.output,type:r.type};const i=await this.waits.wait(this.instanceId,s,r.type);return this.entries.set(`wait:${s}`,{output:i}),{payload:i,type:r.type}}}const F=(p,s)=>{const r=new Map,n=new Map,i=[],u=e=>{const t=e?.key,o=e?.instanceId,a=r.get(t);return a?(a.status="running",delete a.error,o!==void 0&&(a.instanceId=o),{outcome:"continued"}):(r.set(t,{agent:e?.agent,instanceId:o,key:t,messageCount:0,owner:e?.owner,status:"running",title:e?.title}),{outcome:"created"})},g=e=>{n.delete(`${e?.threadKey}:${e?.messageKey}`)},f=e=>{const t=e?.threadKey,o=e?.messageKey,a=`${t}:${o}`,y=n.get(a);if(y)return{seq:y.seq};const R=r.get(t);if(!R)throw new Error(`agentHarness: append to unknown thread "${t}".`);const $=R.messageCount;return R.messageCount+=1,n.set(a,{...e,seq:$}),{seq:$}},v=e=>{const t=e?.key;return[...n.values()].filter(o=>o.threadKey===t).toSorted((o,a)=>o.seq-a.seq)},m=e=>{const t=r.get(e?.key);if(t)for(const[o,a]of Object.entries(e??{}))o!=="key"&&a!==void 0&&(t[o]=a)},S=e=>{const t=r.get(e?.key);if(!e||!t||t.instanceId!==e.instanceId)return{};for(const[o,a]of Object.entries(e))o!=="key"&&o!=="instanceId"&&a!==void 0&&(t[o]=a);return{}},k=(e,t)=>JSON.stringify([e,t]),c=new Map,d=(e,t)=>{const o=[...c.values()].filter(y=>y.threadKey===e&&y.callId===t&&y.status==="pending"),[a]=o;if(!(o.length!==1||a===void 0))return{request:a,slot:a.slot}},l=e=>c.get(k(e?.threadKey,e?.toolCallId)),w=async e=>{const t=k(e?.threadKey,e?.toolCallId),o=c.get(t),a=e?.instanceId;a!==void 0&&o?.instanceId===a||(o?.ref!==void 0&&await s.discard(o.ref),c.set(t,{callId:e?.callId??e?.toolCallId,slot:e?.toolCallId,threadKey:e?.threadKey,consumerTool:e?.consumerTool,instanceId:e?.instanceId,status:"pending"}))},T=e=>{const t=l(e);return t===void 0?{status:"expired"}:t.status==="pending"?(t.status="expired",{status:"expired"}):t.status==="supplied"||t.status==="consumed"?{status:"supplied"}:{status:t.status==="declined"?"declined":"expired"}},M=e=>{const t=l(e);return t?.ref===void 0||t.status!=="supplied"&&t.status!=="consumed"||e?.claimant!==`tool:${t.consumerTool}:${e?.consumerCallId}`?Promise.resolve(void 0):s.take(t.ref,{claimant:e.claimant,scope:e.threadKey})},q=async e=>{const t=l(e);t!==void 0&&(t.ref!==void 0&&await s.discard(t.ref),t.status==="supplied"&&(t.status="consumed"))},x=async(e,t,o)=>{const a=d(e,t);if(a===void 0)throw new Error(`agentHarness: no pending secret request for tool call "${t}" on thread "${e}"`);return a.request.ref=await s.put(o,{scope:e,ttlMs:N}),a.request.status="supplied",{instanceId:a.request.instanceId,slot:a.slot}},K=(e,t)=>{const o=d(e,t);if(o!==void 0)return o.request.status="declined",{instanceId:o.request.instanceId,slot:o.slot}},C=new Map([[h.appendMessage,f],[h.approvalGranted,()=>!1],[h.completeRun,S],[h.deleteMessage,g],[h.discardSecret,q],[h.ensureThread,u],[h.listMessages,v],[h.openSecretRequest,w],[h.patchThread,m],[h.settleSecretRequest,T],[h.takeSecret,M],...Object.entries(p)]);return{declineSecret:K,dispatches:i,expireSecretRequest:(e,t)=>{for(const o of c.values())o.instanceId===e&&o.slot===t&&o.status==="pending"&&(o.status="expired")},messages:n,run:(e,t)=>{const o=e.__lunoraRef;i.push({args:t,path:o});const a=C.get(o);if(!a)throw new Error(`agentHarness: unstubbed dispatch "${o}" — pass a \`functions\` handler for it.`);return Promise.resolve(a(t))},supplySecret:x,threads:r}},D=(p,s)=>{const r=s.exportName??"agent",n=s.env??{LUNORA_TEST:!0},i=s.secrets??O(),u=F(s.functions??{},i),g=[],f=new H(g),v=new Map,m=c=>{const d=v.get(c);if(d!==void 0)return d;const l=new W(f,c,g);return v.set(c,l),l};let S=0;const k=(c,d)=>{S+=1;const l=d?.instanceId??`wf-${String(S)}`,{model:w,onTokenDelta:T}=d??{};if(w!==void 0){const q={...p,model:w};return E({agent:p,env:n,exportName:r,generate:j(q,n),instanceId:l,onTokenDelta:x=>{x.kind!=="progress"&&T?.(x)},params:c,paths:h,run:u.run,step:m(l),streamGenerate:_(q,n)})}const M=d?.script??s.script??[];return E({agent:p,env:n,exportName:r,generate:P(M),instanceId:l,params:c,paths:h,run:u.run,step:m(l)})};return{awaitWait:(c,d)=>f.awaitWait(c,d),deliver:(c,d,l)=>{f.deliver(c,d,l)},decline:(c,d)=>{const l=u.declineSecret(c,d);l!==void 0&&f.deliver(l.instanceId,`secret:${l.slot}`,{})},dispatches:u.dispatches,expire:(c,d)=>{f.expire(c,d),d.startsWith("secret:")&&u.expireSecretRequest(c,d.slice(7))},journalEntries:()=>[...g],messages:c=>[...u.messages.values()].filter(d=>d.threadKey===c).toSorted((d,l)=>d.seq-l.seq).map(({messageKey:d,threadKey:l,...w})=>w),run:k,secrets:i,supply:async(c,d,l)=>{const w=await u.supplySecret(c,d,l);f.deliver(w.instanceId,`secret:${w.slot}`,{})},thread:c=>u.threads.get(c)}},J=(p,s)=>({text:p,toolCalls:[],...s}),V=(p,s,r,n="")=>({text:n,toolCalls:[{id:p,input:r,name:s}]});export{D as agentHarness,J as finalTurn,V as toolCallTurn};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{randomUUID as o}from"node:crypto";import{assertSecretValueSize as a,clampSecretValueTtl as l}from"@lunora/platform";const n=()=>{const c=new Map;let s=0;return{advanceClock:r=>{s+=r},discard:r=>Promise.resolve(c.delete(r)),put:(r,t)=>{const e=`secret-${o()}`;return Promise.resolve().then(()=>(a(r),c.set(e,{expiresAt:s+l(t.ttlMs),scope:t.scope,value:r}),e))},take:(r,t)=>{const e=c.get(r);return e===void 0||e.expiresAt<=s||e.scope!==t.scope?Promise.resolve(void 0):(e.claimant??=t.claimant,Promise.resolve(e.claimant===t.claimant?e.value:void 0))}}};export{n as createFakeSecretValues};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{MockLanguageModelV4 as n}from"ai/test";const c=()=>{let e=()=>{},r=()=>{};const t=new Promise(a=>{e=a}),o=new Promise(a=>{r=a});return{reached:t,release:()=>{r()},wait:()=>(e(),o)}},s=e=>{let r=0,t=!1;return new ReadableStream({pull:async o=>{if(e.error?.after===r){o.error(e.error.cause);return}e.pause?.after===r&&!t&&(t=!0,await e.pause.gate.wait());const a=e.parts[r];if(a===void 0){o.close();return}r+=1,o.enqueue(a)}},{highWaterMark:0})},i=e=>{const r=[...e];return new n({doStream:()=>{const t=r.shift(),o=t?s(t):new ReadableStream({start:a=>{a.error(new Error("recordedModel: the run made more model calls than turns were recorded."))}});return Promise.resolve({stream:o})}})};export{c as createStreamGate,i as recordedModel};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/testing",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.288",
|
|
4
4
|
"description": "Testing toolkit for Lunora: an in-memory harness for queries, mutations, and actions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -50,16 +50,18 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@lunora/agent": "1.0.0-alpha.
|
|
54
|
-
"@lunora/bindings": "1.0.0-alpha.
|
|
53
|
+
"@lunora/agent": "1.0.0-alpha.233",
|
|
54
|
+
"@lunora/bindings": "1.0.0-alpha.128",
|
|
55
55
|
"@lunora/errors": ">=1.0.0-alpha.51 <2.0.0-0",
|
|
56
|
-
"@lunora/mail": "1.0.0-alpha.
|
|
57
|
-
"@lunora/notify": "1.0.0-alpha.
|
|
58
|
-
"@lunora/
|
|
59
|
-
"@lunora/
|
|
60
|
-
"@lunora/
|
|
56
|
+
"@lunora/mail": "1.0.0-alpha.119",
|
|
57
|
+
"@lunora/notify": "1.0.0-alpha.92",
|
|
58
|
+
"@lunora/platform": "1.0.0-alpha.72",
|
|
59
|
+
"@lunora/queue": "1.0.0-alpha.120",
|
|
60
|
+
"@lunora/scheduler": "1.0.0-alpha.142",
|
|
61
|
+
"@lunora/server": "1.0.0-alpha.225",
|
|
61
62
|
"@lunora/wire": "1.0.0-alpha.1",
|
|
62
|
-
"@visulima/notification": "1.2.0"
|
|
63
|
+
"@visulima/notification": "1.2.0",
|
|
64
|
+
"ai": "7.0.127"
|
|
63
65
|
},
|
|
64
66
|
"peerDependencies": {
|
|
65
67
|
"@playwright/test": "^1.61.1"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{runAgentLoop as v,DEFAULT_AGENT_FUNCTION_PATHS as p}from"@lunora/agent";const k=a=>{const e=[...a];return()=>{const o=e.shift();if(!o)throw new Error("agentHarness: scripted generate exhausted — the run took more turns than the script provided.");return Promise.resolve(o)}};class T{invoked=[];entries=new Map;async do(e,o){const c=this.entries.get(e);if(c)return c.output;this.invoked.push(e);const i=await o();return this.entries.set(e,{output:i}),i}async waitForEvent(e,o){return new Promise(()=>{})}}const K=a=>{const e=new Map,o=new Map,c=[],i=t=>{const s=t?.key,n=t?.instanceId,r=e.get(s);return r?(r.status="running",delete r.error,n!==void 0&&(r.instanceId=n),{outcome:"continued"}):(e.set(s,{agent:t?.agent,instanceId:n,key:s,messageCount:0,owner:t?.owner,status:"running",title:t?.title}),{outcome:"created"})},l=t=>{const s=t?.threadKey,n=t?.messageKey,r=`${s}:${n}`,y=o.get(r);if(y)return{seq:y.seq};const m=e.get(s);if(!m)throw new Error(`agentHarness: append to unknown thread "${s}".`);const w=m.messageCount;return m.messageCount+=1,o.set(r,{...t,seq:w}),{seq:w}},f=t=>{const s=t?.key;return[...o.values()].filter(n=>n.threadKey===s).toSorted((n,r)=>n.seq-r.seq)},d=t=>{const s=e.get(t?.key);if(s)for(const[n,r]of Object.entries(t??{}))n!=="key"&&r!==void 0&&(s[n]=r)},u=t=>{const s=e.get(t?.key);if(!t||!s||s.instanceId!==t.instanceId)return{};for(const[n,r]of Object.entries(t))n!=="key"&&n!=="instanceId"&&r!==void 0&&(s[n]=r);return{}},h=new Map([[p.appendMessage,l],[p.completeRun,u],[p.ensureThread,i],[p.listMessages,f],[p.patchThread,d],...Object.entries(a)]);return{dispatches:c,messages:o,run:(t,s)=>{const n=t.__lunoraRef;c.push({args:s,path:n});const r=h.get(n);if(!r)throw new Error(`agentHarness: unstubbed dispatch "${n}" — pass a \`functions\` handler for it.`);return Promise.resolve(r(s))},threads:e}},I=(a,e)=>{const o=e.exportName??"agent",c=e.env??{LUNORA_TEST:!0},i=K(e.functions??{});let l=0;const f=(d,u)=>{l+=1;const h=u?.instanceId??`wf-${String(l)}`,g=u?.script??e.script;return v({agent:a,env:c,exportName:o,generate:k(g),instanceId:h,params:d,paths:p,run:i.run,step:new T})};return{dispatches:i.dispatches,messages:d=>[...i.messages.values()].filter(u=>u.threadKey===d).toSorted((u,h)=>u.seq-h.seq).map(({messageKey:u,threadKey:h,...g})=>g),run:f,thread:d=>i.threads.get(d)}},x=(a,e)=>({text:a,toolCalls:[],...e}),C=(a,e,o,c="")=>({text:c,toolCalls:[{id:a,input:o,name:e}]});export{I as agentHarness,x as finalTurn,C as toolCallTurn};
|