@terreno/mcp 57.8.0 → 57.10.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 (115) hide show
  1. package/dist/docs/upgrades/57.10.1.md +54 -0
  2. package/dist/docs/upgrades/57.9.0.md +28 -0
  3. package/dist/docs/versioned/next/README.md +1 -0
  4. package/dist/docs/versioned/next/explanation/README.md +4 -0
  5. package/dist/docs/versioned/next/explanation/admin-plugin-frontend.md +2 -2
  6. package/dist/docs/versioned/next/explanation/agent-ui-asks.md +299 -0
  7. package/dist/docs/versioned/next/explanation/agent-ui-blocks.md +67 -0
  8. package/dist/docs/versioned/next/explanation/ai-observability.md +101 -0
  9. package/dist/docs/versioned/next/explanation/charts.md +12 -0
  10. package/dist/docs/versioned/next/explanation/durable-agent-harness.md +531 -0
  11. package/dist/docs/versioned/next/explanation/example-coverage.md +4 -2
  12. package/dist/docs/versioned/next/explanation/loop-engineering.md +1 -1
  13. package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +49 -1
  14. package/dist/docs/versioned/next/explanation/typescript-configuration.md +1 -1
  15. package/dist/docs/versioned/next/explanation/versioning-policy.md +1 -1
  16. package/dist/docs/versioned/next/how-to/README.md +7 -1
  17. package/dist/docs/versioned/next/how-to/admin-import-prebuilt.md +1 -1
  18. package/dist/docs/versioned/next/how-to/agent-ui-asks.md +730 -0
  19. package/dist/docs/versioned/next/how-to/agent-ui-blocks.md +104 -0
  20. package/dist/docs/versioned/next/how-to/ai-feature-development.md +156 -0
  21. package/dist/docs/versioned/next/how-to/build-a-durable-workflow.md +232 -0
  22. package/dist/docs/versioned/next/how-to/charts-and-dashboards.md +7 -0
  23. package/dist/docs/versioned/next/how-to/circleci.md +9 -6
  24. package/dist/docs/versioned/next/how-to/compare-chart-rendered-snapshots.md +8 -0
  25. package/dist/docs/versioned/next/how-to/install-agent-skills.md +1 -1
  26. package/dist/docs/versioned/next/how-to/observe-llm-calls.md +197 -0
  27. package/dist/docs/versioned/next/how-to/run-tests-locally.md +2 -1
  28. package/dist/docs/versioned/next/how-to/ship-a-new-task-version.md +76 -0
  29. package/dist/docs/versioned/next/how-to/upgrade-terreno.md +1 -1
  30. package/dist/docs/versioned/next/implementationPlans/agent-ui-asks.md +688 -0
  31. package/dist/docs/versioned/next/implementationPlans/agent-ui-blocks.md +10 -6
  32. package/dist/docs/versioned/next/implementationPlans/ai-observability.md +475 -157
  33. package/dist/docs/versioned/next/implementationPlans/b2b-platform-program.md +1 -1
  34. package/dist/docs/versioned/next/implementationPlans/charts-and-dashboards.md +3 -2
  35. package/dist/docs/versioned/next/implementationPlans/dashboard-chart-parity.md +275 -0
  36. package/dist/docs/versioned/next/implementationPlans/durable-agent-harness.md +340 -0
  37. package/dist/docs/versioned/next/implementationPlans/terreno-langfuse-integration.md +1 -1
  38. package/dist/docs/versioned/next/reference/README.md +3 -0
  39. package/dist/docs/versioned/next/reference/admin-config.md +2 -0
  40. package/dist/docs/versioned/next/reference/admin-frontend.md +216 -0
  41. package/dist/docs/versioned/next/reference/agent-ui-asks.md +1059 -0
  42. package/dist/docs/versioned/next/reference/ai-harness.md +1881 -0
  43. package/dist/docs/versioned/next/reference/ai.md +283 -28
  44. package/dist/docs/versioned/next/reference/api.md +46 -0
  45. package/dist/docs/versioned/next/reference/blocks.md +178 -0
  46. package/dist/docs/versioned/next/reference/environment-variables.md +2 -1
  47. package/dist/docs/versioned/next/reference/feature-flags.md +1 -1
  48. package/dist/docs/versioned/next/reference/mcp-server.md +17 -0
  49. package/dist/docs/versioned/next/reference/test.md +11 -2
  50. package/dist/docs/versioned/next/reference/ui.md +418 -11
  51. package/dist/docs/versioned/next/tasks/agent-ui-asks.md +213 -0
  52. package/dist/docs/versioned/next/tasks/agent-ui-blocks.md +2 -1
  53. package/dist/docs/versioned/next/tasks/ai-observability.md +233 -104
  54. package/dist/docs/versioned/next/tasks/dashboard-chart-parity.md +145 -0
  55. package/dist/docs/versioned/next/tasks/durable-agent-harness.md +183 -0
  56. package/dist/tools.d.ts +4 -0
  57. package/dist/tools.d.ts.map +1 -1
  58. package/dist/tools.js +37 -0
  59. package/dist/tools.js.map +1 -1
  60. package/package.json +3 -2
  61. package/src/docs/upgrades/57.10.1.md +54 -0
  62. package/src/docs/upgrades/57.9.0.md +28 -0
  63. package/src/docs/versioned/next/README.md +1 -0
  64. package/src/docs/versioned/next/explanation/README.md +4 -0
  65. package/src/docs/versioned/next/explanation/admin-plugin-frontend.md +2 -2
  66. package/src/docs/versioned/next/explanation/agent-ui-asks.md +299 -0
  67. package/src/docs/versioned/next/explanation/agent-ui-blocks.md +67 -0
  68. package/src/docs/versioned/next/explanation/ai-observability.md +101 -0
  69. package/src/docs/versioned/next/explanation/charts.md +12 -0
  70. package/src/docs/versioned/next/explanation/durable-agent-harness.md +531 -0
  71. package/src/docs/versioned/next/explanation/example-coverage.md +4 -2
  72. package/src/docs/versioned/next/explanation/loop-engineering.md +1 -1
  73. package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +49 -1
  74. package/src/docs/versioned/next/explanation/typescript-configuration.md +1 -1
  75. package/src/docs/versioned/next/explanation/versioning-policy.md +1 -1
  76. package/src/docs/versioned/next/how-to/README.md +7 -1
  77. package/src/docs/versioned/next/how-to/admin-import-prebuilt.md +1 -1
  78. package/src/docs/versioned/next/how-to/agent-ui-asks.md +730 -0
  79. package/src/docs/versioned/next/how-to/agent-ui-blocks.md +104 -0
  80. package/src/docs/versioned/next/how-to/ai-feature-development.md +156 -0
  81. package/src/docs/versioned/next/how-to/build-a-durable-workflow.md +232 -0
  82. package/src/docs/versioned/next/how-to/charts-and-dashboards.md +7 -0
  83. package/src/docs/versioned/next/how-to/circleci.md +9 -6
  84. package/src/docs/versioned/next/how-to/compare-chart-rendered-snapshots.md +8 -0
  85. package/src/docs/versioned/next/how-to/install-agent-skills.md +1 -1
  86. package/src/docs/versioned/next/how-to/observe-llm-calls.md +197 -0
  87. package/src/docs/versioned/next/how-to/run-tests-locally.md +2 -1
  88. package/src/docs/versioned/next/how-to/ship-a-new-task-version.md +76 -0
  89. package/src/docs/versioned/next/how-to/upgrade-terreno.md +1 -1
  90. package/src/docs/versioned/next/implementationPlans/agent-ui-asks.md +688 -0
  91. package/src/docs/versioned/next/implementationPlans/agent-ui-blocks.md +10 -6
  92. package/src/docs/versioned/next/implementationPlans/ai-observability.md +475 -157
  93. package/src/docs/versioned/next/implementationPlans/b2b-platform-program.md +1 -1
  94. package/src/docs/versioned/next/implementationPlans/charts-and-dashboards.md +3 -2
  95. package/src/docs/versioned/next/implementationPlans/dashboard-chart-parity.md +275 -0
  96. package/src/docs/versioned/next/implementationPlans/durable-agent-harness.md +340 -0
  97. package/src/docs/versioned/next/implementationPlans/terreno-langfuse-integration.md +1 -1
  98. package/src/docs/versioned/next/reference/README.md +3 -0
  99. package/src/docs/versioned/next/reference/admin-config.md +2 -0
  100. package/src/docs/versioned/next/reference/admin-frontend.md +216 -0
  101. package/src/docs/versioned/next/reference/agent-ui-asks.md +1059 -0
  102. package/src/docs/versioned/next/reference/ai-harness.md +1881 -0
  103. package/src/docs/versioned/next/reference/ai.md +283 -28
  104. package/src/docs/versioned/next/reference/api.md +46 -0
  105. package/src/docs/versioned/next/reference/blocks.md +178 -0
  106. package/src/docs/versioned/next/reference/environment-variables.md +2 -1
  107. package/src/docs/versioned/next/reference/feature-flags.md +1 -1
  108. package/src/docs/versioned/next/reference/mcp-server.md +17 -0
  109. package/src/docs/versioned/next/reference/test.md +11 -2
  110. package/src/docs/versioned/next/reference/ui.md +418 -11
  111. package/src/docs/versioned/next/tasks/agent-ui-asks.md +213 -0
  112. package/src/docs/versioned/next/tasks/agent-ui-blocks.md +2 -1
  113. package/src/docs/versioned/next/tasks/ai-observability.md +233 -104
  114. package/src/docs/versioned/next/tasks/dashboard-chart-parity.md +145 -0
  115. package/src/docs/versioned/next/tasks/durable-agent-harness.md +183 -0
