hoi4-agent-tools 3.0.9 → 3.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.
Files changed (124) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.md +4 -0
  3. package/dist/bin/stdio.js +23 -12
  4. package/dist/hoi4_agent_tools/core/analysis-checkpoints.js +82 -0
  5. package/dist/hoi4_agent_tools/core/artifact-resource.js +173 -0
  6. package/dist/hoi4_agent_tools/core/artifacts.js +84 -2
  7. package/dist/hoi4_agent_tools/core/dependency-pages.js +216 -0
  8. package/dist/hoi4_agent_tools/core/dependency-walk.js +266 -0
  9. package/dist/hoi4_agent_tools/core/domain-validation.js +188 -0
  10. package/dist/hoi4_agent_tools/core/engine.js +48 -5
  11. package/dist/hoi4_agent_tools/core/helper-expansion.js +186 -0
  12. package/dist/hoi4_agent_tools/core/index-segments.js +137 -0
  13. package/dist/hoi4_agent_tools/core/index.js +47 -4
  14. package/dist/hoi4_agent_tools/core/job-controls.js +109 -0
  15. package/dist/hoi4_agent_tools/core/job-executor.js +190 -0
  16. package/dist/hoi4_agent_tools/core/job-service.js +112 -0
  17. package/dist/hoi4_agent_tools/core/job-store.js +641 -0
  18. package/dist/hoi4_agent_tools/core/job-transactions.js +126 -0
  19. package/dist/hoi4_agent_tools/core/job-worker-host.js +200 -0
  20. package/dist/hoi4_agent_tools/core/job-worker.js +62 -0
  21. package/dist/hoi4_agent_tools/core/operation-context.js +31 -0
  22. package/dist/hoi4_agent_tools/core/operation-result.js +412 -0
  23. package/dist/hoi4_agent_tools/core/operation-tasks.js +382 -0
  24. package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +383 -0
  25. package/dist/hoi4_agent_tools/core/result.js +1 -0
  26. package/dist/hoi4_agent_tools/core/scanner.js +15 -75
  27. package/dist/hoi4_agent_tools/core/semantic-dependencies.js +233 -0
  28. package/dist/hoi4_agent_tools/core/shared-request-capacity.js +43 -11
  29. package/dist/hoi4_agent_tools/core/source/cache.js +107 -0
  30. package/dist/hoi4_agent_tools/core/source/parser.js +11 -1
  31. package/dist/hoi4_agent_tools/core/transaction-execution.js +134 -0
  32. package/dist/hoi4_agent_tools/core/transactions.js +80 -27
  33. package/dist/hoi4_agent_tools/event/graph.js +214 -290
  34. package/dist/hoi4_agent_tools/event/helper-expansion.js +33 -0
  35. package/dist/hoi4_agent_tools/event/job-operations.js +51 -0
  36. package/dist/hoi4_agent_tools/event/operations.js +179 -0
  37. package/dist/hoi4_agent_tools/event/service.js +67 -15
  38. package/dist/hoi4_agent_tools/event/source-analysis.js +5 -4
  39. package/dist/hoi4_agent_tools/focus/job-operations.js +77 -0
  40. package/dist/hoi4_agent_tools/focus/tool-operations.js +669 -0
  41. package/dist/hoi4_agent_tools/focus/tool-support.js +123 -0
  42. package/dist/hoi4_agent_tools/gui/job-operations.js +81 -0
  43. package/dist/hoi4_agent_tools/gui/tool-operations.js +270 -0
  44. package/dist/hoi4_agent_tools/map/job-operations.js +76 -0
  45. package/dist/hoi4_agent_tools/map/tool-operations.js +493 -0
  46. package/dist/hoi4_agent_tools/mcp/prompts/probability.js +25 -0
  47. package/dist/hoi4_agent_tools/mcp/prompts/register-modern.js +29 -0
  48. package/dist/hoi4_agent_tools/mcp/resources/register-modern.js +34 -0
  49. package/dist/hoi4_agent_tools/mcp/resources/register.js +4 -150
  50. package/dist/hoi4_agent_tools/mcp/server/base-tools.js +3 -219
  51. package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +188 -0
  52. package/dist/hoi4_agent_tools/mcp/server/create.js +16 -6
  53. package/dist/hoi4_agent_tools/mcp/server/instructions.js +2 -0
  54. package/dist/hoi4_agent_tools/mcp/server/job-task-store.js +67 -0
  55. package/dist/hoi4_agent_tools/mcp/server/legacy-task-tools.js +20 -0
  56. package/dist/hoi4_agent_tools/mcp/server/modern-tasks.js +168 -0
  57. package/dist/hoi4_agent_tools/mcp/server/progress.js +17 -15
  58. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +43 -0
  59. package/dist/hoi4_agent_tools/mcp/server/result.js +2 -412
  60. package/dist/hoi4_agent_tools/mcp/server/task-request-context.js +9 -0
  61. package/dist/hoi4_agent_tools/mcp/server/task-tool-catalog.js +14 -0
  62. package/dist/hoi4_agent_tools/mcp/server/task-tool-definition.js +1 -0
  63. package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js +1 -123
  64. package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +38 -21
  65. package/dist/hoi4_agent_tools/mcp/tools/event.js +24 -315
  66. package/dist/hoi4_agent_tools/mcp/tools/focus.js +34 -1039
  67. package/dist/hoi4_agent_tools/mcp/tools/gui.js +19 -504
  68. package/dist/hoi4_agent_tools/mcp/tools/job.js +46 -0
  69. package/dist/hoi4_agent_tools/mcp/tools/map.js +21 -600
  70. package/dist/hoi4_agent_tools/mcp/tools/probability.js +39 -463
  71. package/dist/hoi4_agent_tools/mcp/tools/technology.js +40 -304
  72. package/dist/hoi4_agent_tools/mcp/transports/http.js +58 -1
  73. package/dist/hoi4_agent_tools/mcp/transports/modern-task-routing.js +109 -0
  74. package/dist/hoi4_agent_tools/mcp/transports/negotiated-stdio.js +121 -0
  75. package/dist/hoi4_agent_tools/probability/job-operations.js +58 -0
  76. package/dist/hoi4_agent_tools/probability/operations.js +183 -0
  77. package/dist/hoi4_agent_tools/probability/service.js +24 -3
  78. package/dist/hoi4_agent_tools/schemas/common.js +2 -0
  79. package/dist/hoi4_agent_tools/schemas/event.js +72 -1
  80. package/dist/hoi4_agent_tools/schemas/focus-requests.js +189 -0
  81. package/dist/hoi4_agent_tools/schemas/gui-requests.js +193 -0
  82. package/dist/hoi4_agent_tools/schemas/helper-expansion.js +58 -0
  83. package/dist/hoi4_agent_tools/schemas/map-requests.js +85 -0
  84. package/dist/hoi4_agent_tools/schemas/probability-requests.js +168 -0
  85. package/dist/hoi4_agent_tools/schemas/technology.js +90 -1
  86. package/dist/hoi4_agent_tools/schemas/transaction.js +1 -0
  87. package/dist/hoi4_agent_tools/technology/graph.js +83 -78
  88. package/dist/hoi4_agent_tools/technology/helper-expansion.js +33 -0
  89. package/dist/hoi4_agent_tools/technology/job-operations.js +60 -0
  90. package/dist/hoi4_agent_tools/technology/operations.js +125 -0
  91. package/dist/hoi4_agent_tools/technology/service.js +89 -15
  92. package/dist/hoi4_agent_tools/technology/source-analysis.js +2 -1
  93. package/dist/hoi4_agent_tools/version.js +1 -1
  94. package/docs/README.md +2 -0
  95. package/docs/development.md +2 -0
  96. package/docs/events.md +7 -2
  97. package/docs/helper-expansion.md +78 -0
  98. package/docs/http.md +2 -0
  99. package/docs/jobs.md +85 -0
  100. package/docs/technology.md +6 -2
  101. package/package.json +11 -5
  102. package/schemas/configuration.schema.json +1 -1
  103. package/schemas/continuous-focus-palette.schema.json +1 -1
  104. package/schemas/custom-weighted-pool.schema.json +1 -1
  105. package/schemas/event-feature-manifest.schema.json +1 -1
  106. package/schemas/focus-plan.schema.json +1 -1
  107. package/schemas/focus-planning-sidecar.schema.json +1 -1
  108. package/schemas/gui-animation-source.schema.json +1 -1
  109. package/schemas/gui-helper.schema.json +1 -1
  110. package/schemas/gui-scenario.schema.json +1 -1
  111. package/schemas/helper-expansion-request.schema.json +44 -0
  112. package/schemas/helper-expansion-summary.schema.json +70 -0
  113. package/schemas/map-operation.schema.json +1 -1
  114. package/schemas/operation-result.schema.json +1 -1
  115. package/schemas/probability-analysis-result.schema.json +1 -1
  116. package/schemas/probability-compare-input.schema.json +1 -1
  117. package/schemas/probability-evaluate-input.schema.json +1 -1
  118. package/schemas/probability-inspect-input.schema.json +1 -1
  119. package/schemas/probability-render-input.schema.json +1 -1
  120. package/schemas/probability-scenario-set.schema.json +1 -1
  121. package/schemas/probability-sequence-input.schema.json +1 -1
  122. package/schemas/probability-simulate-input.schema.json +1 -1
  123. package/schemas/probability-sweep-input.schema.json +1 -1
  124. package/server.json +2 -2
