telecodex 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/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/bot/auth.js +64 -0
- package/dist/bot/commandSupport.js +239 -0
- package/dist/bot/createBot.js +51 -0
- package/dist/bot/handlerDeps.js +1 -0
- package/dist/bot/handlers/messageHandlers.js +71 -0
- package/dist/bot/handlers/operationalHandlers.js +131 -0
- package/dist/bot/handlers/projectHandlers.js +192 -0
- package/dist/bot/handlers/sessionConfigHandlers.js +319 -0
- package/dist/bot/inputService.js +372 -0
- package/dist/bot/registerHandlers.js +10 -0
- package/dist/bot/session.js +22 -0
- package/dist/bot/sessionFlow.js +51 -0
- package/dist/cli.js +14 -0
- package/dist/codex/sdkRuntime.js +165 -0
- package/dist/config.js +69 -0
- package/dist/runtime/appPaths.js +14 -0
- package/dist/runtime/bootstrap.js +213 -0
- package/dist/runtime/instanceLock.js +89 -0
- package/dist/runtime/logger.js +75 -0
- package/dist/runtime/secrets.js +45 -0
- package/dist/runtime/sessionRuntime.js +53 -0
- package/dist/runtime/startTelecodex.js +118 -0
- package/dist/store/db.js +267 -0
- package/dist/store/projects.js +47 -0
- package/dist/store/sessions.js +328 -0
- package/dist/telegram/attachments.js +67 -0
- package/dist/telegram/delivery.js +140 -0
- package/dist/telegram/messageBuffer.js +272 -0
- package/dist/telegram/renderer.js +146 -0
- package/dist/telegram/splitMessage.js +141 -0
- package/package.json +66 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { applySessionRuntimeEvent } from "../runtime/sessionRuntime.js";
|
|
2
|
+
import { sendPlainChunks } from "../telegram/delivery.js";
|
|
3
|
+
import { isAbortError } from "../codex/sdkRuntime.js";
|
|
4
|
+
import { numericChatId, numericMessageThreadId } from "./session.js";
|
|
5
|
+
import { describeBusyStatus, formatIsoTimestamp, isSessionBusy, sessionBufferKey, sessionLogFields, truncateSingleLine, } from "./sessionFlow.js";
|
|
6
|
+
export async function handleUserText(input) {
|
|
7
|
+
return handleUserInput({
|
|
8
|
+
prompt: input.text,
|
|
9
|
+
session: input.session,
|
|
10
|
+
store: input.store,
|
|
11
|
+
codex: input.codex,
|
|
12
|
+
buffers: input.buffers,
|
|
13
|
+
bot: input.bot,
|
|
14
|
+
...(input.logger ? { logger: input.logger } : {}),
|
|
15
|
+
...(input.enqueueIfBusy == null ? {} : { enqueueIfBusy: input.enqueueIfBusy }),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export async function handleUserInput(input) {
|
|
19
|
+
const { prompt, store, codex, buffers, bot, logger } = input;
|
|
20
|
+
const enqueueIfBusy = input.enqueueIfBusy ?? true;
|
|
21
|
+
const session = await refreshSessionIfActiveTurnIsStale(input.session, store, codex, bot, logger);
|
|
22
|
+
if (isSessionBusy(session) || codex.isRunning(session.sessionKey)) {
|
|
23
|
+
if (!enqueueIfBusy) {
|
|
24
|
+
return {
|
|
25
|
+
status: "busy",
|
|
26
|
+
consumed: false,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const queued = store.enqueueInput(session.sessionKey, prompt);
|
|
30
|
+
const queueDepth = store.getQueuedInputCount(session.sessionKey);
|
|
31
|
+
await sendPlainChunks(bot, {
|
|
32
|
+
chatId: numericChatId(session),
|
|
33
|
+
messageThreadId: numericMessageThreadId(session),
|
|
34
|
+
text: [
|
|
35
|
+
`The current Codex task is still ${describeBusyStatus(session.runtimeStatus)}. Your message was added to the queue.`,
|
|
36
|
+
`queue position: ${queueDepth}`,
|
|
37
|
+
`queued at: ${formatIsoTimestamp(queued.createdAt)}`,
|
|
38
|
+
"It will be processed automatically after the current run finishes.",
|
|
39
|
+
].join("\n"),
|
|
40
|
+
}, logger);
|
|
41
|
+
return {
|
|
42
|
+
status: "queued",
|
|
43
|
+
consumed: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
await applySessionRuntimeEvent({
|
|
47
|
+
bot,
|
|
48
|
+
store,
|
|
49
|
+
sessionKey: session.sessionKey,
|
|
50
|
+
event: {
|
|
51
|
+
type: "turn.preparing",
|
|
52
|
+
detail: "starting codex sdk run",
|
|
53
|
+
},
|
|
54
|
+
logger,
|
|
55
|
+
});
|
|
56
|
+
const bufferKey = sessionBufferKey(session.sessionKey);
|
|
57
|
+
let outputMessageId;
|
|
58
|
+
try {
|
|
59
|
+
outputMessageId = await buffers.create(bufferKey, {
|
|
60
|
+
chatId: numericChatId(session),
|
|
61
|
+
messageThreadId: numericMessageThreadId(session),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
store.setOutputMessage(session.sessionKey, null);
|
|
66
|
+
await applySessionRuntimeEvent({
|
|
67
|
+
bot,
|
|
68
|
+
store,
|
|
69
|
+
sessionKey: session.sessionKey,
|
|
70
|
+
event: {
|
|
71
|
+
type: "turn.failed",
|
|
72
|
+
message: error instanceof Error ? error.message : String(error),
|
|
73
|
+
},
|
|
74
|
+
logger,
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
status: "failed",
|
|
78
|
+
consumed: false,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
store.setOutputMessage(session.sessionKey, outputMessageId);
|
|
82
|
+
const turnId = createLocalTurnId();
|
|
83
|
+
await applySessionRuntimeEvent({
|
|
84
|
+
bot,
|
|
85
|
+
store,
|
|
86
|
+
sessionKey: session.sessionKey,
|
|
87
|
+
event: {
|
|
88
|
+
type: "turn.started",
|
|
89
|
+
turnId,
|
|
90
|
+
},
|
|
91
|
+
logger,
|
|
92
|
+
});
|
|
93
|
+
void runSessionPrompt({
|
|
94
|
+
sessionKey: session.sessionKey,
|
|
95
|
+
prompt,
|
|
96
|
+
store,
|
|
97
|
+
codex,
|
|
98
|
+
buffers,
|
|
99
|
+
bot,
|
|
100
|
+
turnId,
|
|
101
|
+
bufferKey,
|
|
102
|
+
...(logger ? { logger } : {}),
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
status: "started",
|
|
106
|
+
consumed: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export async function refreshSessionIfActiveTurnIsStale(session, store, codex, bot, logger) {
|
|
110
|
+
const latest = store.get(session.sessionKey) ?? session;
|
|
111
|
+
if (!isSessionBusy(latest))
|
|
112
|
+
return latest;
|
|
113
|
+
if (codex.isRunning(latest.sessionKey))
|
|
114
|
+
return latest;
|
|
115
|
+
store.setOutputMessage(latest.sessionKey, null);
|
|
116
|
+
await applySessionRuntimeEvent({
|
|
117
|
+
bot,
|
|
118
|
+
store,
|
|
119
|
+
sessionKey: latest.sessionKey,
|
|
120
|
+
event: {
|
|
121
|
+
type: "turn.failed",
|
|
122
|
+
turnId: latest.activeTurnId,
|
|
123
|
+
message: "The previous run was lost. Send the message again.",
|
|
124
|
+
},
|
|
125
|
+
logger,
|
|
126
|
+
});
|
|
127
|
+
logger?.warn("reset stale in-memory codex run state", {
|
|
128
|
+
...sessionLogFields(latest),
|
|
129
|
+
});
|
|
130
|
+
return store.get(latest.sessionKey) ?? latest;
|
|
131
|
+
}
|
|
132
|
+
export async function recoverActiveTopicSessions(store, codex, _buffers, bot, logger) {
|
|
133
|
+
const sessions = store.listTopicSessions().filter((session) => isSessionBusy(session));
|
|
134
|
+
if (sessions.length === 0)
|
|
135
|
+
return;
|
|
136
|
+
logger?.warn("recovering stale sdk-backed sessions after startup", {
|
|
137
|
+
activeSessions: sessions.length,
|
|
138
|
+
});
|
|
139
|
+
for (const session of sessions) {
|
|
140
|
+
const refreshed = await refreshSessionIfActiveTurnIsStale(session, store, codex, bot, logger);
|
|
141
|
+
await sendPlainChunks(bot, {
|
|
142
|
+
chatId: numericChatId(refreshed),
|
|
143
|
+
messageThreadId: numericMessageThreadId(refreshed),
|
|
144
|
+
text: "telecodex restarted and cannot resume the previous streamed run state. Send the message again if you want to continue.",
|
|
145
|
+
}, logger).catch((error) => {
|
|
146
|
+
logger?.warn("failed to notify session about stale sdk recovery", {
|
|
147
|
+
...sessionLogFields(refreshed),
|
|
148
|
+
error,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export async function processNextQueuedInputForSession(sessionKey, store, codex, buffers, bot, logger) {
|
|
154
|
+
const session = store.get(sessionKey);
|
|
155
|
+
if (!session || isSessionBusy(session) || codex.isRunning(sessionKey))
|
|
156
|
+
return;
|
|
157
|
+
const next = store.peekNextQueuedInput(sessionKey);
|
|
158
|
+
if (!next)
|
|
159
|
+
return;
|
|
160
|
+
try {
|
|
161
|
+
const result = await handleUserInput({
|
|
162
|
+
prompt: next.input,
|
|
163
|
+
session,
|
|
164
|
+
store,
|
|
165
|
+
codex,
|
|
166
|
+
buffers,
|
|
167
|
+
bot,
|
|
168
|
+
enqueueIfBusy: false,
|
|
169
|
+
...(logger ? { logger } : {}),
|
|
170
|
+
});
|
|
171
|
+
if (result.consumed) {
|
|
172
|
+
store.removeQueuedInput(next.id);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
logger?.warn("failed to process queued telegram input", {
|
|
177
|
+
sessionKey,
|
|
178
|
+
queuedInputId: next.id,
|
|
179
|
+
error,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async function runSessionPrompt(input) {
|
|
184
|
+
const { sessionKey, prompt, store, codex, buffers, bot, turnId, bufferKey, logger } = input;
|
|
185
|
+
const session = store.get(sessionKey);
|
|
186
|
+
if (!session)
|
|
187
|
+
return;
|
|
188
|
+
try {
|
|
189
|
+
const result = await codex.run({
|
|
190
|
+
profile: {
|
|
191
|
+
sessionKey,
|
|
192
|
+
threadId: session.codexThreadId,
|
|
193
|
+
cwd: session.cwd,
|
|
194
|
+
model: session.model,
|
|
195
|
+
sandboxMode: session.sandboxMode,
|
|
196
|
+
approvalPolicy: session.approvalPolicy,
|
|
197
|
+
reasoningEffort: session.reasoningEffort,
|
|
198
|
+
webSearchMode: session.webSearchMode,
|
|
199
|
+
networkAccessEnabled: session.networkAccessEnabled,
|
|
200
|
+
skipGitRepoCheck: session.skipGitRepoCheck,
|
|
201
|
+
additionalDirectories: session.additionalDirectories,
|
|
202
|
+
outputSchema: parseOutputSchema(session.outputSchema),
|
|
203
|
+
},
|
|
204
|
+
prompt: toSdkInput(prompt),
|
|
205
|
+
callbacks: {
|
|
206
|
+
onThreadStarted: async (threadId) => {
|
|
207
|
+
store.bindThread(sessionKey, threadId);
|
|
208
|
+
},
|
|
209
|
+
onEvent: async (event) => {
|
|
210
|
+
await projectEventToTelegramBuffer(buffers, bufferKey, event);
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
const latest = store.get(sessionKey);
|
|
215
|
+
if (latest) {
|
|
216
|
+
store.bindThread(sessionKey, result.threadId);
|
|
217
|
+
store.setOutputMessage(sessionKey, null);
|
|
218
|
+
await applySessionRuntimeEvent({
|
|
219
|
+
bot,
|
|
220
|
+
store,
|
|
221
|
+
sessionKey,
|
|
222
|
+
event: {
|
|
223
|
+
type: "turn.completed",
|
|
224
|
+
turnId,
|
|
225
|
+
},
|
|
226
|
+
logger,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
await buffers.complete(bufferKey, result.finalResponse || undefined);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
const latest = store.get(sessionKey);
|
|
233
|
+
if (latest) {
|
|
234
|
+
store.setOutputMessage(sessionKey, null);
|
|
235
|
+
await applySessionRuntimeEvent({
|
|
236
|
+
bot,
|
|
237
|
+
store,
|
|
238
|
+
sessionKey,
|
|
239
|
+
event: isAbortError(error)
|
|
240
|
+
? {
|
|
241
|
+
type: "turn.interrupted",
|
|
242
|
+
turnId,
|
|
243
|
+
}
|
|
244
|
+
: {
|
|
245
|
+
type: "turn.failed",
|
|
246
|
+
turnId,
|
|
247
|
+
message: error instanceof Error ? error.message : String(error),
|
|
248
|
+
},
|
|
249
|
+
logger,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
if (isAbortError(error)) {
|
|
253
|
+
await buffers.fail(bufferKey, "Current run interrupted.");
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
await buffers.fail(bufferKey, error instanceof Error ? error.message : String(error));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
await processNextQueuedInputForSession(sessionKey, store, codex, buffers, bot, logger);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async function projectEventToTelegramBuffer(buffers, key, event) {
|
|
264
|
+
switch (event.type) {
|
|
265
|
+
case "thread.started":
|
|
266
|
+
buffers.note(key, `thread started: ${event.thread_id}`);
|
|
267
|
+
return;
|
|
268
|
+
case "turn.started":
|
|
269
|
+
buffers.note(key, "started processing");
|
|
270
|
+
return;
|
|
271
|
+
case "turn.completed":
|
|
272
|
+
buffers.note(key, `token usage: in ${event.usage.input_tokens}, out ${event.usage.output_tokens}, cached ${event.usage.cached_input_tokens}`);
|
|
273
|
+
return;
|
|
274
|
+
case "item.started":
|
|
275
|
+
case "item.updated":
|
|
276
|
+
case "item.completed":
|
|
277
|
+
projectItem(buffers, key, event.item, event.type);
|
|
278
|
+
return;
|
|
279
|
+
case "turn.failed":
|
|
280
|
+
buffers.note(key, `run failed: ${event.error.message}`);
|
|
281
|
+
return;
|
|
282
|
+
case "error":
|
|
283
|
+
buffers.note(key, `error: ${event.message}`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function projectItem(buffers, key, item, phase) {
|
|
288
|
+
switch (item.type) {
|
|
289
|
+
case "agent_message":
|
|
290
|
+
buffers.setReplyDraft(key, item.text);
|
|
291
|
+
return;
|
|
292
|
+
case "reasoning":
|
|
293
|
+
buffers.setReasoningSummary(key, item.text);
|
|
294
|
+
return;
|
|
295
|
+
case "command_execution":
|
|
296
|
+
projectCommandExecution(buffers, key, item, phase);
|
|
297
|
+
return;
|
|
298
|
+
case "file_change":
|
|
299
|
+
projectFileChange(buffers, key, item, phase);
|
|
300
|
+
return;
|
|
301
|
+
case "mcp_tool_call":
|
|
302
|
+
projectMcpToolCall(buffers, key, item, phase);
|
|
303
|
+
return;
|
|
304
|
+
case "web_search":
|
|
305
|
+
projectWebSearch(buffers, key, item, phase);
|
|
306
|
+
return;
|
|
307
|
+
case "todo_list":
|
|
308
|
+
projectTodoList(buffers, key, item);
|
|
309
|
+
return;
|
|
310
|
+
case "error":
|
|
311
|
+
buffers.note(key, `error: ${truncateSingleLine(item.message, 120)}`);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function projectCommandExecution(buffers, key, item, phase) {
|
|
316
|
+
if (phase === "item.started") {
|
|
317
|
+
buffers.note(key, `command: ${truncateSingleLine(item.command, 120)}`);
|
|
318
|
+
}
|
|
319
|
+
else if (phase === "item.completed") {
|
|
320
|
+
const exitCode = item.exit_code == null ? "?" : String(item.exit_code);
|
|
321
|
+
const prefix = item.status === "failed" ? "command failed" : "command completed";
|
|
322
|
+
buffers.note(key, `${prefix}: ${truncateSingleLine(item.command, 96)} (exit ${exitCode})`);
|
|
323
|
+
}
|
|
324
|
+
if (item.aggregated_output.trim()) {
|
|
325
|
+
buffers.setToolOutput(key, item.aggregated_output);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function projectFileChange(buffers, key, item, phase) {
|
|
329
|
+
if (phase === "item.started") {
|
|
330
|
+
buffers.note(key, `file changes: ${item.changes.length} entries`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (phase === "item.completed") {
|
|
334
|
+
const prefix = item.status === "failed" ? "file changes failed" : "file changes completed";
|
|
335
|
+
buffers.note(key, `${prefix}: ${item.changes.length} entries`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function projectMcpToolCall(buffers, key, item, phase) {
|
|
339
|
+
if (phase === "item.started") {
|
|
340
|
+
buffers.note(key, `MCP: ${item.server}/${item.tool}`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (phase === "item.completed") {
|
|
344
|
+
buffers.note(key, item.error ? `MCP failed: ${item.server}/${item.tool}` : `MCP completed: ${item.server}/${item.tool}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function projectWebSearch(buffers, key, item, phase) {
|
|
348
|
+
if (phase === "item.completed") {
|
|
349
|
+
buffers.note(key, `web search completed: ${truncateSingleLine(item.query, 120)}`);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
buffers.note(key, `web search: ${truncateSingleLine(item.query, 120)}`);
|
|
353
|
+
}
|
|
354
|
+
function projectTodoList(buffers, key, item) {
|
|
355
|
+
const lines = item.items
|
|
356
|
+
.slice(0, 6)
|
|
357
|
+
.map((entry) => `${entry.completed ? "[done]" : "[todo]"} ${truncateSingleLine(entry.text, 96)}`);
|
|
358
|
+
if (lines.length > 0) {
|
|
359
|
+
buffers.setPlan(key, lines.join("\n"));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function createLocalTurnId() {
|
|
363
|
+
return `sdk-turn-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
364
|
+
}
|
|
365
|
+
function toSdkInput(input) {
|
|
366
|
+
return input;
|
|
367
|
+
}
|
|
368
|
+
function parseOutputSchema(value) {
|
|
369
|
+
if (!value)
|
|
370
|
+
return undefined;
|
|
371
|
+
return JSON.parse(value);
|
|
372
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { registerMessageHandlers } from "./handlers/messageHandlers.js";
|
|
2
|
+
import { registerOperationalHandlers } from "./handlers/operationalHandlers.js";
|
|
3
|
+
import { registerProjectHandlers } from "./handlers/projectHandlers.js";
|
|
4
|
+
import { registerSessionConfigHandlers } from "./handlers/sessionConfigHandlers.js";
|
|
5
|
+
export function registerHandlers(deps) {
|
|
6
|
+
registerOperationalHandlers(deps);
|
|
7
|
+
registerProjectHandlers(deps);
|
|
8
|
+
registerSessionConfigHandlers(deps);
|
|
9
|
+
registerMessageHandlers(deps);
|
|
10
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { makeSessionKey } from "../store/sessions.js";
|
|
2
|
+
export function sessionFromContext(ctx, store, config) {
|
|
3
|
+
const chatId = ctx.chat?.id;
|
|
4
|
+
if (chatId == null)
|
|
5
|
+
throw new Error("Missing Telegram chat id");
|
|
6
|
+
const messageThreadId = ctx.message?.message_thread_id ?? ctx.callbackQuery?.message?.message_thread_id ?? null;
|
|
7
|
+
const sessionKey = makeSessionKey(chatId, messageThreadId);
|
|
8
|
+
return store.getOrCreate({
|
|
9
|
+
sessionKey,
|
|
10
|
+
chatId: String(chatId),
|
|
11
|
+
messageThreadId: messageThreadId == null ? null : String(messageThreadId),
|
|
12
|
+
telegramTopicName: null,
|
|
13
|
+
defaultCwd: config.defaultCwd,
|
|
14
|
+
defaultModel: config.defaultModel,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function numericChatId(session) {
|
|
18
|
+
return Number(session.chatId);
|
|
19
|
+
}
|
|
20
|
+
export function numericMessageThreadId(session) {
|
|
21
|
+
return session.messageThreadId == null ? null : Number(session.messageThreadId);
|
|
22
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function sessionBufferKey(sessionKey) {
|
|
2
|
+
return `session:${sessionKey}`;
|
|
3
|
+
}
|
|
4
|
+
export function formatIsoTimestamp(value) {
|
|
5
|
+
const date = new Date(value);
|
|
6
|
+
if (Number.isNaN(date.getTime()))
|
|
7
|
+
return value;
|
|
8
|
+
return date.toLocaleString("en-GB", {
|
|
9
|
+
hour12: false,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export function truncateSingleLine(text, maxLength) {
|
|
13
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
14
|
+
if (normalized.length <= maxLength)
|
|
15
|
+
return normalized;
|
|
16
|
+
return `${normalized.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
17
|
+
}
|
|
18
|
+
export function sessionLogFields(session) {
|
|
19
|
+
return {
|
|
20
|
+
sessionKey: session.sessionKey,
|
|
21
|
+
chatId: session.chatId,
|
|
22
|
+
messageThreadId: session.messageThreadId,
|
|
23
|
+
cwd: session.cwd,
|
|
24
|
+
model: session.model,
|
|
25
|
+
sandboxMode: session.sandboxMode,
|
|
26
|
+
approvalPolicy: session.approvalPolicy,
|
|
27
|
+
reasoningEffort: session.reasoningEffort,
|
|
28
|
+
webSearchMode: session.webSearchMode,
|
|
29
|
+
networkAccessEnabled: session.networkAccessEnabled ? "true" : "false",
|
|
30
|
+
skipGitRepoCheck: session.skipGitRepoCheck ? "true" : "false",
|
|
31
|
+
runtimeStatus: session.runtimeStatus,
|
|
32
|
+
runtimeStatusDetail: session.runtimeStatusDetail,
|
|
33
|
+
codexThreadId: session.codexThreadId,
|
|
34
|
+
activeTurnId: session.activeTurnId,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function isSessionBusy(session) {
|
|
38
|
+
return session.runtimeStatus === "preparing" || session.runtimeStatus === "running" || session.activeTurnId != null;
|
|
39
|
+
}
|
|
40
|
+
export function describeBusyStatus(status) {
|
|
41
|
+
switch (status) {
|
|
42
|
+
case "preparing":
|
|
43
|
+
return "preparing";
|
|
44
|
+
case "running":
|
|
45
|
+
return "running";
|
|
46
|
+
case "failed":
|
|
47
|
+
return "recovering after a failed run";
|
|
48
|
+
default:
|
|
49
|
+
return "processing";
|
|
50
|
+
}
|
|
51
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { note } from "@clack/prompts";
|
|
3
|
+
import { startTelecodex } from "./runtime/startTelecodex.js";
|
|
4
|
+
function printHelp() {
|
|
5
|
+
note("Run `telecodex` with no environment variables. The first launch will guide setup.", "telecodex");
|
|
6
|
+
}
|
|
7
|
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
8
|
+
printHelp();
|
|
9
|
+
process.exit(0);
|
|
10
|
+
}
|
|
11
|
+
startTelecodex().catch((error) => {
|
|
12
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { Codex, } from "@openai/codex-sdk";
|
|
2
|
+
export class CodexSdkRuntime {
|
|
3
|
+
input;
|
|
4
|
+
codex;
|
|
5
|
+
activeRuns = new Map();
|
|
6
|
+
configOverrides;
|
|
7
|
+
injectedCodex;
|
|
8
|
+
constructor(input) {
|
|
9
|
+
this.input = input;
|
|
10
|
+
this.configOverrides = input.configOverrides;
|
|
11
|
+
this.injectedCodex = input.codex ?? null;
|
|
12
|
+
this.codex = input.codex ?? this.createCodex();
|
|
13
|
+
}
|
|
14
|
+
setConfigOverrides(configOverrides) {
|
|
15
|
+
this.configOverrides = configOverrides;
|
|
16
|
+
if (!this.injectedCodex) {
|
|
17
|
+
this.codex = this.createCodex();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
isRunning(sessionKey) {
|
|
21
|
+
return this.activeRuns.has(sessionKey);
|
|
22
|
+
}
|
|
23
|
+
getActiveRun(sessionKey) {
|
|
24
|
+
return this.activeRuns.get(sessionKey) ?? null;
|
|
25
|
+
}
|
|
26
|
+
interrupt(sessionKey) {
|
|
27
|
+
const run = this.activeRuns.get(sessionKey);
|
|
28
|
+
if (!run)
|
|
29
|
+
return false;
|
|
30
|
+
run.abortController.abort();
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
interruptAll() {
|
|
34
|
+
for (const run of this.activeRuns.values()) {
|
|
35
|
+
run.abortController.abort();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
createCodex() {
|
|
39
|
+
return new Codex({
|
|
40
|
+
codexPathOverride: this.input.codexBin,
|
|
41
|
+
...(this.configOverrides ? { config: this.configOverrides } : {}),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async run(input) {
|
|
45
|
+
if (this.activeRuns.has(input.profile.sessionKey)) {
|
|
46
|
+
throw new Error("Codex run already active for this session");
|
|
47
|
+
}
|
|
48
|
+
const threadOptions = toThreadOptions(input.profile);
|
|
49
|
+
const thread = input.profile.threadId
|
|
50
|
+
? this.codex.resumeThread(input.profile.threadId, threadOptions)
|
|
51
|
+
: this.codex.startThread(threadOptions);
|
|
52
|
+
const abortController = new AbortController();
|
|
53
|
+
const runPromise = this.consumeStream({
|
|
54
|
+
thread,
|
|
55
|
+
prompt: input.prompt,
|
|
56
|
+
signal: abortController.signal,
|
|
57
|
+
initialThreadId: input.profile.threadId,
|
|
58
|
+
sessionKey: input.profile.sessionKey,
|
|
59
|
+
outputSchema: input.profile.outputSchema,
|
|
60
|
+
...(input.callbacks ? { callbacks: input.callbacks } : {}),
|
|
61
|
+
});
|
|
62
|
+
const startedAt = new Date().toISOString();
|
|
63
|
+
this.activeRuns.set(input.profile.sessionKey, {
|
|
64
|
+
sessionKey: input.profile.sessionKey,
|
|
65
|
+
startedAt,
|
|
66
|
+
threadId: input.profile.threadId,
|
|
67
|
+
lastEventAt: startedAt,
|
|
68
|
+
lastEventType: null,
|
|
69
|
+
abortController,
|
|
70
|
+
promise: runPromise,
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
const result = await runPromise;
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
this.activeRuns.delete(input.profile.sessionKey);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async consumeStream(input) {
|
|
81
|
+
const streamed = await input.thread.runStreamed(input.prompt, {
|
|
82
|
+
signal: input.signal,
|
|
83
|
+
...(input.outputSchema === undefined ? {} : { outputSchema: input.outputSchema }),
|
|
84
|
+
});
|
|
85
|
+
const items = new Map();
|
|
86
|
+
let finalResponse = "";
|
|
87
|
+
let usage = null;
|
|
88
|
+
let threadId = input.initialThreadId;
|
|
89
|
+
for await (const event of streamed.events) {
|
|
90
|
+
const activeRun = this.activeRuns.get(input.sessionKey);
|
|
91
|
+
if (activeRun) {
|
|
92
|
+
activeRun.lastEventAt = new Date().toISOString();
|
|
93
|
+
activeRun.lastEventType = event.type;
|
|
94
|
+
}
|
|
95
|
+
if (event.type === "thread.started") {
|
|
96
|
+
threadId = event.thread_id;
|
|
97
|
+
const activeRun = this.activeRuns.get(input.sessionKey);
|
|
98
|
+
if (activeRun) {
|
|
99
|
+
activeRun.threadId = event.thread_id;
|
|
100
|
+
}
|
|
101
|
+
await input.callbacks?.onThreadStarted?.(event.thread_id);
|
|
102
|
+
}
|
|
103
|
+
else if (event.type === "item.started" ||
|
|
104
|
+
event.type === "item.updated" ||
|
|
105
|
+
event.type === "item.completed") {
|
|
106
|
+
items.set(event.item.id, event.item);
|
|
107
|
+
if (event.item.type === "agent_message") {
|
|
108
|
+
finalResponse = event.item.text;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (event.type === "turn.completed") {
|
|
112
|
+
usage = event.usage;
|
|
113
|
+
}
|
|
114
|
+
else if (event.type === "turn.failed") {
|
|
115
|
+
throw new Error(event.error.message);
|
|
116
|
+
}
|
|
117
|
+
else if (event.type === "error") {
|
|
118
|
+
throw new Error(event.message);
|
|
119
|
+
}
|
|
120
|
+
await input.callbacks?.onEvent?.(event);
|
|
121
|
+
}
|
|
122
|
+
if (!threadId) {
|
|
123
|
+
throw new Error("Codex SDK run finished without a thread id");
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
threadId,
|
|
127
|
+
items: [...items.values()],
|
|
128
|
+
finalResponse,
|
|
129
|
+
usage,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export function isAbortError(error) {
|
|
134
|
+
if (!(error instanceof Error))
|
|
135
|
+
return false;
|
|
136
|
+
const message = error.message.toLowerCase();
|
|
137
|
+
return error.name === "AbortError" || message.includes("aborted") || message.includes("aborterror");
|
|
138
|
+
}
|
|
139
|
+
function toThreadOptions(profile) {
|
|
140
|
+
const modelReasoningEffort = profile.reasoningEffort == null || profile.reasoningEffort === "none"
|
|
141
|
+
? undefined
|
|
142
|
+
: profile.reasoningEffort;
|
|
143
|
+
return {
|
|
144
|
+
model: profile.model,
|
|
145
|
+
sandboxMode: toSandboxMode(profile.sandboxMode),
|
|
146
|
+
workingDirectory: profile.cwd,
|
|
147
|
+
skipGitRepoCheck: profile.skipGitRepoCheck,
|
|
148
|
+
...(modelReasoningEffort ? { modelReasoningEffort } : {}),
|
|
149
|
+
networkAccessEnabled: profile.networkAccessEnabled,
|
|
150
|
+
...(profile.webSearchMode ? { webSearchMode: toWebSearchMode(profile.webSearchMode) } : {}),
|
|
151
|
+
...(profile.additionalDirectories.length > 0 ? { additionalDirectories: profile.additionalDirectories } : {}),
|
|
152
|
+
approvalPolicy: toApprovalMode(profile.approvalPolicy),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function toSandboxMode(value) {
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
function toApprovalMode(value) {
|
|
159
|
+
if (value === "on-failure")
|
|
160
|
+
return "on-failure";
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
function toWebSearchMode(value) {
|
|
164
|
+
return value;
|
|
165
|
+
}
|