alphacouncil-agent 0.9.2 → 0.9.4
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/.claude/commands/alpha.md +22 -3
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.grok/commands/alpha.md +22 -3
- package/.opencode/command/alpha.md +22 -3
- package/AGENTS.md +27 -4
- package/CHANGELOG.md +67 -0
- package/CLAUDE.md +41 -7
- package/README.ja.md +21 -11
- package/README.md +35 -11
- package/README.zh-CN.md +31 -11
- package/commands/alpha.md +22 -3
- package/data/build-profile.v1.json +1 -1
- package/data/master-selector-method-locales.v1.mjs +286 -0
- package/docs/INSTALL.md +43 -6
- package/docs/releases/v0.9.3.md +86 -0
- package/docs/releases/v0.9.4.md +79 -0
- package/docs/report-contract.md +50 -1
- package/knowledge/ai-assisted-solo/experiments/runs/a.json +4 -4
- package/knowledge/ai-assisted-solo/experiments/runs/b.json +2 -2
- package/knowledge/ai-assisted-solo/experiments/runs/c.json +2 -2
- package/knowledge/ai-assisted-solo/experiments/runs/d13.json +16 -16
- package/knowledge/ai-assisted-solo/experiments/runs/d26.json +29 -29
- package/knowledge/ai-assisted-solo/experiments/runs/e-d13.json +19 -19
- package/knowledge/ai-assisted-solo/experiments/runs/e-d26.json +32 -32
- package/knowledge/ai-assisted-solo/experiments/runs/h_ai_reference.json +33 -33
- package/knowledge/ai-assisted-solo/experiments/simulation-input.json +79 -79
- package/knowledge/ai-assisted-solo/experiments/simulation-manifest.json +19 -19
- package/knowledge/solo-test/masters/master_ackman/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_aschenbrenner/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_asness/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_buffett/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_burry/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_cathie_wood/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_dalio/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_damodaran/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_druckenmiller/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_duan_yongping/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_fisher/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_forensic_short/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_graham/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_jhunjhunwala/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_klarman/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_li_lu/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_lynch/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_marks/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_munger/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_natenberg/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_pabrai/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_simons/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_sinclair/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_soros/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_taleb/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_thorp/manifest.json +14 -4
- package/mcp/lib/codex.mjs +3 -3
- package/mcp/lib/constants.mjs +61 -25
- package/mcp/lib/council-options.mjs +95 -40
- package/mcp/lib/council-selection.mjs +95 -9
- package/mcp/lib/gates.mjs +78 -11
- package/mcp/lib/lang.mjs +109 -2
- package/mcp/lib/markdown.mjs +444 -135
- package/mcp/lib/master-catalog.mjs +28 -3
- package/mcp/lib/orchestrator.mjs +952 -265
- package/mcp/lib/packets.mjs +173 -20
- package/mcp/lib/personas/engine.mjs +30 -30
- package/mcp/lib/personas-v3/compiler.mjs +2 -0
- package/mcp/lib/personas-v3/loader.mjs +10 -7
- package/mcp/lib/personas-v3/source-acquisition.mjs +10 -3
- package/mcp/lib/prompts.mjs +33 -0
- package/mcp/lib/rpc.mjs +75 -33
- package/mcp/lib/run-store.mjs +23 -0
- package/package.json +2 -2
- package/schemas/persona-v3.schema.json +37 -2
- package/scripts/lib/packaged-host-parity.mjs +54 -5
- package/scripts/lib/persona-v3-solo-test-packs.mjs +31 -5
- package/scripts/package-smoke.mjs +31 -12
- package/scripts/run-tests.mjs +23 -2
- package/skills/alphacouncil-agent/SKILL.md +50 -7
package/mcp/lib/orchestrator.mjs
CHANGED
|
@@ -4,14 +4,14 @@ import { COUNCIL_MODES, DEBATE_ROLES, DEFAULT_TASKS, LIMITS, OUTPUT_MODES, QUICK
|
|
|
4
4
|
import { invalidParams } from "./errors.mjs";
|
|
5
5
|
import { readJson, writeJson } from "./fsutil.mjs";
|
|
6
6
|
import { registry } from "./personas/registry.mjs";
|
|
7
|
-
import { isChineseLanguage, resolveLanguage } from "./lang.mjs";
|
|
7
|
+
import { assertReaderLanguage, isChineseLanguage, localized, resolveLanguage } from "./lang.mjs";
|
|
8
8
|
import { cleanLog } from "./text.mjs";
|
|
9
9
|
import { completenessStatus, verificationStatus } from "./gates.mjs";
|
|
10
10
|
import { agentState, appendEvent, artifactPaths, existingDebate, runPath, runId, safeSymbol, saveRun, taskState, today, updateAgent, updateTask, writeSourceManifest, writeStatus } from "./run-store.mjs";
|
|
11
11
|
import { writeAllAgentsMarkdown, writeAnalystMarkdownFiles, writeArtifactIndex, writeFinalArtifacts } from "./markdown.mjs";
|
|
12
|
-
import { debateFromCodex, debateQnaGate, dryDebate, dryPacket, extractJson, firstFailedDebateResult, managerFallback, mergeDebateRounds, normalizeDebate, normalizeMasterOpinion, normalizePacket, rawRecordText } from "./packets.mjs";
|
|
12
|
+
import { debateFailurePacket, debateFromCodex, debateQnaGate, dryDebate, dryPacket, extractJson, firstFailedDebateResult, managerFallback, mergeDebateRounds, normalizeDebate, normalizeMasterOpinion, normalizeMasterVoice, normalizePacket, rawRecordText } from "./packets.mjs";
|
|
13
13
|
import { mapLimit, runCodex } from "./codex.mjs";
|
|
14
|
-
import { debatePrompt, masterPrompt, selectedMasters, taskPrompt } from "./prompts.mjs";
|
|
14
|
+
import { debatePrompt, masterPrompt, masterVoicePrompt, selectedMasters, taskPrompt } from "./prompts.mjs";
|
|
15
15
|
import { resolveSeatWeights } from "./weights.mjs";
|
|
16
16
|
import { completedMasterOpinion, declinedMasterOpinion, planMasterSeats, reconcileMasterOpinion } from "./personas/engine.mjs";
|
|
17
17
|
import { gatherGrounding } from "./grounding.mjs";
|
|
@@ -28,40 +28,67 @@ function plannedTasks(args = {}) {
|
|
|
28
28
|
return DEFAULT_TASKS;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
return { council_mode: "full", debate_format: "three_round_cross_exam", time_budget_ms: null, deadline_at: null };
|
|
34
|
-
}
|
|
31
|
+
function councilTiming(args, startedAt) {
|
|
32
|
+
const mode = councilMode(args);
|
|
35
33
|
const requested = Number(args.total_timeout_ms);
|
|
34
|
+
const hardMaximum = mode === "quick" ? LIMITS.QUICK_HARD_MAX_MS : LIMITS.FULL_HARD_MAX_MS;
|
|
35
|
+
const defaultBudget = mode === "quick" ? LIMITS.QUICK_TOTAL_MS : LIMITS.FULL_TOTAL_MS;
|
|
36
36
|
const timeBudgetMs = Number.isFinite(requested) && requested > 0
|
|
37
|
-
? Math.min(requested,
|
|
38
|
-
:
|
|
37
|
+
? Math.min(requested, hardMaximum)
|
|
38
|
+
: defaultBudget;
|
|
39
39
|
const deadlineAt = args.queued_run?.deadline_at
|
|
40
40
|
|| new Date(Date.parse(startedAt) + timeBudgetMs).toISOString();
|
|
41
41
|
return {
|
|
42
|
-
council_mode:
|
|
43
|
-
debate_format: "single_round_parallel",
|
|
42
|
+
council_mode: mode,
|
|
43
|
+
debate_format: mode === "quick" ? "single_round_parallel" : "three_round_cross_exam_parallel_per_round",
|
|
44
44
|
time_budget_ms: timeBudgetMs,
|
|
45
45
|
deadline_at: deadlineAt,
|
|
46
|
+
deadline_enforced: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function visibleCouncilTiming(args) {
|
|
51
|
+
const mode = councilMode(args);
|
|
52
|
+
return {
|
|
53
|
+
council_mode: mode,
|
|
54
|
+
debate_format: mode === "quick" ? "single_round_parallel" : "host_managed_visible_debate",
|
|
55
|
+
time_budget_ms: null,
|
|
56
|
+
deadline_at: null,
|
|
57
|
+
deadline_enforced: false,
|
|
46
58
|
};
|
|
47
59
|
}
|
|
48
60
|
|
|
49
|
-
function
|
|
50
|
-
if (
|
|
61
|
+
function remainingCouncilBudget(run, capMs) {
|
|
62
|
+
if (!run.deadline_at) return capMs;
|
|
63
|
+
const configuredReserve = run.council_mode === "quick"
|
|
64
|
+
? LIMITS.QUICK_FINALIZE_RESERVE_MS
|
|
65
|
+
: LIMITS.FULL_FINALIZE_RESERVE_MS;
|
|
51
66
|
const reserve = Math.min(
|
|
52
|
-
|
|
53
|
-
Math.max(100, Math.floor(Number(run.time_budget_ms || LIMITS.
|
|
67
|
+
configuredReserve,
|
|
68
|
+
Math.max(100, Math.floor(Number(run.time_budget_ms || LIMITS.FULL_TOTAL_MS) * 0.1)),
|
|
54
69
|
);
|
|
55
|
-
const
|
|
70
|
+
const killGrace = councilKillGrace(run);
|
|
71
|
+
// runCodex may need one SIGKILL grace after its timeout. Budget that grace before
|
|
72
|
+
// launching a child so the queue-to-persistence deadline remains the outer boundary.
|
|
73
|
+
const usable = Date.parse(run.deadline_at) - Date.now() - reserve - killGrace;
|
|
56
74
|
return Math.max(0, Math.min(capMs, usable));
|
|
57
75
|
}
|
|
58
76
|
|
|
59
|
-
function
|
|
77
|
+
function councilKillGrace(run) {
|
|
78
|
+
const total = Math.max(1_000, Number(run?.time_budget_ms || LIMITS.FULL_TOTAL_MS));
|
|
79
|
+
// A fixed five-second grace would consume tiny operator-supplied budgets before the
|
|
80
|
+
// first worker. Production budgets retain the full grace; small budgets scale it down.
|
|
81
|
+
// The exact same value is passed to runCodex, so forced settlement remains inside the
|
|
82
|
+
// queue-to-persistence deadline rather than becoming an unaccounted overrun.
|
|
83
|
+
return Math.min(LIMITS.SIGKILL_GRACE_MS, Math.max(50, Math.floor(total * 0.02)));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function deadlineResult(run) {
|
|
60
87
|
return {
|
|
61
88
|
ok: false,
|
|
62
89
|
code: null,
|
|
63
90
|
text: "",
|
|
64
|
-
stderr: "
|
|
91
|
+
stderr: `${run?.council_mode || "council"} global deadline exhausted`,
|
|
65
92
|
stdout: "",
|
|
66
93
|
timedOut: true,
|
|
67
94
|
deadline_exhausted: true,
|
|
@@ -169,7 +196,10 @@ export function visibleRun(args) {
|
|
|
169
196
|
const dir = runPath(id);
|
|
170
197
|
mkdirSync(dir, { recursive: true });
|
|
171
198
|
const startedAt = new Date().toISOString();
|
|
172
|
-
|
|
199
|
+
// External host threads are outside this MCP process, so the plugin cannot stop them or
|
|
200
|
+
// make a truthful queue-to-persistence SLA claim. Only analyze_symbol gets the enforced
|
|
201
|
+
// thirty-minute full-council deadline.
|
|
202
|
+
const timing = visibleCouncilTiming(args);
|
|
173
203
|
const run = {
|
|
174
204
|
run_id: id,
|
|
175
205
|
symbol,
|
|
@@ -188,6 +218,18 @@ export function visibleRun(args) {
|
|
|
188
218
|
tasks,
|
|
189
219
|
task_status: Object.fromEntries(tasks.map((task) => [task, { task, status: "pending" }])),
|
|
190
220
|
agent_status: Object.fromEntries(DEBATE_ROLES.map((role) => [role, { role, status: "pending" }])),
|
|
221
|
+
// Visible hosts call record_visible_decision once per role and round. Keep those
|
|
222
|
+
// packets separate: overwriting bull_researcher.json three times destroys the audit
|
|
223
|
+
// chain and makes a one-round shortcut indistinguishable from a full cross-exam.
|
|
224
|
+
visible_debate: {
|
|
225
|
+
contract: "role_round_audit_v1",
|
|
226
|
+
rounds_expected: councilMode(args) === "quick" ? 1 : 3,
|
|
227
|
+
rounds: { bull_researcher: {}, bear_researcher: {} },
|
|
228
|
+
qna_gate: {
|
|
229
|
+
status: councilMode(args) === "quick" ? "not_run" : "pending",
|
|
230
|
+
errors: [],
|
|
231
|
+
},
|
|
232
|
+
},
|
|
191
233
|
packets: [],
|
|
192
234
|
// The receipt resolves the user's exact choice once. This frozen list, not a dynamic
|
|
193
235
|
// roster lookup, is the run truth and every seat is part of the completeness gate.
|
|
@@ -220,9 +262,19 @@ export function visibleRun(args) {
|
|
|
220
262
|
export function visibleAgentSpecs(run, userPrompt = "") {
|
|
221
263
|
const evidence_agents = run.tasks.map((task) => ({
|
|
222
264
|
role: task,
|
|
223
|
-
title:
|
|
265
|
+
title: localized(run.language, {
|
|
266
|
+
en: `AlphaCouncil Agent ${run.symbol} ${task} evidence subagent`,
|
|
267
|
+
zh: `AlphaCouncil Agent ${run.symbol} ${task} 证据子代理`,
|
|
268
|
+
ja: `AlphaCouncil Agent ${run.symbol} ${task} 証拠サブエージェント`,
|
|
269
|
+
ko: `AlphaCouncil Agent ${run.symbol} ${task} 증거 하위 에이전트`,
|
|
270
|
+
}),
|
|
224
271
|
prompt: taskPrompt(task, run.symbol, run.as_of, userPrompt, run.language, run.grounding),
|
|
225
|
-
output_contract:
|
|
272
|
+
output_contract: localized(run.language, {
|
|
273
|
+
en: `Return one JSON evidence packet with reader-facing fields in ${run.language}.`,
|
|
274
|
+
zh: "只返回一个 JSON evidence packet。",
|
|
275
|
+
ja: "読者向けフィールドを日本語にした JSON evidence packet を1つだけ返してください。",
|
|
276
|
+
ko: "독자용 필드를 한국어로 작성한 JSON evidence packet 하나만 반환하십시오.",
|
|
277
|
+
}),
|
|
226
278
|
}));
|
|
227
279
|
const debate_agents = DEBATE_ROLES.map((role) => ({
|
|
228
280
|
role,
|
|
@@ -230,11 +282,13 @@ export function visibleAgentSpecs(run, userPrompt = "") {
|
|
|
230
282
|
prompt_template: [
|
|
231
283
|
debatePrompt(role, run),
|
|
232
284
|
"",
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
285
|
+
localized(run.language, {
|
|
286
|
+
en: "The main thread must paste the completed Evidence JSON before running this visible agent.", zh: "主线程必须先粘贴已完成的 Evidence JSON,再运行这个可见代理。", ja: "メインスレッドは、この可視エージェントを実行する前に完成済みの Evidence JSON を貼り付ける必要があります。", ko: "메인 스레드는 이 표시형 에이전트를 실행하기 전에 완료된 Evidence JSON을 붙여 넣어야 합니다.",
|
|
287
|
+
}),
|
|
288
|
+
role === "bear_researcher" ? localized(run.language, { en: "The main thread must also paste Bull argument JSON.", zh: "主线程还必须粘贴 Bull argument JSON。", ja: "メインスレッドは Bull argument JSON も貼り付ける必要があります。", ko: "메인 스레드는 Bull argument JSON도 붙여 넣어야 합니다." }) : "",
|
|
289
|
+
role === "portfolio_manager" ? localized(run.language, { en: "The main thread must also paste Bull and Bear argument JSON.", zh: "主线程还必须粘贴 Bull 和 Bear argument JSON。", ja: "メインスレッドは Bull と Bear の argument JSON も貼り付ける必要があります。", ko: "메인 스레드는 Bull 및 Bear argument JSON도 붙여 넣어야 합니다." }) : "",
|
|
236
290
|
].filter(Boolean).join("\n"),
|
|
237
|
-
output_contract:
|
|
291
|
+
output_contract: localized(run.language, { en: `Return one JSON debate packet with reader-facing fields in ${run.language}.`, zh: "只返回一个 JSON debate packet。", ja: "読者向けフィールドを日本語にした JSON debate packet を1つだけ返してください。", ko: "독자용 필드를 한국어로 작성한 JSON debate packet 하나만 반환하십시오." }),
|
|
238
292
|
}));
|
|
239
293
|
// The deterministic pass runs first and settles, for free, every seat whose method cannot
|
|
240
294
|
// reach this security. Spawning an agent for a lens that has already declined is how the
|
|
@@ -253,13 +307,19 @@ export function visibleAgentSpecs(run, userPrompt = "") {
|
|
|
253
307
|
"",
|
|
254
308
|
decision ? deterministicVerdictBlock(decision, zh) : "",
|
|
255
309
|
"",
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
: "
|
|
310
|
+
localized(run.language, {
|
|
311
|
+
en: "The main thread must paste the completed Evidence JSON first. Masters run after the evidence stage and before the debate.",
|
|
312
|
+
zh: "主线程必须先粘贴已完成的 Evidence JSON,再运行这个大师议席;大师在证据之后、辩论之前运行。",
|
|
313
|
+
ja: "メインスレッドは先に完成済みの Evidence JSON を貼り付ける必要があります。メソッド席は証拠段階の後、討論の前に実行します。",
|
|
314
|
+
ko: "메인 스레드는 먼저 완료된 Evidence JSON을 붙여 넣어야 합니다. 방법론 좌석은 증거 단계 이후, 토론 이전에 실행합니다.",
|
|
315
|
+
}),
|
|
259
316
|
].filter(Boolean).join("\n"),
|
|
260
|
-
output_contract:
|
|
261
|
-
|
|
262
|
-
:
|
|
317
|
+
output_contract: localized(run.language, {
|
|
318
|
+
en: `Return one JSON master opinion with reader-facing fields in ${run.language}.`,
|
|
319
|
+
zh: "只返回一个 JSON master opinion。",
|
|
320
|
+
ja: "読者向けフィールドを日本語にした JSON master opinion を1つだけ返してください。",
|
|
321
|
+
ko: "독자용 필드를 한국어로 작성한 JSON master opinion 하나만 반환하십시오.",
|
|
322
|
+
}),
|
|
263
323
|
}));
|
|
264
324
|
// A declined seat is settled here rather than skipped: it is written straight into the
|
|
265
325
|
// run as an out_of_scope opinion, so the completeness gate is satisfied and no agent is
|
|
@@ -402,6 +462,7 @@ export function recordMasterOpinion(args) {
|
|
|
402
462
|
run,
|
|
403
463
|
rawRecordText(args.packet),
|
|
404
464
|
);
|
|
465
|
+
assertVisibleReaderLanguage(masterReaderText(normalized), run, `visible master ${args.master}`);
|
|
405
466
|
// A narrated stance that disagrees with the arithmetic does not get to win quietly. The
|
|
406
467
|
// deterministic verdict stands and the disagreement is preserved on the record.
|
|
407
468
|
const reconciled = reconcileMasterOpinion(run, args.master, normalized);
|
|
@@ -453,6 +514,7 @@ export function recordVisiblePacket(args) {
|
|
|
453
514
|
thread_title: args.thread_title,
|
|
454
515
|
execution_mode: "visible_host_threads",
|
|
455
516
|
}, task, run.symbol, run.as_of, rawRecordText(args.packet));
|
|
517
|
+
assertVisibleReaderLanguage(visibleEvidenceReaderText(packet), run, `visible evidence ${task}`);
|
|
456
518
|
const byTask = new Map(run.packets.map((item) => [item.task, item]));
|
|
457
519
|
byTask.set(task, packet);
|
|
458
520
|
run.packets = run.tasks.map((item) => byTask.get(item)).filter(Boolean);
|
|
@@ -472,70 +534,360 @@ export function recordVisiblePacket(args) {
|
|
|
472
534
|
return run;
|
|
473
535
|
}
|
|
474
536
|
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
537
|
+
function visibleDebateState(run) {
|
|
538
|
+
const expected = run.council_mode === "quick" ? 1 : 3;
|
|
539
|
+
run.visible_debate = run.visible_debate && typeof run.visible_debate === "object"
|
|
540
|
+
? run.visible_debate
|
|
541
|
+
: {};
|
|
542
|
+
run.visible_debate.contract = "role_round_audit_v1";
|
|
543
|
+
run.visible_debate.rounds_expected = expected;
|
|
544
|
+
run.visible_debate.rounds = run.visible_debate.rounds && typeof run.visible_debate.rounds === "object"
|
|
545
|
+
? run.visible_debate.rounds
|
|
546
|
+
: {};
|
|
547
|
+
for (const role of ["bull_researcher", "bear_researcher"]) {
|
|
548
|
+
run.visible_debate.rounds[role] = run.visible_debate.rounds[role]
|
|
549
|
+
&& typeof run.visible_debate.rounds[role] === "object"
|
|
550
|
+
? run.visible_debate.rounds[role]
|
|
551
|
+
: {};
|
|
552
|
+
}
|
|
553
|
+
run.visible_debate.qna_gate = run.visible_debate.qna_gate && typeof run.visible_debate.qna_gate === "object"
|
|
554
|
+
? run.visible_debate.qna_gate
|
|
555
|
+
: { status: expected === 1 ? "not_run" : "pending", errors: [] };
|
|
556
|
+
return run.visible_debate;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function visibleRoundEntry(state, role, round) {
|
|
560
|
+
return state?.rounds?.[role]?.[String(round)] || null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function visibleRoundPacket(state, role, round) {
|
|
564
|
+
return visibleRoundEntry(state, role, round)?.packet || null;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function visibleRoundNumbers(state, role) {
|
|
568
|
+
return Object.keys(state?.rounds?.[role] || {})
|
|
569
|
+
.map(Number)
|
|
570
|
+
.filter(Number.isInteger)
|
|
571
|
+
.sort((a, b) => a - b);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function visibleDecisionContentHash(packet) {
|
|
575
|
+
const content = JSON.parse(JSON.stringify(packet || {}));
|
|
576
|
+
delete content.raw_text;
|
|
577
|
+
delete content.thread_id;
|
|
578
|
+
delete content.thread_title;
|
|
579
|
+
delete content.execution_mode;
|
|
580
|
+
return sha256(content);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function roundGateArgs(state, role, round, candidate) {
|
|
584
|
+
const get = (candidateRole, candidateRound) => (
|
|
585
|
+
candidateRole === role && candidateRound === round
|
|
586
|
+
? candidate
|
|
587
|
+
: visibleRoundPacket(state, candidateRole, candidateRound)
|
|
588
|
+
);
|
|
589
|
+
return {
|
|
590
|
+
bullR2: get("bull_researcher", 2),
|
|
591
|
+
bearR2: get("bear_researcher", 2),
|
|
592
|
+
bullR3: get("bull_researcher", 3),
|
|
593
|
+
bearR3: get("bear_researcher", 3),
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function rejectVisibleDecision(run, reason, message, data = {}) {
|
|
598
|
+
throw invalidParams(message, { reason, run_id: run.run_id, ...data });
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function assertVisibleRoundOrder(run, state, role, round) {
|
|
602
|
+
if (round === 1) return;
|
|
603
|
+
const previous = round - 1;
|
|
604
|
+
const missing = ["bull_researcher", "bear_researcher"]
|
|
605
|
+
.filter((side) => !visibleRoundEntry(state, side, previous))
|
|
606
|
+
.map((side) => `${side}:${previous}`);
|
|
607
|
+
if (missing.length) {
|
|
608
|
+
rejectVisibleDecision(
|
|
609
|
+
run,
|
|
610
|
+
"VISIBLE_DEBATE_ROUND_OUT_OF_ORDER",
|
|
611
|
+
`Cannot record ${role} round ${round} before both round ${previous} packets are recorded.`,
|
|
612
|
+
{ role, round, missing_prerequisite_rounds: missing },
|
|
613
|
+
);
|
|
479
614
|
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function assertVisibleRoundQna(run, state, role, round, packet) {
|
|
618
|
+
if (run.council_mode === "quick" || round === 1) return;
|
|
619
|
+
const gate = debateQnaGate(roundGateArgs(state, role, round, packet));
|
|
620
|
+
const ownErrors = gate.errors.filter((error) => error.startsWith(`${role} round ${round}`));
|
|
621
|
+
if (ownErrors.length) {
|
|
622
|
+
rejectVisibleDecision(
|
|
623
|
+
run,
|
|
624
|
+
"VISIBLE_DEBATE_QNA_INVALID",
|
|
625
|
+
`Rejected ${role} round ${round}: ${ownErrors.join("; ")}`,
|
|
626
|
+
{ role, round, qna_gate: { status: "failed", errors: ownErrors } },
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
const other = role === "bull_researcher" ? "bear_researcher" : "bull_researcher";
|
|
630
|
+
if (round === 3 && visibleRoundEntry(state, other, 3) && gate.status !== "passed") {
|
|
631
|
+
rejectVisibleDecision(
|
|
632
|
+
run,
|
|
633
|
+
"VISIBLE_DEBATE_QNA_INVALID",
|
|
634
|
+
`Rejected ${role} round 3 because the completed cross-exam failed the exact Q&A gate.`,
|
|
635
|
+
{ role, round, qna_gate: gate },
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function visibleRoundAgentPatch(run, state, role) {
|
|
641
|
+
const dir = runPath(run.run_id);
|
|
642
|
+
const rounds = visibleRoundNumbers(state, role);
|
|
643
|
+
const latest = rounds.length ? visibleRoundEntry(state, role, rounds.at(-1)) : null;
|
|
644
|
+
return {
|
|
645
|
+
rounds_completed: rounds,
|
|
646
|
+
last_completed_round: rounds.at(-1) || null,
|
|
647
|
+
round_status: rounds.length ? "completed" : "pending",
|
|
648
|
+
qna_gate: state.qna_gate.status,
|
|
649
|
+
thread_id: latest?.thread_id,
|
|
650
|
+
thread_title: latest?.thread_title,
|
|
651
|
+
output: join(dir, `${role}.json`),
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function recordVisibleDebateRound(run, args) {
|
|
480
656
|
const role = args.role;
|
|
481
|
-
|
|
657
|
+
const expected = run.council_mode === "quick" ? [1] : [1, 2, 3];
|
|
658
|
+
if (!Number.isInteger(args.round) || !expected.includes(args.round)) {
|
|
659
|
+
rejectVisibleDecision(
|
|
660
|
+
run,
|
|
661
|
+
"VISIBLE_DEBATE_ROUND_REQUIRED",
|
|
662
|
+
`${role} requires an explicit round=${expected.join("|")}.`,
|
|
663
|
+
{ role, supplied_round: args.round ?? null, allowed_rounds: expected },
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
const round = args.round;
|
|
482
667
|
const dir = runPath(run.run_id);
|
|
668
|
+
const state = visibleDebateState(run);
|
|
669
|
+
const packet = {
|
|
670
|
+
...normalizeDebate({
|
|
671
|
+
...(args.packet || {}),
|
|
672
|
+
thread_id: args.thread_id,
|
|
673
|
+
thread_title: args.thread_title,
|
|
674
|
+
execution_mode: "visible_host_threads",
|
|
675
|
+
}, role, run, rawRecordText(args.packet)),
|
|
676
|
+
round,
|
|
677
|
+
// A caller records exactly one round. Nested supplied rounds would create a second,
|
|
678
|
+
// unaudited history inside the packet and are therefore discarded.
|
|
679
|
+
debate_rounds: [],
|
|
680
|
+
};
|
|
681
|
+
assertVisibleReaderLanguage(debateReaderText(packet), run, `visible debate ${role} round ${round}`);
|
|
682
|
+
const contentHash = visibleDecisionContentHash(packet);
|
|
683
|
+
const existing = visibleRoundEntry(state, role, round);
|
|
684
|
+
if (existing) {
|
|
685
|
+
if (existing.content_hash !== contentHash) {
|
|
686
|
+
rejectVisibleDecision(
|
|
687
|
+
run,
|
|
688
|
+
"VISIBLE_DEBATE_ROUND_CONFLICT",
|
|
689
|
+
`Conflicting replay for ${role} round ${round}.`,
|
|
690
|
+
{ role, round, existing_content_hash: existing.content_hash, submitted_content_hash: contentHash },
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
return { run, decision: existing.packet, idempotent_replay: true };
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
assertVisibleRoundOrder(run, state, role, round);
|
|
697
|
+
assertVisibleRoundQna(run, state, role, round, packet);
|
|
698
|
+
|
|
699
|
+
const completedAt = new Date().toISOString();
|
|
700
|
+
const roundFile = join(dir, `${role}.round-${round}.json`);
|
|
701
|
+
state.rounds[role][String(round)] = {
|
|
702
|
+
round,
|
|
703
|
+
role,
|
|
704
|
+
content_hash: contentHash,
|
|
705
|
+
packet,
|
|
706
|
+
thread_id: args.thread_id,
|
|
707
|
+
thread_title: args.thread_title,
|
|
708
|
+
output: roundFile,
|
|
709
|
+
completed_at: completedAt,
|
|
710
|
+
};
|
|
711
|
+
writeJson(roundFile, packet);
|
|
712
|
+
const merged = mergeDebateRounds(visibleRoundNumbers(state, role)
|
|
713
|
+
.map((item) => visibleRoundPacket(state, role, item)));
|
|
714
|
+
writeJson(join(dir, `${role}.json`), merged);
|
|
715
|
+
|
|
716
|
+
const quick = run.council_mode === "quick";
|
|
717
|
+
const hasBothFinalRounds = ["bull_researcher", "bear_researcher"]
|
|
718
|
+
.every((side) => visibleRoundEntry(state, side, quick ? 1 : 3));
|
|
719
|
+
if (quick) {
|
|
720
|
+
state.qna_gate = { status: "not_run", errors: [], checked_at: completedAt };
|
|
721
|
+
updateAgent(run, role, "completed", {
|
|
722
|
+
...visibleRoundAgentPatch(run, state, role),
|
|
723
|
+
completed_at: completedAt,
|
|
724
|
+
});
|
|
725
|
+
} else if (hasBothFinalRounds) {
|
|
726
|
+
const qnaGate = debateQnaGate(roundGateArgs(state, role, round, packet));
|
|
727
|
+
// The candidate was validated before persistence. This guard protects future state
|
|
728
|
+
// migrations or a manually corrupted run file from manufacturing a passed audit.
|
|
729
|
+
if (qnaGate.status !== "passed") {
|
|
730
|
+
delete state.rounds[role][String(round)];
|
|
731
|
+
rejectVisibleDecision(
|
|
732
|
+
run,
|
|
733
|
+
"VISIBLE_DEBATE_QNA_INVALID",
|
|
734
|
+
"The completed visible cross-exam failed the exact Q&A gate.",
|
|
735
|
+
{ role, round, qna_gate: qnaGate },
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
state.qna_gate = { ...qnaGate, checked_at: completedAt };
|
|
739
|
+
for (const side of ["bull_researcher", "bear_researcher"]) {
|
|
740
|
+
updateAgent(run, side, "completed", {
|
|
741
|
+
...visibleRoundAgentPatch(run, state, side),
|
|
742
|
+
completed_at: completedAt,
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
appendEvent(run, "debate_qna_gate", state.qna_gate);
|
|
746
|
+
} else {
|
|
747
|
+
updateAgent(run, role, "waiting", visibleRoundAgentPatch(run, state, role));
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
run.status = "running";
|
|
751
|
+
run.phase = "visible_debate";
|
|
752
|
+
run.completed_at = null;
|
|
753
|
+
saveRun(run);
|
|
754
|
+
appendEvent(run, "visible_debate_round_recorded", {
|
|
755
|
+
role,
|
|
756
|
+
round,
|
|
757
|
+
content_hash: contentHash,
|
|
758
|
+
output: roundFile,
|
|
759
|
+
qna_gate: state.qna_gate.status,
|
|
760
|
+
});
|
|
761
|
+
writeAnalystMarkdownFiles(run, existingDebate(dir));
|
|
762
|
+
writeArtifactIndex(run, existingDebate(dir));
|
|
763
|
+
writeAllAgentsMarkdown(run, existingDebate(dir));
|
|
764
|
+
return { run, decision: packet, idempotent_replay: false };
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function visiblePmPrerequisites(run, state) {
|
|
768
|
+
const missingEvidence = (run.tasks || [])
|
|
769
|
+
.filter((task) => taskState(run, task).status !== "completed");
|
|
770
|
+
const recordedMasters = new Set((run.master_opinions || []).map((opinion) => opinion.master));
|
|
771
|
+
const missingMasters = selectedMasters(run).filter((master) => !recordedMasters.has(master));
|
|
772
|
+
const expected = run.council_mode === "quick" ? [1] : [1, 2, 3];
|
|
773
|
+
const missingRounds = ["bull_researcher", "bear_researcher"].flatMap((role) => (
|
|
774
|
+
expected.filter((round) => !visibleRoundEntry(state, role, round)).map((round) => `${role}:${round}`)
|
|
775
|
+
));
|
|
776
|
+
const missingSides = ["bull_researcher", "bear_researcher"]
|
|
777
|
+
.filter((role) => agentState(run, role).status !== "completed");
|
|
778
|
+
const qnaRequired = run.council_mode !== "quick";
|
|
779
|
+
return {
|
|
780
|
+
missing_evidence: missingEvidence,
|
|
781
|
+
missing_masters: missingMasters,
|
|
782
|
+
missing_debate_rounds: missingRounds,
|
|
783
|
+
missing_debate_sides: missingSides,
|
|
784
|
+
qna_gate: state.qna_gate,
|
|
785
|
+
passed: missingEvidence.length === 0
|
|
786
|
+
&& missingMasters.length === 0
|
|
787
|
+
&& missingRounds.length === 0
|
|
788
|
+
&& missingSides.length === 0
|
|
789
|
+
&& (!qnaRequired || state.qna_gate.status === "passed"),
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function recordVisiblePortfolioManager(run, args) {
|
|
794
|
+
if (args.round !== undefined && args.round !== null) {
|
|
795
|
+
rejectVisibleDecision(
|
|
796
|
+
run,
|
|
797
|
+
"VISIBLE_PM_ROUND_FORBIDDEN",
|
|
798
|
+
"portfolio_manager is the final decision and does not accept a debate round.",
|
|
799
|
+
{ supplied_round: args.round },
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
const dir = runPath(run.run_id);
|
|
803
|
+
const state = visibleDebateState(run);
|
|
483
804
|
const packet = normalizeDebate({
|
|
484
805
|
...(args.packet || {}),
|
|
485
806
|
thread_id: args.thread_id,
|
|
486
807
|
thread_title: args.thread_title,
|
|
487
808
|
execution_mode: "visible_host_threads",
|
|
488
|
-
},
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
809
|
+
}, "portfolio_manager", run, rawRecordText(args.packet));
|
|
810
|
+
assertVisibleReaderLanguage(debateReaderText(packet), run, "visible portfolio_manager decision");
|
|
811
|
+
const contentHash = visibleDecisionContentHash(packet);
|
|
812
|
+
if (state.portfolio_manager) {
|
|
813
|
+
if (state.portfolio_manager.content_hash !== contentHash) {
|
|
814
|
+
rejectVisibleDecision(run, "VISIBLE_PM_CONFLICT", "Conflicting portfolio_manager replay.", {
|
|
815
|
+
existing_content_hash: state.portfolio_manager.content_hash,
|
|
816
|
+
submitted_content_hash: contentHash,
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
return { run, decision: state.portfolio_manager.packet, idempotent_replay: true };
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const prerequisites = visiblePmPrerequisites(run, state);
|
|
823
|
+
if (!prerequisites.passed) {
|
|
824
|
+
rejectVisibleDecision(
|
|
825
|
+
run,
|
|
826
|
+
"VISIBLE_PM_PREREQUISITES_INCOMPLETE",
|
|
827
|
+
"portfolio_manager rejected: complete evidence, every selected master, all required bull/bear rounds, and the exact Q&A gate are mandatory first.",
|
|
828
|
+
prerequisites,
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const completedAt = new Date().toISOString();
|
|
833
|
+
const file = join(dir, "manager_synthesis.json");
|
|
834
|
+
state.portfolio_manager = {
|
|
835
|
+
content_hash: contentHash,
|
|
836
|
+
packet,
|
|
837
|
+
thread_id: args.thread_id,
|
|
838
|
+
thread_title: args.thread_title,
|
|
839
|
+
output: file,
|
|
840
|
+
completed_at: completedAt,
|
|
841
|
+
};
|
|
842
|
+
writeJson(file, packet);
|
|
843
|
+
writeJson(join(dir, "decision.json"), packet);
|
|
844
|
+
updateAgent(run, "portfolio_manager", "completed", {
|
|
845
|
+
completed_at: completedAt,
|
|
496
846
|
thread_id: args.thread_id,
|
|
497
847
|
thread_title: args.thread_title,
|
|
498
|
-
output:
|
|
848
|
+
output: file,
|
|
849
|
+
debate_qna_gate: state.qna_gate.status,
|
|
499
850
|
});
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
missing_masters: completeness.missing_masters,
|
|
512
|
-
});
|
|
513
|
-
} else if (gate.verification === "needs_verification") {
|
|
514
|
-
run.status = "needs_verification";
|
|
515
|
-
run.phase = "needs_verification";
|
|
516
|
-
appendEvent(run, "needs_verification", { missing: gate.missing_claim_source_ids.length });
|
|
517
|
-
} else {
|
|
518
|
-
run.status = "complete";
|
|
519
|
-
run.phase = "complete";
|
|
520
|
-
}
|
|
521
|
-
run.completed_at = new Date().toISOString();
|
|
522
|
-
} else {
|
|
523
|
-
run.status = "running";
|
|
524
|
-
run.phase = "visible_debate";
|
|
851
|
+
|
|
852
|
+
const gate = verificationStatus(run);
|
|
853
|
+
const completeness = completenessStatus(run);
|
|
854
|
+
if (completeness.completeness === "incomplete") {
|
|
855
|
+
// Defensive only: visiblePmPrerequisites rejects all known incomplete states before
|
|
856
|
+
// this point. Never create a final report if a future completeness field disagrees.
|
|
857
|
+
rejectVisibleDecision(run, "VISIBLE_PM_PREREQUISITES_INCOMPLETE", "portfolio_manager completeness gate failed.", {
|
|
858
|
+
missing_evidence: completeness.missing_evidence,
|
|
859
|
+
missing_debate: completeness.missing_debate,
|
|
860
|
+
missing_masters: completeness.missing_masters,
|
|
861
|
+
});
|
|
525
862
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
writeJson(join(dir, "evidence.json"), run);
|
|
531
|
-
if (run.status === "complete") appendEvent(run, "run_complete", { decision: packet.rating, winner: packet.winner });
|
|
863
|
+
if (gate.verification === "needs_verification") {
|
|
864
|
+
run.status = "needs_verification";
|
|
865
|
+
run.phase = "needs_verification";
|
|
866
|
+
appendEvent(run, "needs_verification", { missing: gate.missing_claim_source_ids.length });
|
|
532
867
|
} else {
|
|
533
|
-
|
|
534
|
-
|
|
868
|
+
run.status = "complete";
|
|
869
|
+
run.phase = "complete";
|
|
535
870
|
}
|
|
871
|
+
run.completed_at = completedAt;
|
|
872
|
+
saveRun(run);
|
|
873
|
+
const finalArtifacts = writeFinalArtifacts(run, existingDebate(dir));
|
|
874
|
+
saveRun(run);
|
|
875
|
+
if (run.status === "complete") appendEvent(run, "run_complete", { decision: packet.rating, winner: packet.winner });
|
|
536
876
|
writeStatus(run);
|
|
537
877
|
writeAllAgentsMarkdown(run, existingDebate(dir));
|
|
538
|
-
return { run, decision: packet, ...finalArtifacts };
|
|
878
|
+
return { run, decision: packet, idempotent_replay: false, ...finalArtifacts };
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
export function recordVisibleDecision(args) {
|
|
882
|
+
const run = readJson(join(runPath(args.run_id), "evidence.json"));
|
|
883
|
+
if (run.execution_mode !== "visible_host_threads") {
|
|
884
|
+
throw invalidParams("record_visible_decision requires a run created by plan_visible_run.");
|
|
885
|
+
}
|
|
886
|
+
const role = args.role;
|
|
887
|
+
if (!DEBATE_ROLES.includes(role)) throw invalidParams(`Unknown decision role: ${role}`);
|
|
888
|
+
return role === "portfolio_manager"
|
|
889
|
+
? recordVisiblePortfolioManager(run, args)
|
|
890
|
+
: recordVisibleDebateRound(run, args);
|
|
539
891
|
}
|
|
540
892
|
|
|
541
893
|
export function isDryRun(args = {}) {
|
|
@@ -550,30 +902,51 @@ export function isDryRun(args = {}) {
|
|
|
550
902
|
* but is not a sourced claim and must never enter evidence.json or downstream debate.
|
|
551
903
|
*/
|
|
552
904
|
export function workerFailureArtifacts({ task, symbol, asOfDate, language, timeoutMs, result, failureKind, parseError }) {
|
|
553
|
-
const
|
|
905
|
+
const copy = localized(language, {
|
|
906
|
+
en: {
|
|
907
|
+
parse: `Evidence worker ${task} returned output that violated the JSON contract; it produced no evidence usable for an investment decision.`,
|
|
908
|
+
language: `Evidence worker ${task} returned reader-facing content in the wrong language; it produced no evidence usable for an investment decision.`,
|
|
909
|
+
failed: `Evidence worker ${task} timed out or failed; it produced no evidence usable for an investment decision.`,
|
|
910
|
+
inspect: `Inspect the separate ${task} failure diagnostic and retry; do not use its partial transcript as evidence.`,
|
|
911
|
+
},
|
|
912
|
+
zh: {
|
|
913
|
+
parse: `证据席位 ${task} 的输出不符合 JSON 契约;未生成可用于投资判断的证据。`,
|
|
914
|
+
language: `证据席位 ${task} 返回了错误语言的读者内容;未生成可用于投资判断的证据。`,
|
|
915
|
+
failed: `证据席位 ${task} 执行超时或失败;未生成可用于投资判断的证据。`,
|
|
916
|
+
inspect: `检查 ${task} 的独立失败诊断并重试;不得用该席位的部分对话补齐证据。`,
|
|
917
|
+
},
|
|
918
|
+
ja: {
|
|
919
|
+
parse: `証拠席 ${task} の出力は JSON 契約に違反しており、投資判断に使用できる証拠は生成されませんでした。`,
|
|
920
|
+
language: `証拠席 ${task} は指定と異なる言語の読者向け内容を返したため、投資判断に使用できる証拠は生成されませんでした。`,
|
|
921
|
+
failed: `証拠席 ${task} はタイムアウトまたは失敗し、投資判断に使用できる証拠は生成されませんでした。`,
|
|
922
|
+
inspect: `${task} の分離された失敗診断を確認して再実行してください。部分的な対話を証拠として使用してはいけません。`,
|
|
923
|
+
},
|
|
924
|
+
ko: {
|
|
925
|
+
parse: `증거 좌석 ${task}의 출력이 JSON 계약을 위반해 투자 판단에 사용할 수 있는 증거를 생성하지 못했습니다.`,
|
|
926
|
+
language: `증거 좌석 ${task}가 지정과 다른 언어의 독자용 내용을 반환해 투자 판단에 사용할 수 있는 증거를 생성하지 못했습니다.`,
|
|
927
|
+
failed: `증거 좌석 ${task}가 시간 초과 또는 실패로 투자 판단에 사용할 수 있는 증거를 생성하지 못했습니다.`,
|
|
928
|
+
inspect: `${task}의 분리된 실패 진단을 확인한 뒤 다시 실행하십시오. 부분 대화를 증거로 사용해서는 안 됩니다.`,
|
|
929
|
+
},
|
|
930
|
+
});
|
|
554
931
|
const timedOut = result?.timedOut === true;
|
|
555
932
|
const parseFailed = failureKind === "parse_failed";
|
|
556
|
-
const
|
|
933
|
+
const languageMismatch = failureKind === "reader_language_mismatch";
|
|
934
|
+
const outputContractFailed = parseFailed || languageMismatch;
|
|
935
|
+
const status = outputContractFailed ? failureKind : (timedOut ? "timed_out" : "failed");
|
|
557
936
|
const exitLabel = Number.isInteger(result?.code) ? `exit code ${result.code}` : "worker error";
|
|
558
937
|
const parseMessage = cleanLog(parseError?.message || parseError || "subagent did not return valid JSON", 1_000);
|
|
559
|
-
const reason =
|
|
938
|
+
const reason = outputContractFailed ? parseMessage : (timedOut ? `timeout after ${timeoutMs}ms` : exitLabel);
|
|
560
939
|
const rawOutput = String(result?.text || "");
|
|
561
|
-
const positionMatch =
|
|
940
|
+
const positionMatch = outputContractFailed ? /\bposition\s+(\d+)\b/i.exec(parseMessage) : null;
|
|
562
941
|
const parsePosition = positionMatch ? Number(positionMatch[1]) : null;
|
|
563
942
|
const contextStart = Number.isInteger(parsePosition) ? Math.max(0, parsePosition - 500) : 0;
|
|
564
943
|
const contextEnd = Number.isInteger(parsePosition) ? Math.min(rawOutput.length, parsePosition + 500) : 0;
|
|
565
944
|
const packet = normalizePacket({
|
|
566
945
|
summary: parseFailed
|
|
567
|
-
?
|
|
568
|
-
|
|
569
|
-
: `Evidence worker ${task} returned output that violated the JSON contract; it produced no evidence usable for an investment decision.`)
|
|
570
|
-
: (chinese
|
|
571
|
-
? `证据席位 ${task} 执行超时或失败;未生成可用于投资判断的证据。`
|
|
572
|
-
: `Evidence worker ${task} timed out or failed; it produced no evidence usable for an investment decision.`),
|
|
946
|
+
? copy.parse
|
|
947
|
+
: languageMismatch ? copy.language : copy.failed,
|
|
573
948
|
claims: [],
|
|
574
|
-
open_questions: [
|
|
575
|
-
? `检查 ${task} 的独立失败诊断并重试;不得用该席位的部分对话补齐证据。`
|
|
576
|
-
: `Inspect the separate ${task} failure diagnostic and retry; do not use its partial transcript as evidence.`],
|
|
949
|
+
open_questions: [copy.inspect],
|
|
577
950
|
confidence: "low",
|
|
578
951
|
}, task, symbol, asOfDate, "");
|
|
579
952
|
const diagnostic = {
|
|
@@ -586,11 +959,11 @@ export function workerFailureArtifacts({ task, symbol, asOfDate, language, timeo
|
|
|
586
959
|
timeout_ms: timeoutMs,
|
|
587
960
|
exit_code: Number.isInteger(result?.code) ? result.code : null,
|
|
588
961
|
reason,
|
|
589
|
-
diagnostic_excerpt: cleanLog(
|
|
962
|
+
diagnostic_excerpt: cleanLog(outputContractFailed
|
|
590
963
|
? (rawOutput || result?.stderr || result?.stdout || reason)
|
|
591
964
|
: (result?.stderr || result?.stdout || rawOutput || reason)),
|
|
592
|
-
...(
|
|
593
|
-
parse_error: parseMessage,
|
|
965
|
+
...(outputContractFailed ? {
|
|
966
|
+
...(parseFailed ? { parse_error: parseMessage } : { reader_language_error: parseMessage }),
|
|
594
967
|
parse_position: Number.isInteger(parsePosition) ? parsePosition : null,
|
|
595
968
|
parse_context: Number.isInteger(parsePosition)
|
|
596
969
|
? cleanLog(rawOutput.slice(contextStart, contextEnd), 1_000)
|
|
@@ -603,6 +976,68 @@ export function workerFailureArtifacts({ task, symbol, asOfDate, language, timeo
|
|
|
603
976
|
return { packet, diagnostic };
|
|
604
977
|
}
|
|
605
978
|
|
|
979
|
+
function evidenceReaderText(packet) {
|
|
980
|
+
return [
|
|
981
|
+
packet?.summary,
|
|
982
|
+
...(packet?.claims || []).flatMap((claim) => [claim?.claim, claim?.evidence]),
|
|
983
|
+
...(packet?.open_questions || []),
|
|
984
|
+
].filter(Boolean).join("\n");
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function readerStrings(value, skipKeys = new Set()) {
|
|
988
|
+
if (typeof value === "string") return value.trim() ? [value] : [];
|
|
989
|
+
if (Array.isArray(value)) return value.flatMap((item) => readerStrings(item, skipKeys));
|
|
990
|
+
if (!value || typeof value !== "object") return [];
|
|
991
|
+
return Object.entries(value).flatMap(([key, item]) => (
|
|
992
|
+
skipKeys.has(key) ? [] : readerStrings(item, skipKeys)
|
|
993
|
+
));
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function visibleEvidenceReaderText(packet) {
|
|
997
|
+
const machine = new Set([
|
|
998
|
+
"task", "symbol", "as_of", "source_ids", "id", "url", "published_at", "retrieved_at",
|
|
999
|
+
"confidence", "information_richness", "thread_id", "execution_mode", "raw_text",
|
|
1000
|
+
]);
|
|
1001
|
+
return [
|
|
1002
|
+
...readerStrings(packet, machine),
|
|
1003
|
+
].filter(Boolean).join("\n");
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function masterReaderText(opinion) {
|
|
1007
|
+
const machine = new Set([
|
|
1008
|
+
"master", "symbol", "as_of", "stance", "source_ids", "confidence", "thread_id", "raw_text",
|
|
1009
|
+
"engine", "voice_status", "voice_language", "dedicated_worker", "runtime_provenance",
|
|
1010
|
+
]);
|
|
1011
|
+
return readerStrings(opinion, machine).join("\n");
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function debateReaderText(packet) {
|
|
1015
|
+
const machine = new Set([
|
|
1016
|
+
"role", "symbol", "as_of", "decision_available", "rating", "winner", "source_ids", "confidence",
|
|
1017
|
+
"thread_id", "execution_mode", "raw_text", "round", "debate_rounds",
|
|
1018
|
+
]);
|
|
1019
|
+
return readerStrings(packet, machine).join("\n");
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function assertVisibleReaderLanguage(text, run, label) {
|
|
1023
|
+
try {
|
|
1024
|
+
return assertReaderLanguage(text, run.language, label);
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
if (error?.code !== "READER_LANGUAGE_MISMATCH") throw error;
|
|
1027
|
+
throw invalidParams(error.message, {
|
|
1028
|
+
reason: "READER_LANGUAGE_MISMATCH",
|
|
1029
|
+
label,
|
|
1030
|
+
...error.data,
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function outputFailureKind(error) {
|
|
1036
|
+
return error?.code === "READER_LANGUAGE_MISMATCH"
|
|
1037
|
+
? "reader_language_mismatch"
|
|
1038
|
+
: "parse_failed";
|
|
1039
|
+
}
|
|
1040
|
+
|
|
606
1041
|
/**
|
|
607
1042
|
* A production headless run must not silently become a prompt-only council because its
|
|
608
1043
|
* caller omitted `grounding`. Visible runs already collect it in rpc.mjs; this gives the
|
|
@@ -669,7 +1104,7 @@ export function queueHeadlessRun(args) {
|
|
|
669
1104
|
const dir = runPath(id);
|
|
670
1105
|
mkdirSync(dir, { recursive: true });
|
|
671
1106
|
const startedAt = new Date().toISOString();
|
|
672
|
-
const timing =
|
|
1107
|
+
const timing = councilTiming(args, startedAt);
|
|
673
1108
|
const run = {
|
|
674
1109
|
run_id: id,
|
|
675
1110
|
symbol,
|
|
@@ -723,21 +1158,32 @@ export async function collectEvidence(args) {
|
|
|
723
1158
|
const language = resolveLanguage(args);
|
|
724
1159
|
const frozen = frozenMasterSelection(args);
|
|
725
1160
|
const startedAt = args.queued_run?.started_at || new Date().toISOString();
|
|
726
|
-
const timing =
|
|
1161
|
+
const timing = councilTiming(args, startedAt);
|
|
727
1162
|
const requestedTimeoutMs = Number.isFinite(args.timeout_ms) ? args.timeout_ms : LIMITS.CODEX_TIMEOUT_MS;
|
|
728
1163
|
const timeoutMs = timing.council_mode === "quick"
|
|
729
1164
|
? Math.min(requestedTimeoutMs, LIMITS.QUICK_EVIDENCE_MS)
|
|
730
|
-
: requestedTimeoutMs;
|
|
731
|
-
const defaultConcurrency = timing.council_mode === "quick"
|
|
732
|
-
|
|
1165
|
+
: Math.min(requestedTimeoutMs, LIMITS.FULL_EVIDENCE_MS);
|
|
1166
|
+
const defaultConcurrency = timing.council_mode === "quick"
|
|
1167
|
+
? QUICK_TASKS.length
|
|
1168
|
+
: Math.min(tasks.length, LIMITS.FULL_EVIDENCE_CONCURRENCY);
|
|
1169
|
+
// The evidence topology is contractual: every planned evidence seat starts in the same
|
|
1170
|
+
// wave. A legacy low max_concurrency override may not silently turn the full fan-out back
|
|
1171
|
+
// into the old three-wave, ~30-minute evidence stage. Higher values have no effect once
|
|
1172
|
+
// every task can launch.
|
|
1173
|
+
const requestedConcurrency = Number(args.max_concurrency || defaultConcurrency);
|
|
1174
|
+
const maxConcurrency = Math.max(
|
|
1175
|
+
defaultConcurrency,
|
|
1176
|
+
Math.min(LIMITS.CONCURRENCY_MAX, Number.isFinite(requestedConcurrency) ? requestedConcurrency : defaultConcurrency),
|
|
1177
|
+
);
|
|
733
1178
|
const grounding = await groundingForHeadlessRun({
|
|
734
1179
|
symbol,
|
|
735
1180
|
asOf: asOfDate,
|
|
736
1181
|
grounding: args.grounding,
|
|
737
1182
|
dryRun,
|
|
738
|
-
timeoutMs:
|
|
739
|
-
|
|
740
|
-
:
|
|
1183
|
+
timeoutMs: remainingCouncilBudget(
|
|
1184
|
+
timing,
|
|
1185
|
+
timing.council_mode === "quick" ? LIMITS.QUICK_GROUNDING_MS : LIMITS.FULL_GROUNDING_MS,
|
|
1186
|
+
),
|
|
741
1187
|
});
|
|
742
1188
|
const dir = runPath(id);
|
|
743
1189
|
mkdirSync(dir, { recursive: true });
|
|
@@ -847,15 +1293,15 @@ export async function collectEvidence(args) {
|
|
|
847
1293
|
return packet;
|
|
848
1294
|
}
|
|
849
1295
|
const workerStartedAt = Date.now();
|
|
850
|
-
const runAttempt = async (workerPrompt, budgetMs, attempt) => {
|
|
851
|
-
const allowedMs =
|
|
852
|
-
if (allowedMs <= 0) return { ...deadlineResult(), budget_ms: 0 };
|
|
1296
|
+
const runAttempt = async (workerPrompt, budgetMs, attempt, { search = true } = {}) => {
|
|
1297
|
+
const allowedMs = remainingCouncilBudget(run, budgetMs);
|
|
1298
|
+
if (allowedMs <= 0) return { ...deadlineResult(run), budget_ms: 0 };
|
|
853
1299
|
const result = await runCodex(workerPrompt, allowedMs, ({ pid, output }) => {
|
|
854
1300
|
updateTask(run, task, "running", { pid, output, attempts: attempt });
|
|
855
1301
|
}, ({ pid, output, elapsed_ms }) => {
|
|
856
1302
|
updateTask(run, task, "running", { pid, output, attempts: attempt });
|
|
857
1303
|
appendEvent(run, "task_heartbeat", { task, pid, output, elapsed_ms, attempt });
|
|
858
|
-
});
|
|
1304
|
+
}, { search, sigkillGraceMs: councilKillGrace(run) });
|
|
859
1305
|
return { ...result, budget_ms: allowedMs };
|
|
860
1306
|
};
|
|
861
1307
|
const commitFailure = ({ failedResult, budgetMs, attempts, failureKind, parseError, retryDiagnostic }) => {
|
|
@@ -882,8 +1328,8 @@ export async function collectEvidence(args) {
|
|
|
882
1328
|
...(retryDiagnostic ? { retry_diagnostic: retryDiagnostic } : {}),
|
|
883
1329
|
attempts,
|
|
884
1330
|
deadline_exhausted: failedResult.deadline_exhausted === true,
|
|
885
|
-
error:
|
|
886
|
-
?
|
|
1331
|
+
error: ["parse_failed", "reader_language_mismatch"].includes(failureKind)
|
|
1332
|
+
? failureKind
|
|
887
1333
|
: (failedResult.deadline_exhausted ? "global_deadline" : failedResult.timedOut ? "timeout" : `exit code ${failedResult.code}`),
|
|
888
1334
|
});
|
|
889
1335
|
return failure.packet;
|
|
@@ -896,10 +1342,12 @@ export async function collectEvidence(args) {
|
|
|
896
1342
|
let packet;
|
|
897
1343
|
try {
|
|
898
1344
|
packet = normalizePacket(extractJson(result.text), task, symbol, asOfDate, result.text);
|
|
1345
|
+
assertReaderLanguage(evidenceReaderText(packet), language, `evidence worker ${task}`);
|
|
899
1346
|
commitPacket(packet);
|
|
900
1347
|
updateTask(run, task, "completed", { completed_at: new Date().toISOString(), output: join(dir, `${task}.json`), attempts: 1 });
|
|
901
1348
|
return packet;
|
|
902
1349
|
} catch (firstParseError) {
|
|
1350
|
+
const firstFailureKind = outputFailureKind(firstParseError);
|
|
903
1351
|
const firstFailure = workerFailureArtifacts({
|
|
904
1352
|
task,
|
|
905
1353
|
symbol,
|
|
@@ -907,19 +1355,23 @@ export async function collectEvidence(args) {
|
|
|
907
1355
|
language,
|
|
908
1356
|
timeoutMs,
|
|
909
1357
|
result,
|
|
910
|
-
failureKind:
|
|
1358
|
+
failureKind: firstFailureKind,
|
|
911
1359
|
parseError: firstParseError,
|
|
912
1360
|
});
|
|
913
1361
|
const retryDiagnostic = join(dir, `${task}.attempt-1.failure.json`);
|
|
914
1362
|
writeJson(retryDiagnostic, firstFailure.diagnostic, { mode: 0o600 });
|
|
915
1363
|
const elapsedMs = Date.now() - workerStartedAt;
|
|
916
|
-
const retryTimeoutMs = Math.min(
|
|
1364
|
+
const retryTimeoutMs = Math.min(
|
|
1365
|
+
LIMITS.PARSE_REPAIR_MS,
|
|
1366
|
+
timeoutMs - elapsedMs,
|
|
1367
|
+
remainingCouncilBudget(run, LIMITS.PARSE_REPAIR_MS),
|
|
1368
|
+
);
|
|
917
1369
|
if (retryTimeoutMs <= 0) {
|
|
918
1370
|
return commitFailure({
|
|
919
1371
|
failedResult: result,
|
|
920
1372
|
budgetMs: result.budget_ms ?? timeoutMs,
|
|
921
1373
|
attempts: 1,
|
|
922
|
-
failureKind:
|
|
1374
|
+
failureKind: firstFailureKind,
|
|
923
1375
|
parseError: firstParseError,
|
|
924
1376
|
retryDiagnostic,
|
|
925
1377
|
});
|
|
@@ -929,13 +1381,22 @@ export async function collectEvidence(args) {
|
|
|
929
1381
|
task,
|
|
930
1382
|
attempt: 2,
|
|
931
1383
|
max_attempts: 2,
|
|
932
|
-
reason:
|
|
1384
|
+
reason: firstFailureKind,
|
|
933
1385
|
retry_diagnostic: retryDiagnostic,
|
|
934
1386
|
remaining_ms: retryTimeoutMs,
|
|
935
1387
|
});
|
|
936
1388
|
updateTask(run, task, "running", { attempts: 2, retry_diagnostic: retryDiagnostic });
|
|
937
|
-
const
|
|
938
|
-
|
|
1389
|
+
const malformed = String(result.text || "").slice(0, LIMITS.PARSE_REPAIR_INPUT_CHARS);
|
|
1390
|
+
const retryPrompt = [
|
|
1391
|
+
"PARSE-ONLY TRANSPORT REPAIR. Do not search, browse, fetch, add facts, or redo the research.",
|
|
1392
|
+
`Target task: ${task}; symbol: ${symbol}; as_of: ${asOfDate}; reader language: ${language}.`,
|
|
1393
|
+
firstFailureKind === "reader_language_mismatch"
|
|
1394
|
+
? "Translate only the reader-facing strings in the supplied valid JSON into the requested language. Preserve the evidence packet schema, claims, numbers, source IDs, URLs, dates, explicit gaps and uncertainty. Return exactly one JSON object and nothing else."
|
|
1395
|
+
: "Convert only the supplied malformed worker output into exactly one valid JSON object matching the evidence packet schema. Preserve claims, numbers, source IDs, URLs, dates, explicit gaps and uncertainty. If a field cannot be recovered, use an empty value and record the loss in open_questions. Return JSON only.",
|
|
1396
|
+
`Write every reader-facing value in ${language}. Translation is permitted only to repair the language mismatch; do not alter facts, numbers, dates, source IDs or uncertainty.`,
|
|
1397
|
+
`Malformed worker output:\n${malformed}`,
|
|
1398
|
+
].join("\n\n");
|
|
1399
|
+
result = await runAttempt(retryPrompt, retryTimeoutMs, 2, { search: false });
|
|
939
1400
|
if (!result.ok) {
|
|
940
1401
|
return commitFailure({
|
|
941
1402
|
failedResult: result,
|
|
@@ -946,6 +1407,7 @@ export async function collectEvidence(args) {
|
|
|
946
1407
|
}
|
|
947
1408
|
try {
|
|
948
1409
|
packet = normalizePacket(extractJson(result.text), task, symbol, asOfDate, result.text);
|
|
1410
|
+
assertReaderLanguage(evidenceReaderText(packet), language, `evidence worker ${task} repair`);
|
|
949
1411
|
commitPacket(packet);
|
|
950
1412
|
updateTask(run, task, "completed", {
|
|
951
1413
|
completed_at: new Date().toISOString(),
|
|
@@ -955,11 +1417,12 @@ export async function collectEvidence(args) {
|
|
|
955
1417
|
});
|
|
956
1418
|
return packet;
|
|
957
1419
|
} catch (secondParseError) {
|
|
1420
|
+
const secondFailureKind = outputFailureKind(secondParseError);
|
|
958
1421
|
return commitFailure({
|
|
959
1422
|
failedResult: result,
|
|
960
1423
|
budgetMs: result.budget_ms ?? retryTimeoutMs,
|
|
961
1424
|
attempts: 2,
|
|
962
|
-
failureKind:
|
|
1425
|
+
failureKind: secondFailureKind,
|
|
963
1426
|
parseError: secondParseError,
|
|
964
1427
|
retryDiagnostic,
|
|
965
1428
|
});
|
|
@@ -1022,11 +1485,14 @@ export async function runHeadlessMasters(run, args = {}) {
|
|
|
1022
1485
|
const requestedTimeoutMs = Number.isFinite(args.timeout_ms) ? args.timeout_ms : LIMITS.CODEX_TIMEOUT_MS;
|
|
1023
1486
|
const timeoutMs = run.council_mode === "quick"
|
|
1024
1487
|
? Math.min(requestedTimeoutMs, LIMITS.QUICK_MASTER_MS)
|
|
1025
|
-
: requestedTimeoutMs;
|
|
1026
|
-
const defaultConcurrency = run.council_mode === "quick"
|
|
1488
|
+
: Math.min(requestedTimeoutMs, LIMITS.FULL_MASTER_MS);
|
|
1489
|
+
const defaultConcurrency = run.council_mode === "quick"
|
|
1490
|
+
? 4
|
|
1491
|
+
: Math.min(selected.length, LIMITS.FULL_MASTER_CONCURRENCY);
|
|
1492
|
+
const requestedConcurrency = Number(args.max_concurrency || defaultConcurrency);
|
|
1027
1493
|
const maxConcurrency = Math.max(
|
|
1028
|
-
|
|
1029
|
-
Math.min(LIMITS.CONCURRENCY_MAX, Number(
|
|
1494
|
+
defaultConcurrency,
|
|
1495
|
+
Math.min(LIMITS.CONCURRENCY_MAX, Number.isFinite(requestedConcurrency) ? requestedConcurrency : defaultConcurrency),
|
|
1030
1496
|
);
|
|
1031
1497
|
const plan = planMasterSeats(run, selected);
|
|
1032
1498
|
const byId = new Map((run.master_opinions || []).map((opinion) => [opinion.master, opinion]));
|
|
@@ -1045,40 +1511,6 @@ export async function runHeadlessMasters(run, args = {}) {
|
|
|
1045
1511
|
blocked: plan.blocked.length,
|
|
1046
1512
|
});
|
|
1047
1513
|
|
|
1048
|
-
for (const item of plan.declined) {
|
|
1049
|
-
const opinion = attachMasterRuntimeProvenance(
|
|
1050
|
-
run,
|
|
1051
|
-
item.id,
|
|
1052
|
-
declinedMasterOpinion(run, item),
|
|
1053
|
-
item.engine,
|
|
1054
|
-
);
|
|
1055
|
-
byId.set(item.id, opinion);
|
|
1056
|
-
writeJson(join(dir, `${item.id}.json`), opinion);
|
|
1057
|
-
updateMasterStatus(run, item.id, "completed", {
|
|
1058
|
-
engine: item.engine || opinion.engine,
|
|
1059
|
-
deterministic_decline: true,
|
|
1060
|
-
completed_at: new Date().toISOString(),
|
|
1061
|
-
});
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
for (const item of plan.completed) {
|
|
1065
|
-
const opinion = attachMasterRuntimeProvenance(
|
|
1066
|
-
run,
|
|
1067
|
-
item.id,
|
|
1068
|
-
completedMasterOpinion(run, item),
|
|
1069
|
-
item.engine,
|
|
1070
|
-
);
|
|
1071
|
-
byId.set(item.id, opinion);
|
|
1072
|
-
writeJson(join(dir, `${item.id}.json`), opinion);
|
|
1073
|
-
updateMasterStatus(run, item.id, "completed", {
|
|
1074
|
-
engine: item.engine,
|
|
1075
|
-
deterministic_execution: true,
|
|
1076
|
-
policy_execution_hash: opinion.policy_execution_hash,
|
|
1077
|
-
frozen_decision_hash: opinion.frozen_decision_hash,
|
|
1078
|
-
completed_at: new Date().toISOString(),
|
|
1079
|
-
});
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
1514
|
for (const item of plan.blocked) {
|
|
1083
1515
|
updateMasterStatus(run, item.id, "failed", {
|
|
1084
1516
|
engine: item.engine,
|
|
@@ -1087,48 +1519,155 @@ export async function runHeadlessMasters(run, args = {}) {
|
|
|
1087
1519
|
});
|
|
1088
1520
|
}
|
|
1089
1521
|
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1522
|
+
const workerItems = [
|
|
1523
|
+
...plan.declined.map((item) => ({
|
|
1524
|
+
...item,
|
|
1525
|
+
frozenOpinion: attachMasterRuntimeProvenance(run, item.id, declinedMasterOpinion(run, item), item.engine),
|
|
1526
|
+
deterministic_decline: true,
|
|
1527
|
+
})),
|
|
1528
|
+
...plan.completed.map((item) => ({
|
|
1529
|
+
...item,
|
|
1530
|
+
frozenOpinion: attachMasterRuntimeProvenance(run, item.id, completedMasterOpinion(run, item), item.engine),
|
|
1531
|
+
deterministic_execution: true,
|
|
1532
|
+
})),
|
|
1533
|
+
...plan.to_run,
|
|
1534
|
+
];
|
|
1535
|
+
for (const item of workerItems) {
|
|
1536
|
+
if (!item.frozenOpinion) continue;
|
|
1537
|
+
writeJson(join(dir, `${item.id}.deterministic.json`), item.frozenOpinion);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
const outcomes = await mapLimit(workerItems, maxConcurrency, async ({ id, decision, engine, frozenOpinion, deterministic_decline, deterministic_execution }) => {
|
|
1541
|
+
const prompt = frozenOpinion
|
|
1542
|
+
? masterVoicePrompt(id, run, frozenOpinion)
|
|
1543
|
+
: [
|
|
1544
|
+
masterPrompt(id, run),
|
|
1545
|
+
decision ? deterministicVerdictBlock(decision, isChineseLanguage(run.language)) : "",
|
|
1546
|
+
].filter(Boolean).join("\n\n");
|
|
1547
|
+
updateMasterStatus(run, id, "running", {
|
|
1548
|
+
started_at: new Date().toISOString(),
|
|
1549
|
+
worker_kind: frozenOpinion ? "dedicated_method_voice" : "dedicated_method_judgment",
|
|
1550
|
+
deterministic_decline: deterministic_decline || undefined,
|
|
1551
|
+
deterministic_execution: deterministic_execution || undefined,
|
|
1552
|
+
});
|
|
1096
1553
|
|
|
1097
1554
|
if (run.dry_run) {
|
|
1555
|
+
if (frozenOpinion) {
|
|
1556
|
+
return {
|
|
1557
|
+
id,
|
|
1558
|
+
engine,
|
|
1559
|
+
opinion: {
|
|
1560
|
+
...frozenOpinion,
|
|
1561
|
+
deterministic_summary: frozenOpinion.summary,
|
|
1562
|
+
voice_statement: frozenOpinion.summary,
|
|
1563
|
+
voice_status: "dry_run",
|
|
1564
|
+
dedicated_worker: { status: "dry_run", language: run.language, execution_mode: "dry_run" },
|
|
1565
|
+
},
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1098
1568
|
const normalized = normalizeMasterOpinion({
|
|
1099
1569
|
verdict: "DRY_RUN",
|
|
1100
1570
|
stance: "out_of_scope",
|
|
1101
|
-
summary: "Dry run: the
|
|
1571
|
+
summary: "Dry run: the dedicated method worker was planned but no model judgment was executed.",
|
|
1102
1572
|
what_would_change_my_mind: ["Run without dry_run to obtain a method judgment."],
|
|
1103
1573
|
confidence: "low",
|
|
1104
1574
|
}, id, run, prompt);
|
|
1105
1575
|
const reconciled = reconcileMasterOpinion(run, id, normalized);
|
|
1106
1576
|
const resolvedEngine = engine || reconciled.engine || (decision ? "v2_method_model" : "v1_prompt");
|
|
1107
|
-
return { id, opinion: attachMasterRuntimeProvenance(run, id, reconciled.opinion, resolvedEngine), engine: resolvedEngine };
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
const allowedMs = remainingQuickBudget(run, timeoutMs);
|
|
1111
|
-
const result = allowedMs <= 0
|
|
1112
|
-
? deadlineResult()
|
|
1113
|
-
: await runCodex(prompt, allowedMs, ({ pid, output }) => {
|
|
1114
|
-
updateMasterStatus(run, id, "running", { pid, output });
|
|
1115
|
-
}, ({ pid, output, elapsed_ms }) => {
|
|
1116
|
-
updateMasterStatus(run, id, "running", { pid, output, elapsed_ms });
|
|
1117
|
-
});
|
|
1118
|
-
if (!result.ok) {
|
|
1119
1577
|
return {
|
|
1120
1578
|
id,
|
|
1121
|
-
|
|
1122
|
-
|
|
1579
|
+
opinion: attachMasterRuntimeProvenance(run, id, {
|
|
1580
|
+
...reconciled.opinion,
|
|
1581
|
+
voice_status: "dry_run",
|
|
1582
|
+
dedicated_worker: { status: "dry_run", language: run.language, execution_mode: "dry_run" },
|
|
1583
|
+
}, resolvedEngine),
|
|
1584
|
+
engine: resolvedEngine,
|
|
1123
1585
|
};
|
|
1124
1586
|
}
|
|
1125
|
-
|
|
1587
|
+
|
|
1588
|
+
let pid = null;
|
|
1589
|
+
const execute = async (workerPrompt, budgetMs, attempt) => {
|
|
1590
|
+
const allowedMs = remainingCouncilBudget(run, budgetMs);
|
|
1591
|
+
if (allowedMs <= 0) return { ...deadlineResult(run), budget_ms: 0 };
|
|
1592
|
+
const result = await runCodex(workerPrompt, allowedMs, ({ pid: workerPid, output }) => {
|
|
1593
|
+
pid = workerPid;
|
|
1594
|
+
updateMasterStatus(run, id, "running", { pid: workerPid, output, attempts: attempt });
|
|
1595
|
+
}, ({ pid: workerPid, output, elapsed_ms }) => {
|
|
1596
|
+
updateMasterStatus(run, id, "running", { pid: workerPid, output, elapsed_ms, attempts: attempt });
|
|
1597
|
+
}, { search: false, sigkillGraceMs: councilKillGrace(run) });
|
|
1598
|
+
return { ...result, budget_ms: allowedMs };
|
|
1599
|
+
};
|
|
1600
|
+
const parse = (result) => {
|
|
1601
|
+
if (frozenOpinion) {
|
|
1602
|
+
const voice = normalizeMasterVoice(extractJson(result.text), id, run, frozenOpinion, result.text);
|
|
1603
|
+
assertReaderLanguage([
|
|
1604
|
+
voice.statement,
|
|
1605
|
+
...(voice.key_findings || []),
|
|
1606
|
+
...(voice.disagreements || []),
|
|
1607
|
+
...(voice.what_would_change_my_mind || []),
|
|
1608
|
+
].filter(Boolean).join("\n"), run.language, `master voice ${id}`);
|
|
1609
|
+
return attachMasterRuntimeProvenance(run, id, {
|
|
1610
|
+
...frozenOpinion,
|
|
1611
|
+
deterministic_summary: frozenOpinion.summary,
|
|
1612
|
+
summary: voice.statement,
|
|
1613
|
+
voice_statement: voice.statement,
|
|
1614
|
+
voice_status: "completed",
|
|
1615
|
+
voice_language: run.language,
|
|
1616
|
+
key_findings: voice.key_findings.length ? voice.key_findings : frozenOpinion.key_findings,
|
|
1617
|
+
disagreements: voice.disagreements,
|
|
1618
|
+
what_would_change_my_mind: voice.what_would_change_my_mind.length
|
|
1619
|
+
? voice.what_would_change_my_mind
|
|
1620
|
+
: frozenOpinion.what_would_change_my_mind,
|
|
1621
|
+
source_ids: [...new Set([...(frozenOpinion.source_ids || []), ...voice.source_ids])],
|
|
1622
|
+
confidence: voice.confidence,
|
|
1623
|
+
dedicated_worker: { status: "completed", pid, language: run.language, execution_mode: "codex_exec" },
|
|
1624
|
+
}, engine);
|
|
1625
|
+
}
|
|
1126
1626
|
const normalized = normalizeMasterOpinion(extractJson(result.text), id, run, result.text);
|
|
1627
|
+
assertReaderLanguage([
|
|
1628
|
+
normalized.summary,
|
|
1629
|
+
normalized.verdict,
|
|
1630
|
+
...(normalized.key_findings || []),
|
|
1631
|
+
...(normalized.disagreements || []),
|
|
1632
|
+
...(normalized.what_would_change_my_mind || []),
|
|
1633
|
+
].filter(Boolean).join("\n"), run.language, `legacy master voice ${id}`);
|
|
1127
1634
|
const reconciled = reconcileMasterOpinion(run, id, normalized);
|
|
1128
1635
|
const resolvedEngine = engine || reconciled.engine || (decision ? "v2_method_model" : "v1_prompt");
|
|
1129
|
-
return
|
|
1130
|
-
|
|
1131
|
-
|
|
1636
|
+
return attachMasterRuntimeProvenance(run, id, {
|
|
1637
|
+
...reconciled.opinion,
|
|
1638
|
+
voice_statement: reconciled.opinion.summary || reconciled.opinion.verdict,
|
|
1639
|
+
voice_status: "completed",
|
|
1640
|
+
voice_language: run.language,
|
|
1641
|
+
dedicated_worker: { status: "completed", pid, language: run.language, execution_mode: "codex_exec" },
|
|
1642
|
+
}, resolvedEngine);
|
|
1643
|
+
};
|
|
1644
|
+
|
|
1645
|
+
let result = await execute(prompt, timeoutMs, 1);
|
|
1646
|
+
if (!result.ok) {
|
|
1647
|
+
return { id, error: result.deadline_exhausted ? "global_deadline" : result.timedOut ? "timeout" : `exit code ${result.code}`, raw: cleanLog(result.stderr || result.stdout || "method worker failed") };
|
|
1648
|
+
}
|
|
1649
|
+
try {
|
|
1650
|
+
return { id, opinion: parse(result), engine };
|
|
1651
|
+
} catch (firstParseError) {
|
|
1652
|
+
const firstFailureKind = outputFailureKind(firstParseError);
|
|
1653
|
+
const repairBudget = Math.min(LIMITS.PARSE_REPAIR_MS, remainingCouncilBudget(run, LIMITS.PARSE_REPAIR_MS));
|
|
1654
|
+
if (repairBudget <= 0) return { id, error: firstFailureKind, raw: cleanLog(firstParseError?.message || result.text) };
|
|
1655
|
+
const repairPrompt = [
|
|
1656
|
+
"PARSE-ONLY TRANSPORT REPAIR. Do not browse, search, add facts or change the frozen method stance.",
|
|
1657
|
+
`Master ID: ${id}; required acknowledged stance: ${frozenOpinion?.stance || "use the original stance"}; output language: ${run.language}.`,
|
|
1658
|
+
frozenOpinion
|
|
1659
|
+
? "Return one JSON object with master, acknowledged_stance, statement, key_findings, disagreements, what_would_change_my_mind, source_ids and confidence."
|
|
1660
|
+
: "Return one JSON object matching the master_opinion schema from the original prompt.",
|
|
1661
|
+
`Write every reader-facing value in ${run.language}. Translation is allowed only to repair language; preserve the frozen stance, facts, numbers and source IDs.`,
|
|
1662
|
+
`Malformed output:\n${String(result.text || "").slice(0, LIMITS.PARSE_REPAIR_INPUT_CHARS)}`,
|
|
1663
|
+
].join("\n\n");
|
|
1664
|
+
result = await execute(repairPrompt, repairBudget, 2);
|
|
1665
|
+
if (!result.ok) return { id, error: result.deadline_exhausted ? "global_deadline" : result.timedOut ? "timeout" : `exit code ${result.code}`, raw: cleanLog(result.stderr || "method repair failed") };
|
|
1666
|
+
try {
|
|
1667
|
+
return { id, opinion: parse(result), engine };
|
|
1668
|
+
} catch (secondParseError) {
|
|
1669
|
+
return { id, error: outputFailureKind(secondParseError), raw: cleanLog(secondParseError?.message || result.text) };
|
|
1670
|
+
}
|
|
1132
1671
|
}
|
|
1133
1672
|
}, (error, { id }) => ({
|
|
1134
1673
|
id,
|
|
@@ -1138,13 +1677,37 @@ export async function runHeadlessMasters(run, args = {}) {
|
|
|
1138
1677
|
|
|
1139
1678
|
for (const outcome of outcomes) {
|
|
1140
1679
|
if (!outcome.opinion) {
|
|
1141
|
-
|
|
1680
|
+
const diagnosticPath = join(dir, `${outcome.id}.failure.json`);
|
|
1681
|
+
writeJson(diagnosticPath, {
|
|
1682
|
+
master: outcome.id,
|
|
1683
|
+
failure_kind: outcome.error,
|
|
1684
|
+
diagnostic: outcome.raw,
|
|
1685
|
+
public_summary: outcome.error === "reader_language_mismatch"
|
|
1686
|
+
? localized(run.language, {
|
|
1687
|
+
en: "The dedicated method worker returned reader-facing content in the wrong language; no method-seat statement is available.",
|
|
1688
|
+
zh: "专属方法席 worker 返回了错误语言的读者内容;没有可用的方法席发言。",
|
|
1689
|
+
ja: "専用メソッド席ワーカーは指定と異なる言語の読者向け内容を返したため、利用可能なメソッド席の発言はありません。",
|
|
1690
|
+
ko: "전용 방법론 좌석 워커가 지정과 다른 언어의 독자용 내용을 반환해 사용할 수 있는 방법론 좌석 발언이 없습니다.",
|
|
1691
|
+
})
|
|
1692
|
+
: localized(run.language, {
|
|
1693
|
+
en: "The dedicated method worker did not complete; no method-seat statement is available.",
|
|
1694
|
+
zh: "专属方法席 worker 未完成;没有可用的方法席发言。",
|
|
1695
|
+
ja: "専用メソッド席ワーカーが完了せず、利用可能なメソッド席の発言はありません。",
|
|
1696
|
+
ko: "전용 방법론 좌석 워커가 완료되지 않아 사용할 수 있는 방법론 좌석 발언이 없습니다.",
|
|
1697
|
+
}),
|
|
1698
|
+
}, { mode: 0o600 });
|
|
1699
|
+
updateMasterStatus(run, outcome.id, "failed", { error: outcome.error, diagnostic: diagnosticPath });
|
|
1142
1700
|
continue;
|
|
1143
1701
|
}
|
|
1144
1702
|
byId.set(outcome.id, outcome.opinion);
|
|
1145
1703
|
writeJson(join(dir, `${outcome.id}.json`), outcome.opinion);
|
|
1146
1704
|
updateMasterStatus(run, outcome.id, "completed", {
|
|
1147
1705
|
engine: outcome.opinion.engine || outcome.engine,
|
|
1706
|
+
worker_kind: outcome.opinion.dedicated_worker?.execution_mode === "dry_run"
|
|
1707
|
+
? "dedicated_method_voice_dry_run"
|
|
1708
|
+
: "dedicated_method_worker",
|
|
1709
|
+
worker_pid: outcome.opinion.dedicated_worker?.pid || null,
|
|
1710
|
+
voice_status: outcome.opinion.voice_status || "completed",
|
|
1148
1711
|
completed_at: new Date().toISOString(),
|
|
1149
1712
|
output: join(dir, `${outcome.id}.json`),
|
|
1150
1713
|
});
|
|
@@ -1162,50 +1725,112 @@ export async function runHeadlessMasters(run, args = {}) {
|
|
|
1162
1725
|
export async function runDebateRole(run, role, context, timeoutMs) {
|
|
1163
1726
|
const prompt = debatePrompt(role, run, context);
|
|
1164
1727
|
updateAgent(run, role, "running", { started_at: new Date().toISOString(), round: context.round });
|
|
1165
|
-
|
|
1166
|
-
? deadlineResult()
|
|
1728
|
+
let result = timeoutMs <= 0
|
|
1729
|
+
? deadlineResult(run)
|
|
1167
1730
|
: await runCodex(prompt, timeoutMs, ({ pid, output }) => {
|
|
1168
1731
|
updateAgent(run, role, "running", { pid, output, round: context.round });
|
|
1169
1732
|
}, ({ pid, output, elapsed_ms }) => {
|
|
1170
1733
|
updateAgent(run, role, "running", { pid, output, round: context.round });
|
|
1171
1734
|
appendEvent(run, "agent_heartbeat", { role, round: context.round, pid, output, elapsed_ms });
|
|
1172
|
-
});
|
|
1173
|
-
const
|
|
1735
|
+
}, { search: false, sigkillGraceMs: councilKillGrace(run) });
|
|
1736
|
+
const enforceLanguage = (candidate) => {
|
|
1737
|
+
if (candidate?.failure_kind) return candidate;
|
|
1738
|
+
try {
|
|
1739
|
+
assertReaderLanguage([
|
|
1740
|
+
candidate?.summary,
|
|
1741
|
+
...(candidate?.long_thesis || []),
|
|
1742
|
+
...(candidate?.short_thesis || []),
|
|
1743
|
+
...(candidate?.catalysts || []),
|
|
1744
|
+
...(candidate?.risks || []),
|
|
1745
|
+
candidate?.position,
|
|
1746
|
+
...(candidate?.invalidation || []),
|
|
1747
|
+
...(candidate?.questions || []),
|
|
1748
|
+
...(candidate?.questions_answered || []).flatMap((item) => [item?.question, item?.answer]),
|
|
1749
|
+
candidate?.report_markdown,
|
|
1750
|
+
].filter(Boolean).join("\n"), run.language, `${role} debate output`);
|
|
1751
|
+
return candidate;
|
|
1752
|
+
} catch (error) {
|
|
1753
|
+
return debateFailurePacket(role, run, "reader_language_mismatch");
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
let packet = enforceLanguage(debateFromCodex(result, role, run, prompt));
|
|
1757
|
+
if (result.ok && ["parse_failed", "reader_language_mismatch"].includes(packet.failure_kind)) {
|
|
1758
|
+
const repairBudget = Math.min(LIMITS.PARSE_REPAIR_MS, remainingCouncilBudget(run, LIMITS.PARSE_REPAIR_MS));
|
|
1759
|
+
if (repairBudget > 0) {
|
|
1760
|
+
const repairReason = packet.failure_kind;
|
|
1761
|
+
appendEvent(run, "agent_parse_repair", { role, round: context.round, budget_ms: repairBudget, reason: repairReason });
|
|
1762
|
+
const repairPrompt = [
|
|
1763
|
+
"PARSE-ONLY TRANSPORT REPAIR. Do not search, browse, add facts or redo the analysis.",
|
|
1764
|
+
`Role: ${role}; symbol: ${run.symbol}; as_of: ${run.as_of}; reader language: ${run.language}; round: ${context.round || "final"}.`,
|
|
1765
|
+
repairReason === "reader_language_mismatch"
|
|
1766
|
+
? "Translate only the reader-facing strings in the supplied valid debate-packet JSON. Preserve exact round-2 questions, exact round-3 question bindings, facts, numbers, source IDs and uncertainty. Return one JSON object only."
|
|
1767
|
+
: "Convert only the supplied malformed output into one valid debate-packet JSON object. Preserve exact round-2 questions and exact round-3 question bindings when present. Return JSON only.",
|
|
1768
|
+
`Write every reader-facing value in ${run.language}. Translation is allowed only to repair language; preserve facts, numbers, source IDs and exact Q&A bindings.`,
|
|
1769
|
+
`Malformed output:\n${String(result.text || "").slice(0, LIMITS.PARSE_REPAIR_INPUT_CHARS)}`,
|
|
1770
|
+
].join("\n\n");
|
|
1771
|
+
result = await runCodex(repairPrompt, repairBudget, ({ pid, output }) => {
|
|
1772
|
+
updateAgent(run, role, "running", { pid, output, round: context.round, attempts: 2 });
|
|
1773
|
+
}, () => {}, { search: false, sigkillGraceMs: councilKillGrace(run) });
|
|
1774
|
+
packet = enforceLanguage(debateFromCodex(result, role, run, repairPrompt));
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1174
1777
|
const roundCompletedAt = new Date().toISOString();
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1778
|
+
if (Number.isInteger(context.round)) {
|
|
1779
|
+
// A role can run three times. Leaving it marked `running` after one awaited invocation
|
|
1780
|
+
// returned made sequential rounds look concurrent in status.json. Record the dependency
|
|
1781
|
+
// boundary explicitly while reserving `completed` for the merged three-round artifact.
|
|
1782
|
+
updateAgent(run, role, "waiting", {
|
|
1783
|
+
round: context.round,
|
|
1784
|
+
round_status: "completed",
|
|
1785
|
+
last_completed_round: context.round,
|
|
1786
|
+
round_completed_at: roundCompletedAt,
|
|
1787
|
+
pid: null,
|
|
1788
|
+
output: null,
|
|
1789
|
+
});
|
|
1790
|
+
appendEvent(run, "agent_round_completed", {
|
|
1791
|
+
role,
|
|
1792
|
+
round: context.round,
|
|
1793
|
+
ok: result.ok,
|
|
1794
|
+
timed_out: result.timedOut === true,
|
|
1795
|
+
verdict: packet.verdict,
|
|
1796
|
+
failure_kind: packet.failure_kind,
|
|
1797
|
+
question_count: packet.questions.length,
|
|
1798
|
+
answered_count: packet.questions_answered.length,
|
|
1799
|
+
});
|
|
1800
|
+
} else {
|
|
1801
|
+
updateAgent(run, role, "waiting", {
|
|
1802
|
+
synthesis_status: "completed",
|
|
1803
|
+
synthesis_completed_at: roundCompletedAt,
|
|
1804
|
+
pid: null,
|
|
1805
|
+
output: null,
|
|
1806
|
+
});
|
|
1807
|
+
appendEvent(run, "agent_role_completed", {
|
|
1808
|
+
role,
|
|
1809
|
+
ok: result.ok,
|
|
1810
|
+
timed_out: result.timedOut === true,
|
|
1811
|
+
verdict: packet.verdict,
|
|
1812
|
+
failure_kind: packet.failure_kind,
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1195
1815
|
return { packet, result };
|
|
1196
1816
|
}
|
|
1197
1817
|
|
|
1198
1818
|
function debateFailure(step) {
|
|
1199
|
-
if (step.result.ok
|
|
1819
|
+
if (!step.result.ok) {
|
|
1820
|
+
if (step.result.deadline_exhausted) return "global_deadline";
|
|
1821
|
+
if (step.result.timedOut) return "timeout";
|
|
1822
|
+
if (Number.isInteger(step.result.code)) return `exit code ${step.result.code}`;
|
|
1823
|
+
return "unexpected_error";
|
|
1824
|
+
}
|
|
1825
|
+
if (step.packet.failure_kind) return step.packet.failure_kind;
|
|
1200
1826
|
if (step.packet.verdict === "PARSE_FAILED") return "parse_failed";
|
|
1201
|
-
|
|
1202
|
-
return step.result.timedOut ? "timeout" : `exit code ${step.result.code ?? "unknown"}`;
|
|
1827
|
+
return undefined;
|
|
1203
1828
|
}
|
|
1204
1829
|
|
|
1205
1830
|
async function synthesizeQuickDecision(run, args, timeoutMs, outputMode) {
|
|
1206
1831
|
const dir = runPath(run.run_id);
|
|
1207
1832
|
appendEvent(run, "debate_round", { round: 1, format: "single_round_parallel" });
|
|
1208
|
-
const sideBudget =
|
|
1833
|
+
const sideBudget = remainingCouncilBudget(run, timeoutMs);
|
|
1209
1834
|
const [bullOutcome, bearOutcome] = await Promise.allSettled([
|
|
1210
1835
|
runDebateRole(run, "bull_researcher", { round: 1, brief: "quick long case" }, sideBudget),
|
|
1211
1836
|
runDebateRole(run, "bear_researcher", { round: 1, brief: "quick short case" }, sideBudget),
|
|
@@ -1249,21 +1874,11 @@ async function synthesizeQuickDecision(run, args, timeoutMs, outputMode) {
|
|
|
1249
1874
|
});
|
|
1250
1875
|
writeAllAgentsMarkdown(run, { bull, bear });
|
|
1251
1876
|
|
|
1252
|
-
const
|
|
1253
|
-
|
|
1254
|
-
const
|
|
1255
|
-
const
|
|
1256
|
-
|
|
1257
|
-
: await runCodex(managerPrompt, managerBudget, ({ pid, output }) => {
|
|
1258
|
-
updateAgent(run, "portfolio_manager", "running", { pid, output });
|
|
1259
|
-
}, ({ pid, output, elapsed_ms }) => {
|
|
1260
|
-
updateAgent(run, "portfolio_manager", "running", { pid, output });
|
|
1261
|
-
appendEvent(run, "agent_heartbeat", { role: "portfolio_manager", pid, output, elapsed_ms });
|
|
1262
|
-
});
|
|
1263
|
-
const manager = managerResult.ok
|
|
1264
|
-
? debateFromCodex(managerResult, "portfolio_manager", run, managerPrompt)
|
|
1265
|
-
: managerFallback(run, args.prompt || "");
|
|
1266
|
-
const managerOk = managerResult.ok && manager.verdict !== "PARSE_FAILED";
|
|
1877
|
+
const managerBudget = remainingCouncilBudget(run, timeoutMs);
|
|
1878
|
+
const managerStep = await runDebateRole(run, "portfolio_manager", { bull, bear, outputMode }, managerBudget);
|
|
1879
|
+
const managerError = debateFailure(managerStep);
|
|
1880
|
+
const managerOk = !managerError;
|
|
1881
|
+
const manager = managerOk ? managerStep.packet : managerFallback(run, args.prompt || "");
|
|
1267
1882
|
writeJson(join(dir, "manager_synthesis.json"), manager);
|
|
1268
1883
|
writeJson(join(dir, "decision.json"), manager);
|
|
1269
1884
|
updateAgent(run, "portfolio_manager", managerOk ? "completed" : "failed", {
|
|
@@ -1271,9 +1886,7 @@ async function synthesizeQuickDecision(run, args, timeoutMs, outputMode) {
|
|
|
1271
1886
|
output: join(dir, "manager_synthesis.json"),
|
|
1272
1887
|
error: managerOk
|
|
1273
1888
|
? undefined
|
|
1274
|
-
:
|
|
1275
|
-
? "parse_failed"
|
|
1276
|
-
: managerResult.deadline_exhausted ? "global_deadline" : managerResult.timedOut ? "timeout" : `exit code ${managerResult.code ?? "unknown"}`,
|
|
1889
|
+
: managerError,
|
|
1277
1890
|
});
|
|
1278
1891
|
|
|
1279
1892
|
const gate = verificationStatus(run);
|
|
@@ -1340,6 +1953,47 @@ function finalizeBeforeDebate(run, args, reason) {
|
|
|
1340
1953
|
return { bull: null, bear: null, manager, ...finalArtifacts };
|
|
1341
1954
|
}
|
|
1342
1955
|
|
|
1956
|
+
function finalizeAfterDebateFailure(run, args, reason, bullRounds = [], bearRounds = []) {
|
|
1957
|
+
const dir = runPath(run.run_id);
|
|
1958
|
+
const bull = mergeDebateRounds(bullRounds.map((step) => step?.packet).filter(Boolean));
|
|
1959
|
+
const bear = mergeDebateRounds(bearRounds.map((step) => step?.packet).filter(Boolean));
|
|
1960
|
+
if (bull) writeJson(join(dir, "bull_researcher.json"), bull);
|
|
1961
|
+
if (bear) writeJson(join(dir, "bear_researcher.json"), bear);
|
|
1962
|
+
updateAgent(run, "bull_researcher", "failed", {
|
|
1963
|
+
error: reason,
|
|
1964
|
+
completed_at: new Date().toISOString(),
|
|
1965
|
+
output: bull ? join(dir, "bull_researcher.json") : undefined,
|
|
1966
|
+
});
|
|
1967
|
+
updateAgent(run, "bear_researcher", "failed", {
|
|
1968
|
+
error: reason,
|
|
1969
|
+
completed_at: new Date().toISOString(),
|
|
1970
|
+
output: bear ? join(dir, "bear_researcher.json") : undefined,
|
|
1971
|
+
});
|
|
1972
|
+
updateAgent(run, "portfolio_manager", "skipped", {
|
|
1973
|
+
error: reason,
|
|
1974
|
+
completed_at: new Date().toISOString(),
|
|
1975
|
+
});
|
|
1976
|
+
const manager = managerFallback(run, args.prompt || "");
|
|
1977
|
+
writeJson(join(dir, "manager_synthesis.json"), manager);
|
|
1978
|
+
writeJson(join(dir, "decision.json"), manager);
|
|
1979
|
+
run.phase = "incomplete";
|
|
1980
|
+
run.status = "incomplete";
|
|
1981
|
+
run.completed_at = new Date().toISOString();
|
|
1982
|
+
const completeness = completenessStatus(run);
|
|
1983
|
+
appendEvent(run, "incomplete", {
|
|
1984
|
+
reason,
|
|
1985
|
+
downstream_model_calls_skipped: true,
|
|
1986
|
+
missing_evidence: completeness.missing_evidence,
|
|
1987
|
+
missing_debate: completeness.missing_debate,
|
|
1988
|
+
missing_masters: completeness.missing_masters,
|
|
1989
|
+
});
|
|
1990
|
+
const finalArtifacts = writeFinalArtifacts(run, { bull, bear, manager });
|
|
1991
|
+
writeJson(join(dir, "evidence.json"), run);
|
|
1992
|
+
writeStatus(run);
|
|
1993
|
+
writeAllAgentsMarkdown(run, { bull, bear, manager });
|
|
1994
|
+
return { bull, bear, manager, ...finalArtifacts };
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1343
1997
|
export async function synthesizeDecision(run, args) {
|
|
1344
1998
|
const dir = runPath(run.run_id);
|
|
1345
1999
|
const requestedSynthesisMs = Number.isFinite(args.synthesis_timeout_ms)
|
|
@@ -1347,7 +2001,7 @@ export async function synthesizeDecision(run, args) {
|
|
|
1347
2001
|
: Number(args.timeout_ms || LIMITS.CODEX_TIMEOUT_MS);
|
|
1348
2002
|
const timeoutMs = run.council_mode === "quick"
|
|
1349
2003
|
? Math.min(requestedSynthesisMs, LIMITS.QUICK_SYNTHESIS_MS)
|
|
1350
|
-
: requestedSynthesisMs;
|
|
2004
|
+
: Math.min(requestedSynthesisMs, LIMITS.FULL_DEBATE_MS);
|
|
1351
2005
|
const outputMode = OUTPUT_MODES.includes(args.output_mode) ? args.output_mode : "public_equity";
|
|
1352
2006
|
run.phase = "debate";
|
|
1353
2007
|
run.status = "running";
|
|
@@ -1403,28 +2057,66 @@ export async function synthesizeDecision(run, args) {
|
|
|
1403
2057
|
return synthesizeQuickDecision(run, args, timeoutMs, outputMode);
|
|
1404
2058
|
}
|
|
1405
2059
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
2060
|
+
const rejectedStep = (reason, role) => {
|
|
2061
|
+
const result = {
|
|
2062
|
+
ok: false, code: null, text: "", stderr: cleanLog(reason?.message || reason), stdout: "",
|
|
2063
|
+
timedOut: false, unexpected_error: true,
|
|
2064
|
+
};
|
|
2065
|
+
return { result, packet: debateFromCodex(result, role, run, "unexpected debate failure") };
|
|
2066
|
+
};
|
|
2067
|
+
const parallelRound = async (round, bullContext, bearContext) => {
|
|
2068
|
+
const budget = remainingCouncilBudget(run, timeoutMs);
|
|
2069
|
+
appendEvent(run, "debate_round", { round, format: "parallel_per_round", budget_ms: budget });
|
|
2070
|
+
const [bullOutcome, bearOutcome] = await Promise.allSettled([
|
|
2071
|
+
runDebateRole(run, "bull_researcher", { round, ...bullContext }, budget),
|
|
2072
|
+
runDebateRole(run, "bear_researcher", { round, ...bearContext }, budget),
|
|
2073
|
+
]);
|
|
2074
|
+
return {
|
|
2075
|
+
bull: bullOutcome.status === "fulfilled" ? bullOutcome.value : rejectedStep(bullOutcome.reason, "bull_researcher"),
|
|
2076
|
+
bear: bearOutcome.status === "fulfilled" ? bearOutcome.value : rejectedStep(bearOutcome.reason, "bear_researcher"),
|
|
2077
|
+
};
|
|
2078
|
+
};
|
|
2079
|
+
const failedRound = (round) => [debateFailure(round.bull), debateFailure(round.bear)].filter(Boolean);
|
|
2080
|
+
|
|
2081
|
+
// Three-round debate with a strict inter-round barrier and parallel sides per round.
|
|
2082
|
+
const r1 = await parallelRound(1, { brief: "long" }, { brief: "short" });
|
|
2083
|
+
if (failedRound(r1).length) {
|
|
2084
|
+
return finalizeAfterDebateFailure(run, args, `debate_round_1_failed:${failedRound(r1).join(",")}`, [r1.bull], [r1.bear]);
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
const r2 = await parallelRound(2,
|
|
2088
|
+
{ otherCaseR1: r1.bear.packet },
|
|
2089
|
+
{ otherCaseR1: r1.bull.packet });
|
|
2090
|
+
if (failedRound(r2).length) {
|
|
2091
|
+
return finalizeAfterDebateFailure(run, args, `debate_round_2_failed:${failedRound(r2).join(",")}`, [r1.bull, r2.bull], [r1.bear, r2.bear]);
|
|
2092
|
+
}
|
|
2093
|
+
const exactlyThreeQuestions = (packet) => Array.isArray(packet?.questions)
|
|
2094
|
+
&& packet.questions.length === 3
|
|
2095
|
+
&& packet.questions.every((question) => typeof question === "string" && question.trim());
|
|
2096
|
+
if (!exactlyThreeQuestions(r2.bull.packet) || !exactlyThreeQuestions(r2.bear.packet)) {
|
|
2097
|
+
appendEvent(run, "debate_qna_gate", { status: "failed", errors: ["round 2 did not produce exactly three questions per side"] });
|
|
2098
|
+
return finalizeAfterDebateFailure(run, args, "debate_round_2_questions_incomplete", [r1.bull, r2.bull], [r1.bear, r2.bear]);
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
const r3 = await parallelRound(3, {
|
|
2102
|
+
otherCaseR1: r2.bear.packet,
|
|
2103
|
+
questionsYouAsked: r2.bull.packet.questions,
|
|
2104
|
+
questionsForYou: r2.bear.packet.questions,
|
|
2105
|
+
}, {
|
|
2106
|
+
otherCaseR1: r2.bull.packet,
|
|
2107
|
+
questionsYouAsked: r2.bear.packet.questions,
|
|
2108
|
+
questionsForYou: r2.bull.packet.questions,
|
|
2109
|
+
});
|
|
2110
|
+
if (failedRound(r3).length) {
|
|
2111
|
+
return finalizeAfterDebateFailure(run, args, `debate_round_3_failed:${failedRound(r3).join(",")}`, [r1.bull, r2.bull, r3.bull], [r1.bear, r2.bear, r3.bear]);
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
const bullR1 = r1.bull;
|
|
2115
|
+
const bearR1 = r1.bear;
|
|
2116
|
+
const bullR2 = r2.bull;
|
|
2117
|
+
const bearR2 = r2.bear;
|
|
2118
|
+
const bullR3 = r3.bull;
|
|
2119
|
+
const bearR3 = r3.bear;
|
|
1428
2120
|
|
|
1429
2121
|
const bull = mergeDebateRounds([bullR1.packet, bullR2.packet, bullR3.packet]);
|
|
1430
2122
|
const bear = mergeDebateRounds([bearR1.packet, bearR2.packet, bearR3.packet]);
|
|
@@ -1435,6 +2127,9 @@ export async function synthesizeDecision(run, args) {
|
|
|
1435
2127
|
bearR3: bearR3.packet,
|
|
1436
2128
|
});
|
|
1437
2129
|
appendEvent(run, "debate_qna_gate", qnaGate);
|
|
2130
|
+
if (qnaGate.status !== "passed") {
|
|
2131
|
+
return finalizeAfterDebateFailure(run, args, "debate_round_3_qna_incomplete", [bullR1, bullR2, bullR3], [bearR1, bearR2, bearR3]);
|
|
2132
|
+
}
|
|
1438
2133
|
const bullQnaErrors = qnaGate.errors.filter((error) => error.startsWith("bull_researcher"));
|
|
1439
2134
|
const bearQnaErrors = qnaGate.errors.filter((error) => error.startsWith("bear_researcher"));
|
|
1440
2135
|
const bullTransportOk = [bullR1, bullR2, bullR3].every((step) => step.result.ok);
|
|
@@ -1472,24 +2167,17 @@ export async function synthesizeDecision(run, args) {
|
|
|
1472
2167
|
});
|
|
1473
2168
|
writeAllAgentsMarkdown(run, { bull, bear });
|
|
1474
2169
|
|
|
1475
|
-
const
|
|
1476
|
-
|
|
1477
|
-
const
|
|
1478
|
-
|
|
1479
|
-
}, ({ pid, output, elapsed_ms }) => {
|
|
1480
|
-
updateAgent(run, "portfolio_manager", "running", { pid, output });
|
|
1481
|
-
appendEvent(run, "agent_heartbeat", { role: "portfolio_manager", pid, output, elapsed_ms });
|
|
1482
|
-
});
|
|
1483
|
-
const manager = managerResult.ok
|
|
1484
|
-
? debateFromCodex(managerResult, "portfolio_manager", run, managerPrompt)
|
|
1485
|
-
: managerFallback(run, args.prompt || "");
|
|
2170
|
+
const pmBudget = remainingCouncilBudget(run, Math.min(requestedSynthesisMs, LIMITS.FULL_PM_MS));
|
|
2171
|
+
const managerStep = await runDebateRole(run, "portfolio_manager", { bull, bear, outputMode }, pmBudget);
|
|
2172
|
+
const managerOk = !debateFailure(managerStep);
|
|
2173
|
+
const manager = managerOk ? managerStep.packet : managerFallback(run, args.prompt || "");
|
|
1486
2174
|
const gate = verificationStatus(run);
|
|
1487
2175
|
writeJson(join(dir, "manager_synthesis.json"), manager);
|
|
1488
2176
|
writeJson(join(dir, "decision.json"), manager);
|
|
1489
|
-
updateAgent(run, "portfolio_manager",
|
|
2177
|
+
updateAgent(run, "portfolio_manager", managerOk ? "completed" : "failed", {
|
|
1490
2178
|
completed_at: new Date().toISOString(),
|
|
1491
2179
|
output: join(dir, "manager_synthesis.json"),
|
|
1492
|
-
error:
|
|
2180
|
+
error: managerOk ? undefined : debateFailure(managerStep),
|
|
1493
2181
|
});
|
|
1494
2182
|
const completeness = completenessStatus(run);
|
|
1495
2183
|
run.completed_at = new Date().toISOString();
|
|
@@ -1532,7 +2220,7 @@ export async function analyzeSymbol(args) {
|
|
|
1532
2220
|
artifacts: debate.artifacts || artifactPaths(run),
|
|
1533
2221
|
};
|
|
1534
2222
|
}
|
|
1535
|
-
if (
|
|
2223
|
+
if (remainingCouncilBudget(run, 1) <= 0) {
|
|
1536
2224
|
const debate = finalizeBeforeDebate(run, args, "global_deadline_before_masters");
|
|
1537
2225
|
return {
|
|
1538
2226
|
run,
|
|
@@ -1546,8 +2234,7 @@ export async function analyzeSymbol(args) {
|
|
|
1546
2234
|
}
|
|
1547
2235
|
await runHeadlessMasters(run, args);
|
|
1548
2236
|
gate = completenessStatus(run);
|
|
1549
|
-
if (gate.missing_masters.length > 0
|
|
1550
|
-
|| (run.council_mode === "quick" && remainingQuickBudget(run, 1) <= 0)) {
|
|
2237
|
+
if (gate.missing_masters.length > 0 || remainingCouncilBudget(run, 1) <= 0) {
|
|
1551
2238
|
const debate = finalizeBeforeDebate(run, args,
|
|
1552
2239
|
gate.missing_masters.length ? "master_gate_failed" : "global_deadline_before_debate");
|
|
1553
2240
|
return {
|