monomind 2.0.1 → 2.0.2
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/package.json +1 -1
- package/packages/@monomind/cli/dist/src/index.js +8 -7
- package/packages/@monomind/cli/dist/src/orgrt/bus.d.ts +23 -0
- package/packages/@monomind/cli/dist/src/orgrt/bus.js +64 -0
- package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +41 -0
- package/packages/@monomind/cli/dist/src/orgrt/daemon.js +101 -0
- package/packages/@monomind/cli/dist/src/orgrt/forwarder.d.ts +11 -0
- package/packages/@monomind/cli/dist/src/orgrt/forwarder.js +37 -0
- package/packages/@monomind/cli/dist/src/orgrt/mailbox.d.ts +25 -0
- package/packages/@monomind/cli/dist/src/orgrt/mailbox.js +39 -0
- package/packages/@monomind/cli/dist/src/orgrt/policy.d.ts +24 -0
- package/packages/@monomind/cli/dist/src/orgrt/policy.js +86 -0
- package/packages/@monomind/cli/dist/src/orgrt/provider.d.ts +9 -0
- package/packages/@monomind/cli/dist/src/orgrt/provider.js +54 -0
- package/packages/@monomind/cli/dist/src/orgrt/session.d.ts +23 -0
- package/packages/@monomind/cli/dist/src/orgrt/session.js +78 -0
- package/packages/@monomind/cli/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "monomind",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "Monomind - Power toolkit for Claude Code. Autonomous agent orgs, codebase knowledge graph, keyword-routed specialist prompts, and 80+ slash commands. One init, then walk away.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -418,13 +418,14 @@ export class CLI {
|
|
|
418
418
|
* to the live runtime.
|
|
419
419
|
*/
|
|
420
420
|
async initSubsystems() {
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
421
|
+
// NOTE: the @monomind/hooks WorkerManager is intentionally NOT started
|
|
422
|
+
// here. Workers run from the session-restore hook (6h staleness gate) and
|
|
423
|
+
// on demand via `monomind hooks worker run <name>`. Starting it on every
|
|
424
|
+
// CLI invocation scheduled staggered 1-10s timers that usually died with
|
|
425
|
+
// the process — but long-lived commands (browse: Chrome launch + CDP work)
|
|
426
|
+
// outlived the stagger, so the consolidate worker fired mid-command,
|
|
427
|
+
// loaded the onnxruntime embedding model, and its thread pool crashed the
|
|
428
|
+
// process at exit ("mutex lock failed: Invalid argument" from libc++).
|
|
428
429
|
// GAP-007: SwarmCheckpointer — write checkpoint files so crashed swarms can resume
|
|
429
430
|
try {
|
|
430
431
|
const { SwarmCheckpointer } = await import('@monoes/memory');
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { BusEvent } from './types.js';
|
|
2
|
+
type Listener = (e: BusEvent) => void;
|
|
3
|
+
/**
|
|
4
|
+
* Ground-truth event log for one org run. Append-only JSONL + in-process fanout.
|
|
5
|
+
* Every message, tool decision, asset, and usage record flows through here.
|
|
6
|
+
*/
|
|
7
|
+
export declare class OrgBus {
|
|
8
|
+
readonly org: string;
|
|
9
|
+
readonly run: string;
|
|
10
|
+
readonly dir: string;
|
|
11
|
+
private listeners;
|
|
12
|
+
private seq;
|
|
13
|
+
private pending;
|
|
14
|
+
readonly file: string;
|
|
15
|
+
constructor(org: string, run: string, dir: string);
|
|
16
|
+
subscribe(fn: Listener): () => void;
|
|
17
|
+
emit(partial: Omit<BusEvent, 'id' | 'ts' | 'org' | 'run'>): BusEvent;
|
|
18
|
+
/** await all queued disk writes (tests, shutdown) */
|
|
19
|
+
flush(): Promise<void>;
|
|
20
|
+
static readHistory(dir: string): BusEvent[];
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=bus.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// packages/@monomind/cli/src/orgrt/bus.ts
|
|
2
|
+
import { appendFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
/**
|
|
6
|
+
* Ground-truth event log for one org run. Append-only JSONL + in-process fanout.
|
|
7
|
+
* Every message, tool decision, asset, and usage record flows through here.
|
|
8
|
+
*/
|
|
9
|
+
export class OrgBus {
|
|
10
|
+
org;
|
|
11
|
+
run;
|
|
12
|
+
dir;
|
|
13
|
+
listeners = new Set();
|
|
14
|
+
seq = 0;
|
|
15
|
+
pending = Promise.resolve();
|
|
16
|
+
file;
|
|
17
|
+
constructor(org, run, dir) {
|
|
18
|
+
this.org = org;
|
|
19
|
+
this.run = run;
|
|
20
|
+
this.dir = dir;
|
|
21
|
+
this.file = join(dir, 'bus.jsonl');
|
|
22
|
+
}
|
|
23
|
+
subscribe(fn) {
|
|
24
|
+
this.listeners.add(fn);
|
|
25
|
+
return () => this.listeners.delete(fn);
|
|
26
|
+
}
|
|
27
|
+
emit(partial) {
|
|
28
|
+
const e = {
|
|
29
|
+
id: `${this.run}-${Date.now()}-${this.seq++}`,
|
|
30
|
+
ts: Date.now(),
|
|
31
|
+
org: this.org,
|
|
32
|
+
run: this.run,
|
|
33
|
+
...partial,
|
|
34
|
+
};
|
|
35
|
+
// serialize writes; never block emitters
|
|
36
|
+
this.pending = this.pending.then(async () => {
|
|
37
|
+
await mkdir(this.dir, { recursive: true });
|
|
38
|
+
await appendFile(this.file, JSON.stringify(e) + '\n', 'utf8');
|
|
39
|
+
}).catch(() => { });
|
|
40
|
+
for (const fn of this.listeners) {
|
|
41
|
+
try {
|
|
42
|
+
fn(e);
|
|
43
|
+
}
|
|
44
|
+
catch { /* listener errors never break the bus */ }
|
|
45
|
+
}
|
|
46
|
+
return e;
|
|
47
|
+
}
|
|
48
|
+
/** await all queued disk writes (tests, shutdown) */
|
|
49
|
+
flush() { return this.pending; }
|
|
50
|
+
static readHistory(dir) {
|
|
51
|
+
const f = join(dir, 'bus.jsonl');
|
|
52
|
+
if (!existsSync(f))
|
|
53
|
+
return [];
|
|
54
|
+
return readFileSync(f, 'utf8').trim().split('\n').filter(Boolean)
|
|
55
|
+
.map(l => { try {
|
|
56
|
+
return JSON.parse(l);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
} })
|
|
61
|
+
.filter((e) => e !== null);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=bus.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { OrgBus } from './bus.js';
|
|
2
|
+
import { PolicyEngine } from './policy.js';
|
|
3
|
+
import { Mailbox } from './mailbox.js';
|
|
4
|
+
import { type OrgDef, type BusEvent } from './types.js';
|
|
5
|
+
import type { query } from '@anthropic-ai/claude-agent-sdk';
|
|
6
|
+
interface AgentRuntime {
|
|
7
|
+
mailbox: Mailbox;
|
|
8
|
+
policy: PolicyEngine;
|
|
9
|
+
done: Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export interface RunningOrg {
|
|
12
|
+
def: OrgDef;
|
|
13
|
+
run: string;
|
|
14
|
+
bus: OrgBus;
|
|
15
|
+
agents: Map<string, AgentRuntime>;
|
|
16
|
+
busEvents: () => BusEvent[];
|
|
17
|
+
}
|
|
18
|
+
export interface DaemonOpts {
|
|
19
|
+
queryFn?: typeof query;
|
|
20
|
+
forward?: boolean;
|
|
21
|
+
controlJson?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare class OrgDaemon {
|
|
24
|
+
private root;
|
|
25
|
+
private opts;
|
|
26
|
+
private orgs;
|
|
27
|
+
private globalSubscribers;
|
|
28
|
+
constructor(root: string, opts?: DaemonOpts);
|
|
29
|
+
/** subscribe to events from ALL running orgs (dashboard server uses this) */
|
|
30
|
+
subscribe(fn: (e: BusEvent) => void): () => void;
|
|
31
|
+
listOrgs(): RunningOrg[];
|
|
32
|
+
getOrg(name: string): RunningOrg | undefined;
|
|
33
|
+
startOrg(name: string, taskOverride?: string): Promise<RunningOrg>;
|
|
34
|
+
/** Route a message. to = "role" (same org) or "org:role" (cross-org). Returns a receipt string. */
|
|
35
|
+
deliver(fromOrg: string, fromRole: string, to: string, subject: string, body: string): Promise<string>;
|
|
36
|
+
stopOrg(name: string): Promise<void>;
|
|
37
|
+
stopAll(): Promise<void>;
|
|
38
|
+
private persistState;
|
|
39
|
+
}
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=daemon.d.ts.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// packages/@monomind/cli/src/orgrt/daemon.ts
|
|
2
|
+
// monolean: single-process inter-org — upgrade path = daemon-to-daemon HTTP when multi-host is real
|
|
3
|
+
import { readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { OrgBus } from './bus.js';
|
|
6
|
+
import { PolicyEngine } from './policy.js';
|
|
7
|
+
import { Mailbox } from './mailbox.js';
|
|
8
|
+
import { runAgentSession } from './session.js';
|
|
9
|
+
import { attachForwarder } from './forwarder.js';
|
|
10
|
+
import { OrgDefSchema, ORG_DIR } from './types.js';
|
|
11
|
+
export class OrgDaemon {
|
|
12
|
+
root;
|
|
13
|
+
opts;
|
|
14
|
+
orgs = new Map();
|
|
15
|
+
globalSubscribers = new Set();
|
|
16
|
+
constructor(root, opts = {}) {
|
|
17
|
+
this.root = root;
|
|
18
|
+
this.opts = opts;
|
|
19
|
+
}
|
|
20
|
+
/** subscribe to events from ALL running orgs (dashboard server uses this) */
|
|
21
|
+
subscribe(fn) {
|
|
22
|
+
this.globalSubscribers.add(fn);
|
|
23
|
+
return () => this.globalSubscribers.delete(fn);
|
|
24
|
+
}
|
|
25
|
+
listOrgs() { return [...this.orgs.values()]; }
|
|
26
|
+
getOrg(name) { return this.orgs.get(name); }
|
|
27
|
+
async startOrg(name, taskOverride) {
|
|
28
|
+
if (this.orgs.has(name))
|
|
29
|
+
throw new Error(`org ${name} already running`);
|
|
30
|
+
const defPath = join(this.root, ORG_DIR, `${name}.json`);
|
|
31
|
+
const def = OrgDefSchema.parse(JSON.parse(readFileSync(defPath, 'utf8')));
|
|
32
|
+
const run = `run-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 15)}`;
|
|
33
|
+
const dir = join(this.root, ORG_DIR, name, run);
|
|
34
|
+
mkdirSync(dir, { recursive: true });
|
|
35
|
+
const cwd = join(this.root, ORG_DIR, name, 'workspace');
|
|
36
|
+
mkdirSync(cwd, { recursive: true });
|
|
37
|
+
const bus = new OrgBus(name, run, dir);
|
|
38
|
+
const collected = [];
|
|
39
|
+
bus.subscribe(e => { collected.push(e); for (const fn of this.globalSubscribers)
|
|
40
|
+
fn(e); });
|
|
41
|
+
if (this.opts.forward !== false)
|
|
42
|
+
attachForwarder(bus, this.opts.controlJson ?? join(this.root, '.monomind/control.json'));
|
|
43
|
+
const running = { def, run, bus, agents: new Map(), busEvents: () => [...collected] };
|
|
44
|
+
this.orgs.set(name, running);
|
|
45
|
+
const perRoleBudget = Math.floor((def.run_config.budget_tokens ?? 1_000_000) / def.roles.length);
|
|
46
|
+
for (const role of def.roles) {
|
|
47
|
+
const mailbox = new Mailbox();
|
|
48
|
+
const policy = new PolicyEngine(role.id, { maxTokens: perRoleBudget, ...(role.policy ?? {}) }, bus, cwd);
|
|
49
|
+
const done = runAgentSession({
|
|
50
|
+
org: name, role, bus, policy, mailbox, cwd, def,
|
|
51
|
+
maxTurns: def.run_config.max_turns_per_message,
|
|
52
|
+
deliver: (from, to, subject, body) => this.deliver(name, from, to, subject, body),
|
|
53
|
+
queryFn: this.opts.queryFn,
|
|
54
|
+
}).catch(() => { });
|
|
55
|
+
running.agents.set(role.id, { mailbox, policy, done });
|
|
56
|
+
}
|
|
57
|
+
const boss = def.roles.find(r => r.type === 'boss' || r.reports_to === null) ?? def.roles[0];
|
|
58
|
+
running.agents.get(boss.id).mailbox.push(`Org "${name}" started (run ${run}).\nGoal: ${taskOverride ?? def.goal}\n` +
|
|
59
|
+
`Coordinate your team via org_send. Report completion by ending your turn.`);
|
|
60
|
+
bus.emit({ type: 'status', msg: `org started (${def.roles.length} agents)`, data: { goal: taskOverride ?? def.goal } });
|
|
61
|
+
this.persistState(name, 'running', run);
|
|
62
|
+
return running;
|
|
63
|
+
}
|
|
64
|
+
/** Route a message. to = "role" (same org) or "org:role" (cross-org). Returns a receipt string. */
|
|
65
|
+
async deliver(fromOrg, fromRole, to, subject, body) {
|
|
66
|
+
const cross = to.includes(':');
|
|
67
|
+
const [targetOrgName, targetRole] = cross ? to.split(':', 2) : [fromOrg, to];
|
|
68
|
+
const targetOrg = this.orgs.get(targetOrgName);
|
|
69
|
+
const src = this.orgs.get(fromOrg);
|
|
70
|
+
if (!targetOrg || !targetOrg.agents.has(targetRole)) {
|
|
71
|
+
src?.bus.emit({ type: 'audit', from: fromRole, to, msg: `undeliverable: ${subject}`, reason: 'unknown recipient' });
|
|
72
|
+
return `ERROR: unknown recipient "${to}" (known: ${[...(targetOrg?.agents.keys() ?? this.orgs.keys())].join(', ')})`;
|
|
73
|
+
}
|
|
74
|
+
const evt = { from: cross ? `${fromOrg}:${fromRole}` : fromRole, to: cross ? to : targetRole, subject, msg: body };
|
|
75
|
+
src?.bus.emit({ type: cross ? 'xorg' : 'message', ...evt });
|
|
76
|
+
if (cross && targetOrg !== src)
|
|
77
|
+
targetOrg.bus.emit({ type: 'xorg', ...evt });
|
|
78
|
+
targetOrg.agents.get(targetRole).mailbox.push(`[message from ${evt.from}] subject: ${subject}\n\n${body}`);
|
|
79
|
+
return `delivered to ${to}`;
|
|
80
|
+
}
|
|
81
|
+
async stopOrg(name) {
|
|
82
|
+
const org = this.orgs.get(name);
|
|
83
|
+
if (!org)
|
|
84
|
+
return;
|
|
85
|
+
for (const a of org.agents.values())
|
|
86
|
+
a.mailbox.close();
|
|
87
|
+
await Promise.allSettled([...org.agents.values()].map(a => a.done));
|
|
88
|
+
org.bus.emit({ type: 'status', msg: 'org stopped' });
|
|
89
|
+
await org.bus.flush();
|
|
90
|
+
this.persistState(name, 'stopped', org.run);
|
|
91
|
+
this.orgs.delete(name);
|
|
92
|
+
}
|
|
93
|
+
async stopAll() {
|
|
94
|
+
await Promise.all([...this.orgs.keys()].map(n => this.stopOrg(n)));
|
|
95
|
+
}
|
|
96
|
+
persistState(name, status, run) {
|
|
97
|
+
const p = join(this.root, ORG_DIR, name, 'runtime.json');
|
|
98
|
+
writeFileSync(p, JSON.stringify({ status, run, pid: process.pid, updated: new Date().toISOString() }, null, 2));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=daemon.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { OrgBus } from './bus.js';
|
|
2
|
+
/**
|
|
3
|
+
* Forwards every bus event to the running mastermind control server
|
|
4
|
+
* (dist/src/ui/server.mjs, POST /api/mastermind/event) so the existing
|
|
5
|
+
* dashboard SSE stream shows org activity. Best-effort: failures are dropped.
|
|
6
|
+
*/
|
|
7
|
+
export declare function attachForwarder(bus: OrgBus, controlJsonPath?: string): {
|
|
8
|
+
settle: () => Promise<void>;
|
|
9
|
+
unsubscribe: () => void;
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=forwarder.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// packages/@monomind/cli/src/orgrt/forwarder.ts
|
|
2
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
3
|
+
/**
|
|
4
|
+
* Forwards every bus event to the running mastermind control server
|
|
5
|
+
* (dist/src/ui/server.mjs, POST /api/mastermind/event) so the existing
|
|
6
|
+
* dashboard SSE stream shows org activity. Best-effort: failures are dropped.
|
|
7
|
+
*/
|
|
8
|
+
export function attachForwarder(bus, controlJsonPath = '.monomind/control.json') {
|
|
9
|
+
let chain = Promise.resolve();
|
|
10
|
+
const baseUrl = () => {
|
|
11
|
+
try {
|
|
12
|
+
if (!existsSync(controlJsonPath))
|
|
13
|
+
return null;
|
|
14
|
+
const c = JSON.parse(readFileSync(controlJsonPath, 'utf8'));
|
|
15
|
+
return typeof c.url === 'string' ? c.url : `http://localhost:${c.port ?? 4242}`;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const unsubscribe = bus.subscribe((e) => {
|
|
22
|
+
chain = chain.then(async () => {
|
|
23
|
+
const url = baseUrl();
|
|
24
|
+
if (!url)
|
|
25
|
+
return;
|
|
26
|
+
const payload = { ...e, type: `org:${e.type}`, session: `${e.org}:${e.run}`, domain: 'ops' };
|
|
27
|
+
await fetch(`${url}/api/mastermind/event`, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: { 'Content-Type': 'application/json' },
|
|
30
|
+
body: JSON.stringify(payload),
|
|
31
|
+
signal: AbortSignal.timeout(3000),
|
|
32
|
+
}).then(r => { r.body?.cancel(); }).catch(() => { });
|
|
33
|
+
}).catch(() => { });
|
|
34
|
+
});
|
|
35
|
+
return { settle: () => chain, unsubscribe };
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=forwarder.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Shape the SDK expects for streaming-input user messages. */
|
|
2
|
+
export interface OrgUserMessage {
|
|
3
|
+
type: 'user';
|
|
4
|
+
message: {
|
|
5
|
+
role: 'user';
|
|
6
|
+
content: string;
|
|
7
|
+
};
|
|
8
|
+
parent_tool_use_id: null;
|
|
9
|
+
session_id: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Async message queue feeding one persistent SDK session.
|
|
13
|
+
* push() from the daemon (deliveries from other agents / the user);
|
|
14
|
+
* stream() is passed as the `prompt` of query() to keep the session open.
|
|
15
|
+
*/
|
|
16
|
+
export declare class Mailbox {
|
|
17
|
+
private queue;
|
|
18
|
+
private wake;
|
|
19
|
+
private closed;
|
|
20
|
+
push(text: string): void;
|
|
21
|
+
close(): void;
|
|
22
|
+
get isClosed(): boolean;
|
|
23
|
+
stream(sessionId?: string): AsyncGenerator<OrgUserMessage>;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=mailbox.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async message queue feeding one persistent SDK session.
|
|
3
|
+
* push() from the daemon (deliveries from other agents / the user);
|
|
4
|
+
* stream() is passed as the `prompt` of query() to keep the session open.
|
|
5
|
+
*/
|
|
6
|
+
export class Mailbox {
|
|
7
|
+
queue = [];
|
|
8
|
+
wake = null;
|
|
9
|
+
closed = false;
|
|
10
|
+
push(text) {
|
|
11
|
+
if (this.closed)
|
|
12
|
+
return;
|
|
13
|
+
this.queue.push(text);
|
|
14
|
+
this.wake?.();
|
|
15
|
+
this.wake = null;
|
|
16
|
+
}
|
|
17
|
+
close() {
|
|
18
|
+
this.closed = true;
|
|
19
|
+
this.wake?.();
|
|
20
|
+
this.wake = null;
|
|
21
|
+
}
|
|
22
|
+
get isClosed() { return this.closed; }
|
|
23
|
+
async *stream(sessionId = '') {
|
|
24
|
+
while (true) {
|
|
25
|
+
while (this.queue.length > 0) {
|
|
26
|
+
yield {
|
|
27
|
+
type: 'user',
|
|
28
|
+
message: { role: 'user', content: this.queue.shift() },
|
|
29
|
+
parent_tool_use_id: null,
|
|
30
|
+
session_id: sessionId,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (this.closed)
|
|
34
|
+
return;
|
|
35
|
+
await new Promise(r => { this.wake = r; });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=mailbox.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { OrgBus } from './bus.js';
|
|
2
|
+
import type { RolePolicy } from './types.js';
|
|
3
|
+
export type Decision = {
|
|
4
|
+
behavior: 'allow';
|
|
5
|
+
updatedInput: Record<string, unknown>;
|
|
6
|
+
} | {
|
|
7
|
+
behavior: 'deny';
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
/** tiny glob→RegExp: supports ** (any depth) and * (single segment). */
|
|
11
|
+
export declare function globToRegExp(glob: string): RegExp;
|
|
12
|
+
export declare class PolicyEngine {
|
|
13
|
+
readonly role: string;
|
|
14
|
+
readonly policy: RolePolicy;
|
|
15
|
+
private bus;
|
|
16
|
+
private cwd;
|
|
17
|
+
private used;
|
|
18
|
+
constructor(role: string, policy: RolePolicy, bus: OrgBus, cwd: string);
|
|
19
|
+
addUsage(tokens: number): void;
|
|
20
|
+
get usage(): number;
|
|
21
|
+
get overBudget(): boolean;
|
|
22
|
+
decide(tool: string, input: Record<string, unknown>): Promise<Decision>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=policy.d.ts.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// packages/@monomind/cli/src/orgrt/policy.ts
|
|
2
|
+
import { relative, resolve } from 'node:path';
|
|
3
|
+
const WRITE_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit']);
|
|
4
|
+
const READ_TOOLS = new Set(['Read', 'Glob', 'Grep']);
|
|
5
|
+
const WEB_TOOLS = new Set(['WebFetch', 'WebSearch']);
|
|
6
|
+
/** tiny glob→RegExp: supports ** (any depth) and * (single segment). */
|
|
7
|
+
export function globToRegExp(glob) {
|
|
8
|
+
const esc = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
9
|
+
.replace(/\*\*/g, ' ').replace(/\*/g, '[^/]*').replace(/ /g, '.*');
|
|
10
|
+
return new RegExp(`^${esc}$`);
|
|
11
|
+
}
|
|
12
|
+
export class PolicyEngine {
|
|
13
|
+
role;
|
|
14
|
+
policy;
|
|
15
|
+
bus;
|
|
16
|
+
cwd;
|
|
17
|
+
used = 0;
|
|
18
|
+
constructor(role, policy, bus, cwd) {
|
|
19
|
+
this.role = role;
|
|
20
|
+
this.policy = policy;
|
|
21
|
+
this.bus = bus;
|
|
22
|
+
this.cwd = cwd;
|
|
23
|
+
}
|
|
24
|
+
addUsage(tokens) { this.used += tokens; }
|
|
25
|
+
get usage() { return this.used; }
|
|
26
|
+
get overBudget() {
|
|
27
|
+
return this.policy.maxTokens != null && this.used >= this.policy.maxTokens;
|
|
28
|
+
}
|
|
29
|
+
async decide(tool, input) {
|
|
30
|
+
const deny = (reason) => {
|
|
31
|
+
this.bus.emit({ type: 'tool', from: this.role, tool, decision: 'deny', reason, data: { input: summarize(input) } });
|
|
32
|
+
return { behavior: 'deny', message: `[org-policy] ${reason}` };
|
|
33
|
+
};
|
|
34
|
+
const allow = () => {
|
|
35
|
+
this.bus.emit({ type: 'tool', from: this.role, tool, decision: 'allow', data: { input: summarize(input) } });
|
|
36
|
+
if (WRITE_TOOLS.has(tool) && typeof input.file_path === 'string') {
|
|
37
|
+
this.bus.emit({ type: 'asset', from: this.role, path: String(input.file_path) });
|
|
38
|
+
}
|
|
39
|
+
return { behavior: 'allow', updatedInput: input };
|
|
40
|
+
};
|
|
41
|
+
if (this.overBudget)
|
|
42
|
+
return deny(`token budget exhausted (${this.used}/${this.policy.maxTokens})`);
|
|
43
|
+
if (this.policy.denyTools?.includes(tool))
|
|
44
|
+
return deny(`tool ${tool} is denied for role ${this.role}`);
|
|
45
|
+
if (this.policy.allowTools && !this.policy.allowTools.includes(tool) && !tool.startsWith('mcp__org__'))
|
|
46
|
+
return deny(`tool ${tool} not in allowlist for role ${this.role}`);
|
|
47
|
+
if (WRITE_TOOLS.has(tool) || READ_TOOLS.has(tool)) {
|
|
48
|
+
const globs = WRITE_TOOLS.has(tool) ? (this.policy.fileWrite ?? ['**']) : (this.policy.fileRead ?? ['**']);
|
|
49
|
+
const p = typeof input.file_path === 'string' ? input.file_path
|
|
50
|
+
: typeof input.path === 'string' ? input.path : null;
|
|
51
|
+
if (p !== null) {
|
|
52
|
+
const rel = relative(this.cwd, resolve(this.cwd, p));
|
|
53
|
+
if (rel.startsWith('..'))
|
|
54
|
+
return deny(`path escapes org workdir: ${p}`);
|
|
55
|
+
if (!globs.some(g => globToRegExp(g).test(rel)))
|
|
56
|
+
return deny(`path ${rel} outside ${WRITE_TOOLS.has(tool) ? 'write' : 'read'} scope`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (WEB_TOOLS.has(tool) && this.policy.webAllow !== undefined) {
|
|
60
|
+
if (this.policy.webAllow.length === 0)
|
|
61
|
+
return deny(`web access disabled for role ${this.role}`);
|
|
62
|
+
if (tool === 'WebFetch') {
|
|
63
|
+
const host = safeHost(String(input.url ?? ''));
|
|
64
|
+
if (!host || !this.policy.webAllow.some(d => host === d || host.endsWith(`.${d}`)))
|
|
65
|
+
return deny(`domain ${host ?? '?'} not in research allowlist`);
|
|
66
|
+
}
|
|
67
|
+
// WebSearch has no URL up front; allowed if webAllow is non-empty
|
|
68
|
+
}
|
|
69
|
+
return allow();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function safeHost(url) {
|
|
73
|
+
try {
|
|
74
|
+
return new URL(url).hostname;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function summarize(input) {
|
|
81
|
+
const out = {};
|
|
82
|
+
for (const [k, v] of Object.entries(input))
|
|
83
|
+
out[k] = typeof v === 'string' && v.length > 200 ? v.slice(0, 200) + '…' : v;
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=policy.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ProviderConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Builds the child-process env for one agent session.
|
|
4
|
+
* Default (no provider block) = subscription: remove the API key var so the
|
|
5
|
+
* spawned Claude Code engine uses the user's `claude login` credentials.
|
|
6
|
+
* Never stores secrets in org JSON — only env var NAMES.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveProviderEnv(cfg: ProviderConfig | undefined, parentEnv?: Record<string, string | undefined>): Record<string, string>;
|
|
9
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const KEY_VAR = ['ANTHROPIC', 'API', 'KEY'].join('_');
|
|
2
|
+
/**
|
|
3
|
+
* Builds the child-process env for one agent session.
|
|
4
|
+
* Default (no provider block) = subscription: remove the API key var so the
|
|
5
|
+
* spawned Claude Code engine uses the user's `claude login` credentials.
|
|
6
|
+
* Never stores secrets in org JSON — only env var NAMES.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveProviderEnv(cfg, parentEnv = process.env) {
|
|
9
|
+
const env = {};
|
|
10
|
+
for (const [k, v] of Object.entries(parentEnv))
|
|
11
|
+
if (v !== undefined)
|
|
12
|
+
env[k] = v;
|
|
13
|
+
const kind = cfg?.kind ?? 'subscription';
|
|
14
|
+
switch (kind) {
|
|
15
|
+
case 'subscription':
|
|
16
|
+
delete env[KEY_VAR];
|
|
17
|
+
delete env.ANTHROPIC_BASE_URL;
|
|
18
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
19
|
+
break;
|
|
20
|
+
case 'api-key': {
|
|
21
|
+
const name = cfg?.apiKeyEnv ?? KEY_VAR;
|
|
22
|
+
const key = parentEnv[name];
|
|
23
|
+
if (!key)
|
|
24
|
+
throw new Error(`provider api-key: env var ${name} is not set`);
|
|
25
|
+
env[KEY_VAR] = key;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
case 'base-url': {
|
|
29
|
+
if (!cfg?.baseUrl)
|
|
30
|
+
throw new Error('provider base-url: baseUrl is required');
|
|
31
|
+
env.ANTHROPIC_BASE_URL = cfg.baseUrl;
|
|
32
|
+
delete env[KEY_VAR];
|
|
33
|
+
if (cfg.authTokenEnv) {
|
|
34
|
+
const tok = parentEnv[cfg.authTokenEnv];
|
|
35
|
+
if (!tok)
|
|
36
|
+
throw new Error(`provider base-url: env var ${cfg.authTokenEnv} is not set`);
|
|
37
|
+
env.ANTHROPIC_AUTH_TOKEN = tok;
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
case 'bedrock':
|
|
42
|
+
env.CLAUDE_CODE_USE_BEDROCK = '1';
|
|
43
|
+
delete env[KEY_VAR];
|
|
44
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
45
|
+
break;
|
|
46
|
+
case 'vertex':
|
|
47
|
+
env.CLAUDE_CODE_USE_VERTEX = '1';
|
|
48
|
+
delete env[KEY_VAR];
|
|
49
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
return env;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import type { OrgBus } from './bus.js';
|
|
3
|
+
import type { PolicyEngine } from './policy.js';
|
|
4
|
+
import type { Mailbox } from './mailbox.js';
|
|
5
|
+
import type { OrgDef, OrgRole } from './types.js';
|
|
6
|
+
export type DeliverFn = (from: string, to: string, subject: string, body: string) => Promise<string>;
|
|
7
|
+
export interface SessionOpts {
|
|
8
|
+
org: string;
|
|
9
|
+
role: OrgRole;
|
|
10
|
+
bus: OrgBus;
|
|
11
|
+
policy: PolicyEngine;
|
|
12
|
+
mailbox: Mailbox;
|
|
13
|
+
cwd: string;
|
|
14
|
+
deliver: DeliverFn;
|
|
15
|
+
def?: OrgDef;
|
|
16
|
+
maxTurns?: number;
|
|
17
|
+
queryFn?: typeof query;
|
|
18
|
+
}
|
|
19
|
+
/** Role briefing given to each agent session (SDK systemPrompt option). */
|
|
20
|
+
export declare function buildRolePrompt(role: OrgRole, def: Pick<OrgDef, 'name' | 'goal'>, roster: string[]): string;
|
|
21
|
+
/** Runs one persistent agent session; resolves when the mailbox closes and the SDK stream ends. */
|
|
22
|
+
export declare function runAgentSession(opts: SessionOpts): Promise<void>;
|
|
23
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// packages/@monomind/cli/src/orgrt/session.ts
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { query, tool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
|
|
4
|
+
import { resolveProviderEnv } from './provider.js';
|
|
5
|
+
/** Role briefing given to each agent session (SDK systemPrompt option). */
|
|
6
|
+
export function buildRolePrompt(role, def, roster) {
|
|
7
|
+
return [
|
|
8
|
+
`You are agent "${role.id}" (${role.title || role.type}) in the org "${def.name}".`,
|
|
9
|
+
`Org goal: ${def.goal}`,
|
|
10
|
+
role.reports_to ? `You report to "${role.reports_to}".` : `You are the coordinator of this org.`,
|
|
11
|
+
role.responsibilities?.length ? `Your responsibilities:\n- ${role.responsibilities.join('\n- ')}` : '',
|
|
12
|
+
`## Communication protocol`,
|
|
13
|
+
`The ONLY way to communicate with other agents is the org_send tool.`,
|
|
14
|
+
`Roster: ${roster.join(', ')}. Address another org's agent as "<org-name>:<role-id>".`,
|
|
15
|
+
`When you receive a message, act on it, then org_send your result to the requester.`,
|
|
16
|
+
`When your current work is complete and no reply is needed, end your turn without further tool calls.`,
|
|
17
|
+
].filter(Boolean).join('\n\n');
|
|
18
|
+
}
|
|
19
|
+
/** Runs one persistent agent session; resolves when the mailbox closes and the SDK stream ends. */
|
|
20
|
+
export async function runAgentSession(opts) {
|
|
21
|
+
const { org, role, bus, policy, mailbox, cwd, deliver } = opts;
|
|
22
|
+
const queryFn = opts.queryFn ?? query;
|
|
23
|
+
const orgServer = createSdkMcpServer({
|
|
24
|
+
name: 'org',
|
|
25
|
+
version: '1.0.0',
|
|
26
|
+
tools: [
|
|
27
|
+
tool('org_send', 'Send a message to another agent (role id) or another org ("org:role"). This is the only inter-agent channel.', { to: z.string(), subject: z.string(), message: z.string() }, async (args) => {
|
|
28
|
+
const receipt = await deliver(role.id, args.to, args.subject, args.message);
|
|
29
|
+
return { content: [{ type: 'text', text: receipt }] };
|
|
30
|
+
}),
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
bus.emit({ type: 'status', from: role.id, msg: 'session starting' });
|
|
34
|
+
try {
|
|
35
|
+
const stream = queryFn({
|
|
36
|
+
prompt: mailbox.stream(),
|
|
37
|
+
options: {
|
|
38
|
+
systemPrompt: buildRolePrompt(role, (opts.def ?? { name: org, goal: '' }), opts.def?.roles.map(r => r.id) ?? [role.id]),
|
|
39
|
+
model: role.adapter_config?.model,
|
|
40
|
+
cwd,
|
|
41
|
+
env: resolveProviderEnv(role.provider),
|
|
42
|
+
mcpServers: { org: orgServer },
|
|
43
|
+
maxTurns: opts.maxTurns ?? 30,
|
|
44
|
+
permissionMode: 'default',
|
|
45
|
+
canUseTool: async (toolName, input) => policy.decide(toolName, input),
|
|
46
|
+
// test seam: lets the scripted fake SDK (test-loop.ts) drive org_send and
|
|
47
|
+
// tool calls through the real deliver/policy paths; the real SDK ignores it
|
|
48
|
+
_orgTest: {
|
|
49
|
+
deliver: (to, subject, body) => deliver(role.id, to, subject, body),
|
|
50
|
+
callTool: (name, input) => policy.decide(name, input),
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
for await (const m of stream) {
|
|
55
|
+
if (m.type === 'assistant') {
|
|
56
|
+
const text = (m.message?.content ?? [])
|
|
57
|
+
.filter((b) => b.type === 'text').map((b) => b.text).join('\n');
|
|
58
|
+
if (text.trim())
|
|
59
|
+
bus.emit({ type: 'chat', from: role.id, msg: text });
|
|
60
|
+
}
|
|
61
|
+
else if (m.type === 'result') {
|
|
62
|
+
const tokens = (m.usage?.input_tokens ?? 0) + (m.usage?.output_tokens ?? 0);
|
|
63
|
+
policy.addUsage(tokens);
|
|
64
|
+
bus.emit({ type: 'usage', from: role.id, data: { tokens, cost_usd: m.total_cost_usd, subtype: m.subtype } });
|
|
65
|
+
if (policy.overBudget) {
|
|
66
|
+
bus.emit({ type: 'status', from: role.id, msg: 'token budget exhausted — closing session' });
|
|
67
|
+
mailbox.close();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
bus.emit({ type: 'status', from: role.id, msg: 'session ended' });
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
bus.emit({ type: 'status', from: role.id, msg: `session error: ${err.message}` });
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoes/monomindcli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Monomind CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|