@homeflare/seat-runtime 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +200 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +508 -0
- package/dist/index.js.map +21 -0
- package/dist/mcp-connect.d.ts +72 -0
- package/dist/mcp-connect.d.ts.map +1 -0
- package/dist/mcp-error.d.ts +77 -0
- package/dist/mcp-error.d.ts.map +1 -0
- package/dist/mcp-pages.d.ts +18 -0
- package/dist/mcp-pages.d.ts.map +1 -0
- package/dist/mcp-render.d.ts +25 -0
- package/dist/mcp-render.d.ts.map +1 -0
- package/dist/mcp-tool.d.ts +61 -0
- package/dist/mcp-tool.d.ts.map +1 -0
- package/dist/mcp-toolkit.d.ts +61 -0
- package/dist/mcp-toolkit.d.ts.map +1 -0
- package/dist/mcp-toolset.d.ts +33 -0
- package/dist/mcp-toolset.d.ts.map +1 -0
- package/dist/rounds.d.ts +108 -0
- package/dist/rounds.d.ts.map +1 -0
- package/dist/seat-model.d.ts +59 -0
- package/dist/seat-model.d.ts.map +1 -0
- package/dist/seat-obs.d.ts +23 -0
- package/dist/seat-obs.d.ts.map +1 -0
- package/dist/seat-state.d.ts +33 -0
- package/dist/seat-state.d.ts.map +1 -0
- package/dist/stamp.d.ts +23 -0
- package/dist/stamp.d.ts.map +1 -0
- package/dist/state-dsn.d.ts +41 -0
- package/dist/state-dsn.d.ts.map +1 -0
- package/dist/state-postgres.d.ts +58 -0
- package/dist/state-postgres.d.ts.map +1 -0
- package/dist/state-valkey-connection.d.ts +49 -0
- package/dist/state-valkey-connection.d.ts.map +1 -0
- package/dist/state-valkey-scrub.d.ts +37 -0
- package/dist/state-valkey-scrub.d.ts.map +1 -0
- package/dist/state-valkey-send.d.ts +35 -0
- package/dist/state-valkey-send.d.ts.map +1 -0
- package/dist/state-valkey.d.ts +83 -0
- package/dist/state-valkey.d.ts.map +1 -0
- package/dist/state.d.ts +9 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +327 -0
- package/dist/state.js.map +16 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/docs/mcp.md +40 -0
- package/docs/pairing.md +39 -0
- package/docs/state.md +174 -0
- package/package.json +45 -0
- package/src/index.ts +25 -0
- package/src/mcp-connect.ts +172 -0
- package/src/mcp-error.ts +150 -0
- package/src/mcp-pages.ts +37 -0
- package/src/mcp-render.ts +67 -0
- package/src/mcp-tool.ts +101 -0
- package/src/mcp-toolkit.ts +183 -0
- package/src/mcp-toolset.ts +91 -0
- package/src/rounds.ts +211 -0
- package/src/seat-model.ts +94 -0
- package/src/seat-obs.ts +103 -0
- package/src/seat-state.ts +53 -0
- package/src/stamp.ts +88 -0
- package/src/state-dsn.ts +112 -0
- package/src/state-postgres.ts +114 -0
- package/src/state-valkey-connection.ts +113 -0
- package/src/state-valkey-scrub.ts +60 -0
- package/src/state-valkey-send.ts +67 -0
- package/src/state-valkey.ts +193 -0
- package/src/state.ts +8 -0
- package/src/version.ts +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Timothy Schneider <tim@taslabs.net>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
10|furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
20|OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# @homeflare/seat-runtime
|
|
2
|
+
|
|
3
|
+
What every HomeFlare coding seat shares: an Effect AI **model** that talks to LiteLLM the
|
|
4
|
+
way the seats need, **telemetry** that lands traces, logs and metrics in the Victoria stack
|
|
5
|
+
on CT100 from one environment block, the **round loop** (`runRounds`, with a hard cap),
|
|
6
|
+
**MCP servers as a toolkit** (`mcpToolkit`), and **Postgres and Valkey state** on a subpath
|
|
7
|
+
(`SeatState`).
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bun add @homeflare/seat-runtime effect@4.0.0-rc.115
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
⛔ **Pin the rc, 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.0-rc.115`
|
|
15
|
+
(`@modelcontextprotocol/sdk` is an exact dependency too, `1.31.0`). If your app also
|
|
16
|
+
uses `@effect/platform-bun`, add this to your own manifest, or a fresh install crashes:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{ "overrides": { "effect": "4.0.0-rc.115", "@effect/platform-node-shared": "4.0.0-rc.115" } }
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Use it
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { Effect, Layer } from 'effect';
|
|
26
|
+
import { LanguageModel } from 'effect/unstable/ai';
|
|
27
|
+
import { SeatModel, SeatObs } from '@homeflare/seat-runtime';
|
|
28
|
+
|
|
29
|
+
const model = SeatModel.layer({
|
|
30
|
+
model: 'cf-code', // a LiteLLM alias
|
|
31
|
+
apiUrl: 'http://127.0.0.1:4100/v1', // required; there is no host default
|
|
32
|
+
apiKey: seatKey, // your per-seat LiteLLM virtual key; held Redacted
|
|
33
|
+
tags: ['host:ct100', 'lane:cfcode', 'seat:cf-coding'], // -> x-litellm-tags
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const program = LanguageModel.generateText({ prompt: 'ping' }).pipe(
|
|
37
|
+
Effect.withSpan('seat.run'),
|
|
38
|
+
Effect.provide(Layer.mergeAll(model, SeatObs.layer)),
|
|
39
|
+
);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### `SeatModel`
|
|
43
|
+
|
|
44
|
+
- `layer({ model, apiUrl, apiKey, tags, noCache?, metadata?, config? })` provides
|
|
45
|
+
`LanguageModel` on the **chat-completions** wire. ⛔ Not `/responses`: that wire fails on
|
|
46
|
+
cf-code with `missing field sequence_number` (landscape PR 165).
|
|
47
|
+
- `embeddingLayer({ model, dimensions, ... })` provides `EmbeddingModel` and
|
|
48
|
+
`EmbeddingModel.Dimensions`. ⚠️ `dimensions` is reported, **not sent**: compat's own
|
|
49
|
+
`OpenAiEmbeddingModel.model(name, { dimensions })` sends it, and a provider with no
|
|
50
|
+
such parameter can refuse the call. Pass `config: { dimensions }` to send it on purpose.
|
|
51
|
+
- `clientLayer(options)` is the shared `OpenAiClient` both build on, for a caller that
|
|
52
|
+
wants to compose its own model.
|
|
53
|
+
|
|
54
|
+
Every request, chat and embedding alike, is stamped by one `HttpClient` transform:
|
|
55
|
+
|
|
56
|
+
| what | where | why |
|
|
57
|
+
| ---------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
58
|
+
| `x-litellm-tags` | header | spend-log attribution. Validated: printable ASCII, no comma or space. |
|
|
59
|
+
| `cache` | body | `{"no-cache": true, "no-store": true}` unless `noCache: false`. The body field is what LiteLLM honours; `caching: false` and a header are no-ops. |
|
|
60
|
+
| `num_retries: 0` | body | a seat retries in Effect, where an attempt is a span. Always set. |
|
|
61
|
+
| `metadata` | body | only when you pass it and the body has none. compat drops `metadata` itself (measured, `tests/stamp.test.ts`). |
|
|
62
|
+
| `strict: false` | each tool | compat sends `strict: true` by default (measured 2026-09-29); the seats never did. A caller's `config` wins. |
|
|
63
|
+
|
|
64
|
+
⚠️ `noCache` defaults to **true**. LiteLLM caches every completion for every key, so a
|
|
65
|
+
repeated call returns the old answer at a tenth of the latency and reads as agreement.
|
|
66
|
+
|
|
67
|
+
### `SeatObs`
|
|
68
|
+
|
|
69
|
+
`SeatObs.layer` is `OtlpTracer`, `OtlpLogger` and `OtlpMetrics` `.layerFromConfig()` over
|
|
70
|
+
`fetch`, protobuf on the wire. With **no** environment it sends to CT100:
|
|
71
|
+
|
|
72
|
+
| signal | default endpoint |
|
|
73
|
+
| ------- | -------------------------------------------------------- |
|
|
74
|
+
| traces | `http://10.100.1.4:10428/insert/opentelemetry/v1/traces` |
|
|
75
|
+
| logs | `http://10.100.1.4:9428/insert/opentelemetry/v1/logs` |
|
|
76
|
+
| metrics | `http://10.100.1.4:8428/opentelemetry/v1/metrics` |
|
|
77
|
+
|
|
78
|
+
Exported as `SeatObs.CT100_ENDPOINTS`. **Verified 2026-09-29 by GET, no payload sent**: each
|
|
79
|
+
mounted path answers, an unmounted sibling answers `unsupported path requested`, and the
|
|
80
|
+
services' own counters carry `format="protobuf"` for traces and logs. End-to-end ingest from
|
|
81
|
+
this package is not measured against the live services; the tests use a stub.
|
|
82
|
+
|
|
83
|
+
⛔ **The defaults are what makes it emit at all.** `layerFromConfig` exports nothing, silently,
|
|
84
|
+
unless the environment names an exporter and an endpoint. The environment still wins: set
|
|
85
|
+
`OTEL_SERVICE_NAME` per seat, or a `service.name` in `OTEL_RESOURCE_ATTRIBUTES` (the default,
|
|
86
|
+
`SeatObs.DEFAULT_SERVICE_NAME`, is `seat-runtime`; Effect reads `OTEL_SERVICE_NAME` first, so
|
|
87
|
+
when both are set the variable wins), `OTEL_SDK_DISABLED=true` to
|
|
88
|
+
silence everything, `OTEL_TRACES_EXPORTER=none` for one signal, or a per-signal
|
|
89
|
+
`OTEL_EXPORTER_OTLP_<TRACES|LOGS|METRICS>_ENDPOINT`. A base `OTEL_EXPORTER_OTLP_ENDPOINT`
|
|
90
|
+
replaces all three defaults (Effect appends `/v1/<signal>`, which fits a collector, not
|
|
91
|
+
the Victoria paths). The Claude Code seats read the same names, so one block wires every seat.
|
|
92
|
+
|
|
93
|
+
Model calls made inside a span carry `traceparent` (asserted against the stub). ⚠️ Whether
|
|
94
|
+
LiteLLM continues that trace is a separate question: it had no trace callback on 2026-09-29 (scout), so the
|
|
95
|
+
trace stops at the gateway until one is added. Each signal flushes when the scope closes, and
|
|
96
|
+
a log POST exists only when something logged.
|
|
97
|
+
|
|
98
|
+
`VERSION` is the package's own version.
|
|
99
|
+
|
|
100
|
+
### `runRounds`
|
|
101
|
+
|
|
102
|
+
The one round loop. `Chat.generateText` resolves the tool calls of **one** model turn and
|
|
103
|
+
returns, and Effect AI has no `maxRounds`; this is the documented `while` with a cap.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const program = Effect.gen(function* () {
|
|
107
|
+
const toolkit = yield* MyToolkit; // a Toolkit with its handlers provided; or `mcp.toolkit`
|
|
108
|
+
const chat = yield* Chat.fromPrompt('Review this diff.');
|
|
109
|
+
const result = yield* runRounds({
|
|
110
|
+
chat,
|
|
111
|
+
toolkit,
|
|
112
|
+
maxRounds: 8,
|
|
113
|
+
onRound: (r) => Effect.log(r.round),
|
|
114
|
+
});
|
|
115
|
+
result.response.text; // the answer
|
|
116
|
+
result.capped; // true when the cap fired; result.rounds counts the forced turn too
|
|
117
|
+
result.unanswered; // true when the forced turn was refused (below)
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- Every round sends an empty prompt: `Chat` appends the model's turn and the tool results.
|
|
122
|
+
- It stops when a turn asks for no tools (`capped: false`), including on round `maxRounds`.
|
|
123
|
+
- After `maxRounds` turns that all asked for tools, **one more turn is forced with no toolkit and
|
|
124
|
+
`toolChoice: 'none'`** (`capped: true`, `rounds: maxRounds + 1`). compat then sends neither
|
|
125
|
+
`tools` nor `tool_choice`; the history still carries the earlier calls and results. Measured
|
|
126
|
+
once on 2026-09-29 through CT100's LiteLLM: cf-code accepted that history and answered with
|
|
127
|
+
`finishReason: 'stop'` and no tool call (one sample, one alias, not the tests' stub).
|
|
128
|
+
- 🔴 **The forced turn can be refused.** A provider that still asks for a tool although none was
|
|
129
|
+
offered answers a turn the SDK cannot use: `AiError` with reason `ToolNotFoundError` (what
|
|
130
|
+
`SeatModel`'s compat provider raises, measured) or `InvalidOutputError` raised by the SDK's own decode (module `LanguageModel`; the same reason raised by `OpenAiClient` for an empty, truncated or non-completion body is a gateway failure and fails the run).
|
|
131
|
+
Either does not fail the run: it returns `capped: true, unanswered: true`, `response` is the
|
|
132
|
+
last tool round's (no answer; its calls ran), `rounds` is `maxRounds`. Any other failure of that
|
|
133
|
+
turn (network, rate limit) still fails the run.
|
|
134
|
+
- `maxRounds` must be a positive integer; `0`, `Infinity` or `NaN` is a `RangeError` defect
|
|
135
|
+
before any model call. Errors are the turn's own (`AiError`, a handler's failure); nothing retries.
|
|
136
|
+
- Observable: a `seat.round` span per turn (`seat.round`, `seat.round.forced`, `seat.tool_calls`),
|
|
137
|
+
counters `seat_rounds_total`, `seat_rounds_capped_total` and `seat_rounds_unanswered_total`,
|
|
138
|
+
a warning log when the cap fires (and another when the forced turn is refused).
|
|
139
|
+
|
|
140
|
+
### `mcpToolkit`
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const program = Effect.gen(function* () {
|
|
144
|
+
const mcp = yield* mcpToolkit('https://mcp.example/mcp', {
|
|
145
|
+
authorization: Redacted.make(`Bearer ${token}`),
|
|
146
|
+
});
|
|
147
|
+
yield* runRounds({ chat, toolkit: mcp.toolkit, maxRounds: 8 });
|
|
148
|
+
const notes = yield* mcp.listResources;
|
|
149
|
+
const hello = yield* mcp.readResource('estate://notes/hello');
|
|
150
|
+
}).pipe(Effect.scoped); // the connection closes with the scope
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The official MCP SDK `Client` over Streamable HTTP; each MCP tool is a `Tool.dynamic` carrying
|
|
154
|
+
the server's own JSON Schema. It needs a `Scope`: the connection closes with it. The detail, with
|
|
155
|
+
what was measured, is in [docs/mcp.md](./docs/mcp.md); the rules to know first:
|
|
156
|
+
|
|
157
|
+
- **A tool failure goes back to the model**, not out of the run. Connecting and listing fail with
|
|
158
|
+
`McpToolkitError`, whose message and `cause` never hold the headers or the query string.
|
|
159
|
+
- **`connectTimeoutMs`** (default 15 s) bounds the handshake and each startup `tools/list` page.
|
|
160
|
+
- ⚠️ The tool list is a **snapshot**, and 🔴 rc.115 cannot decode a `Tool.dynamic`'s call alone: an
|
|
161
|
+
argument the server's schema does not declare is dropped (workaround in `src/mcp-tool.ts`).
|
|
162
|
+
|
|
163
|
+
### `SeatState` (`@homeflare/seat-runtime/state`)
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { SeatState } from '@homeflare/seat-runtime/state';
|
|
167
|
+
const state = SeatState.layer({ postgres: { url: pgDsn }, valkey: { url: valkeyUrl } });
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`SqlClient` and `Redis`, Effect's own services, on their own subpath: the root stays runtime-neutral and
|
|
171
|
+
this holds `node:net` and `Bun.RedisClient` (Valkey is Bun only). ⛔ **No host is held here**: the URLs
|
|
172
|
+
are yours, `Redacted`. A layer connects when built, so a wrong URL fails at startup as a typed error;
|
|
173
|
+
an out-of-prefix write is a typed `RedisError` (`SeatState.isPermissionDenied`); a lost Valkey is
|
|
174
|
+
reconnected by the layer. Span attributes never hold a key, value or password, and a Valkey error is
|
|
175
|
+
scrubbed of quoted arguments (Postgres's is not). **No `subscribe`.** Traps, tests: [docs/state.md](./docs/state.md).
|
|
176
|
+
|
|
177
|
+
## The measured pairing
|
|
178
|
+
|
|
179
|
+
Exact same-rc pins, the `overrides` trap, rc.118's dropped `unstable/` prefix and the MCP SDK's
|
|
180
|
+
pairing, each with what was measured: [docs/pairing.md](./docs/pairing.md). ⚠️ Compat's own `.d.ts`
|
|
181
|
+
has 26 `TS2411` errors under `skipLibCheck: false` (upstream's): keep it `true`.
|
|
182
|
+
|
|
183
|
+
## What is not here
|
|
184
|
+
|
|
185
|
+
No Valkey `subscribe` or migrations, no retry policy (the caller retries in Effect), no way to
|
|
186
|
+
merge an MCP toolkit with a local one (`Toolkit.merge` takes toolkits that still need handlers,
|
|
187
|
+
`mcp.toolkit` already has them), no prompt for the forced final turn (it is sent empty). Nothing
|
|
188
|
+
reads a credential from disk or logs one.
|
|
189
|
+
|
|
190
|
+
## Development
|
|
191
|
+
|
|
192
|
+
```sh
|
|
193
|
+
bun run --filter @homeflare/seat-runtime types # tsc --noEmit, scoped to this package
|
|
194
|
+
bun test packages/seat-runtime # the store suites skip without a store: docs/state.md
|
|
195
|
+
bun run --filter @homeflare/seat-runtime smoke # needs `build` first
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @homeflare/seat-runtime — what every HomeFlare coding seat shares: the model and telemetry
|
|
3
|
+
* layers, the round loop, and MCP servers as a toolkit.
|
|
4
|
+
*
|
|
5
|
+
* ⛔ RUNTIME-NEUTRAL: nothing here imports `bun:*` or `node:*`. It rides `fetch`, so it runs
|
|
6
|
+
* under Bun, Node and workerd alike. ⚠️ That includes the MCP SDK's client entry: bundled with
|
|
7
|
+
* workerd's resolution conditions, this entrypoint and the SDK's client hold no `node:`, `bun:`
|
|
8
|
+
* or bare builtin import (tests/sdk-neutral.test.ts), but its default JSON Schema validator is
|
|
9
|
+
* `ajv`, which needs `new Function`, so workerd itself is untested for `mcpToolkit`.
|
|
10
|
+
*/
|
|
11
|
+
export * as SeatModel from './seat-model.ts';
|
|
12
|
+
export * as SeatObs from './seat-obs.ts';
|
|
13
|
+
export { runRounds } from './rounds.ts';
|
|
14
|
+
export type { Round, RoundsOptions, RoundsResult } from './rounds.ts';
|
|
15
|
+
export { mcpToolkit } from './mcp-toolkit.ts';
|
|
16
|
+
export type { McpHeaders } from './mcp-connect.ts';
|
|
17
|
+
export type { McpResource, McpResourceContent, McpToolkit, McpToolkitOptions, McpTools, } from './mcp-toolkit.ts';
|
|
18
|
+
export { McpToolkitError } from './mcp-error.ts';
|
|
19
|
+
export { VERSION } from './version.ts';
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,SAAS,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,OAAO,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,YAAY,EACV,WAAW,EACX,kBAAkB,EAClB,UAAU,EACV,iBAAiB,EACjB,QAAQ,GACT,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
|