hoi4-agent-tools 1.0.0 → 1.1.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 +9 -0
- package/README.md +8 -2
- package/dist/hoi4_agent_tools/focus/compact.js +432 -0
- package/dist/hoi4_agent_tools/focus/compiler.js +1 -7
- package/dist/hoi4_agent_tools/focus/geometry.js +203 -16
- package/dist/hoi4_agent_tools/focus/index.js +1 -0
- package/dist/hoi4_agent_tools/focus/layout.js +1256 -92
- package/dist/hoi4_agent_tools/focus/limits.js +1 -1
- package/dist/hoi4_agent_tools/focus/lint.js +11 -3
- package/dist/hoi4_agent_tools/focus/planning.js +6 -1
- package/dist/hoi4_agent_tools/focus/render.js +14 -9
- package/dist/hoi4_agent_tools/mcp/server/context-schemas.js +8 -0
- package/dist/hoi4_agent_tools/mcp/server/create.js +0 -1
- package/dist/hoi4_agent_tools/mcp/server/result.js +123 -52
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +93 -7
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +12 -9
- package/dist/hoi4_agent_tools/mcp/tools/map.js +6 -4
- package/dist/hoi4_agent_tools/mcp/tools/mods.js +1 -1
- package/dist/hoi4_agent_tools/schemas/focus.js +64 -8
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/focus.md +17 -7
- package/docs/setup.md +1 -1
- package/package.json +3 -3
- package/server.json +2 -2
|
@@ -2,7 +2,7 @@ export const FOCUS_GRAPH_MAX_DEPTH = 256;
|
|
|
2
2
|
export const FOCUS_GRAPH_MAX_NODES = 10_000;
|
|
3
3
|
export const FOCUS_GRAPH_MAX_EDGES = 50_000;
|
|
4
4
|
export const FOCUS_PAIR_COMPARISON_MAX = 2_000_000;
|
|
5
|
-
export const FOCUS_LAYOUT_WORK_MAX =
|
|
5
|
+
export const FOCUS_LAYOUT_WORK_MAX = 8_000_000;
|
|
6
6
|
export const FOCUS_LINT_WORK_MAX = 2_000_000;
|
|
7
7
|
export const FOCUS_DIAGNOSTIC_MAX = 2_000;
|
|
8
8
|
export const FOCUS_CYCLE_SAMPLE_MAX = 128;
|
|
@@ -2,7 +2,7 @@ import { compareCodeUnits } from '../core/canonical.js';
|
|
|
2
2
|
import { sortDiagnostics } from '../core/diagnostics.js';
|
|
3
3
|
import { DiagnosticCollector } from '../core/diagnostic-collector.js';
|
|
4
4
|
import { ServiceError } from '../core/result.js';
|
|
5
|
-
import {
|
|
5
|
+
import { flattenFocusConnectorCurve, focusConnectorCurve, focusConnectorPolylinesIntersect, focusNodesVisiblyOverlap, } from './geometry.js';
|
|
6
6
|
import { layoutFocusTree } from './layout.js';
|
|
7
7
|
import { FOCUS_CYCLE_SAMPLE_MAX, FOCUS_DIAGNOSTIC_MAX, FOCUS_GRAPH_MAX_DEPTH, FOCUS_GRAPH_MAX_EDGES, FOCUS_GRAPH_MAX_NODES, FOCUS_LINT_WORK_MAX, FOCUS_PAIR_COMPARISON_MAX, } from './limits.js';
|
|
8
8
|
import { layoutNodeMap, } from './model.js';
|
|
@@ -641,7 +641,15 @@ export function lintFocusTree(plan, options = {}) {
|
|
|
641
641
|
const child = positions.get(focus.id);
|
|
642
642
|
return parent === undefined || child === undefined
|
|
643
643
|
? []
|
|
644
|
-
: [
|
|
644
|
+
: [
|
|
645
|
+
{
|
|
646
|
+
parentId,
|
|
647
|
+
childId: focus.id,
|
|
648
|
+
parent,
|
|
649
|
+
child,
|
|
650
|
+
points: flattenFocusConnectorCurve(focusConnectorCurve(parent, child)),
|
|
651
|
+
},
|
|
652
|
+
];
|
|
645
653
|
}));
|
|
646
654
|
const connectorPairCount = (connectors.length * (connectors.length - 1)) / 2;
|
|
647
655
|
if (connectorPairCount > FOCUS_PAIR_COMPARISON_MAX) {
|
|
@@ -668,7 +676,7 @@ export function lintFocusTree(plan, options = {}) {
|
|
|
668
676
|
first.childId === second.childId) {
|
|
669
677
|
continue;
|
|
670
678
|
}
|
|
671
|
-
if (!
|
|
679
|
+
if (!focusConnectorPolylinesIntersect(first.points, second.points))
|
|
672
680
|
continue;
|
|
673
681
|
const focus = focuses.get(second.childId);
|
|
674
682
|
if (focus === undefined)
|
|
@@ -27,6 +27,7 @@ function planningNode(focus) {
|
|
|
27
27
|
aiStrategyIds: [...focus.ai.strategyIds],
|
|
28
28
|
...(focus.payoff === undefined ? {} : { payoff: focus.payoff }),
|
|
29
29
|
...(focus.terminalKind === undefined ? {} : { terminalKind: focus.terminalKind }),
|
|
30
|
+
...(focus.position.mode === 'auto' ? { autoPosition: structuredClone(focus.position) } : {}),
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
export function createFocusPlanningSidecar(plan, sourceHash = plan.provenance.sourceHash) {
|
|
@@ -139,8 +140,12 @@ export function enrichFocusPlanFromSidecar(plan, sidecar) {
|
|
|
139
140
|
delete focus.laneId;
|
|
140
141
|
else
|
|
141
142
|
focus.laneId = saved.laneId;
|
|
142
|
-
if (
|
|
143
|
+
if (saved.autoPosition !== undefined) {
|
|
144
|
+
focus.position = structuredClone(saved.autoPosition);
|
|
145
|
+
}
|
|
146
|
+
else if (focus.position.mode !== 'auto') {
|
|
143
147
|
focus.position.pinned = saved.pinned;
|
|
148
|
+
}
|
|
144
149
|
focus.visibility = saved.visibility;
|
|
145
150
|
if (saved.reveal === undefined)
|
|
146
151
|
delete focus.reveal;
|
|
@@ -6,7 +6,7 @@ import { ServiceError } from '../core/result.js';
|
|
|
6
6
|
import { DeterministicSvgTextRenderer } from '../core/svg-text.js';
|
|
7
7
|
import { PACKAGE_VERSION } from '../version.js';
|
|
8
8
|
import { compileFocusTreeWithSourceMap } from './compiler.js';
|
|
9
|
-
import { FOCUS_HORIZONTAL_GRID_PIXELS, FOCUS_NODE_HEIGHT_PIXELS, FOCUS_NODE_WIDTH_PIXELS, FOCUS_VERTICAL_GRID_PIXELS, focusNodeOrigin, } from './geometry.js';
|
|
9
|
+
import { FOCUS_HORIZONTAL_GRID_PIXELS, FOCUS_NODE_HEIGHT_PIXELS, FOCUS_NODE_WIDTH_PIXELS, FOCUS_VERTICAL_GRID_PIXELS, focusConnectorCurve, focusConnectorSvgPath, focusNodeOrigin, } from './geometry.js';
|
|
10
10
|
import { createFocusPlanningSidecar, serializeFocusPlanningSidecar } from './planning.js';
|
|
11
11
|
import { FOCUS_GRAPH_MAX_EDGES, FOCUS_GRAPH_MAX_NODES } from './lint.js';
|
|
12
12
|
import { layoutNodeMap, } from './model.js';
|
|
@@ -173,6 +173,7 @@ function svgDocument(plan, layout, diagnostics, options) {
|
|
|
173
173
|
node.id,
|
|
174
174
|
focusNodeOrigin(node, minimumX, minimumY, padding, horizontal, vertical),
|
|
175
175
|
]));
|
|
176
|
+
const layoutNodes = new Map(nodes.map((node) => [node.id, node]));
|
|
176
177
|
const focusMap = new Map(plan.focuses.map((focus) => [focus.id, focus]));
|
|
177
178
|
const diagnosticMap = new Map();
|
|
178
179
|
const toolText = new DeterministicSvgTextRenderer();
|
|
@@ -214,16 +215,19 @@ function svgDocument(plan, layout, diagnostics, options) {
|
|
|
214
215
|
'<g id="prerequisites">',
|
|
215
216
|
];
|
|
216
217
|
for (const edge of prerequisiteEdges) {
|
|
217
|
-
const parent =
|
|
218
|
-
const child =
|
|
218
|
+
const parent = layoutNodes.get(edge.parentId);
|
|
219
|
+
const child = layoutNodes.get(edge.childId);
|
|
219
220
|
if (parent === undefined || child === undefined)
|
|
220
221
|
continue;
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
222
|
+
const curve = focusConnectorCurve(parent, child, {
|
|
223
|
+
horizontalSpacing: horizontal,
|
|
224
|
+
verticalSpacing: vertical,
|
|
225
|
+
nodeWidth,
|
|
226
|
+
nodeHeight,
|
|
227
|
+
originX: padding - minimumX * horizontal,
|
|
228
|
+
originY: padding - minimumY * vertical,
|
|
229
|
+
});
|
|
230
|
+
parts.push(`<path d="${focusConnectorSvgPath(curve)}" fill="none" stroke="#8da2bc" stroke-width="2" marker-end="url(#arrow)" data-parent="${escapeXml(edge.parentId)}" data-child="${escapeXml(edge.childId)}" data-prerequisite-group="${edge.groupIndex}"/>`);
|
|
227
231
|
}
|
|
228
232
|
parts.push('</g>', '<g id="mutual-exclusions">');
|
|
229
233
|
for (const [leftId, rightId] of exclusionEdges) {
|
|
@@ -309,6 +313,7 @@ function svgDocument(plan, layout, diagnostics, options) {
|
|
|
309
313
|
hash: layout.layoutHash,
|
|
310
314
|
nodes,
|
|
311
315
|
decisions: layout.decisions,
|
|
316
|
+
metrics: layout.metrics ?? null,
|
|
312
317
|
},
|
|
313
318
|
focuses: [...plan.focuses]
|
|
314
319
|
.sort((left, right) => compareCodeUnits(left.id, right.id))
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
/**
|
|
3
|
+
* Keep a large nested payload schema out of tools/list while retaining its complete runtime parser.
|
|
4
|
+
* The MCP SDK publishes the input side of a pipe and executes both sides when a tool is called.
|
|
5
|
+
*/
|
|
6
|
+
export function compactValidatedInputSchema(schema, description) {
|
|
7
|
+
return z.unknown().describe(description).pipe(schema);
|
|
8
|
+
}
|
|
@@ -12,7 +12,6 @@ export function createMcpServer(engine, context = {}) {
|
|
|
12
12
|
websiteUrl: 'https://github.com/klimPaskov/hoi4-agent-tools',
|
|
13
13
|
}, {
|
|
14
14
|
capabilities: { logging: {} },
|
|
15
|
-
instructions: 'Use this server when a configured HOI4 mod needs a large national or continuous focus, scripted GUI, or map created, inspected, rendered, or cleaned up. Start with hoi4.mods, then use the matching inspect, render, and rewrite tools. Each rewrite performs the complete edit in one call. Read linked hoi4-agent:// artifact resources for complete plans, diagnostics, layouts, previews, and diffs; when a resource content item has a non-null continuationUri in its namespaced _meta byte-range record, follow it until null. Generated GUI renders are offline representations, never game screenshots.',
|
|
16
15
|
});
|
|
17
16
|
registerModsTool(server, engine, context);
|
|
18
17
|
registerFocusTools(server, engine, context);
|
|
@@ -3,15 +3,15 @@ import { canonicalJson } from '../../core/canonical.js';
|
|
|
3
3
|
import { ServiceError } from '../../core/result.js';
|
|
4
4
|
import { diagnosticSchema } from '../../schemas/common.js';
|
|
5
5
|
import { artifactLinkSchema, validationSummarySchema } from '../../schemas/transaction.js';
|
|
6
|
-
export const MAX_INLINE_FILES_SCANNED =
|
|
7
|
-
export const MAX_INLINE_PROPOSED_FILES =
|
|
8
|
-
export const MAX_INLINE_CHANGED_FILES =
|
|
9
|
-
export const MAX_INLINE_DIAGNOSTICS =
|
|
10
|
-
export const MAX_INLINE_ARTIFACT_LINKS =
|
|
11
|
-
export const MAX_INLINE_BLOCKERS =
|
|
6
|
+
export const MAX_INLINE_FILES_SCANNED = 64;
|
|
7
|
+
export const MAX_INLINE_PROPOSED_FILES = 64;
|
|
8
|
+
export const MAX_INLINE_CHANGED_FILES = 64;
|
|
9
|
+
export const MAX_INLINE_DIAGNOSTICS = 20;
|
|
10
|
+
export const MAX_INLINE_ARTIFACT_LINKS = 32;
|
|
11
|
+
export const MAX_INLINE_BLOCKERS = 20;
|
|
12
12
|
export const MAX_INLINE_VALIDATION_CHECKS = 5;
|
|
13
13
|
// Leave headroom for the enclosing JSON-RPC response and transport metadata.
|
|
14
|
-
export const MAX_TOOL_RESULT_BYTES =
|
|
14
|
+
export const MAX_TOOL_RESULT_BYTES = 32_768;
|
|
15
15
|
const deferredFilesDiagnostic = Symbol('deferredFilesDiagnostic');
|
|
16
16
|
const operationResultBaseSchema = z
|
|
17
17
|
.object({
|
|
@@ -39,15 +39,43 @@ export const operationResultSchema = operationResultBaseSchema
|
|
|
39
39
|
.extend({ data: z.record(z.string().max(256), z.unknown()) })
|
|
40
40
|
.strict();
|
|
41
41
|
const emptyToolDataSchema = z.object({}).strict();
|
|
42
|
+
const compactPublicOperationResultSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
status: z.enum(['ok', 'blocked', 'error']),
|
|
45
|
+
code: z.string().max(256),
|
|
46
|
+
workspaceId: z.string().max(64),
|
|
47
|
+
filesScanned: z.array(z.string().max(4096)).max(MAX_INLINE_FILES_SCANNED),
|
|
48
|
+
proposedFiles: z.array(z.string().max(1024)).max(MAX_INLINE_PROPOSED_FILES),
|
|
49
|
+
changedFiles: z.array(z.string().max(1024)).max(MAX_INLINE_CHANGED_FILES),
|
|
50
|
+
diagnostics: z.array(z.unknown()).max(MAX_INLINE_DIAGNOSTICS),
|
|
51
|
+
artifacts: z.array(z.unknown()).max(MAX_INLINE_ARTIFACT_LINKS),
|
|
52
|
+
validation: z
|
|
53
|
+
.object({
|
|
54
|
+
passed: z.boolean(),
|
|
55
|
+
checks: z.array(z.unknown()).max(MAX_INLINE_VALIDATION_CHECKS),
|
|
56
|
+
})
|
|
57
|
+
.strict(),
|
|
58
|
+
blockers: z.array(z.unknown()).max(MAX_INLINE_BLOCKERS),
|
|
59
|
+
data: z.record(z.string().max(256), z.unknown()),
|
|
60
|
+
})
|
|
61
|
+
.strict()
|
|
62
|
+
.describe('Stable operation envelope; data is tool-specific and artifacts link bulk evidence.');
|
|
42
63
|
/**
|
|
43
|
-
* Advertise
|
|
44
|
-
* The generic operation-result schema remains available for stored envelopes
|
|
45
|
-
* a public tool output contract.
|
|
64
|
+
* Advertise one compact operation envelope while retaining exact per-tool runtime validation.
|
|
65
|
+
* The generic operation-result schema remains available for stored envelopes.
|
|
46
66
|
*/
|
|
47
67
|
export function strictOperationResultSchema(successDataSchema) {
|
|
48
|
-
|
|
68
|
+
const exactSchema = operationResultBaseSchema
|
|
49
69
|
.extend({ data: z.union([emptyToolDataSchema, successDataSchema]) })
|
|
50
70
|
.strict();
|
|
71
|
+
return compactPublicOperationResultSchema.superRefine((value, context) => {
|
|
72
|
+
const parsed = exactSchema.safeParse(value);
|
|
73
|
+
if (parsed.success)
|
|
74
|
+
return;
|
|
75
|
+
for (const issue of parsed.error.issues) {
|
|
76
|
+
context.addIssue({ code: 'custom', path: issue.path, message: issue.message });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
51
79
|
}
|
|
52
80
|
/** Keep source inventories compact; complete inventories belong in linked resources. */
|
|
53
81
|
export function setInlineFilesScanned(result, files) {
|
|
@@ -82,11 +110,14 @@ function preserveDeferredFilesDiagnostic(result) {
|
|
|
82
110
|
};
|
|
83
111
|
}
|
|
84
112
|
function boundedResult(result, actualBytes) {
|
|
113
|
+
const hasLinkedEvidence = result.artifacts.length > 0;
|
|
85
114
|
const truncationDiagnostic = {
|
|
86
115
|
code: 'MCP_RESPONSE_TRUNCATED',
|
|
87
116
|
severity: 'warning',
|
|
88
117
|
category: 'configuration',
|
|
89
|
-
message:
|
|
118
|
+
message: hasLinkedEvidence
|
|
119
|
+
? 'Tool output exceeded the wire budget; inspect the linked evidence for complete details'
|
|
120
|
+
: 'Tool output exceeded the wire budget; nonessential inline details were omitted',
|
|
90
121
|
details: { actualBytes, maxBytes: MAX_TOOL_RESULT_BYTES },
|
|
91
122
|
};
|
|
92
123
|
return {
|
|
@@ -106,7 +137,9 @@ function boundedResult(result, actualBytes) {
|
|
|
106
137
|
: [
|
|
107
138
|
{
|
|
108
139
|
code: 'MCP_RESPONSE_TRUNCATED',
|
|
109
|
-
message:
|
|
140
|
+
message: hasLinkedEvidence
|
|
141
|
+
? 'Tool output exceeded the wire budget; inspect the linked evidence for complete details'
|
|
142
|
+
: 'Tool output exceeded the wire budget; nonessential inline details were omitted',
|
|
110
143
|
details: {
|
|
111
144
|
actualBytes,
|
|
112
145
|
maxBytes: MAX_TOOL_RESULT_BYTES,
|
|
@@ -196,32 +229,60 @@ function jsonBytesWithin(value, budget) {
|
|
|
196
229
|
return visit(value);
|
|
197
230
|
}
|
|
198
231
|
function compactDetails(details) {
|
|
199
|
-
return jsonBytesWithin(details,
|
|
232
|
+
return jsonBytesWithin(details, 2_048) ? details : { truncated: true };
|
|
200
233
|
}
|
|
201
234
|
function compactDescriptions(result) {
|
|
202
235
|
const compactLocation = (location) => location === undefined
|
|
203
236
|
? undefined
|
|
204
237
|
: {
|
|
205
238
|
...location,
|
|
206
|
-
path: location.path.slice(0,
|
|
207
|
-
...(location.symbol === undefined ? {} : { symbol: location.symbol.slice(0,
|
|
239
|
+
path: location.path.slice(0, 1_024),
|
|
240
|
+
...(location.symbol === undefined ? {} : { symbol: location.symbol.slice(0, 256) }),
|
|
208
241
|
};
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
:
|
|
221
|
-
|
|
242
|
+
const collectionTotals = {
|
|
243
|
+
filesScanned: result.filesScanned.length,
|
|
244
|
+
proposedFiles: result.proposedFiles.length,
|
|
245
|
+
changedFiles: result.changedFiles.length,
|
|
246
|
+
diagnostics: result.diagnostics.length,
|
|
247
|
+
artifacts: result.artifacts.length,
|
|
248
|
+
validationChecks: result.validation.checks.length,
|
|
249
|
+
blockers: result.blockers.length,
|
|
250
|
+
};
|
|
251
|
+
const collectionLimits = {
|
|
252
|
+
filesScanned: MAX_INLINE_FILES_SCANNED,
|
|
253
|
+
proposedFiles: MAX_INLINE_PROPOSED_FILES,
|
|
254
|
+
changedFiles: MAX_INLINE_CHANGED_FILES,
|
|
255
|
+
diagnostics: MAX_INLINE_DIAGNOSTICS,
|
|
256
|
+
artifacts: MAX_INLINE_ARTIFACT_LINKS,
|
|
257
|
+
validationChecks: MAX_INLINE_VALIDATION_CHECKS,
|
|
258
|
+
blockers: MAX_INLINE_BLOCKERS,
|
|
259
|
+
};
|
|
260
|
+
const truncatedCollections = Object.fromEntries(Object.entries(collectionTotals).flatMap(([name, total]) => {
|
|
261
|
+
const limit = collectionLimits[name];
|
|
262
|
+
return total > limit ? [[name, { total, limit }]] : [];
|
|
263
|
+
}));
|
|
264
|
+
if (Object.keys(truncatedCollections).length > 0 &&
|
|
265
|
+
result.diagnostics.length > MAX_INLINE_DIAGNOSTICS - 1) {
|
|
266
|
+
truncatedCollections.diagnostics = {
|
|
267
|
+
total: result.diagnostics.length,
|
|
268
|
+
limit: MAX_INLINE_DIAGNOSTICS - 1,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const needsTruncationDiagnostic = Object.keys(truncatedCollections).length > 0;
|
|
272
|
+
const diagnosticSlots = Math.max(0, MAX_INLINE_DIAGNOSTICS - (needsTruncationDiagnostic ? 1 : 0));
|
|
273
|
+
const hardDiagnostics = result.diagnostics.filter(({ severity }) => severity === 'error' || severity === 'blocker');
|
|
274
|
+
const priorityDiagnostics = result.diagnostics.filter(({ code, severity }) => severity === 'error' || severity === 'blocker' || code === 'MCP_INLINE_FILES_TRUNCATED');
|
|
275
|
+
const otherDiagnostics = result.diagnostics.filter((diagnostic) => !priorityDiagnostics.includes(diagnostic));
|
|
276
|
+
const retainedPriorityDiagnostics = priorityDiagnostics.slice(0, diagnosticSlots);
|
|
277
|
+
const retainedDiagnostics = [
|
|
278
|
+
...retainedPriorityDiagnostics,
|
|
279
|
+
...otherDiagnostics.slice(0, diagnosticSlots - retainedPriorityDiagnostics.length),
|
|
280
|
+
];
|
|
281
|
+
const omittedHardDiagnostics = hardDiagnostics.filter((diagnostic) => !retainedDiagnostics.includes(diagnostic)).length;
|
|
282
|
+
const compactDiagnostics = retainedDiagnostics.map((diagnostic) => ({
|
|
222
283
|
...diagnostic,
|
|
223
284
|
code: diagnostic.code.slice(0, 256),
|
|
224
|
-
message: diagnostic.message.slice(0,
|
|
285
|
+
message: diagnostic.message.slice(0, 1_024),
|
|
225
286
|
...(diagnostic.operationId === undefined
|
|
226
287
|
? {}
|
|
227
288
|
: { operationId: diagnostic.operationId.slice(0, 256) }),
|
|
@@ -231,35 +292,53 @@ function compactDescriptions(result) {
|
|
|
231
292
|
...(diagnostic.related === undefined
|
|
232
293
|
? {}
|
|
233
294
|
: {
|
|
234
|
-
related: diagnostic.related.slice(0,
|
|
295
|
+
related: diagnostic.related.slice(0, 3).map((location) => compactLocation(location)),
|
|
235
296
|
}),
|
|
236
297
|
...(diagnostic.details === undefined ? {} : { details: compactDetails(diagnostic.details) }),
|
|
237
298
|
}));
|
|
299
|
+
const truncationDiagnostic = needsTruncationDiagnostic
|
|
300
|
+
? {
|
|
301
|
+
code: 'MCP_INLINE_COLLECTIONS_TRUNCATED',
|
|
302
|
+
severity: omittedHardDiagnostics > 0 ? 'blocker' : 'info',
|
|
303
|
+
category: 'configuration',
|
|
304
|
+
message: result.artifacts.length > 0
|
|
305
|
+
? 'Large collections were bounded inline; linked resources retain bulk evidence'
|
|
306
|
+
: 'Large collections were bounded inline',
|
|
307
|
+
details: {
|
|
308
|
+
collections: truncatedCollections,
|
|
309
|
+
diagnosticsReturned: retainedDiagnostics.length,
|
|
310
|
+
omittedHardDiagnostics,
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
: undefined;
|
|
238
314
|
return {
|
|
239
315
|
...result,
|
|
240
|
-
|
|
316
|
+
filesScanned: result.filesScanned.slice(0, MAX_INLINE_FILES_SCANNED),
|
|
317
|
+
proposedFiles: result.proposedFiles.slice(0, MAX_INLINE_PROPOSED_FILES),
|
|
318
|
+
changedFiles: result.changedFiles.slice(0, MAX_INLINE_CHANGED_FILES),
|
|
319
|
+
diagnostics: truncationDiagnostic === undefined
|
|
241
320
|
? compactDiagnostics
|
|
242
|
-
: [
|
|
321
|
+
: [truncationDiagnostic, ...compactDiagnostics],
|
|
322
|
+
artifacts: result.artifacts.slice(0, MAX_INLINE_ARTIFACT_LINKS).map((artifact) => ({
|
|
323
|
+
...artifact,
|
|
324
|
+
...(artifact.description === undefined
|
|
325
|
+
? {}
|
|
326
|
+
: { description: artifact.description.slice(0, 256) }),
|
|
327
|
+
})),
|
|
243
328
|
validation: {
|
|
244
329
|
...result.validation,
|
|
245
330
|
checks: result.validation.checks.slice(0, MAX_INLINE_VALIDATION_CHECKS).map((check) => ({
|
|
246
331
|
...check,
|
|
247
332
|
id: check.id.slice(0, 256),
|
|
248
|
-
message: check.message.slice(0,
|
|
333
|
+
message: check.message.slice(0, 1_024),
|
|
249
334
|
})),
|
|
250
335
|
},
|
|
251
|
-
blockers: result.blockers.map((blocker) => ({
|
|
336
|
+
blockers: result.blockers.slice(0, MAX_INLINE_BLOCKERS).map((blocker) => ({
|
|
252
337
|
...blocker,
|
|
253
338
|
code: blocker.code.slice(0, 256),
|
|
254
|
-
message: blocker.message.slice(0,
|
|
339
|
+
message: blocker.message.slice(0, 1_024),
|
|
255
340
|
...(blocker.details === undefined ? {} : { details: compactDetails(blocker.details) }),
|
|
256
341
|
})),
|
|
257
|
-
artifacts: result.artifacts.map((artifact) => ({
|
|
258
|
-
...artifact,
|
|
259
|
-
...(artifact.description === undefined
|
|
260
|
-
? {}
|
|
261
|
-
: { description: artifact.description.slice(0, 1_024) }),
|
|
262
|
-
})),
|
|
263
342
|
};
|
|
264
343
|
}
|
|
265
344
|
function buildToolResult(selected) {
|
|
@@ -286,23 +365,15 @@ function buildToolResult(selected) {
|
|
|
286
365
|
}
|
|
287
366
|
export function toolResult(result) {
|
|
288
367
|
const completeResult = preserveDeferredFilesDiagnostic(result);
|
|
289
|
-
const exceedsArrayBudget = completeResult.filesScanned.length > MAX_INLINE_FILES_SCANNED ||
|
|
290
|
-
completeResult.proposedFiles.length > MAX_INLINE_PROPOSED_FILES ||
|
|
291
|
-
completeResult.changedFiles.length > MAX_INLINE_CHANGED_FILES ||
|
|
292
|
-
completeResult.diagnostics.length > MAX_INLINE_DIAGNOSTICS ||
|
|
293
|
-
completeResult.artifacts.length > MAX_INLINE_ARTIFACT_LINKS ||
|
|
294
|
-
completeResult.blockers.length > MAX_INLINE_BLOCKERS;
|
|
295
368
|
const compact = compactDescriptions(completeResult);
|
|
296
369
|
const estimatedBytes = jsonBytesWithin(compact, MAX_TOOL_RESULT_BYTES)
|
|
297
370
|
? Buffer.byteLength(canonicalJson(compact), 'utf8')
|
|
298
371
|
: MAX_TOOL_RESULT_BYTES + 1;
|
|
299
|
-
const selected = estimatedBytes > MAX_TOOL_RESULT_BYTES
|
|
300
|
-
? boundedResult(completeResult, estimatedBytes)
|
|
301
|
-
: compact;
|
|
372
|
+
const selected = estimatedBytes > MAX_TOOL_RESULT_BYTES ? boundedResult(compact, estimatedBytes) : compact;
|
|
302
373
|
let output = buildToolResult(selected);
|
|
303
374
|
const wireBytes = Buffer.byteLength(canonicalJson(output), 'utf8');
|
|
304
375
|
if (wireBytes > MAX_TOOL_RESULT_BYTES) {
|
|
305
|
-
output = buildToolResult(boundedResult(
|
|
376
|
+
output = buildToolResult(boundedResult(compact, wireBytes));
|
|
306
377
|
}
|
|
307
378
|
const finalBytes = Buffer.byteLength(canonicalJson(output), 'utf8');
|
|
308
379
|
if (finalBytes > MAX_TOOL_RESULT_BYTES) {
|
|
@@ -6,14 +6,15 @@ import { RenderBudget } from '../../core/render-budget.js';
|
|
|
6
6
|
import { emptyServiceResult, ServiceError } from '../../core/result.js';
|
|
7
7
|
import { parseClausewitz } from '../../core/source/index.js';
|
|
8
8
|
import { readDependenciesFromScannedFiles } from '../../core/transactions.js';
|
|
9
|
-
import { FocusWorkbench, FOCUS_RENDER_MAX_OUTPUT_SCALE, FOCUS_RENDER_MIN_OUTPUT_SCALE, focusPlanHash, focusPresentationEvidence, focusPlanningSidecarPath, importContinuousFocusPalettes, importFocusTrees, linkContinuousFocusPalettes, parseFocusPlanningSidecar, renderContinuousFocusPalette, renderFocusTree, resolveFocusPresentation, enrichFocusPlanFromSidecar, } from '../../focus/index.js';
|
|
10
|
-
import { continuousFocusPaletteSchema, focusLayoutSchema, focusTreePlanSchema, } from '../../schemas/focus.js';
|
|
9
|
+
import { FocusWorkbench, FOCUS_RENDER_MAX_OUTPUT_SCALE, FOCUS_RENDER_MIN_OUTPUT_SCALE, assertCompactLayoutQuality, compactFocusTreePlanAsync, focusPlanHash, focusPresentationEvidence, focusPlanningSidecarPath, importContinuousFocusPalettes, importFocusTrees, linkContinuousFocusPalettes, parseFocusPlanningSidecar, renderContinuousFocusPalette, renderFocusTree, resolveFocusPresentation, enrichFocusPlanFromSidecar, } from '../../focus/index.js';
|
|
10
|
+
import { continuousFocusPaletteSchema, focusLayoutMetricsSchema, focusLayoutSchema, focusTreePlanSchema, } from '../../schemas/focus.js';
|
|
11
11
|
import { workspaceIdSchema, workspaceRelativePathSchema } from '../../schemas/common.js';
|
|
12
12
|
import { PACKAGE_VERSION } from '../../version.js';
|
|
13
13
|
import { nonNegativeIntegerSchema, renderHashesSchema, sha256Schema, } from '../server/output-schemas.js';
|
|
14
14
|
import { progressReporter } from '../server/progress.js';
|
|
15
15
|
import { errorResult, setInlineFilesScanned, strictOperationResultSchema, toolResult, } from '../server/result.js';
|
|
16
16
|
import { requireServerScope } from '../server/base-tools.js';
|
|
17
|
+
import { compactValidatedInputSchema } from '../server/context-schemas.js';
|
|
17
18
|
import { autonomousFailureContext, autonomousResultArtifacts, executePlannedTransaction, } from '../server/transaction-execution.js';
|
|
18
19
|
const focusInspectOutputSchema = strictOperationResultSchema(z
|
|
19
20
|
.object({
|
|
@@ -33,6 +34,7 @@ const focusInspectOutputSchema = strictOperationResultSchema(z
|
|
|
33
34
|
resolvedTitleCount: nonNegativeIntegerSchema,
|
|
34
35
|
layoutHash: sha256Schema,
|
|
35
36
|
layoutDecisionCount: nonNegativeIntegerSchema,
|
|
37
|
+
layoutMetrics: focusLayoutMetricsSchema,
|
|
36
38
|
diagnosticCount: nonNegativeIntegerSchema,
|
|
37
39
|
})
|
|
38
40
|
.strict())
|
|
@@ -114,6 +116,7 @@ const focusPlanOutputSchema = strictOperationResultSchema(z.discriminatedUnion('
|
|
|
114
116
|
})
|
|
115
117
|
.strict(),
|
|
116
118
|
]));
|
|
119
|
+
const compactFocusPlanSchema = compactValidatedInputSchema(z.union([focusTreePlanSchema, continuousFocusPaletteSchema]), `Complete focus plan: https://github.com/klimPaskov/hoi4-agent-tools/blob/v${PACKAGE_VERSION}/docs/focus.md`);
|
|
117
120
|
const focusInspectInput = z
|
|
118
121
|
.object({
|
|
119
122
|
mode: z.enum(['national', 'continuous']).optional(),
|
|
@@ -208,7 +211,9 @@ const focusPlanInput = z
|
|
|
208
211
|
mode: z.enum(['national', 'continuous']).optional(),
|
|
209
212
|
workspaceId: workspaceIdSchema,
|
|
210
213
|
relativePath: workspaceRelativePathSchema,
|
|
211
|
-
|
|
214
|
+
treeId: z.string().min(1).max(256).optional(),
|
|
215
|
+
layoutMode: z.enum(['authored', 'compact']).default('authored'),
|
|
216
|
+
plan: compactFocusPlanSchema.optional(),
|
|
212
217
|
createIfMissing: z.boolean().default(false),
|
|
213
218
|
horizontalSpacing: z.number().int().min(80).max(1000).optional(),
|
|
214
219
|
verticalSpacing: z.number().int().min(60).max(1000).optional(),
|
|
@@ -223,13 +228,31 @@ const focusPlanInput = z
|
|
|
223
228
|
.superRefine((value, context) => {
|
|
224
229
|
const mode = value.mode ?? 'national';
|
|
225
230
|
const schema = mode === 'continuous' ? continuousFocusPaletteSchema : focusTreePlanSchema;
|
|
226
|
-
if (!schema.safeParse(value.plan).success)
|
|
231
|
+
if (value.plan !== undefined && !schema.safeParse(value.plan).success)
|
|
227
232
|
context.addIssue({
|
|
228
233
|
code: 'custom',
|
|
229
234
|
path: ['plan'],
|
|
230
235
|
message: `plan must be a ${mode} focus plan`,
|
|
231
236
|
});
|
|
232
237
|
if (mode === 'continuous') {
|
|
238
|
+
if (value.plan === undefined)
|
|
239
|
+
context.addIssue({
|
|
240
|
+
code: 'custom',
|
|
241
|
+
path: ['plan'],
|
|
242
|
+
message: 'plan is required in continuous mode',
|
|
243
|
+
});
|
|
244
|
+
if (value.treeId !== undefined)
|
|
245
|
+
context.addIssue({
|
|
246
|
+
code: 'custom',
|
|
247
|
+
path: ['treeId'],
|
|
248
|
+
message: 'treeId is only valid in national mode',
|
|
249
|
+
});
|
|
250
|
+
if (value.layoutMode !== 'authored')
|
|
251
|
+
context.addIssue({
|
|
252
|
+
code: 'custom',
|
|
253
|
+
path: ['layoutMode'],
|
|
254
|
+
message: 'compact layout mode is only valid in national mode',
|
|
255
|
+
});
|
|
233
256
|
for (const [field, fieldValue] of [
|
|
234
257
|
['horizontalSpacing', value.horizontalSpacing],
|
|
235
258
|
['verticalSpacing', value.verticalSpacing],
|
|
@@ -244,6 +267,35 @@ const focusPlanInput = z
|
|
|
244
267
|
});
|
|
245
268
|
}
|
|
246
269
|
}
|
|
270
|
+
else {
|
|
271
|
+
if (value.plan === undefined && value.layoutMode !== 'compact')
|
|
272
|
+
context.addIssue({
|
|
273
|
+
code: 'custom',
|
|
274
|
+
path: ['plan'],
|
|
275
|
+
message: 'plan is required unless layoutMode is compact',
|
|
276
|
+
});
|
|
277
|
+
if (value.plan === undefined && value.treeId === undefined)
|
|
278
|
+
context.addIssue({
|
|
279
|
+
code: 'custom',
|
|
280
|
+
path: ['treeId'],
|
|
281
|
+
message: 'treeId is required for plan-free compact reflow',
|
|
282
|
+
});
|
|
283
|
+
if (value.plan === undefined && value.createIfMissing)
|
|
284
|
+
context.addIssue({
|
|
285
|
+
code: 'custom',
|
|
286
|
+
path: ['createIfMissing'],
|
|
287
|
+
message: 'plan-free compact reflow requires an existing focus source',
|
|
288
|
+
});
|
|
289
|
+
if (value.plan !== undefined &&
|
|
290
|
+
value.treeId !== undefined &&
|
|
291
|
+
'id' in value.plan &&
|
|
292
|
+
value.plan.id !== value.treeId)
|
|
293
|
+
context.addIssue({
|
|
294
|
+
code: 'custom',
|
|
295
|
+
path: ['treeId'],
|
|
296
|
+
message: 'treeId must match the supplied national focus plan',
|
|
297
|
+
});
|
|
298
|
+
}
|
|
247
299
|
});
|
|
248
300
|
const artifactProducing = {
|
|
249
301
|
readOnlyHint: false,
|
|
@@ -584,6 +636,7 @@ export function registerFocusTools(server, engine, context) {
|
|
|
584
636
|
resolvedTitleCount: plan.focuses.filter(({ id }) => presentation.entries[id]?.titleSourceLocation !== undefined).length,
|
|
585
637
|
layoutHash: layout.layoutHash,
|
|
586
638
|
layoutDecisionCount: layout.decisions.length,
|
|
639
|
+
layoutMetrics: layout.metrics,
|
|
587
640
|
diagnosticCount,
|
|
588
641
|
})),
|
|
589
642
|
palettes: [],
|
|
@@ -727,7 +780,7 @@ export function registerFocusTools(server, engine, context) {
|
|
|
727
780
|
});
|
|
728
781
|
server.registerTool('hoi4.focus_rewrite', {
|
|
729
782
|
title: 'Create or clean up focus content',
|
|
730
|
-
description: 'Create or clean up a national tree or continuous palette
|
|
783
|
+
description: 'Create or clean up a national tree or continuous palette, validate it, and apply it in one call. Supply a complete plan; set layoutMode compact for automatic arrangement. Existing national trees can omit the plan and use treeId plus layoutMode compact. Set createIfMissing for a new file.',
|
|
731
784
|
inputSchema: focusPlanInput,
|
|
732
785
|
outputSchema: focusPlanOutputSchema,
|
|
733
786
|
annotations: {
|
|
@@ -901,7 +954,7 @@ export function registerFocusTools(server, engine, context) {
|
|
|
901
954
|
: 'Continuous focus rewrite blocked');
|
|
902
955
|
return toolResult(result);
|
|
903
956
|
}
|
|
904
|
-
const
|
|
957
|
+
const suppliedPlan = input.plan;
|
|
905
958
|
if (createIfMissing &&
|
|
906
959
|
(!relativePath.toLowerCase().endsWith('.txt') ||
|
|
907
960
|
!underConfiguredRoot(relativePath, workspace.registration.roots.focus)))
|
|
@@ -919,6 +972,30 @@ export function registerFocusTools(server, engine, context) {
|
|
|
919
972
|
const paletteImport = continuousPalettes(snapshot);
|
|
920
973
|
if (imported !== undefined)
|
|
921
974
|
imported.result.plans = imported.result.plans.map((current) => linkContinuousFocusPalettes(current, paletteImport.palettes));
|
|
975
|
+
let compactPlanning;
|
|
976
|
+
let compactSourcePlan;
|
|
977
|
+
let plan;
|
|
978
|
+
if (suppliedPlan !== undefined) {
|
|
979
|
+
if (input.layoutMode === 'compact') {
|
|
980
|
+
compactSourcePlan = suppliedPlan;
|
|
981
|
+
compactPlanning = await compactFocusTreePlanAsync(suppliedPlan, {
|
|
982
|
+
signal: progress.signal,
|
|
983
|
+
});
|
|
984
|
+
plan = compactPlanning.plan;
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
987
|
+
plan = suppliedPlan;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
else {
|
|
991
|
+
if (imported === undefined)
|
|
992
|
+
throw new ServiceError('FOCUS_COMPACT_SOURCE_REQUIRED', 'Plan-free compact reflow requires an existing national focus source', { relativePath });
|
|
993
|
+
compactSourcePlan = selectPlan(imported.result, input.treeId);
|
|
994
|
+
compactPlanning = await compactFocusTreePlanAsync(compactSourcePlan, {
|
|
995
|
+
signal: progress.signal,
|
|
996
|
+
});
|
|
997
|
+
plan = compactPlanning.plan;
|
|
998
|
+
}
|
|
922
999
|
const currentPlan = imported?.result.plans.find(({ id }) => id === plan.id);
|
|
923
1000
|
if (currentPlan === undefined && !createIfMissing) {
|
|
924
1001
|
if (imported === undefined)
|
|
@@ -954,7 +1031,9 @@ export function registerFocusTools(server, engine, context) {
|
|
|
954
1031
|
});
|
|
955
1032
|
const currentLayout = currentPlan === undefined
|
|
956
1033
|
? undefined
|
|
957
|
-
:
|
|
1034
|
+
: currentPlan === compactSourcePlan && compactPlanning !== undefined
|
|
1035
|
+
? compactPlanning.currentLayout
|
|
1036
|
+
: await workbench.layoutAsync(currentPlan, { signal: progress.signal });
|
|
958
1037
|
const currentDiagnostics = currentPlan === undefined || currentLayout === undefined
|
|
959
1038
|
? []
|
|
960
1039
|
: workbench.lint(currentPlan, {
|
|
@@ -980,6 +1059,11 @@ export function registerFocusTools(server, engine, context) {
|
|
|
980
1059
|
importedPlanHash: proposedPlanHash,
|
|
981
1060
|
},
|
|
982
1061
|
};
|
|
1062
|
+
const proposedLayout = compactPlanning?.proposedLayout ??
|
|
1063
|
+
(await workbench.layoutAsync(reviewPlan, { signal: progress.signal }));
|
|
1064
|
+
if (input.layoutMode === 'compact') {
|
|
1065
|
+
assertCompactLayoutQuality(currentLayout, proposedLayout);
|
|
1066
|
+
}
|
|
983
1067
|
const proposed = await workbench.renderAndStore(workspaceId, reviewPlan, {
|
|
984
1068
|
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
985
1069
|
index: snapshot.index,
|
|
@@ -994,6 +1078,7 @@ export function registerFocusTools(server, engine, context) {
|
|
|
994
1078
|
},
|
|
995
1079
|
budget: renderBudget,
|
|
996
1080
|
signal: progress.signal,
|
|
1081
|
+
layout: proposedLayout,
|
|
997
1082
|
});
|
|
998
1083
|
const beforePng = currentRender?.png ??
|
|
999
1084
|
(await createTransparentPng(proposed.bundle.width, proposed.bundle.height, 'new national focus baseline', renderBudget));
|
|
@@ -1043,6 +1128,7 @@ export function registerFocusTools(server, engine, context) {
|
|
|
1043
1128
|
plan,
|
|
1044
1129
|
createIfMissing,
|
|
1045
1130
|
authority: 'plan',
|
|
1131
|
+
layout: proposed.layout,
|
|
1046
1132
|
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
1047
1133
|
index: snapshot.index,
|
|
1048
1134
|
references: catalog,
|