Skip to content

feat(examples): add Pi agent to blueprint, feature examples, and combined app - #156

Draft
ross-rl wants to merge 1 commit into
pi/sdk-connectionfrom
pi/examples-app
Draft

feat(examples): add Pi agent to blueprint, feature examples, and combined app#156
ross-rl wants to merge 1 commit into
pi/sdk-connectionfrom
pi/examples-app

Conversation

@ross-rl

@ross-rl ross-rl commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

What

Makes Pi reachable from both example vehicles, over the broker's pi_json protocol, running GLM-5.2 on Runloop's dedicated Nebius endpoint.

  • examples/blueprint/Dockerfile — pins @earendil-works/pi-coding-agent@0.82.1 (the same version Reflex pins).
  • feature-examples — a pi agent (blueprint install: Pi has no catalog agent mount), pi_json in the broker-protocol map, a ctx.pi branch in single-prompt, a new session-resume-pi use case, agent-via-blueprint coverage, and a Pi column in the generated matrix.
  • combined-appPiConnectionManager, POST /api/pi/{state,queue}, usePiAgent, and Pi wiring through the registry, prompt/cancel/subscribe routes, timeline sidebar, agent sidebar and setup card.
  • llms.txt regenerated with bun run feature-compat (derived from USE_CASES only, so it is accurate without credentials).

Why

PR 4 of the Pi sub-plan. Builds on #155 (PiAxonConnection), which this PR is based on — it is not a reimplementation of the connection.

Protocol facts this encodes, each with a comment at the site:

  • No initialize() anywhere on the Pi path. Pi has no handshake, so connect() is the whole setup. This is the first module in the repo to break the connect→initialize pattern.
  • A turn ends at agent_settled, never at agent_end. agent_end carries willRetry and may be followed by more streaming; usePiAgent finalises only on agent_settled, and the use cases drain receiveTurn() rather than watching for agent_end.
  • send() resolves on acceptance, not completion; a rejected prompt throws PiCommandError immediately.
  • --mode rpc and --session-dir are broker-owned and are never set here. --session-dir must stay on the durable state root or resume will not survive a snapshot.

No secrets are committed. NEBIUS_API_KEY / NEBIUS_BASE_URL come from the environment — Runloop secrets in feature-examples, devbox environment_variables in combined-app — and the ~/.pi/agent/models.json written at launch stores only the literal "$NEBIUS_API_KEY" for Pi to interpolate.

Gated — draft on purpose

This depends on runloopai/runloop #10256 (feat(broker): plumb Pi protocol selection) being merged and deployed to the target environment. As of this PR, #10256 is open, not merged, so no live Pi devbox can be provisioned yet and this stays a draft. The code is complete and typechecks/builds/tests green; only live verification is blocked.

Two consequences of the gate, stated plainly:

  1. examples/feature-examples/compatibility.md is not regenerated here. Its rows are live-run results, and this box has no RUNLOOP_API_KEY/NEBIUS_* credentials — regenerating would replace every real pass with skip. It is left untouched rather than degraded, so bun run feature-compat --validate currently reports a missing session-resume-pi row until an operator with credentials runs the full suite on a Pi-enabled broker. llms.txt is regenerated, because it derives from USE_CASES alone. CI does not run --validate, so this does not turn CI red. The regeneration is step 10 of the runbook in the follow-up PR.
  2. No live Pi run has been performed. single-prompt, session-resume-pi and the combined-app UI path are unverified against a real devbox.

One deliberate deviation from the plan: session-resume-pi demonstrates client-driven resume (getState()newSession()switchSession(sessionFile)) rather than a devbox suspend/resume, because RunContext exposes no devbox handle — the same reason thread-resume-codex.ts is written that way. Broker-driven resume needs nothing from the client, which the use case's doc comment notes.

Verification performed

Run locally on Node 22 (repo requires >= 22):

bun run check                                        # pass
bun run typecheck                                    # pass
bun run build                                        # pass
bun run test                                         # 659 passed (29 files)
bun run --filter 'feature-examples' typecheck         # pass
bun run --filter '@runloop/example-*' typecheck       # pass
bun run --filter '@runloop/example-combined-app' build # pass
bun run feature-compat                                # all skip (no credentials)

