ci: restore the image build under .hanzo/workflows

Every workflow in this repo was deleted and nothing replaced them, so the sole
producer of ghcr.io/hanzoai/agent no longer existed anywhere and there was no CI
at all. That is silent staleness: no failing run, just an artifact that quietly
stops being rebuilt.

.github/workflows now holds sync.yml and nothing else, copied verbatim from
universe infra/ci/sync.yml. It replaces a mirror-nudge file that POSTed
/api/v1/repos/.../mirror-sync and described git.hanzo.ai as a downstream pull
mirror; the forge is canonical and refs are pushed to it. Without it a
.hanzo/workflows file has nothing to trigger it.

deploy.yml restores docker-publish.yml. Two things could not come across as-is:

- Its body was one `uses: hanzoai/.github/.github/workflows/docker-build.yml@main`.
  A private cross-repo reusable workflow does not resolve on the forge, so the
  amd64 build/push is vendored inline, as hanzoai/bot already does.
- It built `context: control-plane`, and no control-plane directory exists on
  main; the orchestrator was hoisted to the root module in v0.1.1 and the
  directory survives only on the unmerged consolidate/held-work branch.
  Restoring that context restores a build that cannot run. It builds
  Dockerfile.sdk — the one Dockerfile on main whose context exists, previously
  referenced by nothing despite sdk-* tags being live on the image — and tags it
  sdk-* so the two Dockerfiles cannot collide in one tag namespace.

The tag rule is shell rather than docker/metadata-action, so it is two readable
lines with no action semantics to guess at, and it was tested standalone:
v* -> :sdk-X.Y.Z + :sdk-latest, main -> :sdk-sha-<sha7> + :sdk-latest.

ci.yml replaces tests.yml and test.yml, which checked the same things twice, the
second with `|| true` on most steps so it could not fail. It gates what is
verifiable on main today — gofmt/build/vet/test for the Go module, and the
locked install plus `import agents` for Python, which is what Dockerfile.sdk's
builder stage and HEALTHCHECK do. It deliberately does not run ruff, mypy or
pytest: `make sync` cannot resolve (requires-python >=3.9, a transitive numpy
needs >=3.10), ruff reports 2337 errors, and pytest INTERNALERRORs at collection
because a test module sys.exit(1)s at import. tests.yml ran `make sync` first in
all five jobs, so none of them could have gone green. Those four are written down
in LLM.md as work rather than shipped as a permanently red check, which gets read
as broken CI instead of broken code.

gofmt over eight files so that gate is real instead of omitted; build, vet and
test re-run green afterwards. Alignment whitespace only.

