@radcliffetech/symbolos-core
Version:
Core symbolic simulation and execution engine for Symbolos
265 lines (264 loc) • 10.1 kB
JavaScript
import { storeWorldArchive, storeWorldFrame } from '../lib/utils/world-utils';
import chalk from 'chalk';
import crypto from 'node:crypto';
import fs from 'fs/promises';
import path from 'path';
const OUTPUT_DIR = 'sandbox/worlds';
function addToArtifacts(context, obj) {
if (!obj?.id)
return;
context._artifactsById.set(obj.id, obj);
}
function getArtifacts(context) {
return Array.from(context._artifactsById.values());
}
function flattenSymbolicObjects(obj) {
const results = [];
const seen = new Set();
const stack = [obj];
while (stack.length) {
const current = stack.pop();
if (!current || typeof current !== 'object')
continue;
if (current.id && current.type && !seen.has(current.id)) {
seen.add(current.id);
results.push(current);
}
for (const val of Object.values(current)) {
stack.push(val);
}
}
return results;
}
async function executeCycleStep(step, index, input, id, context, verbose, simulationTick, params) {
const { functor, resolveInput } = step;
// Optimize input mapping: avoid full clone and skip unnecessary await
let mappedInput;
if (resolveInput) {
const result = resolveInput(input, context);
mappedInput = result instanceof Promise ? await result : result;
}
else {
mappedInput = input;
}
const stepLabel = `🔁 [${simulationTick}] Step ${index + 1}: ${functor.inputType} ➝ ${functor.outputType}`;
console.log(chalk.cyan.bold(`[runPipeline] ${stepLabel}`));
if (verbose) {
console.log(chalk.yellow(`[runPipeline] Mapped input for step ${index + 1}: ${step.id}`, mappedInput));
}
const inputItems = Array.isArray(mappedInput) ? mappedInput : [mappedInput];
if (verbose) {
console.log(chalk.gray(`[runPipeline] Number of input items for step ${index + 1}: ${inputItems.length}`));
}
let output = [];
for (const [itemIndex, item] of inputItems.entries()) {
const fullInput = { ...item };
if (verbose) {
console.log(chalk.yellow(`[runPipeline] Applying functor ${functor.method} to input item ${itemIndex + 1}:`));
}
const functorOutput = await functor.apply(fullInput, context);
const transformation = {
id: `tx-${crypto.randomUUID()}`,
type: 'Transformation',
method: functor.method,
createdAt: new Date().toISOString(),
tick: simulationTick,
inputId: fullInput.id,
outputId: Array.isArray(functorOutput)
? functorOutput.map((o) => o?.id).filter(Boolean)
: functorOutput?.id,
};
if (verbose) {
console.log(chalk.gray(`[runPipeline] Functor ${functor.method} completed for item ${itemIndex + 1}.`));
}
const flattened = flattenSymbolicObjects(functorOutput);
for (const obj of flattened) {
if (obj && typeof obj === 'object' && !('tick' in obj)) {
obj.tick = simulationTick;
}
}
if (transformation && !('tick' in transformation)) {
transformation.tick = simulationTick;
}
addToArtifacts(context, transformation);
flattened.forEach((obj) => addToArtifacts(context, obj));
output.push(...flattened);
}
if (output.length === 0) {
if (verbose)
console.log(chalk.red(`[runPipeline] ⚠️ No output returned by step ${index + 1}: ${step.id}`));
}
else {
if (verbose) {
console.log(chalk.green(`\n[runPipeline] ✅ Step Output: ${output.length > 1
? output.map((o) => `${o.type} (${o.id})`).join(', ')
: `${output[0].type} (${output[0].id})`}\n`));
}
}
return { output, newId: output.length === 1 ? output[0].id : id };
}
const recordSymbolicAction = async ({ entry, transformationId, context, instrumentId, purpose, tick, }) => {
const action = {
id: `action-${crypto.randomUUID()}`,
type: 'SymbolicAction',
label: `${entry.type} Action`,
transformationId,
actorId: context.subjectiveFrame?.id ?? 'unknown-actor',
contextId: context.contextualFrame?.id ?? 'unknown-context',
criteriaId: context.selectionCriteria?.id ?? 'unknown-criteria',
instrumentId: instrumentId ?? context.instrumentId ?? 'default-instrument', // Use provided instrumentId or default
purpose: purpose ?? context.purpose ?? 'default-purpose', // Use provided purpose or default
inputId: entry.id,
outputId: entry.id,
rootId: 'action-root', // all actions are linked to a root action
status: 'completed',
timestamp: new Date().toISOString(),
tick,
createdAt: new Date().toISOString(),
};
// Add the action to context._artifacts, replacing if already present
addToArtifacts(context, action);
};
export function createWorldFromFrame({ frame, pipelineId, runId, }) {
const artifacts = new Map();
for (const obj of frame.members) {
artifacts.set(obj.id, obj);
}
return {
tick: frame.tick ?? 0,
step: frame.step ?? 0,
runId,
pipelineId,
artifacts,
context: {},
};
}
export function makeNewWorld(pipelineId) {
return {
tick: 0,
step: 0,
runId: new Date().toISOString().replace(/[:.]/g, '-'),
pipelineId,
artifacts: new Map(),
context: {},
};
}
/**
* Run a world pipeline using a SymbolicWorld as the stateful model.
* @param world - The SymbolicWorld instance (mutable, stateful)
* @param userArgs - PipelineArgs object (contains input parameters and config)
* @param steps - Array of FunctorStep pipeline steps
* @param config - Optional run config
* @returns The mutated SymbolicWorld after running all steps
*/
export async function runWorldPipeline({ world, userArgs, steps, config, }) {
const { verbose } = config;
const runId = world.runId;
const pipelineId = world.pipelineId;
console.log(chalk.blueBright(`🔧 Running pipeline: ${pipelineId} with runId: ${runId}`));
console.log(config);
let input = userArgs;
let id = pipelineId;
world.context.pipelineId = pipelineId;
world.context.runId = runId;
world.context._artifactsById = world.artifacts;
addToArtifacts(world.context, userArgs);
for (const [index, step] of steps.entries()) {
if (step.tickAdvance !== false) {
world.tick += 1;
}
const { output, newId } = await executeCycleStep(step, index, input, id, world.context, verbose ?? false, world.tick, userArgs.params);
input = output;
id = newId;
if (step.storeOutputAs) {
world.context[step.storeOutputAs] = output.length === 1 ? output[0] : output;
}
// --- Collect output entries for later batched store/write ---
if (!world.context._batchedEntries)
world.context._batchedEntries = [];
for (const entry of output) {
world.context._batchedEntries.push({
entry,
transformationId: id,
instrumentId: step.functor.id,
purpose: step.purpose,
tick: world.tick,
chainDir: `sandbox/${pipelineId}/${runId}`,
stepPrefix: String(index + 1).padStart(3, '0'),
});
}
await storeWorldFrame({
context: world.context,
pipelineId,
runId,
config,
tick: world.tick,
step: index,
getArtifacts,
});
if (verbose) {
console.log(chalk.gray(`[runWorldPipeline] Saved frame t${world.tick}-s${index}`));
}
}
if (world.context._batchedEntries &&
Array.isArray(world.context._batchedEntries) &&
world.context._batchedEntries.length > 0) {
for (const batched of world.context._batchedEntries) {
recordSymbolicAction({
entry: batched.entry,
transformationId: batched.transformationId,
instrumentId: batched.instrumentId,
purpose: batched.purpose,
context: world.context,
chainDir: batched.chainDir,
stepPrefix: batched.stepPrefix,
tick: batched.tick,
});
}
}
const { filePath: archivePath } = await storeWorldArchive({
context: world.context,
pipelineId,
runId,
config,
getArtifacts,
});
const frontendPath = path.join(OUTPUT_DIR, `${pipelineId}.world.json.gz`);
await fs.mkdir(OUTPUT_DIR, { recursive: true });
await fs.copyFile(archivePath, frontendPath);
const pipelineRun = {
type: 'PipelineRun',
id: `pipeline-run-${crypto.randomUUID()}`,
label: world.context.forkedFromRunId
? `Forked Run from ${world.context.forkedFromRunId}`
: 'Pipeline Run',
pipelineId,
runId,
tickCount: world.tick,
stepCount: steps.length,
createdAt: new Date().toISOString(),
status: 'completed',
...(world.context.forkedFromRunId && { forkedFromRunId: world.context.forkedFromRunId }),
};
addToArtifacts(world.context, pipelineRun);
return world;
}
export function forkWorld(sourceWorld, newParams) {
const artifacts = new Map();
for (const [id, obj] of sourceWorld.artifacts.entries()) {
artifacts.set(id, obj);
}
return {
tick: sourceWorld.tick,
step: sourceWorld.step,
runId: 'forked-' + crypto.randomUUID(),
pipelineId: sourceWorld.pipelineId,
artifacts,
context: {
_artifactsById: artifacts,
...sourceWorld.context,
forkedFromRunId: sourceWorld.runId,
...(newParams ? { userArgs: { params: newParams } } : {}),
},
};
}