lunel-cli 0.1.46 → 0.1.48
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/dist/ai/codex.d.ts +53 -0
- package/dist/ai/codex.js +938 -157
- package/package.json +1 -1
package/dist/ai/codex.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ export declare class CodexProvider implements AIProvider {
|
|
|
6
6
|
private nextId;
|
|
7
7
|
private pending;
|
|
8
8
|
private sessions;
|
|
9
|
+
private pendingPermissionRequestIds;
|
|
10
|
+
private assistantMessageIdByTurnId;
|
|
11
|
+
private partTextById;
|
|
9
12
|
init(): Promise<void>;
|
|
10
13
|
destroy(): Promise<void>;
|
|
11
14
|
subscribe(emitter: AiEventEmitter): () => void;
|
|
@@ -43,4 +46,54 @@ export declare class CodexProvider implements AIProvider {
|
|
|
43
46
|
private send;
|
|
44
47
|
private call;
|
|
45
48
|
private handleLine;
|
|
49
|
+
private handleServerRequest;
|
|
50
|
+
private handleNotification;
|
|
51
|
+
private emitMirroredUserMessage;
|
|
52
|
+
private handleItemStarted;
|
|
53
|
+
private handleStructuredItemCompleted;
|
|
54
|
+
private emitTextPart;
|
|
55
|
+
private emitStructuredToolPart;
|
|
56
|
+
private finishAssistantTurn;
|
|
57
|
+
private emitMessagePartEvent;
|
|
58
|
+
private ensureAssistantMessage;
|
|
59
|
+
private upsertLocalMessagePart;
|
|
60
|
+
private fetchServerThreads;
|
|
61
|
+
private parseThreadListEntry;
|
|
62
|
+
private hasNextCursor;
|
|
63
|
+
private ingestThreadMetadata;
|
|
64
|
+
private upsertSession;
|
|
65
|
+
private ensureLocalSession;
|
|
66
|
+
private resolveSessionFromPayload;
|
|
67
|
+
private resolveInFlightTurnId;
|
|
68
|
+
private decodeMessagesFromThreadRead;
|
|
69
|
+
private decodeItemText;
|
|
70
|
+
private decodeReasoningItemText;
|
|
71
|
+
private decodePlanItemText;
|
|
72
|
+
private decodeCommandExecutionItemText;
|
|
73
|
+
private decodeFileLikeItemText;
|
|
74
|
+
private normalizedCommandPhase;
|
|
75
|
+
private shortCommand;
|
|
76
|
+
private flattenTextValue;
|
|
77
|
+
private toSessionInfo;
|
|
78
|
+
private extractThreadObject;
|
|
79
|
+
private extractThreadTitleFromUnknown;
|
|
80
|
+
private extractThreadTitle;
|
|
81
|
+
private extractTurnId;
|
|
82
|
+
private extractItemId;
|
|
83
|
+
private extractTextPayload;
|
|
84
|
+
private extractThreadId;
|
|
85
|
+
private extractCreatedAt;
|
|
86
|
+
private extractUpdatedAt;
|
|
87
|
+
private readTimestamp;
|
|
88
|
+
private firstString;
|
|
89
|
+
private readArray;
|
|
90
|
+
private readString;
|
|
91
|
+
private asRecord;
|
|
92
|
+
private normalizedItemType;
|
|
93
|
+
private isUserRole;
|
|
94
|
+
private isAssistantMessageItem;
|
|
95
|
+
private extractIncomingItem;
|
|
96
|
+
private describeToolPart;
|
|
97
|
+
private extractToolInput;
|
|
98
|
+
private describeCompletedItemOutput;
|
|
46
99
|
}
|
package/dist/ai/codex.js
CHANGED
|
@@ -1,26 +1,10 @@
|
|
|
1
1
|
// Codex AI provider — spawns `codex app-server` and speaks JSON-RPC 2.0
|
|
2
|
-
// over stdin/stdout. Maps Codex's thread/turn model
|
|
3
|
-
//
|
|
2
|
+
// over stdin/stdout. Maps Codex's thread/turn model onto Lunel's AIProvider
|
|
3
|
+
// contract using the same thread/list + thread/read flow used by Remodex.
|
|
4
4
|
import * as crypto from "crypto";
|
|
5
5
|
import { spawn } from "child_process";
|
|
6
6
|
import { createInterface } from "readline";
|
|
7
|
-
|
|
8
|
-
// Codex notification method → mobile app event type mapping.
|
|
9
|
-
// Update this map as the Codex app-server protocol evolves.
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
const NOTIFICATION_TYPE_MAP = {
|
|
12
|
-
"turn/delta": "message.part.updated",
|
|
13
|
-
"turn/complete": "message.completed",
|
|
14
|
-
"turn/error": "message.error",
|
|
15
|
-
"tool/call": "tool.invoked",
|
|
16
|
-
"tool/result": "tool.result",
|
|
17
|
-
"session/permission": "session.permission.needed",
|
|
18
|
-
"thread/status/changed": "session.status.changed",
|
|
19
|
-
"thread/tokenUsage/updated": "session.token_usage.updated",
|
|
20
|
-
};
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// Provider implementation
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
7
|
+
const THREAD_LIST_SOURCE_KINDS = ["cli", "vscode", "appServer", "exec", "unknown"];
|
|
24
8
|
export class CodexProvider {
|
|
25
9
|
proc = null;
|
|
26
10
|
shuttingDown = false;
|
|
@@ -28,9 +12,9 @@ export class CodexProvider {
|
|
|
28
12
|
nextId = 1;
|
|
29
13
|
pending = new Map();
|
|
30
14
|
sessions = new Map();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
15
|
+
pendingPermissionRequestIds = new Map();
|
|
16
|
+
assistantMessageIdByTurnId = new Map();
|
|
17
|
+
partTextById = new Map();
|
|
34
18
|
async init() {
|
|
35
19
|
console.log("Starting Codex app-server...");
|
|
36
20
|
this.proc = spawn("codex", ["app-server"], {
|
|
@@ -50,7 +34,6 @@ export class CodexProvider {
|
|
|
50
34
|
this.emitter?.({ type: "sse_dead", properties: { error: msg } });
|
|
51
35
|
}
|
|
52
36
|
});
|
|
53
|
-
// Handshake: initialize the JSON-RPC session.
|
|
54
37
|
await this.call("initialize", { clientInfo: { name: "lunel", version: "1.0" } });
|
|
55
38
|
console.log("Codex ready.\n");
|
|
56
39
|
}
|
|
@@ -66,159 +49,137 @@ export class CodexProvider {
|
|
|
66
49
|
this.emitter = null;
|
|
67
50
|
};
|
|
68
51
|
}
|
|
69
|
-
// Codex doesn't need session reconnect validation, so setActiveSession is omitted.
|
|
70
|
-
// -------------------------------------------------------------------------
|
|
71
|
-
// Session management (emulated locally — Codex has no persistent store)
|
|
72
|
-
// -------------------------------------------------------------------------
|
|
73
52
|
async createSession(title) {
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
const result = await this.call("thread/start", { title });
|
|
80
|
-
if (result?.threadId) {
|
|
81
|
-
session.threadId = result.threadId;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
catch (err) {
|
|
85
|
-
console.warn("[codex] thread/start failed:", err.message);
|
|
53
|
+
const result = await this.call("thread/start", {});
|
|
54
|
+
const threadId = this.extractThreadId(result);
|
|
55
|
+
if (!threadId) {
|
|
56
|
+
throw new Error("thread/start response missing threadId");
|
|
86
57
|
}
|
|
87
|
-
|
|
58
|
+
const threadTitle = this.extractThreadTitleFromUnknown(result) ?? title ?? "Conversation";
|
|
59
|
+
const session = this.upsertSession({
|
|
60
|
+
id: threadId,
|
|
61
|
+
title: threadTitle,
|
|
62
|
+
createdAt: this.extractCreatedAt(result) ?? Date.now(),
|
|
63
|
+
updatedAt: this.extractUpdatedAt(result) ?? Date.now(),
|
|
64
|
+
});
|
|
65
|
+
return { session: this.toSessionInfo(session) };
|
|
88
66
|
}
|
|
89
67
|
async listSessions() {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
68
|
+
const remoteThreads = await this.fetchServerThreads();
|
|
69
|
+
for (const thread of remoteThreads) {
|
|
70
|
+
this.upsertSession(thread);
|
|
71
|
+
}
|
|
72
|
+
const sessions = Array.from(this.sessions.values())
|
|
73
|
+
.sort((a, b) => a.updatedAt - b.updatedAt)
|
|
74
|
+
.map((session) => this.toSessionInfo(session));
|
|
75
|
+
return { sessions };
|
|
97
76
|
}
|
|
98
77
|
async getSession(id) {
|
|
99
|
-
const
|
|
100
|
-
if (
|
|
78
|
+
const session = this.sessions.get(id);
|
|
79
|
+
if (session) {
|
|
80
|
+
return { session: this.toSessionInfo(session) };
|
|
81
|
+
}
|
|
82
|
+
await this.listSessions();
|
|
83
|
+
const next = this.sessions.get(id);
|
|
84
|
+
if (!next) {
|
|
101
85
|
throw Object.assign(new Error(`Session ${id} not found`), { code: "ENOENT" });
|
|
102
|
-
|
|
86
|
+
}
|
|
87
|
+
return { session: this.toSessionInfo(next) };
|
|
103
88
|
}
|
|
104
89
|
async deleteSession(id) {
|
|
105
90
|
this.sessions.delete(id);
|
|
106
91
|
return {};
|
|
107
92
|
}
|
|
108
|
-
// -------------------------------------------------------------------------
|
|
109
|
-
// Messages
|
|
110
|
-
// -------------------------------------------------------------------------
|
|
111
93
|
async getMessages(sessionId) {
|
|
112
|
-
const
|
|
113
|
-
|
|
94
|
+
const session = this.ensureLocalSession(sessionId);
|
|
95
|
+
const result = await this.call("thread/read", {
|
|
96
|
+
threadId: sessionId,
|
|
97
|
+
includeTurns: true,
|
|
98
|
+
});
|
|
99
|
+
const threadObject = this.extractThreadObject(result);
|
|
100
|
+
if (!threadObject) {
|
|
101
|
+
return { messages: session.messages };
|
|
102
|
+
}
|
|
103
|
+
const historyMessages = this.decodeMessagesFromThreadRead(sessionId, threadObject);
|
|
104
|
+
if (historyMessages.length > 0) {
|
|
105
|
+
session.messages = historyMessages;
|
|
106
|
+
session.updatedAt = this.extractUpdatedAt(threadObject) ?? session.updatedAt;
|
|
107
|
+
session.createdAt = this.extractCreatedAt(threadObject) ?? session.createdAt;
|
|
108
|
+
const title = this.extractThreadTitle(threadObject);
|
|
109
|
+
if (title)
|
|
110
|
+
session.title = title;
|
|
111
|
+
}
|
|
112
|
+
return { messages: session.messages };
|
|
114
113
|
}
|
|
115
|
-
// -------------------------------------------------------------------------
|
|
116
|
-
// Interaction
|
|
117
|
-
// -------------------------------------------------------------------------
|
|
118
114
|
async prompt(sessionId, text, model, agent) {
|
|
119
|
-
const session = this.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}).catch((err) => {
|
|
137
|
-
console.error("[codex] turn/start error:", err.message);
|
|
138
|
-
this.emitter?.({ type: "prompt_error", properties: { sessionId, error: err.message } });
|
|
139
|
-
});
|
|
115
|
+
const session = this.ensureLocalSession(sessionId);
|
|
116
|
+
session.updatedAt = Date.now();
|
|
117
|
+
(async () => {
|
|
118
|
+
try {
|
|
119
|
+
await this.call("turn/start", {
|
|
120
|
+
threadId: session.id,
|
|
121
|
+
input: [{ type: "text", text }],
|
|
122
|
+
...(model ? { model: `${model.providerID}/${model.modelID}` } : {}),
|
|
123
|
+
...(agent ? { agent } : {}),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
const message = err.message;
|
|
128
|
+
console.error("[codex] turn/start error:", message);
|
|
129
|
+
this.emitter?.({ type: "prompt_error", properties: { sessionId, error: message } });
|
|
130
|
+
}
|
|
131
|
+
})();
|
|
140
132
|
return { ack: true };
|
|
141
133
|
}
|
|
142
134
|
async abort(sessionId) {
|
|
143
|
-
const session = this.
|
|
144
|
-
|
|
135
|
+
const session = this.ensureLocalSession(sessionId);
|
|
136
|
+
const turnId = session.activeTurnId ?? await this.resolveInFlightTurnId(sessionId);
|
|
137
|
+
if (!turnId) {
|
|
138
|
+
throw new Error(`Session ${sessionId} has no active interruptible Codex turn`);
|
|
139
|
+
}
|
|
140
|
+
session.activeTurnId = turnId;
|
|
141
|
+
await this.call("turn/interrupt", { threadId: sessionId, turnId });
|
|
145
142
|
return {};
|
|
146
143
|
}
|
|
147
|
-
// -------------------------------------------------------------------------
|
|
148
|
-
// Metadata
|
|
149
|
-
// -------------------------------------------------------------------------
|
|
150
144
|
async agents() {
|
|
151
|
-
|
|
152
|
-
const result = await this.call("agent/list", {});
|
|
153
|
-
return { agents: result ?? [] };
|
|
154
|
-
}
|
|
155
|
-
catch {
|
|
156
|
-
// Codex may not support agents — return empty list gracefully.
|
|
157
|
-
return { agents: [] };
|
|
158
|
-
}
|
|
145
|
+
return { agents: [] };
|
|
159
146
|
}
|
|
160
147
|
async providers() {
|
|
161
|
-
|
|
162
|
-
const result = await this.call("provider/list", {});
|
|
163
|
-
return {
|
|
164
|
-
providers: result?.providers ?? [],
|
|
165
|
-
default: result?.default ?? {},
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
catch {
|
|
169
|
-
return { providers: [], default: {} };
|
|
170
|
-
}
|
|
148
|
+
return { providers: [], default: {} };
|
|
171
149
|
}
|
|
172
|
-
// -------------------------------------------------------------------------
|
|
173
|
-
// Auth
|
|
174
|
-
// -------------------------------------------------------------------------
|
|
175
150
|
async setAuth(providerId, key) {
|
|
176
|
-
|
|
177
|
-
return {};
|
|
151
|
+
throw new Error("Codex auth configuration is not supported by Lunel yet");
|
|
178
152
|
}
|
|
179
|
-
// -------------------------------------------------------------------------
|
|
180
|
-
// Session operations
|
|
181
|
-
// -------------------------------------------------------------------------
|
|
182
153
|
async command(sessionId, command, args) {
|
|
183
|
-
|
|
184
|
-
const result = await this.call("session/command", {
|
|
185
|
-
...(session?.threadId ? { threadId: session.threadId } : { sessionId }),
|
|
186
|
-
command,
|
|
187
|
-
arguments: args,
|
|
188
|
-
});
|
|
189
|
-
return { result: result ?? null };
|
|
154
|
+
throw new Error("Codex command execution is not supported by Lunel yet");
|
|
190
155
|
}
|
|
191
156
|
async revert(sessionId, messageId) {
|
|
192
|
-
|
|
193
|
-
await this.call("session/revert", {
|
|
194
|
-
...(session?.threadId ? { threadId: session.threadId } : { sessionId }),
|
|
195
|
-
messageId,
|
|
196
|
-
});
|
|
197
|
-
return {};
|
|
157
|
+
throw new Error("Codex revert is not supported by Lunel yet");
|
|
198
158
|
}
|
|
199
159
|
async unrevert(sessionId) {
|
|
200
|
-
|
|
201
|
-
await this.call("session/unrevert", {
|
|
202
|
-
...(session?.threadId ? { threadId: session.threadId } : { sessionId }),
|
|
203
|
-
});
|
|
204
|
-
return {};
|
|
160
|
+
throw new Error("Codex unrevert is not supported by Lunel yet");
|
|
205
161
|
}
|
|
206
162
|
async share(sessionId) {
|
|
207
|
-
// Codex has no share concept — return a null stub so the app degrades gracefully.
|
|
208
163
|
return { share: { url: null } };
|
|
209
164
|
}
|
|
210
165
|
async permissionReply(sessionId, permissionId, response) {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
166
|
+
const pending = this.pendingPermissionRequestIds.get(permissionId);
|
|
167
|
+
if (!pending) {
|
|
168
|
+
throw new Error(`Codex permission request ${permissionId} is no longer pending`);
|
|
169
|
+
}
|
|
170
|
+
const decision = response === "reject"
|
|
171
|
+
? "decline"
|
|
172
|
+
: response === "always"
|
|
173
|
+
? "acceptForSession"
|
|
174
|
+
: "accept";
|
|
175
|
+
this.send({ jsonrpc: "2.0", id: pending.requestId, result: decision });
|
|
176
|
+
this.pendingPermissionRequestIds.delete(permissionId);
|
|
177
|
+
this.emitter?.({
|
|
178
|
+
type: "permission.replied",
|
|
179
|
+
properties: { sessionID: sessionId, permissionId, response },
|
|
216
180
|
});
|
|
217
181
|
return {};
|
|
218
182
|
}
|
|
219
|
-
// -------------------------------------------------------------------------
|
|
220
|
-
// JSON-RPC internals
|
|
221
|
-
// -------------------------------------------------------------------------
|
|
222
183
|
send(req) {
|
|
223
184
|
if (!this.proc?.stdin?.writable)
|
|
224
185
|
return;
|
|
@@ -227,12 +188,12 @@ export class CodexProvider {
|
|
|
227
188
|
call(method, params) {
|
|
228
189
|
return new Promise((resolve, reject) => {
|
|
229
190
|
const id = this.nextId++;
|
|
230
|
-
|
|
191
|
+
const key = String(id);
|
|
192
|
+
this.pending.set(key, { resolve, reject });
|
|
231
193
|
this.send({ jsonrpc: "2.0", id, method, params });
|
|
232
|
-
// Safety timeout: if Codex never responds, don't hang the caller forever.
|
|
233
194
|
setTimeout(() => {
|
|
234
|
-
if (this.pending.has(
|
|
235
|
-
this.pending.delete(
|
|
195
|
+
if (this.pending.has(key)) {
|
|
196
|
+
this.pending.delete(key);
|
|
236
197
|
reject(new Error(`Codex RPC timeout: ${method} (id=${id})`));
|
|
237
198
|
}
|
|
238
199
|
}, 30_000);
|
|
@@ -244,28 +205,848 @@ export class CodexProvider {
|
|
|
244
205
|
msg = JSON.parse(line);
|
|
245
206
|
}
|
|
246
207
|
catch {
|
|
247
|
-
return;
|
|
208
|
+
return;
|
|
248
209
|
}
|
|
249
|
-
|
|
250
|
-
|
|
210
|
+
if ("method" in msg && typeof msg.method === "string") {
|
|
211
|
+
const inbound = msg;
|
|
212
|
+
const params = this.asRecord(inbound.params);
|
|
213
|
+
if (inbound.id != null) {
|
|
214
|
+
this.handleServerRequest(inbound.method, inbound.id, params);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
this.handleNotification(inbound.method, params);
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if ("id" in msg) {
|
|
251
222
|
const resp = msg;
|
|
252
|
-
const pending = this.pending.get(resp.id);
|
|
253
|
-
if (pending)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
223
|
+
const pending = this.pending.get(String(resp.id));
|
|
224
|
+
if (!pending)
|
|
225
|
+
return;
|
|
226
|
+
this.pending.delete(String(resp.id));
|
|
227
|
+
if (resp.error) {
|
|
228
|
+
pending.reject(new Error(resp.error.message));
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
pending.resolve(resp.result ?? null);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
handleServerRequest(method, requestId, params) {
|
|
236
|
+
const session = this.resolveSessionFromPayload(params);
|
|
237
|
+
if (method === "item/tool/requestUserInput") {
|
|
238
|
+
this.send({
|
|
239
|
+
jsonrpc: "2.0",
|
|
240
|
+
id: requestId,
|
|
241
|
+
error: { code: -32601, message: "Structured user input is not supported by Lunel yet" },
|
|
242
|
+
});
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (method === "item/commandExecution/requestApproval"
|
|
246
|
+
|| method === "item/fileChange/requestApproval"
|
|
247
|
+
|| method.endsWith("requestApproval")) {
|
|
248
|
+
const permissionId = String(requestId);
|
|
249
|
+
const callId = this.extractItemId(params) ?? undefined;
|
|
250
|
+
const messageId = session ? this.ensureAssistantMessage(session, this.extractTurnId(params) ?? `session:${session.id}`) : undefined;
|
|
251
|
+
this.pendingPermissionRequestIds.set(permissionId, {
|
|
252
|
+
sessionId: session?.id ?? "",
|
|
253
|
+
requestId,
|
|
254
|
+
messageId,
|
|
255
|
+
callId,
|
|
256
|
+
});
|
|
257
|
+
this.emitter?.({
|
|
258
|
+
type: "permission.updated",
|
|
259
|
+
properties: {
|
|
260
|
+
id: permissionId,
|
|
261
|
+
sessionID: session?.id,
|
|
262
|
+
messageID: messageId,
|
|
263
|
+
callID: callId,
|
|
264
|
+
type: method,
|
|
265
|
+
title: this.readString(params.reason) ?? this.readString(params.command) ?? method,
|
|
266
|
+
metadata: params,
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
this.send({
|
|
272
|
+
jsonrpc: "2.0",
|
|
273
|
+
id: requestId,
|
|
274
|
+
error: { code: -32601, message: `Unsupported request method: ${method}` },
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
handleNotification(method, params) {
|
|
278
|
+
this.ingestThreadMetadata(params);
|
|
279
|
+
const session = this.resolveSessionFromPayload(params);
|
|
280
|
+
switch (method) {
|
|
281
|
+
case "thread/started":
|
|
282
|
+
case "thread/name/updated":
|
|
283
|
+
if (session) {
|
|
284
|
+
const title = this.extractThreadTitle(params);
|
|
285
|
+
if (title)
|
|
286
|
+
session.title = title;
|
|
287
|
+
session.updatedAt = this.extractUpdatedAt(params) ?? Date.now();
|
|
288
|
+
this.emitter?.({ type: "session.updated", properties: { info: this.toSessionInfo(session) } });
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
case "thread/status/changed":
|
|
292
|
+
if (session) {
|
|
293
|
+
this.emitter?.({
|
|
294
|
+
type: "session.status",
|
|
295
|
+
properties: { sessionID: session.id, status: params.status ?? params },
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
case "turn/started":
|
|
300
|
+
if (session) {
|
|
301
|
+
const turnId = this.extractTurnId(params);
|
|
302
|
+
if (turnId)
|
|
303
|
+
session.activeTurnId = turnId;
|
|
304
|
+
session.updatedAt = Date.now();
|
|
305
|
+
this.emitter?.({
|
|
306
|
+
type: "session.status",
|
|
307
|
+
properties: { sessionID: session.id, status: { type: "running" } },
|
|
308
|
+
});
|
|
257
309
|
}
|
|
258
|
-
|
|
259
|
-
|
|
310
|
+
return;
|
|
311
|
+
case "turn/completed":
|
|
312
|
+
case "turn/failed":
|
|
313
|
+
if (session) {
|
|
314
|
+
session.activeTurnId = undefined;
|
|
315
|
+
session.updatedAt = Date.now();
|
|
316
|
+
this.finishAssistantTurn(session, params, method === "turn/failed");
|
|
317
|
+
this.emitter?.({ type: "session.idle", properties: { sessionID: session.id } });
|
|
318
|
+
if (method === "turn/failed") {
|
|
319
|
+
const error = this.readString(this.asRecord(params.error).message) ?? "Turn failed";
|
|
320
|
+
this.emitter?.({ type: "session.error", properties: { sessionID: session.id, error } });
|
|
321
|
+
}
|
|
260
322
|
}
|
|
323
|
+
return;
|
|
324
|
+
case "error":
|
|
325
|
+
case "codex/event/error":
|
|
326
|
+
if (session) {
|
|
327
|
+
const error = this.readString(this.asRecord(params.error).message)
|
|
328
|
+
?? this.readString(params.message)
|
|
329
|
+
?? "Codex error";
|
|
330
|
+
this.emitter?.({ type: "session.error", properties: { sessionID: session.id, error } });
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
case "codex/event/user_message":
|
|
334
|
+
this.emitMirroredUserMessage(session, params);
|
|
335
|
+
return;
|
|
336
|
+
case "item/started":
|
|
337
|
+
case "codex/event/item_started":
|
|
338
|
+
this.handleItemStarted(session, params);
|
|
339
|
+
return;
|
|
340
|
+
case "item/agentMessage/delta":
|
|
341
|
+
case "codex/event/agent_message_content_delta":
|
|
342
|
+
case "codex/event/agent_message_delta":
|
|
343
|
+
this.emitTextPart(session, params, "text", false);
|
|
344
|
+
return;
|
|
345
|
+
case "item/reasoning/summaryTextDelta":
|
|
346
|
+
case "item/reasoning/summaryPartAdded":
|
|
347
|
+
case "item/reasoning/textDelta":
|
|
348
|
+
this.emitTextPart(session, params, "reasoning", false);
|
|
349
|
+
return;
|
|
350
|
+
case "item/fileChange/outputDelta":
|
|
351
|
+
case "item/toolCall/outputDelta":
|
|
352
|
+
case "item/toolCall/output_delta":
|
|
353
|
+
case "item/tool_call/outputDelta":
|
|
354
|
+
case "item/tool_call/output_delta":
|
|
355
|
+
case "item/commandExecution/outputDelta":
|
|
356
|
+
case "item/command_execution/outputDelta":
|
|
357
|
+
case "codex/event/exec_command_output_delta":
|
|
358
|
+
case "codex/event/read":
|
|
359
|
+
case "codex/event/search":
|
|
360
|
+
case "codex/event/list_files":
|
|
361
|
+
case "turn/diff/updated":
|
|
362
|
+
case "codex/event/turn_diff_updated":
|
|
363
|
+
case "codex/event/turn_diff":
|
|
364
|
+
this.emitStructuredToolPart(session, method, params, false);
|
|
365
|
+
return;
|
|
366
|
+
case "item/completed":
|
|
367
|
+
case "codex/event/item_completed":
|
|
368
|
+
case "codex/event/agent_message":
|
|
369
|
+
case "codex/event/exec_command_end":
|
|
370
|
+
case "codex/event/patch_apply_end":
|
|
371
|
+
if (this.handleStructuredItemCompleted(session, params)) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
this.emitTextPart(session, params, "text", true);
|
|
375
|
+
return;
|
|
376
|
+
case "serverRequest/resolved": {
|
|
377
|
+
const permissionId = this.readString(params.requestId) ?? this.readString(params.requestID);
|
|
378
|
+
if (permissionId && this.pendingPermissionRequestIds.has(permissionId)) {
|
|
379
|
+
this.pendingPermissionRequestIds.delete(permissionId);
|
|
380
|
+
this.emitter?.({ type: "permission.replied", properties: { permissionId } });
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
default:
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
emitMirroredUserMessage(session, params) {
|
|
389
|
+
if (!session)
|
|
390
|
+
return;
|
|
391
|
+
const text = this.readString(params.message) ?? this.readString(params.text);
|
|
392
|
+
if (!text)
|
|
393
|
+
return;
|
|
394
|
+
const turnId = this.extractTurnId(params) ?? `mirrored:${crypto.randomUUID()}`;
|
|
395
|
+
const messageId = `user:${turnId}`;
|
|
396
|
+
if (!session.messages.find((message) => message.id === messageId)) {
|
|
397
|
+
session.messages.push({
|
|
398
|
+
id: messageId,
|
|
399
|
+
role: "user",
|
|
400
|
+
parts: [{ id: `${messageId}:text`, type: "text", text }],
|
|
401
|
+
time: Date.now(),
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
this.emitter?.({
|
|
405
|
+
type: "message.updated",
|
|
406
|
+
properties: { info: { sessionID: session.id, id: messageId, role: "user" } },
|
|
407
|
+
});
|
|
408
|
+
this.emitter?.({
|
|
409
|
+
type: "message.part.updated",
|
|
410
|
+
properties: {
|
|
411
|
+
part: { id: `${messageId}:text`, sessionID: session.id, messageID: messageId, type: "text", text },
|
|
412
|
+
message: { sessionID: session.id, id: messageId, role: "user" },
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
handleItemStarted(session, params) {
|
|
417
|
+
if (!session)
|
|
418
|
+
return;
|
|
419
|
+
const item = this.extractIncomingItem(params);
|
|
420
|
+
const itemType = this.normalizedItemType(this.readString(item.type) ?? "");
|
|
421
|
+
if (itemType === "reasoning") {
|
|
422
|
+
this.emitTextPart(session, params, "reasoning", false);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (itemType === "toolcall"
|
|
426
|
+
|| itemType === "commandexecution"
|
|
427
|
+
|| itemType === "filechange"
|
|
428
|
+
|| itemType === "diff"
|
|
429
|
+
|| itemType === "plan"
|
|
430
|
+
|| itemType === "contextcompaction"
|
|
431
|
+
|| itemType === "enteredreviewmode") {
|
|
432
|
+
this.emitStructuredToolPart(session, itemType, params, false);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (this.isAssistantMessageItem(itemType, this.readString(item.role))) {
|
|
436
|
+
this.ensureAssistantMessage(session, this.extractTurnId(params) ?? `session:${session.id}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
handleStructuredItemCompleted(session, params) {
|
|
440
|
+
if (!session)
|
|
441
|
+
return false;
|
|
442
|
+
const item = this.extractIncomingItem(params);
|
|
443
|
+
const itemType = this.normalizedItemType(this.readString(item.type) ?? "");
|
|
444
|
+
if (itemType === "toolcall"
|
|
445
|
+
|| itemType === "commandexecution"
|
|
446
|
+
|| itemType === "filechange"
|
|
447
|
+
|| itemType === "diff"
|
|
448
|
+
|| itemType === "plan"
|
|
449
|
+
|| itemType === "contextcompaction"
|
|
450
|
+
|| itemType === "enteredreviewmode") {
|
|
451
|
+
this.emitStructuredToolPart(session, itemType, params, true);
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
emitTextPart(session, params, partType, preferWholeText) {
|
|
457
|
+
if (!session)
|
|
458
|
+
return;
|
|
459
|
+
const turnId = this.extractTurnId(params) ?? session.activeTurnId ?? `session:${session.id}`;
|
|
460
|
+
const messageId = this.ensureAssistantMessage(session, turnId);
|
|
461
|
+
const partKey = `${messageId}:${partType}:${this.extractItemId(params) ?? "main"}`;
|
|
462
|
+
const nextChunk = this.extractTextPayload(params);
|
|
463
|
+
if (!nextChunk)
|
|
464
|
+
return;
|
|
465
|
+
const nextText = preferWholeText ? nextChunk : `${this.partTextById.get(partKey) ?? ""}${nextChunk}`;
|
|
466
|
+
this.partTextById.set(partKey, nextText);
|
|
467
|
+
session.updatedAt = Date.now();
|
|
468
|
+
this.upsertLocalMessagePart(session, messageId, {
|
|
469
|
+
id: partKey,
|
|
470
|
+
sessionID: session.id,
|
|
471
|
+
messageID: messageId,
|
|
472
|
+
type: partType,
|
|
473
|
+
text: nextText,
|
|
474
|
+
});
|
|
475
|
+
this.emitMessagePartEvent(session.id, messageId, "assistant", {
|
|
476
|
+
id: partKey,
|
|
477
|
+
sessionID: session.id,
|
|
478
|
+
messageID: messageId,
|
|
479
|
+
type: partType,
|
|
480
|
+
text: nextText,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
emitStructuredToolPart(session, method, params, completed) {
|
|
484
|
+
if (!session)
|
|
485
|
+
return;
|
|
486
|
+
const turnId = this.extractTurnId(params) ?? session.activeTurnId ?? `session:${session.id}`;
|
|
487
|
+
const messageId = this.ensureAssistantMessage(session, turnId);
|
|
488
|
+
const item = this.extractIncomingItem(params);
|
|
489
|
+
const normalizedType = this.normalizedItemType(this.readString(item.type) ?? method);
|
|
490
|
+
const itemId = this.extractItemId(params) ?? this.readString(item.id) ?? normalizedType ?? "tool";
|
|
491
|
+
const partId = `${messageId}:tool:${itemId}`;
|
|
492
|
+
const nextText = this.extractTextPayload(params);
|
|
493
|
+
const prevOutput = this.partTextById.get(partId) ?? "";
|
|
494
|
+
const output = completed
|
|
495
|
+
? nextText ?? prevOutput
|
|
496
|
+
: `${prevOutput}${nextText ?? ""}`;
|
|
497
|
+
if (output) {
|
|
498
|
+
this.partTextById.set(partId, output);
|
|
499
|
+
}
|
|
500
|
+
const state = completed ? "completed" : "running";
|
|
501
|
+
const name = this.describeToolPart(normalizedType, method, item);
|
|
502
|
+
const input = this.extractToolInput(item, params);
|
|
503
|
+
const outputValue = output || this.describeCompletedItemOutput(item, normalizedType) || undefined;
|
|
504
|
+
const part = {
|
|
505
|
+
id: partId,
|
|
506
|
+
sessionID: session.id,
|
|
507
|
+
messageID: messageId,
|
|
508
|
+
type: normalizedType === "plan" ? "reasoning" : "tool",
|
|
509
|
+
...(normalizedType === "plan"
|
|
510
|
+
? { text: outputValue ?? "Planning..." }
|
|
511
|
+
: { name, toolName: name, input, output: outputValue, state }),
|
|
512
|
+
};
|
|
513
|
+
this.upsertLocalMessagePart(session, messageId, part);
|
|
514
|
+
this.emitMessagePartEvent(session.id, messageId, "assistant", part);
|
|
515
|
+
}
|
|
516
|
+
finishAssistantTurn(session, params, failed) {
|
|
517
|
+
const turnId = this.extractTurnId(params);
|
|
518
|
+
if (!turnId)
|
|
519
|
+
return;
|
|
520
|
+
const messageId = this.assistantMessageIdByTurnId.get(turnId);
|
|
521
|
+
if (!messageId)
|
|
522
|
+
return;
|
|
523
|
+
const finishPartId = `${messageId}:finish`;
|
|
524
|
+
const part = {
|
|
525
|
+
id: finishPartId,
|
|
526
|
+
sessionID: session.id,
|
|
527
|
+
messageID: messageId,
|
|
528
|
+
type: "step-finish",
|
|
529
|
+
title: failed ? "Failed" : "Completed",
|
|
530
|
+
time: { end: Date.now() },
|
|
531
|
+
};
|
|
532
|
+
this.upsertLocalMessagePart(session, messageId, part);
|
|
533
|
+
this.emitMessagePartEvent(session.id, messageId, "assistant", part);
|
|
534
|
+
}
|
|
535
|
+
emitMessagePartEvent(sessionId, messageId, role, part) {
|
|
536
|
+
this.emitter?.({
|
|
537
|
+
type: "message.updated",
|
|
538
|
+
properties: { info: { sessionID: sessionId, id: messageId, role } },
|
|
539
|
+
});
|
|
540
|
+
this.emitter?.({
|
|
541
|
+
type: "message.part.updated",
|
|
542
|
+
properties: {
|
|
543
|
+
part,
|
|
544
|
+
message: { sessionID: sessionId, id: messageId, role },
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
ensureAssistantMessage(session, turnId) {
|
|
549
|
+
const existing = this.assistantMessageIdByTurnId.get(turnId);
|
|
550
|
+
if (existing)
|
|
551
|
+
return existing;
|
|
552
|
+
const messageId = crypto.randomUUID();
|
|
553
|
+
this.assistantMessageIdByTurnId.set(turnId, messageId);
|
|
554
|
+
session.messages.push({
|
|
555
|
+
id: messageId,
|
|
556
|
+
role: "assistant",
|
|
557
|
+
parts: [{
|
|
558
|
+
id: `${messageId}:start`,
|
|
559
|
+
type: "step-start",
|
|
560
|
+
title: "Working",
|
|
561
|
+
time: { start: Date.now() },
|
|
562
|
+
}],
|
|
563
|
+
time: Date.now(),
|
|
564
|
+
});
|
|
565
|
+
return messageId;
|
|
566
|
+
}
|
|
567
|
+
upsertLocalMessagePart(session, messageId, part) {
|
|
568
|
+
const message = session.messages.find((entry) => entry.id === messageId);
|
|
569
|
+
if (!message)
|
|
570
|
+
return;
|
|
571
|
+
const parts = Array.isArray(message.parts) ? [...message.parts] : [];
|
|
572
|
+
const idx = parts.findIndex((entry) => this.asRecord(entry).id === part.id);
|
|
573
|
+
if (idx >= 0) {
|
|
574
|
+
parts[idx] = part;
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
parts.push(part);
|
|
578
|
+
}
|
|
579
|
+
message.parts = parts;
|
|
580
|
+
message.time = Date.now();
|
|
581
|
+
}
|
|
582
|
+
async fetchServerThreads() {
|
|
583
|
+
const threads = [];
|
|
584
|
+
let nextCursor = null;
|
|
585
|
+
let hasRequestedFirstPage = false;
|
|
586
|
+
do {
|
|
587
|
+
const result = await this.call("thread/list", {
|
|
588
|
+
sourceKinds: THREAD_LIST_SOURCE_KINDS,
|
|
589
|
+
cursor: nextCursor,
|
|
590
|
+
});
|
|
591
|
+
const payload = this.asRecord(result);
|
|
592
|
+
const page = this.readArray(payload.data) ?? this.readArray(payload.items) ?? this.readArray(payload.threads) ?? [];
|
|
593
|
+
for (const entry of page) {
|
|
594
|
+
const parsed = this.parseThreadListEntry(entry);
|
|
595
|
+
if (parsed)
|
|
596
|
+
threads.push(parsed);
|
|
261
597
|
}
|
|
598
|
+
nextCursor = payload.nextCursor ?? payload.next_cursor ?? null;
|
|
599
|
+
hasRequestedFirstPage = true;
|
|
600
|
+
} while (hasRequestedFirstPage && this.hasNextCursor(nextCursor));
|
|
601
|
+
return threads;
|
|
602
|
+
}
|
|
603
|
+
parseThreadListEntry(value) {
|
|
604
|
+
const obj = this.asRecord(value);
|
|
605
|
+
const id = this.extractThreadId(obj);
|
|
606
|
+
if (!id)
|
|
607
|
+
return undefined;
|
|
608
|
+
return {
|
|
609
|
+
id,
|
|
610
|
+
title: this.extractThreadTitle(obj),
|
|
611
|
+
createdAt: this.extractCreatedAt(obj),
|
|
612
|
+
updatedAt: this.extractUpdatedAt(obj),
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
hasNextCursor(value) {
|
|
616
|
+
if (value == null)
|
|
617
|
+
return false;
|
|
618
|
+
if (typeof value === "string")
|
|
619
|
+
return value.trim().length > 0;
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
ingestThreadMetadata(payload) {
|
|
623
|
+
const threadId = this.extractThreadId(payload);
|
|
624
|
+
if (!threadId)
|
|
262
625
|
return;
|
|
626
|
+
const title = this.extractThreadTitleFromUnknown(payload) ?? "Conversation";
|
|
627
|
+
this.upsertSession({
|
|
628
|
+
id: threadId,
|
|
629
|
+
title,
|
|
630
|
+
createdAt: this.extractCreatedAt(payload) ?? Date.now(),
|
|
631
|
+
updatedAt: this.extractUpdatedAt(payload) ?? Date.now(),
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
upsertSession(input) {
|
|
635
|
+
const existing = this.sessions.get(input.id);
|
|
636
|
+
if (existing) {
|
|
637
|
+
existing.title = input.title ?? existing.title;
|
|
638
|
+
existing.createdAt = input.createdAt ?? existing.createdAt;
|
|
639
|
+
existing.updatedAt = input.updatedAt ?? existing.updatedAt;
|
|
640
|
+
return existing;
|
|
641
|
+
}
|
|
642
|
+
const session = {
|
|
643
|
+
id: input.id,
|
|
644
|
+
title: input.title ?? "Conversation",
|
|
645
|
+
createdAt: input.createdAt ?? Date.now(),
|
|
646
|
+
updatedAt: input.updatedAt ?? Date.now(),
|
|
647
|
+
messages: [],
|
|
648
|
+
};
|
|
649
|
+
this.sessions.set(input.id, session);
|
|
650
|
+
return session;
|
|
651
|
+
}
|
|
652
|
+
ensureLocalSession(sessionId) {
|
|
653
|
+
const existing = this.sessions.get(sessionId);
|
|
654
|
+
if (existing)
|
|
655
|
+
return existing;
|
|
656
|
+
return this.upsertSession({ id: sessionId, title: "Conversation", createdAt: Date.now(), updatedAt: Date.now() });
|
|
657
|
+
}
|
|
658
|
+
resolveSessionFromPayload(payload) {
|
|
659
|
+
const threadId = this.extractThreadId(payload);
|
|
660
|
+
return threadId ? this.ensureLocalSession(threadId) : undefined;
|
|
661
|
+
}
|
|
662
|
+
async resolveInFlightTurnId(threadId) {
|
|
663
|
+
const result = await this.call("thread/read", { threadId, includeTurns: true });
|
|
664
|
+
const thread = this.extractThreadObject(result);
|
|
665
|
+
const turns = this.readArray(thread.turns);
|
|
666
|
+
for (const turn of turns.slice().reverse()) {
|
|
667
|
+
const turnObj = this.asRecord(turn);
|
|
668
|
+
const status = this.normalizedItemType(this.readString(turnObj.status) ?? this.readString(this.asRecord(turnObj.status).type) ?? "");
|
|
669
|
+
if (status.includes("running")
|
|
670
|
+
|| status.includes("active")
|
|
671
|
+
|| status.includes("processing")
|
|
672
|
+
|| status.includes("started")
|
|
673
|
+
|| status.includes("pending")) {
|
|
674
|
+
return this.readString(turnObj.id);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return undefined;
|
|
678
|
+
}
|
|
679
|
+
decodeMessagesFromThreadRead(threadId, threadObject) {
|
|
680
|
+
const turns = this.readArray(threadObject.turns);
|
|
681
|
+
const messages = [];
|
|
682
|
+
let orderOffset = 0;
|
|
683
|
+
for (const turn of turns) {
|
|
684
|
+
const turnObject = this.asRecord(turn);
|
|
685
|
+
const turnId = this.readString(turnObject.id);
|
|
686
|
+
const turnTime = this.extractUpdatedAt(turnObject) ?? this.extractCreatedAt(turnObject) ?? Date.now();
|
|
687
|
+
const items = this.readArray(turnObject.items);
|
|
688
|
+
for (const item of items) {
|
|
689
|
+
const itemObject = this.asRecord(item);
|
|
690
|
+
const type = this.normalizedItemType(this.readString(itemObject.type) ?? "");
|
|
691
|
+
const itemId = this.readString(itemObject.id) ?? crypto.randomUUID();
|
|
692
|
+
const timestamp = this.extractUpdatedAt(itemObject) ?? this.extractCreatedAt(itemObject) ?? (turnTime + orderOffset++);
|
|
693
|
+
if (type === "usermessage") {
|
|
694
|
+
const text = this.decodeItemText(itemObject);
|
|
695
|
+
if (!text)
|
|
696
|
+
continue;
|
|
697
|
+
messages.push({
|
|
698
|
+
id: itemId,
|
|
699
|
+
role: "user",
|
|
700
|
+
parts: [{ id: `${itemId}:text`, type: "text", text, sessionID: threadId, messageID: itemId }],
|
|
701
|
+
time: timestamp,
|
|
702
|
+
});
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (type === "agentmessage" || type === "assistantmessage" || (type === "message" && !this.isUserRole(itemObject))) {
|
|
706
|
+
const text = this.decodeItemText(itemObject);
|
|
707
|
+
if (!text)
|
|
708
|
+
continue;
|
|
709
|
+
messages.push({
|
|
710
|
+
id: itemId,
|
|
711
|
+
role: "assistant",
|
|
712
|
+
parts: [{ id: `${itemId}:text`, type: "text", text, sessionID: threadId, messageID: itemId }],
|
|
713
|
+
time: timestamp,
|
|
714
|
+
});
|
|
715
|
+
if (turnId)
|
|
716
|
+
this.assistantMessageIdByTurnId.set(turnId, itemId);
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
if (type === "reasoning") {
|
|
720
|
+
const text = this.decodeReasoningItemText(itemObject);
|
|
721
|
+
messages.push({
|
|
722
|
+
id: itemId,
|
|
723
|
+
role: "assistant",
|
|
724
|
+
parts: [{ id: `${itemId}:reasoning`, type: "reasoning", text, sessionID: threadId, messageID: itemId }],
|
|
725
|
+
time: timestamp,
|
|
726
|
+
});
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
if (type === "plan") {
|
|
730
|
+
const text = this.decodePlanItemText(itemObject);
|
|
731
|
+
messages.push({
|
|
732
|
+
id: itemId,
|
|
733
|
+
role: "assistant",
|
|
734
|
+
parts: [{ id: `${itemId}:plan`, type: "reasoning", text, sessionID: threadId, messageID: itemId }],
|
|
735
|
+
time: timestamp,
|
|
736
|
+
});
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
if (type === "commandexecution" || type === "enteredreviewmode" || type === "contextcompaction") {
|
|
740
|
+
const output = this.decodeCommandExecutionItemText(itemObject, type);
|
|
741
|
+
messages.push({
|
|
742
|
+
id: itemId,
|
|
743
|
+
role: "assistant",
|
|
744
|
+
parts: [{
|
|
745
|
+
id: `${itemId}:tool`,
|
|
746
|
+
type: "tool",
|
|
747
|
+
name: type === "commandexecution" ? "command" : "system",
|
|
748
|
+
toolName: type === "commandexecution" ? "command" : "system",
|
|
749
|
+
output,
|
|
750
|
+
state: "completed",
|
|
751
|
+
sessionID: threadId,
|
|
752
|
+
messageID: itemId,
|
|
753
|
+
}],
|
|
754
|
+
time: timestamp,
|
|
755
|
+
});
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
if (type === "filechange" || type === "toolcall" || type === "diff") {
|
|
759
|
+
const output = this.decodeFileLikeItemText(itemObject);
|
|
760
|
+
if (!output)
|
|
761
|
+
continue;
|
|
762
|
+
messages.push({
|
|
763
|
+
id: itemId,
|
|
764
|
+
role: "assistant",
|
|
765
|
+
parts: [{
|
|
766
|
+
id: `${itemId}:tool`,
|
|
767
|
+
type: "tool",
|
|
768
|
+
name: type,
|
|
769
|
+
toolName: type,
|
|
770
|
+
output,
|
|
771
|
+
state: "completed",
|
|
772
|
+
sessionID: threadId,
|
|
773
|
+
messageID: itemId,
|
|
774
|
+
}],
|
|
775
|
+
time: timestamp,
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return messages;
|
|
781
|
+
}
|
|
782
|
+
decodeItemText(itemObject) {
|
|
783
|
+
const content = this.readArray(itemObject.content);
|
|
784
|
+
const parts = [];
|
|
785
|
+
for (const entry of content) {
|
|
786
|
+
const obj = this.asRecord(entry);
|
|
787
|
+
const type = this.normalizedItemType(this.readString(obj.type) ?? "");
|
|
788
|
+
if (type === "text" || type === "inputtext" || type === "outputtext" || type === "message") {
|
|
789
|
+
const text = this.readString(obj.text) ?? this.readString(this.asRecord(obj.data).text);
|
|
790
|
+
if (text)
|
|
791
|
+
parts.push(text);
|
|
792
|
+
}
|
|
793
|
+
else if (type === "skill") {
|
|
794
|
+
const skill = this.readString(obj.id) ?? this.readString(obj.name);
|
|
795
|
+
if (skill)
|
|
796
|
+
parts.push(`$${skill}`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
const joined = parts.join("\n").trim();
|
|
800
|
+
return joined || this.readString(itemObject.text) || this.readString(itemObject.message) || "";
|
|
801
|
+
}
|
|
802
|
+
decodeReasoningItemText(itemObject) {
|
|
803
|
+
const summary = this.flattenTextValue(itemObject.summary).trim();
|
|
804
|
+
const content = this.flattenTextValue(itemObject.content).trim();
|
|
805
|
+
return [summary, content].filter(Boolean).join("\n\n") || "Thinking...";
|
|
806
|
+
}
|
|
807
|
+
decodePlanItemText(itemObject) {
|
|
808
|
+
return this.decodeItemText(itemObject) || this.flattenTextValue(itemObject.summary).trim() || "Planning...";
|
|
809
|
+
}
|
|
810
|
+
decodeCommandExecutionItemText(itemObject, type) {
|
|
811
|
+
if (type === "enteredreviewmode") {
|
|
812
|
+
return `Reviewing ${this.readString(itemObject.review) ?? "changes"}...`;
|
|
813
|
+
}
|
|
814
|
+
if (type === "contextcompaction") {
|
|
815
|
+
return "Context compacted";
|
|
816
|
+
}
|
|
817
|
+
const status = this.readString(this.asRecord(itemObject.status).type) ?? this.readString(itemObject.status) ?? "completed";
|
|
818
|
+
const command = this.firstString(itemObject, ["command", "cmd", "raw_command", "rawCommand", "input", "invocation"]) ?? "command";
|
|
819
|
+
return `${this.normalizedCommandPhase(status)} ${this.shortCommand(command)}`;
|
|
820
|
+
}
|
|
821
|
+
decodeFileLikeItemText(itemObject) {
|
|
822
|
+
const direct = this.firstString(itemObject, [
|
|
823
|
+
"diff",
|
|
824
|
+
"unified_diff",
|
|
825
|
+
"unifiedDiff",
|
|
826
|
+
"patch",
|
|
827
|
+
"text",
|
|
828
|
+
"message",
|
|
829
|
+
"summary",
|
|
830
|
+
"stdout",
|
|
831
|
+
"stderr",
|
|
832
|
+
"output_text",
|
|
833
|
+
"outputText",
|
|
834
|
+
]);
|
|
835
|
+
if (direct?.trim())
|
|
836
|
+
return direct.trim();
|
|
837
|
+
const changes = this.readArray(itemObject.changes);
|
|
838
|
+
if (changes.length === 0)
|
|
839
|
+
return undefined;
|
|
840
|
+
return changes
|
|
841
|
+
.map((change) => {
|
|
842
|
+
const obj = this.asRecord(change);
|
|
843
|
+
const path = this.readString(obj.path) ?? this.readString(obj.filePath) ?? this.readString(obj.file_path) ?? "file";
|
|
844
|
+
const kind = this.readString(obj.kind) ?? "change";
|
|
845
|
+
const diff = this.readString(obj.diff) ?? this.readString(obj.patch) ?? "";
|
|
846
|
+
return diff ? `${kind}: ${path}\n${diff}` : `${kind}: ${path}`;
|
|
847
|
+
})
|
|
848
|
+
.join("\n\n");
|
|
849
|
+
}
|
|
850
|
+
normalizedCommandPhase(rawStatus) {
|
|
851
|
+
const normalized = rawStatus.trim().toLowerCase();
|
|
852
|
+
if (normalized.includes("fail") || normalized.includes("error"))
|
|
853
|
+
return "failed";
|
|
854
|
+
if (normalized.includes("cancel") || normalized.includes("abort") || normalized.includes("interrupt"))
|
|
855
|
+
return "stopped";
|
|
856
|
+
if (normalized.includes("complete") || normalized.includes("success") || normalized.includes("done"))
|
|
857
|
+
return "completed";
|
|
858
|
+
return "running";
|
|
859
|
+
}
|
|
860
|
+
shortCommand(rawCommand, maxLength = 92) {
|
|
861
|
+
const normalized = rawCommand.trim().replace(/\s+/g, " ");
|
|
862
|
+
if (!normalized)
|
|
863
|
+
return "command";
|
|
864
|
+
if (normalized.length <= maxLength)
|
|
865
|
+
return normalized;
|
|
866
|
+
return `${normalized.slice(0, maxLength - 1)}...`;
|
|
867
|
+
}
|
|
868
|
+
flattenTextValue(value) {
|
|
869
|
+
if (typeof value === "string")
|
|
870
|
+
return value;
|
|
871
|
+
if (Array.isArray(value)) {
|
|
872
|
+
return value.map((entry) => this.flattenTextValue(entry)).filter(Boolean).join("\n");
|
|
873
|
+
}
|
|
874
|
+
if (value && typeof value === "object") {
|
|
875
|
+
const obj = value;
|
|
876
|
+
return this.readString(obj.text) ?? this.readString(obj.message) ?? this.flattenTextValue(obj.content);
|
|
877
|
+
}
|
|
878
|
+
return "";
|
|
879
|
+
}
|
|
880
|
+
toSessionInfo(session) {
|
|
881
|
+
return {
|
|
882
|
+
id: session.id,
|
|
883
|
+
title: session.title,
|
|
884
|
+
time: {
|
|
885
|
+
created: session.createdAt,
|
|
886
|
+
updated: session.updatedAt,
|
|
887
|
+
},
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
extractThreadObject(payload) {
|
|
891
|
+
const obj = this.asRecord(payload);
|
|
892
|
+
const nested = this.asRecord(obj.thread);
|
|
893
|
+
return Object.keys(nested).length > 0 ? nested : obj;
|
|
894
|
+
}
|
|
895
|
+
extractThreadTitleFromUnknown(payload) {
|
|
896
|
+
return this.extractThreadTitle(this.extractThreadObject(payload));
|
|
897
|
+
}
|
|
898
|
+
extractThreadTitle(payload) {
|
|
899
|
+
const thread = this.asRecord(payload.thread);
|
|
900
|
+
return this.readString(thread.title)
|
|
901
|
+
?? this.readString(thread.name)
|
|
902
|
+
?? this.readString(payload.title)
|
|
903
|
+
?? this.readString(payload.name);
|
|
904
|
+
}
|
|
905
|
+
extractTurnId(payload) {
|
|
906
|
+
return (this.readString(payload.turnId)
|
|
907
|
+
?? this.readString(payload.turn_id)
|
|
908
|
+
?? this.readString(this.asRecord(payload.turn).id)
|
|
909
|
+
?? this.readString(this.asRecord(payload.turn).turnId)
|
|
910
|
+
?? this.readString(this.asRecord(payload.event).id)
|
|
911
|
+
?? this.readString(this.asRecord(this.asRecord(payload.event).turn).id)
|
|
912
|
+
?? ((payload.msg != null || payload.event != null) ? this.readString(payload.id) : undefined));
|
|
913
|
+
}
|
|
914
|
+
extractItemId(payload) {
|
|
915
|
+
return (this.readString(payload.itemId)
|
|
916
|
+
?? this.readString(payload.item_id)
|
|
917
|
+
?? this.readString(payload.messageId)
|
|
918
|
+
?? this.readString(payload.message_id)
|
|
919
|
+
?? this.readString(this.asRecord(payload.item).id)
|
|
920
|
+
?? this.readString(this.asRecord(payload.item).itemId)
|
|
921
|
+
?? this.readString(this.asRecord(payload.item).messageId)
|
|
922
|
+
?? this.readString(this.asRecord(payload.event).id)
|
|
923
|
+
?? this.readString(this.asRecord(this.asRecord(payload.event).item).id));
|
|
924
|
+
}
|
|
925
|
+
extractTextPayload(payload) {
|
|
926
|
+
return (this.readString(payload.delta)
|
|
927
|
+
?? this.readString(payload.text)
|
|
928
|
+
?? this.readString(payload.message)
|
|
929
|
+
?? this.readString(this.asRecord(payload.item).text)
|
|
930
|
+
?? this.readString(this.asRecord(payload.item).delta)
|
|
931
|
+
?? this.readString(this.asRecord(payload.item).message)
|
|
932
|
+
?? this.readString(this.asRecord(payload.event).text)
|
|
933
|
+
?? this.readString(this.asRecord(payload.event).delta)
|
|
934
|
+
?? this.readString(this.asRecord(payload.event).message));
|
|
935
|
+
}
|
|
936
|
+
extractThreadId(payload) {
|
|
937
|
+
if (!payload || typeof payload !== "object")
|
|
938
|
+
return undefined;
|
|
939
|
+
const obj = payload;
|
|
940
|
+
const direct = this.readString(obj.threadId) ?? this.readString(obj.thread_id);
|
|
941
|
+
if (direct)
|
|
942
|
+
return direct;
|
|
943
|
+
const thread = this.asRecord(obj.thread);
|
|
944
|
+
return this.readString(thread.id) ?? this.readString(thread.threadId) ?? this.readString(thread.thread_id);
|
|
945
|
+
}
|
|
946
|
+
extractCreatedAt(payload) {
|
|
947
|
+
const obj = this.extractThreadObject(payload);
|
|
948
|
+
return this.readTimestamp(obj.createdAt) ?? this.readTimestamp(obj.created_at);
|
|
949
|
+
}
|
|
950
|
+
extractUpdatedAt(payload) {
|
|
951
|
+
const obj = this.extractThreadObject(payload);
|
|
952
|
+
return this.readTimestamp(obj.updatedAt)
|
|
953
|
+
?? this.readTimestamp(obj.updated_at)
|
|
954
|
+
?? this.readTimestamp(obj.time);
|
|
955
|
+
}
|
|
956
|
+
readTimestamp(value) {
|
|
957
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
958
|
+
return value > 10_000_000_000 ? value : value * 1000;
|
|
959
|
+
}
|
|
960
|
+
if (typeof value === "string" && value.trim()) {
|
|
961
|
+
const asNumber = Number(value);
|
|
962
|
+
if (Number.isFinite(asNumber)) {
|
|
963
|
+
return asNumber > 10_000_000_000 ? asNumber : asNumber * 1000;
|
|
964
|
+
}
|
|
965
|
+
const parsed = Date.parse(value);
|
|
966
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
967
|
+
}
|
|
968
|
+
if (value && typeof value === "object") {
|
|
969
|
+
const obj = value;
|
|
970
|
+
return this.readTimestamp(obj.updated) ?? this.readTimestamp(obj.created) ?? this.readTimestamp(obj.start) ?? this.readTimestamp(obj.end);
|
|
971
|
+
}
|
|
972
|
+
return undefined;
|
|
973
|
+
}
|
|
974
|
+
firstString(obj, keys) {
|
|
975
|
+
for (const key of keys) {
|
|
976
|
+
const value = this.readString(obj[key]);
|
|
977
|
+
if (value)
|
|
978
|
+
return value;
|
|
979
|
+
}
|
|
980
|
+
return undefined;
|
|
981
|
+
}
|
|
982
|
+
readArray(value) {
|
|
983
|
+
return Array.isArray(value) ? value : [];
|
|
984
|
+
}
|
|
985
|
+
readString(value) {
|
|
986
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
987
|
+
}
|
|
988
|
+
asRecord(value) {
|
|
989
|
+
return value && typeof value === "object" ? value : {};
|
|
990
|
+
}
|
|
991
|
+
normalizedItemType(rawType) {
|
|
992
|
+
return rawType.replace(/[_-]/g, "").toLowerCase();
|
|
993
|
+
}
|
|
994
|
+
isUserRole(itemObject) {
|
|
995
|
+
return (this.readString(itemObject.role) ?? "").toLowerCase().includes("user");
|
|
996
|
+
}
|
|
997
|
+
isAssistantMessageItem(itemType, role) {
|
|
998
|
+
const normalizedRole = (role ?? "").toLowerCase();
|
|
999
|
+
return itemType === "agentmessage"
|
|
1000
|
+
|| itemType === "assistantmessage"
|
|
1001
|
+
|| itemType === "exitedreviewmode"
|
|
1002
|
+
|| (itemType === "message" && !normalizedRole.includes("user"));
|
|
1003
|
+
}
|
|
1004
|
+
extractIncomingItem(params) {
|
|
1005
|
+
const direct = this.asRecord(params.item);
|
|
1006
|
+
if (Object.keys(direct).length > 0)
|
|
1007
|
+
return direct;
|
|
1008
|
+
const eventItem = this.asRecord(this.asRecord(params.event).item);
|
|
1009
|
+
if (Object.keys(eventItem).length > 0)
|
|
1010
|
+
return eventItem;
|
|
1011
|
+
return this.asRecord(params.event);
|
|
1012
|
+
}
|
|
1013
|
+
describeToolPart(normalizedType, method, item) {
|
|
1014
|
+
if (normalizedType === "commandexecution") {
|
|
1015
|
+
return "command";
|
|
1016
|
+
}
|
|
1017
|
+
if (normalizedType === "filechange" || normalizedType === "diff") {
|
|
1018
|
+
return "file-change";
|
|
1019
|
+
}
|
|
1020
|
+
if (normalizedType === "plan") {
|
|
1021
|
+
return "plan";
|
|
1022
|
+
}
|
|
1023
|
+
if (normalizedType === "enteredreviewmode") {
|
|
1024
|
+
return "review";
|
|
1025
|
+
}
|
|
1026
|
+
if (normalizedType === "contextcompaction") {
|
|
1027
|
+
return "context";
|
|
1028
|
+
}
|
|
1029
|
+
return this.readString(item.name) ?? method.replace(/^.*\//, "");
|
|
1030
|
+
}
|
|
1031
|
+
extractToolInput(item, params) {
|
|
1032
|
+
return item.input ?? item.command ?? item.path ?? item.args ?? params.command ?? params.path ?? undefined;
|
|
1033
|
+
}
|
|
1034
|
+
describeCompletedItemOutput(item, normalizedType) {
|
|
1035
|
+
if (normalizedType === "commandexecution") {
|
|
1036
|
+
return this.firstString(item, ["stdout", "stderr", "text", "message", "summary"]);
|
|
1037
|
+
}
|
|
1038
|
+
if (normalizedType === "plan") {
|
|
1039
|
+
return this.decodePlanItemText(item);
|
|
1040
|
+
}
|
|
1041
|
+
if (normalizedType === "filechange" || normalizedType === "toolcall" || normalizedType === "diff") {
|
|
1042
|
+
return this.decodeFileLikeItemText(item);
|
|
1043
|
+
}
|
|
1044
|
+
if (normalizedType === "enteredreviewmode") {
|
|
1045
|
+
return `Reviewing ${this.readString(item.review) ?? "changes"}...`;
|
|
1046
|
+
}
|
|
1047
|
+
if (normalizedType === "contextcompaction") {
|
|
1048
|
+
return "Context compacted";
|
|
263
1049
|
}
|
|
264
|
-
|
|
265
|
-
const notif = msg;
|
|
266
|
-
const params = (notif.params ?? {});
|
|
267
|
-
const mappedType = NOTIFICATION_TYPE_MAP[notif.method] ?? notif.method;
|
|
268
|
-
console.log("[codex]", notif.method);
|
|
269
|
-
this.emitter?.({ type: mappedType, properties: params });
|
|
1050
|
+
return this.firstString(item, ["text", "message", "summary", "output", "output_text", "outputText"]);
|
|
270
1051
|
}
|
|
271
1052
|
}
|