@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,531 @@
1
+ # Durable agent harness
2
+
3
+ `@terreno/ai/harness` runs long-lived work so that a crash, deploy, or restart never loses
4
+ it. Work is split into **phases**. Each finished phase commits a **checkpoint**, and the
5
+ same Mongo transaction writes its audit span. API: [AI harness reference](../reference/ai-harness.md).
6
+ Worked example (fetch → agent → sign-off → once-only write, with a crash drill):
7
+ [Build a durable workflow](../how-to/build-a-durable-workflow.md).
8
+ Design lock: [implementation plan](../implementationPlans/durable-agent-harness.md).
9
+
10
+ ## Why phases, not one long handler
11
+
12
+ A chat turn in `/gpt/prompt` lives inside one HTTP request. A `@terreno/jobs` job is one
13
+ opaque handler. Neither can stop halfway and pick up where it left off.
14
+
15
+ A harness task is a small state machine. A phase does some work, then calls
16
+ `rt.commit()` with the next phase or a terminal outcome. Work that was already committed
17
+ never runs again. After a crash, a fresh process restarts the **current phase** from its
18
+ last checkpoint, or parks it for a human when re-running is not safe (see
19
+ [Leases and crash resume](#leases-and-crash-resume)).
20
+
21
+ The harness does not journal and replay individual steps (Temporal/Inngest style). The
22
+ phase is the only replay boundary, so the unit to reason about is "what happens if this
23
+ phase runs twice?" A phase declares the answer with `replay` (`"safe"` or the default
24
+ `"never"`).
25
+
26
+ ## Core concepts
27
+
28
+ | Concept | What it is |
29
+ | --- | --- |
30
+ | **Task** | Durable unit of work: `name@version`, `input`, `state`, `phase`, status, ownership. Model requests, tool calls, workflows, and subagents are all tasks. |
31
+ | **Phase** | A named async function of a task. It does work and calls `rt.commit()` with the next phase or a terminal outcome. A phase is the replay boundary. |
32
+ | **Checkpoint** | The committed `{phase, state}`. Resume always starts the current phase from its checkpoint. |
33
+ | **Conversation** | A transcript plus agent config (model, instructions, tools, extensions). Root, or owned by a task (subagent). |
34
+ | **Agent** | `defineAgent` config. Running an agent runs the built-in `terreno.agent.turn` task on a conversation. |
35
+ | **Tool** | `defineTool` with zod params and `replay: "safe" \| "never"` (default `never`). Runs as a child task of the turn. |
36
+ | **Extension** | Named bundle of prompt sections, tools, hooks, and tool wraps. |
37
+ | **Hook** | `beforeModelRequest`, `beforeTool`, `afterTool`. Can rewrite, block, or annotate. |
38
+ | **Memo** | `rt.memo(key, value?)`: durable decision storage scoped to a task. Survives restarts. |
39
+ | **Approval** | `rt.approval(key, {...})`: creates a `HarnessApproval`, waits for a decision, audits the decider. |
40
+ | **Event** | Append-only `HarnessEvent` rows with a per-stream `seq`. The SSE source. |
41
+ | **Runner** | Decides who executes runnable tasks and when. |
42
+
43
+ Shipped (Phase 1): tasks, phases, checkpoints, the transactional audit span, the
44
+ `InProcessRunner` with owner and task leases, crash resume, `resolveInterrupted`, phase
45
+ retries, child tasks with `rt.waitForTasks`, `harness.abort` over the ownership tree,
46
+ agents, tools, and conversations (see [The agent loop](#the-agent-loop)), subagents,
47
+ extensions, hooks, and memos (see [Extensions, hooks, and memos](#extensions-hooks-and-memos)),
48
+ events, waits, and approvals with the admin inbox, the SSE event stream, and version
49
+ pinning. The example backend's `clinic.intakeSummary` runs all of it end to end, including
50
+ a crash drill: [Build a durable workflow](../how-to/build-a-durable-workflow.md). Other
51
+ runners (jobs-backed, concurrent) come in Phase 2.
52
+
53
+ ## Why the audit span shares the checkpoint transaction
54
+
55
+ Regulated work (clinical automation is the reference case) needs proof of every step.
56
+ `ObsSpan` from [AI observability](ai-observability.md) **is** the harness audit log, so
57
+ the admin Traces screens show harness runs with no extra UI.
58
+
59
+ A best-effort span written after the checkpoint could go missing on a crash. That would
60
+ leave a committed step with no audit record. So `rt.commit()` writes, in **one** Mongo
61
+ transaction:
62
+
63
+ 1. The `HarnessTask` update: next `phase` and `state`, or the terminal `outcome`.
64
+ 2. One `CHAIN` span for the finished phase, parented to the task's root span.
65
+ 3. On a terminal commit, the closed root span and root `ObsTrace`.
66
+
67
+ Either all three land or none do. This is why `Harness.open` refuses to start without a
68
+ replica set (transactions need one) or without the local observability models. Planned
69
+ Langfuse and OTel exports will stay best-effort, after the transaction.
70
+
71
+ ```
72
+ createTask ──tx──> HarnessTask(pending) + ObsTrace + root CHAIN span
73
+ phase "fetch" ── rt.commit ──tx──> task.phase=summarize + CHAIN span "fetch"
74
+ phase "summarize" ── rt.commit ──tx──> task.status=completed + CHAIN span "summarize"
75
+ + root span / trace closed
76
+ ```
77
+
78
+ ## Leases and crash resume
79
+
80
+ A process can die at any moment, and a frozen process can wake up later and keep going.
81
+ Two leases make both cases safe.
82
+
83
+ | Lease | Question it answers | Granularity |
84
+ | --- | --- | --- |
85
+ | **Owner lease** (`HarnessOwner`) | Which process drains tasks right now? | One per database |
86
+ | **Task lease** (`HarnessTask.lease`) | Which run of this phase may commit? | One per running phase, with a random fencing `token` |
87
+
88
+ Both expire unless their holder renews them on a heartbeat. A second process starts on
89
+ **standby** and becomes owner once the owner lease expires (or at once, when the owner
90
+ stops cleanly).
91
+
92
+ The owner lease is about efficiency: one drainer, no claim storms. The **task token** is
93
+ about correctness. Every commit is fenced on the token, so a runner that lost its lease
94
+ (it froze past expiry and someone else took over) is rejected with
95
+ `HarnessCommitConflictError` and writes nothing, even if two processes briefly both think
96
+ they own execution.
97
+
98
+ ```
99
+ process A: claim (token t1) ── phase runs ── A freezes ......... A wakes: commit(t1) ✗ rejected
100
+ process B: standby ........ A's leases expire ── takeover ── recover: phase replay
101
+ claim (token t2) ── commit(t2) ✓
102
+ ```
103
+
104
+ ### Replay is a per-phase promise
105
+
106
+ When a new owner finds a `running` task whose lease expired, it cannot know how far the
107
+ phase got. The phase's `replay` says what is safe:
108
+
109
+ - `replay: "safe"`: the phase can run again from its checkpoint (reads, idempotent writes).
110
+ The task goes back to `pending` and runs again.
111
+ - default `replay: "never"`: the phase might have done something that must not happen
112
+ twice (an EHR write, a payment). The task becomes `interrupted` and stops.
113
+
114
+ The harness never guesses. An operator looks at the external system and calls
115
+ `resolveInterrupted` with `retry` (run the phase again), `abort` (give up), or `complete`
116
+ (it already happened; record the result), plus a reason. That decision is audited like
117
+ any other step.
118
+
119
+ Every interruption also writes an error `CHAIN` span in the same transaction as the status
120
+ change, so the trace shows exactly which phase was cut off and what happened next.
121
+
122
+ ## Retries: a thrown phase is not a crash
123
+
124
+ A crash and a throw look different to the harness. A crash (expired lease) means "we do
125
+ not know how far the phase got", so `replay` decides. A throw means the phase code itself
126
+ said "this attempt failed", so the task's `retry` policy decides: back to `pending` at the
127
+ same checkpoint, with `runAt` pushed out by exponential backoff and jitter, until
128
+ `maxAttempts` runs are used up and the task ends `failed`.
129
+
130
+ Jitter matters because failures cluster. When an EHR goes down, every task that called it
131
+ fails within the same second. Without jitter they would all retry at the same instant and
132
+ knock it over again. Each retry waits a random delay between half and all of its backoff.
133
+
134
+ A retry re-runs the whole phase, so a phase that throws after a side effect repeats it.
135
+ Throw before the side effect, or make it idempotent. Programming errors (committing to an
136
+ unknown phase, forgetting to commit) fail at once: retrying cannot fix them.
137
+
138
+ ### Errors are APIErrors with stable codes
139
+
140
+ Harness errors reach three audiences: the code that called the harness, an HTTP client of
141
+ `HarnessApp`, and an operator reading a failed task or a Sentry issue. All three are served
142
+ by one shape, Terreno's `APIError`. The `code` (`harness-task-terminal`,
143
+ `harness-definition-invalid`, ...) and `title` never change, so callers branch on `code` and
144
+ Sentry groups occurrences together. What changed this time (which task, which key) lives in
145
+ `detail`. The same error thrown from `harness.abort` therefore becomes a 409 over HTTP with
146
+ no route code translating it.
147
+
148
+ The retry rule above rides on the class, not the code. A `HarnessDefinitionError` fails the
149
+ task at once. Any other error, including an `APIError` with the same code from a non-phase
150
+ call, is an ordinary failed attempt. A failed task records the error's `detail`. Each
151
+ harness detail is a full sentence, so the task's `error` reads as a complete message on
152
+ its own.
153
+
154
+ ## The ownership tree
155
+
156
+ Tasks form a tree. A phase calls `rt.createTask` to start a child. The child records
157
+ `ownership: {kind: "task", id: parent}` and shares the parent's `rootTaskId` and trace, so
158
+ its spans nest under the parent's span and the whole tree reads as one trace.
159
+
160
+ ```
161
+ root task (trace)
162
+ ├── child A ── grandchild A1
163
+ └── child B
164
+ ```
165
+
166
+ ### Waiting on children without holding a lease
167
+
168
+ A parent that needs its children's results calls `rt.waitForTasks(ids)`. The harness
169
+ does not keep the parent's phase suspended in memory. That would hold a lease for as long
170
+ as the children take, and a crash would lose the in-memory continuation anyway. Instead
171
+ the parent commits `waiting`, gives up its lease, and its phase stops. When the children
172
+ settle, the parent goes back to `pending` and its phase **runs again from its
173
+ checkpoint**. This time `waitForTasks` finds the children settled and returns their
174
+ outcomes at once.
175
+
176
+ That re-run is why `rt.createTask` is idempotent within one attempt of a phase visit: the
177
+ second run gets the children the first run created, not duplicates. A retry is a fresh
178
+ attempt, so it starts fresh children. It also means work before the
179
+ wait runs twice, so keep it to reads and child creation, and do side effects in a later
180
+ phase. Events and sleeps use the same "commit `waiting`, re-run on wake" shape.
181
+
182
+ ### Waiting for the outside world: events and sleeps
183
+
184
+ `rt.waitFor(event, {timeout})` pauses a task until something outside it happens: a
185
+ webhook, an operator decision, a lab result. `rt.sleep(duration)` pauses it for a fixed
186
+ time. Neither holds a lease while it waits.
187
+
188
+ `harness.sendEvent(taskId, event, payload)` writes the event to the task's **inbox**
189
+ before it does anything else. So the sender never has to know where the task is:
190
+
191
+ - If the task is already waiting on that event, the same transaction sends it back to
192
+ `pending`.
193
+ - If it has not reached the wait yet, the event stays buffered until it does.
194
+ - If no runner is up, the event is still stored, and the task resumes once one starts.
195
+
196
+ A timeout or a sleep is a `timeoutAt` on the wait. The runner claims a waiting task whose
197
+ `timeoutAt` has passed the same way it claims a retry whose `runAt` has passed. There is
198
+ no timer to lose in a crash.
199
+
200
+ When the phase re-runs, each wait call must return the same thing it returned before. The
201
+ harness records how each call resolved on the task, keyed by the phase visit and the
202
+ call's position, together with the event it received. A later run of that phase visit (a
203
+ retry, a crash replay, the wake for the next wait) reads the record and does not take
204
+ another event. A timeout is recorded too, so an event that arrives after the timeout can
205
+ never turn an earlier `undefined` into a payload.
206
+
207
+ `policy: "failFast"` resolves at the first failed or aborted child and aborts the
208
+ siblings still in flight. Use it when one failure makes the rest pointless (for example,
209
+ the allergy check failed, so the dosing calculation should stop).
210
+
211
+ ### Abort runs bottom-up
212
+
213
+ `harness.abort(taskId)` stops a task and everything it owns. Order matters for
214
+ compensation: a parent's `abort` handler often undoes what it set up for its children, so
215
+ it must run after they have stopped and undone their own work. The harness:
216
+
217
+ 1. Fences the whole subtree, top-down. Aborting tasks are never claimed again, and a
218
+ running phase loses its lease token, so nothing it does afterwards can commit. Its
219
+ `rt.signal` aborts so it can stop early.
220
+ 2. Walks the tree **deepest first**. For each task it runs the `abort` handler, then
221
+ commits `aborted` with an audit span. A turn's handler marks an assistant message
222
+ `aborted` when that message still has a tool call with no stored result, so the next
223
+ model request skips it.
224
+
225
+ Concurrent aborts of one task (an operator and a `failFast` sibling abort, say) run its
226
+ handler once. A failing handler does not stop the abort. A half-aborted tree would be worse than a
227
+ missed compensation, and the span records the failure for a human to follow up.
228
+
229
+ `background: true` marks a child that belongs to its owner but not to the owner's current
230
+ conversation turn. Conversations (a later slice) use it so that aborting a turn leaves
231
+ background work running.
232
+
233
+ ## Runners
234
+
235
+ The engine stores everything in Mongo. A **runner** only decides who executes runnable
236
+ tasks. Switching runners needs no data migration.
237
+
238
+ | Runner | Status | Ownership |
239
+ | --- | --- | --- |
240
+ | `InProcessRunner` | Shipped | One process holds the `HarnessOwner` lease and drains `pending` tasks one at a time; others wait on standby and take over on lease expiry. |
241
+ | `JobsRunner` | Planned (Phase 2) | Each runnable phase becomes a `@terreno/jobs` job. The task lease is the authority. |
242
+
243
+ ## Version pinning
244
+
245
+ Each task records `name` and `version`. A runner only claims tasks whose exact
246
+ `name@version` is in its registry. A process that registers only `v2` leaves `v1` rows
247
+ alone. Keep old versions registered until their in-flight tasks finish. `Harness.start()`
248
+ refuses to start while any non-terminal task uses a version the registry lacks, so a
249
+ deploy that drops a version too early fails loudly instead of stranding work. See
250
+ [Ship a new task version](../how-to/ship-a-new-task-version.md).
251
+
252
+ ## The agent loop
253
+
254
+ An agent turn is not special machinery. It is an ordinary registered task,
255
+ `terreno.agent.turn@1`, owned by its conversation, so everything above (leases, replay,
256
+ retries, the ownership tree, abort, version pinning) applies to it unchanged.
257
+
258
+ ```
259
+ submit ──tx──> user message + conversation busy + turn task (pending)
260
+
261
+ turn: request ──tx──> assistant message + LLM span ──┐ tool calls?
262
+ ▲ ▼
263
+ └──tx── tool messages <── waitForTasks <── tools: one child task per call
264
+ (each call = TOOL span)
265
+ request with no tool calls ──tx──> completed; conversation idle
266
+ ```
267
+
268
+ ### Two phases, both safe to replay
269
+
270
+ - **`request`** calls the model. A model call has no side effect, so if the process dies
271
+ mid-call the phase simply runs again. Nothing from the cut-off call reached the
272
+ transcript: the assistant message is only stored in the same transaction as the
273
+ checkpoint. (Its streamed text did reach the event log as expiring deltas; see
274
+ [Watching runs live](#watching-runs-live).)
275
+ - **`tools`** starts one child task per tool call and waits for all of them. Children
276
+ are found again by key on a re-run, so a replay never doubles a call.
277
+
278
+ The side effects live in the tools, and each tool says whether it may run twice.
279
+
280
+ ### Interrupted tools are reported, not hidden
281
+
282
+ A tool with the default `replay: "never"` might have done its work before the crash (a
283
+ note written, a message sent). The harness cannot know. Parking the call for an operator
284
+ would freeze the whole conversation, so instead the call ends with the result
285
+ "Interrupted, not retried" and the loop goes on. The model sees that result and can check
286
+ or ask the user. A tool marked `replay: "safe"` (a read, an idempotent write) just runs
287
+ again.
288
+
289
+ ### Retries belong to the model call
290
+
291
+ Providers fail in two ways. Overload and outages (429, 5xx, dropped connections) usually
292
+ pass, so the `request` phase retries them with backoff and jitter, then tries each
293
+ fallback model. Anything else (a 400 for a bad prompt, a 401 for a bad key) will fail the
294
+ same way every time, so the turn fails at once instead of burning retries and fallbacks.
295
+ Every attempt is listed on the `LLM` span, so the audit shows which model answered and
296
+ what failed before it.
297
+
298
+ ### Why the transcript shares the checkpoint transaction
299
+
300
+ The same rule as phase spans: a message written outside the commit could survive a crash
301
+ whose checkpoint did not, and the next request would replay a half-finished step. Each
302
+ commit writes the messages, the `LLM` span, and the checkpoint together, and each message
303
+ takes its `seq` from a counter on the conversation inside that transaction. The
304
+ transcript is therefore always exactly what the committed phases produced.
305
+
306
+ API: [Agents and conversations](../reference/ai-harness.md#agents-and-conversations).
307
+
308
+
309
+ ## Subagents
310
+
311
+ A workflow often needs one focused model job inside a larger, deterministic flow:
312
+ summarize this chart, classify that note. `rt.runAgent` runs that job as a **subagent**
313
+ and returns its answer to the phase, as text or as a schema-checked object.
314
+
315
+ A subagent is built from parts that already exist, not new machinery:
316
+
317
+ ```
318
+ caller phase ──tx──> subagent conversation (owned by the caller)
319
+ + user message
320
+ + turn task (child of the caller; AGENT span)
321
+ ──> waitForTasks([turn]) ── caller waits, holds no lease
322
+
323
+ turn: request / tools ... ──tx──> completed ── wakes the caller
324
+
325
+ caller phase re-runs ──> same call finds the same turn ──> returns its answer
326
+ ```
327
+
328
+ - **The conversation is owned by the task.** It is a normal transcript, so the
329
+ subagent's prompt, answer, and tool results are kept and auditable like any chat.
330
+ - **The turn is a child task.** Waiting, waking, crash recovery, and abort all come from
331
+ the ownership tree. Aborting the caller aborts the subagent's turn first.
332
+ - **The call is found again, not repeated.** The phase re-runs from its checkpoint after
333
+ the wait. Each `runAgent` call has a key from its position in the phase, so the re-run
334
+ finds the conversation and turn it created and gets the stored result. The
335
+ conversation, its first message, and the turn are created in one transaction, so a
336
+ crash can never leave one without the others.
337
+
338
+ ### Why the caller checks the structured output
339
+
340
+ The schema is a zod object in the caller's code, and code cannot be stored. The turn
341
+ gets a JSON Schema copy: enough to ask the provider for JSON and to reject an answer that
342
+ is not JSON at all. Rules JSON Schema cannot carry (refinements, transforms) are checked
343
+ when the caller gets the result, with the real schema. An invalid answer fails the call,
344
+ and the caller's retry policy decides whether to ask again.
345
+
346
+ ### Why only phases call subagents
347
+
348
+ A phase that waits re-runs from its checkpoint, and phases are written for that. A tool's
349
+ `execute` is not: re-running it from the top would repeat whatever it did before the
350
+ call. Tools therefore cannot call `runAgent`. A phase can run several subagents, one
351
+ after another; for parallel fan-out, start one child task per subagent and wait on all
352
+ of them.
353
+
354
+ API: [Subagents (rt.runAgent)](../reference/ai-harness.md#subagents-rtrunagent).
355
+
356
+ ## Extensions, hooks, and memos
357
+
358
+ Apps need to shape an agent without forking it: add a policy paragraph to the prompt,
359
+ refuse a dangerous tool call, redact a result, log every chart lookup. An **extension**
360
+ bundles those changes under a name, and agents and conversations list the extensions
361
+ they use. Extensions are code in the registry; a conversation stores only their names.
362
+
363
+ ```
364
+ request phase tool call task (child of the turn)
365
+ instructions + sections ─┐ args ──> beforeTool ──┬─ {block} ──> error result
366
+ beforeModelRequest ──────┤ └─ {args} ──> execute (wrapped)
367
+ model call │ │
368
+ commit: [system prompt │ afterTool
369
+ if changed] + │ │
370
+ assistant + LLM │ commit: tool result + TOOL span <────┘
371
+ ```
372
+
373
+ ### Hooks fail the step they run in
374
+
375
+ A hook is part of the step that runs it, so it fails like that step's own code. A
376
+ throwing `beforeModelRequest` hook or section fails the turn, as a model that never
377
+ answers would. A throwing tool hook fails the tool call, and the model gets the error
378
+ like any other tool failure and decides what to do. Nothing is retried: a hook that
379
+ failed once usually fails again, and a retry would call the model or the tool twice.
380
+
381
+ ### Blocking is a tool result, not an exception
382
+
383
+ `beforeTool` returning `{block: reason}` does not stop the turn. The tool never runs and
384
+ the model receives `reason` as an error result. The model can then explain, ask for
385
+ something else, or stop. This keeps the transcript honest (every tool call has a result)
386
+ and lets policy code refuse without knowing how the turn continues.
387
+
388
+ ### Sections are rebuilt, and recorded, every request
389
+
390
+ Sections are functions, so the prompt can change between requests: fresh vitals, a
391
+ policy that depends on the time of day. That makes "what did the model see?" a real
392
+ question for audit and resume. The turn answers it in the transcript: whenever the
393
+ effective system prompt differs from the last one recorded, the request's commit appends
394
+ a `system` message holding the exact text, and the `LLM` span points to it by hash and
395
+ seq. A prompt that does not change is recorded once, so long turns do not copy it into
396
+ every span.
397
+
398
+ ### Later extensions win, and wraps decorate the winner
399
+
400
+ Tool names are resolved in a fixed order: the agent's tools, then each extension's, with
401
+ a later tool of the same name replacing an earlier one. Wraps apply after that, to the
402
+ tool that won. An audit wrap therefore keeps working when another extension swaps in a
403
+ different implementation of the tool it watches.
404
+
405
+ ### Memos make a decision once
406
+
407
+ Phases and tool calls may run more than once (replay, retry, a second process after a
408
+ crash). A decision that must not change, such as "this call was approved" or "route to
409
+ the cardiology queue", goes in `rt.memo(key, value)`. The first write wins, atomically,
410
+ across concurrent calls and processes; every later call gets the stored value back. A
411
+ memo write commits on its own, fenced by the run's lease, so a crash right after the
412
+ decision still keeps it. Hook memos are scoped to the turn task: the turn outlives its
413
+ request re-runs and its tool calls, so their decisions are found again.
414
+
415
+ API: [Extensions](../reference/ai-harness.md#extensions), [Hooks](../reference/ai-harness.md#hooks), [Memos](../reference/ai-harness.md#memos).
416
+
417
+ ## Human approvals
418
+
419
+ Regulated work needs a person to sign off before the irreversible step: a clinician
420
+ approves the note before it is written to the EHR. The harness treats an approval as a
421
+ wait, not a new mechanism. `rt.approval` parks the task on an event that only a decision
422
+ sends, and writes the `HarnessApproval` row in the same commit that parks it. Everything
423
+ waits already guarantee carries over: the task holds no lease while a person thinks, a
424
+ decision made while every runner is down is stored and delivered on restart, and a re-run
425
+ of the phase finds the same approval instead of asking again.
426
+
427
+ ```
428
+ phase: rt.approval ──commit──> task waiting + HarnessApproval(pending)
429
+ │
430
+ inbox (approver) ── approve/reject ──> one transaction: approval decided
431
+ + inbox event (task -> pending)
432
+ + approval:<key> span (decidedBy)
433
+ │
434
+ phase re-runs ──> rt.approval returns the decision
435
+ ```
436
+
437
+ The approver's side is an inbox in `@terreno/admin-frontend`, not a bespoke screen per app.
438
+ `HarnessApp` contributes it to the admin sidebar, and the same component mounts on a
439
+ clinician-facing screen. It lists only what the viewer may decide, so the inbox and the
440
+ approve route answer the same permission question. See
441
+ [the inbox reference](../reference/admin-frontend.md#ai-harness-approvals-inbox).
442
+
443
+ ### Approvers are code, looked up by name
444
+
445
+ Who may approve is a list of permission functions in the same shape as modelRouter
446
+ permissions, so an app reuses its existing checks (`IsAdmin`, "is a clinician at this
447
+ clinic"). Functions cannot be stored, and the process answering the approver's HTTP
448
+ request did not run the phase, so the policy is declared on the task definition (or the
449
+ extension, for tool gates) and found again by `name@version` and key. A process that does
450
+ not register that version denies everyone rather than guessing.
451
+
452
+ ### The decision and its audit are one write
453
+
454
+ The approver's decision, the event that wakes the task, and the span that records who
455
+ decided commit together. There is no window in which the task proceeds without an audit
456
+ record, or an audit record exists for a decision the task never saw. Expiry follows the
457
+ same rule from the other side: it is written with the wait's timeout, only while the
458
+ approval is still pending, so a late decision and an expiry can never both apply.
459
+
460
+ ### Gating tools
461
+
462
+ `approvalGate` puts the same wait inside a `beforeTool` hook. The tool call's task waits;
463
+ the turn waits on the tool call. The decision is memoized by tool call id in the turn, so
464
+ a replayed call does not ask twice, and a rejection reaches the model as the tool's error
465
+ result, like any other block.
466
+
467
+ API: [Approvals](../reference/ai-harness.md#approvals), [approvalGate](../reference/ai-harness.md#approvalgate), [HarnessApp and HTTP routes](../reference/ai-harness.md#harnessapp-and-http-routes).
468
+
469
+ ## Watching runs live
470
+
471
+ People watch agents work: a clinician sees the summary being written, an operator follows
472
+ a long task tree. Clients watch over Server-Sent Events, and every SSE stream is served
473
+ from one durable log, `HarnessEvent`, rather than from the process running the work.
474
+
475
+ ```
476
+ commit tx ── message / status / approval change ──+── event (same tx, next seq)
477
+ model stream ── text ── coalesce ~250 ms ── delta (own tx, expires)
478
+ rt.output ── output (own tx)
479
+ │
480
+ HarnessEvent ── change stream ──> SSE on any instance
481
+ └─ replay after Last-Event-ID
482
+ ```
483
+
484
+ ### Why a log, not a socket
485
+
486
+ The process that runs a turn and the process holding a client's connection are rarely
487
+ the same: runners are leased, HTTP is load-balanced, and both restart. Writing events to
488
+ Mongo lets any instance serve any stream. A client that drops reconnects with
489
+ `Last-Event-ID` (browsers resend it automatically), the instance it lands on replays what
490
+ it missed from the log, then tails new events through a change stream. Replica sets are
491
+ already required, so change streams cost nothing extra. Each `HarnessApp` keeps one
492
+ change stream and fans it out to its viewers in memory, so a hundred viewers cost one
493
+ pooled connection, not a hundred.
494
+
495
+ ### Committed events share the commit
496
+
497
+ `message.created`, `task.status`, `turn.*`, `tool.*`, and `approval.*` are written in the
498
+ same transaction as the change they describe, like the audit span. A client therefore
499
+ never sees a message that was rolled back, and never misses one that committed. Each
500
+ stream hands out `seq` from a counter document inside that transaction; two writers to one
501
+ stream serialize on the counter, so `seq` order is commit order. That is what makes
502
+ `Last-Event-ID` safe: a reader that has seen `seq` 41 has seen everything before it.
503
+
504
+ ### Deltas are a preview; the message is the record
505
+
506
+ Tokens arrive far faster than commits should happen, so streamed text is buffered and
507
+ written as one `delta` roughly every 250 ms, outside the checkpoint transaction, with a
508
+ TTL. Deltas exist so a reconnecting client (on any instance) can catch up on a reply that
509
+ is still being written. They are not the transcript: when the request completes, the
510
+ assistant message commits with the checkpoint and supersedes them. If the process dies
511
+ mid-stream, the deltas of that attempt stay until they expire, no message is stored, and
512
+ recovery asks the model again under a new `requestKey`, so a client discards the old
513
+ preview.
514
+
515
+ ### One stream per conversation, one per task tree
516
+
517
+ Conversation events (transcript, deltas, turns, tool calls) go to the conversation's
518
+ stream. Task events (status, `rt.output`, approvals) go to the stream of the tree's root
519
+ task, tagged with the task's path. Watching a task narrows that stream to the task and its
520
+ descendants, so one `Last-Event-ID` resumes a whole subtree. Each event is written once.
521
+
522
+ ### Sending while a turn runs
523
+
524
+ Messages arrive while the agent is still working. `submit` takes `whenBusy`: `queue` keeps
525
+ the message for its own turn after the current one; `steer` adds it to the current turn.
526
+ A steering message waits on the conversation until the turn's next model request picks it
527
+ up, and that request's commit stores it in the transcript. Storing it then, not on
528
+ arrival, keeps the transcript in the order the model saw it (tool results directly after
529
+ their calls). A steer that arrives after the last request started becomes the next turn.
530
+
531
+ API: [Event stream (SSE)](../reference/ai-harness.md#event-stream-sse), [send, queue, and steer](../reference/ai-harness.md#send-queue-and-steer).
@@ -16,16 +16,18 @@ that file (and its imports) to a working example. Gaps stay explicit.
16
16
  | Admin panel (embedded) | yes — Admin plugin registration in `example-backend/src/server.ts` | yes — `example-frontend/app/admin/` | Expo routes wrap `@terreno/admin-frontend` screens. |
17
17
  | Admin SPA | yes — `AdminSpaServeApp` in `example-backend/src/server.ts` | n/a | Standalone SPA is served from the backend; not a frontend route. |
18
18
  | AI streaming chat | yes — `example-backend/src/api/ai.ts` (`addGptRoutes`) | yes — `example-frontend/app/(tabs)/ai.tsx` (`GPTChat`, optional `mascot`) | Empty-chat mascot is consumer-owned; the example fox is not a framework default. |
19
+ | UI blocks | yes — `uiBlocks` with `repair: true` and `exportDataset` in `example-backend/src/ai/hostActions.ts`, wired from `example-backend/src/api/ai.ts`; `todoStats` in `example-backend/src/ai/tools.ts` stores a `ref` dataset | yes — `example-frontend/app/(tabs)/ai.tsx` (`uiBlocks`, dataset and action hooks, `hostActions` includes `exportDataset`) | e2e mocks a document with one inline and one `ref` chart. Steps: [agent-ui-blocks.md](../how-to/agent-ui-blocks.md). |
20
+ | Agent asks | yes — `asks: {approvals}` in `example-backend/src/api/ai.ts`, with the per-request `deleteCompletedTodos` approval tool from `example-backend/src/api/todoTools.ts`; it also passes `chat` to `addGptHistoryRoutes` for the headless `pendingAsks` and `turn` endpoints; keyless scripted agent with short compact-surface replies in `example-backend/src/api/demoAgent.ts` | yes — `example-frontend/app/(tabs)/ai.tsx` (`onAskSubmit`, `askErrors`), helpers in `example-frontend/lib/gptAsks.ts` | Without `GEMINI_API_KEY`, "Help me pick a plan" asks a `choice` in chat, "Pick toppings" asks a `select: "many"` `choice` with an Other field, "Send the weekly report" and "Archive old chats" ask a `confirm` (the second `destructive`), "Draft an announcement" asks a `markdown` edit of a draft, "Fill in the invoice details" asks a `form` with eight typed fields, "Upload a receipt" asks for `files`, sent as uploads when `GCS_BUCKET` is set and as data URLs otherwise, and "Delete my completed todos" calls `deleteCompletedTodos`, which runs only after the user approves the server's destructive approval card. e2e: `example-frontend/e2e/ai-chat.spec.ts`. Small clients: the curl walkthrough in the how-to and the demo's watch-sized `SimpleAskCard` story. Steps: [agent-ui-asks.md](../how-to/agent-ui-asks.md). |
19
21
  | AI structured output | no | no | `@terreno/ai` supports structured generation; the example only streams chat. Recorded gap — not claimed by a current tutorial. |
20
22
  | Feature flags + live updates | yes — `FeatureFlagsApp` in `example-backend/src/server.ts`, seed `example-backend/src/scripts/seed-feature-flags.ts` | yes — `useTerrenoFeatureFlags` in `example-frontend/app/_layout.tsx` | Socket-backed live updates share the realtime connection. |
21
23
  | Websockets / realtime | yes — `RealtimeApp` in `example-backend/src/server.ts`, `example-backend/src/websockets.ts` | yes — `useSocketConnection` in `example-frontend/app/_layout.tsx` | Sync deltas also use the socket (`SyncApp` + `@terreno/syncdb`). |
22
24
  | Consent forms | yes — `example-backend/src/consentDefinitions.ts`, seed scripts | yes — `example-frontend/app/(tabs)/consents.tsx`, admin `example-frontend/app/admin/consent-forms/` | First-login flow is covered by e2e (`example-frontend/e2e/consents.spec.ts`). |
23
25
  | Product announcements | yes — `AnnouncementsApp` in `example-backend/src/server.ts`, release upload token via `ANNOUNCEMENTS_UPLOAD_TOKEN`, seed `example-backend/src/scripts/seed-announcements.ts`, sample release pack `example-backend/announcements/releases/1.14.0/` with `bun run announcements:pack` | yes — `AnnouncementNavigator` in `example-frontend/app/_layout.tsx` and admin announcement routes | Release packs support multiple Markdown surfaces and idempotent draft or explicit live API import. |
24
- | File upload / GCS | yes — `example-backend/src/api/settings.ts`, AI file routes in `example-backend/src/api/ai.ts` | yes — `example-frontend/app/(tabs)/files.tsx`, `example-frontend/app/gcs-settings.tsx` | |
26
+ | File upload / GCS | yes — `example-backend/src/api/settings.ts`, AI file routes in `example-backend/src/api/ai.ts`, `file-uploads` flag in `example-backend/src/api/fileUploads.ts` | yes — `example-frontend/app/(tabs)/files.tsx`, `example-frontend/app/(tabs)/ai.tsx`, `example-frontend/app/gcs-settings.tsx` | The `file-uploads` feature flag hides upload controls and rejects uploads when off. A missing flag leaves uploads enabled. |
25
27
  | Better Auth | yes — `example-backend/src/utils/betterAuthConfig.ts`, `BetterAuthApp` in `example-backend/src/server.ts` | yes — `example-frontend/app/login.tsx`, `example-frontend/lib/betterAuth.ts` | Default `AUTH_PROVIDER` is `better-auth`. |
26
28
  | syncdb local-first | yes — `SyncApp` in `example-backend/src/server.ts` | yes — `example-frontend/store/syncdb.ts`, `example-frontend/app/(tabs)/index.tsx` | PR #869 closed without merge; `@terreno/syncdb` shipped on the launch line and is the example todos path. |
27
29
  | SyncDB diagnostics | n/a | yes — `debug` registration in `example-frontend/store/syncdb.ts`, debugger UI at `example-frontend/app/syncdb-debug.tsx` | `terreno-mcp-local` can inspect, snapshot/compare/merge, mutate, flush, reconcile/resync, and exercise offline/conflict paths against the running development client. |
28
- | RBAC | yes — `example-backend/src/rbacRoles.ts`, `access.ts`, `access: {resource: "todo"}` on the todo router | yes — `example-frontend/app/admin/roles.tsx` | Shipped. The IP snapshot that marked RBAC "not shipped" is stale. Plan: [rbac-permissions.md](../implementationPlans/rbac-permissions.md). |
30
+ | RBAC | yes — `example-backend/src/rbacRoles.ts` (including optional `aiObservabilityViewer` / `aiObservabilityOperator` roles), `access.ts`, `accessControl: access` on `ObservabilityApp` / `AdminApp`, `access: {resource: "todo"}` on the todo router | yes — `example-frontend/app/admin/roles.tsx` | Shipped. Plan: [rbac-permissions.md](../implementationPlans/rbac-permissions.md). |
29
31
  | Organizations | yes — `createAccess({organizations: true})`, `organizationSettingsSchema`, `orgScopedPlugin` on projects | yes — admin org directory/settings at `example-frontend/app/admin/orgs/` | Settings live on `Organization.settings`; example registers a nested timezone schema. |
30
32
  | Background jobs | no | no | No generic job queue example. Admin maintenance scripts (`example-backend/src/adminScripts.ts`, script runner widget) are a related but different surface. Plan: [admin-script-runner.md](../implementationPlans/admin-script-runner.md). |
31
33
 
@@ -119,7 +119,7 @@ include:
119
119
  | --- | --- |
120
120
  | Backend/API/data | `terreno-backend-api`, `mongoose-schema-safety`, `backend-test-env`, `generate-sdk`, `terreno-data-fetching` |
121
121
  | UI/app | `terreno-ui`, `building-terreno-apps`, `verify-ui-changes` |
122
- | AI/prompts | `ai-prompt-governance` |
122
+ | AI/prompts | `ai-prompt-governance` — new features also follow [Develop an AI feature](../how-to/ai-feature-development.md) |
123
123
  | Docs/submission | `update-docs`, Brew, `fix-conflicts` |
124
124
  | GitHub issues | `create-github-issue`, `work-github-issues`, `implement-ready-for-dev` (Pick plan comment is the Roast contract; unattended Grow may also write an IP) |
125
125
  | Deployment/runtime | `deploy-gcp`, Expo deployment/workflow skills |
@@ -639,6 +639,26 @@ docs are the proof; comms admin stats consume these primitives later.
639
639
 
640
640
  ---
641
641
 
642
+ ## dashboard-chart-parity
643
+
644
+ **Title:** `Ops dashboard chart parity`
645
+
646
+ **Labels:** `area:ui`, `type:feature`
647
+ **Project fields:** Area=`ui`, Target=`Next`, Impact=`Feature`, IP=`dashboard-chart-parity`, Status=`Planned`
648
+
649
+ Follow-up to charts-and-dashboards: scorecards with comparison sparklines, `ChartCard`
650
+ period badges, additive multi-series facades, donut hole labels, spanning `DashboardGrid`
651
+ items, and DataTable period headers / totals / range status so apps can compose a
652
+ “How’s it going?”-class ops dashboard. Terreno theme defaults; no Looker preset. Grammar
653
+ not required.
654
+
655
+ - **Implementation plan:** [dashboard-chart-parity.md](https://github.com/TerrenoLabs/terreno/blob/master/docs/implementationPlans/dashboard-chart-parity.md)
656
+ - **Tasks:** [dashboard-chart-parity.md](https://github.com/TerrenoLabs/terreno/blob/master/docs/tasks/dashboard-chart-parity.md)
657
+ - **RTK flag:** None
658
+ - **Depends on:** charts-and-dashboards
659
+
660
+ ---
661
+
642
662
  ## dark-mode-theme
643
663
 
644
664
  **Title:** `First-class dark mode`
@@ -1140,7 +1160,7 @@ Delete the **legacy RTK cache-patching realtime path** in Terreno 58. Collection
1140
1160
  **Title:** `AI observability (Langfuse-light, pluggable)`
1141
1161
 
1142
1162
  **Labels:** `area:ai`, `type:feature`
1143
- **Project fields:** Area=`ai`, Target=`Future`, Impact=`Feature`, IP=`ai-observability`, Status=`Shaping`
1163
+ **Project fields:** Area=`ai`, Target=`Future`, Impact=`Feature`, IP=`ai-observability`, Status=`Planned`
1144
1164
 
1145
1165
  Ship **Langfuse-like** prompt versioning, nested traces (user / session / cost), multidimensional evaluators (LLM-as-judge + structured I/O), datasets, experiments, and a **local human review queue** — **inside Terreno**, as **plugins on `@terreno/ai`**, with an operator UI in **`admin-frontend` only**.
1146
1166
 
@@ -1271,6 +1291,34 @@ Turn the Expo demo into the workshop for `@terreno/ui`: a person can find a comp
1271
1291
 
1272
1292
  ---
1273
1293
 
1294
+ ## agent-ui-asks
1295
+
1296
+ **Title:** `Agent UI Asks — agents ask the user for a typed answer inside the chat`
1297
+
1298
+ **Labels:** `area:ai`, `type:feature`
1299
+ **Project fields:** Area=`ai`, Target=`Next`, Impact=`Feature`, IP=`agent-ui-asks`, Status=`In progress`
1300
+
1301
+ Let an agent **ask the user something and get a typed answer back in the same turn**. The agent calls an ask tool: pick one, pick many, upload images or files, edit a markdown draft, confirm an action, or fill a short form. `GPTChat` renders the matching `@terreno/ui` control inline. The user answers. The answer returns to the agent as a validated tool result and the turn continues from where it paused.
1302
+
1303
+ - **Implementation plan:** [agent-ui-asks.md](https://github.com/TerrenoLabs/terreno/blob/master/docs/implementationPlans/agent-ui-asks.md)
1304
+ - **Tasks:** [agent-ui-asks.md](https://github.com/TerrenoLabs/terreno/blob/master/docs/tasks/agent-ui-asks.md)
1305
+
1306
+ ---
1307
+
1308
+ ## durable-agent-harness
1309
+
1310
+ **Title:** `Durable agent harness (@terreno/ai/harness)`
1311
+
1312
+ **Labels:** `area:ai`, `type:feature`
1313
+ **Project fields:** Area=`ai`, Target=`Next`, Impact=`Feature`, IP=`durable-agent-harness`, Status=`Planned`
1314
+
1315
+ One easy-to-use harness inside `@terreno/ai` that runs long-lived agentic work durably: deterministic multi-phase workflows, LLM agent loops, tools, subagents, human approvals, retries, and crash recovery — all audited. It must be strong enough to build an OpenClaw/Hermes-class personal agent, a coding agent, and a regulated clinical automation on the same primitives.
1316
+
1317
+ - **Implementation plan:** [durable-agent-harness.md](https://github.com/TerrenoLabs/terreno/blob/master/docs/implementationPlans/durable-agent-harness.md)
1318
+ - **Tasks:** [durable-agent-harness.md](https://github.com/TerrenoLabs/terreno/blob/master/docs/tasks/durable-agent-harness.md)
1319
+
1320
+ ---
1321
+
1274
1322
  # Shipped, umbrella, and declined IPs
1275
1323
 
1276
1324
  Tracking issues created for IPs that previously lacked a `**Roadmap issue:**` header.
@@ -8,7 +8,7 @@ TypeScript 7 removes.
8
8
 
9
9
  | Profile | Packages | `module` | `target` | Emits |
10
10
  | --- | --- | --- | --- | --- |
11
- | Node library | `api`, `ai`, `admin-backend`, `announcements`, `api-health`, `comms`, `feature-flags`, `jobs`, `test`, `admin-spa` server (`tsconfig.server.json`) | `nodenext` | `es2023` | CommonJS `dist/` |
11
+ | Node library | `api`, `ai`, `admin-backend`, `announcements`, `api-health`, `blocks`, `comms`, `feature-flags`, `jobs`, `test`, `admin-spa` server (`tsconfig.server.json`) | `nodenext` | `es2023` | CommonJS `dist/` |
12
12
  | Node ESM tool | `mcp-server`, `create-terreno-app` | `nodenext` | `es2023` | ESM `dist/` (`"type": "module"`) |
13
13
  | Frontend library | `ui`, `admin-frontend`, `rtk`, `syncdb` | `esnext` + `moduleResolution: bundler` | `es2022` | ESM `dist/` for Metro |
14
14
  | Bun CLI | `cli` | `esnext` + `moduleResolution: bundler` + `customConditions: ["bun"]` | `es2022` | ESM `dist/` for Bun |