ubume 0.1.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/README.md +88 -0
- package/bin/codexa-local-harness-bridge.js +3 -0
- package/bin/codexa.js +28 -0
- package/bin/ubume-local-harness-bridge.js +325 -0
- package/bin/ubume.js +398 -0
- package/package.json +66 -0
- package/src/app.tsx +5759 -0
- package/src/commands/handler.ts +889 -0
- package/src/config/appVersion.ts +69 -0
- package/src/config/buildInfo.ts +3 -0
- package/src/config/launchArgs.ts +196 -0
- package/src/config/layeredConfig.ts +853 -0
- package/src/config/legacyEnv.ts +16 -0
- package/src/config/persistence.ts +377 -0
- package/src/config/runtimeConfig.ts +558 -0
- package/src/config/settings.ts +405 -0
- package/src/config/toml-serialize.ts +109 -0
- package/src/config/trustStore.ts +84 -0
- package/src/config/updateCheckCache.ts +85 -0
- package/src/core/README.md +55 -0
- package/src/core/agent/loop.ts +464 -0
- package/src/core/agent/protocol.ts +345 -0
- package/src/core/agent/tools.ts +423 -0
- package/src/core/auth/codexAuth.ts +359 -0
- package/src/core/codex/codexExecArgs.ts +166 -0
- package/src/core/codex/codexLaunch.ts +163 -0
- package/src/core/codex/codexPrompt.ts +429 -0
- package/src/core/debug/inputDebug.ts +51 -0
- package/src/core/debug/localStreamDebug.ts +50 -0
- package/src/core/debug/modelStateDebug.ts +35 -0
- package/src/core/executables/antigravityExecutable.ts +48 -0
- package/src/core/executables/claudeExecutable.ts +63 -0
- package/src/core/executables/codexExecutable.ts +160 -0
- package/src/core/executables/executableResolver.ts +164 -0
- package/src/core/executables/geminiExecutable.ts +78 -0
- package/src/core/models/codexCapabilities.ts +97 -0
- package/src/core/models/codexModelCapabilities.ts +624 -0
- package/src/core/models/codexModelsCacheSeed.ts +153 -0
- package/src/core/models/modelSpecs.ts +303 -0
- package/src/core/models/providerModelCache.ts +94 -0
- package/src/core/perf/profiler.ts +125 -0
- package/src/core/perf/renderDebug.ts +398 -0
- package/src/core/process/CommandRunner.ts +280 -0
- package/src/core/process/processValidation.ts +111 -0
- package/src/core/providerLauncher/launcher.ts +220 -0
- package/src/core/providerLauncher/registry.ts +354 -0
- package/src/core/providerLauncher/types.ts +95 -0
- package/src/core/providerLauncher/workspaceConfig.ts +487 -0
- package/src/core/providerRuntime/anthropic.ts +580 -0
- package/src/core/providerRuntime/antigravity.ts +500 -0
- package/src/core/providerRuntime/capabilityProfile.ts +383 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -0
- package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
- package/src/core/providerRuntime/codexaCupy.ts +97 -0
- package/src/core/providerRuntime/codexaNative.ts +425 -0
- package/src/core/providerRuntime/contextMetadata.ts +397 -0
- package/src/core/providerRuntime/gemini.ts +789 -0
- package/src/core/providerRuntime/lmstudio.ts +118 -0
- package/src/core/providerRuntime/local.ts +770 -0
- package/src/core/providerRuntime/localHarness/runtime.ts +1090 -0
- package/src/core/providerRuntime/localOutputBudget.ts +17 -0
- package/src/core/providerRuntime/mistralVibe.ts +667 -0
- package/src/core/providerRuntime/models.ts +175 -0
- package/src/core/providerRuntime/reasoning.ts +20 -0
- package/src/core/providerRuntime/registry.ts +286 -0
- package/src/core/providerRuntime/types.ts +142 -0
- package/src/core/providerRuntime/unsloth.ts +216 -0
- package/src/core/providers/codexJsonStream.ts +305 -0
- package/src/core/providers/codexSubprocess.ts +378 -0
- package/src/core/providers/codexTranscript.ts +695 -0
- package/src/core/providers/openaiNative.ts +13 -0
- package/src/core/providers/registry.ts +21 -0
- package/src/core/providers/types.ts +91 -0
- package/src/core/shared/attachments.ts +101 -0
- package/src/core/shared/cleanupFastFail.ts +67 -0
- package/src/core/shared/clipboard.ts +24 -0
- package/src/core/shared/clipboardImage.ts +111 -0
- package/src/core/shared/githubDiagnostics.ts +222 -0
- package/src/core/shared/hollowResponseFormat.ts +39 -0
- package/src/core/terminal/clearFrameBoundary.ts +852 -0
- package/src/core/terminal/frameLock.ts +110 -0
- package/src/core/terminal/inkRenderReset.ts +123 -0
- package/src/core/terminal/startupClear.ts +20 -0
- package/src/core/terminal/terminalCapabilities.ts +100 -0
- package/src/core/terminal/terminalControl.ts +169 -0
- package/src/core/terminal/terminalSanitize.ts +147 -0
- package/src/core/terminal/terminalTitle.ts +400 -0
- package/src/core/version/channel.ts +27 -0
- package/src/core/version/packageManager.ts +119 -0
- package/src/core/version/updateCheck.ts +203 -0
- package/src/core/workspace/appData.ts +107 -0
- package/src/core/workspace/conversationStore.ts +335 -0
- package/src/core/workspace/launchContext.ts +259 -0
- package/src/core/workspace/planStorage.ts +135 -0
- package/src/core/workspace/projectInstructions.ts +54 -0
- package/src/core/workspace/scratchDir.ts +64 -0
- package/src/core/workspace/workspaceActivity.ts +384 -0
- package/src/core/workspace/workspaceGuard.ts +377 -0
- package/src/core/workspace/workspaceRoot.ts +47 -0
- package/src/exec.ts +73 -0
- package/src/headless/execArgs.ts +296 -0
- package/src/headless/execRunner.ts +304 -0
- package/src/index.tsx +270 -0
- package/src/legacyEnvBootstrap.ts +5 -0
- package/src/session/appSession.ts +771 -0
- package/src/session/chatLifecycle.ts +994 -0
- package/src/session/conversation.ts +107 -0
- package/src/session/liveRenderScheduler.ts +214 -0
- package/src/session/persistedResponse.ts +93 -0
- package/src/session/planFlow.ts +159 -0
- package/src/session/planTranscript.ts +19 -0
- package/src/session/promptRunSchedule.ts +26 -0
- package/src/session/types.ts +234 -0
- package/src/test/runtimeTestUtils.ts +14 -0
- package/src/types/react-dom.d.ts +3 -0
- package/src/ui/chrome/ActivityBars.tsx +68 -0
- package/src/ui/chrome/ActivityIndicator.tsx +58 -0
- package/src/ui/chrome/AnimatedStatusText.tsx +69 -0
- package/src/ui/chrome/AppShell.tsx +474 -0
- package/src/ui/chrome/BottomComposer.tsx +1135 -0
- package/src/ui/chrome/DashCard.tsx +82 -0
- package/src/ui/chrome/RunFooter.tsx +65 -0
- package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
- package/src/ui/chrome/Spinner.tsx +25 -0
- package/src/ui/chrome/TopHeader.tsx +439 -0
- package/src/ui/chrome/UpdateAvailableCard.tsx +42 -0
- package/src/ui/chrome/busyStatusAnimation.ts +11 -0
- package/src/ui/input/commandNormalize.ts +66 -0
- package/src/ui/input/focus.ts +73 -0
- package/src/ui/input/imageAttachments.ts +18 -0
- package/src/ui/input/inputBuffer.ts +203 -0
- package/src/ui/input/pastedContent.ts +75 -0
- package/src/ui/input/rawArrowKeys.ts +28 -0
- package/src/ui/input/slashCommands.ts +43 -0
- package/src/ui/input/useStdinRawModeLease.ts +23 -0
- package/src/ui/layout.ts +560 -0
- package/src/ui/panels/AttachmentImportPanel.tsx +131 -0
- package/src/ui/panels/AuthPanel.tsx +149 -0
- package/src/ui/panels/BackendPicker.tsx +28 -0
- package/src/ui/panels/ModePicker.tsx +31 -0
- package/src/ui/panels/ModelPicker.tsx +31 -0
- package/src/ui/panels/ModelPickerScreen.tsx +761 -0
- package/src/ui/panels/ModelReasoningPicker.tsx +458 -0
- package/src/ui/panels/Panel.tsx +51 -0
- package/src/ui/panels/PermissionsPanel.tsx +78 -0
- package/src/ui/panels/PlanActionPicker.tsx +187 -0
- package/src/ui/panels/ProviderPicker.tsx +753 -0
- package/src/ui/panels/ProviderSetupPrompt.tsx +52 -0
- package/src/ui/panels/ReasoningPicker.tsx +46 -0
- package/src/ui/panels/ResumePicker.tsx +90 -0
- package/src/ui/panels/SelectionPanel.tsx +138 -0
- package/src/ui/panels/SettingsPanel.tsx +156 -0
- package/src/ui/panels/TextEntryPanel.tsx +139 -0
- package/src/ui/panels/ThemePicker.tsx +32 -0
- package/src/ui/panels/ToolApprovalPanel.tsx +46 -0
- package/src/ui/panels/UpdatePromptPanel.tsx +236 -0
- package/src/ui/panels/responsivePickerViewport.ts +64 -0
- package/src/ui/render/Markdown.tsx +331 -0
- package/src/ui/render/diffRenderer.ts +116 -0
- package/src/ui/render/logoVariants.ts +113 -0
- package/src/ui/render/modeDisplay.ts +52 -0
- package/src/ui/render/outputPipeline.ts +64 -0
- package/src/ui/render/runtimeDisplay.ts +128 -0
- package/src/ui/render/terminalAnswerFormat.ts +128 -0
- package/src/ui/render/textLayout.ts +392 -0
- package/src/ui/theme.tsx +274 -0
- package/src/ui/themeFlow.ts +41 -0
- package/src/ui/timeline/ActionRequiredBlock.tsx +38 -0
- package/src/ui/timeline/AgentBlock.tsx +130 -0
- package/src/ui/timeline/StaticIntroItem.tsx +54 -0
- package/src/ui/timeline/ThinkingBlock.tsx +100 -0
- package/src/ui/timeline/Timeline.tsx +1410 -0
- package/src/ui/timeline/TranscriptShell.tsx +302 -0
- package/src/ui/timeline/TurnGroup.tsx +673 -0
- package/src/ui/timeline/layoutListWindow.ts +145 -0
- package/src/ui/timeline/liveViewportWindow.ts +68 -0
- package/src/ui/timeline/progressEntries.ts +156 -0
- package/src/ui/timeline/runActivityView.ts +37 -0
- package/src/ui/timeline/staticTranscriptCache.ts +174 -0
- package/src/ui/timeline/streamCoalesce.ts +53 -0
- package/src/ui/timeline/timelineMeasure.ts +3273 -0
- package/src/ui/useThrottledValue.ts +31 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_UNSLOTH_ROOT_URL = "http://127.0.0.1:8888";
|
|
10
|
+
|
|
11
|
+
type FetchImpl = typeof fetch;
|
|
12
|
+
|
|
13
|
+
interface UnslothKeyCache {
|
|
14
|
+
servers?: Record<string, {
|
|
15
|
+
saved?: unknown;
|
|
16
|
+
minted?: unknown;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UnslothConnection {
|
|
21
|
+
rootUrl: string;
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
apiKey: string;
|
|
24
|
+
authSource: "environment" | "agent-cache";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface UnslothModelInfo {
|
|
28
|
+
id: string;
|
|
29
|
+
loaded: boolean;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
34
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
35
|
+
? value as Record<string, unknown>
|
|
36
|
+
: null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveHome(env: NodeJS.ProcessEnv): string {
|
|
40
|
+
return env.USERPROFILE?.trim() || env.HOME?.trim() || homedir();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeUnslothRootUrl(value: string): string {
|
|
44
|
+
const url = new URL(value.trim() || DEFAULT_UNSLOTH_ROOT_URL);
|
|
45
|
+
if (url.hostname.toLowerCase() === "localhost") {
|
|
46
|
+
url.hostname = "127.0.0.1";
|
|
47
|
+
}
|
|
48
|
+
url.pathname = url.pathname.replace(/\/+$/, "").replace(/\/v1$/i, "") || "/";
|
|
49
|
+
url.search = "";
|
|
50
|
+
url.hash = "";
|
|
51
|
+
return url.toString().replace(/\/$/, "");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolveUnslothRootUrl(env: NodeJS.ProcessEnv = process.env): string {
|
|
55
|
+
return normalizeUnslothRootUrl(env.UNSLOTH_STUDIO_URL?.trim() || DEFAULT_UNSLOTH_ROOT_URL);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isLoopbackUnslothUrl(value: string): boolean {
|
|
59
|
+
try {
|
|
60
|
+
const hostname = new URL(value).hostname.toLowerCase();
|
|
61
|
+
if (hostname === "localhost" || hostname === "::1") return true;
|
|
62
|
+
const parts = hostname.split(".").map(Number);
|
|
63
|
+
return parts.length === 4 && parts.every(Number.isInteger) && parts[0] === 127;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readIdentitySecret(env: NodeJS.ProcessEnv): Buffer | null {
|
|
70
|
+
const authDb = join(resolveHome(env), ".unsloth", "studio", "auth", "auth.db");
|
|
71
|
+
if (!existsSync(authDb)) return null;
|
|
72
|
+
try {
|
|
73
|
+
const { Database } = require("bun:sqlite") as {
|
|
74
|
+
Database: new (path: string, options: { readonly: boolean }) => {
|
|
75
|
+
query: (sql: string) => { get: (...params: unknown[]) => unknown };
|
|
76
|
+
close: () => void;
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
const database = new Database(authDb, { readonly: true });
|
|
80
|
+
try {
|
|
81
|
+
const row = asRecord(database.query("SELECT value FROM app_secrets WHERE key = ?").get("studio_identity_secret"));
|
|
82
|
+
const value = row?.value;
|
|
83
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/i.test(value)
|
|
84
|
+
? Buffer.from(value, "hex")
|
|
85
|
+
: null;
|
|
86
|
+
} finally {
|
|
87
|
+
database.close();
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function proofMessage(nonce: Buffer, hostname: string, port: number): Buffer {
|
|
95
|
+
const normalizedHost = hostname.toLowerCase() === "localhost" ? "127.0.0.1" : hostname.toLowerCase();
|
|
96
|
+
return Buffer.concat([nonce, Buffer.from(`|${normalizedHost}|${port}`)]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function verifyUnslothIdentity(options: {
|
|
100
|
+
rootUrl: string;
|
|
101
|
+
fetchImpl?: FetchImpl;
|
|
102
|
+
signal?: AbortSignal;
|
|
103
|
+
env?: NodeJS.ProcessEnv;
|
|
104
|
+
identitySecret?: Buffer | null;
|
|
105
|
+
nonce?: Buffer;
|
|
106
|
+
}): Promise<boolean> {
|
|
107
|
+
if (!isLoopbackUnslothUrl(options.rootUrl)) return false;
|
|
108
|
+
const env = options.env ?? process.env;
|
|
109
|
+
const secret = options.identitySecret === undefined ? readIdentitySecret(env) : options.identitySecret;
|
|
110
|
+
if (!secret || secret.length !== 32) return false;
|
|
111
|
+
|
|
112
|
+
const url = new URL(options.rootUrl);
|
|
113
|
+
const port = Number(url.port || (url.protocol === "https:" ? 443 : 80));
|
|
114
|
+
const nonce = options.nonce ?? randomBytes(32);
|
|
115
|
+
const encodedNonce = nonce.toString("base64").replace(/\+/g, "-").replace(/\//g, "_");
|
|
116
|
+
const identityUrl = new URL("/api/auth/identity", options.rootUrl);
|
|
117
|
+
identityUrl.searchParams.set("nonce", encodedNonce);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const response = await (options.fetchImpl ?? globalThis.fetch)(identityUrl, {
|
|
121
|
+
method: "GET",
|
|
122
|
+
redirect: "manual",
|
|
123
|
+
signal: options.signal,
|
|
124
|
+
});
|
|
125
|
+
if (!response.ok || response.status >= 300 && response.status < 400) return false;
|
|
126
|
+
const body = asRecord(await response.json());
|
|
127
|
+
const proof = body?.proof;
|
|
128
|
+
if (typeof proof !== "string" || !/^[a-f0-9]{64}$/i.test(proof)) return false;
|
|
129
|
+
const expected = createHmac("sha256", secret)
|
|
130
|
+
.update(proofMessage(nonce, url.hostname, port))
|
|
131
|
+
.digest();
|
|
132
|
+
const actual = Buffer.from(proof, "hex");
|
|
133
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function cachedKeysForServer(rootUrl: string, env: NodeJS.ProcessEnv): string[] {
|
|
140
|
+
const cachePath = join(resolveHome(env), ".unsloth", "studio", "auth", "agent_api_key.json");
|
|
141
|
+
try {
|
|
142
|
+
const parsed = JSON.parse(readFileSync(cachePath, "utf8")) as UnslothKeyCache;
|
|
143
|
+
const entry = parsed.servers?.[rootUrl];
|
|
144
|
+
const keys = [entry?.saved, entry?.minted]
|
|
145
|
+
.flatMap((value) => Array.isArray(value) ? value : [])
|
|
146
|
+
.filter((value): value is string => typeof value === "string" && value.startsWith("sk-unsloth-"));
|
|
147
|
+
return [...new Set(keys)];
|
|
148
|
+
} catch {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function keyAccepted(rootUrl: string, apiKey: string, fetchImpl: FetchImpl, signal?: AbortSignal): Promise<boolean> {
|
|
154
|
+
const response = await fetchImpl(`${rootUrl}/v1/models`, {
|
|
155
|
+
method: "GET",
|
|
156
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
157
|
+
redirect: "manual",
|
|
158
|
+
signal,
|
|
159
|
+
});
|
|
160
|
+
if (response.status === 401 || response.status === 403) return false;
|
|
161
|
+
if (!response.ok) throw new Error(`Unsloth returned HTTP ${response.status} while checking its API key.`);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function resolveUnslothConnection(options: {
|
|
166
|
+
fetchImpl?: FetchImpl;
|
|
167
|
+
signal?: AbortSignal;
|
|
168
|
+
env?: NodeJS.ProcessEnv;
|
|
169
|
+
identitySecret?: Buffer | null;
|
|
170
|
+
} = {}): Promise<UnslothConnection> {
|
|
171
|
+
const env = options.env ?? process.env;
|
|
172
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
173
|
+
const rootUrl = resolveUnslothRootUrl(env);
|
|
174
|
+
const explicitKey = env.UNSLOTH_API_KEY?.trim();
|
|
175
|
+
if (explicitKey) {
|
|
176
|
+
if (!await keyAccepted(rootUrl, explicitKey, fetchImpl, options.signal)) {
|
|
177
|
+
throw new Error("UNSLOTH_API_KEY was rejected by the configured Unsloth server.");
|
|
178
|
+
}
|
|
179
|
+
return { rootUrl, baseUrl: `${rootUrl}/v1`, apiKey: explicitKey, authSource: "environment" };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const identityVerified = await verifyUnslothIdentity({
|
|
183
|
+
rootUrl,
|
|
184
|
+
fetchImpl,
|
|
185
|
+
signal: options.signal,
|
|
186
|
+
env,
|
|
187
|
+
identitySecret: options.identitySecret,
|
|
188
|
+
});
|
|
189
|
+
if (!identityVerified) {
|
|
190
|
+
throw new Error("Could not securely verify the local Unsloth server. Set UNSLOTH_API_KEY or create an API key in Unsloth Settings > API.");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const apiKey of cachedKeysForServer(rootUrl, env)) {
|
|
194
|
+
if (await keyAccepted(rootUrl, apiKey, fetchImpl, options.signal)) {
|
|
195
|
+
return { rootUrl, baseUrl: `${rootUrl}/v1`, apiKey, authSource: "agent-cache" };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
throw new Error("No valid Unsloth agent API key was found. Set UNSLOTH_API_KEY or create an API key in Unsloth Settings > API.");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function parseUnslothModels(value: unknown): UnslothModelInfo[] {
|
|
202
|
+
const data = asRecord(value)?.data;
|
|
203
|
+
if (!Array.isArray(data)) return [];
|
|
204
|
+
const seen = new Set<string>();
|
|
205
|
+
const models: UnslothModelInfo[] = [];
|
|
206
|
+
for (const item of data) {
|
|
207
|
+
const record = asRecord(item);
|
|
208
|
+
if (!record || typeof record.id !== "string" || !record.id.trim()) continue;
|
|
209
|
+
const id = record.id.trim();
|
|
210
|
+
const key = id.toLowerCase();
|
|
211
|
+
if (seen.has(key)) continue;
|
|
212
|
+
seen.add(key);
|
|
213
|
+
models.push({ ...record, id, loaded: record.loaded === true });
|
|
214
|
+
}
|
|
215
|
+
return models;
|
|
216
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import type { BackendProgressUpdate } from "./types.js";
|
|
2
|
+
import type { RunToolActivity } from "../../session/types.js";
|
|
3
|
+
|
|
4
|
+
type CodexThreadEvent =
|
|
5
|
+
| { type: "thread.started"; thread_id: string }
|
|
6
|
+
| { type: "turn.started" }
|
|
7
|
+
| { type: "turn.completed"; usage?: { input_tokens?: number; cached_input_tokens?: number; output_tokens?: number } }
|
|
8
|
+
| { type: "turn.failed"; error?: { message?: string } }
|
|
9
|
+
| { type: "error"; message?: string }
|
|
10
|
+
| { type: "item.started" | "item.updated" | "item.completed"; item: CodexThreadItem };
|
|
11
|
+
|
|
12
|
+
type CodexThreadItem =
|
|
13
|
+
| {
|
|
14
|
+
id: string;
|
|
15
|
+
type: "agent_message";
|
|
16
|
+
text: string;
|
|
17
|
+
}
|
|
18
|
+
| {
|
|
19
|
+
id: string;
|
|
20
|
+
type: "reasoning";
|
|
21
|
+
text: string;
|
|
22
|
+
}
|
|
23
|
+
| {
|
|
24
|
+
id: string;
|
|
25
|
+
type: "command_execution";
|
|
26
|
+
command: string;
|
|
27
|
+
status: "in_progress" | "completed" | "failed";
|
|
28
|
+
aggregated_output?: string;
|
|
29
|
+
exit_code?: number;
|
|
30
|
+
}
|
|
31
|
+
| {
|
|
32
|
+
id: string;
|
|
33
|
+
type: "mcp_tool_call";
|
|
34
|
+
server: string;
|
|
35
|
+
tool: string;
|
|
36
|
+
status: "in_progress" | "completed" | "failed";
|
|
37
|
+
error?: { message?: string };
|
|
38
|
+
}
|
|
39
|
+
| {
|
|
40
|
+
id: string;
|
|
41
|
+
type: "web_search";
|
|
42
|
+
query: string;
|
|
43
|
+
}
|
|
44
|
+
| {
|
|
45
|
+
id: string;
|
|
46
|
+
type: "todo_list";
|
|
47
|
+
items: Array<{ text: string; completed: boolean }>;
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
id: string;
|
|
51
|
+
type: "file_change";
|
|
52
|
+
changes: Array<{ path: string; kind: "add" | "delete" | "update" }>;
|
|
53
|
+
status: "completed" | "failed";
|
|
54
|
+
}
|
|
55
|
+
| {
|
|
56
|
+
id: string;
|
|
57
|
+
type: "error";
|
|
58
|
+
message: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export interface CodexJsonStreamHandlers {
|
|
62
|
+
onAssistantDelta?: (chunk: string) => void;
|
|
63
|
+
onFinalAnswerObserved?: (response: string) => void;
|
|
64
|
+
onProgress?: (update: BackendProgressUpdate) => void;
|
|
65
|
+
onToolActivity?: (activity: RunToolActivity) => void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeProgressText(text: string | undefined): string | null {
|
|
69
|
+
if (!text) return null;
|
|
70
|
+
const normalized = text
|
|
71
|
+
.replace(/\r\n/g, "\n")
|
|
72
|
+
.replace(/\r/g, "\n")
|
|
73
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
74
|
+
.trim();
|
|
75
|
+
return normalized || null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function firstMeaningfulLine(text: string | undefined): string | null {
|
|
79
|
+
if (!text) return null;
|
|
80
|
+
const line = text
|
|
81
|
+
.replace(/\r\n/g, "\n")
|
|
82
|
+
.replace(/\r/g, "\n")
|
|
83
|
+
.split("\n")
|
|
84
|
+
.map((part) => part.trim())
|
|
85
|
+
.find(Boolean);
|
|
86
|
+
return line ?? null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function summarizeCommandExecution(item: Extract<CodexThreadItem, { type: "command_execution" }>): string {
|
|
90
|
+
const firstLine = firstMeaningfulLine(item.aggregated_output);
|
|
91
|
+
if (item.status === "failed") {
|
|
92
|
+
if (firstLine) return firstLine;
|
|
93
|
+
if (item.exit_code != null) return `Exit code ${item.exit_code}`;
|
|
94
|
+
return "Failed";
|
|
95
|
+
}
|
|
96
|
+
if (firstLine) return firstLine;
|
|
97
|
+
if (item.exit_code != null) return `Exit code ${item.exit_code}`;
|
|
98
|
+
return item.status === "completed" ? "Completed" : "Running";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function summarizeTodoList(item: Extract<CodexThreadItem, { type: "todo_list" }>): string | null {
|
|
102
|
+
if (!item.items.length) return null;
|
|
103
|
+
const completedCount = item.items.filter((entry) => entry.completed).length;
|
|
104
|
+
const nextPending = item.items.find((entry) => !entry.completed);
|
|
105
|
+
if (nextPending) {
|
|
106
|
+
return `Todo ${completedCount}/${item.items.length}: ${nextPending.text}`;
|
|
107
|
+
}
|
|
108
|
+
return `Todo ${completedCount}/${item.items.length}: all tasks complete`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function summarizeReasoning(item: Extract<CodexThreadItem, { type: "reasoning" }>): string | null {
|
|
112
|
+
return normalizeProgressText(item.text);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function summarizeFileChange(item: Extract<CodexThreadItem, { type: "file_change" }>): string | null {
|
|
116
|
+
if (!item.changes.length) return null;
|
|
117
|
+
const [first] = item.changes;
|
|
118
|
+
if (item.changes.length === 1 && first) {
|
|
119
|
+
const verb = first.kind === "add" ? "Created" : first.kind === "delete" ? "Deleted" : "Updated";
|
|
120
|
+
return `${verb} ${first.path}`;
|
|
121
|
+
}
|
|
122
|
+
return `Applied ${item.changes.length} file changes`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function mapToolActivity(item: Extract<CodexThreadItem, {
|
|
126
|
+
type: "command_execution" | "mcp_tool_call" | "web_search";
|
|
127
|
+
}>, phase: "item.started" | "item.updated" | "item.completed", existing: RunToolActivity | undefined): RunToolActivity {
|
|
128
|
+
const startedAt = existing?.startedAt ?? Date.now();
|
|
129
|
+
|
|
130
|
+
if (item.type === "command_execution") {
|
|
131
|
+
const status = item.status === "in_progress" ? "running" : item.status;
|
|
132
|
+
return {
|
|
133
|
+
id: item.id,
|
|
134
|
+
command: item.command,
|
|
135
|
+
status,
|
|
136
|
+
startedAt,
|
|
137
|
+
completedAt: status === "running" ? null : Date.now(),
|
|
138
|
+
summary: status === "running" ? undefined : summarizeCommandExecution(item),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (item.type === "mcp_tool_call") {
|
|
143
|
+
return {
|
|
144
|
+
id: item.id,
|
|
145
|
+
command: `${item.server}:${item.tool}`,
|
|
146
|
+
status: item.status === "in_progress" ? "running" : item.status,
|
|
147
|
+
startedAt,
|
|
148
|
+
completedAt: item.status === "in_progress" ? null : Date.now(),
|
|
149
|
+
summary: item.status === "failed"
|
|
150
|
+
? item.error?.message ?? "Failed"
|
|
151
|
+
: item.status === "completed"
|
|
152
|
+
? "Completed"
|
|
153
|
+
: undefined,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
id: item.id,
|
|
159
|
+
command: `web search: ${item.query}`,
|
|
160
|
+
status: phase === "item.completed" ? "completed" : "running",
|
|
161
|
+
startedAt,
|
|
162
|
+
completedAt: phase === "item.completed" ? Date.now() : null,
|
|
163
|
+
summary: phase === "item.completed" ? "Completed" : undefined,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function createCodexJsonStreamParser(handlers: CodexJsonStreamHandlers) {
|
|
168
|
+
const assistantTextById = new Map<string, string>();
|
|
169
|
+
const progressTextById = new Map<string, string>();
|
|
170
|
+
const toolActivityById = new Map<string, RunToolActivity>();
|
|
171
|
+
let sawEvent = false;
|
|
172
|
+
let finalResponse = "";
|
|
173
|
+
let finalAnswerObserved = false;
|
|
174
|
+
let failureMessage: string | null = null;
|
|
175
|
+
|
|
176
|
+
const emitProgress = (
|
|
177
|
+
key: string,
|
|
178
|
+
source: BackendProgressUpdate["source"],
|
|
179
|
+
summary: string | null | undefined,
|
|
180
|
+
) => {
|
|
181
|
+
const next = normalizeProgressText(summary ?? "");
|
|
182
|
+
if (!next) return;
|
|
183
|
+
if (progressTextById.get(key) === next) return;
|
|
184
|
+
progressTextById.set(key, next);
|
|
185
|
+
handlers.onProgress?.({ id: key, source, text: next });
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// Codex JSON stream sends cumulative text per item ID rather than deltas.
|
|
189
|
+
// We track the last emitted text and emit only the newly appended suffix.
|
|
190
|
+
const emitAssistantText = (itemId: string, nextText: string) => {
|
|
191
|
+
const previous = assistantTextById.get(itemId) ?? "";
|
|
192
|
+
assistantTextById.set(itemId, nextText);
|
|
193
|
+
finalResponse = nextText;
|
|
194
|
+
|
|
195
|
+
if (!nextText) return;
|
|
196
|
+
if (!previous) {
|
|
197
|
+
handlers.onAssistantDelta?.(nextText);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (nextText.startsWith(previous)) {
|
|
202
|
+
const delta = nextText.slice(previous.length);
|
|
203
|
+
if (delta) {
|
|
204
|
+
handlers.onAssistantDelta?.(delta);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const upsertTool = (item: Extract<CodexThreadItem, {
|
|
210
|
+
type: "command_execution" | "mcp_tool_call" | "web_search";
|
|
211
|
+
}>, phase: "item.started" | "item.updated" | "item.completed") => {
|
|
212
|
+
const next = mapToolActivity(item, phase, toolActivityById.get(item.id));
|
|
213
|
+
toolActivityById.set(item.id, next);
|
|
214
|
+
handlers.onToolActivity?.(next);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const handleItem = (phase: "item.started" | "item.updated" | "item.completed", item: CodexThreadItem) => {
|
|
218
|
+
switch (item.type) {
|
|
219
|
+
case "agent_message":
|
|
220
|
+
emitAssistantText(item.id, item.text ?? "");
|
|
221
|
+
break;
|
|
222
|
+
case "reasoning":
|
|
223
|
+
emitProgress(item.id, "reasoning", summarizeReasoning(item));
|
|
224
|
+
break;
|
|
225
|
+
case "todo_list":
|
|
226
|
+
emitProgress(item.id, "todo", summarizeTodoList(item));
|
|
227
|
+
break;
|
|
228
|
+
case "command_execution":
|
|
229
|
+
upsertTool(item, phase);
|
|
230
|
+
emitProgress(item.id, "tool", item.status === "in_progress" ? `Running ${item.command}` : summarizeCommandExecution(item));
|
|
231
|
+
break;
|
|
232
|
+
case "mcp_tool_call":
|
|
233
|
+
upsertTool(item, phase);
|
|
234
|
+
emitProgress(
|
|
235
|
+
item.id,
|
|
236
|
+
"tool",
|
|
237
|
+
item.status === "in_progress" ? `Calling ${item.server}:${item.tool}` : item.error?.message ?? `Completed ${item.server}:${item.tool}`,
|
|
238
|
+
);
|
|
239
|
+
break;
|
|
240
|
+
case "web_search":
|
|
241
|
+
upsertTool(item, phase);
|
|
242
|
+
emitProgress(item.id, "tool", `Searching web: ${item.query}`);
|
|
243
|
+
break;
|
|
244
|
+
case "file_change":
|
|
245
|
+
emitProgress(item.id, "activity", summarizeFileChange(item));
|
|
246
|
+
break;
|
|
247
|
+
case "error":
|
|
248
|
+
failureMessage = item.message;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
feedLine(line: string): boolean {
|
|
255
|
+
const trimmed = line.trim();
|
|
256
|
+
if (!trimmed) return false;
|
|
257
|
+
|
|
258
|
+
let event: CodexThreadEvent;
|
|
259
|
+
try {
|
|
260
|
+
event = JSON.parse(trimmed) as CodexThreadEvent;
|
|
261
|
+
} catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!event || typeof event !== "object" || typeof event.type !== "string") {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
sawEvent = true;
|
|
270
|
+
|
|
271
|
+
switch (event.type) {
|
|
272
|
+
case "item.started":
|
|
273
|
+
case "item.updated":
|
|
274
|
+
case "item.completed":
|
|
275
|
+
handleItem(event.type, event.item);
|
|
276
|
+
break;
|
|
277
|
+
case "turn.completed":
|
|
278
|
+
if (!finalAnswerObserved && finalResponse) {
|
|
279
|
+
finalAnswerObserved = true;
|
|
280
|
+
handlers.onFinalAnswerObserved?.(finalResponse);
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
case "turn.failed":
|
|
284
|
+
failureMessage = event.error?.message ?? "Turn failed";
|
|
285
|
+
break;
|
|
286
|
+
case "error":
|
|
287
|
+
failureMessage = event.message ?? "Stream error";
|
|
288
|
+
break;
|
|
289
|
+
default:
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return true;
|
|
294
|
+
},
|
|
295
|
+
getFinalResponse(): string {
|
|
296
|
+
return finalResponse;
|
|
297
|
+
},
|
|
298
|
+
getFailureMessage(): string | null {
|
|
299
|
+
return failureMessage;
|
|
300
|
+
},
|
|
301
|
+
hasStructuredEvents(): boolean {
|
|
302
|
+
return sawEvent;
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|