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.
- package/adapters/claude-code/package.json +2 -8
- package/adapters/claude-code/src/agent-runtime.ts +182 -0
- package/adapters/claude-code/src/index.ts +2 -13
- package/adapters/claude-code/tests/agent-runtime.test.ts +201 -0
- package/adapters/codex/package.json +15 -0
- package/adapters/codex/src/agent-runtime.ts +197 -0
- package/adapters/codex/src/index.ts +2 -0
- package/adapters/codex/tests/agent-runtime.test.ts +211 -0
- package/adapters/codex/tsconfig.json +20 -0
- package/adapters/gemini/package.json +2 -8
- package/adapters/gemini/src/agent-runtime.ts +193 -0
- package/adapters/gemini/src/index.ts +2 -0
- package/adapters/gemini/tests/agent-runtime.test.ts +148 -0
- package/adapters/pi/package.json +1 -0
- package/adapters/pi/src/agent-runtime.ts +82 -344
- package/adapters/pi/src/event-bus.ts +16 -99
- package/adapters/pi/src/index.ts +5 -20
- package/adapters/shared/package.json +19 -0
- package/adapters/shared/src/base-agent-runtime.ts +331 -0
- package/adapters/shared/src/base-event-bus.ts +133 -0
- package/adapters/{pi/src/workflow.ts → shared/src/base-workflow.ts} +50 -88
- package/adapters/shared/src/compose.ts +76 -0
- package/adapters/shared/src/index.ts +11 -0
- package/adapters/shared/src/terminal-ui.ts +140 -0
- package/adapters/shared/src/types.ts +43 -0
- package/adapters/shared/tests/base-agent-runtime.test.ts +182 -0
- package/adapters/shared/tests/base-event-bus.test.ts +70 -0
- package/adapters/shared/tests/base-workflow.test.ts +84 -0
- package/adapters/shared/tests/compose.test.ts +107 -0
- package/adapters/shared/tests/terminal-ui.test.ts +63 -0
- package/adapters/shared/tsconfig.json +18 -0
- package/core/schema/adapter.schema.json +3 -2
- package/package.json +2 -2
- package/adapters/claude-code/src/generate.ts +0 -246
- package/adapters/claude-code/src/templates.ts +0 -230
- package/adapters/gemini/src/generate.ts +0 -223
- 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.
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
+
}
|