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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- `VERSION` file now reports `0.1.7` (was left at `0.1.6` after the llm
package release; `version.go` embeds this file).
- `llm.ToolCall` / `llm.ToolResult` are type aliases of `tools.ToolCall` /
`tools.ToolResult` so the ecosystem has one tool-call vocabulary.
- Host-facing `llm` types aligned with the eyrie engine facade:
`CatalogHealth`, `DeploymentSummary`, `PreflightReport` (`LiveVerified`),
`ProviderStateSecurity`, and pull-based `EventStreamer` on `Generator.Stream`.
- `proto/hawk/contracts/v1/llm.proto` mirrors llm conversation / host DTOs;
`make proto` regenerates agent + llm stubs under `gen/`.
- Unit tests for `llm` (`StreamResult`, aliases, constants, `EventStreamer`).

## [0.1.7] — 2026-07-22

### Added
Expand Down
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ go get github.com/GrayCodeAI/hawk-core-contracts
| `verify/` | `Report`, `Finding` | Neutral verification findings, stats, and report contracts |
| `sessions/` | `Phase`, `CostAccumulator`, `ParsePhase` | Cross-repo agent session state types |
| `agent/` | `SpawnRequest`, `SpawnResult`, hook event names | Typed subagent spawn + hook vocabulary |
| `llm/` | `Provider` + roles, `EyrieMessage`, `EyrieResponse`, `ChatOptions`, `StreamEvent`, `StreamResult`, `Model`, `Usage`, … | Canonical provider port contract — the hawk↔eyrie boundary |
| `llm/` | `Provider` + roles, `EventStreamer`, `EyrieMessage`, `EyrieResponse`, `ChatOptions`, `EyrieStreamEvent`, `StreamResult`, `Model`, `Usage`, … | Canonical provider port contract — the hawk↔eyrie boundary (`ToolCall`/`ToolResult` alias `tools/`) |

## Architecture

