Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
9 changes: 7 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ jobs:
path: runtimes/pydantic-ai
- package: microsoft-agent-framework adapter
path: runtimes/microsoft-agent-framework
- package: langgraph adapter
path: runtimes/langgraph
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
Expand Down Expand Up @@ -212,19 +214,21 @@ jobs:
make build-agentkit TAG=ci
make build-serve TAG=ci
make build-serve-maf TAG=ci
make build-serve-langgraph TAG=ci

- name: Build test agent images for both runtimes
- name: Build test agent images for all runtimes
run: |
set -euxo pipefail
make build-test-agent TAG=ci BUILDER=
make build-test-agent TAG=ci BUILDER= RUNTIME=maf
make build-test-agent TAG=ci BUILDER= RUNTIME=langgraph

- name: Smoke test built agent images
run: |
set -euo pipefail

cleanup() {
docker rm -f agentkit-smoke-pydantic agentkit-smoke-maf >/dev/null 2>&1 || true
docker rm -f agentkit-smoke-pydantic agentkit-smoke-maf agentkit-smoke-langgraph >/dev/null 2>&1 || true
}
trap cleanup EXIT

Expand Down Expand Up @@ -256,6 +260,7 @@ jobs:

smoke agentkit-smoke-pydantic hello-agent:ci 18080
smoke agentkit-smoke-maf maf-agent:ci 18081
smoke agentkit-smoke-langgraph langgraph-agent:ci 18082

live-copilot-e2e:
name: Live Vekil/Copilot E2E
Expand Down
34 changes: 23 additions & 11 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,22 @@ endif
PLATFORM ?= linux/amd64

# RUNTIME selects which runtime adapter the test-agent targets: `pydantic-ai`
# (default) or the Microsoft Agent Framework, named either `maf` (alias) or
# `microsoft-agent-framework` (canonical) — both are accepted. build-test-agent
# derives the adapter image, the fixture, and the output tag from it, so you can
# build the SAME logical agent under either runtime (the §10.4 equivalence proof):
# make build-serve build-test-agent # pydantic-ai → hello-agent
# make build-serve-maf build-test-agent RUNTIME=maf # MAF → maf-agent
# (default), Microsoft Agent Framework (`maf` alias or canonical name), or
# LangGraph (`langgraph`). build-test-agent derives the adapter image, fixture,
# and output tag from it, so you can build the SAME logical agent under any
# supported runtime (the §10.4 equivalence proof):
# make build-serve build-test-agent # pydantic-ai → hello-agent
# make build-serve-maf build-test-agent RUNTIME=maf # MAF → maf-agent
# make build-serve-langgraph build-test-agent RUNTIME=langgraph # LangGraph → langgraph-agent
RUNTIME ?= pydantic-ai
# Per-runtime adapter image, fixture, and output tag (overridable). The MAF branch
# matches BOTH spellings via $(filter ...) so the canonical name does not silently
# fall through to the pydantic-ai default.
ifneq ($(filter maf microsoft-agent-framework,$(RUNTIME)),)
# Per-runtime adapter image, fixture, and output tag (overridable). Branches
# match all accepted spellings so a canonical name does not silently fall through
# to the pydantic-ai default.
ifneq ($(filter langgraph,$(RUNTIME)),)
SERVE_IMAGE ?= agentkit-serve-langgraph:$(TAG)
FIXTURE ?= test/agentkitfile-langgraph-hello.yaml
AGENT_IMAGE ?= langgraph-agent:$(TAG)
else ifneq ($(filter maf microsoft-agent-framework,$(RUNTIME)),)
SERVE_IMAGE ?= agentkit-serve-maf:$(TAG)
FIXTURE ?= test/agentkitfile-maf-hello.yaml
AGENT_IMAGE ?= maf-agent:$(TAG)
Expand Down Expand Up @@ -92,9 +97,16 @@ build-serve:
build-serve-maf:
docker buildx build . -f runtimes/microsoft-agent-framework/Dockerfile -t agentkit-serve-maf:$(TAG) --load

# Build the LangGraph runtime adapter (agentkit-serve-langgraph) image.
# This is the LLB base used when an agentkitfile selects `runtime: langgraph`.
.PHONY: build-serve-langgraph
build-serve-langgraph:
docker buildx build . -f runtimes/langgraph/Dockerfile -t agentkit-serve-langgraph:$(TAG) --load

