@agent-native/core 0.43.0 → 0.44.1

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 (124) hide show
  1. package/dist/chat-threads/store.d.ts.map +1 -1
  2. package/dist/chat-threads/store.js +71 -10
  3. package/dist/chat-threads/store.js.map +1 -1
  4. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  5. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  6. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  7. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  8. package/dist/cli/recap.d.ts +23 -0
  9. package/dist/cli/recap.d.ts.map +1 -1
  10. package/dist/cli/recap.js +177 -13
  11. package/dist/cli/recap.js.map +1 -1
  12. package/dist/cli/skills.d.ts +3 -3
  13. package/dist/cli/skills.d.ts.map +1 -1
  14. package/dist/cli/skills.js +67 -20
  15. package/dist/cli/skills.js.map +1 -1
  16. package/dist/client/AssistantChat.d.ts.map +1 -1
  17. package/dist/client/AssistantChat.js +76 -18
  18. package/dist/client/AssistantChat.js.map +1 -1
  19. package/dist/client/blocks/index.d.ts +0 -2
  20. package/dist/client/blocks/index.d.ts.map +1 -1
  21. package/dist/client/blocks/index.js +0 -2
  22. package/dist/client/blocks/index.js.map +1 -1
  23. package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
  24. package/dist/client/blocks/library/AnnotatedCodeBlock.js +22 -9
  25. package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
  26. package/dist/client/blocks/library/ApiEndpointBlock.d.ts.map +1 -1
  27. package/dist/client/blocks/library/ApiEndpointBlock.js +113 -13
  28. package/dist/client/blocks/library/ApiEndpointBlock.js.map +1 -1
  29. package/dist/client/blocks/library/DiffBlock.d.ts.map +1 -1
  30. package/dist/client/blocks/library/DiffBlock.js +63 -35
  31. package/dist/client/blocks/library/DiffBlock.js.map +1 -1
  32. package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
  33. package/dist/client/blocks/library/FileTreeBlock.js +4 -0
  34. package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
  35. package/dist/client/blocks/library/JsonExplorerBlock.d.ts.map +1 -1
  36. package/dist/client/blocks/library/JsonExplorerBlock.js +1 -1
  37. package/dist/client/blocks/library/JsonExplorerBlock.js.map +1 -1
  38. package/dist/client/blocks/library/MermaidBlock.d.ts.map +1 -1
  39. package/dist/client/blocks/library/MermaidBlock.js +22 -3
  40. package/dist/client/blocks/library/MermaidBlock.js.map +1 -1
  41. package/dist/client/blocks/library/annotation-rail.d.ts +85 -19
  42. package/dist/client/blocks/library/annotation-rail.d.ts.map +1 -1
  43. package/dist/client/blocks/library/annotation-rail.js +149 -27
  44. package/dist/client/blocks/library/annotation-rail.js.map +1 -1
  45. package/dist/client/blocks/library/code-tabs.js +1 -1
  46. package/dist/client/blocks/library/code-tabs.js.map +1 -1
  47. package/dist/client/blocks/library/diagram.d.ts +17 -0
  48. package/dist/client/blocks/library/diagram.d.ts.map +1 -1
  49. package/dist/client/blocks/library/diagram.js +47 -2
  50. package/dist/client/blocks/library/diagram.js.map +1 -1
  51. package/dist/client/blocks/library/server-specs.d.ts.map +1 -1
  52. package/dist/client/blocks/library/server-specs.js +0 -10
  53. package/dist/client/blocks/library/server-specs.js.map +1 -1
  54. package/dist/client/blocks/library/specs.d.ts.map +1 -1
  55. package/dist/client/blocks/library/specs.js +0 -2
  56. package/dist/client/blocks/library/specs.js.map +1 -1
  57. package/dist/client/blocks/library/wireframe.config.d.ts.map +1 -1
  58. package/dist/client/blocks/library/wireframe.config.js +19 -2
  59. package/dist/client/blocks/library/wireframe.config.js.map +1 -1
  60. package/dist/client/blocks/mdx.d.ts.map +1 -1
  61. package/dist/client/blocks/mdx.js +11 -0
  62. package/dist/client/blocks/mdx.js.map +1 -1
  63. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  64. package/dist/client/composer/TiptapComposer.js +13 -8
  65. package/dist/client/composer/TiptapComposer.js.map +1 -1
  66. package/dist/client/composer/pasted-text.d.ts +25 -0
  67. package/dist/client/composer/pasted-text.d.ts.map +1 -1
  68. package/dist/client/composer/pasted-text.js +86 -4
  69. package/dist/client/composer/pasted-text.js.map +1 -1
  70. package/dist/client/rich-markdown-editor/DragHandle.d.ts.map +1 -1
  71. package/dist/client/rich-markdown-editor/DragHandle.js +35 -72
  72. package/dist/client/rich-markdown-editor/DragHandle.js.map +1 -1
  73. package/dist/client/rich-markdown-editor/RegistryBlockNode.d.ts.map +1 -1
  74. package/dist/client/rich-markdown-editor/RegistryBlockNode.js +1 -1
  75. package/dist/client/rich-markdown-editor/RegistryBlockNode.js.map +1 -1
  76. package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts +9 -1
  77. package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts.map +1 -1
  78. package/dist/client/rich-markdown-editor/SharedRichEditor.js +3 -1
  79. package/dist/client/rich-markdown-editor/SharedRichEditor.js.map +1 -1
  80. package/dist/client/rich-markdown-editor/extensions.d.ts +13 -1
  81. package/dist/client/rich-markdown-editor/extensions.d.ts.map +1 -1
  82. package/dist/client/rich-markdown-editor/extensions.js +4 -2
  83. package/dist/client/rich-markdown-editor/extensions.js.map +1 -1
  84. package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
  85. package/dist/client/rich-markdown-editor/useCollabReconcile.js +11 -1
  86. package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
  87. package/dist/db/migrations.d.ts +10 -0
  88. package/dist/db/migrations.d.ts.map +1 -1
  89. package/dist/db/migrations.js +32 -0
  90. package/dist/db/migrations.js.map +1 -1
  91. package/dist/server/og-fonts-data.d.ts +3 -0
  92. package/dist/server/og-fonts-data.d.ts.map +1 -0
  93. package/dist/server/og-fonts-data.js +9 -0
  94. package/dist/server/og-fonts-data.js.map +1 -0
  95. package/dist/server/og-fonts.d.ts +10 -0
  96. package/dist/server/og-fonts.d.ts.map +1 -0
  97. package/dist/server/og-fonts.js +58 -0
  98. package/dist/server/og-fonts.js.map +1 -0
  99. package/dist/server/poll.d.ts.map +1 -1
  100. package/dist/server/poll.js +30 -14
  101. package/dist/server/poll.js.map +1 -1
  102. package/dist/server/social-og-image.d.ts.map +1 -1
  103. package/dist/server/social-og-image.js +16 -5
  104. package/dist/server/social-og-image.js.map +1 -1
  105. package/dist/styles/blocks.css +121 -2
  106. package/dist/templates/default/.agents/skills/storing-data/SKILL.md +2 -0
  107. package/dist/templates/workspace-core/.agents/skills/performance/SKILL.md +141 -0
  108. package/dist/templates/workspace-core/.agents/skills/storing-data/SKILL.md +2 -0
  109. package/dist/usage/store.d.ts +12 -0
  110. package/dist/usage/store.d.ts.map +1 -1
  111. package/dist/usage/store.js +35 -5
  112. package/dist/usage/store.js.map +1 -1
  113. package/package.json +1 -1
  114. package/src/templates/default/.agents/skills/storing-data/SKILL.md +2 -0
  115. package/src/templates/workspace-core/.agents/skills/performance/SKILL.md +141 -0
  116. package/src/templates/workspace-core/.agents/skills/storing-data/SKILL.md +2 -0
  117. package/dist/client/blocks/library/decision.config.d.ts +0 -37
  118. package/dist/client/blocks/library/decision.config.d.ts.map +0 -1
  119. package/dist/client/blocks/library/decision.config.js +0 -32
  120. package/dist/client/blocks/library/decision.config.js.map +0 -1
  121. package/dist/client/blocks/library/decision.d.ts +0 -19
  122. package/dist/client/blocks/library/decision.d.ts.map +0 -1
  123. package/dist/client/blocks/library/decision.js +0 -119
  124. package/dist/client/blocks/library/decision.js.map +0 -1
