@womp/spark-agent 6.1.1 → 6.3.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/CHANGELOG.md +53 -0
- package/README.md +30 -0
- package/dist/capabilities/flow.js +2 -1
- package/dist/client.d.ts +4 -2
- package/dist/client.js +13 -1
- package/dist/goals.d.ts +46 -3
- package/dist/goals.js +23 -18
- package/dist/intent-bench-cases.d.ts +42 -0
- package/dist/intent-bench-cases.js +149 -0
- package/dist/intent-bench-cli.d.ts +2 -0
- package/dist/intent-bench-cli.js +122 -0
- package/dist/intent-bench-flow-catalog.d.ts +1 -0
- package/dist/intent-bench-flow-catalog.js +4 -0
- package/dist/intent-bench.d.ts +66 -0
- package/dist/intent-bench.js +555 -0
- package/dist/model-history.js +14 -0
- package/dist/prompt.d.ts +1 -1
- package/dist/prompt.js +8 -3
- package/dist/server/express.js +9 -0
- package/dist/server/goal-evaluator.d.ts +6 -1
- package/dist/server/goal-evaluator.js +14 -4
- package/dist/server/goal-proposer.d.ts +15 -0
- package/dist/server/goal-proposer.js +64 -0
- package/dist/server/index.d.ts +3 -2
- package/dist/server/index.js +3 -2
- package/dist/server/service.d.ts +10 -1
- package/dist/server/service.js +63 -11
- package/dist/server/standalone.d.ts +8 -0
- package/dist/server/standalone.js +66 -35
- package/dist/ui.d.ts +15 -2
- package/dist/ui.js +7 -1
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,59 @@ Notable changes to `@womp/spark-agent`, newest first. Versions follow
|
|
|
4
4
|
[Semantic Versioning](https://semver.org/). Add an entry with each version bump; describe what
|
|
5
5
|
changed for hosts and users, and how to migrate across breaking changes.
|
|
6
6
|
|
|
7
|
+
## 6.3.0 — 2026-10-09
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Goals start from the conditions in an ordinary message. `SparkAgentService.proposeGoal`,
|
|
12
|
+
`POST /conversations/:id/goal/proposal` and `SparkChatTransport.proposeGoal` return a title and
|
|
13
|
+
the message's success conditions, each marked `measurable` or left to the user's review. Hosts
|
|
14
|
+
supply `SparkAgentServiceOptions.proposeGoal`; `createSparkGoalProposer` is the shared one.
|
|
15
|
+
- The goal `set` command accepts `title` and `conditions`. The service attaches them to the
|
|
16
|
+
initiating message as a `data-goal` part, the evaluator judges each measurable condition after
|
|
17
|
+
every try, and `SparkGoal.conditions` records `met` and `reason`. A goal with conditions completes
|
|
18
|
+
only when no measurable condition is unmet. `SparkChatTransport.sendMessages` forwards
|
|
19
|
+
`body.goal`, so a confirmed goal is sent with its message.
|
|
20
|
+
- `SparkGoal.tries` keeps each check a goal with conditions has had: the try's number, the run that
|
|
21
|
+
made it, the evaluator's reason and every condition's result, so a host can show a goal's history.
|
|
22
|
+
- The goal `update_budget` command accepts `maxTurns` as well as `creditBudget`, either or both, so
|
|
23
|
+
a goal that used its tries can be given more and resumed.
|
|
24
|
+
- `GOAL_CONFIRMATION_DEFAULTS` (5 tries, 2,000 credits) and `MAX_GOAL_LENGTH` from
|
|
25
|
+
`@womp/spark-agent/ui`, for the confirmation a host shows and the objective it sends.
|
|
26
|
+
- The standalone UI reads each request's conditions, asks the user to confirm them with a tries and
|
|
27
|
+
a credit cap or to try once, and shows the goal's progress. It estimates credits from model cost
|
|
28
|
+
at a development rate; it does not bill.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- A goal's initiating user message can carry a `data-goal` part. Hosts that render or mirror user
|
|
33
|
+
message parts should show it as the confirmed goal or skip it. A host that gives the model its
|
|
34
|
+
own copy of the message, as Crow does from the Swan thread, must keep the part on that copy:
|
|
35
|
+
it is how the worker reads the conditions.
|
|
36
|
+
|
|
37
|
+
### Removed
|
|
38
|
+
|
|
39
|
+
- `parseSparkGoalCommand`. A goal starts from the confirmation, not from a typed `/goal`; hosts
|
|
40
|
+
send the `set`, `pause`, `resume` and `clear` commands from their own controls.
|
|
41
|
+
|
|
42
|
+
## 6.2.0 — 2026-10-09
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- `intent-bench` CLI: measures which route Spark takes (scene, OpenSCAD, Geometry Nodes, image,
|
|
47
|
+
image edit, mesh, image-to-mesh, Flow, import, or a reply) for the build-intent QA requests
|
|
48
|
+
under the current creation rules, preview follow-ups and attached-photo cases, in parallel over
|
|
49
|
+
fixtures, with gpt-6-luna by default. See the README.
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- An attached image without a request gets a question about what to make of it, not a generation.
|
|
54
|
+
Turning an attached image into 3D generates the mesh from it directly, with no new preview.
|
|
55
|
+
- Characters, creatures, people, food, sculptures, specific vehicles and branded items preview
|
|
56
|
+
first even when simple or stylized, unless the request names a method or has a source image.
|
|
57
|
+
- `flow-graphs` (v5) builds the requested graph with the catalog's nodes when an input, such as a
|
|
58
|
+
conversation upload, cannot be expressed in source, and tells the user which input to set.
|
|
59
|
+
|
|
7
60
|
## 6.1.1 — 2026-10-08
|
|
8
61
|
|
|
9
62
|
### Fixed
|
package/README.md
CHANGED
|
@@ -138,6 +138,7 @@ Closing or reloading the page only detaches the SSE subscriber. It does not canc
|
|
|
138
138
|
POST /agent/conversations/:conversationId/messages
|
|
139
139
|
PATCH /agent/conversations/:conversationId/settings
|
|
140
140
|
POST /agent/conversations/:conversationId/goal
|
|
141
|
+
POST /agent/conversations/:conversationId/goal/proposal
|
|
141
142
|
GET /agent/runs/:runId/stream?after=0
|
|
142
143
|
POST /agent/runs/:runId/cancel
|
|
143
144
|
```
|
|
@@ -146,6 +147,15 @@ Conversation settings contain `modelId` and `reasoningEffort`; save them with
|
|
|
146
147
|
`transport.saveConversationSettings(id, settings)`. The goal endpoint accepts `set`, `pause`,
|
|
147
148
|
`resume`, `clear`, and `update_budget` commands.
|
|
148
149
|
|
|
150
|
+
A goal starts from an ordinary message. `transport.proposeGoal(id, message, settings)` returns a
|
|
151
|
+
title and the success conditions read from the message, each marked `measurable` or left to the
|
|
152
|
+
user's review; it stores and starts nothing. After the user confirms them with a tries cap
|
|
153
|
+
(`maxTurns`) and a credit cap, send the message with `body.goal` set to a `set` command carrying
|
|
154
|
+
`title`, `conditions`, `maxTurns` and `creditBudget`. The service attaches the confirmed conditions
|
|
155
|
+
to that message as a `data-goal` part, evaluates each measurable condition after every try, and
|
|
156
|
+
records the result on `goal.conditions`. Send the message without `body.goal` to try once. Hosts
|
|
157
|
+
enable proposals with `SparkAgentServiceOptions.proposeGoal`, such as `createSparkGoalProposer`.
|
|
158
|
+
|
|
149
159
|
The SSE response contains ordered persistence envelopes around standard AI SDK chunks:
|
|
150
160
|
|
|
151
161
|
```ts
|
|
@@ -262,6 +272,26 @@ Progress goes to stderr; one JSON result goes to stdout. Use `yarn --silent agen
|
|
|
262
272
|
stdout. Exit codes: 0 completed, 1 run/transport/export failure, 2 invalid arguments, 3 requires
|
|
263
273
|
user action, 130 interrupted.
|
|
264
274
|
|
|
275
|
+
## Measure intent routing
|
|
276
|
+
|
|
277
|
+
`yarn intent-bench` checks which route Spark takes for the requests in
|
|
278
|
+
`src/intent-bench-cases.ts`: editable scene, OpenSCAD, Geometry Nodes, image, image edit, mesh,
|
|
279
|
+
image-to-mesh, Flow, import, or a reply without acting. It needs no server or renderer. Each run
|
|
280
|
+
gets Swan's skills over fixtures: an empty scene, the case's attachments and earlier turns, and
|
|
281
|
+
Flow open or closed. Photo cases download their image from Womp's media host. The first call that would change the scene, generate, write a graph or
|
|
282
|
+
import is recorded, and the run stops before it takes effect. Material edits do not decide a
|
|
283
|
+
route, since Geometry Nodes prepares its materials in the scene first.
|
|
284
|
+
|
|
285
|
+
```sh
|
|
286
|
+
yarn intent-bench # every case 3 times on gpt-6-luna, 24 at once
|
|
287
|
+
yarn intent-bench --only photo-only,F1 --repeat 10 --model gpt-6-astra
|
|
288
|
+
yarn intent-bench --list
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
It prints each case's routes and pass rate and writes `report.json`, with every run's skills,
|
|
292
|
+
calls, deciding call and reply, under `.spark/intent-bench/`. It exits 1 when any run took a route
|
|
293
|
+
the case does not expect. Model calls are paid; the full set costs about $0.10 on gpt-6-luna.
|
|
294
|
+
|
|
265
295
|
## Run cost estimates
|
|
266
296
|
|
|
267
297
|
The chat displays estimated API token cost in USD after every completed model step, from the
|
|
@@ -11,12 +11,13 @@ export function createFlowCapability(host, catalog) {
|
|
|
11
11
|
skills: [
|
|
12
12
|
{
|
|
13
13
|
name: "flow-graphs",
|
|
14
|
-
version: "
|
|
14
|
+
version: "5",
|
|
15
15
|
description: "Read and edit the open Workbench Flow pipeline: image/mesh generation, processing, and viewers.",
|
|
16
16
|
instructions: `Use womp.flow for the open Workbench pipeline.
|
|
17
17
|
Read the current graph and flow-graphs-reference before writing. Supply the complete edited s-expression document and the latest revision. Preserve unrelated nodes and stable node IDs. Omitting a node deletes it; empty source clears the graph.
|
|
18
18
|
Existing node configuration, edge identity, layout, runtime metadata, and generated outputs are retained for unchanged node identities/types. Scalar inputs represented in source are replaced by your document. The host retains preservedInputs for those same nodes. Non-private preserved inputs can be explicitly replaced in the document; never recreate asset objects or cached values as JSON strings. Private settings remain editor-owned: preserve existing private connections unchanged between retained nodes and do not introduce new private literals or connections.
|
|
19
19
|
If read returns source null, explain the diagnostics. Do not replace an unreadable graph as though it were empty.
|
|
20
|
+
Build the requested graph from the catalog's nodes even when an input cannot be expressed in source, such as a conversation upload or an editor-owned image: leave that input unconnected and tell the user which node input to set in the editor.
|
|
20
21
|
The Flow canvas must be open. If unavailable, tell the user to open Flow or Split in a project, or Nodes in the sidebar, and wait for their next message. Never claim a graph was created after a failed call.
|
|
21
22
|
Writes validate types, preserve the runner lifecycle, and await commit. They edit the graph; they do not report that generation jobs completed. Repair compiler errors in this same conversation with a fresh read. On committed/unknown receipts after failure, inspect the graph before retrying; do not replay blindly.`,
|
|
22
23
|
declarations: WOMP_FLOW_DECLARATIONS,
|
package/dist/client.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ChatRequestOptions, ChatTransport, UIMessageChunk } from "ai";
|
|
2
2
|
import type { SparkRunSettings } from "./server/types.js";
|
|
3
|
-
import type { SparkGoal, SparkGoalCommand } from "./goals.js";
|
|
4
|
-
export type { SparkGoal, SparkGoalCommand } from "./goals.js";
|
|
3
|
+
import type { SparkGoal, SparkGoalCommand, SparkGoalProposal } from "./goals.js";
|
|
4
|
+
export type { SparkGoal, SparkGoalCommand, SparkGoalCondition, SparkGoalProposal, SparkGoalTry } from "./goals.js";
|
|
5
5
|
import type { SparkConversation, SparkConversationSummary, SparkRun } from "./server/types.js";
|
|
6
6
|
import type { SparkUIMessage } from "./ui.js";
|
|
7
7
|
export type { SparkConversation, SparkConversationSummary, SparkRun, SparkRunSettings, } from "./server/types.js";
|
|
@@ -44,6 +44,8 @@ export declare class SparkChatTransport implements ChatTransport<SparkUIMessage>
|
|
|
44
44
|
goal?: SparkGoal;
|
|
45
45
|
run?: SparkRun;
|
|
46
46
|
}>;
|
|
47
|
+
/** The success conditions read from a message, to confirm before sending it as a goal. */
|
|
48
|
+
proposeGoal(id: string, message: SparkUIMessage, settings: SparkRunSettings, signal?: AbortSignal): Promise<SparkGoalProposal>;
|
|
47
49
|
getGoal(id: string): Promise<SparkGoal | undefined>;
|
|
48
50
|
getRun(id: string): Promise<SparkRun>;
|
|
49
51
|
saveConversationSettings(id: string, settings: SparkRunSettings): Promise<void>;
|
package/dist/client.js
CHANGED
|
@@ -51,7 +51,8 @@ export class SparkChatTransport {
|
|
|
51
51
|
: options.messages.find(({ id }) => id === options.messageId) ?? options.messages.at(-1);
|
|
52
52
|
if (message === undefined)
|
|
53
53
|
throw new Error("Spark Agent requires a message to send.");
|
|
54
|
-
const
|
|
54
|
+
const requestBody = options.body;
|
|
55
|
+
const settings = this.settingsFrom(requestBody);
|
|
55
56
|
const response = await this.request(`/conversations/${encodeURIComponent(options.chatId)}/messages`, {
|
|
56
57
|
method: "POST",
|
|
57
58
|
body: JSON.stringify({
|
|
@@ -59,6 +60,8 @@ export class SparkChatTransport {
|
|
|
59
60
|
message,
|
|
60
61
|
messageId: options.messageId,
|
|
61
62
|
settings,
|
|
63
|
+
// A confirmed goal: the message starts it, and the run is its first try.
|
|
64
|
+
...(requestBody?.goal === undefined ? {} : { goal: requestBody.goal }),
|
|
62
65
|
}),
|
|
63
66
|
signal: options.abortSignal,
|
|
64
67
|
}, options.headers);
|
|
@@ -126,6 +129,15 @@ export class SparkChatTransport {
|
|
|
126
129
|
throw await responseError(response);
|
|
127
130
|
return response.json();
|
|
128
131
|
}
|
|
132
|
+
/** The success conditions read from a message, to confirm before sending it as a goal. */
|
|
133
|
+
async proposeGoal(id, message, settings, signal) {
|
|
134
|
+
const response = await this.request(`/conversations/${encodeURIComponent(id)}/goal/proposal`, {
|
|
135
|
+
method: "POST", body: JSON.stringify({ message, settings }), signal,
|
|
136
|
+
});
|
|
137
|
+
if (!response.ok)
|
|
138
|
+
throw await responseError(response);
|
|
139
|
+
return (await response.json()).proposal;
|
|
140
|
+
}
|
|
129
141
|
async getGoal(id) {
|
|
130
142
|
const response = await this.request(`/conversations/${encodeURIComponent(id)}/goal`, { method: "GET" });
|
|
131
143
|
if (!response.ok)
|
package/dist/goals.d.ts
CHANGED
|
@@ -1,9 +1,48 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { SparkRunSettings } from "./server/types.js";
|
|
3
3
|
export declare const MAX_GOAL_LENGTH = 4000;
|
|
4
|
+
export declare const MAX_GOAL_CONDITIONS = 12;
|
|
5
|
+
export declare const MAX_GOAL_CONDITION_LENGTH = 200;
|
|
6
|
+
/** One success condition of a goal, taken from what the user asked for. */
|
|
7
|
+
export declare const sparkGoalConditionSchema: z.ZodObject<{
|
|
8
|
+
text: z.ZodString;
|
|
9
|
+
measurable: z.ZodBoolean;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
/** What a goal's evaluation last found for a condition; unset until one has run. */
|
|
12
|
+
export type SparkGoalCondition = z.infer<typeof sparkGoalConditionSchema> & {
|
|
13
|
+
met?: boolean;
|
|
14
|
+
reason?: string;
|
|
15
|
+
};
|
|
16
|
+
/** What the check after one try found: its number, the run that made it, and each condition's result. */
|
|
17
|
+
export type SparkGoalTry = {
|
|
18
|
+
/** One-based, as the goal counts its tries. */
|
|
19
|
+
turn: number;
|
|
20
|
+
runId: string;
|
|
21
|
+
reason: string;
|
|
22
|
+
/** In the order of the goal's conditions; a condition left to the user has no result. */
|
|
23
|
+
conditions: Pick<SparkGoalCondition, "met" | "reason">[];
|
|
24
|
+
};
|
|
25
|
+
/** The conditions read from a user message, for the user to confirm before a goal starts. */
|
|
26
|
+
export declare const sparkGoalProposalSchema: z.ZodObject<{
|
|
27
|
+
title: z.ZodString;
|
|
28
|
+
conditions: z.ZodArray<z.ZodObject<{
|
|
29
|
+
text: z.ZodString;
|
|
30
|
+
measurable: z.ZodBoolean;
|
|
31
|
+
}, z.core.$strip>>;
|
|
32
|
+
}, z.core.$strip>;
|
|
33
|
+
export type SparkGoalProposal = z.infer<typeof sparkGoalProposalSchema>;
|
|
34
|
+
/** The caps a goal confirmation offers before the user changes them. */
|
|
35
|
+
export declare const GOAL_CONFIRMATION_DEFAULTS: {
|
|
36
|
+
readonly tries: 5;
|
|
37
|
+
readonly credits: 2000;
|
|
38
|
+
};
|
|
4
39
|
export type SparkGoal = {
|
|
5
40
|
id: string;
|
|
6
41
|
objective: string;
|
|
42
|
+
title?: string;
|
|
43
|
+
conditions?: SparkGoalCondition[];
|
|
44
|
+
/** The checks made so far, oldest first. A try that ended before its check has none. */
|
|
45
|
+
tries?: SparkGoalTry[];
|
|
7
46
|
status: "active" | "paused" | "completed" | "blocked" | "budget_limited";
|
|
8
47
|
settings: SparkRunSettings;
|
|
9
48
|
createdAt: string;
|
|
@@ -24,12 +63,18 @@ export type SparkGoal = {
|
|
|
24
63
|
export declare const sparkGoalCommandSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
25
64
|
action: z.ZodLiteral<"set">;
|
|
26
65
|
objective: z.ZodString;
|
|
66
|
+
title: z.ZodOptional<z.ZodString>;
|
|
67
|
+
conditions: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
68
|
+
text: z.ZodString;
|
|
69
|
+
measurable: z.ZodBoolean;
|
|
70
|
+
}, z.core.$strip>>>;
|
|
27
71
|
creditBudget: z.ZodOptional<z.ZodNumber>;
|
|
28
72
|
maxTurns: z.ZodOptional<z.ZodNumber>;
|
|
29
73
|
}, z.core.$strip>, z.ZodObject<{
|
|
30
74
|
action: z.ZodLiteral<"update_budget">;
|
|
31
75
|
goalId: z.ZodString;
|
|
32
|
-
creditBudget: z.ZodNumber
|
|
76
|
+
creditBudget: z.ZodOptional<z.ZodNumber>;
|
|
77
|
+
maxTurns: z.ZodOptional<z.ZodNumber>;
|
|
33
78
|
}, z.core.$strip>, z.ZodObject<{
|
|
34
79
|
action: z.ZodEnum<{
|
|
35
80
|
status: "status";
|
|
@@ -40,5 +85,3 @@ export declare const sparkGoalCommandSchema: z.ZodDiscriminatedUnion<[z.ZodObjec
|
|
|
40
85
|
}, z.core.$strip>], "action">;
|
|
41
86
|
export type SparkGoalCommand = z.infer<typeof sparkGoalCommandSchema>;
|
|
42
87
|
export declare const DEFAULT_GOAL_CREDITS = 50000;
|
|
43
|
-
/** Only the explicit slash command starts a goal; prose inside an ordinary message does not. */
|
|
44
|
-
export declare function parseSparkGoalCommand(text: string): SparkGoalCommand | undefined;
|
package/dist/goals.js
CHANGED
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export const MAX_GOAL_LENGTH = 4_000;
|
|
3
|
+
export const MAX_GOAL_CONDITIONS = 12;
|
|
4
|
+
export const MAX_GOAL_CONDITION_LENGTH = 200;
|
|
5
|
+
/** One success condition of a goal, taken from what the user asked for. */
|
|
6
|
+
export const sparkGoalConditionSchema = z.object({
|
|
7
|
+
text: z.string().trim().min(1).max(MAX_GOAL_CONDITION_LENGTH),
|
|
8
|
+
/**
|
|
9
|
+
* Whether the result can be checked objectively from tool results. The goal is
|
|
10
|
+
* met when every measurable condition is; the rest are left to the user's review.
|
|
11
|
+
*/
|
|
12
|
+
measurable: z.boolean(),
|
|
13
|
+
});
|
|
14
|
+
/** The conditions read from a user message, for the user to confirm before a goal starts. */
|
|
15
|
+
export const sparkGoalProposalSchema = z.object({
|
|
16
|
+
title: z.string().trim().min(1).max(80),
|
|
17
|
+
conditions: z.array(sparkGoalConditionSchema).max(MAX_GOAL_CONDITIONS),
|
|
18
|
+
});
|
|
19
|
+
/** The caps a goal confirmation offers before the user changes them. */
|
|
20
|
+
export const GOAL_CONFIRMATION_DEFAULTS = { tries: 5, credits: 2_000 };
|
|
3
21
|
export const sparkGoalCommandSchema = z.discriminatedUnion("action", [
|
|
4
22
|
z.object({
|
|
5
23
|
action: z.literal("set"),
|
|
6
24
|
objective: z.string().trim().min(1).max(MAX_GOAL_LENGTH),
|
|
25
|
+
title: sparkGoalProposalSchema.shape.title.optional(),
|
|
26
|
+
conditions: sparkGoalProposalSchema.shape.conditions.min(1).optional(),
|
|
7
27
|
creditBudget: z.number().int().positive().max(1_000_000_000).optional(),
|
|
8
28
|
maxTurns: z.number().int().min(1).max(100).optional(),
|
|
9
29
|
}),
|
|
30
|
+
// Raises or lowers a goal's caps; at least one of them.
|
|
10
31
|
z.object({
|
|
11
32
|
action: z.literal("update_budget"),
|
|
12
33
|
goalId: z.string().min(1),
|
|
13
|
-
creditBudget: z.number().int().positive().max(1_000_000_000),
|
|
34
|
+
creditBudget: z.number().int().positive().max(1_000_000_000).optional(),
|
|
35
|
+
maxTurns: z.number().int().min(1).max(100).optional(),
|
|
14
36
|
}),
|
|
15
37
|
z.object({ action: z.enum(["status", "pause", "resume", "clear"]) }),
|
|
16
38
|
]);
|
|
17
39
|
export const DEFAULT_GOAL_CREDITS = 50_000;
|
|
18
|
-
/** Only the explicit slash command starts a goal; prose inside an ordinary message does not. */
|
|
19
|
-
export function parseSparkGoalCommand(text) {
|
|
20
|
-
const match = /^\/goal(?:\s+([\s\S]*))?$/i.exec(text.trim());
|
|
21
|
-
if (!match)
|
|
22
|
-
return undefined;
|
|
23
|
-
let objective = (match[1] ?? "").trim();
|
|
24
|
-
if (["status", "pause", "resume", "clear"].includes(objective.toLowerCase()))
|
|
25
|
-
return { action: objective.toLowerCase() };
|
|
26
|
-
objective = objective.replace(/^["Ҭ]([\s\S]*)["Ӭ]$/, "$1").trim();
|
|
27
|
-
const cap = /(?:[,;]?[\s]+)?cap\s+at\s+(\S+)\s+credits[.!]?$/i.exec(objective);
|
|
28
|
-
if (cap && !/^(?:\d+|\d{1,3}(?:,\d{3})+|\d{1,3}(?:\.\d{3})+)$/.test(cap[1]))
|
|
29
|
-
throw new Error("Use a whole number of credits, for example cap at 1000 credits.");
|
|
30
|
-
const creditBudget = cap ? Number(cap[1].replace(/[.,]/g, "")) : DEFAULT_GOAL_CREDITS;
|
|
31
|
-
if (cap)
|
|
32
|
-
objective = objective.slice(0, cap.index).trim().replace(/^["Ҭ]([\s\S]*)["Ӭ]$/, "$1").trim();
|
|
33
|
-
return sparkGoalCommandSchema.parse({ action: "set", objective, creditBudget });
|
|
34
|
-
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { OpenScadTarget } from "./capabilities.js";
|
|
2
|
+
/** The first action a run commits to, or how it ended without one. */
|
|
3
|
+
export declare const INTENT_ROUTES: readonly ["scene", "openscad", "geometry-nodes", "image", "image-edit", "mesh", "image-to-mesh", "flow", "import", "reply", "undecided", "error"];
|
|
4
|
+
export type IntentRoute = (typeof INTENT_ROUTES)[number];
|
|
5
|
+
/** A photo downloaded from `url`, or a generated solid color. */
|
|
6
|
+
export type IntentImage = {
|
|
7
|
+
url: string;
|
|
8
|
+
} | {
|
|
9
|
+
name: string;
|
|
10
|
+
color: [number, number, number];
|
|
11
|
+
};
|
|
12
|
+
export type IntentTurn = {
|
|
13
|
+
role: "user";
|
|
14
|
+
text?: string;
|
|
15
|
+
images?: IntentImage[];
|
|
16
|
+
}
|
|
17
|
+
/** Earlier assistant text; `generated` images are listed as that turn's generation outputs. */
|
|
18
|
+
| {
|
|
19
|
+
role: "assistant";
|
|
20
|
+
text: string;
|
|
21
|
+
generated?: IntentImage[];
|
|
22
|
+
};
|
|
23
|
+
export type IntentCase = {
|
|
24
|
+
id: string;
|
|
25
|
+
group: "core" | "workspace" | "conversation" | "photo";
|
|
26
|
+
/** Earlier turns; the last user turn is the request under test. */
|
|
27
|
+
turns: IntentTurn[];
|
|
28
|
+
/** Flow is listed whether or not a canvas is open, as in Swan; "unavailable" omits it. */
|
|
29
|
+
flow?: "open" | "closed" | "unavailable";
|
|
30
|
+
/** false omits the 3D stage capabilities (scene, materials, geometry, OpenSCAD, imports). */
|
|
31
|
+
stage?: boolean;
|
|
32
|
+
openScadParts?: OpenScadTarget[];
|
|
33
|
+
/** Acceptable routes. More than one only where the shared prompt allows either. */
|
|
34
|
+
expect: readonly IntentRoute[];
|
|
35
|
+
note?: string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The build-intent QA requests (22 Sep 2026), with expectations updated to the creation
|
|
39
|
+
* rules restored in 6.0 (image previews for ambiguous and likeness-dependent requests),
|
|
40
|
+
* plus preview follow-ups and attached-photo cases.
|
|
41
|
+
*/
|
|
42
|
+
export declare const INTENT_CASES: IntentCase[];
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/** The first action a run commits to, or how it ended without one. */
|
|
2
|
+
export const INTENT_ROUTES = [
|
|
3
|
+
"scene",
|
|
4
|
+
"openscad",
|
|
5
|
+
"geometry-nodes",
|
|
6
|
+
"image",
|
|
7
|
+
"image-edit",
|
|
8
|
+
"mesh",
|
|
9
|
+
"image-to-mesh",
|
|
10
|
+
"flow",
|
|
11
|
+
"import",
|
|
12
|
+
"reply",
|
|
13
|
+
"undecided",
|
|
14
|
+
"error",
|
|
15
|
+
];
|
|
16
|
+
const user = (text, images) => ({ role: "user", text, images });
|
|
17
|
+
// A takeaway coffee cup on a table, from Womp's media host.
|
|
18
|
+
const cup = { url: "https://media.womp.com/staging-kkp/thumbnails/ma_IMG_3417_1791494968752.webp" };
|
|
19
|
+
const washer = "difference() { cylinder(h = thickness, d = outer_diameter, $fn = 96); translate([0, 0, -1]) cylinder(h = thickness + 2, d = hole_diameter, $fn = 96); }";
|
|
20
|
+
/**
|
|
21
|
+
* The build-intent QA requests (22 Sep 2026), with expectations updated to the creation
|
|
22
|
+
* rules restored in 6.0 (image previews for ambiguous and likeness-dependent requests),
|
|
23
|
+
* plus preview follow-ups and attached-photo cases.
|
|
24
|
+
*/
|
|
25
|
+
export const INTENT_CASES = [
|
|
26
|
+
{ id: "P1", group: "core", turns: [user("Make a washer: 20 mm outer diameter, 8 mm hole, 2 mm thick.")], expect: ["openscad"] },
|
|
27
|
+
{ id: "P2", group: "core", turns: [user("Make a parametric phone stand where I can adjust the width and tilt angle.")], expect: ["openscad"] },
|
|
28
|
+
{ id: "P3", group: "core", turns: [user("Make a storage tray, 120 × 80 × 25 mm.")], expect: ["openscad"] },
|
|
29
|
+
{ id: "G1", group: "core", turns: [user("Make a branching coral with adjustable branch density.")], expect: ["geometry-nodes"] },
|
|
30
|
+
{ id: "G2", group: "core", turns: [user("Make a rippling surface with adjustable wave speed.")], expect: ["geometry-nodes"] },
|
|
31
|
+
{ id: "G3", group: "core", turns: [user("Make an organic vine where I can adjust the curl and leaf spacing.")], expect: ["geometry-nodes"] },
|
|
32
|
+
{
|
|
33
|
+
id: "S1", group: "core", turns: [user("A car")], expect: ["image"],
|
|
34
|
+
note: "Ambiguous creative request: one image preview, then a choice. The 4.3 QA expected scene.",
|
|
35
|
+
},
|
|
36
|
+
{ id: "S2", group: "core", turns: [user("Make a cup of water in the scene.")], expect: ["scene"] },
|
|
37
|
+
{
|
|
38
|
+
id: "S3", group: "core", turns: [user("Make a friendly little mushroom in 3D.")], expect: ["image"],
|
|
39
|
+
note: "A character depends on likeness, so it previews first. The 4.3 QA expected scene.",
|
|
40
|
+
},
|
|
41
|
+
{ id: "S4", group: "core", turns: [user("Make a simple wooden stool in 3D.")], expect: ["scene"], note: "Plain functional objects build directly." },
|
|
42
|
+
{ id: "S5", group: "core", turns: [user("Make a 3D corgi.")], expect: ["image"], note: "Creatures preview first." },
|
|
43
|
+
{ id: "S6", group: "core", turns: [user("Make a 3D model of a Ferrari F40.")], expect: ["image"], note: "Specific vehicles preview first." },
|
|
44
|
+
{ id: "S7", group: "core", turns: [user("a cozy cabin in the woods")], expect: ["image"], note: "Ambiguous creative request." },
|
|
45
|
+
{ id: "I1", group: "core", turns: [user("Generate an image of a red toy car on a white background.")], expect: ["image"] },
|
|
46
|
+
{ id: "I2", group: "core", turns: [user("Make a studio product photo of a blue ceramic cup.")], expect: ["image"] },
|
|
47
|
+
{ id: "I3", group: "core", turns: [user("Make a picture of a small fantasy mushroom house.")], expect: ["image"] },
|
|
48
|
+
{ id: "I4", group: "core", turns: [user("Design a logo for a coffee shop called Bean There.")], expect: ["image"] },
|
|
49
|
+
{ id: "M1", group: "core", turns: [user("Generate a red toy car mesh.")], expect: ["mesh"] },
|
|
50
|
+
{ id: "M2", group: "core", turns: [user("Generate a mesh of a blue ceramic water cup.")], expect: ["mesh"] },
|
|
51
|
+
{ id: "M3", group: "core", turns: [user("Generate a fantasy mushroom house mesh.")], expect: ["mesh"] },
|
|
52
|
+
{ id: "F1", group: "core", flow: "open", turns: [user("Make an image of a red toy car using Flow nodes.")], expect: ["flow"] },
|
|
53
|
+
{ id: "F2", group: "core", flow: "open", turns: [user("Make a ceramic cup mesh using Flow.")], expect: ["flow"] },
|
|
54
|
+
{ id: "F3", group: "core", flow: "open", turns: [user("Set up a nodes workflow that makes an image of a mushroom house.")], expect: ["flow"] },
|
|
55
|
+
{
|
|
56
|
+
id: "I1-flow-open", group: "workspace", flow: "open",
|
|
57
|
+
turns: [user("Generate an image of a red toy car on a white background.")], expect: ["image"],
|
|
58
|
+
note: "An open Flow canvas does not redirect ordinary generation.",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "F1-flow-closed", group: "workspace", flow: "closed",
|
|
62
|
+
turns: [user("Make an image of a red toy car using Flow nodes.")], expect: ["reply"],
|
|
63
|
+
note: "Explains that Flow must be open instead of generating inline.",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "S2-no-stage", group: "workspace", stage: false,
|
|
67
|
+
turns: [user("Make a cup of water in the scene.")], expect: ["reply"],
|
|
68
|
+
note: "Explains that the 3D stage is unavailable instead of switching route.",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "P1-follow-up", group: "conversation",
|
|
72
|
+
turns: [
|
|
73
|
+
user("Make a washer: 20 mm outer diameter, 8 mm hole, 2 mm thick."),
|
|
74
|
+
{ role: "assistant", text: "I made the washer as an adjustable part (object 12): 20 mm outer diameter, 8 mm hole and 2 mm thick." },
|
|
75
|
+
user("Make the hole 10 mm."),
|
|
76
|
+
],
|
|
77
|
+
openScadParts: [{
|
|
78
|
+
kind: "part", objectId: 12, parentId: 0, title: "Washer", source: washer,
|
|
79
|
+
params: { outer_diameter: 20, hole_diameter: 8, thickness: 2 },
|
|
80
|
+
}],
|
|
81
|
+
expect: ["openscad"],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "I1-follow-up", group: "conversation",
|
|
85
|
+
turns: [
|
|
86
|
+
user("Generate an image of a red toy car on a white background."),
|
|
87
|
+
{ role: "assistant", text: "Here's your red toy car on a white background.", generated: [{ name: "red-toy-car.png", color: [200, 30, 30] }] },
|
|
88
|
+
user("Make it blue."),
|
|
89
|
+
],
|
|
90
|
+
expect: ["image-edit"],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "ambiguous-yes", group: "conversation",
|
|
94
|
+
turns: [
|
|
95
|
+
user("Generate an image of a red toy car on a white background."),
|
|
96
|
+
{
|
|
97
|
+
role: "assistant",
|
|
98
|
+
text: "Here's your red toy car. I can change its colors, or turn it into a sculpted 3D model.",
|
|
99
|
+
generated: [{ name: "red-toy-car.png", color: [200, 30, 30] }],
|
|
100
|
+
},
|
|
101
|
+
user("Yes"),
|
|
102
|
+
],
|
|
103
|
+
expect: ["reply"],
|
|
104
|
+
note: "Two options were offered, so a bare yes needs clarification.",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "preview-to-mesh", group: "conversation",
|
|
108
|
+
turns: [
|
|
109
|
+
user("A car"),
|
|
110
|
+
{
|
|
111
|
+
role: "assistant",
|
|
112
|
+
text: "Here's a quick preview. Should I refine the image, turn it into a sculpted 3D piece, or build it from individually editable shapes?",
|
|
113
|
+
generated: [{ name: "car-preview.png", color: [40, 90, 200] }],
|
|
114
|
+
},
|
|
115
|
+
user("Turn it into a sculpted 3D piece."),
|
|
116
|
+
],
|
|
117
|
+
expect: ["image-to-mesh"],
|
|
118
|
+
note: "Reuses the preview as the image-to-mesh input.",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "preview-to-shapes", group: "conversation",
|
|
122
|
+
turns: [
|
|
123
|
+
user("A car"),
|
|
124
|
+
{
|
|
125
|
+
role: "assistant",
|
|
126
|
+
text: "Here's a quick preview. Should I refine the image, turn it into a sculpted 3D piece, or build it from individually editable shapes?",
|
|
127
|
+
generated: [{ name: "car-preview.png", color: [40, 90, 200] }],
|
|
128
|
+
},
|
|
129
|
+
user("Build it from editable shapes."),
|
|
130
|
+
],
|
|
131
|
+
expect: ["scene"],
|
|
132
|
+
},
|
|
133
|
+
{ id: "photo-3d", group: "photo", turns: [user("make this 3D", [cup])], expect: ["image-to-mesh"] },
|
|
134
|
+
{
|
|
135
|
+
id: "photo-3d-flow-open", group: "photo", flow: "open", turns: [user("make this 3D", [cup])], expect: ["image-to-mesh"],
|
|
136
|
+
note: "Without an explicit Flow request, an attached photo is standard image-to-mesh.",
|
|
137
|
+
},
|
|
138
|
+
{ id: "photo-shapes", group: "photo", turns: [user("build this with editable shapes", [cup])], expect: ["scene"] },
|
|
139
|
+
{
|
|
140
|
+
id: "photo-only", group: "photo", turns: [user(undefined, [cup])], expect: ["reply"],
|
|
141
|
+
note: "A photo alone does not say what to make, so ask. dev0 thread 145108 asked to open Flow instead.",
|
|
142
|
+
},
|
|
143
|
+
{ id: "photo-only-flow-open", group: "photo", flow: "open", turns: [user(undefined, [cup])], expect: ["reply"] },
|
|
144
|
+
{ id: "photo-flow-closed", group: "photo", flow: "closed", turns: [user("turn this into a 3D model using Flow", [cup])], expect: ["reply"] },
|
|
145
|
+
{
|
|
146
|
+
id: "photo-flow-open", group: "photo", flow: "open", turns: [user("turn this into a 3D model using Flow", [cup])], expect: ["flow"],
|
|
147
|
+
note: "Flow has no node for a chat photo; build the graph with what it has and leave the image input to the user.",
|
|
148
|
+
},
|
|
149
|
+
];
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { loadEnvFile } from "node:process";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import { INTENT_CASES } from "./intent-bench-cases.js";
|
|
8
|
+
import { intentBenchModel, runIntentBench, summarizeIntentRuns } from "./intent-bench.js";
|
|
9
|
+
import { SPARK_MODELS } from "./models.js";
|
|
10
|
+
function positiveInteger(value, name) {
|
|
11
|
+
const number = Number(value);
|
|
12
|
+
if (!Number.isSafeInteger(number) || number < 1)
|
|
13
|
+
throw new Error(`${name} must be a positive integer.`);
|
|
14
|
+
return number;
|
|
15
|
+
}
|
|
16
|
+
async function main() {
|
|
17
|
+
const { values } = parseArgs({ options: {
|
|
18
|
+
model: { type: "string", default: "gpt-6-luna" }, reasoning: { type: "string", default: "medium" },
|
|
19
|
+
repeat: { type: "string", default: "3" }, concurrency: { type: "string", default: "24" },
|
|
20
|
+
only: { type: "string" }, group: { type: "string" }, "max-steps": { type: "string", default: "12" },
|
|
21
|
+
"timeout-seconds": { type: "string", default: "240" }, output: { type: "string" },
|
|
22
|
+
list: { type: "boolean" }, help: { type: "boolean" },
|
|
23
|
+
} });
|
|
24
|
+
if (values.help) {
|
|
25
|
+
console.log(`intent-bench [options]
|
|
26
|
+
|
|
27
|
+
Measures which route Spark takes for ${INTENT_CASES.length} requests: editable scene, OpenSCAD, Geometry Nodes,
|
|
28
|
+
image, image edit, mesh, image-to-mesh, Flow, import, or a reply without acting.
|
|
29
|
+
Runs use Swan's skills over fixtures: no renderer, no generation, no scene changes. The first
|
|
30
|
+
call that would change something is recorded and the run stops there. Makes paid model calls.
|
|
31
|
+
|
|
32
|
+
--model <id> Default: gpt-6-luna. Models: ${SPARK_MODELS.map((model) => model.id).join(", ")}
|
|
33
|
+
--reasoning <effort> Default: medium
|
|
34
|
+
--repeat <n> Runs per case; default: 3
|
|
35
|
+
--concurrency <n> Runs at once; default: 24
|
|
36
|
+
--only <ids> Comma-separated case IDs (--list shows them)
|
|
37
|
+
--group <names> core, workspace, conversation, photo
|
|
38
|
+
--max-steps <n> Model steps per run; default: 12
|
|
39
|
+
--timeout-seconds <n> Per run; default: 240
|
|
40
|
+
--output <directory> New directory only; default: .spark/intent-bench/<timestamp>
|
|
41
|
+
--list List the cases and expected routes`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (values.list) {
|
|
45
|
+
for (const testCase of INTENT_CASES) {
|
|
46
|
+
const prompt = testCase.turns.at(-1);
|
|
47
|
+
const text = prompt.role === "user" ? `${prompt.images?.length ? "[photo] " : ""}${prompt.text ?? ""}` : "";
|
|
48
|
+
console.log(`${testCase.id.padEnd(22)} ${testCase.group.padEnd(13)} ${testCase.expect.join("|").padEnd(22)} ${text}`);
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const model = SPARK_MODELS.find((item) => item.id === values.model);
|
|
53
|
+
if (!model)
|
|
54
|
+
throw new Error("Select a supported model using --model. See --help.");
|
|
55
|
+
const reasoning = values.reasoning;
|
|
56
|
+
if (!model.reasoningEfforts.includes(reasoning))
|
|
57
|
+
throw new Error("Unsupported reasoning effort for this model.");
|
|
58
|
+
const only = values.only?.split(",").map((id) => id.trim()).filter(Boolean);
|
|
59
|
+
const groups = values.group?.split(",").map((group) => group.trim()).filter(Boolean);
|
|
60
|
+
const unknown = only?.filter((id) => !INTENT_CASES.some((testCase) => testCase.id === id)) ?? [];
|
|
61
|
+
if (unknown.length)
|
|
62
|
+
throw new Error(`Unknown case IDs: ${unknown.join(", ")}. Use --list.`);
|
|
63
|
+
const cases = INTENT_CASES.filter((testCase) => (!only || only.includes(testCase.id)) && (!groups || groups.includes(testCase.group)));
|
|
64
|
+
if (!cases.length)
|
|
65
|
+
throw new Error("No cases selected.");
|
|
66
|
+
const repeat = positiveInteger(values.repeat, "--repeat");
|
|
67
|
+
const concurrency = positiveInteger(values.concurrency, "--concurrency");
|
|
68
|
+
const maxSteps = positiveInteger(values["max-steps"], "--max-steps");
|
|
69
|
+
const timeoutMs = positiveInteger(values["timeout-seconds"], "--timeout-seconds") * 1000;
|
|
70
|
+
if (existsSync(".env"))
|
|
71
|
+
loadEnvFile();
|
|
72
|
+
const resolved = intentBenchModel({ modelId: model.id, reasoningEffort: reasoning });
|
|
73
|
+
const output = path.resolve(values.output ?? `.spark/intent-bench/${new Date().toISOString().replaceAll(":", "-")}`);
|
|
74
|
+
// Never reuse a result directory or overwrite a previous benchmark.
|
|
75
|
+
await mkdir(path.dirname(output), { recursive: true });
|
|
76
|
+
await mkdir(output);
|
|
77
|
+
const controller = new AbortController();
|
|
78
|
+
const stop = () => controller.abort(new Error("Benchmark interrupted."));
|
|
79
|
+
process.once("SIGINT", stop);
|
|
80
|
+
process.once("SIGTERM", stop);
|
|
81
|
+
const total = cases.length * repeat;
|
|
82
|
+
console.error(`${total} runs (${cases.length} cases × ${repeat}) on ${model.id}/${reasoning}, ${Math.min(concurrency, total)} at once. Results: ${output}`);
|
|
83
|
+
const startedAt = Date.now();
|
|
84
|
+
let done = 0;
|
|
85
|
+
const line = (run) => `${run.pass ? "pass" : "FAIL"} ${run.caseId.padEnd(22)} #${run.repeat} ${run.route.padEnd(14)} ${run.error ?? run.decision?.call ?? ""}`;
|
|
86
|
+
try {
|
|
87
|
+
const runs = await runIntentBench({
|
|
88
|
+
cases, repeat, concurrency, model: resolved, maxSteps, timeoutMs, signal: controller.signal,
|
|
89
|
+
onRun: (run) => console.error(`[${String(++done).padStart(String(total).length)}/${total}] ${line(run)}`),
|
|
90
|
+
});
|
|
91
|
+
const summary = summarizeIntentRuns(cases, runs);
|
|
92
|
+
const passed = runs.filter((run) => run.pass).length;
|
|
93
|
+
const priced = runs.filter((run) => run.costUsd !== null);
|
|
94
|
+
const costUsd = priced.reduce((sum, run) => sum + run.costUsd, 0);
|
|
95
|
+
const durationMs = Date.now() - startedAt;
|
|
96
|
+
await writeFile(path.join(output, "report.json"), `${JSON.stringify({
|
|
97
|
+
model: model.id, reasoning, repeat, maxSteps, startedAt: new Date(startedAt).toISOString(), durationMs,
|
|
98
|
+
passed, runs: runs.length, costUsd: priced.length ? costUsd : null, costStatus: priced.length === runs.length ? "estimated" : priced.length ? "partial" : "unavailable",
|
|
99
|
+
summary, results: runs,
|
|
100
|
+
}, null, 2)}\n`);
|
|
101
|
+
console.log(`\n${"case".padEnd(22)} ${"expected".padEnd(22)} pass routes`);
|
|
102
|
+
for (const item of summary) {
|
|
103
|
+
const routes = Object.entries(item.routes).map(([route, count]) => `${route}×${count}`).join(" ");
|
|
104
|
+
const mark = item.passed === item.runs ? " " : "!";
|
|
105
|
+
console.log(`${mark}${item.id.padEnd(21)} ${item.expect.join("|").padEnd(22)} ${`${item.passed}/${item.runs}`.padEnd(6)} ${routes}`);
|
|
106
|
+
}
|
|
107
|
+
const reliable = summary.filter((item) => item.passed === item.runs).length;
|
|
108
|
+
const gaps = runs.filter((run) => run.fixtureErrors.length).length;
|
|
109
|
+
console.log(`\n${passed}/${runs.length} runs on the expected route (${Math.round((100 * passed) / runs.length)}%), ` +
|
|
110
|
+
`${reliable}/${summary.length} cases on it every time. ${(durationMs / 1000).toFixed(0)} s, ` +
|
|
111
|
+
`${priced.length ? `$${costUsd.toFixed(3)} estimated` : "cost unavailable"}.` +
|
|
112
|
+
(gaps ? `\n${gaps} runs hit a read the fixtures cannot answer (fixtureErrors in the report).` : ""));
|
|
113
|
+
console.log(`Report: ${path.join(output, "report.json")}`);
|
|
114
|
+
if (passed < runs.length)
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
process.removeListener("SIGINT", stop);
|
|
119
|
+
process.removeListener("SIGTERM", stop);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
main().catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const INTENT_BENCH_FLOW_CATALOG: string;
|