-
Notifications
You must be signed in to change notification settings - Fork 330
feat(agents): add Oh My Pi (omp) agent #1132
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
Changes from all commits
19a6de1
99ad8d1
6907aee
9cc36d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
| import { OmpOutputParser } from '../../../main/parsers/omp-output-parser'; | ||
| import type { ParsedEvent } from '../../../main/parsers/agent-output-parser'; | ||
|
|
||
| /** | ||
| * Captured `omp -p --mode json "Reply with exactly the two characters: ok"` output. | ||
| * Source: local://omp-sample.jsonl (real run on this machine). | ||
| */ | ||
| const OMP_SAMPLE = `{"type":"session","version":3,"id":"019f053e-8426-7000-8d2a-b62b4fb55545","timestamp":"2026-06-26T18:43:30.984Z","cwd":"C:\\\\Users\\\\sydor\\\\AppData\\\\Local\\\\Temp\\\\omp-probe"} | ||
| {"type":"agent_start"} | ||
| {"type":"turn_start"} | ||
| {"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"Reply with exactly the two characters: ok"}],"attribution":"user","timestamp":1782499411936}} | ||
| {"type":"message_end","message":{"role":"user","content":[{"type":"text","text":"Reply with exactly the two characters: ok"}],"attribution":"user","timestamp":1782499411936}} | ||
| {"type":"message_start","message":{"role":"assistant","content":[{"type":"text","text":"ok"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-opus-4-8","usage":{"input":2,"output":4,"cacheRead":0,"cacheWrite":14443,"totalTokens":14449,"cost":{"input":0.00001,"output":0.0001,"cacheRead":0,"cacheWrite":0.09026875000000001,"total":0.09037875000000001},"cttl":{"ephemeral1h":14443}},"stopReason":"stop","timestamp":1782499414886,"responseId":"msg_01CoMSHtJYk8HJTJPP1KC9Ua","duration":1930,"ttft":1929}} | ||
| {"type":"message_update","assistantMessageEvent":{"type":"text_start","contentIndex":0,"partial":{"role":"assistant","content":[{"type":"text","text":"ok"}]}},"message":{"role":"assistant","content":[{"type":"text","text":"ok"}]}} | ||
| {"type":"message_update","assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"ok","partial":{"role":"assistant","content":[{"type":"text","text":"ok"}]}},"message":{"role":"assistant","content":[{"type":"text","text":"ok"}]}} | ||
| {"type":"message_update","assistantMessageEvent":{"type":"text_end","contentIndex":0,"content":"ok","partial":{"role":"assistant","content":[{"type":"text","text":"ok"}]}},"message":{"role":"assistant","content":[{"type":"text","text":"ok"}]}} | ||
| {"type":"message_end","message":{"role":"assistant","content":[{"type":"text","text":"ok"}],"api":"anthropic-messages","provider":"anthropic","model":"claude-opus-4-8","usage":{"input":2,"output":4,"cacheRead":0,"cacheWrite":14443,"totalTokens":14449,"cost":{"input":0.00001,"output":0.0001,"cacheRead":0,"cacheWrite":0.09026875000000001,"total":0.09037875000000001}},"stopReason":"stop","timestamp":1782499414886,"responseId":"msg_01CoMSHtJYk8HJTJPP1KC9Ua","duration":1930,"ttft":1929}} | ||
| {"type":"turn_end","message":{"role":"assistant","content":[{"type":"text","text":"ok"}],"usage":{"input":2,"output":4,"totalTokens":14449,"cost":{"total":0.09037875000000001}}},"toolResults":[]} | ||
| {"type":"agent_end","messages":[{"role":"user","content":[{"type":"text","text":"Reply with exactly the two characters: ok"}],"attribution":"user","timestamp":1782499411936},{"role":"assistant","content":[{"type":"text","text":"ok"}],"model":"claude-opus-4-8","usage":{"input":2,"output":4,"totalTokens":14449,"cost":{"total":0.09037875000000001}},"stopReason":"stop"}]}`; | ||
|
|
||
| describe('OmpOutputParser', () => { | ||
| const parser = new OmpOutputParser(); | ||
|
|
||
| const parseSample = (): ParsedEvent[] => | ||
| OMP_SAMPLE.split('\n') | ||
| .map((line) => parser.parseJsonLine(line)) | ||
| .filter((event): event is ParsedEvent => event !== null); | ||
|
|
||
| it('identifies as the omp agent', () => { | ||
| expect(parser.agentId).toBe('omp'); | ||
| }); | ||
|
|
||
| it('extracts the resumable session id from the session line', () => { | ||
| const events = parseSample(); | ||
| const init = events.find((event) => event.type === 'init'); | ||
|
|
||
| expect(init).toBeDefined(); | ||
| expect(parser.extractSessionId(init!)).toBe('019f053e-8426-7000-8d2a-b62b4fb55545'); | ||
| }); | ||
|
|
||
| it('assembles streamed assistant text from text_delta chunks', () => { | ||
| const events = parseSample(); | ||
| const streamedText = events | ||
| .filter((event) => event.type === 'text' && event.isPartial && !event.isReasoning) | ||
| .map((event) => event.text || '') | ||
| .join(''); | ||
|
|
||
| expect(streamedText).toBe('ok'); | ||
| }); | ||
|
|
||
| it('extracts usage with token totals and cost from the assistant message_end', () => { | ||
| const events = parseSample(); | ||
| const usageEvent = events.find((event) => event.type === 'usage'); | ||
|
|
||
| expect(usageEvent).toBeDefined(); | ||
| const usage = parser.extractUsage(usageEvent!); | ||
| expect(usage).not.toBeNull(); | ||
| expect(usage!.inputTokens).toBe(2); | ||
| expect(usage!.outputTokens).toBe(4); | ||
| expect(usage!.cacheReadTokens).toBe(0); | ||
| expect(usage!.cacheCreationTokens).toBe(14443); | ||
|
|
||
| // omp's totalTokens (14449) is the sum of input + output + cache read + cache write. | ||
| const totalTokens = | ||
| usage!.inputTokens + | ||
| usage!.outputTokens + | ||
| (usage!.cacheReadTokens || 0) + | ||
| (usage!.cacheCreationTokens || 0); | ||
| expect(totalTokens).toBe(14449); | ||
|
|
||
| expect(usage!.costUsd).toBeCloseTo(0.0904, 4); | ||
| }); | ||
|
|
||
| it('treats agent_end as the authoritative final result', () => { | ||
| const events = parseSample(); | ||
| const finalEvent = events.at(-1); | ||
|
|
||
| expect(finalEvent).toBeDefined(); | ||
| expect(parser.isResultMessage(finalEvent!)).toBe(true); | ||
| expect(finalEvent).toMatchObject({ type: 'result', text: 'ok' }); | ||
| }); | ||
|
|
||
| it('does not emit an empty result when agent_end has an empty messages array', () => { | ||
| const event = parser.parseJsonObject({ type: 'agent_end', messages: [] }); | ||
|
|
||
| expect(event).not.toBeNull(); | ||
| expect(event!.type).toBe('system'); | ||
| expect(parser.isResultMessage(event!)).toBe(false); | ||
| }); | ||
|
|
||
| it('does not emit an empty result when agent_end omits the messages array', () => { | ||
| const event = parser.parseJsonObject({ type: 'agent_end' }); | ||
|
|
||
| expect(event).not.toBeNull(); | ||
| expect(event!.type).toBe('system'); | ||
| expect(parser.isResultMessage(event!)).toBe(false); | ||
| }); | ||
|
|
||
| it('does not emit a result when agent_end carries only a user message', () => { | ||
| const event = parser.parseJsonObject({ | ||
| type: 'agent_end', | ||
| messages: [{ role: 'user', content: [{ type: 'text', text: 'hi' }] }], | ||
| }); | ||
|
|
||
| expect(event).not.toBeNull(); | ||
| expect(event!.type).toBe('system'); | ||
| expect(parser.isResultMessage(event!)).toBe(false); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -265,6 +265,44 @@ export const AGENT_CAPABILITIES: Record<string, AgentCapabilities> = { | |
| supportsProjectMemory: false, | ||
| }, | ||
|
|
||
| /** | ||
| * Oh My Pi - JSON event-stream batch integration via `omp -p --mode json`. | ||
| * Oh My Pi emits a session id, streaming message deltas, tool lifecycle | ||
| * events, and per-message usage/cost. It resumes sessions through `--resume` | ||
| * and selects models with a fuzzy `--model` matcher. | ||
| */ | ||
| omp: { | ||
| supportsResume: true, | ||
| supportsReadOnlyMode: false, | ||
| supportsJsonOutput: true, | ||
| supportsSessionId: true, | ||
| supportsImageInput: true, | ||
| supportsImageInputOnResume: true, | ||
| supportsSlashCommands: false, | ||
| supportsSessionStorage: false, | ||
| supportsCostTracking: true, | ||
| supportsUsageStats: true, | ||
| supportsBatchMode: true, | ||
| requiresPromptToStart: true, | ||
| supportsStreaming: true, | ||
| supportsResultMessages: true, | ||
| supportsModelSelection: true, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I checked Useful? React with 👍 / 👎. |
||
| supportsStreamJsonInput: false, | ||
| supportsThinkingDisplay: true, | ||
| supportsContextMerge: true, | ||
| supportsContextExport: false, | ||
| supportsWizard: false, | ||
| supportsGroupChatModeration: false, | ||
| usesJsonLineOutput: true, | ||
| usesCombinedContextWindow: false, | ||
| // omp exposes only the inline `--append-system-prompt` flag, not the | ||
| // `--append-system-prompt-file` variant that Maestro's Windows-local | ||
| // delivery path emits when this is true. Enabling it would break omp on | ||
| // Windows (unknown flag), and the flag name is not overridable per agent. | ||
| supportsAppendSystemPrompt: false, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
OMP exposes Useful? React with 👍 / 👎. |
||
| supportsProjectMemory: false, | ||
| }, | ||
|
|
||
| /** | ||
| * OpenCode - Open source coding assistant | ||
| * https://github.com/opencode-ai/opencode | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With
supportsReadOnlyModefalse, Maestro hides the Read-Only toggle for OMP even though OMP accepts a--toolsallowlist like the Pi integration uses. Since OMP's default approval mode allows write and exec tools, users running OMP on untrusted prompts have no way to start a read-only turn from the UI; add read-only args such as a read/search tool allowlist and mark this capability true.Useful? React with 👍 / 👎.