@the-open-engine/zeroshot 5.4.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/CHANGELOG.md +539 -0
- package/LICENSE +21 -0
- package/README.md +508 -0
- package/cli/commands/inspect-render.js +135 -0
- package/cli/commands/inspect.js +294 -0
- package/cli/commands/providers.js +149 -0
- package/cli/index.js +5431 -0
- package/cli/lib/first-run.js +211 -0
- package/cli/lib/update-checker.js +281 -0
- package/cli/message-formatter-utils.js +75 -0
- package/cli/message-formatters-normal.js +275 -0
- package/cli/message-formatters-watch.js +185 -0
- package/cluster-templates/base-templates/debug-workflow.json +422 -0
- package/cluster-templates/base-templates/full-workflow.json +727 -0
- package/cluster-templates/base-templates/heavy-validation.json +272 -0
- package/cluster-templates/base-templates/quick-validation.json +285 -0
- package/cluster-templates/base-templates/single-worker.json +71 -0
- package/cluster-templates/base-templates/worker-validator.json +230 -0
- package/cluster-templates/conductor-bootstrap.json +130 -0
- package/docker/zeroshot-cluster/Dockerfile +153 -0
- package/docker/zeroshot-cluster/pre-baked-deps.json +28 -0
- package/lib/agent-cli-provider/adapters/claude-parser.d.ts +3 -0
- package/lib/agent-cli-provider/adapters/claude-parser.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/claude-parser.js +122 -0
- package/lib/agent-cli-provider/adapters/claude-parser.js.map +1 -0
- package/lib/agent-cli-provider/adapters/claude-recovery.d.ts +15 -0
- package/lib/agent-cli-provider/adapters/claude-recovery.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/claude-recovery.js +165 -0
- package/lib/agent-cli-provider/adapters/claude-recovery.js.map +1 -0
- package/lib/agent-cli-provider/adapters/claude.d.ts +3 -0
- package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/claude.js +181 -0
- package/lib/agent-cli-provider/adapters/claude.js.map +1 -0
- package/lib/agent-cli-provider/adapters/codex-parser.d.ts +3 -0
- package/lib/agent-cli-provider/adapters/codex-parser.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/codex-parser.js +192 -0
- package/lib/agent-cli-provider/adapters/codex-parser.js.map +1 -0
- package/lib/agent-cli-provider/adapters/codex.d.ts +3 -0
- package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/codex.js +151 -0
- package/lib/agent-cli-provider/adapters/codex.js.map +1 -0
- package/lib/agent-cli-provider/adapters/common.d.ts +28 -0
- package/lib/agent-cli-provider/adapters/common.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/common.js +71 -0
- package/lib/agent-cli-provider/adapters/common.js.map +1 -0
- package/lib/agent-cli-provider/adapters/gemini.d.ts +3 -0
- package/lib/agent-cli-provider/adapters/gemini.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/gemini.js +176 -0
- package/lib/agent-cli-provider/adapters/gemini.js.map +1 -0
- package/lib/agent-cli-provider/adapters/index.d.ts +9 -0
- package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/index.js +95 -0
- package/lib/agent-cli-provider/adapters/index.js.map +1 -0
- package/lib/agent-cli-provider/adapters/opencode.d.ts +3 -0
- package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/opencode.js +228 -0
- package/lib/agent-cli-provider/adapters/opencode.js.map +1 -0
- package/lib/agent-cli-provider/contract-actions.d.ts +5 -0
- package/lib/agent-cli-provider/contract-actions.d.ts.map +1 -0
- package/lib/agent-cli-provider/contract-actions.js +140 -0
- package/lib/agent-cli-provider/contract-actions.js.map +1 -0
- package/lib/agent-cli-provider/contract-env.d.ts +11 -0
- package/lib/agent-cli-provider/contract-env.d.ts.map +1 -0
- package/lib/agent-cli-provider/contract-env.js +113 -0
- package/lib/agent-cli-provider/contract-env.js.map +1 -0
- package/lib/agent-cli-provider/contract-envelope.d.ts +49 -0
- package/lib/agent-cli-provider/contract-envelope.d.ts.map +1 -0
- package/lib/agent-cli-provider/contract-envelope.js +52 -0
- package/lib/agent-cli-provider/contract-envelope.js.map +1 -0
- package/lib/agent-cli-provider/contract-errors.d.ts +22 -0
- package/lib/agent-cli-provider/contract-errors.d.ts.map +1 -0
- package/lib/agent-cli-provider/contract-errors.js +71 -0
- package/lib/agent-cli-provider/contract-errors.js.map +1 -0
- package/lib/agent-cli-provider/contract-fallback.d.ts +7 -0
- package/lib/agent-cli-provider/contract-fallback.d.ts.map +1 -0
- package/lib/agent-cli-provider/contract-fallback.js +102 -0
- package/lib/agent-cli-provider/contract-fallback.js.map +1 -0
- package/lib/agent-cli-provider/contract-invoke.d.ts +5 -0
- package/lib/agent-cli-provider/contract-invoke.d.ts.map +1 -0
- package/lib/agent-cli-provider/contract-invoke.js +89 -0
- package/lib/agent-cli-provider/contract-invoke.js.map +1 -0
- package/lib/agent-cli-provider/contract-options.d.ts +3 -0
- package/lib/agent-cli-provider/contract-options.d.ts.map +1 -0
- package/lib/agent-cli-provider/contract-options.js +140 -0
- package/lib/agent-cli-provider/contract-options.js.map +1 -0
- package/lib/agent-cli-provider/contract-parse.d.ts +15 -0
- package/lib/agent-cli-provider/contract-parse.d.ts.map +1 -0
- package/lib/agent-cli-provider/contract-parse.js +81 -0
- package/lib/agent-cli-provider/contract-parse.js.map +1 -0
- package/lib/agent-cli-provider/contract-support.d.ts +18 -0
- package/lib/agent-cli-provider/contract-support.d.ts.map +1 -0
- package/lib/agent-cli-provider/contract-support.js +154 -0
- package/lib/agent-cli-provider/contract-support.js.map +1 -0
- package/lib/agent-cli-provider/contract.d.ts +13 -0
- package/lib/agent-cli-provider/contract.d.ts.map +1 -0
- package/lib/agent-cli-provider/contract.js +30 -0
- package/lib/agent-cli-provider/contract.js.map +1 -0
- package/lib/agent-cli-provider/env-safety.d.ts +11 -0
- package/lib/agent-cli-provider/env-safety.d.ts.map +1 -0
- package/lib/agent-cli-provider/env-safety.js +83 -0
- package/lib/agent-cli-provider/env-safety.js.map +1 -0
- package/lib/agent-cli-provider/errors.d.ts +5 -0
- package/lib/agent-cli-provider/errors.d.ts.map +1 -0
- package/lib/agent-cli-provider/errors.js +115 -0
- package/lib/agent-cli-provider/errors.js.map +1 -0
- package/lib/agent-cli-provider/executable.d.ts +3 -0
- package/lib/agent-cli-provider/executable.d.ts.map +1 -0
- package/lib/agent-cli-provider/executable.js +24 -0
- package/lib/agent-cli-provider/executable.js.map +1 -0
- package/lib/agent-cli-provider/index.d.ts +8 -0
- package/lib/agent-cli-provider/index.d.ts.map +1 -0
- package/lib/agent-cli-provider/index.js +31 -0
- package/lib/agent-cli-provider/index.js.map +1 -0
- package/lib/agent-cli-provider/invoke-evidence.d.ts +4 -0
- package/lib/agent-cli-provider/invoke-evidence.d.ts.map +1 -0
- package/lib/agent-cli-provider/invoke-evidence.js +17 -0
- package/lib/agent-cli-provider/invoke-evidence.js.map +1 -0
- package/lib/agent-cli-provider/json.d.ts +16 -0
- package/lib/agent-cli-provider/json.d.ts.map +1 -0
- package/lib/agent-cli-provider/json.js +110 -0
- package/lib/agent-cli-provider/json.js.map +1 -0
- package/lib/agent-cli-provider/log-prefix.d.ts +2 -0
- package/lib/agent-cli-provider/log-prefix.d.ts.map +1 -0
- package/lib/agent-cli-provider/log-prefix.js +22 -0
- package/lib/agent-cli-provider/log-prefix.js.map +1 -0
- package/lib/agent-cli-provider/process-runner.d.ts +17 -0
- package/lib/agent-cli-provider/process-runner.d.ts.map +1 -0
- package/lib/agent-cli-provider/process-runner.js +89 -0
- package/lib/agent-cli-provider/process-runner.js.map +1 -0
- package/lib/agent-cli-provider/redaction.d.ts +9 -0
- package/lib/agent-cli-provider/redaction.d.ts.map +1 -0
- package/lib/agent-cli-provider/redaction.js +227 -0
- package/lib/agent-cli-provider/redaction.js.map +1 -0
- package/lib/agent-cli-provider/schema.d.ts +5 -0
- package/lib/agent-cli-provider/schema.d.ts.map +1 -0
- package/lib/agent-cli-provider/schema.js +136 -0
- package/lib/agent-cli-provider/schema.js.map +1 -0
- package/lib/agent-cli-provider/single-agent-runtime.d.ts +15 -0
- package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -0
- package/lib/agent-cli-provider/single-agent-runtime.js +228 -0
- package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -0
- package/lib/agent-cli-provider/types.d.ts +194 -0
- package/lib/agent-cli-provider/types.d.ts.map +1 -0
- package/lib/agent-cli-provider/types.js +12 -0
- package/lib/agent-cli-provider/types.js.map +1 -0
- package/lib/completion.js +174 -0
- package/lib/detached-startup.js +220 -0
- package/lib/docker-config.js +220 -0
- package/lib/git-remote-utils.js +165 -0
- package/lib/id-detector.js +55 -0
- package/lib/provider-defaults.js +62 -0
- package/lib/provider-detection.js +59 -0
- package/lib/provider-names.js +57 -0
- package/lib/repo-settings.js +69 -0
- package/lib/settings/claude-auth.js +78 -0
- package/lib/settings.js +542 -0
- package/lib/start-cluster.js +321 -0
- package/lib/stream-json-parser.js +67 -0
- package/package.json +162 -0
- package/scripts/fix-node-pty-permissions.js +75 -0
- package/scripts/record-demo.sh +279 -0
- package/scripts/setup-merge-queue.sh +170 -0
- package/scripts/test-install.sh +40 -0
- package/scripts/validate-templates.js +107 -0
- package/src/agent/agent-config.js +266 -0
- package/src/agent/agent-context-builder.js +189 -0
- package/src/agent/agent-context-sections.js +338 -0
- package/src/agent/agent-context-sources.js +147 -0
- package/src/agent/agent-hook-executor.js +721 -0
- package/src/agent/agent-input-injector.js +141 -0
- package/src/agent/agent-lifecycle.js +982 -0
- package/src/agent/agent-quality-gate-schema.js +93 -0
- package/src/agent/agent-quality-gates-context.js +51 -0
- package/src/agent/agent-stuck-detector.js +256 -0
- package/src/agent/agent-task-executor.js +2028 -0
- package/src/agent/agent-trigger-evaluator.js +67 -0
- package/src/agent/context-metrics.js +160 -0
- package/src/agent/context-pack-builder.js +367 -0
- package/src/agent/context-replay-policy.js +51 -0
- package/src/agent/guidance-queue.js +77 -0
- package/src/agent/output-extraction.js +367 -0
- package/src/agent/output-reformatter.js +175 -0
- package/src/agent/pr-verification.js +653 -0
- package/src/agent/rate-limit-backoff.js +82 -0
- package/src/agent/schema-utils.js +146 -0
- package/src/agent/validation-platform.js +35 -0
- package/src/agent-cli-provider/adapters/claude-parser.ts +133 -0
- package/src/agent-cli-provider/adapters/claude-recovery.ts +203 -0
- package/src/agent-cli-provider/adapters/claude.ts +247 -0
- package/src/agent-cli-provider/adapters/codex-parser.ts +211 -0
- package/src/agent-cli-provider/adapters/codex.ts +217 -0
- package/src/agent-cli-provider/adapters/common.ts +124 -0
- package/src/agent-cli-provider/adapters/gemini.ts +243 -0
- package/src/agent-cli-provider/adapters/index.ts +126 -0
- package/src/agent-cli-provider/adapters/opencode.ts +286 -0
- package/src/agent-cli-provider/contract-actions.ts +150 -0
- package/src/agent-cli-provider/contract-env.ts +111 -0
- package/src/agent-cli-provider/contract-envelope.ts +110 -0
- package/src/agent-cli-provider/contract-errors.ts +66 -0
- package/src/agent-cli-provider/contract-fallback.ts +121 -0
- package/src/agent-cli-provider/contract-invoke.ts +104 -0
- package/src/agent-cli-provider/contract-options.ts +173 -0
- package/src/agent-cli-provider/contract-parse.ts +94 -0
- package/src/agent-cli-provider/contract-support.ts +167 -0
- package/src/agent-cli-provider/contract.ts +56 -0
- package/src/agent-cli-provider/env-safety.ts +82 -0
- package/src/agent-cli-provider/errors.ts +122 -0
- package/src/agent-cli-provider/executable.ts +24 -0
- package/src/agent-cli-provider/index.ts +83 -0
- package/src/agent-cli-provider/invoke-evidence.ts +18 -0
- package/src/agent-cli-provider/json.ts +114 -0
- package/src/agent-cli-provider/log-prefix.ts +20 -0
- package/src/agent-cli-provider/process-runner.ts +145 -0
- package/src/agent-cli-provider/redaction.ts +282 -0
- package/src/agent-cli-provider/schema.ts +115 -0
- package/src/agent-cli-provider/single-agent-runtime.ts +311 -0
- package/src/agent-cli-provider/types.ts +237 -0
- package/src/agent-wrapper.js +615 -0
- package/src/agents/git-pusher-template.js +705 -0
- package/src/attach/attach-client.js +438 -0
- package/src/attach/attach-server.js +543 -0
- package/src/attach/index.js +37 -0
- package/src/attach/protocol.js +220 -0
- package/src/attach/ring-buffer.js +121 -0
- package/src/attach/send-input.js +88 -0
- package/src/attach/socket-discovery.js +267 -0
- package/src/claude-task-runner.js +661 -0
- package/src/config-router.js +89 -0
- package/src/config-validator.js +2202 -0
- package/src/copy-worker.js +43 -0
- package/src/guidance-topics.js +10 -0
- package/src/input-helpers.js +65 -0
- package/src/isolation-manager.js +1734 -0
- package/src/issue-providers/README.md +305 -0
- package/src/issue-providers/azure-devops-provider.js +307 -0
- package/src/issue-providers/base-provider.js +232 -0
- package/src/issue-providers/github-provider.js +210 -0
- package/src/issue-providers/gitlab-provider.js +262 -0
- package/src/issue-providers/index.js +196 -0
- package/src/issue-providers/jira-provider.js +260 -0
- package/src/ledger.js +692 -0
- package/src/lib/gc.js +232 -0
- package/src/lib/safe-exec.js +88 -0
- package/src/logic-engine.js +201 -0
- package/src/message-buffer.js +81 -0
- package/src/message-bus-bridge.js +144 -0
- package/src/message-bus.js +256 -0
- package/src/name-generator.js +232 -0
- package/src/orchestrator.js +3924 -0
- package/src/preflight.js +712 -0
- package/src/process-metrics.js +608 -0
- package/src/providers/anthropic/index.js +3 -0
- package/src/providers/base-provider.js +355 -0
- package/src/providers/capabilities.js +60 -0
- package/src/providers/google/index.js +3 -0
- package/src/providers/index.js +293 -0
- package/src/providers/openai/index.js +3 -0
- package/src/providers/opencode/index.js +3 -0
- package/src/quality-gates.js +143 -0
- package/src/schemas/sub-cluster.js +208 -0
- package/src/state-snapshot.js +398 -0
- package/src/state-snapshotter.js +142 -0
- package/src/status-footer.js +1026 -0
- package/src/sub-cluster-wrapper.js +693 -0
- package/src/task-runner.js +30 -0
- package/src/template-resolver.js +425 -0
- package/src/template-validation/index.js +338 -0
- package/src/template-validation/simulate-consensus-gates.js +324 -0
- package/src/template-validation/simulate-random-topology.js +541 -0
- package/src/template-validation/simulate-two-stage-validation.js +270 -0
- package/src/worktree-claude-config.js +135 -0
- package/src/worktree-tooling-env.js +150 -0
- package/task-lib/attachable-watcher.js +381 -0
- package/task-lib/commands/clean.js +50 -0
- package/task-lib/commands/episodes.js +105 -0
- package/task-lib/commands/get-log-path.js +23 -0
- package/task-lib/commands/kill.js +32 -0
- package/task-lib/commands/list.js +105 -0
- package/task-lib/commands/logs.js +439 -0
- package/task-lib/commands/resume.js +42 -0
- package/task-lib/commands/run.js +57 -0
- package/task-lib/commands/schedule.js +105 -0
- package/task-lib/commands/scheduler-cmd.js +96 -0
- package/task-lib/commands/schedules.js +148 -0
- package/task-lib/commands/status.js +44 -0
- package/task-lib/commands/unschedule.js +16 -0
- package/task-lib/completion.js +9 -0
- package/task-lib/config.js +11 -0
- package/task-lib/name-generator.js +230 -0
- package/task-lib/package.json +3 -0
- package/task-lib/provider-helper-runtime.js +29 -0
- package/task-lib/runner.js +190 -0
- package/task-lib/scheduler.js +252 -0
- package/task-lib/store.js +529 -0
- package/task-lib/watcher.js +305 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const GUIDANCE_BLOCK_START = '<<GUIDANCE_QUEUE_START>>';
|
|
2
|
+
const GUIDANCE_BLOCK_END = '<<GUIDANCE_QUEUE_END>>';
|
|
3
|
+
|
|
4
|
+
function formatGuidanceMessage(message) {
|
|
5
|
+
const timestamp = Number.isFinite(message.timestamp)
|
|
6
|
+
? new Date(message.timestamp).toISOString()
|
|
7
|
+
: new Date().toISOString();
|
|
8
|
+
const sender = message.sender || 'unknown';
|
|
9
|
+
const topic = message.topic || 'GUIDANCE';
|
|
10
|
+
const target = message.receiver || message.target_agent_id;
|
|
11
|
+
const targetSuffix = target ? ` -> ${target}` : '';
|
|
12
|
+
|
|
13
|
+
let formatted = `[${timestamp}] ${sender} (${topic}${targetSuffix})\n`;
|
|
14
|
+
if (message.content?.text) {
|
|
15
|
+
formatted += `${message.content.text}\n`;
|
|
16
|
+
}
|
|
17
|
+
if (message.content?.data) {
|
|
18
|
+
formatted += `${JSON.stringify(message.content.data, null, 2)}\n`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return formatted.trimEnd();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatGuidanceBlock(messages) {
|
|
25
|
+
if (!Array.isArray(messages) || messages.length === 0) return '';
|
|
26
|
+
|
|
27
|
+
const ordered = messages.slice().sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
|
28
|
+
|
|
29
|
+
let block = '## Guidance (Queued)\n\n';
|
|
30
|
+
block += `${GUIDANCE_BLOCK_START}\n`;
|
|
31
|
+
|
|
32
|
+
ordered.forEach((message, index) => {
|
|
33
|
+
block += `${formatGuidanceMessage(message)}\n`;
|
|
34
|
+
if (index < ordered.length - 1) {
|
|
35
|
+
block += '\n';
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
block += `\n${GUIDANCE_BLOCK_END}\n\n`;
|
|
40
|
+
return block;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function collectQueuedGuidance({ messageBus, clusterId, agentId, lastDeliveredAt, limit }) {
|
|
44
|
+
if (!messageBus) {
|
|
45
|
+
throw new Error('collectQueuedGuidance: messageBus is required');
|
|
46
|
+
}
|
|
47
|
+
if (!clusterId) {
|
|
48
|
+
throw new Error('collectQueuedGuidance: clusterId is required');
|
|
49
|
+
}
|
|
50
|
+
if (!agentId) {
|
|
51
|
+
throw new Error('collectQueuedGuidance: agentId is required');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const messages = messageBus.queryGuidanceMailbox({
|
|
55
|
+
cluster_id: clusterId,
|
|
56
|
+
target_agent_id: agentId,
|
|
57
|
+
lastDeliveredAt,
|
|
58
|
+
limit,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!messages.length) {
|
|
62
|
+
return { messages: [], latestTimestamp: null, guidanceBlock: '' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const ordered = messages.slice().sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
|
66
|
+
const latestTimestamp = ordered[ordered.length - 1].timestamp;
|
|
67
|
+
const guidanceBlock = formatGuidanceBlock(ordered);
|
|
68
|
+
|
|
69
|
+
return { messages: ordered, latestTimestamp, guidanceBlock };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
GUIDANCE_BLOCK_START,
|
|
74
|
+
GUIDANCE_BLOCK_END,
|
|
75
|
+
formatGuidanceBlock,
|
|
76
|
+
collectQueuedGuidance,
|
|
77
|
+
};
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Extraction Module - Multi-Provider JSON Extraction
|
|
3
|
+
*
|
|
4
|
+
* Clean extraction pipeline for structured JSON from AI provider outputs.
|
|
5
|
+
* Each provider has different output formats - this module normalizes them.
|
|
6
|
+
*
|
|
7
|
+
* Provider formats:
|
|
8
|
+
* - Claude: {"type":"result","result":{...}} or {"type":"result","structured_output":{...}}
|
|
9
|
+
* - Codex: Raw text in item.created events, turn.completed has NO result field
|
|
10
|
+
* - Gemini: Raw text in message events, result event may have NO result field
|
|
11
|
+
*
|
|
12
|
+
* Extraction priority (most specific → least specific):
|
|
13
|
+
* 1. Result wrapper with content (type:result + result/structured_output field)
|
|
14
|
+
* 2. Accumulated text from provider parser events
|
|
15
|
+
* 3. Markdown code block extraction
|
|
16
|
+
* 4. Direct JSON parse of entire output
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { parseProviderChunk } = require('../providers');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Strip timestamp prefix from log lines.
|
|
23
|
+
* Format: [epochMs]content or [epochMs]{json...}
|
|
24
|
+
*
|
|
25
|
+
* @param {string} line - Raw log line
|
|
26
|
+
* @returns {string} Content without timestamp prefix
|
|
27
|
+
*/
|
|
28
|
+
function stripTimestamp(line) {
|
|
29
|
+
if (!line || typeof line !== 'string') return '';
|
|
30
|
+
let trimmed = line.trim().replace(/\r$/, '');
|
|
31
|
+
if (!trimmed) return '';
|
|
32
|
+
|
|
33
|
+
const tsMatch = trimmed.match(/^\[(\d{13})\](.*)$/);
|
|
34
|
+
if (tsMatch) trimmed = (tsMatch[2] || '').trimStart();
|
|
35
|
+
|
|
36
|
+
// In cluster logs, lines are often prefixed like:
|
|
37
|
+
// "validator | {json...}"
|
|
38
|
+
// Strip the "<agent> | " prefix so we can JSON.parse the event line.
|
|
39
|
+
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
|
|
40
|
+
const pipeMatch = trimmed.match(/^[^|]{1,40}\|\s*(.*)$/);
|
|
41
|
+
if (pipeMatch) {
|
|
42
|
+
const afterPipe = (pipeMatch[1] || '').trimStart();
|
|
43
|
+
if (afterPipe.startsWith('{') || afterPipe.startsWith('[')) return afterPipe;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return trimmed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Strategy 1: Extract from result wrapper
|
|
52
|
+
* Handles Claude CLI format: {"type":"result","result":{...}}
|
|
53
|
+
*
|
|
54
|
+
* @param {string} output - Raw output
|
|
55
|
+
* @returns {object|null} Extracted JSON or null
|
|
56
|
+
*/
|
|
57
|
+
function extractFromResultWrapper(output) {
|
|
58
|
+
const lines = output.split('\n');
|
|
59
|
+
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
const content = stripTimestamp(line);
|
|
62
|
+
if (!content.startsWith('{')) continue;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const obj = JSON.parse(content);
|
|
66
|
+
const extracted = extractResultContent(obj);
|
|
67
|
+
if (extracted) return extracted;
|
|
68
|
+
} catch {
|
|
69
|
+
// Not valid JSON, continue to next line
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function extractResultContent(obj) {
|
|
77
|
+
// Must be type:result WITH actual content
|
|
78
|
+
if (obj?.type !== 'result') return null;
|
|
79
|
+
|
|
80
|
+
// Check structured_output first (standard CLI format)
|
|
81
|
+
if (obj.structured_output && typeof obj.structured_output === 'object') {
|
|
82
|
+
return obj.structured_output;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check result field - can be object or string
|
|
86
|
+
if (!obj.result) return null;
|
|
87
|
+
|
|
88
|
+
if (typeof obj.result === 'object') {
|
|
89
|
+
return obj.result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (typeof obj.result !== 'string') return null;
|
|
93
|
+
|
|
94
|
+
// Result is string - might contain markdown-wrapped JSON
|
|
95
|
+
return extractFromMarkdown(obj.result) || extractDirectJson(obj.result);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Strategy 2: Extract from accumulated text events
|
|
100
|
+
* Handles non-Claude providers where JSON is in text content
|
|
101
|
+
*
|
|
102
|
+
* @param {string} output - Raw output
|
|
103
|
+
* @param {string} providerName - Provider name for parser selection
|
|
104
|
+
* @returns {object|null} Extracted JSON or null
|
|
105
|
+
*/
|
|
106
|
+
function extractFromTextEvents(output, providerName) {
|
|
107
|
+
const normalized = output
|
|
108
|
+
.split('\n')
|
|
109
|
+
.map((line) => stripTimestamp(line))
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.join('\n');
|
|
112
|
+
const events = parseProviderChunk(providerName, normalized);
|
|
113
|
+
|
|
114
|
+
// Fast-path: many providers eventually emit the full JSON as a single text event.
|
|
115
|
+
// Scan from the end to find the last parseable JSON snippet without requiring
|
|
116
|
+
// the entire concatenated stream to be valid JSON.
|
|
117
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
118
|
+
const e = events[i];
|
|
119
|
+
if (e.type !== 'text' || typeof e.text !== 'string') continue;
|
|
120
|
+
const direct = extractDirectJson(e.text) || extractFromMarkdown(e.text);
|
|
121
|
+
if (direct) return direct;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Accumulate all text events
|
|
125
|
+
const textEvents = events.filter((e) => e.type === 'text').map((e) => e.text);
|
|
126
|
+
const textContent = textEvents.join('');
|
|
127
|
+
|
|
128
|
+
if (!textContent.trim()) return null;
|
|
129
|
+
|
|
130
|
+
// Try parsing accumulated text as JSON
|
|
131
|
+
const combined = extractDirectJson(textContent) || extractFromMarkdown(textContent);
|
|
132
|
+
if (combined) return combined;
|
|
133
|
+
|
|
134
|
+
for (let i = textEvents.length - 1; i >= 0; i--) {
|
|
135
|
+
const candidate = textEvents[i];
|
|
136
|
+
const parsed = extractDirectJson(candidate) || extractFromMarkdown(candidate);
|
|
137
|
+
if (parsed) return parsed;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Strategy 3: Extract JSON from markdown code block
|
|
145
|
+
* Handles: ```json\n{...}\n```
|
|
146
|
+
*
|
|
147
|
+
* @param {string} text - Text that may contain markdown
|
|
148
|
+
* @returns {object|null} Extracted JSON or null
|
|
149
|
+
*/
|
|
150
|
+
function extractFromMarkdown(text) {
|
|
151
|
+
if (!text) return null;
|
|
152
|
+
|
|
153
|
+
// Match ```json ... ``` with any whitespace
|
|
154
|
+
const match = text.match(/```json\s*([\s\S]*?)```/);
|
|
155
|
+
if (!match) return null;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const parsed = JSON.parse(match[1].trim());
|
|
159
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
160
|
+
return parsed;
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
// Invalid JSON in markdown block
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* CLI metadata fields that indicate raw provider output (not agent content).
|
|
171
|
+
* These objects should be rejected - they're wrapper metadata, not actual output.
|
|
172
|
+
*/
|
|
173
|
+
const CLI_METADATA_FIELDS = new Set([
|
|
174
|
+
'duration_ms',
|
|
175
|
+
'duration_api_ms',
|
|
176
|
+
'total_cost_usd',
|
|
177
|
+
'session_id',
|
|
178
|
+
'num_turns',
|
|
179
|
+
'permission_denials',
|
|
180
|
+
'modelUsage',
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Check if an object looks like CLI metadata rather than agent output.
|
|
185
|
+
* CLI metadata has specific fields like duration_ms, session_id, etc.
|
|
186
|
+
*
|
|
187
|
+
* @param {object} obj - Parsed JSON object
|
|
188
|
+
* @returns {boolean} True if this looks like CLI metadata
|
|
189
|
+
*/
|
|
190
|
+
function isCliMetadata(obj) {
|
|
191
|
+
if (!obj || typeof obj !== 'object') return false;
|
|
192
|
+
|
|
193
|
+
// If it has type:result, it's definitely CLI wrapper (should have been handled by extractFromResultWrapper)
|
|
194
|
+
if (obj.type === 'result') return true;
|
|
195
|
+
|
|
196
|
+
// Check for CLI-specific metadata fields
|
|
197
|
+
const keys = Object.keys(obj);
|
|
198
|
+
const metadataFieldCount = keys.filter((k) => CLI_METADATA_FIELDS.has(k)).length;
|
|
199
|
+
|
|
200
|
+
// If 2+ CLI metadata fields present, reject as CLI output
|
|
201
|
+
return metadataFieldCount >= 2;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Strategy 4: Direct JSON parse
|
|
206
|
+
* Handles raw JSON output (single-line or multi-line)
|
|
207
|
+
*
|
|
208
|
+
* IMPORTANT: Rejects CLI metadata objects to prevent schema validation
|
|
209
|
+
* against wrong data structure (e.g., validating {duration_ms, session_id}
|
|
210
|
+
* against agent schema expecting {summary, completionStatus}).
|
|
211
|
+
*
|
|
212
|
+
* @param {string} text - Text to parse
|
|
213
|
+
* @returns {object|null} Parsed JSON or null
|
|
214
|
+
*/
|
|
215
|
+
function extractDirectJson(text) {
|
|
216
|
+
if (!text) return null;
|
|
217
|
+
|
|
218
|
+
const trimmed = text.trim();
|
|
219
|
+
if (!trimmed) return null;
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const parsed = JSON.parse(trimmed);
|
|
223
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
224
|
+
// Reject CLI metadata - this is wrapper output, not agent content
|
|
225
|
+
if (isCliMetadata(parsed)) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
return parsed;
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
// Not valid JSON
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Extract CLI error from provider output (all providers).
|
|
239
|
+
* Returns the error message if the CLI reported an error, null otherwise.
|
|
240
|
+
*
|
|
241
|
+
* Provider error formats:
|
|
242
|
+
* - Claude: {type:"result", is_error:true, errors:["msg"]} or {type:"result", subtype:"error"}
|
|
243
|
+
* - Codex: {type:"turn.failed", error:{message:"msg"}}
|
|
244
|
+
* - Gemini: {type:"result", success:false, error:"msg"}
|
|
245
|
+
* - Opencode: {type:"session.error", error:{message:"msg"}}
|
|
246
|
+
*
|
|
247
|
+
* @param {string} output - Raw CLI output
|
|
248
|
+
* @returns {{error: string, provider: string}|null} Error info or null
|
|
249
|
+
*/
|
|
250
|
+
function extractCliError(output) {
|
|
251
|
+
if (!output || typeof output !== 'string') return null;
|
|
252
|
+
|
|
253
|
+
const lines = output.split('\n');
|
|
254
|
+
|
|
255
|
+
for (const line of lines) {
|
|
256
|
+
const content = stripTimestamp(line);
|
|
257
|
+
if (!content.startsWith('{')) continue;
|
|
258
|
+
|
|
259
|
+
let obj;
|
|
260
|
+
try {
|
|
261
|
+
obj = JSON.parse(content);
|
|
262
|
+
} catch {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Claude: {type:"result", is_error:true, errors:[...]}
|
|
267
|
+
if (obj.type === 'result' && obj.is_error === true) {
|
|
268
|
+
const errorMsg = Array.isArray(obj.errors)
|
|
269
|
+
? obj.errors.join('; ')
|
|
270
|
+
: obj.error || obj.result || 'Unknown CLI error';
|
|
271
|
+
return { error: errorMsg, provider: 'claude' };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Claude: {type:"result", subtype:"error"}
|
|
275
|
+
if (obj.type === 'result' && obj.subtype === 'error') {
|
|
276
|
+
const errorMsg = obj.error || obj.result || 'CLI returned error';
|
|
277
|
+
return { error: errorMsg, provider: 'claude' };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Codex: {type:"turn.failed", error:{message:"..."}}
|
|
281
|
+
if (obj.type === 'turn.failed') {
|
|
282
|
+
const errorMsg = obj.error?.message || obj.error || 'Turn failed';
|
|
283
|
+
return { error: errorMsg, provider: 'codex' };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Gemini: {type:"result", success:false, error:"..."}
|
|
287
|
+
if (obj.type === 'result' && obj.success === false && obj.error) {
|
|
288
|
+
return { error: obj.error, provider: 'gemini' };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Opencode: {type:"session.error", error:{...}}
|
|
292
|
+
if (obj.type === 'session.error') {
|
|
293
|
+
const errorMsg =
|
|
294
|
+
obj.error?.data?.message || obj.error?.message || obj.error?.name || 'Session error';
|
|
295
|
+
return { error: errorMsg, provider: 'opencode' };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Detects fatal standalone output lines that indicate no task output was produced.
|
|
304
|
+
* Only matches when the line itself is the fatal message (not when it appears inside JSON).
|
|
305
|
+
*
|
|
306
|
+
* @param {string} output - Raw output text
|
|
307
|
+
* @returns {boolean} True if a standalone fatal line is present
|
|
308
|
+
*/
|
|
309
|
+
function hasFatalStandaloneOutput(output) {
|
|
310
|
+
if (!output || typeof output !== 'string') return false;
|
|
311
|
+
const lines = output.split('\n');
|
|
312
|
+
for (const line of lines) {
|
|
313
|
+
const stripped = stripTimestamp(line).trim();
|
|
314
|
+
if (!stripped) continue;
|
|
315
|
+
if (/^(task not found|process terminated)\b/i.test(stripped)) {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Main extraction function - tries all strategies in priority order
|
|
324
|
+
*
|
|
325
|
+
* @param {string} output - Raw output from AI provider CLI
|
|
326
|
+
* @param {string} providerName - Provider name ('claude', 'codex', 'gemini')
|
|
327
|
+
* @returns {object|null} Extracted JSON object or null if extraction failed
|
|
328
|
+
*/
|
|
329
|
+
function extractJsonFromOutput(output, providerName = 'claude') {
|
|
330
|
+
if (!output || typeof output !== 'string') return null;
|
|
331
|
+
|
|
332
|
+
const trimmedOutput = output.trim();
|
|
333
|
+
if (!trimmedOutput) return null;
|
|
334
|
+
|
|
335
|
+
// Strategy 1: Result wrapper (Claude format)
|
|
336
|
+
const fromWrapper = extractFromResultWrapper(trimmedOutput);
|
|
337
|
+
if (fromWrapper) return fromWrapper;
|
|
338
|
+
|
|
339
|
+
// Strategy 2: Text events (non-Claude providers)
|
|
340
|
+
const fromText = extractFromTextEvents(trimmedOutput, providerName);
|
|
341
|
+
if (fromText) return fromText;
|
|
342
|
+
|
|
343
|
+
// Strategy 3: Markdown extraction
|
|
344
|
+
const fromMarkdown = extractFromMarkdown(trimmedOutput);
|
|
345
|
+
if (fromMarkdown) return fromMarkdown;
|
|
346
|
+
|
|
347
|
+
// Strategy 4: Direct JSON parse (raw output)
|
|
348
|
+
const fromDirect = extractDirectJson(trimmedOutput);
|
|
349
|
+
if (fromDirect) return fromDirect;
|
|
350
|
+
|
|
351
|
+
if (hasFatalStandaloneOutput(trimmedOutput)) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
module.exports = {
|
|
359
|
+
extractJsonFromOutput,
|
|
360
|
+
extractCliError,
|
|
361
|
+
extractFromResultWrapper,
|
|
362
|
+
extractFromTextEvents,
|
|
363
|
+
extractFromMarkdown,
|
|
364
|
+
extractDirectJson,
|
|
365
|
+
stripTimestamp,
|
|
366
|
+
hasFatalStandaloneOutput,
|
|
367
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Reformatter - Convert non-JSON output to valid JSON
|
|
3
|
+
*
|
|
4
|
+
* When an LLM outputs markdown/text instead of JSON despite schema instructions,
|
|
5
|
+
* this module attempts to extract/reformat the content into valid JSON.
|
|
6
|
+
*
|
|
7
|
+
* STATUS: SDK NOT IMPLEMENTED - Reformatting is not available.
|
|
8
|
+
* This module exists for future extension when SDK support is added.
|
|
9
|
+
*
|
|
10
|
+
* To enable reformatting:
|
|
11
|
+
* 1. Implement SDK support in the provider (getSDKEnvVar, callSimple)
|
|
12
|
+
* 2. The reformatOutput() function will then work automatically
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build the reformatting prompt
|
|
19
|
+
*
|
|
20
|
+
* @param {string} rawOutput - The non-JSON output to reformat
|
|
21
|
+
* @param {Object} schema - Target JSON schema
|
|
22
|
+
* @param {string|null} previousError - Error from previous attempt (for feedback)
|
|
23
|
+
* @returns {string} The prompt for the reformatting model
|
|
24
|
+
*/
|
|
25
|
+
function buildReformatPrompt(rawOutput, schema, previousError = null) {
|
|
26
|
+
const schemaStr = JSON.stringify(schema, null, 2);
|
|
27
|
+
// Truncate long outputs to avoid context limits
|
|
28
|
+
const truncatedOutput = rawOutput.length > 4000 ? rawOutput.slice(-4000) : rawOutput;
|
|
29
|
+
|
|
30
|
+
let prompt = `Convert this text into a JSON object matching the schema.
|
|
31
|
+
|
|
32
|
+
## SCHEMA
|
|
33
|
+
\`\`\`json
|
|
34
|
+
${schemaStr}
|
|
35
|
+
\`\`\`
|
|
36
|
+
|
|
37
|
+
## TEXT TO CONVERT
|
|
38
|
+
\`\`\`
|
|
39
|
+
${truncatedOutput}
|
|
40
|
+
\`\`\`
|
|
41
|
+
|
|
42
|
+
## RULES
|
|
43
|
+
- Output ONLY the JSON object
|
|
44
|
+
- NO markdown code blocks
|
|
45
|
+
- NO explanations
|
|
46
|
+
- Start with { end with }
|
|
47
|
+
- Match ALL required fields from schema`;
|
|
48
|
+
|
|
49
|
+
if (previousError) {
|
|
50
|
+
prompt += `
|
|
51
|
+
|
|
52
|
+
## PREVIOUS ATTEMPT FAILED
|
|
53
|
+
Error: ${previousError}
|
|
54
|
+
Fix this issue in your response.`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return prompt;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Attempt to reformat non-JSON output into valid JSON
|
|
62
|
+
*
|
|
63
|
+
* STATUS: SDK NOT IMPLEMENTED - This function always throws.
|
|
64
|
+
* When SDK support is added to providers, this will work automatically.
|
|
65
|
+
*
|
|
66
|
+
* @param {Object} options
|
|
67
|
+
* @param {string} options.rawOutput - The non-JSON output to reformat
|
|
68
|
+
* @param {Object} options.schema - Target JSON schema
|
|
69
|
+
* @param {string} options.providerName - Provider name (claude, codex, gemini, opencode)
|
|
70
|
+
* @param {number} [options.maxAttempts=3] - Maximum reformatting attempts
|
|
71
|
+
* @param {Function} [options.onAttempt] - Callback for each attempt (attempt, error)
|
|
72
|
+
* @returns {Promise<Object>} The reformatted JSON object
|
|
73
|
+
* @throws {Error} Always throws - SDK not implemented
|
|
74
|
+
*/
|
|
75
|
+
function reformatOutput({
|
|
76
|
+
rawOutput,
|
|
77
|
+
schema: _schema,
|
|
78
|
+
providerName,
|
|
79
|
+
maxAttempts: _maxAttempts = DEFAULT_MAX_ATTEMPTS,
|
|
80
|
+
onAttempt: _onAttempt,
|
|
81
|
+
}) {
|
|
82
|
+
// SDK not implemented - reformatting not available
|
|
83
|
+
// When SDK support is added, uncomment the implementation below
|
|
84
|
+
return Promise.reject(
|
|
85
|
+
new Error(
|
|
86
|
+
`Output reformatting not available: SDK not implemented for provider "${providerName}". ` +
|
|
87
|
+
`Agent output must be valid JSON. Raw output (last 200 chars): ${(rawOutput || '').slice(-200)}`
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// FUTURE: When SDK support is added to providers, uncomment this:
|
|
92
|
+
/*
|
|
93
|
+
const { getProvider } = require('../providers');
|
|
94
|
+
const provider = getProvider(providerName);
|
|
95
|
+
|
|
96
|
+
let lastError = null;
|
|
97
|
+
|
|
98
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
99
|
+
if (onAttempt) {
|
|
100
|
+
onAttempt(attempt, lastError);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const prompt = buildReformatPrompt(rawOutput, schema, lastError);
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const result = await provider.callSimple(prompt, {
|
|
107
|
+
level: 'level1',
|
|
108
|
+
maxTokens: 2000,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!result?.success) {
|
|
112
|
+
lastError = result?.error || 'API call failed';
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!result?.text) {
|
|
117
|
+
lastError = 'Empty response from reformatting model';
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const parsed = extractJsonFromOutput(result.text, providerName);
|
|
122
|
+
|
|
123
|
+
if (!parsed) {
|
|
124
|
+
lastError = 'Could not extract JSON from reformatted output';
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const validationError = validateAgainstSchema(parsed, schema);
|
|
129
|
+
if (validationError) {
|
|
130
|
+
lastError = validationError;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return parsed;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
lastError = err.message;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Failed to reformat output after ${maxAttempts} attempts. Last error: ${lastError}`
|
|
142
|
+
);
|
|
143
|
+
*/
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Validate parsed output against JSON schema
|
|
148
|
+
*
|
|
149
|
+
* @param {Object} parsed - Parsed JSON object
|
|
150
|
+
* @param {Object} schema - JSON schema to validate against
|
|
151
|
+
* @returns {string|null} Error message if validation failed, null if valid
|
|
152
|
+
*/
|
|
153
|
+
function validateAgainstSchema(parsed, schema) {
|
|
154
|
+
const Ajv = require('ajv');
|
|
155
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
156
|
+
const validate = ajv.compile(schema);
|
|
157
|
+
const valid = validate(parsed);
|
|
158
|
+
|
|
159
|
+
if (!valid) {
|
|
160
|
+
const errors = (validate.errors || [])
|
|
161
|
+
.slice(0, 3)
|
|
162
|
+
.map((e) => `${e.instancePath || '#'} ${e.message}`)
|
|
163
|
+
.join('; ');
|
|
164
|
+
return errors || 'Schema validation failed';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
reformatOutput,
|
|
172
|
+
buildReformatPrompt,
|
|
173
|
+
validateAgainstSchema,
|
|
174
|
+
DEFAULT_MAX_ATTEMPTS,
|
|
175
|
+
};
|