@autoview/cli 0.1.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 (297) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +407 -0
  3. package/lib/AutoViewAgent.d.ts +109 -0
  4. package/lib/AutoViewAgent.js +123 -0
  5. package/lib/AutoViewAgent.js.map +1 -0
  6. package/lib/agent/emitMcpServer.d.ts +15 -0
  7. package/lib/agent/emitMcpServer.js +157 -0
  8. package/lib/agent/emitMcpServer.js.map +1 -0
  9. package/lib/agent/emitReport.d.ts +14 -0
  10. package/lib/agent/emitReport.js +85 -0
  11. package/lib/agent/emitReport.js.map +1 -0
  12. package/lib/agent/toolSurface.d.ts +130 -0
  13. package/lib/agent/toolSurface.js +342 -0
  14. package/lib/agent/toolSurface.js.map +1 -0
  15. package/lib/agent/verifyAgentTasks.d.ts +87 -0
  16. package/lib/agent/verifyAgentTasks.js +126 -0
  17. package/lib/agent/verifyAgentTasks.js.map +1 -0
  18. package/lib/cli/main.d.ts +2 -0
  19. package/lib/cli/main.js +295 -0
  20. package/lib/cli/main.js.map +1 -0
  21. package/lib/compiler/AutoViewInterfaceCompiler.d.ts +27 -0
  22. package/lib/compiler/AutoViewInterfaceCompiler.js +68 -0
  23. package/lib/compiler/AutoViewInterfaceCompiler.js.map +1 -0
  24. package/lib/constants/AutoViewFrontendTemplate.d.ts +1 -0
  25. package/lib/constants/AutoViewFrontendTemplate.js +46 -0
  26. package/lib/constants/AutoViewFrontendTemplate.js.map +1 -0
  27. package/lib/constants/AutoViewSystemPromptConstant.d.ts +5 -0
  28. package/lib/constants/AutoViewSystemPromptConstant.js +4 -0
  29. package/lib/constants/AutoViewSystemPromptConstant.js.map +1 -0
  30. package/lib/context/IAutoViewAgentContext.d.ts +60 -0
  31. package/lib/context/IAutoViewAgentContext.js +3 -0
  32. package/lib/context/IAutoViewAgentContext.js.map +1 -0
  33. package/lib/fromSwagger.d.ts +53 -0
  34. package/lib/fromSwagger.js +513 -0
  35. package/lib/fromSwagger.js.map +1 -0
  36. package/lib/generateDeterministic.d.ts +26 -0
  37. package/lib/generateDeterministic.js +75 -0
  38. package/lib/generateDeterministic.js.map +1 -0
  39. package/lib/index.d.ts +15 -0
  40. package/lib/index.js +41 -0
  41. package/lib/index.js.map +1 -0
  42. package/lib/orchestrate/orchestrateAutoView.d.ts +17 -0
  43. package/lib/orchestrate/orchestrateAutoView.js +491 -0
  44. package/lib/orchestrate/orchestrateAutoView.js.map +1 -0
  45. package/lib/orchestrate/orchestrateAutoViewProductPlan.d.ts +37 -0
  46. package/lib/orchestrate/orchestrateAutoViewProductPlan.js +109 -0
  47. package/lib/orchestrate/orchestrateAutoViewProductPlan.js.map +1 -0
  48. package/lib/orchestrate/orchestrateAutoViewRender.d.ts +133 -0
  49. package/lib/orchestrate/orchestrateAutoViewRender.js +943 -0
  50. package/lib/orchestrate/orchestrateAutoViewRender.js.map +1 -0
  51. package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.d.ts +24 -0
  52. package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.js +92 -0
  53. package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.js.map +1 -0
  54. package/lib/orchestrate/orchestrateAutoViewReview.d.ts +48 -0
  55. package/lib/orchestrate/orchestrateAutoViewReview.js +328 -0
  56. package/lib/orchestrate/orchestrateAutoViewReview.js.map +1 -0
  57. package/lib/orchestrate/orchestrateAutoViewScaffold.d.ts +45 -0
  58. package/lib/orchestrate/orchestrateAutoViewScaffold.js +586 -0
  59. package/lib/orchestrate/orchestrateAutoViewScaffold.js.map +1 -0
  60. package/lib/orchestrate/orchestrateAutoViewSdkStudy.d.ts +26 -0
  61. package/lib/orchestrate/orchestrateAutoViewSdkStudy.js +85 -0
  62. package/lib/orchestrate/orchestrateAutoViewSdkStudy.js.map +1 -0
  63. package/lib/orchestrate/structures/IAutoViewProductPlan.d.ts +96 -0
  64. package/lib/orchestrate/structures/IAutoViewProductPlan.js +3 -0
  65. package/lib/orchestrate/structures/IAutoViewProductPlan.js.map +1 -0
  66. package/lib/orchestrate/structures/IAutoViewProductPlanApplication.d.ts +38 -0
  67. package/lib/orchestrate/structures/IAutoViewProductPlanApplication.js +3 -0
  68. package/lib/orchestrate/structures/IAutoViewProductPlanApplication.js.map +1 -0
  69. package/lib/orchestrate/structures/IAutoViewRenderApplication.d.ts +38 -0
  70. package/lib/orchestrate/structures/IAutoViewRenderApplication.js +3 -0
  71. package/lib/orchestrate/structures/IAutoViewRenderApplication.js.map +1 -0
  72. package/lib/orchestrate/structures/IAutoViewReviewApplication.d.ts +40 -0
  73. package/lib/orchestrate/structures/IAutoViewReviewApplication.js +3 -0
  74. package/lib/orchestrate/structures/IAutoViewReviewApplication.js.map +1 -0
  75. package/lib/orchestrate/structures/IAutoViewSdkMap.d.ts +63 -0
  76. package/lib/orchestrate/structures/IAutoViewSdkMap.js +3 -0
  77. package/lib/orchestrate/structures/IAutoViewSdkMap.js.map +1 -0
  78. package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.d.ts +37 -0
  79. package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.js +3 -0
  80. package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.js.map +1 -0
  81. package/lib/orchestrate/utils/HistoryMessage.d.ts +10 -0
  82. package/lib/orchestrate/utils/HistoryMessage.js +25 -0
  83. package/lib/orchestrate/utils/HistoryMessage.js.map +1 -0
  84. package/lib/orchestrate/utils/auditFrontendRuntime.d.ts +53 -0
  85. package/lib/orchestrate/utils/auditFrontendRuntime.js +362 -0
  86. package/lib/orchestrate/utils/auditFrontendRuntime.js.map +1 -0
  87. package/lib/orchestrate/utils/buildDeterministicPlan.d.ts +4 -0
  88. package/lib/orchestrate/utils/buildDeterministicPlan.js +233 -0
  89. package/lib/orchestrate/utils/buildDeterministicPlan.js.map +1 -0
  90. package/lib/orchestrate/utils/buildDeterministicSdkMap.d.ts +22 -0
  91. package/lib/orchestrate/utils/buildDeterministicSdkMap.js +154 -0
  92. package/lib/orchestrate/utils/buildDeterministicSdkMap.js.map +1 -0
  93. package/lib/orchestrate/utils/cacheNodeModules.d.ts +31 -0
  94. package/lib/orchestrate/utils/cacheNodeModules.js +134 -0
  95. package/lib/orchestrate/utils/cacheNodeModules.js.map +1 -0
  96. package/lib/orchestrate/utils/describeEndpointPropsShape.d.ts +37 -0
  97. package/lib/orchestrate/utils/describeEndpointPropsShape.js +192 -0
  98. package/lib/orchestrate/utils/describeEndpointPropsShape.js.map +1 -0
  99. package/lib/orchestrate/utils/describeEndpointRequestBodyShape.d.ts +22 -0
  100. package/lib/orchestrate/utils/describeEndpointRequestBodyShape.js +29 -0
  101. package/lib/orchestrate/utils/describeEndpointRequestBodyShape.js.map +1 -0
  102. package/lib/orchestrate/utils/describeEndpointResponseShape.d.ts +19 -0
  103. package/lib/orchestrate/utils/describeEndpointResponseShape.js +30 -0
  104. package/lib/orchestrate/utils/describeEndpointResponseShape.js.map +1 -0
  105. package/lib/orchestrate/utils/executeCachedBatch.d.ts +22 -0
  106. package/lib/orchestrate/utils/executeCachedBatch.js +64 -0
  107. package/lib/orchestrate/utils/executeCachedBatch.js.map +1 -0
  108. package/lib/orchestrate/utils/loadShoppingFixture.d.ts +33 -0
  109. package/lib/orchestrate/utils/loadShoppingFixture.js +17 -0
  110. package/lib/orchestrate/utils/loadShoppingFixture.js.map +1 -0
  111. package/lib/orchestrate/utils/normalizeProductPlanPaths.d.ts +24 -0
  112. package/lib/orchestrate/utils/normalizeProductPlanPaths.js +77 -0
  113. package/lib/orchestrate/utils/normalizeProductPlanPaths.js.map +1 -0
  114. package/lib/orchestrate/utils/renderJsonSchema.d.ts +23 -0
  115. package/lib/orchestrate/utils/renderJsonSchema.js +122 -0
  116. package/lib/orchestrate/utils/renderJsonSchema.js.map +1 -0
  117. package/lib/orchestrate/utils/renderResourcePage.d.ts +36 -0
  118. package/lib/orchestrate/utils/renderResourcePage.js +1415 -0
  119. package/lib/orchestrate/utils/renderResourcePage.js.map +1 -0
  120. package/lib/orchestrate/utils/validateFrontendTypecheck.d.ts +109 -0
  121. package/lib/orchestrate/utils/validateFrontendTypecheck.js +274 -0
  122. package/lib/orchestrate/utils/validateFrontendTypecheck.js.map +1 -0
  123. package/lib/preview/renderPreview.d.ts +22 -0
  124. package/lib/preview/renderPreview.js +198 -0
  125. package/lib/preview/renderPreview.js.map +1 -0
  126. package/lib/typings/compiler.d.ts +39 -0
  127. package/lib/typings/compiler.js +3 -0
  128. package/lib/typings/compiler.js.map +1 -0
  129. package/lib/typings/events.d.ts +106 -0
  130. package/lib/typings/events.js +3 -0
  131. package/lib/typings/events.js.map +1 -0
  132. package/lib/typings/index.d.ts +10 -0
  133. package/lib/typings/index.js +27 -0
  134. package/lib/typings/index.js.map +1 -0
  135. package/lib/typings/misc.d.ts +78 -0
  136. package/lib/typings/misc.js +3 -0
  137. package/lib/typings/misc.js.map +1 -0
  138. package/lib/utils/ArrayUtil.d.ts +8 -0
  139. package/lib/utils/ArrayUtil.js +30 -0
  140. package/lib/utils/ArrayUtil.js.map +1 -0
  141. package/lib/utils/StringUtil.d.ts +11 -0
  142. package/lib/utils/StringUtil.js +28 -0
  143. package/lib/utils/StringUtil.js.map +1 -0
  144. package/lib/utils/classifyEndpoints.d.ts +62 -0
  145. package/lib/utils/classifyEndpoints.js +216 -0
  146. package/lib/utils/classifyEndpoints.js.map +1 -0
  147. package/lib/utils/endpointFilter.d.ts +26 -0
  148. package/lib/utils/endpointFilter.js +0 -0
  149. package/lib/utils/endpointFilter.js.map +1 -0
  150. package/lib/utils/extractFields.d.ts +85 -0
  151. package/lib/utils/extractFields.js +231 -0
  152. package/lib/utils/extractFields.js.map +1 -0
  153. package/lib/utils/index.d.ts +13 -0
  154. package/lib/utils/index.js +30 -0
  155. package/lib/utils/index.js.map +1 -0
  156. package/lib/utils/normalizeForNestia.d.ts +34 -0
  157. package/lib/utils/normalizeForNestia.js +133 -0
  158. package/lib/utils/normalizeForNestia.js.map +1 -0
  159. package/lib/utils/resourcePlan.d.ts +39 -0
  160. package/lib/utils/resourcePlan.js +95 -0
  161. package/lib/utils/resourcePlan.js.map +1 -0
  162. package/lib/utils/sliceDocument.d.ts +17 -0
  163. package/lib/utils/sliceDocument.js +114 -0
  164. package/lib/utils/sliceDocument.js.map +1 -0
  165. package/lib/utils/toEndpoints.d.ts +90 -0
  166. package/lib/utils/toEndpoints.js +227 -0
  167. package/lib/utils/toEndpoints.js.map +1 -0
  168. package/lib/verify/runWorkflows.d.ts +25 -0
  169. package/lib/verify/runWorkflows.js +366 -0
  170. package/lib/verify/runWorkflows.js.map +1 -0
  171. package/lib/verify/workflows.d.ts +53 -0
  172. package/lib/verify/workflows.js +107 -0
  173. package/lib/verify/workflows.js.map +1 -0
  174. package/package.json +82 -0
  175. package/prompts/AUTOVIEW_RENDER.md +398 -0
  176. package/prompts/AUTOVIEW_REVIEW.md +60 -0
  177. package/prompts/AUTOVIEW_SDK_STUDY.md +89 -0
  178. package/src/AutoViewAgent.ts +222 -0
  179. package/src/agent/emitMcpServer.integration.test.ts +168 -0
  180. package/src/agent/emitMcpServer.test.ts +51 -0
  181. package/src/agent/emitMcpServer.ts +178 -0
  182. package/src/agent/emitReport.ts +117 -0
  183. package/src/agent/toolSurface.test.ts +243 -0
  184. package/src/agent/toolSurface.ts +501 -0
  185. package/src/agent/verifyAgentTasks.test.ts +106 -0
  186. package/src/agent/verifyAgentTasks.ts +171 -0
  187. package/src/cli/main.ts +363 -0
  188. package/src/compiler/AutoViewInterfaceCompiler.ts +69 -0
  189. package/src/constants/AutoViewFrontendTemplate.ts +42 -0
  190. package/src/constants/AutoViewSystemPromptConstant.ts +6 -0
  191. package/src/context/IAutoViewAgentContext.ts +84 -0
  192. package/src/fromSwagger.test.ts +269 -0
  193. package/src/fromSwagger.ts +500 -0
  194. package/src/generateDeterministic.test.ts +39 -0
  195. package/src/generateDeterministic.ts +77 -0
  196. package/src/index.ts +30 -0
  197. package/src/orchestrate/orchestrateAutoView.ts +590 -0
  198. package/src/orchestrate/orchestrateAutoViewProductPlan.ts +121 -0
  199. package/src/orchestrate/orchestrateAutoViewRender.ts +1117 -0
  200. package/src/orchestrate/orchestrateAutoViewRenderDeterministic.ts +101 -0
  201. package/src/orchestrate/orchestrateAutoViewReview.ts +272 -0
  202. package/src/orchestrate/orchestrateAutoViewScaffold.ts +627 -0
  203. package/src/orchestrate/orchestrateAutoViewSdkStudy.ts +90 -0
  204. package/src/orchestrate/renderNavTs.test.ts +74 -0
  205. package/src/orchestrate/structures/IAutoViewProductPlan.ts +119 -0
  206. package/src/orchestrate/structures/IAutoViewProductPlanApplication.ts +41 -0
  207. package/src/orchestrate/structures/IAutoViewRenderApplication.ts +40 -0
  208. package/src/orchestrate/structures/IAutoViewReviewApplication.ts +42 -0
  209. package/src/orchestrate/structures/IAutoViewSdkMap.ts +72 -0
  210. package/src/orchestrate/structures/IAutoViewSdkStudyApplication.ts +40 -0
  211. package/src/orchestrate/utils/HistoryMessage.ts +41 -0
  212. package/src/orchestrate/utils/auditFrontendRuntime.test.ts +18 -0
  213. package/src/orchestrate/utils/auditFrontendRuntime.ts +454 -0
  214. package/src/orchestrate/utils/buildDeterministicPlan.test.ts +170 -0
  215. package/src/orchestrate/utils/buildDeterministicPlan.ts +289 -0
  216. package/src/orchestrate/utils/buildDeterministicSdkMap.test.ts +90 -0
  217. package/src/orchestrate/utils/buildDeterministicSdkMap.ts +169 -0
  218. package/src/orchestrate/utils/cacheNodeModules.ts +136 -0
  219. package/src/orchestrate/utils/describeEndpointPropsShape.test.ts +86 -0
  220. package/src/orchestrate/utils/describeEndpointPropsShape.ts +202 -0
  221. package/src/orchestrate/utils/describeEndpointRequestBodyShape.test.ts +87 -0
  222. package/src/orchestrate/utils/describeEndpointRequestBodyShape.ts +31 -0
  223. package/src/orchestrate/utils/describeEndpointResponseShape.test.ts +70 -0
  224. package/src/orchestrate/utils/describeEndpointResponseShape.ts +32 -0
  225. package/src/orchestrate/utils/executeCachedBatch.ts +59 -0
  226. package/src/orchestrate/utils/loadShoppingFixture.ts +52 -0
  227. package/src/orchestrate/utils/normalizeProductPlanPaths.ts +92 -0
  228. package/src/orchestrate/utils/renderJsonSchema.test.ts +162 -0
  229. package/src/orchestrate/utils/renderJsonSchema.ts +133 -0
  230. package/src/orchestrate/utils/renderResourcePage.test.ts +468 -0
  231. package/src/orchestrate/utils/renderResourcePage.ts +1624 -0
  232. package/src/orchestrate/utils/validateFrontendTypecheck.test.ts +32 -0
  233. package/src/orchestrate/utils/validateFrontendTypecheck.ts +335 -0
  234. package/src/preview/renderPreview.ts +273 -0
  235. package/src/typings/compiler.ts +47 -0
  236. package/src/typings/events.ts +155 -0
  237. package/src/typings/index.ts +10 -0
  238. package/src/typings/misc.ts +93 -0
  239. package/src/utils/ArrayUtil.ts +16 -0
  240. package/src/utils/StringUtil.ts +29 -0
  241. package/src/utils/classifyEndpoints.test.ts +86 -0
  242. package/src/utils/classifyEndpoints.ts +291 -0
  243. package/src/utils/endpointFilter.test.ts +50 -0
  244. package/src/utils/endpointFilter.ts +0 -0
  245. package/src/utils/extractFields.test.ts +82 -0
  246. package/src/utils/extractFields.ts +306 -0
  247. package/src/utils/index.ts +13 -0
  248. package/src/utils/normalizeForNestia.test.ts +93 -0
  249. package/src/utils/normalizeForNestia.ts +139 -0
  250. package/src/utils/resourcePlan.test.ts +104 -0
  251. package/src/utils/resourcePlan.ts +180 -0
  252. package/src/utils/sliceDocument.test.ts +85 -0
  253. package/src/utils/sliceDocument.ts +119 -0
  254. package/src/utils/toEndpoints.test.ts +251 -0
  255. package/src/utils/toEndpoints.ts +343 -0
  256. package/src/verify/runWorkflows.ts +403 -0
  257. package/src/verify/workflows.test.ts +117 -0
  258. package/src/verify/workflows.ts +154 -0
  259. package/template/CLAUDE.md +140 -0
  260. package/template/Dockerfile +31 -0
  261. package/template/PROMPT.md +80 -0
  262. package/template/SANDBOX.md +70 -0
  263. package/template/app/api/health/route.ts +10 -0
  264. package/template/app/globals.css +97 -0
  265. package/template/app/layout.tsx +30 -0
  266. package/template/app/page.tsx +19 -0
  267. package/template/components/AppShell.tsx +114 -0
  268. package/template/components/auto/CatalogGrid.tsx +159 -0
  269. package/template/components/auto/ConfirmButton.tsx +67 -0
  270. package/template/components/auto/EmbeddedCollection.tsx +144 -0
  271. package/template/components/auto/ResourceDashboard.tsx +104 -0
  272. package/template/components/auto/ResourceDetail.tsx +93 -0
  273. package/template/components/auto/ResourceForm.tsx +235 -0
  274. package/template/components/auto/ResourceIcon.tsx +88 -0
  275. package/template/components/auto/ResourceLanding.tsx +155 -0
  276. package/template/components/auto/ResourceTable.tsx +223 -0
  277. package/template/components/auto/formatValue.tsx +186 -0
  278. package/template/components/auto/types.ts +42 -0
  279. package/template/components/ui/badge.tsx +40 -0
  280. package/template/components/ui/button.tsx +57 -0
  281. package/template/components/ui/card.tsx +86 -0
  282. package/template/components/ui/dialog.tsx +119 -0
  283. package/template/components/ui/input.tsx +23 -0
  284. package/template/components/ui/label.tsx +24 -0
  285. package/template/components/ui/pagination.tsx +117 -0
  286. package/template/components/ui/select.tsx +92 -0
  287. package/template/components/ui/sheet.tsx +135 -0
  288. package/template/components/ui/skeleton.tsx +15 -0
  289. package/template/components/ui/table.tsx +120 -0
  290. package/template/components/ui/tabs.tsx +55 -0
  291. package/template/lib/utils.ts +35 -0
  292. package/template/next.config.mjs +52 -0
  293. package/template/package.json +46 -0
  294. package/template/postcss.config.js +6 -0
  295. package/template/scripts/start-shopping-backend.sh +56 -0
  296. package/template/tailwind.config.ts +96 -0
  297. package/template/tsconfig.json +29 -0