@@ -23,6 +23,30 @@
23
23
  --an-callout-risk: 0 84% 60%;
24
24
  --an-callout-warning: 25 95% 48%;
25
25
  --an-callout-success: 142 71% 38%;
26
+
27
+ /* Standardized small code-body size for every monospace code surface in the
28
+ core block library (diffs, annotated-code, JSON explorer, Mermaid source,
29
+ etc.). The block components reference this via `[font-size:var(--plan-code-size)]`;
30
+ without a definition the declaration is invalid and code falls back to the
31
+ large prose size. The plan template overrides this var in its own
32
+ `global.css`; this is the cross-app default. */
33
+ --plan-code-size: 0.75rem;
34
+
35
+ /* AUTHORITATIVE code-body size for DENSE in-document code surfaces (diff rows,
36
+ annotated-code lines, API JSON examples, code-tabs, the `code` primitive
37
+ read surface). The catch-all below forces every such surface to this size
38
+ with `!important`, so dense document code is sized from ONE place no matter
39
+ which component renders it or what inline/utility/template size it would
40
+ otherwise pick up.
41
+
42
+ This is deliberately DECOUPLED from `--plan-code-size`: the plan template
43
+ raises `--plan-code-size` to 1rem for its free-standing prose-scale code,
44
+ which made dense surfaces (annotated diffs, API JSON) read far too large —
45
+ the user's recurring complaint. By pinning dense code to its own small,
46
+ comfortable size here, those surfaces stay compact and scannable in every
47
+ app while a free-standing snippet can still follow `--plan-code-size`.
48
+ Resize ALL dense document code at once by overriding this var. */
49
+ --plan-doc-code-size: 0.8125rem;
26
50
  }
27
51
 
28
52
  .dark {
@@ -33,6 +57,101 @@
33
57
  --an-callout-success: 142 60% 52%;
34
58
  }
35
59
 
60
+ /* ════════════════════════════════════════════════════════════════════════
61
+ AUTHORITATIVE code-surface font-size catch-all.
62
+
63
+ THE single source of truth for how big monospace code renders inside plan
64
+ document content. Historically the size was set per-surface — a Tailwind
65
+ arbitrary `[font-size:var(--plan-code-size)]` on diff/annotated-code rows
66
+ (specificity 0,1,0), the plan template's `.plan-code-surface pre` rule
67
+ (0,2,0), the editor node-view `.an-rich-md-prose .an-code-block pre` rule
68
+ (0,3,0), etc. Because those live in different files at different specificities
69
+ (and the plan template raises `--plan-code-size` to 1rem), one surface could
70
+ render large while another stayed small: the recurring "code is too big in
71
+ the annotated diff / API JSON" whack-a-mole.
72
+
73
+ This rule ends it. It scopes to plan document content via a ZERO-specificity
74
+ `:where()` wrapper, then targets EVERY code surface — shiki/lowlight `<pre>`
75
+ and `<code>`, the `.plan-shiki` wrapper, the shared `.plan-code-surface`
76
+ scroll body, the editor `.an-code-block`, and an explicit
77
+ `[data-code-surface]` hook the dense block components stamp on their code
78
+ containers — and forces `font-size: var(--plan-doc-code-size)` with
79
+ `!important`. The `!important` + the `[data-code-surface]` attribute leaf
80
+ (specificity contribution 0,1,0 inside `:is()`, but `!important` is what
81
+ decides the cascade) guarantees it WINS over every per-surface size above,
82
+ including the plan template's later-loaded `.plan-code-surface pre` and the
83
+ inline Tailwind arbitrary class — no surface can escape it. Free-standing
84
+ prose code (inline `code` in body copy) is intentionally NOT in scope: it is
85
+ matched only when it sits inside one of the code-surface containers.
86
+
87
+ Tune `--plan-doc-code-size` (here or per-app) to resize ALL dense document
88
+ code at once. ════════════════════════════════════════════════════════════ */
89
+ :where(.plan-document-flow, .plan-block, .plan-block-node)
90
+ :is(
91
+ [data-code-surface],
92
+ [data-code-surface] pre,
93
+ [data-code-surface] code,
94
+ [data-code-surface] .plan-shiki,
95
+ .plan-code-surface,
96
+ .plan-code-surface pre,
97
+ .plan-code-surface code,
98
+ .plan-code-surface .plan-shiki,
99
+ .plan-code-surface .plan-code-surface-scroll,
100
+ .an-code-block,
101
+ .an-code-block pre,
102
+ .an-code-block code
103
+ ) {
104
+ font-size: var(--plan-doc-code-size) !important;
105
+ }
106
+
107
+ /* Syntax-token spans nested INSIDE a highlighted `<pre>`/`<code>` (shiki +
108
+ lowlight wrap every token in a `<span>`) inherit the forced size, so a stray
109
+ per-token size can never reintroduce a large glyph. Scoped to spans under
110
+ pre/code only — NOT every span in the surface — so the in-code annotation
111
+ marker pips (their own tiny `text-[9px]`) and other chrome keep their size. */
112
+ :where(.plan-document-flow, .plan-block, .plan-block-node)
113
+ :is([data-code-surface], .plan-code-surface, .an-code-block)
114
+ :is(pre, code)
115
+ span {
116
+ font-size: inherit !important;
117
+ }
118
+
119
+ /* Universal guarantee (NOT gated on a plan-document ancestor): the dense code
120
+ block components — annotated-code, diff, api-endpoint — stamp
121
+ `data-code-surface` / `.an-code-block` on their code containers. Force the
122
+ dense size on those WHEREVER they render, so the standard annotated/diff
123
+ component can never show oversized code on any surface (embedded panels,
124
+ non-plan-document hosts, etc.), not just inside `.plan-document-flow`. The
125
+ `var(--plan-doc-code-size, 0.8125rem)` fallback covers hosts that don't define
126
+ the token. */
127
+ :is(
128
+ [data-code-surface],
129
+ [data-code-surface] pre,
130
+ [data-code-surface] code,
131
+ [data-code-surface] .plan-shiki,
132
+ .an-code-block,
133
+ .an-code-block pre,
134
+ .an-code-block code
135
+ ) {
136
+ font-size: var(--plan-doc-code-size, 0.8125rem) !important;
137
+ }
138
+ :is([data-code-surface], .an-code-block) :is(pre, code) span {
139
+ font-size: inherit !important;
140
+ }
141
+
142
+ /* COMPREHENSIVE NET — the final backstop so "code is too big" can't reappear in
143
+ a NEW place. Every code/data/JSON component opts into monospace via the
144
+ `.font-mono` utility (12+ block components, incl. the JSON-explorer tree which
145
+ renders as div/span — NOT pre/code — and so escaped every earlier rule). Force
146
+ the dense doc-code size on any `.font-mono` inside plan content, and on the
147
+ code-block component hooks wherever they render. Annotation marker pips are NOT
148
+ font-mono, so they keep their tiny size; inline prose `code` is styled by the
149
+ prose plugin (no `.font-mono` class), so body copy is unaffected. */
150
+ :where(.plan-document-flow, .plan-block, .plan-block-node) .font-mono,
151
+ :is([data-code-surface], .an-code-block) .font-mono {
152
+ font-size: var(--plan-doc-code-size, 0.8125rem) !important;
153
+ }
154
+
36
155
  /* Small, muted eyebrow label above a block (block title). */
37
156
  .an-block-label {
38
157
  margin: 0 0 1rem;
@@ -381,7 +500,7 @@
381
500
  .plan-code-surface-scroll {
382
501
  position: relative;
383
502
  overflow: auto;
384
- font-size: 0.78rem;
503
+ font-size: var(--plan-code-size);
385
504
  line-height: 1.6;
386
505
  }
387
506
 
@@ -427,7 +546,7 @@
427
546
  background: var(--shiki-light-bg, hsl(var(--muted))) !important;
428
547
  color: var(--shiki-light, hsl(var(--foreground)));
429
548
  font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
430
- font-size: 0.78rem;
549
+ font-size: var(--plan-code-size);
431
550
  line-height: 1.6;
432
551
  }
433
552
 
@@ -14,6 +14,8 @@ metadata:
14
14
 
