@databricks/appkit 0.72.0 → 0.74.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.
Files changed (156) hide show
  1. package/CLAUDE.md +24 -0
  2. package/NOTICE.md +1 -0
  3. package/dist/appkit/package.js +1 -1
  4. package/dist/beta.d.ts +11 -8
  5. package/dist/beta.js +7 -6
  6. package/dist/cli/commands/agent/eval.js +85 -16
  7. package/dist/cli/commands/agent/eval.js.map +1 -1
  8. package/dist/connectors/index.js +1 -1
  9. package/dist/connectors/mlflow/auth.d.ts +11 -1
  10. package/dist/connectors/mlflow/auth.d.ts.map +1 -1
  11. package/dist/connectors/mlflow/auth.js +22 -2
  12. package/dist/connectors/mlflow/auth.js.map +1 -1
  13. package/dist/connectors/mlflow/index.d.ts +2 -0
  14. package/dist/database/errors.js +15 -5
  15. package/dist/database/errors.js.map +1 -1
  16. package/dist/database/runtime/data-path.d.ts +7 -0
  17. package/dist/database/runtime/data-path.d.ts.map +1 -0
  18. package/dist/database/runtime/data-path.js.map +1 -1
  19. package/dist/database/runtime/engine/drizzle-data-path.js +7 -5
  20. package/dist/database/runtime/engine/drizzle-data-path.js.map +1 -1
  21. package/dist/database/schema-builder/define-schema.d.ts +1 -1
  22. package/dist/database/schema-builder/define-schema.js +1 -1
  23. package/dist/database/schema-builder/define-schema.js.map +1 -1
  24. package/dist/errors/database-validation.d.ts +23 -0
  25. package/dist/errors/database-validation.d.ts.map +1 -0
  26. package/dist/errors/database-validation.js +24 -0
  27. package/dist/errors/database-validation.js.map +1 -0
  28. package/dist/errors/index.js +1 -0
  29. package/dist/evals/dataset.d.ts +49 -0
  30. package/dist/evals/dataset.d.ts.map +1 -0
  31. package/dist/evals/dataset.js +51 -0
  32. package/dist/evals/dataset.js.map +1 -0
  33. package/dist/evals/define-eval.d.ts +4 -2
  34. package/dist/evals/define-eval.d.ts.map +1 -1
  35. package/dist/evals/define-eval.js +5 -1
  36. package/dist/evals/define-eval.js.map +1 -1
  37. package/dist/evals/discover.d.ts +15 -1
  38. package/dist/evals/discover.d.ts.map +1 -1
  39. package/dist/evals/discover.js +26 -2
  40. package/dist/evals/discover.js.map +1 -1
  41. package/dist/evals/http-driver.d.ts.map +1 -1
  42. package/dist/evals/http-driver.js +82 -55
  43. package/dist/evals/http-driver.js.map +1 -1
  44. package/dist/evals/index.d.ts +14 -0
  45. package/dist/evals/index.js +6 -5
  46. package/dist/evals/judge.d.ts +1 -0
  47. package/dist/evals/judge.d.ts.map +1 -1
  48. package/dist/evals/mlflow-report.d.ts +1 -0
  49. package/dist/evals/mlflow-report.d.ts.map +1 -1
  50. package/dist/evals/mlflow-run.d.ts +2 -0
  51. package/dist/evals/mlflow-run.d.ts.map +1 -1
  52. package/dist/evals/report.d.ts +16 -1
  53. package/dist/evals/report.d.ts.map +1 -1
  54. package/dist/evals/report.js +64 -2
  55. package/dist/evals/report.js.map +1 -1
  56. package/dist/evals/run-eval.d.ts +8 -0
  57. package/dist/evals/run-eval.d.ts.map +1 -1
  58. package/dist/evals/run-eval.js +54 -5
  59. package/dist/evals/run-eval.js.map +1 -1
  60. package/dist/evals/run-evals.d.ts +41 -3
  61. package/dist/evals/run-evals.d.ts.map +1 -1
  62. package/dist/evals/run-evals.js +215 -34
  63. package/dist/evals/run-evals.js.map +1 -1
  64. package/dist/evals/types.d.ts +80 -6
  65. package/dist/evals/types.d.ts.map +1 -1
  66. package/dist/index.d.ts +2 -1
  67. package/dist/index.js +2 -1
  68. package/dist/plugin/plugin.d.ts.map +1 -1
  69. package/dist/plugin/plugin.js +1 -1
  70. package/dist/plugin/plugin.js.map +1 -1
  71. package/dist/plugins/database/crud/contract.js +17 -8
  72. package/dist/plugins/database/crud/contract.js.map +1 -1
  73. package/dist/plugins/database/crud/exposure.js +63 -22
  74. package/dist/plugins/database/crud/exposure.js.map +1 -1
  75. package/dist/plugins/database/crud/request.js +50 -0
  76. package/dist/plugins/database/crud/request.js.map +1 -0
  77. package/dist/plugins/database/crud/response.js +77 -0
  78. package/dist/plugins/database/crud/response.js.map +1 -0
  79. package/dist/plugins/database/crud/routes.js +71 -52
  80. package/dist/plugins/database/crud/routes.js.map +1 -1
  81. package/dist/plugins/database/database.d.ts +6 -4
  82. package/dist/plugins/database/database.d.ts.map +1 -1
  83. package/dist/plugins/database/database.js +46 -16
  84. package/dist/plugins/database/database.js.map +1 -1
  85. package/dist/plugins/database/defaults.js +5 -1
  86. package/dist/plugins/database/defaults.js.map +1 -1
  87. package/dist/plugins/database/entity-client.js +143 -10
  88. package/dist/plugins/database/entity-client.js.map +1 -1
  89. package/dist/plugins/database/entity-types.d.ts +1 -1
  90. package/dist/plugins/database/hooks.d.ts +38 -0
  91. package/dist/plugins/database/hooks.d.ts.map +1 -0
  92. package/dist/plugins/database/index.d.ts +3 -2
  93. package/dist/plugins/database/lifecycle.js +67 -28
  94. package/dist/plugins/database/lifecycle.js.map +1 -1
  95. package/dist/plugins/database/scope.js +58 -0
  96. package/dist/plugins/database/scope.js.map +1 -0
  97. package/dist/plugins/database/types.d.ts +40 -12
  98. package/dist/plugins/database/types.d.ts.map +1 -1
  99. package/dist/plugins/server/index.js +2 -2
  100. package/dist/plugins/server/index.js.map +1 -1
  101. package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js +3 -3
  102. package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
  103. package/dist/plugins/server/static-server.js +3 -3
  104. package/dist/plugins/server/static-server.js.map +1 -1
  105. package/dist/plugins/server/utils.js +3 -3
  106. package/dist/plugins/server/utils.js.map +1 -1
  107. package/dist/plugins/server/vite-dev-server.js +4 -4
  108. package/dist/plugins/server/vite-dev-server.js.map +1 -1
  109. package/dist/shared/src/schemas/manifest.d.ts +87 -87
  110. package/dist/type-generator/database/generate.js +3 -3
  111. package/dist/type-generator/database/generate.js.map +1 -1
  112. package/dist/type-generator/migration.js +2 -2
  113. package/dist/type-generator/migration.js.map +1 -1
  114. package/dist/type-generator/serving/server-file-extractor.js +3 -3
  115. package/dist/type-generator/serving/server-file-extractor.js.map +1 -1
  116. package/docs/api/appkit/Class.AppKitError.md +1 -0
  117. package/docs/api/appkit/Class.DatabaseValidationError.md +191 -0
  118. package/docs/api/appkit/Function.defineEvalConfig.md +18 -0
  119. package/docs/api/appkit/Function.defineSchema.md +1 -1
  120. package/docs/api/appkit/Function.discoverEvalConfigs.md +18 -0
  121. package/docs/api/appkit/Function.formatResultsJUnit.md +18 -0
  122. package/docs/api/appkit/Function.formatResultsJson.md +18 -0
  123. package/docs/api/appkit/Function.readEvalDataset.md +21 -0
  124. package/docs/api/appkit/Function.resolveWorkspaceClient.md +18 -0
  125. package/docs/api/appkit/Function.runWithRetries.md +28 -0
  126. package/docs/api/appkit/Function.userTurns.md +20 -0
  127. package/docs/api/appkit/Interface.AssertionHandle.md +1 -1
  128. package/docs/api/appkit/Interface.DatabaseValidationIssue.md +21 -0
  129. package/docs/api/appkit/Interface.DatasetRow.md +21 -0
  130. package/docs/api/appkit/Interface.DiscoveredEvalConfig.md +25 -0
  131. package/docs/api/appkit/Interface.DriveResult.md +28 -0
  132. package/docs/api/appkit/Interface.EntityMutationHooks.md +173 -0
  133. package/docs/api/appkit/Interface.EvalDefinition.md +50 -0
  134. package/docs/api/appkit/Interface.EvalDriver.md +26 -5
  135. package/docs/api/appkit/Interface.EvalResult.md +11 -0
  136. package/docs/api/appkit/Interface.EvalSummary.md +11 -0
  137. package/docs/api/appkit/Interface.HookApp.md +12 -0
  138. package/docs/api/appkit/Interface.HookContext.md +21 -0
  139. package/docs/api/appkit/Interface.ReadEvalDatasetOptions.md +34 -0
  140. package/docs/api/appkit/Interface.ReadSerializerContext.md +21 -0
  141. package/docs/api/appkit/Interface.RunEvalOptions.md +22 -0
  142. package/docs/api/appkit/Interface.RunEvalsOptions.md +45 -1
  143. package/docs/api/appkit/Interface.TestContext.md +67 -8
  144. package/docs/api/appkit/TypeAlias.DatabaseApiConfig.md +53 -0
  145. package/docs/api/appkit/TypeAlias.DatabaseApiWriteOperation.md +8 -0
  146. package/docs/api/appkit/TypeAlias.DatabaseApiWritesConfig.md +49 -0
  147. package/docs/api/appkit/TypeAlias.DatabaseExports.md +3 -3
  148. package/docs/api/appkit/TypeAlias.EntityHooks.md +25 -0
  149. package/docs/api/appkit/TypeAlias.IDatabaseConfig.md +16 -5
  150. package/docs/api/appkit/TypeAlias.ReadSerializer.md +19 -0
  151. package/docs/api/appkit/TypeAlias.TransactionClient.md +19 -0
  152. package/docs/api/appkit.md +142 -119
  153. package/docs/plugins/database.md +144 -0
  154. package/llms.txt +24 -0
  155. package/package.json +2 -2
  156. package/sbom.cdx.json +1 -1