Not restored, with reasons in LLM.md: docs.yml (mkdocs gh-deploy is GitHub Pages,
never published anything, and Pages is not how we serve sites), publish.yml
(pointed at the PyPI package hanzoai/python-sdk owns; deliberately removed
earlier), issues.yml (upstream's stale-issue bot).

LLM.md documents the chain, records the four broken Python checks, and states
the open question this repo actually has: nothing in hanzoai/universe references
ghcr.io/hanzoai/agent, so the image has no consumer and no App CR.

.gitignore ignored .hanzo/ under an "# IDE" heading, which silently dropped
both new workflows from the commit — the same silent-staleness shape this is
meant to remove, one level down. Narrowed to .hanzo/* with .hanzo/workflows/
re-included.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-07-27 09:01:00 -07:00
co-authored by Hanzo Dev
parent ddb88d54a9
commit d5ce906725
13 changed files with 473 additions and 170 deletions
+87 -45
View File
@@ -1,60 +1,102 @@
name: Sync to git.hanzo.ai
# DIRECTION: this repo on git.hanzo.ai is a PULL MIRROR fetching github.com/hanzoai/agent.
# GitHub is UPSTREAM, git.hanzo.ai is DOWNSTREAM. The giveaway is what this file does:
# mirror-sync tells Gitea to PULL, which only makes sense if GitHub is the source.
name: sync
# Carries pushed refs to git.hanzo.ai, the canonical forge, and does nothing
# else: build, test and release run natively there.
#
# This matters beyond labeling: a pull mirror force-updates its refs from upstream on
# every fetch, so anything committed directly on git.hanzo.ai is DISCARDED on the next
# sync. Do not treat the mirror as writable, and do not reword this into a "native on
# git.hanzo.ai" claim — that is the intended end state, not today's.
#
# What IS true: CI/CD runs on git.hanzo.ai — arcd executes .hanzo/workflows/ on the
# Hanzo git-runner. Making git.hanzo.ai canonical means converting the repo
# pull-mirror -> native and flipping the write path; an ops migration, not a config edit.
#
# So this nudge is a LATENCY fix, not a canonicality one: it asks Gitea to pull now
# instead of waiting out the ~10-minute mirror interval, so arcd sees the new commit
# sooner. Without this file the repo still mirrors — just later.
#
# IDEMPOTENT: mirror-sync just tells Gitea to pull HEAD, so N nudges == 1 (same latest
# state); concurrency coalesces a burst of pushes to a single in-flight sync. FAST: no
# checkout, one bounded curl, hard timeouts. FAIL-SOFT: a missing token or hiccup never
# fails the push (the App webhook still mirrors). Set HANZO_GIT_TOKEN to enable the nudge.
# Repo-agnostic. owner/name come from ${{ github.repository }}, so this file is
# dropped into every repo unchanged as .github/workflows/sync.yml and is the
# only workflow GitHub is permitted to run.
on:
push:
branches: [main]
# '**' matches slashes; '*' does not, and would skip feature/foo.
branches: ['**']
tags: ['**']
workflow_dispatch:
# Only the newest push's sync needs to run — cancel any older in-flight one (it would
# pull a now-stale HEAD). This is what makes rapid pushes idempotent + cheap.
# Serialize per ref. Never cancel: a cancelled run drops that ref and nothing
# reconciles it afterwards.
concurrency:
group: sync-git-hanzo-ai
cancel-in-progress: true
group: sync-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
jobs:
sync:
# `.github/workflows` is also on Gitea's WORKFLOW_DIRS, so with Actions enabled on
# the mirror this file runs THERE too on every sync — where asking git.hanzo.ai to
# pull is circular. github.server_url is setting.AppURL on Gitea
# (services/actions/context.go:89), so this is the exact test for "am I upstream".
if: ${{ github.server_url == 'https://github.com' }}
# .github/workflows is also on the forge's own workflow search path, so this
# file executes there too in any repo without .hanzo/workflows, where it
# would push the forge to itself. server_url is the forge's AppURL, which
# makes this the test for "running on GitHub".
if: github.server_url == 'https://github.com'
# GitHub-hosted on purpose. This is the one workflow that must run ON GitHub,
# and the self-hosted pool that used to serve it is retired — a job asking for
# it now queues forever against a label nothing answers. Carrying refs is a
# push, not a build, so it does not belong on the build fabric anyway: builds
# run natively on git.hanzo.ai, which is the whole point of this file.
runs-on: ubuntu-latest
timeout-minutes: 2
timeout-minutes: 15
steps:
- name: Nudge git.hanzo.ai to pull HEAD (mirror-sync)
continue-on-error: true
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
persist-credentials: false
- name: Push refs to git.hanzo.ai
env:
HANZO_GIT_TOKEN: ${{ secrets.HANZO_GIT_TOKEN }}
FORGE: https://git.hanzo.ai
GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
run: |
set -u
if [ -z "${HANZO_GIT_TOKEN:-}" ]; then
echo "HANZO_GIT_TOKEN not set — relying on the mirror App webhook. Skipping nudge."
exit 0
set -euo pipefail
if [ -z "${GIT_TOKEN}" ]; then
echo "::error::GIT_TOKEN is unset, so this push never reached the canonical forge. Add the org secret."
exit 1
fi
if curl -fsS --max-time 20 --retry 2 --retry-delay 2 -X POST \
-H "Authorization: token ${HANZO_GIT_TOKEN}" \
"https://git.hanzo.ai/api/v1/repos/hanzoai/agent/mirror-sync"; then
echo "git.hanzo.ai mirror-sync triggered"
else
echo "mirror-sync nudge failed (non-fatal) — the App webhook still mirrors"
REMOTE="${FORGE}/${GITHUB_REPOSITORY}.git"
# The forge reads the token from the basic-auth password and ignores
# the username. base64 of a secret is not masked automatically.
AUTH="$(printf 'x-access-token:%s' "${GIT_TOKEN}" | base64 -w0)"
echo "::add-mask::${AUTH}"
# Host-scoped, so the forge token is never sent to github.com.
git config --local "http.${FORGE}/.extraheader" "Authorization: Basic ${AUTH}"
# Reachability is the only transient failure here, so it is the only
# thing retried. ls-remote exits 0 on an existing empty repo and
# non-zero when the forge is down or the repo is absent.
ERR="$(mktemp)"
REACHED=""
for attempt in 1 2 3; do
if git ls-remote "${REMOTE}" >/dev/null 2>"${ERR}"; then
REACHED=1
break
fi
echo "forge probe failed (attempt ${attempt}/3): $(cat "${ERR}")"
if [ "${attempt}" -lt 3 ]; then
sleep $((attempt * 5))
fi
done
if [ -z "${REACHED}" ]; then
echo "::error::git.hanzo.ai is unreachable or ${GITHUB_REPOSITORY} does not exist there. $(cat "${ERR}")"
exit 1
fi
# Additive and fast-forward-only: no leading '+', no --force, no
# --prune, no --mirror. Tags go every run so a tag that missed its own
# event still lands; git refuses to move one that already differs.
REFS=('refs/tags/*:refs/tags/*')
case "${GITHUB_REF}" in
refs/heads/*) REFS+=("${GITHUB_REF}:${GITHUB_REF}") ;;
esac
# Not --atomic: a diverged ref must not hold back the refs that are
# clean. Rejections still fail the job.
if ! git push "${REMOTE}" "${REFS[@]}"; then
echo "::error::git.hanzo.ai rejected a ref for ${GITHUB_REPOSITORY}. The forge is canonical and is never force-updated from here; reconcile the diverged branch or tag by hand."
exit 1
fi
+6 -1
View File
@@ -157,4 +157,9 @@ control-plane/web/client/node_modules/
# IDE
.idea/
.hanzo/
# Local scratch under .hanzo/ stays ignored, but not .hanzo/workflows: that is
# where git.hanzo.ai reads this repo's CI from, so ignoring it silently drops
# the build. `.hanzo/*` rather than `.hanzo/` so git descends far enough to see
# the negation.
.hanzo/*
!.hanzo/workflows/
+80
View File
@@ -0,0 +1,80 @@
# Native CI on git.hanzo.ai.
#
# This replaces tests.yml and test.yml, which were two workflows checking the
# same things (the second with `|| true` on most steps, so it could not fail).
# One file, one way.
#
# It gates what is actually verifiable on main today, and no more. Measured on
# this commit:
#
# go build/vet/test ./... pass <- gated below
# uv sync --frozen pass <- gated below
# make sync (uv sync --all-extras --all-packages --group dev)
# CANNOT RESOLVE: requires-python is >=3.9
# and a transitive numpy needs >=3.10
# make lint (ruff check) 2337 errors
# make tests (pytest) INTERNALERROR at collection — a test module
# sys.exit(1)s at import without ANTHROPIC_API_KEY
#
# tests.yml ran `make sync` as the first step of all five of its jobs, so none of
# them could have gone green here. Restoring it would add a workflow that is red
# on arrival for reasons no CI change can fix, and a permanently red check is
# read as broken CI rather than as broken code. The three failures above are
# recorded in LLM.md as work, not hidden behind `|| true` or a missing check.
name: ci
on:
push:
branches: ['**']
pull_request:
workflow_dispatch:
jobs:
go:
runs-on: hanzo-build-linux-amd64
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
# gofmt reports on stdout and exits 0, so the file list is the check.
- name: gofmt
run: |
set -euo pipefail
unformatted="$(gofmt -l .)"
if [ -n "${unformatted}" ]; then
echo "::error::gofmt would rewrite these files:"
echo "${unformatted}"
exit 1
fi
- run: go build ./...
- run: go vet ./...
- run: go test ./...
python:
runs-on: hanzo-build-linux-amd64
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
enable-cache: true
# --frozen, so this fails when uv.lock and pyproject.toml disagree rather
# than silently re-resolving to something the image will never install.
# It is the same step Dockerfile.sdk's builder stage runs, which is why a
# break here means the image cannot be built.
- name: the lock installs
run: uv sync --frozen --no-dev
# Dockerfile.sdk's HEALTHCHECK is `import agents`, and its CMD is
# `python -m agents`. If the package does not import, the container is
# unhealthy the moment it starts.
- name: the package imports
run: uv run --no-sync python -c "import agents; print(agents.__name__)"
+109
View File
@@ -0,0 +1,109 @@
# Builds and pushes ghcr.io/hanzoai/agent. Nothing else.
#
# This is .github/workflows/docker-publish.yml restored here: it was the sole
# producer of this image and was deleted with no replacement, which is exactly
# how an artifact goes stale with no failing run to show it.
#
# Two things could not come across verbatim.
#
# 1. Its body was one `uses: hanzoai/.github/.github/workflows/docker-build.yml@main`,
# and a private cross-repo reusable workflow does not resolve on the forge, so
# the amd64 build/push it performed is vendored inline, the way hanzoai/bot
# already vendors it.
# 2. It built `context: control-plane`, and no control-plane directory exists on
# main — the orchestrator was hoisted into the root module (tag v0.1.1, "drop
# control-plane/pkg nesting"); the directory survives only on the unmerged
# consolidate/held-work branch, and deployments/docker/Dockerfile.control-plane
# still COPYs from it. Restoring that context restores a build that cannot
# run. This builds Dockerfile.sdk, the one Dockerfile on main whose context
# exists, and tags it sdk-* to match what is already published here.
#
# No deploy step: the tag that runs is named by a CR in hanzoai/universe, not by
# CI. Nothing in universe references this image yet — see LLM.md.
name: deploy
on:
push:
branches: [main]
tags: ['v*']
workflow_dispatch:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
packages: write
jobs:
image:
# The forge's git-runner advertises this label (GIT_RUNNER_LABELS in
# universe infra/k8s/git-runner/statefulset.yaml). A label nothing
# advertises does not fail — it queues to the timeout, which is silence.
runs-on: hanzo-build-linux-amd64
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
# Computed here rather than by docker/metadata-action: the rule is two
# lines of shell, and shell is the same on both forges. The sdk- prefix
# keeps this Dockerfile's tags from colliding with any other image built
# out of this repo.
# v* tag -> :sdk-X.Y.Z + :sdk-latest
# main push -> :sdk-sha-<sha7> + :sdk-latest
- id: tags
run: |
set -euo pipefail
image=ghcr.io/hanzoai/agent
case "${GITHUB_REF}" in
refs/tags/v*) primary="${image}:sdk-${GITHUB_REF#refs/tags/v}" ;;
*) primary="${image}:sdk-sha-$(printf '%s' "${GITHUB_SHA}" | cut -c1-7)" ;;
esac
{
echo 'list<<EOF'
echo "${primary}"
echo "${image}:sdk-latest"
echo 'EOF'
} >> "${GITHUB_OUTPUT}"
echo "publishing ${primary} and ${image}:sdk-latest"
- uses: docker/setup-buildx-action@v3
with:
driver: docker-container
driver-opts: network=host
# Without this the push is unauthenticated and the registry 403s.
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ secrets.GHCR_USER }}
password: ${{ secrets.GHCR_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile.sdk
platforms: linux/amd64
tags: ${{ steps.tags.outputs.list }}
provenance: false
push: true
# build-push-action can exit 0 before the manifest resolves, so a green
# run could still mean a later ImagePullBackOff. Prove it pulls.
- name: the pushed image resolves
run: |
set -euo pipefail
for img in ${{ steps.tags.outputs.list }}; do
for i in 1 2 3 4 5 6; do
if docker buildx imagetools inspect "${img}" >/dev/null 2>&1; then
echo "resolved ${img}"
break
fi
if [ "${i}" = 6 ]; then
echo "::error::pushed image never resolved: ${img}"
exit 1
fi
sleep 5
done
done
+67
View File
@@ -52,3 +52,70 @@ make test # Run test suite
- Pydantic models for configuration
- Async-first design
- MCP (Model Context Protocol) for tool integration
## How this ships
One way, and it runs on our own stack:
push -> github.com/hanzoai/agent (a mirror)
.github/workflows/sync.yml carries refs onward
-> git.hanzo.ai/hanzoai/agent CANONICAL
.hanzo/workflows/ci.yml the checks
.hanzo/workflows/deploy.yml builds ghcr.io/hanzoai/agent
**git.hanzo.ai is canonical; GitHub is a mirror.** `.github/workflows/` holds
exactly one file, `sync.yml`, and its only job is getting refs to the forge.
Every build, check and deploy is a workflow under `.hanzo/workflows/`, which the
forge reads. `.hanzo/workflows` uses GitHub Actions syntax, so a workflow moves
between the two by changing directory and nothing else.
`deploy.yml` builds `Dockerfile.sdk` and tags `sdk-*`. It does **not** build
`control-plane`, which is what the workflow it replaces did: there is no
`control-plane/` directory on `main`. The orchestrator was hoisted into the root
Go module (tag `v0.1.1`, "drop control-plane/pkg nesting"), and the directory
survives only on the unmerged `consolidate/held-work` branch. Two things still
point at the old layout and are therefore stale, not load-bearing:
`deployments/docker/Dockerfile.control-plane` (COPYs from `control-plane/`) and
`Makefile.agents` (`cd control-plane && go build`).
A build never deploys itself. Nothing in `hanzoai/universe` references
`ghcr.io/hanzoai/agent`, so this image has **no consumer and no App CR**. That is
a real open question rather than an oversight: either a CR should exist, or the
image should stop being published. It is published today because it was
published yesterday, and because deleting the only producer of a live artifact is
how something goes stale with nothing red to show it.
### What is checked, and what is not
`ci.yml` gates the Go module (`gofmt`, `go build`, `go vet`, `go test` — all
pass) and the Python install the image depends on (`uv sync --frozen --no-dev`,
then `import agents`, matching `Dockerfile.sdk`'s `HEALTHCHECK`).
It deliberately does not run the Python lint/type/test suite, because none of it
passes on `main`:
| Command | State |
|---|---|
| `make sync` | **cannot resolve** — `requires-python` is `>=3.9`, a transitive `numpy` needs `>=3.10` |
| `make lint` (`ruff check`) | 2337 errors |
| `make tests` (`pytest`) | `INTERNALERROR` at collection; a test module `sys.exit(1)`s at import without `ANTHROPIC_API_KEY` |
| `make old_version_tests` | runs 3.9, the version `make sync` cannot resolve |
`tests.yml` ran `make sync` as the first step of all five of its jobs, so none of
them could have gone green. Fixing those four is real work with an owner; a
permanently red check is read as broken CI rather than as broken code, so the
debt is written down here instead.
### Deleted rather than migrated
- `test.yml` — a second copy of `tests.yml` with `|| true` on most steps, so it
could not fail. Folded into one `ci.yml`.
- `docs.yml` — `make deploy-docs` is `mkdocs gh-deploy`, i.e. GitHub Pages. There
is no `gh-pages` branch and `hanzoai.github.io/agent` 404s, so it never
published anything. Pages is not how Hanzo serves sites.
- `publish.yml` — published this repo's dist to PyPI. `pyproject.toml` is
`hanzo-agent 0.0.4`; when it was named `hanzoai` this workflow pointed at the
package `hanzoai/python-sdk` owns (now 3.1.1), so a release here could have
published a downgrade over it. Removed in "fix(pypi): stop publishing this
repo's dist as hanzoai" and deliberately not restored.
- `issues.yml` — upstream's stale-issue bot.
+1 -1
View File
@@ -26,8 +26,8 @@ import (
"fmt"
"strings"
luxlog "github.com/luxfi/log"
openai "github.com/hanzoai/go-openai"
luxlog "github.com/luxfi/log"
"github.com/zap-proto/zip"
)
+1 -1
View File
@@ -11,8 +11,8 @@ import (
"sort"
"testing"
luxlog "github.com/luxfi/log"
openai "github.com/hanzoai/go-openai"
luxlog "github.com/luxfi/log"
fiber "github.com/zap-proto/fiber/v3"
"github.com/zap-proto/zip"
)
+69 -69
View File
@@ -28,8 +28,8 @@ func TestNew(t *testing.T) {
{
name: "valid config",
cfg: Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: "https://api.example.com",
},
wantErr: false,
@@ -42,7 +42,7 @@ func TestNew(t *testing.T) {
{
name: "missing NodeID",
cfg: Config{
Version: "1.0.0",
Version: "1.0.0",
HanzoAgentsURL: "https://api.example.com",
},
wantErr: true,
@@ -50,7 +50,7 @@ func TestNew(t *testing.T) {
{
name: "missing Version",
cfg: Config{
NodeID: "node-1",
NodeID: "node-1",
HanzoAgentsURL: "https://api.example.com",
},
wantErr: true,
@@ -69,8 +69,8 @@ func TestNew(t *testing.T) {
{
name: "defaults applied",
cfg: Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: "https://api.example.com",
},
wantErr: false,
@@ -84,8 +84,8 @@ func TestNew(t *testing.T) {
{
name: "with AIConfig",
cfg: Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: "https://api.example.com",
AIConfig: &ai.Config{
APIKey: "test-key",
@@ -101,10 +101,10 @@ func TestNew(t *testing.T) {
{
name: "invalid AIConfig",
cfg: Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: "https://api.example.com",
AIConfig: &ai.Config{
AIConfig: &ai.Config{
// Missing required fields
},
},
@@ -131,10 +131,10 @@ func TestNew(t *testing.T) {
func TestRegisterReasoner(t *testing.T) {
cfg := Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: "https://api.example.com",
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
agent, err := New(cfg)
@@ -155,10 +155,10 @@ func TestRegisterReasoner(t *testing.T) {
func TestRegisterReasoner_WithOptions(t *testing.T) {
cfg := Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: "https://api.example.com",
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
agent, err := New(cfg)
@@ -182,10 +182,10 @@ func TestRegisterReasoner_WithOptions(t *testing.T) {
func TestRegisterReasoner_NilHandler(t *testing.T) {
cfg := Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: "https://api.example.com",
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
agent, err := New(cfg)
@@ -225,7 +225,7 @@ func TestInitialize(t *testing.T) {
NodeID: "node-1",
Version: "1.0.0",
TeamID: "team-1",
HanzoAgentsURL: server.URL,
HanzoAgentsURL: server.URL,
Logger: log.New(io.Discard, "", 0),
DisableLeaseLoop: true, // Disable for testing
}
@@ -244,10 +244,10 @@ func TestInitialize(t *testing.T) {
func TestInitialize_NoReasoners(t *testing.T) {
cfg := Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: "https://api.example.com",
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
agent, err := New(cfg)
@@ -260,10 +260,10 @@ func TestInitialize_NoReasoners(t *testing.T) {
func TestHandler(t *testing.T) {
cfg := Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: "https://api.example.com",
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
agent, err := New(cfg)
@@ -289,10 +289,10 @@ func TestHandler(t *testing.T) {
func TestHandleReasoner_Sync(t *testing.T) {
cfg := Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: "https://api.example.com",
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
agent, err := New(cfg)
@@ -323,10 +323,10 @@ func TestHandleReasoner_Sync(t *testing.T) {
func TestHandleReasoner_NotFound(t *testing.T) {
cfg := Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: "https://api.example.com",
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
agent, err := New(cfg)
@@ -347,10 +347,10 @@ func TestHandleReasoner_NotFound(t *testing.T) {
func TestHandleReasoner_WrongMethod(t *testing.T) {
cfg := Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: "https://api.example.com",
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
agent, err := New(cfg)
@@ -371,10 +371,10 @@ func TestHandleReasoner_WrongMethod(t *testing.T) {
func TestHandleReasoner_Error(t *testing.T) {
cfg := Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: "https://api.example.com",
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
agent, err := New(cfg)
@@ -428,10 +428,10 @@ func TestCall(t *testing.T) {
defer server.Close()
cfg := Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: server.URL,
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
agent, err := New(cfg)
@@ -485,10 +485,10 @@ func TestCall_ErrorHandling(t *testing.T) {
defer server.Close()
cfg := Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: server.URL,
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
agent, err := New(cfg)
@@ -523,10 +523,10 @@ func TestAI(t *testing.T) {
defer server.Close()
cfg := Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: "https://api.example.com",
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
AIConfig: &ai.Config{
APIKey: "test-key",
BaseURL: server.URL,
@@ -545,10 +545,10 @@ func TestAI(t *testing.T) {
func TestAI_NotConfigured(t *testing.T) {
cfg := Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: "https://api.example.com",
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
// No AIConfig
}
@@ -582,10 +582,10 @@ func TestAIStream(t *testing.T) {
defer server.Close()
cfg := Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: "https://api.example.com",
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
AIConfig: &ai.Config{
APIKey: "test-key",
BaseURL: server.URL,
@@ -627,10 +627,10 @@ func TestAIStream(t *testing.T) {
func TestAIStream_NotConfigured(t *testing.T) {
cfg := Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: "https://api.example.com",
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
agent, err := New(cfg)
@@ -688,13 +688,13 @@ func TestHandleReasonerAsyncPostsStatus(t *testing.T) {
defer callbackServer.Close()
cfg := Config{
NodeID: "node-1",
Version: "1.0.0",
TeamID: "team",
NodeID: "node-1",
Version: "1.0.0",
TeamID: "team",
HanzoAgentsURL: callbackServer.URL,
ListenAddress: ":0",
PublicURL: "http://localhost:0",
Logger: log.New(io.Discard, "[test] ", 0),
ListenAddress: ":0",
PublicURL: "http://localhost:0",
Logger: log.New(io.Discard, "[test] ", 0),
}
agent, err := New(cfg)
@@ -770,10 +770,10 @@ func TestChildContextGeneratesRunID(t *testing.T) {
func TestBuildChildContext(t *testing.T) {
cfg := Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: "http://example.com",
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
ag, err := New(cfg)
@@ -836,10 +836,10 @@ func TestCallLocalEmitsEvents(t *testing.T) {
defer eventServer.Close()
cfg := Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: eventServer.URL,
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
ag, err := New(cfg)
+8 -8
View File
@@ -41,8 +41,8 @@ func TestDiscoverJSON(t *testing.T) {
defer server.Close()
a, err := New(Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: server.URL,
})
require.NoError(t, err)
@@ -77,8 +77,8 @@ func TestDiscoverCompactAndXML(t *testing.T) {
defer xmlServer.Close()
aCompact, err := New(Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: compactServer.URL,
})
require.NoError(t, err)
@@ -90,8 +90,8 @@ func TestDiscoverCompactAndXML(t *testing.T) {
assert.Equal(t, "a1", compactResult.Compact.Reasoners[0].AgentID)
aXML, err := New(Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: xmlServer.URL,
})
require.NoError(t, err)
@@ -106,8 +106,8 @@ func TestDiscoverCompactAndXML(t *testing.T) {
func TestDiscoverRejectsInvalidFormat(t *testing.T) {
a, err := New(Config{
NodeID: "node-1",
Version: "1.0.0",
NodeID: "node-1",
Version: "1.0.0",
HanzoAgentsURL: "http://localhost:8080",
})
require.NoError(t, err)
+2 -2
View File
@@ -355,8 +355,8 @@ func (s *ScopedMemory) GetTyped(ctx context.Context, key string, dest any) error
// InMemoryBackend provides a thread-safe in-memory implementation of MemoryBackend.
// Data is lost when the process exits.
type InMemoryBackend struct {
mu sync.RWMutex
data map[string]map[string]any // "scope:scopeID" -> key -> value
mu sync.RWMutex
data map[string]map[string]any // "scope:scopeID" -> key -> value
vectorData map[string]map[string]vectorRecord // "scope:scopeID" -> key -> vectorRecord
}
+8 -8
View File
@@ -10,14 +10,14 @@ import (
// MemoryMetrics holds memory measurement results
type MemoryMetrics struct {
Name string
AllocBytes uint64
TotalAlloc uint64
HeapAlloc uint64
HeapInuse uint64
NumGC uint32
Iterations int
Duration time.Duration
Name string
AllocBytes uint64
TotalAlloc uint64
HeapAlloc uint64
HeapInuse uint64
NumGC uint32
Iterations int
Duration time.Duration
}
// PerIterationKB returns memory per iteration in KB
+32 -32
View File
@@ -37,10 +37,10 @@ func TestNote_Basic(t *testing.T) {
defer server.Close()
cfg := Config{
NodeID: "test-node",
Version: "1.0.0",
NodeID: "test-node",
Version: "1.0.0",
HanzoAgentsURL: server.URL + "/api/v1", // Will be converted to /api/ui/v1
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
agent, err := New(cfg)
@@ -95,10 +95,10 @@ func TestNotef_Formatted(t *testing.T) {
defer server.Close()
cfg := Config{
NodeID: "test-node",
Version: "1.0.0",
NodeID: "test-node",
Version: "1.0.0",
HanzoAgentsURL: server.URL + "/api/v1",
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
agent, err := New(cfg)
@@ -132,10 +132,10 @@ func TestNote_NoTags(t *testing.T) {
defer server.Close()
cfg := Config{
NodeID: "test-node",
Version: "1.0.0",
NodeID: "test-node",
Version: "1.0.0",
HanzoAgentsURL: server.URL + "/api/v1",
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
agent, err := New(cfg)
@@ -189,10 +189,10 @@ func TestNote_ServerError(t *testing.T) {
defer server.Close()
cfg := Config{
NodeID: "test-node",
Version: "1.0.0",
NodeID: "test-node",
Version: "1.0.0",
HanzoAgentsURL: server.URL + "/api/v1",
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
agent, err := New(cfg)
@@ -221,19 +221,19 @@ func TestNote_URLConversion(t *testing.T) {
defer server.Close()
tests := []struct {
name string
controlPlaneURL string
expectedPath string
name string
controlPlaneURL string
expectedPath string
}{
{
name: "Standard /api/v1 URL",
name: "Standard /api/v1 URL",
controlPlaneURL: server.URL + "/api/v1",
expectedPath: "/api/ui/v1/executions/note",
expectedPath: "/api/ui/v1/executions/note",
},
{
name: "URL without /api/v1",
name: "URL without /api/v1",
controlPlaneURL: server.URL,
expectedPath: "/api/ui/v1/executions/note",
expectedPath: "/api/ui/v1/executions/note",
},
}
@@ -243,10 +243,10 @@ func TestNote_URLConversion(t *testing.T) {
requestPath = ""
cfg := Config{
NodeID: "test-node",
Version: "1.0.0",
NodeID: "test-node",
Version: "1.0.0",
HanzoAgentsURL: tt.controlPlaneURL,
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
agent, err := New(cfg)
@@ -280,11 +280,11 @@ func TestNote_WithToken(t *testing.T) {
defer server.Close()
cfg := Config{
NodeID: "test-node",
Version: "1.0.0",
NodeID: "test-node",
Version: "1.0.0",
HanzoAgentsURL: server.URL + "/api/v1",
Token: "test-token-123",
Logger: log.New(io.Discard, "", 0),
Token: "test-token-123",
Logger: log.New(io.Discard, "", 0),
}
agent, err := New(cfg)
@@ -314,10 +314,10 @@ func TestNote_FireAndForget(t *testing.T) {
defer slowServer.Close()
cfg := Config{
NodeID: "test-node",
Version: "1.0.0",
NodeID: "test-node",
Version: "1.0.0",
HanzoAgentsURL: slowServer.URL + "/api/v1",
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
agent, err := New(cfg)
@@ -348,10 +348,10 @@ func TestNote_MultipleNotes(t *testing.T) {
defer server.Close()
cfg := Config{
NodeID: "test-node",
Version: "1.0.0",
NodeID: "test-node",
Version: "1.0.0",
HanzoAgentsURL: server.URL + "/api/v1",
Logger: log.New(io.Discard, "", 0),
Logger: log.New(io.Discard, "", 0),
}
agent, err := New(cfg)
+3 -3
View File
@@ -41,9 +41,9 @@ func TestDefaultConfig(t *testing.T) {
}()
tests := []struct {
name string
setupEnv func()
checkConfig func(t *testing.T, cfg *Config)
name string
setupEnv func()
checkConfig func(t *testing.T, cfg *Config)
}{
{
name: "default OpenAI config",