@@ -0,0 +1,123 @@
1
+ import { compareCodeUnits } from '../core/canonical.js';
2
+ import { ServiceError } from '../core/result.js';
3
+ import { parseClausewitz } from '../core/source/index.js';
4
+ import { enrichFocusPlanFromSidecar, focusPlanningSidecarPath, importContinuousFocusPalettes, importFocusTrees, linkContinuousFocusPalettes, parseFocusPlanningSidecar, } from './index.js';
5
+ function normalizedRoot(value) {
6
+ return value.replaceAll('\\', '/').replace(/^\.\//u, '').replace(/\/+$/u, '').toLowerCase();
7
+ }
8
+ export function underConfiguredRoot(relativePath, roots) {
9
+ const candidate = relativePath.replaceAll('\\', '/').toLowerCase();
10
+ return roots.some((root) => {
11
+ const normalized = normalizedRoot(root);
12
+ return candidate === normalized || candidate.startsWith(`${normalized}/`);
13
+ });
14
+ }
15
+ export function activeFocusFiles(snapshot, focusRoots) {
16
+ return snapshot.files.filter(({ relativePath, shadowedBy }) => shadowedBy === undefined &&
17
+ underConfiguredRoot(relativePath, focusRoots) &&
18
+ relativePath.toLowerCase().endsWith('.txt'));
19
+ }
20
+ export function activeContinuousFocusFiles(snapshot) {
21
+ return snapshot.files.filter(({ relativePath, shadowedBy }) => shadowedBy === undefined &&
22
+ relativePath.replaceAll('\\', '/').toLowerCase().startsWith('common/continuous_focus/') &&
23
+ relativePath.toLowerCase().endsWith('.txt'));
24
+ }
25
+ export function continuousPalettes(snapshot) {
26
+ const palettes = [];
27
+ const diagnostics = [];
28
+ for (const file of activeContinuousFocusFiles(snapshot)) {
29
+ const document = parseClausewitz(file.bytes, file.displayPath);
30
+ const imported = importContinuousFocusPalettes(document);
31
+ palettes.push(...imported.continuousFocusPalettes);
32
+ diagnostics.push(...imported.diagnostics);
33
+ }
34
+ return { palettes, diagnostics };
35
+ }
36
+ export function activeSidecar(snapshot, focusFile) {
37
+ const relativePath = focusPlanningSidecarPath(focusFile.relativePath).toLowerCase();
38
+ const file = snapshot.files.find((candidate) => candidate.shadowedBy === undefined && candidate.relativePath.toLowerCase() === relativePath);
39
+ return file === undefined ? undefined : { file, sidecar: parseFocusPlanningSidecar(file.bytes) };
40
+ }
41
+ export function importFocusFile(snapshot, file, palettes) {
42
+ const document = parseClausewitz(file.bytes, file.displayPath);
43
+ const imported = importFocusTrees(document, { references: referenceCatalog(snapshot) });
44
+ const sidecar = activeSidecar(snapshot, file);
45
+ imported.plans = imported.plans.map((plan) => {
46
+ const enriched = sidecar === undefined
47
+ ? { plan, diagnostics: [] }
48
+ : enrichFocusPlanFromSidecar(plan, sidecar.sidecar);
49
+ imported.diagnostics.push(...enriched.diagnostics);
50
+ return linkContinuousFocusPalettes(enriched.plan, palettes);
51
+ });
52
+ return imported;
53
+ }
54
+ export function selectedFocusFile(snapshot, focusRoots, relativePath) {
55
+ const file = activeFocusFiles(snapshot, focusRoots).find((candidate) => candidate.relativePath === relativePath);
56
+ if (file === undefined)
57
+ throw new ServiceError('FOCUS_SOURCE_NOT_FOUND', `Focus source was not found: ${relativePath}`);
58
+ return file;
59
+ }
60
+ export function selectedContinuousFocusFile(snapshot, relativePath) {
61
+ const file = activeContinuousFocusFiles(snapshot).find((candidate) => candidate.relativePath === relativePath);
62
+ if (file === undefined)
63
+ throw new ServiceError('CONTINUOUS_FOCUS_SOURCE_NOT_FOUND', `Continuous focus source was not found: ${relativePath}`);
64
+ return file;
65
+ }
66
+ export function selectPlan(result, treeId) {
67
+ if (result.plans.length === 0)
68
+ throw new ServiceError('FOCUS_TREE_NOT_FOUND', 'The source file contains no focus tree');
69
+ if (treeId === undefined) {
70
+ if (result.plans.length !== 1)
71
+ throw new ServiceError('FOCUS_TREE_ID_REQUIRED', 'The source file contains multiple focus trees; provide treeId', { treeIds: result.plans.map(({ id }) => id) });
72
+ return result.plans[0];
73
+ }
74
+ const plan = result.plans.find(({ id }) => id === treeId);
75
+ if (plan === undefined)
76
+ throw new ServiceError('FOCUS_TREE_NOT_FOUND', `Focus tree was not found: ${treeId}`);
77
+ return plan;
78
+ }
79
+ export function selectContinuousPlan(result, paletteId) {
80
+ if (result.continuousFocusPalettes.length === 0)
81
+ throw new ServiceError('CONTINUOUS_FOCUS_PALETTE_NOT_FOUND', 'The source file contains no continuous focus palette');
82
+ if (paletteId === undefined) {
83
+ if (result.continuousFocusPalettes.length !== 1)
84
+ throw new ServiceError('CONTINUOUS_FOCUS_PALETTE_ID_REQUIRED', 'The source file contains multiple continuous focus palettes; provide paletteId', { paletteIds: result.continuousFocusPalettes.map(({ id }) => id) });
85
+ return result.continuousFocusPalettes[0];
86
+ }
87
+ const plan = result.continuousFocusPalettes.find(({ id }) => id === paletteId);
88
+ if (plan === undefined)
89
+ throw new ServiceError('CONTINUOUS_FOCUS_PALETTE_NOT_FOUND', `Continuous focus palette was not found: ${paletteId}`);
90
+ return plan;
91
+ }
92
+ export function referenceCatalog(snapshot) {
93
+ const identifiers = (kind) => [
94
+ ...new Set(snapshot.index
95
+ .findAll(kind)
96
+ .filter(({ overridden }) => !overridden)
97
+ .map(({ id }) => id)),
98
+ ].sort((left, right) => compareCodeUnits(left, right));
99
+ return {
100
+ decision: identifiers('decision'),
101
+ decision_category: identifiers('decision_category'),
102
+ event: identifiers('event'),
103
+ idea: identifiers('idea'),
104
+ leader: identifiers('leader'),
105
+ formable: identifiers('formable'),
106
+ helper: identifiers('scripted_effect'),
107
+ };
108
+ }
109
+ export function validationFromDiagnostics(diagnostics) {
110
+ const blocking = diagnostics.filter(({ severity }) => severity === 'error' || severity === 'blocker');
111
+ return {
112
+ passed: blocking.length === 0,
113
+ checks: [
114
+ {
115
+ id: 'focus-diagnostics',
116
+ passed: blocking.length === 0,
117
+ message: blocking.length === 0
118
+ ? 'Focus source, references, and layout have no blocking diagnostics'
119
+ : `${blocking.length} blocking focus diagnostics`,
120
+ },
121
+ ],
122
+ };
123
+ }
@@ -0,0 +1,81 @@
1
+ import { z } from 'zod/v4';
2
+ import { IdleCacheLifetime } from '../core/idle-cache-lifetime.js';
3
+ import { errorResult, toolResult } from '../core/operation-result.js';
4
+ import { ServiceError } from '../core/result.js';
5
+ import { guiInspectRequestSchema, guiRenderRequestSchema, guiRewriteRequestSchema, } from '../schemas/gui-requests.js';
6
+ import { ScriptedGuiStudio } from './studio.js';
7
+ import { inspectGui, completeGuiRewrite, normalizeGuiInspectRequest, normalizeGuiRenderRequest, normalizeGuiRewriteRequest, prepareGuiRewrite, renderGui, } from './tool-operations.js';
8
+ const wireResult = z.record(z.string(), z.json());
9
+ /** Registers read-only GUI operations without invoking transport handlers. */
10
+ export function registerGuiJobs(operations, engine) {
11
+ const studio = new ScriptedGuiStudio(engine);
12
+ const lifetime = new IdleCacheLifetime(() => studio.clearCaches());
13
+ function register(name, schema, normalize, execute) {
14
+ operations.registerRead(name, schema, async (input, context) => {
15
+ const release = lifetime.begin();
16
+ try {
17
+ if (input.workspaceId !== 'current' && input.workspaceId !== context.workspaceId)
18
+ throw new ServiceError('JOB_ARGUMENT_SCOPE_MISMATCH', 'The request workspace does not match the authorized job workspace');
19
+ const result = await execute(studio, normalize(input), {
20
+ workspaceId: context.workspaceId,
21
+ ...(context.principal === undefined ? {} : { principal: context.principal }),
22
+ signal: context.signal,
23
+ progress: (completed, total, message) => context.progress({ completed, total, message }),
24
+ });
25
+ const wire = wireResult.parse(JSON.parse(JSON.stringify(toolResult(result))));
26
+ const revision = result.data !== null &&
27
+ typeof result.data === 'object' &&
28
+ 'sourceRevision' in result.data &&
29
+ typeof result.data.sourceRevision === 'string'
30
+ ? result.data.sourceRevision
31
+ : result.data !== null &&
32
+ typeof result.data === 'object' &&
33
+ 'sharedRevision' in result.data &&
34
+ typeof result.data.sharedRevision === 'string'
35
+ ? result.data.sharedRevision
36
+ : undefined;
37
+ if (revision !== undefined)
38
+ await context.stageResult(wire, { sourceRevision: revision });
39
+ return wire;
40
+ }
41
+ catch (error) {
42
+ if (context.signal.aborted)
43
+ throw error;
44
+ return wireResult.parse(JSON.parse(JSON.stringify(errorResult(error, context.workspaceId))));
45
+ }
46
+ finally {
47
+ release();
48
+ }
49
+ });
50
+ }
51
+ register('hoi4.gui_inspect', guiInspectRequestSchema, normalizeGuiInspectRequest, (studio, input, context) => inspectGui(engine, studio, input, context));
52
+ register('hoi4.gui_render', guiRenderRequestSchema, normalizeGuiRenderRequest, renderGui);
53
+ operations.registerWrite('hoi4.gui_rewrite', guiRewriteRequestSchema, async (input, context) => {
54
+ if (input.workspaceId !== 'current' && input.workspaceId !== context.workspaceId)
55
+ throw new ServiceError('JOB_ARGUMENT_SCOPE_MISMATCH', 'The request workspace does not match the authorized job workspace');
56
+ const release = lifetime.begin();
57
+ try {
58
+ return await prepareGuiRewrite(studio, normalizeGuiRewriteRequest(input), {
59
+ workspaceId: context.workspaceId,
60
+ ...(context.principal === undefined ? {} : { principal: context.principal }),
61
+ signal: context.signal,
62
+ progress: (completed, total, message) => context.progress({ completed, total, message }),
63
+ });
64
+ }
65
+ finally {
66
+ release();
67
+ }
68
+ }, async (_input, execution, _reconciled, recipe, context) => {
69
+ const result = completeGuiRewrite(context.workspaceId, execution, recipe);
70
+ await context.progress({
71
+ completed: 3,
72
+ total: 3,
73
+ message: execution.outcome === 'applied'
74
+ ? 'GUI rewrite complete'
75
+ : execution.outcome === 'unchanged'
76
+ ? 'GUI content already satisfied the rewrite'
77
+ : 'GUI rewrite blocked',
78
+ });
79
+ return wireResult.parse(JSON.parse(JSON.stringify(toolResult(result))));
80
+ });
81
+ }
@@ -0,0 +1,270 @@
1
+ import { z } from 'zod/v4';
2
+ import { boundedSourceHashEvidence, publicArtifactLink } from '../core/artifacts.js';
3
+ import { canonicalJson, compareCodeUnits, hashCanonical } from '../core/canonical.js';
4
+ import { autonomousResultArtifacts, } from '../core/transaction-execution.js';
5
+ import { emptyServiceResult } from '../core/result.js';
6
+ import { setInlineFilesScanned } from '../core/operation-result.js';
7
+ import { PACKAGE_VERSION } from '../version.js';
8
+ import { compileGuiHelpers } from './helpers.js';
9
+ import { encodeGuiInspectionArtifact, projectGuiGraphForArtifact } from './inspection-artifact.js';
10
+ const guiRewriteRecipeSchema = z
11
+ .object({
12
+ mode: z.enum(['source', 'helpers', 'patches']),
13
+ filesScanned: z.array(z.string().min(1).max(4096)),
14
+ compilation: z
15
+ .object({
16
+ nodeCount: z.number().int().nonnegative(),
17
+ templateInstanceCount: z.number().int().nonnegative(),
18
+ rawEscapeCount: z.number().int().nonnegative(),
19
+ })
20
+ .strict()
21
+ .optional(),
22
+ })
23
+ .strict();
24
+ function validationSummary(validation) {
25
+ return {
26
+ passed: validation.checks.every(({ passed }) => passed) &&
27
+ !validation.diagnostics.some(({ severity }) => severity === 'error' || severity === 'blocker'),
28
+ checks: validation.checks,
29
+ };
30
+ }
31
+ export function normalizeGuiInspectRequest(input) {
32
+ return input;
33
+ }
34
+ export function normalizeGuiRenderRequest(input) {
35
+ return input;
36
+ }
37
+ export function normalizeGuiRewriteRequest(input) {
38
+ return input;
39
+ }
40
+ export async function prepareGuiRewrite(studio, input, context) {
41
+ await context.progress(0, 3, 'Validating the requested GUI result');
42
+ const compilation = input.mode === 'helpers' ? compileGuiHelpers(input.helper) : undefined;
43
+ const transaction = await studio.planSource({
44
+ workspaceId: context.workspaceId,
45
+ relativePath: input.relativePath,
46
+ ...(input.mode === 'patches'
47
+ ? { expectedSourceHash: input.expectedSourceHash, patches: input.patches }
48
+ : {
49
+ source: input.mode === 'source' ? input.source : compilation.source,
50
+ ...(input.additionalFiles === undefined
51
+ ? {}
52
+ : { additionalFiles: input.additionalFiles }),
53
+ }),
54
+ windowName: input.windowName,
55
+ scenario: input.scenario,
56
+ ...(context.principal === undefined ? {} : { principal: context.principal }),
57
+ ...(context.signal === undefined ? {} : { signal: context.signal }),
58
+ });
59
+ await context.progress(2, 3, 'GUI rewrite prepared for journaled application');
60
+ return {
61
+ transaction,
62
+ recipe: guiRewriteRecipeSchema.parse({
63
+ mode: input.mode,
64
+ filesScanned: [
65
+ input.relativePath,
66
+ ...(input.additionalFiles ?? []).map(({ relativePath }) => relativePath),
67
+ ].sort(compareCodeUnits),
68
+ ...(compilation === undefined
69
+ ? {}
70
+ : {
71
+ compilation: {
72
+ nodeCount: compilation.nodeCount,
73
+ templateInstanceCount: compilation.templateInstanceCount,
74
+ rawEscapeCount: compilation.rawEscapeCount,
75
+ },
76
+ }),
77
+ }),
78
+ };
79
+ }
80
+ export function completeGuiRewrite(workspaceId, execution, recipe) {
81
+ const parsed = guiRewriteRecipeSchema.parse(recipe);
82
+ const transaction = execution.transaction;
83
+ const result = emptyServiceResult(workspaceId, {
84
+ mode: parsed.mode,
85
+ execution: execution.outcome,
86
+ fileCount: transaction.files.length,
87
+ artifactCount: transaction.artifacts.length,
88
+ ...(parsed.compilation ?? {}),
89
+ });
90
+ result.status = transaction.validation.passed ? 'ok' : 'blocked';
91
+ result.code =
92
+ execution.outcome === 'applied'
93
+ ? 'GUI_CHANGES_APPLIED'
94
+ : execution.outcome === 'unchanged'
95
+ ? 'GUI_CHANGES_UNCHANGED'
96
+ : 'GUI_CHANGES_BLOCKED';
97
+ setInlineFilesScanned(result, parsed.filesScanned);
98
+ result.proposedFiles = transaction.files.slice(0, 100).map(({ relativePath }) => relativePath);
99
+ result.changedFiles = transaction.appliedFiles.slice(0, 100);
100
+ result.diagnostics = transaction.diagnostics.slice(0, 100);
101
+ result.artifacts = autonomousResultArtifacts(execution);
102
+ result.validation = transaction.validation;
103
+ result.blockers = transaction.diagnostics
104
+ .filter(({ severity }) => severity === 'error' || severity === 'blocker')
105
+ .map(({ code, message, details }) => ({
106
+ code,
107
+ message,
108
+ ...(details === undefined ? {} : { details }),
109
+ }));
110
+ return result;
111
+ }
112
+ export async function inspectGui(engine, studio, input, context) {
113
+ await context.progress(0, 3, 'Building GUI source graph');
114
+ const inspected = input.windowName === undefined || input.scenario === undefined
115
+ ? {
116
+ graph: (await studio.scan(context.workspaceId, context.principal, context.signal)).graph,
117
+ linted: undefined,
118
+ }
119
+ : await (async () => {
120
+ const linted = await studio.lint({
121
+ workspaceId: context.workspaceId,
122
+ windowName: input.windowName,
123
+ scenario: input.scenario,
124
+ ...(input.relatedScenarios === undefined
125
+ ? {}
126
+ : { relatedScenarios: input.relatedScenarios }),
127
+ generatedScenarios: input.generatedScenarios ?? {},
128
+ ...(context.principal === undefined ? {} : { principal: context.principal }),
129
+ ...(context.signal === undefined ? {} : { signal: context.signal }),
130
+ });
131
+ return { graph: linted.graph, linted };
132
+ })();
133
+ const { graph, linted } = inspected;
134
+ const sharedRevision = hashCanonical(graph.sourceHashes);
135
+ const workspace = engine.resolver.get(context.workspaceId, context.principal);
136
+ const sourceEvidence = boundedSourceHashEvidence(graph.sourceHashes);
137
+ const artifactGraph = projectGuiGraphForArtifact(graph, linted?.scene.elements.map(({ sourceId }) => sourceId));
138
+ const inspectionName = `gui-inspect.${sharedRevision.slice(0, 16)}.json`;
139
+ const inspectionJson = `${canonicalJson({
140
+ schemaVersion: 1,
141
+ sharedRevision,
142
+ offline: true,
143
+ graph: artifactGraph.graph,
144
+ ...(artifactGraph.projection === undefined
145
+ ? {}
146
+ : { graphProjection: artifactGraph.projection }),
147
+ ...(linted === undefined
148
+ ? {}
149
+ : {
150
+ scenario: linted.scene.scenario,
151
+ fidelity: linted.scene.fidelity,
152
+ validation: linted.validation,
153
+ }),
154
+ })}\n`;
155
+ const encodedInspection = await encodeGuiInspectionArtifact(inspectionName, inspectionJson);
156
+ const artifact = await engine.artifacts.putChunked(workspace, encodedInspection.name, encodedInspection.mimeType, encodedInspection.content, {
157
+ kind: 'gui-inspect',
158
+ toolVersion: PACKAGE_VERSION,
159
+ schemaVersion: 'gui-inspect.v1',
160
+ sourceHashes: sourceEvidence.sourceHashes,
161
+ metadata: {
162
+ sharedRevision,
163
+ offline: true,
164
+ complete: graph.complete,
165
+ skippedSourceCount: graph.skippedSourceCount,
166
+ sourceHashInventory: sourceEvidence.inventory,
167
+ compressed: encodedInspection.compressed,
168
+ uncompressedBytes: encodedInspection.uncompressedBytes,
169
+ graphProjection: artifactGraph.projection?.mode ?? 'full',
170
+ },
171
+ }, encodedInspection.compressed
172
+ ? 'Gzip-compressed scripted GUI graph, diagnostics, and optional scenario fidelity inspection'
173
+ : 'Scripted GUI graph, diagnostics, and optional scenario fidelity inspection', context.signal);
174
+ const diagnostics = linted === undefined ? graph.diagnostics : linted.validation.diagnostics;
175
+ const result = emptyServiceResult(context.workspaceId, {
176
+ sharedRevision,
177
+ complete: graph.complete,
178
+ skippedSourceCount: graph.skippedSourceCount,
179
+ skippedSources: graph.skippedSources,
180
+ nodes: graph.nodes.length,
181
+ edges: graph.edges.length,
182
+ elements: graph.elements.length,
183
+ sprites: graph.sprites.length,
184
+ fonts: graph.fonts.length,
185
+ scriptedGuis: graph.scriptedGuis.length,
186
+ ...(linted === undefined
187
+ ? {}
188
+ : {
189
+ windowName: input.windowName,
190
+ scenarioId: linted.scene.scenario.id,
191
+ inspectedElementCount: linted.scene.elements.length,
192
+ fidelityCounts: Object.fromEntries(Object.entries(linted.scene.fidelity).map(([key, values]) => [key, values.length])),
193
+ }),
194
+ });
195
+ result.code = graph.complete ? 'GUI_INSPECTED' : 'GUI_INSPECTED_PARTIAL';
196
+ setInlineFilesScanned(result, graph.filesScanned);
197
+ result.diagnostics = diagnostics.slice(0, 100);
198
+ result.artifacts = [publicArtifactLink(artifact)];
199
+ result.validation =
200
+ linted === undefined
201
+ ? {
202
+ passed: !diagnostics.some(({ severity }) => severity === 'error' || severity === 'blocker'),
203
+ checks: [
204
+ {
205
+ id: 'gui-inspection',
206
+ passed: true,
207
+ message: graph.complete
208
+ ? `${graph.nodes.length} GUI graph nodes connected from a complete inventory`
209
+ : `${graph.nodes.length} GUI graph nodes connected; ${graph.skippedSourceCount} over-limit source(s) were skipped`,
210
+ },
211
+ ],
212
+ }
213
+ : validationSummary({ diagnostics, checks: linted.validation.checks });
214
+ await context.progress(3, 3, 'GUI inspection complete');
215
+ return result;
216
+ }
217
+ export async function renderGui(studio, input, context) {
218
+ await context.progress(0, 4, 'Building GUI source graph');
219
+ const rendered = await studio.renderAndStore({
220
+ workspaceId: context.workspaceId,
221
+ windowName: input.windowName,
222
+ scenario: input.scenario,
223
+ ...(input.states === undefined ? {} : { states: input.states }),
224
+ ...(input.resolutions === undefined
225
+ ? {}
226
+ : {
227
+ resolutions: input.resolutions.map(({ width, height, uiScale }) => ({
228
+ width,
229
+ height,
230
+ ...(uiScale === undefined ? {} : { uiScale }),
231
+ })),
232
+ }),
233
+ ...(input.relatedScenarios === undefined ? {} : { relatedScenarios: input.relatedScenarios }),
234
+ generatedScenarios: input.generatedScenarios ?? {},
235
+ ...(input.comparisonScenario === undefined
236
+ ? {}
237
+ : { comparisonScenario: input.comparisonScenario }),
238
+ ...(context.principal === undefined ? {} : { principal: context.principal }),
239
+ ...(context.signal === undefined ? {} : { signal: context.signal }),
240
+ });
241
+ await context.progress(3, 4, 'Preparing GUI result');
242
+ const diagnostics = rendered.validation.diagnostics;
243
+ const result = emptyServiceResult(context.workspaceId, {
244
+ windowName: input.windowName,
245
+ scenarioId: rendered.render.scene.scenario.id,
246
+ sourceRevision: rendered.render.scene.sourceRevision,
247
+ variantCount: rendered.render.images.length,
248
+ variants: rendered.render.images.slice(0, 64).map(({ variant, width, height }) => ({
249
+ variant,
250
+ width,
251
+ height,
252
+ })),
253
+ stateCount: rendered.stateScenes.length,
254
+ scenarioCount: rendered.scenarioScenes.length,
255
+ resolutionCount: rendered.resolutionScenes.length,
256
+ comparison: {
257
+ changedPixels: rendered.comparison.changedPixels,
258
+ changedRatio: rendered.comparison.changedRatio,
259
+ },
260
+ fidelityCounts: Object.fromEntries(Object.entries(rendered.render.fidelity).map(([key, values]) => [key, values.length])),
261
+ offlineRepresentation: true,
262
+ });
263
+ result.code = 'GUI_RENDERED';
264
+ setInlineFilesScanned(result, rendered.filesScanned);
265
+ result.diagnostics = diagnostics.slice(0, 100);
266
+ result.artifacts = rendered.artifacts.map(publicArtifactLink);
267
+ result.validation = validationSummary({ diagnostics, checks: rendered.validation.checks });
268
+ await context.progress(4, 4, 'GUI render complete');
269
+ return result;
270
+ }
@@ -0,0 +1,76 @@
1
+ import { z } from 'zod/v4';
2
+ import { IdleCacheLifetime } from '../core/idle-cache-lifetime.js';
3
+ import { errorResult, toolResult } from '../core/operation-result.js';
4
+ import { ServiceError } from '../core/result.js';
5
+ import { mapInspectRequestSchema, mapRenderRequestSchema, mapRewriteRequestSchema, } from '../schemas/map-requests.js';
6
+ import { AgentNudger } from './service.js';
7
+ import { inspectMap, completeMapRewrite, normalizeMapInspectRequest, normalizeMapRenderRequest, normalizeMapRewriteRequest, prepareMapRewrite, renderMapView, } from './tool-operations.js';
8
+ const wireResult = z.record(z.string(), z.json());
9
+ /** Registers read-only map operations without invoking transport handlers. */
10
+ export function registerMapJobs(operations, engine) {
11
+ const nudger = new AgentNudger(engine);
12
+ const lifetime = new IdleCacheLifetime(() => engine.releaseScanCaches());
13
+ function register(name, schema, normalize, execute) {
14
+ operations.registerRead(name, schema, async (input, context) => {
15
+ const release = lifetime.begin();
16
+ try {
17
+ if (input.workspaceId !== 'current' && input.workspaceId !== context.workspaceId)
18
+ throw new ServiceError('JOB_ARGUMENT_SCOPE_MISMATCH', 'The request workspace does not match the authorized job workspace');
19
+ const result = await execute(nudger, normalize(input), {
20
+ workspaceId: context.workspaceId,
21
+ ...(context.principal === undefined ? {} : { principal: context.principal }),
22
+ signal: context.signal,
23
+ progress: (completed, total, message) => context.progress({ completed, total, message }),
24
+ });
25
+ const wire = wireResult.parse(JSON.parse(JSON.stringify(toolResult(result))));
26
+ const revision = result.data !== null &&
27
+ typeof result.data === 'object' &&
28
+ 'revision' in result.data &&
29
+ typeof result.data.revision === 'string'
30
+ ? result.data.revision
31
+ : undefined;
32
+ if (revision !== undefined)
33
+ await context.stageResult(wire, { sourceRevision: revision });
34
+ return wire;
35
+ }
36
+ catch (error) {
37
+ if (context.signal.aborted)
38
+ throw error;
39
+ return wireResult.parse(JSON.parse(JSON.stringify(errorResult(error, context.workspaceId))));
40
+ }
41
+ finally {
42
+ release();
43
+ }
44
+ });
45
+ }
46
+ register('hoi4.map_inspect', mapInspectRequestSchema, normalizeMapInspectRequest, (nudger, input, context) => inspectMap(engine, nudger, input, context));
47
+ register('hoi4.map_render', mapRenderRequestSchema, normalizeMapRenderRequest, renderMapView);
48
+ operations.registerWrite('hoi4.map_rewrite', mapRewriteRequestSchema, async (input, context) => {
49
+ if (input.workspaceId !== 'current' && input.workspaceId !== context.workspaceId)
50
+ throw new ServiceError('JOB_ARGUMENT_SCOPE_MISMATCH', 'The request workspace does not match the authorized job workspace');
51
+ const release = lifetime.begin();
52
+ try {
53
+ return await prepareMapRewrite(nudger, normalizeMapRewriteRequest(input), {
54
+ workspaceId: context.workspaceId,
55
+ ...(context.principal === undefined ? {} : { principal: context.principal }),
56
+ signal: context.signal,
57
+ progress: (completed, total, message) => context.progress({ completed, total, message }),
58
+ });
59
+ }
60
+ finally {
61
+ release();
62
+ }
63
+ }, async (_input, execution, _reconciled, recipe, context) => {
64
+ const result = completeMapRewrite(context.workspaceId, execution, recipe);
65
+ await context.progress({
66
+ completed: 5,
67
+ total: 5,
68
+ message: execution.outcome === 'applied'
69
+ ? 'Map rewrite complete'
70
+ : execution.outcome === 'unchanged'
71
+ ? 'Map content already satisfied the operations'
72
+ : 'Map rewrite blocked',
73
+ });
74
+ return wireResult.parse(JSON.parse(JSON.stringify(toolResult(result))));
75
+ });
76
+ }