@neuroverseos/nv-sim 0.1.0 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +324 -44
- package/dist/assets/index-DHKd4rcV.js +338 -0
- package/dist/assets/index-SyyA3z3U.css +1 -0
- package/dist/assets/mirotir-logo-DUexumBH.svg +185 -0
- package/dist/assets/reportEngine-BfteK4MN.js +1 -0
- package/dist/assets/swarmSimulation-DHDqjfMa.js +1 -0
- package/dist/engine/aiProvider.js +256 -0
- package/dist/engine/cli.js +334 -299
- package/dist/engine/governance.js +8 -3
- package/dist/engine/governedSimulation.js +38 -12
- package/dist/engine/index.js +16 -1
- package/dist/engine/liveAdapter.js +342 -0
- package/dist/engine/liveVisualizer.js +1857 -0
- package/dist/engine/narrativeInjection.js +305 -0
- package/dist/engine/reasoningEngine.js +57 -3
- package/dist/engine/reportEngine.js +97 -0
- package/dist/engine/scenarioLibrary.js +231 -0
- package/dist/engine/worldComparison.js +194 -0
- package/dist/favicon.ico +0 -0
- package/dist/index.html +23 -0
- package/dist/placeholder.svg +1 -0
- package/dist/robots.txt +14 -0
- package/package.json +13 -12
- package/variants/.gitkeep +0 -0
|
@@ -110,10 +110,15 @@ function runFullGovernanceChecks(request, worldLite) {
|
|
|
110
110
|
// Convert verdict to constitutional checks
|
|
111
111
|
const nvChecks = (0, worldBridge_1.verdictToConstitutionalChecks)(guardVerdict);
|
|
112
112
|
results.push(...nvChecks);
|
|
113
|
-
// If guard blocks,
|
|
113
|
+
// If guard blocks, note the intervention but continue in advisory mode
|
|
114
|
+
// (Reasoning continues so user can see what governance changed)
|
|
114
115
|
if (guardVerdict.status === "BLOCK") {
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
results.push({
|
|
117
|
+
rule_id: "NV-BLOCK",
|
|
118
|
+
rule: "NeuroverseOS guard issued BLOCK — governance intervention active",
|
|
119
|
+
passed: false,
|
|
120
|
+
detail: `Guard blocked: ${guardVerdict.reason ?? "Action blocked by guard"}. Continuing in governed advisory mode.`,
|
|
121
|
+
});
|
|
117
122
|
}
|
|
118
123
|
// If guard pauses, note it but continue (reasoning is advisory)
|
|
119
124
|
if (guardVerdict.status === "PAUSE") {
|
|
@@ -17,6 +17,7 @@ exports.TRADING_DEMO = void 0;
|
|
|
17
17
|
exports.runGovernedComparison = runGovernedComparison;
|
|
18
18
|
const swarmSimulation_1 = require("./swarmSimulation");
|
|
19
19
|
const worldBridge_1 = require("./worldBridge");
|
|
20
|
+
const narrativeInjection_1 = require("./narrativeInjection");
|
|
20
21
|
// ============================================
|
|
21
22
|
// GOVERNED REACTION MODEL
|
|
22
23
|
// ============================================
|
|
@@ -39,11 +40,11 @@ function applyGovernanceToReactions(reactions, invariants, gates, roundIndex, cu
|
|
|
39
40
|
// Cap extreme negative impacts (e.g., "maintain liquidity floor")
|
|
40
41
|
if (desc.includes("floor") || desc.includes("maintain") || desc.includes("minimum")) {
|
|
41
42
|
for (const agent of governed) {
|
|
42
|
-
if (agent.impact < -0.
|
|
43
|
+
if (agent.impact < -0.35) {
|
|
43
44
|
const original = agent.impact;
|
|
44
|
-
agent.impact = Math.max(agent.impact, -0.
|
|
45
|
+
agent.impact = Math.max(agent.impact, -0.3);
|
|
45
46
|
if (original !== agent.impact) {
|
|
46
|
-
interventions.push(`[
|
|
47
|
+
interventions.push(`[BLOCK] ${agent.stakeholder_id}: Liquidity floor violated — impact capped ${original.toFixed(2)} → ${agent.impact.toFixed(2)} (${inv.description})`);
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
}
|
|
@@ -51,11 +52,11 @@ function applyGovernanceToReactions(reactions, invariants, gates, roundIndex, cu
|
|
|
51
52
|
// Limit excessive positive speculation (e.g., "no leverage > 5x")
|
|
52
53
|
if (desc.includes("leverage") || desc.includes("limit") || desc.includes("cap") || desc.includes("restrict")) {
|
|
53
54
|
for (const agent of governed) {
|
|
54
|
-
if (agent.impact > 0.
|
|
55
|
+
if (agent.impact > 0.45) {
|
|
55
56
|
const original = agent.impact;
|
|
56
|
-
agent.impact = Math.min(agent.impact, 0.
|
|
57
|
+
agent.impact = Math.min(agent.impact, 0.4);
|
|
57
58
|
if (original !== agent.impact) {
|
|
58
|
-
interventions.push(`[
|
|
59
|
+
interventions.push(`[BLOCK] ${agent.stakeholder_id}: Leverage limit exceeded — position capped ${original.toFixed(2)} → ${agent.impact.toFixed(2)} (${inv.description})`);
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
}
|
|
@@ -65,10 +66,10 @@ function applyGovernanceToReactions(reactions, invariants, gates, roundIndex, cu
|
|
|
65
66
|
// Pull extreme reactions toward the mean
|
|
66
67
|
const avgImpact = governed.reduce((s, a) => s + a.impact, 0) / governed.length;
|
|
67
68
|
for (const agent of governed) {
|
|
68
|
-
if (Math.abs(agent.impact - avgImpact) > 0.
|
|
69
|
+
if (Math.abs(agent.impact - avgImpact) > 0.25) {
|
|
69
70
|
const original = agent.impact;
|
|
70
71
|
agent.impact = agent.impact * 0.7 + avgImpact * 0.3;
|
|
71
|
-
interventions.push(`[
|
|
72
|
+
interventions.push(`[PAUSE] Rebalanced ${agent.stakeholder_id}: drift ${original.toFixed(2)} → ${agent.impact.toFixed(2)} (${inv.description})`);
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
}
|
|
@@ -92,7 +93,7 @@ function applyGovernanceToReactions(reactions, invariants, gates, roundIndex, cu
|
|
|
92
93
|
if (gate.severity === "critical") {
|
|
93
94
|
// Critical gates act as circuit breakers — compress all impacts toward zero
|
|
94
95
|
const avgAbsImpact = governed.reduce((s, a) => s + Math.abs(a.impact), 0) / governed.length;
|
|
95
|
-
if (avgAbsImpact > 0.
|
|
96
|
+
if (avgAbsImpact > 0.3 || cumulativeAvgImpact < -0.15) {
|
|
96
97
|
const dampingFactor = 0.6;
|
|
97
98
|
for (const agent of governed) {
|
|
98
99
|
const original = agent.impact;
|
|
@@ -131,13 +132,14 @@ function applyGovernanceToReactions(reactions, invariants, gates, roundIndex, cu
|
|
|
131
132
|
* Same agents, same scenario, same reaction model —
|
|
132
133
|
* but world rules reshape the outcomes.
|
|
133
134
|
*/
|
|
134
|
-
async function runGovernedSwarmSimulation(scenario, stakeholders, paths, config, worldDef, nvWorld, request) {
|
|
135
|
+
async function runGovernedSwarmSimulation(scenario, stakeholders, paths, config, worldDef, nvWorld, request, narrativeEvents) {
|
|
135
136
|
// First run the unmodified simulation to get raw reactions
|
|
136
137
|
const rawResult = await (0, swarmSimulation_1.runSwarmSimulation)(scenario, stakeholders, paths, config);
|
|
137
138
|
// Now re-run applying governance to each round
|
|
138
139
|
const governedRounds = [];
|
|
139
140
|
const allInterventions = [];
|
|
140
141
|
const allGuardVerdicts = [];
|
|
142
|
+
const allNarrativeImpacts = [];
|
|
141
143
|
let cumulativeAvgImpact = 0;
|
|
142
144
|
for (const round of rawResult.rounds) {
|
|
143
145
|
// --- Real governance engine: evaluate each agent's action ---
|
|
@@ -159,6 +161,14 @@ async function runGovernedSwarmSimulation(scenario, stakeholders, paths, config,
|
|
|
159
161
|
}
|
|
160
162
|
}
|
|
161
163
|
}
|
|
164
|
+
// --- Narrative injection: information shocks ---
|
|
165
|
+
if (narrativeEvents && narrativeEvents.length > 0) {
|
|
166
|
+
const roundEvents = (0, narrativeInjection_1.getEventsForRound)(narrativeEvents, round.round);
|
|
167
|
+
for (const event of roundEvents) {
|
|
168
|
+
const impact = (0, narrativeInjection_1.injectNarrative)(round.reactions, event, stakeholders);
|
|
169
|
+
allNarrativeImpacts.push(impact);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
162
172
|
// --- Heuristic governance: invariant/gate enforcement (always applied) ---
|
|
163
173
|
const { reactions: governedReactions, interventions } = applyGovernanceToReactions(round.reactions, worldDef.invariants, worldDef.gates ?? [], round.round, cumulativeAvgImpact);
|
|
164
174
|
// Recompute emergent dynamics with governed reactions
|
|
@@ -184,6 +194,7 @@ async function runGovernedSwarmSimulation(scenario, stakeholders, paths, config,
|
|
|
184
194
|
},
|
|
185
195
|
allInterventions,
|
|
186
196
|
guardVerdicts: allGuardVerdicts,
|
|
197
|
+
narrativeImpacts: allNarrativeImpacts,
|
|
187
198
|
};
|
|
188
199
|
}
|
|
189
200
|
function detectGovernedDynamics(reactions, interventions) {
|
|
@@ -323,7 +334,7 @@ function buildComparisonNarrative(baseline, governed, worldThesis) {
|
|
|
323
334
|
* 3. Compare metrics
|
|
324
335
|
* 4. Generate narrative
|
|
325
336
|
*/
|
|
326
|
-
async function runGovernedComparison(request, worldLite, paths) {
|
|
337
|
+
async function runGovernedComparison(request, worldLite, paths, narrativeEvents) {
|
|
327
338
|
const stakeholders = (request.stakeholders ?? []).map((s) => typeof s === "string" ? { id: s, disposition: "unknown" } : s);
|
|
328
339
|
const swarmConfig = request.swarm ?? {
|
|
329
340
|
enabled: true,
|
|
@@ -336,7 +347,7 @@ async function runGovernedComparison(request, worldLite, paths) {
|
|
|
336
347
|
const baselineSwarm = await (0, swarmSimulation_1.runSwarmSimulation)(request.scenario, stakeholders, paths, swarmConfig);
|
|
337
348
|
const baselineMetrics = computeMetrics(baselineSwarm);
|
|
338
349
|
// Step 2: Governed — same scenario, world rules applied (real engine + heuristics)
|
|
339
|
-
const { swarm: governedSwarm, guardVerdicts } = await runGovernedSwarmSimulation(request.scenario, stakeholders, paths, swarmConfig, worldLite, nvWorld, request);
|
|
350
|
+
const { swarm: governedSwarm, guardVerdicts, narrativeImpacts: collectedNarrativeImpacts } = await runGovernedSwarmSimulation(request.scenario, stakeholders, paths, swarmConfig, worldLite, nvWorld, request, narrativeEvents);
|
|
340
351
|
const governedMetrics = computeMetrics(governedSwarm);
|
|
341
352
|
// Step 3: Build world simulation (deterministic state evolution via real engine)
|
|
342
353
|
let worldSimulation;
|
|
@@ -417,6 +428,7 @@ async function runGovernedComparison(request, worldLite, paths) {
|
|
|
417
428
|
narrative,
|
|
418
429
|
},
|
|
419
430
|
governanceStats,
|
|
431
|
+
narrativeImpacts: collectedNarrativeImpacts.length > 0 ? collectedNarrativeImpacts : undefined,
|
|
420
432
|
};
|
|
421
433
|
}
|
|
422
434
|
// ============================================
|
|
@@ -478,6 +490,20 @@ exports.TRADING_DEMO = {
|
|
|
478
490
|
{ id: "GATE-002", label: "Contagion Warning", condition: "contagion_spread == systemic", severity: "critical" },
|
|
479
491
|
{ id: "GATE-003", label: "Leverage Warning", condition: "leverage_ratio > 8", severity: "warning" },
|
|
480
492
|
],
|
|
493
|
+
ai_roles: [
|
|
494
|
+
{
|
|
495
|
+
id: "ai_translator",
|
|
496
|
+
type: "ai",
|
|
497
|
+
permissions: ["translate_input"],
|
|
498
|
+
constraints: ["must_output_valid_schema", "no_invention_of_events", "confidence_must_be_provided"],
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
id: "ai_analyst",
|
|
502
|
+
type: "ai",
|
|
503
|
+
permissions: ["generate_report", "summarize_trace"],
|
|
504
|
+
constraints: ["must_reference_trace", "must_include_blocked_actions", "must_include_metrics", "no_unverifiable_claims"],
|
|
505
|
+
},
|
|
506
|
+
],
|
|
481
507
|
},
|
|
482
508
|
paths: [
|
|
483
509
|
{
|
package/dist/engine/index.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* Mirotir helps you decide WHAT TO DO → the brain
|
|
22
22
|
*/
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
-
exports.parseReasonRequestBody = exports.handleRunPreset = exports.handleGetPreset = exports.handleListPresets = exports.handleHealthCheck = exports.handleCreateCapsule = exports.handleReasonFromCapsule = exports.handleReasonRequest = exports.TRADING_DEMO = exports.runGovernedComparison = exports.analyzeMiroFishSimulation = exports.DEFAULT_MIROFISH_CONFIG = exports.buildMiroFishRequest = exports.miroFishResultToSwarmResult = exports.stakeholdersToMiroFishAgents = exports.runUnifiedSimulation = exports.getMiroFishClient = exports.configureMiroFish = exports.MiroFishError = exports.MiroFishClient = exports.runSwarmSimulation = exports.initNeuroverseModule = exports.validationToEnforcedConstraints = exports.simulationToGovernanceSignals = exports.verdictToConstitutionalChecks = exports.validateScenarioWorld = exports.runWorldSimulation = exports.evaluateScenarioGuard = exports.buildWorldFromScenario = exports.runFullGovernanceChecks = exports.runGovernanceChecks = exports.processReasonRequest = exports.SCENARIO_TEMPLATES = exports.getPresetCapsule = exports.extractCapsuleFromUrl = exports.buildShareableUrl = exports.decodeCapsule = exports.encodeCapsule = exports.capsuleToReasonRequest = exports.createCapsule = exports.runGoalReasoning = void 0;
|
|
24
|
+
exports.extractTrace = exports.generateGovernedReport = exports.evaluateAIAction = exports.listAIProviders = exports.getAIProvider = exports.registerAIProvider = exports.generateDeterministicReport = exports.DeterministicProvider = exports.AI_ROLES = exports.parseReasonRequestBody = exports.handleRunPreset = exports.handleGetPreset = exports.handleListPresets = exports.handleHealthCheck = exports.handleCreateCapsule = exports.handleReasonFromCapsule = exports.handleReasonRequest = exports.TRADING_DEMO = exports.runGovernedComparison = exports.analyzeMiroFishSimulation = exports.DEFAULT_MIROFISH_CONFIG = exports.buildMiroFishRequest = exports.miroFishResultToSwarmResult = exports.stakeholdersToMiroFishAgents = exports.runUnifiedSimulation = exports.getMiroFishClient = exports.configureMiroFish = exports.MiroFishError = exports.MiroFishClient = exports.runSwarmSimulation = exports.initNeuroverseModule = exports.validationToEnforcedConstraints = exports.simulationToGovernanceSignals = exports.verdictToConstitutionalChecks = exports.validateScenarioWorld = exports.runWorldSimulation = exports.evaluateScenarioGuard = exports.buildWorldFromScenario = exports.runFullGovernanceChecks = exports.runGovernanceChecks = exports.processReasonRequest = exports.SCENARIO_TEMPLATES = exports.getPresetCapsule = exports.extractCapsuleFromUrl = exports.buildShareableUrl = exports.decodeCapsule = exports.encodeCapsule = exports.capsuleToReasonRequest = exports.createCapsule = exports.runGoalReasoning = void 0;
|
|
25
25
|
// Goal-Directed Strategy Engine
|
|
26
26
|
var goalEngine_1 = require("./goalEngine");
|
|
27
27
|
Object.defineProperty(exports, "runGoalReasoning", { enumerable: true, get: function () { return goalEngine_1.runGoalReasoning; } });
|
|
@@ -80,3 +80,18 @@ Object.defineProperty(exports, "handleListPresets", { enumerable: true, get: fun
|
|
|
80
80
|
Object.defineProperty(exports, "handleGetPreset", { enumerable: true, get: function () { return api_1.handleGetPreset; } });
|
|
81
81
|
Object.defineProperty(exports, "handleRunPreset", { enumerable: true, get: function () { return api_1.handleRunPreset; } });
|
|
82
82
|
Object.defineProperty(exports, "parseReasonRequestBody", { enumerable: true, get: function () { return api_1.parseReasonRequestBody; } });
|
|
83
|
+
var aiProvider_1 = require("./aiProvider");
|
|
84
|
+
Object.defineProperty(exports, "AI_ROLES", { enumerable: true, get: function () { return aiProvider_1.AI_ROLES; } });
|
|
85
|
+
Object.defineProperty(exports, "DeterministicProvider", { enumerable: true, get: function () { return aiProvider_1.DeterministicProvider; } });
|
|
86
|
+
Object.defineProperty(exports, "generateDeterministicReport", { enumerable: true, get: function () { return aiProvider_1.generateDeterministicReport; } });
|
|
87
|
+
Object.defineProperty(exports, "registerAIProvider", { enumerable: true, get: function () { return aiProvider_1.registerAIProvider; } });
|
|
88
|
+
Object.defineProperty(exports, "getAIProvider", { enumerable: true, get: function () { return aiProvider_1.getAIProvider; } });
|
|
89
|
+
Object.defineProperty(exports, "listAIProviders", { enumerable: true, get: function () { return aiProvider_1.listAIProviders; } });
|
|
90
|
+
Object.defineProperty(exports, "evaluateAIAction", { enumerable: true, get: function () { return aiProvider_1.evaluateAIAction; } });
|
|
91
|
+
var reportEngine_1 = require("./reportEngine");
|
|
92
|
+
Object.defineProperty(exports, "generateGovernedReport", { enumerable: true, get: function () { return reportEngine_1.generateGovernedReport; } });
|
|
93
|
+
Object.defineProperty(exports, "extractTrace", { enumerable: true, get: function () { return reportEngine_1.extractTrace; } });
|
|
94
|
+
// Live Simulation Adapters — plug ANY running simulator into governance + trace
|
|
95
|
+
// NOTE: liveAdapter.ts uses Node.js APIs (child_process, events) and is NOT
|
|
96
|
+
// exported here to avoid Vite browser bundle errors. Import directly from
|
|
97
|
+
// "./liveAdapter" in CLI/server code only.
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Live Simulation Adapter
|
|
4
|
+
*
|
|
5
|
+
* Connects ANY running simulator into the NV-SIM governance + trace pipeline.
|
|
6
|
+
* Instead of running our own swarm simulation, we ingest live events from an
|
|
7
|
+
* external simulator process, normalize them, and feed them through governance.
|
|
8
|
+
*
|
|
9
|
+
* Architecture:
|
|
10
|
+
* External Simulator (running process / stream)
|
|
11
|
+
* ↓ stdout / WebSocket / SSE
|
|
12
|
+
* LiveSimulationAdapter (parse + normalize)
|
|
13
|
+
* ↓ LiveSimulationRound
|
|
14
|
+
* Governance Engine (evaluate guards)
|
|
15
|
+
* ↓ verdicts
|
|
16
|
+
* Trace UI (causality visualization)
|
|
17
|
+
*
|
|
18
|
+
* Supported adapter types:
|
|
19
|
+
* - ProcessAdapter: spawn a child process, parse stdout
|
|
20
|
+
* - StreamAdapter: connect to WebSocket/SSE endpoint (future)
|
|
21
|
+
*
|
|
22
|
+
* Every adapter emits the same LiveSimulationRound events, regardless of source.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.ADAPTER_REGISTRY = void 0;
|
|
26
|
+
exports.createProcessAdapter = createProcessAdapter;
|
|
27
|
+
exports.parseMiroFishLine = parseMiroFishLine;
|
|
28
|
+
exports.parseGenericJsonLine = parseGenericJsonLine;
|
|
29
|
+
exports.createAdapter = createAdapter;
|
|
30
|
+
const child_process_1 = require("child_process");
|
|
31
|
+
const events_1 = require("events");
|
|
32
|
+
function createParserState() {
|
|
33
|
+
return {
|
|
34
|
+
currentRound: 0,
|
|
35
|
+
pendingActions: [],
|
|
36
|
+
pendingEvents: [],
|
|
37
|
+
pendingDynamics: [],
|
|
38
|
+
buffer: {},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Process adapter — spawns a child process and parses its stdout.
|
|
43
|
+
*
|
|
44
|
+
* Usage:
|
|
45
|
+
* const adapter = createProcessAdapter({
|
|
46
|
+
* type: "process",
|
|
47
|
+
* label: "MiroFish (Live)",
|
|
48
|
+
* command: "node",
|
|
49
|
+
* args: ["mirofish.js"],
|
|
50
|
+
* parser: parseMiroFishLine,
|
|
51
|
+
* });
|
|
52
|
+
* adapter.on("round", (round) => { ... });
|
|
53
|
+
* await adapter.start();
|
|
54
|
+
*/
|
|
55
|
+
function createProcessAdapter(config) {
|
|
56
|
+
const emitter = new events_1.EventEmitter();
|
|
57
|
+
let proc = null;
|
|
58
|
+
let _status = "idle";
|
|
59
|
+
const state = createParserState();
|
|
60
|
+
function setStatus(s) {
|
|
61
|
+
_status = s;
|
|
62
|
+
emitter.emit("status", s);
|
|
63
|
+
}
|
|
64
|
+
Object.defineProperties(emitter, {
|
|
65
|
+
status: { get: () => _status },
|
|
66
|
+
source: { get: () => config.type },
|
|
67
|
+
label: { get: () => config.label },
|
|
68
|
+
});
|
|
69
|
+
emitter.start = async function start() {
|
|
70
|
+
setStatus("connecting");
|
|
71
|
+
proc = (0, child_process_1.spawn)(config.command, config.args, {
|
|
72
|
+
cwd: config.cwd,
|
|
73
|
+
env: { ...process.env, ...config.env },
|
|
74
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
75
|
+
});
|
|
76
|
+
let lineBuffer = "";
|
|
77
|
+
proc.stdout?.on("data", (chunk) => {
|
|
78
|
+
lineBuffer += chunk.toString();
|
|
79
|
+
const lines = lineBuffer.split("\n");
|
|
80
|
+
// Keep incomplete last line in buffer
|
|
81
|
+
lineBuffer = lines.pop() ?? "";
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
const trimmed = line.trim();
|
|
84
|
+
if (!trimmed)
|
|
85
|
+
continue;
|
|
86
|
+
try {
|
|
87
|
+
const round = config.parser(trimmed, state);
|
|
88
|
+
if (round) {
|
|
89
|
+
emitter.emit("round", round);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
// Parser error — log but don't crash
|
|
94
|
+
emitter.emit("error", new Error(`Parser error: ${err}`));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (_status === "connecting") {
|
|
98
|
+
setStatus("streaming");
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
proc.stderr?.on("data", (chunk) => {
|
|
102
|
+
// stderr is informational — don't treat as error
|
|
103
|
+
const msg = chunk.toString().trim();
|
|
104
|
+
if (msg) {
|
|
105
|
+
emitter.emit("log", msg);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
proc.on("close", (code) => {
|
|
109
|
+
// Flush any remaining buffer
|
|
110
|
+
if (lineBuffer.trim()) {
|
|
111
|
+
try {
|
|
112
|
+
const round = config.parser(lineBuffer.trim(), state);
|
|
113
|
+
if (round)
|
|
114
|
+
emitter.emit("round", round);
|
|
115
|
+
}
|
|
116
|
+
catch { /* ignore */ }
|
|
117
|
+
}
|
|
118
|
+
if (code === 0) {
|
|
119
|
+
setStatus("completed");
|
|
120
|
+
emitter.emit("complete");
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
setStatus("error");
|
|
124
|
+
emitter.emit("error", new Error(`Process exited with code ${code}`));
|
|
125
|
+
}
|
|
126
|
+
proc = null;
|
|
127
|
+
});
|
|
128
|
+
proc.on("error", (err) => {
|
|
129
|
+
setStatus("error");
|
|
130
|
+
emitter.emit("error", err);
|
|
131
|
+
proc = null;
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
emitter.stop = async function stop() {
|
|
135
|
+
if (proc) {
|
|
136
|
+
proc.kill("SIGTERM");
|
|
137
|
+
// Give it 3s to exit gracefully, then force
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
if (proc)
|
|
140
|
+
proc.kill("SIGKILL");
|
|
141
|
+
}, 3000);
|
|
142
|
+
}
|
|
143
|
+
setStatus("idle");
|
|
144
|
+
};
|
|
145
|
+
return emitter;
|
|
146
|
+
}
|
|
147
|
+
// ============================================
|
|
148
|
+
// MIROFISH STDOUT PARSER
|
|
149
|
+
// ============================================
|
|
150
|
+
/**
|
|
151
|
+
* Parses MiroFish stdout line-by-line.
|
|
152
|
+
*
|
|
153
|
+
* Expected MiroFish output formats:
|
|
154
|
+
*
|
|
155
|
+
* JSON-per-line (preferred):
|
|
156
|
+
* {"step":1,"agent_actions":[{"agent_id":"a1","action":"buy","sentiment":0.5,"confidence":0.8}]}
|
|
157
|
+
*
|
|
158
|
+
* Key-value (fallback):
|
|
159
|
+
* [step 1] agent=swarm_1 action=panic_sell sentiment=-0.7 confidence=0.6
|
|
160
|
+
* [step 1] event=market_crash severity=major
|
|
161
|
+
* [step 1] emergent=herd_behavior
|
|
162
|
+
* [step 2] ...
|
|
163
|
+
*
|
|
164
|
+
* We emit a LiveSimulationRound when we detect a new step number
|
|
165
|
+
* (meaning the previous step is complete).
|
|
166
|
+
*/
|
|
167
|
+
function parseMiroFishLine(line, state) {
|
|
168
|
+
// Try JSON first
|
|
169
|
+
if (line.startsWith("{")) {
|
|
170
|
+
try {
|
|
171
|
+
const data = JSON.parse(line);
|
|
172
|
+
if (data.step != null && Array.isArray(data.agent_actions)) {
|
|
173
|
+
// Parse adaptation data if present
|
|
174
|
+
let adaptation;
|
|
175
|
+
if (data.adaptation) {
|
|
176
|
+
const a = data.adaptation;
|
|
177
|
+
adaptation = {
|
|
178
|
+
count: a.count ?? 0,
|
|
179
|
+
rate: a.rate ?? 0,
|
|
180
|
+
deltas: (a.deltas ?? []).map((d) => ({
|
|
181
|
+
agent: d.agent ?? "unknown",
|
|
182
|
+
intended: d.intended ?? "",
|
|
183
|
+
actual: d.actual ?? "",
|
|
184
|
+
decision: d.decision ?? "BLOCK",
|
|
185
|
+
rule: d.rule,
|
|
186
|
+
reason: d.reason,
|
|
187
|
+
shift: d.shift ?? "unknown",
|
|
188
|
+
})),
|
|
189
|
+
intendedDistribution: a.intended_distribution ?? {},
|
|
190
|
+
actualDistribution: a.actual_distribution ?? {},
|
|
191
|
+
clusterPatterns: (a.cluster_patterns ?? []).map((c) => ({
|
|
192
|
+
type: c.type ?? "unknown",
|
|
193
|
+
strength: c.strength ?? 0,
|
|
194
|
+
agents: c.agents ?? 0,
|
|
195
|
+
})),
|
|
196
|
+
narrative: a.narrative ?? "",
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
// Complete step as JSON — emit immediately
|
|
200
|
+
return {
|
|
201
|
+
round: data.step,
|
|
202
|
+
source: "mirofish",
|
|
203
|
+
agentActions: data.agent_actions.map((a) => ({
|
|
204
|
+
agent: a.agent_id ?? a.agent ?? "unknown",
|
|
205
|
+
action: a.action ?? "",
|
|
206
|
+
impact: a.sentiment ?? a.impact ?? 0,
|
|
207
|
+
confidence: a.confidence ?? 0.5,
|
|
208
|
+
...(a.verdict ? { verdict: { status: a.verdict.status, rule: a.verdict.rule, reason: a.verdict.reason } } : {}),
|
|
209
|
+
})),
|
|
210
|
+
systemEvents: (data.system_events ?? []).map((e) => typeof e === "string"
|
|
211
|
+
? { id: e, label: e }
|
|
212
|
+
: { id: e.id ?? e.event ?? "unknown", label: e.label ?? e.event ?? e.id ?? "", severity: e.severity }),
|
|
213
|
+
emergentDynamics: data.emergent_patterns ?? data.emergent ?? [],
|
|
214
|
+
adaptation,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch { /* not JSON, fall through to key-value parsing */ }
|
|
219
|
+
}
|
|
220
|
+
// Key-value format: [step N] key=value ...
|
|
221
|
+
const stepMatch = line.match(/\[step\s+(\d+)\]/i);
|
|
222
|
+
if (!stepMatch)
|
|
223
|
+
return null;
|
|
224
|
+
const stepNum = parseInt(stepMatch[1], 10);
|
|
225
|
+
let result = null;
|
|
226
|
+
// If new step, flush previous
|
|
227
|
+
if (stepNum > state.currentRound && state.currentRound > 0) {
|
|
228
|
+
result = {
|
|
229
|
+
round: state.currentRound,
|
|
230
|
+
source: "mirofish",
|
|
231
|
+
agentActions: [...state.pendingActions],
|
|
232
|
+
systemEvents: state.pendingEvents.length > 0 ? [...state.pendingEvents] : undefined,
|
|
233
|
+
emergentDynamics: state.pendingDynamics.length > 0 ? [...state.pendingDynamics] : undefined,
|
|
234
|
+
};
|
|
235
|
+
state.pendingActions = [];
|
|
236
|
+
state.pendingEvents = [];
|
|
237
|
+
state.pendingDynamics = [];
|
|
238
|
+
}
|
|
239
|
+
state.currentRound = stepNum;
|
|
240
|
+
const rest = line.slice(stepMatch.index + stepMatch[0].length).trim();
|
|
241
|
+
// Parse agent action
|
|
242
|
+
const agentMatch = rest.match(/agent=(\S+)\s+action=(\S+)(?:\s+sentiment=([-\d.]+))?(?:\s+confidence=([-\d.]+))?/);
|
|
243
|
+
if (agentMatch) {
|
|
244
|
+
state.pendingActions.push({
|
|
245
|
+
agent: agentMatch[1],
|
|
246
|
+
action: agentMatch[2],
|
|
247
|
+
impact: agentMatch[3] ? parseFloat(agentMatch[3]) : 0,
|
|
248
|
+
confidence: agentMatch[4] ? parseFloat(agentMatch[4]) : 0.5,
|
|
249
|
+
});
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
// Parse system event
|
|
253
|
+
const eventMatch = rest.match(/event=(\S+)(?:\s+severity=(\S+))?/);
|
|
254
|
+
if (eventMatch) {
|
|
255
|
+
state.pendingEvents.push({
|
|
256
|
+
id: eventMatch[1],
|
|
257
|
+
label: eventMatch[1].replace(/_/g, " "),
|
|
258
|
+
severity: eventMatch[2],
|
|
259
|
+
});
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
// Parse emergent behavior
|
|
263
|
+
const emergentMatch = rest.match(/emergent=(.+)/);
|
|
264
|
+
if (emergentMatch) {
|
|
265
|
+
state.pendingDynamics.push(emergentMatch[1].trim());
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
// ============================================
|
|
271
|
+
// GENERIC JSON PARSER (works for any JSON-per-line simulator)
|
|
272
|
+
// ============================================
|
|
273
|
+
/**
|
|
274
|
+
* Generic JSON-per-line parser.
|
|
275
|
+
* Expects each line to be a JSON object with at minimum:
|
|
276
|
+
* { "step": N, "agents": [{ "id": "...", "action": "...", "impact": 0.5 }] }
|
|
277
|
+
*/
|
|
278
|
+
function parseGenericJsonLine(line, _state) {
|
|
279
|
+
if (!line.startsWith("{"))
|
|
280
|
+
return null;
|
|
281
|
+
try {
|
|
282
|
+
const data = JSON.parse(line);
|
|
283
|
+
const step = data.step ?? data.tick ?? data.round ?? data.time;
|
|
284
|
+
if (step == null)
|
|
285
|
+
return null;
|
|
286
|
+
const agents = (data.agents ?? data.agent_actions ?? []).map((a) => ({
|
|
287
|
+
agent: a.id ?? a.agent ?? a.agent_id ?? "unknown",
|
|
288
|
+
action: a.action ?? a.behavior ?? "",
|
|
289
|
+
impact: a.impact ?? a.sentiment ?? a.value ?? 0,
|
|
290
|
+
confidence: a.confidence ?? 0.5,
|
|
291
|
+
}));
|
|
292
|
+
return {
|
|
293
|
+
round: typeof step === "number" ? step : parseInt(step, 10),
|
|
294
|
+
source: "generic",
|
|
295
|
+
agentActions: agents,
|
|
296
|
+
systemEvents: (data.events ?? []).map((e) => typeof e === "string"
|
|
297
|
+
? { id: e, label: e }
|
|
298
|
+
: { id: e.id ?? "unknown", label: e.label ?? e.id ?? "", severity: e.severity }),
|
|
299
|
+
emergentDynamics: data.dynamics ?? data.emergent ?? [],
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
exports.ADAPTER_REGISTRY = {
|
|
307
|
+
mirofish: {
|
|
308
|
+
id: "mirofish",
|
|
309
|
+
label: "MiroFish (Live)",
|
|
310
|
+
description: "Spawn MiroFish swarm simulation and stream agent behavior",
|
|
311
|
+
createConfig: (opts = {}) => ({
|
|
312
|
+
type: "process",
|
|
313
|
+
label: "MiroFish (Live)",
|
|
314
|
+
command: opts.command ?? "node",
|
|
315
|
+
args: (opts.args ?? "mirofish.js").split(" "),
|
|
316
|
+
cwd: opts.cwd,
|
|
317
|
+
parser: parseMiroFishLine,
|
|
318
|
+
}),
|
|
319
|
+
},
|
|
320
|
+
generic: {
|
|
321
|
+
id: "generic",
|
|
322
|
+
label: "Generic JSON Process",
|
|
323
|
+
description: "Spawn any process that outputs JSON-per-line simulation data",
|
|
324
|
+
createConfig: (opts = {}) => ({
|
|
325
|
+
type: "process",
|
|
326
|
+
label: opts.label ?? "External Simulator",
|
|
327
|
+
command: opts.command ?? "python",
|
|
328
|
+
args: (opts.args ?? "sim.py").split(" "),
|
|
329
|
+
cwd: opts.cwd,
|
|
330
|
+
parser: parseGenericJsonLine,
|
|
331
|
+
}),
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
/**
|
|
335
|
+
* Create an adapter from the registry.
|
|
336
|
+
*/
|
|
337
|
+
function createAdapter(adapterId, options) {
|
|
338
|
+
const entry = exports.ADAPTER_REGISTRY[adapterId];
|
|
339
|
+
if (!entry)
|
|
340
|
+
return null;
|
|
341
|
+
return createProcessAdapter(entry.createConfig(options));
|
|
342
|
+
}
|