From 00f1571fb7a191cfc5aac07bf62e56e5c09add01 Mon Sep 17 00:00:00 2001 From: Nestor Canales Date: Thu, 16 Jul 2026 11:57:16 -0400 Subject: [PATCH] =?UTF-8?q?feat(embeddings):=20Phase=205=20foundation=20?= =?UTF-8?q?=E2=80=94=20per-platform=20go:embed=20split?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements epic Open Concern #8: one binary can embed only one platform's ONNX Runtime library, so the ORT go:embed moves out of the shared assets_embed.go into per-platform assets_embed__.go files (darwin-arm64 first; each defines embeddedORTLib + ortLibFile). Model and tokenizer stay in the shared embed — identical bytes on every platform. Each later Phase 5 platform lands as one sibling file + manifest entries. Also, prerequisites for building on Linux at all: - Makefile: portable SHA256 var (sha256sum on Linux, shasum -a 256 on mac) - dylibCandidates: add the versioned libonnxruntime.so.1.26.0 the official Linux tarball actually ships - tripwire test: ortLibFile must be a name findDylib recognizes No behavior change on darwin-arm64: untagged tests green, tagged integration test passes (Phase 0 distance ordering reproduced), packaged app re-extracts embedded assets and answers an MCP search. Co-Authored-By: Claude Fable 5 --- Documentation/Epics/local-embeddings.md | 17 ++++++++++- Makefile | 9 ++++-- internal/embeddings/local/assets.go | 6 +++- internal/embeddings/local/assets_embed.go | 29 ++++++++++++------- .../local/assets_embed_darwin_arm64.go | 18 ++++++++++++ internal/embeddings/local/integration_test.go | 12 ++++++++ 6 files changed, 76 insertions(+), 15 deletions(-) create mode 100644 internal/embeddings/local/assets_embed_darwin_arm64.go diff --git a/Documentation/Epics/local-embeddings.md b/Documentation/Epics/local-embeddings.md index ae8c507..36c9a28 100644 --- a/Documentation/Epics/local-embeddings.md +++ b/Documentation/Epics/local-embeddings.md @@ -1,7 +1,7 @@ # Epic: Local Embeddings as the Primary Provider **Date:** 2026-07-02 -**Status:** In Progress — Phase 4 complete (keyless onboarding + provider UI + docs); Phase 5 (cross-platform) + Phase 6 (cleanup) remain +**Status:** In Progress — Phase 5 started (foundation: per-platform go:embed split, branch `feat/xplat-foundation`); Phases 0–4 complete and in PR #2 (draft, awaiting review); Phase 6 (cleanup) remains **Owner:** Bo Motlagh ## Goal @@ -426,6 +426,21 @@ and the indexing-progress UX all already exist and are the extension points. ### Phase 5 — Cross-platform builds +**Execution decisions (recorded 2026-07-16, per the "update the tables, don't silently diverge" rule):** + +- **Delivered as small stacked PRs** (Bo's small-PRs rule; the epic doesn't mandate one PR): + foundation → Linux → macOS x86_64 → Windows. Windows moved last purely for convenience + (only target needing a second machine for the Rust tokenizer build); no dependency reason. +- **Foundation PR (`feat/xplat-foundation`)** implements Open Concern #8: the ORT shared + library's `go:embed` moves from `assets_embed.go` into per-platform + `assets_embed__.go` files (each defines `embeddedORTLib` + `ortLibFile`; + darwin-arm64 first). Model + tokenizer stay in the shared embed (platform-independent + bytes). Each later platform PR adds one sibling file + manifest entries only. +- **Makefile checksum portability**: `shasum -a 256` (macOS-only) replaced by a `SHA256` + variable that picks `sha256sum` on Linux — prerequisite for `make assets` inside Docker/CI. +- `dylibCandidates` gains the versioned Linux name (`libonnxruntime.so.1.26.0`) that the + official Linux tarball actually ships. + 1. Linux x64/arm64: assets manifest entries, CI build, smoke test. 2. Windows x64: CI job builds `libtokenizers.a` with Rust toolchain (no published binary); ORT DLL from official release; smoke test. diff --git a/Makefile b/Makefile index 1dcddb0..9cde85f 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,9 @@ LOCAL_DIR := internal/embeddings/local EMBED_DIR := $(LOCAL_DIR)/embedded LIB_DIR := $(LOCAL_DIR)/lib PLATFORM := $(shell go env GOOS)-$(shell go env GOARCH) +# Portable SHA-256: macOS ships shasum, Linux ships sha256sum. Both print +# " ", so the awk '{print $1}' callers work with either. +SHA256 := $(shell command -v sha256sum >/dev/null 2>&1 && echo sha256sum || echo shasum -a 256) build: assets CGO_LDFLAGS="-L$(PWD)/$(LIB_DIR) -ltokenizers" wails build -skipbindings -tags localembed @@ -41,14 +44,14 @@ assets: destpath=$(LOCAL_DIR)/$$dest; \ want=$$sha; [ -n "$$membersha" ] && want=$$membersha; \ if [ -f "$$destpath" ]; then \ - have=$$(shasum -a 256 "$$destpath" | awk '{print $$1}'); \ + have=$$($(SHA256) "$$destpath" | awk '{print $$1}'); \ if [ "$$have" = "$$want" ]; then echo " ok (cached) $$dest"; continue; fi; \ echo " stale, refetching $$dest"; \ fi; \ echo " downloading $$key -> $$dest"; \ tmp=$$(mktemp); \ curl -fsSL -o "$$tmp" "$$url"; \ - got=$$(shasum -a 256 "$$tmp" | awk '{print $$1}'); \ + got=$$($(SHA256) "$$tmp" | awk '{print $$1}'); \ if [ "$$got" != "$$sha" ]; then \ echo "ERROR: archive checksum mismatch for $$key: got $$got want $$sha"; rm -f "$$tmp"; exit 1; \ fi; \ @@ -58,7 +61,7 @@ assets: tar xzf "$$tmp" -C "$$xd" "$$member"; \ cp "$$xd/$$member" "$$destpath"; \ rm -rf "$$xd"; \ - got2=$$(shasum -a 256 "$$destpath" | awk '{print $$1}'); \ + got2=$$($(SHA256) "$$destpath" | awk '{print $$1}'); \ if [ "$$got2" != "$$membersha" ]; then \ echo "ERROR: member checksum mismatch for $$key: got $$got2 want $$membersha"; rm -f "$$tmp"; exit 1; \ fi; \ diff --git a/internal/embeddings/local/assets.go b/internal/embeddings/local/assets.go index 01b4638..506324f 100644 --- a/internal/embeddings/local/assets.go +++ b/internal/embeddings/local/assets.go @@ -21,10 +21,14 @@ const ( ) // dylibCandidates are the ONNX Runtime shared-library file names we look for, -// most specific first. +// most specific first. Used only for developer-override directories +// (Config.AssetsDir / AGENT_MEMORY_LOCAL_ASSETS); the bundled path resolves the +// per-platform ortLibFile directly. The Linux release tarball ships the +// versioned name (libonnxruntime.so.1.26.0), hence both .so forms. var dylibCandidates = []string{ "libonnxruntime.1.26.0.dylib", "libonnxruntime.dylib", + "libonnxruntime.so.1.26.0", "libonnxruntime.so", "onnxruntime.dll", } diff --git a/internal/embeddings/local/assets_embed.go b/internal/embeddings/local/assets_embed.go index 53d6d6a..fbeafbb 100644 --- a/internal/embeddings/local/assets_embed.go +++ b/internal/embeddings/local/assets_embed.go @@ -9,9 +9,13 @@ import ( "path/filepath" ) -// embeddedAssets carries the runtime assets compiled into the binary. These are +// embeddedAssets carries the platform-independent runtime assets compiled into +// the binary (model + tokenizer — identical bytes on every platform). They are // downloaded and checksum-verified by `make assets` into ./embedded/ before the // tagged build compiles (the go:embed directive requires the files to exist). +// The platform-specific ONNX Runtime shared library is embedded separately in +// the per-platform assets_embed__.go file (embeddedORTLib / +// ortLibFile). // // NOTE: go:embed can only reach files inside this package's own directory tree, // so the runtime assets live under internal/embeddings/local/embedded/ — NOT the @@ -23,7 +27,6 @@ import ( // //go:embed embedded/model_quantized.onnx //go:embed embedded/tokenizer.json -//go:embed embedded/libonnxruntime.1.26.0.dylib var embeddedAssets embed.FS // assetsEmbedded reports whether bundled assets are compiled into this build. @@ -31,12 +34,17 @@ var embeddedAssets embed.FS // the "no override configured" outcome across the two builds. const assetsEmbedded = true -// embeddedAssetPaths are the embed.FS paths of the runtime assets, in the order -// they are extracted. Each is written to disk under its base name. -var embeddedAssetPaths = []string{ - "embedded/" + assetModelFile, - "embedded/" + assetTokenizerFile, - "embedded/" + dylibCandidates[0], // libonnxruntime.1.26.0.dylib +// embeddedAssetSources pairs each runtime asset's embed.FS with its path, in +// extraction order. Each is written to disk under its base name. Model and +// tokenizer come from the shared embeddedAssets; the ONNX Runtime library comes +// from the per-platform embeddedORTLib. +var embeddedAssetSources = []struct { + fs *embed.FS + path string +}{ + {&embeddedAssets, "embedded/" + assetModelFile}, + {&embeddedAssets, "embedded/" + assetTokenizerFile}, + {&embeddedORTLib, "embedded/" + ortLibFile}, } // extractEmbeddedAssets materializes the go:embed-ed runtime assets into @@ -52,8 +60,9 @@ func extractEmbeddedAssets() (string, error) { destDir := filepath.Join(home, ".agent-memory", "runtime", fingerprint("local", modelName, modelDim)) - for _, embedPath := range embeddedAssetPaths { - data, err := embeddedAssets.ReadFile(embedPath) + for _, src := range embeddedAssetSources { + embedPath := src.path + data, err := src.fs.ReadFile(embedPath) if err != nil { return "", fmt.Errorf("local: read embedded asset %s: %w", embedPath, err) } diff --git a/internal/embeddings/local/assets_embed_darwin_arm64.go b/internal/embeddings/local/assets_embed_darwin_arm64.go new file mode 100644 index 0000000..5bbd395 --- /dev/null +++ b/internal/embeddings/local/assets_embed_darwin_arm64.go @@ -0,0 +1,18 @@ +//go:build localembed && darwin && arm64 + +package local + +import "embed" + +// Per-platform ONNX Runtime shared library (macOS arm64). One binary can embed +// only one platform's ORT lib (epic Open Concern #8), so each Phase 5 target +// adds a sibling of this file — the embed directive, the FS var, and the +// ortLibFile constant are the ONLY platform-specific pieces; everything else in +// this package is shared. +// +//go:embed embedded/libonnxruntime.1.26.0.dylib +var embeddedORTLib embed.FS + +// ortLibFile is the base name of this platform's embedded ONNX Runtime shared +// library, both inside embedded/ and after extraction to the runtime dir. +const ortLibFile = "libonnxruntime.1.26.0.dylib" diff --git a/internal/embeddings/local/integration_test.go b/internal/embeddings/local/integration_test.go index 218d2c6..3e5d92b 100644 --- a/internal/embeddings/local/integration_test.go +++ b/internal/embeddings/local/integration_test.go @@ -79,3 +79,15 @@ func cosine(a, b []float32) float64 { } return dot } + +// TestOrtLibFileIsKnownCandidate is a tripwire for future platform files: the +// per-platform ortLibFile must be a name findDylib recognizes, so a developer +// override directory populated with the same artifacts always resolves. +func TestOrtLibFileIsKnownCandidate(t *testing.T) { + for _, name := range dylibCandidates { + if name == ortLibFile { + return + } + } + t.Fatalf("ortLibFile %q is not in dylibCandidates %v", ortLibFile, dylibCandidates) +}