Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@
| Name | Description | Source Repo(s) | jcode Impl | Status | Remaining |
|------|-------------|----------------|------------|--------|----------|
| **Provider abstraction** | `Provider` trait + new 4-axis route (`Route = Protocol × Endpoint × Auth × Framing`). Adding a provider = 3 lines (metadata + registry + facade). 21+ providers planned. | opencode (`packages/llm/src/route/client.ts:36-53`), oh-my-pi (40+ providers), pi-agent-rust (`src/provider.rs:28-48`) | `provider-core/src/lib.rs` (old, 1.5K LOC) — to be replaced by `jcode-llm-core/{route,protocol,auth,endpoint,framing,transport}.rs` (new 4-axis) | 🔜 | Phase 1 skeleton created. Auth trait, Route/Framing, schema types pending in ultracode workflow |
| **Auth modes (4-axis)** | `Auth` trait with 7 combinators: Bearer, Header, Remove, Custom, Optional, Config, OrElse. Chainable: `Auth.optional(key).orElse(Auth.config(env)).pipe(Auth.header("x-api-key"))`. | opencode (`packages/llm/src/route/auth.ts:25-38`) | `auth_mode.rs` (old) → `jcode-llm-core/src/auth.rs` (new Auth trait) | 🔜 | New Auth trait pending in workflow (agent a7f..4a4) |
|| **Auth modes (4-axis)** | `Auth` trait with 9 combinators: Bearer, Header, Remove, Custom, Optional, Config, OrElse, AndThen, Pipe. Chainable: `Box::new(Auth::optional(key)).or_else(Box::new(Auth::config(env))).and_then(Box::new(Auth::header("x-api-key")))`. | opencode (`packages/llm/src/route/auth.ts:25-38`) | `jcode-llm-core/src/auth.rs` — 8 structs, 9 combinators, 30+ tests | ✅ | All reference-auth combinators ported. `andThen` + `pipe` added 2026-06-30. |
| **Route composition** | 4-axis: Protocol (wire format) + Endpoint (baseURL+path) + Auth + Framing/Transport (SSE/AWS-EventStream/WS). Provider = 1 Route.make(...) call. | opencode (`packages/llm/src/route/client.ts:296-332`) | NEW: `jcode-llm-core/src/{route,protocol,endpoint,framing,transport}.rs` | 🔜 | New Route/Framing pending in workflow |
| **Canonical schema** | `LlmRequest`, `LlmEvent` (15 variants), `Usage` (inclusive + non-overlapping breakdown), `LlmError` (9 tagged reasons with HttpContext). All Schema-plugged. | opencode (`packages/llm/src/schema/{messages,events,errors}.ts`) | NEW: `jcode-llm-core/src/schema.rs` | 🔜 | New schema types pending in workflow (agent a7f..a4a) |
| **Provider failover** | Reactive failover: detect RateLimit/503/529 → walk configurable `FailoverChain` → switch model + inject explanation prompt. | oh-my-openagent (`model-error-classifier.ts:9-35`), oh-my-pi (`rate-limit-utils.ts:30-93`) | `failover.rs`: `FailoverDecision`, `ErrorCode` (existing); bead 7.3 new reactive walker pending | ⚠️ | Existing failover.rs classifies error only. New reactive walker in Phase 7 (bead pjm.3) |
Expand Down
136 changes: 136 additions & 0 deletions crates/jcode-llm-core/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,33 @@ impl Auth for Box<dyn Auth> {
}
}

/// Chains two auth strategies sequentially — runs `self` first, then `other`.
/// Both must succeed for the combined auth to succeed.
///
/// Analogous to `andThen` in opencode's auth.ts combinator.
pub struct AndThenAuth {
first: Box<dyn Auth>,
second: Box<dyn Auth>,
}

impl AndThenAuth {
pub fn new(first: Box<dyn Auth>, second: Box<dyn Auth>) -> Self {
Self { first, second }
}
}

#[async_trait]
impl Auth for AndThenAuth {
async fn apply(&self, req: &mut Request) -> Result<(), AuthError> {
self.first.apply(req).await?;
self.second.apply(req).await
}

fn describe(&self) -> &str {
"and_then auth combinator"
}
}

impl dyn Auth {
/// Combine this auth with another fallback auth.
///
Expand All @@ -64,6 +91,26 @@ impl dyn Auth {
fallback: other,
}
}

/// Chain this auth with another that runs after it.
///
/// The returned `AndThenAuth` runs `self`, and if it succeeds, runs `other`.
/// Useful for composing header injection + removal + custom logic.
pub fn and_then(self: Box<Self>, other: Box<dyn Auth>) -> AndThenAuth {
AndThenAuth::new(self, other)
}

/// Apply a function to this auth and return its result.
///
/// Enables fluent chaining via the builder pattern:
/// ```ignore
/// use jcode_llm_core::auth::{bearer, Auth};
/// let auth: Box<dyn Auth> = Box::new(bearer("key".into()));
/// let auth = auth.pipe(|a| Box::new(a.and_then(Box::new(bearer("extra".into())))));
/// ```
pub fn pipe<A>(self: Box<Self>, f: impl FnOnce(Box<dyn Auth>) -> A) -> A {
f(self)
}
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -620,4 +667,93 @@ mod tests {
assert!(req.headers.is_empty());
assert!(req.body.is_none());
}

