claude-flow 3.10.4 → 3.10.5

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.
Files changed (58) hide show
  1. package/package.json +1 -1
  2. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-agent-planning.smoke.d.ts +18 -0
  3. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-agent-planning.smoke.js +253 -0
  4. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-agent.d.ts +198 -0
  5. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-agent.js +651 -0
  6. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-causal-memory.d.ts +133 -0
  7. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-causal-memory.js +281 -0
  8. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-causal-memory.smoke.d.ts +22 -0
  9. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-causal-memory.smoke.js +300 -0
  10. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-convergence.d.ts +138 -0
  11. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-convergence.js +260 -0
  12. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-convergence.smoke.d.ts +19 -0
  13. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-convergence.smoke.js +246 -0
  14. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-critic.d.ts +123 -0
  15. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-critic.js +312 -0
  16. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-critic.smoke.d.ts +21 -0
  17. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-critic.smoke.js +327 -0
  18. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-decomposer.d.ts +125 -0
  19. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-decomposer.js +350 -0
  20. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-decomposer.smoke.d.ts +21 -0
  21. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-decomposer.smoke.js +228 -0
  22. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-e2e-smoke.d.ts +27 -0
  23. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-e2e-smoke.js +136 -0
  24. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-extract.smoke.d.ts +45 -0
  25. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-extract.smoke.js +242 -0
  26. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/features.d.ts +46 -0
  27. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/features.js +170 -0
  28. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/predictor.d.ts +105 -0
  29. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/predictor.js +260 -0
  30. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/predictor.smoke.d.ts +20 -0
  31. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/predictor.smoke.js +235 -0
  32. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/train-data-loader.d.ts +51 -0
  33. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-hardness/train-data-loader.js +179 -0
  34. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-judge.d.ts +88 -0
  35. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-judge.js +437 -0
  36. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-loader.d.ts +87 -0
  37. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-loader.js +326 -0
  38. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/file_read.d.ts +35 -0
  39. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/file_read.js +403 -0
  40. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/grounded_query.d.ts +126 -0
  41. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/grounded_query.js +225 -0
  42. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/index.d.ts +32 -0
  43. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/index.js +36 -0
  44. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/types.d.ts +62 -0
  45. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/types.js +12 -0
  46. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/web_search.d.ts +30 -0
  47. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-tools/web_search.js +210 -0
  48. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-voting.d.ts +88 -0
  49. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-voting.js +297 -0
  50. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-voting.smoke.d.ts +20 -0
  51. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-voting.smoke.js +332 -0
  52. package/v3/@claude-flow/cli/dist/src/commands/gaia-bench.d.ts +40 -0
  53. package/v3/@claude-flow/cli/dist/src/commands/gaia-bench.js +597 -0
  54. package/v3/@claude-flow/cli/dist/src/commands/index.js +2 -0
  55. package/v3/@claude-flow/cli/dist/src/commands/init.js +41 -4
  56. package/v3/@claude-flow/cli/dist/src/init/executor.js +25 -6
  57. package/v3/@claude-flow/cli/dist/src/init/mcp-generator.js +9 -4
  58. package/v3/@claude-flow/cli/package.json +1 -1
