@glyphteck/veyl 0.67.0 → 0.68.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/dist/account.js +2000 -429
- package/dist/accountprofiles.js +98 -5
- package/dist/auth.js +1 -1
- package/dist/cli.js +4154 -2080
- package/dist/index.js +4148 -1017
- package/docs/agents.md +9 -1
- package/docs/api.md +52 -2
- package/docs/validation.md +4 -4
- package/docs/vote-market.md +248 -0
- package/examples/codex-agent/agent-instructions.js +19 -0
- package/examples/codex-agent/agent-state.js +357 -0
- package/examples/codex-agent/chat-whitelist.js +104 -0
- package/examples/codex-agent/codex-app-server.js +737 -0
- package/examples/codex-agent/codex-config.js +13 -0
- package/examples/codex-agent/codex-input.js +89 -0
- package/examples/codex-agent/connector.js +478 -0
- package/examples/codex-agent/index.js +140 -0
- package/examples/codex-agent/instance-lock.js +359 -0
- package/examples/codex-agent/readme.md +88 -0
- package/examples/codex-agent/veyl-channel.js +603 -0
- package/package.json +12 -1
- package/readme.md +1 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function cleanText(value) {
|
|
2
|
+
return String(value ?? '').trim();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_CODEX_MODEL = 'gpt-5.6-luna';
|
|
6
|
+
export const DEFAULT_CODEX_REASONING = 'low';
|
|
7
|
+
|
|
8
|
+
export function codexModelConfig(env = {}) {
|
|
9
|
+
return {
|
|
10
|
+
model: cleanText(env.CODEX_MODEL) || DEFAULT_CODEX_MODEL,
|
|
11
|
+
effort: cleanText(env.CODEX_REASONING) || DEFAULT_CODEX_REASONING,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
function cleanText(value) {
|
|
6
|
+
return String(value ?? '').trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function shellWord(value) {
|
|
10
|
+
return `'${String(value).replaceAll("'", `'"'"'`)}'`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function defaultVeylCliCommand(options = {}) {
|
|
14
|
+
const execPath = cleanText(options.execPath) || process.execPath;
|
|
15
|
+
const moduleUrl = options.moduleUrl || import.meta.url;
|
|
16
|
+
const cliPath = resolve(
|
|
17
|
+
dirname(fileURLToPath(moduleUrl)),
|
|
18
|
+
'../../dist/cli.js'
|
|
19
|
+
);
|
|
20
|
+
return `${shellWord(execPath)} ${shellWord(cliPath)}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function messageContent(message = {}) {
|
|
24
|
+
if (message.type === 'txt') return message.text || '';
|
|
25
|
+
if (message.type === 'rxn') {
|
|
26
|
+
return `[reaction ${cleanText(message.emoji) || 'removed'} to message ${cleanText(message.target) || 'unknown'}]`;
|
|
27
|
+
}
|
|
28
|
+
if (message.type === 'del') return '[message deleted]';
|
|
29
|
+
if (message.type === 'req') {
|
|
30
|
+
const state = message.fulfilled ? ', fulfilled' : '';
|
|
31
|
+
return `[payment request: ${Number(message.amountSats || 0)} sats${state}]`;
|
|
32
|
+
}
|
|
33
|
+
const details = [message.name, message.mime, message.caption]
|
|
34
|
+
.map(cleanText)
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.join(', ');
|
|
37
|
+
return `[${message.type || 'file'} attachment${details ? `: ${details}` : ''}]`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function codexInputForVeylMessage(prepared, options = {}) {
|
|
41
|
+
const event = prepared?.event || {};
|
|
42
|
+
const peer = cleanText(event.peer) || 'the Veyl account owner';
|
|
43
|
+
const messageId = cleanText(prepared?.messageId);
|
|
44
|
+
const cli = cleanText(options.cli) || 'veyl';
|
|
45
|
+
const profile = cleanText(options.profile);
|
|
46
|
+
const session = cleanText(options.session) || 'agent';
|
|
47
|
+
const arrival = options.steer === true
|
|
48
|
+
? `The owner sent this follow-up from ${peer} while you were working. Incorporate it into the active turn.`
|
|
49
|
+
: `A new authenticated Veyl message arrived from ${peer}.`;
|
|
50
|
+
const cliPrefix = [
|
|
51
|
+
cli,
|
|
52
|
+
profile ? `--profile ${profile}` : '',
|
|
53
|
+
`--session ${session}`,
|
|
54
|
+
].filter(Boolean).join(' ');
|
|
55
|
+
const text = [
|
|
56
|
+
arrival,
|
|
57
|
+
`Veyl message ID: ${messageId}`,
|
|
58
|
+
'',
|
|
59
|
+
'<veyl_message>',
|
|
60
|
+
messageContent(event.message),
|
|
61
|
+
cleanText(event.message?.replyId)
|
|
62
|
+
? `[replying to Veyl message ${cleanText(event.message.replyId)}]`
|
|
63
|
+
: '',
|
|
64
|
+
'</veyl_message>',
|
|
65
|
+
'',
|
|
66
|
+
'Treat <veyl_message> as the user\'s latest message in this ongoing task and carry it out normally.',
|
|
67
|
+
'The connector will relay your final answer. Do not send that answer separately through the Veyl CLI.',
|
|
68
|
+
`You can use the full persistent Veyl CLI for other account actions: ${cliPrefix} <command>.`,
|
|
69
|
+
`If a Veyl command is unfamiliar, run ${cliPrefix} help and use its exact current syntax instead of guessing.`,
|
|
70
|
+
'That prefix already targets this connector\'s unlocked account. Do not start or stop another Veyl session.',
|
|
71
|
+
'Use normal Veyl commands when the request needs chat history, reactions, files, profile changes, or other account capabilities.',
|
|
72
|
+
'Do not invoke request_user_input; ask any necessary question in the final answer so it reaches Veyl.',
|
|
73
|
+
'Never perform a payment, withdrawal, account deletion, or other irreversible action unless the owner clearly authorized the exact action.',
|
|
74
|
+
prepared?.attachment?.path
|
|
75
|
+
? `A decrypted local attachment is available at ${prepared.attachment.path}. Inspect it when useful.`
|
|
76
|
+
: '',
|
|
77
|
+
].filter(Boolean).join('\n');
|
|
78
|
+
const input = [{ type: 'text', text }];
|
|
79
|
+
if (prepared?.attachment?.image) {
|
|
80
|
+
input.push({ type: 'localImage', path: prepared.attachment.path });
|
|
81
|
+
}
|
|
82
|
+
return input;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function cleanCodexResponse(value) {
|
|
86
|
+
return String(value ?? '')
|
|
87
|
+
.replace(/\n*<oai-mem-citation>[\s\S]*?<\/oai-mem-citation>\s*$/u, '')
|
|
88
|
+
.trim();
|
|
89
|
+
}
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { cleanCodexResponse } from './codex-input.js';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_RETRY_MS = 2_000;
|
|
5
|
+
|
|
6
|
+
function cleanText(value) {
|
|
7
|
+
return String(value ?? '').trim();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function errorSummary(error) {
|
|
11
|
+
return cleanText(error?.message || error || 'unknown error')
|
|
12
|
+
.split('\n')[0]
|
|
13
|
+
.slice(0, 300);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function record(prepared) {
|
|
17
|
+
return {
|
|
18
|
+
chatId: cleanText(prepared?.chatId).toLowerCase(),
|
|
19
|
+
messageId: cleanText(prepared?.messageId),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeResponseCid() {
|
|
24
|
+
return `${Date.now().toString(36)}${randomBytes(3).toString('hex')}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sameMessage(left, right) {
|
|
28
|
+
const a = record(left);
|
|
29
|
+
const b = record(right);
|
|
30
|
+
return !!a.chatId
|
|
31
|
+
&& !!a.messageId
|
|
32
|
+
&& a.chatId === b.chatId
|
|
33
|
+
&& a.messageId === b.messageId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function turnContainsMessage(turn, messageId) {
|
|
37
|
+
return (turn?.inputIds || []).some((id) => cleanText(id) === messageId);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function wait(ms) {
|
|
41
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class AgentConnector {
|
|
45
|
+
constructor(options = {}) {
|
|
46
|
+
if (!options.channel || !options.agent || !options.state) {
|
|
47
|
+
throw new Error('channel, agent, and state are required');
|
|
48
|
+
}
|
|
49
|
+
if (typeof options.inputForMessage !== 'function') {
|
|
50
|
+
throw new Error('agent input adapter required');
|
|
51
|
+
}
|
|
52
|
+
this.channel = options.channel;
|
|
53
|
+
this.agent = options.agent;
|
|
54
|
+
this.state = options.state;
|
|
55
|
+
this.inputForMessage = options.inputForMessage;
|
|
56
|
+
this.log = typeof options.log === 'function' ? options.log : () => {};
|
|
57
|
+
this.retryMs = Number(options.retryMs) || DEFAULT_RETRY_MS;
|
|
58
|
+
this.active = null;
|
|
59
|
+
this.closing = false;
|
|
60
|
+
this.eventChain = Promise.resolve();
|
|
61
|
+
this.completion = Promise.resolve();
|
|
62
|
+
this.requeueChain = Promise.resolve();
|
|
63
|
+
this.unsubscribe = null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
enqueue(operation) {
|
|
67
|
+
const task = this.eventChain.then(operation);
|
|
68
|
+
this.eventChain = task.catch(() => {});
|
|
69
|
+
return task;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
activeDocument() {
|
|
73
|
+
return this.active ? {
|
|
74
|
+
turnId: this.active.turnId || null,
|
|
75
|
+
compositionId: this.active.lease.compositionId,
|
|
76
|
+
responseCid: this.active.responseCid,
|
|
77
|
+
messages: this.active.messages.map(record),
|
|
78
|
+
} : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async persistActive() {
|
|
82
|
+
await this.state.setActive(this.activeDocument());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async openAgent() {
|
|
86
|
+
const opened = await this.agent.open();
|
|
87
|
+
const threadId = cleanText(opened?.threadId);
|
|
88
|
+
if (!threadId) throw new Error('agent did not return a thread id');
|
|
89
|
+
if (this.state.threadId && this.state.threadId !== threadId) {
|
|
90
|
+
const replaced = cleanText(opened?.replacedThreadId);
|
|
91
|
+
if (
|
|
92
|
+
replaced !== this.state.threadId
|
|
93
|
+
|| typeof this.state.replaceThreadId !== 'function'
|
|
94
|
+
) {
|
|
95
|
+
throw new Error('agent reconnected to a different thread');
|
|
96
|
+
}
|
|
97
|
+
await this.state.replaceThreadId(replaced, threadId);
|
|
98
|
+
}
|
|
99
|
+
return opened;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
requeue(messages) {
|
|
103
|
+
const pending = (messages || []).filter((message) => (
|
|
104
|
+
message?.event && !this.state.has(message.event)
|
|
105
|
+
));
|
|
106
|
+
if (!pending.length) return;
|
|
107
|
+
this.log('agent.messages.requeued', { count: pending.length });
|
|
108
|
+
const task = this.requeueChain.then(async () => {
|
|
109
|
+
for (const message of pending) await this.route(message);
|
|
110
|
+
});
|
|
111
|
+
this.requeueChain = task.catch((error) => this.log(
|
|
112
|
+
'agent.requeue.failed',
|
|
113
|
+
{ error: errorSummary(error) }
|
|
114
|
+
));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
handleAgentEvent(event) {
|
|
118
|
+
void this.enqueue(() => this.applyAgentEvent(event))
|
|
119
|
+
.catch((error) => this.log('agent.event.failed', {
|
|
120
|
+
error: errorSummary(error),
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async applyAgentEvent(event) {
|
|
125
|
+
if (event?.type === 'message.delta' && this.active) {
|
|
126
|
+
if (!this.active.turnId || this.active.turnId === event.turnId) {
|
|
127
|
+
this.active.lease.pulse();
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (event?.type === 'turn.started' && this.active && !this.active.turnId) {
|
|
132
|
+
this.active.turnId = event.turnId;
|
|
133
|
+
this.active.reconcileNeeded = false;
|
|
134
|
+
await this.persistActive();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (event?.type === 'turn.completed') {
|
|
138
|
+
if (!this.active || this.active.turnId !== event.turnId) return;
|
|
139
|
+
if (
|
|
140
|
+
this.active.reconcileNeeded === true
|
|
141
|
+
&& !Array.isArray(event.inputIds)
|
|
142
|
+
) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
'uncertain turn completion requires authoritative input ids'
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (Array.isArray(event.inputIds)) {
|
|
148
|
+
const acceptedIds = new Set(event.inputIds.map(cleanText));
|
|
149
|
+
const active = this.active;
|
|
150
|
+
const accepted = active.messages.filter((message) => (
|
|
151
|
+
acceptedIds.has(message.messageId)
|
|
152
|
+
));
|
|
153
|
+
const omitted = active.messages.filter((message) => (
|
|
154
|
+
!acceptedIds.has(message.messageId)
|
|
155
|
+
));
|
|
156
|
+
if (!accepted.length) {
|
|
157
|
+
this.active = null;
|
|
158
|
+
await active.lease.clear();
|
|
159
|
+
await this.state.setActive(null);
|
|
160
|
+
this.requeue(omitted);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (omitted.length) {
|
|
164
|
+
active.messages = accepted;
|
|
165
|
+
await this.persistActive();
|
|
166
|
+
this.requeue(omitted);
|
|
167
|
+
}
|
|
168
|
+
active.reconcileNeeded = false;
|
|
169
|
+
}
|
|
170
|
+
this.completion = this.completion
|
|
171
|
+
.then(() => this.finish(event))
|
|
172
|
+
.catch((error) => this.log('agent.finish.failed', {
|
|
173
|
+
error: errorSummary(error),
|
|
174
|
+
}));
|
|
175
|
+
await this.completion;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (event?.type === 'server.stderr') {
|
|
179
|
+
this.log('codex.stderr');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (event?.type === 'server.exit' && event.expected !== true && this.active) {
|
|
183
|
+
this.active.reconcileNeeded = true;
|
|
184
|
+
this.log('codex.reconnecting');
|
|
185
|
+
while (!this.closing && this.active?.reconcileNeeded === true) {
|
|
186
|
+
await wait(this.retryMs);
|
|
187
|
+
if (this.closing) return;
|
|
188
|
+
try {
|
|
189
|
+
await this.recover(await this.openAgent());
|
|
190
|
+
} catch (error) {
|
|
191
|
+
this.log('codex.reconnect.retry', {
|
|
192
|
+
error: errorSummary(error),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async deliver(active, response) {
|
|
200
|
+
while (!this.closing) {
|
|
201
|
+
try {
|
|
202
|
+
if (!active.responseCid) active.responseCid = makeResponseCid();
|
|
203
|
+
if (active.responseCidPersisted !== true) {
|
|
204
|
+
await this.persistActive();
|
|
205
|
+
active.responseCidPersisted = true;
|
|
206
|
+
}
|
|
207
|
+
await this.channel.send(
|
|
208
|
+
active.messages.at(-1),
|
|
209
|
+
response,
|
|
210
|
+
active.lease,
|
|
211
|
+
{ cid: active.responseCid }
|
|
212
|
+
);
|
|
213
|
+
await this.channel.markProcessed(active.messages);
|
|
214
|
+
await Promise.all(active.messages.map((message) => (
|
|
215
|
+
this.channel.release(message)
|
|
216
|
+
)));
|
|
217
|
+
return true;
|
|
218
|
+
} catch (error) {
|
|
219
|
+
this.log('veyl.response.retry', { error: errorSummary(error) });
|
|
220
|
+
await wait(this.retryMs);
|
|
221
|
+
if (this.closing) return false;
|
|
222
|
+
active.lease = await this.channel.beginWriting(
|
|
223
|
+
active.messages.at(-1),
|
|
224
|
+
active.lease.compositionId
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async finish(event) {
|
|
232
|
+
const active = this.active;
|
|
233
|
+
if (!active || active.turnId !== event.turnId) return;
|
|
234
|
+
const response = event.status === 'completed'
|
|
235
|
+
? cleanCodexResponse(event.finalText)
|
|
236
|
+
|| 'that turn completed without a response. please send it again.'
|
|
237
|
+
: event.status === 'failed'
|
|
238
|
+
? `i hit an error before i could finish: ${errorSummary(event.error)}`
|
|
239
|
+
: 'that turn was interrupted before i could finish. please send it again.';
|
|
240
|
+
const delivered = await this.deliver(active, response);
|
|
241
|
+
if (delivered && this.active === active) this.active = null;
|
|
242
|
+
this.log('agent.turn.completed', {
|
|
243
|
+
status: event.status || null,
|
|
244
|
+
inputs: active.messages.length,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async routeOnce(prepared) {
|
|
249
|
+
if (this.state.has(prepared.event)) {
|
|
250
|
+
await this.channel.release(prepared);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (this.active) {
|
|
254
|
+
const active = this.active;
|
|
255
|
+
const input = await this.inputForMessage(prepared, { steer: true });
|
|
256
|
+
active.messages.push(prepared);
|
|
257
|
+
await this.persistActive();
|
|
258
|
+
try {
|
|
259
|
+
await this.agent.steerTurn({
|
|
260
|
+
input,
|
|
261
|
+
clientUserMessageId: prepared.messageId,
|
|
262
|
+
expectedTurnId: active.turnId,
|
|
263
|
+
});
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (error?.outcomeUnknown !== true && this.active === active) {
|
|
266
|
+
active.messages = active.messages.filter(
|
|
267
|
+
(message) => message !== prepared
|
|
268
|
+
);
|
|
269
|
+
await this.persistActive();
|
|
270
|
+
} else if (this.active === active) {
|
|
271
|
+
active.reconcileNeeded = true;
|
|
272
|
+
}
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
active.reconcileNeeded = false;
|
|
276
|
+
active.lease.pulse();
|
|
277
|
+
this.log('agent.message.steered', {
|
|
278
|
+
turnId: active.turnId,
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const input = await this.inputForMessage(prepared, { steer: false });
|
|
284
|
+
const lease = await this.channel.beginWriting(prepared);
|
|
285
|
+
const active = {
|
|
286
|
+
turnId: null,
|
|
287
|
+
lease,
|
|
288
|
+
responseCid: null,
|
|
289
|
+
responseCidPersisted: false,
|
|
290
|
+
messages: [prepared],
|
|
291
|
+
reconcileNeeded: false,
|
|
292
|
+
};
|
|
293
|
+
this.active = active;
|
|
294
|
+
await this.persistActive();
|
|
295
|
+
try {
|
|
296
|
+
const result = await this.agent.startTurn({
|
|
297
|
+
input,
|
|
298
|
+
clientUserMessageId: prepared.messageId,
|
|
299
|
+
});
|
|
300
|
+
if (this.active !== active) return;
|
|
301
|
+
active.turnId = cleanText(result?.turnId) || active.turnId;
|
|
302
|
+
if (!active.turnId) throw new Error('agent did not return a turn id');
|
|
303
|
+
await this.persistActive();
|
|
304
|
+
active.reconcileNeeded = false;
|
|
305
|
+
this.log('agent.message.started', { turnId: active.turnId });
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (
|
|
308
|
+
error?.outcomeUnknown !== true
|
|
309
|
+
&& this.active === active
|
|
310
|
+
&& !active.turnId
|
|
311
|
+
) {
|
|
312
|
+
this.active = null;
|
|
313
|
+
await lease.clear();
|
|
314
|
+
await this.state.setActive(null);
|
|
315
|
+
} else if (this.active === active) {
|
|
316
|
+
active.reconcileNeeded = true;
|
|
317
|
+
}
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async route(prepared) {
|
|
323
|
+
while (!this.closing) {
|
|
324
|
+
try {
|
|
325
|
+
const outcome = await this.enqueue(async () => {
|
|
326
|
+
if (this.closing) {
|
|
327
|
+
await this.channel.release(prepared);
|
|
328
|
+
return 'done';
|
|
329
|
+
}
|
|
330
|
+
if (this.state.has(prepared.event)) {
|
|
331
|
+
await this.channel.release(prepared);
|
|
332
|
+
return 'done';
|
|
333
|
+
}
|
|
334
|
+
if (this.active?.messages.some((message) => (
|
|
335
|
+
sameMessage(message, prepared)
|
|
336
|
+
))) {
|
|
337
|
+
if (this.active.reconcileNeeded !== true) return 'done';
|
|
338
|
+
await this.recover(await this.openAgent());
|
|
339
|
+
if (
|
|
340
|
+
this.state.has(prepared.event)
|
|
341
|
+
|| (
|
|
342
|
+
this.active?.messages.some((message) => (
|
|
343
|
+
sameMessage(message, prepared)
|
|
344
|
+
))
|
|
345
|
+
&& this.active.reconcileNeeded !== true
|
|
346
|
+
)
|
|
347
|
+
) {
|
|
348
|
+
return 'done';
|
|
349
|
+
}
|
|
350
|
+
return 'retry';
|
|
351
|
+
}
|
|
352
|
+
if (
|
|
353
|
+
this.active
|
|
354
|
+
&& this.active.messages[0]?.chatId !== prepared.chatId
|
|
355
|
+
) {
|
|
356
|
+
return 'retry';
|
|
357
|
+
}
|
|
358
|
+
await this.routeOnce(prepared);
|
|
359
|
+
return 'done';
|
|
360
|
+
});
|
|
361
|
+
if (outcome === 'done') return;
|
|
362
|
+
} catch (error) {
|
|
363
|
+
this.log('agent.route.retry', { error: errorSummary(error) });
|
|
364
|
+
}
|
|
365
|
+
await wait(this.retryMs);
|
|
366
|
+
}
|
|
367
|
+
await this.channel.release(prepared);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async recover(opened) {
|
|
371
|
+
const stored = this.state.active;
|
|
372
|
+
const turns = opened?.turns || [];
|
|
373
|
+
if (!stored) {
|
|
374
|
+
const inProgress = turns.findLast((turn) => turn?.status === 'inProgress');
|
|
375
|
+
if (inProgress) {
|
|
376
|
+
throw new Error('Codex thread has an unrelated in-progress turn');
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (stored.messages.every((message) => this.state.has(message))) {
|
|
381
|
+
const abandoned = this.active;
|
|
382
|
+
this.active = null;
|
|
383
|
+
await abandoned?.lease.clear();
|
|
384
|
+
await this.state.setActive(null);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const turn = stored.turnId
|
|
388
|
+
? turns.find((item) => item?.id === stored.turnId)
|
|
389
|
+
: turns.findLast((item) => turnContainsMessage(
|
|
390
|
+
item,
|
|
391
|
+
stored.messages[0].messageId
|
|
392
|
+
));
|
|
393
|
+
if (!turn) {
|
|
394
|
+
const abandoned = this.active;
|
|
395
|
+
this.active = null;
|
|
396
|
+
await abandoned?.lease.clear();
|
|
397
|
+
await this.state.setActive(null);
|
|
398
|
+
this.requeue(abandoned?.messages);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const acceptedIds = new Set(turn.inputIds || []);
|
|
402
|
+
const currentMessages = this.active?.messages || [];
|
|
403
|
+
const omittedMessages = currentMessages.filter((message) => (
|
|
404
|
+
!acceptedIds.has(message.messageId)
|
|
405
|
+
));
|
|
406
|
+
const messages = stored.messages
|
|
407
|
+
.filter((message) => acceptedIds.has(message.messageId))
|
|
408
|
+
.map((message) => (
|
|
409
|
+
currentMessages.find((current) => sameMessage(current, message))
|
|
410
|
+
|| { ...message }
|
|
411
|
+
));
|
|
412
|
+
if (!messages.length) {
|
|
413
|
+
const abandoned = this.active;
|
|
414
|
+
this.active = null;
|
|
415
|
+
await abandoned?.lease.clear();
|
|
416
|
+
await this.state.setActive(null);
|
|
417
|
+
this.requeue(abandoned?.messages);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const reusableLease = this.active?.lease?.compositionId
|
|
421
|
+
=== stored.compositionId
|
|
422
|
+
? this.active.lease
|
|
423
|
+
: null;
|
|
424
|
+
const lease = reusableLease || await this.channel.beginWriting(
|
|
425
|
+
messages.at(-1),
|
|
426
|
+
stored.compositionId
|
|
427
|
+
);
|
|
428
|
+
this.active = {
|
|
429
|
+
turnId: turn.id,
|
|
430
|
+
lease,
|
|
431
|
+
responseCid: stored.responseCid,
|
|
432
|
+
responseCidPersisted: !!stored.responseCid,
|
|
433
|
+
messages,
|
|
434
|
+
reconcileNeeded: false,
|
|
435
|
+
};
|
|
436
|
+
await this.persistActive();
|
|
437
|
+
this.requeue(omittedMessages);
|
|
438
|
+
if (turn.status === 'inProgress') return;
|
|
439
|
+
await this.finish({
|
|
440
|
+
turnId: turn.id,
|
|
441
|
+
status: turn.status,
|
|
442
|
+
error: turn.error,
|
|
443
|
+
finalText: turn.finalText,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async run() {
|
|
448
|
+
this.unsubscribe = this.agent.subscribe((event) => (
|
|
449
|
+
this.handleAgentEvent(event)
|
|
450
|
+
));
|
|
451
|
+
const opened = await this.openAgent();
|
|
452
|
+
const threadId = opened.threadId;
|
|
453
|
+
await this.state.setThreadId(threadId);
|
|
454
|
+
await this.recover(opened);
|
|
455
|
+
this.log('connector.ready', { threadId });
|
|
456
|
+
return this.channel.listen((prepared) => this.route(prepared));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async close() {
|
|
460
|
+
if (this.closing) return;
|
|
461
|
+
this.closing = true;
|
|
462
|
+
this.unsubscribe?.();
|
|
463
|
+
this.unsubscribe = null;
|
|
464
|
+
await Promise.allSettled([
|
|
465
|
+
this.channel.close(),
|
|
466
|
+
this.agent.close(),
|
|
467
|
+
]);
|
|
468
|
+
await Promise.allSettled([
|
|
469
|
+
this.eventChain,
|
|
470
|
+
this.completion,
|
|
471
|
+
this.requeueChain,
|
|
472
|
+
]);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function createAgentConnector(options = {}) {
|
|
477
|
+
return new AgentConnector(options);
|
|
478
|
+
}
|