aos-harness 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/adapters/claude-code/package.json +2 -8
  2. package/adapters/claude-code/src/agent-runtime.ts +182 -0
  3. package/adapters/claude-code/src/index.ts +2 -13
  4. package/adapters/claude-code/tests/agent-runtime.test.ts +201 -0
  5. package/adapters/codex/package.json +15 -0
  6. package/adapters/codex/src/agent-runtime.ts +197 -0
  7. package/adapters/codex/src/index.ts +2 -0
  8. package/adapters/codex/tests/agent-runtime.test.ts +211 -0
  9. package/adapters/codex/tsconfig.json +20 -0
  10. package/adapters/gemini/package.json +2 -8
  11. package/adapters/gemini/src/agent-runtime.ts +193 -0
  12. package/adapters/gemini/src/index.ts +2 -0
  13. package/adapters/gemini/tests/agent-runtime.test.ts +148 -0
  14. package/adapters/pi/package.json +1 -0
  15. package/adapters/pi/src/agent-runtime.ts +82 -344
  16. package/adapters/pi/src/event-bus.ts +16 -99
  17. package/adapters/pi/src/index.ts +5 -20
  18. package/adapters/shared/package.json +19 -0
  19. package/adapters/shared/src/base-agent-runtime.ts +331 -0
  20. package/adapters/shared/src/base-event-bus.ts +133 -0
  21. package/adapters/{pi/src/workflow.ts → shared/src/base-workflow.ts} +50 -88
  22. package/adapters/shared/src/compose.ts +76 -0
  23. package/adapters/shared/src/index.ts +11 -0
  24. package/adapters/shared/src/terminal-ui.ts +140 -0
  25. package/adapters/shared/src/types.ts +43 -0
  26. package/adapters/shared/tests/base-agent-runtime.test.ts +182 -0
  27. package/adapters/shared/tests/base-event-bus.test.ts +70 -0
  28. package/adapters/shared/tests/base-workflow.test.ts +84 -0
  29. package/adapters/shared/tests/compose.test.ts +107 -0
  30. package/adapters/shared/tests/terminal-ui.test.ts +63 -0
  31. package/adapters/shared/tsconfig.json +18 -0
  32. package/core/schema/adapter.schema.json +3 -2
  33. package/package.json +2 -2
  34. package/adapters/claude-code/src/generate.ts +0 -246
  35. package/adapters/claude-code/src/templates.ts +0 -230
  36. package/adapters/gemini/src/generate.ts +0 -223
  37. package/adapters/gemini/src/templates.ts +0 -193
