@kbediako/codex-orchestrator 0.1.3 → 0.1.4
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 +6 -1
- package/dist/bin/codex-orchestrator.js +38 -0
- package/dist/orchestrator/src/cli/config/delegationConfig.js +485 -0
- package/dist/orchestrator/src/cli/control/confirmations.js +262 -0
- package/dist/orchestrator/src/cli/control/controlServer.js +1476 -0
- package/dist/orchestrator/src/cli/control/controlState.js +46 -0
- package/dist/orchestrator/src/cli/control/controlWatcher.js +222 -0
- package/dist/orchestrator/src/cli/control/delegationTokens.js +62 -0
- package/dist/orchestrator/src/cli/control/questions.js +106 -0
- package/dist/orchestrator/src/cli/delegationServer.js +1368 -0
- package/dist/orchestrator/src/cli/events/runEventStream.js +246 -0
- package/dist/orchestrator/src/cli/exec/context.js +4 -1
- package/dist/orchestrator/src/cli/exec/stageRunner.js +30 -5
- package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +377 -147
- package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +3 -5
- package/dist/orchestrator/src/cli/orchestrator.js +217 -40
- package/dist/orchestrator/src/cli/rlmRunner.js +26 -3
- package/dist/orchestrator/src/cli/run/manifestPersister.js +33 -3
- package/dist/orchestrator/src/cli/run/runPaths.js +14 -0
- package/dist/orchestrator/src/cli/services/commandRunner.js +1 -1
- package/dist/orchestrator/src/cli/utils/devtools.js +33 -2
- package/dist/orchestrator/src/persistence/ExperienceStore.js +113 -46
- package/dist/orchestrator/src/persistence/PersistenceCoordinator.js +8 -8
- package/dist/orchestrator/src/persistence/TaskStateStore.js +2 -1
- package/dist/orchestrator/src/persistence/lockFile.js +26 -1
- package/dist/orchestrator/src/sync/CloudSyncWorker.js +17 -4
- package/dist/packages/orchestrator/src/telemetry/otel-exporter.js +21 -0
- package/package.json +3 -1
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { createWriteStream } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { isoTimestamp } from '../utils/time.js';
|
|
4
|
+
const SCHEMA_VERSION = 1;
|
|
5
|
+
export class RunEventStream {
|
|
6
|
+
seq;
|
|
7
|
+
stream;
|
|
8
|
+
now;
|
|
9
|
+
taskId;
|
|
10
|
+
runId;
|
|
11
|
+
writeQueue = Promise.resolve();
|
|
12
|
+
streamError = null;
|
|
13
|
+
constructor(stream, options) {
|
|
14
|
+
this.stream = stream;
|
|
15
|
+
this.taskId = options.taskId;
|
|
16
|
+
this.runId = options.runId;
|
|
17
|
+
this.now = options.now;
|
|
18
|
+
this.seq = options.initialSeq;
|
|
19
|
+
this.stream.on('error', (error) => {
|
|
20
|
+
this.streamError = error;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
static async create(options) {
|
|
24
|
+
await mkdir(options.paths.runDir, { recursive: true });
|
|
25
|
+
const initialSeq = await readLastSeq(options.paths.eventsPath);
|
|
26
|
+
const stream = createWriteStream(options.paths.eventsPath, { flags: 'a' });
|
|
27
|
+
return new RunEventStream(stream, {
|
|
28
|
+
taskId: options.taskId,
|
|
29
|
+
runId: options.runId,
|
|
30
|
+
now: options.now ?? isoTimestamp,
|
|
31
|
+
initialSeq
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async append(input) {
|
|
35
|
+
if (this.streamError) {
|
|
36
|
+
throw this.streamError;
|
|
37
|
+
}
|
|
38
|
+
const entry = {
|
|
39
|
+
schema_version: SCHEMA_VERSION,
|
|
40
|
+
seq: this.nextSeq(),
|
|
41
|
+
timestamp: input.timestamp ?? this.now(),
|
|
42
|
+
task_id: this.taskId,
|
|
43
|
+
run_id: this.runId,
|
|
44
|
+
event: input.event,
|
|
45
|
+
actor: input.actor ?? 'runner',
|
|
46
|
+
payload: input.payload ?? null
|
|
47
|
+
};
|
|
48
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
49
|
+
this.writeQueue = this.writeQueue
|
|
50
|
+
.then(() => new Promise((resolve, reject) => {
|
|
51
|
+
this.stream.write(line, (error) => {
|
|
52
|
+
if (error) {
|
|
53
|
+
reject(error);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
resolve();
|
|
57
|
+
});
|
|
58
|
+
}))
|
|
59
|
+
.catch((error) => {
|
|
60
|
+
this.streamError = error;
|
|
61
|
+
throw error;
|
|
62
|
+
});
|
|
63
|
+
await this.writeQueue;
|
|
64
|
+
if (this.streamError) {
|
|
65
|
+
throw this.streamError;
|
|
66
|
+
}
|
|
67
|
+
return entry;
|
|
68
|
+
}
|
|
69
|
+
async close() {
|
|
70
|
+
try {
|
|
71
|
+
await this.writeQueue;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Allow close to proceed even if a prior write failed.
|
|
75
|
+
}
|
|
76
|
+
if (this.streamError) {
|
|
77
|
+
this.stream.destroy();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
await new Promise((resolve, reject) => {
|
|
81
|
+
const onFinish = () => {
|
|
82
|
+
cleanup();
|
|
83
|
+
resolve();
|
|
84
|
+
};
|
|
85
|
+
const onError = (error) => {
|
|
86
|
+
cleanup();
|
|
87
|
+
this.stream.destroy();
|
|
88
|
+
reject(error);
|
|
89
|
+
};
|
|
90
|
+
const cleanup = () => {
|
|
91
|
+
this.stream.off('finish', onFinish);
|
|
92
|
+
this.stream.off('error', onError);
|
|
93
|
+
};
|
|
94
|
+
this.stream.once('finish', onFinish);
|
|
95
|
+
this.stream.once('error', onError);
|
|
96
|
+
this.stream.end();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
nextSeq() {
|
|
100
|
+
this.seq += 1;
|
|
101
|
+
return this.seq;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function attachRunEventAdapter(emitter, stream, onEntry, onError) {
|
|
105
|
+
return emitter.on('*', (event) => {
|
|
106
|
+
const mapped = mapRunEvent(event);
|
|
107
|
+
if (!mapped) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
void stream
|
|
111
|
+
.append(mapped)
|
|
112
|
+
.then((entry) => {
|
|
113
|
+
onEntry?.(entry);
|
|
114
|
+
})
|
|
115
|
+
.catch((error) => {
|
|
116
|
+
onError?.(error, mapped);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function mapRunEvent(event) {
|
|
121
|
+
// The public event stream uses "step_*" naming for pipeline stages to match UI terminology.
|
|
122
|
+
switch (event.type) {
|
|
123
|
+
case 'run:started':
|
|
124
|
+
return {
|
|
125
|
+
event: 'run_started',
|
|
126
|
+
actor: 'runner',
|
|
127
|
+
payload: {
|
|
128
|
+
pipeline_id: event.pipelineId,
|
|
129
|
+
pipeline_title: event.pipelineTitle,
|
|
130
|
+
status: event.status,
|
|
131
|
+
manifest_path: event.manifestPath,
|
|
132
|
+
log_path: event.logPath
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
case 'stage:started':
|
|
136
|
+
return {
|
|
137
|
+
event: 'step_started',
|
|
138
|
+
actor: 'runner',
|
|
139
|
+
payload: {
|
|
140
|
+
stage_id: event.stageId,
|
|
141
|
+
stage_index: event.stageIndex,
|
|
142
|
+
title: event.title,
|
|
143
|
+
kind: event.kind,
|
|
144
|
+
status: event.status,
|
|
145
|
+
log_path: event.logPath
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
case 'stage:completed':
|
|
149
|
+
return {
|
|
150
|
+
event: event.status === 'failed' ? 'step_failed' : 'step_completed',
|
|
151
|
+
actor: 'runner',
|
|
152
|
+
payload: {
|
|
153
|
+
stage_id: event.stageId,
|
|
154
|
+
stage_index: event.stageIndex,
|
|
155
|
+
title: event.title,
|
|
156
|
+
kind: event.kind,
|
|
157
|
+
status: event.status,
|
|
158
|
+
exit_code: event.exitCode,
|
|
159
|
+
summary: event.summary,
|
|
160
|
+
log_path: event.logPath,
|
|
161
|
+
sub_run_id: event.subRunId ?? null
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
case 'run:completed':
|
|
165
|
+
return {
|
|
166
|
+
event: 'run_completed',
|
|
167
|
+
actor: 'runner',
|
|
168
|
+
payload: {
|
|
169
|
+
pipeline_id: event.pipelineId,
|
|
170
|
+
status: event.status,
|
|
171
|
+
manifest_path: event.manifestPath,
|
|
172
|
+
run_summary_path: event.runSummaryPath,
|
|
173
|
+
metrics_path: event.metricsPath,
|
|
174
|
+
summary: event.summary
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
case 'run:error':
|
|
178
|
+
return {
|
|
179
|
+
event: 'run_failed',
|
|
180
|
+
actor: 'runner',
|
|
181
|
+
payload: {
|
|
182
|
+
pipeline_id: event.pipelineId,
|
|
183
|
+
message: event.message,
|
|
184
|
+
stage_id: event.stageId ?? null
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
case 'log':
|
|
188
|
+
return {
|
|
189
|
+
event: 'agent_message',
|
|
190
|
+
actor: 'runner',
|
|
191
|
+
payload: {
|
|
192
|
+
message: event.message,
|
|
193
|
+
level: event.level,
|
|
194
|
+
stage_id: event.stageId ?? null,
|
|
195
|
+
stage_index: event.stageIndex ?? null,
|
|
196
|
+
source: event.source ?? null
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
case 'tool:call':
|
|
200
|
+
return {
|
|
201
|
+
event: 'tool_called',
|
|
202
|
+
actor: 'runner',
|
|
203
|
+
payload: {
|
|
204
|
+
tool_name: event.toolName,
|
|
205
|
+
status: event.status,
|
|
206
|
+
message: event.message ?? null,
|
|
207
|
+
attempt: event.attempt ?? null,
|
|
208
|
+
stage_id: event.stageId ?? null,
|
|
209
|
+
stage_index: event.stageIndex ?? null
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
default:
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function readLastSeq(pathname) {
|
|
217
|
+
try {
|
|
218
|
+
const raw = await readFile(pathname, 'utf8');
|
|
219
|
+
if (!raw.trim()) {
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
const lines = raw.split('\n');
|
|
223
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
224
|
+
const line = lines[index]?.trim();
|
|
225
|
+
if (!line) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const parsed = JSON.parse(line);
|
|
230
|
+
if (typeof parsed.seq === 'number' && Number.isFinite(parsed.seq)) {
|
|
231
|
+
return parsed.seq;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (error.code === 'ENOENT') {
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -2,9 +2,11 @@ import process from 'node:process';
|
|
|
2
2
|
import { bootstrapManifest } from '../run/manifest.js';
|
|
3
3
|
import { generateRunId } from '../utils/runId.js';
|
|
4
4
|
import { ExperienceStore } from '../../persistence/ExperienceStore.js';
|
|
5
|
+
import { EnvUtils } from '../../../../packages/shared/config/index.js';
|
|
5
6
|
import { createTelemetrySink } from '../../../../packages/orchestrator/src/telemetry/otel-exporter.js';
|
|
6
7
|
import { createNotificationSink } from '../../../../packages/orchestrator/src/notifications/index.js';
|
|
7
8
|
import { ManifestPersister, persistManifest } from '../run/manifestPersister.js';
|
|
9
|
+
const MAX_TELEMETRY_EVENTS = EnvUtils.getInt('CODEX_ORCHESTRATOR_TELEMETRY_MAX_EVENTS', 1000);
|
|
8
10
|
export async function bootstrapExecContext(context, invocation) {
|
|
9
11
|
const argv = [invocation.command, ...(invocation.args ?? [])];
|
|
10
12
|
const shellCommand = buildShellCommand(argv);
|
|
@@ -38,7 +40,8 @@ export async function bootstrapExecContext(context, invocation) {
|
|
|
38
40
|
await persistManifest(paths, manifest, persister, { force: true });
|
|
39
41
|
const telemetrySink = context.telemetrySink ?? createTelemetrySink({
|
|
40
42
|
endpoint: invocation.otelEndpoint,
|
|
41
|
-
enabled: Boolean(invocation.otelEndpoint)
|
|
43
|
+
enabled: Boolean(invocation.otelEndpoint),
|
|
44
|
+
maxQueueSize: MAX_TELEMETRY_EVENTS
|
|
42
45
|
});
|
|
43
46
|
const envNotifications = parseNotificationEnv(process.env.CODEX_ORCHESTRATOR_NOTIFY);
|
|
44
47
|
const notificationSink = context.notificationSink ??
|
|
@@ -1,20 +1,45 @@
|
|
|
1
|
-
import { runCommandStage } from '../services/commandRunner.js';
|
|
1
|
+
import { runCommandStage, MAX_CAPTURED_CHUNK_EVENTS } from '../services/commandRunner.js';
|
|
2
2
|
import { serializeExecEvent } from '../../../../packages/shared/events/serializer.js';
|
|
3
3
|
import { sanitizeToolRunRecord } from '../../../../packages/shared/manifest/writer.js';
|
|
4
4
|
export async function runExecStage(context) {
|
|
5
5
|
let runResultSummary = null;
|
|
6
6
|
let toolRecord = null;
|
|
7
|
+
const maxChunkEvents = MAX_CAPTURED_CHUNK_EVENTS;
|
|
8
|
+
let capturedChunkEvents = 0;
|
|
9
|
+
const shouldCaptureEvent = (event) => {
|
|
10
|
+
if (maxChunkEvents <= 0) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
if (event.type !== 'exec:chunk') {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
if (capturedChunkEvents >= maxChunkEvents) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
capturedChunkEvents += 1;
|
|
20
|
+
return true;
|
|
21
|
+
};
|
|
7
22
|
const hooks = {
|
|
8
23
|
onEvent: (event) => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
24
|
+
const captureEvent = shouldCaptureEvent(event);
|
|
25
|
+
let serialized = null;
|
|
26
|
+
const getSerialized = () => {
|
|
27
|
+
if (!serialized) {
|
|
28
|
+
serialized = serializeExecEvent(event);
|
|
29
|
+
}
|
|
30
|
+
return serialized;
|
|
31
|
+
};
|
|
12
32
|
if (context.outputMode === 'jsonl' && context.jsonlWriter) {
|
|
13
|
-
context.jsonlWriter(
|
|
33
|
+
context.jsonlWriter(getSerialized());
|
|
14
34
|
}
|
|
15
35
|
else if (context.outputMode === 'interactive') {
|
|
16
36
|
streamInteractive(context.stdout, context.stderr, event);
|
|
17
37
|
}
|
|
38
|
+
if (captureEvent) {
|
|
39
|
+
context.execEvents.push(event);
|
|
40
|
+
const payload = getSerialized();
|
|
41
|
+
context.telemetryTasks.push(Promise.resolve(context.telemetrySink.record(payload)).then(() => undefined));
|
|
42
|
+
}
|
|
18
43
|
},
|
|
19
44
|
onResult: (result) => {
|
|
20
45
|
runResultSummary = {
|