-
Notifications
You must be signed in to change notification settings - Fork 0
Add Pi coding agent #67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Tim-Pohlmann
wants to merge
6
commits into
test/live-job-yml-plumbing
Choose a base branch
from
feat/pi-agent
base: test/live-job-yml-plumbing
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
e71154c
Add Pi coding agent
Tim-Pohlmann bdf27a5
Restrict Pi cost summation to assistant messages
Tim-Pohlmann 9b92586
Document that Pi's real last stdout line never carries cost data
Tim-Pohlmann 2aaeb5d
Add live e2e coverage for the pi agent
Tim-Pohlmann 21f2b23
Simplify PiAgent cost parsing and dedup e2e comments
Tim-Pohlmann cc2243d
Clarify agent-api-key-env is conditional for pi, not unconditional
Tim-Pohlmann File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| using Rix.Job; | ||
| using Rix.Process; | ||
| using System.Text.Json; | ||
|
|
||
| namespace Rix.Agents; | ||
|
|
||
| /// <summary> | ||
| /// <see cref="ICodingAgent"/> backed by the open-source Pi coding agent CLI: installs it via npm, | ||
| /// launches it in non-interactive JSON event mode, and reads cost from that stream. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Like <see cref="OpenCodeAgent"/>, Pi is multi-provider — <see cref="AgentConfig.Model"/> is | ||
| /// forwarded verbatim as <c>--model</c> (e.g. <c>openai/gpt-4o</c>), and the caller is responsible | ||
| /// for exporting whatever credential env var that provider expects. Pi has no per-run | ||
| /// output-token cap equivalent to <c>CLAUDE_CODE_MAX_OUTPUT_TOKENS</c>, so | ||
| /// <see cref="AgentConfig.MaxTokens"/> is not forwarded and the invocation carries no environment | ||
| /// overrides. Unlike OpenCode, Pi does support <c>--append-system-prompt</c>, so rix's system | ||
| /// prompt is passed the same way as for <see cref="ClaudeAgent"/>. | ||
| /// </remarks> | ||
| internal sealed class PiAgent : ICodingAgent | ||
| { | ||
| private const string Package = "@earendil-works/pi-coding-agent"; | ||
|
|
||
| public Task<InstallResult> EnsureInstalledAsync(RunProcessAsync runProcess, CancellationToken cancellationToken) | ||
| => CodingAgentHelper.EnsureInstalledViaNpmAsync(runProcess, "pi", Package, cancellationToken); | ||
|
|
||
| public AgentInvocation BuildInvocation(JobConfig config, string systemPrompt) | ||
| { | ||
| List<string> args = | ||
| [ | ||
| "--mode", "json", config.Agent.Prompt, "--append-system-prompt", systemPrompt, | ||
| ]; | ||
| if (!string.IsNullOrWhiteSpace(config.Agent.Model)) | ||
| args.AddRange(["--model", config.Agent.Model]); | ||
|
|
||
| return new | ||
| ( | ||
| FileName: "pi", | ||
| Arguments: args, | ||
| EnvironmentOverrides: new Dictionary<string, string>() | ||
| ); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Reads cost from Pi's JSON event stream. Pi emits per-message cost (<c>usage.cost.total</c> | ||
| /// on each assistant message), not a single cumulative total, so this sums every assistant | ||
| /// message's cost from the <c>agent_end</c> event, which carries the full message list. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Only <c>agent_end</c> lines carry the message list; the caller's other stdout lines only | ||
| /// ever have <c>agent_settled</c> as the final line, so this reliably returns <c>null</c> | ||
| /// (reported cost <c>0</c>) in normal operation today. | ||
| /// </remarks> | ||
| public decimal? ParseCost(string outputLine) => CostLine.Read(outputLine, "\"agent_end\"", ReadCost); | ||
|
|
||
| private static decimal? ReadCost(JsonElement root) | ||
| { | ||
| if (!root.TryGetProperty("messages", out var messages) || messages.ValueKind != JsonValueKind.Array) | ||
| return null; | ||
|
|
||
| decimal total = 0m; | ||
| foreach (var message in messages.EnumerateArray()) | ||
| total += ReadAssistantCost(message); | ||
| return total; | ||
| } | ||
|
|
||
| private static decimal ReadAssistantCost(JsonElement message) | ||
| { | ||
| if (!message.TryGetProperty("role", out var role) || role.GetString() != "assistant") | ||
| return 0m; | ||
| if (!message.TryGetProperty("usage", out var usage) || usage.ValueKind != JsonValueKind.Object) | ||
| return 0m; | ||
| if (!usage.TryGetProperty("cost", out var cost) || cost.ValueKind != JsonValueKind.Object) | ||
| return 0m; | ||
| if (!cost.TryGetProperty("total", out var totalCost) || totalCost.ValueKind != JsonValueKind.Number) | ||
| return 0m; | ||
| if (!totalCost.TryGetDecimal(out var v)) | ||
| return 0m; | ||
| return v; | ||
| } | ||
| } |
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| using Rix.Agents; | ||
|
|
||
| namespace Rix.Tests; | ||
|
|
||
| [TestClass] | ||
| public class PiAgentCostTests | ||
| { | ||
| [TestMethod] | ||
| public void ParseCost_SumsAssistantMessageCosts_FromAgentEnd() | ||
| { | ||
| const string line = """ | ||
| {"type":"agent_end","messages":[ | ||
| {"role":"user","content":"hi"}, | ||
| {"role":"assistant","usage":{"cost":{"input":0.01,"output":0.02,"total":0.03}}}, | ||
| {"role":"toolResult","content":"ok"}, | ||
| {"role":"assistant","usage":{"cost":{"input":0.04,"output":0.05,"total":0.09}}} | ||
| ]} | ||
| """; | ||
|
|
||
| Assert.AreEqual(0.12m, new PiAgent().ParseCost(line)); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void ParseCost_ReturnsZero_WhenAgentEndHasNoCostedMessages() | ||
| { | ||
| const string line = """{"type":"agent_end","messages":[{"role":"user","content":"hi"}]}"""; | ||
|
|
||
| Assert.AreEqual(0m, new PiAgent().ParseCost(line)); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| [DataRow("not json at all")] | ||
| [DataRow("""{"type":"message_end","messages":[]}""")] | ||
| [DataRow("{invalid json}")] | ||
| [DataRow("""{"type":123}""")] | ||
| [DataRow("[]")] | ||
| [DataRow("null")] | ||
| [DataRow("42")] | ||
| [DataRow("")] | ||
| public void ParseCost_ReturnsNull_ForNonAgentEndOrMalformedLines(string line) | ||
| { | ||
| Assert.IsNull(new PiAgent().ParseCost(line)); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void ParseCost_ReturnsNull_WhenAgentEndHasNoMessages() | ||
| { | ||
| const string line = """{"type":"agent_end"}"""; | ||
|
|
||
| Assert.IsNull(new PiAgent().ParseCost(line)); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void ParseCost_IgnoresMessagesWithNonNumericCost() | ||
| { | ||
| const string line = """ | ||
| {"type":"agent_end","messages":[ | ||
| {"role":"assistant","usage":{"cost":{"total":"free"}}}, | ||
| {"role":"assistant","usage":{"cost":{"total":0.5}}} | ||
| ]} | ||
| """; | ||
|
|
||
| Assert.AreEqual(0.5m, new PiAgent().ParseCost(line)); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void ParseCost_HandlesLeadingWhitespace() | ||
| { | ||
| const string line = """ {"type":"agent_end","messages":[{"role":"assistant","usage":{"cost":{"total":1.5}}}]}"""; | ||
|
|
||
| Assert.AreEqual(1.5m, new PiAgent().ParseCost(line)); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.