@@ -0,0 +1,299 @@
1
+ # Agent UI Asks
2
+
3
+ An ask lets an agent stop in the middle of a turn, ask the user a typed question inside the chat,
4
+ and continue with the answer as a tool result. This page explains why asks are client-side tool
5
+ calls on the existing chat stream, how one round trip works, how a watch or another small client
6
+ answers them, and how asks and Agent UI Blocks divide the work. Fields, limits, events, and error
7
+ codes are in the [reference](../reference/agent-ui-asks.md).
8
+
9
+ Asks ship today as five kinds: `choice`, to pick one option or several with an optional answer
10
+ of the user's own (Other), `confirm`, to approve or deny one action, `markdown`, to edit a draft
11
+ the agent wrote and send it back, `form`, to fill in a few typed fields at once, and `files`, to
12
+ send images or documents the agent reads in the same turn. The chat asks
13
+ on `POST /gpt/prompt` and shows asks in `GPTChat`. Watches and other small clients answer
14
+ select-one choices and confirms on two JSON endpoints with the compact surface, and can approve a
15
+ markdown draft or submit a form's defaults as is. A host tool can also require an approval that
16
+ the server enforces, shown as a `confirm`. The sandboxed HTML block and the `callout`, `image`,
17
+ and `details` display blocks follow the
18
+ [implementation plan](https://github.com/TerrenoLabs/terreno/blob/master/docs/implementationPlans/agent-ui-asks.md).
19
+
20
+ ## The problem
21
+
22
+ Agents often need one decision before they can go on: which plan, which record, whether to
23
+ proceed. In plain chat the agent asks in prose and the user types a reply. The agent then parses
24
+ free text that may not match any option, nothing ties the reply to the question, and nothing stops
25
+ the user from answering with something that was never offered.
26
+
27
+ ## Why client-side tool calls
28
+
29
+ Each ask kind is an AI SDK tool, such as `ask_choice`, with a Zod input schema, an output schema,
30
+ and no `execute` function. Most of the design follows from that choice:
31
+
32
+ - **The model is held to the schema.** Provider function calling makes the model write a JSON
33
+ object for the tool, and the AI SDK checks it against the Zod schema. An invalid ask goes back
34
+ to the model as a tool error it can fix in its next step. It never reaches the user.
35
+ - **The turn pauses by itself.** A tool without `execute` ends the AI SDK step loop, so the
36
+ server needs no waiting state inside the model call. It saves the paused turn and ends the
37
+ stream.
38
+ - **The answer is tied to the question.** The tool call id is the key. The answer names it, and
39
+ the server accepts an answer only for the ask the conversation waits on.
40
+ - **The model already knows how to read the answer.** It comes back as the tool's result, which
41
+ every provider represents natively, so no prompt wording has to explain what the user chose.
42
+
43
+ This is the same pattern as AI SDK client-side tools, AG-UI interrupts, and CopilotKit
44
+ human-in-the-loop. The answer envelope (`accept`, `decline`, `cancel`) is the MCP elicitation
45
+ shape.
46
+
47
+ Other approaches were rejected:
48
+
49
+ | Approach | Why not |
50
+ | --- | --- |
51
+ | Move the chat to the AI SDK `useChat` UI message stream | Rewrites the SSE contract every host parses, and the history storage |
52
+ | Form blocks inside the reply document, like Slack Block Kit inputs | Values come back as user text with nothing tying them to a question, and they need blocks turned on |
53
+ | Interactive HTML mini-apps, like MCP Apps | No React Native host exists, and they open the widest phishing and data-leak surface |
54
+
55
+ ## One round trip
56
+
57
+ 1. **Ask.** The model calls `ask_choice` with a prompt and options. The AI SDK checks the input;
58
+ an invalid ask goes back to the model as a tool error.
59
+ 2. **Pause.** The step loop ends. The server derives the ask's simple card and saves the paused
60
+ turn on the history as `pendingAsk`, including the AI SDK messages the turn produced. It adds a
61
+ row for the ask with status `pending`, then sends `{ask}` with the conversation's `historyId`
62
+ and `{done}` with `pendingAsk`. Because `{ask}` names the conversation, a client can answer a
63
+ new chat's ask before `{done}` arrives.
64
+ 3. **Answer.** The client shows the ask and posts the answer with the ask's tool call id to
65
+ `POST /gpt/prompt`.
66
+ 4. **Check.** The server confirms the history belongs to the caller (403), that this is the ask
67
+ it waits on (409), and that the answer fits the ask (400, and the model is not called).
68
+ 5. **Resume.** One atomic update stores the answer row, marks the ask answered, and clears
69
+ `pendingAsk`. The server replays the saved messages with the answer as the tool result and
70
+ streams the rest of the turn, starting with `{askResolved}`.
71
+
72
+ The server replays the saved messages instead of rebuilding them from history rows because the AI
73
+ SDK messages carry provider metadata, such as Gemini thought signatures, that the rows do not
74
+ keep. Replaying them resumes the exact turn the provider started. In later turns, the history goes
75
+ through `buildMessages`, which keeps each ask and its answer so the model remembers what it asked
76
+ and what the user chose. Host tool rows stay out of the model's messages, as before.
77
+
78
+ The update is atomic because two answers to one ask, from a double tap or two devices, must resume
79
+ the turn once. It matches on the pending ask's tool call id, so the second answer finds nothing to
80
+ update and gets 409.
81
+
82
+ The answer stays stored even when the model call that continues the turn fails. The user did
83
+ answer, so the next message continues from the ask and its answer instead of asking again. Any
84
+ turn that fails after its stream starts still ends with `{error}` then `{done}` and saves what the
85
+ user saw, so a client always learns the turn ended and which conversation holds it.
86
+
87
+ ## One ask at a time
88
+
89
+ A conversation waits on at most one ask. When the model asks twice in one step, the first ask
90
+ pauses the turn and the others are answered with `cancel` and the reason `one_ask_at_a_time`,
91
+ which the model sees when the turn resumes. Two turns on one conversation, say from two open tabs,
92
+ follow the same rule. Setting the pending ask is a conditional update, so the ask saved first
93
+ keeps waiting and the other turn's ask gets the same `cancel` instead of overwriting it.
94
+
95
+ The user is never stuck behind a question. Typing a message instead of answering records `cancel`
96
+ with the reason `user_sent_message`, then adds the message. The model sees both and can respond to
97
+ what the user actually said. A message is never refused because its ask is gone: when another tab
98
+ answered the ask first, the message goes ahead on the conversation as that answer left it. Only an
99
+ answer can be stale, so only an answer gets 409.
100
+
101
+ Turns never write back a whole copy of the conversation. Each turn appends its rows with one
102
+ atomic update when it ends, and ratings set one field, so two turns or a rating racing a turn
103
+ cannot drop each other's rows. A paused turn remembers how many rows come before and include its
104
+ own user message, and its resume replays exactly those.
105
+
106
+ ## Approvals the server enforces
107
+
108
+ `ask_confirm` works only when the model remembers to call it. A prompt injection, or a model that
109
+ skips the step, can still call a destructive tool directly. So a host tool can set the AI SDK's
110
+ `needsApproval: true`, and the server, not the model, makes the confirm.
111
+
112
+ When the model calls such a tool, the AI SDK stops before `execute` and emits a
113
+ `tool-approval-request`. The server turns it into a `confirm` ask with `origin: "approval"` and
114
+ pauses the turn exactly as for a model ask: same pending slot, same simple card, same answers
115
+ through `/gpt/prompt` and `turn`. The answer becomes the SDK's `tool-approval-response`. On
116
+ resume the SDK runs the tool once when approved, or gives the model a denial when not, so the
117
+ tool's own code never has to check.
118
+
119
+ The approval uses the one pending slot rather than a second one. A client then has one question
120
+ to show and one way to answer, and a watch needs no new card type. When a step holds a model ask
121
+ and an approval, or two approvals, the first pauses the turn and each other approval is denied
122
+ with `one_ask_at_a_time`. The model sees the denial and can ask again in its next turn.
123
+
124
+ The host words the card with `asks.approvals`, because only the host knows what a call changes.
125
+ The default, "Allow <toolName>?" with the tool's description, is safe but vague, so a
126
+ destructive tool should name the effect and set `destructive: true`.
127
+
128
+ Approval rows are for display. Later turns do not replay them, just as they do not replay host
129
+ tool rows: the model's memory of the call is what it wrote in its reply.
130
+
131
+ ## Strict by design
132
+
133
+ Every ask kind is a strict schema with hard limits. The limits live in one constant,
134
+ `ASK_LIMITS`, which the schemas, the system prompt section, the tests, and a doc-parity test all
135
+ read. The model can only ask for controls a client can render, and the user can only send answers
136
+ the ask allows. The same pure validators run on the server and in clients, so a client can enable
137
+ Submit exactly when the server will accept the answer.
138
+
139
+ ## Simple cards
140
+
141
+ Every ask comes with a simple card: short text and at most three buttons, each holding the exact
142
+ answer it sends. A client that knows nothing about ask kinds, such as a watch, a notification, or
143
+ a chat bot, can render the card and answer with one tap by sending back the tapped button's answer.
144
+ When the buttons cannot show every option the ask offers, the card sets `handoff` so the client
145
+ can send the user to the full app. A card with a button for every option does not set it, even
146
+ when Skip is left out to make room.
147
+
148
+ The server derives the card once, when the ask is made, and stores it with the pending ask. A card
149
+ already on a screen never changes, even if the derivation rules change later.
150
+
151
+ Each button carries its exact answer, not a label for the client to turn into one. A client that
152
+ knows no ask kinds cannot build a valid answer: it would need each kind's answer shape and rules.
153
+ With the answer on the button, those rules stay on the server, and a test checks that every
154
+ button's answer passes `validateAskResponse` for its ask. A client can also send only the pressed
155
+ button's `id`. The server looks the id up on the card it stored and answers with that button's
156
+ answer, so a client that sends only ids cannot send an answer the card did not offer.
157
+
158
+ ## In the chat
159
+
160
+ `GPTChat` shows an ask where the model asked it: the ask's tool call row becomes an `AskCard` in
161
+ the transcript, and the continuation streams in below it. The host still owns the network.
162
+ `GPTChat` calls `onAskSubmit`, and the host posts the answer and reads the continuation with the
163
+ same stream reader it uses for prompts. An answer the server rejects comes back to the card as
164
+ `askErrors`, next to the control that caused it.
165
+
166
+ A `choice` whose options all fit its simple card renders as that card's buttons, and a tap answers.
167
+ A phone then shows the same buttons a watch would, and the most common ask, two or three short
168
+ options, takes one tap. Longer lists get radio buttons or a searchable select with Submit.
169
+
170
+ A `select: "many"` choice renders as checkboxes with Submit, because one tap cannot pick several.
171
+ Its Other field lets the user answer with something the agent did not list, without leaving the
172
+ ask for free text: the answer still names the offered ids in `selected`, and only the typed part
173
+ is in `other`. Other counts as one choice against `maxSelected`, so "pick up to three" means three
174
+ things in total, however the user picks them. Other is offered only with `"many"`; an agent that
175
+ wants "one option or your own" asks `"many"` with `maxSelected: 1`, so every ask with free text
176
+ goes through the same control and the same count rule. On a small screen a many-select card can
177
+ offer only its suggested set and Skip, so it always hands off, and the compact surface does not
178
+ offer `"many"` at all.
179
+
180
+ A `confirm` guards one action the agent is about to take, such as deleting data or sending a
181
+ message for the user. Its answer is `{confirmed: true}` or `{confirmed: false}`, so "no" is a real
182
+ answer the agent must respect, not a skip: deny is the second button, and Skip appears only when
183
+ the ask sets `allowDecline`. The card shows the simple card's two buttons in the same order on a
184
+ phone and a watch, approve first and deny last. A `destructive` ask draws the approve button in
185
+ the destructive style, and a watch never makes it the Double Tap button, so a gesture cannot
186
+ approve an action that cannot be undone. The confirm is a pause in the turn, not a lock: the host's
187
+ tool should still check that the action is allowed.
188
+
189
+ A `markdown` ask hands the user a draft, such as an announcement or an email, in a markdown editor
190
+ with a preview. It is one round trip, not a shared document: the user edits and sends the text
191
+ back, and the agent continues with it. The answer carries `changed`, and the server checks it
192
+ against the text, so `changed: false` reliably means "approved as is" and the agent need not diff
193
+ the text itself. Editing needs a keyboard and room, so the compact surface never offers `markdown`.
194
+ A watch still sees a markdown ask the phone's chat made: its card offers Approve draft, when the
195
+ draft already meets the length rules, and Cancel, and it always hands off to the phone. A long
196
+ answer collapses to a preview in the transcript, so one draft does not push the conversation out
197
+ of view.
198
+
199
+ A `form` asks for several values in one step, such as the details for an invoice, instead of a
200
+ string of questions. Its fields are flat and typed, and the same rules check a default when the
201
+ ask is made and a value when the answer arrives, so the agent never receives a value its own form
202
+ would reject and never offers a default the user could not send back. Blank means unanswered.
203
+ Dates travel as ISO strings in fixed shapes (`YYYY-MM-DD`, 24-hour `HH:mm`, and datetimes with an
204
+ offset) so the agent never has to guess a time zone. Each error names its field
205
+ (`content.values.<id>`), so the card shows it where the user can fix it. A small screen cannot
206
+ fill in fields, so the compact surface never offers `form`; a watch can still send the defaults
207
+ when every required field has one, and otherwise hands off to the phone.
208
+
209
+ A `files` ask gets the user's files to the model as the model's own input types: images as image
210
+ parts, PDFs as file parts, and text, CSV, and JSON as text cut at 100 KB. The declared type is not
211
+ trusted. The server reads the bytes and rejects a file whose bytes are another type, so a renamed
212
+ file cannot reach the model as something it is not. Files travel as uploads when the host has
213
+ file storage and as data URLs when it does not, so an app without a bucket still works. Either
214
+ way the history keeps only each file's name, type, and size: the bytes reach the model once, on
215
+ the answering turn, and a long conversation does not grow by the size of its files. Picking files
216
+ needs a phone or a computer, so the compact surface never offers `files`.
217
+
218
+ After an ask ends, the card collapses to one line, such as "You chose: Team" or "You declined:
219
+ Keep them". A long conversation
220
+ then reads as a record of what the agent asked and what the user decided. The answer's
221
+ `tool-result` row stays in the message list but is hidden. Ratings address messages by their index
222
+ in the stored rows, so the client keeps one message per row, and after a reload the summary reads
223
+ the answer from that row.
224
+
225
+ A pending ask takes focus when it appears, so keyboard and screen reader users land on the question
226
+ instead of hunting for it.
227
+
228
+ ## Small screens and the watch
229
+
230
+ A watch shows about three short buttons. A full `choice` can offer 50 options, and on a watch its
231
+ card can only hand off to the phone. So a small client says where the user is with
232
+ `surface: "compact"`, and the server narrows what the agent may ask on that turn: a `choice`
233
+ with `select: "one"` and 2–3 options whose labels fit a button uncut and differ from each other,
234
+ or a `confirm`, whose two labels already fit. Every card made on a
235
+ compact turn can be answered from the watch, and the system prompt asks for replies of at most two
236
+ short sentences. The narrowing is in the tool's input schema, not only in the prompt, so the model
237
+ cannot ask what the watch cannot show. A compact ask that breaks the rules goes back to the model
238
+ as a tool error, like any invalid ask.
239
+
240
+ The surface belongs to a turn, not to the conversation. One conversation can start in the phone's
241
+ chat and continue on the watch, and each turn gets the rules of the screen it was sent from.
242
+
243
+ A watch app, a notification action, or a chat bot wants to send one request and read one
244
+ response, not a server-sent event stream. The `turn` action runs the same turn as `/gpt/prompt`,
245
+ through the same turn runner, and collects its events into one JSON result. Both save the same
246
+ rows, so the phone's chat shows what was answered on the watch. The turn keeps running when the
247
+ client disconnects, which matters on a watch, where the system can suspend the app mid-request.
248
+ The turn is saved, and the client reads the reply or the next ask later. `pendingAsks` lists what
249
+ waits on the user across every conversation, so a watch can show it without opening each one.
250
+
251
+ How an Apple Watch can plug in:
252
+
253
+ | Path | What it needs | Status |
254
+ | --- | --- | --- |
255
+ | SwiftUI watch app | A native watchOS target, because React Native does not run on watchOS. `@bacons/apple-targets` adds one to an Expo app. The phone hands the session token over WatchConnectivity, and the app calls the two endpoints with `URLSession`, always with `surface: "compact"`. The [how-to](../how-to/agent-ui-asks.md#answer-asks-from-an-apple-watch-or-another-small-client) sketches one. | Protocol ready; a sample app is Future Work |
256
+ | Actionable push, no watch app | iOS forwards iPhone notifications with up to 4 action buttons to the watch. It needs `categoryId` on comms push, `data` on `notify()`, templated categories (iOS fixes action labels when the app registers a category), actions that do not open the app, a small native iOS handler (expo-notifications completes before a JavaScript `fetch` finishes), and a one-time action token per ask. | Future Work |
257
+
258
+ Other follow-ups: finishing long turns in a `@terreno/jobs` background job, so a headless answer
259
+ returns at once instead of waiting for the agent, and Wear OS.
260
+
261
+ ## Asks and blocks
262
+
263
+ Asks and [UI blocks](agent-ui-blocks.md) split the work by where the
264
+ result goes. Body components are Phase 5 of the asks plan; the design record remains
265
+ [agent-ui-blocks](../implementationPlans/agent-ui-blocks.md).
266
+
267
+ | Need | Mechanism |
268
+ | --- | --- |
269
+ | Show data, such as a chart, table, or metric | A block in the reply |
270
+ | Suggest a follow-up message | A block button that sends a reply |
271
+ | Run host code, such as an export | A block button with a host callback |
272
+ | Get an answer the agent needs to continue | An ask: tool call, pause, answer, resume |
273
+ | Answer from a watch or another small client | The ask's simple card, through `pendingAsks` and `turn` |
274
+
275
+ Blocks display, and their buttons start something new: a message or host code. Asks collect, and
276
+ their answer goes back to the agent as the result of the call that asked. An ask's `prompt` is
277
+ plain text, so an ask cannot show a chart or table above its control yet. Both live in
278
+ `@terreno/blocks`, which holds the schemas for what the agent can show and ask.
279
+
280
+ ## Trust boundaries
281
+
282
+ - **Only the server writes the paused turn.** The saved messages are replayed to the model
283
+ verbatim, so a client must not be able to plant or edit them. The history REST API drops
284
+ `pendingAsk` from create and update bodies, and the answer a client sends is only the envelope,
285
+ checked against the stored ask.
286
+ - **A button id answers only with the stored card.** The server resolves `buttonId` on the card
287
+ it saved when the ask was made. An id that is not on the card gets 400 `UNKNOWN_BUTTON`, and the
288
+ ask stays pending.
289
+ - **A client can only answer an approval, never make one.** The approval request and the
290
+ `tool-approval-response` live only in the server's stored messages. A client sends the answer
291
+ envelope for the pending ask's `toolCallId`, and the server builds the response for the
292
+ `approvalId` it stored. A crafted approval response in the body gets 400, and another tool
293
+ call's id gets 409.
294
+ - **A turn speaks as the conversation's owner.** Only the owner can send a `turn`. An admin gets
295
+ 403, because the model would read the admin's message or answer as the owner's.
296
+ - **Asks do not collect secrets.** The system prompt tells the model never to ask for passwords,
297
+ payment card numbers, API keys, or other secrets.
298
+ - **HTML is display-only and opt-in.** An `html` block can be used for XSS, phishing, exfiltration, or clickjacking if it runs as part of the host page. The server allows it only when `uiBlocks.html` is on, strips scripts, event handlers, forms, frames, `meta`/`base`/`link`, anchor `href`s, and every URL except `data:image`, then stores that cleaned document. The client draws it in a sandboxed frame with no JavaScript and no navigation, labeled as an agent-generated preview, and only after the reply has finished.
299
+ - **Image hosts are an allowlist.** An `image` block can leak conversation data by requesting a URL the model chooses. `data:image` URLs and `file:` refs are always allowed. An `https` URL loads only when its hostname is in `uiBlocks.imageHosts`. The default list is empty, so a hostile prompt cannot point the image at a server the app did not name.
@@ -0,0 +1,67 @@
1
+ # Agent UI blocks
2
+
3
+ When a chat turns UI blocks on, the assistant reply is one YAML document of Terreno
4
+ components, not free HTML or JSX. This page explains why the catalog is closed and why the
5
+ whole reply is the document. Field tables and error codes are in the
6
+ [reference](../reference/blocks.md). Asks, which return a typed answer to the agent, stay
7
+ in [Agent UI Asks](agent-ui-asks.md).
8
+
9
+ The design record is
10
+ [agent-ui-blocks.md](../implementationPlans/agent-ui-blocks.md). Pick executes that work as
11
+ Phase 5 of [Agent UI Asks](../implementationPlans/agent-ui-asks.md).
12
+
13
+ ## Why a closed catalog
14
+
15
+ An agent that can emit arbitrary components can also emit styles, scripts, and layout the
16
+ app theme does not own. The catalog is the list of `@terreno/ui` components the chat can
17
+ paint: heading, text, metric, badge, divider, context, chart, table, actions, and the two
18
+ layout blocks `columns` and `card`. Unknown
19
+ keys and unknown block types fail validation, so a reply cannot smuggle a `style` object
20
+ or a hex color.
21
+
22
+ The same pure functions run wherever a document is checked. `parseBlocks` and
23
+ `validateBlocks` live in `@terreno/blocks`, which has no React and no Express. The renderer
24
+ and the chat route will call them; they do not grow a second grammar.
25
+
26
+ ## Why the whole reply is YAML
27
+
28
+ The document is the reply. There is no prose before it and no fenced block inside
29
+ markdown. A leading or trailing fence is stripped so a model that adds one still parses.
30
+ JSON is accepted because it is a YAML 1.2 subset and is what a structured-output call
31
+ emits.
32
+
33
+ Key order is `v`, then `datasets`, then `blocks`. Datasets come first so a streaming
34
+ renderer can know the data before the first block. `parseBlocksPartial` returns the
35
+ top-level blocks that have already finished and marks the cut tail pending. The finished
36
+ document is still checked with `validateBlocks`.
37
+
38
+ Text in the same step as a tool call is not the reply, except when that text is a block document and no later step has text. An actions block that left off `id` receives one before storage, and the chat is sent the corrected document.
39
+
40
+ A reply that is not a mapping with `v` is not a document. The chat can still show it:
41
+ `wrapAsTextDocument` turns the raw text into one `text` block. The error
42
+ `NOT_A_DOCUMENT` is how the caller tells that case apart from a document that failed a
43
+ field check.
44
+
45
+ YAML anchors, aliases, and explicit tags are rejected. They let one node stand in for
46
+ another, which makes a small document able to expand in ways the limits cannot see.
47
+
48
+ ## Ownership
49
+
50
+ | Layer | Owns |
51
+ | --- | --- |
52
+ | `@terreno/blocks` | Schema, parse, partial parse, validate, JSON Schema, prompt section, CLI, limits, error codes |
53
+ | `@terreno/ui` | `BlocksView` for heading, text, metric, badge, divider, context, chart, table, actions, columns, and card. A select action changes the target chart or table dataset in that view. A segmented control starts on the option that matches that dataset. A callback stays disabled when `hostActions` is set and does not include its name. `GPTChat` `uiBlocks` renders assistant messages through `BlocksView`, including a spinner while the reply is still streaming. A `file:` image stays as alt text until `resolveImage` returns a URL. |
54
+ | `@terreno/ai` | When `uiBlocks` is on: the chat system prompt, the post-stream `{blocks}` check, `AIDataset` storage, and `POST /gpt/actions`. `AIService.generateBlocks` returns one validated document outside a chat turn. |
55
+
56
+ Asks use the same package under `src/asks/`. A block shows something. An ask collects an
57
+ answer and returns it to the agent.
58
+
59
+ A callback button is the Block Kit `block_actions` path. The document names the callback.
60
+ The host owns the code.
61
+
62
+ | | Block Kit | Terreno |
63
+ | --- | --- | --- |
64
+ | Who runs the click | Slack posts `block_actions` to the app | The client posts `POST /gpt/actions` |
65
+ | Payload check | The app trusts the platform body | The host Zod schema runs before the handler |
66
+ | What comes back | `chat.update` replaces the message | `{replace, blocks}` replaces the block, or `{text}` appends a message |
67
+ | Time limit | The platform's request window | 10 seconds, then 504 |
@@ -0,0 +1,101 @@
1
+ # AI observability
2
+
3
+ Terreno ships Langfuse-like prompt versioning, nested traces, evaluators, datasets, experiments, and a local human review queue **inside `@terreno/ai`**, with operator UI in **`admin-frontend` only**.
4
+
5
+ ## Why not Langfuse-only
6
+
7
+ Apps must iterate prompts without a required vendor account. `LangfuseApp` stays a second plugin. Traces and scores **fan out** to every registered sink (local Mongo, Langfuse, OTLP/OpenInference). Prompts, datasets, experiments, and the review queue have **one writer** each (a primary). The review queue is **local-only**.
8
+
9
+ That split is the two planes:
10
+
11
+ | Plane | Write | Admin read |
12
+ | --- | --- | --- |
13
+ | Telemetry (traces, scores) | All sinks, best-effort | Local if the local plugin is on, else Langfuse |
14
+ | Control (prompts, datasets, experiments, queue) | One primary per capability | That primary |
15
+
16
+ `experiments.primary` must equal `datasets.primary`. Defaults are all `local`.
17
+
18
+ `ObservabilityApp` fails boot (constructor throws) when:
19
+
20
+ | Error | Cause |
21
+ | --- | --- |
22
+ | `experiments.primary must equal datasets.primary` | Mixed dataset/experiment writers |
23
+ | `reviewQueue.primary must be local` | `reviewQueue` set to `langfuse` |
24
+ | `<capability> primary "<id>" has no plugin` | No registered plugin with that `id`, capability, and store |
25
+
26
+ Telemetry sinks still fan out even when a control primary is local-only.
27
+
28
+ Admin chrome lives in `admin-frontend`. One sidebar group **AI Observability** holds Prompts,
29
+ Traces, Evaluators, Datasets, Experiments, and Review (Review is omitted when the local plugin is
30
+ off). Existing **AI Requests** stays under **Screens**. Every observability screen shows
31
+ breadcrumbs `Admin / AI Observability / <Section> / <leaf>` and a status chip from
32
+ `GET /ai/observability/status`. Prompt detail is a hub (Overview, Versions, Traces, Experiments);
33
+ versions are immutable in admin (`Save as vN+1`); apps never inline the string.
34
+
35
+ ## Why this shape for product work
36
+
37
+ Flourish AI features follow an 8-step loop: gold dataset → labels → prompt versions → evaluators → experiments with gates → `production` label → live traces and in-app feedback → weak traces back into the dataset.
38
+
39
+ That loop is the product requirement, not an optional dashboard. Operator steps: [Develop an AI feature](../how-to/ai-feature-development.md). Registration and env: [Observe LLM calls](../how-to/observe-llm-calls.md). Models and routes: [AI reference](../reference/ai.md). Design lock: [implementation plan](../implementationPlans/ai-observability.md).
40
+
41
+ ## RBAC (domain-neutral)
42
+
43
+ Observability HTTP routes use Terreno RBAC resources (`aiPrompt`, `aiTrace`, `aiReview`,
44
+ `aiDataset`, `aiExperiment`, `aiEvaluator`) instead of domain-specific approval vocabulary.
45
+ Pass the same `accessControl` instance on `ObservabilityApp` and `AdminApp` so
46
+ `GET /ai/observability/status` permissions match `/admin/config` screen filtering. Admin widgets
47
+ hide write controls from status flags (fail closed while status is loading or errored). Seeded
48
+ `admin` receives every observability action; `auditor` receives `list` and
49
+ `read` on each observability resource through the read-only sentinel (but not `admin:access` —
50
+ pair auditor with a shell grant in a composed consumer role); `superadmin` receives `*`. Existing
51
+ customized `admin` and `auditor` roles gain only missing observability actions on re-seed. Legacy
52
+ `user.admin` remains a full-access fallback when RBAC is enabled. See
53
+ [API reference](../reference/api.md#ai-observability-rbac). The example backend registers
54
+ `accessControl: access` on `ObservabilityApp` and seeds optional `aiObservabilityViewer` and
55
+ `aiObservabilityOperator` roles (not auto-assigned) in `example-backend/src/rbacRoles.ts`.
56
+
57
+ `AIRequest` remains the cheap per-call log. Observability traces are the nested, scored, user/session/cost record used in the SOP.
58
+
59
+ ## Prompt hub relationships (phase 4)
60
+
61
+ Prompts may carry an optional domain-neutral `description` (folder semantics stay consumer-defined). `GET /ai/observability/prompts/:name` composes bounded recent **traces** and **experiments** for that prompt name so the admin hub does not scan full trace or experiment lists on the client. Trace evidence is version-aware: list filters accept `prompt` plus optional `promptVersion`, and relationship trace rows include the matching `promptVersion` from `ObsTrace.prompts[]`. The same optional positive integer `promptVersion` on prompt detail filters hub traces with `$elemMatch` on `prompts` (name and version). Omit it to keep the unfiltered prompt-name match. Invalid or non-positive versions return **400**. Experiment relationships filter server-side by `promptName` on `GET /ai/observability/experiments?promptName=…` (omit or pass an empty value for no filter). The experiment list endpoint returns summary rows without per-item hydration; use experiment detail for full item results. Reading relationships uses the same `aiPrompt:read` grant as prompt detail.
62
+
63
+ ## Phase 1 reference loop
64
+
65
+ The example backend always registers the local plugin. Its idempotent seed creates
66
+ `examples/example-summarize` with production on v1 and an experimental v2, the human
67
+ `correctness-human` evaluator, the automatic `schema-assert` evaluator, and a two-item
68
+ `example-gold` dataset. It also creates `chat-safety-synthetic`, a proofread set of
69
+ two-person chats for routing, toxicity, privacy, and dismissive-reply experiments.
70
+ This makes the review and experiment loops walkable without Langfuse:
71
+ resolve the production prompt → emit a trace → inspect spans and sensitive I/O → send the
72
+ trace to Review → record a human score.
73
+
74
+ The example frontend supplies that first step from real product usage: **Todos → Summarize**
75
+ calls `/ai/example-summarize`, which resolves `example-summarize` by name and `production`
76
+ label server-side. Prompt selection stays on the server, while the client only contributes
77
+ identity (`x-ai-session-id`) and, when the backend has no provider credentials, the user's own
78
+ key.
79
+
80
+ `AI_OBS_PRICE_MAP_JSON` belongs to deployment configuration because prices change
81
+ independently of prompt versions. A missing model price preserves token counts and omits
82
+ USD cost; it never invents `$0`.
83
+
84
+ ## Phase 2 backend (evaluators, datasets, experiments)
85
+
86
+ With the local plugin registered and primaries set to `local`, the control plane now includes:
87
+
88
+ - **`llm-judge` and `json-assert` evaluators** — judges call `AIService.generateJsonObject` through a named registry prompt (`judgePromptName`); create/update rejects a judge when the prompt `outputSchema` omits a required dimension (the error names that key). `json-assert` supports path/constraint checks and a built-in mode that validates output against the prompt version `outputSchema`. Parse or generation failures record an error outcome instead of throwing to the caller. Custom creates score a full trace; generation span and dataset item targets are reserved. Seeded templates install `correctness` / `hallucination` / `helpfulness` / `toxicity` as `llm-judge`, `schema-assert` as `json-assert`, and `*-human` variants for the review queue.
89
+ - **Human review evaluators** — sending traces to review requires a `human` evaluator. Its dimensions become the review item's score fields and its instructions tell the reviewer how to apply them. `llm-judge` and `json-assert` evaluators run automatically and are rejected from the human queue.
90
+ - **Datasets** — CRUD plus item provenance (`origin`, `proofread`, `sourceTraceId`, tags, outcome class). Import accepts **JSON** (bare objects or structured rows) and **CSV** (quoted fields; `input.*` / `expectedOutput.*` column prefixes). Rows validate against the bound prompt input schema when configured. Adding from a trace copies I/O; sensitive traces always land `proofread: false`. Deleting an item never mutates the trace.
91
+ - **Experiments** — compare 2–3 prompt versions on a dataset with optional `modelOverride` (requires `aiServiceFactory` on `ObservabilityApp`), evaluator thresholds (defaulting to the SOP gates), **per-version gate tiles** (`gates[].version`), outlier/low-confidence item ids, and version-scoped promote (**409** when the selected version's gates fail). Unproofread items are excluded unless `includeUnproofread` is true. Local runs compile exact prompt versions and call `AIService.generateText` with compiled `prompt` + `systemPrompt` (no `promptName`/`promptLabel`). `ObservabilityApp` wires the local experiment runner from `aiService` at register time.
92
+
93
+ Operator UI for datasets and experiments ships in tasks 2.7–2.9; routes and stores are live for API clients and tests today.
94
+
95
+ ## GPT tool spans and nested traces
96
+
97
+ `/gpt/prompt` streams tool rounds to the client **and** records them in observability traces. Each provider `tool-call` / `tool-result` pair becomes a `TOOL` span (input args, cleaned output, timing, `parentSpanId` on the chain root). When tools run, the exported trace uses a `CHAIN` root wrapping those `TOOL` children instead of a lone `LLM` span. Plain `AIService.generate*` calls without tool children still log a single `LLM` root.
98
+
99
+ ## Admin multi-stage trace smoke test
100
+
101
+ `POST /ai/observability/traces/test-multi-stage` (admin-only) exercises a real multi-stage workflow through the configured `ObservabilityApp.aiService`: two `generateJsonObject` LLM passes (`skipTrace: true`) with named output schemas, a deterministic local `text-metrics` tool stage, then a final synthesis `generateJsonObject` call against a combining schema. System prompts live in `OBS_TEST_MULTI_STAGE_*` constants in `ai/src/service/prompts.ts` (never inline). Each LLM span records `{prompt, schemaName, outputSchema}` as input and the parsed object as output. The handler exports one parent `TraceRecord` via `ObservabilityApp.exportTrace` with child span order `LLM`, `LLM`, `TOOL`, `LLM` and returns `{traceId, output, stages}` so operators can open the persisted trace in admin. Missing `aiService` → **503**; child failure exports an error trace then propagates the error.
@@ -20,6 +20,14 @@ Height works the same way: `height` is the whole chart, and the tick row, toolti
20
20
  row come out of that budget. A chart therefore fits a fixed-height slot, and reserving the tooltip
21
21
  row even when no tooltip is showing keeps hovering from reflowing the page.
22
22
 
23
+ Cartesian facades use `xTickPolicy="auto"` by default: up to seven categorical labels
24
+ stay horizontal, while denser sets rotate 45 degrees and reserve a taller axis row.
25
+ That taller row comes out of `height`; the plot shrinks so the chart still fits the
26
+ requested slot. `SparklineChart` scales to the data range so a small period-over-period
27
+ change stays visible.
28
+ Callers may force `"truncate"` or `"rotate"`. `title` and `periodLabel` are convenience
29
+ props backed by the same `ChartCard` used for explicit chart/table composition.
30
+
23
31
  `DashboardGrid` is layout only, so it stays on the eager root export. Chart implementations sit behind the same lazy root boundary as `GPTChat`. Cell width subtracts flex `gap` so `md`/`lg` column counts actually fit.
24
32
 
25
33
  Chart paint is proven with **rendered PNG goldens**, not React test-renderer JSON.
@@ -27,3 +35,7 @@ Chart paint is proven with **rendered PNG goldens**, not React test-renderer JSO
27
35
  (`/demo/chart-visual-gallery`), screenshots each fixture from easy to hard, and
28
36
  pixel-diffs against `demo/rendered-snapshots/`. Agents use the `review-chart-visuals`
29
37
  skill to read golden / actual / diff images when a compare fails.
38
+
39
+ The example app admin home (`/admin`) mounts `AdminCharts` so scorecards, a multi-series line,
40
+ bars, an area, and a donut with a center label render together. See
41
+ [Compose charts in a dashboard grid](../how-to/charts-and-dashboards.md).