From e65856653417da8e62fc3a7e3d0273943e5cd5b7 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 21 Aug 2026 11:55:02 -0700 Subject: [PATCH 1/3] Add tests for real tool-payload budget estimation and default Ollama adapter registration Reproduces the CL-6204 gaps: estimateTurnsChars undercounting tool_call/ tool_result payloads by orders of magnitude (the reviewer's 10-turn, 20,000-char tool_result repro), compaction output exceeding the budget it folded to once the summary turn's own size is counted, and SIDECAR_ADAPTER_MANIFEST defaulting to no adapters so a seeded Ollama model's num_ctx never reaches the request. --- .../compactors.test.ts | 75 +++++++++++++++++++ .../workbench-director.test.ts | 47 +++++++++++- apps/sidecar/test/adapter-registry.test.ts | 49 ++++++++++++ apps/sidecar/test/config.test.ts | 30 +++++++- 4 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 apps/sidecar/test/adapter-registry.test.ts diff --git a/apps/sidecar/src/workflow-substrate-factory/compactors.test.ts b/apps/sidecar/src/workflow-substrate-factory/compactors.test.ts index fb4a51e6..f85bb1c1 100644 --- a/apps/sidecar/src/workflow-substrate-factory/compactors.test.ts +++ b/apps/sidecar/src/workflow-substrate-factory/compactors.test.ts @@ -153,6 +153,81 @@ test("createBudgetedContextCompactor: a conversation past the budget is folded r expect(summary?.role).toBe("system"); }); +test("estimateTurnsChars measures a tool_result's real content size, not a placeholder", () => { + const largeResult = "x".repeat(20_000); + const turn: ConversationTurn = { + role: "user", + timestamp: 0, + content: [ + { + type: "tool_result", + callId: "call_1", + content: [{ type: "text", text: largeResult }], + }, + ], + }; + + // The old estimator measured `excerptBlock`'s placeholder + // (`[tool_result call_1]`, ~20 chars) instead of the real payload. + expect(estimateTurnsChars([turn])).toBeGreaterThanOrEqual(20_000); +}); + +test("estimateTurnsChars measures a tool_call's real argument size, not a placeholder", () => { + const largeArgs = { query: "y".repeat(15_000) }; + const turn: ConversationTurn = { + role: "assistant", + timestamp: 0, + content: [ + { type: "tool_call", id: "c1", name: "search", arguments: largeArgs }, + ], + }; + + expect(estimateTurnsChars([turn])).toBeGreaterThanOrEqual(15_000); +}); + +test("estimateTurnsChars: ten turns each carrying a 20,000-char tool_result sum to their real size, not 10 placeholders", () => { + const turns: ConversationTurn[] = Array.from({ length: 10 }, (_, i) => ({ + role: "user" as const, + timestamp: i, + content: [ + { + type: "tool_result" as const, + callId: `call_${String(i)}`, + content: [{ type: "text" as const, text: "z".repeat(20_000) }], + }, + ], + })); + + const chars = estimateTurnsChars(turns); + + // A hard limit sized for real conversations (e.g. 32,000 chars) must + // see this as over budget -- the old placeholder-based estimator + // returned ~160 chars for the same turns and let it through silently. + expect(chars).toBeGreaterThan(32_000); + expect(chars).toBeGreaterThanOrEqual(200_000); +}); + +test("createBudgetedContextCompactor: folded output never exceeds the budget it folded to", async () => { + // Realistic scale (comparable to `resolveContextBudgetChars` at a + // small `numCtx`, e.g. ~4900 chars for numCtx=2048): a budget bigger + // than `maxSummaryChars` (4000) but not by much, the exact regime + // where an uncounted summary previously pushed the total over. + const turns = Array.from({ length: 40 }, (_, i) => + textTurn( + i % 2 === 0 ? "user" : "assistant", + `message number ${i} `.repeat(25), + i, + ), + ); + const budgetChars = 6_000; + const compactor = createBudgetedContextCompactor(budgetChars); + + const result = await compactor.apply(turns, makeCtx()); + + expect(result.record.reason).toBe("folded-older-turns"); + expect(estimateTurnsChars(result.output)).toBeLessThanOrEqual(budgetChars); +}); + test("createBudgetedContextCompactor: always keeps a minimum verbatim tail even under a near-zero budget", async () => { const turns = Array.from({ length: 10 }, (_, i) => textTurn(i % 2 === 0 ? "user" : "assistant", `message ${i}`, i), diff --git a/apps/sidecar/src/workflow-substrate-factory/workbench-director.test.ts b/apps/sidecar/src/workflow-substrate-factory/workbench-director.test.ts index 5fd4142b..80104b17 100644 --- a/apps/sidecar/src/workflow-substrate-factory/workbench-director.test.ts +++ b/apps/sidecar/src/workflow-substrate-factory/workbench-director.test.ts @@ -20,7 +20,6 @@ import { WORKBENCH_DIRECTOR_ID, createWorkbenchDirector, createWorkbenchDirectorRegistry, - workbenchDirectorFactory, } from "./workbench-director"; const caps = createCapabilities(); @@ -86,6 +85,20 @@ function conversationTurn(text: string): ConversationTurn { return { role: "user", content: [{ type: "text", text }], timestamp: 0 }; } +function toolResultTurn(payload: string, callId: string): ConversationTurn { + return { + role: "user", + content: [ + { + type: "tool_result", + callId, + content: [{ type: "text", text: payload }], + }, + ], + timestamp: 0, + }; +} + function stateWithTurns(turns: ConversationTurn[]): ReactorState { return { ...state(), turns }; } @@ -235,6 +248,37 @@ test("context budget: a short conversation under budget is untouched (infers nor expect(typesOf(actions)).toEqual(["infer"]); }); +test("context budget: tool-heavy history past the hard limit is caught even though every turn's text excerpt is short", async () => { + // The reviewer's exact repro: 10 turns each carrying a 20,000-char + // tool_result. Measured by placeholder length this was ~160 chars + // total (invisible to a 32,000-char hard limit); measured by real + // payload size it is 200,000 chars, well past it. + const director = createWorkbenchDirector( + "you are a test agent", + [], + {}, + { + budgetChars: 16_000, + hardLimitChars: 32_000, + compactorName: "summarize-budgeted-turns", + }, + ); + const bigState = stateWithTurns( + Array.from({ length: 10 }, (_, i) => + toolResultTurn("x".repeat(20_000), `call_${String(i)}`), + ), + ); + + const actions = await director.decide( + { type: "message.received", message: { id: "m1", content: "hi" } as never }, + bigState, + caps, + ); + + expect(typesOf(actions)).toEqual(["checkpoint", "reply"]); + expect(replyOf(actions)).toBe(CONTEXT_OVERFLOW_MESSAGE); +}); + test("context budget: history past the hard limit replies with the honest overflow message instead of inferring", async () => { const director = createWorkbenchDirector( "you are a test agent", @@ -326,7 +370,6 @@ test("context budget: with no contextBudget configured, behavior is unchanged", }); test("the factory is namespaced and is the sidecar registry default", () => { - expect(workbenchDirectorFactory.id).toBe(WORKBENCH_DIRECTOR_ID); const registry = createWorkbenchDirectorRegistry(); expect(registry.defaultFactory().id).toBe(WORKBENCH_DIRECTOR_ID); expect( diff --git a/apps/sidecar/test/adapter-registry.test.ts b/apps/sidecar/test/adapter-registry.test.ts new file mode 100644 index 00000000..e4ab8f0c --- /dev/null +++ b/apps/sidecar/test/adapter-registry.test.ts @@ -0,0 +1,49 @@ +// Proves the default sidecar boot -- no `SIDECAR_ADAPTER_MANIFEST` set -- +// actually registers `@corbits/ollama-adapter` for the `ollama` provider +// key, and that a seeded Ollama model's `quirks.numCtx` reaches the built +// request as `options.num_ctx`. Before this fix, `SIDECAR_ADAPTER_MANIFEST` +// defaulted to `[]`, so a default deployment resolved the built-in OpenAI +// adapter for `ollama` sources and never sent `num_ctx` at all. +import { expect, test } from "bun:test"; +import { loadAdapterRegistry } from "@intx/inference/providers"; +import type { ConversationTurn, LastCycleSource } from "@intx/types/runtime"; + +import { readSidecarConfig } from "../src/config"; + +const VALID_ENV = { + SIDECAR_DATA_DIR: "/var/lib/sidecar", + HUB_WS_URL: "wss://hub.example.com/api/sidecars/ws", + SIDECAR_ID: "sidecar-1", + SIDECAR_TOKEN: "secret-token", + PATH: "/usr/local/bin:/usr/bin", +}; + +test("a default boot (no SIDECAR_ADAPTER_MANIFEST) registers the ollama provider", async () => { + const config = readSidecarConfig(VALID_ENV); + const registry = await loadAdapterRegistry(config.adapterManifest); + + expect(registry.has("ollama")).toBe(true); +}); + +test("a seeded Ollama model's quirks.numCtx reaches the built request as options.num_ctx, with no operator configuration", async () => { + const config = readSidecarConfig(VALID_ENV); + const registry = await loadAdapterRegistry(config.adapterManifest); + + const source: LastCycleSource = { + sourceId: "src_1", + provider: "ollama", + model: "gpt-oss:20b", + }; + // Shaped exactly like `@corbits/hub-client`'s seed writes onto a + // catalog offering's `quirks` column (`quirksForDeployment`). + const quirks = { default: { numCtx: 32_768 } }; + const adapter = registry.resolve(source, quirks); + + const messages: ConversationTurn[] = [ + { role: "user", content: [{ type: "text", text: "hi" }], timestamp: 0 }, + ]; + const built = adapter.buildRequest(messages, "gpt-oss:20b", {}); + const body = JSON.parse(built.body) as { options?: { num_ctx?: number } }; + + expect(body.options?.num_ctx).toBe(32_768); +}); diff --git a/apps/sidecar/test/config.test.ts b/apps/sidecar/test/config.test.ts index e6ca12be..c3851777 100644 --- a/apps/sidecar/test/config.test.ts +++ b/apps/sidecar/test/config.test.ts @@ -9,6 +9,14 @@ const VALID_ENV = { PATH: "/usr/local/bin:/usr/bin", }; +const DEFAULT_MANIFEST = [ + { + provider: "ollama", + specifier: "@corbits/ollama-adapter", + export: "createOllamaAdapter", + }, +]; + test("parses a complete environment into config", () => { const config = readSidecarConfig(VALID_ENV); expect(config).toEqual({ @@ -20,12 +28,32 @@ test("parses a complete environment into config", () => { home: undefined, tmpdir: undefined, toolRegistries: undefined, - adapterManifest: [], + adapterManifest: DEFAULT_MANIFEST, consumedRetentionMs: undefined, readyTimeoutMs: undefined, }); }); +test("an unset SIDECAR_ADAPTER_MANIFEST defaults to the shipped Ollama adapter, not an empty registry", () => { + const config = readSidecarConfig(VALID_ENV); + expect(config.adapterManifest).toEqual(DEFAULT_MANIFEST); +}); + +test("an operator-set adapter manifest fully replaces the default rather than merging with it", () => { + const manifest = [ + { + provider: "anthropic", + specifier: "@acme/custom-anthropic-adapter", + export: "createCustomAdapter", + }, + ]; + const config = readSidecarConfig({ + ...VALID_ENV, + SIDECAR_ADAPTER_MANIFEST: JSON.stringify(manifest), + }); + expect(config.adapterManifest).toEqual(manifest); +}); + test("carries a valid adapter manifest through as its parsed form", () => { const manifest = [ { From ebb53847623d3448312a1e5c812ff0aa28dabe47 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 21 Aug 2026 11:55:19 -0700 Subject: [PATCH 2/3] CL-6204: measure real tool payload size for context budget, register Ollama adapter by default The context-budget estimator (estimateTurnsChars) measured excerptBlock's human-readable placeholder ("[tool_result callId]", ~20 chars) instead of a turn's real payload size, so tool-heavy history silently undercounted by orders of magnitude, missed the hard limit, and let Ollama truncate server-side with no error. Compaction now also reserves the summary turn's own worst-case size out of the budget, so a fold no longer lands a summary on top of an already-full budget. SIDECAR_ADAPTER_MANIFEST defaulted to [], so @corbits/ollama-adapter (shipped in this repo) was only ever wired up by hand-editing an env var -- a default deployment ran the built-in OpenAI adapter against Ollama and never sent options.num_ctx. The manifest now defaults to registering the Ollama adapter for the "ollama" provider key; an operator-set manifest still fully replaces it rather than merging. The adapter package is added as a dependency of the sidecar app and hoisted to the workspace root (matching the other @corbits/* packages already hoisted there) -- without that, @intx/inference's dynamic import of the specifier from deep inside its own node_modules cannot resolve it at all. Also removes workbench-director.ts's dead duplicate director factory (defined/workbenchDirectorFactory/buildWorkbenchDirectorRef): it never received a contextBudget and had zero non-test call sites, a legacy path left beside the one createWorkbenchDirectorRegistry actually uses. --- apps/sidecar/package.json | 1 + apps/sidecar/src/config.ts | 51 +++++++--- .../workflow-substrate-factory/compactors.ts | 96 ++++++++++++++++++- .../workbench-director.ts | 19 ---- bun.lock | 20 +--- package.json | 1 + 6 files changed, 134 insertions(+), 54 deletions(-) diff --git a/apps/sidecar/package.json b/apps/sidecar/package.json index 25862cf4..a6d1b0de 100644 --- a/apps/sidecar/package.json +++ b/apps/sidecar/package.json @@ -17,6 +17,7 @@ "dependencies": { "@corbits/agent-lifecycle": "workspace:*", "@corbits/credential-providers": "workspace:*", + "@corbits/ollama-adapter": "workspace:*", "@corbits/workflow-host-actions": "workspace:*", "@intx/agent": "0.3.0", "@intx/authz": "0.3.0", diff --git a/apps/sidecar/src/config.ts b/apps/sidecar/src/config.ts index 0e05aea1..9ab3923d 100644 --- a/apps/sidecar/src/config.ts +++ b/apps/sidecar/src/config.ts @@ -11,6 +11,25 @@ import { AdapterManifest } from "@intx/inference"; import { parseToolRegistries } from "./tool-materialization"; +/** + * The shipped default manifest: registers `@corbits/ollama-adapter`'s + * `createOllamaAdapter` for the `"ollama"` provider key so a seeded + * Ollama deployment's `quirks.numCtx` (see `@corbits/hub-client`'s + * seed and `./workflow-substrate-factory/context-budget`) actually + * reaches Ollama's `options.num_ctx` instead of silently falling back + * to the built-in adapter's defaults. An operator who sets + * `SIDECAR_ADAPTER_MANIFEST` explicitly gets exactly what they wrote -- + * this default never merges with an operator value, only replaces the + * unset case. + */ +const DEFAULT_ADAPTER_MANIFEST: AdapterManifest = [ + { + provider: "ollama", + specifier: "@corbits/ollama-adapter", + export: "createOllamaAdapter", + }, +]; + const WsURL = type("string").narrow((url, ctx) => { if (!url.startsWith("ws://") && !url.startsWith("wss://")) { return ctx.mustBe("a ws:// or wss:// URL"); @@ -40,14 +59,15 @@ const SidecarEnv = type({ // workflow-process child's spawn env so per-step tool // materialization resolves the exact registries the operator pinned. "SIDECAR_TOOL_REGISTRIES?": "string", - // Optional JSON-encoded custom inference adapter manifest + // Optional JSON-encoded custom inference adapter manifest override // (`AdapterManifestEntry[]`, `[{"provider","specifier","export"}]`). - // Unset means no custom adapters -- `loadAdapterRegistry` resolves the - // built-ins only. Validated here so a malformed manifest kills the boot - // with the variable named, and threaded (as its parsed form) into both - // this process's own adapter registry and every workflow-process - // child's `SIDECAR_ADAPTER_MANIFEST` substrate-config entry, so a child - // resolves the exact custom adapters this boot edge resolved. + // Unset resolves to `DEFAULT_ADAPTER_MANIFEST` (the shipped Ollama + // adapter); set, it replaces that default entirely rather than merging + // with it. Validated here so a malformed manifest kills the boot with + // the variable named, and threaded (as its parsed form) into both this + // process's own adapter registry and every workflow-process child's + // `SIDECAR_ADAPTER_MANIFEST` substrate-config entry, so a child + // resolves the exact adapters this boot edge resolved. "SIDECAR_ADAPTER_MANIFEST?": "string", // Operator overrides for two workflow-supervisor timing bindings, // threaded verbatim to every deployment's supervisor @@ -95,9 +115,10 @@ export type SidecarConfig = { */ readonly toolRegistries: string | undefined; /** - * The operator's custom inference adapter manifest, already validated - * against {@link AdapterManifest}. Empty when the operator configured - * none -- `loadAdapterRegistry([])` then resolves the built-ins only. + * The inference adapter manifest, already validated against + * {@link AdapterManifest}: {@link DEFAULT_ADAPTER_MANIFEST} unless the + * operator set `SIDECAR_ADAPTER_MANIFEST`, in which case it is exactly + * (and only) what the operator wrote. */ readonly adapterManifest: AdapterManifest; /** @@ -117,14 +138,16 @@ export type SidecarConfig = { /** * Parse the optional `SIDECAR_ADAPTER_MANIFEST` env value into a validated - * {@link AdapterManifest}. Unset resolves to `[]` (no custom adapters); - * a malformed value dies at boot with the variable named, rather than - * surfacing as a deep-stack `loadAdapterRegistry` import failure. + * {@link AdapterManifest}. Unset resolves to {@link DEFAULT_ADAPTER_MANIFEST} + * (the shipped Ollama adapter, so `num_ctx` reaches Ollama without operator + * configuration); a malformed value dies at boot with the variable named, + * rather than surfacing as a deep-stack `loadAdapterRegistry` import + * failure. */ export function parseSidecarAdapterManifest( raw: string | undefined, ): AdapterManifest { - if (raw === undefined) return []; + if (raw === undefined) return DEFAULT_ADAPTER_MANIFEST; let parsed: unknown; try { parsed = JSON.parse(raw); diff --git a/apps/sidecar/src/workflow-substrate-factory/compactors.ts b/apps/sidecar/src/workflow-substrate-factory/compactors.ts index 75deb372..8192533a 100644 --- a/apps/sidecar/src/workflow-substrate-factory/compactors.ts +++ b/apps/sidecar/src/workflow-substrate-factory/compactors.ts @@ -56,11 +56,91 @@ const MIN_KEPT_TURNS = 4; function turnChars(turn: ConversationTurn): number { let total = 0; for (const block of turn.content) { - total += excerptBlock(block).length; + total += blockPayloadChars(block); } return total; } +/** + * A media block's real payload size: base64 data / a URL / a file + * reference, whichever the source carries. Shared by top-level media + * blocks and the media items nested in a `tool_result`'s `content`. + */ +function mediaSourceChars(source: { + kind: "base64" | "file-reference" | "url"; + data?: string; + url?: string; + reference?: string; +}): number { + switch (source.kind) { + case "base64": + return source.data?.length ?? 0; + case "url": + return source.url?.length ?? 0; + case "file-reference": + return source.reference?.length ?? 0; + } +} + +/** + * A `tool_result` content item's real size: text length, or the + * underlying media source's size for every other item kind. + */ +function toolResultItemChars( + item: Extract["content"][number], +): number { + return item.type === "text" + ? item.text.length + : mediaSourceChars(item.source); +} + +/** + * A block's true payload size -- what actually ships to the model -- + * as distinct from {@link excerptBlock}'s human-readable placeholder. + * `tool_call.arguments` and `tool_result.content` carry real request/ + * response payloads that can dwarf the rest of a turn; a budget + * estimator blind to them silently undercounts by orders of magnitude. + */ +function blockPayloadChars(block: ContentBlock): number { + switch (block.type) { + case "text": + return block.text.length; + case "refusal": + return block.reason.length; + case "thinking": + return block.thinking.length; + case "redacted_thinking": + return block.data.length; + case "citation": + return block.citedText.length; + case "safety_rating": + return block.blockReason.length; + case "code_execution_request": + return block.code.length; + case "code_execution_result": + return (block.stdout?.length ?? 0) + (block.stderr?.length ?? 0); + case "image": + case "audio": + case "video": + case "document": + return mediaSourceChars(block.source); + case "tool_call": + return block.name.length + JSON.stringify(block.arguments).length; + case "tool_result": { + let total = 0; + for (const item of block.content) { + total += toolResultItemChars(item); + } + if (block.detail !== undefined) { + total += JSON.stringify(block.detail).length; + } + return total; + } + default: + return 0; + } +} + /** Total character length of a turn list -- the budget check's estimate. */ export function estimateTurnsChars(turns: ConversationTurn[]): number { let total = 0; @@ -279,8 +359,11 @@ export function createBudgetedContextCompactor( turns: ConversationTurn[], _ctx: StrategyContext, ): Promise> { - const keep = countTurnsWithinBudget(turns, budgetChars); - if (keep >= turns.length) { + // First check against the full budget, exactly as when no fold is + // needed at all -- a conversation already under budget must stay + // untouched rather than being folded pre-emptively to make room + // for a summary turn nothing will produce. + if (countTurnsWithinBudget(turns, budgetChars) >= turns.length) { return { output: turns, record: { @@ -296,6 +379,13 @@ export function createBudgetedContextCompactor( }, }; } + + // Folding does happen: reserve the summary turn's own worst-case + // size out of the budget so kept-turns chars + summary chars + // together stay within `budgetChars`, instead of the summary + // landing on top of an already-full budget. + const keepBudgetChars = Math.max(0, budgetChars - maxSummaryChars); + const keep = countTurnsWithinBudget(turns, keepBudgetChars); return foldOlderTurns(turns, keep, { strategy: SUMMARIZE_BUDGETED_TURNS_NAME, version: SUMMARIZE_BUDGETED_TURNS_VERSION, diff --git a/apps/sidecar/src/workflow-substrate-factory/workbench-director.ts b/apps/sidecar/src/workflow-substrate-factory/workbench-director.ts index 358c86a1..e8362223 100644 --- a/apps/sidecar/src/workflow-substrate-factory/workbench-director.ts +++ b/apps/sidecar/src/workflow-substrate-factory/workbench-director.ts @@ -289,25 +289,6 @@ function buildWorkbenchFactory( }).factory; } -const defined = defineDirector({ - id: WORKBENCH_DIRECTOR_ID, - configSchema: WorkbenchDirectorConfigSchema, - factory: (config, _env, agent) => { - const policy: DefaultDirectorPolicy = {}; - if (config.mode !== undefined) { - policy.mode = config.mode; - } - return createWorkbenchDirector( - agent.systemPrompt, - [...agent.toolDefinitions], - policy, - ); - }, -}); - -export const workbenchDirectorFactory = defined.factory; -export const buildWorkbenchDirectorRef = defined.build; - /** * Sidecar step-env director registry: workbench is the default so * unspecified AgentDefinitions get empty-turn retry. The built-in diff --git a/bun.lock b/bun.lock index 118135a0..4d609019 100644 --- a/bun.lock +++ b/bun.lock @@ -11,6 +11,7 @@ "@corbits/artifacts": "github:corbitsdev/corbits-artifacts#81049ed24a64e927498c7238bda6ffa66b63d2ab", "@corbits/artifacts-hub": "workspace:*", "@corbits/evals": "workspace:*", + "@corbits/ollama-adapter": "workspace:*", "@corbits/workflow-catalog": "workspace:*", "@eslint/js": "^10.0.0", "@intx/db": "workspace:*", @@ -106,6 +107,7 @@ "dependencies": { "@corbits/agent-lifecycle": "workspace:*", "@corbits/credential-providers": "workspace:*", + "@corbits/ollama-adapter": "workspace:*", "@corbits/workflow-host-actions": "workspace:*", "@intx/agent": "0.3.0", "@intx/authz": "0.3.0", @@ -3487,20 +3489,6 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@corbits/artifacts-hub/@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#81049ed", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/types": "^0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.2", "hono": "^4.12.32", "hono-openapi": "^1.2.0", "postgres": "^3.4.9" } }, "corbitsdev-corbits-artifacts-81049ed", "sha512-oTE0iFDyQdz0ifG1epo39pwaCaYaw19YcKXwfaZqAEQ56a1g9YIozXwH9CG4NaUTwcJKUeYGuNls6oJsMPisCw=="], - - "@corbits/bench-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - - "@corbits/chat-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - - "@corbits/context-menu/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - - "@corbits/plugins-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - - "@corbits/settings-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - - "@corbits/tasks-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -3523,10 +3511,6 @@ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], - "@workbench/hub/@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#81049ed", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/types": "^0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.2", "hono": "^4.12.32", "hono-openapi": "^1.2.0", "postgres": "^3.4.9" } }, "corbitsdev-corbits-artifacts-81049ed", "sha512-oTE0iFDyQdz0ifG1epo39pwaCaYaw19YcKXwfaZqAEQ56a1g9YIozXwH9CG4NaUTwcJKUeYGuNls6oJsMPisCw=="], - - "@workbench/web/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - "ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "better-call/@better-auth/utils": ["@better-auth/utils@0.5.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA=="], diff --git a/package.json b/package.json index dc112ab7..65b7995d 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@corbits/artifacts": "github:corbitsdev/corbits-artifacts#81049ed24a64e927498c7238bda6ffa66b63d2ab", "@corbits/artifacts-hub": "workspace:*", "@corbits/evals": "workspace:*", + "@corbits/ollama-adapter": "workspace:*", "@corbits/workflow-catalog": "workspace:*", "@intx/db": "workspace:*", "@eslint/js": "^10.0.0", From 4853cc4da502ffc00a3258216050e2b0a0d4266b Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 21 Aug 2026 11:55:24 -0700 Subject: [PATCH 3/3] Update docs: SIDECAR_ADAPTER_MANIFEST now defaults to the shipped Ollama adapter Documents that the sidecar registers @corbits/ollama-adapter out of the box and that setting the variable replaces the default wholesale rather than merging with it. --- .env.example | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 8c462825..53b5ac54 100644 --- a/.env.example +++ b/.env.example @@ -315,13 +315,16 @@ ALLOW_UNVERIFIED_EMAILS=1 # HUB_SIDECAR_WEBSOCKET_URL= # SIDECAR_ADAPTER_MANIFEST configures custom Interchange inference adapters -# for a sidecar process, overriding built-in adapters that share a provider -# key (@intx/inference's loadAdapterRegistry). Leave unset (the default) to -# run the built-ins only. The value is a JSON array of +# for a sidecar process, replacing the default manifest wholesale (not +# merging with it). Leave unset (the default) and the sidecar already +# registers @corbits/ollama-adapter for the "ollama" provider key, so a +# seeded Ollama model's per-model num_ctx reaches Ollama with no operator +# configuration. Set this only to point a provider key at a different +# adapter package. The value is a JSON array of # {"provider","specifier","export"} entries; each specifier must resolve # from the sidecar's own module-resolution root (an installed package, not # a bare file path), and every workflow-process child it spawns resolves -# the same manifest. Example activating @corbits/ollama-adapter for the -# "ollama" provider key: -# SIDECAR_ADAPTER_MANIFEST=[{"provider":"ollama","specifier":"@corbits/ollama-adapter","export":"createOllamaAdapter"}] +# the same manifest. Example replacing the default with a custom adapter +# for the "anthropic" provider key: +# SIDECAR_ADAPTER_MANIFEST=[{"provider":"anthropic","specifier":"@acme/custom-anthropic-adapter","export":"createCustomAdapter"}]