@baanish/hydra-cli 0.1.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/LICENSE +21 -0
- package/README.md +122 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +338 -0
- package/dist/config.js.map +1 -0
- package/dist/db/client.d.ts +10 -0
- package/dist/db/client.d.ts.map +1 -0
- package/dist/db/client.js +93 -0
- package/dist/db/client.js.map +1 -0
- package/dist/db/queries.d.ts +67 -0
- package/dist/db/queries.d.ts.map +1 -0
- package/dist/db/queries.js +336 -0
- package/dist/db/queries.js.map +1 -0
- package/dist/engine/concurrency.d.ts +3 -0
- package/dist/engine/concurrency.d.ts.map +1 -0
- package/dist/engine/concurrency.js +42 -0
- package/dist/engine/concurrency.js.map +1 -0
- package/dist/engine/eta.d.ts +16 -0
- package/dist/engine/eta.d.ts.map +1 -0
- package/dist/engine/eta.js +54 -0
- package/dist/engine/eta.js.map +1 -0
- package/dist/engine/model.d.ts +57 -0
- package/dist/engine/model.d.ts.map +1 -0
- package/dist/engine/model.js +445 -0
- package/dist/engine/model.js.map +1 -0
- package/dist/engine/personas.d.ts +30 -0
- package/dist/engine/personas.d.ts.map +1 -0
- package/dist/engine/personas.js +336 -0
- package/dist/engine/personas.js.map +1 -0
- package/dist/engine/pipeline.d.ts +61 -0
- package/dist/engine/pipeline.d.ts.map +1 -0
- package/dist/engine/pipeline.js +638 -0
- package/dist/engine/pipeline.js.map +1 -0
- package/dist/engine/prompts.d.ts +10 -0
- package/dist/engine/prompts.d.ts.map +1 -0
- package/dist/engine/prompts.js +49 -0
- package/dist/engine/prompts.js.map +1 -0
- package/dist/engine/search.d.ts +46 -0
- package/dist/engine/search.d.ts.map +1 -0
- package/dist/engine/search.js +159 -0
- package/dist/engine/search.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +648 -0
- package/dist/index.js.map +1 -0
- package/dist/security.d.ts +18 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +168 -0
- package/dist/security.js.map +1 -0
- package/dist/types.d.ts +143 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/agent-mode.d.ts +8 -0
- package/dist/ui/agent-mode.d.ts.map +1 -0
- package/dist/ui/agent-mode.js +138 -0
- package/dist/ui/agent-mode.js.map +1 -0
- package/dist/ui/animations.d.ts +8 -0
- package/dist/ui/animations.d.ts.map +1 -0
- package/dist/ui/animations.js +19 -0
- package/dist/ui/animations.js.map +1 -0
- package/dist/ui/components/agent-list.d.ts +2 -0
- package/dist/ui/components/agent-list.d.ts.map +1 -0
- package/dist/ui/components/agent-list.js +2 -0
- package/dist/ui/components/agent-list.js.map +1 -0
- package/dist/ui/components/header.d.ts +2 -0
- package/dist/ui/components/header.d.ts.map +1 -0
- package/dist/ui/components/header.js +2 -0
- package/dist/ui/components/header.js.map +1 -0
- package/dist/ui/components/phase-bar.d.ts +2 -0
- package/dist/ui/components/phase-bar.d.ts.map +1 -0
- package/dist/ui/components/phase-bar.js +2 -0
- package/dist/ui/components/phase-bar.js.map +1 -0
- package/dist/ui/components/stats-bar.d.ts +2 -0
- package/dist/ui/components/stats-bar.d.ts.map +1 -0
- package/dist/ui/components/stats-bar.js +2 -0
- package/dist/ui/components/stats-bar.js.map +1 -0
- package/dist/ui/tui.d.ts +18 -0
- package/dist/ui/tui.d.ts.map +1 -0
- package/dist/ui/tui.js +464 -0
- package/dist/ui/tui.js.map +1 -0
- package/dist/web/app.html +1352 -0
- package/dist/web/index.d.ts +2 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +2 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +864 -0
- package/dist/web/server.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { addTokenUsage, completeAgentRun, createAgentRun, createRun, markRunComplete, markRunFailed, updateAgentRun, updateRunStatus, } from "../db/queries.js";
|
|
3
|
+
import { formatErrorMessage, sanitizeForTerminal } from "../security.js";
|
|
4
|
+
import { runWithConcurrency } from "./concurrency.js";
|
|
5
|
+
import { runModelWithOptionalTools } from "./model.js";
|
|
6
|
+
import { allPersonas, generateEphemeralPersonas, loadCustomPersonas, } from "./personas.js";
|
|
7
|
+
import { DEBATE_PROMPT, ORCHESTRATOR_PROMPT, RESEARCH_PROMPT, SYNTHESIS_PROMPT, } from "./prompts.js";
|
|
8
|
+
const DEFAULT_DEPENDENCIES = {
|
|
9
|
+
runModel: runModelWithOptionalTools,
|
|
10
|
+
runWithConcurrency,
|
|
11
|
+
personas: () => allPersonas(),
|
|
12
|
+
createRun,
|
|
13
|
+
createAgentRun,
|
|
14
|
+
completeAgentRun,
|
|
15
|
+
updateAgentRun,
|
|
16
|
+
updateRunStatus,
|
|
17
|
+
markRunComplete,
|
|
18
|
+
markRunFailed,
|
|
19
|
+
addTokenUsage,
|
|
20
|
+
};
|
|
21
|
+
const MAX_DEBATE_CONTEXT_CHARS = 3200;
|
|
22
|
+
const MAX_PERSISTED_ERROR_CHARS = 200;
|
|
23
|
+
function createPersistedErrorSummary(error, fallback) {
|
|
24
|
+
const sanitized = formatErrorMessage(error).replace(/\s+/g, " ").trim();
|
|
25
|
+
const summary = sanitized || fallback;
|
|
26
|
+
return summary.length <= MAX_PERSISTED_ERROR_CHARS
|
|
27
|
+
? summary
|
|
28
|
+
: `${summary.slice(0, MAX_PERSISTED_ERROR_CHARS - 1)}…`;
|
|
29
|
+
}
|
|
30
|
+
function logProcessError(context, error) {
|
|
31
|
+
const sanitizedContext = sanitizeForTerminal(context);
|
|
32
|
+
const sanitizedError = formatErrorMessage(error);
|
|
33
|
+
console.error(`[hydra] ${sanitizedContext} ${sanitizedError}`.trim());
|
|
34
|
+
}
|
|
35
|
+
/** orchestrates a full hydra run across decomposition, research, debate, and synthesis. */
|
|
36
|
+
export class HydraPipeline extends EventEmitter {
|
|
37
|
+
#config;
|
|
38
|
+
#deps;
|
|
39
|
+
#orchestratorModel;
|
|
40
|
+
#researchModel;
|
|
41
|
+
#totalPromptTokens = 0;
|
|
42
|
+
#totalCompletionTokens = 0;
|
|
43
|
+
/** initialize pipeline with validated runtime configuration. */
|
|
44
|
+
constructor(config, dependencies = {}) {
|
|
45
|
+
super();
|
|
46
|
+
this.#config = config;
|
|
47
|
+
this.#orchestratorModel = config.orchestratorModel ?? config.model;
|
|
48
|
+
this.#researchModel = config.researchModel ?? config.model;
|
|
49
|
+
this.#deps = {
|
|
50
|
+
...DEFAULT_DEPENDENCIES,
|
|
51
|
+
...dependencies,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** execute the full pipeline for a user query and return run metadata. */
|
|
55
|
+
async run(query) {
|
|
56
|
+
this.#totalPromptTokens = 0;
|
|
57
|
+
this.#totalCompletionTokens = 0;
|
|
58
|
+
const run = this.#deps.createRun({
|
|
59
|
+
query,
|
|
60
|
+
agentCount: this.#config.agentCount,
|
|
61
|
+
status: "decomposing",
|
|
62
|
+
pipelineState: JSON.stringify({
|
|
63
|
+
apiKeyConfigured: Boolean(this.#config.apiKey),
|
|
64
|
+
searchProvider: this.#config.searchConfig.provider,
|
|
65
|
+
concurrency: this.#config.maxConcurrency,
|
|
66
|
+
debateRounds: this.#config.debateRounds,
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
const createdAt = Date.now();
|
|
70
|
+
this.emit("run-created", {
|
|
71
|
+
type: "run-created",
|
|
72
|
+
runId: run.id,
|
|
73
|
+
query: run.query,
|
|
74
|
+
agentCount: run.agentCount,
|
|
75
|
+
timestamp: createdAt,
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
let personas = this.resolvePersonas();
|
|
79
|
+
let agentCount = this.#config.agentCount;
|
|
80
|
+
if (this.#config.customPersonasOnly) {
|
|
81
|
+
const customPersonas = loadCustomPersonas();
|
|
82
|
+
if (customPersonas.length < agentCount) {
|
|
83
|
+
const gap = agentCount - customPersonas.length;
|
|
84
|
+
const generatedPersonas = await generateEphemeralPersonas(query, gap, async (systemPrompt, userPrompt) => {
|
|
85
|
+
const result = await this.runModel({
|
|
86
|
+
runId: run.id,
|
|
87
|
+
model: this.#orchestratorModel,
|
|
88
|
+
systemPrompt,
|
|
89
|
+
userPrompt,
|
|
90
|
+
allowTools: false,
|
|
91
|
+
});
|
|
92
|
+
return result.output;
|
|
93
|
+
});
|
|
94
|
+
console.error(`[hydra] generated ${gap} ephemeral persona(s) to fill agent count`);
|
|
95
|
+
personas = [...customPersonas, ...generatedPersonas];
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
personas = customPersonas.slice(0, agentCount);
|
|
99
|
+
}
|
|
100
|
+
if (personas.length < 1) {
|
|
101
|
+
throw new Error("custom-personas-only mode requires at least 1 persona; define custom personas with `hydra persona add` or increase agent count");
|
|
102
|
+
}
|
|
103
|
+
if (personas.length < agentCount) {
|
|
104
|
+
console.error(`[hydra] persona pool has ${personas.length} persona(s); clamping agent count from ${agentCount} to ${personas.length}`);
|
|
105
|
+
agentCount = personas.length;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const decomposedAssignments = await this.decompose(query, run.id, personas, agentCount);
|
|
109
|
+
const selectedPersonas = decomposedAssignments.map(({ persona }) => persona);
|
|
110
|
+
this.setStatus(run.id, "researching");
|
|
111
|
+
const researchOutputs = await this.runResearchPhase(run.id, decomposedAssignments);
|
|
112
|
+
const debateSeedOutputs = researchOutputs.filter((item) => typeof item.output === "string" && item.output.trim().length > 0);
|
|
113
|
+
const excludedResearchCount = researchOutputs.length - debateSeedOutputs.length;
|
|
114
|
+
if (excludedResearchCount > 0) {
|
|
115
|
+
console.warn(`[warn] ${excludedResearchCount} research agents returned empty output, excluding from debate`);
|
|
116
|
+
}
|
|
117
|
+
this.setStatus(run.id, "debating");
|
|
118
|
+
const debateOutputs = await this.runDebateRounds(run.id, query, selectedPersonas, debateSeedOutputs);
|
|
119
|
+
this.setStatus(run.id, "synthesizing");
|
|
120
|
+
const brief = await this.synthesize(run.id, query, selectedPersonas, researchOutputs, debateOutputs);
|
|
121
|
+
const completedRun = this.#deps.markRunComplete(run.id, brief);
|
|
122
|
+
this.emit("run-complete", {
|
|
123
|
+
type: "run-complete",
|
|
124
|
+
runId: completedRun.id,
|
|
125
|
+
elapsedMs: completedRun.elapsedMs ?? Date.now() - createdAt,
|
|
126
|
+
totalPromptTokens: completedRun.totalPromptTokens,
|
|
127
|
+
totalCompletionTokens: completedRun.totalCompletionTokens,
|
|
128
|
+
timestamp: Date.now(),
|
|
129
|
+
});
|
|
130
|
+
return { runId: completedRun.id, brief };
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
const sanitizedSummary = createPersistedErrorSummary(error, "pipeline failed");
|
|
134
|
+
logProcessError(`pipeline run failed for ${run.id}:`, error);
|
|
135
|
+
const failedRun = this.#deps.markRunFailed(run.id, sanitizedSummary);
|
|
136
|
+
this.emit("run-status-changed", {
|
|
137
|
+
type: "run-status-changed",
|
|
138
|
+
runId: failedRun.id,
|
|
139
|
+
status: failedRun.status,
|
|
140
|
+
timestamp: Date.now(),
|
|
141
|
+
});
|
|
142
|
+
if (this.listenerCount("error") > 0) {
|
|
143
|
+
this.emit("error", error);
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async decompose(query, runId, personas, agentCount = this.#config.agentCount) {
|
|
149
|
+
const personaLines = personas
|
|
150
|
+
.map((persona) => `- ${persona.name}: ${persona.description}`)
|
|
151
|
+
.join("\n");
|
|
152
|
+
const decomposePrompt = [
|
|
153
|
+
`You are an orchestrator choosing ${agentCount} specialists.`,
|
|
154
|
+
"Choose the most relevant personas for this query.",
|
|
155
|
+
`Available personas:\n${personaLines}`,
|
|
156
|
+
`Query: ${query}`,
|
|
157
|
+
].join("\n");
|
|
158
|
+
const result = await this.runModel({
|
|
159
|
+
runId,
|
|
160
|
+
model: this.#orchestratorModel,
|
|
161
|
+
systemPrompt: ORCHESTRATOR_PROMPT,
|
|
162
|
+
userPrompt: decomposePrompt,
|
|
163
|
+
allowTools: false,
|
|
164
|
+
});
|
|
165
|
+
const parsedAssignments = this.parseAssignments(result.output);
|
|
166
|
+
const resolvedAssignments = this.normalizeAssignments(parsedAssignments, personas, query, agentCount);
|
|
167
|
+
const usedPersonas = new Set();
|
|
168
|
+
return resolvedAssignments.map((assignment) => ({
|
|
169
|
+
assignment,
|
|
170
|
+
persona: this.resolvePersona(assignment, personas, usedPersonas),
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
async runResearchPhase(runId, assignments) {
|
|
174
|
+
const phase = "research";
|
|
175
|
+
const workItems = assignments.map(({ assignment, persona }) => {
|
|
176
|
+
const agentRun = this.#deps.createAgentRun({
|
|
177
|
+
runId,
|
|
178
|
+
phase,
|
|
179
|
+
persona: persona.name,
|
|
180
|
+
status: "queued",
|
|
181
|
+
systemPrompt: RESEARCH_PROMPT(persona),
|
|
182
|
+
});
|
|
183
|
+
return {
|
|
184
|
+
assignment,
|
|
185
|
+
persona,
|
|
186
|
+
agentRun,
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
let completedAgents = 0;
|
|
190
|
+
const totalAgents = workItems.length;
|
|
191
|
+
const results = await this.#deps.runWithConcurrency(workItems, this.#config.maxConcurrency, async (item) => {
|
|
192
|
+
try {
|
|
193
|
+
let hasStarted = false;
|
|
194
|
+
const markStarted = (executionStartedAt) => {
|
|
195
|
+
if (hasStarted) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
hasStarted = true;
|
|
199
|
+
this.#deps.updateAgentRun(item.agentRun.id, {
|
|
200
|
+
status: "running",
|
|
201
|
+
startedAt: executionStartedAt,
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
const result = await this.runModel({
|
|
205
|
+
runId,
|
|
206
|
+
model: this.#researchModel,
|
|
207
|
+
systemPrompt: RESEARCH_PROMPT(item.persona),
|
|
208
|
+
userPrompt: this.formatCodeBlock(item.assignment.subQuestion),
|
|
209
|
+
allowTools: this.#config.searchEnabled,
|
|
210
|
+
maxToolCalls: 5,
|
|
211
|
+
onExecutionStart: markStarted,
|
|
212
|
+
});
|
|
213
|
+
const completed = this.#deps.completeAgentRun(item.agentRun.id, result.output, {
|
|
214
|
+
status: "complete",
|
|
215
|
+
searchQueries: result.searchQueries,
|
|
216
|
+
promptTokens: result.promptTokens,
|
|
217
|
+
completionTokens: result.completionTokens,
|
|
218
|
+
});
|
|
219
|
+
const state = this.toAgentState(completed);
|
|
220
|
+
const event = {
|
|
221
|
+
type: "agent-complete",
|
|
222
|
+
runId,
|
|
223
|
+
agentRunId: completed.id,
|
|
224
|
+
persona: item.persona.name,
|
|
225
|
+
phase,
|
|
226
|
+
state,
|
|
227
|
+
timestamp: Date.now(),
|
|
228
|
+
};
|
|
229
|
+
this.emit("agent-complete", event);
|
|
230
|
+
completedAgents += 1;
|
|
231
|
+
this.emit("agent-progress", {
|
|
232
|
+
type: "agent-progress",
|
|
233
|
+
runId,
|
|
234
|
+
phase,
|
|
235
|
+
completedAgents,
|
|
236
|
+
totalAgents,
|
|
237
|
+
timestamp: Date.now(),
|
|
238
|
+
});
|
|
239
|
+
return {
|
|
240
|
+
persona: item.persona,
|
|
241
|
+
output: result.output,
|
|
242
|
+
searchQueries: result.searchQueries,
|
|
243
|
+
status: "complete",
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
const sanitizedSummary = createPersistedErrorSummary(error, "research agent failed");
|
|
248
|
+
logProcessError(`research agent failed for ${item.persona.name}:`, error);
|
|
249
|
+
const completed = this.#deps.completeAgentRun(item.agentRun.id, sanitizedSummary, {
|
|
250
|
+
status: "error",
|
|
251
|
+
});
|
|
252
|
+
const state = this.toAgentState(completed);
|
|
253
|
+
const event = {
|
|
254
|
+
type: "agent-complete",
|
|
255
|
+
runId,
|
|
256
|
+
agentRunId: completed.id,
|
|
257
|
+
persona: item.persona.name,
|
|
258
|
+
phase,
|
|
259
|
+
state,
|
|
260
|
+
timestamp: Date.now(),
|
|
261
|
+
};
|
|
262
|
+
this.emit("agent-complete", event);
|
|
263
|
+
completedAgents += 1;
|
|
264
|
+
this.emit("agent-progress", {
|
|
265
|
+
type: "agent-progress",
|
|
266
|
+
runId,
|
|
267
|
+
phase,
|
|
268
|
+
completedAgents,
|
|
269
|
+
totalAgents,
|
|
270
|
+
timestamp: Date.now(),
|
|
271
|
+
});
|
|
272
|
+
return {
|
|
273
|
+
persona: item.persona,
|
|
274
|
+
output: "",
|
|
275
|
+
searchQueries: [],
|
|
276
|
+
status: "error",
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
const successfulOutputs = results.filter((item) => item.status === "complete" &&
|
|
281
|
+
typeof item.output === "string" &&
|
|
282
|
+
item.output.trim().length > 0);
|
|
283
|
+
if (successfulOutputs.length === 0) {
|
|
284
|
+
throw new Error(`research phase failed: ${successfulOutputs.length}/${totalAgents} agents succeeded`);
|
|
285
|
+
}
|
|
286
|
+
return results;
|
|
287
|
+
}
|
|
288
|
+
async runDebateRounds(runId, query, personas, startingOutputs) {
|
|
289
|
+
let currentOutputs = [...startingOutputs];
|
|
290
|
+
const rounds = Math.max(1, this.#config.debateRounds);
|
|
291
|
+
for (let round = 1; round <= rounds; round++) {
|
|
292
|
+
currentOutputs = await this.runDebateRound(runId, query, personas, currentOutputs, round);
|
|
293
|
+
}
|
|
294
|
+
return currentOutputs;
|
|
295
|
+
}
|
|
296
|
+
async runDebateRound(runId, query, personas, previousOutputs, round) {
|
|
297
|
+
const phase = "debate";
|
|
298
|
+
const workItems = personas.map((persona) => {
|
|
299
|
+
const successfulPeers = previousOutputs.filter((item) => item.persona.name !== persona.name &&
|
|
300
|
+
item.status === "complete" &&
|
|
301
|
+
item.output.trim().length > 0);
|
|
302
|
+
const fallbackPeers = previousOutputs.filter((item) => item.persona.name !== persona.name);
|
|
303
|
+
const selectedPeers = [
|
|
304
|
+
...successfulPeers.slice(0, 2),
|
|
305
|
+
...fallbackPeers
|
|
306
|
+
.filter((item) => successfulPeers.indexOf(item) === -1)
|
|
307
|
+
.slice(0, Math.max(0, 2 - successfulPeers.length)),
|
|
308
|
+
].slice(0, 2);
|
|
309
|
+
const agentRun = this.#deps.createAgentRun({
|
|
310
|
+
runId,
|
|
311
|
+
phase,
|
|
312
|
+
persona: persona.name,
|
|
313
|
+
status: "queued",
|
|
314
|
+
systemPrompt: DEBATE_PROMPT(persona, round),
|
|
315
|
+
});
|
|
316
|
+
const priorFinding = previousOutputs.find((item) => item.persona.name === persona.name);
|
|
317
|
+
const assignmentMessage = this.buildDebatePrompt(query, persona.name, priorFinding?.output ?? "", selectedPeers, round);
|
|
318
|
+
return {
|
|
319
|
+
persona,
|
|
320
|
+
agentRun,
|
|
321
|
+
prompt: DEBATE_PROMPT(persona, round),
|
|
322
|
+
assignmentMessage,
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
let completedAgents = 0;
|
|
326
|
+
const totalAgents = workItems.length;
|
|
327
|
+
const results = await this.#deps.runWithConcurrency(workItems, this.#config.maxConcurrency, async (item) => {
|
|
328
|
+
try {
|
|
329
|
+
let hasStarted = false;
|
|
330
|
+
const markStarted = (executionStartedAt) => {
|
|
331
|
+
if (hasStarted) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
hasStarted = true;
|
|
335
|
+
this.#deps.updateAgentRun(item.agentRun.id, {
|
|
336
|
+
status: "running",
|
|
337
|
+
startedAt: executionStartedAt,
|
|
338
|
+
});
|
|
339
|
+
};
|
|
340
|
+
const result = await this.runModel({
|
|
341
|
+
runId,
|
|
342
|
+
model: this.#researchModel,
|
|
343
|
+
systemPrompt: item.prompt,
|
|
344
|
+
userPrompt: item.assignmentMessage,
|
|
345
|
+
allowTools: false,
|
|
346
|
+
onExecutionStart: markStarted,
|
|
347
|
+
});
|
|
348
|
+
const completed = this.#deps.completeAgentRun(item.agentRun.id, result.output, {
|
|
349
|
+
status: "complete",
|
|
350
|
+
searchQueries: result.searchQueries,
|
|
351
|
+
promptTokens: result.promptTokens,
|
|
352
|
+
completionTokens: result.completionTokens,
|
|
353
|
+
});
|
|
354
|
+
const state = this.toAgentState(completed);
|
|
355
|
+
this.emit("agent-complete", {
|
|
356
|
+
type: "agent-complete",
|
|
357
|
+
runId,
|
|
358
|
+
agentRunId: completed.id,
|
|
359
|
+
persona: item.persona.name,
|
|
360
|
+
phase,
|
|
361
|
+
state,
|
|
362
|
+
timestamp: Date.now(),
|
|
363
|
+
});
|
|
364
|
+
completedAgents += 1;
|
|
365
|
+
this.emit("agent-progress", {
|
|
366
|
+
type: "agent-progress",
|
|
367
|
+
runId,
|
|
368
|
+
phase,
|
|
369
|
+
completedAgents,
|
|
370
|
+
totalAgents,
|
|
371
|
+
timestamp: Date.now(),
|
|
372
|
+
});
|
|
373
|
+
return {
|
|
374
|
+
persona: item.persona,
|
|
375
|
+
output: result.output,
|
|
376
|
+
searchQueries: result.searchQueries,
|
|
377
|
+
status: "complete",
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
const sanitizedSummary = createPersistedErrorSummary(error, "debate agent failed");
|
|
382
|
+
logProcessError(`debate agent failed for ${item.persona.name}:`, error);
|
|
383
|
+
const completed = this.#deps.completeAgentRun(item.agentRun.id, sanitizedSummary, {
|
|
384
|
+
status: "error",
|
|
385
|
+
});
|
|
386
|
+
const state = this.toAgentState(completed);
|
|
387
|
+
this.emit("agent-complete", {
|
|
388
|
+
type: "agent-complete",
|
|
389
|
+
runId,
|
|
390
|
+
agentRunId: completed.id,
|
|
391
|
+
persona: item.persona.name,
|
|
392
|
+
phase,
|
|
393
|
+
state,
|
|
394
|
+
timestamp: Date.now(),
|
|
395
|
+
});
|
|
396
|
+
completedAgents += 1;
|
|
397
|
+
this.emit("agent-progress", {
|
|
398
|
+
type: "agent-progress",
|
|
399
|
+
runId,
|
|
400
|
+
phase,
|
|
401
|
+
completedAgents,
|
|
402
|
+
totalAgents,
|
|
403
|
+
timestamp: Date.now(),
|
|
404
|
+
});
|
|
405
|
+
return {
|
|
406
|
+
persona: item.persona,
|
|
407
|
+
output: "",
|
|
408
|
+
searchQueries: [],
|
|
409
|
+
status: "error",
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
const successfulOutputs = results.filter((item) => item.status === "complete" &&
|
|
414
|
+
typeof item.output === "string" &&
|
|
415
|
+
item.output.trim().length > 0);
|
|
416
|
+
const requiredSuccessfulOutputs = Math.min(2, totalAgents);
|
|
417
|
+
if (successfulOutputs.length < requiredSuccessfulOutputs) {
|
|
418
|
+
throw new Error(`debate round ${round} failed: ${successfulOutputs.length}/${totalAgents} agents succeeded`);
|
|
419
|
+
}
|
|
420
|
+
return successfulOutputs;
|
|
421
|
+
}
|
|
422
|
+
async synthesize(runId, query, personas, researchOutputs, debateOutputs) {
|
|
423
|
+
const formattedResearch = this.formatPersonaOutputs("research", researchOutputs);
|
|
424
|
+
const formattedDebate = this.formatPersonaOutputs("debate", debateOutputs);
|
|
425
|
+
const userPrompt = [
|
|
426
|
+
`Original query:\n${this.formatCodeBlock(query)}`,
|
|
427
|
+
`Selected personas:\n${personas.map((persona) => `- ${persona.name}`).join("\n")}`,
|
|
428
|
+
formattedResearch,
|
|
429
|
+
formattedDebate,
|
|
430
|
+
].join("\n\n");
|
|
431
|
+
const result = await this.runModel({
|
|
432
|
+
runId,
|
|
433
|
+
model: this.#orchestratorModel,
|
|
434
|
+
systemPrompt: SYNTHESIS_PROMPT,
|
|
435
|
+
userPrompt,
|
|
436
|
+
allowTools: false,
|
|
437
|
+
});
|
|
438
|
+
return result.output.trim();
|
|
439
|
+
}
|
|
440
|
+
async runModel(input) {
|
|
441
|
+
const result = await this.#deps.runModel({
|
|
442
|
+
apiKey: this.#config.apiKey,
|
|
443
|
+
baseUrl: this.#config.baseUrl,
|
|
444
|
+
model: input.model,
|
|
445
|
+
searchConfig: this.#config.searchConfig,
|
|
446
|
+
systemPrompt: input.systemPrompt,
|
|
447
|
+
userPrompt: input.userPrompt,
|
|
448
|
+
allowTools: input.allowTools,
|
|
449
|
+
maxToolCalls: input.maxToolCalls,
|
|
450
|
+
onExecutionStart: input.onExecutionStart,
|
|
451
|
+
});
|
|
452
|
+
const promptTokens = Number.isFinite(result.promptTokens)
|
|
453
|
+
? result.promptTokens
|
|
454
|
+
: 0;
|
|
455
|
+
const completionTokens = Number.isFinite(result.completionTokens)
|
|
456
|
+
? result.completionTokens
|
|
457
|
+
: 0;
|
|
458
|
+
this.#totalPromptTokens += promptTokens;
|
|
459
|
+
this.#totalCompletionTokens += completionTokens;
|
|
460
|
+
this.#deps.addTokenUsage(input.runId, promptTokens, completionTokens);
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
setStatus(runId, status) {
|
|
464
|
+
this.#deps.updateRunStatus(runId, { status });
|
|
465
|
+
this.emit("run-status-changed", {
|
|
466
|
+
type: "run-status-changed",
|
|
467
|
+
runId,
|
|
468
|
+
status,
|
|
469
|
+
timestamp: Date.now(),
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
normalizeAssignments(assignments, personas, query, targetCount = this.#config.agentCount) {
|
|
473
|
+
const usedPersonas = new Set();
|
|
474
|
+
const availablePersonas = personas.filter((persona) => persona.name.trim().length > 0);
|
|
475
|
+
const personaByName = new Map(availablePersonas.map((persona) => [persona.name.toLowerCase(), persona]));
|
|
476
|
+
const deduplicated = [];
|
|
477
|
+
for (const assignment of assignments) {
|
|
478
|
+
const candidateName = assignment.persona.trim();
|
|
479
|
+
const persona = personaByName.get(candidateName.toLowerCase());
|
|
480
|
+
if (!candidateName ||
|
|
481
|
+
!persona ||
|
|
482
|
+
usedPersonas.has(persona.name.toLowerCase())) {
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
usedPersonas.add(persona.name.toLowerCase());
|
|
486
|
+
deduplicated.push({
|
|
487
|
+
...assignment,
|
|
488
|
+
persona: persona.name,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
const remainingPersonas = availablePersonas.filter((persona) => !usedPersonas.has(persona.name.toLowerCase()));
|
|
492
|
+
if (deduplicated.length < targetCount) {
|
|
493
|
+
console.warn(`[hydra] orchestrator returned ${assignments.length} assignments; expected ${targetCount}. Filling missing ones from selected personas.`);
|
|
494
|
+
const shuffledRemaining = [...remainingPersonas];
|
|
495
|
+
for (let i = shuffledRemaining.length - 1; i > 0; i--) {
|
|
496
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
497
|
+
[shuffledRemaining[i], shuffledRemaining[j]] = [
|
|
498
|
+
shuffledRemaining[j],
|
|
499
|
+
shuffledRemaining[i],
|
|
500
|
+
];
|
|
501
|
+
}
|
|
502
|
+
for (let index = deduplicated.length; index < targetCount; index++) {
|
|
503
|
+
const replacementPersona = shuffledRemaining.shift();
|
|
504
|
+
if (!replacementPersona) {
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
usedPersonas.add(replacementPersona.name.toLowerCase());
|
|
508
|
+
deduplicated.push({
|
|
509
|
+
persona: replacementPersona.name,
|
|
510
|
+
subQuestion: query,
|
|
511
|
+
methodology: replacementPersona.methodology,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (deduplicated.length > targetCount) {
|
|
516
|
+
console.warn(`[hydra] orchestrator returned ${deduplicated.length} assignments; expected ${targetCount}. Truncating extras.`);
|
|
517
|
+
return deduplicated.slice(0, targetCount);
|
|
518
|
+
}
|
|
519
|
+
return deduplicated;
|
|
520
|
+
}
|
|
521
|
+
parseAssignments(raw) {
|
|
522
|
+
const trimmed = raw.trim();
|
|
523
|
+
if (!trimmed) {
|
|
524
|
+
return [];
|
|
525
|
+
}
|
|
526
|
+
const fromFence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
527
|
+
const candidate = fromFence?.[1]?.trim() || this.extractBracketPayload(trimmed);
|
|
528
|
+
if (!candidate) {
|
|
529
|
+
return [];
|
|
530
|
+
}
|
|
531
|
+
let parsed;
|
|
532
|
+
try {
|
|
533
|
+
parsed = JSON.parse(candidate);
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
return [];
|
|
537
|
+
}
|
|
538
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
539
|
+
return [];
|
|
540
|
+
}
|
|
541
|
+
const assignments = parsed.map((item) => {
|
|
542
|
+
if (!item || typeof item !== "object") {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
const maybe = item;
|
|
546
|
+
if (typeof maybe.persona !== "string" ||
|
|
547
|
+
typeof maybe.subQuestion !== "string" ||
|
|
548
|
+
typeof maybe.methodology !== "string") {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
persona: maybe.persona.trim(),
|
|
553
|
+
subQuestion: maybe.subQuestion.trim(),
|
|
554
|
+
methodology: maybe.methodology.trim(),
|
|
555
|
+
};
|
|
556
|
+
});
|
|
557
|
+
const discardedAssignments = assignments.length - assignments.filter(Boolean).length;
|
|
558
|
+
if (discardedAssignments > 0) {
|
|
559
|
+
console.warn(`[hydra] discarded ${discardedAssignments} invalid decomposition assignment(s)`);
|
|
560
|
+
}
|
|
561
|
+
return assignments.filter((assignment) => assignment !== null);
|
|
562
|
+
}
|
|
563
|
+
extractBracketPayload(raw) {
|
|
564
|
+
const start = raw.indexOf("[");
|
|
565
|
+
if (start === -1) {
|
|
566
|
+
return "";
|
|
567
|
+
}
|
|
568
|
+
const end = raw.lastIndexOf("]");
|
|
569
|
+
if (end <= start) {
|
|
570
|
+
return "";
|
|
571
|
+
}
|
|
572
|
+
return raw.slice(start, end + 1).trim();
|
|
573
|
+
}
|
|
574
|
+
resolvePersona(assignment, personas, usedPersonas) {
|
|
575
|
+
const assignedName = assignment.persona.trim();
|
|
576
|
+
const byName = personas.find((persona) => persona.name.toLowerCase() === assignedName.toLowerCase() &&
|
|
577
|
+
!usedPersonas.has(persona.name.toLowerCase()));
|
|
578
|
+
if (byName) {
|
|
579
|
+
usedPersonas.add(byName.name.toLowerCase());
|
|
580
|
+
return byName;
|
|
581
|
+
}
|
|
582
|
+
const fallback = personas.find((persona) => !usedPersonas.has(persona.name.toLowerCase()));
|
|
583
|
+
if (!fallback) {
|
|
584
|
+
return personas[0];
|
|
585
|
+
}
|
|
586
|
+
usedPersonas.add(fallback.name.toLowerCase());
|
|
587
|
+
return fallback;
|
|
588
|
+
}
|
|
589
|
+
formatCodeBlock(text) {
|
|
590
|
+
return `\`\`\`text\n${text}\n\`\`\``;
|
|
591
|
+
}
|
|
592
|
+
buildDebatePrompt(query, personaName, ownFinding, peers, round) {
|
|
593
|
+
const ownFindingForPrompt = this.trimDebateContext(ownFinding || "No finding produced.", round);
|
|
594
|
+
const peerLines = peers.length === 0
|
|
595
|
+
? ["No peer findings available."]
|
|
596
|
+
: peers.map((peer) => `${peer.persona.name}:\n${this.formatCodeBlock(this.trimDebateContext(peer.output || "No output.", round))}`);
|
|
597
|
+
return [
|
|
598
|
+
`Original query:\n${this.formatCodeBlock(query)}`,
|
|
599
|
+
`Your finding:\n${this.formatCodeBlock(ownFindingForPrompt)}`,
|
|
600
|
+
`Persona: ${personaName}`,
|
|
601
|
+
`Peer findings:\n${peerLines.join("\n\n")}`,
|
|
602
|
+
"Update your thesis with direct contrasts and revised confidence.",
|
|
603
|
+
].join("\n\n");
|
|
604
|
+
}
|
|
605
|
+
trimDebateContext(text, round) {
|
|
606
|
+
if (round <= 1 || text.length <= MAX_DEBATE_CONTEXT_CHARS) {
|
|
607
|
+
return text;
|
|
608
|
+
}
|
|
609
|
+
return `${text.slice(0, MAX_DEBATE_CONTEXT_CHARS)}\n\n[truncated for context window]`;
|
|
610
|
+
}
|
|
611
|
+
formatPersonaOutputs(label, outputs) {
|
|
612
|
+
if (outputs.length === 0) {
|
|
613
|
+
return `${label.toUpperCase()} OUTPUTS:\nNo outputs.`;
|
|
614
|
+
}
|
|
615
|
+
return `${label.toUpperCase()} OUTPUTS:\n${outputs
|
|
616
|
+
.map((output) => `- ${output.persona.name} (${output.status}):\n${this.formatCodeBlock(output.output || "No output.")}`)
|
|
617
|
+
.join("\n\n")}`;
|
|
618
|
+
}
|
|
619
|
+
toAgentState(record) {
|
|
620
|
+
return {
|
|
621
|
+
runId: record.runId,
|
|
622
|
+
phase: record.phase,
|
|
623
|
+
persona: record.persona,
|
|
624
|
+
status: record.status,
|
|
625
|
+
startedAt: record.startedAt,
|
|
626
|
+
completedAt: record.completedAt,
|
|
627
|
+
promptTokens: record.promptTokens,
|
|
628
|
+
completionTokens: record.completionTokens,
|
|
629
|
+
output: record.output,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
resolvePersonas() {
|
|
633
|
+
return typeof this.#deps.personas === "function"
|
|
634
|
+
? this.#deps.personas()
|
|
635
|
+
: this.#deps.personas;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
//# sourceMappingURL=pipeline.js.map
|