@cursor/july 0.1.26 → 0.1.28

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 (324) hide show
  1. package/dist/ab.d.ts +6 -0
  2. package/dist/ab.d.ts.map +1 -1
  3. package/dist/ab.js +1 -0
  4. package/dist/artifacts.d.ts +34 -0
  5. package/dist/artifacts.d.ts.map +1 -0
  6. package/dist/artifacts.js +57 -0
  7. package/dist/bin/agent-serve.js +43 -12
  8. package/dist/channels/github/github-channel.d.ts +1 -1
  9. package/dist/channels/github/github-channel.d.ts.map +1 -1
  10. package/dist/channels/github/github-channel.js +14 -1
  11. package/dist/channels/github/types.d.ts +6 -1
  12. package/dist/channels/github/types.d.ts.map +1 -1
  13. package/dist/channels/slack/api.d.ts +2 -1
  14. package/dist/channels/slack/api.d.ts.map +1 -1
  15. package/dist/channels/slack/api.js +1 -0
  16. package/dist/channels/slack/auth.d.ts +13 -13
  17. package/dist/channels/slack/auth.d.ts.map +1 -1
  18. package/dist/channels/slack/auth.js +19 -10
  19. package/dist/channels/slack/cursor-account.d.ts +2 -1
  20. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  21. package/dist/channels/slack/cursor-account.js +3 -1
  22. package/dist/channels/slack/defaults.d.ts +7 -4
  23. package/dist/channels/slack/defaults.d.ts.map +1 -1
  24. package/dist/channels/slack/defaults.js +14 -4
  25. package/dist/channels/slack/dispatch.d.ts +10 -1
  26. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  27. package/dist/channels/slack/dispatch.js +59 -21
  28. package/dist/channels/slack/external-policy.d.ts +27 -0
  29. package/dist/channels/slack/external-policy.d.ts.map +1 -0
  30. package/dist/channels/slack/external-policy.js +53 -0
  31. package/dist/channels/slack/index.d.ts +1 -0
  32. package/dist/channels/slack/index.d.ts.map +1 -1
  33. package/dist/channels/slack/index.js +1 -0
  34. package/dist/channels/slack/interactive.d.ts +8 -1
  35. package/dist/channels/slack/interactive.d.ts.map +1 -1
  36. package/dist/channels/slack/interactive.js +43 -15
  37. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  38. package/dist/channels/slack/slack-channel.js +22 -8
  39. package/dist/channels/slack/types.d.ts +16 -1
  40. package/dist/channels/slack/types.d.ts.map +1 -1
  41. package/dist/docs/404.html +2 -2
  42. package/dist/docs/ab.html +3 -3
  43. package/dist/docs/assets/{app.DYcC9FY-.js → app.CqjrYaT2.js} +1 -1
  44. package/dist/docs/assets/chunks/@localSearchIndexroot.VWQoPFJ1.js +1 -0
  45. package/dist/docs/assets/chunks/{VPLocalSearchBox.o4N_knTV.js → VPLocalSearchBox.ag-MwUel.js} +1 -1
  46. package/dist/docs/assets/chunks/{theme.DQ-njyo0.js → theme.BS-DJWat.js} +2 -2
  47. package/dist/docs/assets/{guides_slack.md.D_xFKlnF.js → guides_slack.md.CjmJSvZS.js} +8 -4
  48. package/dist/docs/assets/guides_slack.md.CjmJSvZS.lean.js +1 -0
  49. package/dist/docs/assets/guides_webhooks.md.wSOYas3X.js +183 -0
  50. package/dist/docs/assets/{guides_webhooks.md.B1EswtUu.lean.js → guides_webhooks.md.wSOYas3X.lean.js} +1 -1
  51. package/dist/docs/assets/{index.md.Dfv5ic9t.js → index.md.CH_s5uZe.js} +1 -1
  52. package/dist/docs/assets/{reference_channels.md.DdmiKgqf.js → reference_channels.md.CDhTRfUz.js} +2 -2
  53. package/dist/docs/assets/{reference_channels.md.DdmiKgqf.lean.js → reference_channels.md.CDhTRfUz.lean.js} +1 -1
  54. package/dist/docs/building-with-agents.html +3 -3
  55. package/dist/docs/concepts.html +3 -3
  56. package/dist/docs/deployment.html +3 -3
  57. package/dist/docs/evals.html +3 -3
  58. package/dist/docs/example-agents/approval-buddy.html +3 -3
  59. package/dist/docs/example-agents/benny.html +3 -3
  60. package/dist/docs/example-agents/bugbot.html +3 -3
  61. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  62. package/dist/docs/example-agents/codeowners-review.html +3 -3
  63. package/dist/docs/example-agents/concierge.html +3 -3
  64. package/dist/docs/example-agents/fsd.html +3 -3
  65. package/dist/docs/example-agents/index.html +3 -3
  66. package/dist/docs/example-agents/knowledge-base.html +3 -3
  67. package/dist/docs/example-agents/oncall.html +3 -3
  68. package/dist/docs/example-agents/security-reviewer.html +3 -3
  69. package/dist/docs/example-agents/slack-agent.html +3 -3
  70. package/dist/docs/example-agents/weather-agent.html +3 -3
  71. package/dist/docs/guides/agent-to-agent.html +3 -3
  72. package/dist/docs/guides/cloud-runtime.html +3 -3
  73. package/dist/docs/guides/github.html +3 -3
  74. package/dist/docs/guides/human-in-the-loop.html +3 -3
  75. package/dist/docs/guides/mcp-oauth.html +3 -3
  76. package/dist/docs/guides/slack.html +11 -7
  77. package/dist/docs/guides/webhooks.html +138 -5
  78. package/dist/docs/hashmap.json +1 -1
  79. package/dist/docs/hillclimbing.html +3 -3
  80. package/dist/docs/index.html +5 -5
  81. package/dist/docs/quickstart.html +3 -3
  82. package/dist/docs/reference/agent-config.html +3 -3
  83. package/dist/docs/reference/channels.html +5 -5
  84. package/dist/docs/reference/cli.html +3 -3
  85. package/dist/docs/reference/connections.html +3 -3
  86. package/dist/docs/reference/hooks.html +3 -3
  87. package/dist/docs/reference/http-api.html +3 -3
  88. package/dist/docs/reference/instructions.html +3 -3
  89. package/dist/docs/reference/playground.html +3 -3
  90. package/dist/docs/reference/project-layout.html +3 -3
  91. package/dist/docs/reference/prompt.html +3 -3
  92. package/dist/docs/reference/schedules.html +3 -3
  93. package/dist/docs/reference/sessions.html +3 -3
  94. package/dist/docs/reference/skills.html +3 -3
  95. package/dist/docs/reference/subagents.html +3 -3
  96. package/dist/docs/reference/tools.html +3 -3
  97. package/dist/docs/scaffolding-agents.html +3 -3
  98. package/dist/docs/storage.html +3 -3
  99. package/dist/docs/troubleshooting.html +3 -3
  100. package/dist/evals/assertions.d.ts +10 -1
  101. package/dist/evals/assertions.d.ts.map +1 -1
  102. package/dist/evals/assertions.js +12 -0
  103. package/dist/evals.d.ts +1 -1
  104. package/dist/evals.d.ts.map +1 -1
  105. package/dist/index.d.ts +1 -0
  106. package/dist/index.d.ts.map +1 -1
  107. package/dist/index.js +1 -0
  108. package/dist/internal/ab-fold.d.ts.map +1 -1
  109. package/dist/internal/ab-fold.js +4 -0
  110. package/dist/internal/ab-snapshot.d.ts.map +1 -1
  111. package/dist/internal/ab-snapshot.js +1 -0
  112. package/dist/internal/artifacts-store.d.ts +45 -0
  113. package/dist/internal/artifacts-store.d.ts.map +1 -0
  114. package/dist/internal/artifacts-store.js +250 -0
  115. package/dist/internal/builtin-tools/artifacts.d.ts +10 -0
  116. package/dist/internal/builtin-tools/artifacts.d.ts.map +1 -0
  117. package/dist/internal/builtin-tools/artifacts.js +73 -0
  118. package/dist/internal/builtin-tools/index.d.ts +8 -1
  119. package/dist/internal/builtin-tools/index.d.ts.map +1 -1
  120. package/dist/internal/builtin-tools/index.js +15 -0
  121. package/dist/internal/cli-ax.d.ts +19 -0
  122. package/dist/internal/cli-ax.d.ts.map +1 -1
  123. package/dist/internal/cli-ax.js +196 -57
  124. package/dist/internal/cli-slack.d.ts.map +1 -1
  125. package/dist/internal/cli-slack.js +14 -6
  126. package/dist/internal/cursor-slack-relay.d.ts +2 -0
  127. package/dist/internal/cursor-slack-relay.d.ts.map +1 -1
  128. package/dist/internal/cursor-slack-relay.js +1 -0
  129. package/dist/internal/deploy-client.d.ts +2 -0
  130. package/dist/internal/deploy-client.d.ts.map +1 -1
  131. package/dist/internal/deploy-client.js +1 -0
  132. package/dist/internal/discovery.d.ts.map +1 -1
  133. package/dist/internal/discovery.js +91 -3
  134. package/dist/internal/distribution.d.ts.map +1 -1
  135. package/dist/internal/distribution.js +1 -0
  136. package/dist/internal/model-pricing.d.ts +49 -0
  137. package/dist/internal/model-pricing.d.ts.map +1 -0
  138. package/dist/internal/model-pricing.js +377 -0
  139. package/dist/internal/reminder-runner.d.ts.map +1 -1
  140. package/dist/internal/reminder-runner.js +1 -0
  141. package/dist/internal/schedule-runner.d.ts.map +1 -1
  142. package/dist/internal/schedule-runner.js +1 -0
  143. package/dist/internal/sdk-runner.d.ts.map +1 -1
  144. package/dist/internal/sdk-runner.js +9 -1
  145. package/dist/internal/server.d.ts.map +1 -1
  146. package/dist/internal/server.js +49 -5
  147. package/dist/internal/session-cost.d.ts +49 -0
  148. package/dist/internal/session-cost.d.ts.map +1 -0
  149. package/dist/internal/session-cost.js +175 -0
  150. package/dist/internal/session-engine.d.ts +35 -1
  151. package/dist/internal/session-engine.d.ts.map +1 -1
  152. package/dist/internal/session-engine.js +128 -7
  153. package/dist/internal/storage-coordinator.d.ts +15 -0
  154. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  155. package/dist/internal/storage-coordinator.js +52 -0
  156. package/dist/internal/trajectory.d.ts +9 -1
  157. package/dist/internal/trajectory.d.ts.map +1 -1
  158. package/dist/internal/trajectory.js +32 -5
  159. package/dist/playground/assets/index-Bul1WfrB.css +1 -0
  160. package/dist/playground/assets/index-CkN74U_L.js +87 -0
  161. package/dist/playground/index.html +2 -2
  162. package/dist/storage.d.ts +4 -0
  163. package/dist/storage.d.ts.map +1 -1
  164. package/dist/storage.js +4 -0
  165. package/dist/types.d.ts +164 -1
  166. package/dist/types.d.ts.map +1 -1
  167. package/docs/README.md +1 -0
  168. package/docs/guides/slack.md +32 -7
  169. package/docs/guides/webhooks.md +183 -0
  170. package/docs/reference/channels.md +2 -1
  171. package/package.json +26 -26
  172. package/skills/setup-slack/SKILL.md +3 -0
  173. package/src/ab.ts +7 -0
  174. package/src/bin/agent-serve.ts +42 -5
  175. package/src/channels/slack/auth.ts +29 -22
  176. package/src/channels/slack/cursor-account.ts +2 -0
  177. package/src/channels/slack/defaults.ts +24 -4
  178. package/src/channels/slack/dispatch.ts +42 -1
  179. package/src/channels/slack/external-policy.ts +62 -0
  180. package/src/channels/slack/index.ts +1 -0
  181. package/src/channels/slack/interactive.ts +42 -10
  182. package/src/channels/slack/slack-channel.ts +20 -3
  183. package/src/channels/slack/types.ts +12 -0
  184. package/src/internal/ab-fold.ts +4 -0
  185. package/src/internal/ab-snapshot.ts +1 -0
  186. package/src/internal/cli-ax.ts +205 -34
  187. package/src/internal/cursor-slack-relay.ts +3 -0
  188. package/src/internal/deploy-client.ts +3 -0
  189. package/src/internal/discovery.ts +8 -1
  190. package/src/internal/model-pricing.ts +426 -0
  191. package/src/internal/sdk-runner.ts +10 -2
  192. package/src/internal/server.ts +9 -5
  193. package/src/internal/session-cost.ts +231 -0
  194. package/src/internal/session-engine.ts +28 -0
  195. package/src/internal/trajectory.ts +45 -3
  196. package/src/types.ts +24 -1
  197. package/dist/channels/github/instrument.d.ts +0 -20
  198. package/dist/channels/github/instrument.d.ts.map +0 -1
  199. package/dist/docs/assets/chunks/@localSearchIndexroot.BQTzJjR_.js +0 -1
  200. package/dist/docs/assets/guides_slack.md.D_xFKlnF.lean.js +0 -1
  201. package/dist/docs/assets/guides_webhooks.md.B1EswtUu.js +0 -50
  202. package/dist/internal/json-dir-store.d.ts +0 -32
  203. package/dist/internal/json-dir-store.d.ts.map +0 -1
  204. package/dist/internal/json-dir-store.js +0 -100
  205. package/dist/playground/assets/index-CjOQ4hN9.css +0 -1
  206. package/dist/playground/assets/index-DqXdAFGa.js +0 -85
  207. package/src/bin/agent-serve.version.test.ts +0 -64
  208. package/src/channels/github/api.test.ts +0 -64
  209. package/src/channels/github/auth.test.ts +0 -105
  210. package/src/channels/github/cursor-account.test.ts +0 -204
  211. package/src/channels/github/forward.test.ts +0 -457
  212. package/src/channels/github/github.test.ts +0 -985
  213. package/src/channels/github/replay.test.ts +0 -179
  214. package/src/channels/slack/api.post-message.test.ts +0 -148
  215. package/src/channels/slack/approvals.test.ts +0 -328
  216. package/src/channels/slack/block-actions.test.ts +0 -469
  217. package/src/channels/slack/bot-mentions.test.ts +0 -267
  218. package/src/channels/slack/channel-watch.test.ts +0 -363
  219. package/src/channels/slack/cursor-account.test.ts +0 -301
  220. package/src/channels/slack/defaults.final-post.test.ts +0 -182
  221. package/src/channels/slack/dispatch.test.ts +0 -875
  222. package/src/channels/slack/eval-directive.test.ts +0 -273
  223. package/src/channels/slack/message-body.test.ts +0 -54
  224. package/src/channels/slack/nudge-store.test.ts +0 -143
  225. package/src/channels/slack/slack.test.ts +0 -391
  226. package/src/channels/slack/stop.test.ts +0 -23
  227. package/src/channels/slack/thread-context.test.ts +0 -202
  228. package/src/evals/assertions.test.ts +0 -617
  229. package/src/evals/expect.test.ts +0 -144
  230. package/src/evals/judge.test.ts +0 -181
  231. package/src/evals/loaders.test.ts +0 -132
  232. package/src/evals/matchers.test.ts +0 -95
  233. package/src/evals/reporters.test.ts +0 -303
  234. package/src/evals/run-facts.test.ts +0 -259
  235. package/src/internal/ab-snapshot.test.ts +0 -325
  236. package/src/internal/approval-gate.test.ts +0 -49
  237. package/src/internal/approvals.integration.test.ts +0 -383
  238. package/src/internal/artifacts-store.test.ts +0 -283
  239. package/src/internal/authored-loaders.test.ts +0 -31
  240. package/src/internal/builtin-tools/artifacts.test.ts +0 -247
  241. package/src/internal/builtin-tools/reminders.test.ts +0 -201
  242. package/src/internal/channel-route-schema.test.ts +0 -294
  243. package/src/internal/chat-attach.test.ts +0 -262
  244. package/src/internal/cli-deploy.test.ts +0 -1991
  245. package/src/internal/cli-docs.test.ts +0 -161
  246. package/src/internal/cli-mcp.test.ts +0 -789
  247. package/src/internal/cli-skills.test.ts +0 -133
  248. package/src/internal/cli-slack.test.ts +0 -1654
  249. package/src/internal/cloud-merge.test.ts +0 -74
  250. package/src/internal/cron.test.ts +0 -22
  251. package/src/internal/cursor/account-mcp.test.ts +0 -807
  252. package/src/internal/cursor/backend-client.test.ts +0 -591
  253. package/src/internal/cursor/credentials.test.ts +0 -351
  254. package/src/internal/cursor/github-credentials.test.ts +0 -136
  255. package/src/internal/cursor-account-mcp-auth.test.ts +0 -310
  256. package/src/internal/cursor-account.integration.test.ts +0 -441
  257. package/src/internal/cursor-event-relay.test.ts +0 -746
  258. package/src/internal/cursor-github-credentials.integration.test.ts +0 -271
  259. package/src/internal/cursor-slack-relay.test.ts +0 -525
  260. package/src/internal/deploy-source.test.ts +0 -111
  261. package/src/internal/discovery.artifact-tool.test.ts +0 -136
  262. package/src/internal/discovery.artifacts.test.ts +0 -119
  263. package/src/internal/discovery.builtin-tools.test.ts +0 -94
  264. package/src/internal/discovery.concurrency.test.ts +0 -60
  265. package/src/internal/discovery.cursor-account.test.ts +0 -133
  266. package/src/internal/discovery.cwd.test.ts +0 -83
  267. package/src/internal/discovery.hosting.test.ts +0 -80
  268. package/src/internal/discovery.identity.test.ts +0 -44
  269. package/src/internal/docs-site.test.ts +0 -66
  270. package/src/internal/duration.test.ts +0 -29
  271. package/src/internal/eval-judge-model.test.ts +0 -187
  272. package/src/internal/eval-run-store.cancel.test.ts +0 -142
  273. package/src/internal/eval-run-store.storage.test.ts +0 -211
  274. package/src/internal/eval-runner.http.test.ts +0 -403
  275. package/src/internal/eval-runner.run.test.ts +0 -928
  276. package/src/internal/evals-client.test.ts +0 -307
  277. package/src/internal/event-mapper.test.ts +0 -243
  278. package/src/internal/github-fanout.test.ts +0 -213
  279. package/src/internal/handleAgentServeTrigger.test.ts +0 -185
  280. package/src/internal/host-kv.test.ts +0 -82
  281. package/src/internal/host-platforms.test.ts +0 -126
  282. package/src/internal/http-channel.test.ts +0 -402
  283. package/src/internal/init-project.test.ts +0 -270
  284. package/src/internal/install-cursor-skills.test.ts +0 -262
  285. package/src/internal/local-env.test.ts +0 -120
  286. package/src/internal/log-ring.test.ts +0 -31
  287. package/src/internal/logs-client.test.ts +0 -350
  288. package/src/internal/mcp-endpoint.test.ts +0 -436
  289. package/src/internal/mcp-host.test.ts +0 -298
  290. package/src/internal/mcp-oauth.test.ts +0 -148
  291. package/src/internal/net.test.ts +0 -17
  292. package/src/internal/peer-connections.test.ts +0 -128
  293. package/src/internal/peer-mcp.integration.test.ts +0 -289
  294. package/src/internal/playground/toolchain.test.ts +0 -53
  295. package/src/internal/playground-cli.test.ts +0 -187
  296. package/src/internal/playground-proxy.test.ts +0 -376
  297. package/src/internal/prompt-context.integration.test.ts +0 -232
  298. package/src/internal/prompt-context.test.ts +0 -127
  299. package/src/internal/reminder-runner.test.ts +0 -432
  300. package/src/internal/reminder-store.test.ts +0 -53
  301. package/src/internal/request-headers.test.ts +0 -27
  302. package/src/internal/resolve-prod-target.test.ts +0 -787
  303. package/src/internal/resolved-connections.test.ts +0 -295
  304. package/src/internal/router.test.ts +0 -57
  305. package/src/internal/sdk-runner.test.ts +0 -290
  306. package/src/internal/server.artifacts.test.ts +0 -334
  307. package/src/internal/session-engine.artifacts.test.ts +0 -293
  308. package/src/internal/session-engine.coalesce.test.ts +0 -169
  309. package/src/internal/session-engine.concurrency.test.ts +0 -250
  310. package/src/internal/session-engine.host-oauth-mcp.test.ts +0 -110
  311. package/src/internal/session-engine.interrupt.test.ts +0 -577
  312. package/src/internal/session-engine.storage.test.ts +0 -547
  313. package/src/internal/session-urls.test.ts +0 -28
  314. package/src/internal/sessions-client.test.ts +0 -518
  315. package/src/internal/storage-coordinator.test.ts +0 -517
  316. package/src/internal/tool-call.test.ts +0 -458
  317. package/src/internal/tool-result.test.ts +0 -52
  318. package/src/internal/trajectory.approvals.test.ts +0 -83
  319. package/src/internal/trajectory.subagents.test.ts +0 -198
  320. package/src/internal/turn-governor.test.ts +0 -137
  321. package/src/internal/update-check.test.ts +0 -485
  322. package/src/internal/workspace.test.ts +0 -207
  323. package/src/storage-backends/cursor-hosted.test.ts +0 -121
  324. /package/dist/docs/assets/{index.md.Dfv5ic9t.lean.js → index.md.CH_s5uZe.lean.js} +0 -0
