Add demo operator scaffold and CLI
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
43
README.md
43
README.md
@@ -1,8 +1,41 @@
|
|||||||

|
# BlackRoad OS Demo
|
||||||
|
|
||||||

|
A safe **playground and showcase** for BlackRoad OS patterns. This repository runs a miniature operator, example agents, and a CLI so you can experiment without touching production systems or real data.
|
||||||
|
|
||||||
# Welcome to your organization's demo respository
|
## Quick start
|
||||||
This code repository (or "repo") is designed to demonstrate the best GitHub has to offer with the least amount of noise.
|
|
||||||
|
|
||||||
The repo includes an `index.html` file (so it can render a web page), two GitHub Actions workflows, and a CSS stylesheet dependency.
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run demo
|
||||||
|
```
|
||||||
|
|
||||||
|
Type `help` inside the CLI for available commands:
|
||||||
|
|
||||||
|
- `echo <message>`
|
||||||
|
- `inc`
|
||||||
|
- `tx in|out <amount>`
|
||||||
|
- `fact <key> <true|false>`
|
||||||
|
- `summary`
|
||||||
|
- `journal`
|
||||||
|
|
||||||
|
## What this demo includes
|
||||||
|
|
||||||
|
- **Mini operator**: In-memory event bus and journal inspired by the real BlackRoad OS operator.
|
||||||
|
- **Demo agents**: Echo, counter, finance, and contradiction detectors.
|
||||||
|
- **Examples**: Sample events and finance scenarios in `examples/` and `config/demo-config.json` for reference.
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
|
||||||
|
- [Overview](docs/overview.md)
|
||||||
|
- [Adding an agent](docs/adding-an-agent.md)
|
||||||
|
- [Demo scenarios](docs/demo-scenarios.md)
|
||||||
|
|
||||||
|
## Safety notice
|
||||||
|
|
||||||
|
This repo is for demonstrations only. It contains no real secrets or production integrations. Journals and state live only in memory while the process is running.
|
||||||
|
|
||||||
|
## Future TODOs
|
||||||
|
|
||||||
|
- Minimal web UI for showing event streams.
|
||||||
|
- Dockerfile for quick spin-up.
|
||||||
|
- Optional integration with the real `blackroad-os-operator` backend once available.
|
||||||
|
|||||||
7
config/demo-config.json
Normal file
7
config/demo-config.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"description": "Local demo configuration for BlackRoad OS Demo",
|
||||||
|
"defaultAgents": ["echo", "counter", "demo.finance", "contradiction"],
|
||||||
|
"logging": {
|
||||||
|
"level": "debug"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
docs/adding-an-agent.md
Normal file
13
docs/adding-an-agent.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Adding a new demo agent
|
||||||
|
|
||||||
|
This repo mirrors the shape of the real BlackRoad OS operator. Follow these steps to add your own demo agent.
|
||||||
|
|
||||||
|
1. **Create the agent file** in `src/agents/` and implement the `DemoAgent` interface.
|
||||||
|
- Subscribe to relevant event types inside `init` using the `ctx.bus.subscribe` method.
|
||||||
|
- Use `ctx.journal.journal` to record actions and `ctx.log` for console diagnostics.
|
||||||
|
2. **Register the agent** in `src/demo-operator/demoRegistry.ts` so it gets constructed and initialized when the demo starts.
|
||||||
|
3. **Wire commands (optional)** in `src/cli/runDemo.ts` to make it easy to trigger your agent from the CLI.
|
||||||
|
4. **Run the demo** with `npm run demo` and try your new events.
|
||||||
|
5. **Inspect the journal** with the `journal` command to confirm your agent is journaling the expected entries.
|
||||||
|
|
||||||
|
Use the existing `EchoAgent`, `CounterAgent`, and `DemoFinanceAgent` for concrete patterns to copy.
|
||||||
28
docs/demo-scenarios.md
Normal file
28
docs/demo-scenarios.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Demo scenarios
|
||||||
|
|
||||||
|
Use these guided flows to exercise the demo operator.
|
||||||
|
|
||||||
|
## Finance mini-close
|
||||||
|
|
||||||
|
1. Start the CLI: `npm run demo`.
|
||||||
|
2. Send a few transactions:
|
||||||
|
- `tx in 500`
|
||||||
|
- `tx out 150`
|
||||||
|
- `tx out 50`
|
||||||
|
- `tx in 200`
|
||||||
|
3. Run `summary` to view the current finance snapshot.
|
||||||
|
4. Inspect `journal` to see the transaction trail and hashes.
|
||||||
|
|
||||||
|
Expected final cash balance matches the `examples/finance-scenarios/basic-close.json` scenario: **500**.
|
||||||
|
|
||||||
|
## Contradiction detection
|
||||||
|
|
||||||
|
1. From the CLI, send `fact sky true`.
|
||||||
|
2. Send `fact sky false`.
|
||||||
|
3. The `ContradictionAgent` will journal the conflict and emit a `demo.contradiction` event.
|
||||||
|
4. Run `journal` to see both the recorded fact and contradiction entries.
|
||||||
|
|
||||||
|
## Echo and counter basics
|
||||||
|
|
||||||
|
- `echo Hello, BlackRoad!` triggers the echo agent and emits a `demo.echo.response`.
|
||||||
|
- `inc` increments the counter and emits `counter.updated`. Run `summary` to check the count alongside finance data.
|
||||||
30
docs/overview.md
Normal file
30
docs/overview.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# BlackRoad OS Demo
|
||||||
|
|
||||||
|
This repository hosts a **safe, in-memory playground** for experimenting with BlackRoad OS concepts. It mirrors the shapes of the real operator and finance layer without touching production systems or secrets.
|
||||||
|
|
||||||
|
## What lives here
|
||||||
|
|
||||||
|
- A mini in-process operator with an in-memory event bus and journal.
|
||||||
|
- Example agents (echo, counter, finance, contradiction) that subscribe to demo events.
|
||||||
|
- A CLI (`npm run demo`) to publish events, inspect state, and view journal entries.
|
||||||
|
- Example event payloads and finance scenarios you can replay.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run demo
|
||||||
|
```
|
||||||
|
|
||||||
|
The CLI will start and present a prompt. Type `help` to see available commands.
|
||||||
|
|
||||||
|
## Included demo agents
|
||||||
|
|
||||||
|
- **EchoAgent**: responds to `demo.echo` events and emits `demo.echo.response`.
|
||||||
|
- **CounterAgent**: increments on `counter.increment` and emits `counter.updated`.
|
||||||
|
- **DemoFinanceAgent**: tracks simple cash balances from `finance.transaction` events and emits `finance.summary.updated`.
|
||||||
|
- **ContradictionAgent**: detects conflicting `demo.fact` events and emits `demo.contradiction`.
|
||||||
|
|
||||||
|
## Safety
|
||||||
|
|
||||||
|
Everything runs locally, in memory, and uses toy data only. Journaled entries are not persisted outside the process. Treat this repository as a sandbox for experimenting with BlackRoad OS patterns.
|
||||||
16
examples/events.json
Normal file
16
examples/events.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "evt-1",
|
||||||
|
"type": "demo.echo",
|
||||||
|
"source": "example",
|
||||||
|
"timestamp": "2025-01-01T00:00:00Z",
|
||||||
|
"payload": { "message": "Hello from example!" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "evt-2",
|
||||||
|
"type": "finance.transaction",
|
||||||
|
"source": "example",
|
||||||
|
"timestamp": "2025-01-01T00:00:01Z",
|
||||||
|
"payload": { "type": "in", "amount": 1000 }
|
||||||
|
}
|
||||||
|
]
|
||||||
11
examples/finance-scenarios/basic-close.json
Normal file
11
examples/finance-scenarios/basic-close.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "basic-close",
|
||||||
|
"description": "Toy finance close with a few transactions",
|
||||||
|
"events": [
|
||||||
|
{ "type": "finance.transaction", "payload": { "type": "in", "amount": 500 } },
|
||||||
|
{ "type": "finance.transaction", "payload": { "type": "out", "amount": 150 } },
|
||||||
|
{ "type": "finance.transaction", "payload": { "type": "out", "amount": 50 } },
|
||||||
|
{ "type": "finance.transaction", "payload": { "type": "in", "amount": 200 } }
|
||||||
|
],
|
||||||
|
"expectedFinalCashBalance": 500
|
||||||
|
}
|
||||||
8
jest.config.cjs
Normal file
8
jest.config.cjs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('jest').Config} */
|
||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
roots: ['<rootDir>/tests'],
|
||||||
|
moduleFileExtensions: ['ts', 'js', 'json'],
|
||||||
|
clearMocks: true
|
||||||
|
};
|
||||||
4023
package-lock.json
generated
Normal file
4023
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -1,9 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "demo-repo",
|
"name": "blackroad-os-demo",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"description": "A sample package.json",
|
"description": "Demo, playground, and example hub for BlackRoad OS",
|
||||||
|
"scripts": {
|
||||||
|
"demo": "ts-node src/cli/runDemo.ts",
|
||||||
|
"dev:demo": "ts-node src/cli/runDemo.ts",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@primer/css": "17.0.1"
|
"@primer/css": "17.0.1"
|
||||||
},
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/jest": "29.5.12",
|
||||||
|
"@types/node": "22.7.4",
|
||||||
|
"jest": "29.7.0",
|
||||||
|
"ts-jest": "29.2.5",
|
||||||
|
"ts-node": "10.9.2",
|
||||||
|
"typescript": "5.5.4"
|
||||||
|
},
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
|
|||||||
51
src/agents/contradictionAgent.ts
Normal file
51
src/agents/contradictionAgent.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
import { DemoAgent, DemoAgentContext } from "../demo-operator/demoAgentContext";
|
||||||
|
import { DemoEvent } from "../demo-operator/demoEventBus";
|
||||||
|
|
||||||
|
type FactValue = "true" | "false";
|
||||||
|
|
||||||
|
export class ContradictionAgent implements DemoAgent {
|
||||||
|
id = "demo.contradiction";
|
||||||
|
private ctx: DemoAgentContext | undefined;
|
||||||
|
private beliefs: Record<string, FactValue> = {};
|
||||||
|
|
||||||
|
async init(ctx: DemoAgentContext): Promise<void> {
|
||||||
|
this.ctx = ctx;
|
||||||
|
ctx.bus.subscribe("demo.fact", (event) => this.onEvent(event));
|
||||||
|
ctx.log("ContradictionAgent initialized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
async onEvent(event: DemoEvent): Promise<void> {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const payload = event.payload as { key: string; value: FactValue };
|
||||||
|
if (!payload || typeof payload.key !== "string" || (payload.value !== "true" && payload.value !== "false")) {
|
||||||
|
this.ctx.log("ContradictionAgent received invalid payload", payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = this.beliefs[payload.key];
|
||||||
|
if (existing && existing !== payload.value) {
|
||||||
|
const journalEntry = this.ctx.journal.journal(this.id, "contradiction", {
|
||||||
|
key: payload.key,
|
||||||
|
previous: existing,
|
||||||
|
incoming: payload.value
|
||||||
|
});
|
||||||
|
this.ctx.log("ContradictionAgent detected contradiction", journalEntry);
|
||||||
|
|
||||||
|
await this.ctx.bus.publish({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: "demo.contradiction",
|
||||||
|
source: this.id,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
payload: {
|
||||||
|
key: payload.key,
|
||||||
|
previous: existing,
|
||||||
|
incoming: payload.value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.beliefs[payload.key] = payload.value;
|
||||||
|
this.ctx.journal.journal(this.id, "fact.recorded", payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/agents/counterAgent.ts
Normal file
38
src/agents/counterAgent.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
import { DemoAgent, DemoAgentContext } from "../demo-operator/demoAgentContext";
|
||||||
|
import { DemoEvent } from "../demo-operator/demoEventBus";
|
||||||
|
|
||||||
|
export class CounterAgent implements DemoAgent {
|
||||||
|
id = "demo.counter";
|
||||||
|
private ctx: DemoAgentContext | undefined;
|
||||||
|
private count = 0;
|
||||||
|
|
||||||
|
getCount(): number {
|
||||||
|
return this.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(ctx: DemoAgentContext): Promise<void> {
|
||||||
|
this.ctx = ctx;
|
||||||
|
ctx.bus.subscribe("counter.increment", (event) => this.onEvent(event));
|
||||||
|
ctx.log("CounterAgent initialized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
async onEvent(event: DemoEvent): Promise<void> {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
this.count += 1;
|
||||||
|
const journalEntry = this.ctx.journal.journal(this.id, "increment", {
|
||||||
|
previous: this.count - 1,
|
||||||
|
new: this.count,
|
||||||
|
payload: event.payload
|
||||||
|
});
|
||||||
|
this.ctx.log("CounterAgent updated count", journalEntry);
|
||||||
|
|
||||||
|
await this.ctx.bus.publish({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: "counter.updated",
|
||||||
|
source: this.id,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
payload: { count: this.count }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/agents/demoFinanceAgent.ts
Normal file
60
src/agents/demoFinanceAgent.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
import { DemoAgent, DemoAgentContext } from "../demo-operator/demoAgentContext";
|
||||||
|
import { DemoEvent } from "../demo-operator/demoEventBus";
|
||||||
|
|
||||||
|
export interface FinanceSummary {
|
||||||
|
cashBalance: number;
|
||||||
|
totalIn: number;
|
||||||
|
totalOut: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DemoFinanceAgent implements DemoAgent {
|
||||||
|
id = "demo.finance";
|
||||||
|
private ctx: DemoAgentContext | undefined;
|
||||||
|
private summary: FinanceSummary = {
|
||||||
|
cashBalance: 0,
|
||||||
|
totalIn: 0,
|
||||||
|
totalOut: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
getSummary(): FinanceSummary {
|
||||||
|
return { ...this.summary };
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(ctx: DemoAgentContext): Promise<void> {
|
||||||
|
this.ctx = ctx;
|
||||||
|
ctx.bus.subscribe("finance.transaction", (event) => this.onEvent(event));
|
||||||
|
ctx.log("DemoFinanceAgent initialized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
async onEvent(event: DemoEvent): Promise<void> {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
const payload = event.payload as { amount: number; type: "in" | "out" };
|
||||||
|
if (!payload || typeof payload.amount !== "number" || (payload.type !== "in" && payload.type !== "out")) {
|
||||||
|
this.ctx.log("DemoFinanceAgent received invalid payload", payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.type === "in") {
|
||||||
|
this.summary.totalIn += payload.amount;
|
||||||
|
this.summary.cashBalance += payload.amount;
|
||||||
|
} else {
|
||||||
|
this.summary.totalOut += payload.amount;
|
||||||
|
this.summary.cashBalance -= payload.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
const journalEntry = this.ctx.journal.journal(this.id, "finance.transaction", {
|
||||||
|
...payload,
|
||||||
|
snapshot: this.getSummary()
|
||||||
|
});
|
||||||
|
this.ctx.log("DemoFinanceAgent recorded transaction", journalEntry);
|
||||||
|
|
||||||
|
await this.ctx.bus.publish({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: "finance.summary.updated",
|
||||||
|
source: this.id,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
payload: this.getSummary()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/agents/echoAgent.ts
Normal file
29
src/agents/echoAgent.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
import { DemoAgent, DemoAgentContext } from "../demo-operator/demoAgentContext";
|
||||||
|
import { DemoEvent } from "../demo-operator/demoEventBus";
|
||||||
|
|
||||||
|
export class EchoAgent implements DemoAgent {
|
||||||
|
id = "demo.echo";
|
||||||
|
private ctx: DemoAgentContext | undefined;
|
||||||
|
|
||||||
|
async init(ctx: DemoAgentContext): Promise<void> {
|
||||||
|
this.ctx = ctx;
|
||||||
|
ctx.bus.subscribe("demo.echo", (event) => this.onEvent(event));
|
||||||
|
ctx.log("EchoAgent initialized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
async onEvent(event: DemoEvent): Promise<void> {
|
||||||
|
if (!this.ctx) return;
|
||||||
|
this.ctx.log(`EchoAgent received event ${event.id}`, event.payload);
|
||||||
|
const journalEntry = this.ctx.journal.journal(this.id, "received", event.payload);
|
||||||
|
this.ctx.log("EchoAgent journaled", journalEntry);
|
||||||
|
|
||||||
|
await this.ctx.bus.publish({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: "demo.echo.response",
|
||||||
|
source: this.id,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
payload: { originalId: event.id, message: event.payload }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
130
src/cli/runDemo.ts
Normal file
130
src/cli/runDemo.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
import readline from "readline";
|
||||||
|
import { startDemo } from "../demo-operator/demoRunner";
|
||||||
|
import { DemoFinanceAgent } from "../agents/demoFinanceAgent";
|
||||||
|
import { CounterAgent } from "../agents/counterAgent";
|
||||||
|
import { DemoAgent } from "../demo-operator/demoAgentContext";
|
||||||
|
|
||||||
|
function printHelp() {
|
||||||
|
console.log(`Available commands:
|
||||||
|
echo <message> - send demo.echo event
|
||||||
|
inc - increment counter
|
||||||
|
tx in <amount> - record incoming finance transaction
|
||||||
|
tx out <amount> - record outgoing finance transaction
|
||||||
|
fact <key> <true|false> - record a fact (contradiction demo)
|
||||||
|
summary - print finance summary and counter
|
||||||
|
journal - print journal entries
|
||||||
|
help - show this help
|
||||||
|
exit | quit - stop the demo`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const { bus, journal, agents } = await startDemo();
|
||||||
|
const finance = agents.find((a): a is DemoFinanceAgent => (a as DemoAgent).id === "demo.finance") as
|
||||||
|
| DemoFinanceAgent
|
||||||
|
| undefined;
|
||||||
|
const counter = agents.find((a): a is CounterAgent => (a as DemoAgent).id === "demo.counter") as
|
||||||
|
| CounterAgent
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
console.log("BlackRoad OS Demo ready. Type 'help' for commands.");
|
||||||
|
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
prompt: "blackroad-demo> "
|
||||||
|
});
|
||||||
|
|
||||||
|
rl.prompt();
|
||||||
|
|
||||||
|
rl.on("line", async (line) => {
|
||||||
|
const [cmd, ...rest] = line.trim().split(/\s+/);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (cmd === "echo") {
|
||||||
|
const msg = rest.join(" ");
|
||||||
|
await bus.publish({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: "demo.echo",
|
||||||
|
source: "cli",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
payload: { message: msg }
|
||||||
|
});
|
||||||
|
} else if (cmd === "inc") {
|
||||||
|
await bus.publish({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: "counter.increment",
|
||||||
|
source: "cli",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
payload: { reason: "manual" }
|
||||||
|
});
|
||||||
|
if (counter) {
|
||||||
|
console.log(`Counter is now ${counter.getCount()}`);
|
||||||
|
}
|
||||||
|
} else if (cmd === "tx") {
|
||||||
|
const direction = rest[0];
|
||||||
|
const amount = Number(rest[1]);
|
||||||
|
if ((direction === "in" || direction === "out") && !Number.isNaN(amount)) {
|
||||||
|
await bus.publish({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: "finance.transaction",
|
||||||
|
source: "cli",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
payload: { type: direction, amount }
|
||||||
|
});
|
||||||
|
if (finance) {
|
||||||
|
console.log("Finance summary:", finance.getSummary());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("Usage: tx in|out <amount>");
|
||||||
|
}
|
||||||
|
} else if (cmd === "summary") {
|
||||||
|
if (finance) {
|
||||||
|
console.log("Finance summary:", finance.getSummary());
|
||||||
|
}
|
||||||
|
if (counter) {
|
||||||
|
console.log("Counter:", counter.getCount());
|
||||||
|
}
|
||||||
|
} else if (cmd === "fact") {
|
||||||
|
const key = rest[0];
|
||||||
|
const value = rest[1];
|
||||||
|
if (key && (value === "true" || value === "false")) {
|
||||||
|
await bus.publish({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: "demo.fact",
|
||||||
|
source: "cli",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
payload: { key, value }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("Usage: fact <key> <true|false>");
|
||||||
|
}
|
||||||
|
} else if (cmd === "journal") {
|
||||||
|
console.log(journal.getEntries());
|
||||||
|
} else if (cmd === "help" || cmd === "?") {
|
||||||
|
printHelp();
|
||||||
|
} else if (cmd === "exit" || cmd === "quit") {
|
||||||
|
rl.close();
|
||||||
|
return;
|
||||||
|
} else if (cmd === "") {
|
||||||
|
// ignore empty line
|
||||||
|
} else {
|
||||||
|
console.log("Unknown command. Type 'help' for options.");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error handling command:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.prompt();
|
||||||
|
});
|
||||||
|
|
||||||
|
rl.on("close", () => {
|
||||||
|
console.log("Exiting demo.");
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error("Failed to start demo:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
14
src/demo-operator/demoAgentContext.ts
Normal file
14
src/demo-operator/demoAgentContext.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { InMemoryDemoEventBus, DemoEvent } from "./demoEventBus";
|
||||||
|
import type { DemoJournal } from "./demoJournal";
|
||||||
|
|
||||||
|
export interface DemoAgentContext {
|
||||||
|
bus: InMemoryDemoEventBus;
|
||||||
|
journal: DemoJournal;
|
||||||
|
log: (msg: string, meta?: unknown) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DemoAgent {
|
||||||
|
id: string;
|
||||||
|
init(ctx: DemoAgentContext): Promise<void>;
|
||||||
|
onEvent(event: DemoEvent): Promise<void>;
|
||||||
|
}
|
||||||
31
src/demo-operator/demoEventBus.ts
Normal file
31
src/demo-operator/demoEventBus.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
export type DemoEventHandler = (event: DemoEvent) => Promise<void> | void;
|
||||||
|
|
||||||
|
export interface DemoEvent {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
source: string;
|
||||||
|
timestamp: string;
|
||||||
|
payload: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DemoEventBus {
|
||||||
|
publish(event: DemoEvent): Promise<void>;
|
||||||
|
subscribe(type: string, handler: DemoEventHandler): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InMemoryDemoEventBus implements DemoEventBus {
|
||||||
|
private handlers = new Map<string, DemoEventHandler[]>();
|
||||||
|
|
||||||
|
subscribe(type: string, handler: DemoEventHandler): void {
|
||||||
|
const arr = this.handlers.get(type) || [];
|
||||||
|
arr.push(handler);
|
||||||
|
this.handlers.set(type, arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
async publish(event: DemoEvent): Promise<void> {
|
||||||
|
const handlers = this.handlers.get(event.type) || [];
|
||||||
|
for (const h of handlers) {
|
||||||
|
await h(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/demo-operator/demoJournal.ts
Normal file
38
src/demo-operator/demoJournal.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
export interface DemoJournalEntry {
|
||||||
|
id: string;
|
||||||
|
timestamp: string;
|
||||||
|
actorId: string;
|
||||||
|
actionType: string;
|
||||||
|
payload: unknown;
|
||||||
|
previousHash?: string;
|
||||||
|
hash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DemoJournal {
|
||||||
|
private lastHash: string | undefined;
|
||||||
|
private entries: DemoJournalEntry[] = [];
|
||||||
|
|
||||||
|
journal(actorId: string, actionType: string, payload: unknown): DemoJournalEntry {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const previousHash = this.lastHash;
|
||||||
|
const hash = this.computeHash({ id, timestamp, actorId, actionType, payload, previousHash });
|
||||||
|
|
||||||
|
const entry: DemoJournalEntry = { id, timestamp, actorId, actionType, payload, previousHash, hash };
|
||||||
|
this.entries.push(entry);
|
||||||
|
this.lastHash = hash;
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeHash(obj: Omit<DemoJournalEntry, "hash">): string {
|
||||||
|
const h = crypto.createHash("sha256");
|
||||||
|
h.update(JSON.stringify(obj));
|
||||||
|
return h.digest("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
getEntries(): DemoJournalEntry[] {
|
||||||
|
return [...this.entries];
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/demo-operator/demoRegistry.ts
Normal file
9
src/demo-operator/demoRegistry.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { DemoAgent } from "./demoAgentContext";
|
||||||
|
import { EchoAgent } from "../agents/echoAgent";
|
||||||
|
import { CounterAgent } from "../agents/counterAgent";
|
||||||
|
import { DemoFinanceAgent } from "../agents/demoFinanceAgent";
|
||||||
|
import { ContradictionAgent } from "../agents/contradictionAgent";
|
||||||
|
|
||||||
|
export function createDemoAgents(): DemoAgent[] {
|
||||||
|
return [new EchoAgent(), new CounterAgent(), new DemoFinanceAgent(), new ContradictionAgent()];
|
||||||
|
}
|
||||||
28
src/demo-operator/demoRunner.ts
Normal file
28
src/demo-operator/demoRunner.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { InMemoryDemoEventBus } from "./demoEventBus";
|
||||||
|
import { DemoJournal } from "./demoJournal";
|
||||||
|
import { createDemoAgents } from "./demoRegistry";
|
||||||
|
|
||||||
|
export async function startDemo() {
|
||||||
|
const bus = new InMemoryDemoEventBus();
|
||||||
|
const journal = new DemoJournal();
|
||||||
|
|
||||||
|
const agents = createDemoAgents();
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
bus,
|
||||||
|
journal,
|
||||||
|
log: (msg: string, meta?: unknown) => {
|
||||||
|
if (meta) {
|
||||||
|
console.log(`[DEMO] ${msg}`, meta);
|
||||||
|
} else {
|
||||||
|
console.log(`[DEMO] ${msg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const agent of agents) {
|
||||||
|
await agent.init(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { bus, journal, agents };
|
||||||
|
}
|
||||||
30
src/demo-operator/index.ts
Normal file
30
src/demo-operator/index.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
import { startDemo } from "./demoRunner";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const { bus, journal } = await startDemo();
|
||||||
|
console.log("Demo operator started.");
|
||||||
|
|
||||||
|
await bus.publish({
|
||||||
|
id: "e1",
|
||||||
|
type: "demo.echo",
|
||||||
|
source: "demo-cli",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
payload: { message: "Hello, BlackRoad!" }
|
||||||
|
});
|
||||||
|
|
||||||
|
await bus.publish({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: "counter.increment",
|
||||||
|
source: "demo-cli",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
payload: { reason: "startup" }
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Initial journal:", journal.getEntries());
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error("Error running demo operator:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
40
tests/demoFinanceAgent.test.ts
Normal file
40
tests/demoFinanceAgent.test.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { DemoFinanceAgent } from "../src/agents/demoFinanceAgent";
|
||||||
|
import { InMemoryDemoEventBus } from "../src/demo-operator/demoEventBus";
|
||||||
|
import { DemoJournal } from "../src/demo-operator/demoJournal";
|
||||||
|
import { DemoAgentContext } from "../src/demo-operator/demoAgentContext";
|
||||||
|
|
||||||
|
describe("DemoFinanceAgent", () => {
|
||||||
|
it("tracks cash balance and totals", async () => {
|
||||||
|
const bus = new InMemoryDemoEventBus();
|
||||||
|
const journal = new DemoJournal();
|
||||||
|
const finance = new DemoFinanceAgent();
|
||||||
|
const ctx: DemoAgentContext = {
|
||||||
|
bus,
|
||||||
|
journal,
|
||||||
|
log: () => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
await finance.init(ctx);
|
||||||
|
|
||||||
|
await bus.publish({
|
||||||
|
id: "t1",
|
||||||
|
type: "finance.transaction",
|
||||||
|
source: "test",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
payload: { type: "in", amount: 200 }
|
||||||
|
});
|
||||||
|
|
||||||
|
await bus.publish({
|
||||||
|
id: "t2",
|
||||||
|
type: "finance.transaction",
|
||||||
|
source: "test",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
payload: { type: "out", amount: 50 }
|
||||||
|
});
|
||||||
|
|
||||||
|
const summary = finance.getSummary();
|
||||||
|
expect(summary.cashBalance).toBe(150);
|
||||||
|
expect(summary.totalIn).toBe(200);
|
||||||
|
expect(summary.totalOut).toBe(50);
|
||||||
|
});
|
||||||
|
});
|
||||||
19
tests/demoOperator.test.ts
Normal file
19
tests/demoOperator.test.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { startDemo } from "../src/demo-operator/demoRunner";
|
||||||
|
|
||||||
|
describe("demo operator", () => {
|
||||||
|
it("journals events from agents", async () => {
|
||||||
|
const { bus, journal } = await startDemo();
|
||||||
|
|
||||||
|
await bus.publish({
|
||||||
|
id: "test-echo",
|
||||||
|
type: "demo.echo",
|
||||||
|
source: "test",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
payload: { message: "hello" }
|
||||||
|
});
|
||||||
|
|
||||||
|
const entries = journal.getEntries();
|
||||||
|
expect(entries.length).toBeGreaterThan(0);
|
||||||
|
expect(entries[0].actionType).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*", "tests/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user