aiden-runtime 4.5.0 → 4.6.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/README.md +17 -2
- package/dist/cli/v4/aidenCLI.js +185 -99
- package/dist/cli/v4/chatSession.js +107 -0
- package/dist/cli/v4/commands/_runtimeToggleHelpers.js +2 -0
- package/dist/cli/v4/commands/fanout.js +42 -59
- package/dist/cli/v4/commands/help.js +6 -0
- package/dist/cli/v4/commands/index.js +16 -1
- package/dist/cli/v4/commands/mcp.js +80 -54
- package/dist/cli/v4/commands/plannerGuard.js +53 -0
- package/dist/cli/v4/commands/recovery.js +122 -0
- package/dist/cli/v4/commands/runs.js +22 -2
- package/dist/cli/v4/commands/spawnPause.js +93 -0
- package/dist/cli/v4/daemonAgentBuilder.js +4 -1
- package/dist/cli/v4/defaultSoul.js +1 -1
- package/dist/core/v4/aidenAgent.js +219 -1
- package/dist/core/v4/daemon/bootstrap.js +47 -0
- package/dist/core/v4/daemon/db/migrations.js +66 -0
- package/dist/core/v4/daemon/runStore.js +33 -3
- package/dist/core/v4/providerFallback.js +35 -2
- package/dist/core/v4/runtimeToggles.js +30 -3
- package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
- package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
- package/dist/core/v4/subagent/childBuilder.js +391 -0
- package/dist/core/v4/subagent/fanout.js +75 -51
- package/dist/core/v4/subagent/spawnPause.js +191 -0
- package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
- package/dist/core/v4/toolRegistry.js +19 -3
- package/dist/core/version.js +1 -1
- package/dist/moat/plannerGuard.js +29 -0
- package/dist/providers/v4/anthropicAdapter.js +31 -3
- package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
- package/dist/providers/v4/codexResponsesAdapter.js +25 -2
- package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
- package/dist/tools/v4/index.js +17 -3
- package/dist/tools/v4/skills/lookupToolSchema.js +6 -1
- package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
- package/dist/tools/v4/subagent/subagentFanout.js +53 -1
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -10,14 +10,15 @@
|
|
|
10
10
|
|
|
11
11
|
Autonomous AI Engine — local-first, Windows-native, yours to own
|
|
12
12
|
|
|
13
|
-
74 skills ·
|
|
13
|
+
74 skills · 60 tools · 19 providers · 9 channels · AGPL-3.0
|
|
14
14
|
|
|
15
15
|
Windows · Linux · WSL · macOS (API Mode)
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
|
-
<a href="https://github.com/taracodlabs/aiden/releases/latest"><img src="https://img.shields.io/badge/version-v4.
|
|
19
|
+
<a href="https://github.com/taracodlabs/aiden/releases/latest"><img src="https://img.shields.io/badge/version-v4.6.0-f97316?style=for-the-badge" alt="v4.6.0" /></a>
|
|
20
20
|
<a href="https://www.npmjs.com/package/aiden-runtime"><img src="https://img.shields.io/npm/v/aiden-runtime?color=f97316&label=npm&style=for-the-badge" alt="npm" /></a>
|
|
21
|
+
<a href="https://www.npmjs.com/package/aiden-runtime"><img src="https://img.shields.io/npm/dm/aiden-runtime?color=f97316&label=downloads&style=for-the-badge" alt="npm downloads" /></a>
|
|
21
22
|
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-AGPL--3.0-orange?style=for-the-badge" alt="License: AGPL-3.0" /></a>
|
|
22
23
|
<a href="https://discord.gg/gMZ3hUnQTm"><img src="https://img.shields.io/badge/chat-discord-7289da?logo=discord&logoColor=white&style=for-the-badge" alt="Discord" /></a>
|
|
23
24
|
<a href="https://github.com/taracodlabs/aiden/stargazers"><img src="https://img.shields.io/github/stars/taracodlabs/aiden?style=for-the-badge&color=f9d71c" alt="Stars" /></a>
|
|
@@ -33,6 +34,20 @@ Windows · Linux · WSL · macOS (API Mode)
|
|
|
33
34
|
|
|
34
35
|
---
|
|
35
36
|
|
|
37
|
+
## What's new in v4.6
|
|
38
|
+
|
|
39
|
+
Aiden now spawns workers and learns from itself.
|
|
40
|
+
|
|
41
|
+
- **Sub-agents.** `spawn_sub_agent` runs a focused child with an isolated context + intersected toolset; `subagent_fanout` runs N children in parallel (ensemble or partition) with merge strategies (`all` / `vote` / `pick-best` / `combine`) and provider rotation across configured fallback slots.
|
|
42
|
+
- **Operator kill-switch.** `/spawn-pause on|off|status` blocks new sub-agent spawning while in-flight children continue. Marker file at `~/.aiden/spawn.paused` so the state survives restart and is shared across REPL, daemon, and MCP runtimes. Optional reason field captured in the typed `SUBAGENT_SPAWN_PAUSED` error envelope.
|
|
43
|
+
- **Self-improvement loop foundation.** TCE classifications + recoveries persist to two new SQLite tables (`failure_signatures`, `recovery_reports`); `/recovery list|show|clear` surfaces recurring failure patterns across sessions.
|
|
44
|
+
- **REPL parent-run lineage.** Each REPL turn writes its own `runs` row; sub-agent children link back via `spawned_from_run_id`. `aiden runs list` hides children by default and shows a `(N children, M OK)` badge per parent; `--include-children` flips to flat view.
|
|
45
|
+
- **PlannerGuard opt-in.** The keyword-based per-turn tool narrower is OFF by default in v4.6 (modern models pick well from the full catalog). Enable via `/planner-guard on` or `AIDEN_PLANNER_GUARD=1` for smaller local models.
|
|
46
|
+
|
|
47
|
+
Phase 2 also fixed an MCP-mode `subagent_fanout` regression that had silently broken in the v4.5 refactor.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
36
51
|
## What's new in v4.5
|
|
37
52
|
|
|
38
53
|
Aiden now wakes up by itself.
|
package/dist/cli/v4/aidenCLI.js
CHANGED
|
@@ -97,6 +97,13 @@ const sessionManager_1 = require("../../core/v4/sessionManager");
|
|
|
97
97
|
const toolRegistry_1 = require("../../core/v4/toolRegistry");
|
|
98
98
|
const skillLoader_1 = require("../../core/v4/skillLoader");
|
|
99
99
|
const index_1 = require("../../tools/v4/index");
|
|
100
|
+
// v4.6 Phase 1 — spawn_sub_agent: always-on runStore + LLM-callable tool.
|
|
101
|
+
const node_crypto_1 = require("node:crypto");
|
|
102
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
103
|
+
const daemonConfig_1 = require("../../core/v4/daemon/daemonConfig");
|
|
104
|
+
const connection_1 = require("../../core/v4/daemon/db/connection");
|
|
105
|
+
const runStore_1 = require("../../core/v4/daemon/runStore");
|
|
106
|
+
const spawnSubAgentTool_1 = require("../../tools/v4/subagent/spawnSubAgentTool");
|
|
100
107
|
const skillCommands_1 = require("../../core/v4/skillCommands");
|
|
101
108
|
const aidenAgent_1 = require("../../core/v4/aidenAgent");
|
|
102
109
|
const promptBuilder_1 = require("../../core/v4/promptBuilder");
|
|
@@ -502,6 +509,7 @@ async function main(argv, opts = {}) {
|
|
|
502
509
|
.option('--source <src>', 'list: filter by trigger source (file/webhook/email/schedule/manual)')
|
|
503
510
|
.option('--status <s>', 'list: filter by status (queued/running/completed/failed/cancelled/interrupted)')
|
|
504
511
|
.option('--trigger <prefix>', 'list: sessionId prefix (e.g. "trigger:file:<id>:")')
|
|
512
|
+
.option('--include-children', 'list: include sub-agent children (default: top-level only, with per-parent badge)')
|
|
505
513
|
.action(async (action, posArgs, cmdObj) => {
|
|
506
514
|
const { runRunsSubcommand } = await Promise.resolve().then(() => __importStar(require('./commands/runs')));
|
|
507
515
|
const code = await runRunsSubcommand(action, posArgs ?? [], cmdObj, {
|
|
@@ -623,6 +631,60 @@ async function main(argv, opts = {}) {
|
|
|
623
631
|
async function buildAgentRuntime(cliOpts, opts) {
|
|
624
632
|
const paths = opts.pathsOverride ?? (0, paths_1.resolveAidenPaths)();
|
|
625
633
|
await (0, paths_1.ensureAidenDirsExist)(paths);
|
|
634
|
+
// ── v4.6 Phase 1 — always-on runStore for spawn_sub_agent ──────────────
|
|
635
|
+
//
|
|
636
|
+
// The spawn_sub_agent primitive persists each child run to the runs
|
|
637
|
+
// table via spawned_from_run_id FK. The REPL needs a runStore handle
|
|
638
|
+
// regardless of whether AIDEN_DAEMON=1 or not. SQLite WAL mode
|
|
639
|
+
// (enabled in openDaemonDb) allows REPL + daemon to coexist on the
|
|
640
|
+
// same file without lock contention. The connection.ts module caches
|
|
641
|
+
// per-path, so when daemon foundation has already opened the DB this
|
|
642
|
+
// call returns the same handle. Migration runner is idempotent on
|
|
643
|
+
// already-current schemas.
|
|
644
|
+
const replInstanceId = `repl-${(0, node_crypto_1.randomUUID)().slice(0, 8)}`;
|
|
645
|
+
const replDb = (0, connection_1.openDaemonDb)((0, daemonConfig_1.daemonDbPath)(paths.root));
|
|
646
|
+
// Seed the REPL's daemon_instances row so the FK on runs.instance_id
|
|
647
|
+
// is satisfied. Idempotent under INSERT OR IGNORE — multiple REPL
|
|
648
|
+
// launches reusing the same instance_id (rare with random UUID
|
|
649
|
+
// suffix) silently no-op.
|
|
650
|
+
replDb.prepare(`INSERT OR IGNORE INTO daemon_instances
|
|
651
|
+
(instance_id, pid, hostname, started_at, last_heartbeat, version)
|
|
652
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(replInstanceId, process.pid, node_os_1.default.hostname(), Date.now(), Date.now(), version_1.VERSION);
|
|
653
|
+
const replRunStore = (0, runStore_1.createRunStore)({ db: replDb });
|
|
654
|
+
// v4.6 Phase 3A — operator kill-switch for sub-agent spawning.
|
|
655
|
+
// Initialised as early as possible so any subsequent tool wiring
|
|
656
|
+
// sees the singleton. The marker file lives at
|
|
657
|
+
// `<paths.root>/spawn.paused` and is shared across REPL + daemon
|
|
658
|
+
// + MCP for cross-process coordination. The startup probe (warn
|
|
659
|
+
// operator that pause is active from a prior session) fires
|
|
660
|
+
// later, once `bootLogger` is available — see the
|
|
661
|
+
// `spawnPauseBootStatus` block below.
|
|
662
|
+
const { initSpawnPause } = await Promise.resolve().then(() => __importStar(require('../../core/v4/subagent/spawnPause')));
|
|
663
|
+
const spawnPauseState = initSpawnPause({ aidenHome: paths.root });
|
|
664
|
+
// v4.6 Phase 3b — self-improvement loop. Initialise the durable
|
|
665
|
+
// failure-ledger / recovery-report store against the same
|
|
666
|
+
// daemon.db handle the runStore uses. WAL coexistence: REPL +
|
|
667
|
+
// daemon + MCP all share the same connection-cached handle, so
|
|
668
|
+
// any writes from one runtime are visible to the others. The
|
|
669
|
+
// TCE write-through path inside the agent loop reads through the
|
|
670
|
+
// module-level singleton; initialising here makes spawnSubAgent
|
|
671
|
+
// and daemon-fired turns observe the same persistence.
|
|
672
|
+
const { initRecoveryStore } = await Promise.resolve().then(() => __importStar(require('../../core/v4/selfimprovement/recoveryStore')));
|
|
673
|
+
initRecoveryStore({ db: replDb });
|
|
674
|
+
const spawnPauseBootStatus = spawnPauseState.isPaused()
|
|
675
|
+
? spawnPauseState.status()
|
|
676
|
+
: null;
|
|
677
|
+
// v4.6 Phase 2Q-B — mutable holder for the REPL's current parent
|
|
678
|
+
// run id. ChatSession's `runAgentTurn` writes a row before each
|
|
679
|
+
// turn dispatches and stores the id here (and clears it on
|
|
680
|
+
// completion). The spawn / fanout tool factories below read it
|
|
681
|
+
// through `resolveParentRunId` / `resolveParentSessionId`
|
|
682
|
+
// callbacks so any child spawned mid-turn is linked back to the
|
|
683
|
+
// live REPL parent row via `runs.spawned_from_run_id`.
|
|
684
|
+
const replParentRunRef = {
|
|
685
|
+
runId: null,
|
|
686
|
+
sessionId: null,
|
|
687
|
+
};
|
|
626
688
|
// Phase 16c.2: load `paths.envFile` (the aiden-managed `.env` that
|
|
627
689
|
// `setupWizard.ts::upsertEnvVar` writes to) into `process.env` BEFORE
|
|
628
690
|
// any provider resolution. The bug: setup wrote keys to this file but
|
|
@@ -1238,7 +1300,11 @@ async function buildAgentRuntime(cliOpts, opts) {
|
|
|
1238
1300
|
// ── Build agent with all moat layers attached ────────────────────────
|
|
1239
1301
|
const agent = new aidenAgent_1.AidenAgent({
|
|
1240
1302
|
provider: adapter,
|
|
1241
|
-
tools
|
|
1303
|
+
// v4.6 Phase 1 — 'repl' context filter excludes tools tagged
|
|
1304
|
+
// daemon-only (none today) and INCLUDES tools tagged repl-only
|
|
1305
|
+
// (e.g. spawn_sub_agent, registered after this line). Tools with
|
|
1306
|
+
// no `contexts` field default to visible in both contexts.
|
|
1307
|
+
tools: toolRegistry.getSchemas(undefined, 'repl'),
|
|
1242
1308
|
toolExecutor,
|
|
1243
1309
|
maxTurns: config.getValue('agent.max_turns', 90),
|
|
1244
1310
|
auxiliaryClient,
|
|
@@ -1426,6 +1492,21 @@ async function buildAgentRuntime(cliOpts, opts) {
|
|
|
1426
1492
|
// Wire the gateway singleton's logger BEFORE registering its processor
|
|
1427
1493
|
// so register / unregister channel events are scoped correctly.
|
|
1428
1494
|
gateway_1.gateway.attachLogger(bootLogger.child('gateway'));
|
|
1495
|
+
// v4.6 Phase 3A — startup probe for the spawn-pause kill-switch.
|
|
1496
|
+
// The state was initialised early (line ~740) before tool wiring.
|
|
1497
|
+
// Now that bootLogger exists, emit a visible warning so an
|
|
1498
|
+
// operator who forgot they paused in a prior session learns
|
|
1499
|
+
// immediately rather than puzzling at silent rejected fanouts.
|
|
1500
|
+
if (spawnPauseBootStatus) {
|
|
1501
|
+
const s = spawnPauseBootStatus;
|
|
1502
|
+
const reasonSuffix = s.reason ? ` (reason: ${s.reason})` : '';
|
|
1503
|
+
bootLogger.warn(`spawn_sub_agent / subagent_fanout are PAUSED${reasonSuffix}. ` +
|
|
1504
|
+
'Run /spawn-pause off to resume.', {
|
|
1505
|
+
pausedAt: s.pausedAt ?? null,
|
|
1506
|
+
pausedBy: s.pausedBy ?? null,
|
|
1507
|
+
durationMs: s.durationMs ?? null,
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1429
1510
|
// ── Phase v4.1-subagent.1 — replace subagent_fanout stub with wired version
|
|
1430
1511
|
//
|
|
1431
1512
|
// tools/v4/index.ts registers a stub at boot so the schema is visible
|
|
@@ -1442,10 +1523,45 @@ async function buildAgentRuntime(cliOpts, opts) {
|
|
|
1442
1523
|
// shared subsystems (registry, skillLoader, paths, memoryManager,
|
|
1443
1524
|
// promptBuilder, promptBuilderOptions) are read-only and pass by
|
|
1444
1525
|
// reference.
|
|
1526
|
+
// v4.6 Phase 2Q-A — `runFanout` routes each child through
|
|
1527
|
+
// `spawnSubAgent`. `spawnDeps` mirrors the deps the
|
|
1528
|
+
// `makeSpawnSubAgentTool` factory accepts (see registration below
|
|
1529
|
+
// this block). The legacy per-call `runChild` closure that lived
|
|
1530
|
+
// here pre-2R has been deleted; the primitive owns child
|
|
1531
|
+
// construction now.
|
|
1445
1532
|
toolRegistry.register((0, index_1.makeSubagentFanoutTool)({
|
|
1446
1533
|
logger: bootLogger.child('subagent'),
|
|
1447
1534
|
resolveActiveModel: () => ({ providerId, modelId }),
|
|
1448
1535
|
aggregatorAdapter: adapter,
|
|
1536
|
+
spawnDeps: {
|
|
1537
|
+
toolRegistry,
|
|
1538
|
+
parentToolContext: {
|
|
1539
|
+
cwd: process.cwd(),
|
|
1540
|
+
paths,
|
|
1541
|
+
sessions: sessionManager,
|
|
1542
|
+
memory: memoryManager,
|
|
1543
|
+
memoryGuard,
|
|
1544
|
+
ssrfProtection,
|
|
1545
|
+
tirithScanner,
|
|
1546
|
+
skillLoader,
|
|
1547
|
+
},
|
|
1548
|
+
parentProvider: adapter,
|
|
1549
|
+
parentProviderId: providerId,
|
|
1550
|
+
parentModelId: modelId,
|
|
1551
|
+
resolveVerifiedFlag,
|
|
1552
|
+
resolveToolset,
|
|
1553
|
+
resolveMutates,
|
|
1554
|
+
runStore: replRunStore,
|
|
1555
|
+
instanceId: replInstanceId,
|
|
1556
|
+
logger: bootLogger.child('subagent'),
|
|
1557
|
+
},
|
|
1558
|
+
// v4.6 Phase 2Q-B — REPL parent-run wiring. Reads the shared
|
|
1559
|
+
// `replParentRunRef` mutated by `ChatSession.runAgentTurn` so
|
|
1560
|
+
// fanout children get `spawned_from_run_id` populated. Returns
|
|
1561
|
+
// undefined between turns (ref cleared post-turn), matching the
|
|
1562
|
+
// pre-2Q-B behaviour for slash-command-triggered spawns.
|
|
1563
|
+
resolveParentRunId: () => replParentRunRef.runId ?? undefined,
|
|
1564
|
+
resolveParentSessionId: () => replParentRunRef.sessionId ?? undefined,
|
|
1449
1565
|
resolveProviders: () => {
|
|
1450
1566
|
// When the parent uses FallbackAdapter, expose every key-present
|
|
1451
1567
|
// slot's (providerId, modelId) so rotation can spread children
|
|
@@ -1466,110 +1582,68 @@ async function buildAgentRuntime(cliOpts, opts) {
|
|
|
1466
1582
|
}
|
|
1467
1583
|
return [{ providerId, modelId }];
|
|
1468
1584
|
},
|
|
1469
|
-
runChild: async (childOpts) => {
|
|
1470
|
-
// Per-child context: paths / skillLoader / memoryManager / processes
|
|
1471
|
-
// are SAFE to share (read-only or per-call by design). The approval
|
|
1472
|
-
// engine is intentionally OMITTED — N children competing for one
|
|
1473
|
-
// stdin REPL would deadlock.
|
|
1474
|
-
const childCtx = {
|
|
1475
|
-
cwd: process.cwd(),
|
|
1476
|
-
paths,
|
|
1477
|
-
sessions: sessionManager,
|
|
1478
|
-
memory: memoryManager,
|
|
1479
|
-
skillLoader,
|
|
1480
|
-
// approvalEngine, ssrfProtection, tirithScanner, memoryGuard:
|
|
1481
|
-
// SSRF + Tirith would be safe to share but adding them now
|
|
1482
|
-
// expands the per-child surface; keep lean for v4.1-subagent.1
|
|
1483
|
-
// and revisit when fanout actually exercises network or shell
|
|
1484
|
-
// tools (gated by ALLOW_DESTRUCTIVE).
|
|
1485
|
-
};
|
|
1486
|
-
// Filter the tool surface. Default-safe: read-only tools only.
|
|
1487
|
-
// AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE=1 mirrors the MCP env from
|
|
1488
|
-
// v4.1-mcp — predictable, env-driven.
|
|
1489
|
-
const allowDestructive = process.env.AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE === '1' ||
|
|
1490
|
-
process.env.AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE === 'true';
|
|
1491
|
-
const childToolNames = [];
|
|
1492
|
-
for (const name of toolRegistry.list()) {
|
|
1493
|
-
const h = toolRegistry.get(name);
|
|
1494
|
-
if (!h)
|
|
1495
|
-
continue;
|
|
1496
|
-
if (h.mutates && !allowDestructive)
|
|
1497
|
-
continue;
|
|
1498
|
-
// Avoid recursive fanout this phase — children cannot spawn
|
|
1499
|
-
// their own children. Recursion was capped at depth 1 by
|
|
1500
|
-
// default in prior multi-agent systems for the same reason;
|
|
1501
|
-
// v3 starved nested spawns.
|
|
1502
|
-
if (name === 'subagent_fanout')
|
|
1503
|
-
continue;
|
|
1504
|
-
childToolNames.push(name);
|
|
1505
|
-
}
|
|
1506
|
-
const childExecutor = toolRegistry.buildExecutor(childCtx);
|
|
1507
|
-
const childTools = childToolNames
|
|
1508
|
-
.map((n) => toolRegistry.get(n)?.schema)
|
|
1509
|
-
.filter((s) => !!s);
|
|
1510
|
-
// Provider isolation: clone the FallbackAdapter so per-child
|
|
1511
|
-
// rate-limit state doesn't pollute the parent or siblings.
|
|
1512
|
-
// Non-Fallback adapters are stateless by spec (providers/v4/
|
|
1513
|
-
// types.ts:190) so direct reuse is safe.
|
|
1514
|
-
const childProvider = adapter instanceof providerFallback_1.FallbackAdapter
|
|
1515
|
-
? adapter.clone()
|
|
1516
|
-
: adapter;
|
|
1517
|
-
// Build per-child AidenAgent. Skip the moat layers (PlannerGuard,
|
|
1518
|
-
// HonestyEnforcement, SkillTeacher, SkillEnforcementTracker) —
|
|
1519
|
-
// they're parent-loop concerns and add cost without value at the
|
|
1520
|
-
// child scale. Skip promptBuilder too: children get a SHORT
|
|
1521
|
-
// system prompt (brief identity + role) instead of the parent's
|
|
1522
|
-
// full SOUL.md + 72-skills inventory + memory snapshot. The
|
|
1523
|
-
// tradeoff is deliberate — children answer the GOAL, not "be
|
|
1524
|
-
// Aiden". With the full prompt, trivial queries take 30s+ for
|
|
1525
|
-
// children to generate verbose self-introductions; the lean
|
|
1526
|
-
// child prompt brings n=2 trivial fanouts under 12s. Parent
|
|
1527
|
-
// should pass any context children genuinely need via the
|
|
1528
|
-
// `query` / `tasks[].context` argument.
|
|
1529
|
-
const child = new aidenAgent_1.AidenAgent({
|
|
1530
|
-
provider: childProvider,
|
|
1531
|
-
tools: childTools,
|
|
1532
|
-
toolExecutor: childExecutor,
|
|
1533
|
-
maxTurns: childOpts.maxIterations,
|
|
1534
|
-
providerId: childOpts.provider.providerId,
|
|
1535
|
-
modelId: childOpts.provider.modelId,
|
|
1536
|
-
// No promptBuilder — childSystemPrompt prepended manually below.
|
|
1537
|
-
// No fallback strategy — child failures bubble up to the
|
|
1538
|
-
// orchestrator, which surfaces them in the result envelope.
|
|
1539
|
-
});
|
|
1540
|
-
// Honour the abort signal — if the parent aborts mid-call (or the
|
|
1541
|
-
// per-child timeout fires), short-circuit before dispatching to
|
|
1542
|
-
// the provider. AidenAgent doesn't take an AbortSignal directly;
|
|
1543
|
-
// the AbortController plumbing through fetch is the
|
|
1544
|
-
// v4.1-subagent.2 / v4.2 hardening pass. Pre-check here for the
|
|
1545
|
-
// synchronous path.
|
|
1546
|
-
if (childOpts.signal.aborted) {
|
|
1547
|
-
throw new Error('aborted before dispatch');
|
|
1548
|
-
}
|
|
1549
|
-
// Brief, role-aware system prompt — drops 5KB+ of Aiden identity
|
|
1550
|
-
// boilerplate that would otherwise inflate every child to 30s+
|
|
1551
|
-
// wall-clock for a trivial query. The parent agent retains the
|
|
1552
|
-
// full prompt when it's the orchestrator; children answer the
|
|
1553
|
-
// goal directly.
|
|
1554
|
-
const roleLine = childOpts.role
|
|
1555
|
-
? `Role: ${childOpts.role}. `
|
|
1556
|
-
: '';
|
|
1557
|
-
const childSystemPrompt = `You are one of ${childOpts.index >= 0 ? 'N' : '?'} parallel subagents. ` +
|
|
1558
|
-
`${roleLine}Answer the user's request concisely. Use available tools when ` +
|
|
1559
|
-
`the answer requires real-world information you don't have memorized.`;
|
|
1560
|
-
const history = [
|
|
1561
|
-
{ role: 'system', content: childSystemPrompt },
|
|
1562
|
-
{ role: 'user', content: childOpts.prompt },
|
|
1563
|
-
];
|
|
1564
|
-
const result = await child.runConversation(history);
|
|
1565
|
-
return result.finalContent;
|
|
1566
|
-
},
|
|
1567
1585
|
}));
|
|
1568
1586
|
bootLogger.child('subagent').info('subagent_fanout: wired (replaces stub)', {
|
|
1569
1587
|
providerId,
|
|
1570
1588
|
modelId,
|
|
1571
1589
|
fallback: adapter instanceof providerFallback_1.FallbackAdapter ? 'FallbackAdapter' : 'direct',
|
|
1572
1590
|
});
|
|
1591
|
+
// ── v4.6 Phase 1 — register spawn_sub_agent (REPL only) ────────────────
|
|
1592
|
+
//
|
|
1593
|
+
// The new single-child synchronous primitive. Coexists with
|
|
1594
|
+
// subagent_fanout (Q9 — additive in Phase 1; Phase 2 will refactor
|
|
1595
|
+
// fanout to call this primitive N times).
|
|
1596
|
+
//
|
|
1597
|
+
// Wired here, AFTER `agent` and `toolExecutor` are in scope, because
|
|
1598
|
+
// the child builder needs the parent agent's reference (to read
|
|
1599
|
+
// `getCurrentSignal()` at dispatch time per the agent-instance signal
|
|
1600
|
+
// pattern) and the parent's tool context for ssrf/tirith/memory/etc.
|
|
1601
|
+
//
|
|
1602
|
+
// Deliberately NOT registered in cli/v4/daemonAgentBuilder.ts —
|
|
1603
|
+
// daemon-fired agents don't expose spawn_sub_agent in their tool
|
|
1604
|
+
// catalog (Q6 lock).
|
|
1605
|
+
toolRegistry.register((0, spawnSubAgentTool_1.makeSpawnSubAgentTool)({
|
|
1606
|
+
parentAgent: agent,
|
|
1607
|
+
toolRegistry,
|
|
1608
|
+
parentToolContext: {
|
|
1609
|
+
cwd: process.cwd(),
|
|
1610
|
+
paths,
|
|
1611
|
+
sessions: sessionManager,
|
|
1612
|
+
memory: memoryManager,
|
|
1613
|
+
memoryGuard,
|
|
1614
|
+
// approvalEngine intentionally OMITTED — the child builder
|
|
1615
|
+
// constructs its own auto-deny ApprovalEngine. Listing it here
|
|
1616
|
+
// would be ignored (childBuilder overrides via spread), but
|
|
1617
|
+
// keeping it out makes the intent explicit.
|
|
1618
|
+
ssrfProtection,
|
|
1619
|
+
tirithScanner,
|
|
1620
|
+
skillLoader,
|
|
1621
|
+
},
|
|
1622
|
+
parentProvider: adapter,
|
|
1623
|
+
parentProviderId: providerId,
|
|
1624
|
+
parentModelId: modelId,
|
|
1625
|
+
resolveVerifiedFlag,
|
|
1626
|
+
resolveToolset,
|
|
1627
|
+
resolveMutates,
|
|
1628
|
+
runStore: replRunStore,
|
|
1629
|
+
instanceId: replInstanceId,
|
|
1630
|
+
// v4.6 Phase 2Q-B — REPL parent-run wiring. Reads the same
|
|
1631
|
+
// shared `replParentRunRef` the fanout factory above uses;
|
|
1632
|
+
// ChatSession.runAgentTurn populates it before each turn
|
|
1633
|
+
// dispatches. Returns undefined between turns so spawns from
|
|
1634
|
+
// slash-command handlers stay top-level (consistent with
|
|
1635
|
+
// pre-2Q-B observable behaviour).
|
|
1636
|
+
resolveParentRunId: () => replParentRunRef.runId ?? undefined,
|
|
1637
|
+
resolveParentSessionId: () => replParentRunRef.sessionId ?? undefined,
|
|
1638
|
+
// v4.6 Phase 1 observability — info-level traces for spec at
|
|
1639
|
+
// invocation, child-tools count, completion, and per-tool-call
|
|
1640
|
+
// run_events on the child's runs row.
|
|
1641
|
+
logger: bootLogger.child('subagent'),
|
|
1642
|
+
}));
|
|
1643
|
+
bootLogger.child('subagent').info('spawn_sub_agent: wired (REPL only)', {
|
|
1644
|
+
instanceId: replInstanceId,
|
|
1645
|
+
dbPath: (0, daemonConfig_1.daemonDbPath)(paths.root),
|
|
1646
|
+
});
|
|
1573
1647
|
// ── Phase v4.1-2.1: gateway message processor ────────────────────
|
|
1574
1648
|
//
|
|
1575
1649
|
// Channel adapters call `gateway.routeMessage(...)` for every inbound
|
|
@@ -1762,6 +1836,10 @@ async function buildAgentRuntime(cliOpts, opts) {
|
|
|
1762
1836
|
exploreMode,
|
|
1763
1837
|
channelManager,
|
|
1764
1838
|
daemonAgentBuilder,
|
|
1839
|
+
// v4.6 Phase 2Q-B — REPL parent-run wiring.
|
|
1840
|
+
replRunStore,
|
|
1841
|
+
replInstanceId,
|
|
1842
|
+
replParentRunRef,
|
|
1765
1843
|
};
|
|
1766
1844
|
}
|
|
1767
1845
|
async function runInteractiveChat(cliOpts, opts) {
|
|
@@ -1828,6 +1906,14 @@ async function runInteractiveChat(cliOpts, opts) {
|
|
|
1828
1906
|
// when /quit fires the auto-summary path.
|
|
1829
1907
|
memoryManager: runtime.memoryManager,
|
|
1830
1908
|
memoryGuard: runtime.memoryGuard,
|
|
1909
|
+
// v4.6 Phase 2Q-B — REPL parent-run wiring. ChatSession.runAgentTurn
|
|
1910
|
+
// writes a runs row per turn and stores its id into
|
|
1911
|
+
// `replParentRunRef`; the spawn / fanout factories above read the
|
|
1912
|
+
// ref via their `resolveParentRunId` / `resolveParentSessionId`
|
|
1913
|
+
// callbacks so children get `spawned_from_run_id` populated.
|
|
1914
|
+
replRunStore: runtime.replRunStore,
|
|
1915
|
+
replInstanceId: runtime.replInstanceId,
|
|
1916
|
+
replParentRunRef: runtime.replParentRunRef,
|
|
1831
1917
|
};
|
|
1832
1918
|
if (cliOpts.tui) {
|
|
1833
1919
|
await (0, aidenTUI_1.runTuiMode)({
|
|
@@ -922,6 +922,43 @@ class ChatSession {
|
|
|
922
922
|
const baseHistory = newHistory.length > 0
|
|
923
923
|
? [...this.history, ...newHistory, userMsg]
|
|
924
924
|
: [...this.history, userMsg];
|
|
925
|
+
// v4.6 Phase 2Q-B — REPL parent-run row (best-effort).
|
|
926
|
+
//
|
|
927
|
+
// Insert a `runs` row tagged with this REPL session BEFORE the
|
|
928
|
+
// agent loop dispatches. Capture the row id into the shared
|
|
929
|
+
// `replParentRunRef` so any `spawn_sub_agent` / `subagent_fanout`
|
|
930
|
+
// child this turn produces can link back via
|
|
931
|
+
// `spawned_from_run_id`. The ref is cleared in the catch /
|
|
932
|
+
// success paths below regardless of outcome.
|
|
933
|
+
//
|
|
934
|
+
// Defensive: a runStore write failure (locked DB, schema drift,
|
|
935
|
+
// etc.) must NOT crash the REPL — every persistence call here is
|
|
936
|
+
// wrapped in try/catch and reduces to a logged warning. The
|
|
937
|
+
// user-facing turn still runs.
|
|
938
|
+
let replRunId = null;
|
|
939
|
+
const replRunStore = this.opts.replRunStore;
|
|
940
|
+
const replInstanceId = this.opts.replInstanceId;
|
|
941
|
+
const replParentRunRef = this.opts.replParentRunRef;
|
|
942
|
+
if (replRunStore && replInstanceId && this.sessionId) {
|
|
943
|
+
try {
|
|
944
|
+
replRunId = replRunStore.create({
|
|
945
|
+
sessionId: this.sessionId,
|
|
946
|
+
instanceId: replInstanceId,
|
|
947
|
+
status: 'running',
|
|
948
|
+
startedAt: turnStartedAt,
|
|
949
|
+
});
|
|
950
|
+
if (replParentRunRef) {
|
|
951
|
+
replParentRunRef.runId = replRunId;
|
|
952
|
+
replParentRunRef.sessionId = this.sessionId;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
catch (err) {
|
|
956
|
+
// Logged once per turn; the user's chat is not interrupted.
|
|
957
|
+
// eslint-disable-next-line no-console
|
|
958
|
+
console.warn('[runs] failed to write REPL parent-run row:', err instanceof Error ? err.message : String(err));
|
|
959
|
+
replRunId = null;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
925
962
|
// Phase 16c: streaming gated on display.streaming config.
|
|
926
963
|
// v4.1.4 Part 1.6: PRODUCTION DEFAULT FLIPPED FROM FALSE TO TRUE.
|
|
927
964
|
// Streaming delivers the activity indicator, tool-row live tick,
|
|
@@ -1179,6 +1216,35 @@ class ChatSession {
|
|
|
1179
1216
|
this.history = result.messages;
|
|
1180
1217
|
this.totalUsage.inputTokens += result.totalUsage.inputTokens;
|
|
1181
1218
|
this.totalUsage.outputTokens += result.totalUsage.outputTokens;
|
|
1219
|
+
// v4.6 Phase 2Q-B — finalize the REPL parent-run row on success.
|
|
1220
|
+
// `finishReason` from the agent loop maps directly into our DB
|
|
1221
|
+
// status: `stop` → completed; `interrupted` / `tool_loop` →
|
|
1222
|
+
// surface as 'interrupted' so it's visible in `runs list`;
|
|
1223
|
+
// `budget_exhausted` / `error` → failed. Wrapped in try/catch
|
|
1224
|
+
// so even a runStore write failure here can't crash the REPL.
|
|
1225
|
+
if (replRunStore && replRunId !== null) {
|
|
1226
|
+
try {
|
|
1227
|
+
const dbStatus = result.finishReason === 'stop' ? 'completed' :
|
|
1228
|
+
result.finishReason === 'interrupted' ? 'interrupted' :
|
|
1229
|
+
result.finishReason === 'tool_loop' ? 'interrupted' :
|
|
1230
|
+
'failed';
|
|
1231
|
+
replRunStore.setStatus(replRunId, dbStatus, {
|
|
1232
|
+
finishReason: result.finishReason,
|
|
1233
|
+
completedAt: Date.now(),
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
catch (err) {
|
|
1237
|
+
// eslint-disable-next-line no-console
|
|
1238
|
+
console.warn('[runs] failed to finalize REPL parent-run row:', err instanceof Error ? err.message : String(err));
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
// Clear the shared ref so a subsequent turn (or stray
|
|
1242
|
+
// spawn/fanout dispatched between turns from a slash command
|
|
1243
|
+
// handler) doesn't see a stale parent id.
|
|
1244
|
+
if (replParentRunRef) {
|
|
1245
|
+
replParentRunRef.runId = null;
|
|
1246
|
+
replParentRunRef.sessionId = null;
|
|
1247
|
+
}
|
|
1182
1248
|
// Phase 16d: surface inline confirmations for verified memory writes.
|
|
1183
1249
|
// We MUST gate on verified=true (the post-write read flag from
|
|
1184
1250
|
// MemoryGuard) — HonestyEnforcement uses the same flag to catch
|
|
@@ -1289,6 +1355,26 @@ class ChatSession {
|
|
|
1289
1355
|
progressBar?.hide();
|
|
1290
1356
|
if (streamingActive)
|
|
1291
1357
|
this.opts.display.streamComplete();
|
|
1358
|
+
// v4.6 Phase 2Q-B — finalize REPL parent-run row on error.
|
|
1359
|
+
// Visible in `aiden runs list` as a failed top-level row so
|
|
1360
|
+
// operators can correlate a chat error with whatever children
|
|
1361
|
+
// it had already kicked off this turn.
|
|
1362
|
+
if (replRunStore && replRunId !== null) {
|
|
1363
|
+
try {
|
|
1364
|
+
replRunStore.setStatus(replRunId, 'failed', {
|
|
1365
|
+
finishReason: 'error',
|
|
1366
|
+
completedAt: Date.now(),
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
catch (e2) {
|
|
1370
|
+
// eslint-disable-next-line no-console
|
|
1371
|
+
console.warn('[runs] failed to mark REPL parent-run failed:', e2 instanceof Error ? e2.message : String(e2));
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
if (replParentRunRef) {
|
|
1375
|
+
replParentRunRef.runId = null;
|
|
1376
|
+
replParentRunRef.sessionId = null;
|
|
1377
|
+
}
|
|
1292
1378
|
const msg = err?.message ?? String(err);
|
|
1293
1379
|
// v4.1.3-prebump: classify the error so the suggestion below
|
|
1294
1380
|
// points at the actual fix instead of the generic "/model or
|
|
@@ -1418,6 +1504,27 @@ class ChatSession {
|
|
|
1418
1504
|
providerOk: !this.opts.unconfigured,
|
|
1419
1505
|
version: version_1.VERSION,
|
|
1420
1506
|
}) + '\n');
|
|
1507
|
+
// v4.6 Phase 3A — operator kill-switch indicator. Lands ABOVE
|
|
1508
|
+
// the blank-line + provider-source annotation so an operator
|
|
1509
|
+
// who paused in a prior session sees the state immediately on
|
|
1510
|
+
// boot, alongside the standard status pills. Single dim
|
|
1511
|
+
// warning line; no special chrome — the message itself is the
|
|
1512
|
+
// visual signal.
|
|
1513
|
+
try {
|
|
1514
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1515
|
+
const { getSpawnPause } = require('../../core/v4/subagent/spawnPause');
|
|
1516
|
+
const s = getSpawnPause().status();
|
|
1517
|
+
if (s.paused) {
|
|
1518
|
+
const reasonSuffix = s.reason ? ` · ${s.reason}` : '';
|
|
1519
|
+
const durationSuffix = typeof s.durationMs === 'number'
|
|
1520
|
+
? ` · ${formatDuration(s.durationMs)}`
|
|
1521
|
+
: '';
|
|
1522
|
+
display.warn(`spawn-pause: ON${reasonSuffix}${durationSuffix} — use /spawn-pause off to resume`);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
catch {
|
|
1526
|
+
// Singleton not initialised (test stubs, etc.) — silently skip.
|
|
1527
|
+
}
|
|
1421
1528
|
// v4.5 TUI polish — blank line so the status pills row doesn't
|
|
1422
1529
|
// crowd the muted source annotation right beneath it.
|
|
1423
1530
|
display.write('\n');
|
|
@@ -31,12 +31,14 @@ const LABEL = {
|
|
|
31
31
|
tce: 'TCE',
|
|
32
32
|
browser_depth: 'Browser depth',
|
|
33
33
|
suggestions: 'Suggestions',
|
|
34
|
+
planner_guard: 'Planner-Guard',
|
|
34
35
|
};
|
|
35
36
|
const CONFIG_DOTTED = {
|
|
36
37
|
sandbox: 'runtime_toggles.sandbox',
|
|
37
38
|
tce: 'runtime_toggles.tce',
|
|
38
39
|
browser_depth: 'runtime_toggles.browser_depth',
|
|
39
40
|
suggestions: 'runtime_toggles.suggestions',
|
|
41
|
+
planner_guard: 'runtime_toggles.planner_guard',
|
|
40
42
|
};
|
|
41
43
|
/**
|
|
42
44
|
* Apply a toggle change. When `ctx.config` is wired, persists to
|