@@ -0,0 +1,426 @@
1
+ /**
2
+ * Best-effort dollar-cost estimation for agent turns.
3
+ *
4
+ * The Cursor harness streams token usage (`turn-ended`) but no dollar cost,
5
+ * and server-billed cost is only queryable for cloud agents — so the
6
+ * framework prices token usage against a built-in snapshot of Cursor's
7
+ * published per-model API rates. Estimates are marked `source: "estimate"`
8
+ * everywhere they surface (traces, playground, A/B metrics, CLI); the billed
9
+ * amount on the Cursor dashboard remains the source of truth.
10
+ *
11
+ * Unknown models produce no estimate rather than a wrong one. Deployments
12
+ * can extend or correct the table without a package upgrade via the
13
+ * `AGENT_SERVE_MODEL_PRICING` environment variable (a JSON object merged
14
+ * over the defaults, keyed by model id).
15
+ */
16
+
17
+ import type { ModelSetting, TurnCost, TurnUsage } from "../types.js";
18
+
19
+ /** USD per million tokens for one pricing tier. */
20
+ export interface ModelPricingTier {
21
+ inputUsdPerMTok: number;
22
+ outputUsdPerMTok: number;
23
+ cacheReadUsdPerMTok: number;
24
+ /** Defaults to {@link inputUsdPerMTok} when omitted (provider convention). */
25
+ cacheWriteUsdPerMTok?: number;
26
+ }
27
+
28
+ export interface ModelPricing extends ModelPricingTier {
29
+ /** Rates when the model's `fast` parameter is `"true"`. */
30
+ fast?: ModelPricingTier;
31
+ }
32
+
33
+ const M = 1_000_000;
34
+
35
+ /**
36
+ * Snapshot of Cursor API per-model token rates (USD per million tokens).
37
+ * Keyed by normalized model id; family aliases (`sonnet`, `gpt`, …) point at
38
+ * the model they currently resolve to.
39
+ */
40
+ export const DEFAULT_MODEL_PRICING: Readonly<Record<string, ModelPricing>> = {
41
+ // xAI / Cursor-served Grok
42
+ "grok-4.5": {
43
+ inputUsdPerMTok: 2,
44
+ outputUsdPerMTok: 6,
45
+ cacheReadUsdPerMTok: 0.5,
46
+ fast: { inputUsdPerMTok: 4, outputUsdPerMTok: 18, cacheReadUsdPerMTok: 1 },
47
+ },
48
+ "grok-4.20": {
49
+ inputUsdPerMTok: 2,
50
+ outputUsdPerMTok: 6,
51
+ cacheReadUsdPerMTok: 0.2,
52
+ },
53
+ "grok-4.3": {
54
+ inputUsdPerMTok: 1.25,
55
+ outputUsdPerMTok: 2.5,
56
+ cacheReadUsdPerMTok: 0.125,
57
+ },
58
+ "grok-code": {
59
+ inputUsdPerMTok: 0.2,
60
+ outputUsdPerMTok: 1.5,
61
+ cacheReadUsdPerMTok: 0.02,
62
+ },
63
+
64
+ // Anthropic
65
+ "opus-5": {
66
+ inputUsdPerMTok: 5,
67
+ outputUsdPerMTok: 25,
68
+ cacheReadUsdPerMTok: 0.5,
69
+ cacheWriteUsdPerMTok: 6.25,
70
+ fast: {
71
+ inputUsdPerMTok: 10,
72
+ outputUsdPerMTok: 50,
73
+ cacheReadUsdPerMTok: 1,
74
+ cacheWriteUsdPerMTok: 12.5,
75
+ },
76
+ },
77
+ "opus-4.8": {
78
+ inputUsdPerMTok: 5,
79
+ outputUsdPerMTok: 25,
80
+ cacheReadUsdPerMTok: 0.5,
81
+ cacheWriteUsdPerMTok: 6.25,
82
+ fast: {
83
+ inputUsdPerMTok: 10,
84
+ outputUsdPerMTok: 50,
85
+ cacheReadUsdPerMTok: 1,
86
+ cacheWriteUsdPerMTok: 12.5,
87
+ },
88
+ },
89
+ "opus-4.7": {
90
+ inputUsdPerMTok: 5,
91
+ outputUsdPerMTok: 25,
92
+ cacheReadUsdPerMTok: 0.5,
93
+ cacheWriteUsdPerMTok: 6.25,
94
+ fast: {
95
+ inputUsdPerMTok: 30,
96
+ outputUsdPerMTok: 150,
97
+ cacheReadUsdPerMTok: 3,
98
+ cacheWriteUsdPerMTok: 37.5,
99
+ },
100
+ },
101
+ "opus-4.6": {
102
+ inputUsdPerMTok: 5,
103
+ outputUsdPerMTok: 25,
104
+ cacheReadUsdPerMTok: 0.5,
105
+ cacheWriteUsdPerMTok: 6.25,
106
+ },
107
+ "opus-4.5": {
108
+ inputUsdPerMTok: 5,
109
+ outputUsdPerMTok: 25,
110
+ cacheReadUsdPerMTok: 0.5,
111
+ cacheWriteUsdPerMTok: 6.25,
112
+ },
113
+ "sonnet-5": {
114
+ inputUsdPerMTok: 3,
115
+ outputUsdPerMTok: 15,
116
+ cacheReadUsdPerMTok: 0.3,
117
+ cacheWriteUsdPerMTok: 3.75,
118
+ },
119
+ "sonnet-4.6": {
120
+ inputUsdPerMTok: 3,
121
+ outputUsdPerMTok: 15,
122
+ cacheReadUsdPerMTok: 0.3,
123
+ cacheWriteUsdPerMTok: 3.75,
124
+ },
125
+ "sonnet-4.5": {
126
+ inputUsdPerMTok: 3,
127
+ outputUsdPerMTok: 15,
128
+ cacheReadUsdPerMTok: 0.3,
129
+ cacheWriteUsdPerMTok: 3.75,
130
+ },
131
+ "haiku-4.5": {
132
+ inputUsdPerMTok: 1,
133
+ outputUsdPerMTok: 5,
134
+ cacheReadUsdPerMTok: 0.1,
135
+ cacheWriteUsdPerMTok: 1.25,
136
+ },
137
+ "fable-5": {
138
+ inputUsdPerMTok: 10,
139
+ outputUsdPerMTok: 50,
140
+ cacheReadUsdPerMTok: 1,
141
+ cacheWriteUsdPerMTok: 12.5,
142
+ },
143
+
144
+ // OpenAI
145
+ "gpt-5.6": {
146
+ inputUsdPerMTok: 5,
147
+ outputUsdPerMTok: 30,
148
+ cacheReadUsdPerMTok: 0.5,
149
+ cacheWriteUsdPerMTok: 6.25,
150
+ fast: {
151
+ inputUsdPerMTok: 10,
152
+ outputUsdPerMTok: 60,
153
+ cacheReadUsdPerMTok: 1,
154
+ cacheWriteUsdPerMTok: 12.5,
155
+ },
156
+ },
157
+ "gpt-5.5": {
158
+ inputUsdPerMTok: 5,
159
+ outputUsdPerMTok: 30,
160
+ cacheReadUsdPerMTok: 0.5,
161
+ },
162
+ "gpt-5.4": {
163
+ inputUsdPerMTok: 2.5,
164
+ outputUsdPerMTok: 15,
165
+ cacheReadUsdPerMTok: 0.25,
166
+ },
167
+ "gpt-5.2": {
168
+ inputUsdPerMTok: 1.75,
169
+ outputUsdPerMTok: 14,
170
+ cacheReadUsdPerMTok: 0.175,
171
+ },
172
+ "gpt-mini": {
173
+ inputUsdPerMTok: 0.75,
174
+ outputUsdPerMTok: 4.5,
175
+ cacheReadUsdPerMTok: 0.075,
176
+ },
177
+ "codex-5.3": {
178
+ inputUsdPerMTok: 1.75,
179
+ outputUsdPerMTok: 14,
180
+ cacheReadUsdPerMTok: 0.175,
181
+ },
182
+ "codex-5.2": {
183
+ inputUsdPerMTok: 1.75,
184
+ outputUsdPerMTok: 14,
185
+ cacheReadUsdPerMTok: 0.175,
186
+ },
187
+
188
+ // Google
189
+ "gemini-3.1-pro": {
190
+ inputUsdPerMTok: 2,
191
+ outputUsdPerMTok: 12,
192
+ cacheReadUsdPerMTok: 0.2,
193
+ },
194
+ "gemini-3-pro": {
195
+ inputUsdPerMTok: 2,
196
+ outputUsdPerMTok: 12,
197
+ cacheReadUsdPerMTok: 0.2,
198
+ },
199
+ "gemini-3.6-flash": {
200
+ inputUsdPerMTok: 1.5,
201
+ outputUsdPerMTok: 7.5,
202
+ cacheReadUsdPerMTok: 0.15,
203
+ },
204
+ "gemini-3.5-flash": {
205
+ inputUsdPerMTok: 1.5,
206
+ outputUsdPerMTok: 9,
207
+ cacheReadUsdPerMTok: 0.15,
208
+ },
209
+
210
+ // Cursor Composer
211
+ "composer-2.5": {
212
+ inputUsdPerMTok: 0.5,
213
+ outputUsdPerMTok: 2.5,
214
+ cacheReadUsdPerMTok: 0.2,
215
+ fast: {
216
+ inputUsdPerMTok: 3,
217
+ outputUsdPerMTok: 15,
218
+ cacheReadUsdPerMTok: 0.5,
219
+ },
220
+ },
221
+ "composer-2": {
222
+ inputUsdPerMTok: 0.5,
223
+ outputUsdPerMTok: 2.5,
224
+ cacheReadUsdPerMTok: 0.2,
225
+ },
226
+
227
+ // Moonshot / Zhipu
228
+ "kimi-k3": {
229
+ inputUsdPerMTok: 3,
230
+ outputUsdPerMTok: 15,
231
+ cacheReadUsdPerMTok: 0.3,
232
+ },
233
+ "kimi-k2.7": {
234
+ inputUsdPerMTok: 0.95,
235
+ outputUsdPerMTok: 4,
236
+ cacheReadUsdPerMTok: 0.19,
237
+ },
238
+ "kimi-k2.5": {
239
+ inputUsdPerMTok: 0.6,
240
+ outputUsdPerMTok: 3,
241
+ cacheReadUsdPerMTok: 0.1,
242
+ },
243
+ "glm-5.2": {
244
+ inputUsdPerMTok: 1.4,
245
+ outputUsdPerMTok: 4.4,
246
+ cacheReadUsdPerMTok: 0.26,
247
+ },
248
+ };
249
+
250
+ /** Family aliases → the model id they currently resolve to. */
251
+ const MODEL_ID_ALIASES: Readonly<Record<string, string>> = {
252
+ grok: "grok-4.20",
253
+ "grok-latest": "grok-4.3",
254
+ "grok-code-latest": "grok-code",
255
+ opus: "opus-5",
256
+ "opus-latest": "opus-5",
257
+ sonnet: "sonnet-4.6",
258
+ "sonnet-latest": "sonnet-5",
259
+ haiku: "haiku-4.5",
260
+ "haiku-latest": "haiku-4.5",
261
+ fable: "fable-5",
262
+ gpt: "gpt-5.6",
263
+ "gpt-latest": "gpt-5.6",
264
+ "gpt-5-6-sol": "gpt-5.6",
265
+ "gpt-5-5": "gpt-5.5",
266
+ "gpt-mini-latest": "gpt-mini",
267
+ codex: "codex-5.3",
268
+ "codex-latest": "codex-5.3",
269
+ gemini: "gemini-3.1-pro",
270
+ "gemini-latest": "gemini-3.1-pro",
271
+ "gemini-pro": "gemini-3.1-pro",
272
+ "gemini-pro-latest": "gemini-3.1-pro",
273
+ "gemini-flash": "gemini-3.6-flash",
274
+ "gemini-flash-latest": "gemini-3.6-flash",
275
+ composer: "composer-2.5",
276
+ "composer-latest": "composer-2.5",
277
+ "composer-2-5": "composer-2.5",
278
+ kimi: "kimi-k2.7",
279
+ "kimi-latest": "kimi-k2.7",
280
+ };
281
+
282
+ /** Env var holding a JSON pricing override merged over the defaults. */
283
+ export const MODEL_PRICING_ENV_VAR = "AGENT_SERVE_MODEL_PRICING";
284
+
285
+ let cachedEnvOverride:
286
+ | { raw: string | undefined; table: Record<string, ModelPricing> }
287
+ | undefined;
288
+
289
+ function envOverrideTable(): Record<string, ModelPricing> {
290
+ const raw = process.env[MODEL_PRICING_ENV_VAR];
291
+ if (cachedEnvOverride !== undefined && cachedEnvOverride.raw === raw) {
292
+ return cachedEnvOverride.table;
293
+ }
294
+ let table: Record<string, ModelPricing> = {};
295
+ if (raw !== undefined && raw.trim() !== "") {
296
+ try {
297
+ const parsed = JSON.parse(raw) as unknown;
298
+ if (typeof parsed === "object" && parsed !== null) {
299
+ for (const [key, value] of Object.entries(parsed)) {
300
+ const tier = parsePricing(value);
301
+ if (tier !== undefined) {
302
+ table[normalizeModelId(key)] = tier;
303
+ }
304
+ }
305
+ }
306
+ } catch {
307
+ table = {};
308
+ }
309
+ }
310
+ cachedEnvOverride = { raw, table };
311
+ return table;
312
+ }
313
+
314
+ function parseTier(value: unknown): ModelPricingTier | undefined {
315
+ if (typeof value !== "object" || value === null) {
316
+ return undefined;
317
+ }
318
+ const row = value as Record<string, unknown>;
319
+ const input = row.inputUsdPerMTok;
320
+ const output = row.outputUsdPerMTok;
321
+ const cacheRead = row.cacheReadUsdPerMTok;
322
+ if (
323
+ !isRate(input) ||
324
+ !isRate(output) ||
325
+ (cacheRead !== undefined && !isRate(cacheRead))
326
+ ) {
327
+ return undefined;
328
+ }
329
+ const cacheWrite = row.cacheWriteUsdPerMTok;
330
+ return {
331
+ inputUsdPerMTok: input,
332
+ outputUsdPerMTok: output,
333
+ cacheReadUsdPerMTok: isRate(cacheRead) ? cacheRead : input,
334
+ cacheWriteUsdPerMTok: isRate(cacheWrite) ? cacheWrite : undefined,
335
+ };
336
+ }
337
+
338
+ function parsePricing(value: unknown): ModelPricing | undefined {
339
+ const base = parseTier(value);
340
+ if (base === undefined) {
341
+ return undefined;
342
+ }
343
+ const fast = parseTier((value as Record<string, unknown>).fast);
344
+ return { ...base, fast };
345
+ }
346
+
347
+ function isRate(value: unknown): value is number {
348
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
349
+ }
350
+
351
+ export function normalizeModelId(id: string): string {
352
+ return id.trim().toLowerCase();
353
+ }
354
+
355
+ function modelId(model: ModelSetting): string {
356
+ return typeof model === "string" ? model : model.id;
357
+ }
358
+
359
+ function modelHasFastParam(model: ModelSetting): boolean {
360
+ if (typeof model === "string") {
361
+ return false;
362
+ }
363
+ return (
364
+ model.params?.some(
365
+ (param) => param.id === "fast" && param.value === "true"
366
+ ) === true
367
+ );
368
+ }
369
+
370
+ /**
371
+ * Rates for a model selection, honoring its `fast` parameter. `undefined`
372
+ * when the model is not in the table (no estimate is better than a wrong
373
+ * one).
374
+ */
375
+ export function resolveModelPricing(
376
+ model: ModelSetting
377
+ ): ModelPricingTier | undefined {
378
+ const overrides = envOverrideTable();
379
+ const rawId = normalizeModelId(modelId(model));
380
+ const canonicalId = MODEL_ID_ALIASES[rawId] ?? rawId;
381
+ const pricing =
382
+ overrides[rawId] ??
383
+ overrides[canonicalId] ??
384
+ DEFAULT_MODEL_PRICING[rawId] ??
385
+ DEFAULT_MODEL_PRICING[canonicalId];
386
+ if (pricing === undefined) {
387
+ return undefined;
388
+ }
389
+ if (modelHasFastParam(model) && pricing.fast !== undefined) {
390
+ return pricing.fast;
391
+ }
392
+ return pricing;
393
+ }
394
+
395
+ /**
396
+ * Estimate the dollar cost of one turn's token usage. `undefined` when the
397
+ * model has no known rates.
398
+ */
399
+ export function estimateTurnCost(
400
+ model: ModelSetting,
401
+ usage: TurnUsage
402
+ ): TurnCost | undefined {
403
+ const tier = resolveModelPricing(model);
404
+ if (tier === undefined) {
405
+ return undefined;
406
+ }
407
+ const cacheWriteRate = tier.cacheWriteUsdPerMTok ?? tier.inputUsdPerMTok;
408
+ const inputUsd = (usage.inputTokens / M) * tier.inputUsdPerMTok;
409
+ const outputUsd = (usage.outputTokens / M) * tier.outputUsdPerMTok;
410
+ const cacheReadUsd = (usage.cacheReadTokens / M) * tier.cacheReadUsdPerMTok;
411
+ const cacheWriteUsd = (usage.cacheWriteTokens / M) * cacheWriteRate;
412
+ return {
413
+ totalUsd: roundUsd(inputUsd + outputUsd + cacheReadUsd + cacheWriteUsd),
414
+ inputUsd: roundUsd(inputUsd),
415
+ outputUsd: roundUsd(outputUsd),
416
+ cacheReadUsd: roundUsd(cacheReadUsd),
417
+ cacheWriteUsd: roundUsd(cacheWriteUsd),
418
+ model: normalizeModelId(modelId(model)),
419
+ source: "estimate",
420
+ };
421
+ }
422
+
423
+ /** Keep persisted estimates readable: micro-dollar precision. */
424
+ function roundUsd(usd: number): number {
425
+ return Math.round(usd * 1e6) / 1e6;
426
+ }
@@ -431,9 +431,17 @@ export class CursorSdkRunner implements AgentRunner {
431
431
  if (request.isFirstTurn) {
432
432
  return Agent.create(options);
433
433
  }
434
- throw new Error(
435
- `Cloud session ${request.sessionId} is missing sdkAgentId; cannot resume.`
434
+ // A cloud session past its first turn should carry the bound agent id
435
+ // (the engine persists it via onSdkAgentId as soon as create() reports
436
+ // one). Reaching here means the binding turn failed before the server
437
+ // ever minted an agent — e.g. create() rejected its PR attach — so the
438
+ // session would otherwise fail every future turn. Mirror the local
439
+ // host-replacement path above: a fresh conversation beats failing this
440
+ // thread forever, and the new id binds to the session for later resumes.
441
+ this.log(
442
+ `[agentkit] cloud session ${request.sessionId} has no bound cloud agent; creating a fresh one`
436
443
  );
444
+ return Agent.create(options);
437
445
  }
438
446
 
439
447
  private getStore(stateDir: string): Promise<LocalAgentStore> {
@@ -1747,11 +1747,15 @@ async function startCursorSlackRelay(input: {
1747
1747
 
1748
1748
  // Optional fields may be undefined — the relay JSON-serializes the
1749
1749
  // registration body, which drops them.
1750
- const agents: CursorSlackAgentRegistration[] = targets.map((target) => ({
1751
- name: target.agentName,
1752
- iconUrl: target.accountRuntime.options.agentIcon?.url,
1753
- iconEmoji: target.accountRuntime.options.agentIcon?.emoji,
1754
- }));
1750
+ const agents: CursorSlackAgentRegistration[] = targets.map((target) => {
1751
+ const policy = target.accountRuntime.options.policy;
1752
+ return {
1753
+ name: target.agentName,
1754
+ iconUrl: target.accountRuntime.options.agentIcon?.url,
1755
+ iconEmoji: target.accountRuntime.options.agentIcon?.emoji,
1756
+ blockExternals: policy?.blockExternals === false ? false : undefined,
1757
+ };
1758
+ });
1755
1759
 
1756
1760
  const targetByName = new Map(
1757
1761
  targets.map((target) => [target.agentName.toLowerCase(), target])
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Per-session cost metrics folded from a durable session event stream.
3
+ *
4
+ * Powers `agent-serve cost [sessionId]`: each `turn.completed` event carries
5
+ * the turn's token usage and the estimated dollar cost recorded by the
6
+ * engine (see `model-pricing.ts`), so a session's spend is a pure fold over
7
+ * its `events.ndjson` — no server-side aggregation required.
8
+ */
9
+
10
+ import type { SessionEvent, TurnCost, TurnUsage } from "../types.js";
11
+ import { formatCostUsd, roundCostUsd } from "./trajectory.js";
12
+
13
+ /** One agent loop pass (turn) with its usage and estimated cost. */
14
+ export interface SessionCostTurn {
15
+ turnId: string;
16
+ failed: boolean;
17
+ usage?: TurnUsage;
18
+ cost?: TurnCost;
19
+ }
20
+
21
+ export interface SessionCostTotals {
22
+ turns: number;
23
+ turnFailures: number;
24
+ inputTokens: number;
25
+ outputTokens: number;
26
+ cacheReadTokens: number;
27
+ cacheWriteTokens: number;
28
+ /** Sum of per-turn estimated costs (USD). */
29
+ costUsd: number;
30
+ /** Turns that reported usage but had no known model rates. */
31
+ unpricedTurns: number;
32
+ }
33
+
34
+ export interface SessionCostReport {
35
+ sessionId: string;
36
+ turns: SessionCostTurn[];
37
+ totals: SessionCostTotals;
38
+ }
39
+
40
+ export function emptySessionCostTotals(): SessionCostTotals {
41
+ return {
42
+ turns: 0,
43
+ turnFailures: 0,
44
+ inputTokens: 0,
45
+ outputTokens: 0,
46
+ cacheReadTokens: 0,
47
+ cacheWriteTokens: 0,
48
+ costUsd: 0,
49
+ unpricedTurns: 0,
50
+ };
51
+ }
52
+
53
+ export function addSessionCostTotals(
54
+ target: SessionCostTotals,
55
+ delta: SessionCostTotals
56
+ ): void {
57
+ target.turns += delta.turns;
58
+ target.turnFailures += delta.turnFailures;
59
+ target.inputTokens += delta.inputTokens;
60
+ target.outputTokens += delta.outputTokens;
61
+ target.cacheReadTokens += delta.cacheReadTokens;
62
+ target.cacheWriteTokens += delta.cacheWriteTokens;
63
+ target.costUsd = roundCostUsd(target.costUsd + delta.costUsd);
64
+ target.unpricedTurns += delta.unpricedTurns;
65
+ }
66
+
67
+ /** Fold a session event stream into per-turn and total cost metrics. */
68
+ export function buildSessionCostReport(
69
+ events: readonly SessionEvent[]
70
+ ): SessionCostReport {
71
+ let sessionId = "";
72
+ const turns: SessionCostTurn[] = [];
73
+ const totals = emptySessionCostTotals();
74
+
75
+ for (const event of events) {
76
+ if (sessionId === "" && typeof event.sessionId === "string") {
77
+ sessionId = event.sessionId;
78
+ }
79
+ if (event.type !== "turn.completed" && event.type !== "turn.failed") {
80
+ continue;
81
+ }
82
+ const turn: SessionCostTurn = {
83
+ turnId: event.turnId ?? `turn_${turns.length + 1}`,
84
+ failed: event.type === "turn.failed",
85
+ };
86
+ totals.turns += 1;
87
+ if (event.type === "turn.failed") {
88
+ totals.turnFailures += 1;
89
+ turns.push(turn);
90
+ continue;
91
+ }
92
+ const usage = event.data.usage;
93
+ if (usage !== undefined) {
94
+ turn.usage = usage;
95
+ totals.inputTokens += usage.inputTokens;
96
+ totals.outputTokens += usage.outputTokens;
97
+ totals.cacheReadTokens += usage.cacheReadTokens;
98
+ totals.cacheWriteTokens += usage.cacheWriteTokens;
99
+ }
100
+ const cost = event.data.cost;
101
+ if (cost !== undefined) {
102
+ turn.cost = cost;
103
+ totals.costUsd = roundCostUsd(totals.costUsd + cost.totalUsd);
104
+ } else if (usage !== undefined) {
105
+ totals.unpricedTurns += 1;
106
+ }
107
+ turns.push(turn);
108
+ }
109
+
110
+ return { sessionId, turns, totals };
111
+ }
112
+
113
+ function formatCount(value: number): string {
114
+ return String(value);
115
+ }
116
+
117
+ /** Per-turn table for `agent-serve cost <sessionId>`. */
118
+ export function formatSessionCostText(report: SessionCostReport): string {
119
+ const lines: string[] = [];
120
+ lines.push(
121
+ `session ${report.sessionId || "(unknown)"} est. cost: ${formatCostUsd(report.totals.costUsd)}`
122
+ );
123
+ if (report.turns.length === 0) {
124
+ lines.push("No turns recorded.");
125
+ return `${lines.join("\n")}\n`;
126
+ }
127
+ const header = [
128
+ "TURN",
129
+ "IN",
130
+ "OUT",
131
+ "CACHE R",
132
+ "CACHE W",
133
+ "EST COST",
134
+ "MODEL",
135
+ "STATUS",
136
+ ];
137
+ const rows = report.turns.map((turn) => [
138
+ turn.turnId,
139
+ turn.usage === undefined ? "—" : formatCount(turn.usage.inputTokens),
140
+ turn.usage === undefined ? "—" : formatCount(turn.usage.outputTokens),
141
+ turn.usage === undefined ? "—" : formatCount(turn.usage.cacheReadTokens),
142
+ turn.usage === undefined ? "—" : formatCount(turn.usage.cacheWriteTokens),
143
+ turn.cost === undefined ? "—" : formatCostUsd(turn.cost.totalUsd),
144
+ turn.cost?.model ?? "—",
145
+ turn.failed ? "failed" : "ok",
146
+ ]);
147
+ rows.push([
148
+ "total",
149
+ formatCount(report.totals.inputTokens),
150
+ formatCount(report.totals.outputTokens),
151
+ formatCount(report.totals.cacheReadTokens),
152
+ formatCount(report.totals.cacheWriteTokens),
153
+ formatCostUsd(report.totals.costUsd),
154
+ "",
155
+ report.totals.turnFailures > 0
156
+ ? `${report.totals.turnFailures} failed`
157
+ : "",
158
+ ]);
159
+ lines.push(renderTable(header, rows));
160
+ appendUnpricedNote(lines, report.totals.unpricedTurns);
161
+ return `${lines.join("\n")}\n`;
162
+ }
163
+
164
+ export interface SessionCostRow {
165
+ sessionId: string;
166
+ channelId: string;
167
+ title?: string;
168
+ updatedAt?: string;
169
+ totals: SessionCostTotals;
170
+ }
171
+
172
+ /** All-sessions table for `agent-serve cost` (no session id). */
173
+ export function formatSessionsCostText(rows: SessionCostRow[]): string {
174
+ if (rows.length === 0) {
175
+ return "No sessions.\n";
176
+ }
177
+ const header = [
178
+ "SESSION",
179
+ "CHANNEL",
180
+ "TURNS",
181
+ "IN",
182
+ "OUT",
183
+ "EST COST",
184
+ "UPDATED",
185
+ ];
186
+ const totals = emptySessionCostTotals();
187
+ const body = rows.map((row) => {
188
+ addSessionCostTotals(totals, row.totals);
189
+ return [
190
+ row.sessionId,
191
+ row.channelId,
192
+ formatCount(row.totals.turns),
193
+ formatCount(row.totals.inputTokens),
194
+ formatCount(row.totals.outputTokens),
195
+ formatCostUsd(row.totals.costUsd),
196
+ row.updatedAt ?? "",
197
+ ];
198
+ });
199
+ body.push([
200
+ "total",
201
+ "",
202
+ formatCount(totals.turns),
203
+ formatCount(totals.inputTokens),
204
+ formatCount(totals.outputTokens),
205
+ formatCostUsd(totals.costUsd),
206
+ "",
207
+ ]);
208
+ const lines = [renderTable(header, body)];
209
+ appendUnpricedNote(lines, totals.unpricedTurns);
210
+ return `${lines.join("\n")}\n`;
211
+ }
212
+
213
+ function appendUnpricedNote(lines: string[], unpricedTurns: number): void {
214
+ if (unpricedTurns > 0) {
215
+ lines.push(
216
+ `note: ${unpricedTurns} turn(s) reported usage for a model with no known rates; est. cost excludes them.`
217
+ );
218
+ }
219
+ }
220
+
221
+ function renderTable(header: string[], rows: string[][]): string {
222
+ const widths = header.map((h, i) =>
223
+ Math.max(h.length, ...rows.map((row) => (row[i] ?? "").length))
224
+ );
225
+ const renderRow = (row: string[]): string =>
226
+ row
227
+ .map((cell, i) => cell.padEnd(widths[i] ?? 0))
228
+ .join(" ")
229
+ .trimEnd();
230
+ return [renderRow(header), ...rows.map(renderRow)].join("\n");
231
+ }