@the-open-engine/zeroshot 6.17.0 → 6.19.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 +11 -14
- package/lib/agent-cli-provider/adapters/claude.d.ts +2 -2
- package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/claude.js +30 -0
- package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.d.ts +2 -2
- package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.js +37 -0
- package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
- package/lib/agent-cli-provider/adapters/gemini.d.ts +2 -2
- package/lib/agent-cli-provider/adapters/gemini.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/gemini.js +115 -4
- package/lib/agent-cli-provider/adapters/gemini.js.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode.d.ts +2 -2
- package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode.js +108 -0
- package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
- package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-options.js +12 -0
- package/lib/agent-cli-provider/contract-options.js.map +1 -1
- package/lib/agent-cli-provider/index.d.ts +2 -2
- package/lib/agent-cli-provider/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/index.js +2 -1
- package/lib/agent-cli-provider/index.js.map +1 -1
- package/lib/agent-cli-provider/omp-release.d.ts +15 -0
- package/lib/agent-cli-provider/omp-release.d.ts.map +1 -0
- package/lib/agent-cli-provider/omp-release.js +50 -0
- package/lib/agent-cli-provider/omp-release.js.map +1 -0
- package/lib/agent-cli-provider/omp-rpc-protocol.d.ts +38 -0
- package/lib/agent-cli-provider/omp-rpc-protocol.d.ts.map +1 -0
- package/lib/agent-cli-provider/omp-rpc-protocol.js +267 -0
- package/lib/agent-cli-provider/omp-rpc-protocol.js.map +1 -0
- package/lib/agent-cli-provider/provider-registry.d.ts +35 -8
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +29 -1
- package/lib/agent-cli-provider/provider-registry.js.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.js +36 -5
- package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +30 -3
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/cluster/errors.cjs +4 -1
- package/lib/cluster/errors.d.ts +2 -0
- package/lib/cluster/errors.mjs +2 -0
- package/lib/cluster/index.cjs +8 -1
- package/lib/cluster/index.d.ts +4 -1
- package/lib/cluster/index.mjs +4 -1
- package/lib/cluster/json-source.cjs +61 -0
- package/lib/cluster/json-source.d.ts +4 -0
- package/lib/cluster/json-source.mjs +55 -0
- package/lib/cluster/payload-value.cjs +76 -0
- package/lib/cluster/payload-value.d.ts +7 -0
- package/lib/cluster/payload-value.mjs +72 -0
- package/lib/cluster/validators.cjs +21 -0
- package/lib/cluster/validators.d.ts +4 -1
- package/lib/cluster/validators.mjs +19 -1
- package/lib/provider-names.js +10 -0
- package/lib/settings.js +6 -3
- package/lib/setup-plan.js +5 -5
- package/lib/start-cluster.js +3 -3
- package/package.json +2 -2
- package/src/agent/agent-lifecycle.js +4 -4
- package/src/agent/agent-task-executor.js +201 -416
- package/src/agent/output-extraction.js +39 -14
- package/src/agent/output-reformatter.js +154 -111
- package/src/agent/pr-verification.js +4 -1
- package/src/agent-cli-provider/adapters/claude.ts +39 -2
- package/src/agent-cli-provider/adapters/codex.ts +56 -2
- package/src/agent-cli-provider/adapters/gemini.ts +102 -6
- package/src/agent-cli-provider/adapters/opencode.ts +85 -2
- package/src/agent-cli-provider/contract-options.ts +16 -0
- package/src/agent-cli-provider/index.ts +4 -0
- package/src/agent-cli-provider/omp-release.ts +63 -0
- package/src/agent-cli-provider/omp-rpc-protocol.ts +396 -0
- package/src/agent-cli-provider/provider-registry.ts +52 -4
- package/src/agent-cli-provider/single-agent-runtime.ts +52 -5
- package/src/agent-cli-provider/types.ts +36 -2
- package/src/agent-wrapper.js +5 -5
- package/src/claude-task-runner.js +5 -3
- package/src/cluster/errors.ts +1 -0
- package/src/cluster/index.ts +4 -0
- package/src/cluster/json-source.ts +71 -0
- package/src/cluster/payload-value.ts +84 -0
- package/src/cluster/validators.ts +31 -2
- package/src/config-validator.js +3 -2
- package/src/isolation-manager.js +6 -2
- package/src/orchestrator.js +36 -16
- package/src/preflight.js +2 -1
- package/task-lib/attachable-watcher.js +1 -0
- package/task-lib/command-spec-cleanup.js +57 -11
- package/task-lib/commands/run.js +1 -0
- package/task-lib/provider-session-capture.js +8 -0
- package/task-lib/runner.js +9 -6
- package/task-lib/watcher.js +1 -0
|
@@ -11,17 +11,14 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
const { spawn, spawnSync } = require('child_process');
|
|
14
|
-
const { randomUUID } = require('crypto');
|
|
15
14
|
const path = require('path');
|
|
16
15
|
const fs = require('fs');
|
|
17
|
-
const {
|
|
18
|
-
getNestedExecutionRegistry,
|
|
19
|
-
TaskExecutionHandle,
|
|
20
|
-
} = require('./task-execution-handle');
|
|
16
|
+
const { getNestedExecutionRegistry, TaskExecutionHandle } = require('./task-execution-handle');
|
|
21
17
|
const os = require('os');
|
|
22
18
|
const { parseProviderChunk, getProvider } = require('../providers');
|
|
23
19
|
const { getTask, getTaskBySpawnOwnershipToken } = require('../../task-lib/store.js');
|
|
24
20
|
const { loadSettings } = require('../../lib/settings.js');
|
|
21
|
+
const { getDefaultProviderId } = require('../../lib/provider-names');
|
|
25
22
|
const { resolveClaudeAuth } = require('../../lib/settings/claude-auth.js');
|
|
26
23
|
const { prependWorktreeToolBinToEnv } = require('../worktree-tooling-env.js');
|
|
27
24
|
const { applyDarwinKeychainBoundaryToEnv } = require('../darwin-keychain-boundary.js');
|
|
@@ -54,207 +51,6 @@ const {
|
|
|
54
51
|
} = require('./provider-session');
|
|
55
52
|
const { extractClaudeVertexModelError } = require('./output-extraction');
|
|
56
53
|
const TASK_TERMINAL_STATUSES = new Set(['completed', 'failed', 'killed', 'stale']);
|
|
57
|
-
const OPENCODE_CONFIG_CONTENT_ENV = 'OPENCODE_CONFIG_CONTENT';
|
|
58
|
-
const OPENCODE_AGENT_ENV = 'ZEROSHOT_OPENCODE_AGENT';
|
|
59
|
-
const REFORMATTER_AGENT_PREFIX = 'zeroshot-output-reformatter-';
|
|
60
|
-
|
|
61
|
-
function parseOpenCodeJsonc(content) {
|
|
62
|
-
let withoutComments = '';
|
|
63
|
-
let inString = false;
|
|
64
|
-
let escaped = false;
|
|
65
|
-
let cursor = 0;
|
|
66
|
-
while (cursor < content.length) {
|
|
67
|
-
const char = content[cursor];
|
|
68
|
-
const next = content[cursor + 1];
|
|
69
|
-
if (inString) {
|
|
70
|
-
withoutComments += char;
|
|
71
|
-
if (escaped) {
|
|
72
|
-
escaped = false;
|
|
73
|
-
} else if (char === '\\') {
|
|
74
|
-
escaped = true;
|
|
75
|
-
} else if (char === '"') {
|
|
76
|
-
inString = false;
|
|
77
|
-
}
|
|
78
|
-
cursor += 1;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
if (char === '"') {
|
|
82
|
-
inString = true;
|
|
83
|
-
withoutComments += char;
|
|
84
|
-
cursor += 1;
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
if (char === '/' && next === '/') {
|
|
88
|
-
cursor += 2;
|
|
89
|
-
while (cursor < content.length && content[cursor] !== '\n') cursor += 1;
|
|
90
|
-
withoutComments += '\n';
|
|
91
|
-
cursor += 1;
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
if (char === '/' && next === '*') {
|
|
95
|
-
cursor += 2;
|
|
96
|
-
while (
|
|
97
|
-
cursor < content.length &&
|
|
98
|
-
!(content[cursor] === '*' && content[cursor + 1] === '/')
|
|
99
|
-
) {
|
|
100
|
-
if (content[cursor] === '\n') withoutComments += '\n';
|
|
101
|
-
cursor += 1;
|
|
102
|
-
}
|
|
103
|
-
if (cursor >= content.length) {
|
|
104
|
-
throw new SyntaxError('Unterminated block comment in OpenCode inline config');
|
|
105
|
-
}
|
|
106
|
-
cursor += 2;
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
withoutComments += char;
|
|
110
|
-
cursor += 1;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
let withoutTrailingCommas = '';
|
|
114
|
-
inString = false;
|
|
115
|
-
escaped = false;
|
|
116
|
-
for (let index = 0; index < withoutComments.length; index++) {
|
|
117
|
-
const char = withoutComments[index];
|
|
118
|
-
if (inString) {
|
|
119
|
-
withoutTrailingCommas += char;
|
|
120
|
-
if (escaped) {
|
|
121
|
-
escaped = false;
|
|
122
|
-
} else if (char === '\\') {
|
|
123
|
-
escaped = true;
|
|
124
|
-
} else if (char === '"') {
|
|
125
|
-
inString = false;
|
|
126
|
-
}
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
if (char === '"') {
|
|
130
|
-
inString = true;
|
|
131
|
-
withoutTrailingCommas += char;
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
if (char === ',') {
|
|
135
|
-
let nextIndex = index + 1;
|
|
136
|
-
while (/\s/.test(withoutComments[nextIndex] || '')) nextIndex++;
|
|
137
|
-
if (withoutComments[nextIndex] === '}' || withoutComments[nextIndex] === ']') {
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
withoutTrailingCommas += char;
|
|
142
|
-
}
|
|
143
|
-
return JSON.parse(withoutTrailingCommas);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function ensureFormatterLaunchOptions(providerName, options) {
|
|
147
|
-
if (providerName !== 'opencode' || options.disableTools !== true) return options;
|
|
148
|
-
if (options.formatterAgentName) return options;
|
|
149
|
-
return {
|
|
150
|
-
...options,
|
|
151
|
-
formatterAgentName: `${REFORMATTER_AGENT_PREFIX}${randomUUID()}`,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function applyOpenCodeToolBoundary(env, providerName, options = {}) {
|
|
156
|
-
if (providerName !== 'opencode' || options.disableTools !== true) return env;
|
|
157
|
-
if (!options.formatterAgentName) {
|
|
158
|
-
throw new Error('Tool-disabled OpenCode formatter launch is missing its unique agent identity');
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
let config = {};
|
|
162
|
-
const existingContent = env[OPENCODE_CONFIG_CONTENT_ENV];
|
|
163
|
-
if (existingContent) {
|
|
164
|
-
try {
|
|
165
|
-
config = parseOpenCodeJsonc(existingContent);
|
|
166
|
-
} catch (error) {
|
|
167
|
-
throw new Error(
|
|
168
|
-
`Cannot install tool-disabled OpenCode formatter profile: invalid ${OPENCODE_CONFIG_CONTENT_ENV}: ${error.message}`
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const existingAgents =
|
|
174
|
-
config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)
|
|
175
|
-
? config.agent
|
|
176
|
-
: {};
|
|
177
|
-
const existingModes =
|
|
178
|
-
config.mode && typeof config.mode === 'object' && !Array.isArray(config.mode)
|
|
179
|
-
? config.mode
|
|
180
|
-
: {};
|
|
181
|
-
const formatterProfile = {
|
|
182
|
-
description: 'Convert supplied text to schema-valid JSON without external actions',
|
|
183
|
-
mode: 'primary',
|
|
184
|
-
permission: { '*': 'deny' },
|
|
185
|
-
tools: { '*': false },
|
|
186
|
-
};
|
|
187
|
-
env[OPENCODE_AGENT_ENV] = options.formatterAgentName;
|
|
188
|
-
env[OPENCODE_CONFIG_CONTENT_ENV] = JSON.stringify({
|
|
189
|
-
...config,
|
|
190
|
-
default_agent: options.formatterAgentName,
|
|
191
|
-
permission: 'deny',
|
|
192
|
-
tools: { '*': false },
|
|
193
|
-
agent: {
|
|
194
|
-
...existingAgents,
|
|
195
|
-
[options.formatterAgentName]: formatterProfile,
|
|
196
|
-
},
|
|
197
|
-
mode: {
|
|
198
|
-
...existingModes,
|
|
199
|
-
[options.formatterAgentName]: formatterProfile,
|
|
200
|
-
},
|
|
201
|
-
});
|
|
202
|
-
return env;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function resolveIsolatedOpenCodeConfigContent(manager, clusterId, providerName) {
|
|
206
|
-
if (
|
|
207
|
-
providerName === 'opencode' &&
|
|
208
|
-
typeof manager.getContainerEnvironmentValue === 'function'
|
|
209
|
-
) {
|
|
210
|
-
return manager.getContainerEnvironmentValue(clusterId, OPENCODE_CONFIG_CONTENT_ENV);
|
|
211
|
-
}
|
|
212
|
-
return process.env[OPENCODE_CONFIG_CONTENT_ENV] || null;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
async function resolveIsolatedOpenCodeConfigUnderOwnership({
|
|
216
|
-
manager,
|
|
217
|
-
clusterId,
|
|
218
|
-
providerName,
|
|
219
|
-
executionHandle,
|
|
220
|
-
}) {
|
|
221
|
-
if (!executionHandle) {
|
|
222
|
-
return resolveIsolatedOpenCodeConfigContent(manager, clusterId, providerName);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
let setupPending = true;
|
|
226
|
-
let rejectSetup;
|
|
227
|
-
const setupFailure = new Promise((_resolve, reject) => {
|
|
228
|
-
rejectSetup = reject;
|
|
229
|
-
});
|
|
230
|
-
executionHandle.setCancelAction((reason, details = {}) => {
|
|
231
|
-
if (setupPending) {
|
|
232
|
-
const error = new Error(reason || 'Nested task setup cancelled');
|
|
233
|
-
error.code = details.code || 'REFORMAT_CANCELLED';
|
|
234
|
-
error.nestedExecutionCancellation = true;
|
|
235
|
-
error.nestedExecutionLifecycle = true;
|
|
236
|
-
rejectSetup(error);
|
|
237
|
-
}
|
|
238
|
-
return { forced: true, beforeLaunch: true };
|
|
239
|
-
});
|
|
240
|
-
executionHandle.setFailClosedAction((error) => {
|
|
241
|
-
if (setupPending) rejectSetup(error);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
try {
|
|
245
|
-
const config = await Promise.race([
|
|
246
|
-
Promise.resolve(resolveIsolatedOpenCodeConfigContent(manager, clusterId, providerName)),
|
|
247
|
-
setupFailure,
|
|
248
|
-
]);
|
|
249
|
-
if (executionHandle.isCancelled) {
|
|
250
|
-
throw createNestedCancellationError(executionHandle);
|
|
251
|
-
}
|
|
252
|
-
return config;
|
|
253
|
-
} finally {
|
|
254
|
-
setupPending = false;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
54
|
function runCommandWithTimeout(command, args, options = {}, callback = null) {
|
|
259
55
|
const timeout = options.timeout ?? 30000;
|
|
260
56
|
if (timeout <= 0) {
|
|
@@ -326,9 +122,6 @@ function runCommandSync(command, args, options = {}) {
|
|
|
326
122
|
return result.stdout?.toString() || '';
|
|
327
123
|
}
|
|
328
124
|
|
|
329
|
-
// Schema utilities for normalizing LLM output
|
|
330
|
-
const { normalizeEnumValues } = require('./schema-utils');
|
|
331
|
-
|
|
332
125
|
/**
|
|
333
126
|
* Build Claude-specific environment variables for task spawning
|
|
334
127
|
* Consolidates auth resolution and model mapping logic used by both isolated and non-isolated modes
|
|
@@ -607,7 +400,7 @@ function extractErrorContext({ output, statusOutput, taskId, isNotFound = false,
|
|
|
607
400
|
* @param {string} output - Full NDJSON output from Claude CLI
|
|
608
401
|
* @returns {Object|null} Token usage data or null if not found
|
|
609
402
|
*/
|
|
610
|
-
function extractTokenUsage(output, providerName =
|
|
403
|
+
function extractTokenUsage(output, providerName = getDefaultProviderId()) {
|
|
611
404
|
if (!output) return null;
|
|
612
405
|
|
|
613
406
|
const events = parseProviderChunk(providerName, output);
|
|
@@ -729,7 +522,7 @@ async function settleRegisteredNestedHandle(registry, handle) {
|
|
|
729
522
|
}
|
|
730
523
|
|
|
731
524
|
async function spawnClaudeTask(agent, context, options = {}) {
|
|
732
|
-
const providerName = agent._resolveProvider ? agent._resolveProvider() :
|
|
525
|
+
const providerName = agent._resolveProvider ? agent._resolveProvider() : getDefaultProviderId();
|
|
733
526
|
const modelSpec = resolveAgentModelSpec(agent);
|
|
734
527
|
|
|
735
528
|
const ctPath = agent.taskCliPath || getClaudeTasksPath();
|
|
@@ -740,6 +533,7 @@ async function spawnClaudeTask(agent, context, options = {}) {
|
|
|
740
533
|
providerName,
|
|
741
534
|
modelSpec,
|
|
742
535
|
runOutputFormat,
|
|
536
|
+
structuredOutputRecovery: options.structuredOutputRecovery === true,
|
|
743
537
|
});
|
|
744
538
|
|
|
745
539
|
maybeLogStreamJsonNotice(agent, runOutputFormat);
|
|
@@ -766,7 +560,6 @@ async function spawnClaudeTask(agent, context, options = {}) {
|
|
|
766
560
|
if (agent.isolation?.enabled) {
|
|
767
561
|
return spawnClaudeTaskIsolated(agent, context, options);
|
|
768
562
|
}
|
|
769
|
-
options = ensureFormatterLaunchOptions(providerName, options);
|
|
770
563
|
|
|
771
564
|
const claudeSettingsPath =
|
|
772
565
|
providerName === 'claude'
|
|
@@ -781,11 +574,7 @@ async function spawnClaudeTask(agent, context, options = {}) {
|
|
|
781
574
|
let pendingLaunch;
|
|
782
575
|
try {
|
|
783
576
|
try {
|
|
784
|
-
const spawnEnv = buildSpawnEnv(agent, providerName, modelSpec, {
|
|
785
|
-
claudeSettingsPath,
|
|
786
|
-
disableTools: options.disableTools === true,
|
|
787
|
-
formatterAgentName: options.formatterAgentName,
|
|
788
|
-
});
|
|
577
|
+
const spawnEnv = buildSpawnEnv(agent, providerName, modelSpec, { claudeSettingsPath });
|
|
789
578
|
taskId = await spawnTaskProcess({
|
|
790
579
|
agent,
|
|
791
580
|
ctPath,
|
|
@@ -866,11 +655,7 @@ async function spawnClaudeTask(agent, context, options = {}) {
|
|
|
866
655
|
if (error.terminationExhausted === true && error.retainTaskHandle === true) {
|
|
867
656
|
throw error;
|
|
868
657
|
}
|
|
869
|
-
if (
|
|
870
|
-
nested &&
|
|
871
|
-
handle.isCancelled &&
|
|
872
|
-
handle.cancelDetails.code !== 'NESTED_SETUP_FAILED'
|
|
873
|
-
) {
|
|
658
|
+
if (nested && handle.isCancelled && handle.cancelDetails.code !== 'NESTED_SETUP_FAILED') {
|
|
874
659
|
const termination = await handle.waitForCancellation();
|
|
875
660
|
const cancellationError = createNestedCancellationError(handle);
|
|
876
661
|
if (!isTerminationConfirmed(termination)) {
|
|
@@ -907,28 +692,32 @@ function resolveOutputFormatConfig(agent) {
|
|
|
907
692
|
return { desiredOutputFormat, strictSchema, runOutputFormat };
|
|
908
693
|
}
|
|
909
694
|
|
|
910
|
-
function buildTaskRunArgs({
|
|
695
|
+
function buildTaskRunArgs({
|
|
696
|
+
agent,
|
|
697
|
+
providerName,
|
|
698
|
+
modelSpec,
|
|
699
|
+
runOutputFormat,
|
|
700
|
+
structuredOutputRecovery = false,
|
|
701
|
+
}) {
|
|
911
702
|
const args = ['task', 'run', '--output-format', runOutputFormat, '--provider', providerName];
|
|
912
703
|
const modelSpecSource = agent._resolveModelSpecSource
|
|
913
704
|
? agent._resolveModelSpecSource()
|
|
914
705
|
: 'direct';
|
|
915
706
|
appendTaskRunModelArgs(args, modelSpec, modelSpecSource);
|
|
916
707
|
|
|
917
|
-
// Add verification mode flag if configured
|
|
918
708
|
if (agent.config.verificationMode) {
|
|
919
709
|
args.push('-v');
|
|
920
710
|
}
|
|
921
711
|
|
|
922
|
-
// Add JSON schema if specified in agent config.
|
|
923
|
-
// If we are running stream-json for live logs (strictSchema=false), do NOT pass schema to CLI.
|
|
924
712
|
if (agent.config.jsonSchema && runOutputFormat === 'json') {
|
|
925
|
-
|
|
926
|
-
|
|
713
|
+
args.push('--json-schema', JSON.stringify(agent.config.jsonSchema));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (structuredOutputRecovery) {
|
|
717
|
+
args.push('--structured-output-recovery');
|
|
718
|
+
return args;
|
|
927
719
|
}
|
|
928
720
|
|
|
929
|
-
// MCP servers: explicitly forward the repo config through the task CLI.
|
|
930
|
-
// Claude receives the path; providers such as Copilot receive inlined JSON
|
|
931
|
-
// so it survives container path translation.
|
|
932
721
|
for (const mcpArg of resolveMcpConfigArgs(agent, providerName)) {
|
|
933
722
|
args.push(mcpArg);
|
|
934
723
|
}
|
|
@@ -1056,7 +845,7 @@ function buildSpawnEnv(agent, providerName, modelSpec, options = {}) {
|
|
|
1056
845
|
worktreePath: agent.worktree?.path || null,
|
|
1057
846
|
});
|
|
1058
847
|
|
|
1059
|
-
return
|
|
848
|
+
return spawnEnv;
|
|
1060
849
|
}
|
|
1061
850
|
|
|
1062
851
|
function parseTaskIdFromOutput(stdout) {
|
|
@@ -1073,6 +862,27 @@ function assignDurableTaskId(agent, taskId) {
|
|
|
1073
862
|
});
|
|
1074
863
|
}
|
|
1075
864
|
|
|
865
|
+
async function terminatePendingTaskWrapper(proc, waitForWrapperClose, isWrapperClosed) {
|
|
866
|
+
if (isWrapperClosed()) return;
|
|
867
|
+
if (!proc.pid) {
|
|
868
|
+
await new Promise((resolve) => {
|
|
869
|
+
let settled = false;
|
|
870
|
+
const finish = () => {
|
|
871
|
+
if (settled) return;
|
|
872
|
+
settled = true;
|
|
873
|
+
proc.off('spawn', finish);
|
|
874
|
+
proc.off('error', finish);
|
|
875
|
+
resolve();
|
|
876
|
+
};
|
|
877
|
+
proc.once('spawn', finish);
|
|
878
|
+
proc.once('error', finish);
|
|
879
|
+
if (proc.pid || isWrapperClosed()) finish();
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
if (!isWrapperClosed()) proc.kill('SIGKILL');
|
|
883
|
+
await waitForWrapperClose;
|
|
884
|
+
}
|
|
885
|
+
|
|
1076
886
|
function createPendingTaskLaunchHandle({
|
|
1077
887
|
agent,
|
|
1078
888
|
proc,
|
|
@@ -1112,10 +922,7 @@ function createPendingTaskLaunchHandle({
|
|
|
1112
922
|
};
|
|
1113
923
|
if (taskId) await cancelTask(taskId);
|
|
1114
924
|
|
|
1115
|
-
|
|
1116
|
-
proc.kill('SIGKILL');
|
|
1117
|
-
await waitForWrapperClose;
|
|
1118
|
-
}
|
|
925
|
+
await terminatePendingTaskWrapper(proc, waitForWrapperClose, isWrapperClosed);
|
|
1119
926
|
|
|
1120
927
|
taskId = taskId || (await findLateTaskId());
|
|
1121
928
|
if (taskId && !commandAttempted) await cancelTask(taskId);
|
|
@@ -1165,6 +972,7 @@ function spawnTaskProcess({
|
|
|
1165
972
|
spawnTimeoutMs = 30000,
|
|
1166
973
|
handle = null,
|
|
1167
974
|
nested = false,
|
|
975
|
+
spawnProcess = spawn,
|
|
1168
976
|
}) {
|
|
1169
977
|
// Timeout for spawn phase - if CLI hangs during init (e.g., opencode 429 bug), kill it.
|
|
1170
978
|
const SPAWN_TIMEOUT_MS = spawnTimeoutMs;
|
|
@@ -1174,7 +982,7 @@ function spawnTaskProcess({
|
|
|
1174
982
|
const findPersistedTaskId = () => getTaskBySpawnOwnershipToken(ownershipToken)?.id || null;
|
|
1175
983
|
|
|
1176
984
|
return new Promise((resolve, reject) => {
|
|
1177
|
-
const proc =
|
|
985
|
+
const proc = spawnProcess(ctPath, safeArgs, {
|
|
1178
986
|
cwd,
|
|
1179
987
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1180
988
|
env: { ...spawnEnv, [TASK_SPAWN_OWNERSHIP_TOKEN_ENV]: ownershipToken },
|
|
@@ -1543,7 +1351,7 @@ function requiresStructuredResult(agent) {
|
|
|
1543
1351
|
return outputFormat !== 'text' || !!agent?.config?.jsonSchema;
|
|
1544
1352
|
}
|
|
1545
1353
|
|
|
1546
|
-
async function evaluateStructuredSuccess({ agent, taskId, state, success }) {
|
|
1354
|
+
async function evaluateStructuredSuccess({ agent, taskId, state, success, allowRecovery }) {
|
|
1547
1355
|
if (!success || !requiresStructuredResult(agent)) {
|
|
1548
1356
|
return { success, error: null };
|
|
1549
1357
|
}
|
|
@@ -1556,10 +1364,16 @@ async function evaluateStructuredSuccess({ agent, taskId, state, success }) {
|
|
|
1556
1364
|
// Cache the validated parsed object so completion hooks and {{result.*}}
|
|
1557
1365
|
// substitution consume it directly instead of re-parsing (which could
|
|
1558
1366
|
// trigger a second recovery model call).
|
|
1559
|
-
state._cachedParsedResult = await agent._parseResultOutput(state.output);
|
|
1367
|
+
state._cachedParsedResult = await agent._parseResultOutput(state.output, { allowRecovery });
|
|
1560
1368
|
return { success: true, error: null };
|
|
1561
1369
|
} catch (error) {
|
|
1562
|
-
if (
|
|
1370
|
+
if (
|
|
1371
|
+
isNestedLifecycleError(error) ||
|
|
1372
|
+
error?.permanent === true ||
|
|
1373
|
+
error?.recoveryAbort === true
|
|
1374
|
+
) {
|
|
1375
|
+
throw error;
|
|
1376
|
+
}
|
|
1563
1377
|
const errorContext = sanitizeErrorMessage(error.message);
|
|
1564
1378
|
console.warn(
|
|
1565
1379
|
`[Agent ${agent.id}] Task ${taskId} reported completed but produced invalid structured output; ` +
|
|
@@ -1595,9 +1409,16 @@ async function buildCompletionResult({
|
|
|
1595
1409
|
success,
|
|
1596
1410
|
taskInfo = getTask(taskId),
|
|
1597
1411
|
}) {
|
|
1412
|
+
const { isCompleted } = parseStatusFlags(stripAnsiCodes(stdout));
|
|
1598
1413
|
const classified = state.skipStructuredResultCheck
|
|
1599
1414
|
? { success, error: null }
|
|
1600
|
-
: await evaluateStructuredSuccess({
|
|
1415
|
+
: await evaluateStructuredSuccess({
|
|
1416
|
+
agent,
|
|
1417
|
+
taskId,
|
|
1418
|
+
state,
|
|
1419
|
+
success,
|
|
1420
|
+
allowRecovery: isCompleted,
|
|
1421
|
+
});
|
|
1601
1422
|
const resumeIdentityError = classified.success ? validateCompletedResumeIdentity(taskInfo) : null;
|
|
1602
1423
|
if (resumeIdentityError) {
|
|
1603
1424
|
classified.success = false;
|
|
@@ -1802,18 +1623,7 @@ function handleStatusCompletion({
|
|
|
1802
1623
|
success,
|
|
1803
1624
|
})
|
|
1804
1625
|
.then(resolve)
|
|
1805
|
-
.catch(
|
|
1806
|
-
if (isNestedLifecycleError(error)) {
|
|
1807
|
-
reject(error);
|
|
1808
|
-
return;
|
|
1809
|
-
}
|
|
1810
|
-
resolve({
|
|
1811
|
-
success: false,
|
|
1812
|
-
output: state.output,
|
|
1813
|
-
error: sanitizeErrorMessage(error.message),
|
|
1814
|
-
tokenUsage: extractTokenUsage(state.output, providerName),
|
|
1815
|
-
});
|
|
1816
|
-
});
|
|
1626
|
+
.catch(reject);
|
|
1817
1627
|
}, 500);
|
|
1818
1628
|
|
|
1819
1629
|
return true;
|
|
@@ -1939,7 +1749,7 @@ function createLogFollower({
|
|
|
1939
1749
|
function followClaudeTaskLogs(agent, taskId, options = {}) {
|
|
1940
1750
|
const fsModule = require('fs');
|
|
1941
1751
|
const ctPath = agent.taskCliPath || getClaudeTasksPath();
|
|
1942
|
-
const providerName = agent._resolveProvider ? agent._resolveProvider() :
|
|
1752
|
+
const providerName = agent._resolveProvider ? agent._resolveProvider() : getDefaultProviderId();
|
|
1943
1753
|
|
|
1944
1754
|
return createLogFollower({
|
|
1945
1755
|
agent,
|
|
@@ -2033,8 +1843,7 @@ async function spawnClaudeTaskIsolated(agent, context, options = {}) {
|
|
|
2033
1843
|
|
|
2034
1844
|
async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
|
|
2035
1845
|
const { manager, clusterId } = agent.isolation;
|
|
2036
|
-
const providerName = agent._resolveProvider ? agent._resolveProvider() :
|
|
2037
|
-
options = ensureFormatterLaunchOptions(providerName, options);
|
|
1846
|
+
const providerName = agent._resolveProvider ? agent._resolveProvider() : getDefaultProviderId();
|
|
2038
1847
|
const modelSpec = resolveAgentModelSpec(agent);
|
|
2039
1848
|
const modelSpecSource = agent._resolveModelSpecSource
|
|
2040
1849
|
? agent._resolveModelSpecSource()
|
|
@@ -2050,6 +1859,7 @@ async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
|
|
|
2050
1859
|
providerName,
|
|
2051
1860
|
modelSpec,
|
|
2052
1861
|
runOutputFormat,
|
|
1862
|
+
structuredOutputRecovery: options.structuredOutputRecovery === true,
|
|
2053
1863
|
}),
|
|
2054
1864
|
];
|
|
2055
1865
|
maybeLogStreamJsonNotice(agent, runOutputFormat);
|
|
@@ -2074,26 +1884,10 @@ async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
|
|
|
2074
1884
|
const ownershipToken = createTaskSpawnOwnershipToken();
|
|
2075
1885
|
// Auth env vars are injected by IsolationManager; the launch token is the
|
|
2076
1886
|
// only authoritative bridge back to the detached task row in the container.
|
|
2077
|
-
const
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
clusterId,
|
|
2082
|
-
providerName,
|
|
2083
|
-
executionHandle: options.executionHandle,
|
|
2084
|
-
})
|
|
2085
|
-
: null;
|
|
2086
|
-
const isolatedEnv = applyOpenCodeToolBoundary(
|
|
2087
|
-
{
|
|
2088
|
-
...(providerName === 'claude' ? buildClaudeEnv(modelSpec, { includeAuth: false }) : {}),
|
|
2089
|
-
...(effectiveOpenCodeConfig
|
|
2090
|
-
? { [OPENCODE_CONFIG_CONTENT_ENV]: effectiveOpenCodeConfig }
|
|
2091
|
-
: {}),
|
|
2092
|
-
[TASK_SPAWN_OWNERSHIP_TOKEN_ENV]: ownershipToken,
|
|
2093
|
-
},
|
|
2094
|
-
providerName,
|
|
2095
|
-
options
|
|
2096
|
-
);
|
|
1887
|
+
const isolatedEnv = {
|
|
1888
|
+
...(providerName === 'claude' ? buildClaudeEnv(modelSpec, { includeAuth: false }) : {}),
|
|
1889
|
+
[TASK_SPAWN_OWNERSHIP_TOKEN_ENV]: ownershipToken,
|
|
1890
|
+
};
|
|
2097
1891
|
|
|
2098
1892
|
if (options.executionHandle?.isCancelled) {
|
|
2099
1893
|
throw createNestedCancellationError(options.executionHandle);
|
|
@@ -2156,8 +1950,7 @@ async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
|
|
|
2156
1950
|
rejection.terminationExhausted = true;
|
|
2157
1951
|
rejection.terminationAttempts = 1;
|
|
2158
1952
|
if (options.nested) options.executionHandle?.retainOwnership();
|
|
2159
|
-
rejection.taskId =
|
|
2160
|
-
isolatedTaskId || (!options.nested ? agent.currentTaskId : null) || null;
|
|
1953
|
+
rejection.taskId = isolatedTaskId || (!options.nested ? agent.currentTaskId : null) || null;
|
|
2161
1954
|
} else if (!options.nested && agent.currentTask === isolatedPendingLaunch) {
|
|
2162
1955
|
agent.currentTask = null;
|
|
2163
1956
|
}
|
|
@@ -2184,14 +1977,10 @@ async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
|
|
|
2184
1977
|
commandError = error;
|
|
2185
1978
|
}
|
|
2186
1979
|
|
|
2187
|
-
|
|
2188
|
-
proc.kill('SIGKILL');
|
|
2189
|
-
await waitForWrapperClose;
|
|
2190
|
-
}
|
|
1980
|
+
await terminatePendingTaskWrapper(proc, waitForWrapperClose, () => wrapperClosed);
|
|
2191
1981
|
|
|
2192
1982
|
try {
|
|
2193
|
-
const persistedTaskId =
|
|
2194
|
-
isolatedTaskId || (await findLatePersistedTaskId());
|
|
1983
|
+
const persistedTaskId = isolatedTaskId || (await findLatePersistedTaskId());
|
|
2195
1984
|
if (persistedTaskId && !termination) {
|
|
2196
1985
|
termination = await terminateIsolatedTask(manager, clusterId, persistedTaskId);
|
|
2197
1986
|
commandError = null;
|
|
@@ -2214,8 +2003,8 @@ async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
|
|
|
2214
2003
|
if (!resolved) {
|
|
2215
2004
|
const error =
|
|
2216
2005
|
(sourceError instanceof Error &&
|
|
2217
|
-
|
|
2218
|
-
|
|
2006
|
+
sourceError.code === 'unsupported-capability' &&
|
|
2007
|
+
sourceError.permanent === true
|
|
2219
2008
|
? sourceError
|
|
2220
2009
|
: null) ||
|
|
2221
2010
|
timeoutError ||
|
|
@@ -2456,8 +2245,7 @@ async function terminateIsolatedTask(manager, clusterId, taskId) {
|
|
|
2456
2245
|
);
|
|
2457
2246
|
}
|
|
2458
2247
|
return {
|
|
2459
|
-
alreadyTerminal:
|
|
2460
|
-
Boolean(beforeStatus) || Boolean(afterStatus && afterStatus !== 'killed'),
|
|
2248
|
+
alreadyTerminal: Boolean(beforeStatus) || Boolean(afterStatus && afterStatus !== 'killed'),
|
|
2461
2249
|
forced: !beforeStatus && afterStatus === 'killed',
|
|
2462
2250
|
status: beforeStatus || afterStatus,
|
|
2463
2251
|
};
|
|
@@ -2526,29 +2314,49 @@ function settleIsolatedTerminalStatus({
|
|
|
2526
2314
|
useVertex: process.env.CLAUDE_CODE_USE_VERTEX === '1',
|
|
2527
2315
|
})
|
|
2528
2316
|
: null;
|
|
2529
|
-
const
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2317
|
+
const staleCandidate =
|
|
2318
|
+
status === 'stale' &&
|
|
2319
|
+
!vertexModelError &&
|
|
2320
|
+
determineStaleSuccess({ agent, output: state.fullOutput, providerName, taskId });
|
|
2321
|
+
let success = (status === 'completed' || staleCandidate) && !vertexModelError;
|
|
2322
|
+
let structuredError = null;
|
|
2323
|
+
if (staleCandidate && !state.skipStructuredResultCheck) {
|
|
2324
|
+
const evaluated = await evaluateStructuredSuccess({
|
|
2325
|
+
agent,
|
|
2326
|
+
taskId,
|
|
2327
|
+
state,
|
|
2328
|
+
success,
|
|
2329
|
+
allowRecovery: false,
|
|
2330
|
+
});
|
|
2331
|
+
success = evaluated.success;
|
|
2332
|
+
structuredError = evaluated.error;
|
|
2333
|
+
}
|
|
2334
|
+
const errorContext =
|
|
2335
|
+
structuredError ||
|
|
2336
|
+
(!success
|
|
2337
|
+
? extractErrorContext({
|
|
2338
|
+
output: state.fullOutput,
|
|
2339
|
+
statusOutput: status ? `Status: ${status}` : '',
|
|
2340
|
+
taskId,
|
|
2341
|
+
isNotFound,
|
|
2342
|
+
debug: {
|
|
2343
|
+
agentId: agent.id,
|
|
2344
|
+
providerName,
|
|
2345
|
+
pid: agent.processPid,
|
|
2346
|
+
cwd: agent.config.cwd || process.cwd(),
|
|
2347
|
+
worktreePath: agent.worktree?.path || null,
|
|
2348
|
+
isolation: true,
|
|
2349
|
+
clusterId,
|
|
2350
|
+
logFilePath,
|
|
2351
|
+
},
|
|
2352
|
+
})
|
|
2353
|
+
: null);
|
|
2354
|
+
let parsedResult = null;
|
|
2355
|
+
if (success && !state.skipStructuredResultCheck && !vertexModelError) {
|
|
2356
|
+
parsedResult = staleCandidate
|
|
2357
|
+
? state._cachedParsedResult
|
|
2551
2358
|
: await agent._parseResultOutput(state.fullOutput);
|
|
2359
|
+
}
|
|
2552
2360
|
|
|
2553
2361
|
settleIsolatedFollower({
|
|
2554
2362
|
agent,
|
|
@@ -2836,7 +2644,7 @@ function followClaudeTaskLogsIsolated(agent, taskId, options = {}) {
|
|
|
2836
2644
|
|
|
2837
2645
|
const manager = isolation.manager;
|
|
2838
2646
|
const clusterId = isolation.clusterId;
|
|
2839
|
-
const providerName = agent._resolveProvider ? agent._resolveProvider() :
|
|
2647
|
+
const providerName = agent._resolveProvider ? agent._resolveProvider() : getDefaultProviderId();
|
|
2840
2648
|
|
|
2841
2649
|
return new Promise((resolve, reject) => {
|
|
2842
2650
|
const state = createIsolatedLogState(
|
|
@@ -2844,8 +2652,7 @@ function followClaudeTaskLogsIsolated(agent, taskId, options = {}) {
|
|
|
2844
2652
|
options.nested === true
|
|
2845
2653
|
);
|
|
2846
2654
|
const cleanup = buildIsolatedCleanup(state);
|
|
2847
|
-
const onLine = (line) =>
|
|
2848
|
-
broadcastIsolatedLine({ agent, providerName, taskId, state, line });
|
|
2655
|
+
const onLine = (line) => broadcastIsolatedLine({ agent, providerName, taskId, state, line });
|
|
2849
2656
|
state.lifecycleHandle = buildIsolatedLifecycleHandle({
|
|
2850
2657
|
agent,
|
|
2851
2658
|
manager,
|
|
@@ -2962,65 +2769,72 @@ function followClaudeTaskLogsIsolated(agent, taskId, options = {}) {
|
|
|
2962
2769
|
* @param {String} output - Raw output from agent
|
|
2963
2770
|
* @returns {Promise<Object>} Parsed result data
|
|
2964
2771
|
*/
|
|
2965
|
-
async function parseResultOutput(agent, output) {
|
|
2966
|
-
// Empty outputs = FAIL
|
|
2772
|
+
async function parseResultOutput(agent, output, { allowRecovery = true } = {}) {
|
|
2967
2773
|
if (!output || !output.trim()) {
|
|
2968
2774
|
throw new Error('Task execution failed - no output');
|
|
2969
2775
|
}
|
|
2970
2776
|
|
|
2971
|
-
const providerName = agent._resolveProvider ? agent._resolveProvider() :
|
|
2777
|
+
const providerName = agent._resolveProvider ? agent._resolveProvider() : getDefaultProviderId();
|
|
2972
2778
|
const {
|
|
2973
|
-
extractJsonFromOutput,
|
|
2974
2779
|
extractCliError,
|
|
2780
|
+
extractJsonFromOutput,
|
|
2781
|
+
extractModelTextFromOutput,
|
|
2975
2782
|
hasFatalStandaloneOutput,
|
|
2976
2783
|
} = require('./output-extraction');
|
|
2977
|
-
|
|
2978
|
-
// Check for CLI errors FIRST - surface the actual error message
|
|
2979
|
-
const cliError = extractCliError(output);
|
|
2784
|
+
const cliError = extractCliError(output, providerName);
|
|
2980
2785
|
if (cliError) {
|
|
2981
2786
|
throw new Error(`CLI error (${cliError.provider}): ${cliError.error}`);
|
|
2982
2787
|
}
|
|
2983
2788
|
|
|
2984
|
-
|
|
2985
|
-
|
|
2789
|
+
const parsed = extractJsonFromOutput(output, providerName);
|
|
2790
|
+
const schema = agent.config.jsonSchema;
|
|
2791
|
+
if (!schema) {
|
|
2792
|
+
if (parsed) return parsed;
|
|
2793
|
+
throw new Error(`Agent ${agent.id} output missing required JSON block`);
|
|
2794
|
+
}
|
|
2986
2795
|
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2796
|
+
const { createStructuredOutputValidator, reformatOutput } = require('./output-reformatter');
|
|
2797
|
+
const validateCandidate = createStructuredOutputValidator(schema);
|
|
2798
|
+
const directValidation = parsed ? validateCandidate(parsed) : null;
|
|
2799
|
+
if (directValidation?.valid) {
|
|
2800
|
+
return directValidation.value;
|
|
2801
|
+
}
|
|
2990
2802
|
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
2803
|
+
const schemaIsNonEmpty =
|
|
2804
|
+
typeof schema === 'string'
|
|
2805
|
+
? schema.trim() !== '' && schema.trim() !== '{}'
|
|
2806
|
+
: typeof schema === 'object' && schema !== null && Object.keys(schema).length > 0;
|
|
2807
|
+
const { providerSupportsOutputReformatting } = require('../../lib/provider-names');
|
|
2808
|
+
let recovery = null;
|
|
2809
|
+
if (
|
|
2810
|
+
schemaIsNonEmpty &&
|
|
2811
|
+
!hasFatalStandaloneOutput(output) &&
|
|
2812
|
+
allowRecovery &&
|
|
2813
|
+
providerSupportsOutputReformatting(providerName)
|
|
2814
|
+
) {
|
|
2815
|
+
const modelText = extractModelTextFromOutput(output, providerName);
|
|
2816
|
+
recovery = await reformatOutput({
|
|
2817
|
+
rawOutput: modelText || output,
|
|
2818
|
+
schema,
|
|
2819
|
+
providerName,
|
|
2820
|
+
initialError: directValidation?.error || null,
|
|
2821
|
+
validateCandidate,
|
|
2822
|
+
isCancelled: () => agent.running === false || agent.state === 'stopped',
|
|
2823
|
+
runReformat: (prompt) =>
|
|
2824
|
+
agent._spawnClaudeTask(prompt, {
|
|
2825
|
+
skipStructuredResultCheck: true,
|
|
2826
|
+
nested: true,
|
|
2827
|
+
structuredOutputRecovery: true,
|
|
2828
|
+
}),
|
|
2829
|
+
onAttempt: (attempt, lastError) => {
|
|
2830
|
+
const reason = lastError ? `: ${lastError}` : '';
|
|
2831
|
+
console.warn(
|
|
2832
|
+
`[Agent ${agent.id}] Correcting structured output (attempt ${attempt})${reason}`
|
|
2833
|
+
);
|
|
2834
|
+
},
|
|
2835
|
+
});
|
|
2836
|
+
if (recovery.status === 'recovered') {
|
|
2837
|
+
return recovery.value;
|
|
3024
2838
|
}
|
|
3025
2839
|
}
|
|
3026
2840
|
|
|
@@ -3028,69 +2842,40 @@ async function parseResultOutput(agent, output) {
|
|
|
3028
2842
|
if (hasFatalStandaloneOutput(output)) {
|
|
3029
2843
|
throw new Error('Task execution failed - no output');
|
|
3030
2844
|
}
|
|
3031
|
-
const
|
|
3032
|
-
|
|
3033
|
-
console.error(`🔴 AGENT OUTPUT MISSING REQUIRED JSON BLOCK`);
|
|
3034
|
-
console.error(`${'='.repeat(80)}`);
|
|
3035
|
-
console.error(`Agent: ${agent.id}, Role: ${agent.role}, Provider: ${providerName}`);
|
|
3036
|
-
console.error(`Output (last 500 chars): ${trimmedOutput.slice(-500)}`);
|
|
3037
|
-
console.error(`${'='.repeat(80)}\n`);
|
|
3038
|
-
throw new Error(`Agent ${agent.id} output missing required JSON block`);
|
|
2845
|
+
const recoveryDetail = recovery?.lastError ? ` Recovery exhausted: ${recovery.lastError}.` : '';
|
|
2846
|
+
throw new Error(`Agent ${agent.id} output missing required JSON block.${recoveryDetail}`);
|
|
3039
2847
|
}
|
|
3040
2848
|
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
// Normalize enum values BEFORE validation (handles case mismatches, common variations)
|
|
3046
|
-
// This is provider-agnostic - works for Claude CLI, Gemini, Codex, etc.
|
|
3047
|
-
normalizeEnumValues(parsed, agent.config.jsonSchema);
|
|
3048
|
-
|
|
3049
|
-
const Ajv = require('ajv');
|
|
3050
|
-
const ajv = new Ajv({
|
|
3051
|
-
allErrors: true,
|
|
3052
|
-
strict: false,
|
|
3053
|
-
coerceTypes: false, // STRICT: Reject type mismatches (e.g., null instead of array)
|
|
3054
|
-
useDefaults: true,
|
|
3055
|
-
removeAdditional: true,
|
|
3056
|
-
});
|
|
3057
|
-
const validate = ajv.compile(agent.config.jsonSchema);
|
|
3058
|
-
const valid = validate(parsed);
|
|
3059
|
-
if (!valid) {
|
|
3060
|
-
const errorList = (validate.errors || [])
|
|
3061
|
-
.slice(0, 5)
|
|
3062
|
-
.map((e) => `${e.instancePath || e.schemaPath} ${e.message}`)
|
|
3063
|
-
.join('; ');
|
|
3064
|
-
const msg =
|
|
3065
|
-
`Agent ${agent.id} output failed JSON schema validation: ` +
|
|
3066
|
-
(errorList || 'unknown schema error');
|
|
3067
|
-
|
|
3068
|
-
// Validators stay strict (they already have auto-approval fallback on crash).
|
|
3069
|
-
if (agent.role === 'validator') {
|
|
3070
|
-
throw new Error(msg);
|
|
3071
|
-
}
|
|
2849
|
+
if (!allowRecovery) {
|
|
2850
|
+
const detail = directValidation?.error || 'required structured output is missing';
|
|
2851
|
+
throw new Error(`Agent ${agent.id} output is not valid for non-completed task: ${detail}`);
|
|
2852
|
+
}
|
|
3072
2853
|
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
agent: agent.id,
|
|
3082
|
-
role: agent.role,
|
|
3083
|
-
iteration: agent.iteration,
|
|
3084
|
-
errors: validate.errors || [],
|
|
3085
|
-
},
|
|
3086
|
-
},
|
|
3087
|
-
});
|
|
3088
|
-
}
|
|
2854
|
+
const errorDetail = directValidation?.error || 'unknown schema error';
|
|
2855
|
+
const message = `Agent ${agent.id} output failed JSON schema validation: ${errorDetail}`;
|
|
2856
|
+
if (agent.role === 'validator') {
|
|
2857
|
+
throw new Error(
|
|
2858
|
+
recovery?.status === 'exhausted'
|
|
2859
|
+
? `${message}. Recovery exhausted after ${recovery.attempts} attempts: ${recovery.lastError}`
|
|
2860
|
+
: message
|
|
2861
|
+
);
|
|
3089
2862
|
}
|
|
3090
2863
|
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
2864
|
+
console.warn(`⚠️ ${message}`);
|
|
2865
|
+
agent._publish({
|
|
2866
|
+
topic: 'AGENT_SCHEMA_WARNING',
|
|
2867
|
+
receiver: 'broadcast',
|
|
2868
|
+
content: {
|
|
2869
|
+
text: message,
|
|
2870
|
+
data: {
|
|
2871
|
+
agent: agent.id,
|
|
2872
|
+
role: agent.role,
|
|
2873
|
+
iteration: agent.iteration,
|
|
2874
|
+
errors: directValidation?.errors || [],
|
|
2875
|
+
},
|
|
2876
|
+
},
|
|
2877
|
+
});
|
|
2878
|
+
return directValidation?.value || parsed;
|
|
3094
2879
|
}
|
|
3095
2880
|
|
|
3096
2881
|
/**
|