hoi4-agent-tools 1.0.0 → 1.2.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 +21 -0
- package/README.md +27 -16
- package/dist/hoi4_agent_tools/core/engine.js +12 -0
- package/dist/hoi4_agent_tools/event/algorithms.js +208 -0
- package/dist/hoi4_agent_tools/event/artifact-validation.js +572 -0
- package/dist/hoi4_agent_tools/event/compare.js +413 -0
- package/dist/hoi4_agent_tools/event/graph.js +1586 -0
- package/dist/hoi4_agent_tools/event/index.js +10 -0
- package/dist/hoi4_agent_tools/event/layout.js +218 -0
- package/dist/hoi4_agent_tools/event/limits.js +41 -0
- package/dist/hoi4_agent_tools/event/model.js +2 -0
- package/dist/hoi4_agent_tools/event/queries.js +643 -0
- package/dist/hoi4_agent_tools/event/render.js +443 -0
- package/dist/hoi4_agent_tools/event/scc.js +72 -0
- package/dist/hoi4_agent_tools/event/service.js +1147 -0
- package/dist/hoi4_agent_tools/event/source-analysis.js +1458 -0
- 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/native-effects.js +3 -1
- 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/resources/register.js +1 -1
- package/dist/hoi4_agent_tools/mcp/server/context-schemas.js +8 -0
- package/dist/hoi4_agent_tools/mcp/server/create.js +4 -1
- package/dist/hoi4_agent_tools/mcp/server/result.js +117 -52
- package/dist/hoi4_agent_tools/mcp/tools/event.js +459 -0
- 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/event.js +95 -0
- package/dist/hoi4_agent_tools/schemas/focus.js +64 -8
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +1 -0
- package/docs/development.md +10 -0
- package/docs/events.md +113 -0
- package/docs/focus.md +17 -7
- package/docs/setup.md +3 -3
- package/package.json +9 -5
- package/server.json +3 -3
|
@@ -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)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Source: the official HOI4 1.19.2 effects documentation reviewed 2026-07-10.
|
|
6
6
|
* Keep this list synchronized during compatibility reviews.
|
|
7
7
|
*/
|
|
8
|
-
export const
|
|
8
|
+
export const nativeEffectKeys = new Set([
|
|
9
9
|
'activate_advisor',
|
|
10
10
|
'activate_decision',
|
|
11
11
|
'activate_mission',
|
|
@@ -560,3 +560,5 @@ export const nativeFocusEffectKeys = new Set([
|
|
|
560
560
|
'while_loop_effect',
|
|
561
561
|
'white_peace',
|
|
562
562
|
]);
|
|
563
|
+
/** Backward-compatible focus-domain name for the shared official effect catalog. */
|
|
564
|
+
export const nativeFocusEffectKeys = nativeEffectKeys;
|
|
@@ -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))
|
|
@@ -6,7 +6,7 @@ const artifactChunkMetadataKey = 'io.github.klimpaskov/hoi4-agent-tools.artifact
|
|
|
6
6
|
const fatalUtf8Decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
7
7
|
const artifactResourceMetadata = {
|
|
8
8
|
title: 'HOI4 review artifact',
|
|
9
|
-
description: 'Complete plans, diagnostics, layouts, previews, and diffs. Reads accept zero-based byte offset and positive byte length query selectors; each response reports its exact byte range and continuation URI in _meta.',
|
|
9
|
+
description: 'Complete plans, diagnostics, layouts, previews, event graphs, routes, state and scope reports, comparisons, and diffs. Reads accept zero-based byte offset and positive byte length query selectors; each response reports its exact byte range and continuation URI in _meta.',
|
|
10
10
|
_meta: {
|
|
11
11
|
[artifactChunkMetadataKey]: {
|
|
12
12
|
version: 1,
|
|
@@ -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
|
+
}
|
|
@@ -4,7 +4,9 @@ import { registerModsTool } from '../tools/mods.js';
|
|
|
4
4
|
import { registerFocusTools } from '../tools/focus.js';
|
|
5
5
|
import { registerGuiTools } from '../tools/gui.js';
|
|
6
6
|
import { registerMapTools } from '../tools/map.js';
|
|
7
|
+
import { registerEventTools } from '../tools/event.js';
|
|
7
8
|
import { registerMcpResources } from '../resources/register.js';
|
|
9
|
+
export const SERVER_INSTRUCTIONS = 'Use hoi4.mods to select a workspace. Use hoi4.event_inspect for scan, roots, trace, explain_path, state_flow, lint, and impact; hoi4.event_render and hoi4.event_compare return linked evidence. JSON resources are authoritative. Event tools are read-only. Only focus, GUI, and map rewrite tools edit configured mods.';
|
|
8
10
|
export function createMcpServer(engine, context = {}) {
|
|
9
11
|
const server = new McpServer({
|
|
10
12
|
name: PACKAGE_NAME,
|
|
@@ -12,12 +14,13 @@ export function createMcpServer(engine, context = {}) {
|
|
|
12
14
|
websiteUrl: 'https://github.com/klimPaskov/hoi4-agent-tools',
|
|
13
15
|
}, {
|
|
14
16
|
capabilities: { logging: {} },
|
|
15
|
-
instructions:
|
|
17
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
16
18
|
});
|
|
17
19
|
registerModsTool(server, engine, context);
|
|
18
20
|
registerFocusTools(server, engine, context);
|
|
19
21
|
registerGuiTools(server, engine, context);
|
|
20
22
|
registerMapTools(server, engine, context);
|
|
23
|
+
registerEventTools(server, engine, context);
|
|
21
24
|
registerMcpResources(server, engine, context);
|
|
22
25
|
return server;
|
|
23
26
|
}
|
|
@@ -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,37 @@ 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(),
|
|
46
|
+
workspaceId: z.string(),
|
|
47
|
+
filesScanned: z.array(z.unknown()).max(MAX_INLINE_FILES_SCANNED),
|
|
48
|
+
proposedFiles: z.array(z.unknown()).max(MAX_INLINE_PROPOSED_FILES),
|
|
49
|
+
changedFiles: z.array(z.unknown()).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.unknown(),
|
|
53
|
+
blockers: z.array(z.unknown()).max(MAX_INLINE_BLOCKERS),
|
|
54
|
+
data: z.object({}).catchall(z.unknown()),
|
|
55
|
+
})
|
|
56
|
+
.strict();
|
|
42
57
|
/**
|
|
43
|
-
* Advertise
|
|
44
|
-
* The generic operation-result schema remains available for stored envelopes
|
|
45
|
-
* a public tool output contract.
|
|
58
|
+
* Advertise one compact operation envelope while retaining exact per-tool runtime validation.
|
|
59
|
+
* The generic operation-result schema remains available for stored envelopes.
|
|
46
60
|
*/
|
|
47
61
|
export function strictOperationResultSchema(successDataSchema) {
|
|
48
|
-
|
|
62
|
+
const exactSchema = operationResultBaseSchema
|
|
49
63
|
.extend({ data: z.union([emptyToolDataSchema, successDataSchema]) })
|
|
50
64
|
.strict();
|
|
65
|
+
return compactPublicOperationResultSchema.superRefine((value, context) => {
|
|
66
|
+
const parsed = exactSchema.safeParse(value);
|
|
67
|
+
if (parsed.success)
|
|
68
|
+
return;
|
|
69
|
+
for (const issue of parsed.error.issues) {
|
|
70
|
+
context.addIssue({ code: 'custom', path: issue.path, message: issue.message });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
51
73
|
}
|
|
52
74
|
/** Keep source inventories compact; complete inventories belong in linked resources. */
|
|
53
75
|
export function setInlineFilesScanned(result, files) {
|
|
@@ -82,11 +104,14 @@ function preserveDeferredFilesDiagnostic(result) {
|
|
|
82
104
|
};
|
|
83
105
|
}
|
|
84
106
|
function boundedResult(result, actualBytes) {
|
|
107
|
+
const hasLinkedEvidence = result.artifacts.length > 0;
|
|
85
108
|
const truncationDiagnostic = {
|
|
86
109
|
code: 'MCP_RESPONSE_TRUNCATED',
|
|
87
110
|
severity: 'warning',
|
|
88
111
|
category: 'configuration',
|
|
89
|
-
message:
|
|
112
|
+
message: hasLinkedEvidence
|
|
113
|
+
? 'Tool output exceeded the wire budget; inspect the linked evidence for complete details'
|
|
114
|
+
: 'Tool output exceeded the wire budget; nonessential inline details were omitted',
|
|
90
115
|
details: { actualBytes, maxBytes: MAX_TOOL_RESULT_BYTES },
|
|
91
116
|
};
|
|
92
117
|
return {
|
|
@@ -106,7 +131,9 @@ function boundedResult(result, actualBytes) {
|
|
|
106
131
|
: [
|
|
107
132
|
{
|
|
108
133
|
code: 'MCP_RESPONSE_TRUNCATED',
|
|
109
|
-
message:
|
|
134
|
+
message: hasLinkedEvidence
|
|
135
|
+
? 'Tool output exceeded the wire budget; inspect the linked evidence for complete details'
|
|
136
|
+
: 'Tool output exceeded the wire budget; nonessential inline details were omitted',
|
|
110
137
|
details: {
|
|
111
138
|
actualBytes,
|
|
112
139
|
maxBytes: MAX_TOOL_RESULT_BYTES,
|
|
@@ -196,32 +223,60 @@ function jsonBytesWithin(value, budget) {
|
|
|
196
223
|
return visit(value);
|
|
197
224
|
}
|
|
198
225
|
function compactDetails(details) {
|
|
199
|
-
return jsonBytesWithin(details,
|
|
226
|
+
return jsonBytesWithin(details, 2_048) ? details : { truncated: true };
|
|
200
227
|
}
|
|
201
228
|
function compactDescriptions(result) {
|
|
202
229
|
const compactLocation = (location) => location === undefined
|
|
203
230
|
? undefined
|
|
204
231
|
: {
|
|
205
232
|
...location,
|
|
206
|
-
path: location.path.slice(0,
|
|
207
|
-
...(location.symbol === undefined ? {} : { symbol: location.symbol.slice(0,
|
|
233
|
+
path: location.path.slice(0, 1_024),
|
|
234
|
+
...(location.symbol === undefined ? {} : { symbol: location.symbol.slice(0, 256) }),
|
|
208
235
|
};
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
:
|
|
221
|
-
|
|
236
|
+
const collectionTotals = {
|
|
237
|
+
filesScanned: result.filesScanned.length,
|
|
238
|
+
proposedFiles: result.proposedFiles.length,
|
|
239
|
+
changedFiles: result.changedFiles.length,
|
|
240
|
+
diagnostics: result.diagnostics.length,
|
|
241
|
+
artifacts: result.artifacts.length,
|
|
242
|
+
validationChecks: result.validation.checks.length,
|
|
243
|
+
blockers: result.blockers.length,
|
|
244
|
+
};
|
|
245
|
+
const collectionLimits = {
|
|
246
|
+
filesScanned: MAX_INLINE_FILES_SCANNED,
|
|
247
|
+
proposedFiles: MAX_INLINE_PROPOSED_FILES,
|
|
248
|
+
changedFiles: MAX_INLINE_CHANGED_FILES,
|
|
249
|
+
diagnostics: MAX_INLINE_DIAGNOSTICS,
|
|
250
|
+
artifacts: MAX_INLINE_ARTIFACT_LINKS,
|
|
251
|
+
validationChecks: MAX_INLINE_VALIDATION_CHECKS,
|
|
252
|
+
blockers: MAX_INLINE_BLOCKERS,
|
|
253
|
+
};
|
|
254
|
+
const truncatedCollections = Object.fromEntries(Object.entries(collectionTotals).flatMap(([name, total]) => {
|
|
255
|
+
const limit = collectionLimits[name];
|
|
256
|
+
return total > limit ? [[name, { total, limit }]] : [];
|
|
257
|
+
}));
|
|
258
|
+
if (Object.keys(truncatedCollections).length > 0 &&
|
|
259
|
+
result.diagnostics.length > MAX_INLINE_DIAGNOSTICS - 1) {
|
|
260
|
+
truncatedCollections.diagnostics = {
|
|
261
|
+
total: result.diagnostics.length,
|
|
262
|
+
limit: MAX_INLINE_DIAGNOSTICS - 1,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const needsTruncationDiagnostic = Object.keys(truncatedCollections).length > 0;
|
|
266
|
+
const diagnosticSlots = Math.max(0, MAX_INLINE_DIAGNOSTICS - (needsTruncationDiagnostic ? 1 : 0));
|
|
267
|
+
const hardDiagnostics = result.diagnostics.filter(({ severity }) => severity === 'error' || severity === 'blocker');
|
|
268
|
+
const priorityDiagnostics = result.diagnostics.filter(({ code, severity }) => severity === 'error' || severity === 'blocker' || code === 'MCP_INLINE_FILES_TRUNCATED');
|
|
269
|
+
const otherDiagnostics = result.diagnostics.filter((diagnostic) => !priorityDiagnostics.includes(diagnostic));
|
|
270
|
+
const retainedPriorityDiagnostics = priorityDiagnostics.slice(0, diagnosticSlots);
|
|
271
|
+
const retainedDiagnostics = [
|
|
272
|
+
...retainedPriorityDiagnostics,
|
|
273
|
+
...otherDiagnostics.slice(0, diagnosticSlots - retainedPriorityDiagnostics.length),
|
|
274
|
+
];
|
|
275
|
+
const omittedHardDiagnostics = hardDiagnostics.filter((diagnostic) => !retainedDiagnostics.includes(diagnostic)).length;
|
|
276
|
+
const compactDiagnostics = retainedDiagnostics.map((diagnostic) => ({
|
|
222
277
|
...diagnostic,
|
|
223
278
|
code: diagnostic.code.slice(0, 256),
|
|
224
|
-
message: diagnostic.message.slice(0,
|
|
279
|
+
message: diagnostic.message.slice(0, 1_024),
|
|
225
280
|
...(diagnostic.operationId === undefined
|
|
226
281
|
? {}
|
|
227
282
|
: { operationId: diagnostic.operationId.slice(0, 256) }),
|
|
@@ -231,35 +286,53 @@ function compactDescriptions(result) {
|
|
|
231
286
|
...(diagnostic.related === undefined
|
|
232
287
|
? {}
|
|
233
288
|
: {
|
|
234
|
-
related: diagnostic.related.slice(0,
|
|
289
|
+
related: diagnostic.related.slice(0, 3).map((location) => compactLocation(location)),
|
|
235
290
|
}),
|
|
236
291
|
...(diagnostic.details === undefined ? {} : { details: compactDetails(diagnostic.details) }),
|
|
237
292
|
}));
|
|
293
|
+
const truncationDiagnostic = needsTruncationDiagnostic
|
|
294
|
+
? {
|
|
295
|
+
code: 'MCP_INLINE_COLLECTIONS_TRUNCATED',
|
|
296
|
+
severity: omittedHardDiagnostics > 0 ? 'blocker' : 'info',
|
|
297
|
+
category: 'configuration',
|
|
298
|
+
message: result.artifacts.length > 0
|
|
299
|
+
? 'Large collections were bounded inline; linked resources retain bulk evidence'
|
|
300
|
+
: 'Large collections were bounded inline',
|
|
301
|
+
details: {
|
|
302
|
+
collections: truncatedCollections,
|
|
303
|
+
diagnosticsReturned: retainedDiagnostics.length,
|
|
304
|
+
omittedHardDiagnostics,
|
|
305
|
+
},
|
|
306
|
+
}
|
|
307
|
+
: undefined;
|
|
238
308
|
return {
|
|
239
309
|
...result,
|
|
240
|
-
|
|
310
|
+
filesScanned: result.filesScanned.slice(0, MAX_INLINE_FILES_SCANNED),
|
|
311
|
+
proposedFiles: result.proposedFiles.slice(0, MAX_INLINE_PROPOSED_FILES),
|
|
312
|
+
changedFiles: result.changedFiles.slice(0, MAX_INLINE_CHANGED_FILES),
|
|
313
|
+
diagnostics: truncationDiagnostic === undefined
|
|
241
314
|
? compactDiagnostics
|
|
242
|
-
: [
|
|
315
|
+
: [truncationDiagnostic, ...compactDiagnostics],
|
|
316
|
+
artifacts: result.artifacts.slice(0, MAX_INLINE_ARTIFACT_LINKS).map((artifact) => ({
|
|
317
|
+
...artifact,
|
|
318
|
+
...(artifact.description === undefined
|
|
319
|
+
? {}
|
|
320
|
+
: { description: artifact.description.slice(0, 256) }),
|
|
321
|
+
})),
|
|
243
322
|
validation: {
|
|
244
323
|
...result.validation,
|
|
245
324
|
checks: result.validation.checks.slice(0, MAX_INLINE_VALIDATION_CHECKS).map((check) => ({
|
|
246
325
|
...check,
|
|
247
326
|
id: check.id.slice(0, 256),
|
|
248
|
-
message: check.message.slice(0,
|
|
327
|
+
message: check.message.slice(0, 1_024),
|
|
249
328
|
})),
|
|
250
329
|
},
|
|
251
|
-
blockers: result.blockers.map((blocker) => ({
|
|
330
|
+
blockers: result.blockers.slice(0, MAX_INLINE_BLOCKERS).map((blocker) => ({
|
|
252
331
|
...blocker,
|
|
253
332
|
code: blocker.code.slice(0, 256),
|
|
254
|
-
message: blocker.message.slice(0,
|
|
333
|
+
message: blocker.message.slice(0, 1_024),
|
|
255
334
|
...(blocker.details === undefined ? {} : { details: compactDetails(blocker.details) }),
|
|
256
335
|
})),
|
|
257
|
-
artifacts: result.artifacts.map((artifact) => ({
|
|
258
|
-
...artifact,
|
|
259
|
-
...(artifact.description === undefined
|
|
260
|
-
? {}
|
|
261
|
-
: { description: artifact.description.slice(0, 1_024) }),
|
|
262
|
-
})),
|
|
263
336
|
};
|
|
264
337
|
}
|
|
265
338
|
function buildToolResult(selected) {
|
|
@@ -286,23 +359,15 @@ function buildToolResult(selected) {
|
|
|
286
359
|
}
|
|
287
360
|
export function toolResult(result) {
|
|
288
361
|
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
362
|
const compact = compactDescriptions(completeResult);
|
|
296
363
|
const estimatedBytes = jsonBytesWithin(compact, MAX_TOOL_RESULT_BYTES)
|
|
297
364
|
? Buffer.byteLength(canonicalJson(compact), 'utf8')
|
|
298
365
|
: MAX_TOOL_RESULT_BYTES + 1;
|
|
299
|
-
const selected = estimatedBytes > MAX_TOOL_RESULT_BYTES
|
|
300
|
-
? boundedResult(completeResult, estimatedBytes)
|
|
301
|
-
: compact;
|
|
366
|
+
const selected = estimatedBytes > MAX_TOOL_RESULT_BYTES ? boundedResult(compact, estimatedBytes) : compact;
|
|
302
367
|
let output = buildToolResult(selected);
|
|
303
368
|
const wireBytes = Buffer.byteLength(canonicalJson(output), 'utf8');
|
|
304
369
|
if (wireBytes > MAX_TOOL_RESULT_BYTES) {
|
|
305
|
-
output = buildToolResult(boundedResult(
|
|
370
|
+
output = buildToolResult(boundedResult(compact, wireBytes));
|
|
306
371
|
}
|
|
307
372
|
const finalBytes = Buffer.byteLength(canonicalJson(output), 'utf8');
|
|
308
373
|
if (finalBytes > MAX_TOOL_RESULT_BYTES) {
|