lunel-cli 0.1.47 → 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 +50 -2
- package/dist/ai/codex.js +901 -201
- package/package.json +1 -1
package/dist/ai/codex.d.ts
CHANGED
|
@@ -6,7 +6,9 @@ export declare class CodexProvider implements AIProvider {
|
|
|
6
6
|
private nextId;
|
|
7
7
|
private pending;
|
|
8
8
|
private sessions;
|
|
9
|
-
private
|
|
9
|
+
private pendingPermissionRequestIds;
|
|
10
|
+
private assistantMessageIdByTurnId;
|
|
11
|
+
private partTextById;
|
|
10
12
|
init(): Promise<void>;
|
|
11
13
|
destroy(): Promise<void>;
|
|
12
14
|
subscribe(emitter: AiEventEmitter): () => void;
|
|
@@ -44,8 +46,54 @@ export declare class CodexProvider implements AIProvider {
|
|
|
44
46
|
private send;
|
|
45
47
|
private call;
|
|
46
48
|
private handleLine;
|
|
47
|
-
private
|
|
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;
|
|
48
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;
|
|
49
84
|
private extractThreadId;
|
|
85
|
+
private extractCreatedAt;
|
|
86
|
+
private extractUpdatedAt;
|
|
87
|
+
private readTimestamp;
|
|
88
|
+
private firstString;
|
|
89
|
+
private readArray;
|
|
50
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;
|
|
51
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,10 +12,9 @@ export class CodexProvider {
|
|
|
28
12
|
nextId = 1;
|
|
29
13
|
pending = new Map();
|
|
30
14
|
sessions = new Map();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// -------------------------------------------------------------------------
|
|
15
|
+
pendingPermissionRequestIds = new Map();
|
|
16
|
+
assistantMessageIdByTurnId = new Map();
|
|
17
|
+
partTextById = new Map();
|
|
35
18
|
async init() {
|
|
36
19
|
console.log("Starting Codex app-server...");
|
|
37
20
|
this.proc = spawn("codex", ["app-server"], {
|
|
@@ -51,7 +34,6 @@ export class CodexProvider {
|
|
|
51
34
|
this.emitter?.({ type: "sse_dead", properties: { error: msg } });
|
|
52
35
|
}
|
|
53
36
|
});
|
|
54
|
-
// Handshake: initialize the JSON-RPC session.
|
|
55
37
|
await this.call("initialize", { clientInfo: { name: "lunel", version: "1.0" } });
|
|
56
38
|
console.log("Codex ready.\n");
|
|
57
39
|
}
|
|
@@ -67,77 +49,76 @@ export class CodexProvider {
|
|
|
67
49
|
this.emitter = null;
|
|
68
50
|
};
|
|
69
51
|
}
|
|
70
|
-
// Codex doesn't need session reconnect validation, so setActiveSession is omitted.
|
|
71
|
-
// -------------------------------------------------------------------------
|
|
72
|
-
// Session management (emulated locally — Codex has no persistent store)
|
|
73
|
-
// -------------------------------------------------------------------------
|
|
74
52
|
async createSession(title) {
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
await this.ensureThread(session);
|
|
81
|
-
}
|
|
82
|
-
catch (err) {
|
|
83
|
-
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");
|
|
84
57
|
}
|
|
85
|
-
|
|
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) };
|
|
86
66
|
}
|
|
87
67
|
async listSessions() {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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 };
|
|
95
76
|
}
|
|
96
77
|
async getSession(id) {
|
|
97
|
-
const
|
|
98
|
-
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) {
|
|
99
85
|
throw Object.assign(new Error(`Session ${id} not found`), { code: "ENOENT" });
|
|
100
|
-
|
|
86
|
+
}
|
|
87
|
+
return { session: this.toSessionInfo(next) };
|
|
101
88
|
}
|
|
102
89
|
async deleteSession(id) {
|
|
103
|
-
const session = this.sessions.get(id);
|
|
104
|
-
if (session?.threadId) {
|
|
105
|
-
this.sessionIdByThreadId.delete(session.threadId);
|
|
106
|
-
}
|
|
107
90
|
this.sessions.delete(id);
|
|
108
91
|
return {};
|
|
109
92
|
}
|
|
110
|
-
// -------------------------------------------------------------------------
|
|
111
|
-
// Messages
|
|
112
|
-
// -------------------------------------------------------------------------
|
|
113
93
|
async getMessages(sessionId) {
|
|
114
|
-
const
|
|
115
|
-
|
|
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 };
|
|
116
113
|
}
|
|
117
|
-
// -------------------------------------------------------------------------
|
|
118
|
-
// Interaction
|
|
119
|
-
// -------------------------------------------------------------------------
|
|
120
114
|
async prompt(sessionId, text, model, agent) {
|
|
121
|
-
const session = this.
|
|
122
|
-
|
|
123
|
-
if (session) {
|
|
124
|
-
session.messages.push({
|
|
125
|
-
id: crypto.randomUUID(),
|
|
126
|
-
role: "user",
|
|
127
|
-
parts: [{ type: "text", text }],
|
|
128
|
-
time: Date.now(),
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
// Fire-and-forget — the response streams back as JSON-RPC notifications.
|
|
115
|
+
const session = this.ensureLocalSession(sessionId);
|
|
116
|
+
session.updatedAt = Date.now();
|
|
132
117
|
(async () => {
|
|
133
118
|
try {
|
|
134
|
-
if (!session) {
|
|
135
|
-
throw new Error(`Session ${sessionId} not found`);
|
|
136
|
-
}
|
|
137
|
-
const threadId = await this.ensureThread(session);
|
|
138
119
|
await this.call("turn/start", {
|
|
139
|
-
threadId,
|
|
140
|
-
input: [{ type: "
|
|
120
|
+
threadId: session.id,
|
|
121
|
+
input: [{ type: "text", text }],
|
|
141
122
|
...(model ? { model: `${model.providerID}/${model.modelID}` } : {}),
|
|
142
123
|
...(agent ? { agent } : {}),
|
|
143
124
|
});
|
|
@@ -151,100 +132,54 @@ export class CodexProvider {
|
|
|
151
132
|
return { ack: true };
|
|
152
133
|
}
|
|
153
134
|
async abort(sessionId) {
|
|
154
|
-
const session = this.
|
|
155
|
-
|
|
156
|
-
|
|
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`);
|
|
157
139
|
}
|
|
158
|
-
|
|
140
|
+
session.activeTurnId = turnId;
|
|
141
|
+
await this.call("turn/interrupt", { threadId: sessionId, turnId });
|
|
159
142
|
return {};
|
|
160
143
|
}
|
|
161
|
-
// -------------------------------------------------------------------------
|
|
162
|
-
// Metadata
|
|
163
|
-
// -------------------------------------------------------------------------
|
|
164
144
|
async agents() {
|
|
165
|
-
|
|
166
|
-
const result = await this.call("agent/list", {});
|
|
167
|
-
return { agents: result ?? [] };
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
// Codex may not support agents — return empty list gracefully.
|
|
171
|
-
return { agents: [] };
|
|
172
|
-
}
|
|
145
|
+
return { agents: [] };
|
|
173
146
|
}
|
|
174
147
|
async providers() {
|
|
175
|
-
|
|
176
|
-
const result = await this.call("provider/list", {});
|
|
177
|
-
return {
|
|
178
|
-
providers: result?.providers ?? [],
|
|
179
|
-
default: result?.default ?? {},
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
return { providers: [], default: {} };
|
|
184
|
-
}
|
|
148
|
+
return { providers: [], default: {} };
|
|
185
149
|
}
|
|
186
|
-
// -------------------------------------------------------------------------
|
|
187
|
-
// Auth
|
|
188
|
-
// -------------------------------------------------------------------------
|
|
189
150
|
async setAuth(providerId, key) {
|
|
190
|
-
|
|
191
|
-
return {};
|
|
151
|
+
throw new Error("Codex auth configuration is not supported by Lunel yet");
|
|
192
152
|
}
|
|
193
|
-
// -------------------------------------------------------------------------
|
|
194
|
-
// Session operations
|
|
195
|
-
// -------------------------------------------------------------------------
|
|
196
153
|
async command(sessionId, command, args) {
|
|
197
|
-
|
|
198
|
-
if (!session?.threadId) {
|
|
199
|
-
throw new Error(`Session ${sessionId} has no active Codex thread`);
|
|
200
|
-
}
|
|
201
|
-
const result = await this.call("session/command", {
|
|
202
|
-
threadId: session.threadId,
|
|
203
|
-
command,
|
|
204
|
-
arguments: args,
|
|
205
|
-
});
|
|
206
|
-
return { result: result ?? null };
|
|
154
|
+
throw new Error("Codex command execution is not supported by Lunel yet");
|
|
207
155
|
}
|
|
208
156
|
async revert(sessionId, messageId) {
|
|
209
|
-
|
|
210
|
-
if (!session?.threadId) {
|
|
211
|
-
throw new Error(`Session ${sessionId} has no active Codex thread`);
|
|
212
|
-
}
|
|
213
|
-
await this.call("session/revert", {
|
|
214
|
-
threadId: session.threadId,
|
|
215
|
-
messageId,
|
|
216
|
-
});
|
|
217
|
-
return {};
|
|
157
|
+
throw new Error("Codex revert is not supported by Lunel yet");
|
|
218
158
|
}
|
|
219
159
|
async unrevert(sessionId) {
|
|
220
|
-
|
|
221
|
-
if (!session?.threadId) {
|
|
222
|
-
throw new Error(`Session ${sessionId} has no active Codex thread`);
|
|
223
|
-
}
|
|
224
|
-
await this.call("session/unrevert", {
|
|
225
|
-
threadId: session.threadId,
|
|
226
|
-
});
|
|
227
|
-
return {};
|
|
160
|
+
throw new Error("Codex unrevert is not supported by Lunel yet");
|
|
228
161
|
}
|
|
229
162
|
async share(sessionId) {
|
|
230
|
-
// Codex has no share concept — return a null stub so the app degrades gracefully.
|
|
231
163
|
return { share: { url: null } };
|
|
232
164
|
}
|
|
233
165
|
async permissionReply(sessionId, permissionId, response) {
|
|
234
|
-
const
|
|
235
|
-
if (!
|
|
236
|
-
throw new Error(`
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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 },
|
|
242
180
|
});
|
|
243
181
|
return {};
|
|
244
182
|
}
|
|
245
|
-
// -------------------------------------------------------------------------
|
|
246
|
-
// JSON-RPC internals
|
|
247
|
-
// -------------------------------------------------------------------------
|
|
248
183
|
send(req) {
|
|
249
184
|
if (!this.proc?.stdin?.writable)
|
|
250
185
|
return;
|
|
@@ -253,12 +188,12 @@ export class CodexProvider {
|
|
|
253
188
|
call(method, params) {
|
|
254
189
|
return new Promise((resolve, reject) => {
|
|
255
190
|
const id = this.nextId++;
|
|
256
|
-
|
|
191
|
+
const key = String(id);
|
|
192
|
+
this.pending.set(key, { resolve, reject });
|
|
257
193
|
this.send({ jsonrpc: "2.0", id, method, params });
|
|
258
|
-
// Safety timeout: if Codex never responds, don't hang the caller forever.
|
|
259
194
|
setTimeout(() => {
|
|
260
|
-
if (this.pending.has(
|
|
261
|
-
this.pending.delete(
|
|
195
|
+
if (this.pending.has(key)) {
|
|
196
|
+
this.pending.delete(key);
|
|
262
197
|
reject(new Error(`Codex RPC timeout: ${method} (id=${id})`));
|
|
263
198
|
}
|
|
264
199
|
}, 30_000);
|
|
@@ -270,83 +205,848 @@ export class CodexProvider {
|
|
|
270
205
|
msg = JSON.parse(line);
|
|
271
206
|
}
|
|
272
207
|
catch {
|
|
273
|
-
return;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
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;
|
|
274
220
|
}
|
|
275
|
-
|
|
276
|
-
if ("id" in msg && typeof msg.id === "number") {
|
|
221
|
+
if ("id" in msg) {
|
|
277
222
|
const resp = msg;
|
|
278
|
-
const pending = this.pending.get(resp.id);
|
|
279
|
-
if (pending)
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
+
});
|
|
309
|
+
}
|
|
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
|
+
}
|
|
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;
|
|
283
373
|
}
|
|
284
|
-
|
|
285
|
-
|
|
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 } });
|
|
286
381
|
}
|
|
382
|
+
return;
|
|
287
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)
|
|
288
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
|
+
});
|
|
289
403
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const mappedType = NOTIFICATION_TYPE_MAP[notif.method] ?? notif.method;
|
|
294
|
-
this.ingestThreadMetadata(params);
|
|
295
|
-
console.log("[codex]", notif.method);
|
|
296
|
-
this.emitter?.({ type: mappedType, properties: params });
|
|
297
|
-
}
|
|
298
|
-
async ensureThread(session) {
|
|
299
|
-
if (session.threadId)
|
|
300
|
-
return session.threadId;
|
|
301
|
-
const result = await this.call("thread/start", {
|
|
302
|
-
...(session.title ? { title: session.title } : {}),
|
|
404
|
+
this.emitter?.({
|
|
405
|
+
type: "message.updated",
|
|
406
|
+
properties: { info: { sessionID: session.id, id: messageId, role: "user" } },
|
|
303
407
|
});
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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;
|
|
307
575
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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);
|
|
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;
|
|
311
621
|
}
|
|
312
622
|
ingestThreadMetadata(payload) {
|
|
313
623
|
const threadId = this.extractThreadId(payload);
|
|
314
624
|
if (!threadId)
|
|
315
625
|
return;
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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);
|
|
321
675
|
}
|
|
322
|
-
return;
|
|
323
676
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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}`);
|
|
329
797
|
}
|
|
330
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));
|
|
331
935
|
}
|
|
332
936
|
extractThreadId(payload) {
|
|
333
937
|
if (!payload || typeof payload !== "object")
|
|
334
938
|
return undefined;
|
|
335
939
|
const obj = payload;
|
|
336
|
-
const direct = this.readString(obj.threadId) ??
|
|
337
|
-
this.readString(obj.thread_id);
|
|
940
|
+
const direct = this.readString(obj.threadId) ?? this.readString(obj.thread_id);
|
|
338
941
|
if (direct)
|
|
339
942
|
return direct;
|
|
340
|
-
const thread = obj.thread;
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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);
|
|
346
971
|
}
|
|
347
972
|
return undefined;
|
|
348
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
|
+
}
|
|
349
985
|
readString(value) {
|
|
350
986
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
351
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";
|
|
1049
|
+
}
|
|
1050
|
+
return this.firstString(item, ["text", "message", "summary", "output", "output_text", "outputText"]);
|
|
1051
|
+
}
|
|
352
1052
|
}
|