Skip to content

Commit e7e0aa1

Browse files
feat(server): add Pi coding agent adapter with Azure OpenAI support
Add Pi (@mariozechner/pi-coding-agent) as a third AI agent backend alongside Claude Code and OpenCode. Pi is a provider-agnostic TypeScript coding agent that supports Azure OpenAI, OpenAI, and Anthropic. Key changes: - PiAgentAdapter: builds sandbox spec, runner script with write-tool output, Swift escape sanitization, self-correction retries via -c - PI-AGENTS.md: 42-line orchestrator prompt (vs 268 for Claude Code) - AZURE_OPENAI provider: new LlmProvider enum, proxy route, config - Shared loadClasspathResource() extracted to AgentAdapter interface - sanitizeBodyForAzure now keys off provider enum (not URL heuristic) - Docker image: node:22-slim + Pi 0.65.0 + Bun + precompute infra - CI: agent-pi-build job in docker build workflow Tested end-to-end: 80% accuracy on 3 MRs, 0.848 NEG F1. Delivery verified posting MR summaries + inline diff notes to GitLab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ecafdba commit e7e0aa1

20 files changed

Lines changed: 1155 additions & 72 deletions

.github/workflows/ci-docker-build.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,24 @@ jobs:
162162
org.opencontainers.image.vendor=AET TUM
163163
org.opencontainers.image.licenses=MIT
164164
hephaestus.component=agent-opencode
165+
166+
agent-pi-build:
167+
name: "Agent: Pi"
168+
if: inputs.should_skip != 'true' && inputs.agent_images_changed == 'true'
169+
uses: ./.github/workflows/reusable-docker-build.yml
170+
with:
171+
image-name: "ls1intum/hephaestus/agent-pi"
172+
docker-file: "./docker/agents/pi/Dockerfile"
173+
docker-context: "./docker/agents"
174+
registry: "ghcr.io"
175+
tags: |
176+
${{ github.ref_name }}
177+
${{ github.sha }}
178+
ci-${{ github.run_number }}
179+
${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || 'latest' }}
180+
labels: |
181+
org.opencontainers.image.title=Hephaestus Agent - Pi
182+
org.opencontainers.image.description=Sandboxed Pi coding agent for AI-powered code review
183+
org.opencontainers.image.vendor=AET TUM
184+
org.opencontainers.image.licenses=MIT
185+
hephaestus.component=agent-pi

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.DS_Store
2+
.context
23

34
# IDE files
45
.project

docker/agents/pi/.dockerignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.git
2+
node_modules
3+
*.md

docker/agents/pi/Dockerfile

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# syntax=docker/dockerfile:1.4
2+
# Pi coding agent container image
3+
# Used by PiAgentAdapter — command set by SandboxSpec, no entrypoint.
4+
ARG NODE_TAG=22-slim
5+
FROM node:${NODE_TAG}
6+
7+
RUN apt-get update -qq && apt-get install -y --no-install-recommends git findutils tree jq curl ca-certificates unzip && \
8+
rm -rf /var/lib/apt/lists/*
9+
10+
ARG PI_VERSION=0.65.0
11+
RUN npm install -g @mariozechner/pi-coding-agent@${PI_VERSION} && \
12+
npm cache clean --force && \
13+
mkdir -p /home/agent/.pi /workspace && \
14+
chown -R 1000:1000 /home/agent /workspace
15+
16+
# Bun runtime for precomputation scripts (static analysis before agent runs)
17+
ARG BUN_VERSION=1.3.11
18+
RUN curl -fsSL "https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/bun-linux-x64.zip" -o /tmp/bun.zip && \
19+
unzip -o /tmp/bun.zip -d /tmp && \
20+
mv /tmp/bun-linux-x64/bun /usr/local/bin/bun && \
21+
chmod +x /usr/local/bin/bun && \
22+
rm -rf /tmp/bun*
23+
24+
# Precompute runner + shared libraries (practice scripts injected at runtime from DB)
25+
COPY --chown=1000:1000 precompute/runner.ts /opt/precompute/runner.ts
26+
COPY --chown=1000:1000 precompute/lib/ /opt/precompute/lib/
27+
28+
# Git security: neutralize hooks and external commands from mounted repos.
29+
# System-level config cannot be overridden by repo-local .git/config.
30+
RUN git config --system core.hooksPath /nonexistent && \
31+
git config --system core.fsmonitor false && \
32+
git config --system safe.directory /workspace/repo
33+
34+
ENV HOME=/home/agent
35+
ENV GIT_PAGER=cat
36+
ENV GIT_TERMINAL_PROMPT=0
37+
ENV LANG=C.UTF-8
38+
39+
USER 1000:1000
40+
WORKDIR /workspace

server/application-server/src/main/java/de/tum/in/www1/hephaestus/agent/AgentType.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
* <ul>
88
* <li>{@link #CLAUDE_CODE} — requires {@link LlmProvider#ANTHROPIC}</li>
99
* <li>{@link #OPENCODE} — any provider</li>
10+
* <li>{@link #PI} — any provider (multi-provider via Pi coding agent)</li>
1011
* </ul>
1112
*/
1213
public enum AgentType {
1314
CLAUDE_CODE,
1415
OPENCODE,
16+
PI,
1517
}

