@spacek33z/autoauto 0.0.1
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/README.md +197 -0
- package/package.json +51 -0
- package/src/App.tsx +224 -0
- package/src/cli.ts +772 -0
- package/src/components/AgentPanel.tsx +254 -0
- package/src/components/Chat.test.tsx +71 -0
- package/src/components/Chat.tsx +308 -0
- package/src/components/CycleField.tsx +23 -0
- package/src/components/ModelPicker.tsx +97 -0
- package/src/components/PostUpdatePrompt.tsx +46 -0
- package/src/components/ResultsTable.tsx +172 -0
- package/src/components/RunCompletePrompt.tsx +90 -0
- package/src/components/RunSettingsOverlay.tsx +49 -0
- package/src/components/RunsTable.tsx +219 -0
- package/src/components/StatsHeader.tsx +100 -0
- package/src/daemon.ts +264 -0
- package/src/index.tsx +8 -0
- package/src/lib/agent/agent-provider.test.ts +133 -0
- package/src/lib/agent/claude-provider.ts +277 -0
- package/src/lib/agent/codex-provider.ts +413 -0
- package/src/lib/agent/default-providers.ts +10 -0
- package/src/lib/agent/index.ts +32 -0
- package/src/lib/agent/mock-provider.ts +61 -0
- package/src/lib/agent/opencode-provider.ts +424 -0
- package/src/lib/agent/types.ts +73 -0
- package/src/lib/auth.ts +11 -0
- package/src/lib/config.ts +152 -0
- package/src/lib/daemon-callbacks.ts +59 -0
- package/src/lib/daemon-client.ts +16 -0
- package/src/lib/daemon-lifecycle.ts +368 -0
- package/src/lib/daemon-spawn.ts +122 -0
- package/src/lib/daemon-status.ts +189 -0
- package/src/lib/daemon-watcher.ts +192 -0
- package/src/lib/experiment-loop.ts +679 -0
- package/src/lib/experiment.ts +356 -0
- package/src/lib/finalize.test.ts +143 -0
- package/src/lib/finalize.ts +511 -0
- package/src/lib/format.test.ts +32 -0
- package/src/lib/format.ts +44 -0
- package/src/lib/git.ts +176 -0
- package/src/lib/ideas-backlog.test.ts +54 -0
- package/src/lib/ideas-backlog.ts +109 -0
- package/src/lib/measure.ts +472 -0
- package/src/lib/model-options.ts +24 -0
- package/src/lib/programs.ts +247 -0
- package/src/lib/push-stream.ts +48 -0
- package/src/lib/run-context.ts +112 -0
- package/src/lib/run-setup.ts +34 -0
- package/src/lib/run.ts +383 -0
- package/src/lib/syntax-theme.ts +39 -0
- package/src/lib/system-prompts/experiment.ts +77 -0
- package/src/lib/system-prompts/finalize.ts +90 -0
- package/src/lib/system-prompts/index.ts +7 -0
- package/src/lib/system-prompts/setup.ts +516 -0
- package/src/lib/system-prompts/update.ts +188 -0
- package/src/lib/tool-events.ts +99 -0
- package/src/lib/validate-measurement.ts +326 -0
- package/src/lib/worktree.ts +40 -0
- package/src/screens/AuthErrorScreen.tsx +31 -0
- package/src/screens/ExecutionScreen.tsx +851 -0
- package/src/screens/FirstSetupScreen.tsx +168 -0
- package/src/screens/HomeScreen.tsx +406 -0
- package/src/screens/PreRunScreen.tsx +206 -0
- package/src/screens/SettingsScreen.tsx +189 -0
- package/src/screens/SetupScreen.tsx +226 -0
- package/src/tui.tsx +17 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import {
|
|
2
|
+
query,
|
|
3
|
+
type AccountInfo,
|
|
4
|
+
type SDKMessage,
|
|
5
|
+
type SDKUserMessage,
|
|
6
|
+
type SDKAssistantMessage,
|
|
7
|
+
type SDKPartialAssistantMessage,
|
|
8
|
+
type SDKResultMessage,
|
|
9
|
+
type Query,
|
|
10
|
+
} from "@anthropic-ai/claude-agent-sdk"
|
|
11
|
+
import { createPushStream } from "../push-stream.ts"
|
|
12
|
+
import type {
|
|
13
|
+
AgentProvider,
|
|
14
|
+
AgentSession,
|
|
15
|
+
AgentSessionConfig,
|
|
16
|
+
AgentEvent,
|
|
17
|
+
AgentCost,
|
|
18
|
+
AuthResult,
|
|
19
|
+
AgentModelOption,
|
|
20
|
+
} from "./types.ts"
|
|
21
|
+
|
|
22
|
+
// --- SDK message helpers (formerly in sdk-helpers.ts) ---
|
|
23
|
+
|
|
24
|
+
function getAssistantText(message: SDKAssistantMessage): string {
|
|
25
|
+
return message.message.content
|
|
26
|
+
.flatMap((block) => (block.type === "text" ? [block.text] : []))
|
|
27
|
+
.join("")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getTextDelta(message: SDKPartialAssistantMessage): string | null {
|
|
31
|
+
const event = message.event
|
|
32
|
+
if (event.type !== "content_block_delta" || event.delta.type !== "text_delta") {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
return event.delta.text
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Tracks an in-progress tool_use block while input JSON is being streamed. */
|
|
39
|
+
interface PendingToolUse {
|
|
40
|
+
tool: string
|
|
41
|
+
inputJson: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Processes stream events for tool_use blocks. Returns the tool call only
|
|
46
|
+
* at content_block_stop, after all input_json_delta chunks have arrived,
|
|
47
|
+
* so formatToolEvent can produce descriptive messages (file paths, commands).
|
|
48
|
+
*/
|
|
49
|
+
function processToolUseEvent(
|
|
50
|
+
event: unknown,
|
|
51
|
+
pending: PendingToolUse | null,
|
|
52
|
+
): { pending: PendingToolUse | null; result: { tool: string; input?: Record<string, unknown> } | null } {
|
|
53
|
+
const ev = event as { type?: string; content_block?: { type?: string; name?: string }; delta?: { type?: string; partial_json?: string } }
|
|
54
|
+
const type = ev.type
|
|
55
|
+
|
|
56
|
+
if (type === "content_block_start") {
|
|
57
|
+
const block = ev.content_block
|
|
58
|
+
if (block?.type === "tool_use" && block.name) {
|
|
59
|
+
return { pending: { tool: block.name, inputJson: "" }, result: null }
|
|
60
|
+
}
|
|
61
|
+
return { pending, result: null }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (type === "content_block_delta" && pending) {
|
|
65
|
+
const delta = ev.delta
|
|
66
|
+
if (delta?.type === "input_json_delta" && typeof delta.partial_json === "string") {
|
|
67
|
+
return { pending: { ...pending, inputJson: pending.inputJson + delta.partial_json }, result: null }
|
|
68
|
+
}
|
|
69
|
+
return { pending, result: null }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (type === "content_block_stop" && pending) {
|
|
73
|
+
let input: Record<string, unknown> | undefined
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(pending.inputJson)
|
|
76
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
77
|
+
input = parsed as Record<string, unknown>
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// Malformed JSON — emit without input
|
|
81
|
+
}
|
|
82
|
+
return { pending: null, result: { tool: pending.tool, input } }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { pending, result: null }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function extractCost(message: SDKResultMessage): AgentCost {
|
|
89
|
+
return {
|
|
90
|
+
total_cost_usd: message.total_cost_usd ?? 0,
|
|
91
|
+
duration_ms: message.duration_ms ?? 0,
|
|
92
|
+
duration_api_ms: message.duration_api_ms ?? 0,
|
|
93
|
+
num_turns: message.num_turns ?? 0,
|
|
94
|
+
input_tokens: message.usage?.input_tokens ?? 0,
|
|
95
|
+
output_tokens: message.usage?.output_tokens ?? 0,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- ClaudeSession ---
|
|
100
|
+
|
|
101
|
+
class ClaudeSession implements AgentSession {
|
|
102
|
+
private inputStream = createPushStream<SDKUserMessage>()
|
|
103
|
+
private abortController = new AbortController()
|
|
104
|
+
private queryIterable: Query
|
|
105
|
+
private externalSignal?: AbortSignal
|
|
106
|
+
private signalHandler?: () => void
|
|
107
|
+
|
|
108
|
+
constructor(config: AgentSessionConfig) {
|
|
109
|
+
// Link external signal to our abort controller
|
|
110
|
+
if (config.signal) {
|
|
111
|
+
if (config.signal.aborted) {
|
|
112
|
+
this.abortController.abort()
|
|
113
|
+
} else {
|
|
114
|
+
this.externalSignal = config.signal
|
|
115
|
+
this.signalHandler = () => this.abortController.abort()
|
|
116
|
+
config.signal.addEventListener("abort", this.signalHandler, { once: true })
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.queryIterable = query({
|
|
121
|
+
prompt: this.inputStream,
|
|
122
|
+
options: {
|
|
123
|
+
systemPrompt: config.systemPrompt,
|
|
124
|
+
tools: config.tools ?? [],
|
|
125
|
+
allowedTools: config.allowedTools,
|
|
126
|
+
maxTurns: config.maxTurns,
|
|
127
|
+
cwd: config.cwd,
|
|
128
|
+
model: config.model,
|
|
129
|
+
effort: config.effort as "low" | "medium" | "high" | "max" | undefined,
|
|
130
|
+
permissionMode: "bypassPermissions",
|
|
131
|
+
allowDangerouslySkipPermissions: true,
|
|
132
|
+
persistSession: false,
|
|
133
|
+
includePartialMessages: true,
|
|
134
|
+
abortController: this.abortController,
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
pushMessage(content: string): void {
|
|
140
|
+
this.inputStream.push({
|
|
141
|
+
type: "user",
|
|
142
|
+
message: { role: "user", content },
|
|
143
|
+
parent_tool_use_id: null,
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
endInput(): void {
|
|
148
|
+
this.inputStream.end()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
close(): void {
|
|
152
|
+
if (this.externalSignal && this.signalHandler) {
|
|
153
|
+
this.externalSignal.removeEventListener("abort", this.signalHandler)
|
|
154
|
+
}
|
|
155
|
+
this.abortController.abort()
|
|
156
|
+
this.inputStream.end()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async *[Symbol.asyncIterator](): AsyncIterator<AgentEvent> {
|
|
160
|
+
let pendingTool: PendingToolUse | null = null
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
for await (const message of this.queryIterable as AsyncIterable<SDKMessage>) {
|
|
164
|
+
if (this.abortController.signal.aborted) break
|
|
165
|
+
|
|
166
|
+
if (message.type === "stream_event") {
|
|
167
|
+
const partial = message as unknown as SDKPartialAssistantMessage
|
|
168
|
+
const text = getTextDelta(partial)
|
|
169
|
+
if (text) {
|
|
170
|
+
yield { type: "text_delta", text }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Accumulate tool input across stream events, emit at content_block_stop
|
|
174
|
+
const { pending, result } = processToolUseEvent(partial.event, pendingTool)
|
|
175
|
+
pendingTool = pending
|
|
176
|
+
if (result) {
|
|
177
|
+
yield { type: "tool_use", tool: result.tool, input: result.input }
|
|
178
|
+
}
|
|
179
|
+
} else if (message.type === "assistant") {
|
|
180
|
+
const text = getAssistantText(message as unknown as SDKAssistantMessage)
|
|
181
|
+
yield { type: "assistant_complete", text }
|
|
182
|
+
} else if (message.type === "result") {
|
|
183
|
+
const resultMsg = message as unknown as SDKResultMessage
|
|
184
|
+
const cost = extractCost(resultMsg)
|
|
185
|
+
const success = resultMsg.subtype === "success"
|
|
186
|
+
const error = success
|
|
187
|
+
? undefined
|
|
188
|
+
: resultMsg.errors.join(", ") || resultMsg.subtype
|
|
189
|
+
yield { type: "result", success, error, cost }
|
|
190
|
+
}
|
|
191
|
+
// Skip "user" and "system" message types
|
|
192
|
+
}
|
|
193
|
+
} catch (err: unknown) {
|
|
194
|
+
if (!this.abortController.signal.aborted) {
|
|
195
|
+
yield {
|
|
196
|
+
type: "error",
|
|
197
|
+
error: err instanceof Error ? err.message : String(err),
|
|
198
|
+
retriable: false,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// --- ClaudeProvider ---
|
|
206
|
+
|
|
207
|
+
export class ClaudeProvider implements AgentProvider {
|
|
208
|
+
readonly name = "claude"
|
|
209
|
+
|
|
210
|
+
async listModels(): Promise<AgentModelOption[]> {
|
|
211
|
+
return [
|
|
212
|
+
{
|
|
213
|
+
provider: "claude",
|
|
214
|
+
model: "sonnet",
|
|
215
|
+
label: "Claude / Sonnet",
|
|
216
|
+
description: "Claude Sonnet via Claude Agent SDK",
|
|
217
|
+
isDefault: true,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
provider: "claude",
|
|
221
|
+
model: "opus",
|
|
222
|
+
label: "Claude / Opus",
|
|
223
|
+
description: "Claude Opus via Claude Agent SDK",
|
|
224
|
+
},
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async getDefaultModel(): Promise<string> {
|
|
229
|
+
return "sonnet"
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
createSession(config: AgentSessionConfig): AgentSession {
|
|
233
|
+
return new ClaudeSession(config)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
runOnce(prompt: string, config: AgentSessionConfig): AgentSession {
|
|
237
|
+
const session = this.createSession(config)
|
|
238
|
+
session.pushMessage(prompt)
|
|
239
|
+
session.endInput()
|
|
240
|
+
return session
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async checkAuth(): Promise<AuthResult> {
|
|
244
|
+
const abortController = new AbortController()
|
|
245
|
+
const timeout = setTimeout(() => abortController.abort(), 10_000)
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const idleStream = createPushStream<SDKUserMessage>()
|
|
249
|
+
|
|
250
|
+
const q = query({
|
|
251
|
+
prompt: idleStream,
|
|
252
|
+
options: {
|
|
253
|
+
tools: [],
|
|
254
|
+
persistSession: false,
|
|
255
|
+
abortController,
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
const account: AccountInfo = await q.accountInfo()
|
|
260
|
+
q.close()
|
|
261
|
+
idleStream.end()
|
|
262
|
+
clearTimeout(timeout)
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
authenticated: true,
|
|
266
|
+
account: { ...account } as Record<string, unknown> & { email?: string },
|
|
267
|
+
}
|
|
268
|
+
} catch (err) {
|
|
269
|
+
clearTimeout(timeout)
|
|
270
|
+
abortController.abort()
|
|
271
|
+
return {
|
|
272
|
+
authenticated: false,
|
|
273
|
+
error: err instanceof Error ? err.message : String(err),
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { $ } from "bun"
|
|
2
|
+
import { createRequire } from "node:module"
|
|
3
|
+
import { dirname, join } from "node:path"
|
|
4
|
+
import {
|
|
5
|
+
Codex,
|
|
6
|
+
type ModelReasoningEffort,
|
|
7
|
+
type Thread,
|
|
8
|
+
type ThreadEvent,
|
|
9
|
+
type ThreadItem,
|
|
10
|
+
type ThreadOptions,
|
|
11
|
+
type Usage,
|
|
12
|
+
} from "@openai/codex-sdk"
|
|
13
|
+
import { createPushStream } from "../push-stream.ts"
|
|
14
|
+
import type {
|
|
15
|
+
AgentCost,
|
|
16
|
+
AgentEvent,
|
|
17
|
+
AgentModelOption,
|
|
18
|
+
AgentProvider,
|
|
19
|
+
AgentSession,
|
|
20
|
+
AgentSessionConfig,
|
|
21
|
+
AuthResult,
|
|
22
|
+
} from "./types.ts"
|
|
23
|
+
|
|
24
|
+
const CODEX_DEFAULT_MODEL = "default"
|
|
25
|
+
const PLATFORM_PACKAGE_BY_TARGET: Record<string, string> = {
|
|
26
|
+
"x86_64-unknown-linux-musl": "@openai/codex-linux-x64",
|
|
27
|
+
"aarch64-unknown-linux-musl": "@openai/codex-linux-arm64",
|
|
28
|
+
"x86_64-apple-darwin": "@openai/codex-darwin-x64",
|
|
29
|
+
"aarch64-apple-darwin": "@openai/codex-darwin-arm64",
|
|
30
|
+
"x86_64-pc-windows-msvc": "@openai/codex-win32-x64",
|
|
31
|
+
"aarch64-pc-windows-msvc": "@openai/codex-win32-arm64",
|
|
32
|
+
}
|
|
33
|
+
const require = createRequire(import.meta.url)
|
|
34
|
+
|
|
35
|
+
function getCodexBinPath(): string {
|
|
36
|
+
const targetTriple = getCodexTargetTriple()
|
|
37
|
+
const platformPackage = PLATFORM_PACKAGE_BY_TARGET[targetTriple]
|
|
38
|
+
if (!platformPackage) throw new Error(`Unsupported Codex target triple: ${targetTriple}`)
|
|
39
|
+
|
|
40
|
+
const packageJsonPath = require.resolve(`${platformPackage}/package.json`)
|
|
41
|
+
const vendorRoot = join(dirname(packageJsonPath), "vendor")
|
|
42
|
+
const binaryName = process.platform === "win32" ? "codex.exe" : "codex"
|
|
43
|
+
return join(vendorRoot, targetTriple, "codex", binaryName)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getCodexTargetTriple(): string {
|
|
47
|
+
switch (process.platform) {
|
|
48
|
+
case "darwin":
|
|
49
|
+
if (process.arch === "arm64") return "aarch64-apple-darwin"
|
|
50
|
+
if (process.arch === "x64") return "x86_64-apple-darwin"
|
|
51
|
+
break
|
|
52
|
+
case "linux":
|
|
53
|
+
case "android":
|
|
54
|
+
if (process.arch === "arm64") return "aarch64-unknown-linux-musl"
|
|
55
|
+
if (process.arch === "x64") return "x86_64-unknown-linux-musl"
|
|
56
|
+
break
|
|
57
|
+
case "win32":
|
|
58
|
+
if (process.arch === "arm64") return "aarch64-pc-windows-msvc"
|
|
59
|
+
if (process.arch === "x64") return "x86_64-pc-windows-msvc"
|
|
60
|
+
break
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
throw new Error(`Unsupported Codex platform: ${process.platform} (${process.arch})`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeModel(model: string | undefined): string | undefined {
|
|
67
|
+
if (!model || model === CODEX_DEFAULT_MODEL) return undefined
|
|
68
|
+
return model
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function mapEffort(effort: string | undefined): ModelReasoningEffort | undefined {
|
|
72
|
+
switch (effort) {
|
|
73
|
+
case "low":
|
|
74
|
+
case "medium":
|
|
75
|
+
case "high":
|
|
76
|
+
return effort
|
|
77
|
+
case "max":
|
|
78
|
+
return "xhigh"
|
|
79
|
+
default:
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function stringProviderOption<T extends string>(
|
|
85
|
+
config: AgentSessionConfig,
|
|
86
|
+
key: string,
|
|
87
|
+
allowed: readonly T[],
|
|
88
|
+
): T | undefined {
|
|
89
|
+
const value = config.providerOptions?.[key]
|
|
90
|
+
return typeof value === "string" && (allowed as readonly string[]).includes(value)
|
|
91
|
+
? value as T
|
|
92
|
+
: undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function booleanProviderOption(config: AgentSessionConfig, key: string): boolean | undefined {
|
|
96
|
+
const value = config.providerOptions?.[key]
|
|
97
|
+
return typeof value === "boolean" ? value : undefined
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function hasWriteTools(config: AgentSessionConfig): boolean {
|
|
101
|
+
const tools = config.allowedTools ?? config.tools ?? []
|
|
102
|
+
return tools.some((tool) => {
|
|
103
|
+
const normalized = tool.toLowerCase()
|
|
104
|
+
return normalized === "write" || normalized === "edit"
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildThreadOptions(config: AgentSessionConfig): ThreadOptions {
|
|
109
|
+
const sandboxMode =
|
|
110
|
+
stringProviderOption(config, "sandboxMode", ["read-only", "workspace-write", "danger-full-access"] as const)
|
|
111
|
+
?? (hasWriteTools(config) ? "danger-full-access" : "read-only")
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
model: normalizeModel(config.model),
|
|
115
|
+
modelReasoningEffort: mapEffort(config.effort),
|
|
116
|
+
workingDirectory: config.cwd,
|
|
117
|
+
approvalPolicy: stringProviderOption(config, "approvalPolicy", ["never", "on-request", "on-failure", "untrusted"] as const) ?? "never",
|
|
118
|
+
sandboxMode,
|
|
119
|
+
networkAccessEnabled: booleanProviderOption(config, "networkAccessEnabled") ?? true,
|
|
120
|
+
skipGitRepoCheck: booleanProviderOption(config, "skipGitRepoCheck"),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildPrompt(config: AgentSessionConfig, prompt: string, isFirstTurn: boolean): string {
|
|
125
|
+
const systemPrompt = config.systemPrompt?.trim()
|
|
126
|
+
if (!systemPrompt || !isFirstTurn) return prompt
|
|
127
|
+
|
|
128
|
+
return [
|
|
129
|
+
"System instructions:",
|
|
130
|
+
systemPrompt,
|
|
131
|
+
"",
|
|
132
|
+
"User request:",
|
|
133
|
+
prompt,
|
|
134
|
+
].join("\n")
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getItemText(item: ThreadItem): string | null {
|
|
138
|
+
return item.type === "agent_message" ? item.text : null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getToolUse(item: ThreadItem): { tool: string; input?: Record<string, unknown> } | null {
|
|
142
|
+
switch (item.type) {
|
|
143
|
+
case "command_execution":
|
|
144
|
+
return {
|
|
145
|
+
tool: "Bash",
|
|
146
|
+
input: {
|
|
147
|
+
command: item.command,
|
|
148
|
+
status: item.status,
|
|
149
|
+
exit_code: item.exit_code,
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
case "file_change":
|
|
153
|
+
return {
|
|
154
|
+
tool: "Edit",
|
|
155
|
+
input: {
|
|
156
|
+
file_path: item.changes[0]?.path,
|
|
157
|
+
changes: item.changes,
|
|
158
|
+
status: item.status,
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
case "mcp_tool_call":
|
|
162
|
+
return {
|
|
163
|
+
tool: `${item.server}.${item.tool}`,
|
|
164
|
+
input: {
|
|
165
|
+
arguments: item.arguments,
|
|
166
|
+
status: item.status,
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
case "web_search":
|
|
170
|
+
return {
|
|
171
|
+
tool: "WebSearch",
|
|
172
|
+
input: { query: item.query },
|
|
173
|
+
}
|
|
174
|
+
default:
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function extractCost(usage: Usage, startedAt: number, numTurns: number): AgentCost {
|
|
180
|
+
return {
|
|
181
|
+
total_cost_usd: 0,
|
|
182
|
+
duration_ms: Date.now() - startedAt,
|
|
183
|
+
duration_api_ms: 0,
|
|
184
|
+
num_turns: numTurns,
|
|
185
|
+
input_tokens: usage.input_tokens,
|
|
186
|
+
output_tokens: usage.output_tokens,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
class CodexSession implements AgentSession {
|
|
191
|
+
private input = createPushStream<string>()
|
|
192
|
+
private events = createPushStream<AgentEvent>()
|
|
193
|
+
private abortController = new AbortController()
|
|
194
|
+
private externalSignal?: AbortSignal
|
|
195
|
+
private signalHandler?: () => void
|
|
196
|
+
private closed = false
|
|
197
|
+
private thread: Thread
|
|
198
|
+
private turnCount = 0
|
|
199
|
+
|
|
200
|
+
constructor(
|
|
201
|
+
codex: Codex,
|
|
202
|
+
private config: AgentSessionConfig,
|
|
203
|
+
) {
|
|
204
|
+
this.thread = codex.startThread(buildThreadOptions(config))
|
|
205
|
+
|
|
206
|
+
if (config.signal) {
|
|
207
|
+
if (config.signal.aborted) {
|
|
208
|
+
this.abortController.abort()
|
|
209
|
+
} else {
|
|
210
|
+
this.externalSignal = config.signal
|
|
211
|
+
this.signalHandler = () => this.close()
|
|
212
|
+
config.signal.addEventListener("abort", this.signalHandler, { once: true })
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
this.run().catch((err: unknown) => {
|
|
217
|
+
if (!this.closed) {
|
|
218
|
+
const error = err instanceof Error ? err.message : String(err)
|
|
219
|
+
this.events.push({ type: "error", error, retriable: false })
|
|
220
|
+
this.events.push({ type: "result", success: false, error })
|
|
221
|
+
}
|
|
222
|
+
this.events.end()
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
pushMessage(content: string): void {
|
|
227
|
+
if (this.closed) return
|
|
228
|
+
this.input.push(content)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
endInput(): void {
|
|
232
|
+
this.input.end()
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
close(): void {
|
|
236
|
+
if (this.closed) return
|
|
237
|
+
this.closed = true
|
|
238
|
+
if (this.externalSignal && this.signalHandler) {
|
|
239
|
+
this.externalSignal.removeEventListener("abort", this.signalHandler)
|
|
240
|
+
}
|
|
241
|
+
this.abortController.abort()
|
|
242
|
+
this.input.end()
|
|
243
|
+
this.events.end()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async *[Symbol.asyncIterator](): AsyncIterator<AgentEvent> {
|
|
247
|
+
yield* this.events
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private async run(): Promise<void> {
|
|
251
|
+
for await (const rawPrompt of this.input) {
|
|
252
|
+
if (this.closed || this.abortController.signal.aborted) break
|
|
253
|
+
|
|
254
|
+
if (this.config.maxTurns != null && this.turnCount >= this.config.maxTurns) {
|
|
255
|
+
const error = `Codex session exceeded maxTurns (${this.config.maxTurns})`
|
|
256
|
+
this.events.push({ type: "error", error, retriable: false })
|
|
257
|
+
this.events.push({ type: "result", success: false, error })
|
|
258
|
+
break
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const prompt = buildPrompt(this.config, rawPrompt, this.turnCount === 0)
|
|
262
|
+
await this.runTurn(prompt)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
this.events.end()
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private async runTurn(prompt: string): Promise<void> {
|
|
269
|
+
const startedAt = Date.now()
|
|
270
|
+
this.turnCount += 1
|
|
271
|
+
const textByItemID = new Map<string, string>()
|
|
272
|
+
const emittedTools = new Set<string>()
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const { events } = await this.thread.runStreamed(prompt, {
|
|
276
|
+
signal: this.abortController.signal,
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
for await (const event of events) {
|
|
280
|
+
if (this.closed || this.abortController.signal.aborted) break
|
|
281
|
+
this.handleEvent(event, textByItemID, emittedTools, startedAt)
|
|
282
|
+
}
|
|
283
|
+
} catch (err: unknown) {
|
|
284
|
+
if (!this.closed && !this.abortController.signal.aborted) {
|
|
285
|
+
const error = err instanceof Error ? err.message : String(err)
|
|
286
|
+
this.events.push({ type: "error", error, retriable: false })
|
|
287
|
+
this.events.push({ type: "result", success: false, error })
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private handleEvent(
|
|
293
|
+
event: ThreadEvent,
|
|
294
|
+
textByItemID: Map<string, string>,
|
|
295
|
+
emittedTools: Set<string>,
|
|
296
|
+
startedAt: number,
|
|
297
|
+
): void {
|
|
298
|
+
switch (event.type) {
|
|
299
|
+
case "item.started":
|
|
300
|
+
case "item.updated":
|
|
301
|
+
case "item.completed":
|
|
302
|
+
this.handleItem(event.item, event.type, textByItemID, emittedTools)
|
|
303
|
+
break
|
|
304
|
+
case "turn.completed":
|
|
305
|
+
this.events.push({
|
|
306
|
+
type: "result",
|
|
307
|
+
success: true,
|
|
308
|
+
cost: extractCost(event.usage, startedAt, this.turnCount),
|
|
309
|
+
})
|
|
310
|
+
break
|
|
311
|
+
case "turn.failed":
|
|
312
|
+
this.events.push({ type: "error", error: event.error.message, retriable: false })
|
|
313
|
+
this.events.push({ type: "result", success: false, error: event.error.message })
|
|
314
|
+
break
|
|
315
|
+
case "error":
|
|
316
|
+
this.events.push({ type: "error", error: event.message, retriable: false })
|
|
317
|
+
this.events.push({ type: "result", success: false, error: event.message })
|
|
318
|
+
break
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private handleItem(
|
|
323
|
+
item: ThreadItem,
|
|
324
|
+
eventType: "item.started" | "item.updated" | "item.completed",
|
|
325
|
+
textByItemID: Map<string, string>,
|
|
326
|
+
emittedTools: Set<string>,
|
|
327
|
+
): void {
|
|
328
|
+
const text = getItemText(item)
|
|
329
|
+
if (text != null) {
|
|
330
|
+
const previous = textByItemID.get(item.id) ?? ""
|
|
331
|
+
const delta = text.startsWith(previous) ? text.slice(previous.length) : text
|
|
332
|
+
textByItemID.set(item.id, text)
|
|
333
|
+
if (delta) this.events.push({ type: "text_delta", text: delta })
|
|
334
|
+
if (eventType === "item.completed" && text.trim()) {
|
|
335
|
+
this.events.push({ type: "assistant_complete", text })
|
|
336
|
+
}
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (item.type === "error") {
|
|
341
|
+
this.events.push({ type: "error", error: item.message, retriable: false })
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const toolUse = getToolUse(item)
|
|
346
|
+
if (!toolUse) return
|
|
347
|
+
|
|
348
|
+
const status = "status" in item ? item.status : eventType
|
|
349
|
+
const key = `${item.id}:${item.type}:${status}`
|
|
350
|
+
if (emittedTools.has(key)) return
|
|
351
|
+
emittedTools.add(key)
|
|
352
|
+
this.events.push({ type: "tool_use", ...toolUse })
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export class CodexProvider implements AgentProvider {
|
|
357
|
+
readonly name = "codex"
|
|
358
|
+
private codex = new Codex()
|
|
359
|
+
|
|
360
|
+
async listModels(): Promise<AgentModelOption[]> {
|
|
361
|
+
return [
|
|
362
|
+
{
|
|
363
|
+
provider: "codex",
|
|
364
|
+
model: CODEX_DEFAULT_MODEL,
|
|
365
|
+
label: "Codex / Default",
|
|
366
|
+
description: "Configured Codex CLI default model",
|
|
367
|
+
isDefault: true,
|
|
368
|
+
},
|
|
369
|
+
]
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async getDefaultModel(): Promise<string> {
|
|
373
|
+
return CODEX_DEFAULT_MODEL
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
createSession(config: AgentSessionConfig): AgentSession {
|
|
377
|
+
return new CodexSession(this.codex, config)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
runOnce(prompt: string, config: AgentSessionConfig): AgentSession {
|
|
381
|
+
const session = this.createSession(config)
|
|
382
|
+
session.pushMessage(prompt)
|
|
383
|
+
session.endInput()
|
|
384
|
+
return session
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async checkAuth(): Promise<AuthResult> {
|
|
388
|
+
const codexBinPath = getCodexBinPath()
|
|
389
|
+
const [statusResult, versionResult] = await Promise.all([
|
|
390
|
+
$`${codexBinPath} login status`.nothrow().quiet(),
|
|
391
|
+
$`${codexBinPath} --version`.nothrow().quiet(),
|
|
392
|
+
])
|
|
393
|
+
|
|
394
|
+
const status = `${statusResult.stdout.toString()}${statusResult.stderr.toString()}`.trim()
|
|
395
|
+
const version = `${versionResult.stdout.toString()}${versionResult.stderr.toString()}`.trim()
|
|
396
|
+
|
|
397
|
+
if (statusResult.exitCode === 0) {
|
|
398
|
+
return {
|
|
399
|
+
authenticated: true,
|
|
400
|
+
account: {
|
|
401
|
+
provider: "codex",
|
|
402
|
+
status,
|
|
403
|
+
version,
|
|
404
|
+
},
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
authenticated: false,
|
|
410
|
+
error: status || `Codex login status exited with code ${statusResult.exitCode}`,
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { setProvider } from "./index.ts"
|
|
2
|
+
import { ClaudeProvider } from "./claude-provider.ts"
|
|
3
|
+
import { OpenCodeProvider } from "./opencode-provider.ts"
|
|
4
|
+
import { CodexProvider } from "./codex-provider.ts"
|
|
5
|
+
|
|
6
|
+
export function registerDefaultProviders(): void {
|
|
7
|
+
setProvider("claude", new ClaudeProvider())
|
|
8
|
+
setProvider("opencode", new OpenCodeProvider())
|
|
9
|
+
setProvider("codex", new CodexProvider())
|
|
10
|
+
}
|