@terreno/mcp 57.9.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.
- package/dist/docs/upgrades/57.10.1.md +54 -0
- package/dist/docs/versioned/next/README.md +1 -0
- package/dist/docs/versioned/next/explanation/README.md +4 -0
- package/dist/docs/versioned/next/explanation/admin-plugin-frontend.md +2 -2
- package/dist/docs/versioned/next/explanation/agent-ui-asks.md +299 -0
- package/dist/docs/versioned/next/explanation/agent-ui-blocks.md +67 -0
- package/dist/docs/versioned/next/explanation/ai-observability.md +101 -0
- package/dist/docs/versioned/next/explanation/durable-agent-harness.md +531 -0
- package/dist/docs/versioned/next/explanation/example-coverage.md +3 -1
- package/dist/docs/versioned/next/explanation/loop-engineering.md +1 -1
- package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +29 -1
- package/dist/docs/versioned/next/explanation/typescript-configuration.md +1 -1
- package/dist/docs/versioned/next/explanation/versioning-policy.md +1 -1
- package/dist/docs/versioned/next/how-to/README.md +6 -0
- package/dist/docs/versioned/next/how-to/agent-ui-asks.md +730 -0
- package/dist/docs/versioned/next/how-to/agent-ui-blocks.md +104 -0
- package/dist/docs/versioned/next/how-to/ai-feature-development.md +156 -0
- package/dist/docs/versioned/next/how-to/build-a-durable-workflow.md +232 -0
- package/dist/docs/versioned/next/how-to/charts-and-dashboards.md +2 -0
- package/dist/docs/versioned/next/how-to/circleci.md +5 -3
- package/dist/docs/versioned/next/how-to/observe-llm-calls.md +197 -0
- package/dist/docs/versioned/next/how-to/run-tests-locally.md +2 -1
- package/dist/docs/versioned/next/how-to/ship-a-new-task-version.md +76 -0
- package/dist/docs/versioned/next/how-to/upgrade-terreno.md +1 -1
- package/dist/docs/versioned/next/implementationPlans/agent-ui-asks.md +688 -0
- package/dist/docs/versioned/next/implementationPlans/agent-ui-blocks.md +10 -6
- package/dist/docs/versioned/next/implementationPlans/ai-observability.md +475 -157
- package/dist/docs/versioned/next/implementationPlans/durable-agent-harness.md +340 -0
- package/dist/docs/versioned/next/implementationPlans/terreno-langfuse-integration.md +1 -1
- package/dist/docs/versioned/next/reference/README.md +3 -0
- package/dist/docs/versioned/next/reference/admin-config.md +2 -0
- package/dist/docs/versioned/next/reference/admin-frontend.md +216 -0
- package/dist/docs/versioned/next/reference/agent-ui-asks.md +1059 -0
- package/dist/docs/versioned/next/reference/ai-harness.md +1881 -0
- package/dist/docs/versioned/next/reference/ai.md +251 -25
- package/dist/docs/versioned/next/reference/api.md +46 -0
- package/dist/docs/versioned/next/reference/blocks.md +178 -0
- package/dist/docs/versioned/next/reference/environment-variables.md +1 -0
- package/dist/docs/versioned/next/reference/mcp-server.md +17 -0
- package/dist/docs/versioned/next/reference/test.md +11 -2
- package/dist/docs/versioned/next/reference/ui.md +325 -1
- package/dist/docs/versioned/next/tasks/agent-ui-asks.md +213 -0
- package/dist/docs/versioned/next/tasks/agent-ui-blocks.md +2 -1
- package/dist/docs/versioned/next/tasks/ai-observability.md +233 -104
- package/dist/docs/versioned/next/tasks/durable-agent-harness.md +183 -0
- package/dist/tools.d.ts +4 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +37 -0
- package/dist/tools.js.map +1 -1
- package/package.json +3 -2
- package/src/docs/upgrades/57.10.1.md +54 -0
- package/src/docs/versioned/next/README.md +1 -0
- package/src/docs/versioned/next/explanation/README.md +4 -0
- package/src/docs/versioned/next/explanation/admin-plugin-frontend.md +2 -2
- package/src/docs/versioned/next/explanation/agent-ui-asks.md +299 -0
- package/src/docs/versioned/next/explanation/agent-ui-blocks.md +67 -0
- package/src/docs/versioned/next/explanation/ai-observability.md +101 -0
- package/src/docs/versioned/next/explanation/durable-agent-harness.md +531 -0
- package/src/docs/versioned/next/explanation/example-coverage.md +3 -1
- package/src/docs/versioned/next/explanation/loop-engineering.md +1 -1
- package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +29 -1
- package/src/docs/versioned/next/explanation/typescript-configuration.md +1 -1
- package/src/docs/versioned/next/explanation/versioning-policy.md +1 -1
- package/src/docs/versioned/next/how-to/README.md +6 -0
- package/src/docs/versioned/next/how-to/agent-ui-asks.md +730 -0
- package/src/docs/versioned/next/how-to/agent-ui-blocks.md +104 -0
- package/src/docs/versioned/next/how-to/ai-feature-development.md +156 -0
- package/src/docs/versioned/next/how-to/build-a-durable-workflow.md +232 -0
- package/src/docs/versioned/next/how-to/charts-and-dashboards.md +2 -0
- package/src/docs/versioned/next/how-to/circleci.md +5 -3
- package/src/docs/versioned/next/how-to/observe-llm-calls.md +197 -0
- package/src/docs/versioned/next/how-to/run-tests-locally.md +2 -1
- package/src/docs/versioned/next/how-to/ship-a-new-task-version.md +76 -0
- package/src/docs/versioned/next/how-to/upgrade-terreno.md +1 -1
- package/src/docs/versioned/next/implementationPlans/agent-ui-asks.md +688 -0
- package/src/docs/versioned/next/implementationPlans/agent-ui-blocks.md +10 -6
- package/src/docs/versioned/next/implementationPlans/ai-observability.md +475 -157
- package/src/docs/versioned/next/implementationPlans/durable-agent-harness.md +340 -0
- package/src/docs/versioned/next/implementationPlans/terreno-langfuse-integration.md +1 -1
- package/src/docs/versioned/next/reference/README.md +3 -0
- package/src/docs/versioned/next/reference/admin-config.md +2 -0
- package/src/docs/versioned/next/reference/admin-frontend.md +216 -0
- package/src/docs/versioned/next/reference/agent-ui-asks.md +1059 -0
- package/src/docs/versioned/next/reference/ai-harness.md +1881 -0
- package/src/docs/versioned/next/reference/ai.md +251 -25
- package/src/docs/versioned/next/reference/api.md +46 -0
- package/src/docs/versioned/next/reference/blocks.md +178 -0
- package/src/docs/versioned/next/reference/environment-variables.md +1 -0
- package/src/docs/versioned/next/reference/mcp-server.md +17 -0
- package/src/docs/versioned/next/reference/test.md +11 -2
- package/src/docs/versioned/next/reference/ui.md +325 -1
- package/src/docs/versioned/next/tasks/agent-ui-asks.md +213 -0
- package/src/docs/versioned/next/tasks/agent-ui-blocks.md +2 -1
- package/src/docs/versioned/next/tasks/ai-observability.md +233 -104
- 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,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
|
|
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 |
|
|
@@ -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=`
|
|
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 |
|
|
@@ -4,7 +4,7 @@ All published `@terreno/*` packages share one version. Upgrade them together. Mi
|
|
|
4
4
|
|
|
5
5
|
## Lockstep
|
|
6
6
|
|
|
7
|
-
A git tag `X.Y.Z` (no `v` prefix) is the version for every package that `.github/workflows/publish-on-tag.yml` publishes: `api`, `test`, `ui`, `rtk`, `admin-backend`, `admin-frontend`, `admin-spa`, `ai`, `api-health`, `comms`, `feature-flags`, `mcp`, `syncdb`. Publish jobs read that tag; they do not pick per-package versions.
|
|
7
|
+
A git tag `X.Y.Z` (no `v` prefix) is the version for every package that `.github/workflows/publish-on-tag.yml` publishes: `api`, `test`, `ui`, `rtk`, `admin-backend`, `admin-frontend`, `admin-spa`, `blocks`, `ai`, `api-health`, `comms`, `feature-flags`, `mcp`, `syncdb`. Publish jobs read that tag; they do not pick per-package versions.
|
|
8
8
|
|
|
9
9
|
Inside the monorepo, packages depend on each other with `workspace:*` (for example `@terreno/rtk` → `@terreno/ui`). On npm they are released at the same number. `@terreno/ui` also pins Expo-line peers (`react-native` `~0.86.0` on the 57 line). Installing `@terreno/ui@57` next to Expo 54 is outside support, same as mixing `@terreno/api@57` with `@terreno/ui@0.31`.
|
|
10
10
|
|
|
@@ -34,6 +34,10 @@ Problem-oriented, practical steps. Use these when you know what you want to do.
|
|
|
34
34
|
- [Debug with MCP](debug-with-mcp.md) — Diagnose backend, browser, Metro, app, and client-state failures
|
|
35
35
|
- [Connect an MCP client with a service token](connect-mcp-service-token.md) — Mint a personal `mcp_` key for Perplexity and other remote clients
|
|
36
36
|
- [Install agent skills](install-agent-skills.md) — `npx skills`, the Cursor plugin, the Codex plugin, or the Claude Code plugin
|
|
37
|
+
- [Develop an AI feature](ai-feature-development.md) — Dataset → prompt → evaluators → experiment → production label → live traces (SOP)
|
|
38
|
+
- [Observe LLM calls](observe-llm-calls.md) — Register ObservabilityApp, primaries, price map, sampleRate
|
|
39
|
+
- [Build a durable workflow](build-a-durable-workflow.md) — Phased harness task with an agent, a human sign-off, and a once-only side effect (`clinic.intakeSummary`)
|
|
40
|
+
- [Ship a new task version](ship-a-new-task-version.md) — Change a durable harness task without stranding in-flight runs
|
|
37
41
|
- [GitHub issue lifecycle](github-issue-lifecycle.md) — Create pick-ready issues, Pick ⇄ Roast, or unattended `status:ready-for-dev` pickup
|
|
38
42
|
- [Call external APIs](call-external-apis.md) — Authenticated HTTP client, retries, and error normalization for third-party integrations
|
|
39
43
|
- [Upgrade banner](upgrade-banner.md) — Soft warning and hard-block app update UX
|
|
@@ -46,6 +50,8 @@ Problem-oriented, practical steps. Use these when you know what you want to do.
|
|
|
46
50
|
- [In-app notifications](in-app-notifications.md) — Register `NotificationsApp`, syncdb inbox, and UI bell
|
|
47
51
|
- [Add organizations](add-organizations.md) — Opt into Membership-backed tenancy, RBAC, admin, and seeded examples
|
|
48
52
|
- [Add a GPT chat mascot](add-gpt-chat-mascot.md) — Optional consumer-owned character on empty `GPTChat`
|
|
53
|
+
- [Add agent asks to a chat](agent-ui-asks.md) — Turn on `asks`, show and answer them in `GPTChat`, try the keyless demo agent, and answer asks from an Apple Watch or another small client
|
|
54
|
+
- [Validate a block document locally](agent-ui-blocks.md) — `terreno-blocks validate` for a whole-reply YAML document
|
|
49
55
|
|
|
50
56
|
## Admin
|
|
51
57
|
|