@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,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 bar is on demo and
4
- dev component routes. `?embed=1` applies the same query and hides the bar.
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 bar 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`.
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