Magpie is an autonomous AI coding pipeline built with Rust, designed to take tasks from chat platforms (Discord, Teams, CLI) and execute them end-to-end: from code generation to testing, linting, and opening pull requests.
Core Components
Pipeline Orchestrator Coordinates the full workflow from task receipt to PR creation
Blueprint Engine Executes deterministic + agent steps with conditional flow control
Two-Tier Agents Tier 1 for text generation, Tier 2 for coding tasks
Sandbox Abstraction Isolates execution in local or remote environments
System Architecture
Two-Tier Agent Architecture
Magpie uses two tiers of LLM interaction, each optimized for its purpose:
Tier 1: Claude CLI (Simple Text Generation)
Used for: Branch name generation, task classification, commit messages
Mechanism: Direct claude -p calls — one-shot, clean text responses
async fn claude_call ( prompt : & str , step_name : & str , trace_dir : Option < & PathBuf >) -> Result < String > {
let mut tb = TraceBuilder :: new ( step_name , prompt );
let output = tokio :: process :: Command :: new ( "claude" )
. args ([ "-p" , prompt ])
. env_remove ( "CLAUDECODE" )
. output ()
. await
. context ( "failed to run `claude` CLI — is it installed and on PATH?" ) ? ;
if ! output . status . success () {
let stderr = String :: from_utf8_lossy ( & output . stderr);
tb . record_event ( EventKind :: Error , & stderr );
let trace = tb . finish ( "" );
if let Some ( dir ) = trace_dir {
let _ = trace :: write_trace ( & trace , dir );
}
anyhow :: bail! ( "claude CLI failed: {stderr}" );
}
let response = String :: from_utf8_lossy ( & output . stdout) . trim () . to_string ();
Ok ( response )
}
Tier 1 bypasses Goose entirely to avoid streaming fragmentation. It’s perfect for tasks that need a single, clean text response.
Tier 2: MagpieAgent (Full Coding Tasks)
Used for: File edits, shell commands, test writing, implementation
Mechanism: Goose agent loop with streaming and tool access
pub struct MagpieAgent {
config : MagpieConfig ,
agent : Agent ,
session_manager : Arc < SessionManager >,
}
impl MagpieAgent {
pub fn new ( config : MagpieConfig ) -> Result < Self > {
let agent = Agent :: new ();
let session_manager = Arc :: new ( SessionManager :: instance ());
Ok ( Self { config , agent , session_manager })
}
}
The agent runs prompts with full tool access:
pub async fn run (
& self ,
prompt : & str ,
working_dir : Option < & PathBuf >,
step_name : Option < & str >,
) -> Result < String > {
let step_label = step_name . unwrap_or ( "agent" );
let mut tb = TraceBuilder :: new ( step_label , prompt );
// Create provider
let provider = create_with_named_model (
& self . config . provider,
& self . config . model,
Vec :: < ExtensionConfig > :: new (),
) . await ? ;
// Create session with working directory
let working_dir = match working_dir {
Some ( dir ) => dir . clone (),
None => std :: env :: current_dir () . unwrap_or_else ( | _ | PathBuf :: from ( "." )),
};
let session = self
. session_manager
. create_session ( working_dir , "magpie" . to_string (), SessionType :: Hidden )
. await ? ;
self . agent . update_provider ( provider , & session . id) . await ? ;
let session_config = SessionConfig {
id : session . id . clone (),
schedule_id : None ,
max_turns : Some ( self . config . max_turns),
retry_config : None ,
};
let user_message = Message :: user () . with_text ( prompt );
let mut stream = self . agent . reply ( user_message , session_config , None ) . await ? ;
// Collect assistant text and trace events
let mut response = String :: new ();
while let Some ( event ) = stream . next () . await {
match event {
Ok ( AgentEvent :: Message ( msg )) => {
for content in msg . content . iter () {
if let Some ( text ) = content . as_text () {
response . push_str ( text );
tb . record_event ( EventKind :: Text , text );
}
}
}
Err ( e ) => tb . record_event ( EventKind :: Error , & e . to_string ()),
_ => {}
}
}
Ok ( response )
}
All chat adapters implement a unified interface:
#[async_trait]
pub trait ChatPlatform : Send + Sync {
/// Platform identifier (e.g., "discord", "teams", "slack").
fn name ( & self ) -> & str ;
/// Fetch conversation history from a channel/thread, formatted as text.
async fn fetch_history ( & self , channel_id : & str ) -> Result < String >;
/// Send a message to a channel/thread.
async fn send_message ( & self , channel_id : & str , text : & str ) -> Result <()>;
/// Close / archive a thread after the final response has been sent.
///
/// Default is a no-op — platforms that support thread archiving (e.g. Discord)
/// override this.
async fn close_thread ( & self , _channel_id : & str ) -> Result <()> {
Ok (())
}
}
This keeps magpie-core platform-agnostic. Discord, Teams, and CLI all plug in through the same interface.
Pipeline Result
Every pipeline run returns a structured result:
#[derive( Debug , Clone , Serialize , Deserialize )]
pub struct PipelineResult {
pub output : String ,
pub pr_url : Option < String >,
pub plane_issue_id : Option < String >,
pub ci_passed : bool ,
pub rounds_used : u32 ,
pub status : PipelineStatus ,
}
#[derive( Debug , Clone , PartialEq , Eq , Serialize , Deserialize )]
pub enum PipelineStatus {
Success , // Agent + CI passed
PartialSuccess , // Agent passed, CI failed
AgentFailed , // Agent execution failed
SetupFailed , // Pre-flight checks failed
}
Chat adapters format this into concise status messages rather than dumping raw agent output.
Design Principles
Deterministic steps + agent steps. The engine controls flow, not the LLM.
The chat thread IS the requirements. No duplication or external docs.
Local lint before CI. TDD for Standard tasks (write tests before implementation).
Max 2 rounds. Partial success is OK — better to have a PR with known issues than no progress.
Each task gets only the tools it needs. No kitchen-sink access.
Isolated devbox per run. Clean slate, no cross-contamination.
Right tool for the job. Tier 1 for text, Tier 2 for coding.
Task complexity determines the blueprint path (Simple/Standard/BugFix).
Project Structure
crates/
magpie-core/ # Core library — all other crates depend on this
├── agent.rs # MagpieAgent (Tier 2 wrapper)
├── pipeline.rs # Pipeline orchestrator + Tier 1 calls
├── blueprint/ # Blueprint engine
├── sandbox/ # Sandbox abstraction
├── git.rs # Git operations
├── platform.rs # ChatPlatform trait
└── ...
magpie-cli/ # CLI binary
magpie-discord/ # Discord bot adapter (Serenity)
magpie-teams/ # Teams webhook adapter (Axum)
Most work happens in magpie-core. The adapters are thin: they implement ChatPlatform, build a PipelineConfig from env vars, and call run_pipeline().
Next Steps
Pipeline Flow Understand the full pipeline execution from task to PR
Blueprint Engine Learn how blueprints orchestrate deterministic + agent steps
Task Classification See how tasks are classified into Simple, Standard, and BugFix
Sandbox Abstraction Explore local and remote sandbox implementations