@kin-tio/cli 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +46 -0
- package/CHANGELOG.md +95 -0
- package/LICENSE +202 -0
- package/README.md +150 -0
- package/README.zh-CN.md +79 -0
- package/THIRD_PARTY_NOTICES +31 -0
- package/assets/ilink-login-card.png +0 -0
- package/bin/kintio.js +3 -0
- package/codex-workspace/.agents/skills/wechat-kf-reply-sop/SKILL.md +58 -0
- package/dist/cli.js +3 -0
- package/dist/daemon.js +28 -0
- package/dist/index.js +70 -0
- package/dist/mcp-relay.js +11 -0
- package/dist/src/agent/runtime.js +1 -0
- package/dist/src/app.js +34 -0
- package/dist/src/cli.js +578 -0
- package/dist/src/config.js +237 -0
- package/dist/src/domain/message.js +23 -0
- package/dist/src/domain/send-contract.js +205 -0
- package/dist/src/domain/wecom-message.js +281 -0
- package/dist/src/ilink/executor.js +306 -0
- package/dist/src/ilink/inbound-image.js +310 -0
- package/dist/src/ilink/listener.js +306 -0
- package/dist/src/ilink/login-manager.js +198 -0
- package/dist/src/ilink/login-store.js +197 -0
- package/dist/src/ilink/media-gateway.js +83 -0
- package/dist/src/ilink/media.js +267 -0
- package/dist/src/ilink/message.js +247 -0
- package/dist/src/ilink/protocol/client.js +464 -0
- package/dist/src/ilink/protocol/types.js +35 -0
- package/dist/src/ilink/qr.js +109 -0
- package/dist/src/ilink/secret-box.js +143 -0
- package/dist/src/ilink/sqlite-store.js +1194 -0
- package/dist/src/ilink/store-types.js +63 -0
- package/dist/src/lib/image-format.js +23 -0
- package/dist/src/lib/path-identity.js +38 -0
- package/dist/src/lib/private-directory.js +51 -0
- package/dist/src/lib/text.js +19 -0
- package/dist/src/lib/wecom-crypto.js +74 -0
- package/dist/src/lib/xml.js +8 -0
- package/dist/src/mcp/conversation-memory-server.js +179 -0
- package/dist/src/mcp/ilink-server.js +158 -0
- package/dist/src/mcp/ipc-host.js +275 -0
- package/dist/src/mcp/ipc-protocol.js +226 -0
- package/dist/src/mcp/stdio-relay.js +122 -0
- package/dist/src/mcp/wechat-kf-executor.js +295 -0
- package/dist/src/mcp/wechat-kf-server.js +208 -0
- package/dist/src/routes/wecom.js +89 -0
- package/dist/src/runtime/daemon-protocol.js +202 -0
- package/dist/src/runtime/managed-skill.js +49 -0
- package/dist/src/runtime/native-daemon.js +325 -0
- package/dist/src/runtime/single-instance-lock.js +167 -0
- package/dist/src/runtime.js +503 -0
- package/dist/src/services/codex-agent.js +542 -0
- package/dist/src/services/codex-app-server.js +436 -0
- package/dist/src/services/conversation-processor.js +762 -0
- package/dist/src/services/image-stager.js +49 -0
- package/dist/src/services/media-gateway.js +83 -0
- package/dist/src/services/wecom-api.js +311 -0
- package/dist/src/services/wecom-sync.js +316 -0
- package/dist/src/state/persistence.js +124 -0
- package/dist/src/state/sqlite-store.js +3102 -0
- package/dist/src/supervisor.js +212 -0
- package/dist/src/types.js +1 -0
- package/dist/src/version.js +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import crossSpawn from 'cross-spawn';
|
|
3
|
+
import { KINTIO_VERSION } from '../version.js';
|
|
4
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
5
|
+
const THREAD_LIST_PAGE_SIZE = 100;
|
|
6
|
+
const MAX_THREAD_LIST_PAGES = 100;
|
|
7
|
+
const THREAD_SOURCE_KINDS = [
|
|
8
|
+
'cli', 'vscode', 'exec', 'appServer', 'subAgent', 'subAgentReview',
|
|
9
|
+
'subAgentCompact', 'subAgentThreadSpawn', 'subAgentOther', 'unknown',
|
|
10
|
+
];
|
|
11
|
+
const CODEX_ERROR_CATEGORIES = new Set([
|
|
12
|
+
'contextWindowExceeded', 'sessionBudgetExceeded', 'usageLimitExceeded',
|
|
13
|
+
'rateLimitExceeded', 'serverOverloaded', 'cyberPolicy', 'misalignmentPolicyViolation',
|
|
14
|
+
'internalServerError', 'unauthorized', 'badRequest', 'threadRollbackFailed',
|
|
15
|
+
'sandboxError', 'other',
|
|
16
|
+
]);
|
|
17
|
+
const CODEX_OBJECT_ERROR_CATEGORIES = new Set([
|
|
18
|
+
'httpConnectionFailed', 'responseStreamConnectionFailed',
|
|
19
|
+
'responseStreamDisconnected', 'responseTooManyFailedAttempts',
|
|
20
|
+
'activeTurnNotSteerable',
|
|
21
|
+
]);
|
|
22
|
+
function deferred() {
|
|
23
|
+
let resolve;
|
|
24
|
+
let reject;
|
|
25
|
+
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
26
|
+
resolve = resolvePromise;
|
|
27
|
+
reject = rejectPromise;
|
|
28
|
+
});
|
|
29
|
+
return { promise, resolve, reject };
|
|
30
|
+
}
|
|
31
|
+
function asRecord(value) {
|
|
32
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
33
|
+
? value
|
|
34
|
+
: undefined;
|
|
35
|
+
}
|
|
36
|
+
function codexFailureLabel(value) {
|
|
37
|
+
if (value === undefined || value === null)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (typeof value === 'string') {
|
|
40
|
+
return CODEX_ERROR_CATEGORIES.has(value) ? value : 'other';
|
|
41
|
+
}
|
|
42
|
+
const record = asRecord(value);
|
|
43
|
+
const keys = record ? Object.keys(record) : [];
|
|
44
|
+
const category = keys.length === 1 ? keys[0] || '' : '';
|
|
45
|
+
if (!CODEX_OBJECT_ERROR_CATEGORIES.has(category))
|
|
46
|
+
return 'other';
|
|
47
|
+
const detail = asRecord(record?.[category]);
|
|
48
|
+
const status = detail?.httpStatusCode;
|
|
49
|
+
return category + (typeof status === 'number' && Number.isInteger(status) &&
|
|
50
|
+
status >= 100 && status <= 599
|
|
51
|
+
? ` (HTTP ${status})`
|
|
52
|
+
: '');
|
|
53
|
+
}
|
|
54
|
+
function normalizeInput(input) {
|
|
55
|
+
const values = typeof input === 'string' ? [{ type: 'text', text: input }] : input;
|
|
56
|
+
return values.map((item) => item.type === 'text'
|
|
57
|
+
? { type: 'text', text: item.text, text_elements: [] }
|
|
58
|
+
: { type: 'localImage', path: item.path });
|
|
59
|
+
}
|
|
60
|
+
const defaultSpawn = crossSpawn;
|
|
61
|
+
export class CodexAppServer {
|
|
62
|
+
#options;
|
|
63
|
+
#process = null;
|
|
64
|
+
#reader = null;
|
|
65
|
+
#requestId = 1;
|
|
66
|
+
#pending = new Map();
|
|
67
|
+
#turns = new Map();
|
|
68
|
+
#initializing = null;
|
|
69
|
+
#terminating = null;
|
|
70
|
+
#closed = false;
|
|
71
|
+
eventSequence = 0;
|
|
72
|
+
constructor(options = {}) {
|
|
73
|
+
this.#options = {
|
|
74
|
+
...options,
|
|
75
|
+
requestTimeoutMs: options.requestTimeoutMs || REQUEST_TIMEOUT_MS,
|
|
76
|
+
spawnProcess: options.spawnProcess || defaultSpawn,
|
|
77
|
+
logger: options.logger || console,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
startThread(options) {
|
|
81
|
+
return new CodexAppServerThread(this, null, options);
|
|
82
|
+
}
|
|
83
|
+
resumeThread(threadId, options) {
|
|
84
|
+
return new CodexAppServerThread(this, threadId, options);
|
|
85
|
+
}
|
|
86
|
+
async getThreadState(threadId) {
|
|
87
|
+
await this.initialize();
|
|
88
|
+
const listed = async (archived) => {
|
|
89
|
+
let cursor = null;
|
|
90
|
+
for (let page = 0; page < MAX_THREAD_LIST_PAGES; page += 1) {
|
|
91
|
+
const result = await this.request('thread/list', {
|
|
92
|
+
archived,
|
|
93
|
+
useStateDbOnly: true,
|
|
94
|
+
limit: THREAD_LIST_PAGE_SIZE,
|
|
95
|
+
sourceKinds: THREAD_SOURCE_KINDS,
|
|
96
|
+
...(cursor ? { cursor } : {}),
|
|
97
|
+
});
|
|
98
|
+
if ((result.data || []).some((thread) => thread.id === threadId)) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
cursor = result.nextCursor || null;
|
|
102
|
+
if (!cursor)
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
throw new Error('Codex thread listing exceeded the pagination limit');
|
|
106
|
+
};
|
|
107
|
+
if (await listed(false))
|
|
108
|
+
return 'active';
|
|
109
|
+
return await listed(true) ? 'archived' : 'missing';
|
|
110
|
+
}
|
|
111
|
+
async readThread(threadId, { includeTurns = true } = {}) {
|
|
112
|
+
await this.initialize();
|
|
113
|
+
return this.request('thread/read', { threadId, includeTurns });
|
|
114
|
+
}
|
|
115
|
+
async deleteThread(threadId) {
|
|
116
|
+
await this.initialize();
|
|
117
|
+
await this.request('thread/delete', { threadId });
|
|
118
|
+
}
|
|
119
|
+
initialize() {
|
|
120
|
+
this.#initializing ||= this.#initialize();
|
|
121
|
+
return this.#initializing;
|
|
122
|
+
}
|
|
123
|
+
async #initialize() {
|
|
124
|
+
const configArguments = (this.#options.configOverrides || [])
|
|
125
|
+
.flatMap((value) => ['--config', value]);
|
|
126
|
+
const command = 'codex';
|
|
127
|
+
const argumentsList = ['app-server', '--stdio', ...configArguments];
|
|
128
|
+
const child = this.#options.spawnProcess(command, argumentsList, {
|
|
129
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
130
|
+
});
|
|
131
|
+
this.#process = child;
|
|
132
|
+
child.once('error', (error) => {
|
|
133
|
+
this.#fail(new Error(`Codex app-server process error: ${error.message}`, { cause: error }));
|
|
134
|
+
});
|
|
135
|
+
child.once('exit', (code, signal) => {
|
|
136
|
+
if (this.#process === child)
|
|
137
|
+
this.#process = null;
|
|
138
|
+
if (this.#closed)
|
|
139
|
+
return;
|
|
140
|
+
const detail = signal ? `signal ${signal}` : `code ${code ?? 1}`;
|
|
141
|
+
this.#fail(new Error(`Codex app-server exited with ${detail}`));
|
|
142
|
+
});
|
|
143
|
+
child.stderr?.resume();
|
|
144
|
+
this.#reader = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
145
|
+
this.#reader.on('line', (line) => this.#handleLine(line));
|
|
146
|
+
await this.request('initialize', {
|
|
147
|
+
clientInfo: {
|
|
148
|
+
name: 'kintio_codex',
|
|
149
|
+
title: 'Kintio Codex Adapter',
|
|
150
|
+
version: KINTIO_VERSION,
|
|
151
|
+
},
|
|
152
|
+
capabilities: null,
|
|
153
|
+
});
|
|
154
|
+
this.#write({ method: 'initialized', params: {} });
|
|
155
|
+
}
|
|
156
|
+
request(method, params, timeoutMs = this.#options.requestTimeoutMs) {
|
|
157
|
+
if (this.#closed)
|
|
158
|
+
return Promise.reject(new Error('Codex app-server is closed'));
|
|
159
|
+
const id = this.#requestId++;
|
|
160
|
+
const result = deferred();
|
|
161
|
+
const timer = setTimeout(() => {
|
|
162
|
+
this.#pending.delete(id);
|
|
163
|
+
result.reject(new Error(`Codex app-server request timed out: ${method}`));
|
|
164
|
+
}, timeoutMs);
|
|
165
|
+
timer.unref();
|
|
166
|
+
this.#pending.set(id, { ...result, timer, method });
|
|
167
|
+
try {
|
|
168
|
+
this.#write({ method, id, params });
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
this.#pending.delete(id);
|
|
173
|
+
result.reject(error);
|
|
174
|
+
}
|
|
175
|
+
return result.promise;
|
|
176
|
+
}
|
|
177
|
+
waitForTurn(turnId) {
|
|
178
|
+
return this.#turnState(turnId).waiter.promise.finally(() => {
|
|
179
|
+
this.#turns.delete(turnId);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
#write(message) {
|
|
183
|
+
if (!this.#process?.stdin.writable) {
|
|
184
|
+
throw new Error('Codex app-server stdin is not writable');
|
|
185
|
+
}
|
|
186
|
+
this.#process.stdin.write(`${JSON.stringify(message)}\n`);
|
|
187
|
+
}
|
|
188
|
+
#handleLine(line) {
|
|
189
|
+
let parsed;
|
|
190
|
+
try {
|
|
191
|
+
parsed = JSON.parse(line);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
this.#fail(new Error('Invalid JSON from Codex app-server'));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const message = asRecord(parsed);
|
|
198
|
+
if (!message) {
|
|
199
|
+
this.#fail(new Error('Codex app-server emitted a non-object message'));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (typeof message.id === 'number' && typeof message.method !== 'string') {
|
|
203
|
+
const pending = this.#pending.get(message.id);
|
|
204
|
+
if (!pending)
|
|
205
|
+
return;
|
|
206
|
+
clearTimeout(pending.timer);
|
|
207
|
+
this.#pending.delete(message.id);
|
|
208
|
+
const rpcError = asRecord(message.error);
|
|
209
|
+
if (rpcError) {
|
|
210
|
+
const error = new Error(`Codex app-server request failed: ${pending.method}`);
|
|
211
|
+
if (typeof rpcError.code === 'number' && Number.isSafeInteger(rpcError.code)) {
|
|
212
|
+
error.code = rpcError.code;
|
|
213
|
+
}
|
|
214
|
+
pending.reject(error);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
pending.resolve(message.result);
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (typeof message.id === 'number' && typeof message.method === 'string') {
|
|
222
|
+
this.#write({
|
|
223
|
+
id: message.id,
|
|
224
|
+
error: { code: -32601, message: `Unsupported server request: ${message.method}` },
|
|
225
|
+
});
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
this.#handleNotification(message);
|
|
229
|
+
}
|
|
230
|
+
#turnState(turnId) {
|
|
231
|
+
const existing = this.#turns.get(turnId);
|
|
232
|
+
if (existing)
|
|
233
|
+
return existing;
|
|
234
|
+
const created = {
|
|
235
|
+
items: [],
|
|
236
|
+
itemStarts: new Map(),
|
|
237
|
+
waiter: deferred(),
|
|
238
|
+
};
|
|
239
|
+
this.#turns.set(turnId, created);
|
|
240
|
+
return created;
|
|
241
|
+
}
|
|
242
|
+
#handleNotification(message) {
|
|
243
|
+
const sequence = ++this.eventSequence;
|
|
244
|
+
const params = asRecord(message.params);
|
|
245
|
+
if (message.method === 'item/started') {
|
|
246
|
+
const item = asRecord(params?.item);
|
|
247
|
+
if (typeof params?.turnId === 'string' && typeof item?.id === 'string') {
|
|
248
|
+
this.#turnState(params.turnId).itemStarts.set(item.id, sequence);
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (message.method === 'item/completed') {
|
|
253
|
+
const item = asRecord(params?.item);
|
|
254
|
+
if (typeof params?.turnId === 'string' &&
|
|
255
|
+
item &&
|
|
256
|
+
typeof item.type === 'string') {
|
|
257
|
+
const state = this.#turnState(params.turnId);
|
|
258
|
+
const id = typeof item.id === 'string' ? item.id : '';
|
|
259
|
+
state.items.push({
|
|
260
|
+
...item,
|
|
261
|
+
type: item.type,
|
|
262
|
+
startedSequence: state.itemStarts.get(id) || sequence,
|
|
263
|
+
completedSequence: sequence,
|
|
264
|
+
});
|
|
265
|
+
if (id)
|
|
266
|
+
state.itemStarts.delete(id);
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (message.method === 'turn/completed') {
|
|
271
|
+
const turn = asRecord(params?.turn);
|
|
272
|
+
if (typeof turn?.id !== 'string')
|
|
273
|
+
return;
|
|
274
|
+
const state = this.#turnState(turn.id);
|
|
275
|
+
if (turn.status === 'completed') {
|
|
276
|
+
state.waiter.resolve({ items: state.items });
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
const status = turn.status === 'failed' || turn.status === 'interrupted'
|
|
280
|
+
? turn.status
|
|
281
|
+
: 'unknown';
|
|
282
|
+
const failure = status === 'failed'
|
|
283
|
+
? codexFailureLabel(asRecord(turn.error)?.codexErrorInfo) ?? state.failure
|
|
284
|
+
: undefined;
|
|
285
|
+
state.waiter.reject(new Error(`Codex turn ended with status ${status}` +
|
|
286
|
+
`${failure ? `: ${failure}` : ''}`));
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (message.method === 'error') {
|
|
291
|
+
const error = asRecord(params?.error);
|
|
292
|
+
const failure = codexFailureLabel(error?.codexErrorInfo);
|
|
293
|
+
if (typeof params?.turnId === 'string' && failure !== undefined) {
|
|
294
|
+
this.#turnState(params.turnId).failure = failure;
|
|
295
|
+
}
|
|
296
|
+
this.#options.logger.warn?.(failure !== undefined
|
|
297
|
+
? `[codex] app-server error category=${failure}; content suppressed`
|
|
298
|
+
: '[codex] app-server emitted an error notification; content suppressed');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
#rejectAll(error) {
|
|
302
|
+
for (const pending of this.#pending.values()) {
|
|
303
|
+
clearTimeout(pending.timer);
|
|
304
|
+
pending.reject(error);
|
|
305
|
+
}
|
|
306
|
+
this.#pending.clear();
|
|
307
|
+
for (const state of this.#turns.values())
|
|
308
|
+
state.waiter.reject(error);
|
|
309
|
+
this.#turns.clear();
|
|
310
|
+
}
|
|
311
|
+
#fail(error) {
|
|
312
|
+
void this.#shutdown(error);
|
|
313
|
+
}
|
|
314
|
+
#shutdown(error) {
|
|
315
|
+
if (!this.#closed) {
|
|
316
|
+
this.#closed = true;
|
|
317
|
+
this.#reader?.close();
|
|
318
|
+
this.#rejectAll(error);
|
|
319
|
+
}
|
|
320
|
+
return this.#terminate();
|
|
321
|
+
}
|
|
322
|
+
#terminate() {
|
|
323
|
+
if (this.#terminating)
|
|
324
|
+
return this.#terminating;
|
|
325
|
+
const child = this.#process;
|
|
326
|
+
if (!child || child.exitCode !== null)
|
|
327
|
+
return Promise.resolve();
|
|
328
|
+
this.#terminating = new Promise((resolve) => {
|
|
329
|
+
child.once('exit', () => resolve());
|
|
330
|
+
child.kill('SIGTERM');
|
|
331
|
+
const timer = setTimeout(() => child.kill('SIGKILL'), 2_000);
|
|
332
|
+
timer.unref();
|
|
333
|
+
}).finally(() => {
|
|
334
|
+
this.#terminating = null;
|
|
335
|
+
});
|
|
336
|
+
return this.#terminating;
|
|
337
|
+
}
|
|
338
|
+
async close() {
|
|
339
|
+
await this.#shutdown(new Error('Codex app-server closed'));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
class CodexAppServerThread {
|
|
343
|
+
#server;
|
|
344
|
+
#options;
|
|
345
|
+
#ready = null;
|
|
346
|
+
#activeTurnId = '';
|
|
347
|
+
#lastSteerSequence = 0;
|
|
348
|
+
#lastSteerClientId = '';
|
|
349
|
+
id;
|
|
350
|
+
constructor(server, threadId, options) {
|
|
351
|
+
this.#server = server;
|
|
352
|
+
this.#options = options;
|
|
353
|
+
this.id = threadId;
|
|
354
|
+
}
|
|
355
|
+
#params() {
|
|
356
|
+
return {
|
|
357
|
+
cwd: this.#options.workingDirectory,
|
|
358
|
+
approvalPolicy: this.#options.approvalPolicy,
|
|
359
|
+
sandbox: 'read-only',
|
|
360
|
+
...(this.#options.developerInstructions
|
|
361
|
+
? { developerInstructions: this.#options.developerInstructions }
|
|
362
|
+
: {}),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
#ensureThread() {
|
|
366
|
+
this.#ready ||= (async () => {
|
|
367
|
+
await this.#server.initialize();
|
|
368
|
+
const result = this.id
|
|
369
|
+
? await this.#server.request('thread/resume', {
|
|
370
|
+
threadId: this.id,
|
|
371
|
+
...this.#params(),
|
|
372
|
+
})
|
|
373
|
+
: await this.#server.request('thread/start', this.#params());
|
|
374
|
+
this.id = result.thread.id;
|
|
375
|
+
})();
|
|
376
|
+
return this.#ready;
|
|
377
|
+
}
|
|
378
|
+
async ensure() {
|
|
379
|
+
await this.#ensureThread();
|
|
380
|
+
if (!this.id)
|
|
381
|
+
throw new Error('Codex thread has no ID');
|
|
382
|
+
return this.id;
|
|
383
|
+
}
|
|
384
|
+
async startRun(input, { clientUserMessageId } = {}) {
|
|
385
|
+
await this.#ensureThread();
|
|
386
|
+
if (!this.id)
|
|
387
|
+
throw new Error('Codex thread has no ID');
|
|
388
|
+
if (this.#activeTurnId)
|
|
389
|
+
throw new Error('Codex thread already has an active turn');
|
|
390
|
+
this.#lastSteerSequence = 0;
|
|
391
|
+
this.#lastSteerClientId = '';
|
|
392
|
+
const result = await this.#server.request('turn/start', {
|
|
393
|
+
threadId: this.id,
|
|
394
|
+
input: normalizeInput(input),
|
|
395
|
+
...this.#params(),
|
|
396
|
+
...(clientUserMessageId ? { clientUserMessageId } : {}),
|
|
397
|
+
});
|
|
398
|
+
const turnId = result.turn.id;
|
|
399
|
+
this.#activeTurnId = turnId;
|
|
400
|
+
const completion = this.#server.waitForTurn(turnId).then((completed) => {
|
|
401
|
+
const boundaryItem = [...completed.items].reverse().find((item) => item.type === 'userMessage' && item.clientId === this.#lastSteerClientId);
|
|
402
|
+
return {
|
|
403
|
+
...completed,
|
|
404
|
+
lastSteerSequence: boundaryItem?.completedSequence || this.#lastSteerSequence,
|
|
405
|
+
};
|
|
406
|
+
}).finally(() => {
|
|
407
|
+
if (this.#activeTurnId === turnId)
|
|
408
|
+
this.#activeTurnId = '';
|
|
409
|
+
});
|
|
410
|
+
return { turnId, completion };
|
|
411
|
+
}
|
|
412
|
+
async steer(input, { clientUserMessageId } = {}) {
|
|
413
|
+
await this.#ensureThread();
|
|
414
|
+
if (!this.id || !this.#activeTurnId) {
|
|
415
|
+
throw new Error('Codex thread has no active turn to steer');
|
|
416
|
+
}
|
|
417
|
+
const result = await this.#server.request('turn/steer', {
|
|
418
|
+
threadId: this.id,
|
|
419
|
+
input: normalizeInput(input),
|
|
420
|
+
expectedTurnId: this.#activeTurnId,
|
|
421
|
+
...(clientUserMessageId ? { clientUserMessageId } : {}),
|
|
422
|
+
});
|
|
423
|
+
this.#lastSteerSequence = this.#server.eventSequence;
|
|
424
|
+
this.#lastSteerClientId = clientUserMessageId || '';
|
|
425
|
+
return result.turnId;
|
|
426
|
+
}
|
|
427
|
+
async interrupt() {
|
|
428
|
+
if (!this.id || !this.#activeTurnId)
|
|
429
|
+
return false;
|
|
430
|
+
await this.#server.request('turn/interrupt', {
|
|
431
|
+
threadId: this.id,
|
|
432
|
+
turnId: this.#activeTurnId,
|
|
433
|
+
});
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
}
|