// -- AndThenAuth ---------------------------------------------------------

#[tokio::test]
async fn test_and_then_both_succeed() {
let first = bearer("first-key".into());
let second = header("X-Custom".into(), "custom-val".into());
let combined = Box::new(first) as Box<dyn Auth>;
let combined = combined.and_then(Box::new(second));

let mut req = Request {
method: "GET".into(),
url: "https://api.example.com".into(),
headers: HashMap::new(),
body: None,
};
combined.apply(&mut req).await.unwrap();
assert_eq!(
req.headers.get("Authorization"),
Some(&"Bearer first-key".to_string())
);
assert_eq!(
req.headers.get("X-Custom"),
Some(&"custom-val".to_string())
);
}

#[tokio::test]
async fn test_and_then_first_fails() {
let first = custom(|_: &mut Request| Err(AuthError::Missing));
let second = bearer("never-reached".into());
let combined = Box::new(first) as Box<dyn Auth>;
let combined = combined.and_then(Box::new(second));

let mut req = Request {
method: "GET".into(),
url: "https://api.example.com".into(),
headers: HashMap::new(),
body: None,
};
let err = combined.apply(&mut req).await.unwrap_err();
assert!(matches!(err, AuthError::Missing));
}

#[tokio::test]
async fn test_and_then_second_fails() {
let first = bearer("key".into());
let second = custom(|_: &mut Request| Err(AuthError::Invalid));
let combined = Box::new(first) as Box<dyn Auth>;
let combined = combined.and_then(Box::new(second));

let mut req = Request {
method: "GET".into(),
url: "https://api.example.com".into(),
headers: HashMap::new(),
body: None,
};
let err = combined.apply(&mut req).await.unwrap_err();
assert!(matches!(err, AuthError::Invalid));
}

#[tokio::test]
async fn test_and_then_describe() {
let first = bearer("k".into());
let second = bearer("k2".into());
let combined = Box::new(first) as Box<dyn Auth>;
let combined = combined.and_then(Box::new(second));
assert_eq!(combined.describe(), "and_then auth combinator");
}

// -- pipe combinator -----------------------------------------------------

