diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 409abd0..d5f4df2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -16,6 +16,11 @@ updates: prefix: "deps" include: "scope" groups: + # pi-agent-core depends on pi-ai; bumping them in separate PRs leaves two + # pi-ai copies in the workspace and breaks the StreamFn types. + pi: + patterns: + - "@earendil-works/*" typescript: patterns: - "typescript" diff --git a/docs/integrations.md b/docs/integrations.md index fa86bd9..3a9f2b5 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -286,6 +286,14 @@ id, endpoint, credential, headers, token limits, and relevant provider options still come from OpenCode. An unmapped transport fails during model binding instead of silently switching models or protocols. +When OpenCode supplies a custom `fetch` for authentication or routing, the bridge +passes it to the provider without changing the process-wide fetch function. +The Codex transport uses SSE in this mode; an explicit WebSocket request is +rejected. Custom fetch is not supported by the Google Gemini, Google Vertex, +or Amazon Bedrock transports, so those combinations fail before making a +provider request. Their ordinary host-resolved credential paths remain available +when no custom fetch is supplied. + For `@ai-sdk/anthropic`, configure OpenCode's `baseURL` through the `/v1` prefix. OpenCode appends `/messages`; the MemFlywheel bridge removes that terminal `/v1` before handing the same endpoint to pi-ai, whose Anthropic SDK appends diff --git a/e2e/agents.mjs b/e2e/agents.mjs index 3aed25b..22fb731 100644 --- a/e2e/agents.mjs +++ b/e2e/agents.mjs @@ -24,6 +24,32 @@ const HERMES_POD = "hermes-agent"; const HERMES_HOME = "/opt/data"; const PLUGIN_DIR = `${HERMES_HOME}/plugins/memflywheel`; +const HERMES_MEMORY_PROVIDER = ` +import json +import sys +from ruamel.yaml import YAML +yaml = YAML(typ='safe') +yaml.version = (1, 1) +with open(sys.argv[1]) as source: + config = yaml.load(source) or {} +print(json.dumps((config.get('memory') or {}).get('provider', ''))) +`; + +function hermesMemoryProvider() { + return JSON.parse( + kubectlExec( + HERMES_NS, + HERMES_POD, + "bash", + "-c", + 'exec "${HERMES_PYTHON:?HERMES_PYTHON is required}" -c "$1" "$2"', + "_", + HERMES_MEMORY_PROVIDER, + `${HERMES_HOME}/config.yaml`, + ), + ); +} + function hermesChat(prompt) { return kubectlExec( HERMES_NS, @@ -41,7 +67,11 @@ async function hermesWaitForSetup() { for (let i = 0; i < 30; i++) { try { const listing = kubectlExec(HERMES_NS, HERMES_POD, "ls", PLUGIN_DIR); - if (listing.includes("__init__.py") && listing.includes("worker.mjs")) { + if ( + listing.includes("__init__.py") && + listing.includes("worker.mjs") && + hermesMemoryProvider() === "memflywheel" + ) { installed = true; break; } @@ -50,7 +80,11 @@ async function hermesWaitForSetup() { } await delay(2000); } - check("memflywheel plugin installed", installed, installed ? "files present" : "timed out"); + check( + "memflywheel plugin installed", + installed, + installed ? "files and configuration ready" : "timed out", + ); if (!installed) process.exit(1); } @@ -72,10 +106,10 @@ function hermesVerifySetup() { check("plugin install.json present", false); } try { - const config = kubectlExec(HERMES_NS, HERMES_POD, "cat", `${HERMES_HOME}/config.yaml`); - check("memory.provider configured", /provider:\s*memflywheel/m.test(config), "memflywheel"); - } catch { - check("memory.provider configured", false, "config.yaml not found"); + const provider = hermesMemoryProvider(); + check("memory.provider configured", provider === "memflywheel", provider || "unset"); + } catch (e) { + check("memory.provider configured", false, e.message?.slice(0, 100)); } } diff --git a/e2e/kind/03-memflywheel-install.sh b/e2e/kind/03-memflywheel-install.sh index 633cc41..ff84b77 100755 --- a/e2e/kind/03-memflywheel-install.sh +++ b/e2e/kind/03-memflywheel-install.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/command/with-contenv bash # s6-overlay cont-init script: install memflywheel plugin into HERMES_HOME. # # Runs as root after 01-hermes-setup (which creates $HERMES_HOME and seeds @@ -29,7 +29,7 @@ if [ -f "$CONFIG" ] && ! grep -q "disabled_toolsets" "$CONFIG"; then fi # Run the install script (installed globally from @iflytekopensource/memflywheel) -memflywheel-hermes-install || { +/usr/local/bin/memflywheel-hermes-install || { echo "[memflywheel] ERROR: memflywheel-hermes-install failed" exit 1 } @@ -42,16 +42,21 @@ if [ -d "$PLUGIN_DIR" ]; then ln -sf /usr/local/lib/node_modules "${PLUGIN_DIR}/node_modules" 2>/dev/null || true fi -# Update the model and memory config in $HERMES_HOME/config.yaml. -# `hermes config set` writes to a different location (.hermes/config.yaml), -# so we modify the YAML directly. +# Use the host's Python/YAML runtime to update $HERMES_HOME/config.yaml. CONFIG="${HERMES_HOME}/config.yaml" if [ -f "$CONFIG" ]; then - python3 -c " -import yaml + "${HERMES_PYTHON:?HERMES_PYTHON is required}" -c " +from ruamel.yaml import YAML +yaml = YAML(typ='safe') +yaml.version = (1, 1) +yaml.default_flow_style = False with open('${CONFIG}') as f: - cfg = yaml.safe_load(f) or {} -cfg.setdefault('model', {}) + cfg = yaml.load(f) or {} +model = cfg.get('model') +if model is None or isinstance(model, str): + cfg['model'] = {} +elif not isinstance(model, dict): + raise TypeError('model must be a string or mapping') cfg['model']['default'] = 'mock-llm' cfg['model']['provider'] = 'custom' cfg['model']['base_url'] = 'http://mock-llm.default.svc.cluster.local:8080/v1' @@ -61,9 +66,12 @@ cfg['memory']['provider'] = 'memflywheel' cfg.setdefault('agent', {}) cfg['agent']['disabled_toolsets'] = ['memory'] with open('${CONFIG}', 'w') as f: - yaml.dump(cfg, f, default_flow_style=False) + yaml.dump(cfg, f) print('[memflywheel] Config updated: model=mock-llm, memory.provider=memflywheel, disabled_toolsets=[memory]') -" || echo "[memflywheel] WARNING: Failed to update config.yaml" +" +else + echo "[memflywheel] ERROR: config.yaml is missing" + exit 1 fi # Ensure the plugin directory is owned by hermes (we're running as root) diff --git a/e2e/kind/Dockerfile.hermes b/e2e/kind/Dockerfile.hermes index b18b8f4..4b3f76b 100644 --- a/e2e/kind/Dockerfile.hermes +++ b/e2e/kind/Dockerfile.hermes @@ -9,11 +9,16 @@ FROM nousresearch/hermes-agent:latest USER root +# Verify the configuration writer uses the Python environment supplied by Hermes. +RUN "${HERMES_PYTHON:?HERMES_PYTHON is required}" -c "from ruamel.yaml import YAML" + # Copy the unified memflywheel package COPY e2e/packages/iflytekopensource-memflywheel-*.tgz /tmp/memflywheel.tgz -# Install it globally so memflywheel-hermes-install is available -RUN npm install --global /tmp/memflywheel.tgz +# Keep the install prefix stable when the base image uses a managed Node binary. +RUN npm install --global --prefix /usr/local /tmp/memflywheel.tgz \ + && test -x /usr/local/bin/memflywheel-hermes-install \ + && /usr/local/bin/memflywheel-hermes-install --help # Make globally-installed packages resolvable from any working directory ENV NODE_PATH=/usr/local/lib/node_modules diff --git a/packages/memflywheel/bridge/worker.mjs b/packages/memflywheel/bridge/worker.mjs index d686c8a..47ffc00 100644 --- a/packages/memflywheel/bridge/worker.mjs +++ b/packages/memflywheel/bridge/worker.mjs @@ -15,8 +15,13 @@ import { dirname, join } from "node:path"; const memflywheel = await import( process.env.MEMFLYWHEEL_PACKAGE_IMPORT || "@iflytekopensource/memflywheel" ); -const { createAssistantMessageEventStream, createMemFlywheelHarnessRuntime, normalizeMessages } = - memflywheel; +const { + createAssistantMessageEventStream, + createMemFlywheelHarnessRuntime, + getCurrentSystemPrompt, + getCurrentTools, + normalizeMessages, +} = memflywheel; let runtime; let runtimeKey = ""; @@ -75,11 +80,19 @@ function resolveModel() { return { model: hermesModel, streamFn: async (_model, context, options) => { - const message = await complete({ context, signal: options?.signal?.aborted === true }); + const message = await complete({ + context: { + systemPrompt: getCurrentSystemPrompt(context.messages), + messages: context.messages.filter((entry) => entry.role !== "system"), + tools: getCurrentTools(context.messages), + }, + signal: options?.signal?.aborted === true, + }); const stream = createAssistantMessageEventStream(); if (message.stopReason === "error" || message.stopReason === "aborted") { stream.push({ type: "error", reason: message.stopReason, error: message }); } else { + stream.push({ type: "start", partial: message }); stream.push({ type: "done", reason: message.stopReason, message }); } return stream; diff --git a/packages/memflywheel/package.json b/packages/memflywheel/package.json index 43889c5..2129f93 100644 --- a/packages/memflywheel/package.json +++ b/packages/memflywheel/package.json @@ -84,8 +84,8 @@ "clean": "rm -rf dist tsconfig.tsbuildinfo" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.3", - "@earendil-works/pi-ai": "^0.84.4", + "@earendil-works/pi-agent-core": "^0.99.2", + "@earendil-works/pi-ai": "^0.99.2", "proper-lockfile": "^4.1.2" }, "devDependencies": { diff --git a/packages/memflywheel/src/hermes-worker.test.ts b/packages/memflywheel/src/hermes-worker.test.ts new file mode 100644 index 0000000..8df7a3f --- /dev/null +++ b/packages/memflywheel/src/hermes-worker.test.ts @@ -0,0 +1,108 @@ +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { once } from "node:events"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createInterface } from "node:readline"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; + +import type { AssistantMessage, JsonObject } from "@earendil-works/pi-ai"; + +test( + "Hermes worker preserves host instructions and tools across model RPC", + { timeout: 30_000 }, + async (t) => { + const root = await mkdtemp(join(tmpdir(), "memflywheel-hermes-rpc-")); + const worker = spawn( + process.execPath, + [fileURLToPath(new URL("../bridge/worker.mjs", import.meta.url))], + { + cwd: root, + env: { + ...process.env, + MEMFLYWHEEL_PACKAGE_IMPORT: new URL("./index.js", import.meta.url).href, + }, + stdio: "pipe", + }, + ); + const closed = once(worker, "close"); + const lines = createInterface({ input: worker.stdout }); + const responses = lines[Symbol.asyncIterator](); + let stderr = ""; + worker.stderr.setEncoding("utf8").on("data", (chunk) => { + stderr += chunk; + }); + t.after(async () => { + lines.close(); + worker.kill(); + await closed; + await rm(root, { recursive: true, force: true }); + }); + const send = (message: object) => worker.stdin.write(`${JSON.stringify(message)}\n`); + let modelCalls = 0; + async function command(name: string, payload: object) { + send({ type: "command", id: name, command: name, payload }); + for (;;) { + const line = await responses.next(); + assert.equal(line.done, false, stderr); + const message = JSON.parse(line.value); + if (message.type === "command_response") { + assert.equal(message.id, name); + assert.equal(message.error, undefined); + return; + } + assert.equal(message.type, "model_request"); + const context = message.request.context; + assert.match(context.systemPrompt, /memory extraction engine/); + assert.ok(context.tools.some((tool: { name: string }) => tool.name === "write")); + assert.ok(context.messages.every((entry: { role: string }) => entry.role !== "system")); + modelCalls += 1; + const content: AssistantMessage["content"] = []; + if (modelCalls === 1) { + const args: JsonObject = { + filePath: "preference/tea.md", + content: + "---\ntype: preference\nname: Tea\ndescription: Preferred drink\n---\nThe user prefers tea.\n", + }; + content.push({ type: "toolCall", id: "write-tea", name: "write", arguments: args }); + } else { + assert.ok( + context.messages.some((entry: { role: string }) => entry.role === "toolResult"), + ); + content.push({ type: "text", text: "done" }); + } + const result: AssistantMessage = { + role: "assistant", + content, + api: "openai-completions", + provider: "hermes", + model: "host-active", + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: modelCalls === 1 ? "toolUse" : "stop", + timestamp: Date.now(), + }; + send({ type: "model_response", id: message.id, result }); + } + } + const payload = { sessionId: "hermes-rpc", root, learnedSkills: false }; + await command("initialize", payload); + await command("turn_end", { + ...payload, + messages: [ + { role: "user", text: "Remember that I prefer tea." }, + { role: "assistant", text: "I will remember." }, + ], + }); + assert.equal(modelCalls, 2); + assert.match(await readFile(join(root, "preference", "tea.md"), "utf8"), /prefers tea/); + }, +); diff --git a/packages/memflywheel/src/host-memflywheel.test.ts b/packages/memflywheel/src/host-memflywheel.test.ts index 3a5ba4f..18d9493 100644 --- a/packages/memflywheel/src/host-memflywheel.test.ts +++ b/packages/memflywheel/src/host-memflywheel.test.ts @@ -3,6 +3,7 @@ import assert from "node:assert/strict"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { createServer } from "node:http"; import path from "node:path"; +import type { JsonObject } from "@earendil-works/pi-ai"; import { attachMemFlywheelToHostPort, @@ -164,7 +165,7 @@ function savingModel(): TestModelCompletion { }; } -function toolCall(id: string, name: string, args: unknown) { +function toolCall(id: string, name: string, args: JsonObject) { return { id, name, input: args }; } diff --git a/packages/memflywheel/src/index.ts b/packages/memflywheel/src/index.ts index 5226e66..bf567c5 100644 --- a/packages/memflywheel/src/index.ts +++ b/packages/memflywheel/src/index.ts @@ -53,10 +53,14 @@ export { export { type AdapterSpec, makeAdapter, readString, normalizeMessages } from "./make-adapter.js"; // Hermes installs its bridge worker outside the pnpm workspace. Re-export the -// pi-ai stream primitive through this already-resolved adapter entrypoint so +// pi-ai stream and transcript helpers through this already-resolved entrypoint so // the installed worker has one dependency boundary instead of reaching back // into workspace node_modules by package name. -export { createAssistantMessageEventStream } from "@earendil-works/pi-ai"; +export { + createAssistantMessageEventStream, + getCurrentSystemPrompt, + getCurrentTools, +} from "@earendil-works/pi-ai"; // Host harness port: stable host boundary + capability gates. export { diff --git a/packages/memflywheel/src/model-test-support.test.ts b/packages/memflywheel/src/model-test-support.test.ts index 6c1228e..51b7b77 100644 --- a/packages/memflywheel/src/model-test-support.test.ts +++ b/packages/memflywheel/src/model-test-support.test.ts @@ -1,10 +1,16 @@ -import { createAssistantMessageEventStream, type AssistantMessage } from "@earendil-works/pi-ai"; +import { + createAssistantMessageEventStream, + getCurrentSystemPrompt, + getCurrentTools, + type AssistantMessage, + type JsonObject, +} from "@earendil-works/pi-ai"; import type { ResolvePiAgentModel } from "@memflywheel/sdk"; export interface TestModelMessage { role: "system" | "user" | "assistant" | "tool"; content?: string | null; - toolCalls?: Array<{ id: string; name: string; input: unknown }>; + toolCalls?: Array<{ id: string; name: string; input: JsonObject }>; toolCallId?: string; } @@ -51,7 +57,7 @@ function assistant(response: TestModelResponse): AssistantMessage { type: "toolCall", id: call.id, name: call.name, - arguments: call.input as Record, + arguments: call.input, }); } return { @@ -72,7 +78,8 @@ export function resolveTestModel(completion: TestModelCompletion): ResolvePiAgen model, streamFn: async (_model, context, options) => { const messages: TestModelMessage[] = []; - if (context.systemPrompt) messages.push({ role: "system", content: context.systemPrompt }); + const systemPrompt = getCurrentSystemPrompt(context.messages); + if (systemPrompt) messages.push({ role: "system", content: systemPrompt }); for (const message of context.messages) { if (message.role === "user") { messages.push({ @@ -96,7 +103,7 @@ export function resolveTestModel(completion: TestModelCompletion): ResolvePiAgen : [], ), }); - } else { + } else if (message.role === "toolResult") { messages.push({ role: "tool", toolCallId: message.toolCallId, @@ -109,15 +116,16 @@ export function resolveTestModel(completion: TestModelCompletion): ResolvePiAgen const response = assistant( await completion.complete({ messages, - tools: (context.tools ?? []).map((tool) => ({ + tools: getCurrentTools(context.messages).map((tool) => ({ name: tool.name, description: tool.description, - inputSchema: tool.parameters as never, + inputSchema: tool.parameters, })), signal: options?.signal, }), ); const stream = createAssistantMessageEventStream(); + stream.push({ type: "start", partial: response }); stream.push({ type: "done", reason: response.stopReason === "toolUse" ? "toolUse" : "stop", diff --git a/packages/memflywheel/src/openclaw-native-model.ts b/packages/memflywheel/src/openclaw-native-model.ts index a0f5917..48ea5c9 100644 --- a/packages/memflywheel/src/openclaw-native-model.ts +++ b/packages/memflywheel/src/openclaw-native-model.ts @@ -1,7 +1,10 @@ import { createAssistantMessageEventStream, + getCurrentSystemPrompt, + getCurrentTools, type Api, type AssistantMessage, + type Context, type Model, } from "@earendil-works/pi-ai"; import type { ResolvePiAgentModel } from "@memflywheel/sdk"; @@ -24,7 +27,7 @@ export interface OpenClawNativeModelRuntime { readonly completePrepared: (input: { readonly model: unknown; readonly auth: unknown; - readonly context: unknown; + readonly context: Context; readonly cfg: unknown; readonly options: { readonly signal?: AbortSignal }; }) => Promise; @@ -69,7 +72,12 @@ export function createOpenClawHostModel( await runtime.completePrepared({ model: prepared.model, auth: prepared.auth, - context, + // The host completion API still accepts the pre-transcript context shape. + context: { + systemPrompt: getCurrentSystemPrompt(context.messages), + messages: context.messages.filter((entry) => entry.role !== "system"), + tools: getCurrentTools(context.messages), + }, cfg, options: { signal: options?.signal }, }), @@ -80,6 +88,7 @@ export function createOpenClawHostModel( } else if (message.stopReason === "error" || message.stopReason === "aborted") { stream.push({ type: "error", reason: message.stopReason, error: message }); } else { + stream.push({ type: "start", partial: message }); stream.push({ type: "done", reason: message.stopReason, message }); } return stream; diff --git a/packages/memflywheel/src/openclaw-port.test.ts b/packages/memflywheel/src/openclaw-port.test.ts index ccc2763..4887fb3 100644 --- a/packages/memflywheel/src/openclaw-port.test.ts +++ b/packages/memflywheel/src/openclaw-port.test.ts @@ -1,5 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; +import { normalizeContext, Type, type Context } from "@earendil-works/pi-ai"; import { hostMessagesFromOpenClawMessages, @@ -349,11 +350,11 @@ test("createOpenClawHostModel uses one host-native structured completion", async const response = await ( await binding.streamFn( binding.model, - { + normalizeContext({ systemPrompt: "Extract durable memory.", messages: [{ role: "user", content: "Remember tea.", timestamp: Date.now() }], tools: [], - }, + }), {}, ) ).result(); @@ -387,7 +388,7 @@ test("createOpenClawHostModel rejects non-terminal native completions", async () Promise.resolve( binding.streamFn( binding.model, - { systemPrompt: "Extract memory.", messages: [], tools: [] }, + normalizeContext({ systemPrompt: "Extract memory.", messages: [], tools: [] }), {}, ), ), @@ -401,3 +402,66 @@ test("createOpenClawHarnessPort fails without the host native model runtime", () /requires OpenClaw's native structured model runtime/, ); }); + +test("OpenClaw replays transcript instructions and tools for the host completion API", async () => { + const removedTool = { name: "old", description: "old tool", parameters: Type.Object({}) }; + const activeTool = { + name: "lookup", + description: "look up memory", + parameters: Type.Object({ query: Type.String() }), + }; + const context = normalizeContext({ + systemPrompt: "Base instructions.", + tools: [removedTool], + messages: [ + { role: "user", content: "Remember tea.", timestamp: 1 }, + { + role: "system", + content: "Updated instructions.", + toolsAdded: [activeTool], + toolsRemoved: [{ name: "old" }], + timestamp: 2, + }, + { + role: "toolResult", + toolCallId: "call-1", + toolName: "old", + content: [ + { type: "text", text: "Tea label" }, + { type: "image", data: "aW1hZ2U=", mimeType: "image/png" }, + ], + isError: false, + timestamp: 3, + }, + ], + }); + const originalMessages = structuredClone(context.messages); + const controller = new AbortController(); + let receivedContext: Context | undefined; + let receivedSignal: AbortSignal | undefined; + const binding = await createOpenClawHostModel( + { + currentConfig: () => ({}), + resolveDefaultAgentId: () => "main", + prepareForAgent: async () => ({ model: {}, auth: {} }), + completePrepared: async (input) => { + receivedContext = input.context; + receivedSignal = input.options.signal; + return { role: "assistant", content: [], stopReason: "stop" }; + }, + }, + () => ({}), + )(); + const stream = await binding.streamFn(binding.model, context, { signal: controller.signal }); + const events: string[] = []; + for await (const event of stream) events.push(event.type); + + assert.ok(receivedContext); + assert.match(receivedContext.systemPrompt ?? "", /Base instructions\./); + assert.match(receivedContext.systemPrompt ?? "", /Updated instructions\./); + assert.deepEqual(receivedContext.tools, [activeTool]); + assert.deepEqual(receivedContext.messages, [originalMessages[1], originalMessages[3]]); + assert.deepEqual(context.messages, originalMessages); + assert.strictEqual(receivedSignal, controller.signal); + assert.deepEqual(events, ["start", "done"]); +}); diff --git a/packages/memflywheel/src/opencode-pi-model.test.ts b/packages/memflywheel/src/opencode-pi-model.test.ts new file mode 100644 index 0000000..df6335b --- /dev/null +++ b/packages/memflywheel/src/opencode-pi-model.test.ts @@ -0,0 +1,117 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { normalizeContext, type Api } from "@earendil-works/pi-ai"; + +import { createPiAiModelBinding } from "./opencode-pi-model.js"; + +function binding(api: Api, fetch?: typeof globalThis.fetch) { + return createPiAiModelBinding({ + api, + provider: "test", + model: "test-model", + name: "test-model", + baseUrl: "https://example.invalid", + reasoning: false, + input: ["text"], + contextWindow: 16_384, + maxTokens: 4_096, + fetch, + }); +} + +test("unsupported host fetch transports fail before loading a provider", async () => { + let calls = 0; + const hostFetch: typeof globalThis.fetch = async () => { + calls += 1; + throw new Error("unexpected request"); + }; + for (const api of ["bedrock-converse-stream", "google-generative-ai", "google-vertex"]) { + const model = binding(api, hostFetch); + await assert.rejects( + async () => model.streamFn(model.model, normalizeContext({ messages: [] })), + new RegExp(`OpenCode host fetch is not supported by ${api}`), + ); + assert.equal(binding(api).model.api, api); + } + assert.equal(calls, 0); +}); + +test("Codex host fetch rejects explicit WebSocket transports", async () => { + let calls = 0; + const model = binding("openai-codex-responses", async () => { + calls += 1; + throw new Error("unexpected request"); + }); + for (const transport of ["websocket", "websocket-cached"] as const) { + await assert.rejects( + async () => model.streamFn(model.model, normalizeContext({ messages: [] }), { transport }), + /host fetch requires SSE/, + ); + } + assert.equal(calls, 0); +}); + +test("Codex host fetch uses SSE without attempting a default request or WebSocket", async () => { + const fetchDescriptor = Object.getOwnPropertyDescriptor(globalThis, "fetch"); + const websocketDescriptor = Object.getOwnPropertyDescriptor(globalThis, "WebSocket"); + let defaultRequests = 0; + let websocketRequests = 0; + let hostRequests = 0; + const apiKey = [ + Buffer.from("{}").toString("base64url"), + Buffer.from( + JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "test-account" } }), + ).toString("base64url"), + "test-signature", + ].join("."); + Object.defineProperty(globalThis, "fetch", { + configurable: true, + value: async () => { + defaultRequests += 1; + throw new Error("unexpected default request"); + }, + }); + Object.defineProperty(globalThis, "WebSocket", { + configurable: true, + value: class { + constructor() { + websocketRequests += 1; + throw new Error("unexpected WebSocket request"); + } + }, + }); + try { + const model = binding("openai-codex-responses", async () => { + hostRequests += 1; + return new Response( + `data: ${JSON.stringify({ + type: "response.completed", + response: { + id: "test-response", + status: "completed", + output: [], + usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, + }, + })}\n\n`, + { headers: { "content-type": "text/event-stream" } }, + ); + }); + for (const transport of [undefined, "auto", "sse"] as const) { + const stream = await model.streamFn( + model.model, + normalizeContext({ messages: [{ role: "user", content: "test", timestamp: 1 }] }), + { apiKey, transport, maxRetries: 0 }, + ); + assert.equal((await stream.result()).stopReason, "stop"); + } + assert.equal(hostRequests, 3); + assert.equal(defaultRequests, 0); + assert.equal(websocketRequests, 0); + } finally { + if (fetchDescriptor) Object.defineProperty(globalThis, "fetch", fetchDescriptor); + else Reflect.deleteProperty(globalThis, "fetch"); + if (websocketDescriptor) Object.defineProperty(globalThis, "WebSocket", websocketDescriptor); + else Reflect.deleteProperty(globalThis, "WebSocket"); + } +}); diff --git a/packages/memflywheel/src/opencode-pi-model.ts b/packages/memflywheel/src/opencode-pi-model.ts index 65a82a4..5f0bd1a 100644 --- a/packages/memflywheel/src/opencode-pi-model.ts +++ b/packages/memflywheel/src/opencode-pi-model.ts @@ -3,6 +3,7 @@ import type { Model, OpenAICompletionsCompat, ProviderStreams, + SimpleStreamOptions, ThinkingLevelMap, } from "@earendil-works/pi-ai"; import type { PiAgentModelBinding } from "@memflywheel/sdk"; @@ -32,19 +33,6 @@ export interface OpenCodePiModelConfig { const zeroCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }; -function withProviderFetch(fetchImpl: typeof globalThis.fetch, run: () => T): T { - // Provider SDKs capture the default fetch while streamSimple constructs their - // client. Restore it before any asynchronous network work can interleave. - const descriptor = Object.getOwnPropertyDescriptor(globalThis, "fetch"); - globalThis.fetch = fetchImpl; - try { - return run(); - } finally { - if (descriptor) Object.defineProperty(globalThis, "fetch", descriptor); - else delete (globalThis as { fetch?: typeof globalThis.fetch }).fetch; - } -} - async function loadApi(api: Api): Promise { switch (api) { case "openai-completions": @@ -98,9 +86,28 @@ export function createPiAiModelBinding(config: OpenCodePiModelConfig): PiAgentMo ...(config.requestMaxTokens === undefined ? {} : { maxTokens: config.requestMaxTokens }), }, streamFn: async (activeModel, context, options) => { + const streamOptions: SimpleStreamOptions = { + ...options, + ...(config.fetch ? { fetch: config.fetch } : {}), + }; + if (streamOptions.fetch) { + if ( + activeModel.api === "bedrock-converse-stream" || + activeModel.api === "google-generative-ai" || + activeModel.api === "google-vertex" + ) { + throw new Error(`OpenCode host fetch is not supported by ${activeModel.api}.`); + } + if (activeModel.api === "openai-codex-responses") { + const transport = streamOptions.transport; + if (transport !== undefined && transport !== "auto" && transport !== "sse") { + throw new Error("OpenCode host fetch requires SSE for the Codex transport."); + } + streamOptions.transport = "sse"; + } + } const api = await loadApi(activeModel.api); - const stream = () => api.streamSimple(activeModel, context, options); - return config.fetch ? withProviderFetch(config.fetch, stream) : stream(); + return api.streamSimple(activeModel, context, streamOptions); }, }; } diff --git a/packages/memflywheel/src/opencode-port.test.ts b/packages/memflywheel/src/opencode-port.test.ts index 811b463..6bb2dd0 100644 --- a/packages/memflywheel/src/opencode-port.test.ts +++ b/packages/memflywheel/src/opencode-port.test.ts @@ -1,5 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; +import { normalizeContext, Type } from "@earendil-works/pi-ai"; import { hostMessagesFromOpenCodeSessionMessages, @@ -267,10 +268,11 @@ test("OpenCode OAuth transport drives the registry pi-ai provider and omits host const stream = await binding.streamFn( binding.model, - { + normalizeContext({ systemPrompt: "test", messages: [{ role: "user", content: "test", timestamp: Date.now() }], - }, + tools: [{ name: "lookup", description: "look up memory", parameters: Type.Object({}) }], + }), { ...binding.request, apiKey: "oauth" }, ); assert.equal(globalThis.fetch, originalFetch); @@ -279,6 +281,8 @@ test("OpenCode OAuth transport drives the registry pi-ai provider and omits host assert.equal(result.stopReason, "stop"); assert.equal(requestPayload?.model, "gpt-5.5"); assert.equal(requestPayload?.max_output_tokens, undefined); + assert.ok(Array.isArray(requestPayload?.tools)); + assert.ok(requestPayload.tools.some((tool) => tool.name === "lookup")); assert.equal(globalThis.fetch, originalFetch); }); diff --git a/packages/memflywheel/src/pi-port.test.ts b/packages/memflywheel/src/pi-port.test.ts index 4a65eb2..49eab10 100644 --- a/packages/memflywheel/src/pi-port.test.ts +++ b/packages/memflywheel/src/pi-port.test.ts @@ -1,6 +1,11 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { createAssistantMessageEventStream, type AssistantMessage } from "@earendil-works/pi-ai"; +import { + createAssistantMessageEventStream, + normalizeContext, + Type, + type AssistantMessage, +} from "@earendil-works/pi-ai"; import { createPiHarnessPort, type PiExtensionHandler, type PiStreamSimple } from "./pi-port.js"; @@ -54,9 +59,12 @@ function assistant(): AssistantMessage { test("Pi port resolves the active model, auth, thinking level, and isolated session", async () => { const { api, handlers } = fakePi(); let capturedApiKey: string | undefined; - const streamSimple: PiStreamSimple = (_model, _context, options) => { + let capturedContext: Parameters[1] | undefined; + const streamSimple: PiStreamSimple = (_model, context, options) => { capturedApiKey = options?.apiKey; + capturedContext = context; const stream = createAssistantMessageEventStream(); + stream.push({ type: "start", partial: assistant() }); stream.push({ type: "done", reason: "stop", message: assistant() }); return stream; }; @@ -81,14 +89,16 @@ test("Pi port resolves the active model, auth, thinking level, and isolated sess assert.equal(binding.thinkingLevel, "high"); assert.equal(binding.transport, "sse"); assert.equal(binding.sessionId, "memflywheel:chat-1"); + const context = normalizeContext({ + systemPrompt: "Remember durable facts.", + tools: [{ name: "remember", description: "store facts", parameters: Type.Object({}) }], + messages: [{ role: "user", content: "Remember tea.", timestamp: 1 }], + }); const response = await ( - await binding.streamFn( - binding.model, - { messages: [] }, - { apiKey: await binding.getApiKey?.("x") }, - ) + await binding.streamFn(binding.model, context, { apiKey: await binding.getApiKey?.("x") }) ).result(); assert.equal(capturedApiKey, "host-oauth"); + assert.strictEqual(capturedContext, context); assert.equal(response.usage.totalTokens, 17); assert.equal(response.responseId, "resp_native"); }); diff --git a/packages/memflywheel/tests/provider_contract_test.py b/packages/memflywheel/tests/provider_contract_test.py index 7a891ae..aa89fab 100644 --- a/packages/memflywheel/tests/provider_contract_test.py +++ b/packages/memflywheel/tests/provider_contract_test.py @@ -204,6 +204,10 @@ def __init__(self, **kwargs): def call_llm(*, messages, tools, **kwargs): + assert {tool["function"]["name"] for tool in tools} >= {"read", "write"} + system_messages = [message for message in messages if message.get("role") == "system"] + assert len(system_messages) == 1 + assert system_messages[0]["content"] has_tool_result = any(message.get("role") == "tool" for message in messages) if has_tool_result: return Obj( diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 2a3f8f5..0387e2e 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -46,8 +46,8 @@ "clean": "rm -rf dist tsconfig.tsbuildinfo" }, "dependencies": { - "@earendil-works/pi-agent-core": "^0.84.3", - "@earendil-works/pi-ai": "^0.84.4", + "@earendil-works/pi-agent-core": "^0.99.2", + "@earendil-works/pi-ai": "^0.99.2", "@memflywheel/core": "workspace:*", "@memflywheel/embeddings": "workspace:*" }, diff --git a/packages/sdk/src/agent-runner.test.ts b/packages/sdk/src/agent-runner.test.ts index 5b7be80..8dfa0cc 100644 --- a/packages/sdk/src/agent-runner.test.ts +++ b/packages/sdk/src/agent-runner.test.ts @@ -1,10 +1,18 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { registerSessionResourceCleanup } from "@earendil-works/pi-ai"; +import { + getCurrentSystemPrompt, + getCurrentTools, + normalizeContext, + registerSessionResourceCleanup, + Type, + type JsonObject, + type TranscriptContext, +} from "@earendil-works/pi-ai"; import { runMemoryAgent } from "./agent-runner.js"; -import { scriptedBinding, stopTurn } from "./agent-test-support.test.js"; +import { scriptedBinding, stopTurn, toolTurn } from "./agent-test-support.test.js"; test("runMemoryAgent releases pi-ai resources for its isolated session", async () => { const scripted = scriptedBinding([stopTurn]); @@ -49,3 +57,74 @@ test("host request options override undefined Agent Core defaults", async () => assert.equal(receivedTemperature, 0.25); }); + +test("memory agent carries instructions, tool declarations and results in the transcript", async () => { + const args: JsonObject = { facts: ["tea", null, 2], enabled: true }; + const scripted = scriptedBinding([toolTurn("remember", args), stopTurn]); + const contexts: TranscriptContext[] = []; + let receivedArgs: unknown; + const result = await runMemoryAgent({ + resolveModel: async () => { + const binding = await scripted.resolveModel(); + return { + ...binding, + streamFn: (model, context, options) => { + contexts.push(normalizeContext({ messages: structuredClone(context.messages) })); + return binding.streamFn(model, context, options); + }, + }; + }, + tools: [ + { + name: "remember", + description: "store facts", + inputSchema: Type.Object({ facts: Type.Array(Type.Unknown()), enabled: Type.Boolean() }), + execute: async (input) => { + receivedArgs = input; + return { ok: true, text: "Stored tea.", changed: ["tea.md"] }; + }, + }, + ], + systemPrompt: "Remember durable facts.", + userMessage: "Remember tea.", + }); + + assert.equal(contexts.length, 2); + assert.equal(contexts[0]?.messages[0]?.role, "system"); + assert.equal(getCurrentSystemPrompt(contexts[0]?.messages ?? []), "Remember durable facts."); + assert.deepEqual( + getCurrentTools(contexts[0]?.messages ?? []).map((tool) => tool.name), + ["remember"], + ); + const toolResult = contexts[1]?.messages.find((message) => message.role === "toolResult"); + assert.deepEqual(toolResult?.content, [{ type: "text", text: "Stored tea." }]); + assert.deepEqual(receivedArgs, args); + assert.deepEqual(result.changed, ["tea.md"]); + assert.equal(result.finalContent, "done"); +}); + +test("memory agent releases isolated session resources after provider failure", async () => { + const scripted = scriptedBinding([]); + const cleaned: Array = []; + const unregister = registerSessionResourceCleanup((sessionId) => cleaned.push(sessionId)); + try { + await assert.rejects( + runMemoryAgent({ + resolveModel: async () => ({ + ...(await scripted.resolveModel()), + sessionId: "memflywheel:failed-session", + streamFn: () => { + throw new Error("provider failed"); + }, + }), + tools: [], + systemPrompt: "test", + userMessage: "test", + }), + /provider failed/, + ); + assert.deepEqual(cleaned, ["memflywheel:failed-session"]); + } finally { + unregister(); + } +}); diff --git a/packages/sdk/src/agent-test-support.test.ts b/packages/sdk/src/agent-test-support.test.ts index 91310d1..71fa267 100644 --- a/packages/sdk/src/agent-test-support.test.ts +++ b/packages/sdk/src/agent-test-support.test.ts @@ -1,7 +1,10 @@ import { createAssistantMessageEventStream, + getCurrentSystemPrompt, + getCurrentTools, type AssistantMessage, type Context, + type JsonObject, type Model, } from "@earendil-works/pi-ai"; @@ -32,7 +35,7 @@ const usage = { export interface ScriptedToolCall { id: string; name: string; - arguments: Record; + arguments: JsonObject; } export interface ScriptedTurn { @@ -40,7 +43,7 @@ export interface ScriptedTurn { toolCalls?: ScriptedToolCall[]; } -export function toolTurn(name: string, args: Record, id = "c1"): ScriptedTurn { +export function toolTurn(name: string, args: JsonObject, id = "c1"): ScriptedTurn { return { toolCalls: [{ id, name, arguments: args }] }; } @@ -66,9 +69,11 @@ export function scriptedBinding( streamFn: (_model, context, streamOptions) => { seenApiKeys.push(streamOptions?.apiKey); seen.push({ - systemPrompt: context.systemPrompt, - messages: structuredClone(context.messages), - tools: context.tools ? [...context.tools] : undefined, + systemPrompt: getCurrentSystemPrompt(context.messages), + messages: structuredClone( + context.messages.filter((message) => message.role !== "system"), + ), + tools: getCurrentTools(context.messages), }); const turn = turns[index++]; if (!turn) throw new Error("unexpected test model call"); @@ -88,6 +93,7 @@ export function scriptedBinding( timestamp: Date.now(), }; const stream = createAssistantMessageEventStream(); + stream.push({ type: "start", partial: message }); stream.push({ type: "done", reason: turn.toolCalls?.length ? "toolUse" : "stop", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58c09f8..31e977a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,11 +91,11 @@ importers: packages/memflywheel: dependencies: '@earendil-works/pi-agent-core': - specifier: ^0.84.3 - version: 0.84.3(ws@8.21.3) + specifier: ^0.99.2 + version: 0.99.2(@aws-sdk/credential-provider-node@3.972.82)(@smithy/signature-v4@5.7.3)(ws@8.21.3) '@earendil-works/pi-ai': - specifier: ^0.84.4 - version: 0.84.4(ws@8.21.3) + specifier: ^0.99.2 + version: 0.99.2(@aws-sdk/credential-provider-node@3.972.82)(@smithy/signature-v4@5.7.3)(ws@8.21.3) proper-lockfile: specifier: ^4.1.2 version: 4.1.2 @@ -113,11 +113,11 @@ importers: packages/sdk: dependencies: '@earendil-works/pi-agent-core': - specifier: ^0.84.3 - version: 0.84.3(ws@8.21.3) + specifier: ^0.99.2 + version: 0.99.2(@aws-sdk/credential-provider-node@3.972.82)(@smithy/signature-v4@5.7.3)(ws@8.21.3) '@earendil-works/pi-ai': - specifier: ^0.84.4 - version: 0.84.4(ws@8.21.3) + specifier: ^0.99.2 + version: 0.99.2(@aws-sdk/credential-provider-node@3.972.82)(@smithy/signature-v4@5.7.3)(ws@8.21.3) '@memflywheel/core': specifier: workspace:* version: link:../core @@ -147,8 +147,8 @@ importers: packages: - '@anthropic-ai/sdk@0.91.1': - resolution: {integrity: sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==} + '@anthropic-ai/sdk@0.124.0': + resolution: {integrity: sha512-cN5O8i9UVxHeOQAzj/XjshWXG8KiibJDw9OGpH2Z/eR3n/RBxdoLxDJOcfqAJWvjaMDFfHTBADU04hWRJVkDyA==} hasBin: true peerDependencies: zod: ^3.25.0 || ^4.0.0 @@ -156,21 +156,8 @@ packages: zod: optional: true - '@aws-crypto/sha256-browser@5.2.0': - resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} - - '@aws-crypto/sha256-js@5.2.0': - resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} - engines: {node: '>=16.0.0'} - - '@aws-crypto/supports-web-crypto@5.2.0': - resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} - - '@aws-crypto/util@5.2.0': - resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - - '@aws-sdk/client-bedrock-runtime@3.1048.0': - resolution: {integrity: sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==} + '@aws-sdk/client-bedrock-runtime@3.1127.0': + resolution: {integrity: sha512-IDl/lrPb90aH+pZFHGNDmgH9nAUQj5PlZH1sJ3w7RikctyjHSnY3oNjZhrLoaBoQn/rNK0zsP6OHEqEhj2tdLA==} engines: {node: '>=20.0.0'} '@aws-sdk/core@3.977.9': @@ -229,20 +216,16 @@ packages: resolution: {integrity: sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.1048.0': - resolution: {integrity: sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==} - engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.1116.0': resolution: {integrity: sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q==} engines: {node: '>=20.0.0'} - '@aws-sdk/types@3.974.5': - resolution: {integrity: sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==} + '@aws-sdk/token-providers@3.1127.0': + resolution: {integrity: sha512-Dv2TMWBshJ+tF6ahs2Sy5bh4Iabsd4GAQqVvE9XZmYmnoaVbpS2QKIKE/HRacc7bTtbjEEvP+laGzHvHlf1CiQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-locate-window@3.965.10': - resolution: {integrity: sha512-ycwH6Zd2GhuSqdXX9ihbCjeGTB6xOJs+O3+Jb8/zDG9978XU80qs75dfkPJRMNKe5MvBZPuNeFpd4JZKPoUF4g==} + '@aws-sdk/types@3.974.5': + resolution: {integrity: sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==} engines: {node: '>=20.0.0'} '@aws-sdk/xml-builder@3.972.40': @@ -263,21 +246,21 @@ packages: '@cacheable/utils@2.5.0': resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} - '@earendil-works/pi-agent-core@0.84.3': - resolution: {integrity: sha512-VURr+xBRl3RxYcw3kT9Pn3yfi6LbRoCJgHF7h1mAblMjtLNV/MfG/RyF0uJizBAM886AEakSiw3j9c/aSngppg==} + '@earendil-works/chord@0.99.2': + resolution: {integrity: sha512-2dJWiNsOJm/ioBl1fhpJmw0hIjFUhM0CAteEnVkiVG0Kg4LimR7Asq8PfjclEvrbCPsDntzy7rGKhTZObI9TZw==} engines: {node: '>=22.19.0'} - '@earendil-works/pi-ai@0.84.4': - resolution: {integrity: sha512-AClAZxf5+c4RRu44NJPS6wyQy+Nmq+Mzyyrdvm4ZVMNuixelO02RZX4G4Aq1F145Yzp43wnM5S+hLlSI7ypfVw==} + '@earendil-works/pi-agent-core@0.99.2': + resolution: {integrity: sha512-VX0QMcg8HKBsv1bYP2NwmutBYa44OrwmtqR6Dq7QC3Ln8X/IlpHhUxB5L1JgnXNUUkTm8Wg/HZhzS797NBDMCQ==} engines: {node: '>=22.19.0'} - hasBin: true - '@earendil-works/pi-telemetry@0.84.3': - resolution: {integrity: sha512-sgEkWoKrvSGaKn+YfLLFZmn+/A7B/w62eLwTD57nI+C9to8ITlFFVbgC2OtwvPnT3NFGHdCd53qhBEMIlptD1g==} + '@earendil-works/pi-ai@0.99.2': + resolution: {integrity: sha512-9RFOEdY+ZTJ1AI+UuAFs4RM0tF2Tje/R/CEv9gWTJHt0iTu8XHZs64U/EIZxNSllFmec/4HfwsOxYj1qQek9bg==} engines: {node: '>=22.19.0'} + hasBin: true - '@earendil-works/pi-telemetry@0.84.4': - resolution: {integrity: sha512-8e2CuxM+ht+hedQXTZmi5JVl6/xDK9RpSDL2+MbITevKYQhMZ/z6lJOTFgox3HQyGxO8mOZEtYGVeQNaD4OzqA==} + '@earendil-works/pi-telemetry@0.99.2': + resolution: {integrity: sha512-WThYU4XM6jjjzH4FzLdreN7QAPS9SDdokL0W0Ldheg1ssCIJkfpK7PgebFm7/PoQub7OiXx+SRF6B/oF7z7lXA==} engines: {node: '>=22.19.0'} '@esbuild/aix-ppc64@0.28.1': @@ -466,8 +449,8 @@ packages: resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@google/genai@1.52.0': - resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} + '@google/genai@2.21.0': + resolution: {integrity: sha512-+PDtco2/Z0ONdzCGekCoCT+O1VJS9xJQNN4XzQpXG/t3El/SWWMkCWlFRO1KmivOHPa4Q0VjUYu1HBKCZ/v33Q==} engines: {node: '>=20.0.0'} peerDependencies: '@modelcontextprotocol/sdk': ^1.25.2 @@ -698,18 +681,10 @@ packages: resolution: {integrity: sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==} engines: {node: '>=18.0.0'} - '@smithy/is-array-buffer@2.2.0': - resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} - engines: {node: '>=14.0.0'} - '@smithy/node-http-handler@4.12.1': resolution: {integrity: sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==} engines: {node: '>=18.0.0'} - '@smithy/node-http-handler@4.7.3': - resolution: {integrity: sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==} - engines: {node: '>=18.0.0'} - '@smithy/signature-v4@5.7.3': resolution: {integrity: sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==} engines: {node: '>=18.0.0'} @@ -718,13 +693,8 @@ packages: resolution: {integrity: sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A==} engines: {node: '>=18.0.0'} - '@smithy/util-buffer-from@2.2.0': - resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} - engines: {node: '>=14.0.0'} - - '@smithy/util-utf8@2.3.0': - resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} - engines: {node: '>=14.0.0'} + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} @@ -817,6 +787,10 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + agent-base@9.0.0: + resolution: {integrity: sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==} + engines: {node: '>= 20'} + ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} @@ -984,6 +958,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1042,6 +1019,10 @@ packages: resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} engines: {node: '>=14'} + google-logging-utils@1.2.0: + resolution: {integrity: sha512-WE9av4wKDZgRjBwgVUabocx8T6/7o3Ca1Fat46FXDhXVAFibzNadedcOXrdgd1Kzmk8tsk/9ZH89Wyf/SqeZ3A==} + engines: {node: '>=18'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -1055,20 +1036,24 @@ packages: hookified@2.2.0: resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} - http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} + http-proxy-agent@9.1.0: + resolution: {integrity: sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==} + engines: {node: '>= 20'} https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + https-proxy-agent@9.1.0: + resolution: {integrity: sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==} + engines: {node: '>= 20'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + ignore@7.0.8: + resolution: {integrity: sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==} engines: {node: '>= 4'} ignore@7.0.9: @@ -1170,12 +1155,25 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - openai@6.40.0: - resolution: {integrity: sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==} + openai@7.19.0: + resolution: {integrity: sha512-MX2s3u2L5racTO0CC/SWpCOasJQBCJrqLKXK+l82cAhdeF8mPMBEe/gxMm0ZFa2xpKpOFLRjxv5afYEZbBXmbQ==} + engines: {node: '>=22.0.0'} peerDependencies: - ws: ^8.18.0 + '@aws-sdk/credential-provider-node': '>=3.972.0 <4' + '@smithy/hash-node': '>=4.3.0 <5' + '@smithy/signature-v4': '>=5.4.0 <6' + undici: '>=5 <9' + ws: ^8.21.0 zod: ^3.25 || ^4.0 peerDependenciesMeta: + '@aws-sdk/credential-provider-node': + optional: true + '@smithy/hash-node': + optional: true + '@smithy/signature-v4': + optional: true + undici: + optional: true ws: optional: true zod: @@ -1263,6 +1261,15 @@ packages: resolution: {integrity: sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==} engines: {node: '>=12.0.0'} + proxy-agent-negotiate@1.1.0: + resolution: {integrity: sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==} + engines: {node: '>= 20'} + peerDependencies: + kerberos: ^2.0.0 + peerDependenciesMeta: + kerberos: + optional: true + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1315,6 +1322,9 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + standardwebhooks@1.1.1: + resolution: {integrity: sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ==} + sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} @@ -1380,12 +1390,12 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + typebox@1.3.27: + resolution: {integrity: sha512-zu+jc1pcy4UiNThxikUr36f0Rybk9PEeCg/NE6adeWr/SKsdNO4EzZHYRDlv2YCVAfj3Odq3dESSo/jNyoBXzA==} + typebox@1.3.30: resolution: {integrity: sha512-vRmBLzlaq9O9dvfGmI5CssLGvDC/R594kH6N/Q1uUU5VPO3PTgQMlWe/UVNdNVTr2EET+FX8BWZkFdYgxTglbQ==} - typebox@1.3.7: - resolution: {integrity: sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==} - typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -1436,46 +1446,19 @@ packages: snapshots: - '@anthropic-ai/sdk@0.91.1': + '@anthropic-ai/sdk@0.124.0': dependencies: json-schema-to-ts: 3.1.1 + standardwebhooks: 1.1.1 - '@aws-crypto/sha256-browser@5.2.0': - dependencies: - '@aws-crypto/sha256-js': 5.2.0 - '@aws-crypto/supports-web-crypto': 5.2.0 - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.974.5 - '@aws-sdk/util-locate-window': 3.965.10 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 - - '@aws-crypto/sha256-js@5.2.0': + '@aws-sdk/client-bedrock-runtime@3.1127.0': dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.974.5 - tslib: 2.8.1 - - '@aws-crypto/supports-web-crypto@5.2.0': - dependencies: - tslib: 2.8.1 - - '@aws-crypto/util@5.2.0': - dependencies: - '@aws-sdk/types': 3.974.5 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 - - '@aws-sdk/client-bedrock-runtime@3.1048.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 '@aws-sdk/core': 3.977.9 '@aws-sdk/credential-provider-node': 3.972.82 '@aws-sdk/eventstream-handler-node': 3.972.34 '@aws-sdk/middleware-eventstream': 3.972.29 '@aws-sdk/middleware-websocket': 3.972.52 - '@aws-sdk/token-providers': 3.1048.0 + '@aws-sdk/token-providers': 3.1127.0 '@aws-sdk/types': 3.974.5 '@smithy/core': 3.33.3 '@smithy/fetch-http-handler': 5.8.0 @@ -1620,7 +1603,7 @@ snapshots: '@smithy/types': 4.18.0 tslib: 2.8.1 - '@aws-sdk/token-providers@3.1048.0': + '@aws-sdk/token-providers@3.1116.0': dependencies: '@aws-sdk/core': 3.977.9 '@aws-sdk/nested-clients': 3.997.44 @@ -1629,7 +1612,7 @@ snapshots: '@smithy/types': 4.18.0 tslib: 2.8.1 - '@aws-sdk/token-providers@3.1116.0': + '@aws-sdk/token-providers@3.1127.0': dependencies: '@aws-sdk/core': 3.977.9 '@aws-sdk/nested-clients': 3.997.44 @@ -1643,10 +1626,6 @@ snapshots: '@smithy/types': 4.18.0 tslib: 2.8.1 - '@aws-sdk/util-locate-window@3.965.10': - dependencies: - tslib: 2.8.1 - '@aws-sdk/xml-builder@3.972.40': dependencies: '@smithy/types': 4.18.0 @@ -1668,45 +1647,58 @@ snapshots: hashery: 1.5.1 keyv: 5.6.0 - '@earendil-works/pi-agent-core@0.84.3(ws@8.21.3)': + '@earendil-works/chord@0.99.2': + dependencies: + esbuild: 0.28.1 + + '@earendil-works/pi-agent-core@0.99.2(@aws-sdk/credential-provider-node@3.972.82)(@smithy/signature-v4@5.7.3)(ws@8.21.3)': dependencies: - '@earendil-works/pi-ai': 0.84.4(ws@8.21.3) - '@earendil-works/pi-telemetry': 0.84.3 + '@earendil-works/chord': 0.99.2 + '@earendil-works/pi-ai': 0.99.2(@aws-sdk/credential-provider-node@3.972.82)(@smithy/signature-v4@5.7.3)(ws@8.21.3) + '@earendil-works/pi-telemetry': 0.99.2 diff: 8.0.4 - ignore: 7.0.5 - typebox: 1.3.7 + ignore: 7.0.8 + typebox: 1.3.27 yaml: 2.9.0 transitivePeerDependencies: + - '@aws-sdk/credential-provider-node' - '@modelcontextprotocol/sdk' + - '@smithy/hash-node' + - '@smithy/signature-v4' - bufferutil + - kerberos - supports-color + - undici - utf-8-validate - ws - zod - '@earendil-works/pi-ai@0.84.4(ws@8.21.3)': + '@earendil-works/pi-ai@0.99.2(@aws-sdk/credential-provider-node@3.972.82)(@smithy/signature-v4@5.7.3)(ws@8.21.3)': dependencies: - '@anthropic-ai/sdk': 0.91.1 - '@aws-sdk/client-bedrock-runtime': 3.1048.0 - '@earendil-works/pi-telemetry': 0.84.4 - '@google/genai': 1.52.0 - '@smithy/node-http-handler': 4.7.3 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - openai: 6.40.0(ws@8.21.3) + '@anthropic-ai/sdk': 0.124.0 + '@aws-sdk/client-bedrock-runtime': 3.1127.0 + '@earendil-works/pi-telemetry': 0.99.2 + '@google/genai': 2.21.0 + '@smithy/node-http-handler': 4.12.1 + http-proxy-agent: 9.1.0 + https-proxy-agent: 9.1.0 + openai: 7.19.0(@aws-sdk/credential-provider-node@3.972.82)(@smithy/signature-v4@5.7.3)(ws@8.21.3) partial-json: 0.1.7 - typebox: 1.3.7 + typebox: 1.3.27 transitivePeerDependencies: + - '@aws-sdk/credential-provider-node' - '@modelcontextprotocol/sdk' + - '@smithy/hash-node' + - '@smithy/signature-v4' - bufferutil + - kerberos - supports-color + - undici - utf-8-validate - ws - zod - '@earendil-works/pi-telemetry@0.84.3': {} - - '@earendil-works/pi-telemetry@0.84.4': {} + '@earendil-works/pi-telemetry@0.99.2': {} '@esbuild/aix-ppc64@0.28.1': optional: true @@ -1816,7 +1808,7 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 - '@google/genai@1.52.0': + '@google/genai@2.21.0': dependencies: google-auth-library: 10.9.1 p-retry: 4.6.2 @@ -1979,22 +1971,12 @@ snapshots: '@smithy/types': 4.18.0 tslib: 2.8.1 - '@smithy/is-array-buffer@2.2.0': - dependencies: - tslib: 2.8.1 - '@smithy/node-http-handler@4.12.1': dependencies: '@smithy/core': 3.33.3 '@smithy/types': 4.18.0 tslib: 2.8.1 - '@smithy/node-http-handler@4.7.3': - dependencies: - '@smithy/core': 3.33.3 - '@smithy/types': 4.18.0 - tslib: 2.8.1 - '@smithy/signature-v4@5.7.3': dependencies: '@smithy/core': 3.33.3 @@ -2005,15 +1987,7 @@ snapshots: dependencies: tslib: 2.8.1 - '@smithy/util-buffer-from@2.2.0': - dependencies: - '@smithy/is-array-buffer': 2.2.0 - tslib: 2.8.1 - - '@smithy/util-utf8@2.3.0': - dependencies: - '@smithy/util-buffer-from': 2.2.0 - tslib: 2.8.1 + '@stablelib/base64@1.0.1': {} '@types/esrecurse@4.3.1': {} @@ -2130,6 +2104,8 @@ snapshots: agent-base@7.1.4: {} + agent-base@9.0.0: {} + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 @@ -2316,6 +2292,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-sha256@1.3.0: {} + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -2366,7 +2344,7 @@ snapshots: gcp-metadata@8.1.2: dependencies: gaxios: 7.3.1 - google-logging-utils: 1.1.3 + google-logging-utils: 1.2.0 json-bigint: 1.0.0 transitivePeerDependencies: - supports-color @@ -2388,6 +2366,8 @@ snapshots: google-logging-utils@1.1.3: {} + google-logging-utils@1.2.0: {} + graceful-fs@4.2.11: {} hashery@1.5.1: @@ -2398,11 +2378,13 @@ snapshots: hookified@2.2.0: {} - http-proxy-agent@7.0.2: + http-proxy-agent@9.1.0: dependencies: - agent-base: 7.1.4 + agent-base: 9.0.0 debug: 4.4.3 + proxy-agent-negotiate: 1.1.0 transitivePeerDependencies: + - kerberos - supports-color https-proxy-agent@7.0.6: @@ -2412,9 +2394,18 @@ snapshots: transitivePeerDependencies: - supports-color + https-proxy-agent@9.1.0: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3 + proxy-agent-negotiate: 1.1.0 + transitivePeerDependencies: + - kerberos + - supports-color + ignore@5.3.2: {} - ignore@7.0.5: {} + ignore@7.0.8: {} ignore@7.0.9: {} @@ -2510,8 +2501,10 @@ snapshots: object-assign@4.1.1: {} - openai@6.40.0(ws@8.21.3): + openai@7.19.0(@aws-sdk/credential-provider-node@3.972.82)(@smithy/signature-v4@5.7.3)(ws@8.21.3): optionalDependencies: + '@aws-sdk/credential-provider-node': 3.972.82 + '@smithy/signature-v4': 5.7.3 ws: 8.21.3 optionator@0.9.4: @@ -2590,6 +2583,8 @@ snapshots: '@types/node': 22.20.2 long: 5.3.2 + proxy-agent-negotiate@1.1.0: {} + punycode@2.3.1: {} qified@0.10.1: @@ -2649,6 +2644,11 @@ snapshots: source-map@0.7.6: {} + standardwebhooks@1.1.1: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -2721,9 +2721,9 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typebox@1.3.30: {} + typebox@1.3.27: {} - typebox@1.3.7: {} + typebox@1.3.30: {} typescript@5.9.3: {}