@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
|
@@ -15,6 +15,8 @@ AI service layer for Terreno backends: provider-agnostic chat via the Vercel AI
|
|
|
15
15
|
- [Route registrars](#route-registrars)
|
|
16
16
|
- [AiApp plugin](#aiapp-plugin)
|
|
17
17
|
- [LangfuseApp plugin](#langfuseapp-plugin)
|
|
18
|
+
- [Observability](#observability)
|
|
19
|
+
- [Durable agent harness](#durable-agent-harness)
|
|
18
20
|
- [Langfuse integration](#langfuse-integration)
|
|
19
21
|
- [FileStorageService](#filestorageservice)
|
|
20
22
|
- [MCPService](#mcpservice)
|
|
@@ -64,13 +66,15 @@ src/
|
|
|
64
66
|
project.ts # GPT project + memories
|
|
65
67
|
routes/
|
|
66
68
|
gpt.ts # Streaming chat, remix, tools, ratings
|
|
67
|
-
gptHistories.ts # History CRUD
|
|
69
|
+
gptHistories.ts # History CRUD, headless pendingAsks and turn actions
|
|
68
70
|
aiRequestsExplorer.ts # Admin request explorer
|
|
69
71
|
files.ts # File upload/signed URL/delete
|
|
70
72
|
projects.ts # Project CRUD + memories
|
|
71
73
|
mcp.ts # MCP server status and tools
|
|
72
74
|
service/
|
|
73
75
|
aiService.ts # Provider-agnostic AI service
|
|
76
|
+
asks.ts # Ask tools, reserved ask_ names, paused-turn replay
|
|
77
|
+
chatTurn.ts # Chat turn runner behind /gpt/prompt (SSE sink) and turn (buffered JSON)
|
|
74
78
|
fileStorage.ts # GCS upload helper
|
|
75
79
|
getMCPTools.ts # modelRouter MCP tools as Vercel AI SDK tools
|
|
76
80
|
mcpService.ts # MCP client connections
|
|
@@ -92,8 +96,10 @@ src/
|
|
|
92
96
|
- **Structured output:** `parseAiJson`, `normalizeLlmJsonTextForStructuredOutput`, re-exported `Output`, `jsonSchema`, `JSONValue`, `FlexibleSchema` from `ai`
|
|
93
97
|
- **Langfuse:** `initLangfuseClient`, `getLangfuseClient`, `shutdownLangfuseClient`, `compilePrompt`, `createPrompt`, `getPrompt`, `createTelemetryConfig`, `preparePromptForAI`, `initTracing`, `shutdownTracing`, `LangfuseCache`, cache helpers
|
|
94
98
|
- **Gemini / Vertex:** `listGeminiApiModels`, `normalizeGeminiModelId`, `GEMINI_API_BASE_URL`, `createVertexProvider`, `listEnabledVertexModels`, `verifyVertexModelsEnabled`, `assertVertexModelsEnabled`, `isVertexModelAllowed`, `normalizeVertexModelId`, `DEFAULT_VERTEX_LOCATION`
|
|
95
|
-
- **Prompts:** `CONTENT_SUMMARY_PROMPT`, `DEFAULT_GPT_MEMORY`, `JSON_VALUE_SYSTEM_PROMPT`, `REMIX_PROMPT`, `TITLE_GENERATION_PROMPT`, `TRANSLATION_PROMPT`
|
|
99
|
+
- **Prompts:** `COMPACT_SURFACE_SYSTEM_PROMPT`, `CONTENT_SUMMARY_PROMPT`, `DEFAULT_GPT_MEMORY`, `JSON_VALUE_SYSTEM_PROMPT`, `REMIX_PROMPT`, `TERRENO_ASKS_SYSTEM_PROMPT`, `TITLE_GENERATION_PROMPT`, `TRANSLATION_PROMPT`
|
|
100
|
+
- **Asks:** `createAskTools({kinds, surface?})`, `TERRENO_ASKS_SYSTEM_PROMPT`, `COMPACT_SURFACE_SYSTEM_PROMPT`, types `AsksOptions`, `ApprovalAskInput`, `AskOrigin`, `GptHistoryPendingAsk`, `GptHistoryPromptAsk`, `GptHistoryAskStatus`, and `Ask`, `AskKind`, `AskResponse`, `AskValidationError`, `SimpleCard`, `SimpleCardButton` re-exported from `@terreno/blocks` ([Agent UI Asks](agent-ui-asks.md))
|
|
96
101
|
- **Web search:** `WebSearchProvider`, `WebSearchResult` types
|
|
102
|
+
- **Harness (subpath `@terreno/ai/harness`):** `Harness`, `defineTask`, `defineAgent`, `defineTool`, `defineExtension`, `section`, `hook`, `wrapTool`, `HarnessExtensionError`, `HARNESS_HOOK_KINDS`, `HarnessConversationHandle`, `HarnessConversationBusyError`, `HarnessConversationOwnedError`, `HARNESS_EVENT_TYPES`, `HARNESS_SUBMIT_DISPOSITIONS`, `HARNESS_WHEN_BUSY`, `HarnessModelCallError`, `HarnessSubagentError`, `isRetryableModelError`, `AGENT_TURN_TASK_NAME`, `AGENT_TOOL_TASK_NAME`, `HARNESS_AGENT_DEFAULT_MAX_STEPS`, `HARNESS_CONVERSATION_STATUSES`, `HARNESS_INTERRUPT_ACTIONS`, `HARNESS_MESSAGE_ROLES`, `HARNESS_MODEL_RETRY_DEFAULTS`, `InProcessRunner`, `HarnessCommitConflictError`, `HARNESS_RESOLVE_ACTIONS`, `HARNESS_RETRY_DEFAULTS`, `HARNESS_TASK_STATUSES`, `HARNESS_WAIT_KINDS`, `HARNESS_WAIT_POLICIES`, `HARNESS_WAIT_RESOLUTIONS`, `IN_PROCESS_RUNNER_ROLES`, `approvalGate`, `approvalTaskInput`, `HarnessApp`, `HarnessApprovalConflictError`, `HARNESS_APPROVAL_STATUSES`, `HARNESS_DEFAULT_APPROVERS` — see [AI harness reference](ai-harness.md)
|
|
97
103
|
|
|
98
104
|
## AIService
|
|
99
105
|
|
|
@@ -131,18 +137,26 @@ const aiService = new AIService({
|
|
|
131
137
|
| `generateText(options)` | Non-streaming text generation; logs as `requestType: "general"` |
|
|
132
138
|
| `generateJsonValue(options)` | Any JSON value via `Output.json()`; logs as `"json_value"` |
|
|
133
139
|
| `generateJsonObject(options)` | Typed object from schema/Zod via `Output.object()`; logs as `"json_object"` |
|
|
140
|
+
| `generateBlocks(options)` | One block document via `Output.object(blocksJsonSchema)`, then `validateBlocks`. Temperature is always `TemperaturePresets.DETERMINISTIC` (0). Logs as `"ui_blocks"`. |
|
|
134
141
|
| `generateJsonArray(options)` | Typed array via `Output.array()`; logs as `"json_array"` |
|
|
135
142
|
| `generateTextStream(options)` | Async generator of text chunks; logs full response after stream completes |
|
|
136
143
|
| `generateRemix(options)` | Reword text using `REMIX_PROMPT` at `TemperaturePresets.BALANCED` |
|
|
137
144
|
| `generateSummary(options)` | Summarize text using `CONTENT_SUMMARY_PROMPT` at `TemperaturePresets.LOW` |
|
|
138
145
|
| `translateText(options)` | Translate text using `TRANSLATION_PROMPT` at `TemperaturePresets.LOW` |
|
|
139
|
-
| `buildMessages(prompts)` | Convert `GptHistoryPrompt[]` to Vercel AI SDK `ModelMessage[]
|
|
146
|
+
| `buildMessages(prompts)` | Convert `GptHistoryPrompt[]` to Vercel AI SDK `ModelMessage[]`. Skips host tool-call/result rows and approval ask rows (`ask.origin: "approval"`). Keeps each answered or cancelled ask as an assistant tool call plus its tool result; consecutive ask calls share one assistant message. Skips asks still waiting for an answer. |
|
|
140
147
|
| `generateChatStream(options)` | Stream multi-turn chat with optional tools; logs prompt as joined message text |
|
|
141
148
|
|
|
142
149
|
All generation methods log to `AIRequest` via private `logRequest()`. Logging failures never throw.
|
|
143
150
|
|
|
144
151
|
## Structured JSON output
|
|
145
152
|
|
|
153
|
+
`generateBlocks({prompt, systemPrompt?, userId?, repair?})` asks for one block document:
|
|
154
|
+
|
|
155
|
+
- Uses `TERRENO_UI_BLOCKS_SYSTEM_PROMPT` when `systemPrompt` is omitted.
|
|
156
|
+
- Temperature is `TemperaturePresets.DETERMINISTIC` (0).
|
|
157
|
+
- Checks the object with `validateBlocks`. When `repair` is omitted or true, one retry appends the error list to the user prompt. `repair: false` skips that retry.
|
|
158
|
+
- A second validation failure throws `APIError` 422 (`title: "Block document failed validation"`, `meta.fields` keyed by error code) and stores `metadata.errorCodes` on the `AIRequest`. A model or network error throws 502 (`title: "Block generation failed"`) and is not repaired.
|
|
159
|
+
|
|
146
160
|
`generateJsonValue`, `generateJsonObject`, and `generateJsonArray`:
|
|
147
161
|
|
|
148
162
|
- Default to `TemperaturePresets.DETERMINISTIC` when `temperature` is omitted.
|
|
@@ -186,13 +200,13 @@ Logs all AI calls for monitoring and admin explorer.
|
|
|
186
200
|
|-------|------|-------------|
|
|
187
201
|
| `aiModel` | string | Model identifier (field name avoids Mongoose `model` conflict) |
|
|
188
202
|
| `prompt` | string | Input prompt |
|
|
189
|
-
| `requestType` | string | e.g. `general`, `remix`, `summarization`, `translation`, `json_value`, `json_object`, `json_array` |
|
|
203
|
+
| `requestType` | string | e.g. `general`, `remix`, `summarization`, `translation`, `json_value`, `json_object`, `json_array`, `ui_action`, `ui_blocks` |
|
|
190
204
|
| `response` | string? | Response text |
|
|
191
205
|
| `responseTime` | number? | Milliseconds |
|
|
192
206
|
| `tokensUsed` | number? | Total tokens |
|
|
193
207
|
| `userId` | ObjectId? | Requesting user |
|
|
194
208
|
| `error` | string? | Error message |
|
|
195
|
-
| `metadata` | Mixed? | Extra data (e.g. structured-output debug) |
|
|
209
|
+
| `metadata` | Mixed? | Extra data (e.g. structured-output debug; `ask` and `nextAsk` for [asks](agent-ui-asks.md#stored-state)) |
|
|
196
210
|
| `parentRequestId` | ObjectId? | Parent in multi-agent workflow |
|
|
197
211
|
| `subRequestIds` | ObjectId[]? | Child request refs |
|
|
198
212
|
| `totalResponseTime` | number? | Combined sub-request time |
|
|
@@ -209,11 +223,12 @@ Conversation history with multi-modal prompts.
|
|
|
209
223
|
| Field | Type | Description |
|
|
210
224
|
|-------|------|-------------|
|
|
211
225
|
| `userId` | ObjectId | Owner (required) |
|
|
212
|
-
| `title` | string? | Auto-generated on first `/gpt/prompt`
|
|
226
|
+
| `title` | string? | Auto-generated on the first chat turn's reply (`/gpt/prompt` or `turn`) when empty |
|
|
213
227
|
| `projectId` | ObjectId? | Optional project association |
|
|
214
|
-
| `prompts` | array | Messages: `text`, `type` (`user` \| `assistant` \| `system` \| `tool-call` \| `tool-result`), optional `content` parts, `model`, `rating`, tool fields, `status
|
|
228
|
+
| `prompts` | array | Messages: `text`, `type` (`user` \| `assistant` \| `system` \| `tool-call` \| `tool-result`), optional `content` parts, `model`, `rating`, tool fields (`toolCallId`, `toolName`, `args`, `result`), `ask: {kind, status}` on ask `tool-call` rows (`status`: `pending` \| `answered` \| `cancelled`), and on assistant replies `status` (`streaming` \| `complete` \| `error`) plus `streamId`. `text` is required unless `content` has parts or `status` is set; an image-only assistant response saves `text: ""` |
|
|
229
|
+
| `pendingAsk` | object? | The ask the conversation waits on: `toolCallId`, `kind`, `input`, `simple`, `promptIndex`, `responseMessages`, `created`, and for an approval ask `origin`, `approvalId`, `toolName`. `/gpt/histories` responses leave out `promptIndex` and `responseMessages`. Only a chat turn (`/gpt/prompt` or the `turn` action) sets and clears it; see [Agent UI Asks](agent-ui-asks.md#stored-state). |
|
|
215
230
|
|
|
216
|
-
Assistant replies from `/gpt/prompt` carry `streamId` and `status`: `streaming` while partial text is persisted, then `complete` or `error`. `buildMessages` skips `streaming` replies and `error` replies with no output. Attachment `content` parts carry an optional `gcsKey` when the file was uploaded to durable storage.
|
|
231
|
+
Assistant replies from `/gpt/prompt` carry `streamId` and `status`: `streaming` while partial text is persisted, then `complete` or `error`. A paused ask's reply is `complete`, and `pendingAsk` is set beside it. `buildMessages` skips `streaming` replies and `error` replies with no output. Attachment `content` parts carry an optional `gcsKey` when the file was uploaded to durable storage.
|
|
217
232
|
|
|
218
233
|
**Virtual:** `ownerId` aliases `userId` for `Permissions.IsOwner`.
|
|
219
234
|
|
|
@@ -251,13 +266,61 @@ GPT project with persistent context and memories.
|
|
|
251
266
|
|
|
252
267
|
| Endpoint | Method | Auth | Description |
|
|
253
268
|
|----------|--------|------|-------------|
|
|
254
|
-
| `/gpt/prompt` | POST | `IsAuthenticated` | SSE streaming chat; body
|
|
269
|
+
| `/gpt/prompt` | POST | `IsAuthenticated` | SSE streaming chat turn; [body](#gptprompt-body) and [events](#sse-events) below |
|
|
255
270
|
| `/gpt/remix` | POST | `IsAuthenticated` | Non-streaming text remix; body: `{text}` |
|
|
256
271
|
| `/gpt/histories/:id/rating` | PATCH | `IsAuthenticated` | Rate a prompt; body: `{promptIndex, rating: "up" \| "down" \| null}` |
|
|
257
272
|
| `/gpt/histories/:id/stream` | GET | `IsAuthenticated` (owner) | SSE resume of an in-flight reply; query: optional `streamId`, `offset` |
|
|
258
|
-
| `/gpt/tools` | GET | `IsAuthenticated` | List builtin + MCP tools |
|
|
273
|
+
| `/gpt/tools` | GET | `IsAuthenticated` | List builtin + MCP tools (ask tools are not listed) |
|
|
274
|
+
| `/gpt/datasets/:id` | GET | owner (`IsOwner`; another user is 404) | Read a stored dataset. Mounted only when `uiBlocks` is on. Query: `grain` (`hour` \| `day` \| `week` \| `month`), `limit` (default 500, max 1000), `page`. Response `data`: `{columns, rows, rowCount, page, more}`. `grain` buckets the first date column in UTC. An offset is converted before `startOf`. A date with no zone is that UTC day. Null date cells are skipped. Number columns are summed. Without `page`, a series longer than `limit` is LTTB-downsampled and `more` is false. With `page`, rows are a page and `more` is true when another page remains. |
|
|
275
|
+
| `/gpt/actions` | POST | `IsAuthenticated` plus history owner (another user is 403) | Run a host callback. Mounted only when `uiBlocks` is on, on the `/gpt` path. Body: `{historyId, messageId, blockId, elementId, name, payload?}`. Unknown `name` is 404. A payload that fails the host schema is 400 with `meta.fields`. The handler has 10 seconds (`actionTimeoutMs` can set another cap) and then 504. Response `data`: `{text?, blocks?, replace?}`. An invalid `blocks` document is 500. Logged as `AIRequest` `requestType: "ui_action"`. |
|
|
276
|
+
|
|
277
|
+
Generated images (image-output models such as `gemini-3-pro-image`) arrive as SSE `image` events: `{image: {mimeType, url}}` with a base64 data URL. Each image is sent once, even when the model reports it both as a stream file part and in the final `result.files`. The saved assistant prompt stores one `image` content part per image and `text: ""` when there is no text. On later turns, `buildMessages` sends an image-only assistant prompt to the model as the text `[Generated image]`, because providers reject empty assistant turns.
|
|
278
|
+
|
|
279
|
+
AI resolution order: `x-ai-api-key` header + `createModelFn` → `createServerModelFn(modelId)` → configured `aiService`. When none resolves, `/gpt/prompt` streams a canned demo reply and `/gpt/remix` returns it. This happens whether or not `demoMode` is set.
|
|
280
|
+
|
|
281
|
+
Pass `asks: true` (or `{kinds: ["choice"]}`) to let the model ask the user typed questions in the chat. Asks are off by default; with them off, tools, system prompt, and SSE events are unchanged. See [Agent UI Asks](agent-ui-asks.md).
|
|
259
282
|
|
|
260
|
-
|
|
283
|
+
Pass `uiBlocks: true` (or `{hostActions, html, imageHosts, repair, datasetTtlDays, datasetMaxRows}`) to require each assistant reply to be a block document. `html: true` allows `html` blocks and sanitizes them before they are stored. `imageHosts` lists hostnames allowed on `https` image sources. The client receives that document once, after missing action ids are filled, `repair: true` rewrites it, and html is sanitized, and before `{ask}`. `{replace: "text", text}` is sent only when text was already streamed and then changed. `datasetTtlDays` defaults to `0` (keep the dataset). `datasetMaxRows` defaults to 50,000. Off by default; with it off, the system prompt, SSE events, `/gpt/datasets`, and `/gpt/actions` are unchanged. When it is on, the system prompt gains `TERRENO_UI_BLOCKS_SYSTEM_PROMPT` (host callback names included when `hostActions` is set). After the final text, the route validates it and sends `{blocks: {ok, errors, warnings}}` before `{done}`. `hostActions` is the callback allowlist: a name outside it fails with `UNKNOWN_HOST_ACTION`. `{repair: true}` runs one repair call when validation fails and stores that reply. A document that is still invalid is stored with a `Block validation errors:` note so the next turn sees it. See [Validate a block document locally](../how-to/agent-ui-blocks.md).
|
|
284
|
+
|
|
285
|
+
With asks on, a host tool with the AI SDK's `needsApproval: true` runs only after the user approves it: the turn pauses on a server-made `confirm` ask. `asks.approvals` sets that ask's input per tool name, as `(input) => ConfirmAskInput` (`ApprovalAskInput`); without an entry, the ask is "Allow <toolName>?" with the tool's description. With asks off, such a tool never runs. See [Approval asks](agent-ui-asks.md#approval-asks).
|
|
286
|
+
|
|
287
|
+
With `asks` on, pass the same options to `addGptHistoryRoutes` as `chat` to add the non-streaming [headless endpoints](#addgpthistoryroutesrouter-options) for clients that do not read server-sent events.
|
|
288
|
+
|
|
289
|
+
#### `/gpt/prompt` body
|
|
290
|
+
|
|
291
|
+
| Field | Type | Description |
|
|
292
|
+
|-------|------|-------------|
|
|
293
|
+
| `prompt` | string | The user's message. Required unless `askResponse` is sent. |
|
|
294
|
+
| `historyId` | string? | Continue this conversation; omit to start one. Required with `askResponse`. |
|
|
295
|
+
| `askResponse` | object? | The answer to the pending ask: `{toolCallId, action, content?, reason?}`. Read only when `asks` is on. Send it instead of `prompt`, without `attachments`. See [Answer an ask](agent-ui-asks.md#answer-an-ask). |
|
|
296
|
+
| `surface` | `"full"` \| `"compact"`? | Where the user reads and answers. Default `"full"`. `"compact"` is a watch or another small screen: the model gets only button-sized asks and is asked for replies of at most two short sentences. Any other value returns 400. See [Compact surface](agent-ui-asks.md#compact-surface). |
|
|
297
|
+
| `systemPrompt` | string? | System prompt for this turn; project context and the Langfuse prompt are prepended |
|
|
298
|
+
| `attachments` | array? | `{type: "image" \| "file", url, mimeType, filename?}` items added to the user message |
|
|
299
|
+
| `model` | string? | Model id passed to `createModelFn` or `createServerModelFn` |
|
|
300
|
+
| `projectId` | string? | Project whose context and memories are prepended to the system prompt; saved on a history that has none |
|
|
301
|
+
|
|
302
|
+
#### SSE events
|
|
303
|
+
|
|
304
|
+
`/gpt/prompt` streams `data: <json>` lines, one event object per line. `{askResolved}` is sent when an answer is stored, before the model runs. `{started}` is sent when the model produces its first part and the reply row is saved, before any text from that part. On a prompt that is not answering an ask, `{started}` is first. `{ask}` and `{done}` come last. With `uiBlocks` on, `{text}` is the finished document and comes before `{ask}`.
|
|
305
|
+
|
|
306
|
+
| Event | Shape | When |
|
|
307
|
+
|-------|-------|------|
|
|
308
|
+
| `{started}` | `{started: true, historyId, streamId}` | Sent when the model produces its first part and the reply row is saved. A reload can resume this `streamId`. It comes before text from that part. A model that fails before any part sends no `{started}`. The demo reply does not send it. |
|
|
309
|
+
| `{askResolved}` | `{askResolved: {toolCallId, action}}` | The turn answered the pending ask, or cancelled it because a new `prompt` arrived. Asks only. |
|
|
310
|
+
| `{text}` | `{text: string}` | A step's text, sent when the step ends. Text from a step that calls a tool is dropped, unless that text parses as a block document and the turn has no other text. A trailing JSON `"action"` blob is stripped. With `uiBlocks` on, this is the final document: sent once, after id-fill, repair, and sanitizing, and before `{ask}`. |
|
|
311
|
+
| `{toolCall}` | `{toolCall: {toolCallId, toolName, args}}` | The model called a host tool (route, request, or MCP). Never sent for ask tools. |
|
|
312
|
+
| `{file}` | `{file: {filename, mimeType, url}}` | A host tool result had a `fileData` data URL. Sent before its `{toolResult}`. `filename` defaults to `document` and `mimeType` to `application/octet-stream`. |
|
|
313
|
+
| `{toolResult}` | `{toolResult: {toolCallId, toolName, result}}` | A host tool returned. `fileData` is removed from `result`. For a tool whose approval was denied, `result` is `{approved: false, reason}`. Never sent for ask tools. |
|
|
314
|
+
| `{image}` | `{image: {mimeType, url}}` | The model generated an image; `url` is a `data:` URL. Each data URL is sent once. |
|
|
315
|
+
| `{ask}` | `{ask: {toolCallId, kind, input, simple, origin?, toolName?}, historyId}` | The turn paused on an ask. Sent after the turn is saved. `historyId` is the conversation that waits on the ask, so a new chat's ask can be answered before `{done}`. An [approval ask](agent-ui-asks.md#approval-asks) adds `origin: "approval"` and the host `toolName`. Asks only. |
|
|
316
|
+
| `{error}` | `{error: string}` | The model stream reported an error, or the turn failed after the stream started. `{done}` still follows. |
|
|
317
|
+
| `{replace}` | `{replace: "text", text}` | The assistant document after the server changed text the client already has: a missing actions `id` was filled in, `repair: true` rewrote the document, or `uiBlocks.html` sanitized it. Sent before `{blocks}`. With `uiBlocks` on, the first send is `{text}` of that finished document, so `{replace}` is not sent for it. |
|
|
318
|
+
| `{blocks}` | `{blocks: {ok, errors, warnings}}` | The final assistant text checked as a block document. Sent after the text and before `{done}`, only when `uiBlocks` is on and the turn produced text. |
|
|
319
|
+
| `{done}` | `{done: true, historyId?, title?, pendingAsk?}` | Last event of every turn that started streaming, also after `{error}`. `historyId` is missing only in the demo response and when a failed new chat could not be saved. `title` is set once the conversation has one. `pendingAsk: {toolCallId}` when the turn waits on an ask. |
|
|
320
|
+
|
|
321
|
+
When a turn fails after the stream starts, before the model's first chunk or partway through, the stream sends `{error}` then `{done}` with `historyId`. The turn keeps what the client already saw: the user's message, host tool rows, and any partial assistant text, saved with `status: "error"`. An empty placeholder is removed. An ask the failed stream had started is dropped. When the model call after an answer fails before the client gets any text, tool result, or ask, the answer is undone: the stream is `{askResolved}`, `{error}`, `{done, pendingAsk}`, the ask is pending again, and the same answer can be sent again. When it fails later, the stream is `{askResolved}`, `{error}`, `{done}` and the answer is kept: the ask stays answered and sending it again returns 409. Send a new `prompt` to continue. See [Agent UI Asks](agent-ui-asks.md#answer-an-ask).
|
|
322
|
+
|
|
323
|
+
Errors raised before the stream starts return JSON `{status, title, detail, fields?}` instead: 400 for an invalid body, 403 for another user's history, 404 for an unknown `historyId`, 409 for an answer to an ask that is not pending, and 500 otherwise. A `prompt` never gets 409: when another request resolved the ask it meant to cancel, it goes ahead as a normal message. [Agent UI Asks error responses](agent-ui-asks.md#error-responses) lists the ask cases.
|
|
261
324
|
|
|
262
325
|
#### Attachments
|
|
263
326
|
|
|
@@ -269,11 +332,11 @@ When `fileStorageService` is set, `data:` attachments are uploaded with `FileSto
|
|
|
269
332
|
|
|
270
333
|
#### Stream events and resume
|
|
271
334
|
|
|
272
|
-
`/gpt/prompt` saves the user turn and a `status: "streaming"` assistant placeholder before
|
|
335
|
+
`/gpt/prompt` saves the user turn and a `status: "streaming"` assistant placeholder when the model produces its first part. Waiting until then lets two turns on one history both load it before either writes. While the reply streams, partial text is persisted about every second (`streamPersistIntervalMs`), plus a heartbeat every 10 seconds. A text reply replaces that placeholder in place with `status: "complete"`. On failure, partial text is kept with `status: "error"`; an empty placeholder is removed.
|
|
273
336
|
|
|
274
337
|
| Event | Sent by | Meaning |
|
|
275
338
|
|-------|---------|---------|
|
|
276
|
-
| `{historyId, started: true, streamId}` | prompt |
|
|
339
|
+
| `{historyId, started: true, streamId}` | prompt | The reply row is saved and resumable; before any text from the model's first part |
|
|
277
340
|
| `{historyId, resumed: true, streamId?}` | resume | First event; `streamId` is absent when nothing is streaming |
|
|
278
341
|
| `{text}` | both | Text delta |
|
|
279
342
|
| `{replace: true, text}` | resume | Authoritative whole reply: sent first when the client provides `offset`, and whenever persisted text was rewritten (for example, a step became a tool call) |
|
|
@@ -284,8 +347,6 @@ When `fileStorageService` is set, `data:` attachments are uploaded with `FileSto
|
|
|
284
347
|
|
|
285
348
|
`GET /gpt/histories/:id/stream` re-attaches after a reload or remount. Pass `offset` as the number of characters the client already shows, usually the stored placeholder `text`. The endpoint polls the stored history (`streamResumePollIntervalMs`, default 500 ms), so it works across server instances. A `streaming` reply with no update for `streamStaleAfterMs` (default 60 s) is marked `error`.
|
|
286
349
|
|
|
287
|
-
AI resolution order: `x-ai-api-key` header + `createModelFn` → `createServerModelFn(modelId)` → configured `aiService` → demo SSE response when `demoMode` and none available.
|
|
288
|
-
|
|
289
350
|
### addGptHistoryRoutes(router, options?)
|
|
290
351
|
|
|
291
352
|
CRUD at `/gpt/histories` via `modelRouter`:
|
|
@@ -295,7 +356,16 @@ CRUD at `/gpt/histories` via `modelRouter`:
|
|
|
295
356
|
| Create, List | `IsAuthenticated` |
|
|
296
357
|
| Read, Update, Delete | `IsOwner` |
|
|
297
358
|
|
|
298
|
-
Query filtered by `userId`; sort `-updated`; query fields `userId`, `projectId`.
|
|
359
|
+
Query filtered by `userId`; sort `-updated`; query fields `userId`, `projectId`. Create sets `userId` to the caller, so `{}` is a valid create body even when the app validates request bodies. Create and update bodies drop `pendingAsk` (including dotted `pendingAsk.*` paths), so only a chat turn writes it; the OpenAPI spec marks it `readOnly` on create and update.
|
|
360
|
+
|
|
361
|
+
Pass `chat`, the options given to `addGptRoutes`, to add two headless actions for clients that do not read server-sent events, such as a watch app. Both exist only when `chat` turns `asks` on: without `chat`, or with `asks` off, neither action exists, so a host that never turned asks on gets no new endpoints. `AiApp` adds them when its `asks` option is set.
|
|
362
|
+
|
|
363
|
+
| Endpoint | Method | Auth | Description |
|
|
364
|
+
|----------|--------|------|-------------|
|
|
365
|
+
| `/gpt/histories/pendingAsks` | GET | `IsAuthenticated` | The caller's pending asks with their simple cards, newest first |
|
|
366
|
+
| `/gpt/histories/:id/turn` | POST | `IsOwner`; admins who do not own the history get 403 | Runs one chat turn to completion with the `chat` options and returns it as JSON. Body: one of `{prompt}`, `{askResponse}`, or `{toolCallId, buttonId}`, plus `surface`. The turn finishes and saves even if the client disconnects. |
|
|
367
|
+
|
|
368
|
+
Bodies, results, and errors: [Headless endpoints](agent-ui-asks.md#headless-endpoints).
|
|
299
369
|
|
|
300
370
|
### addProjectRoutes(router, options?)
|
|
301
371
|
|
|
@@ -311,9 +381,11 @@ Requires `fileStorageService` and `gcsBucket` (registered by `AiApp` when both a
|
|
|
311
381
|
|
|
312
382
|
| Endpoint | Method | Auth | Description |
|
|
313
383
|
|----------|--------|------|-------------|
|
|
314
|
-
| `/files/upload` | POST | `IsAuthenticated` | Multipart upload (`file` field); allowed MIME: images, PDF, plain text, CSV, JSON. Returns `403` when `fileUploadsEnabled` is off |
|
|
315
|
-
| `/files/*gcsKey` | GET |
|
|
316
|
-
| `/files/*gcsKey` | DELETE | `IsAuthenticated` (owner) | Soft-delete attachment and remove from GCS |
|
|
384
|
+
| `/files/upload` | POST | `IsAuthenticated` | Multipart upload (`file` field); allowed MIME: images, PDF, plain text, CSV, JSON. Capped at `maxFileSize` (default 10 MB). Returns `403` when `fileUploadsEnabled` is off. Returns `{data: {id, filename, gcsKey, mimeType, size, url}}`; send `id` as the `fileId` of a [`files` ask](agent-ui-asks.md#files) answer. |
|
|
385
|
+
| `/files/*gcsKey` | GET | `IsAuthenticated` (owner) | Returns `{data: {url}}`, a signed read URL (1 hour), for the caller's own upload. Another user's file returns 404, the same as a missing one, so keys cannot be probed; admins get no exception. |
|
|
386
|
+
| `/files/*gcsKey` | DELETE | `IsAuthenticated` (owner) | Soft-delete attachment and remove from GCS. 404 for a missing file, 403 for another user's. |
|
|
387
|
+
|
|
388
|
+
`*gcsKey` is the full key with its slashes, such as `uploads/<userId>/<ms>-<name>`.
|
|
317
389
|
|
|
318
390
|
### addMcpRoutes(router, options)
|
|
319
391
|
|
|
@@ -356,10 +428,11 @@ new AiApp({
|
|
|
356
428
|
| Option | Description |
|
|
357
429
|
|--------|-------------|
|
|
358
430
|
| `aiService` | Pre-configured server-wide AI service |
|
|
431
|
+
| `asks` | Let the model ask the user typed questions in chat: `true` or `{approvals, kinds, maxFileSizeBytes}`. `approvals` sets the [approval ask](agent-ui-asks.md#approval-asks) for host tools with `needsApproval`. Passed to `addGptRoutes`, and adds the headless `pendingAsks` and `turn` actions to `/gpt/histories`; see [Agent UI Asks](agent-ui-asks.md). `maxFileSizeBytes` also caps `/files/upload`. |
|
|
359
432
|
| `createModelFn` | Build model from per-request `x-ai-api-key` |
|
|
360
433
|
| `createServerModelFn` | Server-side model factory (e.g. Vertex ADC) without per-request key |
|
|
361
|
-
| `demoMode` |
|
|
362
|
-
| `fileStorageService` + `gcsBucket` | Enable file upload routes
|
|
434
|
+
| `demoMode` | Not read. The routes send a canned demo reply whenever no AI service resolves |
|
|
435
|
+
| `fileStorageService` + `gcsBucket` | Enable file upload routes, durable `/gpt/prompt` attachments, and `files` ask answers that name uploads by `fileId` |
|
|
363
436
|
| `fileUploadsEnabled` | `false` or a function returning `false` rejects uploads and chat attachments with `403`. Omit to leave uploads enabled |
|
|
364
437
|
| `mcpService` | Enable MCP routes and tool discovery in chat |
|
|
365
438
|
| `tools` | Static Vercel AI SDK tool definitions for chat |
|
|
@@ -402,6 +475,158 @@ Client construction failures log a warning and skip the plugin so the API proces
|
|
|
402
475
|
|
|
403
476
|
Calls `shutdownLangfuseClient()` and `shutdownTracing()` on `SIGTERM`.
|
|
404
477
|
|
|
478
|
+
## Observability
|
|
479
|
+
|
|
480
|
+
In-app prompt versions, nested traces, evaluators, datasets, experiments, review queue, and in-app feedback. Operator loop: [Develop an AI feature](../how-to/ai-feature-development.md). Register plugins: [Observe LLM calls](../how-to/observe-llm-calls.md). Why two planes: [AI observability](../explanation/ai-observability.md). Locked design: [implementation plan](../implementationPlans/ai-observability.md).
|
|
481
|
+
|
|
482
|
+
Register `ObservabilityApp` with at least a local plugin. Construction throws if `experiments.primary !== datasets.primary`, if `reviewQueue` is not `local`, or if a control primary has no matching plugin. Defaults for all four primaries are `local`. Construction also registers the app as the process singleton (`getObservabilityApp()`) through the dependency-free observability registry, so routes do not import the plugin class that registers them. Call `resetObservabilityApp()` in tests. `createLocalObservabilityPlugin()` registers the local Mongo models (`ObsPrompt`, `ObsPromptVersion`, `ObsPromptLabel`, `ObsTrace`, `ObsSpan`, `ObsScore`) on the default connection.
|
|
483
|
+
|
|
484
|
+
The example backend always registers `createLocalObservabilityPlugin()` and passes the
|
|
485
|
+
validated `AI_OBS_PRICE_MAP_JSON` object as `priceMap`. `bun run backend:seed` idempotently
|
|
486
|
+
creates `examples/example-summarize` with production on v1 and an experimental v2, installs
|
|
487
|
+
`correctness-human` and `schema-assert`, and creates a two-item proofread `example-gold`
|
|
488
|
+
dataset bound to the prompt input schema. The same seed creates `examples/chat-safety-screen`
|
|
489
|
+
(production v1 scores the new message alone; v2 reads earlier turns), `examples/chat-safety-judge`,
|
|
490
|
+
the `chat-safety-agreement` llm-judge, and the 12-item proofread synthetic dataset
|
|
491
|
+
`chat-safety-synthetic`. Each row is one new message plus earlier turns from a two-person chat,
|
|
492
|
+
labeled for 988 vs care-team routing, toxicity, a privacy leak, and a dismissive reply.
|
|
493
|
+
Admin → Scripts → `seedChatSafetyDataset`, or `bun run script seedChatSafetyDataset --wet` from
|
|
494
|
+
`example-backend`, loads that set into an already-running database. `SEED_DEFAULTS=true` loads
|
|
495
|
+
it on boot, including PR preview. Invalid price JSON or negative/non-numeric prices
|
|
496
|
+
fail startup with `AI_OBS_PRICE_MAP_JSON` in the error.
|
|
497
|
+
|
|
498
|
+
`POST /ai/example-summarize` (example backend) runs that seeded prompt with
|
|
499
|
+
`promptLabel: "production"`, `userId`, and `sessionId` from `x-ai-session-id`. It uses the
|
|
500
|
+
server `AIService` when configured, otherwise a request-scoped service built from
|
|
501
|
+
`x-ai-api-key`, and returns **503** when neither exists. The example frontend calls it from
|
|
502
|
+
**Todos → Summarize**.
|
|
503
|
+
|
|
504
|
+
### Local observability models
|
|
505
|
+
|
|
506
|
+
| Model | Role |
|
|
507
|
+
| --- | --- |
|
|
508
|
+
| `ObsPrompt` | Named prompt (`name` unique) with `folder` and `tags[]` |
|
|
509
|
+
| `ObsPromptVersion` | Immutable `vN` body, `variables[]`, schemas, `sensitive` (default false), `config` |
|
|
510
|
+
| `ObsPromptLabel` | Movable labels; unique `(promptId, label)` |
|
|
511
|
+
| `ObsTrace` | Root trace: user, session, status, `errorSummary`, `sensitive`, `prompts[]`, usage |
|
|
512
|
+
| `ObsSpan` | Nested span with `kind`, `status`, optional `error`, offsets, usage |
|
|
513
|
+
| `ObsScore` | Scores on a trace/span; many per trace, **no unique index** |
|
|
514
|
+
| `ObsEvaluator` | Evaluator: `type` (`human` \| `llm-judge` \| `json-assert`), `target`, `dimensions[]`, `runModes`, `instructions`, `judgePromptName` (judge), `assertion` (json-assert), `confidenceAlertBelow` (default 0.7) |
|
|
515
|
+
| `ObsReviewItem` | Review queue item: status, evaluator, trace, reason, scores, comment |
|
|
516
|
+
| `ObsDataset` | Named dataset with optional `inputSchemaPromptName` and `expectedOutputSchema` |
|
|
517
|
+
| `ObsDatasetItem` | Item with `input`, `expectedOutput`, `origin`, `proofread`, `tags`, `outcomeClass`, `sourceTraceId`, `metadata` |
|
|
518
|
+
| `ObsExperiment` | Compares 2–3 prompt versions on a dataset with thresholds and aggregates |
|
|
519
|
+
| `ObsExperimentItem` | Per dataset row: outputs per version, evaluator score maps, gate failure flags |
|
|
520
|
+
|
|
521
|
+
`POST /ai/observability/traces/review` requires a `human` `ObsEvaluator`. Its
|
|
522
|
+
`dimensions[]` render as reviewer score fields and `instructions` render above the review form.
|
|
523
|
+
Automatic evaluator types return **400** instead of entering the human queue. Submitting a review
|
|
524
|
+
requires every dimension marked `required`; omitted optional dimensions do not create empty score
|
|
525
|
+
rows. The local score store remains the fallback when no external score sink is configured.
|
|
526
|
+
|
|
527
|
+
`AIService` generate methods:
|
|
528
|
+
|
|
529
|
+
| Option | Default | Behavior |
|
|
530
|
+
| --- | --- | --- |
|
|
531
|
+
| `promptName` | unset | Resolve `PromptRegistry.get({name, label})` **before** the model call, even when `skipTrace` is true |
|
|
532
|
+
| `promptLabel` | `"production"` | Label used with `promptName` |
|
|
533
|
+
| `skipTrace` | `false` | Skip `TraceSink.export` only; prompt resolve and `AIRequest` still run |
|
|
534
|
+
| `sensitive` | inherited | Explicit value wins; otherwise the resolved prompt version's `sensitive` |
|
|
535
|
+
| `sessionId` / `userId` | unset | Copied onto the exported trace |
|
|
536
|
+
| `priceMap` | app `priceMap` | Per-call override; `costUsd` is omitted when the model is unpriced |
|
|
537
|
+
|
|
538
|
+
Missing registry, missing prompt, or missing label throws `APIError` 400 and does not call the model. Sink `export` failures are logged and never fail generate.
|
|
539
|
+
|
|
540
|
+
**GPT tool spans:** `/gpt/prompt` collects streamed `tool-call` / `tool-result` events into `TOOL` child spans (name = tool name, input = args, output = cleaned result with large `fileData` stripped). When any tool span is present, `AIService.recordGenerate` exports one trace with a `CHAIN` root plus `TOOL` children linked by `parentSpanId`. Ordinary `generateText` / JSON helpers without `childSpans` still emit a single `LLM` root span.
|
|
541
|
+
|
|
542
|
+
`ObservabilityApp.exportTrace(trace)` fans out to every `TraceSink` (best-effort) and returns the first persisted `{id}` from sinks that support it (for example `LocalTraceSink`). `TraceSink.export` may return `TraceExportResult` (`{id?: string}`) or `void`; `MemoryTraceSink` remains in-memory only.
|
|
543
|
+
|
|
544
|
+
When `prompts.primary` is `local`, `ObservabilityApp.register` mounts admin-only prompt routes at `/ai/observability`. Pass `aiService` on `ObservabilityApp` for playground runs and the multi-stage trace smoke endpoint. Apps that let an admin supply a per-request provider key may instead set `requestAiServiceFactory`; both the playground and the multi-stage smoke endpoint read the key from `x-ai-api-key`, while a configured server `aiService` remains preferred. `GET /ai/observability/status` is always mounted so admin chrome can read plugin ids, capabilities, primaries, `localOn`, and `playgroundAi.source` (`server` \| `request-key` \| `unavailable`). Admin UIs use that field to distinguish “save a provider key” from true backend misconfiguration.
|
|
545
|
+
|
|
546
|
+
| Method | Path | Behavior |
|
|
547
|
+
| --- | --- | --- |
|
|
548
|
+
| GET | `/ai/observability/status` | Admin chrome. `{plugins, primaries, localOn, playgroundAi}` — drives the status chip, hides Review when `localOn` is false, and tells the prompt playground whether AI comes from `ObservabilityApp.aiService` (`server`), per-request `x-ai-api-key` via `requestAiServiceFactory` (`request-key`), or is not configured (`unavailable`) |
|
|
549
|
+
|
|
550
|
+
| Method | Path | Behavior |
|
|
551
|
+
| --- | --- | --- |
|
|
552
|
+
| GET | `/ai/observability/prompts` | List. Query `folder`, `search`, `include=usage7d` (7-day calls/cost). `production` is `"—"` until a production label exists |
|
|
553
|
+
| POST | `/ai/observability/prompts` | Create prompt in a folder as immutable v1 (`latest` label) |
|
|
554
|
+
| GET | `/ai/observability/prompts/:name` | Prompt + versions + labels |
|
|
555
|
+
| POST | `/ai/observability/prompts/:name/versions` | Create `vN+1`; never mutates an existing version |
|
|
556
|
+
| POST | `/ai/observability/prompts/:name/labels` | Move `production` or `staging`; `outgoingVersion` is the previous pointer |
|
|
557
|
+
| POST | `/ai/observability/prompts/:name/playground` | Compile `{{var}}` + one `AIService` call; returns compiled messages, output, latency, tokens, cost; creates no version. Uses `ObservabilityApp.aiService`, or `requestAiServiceFactory({apiKey, modelId})` when the server service is absent (`apiKey` comes from `x-ai-api-key`) |
|
|
558
|
+
|
|
559
|
+
`PromptRegistry.get({name, label})` (default label `production`) reads the labelled local version. `createLocalObservabilityPlugin()` wires `LocalPromptStore` as that registry and local `TraceSink` / `ScoreSink`.
|
|
560
|
+
|
|
561
|
+
| Method | Path | Behavior |
|
|
562
|
+
| --- | --- | --- |
|
|
563
|
+
| GET | `/ai/observability/traces` | Admin list. Query `from`, `to`, `prompt`, `status`, `userId`, `sessionId`, `hasScore`, `sensitive`, `flaggedForDataset`, `page`, `limit`. Body is `{data, page, limit, more, total}` so pagination survives RTK `{data}` unwrap. Each row includes `spanCount` and `scoreCount`. `prompts.length` is the `N prompts` count |
|
|
564
|
+
| GET | `/ai/observability/traces/:id` | Span tree (kind, offsets, durations, I/O, cost) plus scores. `errorSummary` is the first span with `status: "error"` |
|
|
565
|
+
| POST | `/ai/observability/traces/:id/scores` | Persist a score and fan out to every `ScoreSink` |
|
|
566
|
+
| POST | `/ai/observability/traces/test-multi-stage` | Admin-only smoke workflow, registered only with the local trace sink. Uses `ObservabilityApp.aiService`, or `requestAiServiceFactory({apiKey})` when the server service is absent (`apiKey` comes from `x-ai-api-key`); answers **503** with the same missing-key title as playground when neither exists. Body `{input?: string}` (defaults to a built-in sample). Runs two `AIService.generateJsonObject` calls with `skipTrace: true` and named JSON output schemas (`obs-test-multi-stage-call-1` / `call-2`), a deterministic local `text-metrics` `TOOL` stage, then a final `generateJsonObject` synthesis against `obs-test-multi-stage-final`; exports exactly one parent trace with ordered child spans `LLM`, `LLM`, `TOOL`, `LLM` under a `CHAIN` root. LLM span input includes `outputSchema`. Returns `{traceId, output, stages[]}` where `output` is the final schema object (`sentence`, `phrase`, `keywords`, `metrics`). Child LLM failures export an error trace then rethrow |
|
|
567
|
+
|
|
568
|
+
`createLocalObservabilityPlugin()` registers `ObsEvaluator` with the other local models.
|
|
569
|
+
|
|
570
|
+
| Method | Path | Behavior |
|
|
571
|
+
| --- | --- | --- |
|
|
572
|
+
| GET | `/ai/observability/evaluators/templates` | Seeded templates: `llm-judge` (`correctness`, `hallucination`, `helpfulness`, `toxicity`), `json-assert` (`schema-assert`), and human queue variants (`correctness-human`, …) |
|
|
573
|
+
| POST | `/ai/observability/evaluators/templates/:name` | Install a template by name as an immutable-named evaluator |
|
|
574
|
+
| GET/POST | `/ai/observability/evaluators` | List / create. Create accepts `target: "full trace"` only (`generation span` and `dataset item` → 400). Seeded template install can still store other targets. `llm-judge` requires `judgePromptName`; create rejects when the judge prompt `outputSchema` omits a required dimension (400 names the key). `json-assert` supports `assertion` (`path` + `constraint`) or built-in output-schema mode. Human + `liveSampleRate > 0` → 400. Numeric dimension `range` is `min-max` (for example `0-1`); categorical `range` is `label|label` |
|
|
575
|
+
| GET/PATCH/DELETE | `/ai/observability/evaluators/:id` | Read / update / soft-delete |
|
|
576
|
+
| POST | `/ai/observability/traces/review` | Enqueue one or many traces against a human evaluator (`reason: "manual"`) |
|
|
577
|
+
| GET | `/ai/observability/review` | Queue by `status` with counts; oldest-first. Response includes `more: false` so RTK preserves the count envelope. Rows include `traceName`, `promptName`, assignee, reason, and enqueue time |
|
|
578
|
+
| GET | `/ai/observability/review/:id` | Item + evaluator dimensions + `given` / `wrote` panels and `rawInput` / `rawOutput` for the Raw JSON disclosure |
|
|
579
|
+
| POST | `/ai/observability/review/:id` | `submit` (scores via ScoreSinks, status `done`), `skip`, or `assign` |
|
|
580
|
+
|
|
581
|
+
`createLocalObservabilityPlugin()` wires `LocalDatasetStore` and `LocalExperimentRunner` when datasets/experiments primaries are `local`.
|
|
582
|
+
|
|
583
|
+
| Method | Path | Behavior |
|
|
584
|
+
| --- | --- | --- |
|
|
585
|
+
| GET/POST | `/ai/observability/datasets` | List (includes `humanCount` for proofread items, `autoCount` for unreviewed trace or synthetic items, `needsReviewCount`) / create |
|
|
586
|
+
| GET/PATCH/DELETE | `/ai/observability/datasets/:id` | Detail (with counts) / update / soft-delete. PATCH `null` clears optional dataset fields; omitted fields stay unchanged |
|
|
587
|
+
| GET/POST | `/ai/observability/datasets/:id/items` | List / create items |
|
|
588
|
+
| PATCH/DELETE | `/ai/observability/datasets/:id/items/:itemId` | Update labels (`expectedOutput`, `proofread`, `tags`, `outcomeClass`) / delete (does not touch the source trace). PATCH `null` clears optional item fields |
|
|
589
|
+
| POST | `/ai/observability/datasets/:id/import` | **JSON:** body is an array of bare input objects, or structured rows with `input` / `expectedOutput` / `proofread` / `tags` / `outcomeClass` / `metadata`. **CSV:** `Content-Type: text/csv` with raw CSV body, or JSON `{format: "csv", content: "..."}`. Plain columns map to `input`; plain `input` / `expectedOutput` cells accept JSON values; `input.foo` and `expectedOutput.foo` nest fields; reserved `proofread`, `tags`, `outcomeClass` map metadata. Nested paths reject `__proto__`, `constructor`, and `prototype` segments. Rows validate against the dataset's bound prompt `inputSchema` when `inputSchemaPromptName` is set; 400 reports row number and JSON path |
|
|
590
|
+
| POST | `/ai/observability/traces/add-to-dataset` | `{datasetId, traceId \| traceIds[]}`. Copies span I/O; `origin: "trace"`; `sourceTraceId` set; **sensitive traces always `proofread: false`** |
|
|
591
|
+
|
|
592
|
+
| Method | Path | Behavior |
|
|
593
|
+
| --- | --- | --- |
|
|
594
|
+
| POST | `/ai/observability/experiments/estimate` | `{datasetId, promptName, versions[], evaluatorIds[], modelOverride?}` → generation count, USD, wall-clock estimate |
|
|
595
|
+
| GET/POST | `/ai/observability/experiments` | List / create. Body: dataset, 2–3 version numbers, evaluator ids, optional `thresholds[]` (defaults to `SOP_DEFAULT_THRESHOLDS`), `modelOverride`, `includeUnproofread` (default false). Evaluator ids must be `llm-judge` or `json-assert` (human → 400). Local primary always enqueues `BackgroundTask` (even one item) |
|
|
596
|
+
| GET | `/ai/observability/experiments/:id` | Status, progress, per-version aggregates, gate pass/fail (`gates[].version`), `outlierItemIds`, `lowConfidenceItemIds`, per-item side-by-side (**failed rows first**) |
|
|
597
|
+
| POST | `/ai/observability/experiments/:id/promote` | `{version}` moves the `production` label when **that version's** gates pass; **409** while any gate for the selected version fails |
|
|
598
|
+
|
|
599
|
+
Authenticated `POST /ai/observability/traces/:id/feedback` records thumbs, outcome class, and flag-for-dataset (phase 2.6).
|
|
600
|
+
|
|
601
|
+
## Durable agent harness
|
|
602
|
+
|
|
603
|
+
`@terreno/ai/harness` runs multi-phase tasks that survive restarts. Each phase commits a
|
|
604
|
+
checkpoint and its `ObsSpan` audit record in one Mongo transaction, so it needs a replica
|
|
605
|
+
set and `createLocalObservabilityPlugin()`. Owner and task leases resume crashed work on a
|
|
606
|
+
fresh process; phases that are not replay-safe park as `interrupted` until an operator calls
|
|
607
|
+
`resolveInterrupted`. A thrown phase retries with exponential backoff before it fails.
|
|
608
|
+
Phases can start child tasks (`rt.createTask`), wait on them (`rt.waitForTasks`), and
|
|
609
|
+
`harness.abort` stops a whole ownership tree bottom-up, running compensation handlers.
|
|
610
|
+
`defineAgent` / `defineTool` run durable agent conversations: each turn is a task whose
|
|
611
|
+
model requests retry 429/5xx and fall back to other models, and whose tool calls run as
|
|
612
|
+
child tasks with per-tool replay rules. `rt.runAgent` runs an agent as a subagent from a
|
|
613
|
+
phase (a task-owned conversation) and returns its text or schema-checked output.
|
|
614
|
+
`defineExtension` bundles prompt sections, tools, tool wraps, and hooks
|
|
615
|
+
(`beforeModelRequest`, `beforeTool` with block or rewrite, `afterTool`); `rt.memo` stores
|
|
616
|
+
first-write-wins decisions that survive restarts. `rt.waitFor(event, {timeout})` and
|
|
617
|
+
`rt.sleep(duration)` park a task without a lease; `harness.sendEvent` stores the event
|
|
618
|
+
durably and wakes the task, even when no runner is up. `rt.approval(key, {...})` waits for
|
|
619
|
+
a human decision; `approvalGate` requires one before named tool calls; `HarnessApp` serves
|
|
620
|
+
`GET /harness/approvals` (only what the caller may approve) and `approve` / `reject`
|
|
621
|
+
instance actions, each decision audited in an `approval:<key>` span. Clients watch runs
|
|
622
|
+
over SSE (`GET /harness/conversations/:id/events`, `GET /harness/tasks/:id/events`),
|
|
623
|
+
resumable from any instance with `Last-Event-ID`; turns stream coalesced text deltas, and
|
|
624
|
+
`POST /harness/conversations/:id/submit` queues or steers a message while a turn runs.
|
|
625
|
+
API: [AI harness reference](ai-harness.md).
|
|
626
|
+
Why: [Durable agent harness](../explanation/durable-agent-harness.md).
|
|
627
|
+
How-to: [Build a durable workflow](../how-to/build-a-durable-workflow.md),
|
|
628
|
+
[Ship a new task version](../how-to/ship-a-new-task-version.md).
|
|
629
|
+
|
|
405
630
|
## Langfuse integration
|
|
406
631
|
|
|
407
632
|
Low-level exports (also used by `addGptRoutes` when `langfuseSystemPromptName` is set):
|
|
@@ -423,7 +648,8 @@ const storage = new FileStorageService({
|
|
|
423
648
|
storageOptions: {}, // optional @google-cloud/storage options
|
|
424
649
|
});
|
|
425
650
|
|
|
426
|
-
await storage.upload({buffer, filename, mimeType, userId});
|
|
651
|
+
await storage.upload({buffer, filename, mimeType, userId}); // {id, filename, gcsKey, mimeType, size, url}
|
|
652
|
+
await storage.download(gcsKey); // the upload's bytes, as a Buffer
|
|
427
653
|
await storage.getSignedUrl(gcsKey); // 1-hour v4 signed URL
|
|
428
654
|
await storage.delete(gcsKey); // GCS delete + soft-delete FileAttachment
|
|
429
655
|
```
|
|
@@ -505,7 +731,7 @@ new TerrenoApp({userModel: User})
|
|
|
505
731
|
.start();
|
|
506
732
|
```
|
|
507
733
|
|
|
508
|
-
Legacy `setupServer` pattern: call `addGptHistoryRoutes`, `addGptRoutes`, etc. inside `addRoutes`.
|
|
734
|
+
Legacy `setupServer` pattern: call `addGptHistoryRoutes`, `addGptRoutes`, etc. inside `addRoutes`. With `asks` on, pass the chat options to `addGptHistoryRoutes` as `chat` to keep the headless endpoints.
|
|
509
735
|
|
|
510
736
|
## Environment variables
|
|
511
737
|
|
|
@@ -513,7 +739,7 @@ Legacy `setupServer` pattern: call `addGptHistoryRoutes`, `addGptRoutes`, etc. i
|
|
|
513
739
|
|----------|---------|-------------|
|
|
514
740
|
| `GOOGLE_VERTEX_PROJECT` | `createVertexProvider` | GCP project for Vertex models |
|
|
515
741
|
| `GOOGLE_VERTEX_LOCATION` | `createVertexProvider` | Vertex region (default `global`) |
|
|
516
|
-
| `LANGFUSE_PUBLIC_KEY` | `LangfuseApp` | Langfuse public key |
|
|
742
|
+
| `AI_OBS_PRICE_MAP_JSON` | `ObservabilityApp` | JSON model map with non-negative `inputPerMTok` / `outputPerMTok`; omitted models have tokens but no USD cost || `LANGFUSE_PUBLIC_KEY` | `LangfuseApp` | Langfuse public key |
|
|
517
743
|
| `LANGFUSE_SECRET_KEY` | `LangfuseApp` | Langfuse secret key |
|
|
518
744
|
| `LANGFUSE_BASE_URL` | Langfuse client | Langfuse host URL |
|
|
519
745
|
|
|
@@ -532,7 +758,7 @@ GCS credentials use standard Google Cloud Application Default Credentials for `F
|
|
|
532
758
|
|
|
533
759
|
- Framework: `bun test` with preload `./src/tests/bunSetup.ts`
|
|
534
760
|
- HTTP: supertest against real routes
|
|
535
|
-
- DB: memory
|
|
761
|
+
- DB: in-memory single-node replica set via `@terreno/test` (transactions work); `TERRENO_TEST_MONGODB_URI` overrides it and must point at a replica set
|
|
536
762
|
- Mock AI model: implement `doGenerate` and `doStream` on a fake `LanguageModel`
|
|
537
763
|
|
|
538
764
|
```typescript
|
|
@@ -41,7 +41,7 @@ REST API framework built on Express and Mongoose. Provides modelRouter (CRUD end
|
|
|
41
41
|
- Validation: `configureOpenApiValidator`, `validateRequestBody`, `validateQueryParams`, `createValidator`
|
|
42
42
|
- Middleware: `openApiEtagMiddleware`, `sentryAppVersionMiddleware`
|
|
43
43
|
- Extensibility: `TerrenoPlugin` interface
|
|
44
|
-
- Notifiers: `sendToSlack`, `formatSlackUserMention`, `lookupSlackUserIdByEmail`, `sendToGoogleChat`, `sendToZoom`
|
|
44
|
+
- Notifiers: `sendToSlack`, `formatSlackUserMention`, `lookupSlackUserIdByEmail`, `createSlackPrivateChannel`, `inviteSlackUsersToChannel`, `postSlackMessage`, `findSlackChannelByName`, `sendToGoogleChat`, `sendToZoom`
|
|
45
45
|
- HTTP client: `createAuthenticatedClient`, `withApiErrorHandling`, `normalizeApiError`, `markRetryUnsafe`
|
|
46
46
|
|
|
47
47
|
## Server Setup
|
|
@@ -224,6 +224,30 @@ export const todoRouter = modelRouter("/todos", Todo, {
|
|
|
224
224
|
Do not add `endpoints: (router) => { router.get(...) }` when an action fits.
|
|
225
225
|
Action config supports `allowAnonymous: true` as a per-action override; it does not change authentication for CRUD or sibling actions.
|
|
226
226
|
|
|
227
|
+
## modelRouter list queries
|
|
228
|
+
|
|
229
|
+
`GET /{path}` accepts filters on fields listed in `queryFields` (qs syntax, so operators work:
|
|
230
|
+
`?status[$in][0]=open&created[$gte]=2026-01-01`), plus the reserved `limit`, `page`, and `sort`.
|
|
231
|
+
`limit` is clamped to `maxLimit` (default 500); the response echoes the applied `limit` with
|
|
232
|
+
`{data, limit, more, page, total}`.
|
|
233
|
+
|
|
234
|
+
A filter on a field missing from `queryFields` is rejected before query validation runs, so
|
|
235
|
+
it can never be stripped and silently ignored:
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{
|
|
239
|
+
"status": 400,
|
|
240
|
+
"code": "query-param-not-allowed",
|
|
241
|
+
"title": "Query parameter not allowed",
|
|
242
|
+
"detail": "title is not allowed as a query param. Add \"title\" to queryFields on the Todo modelRouter to filter on it. Allowed: completed, created, ownerId.",
|
|
243
|
+
"source": {"parameter": "title"},
|
|
244
|
+
"meta": {"queryParam": "title", "model": "Todo", "allowedQueryFields": ["completed", "created", "ownerId"]}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`@terreno/syncdb` raises this as `QueryFieldNotAllowedError` (see
|
|
249
|
+
[Query windows](syncdb.md#query-windows)).
|
|
250
|
+
|
|
227
251
|
## modelRouter array operations
|
|
228
252
|
|
|
229
253
|
When a model has at least one array field, `modelRouter` provides subroutes for adding,
|
|
@@ -235,6 +259,10 @@ updating, and removing array items:
|
|
|
235
259
|
| `PATCH` | `/:id/:field/:itemId` | Replace or merge an item |
|
|
236
260
|
| `DELETE` | `/:id/:field/:itemId` | Remove an item |
|
|
237
261
|
|
|
262
|
+
`:id` must be an ObjectId. Any other id leaves this router so a later route on the same
|
|
263
|
+
prefix can handle it. `POST /ai/observability/evaluators` is not an array push on a model
|
|
264
|
+
mounted at `/ai`.
|
|
265
|
+
|
|
238
266
|
The `field` path must resolve to an array on the document. Requests for scalar, object,
|
|
239
267
|
or unknown fields run the normal update transform and `preUpdate` hook first so consumer
|
|
240
268
|
authorization remains authoritative, then return `400` with code
|
|
@@ -834,6 +862,48 @@ await Membership.create({
|
|
|
834
862
|
});
|
|
835
863
|
``````
|
|
836
864
|
|
|
865
|
+
### AI observability RBAC
|
|
866
|
+
|
|
867
|
+
`terrenoStatements` includes domain-neutral resources for `@terreno/ai` `ObservabilityApp`
|
|
868
|
+
routes. Pass `accessControl` from `createAccess` on `ObservabilityApp` to enforce them; omit
|
|
869
|
+
`accessControl` to keep legacy `user.admin` / `Permissions.IsAdmin` behavior.
|
|
870
|
+
|
|
871
|
+
| Resource | Actions |
|
|
872
|
+
| --- | --- |
|
|
873
|
+
| `aiPrompt` | `list`, `read`, `create`, `update`, `promote`, `playground` |
|
|
874
|
+
| `aiTrace` | `list`, `read` |
|
|
875
|
+
| `aiReview` | `list`, `read`, `score`, `assign` |
|
|
876
|
+
| `aiDataset` | `create`, `list`, `read`, `update`, `delete` |
|
|
877
|
+
| `aiEvaluator` | `create`, `list`, `read`, `update`, `delete` |
|
|
878
|
+
| `aiExperiment` | `list`, `read`, `create`, `promote` |
|
|
879
|
+
|
|
880
|
+
Human-readable labels for the admin role editor live in `terrenoStatementDescriptions`
|
|
881
|
+
(optional on `createAccess({ statementDescriptions })`).
|
|
882
|
+
|
|
883
|
+
| Role | Observability grants |
|
|
884
|
+
| --- | --- |
|
|
885
|
+
| `superadmin` | `*` (every action on every resource) |
|
|
886
|
+
| `admin` (seeded) | Full grants on all six resources |
|
|
887
|
+
| `auditor` | Read-only sentinel expands to `list` and `read` on each observability resource (no `admin:access`) |
|
|
888
|
+
| `aiObservabilityViewer` (example-backend only) | `admin:access` plus `list` and `read` on each observability resource |
|
|
889
|
+
| `aiObservabilityOperator` (example-backend only) | `admin:access` plus full grants on each observability resource |
|
|
890
|
+
|
|
891
|
+
The seeded `auditor` role does **not** include `admin:access`. Observability `list` / `read`
|
|
892
|
+
grants alone do not open the admin shell — compose a consumer role (for example
|
|
893
|
+
`auditor` + `admin: ["access"]`, or a custom role) when operators should browse observability
|
|
894
|
+
screens.
|
|
895
|
+
|
|
896
|
+
Re-seeding default roles does not overwrite customized unsealed roles, except seeded
|
|
897
|
+
`admin` and `auditor`: on each `seedDefaults`, Terreno **merges** missing observability
|
|
898
|
+
actions into stored permissions (`admin` — full grants; `auditor` — `list` and `read` per
|
|
899
|
+
resource) without removing consumer-defined grants on other resources.
|
|
900
|
+
|
|
901
|
+
When `accessControl` is configured, each observability HTTP route checks `admin:access` and
|
|
902
|
+
the mapped resource action. Callers with `user.admin === true` still receive full access
|
|
903
|
+
(legacy compatibility). RBAC-only operators need `admin:access` plus the exact action for
|
|
904
|
+
mutations. Route mapping (for example prompt label moves → `aiPrompt:promote`, review
|
|
905
|
+
submit → `aiReview:score`) is documented in [AI reference — Observability](ai.md#observability).
|
|
906
|
+
|
|
837
907
|
## Request Validation
|
|
838
908
|
|
|
839
909
|
@terreno/api provides runtime validation of incoming requests against OpenAPI schemas using [AJV](https://ajv.js.org/). Validation is opt-in and can be enabled globally or per-route.
|
|
@@ -1535,6 +1605,44 @@ await sendToSlack(`${formatSlackUserMention("U012ABCDEF")} Deployment complete`,
|
|
|
1535
1605
|
});
|
|
1536
1606
|
``````
|
|
1537
1607
|
|
|
1608
|
+
Incoming webhooks are fixed to the channel chosen when the webhook was created.
|
|
1609
|
+
Private channels, invites, and messages to a channel id use the bot token for
|
|
1610
|
+
one Slack workspace (`SLACK_BOT_TOKEN`):
|
|
1611
|
+
|
|
1612
|
+
| Function | Slack method | Bot scopes |
|
|
1613
|
+
| --- | --- | --- |
|
|
1614
|
+
| `createSlackPrivateChannel` | `conversations.create`, `conversations.invite`, `chat.postMessage` | `groups:write`, `groups:write.invites`, `chat:write` |
|
|
1615
|
+
| `inviteSlackUsersToChannel` | `conversations.invite` | `groups:write.invites` for private channels, `channels:write.invites` for public |
|
|
1616
|
+
| `postSlackMessage` | `chat.postMessage` | `chat:write` |
|
|
1617
|
+
| `findSlackChannelByName` | `conversations.list` | `channels:read`, `groups:read` |
|
|
1618
|
+
| `lookupSlackUserIdByEmail` | `users.lookupByEmail` | `users:read.email` |
|
|
1619
|
+
|
|
1620
|
+
The bot can post and invite only in channels it belongs to. Creating a private
|
|
1621
|
+
channel makes the bot a member. An existing private channel must already
|
|
1622
|
+
include the app; a bot cannot join a private channel by itself. Store the
|
|
1623
|
+
returned channel id. Names are unique, mutable, and limited to lowercase
|
|
1624
|
+
letters, numbers, hyphens, and underscores.
|
|
1625
|
+
|
|
1626
|
+
``````typescript
|
|
1627
|
+
import {createSlackPrivateChannel, inviteSlackUsersToChannel} from "@terreno/api";
|
|
1628
|
+
|
|
1629
|
+
const channel = await createSlackPrivateChannel({
|
|
1630
|
+
name: "case-12345",
|
|
1631
|
+
userIds: ["U012ABCDEF"],
|
|
1632
|
+
initialMessage: "Case opened.",
|
|
1633
|
+
mentionUserIds: ["U012ABCDEF"],
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
await inviteSlackUsersToChannel({
|
|
1637
|
+
channelId: channel.channelId,
|
|
1638
|
+
userIds: ["U098765432"],
|
|
1639
|
+
});
|
|
1640
|
+
``````
|
|
1641
|
+
|
|
1642
|
+
A taken name throws status 409. `findSlackChannelByName({name})` returns a
|
|
1643
|
+
public channel, or a private channel the bot is already in. It returns
|
|
1644
|
+
`undefined` when that channel is not visible to the bot.
|
|
1645
|
+
|
|
1538
1646
|
### Google Chat Notifications
|
|
1539
1647
|
|
|
1540
1648
|
``````typescript
|
|
@@ -1867,7 +1975,7 @@ Complete reference of environment variables used by @terreno/api:
|
|
|
1867
1975
|
| Variable | Required | Default | Description |
|
|
1868
1976
|
|----------|----------|---------|-------------|
|
|
1869
1977
|
| `SLACK_WEBHOOKS` | No | — | JSON object mapping names to Slack webhook URLs: `{"default":"https://..."}` |
|
|
1870
|
-
| `SLACK_BOT_TOKEN` | No | — | Bot token (`xoxb-…`)
|
|
1978
|
+
| `SLACK_BOT_TOKEN` | No | — | Bot token (`xoxb-…`) for one workspace. Scopes: `users:read.email`, `groups:write`, `groups:write.invites`, `channels:write.invites`, `chat:write`, `channels:read`, `groups:read` |
|
|
1871
1979
|
| `GOOGLE_CHAT_WEBHOOKS` | No | — | JSON object mapping names to Google Chat webhook URLs |
|
|
1872
1980
|
| `ZOOM_CHAT_WEBHOOKS` | No | — | JSON object mapping names to Zoom webhook URLs |
|
|
1873
1981
|
| `WEBHOOK_SECRET` | No | — | Secret for validating incoming webhook signatures |
|