@homeflare/seat-runtime 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/index.js +11 -11
- package/dist/index.js.map +9 -9
- package/dist/mcp-tool.d.ts +1 -1
- package/dist/mcp-tool.d.ts.map +1 -1
- package/dist/mcp-toolkit.d.ts +1 -1
- package/dist/mcp-toolkit.d.ts.map +1 -1
- package/dist/mcp-toolset.d.ts +1 -1
- package/dist/mcp-toolset.d.ts.map +1 -1
- package/dist/rounds.d.ts +4 -4
- package/dist/rounds.d.ts.map +1 -1
- package/dist/seat-model.d.ts +2 -2
- package/dist/seat-model.d.ts.map +1 -1
- package/dist/seat-obs.d.ts +1 -1
- package/dist/seat-obs.d.ts.map +1 -1
- package/dist/seat-state.d.ts +5 -5
- package/dist/seat-state.d.ts.map +1 -1
- package/dist/stamp.d.ts +1 -1
- package/dist/stamp.d.ts.map +1 -1
- package/dist/state-postgres.d.ts +2 -2
- package/dist/state-postgres.d.ts.map +1 -1
- package/dist/state-valkey-send.d.ts +1 -1
- package/dist/state-valkey-send.d.ts.map +1 -1
- package/dist/state-valkey.d.ts +1 -1
- package/dist/state-valkey.d.ts.map +1 -1
- package/dist/state.js +4 -4
- package/dist/state.js.map +5 -5
- package/docs/pairing.md +3 -1
- package/docs/state.md +6 -6
- package/package.json +4 -4
- package/src/mcp-tool.ts +1 -1
- package/src/mcp-toolkit.ts +1 -1
- package/src/mcp-toolset.ts +1 -1
- package/src/rounds.ts +6 -6
- package/src/seat-model.ts +4 -4
- package/src/seat-obs.ts +2 -2
- package/src/seat-state.ts +5 -5
- package/src/stamp.ts +1 -1
- package/src/state-postgres.ts +2 -2
- package/src/state-valkey-send.ts +1 -1
- package/src/state-valkey.ts +1 -1
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -7,23 +7,23 @@ on CT100 from one environment block, the **round loop** (`runRounds`, with a har
|
|
|
7
7
|
(`SeatState`).
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
bun add @homeflare/seat-runtime effect@4.0.
|
|
10
|
+
bun add @homeflare/seat-runtime effect@4.0.1
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
⛔ **Pin the
|
|
14
|
-
and `@effect/ai-openai-compat` and `@effect/sql-pg` exact dependencies, all `4.0.
|
|
13
|
+
⛔ **Pin the exact version, and put `overrides` in YOUR root `package.json`.** `effect` is an exact peer
|
|
14
|
+
and `@effect/ai-openai-compat` and `@effect/sql-pg` exact dependencies, all `4.0.1`
|
|
15
15
|
(`@modelcontextprotocol/sdk` is an exact dependency too, `1.31.0`). If your app also
|
|
16
16
|
uses `@effect/platform-bun`, add this to your own manifest, or a fresh install crashes:
|
|
17
17
|
|
|
18
18
|
```json
|
|
19
|
-
{ "overrides": { "effect": "4.0.
|
|
19
|
+
{ "overrides": { "effect": "4.0.1", "@effect/platform-node-shared": "4.0.1" } }
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
## Use it
|
|
23
23
|
|
|
24
24
|
```ts
|
|
25
25
|
import { Effect, Layer } from 'effect';
|
|
26
|
-
import { LanguageModel } from 'effect/
|
|
26
|
+
import { LanguageModel } from 'effect/ai';
|
|
27
27
|
import { SeatModel, SeatObs } from '@homeflare/seat-runtime';
|
|
28
28
|
|
|
29
29
|
const model = SeatModel.layer({
|
|
@@ -176,7 +176,7 @@ scrubbed of quoted arguments (Postgres's is not). **No `subscribe`.** Traps, tes
|
|
|
176
176
|
|
|
177
177
|
## The measured pairing
|
|
178
178
|
|
|
179
|
-
Exact same-
|
|
179
|
+
Exact same-version pins, the `overrides` trap, rc.118's dropped `unstable/` prefix and the MCP SDK's
|
|
180
180
|
pairing, each with what was measured: [docs/pairing.md](./docs/pairing.md). ⚠️ Compat's own `.d.ts`
|
|
181
181
|
has 26 `TS2411` errors under `skipLibCheck: false` (upstream's): keep it `true`.
|
|
182
182
|
|
package/dist/index.js
CHANGED
|
@@ -25,12 +25,12 @@ __export(exports_seat_model, {
|
|
|
25
25
|
import { OpenAiClient, OpenAiEmbeddingModel, OpenAiLanguageModel } from "@effect/ai-openai-compat";
|
|
26
26
|
import * as Layer from "effect/Layer";
|
|
27
27
|
import * as Redacted from "effect/Redacted";
|
|
28
|
-
import * as EmbeddingModel from "effect/
|
|
29
|
-
import * as FetchHttpClient from "effect/
|
|
30
|
-
import * as HttpClient from "effect/
|
|
28
|
+
import * as EmbeddingModel from "effect/ai/EmbeddingModel";
|
|
29
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
30
|
+
import * as HttpClient from "effect/http/HttpClient";
|
|
31
31
|
|
|
32
32
|
// src/stamp.ts
|
|
33
|
-
import * as HttpClientRequest from "effect/
|
|
33
|
+
import * as HttpClientRequest from "effect/http/HttpClientRequest";
|
|
34
34
|
var TAG = /^[\x21-\x2b\x2d-\x7e]{1,128}$/;
|
|
35
35
|
function joinTags(tags) {
|
|
36
36
|
const list = typeof tags === "string" ? tags.split(",") : [...tags];
|
|
@@ -106,13 +106,13 @@ import * as ConfigProvider from "effect/ConfigProvider";
|
|
|
106
106
|
import * as Effect from "effect/Effect";
|
|
107
107
|
import * as Layer2 from "effect/Layer";
|
|
108
108
|
import * as Schema from "effect/Schema";
|
|
109
|
-
import * as FetchHttpClient2 from "effect/
|
|
109
|
+
import * as FetchHttpClient2 from "effect/http/FetchHttpClient";
|
|
110
110
|
import {
|
|
111
111
|
OtlpLogger,
|
|
112
112
|
OtlpMetrics,
|
|
113
113
|
OtlpSerialization,
|
|
114
114
|
OtlpTracer
|
|
115
|
-
} from "effect/
|
|
115
|
+
} from "effect/observability";
|
|
116
116
|
var CT100_ENDPOINTS = {
|
|
117
117
|
traces: "http://10.100.1.4:10428/insert/opentelemetry/v1/traces",
|
|
118
118
|
logs: "http://10.100.1.4:9428/insert/opentelemetry/v1/logs",
|
|
@@ -141,7 +141,7 @@ var layer4 = Layer2.mergeAll(OtlpTracer.layerFromConfig(), OtlpLogger.layerFromC
|
|
|
141
141
|
// src/rounds.ts
|
|
142
142
|
import * as Effect2 from "effect/Effect";
|
|
143
143
|
import * as Metric from "effect/Metric";
|
|
144
|
-
import * as Prompt from "effect/
|
|
144
|
+
import * as Prompt from "effect/ai/Prompt";
|
|
145
145
|
var isToolRefusal = (error) => error.reason._tag === "ToolNotFoundError" || error.reason._tag === "InvalidOutputError" && error.module === "LanguageModel";
|
|
146
146
|
var roundsTotal = Metric.counter("seat_rounds_total", {
|
|
147
147
|
description: "Model turns taken by runRounds that returned; a refused forced final turn is not counted."
|
|
@@ -269,7 +269,7 @@ class McpToolkitError extends Error {
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
// src/version.ts
|
|
272
|
-
var VERSION = "0.1.
|
|
272
|
+
var VERSION = "0.1.1";
|
|
273
273
|
|
|
274
274
|
// src/mcp-connect.ts
|
|
275
275
|
var DEFAULT_CONNECT_TIMEOUT_MS = 15000;
|
|
@@ -342,7 +342,7 @@ function collect(operation, server, page, redact) {
|
|
|
342
342
|
|
|
343
343
|
// src/mcp-toolset.ts
|
|
344
344
|
import * as Effect5 from "effect/Effect";
|
|
345
|
-
import * as Toolkit from "effect/
|
|
345
|
+
import * as Toolkit from "effect/ai/Toolkit";
|
|
346
346
|
|
|
347
347
|
// src/mcp-render.ts
|
|
348
348
|
var EMPTY_RESULT = "(no content)";
|
|
@@ -385,7 +385,7 @@ function renderResult(result) {
|
|
|
385
385
|
// src/mcp-tool.ts
|
|
386
386
|
import * as Schema2 from "effect/Schema";
|
|
387
387
|
import * as SchemaGetter from "effect/SchemaGetter";
|
|
388
|
-
import * as Tool from "effect/
|
|
388
|
+
import * as Tool from "effect/ai/Tool";
|
|
389
389
|
function declaredParameters(inputSchema) {
|
|
390
390
|
const properties = inputSchema["properties"];
|
|
391
391
|
const names = typeof properties === "object" && properties !== null ? Object.keys(properties) : [];
|
|
@@ -504,5 +504,5 @@ export {
|
|
|
504
504
|
runRounds
|
|
505
505
|
};
|
|
506
506
|
|
|
507
|
-
//# debugId=
|
|
507
|
+
//# debugId=AE11F30DA17DC80F64756E2164756E21
|
|
508
508
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/seat-model.ts", "../src/stamp.ts", "../src/seat-obs.ts", "../src/rounds.ts", "../src/mcp-toolkit.ts", "../src/mcp-connect.ts", "../src/mcp-error.ts", "../src/version.ts", "../src/mcp-pages.ts", "../src/mcp-toolset.ts", "../src/mcp-render.ts", "../src/mcp-tool.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * LiteLLM as an Effect `LanguageModel` and `EmbeddingModel`, on the chat-completions wire.\n *\n * ★ ONE CLIENT SHAPE FOR EVERY SEAT. `@effect/ai-openai-compat` builds the HTTP client;\n * this file adds only what LiteLLM needs to attribute and control a seat's calls (tags,\n * the response cache, retries, metadata — see stamp.ts) and pins `strictJsonSchema: false`.\n * ⛔ THE COMPAT PACKAGE POSTS `/chat/completions` AND `/embeddings`, NOT `/responses`. The\n * responses wire fails on cf-code with `missing field sequence_number` (landscape PR 165),\n * so nothing here points at it.\n */\nimport { OpenAiClient, OpenAiEmbeddingModel, OpenAiLanguageModel } from '@effect/ai-openai-compat';\nimport * as Layer from 'effect/Layer';\nimport * as Redacted from 'effect/Redacted';\nimport * as EmbeddingModel from 'effect/
|
|
6
|
-
"/**\n * The per-request marks a seat puts on every LiteLLM call, applied as an `HttpClient`\n * transform because `@effect/ai-openai-compat` gives no other place to set them.\n *\n * ★ WHY A TRANSFORM AND NOT MODEL CONFIG. The compat mapper forwards unknown config keys\n * into the body but DROPS `metadata` (it is a known Responses-API field with no\n * chat-completions place), and no config key can set a header. Measured in\n * packages/seat-runtime/tests/stamp.test.ts against the installed rc.115.\n * ★ IT IS THE SAME TRANSFORM AS cf-harness (landscape PR 165, cf-harness/src/request.ts),\n * which is where the header and the body fields were measured against LiteLLM.\n */\nimport * as HttpClientRequest from 'effect/
|
|
7
|
-
"/**\n * Traces, logs and metrics from any Effect seat to VictoriaMetrics on CT100, from one\n * environment block.\n *\n * ★ `layerFromConfig`, NOT `Otlp.layer`. The combined layer takes ONE base URL and appends\n * `/v1/traces` and friends, and the three Victoria services each mount OTLP at a different\n * path (measured 2026-09-29, README). The per-signal layers read the per-signal env the\n * Claude Code seats already carry, so one block wires every seat.\n * ⛔ WITH NO ENVIRONMENT `layerFromConfig` EXPORTS NOTHING, SILENTLY. It returns a bare\n * flusher unless `OTEL_<SIGNAL>_EXPORTER` names `otlp` and an endpoint is set. So the CT100\n * defaults below are not a convenience, they are what makes this layer emit at all.\n */\nimport * as Config from 'effect/Config';\nimport * as ConfigProvider from 'effect/ConfigProvider';\nimport * as Effect from 'effect/Effect';\nimport * as Layer from 'effect/Layer';\nimport * as Schema from 'effect/Schema';\nimport * as FetchHttpClient from 'effect/
|
|
8
|
-
"/**\n * The one round loop every seat shares: model turn, tool results, model turn, until the model\n * stops asking for tools or the cap fires — and a cap that fires is followed by ONE more turn\n * with the toolkit taken away, so a run ends with an answer instead of a truncated transcript.\n *\n * ★ WHAT THE SDK DOES AND DOES NOT DO. `Chat.generateText` with a toolkit resolves the tool\n * calls of ONE model turn and returns; it never re-prompts, and Effect AI ships no\n * `maxRounds` or `stopWhen` (grep of LanguageModel.d.ts and Chat.d.ts, 2026-09-29). The\n * documented multi-round agent is a `while` over `session.generateText({ prompt: [], toolkit })`\n * (compat's ai-docs, 30_chat.ts). This file is that `while`, once, with the cap.\n * ⛔ THE CAP IS HARD AND FINITE. `maxRounds` must be a positive integer: `Infinity`, `0`, a\n * fraction or `NaN` is a programming error and dies with a `RangeError` before any model\n * call, because a loop a bad config can un-cap is a loop that spends until something else\n * stops it (LiteLLM's budget hit the claude2 fleet that way, 2026-09-28).\n * ⚠️ THE FORCED TURN SENDS NO TOOLS. No toolkit, and `toolChoice: 'none'` stated anyway: the SDK\n * already defaults `toolChoice` to `'none'` for a call with no toolkit (LanguageModel.js,\n * `providerOptions`, measured by mutation 2026-09-29: dropping the option changed no test), so\n * it is kept for what it says and for the `toolChoice` attribute on the turn's span. compat\n * then sends neither `tools` nor `tool_choice` (prepareTools returns both undefined for an\n * empty tool list, rc.115), because an OpenAI-shaped API refuses a `tool_choice` with no\n * `tools`. The history still carries the earlier tool calls and results, and cf-code accepted\n * exactly that: one live call through CT100's LiteLLM (:4100), 2026-09-29, a history of\n * system, user, assistant tool call and tool result, no `tools`, `toolChoice: 'none'`, came\n * back `finishReason: 'stop'` with text and no tool call. One sample, one alias.\n * ⚠️ `capped` MEANS THE MODEL STILL WANTED TOOLS after `maxRounds` tool rounds. A model that\n * answers on round `maxRounds` exactly is `capped: false`: the cap did not fire.\n * 🔴 THE FORCED TURN CAN BE REFUSED, AND THEN THE RUN DOES NOT DIE OF IT. A provider that asks for\n * a tool although none was offered (a gateway that adds a dummy tool for a history full of\n * tool calls does exactly that) answers a turn the SDK cannot use, and the SDK says so with one\n * of TWO `AiError` reasons, depending on WHO rejects the tool call:\n * - `ToolNotFoundError`: @effect/ai-openai-compat (`SeatModel`'s provider) rejects it first,\n * while it maps the reply (`transformToolCallParams`: \"Tool ... not found. Available tools:\n * none\"). Measured 2026-09-29 (review of PR 328, of the round 2 fix), `SeatModel` against a loopback\n * LiteLLM stand-in that answered every call with a tool call: 3 model calls, tools offered\n * [true, true, false], then the whole run failed with every round already spent. Round 2's\n * fix caught only the reason below and so did nothing for this provider.\n * - `InvalidOutputError` raised by `LanguageModel` (module `LanguageModel`): the SDK's own decode\n * of the provider's parts (\"Expected ... text | reasoning ...\"), reached by a provider that hands\n * the SDK a tool-call part itself (the scripted model in tests/fake-model.ts). The same reason\n * raised by `OpenAiClient` (an empty, truncated or non-completion body, a dropped stream) is NOT\n * a refusal and fails the run (tests/seat-broken-forced.test.ts).\n * Either is caught: the result is `capped` and `unanswered`, its `response` is the LAST TOOL\n * ROUND's (whose calls did run), `rounds` stays `maxRounds`, and a warning, a span error and\n * `seat_rounds_unanswered_total` say what happened. Any OTHER failure of the forced turn\n * (network, rate limit, a handler) still fails the run.\n */\nimport * as Effect from 'effect/Effect';\nimport * as Metric from 'effect/Metric';\nimport type * as Chat from 'effect/unstable/ai/Chat';\nimport type * as LanguageModel from 'effect/unstable/ai/LanguageModel';\nimport * as Prompt from 'effect/unstable/ai/Prompt';\nimport type * as Tool from 'effect/unstable/ai/Tool';\nimport type { AiError } from 'effect/unstable/ai/AiError';\nimport type * as Toolkit from 'effect/unstable/ai/Toolkit';\n\n/**\n * A tool call the forced turn cannot use. `ToolNotFoundError` is compat's own rejection (its reply\n * mapping); `InvalidOutputError` counts ONLY when `LanguageModel` raised it (the SDK's decode of a\n * provider that handed it a tool-call part). compat raises the same reason, from `OpenAiClient`, for\n * a 200 with an empty, non-JSON, truncated or non-completion body and for a body stream that dies\n * mid-read: those are gateway or network failures and must fail the run, not read as a refusal\n * (review of PR 328, round 4; tests/seat-broken-forced.test.ts).\n */\nconst isToolRefusal = (error: AiError): boolean =>\n error.reason._tag === 'ToolNotFoundError' ||\n (error.reason._tag === 'InvalidOutputError' && error.module === 'LanguageModel');\n\n/** Every model turn, forced or not. */\nconst roundsTotal = Metric.counter('seat_rounds_total', {\n description:\n 'Model turns taken by runRounds that returned; a refused forced final turn is not counted.',\n});\n/** Runs whose cap fired: the interesting number, because each one is an agent that did not finish. */\nconst roundsCapped = Metric.counter('seat_rounds_capped_total', {\n description: 'runRounds runs that hit maxRounds and were given a forced final turn.',\n});\n\n/** Runs whose forced final turn was refused: the model still asked for a tool. */\nconst roundsUnanswered = Metric.counter('seat_rounds_unanswered_total', {\n description: 'runRounds runs whose forced final turn came back as a tool call, not an answer.',\n});\n\n/**\n * A turn's response: with the toolkit, or the forced one without.\n * ⚠️ The two modes differ because the SDK's own types do: a call with a toolkit answers\n * `'opaque'` tool parameters, a call without one answers the default `'decoded'`.\n */\ntype Response<Tools extends Record<string, Tool.Any>> =\n | LanguageModel.GenerateTextResponse<Tools, 'opaque'>\n | LanguageModel.GenerateTextResponse<{}, 'decoded'>;\n\n/** One model turn, as `onRound` and the result see it. A refused forced final turn is not reported. */\nexport type Round<Tools extends Record<string, Tool.Any>> = {\n /** 1-based. The forced final turn, when there is one, is `maxRounds + 1`. */\n readonly round: number;\n /** True only for the forced final turn: no toolkit, `toolChoice: 'none'`. */\n readonly forced: boolean;\n readonly response: Response<Tools>;\n};\n\nexport type RoundsOptions<Tools extends Record<string, Tool.Any>> = {\n /**\n * The conversation, already holding the opening prompt (`Chat.fromPrompt`) or a resumed\n * history that ends in a user message or tool results. Every round sends an EMPTY prompt:\n * `Chat` appends the model's turn and its tool results to `history` itself.\n */\n readonly chat: Chat.Chat;\n /**\n * Tools WITH their handlers. A `Toolkit.make(...)` is an Effect that needs its handlers from\n * context: `yield*` it where they are provided, and pass the result. (Typing it as the\n * Effect would hide its requirements, which `generateText` itself cannot track either.)\n */\n readonly toolkit: Toolkit.WithHandler<Tools>;\n /** The most tool rounds allowed. A positive integer; see the header. */\n readonly maxRounds: number;\n /** After every model turn, the forced one included. Runs inside that turn's span. */\n readonly onRound?: ((round: Round<Tools>) => Effect.Effect<void>) | undefined;\n};\n\nexport type RoundsResult<Tools extends Record<string, Tool.Any>> = {\n /** The last model turn: the answer, or the forced final turn when the cap fired. */\n readonly response: Response<Tools>;\n /** Model turns that returned, the forced one included when it did. */\n readonly rounds: number;\n /** The cap fired and `response` is the forced final turn (or see `unanswered`). */\n readonly capped: boolean;\n /**\n * The forced turn was refused (a provider that still asks for a tool) and `response` is the\n * last TOOL round's: it holds no answer, and its tool calls ran. Only ever true with `capped`;\n * `rounds` then counts the turns that returned, so it is `maxRounds`. See the header.\n */\n readonly unanswered: boolean;\n};\n\n/**\n * Run the loop. Fails with whatever a model turn fails with (`AiError`, a tool handler's own\n * failure); nothing is retried here — the caller wraps `Effect.retry` where it wants one.\n */\nexport function runRounds<Tools extends Record<string, Tool.Any>>(\n options: RoundsOptions<Tools>,\n): Effect.Effect<\n RoundsResult<Tools>,\n LanguageModel.ExtractError<{ readonly toolkit: Toolkit.WithHandler<Tools> }>,\n | LanguageModel.LanguageModel\n | LanguageModel.ExtractServices<{ readonly toolkit: Toolkit.WithHandler<Tools> }>\n> {\n const { chat, toolkit, maxRounds, onRound } = options;\n return Effect.gen(function* () {\n if (!Number.isInteger(maxRounds) || maxRounds < 1) {\n return yield* Effect.die(\n new RangeError(`runRounds: maxRounds must be a positive integer, got ${String(maxRounds)}`),\n );\n }\n const finish = (round: number, forced: boolean) =>\n Effect.fnUntraced(function* (response: Response<Tools>) {\n yield* Metric.update(roundsTotal, 1);\n yield* Effect.annotateCurrentSpan({ 'seat.tool_calls': response.toolCalls.length });\n if (onRound !== undefined) yield* onRound({ round, forced, response });\n return response;\n });\n const span = (round: number, forced: boolean) =>\n Effect.withSpan('seat.round', {\n attributes: { 'seat.round': round, 'seat.round.forced': forced },\n });\n\n const turn = (round: number) =>\n chat\n .generateText({ prompt: Prompt.empty, toolkit })\n .pipe(Effect.flatMap(finish(round, false)), span(round, false));\n\n let round = 1;\n let response: Response<Tools> = yield* turn(round);\n // ★ The exit test is the model's: no tool calls means it has answered.\n while (response.toolCalls.length > 0 && round < maxRounds) {\n round += 1;\n response = yield* turn(round);\n }\n if (response.toolCalls.length === 0) {\n return { response, rounds: round, capped: false, unanswered: false };\n }\n\n yield* Metric.update(roundsCapped, 1);\n yield* Effect.logWarning('seat round cap reached; forcing a final turn without tools', {\n maxRounds,\n });\n const forcedRound = maxRounds + 1;\n /** The forced turn came back as a tool call: counted and logged, and the run goes on. */\n const refused = () =>\n Effect.as(\n Effect.all([\n Metric.update(roundsUnanswered, 1),\n Effect.logWarning('seat forced final turn refused: the model still asked for a tool', {\n maxRounds,\n }),\n ]),\n undefined,\n );\n const forced = yield* chat.generateText({ prompt: Prompt.empty, toolChoice: 'none' }).pipe(\n Effect.flatMap(finish(forcedRound, true)),\n span(forcedRound, true),\n // ⚠️ Only a tool call nobody offered: see the header for who raises which. Every other AiError\n // (a dropped connection, an empty or truncated body, a rate limit) still fails the run.\n Effect.catchTag('AiError', (error) =>\n isToolRefusal(error) ? refused() : Effect.fail(error),\n ),\n );\n return forced === undefined\n ? { response, rounds: maxRounds, capped: true, unanswered: true }\n : { response: forced, rounds: forcedRound, capped: true, unanswered: false };\n });\n}\n",
|
|
9
|
-
"/**\n * An MCP server's tools as an Effect AI `Toolkit`, and its resources as two Effects, through\n * the official MCP TypeScript SDK (`Client` over `StreamableHTTPClientTransport`).\n *\n * ★ THE SDK'S CLIENT IS THE CLIENT. Effect AI ships the SERVER half of MCP (`McpServer`,\n * `McpSchema`); no client (walked down 2026-09-29, docs/plans/2026-09-29-cf-seat-sdk-spike.md\n * open item 2). Wrapping the official client as dynamic tools is that plan's smallest option,\n * and the scout measured the pairing: list, call and resource read against an Effect\n * `McpServer` (pair115/mcp.ts).\n * ★ EACH MCP TOOL IS A `Tool.dynamic` WITH THE SERVER'S OWN JSON SCHEMA (mcp-tool.ts). The schema\n * goes to the model as the server wrote it, and no Effect Schema is built from its types: the\n * only client-side check is that the declared `required` arguments are present, and the server\n * validates the rest and its complaint comes back to the model.\n * ⚠️ A TOOL FAILURE IS THE MODEL'S TO SEE, NOT THE RUN'S TO DIE OF. Every tool is\n * `failureMode: 'return'`: an `isError` result, a JSON-RPC error (bad arguments, unknown\n * tool) and a call that never completed all come back to the model as the tool's result, so\n * it can correct itself or answer without the tool. MCP specifies errors-in-results for\n * exactly this. Connecting and listing are different: nothing works without them, so they\n * fail with `McpToolkitError`.\n * ⚠️ THE TOOL LIST IS A SNAPSHOT taken at connect time; a server's `listChanged` notification is\n * not followed. Reconnect for a fresh list.\n * ⛔ HEADERS ARE CREDENTIALS. They ride `requestInit` to the server and nowhere else: not in an\n * error, a span or a log (mcp-error.ts). Pass a bearer value as `Redacted` to keep it out of\n * an accidental print of the argument, as `SeatModel` does with the API key.\n */\nimport type { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport * as Effect from 'effect/Effect';\nimport type * as Scope from 'effect/Scope';\nimport type * as Toolkit from 'effect/
|
|
5
|
+
"/**\n * LiteLLM as an Effect `LanguageModel` and `EmbeddingModel`, on the chat-completions wire.\n *\n * ★ ONE CLIENT SHAPE FOR EVERY SEAT. `@effect/ai-openai-compat` builds the HTTP client;\n * this file adds only what LiteLLM needs to attribute and control a seat's calls (tags,\n * the response cache, retries, metadata — see stamp.ts) and pins `strictJsonSchema: false`.\n * ⛔ THE COMPAT PACKAGE POSTS `/chat/completions` AND `/embeddings`, NOT `/responses`. The\n * responses wire fails on cf-code with `missing field sequence_number` (landscape PR 165),\n * so nothing here points at it.\n */\nimport { OpenAiClient, OpenAiEmbeddingModel, OpenAiLanguageModel } from '@effect/ai-openai-compat';\nimport * as Layer from 'effect/Layer';\nimport * as Redacted from 'effect/Redacted';\nimport * as EmbeddingModel from 'effect/ai/EmbeddingModel';\nimport type * as LanguageModel from 'effect/ai/LanguageModel';\nimport * as FetchHttpClient from 'effect/http/FetchHttpClient';\nimport * as HttpClient from 'effect/http/HttpClient';\nimport { joinTags, stampRequest } from './stamp.ts';\n\nexport type SeatClientOptions = {\n /** LiteLLM's OpenAI-compatible base, e.g. `http://127.0.0.1:4100/v1`. Required: no host default. */\n readonly apiUrl: string;\n /** A per-seat LiteLLM virtual key. Held `Redacted`; this package never logs or reads it from disk. */\n readonly apiKey: string | Redacted.Redacted<string>;\n /** Spend-log attribution, e.g. `['host:ct100', 'lane:cfcode', 'seat:cf-coding']`. */\n readonly tags: string | ReadonlyArray<string>;\n /**\n * Skip LiteLLM's response cache, read and write. Defaults to TRUE.\n * ⚠️ LiteLLM caches every completion for every key, so a seat that repeats a call gets the\n * old answer back at a tenth of the latency — an agent loop would replay itself.\n */\n readonly noCache?: boolean | undefined;\n /** Written into the request body only when the body carries none (compat drops it). */\n readonly metadata?: Readonly<Record<string, string>> | undefined;\n};\n\n/** Compat's own per-model config (`temperature`, `max_output_tokens`, custom body keys). */\ntype ModelConfig = NonNullable<Parameters<typeof OpenAiLanguageModel.layer>[0]['config']>;\ntype EmbeddingConfig = NonNullable<Parameters<typeof OpenAiEmbeddingModel.layer>[0]['config']>;\n\nexport type SeatModelOptions = SeatClientOptions & {\n /** The LiteLLM alias, e.g. `cf-code`. */\n readonly model: string;\n readonly config?: ModelConfig | undefined;\n};\n\nexport type SeatEmbeddingOptions = SeatClientOptions & {\n /** The LiteLLM alias, e.g. `embeddings`. */\n readonly model: string;\n /**\n * What `EmbeddingModel.Dimensions` reports to a vector store. NOT sent to LiteLLM.\n * ⚠️ compat's own `OpenAiEmbeddingModel.model(name, { dimensions })` puts the number in the\n * request body too, and a provider that has no `dimensions` parameter (a llama.cpp bge-m3\n * behind LiteLLM) can refuse it. Pass `config: { dimensions }` to send it deliberately.\n */\n readonly dimensions: number;\n readonly config?: EmbeddingConfig | undefined;\n};\n\n/** The client: bearer key, tag header, cache and retry fields, over `fetch`. */\nexport function clientLayer(options: SeatClientOptions): Layer.Layer<OpenAiClient.OpenAiClient> {\n const stamp = {\n tags: joinTags(options.tags),\n noCache: options.noCache ?? true,\n metadata: options.metadata,\n };\n return OpenAiClient.layer({\n apiKey: Redacted.isRedacted(options.apiKey) ? options.apiKey : Redacted.make(options.apiKey),\n apiUrl: options.apiUrl.replace(/\\/$/, ''),\n transformClient: (http) =>\n HttpClient.mapRequest(http, (request) => stampRequest(request, stamp)),\n }).pipe(Layer.provide(FetchHttpClient.layer));\n}\n\n/** `LanguageModel` for one LiteLLM alias. */\nexport function layer(options: SeatModelOptions): Layer.Layer<LanguageModel.LanguageModel> {\n return OpenAiLanguageModel.layer({\n model: options.model,\n // ★ cf-review's default, and measured here 2026-09-29: compat sends `strict: true` on every\n // tool schema unless told otherwise, which the seats' previous client never did; this\n // sends `strict: false`. A caller's `config` wins.\n config: { strictJsonSchema: false, ...options.config },\n }).pipe(Layer.provide(clientLayer(options)));\n}\n\n/** `EmbeddingModel` (and its `Dimensions`) for one LiteLLM alias. */\nexport function embeddingLayer(\n options: SeatEmbeddingOptions,\n): Layer.Layer<EmbeddingModel.EmbeddingModel | EmbeddingModel.Dimensions> {\n return Layer.merge(\n OpenAiEmbeddingModel.layer({ model: options.model, config: options.config }),\n Layer.succeed(EmbeddingModel.Dimensions, options.dimensions),\n ).pipe(Layer.provide(clientLayer(options)));\n}\n",
|
|
6
|
+
"/**\n * The per-request marks a seat puts on every LiteLLM call, applied as an `HttpClient`\n * transform because `@effect/ai-openai-compat` gives no other place to set them.\n *\n * ★ WHY A TRANSFORM AND NOT MODEL CONFIG. The compat mapper forwards unknown config keys\n * into the body but DROPS `metadata` (it is a known Responses-API field with no\n * chat-completions place), and no config key can set a header. Measured in\n * packages/seat-runtime/tests/stamp.test.ts against the installed rc.115.\n * ★ IT IS THE SAME TRANSFORM AS cf-harness (landscape PR 165, cf-harness/src/request.ts),\n * which is where the header and the body fields were measured against LiteLLM.\n */\nimport * as HttpClientRequest from 'effect/http/HttpClientRequest';\n\nexport type SeatStamp = {\n /** Already validated and joined: LiteLLM reads one comma-separated header. */\n readonly tags: string;\n readonly noCache: boolean;\n readonly metadata: Readonly<Record<string, string>> | undefined;\n};\n\n/**\n * ⛔ A TAG IS A TOKEN, NOT A SENTENCE. LiteLLM splits `x-litellm-tags` on commas and this\n * package joins with them, so a comma inside a tag would silently become two tags, and a\n * control character in a header value is header injection. Printable ASCII, no comma, no\n * space; `host:ct100` and `seat:cf-coding` are the shape.\n */\nconst TAG = /^[\\x21-\\x2b\\x2d-\\x7e]{1,128}$/;\n\n/** Validate and join. Throws at layer construction, where the mistake is one line away. */\nexport function joinTags(tags: string | ReadonlyArray<string>): string {\n const list = typeof tags === 'string' ? tags.split(',') : [...tags];\n if (list.length === 0) throw new TypeError('seat tags: at least one tag is required');\n for (const tag of list) {\n if (!TAG.test(tag)) {\n throw new TypeError(\n `seat tags: ${JSON.stringify(tag)} is not a tag (1-128 printable ASCII, no comma or space)`,\n );\n }\n }\n return list.join(',');\n}\n\n/** The JSON body as an object, or undefined for anything this must leave alone. */\nfunction jsonObject(\n request: HttpClientRequest.HttpClientRequest,\n): Record<string, unknown> | undefined {\n const body = request.body;\n if (body._tag !== 'Uint8Array' || !body.contentType.includes('json')) return undefined;\n // ⚠️ `text` is only \"the original text retained for adapters that can skip encoding\"\n // (HttpBody.d.ts): a body built from bytes has none. Decoding the bytes keeps the stamp\n // from becoming a silent no-op, which is the failure that would leave the LiteLLM cache\n // ON for a seat that asked for it off.\n const text = body.text ?? new TextDecoder().decode(body.body);\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return undefined;\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined;\n return parsed as Record<string, unknown>;\n}\n\n/** Headers and body fields for one request. Pure: the same request in, a new one out. */\nexport function stampRequest(\n request: HttpClientRequest.HttpClientRequest,\n stamp: SeatStamp,\n): HttpClientRequest.HttpClientRequest {\n const headers = stamp.noCache\n ? { 'x-litellm-tags': stamp.tags, 'cache-control': 'no-cache, no-store' }\n : { 'x-litellm-tags': stamp.tags };\n const tagged = HttpClientRequest.setHeaders(request, headers);\n const body = jsonObject(request);\n if (body === undefined) return tagged;\n return HttpClientRequest.bodyJsonUnsafe(tagged, {\n ...body,\n // ⚠️ THE HEADER ALONE DOES NOT TURN THE CACHE OFF. Measured 2026-09-25 (cf-review PR 22,\n // LiteLLM 1.100.0): `cache: {\"no-cache\": true}` skips the read and `\"no-store\": true`\n // skips the write; `caching: false` in the body is a no-op, and a repeat answered from\n // the cache reads as agreement. The Cache-Control header is sent as well, for the\n // Cloudflare AI Gateway below LiteLLM, and is not what LiteLLM honours.\n ...(stamp.noCache ? { cache: { 'no-cache': true, 'no-store': true } } : {}),\n // ★ ZERO, ALWAYS. A seat retries in Effect, where the attempt is a span; LiteLLM retrying\n // behind it would multiply attempts invisibly (and bill each one).\n num_retries: 0,\n ...(stamp.metadata !== undefined && !('metadata' in body) ? { metadata: stamp.metadata } : {}),\n });\n}\n",
|
|
7
|
+
"/**\n * Traces, logs and metrics from any Effect seat to VictoriaMetrics on CT100, from one\n * environment block.\n *\n * ★ `layerFromConfig`, NOT `Otlp.layer`. The combined layer takes ONE base URL and appends\n * `/v1/traces` and friends, and the three Victoria services each mount OTLP at a different\n * path (measured 2026-09-29, README). The per-signal layers read the per-signal env the\n * Claude Code seats already carry, so one block wires every seat.\n * ⛔ WITH NO ENVIRONMENT `layerFromConfig` EXPORTS NOTHING, SILENTLY. It returns a bare\n * flusher unless `OTEL_<SIGNAL>_EXPORTER` names `otlp` and an endpoint is set. So the CT100\n * defaults below are not a convenience, they are what makes this layer emit at all.\n */\nimport * as Config from 'effect/Config';\nimport * as ConfigProvider from 'effect/ConfigProvider';\nimport * as Effect from 'effect/Effect';\nimport * as Layer from 'effect/Layer';\nimport * as Schema from 'effect/Schema';\nimport * as FetchHttpClient from 'effect/http/FetchHttpClient';\nimport {\n type OtlpExporter,\n OtlpLogger,\n OtlpMetrics,\n OtlpSerialization,\n OtlpTracer,\n} from 'effect/observability';\n\n/**\n * CT100's Victoria services, one path each. Verified 2026-09-29 by GET against the live\n * ports (no payload sent): a mounted path answers, an unmounted sibling answers\n * `unsupported path requested`; the counters carry `format=\"protobuf\"` for traces and logs.\n */\nexport const CT100_ENDPOINTS: {\n readonly traces: string;\n readonly logs: string;\n readonly metrics: string;\n} = {\n traces: 'http://10.100.1.4:10428/insert/opentelemetry/v1/traces',\n logs: 'http://10.100.1.4:9428/insert/opentelemetry/v1/logs',\n metrics: 'http://10.100.1.4:8428/opentelemetry/v1/metrics',\n};\n\n/**\n * Shown when a process names itself neither with `OTEL_SERVICE_NAME` nor with a `service.name`\n * in `OTEL_RESOURCE_ATTRIBUTES`; set one per seat.\n */\nexport const DEFAULT_SERVICE_NAME = 'seat-runtime';\n\n/**\n * `OTEL_RESOURCE_ATTRIBUTES` read the way Effect reads it (`OtlpResource.fromConfig`, rc.115:\n * `key=value` pairs, both sides URI-decoded), so \"has a `service.name`\" means what Effect means.\n */\nconst resourceAttributes = Config.Record(\n Schema.StringFromUriComponent,\n Schema.StringFromUriComponent,\n 'OTEL_RESOURCE_ATTRIBUTES',\n).pipe(Config.withDefault(undefined));\n\n/**\n * The fallback config source, computed against the CURRENT provider.\n *\n * ⛔ EVERYTHING HERE IS A FALLBACK. The environment is tried first, so `OTEL_SDK_DISABLED=true`,\n * `OTEL_TRACES_EXPORTER=none` and every endpoint the operator sets win.\n * ⚠️ THE PER-SIGNAL DEFAULTS STEP ASIDE FOR A BASE ENDPOINT. Effect reads\n * `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` and only then `OTEL_EXPORTER_OTLP_ENDPOINT`, so a\n * per-signal default would shadow an operator's base URL and send their traces to CT100.\n * ⚠️ THE SERVICE NAME STEPS ASIDE FOR `service.name` IN `OTEL_RESOURCE_ATTRIBUTES` FOR THE SAME\n * REASON. Effect resolves `OTEL_SERVICE_NAME`, then that attribute, then fails, so an injected\n * `OTEL_SERVICE_NAME` would win over the operator's attribute and Effect then drops the\n * attribute, leaving their seat mislabelled `seat-runtime` in Victoria with nothing to say why.\n */\nconst defaults: Effect.Effect<ConfigProvider.ConfigProvider> = Effect.gen(function* () {\n const current = yield* ConfigProvider.ConfigProvider;\n const base = yield* current.load(['OTEL_EXPORTER_OTLP_ENDPOINT']);\n const attributes = yield* resourceAttributes.parse(current);\n const named = attributes?.['service.name'] !== undefined;\n return ConfigProvider.fromUnknown({\n OTEL_TRACES_EXPORTER: 'otlp',\n OTEL_LOGS_EXPORTER: 'otlp',\n OTEL_METRICS_EXPORTER: 'otlp',\n ...(named ? {} : { OTEL_SERVICE_NAME: DEFAULT_SERVICE_NAME }),\n ...(base === undefined\n ? {\n OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: CT100_ENDPOINTS.traces,\n OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: CT100_ENDPOINTS.logs,\n OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: CT100_ENDPOINTS.metrics,\n }\n : {}),\n });\n}).pipe(Effect.orDie);\n\n/**\n * OTLP tracer, logger and metrics over `fetch`, protobuf on the wire (what VictoriaTraces,\n * VictoriaLogs and VictoriaMetrics ingest, and what the Claude Code seats already send).\n */\nexport const layer: Layer.Layer<OtlpExporter.Flusher> = Layer.mergeAll(\n OtlpTracer.layerFromConfig(),\n OtlpLogger.layerFromConfig(),\n OtlpMetrics.layerFromConfig(),\n).pipe(\n Layer.provide(OtlpSerialization.layerProtobuf),\n Layer.provide(FetchHttpClient.layer),\n Layer.provide(ConfigProvider.layerAdd(defaults)),\n);\n",
|
|
8
|
+
"/**\n * The one round loop every seat shares: model turn, tool results, model turn, until the model\n * stops asking for tools or the cap fires — and a cap that fires is followed by ONE more turn\n * with the toolkit taken away, so a run ends with an answer instead of a truncated transcript.\n *\n * ★ WHAT THE SDK DOES AND DOES NOT DO. `Chat.generateText` with a toolkit resolves the tool\n * calls of ONE model turn and returns; it never re-prompts, and Effect AI ships no\n * `maxRounds` or `stopWhen` (grep of LanguageModel.d.ts and Chat.d.ts, 2026-09-29). The\n * documented multi-round agent is a `while` over `session.generateText({ prompt: [], toolkit })`\n * (compat's ai-docs, 30_chat.ts). This file is that `while`, once, with the cap.\n * ⛔ THE CAP IS HARD AND FINITE. `maxRounds` must be a positive integer: `Infinity`, `0`, a\n * fraction or `NaN` is a programming error and dies with a `RangeError` before any model\n * call, because a loop a bad config can un-cap is a loop that spends until something else\n * stops it (LiteLLM's budget hit the claude2 fleet that way, 2026-09-28).\n * ⚠️ THE FORCED TURN SENDS NO TOOLS. No toolkit, and `toolChoice: 'none'` stated anyway: the SDK\n * already defaults `toolChoice` to `'none'` for a call with no toolkit (LanguageModel.js,\n * `providerOptions`, measured by mutation 2026-09-29: dropping the option changed no test), so\n * it is kept for what it says and for the `toolChoice` attribute on the turn's span. compat\n * then sends neither `tools` nor `tool_choice` (prepareTools returns both undefined for an\n * empty tool list, rc.115), because an OpenAI-shaped API refuses a `tool_choice` with no\n * `tools`. The history still carries the earlier tool calls and results, and cf-code accepted\n * exactly that: one live call through CT100's LiteLLM (:4100), 2026-09-29, a history of\n * system, user, assistant tool call and tool result, no `tools`, `toolChoice: 'none'`, came\n * back `finishReason: 'stop'` with text and no tool call. One sample, one alias.\n * ⚠️ `capped` MEANS THE MODEL STILL WANTED TOOLS after `maxRounds` tool rounds. A model that\n * answers on round `maxRounds` exactly is `capped: false`: the cap did not fire.\n * 🔴 THE FORCED TURN CAN BE REFUSED, AND THEN THE RUN DOES NOT DIE OF IT. A provider that asks for\n * a tool although none was offered (a gateway that adds a dummy tool for a history full of\n * tool calls does exactly that) answers a turn the SDK cannot use, and the SDK says so with one\n * of TWO `AiError` reasons, depending on WHO rejects the tool call:\n * - `ToolNotFoundError`: @effect/ai-openai-compat (`SeatModel`'s provider) rejects it first,\n * while it maps the reply (`transformToolCallParams`: \"Tool ... not found. Available tools:\n * none\"). Measured 2026-09-29 (review of PR 328, of the round 2 fix), `SeatModel` against a loopback\n * LiteLLM stand-in that answered every call with a tool call: 3 model calls, tools offered\n * [true, true, false], then the whole run failed with every round already spent. Round 2's\n * fix caught only the reason below and so did nothing for this provider.\n * - `InvalidOutputError` raised by `LanguageModel` (module `LanguageModel`): the SDK's own decode\n * of the provider's parts (\"Expected ... text | reasoning ...\"), reached by a provider that hands\n * the SDK a tool-call part itself (the scripted model in tests/fake-model.ts). The same reason\n * raised by `OpenAiClient` (an empty, truncated or non-completion body, a dropped stream) is NOT\n * a refusal and fails the run (tests/seat-broken-forced.test.ts).\n * Either is caught: the result is `capped` and `unanswered`, its `response` is the LAST TOOL\n * ROUND's (whose calls did run), `rounds` stays `maxRounds`, and a warning, a span error and\n * `seat_rounds_unanswered_total` say what happened. Any OTHER failure of the forced turn\n * (network, rate limit, a handler) still fails the run.\n */\nimport * as Effect from 'effect/Effect';\nimport * as Metric from 'effect/Metric';\nimport type * as Chat from 'effect/ai/Chat';\nimport type * as LanguageModel from 'effect/ai/LanguageModel';\nimport * as Prompt from 'effect/ai/Prompt';\nimport type * as Tool from 'effect/ai/Tool';\nimport type { AiError } from 'effect/ai/AiError';\nimport type * as Toolkit from 'effect/ai/Toolkit';\n\n/**\n * A tool call the forced turn cannot use. `ToolNotFoundError` is compat's own rejection (its reply\n * mapping); `InvalidOutputError` counts ONLY when `LanguageModel` raised it (the SDK's decode of a\n * provider that handed it a tool-call part). compat raises the same reason, from `OpenAiClient`, for\n * a 200 with an empty, non-JSON, truncated or non-completion body and for a body stream that dies\n * mid-read: those are gateway or network failures and must fail the run, not read as a refusal\n * (review of PR 328, round 4; tests/seat-broken-forced.test.ts).\n */\nconst isToolRefusal = (error: AiError): boolean =>\n error.reason._tag === 'ToolNotFoundError' ||\n (error.reason._tag === 'InvalidOutputError' && error.module === 'LanguageModel');\n\n/** Every model turn, forced or not. */\nconst roundsTotal = Metric.counter('seat_rounds_total', {\n description:\n 'Model turns taken by runRounds that returned; a refused forced final turn is not counted.',\n});\n/** Runs whose cap fired: the interesting number, because each one is an agent that did not finish. */\nconst roundsCapped = Metric.counter('seat_rounds_capped_total', {\n description: 'runRounds runs that hit maxRounds and were given a forced final turn.',\n});\n\n/** Runs whose forced final turn was refused: the model still asked for a tool. */\nconst roundsUnanswered = Metric.counter('seat_rounds_unanswered_total', {\n description: 'runRounds runs whose forced final turn came back as a tool call, not an answer.',\n});\n\n/**\n * A turn's response: with the toolkit, or the forced one without.\n * ⚠️ The two modes differ because the SDK's own types do: a call with a toolkit answers\n * `'opaque'` tool parameters, a call without one answers the default `'decoded'`.\n */\ntype Response<Tools extends Record<string, Tool.Any>> =\n | LanguageModel.GenerateTextResponse<Tools, 'opaque'>\n | LanguageModel.GenerateTextResponse<{}, 'decoded'>;\n\n/** One model turn, as `onRound` and the result see it. A refused forced final turn is not reported. */\nexport type Round<Tools extends Record<string, Tool.Any>> = {\n /** 1-based. The forced final turn, when there is one, is `maxRounds + 1`. */\n readonly round: number;\n /** True only for the forced final turn: no toolkit, `toolChoice: 'none'`. */\n readonly forced: boolean;\n readonly response: Response<Tools>;\n};\n\nexport type RoundsOptions<Tools extends Record<string, Tool.Any>> = {\n /**\n * The conversation, already holding the opening prompt (`Chat.fromPrompt`) or a resumed\n * history that ends in a user message or tool results. Every round sends an EMPTY prompt:\n * `Chat` appends the model's turn and its tool results to `history` itself.\n */\n readonly chat: Chat.Chat;\n /**\n * Tools WITH their handlers. A `Toolkit.make(...)` is an Effect that needs its handlers from\n * context: `yield*` it where they are provided, and pass the result. (Typing it as the\n * Effect would hide its requirements, which `generateText` itself cannot track either.)\n */\n readonly toolkit: Toolkit.WithHandler<Tools>;\n /** The most tool rounds allowed. A positive integer; see the header. */\n readonly maxRounds: number;\n /** After every model turn, the forced one included. Runs inside that turn's span. */\n readonly onRound?: ((round: Round<Tools>) => Effect.Effect<void>) | undefined;\n};\n\nexport type RoundsResult<Tools extends Record<string, Tool.Any>> = {\n /** The last model turn: the answer, or the forced final turn when the cap fired. */\n readonly response: Response<Tools>;\n /** Model turns that returned, the forced one included when it did. */\n readonly rounds: number;\n /** The cap fired and `response` is the forced final turn (or see `unanswered`). */\n readonly capped: boolean;\n /**\n * The forced turn was refused (a provider that still asks for a tool) and `response` is the\n * last TOOL round's: it holds no answer, and its tool calls ran. Only ever true with `capped`;\n * `rounds` then counts the turns that returned, so it is `maxRounds`. See the header.\n */\n readonly unanswered: boolean;\n};\n\n/**\n * Run the loop. Fails with whatever a model turn fails with (`AiError`, a tool handler's own\n * failure); nothing is retried here — the caller wraps `Effect.retry` where it wants one.\n */\nexport function runRounds<Tools extends Record<string, Tool.Any>>(\n options: RoundsOptions<Tools>,\n): Effect.Effect<\n RoundsResult<Tools>,\n LanguageModel.ExtractError<{ readonly toolkit: Toolkit.WithHandler<Tools> }>,\n | LanguageModel.LanguageModel\n | LanguageModel.ExtractServices<{ readonly toolkit: Toolkit.WithHandler<Tools> }>\n> {\n const { chat, toolkit, maxRounds, onRound } = options;\n return Effect.gen(function* () {\n if (!Number.isInteger(maxRounds) || maxRounds < 1) {\n return yield* Effect.die(\n new RangeError(`runRounds: maxRounds must be a positive integer, got ${String(maxRounds)}`),\n );\n }\n const finish = (round: number, forced: boolean) =>\n Effect.fnUntraced(function* (response: Response<Tools>) {\n yield* Metric.update(roundsTotal, 1);\n yield* Effect.annotateCurrentSpan({ 'seat.tool_calls': response.toolCalls.length });\n if (onRound !== undefined) yield* onRound({ round, forced, response });\n return response;\n });\n const span = (round: number, forced: boolean) =>\n Effect.withSpan('seat.round', {\n attributes: { 'seat.round': round, 'seat.round.forced': forced },\n });\n\n const turn = (round: number) =>\n chat\n .generateText({ prompt: Prompt.empty, toolkit })\n .pipe(Effect.flatMap(finish(round, false)), span(round, false));\n\n let round = 1;\n let response: Response<Tools> = yield* turn(round);\n // ★ The exit test is the model's: no tool calls means it has answered.\n while (response.toolCalls.length > 0 && round < maxRounds) {\n round += 1;\n response = yield* turn(round);\n }\n if (response.toolCalls.length === 0) {\n return { response, rounds: round, capped: false, unanswered: false };\n }\n\n yield* Metric.update(roundsCapped, 1);\n yield* Effect.logWarning('seat round cap reached; forcing a final turn without tools', {\n maxRounds,\n });\n const forcedRound = maxRounds + 1;\n /** The forced turn came back as a tool call: counted and logged, and the run goes on. */\n const refused = () =>\n Effect.as(\n Effect.all([\n Metric.update(roundsUnanswered, 1),\n Effect.logWarning('seat forced final turn refused: the model still asked for a tool', {\n maxRounds,\n }),\n ]),\n undefined,\n );\n const forced = yield* chat.generateText({ prompt: Prompt.empty, toolChoice: 'none' }).pipe(\n Effect.flatMap(finish(forcedRound, true)),\n span(forcedRound, true),\n // ⚠️ Only a tool call nobody offered: see the header for who raises which. Every other AiError\n // (a dropped connection, an empty or truncated body, a rate limit) still fails the run.\n Effect.catchTag('AiError', (error) =>\n isToolRefusal(error) ? refused() : Effect.fail(error),\n ),\n );\n return forced === undefined\n ? { response, rounds: maxRounds, capped: true, unanswered: true }\n : { response: forced, rounds: forcedRound, capped: true, unanswered: false };\n });\n}\n",
|
|
9
|
+
"/**\n * An MCP server's tools as an Effect AI `Toolkit`, and its resources as two Effects, through\n * the official MCP TypeScript SDK (`Client` over `StreamableHTTPClientTransport`).\n *\n * ★ THE SDK'S CLIENT IS THE CLIENT. Effect AI ships the SERVER half of MCP (`McpServer`,\n * `McpSchema`); no client (walked down 2026-09-29, docs/plans/2026-09-29-cf-seat-sdk-spike.md\n * open item 2). Wrapping the official client as dynamic tools is that plan's smallest option,\n * and the scout measured the pairing: list, call and resource read against an Effect\n * `McpServer` (pair115/mcp.ts).\n * ★ EACH MCP TOOL IS A `Tool.dynamic` WITH THE SERVER'S OWN JSON SCHEMA (mcp-tool.ts). The schema\n * goes to the model as the server wrote it, and no Effect Schema is built from its types: the\n * only client-side check is that the declared `required` arguments are present, and the server\n * validates the rest and its complaint comes back to the model.\n * ⚠️ A TOOL FAILURE IS THE MODEL'S TO SEE, NOT THE RUN'S TO DIE OF. Every tool is\n * `failureMode: 'return'`: an `isError` result, a JSON-RPC error (bad arguments, unknown\n * tool) and a call that never completed all come back to the model as the tool's result, so\n * it can correct itself or answer without the tool. MCP specifies errors-in-results for\n * exactly this. Connecting and listing are different: nothing works without them, so they\n * fail with `McpToolkitError`.\n * ⚠️ THE TOOL LIST IS A SNAPSHOT taken at connect time; a server's `listChanged` notification is\n * not followed. Reconnect for a fresh list.\n * ⛔ HEADERS ARE CREDENTIALS. They ride `requestInit` to the server and nowhere else: not in an\n * error, a span or a log (mcp-error.ts). Pass a bearer value as `Redacted` to keep it out of\n * an accidental print of the argument, as `SeatModel` does with the API key.\n */\nimport type { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport * as Effect from 'effect/Effect';\nimport type * as Scope from 'effect/Scope';\nimport type * as Toolkit from 'effect/ai/Toolkit';\nimport {\n DEFAULT_CONNECT_TIMEOUT_MS,\n type McpHeaders,\n connectAndBuild,\n headerValues,\n validateTimeout,\n} from './mcp-connect.ts';\nimport { McpToolkitError, type Redact, redactor, serverLabel } from './mcp-error.ts';\nimport { collect } from './mcp-pages.ts';\nimport { type McpTools, toolset } from './mcp-toolset.ts';\n\nexport type { McpTools } from './mcp-toolset.ts';\n\nexport type McpResource = {\n readonly uri: string;\n readonly name: string;\n readonly description?: string | undefined;\n readonly mimeType?: string | undefined;\n};\n\n/** One entry of a resource read: `text` for text, `blob` (base64) for binary. */\nexport type McpResourceContent = {\n readonly uri: string;\n readonly mimeType?: string | undefined;\n readonly text?: string | undefined;\n readonly blob?: string | undefined;\n};\n\nexport type McpToolkit = {\n /** Every tool the server listed, with handlers that call it: hand this to `runRounds`. */\n readonly toolkit: Toolkit.WithHandler<McpTools>;\n /** The server's resources; empty when it does not advertise the resources capability. */\n readonly listResources: Effect.Effect<ReadonlyArray<McpResource>, McpToolkitError>;\n /** The contents of one resource; fails with `McpToolkitError` for an unknown URI. */\n readonly readResource: (\n uri: string,\n ) => Effect.Effect<ReadonlyArray<McpResourceContent>, McpToolkitError>;\n};\n\nexport type McpToolkitOptions = {\n /**\n * The budget for STARTING UP, in milliseconds; default 15 000. A finite number above 0 and at\n * most 2 ** 31 - 1, or a `RangeError` defect before any request (mcp-connect.ts).\n * It covers, each with the full budget:\n * - the WHOLE handshake: `initialize` and the `notifications/initialized` that follows it;\n * - every `tools/list` page read after it (a server holding one fails the call as\n * `McpToolkitError` with `operation: 'listTools'`).\n * ⚠️ It is a budget PER REQUEST, not a total: a server that answers each of its (up to 100)\n * tool pages in just under the budget can still take that many budgets. Wrap the whole call\n * in `Effect.timeout` for a total.\n * ⚠️ The SDK bounds only the `initialize` request (its default is 60 s) and leaves the\n * notification unbounded, so a server that answers `initialize` and then stalls would hold a\n * seat at startup indefinitely. Here the budget covers both, and the handshake and the\n * listing are interruptible, so a caller's `Effect.timeout` or a shutdown also ends them, and\n * either way the client is closed and nothing is left in your scope (mcp-connect.ts).\n * ⚠️ NOT COVERED: `listResources`, `readResource` and tool calls, which are requests you make\n * after startup and keep the SDK's 60 s default per request. They are interruptible too.\n */\n readonly connectTimeoutMs?: number | undefined;\n};\n\n/** The two resource operations against a connected client; neither runs at connect time. */\nfunction resourcesOf(client: Client, server: string, redact: Redact) {\n const listResources = Effect.suspend(() =>\n // ★ A server that does not advertise resources is not asked: a strict one answers\n // \"method not found\", and \"no resources\" is the truthful reading of that.\n client.getServerCapabilities()?.resources === undefined\n ? Effect.succeed<ReadonlyArray<McpResource>>([])\n : collect(\n 'listResources',\n server,\n async (cursor, signal) => {\n const page = await client.listResources(cursor === undefined ? undefined : { cursor }, {\n signal,\n });\n return {\n items: page.resources.map((resource): McpResource => ({\n uri: resource.uri,\n name: resource.name,\n description: resource.description,\n mimeType: resource.mimeType,\n })),\n next: page.nextCursor,\n };\n },\n redact,\n ),\n ).pipe(Effect.withSpan('seat.mcp.list_resources', { attributes: { 'server.address': server } }));\n\n const readResource = (uri: string) =>\n Effect.tryPromise({\n try: async (signal): Promise<ReadonlyArray<McpResourceContent>> => {\n const read = await client.readResource({ uri }, { signal });\n return read.contents.map((entry) => ({\n uri: entry.uri,\n mimeType: entry.mimeType,\n text: 'text' in entry ? entry.text : undefined,\n blob: 'blob' in entry ? entry.blob : undefined,\n }));\n },\n catch: (cause) => new McpToolkitError({ operation: 'readResource', server, cause, redact }),\n }).pipe(\n Effect.withSpan('seat.mcp.read_resource', {\n attributes: { 'mcp.resource': uri, 'server.address': server },\n }),\n );\n\n return { listResources, readResource };\n}\n\n/**\n * Connect to a Streamable HTTP MCP server, list its tools, and return them as a toolkit.\n * The connection lives as long as the surrounding `Scope`.\n *\n * ★ A FAILURE AT ANY POINT LEAVES THE CALLER'S SCOPE EMPTY: the handshake, the `tools/list`\n * (a JSON-RPC error, a page held past `connectTimeoutMs`) and building the toolkit all run\n * before the client's finalizer is registered, and each closes the client if it fails or is\n * interrupted (`connectAndBuild`, mcp-connect.ts). So `Effect.retry` around `mcpToolkit`\n * accumulates no clients, sessions or sockets.\n */\nexport function mcpToolkit(\n url: string | URL,\n headers?: McpHeaders,\n options?: McpToolkitOptions,\n): Effect.Effect<McpToolkit, McpToolkitError, Scope.Scope> {\n return Effect.gen(function* () {\n const timeout = yield* validateTimeout(options?.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS);\n // ⚠️ Parsed INSIDE the Effect: `new URL` throws, and a throw at call time would skip the typed\n // error. The message never repeats the string: a URL is where a token may sit.\n const target = yield* Effect.try({\n try: () => new URL(url),\n catch: () =>\n new McpToolkitError({\n operation: 'connect',\n server: '(unparseable URL)',\n cause: 'the URL did not parse',\n }),\n });\n const server = serverLabel(target);\n // ⛔ Every error and every failure text below goes through this: a server echoes its address,\n // a query value or a header value, and a fetch failure prints the address, query string and\n // all (mcp-error.ts).\n const redact = redactor(target, headerValues(headers));\n\n // The connection lives in the surrounding scope; it is registered there only once the whole\n // toolkit below has been built (see the header of `mcpToolkit`).\n return yield* connectAndBuild(target, headers, server, timeout, redact, (client) =>\n Effect.gen(function* () {\n const toolkit = yield* toolset(client, server, timeout, redact);\n return { toolkit, ...resourcesOf(client, server, redact) };\n }),\n );\n });\n}\n",
|
|
10
10
|
"/**\n * Connect to a Streamable HTTP MCP server with the official SDK client: the transport, the\n * caller's headers on every request, and the WHOLE handshake (`initialize` and the\n * `notifications/initialized` that follows it) bounded in time and interruptible.\n *\n * ⛔ HEADERS ARE CREDENTIALS. They ride `requestInit` to the server and nowhere else: not in an\n * error, a span or a log (mcp-error.ts). A `Redacted` value is unwrapped only here, at the\n * moment the transport is built.\n */\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';\nimport { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';\nimport * as Effect from 'effect/Effect';\nimport * as Redacted from 'effect/Redacted';\nimport type * as Scope from 'effect/Scope';\nimport { McpToolkitError, type Redact } from './mcp-error.ts';\nimport { VERSION } from './version.ts';\n\n/** Header values; a `Redacted` is unwrapped only at the moment the transport is built. */\nexport type McpHeaders = Readonly<Record<string, string | Redacted.Redacted<string>>>;\n\nexport const DEFAULT_CONNECT_TIMEOUT_MS = 15_000;\n\n/**\n * The longest a timer can run: `setTimeout` takes a 32-bit signed delay, and a larger one (like\n * one below 1) is set to 1 ms. Node documents that; Bun does the same (measured, see below).\n */\nexport const MAX_TIMEOUT_MS: number = 2 ** 31 - 1;\n\n/**\n * ⛔ A timeout that is not a finite number of milliseconds in `(0, MAX_TIMEOUT_MS]` is a\n * programming error, and dies with a `RangeError` before any request, as `maxRounds` does.\n * The budget feeds two timers (ours and the SDK's per request), and an out-of-range value made\n * them disagree: measured 2026-09-29 (review of PR 328) against a healthy local stub,\n * `Infinity` failed at once (\"did not finish within Infinity ms\") while `0`, `-1`, `NaN` and\n * `2 ** 31` each happened to succeed, because the timer clamped to 1 ms and the handshake won\n * the race. A real handshake takes longer than that 1 ms (not measured remotely), so those\n * would lose it.\n */\nexport const validateTimeout = (timeout: number): Effect.Effect<number> =>\n Number.isFinite(timeout) && timeout > 0 && timeout <= MAX_TIMEOUT_MS\n ? Effect.succeed(timeout)\n : Effect.die(\n new RangeError(\n `connectTimeoutMs must be a finite number of milliseconds above 0 and at most ${String(MAX_TIMEOUT_MS)}, got ${String(timeout)}`,\n ),\n );\n\nconst plainHeaders = (headers: McpHeaders | undefined): Record<string, string> =>\n Object.fromEntries(\n Object.entries(headers ?? {}).map(([name, value]) => [\n name,\n Redacted.isRedacted(value) ? Redacted.value(value) : value,\n ]),\n );\n\n/**\n * The header values as plain strings, for `redactor` and for nothing else: a value an error\n * message must never repeat is exactly a value the redactor has to know. Kept here so that a\n * `Redacted` is unwrapped in this file only.\n */\nexport function headerValues(headers: McpHeaders | undefined): string[] {\n return Object.values(plainHeaders(headers));\n}\n\n/**\n * ★ THE BUDGET COVERS THE WHOLE HANDSHAKE, NOT ONE REQUEST. SDK 1.31.0 `Client.connect` sends\n * `initialize` (bounded by its `timeout` option) and then AWAITS a `notifications/initialized`\n * POST that carries no timeout at all: only the transport's own abort signal ends it. Measured\n * 2026-09-29 (review of PR 328): a server that answered `initialize` and held the second POST\n * left a caller's `Effect.timeout('2 seconds')` unsettled at 8 s. So a timer here closes the\n * client when the budget is spent, and `close` aborts the transport's fetch, which is the only\n * thing that cancels that pending POST.\n * ★ THE CALLER'S `signal` CLOSES IT TOO. It fires when the fiber is interrupted (a seat shutting\n * down, an `Effect.timeout`), which only means something because `connectAndBuild` runs the handshake\n * in the `restore`d, interruptible part of its mask: a handshake inside `acquireRelease`'s\n * uninterruptible acquire would ignore it (that was the first version of this timeout).\n * ⚠️ Whatever the SDK rejects with after the budget is spent (an `AbortError` from the cancelled\n * fetch, or its own `Request timed out`) is an artefact of the close, so it is replaced by one\n * message that says what happened.\n */\nasync function handshake(\n client: Client,\n url: URL,\n headers: McpHeaders | undefined,\n timeout: number,\n signal: AbortSignal,\n): Promise<void> {\n const transport = new StreamableHTTPClientTransport(url, {\n requestInit: { headers: plainHeaders(headers) },\n });\n const budget = new AbortController();\n const stop = AbortSignal.any([signal, budget.signal]);\n const timer = setTimeout(() => budget.abort(), timeout);\n const close = (): void => void client.close().catch(() => undefined);\n stop.addEventListener('abort', close, { once: true });\n try {\n // ⚠️ The SDK's own `sessionId?: string` reads as `string | undefined` against its own\n // `Transport` under `exactOptionalPropertyTypes` (this repo's baseline), so the two\n // of its types do not line up here. Upstream's typing, not a wrong argument: the\n // class IS the transport, and nothing of it reaches this package's declarations.\n await client.connect(transport as Transport, { signal: stop, timeout });\n } catch (error) {\n // A failure AFTER the transport starts (a refused `initialize`) leaves the client holding\n // an open transport: close it here rather than wait for the scope.\n await client.close().catch(() => undefined);\n const spent = budget.signal.aborted;\n const sdkTimeout = error instanceof McpError && error.code === ErrorCode.RequestTimeout;\n throw (spent || sdkTimeout) && !signal.aborted\n ? new Error(`the MCP handshake did not finish within ${String(timeout)} ms`)\n : error;\n } finally {\n clearTimeout(timer);\n stop.removeEventListener('abort', close);\n }\n}\n\n/**\n * Connect, handshake and BUILD whatever the caller needs from the client (`build`: for\n * `mcpToolkit`, listing the tools and making the toolkit). The client lives as long as the\n * surrounding `Scope`, and only when ALL of that worked.\n *\n * ★ THE FINALIZER IS REGISTERED LAST: after the handshake AND after `build`. A scoped constructor\n * that fails must leave nothing in the caller's scope, and this one has been wrong twice:\n * - it used to register the `Client`'s `close` before the handshake. Each `Client` carries its\n * own JSON Schema validator (SDK client/index.js), so a seat retrying `mcpToolkit` through a\n * gateway outage held one per failed attempt until its scope closed: measured 2026-09-29\n * (review of PR 328), 64.7 MB against 10.6 MB after 3 001 refused attempts in one scope,\n * about 18 KB each. `handshake` closes the client on every failure, so a refused handshake\n * has nothing left to release.\n * - it then registered it right after the handshake, BEFORE the `tools/list` that `build` does,\n * and that listing can fail too: a JSON-RPC error, a page held past `connectTimeoutMs`, a\n * caller who interrupts. Measured 2026-09-29 (review of PR 328, later round), a loopback\n * server that completed the handshake: 20 attempts whose listing errored left 20 finalizers\n * and 20 open SSE streams in one scope; 10 whose listing was held left 10 finalizers, 10\n * streams and 10 `tools/list` POSTs still open (the SDK's per-request timeout rejects the\n * promise and does not abort the fetch). A seat retrying once a second against a slow gateway\n * is about 3 600 sessions and sockets an hour, held for the life of the seat.\n * `close` aborts the transport's fetches (the SSE stream and a held POST alike), which is why\n * closing the client is what releases them.\n * ⚠️ `uninterruptibleMask` keeps the gaps between the steps from being interruption points (a\n * client opened and never closed); only the handshake and `build` are restored to\n * interruptible, and each has an `onError` that closes the client when it fails OR is\n * interrupted, so the SDK's own close-on-abort is not the only line of defence. `onError` runs\n * uninterruptibly, so the close itself cannot be cut off.\n * ★ THE `seat.mcp.connect` SPAN IS THE HANDSHAKE ONLY, not `build`: it is what a slow or refused\n * connect looks like in a trace, and the listing is not that.\n */\nexport const connectAndBuild = <A>(\n url: URL,\n headers: McpHeaders | undefined,\n server: string,\n timeout: number,\n redact: Redact,\n build: (client: Client) => Effect.Effect<A, McpToolkitError>,\n): Effect.Effect<A, McpToolkitError, Scope.Scope> =>\n Effect.uninterruptibleMask((restore) =>\n Effect.gen(function* () {\n const client = new Client({ name: '@homeflare/seat-runtime', version: VERSION });\n const close = Effect.ignore(Effect.tryPromise(() => client.close()));\n yield* restore(\n Effect.tryPromise({\n try: (signal) => handshake(client, url, headers, timeout, signal),\n catch: (cause) => new McpToolkitError({ operation: 'connect', server, cause, redact }),\n }).pipe(Effect.withSpan('seat.mcp.connect', { attributes: { 'server.address': server } })),\n ).pipe(Effect.onError(() => close));\n const built = yield* restore(build(client)).pipe(Effect.onError(() => close));\n yield* Effect.addFinalizer(() => close);\n return built;\n }),\n );\n",
|
|
11
11
|
"/**\n * `mcpToolkit`'s typed failure, and the one place an MCP server's address is spelled for a\n * message or a span.\n *\n * ⛔ THE ADDRESS IS ORIGIN AND PATH ONLY. A query string is where a token lands when a server\n * wants one there, and the headers a caller passes are bearer credentials: neither may reach\n * an error message, a log line or a span attribute. Header NAMES and the header map are never\n * read here; a header VALUE is read only to be redacted (`redactor`).\n * 🔴 A SERVER ECHOES A VALUE ON ITS OWN. Redacting the query STRING and the address is not\n * enough: a server that answers \"rejected key QVALUE\" prints the value alone, and one that says\n * \"bad credential Bearer HVALUE\" prints a header's. Measured 2026-09-29 (review of PR 328,\n * round 2): both reached `McpToolkitError.message`. So `redactor` also takes out each query\n * value and each header value the caller passed (`MIN_SECRET_LENGTH` and up). What it cannot\n * catch: a value the server TRANSFORMS (hashed, base64, truncated), and one under the minimum.\n * 🔴 THE `cause` IS A COPY, NEVER THE ORIGINAL. Measured 2026-09-29 (review of PR 328): under Bun a\n * refused fetch is a `TypeError` whose own `path` field is the FULL URL, query string included,\n * so attaching it as `cause` printed the token through `Bun.inspect`, `console.error`, an\n * uncaught rejection and the default `Effect.logError` — while `error.message` was clean, which\n * is all the first tests looked at. `scrubCause` rebuilds the chain from name, message, stack\n * and a string or numeric `code` only, so no other field (`path`, `url`, `data`, `body`) is\n * carried, and `redactor` takes the query string, fragment and password out of what remains.\n * The price: `cause instanceof McpError` no longer holds; read `cause.code` instead.\n */\n/** Which call failed. `connect` covers the transport and the whole handshake (mcp-connect.ts). */\nexport type McpOperation = 'connect' | 'listTools' | 'listResources' | 'readResource';\n\n/** A server address safe to print: `https://host:port/path`, no credentials, query or fragment. */\nexport function serverLabel(url: URL): string {\n return `${url.origin}${url.pathname}`;\n}\n\n/** Text with a server's query string, fragment and password taken out of it. */\nexport type Redact = (text: string) => string;\n\n/**\n * The shortest value `redactor` treats as a secret ON ITS OWN. Redacting every occurrence of a\n * value costs legible messages (a `?v=1` would blank every \"1\"), and a value this short is not\n * a credential anyone relies on. The query STRING, fragment and password are redacted whole\n * whatever their length.\n */\nexport const MIN_SECRET_LENGTH = 6;\n\n/** The values in a raw query string, as written (percent-encoded) and as decoded. */\nfunction queryValues(url: URL): string[] {\n const raw = url.search\n .slice(1)\n .split('&')\n .map((pair) => pair.slice(pair.indexOf('=') + 1));\n return [...raw, ...url.searchParams.values()];\n}\n\n/**\n * A header value, and its credential when it has a scheme: `Bearer abc` is echoed whole or as\n * `abc`, and \"Bearer\" alone is not a secret.\n */\nfunction headerParts(value: string): string[] {\n const token = value.trim().split(/\\s+/).at(-1) ?? '';\n return [value, token];\n}\n\n/**\n * A `Redact` for one server address, plus any header values the caller passes. The whole URL\n * becomes `serverLabel`; a bare query string, fragment or password left over, each query value\n * and each header value (`MIN_SECRET_LENGTH` and up, longest first) becomes `[redacted]`: a\n * server can echo the address or a value back in an error body, and a fetch failure can print it.\n */\nexport function redactor(url: URL, headerValues: ReadonlyArray<string> = []): Redact {\n const label = serverLabel(url);\n const whole = [\n url.search.length > 1 ? url.search : '',\n url.hash.length > 1 ? url.hash : '',\n url.password,\n ];\n const parts = [...queryValues(url), ...headerValues.flatMap(headerParts)];\n const secrets = [\n ...whole,\n ...[...new Set(parts)]\n .filter((part) => part.length >= MIN_SECRET_LENGTH)\n .sort((a, b) => b.length - a.length),\n ].filter((secret) => secret !== '');\n return (text) => {\n let out = text.split(url.href).join(label);\n for (const secret of secrets) out = out.split(secret).join('[redacted]');\n return out;\n };\n}\n\n/** How many `cause` links `scrubCause` follows: a cycle or a deep chain ends here. */\nconst MAX_CAUSE_DEPTH = 5;\n\n/**\n * A copy of `cause` that is safe to print. ⚠️ An allowlist, not a denylist: an Error is rebuilt\n * from its name, message, stack and `code` (a string or number) and its own `cause`, each run\n * through `redact`; every other own field is left behind, whatever it is called. See the header.\n */\nexport function scrubCause(cause: unknown, redact: Redact, depth = 0): unknown {\n if (!(cause instanceof Error)) return redact(String(cause));\n const inner = depth < MAX_CAUSE_DEPTH && cause.cause !== undefined;\n const copy = new Error(\n redact(cause.message),\n inner ? { cause: scrubCause(cause.cause, redact, depth + 1) } : undefined,\n );\n copy.name = redact(cause.name);\n if (typeof cause.stack === 'string') copy.stack = redact(cause.stack);\n const code: unknown = (cause as { code?: unknown }).code;\n if (typeof code === 'string') Object.assign(copy, { code: redact(code) });\n else if (typeof code === 'number') Object.assign(copy, { code });\n return copy;\n}\n\n/** The message of whatever a promise rejected with; an SDK `McpError` carries the JSON-RPC text. */\nexport function describeCause(cause: unknown): string {\n return cause instanceof Error ? cause.message : String(cause);\n}\n\n/**\n * The connection, a listing or a resource read failed.\n *\n * ★ A PLAIN `Error` WITH A `_tag`, NOT `Data.TaggedError`. `class X extends Data.TaggedError(…)<…>`\n * is TS9021 under `isolatedDeclarations` (\"extends clause can't contain an expression\"; measured\n * 2026-09-15, see packages/alchemy/tsconfig.json), and this package keeps that flag on. The\n * `_tag` is all `Effect.catchTag('McpToolkitError', …)` reads.\n */\nexport class McpToolkitError extends Error {\n readonly _tag = 'McpToolkitError' as const;\n readonly operation: McpOperation;\n /** `serverLabel` of the server: never a header, never a query string. */\n readonly server: string;\n\n constructor(fields: {\n readonly operation: McpOperation;\n readonly server: string;\n /**\n * What the SDK threw: an `McpError` (a JSON-RPC error), a `StreamableHTTPError`, a fetch\n * failure. Never stored as given: `cause` on the error is `scrubCause` of it.\n */\n readonly cause: unknown;\n /** `redactor` of the server's URL. Left out, only the structural scrub of `cause` applies. */\n readonly redact?: Redact | undefined;\n }) {\n const redact = fields.redact ?? ((text: string) => text);\n super(\n `MCP ${fields.operation} against ${fields.server} failed: ${redact(describeCause(fields.cause))}`,\n { cause: scrubCause(fields.cause, redact) },\n );\n this.name = 'McpToolkitError';\n this.operation = fields.operation;\n this.server = fields.server;\n }\n}\n",
|
|
12
|
-
"// ⛔ GENERATED by scripts/sync-versions.ts — do not edit. Source: package.json.\nexport const VERSION: string = '0.1.
|
|
12
|
+
"// ⛔ GENERATED by scripts/sync-versions.ts — do not edit. Source: package.json.\nexport const VERSION: string = '0.1.1';\n",
|
|
13
13
|
"/**\n * MCP's cursor pagination, followed to the end — but not forever.\n *\n * ⛔ THE PAGE COUNT IS CAPPED. A server that answers every page with another `nextCursor` (a\n * bug, or a hostile one) would otherwise hold a seat in a listing loop before its first\n * model call. A hundred pages is far past any estate server's tool list; the cap fails the\n * listing loudly instead of truncating it quietly.\n */\nimport * as Effect from 'effect/Effect';\nimport { type McpOperation, McpToolkitError, type Redact } from './mcp-error.ts';\n\nexport const MAX_PAGES = 100;\n\nexport type Page<T> = { readonly items: ReadonlyArray<T>; readonly next: string | undefined };\n\n/** Every item of a paged listing, or an `McpToolkitError` for `operation`. Interruption aborts the request in flight. */\nexport function collect<T>(\n operation: McpOperation,\n server: string,\n page: (cursor: string | undefined, signal: AbortSignal) => Promise<Page<T>>,\n redact?: Redact | undefined,\n): Effect.Effect<ReadonlyArray<T>, McpToolkitError> {\n return Effect.tryPromise({\n try: async (signal) => {\n const items: T[] = [];\n let cursor: string | undefined;\n for (let pages = 0; pages < MAX_PAGES; pages += 1) {\n const next = await page(cursor, signal);\n items.push(...next.items);\n if (next.next === undefined) return items;\n cursor = next.next;\n }\n throw new Error(`the server returned more than ${String(MAX_PAGES)} pages`);\n },\n catch: (cause) => new McpToolkitError({ operation, server, cause, redact }),\n });\n}\n",
|
|
14
|
-
"/**\n * An MCP server's tools as a ready-to-run Effect AI toolkit: list them (bounded by the startup\n * budget), wrap each as a `Tool.dynamic` (mcp-tool.ts), and give each a handler that calls the\n * server. Split from mcp-toolkit.ts so that `connectAndBuild` can run all of this BEFORE the\n * client's finalizer is registered (mcp-connect.ts): listing can fail, and a failed listing must\n * leave nothing in the caller's scope.\n *\n * ⚠️ A TOOL FAILURE IS THE MODEL'S TO SEE, NOT THE RUN'S TO DIE OF (see mcp-toolkit.ts). Every\n * failure below comes back as a string.\n * ⛔ A FAILURE'S TEXT IS REDACTED, A SUCCESS'S IS NOT. What the model reads for an `isError`\n * result, a JSON-RPC error or a dropped call goes through `redact`: a server that rejects a\n * credential typically prints it (\"bad credential Bearer ...\"), and that text goes on to a\n * cloud model. Measured 2026-09-29 (review of PR 328, later round): an `isError` result that\n * echoed a query value and a bearer token delivered both to the model, while the same echo in\n * an `McpToolkitError` was already redacted. A SUCCESSFUL result is the tool's payload and is\n * delivered verbatim: rewriting it would corrupt legitimate data that happens to contain a\n * value, and a server that returns a credential as data is not a failure this can tell apart.\n */\nimport type { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport * as Effect from 'effect/Effect';\nimport * as Toolkit from 'effect/
|
|
14
|
+
"/**\n * An MCP server's tools as a ready-to-run Effect AI toolkit: list them (bounded by the startup\n * budget), wrap each as a `Tool.dynamic` (mcp-tool.ts), and give each a handler that calls the\n * server. Split from mcp-toolkit.ts so that `connectAndBuild` can run all of this BEFORE the\n * client's finalizer is registered (mcp-connect.ts): listing can fail, and a failed listing must\n * leave nothing in the caller's scope.\n *\n * ⚠️ A TOOL FAILURE IS THE MODEL'S TO SEE, NOT THE RUN'S TO DIE OF (see mcp-toolkit.ts). Every\n * failure below comes back as a string.\n * ⛔ A FAILURE'S TEXT IS REDACTED, A SUCCESS'S IS NOT. What the model reads for an `isError`\n * result, a JSON-RPC error or a dropped call goes through `redact`: a server that rejects a\n * credential typically prints it (\"bad credential Bearer ...\"), and that text goes on to a\n * cloud model. Measured 2026-09-29 (review of PR 328, later round): an `isError` result that\n * echoed a query value and a bearer token delivered both to the model, while the same echo in\n * an `McpToolkitError` was already redacted. A SUCCESSFUL result is the tool's payload and is\n * delivered verbatim: rewriting it would corrupt legitimate data that happens to contain a\n * value, and a server that returns a credential as data is not a failure this can tell apart.\n */\nimport type { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport * as Effect from 'effect/Effect';\nimport * as Toolkit from 'effect/ai/Toolkit';\nimport { type McpToolkitError, type Redact, describeCause } from './mcp-error.ts';\nimport { collect } from './mcp-pages.ts';\nimport { renderResult } from './mcp-render.ts';\nimport { type McpTool, mcpTool } from './mcp-tool.ts';\n\nexport type McpTools = Readonly<Record<string, McpTool>>;\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/** Everything the model can do wrong or the server can refuse comes back as a string. */\nconst call = (client: Client, server: string, redact: Redact, name: string) => (params: unknown) =>\n Effect.gen(function* () {\n // The Toolkit's decode has already rejected a non-object (mcp-tool.ts), so this is the\n // type guard the `unknown` argument needs and not a second line of defence.\n if (!isRecord(params)) {\n return yield* Effect.fail(`${name}: the arguments must be a JSON object`);\n }\n const result = yield* Effect.tryPromise({\n try: (signal) => client.callTool({ name, arguments: params }, undefined, { signal }),\n catch: (cause) => `MCP call to ${name} failed: ${redact(describeCause(cause))}`,\n });\n const text = renderResult(result);\n // ⛔ Only a FAILURE's text is redacted: see the header.\n return result.isError === true ? yield* Effect.fail(redact(text)) : text;\n }).pipe(\n Effect.withSpan('seat.mcp.call_tool', {\n attributes: { 'mcp.tool': name, 'server.address': server },\n }),\n );\n\n/**\n * List the server's tools and return them as a toolkit with handlers.\n * ★ A server that does not advertise tools is not asked for them: a resources-only server\n * answers `tools/list` with \"method not found\", and one such server would otherwise make the\n * whole connection unusable for its resources.\n * ★ `timeout` is the startup budget: without it the SDK waits its 60 s per page.\n */\nexport const toolset = (\n client: Client,\n server: string,\n timeout: number,\n redact: Redact,\n): Effect.Effect<Toolkit.WithHandler<McpTools>, McpToolkitError> =>\n Effect.gen(function* () {\n const listed =\n client.getServerCapabilities()?.tools === undefined\n ? []\n : yield* collect(\n 'listTools',\n server,\n async (cursor, signal) => {\n const page = await client.listTools(cursor === undefined ? undefined : { cursor }, {\n signal,\n timeout,\n });\n return { items: page.tools, next: page.nextCursor };\n },\n redact,\n );\n const tools = listed.map((tool) =>\n mcpTool({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema }),\n );\n const definition = Toolkit.make(...tools);\n const handlers = Object.fromEntries(\n tools.map((tool) => [tool.name, call(client, server, redact, tool.name)]),\n );\n const context = yield* definition.toHandlers(handlers);\n return yield* definition.pipe(Effect.provideContext(context));\n });\n",
|
|
15
15
|
"/**\n * An MCP tool result as the text a model reads.\n *\n * ★ ALWAYS A STRING. A result is a list of content blocks (text, image, audio, a resource, a\n * link), and what goes into the model's context is one message. Text is passed through\n * verbatim, joined by newlines; everything else becomes a one-line marker naming what it was.\n * ⛔ A BINARY BLOCK NEVER REACHES THE MODEL AS ITS BYTES. An image's base64 is tens of\n * kilobytes of context that says nothing to a text model and costs every later round; the\n * marker keeps its type and MIME type so the model knows something was there.\n * ⚠️ AN EMPTY RESULT IS NOT AN EMPTY STRING. Some providers refuse a tool message with empty\n * content (not measured against cf-code), and a refused request ends the whole run over a\n * tool that merely had nothing to say.\n */\n\n/** What the model reads for a tool that returned no content blocks. */\nexport const EMPTY_RESULT = '(no content)';\n\ntype Block = Readonly<Record<string, unknown>>;\n\nconst isBlock = (value: unknown): value is Block => typeof value === 'object' && value !== null;\nconst str = (value: unknown): string | undefined => (typeof value === 'string' ? value : undefined);\n\n/** One block. The shapes are MCP's `TextContent`, `ImageContent`, `AudioContent`, `ResourceLink`, `EmbeddedResource`. */\nfunction renderBlock(block: Block): string {\n const type = str(block['type']) ?? 'unknown';\n switch (type) {\n case 'text':\n return str(block['text']) ?? '';\n case 'image':\n case 'audio':\n return `[${type}: ${str(block['mimeType']) ?? 'unknown type'}, not shown]`;\n case 'resource_link':\n return `[resource link: ${str(block['uri']) ?? 'no uri'}]`;\n case 'resource': {\n const resource = isBlock(block['resource']) ? block['resource'] : {};\n // An embedded resource is either text (shown) or a base64 blob (not).\n return str(resource['text']) ?? `[resource: ${str(resource['uri']) ?? 'no uri'}, not shown]`;\n }\n default:\n return `[unsupported content block: ${type}]`;\n }\n}\n\n/** The text of a `CallToolResult.content`. Anything that is not a block list is JSON, not dropped. */\nexport function renderContent(content: unknown): string {\n if (!Array.isArray(content)) {\n return content === undefined ? EMPTY_RESULT : JSON.stringify(content);\n }\n const lines = content.map((block: unknown) =>\n isBlock(block) ? renderBlock(block) : JSON.stringify(block),\n );\n return lines.length === 0 ? EMPTY_RESULT : lines.join('\\n');\n}\n\n/**\n * The text of a whole `callTool` result. ★ The `content` blocks are what MCP asks a server to\n * always send; a server that sends only `structuredContent` (an `outputSchema` tool that skipped\n * the text copy) would otherwise read as empty, so its JSON is shown instead. The legacy\n * `toolResult` shape (the SDK's compatibility result) is read as content.\n */\nexport function renderResult(result: Readonly<Record<string, unknown>>): string {\n const content = 'content' in result ? result['content'] : result['toolResult'];\n if (Array.isArray(content) && content.length === 0 && result['structuredContent'] !== undefined) {\n return JSON.stringify(result['structuredContent']);\n }\n return renderContent(content);\n}\n",
|
|
16
|
-
"/**\n * One MCP tool as an Effect AI dynamic tool: the server's own JSON Schema goes to the model,\n * and the tool call the model sends back decodes without losing its arguments.\n *\n * ★ `Tool.dynamic` WITH THE SERVER'S JSON SCHEMA IS THE RIGHT SHAPE (its docs name MCP tools\n * discovered at runtime), and the model sees that schema verbatim.\n * 🔴 BUT @effect/ai-openai-compat rc.115 CANNOT DECODE A TOOL CALL FOR IT. Measured 2026-09-29\n * (tests/seat-loop.test.ts, first run): the request goes out, and when the model's reply asks\n * for the tool, `transformToolCallParams` runs the OpenAI structured-output codec over the\n * tool's `parametersSchema` — `Schema.Unknown` in JSON-Schema mode — and fails the whole turn\n * with `UnsupportedSchemaError: Root JSON Schema must have type \"object\" and must not use\n * \"anyOf\"`. rc.118 fixed it upstream: it returns the raw params for a dynamic tool that has a\n * `jsonSchema`. The estate is pinned at rc.115 and rc.118 drops the `unstable/` import prefix,\n * so this file carries the workaround until the pin moves.\n * ★ THE WORKAROUND: keep `jsonSchema` (what the model is sent, and what `Tool.getJsonSchema`\n * returns), and replace `parametersSchema` — what compat's codec and the Toolkit decode with —\n * by an object schema of the DECLARED property names, each `Unknown`, decoded to `Unknown`\n * with an ENCODE THAT IS FORBIDDEN. Every declared value passes through untouched and\n * `required` is honoured, so a missing argument fails as a tool result the model can read\n * instead of reaching the server.\n * 🔴 WHY THE ENCODE IS FORBIDDEN. compat's `transformToolCallParams` (OpenAiLanguageModel.js)\n * decodes the model's params through the OpenAI structured-output codec and re-encodes them\n * with `parametersSchema`, falling back to the params AS SENT when either step fails. That\n * codec rewrites every optional property as nullable and reads `null` as ABSENT, so with a\n * plain object schema an explicit `null` on an optional argument was deleted before the call.\n * Measured 2026-09-29 (review of PR 328), end to end through `SeatModel` and `mcpToolkit` into\n * an Effect `McpServer`: `update_issue {id, assignee: null}` (null unassigns, omitted leaves\n * alone) reached the server as `{id}`, and the run ended 'done'. That normalisation exists for\n * the codec's own JSON Schema, and the model here was sent the server's, where `null` is a\n * value. Forbidding the encode makes the re-encode fail, so compat forwards the params as the\n * model sent them: `null` arrives as `null` (pinned in tests/mcp-arguments.test.ts).\n * ⚠️ WHAT IT COSTS: a property the schema does not declare is DROPPED before the call\n * (Effect's decoder ignores excess keys; v4 has no \"preserve\"), so a server that declares\n * `additionalProperties: true` and relies on undeclared keys gets fewer than the model sent.\n * And a tool that declares no properties takes no arguments: `Tool.EmptyParams` is the only\n * root the codec accepts for it, and it rejects any key.\n * ⛔ REMOVE THIS WHEN THE PIN REACHES rc.118 or later: build the tool from `Tool.dynamic` alone.\n * The clone below copies what `Tool`'s own `setParameters` copies (its prototype and own\n * fields), so it depends on Effect's tool object layout; the end-to-end test is what fails\n * first if a bump changes that.\n */\nimport type * as JsonSchema from 'effect/JsonSchema';\nimport * as Schema from 'effect/Schema';\nimport * as SchemaGetter from 'effect/SchemaGetter';\nimport * as Tool from 'effect/
|
|
16
|
+
"/**\n * One MCP tool as an Effect AI dynamic tool: the server's own JSON Schema goes to the model,\n * and the tool call the model sends back decodes without losing its arguments.\n *\n * ★ `Tool.dynamic` WITH THE SERVER'S JSON SCHEMA IS THE RIGHT SHAPE (its docs name MCP tools\n * discovered at runtime), and the model sees that schema verbatim.\n * 🔴 BUT @effect/ai-openai-compat rc.115 CANNOT DECODE A TOOL CALL FOR IT. Measured 2026-09-29\n * (tests/seat-loop.test.ts, first run): the request goes out, and when the model's reply asks\n * for the tool, `transformToolCallParams` runs the OpenAI structured-output codec over the\n * tool's `parametersSchema` — `Schema.Unknown` in JSON-Schema mode — and fails the whole turn\n * with `UnsupportedSchemaError: Root JSON Schema must have type \"object\" and must not use\n * \"anyOf\"`. rc.118 fixed it upstream: it returns the raw params for a dynamic tool that has a\n * `jsonSchema`. The estate is pinned at rc.115 and rc.118 drops the `unstable/` import prefix,\n * so this file carries the workaround until the pin moves.\n * ★ THE WORKAROUND: keep `jsonSchema` (what the model is sent, and what `Tool.getJsonSchema`\n * returns), and replace `parametersSchema` — what compat's codec and the Toolkit decode with —\n * by an object schema of the DECLARED property names, each `Unknown`, decoded to `Unknown`\n * with an ENCODE THAT IS FORBIDDEN. Every declared value passes through untouched and\n * `required` is honoured, so a missing argument fails as a tool result the model can read\n * instead of reaching the server.\n * 🔴 WHY THE ENCODE IS FORBIDDEN. compat's `transformToolCallParams` (OpenAiLanguageModel.js)\n * decodes the model's params through the OpenAI structured-output codec and re-encodes them\n * with `parametersSchema`, falling back to the params AS SENT when either step fails. That\n * codec rewrites every optional property as nullable and reads `null` as ABSENT, so with a\n * plain object schema an explicit `null` on an optional argument was deleted before the call.\n * Measured 2026-09-29 (review of PR 328), end to end through `SeatModel` and `mcpToolkit` into\n * an Effect `McpServer`: `update_issue {id, assignee: null}` (null unassigns, omitted leaves\n * alone) reached the server as `{id}`, and the run ended 'done'. That normalisation exists for\n * the codec's own JSON Schema, and the model here was sent the server's, where `null` is a\n * value. Forbidding the encode makes the re-encode fail, so compat forwards the params as the\n * model sent them: `null` arrives as `null` (pinned in tests/mcp-arguments.test.ts).\n * ⚠️ WHAT IT COSTS: a property the schema does not declare is DROPPED before the call\n * (Effect's decoder ignores excess keys; v4 has no \"preserve\"), so a server that declares\n * `additionalProperties: true` and relies on undeclared keys gets fewer than the model sent.\n * And a tool that declares no properties takes no arguments: `Tool.EmptyParams` is the only\n * root the codec accepts for it, and it rejects any key.\n * ⛔ REMOVE THIS WHEN THE PIN REACHES rc.118 or later: build the tool from `Tool.dynamic` alone.\n * The clone below copies what `Tool`'s own `setParameters` copies (its prototype and own\n * fields), so it depends on Effect's tool object layout; the end-to-end test is what fails\n * first if a bump changes that.\n */\nimport type * as JsonSchema from 'effect/JsonSchema';\nimport * as Schema from 'effect/Schema';\nimport * as SchemaGetter from 'effect/SchemaGetter';\nimport * as Tool from 'effect/ai/Tool';\n\n/** One dynamic tool per MCP tool: the server's JSON Schema in, text out, failures returned. */\nexport type McpTool = Tool.Dynamic<\n string,\n {\n readonly parameters: JsonSchema.JsonSchema;\n readonly success: typeof Schema.String;\n readonly failure: typeof Schema.String;\n readonly failureMode: 'return';\n }\n>;\n\n/** What `client.listTools()` gives for one tool, narrowed to what is used here. */\nexport type McpToolSpec = {\n readonly name: string;\n readonly description?: string | undefined;\n readonly inputSchema: JsonSchema.JsonSchema;\n};\n\n/** The workaround's `parametersSchema`: the declared property names, each passed through as-is, decode-only. */\nexport function declaredParameters(inputSchema: JsonSchema.JsonSchema): Schema.Constraint {\n const properties = inputSchema['properties'];\n const names =\n typeof properties === 'object' && properties !== null ? Object.keys(properties) : [];\n if (names.length === 0) return Tool.EmptyParams;\n const required = new Set(\n Array.isArray(inputSchema['required']) ? (inputSchema['required'] as unknown[]) : [],\n );\n const declared = Schema.Struct(\n Object.fromEntries(\n names.map((name) => [\n name,\n required.has(name) ? Schema.Unknown : Schema.optional(Schema.Unknown),\n ]),\n ),\n );\n return declared.pipe(\n Schema.decodeTo(Schema.Unknown, {\n decode: SchemaGetter.passthrough(),\n encode: SchemaGetter.forbiddenEncoding,\n }),\n );\n}\n\nexport function mcpTool(spec: McpToolSpec): McpTool {\n const dynamic = Tool.dynamic(spec.name, {\n description: spec.description,\n parameters: spec.inputSchema,\n success: Schema.String,\n failure: Schema.String,\n failureMode: 'return',\n });\n return Object.assign(Object.create(Object.getPrototypeOf(dynamic)), dynamic, {\n parametersSchema: declaredParameters(spec.inputSchema),\n }) as McpTool;\n}\n"
|
|
17
17
|
],
|
|
18
18
|
"mappings": "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",
|
|
19
|
-
"debugId": "
|
|
19
|
+
"debugId": "AE11F30DA17DC80F64756E2164756E21",
|
|
20
20
|
"names": []
|
|
21
21
|
}
|
package/dist/mcp-tool.d.ts
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
*/
|
|
42
42
|
import type * as JsonSchema from 'effect/JsonSchema';
|
|
43
43
|
import * as Schema from 'effect/Schema';
|
|
44
|
-
import * as Tool from 'effect/
|
|
44
|
+
import * as Tool from 'effect/ai/Tool';
|
|
45
45
|
/** One dynamic tool per MCP tool: the server's JSON Schema in, text out, failures returned. */
|
|
46
46
|
export type McpTool = Tool.Dynamic<string, {
|
|
47
47
|
readonly parameters: JsonSchema.JsonSchema;
|
package/dist/mcp-tool.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-tool.d.ts","sourceRoot":"","sources":["../src/mcp-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,KAAK,KAAK,UAAU,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"mcp-tool.d.ts","sourceRoot":"","sources":["../src/mcp-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,KAAK,KAAK,UAAU,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,IAAI,MAAM,gBAAgB,CAAC;AAEvC,+FAA+F;AAC/F,MAAM,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAChC,MAAM,EACN;IACE,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC,UAAU,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,OAAO,MAAM,CAAC,MAAM,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,OAAO,MAAM,CAAC,MAAM,CAAC;IACvC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC;CAChC,CACF,CAAC;AAEF,mFAAmF;AACnF,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC,UAAU,CAAC;CAC7C,CAAC;AAEF,gHAAgH;AAChH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,UAAU,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAsBxF;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAWlD"}
|
package/dist/mcp-toolkit.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as Effect from 'effect/Effect';
|
|
2
2
|
import type * as Scope from 'effect/Scope';
|
|
3
|
-
import type * as Toolkit from 'effect/
|
|
3
|
+
import type * as Toolkit from 'effect/ai/Toolkit';
|
|
4
4
|
import { type McpHeaders } from './mcp-connect.ts';
|
|
5
5
|
import { McpToolkitError } from './mcp-error.ts';
|
|
6
6
|
import { type McpTools } from './mcp-toolset.ts';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-toolkit.d.ts","sourceRoot":"","sources":["../src/mcp-toolkit.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,KAAK,OAAO,MAAM,
|
|
1
|
+
{"version":3,"file":"mcp-toolkit.d.ts","sourceRoot":"","sources":["../src/mcp-toolkit.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,KAAK,OAAO,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAEL,KAAK,UAAU,EAIhB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAsC,MAAM,gBAAgB,CAAC;AAErF,OAAO,EAAE,KAAK,QAAQ,EAAW,MAAM,kBAAkB,CAAC;AAE1D,YAAY,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjD,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC,CAAC;AAEF,iFAAiF;AACjF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,0FAA0F;IAC1F,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;IAChD,yFAAyF;IACzF,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,WAAW,CAAC,EAAE,eAAe,CAAC,CAAC;IACnF,qFAAqF;IACrF,QAAQ,CAAC,YAAY,EAAE,CACrB,GAAG,EAAE,MAAM,KACR,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,kBAAkB,CAAC,EAAE,eAAe,CAAC,CAAC;CACxE,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChD,CAAC;AAmDF;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,OAAO,CAAC,EAAE,UAAU,EACpB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,eAAe,EAAE,KAAK,CAAC,KAAK,CAAC,CA6BzD"}
|
package/dist/mcp-toolset.d.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
20
20
|
import * as Effect from 'effect/Effect';
|
|
21
|
-
import * as Toolkit from 'effect/
|
|
21
|
+
import * as Toolkit from 'effect/ai/Toolkit';
|
|
22
22
|
import { type McpToolkitError, type Redact } from './mcp-error.ts';
|
|
23
23
|
import { type McpTool } from './mcp-tool.ts';
|
|
24
24
|
export type McpTools = Readonly<Record<string, McpTool>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-toolset.d.ts","sourceRoot":"","sources":["../src/mcp-toolset.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACxE,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,OAAO,MAAM,
|
|
1
|
+
{"version":3,"file":"mcp-toolset.d.ts","sourceRoot":"","sources":["../src/mcp-toolset.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACxE,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,OAAO,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,MAAM,EAAiB,MAAM,gBAAgB,CAAC;AAGlF,OAAO,EAAE,KAAK,OAAO,EAAW,MAAM,eAAe,CAAC;AAEtD,MAAM,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AA0BzD;;;;;;GAMG;AACH,eAAO,MAAM,OAAO,WACV,MAAM,UACN,MAAM,WACL,MAAM,UACP,MAAM,KACb,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,eAAe,CA0B3D,CAAC"}
|
package/dist/rounds.d.ts
CHANGED
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
* (network, rate limit, a handler) still fails the run.
|
|
46
46
|
*/
|
|
47
47
|
import * as Effect from 'effect/Effect';
|
|
48
|
-
import type * as Chat from 'effect/
|
|
49
|
-
import type * as LanguageModel from 'effect/
|
|
50
|
-
import type * as Tool from 'effect/
|
|
51
|
-
import type * as Toolkit from 'effect/
|
|
48
|
+
import type * as Chat from 'effect/ai/Chat';
|
|
49
|
+
import type * as LanguageModel from 'effect/ai/LanguageModel';
|
|
50
|
+
import type * as Tool from 'effect/ai/Tool';
|
|
51
|
+
import type * as Toolkit from 'effect/ai/Toolkit';
|
|
52
52
|
/**
|
|
53
53
|
* A turn's response: with the toolkit, or the forced one without.
|
|
54
54
|
* ⚠️ The two modes differ because the SDK's own types do: a call with a toolkit answers
|
package/dist/rounds.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rounds.d.ts","sourceRoot":"","sources":["../src/rounds.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,KAAK,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"rounds.d.ts","sourceRoot":"","sources":["../src/rounds.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,KAAK,IAAI,MAAM,gBAAgB,CAAC;AAC5C,OAAO,KAAK,KAAK,aAAa,MAAM,yBAAyB,CAAC;AAE9D,OAAO,KAAK,KAAK,IAAI,MAAM,gBAAgB,CAAC;AAE5C,OAAO,KAAK,KAAK,OAAO,MAAM,mBAAmB,CAAC;AA6BlD;;;;GAIG;AACH,KAAK,QAAQ,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,IAChD,aAAa,CAAC,oBAAoB,CAAC,KAAK,EAAE,QAAQ,CAAC,GACnD,aAAa,CAAC,oBAAoB,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;AAEtD,uGAAuG;AACvG,MAAM,MAAM,KAAK,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI;IAC1D,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,aAAa,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI;IAClE;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IAC7C,wEAAwE;IACxE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,qFAAqF;IACrF,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS,CAAC;CAC/E,CAAC;AAEF,MAAM,MAAM,YAAY,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI;IACjE,oFAAoF;IACpF,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IACnC,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,mFAAmF;IACnF,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,EAC9D,OAAO,EAAE,aAAa,CAAC,KAAK,CAAC,GAC5B,MAAM,CAAC,MAAM,CACd,YAAY,CAAC,KAAK,CAAC,EACnB,aAAa,CAAC,YAAY,CAAC;IAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAA;CAAE,CAAC,EAC1E,aAAa,CAAC,aAAa,GAC3B,aAAa,CAAC,eAAe,CAAC;IAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAA;CAAE,CAAC,CAClF,CAiEA"}
|
package/dist/seat-model.d.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
import { OpenAiClient, OpenAiEmbeddingModel, OpenAiLanguageModel } from '@effect/ai-openai-compat';
|
|
12
12
|
import * as Layer from 'effect/Layer';
|
|
13
13
|
import * as Redacted from 'effect/Redacted';
|
|
14
|
-
import * as EmbeddingModel from 'effect/
|
|
15
|
-
import type * as LanguageModel from 'effect/
|
|
14
|
+
import * as EmbeddingModel from 'effect/ai/EmbeddingModel';
|
|
15
|
+
import type * as LanguageModel from 'effect/ai/LanguageModel';
|
|
16
16
|
export type SeatClientOptions = {
|
|
17
17
|
/** LiteLLM's OpenAI-compatible base, e.g. `http://127.0.0.1:4100/v1`. Required: no host default. */
|
|
18
18
|
readonly apiUrl: string;
|
package/dist/seat-model.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seat-model.d.ts","sourceRoot":"","sources":["../src/seat-model.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACnG,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,cAAc,MAAM,
|
|
1
|
+
{"version":3,"file":"seat-model.d.ts","sourceRoot":"","sources":["../src/seat-model.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACnG,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,cAAc,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,KAAK,aAAa,MAAM,yBAAyB,CAAC;AAK9D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,oGAAoG;IACpG,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sGAAsG;IACtG,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACpD,qFAAqF;IACrF,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,uFAAuF;IACvF,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;CAClE,CAAC;AAEF,4FAA4F;AAC5F,KAAK,WAAW,GAAG,WAAW,CAAC,UAAU,CAAC,OAAO,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;AAC1F,KAAK,eAAe,GAAG,WAAW,CAAC,UAAU,CAAC,OAAO,oBAAoB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;AAE/F,MAAM,MAAM,gBAAgB,GAAG,iBAAiB,GAAG;IACjD,yCAAyC;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CAC3C,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,iBAAiB,GAAG;IACrD,4CAA4C;IAC5C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;CAC/C,CAAC;AAEF,gFAAgF;AAChF,wBAAgB,WAAW,CAAC,OAAO,EAAE,iBAAiB,GAAG,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,YAAY,CAAC,CAY9F;AAED,6CAA6C;AAC7C,wBAAgB,KAAK,CAAC,OAAO,EAAE,gBAAgB,GAAG,KAAK,CAAC,KAAK,CAAC,aAAa,CAAC,aAAa,CAAC,CAQzF;AAED,qEAAqE;AACrE,wBAAgB,cAAc,CAC5B,OAAO,EAAE,oBAAoB,GAC5B,KAAK,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,GAAG,cAAc,CAAC,UAAU,CAAC,CAKxE"}
|
package/dist/seat-obs.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as Layer from 'effect/Layer';
|
|
2
|
-
import { type OtlpExporter } from 'effect/
|
|
2
|
+
import { type OtlpExporter } from 'effect/observability';
|
|
3
3
|
/**
|
|
4
4
|
* CT100's Victoria services, one path each. Verified 2026-09-29 by GET against the live
|
|
5
5
|
* ports (no payload sent): a mounted path answers, an unmounted sibling answers
|
package/dist/seat-obs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seat-obs.d.ts","sourceRoot":"","sources":["../src/seat-obs.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAGtC,OAAO,EACL,KAAK,YAAY,EAKlB,MAAM
|
|
1
|
+
{"version":3,"file":"seat-obs.d.ts","sourceRoot":"","sources":["../src/seat-obs.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAGtC,OAAO,EACL,KAAK,YAAY,EAKlB,MAAM,sBAAsB,CAAC;AAE9B;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAK1B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,oBAAoB,iBAAiB,CAAC;AA6CnD;;;GAGG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,CAQnD,CAAC"}
|
package/dist/seat-state.d.ts
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
* ⛔ THE LIBRARY HOLDS NO HOST. Every URL comes from the consumer (a value, or an environment
|
|
6
6
|
* variable it names), as `Redacted`; there is no default address here to point a seat at the
|
|
7
7
|
* wrong store, and nothing reads a credential from disk.
|
|
8
|
-
* ★ THE SERVICES ARE EFFECT'S, NOT OURS: `SqlClient` (`effect/
|
|
9
|
-
* Postgres and `Redis` (`effect/
|
|
8
|
+
* ★ THE SERVICES ARE EFFECT'S, NOT OURS: `SqlClient` (`effect/sql/SqlClient`) for
|
|
9
|
+
* Postgres and `Redis` (`effect/persistence/Redis`) for Valkey, so a consumer writes
|
|
10
10
|
* ordinary Effect SQL and Redis code and this package adds only how they are built.
|
|
11
11
|
*/
|
|
12
12
|
import * as Layer from 'effect/Layer';
|
|
13
|
-
import type * as Redis from 'effect/
|
|
14
|
-
import type * as SqlClient from 'effect/
|
|
15
|
-
import type { SqlError } from 'effect/
|
|
13
|
+
import type * as Redis from 'effect/persistence/Redis';
|
|
14
|
+
import type * as SqlClient from 'effect/sql/SqlClient';
|
|
15
|
+
import type { SqlError } from 'effect/sql/SqlError';
|
|
16
16
|
import type { Config } from 'effect';
|
|
17
17
|
import type { PgClient } from '@effect/sql-pg';
|
|
18
18
|
import { type PostgresFromEnvOptions, type PostgresOptions } from './state-postgres.ts';
|
package/dist/seat-state.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seat-state.d.ts","sourceRoot":"","sources":["../src/seat-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,KAAK,KAAK,MAAM,
|
|
1
|
+
{"version":3,"file":"seat-state.d.ts","sourceRoot":"","sources":["../src/seat-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,KAAK,KAAK,MAAM,0BAA0B,CAAC;AACvD,OAAO,KAAK,KAAK,SAAS,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EAGrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAGnB,MAAM,mBAAmB,CAAC;AAE3B,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAGlC,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,oDAAoD;AACpD,eAAO,MAAM,KAAK,YAAa;IAC7B,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;CAChC,KAAG,KAAK,CAAC,KAAK,CACb,QAAQ,CAAC,QAAQ,GAAG,SAAS,CAAC,SAAS,GAAG,KAAK,CAAC,KAAK,EACrD,QAAQ,GAAG,KAAK,CAAC,UAAU,CAC0C,CAAC;AAExE,yGAAyG;AACzG,eAAO,MAAM,YAAY,aAAc;IACrC,QAAQ,CAAC,QAAQ,CAAC,EAAE,sBAAsB,GAAG,SAAS,CAAC;IACvD,QAAQ,CAAC,MAAM,CAAC,EAAE,oBAAoB,GAAG,SAAS,CAAC;CACpD,KAAG,KAAK,CAAC,KAAK,CACb,QAAQ,CAAC,QAAQ,GAAG,SAAS,CAAC,SAAS,GAAG,KAAK,CAAC,KAAK,EACrD,QAAQ,GAAG,KAAK,CAAC,UAAU,GAAG,MAAM,CAAC,WAAW,CACqC,CAAC"}
|
package/dist/stamp.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* ★ IT IS THE SAME TRANSFORM AS cf-harness (landscape PR 165, cf-harness/src/request.ts),
|
|
10
10
|
* which is where the header and the body fields were measured against LiteLLM.
|
|
11
11
|
*/
|
|
12
|
-
import * as HttpClientRequest from 'effect/
|
|
12
|
+
import * as HttpClientRequest from 'effect/http/HttpClientRequest';
|
|
13
13
|
export type SeatStamp = {
|
|
14
14
|
/** Already validated and joined: LiteLLM reads one comma-separated header. */
|
|
15
15
|
readonly tags: string;
|
package/dist/stamp.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stamp.d.ts","sourceRoot":"","sources":["../src/stamp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,iBAAiB,MAAM,
|
|
1
|
+
{"version":3,"file":"stamp.d.ts","sourceRoot":"","sources":["../src/stamp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,iBAAiB,MAAM,+BAA+B,CAAC;AAEnE,MAAM,MAAM,SAAS,GAAG;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;CACjE,CAAC;AAUF,2FAA2F;AAC3F,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,GAAG,MAAM,CAWrE;AAuBD,yFAAyF;AACzF,wBAAgB,YAAY,CAC1B,OAAO,EAAE,iBAAiB,CAAC,iBAAiB,EAC5C,KAAK,EAAE,SAAS,GACf,iBAAiB,CAAC,iBAAiB,CAoBrC"}
|
package/dist/state-postgres.d.ts
CHANGED
|
@@ -23,8 +23,8 @@ import * as Config from 'effect/Config';
|
|
|
23
23
|
import * as Duration from 'effect/Duration';
|
|
24
24
|
import * as Layer from 'effect/Layer';
|
|
25
25
|
import * as Redacted from 'effect/Redacted';
|
|
26
|
-
import type * as SqlClient from 'effect/
|
|
27
|
-
import { SqlError } from 'effect/
|
|
26
|
+
import type * as SqlClient from 'effect/sql/SqlClient';
|
|
27
|
+
import { SqlError } from 'effect/sql/SqlError';
|
|
28
28
|
export type PostgresOptions = {
|
|
29
29
|
/**
|
|
30
30
|
* `postgres://<user>:<password>@<host>:<port>/<database>` (or `postgresql://`), the user and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state-postgres.d.ts","sourceRoot":"","sources":["../src/state-postgres.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAE5C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,KAAK,SAAS,MAAM
|
|
1
|
+
{"version":3,"file":"state-postgres.d.ts","sourceRoot":"","sources":["../src/state-postgres.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAE5C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,KAAK,SAAS,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAmB,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAGhE,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACjD,sDAAsD;IACtD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7C,6FAA6F;IAC7F,QAAQ,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAC;IACrD,qEAAqE;IACrE,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/C,CAAC;AAEF,wFAAwF;AACxF,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AAwCzD;;GAEG;AACH,eAAO,MAAM,QAAQ,YACV,eAAe,KACvB,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,GAAG,SAAS,CAAC,SAAS,EAAE,QAAQ,CACJ,CAAC;AAE7D,MAAM,MAAM,sBAAsB,GAAG,IAAI,CAAC,eAAe,EAAE,KAAK,CAAC,GAAG;IAClE,iEAAiE;IACjE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,aAChB,sBAAsB,KAC/B,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,GAAG,SAAS,CAAC,SAAS,EAAE,QAAQ,GAAG,MAAM,CAAC,WAAW,CAOpF,CAAC"}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* wedge a seat forever, so every command carries `commandTimeout` too.
|
|
16
16
|
*/
|
|
17
17
|
import * as Effect from 'effect/Effect';
|
|
18
|
-
import * as Redis from 'effect/
|
|
18
|
+
import * as Redis from 'effect/persistence/Redis';
|
|
19
19
|
import type * as Duration from 'effect/Duration';
|
|
20
20
|
/** What this file needs of a client: `Bun.RedisClient` has this exact `send`. */
|
|
21
21
|
export type ValkeyCall = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state-valkey-send.d.ts","sourceRoot":"","sources":["../src/state-valkey-send.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,
|
|
1
|
+
{"version":3,"file":"state-valkey-send.d.ts","sourceRoot":"","sources":["../src/state-valkey-send.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,0BAA0B,CAAC;AAClD,OAAO,KAAK,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAGjD,iFAAiF;AACjF,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC3E,CAAC;AAEF,uCAAuC;AACvC,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,OAAO,EAC7B,OAAO,EAAE,MAAM,EACf,GAAG,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,KAC3B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;AAExC,MAAM,MAAM,WAAW,GAAG;IACxB,qEAAqE;IACrE,QAAQ,CAAC,cAAc,EAAE,QAAQ,CAAC,KAAK,CAAC;IACxC,kEAAkE;IAClE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,+FAA+F;IAC/F,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACxD,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAyB/E"}
|
package/dist/state-valkey.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ import * as Config from 'effect/Config';
|
|
|
30
30
|
import * as Duration from 'effect/Duration';
|
|
31
31
|
import * as Layer from 'effect/Layer';
|
|
32
32
|
import * as Redacted from 'effect/Redacted';
|
|
33
|
-
import * as Redis from 'effect/
|
|
33
|
+
import * as Redis from 'effect/persistence/Redis';
|
|
34
34
|
export type ValkeyOptions = {
|
|
35
35
|
/**
|
|
36
36
|
* `redis://<user>:<password>@<host>:<port>[/<db>]` (or `valkey://`, `rediss://` for TLS), the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state-valkey.d.ts","sourceRoot":"","sources":["../src/state-valkey.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAE5C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,KAAK,MAAM,
|
|
1
|
+
{"version":3,"file":"state-valkey.d.ts","sourceRoot":"","sources":["../src/state-valkey.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAE5C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,KAAK,MAAM,0BAA0B,CAAC;AAKlD,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACjD;;;;OAIG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAC;IACxD,uFAAuF;IACvF,QAAQ,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAC;IACrD;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1C,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,QAAQ,CAAC,QAA8B,CAAC;AACjF,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,QAA+B,CAAC;AAe/E;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAK1D;AAuED;;;;;GAKG;AACH,eAAO,MAAM,MAAM,YAAa,aAAa,KAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAItF,CAAC;AAEJ,sFAAsF;AACtF,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,GAAG;IAC9D,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,aACd,oBAAoB,KAC7B,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,GAAG,MAAM,CAAC,WAAW,CAOhE,CAAC"}
|
package/dist/state.js
CHANGED
|
@@ -40,7 +40,7 @@ import * as Duration from "effect/Duration";
|
|
|
40
40
|
import * as Effect from "effect/Effect";
|
|
41
41
|
import * as Layer from "effect/Layer";
|
|
42
42
|
import * as Redacted2 from "effect/Redacted";
|
|
43
|
-
import { ConnectionError, SqlError } from "effect/
|
|
43
|
+
import { ConnectionError, SqlError } from "effect/sql/SqlError";
|
|
44
44
|
|
|
45
45
|
// src/state-dsn.ts
|
|
46
46
|
var OVERRIDING_KEYS = new Set(["host", "port", "user", "dbname"]);
|
|
@@ -145,7 +145,7 @@ import * as Duration3 from "effect/Duration";
|
|
|
145
145
|
import * as Effect4 from "effect/Effect";
|
|
146
146
|
import * as Layer2 from "effect/Layer";
|
|
147
147
|
import * as Redacted4 from "effect/Redacted";
|
|
148
|
-
import * as Redis2 from "effect/
|
|
148
|
+
import * as Redis2 from "effect/persistence/Redis";
|
|
149
149
|
|
|
150
150
|
// src/state-valkey-connection.ts
|
|
151
151
|
import * as Duration2 from "effect/Duration";
|
|
@@ -195,7 +195,7 @@ var keepConnected = Effect2.fnUntraced(function* (client, options) {
|
|
|
195
195
|
|
|
196
196
|
// src/state-valkey-send.ts
|
|
197
197
|
import * as Effect3 from "effect/Effect";
|
|
198
|
-
import * as Redis from "effect/
|
|
198
|
+
import * as Redis from "effect/persistence/Redis";
|
|
199
199
|
|
|
200
200
|
// src/state-valkey-scrub.ts
|
|
201
201
|
var OMITTED = "[arguments omitted]";
|
|
@@ -323,5 +323,5 @@ export {
|
|
|
323
323
|
exports_seat_state as SeatState
|
|
324
324
|
};
|
|
325
325
|
|
|
326
|
-
//# debugId=
|
|
326
|
+
//# debugId=A291DE77D2DBD33F64756E2164756E21
|
|
327
327
|
//# sourceMappingURL=state.js.map
|
package/dist/state.js.map
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/seat-state.ts", "../src/state-postgres.ts", "../src/state-dsn.ts", "../src/state-valkey.ts", "../src/state-valkey-connection.ts", "../src/state-valkey-send.ts", "../src/state-valkey-scrub.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * `SeatState`: a seat's Postgres and Valkey as Effect services, one layer each, in the trace of\n * the run that uses them.\n *\n * ⛔ THE LIBRARY HOLDS NO HOST. Every URL comes from the consumer (a value, or an environment\n * variable it names), as `Redacted`; there is no default address here to point a seat at the\n * wrong store, and nothing reads a credential from disk.\n * ★ THE SERVICES ARE EFFECT'S, NOT OURS: `SqlClient` (`effect/
|
|
6
|
-
"/**\n * Postgres as Effect's own `SqlClient`, through `@effect/sql-pg`.\n *\n * ★ THE SDK'S OWN LAYER, NOT A WRAPPER OF IT. `PgClient` opens the pool and creates one\n * `sql.execute` client span per statement carrying `db.system.name: postgresql`,\n * `db.namespace`, `server.address`, `server.port` and `db.query.text` (measured 2026-09-29\n * against CT100's Postgres), so state lands in the seat's trace with no `withSpan` of ours.\n * ⚠️ `db.query.text` is the statement with its `$1` placeholders: parameter VALUES are not in\n * the span (tests/state-postgres.test.ts asserts a canary value never leaves).\n * ⛔ WHAT THIS ADDS is the two things the driver leaves to the caller, both measured\n * 2026-09-29 and both in state-dsn.ts: a DSN that will not parse never reaches the driver (its\n * error carries the string, password included), and the URL's host, port, database and user are\n * passed as discrete fields so the spans name the database that was actually reached (from `url`\n * alone they say `localhost:5432`, database `postgres`).\n * ⛔ BUILDING THE LAYER RUNS `select 1`. The pool is lazy (a bad host or password otherwise fails\n * at the first query), so a seat that starts against a dead or refusing Postgres is told at\n * startup, as `SqlError`, within `connectTimeout`.\n * ⚠️ PORTABLE. `@effect/sql-pg` rc.115 speaks the wire protocol itself over `node:net`, with no\n * driver package, so this half runs under Bun and Node alike (the Valkey half needs Bun).\n */\nimport { PgClient } from '@effect/sql-pg';\nimport * as Config from 'effect/Config';\nimport * as Duration from 'effect/Duration';\nimport * as Effect from 'effect/Effect';\nimport * as Layer from 'effect/Layer';\nimport * as Redacted from 'effect/Redacted';\nimport type * as SqlClient from 'effect/
|
|
5
|
+
"/**\n * `SeatState`: a seat's Postgres and Valkey as Effect services, one layer each, in the trace of\n * the run that uses them.\n *\n * ⛔ THE LIBRARY HOLDS NO HOST. Every URL comes from the consumer (a value, or an environment\n * variable it names), as `Redacted`; there is no default address here to point a seat at the\n * wrong store, and nothing reads a credential from disk.\n * ★ THE SERVICES ARE EFFECT'S, NOT OURS: `SqlClient` (`effect/sql/SqlClient`) for\n * Postgres and `Redis` (`effect/persistence/Redis`) for Valkey, so a consumer writes\n * ordinary Effect SQL and Redis code and this package adds only how they are built.\n */\nimport * as Layer from 'effect/Layer';\nimport type * as Redis from 'effect/persistence/Redis';\nimport type * as SqlClient from 'effect/sql/SqlClient';\nimport type { SqlError } from 'effect/sql/SqlError';\nimport type { Config } from 'effect';\nimport type { PgClient } from '@effect/sql-pg';\nimport {\n type PostgresFromEnvOptions,\n type PostgresOptions,\n postgres,\n postgresFromEnv,\n} from './state-postgres.ts';\nimport {\n type ValkeyFromEnvOptions,\n type ValkeyOptions,\n valkey,\n valkeyFromEnv,\n} from './state-valkey.ts';\n\nexport * from './state-postgres.ts';\nexport * from './state-valkey.ts';\n// ★ RE-EXPORTED so a consumer under pnpm's strict resolution can name `PgClient` (for `.json`,\n// `.listen`, `.notify`) without also depending on `@effect/sql-pg` itself.\nexport { PgClient } from '@effect/sql-pg';\n\n/** Both stores, each built from an explicit URL. */\nexport const layer = (options: {\n readonly postgres: PostgresOptions;\n readonly valkey: ValkeyOptions;\n}): Layer.Layer<\n PgClient.PgClient | SqlClient.SqlClient | Redis.Redis,\n SqlError | Redis.RedisError\n> => Layer.mergeAll(postgres(options.postgres), valkey(options.valkey));\n\n/** Both stores, each URL read from its environment variable (`SEAT_POSTGRES_URL`, `SEAT_VALKEY_URL`). */\nexport const layerFromEnv = (options?: {\n readonly postgres?: PostgresFromEnvOptions | undefined;\n readonly valkey?: ValkeyFromEnvOptions | undefined;\n}): Layer.Layer<\n PgClient.PgClient | SqlClient.SqlClient | Redis.Redis,\n SqlError | Redis.RedisError | Config.ConfigError\n> => Layer.mergeAll(postgresFromEnv(options?.postgres), valkeyFromEnv(options?.valkey));\n",
|
|
6
|
+
"/**\n * Postgres as Effect's own `SqlClient`, through `@effect/sql-pg`.\n *\n * ★ THE SDK'S OWN LAYER, NOT A WRAPPER OF IT. `PgClient` opens the pool and creates one\n * `sql.execute` client span per statement carrying `db.system.name: postgresql`,\n * `db.namespace`, `server.address`, `server.port` and `db.query.text` (measured 2026-09-29\n * against CT100's Postgres), so state lands in the seat's trace with no `withSpan` of ours.\n * ⚠️ `db.query.text` is the statement with its `$1` placeholders: parameter VALUES are not in\n * the span (tests/state-postgres.test.ts asserts a canary value never leaves).\n * ⛔ WHAT THIS ADDS is the two things the driver leaves to the caller, both measured\n * 2026-09-29 and both in state-dsn.ts: a DSN that will not parse never reaches the driver (its\n * error carries the string, password included), and the URL's host, port, database and user are\n * passed as discrete fields so the spans name the database that was actually reached (from `url`\n * alone they say `localhost:5432`, database `postgres`).\n * ⛔ BUILDING THE LAYER RUNS `select 1`. The pool is lazy (a bad host or password otherwise fails\n * at the first query), so a seat that starts against a dead or refusing Postgres is told at\n * startup, as `SqlError`, within `connectTimeout`.\n * ⚠️ PORTABLE. `@effect/sql-pg` rc.115 speaks the wire protocol itself over `node:net`, with no\n * driver package, so this half runs under Bun and Node alike (the Valkey half needs Bun).\n */\nimport { PgClient } from '@effect/sql-pg';\nimport * as Config from 'effect/Config';\nimport * as Duration from 'effect/Duration';\nimport * as Effect from 'effect/Effect';\nimport * as Layer from 'effect/Layer';\nimport * as Redacted from 'effect/Redacted';\nimport type * as SqlClient from 'effect/sql/SqlClient';\nimport { ConnectionError, SqlError } from 'effect/sql/SqlError';\nimport { postgresFields } from './state-dsn.ts';\n\nexport type PostgresOptions = {\n /**\n * `postgres://<user>:<password>@<host>:<port>/<database>` (or `postgresql://`), the user and\n * password percent-encoded. Held `Redacted`; never in a span, log or error. Required: this\n * package holds no host. `sslmode=require|verify-ca|verify-full|disable` is read; the driver\n * refuses `prefer` and `allow`.\n */\n readonly url: string | Redacted.Redacted<string>;\n /** Pool ceiling; the driver's default when absent. */\n readonly maxConnections?: number | undefined;\n /** How long to wait for a connection, and for the startup `select 1`. Driver default 5 s. */\n readonly connectTimeout?: Duration.Input | undefined;\n /** Shown in `pg_stat_activity`; the driver's default when absent. */\n readonly applicationName?: string | undefined;\n};\n\n/** The environment variable `postgresFromEnv` reads unless told another. Not a host. */\nexport const POSTGRES_URL_VARIABLE = 'SEAT_POSTGRES_URL';\n\nconst DEFAULT_CONNECT_TIMEOUT: Duration.Duration = Duration.seconds(5);\n\n/** A failure that names the reason and never the DSN. */\nconst refused = (message: string): SqlError =>\n new SqlError({\n reason: new ConnectionError({\n cause: new Error(message),\n message: `SeatState postgres: ${message}`,\n operation: 'connect',\n }),\n });\n\nconst acquire = Effect.fnUntraced(function* (options: PostgresOptions) {\n const dsn = Redacted.value(\n typeof options.url === 'string' ? Redacted.make(options.url) : options.url,\n );\n const fields = postgresFields(dsn);\n if (fields === undefined) return yield* refused('the URL is not a postgres:// URL');\n const connectTimeout = options.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT;\n const client = yield* PgClient.make({\n url: Redacted.make(dsn),\n ...fields,\n maxConnections: options.maxConnections,\n connectTimeout,\n applicationName: options.applicationName,\n });\n yield* client`select 1`.pipe(\n Effect.timeoutOrElse({\n duration: connectTimeout,\n orElse: () =>\n Effect.fail(\n refused(`did not answer within ${String(Duration.toMillis(connectTimeout))} ms`),\n ),\n }),\n );\n return client;\n});\n\n/**\n * `SqlClient` and `PgClient` against one Postgres. The pool closes with the layer's scope.\n */\nexport const postgres = (\n options: PostgresOptions,\n): Layer.Layer<PgClient.PgClient | SqlClient.SqlClient, SqlError> =>\n PgClient.layerFrom(Effect.suspend(() => acquire(options)));\n\nexport type PostgresFromEnvOptions = Omit<PostgresOptions, 'url'> & {\n /** The variable holding the URL. Default `SEAT_POSTGRES_URL`. */\n readonly variable?: string | undefined;\n};\n\n/**\n * `postgres`, its URL read from an environment variable as a `Redacted` secret. A missing\n * variable fails as `ConfigError`, naming the variable and nothing else.\n */\nexport const postgresFromEnv = (\n options?: PostgresFromEnvOptions,\n): Layer.Layer<PgClient.PgClient | SqlClient.SqlClient, SqlError | Config.ConfigError> => {\n const { variable, ...rest } = options ?? {};\n return Layer.unwrap(\n Config.Redacted(variable ?? POSTGRES_URL_VARIABLE).pipe(\n Effect.map((url) => postgres({ ...rest, url })),\n ),\n );\n};\n",
|
|
7
7
|
"/**\n * Reading a connection string without ever printing it.\n *\n * ⛔ A DSN CARRIES A PASSWORD, so nothing here returns, throws or formats the string itself: a\n * caller gets back the few non-secret fields (host, port, database, user name) or `undefined`.\n * 🔴 WHY THIS EXISTS AT ALL. Measured 2026-09-29 (`@effect/sql-pg` rc.115, Bun 1.4.0): a DSN that\n * `new URL` cannot parse fails as `SqlError` -> `ConnectionError` whose `cause` is the URL\n * `TypeError`, and that error's text carries the WHOLE string, password included. Three of the\n * six bad DSNs tried leaked it through `JSON.stringify` and `Bun.inspect` alike. Parsing here\n * first, with the same `new URL`, means a string that would leak never reaches the driver:\n * what the driver still rejects (a wrong scheme, an `sslmode` it lacks) echoes only that one\n * token, not the string.\n * 🔴 THE SECOND REASON: `@effect/sql-pg` labels every query span from its discrete config fields\n * and IGNORES the URL for that. A client built from `url` alone exports\n * `server.address: localhost`, `server.port: 5432` and `db.namespace: postgres` for EVERY\n * query whatever it is connected to (measured 2026-09-29 against CT100's Postgres through a\n * tunnel), so a trace would point at the wrong database. `postgresFields` hands the driver the\n * same host, port, database and user the URL names, decoded the way the driver decodes them.\n */\n\n/** The non-secret fields a Postgres URL names; a field the URL leaves out is absent. */\nexport type PostgresFields = {\n readonly host?: string | undefined;\n readonly port?: number | undefined;\n readonly database?: string | undefined;\n readonly username?: string | undefined;\n};\n\n/** The non-secret fields a Valkey URL names; `port` defaults to 6379 for a TCP scheme. */\nexport type ValkeyFields = {\n readonly host?: string | undefined;\n readonly port?: number | undefined;\n /** The logical database index (`redis://host/1`), when there is one. */\n readonly database?: string | undefined;\n};\n\n/**\n * Query keys that OVERRIDE the authority in `@effect/sql-pg`'s own parser (rc.115 `parseUrl`).\n * ⚠️ When a URL carries one, the fields below would be a second, conflicting answer (explicit\n * config wins over the URL), so none are derived and the driver's reading stands.\n */\nconst OVERRIDING_KEYS: ReadonlySet<string> = new Set(['host', 'port', 'user', 'dbname']);\n\n/** `decodeURIComponent` that reports failure as `undefined` instead of throwing its message. */\nfunction decode(value: string): string | undefined {\n try {\n return decodeURIComponent(value);\n } catch {\n return undefined;\n }\n}\n\n/** `new URL` that reports failure as `undefined`: its `TypeError` carries the string. */\nfunction parse(raw: string): URL | undefined {\n try {\n return new URL(raw);\n } catch {\n return undefined;\n }\n}\n\n/**\n * `undefined` when `dsn` is not a URL at all (the one case the driver would leak on); otherwise\n * the fields it names, or none when the URL is one the driver should read alone.\n */\nexport function postgresFields(dsn: string): PostgresFields | undefined {\n const url = parse(dsn);\n if (url === undefined) return undefined;\n for (const key of url.searchParams.keys()) if (OVERRIDING_KEYS.has(key)) return {};\n const hostname =\n url.hostname.startsWith('[') && url.hostname.endsWith(']')\n ? url.hostname.slice(1, -1)\n : decode(url.hostname);\n const database = decode(url.pathname.replace(/^\\//, ''));\n const username = decode(url.username);\n const port = url.port === '' ? undefined : Number(url.port);\n // ⚠️ A piece that will not decode or parse is left to the driver, which fails with its own\n // message (it names a port or a component, never the string).\n if (hostname === undefined || database === undefined || username === undefined) return {};\n if (port !== undefined && !(Number.isInteger(port) && port >= 1 && port <= 65535)) return {};\n return {\n host: hostname === '' ? undefined : hostname,\n port,\n database: database === '' ? undefined : database,\n username: username === '' ? undefined : username,\n };\n}\n\n/** The URL schemes `Bun.RedisClient` accepts (its own error names exactly these seven). */\nconst VALKEY_SCHEMES: ReadonlySet<string> = new Set([\n 'redis:',\n 'valkey:',\n 'rediss:',\n 'valkeys:',\n 'redis+tls:',\n 'redis+unix:',\n 'redis+tls+unix:',\n]);\n\n/** `undefined` when `raw` is not a URL of a scheme `Bun.RedisClient` takes. */\nexport function valkeyFields(raw: string): ValkeyFields | undefined {\n const url = parse(raw);\n if (url === undefined || !VALKEY_SCHEMES.has(url.protocol)) return undefined;\n if (url.protocol.endsWith('+unix:')) return {};\n const host = url.hostname.startsWith('[') ? url.hostname.slice(1, -1) : url.hostname;\n const database = url.pathname.replace(/^\\//, '');\n return {\n host: host === '' ? undefined : host,\n port: url.port === '' ? 6379 : Number(url.port),\n database: database === '' ? undefined : database,\n };\n}\n",
|
|
8
|
-
"/**\n * Valkey as Effect's own `Redis` service, over `Bun.RedisClient`.\n *\n * ★ `Redis.make` OVER THE BUILT-IN CLIENT, NOT `@effect/platform-bun`'s `BunRedis`. Rc.115 ships\n * `BunRedis.layer`, and it would do; but a dependency on `@effect/platform-bun` drags\n * `@effect/platform-node-shared ^rc.115` in behind it, which resolves to rc.118 on a fresh\n * install and kills the process at import, and only a ROOT `overrides` fixes that (docs/pairing.md,\n * measured 2026-09-29). A library cannot ship one. So this is the same ~30 lines (`send` over\n * `client.send`), with the two things `BunRedis` lacks: a connection that must succeed before the\n * layer is built, and a deadline on every command.\n * ⛔ THE URL CARRIES THE ACL USER (`redis://seat:<password>@host:port`, percent-encoded), measured\n * 2026-09-29 against a scratch Valkey with `user default off`: an unauthenticated client is\n * refused `NOAUTH`; the seat user reads and writes its own prefix; a write outside it fails\n * `NOPERM No permissions to access a key`, and a command outside its categories fails\n * `NOPERM User seat has no permissions to run the 'flushall' command`. All of them arrive as\n * `RedisError`, and `isPermissionDenied` names the two `NOPERM` ones.\n * ⛔ NO `subscribe`. `Redis.subscribe` here fails with `RedisError`: a Valkey subscriber needs a\n * connection of its own that this layer does not open. (`BunRedis` opens one, with no reconnect;\n * a caller that needs pub/sub can use it beside this layer.)\n * ⚠️ BUN ONLY, AND SAID SO AT THE FIRST USE. `Bun.RedisClient` is loaded with `import('bun')`, so\n * importing this module under Node still works (the Postgres half is portable); BUILDING the\n * Valkey layer there fails with a `RedisError` naming the reason.\n * 🔴 `connectionTimeout` DOES NOT BOUND DNS. Measured 2026-09-29: with `connectionTimeout: 700`, a\n * host name that does not resolve failed after 31 s. `connect()` therefore also runs under an\n * Effect timeout of the same length.\n * 🔴 BUN'S OWN RECONNECT ENDS, AND THE CLIENT THEN STAYS DEAD (about 31 s of outage here): the layer\n * reconnects it itself, off `onclose` (state-valkey-connection.ts holds the measurement).\n */\nimport * as Config from 'effect/Config';\nimport * as Duration from 'effect/Duration';\nimport * as Effect from 'effect/Effect';\nimport * as Layer from 'effect/Layer';\nimport * as Redacted from 'effect/Redacted';\nimport * as Redis from 'effect/
|
|
8
|
+
"/**\n * Valkey as Effect's own `Redis` service, over `Bun.RedisClient`.\n *\n * ★ `Redis.make` OVER THE BUILT-IN CLIENT, NOT `@effect/platform-bun`'s `BunRedis`. Rc.115 ships\n * `BunRedis.layer`, and it would do; but a dependency on `@effect/platform-bun` drags\n * `@effect/platform-node-shared ^rc.115` in behind it, which resolves to rc.118 on a fresh\n * install and kills the process at import, and only a ROOT `overrides` fixes that (docs/pairing.md,\n * measured 2026-09-29). A library cannot ship one. So this is the same ~30 lines (`send` over\n * `client.send`), with the two things `BunRedis` lacks: a connection that must succeed before the\n * layer is built, and a deadline on every command.\n * ⛔ THE URL CARRIES THE ACL USER (`redis://seat:<password>@host:port`, percent-encoded), measured\n * 2026-09-29 against a scratch Valkey with `user default off`: an unauthenticated client is\n * refused `NOAUTH`; the seat user reads and writes its own prefix; a write outside it fails\n * `NOPERM No permissions to access a key`, and a command outside its categories fails\n * `NOPERM User seat has no permissions to run the 'flushall' command`. All of them arrive as\n * `RedisError`, and `isPermissionDenied` names the two `NOPERM` ones.\n * ⛔ NO `subscribe`. `Redis.subscribe` here fails with `RedisError`: a Valkey subscriber needs a\n * connection of its own that this layer does not open. (`BunRedis` opens one, with no reconnect;\n * a caller that needs pub/sub can use it beside this layer.)\n * ⚠️ BUN ONLY, AND SAID SO AT THE FIRST USE. `Bun.RedisClient` is loaded with `import('bun')`, so\n * importing this module under Node still works (the Postgres half is portable); BUILDING the\n * Valkey layer there fails with a `RedisError` naming the reason.\n * 🔴 `connectionTimeout` DOES NOT BOUND DNS. Measured 2026-09-29: with `connectionTimeout: 700`, a\n * host name that does not resolve failed after 31 s. `connect()` therefore also runs under an\n * Effect timeout of the same length.\n * 🔴 BUN'S OWN RECONNECT ENDS, AND THE CLIENT THEN STAYS DEAD (about 31 s of outage here): the layer\n * reconnects it itself, off `onclose` (state-valkey-connection.ts holds the measurement).\n */\nimport * as Config from 'effect/Config';\nimport * as Duration from 'effect/Duration';\nimport * as Effect from 'effect/Effect';\nimport * as Layer from 'effect/Layer';\nimport * as Redacted from 'effect/Redacted';\nimport * as Redis from 'effect/persistence/Redis';\nimport { valkeyFields } from './state-dsn.ts';\nimport { keepConnected } from './state-valkey-connection.ts';\nimport { instrumentedSend } from './state-valkey-send.ts';\n\nexport type ValkeyOptions = {\n /**\n * `redis://<user>:<password>@<host>:<port>[/<db>]` (or `valkey://`, `rediss://` for TLS), the\n * user and password percent-encoded. Held `Redacted`; never in a span, log or error. Required:\n * this package holds no host.\n */\n readonly url: string | Redacted.Redacted<string>;\n /**\n * How long to wait to connect and authenticate when the layer is built, and for each reconnect\n * attempt after Bun gives up. Default 5 s (Bun's own is 10 s). ⚠️ A finite positive duration of at\n * most 2 ** 31 - 1 ms, or a `RangeError` defect.\n */\n readonly connectionTimeout?: Duration.Input | undefined;\n /** How long one command may take. Default 10 s; same limits as `connectionTimeout`. */\n readonly commandTimeout?: Duration.Input | undefined;\n /**\n * How many times Bun retries a lost connection before it gives up (its default is 20, about 30\n * s of outage). After that THE LAYER reconnects until the server is back, so this only sets\n * when its own loop takes over. A non-negative integer, or a `RangeError` defect.\n */\n readonly maxRetries?: number | undefined;\n};\n\nexport const DEFAULT_CONNECTION_TIMEOUT: Duration.Duration = Duration.seconds(5);\nexport const DEFAULT_COMMAND_TIMEOUT: Duration.Duration = Duration.seconds(10);\n\n/** A duration as whole milliseconds Bun and `setTimeout` accept, or a `RangeError`. */\nfunction milliseconds(input: Duration.Input, name: string): number {\n const ms = Duration.toMillis(input);\n if (!Number.isFinite(ms) || ms <= 0 || ms > 2 ** 31 - 1) {\n throw new RangeError(`${name} must be a finite duration above 0 and at most 2 ** 31 - 1 ms`);\n }\n return Math.ceil(ms);\n}\n\n/** A failure that names the reason and never the URL. */\nconst refused = (reason: string): Redis.RedisError =>\n new Redis.RedisError({ cause: new Error(`SeatState valkey: ${reason}`) });\n\n/**\n * Whether `error` is the server's `NOPERM`: a key outside the user's prefix, or a command outside\n * its categories. ⚠️ It reads the message, because Bun reports both as one `code`\n * (`ERR_REDIS_SERVER_ERROR`) and the server's own text is the only thing that tells them apart\n * from a real server fault.\n */\nexport function isPermissionDenied(error: unknown): boolean {\n if (!(error instanceof Redis.RedisError)) return false;\n const cause: unknown = error.cause;\n const message = cause instanceof Error ? cause.message : String(cause);\n return message.startsWith('NOPERM');\n}\n\n/** Subscribing is not offered; see the header. */\nconst subscribe = (): Effect.Effect<never, Redis.RedisError> =>\n Effect.fail(refused('subscribe is not supported here (a subscriber needs its own connection)'));\n\n/** Loads `Bun.RedisClient`, or fails naming why it is not there (Node, workerd). */\nconst loadClient = Effect.tryPromise({\n try: async () => (await import('bun')).RedisClient,\n catch: () => refused('needs Bun (Bun.RedisClient); this runtime does not have it'),\n});\n\nconst build = Effect.fnUntraced(function* (options: ValkeyOptions) {\n const url = Redacted.value(\n typeof options.url === 'string' ? Redacted.make(options.url) : options.url,\n );\n const fields = valkeyFields(url);\n if (fields === undefined) {\n return yield* refused('the URL is not a redis, valkey, rediss or valkeys URL');\n }\n const connectionTimeout = options.connectionTimeout ?? DEFAULT_CONNECTION_TIMEOUT;\n const commandTimeout = options.commandTimeout ?? DEFAULT_COMMAND_TIMEOUT;\n const connectMs = milliseconds(connectionTimeout, 'connectionTimeout');\n const commandMs = milliseconds(commandTimeout, 'commandTimeout');\n const maxRetries = options.maxRetries;\n if (maxRetries !== undefined && !(Number.isInteger(maxRetries) && maxRetries >= 0)) {\n throw new RangeError('maxRetries must be a non-negative integer');\n }\n const RedisClient = yield* loadClient;\n // ⛔ OFFLINE QUEUE OFF: a command sent while the connection is down fails at once as\n // `RedisError` (a seat retries in Effect, where an attempt is a span) instead of waiting for\n // a reconnect that may not come. Measured 2026-09-29: after a server restart the same client\n // answered again within 1.5 s; past Bun's retry budget the layer reconnects behind the\n // caller's retry (state-valkey-connection.ts). It is also why the layer connects first: an\n // unconnected client with the queue off refuses every command.\n const client = yield* Effect.acquireRelease(\n Effect.try({\n try: () =>\n new RedisClient(url, {\n connectionTimeout: connectMs,\n enableOfflineQueue: false,\n ...(maxRetries === undefined ? {} : { maxRetries }),\n }),\n catch: () => refused('Bun.RedisClient refused the URL'),\n }),\n (opened) => Effect.sync(() => opened.close()),\n );\n yield* Effect.tryPromise({\n try: () => client.connect(),\n catch: (cause) => new Redis.RedisError({ cause }),\n }).pipe(\n Effect.timeoutOrElse({\n duration: connectionTimeout,\n orElse: () => Effect.fail(refused(`did not connect within ${String(connectMs)} ms`)),\n }),\n );\n // ⚠️ Address and namespace only: the URL's user and password never reach an attribute.\n const attributes = {\n 'db.system.name': 'redis',\n ...(fields.host === undefined ? {} : { 'server.address': fields.host }),\n ...(fields.port === undefined ? {} : { 'server.port': fields.port }),\n ...(fields.database === undefined ? {} : { 'db.namespace': fields.database }),\n };\n // ⛔ AFTER the first connect, so a wrong URL or password fails the build instead of being retried.\n yield* keepConnected(client, { connectionTimeout, attributes });\n return yield* Redis.make({\n send: instrumentedSend(client, { commandTimeout, commandTimeoutMs: commandMs, attributes }),\n subscribe,\n });\n});\n\n/**\n * The `Redis` service against one Valkey. ⛔ BUILDING IT CONNECTS AND AUTHENTICATES, so a wrong\n * URL, user or password fails at startup as `RedisError`, not at the first command (Bun reports a\n * refused password as `Connection closed`, the same text as a server that is down).\n * The client is closed when the layer's scope closes.\n */\nexport const valkey = (options: ValkeyOptions): Layer.Layer<Redis.Redis, Redis.RedisError> =>\n Layer.effect(\n Redis.Redis,\n Effect.suspend(() => build(options)),\n );\n\n/** The environment variable `valkeyFromEnv` reads unless told another. Not a host. */\nexport const VALKEY_URL_VARIABLE = 'SEAT_VALKEY_URL';\n\nexport type ValkeyFromEnvOptions = Omit<ValkeyOptions, 'url'> & {\n /** The variable holding the URL. Default `SEAT_VALKEY_URL`. */\n readonly variable?: string | undefined;\n};\n\n/**\n * `valkey`, its URL read from an environment variable as a `Redacted` secret. A missing variable\n * fails as `ConfigError`, naming the variable and nothing else.\n */\nexport const valkeyFromEnv = (\n options?: ValkeyFromEnvOptions,\n): Layer.Layer<Redis.Redis, Redis.RedisError | Config.ConfigError> => {\n const { variable, ...rest } = options ?? {};\n return Layer.unwrap(\n Config.Redacted(variable ?? VALKEY_URL_VARIABLE).pipe(\n Effect.map((url) => valkey({ ...rest, url })),\n ),\n );\n};\n",
|
|
9
9
|
"/**\n * Keeping one `Bun.RedisClient` connected for as long as its layer lives.\n *\n * 🔴 BUN GIVES UP, AND A CLIENT THAT HAS GIVEN UP STAYS DEAD. Measured 2026-09-29 (Bun 1.4.0, a raw\n * client with `enableOfflineQueue: false` and the default `maxRetries` of 20, the server killed\n * for 60 s, then restarted): Bun retried on its own for 31 s (commands failed `Connection is\n * closed and offline queue is disabled`), then GAVE UP: `onclose` ran once and every command\n * after failed `Connection has failed`, still so 10 s after the server was back. Only calling\n * `connect()` again revived it (it resolved at once and `PING` answered). The budget is a sum of\n * backoffs, so it is not one number: the review that found this measured recovery after 30 s and\n * none after 45 s. A seat whose Valkey restarts for a minute (a container restart, CT100 booting)\n * would keep a dead client until the process restarted, and no retry in Effect could help, since\n * every retry hit the same dead client.\n * ★ SO THE LAYER RECONNECTS ITSELF, off `onclose`: single-flight, each attempt bounded by\n * `connectionTimeout` (which does not bound DNS; see state-valkey.ts), backing off 250 ms up to\n * 5 s between attempts, one `valkey reconnect` span per attempt and a warning and an info line\n * around the outage. It stops when the layer's scope closes.\n * ⚠️ `onclose` IS A HINT AND `connected` IS THE FACT. Measured on the same client: `onclose` also\n * runs for every `connect()` that fails (each rejected after ~155 ms with `Connection closed`)\n * and for the client's own `close()`, so a reconnect that trusted it would start itself. The\n * loop therefore runs only while `connected` is false, and a wake-up that arrives after a\n * successful reconnect finds nothing to do.\n * ⚠️ COMMANDS STILL FAIL AT ONCE WHILE IT IS DOWN (the offline queue is off on purpose): the\n * caller's retry is what spans an outage, and from now on the retry is answered once this loop\n * has reconnected, within the pause plus one attempt of the server coming back.\n */\nimport type { RedisClient } from 'bun';\nimport * as Duration from 'effect/Duration';\nimport * as Effect from 'effect/Effect';\nimport * as Queue from 'effect/Queue';\nimport type * as Scope from 'effect/Scope';\n\n/** What this file needs of `Bun.RedisClient`. */\nexport type Reconnectable = Pick<RedisClient, 'connected' | 'connect' | 'onclose'>;\n\nexport type KeepOptions = {\n /** How long one reconnect attempt may take before it is abandoned and retried. */\n readonly connectionTimeout: Duration.Input;\n /** Span attributes every attempt carries: the server address, no secrets. */\n readonly attributes: Readonly<Record<string, unknown>>;\n /** The pause after the first failed attempt, doubled up to `longestPause`. Default 250 ms. */\n readonly firstPause?: Duration.Input | undefined;\n /** The longest pause between attempts. Default 5 s. */\n readonly longestPause?: Duration.Input | undefined;\n};\n\nconst DEFAULT_FIRST_PAUSE_MS = 250;\nconst DEFAULT_LONGEST_PAUSE_MS = 5000;\n\n/**\n * Reconnect `client` whenever Bun gives up on it, for the life of the enclosing scope. Call it\n * AFTER the first `connect()` succeeded, so a wrong URL or password still fails the layer's build\n * instead of being retried forever.\n */\nexport const keepConnected: (\n client: Reconnectable,\n options: KeepOptions,\n) => Effect.Effect<void, never, Scope.Scope> = Effect.fnUntraced(function* (\n client: Reconnectable,\n options: KeepOptions,\n) {\n const wake = yield* Queue.sliding<void>(1);\n const firstPause = Duration.toMillis(options.firstPause ?? DEFAULT_FIRST_PAUSE_MS);\n const longestPause = Duration.toMillis(options.longestPause ?? DEFAULT_LONGEST_PAUSE_MS);\n // ⛔ ONE `connect()` IN FLIGHT AT A TIME. An attempt that times out is abandoned, but Bun's own\n // promise is still pending (a name that does not resolve took 31 s), and a second `connect()`\n // on top of it is not something Bun documents. The next attempt awaits the same promise.\n let connecting: Promise<void> | undefined;\n const connectOnce = (): Promise<void> => {\n connecting ??= client.connect().finally(() => {\n connecting = undefined;\n });\n return connecting;\n };\n const attempt = Effect.tryPromise({ try: connectOnce, catch: (cause) => cause }).pipe(\n Effect.timeoutOrElse({\n duration: options.connectionTimeout,\n orElse: () => Effect.fail(new Error('a reconnect attempt did not finish in time')),\n }),\n Effect.withSpan('valkey reconnect', { kind: 'client', attributes: options.attributes }),\n Effect.as(true),\n Effect.orElseSucceed(() => false),\n );\n const reconnect = Effect.gen(function* () {\n if (client.connected) return;\n yield* Effect.logWarning('SeatState valkey: the connection is down, reconnecting');\n let pause = firstPause;\n let attempts = 0;\n while (!client.connected) {\n attempts += 1;\n yield* attempt;\n // ⚠️ `connected` decides, not the attempt's own answer: an attempt that resolved without the\n // client being connected must pause like a failed one, not spin.\n if (client.connected) break;\n yield* Effect.sleep(pause);\n pause = Math.min(pause * 2, longestPause);\n }\n yield* Effect.logInfo(`SeatState valkey: reconnected after ${String(attempts)} attempt(s)`);\n });\n client.onclose = () => {\n Queue.offerUnsafe(wake, undefined);\n };\n // ⛔ A NO-OP, NEVER `null`: measured 2026-09-29 (Bun 1.4.0), `client.onclose = null` (or\n // `undefined`) is accepted, and then `close()` calls the null and throws `TypeError: ... is not\n // a function` (worded with whatever call site is on the stack, `c.close()` or a fiber's\n // `this[args]()`). bun-types types `onclose` as `... | null`; Bun does not honour it.\n yield* Effect.addFinalizer(() =>\n Effect.sync(() => {\n client.onclose = () => {};\n }),\n );\n yield* Effect.forkScoped(Effect.forever(Queue.take(wake).pipe(Effect.andThen(reconnect))));\n});\n",
|
|
10
|
-
"/**\n * One Valkey command as a span, with a deadline, failing as `RedisError`.\n *\n * ⛔ A SPAN NAMES THE COMMAND, NEVER ITS ARGUMENTS. Keys are seat data and values are whatever the\n * seat stored; `AUTH` and `HELLO ... AUTH` carry a password. Only the upper-cased command name\n * (`SET`, `GET`) is recorded, so the span is low-cardinality and holds nothing to redact.\n * 🔴 NOR IS THE ERROR TEXT: the server quotes the arguments of a call it refuses (`unknown command\n * 'X', with args beginning with: '<key>' '<value>'`), and a failed span exports its error, so\n * what a caller receives is `scrubbedError`, never Bun's own (state-valkey-scrub.ts).\n * 🔴 THE DEADLINE IS NOT OPTIONAL. Measured 2026-09-29 (Bun 1.4.0): a `Bun.RedisClient` on its\n * defaults that has lost its server QUEUES every command and never answers it until the server\n * is back (a `send` sat unresolved past 8 s against a dead port; after a restart the queued\n * command completed ~5 s later). With the offline queue off (`state-valkey.ts` sets it) a dropped\n * connection fails at once, but a peer that holds the socket open and says nothing would still\n * wedge a seat forever, so every command carries `commandTimeout` too.\n */\nimport * as Effect from 'effect/Effect';\nimport * as Redis from 'effect/
|
|
10
|
+
"/**\n * One Valkey command as a span, with a deadline, failing as `RedisError`.\n *\n * ⛔ A SPAN NAMES THE COMMAND, NEVER ITS ARGUMENTS. Keys are seat data and values are whatever the\n * seat stored; `AUTH` and `HELLO ... AUTH` carry a password. Only the upper-cased command name\n * (`SET`, `GET`) is recorded, so the span is low-cardinality and holds nothing to redact.\n * 🔴 NOR IS THE ERROR TEXT: the server quotes the arguments of a call it refuses (`unknown command\n * 'X', with args beginning with: '<key>' '<value>'`), and a failed span exports its error, so\n * what a caller receives is `scrubbedError`, never Bun's own (state-valkey-scrub.ts).\n * 🔴 THE DEADLINE IS NOT OPTIONAL. Measured 2026-09-29 (Bun 1.4.0): a `Bun.RedisClient` on its\n * defaults that has lost its server QUEUES every command and never answers it until the server\n * is back (a `send` sat unresolved past 8 s against a dead port; after a restart the queued\n * command completed ~5 s later). With the offline queue off (`state-valkey.ts` sets it) a dropped\n * connection fails at once, but a peer that holds the socket open and says nothing would still\n * wedge a seat forever, so every command carries `commandTimeout` too.\n */\nimport * as Effect from 'effect/Effect';\nimport * as Redis from 'effect/persistence/Redis';\nimport type * as Duration from 'effect/Duration';\nimport { scrubbedError } from './state-valkey-scrub.ts';\n\n/** What this file needs of a client: `Bun.RedisClient` has this exact `send`. */\nexport type ValkeyCall = {\n readonly send: (command: string, args: Array<string>) => Promise<unknown>;\n};\n\n/** `send` as `Redis.make` wants it. */\nexport type Send = <A = unknown>(\n command: string,\n ...args: ReadonlyArray<string>\n) => Effect.Effect<A, Redis.RedisError>;\n\nexport type SendOptions = {\n /** How long one command may take before it fails as `RedisError`. */\n readonly commandTimeout: Duration.Input;\n /** The command deadline in milliseconds, for the message only. */\n readonly commandTimeoutMs: number;\n /** Span attributes every command carries: `db.system.name`, the server address, no secrets. */\n readonly attributes: Readonly<Record<string, unknown>>;\n};\n\nexport function instrumentedSend(client: ValkeyCall, options: SendOptions): Send {\n return <A = unknown>(command: string, ...args: ReadonlyArray<string>) => {\n const name = command.toUpperCase();\n return Effect.tryPromise({\n // ⚠️ Bun's `send` types its argument list as a mutable array; ours is readonly.\n try: () => client.send(command, [...args]) as Promise<A>,\n catch: (cause) => new Redis.RedisError({ cause: scrubbedError(cause, command) }),\n }).pipe(\n Effect.timeoutOrElse({\n duration: options.commandTimeout,\n orElse: () =>\n Effect.fail(\n new Redis.RedisError({\n cause: new Error(\n `valkey ${name} did not answer within ${String(options.commandTimeoutMs)} ms`,\n ),\n }),\n ),\n }),\n Effect.withSpan(`valkey ${name}`, {\n kind: 'client',\n attributes: { ...options.attributes, 'db.operation.name': name },\n }),\n );\n };\n}\n",
|
|
11
11
|
"/**\n * What a Valkey error may say once it has left the layer.\n *\n * 🔴 THE SERVER'S ERROR TEXT ECHOES THE ARGUMENTS OF THE CALL THAT FAILED. Measured 2026-09-29\n * (Valkey 9.1.1, through `Bun.RedisClient`, with canary values):\n * ERR unknown command 'JSON.SET', with args beginning with: 'seat:key-…' '$' '{\"secret\":…}'\n * ERR unknown subcommand 'seat:key-…'. Try OBJECT HELP.\n * Keys are seat data and values are whatever the seat stored, and Effect's `OtlpTracer` exports\n * every FAILED span's error as `exception.message` and `exception.stacktrace` (with the whole\n * `cause` chain, `includeCauseInStack: true`), so those two errors put a key and a value into\n * the exported trace: the OTLP payload a Victoria service would receive held both (a wire test\n * in tests/state-error-text.test.ts asserts it now does not). A `Logger` that prints a failed\n * call does the same.\n * ★ SO THE ERROR THAT LEAVES THE LAYER IS A NEW `Error` WITH THE ARGUMENTS CUT OUT, and the\n * original is not chained (a `cause` would carry its text straight back into the stack). Kept:\n * Bun's `code` and `name`, and the server's own words up to the first quote that opens\n * something other than the command that was sent. That keeps `NOPERM User seat has no\n * permissions to run the 'flushall' command` whole (so `isPermissionDenied` and the wording are\n * unchanged) and cuts `unknown command 'JSON.SET', with args beginning with: ` at the first\n * argument, even when an argument holds a quote itself.\n * ⚠️ WHAT THIS DOES NOT COVER, said plainly: a server text that echoes an argument WITHOUT quotes,\n * and the text a Lua script raises itself (`error(...)` comes back as `ERR user_script:1: <text>`;\n * measured). Bun's own errors (`Connection closed`, a timeout) quote nothing.\n */\n\n/** What replaces the arguments. */\nexport const OMITTED = '[arguments omitted]';\n\n/**\n * `message` up to the first quoted thing that is not `command` (or one of its subcommands, which\n * the server writes `command|sub`), with `OMITTED` where the rest was.\n */\nexport function cutArguments(message: string, command: string): string {\n const name = command.toLowerCase();\n let at = message.indexOf(\"'\");\n while (at !== -1) {\n const close = message.indexOf(\"'\", at + 1);\n const quoted = close === -1 ? undefined : message.slice(at + 1, close).toLowerCase();\n if (quoted === undefined || !(quoted === name || quoted.startsWith(`${name}|`))) {\n return `${message.slice(0, at)}${OMITTED}`;\n }\n at = message.indexOf(\"'\", close + 1);\n }\n return message;\n}\n\n/**\n * The client's rejection as an `Error` that is safe to put in a span, a log or a `RedisError`:\n * a fresh one, keeping Bun's `name` and `code`, with the arguments cut out of the text.\n */\nexport function scrubbedError(cause: unknown, command: string): Error {\n const original = cause instanceof Error ? cause : undefined;\n const scrubbed = new Error(cutArguments(original?.message ?? String(cause), command));\n if (original !== undefined) {\n scrubbed.name = original.name;\n const code: unknown = (original as { readonly code?: unknown }).code;\n if (typeof code === 'string') Object.assign(scrubbed, { code });\n }\n return scrubbed;\n}\n"
|
|
12
12
|
],
|
|
13
13
|
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAWA;;;ACSA;AACA;AACA;AACA;AACA;AACA;AAEA;;;ACcA,IAAM,kBAAuC,IAAI,IAAI,CAAC,QAAQ,QAAQ,QAAQ,QAAQ,CAAC;AAGvF,SAAS,MAAM,CAAC,OAAmC;AAAA,EACjD,IAAI;AAAA,IACF,OAAO,mBAAmB,KAAK;AAAA,IAC/B,MAAM;AAAA,IACN;AAAA;AAAA;AAKJ,SAAS,KAAK,CAAC,KAA8B;AAAA,EAC3C,IAAI;AAAA,IACF,OAAO,IAAI,IAAI,GAAG;AAAA,IAClB,MAAM;AAAA,IACN;AAAA;AAAA;AAQG,SAAS,cAAc,CAAC,KAAyC;AAAA,EACtE,MAAM,MAAM,MAAM,GAAG;AAAA,EACrB,IAAI,QAAQ;AAAA,IAAW;AAAA,EACvB,WAAW,OAAO,IAAI,aAAa,KAAK;AAAA,IAAG,IAAI,gBAAgB,IAAI,GAAG;AAAA,MAAG,OAAO,CAAC;AAAA,EACjF,MAAM,WACJ,IAAI,SAAS,WAAW,GAAG,KAAK,IAAI,SAAS,SAAS,GAAG,IACrD,IAAI,SAAS,MAAM,GAAG,EAAE,IACxB,OAAO,IAAI,QAAQ;AAAA,EACzB,MAAM,WAAW,OAAO,IAAI,SAAS,QAAQ,OAAO,EAAE,CAAC;AAAA,EACvD,MAAM,WAAW,OAAO,IAAI,QAAQ;AAAA,EACpC,MAAM,OAAO,IAAI,SAAS,KAAK,YAAY,OAAO,IAAI,IAAI;AAAA,EAG1D,IAAI,aAAa,aAAa,aAAa,aAAa,aAAa;AAAA,IAAW,OAAO,CAAC;AAAA,EACxF,IAAI,SAAS,aAAa,EAAE,OAAO,UAAU,IAAI,KAAK,QAAQ,KAAK,QAAQ;AAAA,IAAQ,OAAO,CAAC;AAAA,EAC3F,OAAO;AAAA,IACL,MAAM,aAAa,KAAK,YAAY;AAAA,IACpC;AAAA,IACA,UAAU,aAAa,KAAK,YAAY;AAAA,IACxC,UAAU,aAAa,KAAK,YAAY;AAAA,EAC1C;AAAA;AAIF,IAAM,iBAAsC,IAAI,IAAI;AAAA,EAClD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGM,SAAS,YAAY,CAAC,KAAuC;AAAA,EAClE,MAAM,MAAM,MAAM,GAAG;AAAA,EACrB,IAAI,QAAQ,aAAa,CAAC,eAAe,IAAI,IAAI,QAAQ;AAAA,IAAG;AAAA,EAC5D,IAAI,IAAI,SAAS,SAAS,QAAQ;AAAA,IAAG,OAAO,CAAC;AAAA,EAC7C,MAAM,OAAO,IAAI,SAAS,WAAW,GAAG,IAAI,IAAI,SAAS,MAAM,GAAG,EAAE,IAAI,IAAI;AAAA,EAC5E,MAAM,WAAW,IAAI,SAAS,QAAQ,OAAO,EAAE;AAAA,EAC/C,OAAO;AAAA,IACL,MAAM,SAAS,KAAK,YAAY;AAAA,IAChC,MAAM,IAAI,SAAS,KAAK,OAAO,OAAO,IAAI,IAAI;AAAA,IAC9C,UAAU,aAAa,KAAK,YAAY;AAAA,EAC1C;AAAA;;;AD/DK,IAAM,wBAAwB;AAErC,IAAM,0BAAsD,iBAAQ,CAAC;AAGrE,IAAM,UAAU,CAAC,YACf,IAAI,SAAS;AAAA,EACX,QAAQ,IAAI,gBAAgB;AAAA,IAC1B,OAAO,IAAI,MAAM,OAAO;AAAA,IACxB,SAAS,uBAAuB;AAAA,IAChC,WAAW;AAAA,EACb,CAAC;AACH,CAAC;AAEH,IAAM,UAAiB,kBAAW,UAAU,CAAC,SAA0B;AAAA,EACrE,MAAM,MAAe,gBACnB,OAAO,QAAQ,QAAQ,WAAoB,eAAK,QAAQ,GAAG,IAAI,QAAQ,GACzE;AAAA,EACA,MAAM,SAAS,eAAe,GAAG;AAAA,EACjC,IAAI,WAAW;AAAA,IAAW,OAAO,OAAO,QAAQ,kCAAkC;AAAA,EAClF,MAAM,iBAAiB,QAAQ,kBAAkB;AAAA,EACjD,MAAM,SAAS,OAAO,SAAS,KAAK;AAAA,IAClC,KAAc,eAAK,GAAG;AAAA,OACnB;AAAA,IACH,gBAAgB,QAAQ;AAAA,IACxB;AAAA,IACA,iBAAiB,QAAQ;AAAA,EAC3B,CAAC;AAAA,EACD,OAAO,iBAAiB,KACf,qBAAc;AAAA,IACnB,UAAU;AAAA,IACV,QAAQ,MACC,YACL,QAAQ,yBAAyB,OAAgB,kBAAS,cAAc,CAAC,MAAM,CACjF;AAAA,EACJ,CAAC,CACH;AAAA,EACA,OAAO;AAAA,CACR;AAKM,IAAM,WAAW,CACtB,YAEA,SAAS,UAAiB,eAAQ,MAAM,QAAQ,OAAO,CAAC,CAAC;AAWpD,IAAM,kBAAkB,CAC7B,YACwF;AAAA,EACxF,QAAQ,aAAa,SAAS,WAAW,CAAC;AAAA,EAC1C,OAAa,aACJ,gBAAS,YAAY,qBAAqB,EAAE,KAC1C,WAAI,CAAC,QAAQ,SAAS,KAAK,MAAM,IAAI,CAAC,CAAC,CAChD,CACF;AAAA;;;AEpFF;AACA;AACA;AACA;AACA;AACA;;;ACNA;AACA;AACA;AAiBA,IAAM,yBAAyB;AAC/B,IAAM,2BAA2B;AAO1B,IAAM,gBAGyC,mBAAW,UAAU,CACzE,QACA,SACA;AAAA,EACA,MAAM,OAAO,OAAa,cAAc,CAAC;AAAA,EACzC,MAAM,aAAsB,mBAAS,QAAQ,cAAc,sBAAsB;AAAA,EACjF,MAAM,eAAwB,mBAAS,QAAQ,gBAAgB,wBAAwB;AAAA,EAIvF,IAAI;AAAA,EACJ,MAAM,cAAc,MAAqB;AAAA,IACvC,eAAe,OAAO,QAAQ,EAAE,QAAQ,MAAM;AAAA,MAC5C,aAAa;AAAA,KACd;AAAA,IACD,OAAO;AAAA;AAAA,EAET,MAAM,UAAiB,mBAAW,EAAE,KAAK,aAAa,OAAO,CAAC,UAAU,MAAM,CAAC,EAAE,KACxE,sBAAc;AAAA,IACnB,UAAU,QAAQ;AAAA,IAClB,QAAQ,MAAa,aAAK,IAAI,MAAM,4CAA4C,CAAC;AAAA,EACnF,CAAC,GACM,iBAAS,oBAAoB,EAAE,MAAM,UAAU,YAAY,QAAQ,WAAW,CAAC,GAC/E,WAAG,IAAI,GACP,sBAAc,MAAM,KAAK,CAClC;AAAA,EACA,MAAM,YAAmB,YAAI,UAAU,GAAG;AAAA,IACxC,IAAI,OAAO;AAAA,MAAW;AAAA,IACtB,OAAc,mBAAW,wDAAwD;AAAA,IACjF,IAAI,QAAQ;AAAA,IACZ,IAAI,WAAW;AAAA,IACf,OAAO,CAAC,OAAO,WAAW;AAAA,MACxB,YAAY;AAAA,MACZ,OAAO;AAAA,MAGP,IAAI,OAAO;AAAA,QAAW;AAAA,MACtB,OAAc,cAAM,KAAK;AAAA,MACzB,QAAQ,KAAK,IAAI,QAAQ,GAAG,YAAY;AAAA,IAC1C;AAAA,IACA,OAAc,gBAAQ,uCAAuC,OAAO,QAAQ,cAAc;AAAA,GAC3F;AAAA,EACD,OAAO,UAAU,MAAM;AAAA,IACf,kBAAY,MAAM,SAAS;AAAA;AAAA,EAMnC,OAAc,qBAAa,MAClB,aAAK,MAAM;AAAA,IAChB,OAAO,UAAU,MAAM;AAAA,GACxB,CACH;AAAA,EACA,OAAc,mBAAkB,gBAAc,WAAK,IAAI,EAAE,KAAY,gBAAQ,SAAS,CAAC,CAAC,CAAC;AAAA,CAC1F;;;AChGD;AACA;;;ACSO,IAAM,UAAU;AAMhB,SAAS,YAAY,CAAC,SAAiB,SAAyB;AAAA,EACrE,MAAM,OAAO,QAAQ,YAAY;AAAA,EACjC,IAAI,KAAK,QAAQ,QAAQ,GAAG;AAAA,EAC5B,OAAO,OAAO,IAAI;AAAA,IAChB,MAAM,QAAQ,QAAQ,QAAQ,KAAK,KAAK,CAAC;AAAA,IACzC,MAAM,SAAS,UAAU,KAAK,YAAY,QAAQ,MAAM,KAAK,GAAG,KAAK,EAAE,YAAY;AAAA,IACnF,IAAI,WAAW,aAAa,EAAE,WAAW,QAAQ,OAAO,WAAW,GAAG,OAAO,IAAI;AAAA,MAC/E,OAAO,GAAG,QAAQ,MAAM,GAAG,EAAE,IAAI;AAAA,IACnC;AAAA,IACA,KAAK,QAAQ,QAAQ,KAAK,QAAQ,CAAC;AAAA,EACrC;AAAA,EACA,OAAO;AAAA;AAOF,SAAS,aAAa,CAAC,OAAgB,SAAwB;AAAA,EACpE,MAAM,WAAW,iBAAiB,QAAQ,QAAQ;AAAA,EAClD,MAAM,WAAW,IAAI,MAAM,aAAa,UAAU,WAAW,OAAO,KAAK,GAAG,OAAO,CAAC;AAAA,EACpF,IAAI,aAAa,WAAW;AAAA,IAC1B,SAAS,OAAO,SAAS;AAAA,IACzB,MAAM,OAAiB,SAAyC;AAAA,IAChE,IAAI,OAAO,SAAS;AAAA,MAAU,OAAO,OAAO,UAAU,EAAE,KAAK,CAAC;AAAA,EAChE;AAAA,EACA,OAAO;AAAA;;;ADjBF,SAAS,gBAAgB,CAAC,QAAoB,SAA4B;AAAA,EAC/E,OAAO,CAAc,YAAoB,SAAgC;AAAA,IACvE,MAAM,OAAO,QAAQ,YAAY;AAAA,IACjC,OAAc,mBAAW;AAAA,MAEvB,KAAK,MAAM,OAAO,KAAK,SAAS,CAAC,GAAG,IAAI,CAAC;AAAA,MACzC,OAAO,CAAC,UAAU,IAAU,iBAAW,EAAE,OAAO,cAAc,OAAO,OAAO,EAAE,CAAC;AAAA,IACjF,CAAC,EAAE,KACM,sBAAc;AAAA,MACnB,UAAU,QAAQ;AAAA,MAClB,QAAQ,MACC,aACL,IAAU,iBAAW;AAAA,QACnB,OAAO,IAAI,MACT,UAAU,8BAA8B,OAAO,QAAQ,gBAAgB,MACzE;AAAA,MACF,CAAC,CACH;AAAA,IACJ,CAAC,GACM,iBAAS,UAAU,QAAQ;AAAA,MAChC,MAAM;AAAA,MACN,YAAY,KAAK,QAAQ,YAAY,qBAAqB,KAAK;AAAA,IACjE,CAAC,CACH;AAAA;AAAA;;;AFHG,IAAM,6BAAyD,kBAAQ,CAAC;AACxE,IAAM,0BAAsD,kBAAQ,EAAE;AAG7E,SAAS,YAAY,CAAC,OAAuB,MAAsB;AAAA,EACjE,MAAM,KAAc,mBAAS,KAAK;AAAA,EAClC,IAAI,CAAC,OAAO,SAAS,EAAE,KAAK,MAAM,KAAK,KAAK,KAAK,KAAK,GAAG;AAAA,IACvD,MAAM,IAAI,WAAW,GAAG,mEAAmE;AAAA,EAC7F;AAAA,EACA,OAAO,KAAK,KAAK,EAAE;AAAA;AAIrB,IAAM,WAAU,CAAC,WACf,IAAU,kBAAW,EAAE,OAAO,IAAI,MAAM,qBAAqB,QAAQ,EAAE,CAAC;AAQnE,SAAS,kBAAkB,CAAC,OAAyB;AAAA,EAC1D,IAAI,EAAE,iBAAuB;AAAA,IAAa,OAAO;AAAA,EACjD,MAAM,QAAiB,MAAM;AAAA,EAC7B,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,EACrE,OAAO,QAAQ,WAAW,QAAQ;AAAA;AAIpC,IAAM,YAAY,MACT,aAAK,SAAQ,yEAAyE,CAAC;AAGhG,IAAM,aAAoB,mBAAW;AAAA,EACnC,KAAK,aAAa,MAAa,eAAQ;AAAA,EACvC,OAAO,MAAM,SAAQ,4DAA4D;AACnF,CAAC;AAED,IAAM,QAAe,mBAAW,UAAU,CAAC,SAAwB;AAAA,EACjE,MAAM,MAAe,gBACnB,OAAO,QAAQ,QAAQ,WAAoB,eAAK,QAAQ,GAAG,IAAI,QAAQ,GACzE;AAAA,EACA,MAAM,SAAS,aAAa,GAAG;AAAA,EAC/B,IAAI,WAAW,WAAW;AAAA,IACxB,OAAO,OAAO,SAAQ,uDAAuD;AAAA,EAC/E;AAAA,EACA,MAAM,oBAAoB,QAAQ,qBAAqB;AAAA,EACvD,MAAM,iBAAiB,QAAQ,kBAAkB;AAAA,EACjD,MAAM,YAAY,aAAa,mBAAmB,mBAAmB;AAAA,EACrE,MAAM,YAAY,aAAa,gBAAgB,gBAAgB;AAAA,EAC/D,MAAM,aAAa,QAAQ;AAAA,EAC3B,IAAI,eAAe,aAAa,EAAE,OAAO,UAAU,UAAU,KAAK,cAAc,IAAI;AAAA,IAClF,MAAM,IAAI,WAAW,2CAA2C;AAAA,EAClE;AAAA,EACA,MAAM,cAAc,OAAO;AAAA,EAO3B,MAAM,SAAS,OAAc,uBACpB,YAAI;AAAA,IACT,KAAK,MACH,IAAI,YAAY,KAAK;AAAA,MACnB,mBAAmB;AAAA,MACnB,oBAAoB;AAAA,SAChB,eAAe,YAAY,CAAC,IAAI,EAAE,WAAW;AAAA,IACnD,CAAC;AAAA,IACH,OAAO,MAAM,SAAQ,iCAAiC;AAAA,EACxD,CAAC,GACD,CAAC,WAAkB,aAAK,MAAM,OAAO,MAAM,CAAC,CAC9C;AAAA,EACA,OAAc,mBAAW;AAAA,IACvB,KAAK,MAAM,OAAO,QAAQ;AAAA,IAC1B,OAAO,CAAC,UAAU,IAAU,kBAAW,EAAE,MAAM,CAAC;AAAA,EAClD,CAAC,EAAE,KACM,sBAAc;AAAA,IACnB,UAAU;AAAA,IACV,QAAQ,MAAa,aAAK,SAAQ,0BAA0B,OAAO,SAAS,MAAM,CAAC;AAAA,EACrF,CAAC,CACH;AAAA,EAEA,MAAM,aAAa;AAAA,IACjB,kBAAkB;AAAA,OACd,OAAO,SAAS,YAAY,CAAC,IAAI,EAAE,kBAAkB,OAAO,KAAK;AAAA,OACjE,OAAO,SAAS,YAAY,CAAC,IAAI,EAAE,eAAe,OAAO,KAAK;AAAA,OAC9D,OAAO,aAAa,YAAY,CAAC,IAAI,EAAE,gBAAgB,OAAO,SAAS;AAAA,EAC7E;AAAA,EAEA,OAAO,cAAc,QAAQ,EAAE,mBAAmB,WAAW,CAAC;AAAA,EAC9D,OAAO,OAAa,YAAK;AAAA,IACvB,MAAM,iBAAiB,QAAQ,EAAE,gBAAgB,kBAAkB,WAAW,WAAW,CAAC;AAAA,IAC1F;AAAA,EACF,CAAC;AAAA,CACF;AAQM,IAAM,SAAS,CAAC,YACf,cACE,cACC,gBAAQ,MAAM,MAAM,OAAO,CAAC,CACrC;AAGK,IAAM,sBAAsB;AAW5B,IAAM,gBAAgB,CAC3B,YACoE;AAAA,EACpE,QAAQ,aAAa,SAAS,WAAW,CAAC;AAAA,EAC1C,OAAa,cACJ,iBAAS,YAAY,mBAAmB,EAAE,KACxC,YAAI,CAAC,QAAQ,OAAO,KAAK,MAAM,IAAI,CAAC,CAAC,CAC9C,CACF;AAAA;;;AH7JF,qBAAS;AAGF,IAAM,QAAQ,CAAC,YAMX,gBAAS,SAAS,QAAQ,QAAQ,GAAG,OAAO,QAAQ,MAAM,CAAC;AAG/D,IAAM,eAAe,CAAC,YAMlB,gBAAS,gBAAgB,SAAS,QAAQ,GAAG,cAAc,SAAS,MAAM,CAAC;",
|
|
14
|
-
"debugId": "
|
|
14
|
+
"debugId": "A291DE77D2DBD33F64756E2164756E21",
|
|
15
15
|
"names": []
|
|
16
16
|
}
|
package/docs/pairing.md
CHANGED
|
@@ -13,7 +13,9 @@ Measured 2026-09-29 (a scratch install, then this package's tests and smoke):
|
|
|
13
13
|
- ⚠️ compat beta.107 beside effect rc.115 also installed and passed the same small surface
|
|
14
14
|
(the scout's `pairBeta`). Nothing wider was tried, so the rule stays **same exact rc**.
|
|
15
15
|
- 🔴 **rc.118 drops the `unstable/` prefix**: `effect/unstable/ai` becomes `effect/ai`. rc.116
|
|
16
|
-
and rc.117 keep it.
|
|
16
|
+
and rc.117 keep it. ★ Since 2026-10-06 the estate is on effect 4.0.1 and the kit imports the
|
|
17
|
+
prefix-free paths (`effect/ai`); the rc.115 measurements on this page are historical. Do not
|
|
18
|
+
bump one package alone.
|
|
17
19
|
- 🔴 **`@effect/platform-node-shared` resolves to rc.118** under `@effect/platform-bun`
|
|
18
20
|
rc.115 on a fresh install, and the process dies at import (`Cannot find module
|
|
19
21
|
effect/process/ChildProcess`). Only a **root** `overrides` fixes it. An `overrides` field in
|
package/docs/state.md
CHANGED
|
@@ -4,15 +4,15 @@ A seat's durable state is Postgres (runs, resumes, results) and its fast state i
|
|
|
4
4
|
counters, caches). `@homeflare/seat-runtime/state` builds each as the **Effect service that already
|
|
5
5
|
exists for it**, so consumer code is ordinary Effect SQL and Redis:
|
|
6
6
|
|
|
7
|
-
| store | service | from
|
|
8
|
-
| -------- | ----------------------------------------------------------------- |
|
|
9
|
-
| Postgres | `SqlClient` (and `PgClient`, re-exported as `SeatState.PgClient`) | `@effect/sql-pg`
|
|
10
|
-
| Valkey | `Redis` (`send`, `eval`; not `subscribe`) | `effect/
|
|
7
|
+
| store | service | from |
|
|
8
|
+
| -------- | ----------------------------------------------------------------- | ------------------------------------- |
|
|
9
|
+
| Postgres | `SqlClient` (and `PgClient`, re-exported as `SeatState.PgClient`) | `@effect/sql-pg` 4.0.1, its own layer |
|
|
10
|
+
| Valkey | `Redis` (`send`, `eval`; not `subscribe`) | `effect/persistence/Redis` |
|
|
11
11
|
|
|
12
12
|
```ts
|
|
13
13
|
import { Effect, Redacted } from 'effect';
|
|
14
|
-
import * as Redis from 'effect/
|
|
15
|
-
import * as SqlClient from 'effect/
|
|
14
|
+
import * as Redis from 'effect/persistence/Redis';
|
|
15
|
+
import * as SqlClient from 'effect/sql/SqlClient';
|
|
16
16
|
import { SeatState } from '@homeflare/seat-runtime/state';
|
|
17
17
|
|
|
18
18
|
const state = SeatState.layer({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homeflare/seat-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "One Effect AI model layer and one OTLP layer for HomeFlare's coding seats: LiteLLM tags, no-cache and no-retry per request, traces, logs and metrics to VictoriaMetrics on CT100.",
|
|
5
5
|
"homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/seat-runtime#readme",
|
|
6
6
|
"bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
"access": "public"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@effect/ai-openai-compat": "4.0.
|
|
39
|
-
"@effect/sql-pg": "4.0.
|
|
38
|
+
"@effect/ai-openai-compat": "4.0.1",
|
|
39
|
+
"@effect/sql-pg": "4.0.1",
|
|
40
40
|
"@modelcontextprotocol/sdk": "1.31.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"effect": "4.0.
|
|
43
|
+
"effect": "4.0.1"
|
|
44
44
|
}
|
|
45
45
|
}
|
package/src/mcp-tool.ts
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
import type * as JsonSchema from 'effect/JsonSchema';
|
|
43
43
|
import * as Schema from 'effect/Schema';
|
|
44
44
|
import * as SchemaGetter from 'effect/SchemaGetter';
|
|
45
|
-
import * as Tool from 'effect/
|
|
45
|
+
import * as Tool from 'effect/ai/Tool';
|
|
46
46
|
|
|
47
47
|
/** One dynamic tool per MCP tool: the server's JSON Schema in, text out, failures returned. */
|
|
48
48
|
export type McpTool = Tool.Dynamic<
|
package/src/mcp-toolkit.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
27
27
|
import * as Effect from 'effect/Effect';
|
|
28
28
|
import type * as Scope from 'effect/Scope';
|
|
29
|
-
import type * as Toolkit from 'effect/
|
|
29
|
+
import type * as Toolkit from 'effect/ai/Toolkit';
|
|
30
30
|
import {
|
|
31
31
|
DEFAULT_CONNECT_TIMEOUT_MS,
|
|
32
32
|
type McpHeaders,
|
package/src/mcp-toolset.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
20
20
|
import * as Effect from 'effect/Effect';
|
|
21
|
-
import * as Toolkit from 'effect/
|
|
21
|
+
import * as Toolkit from 'effect/ai/Toolkit';
|
|
22
22
|
import { type McpToolkitError, type Redact, describeCause } from './mcp-error.ts';
|
|
23
23
|
import { collect } from './mcp-pages.ts';
|
|
24
24
|
import { renderResult } from './mcp-render.ts';
|
package/src/rounds.ts
CHANGED
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
*/
|
|
47
47
|
import * as Effect from 'effect/Effect';
|
|
48
48
|
import * as Metric from 'effect/Metric';
|
|
49
|
-
import type * as Chat from 'effect/
|
|
50
|
-
import type * as LanguageModel from 'effect/
|
|
51
|
-
import * as Prompt from 'effect/
|
|
52
|
-
import type * as Tool from 'effect/
|
|
53
|
-
import type { AiError } from 'effect/
|
|
54
|
-
import type * as Toolkit from 'effect/
|
|
49
|
+
import type * as Chat from 'effect/ai/Chat';
|
|
50
|
+
import type * as LanguageModel from 'effect/ai/LanguageModel';
|
|
51
|
+
import * as Prompt from 'effect/ai/Prompt';
|
|
52
|
+
import type * as Tool from 'effect/ai/Tool';
|
|
53
|
+
import type { AiError } from 'effect/ai/AiError';
|
|
54
|
+
import type * as Toolkit from 'effect/ai/Toolkit';
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* A tool call the forced turn cannot use. `ToolNotFoundError` is compat's own rejection (its reply
|
package/src/seat-model.ts
CHANGED
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
import { OpenAiClient, OpenAiEmbeddingModel, OpenAiLanguageModel } from '@effect/ai-openai-compat';
|
|
12
12
|
import * as Layer from 'effect/Layer';
|
|
13
13
|
import * as Redacted from 'effect/Redacted';
|
|
14
|
-
import * as EmbeddingModel from 'effect/
|
|
15
|
-
import type * as LanguageModel from 'effect/
|
|
16
|
-
import * as FetchHttpClient from 'effect/
|
|
17
|
-
import * as HttpClient from 'effect/
|
|
14
|
+
import * as EmbeddingModel from 'effect/ai/EmbeddingModel';
|
|
15
|
+
import type * as LanguageModel from 'effect/ai/LanguageModel';
|
|
16
|
+
import * as FetchHttpClient from 'effect/http/FetchHttpClient';
|
|
17
|
+
import * as HttpClient from 'effect/http/HttpClient';
|
|
18
18
|
import { joinTags, stampRequest } from './stamp.ts';
|
|
19
19
|
|
|
20
20
|
export type SeatClientOptions = {
|
package/src/seat-obs.ts
CHANGED
|
@@ -15,14 +15,14 @@ import * as ConfigProvider from 'effect/ConfigProvider';
|
|
|
15
15
|
import * as Effect from 'effect/Effect';
|
|
16
16
|
import * as Layer from 'effect/Layer';
|
|
17
17
|
import * as Schema from 'effect/Schema';
|
|
18
|
-
import * as FetchHttpClient from 'effect/
|
|
18
|
+
import * as FetchHttpClient from 'effect/http/FetchHttpClient';
|
|
19
19
|
import {
|
|
20
20
|
type OtlpExporter,
|
|
21
21
|
OtlpLogger,
|
|
22
22
|
OtlpMetrics,
|
|
23
23
|
OtlpSerialization,
|
|
24
24
|
OtlpTracer,
|
|
25
|
-
} from 'effect/
|
|
25
|
+
} from 'effect/observability';
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* CT100's Victoria services, one path each. Verified 2026-09-29 by GET against the live
|
package/src/seat-state.ts
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
* ⛔ THE LIBRARY HOLDS NO HOST. Every URL comes from the consumer (a value, or an environment
|
|
6
6
|
* variable it names), as `Redacted`; there is no default address here to point a seat at the
|
|
7
7
|
* wrong store, and nothing reads a credential from disk.
|
|
8
|
-
* ★ THE SERVICES ARE EFFECT'S, NOT OURS: `SqlClient` (`effect/
|
|
9
|
-
* Postgres and `Redis` (`effect/
|
|
8
|
+
* ★ THE SERVICES ARE EFFECT'S, NOT OURS: `SqlClient` (`effect/sql/SqlClient`) for
|
|
9
|
+
* Postgres and `Redis` (`effect/persistence/Redis`) for Valkey, so a consumer writes
|
|
10
10
|
* ordinary Effect SQL and Redis code and this package adds only how they are built.
|
|
11
11
|
*/
|
|
12
12
|
import * as Layer from 'effect/Layer';
|
|
13
|
-
import type * as Redis from 'effect/
|
|
14
|
-
import type * as SqlClient from 'effect/
|
|
15
|
-
import type { SqlError } from 'effect/
|
|
13
|
+
import type * as Redis from 'effect/persistence/Redis';
|
|
14
|
+
import type * as SqlClient from 'effect/sql/SqlClient';
|
|
15
|
+
import type { SqlError } from 'effect/sql/SqlError';
|
|
16
16
|
import type { Config } from 'effect';
|
|
17
17
|
import type { PgClient } from '@effect/sql-pg';
|
|
18
18
|
import {
|
package/src/stamp.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* ★ IT IS THE SAME TRANSFORM AS cf-harness (landscape PR 165, cf-harness/src/request.ts),
|
|
10
10
|
* which is where the header and the body fields were measured against LiteLLM.
|
|
11
11
|
*/
|
|
12
|
-
import * as HttpClientRequest from 'effect/
|
|
12
|
+
import * as HttpClientRequest from 'effect/http/HttpClientRequest';
|
|
13
13
|
|
|
14
14
|
export type SeatStamp = {
|
|
15
15
|
/** Already validated and joined: LiteLLM reads one comma-separated header. */
|
package/src/state-postgres.ts
CHANGED
|
@@ -24,8 +24,8 @@ import * as Duration from 'effect/Duration';
|
|
|
24
24
|
import * as Effect from 'effect/Effect';
|
|
25
25
|
import * as Layer from 'effect/Layer';
|
|
26
26
|
import * as Redacted from 'effect/Redacted';
|
|
27
|
-
import type * as SqlClient from 'effect/
|
|
28
|
-
import { ConnectionError, SqlError } from 'effect/
|
|
27
|
+
import type * as SqlClient from 'effect/sql/SqlClient';
|
|
28
|
+
import { ConnectionError, SqlError } from 'effect/sql/SqlError';
|
|
29
29
|
import { postgresFields } from './state-dsn.ts';
|
|
30
30
|
|
|
31
31
|
export type PostgresOptions = {
|
package/src/state-valkey-send.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* wedge a seat forever, so every command carries `commandTimeout` too.
|
|
16
16
|
*/
|
|
17
17
|
import * as Effect from 'effect/Effect';
|
|
18
|
-
import * as Redis from 'effect/
|
|
18
|
+
import * as Redis from 'effect/persistence/Redis';
|
|
19
19
|
import type * as Duration from 'effect/Duration';
|
|
20
20
|
import { scrubbedError } from './state-valkey-scrub.ts';
|
|
21
21
|
|
package/src/state-valkey.ts
CHANGED
|
@@ -31,7 +31,7 @@ import * as Duration from 'effect/Duration';
|
|
|
31
31
|
import * as Effect from 'effect/Effect';
|
|
32
32
|
import * as Layer from 'effect/Layer';
|
|
33
33
|
import * as Redacted from 'effect/Redacted';
|
|
34
|
-
import * as Redis from 'effect/
|
|
34
|
+
import * as Redis from 'effect/persistence/Redis';
|
|
35
35
|
import { valkeyFields } from './state-dsn.ts';
|
|
36
36
|
import { keepConnected } from './state-valkey-connection.ts';
|
|
37
37
|
import { instrumentedSend } from './state-valkey-send.ts';
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// ⛔ GENERATED by scripts/sync-versions.ts — do not edit. Source: package.json.
|
|
2
|
-
export const VERSION: string = '0.1.
|
|
2
|
+
export const VERSION: string = '0.1.1';
|