15
15
  All application data lives in **SQL** (SQLite locally, persistent database in production). The agent and UI share the same database. Do not store durable app data in the filesystem.
16
16
 
17
+ When you add a data model, a list, or a read path, also follow the `performance` skill: project only the columns a list renders, index the columns hot queries filter/sort on, and avoid query waterfalls — so apps stay fast as data grows.
18
+
17
19
  ## How It Works
18
20
 
19
21
  Agent-native apps use Drizzle ORM over the configured SQL backend. Local development works out of the box with a SQLite file at `data/app.db`; production and shared preview deploys need a persistent `DATABASE_URL` because container/serverless filesystems can reset. The code should behave the same across backends, but the local SQLite file is not durable once deployed.
@@ -0,0 +1,141 @@
1
+ ---
2
+ name: performance
3
+ description: >-
4
+ Keep apps and templates loading fast. Read when adding a data model, a
5
+ list/read action, a page or sidebar that loads data, or when something loads
6
+ slowly. Covers column projection, indexing hot-path queries, avoiding N+1 and
7
+ round-trip waterfalls, cheap polling, and not recomputing on every read.
8
+ metadata:
9
+ internal: true
10
+ ---
11
+
12
+ # Performance — Keep Loads Fast
13
+
14
+ ## Rule
15
+
16
+ Treat every list, every read, and every page load as a latency budget. Two
17
+ things dominate it: **how much data crosses the wire**, and **how many
18
+ round-trips and table scans it takes**. On a hosted/serverless SQL backend each
19
+ query is a network round-trip, and an unindexed filter scans the whole — often
20
+ shared and growing — table. So default to **projected columns**, **indexed
21
+ hot-path queries**, and **parallel/batched** fetches. These rules are
22
+ provider-agnostic: they hold on SQLite, Postgres, or any managed SQL backend.
23
+
24
+ This skill is about the data and load path. See the `storing-data` skill for the schema
25
+ and migration mechanics it references, and the `real-time-sync` skill for how updates
26
+ already reach the UI without polling.
27
+
28
+ ## 1. Project columns — never `SELECT *` on a list
29
+
30
+ A list/index query should select only the columns the list actually renders.
31
+
32
+ - **Never return heavy columns in a list**: large JSON/text blobs such as
33
+ document bodies, rendered HTML, `config`/`layout`/`spec`/`data`/`tracks`,
34
+ tool results, or base64 attachments. Pulling them for every row is the single
35
+ most common cause of a slow list.
36
+ - Heavy/full columns belong on the **single-item GET/detail** path only.
37
+ - Need a preview from a big column? Select a **truncated substring at the DB**,
38
+ not the whole column — and it stays portable:
39
+
40
+ ```ts
41
+ // Drizzle — project, and truncate the heavy column for the preview
42
+ const rows = await db
43
+ .select({
44
+ id: docs.id,
45
+ title: docs.title,
46
+ updatedAt: docs.updatedAt,
47
+ // substr/length work on both SQLite and Postgres
48
+ preview: sql<string>`substr(${docs.content}, 1, 400)`,
49
+ })
50
+ .from(docs)
51
+ .where(accessFilter(docs, docShares))
52
+ .orderBy(desc(docs.updatedAt));
53
+ ```
54
+
55
+ - After narrowing the projection, update the row mapper and its return type so a
56
+ dropped column is provably unused on the list path. If the list genuinely
57
+ renders a heavy column (a thumbnail, an inline preview the UI shows), keep it —
58
+ don't break behavior to chase a payload win.
59
+
60
+ ## 2. Index the hot paths
61
+
62
+ Indexes are added through the **versioned migration array** in
63
+ `server/plugins/db.ts` as `CREATE INDEX IF NOT EXISTS …` — not through a
64
+ schema-level `index()` helper (the framework applies indexes via migrations; see
65
+ the `storing-data` skill). Add an index for any column a hot query **filters or sorts**
66
+ on. The recurring ones:
67
+
68
+ - **Ownable tables** → `(owner_email, org_id, <the list's ORDER BY column>)`.
69
+ Access scoping filters by owner/org and lists sort by `updated_at`/`created_at`.
70
+ - **Shares tables** (`{resource}_shares`) → `(resource_id, principal_type, principal_id)`.
71
+ Access checks run correlated `EXISTS` subqueries against these on every list.
72
+ - **Child / foreign-key columns** used to load children (e.g. `responses.form_id`,
73
+ `comments.parent_id`, an events log's `*_id`) → index the FK, plus its sort
74
+ column when the children are ordered. An unindexed FK means a full scan of the
75
+ child table on every parent open. **A foreign-key reference does not create an
76
+ index automatically** — add it explicitly.
77
+ - **Status-filtered lists** → match the real `WHERE`, e.g. `(owner_email, status)`
78
+ or `(status, <sort>)`.
79
+
80
+ Keep index DDL **dialect-agnostic and idempotent**:
81
+
82
+ ```sql
83
+ CREATE INDEX IF NOT EXISTS forms_owner_org_updated_idx ON forms (owner_email, org_id, updated_at)
84
+ ```
85
+
86
+ No `DESC`, no partial `WHERE`, no provider-specific syntax — it then runs on
87
+ SQLite and Postgres alike, is safe to re-run, and applies on next startup.
88
+ Indexes mostly bite **as data grows** and on **unbounded child tables** (a
89
+ seq-scan of 10 rows is instant; of a shared, ever-growing log it is not), so
90
+ index the growing tables first.
91
+
92
+ ## 3. Don't fan out queries — batch and parallelize
93
+
94
+ - **No N+1.** Never loop issuing one query per item. Load children for many
95
+ parents in one `inArray(child.parentId, ids)` query, then group in memory.
96
+ - **Count in SQL** (`count()`), never "select all rows then `.length`".
97
+ - **Parallelize independent queries** with `Promise.all` rather than sequential
98
+ `await`s — each `await` is another round-trip.
99
+ - Prefer **one composed endpoint** over several dependent calls.
100
+
101
+ ## 4. Avoid client-side waterfalls
102
+
103
+ - Don't gate query B on query A's result unless B truly needs it. Fire
104
+ independent `useActionQuery` / `useQuery` hooks **in parallel**; never make the
105
+ loading skeleton wait on a serial chain.
106
+ - Load the visible page from one read where possible, and **lazy-load**
107
+ secondary / below-the-fold data after first paint.
108
+
109
+ ## 5. Poll cheaply; compute once
110
+
111
+ - Updates already reach the UI through the `real-time-sync` skill (`useDbSync` / SSE).
112
+ Don't add an aggressive `refetchInterval` that re-runs a heavy list/read every
113
+ couple of seconds. If you must poll, use a **wide interval** and a **cheap**
114
+ endpoint.
115
+ - **Never do expensive per-request work on a read that runs on every load/poll**:
116
+ re-rendering HTML/markdown, pretty-printing, re-parsing / migrating /
117
+ normalizing / sanitizing stored JSON. Do that work at **write time** (store the
118
+ result) or compute it **lazily only for the caller that needs it**. Reads on
119
+ the hot path must be cheap.
120
+ - Data the UI doesn't display (export formats, alternate renderings) belongs in a
121
+ separate on-demand action, not baked into the hot read.
122
+
123
+ ## 6. Big payloads and long lists
124
+
125
+ - **Paginate or window** unbounded lists (messages, responses, events, activity).
126
+ Don't load the entire history on open; load a recent window and fetch older on
127
+ demand.
128
+ - Don't store **unbounded blobs inline** in a row that a list/load pulls.
129
+ Reference large content separately so opening the parent stays cheap.
130
+ - **Virtualize** very long rendered lists on the client so off-screen rows aren't
131
+ parsed/rendered every update.
132
+
133
+ ## Checklist — run before shipping a list/read or a new table
134
+
135
+ - [ ] List selects only displayed columns; heavy blobs excluded or `substr`-truncated.
136
+ - [ ] Every hot-path `WHERE` / `ORDER BY` column is indexed (owner/org/sort,
137
+ shares `resource_id`, child FKs, status filters) via a `db.ts` migration.
138
+ - [ ] No N+1; independent queries parallelized; counts via SQL `count()`.
139
+ - [ ] Client fires independent queries in parallel, not a waterfall.
140
+ - [ ] No heavy recompute on every read; no aggressive polling of heavy endpoints.
141
+ - [ ] Unbounded lists are paginated/windowed; large blobs aren't inlined on the hot path.
@@ -14,6 +14,8 @@ metadata:
14
14
 
15
15
  All application data lives in **SQL** (SQLite locally, persistent database in production). The agent and UI share the same database. Do not store durable app data in the filesystem.
16
16
 
17
+ When you add a data model, a list, or a read path, also follow the `performance` skill: project only the columns a list renders, index the columns hot queries filter/sort on, and avoid query waterfalls — so apps stay fast as data grows.
18
+
17
19
  ## How It Works
18
20
 
19
21
  Agent-native apps use Drizzle ORM over the configured SQL backend. Local development works out of the box with a SQLite file at `data/app.db`; production and shared preview deploys need a persistent `DATABASE_URL` because container/serverless filesystems can reset. The code should behave the same across backends, but the local SQLite file is not durable once deployed.
@@ -24,6 +24,18 @@ export interface UsageRecord {
24
24
  label?: string;
25
25
  /** Optional template/app name (e.g. "mail"). Falls back to AGENT_APP / APP_NAME env. */
26
26
  app?: string;
27
+ /**
28
+ * Stable id of the thing this usage belongs to (e.g. a recap plan id). When
29
+ * set, any prior row(s) with the same (label, refId) are deleted before
30
+ * insert, so re-recording the same run overwrites instead of double-counting.
31
+ */
32
+ refId?: string;
33
+ /**
34
+ * Precomputed cost in centicents (1/100¢). When provided, it is stored
35
+ * verbatim instead of being derived from tokens — e.g. to mirror a
36
+ * provider-reported dollar cost so two surfaces agree exactly.
37
+ */
38
+ costCentsX100?: number;
27
39
  }
28
40
  /**
29
41
  * Calculate cost in centicents (1/100th of a cent).
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/usage/store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/usage/store.ts"],"names":[],"mappings":"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"}
@@ -47,6 +47,20 @@ const PRICING = [
47
47
  match: /haiku/i,
48
48
  pricing: { input: 100, output: 500, cacheRead: 10, cacheWrite: 125 },
49
49
  },
50
+ // OpenAI / Codex models (cents per 1M tokens). Without these, a Codex recap
51
+ // model like "gpt-5.5" falls through to the default (Sonnet) row and is
52
+ // mispriced. Published rates as of 2026-06; OpenAI bills cached input at a
53
+ // discount and has no separate cache-write token charge, so cacheWrite is 0
54
+ // (recap usage passes 0 cache-write anyway). The /gpt-5\.5/ row must precede
55
+ // /gpt-5/ since the latter also matches "gpt-5.5".
56
+ {
57
+ match: /gpt-5\.5/i,
58
+ pricing: { input: 500, output: 3000, cacheRead: 50, cacheWrite: 0 },
59
+ },
60
+ {
61
+ match: /gpt-5/i,
62
+ pricing: { input: 125, output: 1000, cacheRead: 12.5, cacheWrite: 0 },
63
+ },
50
64
  // default → sonnet pricing
51
65
  {
52
66
  match: /.*/,
@@ -77,6 +91,7 @@ async function ensureUsageTable() {
77
91
  model TEXT NOT NULL DEFAULT '',
78
92
  label TEXT NOT NULL DEFAULT 'chat',
79
93
  app TEXT NOT NULL DEFAULT '',
94
+ ref_id TEXT NOT NULL DEFAULT '',
80
95
  created_at ${intType()} NOT NULL
81
96
  )
82
97
  `);
@@ -88,6 +103,7 @@ async function ensureUsageTable() {
88
103
  ["cache_write_tokens", `${intType()} NOT NULL DEFAULT 0`],
89
104
  ["label", `TEXT NOT NULL DEFAULT 'chat'`],
90
105
  ["app", `TEXT NOT NULL DEFAULT ''`],
106
+ ["ref_id", `TEXT NOT NULL DEFAULT ''`],
91
107
  ];
92
108
  for (const [col, def] of additions) {
93
109
  try {
@@ -136,20 +152,33 @@ export async function recordUsage(recordOrOwner, inputTokens, outputTokens, mode
136
152
  model: model ?? "",
137
153
  }
138
154
  : recordOrOwner;
139
- const { ownerEmail, inputTokens: inTok, outputTokens: outTok, cacheReadTokens = 0, cacheWriteTokens = 0, model: modelName, label, app, } = record;
155
+ const { ownerEmail, inputTokens: inTok, outputTokens: outTok, cacheReadTokens = 0, cacheWriteTokens = 0, model: modelName, label, app, refId, costCentsX100, } = record;
140
156
  // Skip no-op writes (e.g. a stream aborted before any tokens flowed)
141
157
  if (!inTok && !outTok && !cacheReadTokens && !cacheWriteTokens)
142
158
  return;
143
159
  await ensureUsageTable();
144
160
  const client = getDbExec();
145
- const costX100 = calculateCost(inTok, outTok, modelName, cacheReadTokens, cacheWriteTokens);
146
- const id = Date.now() * 1000 + Math.floor(Math.random() * 1000);
147
161
  const resolvedApp = app ?? process.env.AGENT_APP ?? process.env.APP_NAME ?? "";
148
162
  const resolvedLabel = label ?? "chat";
163
+ const resolvedRef = refId ?? "";
164
+ // Replace any prior usage for this (label, refId) so re-recording the same
165
+ // run — e.g. a recap regenerated on a PR re-push — overwrites instead of
166
+ // double-counting. No-op when refId is unset (the common per-call path).
167
+ if (resolvedRef) {
168
+ await client.execute({
169
+ sql: `DELETE FROM token_usage WHERE label = ? AND ref_id = ?`,
170
+ args: [resolvedLabel, resolvedRef],
171
+ });
172
+ }
173
+ // Prefer an explicit precomputed cost (e.g. a provider-reported dollar cost);
174
+ // otherwise derive it from tokens via the pricing table.
175
+ const costX100 = costCentsX100 ??
176
+ calculateCost(inTok, outTok, modelName, cacheReadTokens, cacheWriteTokens);
177
+ const id = Date.now() * 1000 + Math.floor(Math.random() * 1000);
149
178
  await client.execute({
150
179
  sql: `INSERT INTO token_usage
151
- (id, owner_email, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, cost_cents_x100, model, label, app, created_at)
152
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
180
+ (id, owner_email, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, cost_cents_x100, model, label, app, ref_id, created_at)
181
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
153
182
  args: [
154
183
  id,
155
184
  ownerEmail,
@@ -161,6 +190,7 @@ export async function recordUsage(recordOrOwner, inputTokens, outputTokens, mode
161
190
  modelName,
162
191
  resolvedLabel,
163
192
  resolvedApp,
193
+ resolvedRef,
164
194
  Date.now(),
165
195
  ],
166
196
  });
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/usage/store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Token usage tracking and cost monitoring.\n *\n * Every LLM call made by the framework records a row here so users can\n * see where their spend is going — chat vs automations vs background jobs\n * vs whatever else a template labels its prompts as.\n *\n * Cost is stored as \"centicents\" (1/100th of a cent) for integer precision.\n */\nimport { getDbExec, intType, isPostgres } from \"../db/client.js\";\n\n/**\n * Per-million-token pricing in cents. Cache read is typically ~10% of\n * input; cache write (5m TTL) is ~125%. Pricing is best-effort — keep\n * this table in sync with Anthropic's published prices.\n */\ninterface ModelPricing {\n input: number;\n output: number;\n cacheRead: number;\n cacheWrite: number;\n}\n\nexport const BUILDER_AGENT_CREDIT_MARGIN_MULTIPLIER = 1.25;\nexport const BUILDER_AGENT_CREDITS_PER_USD = 20;\n\nexport type UsageBillingUnit = \"usd\" | \"builder-credits\";\n\nexport interface UsageBillingMode {\n unit: UsageBillingUnit;\n label: string;\n shortLabel: string;\n source: \"estimated-provider-cost\" | \"builder-agent-credits\";\n hardCostMarginMultiplier?: number;\n creditsPerUsd?: number;\n}\n\nexport const USD_USAGE_BILLING: UsageBillingMode = {\n unit: \"usd\",\n label: \"Estimated spend\",\n shortLabel: \"Cost\",\n source: \"estimated-provider-cost\",\n};\n\nexport const BUILDER_CREDIT_USAGE_BILLING: UsageBillingMode = {\n unit: \"builder-credits\",\n label: \"Builder.io credit spend\",\n shortLabel: \"Credits\",\n source: \"builder-agent-credits\",\n hardCostMarginMultiplier: BUILDER_AGENT_CREDIT_MARGIN_MULTIPLIER,\n creditsPerUsd: BUILDER_AGENT_CREDITS_PER_USD,\n};\n\nexport function usageBillingForEngine(\n engineName: string | null | undefined,\n): UsageBillingMode {\n return engineName === \"builder\"\n ? BUILDER_CREDIT_USAGE_BILLING\n : USD_USAGE_BILLING;\n}\n\nexport function builderCreditsFromCostCents(cents: number): number {\n if (!Number.isFinite(cents) || cents <= 0) return 0;\n const dollars = cents / 100;\n const credits =\n dollars *\n BUILDER_AGENT_CREDIT_MARGIN_MULTIPLIER *\n BUILDER_AGENT_CREDITS_PER_USD;\n return Math.ceil(credits * 1000) / 1000;\n}\n\nconst PRICING: Array<{ match: RegExp; pricing: ModelPricing }> = [\n {\n match: /opus/i,\n pricing: { input: 1500, output: 7500, cacheRead: 150, cacheWrite: 1875 },\n },\n {\n match: /haiku/i,\n pricing: { input: 100, output: 500, cacheRead: 10, cacheWrite: 125 },\n },\n // default → sonnet pricing\n {\n match: /.*/,\n pricing: { input: 300, output: 1500, cacheRead: 30, cacheWrite: 375 },\n },\n];\n\nfunction pricingFor(model: string): ModelPricing {\n for (const entry of PRICING) {\n if (entry.match.test(model)) return entry.pricing;\n }\n return PRICING[PRICING.length - 1]!.pricing;\n}\n\nexport interface UsageRecord {\n ownerEmail: string;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n model: string;\n /** Category for this call — e.g. \"chat\", \"automation\", \"job\", \"custom-agent\". */\n label?: string;\n /** Optional template/app name (e.g. \"mail\"). Falls back to AGENT_APP / APP_NAME env. */\n app?: string;\n}\n\nlet _initPromise: Promise<void> | undefined;\n\nasync function ensureUsageTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n await client.execute(`\n CREATE TABLE IF NOT EXISTS token_usage (\n id ${intType()} PRIMARY KEY,\n owner_email TEXT NOT NULL,\n input_tokens ${intType()} NOT NULL DEFAULT 0,\n output_tokens ${intType()} NOT NULL DEFAULT 0,\n cache_read_tokens ${intType()} NOT NULL DEFAULT 0,\n cache_write_tokens ${intType()} NOT NULL DEFAULT 0,\n cost_cents_x100 ${intType()} NOT NULL DEFAULT 0,\n model TEXT NOT NULL DEFAULT '',\n label TEXT NOT NULL DEFAULT 'chat',\n app TEXT NOT NULL DEFAULT '',\n created_at ${intType()} NOT NULL\n )\n `);\n\n // Add columns on older deployments that pre-date the label/cache\n // fields. Each ALTER is wrapped so a dialect without IF NOT EXISTS\n // (SQLite) still makes progress if only some columns are missing.\n const additions: Array<[string, string]> = [\n [\"cache_read_tokens\", `${intType()} NOT NULL DEFAULT 0`],\n [\"cache_write_tokens\", `${intType()} NOT NULL DEFAULT 0`],\n [\"label\", `TEXT NOT NULL DEFAULT 'chat'`],\n [\"app\", `TEXT NOT NULL DEFAULT ''`],\n ];\n for (const [col, def] of additions) {\n try {\n if (isPostgres()) {\n await client.execute(\n `ALTER TABLE token_usage ADD COLUMN IF NOT EXISTS ${col} ${def}`,\n );\n } else {\n await client.execute(\n `ALTER TABLE token_usage ADD COLUMN ${col} ${def}`,\n );\n }\n } catch {\n // Column already exists — ignore\n }\n }\n\n try {\n await client.execute(\n `CREATE INDEX IF NOT EXISTS idx_token_usage_owner_created ON token_usage (owner_email, created_at)`,\n );\n } catch {}\n })().catch((err) => {\n // Retry init on the next call after a failed startup.\n _initPromise = undefined;\n throw err;\n });\n }\n return _initPromise;\n}\n\n/**\n * Calculate cost in centicents (1/100th of a cent).\n * Accepts cache tokens so callers that use prompt caching are priced\n * correctly. Non-cache-aware callers can pass 0 for the cache fields.\n */\nexport function calculateCost(\n inputTokens: number,\n outputTokens: number,\n model: string,\n cacheReadTokens = 0,\n cacheWriteTokens = 0,\n): number {\n const p = pricingFor(model);\n const rawCenticents =\n (inputTokens / 1_000_000) * p.input * 100 +\n (outputTokens / 1_000_000) * p.output * 100 +\n (cacheReadTokens / 1_000_000) * p.cacheRead * 100 +\n (cacheWriteTokens / 1_000_000) * p.cacheWrite * 100;\n return rawCenticents > 0 ? Math.max(1, Math.round(rawCenticents)) : 0;\n}\n\n/**\n * Record token usage from an LLM call.\n *\n * Accepts an object with the full set of fields. A positional overload\n * remains for backward compatibility with the older 4-arg signature.\n */\nexport async function recordUsage(record: UsageRecord): Promise<void>;\nexport async function recordUsage(\n ownerEmail: string,\n inputTokens: number,\n outputTokens: number,\n model: string,\n): Promise<void>;\nexport async function recordUsage(\n recordOrOwner: UsageRecord | string,\n inputTokens?: number,\n outputTokens?: number,\n model?: string,\n): Promise<void> {\n const record: UsageRecord =\n typeof recordOrOwner === \"string\"\n ? {\n ownerEmail: recordOrOwner,\n inputTokens: inputTokens ?? 0,\n outputTokens: outputTokens ?? 0,\n model: model ?? \"\",\n }\n : recordOrOwner;\n\n const {\n ownerEmail,\n inputTokens: inTok,\n outputTokens: outTok,\n cacheReadTokens = 0,\n cacheWriteTokens = 0,\n model: modelName,\n label,\n app,\n } = record;\n\n // Skip no-op writes (e.g. a stream aborted before any tokens flowed)\n if (!inTok && !outTok && !cacheReadTokens && !cacheWriteTokens) return;\n\n await ensureUsageTable();\n const client = getDbExec();\n const costX100 = calculateCost(\n inTok,\n outTok,\n modelName,\n cacheReadTokens,\n cacheWriteTokens,\n );\n const id = Date.now() * 1000 + Math.floor(Math.random() * 1000);\n const resolvedApp =\n app ?? process.env.AGENT_APP ?? process.env.APP_NAME ?? \"\";\n const resolvedLabel = label ?? \"chat\";\n await client.execute({\n sql: `INSERT INTO token_usage\n (id, owner_email, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, cost_cents_x100, model, label, app, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n args: [\n id,\n ownerEmail,\n inTok,\n outTok,\n cacheReadTokens,\n cacheWriteTokens,\n costX100,\n modelName,\n resolvedLabel,\n resolvedApp,\n Date.now(),\n ],\n });\n}\n\n/** Total cost (in cents) charged against a user, across all time. */\nexport async function getUserUsageCents(ownerEmail: string): Promise<number> {\n await ensureUsageTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT COALESCE(SUM(cost_cents_x100), 0) as total FROM token_usage WHERE owner_email = ?`,\n args: [ownerEmail],\n });\n const total = Number((rows[0] as { total?: number })?.total ?? 0);\n return total / 100;\n}\n\n// ─── Admin / UI queries ─────────────────────────────────────────────────\n\nexport interface UsageSummaryOptions {\n ownerEmail: string;\n /** Inclusive lower bound (ms since epoch). Defaults to 30 days ago. */\n sinceMs?: number;\n}\n\nexport interface UsageBucket {\n key: string;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n cents: number;\n calls: number;\n}\n\nexport interface DailyBucket {\n /** YYYY-MM-DD (UTC) */\n date: string;\n cents: number;\n calls: number;\n}\n\nexport interface UsageRecentEntry {\n id: number;\n createdAt: number;\n label: string;\n app: string;\n model: string;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n cents: number;\n}\n\nexport interface UsageSummary {\n billing?: UsageBillingMode;\n totalCents: number;\n totalCalls: number;\n totalInputTokens: number;\n totalOutputTokens: number;\n totalCacheReadTokens: number;\n totalCacheWriteTokens: number;\n sinceMs: number;\n byLabel: UsageBucket[];\n byModel: UsageBucket[];\n byApp: UsageBucket[];\n byDay: DailyBucket[];\n recent: UsageRecentEntry[];\n}\n\nconst DAY_MS = 86_400_000;\n\n/**\n * Produce an aggregated spend view for the Usage admin panel.\n * Scoped to the passed owner email; the UI always passes the session user.\n */\nexport async function getUsageSummary(\n options: UsageSummaryOptions,\n): Promise<UsageSummary> {\n await ensureUsageTable();\n const client = getDbExec();\n const sinceMs = options.sinceMs ?? Date.now() - 30 * DAY_MS;\n\n const totalRow = await client.execute({\n sql: `SELECT\n COALESCE(SUM(cost_cents_x100), 0) AS cents,\n COUNT(*) AS calls,\n COALESCE(SUM(input_tokens), 0) AS in_tok,\n COALESCE(SUM(output_tokens), 0) AS out_tok,\n COALESCE(SUM(cache_read_tokens), 0) AS cr_tok,\n COALESCE(SUM(cache_write_tokens), 0) AS cw_tok\n FROM token_usage WHERE owner_email = ? AND created_at >= ?`,\n args: [options.ownerEmail, sinceMs],\n });\n const t = (totalRow.rows[0] ?? {}) as Record<string, number | null>;\n\n const bucketSql = (col: string) => ({\n sql: `SELECT ${col} AS k,\n COALESCE(SUM(cost_cents_x100), 0) AS cents,\n COUNT(*) AS calls,\n COALESCE(SUM(input_tokens), 0) AS in_tok,\n COALESCE(SUM(output_tokens), 0) AS out_tok,\n COALESCE(SUM(cache_read_tokens), 0) AS cr_tok,\n COALESCE(SUM(cache_write_tokens), 0) AS cw_tok\n FROM token_usage\n WHERE owner_email = ? AND created_at >= ?\n GROUP BY ${col}\n ORDER BY cents DESC`,\n args: [options.ownerEmail, sinceMs],\n });\n\n const mapBuckets = (rows: unknown[]): UsageBucket[] =>\n rows.map((r) => {\n const row = r as Record<string, number | string | null>;\n return {\n key: String(row.k ?? \"\"),\n cents: Number(row.cents ?? 0) / 100,\n calls: Number(row.calls ?? 0),\n inputTokens: Number(row.in_tok ?? 0),\n outputTokens: Number(row.out_tok ?? 0),\n cacheReadTokens: Number(row.cr_tok ?? 0),\n cacheWriteTokens: Number(row.cw_tok ?? 0),\n };\n });\n\n const [byLabelR, byModelR, byAppR] = await Promise.all([\n client.execute(bucketSql(\"label\")),\n client.execute(bucketSql(\"model\")),\n client.execute(bucketSql(\"app\")),\n ]);\n\n // By-day aggregation — done in JS so we don't depend on dialect-specific\n // date functions (SQLite `strftime`, Postgres `to_char`). Cheap enough\n // for a 30-day window; if this grows, swap for a dialect-aware query.\n const dayRows = await client.execute({\n sql: `SELECT created_at, cost_cents_x100 FROM token_usage\n WHERE owner_email = ? AND created_at >= ?`,\n args: [options.ownerEmail, sinceMs],\n });\n const dayMap = new Map<string, { cents: number; calls: number }>();\n for (const row of dayRows.rows as Array<Record<string, number>>) {\n const date = new Date(Number(row.created_at)).toISOString().slice(0, 10);\n const prev = dayMap.get(date) ?? { cents: 0, calls: 0 };\n prev.cents += Number(row.cost_cents_x100 ?? 0);\n prev.calls += 1;\n dayMap.set(date, prev);\n }\n const byDay: DailyBucket[] = [...dayMap.entries()]\n .map(([date, v]) => ({\n date,\n cents: v.cents / 100,\n calls: v.calls,\n }))\n .sort((a, b) => a.date.localeCompare(b.date));\n\n const recentRows = await client.execute({\n sql: `SELECT id, created_at, label, app, model,\n input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,\n cost_cents_x100\n FROM token_usage\n WHERE owner_email = ?\n ORDER BY created_at DESC\n LIMIT 50`,\n args: [options.ownerEmail],\n });\n const recent: UsageRecentEntry[] = (\n recentRows.rows as Array<Record<string, number | string | null>>\n ).map((row) => ({\n id: Number(row.id),\n createdAt: Number(row.created_at),\n label: String(row.label ?? \"chat\"),\n app: String(row.app ?? \"\"),\n model: String(row.model ?? \"\"),\n inputTokens: Number(row.input_tokens ?? 0),\n outputTokens: Number(row.output_tokens ?? 0),\n cacheReadTokens: Number(row.cache_read_tokens ?? 0),\n cacheWriteTokens: Number(row.cache_write_tokens ?? 0),\n cents: Number(row.cost_cents_x100 ?? 0) / 100,\n }));\n\n return {\n billing: USD_USAGE_BILLING,\n totalCents: Number(t.cents ?? 0) / 100,\n totalCalls: Number(t.calls ?? 0),\n totalInputTokens: Number(t.in_tok ?? 0),\n totalOutputTokens: Number(t.out_tok ?? 0),\n totalCacheReadTokens: Number(t.cr_tok ?? 0),\n totalCacheWriteTokens: Number(t.cw_tok ?? 0),\n sinceMs,\n byLabel: mapBuckets(byLabelR.rows),\n byModel: mapBuckets(byModelR.rows),\n byApp: mapBuckets(byAppR.rows),\n byDay,\n recent,\n };\n}\n"]}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/usage/store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Token usage tracking and cost monitoring.\n *\n * Every LLM call made by the framework records a row here so users can\n * see where their spend is going — chat vs automations vs background jobs\n * vs whatever else a template labels its prompts as.\n *\n * Cost is stored as \"centicents\" (1/100th of a cent) for integer precision.\n */\nimport { getDbExec, intType, isPostgres } from \"../db/client.js\";\n\n/**\n * Per-million-token pricing in cents. Cache read is typically ~10% of\n * input; cache write (5m TTL) is ~125%. Pricing is best-effort — keep\n * this table in sync with Anthropic's published prices.\n */\ninterface ModelPricing {\n input: number;\n output: number;\n cacheRead: number;\n cacheWrite: number;\n}\n\nexport const BUILDER_AGENT_CREDIT_MARGIN_MULTIPLIER = 1.25;\nexport const BUILDER_AGENT_CREDITS_PER_USD = 20;\n\nexport type UsageBillingUnit = \"usd\" | \"builder-credits\";\n\nexport interface UsageBillingMode {\n unit: UsageBillingUnit;\n label: string;\n shortLabel: string;\n source: \"estimated-provider-cost\" | \"builder-agent-credits\";\n hardCostMarginMultiplier?: number;\n creditsPerUsd?: number;\n}\n\nexport const USD_USAGE_BILLING: UsageBillingMode = {\n unit: \"usd\",\n label: \"Estimated spend\",\n shortLabel: \"Cost\",\n source: \"estimated-provider-cost\",\n};\n\nexport const BUILDER_CREDIT_USAGE_BILLING: UsageBillingMode = {\n unit: \"builder-credits\",\n label: \"Builder.io credit spend\",\n shortLabel: \"Credits\",\n source: \"builder-agent-credits\",\n hardCostMarginMultiplier: BUILDER_AGENT_CREDIT_MARGIN_MULTIPLIER,\n creditsPerUsd: BUILDER_AGENT_CREDITS_PER_USD,\n};\n\nexport function usageBillingForEngine(\n engineName: string | null | undefined,\n): UsageBillingMode {\n return engineName === \"builder\"\n ? BUILDER_CREDIT_USAGE_BILLING\n : USD_USAGE_BILLING;\n}\n\nexport function builderCreditsFromCostCents(cents: number): number {\n if (!Number.isFinite(cents) || cents <= 0) return 0;\n const dollars = cents / 100;\n const credits =\n dollars *\n BUILDER_AGENT_CREDIT_MARGIN_MULTIPLIER *\n BUILDER_AGENT_CREDITS_PER_USD;\n return Math.ceil(credits * 1000) / 1000;\n}\n\nconst PRICING: Array<{ match: RegExp; pricing: ModelPricing }> = [\n {\n match: /opus/i,\n pricing: { input: 1500, output: 7500, cacheRead: 150, cacheWrite: 1875 },\n },\n {\n match: /haiku/i,\n pricing: { input: 100, output: 500, cacheRead: 10, cacheWrite: 125 },\n },\n // OpenAI / Codex models (cents per 1M tokens). Without these, a Codex recap\n // model like \"gpt-5.5\" falls through to the default (Sonnet) row and is\n // mispriced. Published rates as of 2026-06; OpenAI bills cached input at a\n // discount and has no separate cache-write token charge, so cacheWrite is 0\n // (recap usage passes 0 cache-write anyway). The /gpt-5\\.5/ row must precede\n // /gpt-5/ since the latter also matches \"gpt-5.5\".\n {\n match: /gpt-5\\.5/i,\n pricing: { input: 500, output: 3000, cacheRead: 50, cacheWrite: 0 },\n },\n {\n match: /gpt-5/i,\n pricing: { input: 125, output: 1000, cacheRead: 12.5, cacheWrite: 0 },\n },\n // default → sonnet pricing\n {\n match: /.*/,\n pricing: { input: 300, output: 1500, cacheRead: 30, cacheWrite: 375 },\n },\n];\n\nfunction pricingFor(model: string): ModelPricing {\n for (const entry of PRICING) {\n if (entry.match.test(model)) return entry.pricing;\n }\n return PRICING[PRICING.length - 1]!.pricing;\n}\n\nexport interface UsageRecord {\n ownerEmail: string;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n model: string;\n /** Category for this call — e.g. \"chat\", \"automation\", \"job\", \"custom-agent\". */\n label?: string;\n /** Optional template/app name (e.g. \"mail\"). Falls back to AGENT_APP / APP_NAME env. */\n app?: string;\n /**\n * Stable id of the thing this usage belongs to (e.g. a recap plan id). When\n * set, any prior row(s) with the same (label, refId) are deleted before\n * insert, so re-recording the same run overwrites instead of double-counting.\n */\n refId?: string;\n /**\n * Precomputed cost in centicents (1/100¢). When provided, it is stored\n * verbatim instead of being derived from tokens — e.g. to mirror a\n * provider-reported dollar cost so two surfaces agree exactly.\n */\n costCentsX100?: number;\n}\n\nlet _initPromise: Promise<void> | undefined;\n\nasync function ensureUsageTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n await client.execute(`\n CREATE TABLE IF NOT EXISTS token_usage (\n id ${intType()} PRIMARY KEY,\n owner_email TEXT NOT NULL,\n input_tokens ${intType()} NOT NULL DEFAULT 0,\n output_tokens ${intType()} NOT NULL DEFAULT 0,\n cache_read_tokens ${intType()} NOT NULL DEFAULT 0,\n cache_write_tokens ${intType()} NOT NULL DEFAULT 0,\n cost_cents_x100 ${intType()} NOT NULL DEFAULT 0,\n model TEXT NOT NULL DEFAULT '',\n label TEXT NOT NULL DEFAULT 'chat',\n app TEXT NOT NULL DEFAULT '',\n ref_id TEXT NOT NULL DEFAULT '',\n created_at ${intType()} NOT NULL\n )\n `);\n\n // Add columns on older deployments that pre-date the label/cache\n // fields. Each ALTER is wrapped so a dialect without IF NOT EXISTS\n // (SQLite) still makes progress if only some columns are missing.\n const additions: Array<[string, string]> = [\n [\"cache_read_tokens\", `${intType()} NOT NULL DEFAULT 0`],\n [\"cache_write_tokens\", `${intType()} NOT NULL DEFAULT 0`],\n [\"label\", `TEXT NOT NULL DEFAULT 'chat'`],\n [\"app\", `TEXT NOT NULL DEFAULT ''`],\n [\"ref_id\", `TEXT NOT NULL DEFAULT ''`],\n ];\n for (const [col, def] of additions) {\n try {\n if (isPostgres()) {\n await client.execute(\n `ALTER TABLE token_usage ADD COLUMN IF NOT EXISTS ${col} ${def}`,\n );\n } else {\n await client.execute(\n `ALTER TABLE token_usage ADD COLUMN ${col} ${def}`,\n );\n }\n } catch {\n // Column already exists — ignore\n }\n }\n\n try {\n await client.execute(\n `CREATE INDEX IF NOT EXISTS idx_token_usage_owner_created ON token_usage (owner_email, created_at)`,\n );\n } catch {}\n })().catch((err) => {\n // Retry init on the next call after a failed startup.\n _initPromise = undefined;\n throw err;\n });\n }\n return _initPromise;\n}\n\n/**\n * Calculate cost in centicents (1/100th of a cent).\n * Accepts cache tokens so callers that use prompt caching are priced\n * correctly. Non-cache-aware callers can pass 0 for the cache fields.\n */\nexport function calculateCost(\n inputTokens: number,\n outputTokens: number,\n model: string,\n cacheReadTokens = 0,\n cacheWriteTokens = 0,\n): number {\n const p = pricingFor(model);\n const rawCenticents =\n (inputTokens / 1_000_000) * p.input * 100 +\n (outputTokens / 1_000_000) * p.output * 100 +\n (cacheReadTokens / 1_000_000) * p.cacheRead * 100 +\n (cacheWriteTokens / 1_000_000) * p.cacheWrite * 100;\n return rawCenticents > 0 ? Math.max(1, Math.round(rawCenticents)) : 0;\n}\n\n/**\n * Record token usage from an LLM call.\n *\n * Accepts an object with the full set of fields. A positional overload\n * remains for backward compatibility with the older 4-arg signature.\n */\nexport async function recordUsage(record: UsageRecord): Promise<void>;\nexport async function recordUsage(\n ownerEmail: string,\n inputTokens: number,\n outputTokens: number,\n model: string,\n): Promise<void>;\nexport async function recordUsage(\n recordOrOwner: UsageRecord | string,\n inputTokens?: number,\n outputTokens?: number,\n model?: string,\n): Promise<void> {\n const record: UsageRecord =\n typeof recordOrOwner === \"string\"\n ? {\n ownerEmail: recordOrOwner,\n inputTokens: inputTokens ?? 0,\n outputTokens: outputTokens ?? 0,\n model: model ?? \"\",\n }\n : recordOrOwner;\n\n const {\n ownerEmail,\n inputTokens: inTok,\n outputTokens: outTok,\n cacheReadTokens = 0,\n cacheWriteTokens = 0,\n model: modelName,\n label,\n app,\n refId,\n costCentsX100,\n } = record;\n\n // Skip no-op writes (e.g. a stream aborted before any tokens flowed)\n if (!inTok && !outTok && !cacheReadTokens && !cacheWriteTokens) return;\n\n await ensureUsageTable();\n const client = getDbExec();\n const resolvedApp =\n app ?? process.env.AGENT_APP ?? process.env.APP_NAME ?? \"\";\n const resolvedLabel = label ?? \"chat\";\n const resolvedRef = refId ?? \"\";\n\n // Replace any prior usage for this (label, refId) so re-recording the same\n // run — e.g. a recap regenerated on a PR re-push — overwrites instead of\n // double-counting. No-op when refId is unset (the common per-call path).\n if (resolvedRef) {\n await client.execute({\n sql: `DELETE FROM token_usage WHERE label = ? AND ref_id = ?`,\n args: [resolvedLabel, resolvedRef],\n });\n }\n\n // Prefer an explicit precomputed cost (e.g. a provider-reported dollar cost);\n // otherwise derive it from tokens via the pricing table.\n const costX100 =\n costCentsX100 ??\n calculateCost(inTok, outTok, modelName, cacheReadTokens, cacheWriteTokens);\n const id = Date.now() * 1000 + Math.floor(Math.random() * 1000);\n await client.execute({\n sql: `INSERT INTO token_usage\n (id, owner_email, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, cost_cents_x100, model, label, app, ref_id, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n args: [\n id,\n ownerEmail,\n inTok,\n outTok,\n cacheReadTokens,\n cacheWriteTokens,\n costX100,\n modelName,\n resolvedLabel,\n resolvedApp,\n resolvedRef,\n Date.now(),\n ],\n });\n}\n\n/** Total cost (in cents) charged against a user, across all time. */\nexport async function getUserUsageCents(ownerEmail: string): Promise<number> {\n await ensureUsageTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT COALESCE(SUM(cost_cents_x100), 0) as total FROM token_usage WHERE owner_email = ?`,\n args: [ownerEmail],\n });\n const total = Number((rows[0] as { total?: number })?.total ?? 0);\n return total / 100;\n}\n\n// ─── Admin / UI queries ─────────────────────────────────────────────────\n\nexport interface UsageSummaryOptions {\n ownerEmail: string;\n /** Inclusive lower bound (ms since epoch). Defaults to 30 days ago. */\n sinceMs?: number;\n}\n\nexport interface UsageBucket {\n key: string;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n cents: number;\n calls: number;\n}\n\nexport interface DailyBucket {\n /** YYYY-MM-DD (UTC) */\n date: string;\n cents: number;\n calls: number;\n}\n\nexport interface UsageRecentEntry {\n id: number;\n createdAt: number;\n label: string;\n app: string;\n model: string;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n cents: number;\n}\n\nexport interface UsageSummary {\n billing?: UsageBillingMode;\n totalCents: number;\n totalCalls: number;\n totalInputTokens: number;\n totalOutputTokens: number;\n totalCacheReadTokens: number;\n totalCacheWriteTokens: number;\n sinceMs: number;\n byLabel: UsageBucket[];\n byModel: UsageBucket[];\n byApp: UsageBucket[];\n byDay: DailyBucket[];\n recent: UsageRecentEntry[];\n}\n\nconst DAY_MS = 86_400_000;\n\n/**\n * Produce an aggregated spend view for the Usage admin panel.\n * Scoped to the passed owner email; the UI always passes the session user.\n */\nexport async function getUsageSummary(\n options: UsageSummaryOptions,\n): Promise<UsageSummary> {\n await ensureUsageTable();\n const client = getDbExec();\n const sinceMs = options.sinceMs ?? Date.now() - 30 * DAY_MS;\n\n const totalRow = await client.execute({\n sql: `SELECT\n COALESCE(SUM(cost_cents_x100), 0) AS cents,\n COUNT(*) AS calls,\n COALESCE(SUM(input_tokens), 0) AS in_tok,\n COALESCE(SUM(output_tokens), 0) AS out_tok,\n COALESCE(SUM(cache_read_tokens), 0) AS cr_tok,\n COALESCE(SUM(cache_write_tokens), 0) AS cw_tok\n FROM token_usage WHERE owner_email = ? AND created_at >= ?`,\n args: [options.ownerEmail, sinceMs],\n });\n const t = (totalRow.rows[0] ?? {}) as Record<string, number | null>;\n\n const bucketSql = (col: string) => ({\n sql: `SELECT ${col} AS k,\n COALESCE(SUM(cost_cents_x100), 0) AS cents,\n COUNT(*) AS calls,\n COALESCE(SUM(input_tokens), 0) AS in_tok,\n COALESCE(SUM(output_tokens), 0) AS out_tok,\n COALESCE(SUM(cache_read_tokens), 0) AS cr_tok,\n COALESCE(SUM(cache_write_tokens), 0) AS cw_tok\n FROM token_usage\n WHERE owner_email = ? AND created_at >= ?\n GROUP BY ${col}\n ORDER BY cents DESC`,\n args: [options.ownerEmail, sinceMs],\n });\n\n const mapBuckets = (rows: unknown[]): UsageBucket[] =>\n rows.map((r) => {\n const row = r as Record<string, number | string | null>;\n return {\n key: String(row.k ?? \"\"),\n cents: Number(row.cents ?? 0) / 100,\n calls: Number(row.calls ?? 0),\n inputTokens: Number(row.in_tok ?? 0),\n outputTokens: Number(row.out_tok ?? 0),\n cacheReadTokens: Number(row.cr_tok ?? 0),\n cacheWriteTokens: Number(row.cw_tok ?? 0),\n };\n });\n\n const [byLabelR, byModelR, byAppR] = await Promise.all([\n client.execute(bucketSql(\"label\")),\n client.execute(bucketSql(\"model\")),\n client.execute(bucketSql(\"app\")),\n ]);\n\n // By-day aggregation — done in JS so we don't depend on dialect-specific\n // date functions (SQLite `strftime`, Postgres `to_char`). Cheap enough\n // for a 30-day window; if this grows, swap for a dialect-aware query.\n const dayRows = await client.execute({\n sql: `SELECT created_at, cost_cents_x100 FROM token_usage\n WHERE owner_email = ? AND created_at >= ?`,\n args: [options.ownerEmail, sinceMs],\n });\n const dayMap = new Map<string, { cents: number; calls: number }>();\n for (const row of dayRows.rows as Array<Record<string, number>>) {\n const date = new Date(Number(row.created_at)).toISOString().slice(0, 10);\n const prev = dayMap.get(date) ?? { cents: 0, calls: 0 };\n prev.cents += Number(row.cost_cents_x100 ?? 0);\n prev.calls += 1;\n dayMap.set(date, prev);\n }\n const byDay: DailyBucket[] = [...dayMap.entries()]\n .map(([date, v]) => ({\n date,\n cents: v.cents / 100,\n calls: v.calls,\n }))\n .sort((a, b) => a.date.localeCompare(b.date));\n\n const recentRows = await client.execute({\n sql: `SELECT id, created_at, label, app, model,\n input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,\n cost_cents_x100\n FROM token_usage\n WHERE owner_email = ?\n ORDER BY created_at DESC\n LIMIT 50`,\n args: [options.ownerEmail],\n });\n const recent: UsageRecentEntry[] = (\n recentRows.rows as Array<Record<string, number | string | null>>\n ).map((row) => ({\n id: Number(row.id),\n createdAt: Number(row.created_at),\n label: String(row.label ?? \"chat\"),\n app: String(row.app ?? \"\"),\n model: String(row.model ?? \"\"),\n inputTokens: Number(row.input_tokens ?? 0),\n outputTokens: Number(row.output_tokens ?? 0),\n cacheReadTokens: Number(row.cache_read_tokens ?? 0),\n cacheWriteTokens: Number(row.cache_write_tokens ?? 0),\n cents: Number(row.cost_cents_x100 ?? 0) / 100,\n }));\n\n return {\n billing: USD_USAGE_BILLING,\n totalCents: Number(t.cents ?? 0) / 100,\n totalCalls: Number(t.calls ?? 0),\n totalInputTokens: Number(t.in_tok ?? 0),\n totalOutputTokens: Number(t.out_tok ?? 0),\n totalCacheReadTokens: Number(t.cr_tok ?? 0),\n totalCacheWriteTokens: Number(t.cw_tok ?? 0),\n sinceMs,\n byLabel: mapBuckets(byLabelR.rows),\n byModel: mapBuckets(byModelR.rows),\n byApp: mapBuckets(byAppR.rows),\n byDay,\n recent,\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.43.0",
3
+ "version": "0.44.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -14,6 +14,8 @@ metadata:
14
14
 
15
15
  All application data lives in **SQL** (SQLite locally, persistent database in production). The agent and UI share the same database. Do not store durable app data in the filesystem.
16
16
 
17
+ When you add a data model, a list, or a read path, also follow the `performance` skill: project only the columns a list renders, index the columns hot queries filter/sort on, and avoid query waterfalls — so apps stay fast as data grows.
18
+
17
19
  ## How It Works
18
20
 
19
21
  Agent-native apps use Drizzle ORM over the configured SQL backend. Local development works out of the box with a SQLite file at `data/app.db`; production and shared preview deploys need a persistent `DATABASE_URL` because container/serverless filesystems can reset. The code should behave the same across backends, but the local SQLite file is not durable once deployed.