Skip to content

feat: Add support for Pi coding agent - #97

Merged
kunchenguid merged 8 commits into
kunchenguid:mainfrom
R2D2-cz:main
Apr 29, 2026
Merged

feat: Add support for Pi coding agent#97
kunchenguid merged 8 commits into
kunchenguid:mainfrom
R2D2-cz:main

Conversation

@R2D2-cz

@R2D2-cz R2D2-cz commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Pi coding agent is a minimal terminal coding harness which is a great fit for gnhf
  • Adds Pi as a supported coding agent (src/core/agents/pi.ts): runs in JSON mode, streams JSONL events, and parses structured AgentOutput from Pi's assistant reply; registers it in factory.ts and config.ts with --api-key blocked as a reserved arg.
  • Fixes two streaming correctness bugs found in review: anonymous-message usage key inflation (multiple keyless messages colliding into one counter slot) and agent_end unconditionally overwriting a latestAssistantMessage already set by an earlier message_end event.
  • Documents Pi support in README and AGENTS.md, including the --api-key restriction and the requirement to configure the Pi API key outside of gnhf.

Risk Assessment

✅ Low: All three rounds of previously identified issues have been addressed; the Pi agent implementation is now correct, well-tested, and bounded in scope.

Testing

  • Summary: Ran all 31 unit test files (421 tests total) covering the Pi agent implementation, factory dispatch, config bootstrapping, and CLI --agent flag; every test passed with no failures.
  • npx vitest run src/core/agents/pi.test.ts
  • npx vitest run src/core/agents/factory.test.ts src/core/config.test.ts src/cli.test.ts
  • npm run build && npx vitest run --exclude test/e2e.test.ts (all 31 unit test files)
  • Outcome: ✅ passed across 1 run (45.5s)

Pipeline

Updates from git push no-mistakes

✅ **Rebase** - passed

Round 1 - passed ✅

🔧 **Review** - 3 issues found → auto-fixed (2)

Round 1 - found 3 issues (2 warnings, 1 info)

  • ⚠️ src/core/agents/pi.ts:376 - rememberAssistantMessage in the agent_end handler (pi.ts:377-383) unconditionally overwrites latestAssistantMessage even when it was already set by an earlier message_end event. If Pi emits message_end followed by agent_end (a plausible sequence where agent_end signals overall run completion), the message from agent_end.messages[] replaces the dedicated per-message event. If that message differs from the one in message_end.message (e.g., it is a snapshot with truncated content), JSON.parse(finalText) will fail and trigger a spurious iteration failure. Fix: guard the agent_end body with if (!latestAssistantMessage), or split the rememberAssistantMessage function so agent_end only falls back for both the text and usage paths.
  • ⚠️ src/core/agents/pi.ts:297 - anonymousMessageKey is a single constant string "assistant-anonymous" (pi.ts:297) used as a fallback key for assistant messages that have no responseId, id, or timestamp. When multiple such messages arrive, each usageByMessageKey.set(key, usage) call (pi.ts:306) overwrites the previous entry rather than adding to it, so earlier messages' token counts are silently dropped. The cumulative sum then undercounts. Fix: use a per-invocation counter to generate unique fallback keys (e.g., "assistant-anon-" + (usageByMessageKey.size) before the set).
  • ℹ️ src/core/agents/pi.ts:390 - stopReason === "aborted" (pi.ts:390) is treated identically to stopReason === "error" and surfaces as "pi reported error: ...", which triggers gnhf's backoff streak. A Pi-side abort may indicate quota exhaustion, context overflow, or a user-initiated cancellation — conditions that may not warrant exponential backoff, or that warrant a distinct user-facing message. Worth confirming whether treating Pi's aborted the same as error is intentional, or if it should map to a graceful failure (no backoff increment) or a distinct error message.

Round 2 (auto-fix) - found 1 warning

  • ⚠️ src/core/agents/pi.ts:297 - The WeakMap anonymous-key approach assigns a unique slot per object reference. Each message_update JSONL event produces a fresh parsed object for event.message, so if Pi ever includes usage data in streaming update events for an anonymous message (no responseId/id/timestamp), every update adds a new entry to usageByMessageKey rather than overwriting the previous one, causing cumulative token counts to inflate N× over the actual value. Messages with a stable key are unaffected (their set call overwrites). Confirm whether Pi's streaming protocol ever carries usage in message_update events; if it does, the accumulation guard if (!anonymousKeyMap.has(message)) needs to also check whether the current latestAssistantMessage object should be treated as the canonical slot for that message (e.g., promote to latestAssistantMessage object identity before calling updateUsage).

Round 3 (auto-fix) - passed ✅

✅ **Test** - passed

Round 1 - passed ✅

  • npx vitest run src/core/agents/pi.test.ts
  • npx vitest run src/core/agents/factory.test.ts src/core/config.test.ts src/cli.test.ts
  • npm run build && npx vitest run --exclude test/e2e.test.ts (all 31 unit test files)
🔧 **Document** - 1 issue found → auto-fixed

Round 1 - found 1 warning

  • ⚠️ README.md:234 - Pi is the only agent where --api-key (and --api-key=) is a reserved/blocked flag in isReservedAgentArg (src/core/config.ts:131-132). Users who try agentArgsOverride.pi: ["--api-key", "..."] will get a config validation error with no explanation. The existing "Flags that gnhf manages itself…are rejected" note frames reserved args as output-shaping or local-server flags, so it doesn't signal that a credential flag is blocked. The Pi Requirements column says "configure a usable provider/model first" but doesn't clarify that the API key must be set via Pi's own config or an environment variable rather than gnhf's agentArgsOverride. A note should be added — either in the Agents table Pi row, or in the agentArgsOverride section — explaining that --api-key is blocked for Pi and that Pi API key configuration must happen outside gnhf (via Pi's own config or an environment variable).

Round 2 (auto-fix) - passed ✅

🔧 **Lint** - 3 issues found → auto-fixed

Round 1 - found 3 warnings

  • ⚠️ src/core/agents/pi.ts:336 - Prettier: line exceeds printWidth (80). rememberAssistantMessage arrow function parameters (message: unknown, streaming = false) must be reformatted to multi-line.
  • ⚠️ src/core/agents/pi.test.ts:189 - Prettier: line exceeds printWidth (80). Inline object { type: "text_delta", contentIndex: 0, delta: "hel" } assigned to assistantMessageEvent must be reformatted to multi-line.
  • ⚠️ src/core/agents/pi.test.ts:194 - Prettier: line exceeds printWidth (80). Inline object { type: "text_delta", contentIndex: 0, delta: "lo" } assigned to assistantMessageEvent must be reformatted to multi-line.

Round 2 (auto-fix) - passed ✅

✅ **Push** - passed

Round 1 - passed ✅

@kunchenguid

Copy link
Copy Markdown
Owner

hey @R2D2-cz appreciate the PR - this is great! can you resolve the conflicts and CI failures? will happily merge once that's all green

@kunchenguid kunchenguid added the ezoss/triaged Managed by ezoss label Apr 29, 2026
@kunchenguid
kunchenguid merged commit 380de4e into kunchenguid:main Apr 29, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ezoss/triaged Managed by ezoss

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants