hoi4-agent-tools 2.1.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +32 -18
- package/dist/bin/stdio.js +2 -1
- package/dist/hoi4_agent_tools/core/clausewitz-evaluation.js +90 -0
- package/dist/hoi4_agent_tools/core/engine.js +14 -0
- package/dist/hoi4_agent_tools/core/index.js +349 -2
- package/dist/hoi4_agent_tools/core/source/ast-path.js +41 -0
- package/dist/hoi4_agent_tools/core/source/index.js +1 -0
- package/dist/hoi4_agent_tools/event/limits.js +1 -1
- package/dist/hoi4_agent_tools/focus/render.js +37 -13
- package/dist/hoi4_agent_tools/mcp/server/create.js +5 -1
- package/dist/hoi4_agent_tools/mcp/server/result.js +4 -4
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +39 -2
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +413 -0
- package/dist/hoi4_agent_tools/mcp/tools/technology.js +368 -0
- package/dist/hoi4_agent_tools/probability/adapters.js +344 -0
- package/dist/hoi4_agent_tools/probability/compare.js +178 -0
- package/dist/hoi4_agent_tools/probability/evaluation.js +649 -0
- package/dist/hoi4_agent_tools/probability/index.js +13 -0
- package/dist/hoi4_agent_tools/probability/model.js +5 -0
- package/dist/hoi4_agent_tools/probability/rational.js +145 -0
- package/dist/hoi4_agent_tools/probability/render.js +307 -0
- package/dist/hoi4_agent_tools/probability/sequence.js +858 -0
- package/dist/hoi4_agent_tools/probability/service.js +1168 -0
- package/dist/hoi4_agent_tools/probability/simulation.js +545 -0
- package/dist/hoi4_agent_tools/probability/source-analysis.js +458 -0
- package/dist/hoi4_agent_tools/probability/sweep.js +265 -0
- package/dist/hoi4_agent_tools/probability/trigger-evaluator.js +280 -0
- package/dist/hoi4_agent_tools/probability/value-evaluator.js +357 -0
- package/dist/hoi4_agent_tools/schemas/probability.js +930 -0
- package/dist/hoi4_agent_tools/schemas/technology.js +77 -0
- package/dist/hoi4_agent_tools/technology/compare.js +240 -0
- package/dist/hoi4_agent_tools/technology/graph.js +1196 -0
- package/dist/hoi4_agent_tools/technology/index.js +7 -0
- package/dist/hoi4_agent_tools/technology/model.js +2 -0
- package/dist/hoi4_agent_tools/technology/queries.js +300 -0
- package/dist/hoi4_agent_tools/technology/render.js +641 -0
- package/dist/hoi4_agent_tools/technology/service.js +686 -0
- package/dist/hoi4_agent_tools/technology/source-analysis.js +967 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +2 -0
- package/docs/development.md +7 -1
- package/docs/probability.md +80 -0
- package/docs/research/probability-adapter-evidence.md +64 -0
- package/docs/setup.md +1 -1
- package/docs/technology.md +90 -0
- package/examples/clients/codex-config.toml +3 -0
- package/examples/clients/generic.json +8 -0
- package/examples/probability/adaptive-event-pool.json +92 -0
- package/examples/probability/adaptive-event-sequence-input.json +57 -0
- package/examples/probability/event-mtth-scenarios.json +57 -0
- package/examples/probability/event-mtth-simulate-input.json +37 -0
- package/examples/probability/focus-route-scenarios.json +61 -0
- package/examples/probability/focus-route-sweep-input.json +36 -0
- package/package.json +26 -8
- package/schemas/configuration.schema.json +747 -0
- package/schemas/continuous-focus-palette.schema.json +325 -0
- package/schemas/custom-weighted-pool.schema.json +298 -0
- package/schemas/event-feature-manifest.schema.json +63 -0
- package/schemas/focus-plan.schema.json +927 -0
- package/schemas/focus-planning-sidecar.schema.json +407 -0
- package/schemas/gui-animation-source.schema.json +176 -0
- package/schemas/gui-helper.schema.json +301 -0
- package/schemas/gui-scenario.schema.json +298 -0
- package/schemas/map-operation.schema.json +2284 -0
- package/schemas/operation-result.schema.json +327 -0
- package/schemas/probability-analysis-result.schema.json +2211 -0
- package/schemas/probability-compare-input.schema.json +664 -0
- package/schemas/probability-evaluate-input.schema.json +665 -0
- package/schemas/probability-inspect-input.schema.json +97 -0
- package/schemas/probability-render-input.schema.json +141 -0
- package/schemas/probability-scenario-set.schema.json +430 -0
- package/schemas/probability-sequence-input.schema.json +886 -0
- package/schemas/probability-simulate-input.schema.json +691 -0
- package/schemas/probability-sweep-input.schema.json +702 -0
- package/server.json +3 -3
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const cachedPaths = new WeakMap();
|
|
2
|
+
function indexBlock(block, path, output) {
|
|
3
|
+
output.set(block, path);
|
|
4
|
+
const occurrences = new Map();
|
|
5
|
+
for (const [index, entry] of block.entries.entries()) {
|
|
6
|
+
if (entry.type === 'assignment') {
|
|
7
|
+
const key = entry.key.value;
|
|
8
|
+
const occurrence = occurrences.get(key) ?? 0;
|
|
9
|
+
occurrences.set(key, occurrence + 1);
|
|
10
|
+
const entryPath = [...path, `${key}[${occurrence}]`];
|
|
11
|
+
output.set(entry, entryPath);
|
|
12
|
+
output.set(entry.key, [...entryPath, '$key']);
|
|
13
|
+
output.set(entry.value, entryPath);
|
|
14
|
+
if (entry.value.type === 'block')
|
|
15
|
+
indexBlock(entry.value, entryPath, output);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const entryPath = [...path, `#${entry.type}[${index}]`];
|
|
19
|
+
output.set(entry, entryPath);
|
|
20
|
+
if (entry.type === 'block')
|
|
21
|
+
indexBlock(entry, entryPath, output);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function pathsFor(document) {
|
|
25
|
+
const existing = cachedPaths.get(document);
|
|
26
|
+
if (existing !== undefined)
|
|
27
|
+
return existing;
|
|
28
|
+
const indexed = new WeakMap();
|
|
29
|
+
indexBlock(document.root, [], indexed);
|
|
30
|
+
cachedPaths.set(document, indexed);
|
|
31
|
+
return indexed;
|
|
32
|
+
}
|
|
33
|
+
/** Stable key-and-occurrence path for a node inside one parsed Clausewitz document. */
|
|
34
|
+
export function astPathFor(document, node) {
|
|
35
|
+
const path = pathsFor(document).get(node);
|
|
36
|
+
return path === undefined ? undefined : [...path];
|
|
37
|
+
}
|
|
38
|
+
/** Test and diagnostic helper for callers that already hold a generic source entry. */
|
|
39
|
+
export function sourceEntryAstPath(document, entry) {
|
|
40
|
+
return astPathFor(document, entry);
|
|
41
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ServiceError } from '../core/result.js';
|
|
2
|
-
export const EVENT_GRAPH_MAX_NODES =
|
|
2
|
+
export const EVENT_GRAPH_MAX_NODES = 250_000;
|
|
3
3
|
export const EVENT_GRAPH_MAX_EDGES = 500_000;
|
|
4
4
|
export const EVENT_GRAPH_MAX_STATE_ACCESSES = 250_000;
|
|
5
5
|
export const EVENT_GRAPH_MAX_STATE_LINKS = 250_000;
|
|
@@ -143,6 +143,22 @@ function focusIconDataUri(focus, options) {
|
|
|
143
143
|
? undefined
|
|
144
144
|
: (options.presentation?.icons[icon.sprite]?.dataUri ?? options.iconDataUris?.[icon.sprite]);
|
|
145
145
|
}
|
|
146
|
+
function focusTitleLayout(renderer, value, maximumWidth) {
|
|
147
|
+
const title = value.trim().replace(/\s+/gu, ' ');
|
|
148
|
+
if (renderer.measure(title, 11) <= maximumWidth)
|
|
149
|
+
return { lines: [title], fontSize: 11 };
|
|
150
|
+
const words = title.split(' ');
|
|
151
|
+
if (words.length < 2)
|
|
152
|
+
return { lines: [title], fontSize: 10 };
|
|
153
|
+
let best = { lines: [title], width: renderer.measure(title, 10) };
|
|
154
|
+
for (let index = 1; index < words.length; index += 1) {
|
|
155
|
+
const lines = [words.slice(0, index).join(' '), words.slice(index).join(' ')];
|
|
156
|
+
const width = Math.max(...lines.map((line) => renderer.measure(line, 10)));
|
|
157
|
+
if (width < best.width)
|
|
158
|
+
best = { lines, width };
|
|
159
|
+
}
|
|
160
|
+
return { lines: best.lines, fontSize: 10 };
|
|
161
|
+
}
|
|
146
162
|
function svgDocument(plan, layout, diagnostics, options) {
|
|
147
163
|
const horizontal = options.horizontalSpacing ?? FOCUS_HORIZONTAL_GRID_PIXELS;
|
|
148
164
|
const vertical = options.verticalSpacing ?? FOCUS_VERTICAL_GRID_PIXELS;
|
|
@@ -160,10 +176,10 @@ function svgDocument(plan, layout, diagnostics, options) {
|
|
|
160
176
|
const nodeWidth = FOCUS_NODE_WIDTH_PIXELS;
|
|
161
177
|
const nodeHeight = FOCUS_NODE_HEIGHT_PIXELS;
|
|
162
178
|
const nodes = [...layout.nodes].sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
163
|
-
const minimumX = Math.min(
|
|
164
|
-
const minimumY = Math.min(
|
|
165
|
-
const maximumX = Math.max(
|
|
166
|
-
const maximumY = Math.max(
|
|
179
|
+
const minimumX = nodes.length === 0 ? 0 : Math.min(...nodes.map(({ x }) => x));
|
|
180
|
+
const minimumY = nodes.length === 0 ? 0 : Math.min(...nodes.map(({ y }) => y));
|
|
181
|
+
const maximumX = nodes.length === 0 ? 0 : Math.max(...nodes.map(({ x }) => x));
|
|
182
|
+
const maximumY = nodes.length === 0 ? 0 : Math.max(...nodes.map(({ y }) => y));
|
|
167
183
|
const logicalWidth = Math.max(320, (maximumX - minimumX) * horizontal + nodeWidth + padding * 2);
|
|
168
184
|
const logicalHeight = Math.max(240, (maximumY - minimumY) * vertical + nodeHeight + padding * 2);
|
|
169
185
|
const width = Math.max(1, Math.round(logicalWidth * outputScale));
|
|
@@ -268,6 +284,8 @@ function svgDocument(plan, layout, diagnostics, options) {
|
|
|
268
284
|
.map(({ code }) => code)
|
|
269
285
|
.sort()
|
|
270
286
|
.join(' ');
|
|
287
|
+
const titleWidth = nodeWidth - 72;
|
|
288
|
+
const titleLayout = focusTitleLayout(toolText, title, titleWidth);
|
|
271
289
|
parts.push(`<g class="focus-node ${escapeXml(focus.visibility)} ${severity}" id="focus-${escapeXml(node.id)}" data-focus-id="${escapeXml(node.id)}" data-branch="${escapeXml(focus.branchId ?? '')}" data-lane="${escapeXml(node.laneId)}" data-diagnostics="${escapeXml(diagnosticCodes)}" transform="translate(${point.x} ${point.y})" filter="url(#shadow)">`, `<title>${escapeXml(`${title} (${node.id}) at ${node.x},${node.y}`)}</title>`, `<rect width="${nodeWidth}" height="${nodeHeight}" rx="8" fill="${fill}" stroke="${stroke}" stroke-width="${severity === 'clean' ? 1.5 : 3}"/>`);
|
|
272
290
|
if (iconUri !== undefined) {
|
|
273
291
|
parts.push(`<image href="${escapeXml(iconUri)}" x="8" y="10" width="48" height="48" preserveAspectRatio="xMidYMid meet"/>`);
|
|
@@ -281,20 +299,26 @@ function svgDocument(plan, layout, diagnostics, options) {
|
|
|
281
299
|
fill: '#91a0b4',
|
|
282
300
|
}));
|
|
283
301
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
302
|
+
const titleY = titleLayout.lines.length === 1 ? 25 : 20;
|
|
303
|
+
for (const [index, line] of titleLayout.lines.entries()) {
|
|
304
|
+
const measuredWidth = toolText.measure(line, titleLayout.fontSize);
|
|
305
|
+
parts.push(toolText.render(line, {
|
|
306
|
+
x: 64,
|
|
307
|
+
y: titleY + index * 13,
|
|
308
|
+
fontSize: titleLayout.fontSize,
|
|
309
|
+
weight: 700,
|
|
310
|
+
fill: '#f2f5fa',
|
|
311
|
+
...(measuredWidth <= titleWidth ? {} : { targetWidth: titleWidth }),
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
parts.push(toolText.render(node.id.slice(0, 22), {
|
|
291
315
|
x: 64,
|
|
292
|
-
y:
|
|
316
|
+
y: 49,
|
|
293
317
|
fontSize: 9,
|
|
294
318
|
fill: '#aeb9c8',
|
|
295
319
|
}), toolText.render(`(${node.x}, ${node.y}) · ${node.laneId}`, {
|
|
296
320
|
x: 64,
|
|
297
|
-
y:
|
|
321
|
+
y: 64,
|
|
298
322
|
fontSize: 9,
|
|
299
323
|
fill: '#8391a5',
|
|
300
324
|
}), '</g>');
|
|
@@ -4,8 +4,10 @@ import { registerFocusTools } from '../tools/focus.js';
|
|
|
4
4
|
import { registerGuiTools } from '../tools/gui.js';
|
|
5
5
|
import { registerMapTools } from '../tools/map.js';
|
|
6
6
|
import { registerEventTools } from '../tools/event.js';
|
|
7
|
+
import { registerTechnologyTools } from '../tools/technology.js';
|
|
8
|
+
import { registerProbabilityTools } from '../tools/probability.js';
|
|
7
9
|
import { registerMcpResources } from '../resources/register.js';
|
|
8
|
-
export const SERVER_INSTRUCTIONS = 'Use
|
|
10
|
+
export const SERVER_INSTRUCTIONS = 'Use focus tools for focus trees, GUI tools for interfaces, and map tools for map data. Start unfamiliar event chains with hoi4.event_inspect. Event tools are read-only. Start technology and doctrine work with hoi4.tech_inspect. Technology tools are read-only. Start weighted AI, MTTH, random, and declared-pool analysis with hoi4.probability_inspect. Probability tools are read-only. Large evidence is linked as resources.';
|
|
9
11
|
export function createMcpServer(engine, context = {}) {
|
|
10
12
|
const server = new McpServer({
|
|
11
13
|
name: PACKAGE_NAME,
|
|
@@ -19,6 +21,8 @@ export function createMcpServer(engine, context = {}) {
|
|
|
19
21
|
registerGuiTools(server, engine, context);
|
|
20
22
|
registerMapTools(server, engine, context);
|
|
21
23
|
registerEventTools(server, engine, context);
|
|
24
|
+
registerTechnologyTools(server, engine, context);
|
|
25
|
+
registerProbabilityTools(server, engine, context);
|
|
22
26
|
registerMcpResources(server, engine, context);
|
|
23
27
|
return server;
|
|
24
28
|
}
|
|
@@ -44,13 +44,13 @@ const compactPublicOperationResultSchema = z
|
|
|
44
44
|
status: z.enum(['ok', 'blocked', 'error']),
|
|
45
45
|
code: z.string(),
|
|
46
46
|
workspaceId: z.string(),
|
|
47
|
-
filesScanned: z.array(z.unknown())
|
|
48
|
-
proposedFiles: z.array(z.unknown())
|
|
49
|
-
changedFiles: z.array(z.unknown())
|
|
47
|
+
filesScanned: z.array(z.unknown()),
|
|
48
|
+
proposedFiles: z.array(z.unknown()),
|
|
49
|
+
changedFiles: z.array(z.unknown()),
|
|
50
50
|
diagnostics: z.array(z.unknown()).max(MAX_INLINE_DIAGNOSTICS),
|
|
51
51
|
artifacts: z.array(z.unknown()).max(MAX_INLINE_ARTIFACT_LINKS),
|
|
52
52
|
validation: z.unknown(),
|
|
53
|
-
blockers: z.array(z.unknown())
|
|
53
|
+
blockers: z.array(z.unknown()),
|
|
54
54
|
data: z.object({}).catchall(z.unknown()),
|
|
55
55
|
})
|
|
56
56
|
.strict();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod/v4';
|
|
2
2
|
import { boundedSourceHashEvidence, publicArtifactLink } from '../../core/artifacts.js';
|
|
3
|
-
import { compareCodeUnits, canonicalJson } from '../../core/canonical.js';
|
|
3
|
+
import { compareCodeUnits, canonicalJson, hashCanonical } from '../../core/canonical.js';
|
|
4
4
|
import { RenderBudget } from '../../core/render-budget.js';
|
|
5
5
|
import { emptyServiceResult, ServiceError } from '../../core/result.js';
|
|
6
6
|
import { parseClausewitz } from '../../core/source/index.js';
|
|
@@ -116,6 +116,7 @@ const focusPlanOutputSchema = strictOperationResultSchema(z.discriminatedUnion('
|
|
|
116
116
|
.strict(),
|
|
117
117
|
]));
|
|
118
118
|
const compactFocusPlanSchema = compactValidatedInputSchema(z.union([focusTreePlanSchema, continuousFocusPaletteSchema]), `Complete focus plan: https://github.com/klimPaskov/hoi4-agent-tools/blob/v${PACKAGE_VERSION}/docs/focus.md`);
|
|
119
|
+
const compactFocusLayoutSchema = compactValidatedInputSchema(focusLayoutSchema, 'Prior deterministic focus layout used to stabilize unchanged positions.');
|
|
119
120
|
const focusInspectInput = z
|
|
120
121
|
.object({
|
|
121
122
|
mode: z.enum(['national', 'continuous']).optional(),
|
|
@@ -123,7 +124,7 @@ const focusInspectInput = z
|
|
|
123
124
|
relativePath: workspaceRelativePathSchema.optional(),
|
|
124
125
|
treeId: z.string().min(1).max(256).optional(),
|
|
125
126
|
paletteId: z.string().min(1).max(256).optional(),
|
|
126
|
-
previous:
|
|
127
|
+
previous: compactFocusLayoutSchema.optional(),
|
|
127
128
|
laneSpacing: z.number().int().min(1).max(100).optional(),
|
|
128
129
|
nodeSpacing: z.number().int().min(1).max(100).optional(),
|
|
129
130
|
})
|
|
@@ -307,6 +308,42 @@ const LARGE_FOCUS_RENDER_SCALE = 0.5;
|
|
|
307
308
|
function automaticFocusRenderScale(focusCount) {
|
|
308
309
|
return focusCount >= LARGE_FOCUS_RENDER_NODE_THRESHOLD ? LARGE_FOCUS_RENDER_SCALE : 1;
|
|
309
310
|
}
|
|
311
|
+
export async function computeFocusVisualRevisions(engine, context, workspaceId, snapshot, selectors, signal) {
|
|
312
|
+
const workspace = engine.resolver.get(workspaceId, context.principal);
|
|
313
|
+
const paletteImport = continuousPalettes(snapshot);
|
|
314
|
+
const revisions = [];
|
|
315
|
+
for (const selector of selectors) {
|
|
316
|
+
signal?.throwIfAborted();
|
|
317
|
+
const imported = importFocusFile(snapshot, selectedFocusFile(snapshot, workspace.registration.roots.focus, selector.relativePath), paletteImport.palettes);
|
|
318
|
+
const plan = selectPlan(imported, selector.treeId);
|
|
319
|
+
const linkedPalettes = paletteImport.palettes.filter(({ id }) => plan.continuousFocusPaletteIds.includes(id));
|
|
320
|
+
const presentation = await resolveFocusPresentation({
|
|
321
|
+
plans: [plan],
|
|
322
|
+
palettes: linkedPalettes,
|
|
323
|
+
files: snapshot.files,
|
|
324
|
+
index: snapshot.index,
|
|
325
|
+
scanner: engine.scanner,
|
|
326
|
+
workspace,
|
|
327
|
+
decodeIcons: false,
|
|
328
|
+
budget: new RenderBudget(),
|
|
329
|
+
...(signal === undefined ? {} : { signal }),
|
|
330
|
+
});
|
|
331
|
+
const filesScanned = [
|
|
332
|
+
...new Set([plan.provenance.sourcePath, ...presentation.filesScanned]),
|
|
333
|
+
].sort(compareCodeUnits);
|
|
334
|
+
revisions.push({
|
|
335
|
+
relativePath: selector.relativePath,
|
|
336
|
+
treeId: plan.id,
|
|
337
|
+
revision: hashCanonical({
|
|
338
|
+
planHash: focusPlanHash(plan),
|
|
339
|
+
linkedPalettes,
|
|
340
|
+
presentationSourceHashes: presentation.sourceHashes,
|
|
341
|
+
}),
|
|
342
|
+
filesScanned,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
return revisions;
|
|
346
|
+
}
|
|
310
347
|
async function executeFocusVisualTool(engine, workbench, context, input, extra, rasterize) {
|
|
311
348
|
const { workspaceId: requestedWorkspaceId, relativePath } = input;
|
|
312
349
|
const workspaceId = engine.resolver.resolveWorkspaceId(requestedWorkspaceId, context.principal);
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import { emptyServiceResult } from '../../core/result.js';
|
|
3
|
+
import { ProbabilityAnalyzer, } from '../../probability/service.js';
|
|
4
|
+
import { customWeightedPoolManifestSchema, probabilityAcceptanceBandSchema, probabilityAdapterIdSchema, probabilityDiagnosticThresholdsSchema, probabilityMetricSchema, probabilityOutputSchema, probabilityRenderFilterSchema, probabilityScenarioSetSchema, probabilitySamplingMethodSchema, probabilitySourceSchema, } from '../../schemas/probability.js';
|
|
5
|
+
import { workspaceIdSchema } from '../../schemas/common.js';
|
|
6
|
+
import { compactValidatedInputSchema } from '../server/context-schemas.js';
|
|
7
|
+
import { nonNegativeIntegerSchema, sha256Schema } from '../server/output-schemas.js';
|
|
8
|
+
import { progressReporter } from '../server/progress.js';
|
|
9
|
+
import { errorResult, setInlineFilesScanned, strictOperationResultSchema, toolResult, } from '../server/result.js';
|
|
10
|
+
const nestedSource = compactValidatedInputSchema(probabilitySourceSchema, 'Source selector or proposed source.');
|
|
11
|
+
const nestedScenarios = compactValidatedInputSchema(probabilityScenarioSetSchema, 'Explicit world-state scenarios.');
|
|
12
|
+
const nestedManifest = compactValidatedInputSchema(customWeightedPoolManifestSchema, 'Declared custom weighted pool and state transitions.');
|
|
13
|
+
const nestedRenderFilter = compactValidatedInputSchema(probabilityRenderFilterSchema, 'Optional scenario, candidate, metric, diagnostic, source, and page filter.');
|
|
14
|
+
const nestedAcceptanceBands = compactValidatedInputSchema(z.array(probabilityAcceptanceBandSchema).max(100_000), 'Optional named metric acceptance bands.');
|
|
15
|
+
const nestedDiagnosticThresholds = compactValidatedInputSchema(probabilityDiagnosticThresholdsSchema, 'Optional diagnostic thresholds.');
|
|
16
|
+
const candidatePool = z.array(z.string().min(1).max(512)).max(100_000).optional();
|
|
17
|
+
const outputs = z.array(probabilityOutputSchema).max(10).optional();
|
|
18
|
+
const commonShape = {
|
|
19
|
+
workspaceId: workspaceIdSchema,
|
|
20
|
+
adapter: probabilityAdapterIdSchema,
|
|
21
|
+
source: nestedSource,
|
|
22
|
+
scenarioSet: nestedScenarios,
|
|
23
|
+
candidatePool,
|
|
24
|
+
horizonDays: z.number().positive().max(1_000_000).optional(),
|
|
25
|
+
metrics: z.array(probabilityMetricSchema).min(1).max(4).optional(),
|
|
26
|
+
acceptanceBands: nestedAcceptanceBands.optional(),
|
|
27
|
+
diagnosticThresholds: nestedDiagnosticThresholds.optional(),
|
|
28
|
+
outputs,
|
|
29
|
+
refresh: z.boolean().optional(),
|
|
30
|
+
};
|
|
31
|
+
const inspectInput = z
|
|
32
|
+
.object({
|
|
33
|
+
workspaceId: workspaceIdSchema,
|
|
34
|
+
adapter: probabilityAdapterIdSchema.optional(),
|
|
35
|
+
source: nestedSource.optional(),
|
|
36
|
+
candidatePool,
|
|
37
|
+
refresh: z.boolean().optional(),
|
|
38
|
+
})
|
|
39
|
+
.strict()
|
|
40
|
+
.superRefine((value, context) => {
|
|
41
|
+
if ((value.adapter === undefined) !== (value.source === undefined))
|
|
42
|
+
context.addIssue({
|
|
43
|
+
code: 'custom',
|
|
44
|
+
message: 'Provide adapter and source together, or omit both to list adapters',
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
const evaluateInput = z.object(commonShape).strict();
|
|
48
|
+
const sweepInput = z
|
|
49
|
+
.object({
|
|
50
|
+
...commonShape,
|
|
51
|
+
sweep: z
|
|
52
|
+
.object({
|
|
53
|
+
paths: z.array(z.string().min(1).max(1024)).min(1).max(32),
|
|
54
|
+
steps: z.number().int().min(2).max(10_000).default(25),
|
|
55
|
+
pairwise: z.boolean().default(false),
|
|
56
|
+
findRankReversals: z.boolean().default(true),
|
|
57
|
+
})
|
|
58
|
+
.strict(),
|
|
59
|
+
})
|
|
60
|
+
.strict();
|
|
61
|
+
const simulateInput = z
|
|
62
|
+
.object({
|
|
63
|
+
...commonShape,
|
|
64
|
+
samples: z.number().int().min(100).max(10_000_000).default(100_000),
|
|
65
|
+
seed: z.number().int().min(-2_147_483_648).max(2_147_483_647).default(1),
|
|
66
|
+
confidenceLevel: z.number().min(0.5).max(0.9999).default(0.95),
|
|
67
|
+
samplingMethod: probabilitySamplingMethodSchema.default('latin_hypercube'),
|
|
68
|
+
})
|
|
69
|
+
.strict();
|
|
70
|
+
const sequenceInput = z
|
|
71
|
+
.object({
|
|
72
|
+
workspaceId: workspaceIdSchema,
|
|
73
|
+
scenarioSet: nestedScenarios,
|
|
74
|
+
customPoolManifest: nestedManifest,
|
|
75
|
+
horizonDays: z.number().positive().max(1_000_000),
|
|
76
|
+
maxSteps: z.number().int().min(1).max(100_000).default(1_000),
|
|
77
|
+
samples: z.number().int().min(100).max(10_000_000).default(100_000),
|
|
78
|
+
seed: z.number().int().min(-2_147_483_648).max(2_147_483_647).default(1),
|
|
79
|
+
confidenceLevel: z.number().min(0.5).max(0.9999).default(0.95),
|
|
80
|
+
acceptanceBands: nestedAcceptanceBands.optional(),
|
|
81
|
+
diagnosticThresholds: nestedDiagnosticThresholds.optional(),
|
|
82
|
+
outputs,
|
|
83
|
+
})
|
|
84
|
+
.strict();
|
|
85
|
+
const compareInput = z
|
|
86
|
+
.object({
|
|
87
|
+
workspaceId: workspaceIdSchema,
|
|
88
|
+
adapter: probabilityAdapterIdSchema,
|
|
89
|
+
before: nestedSource,
|
|
90
|
+
after: nestedSource,
|
|
91
|
+
scenarioSet: nestedScenarios,
|
|
92
|
+
candidatePool,
|
|
93
|
+
horizonDays: z.number().positive().max(1_000_000).optional(),
|
|
94
|
+
acceptanceBands: nestedAcceptanceBands.optional(),
|
|
95
|
+
diagnosticThresholds: nestedDiagnosticThresholds.optional(),
|
|
96
|
+
outputs,
|
|
97
|
+
refresh: z.boolean().optional(),
|
|
98
|
+
})
|
|
99
|
+
.strict();
|
|
100
|
+
const renderInput = z
|
|
101
|
+
.object({
|
|
102
|
+
workspaceId: workspaceIdSchema,
|
|
103
|
+
analysisId: z.string().min(1).max(256),
|
|
104
|
+
expectedScenarioHash: sha256Schema.optional(),
|
|
105
|
+
outputs: z.array(probabilityOutputSchema).min(1).max(10),
|
|
106
|
+
includeHtml: z.boolean().default(false),
|
|
107
|
+
filter: nestedRenderFilter.optional(),
|
|
108
|
+
})
|
|
109
|
+
.strict();
|
|
110
|
+
const analysisDataSchema = z
|
|
111
|
+
.object({
|
|
112
|
+
operation: z.enum(['evaluate', 'sweep', 'simulate', 'sequence', 'compare', 'render']),
|
|
113
|
+
analysisId: z.string().max(256),
|
|
114
|
+
analysisStatus: z.enum(['complete', 'partial', 'blocked', 'cancelled', 'stale']),
|
|
115
|
+
adapterId: z.string().max(256),
|
|
116
|
+
sourceRevision: sha256Schema,
|
|
117
|
+
sourceHash: sha256Schema,
|
|
118
|
+
scenarioHash: sha256Schema,
|
|
119
|
+
cacheKey: sha256Schema,
|
|
120
|
+
scenarios: nonNegativeIntegerSchema,
|
|
121
|
+
candidates: nonNegativeIntegerSchema,
|
|
122
|
+
unresolved: nonNegativeIntegerSchema,
|
|
123
|
+
diagnostics: nonNegativeIntegerSchema,
|
|
124
|
+
sweepPoints: nonNegativeIntegerSchema.optional(),
|
|
125
|
+
samples: nonNegativeIntegerSchema.optional(),
|
|
126
|
+
sequenceMethod: z
|
|
127
|
+
.enum(['exact_state_distribution', 'bounded_beam', 'seeded_monte_carlo'])
|
|
128
|
+
.optional(),
|
|
129
|
+
comparisonChanges: nonNegativeIntegerSchema.optional(),
|
|
130
|
+
visualResources: nonNegativeIntegerSchema.optional(),
|
|
131
|
+
})
|
|
132
|
+
.strict();
|
|
133
|
+
const inspectDataSchema = z
|
|
134
|
+
.object({
|
|
135
|
+
adapters: nonNegativeIntegerSchema,
|
|
136
|
+
adapterId: z.string().max(256).optional(),
|
|
137
|
+
sourceRevision: sha256Schema.optional(),
|
|
138
|
+
sourceHash: sha256Schema.optional(),
|
|
139
|
+
candidates: nonNegativeIntegerSchema,
|
|
140
|
+
poolComplete: z.boolean().optional(),
|
|
141
|
+
requiredInputs: nonNegativeIntegerSchema,
|
|
142
|
+
unresolved: nonNegativeIntegerSchema,
|
|
143
|
+
})
|
|
144
|
+
.strict();
|
|
145
|
+
const analysisOutput = strictOperationResultSchema(analysisDataSchema);
|
|
146
|
+
const inspectOutput = strictOperationResultSchema(inspectDataSchema);
|
|
147
|
+
const readOnly = {
|
|
148
|
+
readOnlyHint: true,
|
|
149
|
+
destructiveHint: false,
|
|
150
|
+
idempotentHint: true,
|
|
151
|
+
openWorldHint: false,
|
|
152
|
+
};
|
|
153
|
+
function requestContext(engine, context, workspaceId, refresh, signal) {
|
|
154
|
+
return {
|
|
155
|
+
workspaceId: engine.resolver.resolveWorkspaceId(workspaceId, context.principal),
|
|
156
|
+
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
157
|
+
...(refresh === undefined ? {} : { refresh }),
|
|
158
|
+
signal,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function analysisData(result) {
|
|
162
|
+
return {
|
|
163
|
+
operation: result.operation,
|
|
164
|
+
analysisId: result.analysisId,
|
|
165
|
+
analysisStatus: result.status,
|
|
166
|
+
adapterId: result.adapter.id,
|
|
167
|
+
sourceRevision: result.metadata.sourceRevision,
|
|
168
|
+
sourceHash: result.metadata.sourceHash,
|
|
169
|
+
scenarioHash: result.metadata.scenarioHash,
|
|
170
|
+
cacheKey: result.metadata.cacheKey,
|
|
171
|
+
scenarios: result.scenarios.length,
|
|
172
|
+
candidates: result.scenarios.reduce((sum, scenario) => sum + scenario.candidates.length, 0),
|
|
173
|
+
unresolved: result.unresolved.length,
|
|
174
|
+
diagnostics: result.diagnostics.length,
|
|
175
|
+
...(result.sweep === undefined ? {} : { sweepPoints: result.sweep.points.length }),
|
|
176
|
+
...(result.metadata.samples === undefined ? {} : { samples: result.metadata.samples }),
|
|
177
|
+
...(result.sequence === undefined ? {} : { sequenceMethod: result.sequence.method }),
|
|
178
|
+
...(result.comparison === undefined
|
|
179
|
+
? {}
|
|
180
|
+
: { comparisonChanges: result.comparison.scenarioChanges.length }),
|
|
181
|
+
visualResources: result.resources.filter(({ mimeType }) => mimeType.startsWith('image/'))
|
|
182
|
+
.length,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function analysisServiceResult(workspaceId, result) {
|
|
186
|
+
const output = emptyServiceResult(workspaceId, analysisData(result));
|
|
187
|
+
output.code =
|
|
188
|
+
result.status === 'complete'
|
|
189
|
+
? 'PROBABILITY_ANALYZED'
|
|
190
|
+
: result.status === 'stale'
|
|
191
|
+
? result.diagnostics.some(({ code }) => code === 'PROBABILITY_SCENARIO_STALE')
|
|
192
|
+
? 'PROBABILITY_SCENARIO_STALE'
|
|
193
|
+
: 'PROBABILITY_ANALYSIS_STALE'
|
|
194
|
+
: 'PROBABILITY_ANALYZED_PARTIAL';
|
|
195
|
+
output.artifacts = result.resources;
|
|
196
|
+
output.diagnostics = result.diagnostics;
|
|
197
|
+
output.validation = {
|
|
198
|
+
passed: result.status !== 'blocked' && result.status !== 'cancelled',
|
|
199
|
+
checks: [
|
|
200
|
+
{
|
|
201
|
+
id: 'uncertainty-visible',
|
|
202
|
+
passed: true,
|
|
203
|
+
message: `${result.unresolved.length} unresolved or bounded analysis item(s) are explicit`,
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
return output;
|
|
208
|
+
}
|
|
209
|
+
export function registerProbabilityTools(server, engine, context) {
|
|
210
|
+
const analyzer = new ProbabilityAnalyzer(engine);
|
|
211
|
+
server.registerTool('hoi4.probability_inspect', {
|
|
212
|
+
title: 'Inspect AI and MTTH weighted logic',
|
|
213
|
+
description: 'List versioned adapters or discover weighted blocks, candidate pools, capabilities, provenance, and unsupported constructs.',
|
|
214
|
+
inputSchema: inspectInput,
|
|
215
|
+
outputSchema: inspectOutput,
|
|
216
|
+
annotations: readOnly,
|
|
217
|
+
}, async (input, extra) => {
|
|
218
|
+
const progress = progressReporter(extra);
|
|
219
|
+
const base = requestContext(engine, context, input.workspaceId, input.refresh, progress.signal);
|
|
220
|
+
try {
|
|
221
|
+
await progress.report(0, 2, 'Inspecting weighted source');
|
|
222
|
+
const inspected = await analyzer.inspect(base, input.adapter, input.source, input.candidatePool);
|
|
223
|
+
const result = emptyServiceResult(base.workspaceId, {
|
|
224
|
+
adapters: inspected.adapters.length,
|
|
225
|
+
...(inspected.surface === undefined
|
|
226
|
+
? {}
|
|
227
|
+
: {
|
|
228
|
+
adapterId: inspected.surface.adapter.id,
|
|
229
|
+
sourceRevision: inspected.surface.sourceRevision,
|
|
230
|
+
sourceHash: inspected.surface.sourceHash,
|
|
231
|
+
poolComplete: inspected.surface.poolComplete,
|
|
232
|
+
}),
|
|
233
|
+
candidates: inspected.surface?.candidateCount ?? 0,
|
|
234
|
+
requiredInputs: inspected.surface?.requiredInputs.length ?? 0,
|
|
235
|
+
unresolved: inspected.surface?.unsupported.length ?? 0,
|
|
236
|
+
});
|
|
237
|
+
result.code =
|
|
238
|
+
inspected.surface === undefined
|
|
239
|
+
? 'PROBABILITY_ADAPTERS_LISTED'
|
|
240
|
+
: 'PROBABILITY_SOURCE_INSPECTED';
|
|
241
|
+
result.artifacts = inspected.artifacts;
|
|
242
|
+
setInlineFilesScanned(result, inspected.filesScanned);
|
|
243
|
+
await progress.report(2, 2, 'Weighted source inspection complete');
|
|
244
|
+
return toolResult(result);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
return errorResult(error, base.workspaceId);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
server.registerTool('hoi4.probability_evaluate', {
|
|
251
|
+
title: 'Evaluate AI and MTTH scenarios',
|
|
252
|
+
description: 'Evaluate eligibility, modifier traces, exact values, proven probabilities, MTTH timing, bounds, and unresolved inputs.',
|
|
253
|
+
inputSchema: evaluateInput,
|
|
254
|
+
outputSchema: analysisOutput,
|
|
255
|
+
annotations: readOnly,
|
|
256
|
+
}, async (input, extra) => {
|
|
257
|
+
const progress = progressReporter(extra);
|
|
258
|
+
const base = requestContext(engine, context, input.workspaceId, input.refresh, progress.signal);
|
|
259
|
+
try {
|
|
260
|
+
await progress.report(0, 3, 'Scanning and evaluating weighted source');
|
|
261
|
+
const request = {
|
|
262
|
+
...base,
|
|
263
|
+
...input,
|
|
264
|
+
workspaceId: base.workspaceId,
|
|
265
|
+
};
|
|
266
|
+
const analyzed = await analyzer.evaluate(request);
|
|
267
|
+
await progress.report(3, 3, 'Scenario evaluation complete');
|
|
268
|
+
return toolResult(analysisServiceResult(base.workspaceId, analyzed));
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
return errorResult(error, base.workspaceId);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
server.registerTool('hoi4.probability_sweep', {
|
|
275
|
+
title: 'Sweep AI and MTTH parameters',
|
|
276
|
+
description: 'Evaluate declared ranges, sensitivity, breakpoints, and rank reversals without inventing world state.',
|
|
277
|
+
inputSchema: sweepInput,
|
|
278
|
+
outputSchema: analysisOutput,
|
|
279
|
+
annotations: readOnly,
|
|
280
|
+
}, async (input, extra) => {
|
|
281
|
+
const progress = progressReporter(extra);
|
|
282
|
+
const base = requestContext(engine, context, input.workspaceId, input.refresh, progress.signal);
|
|
283
|
+
try {
|
|
284
|
+
await progress.report(0, 3, 'Evaluating parameter sweep');
|
|
285
|
+
const request = {
|
|
286
|
+
...base,
|
|
287
|
+
...input,
|
|
288
|
+
workspaceId: base.workspaceId,
|
|
289
|
+
};
|
|
290
|
+
const analyzed = await analyzer.sweep(request);
|
|
291
|
+
await progress.report(3, 3, 'Parameter sweep complete');
|
|
292
|
+
return toolResult(analysisServiceResult(base.workspaceId, analyzed));
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
return errorResult(error, base.workspaceId);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
server.registerTool('hoi4.probability_simulate', {
|
|
299
|
+
title: 'Simulate uncertain weighted scenarios',
|
|
300
|
+
description: 'Run reproducible seeded Monte Carlo analysis with distributions, supported correlations, and confidence intervals.',
|
|
301
|
+
inputSchema: simulateInput,
|
|
302
|
+
outputSchema: analysisOutput,
|
|
303
|
+
annotations: readOnly,
|
|
304
|
+
}, async (input, extra) => {
|
|
305
|
+
const progress = progressReporter(extra);
|
|
306
|
+
const base = requestContext(engine, context, input.workspaceId, input.refresh, progress.signal);
|
|
307
|
+
try {
|
|
308
|
+
await progress.report(0, input.samples, 'Sampling uncertain scenarios');
|
|
309
|
+
const request = {
|
|
310
|
+
...base,
|
|
311
|
+
...input,
|
|
312
|
+
workspaceId: base.workspaceId,
|
|
313
|
+
};
|
|
314
|
+
const analyzed = await analyzer.simulate(request);
|
|
315
|
+
await progress.report(input.samples, input.samples, 'Seeded simulation complete');
|
|
316
|
+
return toolResult(analysisServiceResult(base.workspaceId, analyzed));
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
return errorResult(error, base.workspaceId);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
server.registerTool('hoi4.probability_sequence', {
|
|
323
|
+
title: 'Analyze a declared weighted sequence',
|
|
324
|
+
description: 'Analyze only manifest-declared recovery, caps, cooldowns, removals, resets, timers, and terminal states.',
|
|
325
|
+
inputSchema: sequenceInput,
|
|
326
|
+
outputSchema: analysisOutput,
|
|
327
|
+
annotations: readOnly,
|
|
328
|
+
}, async (input, extra) => {
|
|
329
|
+
const progress = progressReporter(extra);
|
|
330
|
+
const base = requestContext(engine, context, input.workspaceId, undefined, progress.signal);
|
|
331
|
+
try {
|
|
332
|
+
await progress.report(0, input.maxSteps, 'Analyzing declared sequence');
|
|
333
|
+
const request = {
|
|
334
|
+
...base,
|
|
335
|
+
...input,
|
|
336
|
+
workspaceId: base.workspaceId,
|
|
337
|
+
};
|
|
338
|
+
const analyzed = await analyzer.sequence(request);
|
|
339
|
+
await progress.report(input.maxSteps, input.maxSteps, 'Sequence analysis complete');
|
|
340
|
+
return toolResult(analysisServiceResult(base.workspaceId, analyzed));
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
return errorResult(error, base.workspaceId);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
server.registerTool('hoi4.probability_compare', {
|
|
347
|
+
title: 'Compare weighted source patches',
|
|
348
|
+
description: 'Compare real or proposed source under identical scenarios with modifier, score, probability, timing, and rank attribution.',
|
|
349
|
+
inputSchema: compareInput,
|
|
350
|
+
outputSchema: analysisOutput,
|
|
351
|
+
annotations: readOnly,
|
|
352
|
+
}, async (input, extra) => {
|
|
353
|
+
const progress = progressReporter(extra);
|
|
354
|
+
const base = requestContext(engine, context, input.workspaceId, input.refresh, progress.signal);
|
|
355
|
+
try {
|
|
356
|
+
await progress.report(0, 4, 'Comparing weighted source');
|
|
357
|
+
const request = {
|
|
358
|
+
...base,
|
|
359
|
+
...input,
|
|
360
|
+
workspaceId: base.workspaceId,
|
|
361
|
+
};
|
|
362
|
+
const analyzed = await analyzer.compare(request);
|
|
363
|
+
await progress.report(4, 4, 'Weighted source comparison complete');
|
|
364
|
+
return toolResult(analysisServiceResult(base.workspaceId, analyzed));
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
return errorResult(error, base.workspaceId);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
server.registerTool('hoi4.probability_render', {
|
|
371
|
+
title: 'Render AI and MTTH analysis',
|
|
372
|
+
description: 'Render cached analysis data as deterministic ranking, matrix, waterfall, timing, sensitivity, sequence, comparison, or unresolved resources.',
|
|
373
|
+
inputSchema: renderInput,
|
|
374
|
+
outputSchema: analysisOutput,
|
|
375
|
+
annotations: readOnly,
|
|
376
|
+
}, async (input, extra) => {
|
|
377
|
+
const progress = progressReporter(extra);
|
|
378
|
+
const base = requestContext(engine, context, input.workspaceId, undefined, progress.signal);
|
|
379
|
+
try {
|
|
380
|
+
await progress.report(0, input.outputs.length, 'Rendering analysis resources');
|
|
381
|
+
const request = {
|
|
382
|
+
...base,
|
|
383
|
+
...input,
|
|
384
|
+
workspaceId: base.workspaceId,
|
|
385
|
+
outputs: input.outputs.filter((output) => output !== 'json'),
|
|
386
|
+
};
|
|
387
|
+
const analyzed = await analyzer.render(request);
|
|
388
|
+
await progress.report(input.outputs.length, input.outputs.length, 'Analysis rendering complete');
|
|
389
|
+
return toolResult(analysisServiceResult(base.workspaceId, analyzed));
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
return errorResult(error, base.workspaceId);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
server.registerPrompt('hoi4.probability_analysis', {
|
|
396
|
+
title: 'Analyze HOI4 weighted logic',
|
|
397
|
+
description: 'Plan one source-linked AI, MTTH, random, or declared-pool analysis and return to the owning modding workflow.',
|
|
398
|
+
argsSchema: {
|
|
399
|
+
objective: z.string().min(1).max(4096),
|
|
400
|
+
sourceHint: z.string().max(1024).optional(),
|
|
401
|
+
},
|
|
402
|
+
}, ({ objective, sourceHint }) => ({
|
|
403
|
+
messages: [
|
|
404
|
+
{
|
|
405
|
+
role: 'user',
|
|
406
|
+
content: {
|
|
407
|
+
type: 'text',
|
|
408
|
+
text: `Analyze this HOI4 weighted-logic task: ${objective}${sourceHint === undefined ? '' : `\nSource hint: ${sourceHint}`}\nIdentify the exact weighted surface and adapter, declare representative world-state scenarios and every required candidate or external factor, inspect first, run the narrowest useful evaluate/sweep/simulate/sequence/compare operation, review linked uncertainty and provenance, then return the findings to the normal owning modding workflow. Do not infer missing state, execute effects, or edit source through probability tools.`,
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
],
|
|
412
|
+
}));
|
|
413
|
+
}
|