@@ -0,0 +1,501 @@
1
+ import { OpenApi } from "@typia/interface";
2
+
3
+ import {
4
+ classifyEndpoints,
5
+ EndpointRole,
6
+ } from "../utils/classifyEndpoints";
7
+ import {
8
+ COLLECTION_FIELD_NAMES,
9
+ extractFields,
10
+ findCollectionField,
11
+ resolveProperties,
12
+ } from "../utils/extractFields";
13
+ import { IAutoViewEndpoint, toEndpoints } from "../utils/toEndpoints";
14
+
15
+ /**
16
+ * Agent tool surface — the deterministic projection of the API's semantic model
17
+ * (IR) into a set of agent-callable tools (MCP / function-calling shape).
18
+ *
19
+ * The same IR that drives the human frontend drives this: resource roles
20
+ * (read vs write), the resource hierarchy, and the producer→consumer chain (which
21
+ * tool's output supplies a given tool's id input). A naive OpenAPI→tools dump
22
+ * has none of that — it emits a flat bag of operations. This emits tools that
23
+ * carry:
24
+ *
25
+ * - `annotations.readOnlyHint` / `destructiveHint` — safety, so an agent (or its
26
+ * guardrails) can tell a read from a mutation without parsing prose.
27
+ * - per-input producer hints — "this `saleId` comes from the `sales.index`
28
+ * tool's `id` field" — so the chain is explicit, not inferred.
29
+ * - only renderable operations (path fragments like `/x#y` that nestia cannot
30
+ * express as a typed call are dropped — `toEndpoints` already excludes them).
31
+ *
32
+ * Pure and deterministic: same document → same tool surface.
33
+ */
34
+ export interface IAgentTool {
35
+ /** Dotted accessor, unique and a valid tool name (`shoppings.sales.index`). */
36
+ name: string;
37
+ description: string;
38
+ /** JSON Schema for the call arguments. */
39
+ inputSchema: {
40
+ type: "object";
41
+ properties: Record<string, unknown>;
42
+ required: string[];
43
+ };
44
+ /** MCP-style behavior hints. */
45
+ annotations: {
46
+ readOnlyHint: boolean;
47
+ destructiveHint: boolean;
48
+ idempotentHint: boolean;
49
+ };
50
+ /** Execution descriptor — how an MCP server / executor invokes the real API. */
51
+ operation: {
52
+ method: string;
53
+ path: string;
54
+ pathParams: string[];
55
+ hasQuery: boolean;
56
+ hasBody: boolean;
57
+ };
58
+ /**
59
+ * Producer links: for each id-like path param, the tool + field that produces
60
+ * its value. Empty when the surface could not resolve a producer.
61
+ */
62
+ producers: Array<{ param: string; tool: string; field: string }>;
63
+ }
64
+
65
+ const READ_ROLES = new Set<EndpointRole>(["list", "search", "detail"]);
66
+
67
+ /** Field names that identify a record (the value a path param consumes). */
68
+ const ID_NAMES = ["id", "uuid"];
69
+
70
+ /** The first identifying field name present (`id` preferred over `uuid`), or null. */
71
+ const firstIdName = (names: Iterable<string>): string | null => {
72
+ const set = new Set([...names].map((n) => n.toLowerCase()));
73
+ return ID_NAMES.find((n) => set.has(n)) ?? null;
74
+ };
75
+
76
+ /**
77
+ * Whether a read endpoint's response element carries an identifying field
78
+ * (`id` / `uuid`) — i.e. whether it can PRODUCE an id for a consumer. Handles
79
+ * both named component responses and INLINE response schemas, and collection
80
+ * wrappers named generically (`data`/`entries`) OR after the resource
81
+ * (`{ databases: [...] }`, DigitalOcean), including inline array items.
82
+ */
83
+ function producedIdField(
84
+ op: IAutoViewEndpoint,
85
+ doc: OpenApi.IDocument,
86
+ ): string | null {
87
+ // Named-type path (nestia/AutoBE convention).
88
+ if (op.responseBody !== null) {
89
+ const elementFields = op.responseBody.isArray
90
+ ? extractFields(op.responseBody.typeName, doc)
91
+ : (() => {
92
+ const top = extractFields(op.responseBody.typeName, doc);
93
+ // The collection field is either generically named (`data`/`entries`)
94
+ // or named after the resource (`{ namespaces: [...] }`, common on
95
+ // DigitalOcean once nestia promotes the inline response to a named
96
+ // type). Fall back to the first array field so the latter is found —
97
+ // producedIdField only runs on list/search reads, so an array IS the
98
+ // collection.
99
+ const col =
100
+ findCollectionField(top) ??
101
+ top.find((f) => f.kind === "array" && f.ref !== undefined);
102
+ return col?.ref !== undefined ? extractFields(col.ref, doc) : top;
103
+ })();
104
+ const named = firstIdName(elementFields.map((f) => f.name));
105
+ if (named !== null) return named;
106
+ // else: the promoted type was a dead/empty ref (nestia emits a `*.GetResponse`
107
+ // not registered in components, just like `*.GetQuery`) — fall through to the
108
+ // raw inline response schema, which still carries the real array.
109
+ }
110
+ // Inline response schema (DigitalOcean): introspect the raw schema.
111
+ if (op.responseSchema === null) return null;
112
+ const { properties } = resolveProperties(op.responseSchema, doc);
113
+ // The collection array: a known-named one, else the sole/first array property.
114
+ const arrayEntries = Object.entries(properties).filter(
115
+ ([, p]) => (p as { type?: unknown }).type === "array",
116
+ );
117
+ const collection =
118
+ arrayEntries.find(([n]) => COLLECTION_FIELD_NAMES.has(n.toLowerCase())) ??
119
+ arrayEntries[0];
120
+ if (collection !== undefined) {
121
+ const items = (collection[1] as { items?: OpenApi.IJsonSchema }).items;
122
+ if (items !== undefined) {
123
+ return firstIdName(Object.keys(resolveProperties(items, doc).properties));
124
+ }
125
+ }
126
+ // A single object (detail) returning an inline schema.
127
+ return firstIdName(Object.keys(properties));
128
+ }
129
+
130
+ /** Roles whose response is a browsable collection — the only legitimate id
131
+ * PRODUCERS. A `detail` read returns one object but needs the id to be called,
132
+ * so it cannot produce that id (circular); excluding it keeps navigability honest. */
133
+ const COLLECTION_ROLES = new Set<EndpointRole>(["list", "search"]);
134
+
135
+ interface IProducer {
136
+ tool: string;
137
+ field: string;
138
+ }
139
+
140
+ /**
141
+ * The shared producer model used by BOTH the tool surface and the consumability
142
+ * report, so they never disagree on what is navigable. For each resource, the
143
+ * collection read (list/search) whose element exposes an id — keyed by the
144
+ * endpoint's CLASSIFIED resource (so a verb path like `/pet/findByStatus` is
145
+ * bucketed under `pet`, not `findByStatus`).
146
+ */
147
+ function producerIndex(document: OpenApi.IDocument): {
148
+ endpoints: IAutoViewEndpoint[];
149
+ roleOf: Map<IAutoViewEndpoint, EndpointRole>;
150
+ producerOf: Map<string, IProducer>;
151
+ readableResources: Set<string>;
152
+ nested: Map<string, INestedProducer>;
153
+ } {
154
+ const endpoints = toEndpoints(document);
155
+ const roleOf = new Map<IAutoViewEndpoint, EndpointRole>();
156
+ const resourceOf = new Map<IAutoViewEndpoint, string>();
157
+ for (const g of classifyEndpoints(endpoints, document)) {
158
+ for (const c of g.endpoints) {
159
+ roleOf.set(c.endpoint, c.role);
160
+ resourceOf.set(c.endpoint, c.chain.at(-1)?.name ?? c.resource);
161
+ }
162
+ }
163
+ const producerOf = new Map<string, IProducer>();
164
+ const readableResources = new Set<string>();
165
+ for (const e of endpoints) {
166
+ const role = roleOf.get(e);
167
+ const resource = resourceOf.get(e);
168
+ if (role === undefined || resource === undefined) continue;
169
+ if (READ_ROLES.has(role)) readableResources.add(resource);
170
+ if (!COLLECTION_ROLES.has(role)) continue;
171
+ // A genuine producer ENUMERATES the resource. One that is itself scoped by
172
+ // that resource's own id (`/files/{file_id}/trash` → resource `files`, but
173
+ // it requires `file_id`) cannot produce that id — it is circular, so skip it.
174
+ const selfScoped = e.parameters.some(
175
+ (p) => resourceOfParam(e.path, p.name) === resource,
176
+ );
177
+ if (selfScoped) continue;
178
+ const field = producedIdField(e, document);
179
+ if (field !== null && !producerOf.has(resource)) {
180
+ producerOf.set(resource, { tool: e.accessor.join("."), field });
181
+ }
182
+ }
183
+ const nested = nestedProducers(endpoints, document, roleOf);
184
+ return { endpoints, roleOf, producerOf, readableResources, nested };
185
+ }
186
+
187
+ /** The named element type of a read endpoint's response (for recursion). */
188
+ function responseElementTypeName(
189
+ op: IAutoViewEndpoint,
190
+ doc: OpenApi.IDocument,
191
+ ): string | null {
192
+ if (op.responseBody === null) return null;
193
+ if (op.responseBody.isArray) return op.responseBody.typeName;
194
+ const col = findCollectionField(extractFields(op.responseBody.typeName, doc));
195
+ return col?.ref ?? op.responseBody.typeName;
196
+ }
197
+
198
+ /** Where a nested id is obtainable: a read tool, the array path to it, and its id field. */
199
+ interface INestedProducer {
200
+ tool: string;
201
+ /** Nested access path to the id-bearing element, e.g. `units[].stocks[]`. */
202
+ path: string;
203
+ field: string;
204
+ }
205
+
206
+ /**
207
+ * Resources whose ids are obtainable NESTED inside a read response, not from a
208
+ * dedicated endpoint — `order` detail returns `goods: IShoppingOrderGood[]`,
209
+ * `sale` returns `units: IShoppingSaleUnit[]` (each unit holds `stocks: […]`).
210
+ * Walks each read's response type graph (depth-bounded) and, for every
211
+ * array-field whose element carries an id, records WHICH read tool exposes it
212
+ * and the nested path — so the tool surface can hand the agent a real producer
213
+ * hint ("stockId ← sale.get, at units[].stocks[].id"), not just say it exists.
214
+ */
215
+ function nestedProducers(
216
+ endpoints: IAutoViewEndpoint[],
217
+ doc: OpenApi.IDocument,
218
+ roleOf: Map<IAutoViewEndpoint, EndpointRole>,
219
+ ): Map<string, INestedProducer> {
220
+ const out = new Map<string, INestedProducer>();
221
+ const visit = (
222
+ typeName: string,
223
+ tool: string,
224
+ parts: string[],
225
+ depth: number,
226
+ seen: Set<string>,
227
+ ): void => {
228
+ if (depth > 3 || seen.has(typeName)) return;
229
+ seen.add(typeName);
230
+ for (const f of extractFields(typeName, doc)) {
231
+ if (f.kind === "array" && f.ref !== undefined) {
232
+ const childParts = [...parts, f.name];
233
+ const idField = firstIdName(
234
+ extractFields(f.ref, doc).map((x) => x.name),
235
+ );
236
+ const key = f.name.toLowerCase();
237
+ if (idField !== null) {
238
+ // Prefer the SHALLOWEST path (fewest array hops) — `sale → units[]`
239
+ // beats `order → goods[].units[]` for the same `units` id, so the
240
+ // agent is pointed at the most direct producer.
241
+ const existing = out.get(key);
242
+ if (existing === undefined || childParts.length < existing.path.split(".").length) {
243
+ out.set(key, {
244
+ tool,
245
+ path: childParts.map((p) => `${p}[]`).join("."),
246
+ field: idField,
247
+ });
248
+ }
249
+ }
250
+ visit(f.ref, tool, childParts, depth + 1, seen); // recurse: units → stocks
251
+ } else if (f.kind === "ref" && f.ref !== undefined) {
252
+ visit(f.ref, tool, parts, depth + 1, seen);
253
+ }
254
+ }
255
+ };
256
+ for (const e of endpoints) {
257
+ const role = roleOf.get(e);
258
+ if (role === undefined || !READ_ROLES.has(role)) continue;
259
+ const el = responseElementTypeName(e, doc);
260
+ if (el !== null) visit(el, e.accessor.join("."), [], 0, new Set());
261
+ }
262
+ return out;
263
+ }
264
+
265
+ /** The static path segment that an `{param}` addresses (`/sales/{saleId}` → `sales`). */
266
+ function resourceOfParam(path: string, param: string): string | null {
267
+ const segs = path.split("/").filter((s) => s.length > 0);
268
+ for (let i = 0; i < segs.length; i++) {
269
+ if (segs[i] === `{${param}}`) return i > 0 ? segs[i - 1]! : null;
270
+ }
271
+ return null;
272
+ }
273
+
274
+ function paramType(schema: OpenApi.IJsonSchema | undefined): "number" | "string" {
275
+ const type = (schema as { type?: unknown } | undefined)?.type;
276
+ return type === "integer" || type === "number" ? "number" : "string";
277
+ }
278
+
279
+ /**
280
+ * Build the agent tool surface from an OpenAPI document.
281
+ */
282
+ export function buildToolSurface(document: OpenApi.IDocument): IAgentTool[] {
283
+ const { endpoints, roleOf, producerOf, nested } = producerIndex(document);
284
+
285
+ return endpoints.map((e): IAgentTool => {
286
+ const role = roleOf.get(e);
287
+ const readOnly = role !== undefined && READ_ROLES.has(role);
288
+ const method = e.method.toUpperCase();
289
+ const name = e.accessor.join(".");
290
+
291
+ const properties: Record<string, unknown> = {};
292
+ const required: string[] = [];
293
+ const producers: IAgentTool["producers"] = [];
294
+ for (const p of e.parameters) {
295
+ const resource = resourceOfParam(e.path, p.name);
296
+ const direct = resource !== null ? producerOf.get(resource) : undefined;
297
+ const nestedP =
298
+ resource !== null ? nested.get(resource.toLowerCase()) : undefined;
299
+ let description = p.description || `path parameter ${p.name}`;
300
+ if (direct !== undefined && direct.tool !== name) {
301
+ // a dedicated list/search produces this id directly
302
+ producers.push({ param: p.name, tool: direct.tool, field: direct.field });
303
+ description = `the ${direct.field} of a ${resource}; obtain it from the \`${direct.tool}\` tool — use each item's \`${direct.field}\` field`;
304
+ } else if (nestedP !== undefined && nestedP.tool !== name) {
305
+ // the id lives nested inside a parent read's response
306
+ producers.push({
307
+ param: p.name,
308
+ tool: nestedP.tool,
309
+ field: `${nestedP.path}.${nestedP.field}`,
310
+ });
311
+ description = `the ${nestedP.field} of a ${resource}; obtain it from the \`${nestedP.tool}\` tool's response at \`${nestedP.path}.${nestedP.field}\``;
312
+ }
313
+ properties[p.name] = { type: paramType(p.schema), description };
314
+ required.push(p.name);
315
+ }
316
+ if (e.query !== null) {
317
+ properties.query = {
318
+ type: "object",
319
+ description: "optional search / filter / pagination",
320
+ };
321
+ }
322
+ if (e.requestBody !== null) {
323
+ properties.body = {
324
+ type: "object",
325
+ description: `request body (${e.requestBody.typeName})`,
326
+ };
327
+ required.push("body");
328
+ }
329
+
330
+ const tag = readOnly ? "[READ]" : "[WRITE — mutates server state]";
331
+ const chainNote =
332
+ producers.length > 0
333
+ ? ` Inputs come from: ${producers.map((p) => `${p.param} ← ${p.tool}.${p.field}`).join("; ")}.`
334
+ : "";
335
+
336
+ return {
337
+ name,
338
+ description: `${tag} ${e.description || `${method} ${e.path}`}`.trim() + chainNote,
339
+ inputSchema: { type: "object", properties, required },
340
+ annotations: {
341
+ readOnlyHint: readOnly,
342
+ destructiveHint: method === "DELETE",
343
+ idempotentHint: method === "GET" || method === "PUT" || method === "DELETE",
344
+ },
345
+ operation: {
346
+ method,
347
+ path: e.path,
348
+ pathParams: e.parameters.map((p) => p.name),
349
+ hasQuery: e.query !== null,
350
+ hasBody: e.requestBody !== null,
351
+ },
352
+ producers,
353
+ };
354
+ });
355
+ }
356
+
357
+ /**
358
+ * Honest consumability analysis with a 4-way split per path-param input, so the
359
+ * report never accuses an API of a design gap it cannot actually prove:
360
+ *
361
+ * - `resolved` — a collection read (list/search) produces the id; the tool
362
+ * surface declares that producer. Directly navigable, and the agent is told
363
+ * exactly where the id comes from.
364
+ * - `nested` — the id is obtainable nested inside a parent read's response
365
+ * (`order` → `goods[].id`, `sale` → `units[].stocks[].id`), but no dedicated
366
+ * list tool produces it. Navigable, just not via a single tool — counted
367
+ * honestly, separate from `resolved` so the report matches the tool surface.
368
+ * - `untraceable`— the resource HAS a read endpoint, but no id could be found in
369
+ * its response (e.g. an inline schema this analysis can't fully introspect).
370
+ * Undetermined, not blamed.
371
+ * - `userInput` — the param is not an entity id but a human-known key
372
+ * (`repository_name`, `template_key`, a `scope`/`slug`), supplied by the
373
+ * caller — a missing producer is expected, NOT a defect.
374
+ * - `orphan` — an entity id (`*_id` / `uuid`) that NO endpoint produces. A
375
+ * genuine consumability defect.
376
+ */
377
+ export interface IConsumabilityAnalysis {
378
+ tools: number;
379
+ readTools: number;
380
+ writeTools: number;
381
+ referenceInputs: number;
382
+ resolved: number;
383
+ nested: number;
384
+ untraceable: Array<{ tool: string; param: string; resource: string }>;
385
+ userInputs: Array<{ tool: string; param: string; resource: string }>;
386
+ orphan: Array<{ tool: string; param: string; resource: string }>;
387
+ }
388
+
389
+ /**
390
+ * True when a path param names an entity identifier (`id`, `*_id`, `*Id`,
391
+ * `uuid`) — the kind that must be produced by another endpoint. A param that is
392
+ * a human-known key instead (`repository_name`, `template_key`, `scope`, a
393
+ * `slug`/`code`) is caller-supplied, so its absence of a producer is not a gap.
394
+ */
395
+ function isIdLikeParam(name: string): boolean {
396
+ return /(^id$)|(_id$)|([a-z]Id$)|(^uuid$)|(_uuid$)|([a-z]Uuid$)/.test(name);
397
+ }
398
+
399
+ export function analyzeConsumability(
400
+ document: OpenApi.IDocument,
401
+ ): IConsumabilityAnalysis {
402
+ // ONE producer model, shared with the tool surface — so the report never
403
+ // claims an input is navigable that the tools give no producer hint for.
404
+ const { endpoints, roleOf, producerOf, readableResources, nested } =
405
+ producerIndex(document);
406
+
407
+ const isReadTool = (e: IAutoViewEndpoint): boolean => {
408
+ const role = roleOf.get(e);
409
+ return role !== undefined && READ_ROLES.has(role);
410
+ };
411
+ // Classify every id input once, then aggregate purely (no mutable counters).
412
+ const inputs = endpoints.flatMap((e) =>
413
+ e.parameters.map((p) => {
414
+ const resource = resourceOfParam(e.path, p.name);
415
+ const ref = { tool: e.accessor.join("."), param: p.name, resource: resource ?? "?" };
416
+ const status =
417
+ resource !== null && producerOf.has(resource)
418
+ ? "resolved"
419
+ : resource !== null && nested.has(resource.toLowerCase())
420
+ ? "nested"
421
+ : // a non-id param (`username`, `scope`, `*_name`) is a caller-known
422
+ // key regardless of whether its resource is readable — not a gap.
423
+ !isIdLikeParam(p.name)
424
+ ? "userInput"
425
+ : // an entity id: readable resource but no producer traced =
426
+ // undetermined; never read at all = a genuine orphan.
427
+ resource !== null && readableResources.has(resource)
428
+ ? "untraceable"
429
+ : "orphan";
430
+ return { ...ref, status };
431
+ }),
432
+ );
433
+ const readTools = endpoints.filter(isReadTool).length;
434
+ return {
435
+ tools: endpoints.length,
436
+ readTools,
437
+ writeTools: endpoints.length - readTools,
438
+ referenceInputs: inputs.length,
439
+ resolved: inputs.filter((i) => i.status === "resolved").length,
440
+ nested: inputs.filter((i) => i.status === "nested").length,
441
+ untraceable: inputs
442
+ .filter((i) => i.status === "untraceable")
443
+ .map(({ tool, param, resource }) => ({ tool, param, resource })),
444
+ userInputs: inputs
445
+ .filter((i) => i.status === "userInput")
446
+ .map(({ tool, param, resource }) => ({ tool, param, resource })),
447
+ orphan: inputs
448
+ .filter((i) => i.status === "orphan")
449
+ .map(({ tool, param, resource }) => ({ tool, param, resource })),
450
+ };
451
+ }
452
+
453
+ /** Coverage summary of a tool surface — how navigable it is for an agent. */
454
+ export interface IToolSurfaceCoverage {
455
+ tools: number;
456
+ /** Tools tagged read vs write. */
457
+ readTools: number;
458
+ writeTools: number;
459
+ /** id-like path-param inputs across the surface. */
460
+ referenceInputs: number;
461
+ /** reference inputs with a declared producer (the navigable fraction). */
462
+ resolvedInputs: number;
463
+ /** reference inputs with no producer — an agent must guess where they come from. */
464
+ orphanInputs: number;
465
+ /**
466
+ * The orphan inputs in detail: a required id with no tool in the API that
467
+ * produces it. An agent (or a human) cannot obtain this value from any
468
+ * endpoint — a consumability defect in the API design.
469
+ */
470
+ orphans: Array<{ tool: string; param: string }>;
471
+ }
472
+
473
+ /**
474
+ * Deterministic structural coverage of a tool surface — the LLM-independent
475
+ * verification: is the tool graph navigable (every id input has a declared
476
+ * producer)? Reported as a report card, distinct from running an agent.
477
+ */
478
+ export function coverToolSurface(tools: IAgentTool[]): IToolSurfaceCoverage {
479
+ // Classify every path param once, then aggregate purely (no mutable counters).
480
+ const inputs = tools.flatMap((t) => {
481
+ const producerParams = new Set(t.producers.map((p) => p.param));
482
+ return t.operation.pathParams.map((param) => ({
483
+ tool: t.name,
484
+ param,
485
+ resolved: producerParams.has(param),
486
+ }));
487
+ });
488
+ const readTools = tools.filter((t) => t.annotations.readOnlyHint).length;
489
+ const resolvedInputs = inputs.filter((i) => i.resolved).length;
490
+ return {
491
+ tools: tools.length,
492
+ readTools,
493
+ writeTools: tools.length - readTools,
494
+ referenceInputs: inputs.length,
495
+ resolvedInputs,
496
+ orphanInputs: inputs.length - resolvedInputs,
497
+ orphans: inputs
498
+ .filter((i) => !i.resolved)
499
+ .map(({ tool, param }) => ({ tool, param })),
500
+ };
501
+ }
@@ -0,0 +1,106 @@
1
+ import http from "http";
2
+
3
+ import { OpenApiConverter } from "@typia/utils";
4
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
5
+
6
+ import { buildToolSurface } from "./toolSurface";
7
+ import {
8
+ executeToolCall,
9
+ functionName,
10
+ IChatClient,
11
+ toFunctionTools,
12
+ verifyAgentTasks,
13
+ } from "./verifyAgentTasks";
14
+
15
+ function doc(paths: Record<string, unknown>, schemas: Record<string, unknown> = {}) {
16
+ return OpenApiConverter.upgradeDocument({
17
+ openapi: "3.0.0", info: { title: "Shop", version: "1.0.0" }, paths, components: { schemas },
18
+ } as never);
19
+ }
20
+
21
+ const fixture = doc(
22
+ {
23
+ "/sales": { get: { operationId: "index", summary: "List sales.", responses: { 200: { description: "ok", content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/Sale" } } } } } } } },
24
+ "/sales/{saleId}": { get: { operationId: "at", summary: "Get a sale.", parameters: [{ name: "saleId", in: "path", required: true, schema: { type: "string" } }], responses: { 200: { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Sale" } } } } } } },
25
+ },
26
+ { Sale: { type: "object", properties: { id: { type: "string" }, title: { type: "string" } }, required: ["id"] } },
27
+ );
28
+ const tools = buildToolSurface(fixture);
29
+
30
+ const SALES = [{ id: "sale_1", title: "MacBook" }, { id: "sale_2", title: "iPhone" }];
31
+ let server: http.Server;
32
+ let base = "";
33
+ beforeAll(async () => {
34
+ server = http.createServer((req, res) => {
35
+ const u = (req.url ?? "").split("?")[0];
36
+ res.writeHead(200, { "content-type": "application/json" });
37
+ if (u === "/sales") return res.end(JSON.stringify({ data: SALES }));
38
+ const m = u!.match(/^\/sales\/([^/]+)$/);
39
+ return res.end(JSON.stringify(m ? SALES.find((s) => s.id === m[1]) ?? {} : {}));
40
+ });
41
+ await new Promise<void>((ok) => server.listen(0, "127.0.0.1", () => ok()));
42
+ base = `http://127.0.0.1:${(server.address() as { port: number }).port}`;
43
+ });
44
+ afterAll(() => server.close());
45
+
46
+ describe("toFunctionTools", () => {
47
+ it("converts the surface to OpenAI function-calling tools", () => {
48
+ const fns = toFunctionTools(tools);
49
+ expect(fns).toHaveLength(tools.length);
50
+ expect(fns[0]!.type).toBe("function");
51
+ // dotted accessor names are sanitized to OpenAI's `[a-zA-Z0-9_-]` charset
52
+ expect(fns[0]!.function.name).toBe(functionName(tools[0]!.name));
53
+ expect(fns[0]!.function.name).not.toContain(".");
54
+ expect(fns[0]!.function.parameters).toBe(tools[0]!.inputSchema);
55
+ });
56
+ });
57
+
58
+ describe("executeToolCall", () => {
59
+ it("maps a list call to the right HTTP request and returns the body", async () => {
60
+ const list = tools.find((t) => t.name === "sales.get")!;
61
+ const res = await executeToolCall(tools, list.name, {}, { baseUrl: base });
62
+ expect(res.ok).toBe(true);
63
+ expect(res.text).toContain("MacBook");
64
+ });
65
+
66
+ it("substitutes path params for a detail call", async () => {
67
+ const detail = tools.find((t) => t.name === "sales.getBySaleid")!;
68
+ const res = await executeToolCall(tools, detail.name, { saleId: "sale_2" }, { baseUrl: base });
69
+ expect(res.text).toContain("iPhone");
70
+ });
71
+
72
+ it("returns an error for an unknown tool", async () => {
73
+ const res = await executeToolCall(tools, "nope", {}, { baseUrl: base });
74
+ expect(res.ok).toBe(false);
75
+ });
76
+ });
77
+
78
+ describe("verifyAgentTasks", () => {
79
+ it("drives the agent loop (scripted client), executes tools, and grades the task", async () => {
80
+ // a fake client: turn 1 → call the sales list tool; turn 2 → final answer
81
+ let turn = 0;
82
+ const client: IChatClient = {
83
+ chat: {
84
+ completions: {
85
+ create: async () => {
86
+ turn++;
87
+ if (turn === 1) {
88
+ return { choices: [{ message: { content: null, tool_calls: [{ id: "c1", function: { name: "sales.get", arguments: "{}" } }] } }] };
89
+ }
90
+ return { choices: [{ message: { content: "There are 2 sales: MacBook and iPhone." } }] };
91
+ },
92
+ },
93
+ },
94
+ };
95
+ const results = await verifyAgentTasks(fixture, {
96
+ client,
97
+ model: "fake",
98
+ baseUrl: base,
99
+ tasks: [{ id: "count", prompt: "How many sales?", expect: (a) => a.includes("2") }],
100
+ });
101
+ expect(results).toHaveLength(1);
102
+ expect(results[0]!.toolCalls).toEqual(["sales.get"]);
103
+ expect(results[0]!.passed).toBe(true);
104
+ expect(results[0]!.answer).toContain("MacBook");
105
+ });
106
+ });