@cursor/july 0.1.16 → 0.1.17

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.
Files changed (236) hide show
  1. package/README.md +9 -4
  2. package/dist/bin/agent-serve.js +60 -2
  3. package/dist/channels/slack/api.d.ts.map +1 -1
  4. package/dist/channels/slack/api.js +13 -3
  5. package/dist/channels/slack/auth.d.ts +7 -1
  6. package/dist/channels/slack/auth.d.ts.map +1 -1
  7. package/dist/channels/slack/auth.js +14 -0
  8. package/dist/channels/slack/blocks.d.ts +61 -0
  9. package/dist/channels/slack/blocks.d.ts.map +1 -0
  10. package/dist/channels/slack/blocks.js +90 -0
  11. package/dist/channels/slack/defaults.d.ts +2 -0
  12. package/dist/channels/slack/defaults.d.ts.map +1 -1
  13. package/dist/channels/slack/defaults.js +29 -3
  14. package/dist/channels/slack/dispatch.d.ts +3 -17
  15. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  16. package/dist/channels/slack/index.d.ts +1 -0
  17. package/dist/channels/slack/index.d.ts.map +1 -1
  18. package/dist/channels/slack/index.js +1 -0
  19. package/dist/channels/slack/interactive.d.ts +39 -3
  20. package/dist/channels/slack/interactive.d.ts.map +1 -1
  21. package/dist/channels/slack/interactive.js +152 -7
  22. package/dist/channels/slack/manifest.d.ts +3 -0
  23. package/dist/channels/slack/manifest.d.ts.map +1 -1
  24. package/dist/channels/slack/manifest.js +3 -0
  25. package/dist/channels/slack/setup.d.ts +4 -1
  26. package/dist/channels/slack/setup.d.ts.map +1 -1
  27. package/dist/channels/slack/setup.js +16 -2
  28. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  29. package/dist/channels/slack/slack-channel.js +24 -3
  30. package/dist/channels/slack/types.d.ts +77 -0
  31. package/dist/channels/slack/types.d.ts.map +1 -1
  32. package/dist/connections.d.ts +4 -1
  33. package/dist/connections.d.ts.map +1 -1
  34. package/dist/connections.js +4 -1
  35. package/dist/docs/404.html +2 -2
  36. package/dist/docs/ab.html +3 -3
  37. package/dist/docs/assets/{app.DjHC-Ggf.js → app.BbEXr2KO.js} +1 -1
  38. package/dist/docs/assets/chunks/@localSearchIndexroot.DCBsrfwP.js +1 -0
  39. package/dist/docs/assets/chunks/{VPLocalSearchBox.DwviwmIq.js → VPLocalSearchBox.DBEi4HDp.js} +1 -1
  40. package/dist/docs/assets/chunks/{theme.BIpeYSon.js → theme.DGfptcHw.js} +2 -2
  41. package/dist/docs/assets/{guides_mcp-oauth.md.DqmEOQY1.js → guides_mcp-oauth.md.Dd8EgSem.js} +3 -2
  42. package/dist/docs/assets/{guides_mcp-oauth.md.DqmEOQY1.lean.js → guides_mcp-oauth.md.Dd8EgSem.lean.js} +1 -1
  43. package/dist/docs/assets/{guides_slack.md.FWale6Ip.js → guides_slack.md.D_xFKlnF.js} +1 -1
  44. package/dist/docs/assets/{reference_connections.md.CsSr3rDz.js → reference_connections.md.B9Q3TOve.js} +12 -5
  45. package/dist/docs/assets/{reference_connections.md.CsSr3rDz.lean.js → reference_connections.md.B9Q3TOve.lean.js} +1 -1
  46. package/dist/docs/building-with-agents.html +3 -3
  47. package/dist/docs/concepts.html +3 -3
  48. package/dist/docs/deployment.html +3 -3
  49. package/dist/docs/evals.html +3 -3
  50. package/dist/docs/example-agents/approval-buddy.html +3 -3
  51. package/dist/docs/example-agents/benny.html +3 -3
  52. package/dist/docs/example-agents/bugbot.html +3 -3
  53. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  54. package/dist/docs/example-agents/codeowners-review.html +3 -3
  55. package/dist/docs/example-agents/concierge.html +3 -3
  56. package/dist/docs/example-agents/fsd.html +3 -3
  57. package/dist/docs/example-agents/index.html +3 -3
  58. package/dist/docs/example-agents/knowledge-base.html +3 -3
  59. package/dist/docs/example-agents/oncall.html +3 -3
  60. package/dist/docs/example-agents/security-reviewer.html +3 -3
  61. package/dist/docs/example-agents/slack-agent.html +3 -3
  62. package/dist/docs/example-agents/weather-agent.html +3 -3
  63. package/dist/docs/guides/agent-to-agent.html +3 -3
  64. package/dist/docs/guides/cloud-runtime.html +3 -3
  65. package/dist/docs/guides/github.html +3 -3
  66. package/dist/docs/guides/human-in-the-loop.html +3 -3
  67. package/dist/docs/guides/mcp-oauth.html +6 -5
  68. package/dist/docs/guides/slack.html +5 -5
  69. package/dist/docs/guides/webhooks.html +3 -3
  70. package/dist/docs/hashmap.json +1 -1
  71. package/dist/docs/hillclimbing.html +3 -3
  72. package/dist/docs/index.html +3 -3
  73. package/dist/docs/quickstart.html +3 -3
  74. package/dist/docs/reference/agent-config.html +3 -3
  75. package/dist/docs/reference/channels.html +3 -3
  76. package/dist/docs/reference/cli.html +3 -3
  77. package/dist/docs/reference/connections.html +14 -7
  78. package/dist/docs/reference/hooks.html +3 -3
  79. package/dist/docs/reference/http-api.html +3 -3
  80. package/dist/docs/reference/instructions.html +3 -3
  81. package/dist/docs/reference/playground.html +3 -3
  82. package/dist/docs/reference/project-layout.html +3 -3
  83. package/dist/docs/reference/prompt.html +3 -3
  84. package/dist/docs/reference/schedules.html +3 -3
  85. package/dist/docs/reference/sessions.html +3 -3
  86. package/dist/docs/reference/skills.html +3 -3
  87. package/dist/docs/reference/subagents.html +3 -3
  88. package/dist/docs/reference/tools.html +3 -3
  89. package/dist/docs/scaffolding-agents.html +3 -3
  90. package/dist/docs/storage.html +3 -3
  91. package/dist/docs/troubleshooting.html +3 -3
  92. package/dist/evals/assertions.d.ts +190 -0
  93. package/dist/evals/assertions.d.ts.map +1 -0
  94. package/dist/evals/assertions.js +461 -0
  95. package/dist/evals/expect.d.ts +95 -0
  96. package/dist/evals/expect.d.ts.map +1 -0
  97. package/dist/evals/expect.js +194 -0
  98. package/dist/evals/judge.d.ts +133 -0
  99. package/dist/evals/judge.d.ts.map +1 -0
  100. package/dist/evals/judge.js +228 -0
  101. package/dist/evals/loaders.d.ts +51 -0
  102. package/dist/evals/loaders.d.ts.map +1 -0
  103. package/dist/evals/loaders.js +124 -0
  104. package/dist/evals/matchers.d.ts +95 -0
  105. package/dist/evals/matchers.d.ts.map +1 -0
  106. package/dist/evals/matchers.js +145 -0
  107. package/dist/evals/reporters.d.ts +70 -0
  108. package/dist/evals/reporters.d.ts.map +1 -0
  109. package/dist/evals/reporters.js +221 -0
  110. package/dist/evals/results.d.ts +103 -0
  111. package/dist/evals/results.d.ts.map +1 -0
  112. package/dist/evals/results.js +28 -0
  113. package/dist/evals/run-facts.d.ts +77 -0
  114. package/dist/evals/run-facts.d.ts.map +1 -0
  115. package/dist/evals/run-facts.js +187 -0
  116. package/dist/evals.d.ts +118 -31
  117. package/dist/evals.d.ts.map +1 -1
  118. package/dist/evals.js +46 -12
  119. package/dist/index.d.ts +1 -1
  120. package/dist/index.d.ts.map +1 -1
  121. package/dist/index.js +1 -1
  122. package/dist/internal/cli-ax.d.ts +27 -0
  123. package/dist/internal/cli-ax.d.ts.map +1 -1
  124. package/dist/internal/cli-ax.js +90 -12
  125. package/dist/internal/cli-mcp.d.ts +8 -0
  126. package/dist/internal/cli-mcp.d.ts.map +1 -1
  127. package/dist/internal/cli-mcp.js +98 -0
  128. package/dist/internal/cli-slack.d.ts +29 -1
  129. package/dist/internal/cli-slack.d.ts.map +1 -1
  130. package/dist/internal/cli-slack.js +777 -12
  131. package/dist/internal/deploy-client.d.ts +7 -0
  132. package/dist/internal/deploy-client.d.ts.map +1 -1
  133. package/dist/internal/deploy-client.js +9 -0
  134. package/dist/internal/discovery.d.ts.map +1 -1
  135. package/dist/internal/discovery.js +29 -9
  136. package/dist/internal/distribution.d.ts.map +1 -1
  137. package/dist/internal/distribution.js +2 -0
  138. package/dist/internal/eval-judge-model.d.ts +59 -0
  139. package/dist/internal/eval-judge-model.d.ts.map +1 -0
  140. package/dist/internal/eval-judge-model.js +131 -0
  141. package/dist/internal/eval-run-store.d.ts.map +1 -1
  142. package/dist/internal/eval-run-store.js +12 -20
  143. package/dist/internal/eval-runner.d.ts +31 -25
  144. package/dist/internal/eval-runner.d.ts.map +1 -1
  145. package/dist/internal/eval-runner.js +329 -126
  146. package/dist/internal/evals-client.d.ts.map +1 -1
  147. package/dist/internal/evals-client.js +4 -1
  148. package/dist/internal/local-env.d.ts +8 -0
  149. package/dist/internal/local-env.d.ts.map +1 -0
  150. package/dist/internal/local-env.js +47 -0
  151. package/dist/internal/playground/static.d.ts +3 -0
  152. package/dist/internal/playground/static.d.ts.map +1 -1
  153. package/dist/internal/playground/static.js +3 -0
  154. package/dist/internal/run-client.d.ts +15 -0
  155. package/dist/internal/run-client.d.ts.map +1 -1
  156. package/dist/internal/run-client.js +3 -2
  157. package/dist/internal/sdk-runner.d.ts +36 -1
  158. package/dist/internal/sdk-runner.d.ts.map +1 -1
  159. package/dist/internal/sdk-runner.js +65 -25
  160. package/dist/internal/server.d.ts.map +1 -1
  161. package/dist/internal/server.js +10 -1
  162. package/dist/internal/session-engine.d.ts +18 -5
  163. package/dist/internal/session-engine.d.ts.map +1 -1
  164. package/dist/internal/session-engine.js +72 -19
  165. package/dist/internal/slack-provision-client.d.ts +130 -0
  166. package/dist/internal/slack-provision-client.d.ts.map +1 -0
  167. package/dist/internal/slack-provision-client.js +284 -0
  168. package/dist/playground/assets/cursor-icons-16-f_W_ogc-.woff2 +0 -0
  169. package/dist/playground/assets/index-CidizGZv.css +1 -0
  170. package/dist/playground/assets/index-DNqirsrK.js +85 -0
  171. package/dist/playground/index.html +2 -2
  172. package/dist/types.d.ts +70 -4
  173. package/dist/types.d.ts.map +1 -1
  174. package/dist/types.js +26 -0
  175. package/docs/guides/mcp-oauth.md +10 -5
  176. package/docs/guides/slack.md +3 -2
  177. package/docs/reference/connections.md +28 -18
  178. package/package.json +20 -2
  179. package/skills/create-agent/SKILL.md +15 -7
  180. package/skills/debug/SKILL.md +3 -1
  181. package/skills/mcp-auth/SKILL.md +1 -1
  182. package/skills/setup-slack/SKILL.md +173 -11
  183. package/src/bin/agent-serve.ts +63 -2
  184. package/src/channels/slack/api.ts +16 -3
  185. package/src/channels/slack/auth.ts +16 -1
  186. package/src/channels/slack/blocks.ts +139 -0
  187. package/src/channels/slack/defaults.ts +32 -4
  188. package/src/channels/slack/dispatch.ts +3 -18
  189. package/src/channels/slack/index.ts +1 -0
  190. package/src/channels/slack/interactive.ts +183 -19
  191. package/src/channels/slack/manifest.ts +3 -0
  192. package/src/channels/slack/setup.ts +16 -2
  193. package/src/channels/slack/slack-channel.ts +37 -6
  194. package/src/channels/slack/types.ts +89 -0
  195. package/src/connections.ts +4 -1
  196. package/src/evals/assertions.ts +704 -0
  197. package/src/evals/expect.ts +272 -0
  198. package/src/evals/judge.ts +346 -0
  199. package/src/evals/loaders.ts +113 -0
  200. package/src/evals/matchers.ts +246 -0
  201. package/src/evals/reporters.ts +274 -0
  202. package/src/evals/results.ts +132 -0
  203. package/src/evals/run-facts.ts +274 -0
  204. package/src/evals.ts +205 -42
  205. package/src/index.ts +2 -0
  206. package/src/internal/cli-ax.ts +134 -15
  207. package/src/internal/cli-mcp.ts +118 -5
  208. package/src/internal/cli-slack.ts +993 -15
  209. package/src/internal/deploy-client.ts +10 -0
  210. package/src/internal/discovery.ts +31 -5
  211. package/src/internal/distribution.ts +2 -0
  212. package/src/internal/eval-judge-model.ts +148 -0
  213. package/src/internal/eval-run-store.ts +10 -14
  214. package/src/internal/eval-runner.ts +460 -214
  215. package/src/internal/evals-client.ts +5 -0
  216. package/src/internal/local-env.ts +53 -0
  217. package/src/internal/playground/static.ts +3 -0
  218. package/src/internal/run-client.ts +17 -1
  219. package/src/internal/sdk-runner.ts +103 -22
  220. package/src/internal/server.ts +10 -0
  221. package/src/internal/session-engine.ts +89 -21
  222. package/src/internal/slack-provision-client.ts +438 -0
  223. package/src/types.ts +92 -4
  224. package/dist/channels/slack/post-update-delivery.d.ts +0 -85
  225. package/dist/channels/slack/post-update-delivery.d.ts.map +0 -1
  226. package/dist/docs/assets/chunks/@localSearchIndexroot.DZtu0ZIc.js +0 -1
  227. package/dist/internal/json-dir-store.d.ts +0 -32
  228. package/dist/internal/json-dir-store.d.ts.map +0 -1
  229. package/dist/internal/persistence-coordinator.d.ts +0 -127
  230. package/dist/internal/persistence-coordinator.d.ts.map +0 -1
  231. package/dist/persistence.d.ts +0 -184
  232. package/dist/persistence.d.ts.map +0 -1
  233. package/dist/playground/assets/cursor-icons-16-CQ50JpfO.woff2 +0 -0
  234. package/dist/playground/assets/index-Bwl4Onx4.js +0 -85
  235. package/dist/playground/assets/index-ByV4nfch.css +0 -1
  236. /package/dist/docs/assets/{guides_slack.md.FWale6Ip.lean.js → guides_slack.md.D_xFKlnF.lean.js} +0 -0
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Fixture loaders for dataset-driven evals.
3
+ *
4
+ * Eval modules are ESM, so top-level `await` can load a dataset and fan one
5
+ * file out over it:
6
+ *
7
+ * ```ts
8
+ * // evals/sql.eval.ts
9
+ * import { defineEval } from "@cursor/july/evals";
10
+ * import { loadYaml } from "@cursor/july/evals/loaders";
11
+ * import { equals } from "@cursor/july/evals";
12
+ *
13
+ * const rows = await loadYaml<{ task: string; prompt: string; sql: string }[]>(
14
+ * "evals/data/cases.yaml"
15
+ * );
16
+ *
17
+ * export default rows.map((row) =>
18
+ * defineEval({
19
+ * description: row.task,
20
+ * async test(t) {
21
+ * await t.send(row.prompt);
22
+ * t.succeeded();
23
+ * t.check(t.reply, equals(row.sql));
24
+ * },
25
+ * })
26
+ * );
27
+ * ```
28
+ *
29
+ * Paths resolve from the app root (the directory holding `agent/` and
30
+ * `evals/`), not from the eval file, so a fixture path reads the same however
31
+ * deep the eval sits. These loaders are for fixtures — do not use them for
32
+ * runtime agent code.
33
+ */
34
+
35
+ import { readFile } from "node:fs/promises";
36
+ import { isAbsolute, resolve } from "node:path";
37
+
38
+ /**
39
+ * App root used to resolve relative fixture paths.
40
+ *
41
+ * `eval` sets `AGENT_SERVE_EVAL_ROOT` to the project it discovered, which keeps
42
+ * fixture paths stable no matter what cwd the CLI was invoked from. Falls back
43
+ * to the process cwd.
44
+ */
45
+ export const EVAL_ROOT_ENV = "AGENT_SERVE_EVAL_ROOT";
46
+
47
+ export function evalFixtureRoot(): string {
48
+ const root = process.env[EVAL_ROOT_ENV];
49
+ return root === undefined || root.trim() === "" ? process.cwd() : root;
50
+ }
51
+
52
+ /** Resolve a fixture path against the app root. */
53
+ export function resolveFixturePath(path: string): string {
54
+ return isAbsolute(path) ? path : resolve(evalFixtureRoot(), path);
55
+ }
56
+
57
+ async function readFixture(path: string): Promise<string> {
58
+ const absolute = resolveFixturePath(path);
59
+ try {
60
+ return await readFile(absolute, "utf8");
61
+ } catch (error) {
62
+ const detail = error instanceof Error ? error.message : String(error);
63
+ throw new Error(`failed to load eval fixture ${absolute}: ${detail}`);
64
+ }
65
+ }
66
+
67
+ /** Load a JSON fixture. */
68
+ export async function loadJson<T = unknown>(path: string): Promise<T> {
69
+ const text = await readFixture(path);
70
+ try {
71
+ return JSON.parse(text) as T;
72
+ } catch (error) {
73
+ const detail = error instanceof Error ? error.message : String(error);
74
+ throw new Error(`${resolveFixturePath(path)} is not valid JSON: ${detail}`);
75
+ }
76
+ }
77
+
78
+ /** Load a newline-delimited JSON fixture, skipping blank lines. */
79
+ export async function loadJsonl<T = unknown>(path: string): Promise<T[]> {
80
+ const text = await readFixture(path);
81
+ const rows: T[] = [];
82
+ let lineNumber = 0;
83
+ for (const line of text.split("\n")) {
84
+ lineNumber++;
85
+ const trimmed = line.trim();
86
+ if (trimmed === "") {
87
+ continue;
88
+ }
89
+ try {
90
+ rows.push(JSON.parse(trimmed) as T);
91
+ } catch (error) {
92
+ const detail = error instanceof Error ? error.message : String(error);
93
+ throw new Error(
94
+ `${resolveFixturePath(path)}:${lineNumber} is not valid JSON: ${detail}`
95
+ );
96
+ }
97
+ }
98
+ return rows;
99
+ }
100
+
101
+ /** Load a YAML fixture. */
102
+ export async function loadYaml<T = unknown>(path: string): Promise<T> {
103
+ const text = await readFixture(path);
104
+ // Imported lazily so a project that only uses JSON fixtures never pays for
105
+ // the YAML parser at eval-discovery time.
106
+ const { load } = await import("js-yaml");
107
+ try {
108
+ return load(text) as T;
109
+ } catch (error) {
110
+ const detail = error instanceof Error ? error.message : String(error);
111
+ throw new Error(`${resolveFixturePath(path)} is not valid YAML: ${detail}`);
112
+ }
113
+ }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Matcher mini-language shared by tool-call, subagent, and event assertions.
3
+ *
4
+ * A matcher is a literal, a RegExp, or a predicate:
5
+ *
6
+ * ```ts
7
+ * t.calledTool("bash", { input: { command: /^pwd/ }, count: 1 });
8
+ * t.calledTool("echo", { count: (n) => n >= 2 });
9
+ * t.calledTool("read", { output: (value) => String(value).includes("hello") });
10
+ * ```
11
+ *
12
+ * Object literals partial-deep-match, so `{ input: { city: "NYC" } }` matches a
13
+ * call whose args also carry unrelated keys. Nested values may themselves be
14
+ * matchers.
15
+ */
16
+
17
+ /**
18
+ * Literal forms a matcher can take.
19
+ *
20
+ * Spelled out rather than written as `unknown` so the union keeps its function
21
+ * member: `unknown` would absorb it, and TypeScript could no longer infer the
22
+ * parameter type of an inline predicate. Nested values are matchers in turn, so
23
+ * a predicate buried in an object literal gets its parameter type too.
24
+ */
25
+ export type EvalMatcherLiteral =
26
+ | string
27
+ | number
28
+ | boolean
29
+ | null
30
+ | Date
31
+ | readonly EvalMatcher[]
32
+ | { readonly [key: string]: EvalMatcher | undefined };
33
+
34
+ /** A literal to partial-match, a RegExp to test, or a predicate. */
35
+ export type EvalMatcher<T = unknown> =
36
+ | EvalMatcherLiteral
37
+ | RegExp
38
+ | ((value: T) => boolean);
39
+
40
+ /** An exact count, or a predicate over the observed count. */
41
+ export type EvalCountMatcher = number | ((count: number) => boolean);
42
+
43
+ /** Lifecycle state of one tool call, derived from the session event stream. */
44
+ export type EvalToolCallStatus =
45
+ /** `action.result` arrived without `isError`. */
46
+ | "completed"
47
+ /** `action.result` arrived with `isError: true`. */
48
+ | "failed"
49
+ /** Requested, no result yet (includes calls parked on human approval). */
50
+ | "pending"
51
+ /** A human denied the approval request for this call. */
52
+ | "rejected";
53
+
54
+ /** Constraints for {@link import("../evals.js").EvalAssertions.calledTool}. */
55
+ export interface EvalToolCallMatcher {
56
+ /** Match the tool's arguments (partial-deep for object literals). */
57
+ input?: EvalMatcher;
58
+ /** Match the tool's result payload. */
59
+ output?: EvalMatcher;
60
+ /**
61
+ * Restrict to one lifecycle state. Omitted matches **any** state, which is
62
+ * request-based and therefore backwards compatible with `calledTool(name)`.
63
+ */
64
+ status?: EvalToolCallStatus;
65
+ /** Require an exact number of matching calls, or satisfy a predicate. */
66
+ count?: EvalCountMatcher;
67
+ }
68
+
69
+ /** Constraints for {@link import("../evals.js").EvalAssertions.calledSubagent}. */
70
+ export interface EvalSubagentMatcher {
71
+ /** Match the delegating `task` call id. */
72
+ callId?: EvalMatcher<string>;
73
+ /** Match the subagent's final output text. */
74
+ output?: EvalMatcher;
75
+ /** `completed` once `subagent.completed` arrived, else `pending`. */
76
+ status?: "completed" | "pending";
77
+ count?: EvalCountMatcher;
78
+ }
79
+
80
+ /** Constraints for {@link import("../evals.js").EvalAssertions.event}. */
81
+ export interface EvalEventMatcher {
82
+ /** Partial-deep match against the event's `data` payload. */
83
+ data?: EvalMatcher;
84
+ count?: EvalCountMatcher;
85
+ }
86
+
87
+ /** One entry of {@link import("../evals.js").EvalAssertions.eventOrder}. */
88
+ export interface EvalEventOrderMatcher extends EvalEventMatcher {
89
+ type: string;
90
+ }
91
+
92
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
93
+ return (
94
+ typeof value === "object" &&
95
+ value !== null &&
96
+ !Array.isArray(value) &&
97
+ !(value instanceof RegExp) &&
98
+ !(value instanceof Date)
99
+ );
100
+ }
101
+
102
+ /** Stable string form used when a RegExp is tested against a non-string. */
103
+ export function stringifyForMatch(value: unknown): string {
104
+ if (typeof value === "string") {
105
+ return value;
106
+ }
107
+ if (value === undefined) {
108
+ return "";
109
+ }
110
+ if (value === null) {
111
+ return "null";
112
+ }
113
+ if (typeof value === "object") {
114
+ try {
115
+ return JSON.stringify(value) ?? "";
116
+ } catch {
117
+ return String(value);
118
+ }
119
+ }
120
+ return String(value);
121
+ }
122
+
123
+ /**
124
+ * Whether `actual` satisfies `matcher`.
125
+ *
126
+ * - `undefined` matcher matches anything (the constraint was not supplied).
127
+ * - A function is called with `actual`.
128
+ * - A RegExp is tested against {@link stringifyForMatch}.
129
+ * - An object or array literal partial-deep-matches, and its own values may be
130
+ * matchers.
131
+ * - Anything else compares by deep equality.
132
+ */
133
+ export function matchValue<T = unknown>(
134
+ actual: T,
135
+ matcher: EvalMatcher<T> | undefined
136
+ ): boolean {
137
+ if (matcher === undefined) {
138
+ return true;
139
+ }
140
+ if (typeof matcher === "function") {
141
+ return matcher(actual) === true;
142
+ }
143
+ if (matcher instanceof RegExp) {
144
+ return matcher.test(stringifyForMatch(actual));
145
+ }
146
+ if (Array.isArray(matcher)) {
147
+ if (!Array.isArray(actual) || actual.length !== matcher.length) {
148
+ return false;
149
+ }
150
+ return matcher.every((entry, index) =>
151
+ matchValue(actual[index], entry as EvalMatcher)
152
+ );
153
+ }
154
+ if (isPlainObject(matcher)) {
155
+ if (!isPlainObject(actual)) {
156
+ return false;
157
+ }
158
+ return Object.entries(matcher).every(([key, entry]) =>
159
+ matchValue(actual[key], entry as EvalMatcher)
160
+ );
161
+ }
162
+ return deepEqual(actual, matcher);
163
+ }
164
+
165
+ /** Whether an observed count satisfies a {@link EvalCountMatcher}. */
166
+ export function matchCount(
167
+ observed: number,
168
+ matcher: EvalCountMatcher | undefined
169
+ ): boolean {
170
+ if (matcher === undefined) {
171
+ return observed > 0;
172
+ }
173
+ if (typeof matcher === "function") {
174
+ return matcher(observed) === true;
175
+ }
176
+ return observed === matcher;
177
+ }
178
+
179
+ /** Human-readable matcher summary for assertion names and failure details. */
180
+ export function describeMatcher(matcher: EvalMatcher | undefined): string {
181
+ if (matcher === undefined) {
182
+ return "any";
183
+ }
184
+ if (typeof matcher === "function") {
185
+ return "predicate";
186
+ }
187
+ if (matcher instanceof RegExp) {
188
+ return String(matcher);
189
+ }
190
+ return stringifyForMatch(matcher);
191
+ }
192
+
193
+ /** Compact `{ input: …, count: 2 }` summary for assertion names. */
194
+ export function describeMatcherObject(
195
+ matcher: Record<string, unknown> | undefined
196
+ ): string {
197
+ if (matcher === undefined) {
198
+ return "";
199
+ }
200
+ const parts = Object.entries(matcher)
201
+ .filter(([, value]) => value !== undefined)
202
+ .map(([key, value]) => `${key}=${describeMatcher(value as EvalMatcher)}`);
203
+ return parts.length === 0 ? "" : `, {${parts.join(", ")}}`;
204
+ }
205
+
206
+ export function deepEqual(a: unknown, b: unknown): boolean {
207
+ if (Object.is(a, b)) {
208
+ return true;
209
+ }
210
+ if (a instanceof RegExp && b instanceof RegExp) {
211
+ return String(a) === String(b);
212
+ }
213
+ if (
214
+ typeof a !== "object" ||
215
+ typeof b !== "object" ||
216
+ a === null ||
217
+ b === null
218
+ ) {
219
+ return false;
220
+ }
221
+ if (Array.isArray(a) !== Array.isArray(b)) {
222
+ return false;
223
+ }
224
+ if (Array.isArray(a) && Array.isArray(b)) {
225
+ if (a.length !== b.length) {
226
+ return false;
227
+ }
228
+ return a.every((item, i) => deepEqual(item, b[i]));
229
+ }
230
+ const aKeys = Object.keys(a as object);
231
+ const bKeys = Object.keys(b as object);
232
+ if (aKeys.length !== bKeys.length) {
233
+ return false;
234
+ }
235
+ for (const key of aKeys) {
236
+ if (
237
+ !deepEqual(
238
+ (a as Record<string, unknown>)[key],
239
+ (b as Record<string, unknown>)[key]
240
+ )
241
+ ) {
242
+ return false;
243
+ }
244
+ }
245
+ return true;
246
+ }
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Built-in eval reporters.
3
+ *
4
+ * The CLI already prints a console summary; reporters add destinations on top.
5
+ * Attach them in `evals/evals.config.ts` to observe every case in the run, or
6
+ * on a single `defineEval({ reporters })` to scope a destination to that case.
7
+ *
8
+ * ```ts
9
+ * // evals/evals.config.ts
10
+ * import { defineEvalConfig } from "@cursor/july/evals";
11
+ * import { JUnit } from "@cursor/july/evals/reporters";
12
+ *
13
+ * export default defineEvalConfig({
14
+ * maxConcurrency: 20,
15
+ * reporters: [JUnit({ filePath: ".agent-serve/junit.xml" })],
16
+ * });
17
+ * ```
18
+ *
19
+ * `agentkit eval --junit <path>` and `--artifacts <dir>` attach these same two
20
+ * reporters without touching the eval files, which is usually the better fit:
21
+ * CI owns the output paths, not the eval author.
22
+ */
23
+
24
+ import { mkdir, writeFile } from "node:fs/promises";
25
+ import { dirname, join, resolve } from "node:path";
26
+ import type {
27
+ EvalReporter,
28
+ EvalReporterEvalInfo,
29
+ EvalReporterTarget,
30
+ EvalRunResult,
31
+ EvalRunSummary,
32
+ } from "./results.js";
33
+
34
+ export type {
35
+ EvalReporter,
36
+ EvalReporterEvalInfo,
37
+ EvalReporterTarget,
38
+ EvalRunResult,
39
+ EvalRunSummary,
40
+ } from "./results.js";
41
+
42
+ export interface JUnitReporterOptions {
43
+ /** Where to write the XML. Relative paths resolve from the process cwd. */
44
+ filePath: string;
45
+ /** `<testsuite name>`; defaults to `agentkit-evals`. */
46
+ suiteName?: string;
47
+ }
48
+
49
+ /**
50
+ * Write JUnit XML so CI shows per-case annotations.
51
+ *
52
+ * Each case becomes one `<testcase>` named by its path-derived id. Failed gates
53
+ * and execution errors become `<failure>`, `t.skip(reason)` becomes `<skipped>`,
54
+ * and a soft threshold miss becomes a `<failure>` only under `--strict` (it is
55
+ * reported as a `<system-out>` score line otherwise).
56
+ */
57
+ export function JUnit(options: JUnitReporterOptions): EvalReporter {
58
+ return {
59
+ async onRunComplete(summary) {
60
+ const xml = renderJUnitXml(
61
+ summary,
62
+ options.suiteName ?? "agentkit-evals"
63
+ );
64
+ const target = resolve(options.filePath);
65
+ await mkdir(dirname(target), { recursive: true });
66
+ await writeFile(target, xml, "utf8");
67
+ },
68
+ };
69
+ }
70
+
71
+ /** Render a {@link EvalRunSummary} as JUnit XML. Exported for tests. */
72
+ export function renderJUnitXml(
73
+ summary: EvalRunSummary,
74
+ suiteName: string
75
+ ): string {
76
+ const lines: string[] = [
77
+ '<?xml version="1.0" encoding="UTF-8"?>',
78
+ `<testsuites name="${escapeXml(suiteName)}" tests="${summary.total}" failures="${
79
+ summary.failed + (summary.strict ? summary.scored : 0)
80
+ }" skipped="${summary.skipped}" time="${(summary.durationMs / 1000).toFixed(3)}">`,
81
+ ` <testsuite name="${escapeXml(suiteName)}" tests="${summary.total}" failures="${
82
+ summary.failed + (summary.strict ? summary.scored : 0)
83
+ }" skipped="${summary.skipped}" time="${(summary.durationMs / 1000).toFixed(3)}">`,
84
+ ];
85
+ for (const result of summary.results) {
86
+ const verdict = result.verdict ?? (result.ok ? "passed" : "failed");
87
+ const time = (result.durationMs / 1000).toFixed(3);
88
+ lines.push(
89
+ ` <testcase name="${escapeXml(result.id)}" classname="${escapeXml(
90
+ suiteName
91
+ )}" time="${time}">`
92
+ );
93
+ if (verdict === "skipped") {
94
+ lines.push(
95
+ ` <skipped message="${escapeXml(result.skipReason ?? "skipped")}"/>`
96
+ );
97
+ } else if (
98
+ verdict === "failed" ||
99
+ (verdict === "scored" && summary.strict)
100
+ ) {
101
+ const failures = result.assertions.filter((a) => !a.passed);
102
+ const message =
103
+ result.error ??
104
+ failures
105
+ .map(
106
+ (a) => `${a.name}${a.detail === undefined ? "" : ` (${a.detail})`}`
107
+ )
108
+ .join("; ") ??
109
+ verdict;
110
+ lines.push(
111
+ ` <failure message="${escapeXml(
112
+ message === "" ? verdict : message
113
+ )}" type="${escapeXml(verdict)}"/>`
114
+ );
115
+ }
116
+ const systemOut = renderSystemOut(result);
117
+ if (systemOut !== "") {
118
+ lines.push(` <system-out>${escapeXml(systemOut)}</system-out>`);
119
+ }
120
+ lines.push(" </testcase>");
121
+ }
122
+ lines.push(" </testsuite>", "</testsuites>", "");
123
+ return lines.join("\n");
124
+ }
125
+
126
+ function renderSystemOut(result: EvalRunResult): string {
127
+ const parts: string[] = [];
128
+ for (const assertion of result.assertions) {
129
+ if (assertion.score === undefined) {
130
+ continue;
131
+ }
132
+ const bar =
133
+ assertion.threshold === undefined ? "" : ` (>= ${assertion.threshold})`;
134
+ parts.push(`${assertion.name}=${assertion.score.toFixed(3)}${bar}`);
135
+ }
136
+ for (const [name, value] of Object.entries(result.metrics)) {
137
+ parts.push(`${name}=${String(value)}`);
138
+ }
139
+ parts.push(...result.logs);
140
+ return parts.join("\n");
141
+ }
142
+
143
+ function escapeXml(text: string): string {
144
+ return stripIllegalXmlChars(text)
145
+ .replace(/&/g, "&amp;")
146
+ .replace(/</g, "&lt;")
147
+ .replace(/>/g, "&gt;")
148
+ .replace(/"/g, "&quot;")
149
+ .replace(/'/g, "&apos;");
150
+ }
151
+
152
+ /**
153
+ * Drop characters XML 1.0 forbids outright.
154
+ *
155
+ * Tool output routinely carries stray control bytes, and they are illegal in
156
+ * XML even when escaped — a CI provider would reject the whole report. Tab,
157
+ * newline, and carriage return are the only legal ones below 0x20.
158
+ */
159
+ function stripIllegalXmlChars(text: string): string {
160
+ let out = "";
161
+ for (const char of text) {
162
+ const code = char.codePointAt(0)!;
163
+ if (code >= 0x20 || code === 0x09 || code === 0x0a || code === 0x0d) {
164
+ out += char;
165
+ }
166
+ }
167
+ return out;
168
+ }
169
+
170
+ export interface ArtifactsReporterOptions {
171
+ /** Run directory. Relative paths resolve from the process cwd. */
172
+ dir: string;
173
+ }
174
+
175
+ /**
176
+ * Persist the full story of a run to disk.
177
+ *
178
+ * Console output stays tight on purpose; when a case fails, the artifact has
179
+ * everything — every assertion with its score, the captured event stream, the
180
+ * tool calls, and `t.log` lines.
181
+ *
182
+ * ```text
183
+ * <dir>/summary.json run totals + config
184
+ * <dir>/results.jsonl one line per case, in completion order
185
+ * <dir>/evals/<case-id>.json per-case detail
186
+ * ```
187
+ */
188
+ export function Artifacts(options: ArtifactsReporterOptions): EvalReporter {
189
+ const root = resolve(options.dir);
190
+ const jsonlPath = join(root, "results.jsonl");
191
+ const lines: string[] = [];
192
+ let target: EvalReporterTarget | undefined;
193
+ let discovered: readonly EvalReporterEvalInfo[] = [];
194
+
195
+ return {
196
+ async onRunStart(evals, runTarget) {
197
+ discovered = evals;
198
+ target = runTarget;
199
+ await mkdir(join(root, "evals"), { recursive: true });
200
+ },
201
+ async onEvalComplete(result) {
202
+ lines.push(
203
+ JSON.stringify({
204
+ id: result.id,
205
+ verdict: result.verdict ?? (result.ok ? "passed" : "failed"),
206
+ ok: result.ok,
207
+ durationMs: result.durationMs,
208
+ })
209
+ );
210
+ const file = join(root, "evals", `${sanitizeCaseId(result.id)}.json`);
211
+ await mkdir(dirname(file), { recursive: true });
212
+ await writeFile(file, `${JSON.stringify(result, null, 2)}\n`, "utf8");
213
+ await writeFile(jsonlPath, `${lines.join("\n")}\n`, "utf8");
214
+ },
215
+ async onRunComplete(summary) {
216
+ await mkdir(root, { recursive: true });
217
+ const { results, ...totals } = summary;
218
+ await writeFile(
219
+ join(root, "summary.json"),
220
+ `${JSON.stringify(
221
+ {
222
+ ...totals,
223
+ caseCount: results.length,
224
+ target,
225
+ discovered: discovered.map((e) => e.id),
226
+ },
227
+ null,
228
+ 2
229
+ )}\n`,
230
+ "utf8"
231
+ );
232
+ },
233
+ };
234
+ }
235
+
236
+ /**
237
+ * Turn a case id into a safe relative path.
238
+ *
239
+ * Ids are already path-shaped (`weather/nyc`), and `defineEval` rejects `..` in
240
+ * case ids, but the artifact writer sanitizes anyway so a hand-built result can
241
+ * never escape the run directory.
242
+ */
243
+ export function sanitizeCaseId(id: string): string {
244
+ return id
245
+ .split("/")
246
+ .map((segment) => segment.replace(/[^A-Za-z0-9._-]/g, "_"))
247
+ .filter((segment) => segment !== "" && segment !== "." && segment !== "..")
248
+ .join("/");
249
+ }
250
+
251
+ /** Fan one lifecycle call out to several reporters, isolating failures. */
252
+ export function combineReporters(
253
+ reporters: readonly EvalReporter[],
254
+ onError?: (error: unknown) => void
255
+ ): EvalReporter {
256
+ const forEach = async (
257
+ run: (reporter: EvalReporter) => void | Promise<void>
258
+ ): Promise<void> => {
259
+ for (const reporter of reporters) {
260
+ try {
261
+ await run(reporter);
262
+ } catch (error) {
263
+ // A broken destination must not fail the eval run itself.
264
+ onError?.(error);
265
+ }
266
+ }
267
+ };
268
+ return {
269
+ onRunStart: (evals, target) =>
270
+ forEach((r) => r.onRunStart?.(evals, target)),
271
+ onEvalComplete: (result) => forEach((r) => r.onEvalComplete?.(result)),
272
+ onRunComplete: (summary) => forEach((r) => r.onRunComplete?.(summary)),
273
+ };
274
+ }