@compilr-dev/cli 0.7.4 → 0.7.5
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/dist/agent.d.ts +7 -2
- package/dist/agent.js +55 -39
- package/dist/commands-v2/handlers/project.js +65 -2
- package/dist/commands-v2/handlers/settings.js +18 -26
- package/dist/compilr-diff-companion.vsix +0 -0
- package/dist/db/schema.d.ts +1 -1
- package/dist/episodes/index.d.ts +13 -13
- package/dist/episodes/index.js +13 -14
- package/dist/guide/cli-guide-entries.js +54 -2
- package/dist/handlers/delegation-handlers.js +228 -50
- package/dist/handlers/interactive-flow-handlers.d.ts +26 -0
- package/dist/handlers/interactive-flow-handlers.js +61 -0
- package/dist/handlers/propose-alternatives-handlers.d.ts +36 -0
- package/dist/handlers/propose-alternatives-handlers.js +65 -0
- package/dist/index.js +13 -2
- package/dist/repl-v2.d.ts +66 -0
- package/dist/repl-v2.js +382 -53
- package/dist/shared-handlers.d.ts +57 -5
- package/dist/shared-handlers.js +50 -0
- package/dist/tools/consult.d.ts +14 -0
- package/dist/tools/consult.js +73 -0
- package/dist/tools/delegate.d.ts +12 -6
- package/dist/tools/delegate.js +35 -19
- package/dist/tools/interactive-flow.d.ts +13 -0
- package/dist/tools/interactive-flow.js +19 -0
- package/dist/tools/platform-adapter.d.ts +14 -2
- package/dist/tools/platform-adapter.js +16 -4
- package/dist/tools/propose-alternatives.d.ts +13 -0
- package/dist/tools/propose-alternatives.js +19 -0
- package/dist/tools.d.ts +22 -98
- package/dist/tools.js +27 -382
- package/dist/ui/markdown-renderer.js +26 -7
- package/dist/ui/overlay/data/tutorials/projects/new-project.js +56 -20
- package/dist/ui/overlay/impl/interactive-flow-overlay-v2.d.ts +79 -0
- package/dist/ui/overlay/impl/interactive-flow-overlay-v2.js +580 -0
- package/dist/ui/overlay/impl/new-overlay-v2.d.ts +32 -0
- package/dist/ui/overlay/impl/new-overlay-v2.js +305 -66
- package/dist/ui/overlay/impl/propose-alternatives-overlay-v2.d.ts +53 -0
- package/dist/ui/overlay/impl/propose-alternatives-overlay-v2.js +326 -0
- package/dist/ui/overlay/index.d.ts +2 -0
- package/dist/ui/overlay/index.js +2 -0
- package/dist/ui/tool-formatters.js +61 -3
- package/package.json +2 -2
package/dist/repl-v2.js
CHANGED
|
@@ -20,7 +20,7 @@ import { initSessionRegistry, getSessionRegistry, setActiveTerminalSessionId, ge
|
|
|
20
20
|
import { initFileLockManager } from './multi-agent/file-locks.js';
|
|
21
21
|
import { initNotificationManager, getNotificationManager } from './multi-agent/notification-manager.js';
|
|
22
22
|
import { getSessionsPath } from './settings/paths.js';
|
|
23
|
-
import {
|
|
23
|
+
import { todoStore } from './tools.js';
|
|
24
24
|
import { getCurrentProject } from './tools/project-db.js';
|
|
25
25
|
import { TerminalUI } from './ui/terminal-ui.js';
|
|
26
26
|
import * as terminal from './ui/terminal.js';
|
|
@@ -31,7 +31,8 @@ import { renderMascotWithLogo } from './ui/mascot/renderer.js';
|
|
|
31
31
|
import { getStartupHighlights } from './changelog/index.js';
|
|
32
32
|
import { registerCommands, executeCommand, allCommands, getAutocompleteCommands, setSkillCommandLoader, saveCurrentSession, saveCurrentTeam, loadProjectSession, archiveCurrentSession, convertMessagesToItems, } from './commands-v2/index.js';
|
|
33
33
|
import { getCustomCommandRegistry } from './commands/custom-registry.js';
|
|
34
|
-
import { getPlanModePrompt } from '@compilr-dev/sdk';
|
|
34
|
+
import { getPlanModePrompt, createHandoffTool, createConsultTool, createDelegateTool, createDelegateBackgroundTool, } from '@compilr-dev/sdk';
|
|
35
|
+
import { getHandoffHandler, getConsultHandler, getDelegateHandler, getDelegateBackgroundHandler, } from './shared-handlers.js';
|
|
35
36
|
import { planRepository } from './db/repositories/index.js';
|
|
36
37
|
import { PlanApprovalOverlayV2, } from './ui/overlay/impl/plan-approval-overlay-v2.js';
|
|
37
38
|
import { PermissionOverlayV2, } from './ui/overlay/impl/permission-overlay-v2.js';
|
|
@@ -40,6 +41,8 @@ import { truncate } from './ui/base/index.js';
|
|
|
40
41
|
import { AskUserSimpleOverlayV2, } from './ui/overlay/impl/ask-user-simple-overlay-v2.js';
|
|
41
42
|
import { GuardrailOverlayV2, } from './ui/overlay/impl/guardrail-overlay-v2.js';
|
|
42
43
|
import { AskUserOverlayV2, } from './ui/overlay/impl/ask-user-overlay-v2.js';
|
|
44
|
+
import { ProposeAlternativesOverlayV2, } from './ui/overlay/impl/propose-alternatives-overlay-v2.js';
|
|
45
|
+
import { InteractiveFlowOverlayV2, } from './ui/overlay/impl/interactive-flow-overlay-v2.js';
|
|
43
46
|
import { IterationLimitOverlayV2, } from './ui/overlay/impl/iteration-limit-overlay-v2.js';
|
|
44
47
|
import { OnboardingWizardOverlayV2, } from './ui/overlay/impl/onboarding-wizard-overlay-v2.js';
|
|
45
48
|
import { LoginOverlayV2, } from './ui/overlay/impl/login-overlay-v2.js';
|
|
@@ -68,9 +71,10 @@ const MAX_QUEUE_SIZE = 10;
|
|
|
68
71
|
const DEBUG_LOG_FILE = '/tmp/compilr-agent-debug.log';
|
|
69
72
|
const DEBUG_ENABLED = process.env.DEBUG_AGENT_REQUESTS === '1';
|
|
70
73
|
let debugRequestCount = 0;
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
// Debug logging fetches tool definitions live from the agent (the SDK's
|
|
75
|
+
// internal meta-registry is fed via the agent's capability config and
|
|
76
|
+
// isn't separately accessible from the CLI; the CLI's old parallel
|
|
77
|
+
// `MetaToolsRegistry` was removed in audit TODO-15).
|
|
74
78
|
/**
|
|
75
79
|
* Write debug info to log file before agent request.
|
|
76
80
|
* Enable with: DEBUG_AGENT_REQUESTS=1 compilr
|
|
@@ -87,26 +91,18 @@ function debugLogAgentRequest(agent, message) {
|
|
|
87
91
|
const systemPromptTokens = estimateTokens(state.systemPrompt);
|
|
88
92
|
const messagesJson = JSON.stringify(state.messages);
|
|
89
93
|
const messagesTokens = estimateTokens(messagesJson);
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
inputSchema: t.definition.inputSchema,
|
|
100
|
-
})));
|
|
101
|
-
const metaToolsJson = JSON.stringify(metaTools.map(t => ({
|
|
102
|
-
name: t.definition.name,
|
|
103
|
-
description: t.definition.description,
|
|
104
|
-
inputSchema: t.definition.inputSchema,
|
|
94
|
+
// Tool definitions come live from the agent. The SDK's internal
|
|
95
|
+
// meta-registry isn't separately accessible — its tools are routed
|
|
96
|
+
// transparently via use_tool() and `get_tool_info` already lives in
|
|
97
|
+
// the agent's direct list (so it's counted here).
|
|
98
|
+
const directTools = agent.getToolDefinitions();
|
|
99
|
+
const directToolsJson = JSON.stringify(directTools.map(d => ({
|
|
100
|
+
name: d.name,
|
|
101
|
+
description: d.description,
|
|
102
|
+
inputSchema: d.inputSchema,
|
|
105
103
|
})));
|
|
106
104
|
const directToolsTokens = estimateTokens(directToolsJson);
|
|
107
|
-
const
|
|
108
|
-
const totalToolTokens = directToolsTokens + metaToolsTokens;
|
|
109
|
-
const totalEstimate = systemPromptTokens + messagesTokens + totalToolTokens;
|
|
105
|
+
const totalEstimate = systemPromptTokens + messagesTokens + directToolsTokens;
|
|
110
106
|
const logEntry = `
|
|
111
107
|
################################################################################
|
|
112
108
|
# REQUEST #${String(debugRequestCount)} @ ${timestamp}
|
|
@@ -120,13 +116,9 @@ CONTEXT SIZES:
|
|
|
120
116
|
- Messages: ${messagesJson.length.toLocaleString()} chars (~${messagesTokens.toLocaleString()} tokens)
|
|
121
117
|
- Message Count: ${String(state.messages.length)}
|
|
122
118
|
- Direct Tools: ${directToolsJson.length.toLocaleString()} chars (~${directToolsTokens.toLocaleString()} tokens) [${String(directTools.length)} tools]
|
|
123
|
-
- Meta Tools: ${metaToolsJson.length.toLocaleString()} chars (~${metaToolsTokens.toLocaleString()} tokens) [${String(metaTools.length)} tools]
|
|
124
|
-
- Meta Registry: ${String(metaRegistryTools.length)} tools (accessed via use_tool)
|
|
125
119
|
--------------------------------------------------------------------------------
|
|
126
120
|
ESTIMATED TOTAL: ~${totalEstimate.toLocaleString()} tokens
|
|
127
|
-
(prompt: ${systemPromptTokens.toLocaleString()} + messages: ${messagesTokens.toLocaleString()} + tools: ${
|
|
128
|
-
|
|
129
|
-
Tool Stats: Direct=${String(toolStats.directTools)}, MetaRegistry=${String(toolStats.metaRegistryTools)}, Savings=~${String(toolStats.tokenSavings)}
|
|
121
|
+
(prompt: ${systemPromptTokens.toLocaleString()} + messages: ${messagesTokens.toLocaleString()} + tools: ${directToolsTokens.toLocaleString()})
|
|
130
122
|
|
|
131
123
|
================================================================================
|
|
132
124
|
FULL SYSTEM PROMPT (${systemPromptChars.toLocaleString()} chars):
|
|
@@ -136,17 +128,7 @@ ${state.systemPrompt}
|
|
|
136
128
|
================================================================================
|
|
137
129
|
DIRECT TOOLS (${String(directTools.length)} tools, ~${directToolsTokens.toLocaleString()} tokens):
|
|
138
130
|
================================================================================
|
|
139
|
-
${directTools.map(
|
|
140
|
-
|
|
141
|
-
================================================================================
|
|
142
|
-
META TOOLS (${String(metaTools.length)} tools, ~${metaToolsTokens.toLocaleString()} tokens):
|
|
143
|
-
================================================================================
|
|
144
|
-
${metaTools.map(t => `- ${t.definition.name}: ${t.definition.description.slice(0, 100)}...`).join('\n')}
|
|
145
|
-
|
|
146
|
-
================================================================================
|
|
147
|
-
META REGISTRY TOOLS (${String(metaRegistryTools.length)} tools, accessed via use_tool):
|
|
148
|
-
================================================================================
|
|
149
|
-
${metaRegistryTools.map(t => `- ${t.definition.name}`).join('\n')}
|
|
131
|
+
${directTools.map(d => `- ${d.name}: ${d.description.slice(0, 100)}...`).join('\n')}
|
|
150
132
|
|
|
151
133
|
================================================================================
|
|
152
134
|
MESSAGES HISTORY (${String(state.messages.length)} messages):
|
|
@@ -156,7 +138,7 @@ ${JSON.stringify(state.messages, null, 2)}
|
|
|
156
138
|
`;
|
|
157
139
|
appendFileSync(DEBUG_LOG_FILE, logEntry);
|
|
158
140
|
log.debug({ component: 'repl', requestNum: debugRequestCount, file: DEBUG_LOG_FILE }, 'Debug request logged');
|
|
159
|
-
log.debug({ component: 'repl', systemPromptTokens, messagesTokens,
|
|
141
|
+
log.debug({ component: 'repl', systemPromptTokens, messagesTokens, directToolsTokens, totalEstimate }, 'Token estimate');
|
|
160
142
|
}
|
|
161
143
|
catch (error) {
|
|
162
144
|
log.error({ component: 'repl', err: error }, 'Failed to log agent request');
|
|
@@ -187,6 +169,8 @@ export class ReplV2 {
|
|
|
187
169
|
onAskUserSimpleReady;
|
|
188
170
|
onGuardrailReady;
|
|
189
171
|
onAskUserReady;
|
|
172
|
+
onProposeAlternativesReady;
|
|
173
|
+
onInteractiveFlowReady;
|
|
190
174
|
onIterationLimitReady;
|
|
191
175
|
onAgentFinish;
|
|
192
176
|
onSuggestionReady;
|
|
@@ -222,6 +206,11 @@ export class ReplV2 {
|
|
|
222
206
|
// Bash abort controllers for Ctrl+B backgrounding feature
|
|
223
207
|
// Maps toolUseId -> AbortController for each running bash command
|
|
224
208
|
bashAbortControllers = new Map();
|
|
209
|
+
// Dedupe set for specialist multi-agent tool registration (handoff/consult).
|
|
210
|
+
// Keyed by agentId — once we've registered these tools on a specialist
|
|
211
|
+
// agent's underlying Agent instance we don't do it again on subsequent
|
|
212
|
+
// switches. Mirrors Desktop's `delegateToolsRegistered` set.
|
|
213
|
+
specialistToolsRegistered = new Set();
|
|
225
214
|
constructor(options = {}) {
|
|
226
215
|
this.agent = options.agent;
|
|
227
216
|
this.team = options.team;
|
|
@@ -236,6 +225,8 @@ export class ReplV2 {
|
|
|
236
225
|
this.onAskUserSimpleReady = options.onAskUserSimpleReady;
|
|
237
226
|
this.onGuardrailReady = options.onGuardrailReady;
|
|
238
227
|
this.onAskUserReady = options.onAskUserReady;
|
|
228
|
+
this.onProposeAlternativesReady = options.onProposeAlternativesReady;
|
|
229
|
+
this.onInteractiveFlowReady = options.onInteractiveFlowReady;
|
|
239
230
|
this.onIterationLimitReady = options.onIterationLimitReady;
|
|
240
231
|
this.onAgentFinish = options.onAgentFinish;
|
|
241
232
|
this.onSuggestionReady = options.onSuggestionReady;
|
|
@@ -485,8 +476,16 @@ export class ReplV2 {
|
|
|
485
476
|
if (teamAgent.agent) {
|
|
486
477
|
this.agent = teamAgent.agent;
|
|
487
478
|
}
|
|
488
|
-
//
|
|
489
|
-
|
|
479
|
+
// 3b. Register multi-agent tools (consult always, handoff for
|
|
480
|
+
// specialists) using SDK factories. Coordinator path also fires here
|
|
481
|
+
// when a user explicitly switches to $default; the startup path
|
|
482
|
+
// covers it via restoreTeamForPrefix.
|
|
483
|
+
this.registerMultiAgentTools(teamAgent);
|
|
484
|
+
// Tool filtering for the new agent is already in effect — each team
|
|
485
|
+
// agent gets its own capability profile baked in at factory time via
|
|
486
|
+
// `agent.ts:467` (`capabilities.profile`). No runtime filter change
|
|
487
|
+
// needed here (audit TODO-15 removed the dead `setMetaToolFilter`
|
|
488
|
+
// call that used to live at this spot).
|
|
490
489
|
// 5. Update footer UI
|
|
491
490
|
if (agentId !== 'default') {
|
|
492
491
|
this.ui.setActiveTeamAgent({
|
|
@@ -550,6 +549,171 @@ export class ReplV2 {
|
|
|
550
549
|
demotedAgentId,
|
|
551
550
|
};
|
|
552
551
|
}
|
|
552
|
+
/**
|
|
553
|
+
* Register multi-agent tools (consult always, handoff for specialists)
|
|
554
|
+
* on the agent's underlying Agent using SDK factories.
|
|
555
|
+
*
|
|
556
|
+
* Tool grants:
|
|
557
|
+
* - **consult**: everyone (coordinator AND specialists). Lightweight
|
|
558
|
+
* "ask another agent a focused question" tool. Originally specced as
|
|
559
|
+
* specialist-only, but empirical evidence (session 220) showed the
|
|
560
|
+
* coordinator's LLM grabs the wrong tool (e.g. `suggest`) when the
|
|
561
|
+
* user says "consult $pm" because no tool by that name was in its
|
|
562
|
+
* registry. delegate is too heavyweight for a quick question.
|
|
563
|
+
* - **handoff**: specialists only. Coordinators delegate, they don't
|
|
564
|
+
* pass the baton. $default → specialist already has delegate.
|
|
565
|
+
* - **delegate / delegate_background**: coordinator only, via the
|
|
566
|
+
* existing CLI tool-array path. Not touched here.
|
|
567
|
+
*
|
|
568
|
+
* Why lazy: the SDK factories need `team` + `currentAgentId` at
|
|
569
|
+
* construction time. The CLI's tool array is built at module load
|
|
570
|
+
* when neither is known.
|
|
571
|
+
*
|
|
572
|
+
* Dedupe via `specialistToolsRegistered` set — tools are registered
|
|
573
|
+
* once per agent for the lifetime of the REPL. Mirrors Desktop's
|
|
574
|
+
* pattern in `agent-manager.ts:registerDelegateToolsIfNeeded`.
|
|
575
|
+
*
|
|
576
|
+
* Called from:
|
|
577
|
+
* - `switchAgent` (every agent switch — covers specialists).
|
|
578
|
+
* - `restoreTeamForPrefix` (team restoration — covers $default at
|
|
579
|
+
* startup, since the coordinator is never switchTo'd from outside).
|
|
580
|
+
*/
|
|
581
|
+
/**
|
|
582
|
+
* Called whenever `this.team` is reassigned (project switch, team
|
|
583
|
+
* restoration). The SDK's createConsultTool factory captures the `team`
|
|
584
|
+
* argument in its closure at construction time, so a tool registered
|
|
585
|
+
* against the old team has a stale roster view. We must:
|
|
586
|
+
* 1. Unregister the stale consult/handoff tools from $default's
|
|
587
|
+
* registry (Agent.registerTools throws on duplicate-name, it
|
|
588
|
+
* does not replace).
|
|
589
|
+
* 2. Re-register fresh tools bound to the new team.
|
|
590
|
+
*
|
|
591
|
+
* $default's underlying Agent is preserved across team swaps (the new
|
|
592
|
+
* team is set as default via setDefaultAgent), so its tool registry
|
|
593
|
+
* accumulates stale tools. Specialists' Agents are fresh after a swap,
|
|
594
|
+
* so they don't have stale tools to clean up — clearing the dedupe set
|
|
595
|
+
* is enough; they'll re-register on demand via switchAgent.
|
|
596
|
+
*
|
|
597
|
+
* Without this, `consult($pm)` from $default reports "no specialists
|
|
598
|
+
* available" because the closure references the fresh-startup team
|
|
599
|
+
* (which only had $default) instead of the restored/built team.
|
|
600
|
+
*/
|
|
601
|
+
rebindMultiAgentToolsAfterTeamSwap() {
|
|
602
|
+
this.specialistToolsRegistered.clear();
|
|
603
|
+
const defaultTeamAgent = this.team?.get('default');
|
|
604
|
+
if (defaultTeamAgent?.agent) {
|
|
605
|
+
// Cast to access the unregister method — it's on
|
|
606
|
+
// DefaultToolRegistry but not on the public ToolRegistry interface.
|
|
607
|
+
const registry = defaultTeamAgent.agent.getToolRegistry();
|
|
608
|
+
// Unregister every tool registerMultiAgentTools may have added to
|
|
609
|
+
// the coordinator on a previous call. Re-registration would fail
|
|
610
|
+
// with "Tool already registered" otherwise.
|
|
611
|
+
registry.unregister?.('consult');
|
|
612
|
+
registry.unregister?.('handoff');
|
|
613
|
+
registry.unregister?.('delegate');
|
|
614
|
+
registry.unregister?.('delegate_background');
|
|
615
|
+
this.registerMultiAgentTools(defaultTeamAgent);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
registerMultiAgentTools(teamAgent) {
|
|
619
|
+
if (!teamAgent.agent)
|
|
620
|
+
return;
|
|
621
|
+
if (!this.team)
|
|
622
|
+
return;
|
|
623
|
+
if (this.specialistToolsRegistered.has(teamAgent.id))
|
|
624
|
+
return;
|
|
625
|
+
const team = this.team;
|
|
626
|
+
const currentAgentId = teamAgent.id;
|
|
627
|
+
const tools = [];
|
|
628
|
+
// Consult — available to everyone.
|
|
629
|
+
tools.push(createConsultTool({
|
|
630
|
+
team,
|
|
631
|
+
currentAgentId,
|
|
632
|
+
onConsult: async (targetId, question, context) => {
|
|
633
|
+
const handler = getConsultHandler();
|
|
634
|
+
if (!handler) {
|
|
635
|
+
return {
|
|
636
|
+
answer: '',
|
|
637
|
+
error: 'Consult handler not initialised. This shouldn\'t happen in normal use.',
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
const result = await handler({ agentId: targetId, question, context });
|
|
641
|
+
return { answer: result.answer, error: result.error };
|
|
642
|
+
},
|
|
643
|
+
}));
|
|
644
|
+
// Handoff — specialists only (per multi-agent-tools-taxonomy-spec.md §2:
|
|
645
|
+
// handoff is the specialist's vocabulary for passing the baton).
|
|
646
|
+
if (teamAgent.id !== 'default') {
|
|
647
|
+
tools.push(createHandoffTool({
|
|
648
|
+
team,
|
|
649
|
+
currentAgentId,
|
|
650
|
+
onHandoff: async (targetId, task, reason) => {
|
|
651
|
+
const handler = getHandoffHandler();
|
|
652
|
+
if (!handler) {
|
|
653
|
+
return {
|
|
654
|
+
success: false,
|
|
655
|
+
error: 'Handoff handler not initialised. This shouldn\'t happen in normal use.',
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
const result = await handler({ agentId: targetId, task, reason });
|
|
659
|
+
return {
|
|
660
|
+
success: result.success,
|
|
661
|
+
declined: result.declined,
|
|
662
|
+
error: result.error,
|
|
663
|
+
};
|
|
664
|
+
},
|
|
665
|
+
}));
|
|
666
|
+
}
|
|
667
|
+
// Delegate + delegate_background — coordinator only (per taxonomy spec
|
|
668
|
+
// §2: coordinators push work down, specialists pass the baton). Built
|
|
669
|
+
// via the SDK factories (same pattern as consult/handoff above) and
|
|
670
|
+
// bridged to the existing shared-handlers registry where the actual
|
|
671
|
+
// approval-overlay + specialist-run code lives.
|
|
672
|
+
if (teamAgent.id === 'default') {
|
|
673
|
+
tools.push(createDelegateTool({
|
|
674
|
+
team,
|
|
675
|
+
currentAgentId,
|
|
676
|
+
onDelegate: async (targetId, task, reason) => {
|
|
677
|
+
const handler = getDelegateHandler();
|
|
678
|
+
if (!handler) {
|
|
679
|
+
return {
|
|
680
|
+
success: false,
|
|
681
|
+
error: "Delegate handler not initialised. This shouldn't happen in normal use.",
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
const result = await handler({ agentId: targetId, task, reason });
|
|
685
|
+
return {
|
|
686
|
+
success: result.success,
|
|
687
|
+
declined: result.declined,
|
|
688
|
+
response: result.response,
|
|
689
|
+
error: result.error,
|
|
690
|
+
};
|
|
691
|
+
},
|
|
692
|
+
}));
|
|
693
|
+
tools.push(createDelegateBackgroundTool({
|
|
694
|
+
team,
|
|
695
|
+
currentAgentId,
|
|
696
|
+
onDelegateBackground: async (args) => {
|
|
697
|
+
const handler = getDelegateBackgroundHandler();
|
|
698
|
+
if (!handler) {
|
|
699
|
+
return {
|
|
700
|
+
delegationId: '',
|
|
701
|
+
status: 'running',
|
|
702
|
+
error: "delegate_background handler not initialised. This shouldn't happen in normal use.",
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
return await handler({
|
|
706
|
+
agentId: args.targetAgentId,
|
|
707
|
+
task: args.task,
|
|
708
|
+
todoIndex: args.todoIndex,
|
|
709
|
+
expectedOutput: args.expectedOutput,
|
|
710
|
+
});
|
|
711
|
+
},
|
|
712
|
+
}));
|
|
713
|
+
}
|
|
714
|
+
teamAgent.agent.registerTools(tools);
|
|
715
|
+
this.specialistToolsRegistered.add(teamAgent.id);
|
|
716
|
+
}
|
|
553
717
|
/**
|
|
554
718
|
* Bring a background agent to foreground.
|
|
555
719
|
* Validates the agent has a background session, then delegates to switchAgent().
|
|
@@ -798,6 +962,16 @@ export class ReplV2 {
|
|
|
798
962
|
displayName: a.displayName,
|
|
799
963
|
mascot: a.mascot,
|
|
800
964
|
})));
|
|
965
|
+
// Register multi-agent tools on $default so it has consult available
|
|
966
|
+
// from the first message — covers brand-new projects where the team
|
|
967
|
+
// has only $default and restoreTeamForPrefix early-returns before
|
|
968
|
+
// hitting its registration block (size <= 1 check).
|
|
969
|
+
// Dedupe via specialistToolsRegistered set, so this is safe to call
|
|
970
|
+
// alongside the restoreTeamForPrefix path.
|
|
971
|
+
const defaultTeamAgent = this.team.get('default');
|
|
972
|
+
if (defaultTeamAgent) {
|
|
973
|
+
this.registerMultiAgentTools(defaultTeamAgent);
|
|
974
|
+
}
|
|
801
975
|
}
|
|
802
976
|
}
|
|
803
977
|
catch {
|
|
@@ -885,6 +1059,37 @@ export class ReplV2 {
|
|
|
885
1059
|
return result ?? { answers: {}, skipped: options.questions.map((q) => q.id) };
|
|
886
1060
|
});
|
|
887
1061
|
}
|
|
1062
|
+
// Provide the propose_alternatives overlay function to the caller
|
|
1063
|
+
if (this.onProposeAlternativesReady) {
|
|
1064
|
+
const ui = this.ui;
|
|
1065
|
+
this.onProposeAlternativesReady(async (options) => {
|
|
1066
|
+
const overlay = new ProposeAlternativesOverlayV2(options);
|
|
1067
|
+
const result = await ui.showOverlay(overlay);
|
|
1068
|
+
// No-overlay-result (Esc to dismiss overlay outside our key handler)
|
|
1069
|
+
// → treat as rejected so the agent gets a defined shape.
|
|
1070
|
+
return (result ?? {
|
|
1071
|
+
chosenIndex: 0,
|
|
1072
|
+
chosenTitle: options.alternatives[0]?.title ?? '',
|
|
1073
|
+
rejected: true,
|
|
1074
|
+
});
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
// Provide the build_interactive_flow overlay function to the caller
|
|
1078
|
+
if (this.onInteractiveFlowReady) {
|
|
1079
|
+
const ui = this.ui;
|
|
1080
|
+
this.onInteractiveFlowReady(async (options) => {
|
|
1081
|
+
const overlay = new InteractiveFlowOverlayV2(options);
|
|
1082
|
+
const result = await ui.showOverlay(overlay);
|
|
1083
|
+
return (result ?? {
|
|
1084
|
+
completed: false,
|
|
1085
|
+
path: [],
|
|
1086
|
+
answers: {},
|
|
1087
|
+
answerLabels: {},
|
|
1088
|
+
abortReason: 'unknown',
|
|
1089
|
+
durationMs: 0,
|
|
1090
|
+
});
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
888
1093
|
// Provide the iteration_limit overlay function to the caller
|
|
889
1094
|
if (this.onIterationLimitReady) {
|
|
890
1095
|
const ui = this.ui;
|
|
@@ -1218,6 +1423,26 @@ export class ReplV2 {
|
|
|
1218
1423
|
scope: 'session',
|
|
1219
1424
|
});
|
|
1220
1425
|
}
|
|
1426
|
+
// Rebind multi-agent tools to the freshly restored team. Without this,
|
|
1427
|
+
// any consult tool previously registered on $default (in start()) is
|
|
1428
|
+
// bound to the pre-restoration team and reports a stale roster.
|
|
1429
|
+
this.rebindMultiAgentToolsAfterTeamSwap();
|
|
1430
|
+
// Update the footer to show the restored active agent's badge.
|
|
1431
|
+
// start() set the footer to the pre-restore default during its sync
|
|
1432
|
+
// path (line ~1437) — handleStartupSequence then ran asynchronously
|
|
1433
|
+
// and got here. If the team was saved with a specialist active, the
|
|
1434
|
+
// footer would otherwise still show "no specialist active" (blank).
|
|
1435
|
+
const restoredActive = persistedTeam.getActive();
|
|
1436
|
+
if (restoredActive.id !== 'default') {
|
|
1437
|
+
this.ui.setActiveTeamAgent({
|
|
1438
|
+
id: restoredActive.id,
|
|
1439
|
+
displayName: restoredActive.displayName,
|
|
1440
|
+
mascot: restoredActive.mascot,
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
else {
|
|
1444
|
+
this.ui.setActiveTeamAgent(null);
|
|
1445
|
+
}
|
|
1221
1446
|
// Sync roster to session registry
|
|
1222
1447
|
const registry = getSessionRegistry();
|
|
1223
1448
|
if (registry) {
|
|
@@ -1745,6 +1970,9 @@ export class ReplV2 {
|
|
|
1745
1970
|
mascot: agent.mascot,
|
|
1746
1971
|
}));
|
|
1747
1972
|
});
|
|
1973
|
+
// Rebind multi-agent tools so consult/handoff closures point at
|
|
1974
|
+
// the new team's roster instead of the pre-swap team.
|
|
1975
|
+
this.rebindMultiAgentToolsAfterTeamSwap();
|
|
1748
1976
|
return oldTeam;
|
|
1749
1977
|
},
|
|
1750
1978
|
restoreAgent: (newAgent) => {
|
|
@@ -2312,6 +2540,13 @@ export class ReplV2 {
|
|
|
2312
2540
|
this.textAccumulator = '';
|
|
2313
2541
|
let lastToolInput = null;
|
|
2314
2542
|
let pendingDiffLines;
|
|
2543
|
+
// Track what was actually rendered to the user this turn. Some models
|
|
2544
|
+
// (Gemini Flash Lite, occasionally) end a turn with zero text output,
|
|
2545
|
+
// which looks like a hang/failure on the terminal. We surface a
|
|
2546
|
+
// diagnostic in that case so the user knows the turn completed even
|
|
2547
|
+
// though nothing was said.
|
|
2548
|
+
let hasRenderedAgentText = false;
|
|
2549
|
+
let hasToolCalls = false;
|
|
2315
2550
|
// Stream agent events (agent is guaranteed to exist in this method)
|
|
2316
2551
|
const agent = this.agent;
|
|
2317
2552
|
if (!agent)
|
|
@@ -2411,9 +2646,11 @@ export class ReplV2 {
|
|
|
2411
2646
|
agentId: activeAgentId,
|
|
2412
2647
|
});
|
|
2413
2648
|
this.textAccumulator = '';
|
|
2649
|
+
hasRenderedAgentText = true;
|
|
2414
2650
|
}
|
|
2415
2651
|
// Track tool calls for per-turn metrics
|
|
2416
2652
|
this.ui.incrementToolCalls();
|
|
2653
|
+
hasToolCalls = true;
|
|
2417
2654
|
// Record tool usage for telemetry
|
|
2418
2655
|
if (event.name) {
|
|
2419
2656
|
getAuthManager().recordTool(event.name);
|
|
@@ -2455,7 +2692,7 @@ export class ReplV2 {
|
|
|
2455
2692
|
this.ui.addBashCommand(event.toolUseId, command);
|
|
2456
2693
|
}
|
|
2457
2694
|
// Update spinner with tool name (skip silent tools)
|
|
2458
|
-
if (!this.agent?.isToolSilent(event.name)
|
|
2695
|
+
if (!this.agent?.isToolSilent(event.name)) {
|
|
2459
2696
|
if (event.name === 'task' && event.toolUseId) {
|
|
2460
2697
|
const subagentType = typeof lastToolInput.subagent_type === 'string'
|
|
2461
2698
|
? lastToolInput.subagent_type
|
|
@@ -2532,10 +2769,12 @@ export class ReplV2 {
|
|
|
2532
2769
|
this.ui.setCurrentTool(null);
|
|
2533
2770
|
continue;
|
|
2534
2771
|
}
|
|
2535
|
-
// Handle todo_write
|
|
2536
|
-
//
|
|
2537
|
-
//
|
|
2538
|
-
|
|
2772
|
+
// Handle todo_write / todo_claim / todo_handoff — all three
|
|
2773
|
+
// mutate the same store, so the UI's todos snapshot needs to
|
|
2774
|
+
// refresh on any of them. Previously only todo_write did this,
|
|
2775
|
+
// so the owner badge (@agent-id) silently never appeared
|
|
2776
|
+
// after a claim/handoff even though the renderer supports it.
|
|
2777
|
+
if (toolName === 'todo_write' || toolName === 'todo_claim' || toolName === 'todo_handoff') {
|
|
2539
2778
|
// Get todos in creation order (getAll() sorts by status which breaks
|
|
2540
2779
|
// task numbering — the footer renderer handles status sorting itself)
|
|
2541
2780
|
const storeTodos = todoStore.getAll()
|
|
@@ -2560,9 +2799,10 @@ export class ReplV2 {
|
|
|
2560
2799
|
this.ui.setCurrentTool(null);
|
|
2561
2800
|
continue;
|
|
2562
2801
|
}
|
|
2563
|
-
// Skip silent tools (no output needed)
|
|
2564
|
-
//
|
|
2565
|
-
|
|
2802
|
+
// Skip silent tools (no output needed). The agent's
|
|
2803
|
+
// tool registry is the single source of truth post-TODO-15
|
|
2804
|
+
// (the CLI's parallel MetaToolsRegistry was dead).
|
|
2805
|
+
if (this.agent?.isToolSilent(toolName)) {
|
|
2566
2806
|
this.ui.setCurrentTool(null);
|
|
2567
2807
|
continue;
|
|
2568
2808
|
}
|
|
@@ -2611,7 +2851,7 @@ export class ReplV2 {
|
|
|
2611
2851
|
this.ui.print({
|
|
2612
2852
|
type: 'tool-result',
|
|
2613
2853
|
name: innerToolName,
|
|
2614
|
-
params: this.formatToolParams(innerArgs),
|
|
2854
|
+
params: this.formatToolParams(innerArgs, innerToolName),
|
|
2615
2855
|
summary,
|
|
2616
2856
|
content: content || undefined,
|
|
2617
2857
|
diffLines: innerDiffLines,
|
|
@@ -2686,7 +2926,7 @@ export class ReplV2 {
|
|
|
2686
2926
|
this.ui.print({
|
|
2687
2927
|
type: 'tool-result',
|
|
2688
2928
|
name: toolName,
|
|
2689
|
-
params: this.formatToolParams(toolInput),
|
|
2929
|
+
params: this.formatToolParams(toolInput, toolName),
|
|
2690
2930
|
summary,
|
|
2691
2931
|
content: content || undefined,
|
|
2692
2932
|
diffLines,
|
|
@@ -2763,6 +3003,21 @@ export class ReplV2 {
|
|
|
2763
3003
|
// Flush remaining text (PLAN_READY marker detection removed — plan_submit tool handles approval)
|
|
2764
3004
|
if (this.textAccumulator.trim()) {
|
|
2765
3005
|
this.ui.print({ type: 'agent-text', text: this.textAccumulator.trim(), expression: this.getActiveAgentMascot(), agentId: activeAgentId });
|
|
3006
|
+
hasRenderedAgentText = true;
|
|
3007
|
+
}
|
|
3008
|
+
// Diagnostic for silent turns — some models occasionally finish a
|
|
3009
|
+
// turn without emitting any user-facing text. Without this, the
|
|
3010
|
+
// terminal just shows the metrics line under the user's input
|
|
3011
|
+
// (e.g. `── 1s · ↓ 20.4k ↑ 15 ──`) and the user is left guessing
|
|
3012
|
+
// whether the agent crashed, is still thinking, or just decided
|
|
3013
|
+
// not to respond. Surface a clear status line instead.
|
|
3014
|
+
if (!hasRenderedAgentText && !signal.aborted) {
|
|
3015
|
+
this.ui.print({
|
|
3016
|
+
type: 'info',
|
|
3017
|
+
message: hasToolCalls
|
|
3018
|
+
? '(no follow-up text after tool calls — the tool result above is the response)'
|
|
3019
|
+
: '(silent turn — the model produced no output. Re-ask, or try a different model.)',
|
|
3020
|
+
});
|
|
2766
3021
|
}
|
|
2767
3022
|
// Print per-turn summary before stopping agent
|
|
2768
3023
|
this.printTurnSummary();
|
|
@@ -3163,7 +3418,7 @@ export class ReplV2 {
|
|
|
3163
3418
|
// Silently fail - session/team saving is best-effort
|
|
3164
3419
|
}
|
|
3165
3420
|
}
|
|
3166
|
-
formatToolParams(input) {
|
|
3421
|
+
formatToolParams(input, toolName) {
|
|
3167
3422
|
if (!input)
|
|
3168
3423
|
return '';
|
|
3169
3424
|
// Format common params
|
|
@@ -3175,6 +3430,80 @@ export class ReplV2 {
|
|
|
3175
3430
|
return String(input.pattern);
|
|
3176
3431
|
if ('command' in input)
|
|
3177
3432
|
return truncate(String(input.command), 50);
|
|
3433
|
+
// ask_user: show the question headers (or first-question-text fallback)
|
|
3434
|
+
// instead of the raw JSON array. Some providers (Gemini Flash via
|
|
3435
|
+
// OpenAI compat, etc.) ship the questions field as a JSON-stringified
|
|
3436
|
+
// string rather than a parsed array, so try parsing if needed.
|
|
3437
|
+
if (toolName === 'ask_user') {
|
|
3438
|
+
const rawQuestions = input.questions;
|
|
3439
|
+
let questions = null;
|
|
3440
|
+
if (Array.isArray(rawQuestions)) {
|
|
3441
|
+
questions = rawQuestions;
|
|
3442
|
+
}
|
|
3443
|
+
else if (typeof rawQuestions === 'string') {
|
|
3444
|
+
try {
|
|
3445
|
+
const parsed = JSON.parse(rawQuestions);
|
|
3446
|
+
if (Array.isArray(parsed)) {
|
|
3447
|
+
questions = parsed;
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
catch {
|
|
3451
|
+
/* fall through to default */
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
if (questions) {
|
|
3455
|
+
const headers = questions.map((q) => {
|
|
3456
|
+
if (typeof q.header === 'string' && q.header.length > 0)
|
|
3457
|
+
return q.header;
|
|
3458
|
+
if (typeof q.question === 'string')
|
|
3459
|
+
return q.question;
|
|
3460
|
+
return '?';
|
|
3461
|
+
});
|
|
3462
|
+
return truncate(headers.join(', '), 50);
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
// build_interactive_flow: show flow.title (same JSON-string fallback)
|
|
3466
|
+
if (toolName === 'build_interactive_flow') {
|
|
3467
|
+
let flow = input.flow;
|
|
3468
|
+
if (typeof flow === 'string') {
|
|
3469
|
+
try {
|
|
3470
|
+
flow = JSON.parse(flow);
|
|
3471
|
+
}
|
|
3472
|
+
catch {
|
|
3473
|
+
/* fall through */
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
if (flow && typeof flow === 'object' && 'title' in flow) {
|
|
3477
|
+
const f = flow;
|
|
3478
|
+
if (typeof f.title === 'string') {
|
|
3479
|
+
return truncate(f.title, 50);
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
// propose_alternatives: show alternative titles. Same defensive JSON
|
|
3484
|
+
// parse as ask_user above — some providers JSON-stringify nested arrays.
|
|
3485
|
+
if (toolName === 'propose_alternatives') {
|
|
3486
|
+
const raw = input.alternatives;
|
|
3487
|
+
let alternatives = null;
|
|
3488
|
+
if (Array.isArray(raw)) {
|
|
3489
|
+
alternatives = raw;
|
|
3490
|
+
}
|
|
3491
|
+
else if (typeof raw === 'string') {
|
|
3492
|
+
try {
|
|
3493
|
+
const parsed = JSON.parse(raw);
|
|
3494
|
+
if (Array.isArray(parsed)) {
|
|
3495
|
+
alternatives = parsed;
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
catch {
|
|
3499
|
+
/* fall through to default */
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
if (alternatives) {
|
|
3503
|
+
const titles = alternatives.map((a) => typeof a.title === 'string' ? a.title : '?');
|
|
3504
|
+
return truncate(titles.join(', '), 50);
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3178
3507
|
// Default: show first param
|
|
3179
3508
|
const keys = Object.keys(input);
|
|
3180
3509
|
if (keys.length > 0) {
|