From fb41f046d47d68941693a763011ad65b490cf511 Mon Sep 17 00:00:00 2001 From: FenjuFu <92919259+FenjuFu@users.noreply.github.com> Date: Fri, 18 Sep 2026 18:32:30 +0800 Subject: [PATCH 1/7] deps(deps): bump pi-ai and pi-agent-core to 0.85.1 together pi-agent-core 0.85.1 depends on pi-ai ^0.85.1. Dependabot opened the two bumps as separate PRs (#75, #76); each one alone leaves a second pi-ai copy in the workspace, so AssistantMessageEventStream and StreamFn resolve to different declarations and the build fails with TS2322. Bump both packages in memflywheel and sdk in one change, and group them in dependabot so future updates land together. Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com> --- .github/dependabot.yml | 6 +++ packages/memflywheel/package.json | 4 +- packages/sdk/package.json | 4 +- pnpm-lock.yaml | 88 ++++++++++++++++++++----------- 4 files changed, 68 insertions(+), 34 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 409abd0..9f808fb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -16,6 +16,12 @@ 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/pi-ai" + - "@earendil-works/pi-agent-core" typescript: patterns: - "typescript" diff --git a/packages/memflywheel/package.json b/packages/memflywheel/package.json index 43889c5..151c079 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.85.1", + "@earendil-works/pi-ai": "^0.85.1", "proper-lockfile": "^4.1.2" }, "devDependencies": { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 2a3f8f5..cab608f 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.85.1", + "@earendil-works/pi-ai": "^0.85.1", "@memflywheel/core": "workspace:*", "@memflywheel/embeddings": "workspace:*" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e632217..3255efb 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.85.1 + version: 0.85.1(ws@8.21.3) '@earendil-works/pi-ai': - specifier: ^0.84.4 - version: 0.84.4(ws@8.21.3) + specifier: ^0.85.1 + version: 0.85.1(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.85.1 + version: 0.85.1(ws@8.21.3) '@earendil-works/pi-ai': - specifier: ^0.84.4 - version: 0.84.4(ws@8.21.3) + specifier: ^0.85.1 + version: 0.85.1(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.123.0': + resolution: {integrity: sha512-Y9oX9mPNGZClHQOFqrWRk43Srcu/UHuPq3rfxxOq7JgW0gi+lJA2MAOK4Ul3k/+AUrwRWFJvd0tK3oC0Pw25dw==} hasBin: true peerDependencies: zod: ^3.25.0 || ^4.0.0 @@ -263,21 +263,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.85.1': + resolution: {integrity: sha512-VDlkEC3dhCzQ5fcyH1OhG19dq+6jCn+rqc/iXFivwDYGR5anwo2RCiXij9PpHhqNR5GuhhE+Er69Zi1Sn4eY6w==} 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.85.1': + resolution: {integrity: sha512-hIXIP3eAWueAYiAl8aMvWCvvZ8Q5gT3Dip5bE5uJyIGh4+YlWRjtMLI4BaeoXoSs93zndjue61u1B/vhefLnuA==} 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.85.1': + resolution: {integrity: sha512-+VgVIJDkDO2efYJKEEqvPTH4zmnIaXdAppGbO+vKFA9qy5PdhFiAenuFAkU+oiCSfOC4dMHDyrjdQeL4ZoC5CQ==} 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.85.1': + resolution: {integrity: sha512-Bg/YN6kA7Swja/NQxka8xFdecb4E/auIEGF2G5A25EaQXhRnPj300/7/KpgsDDMYUzHTDAv4RyUxaQPJKW81Rw==} engines: {node: '>=22.19.0'} '@esbuild/aix-ppc64@0.28.1': @@ -726,6 +726,9 @@ packages: 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==} @@ -984,6 +987,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 +1048,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==} @@ -1315,6 +1325,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'} @@ -1436,9 +1449,10 @@ packages: snapshots: - '@anthropic-ai/sdk@0.91.1': + '@anthropic-ai/sdk@0.123.0': dependencies: json-schema-to-ts: 3.1.1 + standardwebhooks: 1.1.1 '@aws-crypto/sha256-browser@5.2.0': dependencies: @@ -1668,10 +1682,15 @@ 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.85.1': dependencies: - '@earendil-works/pi-ai': 0.84.4(ws@8.21.3) - '@earendil-works/pi-telemetry': 0.84.3 + esbuild: 0.28.1 + + '@earendil-works/pi-agent-core@0.85.1(ws@8.21.3)': + dependencies: + '@earendil-works/chord': 0.85.1 + '@earendil-works/pi-ai': 0.85.1(ws@8.21.3) + '@earendil-works/pi-telemetry': 0.85.1 diff: 8.0.4 ignore: 7.0.5 typebox: 1.3.7 @@ -1684,11 +1703,11 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.84.4(ws@8.21.3)': + '@earendil-works/pi-ai@0.85.1(ws@8.21.3)': dependencies: - '@anthropic-ai/sdk': 0.91.1 + '@anthropic-ai/sdk': 0.123.0 '@aws-sdk/client-bedrock-runtime': 3.1048.0 - '@earendil-works/pi-telemetry': 0.84.4 + '@earendil-works/pi-telemetry': 0.85.1 '@google/genai': 1.52.0 '@smithy/node-http-handler': 4.7.3 http-proxy-agent: 7.0.2 @@ -1704,9 +1723,7 @@ snapshots: - ws - zod - '@earendil-works/pi-telemetry@0.84.3': {} - - '@earendil-works/pi-telemetry@0.84.4': {} + '@earendil-works/pi-telemetry@0.85.1': {} '@esbuild/aix-ppc64@0.28.1': optional: true @@ -2015,6 +2032,8 @@ snapshots: '@smithy/util-buffer-from': 2.2.0 tslib: 2.8.1 + '@stablelib/base64@1.0.1': {} + '@types/esrecurse@4.3.1': {} '@types/estree@1.0.9': {} @@ -2316,6 +2335,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 +2387,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 +2409,8 @@ snapshots: google-logging-utils@1.1.3: {} + google-logging-utils@1.2.0: {} + graceful-fs@4.2.11: {} hashery@1.5.1: @@ -2649,6 +2672,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 From 19422de2aafe9f45d7fb92be805a9ebe73eae2ae Mon Sep 17 00:00:00 2001 From: FenjuFu Date: Mon, 12 Oct 2026 13:25:15 +0800 Subject: [PATCH 2/7] fix(deps): migrate runtime adapters to 0.99.2 Signed-off-by: FenjuFu --- .github/dependabot.yml | 3 +- docs/integrations.md | 8 + packages/memflywheel/package.json | 4 +- .../memflywheel/src/host-memflywheel.test.ts | 3 +- .../src/model-test-support.test.ts | 22 +- .../memflywheel/src/openclaw-native-model.ts | 13 +- .../memflywheel/src/openclaw-port.test.ts | 70 ++++- .../memflywheel/src/opencode-pi-model.test.ts | 117 ++++++++ packages/memflywheel/src/opencode-pi-model.ts | 37 ++- .../memflywheel/src/opencode-port.test.ts | 8 +- packages/memflywheel/src/pi-port.test.ts | 24 +- packages/sdk/package.json | 4 +- packages/sdk/src/agent-runner.test.ts | 83 +++++- packages/sdk/src/agent-test-support.test.ts | 16 +- pnpm-lock.yaml | 276 ++++++++---------- 15 files changed, 486 insertions(+), 202 deletions(-) create mode 100644 packages/memflywheel/src/opencode-pi-model.test.ts diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9f808fb..d5f4df2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -20,8 +20,7 @@ updates: # pi-ai copies in the workspace and breaks the StreamFn types. pi: patterns: - - "@earendil-works/pi-ai" - - "@earendil-works/pi-agent-core" + - "@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/packages/memflywheel/package.json b/packages/memflywheel/package.json index 151c079..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.85.1", - "@earendil-works/pi-ai": "^0.85.1", + "@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/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/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/sdk/package.json b/packages/sdk/package.json index cab608f..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.85.1", - "@earendil-works/pi-ai": "^0.85.1", + "@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 84e441b..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.85.1 - version: 0.85.1(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.85.1 - version: 0.85.1(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.85.1 - version: 0.85.1(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.85.1 - version: 0.85.1(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.123.0': - resolution: {integrity: sha512-Y9oX9mPNGZClHQOFqrWRk43Srcu/UHuPq3rfxxOq7JgW0gi+lJA2MAOK4Ul3k/+AUrwRWFJvd0tK3oC0Pw25dw==} + '@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/chord@0.85.1': - resolution: {integrity: sha512-VDlkEC3dhCzQ5fcyH1OhG19dq+6jCn+rqc/iXFivwDYGR5anwo2RCiXij9PpHhqNR5GuhhE+Er69Zi1Sn4eY6w==} + '@earendil-works/chord@0.99.2': + resolution: {integrity: sha512-2dJWiNsOJm/ioBl1fhpJmw0hIjFUhM0CAteEnVkiVG0Kg4LimR7Asq8PfjclEvrbCPsDntzy7rGKhTZObI9TZw==} engines: {node: '>=22.19.0'} - '@earendil-works/pi-agent-core@0.85.1': - resolution: {integrity: sha512-hIXIP3eAWueAYiAl8aMvWCvvZ8Q5gT3Dip5bE5uJyIGh4+YlWRjtMLI4BaeoXoSs93zndjue61u1B/vhefLnuA==} + '@earendil-works/pi-agent-core@0.99.2': + resolution: {integrity: sha512-VX0QMcg8HKBsv1bYP2NwmutBYa44OrwmtqR6Dq7QC3Ln8X/IlpHhUxB5L1JgnXNUUkTm8Wg/HZhzS797NBDMCQ==} engines: {node: '>=22.19.0'} - '@earendil-works/pi-ai@0.85.1': - resolution: {integrity: sha512-+VgVIJDkDO2efYJKEEqvPTH4zmnIaXdAppGbO+vKFA9qy5PdhFiAenuFAkU+oiCSfOC4dMHDyrjdQeL4ZoC5CQ==} + '@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.85.1': - resolution: {integrity: sha512-Bg/YN6kA7Swja/NQxka8xFdecb4E/auIEGF2G5A25EaQXhRnPj300/7/KpgsDDMYUzHTDAv4RyUxaQPJKW81Rw==} + '@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,14 +693,6 @@ 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==} @@ -820,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==} @@ -1065,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: @@ -1180,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: @@ -1273,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'} @@ -1393,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'} @@ -1449,47 +1446,19 @@ packages: snapshots: - '@anthropic-ai/sdk@0.123.0': + '@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': - 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': + '@aws-sdk/client-bedrock-runtime@3.1127.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 @@ -1634,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 @@ -1643,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 @@ -1657,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 @@ -1682,48 +1647,58 @@ snapshots: hashery: 1.5.1 keyv: 5.6.0 - '@earendil-works/chord@0.85.1': + '@earendil-works/chord@0.99.2': dependencies: esbuild: 0.28.1 - '@earendil-works/pi-agent-core@0.85.1(ws@8.21.3)': + '@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/chord': 0.85.1 - '@earendil-works/pi-ai': 0.85.1(ws@8.21.3) - '@earendil-works/pi-telemetry': 0.85.1 + '@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.85.1(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.123.0 - '@aws-sdk/client-bedrock-runtime': 3.1048.0 - '@earendil-works/pi-telemetry': 0.85.1 - '@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.85.1': {} + '@earendil-works/pi-telemetry@0.99.2': {} '@esbuild/aix-ppc64@0.28.1': optional: true @@ -1833,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 @@ -1996,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 @@ -2022,16 +1987,6 @@ 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': {} @@ -2149,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 @@ -2421,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: @@ -2435,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: {} @@ -2533,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: @@ -2613,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: @@ -2749,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: {} From f3aefb76335885749e2198181caf835b43b5fda2 Mon Sep 17 00:00:00 2001 From: FenjuFu Date: Mon, 12 Oct 2026 13:39:30 +0800 Subject: [PATCH 3/7] fix(e2e): pin the Hermes package install prefix Signed-off-by: FenjuFu --- e2e/kind/03-memflywheel-install.sh | 2 +- e2e/kind/Dockerfile.hermes | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/e2e/kind/03-memflywheel-install.sh b/e2e/kind/03-memflywheel-install.sh index 633cc41..4ffd39b 100755 --- a/e2e/kind/03-memflywheel-install.sh +++ b/e2e/kind/03-memflywheel-install.sh @@ -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 } diff --git a/e2e/kind/Dockerfile.hermes b/e2e/kind/Dockerfile.hermes index b18b8f4..373bc99 100644 --- a/e2e/kind/Dockerfile.hermes +++ b/e2e/kind/Dockerfile.hermes @@ -12,8 +12,10 @@ USER root # 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 From 94dcf661493b58cc9f5726b02718bc2e3a77a10f Mon Sep 17 00:00:00 2001 From: FenjuFu Date: Mon, 12 Oct 2026 13:56:08 +0800 Subject: [PATCH 4/7] fix(hermes): replay transcripts across the host bridge Signed-off-by: FenjuFu --- packages/memflywheel/bridge/worker.mjs | 19 ++- .../memflywheel/src/hermes-worker.test.ts | 108 ++++++++++++++++++ packages/memflywheel/src/index.ts | 8 +- .../tests/provider_contract_test.py | 4 + 4 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 packages/memflywheel/src/hermes-worker.test.ts 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/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/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/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( From 1d2d726dde64d37778f1b94cf0a20b1f546cf7fc Mon Sep 17 00:00:00 2001 From: FenjuFu Date: Mon, 12 Oct 2026 14:02:35 +0800 Subject: [PATCH 5/7] fix(e2e): restore the Hermes init environment Signed-off-by: FenjuFu --- e2e/kind/03-memflywheel-install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/kind/03-memflywheel-install.sh b/e2e/kind/03-memflywheel-install.sh index 4ffd39b..7ff8b17 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 From 2954ef8be5443e3e884824a78a01e2c2de5c8ed7 Mon Sep 17 00:00:00 2001 From: FenjuFu Date: Mon, 12 Oct 2026 14:23:27 +0800 Subject: [PATCH 6/7] fix(e2e): use the Hermes YAML runtime for configuration Signed-off-by: FenjuFu --- e2e/kind/03-memflywheel-install.sh | 26 +++++++++++++++++--------- e2e/kind/Dockerfile.hermes | 3 +++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/e2e/kind/03-memflywheel-install.sh b/e2e/kind/03-memflywheel-install.sh index 7ff8b17..ff84b77 100755 --- a/e2e/kind/03-memflywheel-install.sh +++ b/e2e/kind/03-memflywheel-install.sh @@ -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 373bc99..4b3f76b 100644 --- a/e2e/kind/Dockerfile.hermes +++ b/e2e/kind/Dockerfile.hermes @@ -9,6 +9,9 @@ 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 From a045640f160cf2dca1bd3af3bc9b5d1e4c8c8016 Mon Sep 17 00:00:00 2001 From: FenjuFu Date: Mon, 12 Oct 2026 14:44:30 +0800 Subject: [PATCH 7/7] fix(e2e): wait for the Hermes memory provider configuration Signed-off-by: FenjuFu --- e2e/agents.mjs | 46 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) 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)); } }