@@ -0,0 +1,125 @@
1
+ /**
2
+ * GAIA Question Decomposer — ADR-135 Track E
3
+ *
4
+ * Decomposes complex GAIA benchmark questions into 1-5 ordered sub-questions
5
+ * that can each be answered with a single tool call, then synthesizes the
6
+ * sub-answers into a final response.
7
+ *
8
+ * Motivation (iter 29 finding): tool quality is the bottleneck on L1 (~20.8%).
9
+ * Bad tools that fail on complex queries may succeed on focused sub-queries.
10
+ * This mimics what humans do at 92% on GAIA (decompose-then-solve).
11
+ * Expected L1 lift: +5-10pp on multi-step questions (~30-40% of L1 set).
12
+ *
13
+ * Design:
14
+ * - NEW standalone file only; NOT wired into gaia-bench.ts to avoid merge
15
+ * conflicts with in-flight iter 29/31/34/35/36 branches.
16
+ * Wiring is a small follow-up PR.
17
+ * - `decomposeQuestion()` — Haiku-cheap classification + decomposition
18
+ * (~$0.0003 per question).
19
+ * - `synthesizeFromSubAnswers()` — Sonnet synthesis from sub-answers
20
+ * (the sub-answers are the hard work; synthesis is just combination).
21
+ * - Atomic questions are returned as-is — no overhead when not needed.
22
+ * - Graceful fallback to atomic on API errors or malformed JSON.
23
+ *
24
+ * Cost discipline:
25
+ * - Decomposition uses claude-haiku-4-5 (~$0.0003/question).
26
+ * - Synthesis uses claude-sonnet-4-6 (~$0.002/question).
27
+ * - Total overhead per question (when decomposed): ~$0.002-0.003.
28
+ *
29
+ * Plugin sync TODO (follow-up PR after gaia-bench wiring):
30
+ * - Update plugins/ruflo-workflows/commands/gaia-run.md with --decompose flag.
31
+ * - Update plugins/ruflo-workflows/skills/gaia-debugging/SKILL.md: add
32
+ * decomposition as a recommended strategy for multi-step failures.
33
+ *
34
+ * Refs: ADR-135, ADR-133, iter 29 finding, #2156
35
+ */
36
+ export interface DecomposedQuestion {
37
+ /** The original unmodified question text. */
38
+ originalQuestion: string;
39
+ /**
40
+ * Ordered sub-questions in dependency order (1-5).
41
+ * If decomposed=false, contains exactly one entry equal to originalQuestion.
42
+ */
43
+ subQuestions: string[];
44
+ /**
45
+ * Brief hint for how to combine sub-answers into the final answer.
46
+ * Example: "Multiply the two values found in sub-questions 1 and 2."
47
+ */
48
+ synthesisHint: string;
49
+ /**
50
+ * true if the question was split into multiple sub-questions;
51
+ * false if it was deemed atomic (single lookup / single computation).
52
+ */
53
+ decomposed: boolean;
54
+ /** USD spent on the decomposition API call (0 if fallback/atomic). */
55
+ cost: number;
56
+ }
57
+ export interface DecomposerOptions {
58
+ /**
59
+ * Model to use for decomposition classification.
60
+ * Default: 'claude-haiku-4-5' (cheap).
61
+ */
62
+ model?: string;
63
+ /**
64
+ * Maximum sub-questions to produce when decomposing.
65
+ * Default: 5.
66
+ */
67
+ maxSubQuestions?: number;
68
+ /** API key override. Falls back to ANTHROPIC_API_KEY env var. */
69
+ apiKey?: string;
70
+ }
71
+ export interface SynthesizerOptions {
72
+ /**
73
+ * Model to use for final-answer synthesis.
74
+ * Default: 'claude-sonnet-4-6'.
75
+ */
76
+ model?: string;
77
+ /** API key override. Falls back to ANTHROPIC_API_KEY env var. */
78
+ apiKey?: string;
79
+ }
80
+ export interface SynthesisResult {
81
+ /** The synthesized final answer string. */
82
+ finalAnswer: string;
83
+ /** Brief reasoning that led to the final answer. */
84
+ reasoning: string;
85
+ /** USD spent on the synthesis API call. */
86
+ cost: number;
87
+ }
88
+ /**
89
+ * Decomposes a complex question into 1-5 sub-questions in dependency order,
90
+ * OR returns the question as-is if it is already atomic.
91
+ *
92
+ * Uses `claude-haiku-4-5` by default for cheap classification + decomposition
93
+ * (~$0.0003 per question).
94
+ *
95
+ * Heuristics the model uses internally for "should decompose":
96
+ * - Question contains "and", "then", "after", "if X …"
97
+ * - Question contains multiple named entities that must each be looked up
98
+ * - Question asks for a derived/computed answer (X of Y where Y must be found)
99
+ *
100
+ * Graceful degradation: on API errors or malformed JSON, returns the question
101
+ * as atomic so the calling agent can still attempt a direct answer.
102
+ *
103
+ * @param questionText - The full GAIA question text.
104
+ * @param options - Optional overrides (model, maxSubQuestions, apiKey).
105
+ * @returns - DecomposedQuestion with subQuestions and synthesisHint.
106
+ */
107
+ export declare function decomposeQuestion(questionText: string, options?: DecomposerOptions): Promise<DecomposedQuestion>;
108
+ /**
109
+ * Given a decomposed question and answers to each sub-question, synthesizes
110
+ * a final concise answer.
111
+ *
112
+ * Uses `claude-sonnet-4-6` by default for higher reasoning quality — the
113
+ * sub-answers contain the hard-won information; synthesis is recombination.
114
+ *
115
+ * Graceful degradation: on API errors or malformed JSON, returns the last
116
+ * sub-answer concatenated with reasoning note.
117
+ *
118
+ * @param decomposed - The DecomposedQuestion from `decomposeQuestion()`.
119
+ * @param subAnswers - Array of string answers, one per sub-question.
120
+ * Must be the same length as decomposed.subQuestions.
121
+ * @param options - Optional overrides (model, apiKey).
122
+ * @returns - SynthesisResult with finalAnswer, reasoning, and cost.
123
+ */
124
+ export declare function synthesizeFromSubAnswers(decomposed: DecomposedQuestion, subAnswers: string[], options?: SynthesizerOptions): Promise<SynthesisResult>;
125
+ //# sourceMappingURL=gaia-decomposer.d.ts.map
@@ -0,0 +1,350 @@
1
+ /**
2
+ * GAIA Question Decomposer — ADR-135 Track E
3
+ *
4
+ * Decomposes complex GAIA benchmark questions into 1-5 ordered sub-questions
5
+ * that can each be answered with a single tool call, then synthesizes the
6
+ * sub-answers into a final response.
7
+ *
8
+ * Motivation (iter 29 finding): tool quality is the bottleneck on L1 (~20.8%).
9
+ * Bad tools that fail on complex queries may succeed on focused sub-queries.
10
+ * This mimics what humans do at 92% on GAIA (decompose-then-solve).
11
+ * Expected L1 lift: +5-10pp on multi-step questions (~30-40% of L1 set).
12
+ *
13
+ * Design:
14
+ * - NEW standalone file only; NOT wired into gaia-bench.ts to avoid merge
15
+ * conflicts with in-flight iter 29/31/34/35/36 branches.
16
+ * Wiring is a small follow-up PR.
17
+ * - `decomposeQuestion()` — Haiku-cheap classification + decomposition
18
+ * (~$0.0003 per question).
19
+ * - `synthesizeFromSubAnswers()` — Sonnet synthesis from sub-answers
20
+ * (the sub-answers are the hard work; synthesis is just combination).
21
+ * - Atomic questions are returned as-is — no overhead when not needed.
22
+ * - Graceful fallback to atomic on API errors or malformed JSON.
23
+ *
24
+ * Cost discipline:
25
+ * - Decomposition uses claude-haiku-4-5 (~$0.0003/question).
26
+ * - Synthesis uses claude-sonnet-4-6 (~$0.002/question).
27
+ * - Total overhead per question (when decomposed): ~$0.002-0.003.
28
+ *
29
+ * Plugin sync TODO (follow-up PR after gaia-bench wiring):
30
+ * - Update plugins/ruflo-workflows/commands/gaia-run.md with --decompose flag.
31
+ * - Update plugins/ruflo-workflows/skills/gaia-debugging/SKILL.md: add
32
+ * decomposition as a recommended strategy for multi-step failures.
33
+ *
34
+ * Refs: ADR-135, ADR-133, iter 29 finding, #2156
35
+ */
36
+ import { execSync } from 'node:child_process';
37
+ // ---------------------------------------------------------------------------
38
+ // Constants
39
+ // ---------------------------------------------------------------------------
40
+ const ANTHROPIC_API_URL = 'https://api.anthropic.com/v1/messages';
41
+ const ANTHROPIC_API_VERSION = '2023-06-01';
42
+ /**
43
+ * Haiku for cheap decomposition classification (~$0.0003/question).
44
+ * Callers can override via DecomposerOptions.model.
45
+ */
46
+ const DEFAULT_DECOMPOSER_MODEL = 'claude-haiku-4-5';
47
+ /**
48
+ * Sonnet for synthesis — sub-answers are the expensive part, synthesis is
49
+ * just recombination, but needs good reasoning. Callers can override.
50
+ */
51
+ const DEFAULT_SYNTHESIZER_MODEL = 'claude-sonnet-4-6';
52
+ const DECOMPOSER_MAX_TOKENS = 512;
53
+ const SYNTHESIZER_MAX_TOKENS = 512;
54
+ /** Haiku pricing per million tokens (2026-05-27). */
55
+ const HAIKU_INPUT_COST_PER_M = 0.8;
56
+ const HAIKU_OUTPUT_COST_PER_M = 4.0;
57
+ /** Sonnet pricing per million tokens (2026-05-27). */
58
+ const SONNET_INPUT_COST_PER_M = 3.0;
59
+ const SONNET_OUTPUT_COST_PER_M = 15.0;
60
+ // ---------------------------------------------------------------------------
61
+ // API key resolution (mirrors gaia-agent.ts / gaia-judge.ts pattern)
62
+ // ---------------------------------------------------------------------------
63
+ function resolveApiKey(supplied) {
64
+ if (supplied && supplied.trim())
65
+ return supplied.trim();
66
+ const envKey = process.env.ANTHROPIC_API_KEY;
67
+ if (envKey && envKey.trim())
68
+ return envKey.trim();
69
+ try {
70
+ const out = execSync('gcloud secrets versions access latest --secret=ANTHROPIC_API_KEY 2>/dev/null', { encoding: 'utf-8', timeout: 10_000 }).trim();
71
+ if (out)
72
+ return out;
73
+ }
74
+ catch {
75
+ /* fall through */
76
+ }
77
+ throw new Error('ANTHROPIC_API_KEY not found. Set the env var or store it in GCP Secret Manager ' +
78
+ 'under "ANTHROPIC_API_KEY".');
79
+ }
80
+ async function callAnthropic(systemPrompt, userMessage, model, maxTokens, apiKey) {
81
+ const body = JSON.stringify({
82
+ model,
83
+ max_tokens: maxTokens,
84
+ system: systemPrompt,
85
+ messages: [{ role: 'user', content: userMessage }],
86
+ });
87
+ const response = await fetch(ANTHROPIC_API_URL, {
88
+ method: 'POST',
89
+ headers: {
90
+ 'Content-Type': 'application/json',
91
+ 'x-api-key': apiKey,
92
+ 'anthropic-version': ANTHROPIC_API_VERSION,
93
+ },
94
+ body,
95
+ });
96
+ if (!response.ok) {
97
+ const errText = await response.text().catch(() => '(no body)');
98
+ throw new Error(`Anthropic API error ${response.status}: ${errText}`);
99
+ }
100
+ const data = (await response.json());
101
+ const textBlock = data.content.find((c) => c.type === 'text');
102
+ const text = textBlock?.text ?? '';
103
+ return {
104
+ text,
105
+ tokensIn: data.usage.input_tokens,
106
+ tokensOut: data.usage.output_tokens,
107
+ };
108
+ }
109
+ // ---------------------------------------------------------------------------
110
+ // Decomposer prompt
111
+ // ---------------------------------------------------------------------------
112
+ function buildDecomposerSystemPrompt(maxSubQuestions) {
113
+ return [
114
+ 'You are decomposing GAIA benchmark questions into sub-questions for a tool-using agent.',
115
+ '',
116
+ `Rules:`,
117
+ '1. If the question is ATOMIC (single fact lookup or single computation — one tool call',
118
+ ' would answer it directly), return:',
119
+ ' {"decomposed": false, "subQuestions": ["<original question>"], "synthesisHint": "Use directly."}',
120
+ '',
121
+ `2. If the question requires ${maxSubQuestions > 2 ? '2-' + maxSubQuestions : '2'} dependent steps, decompose into ORDERED`,
122
+ ' sub-questions where each can be answered with a single tool call:',
123
+ ' {"decomposed": true, "subQuestions": ["...", "...", "..."], "synthesisHint": "How to combine"}',
124
+ '',
125
+ 'Sub-question requirements:',
126
+ ' - Each sub-question must be SELF-CONTAINED (no pronouns referring to earlier sub-questions).',
127
+ ' - Sub-questions must be in DEPENDENCY ORDER (earlier answers feed into later questions if needed).',
128
+ ' - Sub-questions should be specific enough for a single web search or computation.',
129
+ ` - Maximum ${maxSubQuestions} sub-questions.`,
130
+ '',
131
+ 'Examples of ATOMIC questions (decomposed=false):',
132
+ ' "What year was the Eiffel Tower built?"',
133
+ ' "What is 25% of 840?"',
134
+ ' "Who wrote Pride and Prejudice?"',
135
+ '',
136
+ 'Examples of COMPLEX questions (decomposed=true):',
137
+ ' "Who directed the highest-grossing film of the decade that contained the year',
138
+ ' the Eiffel Tower was built?" → sub-questions:',
139
+ ' 1. "What year was the Eiffel Tower built?"',
140
+ ' 2. "What decade contains [year from Q1]?" (compute from Q1 answer)',
141
+ ' 3. "What was the highest-grossing film of [decade from Q2]?"',
142
+ ' 4. "Who directed [film from Q3]?"',
143
+ '',
144
+ 'Respond with JSON only. No markdown, no explanation outside the JSON object.',
145
+ ].join('\n');
146
+ }
147
+ // ---------------------------------------------------------------------------
148
+ // Synthesis prompt
149
+ // ---------------------------------------------------------------------------
150
+ function buildSynthesizerSystemPrompt() {
151
+ return [
152
+ 'You are synthesizing a final answer to a GAIA benchmark question from answers',
153
+ 'to sub-questions.',
154
+ '',
155
+ 'Rules:',
156
+ '1. Use ONLY the sub-answers provided — do not hallucinate additional information.',
157
+ '2. The final answer must be CONCISE — match the GAIA expected format (often a',
158
+ ' single word, number, name, or short phrase).',
159
+ '3. Apply the synthesis hint if provided.',
160
+ '4. State your reasoning briefly, then give the final answer.',
161
+ '',
162
+ 'Respond with JSON only:',
163
+ '{"finalAnswer": "<concise answer>", "reasoning": "<brief chain of reasoning, ≤300 chars>"}',
164
+ ].join('\n');
165
+ }
166
+ function parseDecomposerResponse(raw) {
167
+ // Strip optional code fences
168
+ const clean = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '').trim();
169
+ try {
170
+ return JSON.parse(clean);
171
+ }
172
+ catch {
173
+ return null;
174
+ }
175
+ }
176
+ function parseSynthesizerResponse(raw) {
177
+ const clean = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '').trim();
178
+ try {
179
+ return JSON.parse(clean);
180
+ }
181
+ catch {
182
+ return null;
183
+ }
184
+ }
185
+ // ---------------------------------------------------------------------------
186
+ // Atomic fallback
187
+ // ---------------------------------------------------------------------------
188
+ function atomicFallback(questionText) {
189
+ return {
190
+ originalQuestion: questionText,
191
+ subQuestions: [questionText],
192
+ synthesisHint: 'Use directly.',
193
+ decomposed: false,
194
+ cost: 0,
195
+ };
196
+ }
197
+ // ---------------------------------------------------------------------------
198
+ // Public API
199
+ // ---------------------------------------------------------------------------
200
+ /**
201
+ * Decomposes a complex question into 1-5 sub-questions in dependency order,
202
+ * OR returns the question as-is if it is already atomic.
203
+ *
204
+ * Uses `claude-haiku-4-5` by default for cheap classification + decomposition
205
+ * (~$0.0003 per question).
206
+ *
207
+ * Heuristics the model uses internally for "should decompose":
208
+ * - Question contains "and", "then", "after", "if X …"
209
+ * - Question contains multiple named entities that must each be looked up
210
+ * - Question asks for a derived/computed answer (X of Y where Y must be found)
211
+ *
212
+ * Graceful degradation: on API errors or malformed JSON, returns the question
213
+ * as atomic so the calling agent can still attempt a direct answer.
214
+ *
215
+ * @param questionText - The full GAIA question text.
216
+ * @param options - Optional overrides (model, maxSubQuestions, apiKey).
217
+ * @returns - DecomposedQuestion with subQuestions and synthesisHint.
218
+ */
219
+ export async function decomposeQuestion(questionText, options) {
220
+ const model = options?.model ?? DEFAULT_DECOMPOSER_MODEL;
221
+ const maxSubQuestions = options?.maxSubQuestions ?? 5;
222
+ let apiKey;
223
+ try {
224
+ apiKey = resolveApiKey(options?.apiKey);
225
+ }
226
+ catch {
227
+ // No API key available — return atomic fallback (graceful degradation)
228
+ return atomicFallback(questionText);
229
+ }
230
+ let text;
231
+ let tokensIn;
232
+ let tokensOut;
233
+ try {
234
+ ({ text, tokensIn, tokensOut } = await callAnthropic(buildDecomposerSystemPrompt(maxSubQuestions), `Question: ${questionText}`, model, DECOMPOSER_MAX_TOKENS, apiKey));
235
+ }
236
+ catch {
237
+ // API error — return atomic fallback so the pipeline can continue
238
+ return atomicFallback(questionText);
239
+ }
240
+ const costUsd = (tokensIn / 1_000_000) * HAIKU_INPUT_COST_PER_M +
241
+ (tokensOut / 1_000_000) * HAIKU_OUTPUT_COST_PER_M;
242
+ const parsed = parseDecomposerResponse(text);
243
+ // Validate parsed payload — fall back to atomic on any malformed response
244
+ if (parsed === null ||
245
+ !Array.isArray(parsed.subQuestions) ||
246
+ parsed.subQuestions.length === 0) {
247
+ return { ...atomicFallback(questionText), cost: costUsd };
248
+ }
249
+ const subQuestions = parsed.subQuestions
250
+ .slice(0, maxSubQuestions)
251
+ .map((q) => String(q).trim())
252
+ .filter((q) => q.length > 0);
253
+ if (subQuestions.length === 0) {
254
+ return { ...atomicFallback(questionText), cost: costUsd };
255
+ }
256
+ const decomposed = parsed.decomposed !== false && subQuestions.length > 1;
257
+ const synthesisHint = String(parsed.synthesisHint ?? 'Combine sub-answers into a final answer.').trim();
258
+ return {
259
+ originalQuestion: questionText,
260
+ subQuestions,
261
+ synthesisHint,
262
+ decomposed,
263
+ cost: costUsd,
264
+ };
265
+ }
266
+ /**
267
+ * Given a decomposed question and answers to each sub-question, synthesizes
268
+ * a final concise answer.
269
+ *
270
+ * Uses `claude-sonnet-4-6` by default for higher reasoning quality — the
271
+ * sub-answers contain the hard-won information; synthesis is recombination.
272
+ *
273
+ * Graceful degradation: on API errors or malformed JSON, returns the last
274
+ * sub-answer concatenated with reasoning note.
275
+ *
276
+ * @param decomposed - The DecomposedQuestion from `decomposeQuestion()`.
277
+ * @param subAnswers - Array of string answers, one per sub-question.
278
+ * Must be the same length as decomposed.subQuestions.
279
+ * @param options - Optional overrides (model, apiKey).
280
+ * @returns - SynthesisResult with finalAnswer, reasoning, and cost.
281
+ */
282
+ export async function synthesizeFromSubAnswers(decomposed, subAnswers, options) {
283
+ const model = options?.model ?? DEFAULT_SYNTHESIZER_MODEL;
284
+ // If not truly decomposed, just return the single sub-answer directly
285
+ if (!decomposed.decomposed || decomposed.subQuestions.length === 1) {
286
+ const singleAnswer = subAnswers[0] ?? '';
287
+ return {
288
+ finalAnswer: singleAnswer,
289
+ reasoning: 'Atomic question — sub-answer is the final answer.',
290
+ cost: 0,
291
+ };
292
+ }
293
+ let apiKey;
294
+ try {
295
+ apiKey = resolveApiKey(options?.apiKey);
296
+ }
297
+ catch {
298
+ const fallback = subAnswers[subAnswers.length - 1] ?? '';
299
+ return {
300
+ finalAnswer: fallback,
301
+ reasoning: 'No API key — returning last sub-answer as fallback.',
302
+ cost: 0,
303
+ };
304
+ }
305
+ // Build the user message: list each sub-question with its answer
306
+ const qaLines = decomposed.subQuestions.map((q, i) => {
307
+ const a = subAnswers[i] ?? '(no answer)';
308
+ return `Sub-question ${i + 1}: ${q}\nSub-answer ${i + 1}: ${a}`;
309
+ });
310
+ const userMessage = [
311
+ `Original question: ${decomposed.originalQuestion}`,
312
+ '',
313
+ 'Sub-question answers:',
314
+ qaLines.join('\n\n'),
315
+ '',
316
+ `Synthesis hint: ${decomposed.synthesisHint}`,
317
+ ].join('\n');
318
+ let text;
319
+ let tokensIn;
320
+ let tokensOut;
321
+ try {
322
+ ({ text, tokensIn, tokensOut } = await callAnthropic(buildSynthesizerSystemPrompt(), userMessage, model, SYNTHESIZER_MAX_TOKENS, apiKey));
323
+ }
324
+ catch {
325
+ const fallback = subAnswers[subAnswers.length - 1] ?? '';
326
+ return {
327
+ finalAnswer: fallback,
328
+ reasoning: 'API error during synthesis — returning last sub-answer.',
329
+ cost: 0,
330
+ };
331
+ }
332
+ const costUsd = (tokensIn / 1_000_000) * SONNET_INPUT_COST_PER_M +
333
+ (tokensOut / 1_000_000) * SONNET_OUTPUT_COST_PER_M;
334
+ const parsed = parseSynthesizerResponse(text);
335
+ if (!parsed || !parsed.finalAnswer) {
336
+ // Malformed JSON — return last sub-answer as fallback
337
+ const fallback = subAnswers[subAnswers.length - 1] ?? '';
338
+ return {
339
+ finalAnswer: fallback,
340
+ reasoning: `Parse error — raw: ${text.slice(0, 100)}`,
341
+ cost: costUsd,
342
+ };
343
+ }
344
+ return {
345
+ finalAnswer: String(parsed.finalAnswer).trim(),
346
+ reasoning: String(parsed.reasoning ?? '').slice(0, 300),
347
+ cost: costUsd,
348
+ };
349
+ }
350
+ //# sourceMappingURL=gaia-decomposer.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Smoke tests for gaia-decomposer.ts (ADR-135 Track E)
3
+ *
4
+ * All HTTP calls are mocked — no live API calls, no cost.
5
+ * Covers 7 assertions:
6
+ * 1. Atomic question → decomposed=false, single sub-question = original
7
+ * 2. 3-step question → decomposed=true, 3 sub-questions in dependency order
8
+ * 3. Malformed JSON response → fallback to atomic (graceful degradation)
9
+ * 4. API error → fallback to atomic (graceful degradation)
10
+ * 5. synthesizeFromSubAnswers — atomic question returns sub-answer directly (no API call)
11
+ * 6. synthesizeFromSubAnswers — valid JSON response → finalAnswer + reasoning
12
+ * 7. synthesizeFromSubAnswers — malformed JSON → fallback to last sub-answer
13
+ *
14
+ * Run with:
15
+ * npx tsx src/benchmarks/gaia-decomposer.smoke.ts
16
+ *
17
+ * Expected cost: $0 (all mocked).
18
+ */
19
+ declare function runSmoke(): Promise<void>;
20
+ export { runSmoke };
21
+ //# sourceMappingURL=gaia-decomposer.smoke.d.ts.map