From dbbb7410f069379ee760f052e355e0a56cc0c3e6 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Wed, 2 Sep 2026 19:27:08 +0530 Subject: [PATCH 1/2] chore: bridge ecosystem-wiring onto main history --- .github/workflows/ci.yml | 39 +-- AGENTS.md | 10 +- CHANGELOG.md | 4 +- README.md | 10 +- client/adapters/anthropic.go | 2 +- client/adapters/anthropic_base.go | 2 +- client/adapters/azure.go | 2 +- client/adapters/bedrock.go | 2 +- client/adapters/concentrate_responses.go | 2 +- client/adapters/gemini.go | 2 +- client/adapters/openai.go | 2 +- client/adapters/protocol_router.go | 2 +- client/adapters/protocol_router_test.go | 2 +- client/adapters/vertex.go | 2 +- client/aliases.go | 4 +- client/core/core.go | 6 +- client/embeddings/cache_test.go | 2 +- client/extract.go | 2 +- client/token_utils.go | 2 +- config/discovery_status_test.go | 1 + docs/architecture/HOST-ENGINE-BOUNDARY.md | 13 +- docs/design/EYRIE-ENTERPRISE.md | 2 +- docs/guides/DYNAMIC-MODEL-DISCOVERY.md | 9 +- engine/contract_assert.go | 2 +- engine/control_plane.go | 2 +- engine/convert_test.go | 2 +- engine/doc.go | 2 +- engine/engine.go | 2 +- engine/engine_test.go | 2 +- engine/model_policy.go | 2 +- engine/model_policy_custom_test.go | 2 +- engine/types.go | 2 +- go.mod | 20 +- go.sum | 62 ++-- graph/graph.go | 336 ++++++++++++++++++ graph/graph_test.go | 38 ++ internal/observability/genai_semconv.go | 2 +- llm/llm_test.go | 30 ++ llm/provider.go | 402 ++++++++++++++++++++++ llm/types.go | 239 +++++++++++++ operationsgraph/operations_graph.go | 2 +- operationsgraph/projection.go | 4 +- operationsgraph/projection_test.go | 4 +- plans/client-package-decomposition.md | 5 +- scripts/check-ecosystem-boundaries.sh | 6 +- scripts/check-no-replace-directives.sh | 2 +- tools/tool.go | 15 + tools/tools_test.go | 43 +++ tools/versioning.go | 144 ++++++++ 49 files changed, 1364 insertions(+), 132 deletions(-) create mode 100644 graph/graph.go create mode 100644 graph/graph_test.go create mode 100644 llm/llm_test.go create mode 100644 llm/provider.go create mode 100644 llm/types.go create mode 100644 tools/tool.go create mode 100644 tools/tools_test.go create mode 100644 tools/versioning.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ee9143..2987f30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,13 @@ env: GOPRIVATE: "github.com/GrayCodeAI/*" GONOSUMDB: "github.com/GrayCodeAI/*" GONOSUMCHECK: "1" + # CI must resolve the module versions pinned in go.mod/go.sum, not whatever + # sibling working trees a go.work workspace happens to point at. Go's own + # reference warns a committed go.work "may cause a continuous integration (CI) + # system to select and thus test the wrong versions of a module's + # dependencies" and that "CI systems should generally not be allowed to use + # the go.work file". + GOWORK: "off" jobs: # ------------------------------------------------------------------------- @@ -72,10 +79,6 @@ jobs: with: go-version: ${{ env.GO_VERSION }} cache: true - - name: Clone ecosystem deps - run: | - git clone --depth=1 https://github.com/GrayCodeAI/tok.git ../tok - git clone --depth=1 https://github.com/GrayCodeAI/hawk-core-contracts.git ../hawk-core-contracts - name: go mod tidy run: | go mod tidy @@ -106,10 +109,6 @@ jobs: with: go-version: ${{ env.GO_VERSION }} cache: true - - name: Clone ecosystem deps - run: | - git clone --depth=1 https://github.com/GrayCodeAI/tok.git ../tok - git clone --depth=1 https://github.com/GrayCodeAI/hawk-core-contracts.git ../hawk-core-contracts - name: go vet run: go vet ./... @@ -126,10 +125,6 @@ jobs: with: go-version: ${{ env.GO_VERSION }} cache: true - - name: Clone ecosystem deps - run: | - git clone --depth=1 https://github.com/GrayCodeAI/tok.git ../tok - git clone --depth=1 https://github.com/GrayCodeAI/hawk-core-contracts.git ../hawk-core-contracts - name: Boundary guard run: | bash ./scripts/check-ecosystem-boundaries.sh @@ -152,10 +147,6 @@ jobs: with: go-version: ${{ env.GO_VERSION }} cache: true - - name: Clone ecosystem deps - run: | - git clone --depth=1 https://github.com/GrayCodeAI/tok.git ../tok - git clone --depth=1 https://github.com/GrayCodeAI/hawk-core-contracts.git ../hawk-core-contracts - name: Boundary guard run: | bash ./scripts/check-ecosystem-boundaries.sh @@ -193,10 +184,6 @@ jobs: with: go-version: ${{ env.GO_VERSION }} cache: true - - name: Clone ecosystem deps - run: | - git clone --depth=1 https://github.com/GrayCodeAI/tok.git ../tok - git clone --depth=1 https://github.com/GrayCodeAI/hawk-core-contracts.git ../hawk-core-contracts - name: govulncheck run: | go install golang.org/x/vuln/cmd/govulncheck@v1.1.4 @@ -218,10 +205,6 @@ jobs: with: go-version: ${{ env.GO_VERSION }} cache: true - - name: Clone ecosystem deps - run: | - git clone --depth=1 https://github.com/GrayCodeAI/tok.git ../tok - git clone --depth=1 https://github.com/GrayCodeAI/hawk-core-contracts.git ../hawk-core-contracts - name: deadcode run: | go install golang.org/x/tools/cmd/deadcode@v0.30.0 @@ -281,10 +264,6 @@ jobs: with: go-version: ${{ env.GO_VERSION }} cache: true - - name: Clone ecosystem deps - run: | - git clone --depth=1 https://github.com/GrayCodeAI/tok.git ../tok - git clone --depth=1 https://github.com/GrayCodeAI/hawk-core-contracts.git ../hawk-core-contracts - name: Run fuzz targets run: | go test -fuzz=FuzzSanitizeMessages -fuzztime=60s ./client @@ -313,10 +292,6 @@ jobs: with: go-version: ${{ env.GO_VERSION }} cache: true - - name: Clone ecosystem deps - run: | - git clone --depth=1 https://github.com/GrayCodeAI/tok.git ../tok - git clone --depth=1 https://github.com/GrayCodeAI/hawk-core-contracts.git ../hawk-core-contracts - name: Build (library — cross-compile all packages) env: GOOS: ${{ matrix.goos }} diff --git a/AGENTS.md b/AGENTS.md index f28ce4f..736ba18 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,7 +54,10 @@ make ci # Full CI suite - `client.Provider` remains the lower-level compatibility boundary for other consumers; preserve its method set and the facade's type identity - Streaming tests need careful goroutine management -- `go.work` here should stay minimal; hawk's own `go.work` adds an `external/eyrie` replace so hawk can develop against a local eyrie checkout. Do not add extra local `replace` directives here without coordinating with hawk's workspace. +- `go.work` here should stay minimal; the parent `graycode-eco/go.work` + connects this independent `eyrie` checkout beside Hawk for local development. + Do not add extra local `replace` directives here without coordinating with + the parent workspace. ## Naming Conventions @@ -138,4 +141,7 @@ make ci # Full CI suite | Main test file | `client/client_test.go` (httptest servers, provider detection) | | Linter config | `.golangci.yml` (govet, ineffassign, misspell — minimal) | -This repo is a submodule of [hawk](https://github.com/GrayCodeAI/hawk) at `hawk/external/eyrie`. Work in the submodule (go.work picks it up), push, sync here, PR/merge, then pull main in the submodule. +This is an independent repository consumed by Hawk. In the local +`graycode-eco` parent workspace it is checked out beside `hawk` as `../eyrie` +and connected through the parent `go.work`; publish changes here, then update +Hawk's module pin through a separate PR. diff --git a/CHANGELOG.md b/CHANGELOG.md index 336b8e5..4be2f1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,8 +90,8 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · Versioning: 5. Whitespace collapse Reports aggregate `BytesSaved` and `PercentOff` across all tools in the slice. Safe to call concurrently. - Aligns eyrie with the rest of the hawk-eco ecosystem (`hawk`, `tok`, - `yaad`, `sight`, `inspect`). + Aligns eyrie with the rest of the hawk-eco ecosystem (`hawk`, `shrike`, + `harrier`, `kestrel`, `merlin`). ### Added - Output guardrails framework (PII, secrets, injection, harmful content) diff --git a/README.md b/README.md index 4a84745..f915d87 100644 --- a/README.md +++ b/README.md @@ -47,11 +47,11 @@ provider packages. eyrie is a Hawk support engine. Keep the dependency edge one-way: -- host-facing DTOs and the `Provider` port live in `hawk-core-contracts/llm`; `engine/` re-exports them as aliases (`*Engine` implements `llm.Provider`) +- host-facing DTOs and the `Provider` port live in `eagle/llm`; `engine/` re-exports them as aliases (`*Engine` implements `llm.Provider`) - internal provider/transport types stay eyrie-scoped (not shared contracts) - do not import `hawk/internal/*` - do not import removed legacy path `hawk/shared/types` -- do not import other engines (`yaad`, `tok`, `trace`, `sight`, `inspect`) — engines are peers, not dependencies +- do not import other engines (`harrier`, `shrike`, `swift`, `kestrel`, `merlin`) — engines are peers, not dependencies ## Quick Start @@ -308,9 +308,9 @@ eyrie is part of the hawk-eco: |---|---|---| | **hawk** | [GrayCodeAI/hawk](https://github.com/GrayCodeAI/hawk) | AI coding agent | | **eyrie** | This repo | LLM provider runtime | -| **tok** | [GrayCodeAI/tok](https://github.com/GrayCodeAI/tok) | Tokenizer & compression | -| **yaad** | [GrayCodeAI/yaad](https://github.com/GrayCodeAI/yaad) | Graph-based memory | -| **trace** | [GrayCodeAI/trace](https://github.com/GrayCodeAI/trace) | Session capture | +| **shrike** | [GrayCodeAI/shrike](https://github.com/GrayCodeAI/shrike) | Tokenizer & compression | +| **harrier** | [GrayCodeAI/harrier](https://github.com/GrayCodeAI/harrier) | Graph-based memory | +| **swift** | [GrayCodeAI/swift](https://github.com/GrayCodeAI/swift) | Session capture | ## Development diff --git a/client/adapters/anthropic.go b/client/adapters/anthropic.go index a8c4abc..edfb1d1 100644 --- a/client/adapters/anthropic.go +++ b/client/adapters/anthropic.go @@ -10,8 +10,8 @@ import ( "net/http" "strings" + "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" - "github.com/GrayCodeAI/hawk-core-contracts/llm" ) // maxAnthropicRequestSize is the maximum request body size for the Messages API (32 MB). diff --git a/client/adapters/anthropic_base.go b/client/adapters/anthropic_base.go index 24e048c..6295a2f 100644 --- a/client/adapters/anthropic_base.go +++ b/client/adapters/anthropic_base.go @@ -3,8 +3,8 @@ package adapters import ( "strings" + "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" - "github.com/GrayCodeAI/hawk-core-contracts/llm" ) // AnthropicBaseFromOpenAIV1 strips a trailing /v1 from an OpenAI-compatible base URL. diff --git a/client/adapters/azure.go b/client/adapters/azure.go index 4f7cf34..0271eb7 100644 --- a/client/adapters/azure.go +++ b/client/adapters/azure.go @@ -10,8 +10,8 @@ import ( "net/http" "strings" + "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" - "github.com/GrayCodeAI/hawk-core-contracts/llm" ) const ( diff --git a/client/adapters/bedrock.go b/client/adapters/bedrock.go index b2a51f4..7115b8c 100644 --- a/client/adapters/bedrock.go +++ b/client/adapters/bedrock.go @@ -18,8 +18,8 @@ import ( "strings" "time" + "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" - "github.com/GrayCodeAI/hawk-core-contracts/llm" ) const ( diff --git a/client/adapters/concentrate_responses.go b/client/adapters/concentrate_responses.go index a806587..c62e627 100644 --- a/client/adapters/concentrate_responses.go +++ b/client/adapters/concentrate_responses.go @@ -12,8 +12,8 @@ import ( "strings" "time" + "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" - "github.com/GrayCodeAI/hawk-core-contracts/llm" ) // ConcentrateResponsesClient uses the Concentrate Responses API (the production-ready diff --git a/client/adapters/gemini.go b/client/adapters/gemini.go index 6839680..fb5d0a7 100644 --- a/client/adapters/gemini.go +++ b/client/adapters/gemini.go @@ -11,8 +11,8 @@ import ( "os" "strings" + "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" - "github.com/GrayCodeAI/hawk-core-contracts/llm" ) // geminiSharedParserEnvVar is the opt-out flag for the new diff --git a/client/adapters/openai.go b/client/adapters/openai.go index d533450..c415651 100644 --- a/client/adapters/openai.go +++ b/client/adapters/openai.go @@ -10,8 +10,8 @@ import ( "net/http" "strings" + "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" - "github.com/GrayCodeAI/hawk-core-contracts/llm" ) const ( diff --git a/client/adapters/protocol_router.go b/client/adapters/protocol_router.go index b570d60..c3eb40d 100644 --- a/client/adapters/protocol_router.go +++ b/client/adapters/protocol_router.go @@ -5,8 +5,8 @@ import ( "fmt" "strings" + "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" - "github.com/GrayCodeAI/hawk-core-contracts/llm" ) // ChatProtocol selects which existing eyrie client handles a gateway request. diff --git a/client/adapters/protocol_router_test.go b/client/adapters/protocol_router_test.go index 2b29248..d7db241 100644 --- a/client/adapters/protocol_router_test.go +++ b/client/adapters/protocol_router_test.go @@ -6,8 +6,8 @@ import ( "net/http" "testing" + "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" - "github.com/GrayCodeAI/hawk-core-contracts/llm" ) func TestNewStreamWithReasoningFallbackChatFirst(t *testing.T) { diff --git a/client/adapters/vertex.go b/client/adapters/vertex.go index 2c514e5..8ef683f 100644 --- a/client/adapters/vertex.go +++ b/client/adapters/vertex.go @@ -9,8 +9,8 @@ import ( "log/slog" "net/http" + "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" - "github.com/GrayCodeAI/hawk-core-contracts/llm" ) // maxVertexRequestSize is the maximum request body size for Vertex AI (30 MB). diff --git a/client/aliases.go b/client/aliases.go index 3c52a7c..1f38e40 100644 --- a/client/aliases.go +++ b/client/aliases.go @@ -5,10 +5,10 @@ import ( "net/http" "time" + "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/adapters" "github.com/GrayCodeAI/eyrie/client/core" "github.com/GrayCodeAI/eyrie/client/embeddings" - "github.com/GrayCodeAI/hawk-core-contracts/llm" ) // The provider contract and request/response data types live in @@ -234,7 +234,7 @@ var ( // NewStreamResult creates a StreamResult with a cancel function for resource // cleanup. The request ID is optional; pass "" when it is not yet available. // The canonical constructor lives in -// github.com/GrayCodeAI/hawk-core-contracts/llm; this is a thin facade +// github.com/GrayCodeAI/eyrie/llm; this is a thin facade // wrapper that keeps the public client API stable. func NewStreamResult(events <-chan EyrieStreamEvent, cancel context.CancelFunc) *StreamResult { return llm.NewStreamResult(events, "", cancel) diff --git a/client/core/core.go b/client/core/core.go index 8503917..648c6ef 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -4,7 +4,7 @@ // // core is a leaf package — it must not import any other eyrie/client // subpackage. The conversation DTOs below are aliases to the canonical -// hawk-core-contracts/llm definitions; core re-exports them so subpackages +// eagle/llm definitions; core re-exports them so subpackages // share the contract without an import cycle through the facade. The public // names remain available as aliases in github.com/GrayCodeAI/eyrie/client, // which is the API consumers should keep importing. @@ -15,7 +15,7 @@ package core import ( "context" - "github.com/GrayCodeAI/hawk-core-contracts/llm" + "github.com/GrayCodeAI/eyrie/llm" ) // Provider is the core interface for LLM providers. @@ -76,7 +76,7 @@ type EyrieStreamEvent = llm.EyrieStreamEvent // // StreamResult is aliased to the canonical contract type; its Close() // method and canonical constructor (NewStreamResult) live in -// github.com/GrayCodeAI/hawk-core-contracts/llm. +// github.com/GrayCodeAI/eyrie/llm. type StreamResult = llm.StreamResult // ResponseFormat specifies the desired output format for the model response. diff --git a/client/embeddings/cache_test.go b/client/embeddings/cache_test.go index e073273..499e389 100644 --- a/client/embeddings/cache_test.go +++ b/client/embeddings/cache_test.go @@ -6,8 +6,8 @@ import ( "sync" "testing" + "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" - "github.com/GrayCodeAI/hawk-core-contracts/llm" ) // echoMock is a minimal core.Provider that echoes the last user message and diff --git a/client/extract.go b/client/extract.go index 1ec8d31..b6c2ec2 100644 --- a/client/extract.go +++ b/client/extract.go @@ -76,7 +76,7 @@ Rules: // ExtractRelationships extracts subject-predicate-object triples from text using // schema-validated structured output with retry. It is a typed convenience layer -// over ChatWithStructuredOutput, modeled on CocoIndex's ExtractByLlm; yaad and +// over ChatWithStructuredOutput, modeled on CocoIndex's ExtractByLlm; Harrier and // other knowledge-graph consumers can call it instead of hand-rolling extraction // prompts and JSON parsing. func (c *EyrieClient) ExtractRelationships(ctx context.Context, text string, opts ExtractOptions) ([]Relationship, error) { diff --git a/client/token_utils.go b/client/token_utils.go index 142c1c8..07d7313 100644 --- a/client/token_utils.go +++ b/client/token_utils.go @@ -75,7 +75,7 @@ func fallbackTokenCount(text string) int { } // compressForSummary keeps PromptOptimizer self-contained. It performs a small -// whitespace-normalizing reduction instead of depending on tok's full pipeline. +// whitespace-normalizing reduction instead of depending on shrike's full pipeline. func compressForSummary(text string) string { text = strings.TrimSpace(text) if text == "" { diff --git a/config/discovery_status_test.go b/config/discovery_status_test.go index 2791b05..7640798 100644 --- a/config/discovery_status_test.go +++ b/config/discovery_status_test.go @@ -25,6 +25,7 @@ func (emptyCredentialStore) Get(context.Context, string) (string, error) { retur func (emptyCredentialStore) Delete(context.Context, string) error { return nil } func TestHasAnyConfiguredDeployment_RejectsPlaceholder(t *testing.T) { + t.Setenv("HAWK_CONFIG_DIR", t.TempDir()) credentials.SetDefaultStore(emptyCredentialStore{}) t.Cleanup(func() { credentials.SetDefaultStore(nil) }) diff --git a/docs/architecture/HOST-ENGINE-BOUNDARY.md b/docs/architecture/HOST-ENGINE-BOUNDARY.md index e6e122a..7aa5ccd 100644 --- a/docs/architecture/HOST-ENGINE-BOUNDARY.md +++ b/docs/architecture/HOST-ENGINE-BOUNDARY.md @@ -169,7 +169,7 @@ Preflight has two explicit modes: These modes must not be conflated: local readiness is suitable for startup and diagnostics; live readiness is an explicit network operation. -## Release and submodule order +## Release and sibling-repository order The boundary is delivered Eyrie-first: @@ -177,14 +177,15 @@ The boundary is delivered Eyrie-first: 1. Change and verify standalone Eyrie 2. Commit Eyrie and publish a resolvable release/commit 3. Update Hawk's Eyrie module version when required -4. Advance Hawk's Eyrie submodule pin to that exact commit +4. Update Hawk's Eyrie module pin to that exact published commit 5. Verify Hawk integration, boundary checks, and clean-clone/module builds -6. Commit Hawk's code and gitlink update together +6. Commit the Hawk module-pin update in Hawk's repository ``` -Hawk must never point at an uncommitted Eyrie worktree. A submodule pin proves -source identity; a resolvable module version is also required for workflows -that build Hawk with `GOWORK=off`. +Hawk must never depend on an uncommitted Eyrie worktree. The parent workspace +uses a sibling checkout for source identity during local development; a +resolvable published module version is also required for workflows that build +Hawk with `GOWORK=off`. ## Compatibility policy diff --git a/docs/design/EYRIE-ENTERPRISE.md b/docs/design/EYRIE-ENTERPRISE.md index b2746a3..29d2a28 100644 --- a/docs/design/EYRIE-ENTERPRISE.md +++ b/docs/design/EYRIE-ENTERPRISE.md @@ -418,7 +418,7 @@ These repos are privacy-first; the enterprise layer must not regress that. 1. **Single-tenant per binary vs. multi-org per binary?** This doc assumes multi-org within one operator's deployment. Is true cross-customer multi-tenancy ever in scope, or does that stay - a hawk-cloud concern (`TOP20_COMPARISON.md:33-34`)? + a graycode-cloud concern (`TOP20_COMPARISON.md:33-34`)? 2. **SSO scope:** OIDC only for P0, or do enterprise buyers require SAML (which needs a heavier dep)? SAML likely pushes to P2. 3. **Where does `session_id` originate?** From the OpenAI `user` field (`openai_proxy.go:41`), a diff --git a/docs/guides/DYNAMIC-MODEL-DISCOVERY.md b/docs/guides/DYNAMIC-MODEL-DISCOVERY.md index ff3340d..239d736 100644 --- a/docs/guides/DYNAMIC-MODEL-DISCOVERY.md +++ b/docs/guides/DYNAMIC-MODEL-DISCOVERY.md @@ -259,14 +259,15 @@ standalone Eyrie change --> signed Eyrie commit --> publish a resolvable Eyrie module release/commit --> update Hawk module dependency when needed - --> pin Hawk's Eyrie submodule to the same commit + --> update Hawk's Eyrie module pin to the same published commit --> Hawk integration + boundary + clean-clone verification (two passes) --> commit Hawk code and gitlink together ``` -The submodule must point to a committed Eyrie object, never working-tree-only -code. The published module and submodule must expose the same Engine contract -so both workspace builds and `GOWORK=off` builds are reproducible. +The parent workspace must use a committed Eyrie checkout, never working-tree- +only code, and Hawk's module pin must resolve to that same published commit. +Both workspace builds and `GOWORK=off` builds must expose the same Engine +contract. ## Related documentation diff --git a/engine/contract_assert.go b/engine/contract_assert.go index 6d4e275..f649118 100644 --- a/engine/contract_assert.go +++ b/engine/contract_assert.go @@ -1,6 +1,6 @@ package engine -import "github.com/GrayCodeAI/hawk-core-contracts/llm" +import "github.com/GrayCodeAI/eyrie/llm" // Compile-time assertions: the host facade implements the shared port. // If a method is added to llm.Provider (or EventStreamer), this file fails diff --git a/engine/control_plane.go b/engine/control_plane.go index 9236c2b..4cbe216 100644 --- a/engine/control_plane.go +++ b/engine/control_plane.go @@ -6,11 +6,11 @@ import ( "sort" "strings" + llm "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/catalog" "github.com/GrayCodeAI/eyrie/catalog/registry" "github.com/GrayCodeAI/eyrie/config" "github.com/GrayCodeAI/eyrie/credentials" - llm "github.com/GrayCodeAI/hawk-core-contracts/llm" ) // ResolveCredential validates credential input and returns safe provider diff --git a/engine/convert_test.go b/engine/convert_test.go index e3463ab..a0b6260 100644 --- a/engine/convert_test.go +++ b/engine/convert_test.go @@ -3,8 +3,8 @@ package engine import ( "testing" + llm "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client" - llm "github.com/GrayCodeAI/hawk-core-contracts/llm" ) func TestToClientMessages_ReturnsMessagesUnchanged(t *testing.T) { diff --git a/engine/doc.go b/engine/doc.go index 3915022..bba7b43 100644 --- a/engine/doc.go +++ b/engine/doc.go @@ -9,6 +9,6 @@ // Eyrie owns credential, catalog, selection, routing, and model transport. // // Host-facing DTOs and the Provider port live in -// github.com/GrayCodeAI/hawk-core-contracts/llm; this package re-exports them +// github.com/GrayCodeAI/eyrie/llm; this package re-exports them // as type aliases and *Engine implements llm.Provider (see contract_assert.go). package engine diff --git a/engine/engine.go b/engine/engine.go index 123c2b8..8e5a4a7 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -11,13 +11,13 @@ import ( "sync" "time" + "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/catalog" "github.com/GrayCodeAI/eyrie/catalog/registry" "github.com/GrayCodeAI/eyrie/client" "github.com/GrayCodeAI/eyrie/config" "github.com/GrayCodeAI/eyrie/credentials" "github.com/GrayCodeAI/eyrie/setup" - "github.com/GrayCodeAI/hawk-core-contracts/llm" ) // ContractVersion is the compatibility version of the host-facing API. diff --git a/engine/engine_test.go b/engine/engine_test.go index 9cd6692..c51f576 100644 --- a/engine/engine_test.go +++ b/engine/engine_test.go @@ -6,11 +6,11 @@ import ( "path/filepath" "testing" + "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/catalog" "github.com/GrayCodeAI/eyrie/client" "github.com/GrayCodeAI/eyrie/config" "github.com/GrayCodeAI/eyrie/credentials" - "github.com/GrayCodeAI/hawk-core-contracts/llm" ) func TestNewUsesInjectedCredentialStore(t *testing.T) { diff --git a/engine/model_policy.go b/engine/model_policy.go index bd4841c..452bf29 100644 --- a/engine/model_policy.go +++ b/engine/model_policy.go @@ -5,8 +5,8 @@ import ( "sort" "strings" + "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/catalog" - "github.com/GrayCodeAI/hawk-core-contracts/llm" ) func (e *Engine) policyCatalog(ctx context.Context) (*catalog.CompiledCatalog, error) { diff --git a/engine/model_policy_custom_test.go b/engine/model_policy_custom_test.go index ee5acf0..0888d53 100644 --- a/engine/model_policy_custom_test.go +++ b/engine/model_policy_custom_test.go @@ -7,9 +7,9 @@ import ( "slices" "testing" + "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/catalog" "github.com/GrayCodeAI/eyrie/credentials" - "github.com/GrayCodeAI/hawk-core-contracts/llm" ) func TestModelPolicyIncludesInvocationScopedCustomGateways(t *testing.T) { diff --git a/engine/types.go b/engine/types.go index 599cb78..33b3764 100644 --- a/engine/types.go +++ b/engine/types.go @@ -1,7 +1,7 @@ package engine import ( - "github.com/GrayCodeAI/hawk-core-contracts/llm" + "github.com/GrayCodeAI/eyrie/llm" ) // Intent expresses a host's semantic preference without naming a provider. diff --git a/go.mod b/go.mod index e242d77..63774fb 100644 --- a/go.mod +++ b/go.mod @@ -3,35 +3,37 @@ module github.com/GrayCodeAI/eyrie go 1.26.6 require ( - github.com/GrayCodeAI/hawk-core-contracts v0.1.12 github.com/google/uuid v1.6.0 github.com/tiktoken-go/tokenizer v0.8.0 github.com/zalando/go-keyring v0.2.8 go.opentelemetry.io/otel v1.44.0 go.opentelemetry.io/otel/trace v1.44.0 - google.golang.org/grpc v1.81.1 + google.golang.org/grpc v1.82.1 modernc.org/sqlite v1.51.0 ) require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/danieljoos/wincred v1.2.3 // indirect - github.com/dlclark/regexp2/v2 v2.1.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dlclark/regexp2/v2 v2.2.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect - github.com/mattn/go-isatty v0.0.22 // indirect + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/stretchr/objx v0.5.3 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/metric v1.44.0 // indirect - golang.org/x/net v0.53.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/text v0.36.0 // indirect - golang.org/x/tools v0.44.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect + golang.org/x/net v0.57.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/protobuf v1.36.11 // indirect modernc.org/libc v1.72.5 // indirect modernc.org/mathutil v1.7.1 // indirect diff --git a/go.sum b/go.sum index 99238cd..51b3c87 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,11 @@ -github.com/GrayCodeAI/hawk-core-contracts v0.1.12 h1:percfsd771JLmO9gMkrQtENEPBA9ZN3dG1Nc1moN3ZQ= -github.com/GrayCodeAI/hawk-core-contracts v0.1.12/go.mod h1:BXbh68YrCf+s9HVqND5F8DAvl2MnE5NcOwZZZB56HGA= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2/v2 v2.1.0 h1:jHXRmHRZGbuQzDZjMlCAXOvQb75iv3HyLDzXGj5H1AY= -github.com/dlclark/regexp2/v2 v2.1.0/go.mod h1:Bz5TMy5d8fPK0ximH0Yi9KvsRHNnvXqUx9XG6a4wB+I= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0= +github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -21,18 +19,18 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= -github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= @@ -49,30 +47,30 @@ go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/graph/graph.go b/graph/graph.go new file mode 100644 index 0000000..39dfed8 --- /dev/null +++ b/graph/graph.go @@ -0,0 +1,336 @@ +// Package graph defines the portable graph vocabulary shared across hawk-eco. +// +// The package contains data contracts only. Individual repositories retain +// ownership of their graph storage, projections, and runtime behavior. +// +// Graph Engineering Patterns: +// This package implements the foundational patterns from LangGraph, +// Microsoft AutoGen, and Google ADK for multi-agent orchestration as +// nodes, edges, and shared state. +package graph + +import ( + "fmt" + "strings" + "time" +) + +// NodeType represents the specific type/behavior of a node in the graph +type NodeType string + +const ( + // NodeTypeAgent represents an autonomous agent that can make decisions + NodeTypeAgent NodeType = "agent" + // NodeTypeTool represents a tool/function that agents can invoke + NodeTypeTool NodeType = "tool" + // NodeTypeFunction represents a function node (deterministic operation) + NodeTypeFunction NodeType = "function" + // NodeTypeStart represents the entry point of a graph + NodeTypeStart NodeType = "start" + // NodeTypeEnd represents the termination point of a graph + NodeTypeEnd NodeType = "end" + // NodeTypeRouter represents a conditional routing node + NodeTypeRouter NodeType = "router" + // NodeTypeQuality represents a code quality analysis node + NodeTypeQuality NodeType = "quality" + // NodeTypeExecution represents an execution journal node + NodeTypeExecution NodeType = "execution" + // NodeTypeOperations represents an operations orchestration node + NodeTypeOperations NodeType = "operations" + // NodeTypeSystem is an alias for NodeKindSystem for backward compatibility + NodeTypeSystem NodeType = "system" +) + +// NodeKind classifies a node by the ecosystem view to which it belongs. +type NodeKind string + +const ( + NodeSystem NodeKind = "system" + NodeKnowledge NodeKind = "knowledge" + NodeExecution NodeKind = "execution" + NodePolicy NodeKind = "policy" + NodeQuality NodeKind = "quality" + NodeOperations NodeKind = "operations" +) + +// ParseNodeKind normalizes a graph node kind. +func ParseNodeKind(s string) (NodeKind, error) { + switch NodeKind(strings.ToLower(strings.TrimSpace(s))) { + case NodeSystem, NodeKnowledge, NodeExecution, NodePolicy, NodeQuality, NodeOperations: + return NodeKind(strings.ToLower(strings.TrimSpace(s))), nil + default: + return "", fmt.Errorf("graph: unknown node kind %q", s) + } +} + +// ParseNodeType normalizes a graph node type. +func ParseNodeType(s string) (NodeType, error) { + switch NodeType(strings.ToLower(strings.TrimSpace(s))) { + case NodeTypeAgent, NodeTypeTool, NodeTypeFunction, NodeTypeStart, NodeTypeEnd, NodeTypeRouter, NodeTypeQuality, NodeTypeExecution, NodeTypeOperations: + return NodeType(strings.ToLower(strings.TrimSpace(s))), nil + default: + return "", fmt.Errorf("graph: unknown node type %q", s) + } +} + +// NodeSpec describes a node in an orchestration graph. +type NodeSpec struct { + ID string `json:"id"` + Type NodeType `json:"type"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Config map[string]string `json:"config,omitempty"` +} + +// EdgeKind identifies a portable relationship between two graph nodes. +type EdgeKind string + +const ( + EdgeContains EdgeKind = "contains" + EdgeDependsOn EdgeKind = "depends_on" + EdgeReferences EdgeKind = "references" + EdgeProduced EdgeKind = "produced" + EdgeGovernedBy EdgeKind = "governed_by" + EdgeValidatedBy EdgeKind = "validated_by" +) + +// EdgeCondition represents a conditional edge in orchestration graphs. +// When non-empty, the edge is only followed if the condition evaluates to true. +type EdgeCondition struct { + Expression string `json:"expression,omitempty"` + Variables map[string]string `json:"variables,omitempty"` +} + +// EdgeSpec describes an edge in an orchestration graph. +type EdgeSpec struct { + From string `json:"from"` + To string `json:"to"` + Condition *EdgeCondition `json:"condition,omitempty"` + Weight float64 `json:"weight,omitempty"` +} + +// GraphSpec describes a complete orchestration graph. +type GraphSpec struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Nodes []NodeSpec `json:"nodes"` + Edges []EdgeSpec `json:"edges"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// ParseEdgeKind normalizes a graph edge kind. +func ParseEdgeKind(s string) (EdgeKind, error) { + switch EdgeKind(strings.ToLower(strings.TrimSpace(s))) { + case EdgeContains, EdgeDependsOn, EdgeReferences, EdgeProduced, EdgeGovernedBy, EdgeValidatedBy: + return EdgeKind(strings.ToLower(strings.TrimSpace(s))), nil + default: + return "", fmt.Errorf("graph: unknown edge kind %q", s) + } +} + +// EventType identifies a lifecycle event for a graph subject. +type EventType string + +const ( + EventCreated EventType = "created" + EventUpdated EventType = "updated" + EventTransitioned EventType = "transitioned" + EventObserved EventType = "observed" + EventDeleted EventType = "deleted" +) + +// ParseEventType normalizes a graph event type. +func ParseEventType(s string) (EventType, error) { + switch EventType(strings.ToLower(strings.TrimSpace(s))) { + case EventCreated, EventUpdated, EventTransitioned, EventObserved, EventDeleted: + return EventType(strings.ToLower(strings.TrimSpace(s))), nil + default: + return "", fmt.Errorf("graph: unknown event type %q", s) + } +} + +// Scope limits a graph fact to its authorized ecosystem boundary. Empty fields +// are valid for local-only or global facts. +type Scope struct { + TenantID string `json:"tenant_id,omitempty"` + ProjectID string `json:"project_id,omitempty"` + RepositoryID string `json:"repository_id,omitempty"` +} + +// Ref identifies a graph node without embedding its mutable attributes. +type Ref struct { + Kind NodeKind `json:"kind"` + ID string `json:"id"` +} + +// Validate reports whether r can safely identify a graph node. +func (r Ref) Validate() error { + if _, err := ParseNodeKind(string(r.Kind)); err != nil { + return err + } + if strings.TrimSpace(r.ID) == "" { + return fmt.Errorf("graph: reference ID is required") + } + return nil +} + +// ArtifactRef points to immutable evidence outside the contract payload. +type ArtifactRef struct { + URI string `json:"uri"` + Digest string `json:"digest,omitempty"` + MediaType string `json:"media_type,omitempty"` +} + +// Provenance identifies the producer and evidence for a graph fact. +type Provenance struct { + Producer string `json:"producer"` + Version string `json:"version,omitempty"` + SourceID string `json:"source_id,omitempty"` + Evidence []ArtifactRef `json:"evidence,omitempty"` +} + +// Validate reports whether p can support an auditable graph fact. +func (p Provenance) Validate() error { + if strings.TrimSpace(p.Producer) == "" { + return fmt.Errorf("graph: provenance producer is required") + } + for i, evidence := range p.Evidence { + if strings.TrimSpace(evidence.URI) == "" { + return fmt.Errorf("graph: evidence[%d] URI is required", i) + } + } + return nil +} + +// Node is a typed, temporal graph fact. Attributes are intentionally bounded +// to strings; large or sensitive data belongs in ArtifactRef evidence. +type Node struct { + ID string `json:"id"` + Kind NodeKind `json:"kind"` + Scope Scope `json:"scope,omitempty"` + CreatedAt time.Time `json:"created_at"` + EffectiveAt time.Time `json:"effective_at,omitempty"` + Provenance Provenance `json:"provenance"` + Attributes map[string]string `json:"attributes,omitempty"` +} + +// Validate reports whether n satisfies the minimum shared graph contract. +func (n Node) Validate() error { + if strings.TrimSpace(n.ID) == "" { + return fmt.Errorf("graph: node ID is required") + } + if _, err := ParseNodeKind(string(n.Kind)); err != nil { + return err + } + if n.CreatedAt.IsZero() { + return fmt.Errorf("graph: node created_at is required") + } + return n.Provenance.Validate() +} + +// Validate reports whether s satisfies the minimum node spec contract. +func (s NodeSpec) Validate() error { + if strings.TrimSpace(s.ID) == "" { + return fmt.Errorf("graph: node spec ID is required") + } + if _, err := ParseNodeType(string(s.Type)); err != nil { + return err + } + return nil +} + +// Validate reports whether s satisfies the minimum edge spec contract. +func (s EdgeSpec) Validate() error { + if strings.TrimSpace(s.From) == "" { + return fmt.Errorf("graph: edge spec from is required") + } + if strings.TrimSpace(s.To) == "" { + return fmt.Errorf("graph: edge spec to is required") + } + return nil +} + +// Validate reports whether s satisfies the minimum graph spec contract. +func (s GraphSpec) Validate() error { + if strings.TrimSpace(s.ID) == "" { + return fmt.Errorf("graph: graph spec ID is required") + } + if len(s.Nodes) == 0 { + return fmt.Errorf("graph: graph spec must have at least one node") + } + for i, node := range s.Nodes { + if err := node.Validate(); err != nil { + return fmt.Errorf("graph: node[%d]: %w", i, err) + } + } + for i, edge := range s.Edges { + if err := edge.Validate(); err != nil { + return fmt.Errorf("graph: edge[%d]: %w", i, err) + } + } + return nil +} + +// Edge is a typed, temporal relationship between two graph nodes. +type Edge struct { + ID string `json:"id"` + Kind EdgeKind `json:"kind"` + From Ref `json:"from"` + To Ref `json:"to"` + Scope Scope `json:"scope,omitempty"` + CreatedAt time.Time `json:"created_at"` + EffectiveAt time.Time `json:"effective_at,omitempty"` + Provenance Provenance `json:"provenance"` + Attributes map[string]string `json:"attributes,omitempty"` +} + +// Validate reports whether e satisfies the minimum shared graph contract. +func (e Edge) Validate() error { + if strings.TrimSpace(e.ID) == "" { + return fmt.Errorf("graph: edge ID is required") + } + if _, err := ParseEdgeKind(string(e.Kind)); err != nil { + return err + } + if err := e.From.Validate(); err != nil { + return fmt.Errorf("graph: edge from: %w", err) + } + if err := e.To.Validate(); err != nil { + return fmt.Errorf("graph: edge to: %w", err) + } + if e.CreatedAt.IsZero() { + return fmt.Errorf("graph: edge created_at is required") + } + return e.Provenance.Validate() +} + +// Event records an immutable lifecycle observation for a graph subject. +type Event struct { + ID string `json:"id"` + Type EventType `json:"type"` + Subject Ref `json:"subject"` + Scope Scope `json:"scope,omitempty"` + OccurredAt time.Time `json:"occurred_at"` + CorrelationID string `json:"correlation_id,omitempty"` + CausationID string `json:"causation_id,omitempty"` + IdempotencyKey string `json:"idempotency_key,omitempty"` + Provenance Provenance `json:"provenance"` +} + +// Validate reports whether e satisfies the minimum shared graph event contract. +func (e Event) Validate() error { + if strings.TrimSpace(e.ID) == "" { + return fmt.Errorf("graph: event ID is required") + } + if _, err := ParseEventType(string(e.Type)); err != nil { + return err + } + if err := e.Subject.Validate(); err != nil { + return fmt.Errorf("graph: event subject: %w", err) + } + if e.OccurredAt.IsZero() { + return fmt.Errorf("graph: event occurred_at is required") + } + return e.Provenance.Validate() +} diff --git a/graph/graph_test.go b/graph/graph_test.go new file mode 100644 index 0000000..0ce02f7 --- /dev/null +++ b/graph/graph_test.go @@ -0,0 +1,38 @@ +package graph_test + +import ( + "encoding/json" + "testing" + "time" + + "github.com/GrayCodeAI/eyrie/graph" +) + +// TestSchemaParity pins the wire schema of graph.Node to the exact JSON the +// eagle graph contract produces, so a schema drift between eagle and eyrie is +// caught at build time. +func TestSchemaParity(t *testing.T) { + n := graph.Node{ + ID: "n1", + Kind: graph.NodeOperations, + Scope: graph.Scope{RepositoryID: "repo"}, + CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + Provenance: graph.Provenance{ + Producer: "test", + }, + } + + if err := n.Validate(); err != nil { + t.Fatalf("fixture must validate: %v", err) + } + + got, err := json.Marshal(n) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + want := `{"id":"n1","kind":"operations","scope":{"repository_id":"repo"},"created_at":"2024-01-01T00:00:00Z","effective_at":"0001-01-01T00:00:00Z","provenance":{"producer":"test"}}` + if string(got) != want { + t.Fatalf("schema parity mismatch\n got: %s\nwant: %s", got, want) + } +} diff --git a/internal/observability/genai_semconv.go b/internal/observability/genai_semconv.go index 03a87b8..4053efd 100644 --- a/internal/observability/genai_semconv.go +++ b/internal/observability/genai_semconv.go @@ -3,7 +3,7 @@ // These exported constants are the canonical, ecosystem-wide attribute keys for // describing LLM / AI agent operations. They follow the OpenTelemetry GenAI // semantic conventions (gen_ai.*) and are shared as the reference set that the -// other hawk-eco repos (hawk, yaad, tok, trace) should mirror when emitting +// other hawk-eco repos (hawk, harrier, shrike, swift) should mirror when emitting // spans, so dashboards and exporters can correlate cost/usage/identity across // the whole ecosystem. // diff --git a/llm/llm_test.go b/llm/llm_test.go new file mode 100644 index 0000000..76c4ad5 --- /dev/null +++ b/llm/llm_test.go @@ -0,0 +1,30 @@ +package llm_test + +import ( + "encoding/json" + "testing" + + "github.com/GrayCodeAI/eyrie/llm" +) + +// TestLlmParity pins the wire schema of llm.EyrieMessage (with a ContentPart) +// to the exact JSON the eagle llm contract produces. +func TestLlmParity(t *testing.T) { + msg := llm.EyrieMessage{ + Role: "user", + Content: "hello", + ContentParts: []llm.ContentPart{ + {Type: "text", Text: "hi"}, + }, + } + + got, err := json.Marshal(msg) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + want := `{"role":"user","content":"hello","content_parts":[{"type":"text","text":"hi"}]}` + if string(got) != want { + t.Fatalf("schema parity mismatch\n got: %s\nwant: %s", got, want) + } +} diff --git a/llm/provider.go b/llm/provider.go new file mode 100644 index 0000000..7e1cd75 --- /dev/null +++ b/llm/provider.go @@ -0,0 +1,402 @@ +package llm + +import ( + "context" + "encoding/json" + "time" +) + +// Provider is hawk's hawk-owned view of the provider engine: a composition of +// the role interfaces below. It is the single integration surface — hawk never +// holds an *eyrieengine.Engine, and eyrie never imports hawk/internal. +// +// Callers that need only a subset depend on the relevant role interface +// directly (e.g. session_factory depends only on Generator), keeping the +// declared dependency precise and the test stub small. +type Provider interface { + Generator + ModelCatalog + CredentialManager + SelectionManager + GatewayInspector + CatalogMaintenance + NativeCompactor +} + +// Generator is the chat transport facet: the only part the ChatClient path uses. +// +// Stream returns a pull-based EventStreamer (the host engine facade contract). +// Lower-level channel-based streaming still uses StreamResult on the client +// transport layer; that type is intentionally not part of this host port. +type Generator interface { + Generate(ctx context.Context, req GenerateRequest) (*EyrieResponse, error) + Stream(ctx context.Context, req GenerateRequest) (EventStreamer, error) +} + +// EventStreamer is the pull-based host stream contract used by the engine facade. +// Next must not be called concurrently. Close is idempotent. +type EventStreamer interface { + Next() bool + Event() EyrieStreamEvent + Err() error + Close() error +} + +// GenerateRequest is the normalized generation request. +type GenerateRequest struct { + Messages []EyrieMessage + SystemPrompt string + Tools []EyrieTool + Requirements Requirements + Preference Preference + Limits Limits + Metadata Metadata + Temperature *float64 + OutputSchema string + Options GenerationOptions +} + +// Intent expresses a host's semantic preference without naming a provider. +type Intent string + +const ( + IntentFast Intent = "fast" + IntentBalanced Intent = "balanced" + IntentReasoning Intent = "reasoning" + IntentEconomical Intent = "economical" +) + +// Requirements declare what the request needs from the engine. +type Requirements struct { + Streaming bool + Tools bool + Vision bool + StructuredJSON bool + Reasoning bool + MinimumContext int `json:"minimum_context,omitempty"` +} + +// Preference declares the preferred provider/model. +type Preference struct { + Intent Intent `json:"intent,omitempty"` + PreferredProvider string `json:"-"` + PreferredModelID string `json:"preferred_model_id,omitempty"` + AllowFallback bool `json:"allow_fallback,omitempty"` + MaximumCostUSD float64 `json:"maximum_cost_usd,omitempty"` +} + +// Limits declares output limits. +type Limits struct { + MaxOutputTokens int `json:"max_output_tokens,omitempty"` + MaxContinuations int `json:"max_continuations,omitempty"` + MaxTotalOutputTokens int `json:"max_total_output_tokens,omitempty"` + Timeout time.Duration `json:"timeout,omitempty"` +} + +// Metadata carries request-scoped metadata. +type Metadata struct { + SessionID string `json:"session_id,omitempty"` + TurnID string `json:"turn_id,omitempty"` + UserID string `json:"user_id,omitempty"` + ProjectID string `json:"project_id,omitempty"` +} + +// GenerationOptions holds provider-specific generation knobs. +type GenerationOptions struct { + EnableCaching bool + ReasoningEffort string + ThinkingBudgetTokens int + ThinkingMode string + ThinkingDisplay string + ThinkingEnabled *bool + // GLMThinkingEnabled is a deprecated alias of ThinkingEnabled (Z.AI-era). + GLMThinkingEnabled *bool + VirtualKeyID string + KimiContextCacheID string + KimiCacheResetTTL bool + TopP *float64 + TopK *int + StopSequences []string + ToolChoice *ToolChoiceOption + ServiceTier string + OutputEffort string + PresencePenalty *float64 + FrequencyPenalty *float64 + N *int + LogProbs *bool + TopLogProbs *int + Seed *int + Store *bool + Metadata map[string]string + Modalities []string + AudioConfig string + Prediction string + WebSearchOptions string +} + +// ModelCatalog is the model-discovery facet (used by routing + config). +type ModelCatalog interface { + ListModels(ctx context.Context, providerID string, refresh bool) ([]Model, error) + ListLiveModels(ctx context.Context, providerID string) ([]Model, error) + ListPublicModels(ctx context.Context, providerID string) ([]Model, error) + ModelInfo(ctx context.Context, modelID string) (Model, bool, error) + ModelProviders(ctx context.Context) ([]string, error) + DefaultModel(ctx context.Context, provider, fallback string) string + PreferredModel(ctx context.Context, provider string, class ModelClass, fallback string) string + PreferredModels(ctx context.Context, primaryProvider string, class ModelClass, limit int) []string + ModelClassOf(ctx context.Context, modelID string) ModelClass + ProviderForModel(ctx context.Context, modelID string) string + PrimaryModel(ctx context.Context) string + ModelNames(ctx context.Context) []string + Catalog(ctx context.Context) (CatalogSnapshot, error) +} + +// Model is the product-facing view of model metadata. +type Model struct { + ID string `json:"id"` + ProviderID string `json:"provider_id"` + CanonicalID string `json:"canonical_id,omitempty"` + DisplayName string `json:"display_name"` + Description string `json:"description,omitempty"` + Owner string `json:"owner,omitempty"` + GatewayID string `json:"gateway_id,omitempty"` + ContextWindow int `json:"context_window,omitempty"` + MaxOutputTokens int `json:"max_output_tokens,omitempty"` + InputPricePer1M float64 `json:"input_price_per_1m,omitempty"` + OutputPricePer1M float64 `json:"output_price_per_1m,omitempty"` + PriceKnown bool `json:"price_known"` + Capabilities []string `json:"capabilities,omitempty"` + Source string `json:"source,omitempty"` + LiveMetadata json.RawMessage `json:"live_metadata,omitempty"` + + // SupportsThinkingToggle indicates this model's provider wire protocol + // honors the ThinkingEnabled toggle. Populated by the engine catalog. + SupportsThinkingToggle bool `json:"supports_thinking_toggle,omitempty"` + // DefaultThinkingEnabled is the provider-default thinking state for this + // model when no explicit user preference is set. nil means no provider + // default (fall through to global settings). + DefaultThinkingEnabled *bool `json:"default_thinking_enabled,omitempty"` +} + +// ModelClass is a provider-neutral relative model cost/capability band. +type ModelClass string + +const ( + ModelClassEconomical ModelClass = "economical" + ModelClassBalanced ModelClass = "balanced" + ModelClassPremium ModelClass = "premium" +) + +// CatalogSnapshot is an immutable, point-in-time host-facing view of a loaded +// model catalog. It is the canonical definition; eyrie's engine.CatalogSnapshot +// is a type alias to this so a single struct crosses the host boundary. +type CatalogSnapshot struct { + Models []Model `json:"models"` + CachePath string `json:"cache_path,omitempty"` + RemoteURL string `json:"remote_url,omitempty"` + Stale bool `json:"stale,omitempty"` + LoadedAt time.Time `json:"loaded_at"` +} + +// CredentialManager is the key/credential facet (config only). +type CredentialManager interface { + SaveCredential(ctx context.Context, providerID, secret string) (CredentialStatus, error) + RemoveCredential(ctx context.Context, providerID string) error + CredentialStatus(ctx context.Context, providerID string) (CredentialStatus, error) + SaveCredentialEnv(ctx context.Context, envVar, secret string) error + HasCredentialEnv(ctx context.Context, envVar string) bool + CredentialEnvKeys(providerID string) []string + ResolveCredential(ctx context.Context, secret string) CredentialResolution + CredentialProviders(context.Context) []CredentialProviderOption + ApplyCredentials(ctx context.Context, providerID string) (CatalogSnapshot, error) +} + +// CredentialStatus reports whether a provider's credential is configured. +type CredentialStatus struct { + Configured bool `json:"configured"` + ProviderID string `json:"provider_id,omitempty"` + EnvironmentVariable string `json:"environment_variable,omitempty"` + EnvironmentConflict bool `json:"environment_conflict,omitempty"` + Verified bool `json:"verified,omitempty"` + Masked string `json:"masked,omitempty"` + EnvVar string `json:"env_var,omitempty"` +} + +// CredentialResolution is the result of validating a pasted API key. +type CredentialResolution struct { + FormatOK bool `json:"format_ok"` + FormatError string `json:"format_error,omitempty"` + Providers []CredentialProviderOption + ProbeDisambiguationUsed bool `json:"probe_disambiguation_used,omitempty"` +} + +// CredentialProviderOption is one provider row in the key picker. +type CredentialProviderOption struct { + ProviderID string `json:"provider_id"` + DeploymentID string `json:"deployment_id,omitempty"` + EnvVar string `json:"env_var"` + DisplayName string `json:"display_name"` + Inferred bool `json:"inferred,omitempty"` + RequiresKey bool `json:"requires_key"` + Rank int `json:"rank"` +} + +// SelectionManager is the get/set selection facet (config only). +type SelectionManager interface { + ActiveSelection(ctx context.Context) ResolvedRoute + EffectiveSelection(ctx context.Context, opts SelectionOptions) Selection + SetActiveProvider(ctx context.Context, provider string) error + SetActiveModel(ctx context.Context, modelID string) error + SetSelection(ctx context.Context, provider, modelID string) error + ClearSelection(ctx context.Context) error +} + +// Selection is the effective provider/model pair. +type Selection struct { + Provider string `json:"provider"` + Model string `json:"model"` + HasConfiguredDeployment bool `json:"has_configured_deployment"` + DeploymentRouting bool `json:"deployment_routing"` +} + +// SelectionOptions controls effective-selection resolution. +type SelectionOptions struct { + ProviderOverride string + ModelOverride string + DeploymentRoutingOverride *bool +} + +// GatewayInspector is the gateway/deployment facet (config only). +type GatewayInspector interface { + GatewayDefinitions() []Gateway + Gateways(ctx context.Context) []Gateway + GatewayRegion(providerID string) (label string, required bool) + SetGatewayRegion(ctx context.Context, providerID, value string) error + GatewayForModel(ctx context.Context, modelID string) string + CanonicalModel(ctx context.Context, modelID string) string + DeploymentRoutingEnabled(override *bool) bool + DeploymentStatus(ctx context.Context, activeModel string) (string, error) + DeploymentSummary(ctx context.Context, activeModel string) (DeploymentSummary, error) + RoutingPreview(ctx context.Context, modelID string) (string, error) +} + +// GatewayRegionOption describes one selectable region for a regional gateway. +type GatewayRegionOption struct { + Value string `json:"value"` + DisplayName string `json:"display_name"` + Endpoint string `json:"endpoint,omitempty"` +} + +// Gateway is a provider/gateway descriptor. +type Gateway struct { + ID string `json:"id"` + DisplayName string `json:"display_name"` + DeploymentID string `json:"deployment_id,omitempty"` + CredentialEnv string `json:"credential_env"` + RequiresKey bool `json:"requires_key"` + SortOrder int `json:"sort_order"` + ChatPreference int `json:"chat_preference"` + SupportsLiveDiscovery bool `json:"supports_live_discovery"` + CredentialConfigured bool `json:"credential_configured"` + DeploymentConfigured bool `json:"deployment_configured"` + ModelCount int `json:"model_count"` + RegionLabel string `json:"region_label,omitempty"` + RegionRequired bool `json:"region_required"` + RegionOptions []GatewayRegionOption `json:"region_options,omitempty"` + DNSHost string `json:"dns_host,omitempty"` + Active bool `json:"active"` +} + +// DeploymentSummary summarizes deployment routing for a model (host-facing). +type DeploymentSummary struct { + RoutingSource string `json:"routing_source,omitempty"` + RoutingStages int `json:"routing_stages,omitempty"` + Formatted string `json:"formatted"` +} + +// CatalogMaintenance is the refresh/preflight/security facet (config only). +type CatalogMaintenance interface { + RefreshCatalog(ctx context.Context, providerID string) (CatalogSnapshot, error) + CatalogHealth(ctx context.Context) CatalogHealth + StatePaths() StatePaths + DefaultProviderFilter(ctx context.Context) string + Preflight(ctx context.Context) PreflightReport + PreflightWithOptions(ctx context.Context, opts PreflightOptions) PreflightReport + ProviderStateSecurityStatus() ProviderStateSecurity + MigrateProviderSecrets() error + MigrateProviderSecretsContext(ctx context.Context) error +} + +// CatalogHealth reports the health of the model catalog (host-facing). +type CatalogHealth struct { + Path string `json:"path"` + Exists bool `json:"exists"` + ModifiedAt time.Time `json:"modified_at,omitempty"` + Size int64 `json:"size,omitempty"` + Models int `json:"models,omitempty"` + Deployments int `json:"deployments,omitempty"` + Offerings int `json:"offerings,omitempty"` + Stale bool `json:"stale,omitempty"` + StaleAfter time.Time `json:"stale_after,omitempty"` + Source string `json:"source,omitempty"` + Error string `json:"error,omitempty"` +} + +// StatePaths is the on-disk location of catalog + provider config. +type StatePaths struct { + Catalog string `json:"catalog"` + ProviderConfig string `json:"provider_config"` +} + +// PreflightOptions configures a preflight check. +type PreflightOptions struct { + VerifyLive bool `json:"verify_live,omitempty"` +} + +// PreflightReport is the result of a preflight check. +type PreflightReport struct { + Ready bool `json:"ready"` + LiveVerified bool `json:"live_verified"` + Checks []PreflightCheck `json:"checks"` +} + +// CheckStatus is a preflight check status string. +type CheckStatus string + +// Common preflight check statuses. +const ( + CheckOK CheckStatus = "ok" + CheckFail CheckStatus = "fail" + CheckWarn CheckStatus = "warn" +) + +// PreflightCheck is one readiness check. +type PreflightCheck struct { + Name string `json:"name"` + Status CheckStatus `json:"status"` + Detail string `json:"detail,omitempty"` +} + +// ProviderStateSecurity reports security state of provider config. +type ProviderStateSecurity struct { + Path string `json:"path"` + HasSecrets bool `json:"has_secrets"` + Detail string `json:"detail,omitempty"` + Error string `json:"error,omitempty"` +} + +// NativeCompactor is the provider-native-compaction facet. +type NativeCompactor interface { + SupportsNativeCompaction(ctx context.Context, provider, model string) bool + CompactNative(ctx context.Context, req NativeCompactionRequest) (string, error) +} + +// NativeCompactionRequest is a native-compaction request. +type NativeCompactionRequest struct { + Provider string + Model string + Messages []EyrieMessage + ContextWindow int + ThresholdPct int + MaxOutputTokens int +} diff --git a/llm/types.go b/llm/types.go new file mode 100644 index 0000000..0458ac2 --- /dev/null +++ b/llm/types.go @@ -0,0 +1,239 @@ +// Package llm is the canonical provider port contract for the hawk ecosystem. +// +// It is the single source of truth for the conversation DTOs and the Provider +// interface that hawk (product face) and eyrie (provider engine) speak across +// their boundary. Both sides alias to these types, so there is exactly one +// definition of each DTO and no per-call conversion. +// +// hawk owns the product vocabulary (hence names like EyrieMessage); eyrie +// implements the port. eyrie's internal transport types stay eyrie-scoped and +// never appear here. +package llm + +import ( + "context" + + "github.com/GrayCodeAI/eyrie/tools" +) + +// EyrieConfig holds client configuration. +type EyrieConfig struct { + Provider string `json:"provider,omitempty"` + APIKey string `json:"-"` + BaseURL string `json:"base_url,omitempty"` + Model string `json:"model,omitempty"` + MaxRetries int `json:"max_retries,omitempty"` +} + +// ContentPart is a provider-neutral multimodal message part. +type ContentPart struct { + Type string `json:"type"` + Text string `json:"text,omitempty"` + ImageURL *ImageURLPart `json:"image_url,omitempty"` + InputAudio *InputAudioPart `json:"input_audio,omitempty"` +} + +// ImageURLPart describes an image URL or data URI. +type ImageURLPart struct { + URL string `json:"url"` + Detail string `json:"detail,omitempty"` +} + +// InputAudioPart describes base64-encoded audio content. +type InputAudioPart struct { + Data string `json:"data"` + Format string `json:"format"` +} + +// EyrieMessage is the provider-neutral conversation message shape. +type EyrieMessage struct { + Role string `json:"role"` + Content string `json:"content,omitempty"` + Thinking string `json:"thinking,omitempty"` + ContentParts []ContentPart `json:"content_parts,omitempty"` + Images []string `json:"images,omitempty"` + ToolUse []ToolCall `json:"tool_use,omitempty"` + ToolResults []ToolResult `json:"tool_results,omitempty"` +} + +// ToolCall is a tool invocation. Aliased to tools.ToolCall so the ecosystem +// has a single ToolCall/ToolResult vocabulary (see tools/tool.go). +type ToolCall = tools.ToolCall + +// ToolResult is a tool execution result. Aliased to tools.ToolResult. +type ToolResult = tools.ToolResult + +// EyrieTool is a tool definition. +type EyrieTool struct { + Name string `json:"name"` + Description string `json:"description"` + Parameters map[string]interface{} `json:"parameters"` +} + +// ResponseFormat specifies the desired output format for a model response. +type ResponseFormat struct { + Type string `json:"type"` + Schema string `json:"schema,omitempty"` +} + +// ToolChoiceOption controls how the model uses tools. +type ToolChoiceOption struct { + Type string `json:"type"` + Name string `json:"name,omitempty"` + DisableParallelToolUse bool `json:"disable_parallel_tool_use,omitempty"` +} + +// ChatOptions holds request options for an engine chat call. +type ChatOptions struct { + Provider string `json:"provider,omitempty"` + Model string `json:"model,omitempty"` + Temperature *float64 `json:"temperature,omitempty"` + MaxTokens int `json:"max_tokens,omitempty"` + Stream bool `json:"stream,omitempty"` + Tools []EyrieTool `json:"tools,omitempty"` + System string `json:"system,omitempty"` + EnableCaching bool `json:"enable_caching,omitempty"` + ResponseFormat *ResponseFormat `json:"response_format,omitempty"` + ReasoningEffort string `json:"reasoning_effort,omitempty"` + ThinkingBudgetTokens int `json:"thinking_budget_tokens,omitempty"` + ThinkingMode string `json:"thinking_mode,omitempty"` + ThinkingDisplay string `json:"thinking_display,omitempty"` + ThinkingEnabled *bool `json:"thinking_enabled,omitempty"` + // GLMThinkingEnabled is a deprecated alias of ThinkingEnabled kept for + // older Z.AI call sites. New code should set ThinkingEnabled. Adapters + // accept either field; wire encoding is selected per provider via + // OpenAICompatConfig.ThinkingFormat ("zai", "longcat", "agnes", …). + GLMThinkingEnabled *bool `json:"glm_thinking_enabled,omitempty"` + VirtualKeyID string `json:"virtual_key_id,omitempty"` + KimiContextCacheID string `json:"kimi_context_cache_id,omitempty"` + KimiCacheResetTTL bool `json:"kimi_cache_reset_ttl,omitempty"` + TopP *float64 `json:"top_p,omitempty"` + TopK *int `json:"top_k,omitempty"` + StopSequences []string `json:"stop_sequences,omitempty"` + ToolChoice *ToolChoiceOption `json:"tool_choice,omitempty"` + MetadataUserID string `json:"metadata_user_id,omitempty"` + ServiceTier string `json:"service_tier,omitempty"` + OutputEffort string `json:"output_effort,omitempty"` + OutputSchema string `json:"output_schema,omitempty"` + PresencePenalty *float64 `json:"presence_penalty,omitempty"` + FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"` + N *int `json:"n,omitempty"` + LogProbs *bool `json:"logprobs,omitempty"` + TopLogProbs *int `json:"top_logprobs,omitempty"` + Seed *int `json:"seed,omitempty"` + Store *bool `json:"store,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` + Modalities []string `json:"modalities,omitempty"` + AudioConfig string `json:"audio_config,omitempty"` + Prediction string `json:"prediction,omitempty"` + WebSearchOptions string `json:"web_search_options,omitempty"` + PromptCacheOptions *PromptCacheOpts `json:"prompt_cache_options,omitempty"` + CacheControl *CacheControl `json:"cache_control,omitempty"` + // Responses API extensions (Concentrate) + PreviousResponseID string `json:"previous_response_id,omitempty"` + Include []string `json:"include,omitempty"` + Routing *RoutingConfig `json:"routing,omitempty"` + MaxToolCalls *int `json:"max_tool_calls,omitempty"` + ContextManagement []ContextManagement `json:"context_management,omitempty"` + SafetyIdentifier *string `json:"safety_identifier,omitempty"` + Background *bool `json:"background,omitempty"` + Truncation *string `json:"truncation,omitempty"` + Conversation interface{} `json:"conversation,omitempty"` +} + +type PromptCacheOpts struct { + Mode string `json:"mode,omitempty"` + TTL string `json:"ttl,omitempty"` +} + +type CacheControl struct { + Type string `json:"type"` + TTL string `json:"ttl,omitempty"` +} + +type RoutingConfig struct { + Model interface{} `json:"model,omitempty"` + Strategy string `json:"strategy,omitempty"` // "min" or "max" + Metric string `json:"metric,omitempty"` // "cost", "latency", "quality" +} + +type ContextManagement struct { + Type string `json:"type"` // "compact_20260112", "clear_thinking_20251015", etc. +} + +// ContinuationConfig controls output continuation behavior. +type ContinuationConfig struct { + MaxContinuations int + MaxTotalTokens int +} + +// EyrieUsage tracks token usage. +type EyrieUsage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` + CacheCreationTokens int `json:"cache_creation_tokens,omitempty"` + CacheReadTokens int `json:"cache_read_tokens,omitempty"` + ThinkingTokens int `json:"thinking_tokens,omitempty"` +} + +// ResolvedRoute is the concrete provider/model route selected by the engine. +type ResolvedRoute struct { + Provider string `json:"provider"` + Model string `json:"model"` + DeploymentRouting bool `json:"deployment_routing,omitempty"` +} + +// EyrieResponse is the chat response DTO. +type EyrieResponse struct { + Content string `json:"content"` + Thinking string `json:"thinking,omitempty"` + Usage *EyrieUsage `json:"usage,omitempty"` + ToolCalls []ToolCall `json:"tool_calls,omitempty"` + FinishReason string `json:"finish_reason"` + RequestID string `json:"request_id,omitempty"` + OrganizationID string `json:"organization_id,omitempty"` + Route *ResolvedRoute `json:"route,omitempty"` +} + +// EyrieStreamEvent is a streaming event. +type EyrieStreamEvent struct { + Type string `json:"type"` + Content string `json:"content,omitempty"` + ToolCall *ToolCall `json:"tool_call,omitempty"` + Thinking string `json:"thinking,omitempty"` + Error string `json:"error,omitempty"` + Warning string `json:"warning,omitempty"` + RequestID string `json:"request_id,omitempty"` + Usage *EyrieUsage `json:"usage,omitempty"` + StopReason string `json:"stop_reason,omitempty"` + // TTFT and TTFTms both carry time-to-first-token in milliseconds but ride + // different events: the dedicated "ttft" event populates TTFT, while the + // terminal "done" event populates TTFTms. The engine normalizes the two + // into a single value (preferring TTFTms, falling back to TTFT). Both are + // retained for wire compatibility with existing producers/consumers. + TTFTms int `json:"ttft_ms,omitempty"` + TTFT int `json:"ttft,omitempty"` + Route *ResolvedRoute `json:"route,omitempty"` +} + +// StreamResult wraps a streaming response with cleanup. Callers must call Close() +// when done reading events, or cancel the context. +type StreamResult struct { + Events <-chan EyrieStreamEvent + RequestID string + cancel context.CancelFunc +} + +// NewStreamResult constructs a stream result. The cancel function is optional +// and must be idempotent. +func NewStreamResult(events <-chan EyrieStreamEvent, requestID string, cancel context.CancelFunc) *StreamResult { + return &StreamResult{Events: events, RequestID: requestID, cancel: cancel} +} + +// Close stops the stream and releases resources. +func (sr *StreamResult) Close() { + if sr != nil && sr.cancel != nil { + sr.cancel() + } +} diff --git a/operationsgraph/operations_graph.go b/operationsgraph/operations_graph.go index 7341c1b..4c99fe3 100644 --- a/operationsgraph/operations_graph.go +++ b/operationsgraph/operations_graph.go @@ -6,7 +6,7 @@ import ( "sync" "time" - graphcontracts "github.com/GrayCodeAI/hawk-core-contracts/graph" + graphcontracts "github.com/GrayCodeAI/eyrie/graph" ) // OperationNode represents a node in the operations graph. diff --git a/operationsgraph/projection.go b/operationsgraph/projection.go index 999296e..6ffe37d 100644 --- a/operationsgraph/projection.go +++ b/operationsgraph/projection.go @@ -12,8 +12,8 @@ import ( "strings" "time" - graphcontracts "github.com/GrayCodeAI/hawk-core-contracts/graph" - llmcontracts "github.com/GrayCodeAI/hawk-core-contracts/llm" + graphcontracts "github.com/GrayCodeAI/eyrie/graph" + llmcontracts "github.com/GrayCodeAI/eyrie/llm" ) const SchemaVersion = "eyrie.graph/v1" diff --git a/operationsgraph/projection_test.go b/operationsgraph/projection_test.go index 805b812..09f718a 100644 --- a/operationsgraph/projection_test.go +++ b/operationsgraph/projection_test.go @@ -6,9 +6,9 @@ import ( "testing" "time" + graphcontracts "github.com/GrayCodeAI/eyrie/graph" + llmcontracts "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/operationsgraph" - graphcontracts "github.com/GrayCodeAI/hawk-core-contracts/graph" - llmcontracts "github.com/GrayCodeAI/hawk-core-contracts/llm" ) func TestBuildPrivacySafeOperationsProjection(t *testing.T) { diff --git a/plans/client-package-decomposition.md b/plans/client-package-decomposition.md index e383b3f..0d0e868 100644 --- a/plans/client-package-decomposition.md +++ b/plans/client-package-decomposition.md @@ -3,7 +3,8 @@ **Status:** In Progress — Phases 1–3 implemented 2026-07-13; layering guard live **Author:** Claude (architecture review session) **Date:** 2026-07-12 -**Repos affected:** eyrie (all changes), hawk (no code changes required; re-pin external/eyrie) +**Repos affected:** eyrie (all changes), hawk (no code changes required; update +the published Eyrie module pin) ## Problem Statement @@ -196,7 +197,7 @@ Learned in Phases 1–2 (apply to later phases): | Hidden unexported coupling beyond the measured sets | med | Phases are one-cluster-at-a-time; the compiler finds every missed reference at move time; abort/expand `core` rather than weaken boundaries | | Type identity breakage for consumers doing type switches | high | Use aliases (`=`), never new named types, for everything that already exists | | Method sets split from their types | high | Methods move with their receiver's file into the same subpackage — never leave methods behind | -| external/eyrie pin drift in hawk during the refactor | low | Land phases as individual PRs; re-pin hawk after each; `make sync-external` reports drift | +| Eyrie module-pin drift in Hawk during the refactor | low | Land phases as individual PRs; update Hawk after each; `make sync` reports drift | | Facade grows stale re-exports | low | Phase 5 CI check + deprecation comments | ## References diff --git a/scripts/check-ecosystem-boundaries.sh b/scripts/check-ecosystem-boundaries.sh index af7ddc4..5d6d58b 100755 --- a/scripts/check-ecosystem-boundaries.sh +++ b/scripts/check-ecosystem-boundaries.sh @@ -5,10 +5,10 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT_DIR" # Eyrie is host-neutral: it must not depend on any Hawk package. Shared -# ecosystem vocabulary belongs in hawk-core-contracts, whose module path does +# ecosystem vocabulary belongs in eagle, whose module path does # not match this expression. FORBIDDEN_HAWK='github\.com/GrayCodeAI/hawk(/|")' -FORBIDDEN_ENGINES='github\.com/GrayCodeAI/(yaad|tok|trace|sight|inspect)(/|")' +FORBIDDEN_ENGINES='github\.com/GrayCodeAI/(harrier|shrike|swift|kestrel|merlin)(/|")' exit_code=0 @@ -24,7 +24,7 @@ if [[ -n "${violations}" ]]; then echo "forbidden Hawk imports found:" echo "${violations}" echo - echo "eyrie must use hawk-core-contracts or local contracts, never the Hawk product module" + echo "eyrie must use eagle or local contracts, never the Hawk product module" exit_code=1 fi diff --git a/scripts/check-no-replace-directives.sh b/scripts/check-no-replace-directives.sh index b813b15..8a8d844 100755 --- a/scripts/check-no-replace-directives.sh +++ b/scripts/check-no-replace-directives.sh @@ -8,7 +8,7 @@ if grep -qE '^replace .+ => \.\./' go.mod; then grep -nE '^replace .+ => \.\./' go.mod echo "" echo "Local replace directives must be removed before tagging a release." - echo "For a release, the published module version of hawk-core-contracts must be used." + echo "For a release, the published module version of eagle must be used." exit 1 fi echo "OK: no local replace directives in go.mod." diff --git a/tools/tool.go b/tools/tool.go new file mode 100644 index 0000000..87545a1 --- /dev/null +++ b/tools/tool.go @@ -0,0 +1,15 @@ +package tools + +// ToolCall represents a provider-neutral tool invocation contract. +type ToolCall struct { + ID string `json:"id,omitempty"` + Name string `json:"name"` + Arguments map[string]interface{} `json:"arguments"` +} + +// ToolResult represents a provider-neutral tool execution result contract. +type ToolResult struct { + ToolUseID string `json:"tool_use_id"` + Content string `json:"content"` + IsError bool `json:"is_error,omitempty"` +} diff --git a/tools/tools_test.go b/tools/tools_test.go new file mode 100644 index 0000000..f48ec72 --- /dev/null +++ b/tools/tools_test.go @@ -0,0 +1,43 @@ +package tools_test + +import ( + "encoding/json" + "testing" + + "github.com/GrayCodeAI/eyrie/tools" +) + +// TestToolsParity pins the wire schema of tools.ToolCall and tools.ToolResult +// to the exact JSON the eagle tools contract produces. +func TestToolsParity(t *testing.T) { + call := tools.ToolCall{ + ID: "tc1", + Name: "search", + Arguments: map[string]interface{}{"q": "go"}, + } + + gotCall, err := json.Marshal(call) + if err != nil { + t.Fatalf("marshal ToolCall: %v", err) + } + + wantCall := `{"id":"tc1","name":"search","arguments":{"q":"go"}}` + if string(gotCall) != wantCall { + t.Fatalf("ToolCall schema parity mismatch\n got: %s\nwant: %s", gotCall, wantCall) + } + + result := tools.ToolResult{ + ToolUseID: "tc1", + Content: "found", + } + + gotResult, err := json.Marshal(result) + if err != nil { + t.Fatalf("marshal ToolResult: %v", err) + } + + wantResult := `{"tool_use_id":"tc1","content":"found"}` + if string(gotResult) != wantResult { + t.Fatalf("ToolResult schema parity mismatch\n got: %s\nwant: %s", gotResult, wantResult) + } +} diff --git a/tools/versioning.go b/tools/versioning.go new file mode 100644 index 0000000..9d4f3ce --- /dev/null +++ b/tools/versioning.go @@ -0,0 +1,144 @@ +package tools + +import "fmt" + +// BehaviorPreset selects a back-compat behavior contract for tool execution. +// FinalizeToolConfigRequest.behavior_version carries one of these presets so a +// single locked configuration can keep older clients working without the server +// having to special-case version strings. +type BehaviorPreset string + +const ( + // BehaviorPresetUnspecified is the zero value; a finalize call SHOULD reject + // it and require an explicit choice. + BehaviorPresetUnspecified BehaviorPreset = "" + // BehaviorPresetCurrent is the actively-supported behavior contract. + BehaviorPresetCurrent BehaviorPreset = "current" + // BehaviorPresetLegacy selects the prior behavior contract for back-compat + // with clients that haven't adopted BehaviorPresetCurrent. + BehaviorPresetLegacy BehaviorPreset = "legacy" +) + +// BehaviorPresetFrom parses a behavior_version string into a BehaviorPreset. +// Returns an error for any unrecognized value — an empty or unknown preset is +// invalid, not silently defaulted (matches the closed-enum forward-safety rule +// applied to ToolNamespace). +func BehaviorPresetFrom(s string) (BehaviorPreset, error) { + switch BehaviorPreset(s) { + case BehaviorPresetCurrent, BehaviorPresetLegacy, BehaviorPresetUnspecified: + return BehaviorPreset(s), nil + default: + return BehaviorPresetUnspecified, fmt.Errorf("tools: unknown behavior preset %q", s) + } +} + +// FinalizeErrorCode classifies a finalize warning or violation, mirroring the +// FINALIZE_ERROR_CODE enum in proto/hawk/contracts/v1/tool.proto. +type FinalizeErrorCode int + +const ( + FinalizeErrorCodeUnspecified FinalizeErrorCode = iota // unspecified + FinalizeErrorCodeUnknownTool // unknown tool id + FinalizeErrorCodeIncompatibleVersion // behavior_version incompatible with enabled_tools + FinalizeErrorCodeDeprecatedReplaced // tool deprecated/replaced + FinalizeErrorCodeInvalidParams // invalid tool parameters +) + +var finalizeErrorCodeNames = [...]string{ + "unspecified", + "unknown_tool", + "incompatible_version", + "deprecated_replaced", + "invalid_params", +} + +func (c FinalizeErrorCode) String() string { + if int(c) >= 0 && int(c) < len(finalizeErrorCodeNames) { + return finalizeErrorCodeNames[c] + } + return "unknown" +} + +// VersionWarning is a non-fatal, behavior-version drift reported by finalize. +type VersionWarning struct { + Code FinalizeErrorCode `json:"code"` + Message string `json:"message"` + AffectedTools []string `json:"affected_tools,omitempty"` +} + +// FinalizeConfigViolation is a fatal, deterministic problem that prevents +// finalize from succeeding. +type FinalizeConfigViolation struct { + Code FinalizeErrorCode `json:"code"` + ToolID string `json:"tool_id"` + Message string `json:"message"` +} + +// FinalizeResult is the decoded outcome of a FinalizeToolConfig call. +type FinalizeResult struct { + BehaviorVersion string `json:"behavior_version"` + Warnings []VersionWarning `json:"warnings,omitempty"` + Violations []FinalizeConfigViolation `json:"violations,omitempty"` + Finalized bool `json:"finalized"` +} + +// Ok reports whether finalize succeeded and the tool set is safe to call: +// Finalized must be true AND there must be no fatal violations. Warnings alone +// do not make Ok return false. +func (r FinalizeResult) Ok() bool { + return r.Finalized && len(r.Violations) == 0 +} + +// ToolMeta is the canonical identity envelope attached to tool-call events, +// mirroring the ToolMeta message in proto/hawk/contracts/v1/tool.proto. +// version is an additive-only bump: new additive fields do NOT change it. +type ToolMeta struct { + Version string `json:"version"` + Name string `json:"name"` + Kind string `json:"kind"` + Namespace ToolNamespace `json:"namespace"` + Label string `json:"label,omitempty"` + ReadOnly bool `json:"read_only"` +} + +// ToolNamespace is a CLOSED enum identifying the harness that owns a tool, +// mirroring the ToolNamespace enum in proto/hawk/contracts/v1/tool.proto. A new +// unknown namespace is a wire-breaking change that intentionally fails +// ToolNamespaceFrom (forward-safety): a deploy that rolls the contract forward +// before the consumer code cannot silently mis-route a tool it doesn't +// understand. ToolNamespaceAcp is reserved for the forthcoming hawk-acp repo. +type ToolNamespace string + +const ( + ToolNamespaceUnspecified ToolNamespace = "" // unspecified + ToolNamespaceHawkBuild ToolNamespace = "hawk_build" // hawk + ToolNamespaceHawkBuildConcise ToolNamespace = "hawk_build_concise" + ToolNamespaceCodex ToolNamespace = "codex" // codex harness + ToolNamespaceOpencode ToolNamespace = "opencode" // opencode harness + ToolNamespaceMcp ToolNamespace = "mcp" // MCP servers (falcon) + ToolNamespaceAcp ToolNamespace = "acp" // reserved: hawk-acp +) + +// ToolNamespaceFrom parses a namespace string into a ToolNamespace. The set is +// closed: an unrecognized value returns an error rather than silently mapping +// to ToolNamespaceUnspecified, so a new namespace on the wire fails strict +// typed deserialization (see the closed-enum forward-safety rule above). +func ToolNamespaceFrom(s string) (ToolNamespace, error) { + switch ToolNamespace(s) { + case ToolNamespaceUnspecified, ToolNamespaceHawkBuild, + ToolNamespaceHawkBuildConcise, ToolNamespaceCodex, + ToolNamespaceOpencode, ToolNamespaceMcp, ToolNamespaceAcp: + return ToolNamespace(s), nil + default: + return ToolNamespaceUnspecified, fmt.Errorf("tools: unknown tool namespace %q", s) + } +} + +// String returns the namespace string as-is, mapping the unspecified zero +// value to "unspecified" so log output and JSON stay unambiguous. +func (n ToolNamespace) String() string { + if n == ToolNamespaceUnspecified { + return "unspecified" + } + return string(n) +} From 83ee376fec72f2b5e33aa55a8de8f087779208fa Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Wed, 2 Sep 2026 19:28:57 +0530 Subject: [PATCH 2/2] style: apply gofumpt formatting --- client/adapters/anthropic.go | 2 +- client/adapters/anthropic_base.go | 2 +- client/adapters/azure.go | 2 +- client/adapters/bedrock.go | 2 +- client/adapters/concentrate_responses.go | 2 +- client/adapters/gemini.go | 2 +- client/adapters/openai.go | 2 +- client/adapters/protocol_router.go | 2 +- client/adapters/protocol_router_test.go | 2 +- client/adapters/vertex.go | 2 +- client/aliases.go | 2 +- client/embeddings/cache_test.go | 2 +- engine/control_plane.go | 2 +- engine/convert_test.go | 2 +- engine/engine.go | 2 +- engine/engine_test.go | 2 +- engine/model_policy.go | 2 +- engine/model_policy_custom_test.go | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/client/adapters/anthropic.go b/client/adapters/anthropic.go index edfb1d1..74e6a3d 100644 --- a/client/adapters/anthropic.go +++ b/client/adapters/anthropic.go @@ -10,8 +10,8 @@ import ( "net/http" "strings" - "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" + "github.com/GrayCodeAI/eyrie/llm" ) // maxAnthropicRequestSize is the maximum request body size for the Messages API (32 MB). diff --git a/client/adapters/anthropic_base.go b/client/adapters/anthropic_base.go index 6295a2f..46809cd 100644 --- a/client/adapters/anthropic_base.go +++ b/client/adapters/anthropic_base.go @@ -3,8 +3,8 @@ package adapters import ( "strings" - "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" + "github.com/GrayCodeAI/eyrie/llm" ) // AnthropicBaseFromOpenAIV1 strips a trailing /v1 from an OpenAI-compatible base URL. diff --git a/client/adapters/azure.go b/client/adapters/azure.go index 0271eb7..addf5ae 100644 --- a/client/adapters/azure.go +++ b/client/adapters/azure.go @@ -10,8 +10,8 @@ import ( "net/http" "strings" - "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" + "github.com/GrayCodeAI/eyrie/llm" ) const ( diff --git a/client/adapters/bedrock.go b/client/adapters/bedrock.go index 7115b8c..9f238da 100644 --- a/client/adapters/bedrock.go +++ b/client/adapters/bedrock.go @@ -18,8 +18,8 @@ import ( "strings" "time" - "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" + "github.com/GrayCodeAI/eyrie/llm" ) const ( diff --git a/client/adapters/concentrate_responses.go b/client/adapters/concentrate_responses.go index c62e627..59d576c 100644 --- a/client/adapters/concentrate_responses.go +++ b/client/adapters/concentrate_responses.go @@ -12,8 +12,8 @@ import ( "strings" "time" - "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" + "github.com/GrayCodeAI/eyrie/llm" ) // ConcentrateResponsesClient uses the Concentrate Responses API (the production-ready diff --git a/client/adapters/gemini.go b/client/adapters/gemini.go index fb5d0a7..71d8d30 100644 --- a/client/adapters/gemini.go +++ b/client/adapters/gemini.go @@ -11,8 +11,8 @@ import ( "os" "strings" - "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" + "github.com/GrayCodeAI/eyrie/llm" ) // geminiSharedParserEnvVar is the opt-out flag for the new diff --git a/client/adapters/openai.go b/client/adapters/openai.go index c415651..2173ad1 100644 --- a/client/adapters/openai.go +++ b/client/adapters/openai.go @@ -10,8 +10,8 @@ import ( "net/http" "strings" - "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" + "github.com/GrayCodeAI/eyrie/llm" ) const ( diff --git a/client/adapters/protocol_router.go b/client/adapters/protocol_router.go index c3eb40d..1c9ece0 100644 --- a/client/adapters/protocol_router.go +++ b/client/adapters/protocol_router.go @@ -5,8 +5,8 @@ import ( "fmt" "strings" - "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" + "github.com/GrayCodeAI/eyrie/llm" ) // ChatProtocol selects which existing eyrie client handles a gateway request. diff --git a/client/adapters/protocol_router_test.go b/client/adapters/protocol_router_test.go index d7db241..6c5aaa8 100644 --- a/client/adapters/protocol_router_test.go +++ b/client/adapters/protocol_router_test.go @@ -6,8 +6,8 @@ import ( "net/http" "testing" - "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" + "github.com/GrayCodeAI/eyrie/llm" ) func TestNewStreamWithReasoningFallbackChatFirst(t *testing.T) { diff --git a/client/adapters/vertex.go b/client/adapters/vertex.go index 8ef683f..f5db75f 100644 --- a/client/adapters/vertex.go +++ b/client/adapters/vertex.go @@ -9,8 +9,8 @@ import ( "log/slog" "net/http" - "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" + "github.com/GrayCodeAI/eyrie/llm" ) // maxVertexRequestSize is the maximum request body size for Vertex AI (30 MB). diff --git a/client/aliases.go b/client/aliases.go index 1f38e40..cacb395 100644 --- a/client/aliases.go +++ b/client/aliases.go @@ -5,10 +5,10 @@ import ( "net/http" "time" - "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/adapters" "github.com/GrayCodeAI/eyrie/client/core" "github.com/GrayCodeAI/eyrie/client/embeddings" + "github.com/GrayCodeAI/eyrie/llm" ) // The provider contract and request/response data types live in diff --git a/client/embeddings/cache_test.go b/client/embeddings/cache_test.go index 499e389..81ba42d 100644 --- a/client/embeddings/cache_test.go +++ b/client/embeddings/cache_test.go @@ -6,8 +6,8 @@ import ( "sync" "testing" - "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client/core" + "github.com/GrayCodeAI/eyrie/llm" ) // echoMock is a minimal core.Provider that echoes the last user message and diff --git a/engine/control_plane.go b/engine/control_plane.go index 4cbe216..da170a7 100644 --- a/engine/control_plane.go +++ b/engine/control_plane.go @@ -6,11 +6,11 @@ import ( "sort" "strings" - llm "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/catalog" "github.com/GrayCodeAI/eyrie/catalog/registry" "github.com/GrayCodeAI/eyrie/config" "github.com/GrayCodeAI/eyrie/credentials" + llm "github.com/GrayCodeAI/eyrie/llm" ) // ResolveCredential validates credential input and returns safe provider diff --git a/engine/convert_test.go b/engine/convert_test.go index a0b6260..81b6674 100644 --- a/engine/convert_test.go +++ b/engine/convert_test.go @@ -3,8 +3,8 @@ package engine import ( "testing" - llm "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/client" + llm "github.com/GrayCodeAI/eyrie/llm" ) func TestToClientMessages_ReturnsMessagesUnchanged(t *testing.T) { diff --git a/engine/engine.go b/engine/engine.go index 8e5a4a7..8b64778 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -11,12 +11,12 @@ import ( "sync" "time" - "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/catalog" "github.com/GrayCodeAI/eyrie/catalog/registry" "github.com/GrayCodeAI/eyrie/client" "github.com/GrayCodeAI/eyrie/config" "github.com/GrayCodeAI/eyrie/credentials" + "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/setup" ) diff --git a/engine/engine_test.go b/engine/engine_test.go index c51f576..cc996db 100644 --- a/engine/engine_test.go +++ b/engine/engine_test.go @@ -6,11 +6,11 @@ import ( "path/filepath" "testing" - "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/catalog" "github.com/GrayCodeAI/eyrie/client" "github.com/GrayCodeAI/eyrie/config" "github.com/GrayCodeAI/eyrie/credentials" + "github.com/GrayCodeAI/eyrie/llm" ) func TestNewUsesInjectedCredentialStore(t *testing.T) { diff --git a/engine/model_policy.go b/engine/model_policy.go index 452bf29..aa2fac4 100644 --- a/engine/model_policy.go +++ b/engine/model_policy.go @@ -5,8 +5,8 @@ import ( "sort" "strings" - "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/catalog" + "github.com/GrayCodeAI/eyrie/llm" ) func (e *Engine) policyCatalog(ctx context.Context) (*catalog.CompiledCatalog, error) { diff --git a/engine/model_policy_custom_test.go b/engine/model_policy_custom_test.go index 0888d53..39df3c9 100644 --- a/engine/model_policy_custom_test.go +++ b/engine/model_policy_custom_test.go @@ -7,9 +7,9 @@ import ( "slices" "testing" - "github.com/GrayCodeAI/eyrie/llm" "github.com/GrayCodeAI/eyrie/catalog" "github.com/GrayCodeAI/eyrie/credentials" + "github.com/GrayCodeAI/eyrie/llm" ) func TestModelPolicyIncludesInvocationScopedCustomGateways(t *testing.T) {