@the-open-engine/zeroshot 6.17.0 → 6.18.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 +9 -13
- 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/provider-registry.d.ts +19 -7
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +4 -0
- 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 +35 -4
- 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 +5 -0
- package/package.json +2 -2
- package/src/agent/agent-task-executor.js +194 -410
- package/src/agent/output-extraction.js +39 -14
- package/src/agent/output-reformatter.js +154 -111
- 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/provider-registry.ts +23 -3
- package/src/agent-cli-provider/single-agent-runtime.ts +50 -4
- package/src/agent-cli-provider/types.ts +36 -2
- package/src/agent-wrapper.js +2 -2
- 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/orchestrator.js +33 -13
- 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,13 +11,9 @@
|
|
|
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');
|
|
@@ -54,207 +50,6 @@ const {
|
|
|
54
50
|
} = require('./provider-session');
|
|
55
51
|
const { extractClaudeVertexModelError } = require('./output-extraction');
|
|
56
52
|
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
53
|
function runCommandWithTimeout(command, args, options = {}, callback = null) {
|
|
259
54
|
const timeout = options.timeout ?? 30000;
|
|
260
55
|
if (timeout <= 0) {
|
|
@@ -326,9 +121,6 @@ function runCommandSync(command, args, options = {}) {
|
|
|
326
121
|
return result.stdout?.toString() || '';
|
|
327
122
|
}
|
|
328
123
|
|
|
329
|
-
// Schema utilities for normalizing LLM output
|
|
330
|
-
const { normalizeEnumValues } = require('./schema-utils');
|
|
331
|
-
|
|
332
124
|
/**
|
|
333
125
|
* Build Claude-specific environment variables for task spawning
|
|
334
126
|
* Consolidates auth resolution and model mapping logic used by both isolated and non-isolated modes
|
|
@@ -740,6 +532,7 @@ async function spawnClaudeTask(agent, context, options = {}) {
|
|
|
740
532
|
providerName,
|
|
741
533
|
modelSpec,
|
|
742
534
|
runOutputFormat,
|
|
535
|
+
structuredOutputRecovery: options.structuredOutputRecovery === true,
|
|
743
536
|
});
|
|
744
537
|
|
|
745
538
|
maybeLogStreamJsonNotice(agent, runOutputFormat);
|
|
@@ -766,7 +559,6 @@ async function spawnClaudeTask(agent, context, options = {}) {
|
|
|
766
559
|
if (agent.isolation?.enabled) {
|
|
767
560
|
return spawnClaudeTaskIsolated(agent, context, options);
|
|
768
561
|
}
|
|
769
|
-
options = ensureFormatterLaunchOptions(providerName, options);
|
|
770
562
|
|
|
771
563
|
const claudeSettingsPath =
|
|
772
564
|
providerName === 'claude'
|
|
@@ -781,11 +573,7 @@ async function spawnClaudeTask(agent, context, options = {}) {
|
|
|
781
573
|
let pendingLaunch;
|
|
782
574
|
try {
|
|
783
575
|
try {
|
|
784
|
-
const spawnEnv = buildSpawnEnv(agent, providerName, modelSpec, {
|
|
785
|
-
claudeSettingsPath,
|
|
786
|
-
disableTools: options.disableTools === true,
|
|
787
|
-
formatterAgentName: options.formatterAgentName,
|
|
788
|
-
});
|
|
576
|
+
const spawnEnv = buildSpawnEnv(agent, providerName, modelSpec, { claudeSettingsPath });
|
|
789
577
|
taskId = await spawnTaskProcess({
|
|
790
578
|
agent,
|
|
791
579
|
ctPath,
|
|
@@ -866,11 +654,7 @@ async function spawnClaudeTask(agent, context, options = {}) {
|
|
|
866
654
|
if (error.terminationExhausted === true && error.retainTaskHandle === true) {
|
|
867
655
|
throw error;
|
|
868
656
|
}
|
|
869
|
-
if (
|
|
870
|
-
nested &&
|
|
871
|
-
handle.isCancelled &&
|
|
872
|
-
handle.cancelDetails.code !== 'NESTED_SETUP_FAILED'
|
|
873
|
-
) {
|
|
657
|
+
if (nested && handle.isCancelled && handle.cancelDetails.code !== 'NESTED_SETUP_FAILED') {
|
|
874
658
|
const termination = await handle.waitForCancellation();
|
|
875
659
|
const cancellationError = createNestedCancellationError(handle);
|
|
876
660
|
if (!isTerminationConfirmed(termination)) {
|
|
@@ -907,28 +691,32 @@ function resolveOutputFormatConfig(agent) {
|
|
|
907
691
|
return { desiredOutputFormat, strictSchema, runOutputFormat };
|
|
908
692
|
}
|
|
909
693
|
|
|
910
|
-
function buildTaskRunArgs({
|
|
694
|
+
function buildTaskRunArgs({
|
|
695
|
+
agent,
|
|
696
|
+
providerName,
|
|
697
|
+
modelSpec,
|
|
698
|
+
runOutputFormat,
|
|
699
|
+
structuredOutputRecovery = false,
|
|
700
|
+
}) {
|
|
911
701
|
const args = ['task', 'run', '--output-format', runOutputFormat, '--provider', providerName];
|
|
912
702
|
const modelSpecSource = agent._resolveModelSpecSource
|
|
913
703
|
? agent._resolveModelSpecSource()
|
|
914
704
|
: 'direct';
|
|
915
705
|
appendTaskRunModelArgs(args, modelSpec, modelSpecSource);
|
|
916
706
|
|
|
917
|
-
// Add verification mode flag if configured
|
|
918
707
|
if (agent.config.verificationMode) {
|
|
919
708
|
args.push('-v');
|
|
920
709
|
}
|
|
921
710
|
|
|
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
711
|
if (agent.config.jsonSchema && runOutputFormat === 'json') {
|
|
925
|
-
|
|
926
|
-
|
|
712
|
+
args.push('--json-schema', JSON.stringify(agent.config.jsonSchema));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (structuredOutputRecovery) {
|
|
716
|
+
args.push('--structured-output-recovery');
|
|
717
|
+
return args;
|
|
927
718
|
}
|
|
928
719
|
|
|
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
720
|
for (const mcpArg of resolveMcpConfigArgs(agent, providerName)) {
|
|
933
721
|
args.push(mcpArg);
|
|
934
722
|
}
|
|
@@ -1056,7 +844,7 @@ function buildSpawnEnv(agent, providerName, modelSpec, options = {}) {
|
|
|
1056
844
|
worktreePath: agent.worktree?.path || null,
|
|
1057
845
|
});
|
|
1058
846
|
|
|
1059
|
-
return
|
|
847
|
+
return spawnEnv;
|
|
1060
848
|
}
|
|
1061
849
|
|
|
1062
850
|
function parseTaskIdFromOutput(stdout) {
|
|
@@ -1073,6 +861,27 @@ function assignDurableTaskId(agent, taskId) {
|
|
|
1073
861
|
});
|
|
1074
862
|
}
|
|
1075
863
|
|
|
864
|
+
async function terminatePendingTaskWrapper(proc, waitForWrapperClose, isWrapperClosed) {
|
|
865
|
+
if (isWrapperClosed()) return;
|
|
866
|
+
if (!proc.pid) {
|
|
867
|
+
await new Promise((resolve) => {
|
|
868
|
+
let settled = false;
|
|
869
|
+
const finish = () => {
|
|
870
|
+
if (settled) return;
|
|
871
|
+
settled = true;
|
|
872
|
+
proc.off('spawn', finish);
|
|
873
|
+
proc.off('error', finish);
|
|
874
|
+
resolve();
|
|
875
|
+
};
|
|
876
|
+
proc.once('spawn', finish);
|
|
877
|
+
proc.once('error', finish);
|
|
878
|
+
if (proc.pid || isWrapperClosed()) finish();
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
if (!isWrapperClosed()) proc.kill('SIGKILL');
|
|
882
|
+
await waitForWrapperClose;
|
|
883
|
+
}
|
|
884
|
+
|
|
1076
885
|
function createPendingTaskLaunchHandle({
|
|
1077
886
|
agent,
|
|
1078
887
|
proc,
|
|
@@ -1112,10 +921,7 @@ function createPendingTaskLaunchHandle({
|
|
|
1112
921
|
};
|
|
1113
922
|
if (taskId) await cancelTask(taskId);
|
|
1114
923
|
|
|
1115
|
-
|
|
1116
|
-
proc.kill('SIGKILL');
|
|
1117
|
-
await waitForWrapperClose;
|
|
1118
|
-
}
|
|
924
|
+
await terminatePendingTaskWrapper(proc, waitForWrapperClose, isWrapperClosed);
|
|
1119
925
|
|
|
1120
926
|
taskId = taskId || (await findLateTaskId());
|
|
1121
927
|
if (taskId && !commandAttempted) await cancelTask(taskId);
|
|
@@ -1165,6 +971,7 @@ function spawnTaskProcess({
|
|
|
1165
971
|
spawnTimeoutMs = 30000,
|
|
1166
972
|
handle = null,
|
|
1167
973
|
nested = false,
|
|
974
|
+
spawnProcess = spawn,
|
|
1168
975
|
}) {
|
|
1169
976
|
// Timeout for spawn phase - if CLI hangs during init (e.g., opencode 429 bug), kill it.
|
|
1170
977
|
const SPAWN_TIMEOUT_MS = spawnTimeoutMs;
|
|
@@ -1174,7 +981,7 @@ function spawnTaskProcess({
|
|
|
1174
981
|
const findPersistedTaskId = () => getTaskBySpawnOwnershipToken(ownershipToken)?.id || null;
|
|
1175
982
|
|
|
1176
983
|
return new Promise((resolve, reject) => {
|
|
1177
|
-
const proc =
|
|
984
|
+
const proc = spawnProcess(ctPath, safeArgs, {
|
|
1178
985
|
cwd,
|
|
1179
986
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1180
987
|
env: { ...spawnEnv, [TASK_SPAWN_OWNERSHIP_TOKEN_ENV]: ownershipToken },
|
|
@@ -1543,7 +1350,7 @@ function requiresStructuredResult(agent) {
|
|
|
1543
1350
|
return outputFormat !== 'text' || !!agent?.config?.jsonSchema;
|
|
1544
1351
|
}
|
|
1545
1352
|
|
|
1546
|
-
async function evaluateStructuredSuccess({ agent, taskId, state, success }) {
|
|
1353
|
+
async function evaluateStructuredSuccess({ agent, taskId, state, success, allowRecovery }) {
|
|
1547
1354
|
if (!success || !requiresStructuredResult(agent)) {
|
|
1548
1355
|
return { success, error: null };
|
|
1549
1356
|
}
|
|
@@ -1556,10 +1363,16 @@ async function evaluateStructuredSuccess({ agent, taskId, state, success }) {
|
|
|
1556
1363
|
// Cache the validated parsed object so completion hooks and {{result.*}}
|
|
1557
1364
|
// substitution consume it directly instead of re-parsing (which could
|
|
1558
1365
|
// trigger a second recovery model call).
|
|
1559
|
-
state._cachedParsedResult = await agent._parseResultOutput(state.output);
|
|
1366
|
+
state._cachedParsedResult = await agent._parseResultOutput(state.output, { allowRecovery });
|
|
1560
1367
|
return { success: true, error: null };
|
|
1561
1368
|
} catch (error) {
|
|
1562
|
-
if (
|
|
1369
|
+
if (
|
|
1370
|
+
isNestedLifecycleError(error) ||
|
|
1371
|
+
error?.permanent === true ||
|
|
1372
|
+
error?.recoveryAbort === true
|
|
1373
|
+
) {
|
|
1374
|
+
throw error;
|
|
1375
|
+
}
|
|
1563
1376
|
const errorContext = sanitizeErrorMessage(error.message);
|
|
1564
1377
|
console.warn(
|
|
1565
1378
|
`[Agent ${agent.id}] Task ${taskId} reported completed but produced invalid structured output; ` +
|
|
@@ -1595,9 +1408,16 @@ async function buildCompletionResult({
|
|
|
1595
1408
|
success,
|
|
1596
1409
|
taskInfo = getTask(taskId),
|
|
1597
1410
|
}) {
|
|
1411
|
+
const { isCompleted } = parseStatusFlags(stripAnsiCodes(stdout));
|
|
1598
1412
|
const classified = state.skipStructuredResultCheck
|
|
1599
1413
|
? { success, error: null }
|
|
1600
|
-
: await evaluateStructuredSuccess({
|
|
1414
|
+
: await evaluateStructuredSuccess({
|
|
1415
|
+
agent,
|
|
1416
|
+
taskId,
|
|
1417
|
+
state,
|
|
1418
|
+
success,
|
|
1419
|
+
allowRecovery: isCompleted,
|
|
1420
|
+
});
|
|
1601
1421
|
const resumeIdentityError = classified.success ? validateCompletedResumeIdentity(taskInfo) : null;
|
|
1602
1422
|
if (resumeIdentityError) {
|
|
1603
1423
|
classified.success = false;
|
|
@@ -1802,18 +1622,7 @@ function handleStatusCompletion({
|
|
|
1802
1622
|
success,
|
|
1803
1623
|
})
|
|
1804
1624
|
.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
|
-
});
|
|
1625
|
+
.catch(reject);
|
|
1817
1626
|
}, 500);
|
|
1818
1627
|
|
|
1819
1628
|
return true;
|
|
@@ -2034,7 +1843,6 @@ async function spawnClaudeTaskIsolated(agent, context, options = {}) {
|
|
|
2034
1843
|
async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
|
|
2035
1844
|
const { manager, clusterId } = agent.isolation;
|
|
2036
1845
|
const providerName = agent._resolveProvider ? agent._resolveProvider() : 'claude';
|
|
2037
|
-
options = ensureFormatterLaunchOptions(providerName, options);
|
|
2038
1846
|
const modelSpec = resolveAgentModelSpec(agent);
|
|
2039
1847
|
const modelSpecSource = agent._resolveModelSpecSource
|
|
2040
1848
|
? agent._resolveModelSpecSource()
|
|
@@ -2050,6 +1858,7 @@ async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
|
|
|
2050
1858
|
providerName,
|
|
2051
1859
|
modelSpec,
|
|
2052
1860
|
runOutputFormat,
|
|
1861
|
+
structuredOutputRecovery: options.structuredOutputRecovery === true,
|
|
2053
1862
|
}),
|
|
2054
1863
|
];
|
|
2055
1864
|
maybeLogStreamJsonNotice(agent, runOutputFormat);
|
|
@@ -2074,26 +1883,10 @@ async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
|
|
|
2074
1883
|
const ownershipToken = createTaskSpawnOwnershipToken();
|
|
2075
1884
|
// Auth env vars are injected by IsolationManager; the launch token is the
|
|
2076
1885
|
// 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
|
-
);
|
|
1886
|
+
const isolatedEnv = {
|
|
1887
|
+
...(providerName === 'claude' ? buildClaudeEnv(modelSpec, { includeAuth: false }) : {}),
|
|
1888
|
+
[TASK_SPAWN_OWNERSHIP_TOKEN_ENV]: ownershipToken,
|
|
1889
|
+
};
|
|
2097
1890
|
|
|
2098
1891
|
if (options.executionHandle?.isCancelled) {
|
|
2099
1892
|
throw createNestedCancellationError(options.executionHandle);
|
|
@@ -2156,8 +1949,7 @@ async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
|
|
|
2156
1949
|
rejection.terminationExhausted = true;
|
|
2157
1950
|
rejection.terminationAttempts = 1;
|
|
2158
1951
|
if (options.nested) options.executionHandle?.retainOwnership();
|
|
2159
|
-
rejection.taskId =
|
|
2160
|
-
isolatedTaskId || (!options.nested ? agent.currentTaskId : null) || null;
|
|
1952
|
+
rejection.taskId = isolatedTaskId || (!options.nested ? agent.currentTaskId : null) || null;
|
|
2161
1953
|
} else if (!options.nested && agent.currentTask === isolatedPendingLaunch) {
|
|
2162
1954
|
agent.currentTask = null;
|
|
2163
1955
|
}
|
|
@@ -2184,14 +1976,10 @@ async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
|
|
|
2184
1976
|
commandError = error;
|
|
2185
1977
|
}
|
|
2186
1978
|
|
|
2187
|
-
|
|
2188
|
-
proc.kill('SIGKILL');
|
|
2189
|
-
await waitForWrapperClose;
|
|
2190
|
-
}
|
|
1979
|
+
await terminatePendingTaskWrapper(proc, waitForWrapperClose, () => wrapperClosed);
|
|
2191
1980
|
|
|
2192
1981
|
try {
|
|
2193
|
-
const persistedTaskId =
|
|
2194
|
-
isolatedTaskId || (await findLatePersistedTaskId());
|
|
1982
|
+
const persistedTaskId = isolatedTaskId || (await findLatePersistedTaskId());
|
|
2195
1983
|
if (persistedTaskId && !termination) {
|
|
2196
1984
|
termination = await terminateIsolatedTask(manager, clusterId, persistedTaskId);
|
|
2197
1985
|
commandError = null;
|
|
@@ -2214,8 +2002,8 @@ async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
|
|
|
2214
2002
|
if (!resolved) {
|
|
2215
2003
|
const error =
|
|
2216
2004
|
(sourceError instanceof Error &&
|
|
2217
|
-
|
|
2218
|
-
|
|
2005
|
+
sourceError.code === 'unsupported-capability' &&
|
|
2006
|
+
sourceError.permanent === true
|
|
2219
2007
|
? sourceError
|
|
2220
2008
|
: null) ||
|
|
2221
2009
|
timeoutError ||
|
|
@@ -2456,8 +2244,7 @@ async function terminateIsolatedTask(manager, clusterId, taskId) {
|
|
|
2456
2244
|
);
|
|
2457
2245
|
}
|
|
2458
2246
|
return {
|
|
2459
|
-
alreadyTerminal:
|
|
2460
|
-
Boolean(beforeStatus) || Boolean(afterStatus && afterStatus !== 'killed'),
|
|
2247
|
+
alreadyTerminal: Boolean(beforeStatus) || Boolean(afterStatus && afterStatus !== 'killed'),
|
|
2461
2248
|
forced: !beforeStatus && afterStatus === 'killed',
|
|
2462
2249
|
status: beforeStatus || afterStatus,
|
|
2463
2250
|
};
|
|
@@ -2526,29 +2313,49 @@ function settleIsolatedTerminalStatus({
|
|
|
2526
2313
|
useVertex: process.env.CLAUDE_CODE_USE_VERTEX === '1',
|
|
2527
2314
|
})
|
|
2528
2315
|
: null;
|
|
2529
|
-
const
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2316
|
+
const staleCandidate =
|
|
2317
|
+
status === 'stale' &&
|
|
2318
|
+
!vertexModelError &&
|
|
2319
|
+
determineStaleSuccess({ agent, output: state.fullOutput, providerName, taskId });
|
|
2320
|
+
let success = (status === 'completed' || staleCandidate) && !vertexModelError;
|
|
2321
|
+
let structuredError = null;
|
|
2322
|
+
if (staleCandidate && !state.skipStructuredResultCheck) {
|
|
2323
|
+
const evaluated = await evaluateStructuredSuccess({
|
|
2324
|
+
agent,
|
|
2325
|
+
taskId,
|
|
2326
|
+
state,
|
|
2327
|
+
success,
|
|
2328
|
+
allowRecovery: false,
|
|
2329
|
+
});
|
|
2330
|
+
success = evaluated.success;
|
|
2331
|
+
structuredError = evaluated.error;
|
|
2332
|
+
}
|
|
2333
|
+
const errorContext =
|
|
2334
|
+
structuredError ||
|
|
2335
|
+
(!success
|
|
2336
|
+
? extractErrorContext({
|
|
2337
|
+
output: state.fullOutput,
|
|
2338
|
+
statusOutput: status ? `Status: ${status}` : '',
|
|
2339
|
+
taskId,
|
|
2340
|
+
isNotFound,
|
|
2341
|
+
debug: {
|
|
2342
|
+
agentId: agent.id,
|
|
2343
|
+
providerName,
|
|
2344
|
+
pid: agent.processPid,
|
|
2345
|
+
cwd: agent.config.cwd || process.cwd(),
|
|
2346
|
+
worktreePath: agent.worktree?.path || null,
|
|
2347
|
+
isolation: true,
|
|
2348
|
+
clusterId,
|
|
2349
|
+
logFilePath,
|
|
2350
|
+
},
|
|
2351
|
+
})
|
|
2352
|
+
: null);
|
|
2353
|
+
let parsedResult = null;
|
|
2354
|
+
if (success && !state.skipStructuredResultCheck && !vertexModelError) {
|
|
2355
|
+
parsedResult = staleCandidate
|
|
2356
|
+
? state._cachedParsedResult
|
|
2551
2357
|
: await agent._parseResultOutput(state.fullOutput);
|
|
2358
|
+
}
|
|
2552
2359
|
|
|
2553
2360
|
settleIsolatedFollower({
|
|
2554
2361
|
agent,
|
|
@@ -2844,8 +2651,7 @@ function followClaudeTaskLogsIsolated(agent, taskId, options = {}) {
|
|
|
2844
2651
|
options.nested === true
|
|
2845
2652
|
);
|
|
2846
2653
|
const cleanup = buildIsolatedCleanup(state);
|
|
2847
|
-
const onLine = (line) =>
|
|
2848
|
-
broadcastIsolatedLine({ agent, providerName, taskId, state, line });
|
|
2654
|
+
const onLine = (line) => broadcastIsolatedLine({ agent, providerName, taskId, state, line });
|
|
2849
2655
|
state.lifecycleHandle = buildIsolatedLifecycleHandle({
|
|
2850
2656
|
agent,
|
|
2851
2657
|
manager,
|
|
@@ -2962,65 +2768,72 @@ function followClaudeTaskLogsIsolated(agent, taskId, options = {}) {
|
|
|
2962
2768
|
* @param {String} output - Raw output from agent
|
|
2963
2769
|
* @returns {Promise<Object>} Parsed result data
|
|
2964
2770
|
*/
|
|
2965
|
-
async function parseResultOutput(agent, output) {
|
|
2966
|
-
// Empty outputs = FAIL
|
|
2771
|
+
async function parseResultOutput(agent, output, { allowRecovery = true } = {}) {
|
|
2967
2772
|
if (!output || !output.trim()) {
|
|
2968
2773
|
throw new Error('Task execution failed - no output');
|
|
2969
2774
|
}
|
|
2970
2775
|
|
|
2971
2776
|
const providerName = agent._resolveProvider ? agent._resolveProvider() : 'claude';
|
|
2972
2777
|
const {
|
|
2973
|
-
extractJsonFromOutput,
|
|
2974
2778
|
extractCliError,
|
|
2779
|
+
extractJsonFromOutput,
|
|
2780
|
+
extractModelTextFromOutput,
|
|
2975
2781
|
hasFatalStandaloneOutput,
|
|
2976
2782
|
} = require('./output-extraction');
|
|
2977
|
-
|
|
2978
|
-
// Check for CLI errors FIRST - surface the actual error message
|
|
2979
|
-
const cliError = extractCliError(output);
|
|
2783
|
+
const cliError = extractCliError(output, providerName);
|
|
2980
2784
|
if (cliError) {
|
|
2981
2785
|
throw new Error(`CLI error (${cliError.provider}): ${cliError.error}`);
|
|
2982
2786
|
}
|
|
2983
2787
|
|
|
2984
|
-
|
|
2985
|
-
|
|
2788
|
+
const parsed = extractJsonFromOutput(output, providerName);
|
|
2789
|
+
const schema = agent.config.jsonSchema;
|
|
2790
|
+
if (!schema) {
|
|
2791
|
+
if (parsed) return parsed;
|
|
2792
|
+
throw new Error(`Agent ${agent.id} output missing required JSON block`);
|
|
2793
|
+
}
|
|
2986
2794
|
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2795
|
+
const { createStructuredOutputValidator, reformatOutput } = require('./output-reformatter');
|
|
2796
|
+
const validateCandidate = createStructuredOutputValidator(schema);
|
|
2797
|
+
const directValidation = parsed ? validateCandidate(parsed) : null;
|
|
2798
|
+
if (directValidation?.valid) {
|
|
2799
|
+
return directValidation.value;
|
|
2800
|
+
}
|
|
2990
2801
|
|
|
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
|
-
|
|
2802
|
+
const schemaIsNonEmpty =
|
|
2803
|
+
typeof schema === 'string'
|
|
2804
|
+
? schema.trim() !== '' && schema.trim() !== '{}'
|
|
2805
|
+
: typeof schema === 'object' && schema !== null && Object.keys(schema).length > 0;
|
|
2806
|
+
const { providerSupportsOutputReformatting } = require('../../lib/provider-names');
|
|
2807
|
+
let recovery = null;
|
|
2808
|
+
if (
|
|
2809
|
+
schemaIsNonEmpty &&
|
|
2810
|
+
!hasFatalStandaloneOutput(output) &&
|
|
2811
|
+
allowRecovery &&
|
|
2812
|
+
providerSupportsOutputReformatting(providerName)
|
|
2813
|
+
) {
|
|
2814
|
+
const modelText = extractModelTextFromOutput(output, providerName);
|
|
2815
|
+
recovery = await reformatOutput({
|
|
2816
|
+
rawOutput: modelText || output,
|
|
2817
|
+
schema,
|
|
2818
|
+
providerName,
|
|
2819
|
+
initialError: directValidation?.error || null,
|
|
2820
|
+
validateCandidate,
|
|
2821
|
+
isCancelled: () => agent.running === false || agent.state === 'stopped',
|
|
2822
|
+
runReformat: (prompt) =>
|
|
2823
|
+
agent._spawnClaudeTask(prompt, {
|
|
2824
|
+
skipStructuredResultCheck: true,
|
|
2825
|
+
nested: true,
|
|
2826
|
+
structuredOutputRecovery: true,
|
|
2827
|
+
}),
|
|
2828
|
+
onAttempt: (attempt, lastError) => {
|
|
2829
|
+
const reason = lastError ? `: ${lastError}` : '';
|
|
2830
|
+
console.warn(
|
|
2831
|
+
`[Agent ${agent.id}] Correcting structured output (attempt ${attempt})${reason}`
|
|
2832
|
+
);
|
|
2833
|
+
},
|
|
2834
|
+
});
|
|
2835
|
+
if (recovery.status === 'recovered') {
|
|
2836
|
+
return recovery.value;
|
|
3024
2837
|
}
|
|
3025
2838
|
}
|
|
3026
2839
|
|
|
@@ -3028,69 +2841,40 @@ async function parseResultOutput(agent, output) {
|
|
|
3028
2841
|
if (hasFatalStandaloneOutput(output)) {
|
|
3029
2842
|
throw new Error('Task execution failed - no output');
|
|
3030
2843
|
}
|
|
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`);
|
|
2844
|
+
const recoveryDetail = recovery?.lastError ? ` Recovery exhausted: ${recovery.lastError}.` : '';
|
|
2845
|
+
throw new Error(`Agent ${agent.id} output missing required JSON block.${recoveryDetail}`);
|
|
3039
2846
|
}
|
|
3040
2847
|
|
|
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
|
-
}
|
|
2848
|
+
if (!allowRecovery) {
|
|
2849
|
+
const detail = directValidation?.error || 'required structured output is missing';
|
|
2850
|
+
throw new Error(`Agent ${agent.id} output is not valid for non-completed task: ${detail}`);
|
|
2851
|
+
}
|
|
3072
2852
|
|
|
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
|
-
}
|
|
2853
|
+
const errorDetail = directValidation?.error || 'unknown schema error';
|
|
2854
|
+
const message = `Agent ${agent.id} output failed JSON schema validation: ${errorDetail}`;
|
|
2855
|
+
if (agent.role === 'validator') {
|
|
2856
|
+
throw new Error(
|
|
2857
|
+
recovery?.status === 'exhausted'
|
|
2858
|
+
? `${message}. Recovery exhausted after ${recovery.attempts} attempts: ${recovery.lastError}`
|
|
2859
|
+
: message
|
|
2860
|
+
);
|
|
3089
2861
|
}
|
|
3090
2862
|
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
2863
|
+
console.warn(`⚠️ ${message}`);
|
|
2864
|
+
agent._publish({
|
|
2865
|
+
topic: 'AGENT_SCHEMA_WARNING',
|
|
2866
|
+
receiver: 'broadcast',
|
|
2867
|
+
content: {
|
|
2868
|
+
text: message,
|
|
2869
|
+
data: {
|
|
2870
|
+
agent: agent.id,
|
|
2871
|
+
role: agent.role,
|
|
2872
|
+
iteration: agent.iteration,
|
|
2873
|
+
errors: directValidation?.errors || [],
|
|
2874
|
+
},
|
|
2875
|
+
},
|
|
2876
|
+
});
|
|
2877
|
+
return directValidation?.value || parsed;
|
|
3094
2878
|
}
|
|
3095
2879
|
|
|
3096
2880
|
/**
|