@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.
- package/dist/docs/upgrades/57.10.1.md +54 -0
- package/dist/docs/upgrades/57.11.0.md +80 -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 +544 -0
- package/dist/docs/versioned/next/explanation/example-coverage.md +3 -1
- package/dist/docs/versioned/next/explanation/local-first-data.md +1 -0
- 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 +7 -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/call-external-apis.md +1 -1
- 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/migrate-rtk-to-syncdb.md +31 -1
- package/dist/docs/versioned/next/how-to/observe-llm-calls.md +197 -0
- package/dist/docs/versioned/next/how-to/preview-demo.md +7 -3
- package/dist/docs/versioned/next/how-to/run-harness-on-multiple-instances.md +93 -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 +1974 -0
- package/dist/docs/versioned/next/reference/ai.md +251 -25
- package/dist/docs/versioned/next/reference/api.md +110 -2
- package/dist/docs/versioned/next/reference/blocks.md +178 -0
- package/dist/docs/versioned/next/reference/environment-variables.md +2 -1
- package/dist/docs/versioned/next/reference/lifecycle-plugin.md +11 -0
- package/dist/docs/versioned/next/reference/mcp-server.md +17 -0
- package/dist/docs/versioned/next/reference/syncdb.md +156 -6
- package/dist/docs/versioned/next/reference/test.md +11 -2
- package/dist/docs/versioned/next/reference/ui.md +331 -6
- 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 +185 -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/upgrades/57.11.0.md +80 -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 +544 -0
- package/src/docs/versioned/next/explanation/example-coverage.md +3 -1
- package/src/docs/versioned/next/explanation/local-first-data.md +1 -0
- 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 +7 -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/call-external-apis.md +1 -1
- 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/migrate-rtk-to-syncdb.md +31 -1
- package/src/docs/versioned/next/how-to/observe-llm-calls.md +197 -0
- package/src/docs/versioned/next/how-to/preview-demo.md +7 -3
- package/src/docs/versioned/next/how-to/run-harness-on-multiple-instances.md +93 -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 +1974 -0
- package/src/docs/versioned/next/reference/ai.md +251 -25
- package/src/docs/versioned/next/reference/api.md +110 -2
- package/src/docs/versioned/next/reference/blocks.md +178 -0
- package/src/docs/versioned/next/reference/environment-variables.md +2 -1
- package/src/docs/versioned/next/reference/lifecycle-plugin.md +11 -0
- package/src/docs/versioned/next/reference/mcp-server.md +17 -0
- package/src/docs/versioned/next/reference/syncdb.md +156 -6
- package/src/docs/versioned/next/reference/test.md +11 -2
- package/src/docs/versioned/next/reference/ui.md +331 -6
- 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 +185 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Observe LLM calls
|
|
2
|
+
|
|
3
|
+
Register observability on the backend. Registration **is** the feature flag. Then run product iteration in [Develop an AI feature](ai-feature-development.md).
|
|
4
|
+
|
|
5
|
+
## Register
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import {access} from "./access";
|
|
9
|
+
|
|
10
|
+
const priceMap = parseObservabilityPriceMap(process.env.AI_OBS_PRICE_MAP_JSON);
|
|
11
|
+
|
|
12
|
+
server.register(new ObservabilityApp({
|
|
13
|
+
accessControl: access,
|
|
14
|
+
aiService,
|
|
15
|
+
plugins: [localPlugin, langfuseAdapter, otelSink].filter(Boolean),
|
|
16
|
+
control: {
|
|
17
|
+
datasets: process.env.AI_OBS_DATASETS_PRIMARY ?? "local",
|
|
18
|
+
experiments: process.env.AI_OBS_EXPERIMENTS_PRIMARY ?? "local",
|
|
19
|
+
prompts: process.env.AI_OBS_PROMPTS_PRIMARY ?? "local",
|
|
20
|
+
reviewQueue: "local",
|
|
21
|
+
},
|
|
22
|
+
priceMap,
|
|
23
|
+
sampleRate: Number(process.env.AI_OBS_SAMPLE_RATE ?? 0),
|
|
24
|
+
}));
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For an admin playground that accepts a user-supplied provider key, add
|
|
28
|
+
`requestAiServiceFactory: ({apiKey, modelId}) => ...`. The playground route passes
|
|
29
|
+
`x-ai-api-key` to that factory only when the server-wide `aiService` is absent. Never persist or
|
|
30
|
+
log the key. `GET /ai/observability/status` exposes `playgroundAi.source` so admin UIs can block
|
|
31
|
+
**Run once** with a host-specific hint (`request-key`) instead of surfacing the backend **503**
|
|
32
|
+
missing-key title as a server misconfiguration.
|
|
33
|
+
|
|
34
|
+
Validate the parsed object before registration: each model needs non-negative numeric
|
|
35
|
+
`inputPerMTok` and `outputPerMTok`. The example implementation is
|
|
36
|
+
`example-backend/src/utils/observabilityConfig.ts`; malformed values fail startup with the
|
|
37
|
+
variable name.
|
|
38
|
+
|
|
39
|
+
Keep existing `LangfuseApp` if you already use Langfuse keys. The Langfuse **adapter** must reuse `getLangfuseClient()` and must not start a second OpenTelemetry SDK.
|
|
40
|
+
|
|
41
|
+
Boot **fails** if `experiments.primary !== datasets.primary`, if `reviewQueue` is `langfuse`, or if a primary’s plugin is missing.
|
|
42
|
+
|
|
43
|
+
Pass the same `access` object you use for `TerrenoApp` and `AdminApp` so
|
|
44
|
+
`GET /ai/observability/status` permissions match `/admin/config` screen filtering and every
|
|
45
|
+
observability route enforces RBAC.
|
|
46
|
+
|
|
47
|
+
### Example consumer roles
|
|
48
|
+
|
|
49
|
+
The example backend seeds two optional roles in `example-backend/src/rbacRoles.ts`. They are
|
|
50
|
+
**not** assigned automatically — attach them in the admin **Roles** UI or on `User.roles`.
|
|
51
|
+
|
|
52
|
+
| Role | Grants |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `aiObservabilityViewer` | `admin:access` plus `list` and `read` on all six observability resources |
|
|
55
|
+
| `aiObservabilityOperator` | `admin:access` plus every approved action on those resources |
|
|
56
|
+
|
|
57
|
+
Compose your own roles the same way: grant `admin: ["access"]` for the admin shell, then add
|
|
58
|
+
only the `aiPrompt` / `aiTrace` / `aiReview` / `aiDataset` / `aiEvaluator` / `aiExperiment`
|
|
59
|
+
actions each persona needs. Prompt `folder` and `description` stay consumer-defined metadata —
|
|
60
|
+
Terreno does not ship domain-specific folder names or an in-app approval workflow.
|
|
61
|
+
|
|
62
|
+
## Env
|
|
63
|
+
|
|
64
|
+
| Variable | Role |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| `AI_OBS_PROMPTS_PRIMARY` | `local` or `langfuse` (default `local`) |
|
|
67
|
+
| `AI_OBS_DATASETS_PRIMARY` | default `local` |
|
|
68
|
+
| `AI_OBS_EXPERIMENTS_PRIMARY` | must equal datasets |
|
|
69
|
+
| `AI_OBS_SAMPLE_RATE` | `0`–`1`, default `0` (live evals off) |
|
|
70
|
+
| `AI_OBS_PRICE_MAP_JSON` | `{ "gemini-2.5-flash": { "inputPerMTok": 0.1, "outputPerMTok": 0.4 } }` |
|
|
71
|
+
| `LANGFUSE_PUBLIC_KEY` / `LANGFUSE_SECRET_KEY` | existing Langfuse plugin |
|
|
72
|
+
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | optional OTLP trace sink |
|
|
73
|
+
|
|
74
|
+
## App generate calls
|
|
75
|
+
|
|
76
|
+
Pass identity and the production prompt ref so traces feed the SOP loop:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
await aiService.generateText({
|
|
80
|
+
prompt: text,
|
|
81
|
+
promptLabel: "production",
|
|
82
|
+
promptName: "example-summarize",
|
|
83
|
+
sessionId: req.header("x-ai-session-id"),
|
|
84
|
+
userId: req.user?.id,
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Opt out of tracing with `skipTrace: true`. Sink failures are logged and never fail the generate. `AIRequest` is still written.
|
|
89
|
+
|
|
90
|
+
## Admin
|
|
91
|
+
|
|
92
|
+
Phase 1 admin ships **Prompts**, **Traces**, and **Review**. Datasets, Evaluators, and
|
|
93
|
+
Experiments screens arrive in phase 2. **Open in Langfuse** appears in phase 3 when the
|
|
94
|
+
Langfuse plugin is registered. Review is hidden when the local plugin is off.
|
|
95
|
+
|
|
96
|
+
On **Traces**, **Run multi-stage trace test** (local sink only) runs two schema-backed
|
|
97
|
+
`generateJsonObject` calls, a deterministic tool stage, and a final schema-backed combine
|
|
98
|
+
call, then opens the nested trace. Each LLM span input includes `outputSchema`. It uses the
|
|
99
|
+
server `aiService` when one is configured; otherwise it sends the admin's saved key as
|
|
100
|
+
`x-ai-api-key` to `requestAiServiceFactory`, and the button stays disabled with a hint until
|
|
101
|
+
that key exists.
|
|
102
|
+
|
|
103
|
+
## Run the example locally
|
|
104
|
+
|
|
105
|
+
1. Set `MONGO_URI` to a replica set and the auth secrets from
|
|
106
|
+
`example-backend/.env.example`.
|
|
107
|
+
2. Run `bun run backend:seed`. The idempotent seed creates `examples/example-summarize`
|
|
108
|
+
v1 with `production` pointing to v1 and candidate v2, plus the human
|
|
109
|
+
`correctness-human` evaluator, automatic `schema-assert` evaluator, and
|
|
110
|
+
two-item `example-gold` dataset. It also creates `chat-safety-synthetic` (12 proofread
|
|
111
|
+
two-person chats), `chat-safety-screen` v1 and v2, and the `chat-safety-agreement`
|
|
112
|
+
evaluator. On a database that already booted, run Admin → Scripts →
|
|
113
|
+
`seedChatSafetyDataset` as a wet run, or
|
|
114
|
+
`cd example-backend && bun run script seedChatSafetyDataset --wet`.
|
|
115
|
+
3. Start `bun run backend:dev` and `bun run frontend:web`, then sign in as the seeded
|
|
116
|
+
admin.
|
|
117
|
+
4. Open **AI Observability → Prompts** to inspect or save a new immutable version. Move
|
|
118
|
+
`production` explicitly; the seed never overwrites an existing label.
|
|
119
|
+
5. Configure `GOOGLE_VERTEX_PROJECT` or `GEMINI_API_KEY`, then sign in with Better Auth
|
|
120
|
+
and call the seeded route:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
API=http://localhost:4000
|
|
124
|
+
COOKIE_JAR="$(mktemp)"
|
|
125
|
+
curl -sS -c "$COOKIE_JAR" \
|
|
126
|
+
-H "Content-Type: application/json" \
|
|
127
|
+
-d '{"email":"admin@example.com","password":"testpassword123"}' \
|
|
128
|
+
"$API/api/auth/sign-in/email" >/dev/null
|
|
129
|
+
|
|
130
|
+
curl -X POST "$API/ai/example-summarize" \
|
|
131
|
+
-b "$COOKIE_JAR" \
|
|
132
|
+
-H "Content-Type: application/json" \
|
|
133
|
+
-H "x-ai-session-id: docs-walkthrough" \
|
|
134
|
+
-d '{"text":"Terreno records local nested traces for every AI call."}'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
6. Open its `example-summarize` trace, send it to Review, score **correct**, and submit
|
|
138
|
+
until the queue reports **Queue clear**.
|
|
139
|
+
|
|
140
|
+
The example app also calls this route from the UI: **Todos → Summarize** sends the user's todo
|
|
141
|
+
list, so each press produces a trace with the production prompt reference, `userId`, and a
|
|
142
|
+
per-app-run `x-ai-session-id`. Without server credentials the screen forwards the Gemini key
|
|
143
|
+
saved in **Profile** as `x-ai-api-key`; otherwise the route answers **503**.
|
|
144
|
+
|
|
145
|
+
The route sends `text` as the user prompt. The labelled registry version supplies the
|
|
146
|
+
system prompt and the route uses `TemperaturePresets.LOW`.
|
|
147
|
+
|
|
148
|
+
The example registers the local plugin even when no Langfuse keys exist. Set
|
|
149
|
+
`AI_OBS_PRICE_MAP_JSON` to calculate USD cost; unlisted models show tokens with no cost,
|
|
150
|
+
never `$0`.
|
|
151
|
+
|
|
152
|
+
## Create a prompt and pin production
|
|
153
|
+
|
|
154
|
+
Admin-only routes live under `/ai/observability`. Create the prompt in a folder, save later edits as `vN+1` (v1 stays immutable), then move `production` — the response names the outgoing version for the confirm modal.
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# 1. Create v1 in a folder
|
|
158
|
+
curl -X POST "$API/ai/observability/prompts" \
|
|
159
|
+
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
|
160
|
+
-H "Content-Type: application/json" \
|
|
161
|
+
-d '{"folder":"examples","name":"example-summarize","system":"Summarize {{text}}","template":"{{text}}","type":"text"}'
|
|
162
|
+
|
|
163
|
+
# 2. Pin production (first pin has no outgoing version)
|
|
164
|
+
curl -X POST "$API/ai/observability/prompts/example-summarize/labels" \
|
|
165
|
+
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
|
166
|
+
-H "Content-Type: application/json" \
|
|
167
|
+
-d '{"label":"production","version":1}'
|
|
168
|
+
|
|
169
|
+
# 3. Save as v2, then move production (response.outgoingVersion is 1)
|
|
170
|
+
curl -X POST "$API/ai/observability/prompts/example-summarize/versions" \
|
|
171
|
+
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
|
172
|
+
-H "Content-Type: application/json" \
|
|
173
|
+
-d '{"system":"Summarize {{text}} in one sentence","template":"{{text}}","type":"text"}'
|
|
174
|
+
curl -X POST "$API/ai/observability/prompts/example-summarize/labels" \
|
|
175
|
+
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
|
176
|
+
-H "Content-Type: application/json" \
|
|
177
|
+
-d '{"label":"production","version":2}'
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`GET /ai/observability/prompts?folder=examples&search=sum&include=usage7d` lists folder matches with 7-day call/cost rollups. A prompt with no `production` label returns `production: "—"`. Playground `POST /ai/observability/prompts/:name/playground` compiles `{{var}}`, runs one `AIService` call, and does not create a version.
|
|
181
|
+
|
|
182
|
+
The example backend registers `requestAiServiceFactory` but no server `aiService`, so status reports
|
|
183
|
+
`playgroundAi.source: "request-key"`. In **AI Observability → Prompts → Playground**, save a Gemini
|
|
184
|
+
API key on **Profile** first; the admin UI blocks **Run once** with that hint until the key loads
|
|
185
|
+
from storage and is sent as `x-ai-api-key`. **Traces → Run multi-stage trace test** uses the same
|
|
186
|
+
saved key and is blocked the same way.
|
|
187
|
+
|
|
188
|
+
## Review a trace
|
|
189
|
+
|
|
190
|
+
1. Use the seeded human `correctness-human` evaluator. If you skipped `bun run backend:seed`,
|
|
191
|
+
install it once with `POST /ai/observability/evaluators/templates/correctness-human`; a
|
|
192
|
+
duplicate install returns 409.
|
|
193
|
+
2. Enqueue traces: `POST /ai/observability/traces/review` with `{evaluatorId, traceIds, reason: "manual"}`.
|
|
194
|
+
3. List oldest-first: `GET /ai/observability/review?status=pending` (response includes per-status counts).
|
|
195
|
+
4. Open an item: `GET /ai/observability/review/:id` returns evaluator dimensions plus `given` / `wrote` panels (variable labels when present, otherwise raw keys).
|
|
196
|
+
5. `POST /ai/observability/review/:id` with `action: "submit"` writes dimension-keyed scores through every ScoreSink and marks the item `done` (it leaves the pending list). `skip` and `assign` move status to `skipped` / `in_progress`.
|
|
197
|
+
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# Preview a component demo
|
|
2
2
|
|
|
3
|
-
Open a component demo and change the shareable query. The preview
|
|
4
|
-
dev component routes
|
|
3
|
+
Open a component demo and change the shareable query. The preview controls sit in the
|
|
4
|
+
navigation header on demo and dev component routes: dropdowns for theme, viewport, and
|
|
5
|
+
background, plus a settings button (sliders icon) that opens a modal for right-to-left,
|
|
6
|
+
reduced motion, locale, and the share query. In windows narrower than 768px every
|
|
7
|
+
preview control moves into that modal; the Dev/Demo mode link stays in the header. `?embed=1` applies the same query and hides
|
|
8
|
+
the controls.
|
|
5
9
|
|
|
6
10
|
```text
|
|
7
11
|
/demo/Button?theme=dark&viewport=375&background=inverse&locale=en-US&rtl=1&reducedMotion=1
|
|
@@ -16,4 +20,4 @@ dev component routes. `?embed=1` applies the same query and hides the bar.
|
|
|
16
20
|
| `rtl` | `1` sets document direction to `rtl` on web |
|
|
17
21
|
| `reducedMotion` | `1` sets `data-reduced-motion="1"` on the document element |
|
|
18
22
|
|
|
19
|
-
Reload keeps the query. The
|
|
23
|
+
Reload keeps the query. The settings modal shows the current query when any value is not the default. Choosing a control's default removes that key from the URL. Leaving the component route restores the app theme and, on web, document direction and reduced motion. Custom palettes stay on `/palette`.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Run the harness on multiple instances
|
|
2
|
+
|
|
3
|
+
Run durable harness tasks on every instance of a scaled-out backend (for example several
|
|
4
|
+
Cloud Run instances). Swap the default `InProcessRunner`, where one process drains every
|
|
5
|
+
task, for `JobsRunner`, which runs each phase as a `@terreno/jobs` job. API:
|
|
6
|
+
[AI harness reference → JobsRunner](../reference/ai-harness.md#jobsrunner). Why it is safe:
|
|
7
|
+
[Durable agent harness → Runners](../explanation/durable-agent-harness.md#runners).
|
|
8
|
+
|
|
9
|
+
You need a working harness (see [Build a durable workflow](build-a-durable-workflow.md)) and
|
|
10
|
+
a MongoDB replica set. Task data does not change, so you can switch runners without a
|
|
11
|
+
migration.
|
|
12
|
+
|
|
13
|
+
## 1. Install the jobs package (1 minute)
|
|
14
|
+
|
|
15
|
+
`@terreno/jobs` is an optional peer of `@terreno/ai`. Add it to the app:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun add @terreno/jobs
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 2. Open the harness with a JobsRunner (5 minutes)
|
|
22
|
+
|
|
23
|
+
Create the `JobsApp` first, hand it to the runner, and register it on the app. Build the
|
|
24
|
+
app before the harness starts: `harness.start()` throws when no `JobsApp` is registered.
|
|
25
|
+
The runner defines the `terreno.harness.phase` job on it.
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import {Harness} from "@terreno/ai/harness";
|
|
29
|
+
import {JobsRunner} from "@terreno/ai/harness/jobsRunner";
|
|
30
|
+
import {TerrenoApp} from "@terreno/api";
|
|
31
|
+
import {JobsApp} from "@terreno/jobs";
|
|
32
|
+
|
|
33
|
+
const jobs = new JobsApp();
|
|
34
|
+
const harness = await Harness.open({
|
|
35
|
+
registry: [intakeSummary, summarizer],
|
|
36
|
+
runner: new JobsRunner({jobs}),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const app = new TerrenoApp({userModel: User}).register(jobs);
|
|
40
|
+
// ...register HarnessApp and your routes, then build or start the server.
|
|
41
|
+
app.start();
|
|
42
|
+
|
|
43
|
+
await harness.start();
|
|
44
|
+
await jobs.startWorker();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Shut down in the reverse order so phases in flight finish:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
await harness.stop(); // stops dispatching, waits for this instance's phases
|
|
51
|
+
await jobs.stopWorker();
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 3. Run the same code on every instance
|
|
55
|
+
|
|
56
|
+
Every instance runs the same startup. Each one:
|
|
57
|
+
|
|
58
|
+
- scans for runnable tasks and enqueues one job per runnable phase (duplicates collapse on
|
|
59
|
+
the job's idempotency key),
|
|
60
|
+
- runs phase jobs from the shared jobs queue, and
|
|
61
|
+
- sweeps task leases that expired because an instance died.
|
|
62
|
+
|
|
63
|
+
No instance is special, and none waits on standby.
|
|
64
|
+
|
|
65
|
+
## 4. Check it works (2 minutes)
|
|
66
|
+
|
|
67
|
+
1. Start two backend processes against the same database (different `PORT`s).
|
|
68
|
+
2. Create a task (for the example backend, run `startClinicalIntake`).
|
|
69
|
+
3. Open the admin **Jobs** screen. Each phase shows as its own `terreno.harness.phase` job
|
|
70
|
+
with payload `{taskId}`.
|
|
71
|
+
4. Kill one process mid-phase. Within one lease (`leaseDuration`, default 30 s) the other
|
|
72
|
+
process replays a `replay: "safe"` phase, or parks any other phase as `interrupted`.
|
|
73
|
+
|
|
74
|
+
## Tune it
|
|
75
|
+
|
|
76
|
+
| Want | Set |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| Faster pickup of new work | `new JobsRunner({jobs, pollInterval: {milliseconds: 200}})` and `new JobsApp({pollIntervalMs: 200})` |
|
|
79
|
+
| Faster crash recovery | A shorter `leaseDuration` with `heartbeatInterval` about a third of it |
|
|
80
|
+
| Phases longer than the jobs lock TTL | Nothing: the task lease, renewed by heartbeats, decides who commits |
|
|
81
|
+
|
|
82
|
+
## Troubleshoot
|
|
83
|
+
|
|
84
|
+
- **Jobs fail with `harness-runner-stopped`.** A jobs worker runs on an instance whose
|
|
85
|
+
harness is not started. Start the harness before `jobs.startWorker()`. The jobs worker
|
|
86
|
+
retries the job, so no task is lost.
|
|
87
|
+
- **`harness.start()` throws "JobsRunner needs a registered JobsApp".** The dispatcher
|
|
88
|
+
enqueues through the registered jobs service, and `TerrenoApp` registers plugins when it
|
|
89
|
+
builds. Call `app.start()` (or `build()`) before `harness.start()`.
|
|
90
|
+
- **A task stays `pending`.** Check that its `name@version` is in the registry of the
|
|
91
|
+
running instances. A runner only dispatches registered versions.
|
|
92
|
+
- **Pick one runner per database.** Mixing them is safe (every claim and commit is fenced),
|
|
93
|
+
but an `InProcessRunner` owner then claims tasks itself and runs them to a stop.
|
|
@@ -48,6 +48,7 @@ bun test --only-failures <path>
|
|
|
48
48
|
| --- | --- |
|
|
49
49
|
| `api-ci` | `bun run api:test` |
|
|
50
50
|
| `ai-ci` | `cd ai && bun run test:ci` |
|
|
51
|
+
| `blocks-ci` | `bun run blocks:test` |
|
|
51
52
|
| `ui-ci` | `bun run ui:test` |
|
|
52
53
|
| `rtk-ci` | `bun run rtk:test` |
|
|
53
54
|
| `syncdb-ci` | `bun run syncdb:test` |
|
|
@@ -81,7 +82,7 @@ MONGO_URI="mongodb://127.0.0.1:27017/terreno-e2e?replicaSet=rs0" \
|
|
|
81
82
|
| `e2e` `auth` | `login` `signup` `consents` `forgot-password` `reset-password` `verify-email` |
|
|
82
83
|
| `e2e` `app` | `todos` `profile` `realtime` `ai-chat` `pdf` |
|
|
83
84
|
| `e2e` `admin-core` | `admin` `admin-home` `admin-form` `admin-todo-crud` `admin-title-update-depth` |
|
|
84
|
-
| `e2e` `admin-table` | `admin-table-search-filter` `admin-table-bulk-actions` `admin-custom-screens` `admin-comms-back` |
|
|
85
|
+
| `e2e` `admin-table` | `admin-table-search-filter` `admin-table-bulk-actions` `admin-custom-screens` `admin-comms-back` `harness-approvals` `harness-intake` |
|
|
85
86
|
| `e2e` `syncdb` | `syncdb-load-delta` `syncdb-offline` `syncdb-conflicts` `syncdb-storage` `syncdb-chaos` |
|
|
86
87
|
|
|
87
88
|
Full local suite: `bun run frontend:e2e`. Nightly load (`e2e-load` / `syncdb-loadlab`) is
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Ship a new task version
|
|
2
|
+
|
|
3
|
+
Change a durable harness task without breaking runs that already started on the old code.
|
|
4
|
+
Rules: [Versioning](../reference/ai-harness.md#versioning). Why:
|
|
5
|
+
[Version pinning](../explanation/durable-agent-harness.md#version-pinning).
|
|
6
|
+
|
|
7
|
+
## When to bump `version`
|
|
8
|
+
|
|
9
|
+
Bump it on any change an in-flight task could notice:
|
|
10
|
+
|
|
11
|
+
- A phase is renamed, removed, or added between existing phases.
|
|
12
|
+
- The shape of `state`, `input`, or the result changes.
|
|
13
|
+
- A phase's side effects or `replay` policy change.
|
|
14
|
+
|
|
15
|
+
A fix that keeps the same phases, state shape, and side effects can ship under the same
|
|
16
|
+
version.
|
|
17
|
+
|
|
18
|
+
## Steps
|
|
19
|
+
|
|
20
|
+
1. Copy the definition and bump `version`. Keep the old one unchanged.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
export const intakeV1 = defineTask<IntakeInput, IntakeStateV1, IntakeResult>({
|
|
24
|
+
name: "clinic.intake",
|
|
25
|
+
version: 1,
|
|
26
|
+
// ...the old phases, untouched
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const intakeV2 = defineTask<IntakeInput, IntakeStateV2, IntakeResult>({
|
|
30
|
+
name: "clinic.intake",
|
|
31
|
+
version: 2,
|
|
32
|
+
// ...the new phases
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
2. Register both versions. Point new work at v2.
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
const harness = await Harness.open({registry: [intakeV1, intakeV2]});
|
|
40
|
+
await harness.start();
|
|
41
|
+
|
|
42
|
+
await harness.createTask(intakeV2, {patientId});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Tasks created on v1 finish on v1's phases and v1's `abort` handler. New tasks run on
|
|
46
|
+
v2. Parent tasks that call `rt.createTask(intakeV1, ...)` keep making v1 children
|
|
47
|
+
until you change that call.
|
|
48
|
+
|
|
49
|
+
3. Deploy. Every process that runs the harness needs the same registry.
|
|
50
|
+
|
|
51
|
+
4. Wait until no v1 task is left in flight. Check in a Mongo shell:
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
db.harnesstasks.countDocuments({
|
|
55
|
+
name: "clinic.intake",
|
|
56
|
+
version: 1,
|
|
57
|
+
status: {$in: ["pending", "running", "waiting", "interrupted"]},
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Resolve stuck `interrupted` v1 tasks with `harness.resolveInterrupted`, or stop them
|
|
62
|
+
with `harness.abort`.
|
|
63
|
+
|
|
64
|
+
5. When the count is 0, remove `intakeV1` from the registry and deploy again.
|
|
65
|
+
|
|
66
|
+
## If you remove a version too early
|
|
67
|
+
|
|
68
|
+
`Harness.start()` throws an `APIError` with code `harness-config-invalid` and claims
|
|
69
|
+
nothing. Its `detail` reads:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
Harness.start: in-flight tasks use task versions this registry does not register: clinic.intake@1 (4 tasks). Register those definitions (keep old versions until their tasks finish) or resolve the tasks first.
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Fix: add the old definition back to `registry` and redeploy. Its tasks resume from their
|
|
76
|
+
last checkpoint.
|
|
@@ -38,7 +38,7 @@ Maintainers watching the next SDK beta use `track-upstream-expo` / [`track-upstr
|
|
|
38
38
|
|
|
39
39
|
### Backend vs frontend packages
|
|
40
40
|
|
|
41
|
-
From `publish-on-tag.yml`: **backend** `api`, `test`, `admin-backend`, `ai`, `api-health`, `comms`, `feature-flags`, `mcp`. **frontend** `ui`, `rtk`, `syncdb`, `admin-frontend`, `admin-spa`.
|
|
41
|
+
From `publish-on-tag.yml`: **backend** `api`, `test`, `admin-backend`, `blocks`, `ai`, `api-health`, `comms`, `feature-flags`, `mcp`. **frontend** `ui`, `rtk`, `syncdb`, `admin-frontend`, `admin-spa`. `blocks` holds contracts that both `ai` and `ui` depend on, so it moves with the backend step.
|
|
42
42
|
|
|
43
43
|
## 4. RTK → syncdb is not a version bump
|
|
44
44
|
|