@terreno/mcp 57.9.0 → 57.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/docs/upgrades/57.10.1.md +54 -0
  2. package/dist/docs/upgrades/57.11.0.md +80 -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/durable-agent-harness.md +544 -0
  10. package/dist/docs/versioned/next/explanation/example-coverage.md +3 -1
  11. package/dist/docs/versioned/next/explanation/local-first-data.md +1 -0
  12. package/dist/docs/versioned/next/explanation/loop-engineering.md +1 -1
  13. package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +29 -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 -0
  17. package/dist/docs/versioned/next/how-to/agent-ui-asks.md +730 -0
  18. package/dist/docs/versioned/next/how-to/agent-ui-blocks.md +104 -0
  19. package/dist/docs/versioned/next/how-to/ai-feature-development.md +156 -0
  20. package/dist/docs/versioned/next/how-to/build-a-durable-workflow.md +232 -0
  21. package/dist/docs/versioned/next/how-to/call-external-apis.md +1 -1
  22. package/dist/docs/versioned/next/how-to/charts-and-dashboards.md +2 -0
  23. package/dist/docs/versioned/next/how-to/circleci.md +5 -3
  24. package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +31 -1
  25. package/dist/docs/versioned/next/how-to/observe-llm-calls.md +197 -0
  26. package/dist/docs/versioned/next/how-to/preview-demo.md +7 -3
  27. package/dist/docs/versioned/next/how-to/run-harness-on-multiple-instances.md +93 -0
  28. package/dist/docs/versioned/next/how-to/run-tests-locally.md +2 -1
  29. package/dist/docs/versioned/next/how-to/ship-a-new-task-version.md +76 -0
  30. package/dist/docs/versioned/next/how-to/upgrade-terreno.md +1 -1
  31. package/dist/docs/versioned/next/implementationPlans/agent-ui-asks.md +688 -0
  32. package/dist/docs/versioned/next/implementationPlans/agent-ui-blocks.md +10 -6
  33. package/dist/docs/versioned/next/implementationPlans/ai-observability.md +475 -157
  34. package/dist/docs/versioned/next/implementationPlans/durable-agent-harness.md +340 -0
  35. package/dist/docs/versioned/next/implementationPlans/terreno-langfuse-integration.md +1 -1
  36. package/dist/docs/versioned/next/reference/README.md +3 -0
  37. package/dist/docs/versioned/next/reference/admin-config.md +2 -0
  38. package/dist/docs/versioned/next/reference/admin-frontend.md +216 -0
  39. package/dist/docs/versioned/next/reference/agent-ui-asks.md +1059 -0
  40. package/dist/docs/versioned/next/reference/ai-harness.md +1974 -0
  41. package/dist/docs/versioned/next/reference/ai.md +251 -25
  42. package/dist/docs/versioned/next/reference/api.md +110 -2
  43. package/dist/docs/versioned/next/reference/blocks.md +178 -0
  44. package/dist/docs/versioned/next/reference/environment-variables.md +2 -1
  45. package/dist/docs/versioned/next/reference/lifecycle-plugin.md +11 -0
  46. package/dist/docs/versioned/next/reference/mcp-server.md +17 -0
  47. package/dist/docs/versioned/next/reference/syncdb.md +156 -6
  48. package/dist/docs/versioned/next/reference/test.md +11 -2
  49. package/dist/docs/versioned/next/reference/ui.md +331 -6
  50. package/dist/docs/versioned/next/tasks/agent-ui-asks.md +213 -0
  51. package/dist/docs/versioned/next/tasks/agent-ui-blocks.md +2 -1
  52. package/dist/docs/versioned/next/tasks/ai-observability.md +233 -104
  53. package/dist/docs/versioned/next/tasks/durable-agent-harness.md +185 -0
  54. package/dist/tools.d.ts +4 -0
  55. package/dist/tools.d.ts.map +1 -1
  56. package/dist/tools.js +37 -0
  57. package/dist/tools.js.map +1 -1
  58. package/package.json +3 -2
  59. package/src/docs/upgrades/57.10.1.md +54 -0
  60. package/src/docs/upgrades/57.11.0.md +80 -0
  61. package/src/docs/versioned/next/README.md +1 -0
  62. package/src/docs/versioned/next/explanation/README.md +4 -0
  63. package/src/docs/versioned/next/explanation/admin-plugin-frontend.md +2 -2
  64. package/src/docs/versioned/next/explanation/agent-ui-asks.md +299 -0
  65. package/src/docs/versioned/next/explanation/agent-ui-blocks.md +67 -0
  66. package/src/docs/versioned/next/explanation/ai-observability.md +101 -0
  67. package/src/docs/versioned/next/explanation/durable-agent-harness.md +544 -0
  68. package/src/docs/versioned/next/explanation/example-coverage.md +3 -1
  69. package/src/docs/versioned/next/explanation/local-first-data.md +1 -0
  70. package/src/docs/versioned/next/explanation/loop-engineering.md +1 -1
  71. package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +29 -1
  72. package/src/docs/versioned/next/explanation/typescript-configuration.md +1 -1
  73. package/src/docs/versioned/next/explanation/versioning-policy.md +1 -1
  74. package/src/docs/versioned/next/how-to/README.md +7 -0
  75. package/src/docs/versioned/next/how-to/agent-ui-asks.md +730 -0
  76. package/src/docs/versioned/next/how-to/agent-ui-blocks.md +104 -0
  77. package/src/docs/versioned/next/how-to/ai-feature-development.md +156 -0
  78. package/src/docs/versioned/next/how-to/build-a-durable-workflow.md +232 -0
  79. package/src/docs/versioned/next/how-to/call-external-apis.md +1 -1
  80. package/src/docs/versioned/next/how-to/charts-and-dashboards.md +2 -0
  81. package/src/docs/versioned/next/how-to/circleci.md +5 -3
  82. package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +31 -1
  83. package/src/docs/versioned/next/how-to/observe-llm-calls.md +197 -0
  84. package/src/docs/versioned/next/how-to/preview-demo.md +7 -3
  85. package/src/docs/versioned/next/how-to/run-harness-on-multiple-instances.md +93 -0
  86. package/src/docs/versioned/next/how-to/run-tests-locally.md +2 -1
  87. package/src/docs/versioned/next/how-to/ship-a-new-task-version.md +76 -0
  88. package/src/docs/versioned/next/how-to/upgrade-terreno.md +1 -1
  89. package/src/docs/versioned/next/implementationPlans/agent-ui-asks.md +688 -0
  90. package/src/docs/versioned/next/implementationPlans/agent-ui-blocks.md +10 -6
  91. package/src/docs/versioned/next/implementationPlans/ai-observability.md +475 -157
  92. package/src/docs/versioned/next/implementationPlans/durable-agent-harness.md +340 -0
  93. package/src/docs/versioned/next/implementationPlans/terreno-langfuse-integration.md +1 -1
  94. package/src/docs/versioned/next/reference/README.md +3 -0
  95. package/src/docs/versioned/next/reference/admin-config.md +2 -0
  96. package/src/docs/versioned/next/reference/admin-frontend.md +216 -0
  97. package/src/docs/versioned/next/reference/agent-ui-asks.md +1059 -0
  98. package/src/docs/versioned/next/reference/ai-harness.md +1974 -0
  99. package/src/docs/versioned/next/reference/ai.md +251 -25
  100. package/src/docs/versioned/next/reference/api.md +110 -2
  101. package/src/docs/versioned/next/reference/blocks.md +178 -0
  102. package/src/docs/versioned/next/reference/environment-variables.md +2 -1
  103. package/src/docs/versioned/next/reference/lifecycle-plugin.md +11 -0
  104. package/src/docs/versioned/next/reference/mcp-server.md +17 -0
  105. package/src/docs/versioned/next/reference/syncdb.md +156 -6
  106. package/src/docs/versioned/next/reference/test.md +11 -2
  107. package/src/docs/versioned/next/reference/ui.md +331 -6
  108. package/src/docs/versioned/next/tasks/agent-ui-asks.md +213 -0
  109. package/src/docs/versioned/next/tasks/agent-ui-blocks.md +2 -1
  110. package/src/docs/versioned/next/tasks/ai-observability.md +233 -104
  111. package/src/docs/versioned/next/tasks/durable-agent-harness.md +185 -0
