@b4run/testing 0.12.0 → 0.13.0
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/harness.d.ts +9 -0
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js +14 -1
- package/dist/record-fixtures.d.ts.map +1 -1
- package/dist/record-fixtures.js +60 -1
- package/dist/thread-access-harness.d.ts +8 -1
- package/dist/thread-access-harness.d.ts.map +1 -1
- package/dist/thread-access-harness.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +15 -15
package/dist/harness.d.ts
CHANGED
|
@@ -36,6 +36,15 @@ export interface AgentHarnessOptions {
|
|
|
36
36
|
readonly record?: boolean;
|
|
37
37
|
/** Upstream base URL for record mode (no /v1 suffix). Default https://api.openai.com. */
|
|
38
38
|
readonly recordUpstream?: string;
|
|
39
|
+
/**
|
|
40
|
+
* A JSON Schema the root model's final message must match — the value a
|
|
41
|
+
* Hashbrown client sends as `hashbrown.responseSchema` on every AG-UI run.
|
|
42
|
+
* Bound on every turn exactly as the server binds it, so a scripted, live or
|
|
43
|
+
* recorded run sends the model the same request production does. Only an
|
|
44
|
+
* `agent` route on a provider that supports it can take one; any other
|
|
45
|
+
* route fails the run rather than running unconstrained.
|
|
46
|
+
*/
|
|
47
|
+
readonly responseSchema?: Readonly<Record<string, unknown>>;
|
|
39
48
|
}
|
|
40
49
|
export interface AgentHarness {
|
|
41
50
|
readonly baseUrl: string;
|
package/dist/harness.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,aAAa,
|
|
1
|
+
{"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,aAAa,EAUnB,MAAM,oBAAoB,CAAA;AAG3B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAErE,OAAO,EAAE,KAAK,cAAc,EAAoB,MAAM,iBAAiB,CAAA;AA0BvE,qFAAqF;AACrF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,aAAa,EAAE,CAAA;CAC3C;AAED;;;;GAIG;AACH,MAAM,MAAM,6BAA6B,GACrC,WAAW,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,GACxC,CAAC,CACC,GAAG,EAAE,mBAAmB,KAEtB,WAAW,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,GACxC,SAAS,GACT,OAAO,CAAC,WAAW,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAA;AAEtE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAA;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,6BAA6B,CAAA;IAC1D;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;IACvB,uFAAuF;IACvF,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB,yFAAyF;IACzF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CAC5D;AAYD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,GAAG,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,UAAU,GAAG,aAAa,CAAA;KAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;IAC5F,MAAM,CAAC,IAAI,EAAE;QACX,MAAM,EAAE,SAAS,aAAa,EAAE,CAAA;QAChC,QAAQ,CAAC,EAAE,UAAU,GAAG,aAAa,CAAA;KACtC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;IAC3B,KAAK,IAAI,IAAI,CAAA;IACb,KAAK,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxE,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtC,4FAA4F;IAC5F,mBAAmB,IAAI,UAAU,CAAA;CAClC;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,YAAY,CAAC,CA2N5F"}
|
package/dist/harness.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { __resetMaterializedAgentsForTests, __resetRouteLoadCachesForTests, createRuntimeRegistry, readPendingInterrupts, resolveCheckpointer, resolvePendingResume, resolveSandboxManager, runTypegen, streamResolvedRoute, } from "@b4run/cli/runtime";
|
|
2
|
+
import { __resetMaterializedAgentsForTests, __resetRouteLoadCachesForTests, createRuntimeRegistry, readPendingInterrupts, readResponseFormat, resolveCheckpointer, resolvePendingResume, resolveSandboxManager, runTypegen, streamResolvedRoute, } from "@b4run/cli/runtime";
|
|
3
3
|
import { __clearB4ConfigCacheForTests } from "@b4run/core";
|
|
4
4
|
import { discoverRoutes } from "@b4run/core/node";
|
|
5
5
|
import { createAimock } from "./aimock-runner.js";
|
|
@@ -37,6 +37,18 @@ async function resolveMiddlewareContext(option, run) {
|
|
|
37
37
|
export async function createAgentHarness(options) {
|
|
38
38
|
const live = options.live ?? false;
|
|
39
39
|
const record = options.record ?? false;
|
|
40
|
+
const responseSchema = options.responseSchema;
|
|
41
|
+
if (responseSchema !== undefined &&
|
|
42
|
+
(typeof responseSchema !== "object" || responseSchema === null || Array.isArray(responseSchema))) {
|
|
43
|
+
throw new Error("createAgentHarness: `responseSchema` must be a JSON Schema object");
|
|
44
|
+
}
|
|
45
|
+
// Read through the server's own parser so the bound format — including the
|
|
46
|
+
// provider-facing schema name — is the one an AG-UI run would carry.
|
|
47
|
+
const responseFormatResult = readResponseFormat(responseSchema !== undefined ? { hashbrown: { responseSchema } } : undefined);
|
|
48
|
+
if (!responseFormatResult.ok) {
|
|
49
|
+
throw new Error(`createAgentHarness: ${responseFormatResult.message}`);
|
|
50
|
+
}
|
|
51
|
+
const responseFormat = responseFormatResult.responseFormat;
|
|
40
52
|
// Guard: live mode requires a real API key before doing anything else.
|
|
41
53
|
if (live && !process.env.OPENAI_API_KEY) {
|
|
42
54
|
throw new Error("createAgentHarness({ live: true }) requires OPENAI_API_KEY to be set in the environment");
|
|
@@ -144,6 +156,7 @@ export async function createAgentHarness(options) {
|
|
|
144
156
|
...(sandboxManager ? { sandboxManager } : {}),
|
|
145
157
|
...(resolvedResume ? { resume: resolvedResume } : {}),
|
|
146
158
|
...(middlewareContext !== undefined ? { middlewareContext } : {}),
|
|
159
|
+
...(responseFormat !== undefined ? { responseFormat } : {}),
|
|
147
160
|
};
|
|
148
161
|
const stream = streamResolvedRoute(streamArgs);
|
|
149
162
|
const result = await collectRunResult(stream, threadId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"record-fixtures.d.ts","sourceRoot":"","sources":["../src/record-fixtures.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"record-fixtures.d.ts","sourceRoot":"","sources":["../src/record-fixtures.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAiB,cAAc,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAErF,2FAA2F;AAC3F,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE;QAChB,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC;YAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;YACrB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;SAC1B,CAAC,CAAA;KACH,CAAA;IACD,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAA;CAClC;AAaD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,GAAG,UAAU,CAcjF"}
|
package/dist/record-fixtures.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LLMock } from "@copilotkit/aimock";
|
|
1
2
|
function firstUserMessage(req) {
|
|
2
3
|
for (const m of req.messages ?? []) {
|
|
3
4
|
if (m.role === "user" && typeof m.content === "string")
|
|
@@ -17,7 +18,7 @@ function hasToolResult(req) {
|
|
|
17
18
|
* thread); `hasToolResult` is whether a tool-role message is already present.
|
|
18
19
|
*/
|
|
19
20
|
export function recordingsToFixtures(recordings) {
|
|
20
|
-
|
|
21
|
+
const fixtures = recordings.map((rec, turnIndex) => {
|
|
21
22
|
const userMessage = firstUserMessage(rec.request);
|
|
22
23
|
return {
|
|
23
24
|
match: {
|
|
@@ -28,4 +29,62 @@ export function recordingsToFixtures(recordings) {
|
|
|
28
29
|
response: rec.response,
|
|
29
30
|
};
|
|
30
31
|
});
|
|
32
|
+
assertReplayable(fixtures);
|
|
33
|
+
return fixtures;
|
|
34
|
+
}
|
|
35
|
+
/** aimock's own load-time error for one fixture, or undefined when it loads. */
|
|
36
|
+
function replayError(fixtures) {
|
|
37
|
+
try {
|
|
38
|
+
// The same call replay makes (see createAimock), on a mock that is never
|
|
39
|
+
// started — so a recording is held to exactly the rules its replay will be.
|
|
40
|
+
new LLMock({ port: 0 }).addFixturesFromJSON(fixtures);
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return error instanceof Error ? error.message : String(error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Refuse a recording that replay would refuse (#778). aimock rejects a
|
|
49
|
+
* fixture whose `content` is an empty string, because its recorder writes one
|
|
50
|
+
* for several different upstream outcomes — a genuinely empty reply, a
|
|
51
|
+
* refusal it does not capture, a reply cut off at the token limit — and a
|
|
52
|
+
* tape cannot say which. Failing here, while the recording is being made,
|
|
53
|
+
* names the turn instead of leaving a tape that breaks on its next replay.
|
|
54
|
+
*/
|
|
55
|
+
function assertReplayable(fixtures) {
|
|
56
|
+
const whole = replayError(fixtures);
|
|
57
|
+
if (whole === undefined)
|
|
58
|
+
return;
|
|
59
|
+
const turns = fixtures.flatMap((fixture, turnIndex) => {
|
|
60
|
+
const error = replayError([fixture]);
|
|
61
|
+
if (error === undefined)
|
|
62
|
+
return [];
|
|
63
|
+
const detail = describeValidationError(error);
|
|
64
|
+
const where = fixture.match.userMessage !== undefined
|
|
65
|
+
? `turn ${turnIndex} of ${JSON.stringify(fixture.match.userMessage)}`
|
|
66
|
+
: `turn ${turnIndex}`;
|
|
67
|
+
return [` - ${where}: ${detail}`];
|
|
68
|
+
});
|
|
69
|
+
const lines = turns.length > 0 ? turns.join("\n") : ` - ${describeValidationError(whole)}`;
|
|
70
|
+
throw new Error(`Recorded fixtures would not replay: aimock rejects them on load.\n${lines}\n` +
|
|
71
|
+
"An empty assistant message usually means the model refused (a strict response " +
|
|
72
|
+
"schema reports refusals outside `content`), ran out of output tokens, or was " +
|
|
73
|
+
"given nothing left to do. If the run's answer is a tool call, export " +
|
|
74
|
+
"`returnDirect = true` from that tool so the run ends on its result instead of " +
|
|
75
|
+
"asking the model for a closing message.");
|
|
76
|
+
}
|
|
77
|
+
/** Reduce aimock's `Fixture validation failed: [...]` to its messages. */
|
|
78
|
+
function describeValidationError(error) {
|
|
79
|
+
const prefix = "Fixture validation failed: ";
|
|
80
|
+
if (!error.startsWith(prefix))
|
|
81
|
+
return error;
|
|
82
|
+
try {
|
|
83
|
+
const issues = JSON.parse(error.slice(prefix.length));
|
|
84
|
+
const messages = issues.map((issue) => issue.message).filter((m) => typeof m === "string");
|
|
85
|
+
return messages.length > 0 ? messages.join("; ") : error;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return error;
|
|
89
|
+
}
|
|
31
90
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ThreadAccessPolicy, ThreadAccessResult, ThreadAction, ThreadOperation, ThreadSubject } from "@b4run/sdk";
|
|
1
|
+
import type { ThreadAccessPolicy, ThreadAccessRequestedWorkspace, ThreadAccessResult, ThreadAction, ThreadOperation, ThreadSubject } from "@b4run/sdk";
|
|
2
2
|
/** A partial `ThreadAccessRequest`: only `action` is required. */
|
|
3
3
|
export interface ThreadAccessCheckSpec {
|
|
4
4
|
readonly action: ThreadAction;
|
|
@@ -10,6 +10,13 @@ export interface ThreadAccessCheckSpec {
|
|
|
10
10
|
readonly method?: string;
|
|
11
11
|
readonly url?: string;
|
|
12
12
|
readonly requestedMetadata?: Readonly<Record<string, unknown>>;
|
|
13
|
+
/**
|
|
14
|
+
* The workspace the request stages or chooses — see
|
|
15
|
+
* `ThreadAccessRequest.requestedWorkspace`: `{ sourceDigest }` for a
|
|
16
|
+
* `workspace.source.put`, the whole reference for a `thread.create` that
|
|
17
|
+
* names one.
|
|
18
|
+
*/
|
|
19
|
+
readonly requestedWorkspace?: ThreadAccessRequestedWorkspace;
|
|
13
20
|
/**
|
|
14
21
|
* Whether this request continues a parked turn — see
|
|
15
22
|
* `ThreadAccessRequest.resuming`. Defaults to `false`, the value every
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thread-access-harness.d.ts","sourceRoot":"","sources":["../src/thread-access-harness.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,kBAAkB,EAElB,kBAAkB,EAClB,YAAY,EACZ,eAAe,EACf,aAAa,EACd,MAAM,YAAY,CAAA;AAEnB,kEAAkE;AAClE,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;IAC7B,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,CAAC,EAAE,eAAe,CAAA;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAA;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACnD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,iBAAiB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IAC9D;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,KAAK,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAA;CAChE;AAeD;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE;IACjD,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAA;CACpC,GAAG,mBAAmB,
|
|
1
|
+
{"version":3,"file":"thread-access-harness.d.ts","sourceRoot":"","sources":["../src/thread-access-harness.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,kBAAkB,EAElB,8BAA8B,EAC9B,kBAAkB,EAClB,YAAY,EACZ,eAAe,EACf,aAAa,EACd,MAAM,YAAY,CAAA;AAEnB,kEAAkE;AAClE,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;IAC7B,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,CAAC,EAAE,eAAe,CAAA;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAA;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACnD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,iBAAiB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IAC9D;;;;;OAKG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,8BAA8B,CAAA;IAC5D;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,KAAK,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAA;CAChE;AAeD;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE;IACjD,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAA;CACpC,GAAG,mBAAmB,CAoBtB"}
|
|
@@ -30,6 +30,7 @@ export function createThreadAccessHarness(options) {
|
|
|
30
30
|
method: spec.method ?? defaultMethod(spec.action),
|
|
31
31
|
operation,
|
|
32
32
|
requestedMetadata: spec.requestedMetadata,
|
|
33
|
+
requestedWorkspace: spec.requestedWorkspace,
|
|
33
34
|
resuming: spec.resuming ?? false,
|
|
34
35
|
thread: spec.thread,
|
|
35
36
|
threadId: spec.threadId,
|