426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
1016 lines
33 KiB
TypeScript
1016 lines
33 KiB
TypeScript
#!/usr/bin/env bun
|
|
|
|
/**
|
|
* Generate Simulation Data
|
|
*
|
|
* Unified command to run N hours of Feed simulation and export EVERYTHING
|
|
* to reviewable files: all LLM inputs/outputs, agent trajectories, world
|
|
* narratives, actor state, NPC decisions, posts, trades, messages.
|
|
*
|
|
* Usage:
|
|
* bun run sim:generate -- --hours=2 # 2 hours (40 cycles)
|
|
* bun run sim:generate -- --ticks=10 --parallel=5 # 10 cycles, 5 agents at once
|
|
* bun run sim:generate -- --ticks=20 --fast # fast: skip world ticks entirely
|
|
* bun run sim:generate -- --ticks=10 --world-tick-every=5 # world tick every 5th cycle
|
|
* bun run sim:generate -- --ticks=5 --delay=200 # faster batching (200ms between)
|
|
*
|
|
* Speed guide:
|
|
* --fast Skip world ticks (saves ~10-30s per cycle)
|
|
* --world-tick-every=N Run world tick only every Nth cycle (default: 3)
|
|
* --delay=200 Reduce inter-batch delay (default: 500ms, was 2000ms)
|
|
* --parallel=8 Increase batch size (default: 5, limited by Groq rate limit)
|
|
*
|
|
* Output: runs/simulation-data/<timestamp>/
|
|
*
|
|
* Mapping: 1 hour ≈ 20 tick cycles (each cycle = 1 world tick + 1 agent round).
|
|
* Override with --ticks-per-hour=N.
|
|
*/
|
|
|
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { parseArgs } from "node:util";
|
|
import type { IAgentRuntime } from "@elizaos/core";
|
|
import {
|
|
agentRuntimeManager,
|
|
autonomousCoordinator,
|
|
createTestAgent,
|
|
} from "@feed/agents";
|
|
import {
|
|
db,
|
|
desc,
|
|
eq,
|
|
gte,
|
|
inArray,
|
|
llmCallLogs,
|
|
posts,
|
|
trajectories,
|
|
users,
|
|
worldEvents,
|
|
} from "@feed/db";
|
|
import { executeGameTick } from "@feed/engine";
|
|
import { sleep } from "@feed/shared";
|
|
import { config as loadDotenv } from "dotenv";
|
|
import {
|
|
buildCanonicalSimulationRoster,
|
|
type CharacterMessageExampleTurn,
|
|
type FeedCharacterSheet,
|
|
writeLocalCharacterSheets,
|
|
} from "../packages/agents/src/character-roster/local-roster";
|
|
import { upsertAgentConfig } from "../packages/agents/src/shared/agent-config";
|
|
import {
|
|
getLLMCallCallback,
|
|
type LLMCallInput,
|
|
setLLMCallCallback,
|
|
} from "../packages/engine/src/dag-trace";
|
|
|
|
loadDotenv({ path: path.resolve(process.cwd(), ".env") });
|
|
loadDotenv({ path: path.resolve(process.cwd(), ".env.local") });
|
|
|
|
// Force trajectory recording and enable posting
|
|
process.env.RECORD_AGENT_TRAJECTORIES = "true";
|
|
process.env.FEED_ENABLE_PLAYER_POSTING = "1";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
interface SimOptions {
|
|
hours: number;
|
|
ticks: number;
|
|
ticksPerHour: number;
|
|
parallel: number;
|
|
delayMs: number;
|
|
outputDir: string;
|
|
fast: boolean;
|
|
worldTickEvery: number;
|
|
}
|
|
|
|
interface AgentTickResult {
|
|
agentId: string;
|
|
username: string;
|
|
characterId: string;
|
|
success: boolean;
|
|
trajectoryId?: string;
|
|
error?: string;
|
|
durationMs: number;
|
|
}
|
|
|
|
interface CycleSummary {
|
|
cycleNumber: number;
|
|
worldTick: {
|
|
durationMs: number;
|
|
postsCreated: number;
|
|
eventsCreated: number;
|
|
marketsUpdated: number;
|
|
questionsCreated: number;
|
|
};
|
|
agentRound: {
|
|
durationMs: number;
|
|
total: number;
|
|
successful: number;
|
|
failed: number;
|
|
trajectoriesCaptured: number;
|
|
results: AgentTickResult[];
|
|
};
|
|
llmCallsInCycle: number;
|
|
}
|
|
|
|
type RuntimeCharacter = IAgentRuntime["character"] & {
|
|
username?: string;
|
|
lore?: string[];
|
|
topics?: string[];
|
|
adjectives?: string[];
|
|
postExamples?: string[];
|
|
messageExamples?: CharacterMessageExampleTurn[][];
|
|
settings?: Record<string, string | number>;
|
|
};
|
|
|
|
type RuntimeWithSettings = IAgentRuntime & {
|
|
settings?: Record<string, string>;
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CLI parsing
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function parseOptions(): SimOptions {
|
|
const { values } = parseArgs({
|
|
args: Bun.argv.slice(2),
|
|
options: {
|
|
hours: { type: "string", default: "1" },
|
|
ticks: { type: "string", default: "0" },
|
|
"ticks-per-hour": { type: "string", default: "20" },
|
|
parallel: { type: "string", default: "5" },
|
|
delay: { type: "string", default: "500" },
|
|
fast: { type: "boolean", default: false },
|
|
"world-tick-every": { type: "string", default: "3" },
|
|
output: { type: "string", default: "" },
|
|
},
|
|
strict: true,
|
|
allowPositionals: false,
|
|
});
|
|
|
|
const hours = Math.max(0, parseFloat(values.hours ?? "1"));
|
|
const ticksPerHour = Math.max(
|
|
1,
|
|
parseInt(values["ticks-per-hour"] ?? "20", 10),
|
|
);
|
|
const explicitTicks = parseInt(values.ticks ?? "0", 10);
|
|
const ticks =
|
|
explicitTicks > 0
|
|
? explicitTicks
|
|
: Math.max(1, Math.round(hours * ticksPerHour));
|
|
|
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
const defaultDir = path.resolve(
|
|
process.cwd(),
|
|
"runs",
|
|
"simulation-data",
|
|
stamp,
|
|
);
|
|
|
|
return {
|
|
hours,
|
|
ticks,
|
|
ticksPerHour,
|
|
parallel: Math.max(1, parseInt(values.parallel ?? "5", 10)),
|
|
delayMs: Math.max(0, parseInt(values.delay ?? "500", 10)),
|
|
fast: values.fast ?? false,
|
|
worldTickEvery: Math.max(
|
|
1,
|
|
parseInt(values["world-tick-every"] ?? "3", 10),
|
|
),
|
|
outputDir: values.output
|
|
? path.resolve(process.cwd(), values.output)
|
|
: defaultDir,
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// File helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function ensureDir(dir: string): void {
|
|
mkdirSync(dir, { recursive: true });
|
|
}
|
|
|
|
function writeJson(filePath: string, data: unknown): void {
|
|
writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
|
|
}
|
|
|
|
function appendJsonl(filePath: string, record: unknown): void {
|
|
writeFileSync(filePath, `${JSON.stringify(record)}\n`, { flag: "a" });
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Character / agent setup (mirrors run-local-character-simulation.ts)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function inferModelTier(sheet: FeedCharacterSheet): "free" | "pro" {
|
|
return sheet.settings.groq.large.startsWith("llama-") ? "free" : "pro";
|
|
}
|
|
|
|
function buildAgentPersonalitySummary(sheet: FeedCharacterSheet): string {
|
|
return [
|
|
`${sheet.feed.alignment} ${sheet.feed.team} posture`,
|
|
sheet.feed.socialStyle,
|
|
`scam:${sheet.feed.scamProfile.replaceAll("_", " ")}`,
|
|
`caution:${sheet.feed.caution}`,
|
|
`deception:${sheet.feed.deception}`,
|
|
].join(" | ");
|
|
}
|
|
|
|
function buildConfigStyle(sheet: FeedCharacterSheet) {
|
|
return {
|
|
all: sheet.style.all,
|
|
chat: sheet.style.chat,
|
|
post: sheet.style.post,
|
|
feed: {
|
|
sheetId: sheet.id,
|
|
username: sheet.username,
|
|
bio: sheet.bio,
|
|
lore: sheet.lore,
|
|
topics: sheet.topics,
|
|
adjectives: sheet.adjectives,
|
|
postExamples: sheet.postExamples,
|
|
models: sheet.settings.groq,
|
|
metadata: sheet.feed,
|
|
},
|
|
};
|
|
}
|
|
|
|
async function ensureCharacterAgent(
|
|
sheet: FeedCharacterSheet,
|
|
): Promise<{ agentId: string; username: string }> {
|
|
const result = await createTestAgent(sheet.id, {
|
|
username: sheet.username,
|
|
displayName: sheet.name,
|
|
virtualBalance: 25000,
|
|
autonomousTrading: sheet.feed.autonomy.trading,
|
|
autonomousPosting: sheet.feed.autonomy.posting,
|
|
autonomousCommenting: sheet.feed.autonomy.commenting,
|
|
autonomousDMs: sheet.feed.autonomy.dms,
|
|
autonomousGroupChats: sheet.feed.autonomy.groups,
|
|
systemPrompt: sheet.system,
|
|
});
|
|
|
|
await db
|
|
.update(users)
|
|
.set({
|
|
displayName: sheet.name,
|
|
bio: sheet.bio.join("\n"),
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(users.id, result.agentId));
|
|
|
|
await upsertAgentConfig(result.agentId, {
|
|
systemPrompt: sheet.system,
|
|
personality: buildAgentPersonalitySummary(sheet),
|
|
tradingStrategy: sheet.feed.tradingStyle,
|
|
style: buildConfigStyle(sheet),
|
|
messageExamples: sheet.messageExamples,
|
|
personaPrompt: JSON.stringify(sheet),
|
|
goals: {
|
|
motivations: sheet.feed.motivations,
|
|
fears: sheet.feed.fears,
|
|
topics: sheet.topics,
|
|
},
|
|
directives: sheet.style.all,
|
|
constraints: [
|
|
`alignment:${sheet.feed.alignment}`,
|
|
`team:${sheet.feed.team}`,
|
|
`scam_profile:${sheet.feed.scamProfile}`,
|
|
`deception:${sheet.feed.deception}`,
|
|
`competence:${sheet.feed.competence}`,
|
|
],
|
|
planningHorizon: sheet.feed.autonomy.groups
|
|
? sheet.feed.autonomy.dms
|
|
? "campaign"
|
|
: sheet.feed.team === "gray"
|
|
? "swing"
|
|
: "campaign"
|
|
: "single",
|
|
riskTolerance:
|
|
sheet.feed.caution === "paranoid"
|
|
? "low"
|
|
: sheet.feed.caution === "reckless"
|
|
? "high"
|
|
: sheet.settings.temperature > 0.75
|
|
? "high"
|
|
: sheet.settings.temperature < 0.6
|
|
? "low"
|
|
: "medium",
|
|
maxActionsPerTick:
|
|
sheet.feed.caution === "paranoid"
|
|
? 2
|
|
: sheet.feed.caution === "careful"
|
|
? 3
|
|
: 5,
|
|
modelTier: inferModelTier(sheet),
|
|
autonomousTrading: sheet.feed.autonomy.trading,
|
|
autonomousPosting: sheet.feed.autonomy.posting,
|
|
autonomousCommenting: sheet.feed.autonomy.commenting,
|
|
autonomousDMs: sheet.feed.autonomy.dms,
|
|
autonomousGroupChats: sheet.feed.autonomy.groups,
|
|
a2aEnabled: false,
|
|
updatedAt: new Date(),
|
|
});
|
|
|
|
return { agentId: result.agentId, username: result.agent.username };
|
|
}
|
|
|
|
function applySheetToRuntime(
|
|
runtime: IAgentRuntime,
|
|
sheet: FeedCharacterSheet,
|
|
): void {
|
|
const rc = runtime.character as RuntimeCharacter;
|
|
rc.name = sheet.name;
|
|
rc.system = sheet.system;
|
|
rc.bio = [...sheet.bio];
|
|
rc.username = sheet.username;
|
|
rc.lore = [...sheet.lore];
|
|
rc.topics = [...sheet.topics];
|
|
rc.adjectives = [...sheet.adjectives];
|
|
rc.postExamples = [...sheet.postExamples];
|
|
rc.messageExamples = sheet.messageExamples;
|
|
rc.style = buildConfigStyle(sheet);
|
|
rc.settings = {
|
|
...(rc.settings || {}),
|
|
GROQ_PRIMARY_MODEL: sheet.settings.groq.primary,
|
|
GROQ_SMALL_MODEL: sheet.settings.groq.small,
|
|
GROQ_LARGE_MODEL: sheet.settings.groq.large,
|
|
MODEL_VERSION: sheet.settings.groq.primary,
|
|
TEMPERATURE: String(sheet.settings.temperature),
|
|
MAX_TOKENS: String(sheet.settings.maxTokens),
|
|
};
|
|
const rws = runtime as RuntimeWithSettings;
|
|
rws.settings = {
|
|
...(rws.settings || {}),
|
|
GROQ_PRIMARY_MODEL: sheet.settings.groq.primary,
|
|
GROQ_SMALL_MODEL: sheet.settings.groq.small,
|
|
GROQ_LARGE_MODEL: sheet.settings.groq.large,
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Main
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function main(): Promise<void> {
|
|
const opts = parseOptions();
|
|
const runStartedAt = new Date();
|
|
|
|
console.log("=".repeat(72));
|
|
console.log(" FEED SIMULATION DATA GENERATOR");
|
|
console.log("=".repeat(72));
|
|
console.log(` Hours requested : ${opts.hours}`);
|
|
console.log(` Total cycles : ${opts.ticks}`);
|
|
console.log(` Ticks per hour : ${opts.ticksPerHour}`);
|
|
console.log(` Parallelism : ${opts.parallel}`);
|
|
console.log(` Delay (ms) : ${opts.delayMs}`);
|
|
console.log(
|
|
` Fast mode : ${opts.fast ? "YES (no world ticks)" : "no"}`,
|
|
);
|
|
console.log(
|
|
` World tick every: ${opts.fast ? "N/A" : `${opts.worldTickEvery} cycles`}`,
|
|
);
|
|
console.log(` Output dir : ${opts.outputDir}`);
|
|
console.log("=".repeat(72));
|
|
console.log("");
|
|
|
|
// Create output directories
|
|
const dirs = {
|
|
root: opts.outputDir,
|
|
actors: path.join(opts.outputDir, "actors"),
|
|
worldTicks: path.join(opts.outputDir, "world-ticks"),
|
|
agentTicks: path.join(opts.outputDir, "agent-ticks"),
|
|
llmCalls: path.join(opts.outputDir, "llm-calls"),
|
|
narratives: path.join(opts.outputDir, "narratives"),
|
|
trajectories: path.join(opts.outputDir, "trajectories"),
|
|
};
|
|
for (const dir of Object.values(dirs)) {
|
|
ensureDir(dir);
|
|
}
|
|
|
|
// Initialize JSONL files (empty)
|
|
const jsonlFiles = {
|
|
llmCallsAll: path.join(dirs.root, "llm-calls-all.jsonl"),
|
|
posts: path.join(dirs.narratives, "posts.jsonl"),
|
|
events: path.join(dirs.narratives, "events.jsonl"),
|
|
trades: path.join(dirs.narratives, "trades.jsonl"),
|
|
agentActions: path.join(dirs.narratives, "agent-actions.jsonl"),
|
|
trajectoriesAll: path.join(dirs.root, "trajectories-all.jsonl"),
|
|
};
|
|
|
|
// -----------------------------------------------------------------------
|
|
// LLM call capture - intercept engine LLM calls (world tick phase)
|
|
// Agent LLM calls are captured separately via the trajectory DB.
|
|
// -----------------------------------------------------------------------
|
|
let llmCallSequence = 0;
|
|
let currentCycle = 0;
|
|
let engineTokens = 0;
|
|
const enginePromptTypes: Record<string, number> = {};
|
|
const engineModels: Record<string, number> = {};
|
|
|
|
const priorCallback = getLLMCallCallback();
|
|
setLLMCallCallback((call: LLMCallInput) => {
|
|
llmCallSequence++;
|
|
|
|
const captured = {
|
|
...call,
|
|
capturedAt: new Date().toISOString(),
|
|
source: "engine",
|
|
sequenceNumber: llmCallSequence,
|
|
cycleNumber: currentCycle,
|
|
};
|
|
|
|
engineTokens += call.totalTokens || 0;
|
|
const pt = call.promptType || "unknown";
|
|
enginePromptTypes[pt] = (enginePromptTypes[pt] || 0) + 1;
|
|
const mdl = call.model || "unknown";
|
|
engineModels[mdl] = (engineModels[mdl] || 0) + 1;
|
|
|
|
// Write individual LLM call file
|
|
const callFileName = `${String(llmCallSequence).padStart(6, "0")}-${call.promptType || "unknown"}.json`;
|
|
writeJson(path.join(dirs.llmCalls, callFileName), captured);
|
|
|
|
// Append to master JSONL
|
|
appendJsonl(jsonlFiles.llmCallsAll, captured);
|
|
|
|
// Forward to any prior callback
|
|
priorCallback?.(call);
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Build character roster and create agents
|
|
// -----------------------------------------------------------------------
|
|
console.log("Loading character roster...");
|
|
const roster = buildCanonicalSimulationRoster();
|
|
await writeLocalCharacterSheets();
|
|
|
|
console.log(`Characters: ${roster.length} canonical agents`);
|
|
console.log("");
|
|
|
|
// Write all actor/character sheets
|
|
for (const sheet of roster) {
|
|
writeJson(path.join(dirs.actors, `${sheet.id}.json`), {
|
|
characterSheet: sheet,
|
|
summary: {
|
|
name: sheet.name,
|
|
username: sheet.username,
|
|
alignment: sheet.feed.alignment,
|
|
team: sheet.feed.team,
|
|
scamProfile: sheet.feed.scamProfile,
|
|
competence: sheet.feed.competence,
|
|
caution: sheet.feed.caution,
|
|
deception: sheet.feed.deception,
|
|
socialStyle: sheet.feed.socialStyle,
|
|
tradingStyle: sheet.feed.tradingStyle,
|
|
autonomy: sheet.feed.autonomy,
|
|
model: sheet.settings.groq.primary,
|
|
temperature: sheet.settings.temperature,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Create/ensure agents in DB
|
|
const idByCharacterId = new Map<string, string>();
|
|
const usernameByAgentId = new Map<string, string>();
|
|
|
|
for (const sheet of roster) {
|
|
try {
|
|
const agent = await ensureCharacterAgent(sheet);
|
|
idByCharacterId.set(sheet.id, agent.agentId);
|
|
usernameByAgentId.set(agent.agentId, agent.username);
|
|
console.log(
|
|
` Ready: ${sheet.name} (@${sheet.username}) -> ${agent.agentId}`,
|
|
);
|
|
} catch (err) {
|
|
console.error(` FAILED to create agent for ${sheet.name}: ${err}`);
|
|
}
|
|
}
|
|
|
|
console.log("");
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Run simulation cycles
|
|
// -----------------------------------------------------------------------
|
|
const cycleSummaries: CycleSummary[] = [];
|
|
const allTrajectoryIds: string[] = [];
|
|
|
|
for (let cycle = 1; cycle <= opts.ticks; cycle++) {
|
|
currentCycle = cycle;
|
|
const llmCallsBefore = llmCallSequence;
|
|
|
|
console.log(`--- Cycle ${cycle}/${opts.ticks} ---`);
|
|
|
|
// --- World tick (skip in --fast mode, or only run every N cycles) ---
|
|
const runWorldTick =
|
|
!opts.fast && (cycle === 1 || cycle % opts.worldTickEvery === 0);
|
|
|
|
const worldStart = Date.now();
|
|
let worldResult = {
|
|
postsCreated: 0,
|
|
eventsCreated: 0,
|
|
marketsUpdated: 0,
|
|
questionsCreated: 0,
|
|
};
|
|
|
|
if (!runWorldTick) {
|
|
console.log(" World: skipped");
|
|
}
|
|
|
|
try {
|
|
if (!runWorldTick) throw null; // skip to catch
|
|
const tickResult = await executeGameTick(false);
|
|
worldResult = {
|
|
postsCreated: tickResult.postsCreated ?? 0,
|
|
eventsCreated: tickResult.eventsCreated ?? 0,
|
|
marketsUpdated: tickResult.marketsUpdated ?? 0,
|
|
questionsCreated: tickResult.questionsCreated ?? 0,
|
|
};
|
|
|
|
// Write world tick result
|
|
writeJson(
|
|
path.join(
|
|
dirs.worldTicks,
|
|
`cycle-${String(cycle).padStart(4, "0")}.json`,
|
|
),
|
|
{
|
|
cycle,
|
|
timestamp: new Date().toISOString(),
|
|
durationMs: Date.now() - worldStart,
|
|
result: tickResult,
|
|
},
|
|
);
|
|
|
|
console.log(
|
|
` World: posts=${worldResult.postsCreated} events=${worldResult.eventsCreated} ` +
|
|
`markets=${worldResult.marketsUpdated} questions=${worldResult.questionsCreated} ` +
|
|
`(${Date.now() - worldStart}ms)`,
|
|
);
|
|
} catch (err) {
|
|
if (err !== null) {
|
|
console.error(` World tick FAILED: ${err}`);
|
|
writeJson(
|
|
path.join(
|
|
dirs.worldTicks,
|
|
`cycle-${String(cycle).padStart(4, "0")}.json`,
|
|
),
|
|
{ cycle, error: String(err), timestamp: new Date().toISOString() },
|
|
);
|
|
}
|
|
}
|
|
const worldDurationMs = Date.now() - worldStart;
|
|
|
|
// --- Agent tick round ---
|
|
const agentStart = Date.now();
|
|
const agentResults: AgentTickResult[] = [];
|
|
const cycleTrajectoryIds: string[] = [];
|
|
|
|
const roundDir = path.join(
|
|
dirs.agentTicks,
|
|
`round-${String(cycle).padStart(4, "0")}`,
|
|
);
|
|
ensureDir(roundDir);
|
|
|
|
for (
|
|
let batchStart = 0;
|
|
batchStart < roster.length;
|
|
batchStart += opts.parallel
|
|
) {
|
|
const batch = roster.slice(batchStart, batchStart + opts.parallel);
|
|
|
|
const batchResults = await Promise.all(
|
|
batch.map(async (sheet) => {
|
|
const agentId = idByCharacterId.get(sheet.id);
|
|
if (!agentId) {
|
|
return {
|
|
agentId: "",
|
|
username: sheet.username,
|
|
characterId: sheet.id,
|
|
success: false,
|
|
error: `No agent created for ${sheet.id}`,
|
|
durationMs: 0,
|
|
} satisfies AgentTickResult;
|
|
}
|
|
|
|
const tickStart = Date.now();
|
|
try {
|
|
const runtime = await agentRuntimeManager.getRuntime(agentId);
|
|
applySheetToRuntime(runtime, sheet);
|
|
|
|
const result = await autonomousCoordinator.executeAutonomousTick(
|
|
agentId,
|
|
runtime,
|
|
true, // capture trajectory
|
|
);
|
|
|
|
const tickResult: AgentTickResult = {
|
|
agentId,
|
|
username: sheet.username,
|
|
characterId: sheet.id,
|
|
success: result.success,
|
|
trajectoryId: result.trajectoryId,
|
|
error: result.error,
|
|
durationMs: Date.now() - tickStart,
|
|
};
|
|
|
|
if (result.trajectoryId) {
|
|
cycleTrajectoryIds.push(result.trajectoryId);
|
|
}
|
|
|
|
// Write individual agent tick result
|
|
writeJson(path.join(roundDir, `${sheet.username}.json`), {
|
|
...tickResult,
|
|
characterSheet: sheet.id,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
return tickResult;
|
|
} catch (err) {
|
|
const tickResult: AgentTickResult = {
|
|
agentId,
|
|
username: sheet.username,
|
|
characterId: sheet.id,
|
|
success: false,
|
|
error: err instanceof Error ? err.message : String(err),
|
|
durationMs: Date.now() - tickStart,
|
|
};
|
|
writeJson(path.join(roundDir, `${sheet.username}.json`), {
|
|
...tickResult,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
return tickResult;
|
|
}
|
|
}),
|
|
);
|
|
|
|
agentResults.push(...batchResults);
|
|
|
|
if (batchStart + opts.parallel < roster.length && opts.delayMs > 0) {
|
|
await sleep(opts.delayMs);
|
|
}
|
|
}
|
|
|
|
const agentSuccessful = agentResults.filter((r) => r.success).length;
|
|
const agentWithTrajectory = agentResults.filter(
|
|
(r) => r.trajectoryId,
|
|
).length;
|
|
const agentDurationMs = Date.now() - agentStart;
|
|
|
|
console.log(
|
|
` Agents: ${agentSuccessful}/${agentResults.length} ok, ` +
|
|
`${agentWithTrajectory} trajectories (${agentDurationMs}ms)`,
|
|
);
|
|
|
|
allTrajectoryIds.push(...cycleTrajectoryIds);
|
|
|
|
// Cycle summary
|
|
const llmCallsInCycle = llmCallSequence - llmCallsBefore;
|
|
const summary: CycleSummary = {
|
|
cycleNumber: cycle,
|
|
worldTick: { durationMs: worldDurationMs, ...worldResult },
|
|
agentRound: {
|
|
durationMs: agentDurationMs,
|
|
total: agentResults.length,
|
|
successful: agentSuccessful,
|
|
failed: agentResults.length - agentSuccessful,
|
|
trajectoriesCaptured: agentWithTrajectory,
|
|
results: agentResults,
|
|
},
|
|
llmCallsInCycle,
|
|
};
|
|
cycleSummaries.push(summary);
|
|
|
|
console.log(` LLM calls this cycle: ${llmCallsInCycle}`);
|
|
console.log("");
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Export trajectories from DB
|
|
// -----------------------------------------------------------------------
|
|
console.log("Exporting trajectory data from database...");
|
|
|
|
if (allTrajectoryIds.length > 0) {
|
|
try {
|
|
const trajectoryRows = await db
|
|
.select()
|
|
.from(trajectories)
|
|
.where(inArray(trajectories.trajectoryId, allTrajectoryIds))
|
|
.orderBy(desc(trajectories.createdAt));
|
|
|
|
for (const row of trajectoryRows) {
|
|
const username = usernameByAgentId.get(row.agentId) ?? row.agentId;
|
|
writeJson(
|
|
path.join(dirs.trajectories, `${username}-${row.trajectoryId}.json`),
|
|
row,
|
|
);
|
|
appendJsonl(jsonlFiles.trajectoriesAll, row);
|
|
}
|
|
console.log(` Trajectories exported: ${trajectoryRows.length}`);
|
|
|
|
// Export linked LLM call logs from DB
|
|
const dbLlmCalls = await db
|
|
.select()
|
|
.from(llmCallLogs)
|
|
.where(inArray(llmCallLogs.trajectoryId, allTrajectoryIds))
|
|
.orderBy(desc(llmCallLogs.createdAt));
|
|
|
|
if (dbLlmCalls.length > 0) {
|
|
const dbLlmCallsFile = path.join(dirs.root, "db-llm-call-logs.jsonl");
|
|
for (const row of dbLlmCalls) {
|
|
appendJsonl(dbLlmCallsFile, row);
|
|
}
|
|
console.log(` DB LLM call logs exported: ${dbLlmCalls.length}`);
|
|
}
|
|
} catch (err) {
|
|
console.error(` Failed to export trajectories: ${err}`);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Extract narratives from trajectories + DB
|
|
// -----------------------------------------------------------------------
|
|
console.log("Extracting narratives...");
|
|
|
|
let narrativePosts = 0;
|
|
let narrativeTrades = 0;
|
|
let narrativeActions = 0;
|
|
|
|
if (allTrajectoryIds.length > 0) {
|
|
try {
|
|
// Extract actions from trajectory stepsJson
|
|
const trajectoryRows = await db
|
|
.select({
|
|
stepsJson: trajectories.stepsJson,
|
|
agentId: trajectories.agentId,
|
|
trajectoryId: trajectories.trajectoryId,
|
|
})
|
|
.from(trajectories)
|
|
.where(inArray(trajectories.trajectoryId, allTrajectoryIds));
|
|
|
|
for (const row of trajectoryRows) {
|
|
try {
|
|
const steps = JSON.parse(row.stepsJson);
|
|
const username = usernameByAgentId.get(row.agentId) ?? row.agentId;
|
|
for (const step of steps) {
|
|
const action = step.action;
|
|
if (!action) continue;
|
|
|
|
narrativeActions++;
|
|
appendJsonl(jsonlFiles.agentActions, {
|
|
trajectoryId: row.trajectoryId,
|
|
agentId: row.agentId,
|
|
username,
|
|
stepNumber: step.stepNumber,
|
|
actionType: action.actionType,
|
|
success: action.success,
|
|
reasoning: action.reasoning,
|
|
parameters: action.parameters,
|
|
result: action.result,
|
|
reward: step.reward,
|
|
timestamp: step.timestamp,
|
|
});
|
|
|
|
if (action.actionType === "TRADE" && action.success) {
|
|
narrativeTrades++;
|
|
appendJsonl(jsonlFiles.trades, {
|
|
agentId: row.agentId,
|
|
username,
|
|
marketId: action.parameters?.marketId,
|
|
side: action.parameters?.side,
|
|
amount: action.parameters?.amount,
|
|
shares: action.result?.shares,
|
|
reasoning: action.reasoning,
|
|
timestamp: step.timestamp,
|
|
});
|
|
}
|
|
|
|
if (action.actionType === "POST" && action.success) {
|
|
narrativePosts++;
|
|
appendJsonl(jsonlFiles.posts, {
|
|
agentId: row.agentId,
|
|
username,
|
|
content: action.parameters?.content,
|
|
postId: action.result?.postId,
|
|
timestamp: step.timestamp,
|
|
});
|
|
}
|
|
}
|
|
} catch (parseErr) {
|
|
console.warn(
|
|
` Skipped malformed stepsJson for trajectory ${row.trajectoryId}: ${parseErr}`,
|
|
);
|
|
}
|
|
}
|
|
console.log(
|
|
` Actions: ${narrativeActions}, Trades: ${narrativeTrades}, Posts: ${narrativePosts}`,
|
|
);
|
|
} catch (err) {
|
|
console.error(` Failed to extract narratives: ${err}`);
|
|
}
|
|
|
|
// Extract world events created during this run
|
|
try {
|
|
const eventRows = await db
|
|
.select()
|
|
.from(worldEvents)
|
|
.where(gte(worldEvents.createdAt, runStartedAt))
|
|
.orderBy(desc(worldEvents.createdAt));
|
|
|
|
for (const row of eventRows) {
|
|
appendJsonl(jsonlFiles.events, row);
|
|
}
|
|
if (eventRows.length > 0) {
|
|
console.log(` World events: ${eventRows.length}`);
|
|
}
|
|
} catch {
|
|
/* worldEvents table may not exist */
|
|
}
|
|
|
|
// Extract posts created during this run
|
|
try {
|
|
const postRows = await db
|
|
.select()
|
|
.from(posts)
|
|
.where(gte(posts.createdAt, runStartedAt))
|
|
.orderBy(desc(posts.createdAt));
|
|
|
|
for (const row of postRows) {
|
|
appendJsonl(jsonlFiles.posts, row);
|
|
}
|
|
if (postRows.length > 0) {
|
|
console.log(` World posts: ${postRows.length}`);
|
|
}
|
|
} catch {
|
|
/* posts table may not exist */
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Write agent LLM calls from DB as individual files too
|
|
// -----------------------------------------------------------------------
|
|
let dbAgentLlmCallCount = 0;
|
|
const agentLlmModels: Record<string, number> = {};
|
|
const agentLlmPurposes: Record<string, number> = {};
|
|
let agentLlmTotalTokens = 0;
|
|
if (allTrajectoryIds.length > 0) {
|
|
try {
|
|
const agentLlmDir = path.join(dirs.llmCalls, "agent");
|
|
ensureDir(agentLlmDir);
|
|
const dbCalls = await db
|
|
.select()
|
|
.from(llmCallLogs)
|
|
.where(inArray(llmCallLogs.trajectoryId, allTrajectoryIds))
|
|
.orderBy(llmCallLogs.timestamp);
|
|
|
|
for (const call of dbCalls) {
|
|
dbAgentLlmCallCount++;
|
|
const model = call.model ?? "unknown";
|
|
const purpose = call.purpose ?? call.actionType ?? "unknown";
|
|
agentLlmModels[model] = (agentLlmModels[model] || 0) + 1;
|
|
agentLlmPurposes[purpose] = (agentLlmPurposes[purpose] || 0) + 1;
|
|
agentLlmTotalTokens +=
|
|
(call.promptTokens ?? 0) + (call.completionTokens ?? 0);
|
|
writeJson(
|
|
path.join(
|
|
agentLlmDir,
|
|
`${String(dbAgentLlmCallCount).padStart(6, "0")}-${purpose}.json`,
|
|
),
|
|
call,
|
|
);
|
|
}
|
|
if (dbAgentLlmCallCount > 0) {
|
|
console.log(
|
|
` Agent LLM calls written as individual files: ${dbAgentLlmCallCount}`,
|
|
);
|
|
}
|
|
} catch (err) {
|
|
console.error(` Failed to write agent LLM call files: ${err}`);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Restore callback
|
|
// -----------------------------------------------------------------------
|
|
setLLMCallCallback(priorCallback);
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Write manifest and summary
|
|
// -----------------------------------------------------------------------
|
|
const runCompletedAt = new Date();
|
|
const totalDurationMs = runCompletedAt.getTime() - runStartedAt.getTime();
|
|
|
|
const totalSuccessful = cycleSummaries.reduce(
|
|
(s, c) => s + c.agentRound.successful,
|
|
0,
|
|
);
|
|
const totalFailed = cycleSummaries.reduce(
|
|
(s, c) => s + c.agentRound.failed,
|
|
0,
|
|
);
|
|
const totalTrajectories = allTrajectoryIds.length;
|
|
|
|
const manifest = {
|
|
version: "1.0.0",
|
|
generatedAt: runCompletedAt.toISOString(),
|
|
startedAt: runStartedAt.toISOString(),
|
|
totalDurationMs,
|
|
totalDurationHuman: `${Math.floor(totalDurationMs / 60000)}m ${Math.floor((totalDurationMs % 60000) / 1000)}s`,
|
|
config: {
|
|
hoursRequested: opts.hours,
|
|
totalCycles: opts.ticks,
|
|
ticksPerHour: opts.ticksPerHour,
|
|
parallel: opts.parallel,
|
|
delayMs: opts.delayMs,
|
|
},
|
|
stats: {
|
|
characters: roster.length,
|
|
totalCycles: cycleSummaries.length,
|
|
totalAgentTickAttempts: totalSuccessful + totalFailed,
|
|
totalAgentTickSuccesses: totalSuccessful,
|
|
totalAgentTickFailures: totalFailed,
|
|
totalTrajectoriesCaptured: totalTrajectories,
|
|
totalLLMCalls: llmCallSequence + dbAgentLlmCallCount,
|
|
engineLLMCalls: llmCallSequence,
|
|
agentLLMCalls: dbAgentLlmCallCount,
|
|
totalTokens: engineTokens + agentLlmTotalTokens,
|
|
engineTokens,
|
|
agentTokens: agentLlmTotalTokens,
|
|
narratives: {
|
|
totalActions: narrativeActions,
|
|
trades: narrativeTrades,
|
|
posts: narrativePosts,
|
|
},
|
|
engineLlmByPromptType: Object.fromEntries(
|
|
Object.entries(enginePromptTypes).sort(([, a], [, b]) => b - a),
|
|
),
|
|
agentLlmByPurpose: Object.fromEntries(
|
|
Object.entries(agentLlmPurposes).sort(([, a], [, b]) => b - a),
|
|
),
|
|
llmCallsByModel: Object.fromEntries(
|
|
Object.entries({
|
|
...engineModels,
|
|
...Object.fromEntries(
|
|
Object.entries(agentLlmModels).map(([k, v]) => [`${k} (agent)`, v]),
|
|
),
|
|
}).sort(([, a], [, b]) => b - a),
|
|
),
|
|
},
|
|
outputStructure: {
|
|
"actors/": "Character sheets and config for each agent",
|
|
"world-ticks/": "World tick results per cycle (posts, events, markets)",
|
|
"agent-ticks/":
|
|
"Per-agent tick results per cycle (trajectory IDs, success/fail)",
|
|
"llm-calls/":
|
|
"Individual JSON file for EVERY engine LLM call (full prompt + response)",
|
|
"llm-calls/agent/":
|
|
"Individual JSON file for EVERY agent LLM call (from trajectory DB)",
|
|
"llm-calls-all.jsonl":
|
|
"All LLM calls in JSONL (one per line, for grep/analysis)",
|
|
"trajectories/": "Full trajectory records exported from DB",
|
|
"trajectories-all.jsonl": "All trajectories in JSONL",
|
|
"narratives/": "Posts, events, trades, agent actions",
|
|
"db-llm-call-logs.jsonl":
|
|
"LLM call logs from DB (linked to trajectories)",
|
|
"cycles.json": "Per-cycle summary with timing and counts",
|
|
"manifest.json": "This file - run metadata and aggregate stats",
|
|
},
|
|
};
|
|
|
|
writeJson(path.join(dirs.root, "manifest.json"), manifest);
|
|
writeJson(path.join(dirs.root, "cycles.json"), cycleSummaries);
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Final report
|
|
// -----------------------------------------------------------------------
|
|
console.log("=".repeat(72));
|
|
console.log(" SIMULATION COMPLETE");
|
|
console.log("=".repeat(72));
|
|
console.log(` Duration : ${manifest.totalDurationHuman}`);
|
|
console.log(` Cycles completed : ${cycleSummaries.length}`);
|
|
console.log(` Characters : ${roster.length}`);
|
|
console.log(
|
|
` Agent ticks : ${totalSuccessful} ok / ${totalFailed} failed`,
|
|
);
|
|
console.log(` Trajectories : ${totalTrajectories}`);
|
|
console.log(` LLM calls total : ${llmCallSequence + dbAgentLlmCallCount}`);
|
|
console.log(` Engine (world) : ${llmCallSequence}`);
|
|
console.log(` Agent (DB) : ${dbAgentLlmCallCount}`);
|
|
console.log(
|
|
` Total tokens : ${manifest.stats.totalTokens.toLocaleString()}`,
|
|
);
|
|
console.log(
|
|
` Actions : ${narrativeActions} (${narrativeTrades} trades, ${narrativePosts} posts)`,
|
|
);
|
|
console.log("");
|
|
console.log(` Output: ${opts.outputDir}`);
|
|
console.log("=".repeat(72));
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error("Simulation failed:", err);
|
|
process.exit(1);
|
|
});
|