@excitedjs/agent-runtime-codex 0.3.5-beta.168 → 0.4.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/README.md +39 -1
- package/dist/events.d.ts +8 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +119 -47
- package/dist/events.js.map +1 -1
- package/dist/provider.d.ts +3 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +6 -3
- package/dist/provider.js.map +1 -1
- package/dist/rpc.d.ts +1 -0
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +9 -1
- package/dist/rpc.js.map +1 -1
- package/dist/runtime.d.ts +15 -7
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +242 -80
- package/dist/runtime.js.map +1 -1
- package/dist/supervisor.d.ts +7 -0
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +99 -43
- package/dist/supervisor.js.map +1 -1
- package/dist/transcript/budget.d.ts +8 -0
- package/dist/transcript/budget.d.ts.map +1 -0
- package/dist/transcript/budget.js +9 -0
- package/dist/transcript/budget.js.map +1 -0
- package/dist/transcript/cursor.d.ts +23 -0
- package/dist/transcript/cursor.d.ts.map +1 -0
- package/dist/transcript/cursor.js +57 -0
- package/dist/transcript/cursor.js.map +1 -0
- package/dist/transcript/error.d.ts +9 -0
- package/dist/transcript/error.d.ts.map +1 -0
- package/dist/transcript/error.js +9 -0
- package/dist/transcript/error.js.map +1 -0
- package/dist/transcript/opened-file.d.ts +10 -0
- package/dist/transcript/opened-file.d.ts.map +1 -0
- package/dist/transcript/opened-file.js +61 -0
- package/dist/transcript/opened-file.js.map +1 -0
- package/dist/transcript/path.d.ts +28 -0
- package/dist/transcript/path.d.ts.map +1 -0
- package/dist/transcript/path.js +422 -0
- package/dist/transcript/path.js.map +1 -0
- package/dist/transcript/projection.d.ts +6 -0
- package/dist/transcript/projection.d.ts.map +1 -0
- package/dist/transcript/projection.js +170 -0
- package/dist/transcript/projection.js.map +1 -0
- package/dist/transcript/reader.d.ts +7 -0
- package/dist/transcript/reader.d.ts.map +1 -0
- package/dist/transcript/reader.js +421 -0
- package/dist/transcript/reader.js.map +1 -0
- package/dist/turn-manager.d.ts +34 -56
- package/dist/turn-manager.d.ts.map +1 -1
- package/dist/turn-manager.js +369 -303
- package/dist/turn-manager.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -4
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;
|
|
@@ -16,332 +7,364 @@ export class TurnManager {
|
|
|
16
7
|
seenMessageIdOrder = [];
|
|
17
8
|
seenTextInputIds = new Set();
|
|
18
9
|
seenTextInputIdOrder = [];
|
|
10
|
+
pendingMessageIds = new Map();
|
|
11
|
+
pendingTextInputIds = new Map();
|
|
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();
|
|
19
23
|
stopped = false;
|
|
20
|
-
activeTurnSlot = null;
|
|
21
|
-
activeTurnId = null;
|
|
22
|
-
/**
|
|
23
|
-
* Turn ids submitted to Codex that have not yet reached `turn/completed`. On
|
|
24
|
-
* `stop()` each still-pending turn is settled as `stopped` so a teammate turn
|
|
25
|
-
* interrupted by teardown is not lost.
|
|
26
|
-
*/
|
|
27
|
-
pendingTurns = new Map();
|
|
28
24
|
idlePromise = null;
|
|
29
25
|
idleResolve = null;
|
|
30
26
|
log;
|
|
31
27
|
messageIdDedupeWindow;
|
|
32
28
|
constructor(opts) {
|
|
33
29
|
this.opts = opts;
|
|
34
|
-
this.log = opts.log ?? ((
|
|
35
|
-
const prefix = `[turn-manager ${opts.dispatcherId}] ${
|
|
36
|
-
if (
|
|
37
|
-
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);
|
|
38
34
|
else
|
|
39
|
-
console.error(prefix,
|
|
35
|
+
console.error(prefix, message, error);
|
|
40
36
|
});
|
|
41
37
|
this.messageIdDedupeWindow = Math.max(0, opts.messageIdDedupeWindow ?? DEFAULT_MESSAGE_ID_DEDUPE_WINDOW);
|
|
42
38
|
}
|
|
43
39
|
isBusy() {
|
|
44
|
-
return this.
|
|
40
|
+
return this.pendingAdmissions.size > 0 || [...this.nativeTurns.values()].some((record) => record.completion === null);
|
|
45
41
|
}
|
|
46
42
|
waitIdle() {
|
|
47
43
|
if (!this.isBusy())
|
|
48
44
|
return Promise.resolve();
|
|
49
|
-
|
|
50
|
-
// is replaced with a fresh one the next time the runtime goes busy.
|
|
51
|
-
if (this.idlePromise === null) {
|
|
52
|
-
this.idlePromise = new Promise((resolve) => {
|
|
53
|
-
this.idleResolve = resolve;
|
|
54
|
-
});
|
|
55
|
-
}
|
|
45
|
+
this.idlePromise ??= new Promise((resolve) => { this.idleResolve = resolve; });
|
|
56
46
|
return this.idlePromise;
|
|
57
47
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
catch (err) {
|
|
89
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
90
|
-
this.recordTurnStartFailure(activeTurn.slot, error, activeTurn.primary);
|
|
91
|
-
this.log('error', `turn/start submission failed for message ${input.sourceId === '' ? '<none>' : input.sourceId}: ${error.message}`, error);
|
|
92
|
-
return { status: 'failed', error };
|
|
93
|
-
}
|
|
94
|
-
const turnId = this.recordTurnStartSuccess(activeTurn.slot, res.turn.id, activeTurn.primary);
|
|
95
|
-
try {
|
|
96
|
-
return {
|
|
97
|
-
status: 'submitted',
|
|
98
|
-
turnId: turnId ?? await activeTurn.slot.turnIdPromise,
|
|
99
|
-
};
|
|
48
|
+
enqueue(input) {
|
|
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>'}`))));
|
|
50
|
+
}
|
|
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' });
|
|
100
77
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
78
|
+
for (const [turnId, record] of this.nativeTurns) {
|
|
79
|
+
if (record.completion === null)
|
|
80
|
+
this.nativeTurns.delete(turnId);
|
|
104
81
|
}
|
|
82
|
+
this.terminalOrder.length = 0;
|
|
83
|
+
this.pendingActivity.clear();
|
|
84
|
+
this.unboundObservedTurnIds.clear();
|
|
85
|
+
this.resolveIdleWaitersIfIdle();
|
|
105
86
|
}
|
|
106
|
-
async
|
|
87
|
+
async submit(text, codec, description) {
|
|
107
88
|
if (this.stopped)
|
|
108
89
|
return { status: 'stopped' };
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
!this.rememberTextInputId(input.sourceId)) {
|
|
112
|
-
return { status: 'duplicate' };
|
|
113
|
-
}
|
|
90
|
+
if (this.protocolFailure !== null)
|
|
91
|
+
return { status: 'failed', error: this.protocolFailure };
|
|
114
92
|
const threadId = this.opts.getThreadId();
|
|
115
|
-
if (threadId === null)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
catch (err) {
|
|
126
|
-
const error = asError(err);
|
|
127
|
-
this.log('error', error.message, error);
|
|
128
|
-
return { status: 'failed', error };
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
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;
|
|
132
103
|
try {
|
|
133
|
-
|
|
104
|
+
response = await submitTurnStart(this.opts.client, threadId, text, this.opts.turnCwd ?? null, codec?.wireSchema);
|
|
134
105
|
}
|
|
135
|
-
catch (
|
|
136
|
-
const
|
|
137
|
-
this.log('error',
|
|
138
|
-
|
|
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 };
|
|
139
113
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
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' });
|
|
144
117
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
this.recordTurnStartFailure(activeTurn.slot, error, activeTurn.primary);
|
|
148
|
-
this.log('error', `text turn/start submission failed: ${error.message}`, error);
|
|
149
|
-
return { status: 'failed', error };
|
|
118
|
+
else {
|
|
119
|
+
this.bindSubmission(response.turn.id, deferred, codec);
|
|
150
120
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
121
|
+
this.inFlightNativeAdmissions.delete(admissionId);
|
|
122
|
+
this.releaseCompletedRecords(admissionId);
|
|
123
|
+
this.dropOrphanActivityIfIdle();
|
|
124
|
+
return { status: 'submitted', submission: deferred.submission };
|
|
125
|
+
}
|
|
126
|
+
enqueueDecision(operation) {
|
|
127
|
+
const task = this.decisionTail.then(operation, operation);
|
|
128
|
+
this.decisionTail = task.then(() => undefined, () => undefined);
|
|
129
|
+
return task;
|
|
130
|
+
}
|
|
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');
|
|
157
138
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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: [],
|
|
160
|
+
codec,
|
|
161
|
+
completion: null,
|
|
162
|
+
terminal: null,
|
|
163
|
+
releaseAfterAdmissions: null,
|
|
164
|
+
};
|
|
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);
|
|
176
|
+
return;
|
|
161
177
|
}
|
|
178
|
+
if (record.completion !== null)
|
|
179
|
+
deferred.settle({ kind: 'completion', completion: record.completion });
|
|
180
|
+
this.drainTerminalOrder();
|
|
162
181
|
}
|
|
163
|
-
|
|
164
|
-
this.
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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)
|
|
189
|
+
return;
|
|
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)
|
|
204
|
+
return;
|
|
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;
|
|
212
|
+
}
|
|
213
|
+
this.terminalOrder.shift();
|
|
214
|
+
this.finalize(turnId, record, record.terminal);
|
|
170
215
|
}
|
|
171
|
-
|
|
172
|
-
|
|
216
|
+
}
|
|
217
|
+
finalize(turnId, record, terminal) {
|
|
218
|
+
if (record.completion !== null)
|
|
219
|
+
return;
|
|
220
|
+
if (record.representative === null)
|
|
221
|
+
return;
|
|
222
|
+
let completion;
|
|
223
|
+
if (terminal instanceof Error) {
|
|
224
|
+
completion = Object.freeze({ status: 'failed', displaySubmission: record.representative, error: terminal });
|
|
173
225
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
226
|
+
else {
|
|
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;
|
|
242
|
+
}
|
|
243
|
+
this.opts.onTurnCompleted?.(completedTurn);
|
|
244
|
+
completion = Object.freeze({
|
|
245
|
+
status: 'completed',
|
|
246
|
+
displaySubmission: record.representative,
|
|
247
|
+
resultText: extractAssistantText(completedTurn),
|
|
248
|
+
truncated: false,
|
|
182
249
|
});
|
|
183
250
|
}
|
|
184
|
-
|
|
185
|
-
|
|
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);
|
|
186
257
|
this.resolveIdleWaitersIfIdle();
|
|
187
258
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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);
|
|
193
267
|
}
|
|
194
|
-
|
|
195
|
-
let rejectTurnId;
|
|
196
|
-
const turnIdPromise = new Promise((resolve, reject) => {
|
|
197
|
-
resolveTurnId = resolve;
|
|
198
|
-
rejectTurnId = reject;
|
|
199
|
-
});
|
|
200
|
-
// The primary submitter returns its own failure directly. The shared promise
|
|
201
|
-
// only exists for concurrent followers waiting for that primary turn id, so
|
|
202
|
-
// reject it without producing an unhandled rejection when there are none.
|
|
203
|
-
turnIdPromise.catch(() => undefined);
|
|
204
|
-
const slot = {
|
|
205
|
-
collector: subscribeTurnCollection(this.opts.client, threadId),
|
|
206
|
-
codec,
|
|
207
|
-
turnId: null,
|
|
208
|
-
candidateTurnId: null,
|
|
209
|
-
primaryFailed: false,
|
|
210
|
-
pendingSubmissions: 0,
|
|
211
|
-
turnIdPromise,
|
|
212
|
-
resolveTurnId,
|
|
213
|
-
rejectTurnId,
|
|
214
|
-
};
|
|
215
|
-
this.activeTurnSlot = slot;
|
|
216
|
-
return { slot, primary: true };
|
|
268
|
+
this.resolveIdleWaitersIfIdle();
|
|
217
269
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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);
|
|
229
283
|
}
|
|
230
|
-
slot.candidateTurnId ??= turnId;
|
|
231
|
-
return null;
|
|
232
284
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (slot.turnId !== null)
|
|
285
|
+
releaseRecordIfReady(turnId, record) {
|
|
286
|
+
if (record.completion === null || (record.releaseAfterAdmissions?.size ?? 0) > 0)
|
|
236
287
|
return;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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)
|
|
241
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);
|
|
242
301
|
}
|
|
243
|
-
|
|
244
|
-
if (this.activeTurnSlot === slot)
|
|
245
|
-
this.activeTurnSlot = null;
|
|
246
|
-
slot.collector.dispose();
|
|
247
|
-
slot.codec = null;
|
|
248
|
-
slot.rejectTurnId(error);
|
|
249
|
-
this.resolveIdleWaitersIfIdle();
|
|
250
|
-
}
|
|
302
|
+
this.unboundObservedTurnIds.clear();
|
|
251
303
|
}
|
|
252
|
-
|
|
253
|
-
|
|
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)
|
|
254
322
|
return;
|
|
255
|
-
|
|
256
|
-
this.trackTurn(turnId, slot.collector, slot);
|
|
257
|
-
slot.resolveTurnId(turnId);
|
|
323
|
+
this.emitActivity(record, activity, occurredAt);
|
|
258
324
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
* `turn/completed` the turn is removed from the pending set and the snapshot
|
|
262
|
-
* hook fires (which is where the runtime emits the `completed` settlement). On
|
|
263
|
-
* a terminal turn failure (collector rejects: codex `error` with
|
|
264
|
-
* `willRetry: false`, or a `turn/completed` carrying `turn.error`) the turn is
|
|
265
|
-
* settled as `failed` here, so a teammate turn that errors at the model level
|
|
266
|
-
* is delivered with a status instead of hanging until teardown.
|
|
267
|
-
*/
|
|
268
|
-
trackTurn(turnId, collector, slot) {
|
|
269
|
-
if (this.pendingTurns.has(turnId))
|
|
325
|
+
emitActivity(record, activity, occurredAt) {
|
|
326
|
+
if (record.representative === null)
|
|
270
327
|
return;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
this.resolveIdleWaitersIfIdle();
|
|
297
|
-
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
|
+
}
|
|
335
|
+
reserveSource(sourceId, committed, order, pending, operation) {
|
|
336
|
+
if (sourceId === undefined || sourceId === '')
|
|
337
|
+
return operation();
|
|
338
|
+
if (committed.has(sourceId))
|
|
339
|
+
return Promise.resolve({ status: 'duplicate' });
|
|
340
|
+
const existing = pending.get(sourceId);
|
|
341
|
+
if (existing !== undefined)
|
|
342
|
+
return existing;
|
|
343
|
+
const task = Promise.resolve().then(operation).catch((error) => ({ status: 'ambiguous', error: asError(error) }));
|
|
344
|
+
pending.set(sourceId, task);
|
|
345
|
+
void task.then((admission) => {
|
|
346
|
+
if (admission.status === 'submitted' || admission.status === 'ambiguous') {
|
|
347
|
+
committed.add(sourceId);
|
|
348
|
+
order.push(sourceId);
|
|
349
|
+
while (order.length > this.messageIdDedupeWindow) {
|
|
350
|
+
const evicted = order.shift();
|
|
351
|
+
if (evicted !== undefined)
|
|
352
|
+
committed.delete(evicted);
|
|
298
353
|
}
|
|
299
|
-
this.opts.onTurnCompleted?.(completedTurn);
|
|
300
|
-
this.resolveIdleWaitersIfIdle();
|
|
301
|
-
}
|
|
302
|
-
}, (err) => {
|
|
303
|
-
// Same mutual-exclusion guard as the completed path: only settle as
|
|
304
|
-
// `failed` if `stop()` did not already settle it as `stopped`.
|
|
305
|
-
if (this.pendingTurns.delete(turnId)) {
|
|
306
|
-
if (this.activeTurnSlot === slot)
|
|
307
|
-
this.activeTurnSlot = null;
|
|
308
|
-
if (this.activeTurnId === turnId)
|
|
309
|
-
this.activeTurnId = null;
|
|
310
|
-
this.opts.onTurnSettled?.({
|
|
311
|
-
turnId,
|
|
312
|
-
status: 'failed',
|
|
313
|
-
result: { text: null },
|
|
314
|
-
error: err instanceof Error ? err : new Error(String(err)),
|
|
315
|
-
});
|
|
316
|
-
this.resolveIdleWaitersIfIdle();
|
|
317
354
|
}
|
|
355
|
+
if (pending.get(sourceId) === task)
|
|
356
|
+
pending.delete(sourceId);
|
|
318
357
|
});
|
|
358
|
+
return task;
|
|
319
359
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const evicted = this.seenMessageIdOrder.shift();
|
|
329
|
-
if (evicted !== undefined)
|
|
330
|
-
this.seenMessageIds.delete(evicted);
|
|
331
|
-
}
|
|
332
|
-
return true;
|
|
333
|
-
}
|
|
334
|
-
rememberTextInputId(id) {
|
|
335
|
-
if (this.seenTextInputIds.has(id))
|
|
336
|
-
return false;
|
|
337
|
-
this.seenTextInputIds.add(id);
|
|
338
|
-
this.seenTextInputIdOrder.push(id);
|
|
339
|
-
while (this.seenTextInputIdOrder.length > this.messageIdDedupeWindow) {
|
|
340
|
-
const evicted = this.seenTextInputIdOrder.shift();
|
|
341
|
-
if (evicted !== undefined)
|
|
342
|
-
this.seenTextInputIds.delete(evicted);
|
|
343
|
-
}
|
|
344
|
-
return true;
|
|
360
|
+
trackAdmission(admission) {
|
|
361
|
+
this.pendingAdmissions.add(admission);
|
|
362
|
+
void admission.finally(() => {
|
|
363
|
+
this.pendingAdmissions.delete(admission);
|
|
364
|
+
this.drainTerminalOrder();
|
|
365
|
+
this.resolveIdleWaitersIfIdle();
|
|
366
|
+
}).catch(() => undefined);
|
|
367
|
+
return admission;
|
|
345
368
|
}
|
|
346
369
|
resolveIdleWaitersIfIdle() {
|
|
347
370
|
if (this.isBusy())
|
|
@@ -352,37 +375,80 @@ export class TurnManager {
|
|
|
352
375
|
resolve?.();
|
|
353
376
|
}
|
|
354
377
|
}
|
|
355
|
-
function
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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);
|
|
362
436
|
}
|
|
363
|
-
throw unsupportedFeatureError('outputSchema', active === null
|
|
364
|
-
? 'codex cannot fold structured output into an active unstructured turn'
|
|
365
|
-
: 'codex cannot fold unstructured input into an active structured turn');
|
|
366
437
|
}
|
|
367
438
|
function restoreCollectedTurn(turn, codec) {
|
|
368
439
|
const text = extractAssistantText(turn);
|
|
369
|
-
if (text === null)
|
|
370
|
-
throw new Error(
|
|
371
|
-
'completed turn has no assistant JSON text');
|
|
372
|
-
}
|
|
440
|
+
if (text === null)
|
|
441
|
+
throw new Error('codex outputSchema restoration failed: completed turn has no assistant JSON text');
|
|
373
442
|
const restoredText = codec.restore(text);
|
|
374
443
|
let replaced = false;
|
|
375
444
|
const items = [...turn.items].reverse().map((item) => {
|
|
376
|
-
if (replaced || item.type !== 'agentMessage' || item.text !== text)
|
|
445
|
+
if (replaced || item.type !== 'agentMessage' || item.text !== text)
|
|
377
446
|
return item;
|
|
378
|
-
}
|
|
379
447
|
replaced = true;
|
|
380
448
|
return { ...item, text: restoredText };
|
|
381
449
|
}).reverse();
|
|
382
|
-
if (!replaced)
|
|
383
|
-
throw new Error(
|
|
384
|
-
'assistant JSON text was not found');
|
|
385
|
-
}
|
|
450
|
+
if (!replaced)
|
|
451
|
+
throw new Error('codex outputSchema restoration failed: assistant JSON text was not found');
|
|
386
452
|
return { ...turn, items };
|
|
387
453
|
}
|
|
388
454
|
function asError(error) {
|