server/application-server/src/main/java/de/tum/in/www1/hephaestus/agent/LlmProvider.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66
public enum LlmProvider {
77
ANTHROPIC,
88
OPENAI,
9+
AZURE_OPENAI,
910
}

server/application-server/src/main/java/de/tum/in/www1/hephaestus/agent/adapter/AgentAdapterConfiguration.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ public AgentAdapter openCodeAgentAdapter(ObjectMapper objectMapper) {
2626
return new OpenCodeAgentAdapter(objectMapper);
2727
}
2828

29+
@Bean
30+
public AgentAdapter piAgentAdapter(ObjectMapper objectMapper) {
31+
return new PiAgentAdapter(objectMapper);
32+
}
33+
2934
@Bean
3035
public AgentAdapterRegistry agentAdapterRegistry(List<AgentAdapter> adapters) {
3136
return new AgentAdapterRegistry(adapters);

server/application-server/src/main/java/de/tum/in/www1/hephaestus/agent/adapter/ClaudeCodeAgentAdapter.java

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
import de.tum.in.www1.hephaestus.agent.adapter.spi.AgentAdapterRequest;
88
import de.tum.in.www1.hephaestus.agent.adapter.spi.AgentSandboxSpec;
99
import de.tum.in.www1.hephaestus.agent.sandbox.spi.SandboxResult;
10-
import java.io.IOException;
11-
import java.io.InputStream;
1210
import java.nio.charset.StandardCharsets;
1311
import java.util.HashMap;
1412
import java.util.LinkedHashMap;
@@ -72,7 +70,7 @@ public AgentSandboxSpec buildSandboxSpec(AgentAdapterRequest request) {
7270
inputFiles.put(".json-schema", buildJsonSchema());
7371

7472
// Claude Code-specific orchestrator file (auto-discovered by Claude Code CLI)
75-
inputFiles.put("CLAUDE.md", loadClasspathResource("CLAUDE.md"));
73+
inputFiles.put("CLAUDE.md", AgentAdapter.loadClasspathResource("CLAUDE.md"));
7674

7775
// Node.js runner script handles: run claude → validate output → retry via --continue
7876
long agentTimeoutMs = Math.max(60_000L, (long) (request.timeoutSeconds() - TIMEOUT_BUFFER_SECONDS) * 1000);
@@ -502,22 +500,6 @@ private boolean isValidJsonWithFindings(String text) {
502500
}
503501
}
504502

505-
/** Classpath prefix for agent resource files. */
506-
private static final String AGENT_RESOURCE_PREFIX = "agent/";
507-
508-
/** Load a classpath resource from the {@code agent/} directory. */
509-
private static byte[] loadClasspathResource(String relativePath) {
510-
String fullPath = AGENT_RESOURCE_PREFIX + relativePath;
511-
try (InputStream is = ClaudeCodeAgentAdapter.class.getClassLoader().getResourceAsStream(fullPath)) {
512-
if (is == null) {
513-
throw new IllegalStateException("Missing classpath resource: " + fullPath);
514-
}
515-
return is.readAllBytes();
516-
} catch (IOException e) {
517-
throw new IllegalStateException("Failed to read classpath resource: " + fullPath, e);
518-
}
519-
}
520-
521503
/**
522504
* Build the shell auth setup prefix and populate env vars based on credential mode.
523505
*/

server/application-server/src/main/java/de/tum/in/www1/hephaestus/agent/adapter/OpenCodeAgentAdapter.java

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
import de.tum.in.www1.hephaestus.agent.adapter.spi.AgentAdapterRequest;
1010
import de.tum.in.www1.hephaestus.agent.adapter.spi.AgentSandboxSpec;
1111
import de.tum.in.www1.hephaestus.agent.sandbox.spi.SandboxResult;
12-
import java.io.IOException;
13-
import java.io.InputStream;
1412
import java.nio.charset.StandardCharsets;
1513
import java.util.HashMap;
1614
import java.util.LinkedHashMap;
@@ -66,8 +64,14 @@ public AgentSandboxSpec buildSandboxSpec(AgentAdapterRequest request) {
6664

6765
// OpenCode agent definitions loaded from classpath resources.
6866
// The orchestrator (practice-review) spawns per-practice subagents via the task tool.
69-
inputFiles.put(".opencode/agents/practice-review.md", loadClasspathResource("opencode-orchestrator.md"));
70-
inputFiles.put(".opencode/agents/practice-analyzer.md", loadClasspathResource("opencode-practice-analyzer.md"));
67+
inputFiles.put(
68+
".opencode/agents/practice-review.md",
69+
AgentAdapter.loadClasspathResource("opencode-orchestrator.md")
70+
);
71+
inputFiles.put(
72+
".opencode/agents/practice-analyzer.md",
73+
AgentAdapter.loadClasspathResource("opencode-practice-analyzer.md")
74+
);
7175

7276
// Use a Node.js wrapper to invoke opencode with spawnSync.
7377
// This bypasses shell variable handling entirely — the prompt file is read
@@ -375,6 +379,7 @@ private String buildProxyEnvAliases(AgentAdapterRequest request) {
375379
return switch (request.llmProvider()) {
376380
case OPENAI -> " && export OPENAI_BASE_URL=$LLM_PROXY_URL && export OPENAI_API_KEY=$LLM_PROXY_TOKEN";
377381
case ANTHROPIC -> " && export ANTHROPIC_BASE_URL=$LLM_PROXY_URL && export ANTHROPIC_API_KEY=$LLM_PROXY_TOKEN";
382+
case AZURE_OPENAI -> " && export OPENAI_BASE_URL=$LLM_PROXY_URL && export OPENAI_API_KEY=$LLM_PROXY_TOKEN";
378383
};
379384
}
380385

@@ -489,22 +494,6 @@ NdjsonParseResult parseNdjson(String ndjsonContent) {
489494
return new NdjsonParseResult(text, usage);
490495
}
491496

492-
/** Classpath prefix for agent resource files. */
493-
private static final String AGENT_RESOURCE_PREFIX = "agent/";
494-
495-
/** Load a classpath resource from the {@code agent/} directory. */
496-
private static byte[] loadClasspathResource(String relativePath) {
497-
String fullPath = AGENT_RESOURCE_PREFIX + relativePath;
498-
try (InputStream is = OpenCodeAgentAdapter.class.getClassLoader().getResourceAsStream(fullPath)) {
499-
if (is == null) {
500-
throw new IllegalStateException("Missing classpath resource: " + fullPath);
501-
}
502-
return is.readAllBytes();
503-
} catch (IOException e) {
504-
throw new IllegalStateException("Failed to read classpath resource: " + fullPath, e);
505-
}
506-
}
507-
508497
void configureAuth(AgentAdapterRequest request, Map<String, String> env) {
509498
switch (request.credentialMode()) {
510499
case PROXY -> {
@@ -518,6 +507,7 @@ void configureAuth(AgentAdapterRequest request, Map<String, String> env) {
518507
switch (request.llmProvider()) {
519508
case ANTHROPIC -> env.put("ANTHROPIC_API_KEY", request.credential());
520509
case OPENAI -> env.put("OPENAI_API_KEY", request.credential());
510+
case AZURE_OPENAI -> env.put("OPENAI_API_KEY", request.credential());
521511
}
522512
}
523513
}
@@ -543,7 +533,7 @@ byte[] buildConfigJson(AgentAdapterRequest request) {
543533
// In proxy mode, OPENAI_BASE_URL/ANTHROPIC_BASE_URL env vars redirect to the proxy.
544534
String providerPrefix = switch (request.llmProvider()) {
545535
case ANTHROPIC -> "anthropic";
546-
case OPENAI -> "openai";
536+
case OPENAI, AZURE_OPENAI -> "openai";
547537
};
548538
config.put("model", providerPrefix + "/" + model);
549539

0 commit comments

Comments
 (0)