@kbediako/codex-orchestrator 0.1.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/LICENSE +7 -0
- package/README.md +238 -0
- package/dist/bin/codex-orchestrator.js +507 -0
- package/dist/orchestrator/src/agents/builder.js +16 -0
- package/dist/orchestrator/src/agents/index.js +4 -0
- package/dist/orchestrator/src/agents/planner.js +17 -0
- package/dist/orchestrator/src/agents/reviewer.js +13 -0
- package/dist/orchestrator/src/agents/tester.js +13 -0
- package/dist/orchestrator/src/cli/adapters/CommandBuilder.js +20 -0
- package/dist/orchestrator/src/cli/adapters/CommandPlanner.js +164 -0
- package/dist/orchestrator/src/cli/adapters/CommandReviewer.js +32 -0
- package/dist/orchestrator/src/cli/adapters/CommandTester.js +33 -0
- package/dist/orchestrator/src/cli/adapters/index.js +4 -0
- package/dist/orchestrator/src/cli/config/userConfig.js +28 -0
- package/dist/orchestrator/src/cli/doctor.js +48 -0
- package/dist/orchestrator/src/cli/events/runEvents.js +84 -0
- package/dist/orchestrator/src/cli/exec/command.js +56 -0
- package/dist/orchestrator/src/cli/exec/context.js +108 -0
- package/dist/orchestrator/src/cli/exec/experience.js +77 -0
- package/dist/orchestrator/src/cli/exec/finalization.js +140 -0
- package/dist/orchestrator/src/cli/exec/learning.js +62 -0
- package/dist/orchestrator/src/cli/exec/stageRunner.js +71 -0
- package/dist/orchestrator/src/cli/exec/summary.js +109 -0
- package/dist/orchestrator/src/cli/exec/telemetry.js +18 -0
- package/dist/orchestrator/src/cli/exec/tfgrpo.js +200 -0
- package/dist/orchestrator/src/cli/exec/tfgrpoArtifacts.js +19 -0
- package/dist/orchestrator/src/cli/exec/types.js +1 -0
- package/dist/orchestrator/src/cli/init.js +64 -0
- package/dist/orchestrator/src/cli/mcp.js +124 -0
- package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +404 -0
- package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +138 -0
- package/dist/orchestrator/src/cli/orchestrator.js +554 -0
- package/dist/orchestrator/src/cli/pipelines/defaultDiagnostics.js +32 -0
- package/dist/orchestrator/src/cli/pipelines/designReference.js +72 -0
- package/dist/orchestrator/src/cli/pipelines/hiFiDesignToolkit.js +71 -0
- package/dist/orchestrator/src/cli/pipelines/index.js +34 -0
- package/dist/orchestrator/src/cli/run/environment.js +24 -0
- package/dist/orchestrator/src/cli/run/manifest.js +367 -0
- package/dist/orchestrator/src/cli/run/manifestPersister.js +88 -0
- package/dist/orchestrator/src/cli/run/runPaths.js +30 -0
- package/dist/orchestrator/src/cli/selfCheck.js +12 -0
- package/dist/orchestrator/src/cli/services/commandRunner.js +420 -0
- package/dist/orchestrator/src/cli/services/controlPlaneService.js +107 -0
- package/dist/orchestrator/src/cli/services/execRuntime.js +69 -0
- package/dist/orchestrator/src/cli/services/pipelineResolver.js +47 -0
- package/dist/orchestrator/src/cli/services/runPreparation.js +82 -0
- package/dist/orchestrator/src/cli/services/runSummaryWriter.js +35 -0
- package/dist/orchestrator/src/cli/services/schedulerService.js +42 -0
- package/dist/orchestrator/src/cli/tasks/taskMetadata.js +19 -0
- package/dist/orchestrator/src/cli/telemetry/schema.js +8 -0
- package/dist/orchestrator/src/cli/types.js +1 -0
- package/dist/orchestrator/src/cli/ui/HudApp.js +112 -0
- package/dist/orchestrator/src/cli/ui/controller.js +26 -0
- package/dist/orchestrator/src/cli/ui/store.js +240 -0
- package/dist/orchestrator/src/cli/utils/enforcementMode.js +12 -0
- package/dist/orchestrator/src/cli/utils/fs.js +8 -0
- package/dist/orchestrator/src/cli/utils/interactive.js +25 -0
- package/dist/orchestrator/src/cli/utils/jsonlWriter.js +10 -0
- package/dist/orchestrator/src/cli/utils/optionalDeps.js +30 -0
- package/dist/orchestrator/src/cli/utils/packageInfo.js +25 -0
- package/dist/orchestrator/src/cli/utils/planFormatter.js +49 -0
- package/dist/orchestrator/src/cli/utils/runId.js +7 -0
- package/dist/orchestrator/src/cli/utils/specGuardRunner.js +26 -0
- package/dist/orchestrator/src/cli/utils/strings.js +8 -0
- package/dist/orchestrator/src/cli/utils/time.js +6 -0
- package/dist/orchestrator/src/control-plane/drift-reporter.js +109 -0
- package/dist/orchestrator/src/control-plane/index.js +3 -0
- package/dist/orchestrator/src/control-plane/request-builder.js +217 -0
- package/dist/orchestrator/src/control-plane/types.js +1 -0
- package/dist/orchestrator/src/control-plane/validator.js +50 -0
- package/dist/orchestrator/src/credentials/CredentialBroker.js +1 -0
- package/dist/orchestrator/src/events/EventBus.js +25 -0
- package/dist/orchestrator/src/learning/crystalizer.js +108 -0
- package/dist/orchestrator/src/learning/harvester.js +146 -0
- package/dist/orchestrator/src/learning/manifest.js +56 -0
- package/dist/orchestrator/src/learning/runner.js +177 -0
- package/dist/orchestrator/src/learning/validator.js +164 -0
- package/dist/orchestrator/src/logger.js +20 -0
- package/dist/orchestrator/src/manager.js +388 -0
- package/dist/orchestrator/src/persistence/ArtifactStager.js +95 -0
- package/dist/orchestrator/src/persistence/ExperienceStore.js +210 -0
- package/dist/orchestrator/src/persistence/PersistenceCoordinator.js +65 -0
- package/dist/orchestrator/src/persistence/RunManifestWriter.js +23 -0
- package/dist/orchestrator/src/persistence/TaskStateStore.js +172 -0
- package/dist/orchestrator/src/persistence/identifierGuards.js +1 -0
- package/dist/orchestrator/src/persistence/lockFile.js +26 -0
- package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +26 -0
- package/dist/orchestrator/src/persistence/sanitizeRunId.js +8 -0
- package/dist/orchestrator/src/persistence/sanitizeTaskId.js +8 -0
- package/dist/orchestrator/src/persistence/writeAtomicFile.js +4 -0
- package/dist/orchestrator/src/privacy/guard.js +111 -0
- package/dist/orchestrator/src/scheduler/index.js +1 -0
- package/dist/orchestrator/src/scheduler/plan.js +171 -0
- package/dist/orchestrator/src/scheduler/types.js +1 -0
- package/dist/orchestrator/src/sync/CloudRunsClient.js +1 -0
- package/dist/orchestrator/src/sync/CloudRunsHttpClient.js +82 -0
- package/dist/orchestrator/src/sync/CloudSyncWorker.js +206 -0
- package/dist/orchestrator/src/sync/createCloudSyncWorker.js +15 -0
- package/dist/orchestrator/src/types.js +1 -0
- package/dist/orchestrator/src/utils/atomicWrite.js +15 -0
- package/dist/orchestrator/src/utils/errorMessage.js +14 -0
- package/dist/orchestrator/src/utils/executionMode.js +69 -0
- package/dist/packages/control-plane-schemas/src/index.js +1 -0
- package/dist/packages/control-plane-schemas/src/run-request.js +548 -0
- package/dist/packages/orchestrator/src/exec/handle-service.js +203 -0
- package/dist/packages/orchestrator/src/exec/session-manager.js +147 -0
- package/dist/packages/orchestrator/src/exec/unified-exec.js +432 -0
- package/dist/packages/orchestrator/src/index.js +3 -0
- package/dist/packages/orchestrator/src/instructions/loader.js +101 -0
- package/dist/packages/orchestrator/src/instructions/promptPacks.js +151 -0
- package/dist/packages/orchestrator/src/notifications/index.js +74 -0
- package/dist/packages/orchestrator/src/telemetry/otel-exporter.js +142 -0
- package/dist/packages/orchestrator/src/tool-orchestrator.js +161 -0
- package/dist/packages/sdk-node/src/orchestrator.js +195 -0
- package/dist/packages/shared/config/designConfig.js +495 -0
- package/dist/packages/shared/config/env.js +37 -0
- package/dist/packages/shared/config/index.js +2 -0
- package/dist/packages/shared/design-artifacts/writer.js +221 -0
- package/dist/packages/shared/events/serializer.js +84 -0
- package/dist/packages/shared/events/types.js +1 -0
- package/dist/packages/shared/manifest/artifactUtils.js +36 -0
- package/dist/packages/shared/manifest/designArtifacts.js +665 -0
- package/dist/packages/shared/manifest/fileIO.js +29 -0
- package/dist/packages/shared/manifest/toolRuns.js +78 -0
- package/dist/packages/shared/manifest/toolkitArtifacts.js +223 -0
- package/dist/packages/shared/manifest/types.js +5 -0
- package/dist/packages/shared/manifest/validator.js +73 -0
- package/dist/packages/shared/manifest/writer.js +2 -0
- package/dist/packages/shared/streams/stdio.js +112 -0
- package/dist/scripts/design/pipeline/advanced-assets.js +466 -0
- package/dist/scripts/design/pipeline/componentize.js +74 -0
- package/dist/scripts/design/pipeline/context.js +34 -0
- package/dist/scripts/design/pipeline/extract.js +249 -0
- package/dist/scripts/design/pipeline/optionalDeps.js +107 -0
- package/dist/scripts/design/pipeline/prepare.js +46 -0
- package/dist/scripts/design/pipeline/reference.js +94 -0
- package/dist/scripts/design/pipeline/state.js +206 -0
- package/dist/scripts/design/pipeline/toolkit/common.js +94 -0
- package/dist/scripts/design/pipeline/toolkit/extract.js +258 -0
- package/dist/scripts/design/pipeline/toolkit/publish.js +202 -0
- package/dist/scripts/design/pipeline/toolkit/publishActions.js +12 -0
- package/dist/scripts/design/pipeline/toolkit/reference.js +846 -0
- package/dist/scripts/design/pipeline/toolkit/snapshot.js +882 -0
- package/dist/scripts/design/pipeline/toolkit/tokens.js +456 -0
- package/dist/scripts/design/pipeline/visual-regression.js +137 -0
- package/dist/scripts/design/pipeline/write-artifacts.js +61 -0
- package/package.json +97 -0
- package/schemas/manifest.json +1064 -0
- package/templates/README.md +12 -0
- package/templates/codex/mcp-client.json +8 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export function createNotificationSink(options = {}) {
|
|
2
|
+
const targets = resolveTargets(options.targets, options.envTargets, options.configTargets);
|
|
3
|
+
if (targets.length === 0) {
|
|
4
|
+
return new NoopNotificationSink();
|
|
5
|
+
}
|
|
6
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
7
|
+
const dispatcher = options.dispatcher ?? createHttpDispatcher(fetchImpl);
|
|
8
|
+
return new HttpNotificationSink(targets, dispatcher, options.logger ?? console);
|
|
9
|
+
}
|
|
10
|
+
class NoopNotificationSink {
|
|
11
|
+
targets;
|
|
12
|
+
constructor(targets = []) {
|
|
13
|
+
this.targets = targets;
|
|
14
|
+
}
|
|
15
|
+
async notify() {
|
|
16
|
+
return { delivered: [], failures: [] };
|
|
17
|
+
}
|
|
18
|
+
async shutdown() {
|
|
19
|
+
// no-op
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
class HttpNotificationSink {
|
|
23
|
+
targets;
|
|
24
|
+
dispatcher;
|
|
25
|
+
logger;
|
|
26
|
+
constructor(targets, dispatcher, logger) {
|
|
27
|
+
this.targets = targets;
|
|
28
|
+
this.dispatcher = dispatcher;
|
|
29
|
+
this.logger = logger;
|
|
30
|
+
}
|
|
31
|
+
async notify(summary) {
|
|
32
|
+
const delivered = [];
|
|
33
|
+
const failures = [];
|
|
34
|
+
await Promise.all(this.targets.map(async (target) => {
|
|
35
|
+
try {
|
|
36
|
+
await this.dispatcher(target, summary);
|
|
37
|
+
delivered.push(target);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
41
|
+
failures.push({ target, error: message });
|
|
42
|
+
this.logger.warn(`Notification delivery failed for ${target}: ${message}`);
|
|
43
|
+
}
|
|
44
|
+
}));
|
|
45
|
+
return { delivered, failures };
|
|
46
|
+
}
|
|
47
|
+
async shutdown() {
|
|
48
|
+
// nothing to clean up yet
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function resolveTargets(cliTargets, envTargets, configTargets) {
|
|
52
|
+
const source = cliTargets?.length ? cliTargets : envTargets?.length ? envTargets : configTargets ?? [];
|
|
53
|
+
const normalized = source.map((target) => target.trim()).filter(Boolean);
|
|
54
|
+
return Array.from(new Set(normalized));
|
|
55
|
+
}
|
|
56
|
+
function createHttpDispatcher(fetchImpl) {
|
|
57
|
+
if (!fetchImpl) {
|
|
58
|
+
return async () => {
|
|
59
|
+
throw new Error('Notifications disabled: fetch API unavailable');
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return async (target, summary) => {
|
|
63
|
+
const response = await fetchImpl(target, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: {
|
|
66
|
+
'content-type': 'application/json'
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify(summary)
|
|
69
|
+
});
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
throw new Error(`Notification endpoint responded with status ${response.status}`);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const DEFAULT_BACKOFF_MS = 500;
|
|
2
|
+
const DEFAULT_MAX_BACKOFF_MS = 5_000;
|
|
3
|
+
const DEFAULT_MAX_FAILURES = 3;
|
|
4
|
+
export function createTelemetrySink(options = {}) {
|
|
5
|
+
const endpoint = options.endpoint ?? null;
|
|
6
|
+
const explicitlyDisabled = options.enabled === false;
|
|
7
|
+
if (!endpoint || explicitlyDisabled) {
|
|
8
|
+
return new NoopTelemetrySink();
|
|
9
|
+
}
|
|
10
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
11
|
+
if (!fetchImpl) {
|
|
12
|
+
return new NoopTelemetrySink();
|
|
13
|
+
}
|
|
14
|
+
return new OtelTelemetrySink({
|
|
15
|
+
endpoint,
|
|
16
|
+
headers: options.headers ?? {},
|
|
17
|
+
maxFailures: options.maxFailures ?? DEFAULT_MAX_FAILURES,
|
|
18
|
+
backoffMs: options.backoffMs ?? DEFAULT_BACKOFF_MS,
|
|
19
|
+
maxBackoffMs: options.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS,
|
|
20
|
+
fetch: fetchImpl,
|
|
21
|
+
logger: options.logger ?? console
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
class NoopTelemetrySink {
|
|
25
|
+
async record() {
|
|
26
|
+
// no-op
|
|
27
|
+
}
|
|
28
|
+
async recordSummary() {
|
|
29
|
+
// no-op
|
|
30
|
+
}
|
|
31
|
+
async flush() {
|
|
32
|
+
// no-op
|
|
33
|
+
}
|
|
34
|
+
async shutdown() {
|
|
35
|
+
// no-op
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
class OtelTelemetrySink {
|
|
39
|
+
endpoint;
|
|
40
|
+
headers;
|
|
41
|
+
fetchImpl;
|
|
42
|
+
logger;
|
|
43
|
+
maxFailures;
|
|
44
|
+
backoffMs;
|
|
45
|
+
maxBackoffMs;
|
|
46
|
+
queue = [];
|
|
47
|
+
disabled = false;
|
|
48
|
+
consecutiveFailures = 0;
|
|
49
|
+
constructor(config) {
|
|
50
|
+
this.endpoint = config.endpoint;
|
|
51
|
+
this.headers = { 'content-type': 'application/json', ...config.headers };
|
|
52
|
+
this.fetchImpl = config.fetch;
|
|
53
|
+
this.logger = config.logger;
|
|
54
|
+
this.maxFailures = config.maxFailures;
|
|
55
|
+
this.backoffMs = config.backoffMs;
|
|
56
|
+
this.maxBackoffMs = config.maxBackoffMs;
|
|
57
|
+
}
|
|
58
|
+
async record(event) {
|
|
59
|
+
if (this.disabled) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this.queue.push(event);
|
|
63
|
+
}
|
|
64
|
+
async recordSummary(summary) {
|
|
65
|
+
if (this.disabled) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
await this.flush();
|
|
69
|
+
try {
|
|
70
|
+
const dimensions = buildSummaryDimensions(summary.payload.metrics);
|
|
71
|
+
const payload = dimensions ? { kind: 'summary', summary, dimensions } : { kind: 'summary', summary };
|
|
72
|
+
await this.sendPayload(payload);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
this.handleFailure(error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async flush() {
|
|
79
|
+
if (this.disabled || this.queue.length === 0) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const batch = this.queue.splice(0, this.queue.length);
|
|
83
|
+
try {
|
|
84
|
+
await this.sendPayload({ kind: 'events', events: batch });
|
|
85
|
+
this.consecutiveFailures = 0;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
this.queue.unshift(...batch);
|
|
89
|
+
this.handleFailure(error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async shutdown() {
|
|
93
|
+
await this.flush();
|
|
94
|
+
}
|
|
95
|
+
async sendPayload(payload) {
|
|
96
|
+
if (this.consecutiveFailures > 0) {
|
|
97
|
+
const delay = Math.min(this.backoffMs * 2 ** (this.consecutiveFailures - 1), this.maxBackoffMs);
|
|
98
|
+
await wait(delay);
|
|
99
|
+
}
|
|
100
|
+
const response = await this.fetchImpl(this.endpoint, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: this.headers,
|
|
103
|
+
body: JSON.stringify(payload)
|
|
104
|
+
});
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(`Telemetry request failed with status ${response.status}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
handleFailure(error) {
|
|
110
|
+
this.consecutiveFailures += 1;
|
|
111
|
+
if (this.consecutiveFailures >= this.maxFailures) {
|
|
112
|
+
this.disabled = true;
|
|
113
|
+
this.logger.warn(`Telemetry disabled after ${this.consecutiveFailures} consecutive failures: ${String(error)}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function buildSummaryDimensions(metrics) {
|
|
118
|
+
if (!metrics) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
const dimensions = {
|
|
122
|
+
tool_cost_usd: metrics.costUsd,
|
|
123
|
+
latency_ms: metrics.latencyMs
|
|
124
|
+
};
|
|
125
|
+
if (metrics.tfgrpo) {
|
|
126
|
+
dimensions.tfgrpo_epoch = metrics.tfgrpo.epoch;
|
|
127
|
+
dimensions.tfgrpo_group_size = metrics.tfgrpo.groupSize;
|
|
128
|
+
dimensions.tfgrpo_group_id = metrics.tfgrpo.groupId;
|
|
129
|
+
}
|
|
130
|
+
if (metrics.perTool.length > 0) {
|
|
131
|
+
dimensions.tool_costs = Object.fromEntries(metrics.perTool.map((entry) => [entry.tool, entry.costUsd]));
|
|
132
|
+
}
|
|
133
|
+
return dimensions;
|
|
134
|
+
}
|
|
135
|
+
async function wait(ms) {
|
|
136
|
+
if (ms <= 0) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
await new Promise((resolve) => {
|
|
140
|
+
setTimeout(resolve, ms);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
export class SandboxRetryableError extends Error {
|
|
2
|
+
nextState;
|
|
3
|
+
constructor(message, nextState = undefined) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.nextState = nextState;
|
|
6
|
+
this.name = 'SandboxRetryableError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class ApprovalRequiredError extends Error {
|
|
10
|
+
context;
|
|
11
|
+
policy;
|
|
12
|
+
constructor(message, context, policy) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.context = context;
|
|
15
|
+
this.policy = policy;
|
|
16
|
+
this.name = 'ApprovalRequiredError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class ApprovalDeniedError extends Error {
|
|
20
|
+
context;
|
|
21
|
+
constructor(message, context) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.context = context;
|
|
24
|
+
this.name = 'ApprovalDeniedError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class ToolInvocationFailedError extends Error {
|
|
28
|
+
record;
|
|
29
|
+
cause;
|
|
30
|
+
constructor(message, record, cause) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.record = record;
|
|
33
|
+
this.cause = cause;
|
|
34
|
+
this.name = 'ToolInvocationFailedError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
38
|
+
const DEFAULT_BACKOFF_MS = 250;
|
|
39
|
+
export class ToolOrchestrator {
|
|
40
|
+
approvalPolicy;
|
|
41
|
+
approvalCache;
|
|
42
|
+
approvalPrompter;
|
|
43
|
+
maxAttempts;
|
|
44
|
+
initialBackoffMs;
|
|
45
|
+
wait;
|
|
46
|
+
now;
|
|
47
|
+
constructor(options = {}) {
|
|
48
|
+
this.approvalPolicy = options.approvalPolicy ?? 'on-request';
|
|
49
|
+
this.approvalCache = options.approvalCache;
|
|
50
|
+
this.approvalPrompter = options.approvalPrompter;
|
|
51
|
+
this.maxAttempts = Math.max(1, options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
|
|
52
|
+
this.initialBackoffMs = Math.max(0, options.initialBackoffMs ?? DEFAULT_BACKOFF_MS);
|
|
53
|
+
this.wait =
|
|
54
|
+
options.wait ??
|
|
55
|
+
((ms) => new Promise((resolve) => {
|
|
56
|
+
setTimeout(resolve, ms);
|
|
57
|
+
}));
|
|
58
|
+
this.now = options.now ?? (() => new Date());
|
|
59
|
+
}
|
|
60
|
+
async invoke(invocation) {
|
|
61
|
+
if (!invocation.id) {
|
|
62
|
+
throw new Error('Tool invocation id is required.');
|
|
63
|
+
}
|
|
64
|
+
if (!invocation.tool) {
|
|
65
|
+
throw new Error('Tool invocation tool name is required.');
|
|
66
|
+
}
|
|
67
|
+
const approvalSource = await this.resolveApproval(invocation);
|
|
68
|
+
let sandboxState = invocation.sandbox?.initialState ?? 'sandboxed';
|
|
69
|
+
const startedAt = this.now().toISOString();
|
|
70
|
+
let attempt = 1;
|
|
71
|
+
let lastError = null;
|
|
72
|
+
while (attempt <= this.maxAttempts) {
|
|
73
|
+
try {
|
|
74
|
+
const output = await invocation.run({ attempt, sandboxState });
|
|
75
|
+
const completedAt = this.now().toISOString();
|
|
76
|
+
const record = this.createRecord(invocation, approvalSource, sandboxState, attempt, completedAt, startedAt, 'succeeded');
|
|
77
|
+
return { output, record };
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
lastError = error;
|
|
81
|
+
const decision = this.classifySandboxDecision(invocation.sandbox, error);
|
|
82
|
+
const reachedMaxAttempts = attempt >= this.maxAttempts;
|
|
83
|
+
if (!decision.retry || reachedMaxAttempts) {
|
|
84
|
+
const completedAt = this.now().toISOString();
|
|
85
|
+
const record = this.createRecord(invocation, approvalSource, decision.nextState ?? sandboxState, attempt, completedAt, startedAt, 'failed');
|
|
86
|
+
throw new ToolInvocationFailedError(`Tool invocation ${invocation.id} failed after ${attempt} attempt${attempt === 1 ? '' : 's'}.`, record, error);
|
|
87
|
+
}
|
|
88
|
+
const delay = this.initialBackoffMs * 2 ** (attempt - 1);
|
|
89
|
+
await invocation.sandbox?.onRetry?.({
|
|
90
|
+
attempt,
|
|
91
|
+
delayMs: delay,
|
|
92
|
+
sandboxState,
|
|
93
|
+
error
|
|
94
|
+
});
|
|
95
|
+
await this.wait(delay);
|
|
96
|
+
sandboxState = decision.nextState ?? sandboxState;
|
|
97
|
+
attempt += 1;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// This branch is defensive; the loop should always return or throw earlier.
|
|
101
|
+
throw lastError instanceof Error ? lastError : new Error('Tool invocation aborted unexpectedly.');
|
|
102
|
+
}
|
|
103
|
+
async resolveApproval(invocation) {
|
|
104
|
+
if (!invocation.approvalRequired) {
|
|
105
|
+
return 'not-required';
|
|
106
|
+
}
|
|
107
|
+
if (!invocation.approvalKey) {
|
|
108
|
+
throw new Error(`Tool invocation ${invocation.id} requires an approvalKey when approvalRequired is true.`);
|
|
109
|
+
}
|
|
110
|
+
const context = {
|
|
111
|
+
toolId: invocation.tool,
|
|
112
|
+
description: invocation.description
|
|
113
|
+
};
|
|
114
|
+
const cached = this.approvalCache?.get(invocation.approvalKey);
|
|
115
|
+
if (cached?.granted) {
|
|
116
|
+
return 'cache';
|
|
117
|
+
}
|
|
118
|
+
if (this.approvalPolicy === 'never') {
|
|
119
|
+
throw new ApprovalRequiredError(`Approval required for ${invocation.tool} but approval policy forbids new prompts.`, context, this.approvalPolicy);
|
|
120
|
+
}
|
|
121
|
+
if (!this.approvalPrompter) {
|
|
122
|
+
throw new ApprovalRequiredError(`Approval required for ${invocation.tool} but no approval prompter is available.`, context, this.approvalPolicy);
|
|
123
|
+
}
|
|
124
|
+
const grant = await this.approvalPrompter.requestApproval(invocation.approvalKey, context);
|
|
125
|
+
if (!grant.granted) {
|
|
126
|
+
throw new ApprovalDeniedError(`Approval denied for ${invocation.tool}.`, context);
|
|
127
|
+
}
|
|
128
|
+
const stampedGrant = {
|
|
129
|
+
...grant,
|
|
130
|
+
granted: true,
|
|
131
|
+
timestamp: grant.timestamp ?? this.now().toISOString()
|
|
132
|
+
};
|
|
133
|
+
this.approvalCache?.set(invocation.approvalKey, stampedGrant);
|
|
134
|
+
return 'prompt';
|
|
135
|
+
}
|
|
136
|
+
classifySandboxDecision(sandbox, error) {
|
|
137
|
+
const decision = sandbox?.classifyError?.(error);
|
|
138
|
+
if (decision) {
|
|
139
|
+
return decision;
|
|
140
|
+
}
|
|
141
|
+
if (error instanceof SandboxRetryableError) {
|
|
142
|
+
return { retry: true, nextState: error.nextState };
|
|
143
|
+
}
|
|
144
|
+
return { retry: false };
|
|
145
|
+
}
|
|
146
|
+
createRecord(invocation, approvalSource, sandboxState, attemptCount, completedAt, startedAt, status) {
|
|
147
|
+
const metadata = invocation.metadata ? { ...invocation.metadata } : undefined;
|
|
148
|
+
return {
|
|
149
|
+
id: invocation.id,
|
|
150
|
+
tool: invocation.tool,
|
|
151
|
+
approvalSource,
|
|
152
|
+
retryCount: Math.max(0, attemptCount - 1),
|
|
153
|
+
sandboxState,
|
|
154
|
+
status,
|
|
155
|
+
startedAt,
|
|
156
|
+
completedAt,
|
|
157
|
+
attemptCount,
|
|
158
|
+
...(metadata ? { metadata } : {})
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createWriteStream, mkdtempSync } from 'node:fs';
|
|
3
|
+
import { rm } from 'node:fs/promises';
|
|
4
|
+
import { EventEmitter } from 'node:events';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
import { PassThrough } from 'node:stream';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
export class ExecClient {
|
|
10
|
+
cliPath;
|
|
11
|
+
cwd;
|
|
12
|
+
baseEnv;
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.cliPath = options.cliPath ?? 'codex-orchestrator';
|
|
15
|
+
this.cwd = options.cwd ?? process.cwd();
|
|
16
|
+
this.baseEnv = { ...process.env, ...options.env };
|
|
17
|
+
}
|
|
18
|
+
run(options) {
|
|
19
|
+
if (!options.command) {
|
|
20
|
+
throw new Error('Exec command requires a command to run.');
|
|
21
|
+
}
|
|
22
|
+
const internal = this.normalizeOptions(options);
|
|
23
|
+
const child = spawn(internal.cliPath, buildCliArgs(internal), {
|
|
24
|
+
cwd: internal.spawnCwd,
|
|
25
|
+
env: internal.inheritedEnv,
|
|
26
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
27
|
+
});
|
|
28
|
+
return new ExecRunHandle(this, internal, child);
|
|
29
|
+
}
|
|
30
|
+
normalizeOptions(options) {
|
|
31
|
+
const spawnCwd = this.cwd;
|
|
32
|
+
const mergedEnv = { ...this.baseEnv, ...options.env };
|
|
33
|
+
return {
|
|
34
|
+
...options,
|
|
35
|
+
cliPath: this.cliPath,
|
|
36
|
+
spawnCwd,
|
|
37
|
+
inheritedEnv: mergedEnv
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class ExecRunHandle extends EventEmitter {
|
|
42
|
+
client;
|
|
43
|
+
baseOptions;
|
|
44
|
+
child;
|
|
45
|
+
eventsList = [];
|
|
46
|
+
stderrLines = [];
|
|
47
|
+
eventsStream;
|
|
48
|
+
stderrStream;
|
|
49
|
+
eventsFilePath;
|
|
50
|
+
stderrFilePath;
|
|
51
|
+
artifactRoot;
|
|
52
|
+
maxEventBuffer = 200;
|
|
53
|
+
maxStderrBuffer = 200;
|
|
54
|
+
streamsClosed = false;
|
|
55
|
+
summaryEvent = null;
|
|
56
|
+
resultPromise;
|
|
57
|
+
resolveResult;
|
|
58
|
+
rejectResult;
|
|
59
|
+
constructor(client, baseOptions, child) {
|
|
60
|
+
super();
|
|
61
|
+
this.client = client;
|
|
62
|
+
this.baseOptions = baseOptions;
|
|
63
|
+
this.child = child;
|
|
64
|
+
this.artifactRoot = mkdtempSync(join(tmpdir(), 'codex-exec-'));
|
|
65
|
+
this.eventsFilePath = join(this.artifactRoot, 'events.ndjson');
|
|
66
|
+
this.stderrFilePath = join(this.artifactRoot, 'stderr.log');
|
|
67
|
+
this.eventsStream = createWriteStream(this.eventsFilePath, { flags: 'a' });
|
|
68
|
+
this.stderrStream = createWriteStream(this.stderrFilePath, { flags: 'a' });
|
|
69
|
+
this.resultPromise = new Promise((resolve, reject) => {
|
|
70
|
+
this.resolveResult = resolve;
|
|
71
|
+
this.rejectResult = reject;
|
|
72
|
+
});
|
|
73
|
+
const stdout = child.stdout ?? new PassThrough();
|
|
74
|
+
const stderr = child.stderr ?? new PassThrough();
|
|
75
|
+
const rl = createInterface({ input: stdout, crlfDelay: Infinity });
|
|
76
|
+
rl.on('line', (line) => {
|
|
77
|
+
const trimmed = line.trim();
|
|
78
|
+
if (!trimmed) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
let parsed;
|
|
82
|
+
try {
|
|
83
|
+
parsed = JSON.parse(trimmed);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
this.emit('warning', new Error(`Failed to parse JSONL line: ${trimmed}`));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
this.eventsStream.write(`${JSON.stringify(parsed)}\n`);
|
|
90
|
+
this.eventsList.push(parsed);
|
|
91
|
+
if (this.eventsList.length > this.maxEventBuffer) {
|
|
92
|
+
this.eventsList.shift();
|
|
93
|
+
}
|
|
94
|
+
this.emit('event', parsed);
|
|
95
|
+
if (parsed.type === 'run:summary') {
|
|
96
|
+
this.summaryEvent = parsed;
|
|
97
|
+
this.resolveResult(this.buildResult());
|
|
98
|
+
this.emit('summary', this.summaryEvent);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
stderr.on('data', (chunk) => {
|
|
102
|
+
const text = chunk.toString();
|
|
103
|
+
this.stderrStream.write(text);
|
|
104
|
+
if (this.stderrLines.length >= this.maxStderrBuffer) {
|
|
105
|
+
this.stderrLines.shift();
|
|
106
|
+
}
|
|
107
|
+
this.stderrLines.push(text.trim());
|
|
108
|
+
this.emit('stderr', text);
|
|
109
|
+
});
|
|
110
|
+
child.once('error', (error) => {
|
|
111
|
+
this.closeStreams();
|
|
112
|
+
this.emit('error', error);
|
|
113
|
+
this.rejectResult(error);
|
|
114
|
+
});
|
|
115
|
+
child.once('close', (code, signal) => {
|
|
116
|
+
this.closeStreams();
|
|
117
|
+
this.emit('exit', { code, signal });
|
|
118
|
+
if (!this.summaryEvent) {
|
|
119
|
+
const error = new Error('Exec command exited without emitting a summary event.');
|
|
120
|
+
error.exitCode = code ?? null;
|
|
121
|
+
error.signal = signal ?? null;
|
|
122
|
+
this.rejectResult(error);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
get events() {
|
|
127
|
+
return this.eventsList;
|
|
128
|
+
}
|
|
129
|
+
get summary() {
|
|
130
|
+
return this.summaryEvent;
|
|
131
|
+
}
|
|
132
|
+
get result() {
|
|
133
|
+
return this.resultPromise;
|
|
134
|
+
}
|
|
135
|
+
cancel(signal = 'SIGTERM') {
|
|
136
|
+
if (!this.child.killed) {
|
|
137
|
+
this.child.kill(signal);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
retry(overrides = {}) {
|
|
141
|
+
const merged = {
|
|
142
|
+
...this.baseOptions,
|
|
143
|
+
...overrides
|
|
144
|
+
};
|
|
145
|
+
delete merged.cliPath;
|
|
146
|
+
delete merged.spawnCwd;
|
|
147
|
+
delete merged.inheritedEnv;
|
|
148
|
+
return this.client.run(merged);
|
|
149
|
+
}
|
|
150
|
+
buildResult() {
|
|
151
|
+
if (!this.summaryEvent) {
|
|
152
|
+
throw new Error('Summary not available');
|
|
153
|
+
}
|
|
154
|
+
const payload = this.summaryEvent.payload;
|
|
155
|
+
return {
|
|
156
|
+
summary: this.summaryEvent,
|
|
157
|
+
events: [...this.eventsList],
|
|
158
|
+
eventsPath: this.eventsFilePath,
|
|
159
|
+
exitCode: payload.result.exitCode ?? null,
|
|
160
|
+
status: payload.status,
|
|
161
|
+
manifestPath: payload.run.manifest,
|
|
162
|
+
rawStderr: [...this.stderrLines],
|
|
163
|
+
stderrPath: this.stderrFilePath
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
closeStreams() {
|
|
167
|
+
if (this.streamsClosed) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
this.streamsClosed = true;
|
|
171
|
+
this.eventsStream.end();
|
|
172
|
+
this.stderrStream.end();
|
|
173
|
+
void rm(this.artifactRoot, { recursive: true, force: true }).catch(() => { });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function buildCliArgs(options) {
|
|
177
|
+
const args = ['exec', '--jsonl'];
|
|
178
|
+
if (options.taskId) {
|
|
179
|
+
args.push('--task', options.taskId);
|
|
180
|
+
}
|
|
181
|
+
if (options.cwd) {
|
|
182
|
+
args.push('--cwd', options.cwd);
|
|
183
|
+
}
|
|
184
|
+
if (options.otelEndpoint) {
|
|
185
|
+
args.push('--otel-endpoint', options.otelEndpoint);
|
|
186
|
+
}
|
|
187
|
+
if (options.notify) {
|
|
188
|
+
for (const target of options.notify) {
|
|
189
|
+
args.push('--notify', target);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
args.push('--');
|
|
193
|
+
args.push(options.command, ...(options.args ?? []));
|
|
194
|
+
return args;
|
|
195
|
+
}
|