@thispointon/kondi-chat 0.1.2
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 +556 -0
- package/bin/kondi-chat +56 -0
- package/bin/kondi-chat.js +72 -0
- package/package.json +55 -0
- package/scripts/demo.tape +49 -0
- package/scripts/postinstall.cjs +103 -0
- package/src/audit/analytics.ts +261 -0
- package/src/audit/ledger.ts +253 -0
- package/src/audit/telemetry.ts +165 -0
- package/src/cli/backend.ts +675 -0
- package/src/cli/commands.ts +419 -0
- package/src/cli/help.ts +182 -0
- package/src/cli/submit-helpers.ts +159 -0
- package/src/cli/submit.ts +539 -0
- package/src/cli/wizard.ts +121 -0
- package/src/context/bootstrap.ts +138 -0
- package/src/context/budget.ts +100 -0
- package/src/context/manager.ts +666 -0
- package/src/context/memory.ts +160 -0
- package/src/context/preflight.ts +176 -0
- package/src/context/project-brain.ts +101 -0
- package/src/context/receipts.ts +108 -0
- package/src/context/skills.ts +154 -0
- package/src/context/symbol-index.ts +240 -0
- package/src/council/profiles.ts +137 -0
- package/src/council/tool.ts +138 -0
- package/src/council-engine/cli/council-artifacts.ts +230 -0
- package/src/council-engine/cli/council-config.ts +178 -0
- package/src/council-engine/cli/council-session-export.ts +116 -0
- package/src/council-engine/cli/kondi.ts +98 -0
- package/src/council-engine/cli/llm-caller.ts +229 -0
- package/src/council-engine/cli/localStorage-shim.ts +119 -0
- package/src/council-engine/cli/node-platform.ts +68 -0
- package/src/council-engine/cli/run-council.ts +481 -0
- package/src/council-engine/cli/run-pipeline.ts +772 -0
- package/src/council-engine/cli/session-export.ts +153 -0
- package/src/council-engine/configs/councils/analysis.json +101 -0
- package/src/council-engine/configs/councils/code-planning.json +86 -0
- package/src/council-engine/configs/councils/coding.json +89 -0
- package/src/council-engine/configs/councils/debate.json +97 -0
- package/src/council-engine/configs/councils/solo-claude.json +34 -0
- package/src/council-engine/configs/councils/solo-gpt.json +34 -0
- package/src/council-engine/council/coding-orchestrator.ts +1205 -0
- package/src/council-engine/council/context-bootstrap.ts +147 -0
- package/src/council-engine/council/context-inspection.ts +42 -0
- package/src/council-engine/council/context-store.ts +763 -0
- package/src/council-engine/council/deliberation-orchestrator.ts +2762 -0
- package/src/council-engine/council/factory.ts +164 -0
- package/src/council-engine/council/index.ts +201 -0
- package/src/council-engine/council/ledger-store.ts +438 -0
- package/src/council-engine/council/prompts.ts +1689 -0
- package/src/council-engine/council/storage-cleanup.ts +164 -0
- package/src/council-engine/council/store.ts +1110 -0
- package/src/council-engine/council/synthesis.ts +291 -0
- package/src/council-engine/council/types.ts +845 -0
- package/src/council-engine/council/validation.ts +613 -0
- package/src/council-engine/pipeline/build-detect.ts +73 -0
- package/src/council-engine/pipeline/executor.ts +1048 -0
- package/src/council-engine/pipeline/index.ts +9 -0
- package/src/council-engine/pipeline/install-detect.ts +84 -0
- package/src/council-engine/pipeline/memory-store.ts +182 -0
- package/src/council-engine/pipeline/output-parsers.ts +146 -0
- package/src/council-engine/pipeline/run-output.ts +149 -0
- package/src/council-engine/pipeline/session-import.ts +177 -0
- package/src/council-engine/pipeline/store.ts +753 -0
- package/src/council-engine/pipeline/test-detect.ts +82 -0
- package/src/council-engine/pipeline/types.ts +401 -0
- package/src/council-engine/services/deliberationSummary.ts +114 -0
- package/src/council-engine/tsconfig.json +16 -0
- package/src/council-engine/types/mcp.ts +122 -0
- package/src/council-engine/utils/filterTools.ts +73 -0
- package/src/engine/apply.ts +238 -0
- package/src/engine/checkpoints.ts +237 -0
- package/src/engine/consultants.ts +347 -0
- package/src/engine/diff.ts +171 -0
- package/src/engine/errors.ts +102 -0
- package/src/engine/git-tools.ts +246 -0
- package/src/engine/hooks.ts +181 -0
- package/src/engine/loop-guard.ts +155 -0
- package/src/engine/permissions.ts +293 -0
- package/src/engine/pipeline.ts +376 -0
- package/src/engine/sub-agents.ts +133 -0
- package/src/engine/task-card.ts +185 -0
- package/src/engine/task-router.ts +256 -0
- package/src/engine/task-store.ts +86 -0
- package/src/engine/tools.ts +783 -0
- package/src/engine/verify.ts +111 -0
- package/src/mcp/client.ts +225 -0
- package/src/mcp/config.ts +120 -0
- package/src/mcp/tool-manager.ts +192 -0
- package/src/mcp/types.ts +61 -0
- package/src/providers/llm-caller.ts +943 -0
- package/src/providers/rate-limiter.ts +238 -0
- package/src/router/NOTES.md +28 -0
- package/src/router/collector.ts +474 -0
- package/src/router/embeddings.ts +286 -0
- package/src/router/index.ts +299 -0
- package/src/router/intent-router.ts +225 -0
- package/src/router/nn-router.ts +205 -0
- package/src/router/profiles.ts +309 -0
- package/src/router/registry.ts +565 -0
- package/src/router/rules.ts +274 -0
- package/src/router/train.py +408 -0
- package/src/session/store.ts +211 -0
- package/src/test-utils/mock-llm.ts +39 -0
- package/src/types.ts +322 -0
- package/src/web/manager.ts +311 -0
|
@@ -0,0 +1,2762 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Council: Deliberation Orchestrator
|
|
3
|
+
* Deterministic state machine for structured multi-agent deliberation
|
|
4
|
+
*
|
|
5
|
+
* The orchestrator is CODE, not an agent. It:
|
|
6
|
+
* - Drives the state machine
|
|
7
|
+
* - Invokes agents with correct context
|
|
8
|
+
* - Manages artifacts and ledger
|
|
9
|
+
* - Enforces invariants
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
Council,
|
|
14
|
+
Persona,
|
|
15
|
+
LedgerEntry,
|
|
16
|
+
LedgerEntryType,
|
|
17
|
+
DeliberationPhase,
|
|
18
|
+
DeliberationRole,
|
|
19
|
+
DeliberationRoleAssignment,
|
|
20
|
+
ManagerEvaluation,
|
|
21
|
+
ManagerReview,
|
|
22
|
+
ContextArtifact,
|
|
23
|
+
ContextPatch,
|
|
24
|
+
ArtifactRef,
|
|
25
|
+
} from './types';
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
ledgerStore,
|
|
29
|
+
getEntries,
|
|
30
|
+
getAllEntries,
|
|
31
|
+
getLatestOfType,
|
|
32
|
+
getEntriesForRound,
|
|
33
|
+
getManagerNotes,
|
|
34
|
+
getLedgerTokenCount,
|
|
35
|
+
formatEntriesForContext,
|
|
36
|
+
buildMechanicalSummary,
|
|
37
|
+
} from './ledger-store';
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
getCurrentContext,
|
|
41
|
+
getContextHistory,
|
|
42
|
+
createInitialContext,
|
|
43
|
+
createContextVersion,
|
|
44
|
+
getPendingPatches,
|
|
45
|
+
createPatch,
|
|
46
|
+
acceptPatch,
|
|
47
|
+
rejectPatch,
|
|
48
|
+
createDecision,
|
|
49
|
+
createPlan,
|
|
50
|
+
createDirective,
|
|
51
|
+
createOutput,
|
|
52
|
+
createRevisionOutput,
|
|
53
|
+
getDecision,
|
|
54
|
+
getPlan,
|
|
55
|
+
getDirective,
|
|
56
|
+
getLatestOutput,
|
|
57
|
+
} from './context-store';
|
|
58
|
+
|
|
59
|
+
import {
|
|
60
|
+
councilStore,
|
|
61
|
+
getPersonaByRole,
|
|
62
|
+
getRoleAssignment,
|
|
63
|
+
isDeliberationMode,
|
|
64
|
+
} from './store';
|
|
65
|
+
|
|
66
|
+
import { bootstrapDirectoryContext } from './context-bootstrap';
|
|
67
|
+
import { buildContextInspection } from './context-inspection';
|
|
68
|
+
import type { ContextInspection } from './types';
|
|
69
|
+
|
|
70
|
+
import {
|
|
71
|
+
buildManagerFramingPrompt,
|
|
72
|
+
buildManagerEvaluationPrompt,
|
|
73
|
+
buildManagerDecisionPrompt,
|
|
74
|
+
buildManagerForcedDecisionPrompt,
|
|
75
|
+
buildManagerPlanPrompt,
|
|
76
|
+
buildWorkDirectivePrompt,
|
|
77
|
+
buildManagerReviewPrompt,
|
|
78
|
+
buildManagerRoundSummaryPrompt,
|
|
79
|
+
buildIndependentAnalysisPrompt,
|
|
80
|
+
buildDeliberationResponsePrompt,
|
|
81
|
+
buildConsultantFinalPositionPrompt,
|
|
82
|
+
buildConsultantReviewPrompt,
|
|
83
|
+
buildWorkerExecutionPrompt,
|
|
84
|
+
buildWorkerRevisionPrompt,
|
|
85
|
+
getMinimalWorkerSystemPrompt,
|
|
86
|
+
type WorkerPermissions,
|
|
87
|
+
} from './prompts';
|
|
88
|
+
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// Types
|
|
91
|
+
// ============================================================================
|
|
92
|
+
|
|
93
|
+
export interface AgentInvocation {
|
|
94
|
+
personaId: string;
|
|
95
|
+
systemPrompt: string;
|
|
96
|
+
userMessage: string;
|
|
97
|
+
/** When true, the invoker should NOT pass MCP tools (avoids slow MCP connections) */
|
|
98
|
+
skipTools?: boolean;
|
|
99
|
+
/** Restrict to specific tool names (e.g. ['Read', 'Write', 'Bash']) */
|
|
100
|
+
allowedTools?: string[];
|
|
101
|
+
/** MCP servers this invocation can access (undefined = all servers) */
|
|
102
|
+
allowedServerIds?: string[];
|
|
103
|
+
/** Working directory override for local tool calls (bypasses singleton) */
|
|
104
|
+
workingDirectory?: string;
|
|
105
|
+
/** Timeout in ms — set by invokeAgentSafe based on role/context defaults */
|
|
106
|
+
timeoutMs?: number;
|
|
107
|
+
/** Cacheable context (e.g., bootstrap directory scan) for Anthropic prompt caching */
|
|
108
|
+
cacheableContext?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface AgentResponse {
|
|
112
|
+
content: string;
|
|
113
|
+
tokensUsed: number;
|
|
114
|
+
latencyMs: number;
|
|
115
|
+
structured?: Record<string, unknown>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type AgentInvoker = (invocation: AgentInvocation, persona: Persona) => Promise<AgentResponse>;
|
|
119
|
+
|
|
120
|
+
export interface OrchestratorConfig {
|
|
121
|
+
invokeAgent: AgentInvoker;
|
|
122
|
+
onPhaseChange?: (from: DeliberationPhase, to: DeliberationPhase) => void;
|
|
123
|
+
onEntryAdded?: (entry: LedgerEntry) => void;
|
|
124
|
+
onError?: (error: Error, context: string) => void;
|
|
125
|
+
/** Called when a persona starts thinking (startedAt = Date.now() timestamp, prompt = user message sent) */
|
|
126
|
+
onAgentThinkingStart?: (persona: Persona, startedAt: number, prompt?: string) => void;
|
|
127
|
+
/** Called when a persona finishes thinking */
|
|
128
|
+
onAgentThinkingEnd?: (persona: Persona) => void;
|
|
129
|
+
/** Called when an agent times out (before retry). Enables UI timeout warnings. */
|
|
130
|
+
onAgentTimeout?: (persona: Persona, context: string, elapsedMs: number) => void;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// Phase Transition Table
|
|
135
|
+
// ============================================================================
|
|
136
|
+
|
|
137
|
+
const PHASE_TRANSITIONS: Record<DeliberationPhase, {
|
|
138
|
+
validNext: DeliberationPhase[];
|
|
139
|
+
terminal: boolean;
|
|
140
|
+
}> = {
|
|
141
|
+
created: { validNext: ['problem_framing', 'executing'], terminal: false },
|
|
142
|
+
problem_framing: { validNext: ['round_independent', 'deciding'], terminal: false },
|
|
143
|
+
round_independent: { validNext: ['round_waiting_for_manager'], terminal: false },
|
|
144
|
+
round_interactive: { validNext: ['round_waiting_for_manager'], terminal: false },
|
|
145
|
+
round_waiting_for_manager: { validNext: ['round_interactive', 'deciding', 'planning'], terminal: false },
|
|
146
|
+
planning: { validNext: ['directing'], terminal: false },
|
|
147
|
+
deciding: { validNext: ['planning', 'directing', 'completed'], terminal: false },
|
|
148
|
+
directing: { validNext: ['executing'], terminal: false },
|
|
149
|
+
executing: { validNext: ['reviewing', 'completed'], terminal: false },
|
|
150
|
+
reviewing: { validNext: ['completed', 'revising', 'round_interactive'], terminal: false },
|
|
151
|
+
revising: { validNext: ['reviewing', 'completed'], terminal: false },
|
|
152
|
+
decomposing: { validNext: [], terminal: false },
|
|
153
|
+
implementing: { validNext: [], terminal: false },
|
|
154
|
+
code_reviewing: { validNext: [], terminal: false },
|
|
155
|
+
testing: { validNext: [], terminal: false },
|
|
156
|
+
debugging: { validNext: [], terminal: false },
|
|
157
|
+
paused: { validNext: ['created', 'problem_framing', 'round_independent', 'round_interactive', 'round_waiting_for_manager', 'planning', 'deciding', 'directing', 'executing', 'reviewing', 'revising', 'decomposing', 'implementing', 'code_reviewing', 'testing', 'debugging'], terminal: false },
|
|
158
|
+
completed: { validNext: [], terminal: true },
|
|
159
|
+
cancelled: { validNext: [], terminal: true },
|
|
160
|
+
failed: { validNext: [], terminal: true },
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// ============================================================================
|
|
164
|
+
// Deliberation Orchestrator
|
|
165
|
+
// ============================================================================
|
|
166
|
+
|
|
167
|
+
export class DeliberationOrchestrator {
|
|
168
|
+
private config: OrchestratorConfig;
|
|
169
|
+
private activeCouncilId: string | null = null;
|
|
170
|
+
/** Bootstrapped directory context — shared with all personas via prompt */
|
|
171
|
+
private bootstrappedContext: string = '';
|
|
172
|
+
|
|
173
|
+
constructor(config: OrchestratorConfig) {
|
|
174
|
+
this.config = config;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ==========================================================================
|
|
178
|
+
// Auto-Run: Full Deliberation Loop
|
|
179
|
+
// ==========================================================================
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Run the full deliberation automatically from start to completion.
|
|
183
|
+
* Frames the problem then hands off to continueDeliberation.
|
|
184
|
+
*/
|
|
185
|
+
async runFullDeliberation(council: Council, rawProblem: string): Promise<void> {
|
|
186
|
+
const deliberationStart = Date.now();
|
|
187
|
+
console.log('[Orchestrator] Starting full deliberation...');
|
|
188
|
+
this.activeCouncilId = council.id;
|
|
189
|
+
|
|
190
|
+
// Always read fresh from store to avoid stale React state
|
|
191
|
+
council = councilStore.get(council.id) || council;
|
|
192
|
+
|
|
193
|
+
// Pre-flight check: log role assignments and personas for debugging
|
|
194
|
+
const roleAssignments = council.deliberation?.roleAssignments || [];
|
|
195
|
+
const consultantAssignments = roleAssignments.filter(r => r.role === 'consultant');
|
|
196
|
+
const managerAssignments = roleAssignments.filter(r => r.role === 'manager');
|
|
197
|
+
const workerAssignments = roleAssignments.filter(r => r.role === 'worker');
|
|
198
|
+
|
|
199
|
+
console.log('[Orchestrator] PRE-FLIGHT CHECK:', {
|
|
200
|
+
totalPersonas: council.personas.length,
|
|
201
|
+
personaNames: council.personas.map(p => `${p.name} (${p.id.slice(0, 8)})`),
|
|
202
|
+
totalRoleAssignments: roleAssignments.length,
|
|
203
|
+
roleBreakdown: {
|
|
204
|
+
managers: managerAssignments.length,
|
|
205
|
+
consultants: consultantAssignments.length,
|
|
206
|
+
workers: workerAssignments.length,
|
|
207
|
+
},
|
|
208
|
+
consultantDetails: consultantAssignments.map(r => {
|
|
209
|
+
const p = council.personas.find(p => p.id === r.personaId);
|
|
210
|
+
return `${p?.name || 'MISSING'} (${r.personaId.slice(0, 8)})`;
|
|
211
|
+
}),
|
|
212
|
+
// Check for personas without role assignments
|
|
213
|
+
unassignedPersonas: council.personas
|
|
214
|
+
.filter(p => !roleAssignments.some(r => r.personaId === p.id))
|
|
215
|
+
.map(p => `${p.name} (${p.id.slice(0, 8)})`),
|
|
216
|
+
// Check for role assignments without matching personas
|
|
217
|
+
orphanedAssignments: roleAssignments
|
|
218
|
+
.filter(r => !council.personas.some(p => p.id === r.personaId))
|
|
219
|
+
.map(r => `${r.role}:${r.personaId.slice(0, 8)}`),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (consultantAssignments.length < 2 && consultantAssignments.length > 0) {
|
|
223
|
+
console.warn(`[Orchestrator] WARNING: Only ${consultantAssignments.length} consultant(s) found. ` +
|
|
224
|
+
`If you expected more, check role assignments in Setup.`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Auto-repair: ensure every persona has a role assignment
|
|
228
|
+
const unassigned = council.personas.filter(
|
|
229
|
+
p => !roleAssignments.some(r => r.personaId === p.id)
|
|
230
|
+
);
|
|
231
|
+
if (unassigned.length > 0) {
|
|
232
|
+
console.warn(`[Orchestrator] REPAIRING: ${unassigned.length} persona(s) without role assignments:`,
|
|
233
|
+
unassigned.map(p => p.name));
|
|
234
|
+
const repairedAssignments = [
|
|
235
|
+
...roleAssignments,
|
|
236
|
+
...unassigned.map(p => ({
|
|
237
|
+
personaId: p.id,
|
|
238
|
+
role: (p.preferredDeliberationRole || 'consultant') as 'manager' | 'consultant' | 'worker',
|
|
239
|
+
})),
|
|
240
|
+
];
|
|
241
|
+
councilStore.setRoleAssignments(council.id, repairedAssignments);
|
|
242
|
+
// Re-fetch council with repaired assignments
|
|
243
|
+
council = councilStore.get(council.id)!;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Special path: 0 managers → direct execution (worker-only council)
|
|
247
|
+
if (managerAssignments.length === 0) {
|
|
248
|
+
console.log('[Orchestrator] No manager — running direct execution path');
|
|
249
|
+
await this.runDirectExecution(council, rawProblem);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Lightweight council types (agent/analysis): skip full deliberation,
|
|
254
|
+
// go straight to worker execution. The manager's system prompt is folded into
|
|
255
|
+
// the worker's context so its guidance isn't lost.
|
|
256
|
+
const stepType = council.deliberation?.stepType;
|
|
257
|
+
if (stepType === 'agent' || stepType === 'analysis') {
|
|
258
|
+
console.log(`[Orchestrator] Lightweight step type '${stepType}' — running direct execution path`);
|
|
259
|
+
await this.runDirectExecution(council, rawProblem);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Phase 1: Frame the problem
|
|
264
|
+
const t0 = Date.now();
|
|
265
|
+
await this.frameProblem(council, rawProblem);
|
|
266
|
+
console.log(`[Orchestrator:Timing] frameProblem took ${((Date.now() - t0) / 1000).toFixed(1)}s (elapsed: ${((Date.now() - deliberationStart) / 1000).toFixed(0)}s)`);
|
|
267
|
+
|
|
268
|
+
// Continue through all remaining phases automatically
|
|
269
|
+
await this.continueDeliberation(council.id, deliberationStart);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Continue deliberation from whatever phase it's currently in.
|
|
274
|
+
* Used after initial start (runFullDeliberation) and after resume from pause.
|
|
275
|
+
* Runs fully automatically — no manual step-by-step buttons needed.
|
|
276
|
+
*/
|
|
277
|
+
async continueDeliberation(councilId: string, startTime?: number): Promise<void> {
|
|
278
|
+
this.activeCouncilId = councilId;
|
|
279
|
+
const elapsed = () => startTime ? ` (elapsed: ${((Date.now() - startTime) / 1000).toFixed(0)}s)` : '';
|
|
280
|
+
let council = councilStore.get(councilId)!;
|
|
281
|
+
|
|
282
|
+
// Phase 2: Deliberation rounds
|
|
283
|
+
const maxRounds = council.deliberation?.maxRounds || 4;
|
|
284
|
+
|
|
285
|
+
// Run rounds until manager decides or max rounds reached
|
|
286
|
+
let loopGuard = 0;
|
|
287
|
+
const maxLoopIterations = maxRounds * 3 + 10; // Safety limit
|
|
288
|
+
|
|
289
|
+
while (loopGuard++ < maxLoopIterations) {
|
|
290
|
+
council = councilStore.get(councilId)!;
|
|
291
|
+
const phase = council.deliberationState?.currentPhase;
|
|
292
|
+
|
|
293
|
+
// Stop on pause or terminal states
|
|
294
|
+
if (phase === 'paused') {
|
|
295
|
+
console.log('[Orchestrator] Deliberation paused — stopping auto-run');
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (phase === 'deciding' || phase === 'planning' || phase === 'directing' ||
|
|
299
|
+
phase === 'executing' || phase === 'reviewing' || phase === 'revising' ||
|
|
300
|
+
phase === 'completed' || phase === 'cancelled' || phase === 'failed') {
|
|
301
|
+
break; // Move past deliberation rounds
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
const phaseStart = Date.now();
|
|
306
|
+
if (phase === 'round_independent') {
|
|
307
|
+
console.log(`[Orchestrator] Running independent round...`);
|
|
308
|
+
await this.runIndependentRound(council);
|
|
309
|
+
console.log(`[Orchestrator:Timing] round_independent took ${((Date.now() - phaseStart) / 1000).toFixed(1)}s${elapsed()}`);
|
|
310
|
+
// Fallback: if phase didn't advance (isRoundComplete returned false), force it
|
|
311
|
+
const afterPhase = councilStore.get(councilId)?.deliberationState?.currentPhase;
|
|
312
|
+
if (afterPhase === 'round_independent') {
|
|
313
|
+
console.warn('[Orchestrator] Round did not advance after independent round — forcing transition');
|
|
314
|
+
this.transitionPhase(councilId, 'round_waiting_for_manager');
|
|
315
|
+
}
|
|
316
|
+
} else if (phase === 'round_waiting_for_manager') {
|
|
317
|
+
console.log('[Orchestrator] Manager evaluating...');
|
|
318
|
+
await this.managerEvaluate(council);
|
|
319
|
+
console.log(`[Orchestrator:Timing] managerEvaluate took ${((Date.now() - phaseStart) / 1000).toFixed(1)}s${elapsed()}`);
|
|
320
|
+
} else if (phase === 'round_interactive') {
|
|
321
|
+
console.log('[Orchestrator] Running interactive round...');
|
|
322
|
+
await this.runInteractiveRound(council);
|
|
323
|
+
console.log(`[Orchestrator:Timing] round_interactive took ${((Date.now() - phaseStart) / 1000).toFixed(1)}s${elapsed()}`);
|
|
324
|
+
// Fallback: if phase didn't advance, force it
|
|
325
|
+
const afterPhase = councilStore.get(councilId)?.deliberationState?.currentPhase;
|
|
326
|
+
if (afterPhase === 'round_interactive') {
|
|
327
|
+
console.warn('[Orchestrator] Round did not advance after interactive round — forcing transition');
|
|
328
|
+
this.transitionPhase(councilId, 'round_waiting_for_manager');
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
console.log(`[Orchestrator] Unexpected phase during deliberation: ${phase}`);
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
} catch (error) {
|
|
335
|
+
const errMsg = (error as Error).message || String(error);
|
|
336
|
+
const isRateLimit = /\b(429|rate.limit|too many requests)\b/i.test(errMsg);
|
|
337
|
+
|
|
338
|
+
if (isRateLimit) {
|
|
339
|
+
// Rate limit during deliberation round — wait and retry this iteration
|
|
340
|
+
const delayMs = 120_000; // 2 minutes
|
|
341
|
+
console.warn(
|
|
342
|
+
`[Orchestrator] Rate limit during ${phase}, waiting ${Math.round(delayMs / 1000)}s before retrying...`
|
|
343
|
+
);
|
|
344
|
+
this.createEntry(
|
|
345
|
+
councilId, 'manager', this.getManager(council).id,
|
|
346
|
+
'error', phase || 'failed',
|
|
347
|
+
`Rate limited during ${phase} — waiting ${Math.round(delayMs / 1000)}s before retrying...`,
|
|
348
|
+
0, 0, undefined, council.deliberationState?.currentRound
|
|
349
|
+
);
|
|
350
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
351
|
+
loopGuard--; // don't count this as a loop iteration
|
|
352
|
+
continue; // retry the same phase
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
console.error(`[Orchestrator] Error during phase ${phase}:`, errMsg);
|
|
356
|
+
|
|
357
|
+
// Record the error as a ledger entry — attribute to the phase's active role
|
|
358
|
+
const isConsultantPhase = phase === 'round_independent' || phase === 'round_interactive';
|
|
359
|
+
const role = isConsultantPhase ? 'consultant' : 'manager';
|
|
360
|
+
const fallbackPersona = isConsultantPhase
|
|
361
|
+
? (council.personas.find(p => p.preferredDeliberationRole === 'consultant') || this.getManager(council))
|
|
362
|
+
: this.getManager(council);
|
|
363
|
+
this.createEntry(
|
|
364
|
+
councilId,
|
|
365
|
+
role,
|
|
366
|
+
fallbackPersona.id,
|
|
367
|
+
'error',
|
|
368
|
+
phase || 'failed',
|
|
369
|
+
`Deliberation error during ${phase}: ${errMsg}`,
|
|
370
|
+
0, 0, undefined,
|
|
371
|
+
council.deliberationState?.currentRound
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
// Transition to failed state so UI reflects the problem
|
|
375
|
+
this.transitionPhase(councilId, 'failed');
|
|
376
|
+
throw error; // Re-throw so caller can handle
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Phase 3: Decision + Plan + Directive + Execution
|
|
381
|
+
// Wrapped with phase-level rate-limit retry: if a phase fails due to
|
|
382
|
+
// rate limiting (after invokeAgentSafe's own 5 retries are exhausted),
|
|
383
|
+
// we wait and retry from the current phase instead of failing the council.
|
|
384
|
+
const PHASE_RETRY_MAX = 3;
|
|
385
|
+
const PHASE_RETRY_BASE_MS = 120_000; // 2 minutes
|
|
386
|
+
|
|
387
|
+
for (let phaseAttempt = 0; phaseAttempt <= PHASE_RETRY_MAX; phaseAttempt++) {
|
|
388
|
+
try {
|
|
389
|
+
council = councilStore.get(councilId)!;
|
|
390
|
+
let phase = council.deliberationState?.currentPhase;
|
|
391
|
+
|
|
392
|
+
// If still in round phases after max iterations, force to deciding
|
|
393
|
+
if (phase === 'round_waiting_for_manager' || phase === 'round_interactive' || phase === 'round_independent') {
|
|
394
|
+
console.log('[Orchestrator] Max rounds exhausted, forcing decision phase');
|
|
395
|
+
this.transitionPhase(councilId, 'deciding');
|
|
396
|
+
council = councilStore.get(councilId)!;
|
|
397
|
+
phase = council.deliberationState?.currentPhase;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (phase === 'paused' || phase === 'failed') return;
|
|
401
|
+
|
|
402
|
+
if (phase === 'deciding') {
|
|
403
|
+
const t = Date.now();
|
|
404
|
+
console.log('[Orchestrator] Making decision...');
|
|
405
|
+
await this.makeDecision(council);
|
|
406
|
+
console.log(`[Orchestrator:Timing] makeDecision took ${((Date.now() - t) / 1000).toFixed(1)}s${elapsed()}`);
|
|
407
|
+
council = councilStore.get(councilId)!;
|
|
408
|
+
phase = council.deliberationState?.currentPhase;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (phase === 'paused' || phase === 'failed') return;
|
|
412
|
+
|
|
413
|
+
if (phase === 'planning') {
|
|
414
|
+
const t = Date.now();
|
|
415
|
+
console.log('[Orchestrator] Creating plan...');
|
|
416
|
+
await this.createPlan(council);
|
|
417
|
+
console.log(`[Orchestrator:Timing] createPlan took ${((Date.now() - t) / 1000).toFixed(1)}s${elapsed()}`);
|
|
418
|
+
council = councilStore.get(councilId)!;
|
|
419
|
+
phase = council.deliberationState?.currentPhase;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (phase === 'paused' || phase === 'failed') return;
|
|
423
|
+
|
|
424
|
+
if (phase === 'directing') {
|
|
425
|
+
const t = Date.now();
|
|
426
|
+
console.log('[Orchestrator] Issuing directive...');
|
|
427
|
+
await this.issueDirective(council);
|
|
428
|
+
console.log(`[Orchestrator:Timing] issueDirective took ${((Date.now() - t) / 1000).toFixed(1)}s${elapsed()}`);
|
|
429
|
+
council = councilStore.get(councilId)!;
|
|
430
|
+
phase = council.deliberationState?.currentPhase;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Phase 4: Execution loop
|
|
434
|
+
const maxRevisions = council.deliberation?.maxRevisions || 3;
|
|
435
|
+
let revisionCount = council.deliberationState?.revisionCount || 0;
|
|
436
|
+
|
|
437
|
+
while (revisionCount < maxRevisions + 1) {
|
|
438
|
+
council = councilStore.get(councilId)!;
|
|
439
|
+
phase = council.deliberationState?.currentPhase;
|
|
440
|
+
|
|
441
|
+
if (phase === 'paused' || phase === 'failed') {
|
|
442
|
+
console.log('[Orchestrator] Deliberation paused/failed — stopping auto-run');
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (phase === 'executing') {
|
|
447
|
+
const t = Date.now();
|
|
448
|
+
console.log('[Orchestrator] Executing work...');
|
|
449
|
+
await this.executeWork(council);
|
|
450
|
+
console.log(`[Orchestrator:Timing] executeWork took ${((Date.now() - t) / 1000).toFixed(1)}s${elapsed()}`);
|
|
451
|
+
} else if (phase === 'reviewing') {
|
|
452
|
+
const t = Date.now();
|
|
453
|
+
console.log('[Orchestrator] Reviewing work...');
|
|
454
|
+
const { review } = await this.reviewWork(council);
|
|
455
|
+
console.log(`[Orchestrator:Timing] reviewWork took ${((Date.now() - t) / 1000).toFixed(1)}s${elapsed()}`);
|
|
456
|
+
|
|
457
|
+
if (review.verdict === 'accept') {
|
|
458
|
+
console.log('[Orchestrator] Work approved!');
|
|
459
|
+
break;
|
|
460
|
+
} else if (review.verdict === 'revise') {
|
|
461
|
+
revisionCount++;
|
|
462
|
+
console.log(`[Orchestrator] Revision requested (${revisionCount}/${maxRevisions})`);
|
|
463
|
+
} else if (review.verdict === 're_deliberate') {
|
|
464
|
+
const reDelibCount = council.deliberationState?.reDeliberationCount || 0;
|
|
465
|
+
if (reDelibCount >= 1) {
|
|
466
|
+
console.log('[Orchestrator] Max re-deliberations reached — accepting with best effort');
|
|
467
|
+
this.transitionPhase(councilId, 'completed');
|
|
468
|
+
councilStore.setStatus(councilId, 'resolved');
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
console.log('[Orchestrator] Re-deliberation requested — looping back');
|
|
472
|
+
councilStore.updateDeliberationState(councilId, {
|
|
473
|
+
reDeliberationCount: reDelibCount + 1,
|
|
474
|
+
});
|
|
475
|
+
// Phase transitions back to round_interactive, re-enter deliberation loop
|
|
476
|
+
await this.continueDeliberation(councilId);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
} else if (phase === 'revising') {
|
|
480
|
+
console.log('[Orchestrator] Revising work...');
|
|
481
|
+
await this.requestRevision(council);
|
|
482
|
+
} else if (phase === 'completed') {
|
|
483
|
+
console.log('[Orchestrator] Deliberation completed!');
|
|
484
|
+
break;
|
|
485
|
+
} else {
|
|
486
|
+
console.log(`[Orchestrator] Unexpected phase during execution: ${phase}`);
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Success — break out of the phase-level retry loop
|
|
492
|
+
break;
|
|
493
|
+
|
|
494
|
+
} catch (error) {
|
|
495
|
+
const errMsg = (error as Error).message || String(error);
|
|
496
|
+
const isRateLimit = /\b(429|rate.limit|too many requests)\b/i.test(errMsg);
|
|
497
|
+
|
|
498
|
+
council = councilStore.get(councilId)!;
|
|
499
|
+
const errPhase = council.deliberationState?.currentPhase;
|
|
500
|
+
|
|
501
|
+
if (isRateLimit && phaseAttempt < PHASE_RETRY_MAX) {
|
|
502
|
+
const delayMs = PHASE_RETRY_BASE_MS * Math.pow(1.5, phaseAttempt);
|
|
503
|
+
console.warn(
|
|
504
|
+
`[Orchestrator] Rate limit during ${errPhase}, retrying phase in ${Math.round(delayMs / 1000)}s ` +
|
|
505
|
+
`(phase attempt ${phaseAttempt + 1}/${PHASE_RETRY_MAX})`
|
|
506
|
+
);
|
|
507
|
+
this.createEntry(
|
|
508
|
+
councilId, 'manager', this.getManager(council).id,
|
|
509
|
+
'error', errPhase || 'failed',
|
|
510
|
+
`Rate limited during ${errPhase} — waiting ${Math.round(delayMs / 1000)}s before retrying...`,
|
|
511
|
+
0, 0, undefined, council.deliberationState?.currentRound
|
|
512
|
+
);
|
|
513
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
514
|
+
continue; // retry from current phase
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
console.error(`[Orchestrator] Error during post-round phase:`, errMsg);
|
|
518
|
+
|
|
519
|
+
// Record error in ledger — attribute to the phase's active role
|
|
520
|
+
try {
|
|
521
|
+
const isWorkerPhase = errPhase === 'executing' || errPhase === 'revising';
|
|
522
|
+
const role = isWorkerPhase ? 'worker' : 'manager';
|
|
523
|
+
const persona = isWorkerPhase
|
|
524
|
+
? (council.personas.find(p => p.preferredDeliberationRole === 'worker') || this.getManager(council))
|
|
525
|
+
: this.getManager(council);
|
|
526
|
+
this.createEntry(
|
|
527
|
+
councilId,
|
|
528
|
+
role,
|
|
529
|
+
persona.id,
|
|
530
|
+
'error',
|
|
531
|
+
errPhase || 'failed',
|
|
532
|
+
`Deliberation error during ${errPhase}: ${errMsg}`,
|
|
533
|
+
0, 0, undefined,
|
|
534
|
+
council.deliberationState?.currentRound
|
|
535
|
+
);
|
|
536
|
+
} catch { /* best effort */ }
|
|
537
|
+
|
|
538
|
+
this.transitionPhase(councilId, 'failed');
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
} // end phase-level retry loop
|
|
542
|
+
|
|
543
|
+
console.log(`[Orchestrator] Auto-run finished${elapsed()}`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ==========================================================================
|
|
547
|
+
// Phase 1: Problem Framing
|
|
548
|
+
// ==========================================================================
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Manager frames the problem, creating Context v1
|
|
552
|
+
*/
|
|
553
|
+
async frameProblem(council: Council, rawProblem: string): Promise<LedgerEntry> {
|
|
554
|
+
this.validatePhase(council, 'created');
|
|
555
|
+
const manager = this.getManager(council);
|
|
556
|
+
|
|
557
|
+
// Transition to problem_framing
|
|
558
|
+
this.transitionPhase(council.id, 'problem_framing');
|
|
559
|
+
|
|
560
|
+
// Show manager as thinking immediately (before bootstrap, which can be slow)
|
|
561
|
+
this.config.onAgentThinkingStart?.(manager, Date.now(), rawProblem);
|
|
562
|
+
|
|
563
|
+
// Bootstrap directory context if enabled — stored for all personas
|
|
564
|
+
let enrichedProblem = rawProblem;
|
|
565
|
+
if (council.deliberation?.bootstrapContext && council.deliberation?.workingDirectory) {
|
|
566
|
+
try {
|
|
567
|
+
const dirContext = await bootstrapDirectoryContext(council.deliberation.workingDirectory, { deep: true });
|
|
568
|
+
if (dirContext) {
|
|
569
|
+
this.bootstrappedContext = dirContext;
|
|
570
|
+
enrichedProblem = `${dirContext}\n\n---\n\n${rawProblem}`;
|
|
571
|
+
}
|
|
572
|
+
} catch (error) {
|
|
573
|
+
console.warn('[Orchestrator] Directory context bootstrap failed:', error);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Build prompts per Section 9.1
|
|
578
|
+
const systemPrompt = manager.predisposition.systemPrompt;
|
|
579
|
+
const userMessage = buildManagerFramingPrompt(enrichedProblem);
|
|
580
|
+
|
|
581
|
+
// Note: onAgentThinkingStart was already called above (before bootstrap).
|
|
582
|
+
// invokeAgentSafe will call it again (idempotent) and onAgentThinkingEnd when done.
|
|
583
|
+
|
|
584
|
+
// Invoke manager
|
|
585
|
+
const response = await this.invokeAgentSafe(
|
|
586
|
+
{ personaId: manager.id, systemPrompt, userMessage },
|
|
587
|
+
manager,
|
|
588
|
+
'problem_framing'
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
// Create Context v1
|
|
592
|
+
const context = createInitialContext(council.id, response.content, manager.id);
|
|
593
|
+
|
|
594
|
+
// Update deliberation state
|
|
595
|
+
councilStore.setActiveContext(council.id, context.id, context.version);
|
|
596
|
+
|
|
597
|
+
// Create ledger entry
|
|
598
|
+
const entry = this.createEntry(
|
|
599
|
+
council.id,
|
|
600
|
+
'manager',
|
|
601
|
+
manager.id,
|
|
602
|
+
'problem_statement',
|
|
603
|
+
'problem_framing',
|
|
604
|
+
response.content,
|
|
605
|
+
response.tokensUsed,
|
|
606
|
+
response.latencyMs,
|
|
607
|
+
[{ artifactType: 'context', artifactId: context.id, version: 1 }],
|
|
608
|
+
undefined,
|
|
609
|
+
response.structured
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
// Check if consultants exist — skip deliberation rounds if none
|
|
613
|
+
const consultants = this.getConsultants(council);
|
|
614
|
+
if (consultants.length === 0) {
|
|
615
|
+
console.log('[Orchestrator] 0 consultants — skipping deliberation rounds, moving to deciding');
|
|
616
|
+
this.transitionPhase(council.id, 'deciding');
|
|
617
|
+
} else {
|
|
618
|
+
// Transition to round_independent
|
|
619
|
+
this.transitionPhase(council.id, 'round_independent');
|
|
620
|
+
councilStore.advanceRound(council.id);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return entry;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ==========================================================================
|
|
627
|
+
// Phase 2: Deliberation Rounds
|
|
628
|
+
// ==========================================================================
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Generate independent analysis for all consultants (Round 1)
|
|
632
|
+
*/
|
|
633
|
+
async runIndependentRound(council: Council): Promise<LedgerEntry[]> {
|
|
634
|
+
this.validatePhase(council, 'round_independent');
|
|
635
|
+
const consultants = this.getConsultants(council);
|
|
636
|
+
const context = getCurrentContext(council.id);
|
|
637
|
+
|
|
638
|
+
// Detailed logging to debug missing consultant participation
|
|
639
|
+
const allRoleAssignments = council.deliberation?.roleAssignments || [];
|
|
640
|
+
const consultantRoles = allRoleAssignments.filter(r => r.role === 'consultant');
|
|
641
|
+
console.log(`[Orchestrator] Independent round: ${consultants.length} consultant(s) ` +
|
|
642
|
+
`from ${consultantRoles.length} consultant role assignment(s)`,
|
|
643
|
+
{
|
|
644
|
+
consultantNames: consultants.map(c => `${c.name} (${c.id.slice(0, 8)})`),
|
|
645
|
+
consultantRoleIds: consultantRoles.map(r => r.personaId.slice(0, 8)),
|
|
646
|
+
allPersonaIds: council.personas.map(p => `${p.name}:${p.id.slice(0, 8)}`),
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
if (!context) {
|
|
650
|
+
throw new Error('No context found - problem must be framed first');
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (consultants.length === 0) {
|
|
654
|
+
console.warn('[Orchestrator] No consultants assigned - skipping independent round');
|
|
655
|
+
this.transitionPhase(council.id, 'round_waiting_for_manager');
|
|
656
|
+
return [];
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const entries: LedgerEntry[] = [];
|
|
660
|
+
|
|
661
|
+
const runOne = async (consultant: Persona): Promise<LedgerEntry> => {
|
|
662
|
+
const assignment = getRoleAssignment(council, consultant.id);
|
|
663
|
+
const focusArea = assignment?.focusArea || consultant.predisposition.domain || 'general';
|
|
664
|
+
|
|
665
|
+
const systemPrompt = consultant.predisposition.systemPrompt;
|
|
666
|
+
const userMessage = buildIndependentAnalysisPrompt(consultant, focusArea, context.content);
|
|
667
|
+
|
|
668
|
+
const response = await this.invokeAgentSafe(
|
|
669
|
+
{ personaId: consultant.id, systemPrompt, userMessage },
|
|
670
|
+
consultant,
|
|
671
|
+
'independent_analysis'
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
const proposedPatch = this.extractContextProposal(response.content);
|
|
675
|
+
|
|
676
|
+
const entry = this.createEntry(
|
|
677
|
+
council.id,
|
|
678
|
+
'consultant',
|
|
679
|
+
consultant.id,
|
|
680
|
+
'analysis',
|
|
681
|
+
'round_independent',
|
|
682
|
+
response.content,
|
|
683
|
+
response.tokensUsed,
|
|
684
|
+
response.latencyMs,
|
|
685
|
+
undefined,
|
|
686
|
+
council.deliberationState?.currentRound,
|
|
687
|
+
response.structured
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
councilStore.recordSubmission(council.id, consultant.id);
|
|
691
|
+
|
|
692
|
+
// Evolve context with consultant findings
|
|
693
|
+
this.appendToContext(council.id, 'Consultant Analysis', consultant.name, response.content, 'consultant', consultant.id);
|
|
694
|
+
|
|
695
|
+
if (proposedPatch) {
|
|
696
|
+
const patch = createPatch(
|
|
697
|
+
council.id,
|
|
698
|
+
context.id,
|
|
699
|
+
context.version,
|
|
700
|
+
proposedPatch.diff,
|
|
701
|
+
proposedPatch.rationale,
|
|
702
|
+
consultant.id,
|
|
703
|
+
council.deliberationState?.currentRound || 1
|
|
704
|
+
);
|
|
705
|
+
councilStore.addPendingPatch(council.id, patch.id);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return entry;
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
// Respect consultantExecution setting
|
|
712
|
+
const isSequential = council.deliberation?.consultantExecution === 'sequential';
|
|
713
|
+
|
|
714
|
+
if (isSequential) {
|
|
715
|
+
// Sequential: run one at a time, each with retry
|
|
716
|
+
for (const consultant of consultants) {
|
|
717
|
+
const entry = await this.runConsultantWithRetry(council, consultant, () => runOne(consultant));
|
|
718
|
+
if (entry) entries.push(entry);
|
|
719
|
+
}
|
|
720
|
+
} else {
|
|
721
|
+
// Parallel: all at once, each with retry
|
|
722
|
+
const results = await Promise.allSettled(
|
|
723
|
+
consultants.map((consultant) =>
|
|
724
|
+
this.runConsultantWithRetry(council, consultant, () => runOne(consultant))
|
|
725
|
+
)
|
|
726
|
+
);
|
|
727
|
+
for (const result of results) {
|
|
728
|
+
if (result.status === 'fulfilled' && result.value) {
|
|
729
|
+
entries.push(result.value);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Check if round is complete (all consultants submitted or accounted for)
|
|
735
|
+
if (councilStore.isRoundComplete(council.id)) {
|
|
736
|
+
this.transitionPhase(council.id, 'round_waiting_for_manager');
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return entries;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Generate deliberation response for a consultant (Round 2+)
|
|
744
|
+
*/
|
|
745
|
+
async generateDeliberationResponse(council: Council, consultantId: string): Promise<LedgerEntry> {
|
|
746
|
+
this.validatePhase(council, 'round_interactive');
|
|
747
|
+
const consultant = council.personas.find((p) => p.id === consultantId);
|
|
748
|
+
|
|
749
|
+
if (!consultant) {
|
|
750
|
+
throw new Error(`Consultant not found: ${consultantId}`);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const assignment = getRoleAssignment(council, consultant.id);
|
|
754
|
+
const focusArea = assignment?.focusArea || consultant.predisposition.domain || 'general';
|
|
755
|
+
|
|
756
|
+
// Build context per Section 9.3
|
|
757
|
+
// In sequential mode, include current round entries so later consultants
|
|
758
|
+
// can see and build on earlier consultants' responses from this round
|
|
759
|
+
const isSequential = council.deliberation?.consultantExecution === 'sequential';
|
|
760
|
+
const contextContent = this.buildRoundNContext(council, isSequential);
|
|
761
|
+
|
|
762
|
+
// Build prompts
|
|
763
|
+
const systemPrompt = consultant.predisposition.systemPrompt;
|
|
764
|
+
const userMessage = buildDeliberationResponsePrompt(consultant, focusArea, contextContent);
|
|
765
|
+
|
|
766
|
+
const response = await this.invokeAgentSafe(
|
|
767
|
+
{ personaId: consultant.id, systemPrompt, userMessage },
|
|
768
|
+
consultant,
|
|
769
|
+
'deliberation_response'
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
// Check for context change proposal
|
|
773
|
+
const proposedPatch = this.extractContextProposal(response.content);
|
|
774
|
+
const context = getCurrentContext(council.id);
|
|
775
|
+
|
|
776
|
+
// Create ledger entry
|
|
777
|
+
const entryType: LedgerEntryType = proposedPatch ? 'proposal' : 'response';
|
|
778
|
+
const entry = this.createEntry(
|
|
779
|
+
council.id,
|
|
780
|
+
'consultant',
|
|
781
|
+
consultant.id,
|
|
782
|
+
entryType,
|
|
783
|
+
'round_interactive',
|
|
784
|
+
response.content,
|
|
785
|
+
response.tokensUsed,
|
|
786
|
+
response.latencyMs,
|
|
787
|
+
undefined,
|
|
788
|
+
council.deliberationState?.currentRound,
|
|
789
|
+
response.structured
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
// Record submission
|
|
793
|
+
councilStore.recordSubmission(council.id, consultant.id);
|
|
794
|
+
|
|
795
|
+
// Create patch if proposed
|
|
796
|
+
if (proposedPatch && context) {
|
|
797
|
+
const patch = createPatch(
|
|
798
|
+
council.id,
|
|
799
|
+
context.id,
|
|
800
|
+
context.version,
|
|
801
|
+
proposedPatch.diff,
|
|
802
|
+
proposedPatch.rationale,
|
|
803
|
+
consultant.id,
|
|
804
|
+
council.deliberationState?.currentRound || 1
|
|
805
|
+
);
|
|
806
|
+
councilStore.addPendingPatch(council.id, patch.id);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return entry;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Run a full interactive round for all consultants
|
|
814
|
+
*/
|
|
815
|
+
async runInteractiveRound(council: Council): Promise<LedgerEntry[]> {
|
|
816
|
+
this.validatePhase(council, 'round_interactive');
|
|
817
|
+
const consultants = this.getConsultants(council);
|
|
818
|
+
const entries: LedgerEntry[] = [];
|
|
819
|
+
|
|
820
|
+
// Detailed logging to debug missing consultant participation
|
|
821
|
+
const allRoleAssignments = council.deliberation?.roleAssignments || [];
|
|
822
|
+
const consultantRoles = allRoleAssignments.filter(r => r.role === 'consultant');
|
|
823
|
+
console.log(`[Orchestrator] Interactive round ${council.deliberationState?.currentRound}: ` +
|
|
824
|
+
`${consultants.length} consultant(s) from ${consultantRoles.length} consultant role assignment(s)`,
|
|
825
|
+
{
|
|
826
|
+
consultantNames: consultants.map(c => `${c.name} (${c.id.slice(0, 8)})`),
|
|
827
|
+
consultantRoleIds: consultantRoles.map(r => r.personaId.slice(0, 8)),
|
|
828
|
+
allPersonaIds: council.personas.map(p => `${p.name}:${p.id.slice(0, 8)}`),
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
if (consultants.length === 0) {
|
|
832
|
+
console.warn('[Orchestrator] No consultants assigned - skipping interactive round');
|
|
833
|
+
this.transitionPhase(council.id, 'round_waiting_for_manager');
|
|
834
|
+
return [];
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Respect consultantExecution setting
|
|
838
|
+
const isSequential = council.deliberation?.consultantExecution === 'sequential';
|
|
839
|
+
|
|
840
|
+
if (isSequential) {
|
|
841
|
+
// Sequential: each consultant sees previous consultants' responses in this round
|
|
842
|
+
for (const consultant of consultants) {
|
|
843
|
+
const entry = await this.runConsultantWithRetry(
|
|
844
|
+
council,
|
|
845
|
+
consultant,
|
|
846
|
+
() => this.generateDeliberationResponse(councilStore.get(council.id)!, consultant.id)
|
|
847
|
+
);
|
|
848
|
+
if (entry) entries.push(entry);
|
|
849
|
+
}
|
|
850
|
+
} else {
|
|
851
|
+
// Parallel: all consultants respond simultaneously with same context
|
|
852
|
+
const results = await Promise.allSettled(
|
|
853
|
+
consultants.map((consultant) =>
|
|
854
|
+
this.runConsultantWithRetry(
|
|
855
|
+
council,
|
|
856
|
+
consultant,
|
|
857
|
+
() => this.generateDeliberationResponse(council, consultant.id)
|
|
858
|
+
)
|
|
859
|
+
)
|
|
860
|
+
);
|
|
861
|
+
for (const result of results) {
|
|
862
|
+
if (result.status === 'fulfilled' && result.value) {
|
|
863
|
+
entries.push(result.value);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Check if round is complete (all consultants submitted or accounted for)
|
|
869
|
+
const updatedCouncil = councilStore.get(council.id);
|
|
870
|
+
if (updatedCouncil && councilStore.isRoundComplete(council.id)) {
|
|
871
|
+
this.transitionPhase(council.id, 'round_waiting_for_manager');
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return entries;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Manager evaluates the round
|
|
879
|
+
*/
|
|
880
|
+
async managerEvaluate(council: Council): Promise<ManagerEvaluation> {
|
|
881
|
+
this.validatePhase(council, 'round_waiting_for_manager');
|
|
882
|
+
const manager = this.getManager(council);
|
|
883
|
+
|
|
884
|
+
// Build context per Section 9.4
|
|
885
|
+
const evalContext = this.buildManagerEvalContext(council);
|
|
886
|
+
const pendingPatches = getPendingPatches(council.id);
|
|
887
|
+
const expectedOutput = council.deliberation?.expectedOutput;
|
|
888
|
+
|
|
889
|
+
// Build prompts
|
|
890
|
+
const systemPrompt = manager.predisposition.systemPrompt;
|
|
891
|
+
const userMessage = buildManagerEvaluationPrompt(evalContext, pendingPatches, expectedOutput);
|
|
892
|
+
|
|
893
|
+
const response = await this.invokeAgentSafe(
|
|
894
|
+
{ personaId: manager.id, systemPrompt, userMessage },
|
|
895
|
+
manager,
|
|
896
|
+
'manager_evaluation'
|
|
897
|
+
);
|
|
898
|
+
|
|
899
|
+
// Parse structured response
|
|
900
|
+
const evaluation = this.parseManagerEvaluation(response.content);
|
|
901
|
+
|
|
902
|
+
// Process patch decisions
|
|
903
|
+
if (evaluation.patchDecisions) {
|
|
904
|
+
for (const decision of evaluation.patchDecisions) {
|
|
905
|
+
const patch = getPendingPatches(council.id).find((p) => p.id === decision.patchId);
|
|
906
|
+
if (!patch) {
|
|
907
|
+
console.warn(`[Deliberation] Patch not found: ${decision.patchId}`);
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (decision.accepted) {
|
|
912
|
+
const currentContext = getCurrentContext(council.id);
|
|
913
|
+
|
|
914
|
+
// Check for staleness - patch was proposed against an older context version
|
|
915
|
+
const isStale = currentContext ? patch.baseVersion < currentContext.version : false;
|
|
916
|
+
|
|
917
|
+
if (isStale) {
|
|
918
|
+
// For stale patches, we need to either:
|
|
919
|
+
// 1. Reject and ask for rebase
|
|
920
|
+
// 2. Apply with allowStale (if manager explicitly approves)
|
|
921
|
+
// For now, we log a warning and allow with explicit flag
|
|
922
|
+
console.warn(
|
|
923
|
+
`[Deliberation] Accepting stale patch ${patch.id}: ` +
|
|
924
|
+
`proposed against v${patch.baseVersion}, current is v${currentContext!.version}`
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Intelligently integrate the patch into the current context
|
|
929
|
+
// The patch.diff contains the proposed change/addition
|
|
930
|
+
// Instead of naive append, use the manager's decision reason as context
|
|
931
|
+
const integrationNote = isStale
|
|
932
|
+
? `[Context change from ${this.getPersonaDisplayName(council, patch.authorPersonaId)}, ` +
|
|
933
|
+
`originally proposed against v${patch.baseVersion}, integrated at v${currentContext?.version || 1}]`
|
|
934
|
+
: `[Context change from ${this.getPersonaDisplayName(council, patch.authorPersonaId)}]`;
|
|
935
|
+
|
|
936
|
+
const newContent = currentContext
|
|
937
|
+
? `${currentContext.content}\n\n${integrationNote}:\n${patch.diff}`
|
|
938
|
+
: patch.diff;
|
|
939
|
+
|
|
940
|
+
const result = acceptPatch(
|
|
941
|
+
council.id,
|
|
942
|
+
decision.patchId,
|
|
943
|
+
manager.id,
|
|
944
|
+
decision.reason,
|
|
945
|
+
newContent,
|
|
946
|
+
{
|
|
947
|
+
allowStale: true, // Manager made explicit decision to accept
|
|
948
|
+
changeSummary: `${patch.rationale}${isStale ? ' (applied to newer context)' : ''}`,
|
|
949
|
+
}
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
councilStore.removePendingPatch(council.id, decision.patchId);
|
|
953
|
+
councilStore.setActiveContext(council.id, result.newContext.id, result.newContext.version);
|
|
954
|
+
|
|
955
|
+
// Create acceptance ledger entry with staleness info
|
|
956
|
+
this.createEntry(
|
|
957
|
+
council.id,
|
|
958
|
+
'manager',
|
|
959
|
+
manager.id,
|
|
960
|
+
'context_acceptance',
|
|
961
|
+
'round_waiting_for_manager',
|
|
962
|
+
`Accepted context change: ${decision.reason}${result.wasStale ? ' (applied to newer context version)' : ''}`,
|
|
963
|
+
0, 0,
|
|
964
|
+
[{ artifactType: 'context', artifactId: result.newContext.id, version: result.newContext.version }]
|
|
965
|
+
);
|
|
966
|
+
} else {
|
|
967
|
+
rejectPatch(council.id, decision.patchId, manager.id, decision.reason);
|
|
968
|
+
councilStore.removePendingPatch(council.id, decision.patchId);
|
|
969
|
+
|
|
970
|
+
// Create rejection ledger entry
|
|
971
|
+
this.createEntry(
|
|
972
|
+
council.id,
|
|
973
|
+
'manager',
|
|
974
|
+
manager.id,
|
|
975
|
+
'context_rejection',
|
|
976
|
+
'round_waiting_for_manager',
|
|
977
|
+
`Rejected context change: ${decision.reason}`,
|
|
978
|
+
0, 0
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Store evaluation
|
|
985
|
+
councilStore.setManagerEvaluation(council.id, evaluation);
|
|
986
|
+
|
|
987
|
+
// Handle action
|
|
988
|
+
const currentRound = council.deliberationState?.currentRound || 1;
|
|
989
|
+
const minRounds = council.deliberation?.minRounds || 1;
|
|
990
|
+
const maxRounds = council.deliberation?.maxRounds || 4;
|
|
991
|
+
|
|
992
|
+
// Enforce min rounds: if manager wants to decide but we haven't hit minRounds, continue instead
|
|
993
|
+
const canDecide = currentRound >= minRounds;
|
|
994
|
+
const mustDecide = currentRound >= maxRounds;
|
|
995
|
+
|
|
996
|
+
if (mustDecide || (evaluation.action === 'decide' && canDecide)) {
|
|
997
|
+
this.transitionPhase(council.id, 'deciding');
|
|
998
|
+
} else if (!canDecide && evaluation.action === 'decide') {
|
|
999
|
+
// Manager tried to decide too early — force another round
|
|
1000
|
+
console.log(`[Orchestrator] Manager wanted to decide at round ${currentRound}, but minRounds is ${minRounds}. Continuing.`);
|
|
1001
|
+
councilStore.advanceRound(council.id);
|
|
1002
|
+
this.transitionPhase(council.id, 'round_interactive');
|
|
1003
|
+
} else if (evaluation.action === 'continue' || evaluation.action === 'redirect') {
|
|
1004
|
+
// Create question/redirect entry if provided
|
|
1005
|
+
if (evaluation.question) {
|
|
1006
|
+
const entryType: LedgerEntryType = evaluation.action === 'redirect' ? 'manager_redirect' : 'manager_question';
|
|
1007
|
+
this.createEntry(
|
|
1008
|
+
council.id,
|
|
1009
|
+
'manager',
|
|
1010
|
+
manager.id,
|
|
1011
|
+
entryType,
|
|
1012
|
+
'round_waiting_for_manager',
|
|
1013
|
+
evaluation.question,
|
|
1014
|
+
response.tokensUsed,
|
|
1015
|
+
response.latencyMs,
|
|
1016
|
+
undefined,
|
|
1017
|
+
currentRound,
|
|
1018
|
+
response.structured
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Generate summary if needed
|
|
1023
|
+
if (this.shouldSummarize(council)) {
|
|
1024
|
+
await this.generateRoundSummary(council);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Advance to next round
|
|
1028
|
+
councilStore.advanceRound(council.id);
|
|
1029
|
+
this.transitionPhase(council.id, 'round_interactive');
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
return evaluation;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Generate round summary
|
|
1037
|
+
*/
|
|
1038
|
+
async generateRoundSummary(council: Council): Promise<LedgerEntry> {
|
|
1039
|
+
const manager = this.getManager(council);
|
|
1040
|
+
const currentRound = council.deliberationState?.currentRound || 1;
|
|
1041
|
+
const roundEntries = getEntriesForRound(council.id, currentRound);
|
|
1042
|
+
|
|
1043
|
+
let summaryContent: string;
|
|
1044
|
+
let tokensUsed = 0;
|
|
1045
|
+
let latencyMs = 0;
|
|
1046
|
+
let summaryStructured: Record<string, unknown> | undefined;
|
|
1047
|
+
|
|
1048
|
+
const summaryMode = council.deliberation?.summaryMode || 'manager';
|
|
1049
|
+
const summarizeAfterRound = council.deliberation?.summarizeAfterRound || 2;
|
|
1050
|
+
|
|
1051
|
+
// Determine whether to use automatic or manager summary
|
|
1052
|
+
const useAutomatic = summaryMode === 'automatic' ||
|
|
1053
|
+
(summaryMode === 'hybrid' && currentRound <= summarizeAfterRound + 1);
|
|
1054
|
+
|
|
1055
|
+
if (useAutomatic) {
|
|
1056
|
+
summaryContent = buildMechanicalSummary(roundEntries);
|
|
1057
|
+
} else {
|
|
1058
|
+
// Manager-generated summary
|
|
1059
|
+
const systemPrompt = manager.predisposition.systemPrompt;
|
|
1060
|
+
const userMessage = buildManagerRoundSummaryPrompt(roundEntries);
|
|
1061
|
+
|
|
1062
|
+
const response = await this.invokeAgentSafe(
|
|
1063
|
+
{ personaId: manager.id, systemPrompt, userMessage },
|
|
1064
|
+
manager,
|
|
1065
|
+
'round_summary'
|
|
1066
|
+
);
|
|
1067
|
+
|
|
1068
|
+
summaryContent = response.content;
|
|
1069
|
+
tokensUsed = response.tokensUsed;
|
|
1070
|
+
latencyMs = response.latencyMs;
|
|
1071
|
+
summaryStructured = response.structured;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Store summary
|
|
1075
|
+
councilStore.setRoundSummary(council.id, currentRound, summaryContent);
|
|
1076
|
+
|
|
1077
|
+
// Create ledger entry
|
|
1078
|
+
return this.createEntry(
|
|
1079
|
+
council.id,
|
|
1080
|
+
'manager',
|
|
1081
|
+
manager.id,
|
|
1082
|
+
'round_summary',
|
|
1083
|
+
council.deliberationState?.currentPhase || 'round_waiting_for_manager',
|
|
1084
|
+
summaryContent,
|
|
1085
|
+
tokensUsed,
|
|
1086
|
+
latencyMs,
|
|
1087
|
+
undefined,
|
|
1088
|
+
currentRound,
|
|
1089
|
+
summaryStructured
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// ==========================================================================
|
|
1094
|
+
// Consultant Final Positions (before decision)
|
|
1095
|
+
// ==========================================================================
|
|
1096
|
+
|
|
1097
|
+
private async collectConsultantFinalPositions(council: Council): Promise<string> {
|
|
1098
|
+
const consultants = this.getConsultants(council);
|
|
1099
|
+
if (consultants.length === 0) return '';
|
|
1100
|
+
|
|
1101
|
+
const contextContent = this.buildRoundNContext(council, false);
|
|
1102
|
+
const positions: string[] = [];
|
|
1103
|
+
|
|
1104
|
+
for (const consultant of consultants) {
|
|
1105
|
+
const assignment = getRoleAssignment(council, consultant.id);
|
|
1106
|
+
const focusArea = assignment?.focusArea || consultant.predisposition.domain || 'general';
|
|
1107
|
+
const systemPrompt = consultant.predisposition.systemPrompt;
|
|
1108
|
+
const userMessage = buildConsultantFinalPositionPrompt(consultant, focusArea, contextContent);
|
|
1109
|
+
|
|
1110
|
+
this.config.onAgentThinkingStart?.(consultant, Date.now(), userMessage);
|
|
1111
|
+
const response = await this.invokeAgentSafe(
|
|
1112
|
+
{ personaId: consultant.id, systemPrompt, userMessage },
|
|
1113
|
+
consultant,
|
|
1114
|
+
'deliberation_response' // reuse existing tool phase (READ_ONLY_TOOLS)
|
|
1115
|
+
);
|
|
1116
|
+
this.config.onAgentThinkingEnd?.(consultant);
|
|
1117
|
+
|
|
1118
|
+
positions.push(`[${consultant.name} - ${focusArea}]: ${response.content}`);
|
|
1119
|
+
|
|
1120
|
+
this.createEntry(council.id, 'consultant', consultant.id, 'response', 'deciding',
|
|
1121
|
+
response.content, response.tokensUsed, response.latencyMs, undefined,
|
|
1122
|
+
council.deliberationState?.currentRound, response.structured);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
return positions.join('\n\n');
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// ==========================================================================
|
|
1129
|
+
// Phase 3: Decision
|
|
1130
|
+
// ==========================================================================
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Manager makes final decision
|
|
1134
|
+
*/
|
|
1135
|
+
async makeDecision(council: Council): Promise<LedgerEntry> {
|
|
1136
|
+
this.validatePhase(council, 'deciding');
|
|
1137
|
+
const manager = this.getManager(council);
|
|
1138
|
+
|
|
1139
|
+
// Collect consultant final positions before manager decides
|
|
1140
|
+
const consultantPositions = await this.collectConsultantFinalPositions(council);
|
|
1141
|
+
|
|
1142
|
+
// Build context per Section 9.5
|
|
1143
|
+
const decisionContext = this.buildDecisionContext(council);
|
|
1144
|
+
const fullContext = consultantPositions
|
|
1145
|
+
? `CONSULTANT FINAL POSITIONS:\n${consultantPositions}\n\n---\n\n${decisionContext}`
|
|
1146
|
+
: decisionContext;
|
|
1147
|
+
const decisionCriteria = council.deliberation?.decisionCriteria;
|
|
1148
|
+
const expectedOutput = council.deliberation?.expectedOutput;
|
|
1149
|
+
|
|
1150
|
+
// Build prompts
|
|
1151
|
+
const systemPrompt = manager.predisposition.systemPrompt;
|
|
1152
|
+
const stepType = council.deliberation?.stepType;
|
|
1153
|
+
const userMessage = buildManagerDecisionPrompt(fullContext, decisionCriteria, expectedOutput, stepType);
|
|
1154
|
+
|
|
1155
|
+
const response = await this.invokeAgentSafe(
|
|
1156
|
+
{ personaId: manager.id, systemPrompt, userMessage },
|
|
1157
|
+
manager,
|
|
1158
|
+
'decision'
|
|
1159
|
+
);
|
|
1160
|
+
|
|
1161
|
+
// Create decision artifact
|
|
1162
|
+
const context = getCurrentContext(council.id);
|
|
1163
|
+
const acceptanceCriteria = this.extractAcceptanceCriteria(response.content);
|
|
1164
|
+
|
|
1165
|
+
const decision = createDecision(
|
|
1166
|
+
council.id,
|
|
1167
|
+
response.content,
|
|
1168
|
+
context?.version || 1,
|
|
1169
|
+
acceptanceCriteria
|
|
1170
|
+
);
|
|
1171
|
+
|
|
1172
|
+
// Update state
|
|
1173
|
+
councilStore.setFinalDecision(council.id, decision.id);
|
|
1174
|
+
|
|
1175
|
+
// Create ledger entry
|
|
1176
|
+
const entry = this.createEntry(
|
|
1177
|
+
council.id,
|
|
1178
|
+
'manager',
|
|
1179
|
+
manager.id,
|
|
1180
|
+
'decision',
|
|
1181
|
+
'deciding',
|
|
1182
|
+
response.content,
|
|
1183
|
+
response.tokensUsed,
|
|
1184
|
+
response.latencyMs,
|
|
1185
|
+
[{ artifactType: 'decision', artifactId: decision.id }],
|
|
1186
|
+
undefined,
|
|
1187
|
+
response.structured
|
|
1188
|
+
);
|
|
1189
|
+
|
|
1190
|
+
// Check if workers exist — if not, decision IS the output
|
|
1191
|
+
const workers = getPersonaByRole(council, 'worker');
|
|
1192
|
+
if (workers.length === 0) {
|
|
1193
|
+
console.log('[Orchestrator] 0 workers — decision is the final output');
|
|
1194
|
+
councilStore.updateDeliberationState(council.id, { completionSummary: response.content });
|
|
1195
|
+
this.transitionPhase(council.id, 'completed');
|
|
1196
|
+
councilStore.setStatus(council.id, 'resolved');
|
|
1197
|
+
return entry;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Transition to planning or directing
|
|
1201
|
+
const requirePlan = council.deliberation?.requirePlan || false;
|
|
1202
|
+
this.transitionPhase(council.id, requirePlan ? 'planning' : 'directing');
|
|
1203
|
+
|
|
1204
|
+
return entry;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* Manager creates execution plan (optional)
|
|
1209
|
+
*/
|
|
1210
|
+
async createPlan(council: Council): Promise<LedgerEntry> {
|
|
1211
|
+
this.validatePhase(council, 'planning');
|
|
1212
|
+
const manager = this.getManager(council);
|
|
1213
|
+
const decision = getDecision(council.id);
|
|
1214
|
+
|
|
1215
|
+
if (!decision) {
|
|
1216
|
+
throw new Error('No decision found - must make decision first');
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Build prompts
|
|
1220
|
+
const systemPrompt = manager.predisposition.systemPrompt;
|
|
1221
|
+
const userMessage = buildManagerPlanPrompt(decision.content);
|
|
1222
|
+
|
|
1223
|
+
const response = await this.invokeAgentSafe(
|
|
1224
|
+
{ personaId: manager.id, systemPrompt, userMessage },
|
|
1225
|
+
manager,
|
|
1226
|
+
'plan'
|
|
1227
|
+
);
|
|
1228
|
+
|
|
1229
|
+
// Create plan artifact
|
|
1230
|
+
const plan = createPlan(council.id, response.content, decision.id);
|
|
1231
|
+
|
|
1232
|
+
// Create ledger entry
|
|
1233
|
+
const entry = this.createEntry(
|
|
1234
|
+
council.id,
|
|
1235
|
+
'manager',
|
|
1236
|
+
manager.id,
|
|
1237
|
+
'plan',
|
|
1238
|
+
'planning',
|
|
1239
|
+
response.content,
|
|
1240
|
+
response.tokensUsed,
|
|
1241
|
+
response.latencyMs,
|
|
1242
|
+
[{ artifactType: 'plan', artifactId: plan.id }],
|
|
1243
|
+
undefined,
|
|
1244
|
+
response.structured
|
|
1245
|
+
);
|
|
1246
|
+
|
|
1247
|
+
this.transitionPhase(council.id, 'directing');
|
|
1248
|
+
return entry;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// ==========================================================================
|
|
1252
|
+
// Phase 4: Work Directive
|
|
1253
|
+
// ==========================================================================
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Manager issues work directive
|
|
1257
|
+
*/
|
|
1258
|
+
async issueDirective(council: Council): Promise<LedgerEntry> {
|
|
1259
|
+
this.validatePhase(council, 'directing');
|
|
1260
|
+
const manager = this.getManager(council);
|
|
1261
|
+
const decision = getDecision(council.id);
|
|
1262
|
+
const plan = getPlan(council.id);
|
|
1263
|
+
|
|
1264
|
+
if (!decision) {
|
|
1265
|
+
throw new Error('No decision found - must make decision first');
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// Check if the worker has write permissions (informs the directive to emphasize implementation)
|
|
1269
|
+
const worker = this.getWorker(council);
|
|
1270
|
+
const workerAssignment = getRoleAssignment(council, worker.id);
|
|
1271
|
+
const hasWritePermissions = !!(workerAssignment?.writePermissions);
|
|
1272
|
+
const stepType = council.deliberation?.stepType;
|
|
1273
|
+
|
|
1274
|
+
// Build prompts per Section 9.6
|
|
1275
|
+
const systemPrompt = manager.predisposition.systemPrompt;
|
|
1276
|
+
const userMessage = buildWorkDirectivePrompt(decision.content, plan?.content, hasWritePermissions, stepType);
|
|
1277
|
+
|
|
1278
|
+
const response = await this.invokeAgentSafe(
|
|
1279
|
+
{ personaId: manager.id, systemPrompt, userMessage },
|
|
1280
|
+
manager,
|
|
1281
|
+
'directive'
|
|
1282
|
+
);
|
|
1283
|
+
|
|
1284
|
+
// Create directive artifact
|
|
1285
|
+
const directive = createDirective(
|
|
1286
|
+
council.id,
|
|
1287
|
+
response.content,
|
|
1288
|
+
decision.id,
|
|
1289
|
+
plan?.id
|
|
1290
|
+
);
|
|
1291
|
+
|
|
1292
|
+
// Update state
|
|
1293
|
+
councilStore.setWorkDirective(council.id, directive.id);
|
|
1294
|
+
|
|
1295
|
+
// Create ledger entry
|
|
1296
|
+
const entry = this.createEntry(
|
|
1297
|
+
council.id,
|
|
1298
|
+
'manager',
|
|
1299
|
+
manager.id,
|
|
1300
|
+
'work_directive',
|
|
1301
|
+
'directing',
|
|
1302
|
+
response.content,
|
|
1303
|
+
response.tokensUsed,
|
|
1304
|
+
response.latencyMs,
|
|
1305
|
+
[{ artifactType: 'directive', artifactId: directive.id }],
|
|
1306
|
+
undefined,
|
|
1307
|
+
response.structured
|
|
1308
|
+
);
|
|
1309
|
+
|
|
1310
|
+
this.transitionPhase(council.id, 'executing');
|
|
1311
|
+
return entry;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// ==========================================================================
|
|
1315
|
+
// Phase 5: Execution
|
|
1316
|
+
// ==========================================================================
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* Worker executes directive
|
|
1320
|
+
*/
|
|
1321
|
+
async executeWork(council: Council): Promise<LedgerEntry> {
|
|
1322
|
+
this.validatePhase(council, 'executing');
|
|
1323
|
+
const worker = this.getWorker(council);
|
|
1324
|
+
const directive = getDirective(council.id);
|
|
1325
|
+
|
|
1326
|
+
if (!directive) {
|
|
1327
|
+
throw new Error('No directive found - must issue directive first');
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// Get role assignment for suppress check
|
|
1331
|
+
const assignment = getRoleAssignment(council, worker.id);
|
|
1332
|
+
const suppressPersona = assignment?.suppressPersona !== false; // Default true for worker
|
|
1333
|
+
|
|
1334
|
+
// Build worker permissions from role assignment + deliberation config
|
|
1335
|
+
const workerPermissions: WorkerPermissions = {
|
|
1336
|
+
writePermissions: assignment?.writePermissions,
|
|
1337
|
+
workingDirectory: council.deliberation?.workingDirectory,
|
|
1338
|
+
directoryConstrained: council.deliberation?.directoryConstrained,
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1341
|
+
// Build prompts per Section 9.7
|
|
1342
|
+
const stepType = council.deliberation?.stepType;
|
|
1343
|
+
const systemPrompt = suppressPersona
|
|
1344
|
+
? getMinimalWorkerSystemPrompt(workerPermissions, stepType)
|
|
1345
|
+
: worker.predisposition.systemPrompt;
|
|
1346
|
+
|
|
1347
|
+
const userMessage = buildWorkerExecutionPrompt(directive.content, workerPermissions, stepType);
|
|
1348
|
+
|
|
1349
|
+
const response = await this.invokeAgentSafe(
|
|
1350
|
+
{ personaId: worker.id, systemPrompt, userMessage },
|
|
1351
|
+
worker,
|
|
1352
|
+
'execution'
|
|
1353
|
+
);
|
|
1354
|
+
|
|
1355
|
+
// Create output artifact
|
|
1356
|
+
const output = createOutput(council.id, response.content, directive.id);
|
|
1357
|
+
|
|
1358
|
+
// Update state
|
|
1359
|
+
councilStore.setCurrentOutput(council.id, output.id);
|
|
1360
|
+
|
|
1361
|
+
// Create ledger entry
|
|
1362
|
+
const entry = this.createEntry(
|
|
1363
|
+
council.id,
|
|
1364
|
+
'worker',
|
|
1365
|
+
worker.id,
|
|
1366
|
+
'work_output',
|
|
1367
|
+
'executing',
|
|
1368
|
+
response.content,
|
|
1369
|
+
response.tokensUsed,
|
|
1370
|
+
response.latencyMs,
|
|
1371
|
+
[{ artifactType: 'output', artifactId: output.id, version: output.version }],
|
|
1372
|
+
undefined,
|
|
1373
|
+
response.structured
|
|
1374
|
+
);
|
|
1375
|
+
|
|
1376
|
+
// Evolve context with worker results
|
|
1377
|
+
this.appendToContext(council.id, 'Worker Execution', worker.name, response.content, 'worker', worker.id);
|
|
1378
|
+
|
|
1379
|
+
this.transitionPhase(council.id, 'reviewing');
|
|
1380
|
+
return entry;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// ==========================================================================
|
|
1384
|
+
// Consultant Reviews (before manager verdict)
|
|
1385
|
+
// ==========================================================================
|
|
1386
|
+
|
|
1387
|
+
private async collectConsultantReviews(council: Council): Promise<string> {
|
|
1388
|
+
const consultants = this.getConsultants(council);
|
|
1389
|
+
if (consultants.length === 0) return '';
|
|
1390
|
+
|
|
1391
|
+
const output = getLatestOutput(council.id);
|
|
1392
|
+
const directive = getDirective(council.id);
|
|
1393
|
+
if (!output || !directive) return '';
|
|
1394
|
+
|
|
1395
|
+
const expectedOutput = council.deliberation?.expectedOutput;
|
|
1396
|
+
|
|
1397
|
+
// Run consultant reviews in parallel to reduce total time
|
|
1398
|
+
const reviewOne = async (consultant: Persona): Promise<string> => {
|
|
1399
|
+
const assignment = getRoleAssignment(council, consultant.id);
|
|
1400
|
+
const focusArea = assignment?.focusArea || consultant.predisposition.domain || 'general';
|
|
1401
|
+
const systemPrompt = consultant.predisposition.systemPrompt;
|
|
1402
|
+
const userMessage = buildConsultantReviewPrompt(
|
|
1403
|
+
consultant, focusArea, output.content, directive.content, expectedOutput
|
|
1404
|
+
);
|
|
1405
|
+
|
|
1406
|
+
const response = await this.invokeAgentSafe(
|
|
1407
|
+
{ personaId: consultant.id, systemPrompt, userMessage },
|
|
1408
|
+
consultant,
|
|
1409
|
+
'consultant_review'
|
|
1410
|
+
);
|
|
1411
|
+
|
|
1412
|
+
this.createEntry(council.id, 'consultant', consultant.id, 'review', 'reviewing',
|
|
1413
|
+
response.content, response.tokensUsed, response.latencyMs, undefined,
|
|
1414
|
+
council.deliberationState?.currentRound, response.structured);
|
|
1415
|
+
|
|
1416
|
+
return `[${consultant.name} - ${focusArea}]: ${response.content}`;
|
|
1417
|
+
};
|
|
1418
|
+
|
|
1419
|
+
const results = await Promise.allSettled(consultants.map(c => reviewOne(c)));
|
|
1420
|
+
|
|
1421
|
+
const reviews: string[] = [];
|
|
1422
|
+
for (const result of results) {
|
|
1423
|
+
if (result.status === 'fulfilled') {
|
|
1424
|
+
reviews.push(result.value);
|
|
1425
|
+
} else {
|
|
1426
|
+
console.warn('[Orchestrator] Consultant review failed:', result.reason);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
return reviews.join('\n\n');
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// ==========================================================================
|
|
1434
|
+
// Phase 6: Review
|
|
1435
|
+
// ==========================================================================
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* Manager reviews work output
|
|
1439
|
+
*/
|
|
1440
|
+
async reviewWork(council: Council): Promise<{ entry: LedgerEntry; review: ManagerReview }> {
|
|
1441
|
+
this.validatePhase(council, 'reviewing');
|
|
1442
|
+
const manager = this.getManager(council);
|
|
1443
|
+
const directive = getDirective(council.id);
|
|
1444
|
+
const output = getLatestOutput(council.id);
|
|
1445
|
+
const decision = getDecision(council.id);
|
|
1446
|
+
|
|
1447
|
+
if (!directive || !output) {
|
|
1448
|
+
throw new Error('Missing directive or output for review');
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// Collect consultant reviews first
|
|
1452
|
+
const consultantReviews = await this.collectConsultantReviews(council);
|
|
1453
|
+
|
|
1454
|
+
// Check if worker had write permissions (affects review criteria)
|
|
1455
|
+
const worker = this.getWorker(council);
|
|
1456
|
+
const workerAssignment = getRoleAssignment(council, worker.id);
|
|
1457
|
+
const hasWritePermissions = !!(workerAssignment?.writePermissions);
|
|
1458
|
+
const stepType = council.deliberation?.stepType;
|
|
1459
|
+
|
|
1460
|
+
// Build prompts per Section 9.8
|
|
1461
|
+
const systemPrompt = manager.predisposition.systemPrompt;
|
|
1462
|
+
const expectedOutput = council.deliberation?.expectedOutput;
|
|
1463
|
+
const userMessage = buildManagerReviewPrompt(
|
|
1464
|
+
output.content,
|
|
1465
|
+
directive.content,
|
|
1466
|
+
decision?.acceptanceCriteria,
|
|
1467
|
+
expectedOutput,
|
|
1468
|
+
hasWritePermissions,
|
|
1469
|
+
stepType,
|
|
1470
|
+
consultantReviews,
|
|
1471
|
+
);
|
|
1472
|
+
|
|
1473
|
+
const response = await this.invokeAgentSafe(
|
|
1474
|
+
{ personaId: manager.id, systemPrompt, userMessage },
|
|
1475
|
+
manager,
|
|
1476
|
+
'review'
|
|
1477
|
+
);
|
|
1478
|
+
|
|
1479
|
+
// Parse review
|
|
1480
|
+
const review = this.parseManagerReview(response.content);
|
|
1481
|
+
|
|
1482
|
+
// Create ledger entry
|
|
1483
|
+
const entry = this.createEntry(
|
|
1484
|
+
council.id,
|
|
1485
|
+
'manager',
|
|
1486
|
+
manager.id,
|
|
1487
|
+
'review',
|
|
1488
|
+
'reviewing',
|
|
1489
|
+
response.content,
|
|
1490
|
+
response.tokensUsed,
|
|
1491
|
+
response.latencyMs,
|
|
1492
|
+
undefined,
|
|
1493
|
+
undefined,
|
|
1494
|
+
response.structured,
|
|
1495
|
+
undefined,
|
|
1496
|
+
review.verdict
|
|
1497
|
+
);
|
|
1498
|
+
|
|
1499
|
+
// Handle verdict
|
|
1500
|
+
if (review.verdict === 'accept') {
|
|
1501
|
+
this.transitionPhase(council.id, 'completed');
|
|
1502
|
+
councilStore.setStatus(council.id, 'resolved');
|
|
1503
|
+
} else if (review.verdict === 'revise') {
|
|
1504
|
+
const revisionCount = council.deliberationState?.revisionCount || 0;
|
|
1505
|
+
const maxRevisions = council.deliberation?.maxRevisions || 3;
|
|
1506
|
+
|
|
1507
|
+
if (revisionCount >= maxRevisions) {
|
|
1508
|
+
// Max revisions reached - complete with best effort
|
|
1509
|
+
this.transitionPhase(council.id, 'completed');
|
|
1510
|
+
councilStore.setStatus(council.id, 'resolved');
|
|
1511
|
+
} else {
|
|
1512
|
+
// Request revision
|
|
1513
|
+
if (review.feedback) {
|
|
1514
|
+
this.createEntry(
|
|
1515
|
+
council.id,
|
|
1516
|
+
'manager',
|
|
1517
|
+
manager.id,
|
|
1518
|
+
'revision_request',
|
|
1519
|
+
'reviewing',
|
|
1520
|
+
review.feedback,
|
|
1521
|
+
0, 0
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
councilStore.incrementRevisionCount(council.id);
|
|
1525
|
+
this.transitionPhase(council.id, 'revising');
|
|
1526
|
+
}
|
|
1527
|
+
} else if (review.verdict === 're_deliberate') {
|
|
1528
|
+
// Start new deliberation round with new information
|
|
1529
|
+
let newContextVersion: number | undefined;
|
|
1530
|
+
|
|
1531
|
+
if (review.newInformation) {
|
|
1532
|
+
// Add new information to context
|
|
1533
|
+
const currentContext = getCurrentContext(council.id);
|
|
1534
|
+
if (currentContext) {
|
|
1535
|
+
const updatedContext = createContextVersion(
|
|
1536
|
+
council.id,
|
|
1537
|
+
`${currentContext.content}\n\n[New Information from Review]:\n${review.newInformation}`,
|
|
1538
|
+
'New information emerged during review - re-deliberation required',
|
|
1539
|
+
'manager',
|
|
1540
|
+
manager.id
|
|
1541
|
+
);
|
|
1542
|
+
newContextVersion = updatedContext.version;
|
|
1543
|
+
|
|
1544
|
+
// Update council's active context reference
|
|
1545
|
+
councilStore.setActiveContext(council.id, updatedContext.id, updatedContext.version);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// Write ledger entry for re-deliberation decision
|
|
1550
|
+
this.createEntry(
|
|
1551
|
+
council.id,
|
|
1552
|
+
'manager',
|
|
1553
|
+
manager.id,
|
|
1554
|
+
're_deliberation',
|
|
1555
|
+
'reviewing',
|
|
1556
|
+
`Re-deliberation requested: ${review.reasoning || 'New information requires additional deliberation'}${
|
|
1557
|
+
review.newInformation ? `\n\nNew information:\n${review.newInformation}` : ''
|
|
1558
|
+
}${newContextVersion ? `\n\n[Context updated to version ${newContextVersion}]` : ''}`,
|
|
1559
|
+
0, 0
|
|
1560
|
+
);
|
|
1561
|
+
|
|
1562
|
+
councilStore.advanceRound(council.id);
|
|
1563
|
+
this.transitionPhase(council.id, 'round_interactive');
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
return { entry, review };
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/**
|
|
1570
|
+
* Worker revises output based on feedback
|
|
1571
|
+
*/
|
|
1572
|
+
async requestRevision(council: Council): Promise<LedgerEntry> {
|
|
1573
|
+
this.validatePhase(council, 'revising');
|
|
1574
|
+
const worker = this.getWorker(council);
|
|
1575
|
+
const directive = getDirective(council.id);
|
|
1576
|
+
const previousOutput = getLatestOutput(council.id);
|
|
1577
|
+
|
|
1578
|
+
// Get latest revision request
|
|
1579
|
+
const revisionRequest = getLatestOfType(council.id, 'revision_request');
|
|
1580
|
+
|
|
1581
|
+
if (!directive || !previousOutput || !revisionRequest) {
|
|
1582
|
+
throw new Error('Missing directive, previous output, or revision feedback');
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// Get role assignment for suppress check
|
|
1586
|
+
const assignment = getRoleAssignment(council, worker.id);
|
|
1587
|
+
const suppressPersona = assignment?.suppressPersona !== false;
|
|
1588
|
+
|
|
1589
|
+
// Build worker permissions from role assignment + deliberation config
|
|
1590
|
+
const workerPermissions: WorkerPermissions = {
|
|
1591
|
+
writePermissions: assignment?.writePermissions,
|
|
1592
|
+
workingDirectory: council.deliberation?.workingDirectory,
|
|
1593
|
+
directoryConstrained: council.deliberation?.directoryConstrained,
|
|
1594
|
+
};
|
|
1595
|
+
|
|
1596
|
+
// Build prompts per Section 9.7.1
|
|
1597
|
+
const stepType = council.deliberation?.stepType;
|
|
1598
|
+
const systemPrompt = suppressPersona
|
|
1599
|
+
? getMinimalWorkerSystemPrompt(workerPermissions, stepType)
|
|
1600
|
+
: worker.predisposition.systemPrompt;
|
|
1601
|
+
|
|
1602
|
+
const userMessage = buildWorkerRevisionPrompt(
|
|
1603
|
+
directive.content,
|
|
1604
|
+
previousOutput.content,
|
|
1605
|
+
revisionRequest.content,
|
|
1606
|
+
workerPermissions,
|
|
1607
|
+
stepType,
|
|
1608
|
+
);
|
|
1609
|
+
|
|
1610
|
+
const response = await this.invokeAgentSafe(
|
|
1611
|
+
{ personaId: worker.id, systemPrompt, userMessage },
|
|
1612
|
+
worker,
|
|
1613
|
+
'revision'
|
|
1614
|
+
);
|
|
1615
|
+
|
|
1616
|
+
// Create revision output
|
|
1617
|
+
const output = createRevisionOutput(
|
|
1618
|
+
council.id,
|
|
1619
|
+
response.content,
|
|
1620
|
+
directive.id,
|
|
1621
|
+
previousOutput.id
|
|
1622
|
+
);
|
|
1623
|
+
|
|
1624
|
+
// Update state
|
|
1625
|
+
councilStore.setCurrentOutput(council.id, output.id);
|
|
1626
|
+
|
|
1627
|
+
// Create ledger entry
|
|
1628
|
+
const entry = this.createEntry(
|
|
1629
|
+
council.id,
|
|
1630
|
+
'worker',
|
|
1631
|
+
worker.id,
|
|
1632
|
+
'work_output',
|
|
1633
|
+
'revising',
|
|
1634
|
+
response.content,
|
|
1635
|
+
response.tokensUsed,
|
|
1636
|
+
response.latencyMs,
|
|
1637
|
+
[{ artifactType: 'output', artifactId: output.id, version: output.version }],
|
|
1638
|
+
undefined,
|
|
1639
|
+
response.structured
|
|
1640
|
+
);
|
|
1641
|
+
|
|
1642
|
+
// Evolve context with revision results
|
|
1643
|
+
this.appendToContext(council.id, 'Worker Revision', worker.name, response.content, 'worker', worker.id);
|
|
1644
|
+
|
|
1645
|
+
this.transitionPhase(council.id, 'reviewing');
|
|
1646
|
+
return entry;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// ==========================================================================
|
|
1650
|
+
// Direct Execution (0 managers — worker-only council)
|
|
1651
|
+
// ==========================================================================
|
|
1652
|
+
|
|
1653
|
+
/**
|
|
1654
|
+
* Run direct execution: worker executes the raw input directly.
|
|
1655
|
+
* No manager framing, no deliberation, no review.
|
|
1656
|
+
* Used when a council has only worker(s) and no manager.
|
|
1657
|
+
*/
|
|
1658
|
+
async runDirectExecution(council: Council, rawProblem: string): Promise<void> {
|
|
1659
|
+
this.activeCouncilId = council.id;
|
|
1660
|
+
const workers = getPersonaByRole(council, 'worker');
|
|
1661
|
+
if (workers.length === 0) {
|
|
1662
|
+
throw new Error('No workers assigned for direct execution');
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
const worker = workers[0];
|
|
1666
|
+
const assignment = getRoleAssignment(council, worker.id);
|
|
1667
|
+
const suppressPersona = assignment?.suppressPersona !== false;
|
|
1668
|
+
|
|
1669
|
+
// Bootstrap directory context if enabled
|
|
1670
|
+
let enrichedProblem = rawProblem;
|
|
1671
|
+
if (council.deliberation?.bootstrapContext && council.deliberation?.workingDirectory) {
|
|
1672
|
+
try {
|
|
1673
|
+
const dirContext = await bootstrapDirectoryContext(council.deliberation.workingDirectory, { deep: true });
|
|
1674
|
+
if (dirContext) {
|
|
1675
|
+
enrichedProblem = `${dirContext}\n\n---\n\n${rawProblem}`;
|
|
1676
|
+
}
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
console.warn('[Orchestrator] Directory context bootstrap failed:', error);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// Create a synthetic directive from the raw input
|
|
1683
|
+
this.transitionPhase(council.id, 'executing');
|
|
1684
|
+
|
|
1685
|
+
const directive = createDirective(council.id, enrichedProblem, 'direct-execution');
|
|
1686
|
+
councilStore.setWorkDirective(council.id, directive.id);
|
|
1687
|
+
|
|
1688
|
+
this.createEntry(
|
|
1689
|
+
council.id,
|
|
1690
|
+
'worker',
|
|
1691
|
+
worker.id,
|
|
1692
|
+
'work_directive',
|
|
1693
|
+
'executing',
|
|
1694
|
+
enrichedProblem,
|
|
1695
|
+
0, 0,
|
|
1696
|
+
[{ artifactType: 'directive', artifactId: directive.id }]
|
|
1697
|
+
);
|
|
1698
|
+
|
|
1699
|
+
// Build worker permissions
|
|
1700
|
+
const workerPermissions: WorkerPermissions = {
|
|
1701
|
+
writePermissions: assignment?.writePermissions,
|
|
1702
|
+
workingDirectory: council.deliberation?.workingDirectory,
|
|
1703
|
+
directoryConstrained: council.deliberation?.directoryConstrained,
|
|
1704
|
+
};
|
|
1705
|
+
|
|
1706
|
+
const stepType = council.deliberation?.stepType;
|
|
1707
|
+
const systemPrompt = suppressPersona
|
|
1708
|
+
? getMinimalWorkerSystemPrompt(workerPermissions, stepType)
|
|
1709
|
+
: worker.predisposition.systemPrompt;
|
|
1710
|
+
|
|
1711
|
+
const userMessage = buildWorkerExecutionPrompt(enrichedProblem, workerPermissions, stepType);
|
|
1712
|
+
|
|
1713
|
+
const response = await this.invokeAgentSafe(
|
|
1714
|
+
{ personaId: worker.id, systemPrompt, userMessage },
|
|
1715
|
+
worker,
|
|
1716
|
+
'execution'
|
|
1717
|
+
);
|
|
1718
|
+
|
|
1719
|
+
// Create output artifact
|
|
1720
|
+
const output = createOutput(council.id, response.content, directive.id);
|
|
1721
|
+
councilStore.setCurrentOutput(council.id, output.id);
|
|
1722
|
+
|
|
1723
|
+
this.createEntry(
|
|
1724
|
+
council.id,
|
|
1725
|
+
'worker',
|
|
1726
|
+
worker.id,
|
|
1727
|
+
'work_output',
|
|
1728
|
+
'executing',
|
|
1729
|
+
response.content,
|
|
1730
|
+
response.tokensUsed,
|
|
1731
|
+
response.latencyMs,
|
|
1732
|
+
[{ artifactType: 'output', artifactId: output.id, version: output.version }],
|
|
1733
|
+
undefined,
|
|
1734
|
+
response.structured
|
|
1735
|
+
);
|
|
1736
|
+
|
|
1737
|
+
// No review — first output is accepted
|
|
1738
|
+
councilStore.updateDeliberationState(council.id, { completionSummary: response.content });
|
|
1739
|
+
this.transitionPhase(council.id, 'completed');
|
|
1740
|
+
councilStore.setStatus(council.id, 'resolved');
|
|
1741
|
+
|
|
1742
|
+
console.log('[Orchestrator] Direct execution completed');
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// ==========================================================================
|
|
1746
|
+
// Control Operations
|
|
1747
|
+
// ==========================================================================
|
|
1748
|
+
|
|
1749
|
+
/**
|
|
1750
|
+
* Pause deliberation
|
|
1751
|
+
*/
|
|
1752
|
+
async pause(council: Council): Promise<void> {
|
|
1753
|
+
const currentPhase = council.deliberationState?.currentPhase;
|
|
1754
|
+
if (!currentPhase || PHASE_TRANSITIONS[currentPhase].terminal) {
|
|
1755
|
+
throw new Error('Cannot pause - deliberation is in terminal state');
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
councilStore.setDeliberationPhase(council.id, 'paused', currentPhase);
|
|
1759
|
+
councilStore.setStatus(council.id, 'paused');
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
/**
|
|
1763
|
+
* Resume deliberation
|
|
1764
|
+
*/
|
|
1765
|
+
async resume(council: Council): Promise<void> {
|
|
1766
|
+
if (council.deliberationState?.currentPhase !== 'paused') {
|
|
1767
|
+
throw new Error('Council is not paused');
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
const previousPhase = council.deliberationState.previousPhase;
|
|
1771
|
+
if (!previousPhase) {
|
|
1772
|
+
throw new Error('No previous phase to resume from');
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
councilStore.setDeliberationPhase(council.id, previousPhase);
|
|
1776
|
+
councilStore.setStatus(council.id, 'active');
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
/**
|
|
1780
|
+
* Cancel and force decision
|
|
1781
|
+
*/
|
|
1782
|
+
async cancelAndForceDecision(council: Council): Promise<LedgerEntry> {
|
|
1783
|
+
const manager = this.getManager(council);
|
|
1784
|
+
|
|
1785
|
+
// Create cancellation entry
|
|
1786
|
+
this.createEntry(
|
|
1787
|
+
council.id,
|
|
1788
|
+
'manager',
|
|
1789
|
+
manager.id,
|
|
1790
|
+
'cancellation',
|
|
1791
|
+
council.deliberationState?.currentPhase || 'created',
|
|
1792
|
+
'Deliberation ended early by user - forcing decision',
|
|
1793
|
+
0, 0
|
|
1794
|
+
);
|
|
1795
|
+
|
|
1796
|
+
// Build forced decision context per Section 9.9
|
|
1797
|
+
const contextContent = this.buildForcedDecisionContext(council);
|
|
1798
|
+
|
|
1799
|
+
const systemPrompt = manager.predisposition.systemPrompt;
|
|
1800
|
+
const stepType = council.deliberation?.stepType;
|
|
1801
|
+
const userMessage = buildManagerForcedDecisionPrompt(contextContent, stepType);
|
|
1802
|
+
|
|
1803
|
+
const response = await this.invokeAgentSafe(
|
|
1804
|
+
{ personaId: manager.id, systemPrompt, userMessage },
|
|
1805
|
+
manager,
|
|
1806
|
+
'forced_decision'
|
|
1807
|
+
);
|
|
1808
|
+
|
|
1809
|
+
// Create decision artifact
|
|
1810
|
+
const context = getCurrentContext(council.id);
|
|
1811
|
+
const decision = createDecision(
|
|
1812
|
+
council.id,
|
|
1813
|
+
response.content,
|
|
1814
|
+
context?.version || 1
|
|
1815
|
+
);
|
|
1816
|
+
|
|
1817
|
+
councilStore.setFinalDecision(council.id, decision.id);
|
|
1818
|
+
|
|
1819
|
+
const entry = this.createEntry(
|
|
1820
|
+
council.id,
|
|
1821
|
+
'manager',
|
|
1822
|
+
manager.id,
|
|
1823
|
+
'decision',
|
|
1824
|
+
'deciding',
|
|
1825
|
+
response.content,
|
|
1826
|
+
response.tokensUsed,
|
|
1827
|
+
response.latencyMs,
|
|
1828
|
+
[{ artifactType: 'decision', artifactId: decision.id }],
|
|
1829
|
+
undefined,
|
|
1830
|
+
response.structured
|
|
1831
|
+
);
|
|
1832
|
+
|
|
1833
|
+
// Continue with normal flow
|
|
1834
|
+
this.transitionPhase(council.id, 'directing');
|
|
1835
|
+
|
|
1836
|
+
return entry;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
/**
|
|
1840
|
+
* Abort deliberation
|
|
1841
|
+
*/
|
|
1842
|
+
async abort(council: Council): Promise<void> {
|
|
1843
|
+
const manager = this.getManager(council);
|
|
1844
|
+
|
|
1845
|
+
this.createEntry(
|
|
1846
|
+
council.id,
|
|
1847
|
+
'manager',
|
|
1848
|
+
manager.id,
|
|
1849
|
+
'cancellation',
|
|
1850
|
+
council.deliberationState?.currentPhase || 'created',
|
|
1851
|
+
'Deliberation aborted by user',
|
|
1852
|
+
0, 0
|
|
1853
|
+
);
|
|
1854
|
+
|
|
1855
|
+
this.transitionPhase(council.id, 'cancelled');
|
|
1856
|
+
councilStore.setStatus(council.id, 'resolved');
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// ==========================================================================
|
|
1860
|
+
// Context Builders
|
|
1861
|
+
// ==========================================================================
|
|
1862
|
+
|
|
1863
|
+
/**
|
|
1864
|
+
* Build context for Round 1 (independent analysis) - Section 9.2
|
|
1865
|
+
* ONLY context artifact, no other consultant output
|
|
1866
|
+
*/
|
|
1867
|
+
buildRound1Context(council: Council): string {
|
|
1868
|
+
const context = getCurrentContext(council.id);
|
|
1869
|
+
if (!context) {
|
|
1870
|
+
throw new Error('No context found');
|
|
1871
|
+
}
|
|
1872
|
+
return `SHARED CONTEXT (v${context.version}):\n${context.content}`;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
/**
|
|
1876
|
+
* Build context for Round 2+ (interactive deliberation) - Section 9.3
|
|
1877
|
+
* Context artifact + prior rounds (full or summarized) + manager notes
|
|
1878
|
+
* When includeCurrentRound is true (sequential mode), appends current round
|
|
1879
|
+
* entries so later consultants can see earlier consultants' contributions.
|
|
1880
|
+
*/
|
|
1881
|
+
buildRoundNContext(council: Council, includeCurrentRound: boolean = false): string {
|
|
1882
|
+
const context = getCurrentContext(council.id);
|
|
1883
|
+
if (!context) {
|
|
1884
|
+
throw new Error('No context found');
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
let result = `SHARED CONTEXT (v${context.version}):\n${context.content}\n\n---\n`;
|
|
1888
|
+
|
|
1889
|
+
const currentRound = council.deliberationState?.currentRound || 1;
|
|
1890
|
+
const useSummaries = this.shouldSummarize(council);
|
|
1891
|
+
const roundSummaries = council.deliberationState?.roundSummaries || {};
|
|
1892
|
+
|
|
1893
|
+
// Always use summaries for old rounds (round 2+), full for most recent
|
|
1894
|
+
// This saves 30-40% of input tokens per round
|
|
1895
|
+
if (currentRound > 1) {
|
|
1896
|
+
// OLD rounds: summaries only (generate on-the-fly if missing)
|
|
1897
|
+
for (let r = 1; r < currentRound - 1; r++) {
|
|
1898
|
+
const summary = roundSummaries[r];
|
|
1899
|
+
if (summary) {
|
|
1900
|
+
result += `\nROUND ${r} SUMMARY:\n${summary}\n`;
|
|
1901
|
+
} else {
|
|
1902
|
+
// Fallback: generate mechanical summary instead of sending full entries
|
|
1903
|
+
const roundEntries = getEntriesForRound(council.id, r);
|
|
1904
|
+
if (roundEntries.length > 0) {
|
|
1905
|
+
const fallbackSummary = roundEntries
|
|
1906
|
+
.map(e => `${e.authorName || e.authorRole}: ${e.content.slice(0, 200)}`)
|
|
1907
|
+
.join('\n');
|
|
1908
|
+
result += `\nROUND ${r} SUMMARY:\n${fallbackSummary}\n`;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// MOST RECENT prior round: full entries (model needs detail for response)
|
|
1914
|
+
const recentEntries = getEntriesForRound(council.id, currentRound - 1);
|
|
1915
|
+
result += `\nROUND ${currentRound - 1}:\n`;
|
|
1916
|
+
result += formatEntriesForContext(recentEntries);
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// Manager notes: summarize if more than 3, keep last 2 full
|
|
1920
|
+
const managerNotes = getManagerNotes(council.id, currentRound);
|
|
1921
|
+
if (managerNotes.length > 3) {
|
|
1922
|
+
const oldNotes = managerNotes.slice(0, -2);
|
|
1923
|
+
const recentNotes = managerNotes.slice(-2);
|
|
1924
|
+
result += `\n---\nMANAGER NOTES (earlier, summarized):\n`;
|
|
1925
|
+
result += oldNotes.map(n => `- ${n.content.slice(0, 150)}`).join('\n');
|
|
1926
|
+
result += `\n\nMANAGER NOTES (recent):\n`;
|
|
1927
|
+
result += formatEntriesForContext(recentNotes);
|
|
1928
|
+
} else if (managerNotes.length > 0) {
|
|
1929
|
+
result += `\n---\nMANAGER NOTES:\n`;
|
|
1930
|
+
result += formatEntriesForContext(managerNotes);
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// In sequential mode, include current round entries so later consultants
|
|
1934
|
+
// can see what earlier consultants said in this round
|
|
1935
|
+
if (includeCurrentRound) {
|
|
1936
|
+
const currentRoundEntries = getEntriesForRound(council.id, currentRound);
|
|
1937
|
+
// Only include consultant entries (not manager entries which are from previous evaluation)
|
|
1938
|
+
const consultantEntries = currentRoundEntries.filter(e => e.authorRole === 'consultant');
|
|
1939
|
+
if (consultantEntries.length > 0) {
|
|
1940
|
+
result += `\n---\nCURRENT ROUND (so far):\n`;
|
|
1941
|
+
result += formatEntriesForContext(consultantEntries);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
return result;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
/**
|
|
1949
|
+
* Build context for Manager evaluation - Section 9.4
|
|
1950
|
+
* Uses summaries for older rounds to keep token usage manageable.
|
|
1951
|
+
* Manager sees: shared context + summarised old rounds + full current round.
|
|
1952
|
+
*/
|
|
1953
|
+
buildManagerEvalContext(council: Council): string {
|
|
1954
|
+
const context = getCurrentContext(council.id);
|
|
1955
|
+
const currentRound = council.deliberationState?.currentRound || 1;
|
|
1956
|
+
const roundSummaries = council.deliberationState?.roundSummaries || {};
|
|
1957
|
+
|
|
1958
|
+
let result = `SHARED CONTEXT (v${context?.version || 1}):\n${context?.content || ''}\n\n---\n`;
|
|
1959
|
+
|
|
1960
|
+
// For rounds 1-2 send full history (not much to summarize yet)
|
|
1961
|
+
// For rounds 3+ use summaries for older rounds, full for recent 2 rounds
|
|
1962
|
+
if (currentRound <= 2) {
|
|
1963
|
+
for (let r = 1; r <= currentRound; r++) {
|
|
1964
|
+
const roundEntries = getEntriesForRound(council.id, r);
|
|
1965
|
+
if (roundEntries.length > 0) {
|
|
1966
|
+
result += `\nROUND ${r}:\n`;
|
|
1967
|
+
result += formatEntriesForContext(roundEntries);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
} else {
|
|
1971
|
+
// Old rounds: summaries (rounds 1 to currentRound-2)
|
|
1972
|
+
for (let r = 1; r <= currentRound - 2; r++) {
|
|
1973
|
+
const summary = roundSummaries[r];
|
|
1974
|
+
if (summary) {
|
|
1975
|
+
result += `\nROUND ${r} SUMMARY:\n${summary}\n`;
|
|
1976
|
+
} else {
|
|
1977
|
+
// No summary available yet — use full entries as fallback
|
|
1978
|
+
const roundEntries = getEntriesForRound(council.id, r);
|
|
1979
|
+
result += `\nROUND ${r}:\n`;
|
|
1980
|
+
result += formatEntriesForContext(roundEntries);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
// Recent 2 rounds: full entries (manager needs detail for evaluation)
|
|
1985
|
+
for (let r = Math.max(1, currentRound - 1); r <= currentRound; r++) {
|
|
1986
|
+
const roundEntries = getEntriesForRound(council.id, r);
|
|
1987
|
+
if (roundEntries.length > 0) {
|
|
1988
|
+
result += `\nROUND ${r}:\n`;
|
|
1989
|
+
result += formatEntriesForContext(roundEntries);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// Manager notes for current round only (old notes are captured in summaries)
|
|
1995
|
+
const managerNotes = getManagerNotes(council.id, currentRound);
|
|
1996
|
+
if (managerNotes.length > 0) {
|
|
1997
|
+
result += `\n---\nMANAGER NOTES:\n`;
|
|
1998
|
+
result += formatEntriesForContext(managerNotes);
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
return result;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
/**
|
|
2005
|
+
* Build context for decision - Section 9.5
|
|
2006
|
+
* Same as manager eval context (uses summaries for older rounds).
|
|
2007
|
+
*/
|
|
2008
|
+
buildDecisionContext(council: Council): string {
|
|
2009
|
+
return this.buildManagerEvalContext(council);
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
/**
|
|
2013
|
+
* Build context for directive - Section 9.6
|
|
2014
|
+
*/
|
|
2015
|
+
buildDirectiveContext(council: Council): string {
|
|
2016
|
+
const decision = getDecision(council.id);
|
|
2017
|
+
const plan = getPlan(council.id);
|
|
2018
|
+
|
|
2019
|
+
let result = `YOUR DECISION:\n${decision?.content || ''}\n`;
|
|
2020
|
+
|
|
2021
|
+
if (plan) {
|
|
2022
|
+
result += `\nPLAN:\n${plan.content}\n`;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
return result;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
/**
|
|
2029
|
+
* Build context for Worker - Section 9.7
|
|
2030
|
+
* ONLY directive, no deliberation history
|
|
2031
|
+
*/
|
|
2032
|
+
buildWorkerContext(council: Council): string {
|
|
2033
|
+
const directive = getDirective(council.id);
|
|
2034
|
+
return `DIRECTIVE:\n${directive?.content || ''}`;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
/**
|
|
2038
|
+
* Build context for revision - Section 9.7.1
|
|
2039
|
+
*/
|
|
2040
|
+
buildRevisionContext(council: Council): string {
|
|
2041
|
+
const directive = getDirective(council.id);
|
|
2042
|
+
const previousOutput = getLatestOutput(council.id);
|
|
2043
|
+
const revisionRequest = getLatestOfType(council.id, 'revision_request');
|
|
2044
|
+
|
|
2045
|
+
return `DIRECTIVE:\n${directive?.content || ''}\n\nYOUR PREVIOUS OUTPUT:\n${previousOutput?.content || ''}\n\nREVISION FEEDBACK:\n${revisionRequest?.content || ''}`;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
/**
|
|
2049
|
+
* Build context for review - Section 9.8
|
|
2050
|
+
*/
|
|
2051
|
+
buildReviewContext(council: Council): string {
|
|
2052
|
+
const directive = getDirective(council.id);
|
|
2053
|
+
const output = getLatestOutput(council.id);
|
|
2054
|
+
const decision = getDecision(council.id);
|
|
2055
|
+
|
|
2056
|
+
let result = `WORK DIRECTIVE:\n${directive?.content || ''}\n`;
|
|
2057
|
+
|
|
2058
|
+
if (decision?.acceptanceCriteria) {
|
|
2059
|
+
result += `\nACCEPTANCE CRITERIA:\n${decision.acceptanceCriteria}\n`;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
result += `\nWORKER OUTPUT:\n${output?.content || ''}`;
|
|
2063
|
+
|
|
2064
|
+
return result;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
/**
|
|
2068
|
+
* Build context for forced decision - Section 9.9
|
|
2069
|
+
*/
|
|
2070
|
+
buildForcedDecisionContext(council: Council): string {
|
|
2071
|
+
const context = getCurrentContext(council.id);
|
|
2072
|
+
const entries = getAllEntries(council.id);
|
|
2073
|
+
|
|
2074
|
+
let result = `SHARED CONTEXT (v${context?.version || 1}):\n${context?.content || ''}\n\n---\n`;
|
|
2075
|
+
result += `DELIBERATION SO FAR (incomplete):\n`;
|
|
2076
|
+
result += formatEntriesForContext(entries);
|
|
2077
|
+
|
|
2078
|
+
return result;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// ==========================================================================
|
|
2082
|
+
// Helper Methods
|
|
2083
|
+
// ==========================================================================
|
|
2084
|
+
|
|
2085
|
+
/**
|
|
2086
|
+
* Determine if summarization should be used
|
|
2087
|
+
*/
|
|
2088
|
+
shouldSummarize(council: Council): boolean {
|
|
2089
|
+
const config = council.deliberation;
|
|
2090
|
+
if (!config) return false;
|
|
2091
|
+
|
|
2092
|
+
if (config.summaryMode === 'none') return false;
|
|
2093
|
+
|
|
2094
|
+
const currentRound = council.deliberationState?.currentRound || 1;
|
|
2095
|
+
// Always summarize after round 1 completes
|
|
2096
|
+
if (currentRound <= 1) return false;
|
|
2097
|
+
|
|
2098
|
+
// Always summarize from round 2+ (was round 3+, changed for cost efficiency)
|
|
2099
|
+
return true;
|
|
2100
|
+
|
|
2101
|
+
return false;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
/**
|
|
2105
|
+
* Get the next valid phase based on current state and action
|
|
2106
|
+
*/
|
|
2107
|
+
getNextPhase(
|
|
2108
|
+
council: Council,
|
|
2109
|
+
action?: 'continue' | 'decide' | 'redirect' | 'accept' | 'revise' | 're_deliberate'
|
|
2110
|
+
): DeliberationPhase {
|
|
2111
|
+
const currentPhase = council.deliberationState?.currentPhase || 'created';
|
|
2112
|
+
const validNext = PHASE_TRANSITIONS[currentPhase].validNext;
|
|
2113
|
+
|
|
2114
|
+
if (validNext.length === 0) {
|
|
2115
|
+
return currentPhase; // Terminal state
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
// Determine next based on action
|
|
2119
|
+
switch (currentPhase) {
|
|
2120
|
+
case 'round_waiting_for_manager':
|
|
2121
|
+
if (action === 'decide') return 'deciding';
|
|
2122
|
+
if (action === 'continue' || action === 'redirect') return 'round_interactive';
|
|
2123
|
+
return 'deciding';
|
|
2124
|
+
|
|
2125
|
+
case 'deciding':
|
|
2126
|
+
return council.deliberation?.requirePlan ? 'planning' : 'directing';
|
|
2127
|
+
|
|
2128
|
+
case 'reviewing':
|
|
2129
|
+
if (action === 'accept') return 'completed';
|
|
2130
|
+
if (action === 'revise') return 'revising';
|
|
2131
|
+
if (action === 're_deliberate') return 'round_interactive';
|
|
2132
|
+
return 'completed';
|
|
2133
|
+
|
|
2134
|
+
default:
|
|
2135
|
+
return validNext[0];
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
/**
|
|
2140
|
+
* Check if current phase is complete and can transition
|
|
2141
|
+
*/
|
|
2142
|
+
isPhaseComplete(council: Council): boolean {
|
|
2143
|
+
const phase = council.deliberationState?.currentPhase;
|
|
2144
|
+
|
|
2145
|
+
switch (phase) {
|
|
2146
|
+
case 'round_independent':
|
|
2147
|
+
case 'round_interactive':
|
|
2148
|
+
return councilStore.isRoundComplete(council.id);
|
|
2149
|
+
default:
|
|
2150
|
+
return true;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
// ==========================================================================
|
|
2155
|
+
// Private Helpers
|
|
2156
|
+
// ==========================================================================
|
|
2157
|
+
|
|
2158
|
+
private getManager(council: Council): Persona {
|
|
2159
|
+
const managers = getPersonaByRole(council, 'manager');
|
|
2160
|
+
if (managers.length === 0) {
|
|
2161
|
+
throw new Error('No manager assigned to council');
|
|
2162
|
+
}
|
|
2163
|
+
return managers[0];
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
private getConsultants(council: Council): Persona[] {
|
|
2167
|
+
return getPersonaByRole(council, 'consultant');
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
private getWorker(council: Council): Persona {
|
|
2171
|
+
const workers = getPersonaByRole(council, 'worker');
|
|
2172
|
+
if (workers.length === 0) {
|
|
2173
|
+
throw new Error('No worker assigned to council');
|
|
2174
|
+
}
|
|
2175
|
+
return workers[0];
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
private getPersonaDisplayName(council: Council, personaId: string): string {
|
|
2179
|
+
const persona = council.personas.find((p) => p.id === personaId);
|
|
2180
|
+
return persona?.name || personaId;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
private validatePhase(council: Council, expected: DeliberationPhase): void {
|
|
2184
|
+
// If deliberationState is undefined, treat as 'created'
|
|
2185
|
+
const current = council.deliberationState?.currentPhase ?? 'created';
|
|
2186
|
+
if (current !== expected) {
|
|
2187
|
+
throw new Error(`Invalid phase: expected ${expected}, got ${current}`);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
private transitionPhase(councilId: string, to: DeliberationPhase): void {
|
|
2192
|
+
const council = councilStore.get(councilId);
|
|
2193
|
+
const from = council?.deliberationState?.currentPhase || 'created';
|
|
2194
|
+
|
|
2195
|
+
// Validate transition
|
|
2196
|
+
const validNext = PHASE_TRANSITIONS[from].validNext;
|
|
2197
|
+
if (!validNext.includes(to) && from !== 'paused') {
|
|
2198
|
+
console.warn(`[Orchestrator] Invalid phase transition: ${from} -> ${to}`);
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
councilStore.setDeliberationPhase(councilId, to, from);
|
|
2202
|
+
this.config.onPhaseChange?.(from, to);
|
|
2203
|
+
|
|
2204
|
+
console.log(`[Orchestrator] Phase transition: ${from} -> ${to}`);
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
private async invokeAgentSafe(
|
|
2208
|
+
invocation: AgentInvocation,
|
|
2209
|
+
persona: Persona,
|
|
2210
|
+
context: string
|
|
2211
|
+
): Promise<AgentResponse> {
|
|
2212
|
+
// Scope tool access by phase/role:
|
|
2213
|
+
// Built-in tools (WebSearch, WebFetch) are always included for any tool-enabled phase.
|
|
2214
|
+
// Workers (execution, revision, debug): full tools + search
|
|
2215
|
+
// Planning workers: limited tools (Read, Write, Glob — save plan doc, no code editing)
|
|
2216
|
+
// Consultants (independent_analysis, deliberation_response): read-only + search
|
|
2217
|
+
// Reviewers (code_review): read-only + search
|
|
2218
|
+
// Manager phases: no tools
|
|
2219
|
+
const BUILTIN_TOOLS = ['WebSearch', 'WebFetch'];
|
|
2220
|
+
const FULL_TOOLS = [...BUILTIN_TOOLS, 'Edit', 'Write', 'Read', 'Bash', 'Glob', 'Grep'];
|
|
2221
|
+
const PLAN_TOOLS = [...BUILTIN_TOOLS, 'Read', 'Write', 'Glob'];
|
|
2222
|
+
const READ_ONLY_TOOLS = [...BUILTIN_TOOLS, 'Read', 'Grep', 'Glob'];
|
|
2223
|
+
const MANAGER_TOOLS = [...BUILTIN_TOOLS, 'Read', 'Glob', 'Grep', 'Bash'];
|
|
2224
|
+
|
|
2225
|
+
const council = this.activeCouncilId ? councilStore.get(this.activeCouncilId) : null;
|
|
2226
|
+
const stepType = council?.deliberation?.stepType;
|
|
2227
|
+
|
|
2228
|
+
const workerToolPhases = ['execution', 'revision', 'debug'];
|
|
2229
|
+
const readOnlyToolPhases = ['independent_analysis', 'deliberation_response', 'code_review'];
|
|
2230
|
+
const managerToolPhases = ['problem_framing', 'directive', 'review'];
|
|
2231
|
+
|
|
2232
|
+
if (workerToolPhases.includes(context)) {
|
|
2233
|
+
// Planning workers get limited tools (no Edit, Bash, Grep — prevents code writing)
|
|
2234
|
+
// Coding/other workers get full tools
|
|
2235
|
+
if (stepType === 'code_planning') {
|
|
2236
|
+
invocation = { ...invocation, allowedTools: PLAN_TOOLS };
|
|
2237
|
+
} else {
|
|
2238
|
+
invocation = { ...invocation, allowedTools: FULL_TOOLS };
|
|
2239
|
+
}
|
|
2240
|
+
} else if (readOnlyToolPhases.includes(context)) {
|
|
2241
|
+
invocation = { ...invocation, allowedTools: READ_ONLY_TOOLS };
|
|
2242
|
+
} else if (managerToolPhases.includes(context)) {
|
|
2243
|
+
invocation = { ...invocation, allowedTools: MANAGER_TOOLS };
|
|
2244
|
+
} else if (!invocation.skipTools) {
|
|
2245
|
+
invocation = { ...invocation, skipTools: true };
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
// Compute effective allowed servers (intersection of step + persona)
|
|
2249
|
+
const stepServers = council?.deliberation?.allowedServerIds;
|
|
2250
|
+
const personaServers = persona.allowedServerIds;
|
|
2251
|
+
let effectiveServers: string[] | undefined;
|
|
2252
|
+
if (stepServers && personaServers) {
|
|
2253
|
+
effectiveServers = stepServers.filter(id => personaServers.includes(id));
|
|
2254
|
+
} else {
|
|
2255
|
+
effectiveServers = personaServers || stepServers;
|
|
2256
|
+
}
|
|
2257
|
+
if (effectiveServers) {
|
|
2258
|
+
invocation = { ...invocation, allowedServerIds: effectiveServers };
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// Apply per-persona tool access overrides from role assignment
|
|
2262
|
+
const roleAssignment = council?.deliberation?.roleAssignments
|
|
2263
|
+
?.find(r => r.personaId === persona.id);
|
|
2264
|
+
|
|
2265
|
+
console.log(`[Orchestrator:ToolAccess] persona=${persona.name} context=${context} roleAssignment.toolAccess=${roleAssignment?.toolAccess} roleAssignment.found=${!!roleAssignment} totalAssignments=${council?.deliberation?.roleAssignments?.length}`);
|
|
2266
|
+
|
|
2267
|
+
if (roleAssignment?.toolAccess === 'none') {
|
|
2268
|
+
invocation = { ...invocation, skipTools: true };
|
|
2269
|
+
} else if (roleAssignment?.toolAccess === 'full') {
|
|
2270
|
+
const { skipTools: _, ...rest } = invocation;
|
|
2271
|
+
invocation = rest as AgentInvocation;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
if (roleAssignment?.allowedServerIds) {
|
|
2275
|
+
invocation = { ...invocation, allowedServerIds: roleAssignment.allowedServerIds };
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
// Inject bootstrapped context into every prompt — cacheable if prompt caching is enabled.
|
|
2279
|
+
if (this.bootstrappedContext) {
|
|
2280
|
+
const hasContext = invocation.userMessage.includes(this.bootstrappedContext.slice(0, 100));
|
|
2281
|
+
if (!hasContext) {
|
|
2282
|
+
// Instead of injecting into userMessage, pass as separate cacheableContext
|
|
2283
|
+
// so Anthropic's prompt caching can deduplicate it across calls
|
|
2284
|
+
invocation = {
|
|
2285
|
+
...invocation,
|
|
2286
|
+
cacheableContext: `## PROJECT CONTEXT\n\nThe complete source code and project structure are provided below. This is your primary source of information. Analyze the code directly from what is provided here.\n\n${this.bootstrappedContext}`,
|
|
2287
|
+
userMessage: invocation.userMessage, // Keep userMessage clean
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
// Inject brevity instruction if maxWordsPerResponse is configured
|
|
2293
|
+
const wordLimit = this.activeCouncilId
|
|
2294
|
+
? councilStore.get(this.activeCouncilId)?.deliberation?.maxWordsPerResponse
|
|
2295
|
+
: undefined;
|
|
2296
|
+
if (wordLimit && wordLimit > 0) {
|
|
2297
|
+
invocation = {
|
|
2298
|
+
...invocation,
|
|
2299
|
+
userMessage: invocation.userMessage +
|
|
2300
|
+
`\n\nIMPORTANT: Keep your response concise — aim for approximately ${wordLimit} words or fewer. Be direct and avoid unnecessary elaboration.`,
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
// Thread working directory from council so each invocation uses its own dir
|
|
2305
|
+
const councilDir = council?.deliberation?.workingDirectory;
|
|
2306
|
+
if (councilDir) {
|
|
2307
|
+
invocation = { ...invocation, workingDirectory: councilDir };
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
// Apply role-based default timeouts if not already set
|
|
2311
|
+
const DEFAULT_TIMEOUTS: Record<string, number> = {
|
|
2312
|
+
// Worker contexts — 30 min
|
|
2313
|
+
execution: 1_800_000,
|
|
2314
|
+
revision: 1_800_000,
|
|
2315
|
+
debug: 1_800_000,
|
|
2316
|
+
direct_execution: 1_800_000,
|
|
2317
|
+
// Manager contexts — 10 min
|
|
2318
|
+
problem_framing: 600_000,
|
|
2319
|
+
evaluation: 600_000,
|
|
2320
|
+
manager_evaluation: 600_000,
|
|
2321
|
+
decision: 600_000,
|
|
2322
|
+
forced_decision: 600_000,
|
|
2323
|
+
directive: 600_000,
|
|
2324
|
+
plan: 600_000,
|
|
2325
|
+
review: 600_000,
|
|
2326
|
+
round_summary: 600_000,
|
|
2327
|
+
// Consultant contexts — 10 min
|
|
2328
|
+
independent_analysis: 600_000,
|
|
2329
|
+
deliberation_response: 600_000,
|
|
2330
|
+
final_position: 600_000,
|
|
2331
|
+
consultant_review: 600_000,
|
|
2332
|
+
};
|
|
2333
|
+
invocation = { ...invocation, timeoutMs: invocation.timeoutMs ?? DEFAULT_TIMEOUTS[context] ?? 600_000 };
|
|
2334
|
+
|
|
2335
|
+
// Build context inspection for this call
|
|
2336
|
+
const toolScope: ContextInspection['toolScope'] = invocation.skipTools ? 'none'
|
|
2337
|
+
: invocation.allowedTools === FULL_TOOLS ? 'full'
|
|
2338
|
+
: invocation.allowedTools === PLAN_TOOLS ? 'plan'
|
|
2339
|
+
: invocation.allowedTools === READ_ONLY_TOOLS ? 'read_only'
|
|
2340
|
+
: invocation.allowedTools === MANAGER_TOOLS ? 'manager'
|
|
2341
|
+
: invocation.allowedTools ? 'read_only' // custom list after override
|
|
2342
|
+
: 'none';
|
|
2343
|
+
|
|
2344
|
+
const systemPromptSource = workerToolPhases.includes(context) ? 'minimal_worker'
|
|
2345
|
+
: readOnlyToolPhases.includes(context) && context === 'code_review' ? 'reviewer'
|
|
2346
|
+
: readOnlyToolPhases.includes(context) ? 'persona_predisposition'
|
|
2347
|
+
: 'persona_predisposition';
|
|
2348
|
+
|
|
2349
|
+
const inspection = buildContextInspection({
|
|
2350
|
+
systemPrompt: invocation.systemPrompt,
|
|
2351
|
+
systemPromptSource,
|
|
2352
|
+
userMessage: invocation.userMessage,
|
|
2353
|
+
toolScope,
|
|
2354
|
+
toolNames: invocation.allowedTools,
|
|
2355
|
+
effectiveServerIds: invocation.allowedServerIds,
|
|
2356
|
+
wordLimitApplied: wordLimit && wordLimit > 0 ? wordLimit : undefined,
|
|
2357
|
+
contextTokenBudget: council?.deliberation?.contextTokenBudget,
|
|
2358
|
+
timeoutMs: invocation.timeoutMs,
|
|
2359
|
+
});
|
|
2360
|
+
|
|
2361
|
+
// Retry transient errors (rate limits, overload) with exponential backoff.
|
|
2362
|
+
// This covers ALL roles (manager, consultant, worker) uniformly.
|
|
2363
|
+
// Timeout errors get 1 retry (no backoff — the timeout itself was the wait).
|
|
2364
|
+
const MAX_TRANSIENT_RETRIES = 5;
|
|
2365
|
+
const BASE_DELAY_MS = 15_000; // 15 seconds
|
|
2366
|
+
|
|
2367
|
+
const sysPromptKB = (invocation.systemPrompt.length / 1024).toFixed(1);
|
|
2368
|
+
const userMsgKB = (invocation.userMessage.length / 1024).toFixed(1);
|
|
2369
|
+
const toolCount = invocation.allowedTools?.length || 0;
|
|
2370
|
+
console.log(
|
|
2371
|
+
`[Orchestrator:Call] ${persona.name} (${context}) — ` +
|
|
2372
|
+
`system: ${sysPromptKB}KB, user: ${userMsgKB}KB, tools: ${toolCount}, ` +
|
|
2373
|
+
`timeout: ${Math.round((invocation.timeoutMs || 0) / 1000)}s`
|
|
2374
|
+
);
|
|
2375
|
+
|
|
2376
|
+
for (let attempt = 0; attempt <= MAX_TRANSIENT_RETRIES; attempt++) {
|
|
2377
|
+
try {
|
|
2378
|
+
const startedAt = Date.now();
|
|
2379
|
+
this.config.onAgentThinkingStart?.(persona, startedAt, invocation.userMessage);
|
|
2380
|
+
const response = await this.config.invokeAgent(invocation, persona);
|
|
2381
|
+
this.config.onAgentThinkingEnd?.(persona);
|
|
2382
|
+
const callDuration = ((Date.now() - startedAt) / 1000).toFixed(1);
|
|
2383
|
+
console.log(`[Orchestrator:Call] ${persona.name} (${context}) completed in ${callDuration}s — ${response.tokensUsed} tokens`);
|
|
2384
|
+
// Attach context inspection to the response
|
|
2385
|
+
response.structured = { ...response.structured, contextInspection: inspection };
|
|
2386
|
+
return response;
|
|
2387
|
+
} catch (error) {
|
|
2388
|
+
this.config.onAgentThinkingEnd?.(persona);
|
|
2389
|
+
|
|
2390
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
2391
|
+
const isTransient = /\b(429|529|rate.limit|overloaded|too many requests)\b/i.test(errMsg);
|
|
2392
|
+
const isTimeout = /\btimed?\s*out\b/i.test(errMsg);
|
|
2393
|
+
|
|
2394
|
+
// Timeout: retry once, then fail
|
|
2395
|
+
if (isTimeout && attempt === 0) {
|
|
2396
|
+
const elapsedMs = invocation.timeoutMs || 0;
|
|
2397
|
+
console.warn(`[Orchestrator] ${persona.name} (${context}) timed out after ${Math.round(elapsedMs / 1000)}s, retrying once...`);
|
|
2398
|
+
this.config.onAgentTimeout?.(persona, context, elapsedMs);
|
|
2399
|
+
this.config.onError?.(new Error(`${persona.name} timed out — retrying`), context);
|
|
2400
|
+
continue; // retry once
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
if (isTransient && attempt < MAX_TRANSIENT_RETRIES) {
|
|
2404
|
+
const delayMs = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
2405
|
+
console.warn(
|
|
2406
|
+
`[Orchestrator] Transient error for ${persona.name} (${context}), ` +
|
|
2407
|
+
`retrying in ${Math.round(delayMs / 1000)}s (attempt ${attempt + 1}/${MAX_TRANSIENT_RETRIES}): ${errMsg}`
|
|
2408
|
+
);
|
|
2409
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
2410
|
+
continue;
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
this.config.onError?.(error as Error, context);
|
|
2414
|
+
throw error;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
// Should not reach here
|
|
2419
|
+
throw new Error('Unexpected: exhausted transient retry loop');
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
/**
|
|
2423
|
+
* Clean JSON artifacts from LLM responses for display.
|
|
2424
|
+
* LLMs sometimes respond with JSON when prompted for structured output.
|
|
2425
|
+
* This extracts readable text from those responses.
|
|
2426
|
+
*/
|
|
2427
|
+
cleanLLMResponse(content: string): string {
|
|
2428
|
+
if (!content || typeof content !== 'string') return content;
|
|
2429
|
+
|
|
2430
|
+
const trimmed = content.trim();
|
|
2431
|
+
|
|
2432
|
+
// Try to extract JSON from various formats
|
|
2433
|
+
const jsonStr = this.extractJsonString(trimmed);
|
|
2434
|
+
if (jsonStr) {
|
|
2435
|
+
try {
|
|
2436
|
+
const parsed = JSON.parse(jsonStr);
|
|
2437
|
+
const formatted = this.formatParsedJson(parsed);
|
|
2438
|
+
if (formatted) return formatted;
|
|
2439
|
+
} catch {
|
|
2440
|
+
// Not valid JSON, fall through
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
// Check if content is text followed by a raw JSON object at the end
|
|
2445
|
+
const trailingJsonMatch = trimmed.match(/^([\s\S]+?)\n\s*(\{[\s\S]*\})\s*$/);
|
|
2446
|
+
if (trailingJsonMatch) {
|
|
2447
|
+
const textPart = trailingJsonMatch[1].trim();
|
|
2448
|
+
try {
|
|
2449
|
+
JSON.parse(trailingJsonMatch[2]);
|
|
2450
|
+
if (textPart.length > 20) return textPart;
|
|
2451
|
+
} catch {
|
|
2452
|
+
// Not JSON, return as-is
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
return content;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
/**
|
|
2460
|
+
* Extract a JSON string from content that may be raw JSON, markdown-wrapped, etc.
|
|
2461
|
+
*/
|
|
2462
|
+
private extractJsonString(trimmed: string): string | null {
|
|
2463
|
+
// Raw JSON object
|
|
2464
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
2465
|
+
return trimmed;
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// Markdown code block: ```json\n{...}\n```
|
|
2469
|
+
const codeBlockMatch = trimmed.match(/```(?:json)?\s*\n?(\{[\s\S]*?\})\s*\n?```/);
|
|
2470
|
+
if (codeBlockMatch) {
|
|
2471
|
+
return codeBlockMatch[1];
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
return null;
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
/**
|
|
2478
|
+
* Format a parsed JSON object into readable text.
|
|
2479
|
+
* Handles evaluation/review/decision structured responses.
|
|
2480
|
+
*/
|
|
2481
|
+
private formatParsedJson(parsed: Record<string, unknown>): string | null {
|
|
2482
|
+
// Check for structured responses FIRST (action/verdict indicate eval/review)
|
|
2483
|
+
// These have multiple important fields that should all be shown
|
|
2484
|
+
if (parsed.action || parsed.verdict) {
|
|
2485
|
+
const parts: string[] = [];
|
|
2486
|
+
if (parsed.verdict && typeof parsed.verdict === 'string')
|
|
2487
|
+
parts.push(`Verdict: ${parsed.verdict}`);
|
|
2488
|
+
if (parsed.action && typeof parsed.action === 'string')
|
|
2489
|
+
parts.push(`Action: ${parsed.action}`);
|
|
2490
|
+
if (parsed.confidence != null)
|
|
2491
|
+
parts.push(`Confidence: ${Math.round((parsed.confidence as number) * 100)}%`);
|
|
2492
|
+
if (parsed.reasoning && typeof parsed.reasoning === 'string')
|
|
2493
|
+
parts.push(parsed.reasoning);
|
|
2494
|
+
if (parsed.feedback && typeof parsed.feedback === 'string')
|
|
2495
|
+
parts.push(`Feedback: ${parsed.feedback}`);
|
|
2496
|
+
if (parsed.question && typeof parsed.question === 'string')
|
|
2497
|
+
parts.push(`Question: ${parsed.question}`);
|
|
2498
|
+
if (parsed.newInformation && typeof parsed.newInformation === 'string')
|
|
2499
|
+
parts.push(`New information: ${parsed.newInformation}`);
|
|
2500
|
+
if (Array.isArray(parsed.missingInformation) && parsed.missingInformation.length)
|
|
2501
|
+
parts.push(`Missing: ${parsed.missingInformation.join(', ')}`);
|
|
2502
|
+
if (parts.length > 0) return parts.join('\n\n');
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
// For plain content responses, extract the main text field
|
|
2506
|
+
const textFields = ['reasoning', 'content', 'message', 'summary', 'analysis', 'feedback', 'explanation'];
|
|
2507
|
+
for (const field of textFields) {
|
|
2508
|
+
if (parsed[field] && typeof parsed[field] === 'string') {
|
|
2509
|
+
return parsed[field] as string;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
return null;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
private createEntry(
|
|
2517
|
+
councilId: string,
|
|
2518
|
+
role: DeliberationRole,
|
|
2519
|
+
authorId: string,
|
|
2520
|
+
entryType: LedgerEntryType,
|
|
2521
|
+
phase: DeliberationPhase,
|
|
2522
|
+
content: string,
|
|
2523
|
+
tokensUsed: number,
|
|
2524
|
+
latencyMs: number,
|
|
2525
|
+
artifactRefs?: ArtifactRef[],
|
|
2526
|
+
roundNumber?: number,
|
|
2527
|
+
structured?: Record<string, unknown>,
|
|
2528
|
+
referencedEntries?: string[],
|
|
2529
|
+
reviewOutcome?: 'accept' | 'revise' | 're_deliberate'
|
|
2530
|
+
): LedgerEntry {
|
|
2531
|
+
// Clean JSON artifacts from LLM responses before storing
|
|
2532
|
+
const cleanedContent = this.cleanLLMResponse(content);
|
|
2533
|
+
|
|
2534
|
+
const entry: LedgerEntry = {
|
|
2535
|
+
id: crypto.randomUUID(),
|
|
2536
|
+
timestamp: new Date().toISOString(),
|
|
2537
|
+
authorRole: role,
|
|
2538
|
+
authorPersonaId: authorId,
|
|
2539
|
+
entryType,
|
|
2540
|
+
phase,
|
|
2541
|
+
content: cleanedContent,
|
|
2542
|
+
tokensUsed,
|
|
2543
|
+
latencyMs,
|
|
2544
|
+
artifactRefs,
|
|
2545
|
+
roundNumber,
|
|
2546
|
+
referencedEntries,
|
|
2547
|
+
reviewOutcome,
|
|
2548
|
+
structured,
|
|
2549
|
+
};
|
|
2550
|
+
|
|
2551
|
+
// Use ledgerStore.append to notify subscribers (enables real-time UI updates)
|
|
2552
|
+
ledgerStore.append(councilId, entry);
|
|
2553
|
+
this.config.onEntryAdded?.(entry);
|
|
2554
|
+
|
|
2555
|
+
return entry;
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
private extractContextProposal(content: string): { diff: string; rationale: string } | null {
|
|
2559
|
+
const proposalMatch = content.match(/PROPOSED CONTEXT CHANGE:\s*\n*What:\s*(.+?)(?:\n+Why:\s*(.+))?$/is);
|
|
2560
|
+
|
|
2561
|
+
if (proposalMatch) {
|
|
2562
|
+
return {
|
|
2563
|
+
diff: proposalMatch[1].trim(),
|
|
2564
|
+
rationale: proposalMatch[2]?.trim() || 'No rationale provided',
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
return null;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
/**
|
|
2571
|
+
* Append a summary to the shared context document when evolveContext is enabled.
|
|
2572
|
+
* Creates a new context version with the appended information.
|
|
2573
|
+
*/
|
|
2574
|
+
private appendToContext(
|
|
2575
|
+
councilId: string,
|
|
2576
|
+
label: string,
|
|
2577
|
+
personaName: string,
|
|
2578
|
+
content: string,
|
|
2579
|
+
role: 'consultant' | 'worker' | 'manager',
|
|
2580
|
+
personaId?: string,
|
|
2581
|
+
): void {
|
|
2582
|
+
const council = this.activeCouncilId ? councilStore.get(this.activeCouncilId) : null;
|
|
2583
|
+
if (!council?.deliberation?.evolveContext) return;
|
|
2584
|
+
|
|
2585
|
+
const currentContext = getCurrentContext(councilId);
|
|
2586
|
+
if (!currentContext) return;
|
|
2587
|
+
|
|
2588
|
+
// Extract a concise summary — first 2000 chars or up to COMPLETION SUMMARY
|
|
2589
|
+
const summaryMatch = content.match(/## COMPLETION SUMMARY[\s\S]*/i);
|
|
2590
|
+
const summary = summaryMatch
|
|
2591
|
+
? summaryMatch[0].slice(0, 1500)
|
|
2592
|
+
: content.slice(0, 2000);
|
|
2593
|
+
|
|
2594
|
+
const round = council.deliberationState?.currentRound || 1;
|
|
2595
|
+
const appendText = `\n\n---\n[Round ${round} — ${label} by ${personaName}]:\n${summary}`;
|
|
2596
|
+
|
|
2597
|
+
const updated = createContextVersion(
|
|
2598
|
+
councilId,
|
|
2599
|
+
currentContext.content + appendText,
|
|
2600
|
+
`${label} by ${personaName} (round ${round})`,
|
|
2601
|
+
role,
|
|
2602
|
+
personaId,
|
|
2603
|
+
round,
|
|
2604
|
+
);
|
|
2605
|
+
|
|
2606
|
+
councilStore.setActiveContext(councilId, updated.id, updated.version);
|
|
2607
|
+
console.log(`[Orchestrator] Context evolved to v${updated.version}: ${label} by ${personaName}`);
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
private extractAcceptanceCriteria(content: string): string | undefined {
|
|
2611
|
+
const match = content.match(/ACCEPTANCE CRITERIA[:\s]*\n*([\s\S]*?)(?=\n\n|$)/i);
|
|
2612
|
+
return match?.[1]?.trim();
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
private parseManagerEvaluation(content: string): ManagerEvaluation {
|
|
2616
|
+
try {
|
|
2617
|
+
// Try to extract JSON from the response
|
|
2618
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
2619
|
+
if (jsonMatch) {
|
|
2620
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
2621
|
+
// Filter patchDecisions to only include entries with valid UUID patchIds
|
|
2622
|
+
// (the LLM may hallucinate non-UUID strings)
|
|
2623
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2624
|
+
const patchDecisions = Array.isArray(parsed.patchDecisions)
|
|
2625
|
+
? parsed.patchDecisions.filter(
|
|
2626
|
+
(d: any) => d && typeof d.patchId === 'string' && UUID_RE.test(d.patchId)
|
|
2627
|
+
)
|
|
2628
|
+
: undefined;
|
|
2629
|
+
|
|
2630
|
+
return {
|
|
2631
|
+
action: parsed.action || 'decide',
|
|
2632
|
+
reasoning: parsed.reasoning || content,
|
|
2633
|
+
confidence: parsed.confidence ?? undefined,
|
|
2634
|
+
missingInformation: parsed.missingInformation ?? undefined,
|
|
2635
|
+
question: parsed.question ?? undefined,
|
|
2636
|
+
patchDecisions: patchDecisions && patchDecisions.length > 0 ? patchDecisions : undefined,
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
} catch {
|
|
2640
|
+
// Fall back to text parsing
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
// Default interpretation from text
|
|
2644
|
+
const lowerContent = content.toLowerCase();
|
|
2645
|
+
let action: 'continue' | 'decide' | 'redirect' = 'decide';
|
|
2646
|
+
|
|
2647
|
+
if (lowerContent.includes('continue') || lowerContent.includes('another round')) {
|
|
2648
|
+
action = 'continue';
|
|
2649
|
+
} else if (lowerContent.includes('redirect') || lowerContent.includes('refocus')) {
|
|
2650
|
+
action = 'redirect';
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
return {
|
|
2654
|
+
action,
|
|
2655
|
+
reasoning: content,
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
private parseManagerReview(content: string): ManagerReview {
|
|
2660
|
+
try {
|
|
2661
|
+
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
2662
|
+
if (jsonMatch) {
|
|
2663
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
2664
|
+
return {
|
|
2665
|
+
verdict: parsed.verdict || 'accept',
|
|
2666
|
+
reasoning: parsed.reasoning || content,
|
|
2667
|
+
feedback: parsed.feedback ?? undefined,
|
|
2668
|
+
newInformation: parsed.newInformation ?? undefined,
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2671
|
+
} catch {
|
|
2672
|
+
// Fall back to text parsing
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
// Default interpretation
|
|
2676
|
+
const lowerContent = content.toLowerCase();
|
|
2677
|
+
let verdict: 'accept' | 'revise' | 're_deliberate' = 'accept';
|
|
2678
|
+
|
|
2679
|
+
if (lowerContent.includes('revise') || lowerContent.includes('revision')) {
|
|
2680
|
+
verdict = 'revise';
|
|
2681
|
+
} else if (lowerContent.includes('re-deliberate') || lowerContent.includes('redeliberate')) {
|
|
2682
|
+
verdict = 're_deliberate';
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
return {
|
|
2686
|
+
verdict,
|
|
2687
|
+
reasoning: content,
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
/**
|
|
2692
|
+
* Run a single consultant's contribution with retry logic.
|
|
2693
|
+
* Ensures every consultant gets a fair chance to contribute:
|
|
2694
|
+
* - 'retry' policy: retries up to maxRetries times with backoff
|
|
2695
|
+
* - 'skip' policy: records error and moves on
|
|
2696
|
+
* - 'fail' policy: fails the entire deliberation
|
|
2697
|
+
*
|
|
2698
|
+
* On final failure, records the submission anyway so the round can complete.
|
|
2699
|
+
*/
|
|
2700
|
+
private async runConsultantWithRetry(
|
|
2701
|
+
council: Council,
|
|
2702
|
+
consultant: Persona,
|
|
2703
|
+
runFn: () => Promise<LedgerEntry>
|
|
2704
|
+
): Promise<LedgerEntry | null> {
|
|
2705
|
+
const maxRetries = council.deliberation?.maxRetries || 2;
|
|
2706
|
+
const policy = council.deliberation?.consultantErrorPolicy || 'retry';
|
|
2707
|
+
|
|
2708
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
2709
|
+
try {
|
|
2710
|
+
return await runFn();
|
|
2711
|
+
} catch (error) {
|
|
2712
|
+
const isLastAttempt = attempt >= maxRetries || policy !== 'retry';
|
|
2713
|
+
|
|
2714
|
+
console.error(
|
|
2715
|
+
`[Orchestrator] Consultant ${consultant.name} failed (attempt ${attempt + 1}/${maxRetries + 1}):`,
|
|
2716
|
+
(error as Error).message
|
|
2717
|
+
);
|
|
2718
|
+
|
|
2719
|
+
councilStore.addError(
|
|
2720
|
+
council.id,
|
|
2721
|
+
`Consultant ${consultant.name}: ${(error as Error).message} (attempt ${attempt + 1})`
|
|
2722
|
+
);
|
|
2723
|
+
|
|
2724
|
+
if (policy === 'fail') {
|
|
2725
|
+
this.transitionPhase(council.id, 'failed');
|
|
2726
|
+
throw error;
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
if (isLastAttempt) {
|
|
2730
|
+
// Record submission for failed consultant so the round can still complete
|
|
2731
|
+
councilStore.recordSubmission(council.id, consultant.id);
|
|
2732
|
+
|
|
2733
|
+
// Create an error entry in the ledger so it's visible in the UI
|
|
2734
|
+
this.createEntry(
|
|
2735
|
+
council.id,
|
|
2736
|
+
'consultant',
|
|
2737
|
+
consultant.id,
|
|
2738
|
+
'error',
|
|
2739
|
+
council.deliberationState?.currentPhase || 'round_independent',
|
|
2740
|
+
`Failed to contribute after ${attempt + 1} attempt(s): ${(error as Error).message}`,
|
|
2741
|
+
0,
|
|
2742
|
+
0,
|
|
2743
|
+
undefined,
|
|
2744
|
+
council.deliberationState?.currentRound
|
|
2745
|
+
);
|
|
2746
|
+
|
|
2747
|
+
return null;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
// Delay before retry — longer backoff for rate limit errors
|
|
2751
|
+
const errMsg = (error as Error).message || '';
|
|
2752
|
+
const isRateLimit = /\b(429|529|rate.limit|overloaded|too many requests)\b/i.test(errMsg);
|
|
2753
|
+
const delayMs = isRateLimit
|
|
2754
|
+
? 15_000 * Math.pow(2, attempt) // 15s, 30s, 60s for rate limits
|
|
2755
|
+
: 2_000 * (attempt + 1); // 2s, 4s, 6s for other errors
|
|
2756
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
return null;
|
|
2761
|
+
}
|
|
2762
|
+
}
|