@the-open-engine/zeroshot 6.35.3 → 6.37.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/cli/index.js +99 -29
- package/lib/agent-cli-provider/adapters/omp.js +1 -1
- package/lib/agent-cli-provider/adapters/omp.js.map +1 -1
- package/lib/hosted-target/adapter-types.d.cts +0 -8
- package/lib/hosted-target/adapter-types.d.mts +0 -8
- package/lib/hosted-target/adapter-types.d.ts +0 -8
- package/lib/hosted-target/index.d.cts +1 -1
- package/lib/hosted-target/index.d.mts +1 -1
- package/lib/hosted-target/index.d.ts +1 -1
- package/lib/hosted-target/retry-executor.d.cts +1 -1
- package/lib/hosted-target/retry-executor.d.mts +1 -1
- package/lib/hosted-target/retry-executor.d.ts +1 -1
- package/lib/hosted-target/target-adapter.d.cts +1 -1
- package/lib/hosted-target/target-adapter.d.mts +1 -1
- package/lib/hosted-target/target-adapter.d.ts +1 -1
- package/lib/hosted-target/zero-cloud-v1-adapter.cjs +0 -29
- package/lib/hosted-target/zero-cloud-v1-adapter.d.cts +0 -2
- package/lib/hosted-target/zero-cloud-v1-adapter.d.mts +0 -2
- package/lib/hosted-target/zero-cloud-v1-adapter.d.ts +0 -2
- package/lib/hosted-target/zero-cloud-v1-adapter.mjs +0 -29
- package/lib/target/discovery-sections.cjs +2 -21
- package/lib/target/discovery-sections.d.cts +0 -2
- package/lib/target/discovery-sections.d.mts +0 -2
- package/lib/target/discovery-sections.d.ts +0 -2
- package/lib/target/discovery-sections.js +2 -21
- package/lib/target/discovery-sections.mjs +3 -22
- package/lib/target/discovery-validation.cjs +4 -28
- package/lib/target/discovery-validation.d.cts +0 -10
- package/lib/target/discovery-validation.d.mts +0 -10
- package/lib/target/discovery-validation.d.ts +0 -10
- package/lib/target/discovery-validation.js +4 -28
- package/lib/target/discovery-validation.mjs +4 -27
- package/lib/target/discovery.cjs +0 -1
- package/lib/target/discovery.d.cts +0 -3
- package/lib/target/discovery.d.mts +0 -3
- package/lib/target/discovery.d.ts +0 -3
- package/lib/target/discovery.js +0 -1
- package/lib/target/discovery.mjs +1 -2
- package/lib/target/index.d.ts +1 -1
- package/lib/target/run-intent-discovery.cjs +3 -1
- package/lib/target/run-intent-discovery.js +3 -1
- package/lib/target/run-intent-discovery.mjs +4 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/agent/agent-lifecycle.js +25 -76
- package/src/agent/agent-liveness-poll.js +93 -0
- package/src/agent/agent-task-executor.js +19 -3
- package/src/agent-cli-provider/adapters/omp.ts +1 -1
- package/src/foreground-benchmark-files.js +81 -0
- package/src/foreground-benchmark-result.js +233 -0
- package/src/foreground-benchmark-run.js +72 -0
- package/src/hosted-target/adapter-types.ts +0 -14
- package/src/hosted-target/index.ts +0 -1
- package/src/hosted-target/retry-executor.ts +12 -14
- package/src/hosted-target/target-adapter.ts +1 -5
- package/src/hosted-target/zero-cloud-v1-adapter.ts +0 -37
- package/src/target/discovery-sections.ts +3 -28
- package/src/target/discovery-validation.ts +15 -61
- package/src/target/discovery.ts +1 -9
- package/src/target/index.ts +0 -1
- package/src/target/run-intent-discovery.ts +3 -2
- package/task-lib/runner.js +7 -0
- package/lib/hosted-target/runtime-install.cjs +0 -45
- package/lib/hosted-target/runtime-install.d.cts +0 -14
- package/lib/hosted-target/runtime-install.d.mts +0 -14
- package/lib/hosted-target/runtime-install.d.ts +0 -14
- package/lib/hosted-target/runtime-install.mjs +0 -42
- package/src/hosted-target/runtime-install.ts +0 -83
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const { isPlatformSupported } = require('./agent-stuck-detector');
|
|
2
|
+
|
|
3
|
+
function hasRecoverableTask(agent) {
|
|
4
|
+
return (
|
|
5
|
+
Boolean(agent.currentTask) ||
|
|
6
|
+
Boolean(agent.isolation?.enabled && agent.currentTaskId) ||
|
|
7
|
+
agent.nestedExecutions?.hasActive === true
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function handlePendingTermination(agent, settings, now, attemptTermination) {
|
|
12
|
+
if (!agent.livenessTerminationContext) return false;
|
|
13
|
+
if (now >= agent.livenessTerminationRetryAt) attemptTermination(agent, settings);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function taskTiming(agent, now) {
|
|
18
|
+
const taskStartedAt = agent.taskStartedAt || agent.lastOutputTime || now;
|
|
19
|
+
const lastOutputTime = agent.lastOutputTime || taskStartedAt;
|
|
20
|
+
return {
|
|
21
|
+
taskRuntime: now - taskStartedAt,
|
|
22
|
+
timeSinceLastOutput: now - lastOutputTime,
|
|
23
|
+
lastOutputTime,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function handleTaskTimeout(context, timing) {
|
|
28
|
+
const { agent, settings, configuredTimeout, beginTermination } = context;
|
|
29
|
+
if (!configuredTimeout || timing.taskRuntime < configuredTimeout) return false;
|
|
30
|
+
beginTermination(
|
|
31
|
+
agent,
|
|
32
|
+
settings,
|
|
33
|
+
`Task timed out after ${configuredTimeout}ms`,
|
|
34
|
+
'AGENT_TASK_TIMEOUT',
|
|
35
|
+
{
|
|
36
|
+
taskId: agent.currentTaskId,
|
|
37
|
+
taskRuntime: timing.taskRuntime,
|
|
38
|
+
timeout: configuredTimeout,
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function publishStaleWarning(context, timing) {
|
|
45
|
+
const { agent, staleDuration, warningsBeforeKill } = context;
|
|
46
|
+
agent.consecutiveStaleWarnings += 1;
|
|
47
|
+
agent._publishLifecycle('AGENT_STALE_WARNING', {
|
|
48
|
+
taskId: agent.currentTaskId,
|
|
49
|
+
timeSinceLastOutput: timing.timeSinceLastOutput,
|
|
50
|
+
staleDuration,
|
|
51
|
+
lastOutputTime: timing.lastOutputTime,
|
|
52
|
+
consecutiveWarnings: agent.consecutiveStaleWarnings,
|
|
53
|
+
warningsBeforeKill,
|
|
54
|
+
processDiagnosticsAvailable: isPlatformSupported(),
|
|
55
|
+
analysis: `Provider produced no output for ${timing.timeSinceLastOutput}ms`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function terminateForInactivity(context, timing) {
|
|
60
|
+
const { agent, settings, staleDuration, beginTermination } = context;
|
|
61
|
+
beginTermination(
|
|
62
|
+
agent,
|
|
63
|
+
settings,
|
|
64
|
+
`Provider produced no output for ${timing.timeSinceLastOutput}ms`,
|
|
65
|
+
'PROVIDER_INACTIVITY_TIMEOUT',
|
|
66
|
+
{
|
|
67
|
+
taskId: agent.currentTaskId,
|
|
68
|
+
timeSinceLastOutput: timing.timeSinceLastOutput,
|
|
69
|
+
staleDuration,
|
|
70
|
+
consecutiveWarnings: agent.consecutiveStaleWarnings,
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createLivenessPoll(context) {
|
|
76
|
+
const { agent, settings, staleDuration, warningsBeforeKill, attemptTermination } = context;
|
|
77
|
+
return () => {
|
|
78
|
+
if (!hasRecoverableTask(agent) || agent.livenessTerminationStarted) return;
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
if (handlePendingTermination(agent, settings, now, attemptTermination)) return;
|
|
81
|
+
const timing = taskTiming(agent, now);
|
|
82
|
+
if (handleTaskTimeout(context, timing)) return;
|
|
83
|
+
if (timing.timeSinceLastOutput < staleDuration) {
|
|
84
|
+
agent.consecutiveStaleWarnings = 0;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
publishStaleWarning(context, timing);
|
|
88
|
+
if (agent.consecutiveStaleWarnings < warningsBeforeKill) return;
|
|
89
|
+
terminateForInactivity(context, timing);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { createLivenessPoll };
|
|
@@ -1866,6 +1866,19 @@ function finalizeLogFollow(agent, state) {
|
|
|
1866
1866
|
}
|
|
1867
1867
|
}
|
|
1868
1868
|
|
|
1869
|
+
function finishHostLogCapture(agent, state, pollLogFile, processNewContent, broadcastLine) {
|
|
1870
|
+
// A task status can become terminal before the watcher has flushed its final
|
|
1871
|
+
// filesystem write (observed on Modal). Stop the timers first, then take one
|
|
1872
|
+
// authoritative catch-up read and finish any final UTF-8/non-newline record.
|
|
1873
|
+
finalizeLogFollow(agent, state);
|
|
1874
|
+
pollLogFile();
|
|
1875
|
+
const decoderTail = state.logDecoder.end();
|
|
1876
|
+
if (decoderTail) processNewContent(decoderTail);
|
|
1877
|
+
if (state.lineBuffer.byteLength > 0) {
|
|
1878
|
+
completeLogRecord(state.lineBuffer, broadcastLine, true);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1869
1882
|
function settleHostStatusFailure({ agent, providerName, state, resolve, text, data, error }) {
|
|
1870
1883
|
if (state.resolved) return;
|
|
1871
1884
|
state.resolved = true;
|
|
@@ -1986,6 +1999,7 @@ function handleStatusCompletion({
|
|
|
1986
1999
|
state,
|
|
1987
2000
|
stdout,
|
|
1988
2001
|
pollLogFile,
|
|
2002
|
+
finishLogCapture,
|
|
1989
2003
|
resolve,
|
|
1990
2004
|
reject,
|
|
1991
2005
|
}) {
|
|
@@ -2012,7 +2026,7 @@ function handleStatusCompletion({
|
|
|
2012
2026
|
if (state.resolved) return;
|
|
2013
2027
|
state.resolved = true;
|
|
2014
2028
|
|
|
2015
|
-
|
|
2029
|
+
finishLogCapture();
|
|
2016
2030
|
flushAgentOutput(agent, providerName, state);
|
|
2017
2031
|
|
|
2018
2032
|
buildCompletionResult({
|
|
@@ -2066,14 +2080,12 @@ function createLogFollower({
|
|
|
2066
2080
|
const state = createLogFollowState();
|
|
2067
2081
|
state.skipStructuredResultCheck = skipStructuredResultCheck;
|
|
2068
2082
|
state.nested = nested;
|
|
2069
|
-
|
|
2070
2083
|
state.logFilePath = lookupLogFilePath(ctPath, taskId);
|
|
2071
2084
|
if (state.logFilePath) {
|
|
2072
2085
|
agent._log(`📋 Agent ${agent.id}: Following ct logs for ${taskId}`);
|
|
2073
2086
|
} else {
|
|
2074
2087
|
agent._log(`⏳ Agent ${agent.id}: Waiting for log file...`);
|
|
2075
2088
|
}
|
|
2076
|
-
|
|
2077
2089
|
const broadcastLine = (line) => broadcastAgentLine({ agent, providerName, state, line });
|
|
2078
2090
|
const processNewContent = (content) => appendContentToBuffer(state, content, broadcastLine);
|
|
2079
2091
|
const pollLogFile = () =>
|
|
@@ -2085,6 +2097,8 @@ function createLogFollower({
|
|
|
2085
2097
|
state,
|
|
2086
2098
|
onNewContent: processNewContent,
|
|
2087
2099
|
});
|
|
2100
|
+
const finishLogCapture = () =>
|
|
2101
|
+
finishHostLogCapture(agent, state, pollLogFile, processNewContent, broadcastLine);
|
|
2088
2102
|
|
|
2089
2103
|
state.pollInterval = setInterval(pollLogFile, 300);
|
|
2090
2104
|
|
|
@@ -2120,6 +2134,7 @@ function createLogFollower({
|
|
|
2120
2134
|
state,
|
|
2121
2135
|
stdout,
|
|
2122
2136
|
pollLogFile,
|
|
2137
|
+
finishLogCapture,
|
|
2123
2138
|
resolve,
|
|
2124
2139
|
reject,
|
|
2125
2140
|
});
|
|
@@ -3546,6 +3561,7 @@ module.exports = {
|
|
|
3546
3561
|
consumeIsolatedTailChunk,
|
|
3547
3562
|
flushAgentOutput,
|
|
3548
3563
|
flushIsolatedOutput,
|
|
3564
|
+
finishHostLogCapture,
|
|
3549
3565
|
CONTROL_PLANE_OUTPUT_LIMITS: Object.freeze({
|
|
3550
3566
|
maxBytes: MAX_CONTROL_PLANE_OUTPUT_BYTES,
|
|
3551
3567
|
maxRecords: MAX_CONTROL_PLANE_OUTPUT_RECORDS,
|
|
@@ -81,7 +81,7 @@ function detectCliFeatures(helpText?: string | null, versionText?: string | null
|
|
|
81
81
|
return {
|
|
82
82
|
provider: 'omp',
|
|
83
83
|
versionMatches: VERSION_TOKEN_PATTERN.test(version),
|
|
84
|
-
supportsRpcMode: /(
|
|
84
|
+
supportsRpcMode: /(?<![A-Za-z0-9_-])rpc(?![A-Za-z0-9_-])/.test(help),
|
|
85
85
|
supportsConfig: /--config\b/.test(help),
|
|
86
86
|
supportsModel: /--model\b/.test(help),
|
|
87
87
|
supportsThinking: /--thinking\b/.test(help),
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const crypto = require('node:crypto');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const { buildTelemetry } = require('./foreground-benchmark-result');
|
|
6
|
+
|
|
7
|
+
function serialized(value) {
|
|
8
|
+
return Buffer.from(`${JSON.stringify(value)}\n`, 'utf8');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function atomicWriteNew(targetPath, content) {
|
|
12
|
+
const directory = path.dirname(targetPath);
|
|
13
|
+
const temporary = path.join(
|
|
14
|
+
directory,
|
|
15
|
+
`.${path.basename(targetPath)}.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`
|
|
16
|
+
);
|
|
17
|
+
let descriptor;
|
|
18
|
+
let published = false;
|
|
19
|
+
try {
|
|
20
|
+
descriptor = fs.openSync(temporary, 'wx', 0o600);
|
|
21
|
+
fs.writeFileSync(descriptor, content);
|
|
22
|
+
fs.fsyncSync(descriptor);
|
|
23
|
+
fs.closeSync(descriptor);
|
|
24
|
+
descriptor = undefined;
|
|
25
|
+
fs.linkSync(temporary, targetPath);
|
|
26
|
+
published = true;
|
|
27
|
+
fs.unlinkSync(temporary);
|
|
28
|
+
const directoryDescriptor = fs.openSync(directory, fs.constants.O_RDONLY);
|
|
29
|
+
try {
|
|
30
|
+
fs.fsyncSync(directoryDescriptor);
|
|
31
|
+
} finally {
|
|
32
|
+
fs.closeSync(directoryDescriptor);
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
error.atomicTargetPublished = published;
|
|
36
|
+
throw error;
|
|
37
|
+
} finally {
|
|
38
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
39
|
+
try {
|
|
40
|
+
fs.unlinkSync(temporary);
|
|
41
|
+
} catch {
|
|
42
|
+
// Preserve the primary write error; a leftover randomized temp is non-authoritative.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function removeOrphanTelemetry(telemetryPath, primaryError) {
|
|
48
|
+
try {
|
|
49
|
+
fs.unlinkSync(telemetryPath);
|
|
50
|
+
} catch (cleanupError) {
|
|
51
|
+
if (cleanupError.code !== 'ENOENT') primaryError.cleanupError = cleanupError;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeBenchmarkResultBundle(resultPath, result, snapshot) {
|
|
56
|
+
if (typeof resultPath !== 'string' || resultPath.length === 0) {
|
|
57
|
+
throw new Error('result path must be non-empty text');
|
|
58
|
+
}
|
|
59
|
+
const resolvedResult = path.resolve(resultPath);
|
|
60
|
+
const telemetryPath = `${resolvedResult}.telemetry.json`;
|
|
61
|
+
const telemetry = buildTelemetry(result.runId, snapshot);
|
|
62
|
+
const telemetryBytes = serialized(telemetry);
|
|
63
|
+
atomicWriteNew(telemetryPath, telemetryBytes);
|
|
64
|
+
const receipt = {
|
|
65
|
+
...result,
|
|
66
|
+
telemetry: {
|
|
67
|
+
artifact: path.basename(telemetryPath),
|
|
68
|
+
byteLength: telemetryBytes.length,
|
|
69
|
+
sha256: crypto.createHash('sha256').update(telemetryBytes).digest('hex'),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
try {
|
|
73
|
+
atomicWriteNew(resolvedResult, serialized(receipt));
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (!error.atomicTargetPublished) removeOrphanTelemetry(telemetryPath, error);
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
return receipt;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { writeBenchmarkResultBundle };
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
const crypto = require('node:crypto');
|
|
2
|
+
const { VALID_PROVIDERS } = require('../lib/provider-names');
|
|
3
|
+
|
|
4
|
+
const RESULT_SCHEMA = 'zeroshot-benchmark-result/v1';
|
|
5
|
+
const TELEMETRY_SCHEMA = 'zeroshot-benchmark-telemetry/v1';
|
|
6
|
+
const EMPTY_DIAGNOSTIC = Object.freeze({
|
|
7
|
+
byteLength: 0,
|
|
8
|
+
sha256: crypto.createHash('sha256').update('').digest('hex'),
|
|
9
|
+
});
|
|
10
|
+
const TASK_FAILURE_REASONS = new Set(['max_iterations', 'structured_output_invalid']);
|
|
11
|
+
const PROVIDER_CODES = new Set(['crash', 'refusal']);
|
|
12
|
+
const PROVIDERS = new Set(VALID_PROVIDERS);
|
|
13
|
+
const PROVIDER_EVENTS = new Set(['terminal_error', 'turn.failed']);
|
|
14
|
+
const PROVIDER_CATEGORIES = new Set([
|
|
15
|
+
'authentication',
|
|
16
|
+
'permanent',
|
|
17
|
+
'quota',
|
|
18
|
+
'transient',
|
|
19
|
+
'unknown',
|
|
20
|
+
]);
|
|
21
|
+
const PROVIDER_KINDS = new Set([
|
|
22
|
+
'permanent-pattern',
|
|
23
|
+
'rate-limit',
|
|
24
|
+
'retryable-pattern',
|
|
25
|
+
'status-permanent',
|
|
26
|
+
'status-retryable',
|
|
27
|
+
'code-retryable',
|
|
28
|
+
'unknown-retryable',
|
|
29
|
+
]);
|
|
30
|
+
const TOKEN_FIELDS = [
|
|
31
|
+
'inputTokens',
|
|
32
|
+
'outputTokens',
|
|
33
|
+
'cacheReadInputTokens',
|
|
34
|
+
'cacheCreationInputTokens',
|
|
35
|
+
'totalCostUsd',
|
|
36
|
+
'count',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function requireObject(value, label) {
|
|
40
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
41
|
+
throw new Error(`${label} must be an object`);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function requireClosedText(value, allowed, label) {
|
|
47
|
+
if (typeof value !== 'string' || !allowed.has(value)) {
|
|
48
|
+
throw new Error(`${label} is outside the closed result contract`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function requireDiagnostic(value) {
|
|
54
|
+
if (
|
|
55
|
+
value &&
|
|
56
|
+
Number.isSafeInteger(value.byteLength) &&
|
|
57
|
+
value.byteLength >= 0 &&
|
|
58
|
+
typeof value.sha256 === 'string' &&
|
|
59
|
+
/^[a-f0-9]{64}$/.test(value.sha256)
|
|
60
|
+
) {
|
|
61
|
+
return { byteLength: value.byteLength, sha256: value.sha256 };
|
|
62
|
+
}
|
|
63
|
+
throw new Error('provider diagnostic is outside the closed result contract');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function terminalData(message) {
|
|
67
|
+
return requireObject(requireObject(message.content, 'terminal content').data, 'terminal data');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isExplicitTaskFailure(message, data) {
|
|
71
|
+
if (message.sender === 'orchestrator' || !TASK_FAILURE_REASONS.has(data.reason)) return false;
|
|
72
|
+
if (data.reason === 'max_iterations') return message.receiver === 'system';
|
|
73
|
+
return message.receiver === 'broadcast' && data.code === 'STRUCTURED_OUTPUT_INVALID';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function classifyTerminal(message) {
|
|
77
|
+
if (message.topic === 'CLUSTER_COMPLETE') {
|
|
78
|
+
return {
|
|
79
|
+
outcome: 'completed',
|
|
80
|
+
terminalOwner: 'task',
|
|
81
|
+
code: 'ok',
|
|
82
|
+
kind: 'workflow_complete',
|
|
83
|
+
retryable: false,
|
|
84
|
+
diagnostic: { ...EMPTY_DIAGNOSTIC },
|
|
85
|
+
provider: null,
|
|
86
|
+
event: null,
|
|
87
|
+
category: null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (message.topic !== 'CLUSTER_FAILED') {
|
|
92
|
+
throw new Error(`unsupported terminal topic: ${message.topic}`);
|
|
93
|
+
}
|
|
94
|
+
const data = terminalData(message);
|
|
95
|
+
if (isExplicitTaskFailure(message, data)) {
|
|
96
|
+
return {
|
|
97
|
+
outcome: 'task_failure',
|
|
98
|
+
terminalOwner: 'task',
|
|
99
|
+
code: data.reason,
|
|
100
|
+
kind: 'declared_failure',
|
|
101
|
+
retryable: false,
|
|
102
|
+
diagnostic: { ...EMPTY_DIAGNOSTIC },
|
|
103
|
+
provider: null,
|
|
104
|
+
event: null,
|
|
105
|
+
category: null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (data.reason === 'provider_execution_failed') {
|
|
109
|
+
if (typeof data.retryable !== 'boolean') {
|
|
110
|
+
throw new Error('provider retryable must be boolean');
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
outcome: 'provider_failure',
|
|
114
|
+
terminalOwner: 'provider',
|
|
115
|
+
code: requireClosedText(data.code, PROVIDER_CODES, 'provider code'),
|
|
116
|
+
kind: requireClosedText(data.kind, PROVIDER_KINDS, 'provider kind'),
|
|
117
|
+
retryable: data.retryable,
|
|
118
|
+
diagnostic: requireDiagnostic(data.diagnostic),
|
|
119
|
+
provider: requireClosedText(data.provider, PROVIDERS, 'provider'),
|
|
120
|
+
event: requireClosedText(data.event, PROVIDER_EVENTS, 'provider event'),
|
|
121
|
+
category: requireClosedText(data.category, PROVIDER_CATEGORIES, 'provider category'),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
outcome: 'engine_failure',
|
|
126
|
+
terminalOwner: 'engine',
|
|
127
|
+
code: 'engine_failed',
|
|
128
|
+
kind: 'declared_failure',
|
|
129
|
+
retryable: false,
|
|
130
|
+
diagnostic: { ...EMPTY_DIAGNOSTIC },
|
|
131
|
+
provider: null,
|
|
132
|
+
event: null,
|
|
133
|
+
category: null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validateStoppedAgents(agents) {
|
|
138
|
+
if (!Array.isArray(agents)) throw new Error('agents must be an array');
|
|
139
|
+
for (const agent of agents) {
|
|
140
|
+
const state = requireObject(agent, 'agent state');
|
|
141
|
+
if (state.pid !== null && state.pid !== undefined) {
|
|
142
|
+
throw new Error(`agent ${String(state.id)} still has a live process identity`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function validateRunId(runId) {
|
|
148
|
+
const parts = typeof runId === 'string' ? runId.split('-') : [];
|
|
149
|
+
const valid =
|
|
150
|
+
parts.length >= 2 &&
|
|
151
|
+
parts.every(
|
|
152
|
+
(part) => part.length > 0 && [...part].every((character) => /[a-z0-9]/.test(character))
|
|
153
|
+
);
|
|
154
|
+
if (!valid) {
|
|
155
|
+
throw new Error('runId must be a canonical cluster id');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildBenchmarkResult({ runId, terminalMessages, agents }) {
|
|
160
|
+
validateRunId(runId);
|
|
161
|
+
if (!Array.isArray(terminalMessages) || terminalMessages.length !== 1) {
|
|
162
|
+
throw new Error('foreground run must have exactly one terminal event');
|
|
163
|
+
}
|
|
164
|
+
validateStoppedAgents(agents);
|
|
165
|
+
return {
|
|
166
|
+
schema: RESULT_SCHEMA,
|
|
167
|
+
runId,
|
|
168
|
+
...classifyTerminal(terminalMessages[0]),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function buildCancelledResult({ runId, agents }) {
|
|
173
|
+
validateRunId(runId);
|
|
174
|
+
validateStoppedAgents(agents);
|
|
175
|
+
return {
|
|
176
|
+
schema: RESULT_SCHEMA,
|
|
177
|
+
runId,
|
|
178
|
+
outcome: 'cancelled',
|
|
179
|
+
terminalOwner: 'controller',
|
|
180
|
+
code: 'cancelled',
|
|
181
|
+
kind: 'controlled_cancellation',
|
|
182
|
+
retryable: false,
|
|
183
|
+
diagnostic: { ...EMPTY_DIAGNOSTIC },
|
|
184
|
+
provider: null,
|
|
185
|
+
event: null,
|
|
186
|
+
category: null,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function normalizeTokenEntry(value, label) {
|
|
191
|
+
const source = requireObject(value, label);
|
|
192
|
+
const entry = {};
|
|
193
|
+
for (const field of TOKEN_FIELDS) {
|
|
194
|
+
const amount = source[field] ?? 0;
|
|
195
|
+
if (typeof amount !== 'number' || !Number.isFinite(amount) || amount < 0) {
|
|
196
|
+
throw new Error(`${label}.${field} must be a finite non-negative number`);
|
|
197
|
+
}
|
|
198
|
+
entry[field] = amount;
|
|
199
|
+
}
|
|
200
|
+
return entry;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function buildTelemetry(runId, snapshot) {
|
|
204
|
+
validateRunId(runId);
|
|
205
|
+
const source = requireObject(snapshot, 'telemetry snapshot');
|
|
206
|
+
if (!Number.isSafeInteger(source.messageCount) || source.messageCount < 0) {
|
|
207
|
+
throw new Error('telemetry messageCount must be a non-negative safe integer');
|
|
208
|
+
}
|
|
209
|
+
const roles = requireObject(source.tokensByRole, 'tokensByRole');
|
|
210
|
+
const names = Object.keys(roles).sort();
|
|
211
|
+
if (names.length > 64) throw new Error('telemetry role count exceeds 64');
|
|
212
|
+
const tokensByRole = {};
|
|
213
|
+
for (const name of names) {
|
|
214
|
+
const validRoleName =
|
|
215
|
+
name === '_total' ||
|
|
216
|
+
(name.length <= 128 &&
|
|
217
|
+
/^[a-zA-Z0-9]$/.test(name[0] || '') &&
|
|
218
|
+
[...name].every((character) => /[a-zA-Z0-9_.-]/.test(character)));
|
|
219
|
+
if (!validRoleName) {
|
|
220
|
+
throw new Error('telemetry role name is invalid');
|
|
221
|
+
}
|
|
222
|
+
tokensByRole[name] = normalizeTokenEntry(roles[name], `tokensByRole.${name}`);
|
|
223
|
+
}
|
|
224
|
+
return { schema: TELEMETRY_SCHEMA, runId, messageCount: source.messageCount, tokensByRole };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
RESULT_SCHEMA,
|
|
229
|
+
TELEMETRY_SCHEMA,
|
|
230
|
+
buildBenchmarkResult,
|
|
231
|
+
buildCancelledResult,
|
|
232
|
+
buildTelemetry,
|
|
233
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const { buildBenchmarkResult, buildCancelledResult } = require('./foreground-benchmark-result');
|
|
2
|
+
const { writeBenchmarkResultBundle } = require('./foreground-benchmark-files');
|
|
3
|
+
|
|
4
|
+
const TERMINAL_TOPICS = ['CLUSTER_COMPLETE', 'CLUSTER_FAILED'];
|
|
5
|
+
const VERIFIER_ELIGIBLE = new Set(['completed', 'task_failure']);
|
|
6
|
+
const EXIT_CODES = Object.freeze({
|
|
7
|
+
provider_failure: 20,
|
|
8
|
+
engine_failure: 21,
|
|
9
|
+
cancelled: 22,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function isForegroundStatusSettled(status) {
|
|
13
|
+
return (
|
|
14
|
+
status &&
|
|
15
|
+
['stopped', 'killed'].includes(status.state) &&
|
|
16
|
+
status.isZombie === false &&
|
|
17
|
+
Array.isArray(status.agents) &&
|
|
18
|
+
status.agents.every((agent) => agent && (agent.pid === null || agent.pid === undefined))
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function terminalMessages(cluster, clusterId) {
|
|
23
|
+
const messages = TERMINAL_TOPICS.flatMap((topic) =>
|
|
24
|
+
cluster.messageBus.query({ cluster_id: clusterId, topic })
|
|
25
|
+
);
|
|
26
|
+
return messages.sort((left, right) => {
|
|
27
|
+
const a = BigInt(left.sequence);
|
|
28
|
+
const b = BigInt(right.sequence);
|
|
29
|
+
if (a < b) return -1;
|
|
30
|
+
if (a > b) return 1;
|
|
31
|
+
return 0;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function requireSettledStatus(orchestrator, clusterId) {
|
|
36
|
+
const status = orchestrator.getStatus(clusterId);
|
|
37
|
+
if (!isForegroundStatusSettled(status)) {
|
|
38
|
+
throw new Error(`foreground cluster is not settled: ${String(status?.state || 'unavailable')}`);
|
|
39
|
+
}
|
|
40
|
+
return status;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeForegroundResult({ orchestrator, cluster, clusterId, resultPath, cancelled }) {
|
|
44
|
+
const status = requireSettledStatus(orchestrator, clusterId);
|
|
45
|
+
const terminals = terminalMessages(cluster, clusterId);
|
|
46
|
+
let result;
|
|
47
|
+
if (cancelled && terminals.length === 0) {
|
|
48
|
+
result = buildCancelledResult({ runId: clusterId, agents: status.agents });
|
|
49
|
+
} else {
|
|
50
|
+
result = buildBenchmarkResult({
|
|
51
|
+
runId: clusterId,
|
|
52
|
+
terminalMessages: terminals,
|
|
53
|
+
agents: status.agents,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const snapshot = cluster.messageBus.readSnapshot(clusterId);
|
|
57
|
+
return writeBenchmarkResultBundle(resultPath, result, snapshot);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function exitCodeForResult(result) {
|
|
61
|
+
if (VERIFIER_ELIGIBLE.has(result.outcome)) return 0;
|
|
62
|
+
const exitCode = EXIT_CODES[result.outcome];
|
|
63
|
+
if (exitCode === undefined) throw new Error(`unsupported result outcome: ${result.outcome}`);
|
|
64
|
+
return exitCode;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
exitCodeForResult,
|
|
69
|
+
isForegroundStatusSettled,
|
|
70
|
+
terminalMessages,
|
|
71
|
+
writeForegroundResult,
|
|
72
|
+
};
|
|
@@ -12,13 +12,6 @@ import type {
|
|
|
12
12
|
TargetAccessTokenProvider,
|
|
13
13
|
} from './types.js';
|
|
14
14
|
|
|
15
|
-
export type CredentialInstallCapability =
|
|
16
|
-
| { readonly supported: false }
|
|
17
|
-
| {
|
|
18
|
-
readonly supported: true;
|
|
19
|
-
readonly descriptor: NonNullable<TargetDiscoveryDescriptor['credentialInstall']>;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
15
|
export interface TargetAdapter {
|
|
23
16
|
allocate(req: AllocateRequest, signal?: AbortSignal): Promise<Capsule>;
|
|
24
17
|
list(req?: ListRequest, signal?: AbortSignal): Promise<CapsuleListPage>;
|
|
@@ -26,13 +19,6 @@ export interface TargetAdapter {
|
|
|
26
19
|
terminate(capsuleId: string, signal?: AbortSignal): Promise<Capsule>;
|
|
27
20
|
limits(signal?: AbortSignal): Promise<CapsuleLimits>;
|
|
28
21
|
access(capsuleId: string, signal?: AbortSignal): Promise<CapsuleAccess>;
|
|
29
|
-
installRuntime(
|
|
30
|
-
capsuleId: string,
|
|
31
|
-
runtime: unknown,
|
|
32
|
-
accessToken: string,
|
|
33
|
-
signal?: AbortSignal
|
|
34
|
-
): Promise<void>;
|
|
35
|
-
readonly credentialInstall: CredentialInstallCapability;
|
|
36
22
|
}
|
|
37
23
|
|
|
38
24
|
export interface CreateTargetAdapterOptions {
|
|
@@ -2,14 +2,7 @@ import { MAX_RETRY_ELAPSED_MS } from './bounds.js';
|
|
|
2
2
|
import { TargetAdapterError } from './errors.js';
|
|
3
3
|
import type { Clock, RetryPolicy } from './types.js';
|
|
4
4
|
|
|
5
|
-
export type TargetOperation =
|
|
6
|
-
| 'allocate'
|
|
7
|
-
| 'list'
|
|
8
|
-
| 'inspect'
|
|
9
|
-
| 'terminate'
|
|
10
|
-
| 'limits'
|
|
11
|
-
| 'access'
|
|
12
|
-
| 'installRuntime';
|
|
5
|
+
export type TargetOperation = 'allocate' | 'list' | 'inspect' | 'terminate' | 'limits' | 'access';
|
|
13
6
|
|
|
14
7
|
function throwIfAborted(signal?: AbortSignal): void {
|
|
15
8
|
if (signal?.aborted) {
|
|
@@ -22,10 +15,16 @@ async function wait(delayMs: number, signal?: AbortSignal): Promise<void> {
|
|
|
22
15
|
if (delayMs <= 0) return;
|
|
23
16
|
await new Promise<void>((resolve, reject) => {
|
|
24
17
|
const timer = setTimeout(resolve, delayMs);
|
|
25
|
-
signal?.addEventListener(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
18
|
+
signal?.addEventListener(
|
|
19
|
+
'abort',
|
|
20
|
+
() => {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
reject(
|
|
23
|
+
signal.reason ?? new globalThis.DOMException('The operation was aborted', 'AbortError')
|
|
24
|
+
);
|
|
25
|
+
},
|
|
26
|
+
{ once: true }
|
|
27
|
+
);
|
|
29
28
|
});
|
|
30
29
|
}
|
|
31
30
|
|
|
@@ -41,12 +40,11 @@ function validRetryDelay(retry: boolean, delayMs: number, remaining: number): bo
|
|
|
41
40
|
return retry && Number.isFinite(delayMs) && delayMs >= 0 && delayMs < remaining;
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
|
|
45
43
|
export async function withTargetRetry<T>(
|
|
46
44
|
operation: TargetOperation,
|
|
47
45
|
effect: () => Promise<T>,
|
|
48
46
|
signal: AbortSignal | undefined,
|
|
49
|
-
context: RetryContext
|
|
47
|
+
context: RetryContext
|
|
50
48
|
): Promise<T> {
|
|
51
49
|
const retrySafe = operation !== 'access';
|
|
52
50
|
const started = context.clock.now();
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { ZeroCloudV1TargetAdapter } from './zero-cloud-v1-adapter.js';
|
|
2
2
|
import type { CreateTargetAdapterOptions, TargetAdapter } from './adapter-types.js';
|
|
3
|
-
export type {
|
|
4
|
-
CreateTargetAdapterOptions,
|
|
5
|
-
CredentialInstallCapability,
|
|
6
|
-
TargetAdapter,
|
|
7
|
-
} from './adapter-types.js';
|
|
3
|
+
export type { CreateTargetAdapterOptions, TargetAdapter } from './adapter-types.js';
|
|
8
4
|
|
|
9
5
|
export function createTargetAdapter(options: CreateTargetAdapterOptions): TargetAdapter {
|
|
10
6
|
if (options.descriptor.adapter.majorVersion !== 1) {
|