Skip to content

Commit 163818b

Browse files
authored
Merge pull request #26 from odefun/ode_1770346674.753639
refactor runtime into modular services + split Slack routing
2 parents 41a2d7d + 9813c74 commit 163818b

32 files changed

+2075
-1027
lines changed

.github/workflows/ci.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches-ignore:
7+
- main
8+
9+
jobs:
10+
fast-checks:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout
15+
uses: actions/checkout@v4
16+
17+
- name: Setup Bun
18+
uses: oven-sh/setup-bun@v2
19+
20+
- name: Install dependencies
21+
run: bun install
22+
23+
- name: Install web-ui deps
24+
run: bun install --cwd packages/web-ui
25+
26+
- name: Typecheck
27+
run: bun run typecheck
28+
29+
- name: Core tests
30+
run: bun test packages/core/test
31+
32+
- name: Agent fast tests
33+
run: bun test packages/agents/test/cli-command.test.ts packages/agents/test/agent-registry.test.ts

.github/workflows/live-smoke.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Live Smoke Tests
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
run_live:
7+
description: Run live smoke tests
8+
required: true
9+
default: false
10+
type: boolean
11+
schedule:
12+
- cron: "0 4 * * *"
13+
14+
jobs:
15+
live-smoke:
16+
if: ${{ (github.event_name == 'workflow_dispatch' && inputs.run_live) || (github.event_name == 'schedule' && vars.LIVE_SMOKE_ENABLED == 'true') }}
17+
runs-on: ubuntu-latest
18+
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
23+
- name: Setup Bun
24+
uses: oven-sh/setup-bun@v2
25+
26+
- name: Install dependencies
27+
run: bun install
28+
29+
- name: Run live smoke tests
30+
run: bun test packages/agents/test/*.live.test.ts

.github/workflows/release.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ jobs:
3535
- name: Install dependencies
3636
run: bun install
3737

38+
- name: Run core tests
39+
run: bun test packages/core/test
40+
3841
- name: Build web UI
3942
run: bun run build:web
4043

packages/agents/adapter.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { AgentAdapter, NormalizedQuestion } from "@/core/types";
22
import type { QuestionInfo } from "@opencode-ai/sdk/v2";
33
import {
4+
selectedAgent,
45
getOrCreateSession,
56
sendMessage,
67
abortSession,
@@ -19,6 +20,9 @@ export function createAgentAdapter(): AgentAdapter {
1920
ensureSession,
2021
subscribeToSession,
2122
async replyToQuestion({ requestId, sessionId, directory, answers }) {
23+
if (selectedAgent !== "opencode") {
24+
throw new Error(`Question replies are not supported for agent: ${selectedAgent}`);
25+
}
2226
const client = await getSessionClient(sessionId);
2327
const response = await client.question.reply({
2428
requestID: requestId,
@@ -30,6 +34,7 @@ export function createAgentAdapter(): AgentAdapter {
3034
}
3135
},
3236
normalizeQuestions(questions: unknown): NormalizedQuestion[] {
37+
if (selectedAgent !== "opencode") return [];
3338
if (!Array.isArray(questions) || questions.length === 0) return [];
3439
return (questions as QuestionInfo[])
3540
.map((question) => {

packages/agents/index.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import * as claude from "./claude";
2-
import * as opencode from "./opencode";
1+
import { getSelectedAgentProvider } from "./registry";
32

43
export type {
54
OpenCodeMessage,
@@ -9,10 +8,10 @@ export type {
98
OpenCodeSessionInfo,
109
} from "./types";
1110

12-
const agent = opencode;
11+
const agent = getSelectedAgentProvider();
1312

14-
export const selectedAgent = "opencode";
15-
export const supportsEventStream = true;
13+
export const selectedAgent = agent.id;
14+
export const supportsEventStream = agent.supportsEventStream;
1615

1716
export const startServer = agent.startServer;
1817
export const stopServer = agent.stopServer;

packages/agents/registry.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import * as claude from "./claude";
2+
import * as opencode from "./opencode";
3+
import type {
4+
OpenCodeMessage,
5+
OpenCodeMessageContext,
6+
OpenCodeOptions,
7+
OpenCodeSessionInfo,
8+
} from "./types";
9+
10+
export type AgentProviderId = "opencode" | "claude";
11+
12+
export type AgentProvider = {
13+
id: AgentProviderId;
14+
supportsEventStream: boolean;
15+
startServer: () => Promise<void>;
16+
stopServer: () => void | Promise<void>;
17+
createSession: (workingPath: string, env?: Record<string, string>) => Promise<string>;
18+
getOrCreateSession: (
19+
channelId: string,
20+
threadId: string,
21+
workingPath: string,
22+
env?: Record<string, string>
23+
) => Promise<OpenCodeSessionInfo>;
24+
sendMessage: (
25+
channelId: string,
26+
sessionId: string,
27+
message: string,
28+
workingPath: string,
29+
options?: OpenCodeOptions,
30+
context?: OpenCodeMessageContext
31+
) => Promise<OpenCodeMessage[]>;
32+
abortSession: (sessionId: string, directory?: string) => Promise<void>;
33+
cancelActiveRequest: (channelId: string, sessionId: string, directory?: string) => Promise<boolean>;
34+
ensureSession: (sessionId: string) => Promise<void>;
35+
subscribeToSession: (sessionId: string, handler: (event: unknown) => void) => () => void;
36+
};
37+
38+
const providers: Record<AgentProviderId, AgentProvider> = {
39+
opencode: {
40+
id: "opencode",
41+
supportsEventStream: true,
42+
startServer: opencode.startServer,
43+
stopServer: opencode.stopServer,
44+
createSession: opencode.createSession,
45+
getOrCreateSession: opencode.getOrCreateSession,
46+
sendMessage: opencode.sendMessage,
47+
abortSession: opencode.abortSession,
48+
cancelActiveRequest: opencode.cancelActiveRequest,
49+
ensureSession: opencode.ensureSession,
50+
subscribeToSession: opencode.subscribeToSession,
51+
},
52+
claude: {
53+
id: "claude",
54+
supportsEventStream: false,
55+
startServer: claude.startServer,
56+
stopServer: claude.stopServer,
57+
createSession: claude.createSession,
58+
getOrCreateSession: claude.getOrCreateSession,
59+
sendMessage: claude.sendMessage,
60+
abortSession: claude.abortSession,
61+
cancelActiveRequest: claude.cancelActiveRequest,
62+
ensureSession: claude.ensureSession,
63+
subscribeToSession: claude.subscribeToSession,
64+
},
65+
};
66+
67+
export function getSelectedAgentProviderId(): AgentProviderId {
68+
const raw = process.env.ODE_AGENT_PROVIDER?.trim().toLowerCase();
69+
if (raw === "claude") return "claude";
70+
return "opencode";
71+
}
72+
73+
export function getSelectedAgentProvider(): AgentProvider {
74+
return providers[getSelectedAgentProviderId()];
75+
}
76+
77+
export function getAgentProvider(providerId: AgentProviderId): AgentProvider {
78+
return providers[providerId];
79+
}

packages/agents/shared.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export function buildSlackSystemPrompt(slack?: SlackContext): string {
1111
"- Be concise and conversational - this is chat, not documentation",
1212
"- Use short paragraphs, avoid walls of text",
1313
"- Get straight to the point",
14+
"- Do not truncate final answers for brevity; include complete results when details matter",
1415
"",
1516
"PROGRESS CHECKLIST:",
1617
"- Share a short checklist of what you're doing",
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { afterEach, describe, expect, it } from "bun:test";
2+
import { getAgentProvider, getSelectedAgentProviderId } from "../registry";
3+
4+
describe("agent registry", () => {
5+
const original = process.env.ODE_AGENT_PROVIDER;
6+
7+
afterEach(() => {
8+
if (original === undefined) {
9+
delete process.env.ODE_AGENT_PROVIDER;
10+
} else {
11+
process.env.ODE_AGENT_PROVIDER = original;
12+
}
13+
});
14+
15+
it("defaults to opencode", () => {
16+
delete process.env.ODE_AGENT_PROVIDER;
17+
expect(getSelectedAgentProviderId()).toBe("opencode");
18+
});
19+
20+
it("selects claude from env", () => {
21+
process.env.ODE_AGENT_PROVIDER = "claude";
22+
expect(getSelectedAgentProviderId()).toBe("claude");
23+
});
24+
25+
it("returns provider metadata", () => {
26+
const opencode = getAgentProvider("opencode");
27+
const claude = getAgentProvider("claude");
28+
expect(opencode.supportsEventStream).toBe(true);
29+
expect(claude.supportsEventStream).toBe(false);
30+
});
31+
});
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)