hoi4-agent-tools 2.3.0 → 2.3.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/CHANGELOG.md +18 -0
- package/dist/bin/stdio.js +4 -0
- package/dist/hoi4_agent_tools/event/algorithms.js +178 -1
- package/dist/hoi4_agent_tools/event/graph.js +38 -9
- package/dist/hoi4_agent_tools/event/limits.js +4 -1
- package/dist/hoi4_agent_tools/event/service.js +200 -46
- package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +480 -0
- package/dist/hoi4_agent_tools/mcp/tools/event.js +10 -3
- package/dist/hoi4_agent_tools/mcp/tools/technology.js +4 -1
- package/dist/hoi4_agent_tools/runtime.js +109 -7
- package/dist/hoi4_agent_tools/technology/graph.js +30 -3
- package/dist/hoi4_agent_tools/technology/queries.js +70 -3
- package/dist/hoi4_agent_tools/technology/service.js +38 -2
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/events.md +2 -2
- package/docs/setup.md +2 -0
- package/docs/technology.md +4 -2
- package/package.json +3 -1
- package/schemas/configuration.schema.json +1 -1
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.3.4 - 2026-07-28
|
|
4
|
+
|
|
5
|
+
- Bounded large game-backed event scans before workspace-wide lifecycle and helper analysis could exhaust the MCP process and close the stdio transport.
|
|
6
|
+
- Added automatic focused event analysis for large workspaces, explicit deferred-analysis diagnostics, source-profile reporting, and deterministic regression coverage.
|
|
7
|
+
|
|
8
|
+
## 2.3.3 - 2026-07-26
|
|
9
|
+
|
|
10
|
+
- Regenerated the npm lockfile with the pinned release npm so clean installs include the patched optional native runtime packages.
|
|
11
|
+
|
|
12
|
+
## 2.3.2 - 2026-07-26
|
|
13
|
+
|
|
14
|
+
- Pinned patched transitive `brace-expansion` and `postcss` releases so the CI and release audit remain clean without changing the MCP Inspector surface.
|
|
15
|
+
|
|
16
|
+
## 2.3.1 - 2026-07-26
|
|
17
|
+
|
|
18
|
+
- Fixed large vanilla-plus-mod technology scans that could consume multiple gigabytes and leave the MCP transport open until a client timeout closed it.
|
|
19
|
+
- Added automatic focused technology analysis for game-backed and large workspaces, retaining direct source evidence and helper-call indexing while deferring workspace-wide scripted-effect projections.
|
|
20
|
+
|
|
3
21
|
## 2.3.0 - 2026-07-22
|
|
4
22
|
|
|
5
23
|
- Added a read-only AI and MTTH Scenario Analyzer with inspect, evaluate, sweep, simulate, sequence, compare, and render tools.
|
package/dist/bin/stdio.js
CHANGED
|
@@ -8,6 +8,10 @@ async function main() {
|
|
|
8
8
|
const engine = await createEngine();
|
|
9
9
|
const context = {};
|
|
10
10
|
const server = createMcpServer(engine, context);
|
|
11
|
+
if (process.env.HOI4_AGENT_TOOLS_CHAOSX === '1') {
|
|
12
|
+
const { registerChaosxTools } = await import('../hoi4_agent_tools/mcp/tools/chaosx.js');
|
|
13
|
+
registerChaosxTools(server, engine, context);
|
|
14
|
+
}
|
|
11
15
|
server.server.onerror = (error) => {
|
|
12
16
|
const fatalInputError = error instanceof StdioFrameLimitError || error instanceof StdioInvalidUtf8Error;
|
|
13
17
|
const rejectedMessage = error instanceof StdioInvalidMessageError;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { compareCodeUnits } from '../core/canonical.js';
|
|
1
|
+
import { compareCodeUnits, hashCanonical } from '../core/canonical.js';
|
|
2
2
|
import { EventAnalysisBudget } from './limits.js';
|
|
3
3
|
import { iterativeStronglyConnectedComponents } from './scc.js';
|
|
4
4
|
function sortedUnique(values) {
|
|
@@ -41,6 +41,178 @@ function edgeMaps(edges) {
|
|
|
41
41
|
}
|
|
42
42
|
return { outgoing, incoming };
|
|
43
43
|
}
|
|
44
|
+
function helperName(node) {
|
|
45
|
+
const name = node.metadata.name;
|
|
46
|
+
if (typeof name === 'string')
|
|
47
|
+
return name;
|
|
48
|
+
return node.label || node.id.replace(/^helper:/u, '');
|
|
49
|
+
}
|
|
50
|
+
function collapsedHelperEdge(path, helperStack, from, to) {
|
|
51
|
+
const first = path[0];
|
|
52
|
+
const last = path.at(-1);
|
|
53
|
+
if (helperStack.length === 0)
|
|
54
|
+
return first;
|
|
55
|
+
return {
|
|
56
|
+
id: `edge:lazy-helper-projection:${hashCanonical({
|
|
57
|
+
from,
|
|
58
|
+
to,
|
|
59
|
+
edges: path.map(({ id }) => id),
|
|
60
|
+
}).slice(0, 32)}`,
|
|
61
|
+
from,
|
|
62
|
+
to,
|
|
63
|
+
reason: 'scripted_effect_expansion',
|
|
64
|
+
conditions: path.flatMap(({ conditions }) => conditions),
|
|
65
|
+
helperStack: [...helperStack],
|
|
66
|
+
location: first.location,
|
|
67
|
+
provenance: path.flatMap(({ provenance }) => provenance),
|
|
68
|
+
confidence: last.confidence,
|
|
69
|
+
derived: true,
|
|
70
|
+
...(last.timing === undefined ? {} : { timing: last.timing }),
|
|
71
|
+
...(last.weight === undefined ? {} : { weight: last.weight }),
|
|
72
|
+
...(last.scope === undefined ? {} : { scope: last.scope }),
|
|
73
|
+
metadata: { structuralEdgeIds: path.map(({ id }) => id) },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Build only the helper projections reached by one bounded trace. Full-workspace
|
|
78
|
+
* helper materialisation is useful for broad reports, but it is unnecessarily
|
|
79
|
+
* expensive when an agent asks for one route in a large installed game.
|
|
80
|
+
*/
|
|
81
|
+
function lazyCollapsedEdges(graph, nodeId, direction, maps, maximumPaths, signal) {
|
|
82
|
+
const nodes = nodeMap(graph);
|
|
83
|
+
const retained = new Map();
|
|
84
|
+
let pathVisits = 0;
|
|
85
|
+
const add = (edge) => {
|
|
86
|
+
retained.set(edge.id, edge);
|
|
87
|
+
};
|
|
88
|
+
const downstream = (edge, path, stack, visited) => {
|
|
89
|
+
signal?.throwIfAborted();
|
|
90
|
+
if (++pathVisits > maximumPaths)
|
|
91
|
+
return;
|
|
92
|
+
if (path.length > 64)
|
|
93
|
+
return;
|
|
94
|
+
const target = nodes.get(edge.to);
|
|
95
|
+
if (target?.kind !== 'helper') {
|
|
96
|
+
add(collapsedHelperEdge(path, stack, nodeId, edge.to));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (visited.has(target.id))
|
|
100
|
+
return;
|
|
101
|
+
for (const next of maps.outgoing.get(target.id) ?? []) {
|
|
102
|
+
downstream(next, [...path, next], [...stack, helperName(target)], new Set([...visited, target.id]));
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const upstream = (edge, path, stack, visited) => {
|
|
106
|
+
signal?.throwIfAborted();
|
|
107
|
+
if (++pathVisits > maximumPaths)
|
|
108
|
+
return;
|
|
109
|
+
if (path.length > 64)
|
|
110
|
+
return;
|
|
111
|
+
const source = nodes.get(edge.from);
|
|
112
|
+
if (source?.kind !== 'helper') {
|
|
113
|
+
const forwardPath = [...path].reverse();
|
|
114
|
+
add(collapsedHelperEdge(forwardPath, [...stack].reverse(), edge.from, nodeId));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (visited.has(source.id))
|
|
118
|
+
return;
|
|
119
|
+
for (const previous of maps.incoming.get(source.id) ?? []) {
|
|
120
|
+
upstream(previous, [...path, previous], [...stack, helperName(source)], new Set([...visited, source.id]));
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
if (direction !== 'upstream') {
|
|
124
|
+
for (const edge of maps.outgoing.get(nodeId) ?? [])
|
|
125
|
+
downstream(edge, [edge], [], new Set());
|
|
126
|
+
}
|
|
127
|
+
if (direction !== 'downstream') {
|
|
128
|
+
for (const edge of maps.incoming.get(nodeId) ?? [])
|
|
129
|
+
upstream(edge, [edge], [], new Set());
|
|
130
|
+
}
|
|
131
|
+
return [...retained.values()].sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
132
|
+
}
|
|
133
|
+
function traceCollapsedHelpers(graph, starts, boundary, signal) {
|
|
134
|
+
const byId = nodeMap(graph);
|
|
135
|
+
const maps = edgeMaps(graph.edges.filter(({ derived }) => !derived));
|
|
136
|
+
const edgeCache = new Map();
|
|
137
|
+
const edgesFor = (nodeId) => {
|
|
138
|
+
const key = `${nodeId}\0${boundary.direction}`;
|
|
139
|
+
const cached = edgeCache.get(key);
|
|
140
|
+
if (cached !== undefined)
|
|
141
|
+
return cached;
|
|
142
|
+
const edges = lazyCollapsedEdges(graph, nodeId, boundary.direction, maps, Math.max(256, boundary.maxEdges * 2), signal);
|
|
143
|
+
edgeCache.set(key, edges);
|
|
144
|
+
return edges;
|
|
145
|
+
};
|
|
146
|
+
const depthByNode = new Map(starts.map((id) => [id, 0]));
|
|
147
|
+
const queue = [...starts];
|
|
148
|
+
const retainedEdges = new Map();
|
|
149
|
+
const boundaryNodes = new Set();
|
|
150
|
+
let truncated = false;
|
|
151
|
+
let cursor = 0;
|
|
152
|
+
while (cursor < queue.length) {
|
|
153
|
+
if ((cursor & 255) === 0)
|
|
154
|
+
signal?.throwIfAborted();
|
|
155
|
+
const current = queue[cursor++];
|
|
156
|
+
const depth = depthByNode.get(current) ?? 0;
|
|
157
|
+
const nextEdges = edgesFor(current);
|
|
158
|
+
if (depth >= boundary.maxDepth) {
|
|
159
|
+
if (nextEdges.length > 0) {
|
|
160
|
+
boundaryNodes.add(current);
|
|
161
|
+
truncated = true;
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
for (const edge of nextEdges) {
|
|
166
|
+
if (retainedEdges.size >= boundary.maxEdges) {
|
|
167
|
+
boundaryNodes.add(current);
|
|
168
|
+
truncated = true;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
const next = boundary.direction === 'upstream'
|
|
172
|
+
? edge.from
|
|
173
|
+
: boundary.direction === 'downstream'
|
|
174
|
+
? edge.to
|
|
175
|
+
: edge.from === current
|
|
176
|
+
? edge.to
|
|
177
|
+
: edge.from;
|
|
178
|
+
const known = depthByNode.has(next);
|
|
179
|
+
if (!known && depthByNode.size >= boundary.maxNodes) {
|
|
180
|
+
boundaryNodes.add(current);
|
|
181
|
+
truncated = true;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
retainedEdges.set(edge.id, edge);
|
|
185
|
+
if (!known) {
|
|
186
|
+
depthByNode.set(next, depth + 1);
|
|
187
|
+
queue.push(next);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const retainedNodes = [...depthByNode.keys()]
|
|
192
|
+
.flatMap((id) => {
|
|
193
|
+
const node = byId.get(id);
|
|
194
|
+
return node === undefined ? [] : [node];
|
|
195
|
+
})
|
|
196
|
+
.sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
197
|
+
const layers = [...new Set(depthByNode.values())]
|
|
198
|
+
.sort((left, right) => left - right)
|
|
199
|
+
.map((depth) => ({
|
|
200
|
+
depth,
|
|
201
|
+
nodeIds: [...depthByNode.entries()]
|
|
202
|
+
.filter(([, value]) => value === depth)
|
|
203
|
+
.map(([id]) => id)
|
|
204
|
+
.sort(compareCodeUnits),
|
|
205
|
+
}));
|
|
206
|
+
return {
|
|
207
|
+
startNodeIds: [...starts],
|
|
208
|
+
nodes: retainedNodes,
|
|
209
|
+
edges: [...retainedEdges.values()].sort((left, right) => compareCodeUnits(left.id, right.id)),
|
|
210
|
+
layers,
|
|
211
|
+
boundary,
|
|
212
|
+
boundaryNodeIds: [...boundaryNodes].sort(compareCodeUnits),
|
|
213
|
+
truncated,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
44
216
|
function edgesForDirection(nodeId, direction, maps) {
|
|
45
217
|
const results = [];
|
|
46
218
|
if (direction !== 'upstream') {
|
|
@@ -58,6 +230,11 @@ export function traceEventGraph(graph, startNodeIds, boundary, signal) {
|
|
|
58
230
|
const byId = nodeMap(graph);
|
|
59
231
|
const resolvedStarts = sortedUnique(startNodeIds).filter((id) => byId.has(id));
|
|
60
232
|
const starts = resolvedStarts.slice(0, Math.max(0, boundary.maxNodes));
|
|
233
|
+
if (!boundary.expandHelpers &&
|
|
234
|
+
!graph.edges.some(({ derived }) => derived) &&
|
|
235
|
+
starts.every((id) => byId.get(id)?.kind !== 'helper')) {
|
|
236
|
+
return traceCollapsedHelpers(graph, starts, boundary, signal);
|
|
237
|
+
}
|
|
61
238
|
const edges = eventFlowEdges(graph, boundary.expandHelpers);
|
|
62
239
|
const maps = edgeMaps(edges);
|
|
63
240
|
const depthByNode = new Map(starts.map((id) => [id, 0]));
|
|
@@ -29,6 +29,14 @@ function activeFiles(snapshot) {
|
|
|
29
29
|
!/(?:^|\/)(?:supply_nodes|railways|buildings|unitstacks|weatherpositions)\.txt$/iu.test(relativePath.replaceAll('\\', '/')))
|
|
30
30
|
.sort((left, right) => compareCodeUnits(left.displayPath, right.displayPath));
|
|
31
31
|
}
|
|
32
|
+
const EVENT_SETUP_CALL_PATTERN = /\b(?:country_event|news_event|state_event|unit_leader_event|operative_leader_event|fire_event|event)\s*=/iu;
|
|
33
|
+
function shouldAnalyzeEventFile(file) {
|
|
34
|
+
const sourcePath = file.relativePath.replaceAll('\\', '/').toLowerCase();
|
|
35
|
+
if (!sourcePath.startsWith('history/countries/') && !sourcePath.startsWith('history/states/')) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return EVENT_SETUP_CALL_PATTERN.test(file.bytes.toString('utf8'));
|
|
39
|
+
}
|
|
32
40
|
function catalogFor(snapshot) {
|
|
33
41
|
const unshadowedEventSymbols = snapshot.index
|
|
34
42
|
.findAll('event')
|
|
@@ -1444,6 +1452,8 @@ function analyzeFragments(snapshot, files, catalog, options, budget) {
|
|
|
1444
1452
|
const fragments = [];
|
|
1445
1453
|
for (const file of files) {
|
|
1446
1454
|
budget.spend('source_fragment');
|
|
1455
|
+
if (!shouldAnalyzeEventFile(file))
|
|
1456
|
+
continue;
|
|
1447
1457
|
if (snapshot.index.isSourceSkipped(file.displayPath))
|
|
1448
1458
|
continue;
|
|
1449
1459
|
const cacheKey = eventSemanticFragmentCacheKey(file, catalog.fingerprint);
|
|
@@ -1560,14 +1570,31 @@ export function buildEventGraph(snapshot, options = {}) {
|
|
|
1560
1570
|
graph.edges = stableUnique(graph.edges);
|
|
1561
1571
|
graph.stateAccesses = stableUnique(graph.stateAccesses);
|
|
1562
1572
|
graph.unresolved = stableUnique(graph.unresolved);
|
|
1563
|
-
const
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1573
|
+
const focused = options.analysisMode === 'focused';
|
|
1574
|
+
if (focused) {
|
|
1575
|
+
graph.unresolved.push({
|
|
1576
|
+
id: 'event-analysis-focused-boundary',
|
|
1577
|
+
kind: 'partial_source',
|
|
1578
|
+
expression: 'workspace-wide event helper projections and lifecycle diagnostics',
|
|
1579
|
+
confidence: 'unresolved',
|
|
1580
|
+
blockers: [
|
|
1581
|
+
{
|
|
1582
|
+
code: 'EVENT_FOCUSED_ANALYSIS_DEFERRED',
|
|
1583
|
+
message: 'Large event workspaces defer workspace-wide helper projections and lifecycle passes; direct source evidence remains available.',
|
|
1584
|
+
},
|
|
1585
|
+
],
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
const stateLinks = focused ? [] : stateLinksFor(graph, budget);
|
|
1589
|
+
if (!focused) {
|
|
1590
|
+
duplicateAndReferenceIssues(graph);
|
|
1591
|
+
unresolvedIssues(graph);
|
|
1592
|
+
structuralCallIssues(graph, budget);
|
|
1593
|
+
localisationIssues(graph);
|
|
1594
|
+
stateLifecycleIssues(graph, stateLinks, budget);
|
|
1595
|
+
unreachableIssues(graph, budget);
|
|
1596
|
+
normalizeIssueSubjects(graph);
|
|
1597
|
+
}
|
|
1571
1598
|
graph.issues = stableIssues(graph.issues);
|
|
1572
1599
|
assertGraphLimit(graph.nodes.length, EVENT_GRAPH_MAX_NODES, 'EVENT_NODE_LIMIT', 'node count');
|
|
1573
1600
|
assertGraphLimit(graph.edges.length, EVENT_GRAPH_MAX_EDGES, 'EVENT_EDGE_LIMIT', 'edge count');
|
|
@@ -1577,7 +1604,8 @@ export function buildEventGraph(snapshot, options = {}) {
|
|
|
1577
1604
|
assertGraphLimit(graph.unresolved.length, EVENT_GRAPH_MAX_UNRESOLVED, 'EVENT_UNRESOLVED_LIMIT', 'unresolved-analysis count');
|
|
1578
1605
|
budget.check();
|
|
1579
1606
|
const diagnostics = sortDiagnostics([...snapshot.diagnostics, ...partial.diagnostics]);
|
|
1580
|
-
const complete =
|
|
1607
|
+
const complete = !focused &&
|
|
1608
|
+
snapshot.complete &&
|
|
1581
1609
|
graph.unresolved.length === 0 &&
|
|
1582
1610
|
!graph.issues.some(({ classification, severity }) => classification === 'unresolved_analysis' || severity === 'blocker');
|
|
1583
1611
|
const nodes = sortNodes(graph.nodes);
|
|
@@ -1588,6 +1616,7 @@ export function buildEventGraph(snapshot, options = {}) {
|
|
|
1588
1616
|
return {
|
|
1589
1617
|
schemaVersion: EVENT_GRAPH_SCHEMA_VERSION,
|
|
1590
1618
|
parserVersion: EVENT_GRAPH_PARSER_VERSION,
|
|
1619
|
+
analysisMode: options.analysisMode ?? 'full',
|
|
1591
1620
|
workspaceId: snapshot.workspaceId,
|
|
1592
1621
|
workspaceIdentity: options.workspaceIdentity ?? hashCanonical({ workspaceId: snapshot.workspaceId }),
|
|
1593
1622
|
revision: snapshot.revision,
|
|
@@ -4,7 +4,10 @@ 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;
|
|
6
6
|
export const EVENT_GRAPH_MAX_STATE_LINK_CANDIDATES = 500_000;
|
|
7
|
-
|
|
7
|
+
// Installed-game scans can legitimately produce more than twenty thousand source-linked
|
|
8
|
+
// findings before an agent narrows the query. Keep the graph bounded, but do not reject a
|
|
9
|
+
// complete large workspace merely because its diagnostics are numerous.
|
|
10
|
+
export const EVENT_GRAPH_MAX_ISSUES = 100_000;
|
|
8
11
|
export const EVENT_GRAPH_MAX_UNRESOLVED = 50_000;
|
|
9
12
|
export const EVENT_GRAPH_MAX_HELPER_DEPTH = 64;
|
|
10
13
|
export const EVENT_GRAPH_MAX_HELPER_PROJECTIONS = 200_000;
|