@excitedjs/agent-runtime-codex 1.0.0-beta.172 → 1.0.0-beta.173
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/README.md +4 -2
- package/dist/events.d.ts +7 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +79 -5
- package/dist/events.js.map +1 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +1 -0
- package/dist/provider.js.map +1 -1
- package/dist/runtime.d.ts +2 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +1 -0
- package/dist/runtime.js.map +1 -1
- package/dist/turn-manager.d.ts +27 -45
- package/dist/turn-manager.d.ts.map +1 -1
- package/dist/turn-manager.js +336 -276
- package/dist/turn-manager.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -3
package/dist/turn-manager.js
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-dispatcher in-memory inbound submitter.
|
|
3
|
-
*
|
|
4
|
-
* Contract:
|
|
5
|
-
* - accepted inbound messages are not persisted;
|
|
6
|
-
* - Feishu message_id redelivery is deduped within this server process;
|
|
7
|
-
* - one accepted inbound message becomes one Codex `turn/start` submission;
|
|
8
|
-
* - Codex text output alone does not send anything to Feishu.
|
|
9
|
-
*/
|
|
10
1
|
import { extractAssistantText, subscribeTurnCollection, submitTurnStart, } from './events.js';
|
|
11
|
-
import { compileCodexOutputSchema
|
|
2
|
+
import { compileCodexOutputSchema } from './output-schema-codec.js';
|
|
12
3
|
import { DEFAULT_MESSAGE_ID_DEDUPE_WINDOW, unsupportedFeatureError, } from '@excitedjs/dreamux-utils';
|
|
13
4
|
export class TurnManager {
|
|
14
5
|
opts;
|
|
@@ -19,298 +10,340 @@ export class TurnManager {
|
|
|
19
10
|
pendingMessageIds = new Map();
|
|
20
11
|
pendingTextInputIds = new Map();
|
|
21
12
|
pendingAdmissions = new Set();
|
|
13
|
+
inFlightNativeAdmissions = new Set();
|
|
14
|
+
nextNativeAdmission = 0;
|
|
15
|
+
nativeTurns = new Map();
|
|
16
|
+
pendingActivity = new Map();
|
|
17
|
+
unboundObservedTurnIds = new Set();
|
|
18
|
+
terminalOrder = [];
|
|
19
|
+
protocolFailure = null;
|
|
20
|
+
collector = null;
|
|
21
|
+
collectorThreadId = null;
|
|
22
|
+
decisionTail = Promise.resolve();
|
|
22
23
|
stopped = false;
|
|
23
|
-
activeTurnSlot = null;
|
|
24
|
-
/**
|
|
25
|
-
* Turn ids submitted to Codex that have not yet reached `turn/completed`. On
|
|
26
|
-
* `stop()` each still-pending turn is settled as `stopped` so a teammate turn
|
|
27
|
-
* interrupted by teardown is not lost.
|
|
28
|
-
*/
|
|
29
|
-
pendingTurns = new Map();
|
|
30
24
|
idlePromise = null;
|
|
31
25
|
idleResolve = null;
|
|
32
26
|
log;
|
|
33
27
|
messageIdDedupeWindow;
|
|
34
28
|
constructor(opts) {
|
|
35
29
|
this.opts = opts;
|
|
36
|
-
this.log = opts.log ?? ((
|
|
37
|
-
const prefix = `[turn-manager ${opts.dispatcherId}] ${
|
|
38
|
-
if (
|
|
39
|
-
console.error(prefix,
|
|
30
|
+
this.log = opts.log ?? ((level, message, error) => {
|
|
31
|
+
const prefix = `[turn-manager ${opts.dispatcherId}] ${level}`;
|
|
32
|
+
if (error === undefined)
|
|
33
|
+
console.error(prefix, message);
|
|
40
34
|
else
|
|
41
|
-
console.error(prefix,
|
|
35
|
+
console.error(prefix, message, error);
|
|
42
36
|
});
|
|
43
37
|
this.messageIdDedupeWindow = Math.max(0, opts.messageIdDedupeWindow ?? DEFAULT_MESSAGE_ID_DEDUPE_WINDOW);
|
|
44
38
|
}
|
|
45
39
|
isBusy() {
|
|
46
|
-
return this.
|
|
40
|
+
return this.pendingAdmissions.size > 0 || [...this.nativeTurns.values()].some((record) => record.completion === null);
|
|
47
41
|
}
|
|
48
42
|
waitIdle() {
|
|
49
43
|
if (!this.isBusy())
|
|
50
44
|
return Promise.resolve();
|
|
51
|
-
|
|
52
|
-
// is replaced with a fresh one the next time the runtime goes busy.
|
|
53
|
-
if (this.idlePromise === null) {
|
|
54
|
-
this.idlePromise = new Promise((resolve) => {
|
|
55
|
-
this.idleResolve = resolve;
|
|
56
|
-
});
|
|
57
|
-
}
|
|
45
|
+
this.idlePromise ??= new Promise((resolve) => { this.idleResolve = resolve; });
|
|
58
46
|
return this.idlePromise;
|
|
59
47
|
}
|
|
60
|
-
/**
|
|
61
|
-
* Submit one accepted inbound message to Codex. Returns duplicate when this
|
|
62
|
-
* process already saw the message_id.
|
|
63
|
-
*/
|
|
64
48
|
enqueue(input) {
|
|
65
|
-
return this.trackAdmission(this.reserveSource(input.sourceId, this.seenMessageIds, this.seenMessageIdOrder, this.pendingMessageIds, () => this.
|
|
49
|
+
return this.trackAdmission(this.reserveSource(input.sourceId, this.seenMessageIds, this.seenMessageIdOrder, this.pendingMessageIds, () => this.enqueueDecision(() => this.submit(input.text, null, `message ${input.sourceId || '<none>'}`))));
|
|
66
50
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
51
|
+
submitTextInput(input) {
|
|
52
|
+
return this.trackAdmission(this.reserveSource(input.sourceId, this.seenTextInputIds, this.seenTextInputIdOrder, this.pendingTextInputIds, async () => {
|
|
53
|
+
let codec = null;
|
|
54
|
+
if (input.outputSchema !== undefined) {
|
|
55
|
+
try {
|
|
56
|
+
codec = compileCodexOutputSchema(input.outputSchema);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return { status: 'failed', error: asError(error) };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return this.enqueueDecision(() => this.submit(input.text, codec, 'text input'));
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
async stop() {
|
|
66
|
+
this.stopped = true;
|
|
67
|
+
this.collector?.dispose();
|
|
68
|
+
this.collector = null;
|
|
69
|
+
while (this.pendingAdmissions.size > 0)
|
|
70
|
+
await Promise.allSettled([...this.pendingAdmissions]);
|
|
71
|
+
this.drainTerminalOrder();
|
|
72
|
+
for (const record of this.nativeTurns.values()) {
|
|
73
|
+
if (record.completion !== null)
|
|
74
|
+
continue;
|
|
75
|
+
for (const member of record.members)
|
|
76
|
+
member.settle({ kind: 'stopped' });
|
|
90
77
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
this.log('error', `turn/start submission failed for message ${input.sourceId === '' ? '<none>' : input.sourceId}: ${error.message}`, error);
|
|
95
|
-
return { status: 'ambiguous', error };
|
|
78
|
+
for (const [turnId, record] of this.nativeTurns) {
|
|
79
|
+
if (record.completion === null)
|
|
80
|
+
this.nativeTurns.delete(turnId);
|
|
96
81
|
}
|
|
97
|
-
this.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return this.trackAdmission(this.reserveSource(input.sourceId, this.seenTextInputIds, this.seenTextInputIdOrder, this.pendingTextInputIds, () => this.submitTextInputUnreserved(input)));
|
|
82
|
+
this.terminalOrder.length = 0;
|
|
83
|
+
this.pendingActivity.clear();
|
|
84
|
+
this.unboundObservedTurnIds.clear();
|
|
85
|
+
this.resolveIdleWaitersIfIdle();
|
|
102
86
|
}
|
|
103
|
-
async
|
|
87
|
+
async submit(text, codec, description) {
|
|
104
88
|
if (this.stopped)
|
|
105
89
|
return { status: 'stopped' };
|
|
90
|
+
if (this.protocolFailure !== null)
|
|
91
|
+
return { status: 'failed', error: this.protocolFailure };
|
|
106
92
|
const threadId = this.opts.getThreadId();
|
|
107
|
-
if (threadId === null)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
catch (err) {
|
|
118
|
-
const error = asError(err);
|
|
119
|
-
this.log('error', error.message, error);
|
|
120
|
-
return { status: 'failed', error };
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
let activeTurn;
|
|
93
|
+
if (threadId === null)
|
|
94
|
+
return { status: 'failed', error: new Error('input submitted without thread_id') };
|
|
95
|
+
const deferred = createRuntimeSubmission();
|
|
96
|
+
this.ensureCollector(threadId);
|
|
97
|
+
const incompatible = this.incompatibleCodecError(codec);
|
|
98
|
+
if (incompatible !== null)
|
|
99
|
+
return { status: 'failed', error: incompatible };
|
|
100
|
+
const admissionId = this.nextNativeAdmission++;
|
|
101
|
+
this.inFlightNativeAdmissions.add(admissionId);
|
|
102
|
+
let response;
|
|
124
103
|
try {
|
|
125
|
-
|
|
104
|
+
response = await submitTurnStart(this.opts.client, threadId, text, this.opts.turnCwd ?? null, codec?.wireSchema);
|
|
126
105
|
}
|
|
127
|
-
catch (
|
|
128
|
-
const
|
|
129
|
-
this.log('error',
|
|
130
|
-
|
|
106
|
+
catch (error) {
|
|
107
|
+
const normalized = asError(error);
|
|
108
|
+
this.log('error', `turn/start submission failed for ${description}: ${normalized.message}`, normalized);
|
|
109
|
+
this.inFlightNativeAdmissions.delete(admissionId);
|
|
110
|
+
this.releaseCompletedRecords(admissionId);
|
|
111
|
+
this.dropOrphanActivityIfIdle();
|
|
112
|
+
return { status: 'ambiguous', error: normalized };
|
|
131
113
|
}
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
res = await submitTurnStart(this.opts.client, threadId, input.text, this.opts.turnCwd ?? null, activeTurn.slot.codec?.wireSchema);
|
|
114
|
+
const observed = this.nativeTurns.get(response.turn.id);
|
|
115
|
+
if (this.stopped && (observed === undefined || observed.terminal === null)) {
|
|
116
|
+
deferred.settle({ kind: 'stopped' });
|
|
137
117
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
this.recordTurnStartFailure(activeTurn.slot, error);
|
|
141
|
-
this.log('error', `text turn/start submission failed: ${error.message}`, error);
|
|
142
|
-
return { status: 'ambiguous', error };
|
|
118
|
+
else {
|
|
119
|
+
this.bindSubmission(response.turn.id, deferred, codec);
|
|
143
120
|
}
|
|
144
|
-
this.
|
|
145
|
-
|
|
121
|
+
this.inFlightNativeAdmissions.delete(admissionId);
|
|
122
|
+
this.releaseCompletedRecords(admissionId);
|
|
123
|
+
this.dropOrphanActivityIfIdle();
|
|
124
|
+
return { status: 'submitted', submission: deferred.submission };
|
|
146
125
|
}
|
|
147
|
-
|
|
148
|
-
this.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (activeSlot !== null) {
|
|
152
|
-
activeSlot.stopped = true;
|
|
153
|
-
activeSlot.collector.dispose();
|
|
154
|
-
activeSlot.codec = null;
|
|
155
|
-
activeSlot.settle({ status: 'stopped' });
|
|
156
|
-
}
|
|
157
|
-
// Any turn still in flight at teardown will never reach `turn/completed`
|
|
158
|
-
// (the WS is closing). Settle each as `stopped` so an interrupted teammate
|
|
159
|
-
// turn is delivered with a status rather than vanishing.
|
|
160
|
-
for (const slot of this.pendingTurns.values()) {
|
|
161
|
-
slot.settle({ status: 'stopped' });
|
|
162
|
-
}
|
|
163
|
-
this.pendingTurns.clear();
|
|
164
|
-
this.resolveIdleWaitersIfIdle();
|
|
165
|
-
while (this.pendingAdmissions.size > 0) {
|
|
166
|
-
await Promise.allSettled([...this.pendingAdmissions]);
|
|
167
|
-
}
|
|
126
|
+
enqueueDecision(operation) {
|
|
127
|
+
const task = this.decisionTail.then(operation, operation);
|
|
128
|
+
this.decisionTail = task.then(() => undefined, () => undefined);
|
|
129
|
+
return task;
|
|
168
130
|
}
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
131
|
+
incompatibleCodecError(candidate) {
|
|
132
|
+
for (const record of this.nativeTurns.values()) {
|
|
133
|
+
if (record.completion !== null || record.terminal !== null)
|
|
134
|
+
continue;
|
|
135
|
+
if (sameCodec(record.codec, candidate))
|
|
136
|
+
continue;
|
|
137
|
+
return unsupportedFeatureError('outputSchema', 'codex cannot submit an incompatible outputSchema while a native turn is active');
|
|
174
138
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
ensureCollector(threadId) {
|
|
142
|
+
if (this.collector !== null && this.collectorThreadId === threadId)
|
|
143
|
+
return;
|
|
144
|
+
this.collector?.dispose();
|
|
145
|
+
this.collectorThreadId = threadId;
|
|
146
|
+
this.collector = subscribeTurnCollection(this.opts.client, threadId, {
|
|
147
|
+
retainAfterTerminal: true,
|
|
148
|
+
onItemStarted: (turnId, item) => this.observeItem(turnId, item, 'started', Date.now()),
|
|
149
|
+
onItemCompleted: (turnId, item, occurredAt) => this.observeItem(turnId, item, 'completed', occurredAt),
|
|
150
|
+
onTerminal: (turnId, terminal) => this.observeTerminal(turnId, terminal),
|
|
151
|
+
onUnscopedFailure: (error) => this.failProtocol(error),
|
|
152
|
+
onProtocolViolation: (error) => this.failProtocol(error),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
bindSubmission(turnId, deferred, codec) {
|
|
156
|
+
this.unboundObservedTurnIds.delete(turnId);
|
|
157
|
+
const record = this.nativeTurns.get(turnId) ?? {
|
|
158
|
+
representative: null,
|
|
159
|
+
members: [],
|
|
180
160
|
codec,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
pendingSubmissions: 0,
|
|
185
|
-
nextSubmissionIndex: 0,
|
|
186
|
-
acceptedTurnIds: new Map(),
|
|
187
|
-
pendingNativeTurnIds: new Set(),
|
|
188
|
-
completedNativeTurns: new Map(),
|
|
189
|
-
nativeFailures: new Map(),
|
|
190
|
-
lastSubmissionError: null,
|
|
191
|
-
stopped: false,
|
|
161
|
+
completion: null,
|
|
162
|
+
terminal: null,
|
|
163
|
+
releaseAfterAdmissions: null,
|
|
192
164
|
};
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
165
|
+
if (record.representative === null && record.members.length === 0)
|
|
166
|
+
record.codec = codec;
|
|
167
|
+
record.representative ??= deferred.submission;
|
|
168
|
+
if (record.completion === null)
|
|
169
|
+
record.members.push(deferred);
|
|
170
|
+
this.nativeTurns.set(turnId, record);
|
|
171
|
+
for (const fact of this.pendingActivity.get(turnId) ?? [])
|
|
172
|
+
this.emitActivity(record, fact.activity, fact.occurredAt);
|
|
173
|
+
this.pendingActivity.delete(turnId);
|
|
174
|
+
if (this.protocolFailure !== null) {
|
|
175
|
+
this.failRecord(turnId, record, this.protocolFailure);
|
|
200
176
|
return;
|
|
201
177
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
this.
|
|
205
|
-
this.finalizeSlotIfReady(slot);
|
|
206
|
-
}
|
|
207
|
-
recordTurnStartFailure(slot, error) {
|
|
208
|
-
slot.pendingSubmissions = Math.max(0, slot.pendingSubmissions - 1);
|
|
209
|
-
slot.lastSubmissionError = error;
|
|
210
|
-
this.finalizeSlotIfReady(slot);
|
|
178
|
+
if (record.completion !== null)
|
|
179
|
+
deferred.settle({ kind: 'completion', completion: record.completion });
|
|
180
|
+
this.drainTerminalOrder();
|
|
211
181
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
* is delivered with a status instead of hanging until teardown.
|
|
220
|
-
*/
|
|
221
|
-
trackTurn(turnId, collector, slot) {
|
|
222
|
-
if (slot.pendingNativeTurnIds.has(turnId))
|
|
182
|
+
observeTerminal(turnId, terminal) {
|
|
183
|
+
this.unboundObservedTurnIds.delete(turnId);
|
|
184
|
+
const record = this.nativeTurns.get(turnId) ?? {
|
|
185
|
+
representative: null, members: [], codec: null, completion: null, terminal: null,
|
|
186
|
+
releaseAfterAdmissions: null,
|
|
187
|
+
};
|
|
188
|
+
if (record.terminal !== null || record.completion !== null)
|
|
223
189
|
return;
|
|
224
|
-
|
|
225
|
-
this.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
catch (err) {
|
|
240
|
-
pending.nativeFailures.set(turnId, asError(err));
|
|
241
|
-
this.finalizeSlotIfReady(pending);
|
|
190
|
+
record.terminal = terminal;
|
|
191
|
+
record.releaseAfterAdmissions = new Set(this.inFlightNativeAdmissions);
|
|
192
|
+
this.nativeTurns.set(turnId, record);
|
|
193
|
+
this.terminalOrder.push(turnId);
|
|
194
|
+
this.drainTerminalOrder();
|
|
195
|
+
}
|
|
196
|
+
drainTerminalOrder() {
|
|
197
|
+
while (this.terminalOrder.length > 0) {
|
|
198
|
+
const turnId = this.terminalOrder[0];
|
|
199
|
+
const record = this.nativeTurns.get(turnId);
|
|
200
|
+
if (record === undefined || record.terminal === null)
|
|
201
|
+
return;
|
|
202
|
+
if (record.representative === null) {
|
|
203
|
+
if (this.pendingAdmissions.size > 0)
|
|
242
204
|
return;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
this.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (this.pendingTurns.delete(turnId)) {
|
|
251
|
-
slot.pendingNativeTurnIds.delete(turnId);
|
|
252
|
-
slot.nativeFailures.set(turnId, err instanceof Error ? err : new Error(String(err)));
|
|
253
|
-
this.finalizeSlotIfReady(slot);
|
|
205
|
+
this.terminalOrder.shift();
|
|
206
|
+
this.nativeTurns.delete(turnId);
|
|
207
|
+
this.pendingActivity.delete(turnId);
|
|
208
|
+
this.unboundObservedTurnIds.delete(turnId);
|
|
209
|
+
this.collector?.releaseTurn(turnId);
|
|
210
|
+
this.log('warn', `dropping native terminal ${turnId} without an accepted submission`);
|
|
211
|
+
continue;
|
|
254
212
|
}
|
|
255
|
-
|
|
213
|
+
this.terminalOrder.shift();
|
|
214
|
+
this.finalize(turnId, record, record.terminal);
|
|
215
|
+
}
|
|
256
216
|
}
|
|
257
|
-
|
|
258
|
-
if (
|
|
259
|
-
slot.pendingSubmissions !== 0 ||
|
|
260
|
-
slot.pendingNativeTurnIds.size !== 0) {
|
|
217
|
+
finalize(turnId, record, terminal) {
|
|
218
|
+
if (record.completion !== null)
|
|
261
219
|
return;
|
|
262
|
-
|
|
263
|
-
const accepted = [...slot.acceptedTurnIds.entries()]
|
|
264
|
-
.sort(([left], [right]) => left - right);
|
|
265
|
-
if (accepted.length === 0 && slot.lastSubmissionError === null)
|
|
220
|
+
if (record.representative === null)
|
|
266
221
|
return;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
slot.codec = null;
|
|
271
|
-
const nativeFailure = accepted
|
|
272
|
-
.map(([, turnId]) => slot.nativeFailures.get(turnId))
|
|
273
|
-
.find((error) => error !== undefined);
|
|
274
|
-
if (nativeFailure !== undefined) {
|
|
275
|
-
slot.settle({ status: 'failed', error: nativeFailure });
|
|
276
|
-
}
|
|
277
|
-
else if (accepted.length === 0) {
|
|
278
|
-
slot.settle({ status: 'failed', error: slot.lastSubmissionError });
|
|
222
|
+
let completion;
|
|
223
|
+
if (terminal instanceof Error) {
|
|
224
|
+
completion = Object.freeze({ status: 'failed', displaySubmission: record.representative, error: terminal });
|
|
279
225
|
}
|
|
280
226
|
else {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
227
|
+
let completedTurn = terminal;
|
|
228
|
+
try {
|
|
229
|
+
if (record.codec !== null)
|
|
230
|
+
completedTurn = restoreCollectedTurn(terminal, record.codec);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
completion = Object.freeze({ status: 'failed', displaySubmission: record.representative, error: asError(error) });
|
|
234
|
+
record.completion = completion;
|
|
235
|
+
for (const member of record.members)
|
|
236
|
+
member.settle({ kind: 'completion', completion });
|
|
237
|
+
record.members.length = 0;
|
|
238
|
+
record.terminal = null;
|
|
239
|
+
this.releaseRecordIfReady(turnId, record);
|
|
240
|
+
this.resolveIdleWaitersIfIdle();
|
|
241
|
+
return;
|
|
285
242
|
}
|
|
286
|
-
this.opts.onTurnCompleted?.(
|
|
287
|
-
|
|
243
|
+
this.opts.onTurnCompleted?.(completedTurn);
|
|
244
|
+
completion = Object.freeze({
|
|
288
245
|
status: 'completed',
|
|
289
|
-
|
|
246
|
+
displaySubmission: record.representative,
|
|
247
|
+
resultText: extractAssistantText(completedTurn),
|
|
290
248
|
truncated: false,
|
|
291
249
|
});
|
|
292
250
|
}
|
|
251
|
+
record.completion = completion;
|
|
252
|
+
for (const member of record.members)
|
|
253
|
+
member.settle({ kind: 'completion', completion });
|
|
254
|
+
record.members.length = 0;
|
|
255
|
+
record.terminal = null;
|
|
256
|
+
this.releaseRecordIfReady(turnId, record);
|
|
293
257
|
this.resolveIdleWaitersIfIdle();
|
|
294
258
|
}
|
|
259
|
+
failProtocol(error) {
|
|
260
|
+
this.protocolFailure ??= error;
|
|
261
|
+
this.log('error', error.message, error);
|
|
262
|
+
this.terminalOrder.length = 0;
|
|
263
|
+
for (const [turnId, record] of this.nativeTurns) {
|
|
264
|
+
if (record.completion !== null)
|
|
265
|
+
continue;
|
|
266
|
+
this.failRecord(turnId, record, this.protocolFailure);
|
|
267
|
+
}
|
|
268
|
+
this.resolveIdleWaitersIfIdle();
|
|
269
|
+
}
|
|
270
|
+
failRecord(turnId, record, error) {
|
|
271
|
+
for (const member of record.members)
|
|
272
|
+
member.settle({ kind: 'failed', error });
|
|
273
|
+
record.members.length = 0;
|
|
274
|
+
this.nativeTurns.delete(turnId);
|
|
275
|
+
this.pendingActivity.delete(turnId);
|
|
276
|
+
this.unboundObservedTurnIds.delete(turnId);
|
|
277
|
+
this.collector?.releaseTurn(turnId);
|
|
278
|
+
}
|
|
279
|
+
releaseCompletedRecords(admissionId) {
|
|
280
|
+
for (const [turnId, record] of this.nativeTurns) {
|
|
281
|
+
record.releaseAfterAdmissions?.delete(admissionId);
|
|
282
|
+
this.releaseRecordIfReady(turnId, record);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
releaseRecordIfReady(turnId, record) {
|
|
286
|
+
if (record.completion === null || (record.releaseAfterAdmissions?.size ?? 0) > 0)
|
|
287
|
+
return;
|
|
288
|
+
this.nativeTurns.delete(turnId);
|
|
289
|
+
this.pendingActivity.delete(turnId);
|
|
290
|
+
this.unboundObservedTurnIds.delete(turnId);
|
|
291
|
+
this.collector?.releaseTurn(turnId);
|
|
292
|
+
}
|
|
293
|
+
dropOrphanActivityIfIdle() {
|
|
294
|
+
if (this.inFlightNativeAdmissions.size !== 0)
|
|
295
|
+
return;
|
|
296
|
+
for (const turnId of this.unboundObservedTurnIds) {
|
|
297
|
+
if (this.nativeTurns.has(turnId))
|
|
298
|
+
continue;
|
|
299
|
+
this.pendingActivity.delete(turnId);
|
|
300
|
+
this.collector?.releaseTurn(turnId);
|
|
301
|
+
}
|
|
302
|
+
this.unboundObservedTurnIds.clear();
|
|
303
|
+
}
|
|
304
|
+
observeItem(turnId, item, phase, occurredAt) {
|
|
305
|
+
const record = this.nativeTurns.get(turnId);
|
|
306
|
+
if (record === undefined || record.representative === null) {
|
|
307
|
+
this.unboundObservedTurnIds.add(turnId);
|
|
308
|
+
if (this.inFlightNativeAdmissions.size === 0) {
|
|
309
|
+
this.dropOrphanActivityIfIdle();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const activity = itemActivity(turnId, item, phase);
|
|
313
|
+
if (activity === null)
|
|
314
|
+
return;
|
|
315
|
+
const pending = this.pendingActivity.get(turnId) ?? [];
|
|
316
|
+
pending.push({ activity, occurredAt });
|
|
317
|
+
this.pendingActivity.set(turnId, pending);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const activity = itemActivity(turnId, item, phase);
|
|
321
|
+
if (activity === null)
|
|
322
|
+
return;
|
|
323
|
+
this.emitActivity(record, activity, occurredAt);
|
|
324
|
+
}
|
|
325
|
+
emitActivity(record, activity, occurredAt) {
|
|
326
|
+
if (record.representative === null)
|
|
327
|
+
return;
|
|
328
|
+
try {
|
|
329
|
+
this.opts.activitySink(Object.freeze({ submission: record.representative, activity: Object.freeze(activity), occurredAt }));
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
this.log('warn', 'codex activity projection failed', error);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
295
335
|
reserveSource(sourceId, committed, order, pending, operation) {
|
|
296
336
|
if (sourceId === undefined || sourceId === '')
|
|
297
337
|
return operation();
|
|
298
|
-
if (committed.has(sourceId))
|
|
338
|
+
if (committed.has(sourceId))
|
|
299
339
|
return Promise.resolve({ status: 'duplicate' });
|
|
300
|
-
}
|
|
301
340
|
const existing = pending.get(sourceId);
|
|
302
341
|
if (existing !== undefined)
|
|
303
342
|
return existing;
|
|
304
|
-
const task = Promise.resolve()
|
|
305
|
-
.then(operation)
|
|
306
|
-
.catch((error) => ({
|
|
307
|
-
status: 'ambiguous',
|
|
308
|
-
error: asError(error),
|
|
309
|
-
}));
|
|
343
|
+
const task = Promise.resolve().then(operation).catch((error) => ({ status: 'ambiguous', error: asError(error) }));
|
|
310
344
|
pending.set(sourceId, task);
|
|
311
345
|
void task.then((admission) => {
|
|
312
|
-
if (admission.status === 'submitted' ||
|
|
313
|
-
admission.status === 'ambiguous') {
|
|
346
|
+
if (admission.status === 'submitted' || admission.status === 'ambiguous') {
|
|
314
347
|
committed.add(sourceId);
|
|
315
348
|
order.push(sourceId);
|
|
316
349
|
while (order.length > this.messageIdDedupeWindow) {
|
|
@@ -328,6 +361,8 @@ export class TurnManager {
|
|
|
328
361
|
this.pendingAdmissions.add(admission);
|
|
329
362
|
void admission.finally(() => {
|
|
330
363
|
this.pendingAdmissions.delete(admission);
|
|
364
|
+
this.drainTerminalOrder();
|
|
365
|
+
this.resolveIdleWaitersIfIdle();
|
|
331
366
|
}).catch(() => undefined);
|
|
332
367
|
return admission;
|
|
333
368
|
}
|
|
@@ -340,58 +375,83 @@ export class TurnManager {
|
|
|
340
375
|
resolve?.();
|
|
341
376
|
}
|
|
342
377
|
}
|
|
343
|
-
function
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
378
|
+
function sameCodec(left, right) {
|
|
379
|
+
return left === null ? right === null : right !== null && left.fingerprint === right.fingerprint;
|
|
380
|
+
}
|
|
381
|
+
function createRuntimeSubmission() {
|
|
382
|
+
let resolve;
|
|
383
|
+
let settled = false;
|
|
384
|
+
const submission = Object.freeze({ settled: new Promise((value) => { resolve = value; }) });
|
|
385
|
+
return { submission, settle(settlement) { if (settled)
|
|
386
|
+
return false; settled = true; resolve(settlement); return true; } };
|
|
387
|
+
}
|
|
388
|
+
function itemActivity(turnId, item, phase) {
|
|
389
|
+
const itemId = typeof item.id === 'string' && item.id !== '' ? item.id : null;
|
|
390
|
+
if (itemId === null)
|
|
391
|
+
return null;
|
|
392
|
+
if (item.type === 'agentMessage') {
|
|
393
|
+
if (phase !== 'completed' || typeof item.text !== 'string' || item.text === '')
|
|
394
|
+
return null;
|
|
395
|
+
return { kind: 'assistant.message', id: `${turnId}:${itemId}:completed`, text: item.text, truncated: false };
|
|
396
|
+
}
|
|
397
|
+
const toolName = toolNameFor(item);
|
|
398
|
+
if (toolName === null)
|
|
399
|
+
return null;
|
|
400
|
+
const failed = phase === 'completed' && (item['status'] === 'failed' || item['error'] != null);
|
|
401
|
+
return {
|
|
402
|
+
kind: 'tool.call',
|
|
403
|
+
id: `${turnId}:${itemId}:${phase}`,
|
|
404
|
+
callId: itemId,
|
|
405
|
+
toolName,
|
|
406
|
+
status: phase === 'started' ? 'started' : failed ? 'failed' : 'completed',
|
|
407
|
+
arguments: toJsonValue(item['arguments'] ?? item['input'] ?? item['command'] ?? null),
|
|
408
|
+
result: phase === 'completed' ? toJsonValue(item['result'] ?? item['output'] ?? item['aggregatedOutput'] ?? null) : null,
|
|
409
|
+
error: failed ? String(item['error'] ?? 'tool call failed') : null,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
function toolNameFor(item) {
|
|
413
|
+
if (item.type === 'commandExecution')
|
|
414
|
+
return 'exec_command';
|
|
415
|
+
if (item.type === 'fileChange')
|
|
416
|
+
return 'apply_patch';
|
|
417
|
+
if (item.type === 'mcpToolCall') {
|
|
418
|
+
const server = typeof item['server'] === 'string' ? item['server'] : null;
|
|
419
|
+
const tool = typeof item['tool'] === 'string' ? item['tool'] : null;
|
|
420
|
+
return server !== null && tool !== null ? `${server}.${tool}` : null;
|
|
421
|
+
}
|
|
422
|
+
if (typeof item['name'] === 'string')
|
|
423
|
+
return item['name'];
|
|
424
|
+
if (typeof item['tool'] === 'string')
|
|
425
|
+
return item['tool'];
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
function toJsonValue(value) {
|
|
429
|
+
if (value === undefined)
|
|
430
|
+
return null;
|
|
431
|
+
try {
|
|
432
|
+
return JSON.parse(JSON.stringify(value));
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
return String(value);
|
|
350
436
|
}
|
|
351
|
-
throw unsupportedFeatureError('outputSchema', active === null
|
|
352
|
-
? 'codex cannot fold structured output into an active unstructured turn'
|
|
353
|
-
: 'codex cannot fold unstructured input into an active structured turn');
|
|
354
437
|
}
|
|
355
438
|
function restoreCollectedTurn(turn, codec) {
|
|
356
439
|
const text = extractAssistantText(turn);
|
|
357
|
-
if (text === null)
|
|
358
|
-
throw new Error('codex outputSchema restoration failed: completed turn has no '
|
|
359
|
-
'assistant JSON text');
|
|
360
|
-
}
|
|
440
|
+
if (text === null)
|
|
441
|
+
throw new Error('codex outputSchema restoration failed: completed turn has no assistant JSON text');
|
|
361
442
|
const restoredText = codec.restore(text);
|
|
362
443
|
let replaced = false;
|
|
363
444
|
const items = [...turn.items].reverse().map((item) => {
|
|
364
|
-
if (replaced || item.type !== 'agentMessage' || item.text !== text)
|
|
445
|
+
if (replaced || item.type !== 'agentMessage' || item.text !== text)
|
|
365
446
|
return item;
|
|
366
|
-
}
|
|
367
447
|
replaced = true;
|
|
368
448
|
return { ...item, text: restoredText };
|
|
369
449
|
}).reverse();
|
|
370
|
-
if (!replaced)
|
|
450
|
+
if (!replaced)
|
|
371
451
|
throw new Error('codex outputSchema restoration failed: assistant JSON text was not found');
|
|
372
|
-
}
|
|
373
452
|
return { ...turn, items };
|
|
374
453
|
}
|
|
375
454
|
function asError(error) {
|
|
376
455
|
return error instanceof Error ? error : new Error(String(error));
|
|
377
456
|
}
|
|
378
|
-
function createRuntimeTurn() {
|
|
379
|
-
let resolve;
|
|
380
|
-
let settled = false;
|
|
381
|
-
const turn = Object.freeze({
|
|
382
|
-
settled: new Promise((value) => {
|
|
383
|
-
resolve = value;
|
|
384
|
-
}),
|
|
385
|
-
});
|
|
386
|
-
return {
|
|
387
|
-
turn,
|
|
388
|
-
settle(outcome) {
|
|
389
|
-
if (settled)
|
|
390
|
-
return false;
|
|
391
|
-
settled = true;
|
|
392
|
-
resolve(outcome);
|
|
393
|
-
return true;
|
|
394
|
-
},
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
457
|
//# sourceMappingURL=turn-manager.js.map
|