Not run: bun run build-blueprint (needs RUNLOOP_API_KEY) and any live agent run.

PR title format

feat(examples): add Pi agent to blueprint, feature examples, and combined app

Checklist

  • PR title follows <type>(<scope>): <description> format
  • bun run check passes (lint + format)
  • bun run build passes
  • bun run test passes
  • SDK documentation updated (if applicable) — no sdk/ changes here; example READMEs updated

…ined app

Pin @earendil-works/pi-coding-agent@0.82.1 in the axon-agents blueprint and
make Pi reachable from both example vehicles over the broker's pi_json
protocol, running GLM-5.2 on Runloop's dedicated Nebius endpoint.

feature-examples gains a `pi` agent (blueprint install — Pi has no catalog
agent mount), a `ctx.pi` branch in single-prompt, and a session-resume-pi use
case. combined-app gains PiConnectionManager, /api/pi routes, usePiAgent, and
Pi wiring through the registry, timeline and setup UI.

Pi has no handshake, so connect() is the whole setup: there is no initialize()
step anywhere on the Pi path. Turns settle on agent_settled only — agent_end
may be followed by an auto-retry. `--mode rpc` and `--session-dir` stay
broker-owned and are never passed from here.

Credentials come from the environment: NEBIUS_API_KEY and NEBIUS_BASE_URL
travel as Runloop secrets (feature-examples) or devbox environment variables
(combined-app), and the models.json written at launch stores only the literal
"$NEBIUS_API_KEY" for Pi to interpolate.
this.axonEvents.push(ev);
});

conn.onTimelineEvent((ev) => {

@ross-rl ross-rl Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BUG] major — This connection only observes the push timeline and never drains receiveAgentEvents()/receiveTurn(). The SDK still enqueues every Pi frame independently, so token-by-token message_update frames (each containing a cumulative partial) accumulate for the manager's whole lifetime; the 1,000-frame mark merely warns and does not bound memory. A few long UI turns can therefore consume continually growing memory. Please start and lifecycle-manage a background drain for this example, or land an SDK-side fix in #155 that makes push-only consumption not retain a second copy.

@ross-rl ross-rl left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

Findings: 1 major, 1 minor.

  • [BUG] major — The combined-app manager uses only timeline listeners while the SDK retains a second copy of every high-volume Pi frame forever; see inline. Fixing this in #155 is also sufficient.
  • [TEST] minor — Because this PR targets a stacked branch, GitHub ran only the PR-title check. These example changes include substantial server/client TypeScript and the required feature-examples typecheck has no CI evidence. Please run the example typecheck/build in CI (or broaden the PR trigger) before landing.

Light review found the blueprint pin, pi_json mount, broker-owned RPC/session arguments, session resume recipe, and agent_settled UI boundary internally consistent.

@ross-rl

ross-rl commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

Coordinating from #155 (SDK owner here; I have not touched this PR).

The push-only queue finding is fixed in the SDK, so the combined app needs no drain loop. 05b03a0 on pi/sdk-connection bounds the pull queue: PiAxonConnectionOptions.maxQueuedFrames (default 1000) caps what receiveAgentEvents()/receiveTurn() retain, and past the cap the oldest frame is discarded with a one-time warning. Listeners still see every frame, so a manager that consumes only through onTimelineEvent() — exactly this app's pattern — now has bounded memory across any number of turns. Merge pi/sdk-connection into pi/examples-app to pick it up.

One behaviour change worth knowing about if you do add a pull consumer later: a rejected-prompt ack now only terminates receiveTurn() for the prompt it belongs to, so an undrained rejection can no longer cut a later turn short.

On the CI finding for this PR: I opened #158, which drops the branches: [main] filter from ci.yml's pull_request trigger so stacked PRs get check (Node 22 and 24) and audit. It is green. Once it merges, this PR gets a real typecheck/build result on its next push — no change needed here. Until then, it would be worth saying in the body that only conventional-commit ran, so the short check list is not mistaken for a passing build.

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