@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
|
@@ -5,15 +5,30 @@
|
|
|
5
5
|
* The deps object is SharedState passed by reference, so live mutations
|
|
6
6
|
* (e.g. team set after REPL starts) work exactly as before.
|
|
7
7
|
*/
|
|
8
|
-
import { registerDelegateHandler, registerDelegateBackgroundHandler, registerHandoffHandler, } from '../shared-handlers.js';
|
|
8
|
+
import { registerDelegateHandler, registerDelegateBackgroundHandler, registerHandoffHandler, registerConsultHandler, } from '../shared-handlers.js';
|
|
9
9
|
import { getDelegationTracker } from '../multi-agent/delegation-tracker.js';
|
|
10
|
+
import { buildHandoffTaskMessage, buildConsultQuestionMessage, } from '@compilr-dev/sdk';
|
|
10
11
|
import chalk from 'chalk';
|
|
11
12
|
import { truncate } from '../ui/base/index.js';
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
13
14
|
// Factory
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
export function registerDelegationHandlers(deps) {
|
|
16
|
-
// Register delegate handler for multi-agent delegation
|
|
17
|
+
// Register delegate handler for multi-agent delegation.
|
|
18
|
+
//
|
|
19
|
+
// Behavior (per multi-agent-tools-taxonomy-spec.md §2):
|
|
20
|
+
// - Coordinator → specialist, foreground, SYNC response.
|
|
21
|
+
// - Approval overlay shown (preserved UX).
|
|
22
|
+
// - On approval, specialist runs the task INLINE and the response is
|
|
23
|
+
// captured + returned to the coordinator as the tool result. The
|
|
24
|
+
// active speaker does NOT permanently switch — coordinator stays in
|
|
25
|
+
// charge of the conversation.
|
|
26
|
+
// - Specialist's history IS persisted (delegate is the "persistent" sibling
|
|
27
|
+
// of consult — see consult handler below for the transient version).
|
|
28
|
+
//
|
|
29
|
+
// Previously (pre-spec): set `pendingDelegation` so the REPL post-turn loop
|
|
30
|
+
// would switch active agent permanently. That was effectively
|
|
31
|
+
// "handoff with approval" and didn't match the intended semantics.
|
|
17
32
|
registerDelegateHandler(async (input) => {
|
|
18
33
|
// Wait for event loop to process any pending tool_end events
|
|
19
34
|
await new Promise((resolve) => setImmediate(resolve));
|
|
@@ -28,8 +43,8 @@ export function registerDelegationHandlers(deps) {
|
|
|
28
43
|
};
|
|
29
44
|
}
|
|
30
45
|
// Validate target agent exists
|
|
31
|
-
const
|
|
32
|
-
if (!
|
|
46
|
+
const targetTeamAgent = deps.team.get(input.agentId);
|
|
47
|
+
if (!targetTeamAgent) {
|
|
33
48
|
return {
|
|
34
49
|
success: false,
|
|
35
50
|
declined: false,
|
|
@@ -44,63 +59,74 @@ export function registerDelegationHandlers(deps) {
|
|
|
44
59
|
error: 'Only the coordinator can delegate tasks. Use $<agent> to switch agents manually.',
|
|
45
60
|
};
|
|
46
61
|
}
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
success: false,
|
|
58
|
-
declined: true,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
// Set pending delegation - REPL will handle the switch after agent finishes
|
|
62
|
-
deps.pendingDelegation = {
|
|
63
|
-
agentId: input.agentId,
|
|
64
|
-
task: input.task,
|
|
65
|
-
};
|
|
62
|
+
// Ask for user approval — V2 overlay first, ask_user_simple fallback
|
|
63
|
+
const approval = await askForDelegationApproval({
|
|
64
|
+
deps,
|
|
65
|
+
agentId: input.agentId,
|
|
66
|
+
displayName: targetTeamAgent.displayName,
|
|
67
|
+
task: input.task,
|
|
68
|
+
reason: input.reason,
|
|
69
|
+
});
|
|
70
|
+
if (approval.kind === 'no-ui') {
|
|
66
71
|
return {
|
|
67
|
-
success:
|
|
72
|
+
success: false,
|
|
68
73
|
declined: false,
|
|
74
|
+
error: 'Delegation UI not available. Ensure you are running in interactive mode.',
|
|
69
75
|
};
|
|
70
76
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (result.skipped || result.answer === 'No') {
|
|
77
|
+
if (approval.kind === 'declined') {
|
|
78
|
+
return { success: false, declined: true };
|
|
79
|
+
}
|
|
80
|
+
// Approved → run the specialist inline. Same pattern as consult below,
|
|
81
|
+
// minus the history snapshot/restore (delegate is persistent — the
|
|
82
|
+
// specialist remembers the work).
|
|
83
|
+
if (!targetTeamAgent.agent) {
|
|
84
|
+
try {
|
|
85
|
+
await deps.team.initializeAll();
|
|
86
|
+
}
|
|
87
|
+
catch (initErr) {
|
|
83
88
|
return {
|
|
84
89
|
success: false,
|
|
85
|
-
declined:
|
|
90
|
+
declined: false,
|
|
91
|
+
error: `Failed to initialize $${input.agentId}: ${initErr instanceof Error ? initErr.message : String(initErr)}`,
|
|
86
92
|
};
|
|
87
93
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
}
|
|
95
|
+
const targetAgent = targetTeamAgent.agent;
|
|
96
|
+
if (!targetAgent) {
|
|
97
|
+
return {
|
|
98
|
+
success: false,
|
|
99
|
+
declined: false,
|
|
100
|
+
error: `Agent '$${input.agentId}' could not be initialized.`,
|
|
92
101
|
};
|
|
102
|
+
}
|
|
103
|
+
// Build the delegated task message. Mirrors the delegate_background
|
|
104
|
+
// preamble's SCOPE block so the specialist knows it's a delegation.
|
|
105
|
+
const taskMessage = buildDelegateTaskMessage({
|
|
106
|
+
task: input.task,
|
|
107
|
+
reason: input.reason,
|
|
108
|
+
});
|
|
109
|
+
try {
|
|
110
|
+
const runResult = await targetAgent.run(taskMessage);
|
|
111
|
+
// Truncate very long responses to keep the coordinator's context lean.
|
|
112
|
+
const response = runResult.response.length > 2000
|
|
113
|
+
? runResult.response.slice(0, 2000) + '\n…[truncated to 2000 chars]'
|
|
114
|
+
: runResult.response;
|
|
93
115
|
return {
|
|
94
116
|
success: true,
|
|
95
117
|
declined: false,
|
|
118
|
+
response,
|
|
119
|
+
agentId: input.agentId,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
return {
|
|
124
|
+
success: false,
|
|
125
|
+
declined: false,
|
|
126
|
+
agentId: input.agentId,
|
|
127
|
+
error: `Delegate failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
96
128
|
};
|
|
97
129
|
}
|
|
98
|
-
// No handler available
|
|
99
|
-
return {
|
|
100
|
-
success: false,
|
|
101
|
-
declined: false,
|
|
102
|
-
error: 'Delegation UI not available. Ensure you are running in interactive mode.',
|
|
103
|
-
};
|
|
104
130
|
});
|
|
105
131
|
// Register delegate_background handler for coordinator mode
|
|
106
132
|
registerDelegateBackgroundHandler(async (input) => {
|
|
@@ -277,15 +303,167 @@ export function registerDelegationHandlers(deps) {
|
|
|
277
303
|
}
|
|
278
304
|
// Record the handoff for one-hop tracking
|
|
279
305
|
deps.team.recordHandoff(input.agentId, sourceAgentId);
|
|
280
|
-
// Set pending delegation - REPL will handle the switch after agent finishes
|
|
281
|
-
// Reuses the same pendingDelegation mechanism as delegate
|
|
306
|
+
// Set pending delegation - REPL will handle the switch after agent finishes.
|
|
307
|
+
// Reuses the same pendingDelegation mechanism as delegate. We dispatch the
|
|
308
|
+
// task via `buildHandoffTaskMessage` (SDK helper) so the target receives
|
|
309
|
+
// the canonical `[Handoff from $arch]\n\nTask: …` wrapper — identical
|
|
310
|
+
// wording to what the Desktop sends on handoff, regardless of host.
|
|
311
|
+
const intent = {
|
|
312
|
+
// CLI has a single REPL session — the conversation key is implicit.
|
|
313
|
+
// SDK takes this as opaque, so any stable string is fine.
|
|
314
|
+
conversationId: 'cli',
|
|
315
|
+
sourceAgentId,
|
|
316
|
+
targetAgentId: input.agentId,
|
|
317
|
+
task: input.task,
|
|
318
|
+
reason: input.reason,
|
|
319
|
+
};
|
|
282
320
|
deps.pendingDelegation = {
|
|
283
321
|
agentId: input.agentId,
|
|
284
|
-
task:
|
|
322
|
+
task: buildHandoffTaskMessage(intent),
|
|
285
323
|
};
|
|
286
324
|
return {
|
|
287
325
|
success: true,
|
|
288
326
|
declined: false,
|
|
289
327
|
};
|
|
290
328
|
});
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
// consult handler — specialist-to-specialist sync borrow
|
|
331
|
+
//
|
|
332
|
+
// Unlike delegate/handoff (which set `pendingDelegation` so the REPL can
|
|
333
|
+
// run the target agent on the NEXT turn), consult must run the target
|
|
334
|
+
// INLINE during the borrower's current turn and return the answer as the
|
|
335
|
+
// tool's output. The borrower's run never yields control.
|
|
336
|
+
//
|
|
337
|
+
// Transient design: snapshot the target's history, run the synthesised
|
|
338
|
+
// question, restore the history. The target's chat is untouched.
|
|
339
|
+
//
|
|
340
|
+
// Errors are returned as `{ answer: '', error: ... }` so the borrower's
|
|
341
|
+
// LLM sees them as tool output and can decide what to do.
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
registerConsultHandler(async (input) => {
|
|
344
|
+
// Yield to event loop (consistent with the other handlers in this file).
|
|
345
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
346
|
+
if (!deps.team) {
|
|
347
|
+
return {
|
|
348
|
+
answer: '',
|
|
349
|
+
agentId: input.agentId,
|
|
350
|
+
error: 'Multi-agent team not initialized. Consult requires an active team.',
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const targetTeamAgent = deps.team.get(input.agentId);
|
|
354
|
+
if (!targetTeamAgent) {
|
|
355
|
+
return {
|
|
356
|
+
answer: '',
|
|
357
|
+
agentId: input.agentId,
|
|
358
|
+
error: `Agent '${input.agentId}' not found in team.`,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
// Auto-initialize the target if it hasn't been used yet. This used
|
|
362
|
+
// to be an error ("switch to them once first") but the friction was
|
|
363
|
+
// wrong — a coordinator asking the user to manually warm up every
|
|
364
|
+
// specialist before consult is unreasonable. team.initializeAll()
|
|
365
|
+
// is the SDK's lazy-init entry point; it skips already-initialized
|
|
366
|
+
// agents, so subsequent consults pay only the first-time cost.
|
|
367
|
+
if (!targetTeamAgent.agent) {
|
|
368
|
+
try {
|
|
369
|
+
await deps.team.initializeAll();
|
|
370
|
+
}
|
|
371
|
+
catch (initErr) {
|
|
372
|
+
return {
|
|
373
|
+
answer: '',
|
|
374
|
+
agentId: input.agentId,
|
|
375
|
+
error: `Failed to initialize $${input.agentId}: ${initErr instanceof Error ? initErr.message : String(initErr)}`,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const targetAgent = targetTeamAgent.agent;
|
|
380
|
+
if (!targetAgent) {
|
|
381
|
+
return {
|
|
382
|
+
answer: '',
|
|
383
|
+
agentId: input.agentId,
|
|
384
|
+
error: `Agent '$${input.agentId}' could not be initialized.`,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
const sourceAgentId = deps.team.activeAgentId;
|
|
388
|
+
const consultMessage = buildConsultQuestionMessage({
|
|
389
|
+
sourceAgentId,
|
|
390
|
+
question: input.question,
|
|
391
|
+
context: input.context,
|
|
392
|
+
});
|
|
393
|
+
// Snapshot the target's history so the consult is transient — the
|
|
394
|
+
// target's actual chat shouldn't grow a record of consults received.
|
|
395
|
+
const historySnapshot = targetAgent.getHistory();
|
|
396
|
+
try {
|
|
397
|
+
// Run inline. No onEvent forwarder → the target's tool calls and
|
|
398
|
+
// streaming text don't render to the REPL (we want the source agent's
|
|
399
|
+
// voice foregrounded; the answer comes back as a tool result).
|
|
400
|
+
const runResult = await targetAgent.run(consultMessage);
|
|
401
|
+
return { answer: runResult.response, agentId: input.agentId };
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
return {
|
|
405
|
+
answer: '',
|
|
406
|
+
agentId: input.agentId,
|
|
407
|
+
error: `Consult failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
// Restore history regardless of success/failure — even a partial run
|
|
412
|
+
// mutates the target's message log, and we promised it'd be transient.
|
|
413
|
+
try {
|
|
414
|
+
await targetAgent.setHistory(historySnapshot);
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
// History restore is best-effort; if it fails the next message the
|
|
418
|
+
// user sends to the target will see the consult artefacts. Logging
|
|
419
|
+
// happens via the agent's own error path.
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Show the delegation approval overlay (V2 if available, ask_user_simple
|
|
426
|
+
* fallback otherwise). Centralises the dual-path logic that the old
|
|
427
|
+
* delegate handler inlined twice.
|
|
428
|
+
*/
|
|
429
|
+
async function askForDelegationApproval(args) {
|
|
430
|
+
const { deps, agentId, displayName, task, reason } = args;
|
|
431
|
+
// V2 mode: TerminalUI overlay
|
|
432
|
+
if (deps.showDelegateOverlayV2) {
|
|
433
|
+
const result = await deps.showDelegateOverlayV2({
|
|
434
|
+
agentId,
|
|
435
|
+
agentDisplayName: displayName,
|
|
436
|
+
task,
|
|
437
|
+
reason,
|
|
438
|
+
});
|
|
439
|
+
return result.approved ? { kind: 'approved' } : { kind: 'declined' };
|
|
440
|
+
}
|
|
441
|
+
// Fallback: ask_user_simple
|
|
442
|
+
if (deps.showAskUserSimpleOverlayV2) {
|
|
443
|
+
const question = reason
|
|
444
|
+
? `Delegate to $${agentId} (${displayName})?\n${chalk.dim(`Reason: ${reason}`)}\n${chalk.dim(`Task: ${truncate(task, 100)}`)}`
|
|
445
|
+
: `Delegate to $${agentId} (${displayName})?\n${chalk.dim(`Task: ${truncate(task, 100)}`)}`;
|
|
446
|
+
const result = await deps.showAskUserSimpleOverlayV2({
|
|
447
|
+
question,
|
|
448
|
+
options: ['Yes', 'No'],
|
|
449
|
+
allowCustom: false,
|
|
450
|
+
});
|
|
451
|
+
if (result.skipped || result.answer === 'No')
|
|
452
|
+
return { kind: 'declined' };
|
|
453
|
+
return { kind: 'approved' };
|
|
454
|
+
}
|
|
455
|
+
return { kind: 'no-ui' };
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Build the delegated task message handed to the specialist. Mirrors the
|
|
459
|
+
* delegate_background preamble's SCOPE block so the specialist knows it
|
|
460
|
+
* was a delegation (versus a direct turn from the user or a consult).
|
|
461
|
+
*/
|
|
462
|
+
function buildDelegateTaskMessage(args) {
|
|
463
|
+
const { task, reason } = args;
|
|
464
|
+
const lines = ['[DELEGATION from coordinator]'];
|
|
465
|
+
if (reason)
|
|
466
|
+
lines.push(`Reason: ${reason}`);
|
|
467
|
+
lines.push('', 'SCOPE: Focus ONLY on the task below. Do NOT pick up, start, or ask', 'about other tasks. Report your final result when done.', '', 'Task:', task);
|
|
468
|
+
return lines.join('\n');
|
|
291
469
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* build_interactive_flow handler factory — mirrors ask-user-handlers.ts.
|
|
3
|
+
*
|
|
4
|
+
* Registers a handler that:
|
|
5
|
+
* 1. Waits for any pending tool_end events to drain
|
|
6
|
+
* 2. Flushes accumulated agent text so the wizard doesn't crowd the
|
|
7
|
+
* conversation log
|
|
8
|
+
* 3. For background agents, enqueues the request via the pending-
|
|
9
|
+
* requests manager (user services it through /pending)
|
|
10
|
+
* 4. For foreground agents, shows InteractiveFlowOverlayV2
|
|
11
|
+
*
|
|
12
|
+
* Spec: project-docs/00-requirements/compilr-dev-cli/interactive-flow-cli-spec.md
|
|
13
|
+
*/
|
|
14
|
+
import { type InteractiveFlowResult, type Flow } from '../shared-handlers.js';
|
|
15
|
+
export interface InteractiveFlowHandlerDeps {
|
|
16
|
+
flushTextBuffer?: () => void;
|
|
17
|
+
getActiveBackgroundAgentId?: () => string | null;
|
|
18
|
+
isBackgroundAgent?: (agentId?: string) => boolean;
|
|
19
|
+
getBackgroundAgentInfo?: (agentId: string) => {
|
|
20
|
+
mascot: string;
|
|
21
|
+
} | null;
|
|
22
|
+
showInteractiveFlowOverlayV2?: (options: {
|
|
23
|
+
flow: Flow;
|
|
24
|
+
}) => Promise<InteractiveFlowResult>;
|
|
25
|
+
}
|
|
26
|
+
export declare function registerInteractiveFlowHandlers(deps: InteractiveFlowHandlerDeps): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* build_interactive_flow handler factory — mirrors ask-user-handlers.ts.
|
|
3
|
+
*
|
|
4
|
+
* Registers a handler that:
|
|
5
|
+
* 1. Waits for any pending tool_end events to drain
|
|
6
|
+
* 2. Flushes accumulated agent text so the wizard doesn't crowd the
|
|
7
|
+
* conversation log
|
|
8
|
+
* 3. For background agents, enqueues the request via the pending-
|
|
9
|
+
* requests manager (user services it through /pending)
|
|
10
|
+
* 4. For foreground agents, shows InteractiveFlowOverlayV2
|
|
11
|
+
*
|
|
12
|
+
* Spec: project-docs/00-requirements/compilr-dev-cli/interactive-flow-cli-spec.md
|
|
13
|
+
*/
|
|
14
|
+
import { registerInteractiveFlowHandler, } from '../shared-handlers.js';
|
|
15
|
+
import { getPendingRequestsManager } from '../multi-agent/index.js';
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Factory
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
export function registerInteractiveFlowHandlers(deps) {
|
|
20
|
+
registerInteractiveFlowHandler(async (input) => {
|
|
21
|
+
// Wait for any pending tool_end events to drain
|
|
22
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
23
|
+
// Flush accumulated text BEFORE showing the overlay
|
|
24
|
+
deps.flushTextBuffer?.();
|
|
25
|
+
// Background-agent routing — enqueue via pending-requests manager
|
|
26
|
+
const backgroundAgentId = deps.getActiveBackgroundAgentId?.();
|
|
27
|
+
if (backgroundAgentId && deps.isBackgroundAgent?.(backgroundAgentId)) {
|
|
28
|
+
const agentInfo = deps.getBackgroundAgentInfo?.(backgroundAgentId);
|
|
29
|
+
const pendingManager = getPendingRequestsManager();
|
|
30
|
+
try {
|
|
31
|
+
const response = await pendingManager.enqueue({
|
|
32
|
+
agentId: backgroundAgentId,
|
|
33
|
+
mascot: agentInfo?.mascot ?? '[•_•]',
|
|
34
|
+
type: 'question',
|
|
35
|
+
prompt: input.flow.title,
|
|
36
|
+
context: {
|
|
37
|
+
toolType: 'build_interactive_flow',
|
|
38
|
+
originalInput: input,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
return response.overlayResult;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Request was cancelled — return aborted result
|
|
45
|
+
return {
|
|
46
|
+
completed: false,
|
|
47
|
+
path: [],
|
|
48
|
+
answers: {},
|
|
49
|
+
answerLabels: {},
|
|
50
|
+
abortReason: 'unknown',
|
|
51
|
+
durationMs: 0,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Foreground — show the overlay
|
|
56
|
+
if (!deps.showInteractiveFlowOverlayV2) {
|
|
57
|
+
throw new Error('build_interactive_flow overlay not initialized');
|
|
58
|
+
}
|
|
59
|
+
return deps.showInteractiveFlowOverlayV2({ flow: input.flow });
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* propose_alternatives handler factory — mirrors ask-user-handlers.ts.
|
|
3
|
+
*
|
|
4
|
+
* Registers a handler that:
|
|
5
|
+
* 1. Waits for any pending tool_end events to drain
|
|
6
|
+
* 2. Flushes accumulated agent text so the overlay doesn't crowd the
|
|
7
|
+
* conversation log
|
|
8
|
+
* 3. For background agents, enqueues the request via the pending-
|
|
9
|
+
* requests manager (user services it through /pending)
|
|
10
|
+
* 4. For foreground agents, shows ProposeAlternativesOverlayV2
|
|
11
|
+
*
|
|
12
|
+
* The deps object is SharedState passed by reference, so live overlay
|
|
13
|
+
* callbacks wired after REPL starts work as expected.
|
|
14
|
+
*
|
|
15
|
+
* Spec: project-docs/00-requirements/compilr-dev-cli/propose-alternatives-spec.md
|
|
16
|
+
*/
|
|
17
|
+
import { type Alternative } from '../shared-handlers.js';
|
|
18
|
+
export interface ProposeAlternativesHandlerDeps {
|
|
19
|
+
flushTextBuffer?: () => void;
|
|
20
|
+
getActiveBackgroundAgentId?: () => string | null;
|
|
21
|
+
isBackgroundAgent?: (agentId?: string) => boolean;
|
|
22
|
+
getBackgroundAgentInfo?: (agentId: string) => {
|
|
23
|
+
mascot: string;
|
|
24
|
+
} | null;
|
|
25
|
+
showProposeAlternativesOverlayV2?: (options: {
|
|
26
|
+
question: string;
|
|
27
|
+
context?: string;
|
|
28
|
+
alternatives: Alternative[];
|
|
29
|
+
}) => Promise<{
|
|
30
|
+
chosenIndex: number;
|
|
31
|
+
chosenTitle: string;
|
|
32
|
+
notes?: string;
|
|
33
|
+
rejected: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
export declare function registerProposeAlternativesHandlers(deps: ProposeAlternativesHandlerDeps): void;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* propose_alternatives handler factory — mirrors ask-user-handlers.ts.
|
|
3
|
+
*
|
|
4
|
+
* Registers a handler that:
|
|
5
|
+
* 1. Waits for any pending tool_end events to drain
|
|
6
|
+
* 2. Flushes accumulated agent text so the overlay doesn't crowd the
|
|
7
|
+
* conversation log
|
|
8
|
+
* 3. For background agents, enqueues the request via the pending-
|
|
9
|
+
* requests manager (user services it through /pending)
|
|
10
|
+
* 4. For foreground agents, shows ProposeAlternativesOverlayV2
|
|
11
|
+
*
|
|
12
|
+
* The deps object is SharedState passed by reference, so live overlay
|
|
13
|
+
* callbacks wired after REPL starts work as expected.
|
|
14
|
+
*
|
|
15
|
+
* Spec: project-docs/00-requirements/compilr-dev-cli/propose-alternatives-spec.md
|
|
16
|
+
*/
|
|
17
|
+
import { registerProposeAlternativesHandler, } from '../shared-handlers.js';
|
|
18
|
+
import { getPendingRequestsManager } from '../multi-agent/index.js';
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Factory
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
export function registerProposeAlternativesHandlers(deps) {
|
|
23
|
+
registerProposeAlternativesHandler(async (input) => {
|
|
24
|
+
// Wait for any pending tool_end events to drain
|
|
25
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
26
|
+
// Flush accumulated text BEFORE showing the overlay
|
|
27
|
+
deps.flushTextBuffer?.();
|
|
28
|
+
// Background-agent routing — enqueue via pending-requests manager
|
|
29
|
+
const backgroundAgentId = deps.getActiveBackgroundAgentId?.();
|
|
30
|
+
if (backgroundAgentId && deps.isBackgroundAgent?.(backgroundAgentId)) {
|
|
31
|
+
const agentInfo = deps.getBackgroundAgentInfo?.(backgroundAgentId);
|
|
32
|
+
const pendingManager = getPendingRequestsManager();
|
|
33
|
+
try {
|
|
34
|
+
const response = await pendingManager.enqueue({
|
|
35
|
+
agentId: backgroundAgentId,
|
|
36
|
+
mascot: agentInfo?.mascot ?? '[•_•]',
|
|
37
|
+
type: 'question',
|
|
38
|
+
prompt: input.question,
|
|
39
|
+
context: {
|
|
40
|
+
toolType: 'propose_alternatives',
|
|
41
|
+
originalInput: input,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
return response.overlayResult;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Request was cancelled — fall back to a rejected result
|
|
48
|
+
return {
|
|
49
|
+
chosenIndex: 0,
|
|
50
|
+
chosenTitle: input.alternatives[0]?.title ?? '',
|
|
51
|
+
rejected: true,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Foreground — show the overlay
|
|
56
|
+
if (!deps.showProposeAlternativesOverlayV2) {
|
|
57
|
+
throw new Error('propose_alternatives overlay not initialized');
|
|
58
|
+
}
|
|
59
|
+
return deps.showProposeAlternativesOverlayV2({
|
|
60
|
+
question: input.question,
|
|
61
|
+
context: input.context,
|
|
62
|
+
alternatives: input.alternatives,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -36,8 +36,9 @@ import { cleanupStaleDiffs } from './ui/vscode-diff-ipc.js';
|
|
|
36
36
|
import { createPermissionHandler } from './handlers/permission-handler.js';
|
|
37
37
|
import { registerDelegationHandlers } from './handlers/delegation-handlers.js';
|
|
38
38
|
import { registerAskUserHandlers } from './handlers/ask-user-handlers.js';
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
39
|
+
import { registerProposeAlternativesHandlers } from './handlers/propose-alternatives-handlers.js';
|
|
40
|
+
import { registerInteractiveFlowHandlers } from './handlers/interactive-flow-handlers.js';
|
|
41
|
+
import { EpisodeRecorder, FileEpisodeStore, setGlobalEpisodeStore, updateWorkSummaryAnchor, } from './episodes/index.js';
|
|
41
42
|
import { getSessionsPath } from './settings/paths.js';
|
|
42
43
|
import * as path from 'path';
|
|
43
44
|
perf('imports-done');
|
|
@@ -447,6 +448,8 @@ async function main() {
|
|
|
447
448
|
const onPermissionRequest = createPermissionHandler(sharedState);
|
|
448
449
|
// Register ask_user and ask_user_simple handlers
|
|
449
450
|
registerAskUserHandlers(sharedState);
|
|
451
|
+
registerProposeAlternativesHandlers(sharedState);
|
|
452
|
+
registerInteractiveFlowHandlers(sharedState);
|
|
450
453
|
// Register delegate, delegate_background, and handoff handlers for multi-agent delegation
|
|
451
454
|
registerDelegationHandlers(sharedState);
|
|
452
455
|
// Create iteration limit handler that shows overlay
|
|
@@ -712,6 +715,14 @@ async function main() {
|
|
|
712
715
|
onAskUserReady: (showOverlay) => {
|
|
713
716
|
sharedState.showAskUserOverlayV2 = showOverlay;
|
|
714
717
|
},
|
|
718
|
+
// Set up propose_alternatives V2 overlay callback
|
|
719
|
+
onProposeAlternativesReady: (showOverlay) => {
|
|
720
|
+
sharedState.showProposeAlternativesOverlayV2 = showOverlay;
|
|
721
|
+
},
|
|
722
|
+
// Set up build_interactive_flow V2 overlay callback
|
|
723
|
+
onInteractiveFlowReady: (showOverlay) => {
|
|
724
|
+
sharedState.showInteractiveFlowOverlayV2 = showOverlay;
|
|
725
|
+
},
|
|
715
726
|
// Set up iteration_limit V2 overlay callback
|
|
716
727
|
onIterationLimitReady: (showOverlay) => {
|
|
717
728
|
sharedState.showIterationLimitOverlayV2 = showOverlay;
|
package/dist/repl-v2.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { type PermissionResult, type PermissionOverlayV2Options } from './ui/ove
|
|
|
15
15
|
import { type AskUserSimpleOptionsV2, type AskUserSimpleResultV2 } from './ui/overlay/impl/ask-user-simple-overlay-v2.js';
|
|
16
16
|
import { type GuardrailConfirmOptionsV2, type GuardrailConfirmResultV2 } from './ui/overlay/impl/guardrail-overlay-v2.js';
|
|
17
17
|
import { type AskUserOptionsV2, type AskUserResultV2 } from './ui/overlay/impl/ask-user-overlay-v2.js';
|
|
18
|
+
import { type ProposeAlternativesOptionsV2, type ProposeAlternativesResultV2 } from './ui/overlay/impl/propose-alternatives-overlay-v2.js';
|
|
19
|
+
import { type InteractiveFlowOptionsV2, type InteractiveFlowResultV2 } from './ui/overlay/impl/interactive-flow-overlay-v2.js';
|
|
18
20
|
import { type IterationLimitOptionsV2, type IterationLimitResultV2 } from './ui/overlay/impl/iteration-limit-overlay-v2.js';
|
|
19
21
|
export interface DelegateOverlayOptions {
|
|
20
22
|
agentId: string;
|
|
@@ -70,6 +72,16 @@ export interface ReplV2Options {
|
|
|
70
72
|
* Allows index.ts to use V2 overlay for multi-question forms.
|
|
71
73
|
*/
|
|
72
74
|
onAskUserReady?: (showOverlay: (options: AskUserOptionsV2) => Promise<AskUserResultV2>) => void;
|
|
75
|
+
/**
|
|
76
|
+
* Callback invoked when propose_alternatives overlay is ready.
|
|
77
|
+
* Allows index.ts to wire showProposeAlternativesOverlayV2 on sharedState.
|
|
78
|
+
*/
|
|
79
|
+
onProposeAlternativesReady?: (showOverlay: (options: ProposeAlternativesOptionsV2) => Promise<ProposeAlternativesResultV2>) => void;
|
|
80
|
+
/**
|
|
81
|
+
* Callback invoked when build_interactive_flow overlay is ready.
|
|
82
|
+
* Allows index.ts to wire showInteractiveFlowOverlayV2 on sharedState.
|
|
83
|
+
*/
|
|
84
|
+
onInteractiveFlowReady?: (showOverlay: (options: InteractiveFlowOptionsV2) => Promise<InteractiveFlowResultV2>) => void;
|
|
73
85
|
/**
|
|
74
86
|
* Callback invoked when iteration_limit overlay is ready.
|
|
75
87
|
* Allows index.ts to use V2 overlay for iteration limit prompts.
|
|
@@ -238,6 +250,8 @@ export declare class ReplV2 {
|
|
|
238
250
|
private readonly onAskUserSimpleReady?;
|
|
239
251
|
private readonly onGuardrailReady?;
|
|
240
252
|
private readonly onAskUserReady?;
|
|
253
|
+
private readonly onProposeAlternativesReady?;
|
|
254
|
+
private readonly onInteractiveFlowReady?;
|
|
241
255
|
private readonly onIterationLimitReady?;
|
|
242
256
|
private readonly onAgentFinish?;
|
|
243
257
|
private readonly onSuggestionReady?;
|
|
@@ -264,6 +278,7 @@ export declare class ReplV2 {
|
|
|
264
278
|
private sessionRequests;
|
|
265
279
|
private textAccumulator;
|
|
266
280
|
private readonly bashAbortControllers;
|
|
281
|
+
private readonly specialistToolsRegistered;
|
|
267
282
|
constructor(options?: ReplV2Options);
|
|
268
283
|
/**
|
|
269
284
|
* Flush accumulated agent text to the UI.
|
|
@@ -354,6 +369,57 @@ export declare class ReplV2 {
|
|
|
354
369
|
* Callers handle their own messaging (different styles/wording per context).
|
|
355
370
|
*/
|
|
356
371
|
private switchAgent;
|
|
372
|
+
/**
|
|
373
|
+
* Register multi-agent tools (consult always, handoff for specialists)
|
|
374
|
+
* on the agent's underlying Agent using SDK factories.
|
|
375
|
+
*
|
|
376
|
+
* Tool grants:
|
|
377
|
+
* - **consult**: everyone (coordinator AND specialists). Lightweight
|
|
378
|
+
* "ask another agent a focused question" tool. Originally specced as
|
|
379
|
+
* specialist-only, but empirical evidence (session 220) showed the
|
|
380
|
+
* coordinator's LLM grabs the wrong tool (e.g. `suggest`) when the
|
|
381
|
+
* user says "consult $pm" because no tool by that name was in its
|
|
382
|
+
* registry. delegate is too heavyweight for a quick question.
|
|
383
|
+
* - **handoff**: specialists only. Coordinators delegate, they don't
|
|
384
|
+
* pass the baton. $default → specialist already has delegate.
|
|
385
|
+
* - **delegate / delegate_background**: coordinator only, via the
|
|
386
|
+
* existing CLI tool-array path. Not touched here.
|
|
387
|
+
*
|
|
388
|
+
* Why lazy: the SDK factories need `team` + `currentAgentId` at
|
|
389
|
+
* construction time. The CLI's tool array is built at module load
|
|
390
|
+
* when neither is known.
|
|
391
|
+
*
|
|
392
|
+
* Dedupe via `specialistToolsRegistered` set — tools are registered
|
|
393
|
+
* once per agent for the lifetime of the REPL. Mirrors Desktop's
|
|
394
|
+
* pattern in `agent-manager.ts:registerDelegateToolsIfNeeded`.
|
|
395
|
+
*
|
|
396
|
+
* Called from:
|
|
397
|
+
* - `switchAgent` (every agent switch — covers specialists).
|
|
398
|
+
* - `restoreTeamForPrefix` (team restoration — covers $default at
|
|
399
|
+
* startup, since the coordinator is never switchTo'd from outside).
|
|
400
|
+
*/
|
|
401
|
+
/**
|
|
402
|
+
* Called whenever `this.team` is reassigned (project switch, team
|
|
403
|
+
* restoration). The SDK's createConsultTool factory captures the `team`
|
|
404
|
+
* argument in its closure at construction time, so a tool registered
|
|
405
|
+
* against the old team has a stale roster view. We must:
|
|
406
|
+
* 1. Unregister the stale consult/handoff tools from $default's
|
|
407
|
+
* registry (Agent.registerTools throws on duplicate-name, it
|
|
408
|
+
* does not replace).
|
|
409
|
+
* 2. Re-register fresh tools bound to the new team.
|
|
410
|
+
*
|
|
411
|
+
* $default's underlying Agent is preserved across team swaps (the new
|
|
412
|
+
* team is set as default via setDefaultAgent), so its tool registry
|
|
413
|
+
* accumulates stale tools. Specialists' Agents are fresh after a swap,
|
|
414
|
+
* so they don't have stale tools to clean up — clearing the dedupe set
|
|
415
|
+
* is enough; they'll re-register on demand via switchAgent.
|
|
416
|
+
*
|
|
417
|
+
* Without this, `consult($pm)` from $default reports "no specialists
|
|
418
|
+
* available" because the closure references the fresh-startup team
|
|
419
|
+
* (which only had $default) instead of the restored/built team.
|
|
420
|
+
*/
|
|
421
|
+
private rebindMultiAgentToolsAfterTeamSwap;
|
|
422
|
+
private registerMultiAgentTools;
|
|
357
423
|
/**
|
|
358
424
|
* Bring a background agent to foreground.
|
|
359
425
|
* Validates the agent has a background session, then delegates to switchAgent().
|