@bahulam/code 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -2
- package/src/agents/loader.mjs +26 -4
- package/src/auth/bahulam-auth.mjs +2 -13
- package/src/auth/tarang-auth.mjs +313 -0
- package/src/commands/agent.mjs +7 -7
- package/src/commands/plugin-manage.mjs +449 -0
- package/src/commands/plugin.mjs +247 -0
- package/src/config/cli-args.mjs +16 -0
- package/src/config/env.mjs +2 -0
- package/src/config/hook-runner.mjs +8 -8
- package/src/config/memory-loader.mjs +7 -3
- package/src/config/settings-loader.mjs +5 -3
- package/src/core/attachments.mjs +2 -2
- package/src/core/background-tasks.mjs +186 -0
- package/src/core/headless.mjs +59 -3
- package/src/core/local-agent.mjs +10 -1
- package/src/core/local-store.mjs +10 -10
- package/src/core/paths.mjs +10 -96
- package/src/core/policy-resolver.mjs +1 -1
- package/src/core/project-context-loader.mjs +2 -2
- package/src/core/risk-tier.mjs +1 -0
- package/src/core/stream-client.mjs +148 -1
- package/src/core/system-prompt.mjs +31 -12
- package/src/core/tool-executor.mjs +457 -12
- package/src/local-service/agent-relay.mjs +139 -12
- package/src/local-service/server.mjs +345 -20
- package/src/orchestration/approval.mjs +30 -0
- package/src/orchestration/completion-triggers.mjs +40 -0
- package/src/orchestration/dispatch.mjs +118 -0
- package/src/orchestration/events.mjs +19 -0
- package/src/orchestration/graph.mjs +126 -0
- package/src/orchestration/node-runner.mjs +193 -0
- package/src/orchestration/runner.mjs +200 -0
- package/src/plugins/executor.mjs +121 -0
- package/src/plugins/loader.mjs +123 -123
- package/src/plugins/manifest.mjs +291 -0
- package/src/plugins/preflight.mjs +227 -0
- package/src/plugins/registry.mjs +233 -0
- package/src/plugins/state.mjs +290 -0
- package/src/terminal/agents.mjs +26 -4
- package/src/terminal/init.mjs +2 -2
- package/src/terminal/main.mjs +83 -4
- package/src/terminal/repl-explore.mjs +1 -1
- package/src/terminal/repl-render.mjs +67 -12
- package/src/terminal/repl-state.mjs +4 -2
- package/src/terminal/repl.mjs +621 -99
- package/src/tools/agent.mjs +6 -2
- package/src/tools/analyze-image.mjs +1 -1
- package/src/tools/project-overview.mjs +7 -7
- package/src/tools/registry.mjs +88 -4
- package/src/ui/slash-commands.mjs +19 -1
- package/src/ui/sub-agent.mjs +14 -8
package/src/terminal/repl.mjs
CHANGED
|
@@ -64,6 +64,10 @@ import { SkillInstaller } from '../skills/installer.mjs';
|
|
|
64
64
|
import { SkillsLoader } from '../skills/loader.mjs';
|
|
65
65
|
import { openSkillsPicker, formatSkillsList } from './skills-picker.mjs';
|
|
66
66
|
import { createAgentFile, isVsCodeTerminal, listLocalAgents, openAgentFile, syncAgentsToBackend } from '../agents/scaffold.mjs';
|
|
67
|
+
import { listLocalWorkflows } from '../agents/workflow_scaffold.mjs';
|
|
68
|
+
import { dispatch } from '../orchestration/dispatch.mjs';
|
|
69
|
+
import { registerJobCompletionDispatch } from '../orchestration/completion-triggers.mjs';
|
|
70
|
+
import { PluginRegistry } from '../plugins/registry.mjs';
|
|
67
71
|
import { SessionManager } from '../core/session-manager.mjs';
|
|
68
72
|
import { parseArgs } from '../config/cli-args.mjs';
|
|
69
73
|
import { pickModelOverridesForm } from './repl-model-form.mjs';
|
|
@@ -114,7 +118,7 @@ import {
|
|
|
114
118
|
flushPendingHead,
|
|
115
119
|
isInlineOutcomeTool,
|
|
116
120
|
pushSubAgentWindowLine,
|
|
117
|
-
|
|
121
|
+
rebuildSubAgentWindowGroups,
|
|
118
122
|
renderBlockBoundary,
|
|
119
123
|
renderExploreRun,
|
|
120
124
|
renderFileDiffEvent,
|
|
@@ -875,7 +879,7 @@ function handleAttachmentsCommand(rest = '', ctx) {
|
|
|
875
879
|
}
|
|
876
880
|
|
|
877
881
|
async function confirmVisionUpload(ctx, attachments, { skip = false } = {}) {
|
|
878
|
-
if (skip || process.env.
|
|
882
|
+
if (skip || process.env.BAHULAM_VISION_CONFIRM === '0' || process.env.BAHULAM_VISION_CONFIRM === 'false') {
|
|
879
883
|
return true;
|
|
880
884
|
}
|
|
881
885
|
if (!ctx?._rl || !process.stdin.isTTY) return false;
|
|
@@ -965,6 +969,7 @@ async function handleAgentsCommand(rest = '', ctx) {
|
|
|
965
969
|
force: Boolean(flags.force),
|
|
966
970
|
});
|
|
967
971
|
process.stderr.write(` ${c.green('✓')} ${c.dim('Created local agent:')} ${created.filePath}\n`);
|
|
972
|
+
process.stderr.write(` ${c.dim('Available now in this workspace:')} /run ${created.slug} "<task>"\n`);
|
|
968
973
|
const shouldOpen = !flags['no-open'] && (Boolean(flags.open) || isVsCodeTerminal());
|
|
969
974
|
if (shouldOpen) {
|
|
970
975
|
const opened = openAgentFile(created.filePath, {
|
|
@@ -976,7 +981,7 @@ async function handleAgentsCommand(rest = '', ctx) {
|
|
|
976
981
|
process.stderr.write(` ${c.dim(opened.reason)}\n`);
|
|
977
982
|
}
|
|
978
983
|
}
|
|
979
|
-
process.stderr.write(` ${c.dim('
|
|
984
|
+
process.stderr.write(` ${c.dim('Optional cloud/account sync:')} /agents sync ${created.slug}\n`);
|
|
980
985
|
} catch (err) {
|
|
981
986
|
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
982
987
|
}
|
|
@@ -1002,7 +1007,7 @@ async function handleAgentsCommand(rest = '', ctx) {
|
|
|
1002
1007
|
process.stderr.write(` ${c.yellow('!')} ${c.dim(opened.reason)}\n`);
|
|
1003
1008
|
process.stderr.write(` ${c.dim('Agent file:')} ${agent.source}\n`);
|
|
1004
1009
|
}
|
|
1005
|
-
process.stderr.write(` ${c.dim('
|
|
1010
|
+
process.stderr.write(` ${c.dim('Local changes are available immediately in this workspace. Optional cloud/account sync:')} /agents sync ${agent.slug}\n`);
|
|
1006
1011
|
return;
|
|
1007
1012
|
}
|
|
1008
1013
|
|
|
@@ -1024,7 +1029,7 @@ async function handleAgentsCommand(rest = '', ctx) {
|
|
|
1024
1029
|
agents: selected,
|
|
1025
1030
|
});
|
|
1026
1031
|
const synced = result.synced ?? selected.length;
|
|
1027
|
-
process.stderr.write(` ${c.green('✓')} ${c.dim(`Synced ${synced} agent${synced === 1 ? '' : 's'} to
|
|
1032
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`Synced ${synced} agent${synced === 1 ? '' : 's'} to the backend for account/cloud reuse.`)}\n`);
|
|
1028
1033
|
} catch (err) {
|
|
1029
1034
|
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
1030
1035
|
}
|
|
@@ -1044,6 +1049,43 @@ function printSkillsUsage() {
|
|
|
1044
1049
|
process.stderr.write(` ${c.dim(' /skills update <name> [--project]')}\n`);
|
|
1045
1050
|
}
|
|
1046
1051
|
|
|
1052
|
+
async function handlePluginsCommand(rest = '', ctx) {
|
|
1053
|
+
const argv = String(rest || '').trim().split(/\s+/).filter(Boolean);
|
|
1054
|
+
const action = (argv[0] || 'list').toLowerCase();
|
|
1055
|
+
const known = new Set(['install', 'validate', 'check', 'lint', 'list', 'ls', 'remove', 'rm', 'uninstall', 'enable', 'disable', 'info', 'update', 'upgrade']);
|
|
1056
|
+
|
|
1057
|
+
// Bare `/plugins <name>` (no action verb) is treated as info, mirroring how
|
|
1058
|
+
// `/skills <name>` behaves. `/plugins` alone lists.
|
|
1059
|
+
if (!known.has(action)) {
|
|
1060
|
+
if (argv.length === 0) return handlePluginsCommand('list', ctx);
|
|
1061
|
+
return handlePluginsCommand(`info ${argv.join(' ')}`, ctx);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const args = {
|
|
1065
|
+
action, pluginName: null, source: null,
|
|
1066
|
+
global: true, force: false, ref: null, json: false,
|
|
1067
|
+
};
|
|
1068
|
+
for (let i = 1; i < argv.length; i++) {
|
|
1069
|
+
const a = argv[i];
|
|
1070
|
+
if (a === '--project') args.global = false;
|
|
1071
|
+
else if (a === '--global') args.global = true;
|
|
1072
|
+
else if (a === '--force' || a === '-f') args.force = true;
|
|
1073
|
+
else if (a === '--json') args.json = true;
|
|
1074
|
+
else if (a === '--ref' || a === '--tag' || a === '--branch') args.ref = argv[++i];
|
|
1075
|
+
else if (!a.startsWith('-')) {
|
|
1076
|
+
if (action === 'install' && !args.source) args.source = a;
|
|
1077
|
+
else if (['validate', 'check', 'lint'].includes(action) && !args.source && !args.pluginName) {
|
|
1078
|
+
if (a.includes('/')) args.source = a; else args.pluginName = a;
|
|
1079
|
+
}
|
|
1080
|
+
else if (!args.pluginName) args.pluginName = a;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
try {
|
|
1084
|
+
const { handlePluginManagementCommand } = await import('../commands/plugin-manage.mjs');
|
|
1085
|
+
await handlePluginManagementCommand(args, { cwd: process.cwd(), throwOnError: true });
|
|
1086
|
+
} catch { /* already printed by the handler */ }
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1047
1089
|
async function handleSkillsCommand(rest = '', ctx) {
|
|
1048
1090
|
const parts = String(rest || '').trim().split(/\s+/).filter(Boolean);
|
|
1049
1091
|
const hasFlag = (flag) => parts.includes(flag);
|
|
@@ -1482,10 +1524,14 @@ function isDeniedStatusMessage(message = '') {
|
|
|
1482
1524
|
}
|
|
1483
1525
|
|
|
1484
1526
|
function toolCallId(data = {}, tool = 'tool') {
|
|
1485
|
-
return data.call_id || data._callId || data.request_id || data.id ||
|
|
1527
|
+
return data.call_id || data._callId || data.tool_call_id || data.request_id || data.id ||
|
|
1486
1528
|
`${tool}:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1487
1529
|
}
|
|
1488
1530
|
|
|
1531
|
+
function explicitToolCallId(data = {}) {
|
|
1532
|
+
return data.call_id || data._callId || data.tool_call_id || data.request_id || data.id || null;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1489
1535
|
function isSubAgentToolEvent(data = {}) {
|
|
1490
1536
|
return Boolean(data?.internal || data?.sub_agent);
|
|
1491
1537
|
}
|
|
@@ -1495,7 +1541,7 @@ function shouldFoldSubAgentTool(data = {}) {
|
|
|
1495
1541
|
}
|
|
1496
1542
|
|
|
1497
1543
|
function foldedSubAgentName(data = {}) {
|
|
1498
|
-
return data?.sub_agent || data?.agent || data?.type || 'sub-agent';
|
|
1544
|
+
return data?.sub_agent_label || data?.sub_agent || data?.agent || data?.type || 'sub-agent';
|
|
1499
1545
|
}
|
|
1500
1546
|
|
|
1501
1547
|
function subAgentStartingLine(agentType = 'sub-agent') {
|
|
@@ -1508,19 +1554,131 @@ function subAgentStartingLine(agentType = 'sub-agent') {
|
|
|
1508
1554
|
return `→ starting ${normalized}`;
|
|
1509
1555
|
}
|
|
1510
1556
|
|
|
1511
|
-
function
|
|
1557
|
+
function subAgentRunId(data = {}) {
|
|
1558
|
+
return data?.run_id || data?.sub_agent_run_id || null;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
function normalizeSubAgentRunData(data = {}) {
|
|
1562
|
+
if (!data || typeof data !== 'object') return data;
|
|
1563
|
+
const runId = subAgentRunId(data);
|
|
1564
|
+
if (!runId) return data;
|
|
1565
|
+
const laneRun = session.activeSubAgentRuns?.get(runId);
|
|
1566
|
+
const patch = {};
|
|
1567
|
+
if (!data.run_id) patch.run_id = runId;
|
|
1568
|
+
if (laneRun?.type && !data.sub_agent) patch.sub_agent = laneRun.type;
|
|
1569
|
+
if (laneRun && !data.sub_agent_label) patch.sub_agent_label = subAgentLaneLabel(laneRun);
|
|
1570
|
+
return Object.keys(patch).length ? { ...data, ...patch } : data;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
function foldedSubAgentKey(data = {}) {
|
|
1574
|
+
return subAgentRunId(data) || foldedSubAgentName(data);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function hasParallelSubAgentRuns() {
|
|
1578
|
+
return activeSubAgentLanes().length > 1;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function hasReliableSubAgentAttribution(data = {}) {
|
|
1582
|
+
return !(hasParallelSubAgentRuns() && isSubAgentToolEvent(data) && !subAgentRunId(data));
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
function ensureFoldedSubAgentTools(agentType, key = agentType, data = {}) {
|
|
1586
|
+
runtime.foldedSubAgentToolMap = runtime.foldedSubAgentToolMap || new Map();
|
|
1512
1587
|
const current = runtime.foldedSubAgentTools;
|
|
1513
|
-
if (current && current.
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1588
|
+
if (current && current.key === key) return current;
|
|
1589
|
+
|
|
1590
|
+
let fold = runtime.foldedSubAgentToolMap.get(key);
|
|
1591
|
+
if (!fold) {
|
|
1592
|
+
const runId = subAgentRunId(data);
|
|
1593
|
+
const laneRun = runId ? session.activeSubAgentRuns?.get(runId) : null;
|
|
1594
|
+
fold = {
|
|
1595
|
+
key,
|
|
1596
|
+
runId,
|
|
1597
|
+
agentType,
|
|
1598
|
+
label: laneRun?.label || agentType,
|
|
1599
|
+
query: laneRun?.query || data?.query || '',
|
|
1600
|
+
entries: [],
|
|
1601
|
+
startedAt: Date.now(),
|
|
1602
|
+
};
|
|
1603
|
+
runtime.foldedSubAgentToolMap.set(key, fold);
|
|
1604
|
+
} else {
|
|
1605
|
+
const runId = subAgentRunId(data);
|
|
1606
|
+
const laneRun = runId ? session.activeSubAgentRuns?.get(runId) : null;
|
|
1607
|
+
if (runId && !fold.runId) fold.runId = runId;
|
|
1608
|
+
if (laneRun) fold.label = subAgentLaneLabel(laneRun);
|
|
1609
|
+
if (laneRun?.query && !fold.query) fold.query = laneRun.query;
|
|
1610
|
+
}
|
|
1611
|
+
runtime.foldedSubAgentTools = fold;
|
|
1612
|
+
return fold;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
function createSubAgentLane(agentType, query, runId, data = {}) {
|
|
1616
|
+
session.activeSubAgentRuns = session.activeSubAgentRuns || new Map();
|
|
1617
|
+
const sameTypeOrdinals = [...session.activeSubAgentRuns.values()]
|
|
1618
|
+
.filter(run => run.type === agentType)
|
|
1619
|
+
.map(run => Number(run.ordinal || 1));
|
|
1620
|
+
const ordinal = sameTypeOrdinals.length ? Math.max(...sameTypeOrdinals) + 1 : 1;
|
|
1621
|
+
const lane = {
|
|
1622
|
+
type: agentType,
|
|
1623
|
+
ordinal,
|
|
1624
|
+
label: agentType,
|
|
1625
|
+
runId,
|
|
1626
|
+
query,
|
|
1627
|
+
tools: 0,
|
|
1628
|
+
// Backend signals the run starts as one of N siblings — label with
|
|
1629
|
+
// the ordinal from the first event (explore#1) so the open block and
|
|
1630
|
+
// the close line agree even for the first run of a batch.
|
|
1631
|
+
forceOrdinal: Number(data?.parallel_batch) > 1,
|
|
1519
1632
|
};
|
|
1520
|
-
|
|
1633
|
+
session.activeSubAgentRuns.set(runId, lane);
|
|
1634
|
+
ensureFoldedSubAgentTools(agentType, runId, { type: agentType, query, run_id: runId });
|
|
1635
|
+
_syncSubAgentWindow();
|
|
1636
|
+
return lane;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
function activeSubAgentLanes() {
|
|
1640
|
+
return session.activeSubAgentRuns instanceof Map
|
|
1641
|
+
? [...session.activeSubAgentRuns.values()]
|
|
1642
|
+
: [];
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function subAgentLaneLabel(lane, lanes = activeSubAgentLanes()) {
|
|
1646
|
+
const sameType = lanes.filter(item => item.type === lane.type).length;
|
|
1647
|
+
if (lane.forceOrdinal || sameType > 1 || Number(lane.ordinal || 1) > 1) {
|
|
1648
|
+
return `${lane.type}#${lane.ordinal || 1}`;
|
|
1649
|
+
}
|
|
1650
|
+
return lane.label || lane.type || 'sub-agent';
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
function removeFoldedSubAgentTools(key) {
|
|
1654
|
+
const map = runtime.foldedSubAgentToolMap;
|
|
1655
|
+
if (key && map?.has(key)) {
|
|
1656
|
+
const fold = map.get(key);
|
|
1657
|
+
map.delete(key);
|
|
1658
|
+
if (runtime.foldedSubAgentTools?.key === key) {
|
|
1659
|
+
runtime.foldedSubAgentTools = map.values().next().value || null;
|
|
1660
|
+
}
|
|
1661
|
+
return fold;
|
|
1662
|
+
}
|
|
1663
|
+
if (!key && map?.size) {
|
|
1664
|
+
const folds = [...map.values()];
|
|
1665
|
+
map.clear();
|
|
1666
|
+
runtime.foldedSubAgentTools = null;
|
|
1667
|
+
return folds;
|
|
1668
|
+
}
|
|
1669
|
+
const fold = runtime.foldedSubAgentTools;
|
|
1670
|
+
runtime.foldedSubAgentTools = null;
|
|
1671
|
+
if (map?.clear) map.clear();
|
|
1672
|
+
return fold;
|
|
1521
1673
|
}
|
|
1522
1674
|
|
|
1523
|
-
function
|
|
1675
|
+
function resetFoldedSubAgentTools() {
|
|
1676
|
+
runtime.foldedSubAgentTools = null;
|
|
1677
|
+
if (runtime.foldedSubAgentToolMap?.clear) runtime.foldedSubAgentToolMap.clear();
|
|
1678
|
+
else runtime.foldedSubAgentToolMap = new Map();
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
function findFoldedToolEntry(fold, callId, tool, summary = null) {
|
|
1524
1682
|
if (!fold) return null;
|
|
1525
1683
|
if (callId) {
|
|
1526
1684
|
const exact = fold.entries.find(entry => entry.callId === callId);
|
|
@@ -1530,21 +1688,37 @@ function findFoldedToolEntry(fold, callId, tool) {
|
|
|
1530
1688
|
const entry = fold.entries[i];
|
|
1531
1689
|
if (entry.tool === tool && !entry.result) return entry;
|
|
1532
1690
|
}
|
|
1691
|
+
// The same underlying call arrives on two event streams with different
|
|
1692
|
+
// id namespaces (bridge tool_call call_id vs framework sub_agent_tool
|
|
1693
|
+
// tool_id). When an entry for this exact tool+summary exists — even one
|
|
1694
|
+
// already resolved — treat the second stream's event as the same call
|
|
1695
|
+
// instead of double-counting it (54 "tool uses" for 27 real calls).
|
|
1696
|
+
if (summary) {
|
|
1697
|
+
for (let i = fold.entries.length - 1; i >= 0; i--) {
|
|
1698
|
+
const entry = fold.entries[i];
|
|
1699
|
+
if (entry.tool === tool && entry.summary === summary) return entry;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1533
1702
|
return null;
|
|
1534
1703
|
}
|
|
1535
1704
|
|
|
1536
1705
|
function foldSubAgentToolCall(data = {}) {
|
|
1706
|
+
// Under parallel runs, an event without a run_id cannot be attributed
|
|
1707
|
+
// to a lane — drop it from the fold rather than guess (it would render
|
|
1708
|
+
// under whichever agent's group happens to match by name).
|
|
1709
|
+
if (!hasReliableSubAgentAttribution(data)) return;
|
|
1537
1710
|
const tool = data?.tool || 'unknown';
|
|
1538
1711
|
const args = data?.args || {};
|
|
1539
|
-
const callId =
|
|
1712
|
+
const callId = explicitToolCallId(data);
|
|
1540
1713
|
const agentType = foldedSubAgentName(data);
|
|
1541
|
-
const fold = ensureFoldedSubAgentTools(agentType);
|
|
1542
|
-
const
|
|
1714
|
+
const fold = ensureFoldedSubAgentTools(agentType, foldedSubAgentKey(data), data);
|
|
1715
|
+
const displaySummary = toolDisplaySummary(tool, args, { cwd: safeCwd() });
|
|
1716
|
+
const existing = findFoldedToolEntry(fold, callId, tool, displaySummary);
|
|
1543
1717
|
const entry = existing || {
|
|
1544
1718
|
callId,
|
|
1545
1719
|
tool,
|
|
1546
1720
|
args,
|
|
1547
|
-
summary:
|
|
1721
|
+
summary: displaySummary,
|
|
1548
1722
|
startedAt: Date.now(),
|
|
1549
1723
|
result: null,
|
|
1550
1724
|
durationMs: null,
|
|
@@ -1554,7 +1728,9 @@ function foldSubAgentToolCall(data = {}) {
|
|
|
1554
1728
|
if (!existing) fold.entries.push(entry);
|
|
1555
1729
|
recordCard({ id: callId, tool, args, startedAt: entry.startedAt });
|
|
1556
1730
|
session.toolCounts[tool] = (session.toolCounts[tool] || 0) + 1;
|
|
1557
|
-
|
|
1731
|
+
// Parallel runs own the aggregate spinner text ("2 agents · explore#1 8
|
|
1732
|
+
// · explore#2 10") — don't clobber it with a single run's tool.
|
|
1733
|
+
if (!hasParallelSubAgentRuns()) updateSpinner(`${agentType} → ${tool}`);
|
|
1558
1734
|
// Live sub-agent window: rebuild from the accumulating fold entries so
|
|
1559
1735
|
// the same rich '• tool head — outcome' bullets that appear in the
|
|
1560
1736
|
// final summary render live during the run. Users previously saw only
|
|
@@ -1564,13 +1740,37 @@ function foldSubAgentToolCall(data = {}) {
|
|
|
1564
1740
|
_syncSubAgentWindow(fold);
|
|
1565
1741
|
}
|
|
1566
1742
|
|
|
1743
|
+
function foldSubAgentToolProgress(data = {}) {
|
|
1744
|
+
const tool = data?.tool || 'unknown';
|
|
1745
|
+
const args = data?.args || {};
|
|
1746
|
+
const callId = explicitToolCallId(data);
|
|
1747
|
+
const agentType = foldedSubAgentName(data);
|
|
1748
|
+
const fold = ensureFoldedSubAgentTools(agentType, foldedSubAgentKey(data), data);
|
|
1749
|
+
const displaySummary = toolDisplaySummary(tool, args, { cwd: safeCwd() });
|
|
1750
|
+
const existing = findFoldedToolEntry(fold, callId, tool, displaySummary);
|
|
1751
|
+
if (!existing) {
|
|
1752
|
+
fold.entries.push({
|
|
1753
|
+
callId,
|
|
1754
|
+
tool,
|
|
1755
|
+
args,
|
|
1756
|
+
summary: displaySummary,
|
|
1757
|
+
startedAt: Date.now(),
|
|
1758
|
+
result: null,
|
|
1759
|
+
durationMs: null,
|
|
1760
|
+
outcome: '',
|
|
1761
|
+
tone: 'dim',
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
_syncSubAgentWindow(fold);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1567
1767
|
function foldSubAgentToolResult(data = {}) {
|
|
1568
1768
|
const tool = data?.tool || data?._tool || 'unknown';
|
|
1569
1769
|
const args = data?.args || {};
|
|
1570
|
-
const callId =
|
|
1770
|
+
const callId = explicitToolCallId(data);
|
|
1571
1771
|
const agentType = foldedSubAgentName(data);
|
|
1572
|
-
const fold = ensureFoldedSubAgentTools(agentType);
|
|
1573
|
-
let entry = findFoldedToolEntry(fold, callId, tool);
|
|
1772
|
+
const fold = ensureFoldedSubAgentTools(agentType, foldedSubAgentKey(data), data);
|
|
1773
|
+
let entry = findFoldedToolEntry(fold, callId, tool, toolDisplaySummary(tool, args, { cwd: safeCwd() }));
|
|
1574
1774
|
if (!entry) {
|
|
1575
1775
|
entry = {
|
|
1576
1776
|
callId,
|
|
@@ -1594,7 +1794,7 @@ function foldSubAgentToolResult(data = {}) {
|
|
|
1594
1794
|
entry.tone = summary.tone || 'dim';
|
|
1595
1795
|
if (data._blocked) session.blockedOps++;
|
|
1596
1796
|
recordCard({ id: callId, tool, args: entry.args, result: data, durationMs, startedAt: entry.startedAt });
|
|
1597
|
-
updateSpinner(`${agentType} → ${tool}`);
|
|
1797
|
+
if (!hasParallelSubAgentRuns()) updateSpinner(`${agentType} → ${tool}`);
|
|
1598
1798
|
// Same live-window sync on result — updates the '• tool' line into
|
|
1599
1799
|
// '• tool — outcome' as each result lands, without waiting for the
|
|
1600
1800
|
// whole sub-agent to complete.
|
|
@@ -1607,15 +1807,63 @@ function foldSubAgentToolResult(data = {}) {
|
|
|
1607
1807
|
* Uses the same foldedToolLine formatter as the completion summary for
|
|
1608
1808
|
* consistency.
|
|
1609
1809
|
*/
|
|
1610
|
-
function _syncSubAgentWindow(
|
|
1611
|
-
if (!fold || !Array.isArray(fold.entries)) return;
|
|
1810
|
+
function _syncSubAgentWindow() {
|
|
1612
1811
|
const cols = process.stderr.columns || 120;
|
|
1613
1812
|
// Reserve column budget for the ' ' indent the window applies in
|
|
1614
1813
|
// presentStatus so we don't wrap.
|
|
1615
|
-
const
|
|
1616
|
-
|
|
1814
|
+
const maxToolRows = 6;
|
|
1815
|
+
const lanes = activeSubAgentLanes();
|
|
1816
|
+
const visibleLanes = lanes.length ? lanes : (
|
|
1817
|
+
runtime.foldedSubAgentToolMap?.size
|
|
1818
|
+
? [...runtime.foldedSubAgentToolMap.values()].map(fold => ({
|
|
1819
|
+
type: fold.agentType,
|
|
1820
|
+
label: fold.label,
|
|
1821
|
+
runId: fold.runId || fold.key,
|
|
1822
|
+
ordinal: 1,
|
|
1823
|
+
}))
|
|
1824
|
+
: []
|
|
1617
1825
|
);
|
|
1618
|
-
|
|
1826
|
+
if (!visibleLanes.length) return;
|
|
1827
|
+
const multi = visibleLanes.length > 1;
|
|
1828
|
+
const groups = [];
|
|
1829
|
+
for (const lane of visibleLanes) {
|
|
1830
|
+
// With multiple lanes, a lane shows ONLY its own run's fold — never
|
|
1831
|
+
// the label/name-keyed or "current" fallbacks, which would leak
|
|
1832
|
+
// unattributed or sibling entries into the wrong agent's group.
|
|
1833
|
+
const fold = runtime.foldedSubAgentToolMap?.get(lane.runId)
|
|
1834
|
+
|| (multi ? null : (runtime.foldedSubAgentToolMap?.get(lane.label) || runtime.foldedSubAgentTools));
|
|
1835
|
+
const entries = Array.isArray(fold?.entries) ? fold.entries : [];
|
|
1836
|
+
const label = subAgentLaneLabel(lane, visibleLanes);
|
|
1837
|
+
if (multi) {
|
|
1838
|
+
// One rotating row per agent under the aggregate spinner: the
|
|
1839
|
+
// latest tool, refreshed as calls arrive. Counts live in the
|
|
1840
|
+
// spinner summary ("2 agents · explore#1 31 · explore#2 14").
|
|
1841
|
+
const last = entries[entries.length - 1];
|
|
1842
|
+
const body = last
|
|
1843
|
+
? foldedToolLine(last, '', Math.max(40, cols - 16)).trimStart()
|
|
1844
|
+
: subAgentStartingLine(lane.type);
|
|
1845
|
+
groups.push({
|
|
1846
|
+
key: lane.runId || label,
|
|
1847
|
+
runId: lane.runId,
|
|
1848
|
+
label,
|
|
1849
|
+
header: '',
|
|
1850
|
+
lines: [fitAnsiLine(`${paint.brand.data(label)} ${body}`, cols)],
|
|
1851
|
+
});
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
const lines = entries.slice(-maxToolRows).map(entry =>
|
|
1855
|
+
foldedToolLine(entry, '', Math.max(40, cols - 4)).trimStart()
|
|
1856
|
+
);
|
|
1857
|
+
if (!lines.length) lines.push(subAgentStartingLine(lane.type));
|
|
1858
|
+
groups.push({
|
|
1859
|
+
key: lane.runId || lane.label || lane.type,
|
|
1860
|
+
runId: lane.runId,
|
|
1861
|
+
label,
|
|
1862
|
+
header: '',
|
|
1863
|
+
lines,
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
rebuildSubAgentWindowGroups(groups);
|
|
1619
1867
|
}
|
|
1620
1868
|
|
|
1621
1869
|
function foldedOutcome(entry) {
|
|
@@ -1637,11 +1885,8 @@ function foldedToolLine(entry, indent, columns) {
|
|
|
1637
1885
|
return fitAnsiLine(line, Math.max(32, columns));
|
|
1638
1886
|
}
|
|
1639
1887
|
|
|
1640
|
-
function
|
|
1641
|
-
const fold = runtime.foldedSubAgentTools;
|
|
1642
|
-
if (!fold) return;
|
|
1888
|
+
function flushOneFoldedSubAgentTools(fold) {
|
|
1643
1889
|
const entries = Array.isArray(fold.entries) ? fold.entries : [];
|
|
1644
|
-
runtime.foldedSubAgentTools = null;
|
|
1645
1890
|
if (!entries.length) return;
|
|
1646
1891
|
|
|
1647
1892
|
renderBlockBoundary('tool', { compactSame: true });
|
|
@@ -1674,6 +1919,16 @@ function flushFoldedSubAgentTools() {
|
|
|
1674
1919
|
runtime.lastRenderedBlock = 'tool';
|
|
1675
1920
|
}
|
|
1676
1921
|
|
|
1922
|
+
function flushFoldedSubAgentTools(key = null) {
|
|
1923
|
+
const fold = removeFoldedSubAgentTools(key);
|
|
1924
|
+
if (!fold) return;
|
|
1925
|
+
if (Array.isArray(fold)) {
|
|
1926
|
+
for (const item of fold) flushOneFoldedSubAgentTools(item);
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
flushOneFoldedSubAgentTools(fold);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1677
1932
|
function renderEvent(event) {
|
|
1678
1933
|
const { type, data } = event;
|
|
1679
1934
|
|
|
@@ -1869,10 +2124,11 @@ function renderEvent(event) {
|
|
|
1869
2124
|
|
|
1870
2125
|
case 'tool_call':
|
|
1871
2126
|
case 'tool_request': {
|
|
2127
|
+
const eventData = normalizeSubAgentRunData(data);
|
|
1872
2128
|
if (watchState.active) {
|
|
1873
|
-
watchState.addEntry('tool', { label:
|
|
2129
|
+
watchState.addEntry('tool', { label: eventData?.tool, detail: eventData?.args?.file_path || eventData?.args?.path || eventData?.args?.pattern || eventData?.args?.query || '' });
|
|
1874
2130
|
}
|
|
1875
|
-
const isInternal = Boolean(
|
|
2131
|
+
const isInternal = Boolean(eventData?.internal || eventData?.sub_agent);
|
|
1876
2132
|
if (isInternal) {
|
|
1877
2133
|
session.subAgentToolCalls++;
|
|
1878
2134
|
session.totalSubAgentToolCalls++;
|
|
@@ -1881,13 +2137,16 @@ function renderEvent(event) {
|
|
|
1881
2137
|
session.totalPrimaryToolCalls++;
|
|
1882
2138
|
}
|
|
1883
2139
|
session.totalToolCalls++;
|
|
1884
|
-
if (
|
|
1885
|
-
|
|
2140
|
+
if (!hasReliableSubAgentAttribution(eventData)) {
|
|
2141
|
+
break;
|
|
2142
|
+
}
|
|
2143
|
+
if (shouldFoldSubAgentTool(eventData)) {
|
|
2144
|
+
foldSubAgentToolCall(eventData);
|
|
1886
2145
|
break;
|
|
1887
2146
|
}
|
|
1888
2147
|
stopSpinner();
|
|
1889
2148
|
flushContent();
|
|
1890
|
-
renderToolCall(
|
|
2149
|
+
renderToolCall(eventData);
|
|
1891
2150
|
break;
|
|
1892
2151
|
}
|
|
1893
2152
|
|
|
@@ -1929,16 +2188,20 @@ function renderEvent(event) {
|
|
|
1929
2188
|
|
|
1930
2189
|
case 'tool_result':
|
|
1931
2190
|
case 'tool_done': {
|
|
2191
|
+
const eventData = normalizeSubAgentRunData(data);
|
|
1932
2192
|
if (watchState.active) {
|
|
1933
|
-
const success =
|
|
1934
|
-
watchState.addEntry('done', { label:
|
|
2193
|
+
const success = eventData?.success !== false;
|
|
2194
|
+
watchState.addEntry('done', { label: eventData?.tool, detail: success ? '✓' : '✗' });
|
|
2195
|
+
}
|
|
2196
|
+
if (!hasReliableSubAgentAttribution(eventData)) {
|
|
2197
|
+
break;
|
|
1935
2198
|
}
|
|
1936
|
-
if (shouldFoldSubAgentTool(
|
|
1937
|
-
foldSubAgentToolResult(
|
|
2199
|
+
if (shouldFoldSubAgentTool(eventData)) {
|
|
2200
|
+
foldSubAgentToolResult(eventData);
|
|
1938
2201
|
break;
|
|
1939
2202
|
}
|
|
1940
2203
|
stopSpinner();
|
|
1941
|
-
renderToolResult(
|
|
2204
|
+
renderToolResult(eventData, type);
|
|
1942
2205
|
break;
|
|
1943
2206
|
}
|
|
1944
2207
|
|
|
@@ -2052,32 +2315,69 @@ function renderEvent(event) {
|
|
|
2052
2315
|
if (watchState.active) {
|
|
2053
2316
|
watchState.addEntry('spawn', { type: data?.type, label: (data?.query || '').slice(0, 60) });
|
|
2054
2317
|
}
|
|
2055
|
-
stopSpinner();
|
|
2056
|
-
clearPendingHead();
|
|
2057
|
-
flushFoldedSubAgentTools();
|
|
2058
2318
|
const agentType = data?.type || 'sub-agent';
|
|
2059
2319
|
const query = data?.query || '';
|
|
2320
|
+
// Parallel sub-agents: track each run in its own display lane. Two
|
|
2321
|
+
// concurrent runs of the same type are only distinguishable by the
|
|
2322
|
+
// backend-issued run_id; label lanes explore#1 / explore#2 when
|
|
2323
|
+
// more than one run is active. Solo runs render exactly as before.
|
|
2324
|
+
session.activeSubAgentRuns = session.activeSubAgentRuns || new Map();
|
|
2325
|
+
const hadActiveRuns = session.activeSubAgentRuns.size > 0;
|
|
2326
|
+
if (!hadActiveRuns) {
|
|
2327
|
+
stopSpinner();
|
|
2328
|
+
clearPendingHead();
|
|
2329
|
+
flushFoldedSubAgentTools();
|
|
2330
|
+
}
|
|
2331
|
+
const runId = data?.run_id || `${agentType}:${Date.now().toString(36)}`;
|
|
2332
|
+
const lane = createSubAgentLane(agentType, query, runId, data);
|
|
2333
|
+
const parallel = session.activeSubAgentRuns.size > 1 || lane.forceOrdinal;
|
|
2334
|
+
const label = subAgentLaneLabel(lane);
|
|
2060
2335
|
renderBlockBoundary('subagent');
|
|
2061
|
-
process.stderr.write(renderSubAgentOpen({ type:
|
|
2336
|
+
process.stderr.write(renderSubAgentOpen({ id: runId, type: label, query, parentDepth: parallel ? 0 : undefined }).replace(/^\n/, '') + '\n');
|
|
2062
2337
|
runtime.lastRenderedBlock = 'subagent';
|
|
2063
2338
|
session.inSubAgent = inSubAgentBlock(); // kept for legacy readers
|
|
2064
2339
|
session.subAgentCounts[agentType] = (session.subAgentCounts[agentType] || 0) + 1;
|
|
2065
2340
|
// Fixed-height live tool window under the spinner (queue mode):
|
|
2066
2341
|
// inner tool calls stream here instead of flooding the transcript.
|
|
2067
2342
|
setSubAgentWindowActive(true);
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2343
|
+
if (hadActiveRuns) {
|
|
2344
|
+
updateSpinner(`${session.activeSubAgentRuns.size} agents running`);
|
|
2345
|
+
} else {
|
|
2346
|
+
// Phase per sub-agent run: the status line counts elapsed time and
|
|
2347
|
+
// tool calls live ("plan agent · 4 calls · 32s") for the whole run.
|
|
2348
|
+
// First run of a signaled batch starts the spinner with its
|
|
2349
|
+
// ordinal label so the display matches the open block.
|
|
2350
|
+
startSpinner(`${label} agent`, { phase: `sub:${agentType}:${Date.now()}` });
|
|
2351
|
+
}
|
|
2352
|
+
_syncSubAgentWindow();
|
|
2072
2353
|
break;
|
|
2073
2354
|
}
|
|
2074
2355
|
|
|
2075
2356
|
case 'sub_agent_tool': {
|
|
2076
2357
|
// The regular tool_call event renders the card, indented by the
|
|
2077
2358
|
// sub-agent stack depth. Just update the spinner text here.
|
|
2359
|
+
const eventData = normalizeSubAgentRunData(data);
|
|
2078
2360
|
const agentType = data?.type || 'sub-agent';
|
|
2079
|
-
const tool =
|
|
2361
|
+
const tool = eventData?.tool || '';
|
|
2080
2362
|
if (!tool) break;
|
|
2363
|
+
// Lane attribution for parallel runs: prefix window/spinner lines
|
|
2364
|
+
// with the run's label so interleaved streams stay readable.
|
|
2365
|
+
const eventRunId = subAgentRunId(eventData);
|
|
2366
|
+
if (!eventRunId && hasParallelSubAgentRuns()) {
|
|
2367
|
+
break;
|
|
2368
|
+
}
|
|
2369
|
+
const laneRun = session.activeSubAgentRuns?.get(eventRunId);
|
|
2370
|
+
if (laneRun) laneRun.tools++;
|
|
2371
|
+
foldSubAgentToolProgress(eventData);
|
|
2372
|
+
const laneParallel = (session.activeSubAgentRuns?.size || 0) > 1;
|
|
2373
|
+
if (laneParallel) {
|
|
2374
|
+
bumpSpinnerProgress();
|
|
2375
|
+
const activeLanes = activeSubAgentLanes();
|
|
2376
|
+
const lanes = activeLanes
|
|
2377
|
+
.map(r => `${subAgentLaneLabel(r, activeLanes)} ${r.tools}`).join(' · ');
|
|
2378
|
+
updateSpinner(`${session.activeSubAgentRuns.size} agents · ${lanes}`);
|
|
2379
|
+
break;
|
|
2380
|
+
}
|
|
2081
2381
|
// Feed the live window from THIS event — it always fires (55/55 in
|
|
2082
2382
|
// observed runs), unlike the inner tool_call render path which
|
|
2083
2383
|
// diverts for explore-category tools and folded verbosity modes.
|
|
@@ -2092,7 +2392,7 @@ function renderEvent(event) {
|
|
|
2092
2392
|
// + window; the fallback line is only for the "otherwise blind"
|
|
2093
2393
|
// case that surfaced in earlier local terminal reports.
|
|
2094
2394
|
if (!rqueue.isActive()) {
|
|
2095
|
-
const label =
|
|
2395
|
+
const label = eventData?.label || '';
|
|
2096
2396
|
const hint = label ? ` · ${label}` : '';
|
|
2097
2397
|
const key = `${agentType}:${tool}:${label}`;
|
|
2098
2398
|
if (session._lastSubAgentInlineKey !== key) {
|
|
@@ -2111,6 +2411,15 @@ function renderEvent(event) {
|
|
|
2111
2411
|
break;
|
|
2112
2412
|
}
|
|
2113
2413
|
|
|
2414
|
+
case 'sub_agent_tool_result': {
|
|
2415
|
+
const eventData = normalizeSubAgentRunData(data);
|
|
2416
|
+
if (!hasReliableSubAgentAttribution(eventData)) {
|
|
2417
|
+
break;
|
|
2418
|
+
}
|
|
2419
|
+
foldSubAgentToolResult(eventData);
|
|
2420
|
+
break;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2114
2423
|
case 'sub_agent_complete': {
|
|
2115
2424
|
if (watchState.active) {
|
|
2116
2425
|
const agentType = data?.type || 'sub-agent';
|
|
@@ -2118,11 +2427,42 @@ function renderEvent(event) {
|
|
|
2118
2427
|
const durationS = data?.duration_s || 0;
|
|
2119
2428
|
watchState.addEntry('done', { type: agentType, detail: `${toolCalls} tools · ${durationS.toFixed(1)}s`, status: 'done' });
|
|
2120
2429
|
}
|
|
2121
|
-
|
|
2122
|
-
stopSpinner();
|
|
2123
|
-
clearPendingHead();
|
|
2124
|
-
flushFoldedSubAgentTools();
|
|
2430
|
+
const eventData = normalizeSubAgentRunData(data);
|
|
2125
2431
|
const agentType = data?.type || 'sub-agent';
|
|
2432
|
+
// Retire this run's display lane; while sibling runs are still
|
|
2433
|
+
// active keep the shared window/spinner alive for them.
|
|
2434
|
+
const eventRunId = subAgentRunId(eventData);
|
|
2435
|
+
const doneRun = session.activeSubAgentRuns?.get(eventRunId);
|
|
2436
|
+
const doneLabel = doneRun ? subAgentLaneLabel(doneRun) : agentType;
|
|
2437
|
+
const doneFold = eventRunId ? removeFoldedSubAgentTools(eventRunId) : null;
|
|
2438
|
+
if (doneFold) {
|
|
2439
|
+
doneFold.agentType = doneLabel;
|
|
2440
|
+
doneFold.label = doneLabel;
|
|
2441
|
+
}
|
|
2442
|
+
if (eventRunId) session.activeSubAgentRuns?.delete(eventRunId);
|
|
2443
|
+
else session.activeSubAgentRuns?.clear();
|
|
2444
|
+
const siblingsActive = (session.activeSubAgentRuns?.size || 0) > 0;
|
|
2445
|
+
// In verbose mode each sub-agent tool already rendered a full
|
|
2446
|
+
// transcript card live — flushing the fold would list every tool a
|
|
2447
|
+
// second time. The fold batch is the durable record ONLY when tools
|
|
2448
|
+
// were folded (default verbosity).
|
|
2449
|
+
const toolsRenderedLive = showSubAgentTools(getVerbosity());
|
|
2450
|
+
if (siblingsActive) {
|
|
2451
|
+
if (doneFold && !toolsRenderedLive) flushOneFoldedSubAgentTools(doneFold);
|
|
2452
|
+
_syncSubAgentWindow();
|
|
2453
|
+
} else {
|
|
2454
|
+
setSubAgentWindowActive(false);
|
|
2455
|
+
stopSpinner();
|
|
2456
|
+
clearPendingHead();
|
|
2457
|
+
session._lanesPreservedAtTurnEnd = false;
|
|
2458
|
+
if (toolsRenderedLive) {
|
|
2459
|
+
if (!doneFold) removeFoldedSubAgentTools(null);
|
|
2460
|
+
} else if (doneFold) {
|
|
2461
|
+
flushOneFoldedSubAgentTools(doneFold);
|
|
2462
|
+
} else {
|
|
2463
|
+
flushFoldedSubAgentTools();
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2126
2466
|
const usage = data?.usage || {};
|
|
2127
2467
|
// Output tokens = generation size. Summing input+output across a
|
|
2128
2468
|
// multi-iteration sub-agent double-counts the context re-shipped each
|
|
@@ -2135,7 +2475,8 @@ function renderEvent(event) {
|
|
|
2135
2475
|
const summary = data?.result_summary
|
|
2136
2476
|
|| (data?.result_length > 0 ? `${agentType} returned ${data.result_length} chars` : '');
|
|
2137
2477
|
process.stderr.write(renderSubAgentClose({
|
|
2138
|
-
|
|
2478
|
+
id: eventRunId,
|
|
2479
|
+
type: doneLabel,
|
|
2139
2480
|
success: data?.success !== false,
|
|
2140
2481
|
summary,
|
|
2141
2482
|
costUsd,
|
|
@@ -2386,8 +2727,24 @@ function renderEvent(event) {
|
|
|
2386
2727
|
if (session.turns === 1 && session.user) telemetry.track('first_answer', {});
|
|
2387
2728
|
stopSpinner();
|
|
2388
2729
|
flushContent();
|
|
2389
|
-
|
|
2390
|
-
|
|
2730
|
+
// Background/parallel runs can outlive the turn. When lanes are
|
|
2731
|
+
// still active at turn end, preserve them (folds, stack, window) so
|
|
2732
|
+
// late events land correctly instead of corrupting fresh state —
|
|
2733
|
+
// but only for ONE turn boundary: if they're still around at the
|
|
2734
|
+
// next complete with no closure, force-clean to avoid stale lanes.
|
|
2735
|
+
const lanesLive = (session.activeSubAgentRuns?.size || 0) > 0;
|
|
2736
|
+
if (lanesLive && !session._lanesPreservedAtTurnEnd) {
|
|
2737
|
+
session._lanesPreservedAtTurnEnd = true;
|
|
2738
|
+
const n = session.activeSubAgentRuns.size;
|
|
2739
|
+
process.stderr.write(` ${c.dim(`${n} agent run${n === 1 ? '' : 's'} still active in background — progress continues below`)}\n`);
|
|
2740
|
+
} else {
|
|
2741
|
+
session._lanesPreservedAtTurnEnd = false;
|
|
2742
|
+
if (showSubAgentTools(getVerbosity())) resetFoldedSubAgentTools();
|
|
2743
|
+
else flushFoldedSubAgentTools();
|
|
2744
|
+
resetSubAgents();
|
|
2745
|
+
session.activeSubAgentRuns?.clear();
|
|
2746
|
+
setSubAgentWindowActive(false);
|
|
2747
|
+
}
|
|
2391
2748
|
session.inSubAgent = false;
|
|
2392
2749
|
|
|
2393
2750
|
const summary = data?.summary || '';
|
|
@@ -2801,57 +3158,160 @@ async function prepareDirectAgentRunContext(ctx, instruction = '') {
|
|
|
2801
3158
|
return execContext;
|
|
2802
3159
|
}
|
|
2803
3160
|
|
|
3161
|
+
function stripWrappingQuotes(value = '') {
|
|
3162
|
+
const text = String(value || '').trim();
|
|
3163
|
+
if (text.length >= 2) {
|
|
3164
|
+
const first = text[0];
|
|
3165
|
+
const last = text[text.length - 1];
|
|
3166
|
+
if ((first === '"' || first === "'") && first === last) {
|
|
3167
|
+
return text.slice(1, -1).trim();
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
return text;
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
function addRunTarget(map, agent, kind = 'agent') {
|
|
3174
|
+
const slug = String(agent?.slug || agent?.command || agent?.name || '').trim();
|
|
3175
|
+
if (!slug || map.has(`${kind}:${slug}`)) return;
|
|
3176
|
+
map.set(`${kind}:${slug}`, {
|
|
3177
|
+
slug,
|
|
3178
|
+
name: agent?.name || slug,
|
|
3179
|
+
description: agent?.description || '',
|
|
3180
|
+
scope: agent?.source_scope || agent?.source || kind,
|
|
3181
|
+
kind,
|
|
3182
|
+
});
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
function printRunUsage(ctx) {
|
|
3186
|
+
process.stderr.write(` ${c.gray('Usage: /run <agent-or-workflow> [instruction]')}\n`);
|
|
3187
|
+
process.stderr.write(` ${c.gray('Example: /run docker-analyzer Analyze all running Docker containers')}\n`);
|
|
3188
|
+
|
|
3189
|
+
const targets = new Map();
|
|
3190
|
+
for (const agent of listLocalAgents(safeCwd())) addRunTarget(targets, agent);
|
|
3191
|
+
for (const agent of BUILTIN_AGENTS) addRunTarget(targets, agent);
|
|
3192
|
+
for (const agent of ctx.toolExecutor?.listRunnables?.() || []) addRunTarget(targets, agent);
|
|
3193
|
+
for (const agent of pluginRegistry?.listAgents?.() || []) addRunTarget(targets, agent);
|
|
3194
|
+
|
|
3195
|
+
const agents = [...targets.values()].filter(item => item.kind === 'agent').slice(0, 12);
|
|
3196
|
+
if (agents.length) {
|
|
3197
|
+
process.stderr.write(`\n ${c.dim('Agents')}\n`);
|
|
3198
|
+
for (const agent of agents) {
|
|
3199
|
+
const desc = agent.description ? ` ${c.dim('- ' + agent.description)}` : '';
|
|
3200
|
+
const scope = agent.scope ? ` ${c.dim('[' + agent.scope + ']')}` : '';
|
|
3201
|
+
process.stderr.write(` ${c.brand(agent.slug)}${scope}${desc}\n`);
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
const workflows = listLocalWorkflows(safeCwd()).slice(0, 8);
|
|
3206
|
+
if (workflows.length) {
|
|
3207
|
+
process.stderr.write(`\n ${c.dim('Workflows')}\n`);
|
|
3208
|
+
for (const workflow of workflows) {
|
|
3209
|
+
const slug = workflow.slug || workflow.name;
|
|
3210
|
+
const desc = workflow.description ? ` ${c.dim('- ' + workflow.description)}` : '';
|
|
3211
|
+
process.stderr.write(` ${c.brand(slug)}${desc}\n`);
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
|
|
2804
3216
|
async function handleRunCommand(rest = '', ctx) {
|
|
2805
3217
|
const parts = String(rest || '').trim().split(/\s+/).filter(Boolean);
|
|
2806
3218
|
const target = parts.shift();
|
|
2807
|
-
const instruction = parts.join(' ');
|
|
3219
|
+
const instruction = stripWrappingQuotes(parts.join(' '));
|
|
2808
3220
|
|
|
2809
3221
|
if (!target) {
|
|
2810
|
-
|
|
3222
|
+
printRunUsage(ctx);
|
|
2811
3223
|
return;
|
|
2812
3224
|
}
|
|
2813
3225
|
|
|
2814
3226
|
const localAgent = listLocalAgents(safeCwd()).find(agent => localAgentMatches(agent, target));
|
|
2815
3227
|
const builtinAgent = findBuiltinAgent(target);
|
|
2816
|
-
const
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
3228
|
+
const registeredAgent = ctx.toolExecutor?.listRunnables?.()
|
|
3229
|
+
?.find(agent => localAgentMatches(agent, target));
|
|
3230
|
+
const pluginAgent = pluginRegistry?.listAgents?.()
|
|
3231
|
+
?.find(agent => localAgentMatches(agent, target));
|
|
3232
|
+
const runnableAgent = localAgent || builtinAgent || registeredAgent || pluginAgent;
|
|
3233
|
+
|
|
3234
|
+
const creds = ctx.auth?.loadCredentials?.() || {};
|
|
3235
|
+
const dispatchCtx = {
|
|
3236
|
+
toolExecutor: ctx.toolExecutor,
|
|
3237
|
+
listRunnables: () => ctx.toolExecutor?.listRunnables?.() || [],
|
|
3238
|
+
listLocalWorkflows: () => listLocalWorkflows(safeCwd()),
|
|
3239
|
+
renderEvent,
|
|
3240
|
+
sessionSubstrate: makeSessionSubstrate(ctx),
|
|
3241
|
+
auth: { token: creds.token || null },
|
|
3242
|
+
credentials: {
|
|
3243
|
+
apiKey: process.env.ANTHROPIC_API_KEY || creds.anthropicKey || null,
|
|
3244
|
+
openRouterKey: process.env.OPENROUTER_API_KEY || creds.openRouterKey || null,
|
|
3245
|
+
},
|
|
3246
|
+
cwd: safeCwd(),
|
|
3247
|
+
};
|
|
3248
|
+
|
|
3249
|
+
try {
|
|
3250
|
+
if (!runnableAgent) process.stderr.write(` ${c.dim(`Running workflow '${target}'...`)}\n`);
|
|
3251
|
+
const outcome = await dispatch({
|
|
3252
|
+
type: 'manual',
|
|
3253
|
+
source: 'repl:/run',
|
|
3254
|
+
target: runnableAgent ? { kind: 'agent', slug: target, agent: runnableAgent } : target,
|
|
3255
|
+
params: { instruction },
|
|
3256
|
+
channel: null,
|
|
3257
|
+
}, dispatchCtx);
|
|
3258
|
+
|
|
3259
|
+
if (!outcome.dispatched) {
|
|
3260
|
+
process.stderr.write(` ${c.red('✗')} ${outcome.reason}\n`);
|
|
3261
|
+
process.stderr.write(` ${c.gray('Usage: /run <agent-or-workflow> [instruction]')}\n`);
|
|
2826
3262
|
return;
|
|
2827
3263
|
}
|
|
2828
|
-
}
|
|
2829
3264
|
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
3265
|
+
const result = outcome.result || {};
|
|
3266
|
+
if (outcome.channel === 'server') {
|
|
3267
|
+
if (result?.success === false) {
|
|
3268
|
+
process.stderr.write(` ${c.red('✗')} ${result.output || `Workflow '${target}' failed.`}\n`);
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3271
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`Workflow '${target}' complete`)}\n`);
|
|
3272
|
+
const details = [];
|
|
3273
|
+
if (result?.run_id) details.push(`run ${result.run_id}`);
|
|
3274
|
+
if (result?.duration_s) details.push(`${result.duration_s}s`);
|
|
3275
|
+
if (result?.total_tokens) details.push(`${formatTokens(result.total_tokens)} tok`);
|
|
3276
|
+
if (result?.total_cost) details.push(formatCostValue(result.total_cost));
|
|
3277
|
+
if (details.length) process.stderr.write(` ${c.dim(details.join(' · '))}\n`);
|
|
3278
|
+
|
|
3279
|
+
// Push server workflow output to conversation history so the main
|
|
3280
|
+
// agent sees the outcome next turn (mirrors local graph run below).
|
|
3281
|
+
const serverOutput = result?.result || result?.output || '';
|
|
3282
|
+
if (serverOutput && result?.success !== false) {
|
|
3283
|
+
session.history.push(
|
|
3284
|
+
{ role: 'user', content: `[${target}] ${instruction || 'run'}` },
|
|
3285
|
+
{ role: 'assistant', content: String(serverOutput) },
|
|
3286
|
+
);
|
|
3287
|
+
} else if (result?.success === false && result?.output) {
|
|
3288
|
+
session.history.push(
|
|
3289
|
+
{ role: 'user', content: `[${target}] ${instruction || 'run'}` },
|
|
3290
|
+
{ role: 'assistant', content: `Workflow failed: ${String(result.output)}` },
|
|
3291
|
+
);
|
|
3292
|
+
}
|
|
2836
3293
|
|
|
2837
|
-
|
|
2838
|
-
|
|
3294
|
+
const output = serverOutput;
|
|
3295
|
+
if (output) {
|
|
3296
|
+
process.stderr.write('\n');
|
|
3297
|
+
process.stderr.write(renderMarkdown(String(output), { width: process.stderr.columns || 96 }));
|
|
3298
|
+
process.stderr.write('\n');
|
|
3299
|
+
}
|
|
2839
3300
|
return;
|
|
2840
3301
|
}
|
|
2841
3302
|
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
if (result
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
process.stderr.write('\n');
|
|
3303
|
+
// Local graph run. Session-substrate agent runs already push their own
|
|
3304
|
+
// conversation history; feed workflow results (and logged-out direct
|
|
3305
|
+
// runs) back so the main agent sees the outcome next turn.
|
|
3306
|
+
if (result.status === 'failed') {
|
|
3307
|
+
process.stderr.write(` ${c.red('✗')} ${result.output || `'${target}' failed.`}\n`);
|
|
3308
|
+
}
|
|
3309
|
+
const historyCovered = Boolean(runnableAgent) && Boolean(creds.token);
|
|
3310
|
+
if (result.output && result.status !== 'failed' && !historyCovered) {
|
|
3311
|
+
session.history.push(
|
|
3312
|
+
{ role: 'user', content: `[${target}] ${instruction || 'run'}` },
|
|
3313
|
+
{ role: 'assistant', content: String(result.output) },
|
|
3314
|
+
);
|
|
2855
3315
|
}
|
|
2856
3316
|
} catch (err) {
|
|
2857
3317
|
process.stderr.write(` ${c.red('✗')} ${err.message || String(err)}\n`);
|
|
@@ -2859,6 +3319,38 @@ async function handleRunCommand(rest = '', ctx) {
|
|
|
2859
3319
|
}
|
|
2860
3320
|
}
|
|
2861
3321
|
|
|
3322
|
+
// Bridges the graph engine's session substrate onto runAgentDefinition:
|
|
3323
|
+
// the node's events stream through the same renderer, and the run keeps
|
|
3324
|
+
// its existing conversation-history feedback.
|
|
3325
|
+
function makeSessionSubstrate(ctx) {
|
|
3326
|
+
return (agent, node, instruction) => (async function* () {
|
|
3327
|
+
const queue = [];
|
|
3328
|
+
let notify = null;
|
|
3329
|
+
let finished = false;
|
|
3330
|
+
const push = (event) => {
|
|
3331
|
+
queue.push(event);
|
|
3332
|
+
const wake = notify; notify = null;
|
|
3333
|
+
wake?.();
|
|
3334
|
+
};
|
|
3335
|
+
const execContext = await prepareDirectAgentRunContext(ctx, instruction);
|
|
3336
|
+
const done = runAgentDefinition(agent, instruction, ctx, session, push, {
|
|
3337
|
+
cwd: execContext.cwd,
|
|
3338
|
+
execContext,
|
|
3339
|
+
pluginRegistry,
|
|
3340
|
+
}).catch(err => push({ type: 'error', data: { message: err?.message || String(err) } }))
|
|
3341
|
+
.finally(() => {
|
|
3342
|
+
finished = true;
|
|
3343
|
+
const wake = notify; notify = null;
|
|
3344
|
+
wake?.();
|
|
3345
|
+
});
|
|
3346
|
+
while (!finished || queue.length) {
|
|
3347
|
+
if (!queue.length) await new Promise(resolve => { notify = resolve; });
|
|
3348
|
+
while (queue.length) yield queue.shift();
|
|
3349
|
+
}
|
|
3350
|
+
await done;
|
|
3351
|
+
})();
|
|
3352
|
+
}
|
|
3353
|
+
|
|
2862
3354
|
async function handleCommand(input, ctx) {
|
|
2863
3355
|
const { cmd, rest, aliasTarget } = normalizeCommandInput(input);
|
|
2864
3356
|
if (aliasTarget) {
|
|
@@ -3411,7 +3903,8 @@ async function handleCommand(input, ctx) {
|
|
|
3411
3903
|
session.agentHistory.length = 0;
|
|
3412
3904
|
session.toolCalls = 0;
|
|
3413
3905
|
session.subAgentToolCalls = 0;
|
|
3414
|
-
|
|
3906
|
+
session.activeSubAgentRuns = new Map();
|
|
3907
|
+
resetFoldedSubAgentTools();
|
|
3415
3908
|
clearCards();
|
|
3416
3909
|
process.stderr.write(` ${c.gray('Conversation cleared.')}\n`);
|
|
3417
3910
|
return;
|
|
@@ -3720,6 +4213,11 @@ async function handleCommand(input, ctx) {
|
|
|
3720
4213
|
await handleSkillsCommand(rest, ctx);
|
|
3721
4214
|
return;
|
|
3722
4215
|
|
|
4216
|
+
case '/plugin':
|
|
4217
|
+
case '/plugins':
|
|
4218
|
+
await handlePluginsCommand(rest, ctx);
|
|
4219
|
+
return;
|
|
4220
|
+
|
|
3723
4221
|
case '/explore':
|
|
3724
4222
|
case '/review':
|
|
3725
4223
|
case '/architect': {
|
|
@@ -3813,6 +4311,7 @@ export async function startTerminalRepl() {
|
|
|
3813
4311
|
return createToolExecutor({
|
|
3814
4312
|
checkpoints,
|
|
3815
4313
|
hookRunner,
|
|
4314
|
+
pluginRegistry,
|
|
3816
4315
|
interactionHandler: askUserInteraction,
|
|
3817
4316
|
onAutoRegisterStart: shouldShowIndexStatus ? (root) => {
|
|
3818
4317
|
const name = path.basename(root || safeCwd()) || root || 'project';
|
|
@@ -3828,6 +4327,7 @@ export async function startTerminalRepl() {
|
|
|
3828
4327
|
});
|
|
3829
4328
|
}
|
|
3830
4329
|
|
|
4330
|
+
const pluginRegistry = new PluginRegistry().scan();
|
|
3831
4331
|
let toolExecutor = null;
|
|
3832
4332
|
const skipPerms = cliArgs.skipPermissions;
|
|
3833
4333
|
let approval = new ApprovalManager({ autoApprove: skipPerms, cwd: safeCwd(), policy: effectivePolicy.policy });
|
|
@@ -3844,6 +4344,26 @@ export async function startTerminalRepl() {
|
|
|
3844
4344
|
|
|
3845
4345
|
const ctx = { auth, toolExecutor: null, approval, jsonlWriter, sessionMgr, checkpoints, effectivePolicy, latestProjectContext, latestEnvelope, pendingVisionPaths: [] };
|
|
3846
4346
|
|
|
4347
|
+
// Wake-on-finish: background jobs with on_complete dispatch their target
|
|
4348
|
+
// agent through the trigger funnel when they exit. The ctx builder runs
|
|
4349
|
+
// lazily at fire time so it sees the live tool executor.
|
|
4350
|
+
registerJobCompletionDispatch(() => {
|
|
4351
|
+
const creds = ctx.auth?.loadCredentials?.() || {};
|
|
4352
|
+
return {
|
|
4353
|
+
toolExecutor: ctx.toolExecutor,
|
|
4354
|
+
listRunnables: () => ctx.toolExecutor?.listRunnables?.() || [],
|
|
4355
|
+
listLocalWorkflows: () => listLocalWorkflows(safeCwd()),
|
|
4356
|
+
renderEvent,
|
|
4357
|
+
sessionSubstrate: makeSessionSubstrate(ctx),
|
|
4358
|
+
auth: { token: creds.token || null },
|
|
4359
|
+
credentials: {
|
|
4360
|
+
apiKey: process.env.ANTHROPIC_API_KEY || creds.anthropicKey || null,
|
|
4361
|
+
openRouterKey: process.env.OPENROUTER_API_KEY || creds.openRouterKey || null,
|
|
4362
|
+
},
|
|
4363
|
+
cwd: safeCwd(),
|
|
4364
|
+
};
|
|
4365
|
+
});
|
|
4366
|
+
|
|
3847
4367
|
let startupOutputRow = 1;
|
|
3848
4368
|
let startupOutputCol = 1;
|
|
3849
4369
|
|
|
@@ -3897,7 +4417,7 @@ export async function startTerminalRepl() {
|
|
|
3897
4417
|
flushContent();
|
|
3898
4418
|
flushPendingHead();
|
|
3899
4419
|
flushExploreRun();
|
|
3900
|
-
|
|
4420
|
+
resetFoldedSubAgentTools();
|
|
3901
4421
|
clearCards();
|
|
3902
4422
|
|
|
3903
4423
|
const preserved = {
|
|
@@ -3964,6 +4484,7 @@ export async function startTerminalRepl() {
|
|
|
3964
4484
|
lastTurnDuration: 0,
|
|
3965
4485
|
toolCounts: {},
|
|
3966
4486
|
subAgentCounts: {},
|
|
4487
|
+
activeSubAgentRuns: new Map(),
|
|
3967
4488
|
savedUsd: 0,
|
|
3968
4489
|
lastTask: '',
|
|
3969
4490
|
lastReasoning: '',
|
|
@@ -4224,8 +4745,8 @@ export async function startTerminalRepl() {
|
|
|
4224
4745
|
printBanner(auth);
|
|
4225
4746
|
|
|
4226
4747
|
// Preflight diagnostic (PRD-055 §9). Non-blocking; opt-out via
|
|
4227
|
-
//
|
|
4228
|
-
if (process.env.
|
|
4748
|
+
// BAHULAM_NO_PREFLIGHT=1 (used by tests / scripted runs).
|
|
4749
|
+
if (process.env.BAHULAM_NO_PREFLIGHT !== '1' && !cliArgs.skipPermissions) {
|
|
4229
4750
|
try { await runPreflight({ auth, cwd: safeCwd(), version: VERSION }); }
|
|
4230
4751
|
catch { /* preflight is best-effort */ }
|
|
4231
4752
|
}
|
|
@@ -4902,6 +5423,7 @@ export async function startTerminalRepl() {
|
|
|
4902
5423
|
token: creds.token,
|
|
4903
5424
|
toolExecutor,
|
|
4904
5425
|
approvalManager: approval,
|
|
5426
|
+
pluginRegistry,
|
|
4905
5427
|
});
|
|
4906
5428
|
}
|
|
4907
5429
|
const client = streamClient;
|