@@ -22,7 +22,11 @@ function defineEval(def) {
22
22
  if (typeof def.test !== "function") throw new Error("defineEval: `test` must be a function");
23
23
  return def;
24
24
  }
25
+ /** Define per-directory eval config. Default-export from `evals.config.ts`. */
26
+ function defineEvalConfig(config) {
27
+ return config;
28
+ }
25
29
 
26
30
  //#endregion
27
- export { defineEval };
31
+ export { defineEval, defineEvalConfig };
28
32
  //# sourceMappingURL=define-eval.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"define-eval.js","names":[],"sources":["../../src/evals/define-eval.ts"],"sourcesContent":["import type { EvalDefinition } from \"./types\";\n\n/**\n * Define an agent eval. Default-export the result from a\n * `server/agents/<id>/evals/*.eval.ts` file.\n *\n * @example\n * ```ts\n * import { defineEval, includes } from \"@databricks/appkit/beta\";\n *\n * export default defineEval({\n * description: \"Weather agent basic coverage\",\n * async test(t) {\n * await t.send(\"What's the weather in Brooklyn?\");\n * t.succeeded();\n * t.calledTool(\"get_weather\");\n * t.check(t.reply, includes(\"Sunny\"));\n * },\n * });\n * ```\n */\nexport function defineEval(def: EvalDefinition): EvalDefinition {\n if (typeof def.test !== \"function\") {\n throw new Error(\"defineEval: `test` must be a function\");\n }\n return def;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,WAAW,KAAqC;AAC9D,KAAI,OAAO,IAAI,SAAS,WACtB,OAAM,IAAI,MAAM,wCAAwC;AAE1D,QAAO"}
1
+ {"version":3,"file":"define-eval.js","names":[],"sources":["../../src/evals/define-eval.ts"],"sourcesContent":["import type { EvalConfig, EvalDefinition } from \"./types\";\n\n/**\n * Define an agent eval. Default-export the result from a\n * `server/agents/<id>/evals/*.eval.ts` file.\n *\n * @example\n * ```ts\n * import { defineEval, includes } from \"@databricks/appkit/beta\";\n *\n * export default defineEval({\n * description: \"Weather agent basic coverage\",\n * async test(t) {\n * await t.send(\"What's the weather in Brooklyn?\");\n * t.succeeded();\n * t.calledTool(\"get_weather\");\n * t.check(t.reply, includes(\"Sunny\"));\n * },\n * });\n * ```\n */\nexport function defineEval(def: EvalDefinition): EvalDefinition {\n if (typeof def.test !== \"function\") {\n throw new Error(\"defineEval: `test` must be a function\");\n }\n return def;\n}\n\n/** Define per-directory eval config. Default-export from `evals.config.ts`. */\nexport function defineEvalConfig(config: EvalConfig): EvalConfig {\n return config;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,WAAW,KAAqC;AAC9D,KAAI,OAAO,IAAI,SAAS,WACtB,OAAM,IAAI,MAAM,wCAAwC;AAE1D,QAAO;;;AAIT,SAAgB,iBAAiB,QAAgC;AAC/D,QAAO"}
@@ -8,6 +8,13 @@ interface DiscoveredEval {
8
8
  /** The agent id (the `server/agents/<agent>` directory name). */
9
9
  agent: string;
10
10
  }
11
+ /** A per-agent `evals.config.ts` found under `server/agents/<agent>/evals/`. */
12
+ interface DiscoveredEvalConfig {
13
+ /** Absolute path to the `evals.config.ts` file. */
14
+ file: string;
15
+ /** The agent id whose evals this config applies to. */
16
+ agent: string;
17
+ }
11
18
  /**
12
19
  * Discover evals under `<rootDir>/server/agents/<agent>/evals/` — co-located
13
20
  * with each agent's `agent.{md,ts}` (same folder-per-agent layout the agents
@@ -15,6 +22,13 @@ interface DiscoveredEval {
15
22
  * path relative to that evals dir with `.eval.ts` stripped. Sorted + stable.
16
23
  */
17
24
  declare function discoverEvalFiles(rootDir: string): DiscoveredEval[];
25
+ /**
26
+ * Discover the per-agent `evals.config.ts` (from {@link defineEvalConfig}) at
27
+ * `<rootDir>/server/agents/<agent>/evals/evals.config.ts`. Config is per-agent:
28
+ * each agent's config applies only to that agent's evals. Agents without a
29
+ * config file are omitted. Returns a stable, sorted list.
30
+ */
31
+ declare function discoverEvalConfigs(rootDir: string): DiscoveredEvalConfig[];
18
32
  //#endregion
19
- export { DiscoveredEval, discoverEvalFiles };
33
+ export { DiscoveredEval, DiscoveredEvalConfig, discoverEvalConfigs, discoverEvalFiles };
20
34
  //# sourceMappingURL=discover.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"discover.d.ts","names":[],"sources":["../../src/evals/discover.ts"],"mappings":";;UAOiB,cAAA;EAAc;EAE7B,IAAA;EAF6B;EAI7B,EAAA;EAAA;EAEA,KAAA;AAAA;;AAoBF;;;;;iBAAgB,iBAAA,CAAkB,OAAA,WAAkB,cAAA"}
1
+ {"version":3,"file":"discover.d.ts","names":[],"sources":["../../src/evals/discover.ts"],"mappings":";;UAOiB,cAAA;EAAc;EAE7B,IAAA;EAF6B;EAI7B,EAAA;EAAA;EAEA,KAAA;AAAA;;UAIe,oBAAA;EAAoB;EAEnC,IAAA;EAAA;EAEA,KAAA;AAAA;;;;;AAsDF;;iBAlCgB,iBAAA,CAAkB,OAAA,WAAkB,cAAA;;;;;;;iBAkCpC,mBAAA,CAAoB,OAAA,WAAkB,oBAAA"}
@@ -1,7 +1,7 @@
1
1
  import { agentDirNames } from "../core/agent/agent-dirs.js";
2
2
  import { CODE_AGENTS_SOURCE_DIR } from "../core/agent/load-code-agents.js";
3
3
  import path from "node:path";
4
- import { readdirSync } from "node:fs";
4
+ import { existsSync, readdirSync } from "node:fs";
5
5
 
6
6
  //#region src/evals/discover.ts
7
7
  /** Recursively collect `*.eval.ts` files under `dir`. Empty when `dir` is absent. */
@@ -43,7 +43,31 @@ function discoverEvalFiles(rootDir) {
43
43
  }
44
44
  return out.sort((a, b) => a.agent.localeCompare(b.agent) || a.id.localeCompare(b.id));
45
45
  }
46
+ /**
47
+ * Discover the per-agent `evals.config.ts` (from {@link defineEvalConfig}) at
48
+ * `<rootDir>/server/agents/<agent>/evals/evals.config.ts`. Config is per-agent:
49
+ * each agent's config applies only to that agent's evals. Agents without a
50
+ * config file are omitted. Returns a stable, sorted list.
51
+ */
52
+ function discoverEvalConfigs(rootDir) {
53
+ const agentsDir = path.join(rootDir, CODE_AGENTS_SOURCE_DIR);
54
+ const out = [];
55
+ let entries;
56
+ try {
57
+ entries = readdirSync(agentsDir, { withFileTypes: true });
58
+ } catch {
59
+ return out;
60
+ }
61
+ for (const agent of agentDirNames(entries)) {
62
+ const file = path.join(agentsDir, agent, "evals", "evals.config.ts");
63
+ if (existsSync(file)) out.push({
64
+ file,
65
+ agent
66
+ });
67
+ }
68
+ return out.sort((a, b) => a.agent.localeCompare(b.agent));
69
+ }
46
70
 
47
71
  //#endregion
48
- export { discoverEvalFiles };
72
+ export { discoverEvalConfigs, discoverEvalFiles };
49
73
  //# sourceMappingURL=discover.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"discover.js","names":[],"sources":["../../src/evals/discover.ts"],"sourcesContent":["import { type Dirent, readdirSync } from \"node:fs\";\nimport path from \"node:path\";\n\nimport { agentDirNames } from \"../core/agent/agent-dirs\";\nimport { CODE_AGENTS_SOURCE_DIR } from \"../core/agent/load-code-agents\";\n\n/** An eval file found under `server/agents/<agent>/evals/`. */\nexport interface DiscoveredEval {\n /** Absolute path to the `*.eval.ts` file. */\n file: string;\n /** Id relative to the agent's evals dir, without `.eval.ts` (e.g. `weather/basic`). */\n id: string;\n /** The agent id (the `server/agents/<agent>` directory name). */\n agent: string;\n}\n\n/** Recursively collect `*.eval.ts` files under `dir`. Empty when `dir` is absent. */\nfunction evalFilesIn(dir: string): string[] {\n try {\n return readdirSync(dir, { recursive: true, withFileTypes: true })\n .filter((e) => e.isFile() && e.name.endsWith(\".eval.ts\"))\n .map((e) => path.join(e.parentPath, e.name));\n } catch {\n return [];\n }\n}\n\n/**\n * Discover evals under `<rootDir>/server/agents/<agent>/evals/` — co-located\n * with each agent's `agent.{md,ts}` (same folder-per-agent layout the agents\n * plugin discovers). The agent id is the folder name; the eval id is the file\n * path relative to that evals dir with `.eval.ts` stripped. Sorted + stable.\n */\nexport function discoverEvalFiles(rootDir: string): DiscoveredEval[] {\n const agentsDir = path.join(rootDir, CODE_AGENTS_SOURCE_DIR);\n const out: DiscoveredEval[] = [];\n\n let entries: Dirent[];\n try {\n entries = readdirSync(agentsDir, { withFileTypes: true });\n } catch {\n return out;\n }\n\n for (const agent of agentDirNames(entries)) {\n const evalsDir = path.join(agentsDir, agent, \"evals\");\n for (const file of evalFilesIn(evalsDir)) {\n const id = path\n .relative(evalsDir, file)\n .replace(/\\.eval\\.ts$/, \"\")\n .split(path.sep)\n .join(\"/\");\n out.push({ file, id, agent });\n }\n }\n\n return out.sort(\n (a, b) => a.agent.localeCompare(b.agent) || a.id.localeCompare(b.id),\n );\n}\n"],"mappings":";;;;;;;AAiBA,SAAS,YAAY,KAAuB;AAC1C,KAAI;AACF,SAAO,YAAY,KAAK;GAAE,WAAW;GAAM,eAAe;GAAM,CAAC,CAC9D,QAAQ,MAAM,EAAE,QAAQ,IAAI,EAAE,KAAK,SAAS,WAAW,CAAC,CACxD,KAAK,MAAM,KAAK,KAAK,EAAE,YAAY,EAAE,KAAK,CAAC;SACxC;AACN,SAAO,EAAE;;;;;;;;;AAUb,SAAgB,kBAAkB,SAAmC;CACnE,MAAM,YAAY,KAAK,KAAK,SAAS,uBAAuB;CAC5D,MAAM,MAAwB,EAAE;CAEhC,IAAI;AACJ,KAAI;AACF,YAAU,YAAY,WAAW,EAAE,eAAe,MAAM,CAAC;SACnD;AACN,SAAO;;AAGT,MAAK,MAAM,SAAS,cAAc,QAAQ,EAAE;EAC1C,MAAM,WAAW,KAAK,KAAK,WAAW,OAAO,QAAQ;AACrD,OAAK,MAAM,QAAQ,YAAY,SAAS,EAAE;GACxC,MAAM,KAAK,KACR,SAAS,UAAU,KAAK,CACxB,QAAQ,eAAe,GAAG,CAC1B,MAAM,KAAK,IAAI,CACf,KAAK,IAAI;AACZ,OAAI,KAAK;IAAE;IAAM;IAAI;IAAO,CAAC;;;AAIjC,QAAO,IAAI,MACR,GAAG,MAAM,EAAE,MAAM,cAAc,EAAE,MAAM,IAAI,EAAE,GAAG,cAAc,EAAE,GAAG,CACrE"}