Expand All @@ -40,9 +40,15 @@ hawk-core-contracts (stdlib only)
├── verify/ Report, Finding — verification report contracts
├── sessions/ Phase, CostAccumulator — session state contracts
├── agent/ SpawnRequest, SpawnResult, hook events — subagent spawn contracts
└── llm/ Provider + 7 roles, conversation DTOs — the hawk↔eyrie port contract
└── llm/ Provider + 7 roles, EventStreamer, conversation DTOs — hawk↔eyrie port
```

`llm.ToolCall` / `llm.ToolResult` are type aliases of `tools.ToolCall` /
`tools.ToolResult` (single vocabulary). Host streaming on the port is
pull-based (`EventStreamer`); channel-based `StreamResult` remains for
lower-level client transports.


## Scope

### Allowed here
Expand All @@ -64,8 +70,9 @@ hawk-core-contracts (stdlib only)
- anything that belongs in a single consuming repo

Engines should depend on this repo only when they produce or consume a shared
cross-repo contract. Contract-free engines (e.g., `eyrie`, `yaad`, `trace`)
should not add the dependency just for consistency.
cross-repo contract. Eyrie depends on `llm/` for the host port; other engines
(e.g. `yaad`, `trace`) stay contract-free unless they share a real cross-repo
type.

If a type is only used inside one repo, it should stay in that repo.

Expand Down Expand Up @@ -186,6 +193,8 @@ numeric values identical to the Go `iota` constants that
| `/review/` | `@GrayCodeAI/llm-team` |
| `/verify/` | `@GrayCodeAI/llm-team` |
| `/sessions/` | `@GrayCodeAI/llm-team` |
| `/agent/` | `@GrayCodeAI/llm-team` |
| `/llm/` | `@GrayCodeAI/llm-team` |
| `/proto/` | `@GrayCodeAI/llm-team` |
| `/VERSION` | `@GrayCodeAI/maintainers` |
| `/Makefile` | `@GrayCodeAI/devops-team` |
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.6
0.1.7
77 changes: 50 additions & 27 deletions llm/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,22 @@ type Provider interface {
}

// 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) (*StreamResult, 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.
Expand Down Expand Up @@ -281,12 +294,11 @@ type Gateway struct {
Active bool `json:"active"`
}

// DeploymentSummary summarizes deployment routing for a model.
// DeploymentSummary summarizes deployment routing for a model (host-facing).
type DeploymentSummary struct {
// mirrors eyrieengine.DeploymentSummary
ActiveModel string `json:"active_model"`
Status string `json:"status"`
Router string `json:"router,omitempty"`
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).
Expand All @@ -302,12 +314,19 @@ type CatalogMaintenance interface {
MigrateProviderSecretsContext(ctx context.Context) error
}

// CatalogHealth reports the health of the model catalog.
// CatalogHealth reports the health of the model catalog (host-facing).
type CatalogHealth struct {
Healthy bool `json:"healthy"`
Path string `json:"path,omitempty"`
Models int `json:"models,omitempty"`
Error string `json:"error,omitempty"`
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.
Expand All @@ -318,35 +337,39 @@ type StatePaths struct {

// PreflightOptions configures a preflight check.
type PreflightOptions struct {
VerifyLive bool
VerifyLive bool `json:"verify_live,omitempty"`
}

// PreflightReport is the result of a preflight check.
type PreflightReport struct {
Ready bool `json:"ready"`
Checks []PreflightCheck `json:"checks"`
Ready bool `json:"ready"`
LiveVerified bool `json:"live_verified"`
Checks []PreflightCheck `json:"checks"`
}

// PreflightCheck is one readiness check.
type PreflightCheck struct {
Name string `json:"name"`
Status string `json:"status"`
Detail string `json:"detail,omitempty"`
}
// CheckStatus is a preflight check status string.
type CheckStatus string

// Common preflight check statuses.
const (
CheckOK = "ok"
CheckFail = "fail"
CheckWarn = "warn"
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 {
PlatformStore string `json:"platform_store,omitempty"`
Sanitized bool `json:"sanitized"`
Detail string `json:"detail,omitempty"`
Error string `json:"error,omitempty"`
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.
Expand Down
23 changes: 10 additions & 13 deletions llm/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
// never appear here.
package llm

import "context"
import (
"context"

"github.com/GrayCodeAI/hawk-core-contracts/tools"
)

// EyrieConfig holds client configuration.
type EyrieConfig struct {
Expand Down Expand Up @@ -52,19 +56,12 @@ type EyrieMessage struct {
ToolResults []ToolResult `json:"tool_results,omitempty"`
}

// ToolCall is a tool invocation.
type ToolCall struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Arguments map[string]interface{} `json:"arguments"`
}
// 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.
type ToolResult struct {
ToolUseID string `json:"tool_use_id"`
Content string `json:"content"`
IsError bool `json:"is_error,omitempty"`
}
// ToolResult is a tool execution result. Aliased to tools.ToolResult.
type ToolResult = tools.ToolResult

// EyrieTool is a tool definition.
type EyrieTool struct {
Expand Down
98 changes: 98 additions & 0 deletions llm/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package llm

import (
"context"
"testing"

"github.com/GrayCodeAI/hawk-core-contracts/tools"
)

func TestToolCallAliasesToolsPackage(t *testing.T) {
// Ensure llm.ToolCall and tools.ToolCall are the same type (not merely similar):
// tools.ToolCall must be assignable to llm.ToolCall without conversion.
assertLLMToolCall(t, tools.ToolCall{
ID: "t1",
Name: "read",
Arguments: map[string]interface{}{
"path": "main.go",
},
})
assertLLMToolResult(t, tools.ToolResult{
ToolUseID: "t1",
Content: "ok",
IsError: false,
})
}

func assertLLMToolCall(t *testing.T, call ToolCall) {
t.Helper()
if call.ID != "t1" || call.Name != "read" {
t.Fatalf("ToolCall alias lost fields: %+v", call)
}
}

func assertLLMToolResult(t *testing.T, result ToolResult) {
t.Helper()
if result.ToolUseID != "t1" || result.Content != "ok" {
t.Fatalf("ToolResult alias lost fields: %+v", result)
}
}

func TestNewStreamResultAndClose(t *testing.T) {
events := make(chan EyrieStreamEvent, 1)
events <- EyrieStreamEvent{Type: "content", Content: "hi"}
close(events)

cancelled := false
sr := NewStreamResult(events, "req-1", func() { cancelled = true })
if sr.RequestID != "req-1" {
t.Fatalf("RequestID = %q, want req-1", sr.RequestID)
}
got := <-sr.Events
if got.Content != "hi" {
t.Fatalf("event content = %q, want hi", got.Content)
}
sr.Close()
if !cancelled {
t.Fatal("Close did not invoke cancel")
}
// Close is safe when cancel is nil.
NewStreamResult(nil, "", nil).Close()
}

func TestStreamResultCloseNilReceiver(t *testing.T) {
var sr *StreamResult
sr.Close() // must not panic
}

func TestIntentAndModelClassConstants(t *testing.T) {
if IntentFast == "" || IntentBalanced == "" || IntentReasoning == "" || IntentEconomical == "" {
t.Fatal("Intent constants must be non-empty")
}
if ModelClassEconomical == "" || ModelClassBalanced == "" || ModelClassPremium == "" {
t.Fatal("ModelClass constants must be non-empty")
}
if CheckOK != "ok" || CheckFail != "fail" || CheckWarn != "warn" {
t.Fatalf("check statuses: ok=%q fail=%q warn=%q", CheckOK, CheckFail, CheckWarn)
}
}

func TestEventStreamerInterfaceShape(t *testing.T) {
// Compile-time: a stub that matches EventStreamer keeps the host port stable.
var _ EventStreamer = stubStreamer{}
}

type stubStreamer struct{}

func (stubStreamer) Next() bool { return false }
func (stubStreamer) Event() EyrieStreamEvent { return EyrieStreamEvent{} }
func (stubStreamer) Err() error { return nil }
func (stubStreamer) Close() error { return nil }

func TestGenerateRequestZeroValue(t *testing.T) {
var req GenerateRequest
if req.Messages != nil || req.SystemPrompt != "" {
t.Fatalf("unexpected zero GenerateRequest: %+v", req)
}
_ = context.Background()
}
Loading
Loading