@@ -0,0 +1,544 @@
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).
51
+
52
+ Phase 2 so far: `InProcessRunner` concurrency (up to 8 tasks at once by
53
+ default) and the `JobsRunner`, which runs each phase as a `@terreno/jobs` job across many
54
+ instances. The Runs admin screen is still to come.
55
+
56
+ ## Why the audit span shares the checkpoint transaction
57
+
58
+ Regulated work (clinical automation is the reference case) needs proof of every step.
59
+ `ObsSpan` from [AI observability](ai-observability.md) **is** the harness audit log, so
60
+ the admin Traces screens show harness runs with no extra UI.
61
+
62
+ A best-effort span written after the checkpoint could go missing on a crash. That would
63
+ leave a committed step with no audit record. So `rt.commit()` writes, in **one** Mongo
64
+ transaction:
65
+
66
+ 1. The `HarnessTask` update: next `phase` and `state`, or the terminal `outcome`.
67
+ 2. One `CHAIN` span for the finished phase, parented to the task's root span.
68
+ 3. On a terminal commit, the closed root span and root `ObsTrace`.
69
+
70
+ Either all three land or none do. This is why `Harness.open` refuses to start without a
71
+ replica set (transactions need one) or without the local observability models. Planned
72
+ Langfuse and OTel exports will stay best-effort, after the transaction.
73
+
74
+ ```
75
+ createTask ──tx──> HarnessTask(pending) + ObsTrace + root CHAIN span
76
+ phase "fetch" ── rt.commit ──tx──> task.phase=summarize + CHAIN span "fetch"
77
+ phase "summarize" ── rt.commit ──tx──> task.status=completed + CHAIN span "summarize"
78
+ + root span / trace closed
79
+ ```
80
+
81
+ ## Leases and crash resume
82
+
83
+ A process can die at any moment, and a frozen process can wake up later and keep going.
84
+ Two leases make both cases safe.
85
+
86
+ | Lease | Question it answers | Granularity |
87
+ | --- | --- | --- |
88
+ | **Owner lease** (`HarnessOwner`) | Which process drains tasks right now? | One per database |
89
+ | **Task lease** (`HarnessTask.lease`) | Which run of this phase may commit? | One per running phase, with a random fencing `token` |
90
+
91
+ Both expire unless their holder renews them on a heartbeat. A second process starts on
92
+ **standby** and becomes owner once the owner lease expires (or at once, when the owner
93
+ stops cleanly).
94
+
95
+ The owner lease is about efficiency: one drainer, no claim storms. The **task token** is
96
+ about correctness. Every commit is fenced on the token, so a runner that lost its lease
97
+ (it froze past expiry and someone else took over) is rejected with
98
+ `HarnessCommitConflictError` and writes nothing, even if two processes briefly both think
99
+ they own execution.
100
+
101
+ ```
102
+ process A: claim (token t1) ── phase runs ── A freezes ......... A wakes: commit(t1) ✗ rejected
103
+ process B: standby ........ A's leases expire ── takeover ── recover: phase replay
104
+ claim (token t2) ── commit(t2) ✓
105
+ ```
106
+
107
+ ### Replay is a per-phase promise
108
+
109
+ When a new owner finds a `running` task whose lease expired, it cannot know how far the
110
+ phase got. The phase's `replay` says what is safe:
111
+
112
+ - `replay: "safe"`: the phase can run again from its checkpoint (reads, idempotent writes).
113
+ The task goes back to `pending` and runs again.
114
+ - default `replay: "never"`: the phase might have done something that must not happen
115
+ twice (an EHR write, a payment). The task becomes `interrupted` and stops.
116
+
117
+ The harness never guesses. An operator looks at the external system and calls
118
+ `resolveInterrupted` with `retry` (run the phase again), `abort` (give up), or `complete`
119
+ (it already happened; record the result), plus a reason. That decision is audited like
120
+ any other step.
121
+
122
+ Every interruption also writes an error `CHAIN` span in the same transaction as the status
123
+ change, so the trace shows exactly which phase was cut off and what happened next.
124
+
125
+ ## Retries: a thrown phase is not a crash
126
+
127
+ A crash and a throw look different to the harness. A crash (expired lease) means "we do
128
+ not know how far the phase got", so `replay` decides. A throw means the phase code itself
129
+ said "this attempt failed", so the task's `retry` policy decides: back to `pending` at the
130
+ same checkpoint, with `runAt` pushed out by exponential backoff and jitter, until
131
+ `maxAttempts` runs are used up and the task ends `failed`.
132
+
133
+ Jitter matters because failures cluster. When an EHR goes down, every task that called it
134
+ fails within the same second. Without jitter they would all retry at the same instant and
135
+ knock it over again. Each retry waits a random delay between half and all of its backoff.
136
+
137
+ A retry re-runs the whole phase, so a phase that throws after a side effect repeats it.
138
+ Throw before the side effect, or make it idempotent. Programming errors (committing to an
139
+ unknown phase, forgetting to commit) fail at once: retrying cannot fix them.
140
+
141
+ ### Errors are APIErrors with stable codes
142
+
143
+ Harness errors reach three audiences: the code that called the harness, an HTTP client of
144
+ `HarnessApp`, and an operator reading a failed task or a Sentry issue. All three are served
145
+ by one shape, Terreno's `APIError`. The `code` (`harness-task-terminal`,
146
+ `harness-definition-invalid`, ...) and `title` never change, so callers branch on `code` and
147
+ Sentry groups occurrences together. What changed this time (which task, which key) lives in
148
+ `detail`. The same error thrown from `harness.abort` therefore becomes a 409 over HTTP with
149
+ no route code translating it.
150
+
151
+ The retry rule above rides on the class, not the code. A `HarnessDefinitionError` fails the
152
+ task at once. Any other error, including an `APIError` with the same code from a non-phase
153
+ call, is an ordinary failed attempt. A failed task records the error's `detail`. Each
154
+ harness detail is a full sentence, so the task's `error` reads as a complete message on
155
+ its own.
156
+
157
+ ## The ownership tree
158
+
159
+ Tasks form a tree. A phase calls `rt.createTask` to start a child. The child records
160
+ `ownership: {kind: "task", id: parent}` and shares the parent's `rootTaskId` and trace, so
161
+ its spans nest under the parent's span and the whole tree reads as one trace.
162
+
163
+ ```
164
+ root task (trace)
165
+ ├── child A ── grandchild A1
166
+ └── child B
167
+ ```
168
+
169
+ ### Waiting on children without holding a lease
170
+
171
+ A parent that needs its children's results calls `rt.waitForTasks(ids)`. The harness
172
+ does not keep the parent's phase suspended in memory. That would hold a lease for as long
173
+ as the children take, and a crash would lose the in-memory continuation anyway. Instead
174
+ the parent commits `waiting`, gives up its lease, and its phase stops. When the children
175
+ settle, the parent goes back to `pending` and its phase **runs again from its
176
+ checkpoint**. This time `waitForTasks` finds the children settled and returns their
177
+ outcomes at once.
178
+
179
+ That re-run is why `rt.createTask` is idempotent within one attempt of a phase visit: the
180
+ second run gets the children the first run created, not duplicates. A retry is a fresh
181
+ attempt, so it starts fresh children. It also means work before the
182
+ wait runs twice, so keep it to reads and child creation, and do side effects in a later
183
+ phase. Events and sleeps use the same "commit `waiting`, re-run on wake" shape.
184
+
185
+ ### Waiting for the outside world: events and sleeps
186
+
187
+ `rt.waitFor(event, {timeout})` pauses a task until something outside it happens: a
188
+ webhook, an operator decision, a lab result. `rt.sleep(duration)` pauses it for a fixed
189
+ time. Neither holds a lease while it waits.
190
+
191
+ `harness.sendEvent(taskId, event, payload)` writes the event to the task's **inbox**
192
+ before it does anything else. So the sender never has to know where the task is:
193
+
194
+ - If the task is already waiting on that event, the same transaction sends it back to
195
+ `pending`.
196
+ - If it has not reached the wait yet, the event stays buffered until it does.
197
+ - If no runner is up, the event is still stored, and the task resumes once one starts.
198
+
199
+ A timeout or a sleep is a `timeoutAt` on the wait. The runner claims a waiting task whose
200
+ `timeoutAt` has passed the same way it claims a retry whose `runAt` has passed. There is
201
+ no timer to lose in a crash.
202
+
203
+ When the phase re-runs, each wait call must return the same thing it returned before. The
204
+ harness records how each call resolved on the task, keyed by the phase visit and the
205
+ call's position, together with the event it received. A later run of that phase visit (a
206
+ retry, a crash replay, the wake for the next wait) reads the record and does not take
207
+ another event. A timeout is recorded too, so an event that arrives after the timeout can
208
+ never turn an earlier `undefined` into a payload.
209
+
210
+ `policy: "failFast"` resolves at the first failed or aborted child and aborts the
211
+ siblings still in flight. Use it when one failure makes the rest pointless (for example,
212
+ the allergy check failed, so the dosing calculation should stop).
213
+
214
+ ### Abort runs bottom-up
215
+
216
+ `harness.abort(taskId)` stops a task and everything it owns. Order matters for
217
+ compensation: a parent's `abort` handler often undoes what it set up for its children, so
218
+ it must run after they have stopped and undone their own work. The harness:
219
+
220
+ 1. Fences the whole subtree, top-down. Aborting tasks are never claimed again, and a
221
+ running phase loses its lease token, so nothing it does afterwards can commit. Its
222
+ `rt.signal` aborts so it can stop early.
223
+ 2. Walks the tree **deepest first**. For each task it runs the `abort` handler, then
224
+ commits `aborted` with an audit span. A turn's handler marks an assistant message
225
+ `aborted` when that message still has a tool call with no stored result, so the next
226
+ model request skips it.
227
+
228
+ Concurrent aborts of one task (an operator and a `failFast` sibling abort, say) run its
229
+ handler once. A failing handler does not stop the abort. A half-aborted tree would be worse than a
230
+ missed compensation, and the span records the failure for a human to follow up.
231
+
232
+ `background: true` marks a child that belongs to its owner but not to the owner's current
233
+ conversation turn. Conversations (a later slice) use it so that aborting a turn leaves
234
+ background work running.
235
+
236
+ ## Runners
237
+
238
+ The engine stores everything in Mongo. A **runner** only decides who executes runnable
239
+ tasks. Switching runners needs no data migration.
240
+
241
+ | Runner | Status | Ownership |
242
+ | --- | --- | --- |
243
+ | `InProcessRunner` | Shipped | One process holds the `HarnessOwner` lease and runs up to `concurrency` (default 8) claimed tasks at once, each under its own task lease; others wait on standby and take over on lease expiry. |
244
+ | `JobsRunner` | Shipped | Every instance dispatches runnable tasks as `terreno.harness.phase` jobs; a job claims its task, runs one phase, and hands the task back. The task lease is the authority. |
245
+
246
+ Why the jobs runner runs **one** phase per job: a phase is the unit that commits, so it is
247
+ the natural unit of work to spread across instances and to bound by a request or job
248
+ timeout. Why the task lease and not the job lock decides who commits: a jobs lock can
249
+ expire while a long phase still runs, and a queue can deliver a job twice. Both are
250
+ harmless here, because the claim is an atomic fenced update and every commit checks the
251
+ lease token. Why the job key carries `claims`: `taskId:phase:attempt` alone repeats when a
252
+ task returns to the same phase without a failure (an event wake or a crash replay), and a
253
+ permanent idempotency key would then swallow the new visit. `claims` changes on every
254
+ claim, so each visit gets one job.
255
+
256
+ ## Version pinning
257
+
258
+ Each task records `name` and `version`. A runner only claims tasks whose exact
259
+ `name@version` is in its registry. A process that registers only `v2` leaves `v1` rows
260
+ alone. Keep old versions registered until their in-flight tasks finish. `Harness.start()`
261
+ refuses to start while any non-terminal task uses a version the registry lacks, so a
262
+ deploy that drops a version too early fails loudly instead of stranding work. See
263
+ [Ship a new task version](../how-to/ship-a-new-task-version.md).
264
+
265
+ ## The agent loop
266
+
267
+ An agent turn is not special machinery. It is an ordinary registered task,
268
+ `terreno.agent.turn@1`, owned by its conversation, so everything above (leases, replay,
269
+ retries, the ownership tree, abort, version pinning) applies to it unchanged.
270
+
271
+ ```
272
+ submit ──tx──> user message + conversation busy + turn task (pending)
273
+
274
+ turn: request ──tx──> assistant message + LLM span ──┐ tool calls?
275
+ ▲ ▼
276
+ └──tx── tool messages <── waitForTasks <── tools: one child task per call
277
+ (each call = TOOL span)
278
+ request with no tool calls ──tx──> completed; conversation idle
279
+ ```
280
+
281
+ ### Two phases, both safe to replay
282
+
283
+ - **`request`** calls the model. A model call has no side effect, so if the process dies
284
+ mid-call the phase simply runs again. Nothing from the cut-off call reached the
285
+ transcript: the assistant message is only stored in the same transaction as the
286
+ checkpoint. (Its streamed text did reach the event log as expiring deltas; see
287
+ [Watching runs live](#watching-runs-live).)
288
+ - **`tools`** starts one child task per tool call and waits for all of them. Children
289
+ are found again by key on a re-run, so a replay never doubles a call.
290
+
291
+ The side effects live in the tools, and each tool says whether it may run twice.
292
+
293
+ ### Interrupted tools are reported, not hidden
294
+
295
+ A tool with the default `replay: "never"` might have done its work before the crash (a
296
+ note written, a message sent). The harness cannot know. Parking the call for an operator
297
+ would freeze the whole conversation, so instead the call ends with the result
298
+ "Interrupted, not retried" and the loop goes on. The model sees that result and can check
299
+ or ask the user. A tool marked `replay: "safe"` (a read, an idempotent write) just runs
300
+ again.
301
+
302
+ ### Retries belong to the model call
303
+
304
+ Providers fail in two ways. Overload and outages (429, 5xx, dropped connections) usually
305
+ pass, so the `request` phase retries them with backoff and jitter, then tries each
306
+ fallback model. Anything else (a 400 for a bad prompt, a 401 for a bad key) will fail the
307
+ same way every time, so the turn fails at once instead of burning retries and fallbacks.
308
+ Every attempt is listed on the `LLM` span, so the audit shows which model answered and
309
+ what failed before it.
310
+
311
+ ### Why the transcript shares the checkpoint transaction
312
+
313
+ The same rule as phase spans: a message written outside the commit could survive a crash
314
+ whose checkpoint did not, and the next request would replay a half-finished step. Each
315
+ commit writes the messages, the `LLM` span, and the checkpoint together, and each message
316
+ takes its `seq` from a counter on the conversation inside that transaction. The
317
+ transcript is therefore always exactly what the committed phases produced.
318
+
319
+ API: [Agents and conversations](../reference/ai-harness.md#agents-and-conversations).
320
+
321
+
322
+ ## Subagents
323
+
324
+ A workflow often needs one focused model job inside a larger, deterministic flow:
325
+ summarize this chart, classify that note. `rt.runAgent` runs that job as a **subagent**
326
+ and returns its answer to the phase, as text or as a schema-checked object.
327
+
328
+ A subagent is built from parts that already exist, not new machinery:
329
+
330
+ ```
331
+ caller phase ──tx──> subagent conversation (owned by the caller)
332
+ + user message
333
+ + turn task (child of the caller; AGENT span)
334
+ ──> waitForTasks([turn]) ── caller waits, holds no lease
335
+
336
+ turn: request / tools ... ──tx──> completed ── wakes the caller
337
+
338
+ caller phase re-runs ──> same call finds the same turn ──> returns its answer
339
+ ```
340
+
341
+ - **The conversation is owned by the task.** It is a normal transcript, so the
342
+ subagent's prompt, answer, and tool results are kept and auditable like any chat.
343
+ - **The turn is a child task.** Waiting, waking, crash recovery, and abort all come from
344
+ the ownership tree. Aborting the caller aborts the subagent's turn first.
345
+ - **The call is found again, not repeated.** The phase re-runs from its checkpoint after
346
+ the wait. Each `runAgent` call has a key from its position in the phase, so the re-run
347
+ finds the conversation and turn it created and gets the stored result. The
348
+ conversation, its first message, and the turn are created in one transaction, so a
349
+ crash can never leave one without the others.
350
+
351
+ ### Why the caller checks the structured output
352
+
353
+ The schema is a zod object in the caller's code, and code cannot be stored. The turn
354
+ gets a JSON Schema copy: enough to ask the provider for JSON and to reject an answer that
355
+ is not JSON at all. Rules JSON Schema cannot carry (refinements, transforms) are checked
356
+ when the caller gets the result, with the real schema. An invalid answer fails the call,
357
+ and the caller's retry policy decides whether to ask again.
358
+
359
+ ### Why only phases call subagents
360
+
361
+ A phase that waits re-runs from its checkpoint, and phases are written for that. A tool's
362
+ `execute` is not: re-running it from the top would repeat whatever it did before the
363
+ call. Tools therefore cannot call `runAgent`. A phase can run several subagents, one
364
+ after another; for parallel fan-out, start one child task per subagent and wait on all
365
+ of them.
366
+
367
+ API: [Subagents (rt.runAgent)](../reference/ai-harness.md#subagents-rtrunagent).
368
+
369
+ ## Extensions, hooks, and memos
370
+
371
+ Apps need to shape an agent without forking it: add a policy paragraph to the prompt,
372
+ refuse a dangerous tool call, redact a result, log every chart lookup. An **extension**
373
+ bundles those changes under a name, and agents and conversations list the extensions
374
+ they use. Extensions are code in the registry; a conversation stores only their names.
375
+
376
+ ```
377
+ request phase tool call task (child of the turn)
378
+ instructions + sections ─┐ args ──> beforeTool ──┬─ {block} ──> error result
379
+ beforeModelRequest ──────┤ └─ {args} ──> execute (wrapped)
380
+ model call │ │
381
+ commit: [system prompt │ afterTool
382
+ if changed] + │ │
383
+ assistant + LLM │ commit: tool result + TOOL span <────┘
384
+ ```
385
+
386
+ ### Hooks fail the step they run in
387
+
388
+ A hook is part of the step that runs it, so it fails like that step's own code. A
389
+ throwing `beforeModelRequest` hook or section fails the turn, as a model that never
390
+ answers would. A throwing tool hook fails the tool call, and the model gets the error
391
+ like any other tool failure and decides what to do. Nothing is retried: a hook that
392
+ failed once usually fails again, and a retry would call the model or the tool twice.
393
+
394
+ ### Blocking is a tool result, not an exception
395
+
396
+ `beforeTool` returning `{block: reason}` does not stop the turn. The tool never runs and
397
+ the model receives `reason` as an error result. The model can then explain, ask for
398
+ something else, or stop. This keeps the transcript honest (every tool call has a result)
399
+ and lets policy code refuse without knowing how the turn continues.
400
+
401
+ ### Sections are rebuilt, and recorded, every request
402
+
403
+ Sections are functions, so the prompt can change between requests: fresh vitals, a
404
+ policy that depends on the time of day. That makes "what did the model see?" a real
405
+ question for audit and resume. The turn answers it in the transcript: whenever the
406
+ effective system prompt differs from the last one recorded, the request's commit appends
407
+ a `system` message holding the exact text, and the `LLM` span points to it by hash and
408
+ seq. A prompt that does not change is recorded once, so long turns do not copy it into
409
+ every span.
410
+
411
+ ### Later extensions win, and wraps decorate the winner
412
+
413
+ Tool names are resolved in a fixed order: the agent's tools, then each extension's, with
414
+ a later tool of the same name replacing an earlier one. Wraps apply after that, to the
415
+ tool that won. An audit wrap therefore keeps working when another extension swaps in a
416
+ different implementation of the tool it watches.
417
+
418
+ ### Memos make a decision once
419
+
420
+ Phases and tool calls may run more than once (replay, retry, a second process after a
421
+ crash). A decision that must not change, such as "this call was approved" or "route to
422
+ the cardiology queue", goes in `rt.memo(key, value)`. The first write wins, atomically,
423
+ across concurrent calls and processes; every later call gets the stored value back. A
424
+ memo write commits on its own, fenced by the run's lease, so a crash right after the
425
+ decision still keeps it. Hook memos are scoped to the turn task: the turn outlives its
426
+ request re-runs and its tool calls, so their decisions are found again.
427
+
428
+ API: [Extensions](../reference/ai-harness.md#extensions), [Hooks](../reference/ai-harness.md#hooks), [Memos](../reference/ai-harness.md#memos).
429
+
430
+ ## Human approvals
431
+
432
+ Regulated work needs a person to sign off before the irreversible step: a clinician
433
+ approves the note before it is written to the EHR. The harness treats an approval as a
434
+ wait, not a new mechanism. `rt.approval` parks the task on an event that only a decision
435
+ sends, and writes the `HarnessApproval` row in the same commit that parks it. Everything
436
+ waits already guarantee carries over: the task holds no lease while a person thinks, a
437
+ decision made while every runner is down is stored and delivered on restart, and a re-run
438
+ of the phase finds the same approval instead of asking again.
439
+
440
+ ```
441
+ phase: rt.approval ──commit──> task waiting + HarnessApproval(pending)
442
+ │
443
+ inbox (approver) ── approve/reject ──> one transaction: approval decided
444
+ + inbox event (task -> pending)
445
+ + approval:<key> span (decidedBy)
446
+ │
447
+ phase re-runs ──> rt.approval returns the decision
448
+ ```
449
+
450
+ The approver's side is an inbox in `@terreno/admin-frontend`, not a bespoke screen per app.
451
+ `HarnessApp` contributes it to the admin sidebar, and the same component mounts on a
452
+ clinician-facing screen. It lists only what the viewer may decide, so the inbox and the
453
+ approve route answer the same permission question. See
454
+ [the inbox reference](../reference/admin-frontend.md#ai-harness-approvals-inbox).
455
+
456
+ ### Approvers are code, looked up by name
457
+
458
+ Who may approve is a list of permission functions in the same shape as modelRouter
459
+ permissions, so an app reuses its existing checks (`IsAdmin`, "is a clinician at this
460
+ clinic"). Functions cannot be stored, and the process answering the approver's HTTP
461
+ request did not run the phase, so the policy is declared on the task definition (or the
462
+ extension, for tool gates) and found again by `name@version` and key. A process that does
463
+ not register that version denies everyone rather than guessing.
464
+
465
+ ### The decision and its audit are one write
466
+
467
+ The approver's decision, the event that wakes the task, and the span that records who
468
+ decided commit together. There is no window in which the task proceeds without an audit
469
+ record, or an audit record exists for a decision the task never saw. Expiry follows the
470
+ same rule from the other side: it is written with the wait's timeout, only while the
471
+ approval is still pending, so a late decision and an expiry can never both apply.
472
+
473
+ ### Gating tools
474
+
475
+ `approvalGate` puts the same wait inside a `beforeTool` hook. The tool call's task waits;
476
+ the turn waits on the tool call. The decision is memoized by tool call id in the turn, so
477
+ a replayed call does not ask twice, and a rejection reaches the model as the tool's error
478
+ result, like any other block.
479
+
480
+ 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).
481
+
482
+ ## Watching runs live
483
+
484
+ People watch agents work: a clinician sees the summary being written, an operator follows
485
+ a long task tree. Clients watch over Server-Sent Events, and every SSE stream is served
486
+ from one durable log, `HarnessEvent`, rather than from the process running the work.
487
+
488
+ ```
489
+ commit tx ── message / status / approval change ──+── event (same tx, next seq)
490
+ model stream ── text ── coalesce ~250 ms ── delta (own tx, expires)
491
+ rt.output ── output (own tx)
492
+ │
493
+ HarnessEvent ── change stream ──> SSE on any instance
494
+ └─ replay after Last-Event-ID
495
+ ```
496
+
497
+ ### Why a log, not a socket
498
+
499
+ The process that runs a turn and the process holding a client's connection are rarely
500
+ the same: runners are leased, HTTP is load-balanced, and both restart. Writing events to
501
+ Mongo lets any instance serve any stream. A client that drops reconnects with
502
+ `Last-Event-ID` (browsers resend it automatically), the instance it lands on replays what
503
+ it missed from the log, then tails new events through a change stream. Replica sets are
504
+ already required, so change streams cost nothing extra. Each `HarnessApp` keeps one
505
+ change stream and fans it out to its viewers in memory, so a hundred viewers cost one
506
+ pooled connection, not a hundred.
507
+
508
+ ### Committed events share the commit
509
+
510
+ `message.created`, `task.status`, `turn.*`, `tool.*`, and `approval.*` are written in the
511
+ same transaction as the change they describe, like the audit span. A client therefore
512
+ never sees a message that was rolled back, and never misses one that committed. Each
513
+ stream hands out `seq` from a counter document inside that transaction; two writers to one
514
+ stream serialize on the counter, so `seq` order is commit order. That is what makes
515
+ `Last-Event-ID` safe: a reader that has seen `seq` 41 has seen everything before it.
516
+
517
+ ### Deltas are a preview; the message is the record
518
+
519
+ Tokens arrive far faster than commits should happen, so streamed text is buffered and
520
+ written as one `delta` roughly every 250 ms, outside the checkpoint transaction, with a
521
+ TTL. Deltas exist so a reconnecting client (on any instance) can catch up on a reply that
522
+ is still being written. They are not the transcript: when the request completes, the
523
+ assistant message commits with the checkpoint and supersedes them. If the process dies
524
+ mid-stream, the deltas of that attempt stay until they expire, no message is stored, and
525
+ recovery asks the model again under a new `requestKey`, so a client discards the old
526
+ preview.
527
+
528
+ ### One stream per conversation, one per task tree
529
+
530
+ Conversation events (transcript, deltas, turns, tool calls) go to the conversation's
531
+ stream. Task events (status, `rt.output`, approvals) go to the stream of the tree's root
532
+ task, tagged with the task's path. Watching a task narrows that stream to the task and its
533
+ descendants, so one `Last-Event-ID` resumes a whole subtree. Each event is written once.
534
+
535
+ ### Sending while a turn runs
536
+
537
+ Messages arrive while the agent is still working. `submit` takes `whenBusy`: `queue` keeps
538
+ the message for its own turn after the current one; `steer` adds it to the current turn.
539
+ A steering message waits on the conversation until the turn's next model request picks it
540
+ up, and that request's commit stores it in the transcript. Storing it then, not on
541
+ arrival, keeps the transcript in the order the model saw it (tool results directly after
542
+ their calls). A steer that arrives after the last request started becomes the next turn.
543
+
544
+ API: [Event stream (SSE)](../reference/ai-harness.md#event-stream-sse), [send, queue, and steer](../reference/ai-harness.md#send-queue-and-steer).
@@ -16,6 +16,8 @@ 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`). |
@@ -25,7 +27,7 @@ that file (and its imports) to a working example. Gaps stay explicit.
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
 
@@ -39,6 +39,7 @@ Terreno 57.
39
39
  3. **Encryption at rest (web)** — Web persistence uses AES-GCM via the default `createServerKeyProvider`; understand key fetch and `onDecryptFailure` for support flows.
40
40
  4. **Key lifecycle on logout** — Call `syncDb.stop()` and consider `wipeLocalData` when the authenticated user changes so one account does not read another's local store.
41
41
  5. **Backend sync registration** — Every synced collection needs `syncPlugin`, `isDeletedPlugin`, and a `sync` block on `modelRouter`, plus `SyncApp` and `RealtimeApp` on the server. Optional `sync.adminBroadcast: true` also emits `sync:delta` to `{collection}|admin`. Admin clients subscribe with `mode: "window"` (`createSyncDb({windowCollections})`) so they do not snapshot or reconcile that collection; they call `hydrateWindow` (REST membership ids + `GET /sync/entities`). Pass the same `accessControl` into `SyncApp` that `AdminApp` uses so `{collection}|admin` requires `admin:access`. `AdminApp.register` also installs that model's admin list/read/`queryFilter` on the admin window so hydrate and live `|admin` deltas match `/admin` REST. Without RBAC, `user.admin` is enough for the shell gate. Live deltas on that stream apply only to ids already in the window; Refresh is a REST re-query plus `hydrateWindow`.
42
+ 6. **Collections too large to sync whole** — List them in `createSyncDb({queryCollections})`. They skip snapshot bootstrap; screens load server-filtered *query windows* (`useWindowQuery`) from the `modelRouter` list endpoint. A window stores only membership (ordered ids); rows live once in the shared entity table. Overlapping windows therefore share rows — one delta updates both views — while each view lists only its own query's members, plus cached rows that newly match its `where` inside the loaded range. Live deltas still arrive on the user's normal streams; windows on screen refetch on reconnect to cover deltas missed offline.
42
43
 
43
44
  ## Tradeoffs
44
45
 
@@ -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 |
@@ -1160,7 +1160,7 @@ Delete the **legacy RTK cache-patching realtime path** in Terreno 58. Collection
1160
1160
  **Title:** `AI observability (Langfuse-light, pluggable)`
1161
1161
 
1162
1162
  **Labels:** `area:ai`, `type:feature`
1163
- **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`
1164
1164
 
1165
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**.
1166
1166
 
@@ -1291,6 +1291,34 @@ Turn the Expo demo into the workshop for `@terreno/ui`: a person can find a comp
1291
1291
 
1292
1292
  ---
1293
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
+
1294
1322
  # Shipped, umbrella, and declined IPs
1295
1323
 
1296
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 |