Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion Documentation/Epics/local-embeddings.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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_<GOOS>_<GOARCH>.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.
Expand Down
9 changes: 6 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
# "<hash> <file>", 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
Expand Down Expand Up @@ -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; \
Expand All @@ -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; \
Expand Down
6 changes: 5 additions & 1 deletion internal/embeddings/local/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
Expand Down
29 changes: 19 additions & 10 deletions internal/embeddings/local/assets_embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -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_<GOOS>_<GOARCH>.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
Expand All @@ -23,20 +27,24 @@ 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.
// True here (localembed); the stub sets it false. Tests use it to distinguish
// 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
Expand All @@ -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)
}
Expand Down
18 changes: 18 additions & 0 deletions internal/embeddings/local/assets_embed_darwin_arm64.go
Original file line number Diff line number Diff line change
@@ -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"
12 changes: 12 additions & 0 deletions internal/embeddings/local/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}