1
+ {"version":3,"file":"discover.js","names":[],"sources":["../../src/evals/discover.ts"],"sourcesContent":["import { type Dirent, existsSync, readdirSync } from \"node:fs\";\nimport path from \"node:path\";\n\nimport { agentDirNames } from \"../core/agent/agent-dirs\";\nimport { CODE_AGENTS_SOURCE_DIR } from \"../core/agent/load-code-agents\";\n\n/** An eval file found under `server/agents/<agent>/evals/`. */\nexport interface DiscoveredEval {\n /** Absolute path to the `*.eval.ts` file. */\n file: string;\n /** Id relative to the agent's evals dir, without `.eval.ts` (e.g. `weather/basic`). */\n id: string;\n /** The agent id (the `server/agents/<agent>` directory name). */\n agent: string;\n}\n\n/** A per-agent `evals.config.ts` found under `server/agents/<agent>/evals/`. */\nexport interface DiscoveredEvalConfig {\n /** Absolute path to the `evals.config.ts` file. */\n file: string;\n /** The agent id whose evals this config applies to. */\n agent: string;\n}\n\n/** Recursively collect `*.eval.ts` files under `dir`. Empty when `dir` is absent. */\nfunction evalFilesIn(dir: string): string[] {\n try {\n return readdirSync(dir, { recursive: true, withFileTypes: true })\n .filter((e) => e.isFile() && e.name.endsWith(\".eval.ts\"))\n .map((e) => path.join(e.parentPath, e.name));\n } catch {\n return [];\n }\n}\n\n/**\n * Discover evals under `<rootDir>/server/agents/<agent>/evals/` — co-located\n * with each agent's `agent.{md,ts}` (same folder-per-agent layout the agents\n * plugin discovers). The agent id is the folder name; the eval id is the file\n * path relative to that evals dir with `.eval.ts` stripped. Sorted + stable.\n */\nexport function discoverEvalFiles(rootDir: string): DiscoveredEval[] {\n const agentsDir = path.join(rootDir, CODE_AGENTS_SOURCE_DIR);\n const out: DiscoveredEval[] = [];\n\n let entries: Dirent[];\n try {\n entries = readdirSync(agentsDir, { withFileTypes: true });\n } catch {\n return out;\n }\n\n for (const agent of agentDirNames(entries)) {\n const evalsDir = path.join(agentsDir, agent, \"evals\");\n for (const file of evalFilesIn(evalsDir)) {\n const id = path\n .relative(evalsDir, file)\n .replace(/\\.eval\\.ts$/, \"\")\n .split(path.sep)\n .join(\"/\");\n out.push({ file, id, agent });\n }\n }\n\n return out.sort(\n (a, b) => a.agent.localeCompare(b.agent) || a.id.localeCompare(b.id),\n );\n}\n\n/**\n * Discover the per-agent `evals.config.ts` (from {@link defineEvalConfig}) at\n * `<rootDir>/server/agents/<agent>/evals/evals.config.ts`. Config is per-agent:\n * each agent's config applies only to that agent's evals. Agents without a\n * config file are omitted. Returns a stable, sorted list.\n */\nexport function discoverEvalConfigs(rootDir: string): DiscoveredEvalConfig[] {\n const agentsDir = path.join(rootDir, CODE_AGENTS_SOURCE_DIR);\n const out: DiscoveredEvalConfig[] = [];\n\n let entries: Dirent[];\n try {\n entries = readdirSync(agentsDir, { withFileTypes: true });\n } catch {\n return out;\n }\n\n for (const agent of agentDirNames(entries)) {\n const file = path.join(agentsDir, agent, \"evals\", \"evals.config.ts\");\n if (existsSync(file)) out.push({ file, agent });\n }\n\n return out.sort((a, b) => a.agent.localeCompare(b.agent));\n}\n"],"mappings":";;;;;;;AAyBA,SAAS,YAAY,KAAuB;AAC1C,KAAI;AACF,SAAO,YAAY,KAAK;GAAE,WAAW;GAAM,eAAe;GAAM,CAAC,CAC9D,QAAQ,MAAM,EAAE,QAAQ,IAAI,EAAE,KAAK,SAAS,WAAW,CAAC,CACxD,KAAK,MAAM,KAAK,KAAK,EAAE,YAAY,EAAE,KAAK,CAAC;SACxC;AACN,SAAO,EAAE;;;;;;;;;AAUb,SAAgB,kBAAkB,SAAmC;CACnE,MAAM,YAAY,KAAK,KAAK,SAAS,uBAAuB;CAC5D,MAAM,MAAwB,EAAE;CAEhC,IAAI;AACJ,KAAI;AACF,YAAU,YAAY,WAAW,EAAE,eAAe,MAAM,CAAC;SACnD;AACN,SAAO;;AAGT,MAAK,MAAM,SAAS,cAAc,QAAQ,EAAE;EAC1C,MAAM,WAAW,KAAK,KAAK,WAAW,OAAO,QAAQ;AACrD,OAAK,MAAM,QAAQ,YAAY,SAAS,EAAE;GACxC,MAAM,KAAK,KACR,SAAS,UAAU,KAAK,CACxB,QAAQ,eAAe,GAAG,CAC1B,MAAM,KAAK,IAAI,CACf,KAAK,IAAI;AACZ,OAAI,KAAK;IAAE;IAAM;IAAI;IAAO,CAAC;;;AAIjC,QAAO,IAAI,MACR,GAAG,MAAM,EAAE,MAAM,cAAc,EAAE,MAAM,IAAI,EAAE,GAAG,cAAc,EAAE,GAAG,CACrE;;;;;;;;AASH,SAAgB,oBAAoB,SAAyC;CAC3E,MAAM,YAAY,KAAK,KAAK,SAAS,uBAAuB;CAC5D,MAAM,MAA8B,EAAE;CAEtC,IAAI;AACJ,KAAI;AACF,YAAU,YAAY,WAAW,EAAE,eAAe,MAAM,CAAC;SACnD;AACN,SAAO;;AAGT,MAAK,MAAM,SAAS,cAAc,QAAQ,EAAE;EAC1C,MAAM,OAAO,KAAK,KAAK,WAAW,OAAO,SAAS,kBAAkB;AACpE,MAAI,WAAW,KAAK,CAAE,KAAI,KAAK;GAAE;GAAM;GAAO,CAAC;;AAGjD,QAAO,IAAI,MAAM,GAAG,MAAM,EAAE,MAAM,cAAc,EAAE,MAAM,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"http-driver.d.ts","names":[],"sources":["../../src/evals/http-driver.ts"],"mappings":";;;UAGiB,iBAAA;;EAEf,OAAA;EAFgC;EAIhC,KAAA;EAEgB;EAAhB,OAAA,GAAU,MAAA;EAFV;EAIA,IAAA;EAFU;EAIV,WAAA;EAAA;;;;AA6GF;;;EApGE,SAAA;AAAA;;;;;;;iBAoGc,gBAAA,CAAiB,OAAA,EAAS,iBAAA,GAAoB,UAAA"}
1
+ {"version":3,"file":"http-driver.d.ts","names":[],"sources":["../../src/evals/http-driver.ts"],"mappings":";;;UAGiB,iBAAA;;EAEf,OAAA;EAFgC;EAIhC,KAAA;EAEgB;EAAhB,OAAA,GAAU,MAAA;EAFV;EAIA,IAAA;EAFU;EAIV,WAAA;EAAA;;;;AA4IF;;;EAnIE,SAAA;AAAA;;;;;;;iBAmIc,gBAAA,CAAiB,OAAA,EAAS,iBAAA,GAAoB,UAAA"}
@@ -1,19 +1,37 @@
1
1
  import { readSseEvents } from "../stream/sse-reader.js";
2
2
 
3
3
  //#region src/evals/http-driver.ts
4
- /** Record a `function_call` output item once per call id (deduped). */
4
+ /**
5
+ * Record a `function_call` output item, keyed by `call_id ?? name`, capturing
6
+ * its parsed arguments. A later `done` event's fuller args win over the initial
7
+ * `added` event's (often empty) args.
8
+ */
5
9
  function recordToolCall(item, state) {
6
10
  if (item?.type !== "function_call" || !item.name) return;
7
11
  const key = item.call_id ?? item.name;
8
- if (state.seen.has(key)) return;
9
- state.seen.add(key);
10
- state.toolCalls.push(item.name);
12
+ const args = parseArgs(item.arguments);
13
+ const existing = state.toolCalls.get(key);
14
+ if (!existing) state.toolCalls.set(key, {
15
+ name: item.name,
16
+ args
17
+ });
18
+ else if (Object.keys(args).length > 0) existing.args = args;
11
19
  }
12
20
  /** Apply an `appkit.metadata` event's thread/trace ids. */
13
21
  function applyMetadata(data, state, setThread) {
14
22
  if (data?.threadId) setThread(data.threadId);
15
23
  if (data?.mlflowTraceId) state.traceId = data.mlflowTraceId;
16
24
  }
25
+ /** Parse a function-call `arguments` JSON string; `{}` on missing/invalid. */
26
+ function parseArgs(raw) {
27
+ if (typeof raw !== "string" || raw.trim() === "") return {};
28
+ try {
29
+ const parsed = JSON.parse(raw);
30
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
31
+ } catch {
32
+ return {};
33
+ }
34
+ }
17
35
  /** Parse a single Responses-API SSE `data:` payload into the running totals. */
18
36
  function applyEvent(event, state, setThread) {
19
37
  const type = event.type;
@@ -55,62 +73,71 @@ function createHttpDriver(options) {
55
73
  const chatPath = options.path ?? "/api/agents/chat";
56
74
  const timeoutMs = options.timeoutMs ?? 12e4;
57
75
  let threadId;
58
- return { async send(message) {
59
- const signal = AbortSignal.timeout(timeoutMs);
60
- let res;
61
- try {
62
- res = await fetch(`${options.baseUrl}${chatPath}`, {
63
- method: "POST",
64
- headers: {
65
- "content-type": "application/json",
66
- ...options.headers
67
- },
68
- body: buildRequestBody(message, options, threadId),
69
- redirect: "manual",
70
- signal
71
- });
72
- } catch {
73
- return {
76
+ return {
77
+ reset() {
78
+ threadId = void 0;
79
+ },
80
+ async send(message, opts) {
81
+ const timeout = AbortSignal.timeout(timeoutMs);
82
+ const signal = opts?.signal ? AbortSignal.any([timeout, opts.signal]) : timeout;
83
+ let res;
84
+ try {
85
+ res = await fetch(`${options.baseUrl}${chatPath}`, {
86
+ method: "POST",
87
+ headers: {
88
+ "content-type": "application/json",
89
+ ...options.headers
90
+ },
91
+ body: buildRequestBody(message, options, threadId),
92
+ redirect: "manual",
93
+ signal
94
+ });
95
+ } catch {
96
+ return {
97
+ reply: "",
98
+ toolCalls: [],
99
+ toolCallDetails: [],
100
+ succeeded: false
101
+ };
102
+ }
103
+ if (!res.ok || !res.body) return {
74
104
  reply: "",
75
105
  toolCalls: [],
76
- succeeded: false
106
+ toolCallDetails: [],
107
+ succeeded: false,
108
+ sessionId: threadId
77
109
  };
78
- }
79
- if (!res.ok || !res.body) return {
80
- reply: "",
81
- toolCalls: [],
82
- succeeded: false,
83
- sessionId: threadId
84
- };
85
- const state = {
86
- reply: "",
87
- toolCalls: [],
88
- seen: /* @__PURE__ */ new Set(),
89
- ok: true
90
- };
91
- const setThread = (id) => {
92
- threadId = id;
93
- };
94
- try {
95
- for await (const { event, data } of readSseEvents(res.body, signal)) {
96
- if (data === "[DONE]") continue;
97
- if (event === "error") state.ok = false;
98
- try {
99
- applyEvent(JSON.parse(data), state, setThread);
100
- } catch {}
110
+ const state = {
111
+ reply: "",
112
+ toolCalls: /* @__PURE__ */ new Map(),
113
+ ok: true
114
+ };
115
+ const setThread = (id) => {
116
+ threadId = id;
117
+ };
118
+ try {
119
+ for await (const { event, data } of readSseEvents(res.body, signal)) {
120
+ if (data === "[DONE]") continue;
121
+ if (event === "error") state.ok = false;
122
+ try {
123
+ applyEvent(JSON.parse(data), state, setThread);
124
+ } catch {}
125
+ }
126
+ } catch {
127
+ state.ok = false;
101
128
  }
102
- } catch {
103
- state.ok = false;
129
+ if (signal.aborted) state.ok = false;
130
+ const toolCallDetails = [...state.toolCalls.values()];
131
+ return {
132
+ reply: state.reply,
133
+ toolCalls: toolCallDetails.map((c) => c.name),
134
+ toolCallDetails,
135
+ succeeded: state.ok,
136
+ sessionId: threadId,
137
+ traceId: state.traceId
138
+ };
104
139
  }
105
- if (signal.aborted) state.ok = false;
106
- return {
107
- reply: state.reply,
108
- toolCalls: state.toolCalls,
109
- succeeded: state.ok,
110
- sessionId: threadId,
111
- traceId: state.traceId
112
- };
113
- } };
140
+ };
114
141
  }
115
142
 
116
143
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"http-driver.js","names":[],"sources":["../../src/evals/http-driver.ts"],"sourcesContent":["import { readSseEvents } from \"../stream/sse-reader\";\nimport type { DriveResult, EvalDriver } from \"./types\";\n\nexport interface HttpDriverOptions {\n /** Base URL of the running app, e.g. `http://localhost:3000`. */\n baseUrl: string;\n /** Agent alias to target. Omit to use the app's default agent. */\n agent?: string;\n /** Extra request headers (e.g. auth for a deployed app). */\n headers?: Record<string, string>;\n /** Chat endpoint path. Defaults to `/api/agents/chat`. */\n path?: string;\n /** MLflow run id to link each turn's trace to (for evaluation runs). */\n mlflowRunId?: string;\n /**\n * Max wall-clock time for a single turn before it is abandoned as a failed\n * turn (`succeeded: false`). Without this a hung agent — a blocked tool, a\n * stalled model — never ends the SSE stream (heartbeats keep it alive), so\n * the read loop spins forever and wedges the whole sequential suite.\n * Defaults to 120s.\n */\n // ponytail: total per-turn cap; switch to an idle timeout if long legit turns get killed.\n timeoutMs?: number;\n}\n\n/** Mutable running totals accumulated while draining one turn's SSE stream. */\ninterface DriveState {\n reply: string;\n toolCalls: string[];\n seen: Set<string>;\n ok: boolean;\n traceId?: string;\n}\n\n/** Record a `function_call` output item once per call id (deduped). */\nfunction recordToolCall(\n item: { type?: string; name?: string; call_id?: string } | undefined,\n state: DriveState,\n): void {\n if (item?.type !== \"function_call\" || !item.name) return;\n const key = item.call_id ?? item.name;\n if (state.seen.has(key)) return;\n state.seen.add(key);\n state.toolCalls.push(item.name);\n}\n\n/** Apply an `appkit.metadata` event's thread/trace ids. */\nfunction applyMetadata(\n data: { threadId?: string; mlflowTraceId?: string } | undefined,\n state: DriveState,\n setThread: (id: string) => void,\n): void {\n if (data?.threadId) setThread(data.threadId);\n if (data?.mlflowTraceId) state.traceId = data.mlflowTraceId;\n}\n\n/** Parse a single Responses-API SSE `data:` payload into the running totals. */\nfunction applyEvent(\n event: Record<string, unknown>,\n state: DriveState,\n setThread: (id: string) => void,\n): void {\n const type = event.type;\n if (type === \"response.output_text.delta\") {\n if (typeof event.delta === \"string\") state.reply += event.delta;\n return;\n }\n if (\n type === \"response.output_item.added\" ||\n type === \"response.output_item.done\"\n ) {\n const item = event.item as {\n type?: string;\n name?: string;\n call_id?: string;\n content?: Array<{ text?: string }>;\n };\n recordToolCall(item, state);\n // A terminal `message` item carries the full reply text and *replaces*\n // the accumulated deltas — the server emits no delta for a full message\n // (event-translator `handleFullMessage`), so a non-streaming adapter's\n // whole reply arrives only here. Guard on non-empty so a streaming turn's\n // trailing done event can't clobber the deltas with \"\".\n if (type === \"response.output_item.done\" && item.type === \"message\") {\n const text = (item.content ?? []).map((c) => c.text ?? \"\").join(\"\");\n if (text) state.reply = text;\n }\n return;\n }\n if (type === \"error\" || type === \"response.failed\") {\n state.ok = false;\n return;\n }\n if (type === \"appkit.metadata\") {\n applyMetadata(\n event.data as { threadId?: string; mlflowTraceId?: string },\n state,\n setThread,\n );\n }\n}\n\n/** Build the chat request payload, including only the optional fields that are set. */\nfunction buildRequestBody(\n message: string,\n options: HttpDriverOptions,\n threadId: string | undefined,\n): string {\n return JSON.stringify({\n message,\n ...(options.agent ? { agent: options.agent } : {}),\n ...(threadId ? { threadId } : {}),\n ...(options.mlflowRunId ? { mlflowRunId: options.mlflowRunId } : {}),\n });\n}\n\n/**\n * Drives an agent by POSTing to a running app's chat endpoint and parsing the\n * SSE response. Keeps the thread id across `send`s so multi-turn evals share a\n * conversation. Agent/stream errors surface as `succeeded: false` rather than\n * throwing, so `t.succeeded()` can assert on them.\n */\nexport function createHttpDriver(options: HttpDriverOptions): EvalDriver {\n const chatPath = options.path ?? \"/api/agents/chat\";\n const timeoutMs = options.timeoutMs ?? 120_000;\n let threadId: string | undefined;\n\n return {\n async send(message: string): Promise<DriveResult> {\n // Bounds connect + the entire read below. Passed to both the fetch and\n // the SSE reader: on expiry the reader is cancelled and the turn fails.\n const signal = AbortSignal.timeout(timeoutMs);\n let res: Response;\n try {\n res = await fetch(`${options.baseUrl}${chatPath}`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", ...options.headers },\n body: buildRequestBody(message, options, threadId),\n // A chat endpoint never needs a redirect; following one would replay\n // custom auth headers (`options.headers`) to the redirect target.\n // A 3xx becomes an opaque response (res.ok === false), handled as a\n // failed turn by the !res.ok guard below.\n redirect: \"manual\",\n signal,\n });\n } catch {\n return { reply: \"\", toolCalls: [], succeeded: false };\n }\n\n if (!res.ok || !res.body) {\n return {\n reply: \"\",\n toolCalls: [],\n succeeded: false,\n sessionId: threadId,\n };\n }\n\n const state: DriveState = {\n reply: \"\",\n toolCalls: [],\n seen: new Set<string>(),\n ok: true,\n };\n const setThread = (id: string) => {\n threadId = id;\n };\n try {\n for await (const { event, data } of readSseEvents(res.body, signal)) {\n if (data === \"[DONE]\") continue;\n // A stream-level error frame (a thrown exception in the generator)\n // is framed as `event: error` with a payload that carries no `type`,\n // so the SSE event name — not the payload — is the real signal.\n if (event === \"error\") state.ok = false;\n try {\n applyEvent(\n JSON.parse(data) as Record<string, unknown>,\n state,\n setThread,\n );\n } catch {\n // skip malformed event payloads\n }\n }\n } catch {\n // mid-stream transport error or DoS-guard throw: a broken turn.\n state.ok = false;\n }\n // A timeout ends the iterator cleanly (reader cancel) rather than\n // throwing, so mark an aborted turn failed explicitly.\n if (signal.aborted) state.ok = false;\n\n return {\n reply: state.reply,\n toolCalls: state.toolCalls,\n succeeded: state.ok,\n sessionId: threadId,\n traceId: state.traceId,\n };\n },\n };\n}\n"],"mappings":";;;;AAmCA,SAAS,eACP,MACA,OACM;AACN,KAAI,MAAM,SAAS,mBAAmB,CAAC,KAAK,KAAM;CAClD,MAAM,MAAM,KAAK,WAAW,KAAK;AACjC,KAAI,MAAM,KAAK,IAAI,IAAI,CAAE;AACzB,OAAM,KAAK,IAAI,IAAI;AACnB,OAAM,UAAU,KAAK,KAAK,KAAK;;;AAIjC,SAAS,cACP,MACA,OACA,WACM;AACN,KAAI,MAAM,SAAU,WAAU,KAAK,SAAS;AAC5C,KAAI,MAAM,cAAe,OAAM,UAAU,KAAK;;;AAIhD,SAAS,WACP,OACA,OACA,WACM;CACN,MAAM,OAAO,MAAM;AACnB,KAAI,SAAS,8BAA8B;AACzC,MAAI,OAAO,MAAM,UAAU,SAAU,OAAM,SAAS,MAAM;AAC1D;;AAEF,KACE,SAAS,gCACT,SAAS,6BACT;EACA,MAAM,OAAO,MAAM;AAMnB,iBAAe,MAAM,MAAM;AAM3B,MAAI,SAAS,+BAA+B,KAAK,SAAS,WAAW;GACnE,MAAM,QAAQ,KAAK,WAAW,EAAE,EAAE,KAAK,MAAM,EAAE,QAAQ,GAAG,CAAC,KAAK,GAAG;AACnE,OAAI,KAAM,OAAM,QAAQ;;AAE1B;;AAEF,KAAI,SAAS,WAAW,SAAS,mBAAmB;AAClD,QAAM,KAAK;AACX;;AAEF,KAAI,SAAS,kBACX,eACE,MAAM,MACN,OACA,UACD;;;AAKL,SAAS,iBACP,SACA,SACA,UACQ;AACR,QAAO,KAAK,UAAU;EACpB;EACA,GAAI,QAAQ,QAAQ,EAAE,OAAO,QAAQ,OAAO,GAAG,EAAE;EACjD,GAAI,WAAW,EAAE,UAAU,GAAG,EAAE;EAChC,GAAI,QAAQ,cAAc,EAAE,aAAa,QAAQ,aAAa,GAAG,EAAE;EACpE,CAAC;;;;;;;;AASJ,SAAgB,iBAAiB,SAAwC;CACvE,MAAM,WAAW,QAAQ,QAAQ;CACjC,MAAM,YAAY,QAAQ,aAAa;CACvC,IAAI;AAEJ,QAAO,EACL,MAAM,KAAK,SAAuC;EAGhD,MAAM,SAAS,YAAY,QAAQ,UAAU;EAC7C,IAAI;AACJ,MAAI;AACF,SAAM,MAAM,MAAM,GAAG,QAAQ,UAAU,YAAY;IACjD,QAAQ;IACR,SAAS;KAAE,gBAAgB;KAAoB,GAAG,QAAQ;KAAS;IACnE,MAAM,iBAAiB,SAAS,SAAS,SAAS;IAKlD,UAAU;IACV;IACD,CAAC;UACI;AACN,UAAO;IAAE,OAAO;IAAI,WAAW,EAAE;IAAE,WAAW;IAAO;;AAGvD,MAAI,CAAC,IAAI,MAAM,CAAC,IAAI,KAClB,QAAO;GACL,OAAO;GACP,WAAW,EAAE;GACb,WAAW;GACX,WAAW;GACZ;EAGH,MAAM,QAAoB;GACxB,OAAO;GACP,WAAW,EAAE;GACb,sBAAM,IAAI,KAAa;GACvB,IAAI;GACL;EACD,MAAM,aAAa,OAAe;AAChC,cAAW;;AAEb,MAAI;AACF,cAAW,MAAM,EAAE,OAAO,UAAU,cAAc,IAAI,MAAM,OAAO,EAAE;AACnE,QAAI,SAAS,SAAU;AAIvB,QAAI,UAAU,QAAS,OAAM,KAAK;AAClC,QAAI;AACF,gBACE,KAAK,MAAM,KAAK,EAChB,OACA,UACD;YACK;;UAIJ;AAEN,SAAM,KAAK;;AAIb,MAAI,OAAO,QAAS,OAAM,KAAK;AAE/B,SAAO;GACL,OAAO,MAAM;GACb,WAAW,MAAM;GACjB,WAAW,MAAM;GACjB,WAAW;GACX,SAAS,MAAM;GAChB;IAEJ"}
1
+ {"version":3,"file":"http-driver.js","names":[],"sources":["../../src/evals/http-driver.ts"],"sourcesContent":["import { readSseEvents } from \"../stream/sse-reader\";\nimport type { DriveResult, EvalDriver } from \"./types\";\n\nexport interface HttpDriverOptions {\n /** Base URL of the running app, e.g. `http://localhost:3000`. */\n baseUrl: string;\n /** Agent alias to target. Omit to use the app's default agent. */\n agent?: string;\n /** Extra request headers (e.g. auth for a deployed app). */\n headers?: Record<string, string>;\n /** Chat endpoint path. Defaults to `/api/agents/chat`. */\n path?: string;\n /** MLflow run id to link each turn's trace to (for evaluation runs). */\n mlflowRunId?: string;\n /**\n * Max wall-clock time for a single turn before it is abandoned as a failed\n * turn (`succeeded: false`). Without this a hung agent — a blocked tool, a\n * stalled model — never ends the SSE stream (heartbeats keep it alive), so\n * the read loop spins forever and wedges the whole sequential suite.\n * Defaults to 120s.\n */\n // ponytail: total per-turn cap; switch to an idle timeout if long legit turns get killed.\n timeoutMs?: number;\n}\n\n/** Mutable running totals accumulated while draining one turn's SSE stream. */\ninterface DriveState {\n reply: string;\n /** Captured tool calls, keyed by `call_id ?? name` (dedupe). */\n toolCalls: Map<string, ToolCall>;\n ok: boolean;\n traceId?: string;\n}\n\n/**\n * Record a `function_call` output item, keyed by `call_id ?? name`, capturing\n * its parsed arguments. A later `done` event's fuller args win over the initial\n * `added` event's (often empty) args.\n */\nfunction recordToolCall(\n item:\n | { type?: string; name?: string; call_id?: string; arguments?: string }\n | undefined,\n state: DriveState,\n): void {\n if (item?.type !== \"function_call\" || !item.name) return;\n const key = item.call_id ?? item.name;\n const args = parseArgs(item.arguments);\n const existing = state.toolCalls.get(key);\n if (!existing) {\n state.toolCalls.set(key, { name: item.name, args });\n } else if (Object.keys(args).length > 0) {\n // The initial `added` event may carry empty args while the later `done`\n // carries the full arguments — keep the fuller set.\n existing.args = args;\n }\n}\n\n/** Apply an `appkit.metadata` event's thread/trace ids. */\nfunction applyMetadata(\n data: { threadId?: string; mlflowTraceId?: string } | undefined,\n state: DriveState,\n setThread: (id: string) => void,\n): void {\n if (data?.threadId) setThread(data.threadId);\n if (data?.mlflowTraceId) state.traceId = data.mlflowTraceId;\n}\n\n/** A single captured tool call, deduped by `call_id ?? name`. */\ntype ToolCall = { name: string; args: Record<string, unknown> };\n\n/** Parse a function-call `arguments` JSON string; `{}` on missing/invalid. */\nfunction parseArgs(raw: unknown): Record<string, unknown> {\n if (typeof raw !== \"string\" || raw.trim() === \"\") return {};\n try {\n const parsed = JSON.parse(raw);\n return parsed !== null &&\n typeof parsed === \"object\" &&\n !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\n/** Parse a single Responses-API SSE `data:` payload into the running totals. */\nfunction applyEvent(\n event: Record<string, unknown>,\n state: DriveState,\n setThread: (id: string) => void,\n): void {\n const type = event.type;\n if (type === \"response.output_text.delta\") {\n if (typeof event.delta === \"string\") state.reply += event.delta;\n return;\n }\n if (\n type === \"response.output_item.added\" ||\n type === \"response.output_item.done\"\n ) {\n const item = event.item as {\n type?: string;\n name?: string;\n call_id?: string;\n arguments?: string;\n content?: Array<{ text?: string }>;\n };\n recordToolCall(item, state);\n // A terminal `message` item carries the full reply text and *replaces*\n // the accumulated deltas — the server emits no delta for a full message\n // (event-translator `handleFullMessage`), so a non-streaming adapter's\n // whole reply arrives only here. Guard on non-empty so a streaming turn's\n // trailing done event can't clobber the deltas with \"\".\n if (type === \"response.output_item.done\" && item.type === \"message\") {\n const text = (item.content ?? []).map((c) => c.text ?? \"\").join(\"\");\n if (text) state.reply = text;\n }\n return;\n }\n if (type === \"error\" || type === \"response.failed\") {\n state.ok = false;\n return;\n }\n if (type === \"appkit.metadata\") {\n applyMetadata(\n event.data as { threadId?: string; mlflowTraceId?: string },\n state,\n setThread,\n );\n }\n}\n\n/** Build the chat request payload, including only the optional fields that are set. */\nfunction buildRequestBody(\n message: string,\n options: HttpDriverOptions,\n threadId: string | undefined,\n): string {\n return JSON.stringify({\n message,\n ...(options.agent ? { agent: options.agent } : {}),\n ...(threadId ? { threadId } : {}),\n ...(options.mlflowRunId ? { mlflowRunId: options.mlflowRunId } : {}),\n });\n}\n\n/**\n * Drives an agent by POSTing to a running app's chat endpoint and parsing the\n * SSE response. Keeps the thread id across `send`s so multi-turn evals share a\n * conversation. Agent/stream errors surface as `succeeded: false` rather than\n * throwing, so `t.succeeded()` can assert on them.\n */\nexport function createHttpDriver(options: HttpDriverOptions): EvalDriver {\n const chatPath = options.path ?? \"/api/agents/chat\";\n const timeoutMs = options.timeoutMs ?? 120_000;\n let threadId: string | undefined;\n\n return {\n reset(): void {\n threadId = undefined;\n },\n async send(\n message: string,\n opts?: { signal?: AbortSignal },\n ): Promise<DriveResult> {\n // Bounds connect + the entire read below. Passed to both the fetch and\n // the SSE reader: on expiry the reader is cancelled and the turn fails.\n // Composed with the caller's signal so a per-eval timeout also aborts this turn.\n const timeout = AbortSignal.timeout(timeoutMs);\n const signal = opts?.signal\n ? AbortSignal.any([timeout, opts.signal])\n : timeout;\n let res: Response;\n try {\n res = await fetch(`${options.baseUrl}${chatPath}`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", ...options.headers },\n body: buildRequestBody(message, options, threadId),\n // A chat endpoint never needs a redirect; following one would replay\n // custom auth headers (`options.headers`) to the redirect target.\n // A 3xx becomes an opaque response (res.ok === false), handled as a\n // failed turn by the !res.ok guard below.\n redirect: \"manual\",\n signal,\n });\n } catch {\n return {\n reply: \"\",\n toolCalls: [],\n toolCallDetails: [],\n succeeded: false,\n };\n }\n\n if (!res.ok || !res.body) {\n return {\n reply: \"\",\n toolCalls: [],\n toolCallDetails: [],\n succeeded: false,\n sessionId: threadId,\n };\n }\n\n const state: DriveState = {\n reply: \"\",\n toolCalls: new Map<string, ToolCall>(),\n ok: true,\n };\n const setThread = (id: string) => {\n threadId = id;\n };\n try {\n for await (const { event, data } of readSseEvents(res.body, signal)) {\n if (data === \"[DONE]\") continue;\n // A stream-level error frame (a thrown exception in the generator)\n // is framed as `event: error` with a payload that carries no `type`,\n // so the SSE event name — not the payload — is the real signal.\n if (event === \"error\") state.ok = false;\n try {\n applyEvent(\n JSON.parse(data) as Record<string, unknown>,\n state,\n setThread,\n );\n } catch {\n // skip malformed event payloads\n }\n }\n } catch {\n // mid-stream transport error or DoS-guard throw: a broken turn.\n state.ok = false;\n }\n // A timeout ends the iterator cleanly (reader cancel) rather than\n // throwing, so mark an aborted turn failed explicitly.\n if (signal.aborted) state.ok = false;\n\n const toolCallDetails = [...state.toolCalls.values()];\n return {\n reply: state.reply,\n toolCalls: toolCallDetails.map((c) => c.name),\n toolCallDetails,\n succeeded: state.ok,\n sessionId: threadId,\n traceId: state.traceId,\n };\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import { DatabricksAuth, ResolveDatabricksAuthOptions, resolveDatabricksAuth, resolveWorkspaceClient } from "../connectors/mlflow/auth.js";
2
+ import { MlflowClient, PostResult, normalizeHost } from "../connectors/mlflow/client.js";
3
+ import "../connectors/mlflow/index.js";
4
+ import { DatasetRow, ReadEvalDatasetOptions, readEvalDataset, userTurns } from "./dataset.js";
5
+ import { AssertionHandle, AssertionResult, CustomJudgeSpec, DriveResult, EvalDefinition, EvalDriver, EvalResult, MatchResult, Matcher, Severity, TestContext } from "./types.js";
6
+ import { defineEval, defineEvalConfig } from "./define-eval.js";
7
+ import { DiscoveredEval, DiscoveredEvalConfig, discoverEvalConfigs, discoverEvalFiles } from "./discover.js";
8
+ import { HttpDriverOptions, createHttpDriver } from "./http-driver.js";
9
+ import { JudgeConfig, JudgeScore, configureJudge, isJudgeConfigured } from "./judge.js";
10
+ import { equals, includes, matches } from "./matchers.js";
11
+ import { Assessment, ReportOutcome, buildAssessments, reportToMlflow } from "./mlflow-report.js";
12
+ import { EvalSummary, evalGlyph, formatEvalDetail, formatEvalHeadline, formatEvalResults, formatResultsJUnit, formatResultsJson, formatSummaryLine, summarize } from "./report.js";
13
+ import { RunEvalOptions, runEval } from "./run-eval.js";
14
+ import { EvalProgress, EvalRunSummary, RunEvalsOptions, runEvalsInDir, runWithRetries } from "./run-evals.js";
@@ -1,13 +1,14 @@
1
- import { resolveDatabricksAuth } from "../connectors/mlflow/auth.js";
1
+ import { resolveDatabricksAuth, resolveWorkspaceClient } from "../connectors/mlflow/auth.js";
2
2
  import { MlflowClient, normalizeHost } from "../connectors/mlflow/client.js";
3
- import { defineEval } from "./define-eval.js";
4
- import { discoverEvalFiles } from "./discover.js";
3
+ import { readEvalDataset, userTurns } from "./dataset.js";
4
+ import { defineEval, defineEvalConfig } from "./define-eval.js";
5
+ import { discoverEvalConfigs, discoverEvalFiles } from "./discover.js";
5
6
  import { createHttpDriver } from "./http-driver.js";
6
7
  import { configureJudge, isJudgeConfigured } from "./judge.js";
7
8
  import { equals, includes, matches } from "./matchers.js";
8
9
  import { buildAssessments, reportToMlflow } from "./mlflow-report.js";
9
- import { evalGlyph, formatEvalDetail, formatEvalHeadline, formatEvalResults, formatSummaryLine, summarize } from "./report.js";
10
+ import { evalGlyph, formatEvalDetail, formatEvalHeadline, formatEvalResults, formatResultsJUnit, formatResultsJson, formatSummaryLine, summarize } from "./report.js";
10
11
  import { runEval } from "./run-eval.js";
11
- import { runEvalsInDir } from "./run-evals.js";
12
+ import { runEvalsInDir, runWithRetries } from "./run-evals.js";
12
13
 
13
14
  export { };
@@ -1,4 +1,5 @@
1
1
  import { MlflowClient } from "../connectors/mlflow/client.js";
2
+ import "../connectors/mlflow/index.js";
2
3
 
3
4
  //#region src/evals/judge.d.ts
4
5
  interface JudgeConfig {
@@ -1 +1 @@
1
- {"version":3,"file":"judge.d.ts","names":[],"sources":["../../src/evals/judge.ts"],"mappings":";;;UAuBiB,WAAA;;EAEf,MAAA,EAAQ,YAAA;EAFkB;EAI1B,KAAA;EAFoB;EAIpB,KAAA;AAAA;;UAIe,UAAA;EACf,KAAA;EACA,SAAA;AAAA;;;;;AAQF;iBAAsB,cAAA,CAAe,MAAA,EAAQ,WAAA,GAAc,OAAA;AAAA,iBAe3C,iBAAA,CAAA"}
1
+ {"version":3,"file":"judge.d.ts","names":[],"sources":["../../src/evals/judge.ts"],"mappings":";;;;UAuBiB,WAAA;;EAEf,MAAA,EAAQ,YAAA;EAFO;EAIf,KAAA;;EAEA,KAAA;AAAA;;UAIe,UAAA;EACf,KAAA;EACA,SAAA;AAAA;AAFF;;;;;AAAA,iBAUsB,cAAA,CAAe,MAAA,EAAQ,WAAA,GAAc,OAAA;AAAA,iBAe3C,iBAAA,CAAA"}
@@ -1,4 +1,5 @@
1
1
  import { MlflowClient } from "../connectors/mlflow/client.js";
2
+ import "../connectors/mlflow/index.js";
2
3
  import { EvalResult } from "./types.js";
3
4
 
4
5
  //#region src/evals/mlflow-report.d.ts
@@ -1 +1 @@
1
- {"version":3,"file":"mlflow-report.d.ts","names":[],"sources":["../../src/evals/mlflow-report.ts"],"mappings":";;;;;UAqBiB,UAAA;EACf,QAAA;EACA,eAAA;EACA,MAAA;IAAU,WAAA;IAA6C,SAAA;EAAA;EACvD,QAAA;IAAY,KAAA;EAAA;EACZ,SAAA;EACA,QAAA,GAAW,MAAA;AAAA;AAAA,UAGI,aAAA;EACf,OAAA;EACA,OAAA;EACA,QAAA,EAAU,KAAA;IAAQ,OAAA;IAAiB,MAAA;IAAiB,KAAA;EAAA;AAAA;AAAA,iBA2DtC,gBAAA,CAAiB,MAAA,EAAQ,UAAA,GAAa,UAAA;;;;;iBA6FhC,cAAA,CACpB,MAAA,EAAQ,YAAA,EACR,OAAA,EAAS,UAAA,IACT,cAAA,YACC,OAAA,CAAQ,aAAA"}
1
+ {"version":3,"file":"mlflow-report.d.ts","names":[],"sources":["../../src/evals/mlflow-report.ts"],"mappings":";;;;;;UAqBiB,UAAA;EACf,QAAA;EACA,eAAA;EACA,MAAA;IAAU,WAAA;IAA6C,SAAA;EAAA;EACvD,QAAA;IAAY,KAAA;EAAA;EACZ,SAAA;EACA,QAAA,GAAW,MAAA;AAAA;AAAA,UAGI,aAAA;EACf,OAAA;EACA,OAAA;EACA,QAAA,EAAU,KAAA;IAAQ,OAAA;IAAiB,MAAA;IAAiB,KAAA;EAAA;AAAA;AAAA,iBA2DtC,gBAAA,CAAiB,MAAA,EAAQ,UAAA,GAAa,UAAA;;;;;iBA6FhC,cAAA,CACpB,MAAA,EAAQ,YAAA,EACR,OAAA,EAAS,UAAA,IACT,cAAA,YACC,OAAA,CAAQ,aAAA"}
@@ -1,3 +1,5 @@
1
+ import "../connectors/mlflow/index.js";
2
+
1
3
  //#region src/evals/mlflow-run.d.ts
2
4
  interface FinishOutcome {
3
5
  finished: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"mlflow-run.d.ts","names":[],"sources":["../../src/evals/mlflow-run.ts"],"mappings":";UA6EiB,aAAA;EACf,QAAA;;EAEA,YAAA;;EAEA,WAAA;AAAA"}
1
+ {"version":3,"file":"mlflow-run.d.ts","names":[],"sources":["../../src/evals/mlflow-run.ts"],"mappings":";;;UA6EiB,aAAA;EACf,QAAA;;EAEA,YAAA;;EAEA,WAAA;AAAA"}
@@ -8,6 +8,8 @@ interface EvalSummary {
8
8
  skipped: number;
9
9
  /** True when no eval failed (skips don't count as failures). */
10
10
  allPassed: boolean;
11
+ /** Fraction of scored (non-skipped) evals that passed, 0..1 (1 when none scored). */
12
+ passRate: number;
11
13
  }
12
14
  declare function summarize(results: EvalResult[]): EvalSummary;
13
15
  /** Status glyph for a single eval result. */
@@ -20,6 +22,19 @@ declare function formatEvalDetail(result: EvalResult): string[];
20
22
  declare function formatSummaryLine(results: EvalResult[]): string;
21
23
  /** Render all results as a human-readable console report (non-streaming). */
22
24
  declare function formatEvalResults(results: EvalResult[]): string;
25
+ /**
26
+ * Render results as a machine-readable JSON report (2-space indented):
27
+ * `{ summary: EvalSummary, results: EvalResult[] }`. Faithful to the types —
28
+ * every field present on a result round-trips.
29
+ */
30
+ declare function formatResultsJson(results: EvalResult[]): string;
31
+ /**
32
+ * Render results as JUnit XML for standard CI test reporters: a single
33
+ * `<testsuite name="appkit-agent-evals">` with one `<testcase>` per result.
34
+ * Failures carry a `<failure>` (error or failing-gate summary); skips a
35
+ * `<skipped>`. All attribute/text values are XML-escaped.
36
+ */
37
+ declare function formatResultsJUnit(results: EvalResult[]): string;
23
38
  //#endregion
24
- export { EvalSummary, evalGlyph, formatEvalDetail, formatEvalHeadline, formatEvalResults, formatSummaryLine, summarize };
39
+ export { EvalSummary, evalGlyph, formatEvalDetail, formatEvalHeadline, formatEvalResults, formatResultsJUnit, formatResultsJson, formatSummaryLine, summarize };
25
40
  //# sourceMappingURL=report.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"report.d.ts","names":[],"sources":["../../src/evals/report.ts"],"mappings":";;;UAEiB,WAAA;EACf,KAAA;EACA,MAAA;EACA,MAAA;EACA,OAAA;EAJ0B;EAM1B,SAAA;AAAA;AAAA,iBAGc,SAAA,CAAU,OAAA,EAAS,UAAA,KAAe,WAAA;;iBAmBlC,SAAA,CAAU,MAAA,EAAQ,UAAA;;iBAMlB,kBAAA,CAAmB,MAAA,EAAQ,UAAA;AAzB3C;AAAA,iBAqCgB,gBAAA,CAAiB,MAAA,EAAQ,UAAA;;iBAYzB,iBAAA,CAAkB,OAAA,EAAS,UAAA;;iBAM3B,iBAAA,CAAkB,OAAA,EAAS,UAAA"}
1
+ {"version":3,"file":"report.d.ts","names":[],"sources":["../../src/evals/report.ts"],"mappings":";;;UAEiB,WAAA;EACf,KAAA;EACA,MAAA;EACA,MAAA;EACA,OAAA;EAJ0B;EAM1B,SAAA;EAJA;EAMA,QAAA;AAAA;AAAA,iBAGc,SAAA,CAAU,OAAA,EAAS,UAAA,KAAe,WAAA;;iBAqBlC,SAAA,CAAU,MAAA,EAAQ,UAAA;;iBAMlB,kBAAA,CAAmB,MAAA,EAAQ,UAAA;;iBAY3B,gBAAA,CAAiB,MAAA,EAAQ,UAAA;;iBAYzB,iBAAA,CAAkB,OAAA,EAAS,UAAA;;iBAM3B,iBAAA,CAAkB,OAAA,EAAS,UAAA;;;AApC3C;;;iBAoDgB,iBAAA,CAAkB,OAAA,EAAS,UAAA;;AA9C3C;;;;;iBA2FgB,kBAAA,CAAmB,OAAA,EAAS,UAAA"}
@@ -6,12 +6,14 @@ function summarize(results) {
6
6
  for (const r of results) if (r.skipped) skipped++;
7
7
  else if (r.passed) passed++;
8
8
  else failed++;
9
+ const scored = passed + failed;
9
10
  return {
10
11
  total: results.length,
11
12
  passed,
12
13
  failed,
13
14
  skipped,
14
- allPassed: failed === 0
15
+ allPassed: failed === 0,
16
+ passRate: scored === 0 ? 1 : passed / scored
15
17
  };
16
18
  }
17
19
  /** Status glyph for a single eval result. */
@@ -51,7 +53,67 @@ function formatEvalResults(results) {
51
53
  lines.push(formatSummaryLine(results));
52
54
  return lines.join("\n");
53
55
  }
56
+ /**
57
+ * Render results as a machine-readable JSON report (2-space indented):
58
+ * `{ summary: EvalSummary, results: EvalResult[] }`. Faithful to the types —
59
+ * every field present on a result round-trips.
60
+ */
61
+ function formatResultsJson(results) {
62
+ return JSON.stringify({
63
+ summary: summarize(results),
64
+ results
65
+ }, null, 2);
66
+ }
67
+ /**
68
+ * Drop the C0 control chars XML 1.0 forbids even when escaped (except tab, LF,
69
+ * CR) — a raw NUL or ANSI escape would otherwise make the JUnit doc reject on parse.
70
+ */
71
+ function stripXmlControlChars(value) {
72
+ let out = "";
73
+ for (const ch of value) {
74
+ const code = ch.codePointAt(0);
75
+ if (code >= 32 || code === 9 || code === 10 || code === 13) out += ch;
76
+ }
77
+ return out;
78
+ }
79
+ /** Escape a value for use in XML text/attribute content (control chars dropped). */
80
+ function escapeXml(value) {
81
+ return stripXmlControlChars(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
82
+ }
83
+ /** One-line reason a result failed: its error, else its failing gate labels. */
84
+ function failureMessage(result) {
85
+ if (result.error) return result.error;
86
+ const gates = result.assertions.filter((a) => !a.pass && a.severity === "gate").map((a) => a.detail ? `${a.label} — ${a.detail}` : a.label);
87
+ return gates.length ? gates.join("; ") : "eval failed";
88
+ }
89
+ /**
90
+ * Render results as JUnit XML for standard CI test reporters: a single
91
+ * `<testsuite name="appkit-agent-evals">` with one `<testcase>` per result.
92
+ * Failures carry a `<failure>` (error or failing-gate summary); skips a
93
+ * `<skipped>`. All attribute/text values are XML-escaped.
94
+ */
95
+ function formatResultsJUnit(results) {
96
+ const s = summarize(results);
97
+ const lines = [];
98
+ lines.push("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
99
+ lines.push(`<testsuite name="appkit-agent-evals" tests="${s.total}" failures="${s.failed}" skipped="${s.skipped}">`);
100
+ for (const r of results) {
101
+ const open = ` <testcase name="${escapeXml(r.id)}" classname="agent-eval"`;
102
+ if (r.skipped) {
103
+ lines.push(`${open}>`);
104
+ lines.push(r.skipped.reason ? ` <skipped message="${escapeXml(r.skipped.reason)}"/>` : " <skipped/>");
105
+ lines.push(" </testcase>");
106
+ } else if (!r.passed) {
107
+ const message = failureMessage(r);
108
+ lines.push(`${open}>`);
109
+ lines.push(` <failure message="${escapeXml(message)}"/>`);
110
+ lines.push(" </testcase>");
111
+ } else lines.push(`${open}/>`);
112
+ }
113
+ lines.push("</testsuite>");
114
+ return lines.join("\n");
115
+ }
54
116
 
55
117
  //#endregion
56
- export { evalGlyph, formatEvalDetail, formatEvalHeadline, formatEvalResults, formatSummaryLine, summarize };
118
+ export { evalGlyph, formatEvalDetail, formatEvalHeadline, formatEvalResults, formatResultsJUnit, formatResultsJson, formatSummaryLine, summarize };
57
119
  //# sourceMappingURL=report.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"report.js","names":[],"sources":["../../src/evals/report.ts"],"sourcesContent":["import type { EvalResult } from \"./types\";\n\nexport interface EvalSummary {\n total: number;\n passed: number;\n failed: number;\n skipped: number;\n /** True when no eval failed (skips don't count as failures). */\n allPassed: boolean;\n}\n\nexport function summarize(results: EvalResult[]): EvalSummary {\n let passed = 0;\n let failed = 0;\n let skipped = 0;\n for (const r of results) {\n if (r.skipped) skipped++;\n else if (r.passed) passed++;\n else failed++;\n }\n return {\n total: results.length,\n passed,\n failed,\n skipped,\n allPassed: failed === 0,\n };\n}\n\n/** Status glyph for a single eval result. */\nexport function evalGlyph(result: EvalResult): string {\n if (result.skipped) return \"−\";\n return result.passed ? \"✓\" : \"✗\";\n}\n\n/** The one-line header for a single eval result (no failure detail). */\nexport function formatEvalHeadline(result: EvalResult): string {\n if (result.skipped) {\n return `− ${result.id} (skipped${\n result.skipped.reason ? `: ${result.skipped.reason}` : \"\"\n })`;\n }\n return `${evalGlyph(result)} ${result.id}${\n result.description ? ` — ${result.description}` : \"\"\n }`;\n}\n\n/** Indented detail lines for a failing eval (error + failing assertions). */\nexport function formatEvalDetail(result: EvalResult): string[] {\n const lines: string[] = [];\n if (result.error) lines.push(` error: ${result.error}`);\n for (const a of result.assertions) {\n if (a.pass) continue;\n const tag = a.severity === \"soft\" ? \"soft\" : \"gate\";\n lines.push(` ✗ [${tag}] ${a.label}${a.detail ? ` — ${a.detail}` : \"\"}`);\n }\n return lines;\n}\n\n/** The final PASS/FAIL summary line. */\nexport function formatSummaryLine(results: EvalResult[]): string {\n const s = summarize(results);\n return `${s.allPassed ? \"PASS\" : \"FAIL\"} — ${s.passed} passed, ${s.failed} failed, ${s.skipped} skipped (${s.total} total)`;\n}\n\n/** Render all results as a human-readable console report (non-streaming). */\nexport function formatEvalResults(results: EvalResult[]): string {\n const lines: string[] = [];\n for (const r of results) {\n lines.push(formatEvalHeadline(r));\n lines.push(...formatEvalDetail(r));\n }\n lines.push(\"\");\n lines.push(formatSummaryLine(results));\n return lines.join(\"\\n\");\n}\n"],"mappings":";AAWA,SAAgB,UAAU,SAAoC;CAC5D,IAAI,SAAS;CACb,IAAI,SAAS;CACb,IAAI,UAAU;AACd,MAAK,MAAM,KAAK,QACd,KAAI,EAAE,QAAS;UACN,EAAE,OAAQ;KACd;AAEP,QAAO;EACL,OAAO,QAAQ;EACf;EACA;EACA;EACA,WAAW,WAAW;EACvB;;;AAIH,SAAgB,UAAU,QAA4B;AACpD,KAAI,OAAO,QAAS,QAAO;AAC3B,QAAO,OAAO,SAAS,MAAM;;;AAI/B,SAAgB,mBAAmB,QAA4B;AAC7D,KAAI,OAAO,QACT,QAAO,KAAK,OAAO,GAAG,WACpB,OAAO,QAAQ,SAAS,KAAK,OAAO,QAAQ,WAAW,GACxD;AAEH,QAAO,GAAG,UAAU,OAAO,CAAC,GAAG,OAAO,KACpC,OAAO,cAAc,MAAM,OAAO,gBAAgB;;;AAKtD,SAAgB,iBAAiB,QAA8B;CAC7D,MAAM,QAAkB,EAAE;AAC1B,KAAI,OAAO,MAAO,OAAM,KAAK,cAAc,OAAO,QAAQ;AAC1D,MAAK,MAAM,KAAK,OAAO,YAAY;AACjC,MAAI,EAAE,KAAM;EACZ,MAAM,MAAM,EAAE,aAAa,SAAS,SAAS;AAC7C,QAAM,KAAK,UAAU,IAAI,IAAI,EAAE,QAAQ,EAAE,SAAS,MAAM,EAAE,WAAW,KAAK;;AAE5E,QAAO;;;AAIT,SAAgB,kBAAkB,SAA+B;CAC/D,MAAM,IAAI,UAAU,QAAQ;AAC5B,QAAO,GAAG,EAAE,YAAY,SAAS,OAAO,KAAK,EAAE,OAAO,WAAW,EAAE,OAAO,WAAW,EAAE,QAAQ,YAAY,EAAE,MAAM;;;AAIrH,SAAgB,kBAAkB,SAA+B;CAC/D,MAAM,QAAkB,EAAE;AAC1B,MAAK,MAAM,KAAK,SAAS;AACvB,QAAM,KAAK,mBAAmB,EAAE,CAAC;AACjC,QAAM,KAAK,GAAG,iBAAiB,EAAE,CAAC;;AAEpC,OAAM,KAAK,GAAG;AACd,OAAM,KAAK,kBAAkB,QAAQ,CAAC;AACtC,QAAO,MAAM,KAAK,KAAK"}
1
+ {"version":3,"file":"report.js","names":[],"sources":["../../src/evals/report.ts"],"sourcesContent":["import type { EvalResult } from \"./types\";\n\nexport interface EvalSummary {\n total: number;\n passed: number;\n failed: number;\n skipped: number;\n /** True when no eval failed (skips don't count as failures). */\n allPassed: boolean;\n /** Fraction of scored (non-skipped) evals that passed, 0..1 (1 when none scored). */\n passRate: number;\n}\n\nexport function summarize(results: EvalResult[]): EvalSummary {\n let passed = 0;\n let failed = 0;\n let skipped = 0;\n for (const r of results) {\n if (r.skipped) skipped++;\n else if (r.passed) passed++;\n else failed++;\n }\n const scored = passed + failed;\n return {\n total: results.length,\n passed,\n failed,\n skipped,\n allPassed: failed === 0,\n passRate: scored === 0 ? 1 : passed / scored,\n };\n}\n\n/** Status glyph for a single eval result. */\nexport function evalGlyph(result: EvalResult): string {\n if (result.skipped) return \"−\";\n return result.passed ? \"✓\" : \"✗\";\n}\n\n/** The one-line header for a single eval result (no failure detail). */\nexport function formatEvalHeadline(result: EvalResult): string {\n if (result.skipped) {\n return `− ${result.id} (skipped${\n result.skipped.reason ? `: ${result.skipped.reason}` : \"\"\n })`;\n }\n return `${evalGlyph(result)} ${result.id}${\n result.description ? ` — ${result.description}` : \"\"\n }`;\n}\n\n/** Indented detail lines for a failing eval (error + failing assertions). */\nexport function formatEvalDetail(result: EvalResult): string[] {\n const lines: string[] = [];\n if (result.error) lines.push(` error: ${result.error}`);\n for (const a of result.assertions) {\n if (a.pass) continue;\n const tag = a.severity === \"soft\" ? \"soft\" : \"gate\";\n lines.push(` ✗ [${tag}] ${a.label}${a.detail ? ` — ${a.detail}` : \"\"}`);\n }\n return lines;\n}\n\n/** The final PASS/FAIL summary line. */\nexport function formatSummaryLine(results: EvalResult[]): string {\n const s = summarize(results);\n return `${s.allPassed ? \"PASS\" : \"FAIL\"} — ${s.passed} passed, ${s.failed} failed, ${s.skipped} skipped (${s.total} total)`;\n}\n\n/** Render all results as a human-readable console report (non-streaming). */\nexport function formatEvalResults(results: EvalResult[]): string {\n const lines: string[] = [];\n for (const r of results) {\n lines.push(formatEvalHeadline(r));\n lines.push(...formatEvalDetail(r));\n }\n lines.push(\"\");\n lines.push(formatSummaryLine(results));\n return lines.join(\"\\n\");\n}\n\n/**\n * Render results as a machine-readable JSON report (2-space indented):\n * `{ summary: EvalSummary, results: EvalResult[] }`. Faithful to the types —\n * every field present on a result round-trips.\n */\nexport function formatResultsJson(results: EvalResult[]): string {\n return JSON.stringify({ summary: summarize(results), results }, null, 2);\n}\n\n/**\n * Drop the C0 control chars XML 1.0 forbids even when escaped (except tab, LF,\n * CR) — a raw NUL or ANSI escape would otherwise make the JUnit doc reject on parse.\n */\nfunction stripXmlControlChars(value: string): string {\n // Code-point filter, not a regex: oxlint `no-control-regex` rejects the class.\n let out = \"\";\n for (const ch of value) {\n const code = ch.codePointAt(0) as number;\n if (code >= 0x20 || code === 0x09 || code === 0x0a || code === 0x0d) {\n out += ch;\n }\n }\n return out;\n}\n\n/** Escape a value for use in XML text/attribute content (control chars dropped). */\nfunction escapeXml(value: string): string {\n return stripXmlControlChars(value)\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n\n/** One-line reason a result failed: its error, else its failing gate labels. */\nfunction failureMessage(result: EvalResult): string {\n if (result.error) return result.error;\n const gates = result.assertions\n .filter((a) => !a.pass && a.severity === \"gate\")\n .map((a) => (a.detail ? `${a.label} — ${a.detail}` : a.label));\n return gates.length ? gates.join(\"; \") : \"eval failed\";\n}\n\n/**\n * Render results as JUnit XML for standard CI test reporters: a single\n * `<testsuite name=\"appkit-agent-evals\">` with one `<testcase>` per result.\n * Failures carry a `<failure>` (error or failing-gate summary); skips a\n * `<skipped>`. All attribute/text values are XML-escaped.\n */\nexport function formatResultsJUnit(results: EvalResult[]): string {\n const s = summarize(results);\n const lines: string[] = [];\n lines.push('<?xml version=\"1.0\" encoding=\"UTF-8\"?>');\n lines.push(\n `<testsuite name=\"appkit-agent-evals\" tests=\"${s.total}\" failures=\"${s.failed}\" skipped=\"${s.skipped}\">`,\n );\n for (const r of results) {\n const open = ` <testcase name=\"${escapeXml(r.id)}\" classname=\"agent-eval\"`;\n if (r.skipped) {\n lines.push(`${open}>`);\n lines.push(\n r.skipped.reason\n ? ` <skipped message=\"${escapeXml(r.skipped.reason)}\"/>`\n : \" <skipped/>\",\n );\n lines.push(\" </testcase>\");\n } else if (!r.passed) {\n const message = failureMessage(r);\n lines.push(`${open}>`);\n lines.push(` <failure message=\"${escapeXml(message)}\"/>`);\n lines.push(\" </testcase>\");\n } else {\n lines.push(`${open}/>`);\n }\n }\n lines.push(\"</testsuite>\");\n return lines.join(\"\\n\");\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { DatasetRow } from "./dataset.js";
1
2
  import { EvalDefinition, EvalDriver, EvalResult } from "./types.js";
2
3
 
3
4
  //#region src/evals/run-eval.d.ts
@@ -8,6 +9,13 @@ interface RunEvalOptions {
8
9
  driver: EvalDriver;
9
10
  /** When true, soft assertion failures also fail the eval. */
10
11
  strict?: boolean;
12
+ /** Dataset row bound to `t.input`/`t.expected` for dataset-driven evals. */
13
+ row?: DatasetRow;
14
+ /**
15
+ * Runner-level default per-eval timeout (ms). `def.timeoutMs` wins over this;
16
+ * when both are unset the eval runs unbounded (current behavior).
17
+ */
18
+ timeoutMs?: number;
11
19
  }
12
20
  /**
13
21
  * Run a single eval against a driver. Never throws for assertion or agent
@@ -1 +1 @@
1
- {"version":3,"file":"run-eval.d.ts","names":[],"sources":["../../src/evals/run-eval.ts"],"mappings":";;;UAsBiB,cAAA;;EAEf,EAAA;EAF6B;EAI7B,MAAA,EAAQ,UAAA;EAAU;EAElB,MAAA;AAAA;;;;;AAQF;iBAAsB,OAAA,CACpB,GAAA,EAAK,cAAA,EACL,OAAA,EAAS,cAAA,GACR,OAAA,CAAQ,UAAA"}
1
+ {"version":3,"file":"run-eval.d.ts","names":[],"sources":["../../src/evals/run-eval.ts"],"mappings":";;;;UAoDiB,cAAA;;EAEf,EAAA;EAF6B;EAI7B,MAAA,EAAQ,UAAA;EAIQ;EAFhB,MAAA;EAFA;EAIA,GAAA,GAAM,UAAA;EAFN;;;;EAOA,SAAA;AAAA;AAQF;;;;;AAAA,iBAAsB,OAAA,CACpB,GAAA,EAAK,cAAA,EACL,OAAA,EAAS,cAAA,GACR,OAAA,CAAQ,UAAA"}