@eventmodelers/cli 1.0.70 → 1.0.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -272,9 +272,12 @@ the turn's own instructions rather than being enforced from outside — the `cla
|
|
|
272
272
|
what spawns the agents — so it's a budget the agent is told to keep, not a hard ceiling.
|
|
273
273
|
|
|
274
274
|
Every event that arrives is remembered until a turn carries it — including events that land
|
|
275
|
-
while a turn is running. Its own writes come back on that same channel and
|
|
276
|
-
|
|
277
|
-
|
|
275
|
+
while a turn is running. Its own writes come back on that same channel, and each event says who
|
|
276
|
+
made it (`agent_id` from the writer's `x-agent-id` header, `user_id` for a browser), so the
|
|
277
|
+
agent's own echo is recognized exactly: a burst that is nothing but its own writes is dropped
|
|
278
|
+
without spending a turn, and one that is mixed lists its own lines marked as such. Only a write
|
|
279
|
+
that reached the platform with neither id falls back to the echo window's guess and is labelled
|
|
280
|
+
"possibly your own". The lane is damped on timing on top of that: it waits for a quiet period (but not forever), waits
|
|
278
281
|
out the echo window of its last turn, never fires twice in quick succession, and widens that
|
|
279
282
|
floor each time it answers `NOOP`, so a finished board goes quiet by itself. Override the
|
|
280
283
|
windows if the defaults don't suit your board:
|
|
@@ -283,7 +286,7 @@ windows if the defaults don't suit your board:
|
|
|
283
286
|
|---|---|---|
|
|
284
287
|
| `EVENTMODELERS_STANDALONE_DEBOUNCE_MS` | `8000` | quiet period before buffered board changes turn into a turn |
|
|
285
288
|
| `EVENTMODELERS_STANDALONE_MAX_WAIT_MS` | `90000` | cap on that quiet period, so a board being edited continuously still gets a turn |
|
|
286
|
-
| `EVENTMODELERS_STANDALONE_ECHO_WINDOW_MS` | `20000` | after a turn, how long incoming
|
|
289
|
+
| `EVENTMODELERS_STANDALONE_ECHO_WINDOW_MS` | `20000` | after a turn, how long an *unattributed* incoming change is labelled as probably the agent's own echo (attributed ones are identified outright) |
|
|
287
290
|
| `EVENTMODELERS_STANDALONE_MIN_INTERVAL_MS` | `60000` | floor between two self-directed turns |
|
|
288
291
|
| `EVENTMODELERS_STANDALONE_BACKOFF_CAP_MS` | `900000` | ceiling that floor doubles up to while turns keep answering `NOOP` |
|
|
289
292
|
| `EVENTMODELERS_STANDALONE_IDLE_MS` | `900000` | with nothing happening at all, how long before the agent reviews the model anyway (`0` disables it) |
|
|
@@ -416,6 +419,18 @@ npx @eventmodelers/cli run --id <id> # pin ONE identit
|
|
|
416
419
|
|
|
417
420
|
`run --id` is what you want when an agent has to keep the same identity every time it starts: a supervisor that already knows the id, a second agent of the same type in one *project* (which would otherwise share the project's single minted id), or an agent a board has starred as its **preferred agent** — that star addresses prompts to one id, so an agent whose id changes per run loses it on restart. `run --id`/`run --name` are per-run only: nothing is written to disk.
|
|
418
421
|
|
|
422
|
+
#### Working only what you were addressed (`--exclusive`)
|
|
423
|
+
|
|
424
|
+
By default an agent claims two kinds of prompt: the ones addressed to its own id, and every prompt nobody addressed to anyone. That's right for the single agent on a board, and wrong for a dedicated one — a specialist sitting next to a general agent, or an agent a supervisor drives by id, ends up answering whatever the queue happens to hold. `--exclusive` drops that second kind:
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
npx @eventmodelers/cli run --standalone --board-id <uuid> --id <agent-uuid> --exclusive
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Only prompts carrying this agent's id are worked. Anything untargeted is handed straight back to the queue (status `ADDED`) for another agent to take — the addressee filter lives in the queue's claim query, which hands an agent its own prompts *and* the untargeted ones, so claiming is the only way to find out which arrived. An exclusive run therefore claims as usual and gives back what wasn't meant for it, once it has walked past it to its own work.
|
|
431
|
+
|
|
432
|
+
Pair it with `--id`: a `--standalone`/`--global` run mints a fresh id per run, so prompts addressed to the previous run's id are never claimed. `--exclusive` applies to the prompt queue only — a `--standalone` agent's self-directed turns are nobody's prompt, and it keeps taking them.
|
|
433
|
+
|
|
419
434
|
### Env vars and `--config` (scripted/CI installs)
|
|
420
435
|
|
|
421
436
|
Every config field can be set via an `EVENTMODELERS_*` env var instead of the interactive prompts — these always win over whatever's in `config.json`, so a fully env-driven install never prompts for credentials or Claude execution settings:
|
package/cli.js
CHANGED
|
@@ -1310,7 +1310,15 @@ function configureHooks({ hooksSrc, targetDir }) {
|
|
|
1310
1310
|
// per task, or a long-lived warm process) must call this first — a `.mcp.json`
|
|
1311
1311
|
// written mid-session by the process itself is too late for that same process.
|
|
1312
1312
|
// The token itself is never written to disk here — `${EVENTMODELERS_TOKEN}` is
|
|
1313
|
-
// resolved by `claude` from its own process env, which the caller must set.
|
|
1313
|
+
// resolved by `claude` from its own process env, which the caller must set. Same for
|
|
1314
|
+
// `${EVENTMODELERS_AGENT_ID}`: without it on the transport, every MCP write this agent makes
|
|
1315
|
+
// reaches the platform unattributed (board_events.agent_id null), and the board shows one
|
|
1316
|
+
// anonymous robot for it instead of this agent's name. It belongs here rather than only in the
|
|
1317
|
+
// skill's Step 3.5 because this entry is rewritten on every run — it would otherwise overwrite
|
|
1318
|
+
// the header the skill just added — and because a `.mcp.json` fixed mid-session comes too late
|
|
1319
|
+
// for the `claude` process already running. An unset var arrives at the server as the literal
|
|
1320
|
+
// `${EVENTMODELERS_AGENT_ID}` text, which it drops (it only accepts a uuid), so the entry is the
|
|
1321
|
+
// same one for a human's session and an agent's.
|
|
1314
1322
|
function ensureMcpRegistered(projectDir, baseUrl) {
|
|
1315
1323
|
const mcpConfigPath = join(projectDir, '.mcp.json');
|
|
1316
1324
|
const mcpConfig = readJsonSafe(mcpConfigPath);
|
|
@@ -1318,7 +1326,7 @@ function ensureMcpRegistered(projectDir, baseUrl) {
|
|
|
1318
1326
|
mcpConfig.mcpServers.eventmodelers = {
|
|
1319
1327
|
type: 'http',
|
|
1320
1328
|
url: `${baseUrl}/mcp`,
|
|
1321
|
-
headers: { 'x-token': '${EVENTMODELERS_TOKEN}' },
|
|
1329
|
+
headers: { 'x-token': '${EVENTMODELERS_TOKEN}', 'x-agent-id': '${EVENTMODELERS_AGENT_ID}' },
|
|
1322
1330
|
};
|
|
1323
1331
|
writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
|
|
1324
1332
|
}
|
|
@@ -1672,7 +1680,13 @@ async function ensureGlobalKit(baseUrl) {
|
|
|
1672
1680
|
// question, sketch a screen. Without the flag that channel is still subscribed on
|
|
1673
1681
|
// the same connection and every event on it is dropped, so the two modes differ by
|
|
1674
1682
|
// one filter rather than by a whole second realtime stack.
|
|
1675
|
-
|
|
1683
|
+
//
|
|
1684
|
+
// `exclusive` narrows the prompt lane to this agent alone: only a prompt the user
|
|
1685
|
+
// addressed to this agent id (the board's "preferred agent") is worked, and anything
|
|
1686
|
+
// untargeted is handed straight back to the queue for another agent to take. It says
|
|
1687
|
+
// nothing about the standalone lane — a self-directed turn is nobody's task, so an
|
|
1688
|
+
// exclusive standalone agent still works the board on its own initiative.
|
|
1689
|
+
async function runModeling(kitDir, projectDir, { verbose = false, standalone = false, exclusive = false, overrides = null, maxAgents = DEFAULT_MAX_AGENTS, identity = {} } = {}) {
|
|
1676
1690
|
const configLibPath = join(kitDir, 'lib', 'config.js');
|
|
1677
1691
|
if (!existsSync(configLibPath)) {
|
|
1678
1692
|
console.error(`❌ ${relative(process.cwd(), configLibPath)} not found — --modeling needs a kit installed via \`init --modeling\`.`);
|
|
@@ -1710,6 +1724,12 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
|
|
|
1710
1724
|
console.error('❌ --modeling needs a boardId — a modeling agent always runs for exactly one board. Run `/connect board=<uuid>` once, or add boardId to .eventmodelers/config.json.');
|
|
1711
1725
|
process.exit(1);
|
|
1712
1726
|
}
|
|
1727
|
+
// Nothing can be addressed to an agent with no id, so an exclusive run without one would
|
|
1728
|
+
// hand every prompt back and sit idle forever — a silent no-op worth failing on instead.
|
|
1729
|
+
if (exclusive && !cfg.agentId) {
|
|
1730
|
+
console.error('❌ --exclusive needs an agent id — that is what a prompt is addressed to. Pass `run --id <uuid>` (or let the kit mint one) and address the prompt to it on the board.');
|
|
1731
|
+
process.exit(1);
|
|
1732
|
+
}
|
|
1713
1733
|
|
|
1714
1734
|
const subagentModel = cfg.subagentModel || DEFAULT_SUBAGENT_MODEL;
|
|
1715
1735
|
|
|
@@ -1928,6 +1948,13 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
|
|
|
1928
1948
|
? `standalone: ON — reacting to direct prompts AND to board changes on its own initiative (max ${maxAgents} subagent(s) per self-directed turn)`
|
|
1929
1949
|
: 'standalone: off — reacting to direct prompts only (board changes are dropped)',
|
|
1930
1950
|
);
|
|
1951
|
+
if (exclusive) {
|
|
1952
|
+
log(`exclusive: ON — only prompts addressed to ${cfg.agentId} are worked; every untargeted prompt is handed back to the queue`);
|
|
1953
|
+
// A global/standalone run mints its id per run (see resolveModelingCredentials), so an id
|
|
1954
|
+
// someone addressed a prompt to yesterday is not this agent — worth saying out loud here,
|
|
1955
|
+
// where the alternative is an agent that looks healthy and quietly works nothing.
|
|
1956
|
+
if (overrides && !identity.agentId) log('exclusive: this run minted a fresh agent id — star it on the board now, or restart with `--id <uuid>` to keep one addressable identity');
|
|
1957
|
+
}
|
|
1931
1958
|
warmUpSession();
|
|
1932
1959
|
|
|
1933
1960
|
async function getRealtimeToken() {
|
|
@@ -1947,15 +1974,47 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
|
|
|
1947
1974
|
return res.json();
|
|
1948
1975
|
}
|
|
1949
1976
|
|
|
1977
|
+
// Puts a prompt this agent claimed but will not work back on the queue (CLAIMED -> ADDED),
|
|
1978
|
+
// so whichever agent it was actually open to can still take it. `x-token` only — the status
|
|
1979
|
+
// endpoint is meant to be called by the agent holding the prompt.
|
|
1980
|
+
async function releasePrompt(promptId) {
|
|
1981
|
+
const res = await fetch(`${cfg.baseUrl}/api/org/${cfg.organizationId}/prompts/${promptId}/status`, {
|
|
1982
|
+
method: 'POST',
|
|
1983
|
+
headers: { 'x-token': cfg.token, 'Content-Type': 'application/json', ...agentHeaders(cfg) },
|
|
1984
|
+
body: JSON.stringify({ status: 'ADDED' }),
|
|
1985
|
+
});
|
|
1986
|
+
if (!res.ok) throw new Error(`prompts/${promptId}/status: HTTP ${res.status}`);
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1950
1989
|
let realtimeToken = await getRealtimeToken();
|
|
1951
1990
|
|
|
1952
1991
|
let draining = false;
|
|
1953
1992
|
async function drain() {
|
|
1954
1993
|
if (draining) return;
|
|
1955
1994
|
draining = true;
|
|
1995
|
+
// --exclusive only: prompts claimed in this pass that weren't addressed to this agent,
|
|
1996
|
+
// handed back once the pass is over (see below).
|
|
1997
|
+
const handBack = [];
|
|
1956
1998
|
try {
|
|
1957
1999
|
let p;
|
|
1958
2000
|
while ((p = await fetchNextPrompt(realtimeToken)) !== null) {
|
|
2001
|
+
// The queue can't filter by addressee for us: `prompts/next` hands an agent both the
|
|
2002
|
+
// prompts addressed to it and every untargeted one (`agent_id IS NULL`) — claiming is
|
|
2003
|
+
// what reveals which kind arrived — so an exclusive run claims as usual and gives back
|
|
2004
|
+
// what wasn't meant for it.
|
|
2005
|
+
//
|
|
2006
|
+
// The hand-back is deferred to the end of the pass on purpose: a prompt released
|
|
2007
|
+
// mid-loop goes straight back to the head of the very queue this loop is reading, so
|
|
2008
|
+
// the next fetch would return the prompt just released instead of the addressed one
|
|
2009
|
+
// queued behind it, and the agent would never reach its own work. Holding them CLAIMED
|
|
2010
|
+
// until the queue runs dry walks past them instead. The cost is a brief CLAIMED blip on
|
|
2011
|
+
// someone else's prompt, and — if no other agent happens to be draining when the
|
|
2012
|
+
// hand-back lands — that prompt waiting for the next `prompt:created` to be noticed.
|
|
2013
|
+
if (exclusive && (p.agent_id ?? null) !== cfg.agentId) {
|
|
2014
|
+
log(`prompt ${p.id} ${p.agent_id ? `is addressed to agent ${p.agent_id}` : 'is addressed to no agent'} — handing it back (--exclusive)`);
|
|
2015
|
+
handBack.push(p.id);
|
|
2016
|
+
continue;
|
|
2017
|
+
}
|
|
1959
2018
|
log(`prompt received: "${p.prompt}" (board=${p.board_id ?? cfg.boardId ?? 'n/a'}, priority=${p.priority})`);
|
|
1960
2019
|
try {
|
|
1961
2020
|
await runClaudeWarm(buildTurn(p));
|
|
@@ -1964,6 +2023,16 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
|
|
|
1964
2023
|
}
|
|
1965
2024
|
}
|
|
1966
2025
|
} finally {
|
|
2026
|
+
for (const id of handBack) {
|
|
2027
|
+
try {
|
|
2028
|
+
await releasePrompt(id);
|
|
2029
|
+
} catch (err) {
|
|
2030
|
+
// Left CLAIMED, which is worse for whoever sent it than a retry would be — but
|
|
2031
|
+
// retrying here risks wedging the loop, and the next pass claims nothing new
|
|
2032
|
+
// while this one is still unwinding. Say so and move on.
|
|
2033
|
+
log(`handing prompt ${id} back failed, it stays CLAIMED: ${err.message}`);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
1967
2036
|
draining = false;
|
|
1968
2037
|
// A prompt turn counts as activity: the board isn't idle just because nobody edited
|
|
1969
2038
|
// it while the agent was busy answering someone.
|
|
@@ -1983,20 +2052,22 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
|
|
|
1983
2052
|
// later adds to these payloads lands here without a client change.
|
|
1984
2053
|
const BOARD_CHANGE_EVENTS = ['node:created', 'node:changed', 'node:deleted', 'edge:added', 'edge:removed', 'board:cleared'];
|
|
1985
2054
|
|
|
1986
|
-
// Four knobs
|
|
1987
|
-
//
|
|
1988
|
-
// agent's own edits are indistinguishable from the human's. They all govern *when* a
|
|
1989
|
-
// self-directed turn fires — never whether an event is remembered. Everything that
|
|
1990
|
-
// arrives is buffered (see onBoardEvent): an event seen while a turn runs, or inside the
|
|
1991
|
-
// echo window, is only *marked* as possibly the agent's own write, so a burst that
|
|
2055
|
+
// Four knobs. They govern *when* a self-directed turn fires — never whether an event is
|
|
2056
|
+
// remembered: everything that arrives is buffered (see onBoardEvent), so a burst that
|
|
1992
2057
|
// straddles a turn boundary still reaches the next turn instead of being thrown away and
|
|
1993
2058
|
// leaving the agent looking at whichever single event happened to land last.
|
|
2059
|
+
// Who wrote an event is *read off the event*, not inferred from these windows: the payload
|
|
2060
|
+
// carries `agent_id` (stamped from the writer's `x-agent-id` header) and `user_id` (a
|
|
2061
|
+
// browser session), so this agent's own echo is identified exactly and dropped without
|
|
2062
|
+
// costing a turn. The windows below only cover the one case attribution can't: a write that
|
|
2063
|
+
// carries neither id.
|
|
1994
2064
|
// DEBOUNCE — one gesture (place a node, drag a column) fans out into several
|
|
1995
2065
|
// events; wait for the board to fall quiet, then send a single turn.
|
|
1996
2066
|
// MAX_WAIT — cap on that quiet period: a board someone keeps editing never falls
|
|
1997
2067
|
// quiet, and the debounce alone would slide forever.
|
|
1998
|
-
// ECHO_WINDOW — how long after a turn its own writes are expected back;
|
|
1999
|
-
// that window
|
|
2068
|
+
// ECHO_WINDOW — how long after a turn its own writes are expected back; an
|
|
2069
|
+
// *unattributed* change in that window is labelled a possible echo, and
|
|
2070
|
+
// the next turn waits it out so a write and its echo don't each get one.
|
|
2000
2071
|
// MIN_INTERVAL — a floor between self-directed turns, so a mistake upstream can't
|
|
2001
2072
|
// become a self-feeding loop burning tokens unattended. Doubles per
|
|
2002
2073
|
// consecutive NOOP up to BACKOFF_CAP, and resets as soon as a turn
|
|
@@ -2016,10 +2087,11 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
|
|
|
2016
2087
|
const STANDALONE_BACKOFF_CAP_MS = envMs('EVENTMODELERS_STANDALONE_BACKOFF_CAP_MS', 15 * 60_000);
|
|
2017
2088
|
const STANDALONE_IDLE_MS = envMs('EVENTMODELERS_STANDALONE_IDLE_MS', 15 * 60_000);
|
|
2018
2089
|
|
|
2019
|
-
// node_id (or '(board)') -> { types: Set<string>, count
|
|
2020
|
-
//
|
|
2021
|
-
//
|
|
2022
|
-
//
|
|
2090
|
+
// node_id (or '(board)') -> { types: Set<string>, count, own, other, maybe } for
|
|
2091
|
+
// everything seen since the last self-directed turn, each event counted into exactly one
|
|
2092
|
+
// origin bucket: `own` = attributed to this agent's own id, `other` = attributed to a human
|
|
2093
|
+
// or another agent, `maybe` = carries no attribution at all and landed inside the echo
|
|
2094
|
+
// window, so it *might* be this agent's. Only `maybe` is a guess.
|
|
2023
2095
|
const observed = new Map();
|
|
2024
2096
|
let observedCount = 0;
|
|
2025
2097
|
let seqLo = null;
|
|
@@ -2036,6 +2108,16 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
|
|
|
2036
2108
|
return Math.min(STANDALONE_MIN_INTERVAL_MS * 2 ** noopStreak, STANDALONE_BACKOFF_CAP_MS);
|
|
2037
2109
|
}
|
|
2038
2110
|
|
|
2111
|
+
// Empties the buffer — every field of it, which is why it is one function and not five
|
|
2112
|
+
// lines repeated at each place a burst stops being pending.
|
|
2113
|
+
function resetObserved() {
|
|
2114
|
+
observed.clear();
|
|
2115
|
+
observedCount = 0;
|
|
2116
|
+
seqLo = null;
|
|
2117
|
+
seqHi = null;
|
|
2118
|
+
firstObservedAt = 0;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2039
2121
|
function onBoardEvent(type, payload) {
|
|
2040
2122
|
if (!standalone) {
|
|
2041
2123
|
if (verbose) log(`board event ${type} dropped — not running with --standalone`);
|
|
@@ -2046,12 +2128,26 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
|
|
|
2046
2128
|
// The warm-up turn is read-only, so a change that lands while it runs is somebody
|
|
2047
2129
|
// else's — labelling it "possibly your own write" would only teach the agent to
|
|
2048
2130
|
// discount the very edits it just came up to work on.
|
|
2049
|
-
const
|
|
2131
|
+
const guessOwn = (!!pending && !warmingUp) || draining || inEchoWindow;
|
|
2132
|
+
// Attribution beats the clock in both directions. `agent_id === ours` is this agent's own
|
|
2133
|
+
// write, certainly, whenever it comes back. Any *other* id — a person's user_id, another
|
|
2134
|
+
// agent's — is certainly not ours, which is the half the timer used to get wrong: a human
|
|
2135
|
+
// editing while this agent worked had their change written off as an echo of it.
|
|
2136
|
+
const writerAgent = payload?.agent_id || null;
|
|
2137
|
+
const writerUser = payload?.user_id || null;
|
|
2138
|
+
const origin =
|
|
2139
|
+
writerAgent && cfg.agentId && writerAgent === cfg.agentId
|
|
2140
|
+
? 'own'
|
|
2141
|
+
: writerAgent || writerUser
|
|
2142
|
+
? 'other'
|
|
2143
|
+
: guessOwn
|
|
2144
|
+
? 'maybe'
|
|
2145
|
+
: 'other';
|
|
2050
2146
|
const nodeId = payload?.node_id ?? '(board)';
|
|
2051
|
-
const entry = observed.get(nodeId) ?? { types: new Set(), count: 0,
|
|
2147
|
+
const entry = observed.get(nodeId) ?? { types: new Set(), count: 0, own: 0, other: 0, maybe: 0 };
|
|
2052
2148
|
entry.types.add(type);
|
|
2053
2149
|
entry.count += 1;
|
|
2054
|
-
|
|
2150
|
+
entry[origin] += 1;
|
|
2055
2151
|
observed.set(nodeId, entry);
|
|
2056
2152
|
observedCount += 1;
|
|
2057
2153
|
if (!firstObservedAt) firstObservedAt = Date.now();
|
|
@@ -2060,7 +2156,17 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
|
|
|
2060
2156
|
if (seqLo === null || seq < seqLo) seqLo = seq;
|
|
2061
2157
|
if (seqHi === null || seq > seqHi) seqHi = seq;
|
|
2062
2158
|
}
|
|
2063
|
-
|
|
2159
|
+
const writtenBy =
|
|
2160
|
+
origin === 'own'
|
|
2161
|
+
? ' — own write'
|
|
2162
|
+
: origin === 'maybe'
|
|
2163
|
+
? ' — unattributed, maybe own write'
|
|
2164
|
+
: writerUser
|
|
2165
|
+
? ' — by a person'
|
|
2166
|
+
: writerAgent
|
|
2167
|
+
? ` — by agent ${writerAgent.slice(0, 8)}`
|
|
2168
|
+
: '';
|
|
2169
|
+
log(`board change: ${type} node=${nodeId}${Number.isFinite(seq) ? ` seq=${seq}` : ''}${writtenBy}`);
|
|
2064
2170
|
armStandaloneTurn(nextDelayMs());
|
|
2065
2171
|
}
|
|
2066
2172
|
|
|
@@ -2125,10 +2231,17 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
|
|
|
2125
2231
|
'nothing, change nothing and reply <promise>NOOP</promise>.';
|
|
2126
2232
|
|
|
2127
2233
|
function buildStandaloneTurn() {
|
|
2128
|
-
const lines = [...observed.entries()].map(
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2234
|
+
const lines = [...observed.entries()].map(([nodeId, entry]) => {
|
|
2235
|
+
const origin =
|
|
2236
|
+
entry.own === entry.count
|
|
2237
|
+
? ' — YOUR OWN earlier write, echoed back'
|
|
2238
|
+
: entry.own
|
|
2239
|
+
? ` — ${entry.own} of ${entry.count} are YOUR OWN earlier writes, the rest are not`
|
|
2240
|
+
: entry.maybe === entry.count
|
|
2241
|
+
? ' — unattributed, possibly your own earlier write'
|
|
2242
|
+
: '';
|
|
2243
|
+
return `- ${nodeId}: ${[...entry.types].join(', ')} (${entry.count}×)${origin}`;
|
|
2244
|
+
});
|
|
2132
2245
|
const header = [
|
|
2133
2246
|
'BOARD_CHANGE',
|
|
2134
2247
|
`board_id=${cfg.boardId}`,
|
|
@@ -2177,6 +2290,15 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
|
|
|
2177
2290
|
|
|
2178
2291
|
async function dispatchStandaloneTurn({ idle = false } = {}) {
|
|
2179
2292
|
if (!idle && !observed.size) return;
|
|
2293
|
+
// Every buffered event is provably this agent's own write coming back — attribution says
|
|
2294
|
+
// so, not a timer. There is nothing new on the board, so it costs no turn; if the board
|
|
2295
|
+
// then stays quiet the idle review still comes around.
|
|
2296
|
+
if (!idle && [...observed.values()].every((entry) => entry.own === entry.count)) {
|
|
2297
|
+
log(`standalone turn skipped: ${observedCount} board event(s) on ${observed.size} node(s), all own writes`);
|
|
2298
|
+
resetObserved();
|
|
2299
|
+
armIdleReview();
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2180
2302
|
// A direct message always outranks the agent's own initiative — re-arm instead of
|
|
2181
2303
|
// queueing behind the prompt lane, so the buffer just keeps collecting meanwhile.
|
|
2182
2304
|
if (pending || draining) {
|
|
@@ -2194,17 +2316,20 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
|
|
|
2194
2316
|
if (idle) {
|
|
2195
2317
|
log(`standalone review turn: board quiet for ${Math.round(STANDALONE_IDLE_MS / 1000)}s`);
|
|
2196
2318
|
} else {
|
|
2197
|
-
const
|
|
2319
|
+
const totals = [...observed.values()].reduce(
|
|
2320
|
+
(acc, entry) => ({ own: acc.own + entry.own, maybe: acc.maybe + entry.maybe }),
|
|
2321
|
+
{ own: 0, maybe: 0 },
|
|
2322
|
+
);
|
|
2323
|
+
const breakdown = [
|
|
2324
|
+
totals.own ? `${totals.own} own` : null,
|
|
2325
|
+
totals.maybe ? `${totals.maybe} unattributed` : null,
|
|
2326
|
+
].filter(Boolean);
|
|
2198
2327
|
log(
|
|
2199
2328
|
`standalone turn: ${observedCount} board event(s) on ${observed.size} node(s)` +
|
|
2200
|
-
`${
|
|
2329
|
+
`${breakdown.length ? ` (${breakdown.join(', ')})` : ''}`,
|
|
2201
2330
|
);
|
|
2202
2331
|
}
|
|
2203
|
-
|
|
2204
|
-
observedCount = 0;
|
|
2205
|
-
seqLo = null;
|
|
2206
|
-
seqHi = null;
|
|
2207
|
-
firstObservedAt = 0;
|
|
2332
|
+
resetObserved();
|
|
2208
2333
|
lastStandaloneAt = Date.now();
|
|
2209
2334
|
try {
|
|
2210
2335
|
const result = await runClaudeWarm(text);
|
|
@@ -2806,6 +2931,7 @@ credentialFlags(program
|
|
|
2806
2931
|
.option('--modeling', 'Keep one Claude process warm across prompts instead of spawning a fresh one per task, for low-latency voice/live use. Runs from a modeling-kit install in this directory, or from the global install (~/.eventmodelers/kit) when there is none. Built into the CLI, not a per-project file.')
|
|
2807
2932
|
.option('--standalone', 'Let the modeling agent work the board in the background, on its own initiative: on top of direct prompts it subscribes to the board\'s change channel (like the build agents do) and, whenever the board goes quiet after an edit — or has simply been idle for a while — it takes a turn nobody asked for. Changed nodes are a notification, not the task: it judges the model as a whole and fans the work out over parallel subagents, one per changed area (examples on a new node, specs for a new command or read model, a missing attribute along a chain, a screen, a question comment). Filling that detail in while the human keeps modeling is the point — it does not wait for the board to be finished. Implies --modeling.')
|
|
2808
2933
|
.option('--max-agents <n>', 'Cap how many subagents a self-directed --standalone turn may dispatch at once, to bound what an unattended agent can spend per turn. The agent merges work that shares a slice or chain first, then takes the most valuable pieces up to this many and leaves the rest for a later turn. 1 makes it do the single most valuable piece itself, without spawning anything. Default 5. Ignored without --standalone — prompt turns are one piece of work by definition.', '5')
|
|
2934
|
+
.option('--exclusive', 'Work only the prompts addressed to this agent\'s id — the board\'s "preferred agent" (the star in the prompts panel) — and hand every untargeted prompt straight back to the queue for another agent to take. Without it an agent also works everything nobody addressed to anyone, which is what you want for a single agent and exactly what you do not want for a dedicated one (a board with a general agent plus a specialist, or an agent a supervisor drives by id). Pair it with --id so the same agent is addressable across restarts — --global/--standalone otherwise mint a fresh id per run, and prompts addressed to the previous run\'s id are never claimed. Leaves --standalone alone: a self-directed turn is nobody\'s prompt, so an exclusive standalone agent still works the board on its own initiative.')
|
|
2809
2935
|
.option('--global', 'Run the modeling agent from the global install (~/.eventmodelers/kit), initializing it on first use, and ignore any kit in this directory. This is also what --modeling/--standalone fall back to on their own when nothing is installed here — pass it explicitly to prefer the global install over a local one. Credentials come from the flags below, EVENTMODELERS_* env vars, or ~/.eventmodelers/boards/<board>.json, so nothing is written into the current directory.')
|
|
2810
2936
|
.option('--local', 'Skip platform config/credential lookup entirely and run the local-only loop (no board sync, no realtime agent) — even if .eventmodelers/config.json has credentials (build-kit stacks only)')
|
|
2811
2937
|
.option('--verbose', 'Log every tool call\'s full input (commands, skill args, file paths) and assistant reasoning text. Default is condensed, high-level per-step logging only.')
|
|
@@ -2822,6 +2948,11 @@ credentialFlags(program
|
|
|
2822
2948
|
if (command.getOptionValueSource('maxAgents') === 'cli' && !opts.standalone) {
|
|
2823
2949
|
console.log('ℹ️ --max-agents only applies to --standalone turns; ignoring it here.');
|
|
2824
2950
|
}
|
|
2951
|
+
// The build-kit runners claim their work from the same queue but have no addressee
|
|
2952
|
+
// filter, so the flag would silently do nothing there rather than half of what it says.
|
|
2953
|
+
if (opts.exclusive && !(opts.modeling || opts.standalone || opts.global)) {
|
|
2954
|
+
console.log('ℹ️ --exclusive only applies to the modeling loop (--modeling/--standalone/--global); ignoring it here.');
|
|
2955
|
+
}
|
|
2825
2956
|
// --id/--name are what the platform will see for this run, so a blank one is a
|
|
2826
2957
|
// mistake worth failing on rather than silently falling back to the stored identity.
|
|
2827
2958
|
const identity = {
|
|
@@ -2902,7 +3033,7 @@ credentialFlags(program
|
|
|
2902
3033
|
const shown = relative(cwd, kitDir);
|
|
2903
3034
|
await new Promise((res) => process.stdout.write(`▶ Starting modeling loop (warm Claude process) for ${shown && !shown.startsWith('..') ? shown : kitDir}...\n\n`, res));
|
|
2904
3035
|
try {
|
|
2905
|
-
await runModeling(kitDir, projectDir, !!opts.verbose, !!opts.standalone, overrides, maxAgents, identity);
|
|
3036
|
+
await runModeling(kitDir, projectDir, { verbose: !!opts.verbose, standalone: !!opts.standalone, exclusive: !!opts.exclusive, overrides, maxAgents, identity });
|
|
2906
3037
|
} catch (err) {
|
|
2907
3038
|
console.error('[modeling] Fatal:', err);
|
|
2908
3039
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eventmodelers/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.72",
|
|
4
4
|
"description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, Cratis, OpenCQRS, UmaDB, Kurrent, or modeling-only)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -860,6 +860,8 @@ Claim the next pending (`ADDED`) prompt for a board — atomically flips it to `
|
|
|
860
860
|
|
|
861
861
|
Send `x-agent-id` here too: a prompt the user addressed to one preferred agent (`prompts.agent_id`) is only ever handed to the agent claiming with that id, and a caller without the header claims untargeted prompts only. Addressing one is `POST /api/org/:orgId/prompts` with `agent_id: "<uuid>"` — the board's prompts panel does it when someone stars an agent.
|
|
862
862
|
|
|
863
|
+
Note what this endpoint does *not* do: an agent that sends its id still gets every untargeted prompt on top of its own. A caller that wants only what was addressed to it has to hand the rest back itself (`POST /prompts/:id/status` with `status: 'ADDED'`), which is what `eventmodelers run --exclusive` does.
|
|
864
|
+
|
|
863
865
|
**Query params**: `board_id` (required)
|
|
864
866
|
**Response**: `200` — the claimed row (now `status: "CLAIMED"`), including its parsed `context` and the `hidden` flag · `404` — no `ADDED` prompts available
|
|
865
867
|
|
|
@@ -879,7 +881,7 @@ Set a prompt's status, optionally attaching a progress comment. Auth: `x-token`
|
|
|
879
881
|
```
|
|
880
882
|
|
|
881
883
|
**Response**: `200` — the updated row
|
|
882
|
-
**Errors**: `400` invalid/missing `status` · `403` token not for this prompt's org · `404` prompt not found
|
|
884
|
+
**Errors**: `400` invalid/missing `status` · `403` token not for this prompt's org · `404` prompt not found · `409` (`PROMPT_STATUS_ALREADY_SET`) the prompt already carries that status — another agent already made this transition, so don't retry it
|
|
883
885
|
|
|
884
886
|
---
|
|
885
887
|
|
|
@@ -52,6 +52,7 @@ Omit `comment` entirely when there isn't one — don't pass an empty string.
|
|
|
52
52
|
|----------|---------|--------|
|
|
53
53
|
| `400` | `status` missing or not a valid value | Fix the value and retry — do not retry with the same bad value. |
|
|
54
54
|
| `404` | Prompt not found | The prompt may have been deleted by its author while you were working. Report this and move on — do not treat it as a failure of your actual task work. |
|
|
55
|
+
| `409` | The prompt already carries the status you asked for (`PROMPT_STATUS_ALREADY_SET`) | Somebody else — usually another agent working the same queue — already made this transition. Do not retry it; treat the transition as done and carry on. |
|
|
55
56
|
| `401`/`403` | Token invalid or wrong organization | Re-run `connect` to refresh credentials, then retry once. |
|
|
56
57
|
|
|
57
58
|
---
|
|
@@ -199,6 +199,9 @@ Steps:
|
|
|
199
199
|
|
|
200
200
|
Keep these turns finished within the turn: wait for the subagents you dispatched, don't leave
|
|
201
201
|
work trailing. Everything you and they write to the board comes back on this same channel as
|
|
202
|
-
another change
|
|
203
|
-
|
|
204
|
-
|
|
202
|
+
another change. A burst that is *only* your own writes never becomes a turn at all, so a
|
|
203
|
+
change list you are handed always contains something that isn't yours — but it may still list
|
|
204
|
+
yours alongside it, marked `YOUR OWN earlier write`. Take that mark literally: those lines are
|
|
205
|
+
there for context, not to be reworked. A line marked `unattributed, possibly your own earlier
|
|
206
|
+
write` is the one uncertain case (a write that reached the platform without an agent id);
|
|
207
|
+
anything unmarked was written by someone else and is real work to look at.
|