# Build a test agent against the LOCAL frontend (BUILDKIT_SYNTAX) and the LOCAL
# adapter (--build-arg adapter). The runtime, fixture, adapter image, and output
# tag all derive from RUNTIME (default pydantic-ai; `RUNTIME=maf` for MAF).
# tag all derive from RUNTIME (default pydantic-ai; `RUNTIME=maf` for MAF;
# `RUNTIME=langgraph` for LangGraph).
# --provenance=false keeps the output a plain single-platform image for --load.
.PHONY: build-test-agent
build-test-agent:
Expand Down
46 changes: 30 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,18 @@ the optional `runtime:` key:
|---|---|---|
| *(omitted)* / `pydantic-ai` | `agentkit-serve` | [pydantic-ai](https://ai.pydantic.dev) (default) |
| `microsoft-agent-framework` (alias `maf`) | `agentkit-serve-maf` | [Microsoft Agent Framework](https://github.com/microsoft/agent-framework) |
| `langgraph` | `agentkit-serve-langgraph` | [LangChain/LangGraph](https://docs.langchain.com/oss/python/langgraph/overview) |

```yaml
runtime: microsoft-agent-framework # or: maf
runtime: langgraph
# or: runtime: microsoft-agent-framework # alias: maf
```

Both runtimes consume the **same** baked `/agent/agent.yaml` and serve the
All runtimes consume the **same** baked `/agent/agent.yaml` and serve the
**same** non-streaming OpenAI `/v1` façade with the same guards — so the same
agentkitfile produces a behavior-compatible image under either. Only the in-image
runtime adapter differs. The `AGENTKIT_MCP_TIMEOUT` knob applies to both.
agentkitfile produces a behavior-compatible image under any supported runtime.
Only the in-image runtime adapter differs. The `AGENTKIT_MCP_TIMEOUT` knob
applies to all runtimes.

## Local dev loop (3 steps)

Expand All @@ -92,16 +95,27 @@ make build-serve-maf # agentkit-serve-maf:test
make build-test-agent RUNTIME=maf # test/agentkitfile-maf-hello.yaml -> maf-agent:test
```

To iterate on the **LangGraph** runtime, build its adapter and target it with
`RUNTIME=langgraph`:

```sh
make build-serve-langgraph # agentkit-serve-langgraph:test
make build-test-agent RUNTIME=langgraph # test/agentkitfile-langgraph-hello.yaml -> langgraph-agent:test
```

## CI

GitHub Actions runs the full closeout loop on pushes and pull requests:

- Go lint, formatting, vet, race tests, and frontend build.
- Python compile, pytest, and wheel checks for `runtimes/common/`,
`runtimes/pydantic-ai/`, and `runtimes/microsoft-agent-framework/`.
- Docker builds for the frontend and both runtime adapters, followed by offline
`/healthz` smoke tests for generated pydantic-ai and MAF agent images.
- Optional live Vekil-backed Copilot E2E, using the official pinned `ghcr.io/sozercan/vekil` image and a repository secret named
`runtimes/pydantic-ai/`, `runtimes/microsoft-agent-framework/`, and
`runtimes/langgraph/`.
- Docker builds for the frontend and all three runtime adapters, followed by
offline `/healthz` smoke tests for generated pydantic-ai, MAF, and LangGraph
agent images.
- Optional live Vekil-backed Copilot E2E, using the official pinned
`ghcr.io/sozercan/vekil` image and a repository secret named
`COPILOT_GITHUB_TOKEN`. If that secret is unavailable (for example on forks or
unconfigured repos), or Vekil reports that the token lacks Copilot access/
permissions, the live job is skipped while the offline checks still run.
Expand Down Expand Up @@ -142,21 +156,21 @@ Each adapter is a thin shell over a shared core:
ABI loader, the OpenAI `/v1` façade, the CLI/network posture, and the neutral run
contract (`RunResult`, `AgentRunError`, the `RuntimeFactory` protocol). Imports no
agent framework.
- `runtimes/pydantic-ai/`, `runtimes/microsoft-agent-framework/` — each ships
only an `agent_factory.py` (the one file that imports its framework) plus a
thin `__main__.py`, implementing `RuntimeFactory`. They stay **separate images**
with disjoint framework deps; that physical separation is what guarantees the
lock-in boundary.
- `runtimes/pydantic-ai/`, `runtimes/microsoft-agent-framework/`,
`runtimes/langgraph/` — each ships only an `agent_factory.py` (the one file
that imports its framework) plus a thin `__main__.py`, implementing
`RuntimeFactory`. They stay **separate images** with disjoint framework deps;
that physical separation is what guarantees the lock-in boundary.

Adding a single-agent runtime is therefore one `agent_factory.py` + one Go
`utils.RuntimeSpec` entry; it inherits the shared `/v1` façade and the conformance
test suite for free.

## v0 scope / not yet

- **v0**: two runtimes (pydantic-ai default + microsoft-agent-framework),
`provider: openai-compatible` only, stdio `command` MCP tools, the OpenAI `/v1`
façade, single OCI image output.
- **v0**: three runtimes (pydantic-ai default + microsoft-agent-framework +
langgraph), `provider: openai-compatible` only, stdio `command` MCP tools, the
OpenAI `/v1` façade, single OCI image output.
- **Not yet**: image-based MCP tools, evals, lock file / SBOM / signing,
agentpack, `extends`/patches, knowledge/RAG, memory/state, model fallback,
streaming, and embedded/BYO serving targets.
Expand Down
12 changes: 7 additions & 5 deletions pkg/agentkit/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,10 @@ expose:
}

// TestValidateAcceptsRegisteredRuntimes proves the widened runtime gate (plan §8):
// the canonical MAF name, its "maf" alias, the default runtime, and an omitted
// runtime all validate.
// the canonical MAF name, its "maf" alias, LangGraph, the default runtime, and
// an omitted runtime all validate.
func TestValidateAcceptsRegisteredRuntimes(t *testing.T) {
for _, rt := range []string{"", "pydantic-ai", "microsoft-agent-framework", "maf"} {
for _, rt := range []string{"", "pydantic-ai", "microsoft-agent-framework", "maf", "langgraph"} {
cfg, err := NewFromBytes(agentBaseYAML(rt))
if err != nil {
t.Fatalf("runtime %q: parse error: %v", rt, err)
Expand All @@ -217,7 +217,9 @@ func TestValidateRejectsUnknownRuntime(t *testing.T) {
if verr == nil || !strings.Contains(verr.Error(), "runtime") {
t.Fatalf("expected unknown-runtime rejection, got: %v", verr)
}
if !strings.Contains(verr.Error(), "microsoft-agent-framework") {
t.Errorf("error should list supported runtimes; got: %v", verr)
for _, want := range []string{"microsoft-agent-framework", "langgraph"} {
if !strings.Contains(verr.Error(), want) {
t.Errorf("error should list supported runtime %q; got: %v", want, verr)
}
}
}
35 changes: 29 additions & 6 deletions pkg/build/router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package build
import "testing"

const (
wantImageRoute = "pydantic-ai/image"
runtimePydca = "pydantic-ai"
runtimeMAFName = "microsoft-agent-framework"
runtimeMAFAls = "maf"
wantMAFRoute = "microsoft-agent-framework/image"
wantImageRoute = "pydantic-ai/image"
runtimePydca = "pydantic-ai"
runtimeMAFName = "microsoft-agent-framework"
runtimeMAFAls = "maf"
wantMAFRoute = "microsoft-agent-framework/image"
runtimeLangGraph = "langgraph"
wantLangGraphRoute = "langgraph/image"
)

func TestLookupRouteEmptyTargetDefaults(t *testing.T) {
Expand Down Expand Up @@ -43,6 +45,27 @@ func TestLookupRouteUnknownRuntime(t *testing.T) {
}
}

// TestLookupRouteLangGraph proves the LangGraph runtime resolves through the
// same data-derived router as pydantic-ai and MAF.
func TestLookupRouteLangGraph(t *testing.T) {
// empty target + LangGraph runtime → LangGraph image route.
matched, _, rc, ok := lookupRoute("", runtimeLangGraph)
if !ok || matched != wantLangGraphRoute {
t.Fatalf("LangGraph empty target: matched=%q ok=%v, want %s", matched, ok, wantLangGraphRoute)
}
if rc == nil || rc.Name != runtimeLangGraph {
t.Fatalf("rc = %+v, want langgraph", rc)
}
// exact target match.
if m, _, _, okExact := lookupRoute(wantLangGraphRoute, runtimeLangGraph); !okExact || m != wantLangGraphRoute {
t.Fatalf("LangGraph exact target: matched=%q ok=%v", m, okExact)
}
// bare runtime target.
if m, _, _, okBare := lookupRoute(runtimeLangGraph, runtimeLangGraph); !okBare || m != wantLangGraphRoute {
t.Fatalf("LangGraph bare target: matched=%q ok=%v", m, okBare)
}
}

// TestLookupRouteMAF proves the second runtime resolves through the SAME flat
// router with zero handler changes (plan §8 — "the router already handles the
// second runtime").
Expand Down Expand Up @@ -116,7 +139,7 @@ func TestLookupRouteAliasTargetEmptyRuntime(t *testing.T) {
// TestIsRegisteredRuntime locks the validator's seam: every canonical runtime and
// the alias are registered; an unknown name is not.
func TestIsRegisteredRuntime(t *testing.T) {
for _, name := range []string{runtimePydca, runtimeMAFName, runtimeMAFAls} {
for _, name := range []string{runtimePydca, runtimeMAFName, runtimeMAFAls, runtimeLangGraph} {
if !IsRegisteredRuntime(name) {
t.Errorf("IsRegisteredRuntime(%q) = false, want true", name)
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/utils/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ const (
// write in `runtime:`; it resolves to RuntimeMAF (see CanonicalRuntime).
RuntimeMAFAlias = "maf"

// RuntimeLangGraph is the LangChain/LangGraph runtime adapter.
RuntimeLangGraph = "langgraph"

// ProviderOpenAICompatible is the only model provider supported in v0.
ProviderOpenAICompatible = "openai-compatible"

Expand Down
4 changes: 4 additions & 0 deletions pkg/utils/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ var Runtimes = []RuntimeSpec{
Aliases: []string{RuntimeMAFAlias}, // "maf" → "microsoft-agent-framework"
DefaultAdapterRef: "ghcr.io/sozercan/agentkit/serve-maf:latest",
},
{
Name: RuntimeLangGraph,
DefaultAdapterRef: "ghcr.io/sozercan/agentkit/serve-langgraph:latest",
},
}

// DefaultRuntime is the runtime used when an agentkitfile does not name one. It is
Expand Down
7 changes: 4 additions & 3 deletions pkg/utils/runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func TestCanonicalRuntime(t *testing.T) {
RuntimePydanticAI: RuntimePydanticAI, // canonical → itself
RuntimeMAF: RuntimeMAF, // canonical → itself
RuntimeMAFAlias: RuntimeMAF, // alias → canonical
RuntimeLangGraph: RuntimeLangGraph, // canonical → itself
nonexistentRuntime: nonexistentRuntime, // unknown returned verbatim
}
for in, want := range cases {
Expand All @@ -25,7 +26,7 @@ func TestCanonicalRuntime(t *testing.T) {
}

func TestIsKnownRuntime(t *testing.T) {
for _, name := range []string{RuntimePydanticAI, RuntimeMAF, RuntimeMAFAlias} {
for _, name := range []string{RuntimePydanticAI, RuntimeMAF, RuntimeMAFAlias, RuntimeLangGraph} {
if !IsKnownRuntime(name) {
t.Errorf("IsKnownRuntime(%q) = false, want true", name)
}
Expand All @@ -37,10 +38,10 @@ func TestIsKnownRuntime(t *testing.T) {
}
}

func TestKnownRuntimesContainsBoth(t *testing.T) {
func TestKnownRuntimesContainsAll(t *testing.T) {
got := KnownRuntimes()
sort.Strings(got)
want := []string{RuntimeMAF, RuntimePydanticAI}
want := []string{RuntimeLangGraph, RuntimeMAF, RuntimePydanticAI}
if len(got) != len(want) {
t.Fatalf("KnownRuntimes() = %v, want %v", got, want)
}
Expand Down
13 changes: 13 additions & 0 deletions runtimes/catalog/langgraph.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# AgentKit runtime catalog entry — documentation only.
#
# Purpose: names the "langgraph" runtime adapter and its default adapter image.
# The converter uses this adapter image as the LLB BASE and merges the resolved
# /agent/agent.yaml layer on top; the image then serves the same OpenAI /v1
# façade as the pydantic-ai and Microsoft Agent Framework runtimes — byte-for-byte
# ABI compatible. Select it from an agentkitfile with `runtime: langgraph`.
# Override the adapter per build with `--build-arg adapter=<ref>` (the local dev
# loop points it at `agentkit-serve-langgraph:test`). Nothing parses this file in
# v0 — it is the human-readable catalog entry for this runtime.
apiVersion: v1alpha1
runtime: langgraph
adapter: ghcr.io/sozercan/agentkit/serve-langgraph:latest
12 changes: 11 additions & 1 deletion runtimes/common/agentkit_serve_common/conformance.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,17 @@
# Framework/model-SDK roots that must NOT be imported outside an adapter's
# agent_factory.py. The union across adapters is fine: each adapter only has one
# of these installed, so listing all is harmless and keeps this test shared.
_FRAMEWORK_SDK_ROOTS = {"agent_framework", "pydantic_ai", "openai"}
_FRAMEWORK_SDK_ROOTS = {
"agent_framework",
"pydantic_ai",
"openai",
"langchain",
"langchain_core",
"langchain_mcp_adapters",
"langchain_openai",
"langgraph",
"mcp",
}


def test_healthz_open(make_client):
Expand Down
Loading
Loading