@monoes/monomindcli 2.12.0 → 2.14.0
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/.claude/skills/mastermind/SKILL.md +1 -15
- package/README.md +1 -1
- package/dist/src/commands/doctor-mcp-probe.d.ts +48 -0
- package/dist/src/commands/doctor-mcp-probe.d.ts.map +1 -0
- package/dist/src/commands/doctor-mcp-probe.js +205 -0
- package/dist/src/commands/doctor-mcp-probe.js.map +1 -0
- package/dist/src/commands/doctor-monoes-checks.d.ts +1 -0
- package/dist/src/commands/doctor-monoes-checks.d.ts.map +1 -1
- package/dist/src/commands/doctor-monoes-checks.js +96 -34
- package/dist/src/commands/doctor-monoes-checks.js.map +1 -1
- package/dist/src/commands/doctor-project-checks.d.ts +31 -1
- package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
- package/dist/src/commands/doctor-project-checks.js +109 -4
- package/dist/src/commands/doctor-project-checks.js.map +1 -1
- package/dist/src/commands/doctor.d.ts.map +1 -1
- package/dist/src/commands/doctor.js +21 -3
- package/dist/src/commands/doctor.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +29 -0
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/mcp.d.ts.map +1 -1
- package/dist/src/commands/mcp.js +4 -3
- package/dist/src/commands/mcp.js.map +1 -1
- package/dist/src/commands/org-observe.d.ts.map +1 -1
- package/dist/src/commands/org-observe.js +14 -0
- package/dist/src/commands/org-observe.js.map +1 -1
- package/dist/src/commands/org.d.ts.map +1 -1
- package/dist/src/commands/org.js +21 -4
- package/dist/src/commands/org.js.map +1 -1
- package/dist/src/commands/ui.d.ts.map +1 -1
- package/dist/src/commands/ui.js +4 -0
- package/dist/src/commands/ui.js.map +1 -1
- package/dist/src/init/claudemd-generator.d.ts.map +1 -1
- package/dist/src/init/claudemd-generator.js +2 -1
- package/dist/src/init/claudemd-generator.js.map +1 -1
- package/dist/src/init/copy-assets.d.ts.map +1 -1
- package/dist/src/init/copy-assets.js +56 -18
- package/dist/src/init/copy-assets.js.map +1 -1
- package/dist/src/init/executor.d.ts.map +1 -1
- package/dist/src/init/executor.js +9 -1
- package/dist/src/init/executor.js.map +1 -1
- package/dist/src/init/mcp-generator.d.ts.map +1 -1
- package/dist/src/init/mcp-generator.js +3 -3
- package/dist/src/init/mcp-generator.js.map +1 -1
- package/dist/src/init/never-commit.d.ts +39 -0
- package/dist/src/init/never-commit.d.ts.map +1 -0
- package/dist/src/init/never-commit.js +15 -0
- package/dist/src/init/never-commit.js.map +1 -0
- package/dist/src/init/shared.d.ts +74 -1
- package/dist/src/init/shared.d.ts.map +1 -1
- package/dist/src/init/shared.js +166 -0
- package/dist/src/init/shared.js.map +1 -1
- package/dist/src/init/types.d.ts +11 -0
- package/dist/src/init/types.d.ts.map +1 -1
- package/dist/src/init/types.js.map +1 -1
- package/dist/src/init/upgrade.d.ts.map +1 -1
- package/dist/src/init/upgrade.js +1 -0
- package/dist/src/init/upgrade.js.map +1 -1
- package/dist/src/init/write-kimicode.d.ts.map +1 -1
- package/dist/src/init/write-kimicode.js +13 -9
- package/dist/src/init/write-kimicode.js.map +1 -1
- package/dist/src/init/write-opencode.d.ts.map +1 -1
- package/dist/src/init/write-opencode.js +44 -2
- package/dist/src/init/write-opencode.js.map +1 -1
- package/dist/src/init/write-runtime-config.d.ts +2 -0
- package/dist/src/init/write-runtime-config.d.ts.map +1 -1
- package/dist/src/init/write-runtime-config.js +65 -38
- package/dist/src/init/write-runtime-config.js.map +1 -1
- package/dist/src/mastermind/manifest-data.d.ts.map +1 -1
- package/dist/src/mastermind/manifest-data.js +14 -0
- package/dist/src/mastermind/manifest-data.js.map +1 -1
- package/dist/src/mcp/monoes-mcp-entry.mjs +86 -0
- package/dist/src/mcp-tools/terminal-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/terminal-tools.js +52 -9
- package/dist/src/mcp-tools/terminal-tools.js.map +1 -1
- package/dist/src/memory/memory-bridge.d.ts +84 -3
- package/dist/src/memory/memory-bridge.d.ts.map +1 -1
- package/dist/src/memory/memory-bridge.js +215 -17
- package/dist/src/memory/memory-bridge.js.map +1 -1
- package/dist/src/orgrt/agent-runner.d.ts +38 -0
- package/dist/src/orgrt/agent-runner.d.ts.map +1 -1
- package/dist/src/orgrt/agent-runner.js +47 -0
- package/dist/src/orgrt/agent-runner.js.map +1 -1
- package/dist/src/orgrt/checkpoint.d.ts +12 -1
- package/dist/src/orgrt/checkpoint.d.ts.map +1 -1
- package/dist/src/orgrt/checkpoint.js +15 -6
- package/dist/src/orgrt/checkpoint.js.map +1 -1
- package/dist/src/orgrt/completion-gate.d.ts +74 -0
- package/dist/src/orgrt/completion-gate.d.ts.map +1 -1
- package/dist/src/orgrt/completion-gate.js +45 -0
- package/dist/src/orgrt/completion-gate.js.map +1 -1
- package/dist/src/orgrt/cost-tier.d.ts +144 -0
- package/dist/src/orgrt/cost-tier.d.ts.map +1 -0
- package/dist/src/orgrt/cost-tier.js +183 -0
- package/dist/src/orgrt/cost-tier.js.map +1 -0
- package/dist/src/orgrt/cross-org.d.ts.map +1 -1
- package/dist/src/orgrt/cross-org.js +14 -0
- package/dist/src/orgrt/cross-org.js.map +1 -1
- package/dist/src/orgrt/daemon.d.ts +12 -1
- package/dist/src/orgrt/daemon.d.ts.map +1 -1
- package/dist/src/orgrt/daemon.js +216 -49
- package/dist/src/orgrt/daemon.js.map +1 -1
- package/dist/src/orgrt/decisions.d.ts +14 -2
- package/dist/src/orgrt/decisions.d.ts.map +1 -1
- package/dist/src/orgrt/decisions.js +216 -10
- package/dist/src/orgrt/decisions.js.map +1 -1
- package/dist/src/orgrt/file-roots.d.ts +7 -0
- package/dist/src/orgrt/file-roots.d.ts.map +1 -1
- package/dist/src/orgrt/file-roots.js +38 -1
- package/dist/src/orgrt/file-roots.js.map +1 -1
- package/dist/src/orgrt/forwarder.d.ts.map +1 -1
- package/dist/src/orgrt/forwarder.js +16 -2
- package/dist/src/orgrt/forwarder.js.map +1 -1
- package/dist/src/orgrt/idle-deadline.d.ts +74 -1
- package/dist/src/orgrt/idle-deadline.d.ts.map +1 -1
- package/dist/src/orgrt/idle-deadline.js +52 -2
- package/dist/src/orgrt/idle-deadline.js.map +1 -1
- package/dist/src/orgrt/loadouts.d.ts +38 -0
- package/dist/src/orgrt/loadouts.d.ts.map +1 -0
- package/dist/src/orgrt/loadouts.js +129 -0
- package/dist/src/orgrt/loadouts.js.map +1 -0
- package/dist/src/orgrt/mailbox.d.ts +17 -1
- package/dist/src/orgrt/mailbox.d.ts.map +1 -1
- package/dist/src/orgrt/mailbox.js +50 -3
- package/dist/src/orgrt/mailbox.js.map +1 -1
- package/dist/src/orgrt/policy.d.ts +42 -2
- package/dist/src/orgrt/policy.d.ts.map +1 -1
- package/dist/src/orgrt/policy.js +53 -8
- package/dist/src/orgrt/policy.js.map +1 -1
- package/dist/src/orgrt/prompt-vars.d.ts +11 -0
- package/dist/src/orgrt/prompt-vars.d.ts.map +1 -0
- package/dist/src/orgrt/prompt-vars.js +49 -0
- package/dist/src/orgrt/prompt-vars.js.map +1 -0
- package/dist/src/orgrt/questions.d.ts +22 -1
- package/dist/src/orgrt/questions.d.ts.map +1 -1
- package/dist/src/orgrt/questions.js +24 -3
- package/dist/src/orgrt/questions.js.map +1 -1
- package/dist/src/orgrt/review-packet.d.ts +29 -0
- package/dist/src/orgrt/review-packet.d.ts.map +1 -0
- package/dist/src/orgrt/review-packet.js +67 -0
- package/dist/src/orgrt/review-packet.js.map +1 -0
- package/dist/src/orgrt/role-sandbox.d.ts.map +1 -1
- package/dist/src/orgrt/role-sandbox.js +16 -3
- package/dist/src/orgrt/role-sandbox.js.map +1 -1
- package/dist/src/orgrt/session-ledger.d.ts +61 -0
- package/dist/src/orgrt/session-ledger.d.ts.map +1 -0
- package/dist/src/orgrt/session-ledger.js +113 -0
- package/dist/src/orgrt/session-ledger.js.map +1 -0
- package/dist/src/orgrt/session.d.ts +45 -5
- package/dist/src/orgrt/session.d.ts.map +1 -1
- package/dist/src/orgrt/session.js +384 -27
- package/dist/src/orgrt/session.js.map +1 -1
- package/dist/src/orgrt/task-dag.d.ts +34 -1
- package/dist/src/orgrt/task-dag.d.ts.map +1 -1
- package/dist/src/orgrt/task-dag.js +58 -7
- package/dist/src/orgrt/task-dag.js.map +1 -1
- package/dist/src/orgrt/tool-spill.d.ts +94 -0
- package/dist/src/orgrt/tool-spill.d.ts.map +1 -0
- package/dist/src/orgrt/tool-spill.js +180 -0
- package/dist/src/orgrt/tool-spill.js.map +1 -0
- package/dist/src/orgrt/types.d.ts +138 -0
- package/dist/src/orgrt/types.d.ts.map +1 -1
- package/dist/src/orgrt/types.js +148 -0
- package/dist/src/orgrt/types.js.map +1 -1
- package/dist/src/platform-adapters/renderers/mcp.d.ts +20 -2
- package/dist/src/platform-adapters/renderers/mcp.d.ts.map +1 -1
- package/dist/src/platform-adapters/renderers/mcp.js +25 -5
- package/dist/src/platform-adapters/renderers/mcp.js.map +1 -1
- package/dist/src/platform-adapters/renderers/skills.d.ts +0 -6
- package/dist/src/platform-adapters/renderers/skills.d.ts.map +1 -1
- package/dist/src/platform-adapters/renderers/skills.js +12 -22
- package/dist/src/platform-adapters/renderers/skills.js.map +1 -1
- package/dist/src/protocol-capabilities.d.ts +2 -1
- package/dist/src/protocol-capabilities.d.ts.map +1 -1
- package/dist/src/protocol-capabilities.js +2 -1
- package/dist/src/protocol-capabilities.js.map +1 -1
- package/dist/src/ui/dashboard.html +457 -164
- package/dist/src/ui/org-hil.mjs +276 -0
- package/dist/src/ui/org-runtime.mjs +350 -0
- package/dist/src/ui/orgs.html +22 -4
- package/dist/src/ui/routes-monoes.mjs +28 -3
- package/dist/src/ui/routes-org.mjs +290 -541
- package/dist/src/ui/server.mjs +185 -19
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -6
|
@@ -12,10 +12,15 @@ import { StateDetector } from './state-detector.js';
|
|
|
12
12
|
* "boss appears hung". */
|
|
13
13
|
const SILENT_SESSION_MS = 4 * 60_000;
|
|
14
14
|
const CONTEXT_LIMIT_RE = /context.window.limit|context.length.exceeded|maximum.context/i;
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
15
16
|
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { resolveRoleCostTier } from './cost-tier.js';
|
|
19
|
+
import { expandRolePromptVars, promptVarsFor } from './prompt-vars.js';
|
|
16
20
|
import { resolveProviderEnv, resolveRoleProvider } from './provider.js';
|
|
17
21
|
import { resolveRoleGitEnforcement } from './role-sandbox.js';
|
|
18
22
|
import { loadBuiltinRoleSkill } from './role-skills.js';
|
|
23
|
+
import { ROLE_SESSION_KEY, resolveSessionScope, SessionLedger, taskKeyOf, } from './session-ledger.js';
|
|
19
24
|
import { DEFAULT_CLAUDE_MODEL, VERCEL_PROVIDERS } from './vercel-providers.js';
|
|
20
25
|
/**
|
|
21
26
|
* Resolves the extra system-prompt block for a role: built-in archetype
|
|
@@ -189,6 +194,14 @@ endpointBriefing) {
|
|
|
189
194
|
.filter(Boolean)
|
|
190
195
|
.join('\n\n');
|
|
191
196
|
}
|
|
197
|
+
/** The system prompt one session of this role is built with. */
|
|
198
|
+
function rolePromptFor(opts) {
|
|
199
|
+
return buildRolePrompt(expandRolePromptVars(opts.role, promptVarsFor(opts.orgRoot ?? opts.cwd)), (opts.def ?? { name: opts.org, goal: '' }), opts.def?.roles.map((r) => r.id) ?? [opts.role.id], opts.glossary,
|
|
200
|
+
// D7: the loadout's text follows the role's own guidance. With no
|
|
201
|
+
// loadout this is exactly resolveRoleExtraGuidance(role), as before.
|
|
202
|
+
[resolveRoleExtraGuidance(opts.role), opts.loadout?.guidance].filter(Boolean).join('\n\n') ||
|
|
203
|
+
undefined, opts.onComplete ? endpointBriefingLines(opts.def) : undefined);
|
|
204
|
+
}
|
|
192
205
|
/**
|
|
193
206
|
* Runs a role for the life of the org, transparently restarting the
|
|
194
207
|
* underlying SDK session whenever it ends on its own (`maxTurns` reached)
|
|
@@ -248,23 +261,138 @@ async function runAgentSessionLoop(opts) {
|
|
|
248
261
|
// SDK session id here (it must outlive individual runOneSession calls,
|
|
249
262
|
// since resume continues the same billing session) and emit only deltas.
|
|
250
263
|
const sessionCostTotals = new Map();
|
|
264
|
+
// ADR-O001 D1: modelUsage is cumulative per session exactly like
|
|
265
|
+
// total_cost_usd, so it needs the same prev-value map to become a delta.
|
|
266
|
+
const sessionTokenTotals = new Map();
|
|
267
|
+
// ADR-O001 D3. 'role' scope (the default) keeps the pre-D3 loop exactly: one
|
|
268
|
+
// model session for the role's life, resumed across maxTurns restarts via
|
|
269
|
+
// resumeSessionId. 'task' scope keys model sessions by the task a message
|
|
270
|
+
// belongs to: the process exits at a task boundary (or on idle) and the next
|
|
271
|
+
// one resumes that task's session from the ledger. Either way every session
|
|
272
|
+
// run is recorded with its session id before and after.
|
|
273
|
+
const scope = resolveSessionScope(opts.role, opts.def);
|
|
274
|
+
const idleExitMs = opts.def?.run_config
|
|
275
|
+
?.session_idle_exit_ms;
|
|
276
|
+
const ledger = opts.sessionLedger ?? new SessionLedger();
|
|
277
|
+
const runtimeKey = opts.role.runtime ?? opts.def?.runtime ?? 'claude';
|
|
278
|
+
let taskKey = ROLE_SESSION_KEY;
|
|
279
|
+
// Why the next fresh session for a key is fresh, when the loop itself threw
|
|
280
|
+
// the record away (stale resume, turn-limit error) — recorded, not guessed.
|
|
281
|
+
const droppedBecause = new Map();
|
|
282
|
+
const staleTried = new Set();
|
|
283
|
+
// The options THIS session is built from — the role's own, except that a
|
|
284
|
+
// task-scoped session carries its task's loadout (D7).
|
|
285
|
+
let sessionOpts = opts;
|
|
286
|
+
let promptHash = '*';
|
|
251
287
|
// Always run at least once: a mailbox can be closed with queued items still
|
|
252
288
|
// pending (stream() drains the queue before honoring `closed`), which is a
|
|
253
289
|
// normal, valid starting state - checking isClosed before the first run
|
|
254
290
|
// would skip that drain entirely.
|
|
255
291
|
while (true) {
|
|
292
|
+
// Opt-in only: keep the process DOWN until there is mail, instead of
|
|
293
|
+
// starting a query() that parks on an empty mailbox. waitForMessage()
|
|
294
|
+
// still returns true for a closed mailbox with queued items.
|
|
295
|
+
if (scope !== 'role' || idleExitMs !== undefined) {
|
|
296
|
+
if (!(await mailbox.waitForMessage()))
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
let startReason;
|
|
300
|
+
if (scope === 'cold') {
|
|
301
|
+
// D6: nothing carries over — not a checkpointed session, not the last
|
|
302
|
+
// message's. Paying the cache miss here is the point.
|
|
303
|
+
resumeSessionId = undefined;
|
|
304
|
+
startReason = 'fresh-cold';
|
|
305
|
+
}
|
|
306
|
+
else if (scope === 'task') {
|
|
307
|
+
// An untagged message (mail, an answer, a continuation) belongs to the
|
|
308
|
+
// session the role is already in.
|
|
309
|
+
taskKey = taskKeyOf(mailbox.peek() ?? '') ?? taskKey;
|
|
310
|
+
// D7: this task's session is built with this task's loadout.
|
|
311
|
+
sessionOpts =
|
|
312
|
+
taskKey !== ROLE_SESSION_KEY && opts.loadoutFor
|
|
313
|
+
? { ...opts, loadout: opts.loadoutFor(taskKey) }
|
|
314
|
+
: opts;
|
|
315
|
+
promptHash = createHash('sha256')
|
|
316
|
+
.update(rolePromptFor(sessionOpts))
|
|
317
|
+
.digest('hex')
|
|
318
|
+
.slice(0, 16);
|
|
319
|
+
const pick = ledger.resumeFor({
|
|
320
|
+
role: opts.role.id,
|
|
321
|
+
runtime: runtimeKey,
|
|
322
|
+
taskKey,
|
|
323
|
+
cwd: opts.cwd,
|
|
324
|
+
promptHash,
|
|
325
|
+
});
|
|
326
|
+
resumeSessionId = pick.sessionId;
|
|
327
|
+
startReason =
|
|
328
|
+
pick.reason === 'fresh-no-record'
|
|
329
|
+
? (droppedBecause.get(taskKey) ?? pick.reason)
|
|
330
|
+
: pick.reason;
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
startReason = resumeSessionId ? 'resumed' : 'fresh-no-record';
|
|
334
|
+
}
|
|
335
|
+
const sessionKey = taskKey;
|
|
336
|
+
const streamOpts = scope === 'cold'
|
|
337
|
+
? { stopBefore: () => true, idleExitMs }
|
|
338
|
+
: scope === 'task'
|
|
339
|
+
? {
|
|
340
|
+
stopBefore: (next) => {
|
|
341
|
+
const k = taskKeyOf(next);
|
|
342
|
+
return k !== undefined && k !== sessionKey;
|
|
343
|
+
},
|
|
344
|
+
idleExitMs,
|
|
345
|
+
}
|
|
346
|
+
: idleExitMs !== undefined
|
|
347
|
+
? { idleExitMs }
|
|
348
|
+
: undefined;
|
|
349
|
+
const sessionIdBefore = resumeSessionId;
|
|
350
|
+
const startedAt = Date.now();
|
|
351
|
+
const recordRun = (after, error) => {
|
|
352
|
+
const run = ledger.recordRun({
|
|
353
|
+
role: opts.role.id,
|
|
354
|
+
runtime: runtimeKey,
|
|
355
|
+
taskKey: sessionKey,
|
|
356
|
+
sessionIdBefore,
|
|
357
|
+
sessionIdAfter: after,
|
|
358
|
+
reason: startReason,
|
|
359
|
+
startedAt,
|
|
360
|
+
endedAt: Date.now(),
|
|
361
|
+
...(error ? { error } : {}),
|
|
362
|
+
});
|
|
363
|
+
opts.bus.emit({
|
|
364
|
+
type: 'audit',
|
|
365
|
+
from: opts.role.id,
|
|
366
|
+
reason: 'session-run',
|
|
367
|
+
msg: `session ${run.resumed ? 'resumed' : 'started fresh'} (${startReason}) for ${sessionKey}`,
|
|
368
|
+
data: run,
|
|
369
|
+
});
|
|
370
|
+
};
|
|
256
371
|
const realBefore = mailbox.consumedRealCount;
|
|
257
372
|
let sessionId;
|
|
258
373
|
let hitTurnLimit = false;
|
|
259
374
|
const attempt = { replied: false };
|
|
260
375
|
try {
|
|
261
|
-
const res = await runOneSession(
|
|
376
|
+
const res = await runOneSession(sessionOpts, resumeSessionId, sessionCostTotals, attempt, sessionTokenTotals, streamOpts);
|
|
262
377
|
sessionId = res.sessionId;
|
|
263
378
|
hitTurnLimit = res.hitTurnLimit;
|
|
264
379
|
resumeSessionId = sessionId;
|
|
380
|
+
recordRun(sessionId);
|
|
381
|
+
if (scope === 'task' && sessionId) {
|
|
382
|
+
droppedBecause.delete(sessionKey);
|
|
383
|
+
ledger.set({
|
|
384
|
+
role: opts.role.id,
|
|
385
|
+
runtime: runtimeKey,
|
|
386
|
+
taskKey: sessionKey,
|
|
387
|
+
cwd: opts.cwd,
|
|
388
|
+
promptHash,
|
|
389
|
+
sessionId,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
265
392
|
}
|
|
266
393
|
catch (err) {
|
|
267
394
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
395
|
+
recordRun(undefined, errMsg);
|
|
268
396
|
// #304 (review round 3): an org's own stop aborts whatever this attempt
|
|
269
397
|
// was doing — max-turns and stale-resume below are diagnoses for a
|
|
270
398
|
// genuinely failed attempt, not for one the org itself just cut off.
|
|
@@ -285,8 +413,36 @@ async function runAgentSessionLoop(opts) {
|
|
|
285
413
|
sessionId = undefined;
|
|
286
414
|
resumeSessionId = undefined;
|
|
287
415
|
hitTurnLimit = true;
|
|
416
|
+
if (scope === 'task') {
|
|
417
|
+
ledger.drop({ role: opts.role.id, runtime: runtimeKey, taskKey: sessionKey });
|
|
418
|
+
droppedBecause.set(sessionKey, 'fresh-after-turn-limit');
|
|
419
|
+
}
|
|
288
420
|
}
|
|
289
421
|
else if (!stopping &&
|
|
422
|
+
scope === 'task' &&
|
|
423
|
+
sessionIdBefore !== undefined &&
|
|
424
|
+
!staleTried.has(sessionKey) &&
|
|
425
|
+
!attempt.replied) {
|
|
426
|
+
// D3's per-key form of #149 below: a recorded session that fails
|
|
427
|
+
// before replying is treated as expired — forget it and retry that
|
|
428
|
+
// task fresh once. Anything it had already pulled goes back first.
|
|
429
|
+
staleTried.add(sessionKey);
|
|
430
|
+
ledger.drop({ role: opts.role.id, runtime: runtimeKey, taskKey: sessionKey });
|
|
431
|
+
droppedBecause.set(sessionKey, 'fresh-after-stale-resume');
|
|
432
|
+
mailbox.reclaimInFlight();
|
|
433
|
+
sessionId = undefined;
|
|
434
|
+
resumeSessionId = undefined;
|
|
435
|
+
hitTurnLimit = false;
|
|
436
|
+
opts.bus.emit({
|
|
437
|
+
type: 'status',
|
|
438
|
+
from: opts.role.id,
|
|
439
|
+
reason: 'resume-session-stale',
|
|
440
|
+
msg: `agent "${opts.role.id}" could not resume its session for ${sessionKey} — retrying with a fresh session`,
|
|
441
|
+
data: { error: errMsg },
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
else if (!stopping &&
|
|
445
|
+
scope === 'role' &&
|
|
290
446
|
resumeSessionId &&
|
|
291
447
|
resumeSessionId === initialResumeSessionId &&
|
|
292
448
|
!triedFreshAfterResumeFailure &&
|
|
@@ -357,6 +513,17 @@ async function runAgentSessionLoop(opts) {
|
|
|
357
513
|
});
|
|
358
514
|
}
|
|
359
515
|
}
|
|
516
|
+
else if (mailbox.lastStreamEnd) {
|
|
517
|
+
// D3: the process ended on purpose — a task boundary or idle — and the
|
|
518
|
+
// model session is kept for the next wake.
|
|
519
|
+
opts.bus.emit({
|
|
520
|
+
type: 'status',
|
|
521
|
+
from: opts.role.id,
|
|
522
|
+
reason: 'session-cycled',
|
|
523
|
+
msg: `process cycled (${mailbox.lastStreamEnd}); model session kept for ${sessionKey}`,
|
|
524
|
+
data: { taskKey: sessionKey, end: mailbox.lastStreamEnd, sessionId },
|
|
525
|
+
});
|
|
526
|
+
}
|
|
360
527
|
else {
|
|
361
528
|
opts.bus.emit({
|
|
362
529
|
type: 'status',
|
|
@@ -366,11 +533,68 @@ async function runAgentSessionLoop(opts) {
|
|
|
366
533
|
}
|
|
367
534
|
}
|
|
368
535
|
}
|
|
536
|
+
/** ADR-O001 D1 — token-metering helpers.
|
|
537
|
+
*
|
|
538
|
+
* `cache_read_input_tokens` and `cache_creation_input_tokens` are siblings
|
|
539
|
+
* of `input_tokens` in the Anthropic API, not subsets of it, and both are
|
|
540
|
+
* billable. Everything below therefore sums all four. */
|
|
541
|
+
function totalTokens(u) {
|
|
542
|
+
return u.input + u.output + u.cacheRead + u.cacheCreation;
|
|
543
|
+
}
|
|
544
|
+
function addTo(target, add) {
|
|
545
|
+
target.input += add.input;
|
|
546
|
+
target.output += add.output;
|
|
547
|
+
target.cacheRead += add.cacheRead;
|
|
548
|
+
target.cacheCreation += add.cacheCreation;
|
|
549
|
+
}
|
|
550
|
+
/** One model turn's own usage, off an 'assistant' (or per-turn 'result')
|
|
551
|
+
* message. */
|
|
552
|
+
function turnBreakdown(m) {
|
|
553
|
+
return {
|
|
554
|
+
input: m.input_tokens ?? 0,
|
|
555
|
+
output: m.output_tokens ?? 0,
|
|
556
|
+
cacheRead: m.cache_read_input_tokens ?? 0,
|
|
557
|
+
cacheCreation: m.cache_creation_input_tokens ?? 0,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
/** What a 'result' message says this mailbox message consumed.
|
|
561
|
+
*
|
|
562
|
+
* When the runner reports `cumulative_tokens` (the Claude SDK's whole-pipeline
|
|
563
|
+
* `modelUsage`, which unlike `usage` includes Task subagents and sidechains),
|
|
564
|
+
* that value is CUMULATIVE per session — the same lifecycle as
|
|
565
|
+
* `total_cost_usd` — so it is converted to a delta against the previous value
|
|
566
|
+
* for the same session_id. A fresh/restarted session has no prior entry and
|
|
567
|
+
* correctly yields its full value; a value that ticks down (a provider-side
|
|
568
|
+
* correction) floors at 0 rather than re-adding the whole cumulative total.
|
|
569
|
+
* Without `cumulative_tokens` the per-turn fields are used as before. */
|
|
570
|
+
function resultBreakdown(m, tokenTotals, sid) {
|
|
571
|
+
const cum = m.cumulative_tokens;
|
|
572
|
+
if (!cum)
|
|
573
|
+
return turnBreakdown(m);
|
|
574
|
+
const now = {
|
|
575
|
+
input: cum.input,
|
|
576
|
+
output: cum.output,
|
|
577
|
+
cacheRead: cum.cache_read,
|
|
578
|
+
cacheCreation: cum.cache_creation,
|
|
579
|
+
};
|
|
580
|
+
if (!tokenTotals)
|
|
581
|
+
return now;
|
|
582
|
+
const prev = tokenTotals.get(sid);
|
|
583
|
+
tokenTotals.set(sid, now);
|
|
584
|
+
if (!prev)
|
|
585
|
+
return now;
|
|
586
|
+
return {
|
|
587
|
+
input: Math.max(0, now.input - prev.input),
|
|
588
|
+
output: Math.max(0, now.output - prev.output),
|
|
589
|
+
cacheRead: Math.max(0, now.cacheRead - prev.cacheRead),
|
|
590
|
+
cacheCreation: Math.max(0, now.cacheCreation - prev.cacheCreation),
|
|
591
|
+
};
|
|
592
|
+
}
|
|
369
593
|
/** One bounded SDK session for a role; resolves with the SDK's session_id (for
|
|
370
594
|
* resuming on restart) and whether it ended by hitting the turn limit (so the
|
|
371
595
|
* caller can push a continuation) when the stream ends (mailbox closed or
|
|
372
596
|
* maxTurns reached). */
|
|
373
|
-
async function runOneSession(opts, resume, costTotals, progress) {
|
|
597
|
+
async function runOneSession(opts, resume, costTotals, progress, tokenTotals, streamOpts) {
|
|
374
598
|
const { org, role, bus, policy, mailbox, cwd } = opts;
|
|
375
599
|
// Read lastMessageId live from opts instead of capturing at session start
|
|
376
600
|
// This ensures chat responses link to the most recent message delivered
|
|
@@ -389,7 +613,24 @@ async function runOneSession(opts, resume, costTotals, progress) {
|
|
|
389
613
|
// The named provider's default model fills in adapter_config.model when the
|
|
390
614
|
// role didn't pin one.
|
|
391
615
|
const prov = resolveRoleProvider(role, opts.orgRoot ?? opts.cwd);
|
|
616
|
+
// ADR-O001 D8: the role's cost tier, when the org declares one. Resolved
|
|
617
|
+
// here — the single choke point where a role's model is decided — so the
|
|
618
|
+
// documented precedence holds in exactly one place:
|
|
619
|
+
// explicit adapter_config.model > tier > named-provider default > runtime
|
|
620
|
+
// The tier's EFFORT is applied even when the model came from an explicit
|
|
621
|
+
// pin: which model to run and how hard to think are separate axes, and
|
|
622
|
+
// silently dropping the effort because a model was pinned would be the
|
|
623
|
+
// "silent downgrade" this decision exists to prevent.
|
|
624
|
+
// Throws (fails the session) rather than guessing when the tier has no
|
|
625
|
+
// entry for this role's provider — daemon.ts validates the whole roster
|
|
626
|
+
// up front so that is normally caught before any token is spent.
|
|
627
|
+
const tier = resolveRoleCostTier({
|
|
628
|
+
role,
|
|
629
|
+
def: opts.def,
|
|
630
|
+
vendor: role.provider?.vendor ?? prov.cfg?.vendor,
|
|
631
|
+
});
|
|
392
632
|
const model = role.adapter_config?.model ??
|
|
633
|
+
tier?.model ??
|
|
393
634
|
prov.defaultModel ??
|
|
394
635
|
resolveModel(role, role.runtime, role.provider?.vendor ?? prov.cfg?.vendor);
|
|
395
636
|
bus.emit({ type: 'status', from: role.id, msg: 'session starting' });
|
|
@@ -401,7 +642,7 @@ async function runOneSession(opts, resume, costTotals, progress) {
|
|
|
401
642
|
// time a 'result' message ends one mailbox message and the next one starts.
|
|
402
643
|
// Exists purely so the 'result' branch never re-adds what this branch
|
|
403
644
|
// already added (see there for why it can't just always add).
|
|
404
|
-
let messageTurnTokens = 0;
|
|
645
|
+
let messageTurnTokens = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
405
646
|
// Abort hook for the runner (AgentRunArgs.signal): the silent-stream
|
|
406
647
|
// abort below used to call iterator.return() only, which queues behind a
|
|
407
648
|
// subprocess runner blocked in `for await (child.stdout)` — the child was
|
|
@@ -438,12 +679,18 @@ async function runOneSession(opts, resume, costTotals, progress) {
|
|
|
438
679
|
});
|
|
439
680
|
const stream = runner.run({
|
|
440
681
|
tools,
|
|
441
|
-
|
|
442
|
-
|
|
682
|
+
// No options = the pre-D3 stream, exactly.
|
|
683
|
+
prompt: streamOpts ? mailbox.stream('', streamOpts) : mailbox.stream(),
|
|
684
|
+
systemPrompt: rolePromptFor(opts),
|
|
443
685
|
model,
|
|
444
686
|
cwd,
|
|
687
|
+
effort: tier?.effort,
|
|
445
688
|
env: {
|
|
446
689
|
...resolveProviderEnv(prov.cfg),
|
|
690
|
+
// D8: how a NON-Claude provider expresses the tier's effort level.
|
|
691
|
+
// Empty for Claude (handled natively by ClaudeAgentRunner) and for a
|
|
692
|
+
// provider that declares no mechanism — which simply ignores effort.
|
|
693
|
+
...(tier?.env ?? {}),
|
|
447
694
|
// Custom-endpoint providers (named-provider path): pin the engine's
|
|
448
695
|
// model env so background/haiku tasks also route to the endpoint's
|
|
449
696
|
// model instead of erroring on an Anthropic-only default.
|
|
@@ -475,6 +722,13 @@ async function runOneSession(opts, resume, costTotals, progress) {
|
|
|
475
722
|
maxTurns: opts.maxTurns ?? 30,
|
|
476
723
|
resume,
|
|
477
724
|
claudeRestrictions: gitEnforcement.claudeRestrictions,
|
|
725
|
+
// ADR-O001 D2: tool results are 76% of a role's context mass and nothing
|
|
726
|
+
// bounded them. Under the ORG STATE dir (never the workspace cwd, which
|
|
727
|
+
// may be the repo), and under orgRoot — which file-roots.ts already
|
|
728
|
+
// makes readable to the role's file tools and role-sandbox.ts already
|
|
729
|
+
// makes readable to Bash — so the path in the digest actually resolves
|
|
730
|
+
// when the role decides it needs the full text.
|
|
731
|
+
toolSpillDir: join(opts.orgDir ?? opts.cwd, 'tool-results', role.id.replace(/[^a-zA-Z0-9_.-]/g, '_')),
|
|
478
732
|
canUseTool: gatedCanUseTool(policy, opts.beforeTool, role.id, opts.fence, opts.onDecision
|
|
479
733
|
? (toolName, _input, decision, kind) => opts.onDecision?.(role.id, toolName, decision.message ?? 'denied', kind)
|
|
480
734
|
: undefined, opts.hasPendingGate),
|
|
@@ -624,10 +878,18 @@ async function runOneSession(opts, resume, costTotals, progress) {
|
|
|
624
878
|
// .d.ts, which puts `usage`/token counts on BetaMessage but cost only
|
|
625
879
|
// on SDKResultSuccess.total_cost_usd/modelUsage. overBudgetUsd is
|
|
626
880
|
// still checked below, once per message, same as before this fix.)
|
|
627
|
-
|
|
881
|
+
//
|
|
882
|
+
// ADR-O001 D1: the sum must include BOTH cache fields. They are
|
|
883
|
+
// siblings of input_tokens in the Anthropic API, not subsets of it —
|
|
884
|
+
// `input_tokens` is the uncached remainder — and both are billable
|
|
885
|
+
// (~0.1x and ~1.25x input). Omitting them meant the better the cache
|
|
886
|
+
// worked the less the meter saw: on one measured run, 2,765M tokens
|
|
887
|
+
// billed against 8.1M recorded, with input_tokens at 0.0M.
|
|
888
|
+
const turn = turnBreakdown(m);
|
|
889
|
+
const turnTokens = totalTokens(turn);
|
|
628
890
|
if (turnTokens > 0) {
|
|
629
|
-
messageTurnTokens
|
|
630
|
-
policy.
|
|
891
|
+
addTo(messageTurnTokens, turn);
|
|
892
|
+
policy.addTokenUsage(turn);
|
|
631
893
|
if (policy.overBudget) {
|
|
632
894
|
bus.emit({
|
|
633
895
|
type: 'status',
|
|
@@ -664,20 +926,48 @@ async function runOneSession(opts, resume, costTotals, progress) {
|
|
|
664
926
|
});
|
|
665
927
|
}
|
|
666
928
|
else if (m.type === 'result') {
|
|
667
|
-
|
|
929
|
+
// ADR-O001 D1: prefer the SDK's `modelUsage` over `usage`. The SDK
|
|
930
|
+
// documents `usage` as "MAIN AGENT LOOP ONLY — excludes Task
|
|
931
|
+
// subagent, sidechain, and auxiliary model calls ... Prefer
|
|
932
|
+
// modelUsage for token/cost accounting"; the measured run made 46
|
|
933
|
+
// subagent calls this counter never saw. modelUsage is CUMULATIVE per
|
|
934
|
+
// session (same lifecycle as total_cost_usd, per its own type doc),
|
|
935
|
+
// so it is converted to a delta here rather than added, exactly as
|
|
936
|
+
// cost is below. A runner that reports no modelUsage falls back to
|
|
937
|
+
// the per-turn `usage` fields, which keep their old semantics.
|
|
938
|
+
const resultTokens = resultBreakdown(m, tokenTotals, m.session_id ?? sessionId ?? '');
|
|
668
939
|
// Per the SDK's own type docs, a 'result' message's usage is that
|
|
669
940
|
// message's own (effectively last-turn) usage in streaming-input mode,
|
|
670
941
|
// NOT a cumulative total across every turn of the mailbox message —
|
|
671
942
|
// and that last turn was already counted above via its own 'assistant'
|
|
672
943
|
// message, specifically so overBudget could trip mid-message. Adding
|
|
673
|
-
//
|
|
944
|
+
// the result's own usage again unconditionally would double-count it.
|
|
945
|
+
// (A modelUsage-derived delta is per-session-cumulative, so the same
|
|
946
|
+
// subtraction is exactly right there too: it removes what the
|
|
947
|
+
// assistant turns of THIS message already contributed and leaves the
|
|
948
|
+
// subagent/auxiliary volume the main loop never reported.) Only make
|
|
674
949
|
// up the shortfall (never negative) so a turn whose usage somehow
|
|
675
950
|
// never reached the 'assistant' branch (e.g. a runner/test double that
|
|
676
951
|
// doesn't emit per-turn usage) still gets counted at least once.
|
|
677
|
-
const shortfall =
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
952
|
+
const shortfall = {
|
|
953
|
+
input: Math.max(0, resultTokens.input - messageTurnTokens.input),
|
|
954
|
+
output: Math.max(0, resultTokens.output - messageTurnTokens.output),
|
|
955
|
+
cacheRead: Math.max(0, resultTokens.cacheRead - messageTurnTokens.cacheRead),
|
|
956
|
+
cacheCreation: Math.max(0, resultTokens.cacheCreation - messageTurnTokens.cacheCreation),
|
|
957
|
+
};
|
|
958
|
+
if (totalTokens(shortfall) > 0)
|
|
959
|
+
policy.addTokenUsage(shortfall);
|
|
960
|
+
// What this whole mailbox message actually added to the meter: the
|
|
961
|
+
// per-turn accounting above plus whatever the result topped up. This
|
|
962
|
+
// is what the 'usage' event reports, so a consumer summing events
|
|
963
|
+
// lands on the same number as policy.usage.
|
|
964
|
+
const messageTokens = {
|
|
965
|
+
input: messageTurnTokens.input + shortfall.input,
|
|
966
|
+
output: messageTurnTokens.output + shortfall.output,
|
|
967
|
+
cacheRead: messageTurnTokens.cacheRead + shortfall.cacheRead,
|
|
968
|
+
cacheCreation: messageTurnTokens.cacheCreation + shortfall.cacheCreation,
|
|
969
|
+
};
|
|
970
|
+
messageTurnTokens = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
681
971
|
// Convert the SDK's cumulative-per-session total_cost_usd into a
|
|
682
972
|
// per-result delta before emitting - downstream sums usage events.
|
|
683
973
|
// costTotals is keyed by session_id, so a genuinely new/restarted
|
|
@@ -701,7 +991,19 @@ async function runOneSession(opts, resume, costTotals, progress) {
|
|
|
701
991
|
bus.emit({
|
|
702
992
|
type: 'usage',
|
|
703
993
|
from: role.id,
|
|
704
|
-
|
|
994
|
+
// ADR-O001 D1: the four quantities travel separately so every
|
|
995
|
+
// downstream consumer (forwarder → dashboard state.json, reporting,
|
|
996
|
+
// `org costs`) can record real values instead of the 0s they used
|
|
997
|
+
// to persist. `tokens` stays the single billable total.
|
|
998
|
+
data: {
|
|
999
|
+
tokens: totalTokens(messageTokens),
|
|
1000
|
+
cost_usd: costDelta,
|
|
1001
|
+
subtype: m.subtype,
|
|
1002
|
+
tokens_in: messageTokens.input,
|
|
1003
|
+
tokens_out: messageTokens.output,
|
|
1004
|
+
cache_read: messageTokens.cacheRead,
|
|
1005
|
+
cache_creation: messageTokens.cacheCreation,
|
|
1006
|
+
},
|
|
705
1007
|
});
|
|
706
1008
|
if (m.subtype && m.subtype !== 'success') {
|
|
707
1009
|
if (m.subtype === 'error_max_turns')
|
|
@@ -787,6 +1089,13 @@ async function runOneSession(opts, resume, costTotals, progress) {
|
|
|
787
1089
|
providerSet?.close();
|
|
788
1090
|
}
|
|
789
1091
|
}
|
|
1092
|
+
/** ADR-O001 D7: org_task's description suffix for an org with a catalog. */
|
|
1093
|
+
function loadoutHelp(catalog) {
|
|
1094
|
+
const list = catalog
|
|
1095
|
+
.map((l) => (l.description ? `${l.name} (${l.description})` : l.name))
|
|
1096
|
+
.join(', ');
|
|
1097
|
+
return ` Optionally select a "loadout" — the named, stable specialisation the assignee's session is built with: ${list}. Select by kind of work; put everything specific to this task (which diff, criteria, what failed last time) in the title or a message, not in the choice of loadout. The selection is recorded on the task and reused on every retry.`;
|
|
1098
|
+
}
|
|
790
1099
|
/** Build the org tool surface as platform-agnostic OrgToolDef[]. The handlers
|
|
791
1100
|
* close over sessionOpts callbacks (deliver, recall, remember, …) — same
|
|
792
1101
|
* wiring as the previous inline createSdkMcpServer block, just decoupled from
|
|
@@ -912,22 +1221,64 @@ export function buildOrgTools(opts) {
|
|
|
912
1221
|
handler: async (args) => text(await onGate(role.id, args.name, args.description)),
|
|
913
1222
|
});
|
|
914
1223
|
}
|
|
1224
|
+
// ADR-O001 D7: the `loadout` argument exists only for an org with a
|
|
1225
|
+
// catalog, so every other org's tool list stays byte-identical.
|
|
1226
|
+
const catalog = opts.loadoutCatalog?.length ? opts.loadoutCatalog : undefined;
|
|
1227
|
+
const loadoutArg = catalog
|
|
1228
|
+
? { loadout: z.enum(catalog.map((l) => l.name)).optional() }
|
|
1229
|
+
: {};
|
|
915
1230
|
const createTask = opts.createTask;
|
|
916
1231
|
if (createTask) {
|
|
917
1232
|
tools.push({
|
|
918
1233
|
name: 'org_task',
|
|
919
|
-
description: 'Create a task in the DAG with optional dependencies. Dependencies must be existing task IDs. Tasks become ready when all deps are done, then get dispatched to the assignee.'
|
|
920
|
-
|
|
921
|
-
|
|
1234
|
+
description: 'Create a task in the DAG with optional dependencies. Dependencies must be existing task IDs. Tasks become ready when all deps are done, then get dispatched to the assignee.' +
|
|
1235
|
+
(catalog ? loadoutHelp(catalog) : ''),
|
|
1236
|
+
schema: {
|
|
1237
|
+
title: z.string(),
|
|
1238
|
+
assignee: z.string(),
|
|
1239
|
+
deps: z.array(z.string()).default([]),
|
|
1240
|
+
...loadoutArg,
|
|
1241
|
+
},
|
|
1242
|
+
handler: async (args) => text(createTask(role.id, args.title, args.assignee, args.deps ?? [], args.loadout)),
|
|
922
1243
|
});
|
|
923
1244
|
}
|
|
924
1245
|
const completeTask = opts.completeTask;
|
|
925
1246
|
if (completeTask) {
|
|
926
1247
|
tools.push({
|
|
927
1248
|
name: 'org_task_done',
|
|
928
|
-
description:
|
|
929
|
-
|
|
930
|
-
|
|
1249
|
+
description: opts.requireTaskEvidence
|
|
1250
|
+
? 'Mark a task as completed. This org requires EVIDENCE (run_config.completion_evidence): pass `evidence` with the current commit sha and one entry per acceptance criterion — the command you actually ran, its real exit code, and its output. Evidence pinned to an older commit is stale and will be refused, and a refused completion puts the task back in your queue with the reason — but only up to run_config.max_evidence_attempts times (default 3), after which the task is recorded as failed and escalated to the boss instead of returned to you. Any downstream tasks whose deps are now all done become ready and are dispatched.'
|
|
1251
|
+
: 'Mark a task as completed and optionally provide a result summary. Any downstream tasks whose deps are now all done will become ready and be dispatched.',
|
|
1252
|
+
schema: {
|
|
1253
|
+
taskId: z.string(),
|
|
1254
|
+
result: z.string().optional(),
|
|
1255
|
+
evidence: z
|
|
1256
|
+
.object({
|
|
1257
|
+
headSha: z.string(),
|
|
1258
|
+
checks: z
|
|
1259
|
+
.array(z.object({
|
|
1260
|
+
command: z.string(),
|
|
1261
|
+
exitCode: z.number().int(),
|
|
1262
|
+
output: z.string().optional(),
|
|
1263
|
+
}))
|
|
1264
|
+
.default([]),
|
|
1265
|
+
})
|
|
1266
|
+
.optional(),
|
|
1267
|
+
},
|
|
1268
|
+
handler: async (args) => text(completeTask(role.id, args.taskId, args.result, args.evidence)),
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
const requestReview = opts.requestReview;
|
|
1272
|
+
if (requestReview) {
|
|
1273
|
+
tools.push({
|
|
1274
|
+
name: 'org_review',
|
|
1275
|
+
description: "Ask an artifact-only reviewer for a verdict on a task. You pass ids only: the runtime builds the review from the task's text, its assignee's latest org_task_done evidence (commands, exit codes, output) and its own git diff of base...headSha — nothing you write is added, so there is no summary to give. The reviewer starts cold every time and replies to you with org_send. Refused if the task has no evidence yet.",
|
|
1276
|
+
schema: {
|
|
1277
|
+
taskId: z.string(),
|
|
1278
|
+
reviewer: z.string(),
|
|
1279
|
+
base: z.string().optional().describe("git ref to diff against (default 'main')"),
|
|
1280
|
+
},
|
|
1281
|
+
handler: async (args) => text(requestReview(role.id, args.taskId, args.reviewer, args.base)),
|
|
931
1282
|
});
|
|
932
1283
|
}
|
|
933
1284
|
const listTasks = opts.listTasks;
|
|
@@ -982,7 +1333,8 @@ export function buildOrgTools(opts) {
|
|
|
982
1333
|
if (planGraph) {
|
|
983
1334
|
tools.push({
|
|
984
1335
|
name: 'org_plan_graph',
|
|
985
|
-
description: 'Propose a full work graph in one call. Each task spec uses a local "name" and references other specs by name in "after".'
|
|
1336
|
+
description: 'Propose a full work graph in one call. Each task spec uses a local "name" and references other specs by name in "after".' +
|
|
1337
|
+
(catalog ? ' Each spec may select a "loadout" exactly as org_task does.' : ''),
|
|
986
1338
|
schema: {
|
|
987
1339
|
tasks: z
|
|
988
1340
|
.array(z.object({
|
|
@@ -990,11 +1342,11 @@ export function buildOrgTools(opts) {
|
|
|
990
1342
|
title: z.string(),
|
|
991
1343
|
assignee: z.string(),
|
|
992
1344
|
after: z.array(z.string()).default([]),
|
|
1345
|
+
...loadoutArg,
|
|
993
1346
|
}))
|
|
994
1347
|
.min(1),
|
|
995
1348
|
},
|
|
996
|
-
handler: async (args) => text(planGraph(role.id, args.tasks ??
|
|
997
|
-
[])),
|
|
1349
|
+
handler: async (args) => text(planGraph(role.id, args.tasks ?? [])),
|
|
998
1350
|
});
|
|
999
1351
|
}
|
|
1000
1352
|
tools.push({
|
|
@@ -1015,12 +1367,17 @@ export function buildOrgTools(opts) {
|
|
|
1015
1367
|
});
|
|
1016
1368
|
tools.push({
|
|
1017
1369
|
name: 'ask_human',
|
|
1018
|
-
description: 'Ask a human a free-form question
|
|
1019
|
-
|
|
1370
|
+
description: 'Ask a human a free-form question. Use only when you genuinely need human judgment. ' +
|
|
1371
|
+
'Set blocking: true ONLY if you cannot continue until it is answered — a blocking question pauses the ' +
|
|
1372
|
+
"org's idle watchdog (for up to an hour; after that the run resumes its normal idle checks either way). " +
|
|
1373
|
+
'If you can keep working while you wait — an FYI, a preference, anything you would describe as "not blocking on this" — ' +
|
|
1374
|
+
'pass blocking: false and carry on; the question is still recorded and answered, it just does not freeze the run. ' +
|
|
1375
|
+
'Defaults to blocking.',
|
|
1376
|
+
schema: { question: z.string(), blocking: z.boolean().optional() },
|
|
1020
1377
|
handler: async (args) => {
|
|
1021
1378
|
if (!opts.askHuman)
|
|
1022
1379
|
return text('ask_human is not available in this session');
|
|
1023
|
-
const receipt = await opts.askHuman(role.id, args.question);
|
|
1380
|
+
const receipt = await opts.askHuman(role.id, args.question, args.blocking);
|
|
1024
1381
|
return text(receipt);
|
|
1025
1382
|
},
|
|
1026
1383
|
});
|