@@ -1,20 +1,14 @@
1
1
  {
2
2
  "name": "@aos-harness/claude-code-adapter",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
- "main": "src/generate.ts",
6
- "scripts": {
7
- "generate": "bun run src/generate.ts",
8
- "typecheck": "bun x tsc --noEmit"
9
- },
10
5
  "exports": { ".": "./src/index.ts" },
11
6
  "files": ["src/"],
12
7
  "dependencies": {
13
8
  "@aos-harness/runtime": "workspace:*",
14
- "js-yaml": "^4.1.0"
9
+ "@aos-harness/adapter-shared": "workspace:*"
15
10
  },
16
11
  "devDependencies": {
17
- "@types/js-yaml": "^4.0.9",
18
12
  "@types/bun": "latest",
19
13
  "typescript": "^5.8.0"
20
14
  }
@@ -0,0 +1,182 @@
1
+ // ── ClaudeCodeAgentRuntime (L1) ───────────────────────────────────
2
+ // Extends BaseAgentRuntime with Claude Code CLI integration.
3
+
4
+ import { execSync } from "node:child_process";
5
+ import type {
6
+ AuthMode,
7
+ ModelCost,
8
+ ModelTier,
9
+ MessageOpts,
10
+ } from "@aos-harness/runtime/types";
11
+ import {
12
+ BaseAgentRuntime,
13
+ type HandleState,
14
+ type ParsedEvent,
15
+ type StdoutFormat,
16
+ type ModelInfo,
17
+ } from "@aos-harness/adapter-shared";
18
+ import type { BaseEventBus } from "@aos-harness/adapter-shared";
19
+
20
+ // ── ClaudeCodeAgentRuntime ────────────────────────────────────────
21
+
22
+ export class ClaudeCodeAgentRuntime extends BaseAgentRuntime {
23
+ constructor(eventBus: BaseEventBus, modelOverrides?: Partial<Record<ModelTier, string>>) {
24
+ super(eventBus, modelOverrides);
25
+ }
26
+
27
+ cliBinary(): string {
28
+ return "claude";
29
+ }
30
+
31
+ stdoutFormat(): StdoutFormat {
32
+ return "ndjson";
33
+ }
34
+
35
+ buildArgs(state: HandleState, message: string, isFirstCall: boolean, opts?: MessageOpts): string[] {
36
+ const args: string[] = ["--print", "--output-format", "json", "--verbose"];
37
+
38
+ if (isFirstCall) {
39
+ // System prompt
40
+ const systemPrompt = state.config.systemPrompt || "";
41
+ if (systemPrompt) {
42
+ args.push("--system-prompt", systemPrompt);
43
+ }
44
+
45
+ // Model
46
+ args.push("--model", this.resolveModelId(state.modelConfig.tier));
47
+
48
+ // Context files
49
+ const contextFiles = opts?.contextFiles?.length
50
+ ? opts.contextFiles
51
+ : state.contextFiles;
52
+ for (const file of contextFiles) {
53
+ args.push("--add-file", file);
54
+ }
55
+ } else {
56
+ // Resume session
57
+ args.push("--resume", state.sessionFile);
58
+ }
59
+
60
+ // Message is always the final argument
61
+ args.push(message);
62
+ return args;
63
+ }
64
+
65
+ parseEventLine(line: string): ParsedEvent | null {
66
+ let event: any;
67
+ try {
68
+ event = JSON.parse(line);
69
+ } catch {
70
+ return null;
71
+ }
72
+
73
+ // Final result with usage stats
74
+ if (event.type === "result") {
75
+ const usage = event.usage ?? {};
76
+ return {
77
+ type: "message_end",
78
+ text: event.result ?? "",
79
+ tokensIn: usage.input_tokens ?? 0,
80
+ tokensOut: usage.output_tokens ?? 0,
81
+ cost: event.cost_usd ?? 0,
82
+ contextTokens: (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0),
83
+ model: event.model ?? "",
84
+ };
85
+ }
86
+
87
+ // Streaming text delta
88
+ if (event.type === "content_block_delta" && event.delta?.text !== undefined) {
89
+ return { type: "text_delta", text: event.delta.text };
90
+ }
91
+
92
+ // Tool call
93
+ if (event.type === "tool_use") {
94
+ return { type: "tool_call", name: event.name ?? "unknown", input: event.input ?? {} };
95
+ }
96
+
97
+ // Tool result — content or output field
98
+ if (event.type === "tool_result") {
99
+ const result = event.content ?? event.output ?? null;
100
+ return { type: "tool_result", name: event.name ?? "unknown", input: {}, result };
101
+ }
102
+
103
+ return { type: "ignored" };
104
+ }
105
+
106
+ buildSubprocessEnv(): Record<string, string> {
107
+ const env: Record<string, string> = {};
108
+ const allowlist = [
109
+ "PATH", "HOME", "USER", "SHELL", "TERM", "LANG",
110
+ "ANTHROPIC_API_KEY",
111
+ "AOS_MODEL_ECONOMY", "AOS_MODEL_STANDARD", "AOS_MODEL_PREMIUM",
112
+ ];
113
+ for (const key of allowlist) {
114
+ if (process.env[key] !== undefined) env[key] = process.env[key]!;
115
+ }
116
+ return env;
117
+ }
118
+
119
+ async discoverModels(): Promise<ModelInfo[]> {
120
+ try {
121
+ const output = execSync("claude model list --json", {
122
+ encoding: "utf-8",
123
+ timeout: 10_000,
124
+ env: this.buildSubprocessEnv(),
125
+ });
126
+ const parsed = JSON.parse(output);
127
+ if (Array.isArray(parsed)) {
128
+ return parsed.map((m: any) => ({
129
+ id: m.id ?? m.name,
130
+ name: m.name ?? m.id,
131
+ contextWindow: m.context_window ?? m.contextWindow ?? 200_000,
132
+ provider: "claude",
133
+ }));
134
+ }
135
+ } catch {
136
+ // Fall through to defaults
137
+ }
138
+ const defaults = this.defaultModelMap();
139
+ return Object.entries(defaults).map(([_tier, id]) => ({
140
+ id,
141
+ name: id,
142
+ contextWindow: 200_000,
143
+ provider: "claude",
144
+ }));
145
+ }
146
+
147
+ defaultModelMap(): Record<ModelTier, string> {
148
+ return {
149
+ economy: "claude-haiku-4-5",
150
+ standard: "claude-sonnet-4-6",
151
+ premium: "claude-opus-4-6",
152
+ };
153
+ }
154
+
155
+ getAuthMode(): AuthMode {
156
+ if (process.env.ANTHROPIC_API_KEY) {
157
+ return { type: "api_key", metered: true };
158
+ }
159
+ return { type: "subscription", metered: false };
160
+ }
161
+
162
+ getModelCost(tier: ModelTier): ModelCost {
163
+ const pricing: Record<ModelTier, ModelCost> = {
164
+ economy: {
165
+ inputPerMillionTokens: 0.80,
166
+ outputPerMillionTokens: 4.00,
167
+ currency: "USD",
168
+ },
169
+ standard: {
170
+ inputPerMillionTokens: 3.00,
171
+ outputPerMillionTokens: 15.00,
172
+ currency: "USD",
173
+ },
174
+ premium: {
175
+ inputPerMillionTokens: 15.00,
176
+ outputPerMillionTokens: 75.00,
177
+ currency: "USD",
178
+ },
179
+ };
180
+ return pricing[tier];
181
+ }
182
+ }
@@ -1,13 +1,2 @@
1
- /**
2
- * AOS Harness Claude Code Adapter
3
- *
4
- * Barrel file re-exporting the adapter's public API.
5
- */
6
-
7
- export { generateClaudeCodeArtifacts } from "./generate";
8
- export {
9
- generateAgentFile,
10
- generateCommandFile,
11
- generateClaudeMdFragment,
12
- mapTierToModel,
13
- } from "./templates";
1
+ export { ClaudeCodeAgentRuntime } from "./agent-runtime";
2
+ export { BaseEventBus, TerminalUI, BaseWorkflow, composeAdapter } from "@aos-harness/adapter-shared";
@@ -0,0 +1,201 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2
+ import { ClaudeCodeAgentRuntime } from "../src/agent-runtime";
3
+ import { BaseEventBus } from "@aos-harness/adapter-shared";
4
+
5
+ // Minimal stub for BaseEventBus
6
+ class StubEventBus extends BaseEventBus {}
7
+
8
+ function makeRuntime(env: Record<string, string> = {}): ClaudeCodeAgentRuntime {
9
+ const saved: Record<string, string | undefined> = {};
10
+ for (const [k, v] of Object.entries(env)) {
11
+ saved[k] = process.env[k];
12
+ process.env[k] = v;
13
+ }
14
+ const runtime = new ClaudeCodeAgentRuntime(new StubEventBus());
15
+ // restore after construction (actual calls may still read process.env at call time)
16
+ for (const [k] of Object.entries(env)) {
17
+ if (saved[k] === undefined) delete process.env[k];
18
+ else process.env[k] = saved[k];
19
+ }
20
+ return runtime;
21
+ }
22
+
23
+ describe("ClaudeCodeAgentRuntime", () => {
24
+ it("cliBinary returns 'claude'", () => {
25
+ const rt = new ClaudeCodeAgentRuntime(new StubEventBus());
26
+ expect(rt.cliBinary()).toBe("claude");
27
+ });
28
+
29
+ it("stdoutFormat returns 'ndjson'", () => {
30
+ const rt = new ClaudeCodeAgentRuntime(new StubEventBus());
31
+ expect(rt.stdoutFormat()).toBe("ndjson");
32
+ });
33
+
34
+ describe("buildArgs", () => {
35
+ const state = {
36
+ config: {
37
+ id: "test-agent",
38
+ systemPrompt: "You are a helpful assistant.",
39
+ model: { tier: "standard" as const, thinking: "on" as const },
40
+ tools: [],
41
+ skills: [],
42
+ },
43
+ sessionFile: "/tmp/test-session.jsonl",
44
+ contextFiles: ["/tmp/context.md"],
45
+ modelConfig: { tier: "standard" as const, thinking: "on" as const },
46
+ lastContextTokens: 0,
47
+ };
48
+
49
+ it("first call includes --print, --output-format, --system-prompt, --model, and message", () => {
50
+ const rt = new ClaudeCodeAgentRuntime(new StubEventBus());
51
+ const args = rt.buildArgs(state, "Hello world", true);
52
+
53
+ expect(args).toContain("--print");
54
+ expect(args).toContain("--output-format");
55
+ expect(args).toContain("json");
56
+ expect(args).toContain("--verbose");
57
+ expect(args).toContain("--system-prompt");
58
+ expect(args).toContain("You are a helpful assistant.");
59
+ expect(args).toContain("--model");
60
+ // Message should be the last argument
61
+ expect(args[args.length - 1]).toBe("Hello world");
62
+ });
63
+
64
+ it("first call includes --add-file for context files", () => {
65
+ const rt = new ClaudeCodeAgentRuntime(new StubEventBus());
66
+ const args = rt.buildArgs(state, "Hello", true);
67
+
68
+ expect(args).toContain("--add-file");
69
+ expect(args).toContain("/tmp/context.md");
70
+ });
71
+
72
+ it("subsequent call includes --resume and no --system-prompt", () => {
73
+ const rt = new ClaudeCodeAgentRuntime(new StubEventBus());
74
+ const args = rt.buildArgs(state, "Follow-up message", false);
75
+
76
+ expect(args).toContain("--print");
77
+ expect(args).toContain("--output-format");
78
+ expect(args).toContain("json");
79
+ expect(args).toContain("--verbose");
80
+ expect(args).toContain("--resume");
81
+ expect(args).toContain("/tmp/test-session.jsonl");
82
+ expect(args).not.toContain("--system-prompt");
83
+ expect(args[args.length - 1]).toBe("Follow-up message");
84
+ });
85
+ });
86
+
87
+ describe("parseEventLine", () => {
88
+ const rt = new ClaudeCodeAgentRuntime(new StubEventBus());
89
+
90
+ it("parses result type → message_end", () => {
91
+ const line = JSON.stringify({
92
+ type: "result",
93
+ result: "Hello from Claude",
94
+ usage: { input_tokens: 100, output_tokens: 50 },
95
+ cost_usd: 0.002,
96
+ model: "claude-sonnet-4-6",
97
+ });
98
+ const event = rt.parseEventLine(line);
99
+ expect(event).not.toBeNull();
100
+ expect(event!.type).toBe("message_end");
101
+ if (event!.type === "message_end") {
102
+ expect(event.text).toBe("Hello from Claude");
103
+ expect(event.tokensIn).toBe(100);
104
+ expect(event.tokensOut).toBe(50);
105
+ expect(event.cost).toBe(0.002);
106
+ expect(event.model).toBe("claude-sonnet-4-6");
107
+ }
108
+ });
109
+
110
+ it("parses content_block_delta → text_delta", () => {
111
+ const line = JSON.stringify({
112
+ type: "content_block_delta",
113
+ delta: { text: "streaming text" },
114
+ });
115
+ const event = rt.parseEventLine(line);
116
+ expect(event).not.toBeNull();
117
+ expect(event!.type).toBe("text_delta");
118
+ if (event!.type === "text_delta") {
119
+ expect(event.text).toBe("streaming text");
120
+ }
121
+ });
122
+
123
+ it("parses tool_use → tool_call", () => {
124
+ const line = JSON.stringify({
125
+ type: "tool_use",
126
+ name: "bash",
127
+ input: { command: "ls" },
128
+ });
129
+ const event = rt.parseEventLine(line);
130
+ expect(event).not.toBeNull();
131
+ expect(event!.type).toBe("tool_call");
132
+ if (event!.type === "tool_call") {
133
+ expect(event.name).toBe("bash");
134
+ expect(event.input).toEqual({ command: "ls" });
135
+ }
136
+ });
137
+
138
+ it("parses tool_result → tool_result", () => {
139
+ const line = JSON.stringify({
140
+ type: "tool_result",
141
+ name: "bash",
142
+ content: "file1.txt\nfile2.txt",
143
+ });
144
+ const event = rt.parseEventLine(line);
145
+ expect(event).not.toBeNull();
146
+ expect(event!.type).toBe("tool_result");
147
+ if (event!.type === "tool_result") {
148
+ expect(event.name).toBe("bash");
149
+ }
150
+ });
151
+
152
+ it("returns ignored for unknown event types", () => {
153
+ const line = JSON.stringify({ type: "unknown_event", data: "foo" });
154
+ const event = rt.parseEventLine(line);
155
+ expect(event).not.toBeNull();
156
+ expect(event!.type).toBe("ignored");
157
+ });
158
+
159
+ it("returns null for invalid JSON", () => {
160
+ const event = rt.parseEventLine("not valid json");
161
+ expect(event).toBeNull();
162
+ });
163
+ });
164
+
165
+ it("defaultModelMap returns correct models", () => {
166
+ const rt = new ClaudeCodeAgentRuntime(new StubEventBus());
167
+ const map = rt.defaultModelMap();
168
+ expect(map.economy).toBe("claude-haiku-4-5");
169
+ expect(map.standard).toBe("claude-sonnet-4-6");
170
+ expect(map.premium).toBe("claude-opus-4-6");
171
+ });
172
+
173
+ describe("getAuthMode", () => {
174
+ let savedKey: string | undefined;
175
+
176
+ beforeEach(() => {
177
+ savedKey = process.env.ANTHROPIC_API_KEY;
178
+ });
179
+
180
+ afterEach(() => {
181
+ if (savedKey === undefined) delete process.env.ANTHROPIC_API_KEY;
182
+ else process.env.ANTHROPIC_API_KEY = savedKey;
183
+ });
184
+
185
+ it("returns api_key when ANTHROPIC_API_KEY is set", () => {
186
+ process.env.ANTHROPIC_API_KEY = "sk-test-key";
187
+ const rt = new ClaudeCodeAgentRuntime(new StubEventBus());
188
+ const auth = rt.getAuthMode();
189
+ expect(auth.type).toBe("api_key");
190
+ expect(auth.metered).toBe(true);
191
+ });
192
+
193
+ it("returns subscription when ANTHROPIC_API_KEY is not set", () => {
194
+ delete process.env.ANTHROPIC_API_KEY;
195
+ const rt = new ClaudeCodeAgentRuntime(new StubEventBus());
196
+ const auth = rt.getAuthMode();
197
+ expect(auth.type).toBe("subscription");
198
+ expect(auth.metered).toBe(false);
199
+ });
200
+ });
201
+ });
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@aos-harness/codex-adapter",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": { ".": "./src/index.ts" },
6
+ "files": ["src/"],
7
+ "dependencies": {
8
+ "@aos-harness/runtime": "workspace:*",
9
+ "@aos-harness/adapter-shared": "workspace:*"
10
+ },
11
+ "devDependencies": {
12
+ "@types/bun": "latest",
13
+ "typescript": "^5.8.0"
14
+ }
15
+ }
@@ -0,0 +1,197 @@
1
+ // ── CodexAgentRuntime (L1) ────────────────────────────────────────
2
+ // Extends BaseAgentRuntime with OpenAI Codex CLI integration.
3
+
4
+ import { execSync } from "node:child_process";
5
+ import type {
6
+ AuthMode,
7
+ ModelCost,
8
+ ModelTier,
9
+ MessageOpts,
10
+ } from "@aos-harness/runtime/types";
11
+ import {
12
+ BaseAgentRuntime,
13
+ type HandleState,
14
+ type ParsedEvent,
15
+ type StdoutFormat,
16
+ type ModelInfo,
17
+ } from "@aos-harness/adapter-shared";
18
+ import type { BaseEventBus } from "@aos-harness/adapter-shared";
19
+
20
+ // ── CodexAgentRuntime ─────────────────────────────────────────────
21
+
22
+ export class CodexAgentRuntime extends BaseAgentRuntime {
23
+ constructor(eventBus: BaseEventBus, modelOverrides?: Partial<Record<ModelTier, string>>) {
24
+ super(eventBus, modelOverrides);
25
+ }
26
+
27
+ cliBinary(): string {
28
+ return "codex";
29
+ }
30
+
31
+ stdoutFormat(): StdoutFormat {
32
+ return "ndjson";
33
+ }
34
+
35
+ buildArgs(state: HandleState, message: string, isFirstCall: boolean, opts?: MessageOpts): string[] {
36
+ const args: string[] = ["--full-auto", "--model", this.resolveModelId(state.modelConfig.tier)];
37
+
38
+ if (isFirstCall) {
39
+ // System prompt
40
+ const systemPrompt = state.config.systemPrompt || "";
41
+ if (systemPrompt) {
42
+ args.push("--system-prompt", systemPrompt);
43
+ }
44
+
45
+ // Context files
46
+ const contextFiles = opts?.contextFiles?.length
47
+ ? opts.contextFiles
48
+ : state.contextFiles;
49
+ for (const file of contextFiles) {
50
+ args.push("--file", file);
51
+ }
52
+ } else {
53
+ // Resume session
54
+ args.push("--session", state.sessionFile);
55
+ }
56
+
57
+ // Message is always the final argument
58
+ args.push(message);
59
+ return args;
60
+ }
61
+
62
+ parseEventLine(line: string): ParsedEvent | null {
63
+ let event: any;
64
+ try {
65
+ event = JSON.parse(line);
66
+ } catch {
67
+ return null;
68
+ }
69
+
70
+ // Codex result format with usage stats
71
+ if (event.type === "result") {
72
+ const usage = event.usage ?? {};
73
+ return {
74
+ type: "message_end",
75
+ text: event.result ?? "",
76
+ tokensIn: usage.input_tokens ?? 0,
77
+ tokensOut: usage.output_tokens ?? 0,
78
+ cost: event.cost_usd ?? 0,
79
+ contextTokens: (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0),
80
+ model: event.model ?? "",
81
+ };
82
+ }
83
+
84
+ // Streaming text delta (Anthropic-style)
85
+ if (event.type === "content_block_delta" && event.delta?.text !== undefined) {
86
+ return { type: "text_delta", text: event.delta.text };
87
+ }
88
+
89
+ // OpenAI streaming choices format
90
+ if (Array.isArray(event.choices)) {
91
+ const choice = event.choices[0];
92
+
93
+ // Streaming delta with content
94
+ if (choice?.delta?.content !== undefined && choice.delta.content !== null) {
95
+ return { type: "text_delta", text: choice.delta.content };
96
+ }
97
+
98
+ // Non-streaming message with full content and usage
99
+ if (choice?.message?.content !== undefined) {
100
+ const usage = event.usage ?? {};
101
+ return {
102
+ type: "message_end",
103
+ text: choice.message.content ?? "",
104
+ tokensIn: usage.prompt_tokens ?? 0,
105
+ tokensOut: usage.completion_tokens ?? 0,
106
+ cost: 0,
107
+ contextTokens: (usage.prompt_tokens ?? 0) + (usage.completion_tokens ?? 0),
108
+ model: event.model ?? "",
109
+ };
110
+ }
111
+ }
112
+
113
+ // Tool call / function call
114
+ if (event.type === "tool_call" || event.type === "function_call") {
115
+ return { type: "tool_call", name: event.name ?? "unknown", input: event.input ?? event.args ?? {} };
116
+ }
117
+
118
+ return { type: "ignored" };
119
+ }
120
+
121
+ buildSubprocessEnv(): Record<string, string> {
122
+ const env: Record<string, string> = {};
123
+ const allowlist = [
124
+ "PATH", "HOME", "USER", "SHELL", "TERM", "LANG",
125
+ "OPENAI_API_KEY",
126
+ "AOS_MODEL_ECONOMY", "AOS_MODEL_STANDARD", "AOS_MODEL_PREMIUM",
127
+ ];
128
+ for (const key of allowlist) {
129
+ if (process.env[key] !== undefined) env[key] = process.env[key]!;
130
+ }
131
+ return env;
132
+ }
133
+
134
+ async discoverModels(): Promise<ModelInfo[]> {
135
+ try {
136
+ const output = execSync("codex model list --json", {
137
+ encoding: "utf-8",
138
+ timeout: 10_000,
139
+ env: this.buildSubprocessEnv(),
140
+ });
141
+ const parsed = JSON.parse(output);
142
+ if (Array.isArray(parsed)) {
143
+ return parsed.map((m: any) => ({
144
+ id: m.id ?? m.name,
145
+ name: m.name ?? m.id,
146
+ contextWindow: m.context_window ?? m.contextWindow ?? 200_000,
147
+ provider: "codex",
148
+ }));
149
+ }
150
+ } catch {
151
+ // Fall through to defaults
152
+ }
153
+ const defaults = this.defaultModelMap();
154
+ return Object.entries(defaults).map(([_tier, id]) => ({
155
+ id,
156
+ name: id,
157
+ contextWindow: 200_000,
158
+ provider: "codex",
159
+ }));
160
+ }
161
+
162
+ defaultModelMap(): Record<ModelTier, string> {
163
+ return {
164
+ economy: "o4-mini",
165
+ standard: "o3",
166
+ premium: "o3",
167
+ };
168
+ }
169
+
170
+ getAuthMode(): AuthMode {
171
+ if (process.env.OPENAI_API_KEY) {
172
+ return { type: "api_key", metered: true };
173
+ }
174
+ return { type: "unknown", metered: false };
175
+ }
176
+
177
+ getModelCost(tier: ModelTier): ModelCost {
178
+ const pricing: Record<ModelTier, ModelCost> = {
179
+ economy: {
180
+ inputPerMillionTokens: 1.10,
181
+ outputPerMillionTokens: 4.40,
182
+ currency: "USD",
183
+ },
184
+ standard: {
185
+ inputPerMillionTokens: 10.00,
186
+ outputPerMillionTokens: 40.00,
187
+ currency: "USD",
188
+ },
189
+ premium: {
190
+ inputPerMillionTokens: 10.00,
191
+ outputPerMillionTokens: 40.00,
192
+ currency: "USD",
193
+ },
194
+ };
195
+ return pricing[tier];
196
+ }
197
+ }
@@ -0,0 +1,2 @@
1
+ export { CodexAgentRuntime } from "./agent-runtime";
2
+ export { BaseEventBus, TerminalUI, BaseWorkflow, composeAdapter } from "@aos-harness/adapter-shared";