Skip to content

Commit b15f5e0

Browse files
author
Jordan
committed
Add OMP (Oh-My-Pi) as a first-class importable provider
Parameterizes the Pi adapter over a PiFamilyConfig (binary name, home dir, env-var prefix, display label, MCP adapter marker) and ships OMP_FAMILY alongside PI_FAMILY. OmpRpcAgentClient is a thin subclass of PiRpcAgentClient that injects OMP_FAMILY. OMP and Pi share session lifecycle, history mapping, tool-call translation, permission dialog bridging, MCP adapter detection, and the --mode rpc client. Only the binary name (omp vs pi), home directory (~/.omp vs ~/.pi), env-var prefix (OMP_CODING_AGENT_* vs PI_CODING_AGENT_*), and display label differ. OMP sessions started outside Paseo (in the terminal) are discovered by reading ~/.omp/agent/sessions/**/*.jsonl via the new providers/omp/session-descriptor.ts. This mirrors the historical Pi descriptor (removed upstream in #1154 in favor of runtime extension capture) but is scoped to OMP paths so it does not contradict the Pi direction — Pi continues to use paseo_capture_entries at runtime. Pi behavior is unchanged. All 34 pre-existing Pi tests pass. 11 new tests cover OMP-side session discovery, env-var precedence, family isolation (OMP cannot see Pi sessions and vice versa), and the OmpRpcAgentClient subclass surface.
1 parent 423956c commit b15f5e0

16 files changed

Lines changed: 1140 additions & 67 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## Unreleased
2+
3+
### Added
4+
5+
- **OMP (Oh-My-Pi) as a first-class importable provider** alongside Pi. OMP is a downstream fork of Pi (`@oh-my-pi/pi-coding-agent`) that ships its own binary (`omp`) and home directory (`~/.omp`) but preserves Pi's `--mode rpc` wire protocol and JSONL session schema. The Pi adapter is now parameterized over a `PiFamilyConfig` so both providers share session lifecycle, history mapping, tool-call translation, permission dialogs, and the RPC client.
6+
- Surfaces as **OMP** in `paseo provider ls`, the in-app provider picker, and `paseo import --provider omp`
7+
- OMP sessions started outside Paseo (in the terminal) are discovered by reading `~/.omp/agent/sessions/**/*.jsonl` (overridable via `$OMP_CODING_AGENT_DIR`, `$OMP_CODING_AGENT_SESSION_DIR`, or `settings.json:sessionDir`)
8+
- Custom providers can `extends: "omp"` in `~/.paseo/config.json` for alternative OMP binaries or wrappers
9+
110
# Changelog
211

312
## 0.1.81 - 2026-05-24

docs/providers.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The only built-in ACP provider today is `copilot` (`copilot-acp-agent.ts`). `Gen
1414

1515
Implement the `AgentClient` and `AgentSession` interfaces from `agent-sdk-types.ts` yourself. This gives full control but requires you to handle process management, streaming, permissions, and session persistence from scratch.
1616

17-
Existing direct providers: `claude` (in `providers/claude/agent.ts`), `codex` (`codex-app-server-agent.ts`), `opencode` (`opencode-agent.ts`), `pi` (`providers/pi/agent.ts`). The dev-only `mock` provider (`mock-load-test-agent.ts`) is also direct.
17+
Existing direct providers: `claude` (in `providers/claude/agent.ts`), `codex` (`codex-app-server-agent.ts`), `opencode` (`opencode-agent.ts`), `pi` (`providers/pi/agent.ts`), `omp` (`providers/omp/agent.ts`). The dev-only `mock` provider (`mock-load-test-agent.ts`) is also direct.
1818

1919
Pi is a process-backed provider. Paseo requires the user to have the `pi` binary installed and talks to it through `pi --mode rpc`; the server package does not embed Pi's SDK/runtime packages.
2020

@@ -24,6 +24,10 @@ Pi MCP support depends on the open-source `pi-mcp-adapter` extension being loade
2424

2525
Pi import discovery reads Pi's persisted JSONL session files because Pi RPC does not expose a recent-session listing command. Resume and full history hydration still go through `pi --mode rpc` using the session file as `nativeHandle`.
2626

27+
OMP (Oh-My-Pi, https://github.com/oh-my-pi/pi-coding-agent) is a downstream fork of Pi that ships its own binary (`omp`) and home directory (`~/.omp`). Because OMP preserves Pi's `--mode rpc` protocol and JSONL session schema byte-for-byte, the `omp` provider reuses the Pi adapter via a `PiFamilyConfig` parameter declared in `providers/pi/family-config.ts` (`PI_FAMILY` and `OMP_FAMILY`). `OmpRpcAgentClient` in `providers/omp/agent.ts` is a thin subclass of `PiRpcAgentClient` that injects `OMP_FAMILY`. Both providers share session lifecycle, history mapping, tool-call translation, MCP adapter detection, and the RPC client; only the binary name, home dir, env-var prefix (`OMP_CODING_AGENT_*` instead of `PI_CODING_AGENT_*`), and display label differ.
28+
29+
OMP import discovery reads OMP's persisted JSONL session files via `providers/omp/session-descriptor.ts` (resolving `OMP_CODING_AGENT_DIR`, `OMP_CODING_AGENT_SESSION_DIR`, `~/.omp/agent/settings.json:sessionDir`, `<cwd>/.omp/settings.json:sessionDir`, then defaulting to `~/.omp/agent/sessions/`). Pi's runtime-side capture (`paseo_capture_entries` extension) is the canonical Pi import path; offline JSONL parsing for Pi was removed in #1154.
30+
2731
Pi RPC extension UI dialog requests (`select`, `input`, `editor`, `confirm`) are bridged into Paseo question permissions and answered with `extension_ui_response`. Fire-and-forget extension UI requests such as notifications are intentionally ignored by the provider adapter unless Paseo grows first-class UI for them.
2832

2933
OpenCode MCP injection is dynamic and session-scoped. Call OpenCode's `mcp.add` endpoint with the MCP server config and do not follow it with `mcp.connect`; `connect` only toggles MCP servers already present in OpenCode's own config. New OpenCode versions return `McpServerNotFoundError`/404 for `connect` after a dynamic add because the server is not config-backed, while older versions silently swallowed the same missing-config path.

packages/cli/src/commands/agent/import.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { CommandError, CommandOptions, SingleResult } from "../../output/in
55
import { agentRunSchema, type AgentRunResult } from "./run.js";
66
import type { AgentSnapshotPayload } from "@getpaseo/server";
77

8-
const IMPORT_PROVIDER_LIST = ["claude", "codex", "opencode", "pi", "acp"] as const;
8+
const IMPORT_PROVIDER_LIST = ["claude", "codex", "opencode", "pi", "omp", "acp"] as const;
99
const IMPORT_PROVIDERS = new Set<string>(IMPORT_PROVIDER_LIST);
1010
const IMPORT_PROVIDER_HELP = IMPORT_PROVIDER_LIST.join(", ");
1111

packages/server/src/server/agent/provider-manifest.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,13 @@ export const AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [
206206
defaultModeId: null,
207207
modes: [],
208208
},
209+
{
210+
id: "omp",
211+
label: "OMP",
212+
description: "Oh-My-Pi: Pi fork with extended plugin ecosystem (~/.omp)",
213+
defaultModeId: null,
214+
modes: [],
215+
},
209216
];
210217

211218
export const DEV_AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [

packages/server/src/server/agent/provider-registry.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { GenericACPAgentClient } from "./providers/generic-acp-agent.js";
3030
import { OpenCodeAgentClient } from "./providers/opencode-agent.js";
3131
import { OpenCodeServerManager } from "./providers/opencode/server-manager.js";
3232
import { PiRpcAgentClient } from "./providers/pi/agent.js";
33+
import { OmpRpcAgentClient } from "./providers/omp/agent.js";
3334
import { MockLoadTestAgentClient } from "./providers/mock-load-test-agent.js";
3435
import {
3536
AGENT_PROVIDER_DEFINITIONS,
@@ -125,6 +126,11 @@ const PROVIDER_CLIENT_FACTORIES: Record<string, ProviderClientFactory> = {
125126
logger,
126127
runtimeSettings,
127128
}),
129+
omp: (logger, runtimeSettings) =>
130+
new OmpRpcAgentClient({
131+
logger,
132+
runtimeSettings,
133+
}),
128134
mock: (logger) => new MockLoadTestAgentClient(logger),
129135
};
130136

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
2+
import { tmpdir } from "node:os";
3+
import path from "node:path";
4+
import pino from "pino";
5+
import { describe, expect, test } from "vitest";
6+
7+
import type { AgentSessionConfig } from "../../agent-sdk-types.js";
8+
import { FakePi } from "../pi/test-utils/fake-pi.js";
9+
import { OmpRpcAgentClient } from "./agent.js";
10+
11+
function createClientWithOmpAgentDir(agentDir: string): OmpRpcAgentClient {
12+
return new OmpRpcAgentClient({
13+
logger: pino({ level: "silent" }),
14+
runtime: new FakePi(),
15+
runtimeSettings: { env: { OMP_CODING_AGENT_DIR: agentDir } },
16+
});
17+
}
18+
19+
function createConfig(overrides: Partial<AgentSessionConfig> = {}): AgentSessionConfig {
20+
return {
21+
provider: "omp",
22+
cwd: "/tmp/paseo-omp-rpc-test",
23+
...overrides,
24+
};
25+
}
26+
27+
describe("OmpRpcAgentClient", () => {
28+
test("identifies itself with the omp provider id", () => {
29+
const client = new OmpRpcAgentClient({
30+
logger: pino({ level: "silent" }),
31+
runtime: new FakePi(),
32+
});
33+
expect(client.provider).toBe("omp");
34+
});
35+
36+
test("creates a session that reports the omp provider", async () => {
37+
const client = new OmpRpcAgentClient({
38+
logger: pino({ level: "silent" }),
39+
runtime: new FakePi(),
40+
});
41+
42+
const session = await client.createSession(createConfig());
43+
expect(session.provider).toBe("omp");
44+
await session.close();
45+
});
46+
47+
test("lists persisted OMP sessions from the configured OMP agent directory", async () => {
48+
const root = mkdtempSync(path.join(tmpdir(), "paseo-omp-client-"));
49+
const cwd = path.join(root, "workspace");
50+
const agentDir = path.join(root, "agent");
51+
const sessionsDir = path.join(agentDir, "sessions", "--workspace--");
52+
mkdirSync(sessionsDir, { recursive: true });
53+
const sessionFile = path.join(sessionsDir, "20260101_session.jsonl");
54+
writeFileSync(
55+
sessionFile,
56+
[
57+
JSON.stringify({
58+
type: "session",
59+
version: 3,
60+
id: "omp-session",
61+
timestamp: "2026-01-01T00:00:00.000Z",
62+
cwd,
63+
}),
64+
JSON.stringify({
65+
type: "message",
66+
id: "entry-1",
67+
parentId: null,
68+
timestamp: "2026-01-01T00:00:01.000Z",
69+
message: { role: "user", content: "remember this" },
70+
}),
71+
].join("\n") + "\n",
72+
"utf8",
73+
);
74+
const client = createClientWithOmpAgentDir(agentDir);
75+
76+
await expect(client.listPersistedAgents({ cwd })).resolves.toMatchObject([
77+
{
78+
provider: "omp",
79+
sessionId: "omp-session",
80+
cwd,
81+
persistence: {
82+
provider: "omp",
83+
sessionId: "omp-session",
84+
nativeHandle: sessionFile,
85+
metadata: { provider: "omp", cwd },
86+
},
87+
timeline: [{ type: "user_message", text: "remember this" }],
88+
},
89+
]);
90+
});
91+
92+
test("ignores PI_CODING_AGENT_DIR when running as the OMP provider", async () => {
93+
const root = mkdtempSync(path.join(tmpdir(), "paseo-omp-isolation-"));
94+
const cwd = path.join(root, "workspace");
95+
const piAgentDir = path.join(root, "pi-agent");
96+
const ompAgentDir = path.join(root, "omp-agent");
97+
const piSessionsDir = path.join(piAgentDir, "sessions");
98+
const ompSessionsDir = path.join(ompAgentDir, "sessions");
99+
mkdirSync(piSessionsDir, { recursive: true });
100+
mkdirSync(ompSessionsDir, { recursive: true });
101+
102+
const sessionRecord = (id: string) =>
103+
[
104+
JSON.stringify({
105+
type: "session",
106+
version: 3,
107+
id,
108+
timestamp: "2026-01-01T00:00:00.000Z",
109+
cwd,
110+
}),
111+
JSON.stringify({
112+
type: "message",
113+
id: "entry-1",
114+
parentId: null,
115+
timestamp: "2026-01-01T00:00:01.000Z",
116+
message: { role: "user", content: id },
117+
}),
118+
].join("\n") + "\n";
119+
writeFileSync(path.join(piSessionsDir, "pi.jsonl"), sessionRecord("pi-only"), "utf8");
120+
writeFileSync(path.join(ompSessionsDir, "omp.jsonl"), sessionRecord("omp-only"), "utf8");
121+
122+
const client = new OmpRpcAgentClient({
123+
logger: pino({ level: "silent" }),
124+
runtime: new FakePi(),
125+
runtimeSettings: {
126+
env: {
127+
PI_CODING_AGENT_DIR: piAgentDir,
128+
OMP_CODING_AGENT_DIR: ompAgentDir,
129+
},
130+
},
131+
});
132+
133+
const descriptors = await client.listPersistedAgents({ cwd });
134+
expect(descriptors.map((d) => d.sessionId)).toEqual(["omp-only"]);
135+
});
136+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { Logger } from "pino";
2+
3+
import type {
4+
ListPersistedAgentsOptions,
5+
PersistedAgentDescriptor,
6+
} from "../../agent-sdk-types.js";
7+
import type { ProviderRuntimeSettings } from "../../provider-launch-config.js";
8+
import { PiRpcAgentClient } from "../pi/agent.js";
9+
import { OMP_FAMILY } from "../pi/family-config.js";
10+
import type { PiRuntime } from "../pi/runtime.js";
11+
12+
import { listOmpPersistedAgents } from "./session-descriptor.js";
13+
14+
/**
15+
* OMP (Oh-My-Pi, https://github.com/oh-my-pi/pi-coding-agent) is a downstream
16+
* fork of Pi that ships its own binary (`omp`), home directory (`~/.omp`), and
17+
* plugin ecosystem. It preserves Pi's `--mode rpc` wire protocol and JSONL
18+
* session schema verbatim, so the entire Pi adapter is reused — only family
19+
* defaults differ.
20+
*
21+
* Unlike Pi (which uses Paseo-injected `paseo_capture_entries` extensions to
22+
* track session identity at runtime), OMP sessions started outside Paseo —
23+
* e.g. via `omp` invoked directly in the terminal — are discovered by reading
24+
* the JSONL session files on disk. The `omp/session-descriptor.ts` module
25+
* mirrors the historical Pi descriptor (removed upstream in #1154) but scoped
26+
* to OMP paths.
27+
*/
28+
export interface OmpRpcAgentClientOptions {
29+
logger: Logger;
30+
runtimeSettings?: ProviderRuntimeSettings;
31+
runtime?: PiRuntime;
32+
}
33+
34+
export class OmpRpcAgentClient extends PiRpcAgentClient {
35+
constructor(options: OmpRpcAgentClientOptions) {
36+
super({ ...options, family: OMP_FAMILY });
37+
}
38+
39+
override async listPersistedAgents(
40+
options?: ListPersistedAgentsOptions,
41+
): Promise<PersistedAgentDescriptor[]> {
42+
return await listOmpPersistedAgents({
43+
...options,
44+
runtimeSettings: this.runtimeSettings,
45+
});
46+
}
47+
}

0 commit comments

Comments
 (0)