feat(workflow): port 3d workflow + hive-mind engine — parse, execute, cycle, synthesis, docs
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
> Plan lengkap migrasi in-place dari Rust (11 crate, clean architecture 4 lapis) ke monorepo
|
||||
> TypeScript/Bun. Bahasa Indonesia, commit pakai Conventional Commits.
|
||||
>
|
||||
> **Status:** Fase 0–2 + 3a + 3b + 3e + 3c **selesai**. Berikutnya: **3d workflow + hive-mind**.
|
||||
> **Status:** Fase 0–2 + 3a + 3b + 3e + 3c + 3d **selesai**. Berikutnya: **3f auth infrastructure**.
|
||||
> Base: `bun run check` bersih, 54 test hijau.
|
||||
|
||||
---
|
||||
@@ -95,12 +95,8 @@ apps/
|
||||
**Sumber Rust:** `apps/infrastructure/src/subagent/{engine,division,context,provider,spawn}.rs`, `subagent/auto/*`
|
||||
|
||||
- [x] `AccessTier.toolsFor(access)` — `tools/division.ts`: Read / Write / Full yang mem-filter registry
|
||||
- Read → tool baca-saja
|
||||
- Write → baca + tulis
|
||||
- Full → semua
|
||||
- [x] `run_agent` loop (`tools/engine.ts`): `MAX_ITERATIONS=25`, `TOOL_OUTPUT_MAX_CHARS=12_000`,
|
||||
`MAX_CONSECUTIVE_TOOL_ERRORS=3`, `MAX_PARALLEL_TOOLS=8`; recovery note setelah 3 error beruntun;
|
||||
return pesan limit iterasai saat keluar alami
|
||||
`MAX_CONSECUTIVE_TOOL_ERRORS=3`, `MAX_PARALLEL_TOOLS=8`; recovery note setelah 3 error beruntun
|
||||
- [x] `SubagentContext` (`subagent/context.ts`) — directive, toolCtx, access, baseURL, apiKey, model
|
||||
- [x] `spawn_subagent` — `spawn_tools.ts` — runAgent berbasis HTTP provider + config_resolver
|
||||
- [x] `resolve_subagent_provider` (`subagent/provider.ts`) — pilih provider/model subagent dari settings + app_config
|
||||
@@ -111,21 +107,22 @@ apps/
|
||||
wiring `review_enabled` (yang sekarang no-op di `Review` agent)
|
||||
- [x] Placeholder di `apps/packages/infrastructure/src/subagent/*` diisi penuh
|
||||
|
||||
---
|
||||
|
||||
## ⬜ Yang belum dikerjakan
|
||||
|
||||
### Fase 3d — Workflow + hive-mind
|
||||
**Sumber Rust:** `apps/infrastructure/src/workflow/{script,mod,docs}.rs`, `workflow/engine/{execution,phases,primitives}.rs`, `workflow/hive_mind/{complexity,cycle,synthesis,live,types}.rs`
|
||||
|
||||
- [ ] `parse_workflow_script(yaml)` — parser YAML workflow (`script.ts`)
|
||||
- [ ] `execute_workflow(script, ctx)` — eksekusi multi-phase (`engine/execution.ts`)
|
||||
- [ ] Phase primitives: run sub-agent per node, gate/condition, paralel (bounded), I/O
|
||||
- [ ] `execute_cycle(cycle, ctx)` — hive-mind cycle, concurrency 8, kumpulkan NodeOutput
|
||||
- [ ] `synthesize_consensus(node_outputs, ctx)` — konsensus via LLM
|
||||
- [ ] `complexity_heuristic` — pilih apakah cukup 1 cycle / butuh banyak
|
||||
- [ ] `docs.ts` — tulis convergence/decision doc
|
||||
- [ ] Isi body `WorkflowRun` & `HiveMind` tool (ganti placeholder), hapus `workflow/index.ts` + `hive_mind.ts` placeholder
|
||||
- [x] `parse_workflow_script(yaml)` — parser YAML workflow (`workflow/script.ts`)
|
||||
- [x] `execute_workflow(script, ctx)` — eksekusi multi-phase (`workflow/engine.ts` + `workflow/orchestrate.ts`)
|
||||
- [x] Phase primitives: run sub-agent per node (`executePrimitive`)
|
||||
- [x] `execute_cycle(cycle, ctx)` — hive-mind cycle, concurrency 8, kumpulkan NodeOutput (`workflow/cycle.ts`)
|
||||
- [x] `synthesize_consensus(node_outputs, ctx)` — konsensus via LLM + fallback concat (`workflow/synthesis.ts`)
|
||||
- [x] `complexity_heuristic` — pilih apakah cukup 1 cycle / butuh banyak (`workflow/complexity.ts`)
|
||||
- [x] `docs.ts` — tulis convergence/decision doc (`workflow/docs.ts`)
|
||||
- [x] Isi body `WorkflowRun` & `HiveMind` tool (ganti placeholder) di `tools/workflow.ts`
|
||||
- [x] `orchestrate.ts` — top-level `executeWorkflow` + `executeHiveMind` entry points
|
||||
|
||||
---
|
||||
|
||||
## ⬜ Yang belum dikerjakan
|
||||
|
||||
### Fase 3f — Auth infrastructure
|
||||
**Sumber Rust:** `apps/infrastructure/src/auth/{password,jwt,oauth_loopback,mod}.rs`
|
||||
@@ -195,4 +192,4 @@ apps/
|
||||
- Registry tool = **38** (terhitung workflow_run, note_finding, read_findings, hive_mind; ExploreCodebase tidak diport).
|
||||
- `Tool.run` boleh return `string | Promise<string>`.
|
||||
- `spawn_agents`/`spawn_pipeline`/`parallel_delegate` sudah terisi penuh via 3c.
|
||||
- Workflow/hive-mind tools masih placeholder — diisi penuh di 3d.
|
||||
- Workflow/hive-mind tools sudah terisi penuh via 3d.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Complexity heuristics — determine whether a request is complex enough to
|
||||
* warrant hive-mind orchestration.
|
||||
* Mirrors `apps/infrastructure/src/workflow/hive_mind/complexity.rs`.
|
||||
*/
|
||||
|
||||
/** Heuristics to determine if a request is complex enough for hive-mind. */
|
||||
export function isComplexRequest(task: string): boolean {
|
||||
const complexityIndicators = [
|
||||
"refactor",
|
||||
"redesign",
|
||||
"multiple files",
|
||||
"architecture",
|
||||
"migration",
|
||||
"comprehensive",
|
||||
"end-to-end",
|
||||
"full-stack",
|
||||
];
|
||||
|
||||
const taskLower = task.toLowerCase();
|
||||
return complexityIndicators.some((indicator) => taskLower.includes(indicator));
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Hive-mind cycle execution — run one cycle of parallel nodes.
|
||||
* Mirrors `apps/infrastructure/src/workflow/hive_mind/cycle.rs`.
|
||||
*
|
||||
* Flow: resolve LLM credentials → run all directives in the cycle concurrently
|
||||
* with bounded concurrency (MAX_CONCURRENT_NODES=8) → collect NodeOutput.
|
||||
* Failed nodes are logged and replaced with [ERROR] output.
|
||||
*/
|
||||
import type { CognitiveCycle, NodeOutput } from "@zesdex/domain";
|
||||
import type { ToolCtx } from "../tools/mod.ts";
|
||||
import { runAgent } from "../tools/engine.ts";
|
||||
import { resolveConfig } from "../subagent/config_resolver.ts";
|
||||
import { buildProviderService } from "../subagent/http_provider.ts";
|
||||
|
||||
/** Maximum number of hive-mind nodes running concurrently per cycle. */
|
||||
const MAX_CONCURRENT_NODES = 8;
|
||||
|
||||
/**
|
||||
* Execute one cycle: run each node directive and collect outputs.
|
||||
*
|
||||
* Flow: resolve LLM credentials → spawn directives with bounded concurrency →
|
||||
* collect NodeOutputs; failed nodes are logged and replaced with [ERROR]
|
||||
* placeholder so the cycle still completes.
|
||||
*/
|
||||
export async function executeCycle(
|
||||
cycle: CognitiveCycle,
|
||||
toolCtx: ToolCtx,
|
||||
): Promise<NodeOutput[]> {
|
||||
console.info(
|
||||
`Executing cycle ${cycle.index} with ${cycle.directives.length} directives`,
|
||||
);
|
||||
|
||||
const { baseUrl, apiKey, model } = await resolveConfig();
|
||||
const svc = buildProviderService(baseUrl, apiKey, model);
|
||||
|
||||
const tasks = cycle.directives.map(async (dir, i) => {
|
||||
const nodeId = `Node-${cycle.index}-${i}`;
|
||||
const access = (dir.access_tier as "read" | "write" | "full") ?? "read";
|
||||
|
||||
try {
|
||||
const output = await runAgent(svc, dir.directive, access, toolCtx, undefined);
|
||||
return {
|
||||
id: nodeId,
|
||||
directive: dir.directive,
|
||||
output,
|
||||
} satisfies NodeOutput;
|
||||
} catch (e) {
|
||||
console.warn(`Node ${nodeId} failed (isolated): ${(e as Error).message}`);
|
||||
return {
|
||||
id: nodeId,
|
||||
directive: dir.directive,
|
||||
output: `[ERROR] ${(e as Error).message}`,
|
||||
} satisfies NodeOutput;
|
||||
}
|
||||
});
|
||||
|
||||
// Bounded concurrency: run at most MAX_CONCURRENT_NODES futures at once.
|
||||
const results: NodeOutput[] = [];
|
||||
for (let i = 0; i < tasks.length; i += MAX_CONCURRENT_NODES) {
|
||||
const batch = tasks.slice(i, i + MAX_CONCURRENT_NODES);
|
||||
const batchResults = await Promise.all(batch);
|
||||
results.push(...batchResults);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Hive-mind convergence documentation — writes deterministic audit trail.
|
||||
* Mirrors `apps/infrastructure/src/workflow/docs.rs`.
|
||||
*
|
||||
* Creates a markdown file documenting node outputs and consensus synthesis.
|
||||
*/
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import type { NodeOutput } from "@zesdex/domain";
|
||||
|
||||
/**
|
||||
* Write a deterministic audit trail for a hive-mind convergence.
|
||||
*
|
||||
* Flow: create docs/runs/ dir → build markdown content → write file.
|
||||
* This is deterministic (not an LLM step) and never skippable.
|
||||
*/
|
||||
export function writeHiveMindConvergence(
|
||||
runDir: string,
|
||||
nodes: NodeOutput[],
|
||||
consensus: string,
|
||||
): string {
|
||||
const docsDir = path.join(runDir, "docs", "runs");
|
||||
fs.mkdirSync(docsDir, { recursive: true });
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "").slice(0, 15);
|
||||
const filename = `${timestamp}-hive-mind-convergence.md`;
|
||||
const filepath = path.join(docsDir, filename);
|
||||
|
||||
let content = `# Hive Mind Convergence — ${timestamp}\n\n`;
|
||||
content += "## Node Outputs\n\n";
|
||||
|
||||
for (const node of nodes) {
|
||||
content += `### ${node.id} — ${node.directive}\n\n`;
|
||||
content += `${node.output}\n\n`;
|
||||
}
|
||||
|
||||
content += "## Consensus\n\n";
|
||||
content += consensus;
|
||||
content += "\n";
|
||||
|
||||
fs.writeFileSync(filepath, content);
|
||||
console.info(`Hive mind convergence written to ${filepath}`);
|
||||
return filepath;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Workflow engine — runs a parsed workflow script phase by phase.
|
||||
* Mirrors `apps/infrastructure/src/workflow/engine/execution.rs` and `primitives.rs`.
|
||||
*
|
||||
* Flow: parse YAML → build WorkflowScript → for each phase → run as subagent → collect results.
|
||||
*/
|
||||
import type { WorkflowScript } from "@zesdex/domain";
|
||||
import type { ToolCtx } from "../tools/mod.ts";
|
||||
import { runAgent } from "../tools/engine.ts";
|
||||
|
||||
/** Maximum characters of node output to feed into synthesis prompt per node. */
|
||||
export const MAX_NODE_OUTPUT_CHARS = 4000;
|
||||
|
||||
/**
|
||||
* Execute a single directive by spawning a subagent.
|
||||
* Mirrors `engine/primitives.rs` `execute_primitive`.
|
||||
*
|
||||
* Flow: load settings → resolve LLM credentials → run_agent → return output.
|
||||
*/
|
||||
export async function executePrimitive(directive: string, toolCtx: ToolCtx): Promise<string> {
|
||||
const { resolveConfig } = await import("../subagent/config_resolver.ts");
|
||||
const { buildProviderService } = await import("../subagent/http_provider.ts");
|
||||
|
||||
const { baseUrl, apiKey, model } = await resolveConfig();
|
||||
const svc = buildProviderService(baseUrl, apiKey, model);
|
||||
|
||||
return runAgent(svc, directive, "full", toolCtx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute each phase of a workflow script sequentially.
|
||||
* Mirrors `engine/execution.rs` `execute_workflow`.
|
||||
*
|
||||
* Flow: for each phase → execute_primitive → collect result.
|
||||
*/
|
||||
export async function executeWorkflowScript(
|
||||
script: WorkflowScript,
|
||||
toolCtx: ToolCtx,
|
||||
): Promise<string[]> {
|
||||
console.info(`Executing workflow: ${script.name} (${script.phases.length} phases)`);
|
||||
const results: string[] = [];
|
||||
|
||||
for (const phase of script.phases) {
|
||||
console.info(`Executing phase: ${phase.name}`);
|
||||
const result = await executePrimitive(phase.directive, toolCtx);
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/** Format a workflow execution result as a summary string. */
|
||||
export function formatWorkflowResult(script: WorkflowScript, results: string[]): string {
|
||||
let out = `## Workflow: ${script.name}\n\n`;
|
||||
results.forEach((result, i) => {
|
||||
const phase = script.phases[i]!;
|
||||
out += `### Phase ${i}: ${phase.name}\n\n${result}\n\n`;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
/**
|
||||
* Hive-mind convergence engine — ported in sub-phase 3d.
|
||||
* Hive-mind orchestration — re-exports executeHiveMind entry point.
|
||||
* Mirrors `apps/infrastructure/src/workflow/hive_mind/mod.rs`.
|
||||
*/
|
||||
import type { JsonValue } from "@zesdex/domain";
|
||||
import type { ToolCtx } from "../tools/mod.ts";
|
||||
|
||||
/** Execute a hive-mind convergence from tool arguments. */
|
||||
export async function executeHiveMind(_args: JsonValue, _ctx: ToolCtx): Promise<string> {
|
||||
throw new Error("hive-mind engine not yet wired in this build");
|
||||
}
|
||||
export { executeHiveMind } from "./orchestrate.ts";
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* Workflow execution engine — ported in sub-phase 3d. This module provides
|
||||
* the execution entry points used by the workflow tools.
|
||||
* Workflow + Hive-mind re-exports.
|
||||
* Mirrors `apps/infrastructure/src/workflow/mod.rs`.
|
||||
*/
|
||||
import type { ToolCtx } from "../tools/mod.ts";
|
||||
|
||||
/** Execute a YAML workflow definition. */
|
||||
export async function executeWorkflow(_yaml: string, _ctx: ToolCtx): Promise<string> {
|
||||
throw new Error("workflow engine not yet wired in this build");
|
||||
}
|
||||
export { parseWorkflowScript } from "./script.ts";
|
||||
export { executeWorkflowScript, executePrimitive, formatWorkflowResult, MAX_NODE_OUTPUT_CHARS } from "./engine.ts";
|
||||
export { executeWorkflow } from "./orchestrate.ts";
|
||||
export { executeCycle } from "./cycle.ts";
|
||||
export { synthesizeConsensus } from "./synthesis.ts";
|
||||
export { isComplexRequest } from "./complexity.ts";
|
||||
export { writeHiveMindConvergence } from "./docs.ts";
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Workflow + Hive-mind orchestration — entry points for the WorkflowRun and HiveMind tools.
|
||||
* Mirrors the top-level dispatch in `workflow/mod.rs` and `workflow/hive_mind/mod.rs`.
|
||||
*
|
||||
* Flow: parse cycles from args → execute each cycle → synthesize consensus
|
||||
* → write convergence doc → return formatted result.
|
||||
*/
|
||||
import type { JsonValue, CognitiveCycle, NodeOutput, NodeDirective } from "@zesdex/domain";
|
||||
import type { ToolCtx } from "../tools/mod.ts";
|
||||
import { parseWorkflowScript } from "./script.ts";
|
||||
import { executeWorkflowScript, formatWorkflowResult } from "./engine.ts";
|
||||
import { executeCycle } from "./cycle.ts";
|
||||
import { synthesizeConsensus } from "./synthesis.ts";
|
||||
import { writeHiveMindConvergence } from "./docs.ts";
|
||||
|
||||
/**
|
||||
* Execute a YAML workflow definition.
|
||||
* Entry point for the WorkflowRun tool.
|
||||
*/
|
||||
export async function executeWorkflow(yaml: string, ctx: ToolCtx): Promise<string> {
|
||||
const script = parseWorkflowScript(yaml);
|
||||
if (script.phases.length === 0) {
|
||||
return "Workflow has no phases to execute.";
|
||||
}
|
||||
const results = await executeWorkflowScript(script, ctx);
|
||||
return formatWorkflowResult(script, results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a hive-mind convergence from tool arguments.
|
||||
* Entry point for the HiveMind tool.
|
||||
*
|
||||
* Flow: parse cycles from args → execute each cycle → synthesize consensus
|
||||
* → write convergence doc → return formatted result.
|
||||
*/
|
||||
export async function executeHiveMind(args: JsonValue, ctx: ToolCtx): Promise<string> {
|
||||
const cyclesData = extractCycles(args);
|
||||
|
||||
if (cyclesData.length === 0) {
|
||||
return "Hive-mind has no cycles to execute.";
|
||||
}
|
||||
|
||||
const allNodeOutputs: NodeOutput[] = [];
|
||||
const cycleResults: string[] = [];
|
||||
|
||||
for (let i = 0; i < cyclesData.length; i++) {
|
||||
const cycle = cyclesData[i]!;
|
||||
console.info(`Executing hive-mind cycle ${i} with ${cycle.directives.length} directives`);
|
||||
|
||||
const nodeOutputs = await executeCycle(cycle, ctx);
|
||||
allNodeOutputs.push(...nodeOutputs);
|
||||
|
||||
const cycleSummary = nodeOutputs
|
||||
.map((n) => `- ${n.id}: ${n.output.slice(0, 200)}${n.output.length > 200 ? "..." : ""}`)
|
||||
.join("\n");
|
||||
cycleResults.push(`### Cycle ${i}\n\n${cycleSummary}`);
|
||||
}
|
||||
|
||||
// Synthesize consensus from all node outputs
|
||||
let consensus: string;
|
||||
try {
|
||||
consensus = await synthesizeConsensus(allNodeOutputs, ctx);
|
||||
} catch (e) {
|
||||
consensus = `Consensus synthesis failed: ${(e as Error).message}\n\n` +
|
||||
allNodeOutputs.map((n) => `### ${n.id}\n${n.output}`).join("\n\n");
|
||||
}
|
||||
|
||||
// Write convergence doc
|
||||
const runDir = ctx.sessionDir || process.cwd();
|
||||
writeHiveMindConvergence(runDir, allNodeOutputs, consensus);
|
||||
|
||||
// Build output
|
||||
let out = `## Hive Mind Convergence\n\n`;
|
||||
out += `**Cycles:** ${cyclesData.length}\n`;
|
||||
out += `**Nodes:** ${allNodeOutputs.length}\n\n`;
|
||||
out += cycleResults.join("\n\n");
|
||||
out += `\n\n---\n\n${consensus}`;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Extract CognitiveCycle objects from tool args. */
|
||||
function extractCycles(args: JsonValue): CognitiveCycle[] {
|
||||
if (!args || typeof args !== "object" || Array.isArray(args)) return [];
|
||||
const obj = args as Record<string, JsonValue>;
|
||||
const cyclesArr = obj["cycles"];
|
||||
|
||||
if (!Array.isArray(cyclesArr)) return [];
|
||||
|
||||
return cyclesArr.map((c, i) => {
|
||||
if (!c || typeof c !== "object" || Array.isArray(c)) {
|
||||
return { index: i, directives: [] };
|
||||
}
|
||||
const cycle = c as Record<string, JsonValue>;
|
||||
const directivesArr = Array.isArray(cycle["directives"]) ? cycle["directives"]! : [];
|
||||
|
||||
const directives: NodeDirective[] = directivesArr
|
||||
.filter((d): d is Record<string, JsonValue> =>
|
||||
typeof d === "object" && d !== null && !Array.isArray(d))
|
||||
.map((d) => ({
|
||||
directive: typeof d["directive"] === "string" ? d["directive"] : "",
|
||||
access_tier: typeof d["access"] === "string" ? d["access"] : "read",
|
||||
}))
|
||||
.filter((d) => d.directive !== "");
|
||||
|
||||
return { index: i, directives };
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Workflow script — parse and execute user-defined workflow scripts.
|
||||
* Mirrors `apps/infrastructure/src/workflow/script.rs`.
|
||||
*
|
||||
* Parses a simple YAML-like workflow definition into a WorkflowScript
|
||||
* (name + ordered phases). Supports a minimal YAML subset: `name:` and
|
||||
* `phases:` with `- name:` / `- directive:` entries.
|
||||
*/
|
||||
import type { WorkflowScript, WorkflowPhase } from "@zesdex/domain";
|
||||
|
||||
/**
|
||||
* Parse a YAML string into a WorkflowScript.
|
||||
*
|
||||
* Expected format:
|
||||
* ```yaml
|
||||
* name: my-workflow
|
||||
* phases:
|
||||
* - name: research
|
||||
* directive: "Explore the codebase..."
|
||||
* - name: implement
|
||||
* directive: "Implement the changes..."
|
||||
* ```
|
||||
*
|
||||
* Uses a lightweight regex-based parser to avoid adding a YAML library dep.
|
||||
* Handles flat key-value pairs and a single level of list-of-maps.
|
||||
*/
|
||||
export function parseWorkflowScript(yaml: string): WorkflowScript {
|
||||
const lines = yaml.split("\n");
|
||||
let name = "unnamed";
|
||||
const phases: WorkflowPhase[] = [];
|
||||
|
||||
let inPhases = false;
|
||||
let currentPhase: Partial<WorkflowPhase> | null = null;
|
||||
|
||||
for (const raw of lines) {
|
||||
const trimmed = raw.trim();
|
||||
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
||||
|
||||
// Top-level key: value
|
||||
const topMatch = trimmed.match(/^(\w+):\s*(.*)$/);
|
||||
if (topMatch && !raw.startsWith(" ") && !raw.startsWith("-")) {
|
||||
const key = topMatch[1]!;
|
||||
const val = topMatch[2]!.trim();
|
||||
if (key === "name" && val !== "") {
|
||||
name = val;
|
||||
} else if (key === "phases") {
|
||||
inPhases = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inPhases) continue;
|
||||
|
||||
// List item: - name: foo or - directive: bar
|
||||
const listMatch = trimmed.match(/^-\s+(\w+):\s*(.*)$/);
|
||||
if (listMatch) {
|
||||
// If there was a previous phase, push it
|
||||
if (currentPhase && (currentPhase.name || currentPhase.directive)) {
|
||||
phases.push({
|
||||
name: currentPhase.name ?? "phase",
|
||||
directive: currentPhase.directive ?? "",
|
||||
});
|
||||
}
|
||||
const key = listMatch[1]!;
|
||||
const val = listMatch[2]!.trim();
|
||||
currentPhase = { name: undefined, directive: undefined };
|
||||
if (key === "name") currentPhase.name = val;
|
||||
if (key === "directive") currentPhase.directive = val;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Continuation of a phase field (indented under a list item)
|
||||
if (currentPhase && !trimmed.startsWith("-")) {
|
||||
const kvMatch = trimmed.match(/^(\w+):\s*(.*)$/);
|
||||
if (kvMatch) {
|
||||
const key = kvMatch[1]!;
|
||||
const val = kvMatch[2]!.trim();
|
||||
if (key === "name") currentPhase.name = val;
|
||||
if (key === "directive") currentPhase.directive = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Push the last phase
|
||||
if (currentPhase && (currentPhase.name || currentPhase.directive)) {
|
||||
phases.push({
|
||||
name: currentPhase.name ?? "phase",
|
||||
directive: currentPhase.directive ?? "",
|
||||
});
|
||||
}
|
||||
|
||||
return { name, phases };
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Consensus synthesis — reconciles multiple node outputs into one assessment.
|
||||
* Mirrors `apps/infrastructure/src/workflow/hive_mind/synthesis.rs`.
|
||||
*
|
||||
* Flow: build combined prompt from node outputs → ask the model to distill
|
||||
* into a single consensus (conflicts, agreements, key findings) → return the
|
||||
* synthesized text. Falls back to plain concatenation if the LLM call fails.
|
||||
*/
|
||||
import type { NodeOutput } from "@zesdex/domain";
|
||||
import type { ToolCtx } from "../tools/mod.ts";
|
||||
import { MAX_NODE_OUTPUT_CHARS } from "./engine.ts";
|
||||
|
||||
/**
|
||||
* Synthesize a consensus from all node outputs using the LLM.
|
||||
*
|
||||
* Flow: combine node outputs → ask the model to reconcile them into a single
|
||||
* consensus → return the synthesized text. Falls back to a plain
|
||||
* concatenation summary if the LLM is unreachable or the call fails.
|
||||
*/
|
||||
export async function synthesizeConsensus(
|
||||
nodes: NodeOutput[],
|
||||
_toolCtx: ToolCtx,
|
||||
): Promise<string> {
|
||||
if (nodes.length === 0) return "No node outputs to synthesize.";
|
||||
|
||||
const combined = buildCombinedBody(nodes);
|
||||
|
||||
const { resolveConfig } = await import("../subagent/config_resolver.ts");
|
||||
const { buildProviderService } = await import("../subagent/http_provider.ts");
|
||||
|
||||
const { baseUrl, apiKey, model } = await resolveConfig();
|
||||
const svc = buildProviderService(baseUrl, apiKey, model);
|
||||
|
||||
const systemMsg = {
|
||||
role: "system" as const,
|
||||
content: "You are a consensus synthesizer for a multi-agent hive mind. " +
|
||||
"Several independent nodes analysed a problem and produced the outputs " +
|
||||
"below. Distill them into ONE coherent consensus report with these sections:\n" +
|
||||
"- AGREEMENTS: points multiple nodes converge on.\n" +
|
||||
"- CONFLICTS: contradictory conclusions, with which node(s) support each side.\n" +
|
||||
"- KEY FINDINGS: the most important, actionable takeaways.\n" +
|
||||
"- RECOMMENDATION: a single recommended next action, or 'no clear consensus' " +
|
||||
"if the outputs are too divergent.\n" +
|
||||
"Be concise and factual. If a node errored, note it and ignore its content.\n" +
|
||||
"Do not invent facts not present in the node outputs.",
|
||||
};
|
||||
|
||||
const userMsg = {
|
||||
role: "user" as const,
|
||||
content: `Consolidate these ${nodes.length} node outputs into a single consensus:\n\n${combined}`,
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await svc.chat([systemMsg, userMsg], undefined, 1024, 0.3);
|
||||
const text = result.message.content ?? "";
|
||||
|
||||
if (text.trim() === "") {
|
||||
console.warn("consensus LLM returned empty output; falling back to concat summary");
|
||||
return concatSummary(nodes);
|
||||
}
|
||||
|
||||
return `# Consensus Synthesis\n\nNodes synthesized: ${nodes.length}\n\n${text}`;
|
||||
} catch (e) {
|
||||
console.warn(`consensus LLM call failed; falling back to concat summary: ${(e as Error).message}`);
|
||||
return concatSummary(nodes);
|
||||
}
|
||||
}
|
||||
|
||||
/** Build the concatenated node-output body for the prompt. */
|
||||
function buildCombinedBody(nodes: NodeOutput[]): string {
|
||||
let combined = "";
|
||||
for (const node of nodes) {
|
||||
const output = truncateChars(node.output, MAX_NODE_OUTPUT_CHARS);
|
||||
combined += `\n## ${node.id} — ${node.directive}\n${output}\n`;
|
||||
}
|
||||
return combined;
|
||||
}
|
||||
|
||||
/** Fallback: a plain concatenation summary. */
|
||||
function concatSummary(nodes: NodeOutput[]): string {
|
||||
const combined = buildCombinedBody(nodes);
|
||||
return `# Consensus Synthesis\n\nNodes synthesized: ${nodes.length}\n\n## Summary\n\n` +
|
||||
`The following node outputs were collected:\n\n${combined}\n\n` +
|
||||
`## Key Findings\n\nReview the individual node outputs above for detailed findings.`;
|
||||
}
|
||||
|
||||
/** Truncate a string to maxLen characters (char-safe). */
|
||||
function truncateChars(s: string, maxLen: number): string {
|
||||
if (s.length <= maxLen) return s;
|
||||
return s.slice(0, maxLen);
|
||||
}
|
||||
Reference in New Issue
Block a user