refactor: make the v2 stack assemblable outside package main - #2543
Closed
jjamroga wants to merge 1 commit into
Closed
refactor: make the v2 stack assemblable outside package main#2543jjamroga wants to merge 1 commit into
jjamroga wants to merge 1 commit into
Conversation
… v2 stack
Nothing outside this module can currently assemble a controller on kagent's
own v2 components. api/database exports the Client interface but not a
constructor, and grpcserver is internal, so a downstream can only ever
receive a Client from core/pkg/app -- which never sets A2AHandler and so
never registers the A2A service.
Two changes, no behaviour change:
* core/pkg/database re-exports Connect/NewClient/ResolveURL and aliases
PostgresConfig. The implementation stays in core/internal/database.
* core/internal/grpcserver moves to core/pkg/grpcserver.
Config keeps its fields typed on internal service packages. That is fine for
external callers: omitting a struct field never requires importing its type,
and the ones a downstream cannot name are all optional -- SystemService and
MethodPolicies self-default, so DefaultMethodPolicies still applies.
Signed-off-by: Jonathan Jamroga <jjamroga@gmail.com>
jjamroga
force-pushed
the
export-v2-controller-building-blocks
branch
from
August 24, 2026 18:17
55227b6 to
d3175f5
Compare
Collaborator
Author
|
Superseded by #2544, which is a much smaller change covering the same need: rather than exporting The integration-testing argument here still stands on its own — assembling the v2 stack outside |
EItanya
pushed a commit
that referenced
this pull request
Aug 25, 2026
## Problem `grpcserver` registers the A2A service only when `A2AHandler` is non-nil, and `core/pkg/app` never sets it. A controller built on `app.Start` can create AgentInstances but cannot talk to them: ``` Unimplemented: unknown service lf.a2a.v1.A2AService ``` `core/cmd/controller-v2` does set it, but that is `package main`, so the capability is unreachable to anything embedding `app.Start`. ## Change `ExtensionConfig` gains an `A2AHandler` field, passed through to `grpcserver`. That's it — one file, two lines of code. Nothing else changes. The field defaults to nil and `grpcserver` skips registration exactly as it does today, so this is inert unless an extension opts in. It also needs no reordering: `getExtensionConfig` is called with the `DbClient` in `BootstrapConfig` well before the gRPC server is constructed, so an extension can build a handler with `a2agateway.New` and return it. ## Alternative considered I first went at this by exporting `core/internal/grpcserver` and adding a public constructor for the store, so the v2 stack could be assembled outside `package main` (#2543). This is much smaller, keeps the internals internal, and covers the actual need — so I've closed that one in favour of this. The larger export still has an independent argument: it would let an external module stand up the gRPC surface with a real Postgres and fakes for `a2agateway`'s dialer and `agentinstance`'s actor client, and test AgentInstance lifecycle, ownership checks, A2A routing and `DefaultMethodPolicies` without a cluster or Substrate. Happy to revive it if that's wanted, but it shouldn't be bundled with this. ## Testing `go build ./...`, `go vet`, and `go test ./core/pkg/app/ ./core/internal/grpcserver/` all pass. Signed-off-by: Jonathan Jamroga <jjamroga@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
core/cmd/controller-v2is the only place the v2 stack is assembled, and it'spackage main. Two of its dependencies are internal, so nothing outside this module can reproduce it:api/databaseexports theClientinterface but no constructor —Connect/NewClientare incore/internal/database. Every v2 component takes a store, so there's nothing to hand them.core/internal/grpcserveris internal, so the server that registers those services, including A2A, is unavailable.The visible consequence:
core/pkg/appis the only entry point available externally, and it never setsA2AHandler.grpcserverregisters A2A only when that field is non-nil, so a controller built onapp.Startcan create AgentInstances but not talk to them —Unimplemented: unknown service lf.a2a.v1.A2AService.Why restructure it this way
This is a restructuring, not a feature, and it doesn't add tests. What it could enable is testing the substrate-independent half of the v2 stack without the substrate-dependent machinery.
Today
TestAgentInstanceInteractioncan't run until CI has stood up a kind cluster, installed Substrate, bootstrapped the CA/JWT pools withkubectl-ate, brought up a gVisor WorkerPool and built a golden actor. Much of what it then asserts isn't substrate behaviour: AgentInstance lifecycle, the ownership check, A2A routing by instance headers,DefaultMethodPolicieson the A2A methods, task persistence.Those are reachable with a Postgres and two fakes —
a2agateway's dialer is one method,agentinstance's actor client is six — leaving the cluster e2e to cover what genuinely needs a cluster. That's also whata2agateway/gateway.goalready anticipates: "a standalone gateway can register the same handler on its own server later."Two honest caveats. This PR is only the first half: the composition still lives in
func main(), so a test would have to duplicate that wiring. Extracting it into an exported constructor would finish the job — happy to do that here or as a follow-up. And tests inside this module are unaffected either way;go/core/test/e2ecan already importcore/internal/....Change
No behaviour change.
core/pkg/database— thin re-export ofConnect,NewClient,ResolveURL,PostgresConfigaliased. Implementation stays internal.core/internal/grpcserver→core/pkg/grpcserver— move plus import updates incore/pkg/appandcore/cmd/controller-v2.Configkeeps fields typed on internal packages; that's fine, since omitting a field never requires importing its type and every such field is optional.SystemServiceandMethodPoliciesself-default, soDefaultMethodPolicies()still applies and A2A authorization is unchanged.Testing
go build ./...andgo vetclean;go test ./core/pkg/grpcserver/passes, with the existing tests moving with the package.core/internal/service/{session,task}stay internal, soSessionService/TaskServicestill can't be registered externally — both optional; happy to move them here or in a follow-up.