Skip to content

Replace the Substrate turn transport with a kagent A2A client - #74

Merged
oldsj merged 1 commit into
mainfrom
kagent-a2a-client
Oct 4, 2026
Merged

oldsj merged 1 commit into
mainfrom
kagent-a2a-client

Conversation

@oldsj

@oldsj oldsj commented Oct 4, 2026

Copy link
Copy Markdown
Owner

Native Claude Code and Codex turns now run on kagent harness agents, from the oldsj/kagent fork. Mainloop is an A2A client of the kagent gateway. There is no feature flag and no compatibility path.

This is slice (a) of the kagent migration. Workspaces, previews and agent-to-Mainloop tools follow in later PRs (see Known gaps).

What's in it

  • Client (runtime/kagent_client.py): A2A JSON-RPC for turns (SendStreamingMessage, SubscribeToTask, GetTask, ListTasks, CancelTask) and SessionService over grpc-web for Session lifecycle.
  • Delivery rules:
    • every message is recorded before it is sent and never replayed;
    • "send not accepted" retries the identical message within 30s;
    • any other uncertain send is resolved by looking up its messageId across all ListTasks pages, then re-reading the task so the reply is kept.
  • Session flow (runtime/native_sessions.py):
    • only the pass that claims a recorded delivery sends it, so a cancel or a second pass can't send it again;
    • after a restart, recorded deliveries are sent once;
    • a kagent Session deleted by idle expiry is replaced under a fresh request id, with standing context resent.
  • Atomic session creation: main and child sessions are created with their bindings in one transaction; the main session takes an advisory lock.
  • Interim guard: POST /workspaces returns 409 until workspaces move to kagent.
  • Events: new SSE event turn:updated (no reply text yet). Fixes enum encoding in SSEEvent.
  • Removed: the journal, projection, contract and per-provider stream modules, the native_events and native_lineage tables, main-thread rotation and its settings, and the docs describing them.
  • Docs: specs, architecture, README, and AGENTS.md/ROADMAP.md now name kagent as the workspace runtime.

Verification

  • Backend suite against PostgreSQL 18: 332 tests, OK, none skipped. make lint and svelte-check clean.
  • Live, against a kind cluster running the kagent fork, with Claude and Codex subscription agents through the Mainloop API: create, send and stream, restart reconciliation without a duplicate send, suspend and resume with context, delete, and a clear failure when kagent is unreachable.
  • Fixtures only: the "send not accepted" retry, Session replacement, and recovery of a lost reply from an already-completed task.

Known gaps

  • Agents can't call Mainloop tools yet. Delegation, notes, topics and reports don't work from kagent agents; children finish only through auto-report. The next slice adds an MCP tool channel with per-session identity.
  • Workspaces: creation returns 409 until the workspace slice.
  • No "stop current turn": a turn waiting for input blocks the main thread until the approvals slice adds it.
  • Main-thread expiry: kagent's idle TTL is controller-wide (7 days). After that the main thread gets a fresh Session with standing context; a per-agent TTL is planned.
  • Security: the kagent gateway runs unauthenticated. Use only on isolated clusters until gateway auth and NetworkPolicy land.

Native Claude and Codex turns now go through kagent: A2A JSON-RPC for
messages (SendStreamingMessage, SubscribeToTask, GetTask, ListTasks,
CancelTask) and SessionService over grpc-web for Session lifecycle.
There is no feature flag and no compatibility path.

- Add runtime/kagent_client.py. A task snapshot replaces the projection
  because kagent has no event cursor. "Send not accepted" is read from
  ErrorInfo.metadata.reason and retried with the identical message
  within a 30s wall-clock budget (KAGENT_SEND_RETRY_BUDGET_SECONDS).
  Every other failure after a send is resolved by messageId lookup,
  never by replaying the prompt. The lookup follows ListTasks pages and
  re-reads the match with GetTask so the reply is mirrored.
- Rewrite runtime/native_sessions.py on that client, keeping the
  delivery ledger (queued, recorded, sending, delivered, completed,
  uncertain, failed), the per-session lock, queued reports and
  deterministic assistant-message ids. A delivery is sent only by the
  pass that claims it from recorded, so a cancel or a second pass
  cannot send it again. Reconciliation sends recorded deliveries left
  by a restart once, and expires a sending delivery whose lookup keeps
  failing to uncertain.
- Replace a kagent Session that was deleted (for example by idle
  expiry) once, under a fresh persisted request id, and resend standing
  context to it. Open turns on the old Session become uncertain.
  Changing KAGENT_USER_ID is a migration: every Session is replaced.
- Create the main session and child sessions with their bindings in
  one transaction; the main session is created under an advisory lock.
- POST /workspaces returns 409 until workspaces move to kagent.
- Add the SSE event turn:updated without reply text, with
  deterministic ids. Fix SSEEvent to encode enum members by value.
- Delete the journal, projection, contract and per-provider stream
  modules, their fixtures and tests, the native_events and
  native_lineage tables, main-thread rotation and its settings, and the
  two native-agent architecture documents that described them. Trim
  substrate_workspace to read-only access. native_bindings and
  native_deliveries now carry the kagent Session, request and task
  identity.
- Add kagent_* settings and reshape NativeSessionInfo and
  NativeDeliveryInfo in the shared models. Update the frontend types,
  the SSE client and chat components, the specs, architecture notes and
  README, including the known gap that agents cannot yet call Mainloop
  tools from kagent.
- Add a fake kagent gateway that serves kagent's paged, artifact-free
  ListTasks shape, sanitized fixtures, and tests for the client, the
  native session flow, the SSE encoding and the Postgres ledger (opt-in
  via MAINLOOP_TEST_DATABASE_URL).

Verified live against kind-kagent-spike with claude-subscription and
codex-subscription-https through the Mainloop API: create, send and
stream, restart reconciliation without a duplicate send, suspend and
resume with context, delete, and a clear failure status when kagent is
unreachable. The send retry and Session replacement paths are covered
by fixtures, not live runs. Backend suite passes against PostgreSQL
18; make lint and svelte-check are clean.
@oldsj
oldsj merged commit 8751a55 into main Oct 4, 2026
4 checks passed
@oldsj
oldsj deleted the kagent-a2a-client branch October 4, 2026 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant