From cb0fd2bcb08b818b1f98d0b803f3b2d6e454d616 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 17 Sep 2026 11:50:02 -0700 Subject: [PATCH] feat(memory)!: ride on @corbits/memory; drop memory-hub and memory-tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CL-8186: the memory plane was dark — nothing constructed @corbits/memory and memory-hub was never mounted. Cuts over fully: - Delete packages/memory-hub and packages/memory-tools entirely (they duplicated a workflow-run-authenticated HTTP surface @corbits/memory now serves directly on the stock /api/tenants/:tenantId/memory/* routes). - apps/hub/src/index.ts constructs @corbits/memory once via loadMemoryConfig() and mounts it next to @corbits/mailbox, guarded by the hub's own requireGrant. - Myra (workflows/assistant) and Scout (packages/scout-agent) and diligence-brief now pin @corbits/memory instead of @corbits/memory-tools. Since @corbits/memory is a published dependency rather than a workspace-built tool package, the tool-package-pins check now also resolves a pin against an installed (non-workspace:*) @corbits dependency's own package.json version. - apps/sidecar's step-env wires memoryBaseUrl/memoryTenantId/ memoryAuthToken/memoryFetch for @corbits/memory/tools, reusing the same sidecar-token + run-address bearer scheme every other workflow-run-authenticated tool bundle uses (memoryFetch injects the x-workflow-run-address header @corbits/memory's own HTTP client doesn't send). - packages/onboarding's tenant-seed SEED_GRANTS gains {resource: "memory", action: "add"} and {resource: "memory", action: "search"} (list reuses the search grant) for the tenant's shared workflow principal — @corbits/memory's own grant vocabulary (see its routes/deps.ts grantGuard). - REQUIRED_SEED_TOOL_PACKAGES / CORBITS_TOOL_PACKAGE_DIRS in tool-registry-publish now name @corbits/capability-tools instead of the deleted @corbits/memory-tools, since @corbits/memory is not a workspace package this repo packs into the corbits-tools registry. Deleted tests: packages/memory-hub/src/workflow-routes.test.ts and packages/memory-tools/src/{client,tool}.test.ts went with their packages. Every other touched test file was adapted (fixture rename or behavior-consistent update), not deleted. --- .changeset/config.json | 1 - apps/hub/package.json | 1 - apps/hub/src/index.ts | 23 ++ apps/hub/src/tool-grants.test.ts | 28 +- .../workflow-substrate-factory/compactors.ts | 6 +- .../step-env.test.ts | 13 +- .../workflow-substrate-factory/step-env.ts | 45 ++- apps/sidecar/test/deploy-router.test.ts | 2 +- .../src/capability-inventory.ts | 2 +- .../test/workflow-create-routes.test.ts | 14 +- packages/evals/package.json | 1 - packages/evals/src/scorers/tool-names.test.ts | 5 +- .../evals/src/scorers/world-scorers.test.ts | 7 +- packages/memory-hub/LICENSE | 176 --------- packages/memory-hub/README.md | 48 --- packages/memory-hub/package.json | 26 -- packages/memory-hub/src/index.ts | 9 - .../memory-hub/src/workflow-routes.test.ts | 342 ------------------ packages/memory-hub/src/workflow-routes.ts | 333 ----------------- packages/memory-hub/tsconfig.json | 23 -- packages/memory-hub/tsconfig.src.json | 21 -- packages/memory-tools/LICENSE | 176 --------- packages/memory-tools/README.md | 88 ----- packages/memory-tools/package.json | 38 -- packages/memory-tools/src/client.test.ts | 199 ---------- packages/memory-tools/src/client.ts | 183 ---------- packages/memory-tools/src/index.ts | 19 - packages/memory-tools/src/tool.test.ts | 269 -------------- packages/memory-tools/src/tool.ts | 244 ------------- packages/memory-tools/tsconfig.json | 15 - packages/memory-tools/tsconfig.src.json | 12 - packages/onboarding/src/tenant-seed.ts | 9 + .../test/connect-deploys-nothing.test.ts | 2 +- .../test/desired-state-reconcile.test.ts | 12 +- .../onboarding/test/desired-state.test.ts | 2 +- packages/onboarding/test/routes.test.ts | 10 +- packages/scout-agent/package.json | 3 +- packages/scout-agent/src/definition.ts | 18 +- .../tool-registry-publish/src/publish.test.ts | 10 +- packages/tool-registry-publish/src/publish.ts | 2 +- .../tool-registry-publish/src/registry.ts | 15 +- .../src/surface-reader.test.ts | 12 +- scripts/checks/report-error-baseline.txt | 5 - scripts/checks/tool-package-pins.ts | 69 ++++ tsconfig.build.json | 6 - workflows/assistant/package.json | 3 +- workflows/assistant/src/index.ts | 15 +- workflows/assistant/src/prompt.ts | 19 +- workflows/diligence-brief/package.json | 1 + workflows/diligence-brief/src/index.ts | 14 +- 50 files changed, 251 insertions(+), 2345 deletions(-) delete mode 100644 packages/memory-hub/LICENSE delete mode 100644 packages/memory-hub/README.md delete mode 100644 packages/memory-hub/package.json delete mode 100644 packages/memory-hub/src/index.ts delete mode 100644 packages/memory-hub/src/workflow-routes.test.ts delete mode 100644 packages/memory-hub/src/workflow-routes.ts delete mode 100644 packages/memory-hub/tsconfig.json delete mode 100644 packages/memory-hub/tsconfig.src.json delete mode 100644 packages/memory-tools/LICENSE delete mode 100644 packages/memory-tools/README.md delete mode 100644 packages/memory-tools/package.json delete mode 100644 packages/memory-tools/src/client.test.ts delete mode 100644 packages/memory-tools/src/client.ts delete mode 100644 packages/memory-tools/src/index.ts delete mode 100644 packages/memory-tools/src/tool.test.ts delete mode 100644 packages/memory-tools/src/tool.ts delete mode 100644 packages/memory-tools/tsconfig.json delete mode 100644 packages/memory-tools/tsconfig.src.json diff --git a/.changeset/config.json b/.changeset/config.json index 882c1d795a..2ae409369d 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -36,7 +36,6 @@ "@corbits/inference-settings", "@corbits/jimmy-agent", "@corbits/longevity-sim", - "@corbits/memory-hub", "@corbits/migration-runner", "@corbits/mocks", "@corbits/notify", diff --git a/apps/hub/package.json b/apps/hub/package.json index f77ca40f3c..71104b09f8 100644 --- a/apps/hub/package.json +++ b/apps/hub/package.json @@ -40,7 +40,6 @@ "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#bc964040fa42ffe4e5d5004f87edaab6486e5d60", "@corbits/mcp-tools": "workspace:*", "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", - "@corbits/memory-hub": "workspace:*", "@corbits/morning-brief-workflow": "workspace:*", "@corbits/notify": "workspace:*", "@corbits/pain-point-collateral-workflow": "workspace:*", diff --git a/apps/hub/src/index.ts b/apps/hub/src/index.ts index 6d19f801c5..65d9c7a687 100644 --- a/apps/hub/src/index.ts +++ b/apps/hub/src/index.ts @@ -101,6 +101,7 @@ import { createMailboxPersist, mountMailbox, } from "@corbits/mailbox"; +import { createMemory, loadMemoryConfig } from "@corbits/memory"; import { createHubMailboxAuthorizeSender, hubMailboxResolveRefs, @@ -1576,6 +1577,28 @@ export async function createHub(config: HubConfig) { app.route(`${TENANT_PREFIX}/mailbox`, mailboxApp); } + { + // Firm memory (CL-8186): `@corbits/memory` registers its own + // absolute `/api/tenants/:tenantId/memory/*` routes on whatever + // Hono app it's given (see its `routes/add.ts`, etc.) rather than + // composing under a mount prefix, so it gets its own `Hono` + // — matching `mailboxApp` above — routed at "/" rather than + // `TENANT_PREFIX`, which would double the tenant-route prefix. + // Guarded by the hub's own `requireGrant`: the same + // `chatGrantStore`/`grantConditionRegistry` every other tenant-scoped + // mount above uses. `loadMemoryConfig` reads + // `DATABASE_URL`/`EMBED_BASE_URL`/`EMBED_MODEL` (and friends) from + // env itself; nothing else in this file constructs the memory plane. + const memoryApp = new Hono(); + createMemory({ + app: memoryApp, + config: loadMemoryConfig(), + grantStore: chatGrantStore, + conditionRegistry: grantConditionRegistry, + }); + app.route("/", memoryApp); + } + // The hub-side deploy seam the on-demand catalog-block route below // drives — the SAME `prepareProvisionedDeployment` the native `POST // /workflows/deployments` route drives. Inference sources ride as diff --git a/apps/hub/src/tool-grants.test.ts b/apps/hub/src/tool-grants.test.ts index 6dcfabe325..15783b76cc 100644 --- a/apps/hub/src/tool-grants.test.ts +++ b/apps/hub/src/tool-grants.test.ts @@ -48,8 +48,10 @@ function assetRow(id: string, direct: boolean): AssetWithOrigin { } as AssetWithOrigin; } -const MEMORY_DIR = new URL("../../../packages/memory-tools", import.meta.url) - .pathname; +const CAPABILITY_DIR = new URL( + "../../../packages/capability-tools", + import.meta.url, +).pathname; const MANUS_DIR = new URL("../../../packages/manus-tools", import.meta.url) .pathname; @@ -71,17 +73,15 @@ async function grantsFor( describe("createToolGrantsForPins", () => { test("mints tool:/invoke for every tool the installed manifest declares", async () => { const assetService = await fakeRegistryAssetService([ - await packToolPackageTarball(MEMORY_DIR), + await packToolPackageTarball(CAPABILITY_DIR), ]); const grants = await grantsFor( assetService, [assetRow("asset_1", true)], - [{ name: "@corbits/memory-tools", version: "^1" }], + [{ name: "@corbits/capability-tools", version: "^1" }], ); expect(grants.map((grant) => grant.resource)).toEqual([ - "tool:@corbits/memory-tools/memory:memory_search", - "tool:@corbits/memory-tools/memory:memory_add", - "tool:@corbits/memory-tools/memory:memory_list", + "tool:@corbits/capability-tools/cap:request_capability", ]); expect(grants.every((grant) => grant.action === "invoke")).toBe(true); }); @@ -109,20 +109,20 @@ describe("createToolGrantsForPins", () => { test("unions grants across every pinned package and skips unknown pins", async () => { const assetService = await fakeRegistryAssetService([ - await packToolPackageTarball(MEMORY_DIR), + await packToolPackageTarball(CAPABILITY_DIR), await packToolPackageTarball(MANUS_DIR), ]); const grants = await grantsFor( assetService, [assetRow("asset_1", true)], [ - { name: "@corbits/memory-tools", version: "^1" }, + { name: "@corbits/capability-tools", version: "^1" }, { name: "@corbits/manus-tools", version: "*" }, { name: "@corbits/unknown-tools", version: "^1" }, ], ); expect( - grants.some((grant) => grant.resource.includes("memory-tools")), + grants.some((grant) => grant.resource.includes("capability-tools")), ).toBe(true); expect(grants.some((grant) => grant.resource.includes("manus-tools"))).toBe( true, @@ -134,12 +134,12 @@ describe("createToolGrantsForPins", () => { test("resolves an inherited registry, not only a direct one", async () => { const assetService = await fakeRegistryAssetService([ - await packToolPackageTarball(MEMORY_DIR), + await packToolPackageTarball(CAPABILITY_DIR), ]); const grants = await grantsFor( assetService, [assetRow("asset_root", false)], - [{ name: "@corbits/memory-tools", version: "^1" }], + [{ name: "@corbits/capability-tools", version: "^1" }], ); expect(grants.length).toBeGreaterThan(0); }); @@ -149,14 +149,14 @@ describe("createToolGrantsForPins", () => { const grants = await grantsFor( assetService, [], - [{ name: "@corbits/memory-tools", version: "^1" }], + [{ name: "@corbits/capability-tools", version: "^1" }], ); expect(grants).toEqual([]); }); test("no pins yields no grants", async () => { const assetService = await fakeRegistryAssetService([ - await packToolPackageTarball(MEMORY_DIR), + await packToolPackageTarball(CAPABILITY_DIR), ]); expect( await grantsFor(assetService, [assetRow("asset_1", true)], []), diff --git a/apps/sidecar/src/workflow-substrate-factory/compactors.ts b/apps/sidecar/src/workflow-substrate-factory/compactors.ts index 8192533a23..ba6b22ddb6 100644 --- a/apps/sidecar/src/workflow-substrate-factory/compactors.ts +++ b/apps/sidecar/src/workflow-substrate-factory/compactors.ts @@ -22,8 +22,8 @@ // Both folds are deliberately lossy -- a bounded recap of what was // active in the folded turns, not a durable record. Anything a channel // agent needs to recall past the working window is firm memory's job -// (`@corbits/memory-tools`'s `memory_search`/`memory_add`, already -// pinned for the assistant workflow), not this recap's. +// (`@corbits/memory/tools`'s `memory_search`/`memory_add`, already +// wired for the assistant workflow), not this recap's. import type { Compactor, @@ -270,7 +270,7 @@ export function createSummarizeOlderTurnsCompactor( * deliberately lossy -- it is a bounded reminder of what was active in * the folded turns, not a durable record. Anything a channel agent * needs to recall past the working window belongs in firm memory - * (`@corbits/memory-tools`'s `memory_search`/`memory_add`), not in this + * (`@corbits/memory/tools`'s `memory_search`/`memory_add`), not in this * recap. */ function foldOlderTurns( diff --git a/apps/sidecar/src/workflow-substrate-factory/step-env.test.ts b/apps/sidecar/src/workflow-substrate-factory/step-env.test.ts index 5a34ab0bf5..5bf5ec4fb8 100644 --- a/apps/sidecar/src/workflow-substrate-factory/step-env.test.ts +++ b/apps/sidecar/src/workflow-substrate-factory/step-env.test.ts @@ -1,9 +1,7 @@ // Proves the CL-6086 binding: `createSidecarStepBuildEnv` threads the // deploying definition's own `definitionId` (and the derived // `hubCapabilitiesUrl`) onto the built step env, following exactly the -// binding pattern `hubMemoryUrl`/`hubSkillsUrl`/`sidecarToken`/`address` -// already use — see this file's header comment for the memory-tools -// precedent this mirrors. +// binding pattern `hubSkillsUrl`/`sidecarToken`/`address` already use. import fs from "node:fs"; import os from "node:os"; import path from "node:path"; @@ -87,14 +85,17 @@ test("the built step env carries the deploying definition's own definitionId and expect( (env as unknown as { hubCapabilitiesUrl: string }).hubCapabilitiesUrl, ).toBe("https://hub.example.com"); - // Same hub origin as the sibling artifacts/memory/skills bindings — - // one origin, one name per tool-bundle surface, never overloaded. + // Same hub origin as the sibling artifacts/skills bindings — one + // origin, one name per tool-bundle surface, never overloaded. expect((env as unknown as { hubArtifactsUrl: string }).hubArtifactsUrl).toBe( "https://hub.example.com", ); - expect((env as unknown as { hubMemoryUrl: string }).hubMemoryUrl).toBe( + expect((env as unknown as { memoryBaseUrl: string }).memoryBaseUrl).toBe( "https://hub.example.com", ); + expect((env as unknown as { memoryTenantId: string }).memoryTenantId).toBe( + "ten_test", + ); expect((env as unknown as { address: string }).address).toBe( "run_1@example.com", ); diff --git a/apps/sidecar/src/workflow-substrate-factory/step-env.ts b/apps/sidecar/src/workflow-substrate-factory/step-env.ts index 4b348f7a33..eafab68ae8 100644 --- a/apps/sidecar/src/workflow-substrate-factory/step-env.ts +++ b/apps/sidecar/src/workflow-substrate-factory/step-env.ts @@ -126,11 +126,11 @@ export interface SidecarStepBuildEnvDeps { * (`@corbits/artifacts-hub`'s `createWorkflowArtifactRoutes`, CL-6000) * without ever holding a database handle. `address` is already on * `env` via `mailboxAddress` below; these two widen the same surface. - * The same origin is re-exposed on the step env as `hubMemoryUrl` for - * `@corbits/memory-tools` (`requires: ["hubMemoryUrl", "sidecarToken", - * "address"]`), which calls the sibling workflow-memory HTTP surface - * (`@corbits/memory-hub`'s `createWorkflowMemoryRoutes`, CL-5852), and - * again as `hubSkillsUrl` for `@corbits/tools-skills`, which calls + * The same origin is re-exposed on the step env as `memoryBaseUrl` for + * `@corbits/memory/tools` (CL-8186), which calls the STOCK + * `/api/tenants/:tenantId/memory/*` routes `@corbits/memory` mounts on + * the hub directly (see `memoryFetch` below), and again as + * `hubSkillsUrl` for `@corbits/tools-skills`, which calls * `@corbits/skills`' `createWorkflowSkillRoutes`. */ hubArtifactsUrl: string; @@ -372,7 +372,13 @@ export function createSidecarStepBuildEnv( transport: MessageTransport; address: string; hubArtifactsUrl: string; - hubMemoryUrl: string; + memoryBaseUrl: string; + memoryTenantId: string; + memoryAuthToken: string; + memoryFetch: ( + input: Parameters[0], + init?: Parameters[1], + ) => ReturnType; hubSkillsUrl: string; hubCapabilitiesUrl: string; hubRoutinesUrl: string; @@ -420,13 +426,26 @@ export function createSidecarStepBuildEnv( transport, address: deps.mailboxAddress, hubArtifactsUrl: deps.hubArtifactsUrl, - // Same hub HTTP origin as `hubArtifactsUrl` above, under the key - // `@corbits/memory-tools` declares (`requires: ["hubMemoryUrl", - // "sidecarToken", "address"]`) — one hub origin, two accurately - // named env keys per tool-bundle surface, matching the artifact - // bundle's own precedent rather than overloading its name for an - // unrelated surface. - hubMemoryUrl: deps.hubArtifactsUrl, + // `@corbits/memory/tools` (CL-8186) calls the STOCK + // `/api/tenants/:tenantId/memory/*` routes `@corbits/memory` mounts + // on the hub, the same run-bearer scheme `@corbits/connections-tools` + // and friends use: sidecar token as the bearer, `tenantId` as the + // path segment. Its own HTTP client sends only an `Authorization` + // header, so `memoryFetch` (the client's documented fetch override, + // never part of `requires`) is what actually attaches the + // `x-workflow-run-address` header the hub's `withWorkflowRunTenantAuth` + // needs to resolve the bearer to this run's principal. + memoryBaseUrl: deps.hubArtifactsUrl, + memoryTenantId: deps.tenantId, + memoryAuthToken: deps.sidecarToken, + memoryFetch: (input, init) => + fetch(input, { + ...init, + headers: { + ...init?.headers, + "x-workflow-run-address": deps.mailboxAddress, + }, + }), // And once more under the key `@corbits/tools-skills` declares // (`requires: ["hubSkillsUrl", "sidecarToken", "address"]`) for the // skill registry's own run-authenticated surface. diff --git a/apps/sidecar/test/deploy-router.test.ts b/apps/sidecar/test/deploy-router.test.ts index ebdc47d19c..d6165983f2 100644 --- a/apps/sidecar/test/deploy-router.test.ts +++ b/apps/sidecar/test/deploy-router.test.ts @@ -285,7 +285,7 @@ test("a single-step deploy writes the self-anchored run's grants before spawning const runId = `run_${"a1".repeat(16)}`; const grant = { id: "grant_1", - resource: "tool:@corbits/memory-tools:memory_search", + resource: "tool:@corbits/catalog-tools:models_list", action: "invoke", effect: "allow" as const, origin: "system" as const, diff --git a/packages/agent-directory/src/capability-inventory.ts b/packages/agent-directory/src/capability-inventory.ts index 28c03d5293..d02b1cc43f 100644 --- a/packages/agent-directory/src/capability-inventory.ts +++ b/packages/agent-directory/src/capability-inventory.ts @@ -25,7 +25,7 @@ export type CapabilityModelEntry = { readonly canonicalName: string }; */ export const BASELINE_AGENT_TOOL_PINS = [ "@corbits/mcp-tools", - "@corbits/memory-tools", + "@corbits/memory", "@corbits/interaction-tools", ] as const; diff --git a/packages/agent-directory/test/workflow-create-routes.test.ts b/packages/agent-directory/test/workflow-create-routes.test.ts index 4947fa7b79..c2491d5a52 100644 --- a/packages/agent-directory/test/workflow-create-routes.test.ts +++ b/packages/agent-directory/test/workflow-create-routes.test.ts @@ -31,7 +31,7 @@ const RUN_ADDRESS = `${RUN_ID}@example.com`; const fakeCapabilityInventory: CapabilityInventoryProvider = { resolve: () => Promise.resolve({ - toolPackages: [{ name: "@corbits/memory-tools" }], + toolPackages: [{ name: "@corbits/memory" }], skills: [{ name: "research" }], models: [{ canonicalName: "anthropic/claude-sonnet" }], }), @@ -53,8 +53,8 @@ function fakeAssetService(overrides: Partial = {}): AssetService { throw new Error("not used in these tests"); }, // `resolvePinnedVersion`'s tarball listing — every create test in - // this file pins (or gets baseline-pinned) `@corbits/memory-tools`. - listAssetBlobs: () => Promise.resolve(["corbits-memory-tools-1.4.0.tgz"]), + // this file pins (or gets baseline-pinned) `@corbits/memory`. + listAssetBlobs: () => Promise.resolve(["corbits-memory-1.4.0.tgz"]), ...overrides, } as unknown as AssetService; } @@ -296,12 +296,12 @@ test("a toolPackagePins entry the tenant's inventory offers is pinned onto the c name: "Research Buddy", handle: "research-buddy", systemPrompt: "You are a careful research assistant.", - toolPackagePins: ["@corbits/memory-tools"], + toolPackagePins: ["@corbits/memory"], }), }); expect(response.status).toBe(201); const written = definitionFrom(writtenFiles); - expect(written).toContain("@corbits/memory-tools"); + expect(written).toContain("@corbits/memory"); }); test("a create naming no pins gets the baseline set the inventory offers — a specialist is never toolless (CL-6206)", async () => { @@ -325,10 +325,10 @@ test("a create naming no pins gets the baseline set the inventory offers — a s }); expect(response.status).toBe(201); const written = definitionFrom(writtenFiles); - // The fake inventory offers memory-tools (see buildApp); mcp-tools and + // The fake inventory offers memory (see buildApp); mcp-tools and // interaction-tools are not offered, so only the resolvable baseline // member is pinned — never a pin that would fail at launch. - expect(written).toContain("@corbits/memory-tools"); + expect(written).toContain("@corbits/memory"); expect(written).not.toContain("@corbits/mcp-tools"); }); diff --git a/packages/evals/package.json b/packages/evals/package.json index 48715415af..a05946e36b 100644 --- a/packages/evals/package.json +++ b/packages/evals/package.json @@ -38,7 +38,6 @@ "@corbits/connections-tools": "workspace:*", "@corbits/github-tools": "workspace:*", "@corbits/mcp-tools": "workspace:*", - "@corbits/memory-tools": "workspace:*", "@types/bun": "catalog:", "typescript": "catalog:" } diff --git a/packages/evals/src/scorers/tool-names.test.ts b/packages/evals/src/scorers/tool-names.test.ts index c2ce6a06da..16db0919cb 100644 --- a/packages/evals/src/scorers/tool-names.test.ts +++ b/packages/evals/src/scorers/tool-names.test.ts @@ -1,7 +1,6 @@ import { expect, test } from "bun:test"; import { CREATE_AGENT_TOOL as REAL_CREATE_AGENT_TOOL } from "@corbits/agent-directory-tools"; -import { MEMORY_ADD_TOOL as REAL_MEMORY_ADD_TOOL } from "@corbits/memory-tools"; import { LIST_CONNECTIONS_TOOL as REAL_LIST_CONNECTIONS_TOOL } from "@corbits/connections-tools"; import { GITHUB_PULL_REQUEST_DIFF_TOOL as REAL_GITHUB_PULL_REQUEST_DIFF_TOOL, @@ -19,7 +18,9 @@ import { test("eval tool-name constants match the real manager-tools bundles", () => { expect(CREATE_AGENT_TOOL).toBe(REAL_CREATE_AGENT_TOOL); - expect(MEMORY_ADD_TOOL).toBe(REAL_MEMORY_ADD_TOOL); + // `@corbits/memory` exports no name constant (CL-8186) — its + // `memoryAdd` tool factory names itself "memory_add" inline. + expect(MEMORY_ADD_TOOL).toBe("memory_add"); expect(LIST_CONNECTIONS_TOOL).toBe(REAL_LIST_CONNECTIONS_TOOL); }); diff --git a/packages/evals/src/scorers/world-scorers.test.ts b/packages/evals/src/scorers/world-scorers.test.ts index d419e1177b..4f3af204e0 100644 --- a/packages/evals/src/scorers/world-scorers.test.ts +++ b/packages/evals/src/scorers/world-scorers.test.ts @@ -30,10 +30,7 @@ describe("agentHasTools", () => { id: "def-1", name: "AI Daily researcher", displayName: null, - toolPackagePins: [ - "@corbits/web-search-tools", - "@corbits/memory-tools", - ], + toolPackagePins: ["@corbits/web-search-tools", "@corbits/memory"], skills: [], model: null, }, @@ -52,7 +49,7 @@ describe("agentHasTools", () => { id: "def-1", name: "AI Daily researcher", displayName: null, - toolPackagePins: ["@corbits/memory-tools"], + toolPackagePins: ["@corbits/memory"], skills: [], model: null, }, diff --git a/packages/memory-hub/LICENSE b/packages/memory-hub/LICENSE deleted file mode 100644 index c6487f4fdf..0000000000 --- a/packages/memory-hub/LICENSE +++ /dev/null @@ -1,176 +0,0 @@ -GNU LESSER GENERAL PUBLIC LICENSE - -Version 2.1, February 1999 - -Copyright (C) 1991, 1999 Free Software Foundation, Inc. -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - -[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] - -Preamble - -The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. - -This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. - -When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. - -To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. - -For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. - -We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. - -To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. - -Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. - -Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. - -When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. - -We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. - -For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. - -In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. - -Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. - -The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. - -GNU LESSER GENERAL PUBLIC LICENSE -TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - -0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". - -A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. - -The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) - -"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. - -Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. - -1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. - -You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. - -2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. - -(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. - -3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. - -Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. - -This option is useful when you wish to copy part of the code of the Library into a program that is not a library. - -4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. - -If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. - -5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. - -However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. - -When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. - -If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) - -Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. - -6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. - -You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: - - a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. - - e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. - -For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. - -It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. - -7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. - - b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. - -8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. - -9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. - -10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. - -11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. - -This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. - -12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. - -13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. - -14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. - -NO WARRANTY - -15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -END OF TERMS AND CONDITIONS - -How to Apply These Terms to Your New Libraries - -If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). - -To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - one line to give the library's name and an idea of what it does. - Copyright (C) year name of author - - This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: - -Yoyodyne, Inc., hereby disclaims all copyright interest in -the library `Frob' (a library for tweaking knobs) written -by James Random Hacker. - -signature of Ty Coon, 1 April 1990 -Ty Coon, President of Vice -That's all there is to it! diff --git a/packages/memory-hub/README.md b/packages/memory-hub/README.md deleted file mode 100644 index 4ab622ba39..0000000000 --- a/packages/memory-hub/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# @corbits/memory-hub - -The sanctioned path for a workflow-process child to reach the mounted -`@corbits/memory` plane (CL-5852): `createWorkflowMemoryRoutes`, mounted -at `/api/workflow-memory` outside the tenant-session prefix, mirroring -`@corbits/artifacts-hub`'s `createWorkflowArtifactRoutes`. - -## Why not `@corbits/memory`'s own HTTP routes - -`@corbits/memory` is a vendored pin (never edited in this repo). Its own -`registerMemoryRoutes` authenticates via `c.get("principal")`, set by -the platform's tenant-session middleware for a browser/API caller. A -workflow-process child has no browser session — only the sidecar's own -bearer token and the run's own mailbox address. Reusing the plane's -tenant-session routes for that caller isn't possible without a session -to present, so this package establishes the same seam -`@corbits/artifact-tools`/`@corbits/artifacts-hub` already did for -Library artifacts (CL-6000): a narrower surface, authenticated by -`@corbits/artifacts-hub`'s `WorkflowRunAuthenticator` (itself generic — -no artifact coupling), serving through the plane's SAME in-process -`Memory` handle `apps/hub/src/memory-mount.ts`'s `mountMemory` returns, -never a second connection. - -## Routes - -- `POST /search` — `{query, limit?, kinds?}` → the tenant's search - results. -- `POST /add` — `{title, text, kind?}` → the created entry's - `documentId`/`versionId`. -- `GET /list?limit=` — the tenant's recent timeline. - -Every route scopes to the authenticated run's own tenant + principal. -Identity never rides in the request body: a caller-supplied `tenantId` -or `principalId` in a body is parsed and then explicitly discarded, not -forwarded to the plane. - -`createUnavailableWorkflowMemoryRoutes` answers `503` on every route -when the memory plane isn't mounted (no `EMBED_BASE_URL` / -`OLLAMA_BASE_URL`, and no native-Ollama inject from `bun run dev`). - -## Tests - -``` -cd packages/memory-hub && bun test -``` - -No DATABASE_URL needed — the authenticator and memory plane are injected -fakes in tests. diff --git a/packages/memory-hub/package.json b/packages/memory-hub/package.json deleted file mode 100644 index 65c013522c..0000000000 --- a/packages/memory-hub/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@corbits/memory-hub", - "private": true, - "description": "The sanctioned path for a workflow-process child to reach the mounted @corbits/memory plane: sidecar-token + run-address authenticated HTTP over the run's own in-process Memory handle, mirroring @corbits/artifacts-hub's workflow-artifacts surface", - "version": "0.0.1", - "license": "LGPL-2.1-or-later", - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "dependencies": { - "@corbits/artifacts-hub": "workspace:*", - "@corbits/error-sink": "workspace:*", - "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", - "arktype": "catalog:", - "hono": "catalog:" - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" - } -} diff --git a/packages/memory-hub/src/index.ts b/packages/memory-hub/src/index.ts deleted file mode 100644 index 4e185c7295..0000000000 --- a/packages/memory-hub/src/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { - createUnavailableWorkflowMemoryRoutes, - createWorkflowMemoryRoutes, - createWorkflowMemoryStore, - type AddedMemoryEntry, - type CreateWorkflowMemoryRoutesDeps, - type WorkflowMemoryEnv, - type WorkflowMemoryRoutesStore, -} from "./workflow-routes"; diff --git a/packages/memory-hub/src/workflow-routes.test.ts b/packages/memory-hub/src/workflow-routes.test.ts deleted file mode 100644 index 29de9969d7..0000000000 --- a/packages/memory-hub/src/workflow-routes.test.ts +++ /dev/null @@ -1,342 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { createFakeDocumentStore, createMemory } from "@corbits/memory"; -import type { ResolvedWorkflowRunScope } from "@corbits/artifacts-hub"; - -import { - createUnavailableWorkflowMemoryRoutes, - createWorkflowMemoryRoutes, - createWorkflowMemoryStore, - type WorkflowMemoryRoutesStore, -} from "./workflow-routes"; - -const SCOPE: ResolvedWorkflowRunScope = { - tenantId: "ten_1", - principalId: "prn_1", - runId: "run_1", -}; - -const OTHER_SCOPE: ResolvedWorkflowRunScope = { - tenantId: "ten_2", - principalId: "prn_2", - runId: "run_2", -}; - -const GOOD_TOKEN = "sidecar-token"; -const GOOD_ADDRESS = "run_1@workflow"; - -function fakeStore(overrides: Partial = {}) { - const calls: { - method: "search" | "add" | "list"; - scope: ResolvedWorkflowRunScope; - input: unknown; - }[] = []; - const store: WorkflowMemoryRoutesStore = { - async search(scope, input) { - calls.push({ method: "search", scope, input }); - return { items: [] }; - }, - async add(scope, input) { - calls.push({ method: "add", scope, input }); - return { documentId: "doc_1", versionId: "ver_1" }; - }, - async list(scope, limit) { - calls.push({ method: "list", scope, input: { limit } }); - return []; - }, - ...overrides, - }; - return { store, calls }; -} - -function appFor(store: WorkflowMemoryRoutesStore) { - return createWorkflowMemoryRoutes({ - authenticator: { - async resolve(token, address) { - if (token === GOOD_TOKEN && address === GOOD_ADDRESS) return SCOPE; - return null; - }, - }, - store, - }); -} - -describe("createWorkflowMemoryRoutes auth", () => { - test("rejects a request with no bearer token", async () => { - const { store } = fakeStore(); - const res = await appFor(store).request("/list", { - headers: { "x-workflow-run-address": GOOD_ADDRESS }, - }); - expect(res.status).toBe(401); - }); - - test("rejects a request with a wrong run address — never scopes to the caller's claimed tenant", async () => { - const { store, calls } = fakeStore(); - const res = await appFor(store).request("/search", { - method: "POST", - headers: { - authorization: `Bearer ${GOOD_TOKEN}`, - "x-workflow-run-address": "someone-else@workflow", - "content-type": "application/json", - }, - body: JSON.stringify({ query: "q" }), - }); - expect(res.status).toBe(401); - expect(calls).toHaveLength(0); - }); -}); - -describe("POST /add", () => { - test("adds a memory entry scoped to the authenticated run's tenant + principal, never the body", async () => { - const { store, calls } = fakeStore(); - const res = await appFor(store).request("/add", { - method: "POST", - headers: { - authorization: `Bearer ${GOOD_TOKEN}`, - "x-workflow-run-address": GOOD_ADDRESS, - "content-type": "application/json", - }, - // A malicious/confused caller naming a different tenant in the body - // must never override the authenticated scope. - body: JSON.stringify({ - title: "Note", - text: "hello", - tenantId: OTHER_SCOPE.tenantId, - principalId: OTHER_SCOPE.principalId, - }), - }); - expect(res.status).toBe(201); - const body = (await res.json()) as { - data: { documentId: string; versionId: string }; - }; - expect(body.data).toEqual({ documentId: "doc_1", versionId: "ver_1" }); - expect(calls).toEqual([ - { method: "add", scope: SCOPE, input: { title: "Note", text: "hello" } }, - ]); - }); - - test("rejects a body missing required fields", async () => { - const { store } = fakeStore(); - const res = await appFor(store).request("/add", { - method: "POST", - headers: { - authorization: `Bearer ${GOOD_TOKEN}`, - "x-workflow-run-address": GOOD_ADDRESS, - "content-type": "application/json", - }, - body: JSON.stringify({ title: "Note" }), - }); - expect(res.status).toBe(400); - }); - - test("rejects invalid JSON", async () => { - const { store } = fakeStore(); - const res = await appFor(store).request("/add", { - method: "POST", - headers: { - authorization: `Bearer ${GOOD_TOKEN}`, - "x-workflow-run-address": GOOD_ADDRESS, - "content-type": "application/json", - }, - body: "not json", - }); - expect(res.status).toBe(400); - }); - - test("rejects text over 64,000 characters with a 413 that tells the model to shorten or split it", async () => { - const { store, calls } = fakeStore(); - const res = await appFor(store).request("/add", { - method: "POST", - headers: { - authorization: `Bearer ${GOOD_TOKEN}`, - "x-workflow-run-address": GOOD_ADDRESS, - "content-type": "application/json", - }, - body: JSON.stringify({ title: "Note", text: "a".repeat(64_001) }), - }); - expect(res.status).toBe(413); - const body = (await res.json()) as { - error: { code: string; userMessage: string; refId: string }; - }; - expect(body.error.code).toBe("text_too_large"); - expect(body.error.userMessage).toMatch(/shorten|split/); - expect(calls).toHaveLength(0); - }); - - test("accepts text at exactly the 64,000-character limit", async () => { - const { store } = fakeStore(); - const res = await appFor(store).request("/add", { - method: "POST", - headers: { - authorization: `Bearer ${GOOD_TOKEN}`, - "x-workflow-run-address": GOOD_ADDRESS, - "content-type": "application/json", - }, - body: JSON.stringify({ title: "Note", text: "a".repeat(64_000) }), - }); - expect(res.status).toBe(201); - }); - - test("rejects the 31st add for the same run within a minute with a 429", async () => { - const { store } = fakeStore(); - const app = appFor(store); - const request = () => - app.request("/add", { - method: "POST", - headers: { - authorization: `Bearer ${GOOD_TOKEN}`, - "x-workflow-run-address": GOOD_ADDRESS, - "content-type": "application/json", - }, - body: JSON.stringify({ title: "Note", text: "hello" }), - }); - - for (let i = 0; i < 30; i++) { - const res = await request(); - expect(res.status).toBe(201); - } - const limited = await request(); - expect(limited.status).toBe(429); - const body = (await limited.json()) as { - error: { code: string; userMessage: string; refId: string }; - }; - expect(body.error.code).toBe("rate_limited"); - expect(body.error.userMessage).toMatch(/wait/i); - }); - - test("rate-limits per run, not globally — a different run's 1st add still succeeds", async () => { - const { store } = fakeStore(); - const app = createWorkflowMemoryRoutes({ - authenticator: { - async resolve(token, address) { - if (token !== GOOD_TOKEN) return null; - if (address === GOOD_ADDRESS) return SCOPE; - if (address === "run_2@workflow") return OTHER_SCOPE; - return null; - }, - }, - store, - }); - const addFor = (address: string) => - app.request("/add", { - method: "POST", - headers: { - authorization: `Bearer ${GOOD_TOKEN}`, - "x-workflow-run-address": address, - "content-type": "application/json", - }, - body: JSON.stringify({ title: "Note", text: "hello" }), - }); - - for (let i = 0; i < 30; i++) { - expect((await addFor(GOOD_ADDRESS)).status).toBe(201); - } - expect((await addFor(GOOD_ADDRESS)).status).toBe(429); - expect((await addFor("run_2@workflow")).status).toBe(201); - }); -}); - -describe("POST /search", () => { - test("searches scoped to the authenticated run, defaulting the limit", async () => { - const { store, calls } = fakeStore(); - const res = await appFor(store).request("/search", { - method: "POST", - headers: { - authorization: `Bearer ${GOOD_TOKEN}`, - "x-workflow-run-address": GOOD_ADDRESS, - "content-type": "application/json", - }, - body: JSON.stringify({ query: "what did we decide" }), - }); - expect(res.status).toBe(200); - expect(calls).toEqual([ - { - method: "search", - scope: SCOPE, - input: { query: "what did we decide", limit: 8, kinds: undefined }, - }, - ]); - }); - - test("rejects an empty query", async () => { - const { store } = fakeStore(); - const res = await appFor(store).request("/search", { - method: "POST", - headers: { - authorization: `Bearer ${GOOD_TOKEN}`, - "x-workflow-run-address": GOOD_ADDRESS, - "content-type": "application/json", - }, - body: JSON.stringify({ query: "" }), - }); - expect(res.status).toBe(400); - }); -}); - -describe("GET /list", () => { - test("lists scoped to the authenticated run's tenant", async () => { - const { store, calls } = fakeStore(); - const res = await appFor(store).request("/list?limit=5", { - headers: { - authorization: `Bearer ${GOOD_TOKEN}`, - "x-workflow-run-address": GOOD_ADDRESS, - }, - }); - expect(res.status).toBe(200); - expect(calls).toEqual([ - { method: "list", scope: SCOPE, input: { limit: 5 } }, - ]); - }); -}); - -describe("createUnavailableWorkflowMemoryRoutes", () => { - test("every route answers 503 when the memory plane is not mounted", async () => { - const app = createUnavailableWorkflowMemoryRoutes(); - const search = await app.request("/search", { method: "POST" }); - const add = await app.request("/add", { method: "POST" }); - const list = await app.request("/list"); - expect(search.status).toBe(503); - expect(add.status).toBe(503); - expect(list.status).toBe(503); - }); -}); - -describe("createWorkflowMemoryStore over the published @corbits/memory plane", () => { - test("add/search/list go through createMemory + createFakeDocumentStore, scoped to tenant+principal", async () => { - const memory = createMemory({ - documentStore: createFakeDocumentStore(), - }); - try { - const store = createWorkflowMemoryStore(memory); - const added = await store.add(SCOPE, { - title: "Decision A", - text: "Tenant one shipped memory tools.", - }); - expect(added.documentId).toMatch(/^fake_doc_/); - await store.add(OTHER_SCOPE, { - title: "Decision B", - text: "Tenant two shipped memory tools.", - }); - const found = await store.search(SCOPE, { - query: "shipped memory tools", - }); - expect(found.items.map((item) => item.title)).toEqual(["Decision A"]); - const other = await store.search(OTHER_SCOPE, { - query: "shipped memory tools", - }); - expect(other.items.map((item) => item.title)).toEqual(["Decision B"]); - const listed = await store.list(SCOPE, 8); - expect(listed.map((event) => event.title)).toEqual(["Decision A"]); - } finally { - await memory.close(); - } - }); - - test("pins the published github @corbits/memory package, not a workbench-local store", async () => { - const pkg = (await Bun.file( - new URL("../package.json", import.meta.url), - ).json()) as { dependencies: Record }; - expect(pkg.dependencies["@corbits/memory"]).toMatch( - /^github:corbitsdev\/corbits-memory#/, - ); - }); -}); diff --git a/packages/memory-hub/src/workflow-routes.ts b/packages/memory-hub/src/workflow-routes.ts deleted file mode 100644 index ab99f49809..0000000000 --- a/packages/memory-hub/src/workflow-routes.ts +++ /dev/null @@ -1,333 +0,0 @@ -/** - * The sanctioned path for a workflow-process child to reach the mounted - * `@corbits/memory` plane. Mirrors `@corbits/artifacts-hub`'s - * `createWorkflowArtifactRoutes` (CL-6000) rather than reusing - * `@corbits/memory`'s own `registerMemoryRoutes` / - * `/api/tenants/:tenantId/memory/*`: that surface authenticates via - * `c.get("principal")`, set by the platform's tenant-session middleware - * (`createResolveTenant`) for a browser/API caller — a workflow-process - * child has no browser session, only its own sidecar bearer token and - * run address (`BaseEnv.address`, `apps/sidecar/src/workflow-substrate-factory/step-env.ts`). - * `@corbits/memory` is a vendored pin (never edited in this repo), so a - * new, narrower surface is the only honest option — same seam - * `@corbits/artifact-tools` already established for Library artifacts. - * - * Mounted OUTSIDE the tenant-session prefix (`TENANT_PREFIX`) for the - * same reason `/api/workflow-artifacts` is: every request here - * authenticates via `WorkflowRunAuthenticator` (reused from - * `@corbits/artifacts-hub`, itself generic over "a sidecar-provisioned - * caller acting as one resolved run's tenant + principal" — no artifact - * coupling), never `resolveTenant` + `requireGrant`. Every read/write is - * scoped to the authenticated run's own tenant + principal: identity - * NEVER rides in the request body, so a model's tool arguments can never - * name a different tenant or principal than the run's own. - */ -import { type } from "arktype"; -import { Hono } from "hono"; -import { makeErrorEnvelope } from "@corbits/error-sink"; -import type { - ResolvedWorkflowRunScope, - WorkflowRunAuthenticator, -} from "@corbits/artifacts-hub"; -import type { Memory, SearchResult, TimelineEvent } from "@corbits/memory"; - -const DEFAULT_SEARCH_LIMIT = 8; -const DEFAULT_LIST_LIMIT = 20; - -// A memory entry is a durable note, not a document store — 64k -// characters is generous for any honest note while still catching a -// model that pastes an entire tool result or file verbatim instead of -// summarizing it. Mirrors `@corbits/artifacts-hub`'s upload size caps -// (`./routes.ts`'s `MAX_UPLOAD_BYTES`) in spirit: a bound stated here, -// in the package, not left to the caller's judgment. -const MAX_ADD_TEXT_CHARS = 64_000; - -// A finalized turn can legitimately record a handful of memory entries -// (one per persisted artifact, plus a digest) in one burst; 30/minute -// per run comfortably covers that while still catching a runaway loop -// that would otherwise flood the memory plane's storage before a human -// notices. -const MAX_ADDS_PER_RUN_PER_MINUTE = 30; -const RATE_WINDOW_MS = 60_000; - -/** - * In-process sliding-window rate limiter, closed over per - * `createWorkflowMemoryRoutes` call — resets on hub restart, which is - * fine: unlike the durable redelivery-dedup claim (`@corbits/chat`'s - * `WriteClaimStore`), a rate bound only needs to hold within one - * process's uptime, never across it. Per-process also means per-replica: - * N hub replicas give one run an effective N × `MAX_ADDS_PER_RUN_PER_MINUTE` - * budget, since each replica counts only what it personally handled — - * a known fail-open gap, not a fail-closed one, so it under-limits rather - * than wrongly rejecting a caller a sibling replica hasn't seen yet. - */ -function createRunAddRateLimiter(maxPerWindow: number) { - const timestampsByRunId = new Map(); - return { - allow(runId: string): boolean { - const now = Date.now(); - const cutoff = now - RATE_WINDOW_MS; - const recent = (timestampsByRunId.get(runId) ?? []).filter( - (timestamp) => timestamp > cutoff, - ); - if (recent.length >= maxPerWindow) { - timestampsByRunId.set(runId, recent); - return false; - } - recent.push(now); - timestampsByRunId.set(runId, recent); - return true; - }, - }; -} - -export type WorkflowMemoryEnv = { - Variables: { workflowRunScope: ResolvedWorkflowRunScope }; -}; - -const SearchBody = type({ - query: "string > 0", - "limit?": "number", - "kinds?": "string[]", -}); - -const AddBody = type({ - title: "string > 0", - text: "string > 0", - "kind?": "string > 0", -}); - -export type AddedMemoryEntry = { - readonly documentId: string; - readonly versionId: string; -}; - -/** Minimal port the routes need over the plane's in-process `Memory` handle. */ -export type WorkflowMemoryRoutesStore = { - search( - scope: ResolvedWorkflowRunScope, - input: { query: string; limit?: number; kinds?: string[] }, - ): Promise; - add( - scope: ResolvedWorkflowRunScope, - input: { title: string; text: string; kind?: string }, - ): Promise; - list( - scope: ResolvedWorkflowRunScope, - limit: number, - ): Promise; -}; - -function parseLimit(raw: string | undefined, fallback: number): number { - if (raw === undefined || raw === "") return fallback; - const n = Number.parseInt(raw, 10); - if (!Number.isFinite(n) || n < 1) return fallback; - return n; -} - -/** - * Production store over `@corbits/memory`'s in-process `Memory` handle - * (`apps/hub/src/memory-mount.ts`'s `MemoryMountHandle.memory`) — the - * SAME plane instance the mount's HTTP routes serve, never a second - * connection. - */ -export function createWorkflowMemoryStore( - memory: Pick, -): WorkflowMemoryRoutesStore { - return { - async search(scope, input) { - const base = { - principalId: scope.principalId, - tenantId: scope.tenantId, - query: input.query, - }; - const withLimit = - input.limit !== undefined ? { ...base, limit: input.limit } : base; - const params = - input.kinds !== undefined - ? { ...withLimit, kinds: input.kinds } - : withLimit; - return memory.search(params); - }, - async add(scope, input) { - const base = { - principalId: scope.principalId, - tenantId: scope.tenantId, - content: { title: input.title, text: input.text }, - }; - const params = - input.kind !== undefined ? { ...base, kind: input.kind } : base; - const result = await memory.add(params); - return { documentId: result.documentId, versionId: result.versionId }; - }, - async list(scope, limit) { - return memory.list({ - principalId: scope.principalId, - tenantId: scope.tenantId, - limit, - }); - }, - }; -} - -export type CreateWorkflowMemoryRoutesDeps = { - authenticator: WorkflowRunAuthenticator; - store: WorkflowMemoryRoutesStore; -}; - -export function createWorkflowMemoryRoutes( - deps: CreateWorkflowMemoryRoutesDeps, -): Hono { - const app = new Hono(); - const addRateLimiter = createRunAddRateLimiter(MAX_ADDS_PER_RUN_PER_MINUTE); - - app.use("*", async (c, next) => { - const authHeader = c.req.header("authorization") ?? ""; - const token = authHeader.startsWith("Bearer ") - ? authHeader.slice("Bearer ".length) - : ""; - const address = c.req.header("x-workflow-run-address") ?? ""; - const scope = await deps.authenticator.resolve(token, address); - if (scope === null) { - return c.json( - makeErrorEnvelope({ - code: "unauthorized", - userMessage: - "Missing or unrecognized sidecar bearer token / run address", - }), - 401, - ); - } - c.set("workflowRunScope", scope); - await next(); - }); - - app.post("/search", async (c) => { - let body: unknown; - try { - body = await c.req.json(); - } catch { - return c.json( - makeErrorEnvelope({ - code: "bad_request", - userMessage: "Invalid JSON body", - }), - 400, - ); - } - const parsed = SearchBody(body); - if (parsed instanceof type.errors) { - return c.json( - makeErrorEnvelope({ - code: "bad_request", - userMessage: parsed.summary, - }), - 400, - ); - } - const searchInput = { - query: parsed.query, - limit: parsed.limit ?? DEFAULT_SEARCH_LIMIT, - }; - const result = await deps.store.search( - c.get("workflowRunScope"), - parsed.kinds !== undefined - ? { ...searchInput, kinds: parsed.kinds } - : searchInput, - ); - return c.json({ data: result }); - }); - - app.post("/add", async (c) => { - let body: unknown; - try { - body = await c.req.json(); - } catch { - return c.json( - makeErrorEnvelope({ - code: "bad_request", - userMessage: "Invalid JSON body", - }), - 400, - ); - } - const parsed = AddBody(body); - if (parsed instanceof type.errors) { - return c.json( - makeErrorEnvelope({ - code: "bad_request", - userMessage: parsed.summary, - }), - 400, - ); - } - if (parsed.text.length > MAX_ADD_TEXT_CHARS) { - return c.json( - makeErrorEnvelope({ - code: "text_too_large", - userMessage: - `text is ${parsed.text.length} characters, over the ` + - `${MAX_ADD_TEXT_CHARS}-character limit — shorten it or split ` + - "it into multiple memory entries and try again.", - }), - 413, - ); - } - - const scope = c.get("workflowRunScope"); - if (!addRateLimiter.allow(scope.runId)) { - return c.json( - makeErrorEnvelope({ - code: "rate_limited", - userMessage: - `too many memory writes for this run in the last minute ` + - `(limit ${MAX_ADDS_PER_RUN_PER_MINUTE}/min) — wait a moment ` + - "before adding more.", - }), - 429, - ); - } - - // Explicit pick, never a spread of `parsed`: arktype does not strip - // unvalidated keys, so a caller-supplied `tenantId`/`principalId` in - // the body must never ride through to the store — attribution comes - // only from the authenticated `workflowRunScope` above. - const addInput = { title: parsed.title, text: parsed.text }; - const added = await deps.store.add( - scope, - parsed.kind !== undefined ? { ...addInput, kind: parsed.kind } : addInput, - ); - return c.json({ data: added }, 201); - }); - - app.get("/list", async (c) => { - const limit = parseLimit(c.req.query("limit"), DEFAULT_LIST_LIMIT); - const data = await deps.store.list(c.get("workflowRunScope"), limit); - return c.json({ data }); - }); - - return app; -} - -/** - * Honest degraded surface when the memory plane is not mounted (no - * `EMBED_BASE_URL` / `OLLAMA_BASE_URL`, see `apps/hub/src/memory-mount.ts`) - * — same convention as `createUnavailableWorkflowArtifactRoutes`. - */ -export function createUnavailableWorkflowMemoryRoutes(): Hono { - const app = new Hono(); - const unavailable = (c: { - json: (body: unknown, status: 503) => Response | Promise; - }) => - c.json( - makeErrorEnvelope({ - code: "unavailable", - userMessage: "Memory plane is not configured on this hub", - }), - 503, - ); - app.post("/search", unavailable); - app.post("/add", unavailable); - app.get("/list", unavailable); - return app; -} diff --git a/packages/memory-hub/tsconfig.json b/packages/memory-hub/tsconfig.json deleted file mode 100644 index e67af9fa66..0000000000 --- a/packages/memory-hub/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "extends": "./tsconfig.src.json", - "compilerOptions": { - "composite": false, - "noEmit": true, - "disableSourceOfProjectReferenceRedirect": true, - "declaration": false, - "declarationMap": false, - "emitDeclarationOnly": false, - "tsBuildInfoFile": "tsconfig.tsbuildinfo", - "rootDir": "../.." - }, - "include": ["src"], - "exclude": [], - "references": [ - { - "path": "../artifacts-hub/tsconfig.src.json" - }, - { - "path": "../error-sink/tsconfig.src.json" - } - ] -} diff --git a/packages/memory-hub/tsconfig.src.json b/packages/memory-hub/tsconfig.src.json deleted file mode 100644 index 5869ac9747..0000000000 --- a/packages/memory-hub/tsconfig.src.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "package.json", "src/**/*.json"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], - "compilerOptions": { - "types": ["bun"], - "customConditions": ["bun"], - "composite": true, - "emitDeclarationOnly": true, - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - }, - "references": [ - { - "path": "../artifacts-hub/tsconfig.src.json" - }, - { - "path": "../error-sink/tsconfig.src.json" - } - ] -} diff --git a/packages/memory-tools/LICENSE b/packages/memory-tools/LICENSE deleted file mode 100644 index c6487f4fdf..0000000000 --- a/packages/memory-tools/LICENSE +++ /dev/null @@ -1,176 +0,0 @@ -GNU LESSER GENERAL PUBLIC LICENSE - -Version 2.1, February 1999 - -Copyright (C) 1991, 1999 Free Software Foundation, Inc. -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - -[This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] - -Preamble - -The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. - -This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. - -When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. - -To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. - -For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. - -We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. - -To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. - -Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. - -Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. - -When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. - -We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. - -For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. - -In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. - -Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. - -The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. - -GNU LESSER GENERAL PUBLIC LICENSE -TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - -0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". - -A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. - -The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) - -"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. - -Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. - -1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. - -You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. - -2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. - -(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. - -3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. - -Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. - -This option is useful when you wish to copy part of the code of the Library into a program that is not a library. - -4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. - -If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. - -5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. - -However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. - -When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. - -If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) - -Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. - -6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. - -You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: - - a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. - - e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. - -For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. - -It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. - -7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. - - b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. - -8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. - -9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. - -10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. - -11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. - -This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. - -12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. - -13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. - -14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. - -NO WARRANTY - -15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -END OF TERMS AND CONDITIONS - -How to Apply These Terms to Your New Libraries - -If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). - -To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - one line to give the library's name and an idea of what it does. - Copyright (C) year name of author - - This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: - -Yoyodyne, Inc., hereby disclaims all copyright interest in -the library `Frob' (a library for tweaking knobs) written -by James Random Hacker. - -signature of Ty Coon, 1 April 1990 -Ty Coon, President of Vice -That's all there is to it! diff --git a/packages/memory-tools/README.md b/packages/memory-tools/README.md deleted file mode 100644 index ff4d0250f6..0000000000 --- a/packages/memory-tools/README.md +++ /dev/null @@ -1,88 +0,0 @@ -# @corbits/memory-tools - -The `memory_search`, `memory_add`, and `memory_list` `@intx/agent` tool -bundle (CL-5852): the agent-facing surface a workflow definition pins to -reach the tenant's firm-memory plane (`@corbits/memory`, mounted by -`apps/hub/src/memory-mount.ts`). Also exports the HTTP client -(`searchMemory`, `addMemory`, `listMemory`) the sanctioned -workflow-memory path is built on. - -## Tools - -- `memory_search` — searches the tenant's firm memory for entries - relevant to a query. -- `memory_add` — records one entry (title + text, optional kind). -- `memory_list` — lists the tenant's most recent entries as a timeline. - -None of the three tools' input schemas accept a tenant or principal -argument. Attribution always comes from the run's own authenticated -identity on the hub side, never from a model-supplied value. - -## The sanctioned workflow-memory path (CL-5852) - -`@corbits/memory`'s own HTTP routes (`registerMemoryRoutes`, mounted at -`/api/tenants/:tenantId/memory/*`) authenticate via -`c.get("principal")`, set by the platform's tenant-session middleware — -a caller with a browser/API session. A workflow-process child has -neither a database handle nor a browser session, only the sidecar's own -bearer token and the run's own mailbox address (the same pair -`@corbits/artifact-tools` already established this precedent for). This -package's tools instead call `@corbits/memory-hub`'s -`createWorkflowMemoryRoutes` (mounted at `/api/workflow-memory`, outside -the tenant-session prefix), which authenticates that pair via -`@corbits/artifacts-hub`'s `WorkflowRunAuthenticator` and resolves it to -the run's own tenant + principal before touching the plane. - -A transport, HTTP, or shape failure comes back as a completed result -with `isError: true` — never fabricate a memory or a search result. - -## When the memory plane isn't configured (CL-6168) - -`apps/hub/src/memory-mount.ts` decides whether the memory plane is -mounted at hub boot — from `EMBED_BASE_URL`, `OLLAMA_BASE_URL`, or the -native-Ollama inject `bun run dev` applies — never by making a call and -seeing what happens. An unmounted plane is reflected two ways, both -driven by that one boot-time decision: - -- The hub's tool inventory (`listMyraUsableToolPackages` in - `apps/hub/src/index.ts`) simply never offers `@corbits/memory-tools` to - an agent, the same way `@corbits/mcp-tools` is only offered when an MCP - server connection exists — so an unconfigured hub never tempts Myra - into believing memory works. -- If a tool call reaches `/api/workflow-memory` anyway, - `createUnavailableWorkflowMemoryRoutes` answers a `503` with - `error.code: "unavailable"`, which `memory_search`/`memory_add`/ - `memory_list` each translate into a plain-language, `isError: false` - result ("Memory isn't set up on this server yet — proceeding without - it.") instead of surfacing HTTP noise. Any OTHER failure (a real - network error, an unexpected shape, an actual 5xx) still comes back as - `isError: true` — the distinction is the hub's own honest signal, not a - transport error being swallowed. - -See the root `.env.example`'s `EMBED_BASE_URL` section for the local-dev -option (a local Ollama instance, no API key needed) alongside the -managed-provider one. - -## Usage - -```ts -import { memoryTools } from "@corbits/memory-tools"; - -const agent = defineAgent({ - // ... - tools: [memoryTools], -}); -``` - -The bundle's env requirements (`hubMemoryUrl`, `sidecarToken`, -`address`) are populated automatically for every workflow step — see -`apps/sidecar/src/workflow-substrate-factory/step-env.ts`. - -## Tests - -``` -cd packages/memory-tools && bun test -``` - -No DATABASE_URL needed — the HTTP client and tool tests run against -injected fakes/mocked fetch. diff --git a/packages/memory-tools/package.json b/packages/memory-tools/package.json deleted file mode 100644 index 066cc924dc..0000000000 --- a/packages/memory-tools/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "@corbits/memory-tools", - "description": "Firm-memory search/add/list as an @intx/agent tool bundle for workflow definitions: calls the sanctioned workflow-memory HTTP surface (@corbits/memory-hub) with the sidecar's own bearer token and the run's own address, never a database handle or model-supplied identity", - "version": "0.0.4", - "license": "LGPL-2.1-or-later", - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "files": [ - "src", - "LICENSE", - "README.md" - ], - "repository": { - "type": "git", - "url": "https://github.com/corbitsdev/workbench.git", - "directory": "packages/memory-tools" - }, - "publishConfig": { - "access": "public" - }, - "scripts": { - "typecheck": "tsc --noEmit", - "test": "bun test" - }, - "dependencies": { - "arktype": "catalog:" - }, - "peerDependencies": { - "@intx/agent": "0.3.0", - "@intx/types": "0.3.0" - }, - "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" - } -} diff --git a/packages/memory-tools/src/client.test.ts b/packages/memory-tools/src/client.test.ts deleted file mode 100644 index 872728107c..0000000000 --- a/packages/memory-tools/src/client.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { expect, test } from "bun:test"; - -import { - addMemory, - listMemory, - MemoryUnavailableError, - searchMemory, -} from "./client"; - -const CONFIG = { - hubMemoryUrl: "https://hub.example.com", - sidecarToken: "sc-token", - runAddress: "run_1@workflow", -}; - -test("searchMemory posts to the workflow-memory search endpoint and returns items", async () => { - const captured: { url: string; init: RequestInit | undefined } = { - url: "", - init: undefined, - }; - const fetchImpl = (async (input: URL | string, init?: RequestInit) => { - captured.url = String(input); - captured.init = init; - return new Response( - JSON.stringify({ - data: { - items: [ - { - documentId: "doc_1", - title: "Decision", - snippet: "We decided...", - score: 0.9, - kind: "decision", - }, - ], - }, - }), - { status: 200 }, - ); - }) as unknown as typeof fetch; - - const result = await searchMemory( - { ...CONFIG, fetchImpl }, - { query: "what did we decide" }, - ); - - expect(result).toEqual([ - { - documentId: "doc_1", - title: "Decision", - snippet: "We decided...", - score: 0.9, - kind: "decision", - }, - ]); - expect(captured.url).toBe( - "https://hub.example.com/api/workflow-memory/search", - ); - const headers = captured.init?.headers as Record; - expect(headers["authorization"]).toBe("Bearer sc-token"); - expect(headers["x-workflow-run-address"]).toBe("run_1@workflow"); - expect(JSON.parse(String(captured.init?.body))).toEqual({ - query: "what did we decide", - }); -}); - -test("searchMemory throws on a non-ok HTTP response", async () => { - const fetchImpl = (async () => - new Response("nope", { status: 401 })) as unknown as typeof fetch; - - await expect( - searchMemory({ ...CONFIG, fetchImpl }, { query: "q" }), - ).rejects.toThrow(/401/); -}); - -test("searchMemory throws when the response doesn't match the expected shape", async () => { - const fetchImpl = (async () => - new Response(JSON.stringify({ data: {} }), { - status: 200, - })) as unknown as typeof fetch; - - await expect( - searchMemory({ ...CONFIG, fetchImpl }, { query: "q" }), - ).rejects.toThrow(/did not match the expected shape/); -}); - -test("searchMemory throws MemoryUnavailableError when the hub reports the memory plane isn't mounted", async () => { - const fetchImpl = (async () => - new Response( - JSON.stringify({ - error: { - code: "unavailable", - message: "Memory plane is not configured on this hub", - }, - }), - { status: 503 }, - )) as unknown as typeof fetch; - - await expect( - searchMemory({ ...CONFIG, fetchImpl }, { query: "q" }), - ).rejects.toBeInstanceOf(MemoryUnavailableError); -}); - -test("addMemory posts title/text/kind and returns the created ids", async () => { - const captured: { url: string; init: RequestInit | undefined } = { - url: "", - init: undefined, - }; - const fetchImpl = (async (input: URL | string, init?: RequestInit) => { - captured.url = String(input); - captured.init = init; - return new Response( - JSON.stringify({ data: { documentId: "doc_1", versionId: "ver_1" } }), - { status: 201 }, - ); - }) as unknown as typeof fetch; - - const result = await addMemory( - { ...CONFIG, fetchImpl }, - { title: "Note", text: "hello", kind: "fact" }, - ); - - expect(result).toEqual({ documentId: "doc_1", versionId: "ver_1" }); - expect(captured.url).toBe("https://hub.example.com/api/workflow-memory/add"); - expect(JSON.parse(String(captured.init?.body))).toEqual({ - title: "Note", - text: "hello", - kind: "fact", - }); -}); - -test("addMemory throws on a non-ok HTTP response", async () => { - const fetchImpl = (async () => - new Response("nope", { status: 500 })) as unknown as typeof fetch; - - await expect( - addMemory({ ...CONFIG, fetchImpl }, { title: "Note", text: "hello" }), - ).rejects.toThrow(/500/); -}); - -test("addMemory throws MemoryUnavailableError when the hub reports the memory plane isn't mounted", async () => { - const fetchImpl = (async () => - new Response( - JSON.stringify({ - error: { - code: "unavailable", - message: "Memory plane is not configured on this hub", - }, - }), - { status: 503 }, - )) as unknown as typeof fetch; - - await expect( - addMemory({ ...CONFIG, fetchImpl }, { title: "Note", text: "hello" }), - ).rejects.toBeInstanceOf(MemoryUnavailableError); -}); - -test("listMemory GETs the list endpoint with the limit query param", async () => { - const captured: { url: string } = { url: "" }; - const fetchImpl = (async (input: URL | string) => { - captured.url = String(input); - return new Response( - JSON.stringify({ - data: [ - { at: "2026-08-13T00:00:00.000Z", title: "Note", source: "local" }, - ], - }), - { status: 200 }, - ); - }) as unknown as typeof fetch; - - const result = await listMemory({ ...CONFIG, fetchImpl }, { limit: 5 }); - - expect(result).toEqual([ - { at: "2026-08-13T00:00:00.000Z", title: "Note", source: "local" }, - ]); - expect(captured.url).toBe( - "https://hub.example.com/api/workflow-memory/list?limit=5", - ); -}); - -test("listMemory omits the query param when no limit is given", async () => { - const captured: { url: string } = { url: "" }; - const fetchImpl = (async (input: URL | string) => { - captured.url = String(input); - return new Response(JSON.stringify({ data: [] }), { status: 200 }); - }) as unknown as typeof fetch; - - await listMemory({ ...CONFIG, fetchImpl }); - - expect(captured.url).toBe("https://hub.example.com/api/workflow-memory/list"); -}); - -test("listMemory throws on a non-ok HTTP response", async () => { - const fetchImpl = (async () => - new Response("nope", { status: 401 })) as unknown as typeof fetch; - - await expect(listMemory({ ...CONFIG, fetchImpl })).rejects.toThrow(/401/); -}); diff --git a/packages/memory-tools/src/client.ts b/packages/memory-tools/src/client.ts deleted file mode 100644 index ca89e4e4ec..0000000000 --- a/packages/memory-tools/src/client.ts +++ /dev/null @@ -1,183 +0,0 @@ -// A minimal client for the sanctioned workflow-memory HTTP surface -// (`@corbits/memory-hub`'s `createWorkflowMemoryRoutes`): search, add, -// and list against the mounted `@corbits/memory` plane. Authenticates -// with the sidecar's own bearer token plus the run's own mailbox -// address (both already reach a workflow-process child's tool env, see -// `apps/sidecar/src/workflow-substrate-factory/step-env.ts`) — never a -// database handle, and never a model-supplied tenant or principal. -import { type } from "arktype"; - -export interface WorkflowMemoryClientConfig { - readonly hubMemoryUrl: string; - readonly sidecarToken: string; - readonly runAddress: string; - /** Override for tests; defaults to the global `fetch`. */ - readonly fetchImpl?: typeof fetch; -} - -/** - * Thrown specifically for the hub's `createUnavailableWorkflowMemoryRoutes` - * response (503, `error.code === "unavailable"`) — the honest "memory - * plane isn't mounted" signal, distinct from a real transport/HTTP - * failure. Callers (`./tool.ts`) use this to degrade calmly instead of - * surfacing a tool error. - */ -export class MemoryUnavailableError extends Error {} - -export type SearchMemoryInput = { - readonly query: string; - readonly limit?: number; - readonly kinds?: readonly string[]; -}; - -export type MemorySearchItem = { - readonly documentId: string; - readonly title: string; - readonly snippet: string; - readonly score: number; - readonly kind: string; -}; - -export type AddMemoryInput = { - readonly title: string; - readonly text: string; - readonly kind?: string; -}; - -export type AddedMemoryEntry = { - readonly documentId: string; - readonly versionId: string; -}; - -export type MemoryTimelineEntry = { - readonly at: string; - readonly title: string; - readonly source: string; -}; - -const MemorySearchResponse = type({ - data: { - items: type({ - documentId: "string", - title: "string", - snippet: "string", - score: "number", - kind: "string", - }).array(), - }, -}); - -const AddedMemoryEntryResponse = type({ - data: { documentId: "string", versionId: "string" }, -}); - -const MemoryTimelineResponse = type({ - data: type({ - at: "string", - title: "string", - source: "string", - }).array(), -}); - -function authHeaders( - config: WorkflowMemoryClientConfig, -): Record { - return { - authorization: `Bearer ${config.sidecarToken}`, - "x-workflow-run-address": config.runAddress, - }; -} - -function endpoint(config: WorkflowMemoryClientConfig, path: string): string { - return `${config.hubMemoryUrl}/api/workflow-memory${path}`; -} - -async function throwForFailedResponse( - response: Response, - action: string, -): Promise { - if (response.status === 503) { - const body: unknown = await response.json().catch(() => null); - const code = (body as { error?: { code?: string } } | null)?.error?.code; - if (code === "unavailable") { - throw new MemoryUnavailableError( - "Memory plane is not configured on this hub", - ); - } - } - throw new Error( - `${action} failed: ${response.status} ${response.statusText}`, - ); -} - -/** Searches the tenant's memory. Throws on any transport, HTTP, or shape failure. */ -export async function searchMemory( - config: WorkflowMemoryClientConfig, - input: SearchMemoryInput, -): Promise { - const doFetch = config.fetchImpl ?? fetch; - const response = await doFetch(endpoint(config, "/search"), { - method: "POST", - headers: { ...authHeaders(config), "content-type": "application/json" }, - body: JSON.stringify(input), - }); - if (!response.ok) { - await throwForFailedResponse(response, "Memory search"); - } - const body: unknown = await response.json(); - const parsed = MemorySearchResponse(body); - if (parsed instanceof type.errors) { - throw new Error( - `Memory search response did not match the expected shape: ${parsed.summary}`, - ); - } - return parsed.data.items; -} - -/** Adds one memory entry. Throws on any transport, HTTP, or shape failure. */ -export async function addMemory( - config: WorkflowMemoryClientConfig, - input: AddMemoryInput, -): Promise { - const doFetch = config.fetchImpl ?? fetch; - const response = await doFetch(endpoint(config, "/add"), { - method: "POST", - headers: { ...authHeaders(config), "content-type": "application/json" }, - body: JSON.stringify(input), - }); - if (!response.ok) { - await throwForFailedResponse(response, "Memory add"); - } - const body: unknown = await response.json(); - const parsed = AddedMemoryEntryResponse(body); - if (parsed instanceof type.errors) { - throw new Error( - `Memory add response did not match the expected shape: ${parsed.summary}`, - ); - } - return parsed.data; -} - -/** Lists the tenant's recent memory timeline. Throws on any transport, HTTP, or shape failure. */ -export async function listMemory( - config: WorkflowMemoryClientConfig, - params: { readonly limit?: number } = {}, -): Promise { - const doFetch = config.fetchImpl ?? fetch; - const query = - params.limit !== undefined ? `?limit=${String(params.limit)}` : ""; - const response = await doFetch(endpoint(config, `/list${query}`), { - headers: authHeaders(config), - }); - if (!response.ok) { - await throwForFailedResponse(response, "Memory list"); - } - const body: unknown = await response.json(); - const parsed = MemoryTimelineResponse(body); - if (parsed instanceof type.errors) { - throw new Error( - `Memory list response did not match the expected shape: ${parsed.summary}`, - ); - } - return parsed.data; -} diff --git a/packages/memory-tools/src/index.ts b/packages/memory-tools/src/index.ts deleted file mode 100644 index 084089feb9..0000000000 --- a/packages/memory-tools/src/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -export { - addMemory, - listMemory, - searchMemory, - MemoryUnavailableError, - type AddedMemoryEntry, - type AddMemoryInput, - type MemorySearchItem, - type MemoryTimelineEntry, - type SearchMemoryInput, - type WorkflowMemoryClientConfig, -} from "./client"; -export { - memoryTools, - MEMORY_ADD_TOOL, - MEMORY_LIST_TOOL, - MEMORY_SEARCH_TOOL, - type WorkflowMemoryEnv, -} from "./tool"; diff --git a/packages/memory-tools/src/tool.test.ts b/packages/memory-tools/src/tool.test.ts deleted file mode 100644 index ce4246de1a..0000000000 --- a/packages/memory-tools/src/tool.test.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { expect, test } from "bun:test"; -import type { ToolCall } from "@intx/types/runtime"; - -import { - MEMORY_ADD_TOOL, - MEMORY_LIST_TOOL, - MEMORY_SEARCH_TOOL, - memoryTools, -} from "./tool"; -import type { WorkflowMemoryEnv } from "./tool"; - -function testEnv(): WorkflowMemoryEnv { - return { - hubMemoryUrl: "https://hub.example.com", - sidecarToken: "sc-token", - address: "run_1@workflow", - } as unknown as WorkflowMemoryEnv; -} - -function callFor(name: string, args: Record = {}): ToolCall { - return { id: "call_1", name, arguments: args }; -} - -test("declares the memory_search, memory_add, and memory_list tools", () => { - const bundle = memoryTools(testEnv()); - expect(bundle.definitions.map((d) => d.name)).toEqual([ - MEMORY_SEARCH_TOOL, - MEMORY_ADD_TOOL, - MEMORY_LIST_TOOL, - ]); -}); - -test("requires the sanctioned workflow-memory env keys, not a per-user credential", () => { - expect(memoryTools.requires).toEqual([ - "hubMemoryUrl", - "sidecarToken", - "address", - ]); -}); - -test("no tool's input schema accepts a tenant or principal argument — attribution is never model-supplied", () => { - const bundle = memoryTools(testEnv()); - for (const definition of bundle.definitions) { - const properties = ( - definition as unknown as { - inputSchema?: { properties?: Record }; - } - ).inputSchema?.properties; - expect(properties?.["tenantId"]).toBeUndefined(); - expect(properties?.["principalId"]).toBeUndefined(); - } -}); - -test("memory_search returns matching items on a successful call", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response( - JSON.stringify({ - data: { - items: [ - { - documentId: "doc_1", - title: "Decision", - snippet: "We decided...", - score: 0.9, - kind: "decision", - }, - ], - }, - }), - )) as unknown as typeof fetch; - try { - const bundle = memoryTools(testEnv()); - const result = await bundle.run( - callFor(MEMORY_SEARCH_TOOL, { query: "what did we decide" }), - new AbortController().signal, - ); - expect(result.isError).toBeFalsy(); - const parsed = JSON.parse(String(result.content)) as { - items: { documentId: string }[]; - }; - expect(parsed.items).toHaveLength(1); - expect(parsed.items[0]?.documentId).toBe("doc_1"); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("memory_search rejects a missing query without calling out", async () => { - const bundle = memoryTools(testEnv()); - const result = await bundle.run( - callFor(MEMORY_SEARCH_TOOL, {}), - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toMatch(/requires a query/); -}); - -test("memory_add returns the created ids on a successful call", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response( - JSON.stringify({ data: { documentId: "doc_1", versionId: "ver_1" } }), - { status: 201 }, - )) as unknown as typeof fetch; - try { - const bundle = memoryTools(testEnv()); - const result = await bundle.run( - callFor(MEMORY_ADD_TOOL, { title: "Note", text: "hello" }), - new AbortController().signal, - ); - expect(result.isError).toBeFalsy(); - expect(JSON.parse(String(result.content))).toEqual({ - documentId: "doc_1", - versionId: "ver_1", - }); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("memory_add rejects missing title or text without calling out", async () => { - const bundle = memoryTools(testEnv()); - const missingTitle = await bundle.run( - callFor(MEMORY_ADD_TOOL, { text: "hello" }), - new AbortController().signal, - ); - expect(missingTitle.isError).toBe(true); - expect(missingTitle.content).toMatch(/requires a title/); - - const missingText = await bundle.run( - callFor(MEMORY_ADD_TOOL, { title: "Note" }), - new AbortController().signal, - ); - expect(missingText.isError).toBe(true); - expect(missingText.content).toMatch(/requires text/); -}); - -test("memory_list returns recent entries on a successful call", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response( - JSON.stringify({ - data: [ - { at: "2026-08-13T00:00:00.000Z", title: "Note", source: "local" }, - ], - }), - )) as unknown as typeof fetch; - try { - const bundle = memoryTools(testEnv()); - const result = await bundle.run( - callFor(MEMORY_LIST_TOOL), - new AbortController().signal, - ); - expect(result.isError).toBeFalsy(); - const parsed = JSON.parse(String(result.content)) as { - entries: { at: string; title: string; source: string }[]; - }; - expect(parsed.entries).toEqual([ - { at: "2026-08-13T00:00:00.000Z", title: "Note", source: "local" }, - ]); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("memory_search degrades calmly when the memory plane isn't mounted, without isError", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response( - JSON.stringify({ - error: { - code: "unavailable", - message: "Memory plane is not configured on this hub", - }, - }), - { status: 503 }, - )) as unknown as typeof fetch; - try { - const bundle = memoryTools(testEnv()); - const result = await bundle.run( - callFor(MEMORY_SEARCH_TOOL, { query: "q" }), - new AbortController().signal, - ); - expect(result.isError).toBe(false); - expect(result.content).toMatch(/isn't set up on this server yet/); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("memory_add degrades calmly when the memory plane isn't mounted, without isError", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response( - JSON.stringify({ - error: { - code: "unavailable", - message: "Memory plane is not configured on this hub", - }, - }), - { status: 503 }, - )) as unknown as typeof fetch; - try { - const bundle = memoryTools(testEnv()); - const result = await bundle.run( - callFor(MEMORY_ADD_TOOL, { title: "Note", text: "hello" }), - new AbortController().signal, - ); - expect(result.isError).toBe(false); - expect(result.content).toMatch(/isn't set up on this server yet/); - expect(result.content).toMatch(/not saved/); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("memory_list degrades calmly when the memory plane isn't mounted, without isError", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => - new Response( - JSON.stringify({ - error: { - code: "unavailable", - message: "Memory plane is not configured on this hub", - }, - }), - { status: 503 }, - )) as unknown as typeof fetch; - try { - const bundle = memoryTools(testEnv()); - const result = await bundle.run( - callFor(MEMORY_LIST_TOOL), - new AbortController().signal, - ); - expect(result.isError).toBe(false); - expect(result.content).toMatch(/isn't set up on this server yet/); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("returns an honest error result on an unreachable hub, never fabricating a memory result", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async () => { - throw new Error("fetch failed: connection refused"); - }) as unknown as typeof fetch; - try { - const bundle = memoryTools(testEnv()); - const result = await bundle.run( - callFor(MEMORY_SEARCH_TOOL, { query: "q" }), - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toMatch(/connection refused/); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test("an unknown tool name returns an honest error, never a silent no-op", async () => { - const bundle = memoryTools(testEnv()); - const result = await bundle.run( - callFor("memory_delete_everything"), - new AbortController().signal, - ); - expect(result.isError).toBe(true); - expect(result.content).toMatch(/unknown tool/); -}); diff --git a/packages/memory-tools/src/tool.ts b/packages/memory-tools/src/tool.ts deleted file mode 100644 index 5b115a69ed..0000000000 --- a/packages/memory-tools/src/tool.ts +++ /dev/null @@ -1,244 +0,0 @@ -// The `@corbits/memory-tools` bundle: `memory_search`, `memory_add`, and -// `memory_list`, the agent-facing surface a workflow definition pins to -// reach the tenant's firm-memory plane. Calls `./client.ts` against the -// sanctioned workflow-memory HTTP surface (`@corbits/memory-hub`'s -// `createWorkflowMemoryRoutes`), authenticating with the sidecar's own -// bearer token and the run's own mailbox address — both already reach a -// workflow-process child's tool env (`apps/sidecar/src/workflow-substrate-factory/step-env.ts`), -// so this bundle needs no per-user credential and never touches a -// database handle. -// -// Identity is never a model-supplied argument: none of the three tools' -// input schemas below carry a tenant or principal field, and none of -// `./client.ts`'s request bodies do either — attribution comes entirely -// from the run's own authenticated address on the hub side -// (`@corbits/memory-hub`'s `WorkflowRunAuthenticator`), matching -// `@corbits/artifact-tools`' precedent. A transport, HTTP, or shape -// failure comes back as a completed `ToolResult` with `isError: true` — -// never fabricate a memory or a search result when a call fails. -import { defineTool } from "@intx/agent"; -import type { BaseEnv } from "@intx/agent"; -import type { ToolCall, ToolResult } from "@intx/types/runtime"; - -import { - addMemory, - listMemory, - MemoryUnavailableError, - searchMemory, -} from "./client"; - -const MEMORY_NOT_SET_UP_MESSAGE = - "Memory isn't set up on this server yet — proceeding without it."; - -export const MEMORY_SEARCH_TOOL = "memory_search"; -export const MEMORY_ADD_TOOL = "memory_add"; -export const MEMORY_LIST_TOOL = "memory_list"; - -/** Env this bundle needs beyond `BaseEnv`: the run's hub-reach credential. */ -export interface WorkflowMemoryEnv extends BaseEnv { - readonly hubMemoryUrl: string; - readonly sidecarToken: string; - readonly address: string; -} - -function errorResult(callId: string, err: unknown): ToolResult { - return { - callId, - isError: true, - content: err instanceof Error ? err.message : String(err), - }; -} - -function clientConfig(env: WorkflowMemoryEnv) { - return { - hubMemoryUrl: env.hubMemoryUrl, - sidecarToken: env.sidecarToken, - runAddress: env.address, - }; -} - -async function runMemorySearch( - env: WorkflowMemoryEnv, - call: ToolCall, -): Promise { - const query = call.arguments["query"]; - if (typeof query !== "string" || query === "") { - return errorResult(call.id, new Error("memory_search requires a query")); - } - const limitArg = call.arguments["limit"]; - const limit = typeof limitArg === "number" ? limitArg : undefined; - try { - const searchInput = limit !== undefined ? { query, limit } : { query }; - const items = await searchMemory(clientConfig(env), searchInput); - return { - callId: call.id, - isError: false, - content: JSON.stringify({ items }), - }; - } catch (err) { - if (err instanceof MemoryUnavailableError) { - return { - callId: call.id, - isError: false, - content: JSON.stringify({ items: [], note: MEMORY_NOT_SET_UP_MESSAGE }), - }; - } - return errorResult(call.id, err); - } -} - -async function runMemoryAdd( - env: WorkflowMemoryEnv, - call: ToolCall, -): Promise { - const title = call.arguments["title"]; - const text = call.arguments["text"]; - if (typeof title !== "string" || title === "") { - return errorResult(call.id, new Error("memory_add requires a title")); - } - if (typeof text !== "string" || text === "") { - return errorResult(call.id, new Error("memory_add requires text")); - } - const kindArg = call.arguments["kind"]; - const kind = typeof kindArg === "string" ? kindArg : undefined; - try { - const addInput = - kind !== undefined ? { title, text, kind } : { title, text }; - const added = await addMemory(clientConfig(env), addInput); - return { callId: call.id, isError: false, content: JSON.stringify(added) }; - } catch (err) { - if (err instanceof MemoryUnavailableError) { - return { - callId: call.id, - isError: false, - content: JSON.stringify({ - saved: false, - note: `${MEMORY_NOT_SET_UP_MESSAGE} This entry was not saved.`, - }), - }; - } - return errorResult(call.id, err); - } -} - -async function runMemoryList( - env: WorkflowMemoryEnv, - call: ToolCall, -): Promise { - const limitArg = call.arguments["limit"]; - const limit = typeof limitArg === "number" ? limitArg : undefined; - try { - const entries = await listMemory( - clientConfig(env), - limit !== undefined ? { limit } : {}, - ); - return { - callId: call.id, - isError: false, - content: JSON.stringify({ entries }), - }; - } catch (err) { - if (err instanceof MemoryUnavailableError) { - return { - callId: call.id, - isError: false, - content: JSON.stringify({ - entries: [], - note: MEMORY_NOT_SET_UP_MESSAGE, - }), - }; - } - return errorResult(call.id, err); - } -} - -/** - * The `@corbits/memory-tools` bundle factory: three tools, three env keys - * — the sanctioned CL-5852 path, mirroring `@corbits/artifact-tools`' - * one-bundle shape. - */ -export const memoryTools = defineTool({ - id: "@corbits/memory-tools/memory", - requires: ["hubMemoryUrl", "sidecarToken", "address"], - definitions: [ - { name: MEMORY_SEARCH_TOOL }, - { name: MEMORY_ADD_TOOL }, - { name: MEMORY_LIST_TOOL }, - ], - factory: (env) => ({ - definitions: [ - { - name: MEMORY_SEARCH_TOOL, - description: - "Searches the tenant's firm memory (facts, decisions, and " + - "notes recorded over time) for entries relevant to a query. " + - "Returns an error result naming the failure when the memory " + - "plane is unreachable — never fabricate a search result when " + - "this happens.", - inputSchema: { - type: "object", - properties: { - query: { type: "string", description: "What to search for." }, - limit: { - type: "number", - description: "Maximum number of results to return.", - }, - }, - required: ["query"], - }, - }, - { - name: MEMORY_ADD_TOOL, - description: - "Records one entry into the tenant's firm memory — a fact, " + - "decision, or note worth recalling later. Attribution (who " + - "and which tenant) comes from the run itself, never from " + - "these arguments.", - inputSchema: { - type: "object", - properties: { - title: { type: "string", description: "A short title." }, - text: { type: "string", description: "The entry's full text." }, - kind: { - type: "string", - description: 'Optional entry kind (e.g. "decision", "fact").', - }, - }, - required: ["title", "text"], - }, - }, - { - name: MEMORY_LIST_TOOL, - description: - "Lists the tenant's most recent firm-memory entries as a " + - "timeline, most recent first.", - inputSchema: { - type: "object", - properties: { - limit: { - type: "number", - description: "Maximum number of entries to return.", - }, - }, - }, - }, - ], - run: (call: ToolCall, _signal: AbortSignal) => { - switch (call.name) { - case MEMORY_SEARCH_TOOL: - return runMemorySearch(env, call); - case MEMORY_ADD_TOOL: - return runMemoryAdd(env, call); - case MEMORY_LIST_TOOL: - return runMemoryList(env, call); - default: - return Promise.resolve( - errorResult( - call.id, - new Error(`@corbits/memory-tools: unknown tool "${call.name}"`), - ), - ); - } - }, - }), -}); diff --git a/packages/memory-tools/tsconfig.json b/packages/memory-tools/tsconfig.json deleted file mode 100644 index e125e90614..0000000000 --- a/packages/memory-tools/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "./tsconfig.src.json", - "compilerOptions": { - "composite": false, - "noEmit": true, - "disableSourceOfProjectReferenceRedirect": true, - "declaration": false, - "declarationMap": false, - "emitDeclarationOnly": false, - "tsBuildInfoFile": "tsconfig.tsbuildinfo", - "rootDir": "../.." - }, - "include": ["src"], - "exclude": [] -} diff --git a/packages/memory-tools/tsconfig.src.json b/packages/memory-tools/tsconfig.src.json deleted file mode 100644 index fc8724305f..0000000000 --- a/packages/memory-tools/tsconfig.src.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["src", "package.json", "src/**/*.json"], - "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], - "compilerOptions": { - "types": ["bun"], - "composite": true, - "emitDeclarationOnly": true, - "outDir": "dist", - "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - } -} diff --git a/packages/onboarding/src/tenant-seed.ts b/packages/onboarding/src/tenant-seed.ts index 8a2c19b697..ecf825399b 100644 --- a/packages/onboarding/src/tenant-seed.ts +++ b/packages/onboarding/src/tenant-seed.ts @@ -541,6 +541,15 @@ export const SEED_GRANTS: readonly { resource: string; action: string }[] = [ // vocabulary. { resource: "asset:*", action: "create" }, { resource: "asset:*", action: "write" }, + // Firm memory (CL-8186): `@corbits/memory`'s mounted routes gate on + // `requireGrant("memory", action)` (see its `routes/deps.ts` + // `grantGuard`) — "add" for `POST .../memory/add`, "search" for both + // `POST .../memory/search` and `GET .../memory/list` (list reuses the + // search grant; there is no separate "list" action). Grants never + // inherit across tenants, so every workflow-run principal needs its + // own pair, exactly like every other tenant-scoped resource above. + { resource: "memory", action: "add" }, + { resource: "memory", action: "search" }, ]; // Grants planted ONLY on Myra's own run principal (see diff --git a/packages/onboarding/test/connect-deploys-nothing.test.ts b/packages/onboarding/test/connect-deploys-nothing.test.ts index 70427f7217..f6fbb83417 100644 --- a/packages/onboarding/test/connect-deploys-nothing.test.ts +++ b/packages/onboarding/test/connect-deploys-nothing.test.ts @@ -112,7 +112,7 @@ function fakeHub( hub.get("/api/tenants/:id/assets/ast_registry/tarballs", (c) => c.json([ { - filename: "corbits-memory-tools-0.0.4.tgz", + filename: "corbits-capability-tools-0.0.6.tgz", size: 1, integrity: "sha512-x", }, diff --git a/packages/onboarding/test/desired-state-reconcile.test.ts b/packages/onboarding/test/desired-state-reconcile.test.ts index 19ce29c43e..f6eab0630c 100644 --- a/packages/onboarding/test/desired-state-reconcile.test.ts +++ b/packages/onboarding/test/desired-state-reconcile.test.ts @@ -96,7 +96,7 @@ function harness(state: Stub) { data: state.registryTarballs ? [ { - filename: "corbits-memory-tools-0.0.4.tgz", + filename: "corbits-capability-tools-0.0.6.tgz", size: 1, integrity: "sha512-x", }, @@ -331,8 +331,8 @@ describe("reconcileTenantDesiredState", () => { cookies: [], hubUrl: "https://hub.example.com", tenantId: TENANT_ID, - name: "@corbits/memory-tools", - version: "0.0.4", + name: "@corbits/capability-tools", + version: "0.0.6", fetchSource: async () => bytes, fetchImpl: async (input) => { putUrls.push(String(input)); @@ -343,7 +343,7 @@ describe("reconcileTenantDesiredState", () => { expect(outcome).toBe("installed"); expect(putUrls.length).toBe(1); expect(putUrls[0]).toContain( - `/api/tenants/${TENANT_ID}/assets/${REGISTRY_ASSET.id}/tarballs/corbits-memory-tools-0.0.4.tgz`, + `/api/tenants/${TENANT_ID}/assets/${REGISTRY_ASSET.id}/tarballs/corbits-capability-tools-0.0.6.tgz`, ); // And a republished same name@version is skipped (immutable). The @@ -354,8 +354,8 @@ describe("reconcileTenantDesiredState", () => { cookies: [], hubUrl: "https://hub.example.com", tenantId: TENANT_ID, - name: "@corbits/memory-tools", - version: "0.0.4", + name: "@corbits/capability-tools", + version: "0.0.6", fetchSource: async () => bytes, fetchImpl: async () => { throw new Error("must not PUT over an existing name@version"); diff --git a/packages/onboarding/test/desired-state.test.ts b/packages/onboarding/test/desired-state.test.ts index 44c1f84f4a..30a0cc6efd 100644 --- a/packages/onboarding/test/desired-state.test.ts +++ b/packages/onboarding/test/desired-state.test.ts @@ -92,7 +92,7 @@ function stubApi(state: StubState): ApiCall & { calls: [string, string][] } { data: state.registryTarballs ? [ { - filename: "corbits-memory-tools-0.0.4.tgz", + filename: "corbits-capability-tools-0.0.6.tgz", size: 1, integrity: "sha512-x", }, diff --git a/packages/onboarding/test/routes.test.ts b/packages/onboarding/test/routes.test.ts index 6634b831bd..2db6289204 100644 --- a/packages/onboarding/test/routes.test.ts +++ b/packages/onboarding/test/routes.test.ts @@ -543,8 +543,8 @@ describe("POST /complete", () => { testAndPersistCredentialFn: async () => { throw new HubApiError( "publishing the corbits-tools package-registry asset failed: " + - "tool-package freshness: @corbits/memory-tools@1.2.0 changed " + - "src/ without bumping version.\n /Users/alice/abklabs/workbench/packages/memory-tools", + "tool-package freshness: @corbits/capability-tools@1.2.0 changed " + + "src/ without bumping version.\n /Users/alice/abklabs/workbench/packages/capability-tools", "check the hub logs for the underlying failure, then re-run: workbench seed", ); }, @@ -572,7 +572,9 @@ describe("POST /complete", () => { lines.some( (line) => line.includes(body.error.refId) && - line.includes("/Users/alice/abklabs/workbench/packages/memory-tools"), + line.includes( + "/Users/alice/abklabs/workbench/packages/capability-tools", + ), ), ).toBe(true); }); @@ -746,7 +748,7 @@ describe("CL-7584 desired-state kicks and steps", () => { state.seeded ? [ { - filename: "corbits-memory-tools-0.0.4.tgz", + filename: "corbits-capability-tools-0.0.6.tgz", size: 1, integrity: "sha512-x", }, diff --git a/packages/scout-agent/package.json b/packages/scout-agent/package.json index 81f6f446ef..97d380a0f5 100644 --- a/packages/scout-agent/package.json +++ b/packages/scout-agent/package.json @@ -2,7 +2,7 @@ "name": "@corbits/scout-agent", "private": true, "description": "Scout, the research/due-diligence agent: a ported system prompt plus its firm-memory, web-research, and Library-artifact tool declarations for a workbench chat agent", - "version": "0.0.2", + "version": "0.0.3", "license": "LGPL-2.1-or-later", "type": "module", "exports": { @@ -19,6 +19,7 @@ "arktype": "catalog:" }, "devDependencies": { + "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", "@corbits/web-search-tools": "workspace:*", "@types/bun": "catalog:", "typescript": "catalog:" diff --git a/packages/scout-agent/src/definition.ts b/packages/scout-agent/src/definition.ts index 0d4165ce79..22fc8627d7 100644 --- a/packages/scout-agent/src/definition.ts +++ b/packages/scout-agent/src/definition.ts @@ -9,12 +9,14 @@ // Tools are never inlined here, matching every workflow definition in // this catalog (see `workflows/attio-task-agent/src/index.ts`'s header // comment): they arrive as pinned packages at deploy time, keeping this -// definition pure data. Two of the three pins are existing, already-wired -// workbench tool packages reused as-is — `@corbits/memory-tools` -// (knowledge-search/memory-add/memory-list) and `@corbits/web-search-tools` -// (web-research, Exa-backed, credential handle `"exa"`, satisfied by the -// keyless Exa MCP preset in `packages/connections/src/mcp-presets.ts`). -// The third, `@corbits/scout-agent` itself, supplies `./artifact-tool.ts`. +// definition pure data. `@corbits/memory` (CL-8186) supplies firm +// memory (memory_search/memory_add/memory_list) directly — it is not a +// workbench-built tool package but a published dependency that declares +// its own `interchange.tools` surface, resolved the same way as any +// other pin. `@corbits/web-search-tools` gives web-research (Exa-backed, +// credential handle `"exa"`, satisfied by the keyless Exa MCP preset in +// `packages/connections/src/mcp-presets.ts`). The third, +// `@corbits/scout-agent` itself, supplies `./artifact-tool.ts`. // // `launch-diligence-brief` and `launch-fact-check` are not pinned here — // see `./system-prompt.ts`'s header comment and this package's README for @@ -30,9 +32,9 @@ export const SCOUT_AGENT_DESCRIPTION = "saves and recalls Library write-ups, and answers with sources."; export const SCOUT_TOOL_PACKAGE_PINS: readonly ToolPackagePin[] = [ - { name: "@corbits/memory-tools", version: "0.0.4" }, + { name: "@corbits/memory", version: "0.1.2" }, { name: "@corbits/web-search-tools", version: "0.0.3" }, - { name: "@corbits/scout-agent", version: "0.0.2" }, + { name: "@corbits/scout-agent", version: "0.0.3" }, ]; /** The plain-data shape the agent-directory create path takes. */ diff --git a/packages/tool-registry-publish/src/publish.test.ts b/packages/tool-registry-publish/src/publish.test.ts index a5a29ef09d..40fee0792e 100644 --- a/packages/tool-registry-publish/src/publish.test.ts +++ b/packages/tool-registry-publish/src/publish.test.ts @@ -41,9 +41,9 @@ describe("tarballsCoverRequiredSeedPackages", () => { expect(tarballsCoverRequiredSeedPackages([])).toBe(false); }); - test("covers @corbits/memory-tools at any version", () => { + test("covers @corbits/capability-tools at any version", () => { expect( - tarballsCoverRequiredSeedPackages(["corbits-memory-tools-0.0.4.tgz"]), + tarballsCoverRequiredSeedPackages(["corbits-capability-tools-0.0.6.tgz"]), ).toBe(true); }); @@ -89,13 +89,13 @@ describe("publishCorbitsToolsRegistry", () => { "uploaded none", ); expect((cause as EmptyRegistryPublishError).message).toContain( - "still missing @corbits/memory-tools", + "still missing @corbits/capability-tools", ); } expect(createdAssets).toBe(0); }); - test("success is false when uploads land but memory-tools is still missing", async () => { + test("success is false when uploads land but capability-tools is still missing", async () => { const api: ApiCall = async (method, path) => { if ( method === "GET" && @@ -153,7 +153,7 @@ describe("publishCorbitsToolsRegistry", () => { "uploaded corbits-other-tools-1.0.0.tgz", ); expect((cause as EmptyRegistryPublishError).message).toContain( - "still missing @corbits/memory-tools", + "still missing @corbits/capability-tools", ); expect((cause as EmptyRegistryPublishError).message).not.toContain( "uploaded none", diff --git a/packages/tool-registry-publish/src/publish.ts b/packages/tool-registry-publish/src/publish.ts index 8fc4d6d879..8aa84a02b2 100644 --- a/packages/tool-registry-publish/src/publish.ts +++ b/packages/tool-registry-publish/src/publish.ts @@ -146,7 +146,7 @@ const TarballListResponse = TarballSummary.array(); /** * Thrown when a publish run leaves `corbits-tools` without the required - * seed tarballs (`@corbits/memory-tools`). Covers both zero uploads onto + * seed tarballs (`@corbits/capability-tools`). Covers both zero uploads onto * an empty/dangling registry (`GET tarballs` → `[]`) and a partial run * that uploaded other packages but still cannot resolve first-launch * pins. Callers must not treat this as success. diff --git a/packages/tool-registry-publish/src/registry.ts b/packages/tool-registry-publish/src/registry.ts index e8b1cbc925..641e83b00e 100644 --- a/packages/tool-registry-publish/src/registry.ts +++ b/packages/tool-registry-publish/src/registry.ts @@ -16,11 +16,17 @@ export const CORBITS_TOOLS_REGISTRY = "corbits-tools"; /** * Package names a seeded `corbits-tools` registry must carry for the * default workflow set to launch. Default workflows pin at least these - * (Myra's assistant pins `@corbits/memory-tools`); an empty or dangling - * registry — a package-registry row whose git repo has no tarball - * commits — is not seeded. + * (Myra's assistant pins `@corbits/capability-tools` into every drafted + * agent's default tool-package set); an empty or dangling registry — a + * package-registry row whose git repo has no tarball commits — is not + * seeded. `@corbits/memory` (CL-8186) is not listed here: it is a + * published dependency this repo does not build or publish into this + * registry itself, not one of the workspace `packages/*-tools` this + * publisher packs. */ -export const REQUIRED_SEED_TOOL_PACKAGES = ["@corbits/memory-tools"] as const; +export const REQUIRED_SEED_TOOL_PACKAGES = [ + "@corbits/capability-tools", +] as const; /** True when `filename` is an npm-style tarball for `packageName` (any version). */ export function tarballCoversPackage( @@ -57,7 +63,6 @@ export function tarballsCoverRequiredSeedPackages( * README for how its request_capability tool reaches the hub. */ export const CORBITS_TOOL_PACKAGE_DIRS: readonly string[] = [ - new URL("../../memory-tools", import.meta.url).pathname, new URL("../../capability-tools", import.meta.url).pathname, new URL("../../connections-tools", import.meta.url).pathname, new URL("../../catalog-tools", import.meta.url).pathname, diff --git a/packages/tool-registry-publish/src/surface-reader.test.ts b/packages/tool-registry-publish/src/surface-reader.test.ts index 1b42cd4a53..25306276c2 100644 --- a/packages/tool-registry-publish/src/surface-reader.test.ts +++ b/packages/tool-registry-publish/src/surface-reader.test.ts @@ -41,7 +41,7 @@ function memorySource(files: Map) { describe("readToolSurfaceManifests", () => { test("round-trips a packed tarball into a validated manifest", async () => { const tarball = await packToolPackageTarball( - new URL("../../memory-tools", import.meta.url).pathname, + new URL("../../capability-tools", import.meta.url).pathname, ); const source = memorySource( new Map([[`tarballs/${tarball.filename}`, tarball.bytes]]), @@ -51,9 +51,9 @@ describe("readToolSurfaceManifests", () => { rootDir: "tarballs", }); expect(manifests).toHaveLength(1); - expect(manifests[0]?.name).toBe("@corbits/memory-tools"); + expect(manifests[0]?.name).toBe("@corbits/capability-tools"); expect(manifests[0]?.surface.map((entry) => entry.qualifiedId)).toContain( - "@corbits/memory-tools/memory:memory_add", + "@corbits/capability-tools/cap:request_capability", ); expect(manifests[0]?.surface.every((entry) => entry.kind === "tool")).toBe( true, @@ -71,7 +71,7 @@ describe("readToolSurfaceManifests", () => { test("skips a tarball whose package.json is not a valid manifest", async () => { const tarball = await packToolPackageTarball( - new URL("../../memory-tools", import.meta.url).pathname, + new URL("../../capability-tools", import.meta.url).pathname, ); // Corrupt the packaged manifest by re-packing a tampered file is // overkill; a truncated tarball is enough to prove the reader skips @@ -87,7 +87,7 @@ describe("readToolSurfaceManifests", () => { test("returns the valid manifests and skips the manifest-less legacy ones", async () => { const tarball = await packToolPackageTarball( - new URL("../../memory-tools", import.meta.url).pathname, + new URL("../../capability-tools", import.meta.url).pathname, ); const source = memorySource( new Map([ @@ -100,6 +100,6 @@ describe("readToolSurfaceManifests", () => { rootDir: "tarballs", }); expect(manifests).toHaveLength(1); - expect(manifests[0]?.name).toBe("@corbits/memory-tools"); + expect(manifests[0]?.name).toBe("@corbits/capability-tools"); }); }); diff --git a/scripts/checks/report-error-baseline.txt b/scripts/checks/report-error-baseline.txt index 375a002fd6..03fa8b17c9 100644 --- a/scripts/checks/report-error-baseline.txt +++ b/scripts/checks/report-error-baseline.txt @@ -161,11 +161,6 @@ packages/mcp-tools/src/tool.ts 1 return { packages/mcp-tools/src/tool.ts 2 return errorResult(call.id, err); packages/mcp-tools/src/tool.ts 3 return errorResult(call.id, err); packages/mcp-tools/src/tool.ts 4 return errorResult(call.id, err); -packages/memory-hub/src/workflow-routes.ts 1 return c.json( -packages/memory-hub/src/workflow-routes.ts 2 return c.json( -packages/memory-tools/src/tool.ts 1 if (err instanceof MemoryUnavailableError) { -packages/memory-tools/src/tool.ts 2 if (err instanceof MemoryUnavailableError) { -packages/memory-tools/src/tool.ts 3 if (err instanceof MemoryUnavailableError) { packages/mocks/src/ollama/capture.ts 1 return raw; packages/ollama-adapter/src/inline-tool-json.ts 1 return false; packages/ollama-adapter/src/inline-tool-json.ts 1 return { kind: "incomplete" }; diff --git a/scripts/checks/tool-package-pins.ts b/scripts/checks/tool-package-pins.ts index f52c4be4ce..4dd565bf8b 100644 --- a/scripts/checks/tool-package-pins.ts +++ b/scripts/checks/tool-package-pins.ts @@ -118,9 +118,78 @@ async function manifestVersions(root: string): Promise> { } versions.set(manifest.name, manifest.version); } + await addVendoredDependencyVersions(root, versions); return versions; } +/** + * A pin may also name a `@corbits/*` package that is a real (non- + * `workspace:*`) dependency of some workspace manifest rather than a + * package this repo itself builds and publishes — `@corbits/memory` + * (CL-8186), pinned as a git dependency in `apps/hub`, is the first of + * these: it declares its own `interchange.tools` surface and resolves + * through the same tool-package registry as any workspace-built tool + * package, so a pin naming it is not unresolvable. Its authoritative + * version is whatever actually got installed, read from the installed + * copy's own `package.json` — never the dependency range string itself + * (a `github:...#sha` range names no semver). + */ +async function addVendoredDependencyVersions( + root: string, + versions: Map, +): Promise { + const glob = new Glob("{apps,packages,tools,workflows}/*/package.json"); + const corbitsDepNames = new Set(); + for await (const relPath of glob.scan(root)) { + if (relPath.includes("node_modules/")) continue; + const manifest = (await Bun.file(path.join(root, relPath)).json()) as { + dependencies?: Record; + peerDependencies?: Record; + }; + for (const field of ["dependencies", "peerDependencies"] as const) { + for (const [name, range] of Object.entries(manifest[field] ?? {})) { + if (!name.startsWith("@corbits/")) continue; + if (range.startsWith("workspace:")) continue; + if (versions.has(name)) continue; + corbitsDepNames.add(name); + } + } + } + for (const name of corbitsDepNames) { + const version = await installedVersion(root, name); + if (version !== undefined) { + versions.set(name, version); + } + } +} + +/** + * Reads a `@corbits/*` package's actually-installed version. Bun hoists a + * git/tarball dependency into root `node_modules/.bun//…` + * and symlinks it into each consuming workspace package's own + * `node_modules/@corbits/` — never into the repo-root + * `node_modules/@corbits/` unless the root manifest itself depends + * on it — so this checks every workspace's own `node_modules` first, then + * falls back to the root's. + */ +async function installedVersion( + root: string, + name: string, +): Promise { + const candidateDirs = [".", "apps/*", "packages/*", "tools/*", "workflows/*"]; + for (const dir of candidateDirs) { + const glob = new Glob(`${dir}/node_modules/${name}/package.json`); + for await (const relPath of glob.scan(root)) { + const manifest = await Bun.file(path.join(root, relPath)) + .json() + .catch(() => undefined); + const version = (manifest as { version?: string } | undefined)?.version; + if (version !== undefined) return version; + } + } + return undefined; +} + function isExcludedPath(relPath: string): boolean { return EXCLUDED_SEGMENTS.some( (segment) => diff --git a/tsconfig.build.json b/tsconfig.build.json index 7b65915287..09c7589e15 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -106,12 +106,6 @@ { "path": "./packages/mcp-tools/tsconfig.src.json" }, - { - "path": "./packages/memory-hub/tsconfig.src.json" - }, - { - "path": "./packages/memory-tools/tsconfig.src.json" - }, { "path": "./packages/migration-runner/tsconfig.src.json" }, diff --git a/workflows/assistant/package.json b/workflows/assistant/package.json index 11d4504770..7e61d8f7fa 100644 --- a/workflows/assistant/package.json +++ b/workflows/assistant/package.json @@ -36,7 +36,8 @@ "test": "bun test" }, "dependencies": { - "@corbits/catalog-tools": "0.1.0" + "@corbits/catalog-tools": "0.1.0", + "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3" }, "peerDependencies": { "@intx/agent": "0.3.0", diff --git a/workflows/assistant/src/index.ts b/workflows/assistant/src/index.ts index a8527b574c..0a8a907477 100644 --- a/workflows/assistant/src/index.ts +++ b/workflows/assistant/src/index.ts @@ -14,12 +14,15 @@ // config — it is vendored, read-only source for this change — so the // agent below is built directly against `AgentDefinition`'s own type, // which already carries the field, matching -// `workflows/collateral-generation`'s precedent. `@corbits/memory-tools` -// is pinned so this deployment can search, add, and list the tenant's -// firm memory (`memory_search`/`memory_add`/`memory_list`); whether the -// pin *resolves* at deploy time still depends on an operator publishing -// it to a registry the host's tool-package resolver can reach (see -// `apps/hub/src/index.ts`'s `toolPackageRegistries` wiring). +// `workflows/collateral-generation`'s precedent. `@corbits/memory` +// (CL-8186) is pinned so this deployment can search, add, and list the +// tenant's firm memory (`memory_search`/`memory_add`/`memory_list`) — a +// published dependency, not a workbench-built tool package, resolved +// through its own `interchange.tools` surface the same way any other +// pin is; whether the pin *resolves* at deploy time still depends on an +// operator publishing it to a registry the host's tool-package resolver +// can reach (see `apps/hub/src/index.ts`'s `toolPackageRegistries` +// wiring). import type { AgentDefinition, InferencePreference } from "@intx/agent"; import { defineWorkflow, step } from "@intx/workflow"; diff --git a/workflows/assistant/src/prompt.ts b/workflows/assistant/src/prompt.ts index 81ae926b1a..fedb4f0d03 100644 --- a/workflows/assistant/src/prompt.ts +++ b/workflows/assistant/src/prompt.ts @@ -13,12 +13,17 @@ export const ASSISTANT_WORKFLOW_ID = "wf_assistant"; export const ASSISTANT_STEP_ID = "assistant"; /** - * The tool packages this deployment pins. `@corbits/memory-tools` - * is the original pin; `@corbits/capability-tools` lets Myra self-service - * a missing tool, skill, or model; the manager-tools bundles give Myra - * real workbench-management capability — a specialist agent she can - * create (each gets their own chat), connection visibility, and skill - * capture — each a thin wrapper over an existing platform primitive (see each package's own file-header comment for + * The tool packages this deployment pins. Firm memory + * (`memory_add`/`memory_search`/`memory_list`) is no longer one of + * these: `@corbits/memory` is a normal published dependency, not a + * workspace tool package the pin/registry resolver can name, so its + * tool factories are attached directly on `toolFactories` in + * `./index.ts` instead (CL-8186). `@corbits/capability-tools` lets Myra + * self-service a missing tool, skill, or model; the manager-tools + * bundles give Myra real workbench-management capability — a specialist + * agent she can create (each gets their own chat), connection + * visibility, and skill capture — each a thin wrapper over an existing + * platform primitive (see each package's own file-header comment for * which one). `@corbits/mcp-tools` and `@corbits/interaction-tools` * expose tenant-connected MCP servers and the ask-user card; * `@corbits/manus-tools` is pinned so Manus tools exist when the tenant @@ -30,7 +35,7 @@ export const ASSISTANT_STEP_ID = "assistant"; * access. */ export const ASSISTANT_TOOL_PACKAGE_PINS: readonly ToolPackagePin[] = [ - { name: "@corbits/memory-tools", version: "0.0.4" }, + { name: "@corbits/memory", version: "0.1.2" }, { name: "@corbits/capability-tools", version: "0.0.6" }, { name: "@corbits/agent-directory-tools", version: "0.0.6" }, { name: "@corbits/connections-tools", version: "0.0.11" }, diff --git a/workflows/diligence-brief/package.json b/workflows/diligence-brief/package.json index b915406e74..92e10de126 100644 --- a/workflows/diligence-brief/package.json +++ b/workflows/diligence-brief/package.json @@ -35,6 +35,7 @@ "test": "bun test" }, "dependencies": { + "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", "arktype": "catalog:" }, "peerDependencies": { diff --git a/workflows/diligence-brief/src/index.ts b/workflows/diligence-brief/src/index.ts index b4ad88681c..ddd6c62d23 100644 --- a/workflows/diligence-brief/src/index.ts +++ b/workflows/diligence-brief/src/index.ts @@ -102,19 +102,21 @@ export const DILIGENCE_BRIEF_WIRED_SOURCES = [ /** * Tool packages this definition pins (CL-5999 shape, matching * `code-review`/`collateral-generation`/`assistant`'s precedent): - * `@corbits/web-search-tools` for live research, `@corbits/memory-tools` - * so a brief can check (and later inform) the tenant's firm memory. + * `@corbits/web-search-tools` for live research, `@corbits/memory` + * (CL-8186) so a brief can check (and later inform) the tenant's firm + * memory — a published dependency, not a workbench-built tool package, + * resolved the same way through its own `interchange.tools` surface. */ export const DILIGENCE_BRIEF_TOOL_PACKAGE_PINS: readonly ToolPackagePin[] = [ { name: "@corbits/web-search-tools", version: "0.0.3" }, - { name: "@corbits/memory-tools", version: "0.0.4" }, + { name: "@corbits/memory", version: "0.1.2" }, ]; /** * Binds the pinned web-search package's "exa" handle to the tenant's - * connection — `@corbits/memory-tools` needs no credential binding of - * its own; its env keys (`hubMemoryUrl`/`sidecarToken`/`address`) are - * populated for every workflow step directly, the same as + * connection — `@corbits/memory` needs no credential binding of its + * own; its env keys (`memoryBaseUrl`/`memoryTenantId`/`memoryAuthToken`) + * are populated for every workflow step directly, the same as * `@corbits/artifact-tools`' trio. */ export const DILIGENCE_BRIEF_CREDENTIAL_BINDINGS: readonly CredentialBinding[] =