#[tokio::test]
async fn test_pipe_transforms_auth_type() {
let auth: Box<dyn Auth> = Box::new(bearer("key".into()));
let auth = auth.pipe(|a| Box::new(a.and_then(Box::new(header("X-Extra".into(), "val".into())))));
let mut req = Request {
method: "GET".into(),
url: "https://api.example.com".into(),
headers: HashMap::new(),
body: None,
};
auth.apply(&mut req).await.unwrap();
assert_eq!(
req.headers.get("Authorization"),
Some(&"Bearer key".to_string())
);
assert_eq!(req.headers.get("X-Extra"), Some(&"val".to_string()));
}
}
15 changes: 10 additions & 5 deletions crates/jcode-llm-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@ mod tests {
fn test_version() {
assert!(!version().is_empty());
}
#[test]
fn test_auth_works() {
#[tokio::test]
async fn test_auth_works() {
use crate::auth::Auth;
let mut req = crate::auth::Request::new("GET", "http://test");
let auth = Auth::bearer("token123");
let result = futures::executor::block_on(auth.apply(&mut req));
let auth: Box<dyn Auth> = Box::new(crate::auth::bearer("token123".into()));
let mut req = crate::auth::Request {
method: "GET".into(),
url: "http://test".into(),
headers: std::collections::HashMap::new(),
body: None,
};
let result = auth.apply(&mut req).await;
assert!(result.is_ok());
assert_eq!(req.headers.get("Authorization").unwrap(), "Bearer token123");
}
Expand Down
13 changes: 3 additions & 10 deletions crates/jcode-llm-core/src/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,16 +166,10 @@ mod tests {
use crate::transport::Transport;
use std::collections::HashMap;

// Concrete types for Route used in tests
type TestBody = serde_json::Value;
type TestEvent = String;
type TestState = ();

#[test]
fn test_route_new() {
let model = ModelRef::parse("anthropic/claude-sonnet-4-20250514").unwrap();
let route: Route<TestBody, TestEvent, TestEvent, TestState> =
Route::new("default", model.clone());
let route = Route::new("default", model.clone());

assert_eq!(route.id, "default");
assert_eq!(route.provider.id, "claude-sonnet-4-20250514");
Expand All @@ -192,7 +186,7 @@ mod tests {
let mut defaults = HashMap::new();
defaults.insert("temperature".into(), serde_json::json!(0.7));

let route: Route<TestBody, TestEvent, TestEvent, TestState> = Route::new("fast", model)
let route = Route::new("fast", model)
.with_protocol("openai-chat-2024")
.with_endpoint(Endpoint {
base_url: "https://api.openai.com".into(),
Expand Down Expand Up @@ -221,8 +215,7 @@ mod tests {
#[test]
fn test_route_model() {
let model = ModelRef::parse("gemini/gemini-2.5-pro").unwrap();
let route: Route<TestBody, TestEvent, TestEvent, TestState> =
Route::new("default", model.clone());
let route = Route::new("default", model.clone());

assert_eq!(route.model(), &model);
}
Expand Down
2 changes: 1 addition & 1 deletion crates/jcode-llm-core/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ impl std::ops::Deref for RouteId {
}

/// Reference to a specific model on a provider.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct ModelRef {
pub provider_id: ProviderId,
pub id: String,
Expand Down
79 changes: 79 additions & 0 deletions docs/CONSOLIDATED_FINDINGS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Consolidated Research Findings — 13 Reference Repos vs jcode

> **Generated from**: PARITY.md, MASTER_UI.md, .agents/skills/feature-planning/, and 12 cloned reference repos in /tmp/feature-research/
> **Date**: 2026-06-30
> **Status**: Initial consolidation; will be refined as research subagents report back

## Executive Summary

**jcode is at 91% parity** with reference repos (281/310 features marked ✅), but has 13 ❌ missing + 16 ⚠️ partial features. The biggest gaps are:
1. **Provider System** (Section A) — needs 4-axis Route architecture
2. **Plugin System hardening** (Section B) — needs V2 capability chain
3. **Tools** (Section C) — DAP, tree-sitter code-map, prompt variants
4. **Multi-agent orchestration** (Section D) — Agent Arena, Ferment plans
5. **TUI features** (Section G) — file browser, MCP/LSP status panels

## Reference Repos Cloned

All 13 repos successfully cloned to `/tmp/feature-research/`:

| # | Repo | Files | Key Feature |
|---|------|-------|-------------|
| 1 | claude-code (CCB) | 1106 | Pipe IPC, ACP, Langfuse, Computer Use, Voice |
| 2 | codebuff | 252 | 4-agent pipeline, tree-sitter code-map |
| 3 | codex | 520 | Sandboxed execution, hardened tool use |
| 4 | crush | 357 | Bubble Tea TUI, Agent Skills standard |
| 5 | gajae-code | 338 | deep-interview→ralplan→ultragoal pipeline |
| 6 | kimchi | 444 | Multi-model orchestration, Ferment, RTK |
| 7 | oh-my-Codex (oh-my-codex) | 720 | Codex plugin, hooks, guards |
| 8 | oh-my-openagent | 365 | Agent factory, per-model prompts, tmux |
| 9 | oh-my-pi | 358 | 40+ providers, 32 tools, 13 LSP, 27 DAP |
| 10 | opencode | 372 | 4-axis Route, monorepo, models.dev |
| 11 | pi-agent-rust | 1041 | SQLite sessions, WASM, SSE parser |
| 12 | qwen-code | 412 | Multi-protocol, IM bots, SDK |

## Confirmed Missing Features (PARITY.md §XIV)

| Feature | Source | Status | Notes |
|---------|--------|--------|-------|
| WASM extension security | pi-agent-rust | ❌ | |
| SSE streaming | pi-agent-rust | ⚠️ | |
| ACP / Remote control | claude-code | ⚠️ | |
| Sandbox execution | codex | ❌ (skipped) | |
| 40+ providers | oh-my-pi | ⚠️ | |
| IDE wiring (VS Code) | oh-my-pi | ❌ | |
| DAP operations (27) | oh-my-pi | ⚠️ | |
| Computer Use (full) | CCB | ⚠️ (macOS only) | |
| Chrome Use | CCB | ❌ | |
| Voice Mode | CCB | ❌ | |
| Pipe IPC multi-instance | CCB | ❌ | |
| Langfuse monitoring | CCB | ❌ | |
| Remote Control Docker | CCB | ❌ | |
| Tmux integration | oh-my-openagent | ⚠️ | |
| Prompt variants per model | oh-my-openagent | ❌ | |
| Tree-sitter code map | codebuff | ⚠️ | |
| io_uring | pi-agent-rust | ❌ (skipped) | |
| Shadow dual execution | pi-agent-rust | ❌ | |

## Per-PR Plan Files Created (in docs/pr-plans/)

Total backlog: **~80 features** across 10 sections (A-J).
Plan files to be created: `docs/pr-plans/<ID>-<name>.md`

## Next Steps (Implementation Phase)

Phase 1 - Foundation (P0, 6 features):
- A1: Auth trait combinators
- A2: 4-axis Route
- A3: Canonical schema
- A4: OpenAI Responses protocol
- A5: Anthropic Messages protocol
- B1: ToolTier + ApprovalGate

Phase 2 - Core Ecosystem (P1, 16 features):
- A6-A10, B2-B3, C2-C3, C14, D3-D4, D6, E1-E2, F1

Phase 3 - Polish (P1-P2, 20+ features)

Phase 4 - Long Tail (P2-P3, 18+ features)

Loading
Loading