@agent-link/agent 0.1.334 → 0.1.335

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/copilot.js CHANGED
@@ -14,14 +14,12 @@ import { readdirSync, readFileSync, writeFileSync, existsSync, rmSync, mkdirSync
14
14
  import { basename, dirname, extname, join, resolve } from 'path';
15
15
  import { homedir, platform } from 'os';
16
16
  import { randomUUID } from 'crypto';
17
- import * as acp from '@agentclientprotocol/sdk';
18
17
  import { processUserMessage } from './history-message-transform.js';
19
18
  import { copilotCapabilities } from './backends/types.js';
20
19
  import { CONFIG_DIR } from './config.js';
21
20
  import { getCleanEnv, resolveExecutablePath } from './sdk.js';
22
21
  import { getProvider, resolveProviderCommand } from './providers.js';
23
22
  import { FileCache } from './file-cache.js';
24
- import { CopilotCommandRuntime, } from './copilot-command-bridge.js';
25
23
  // ── Module state ───────────────────────────────────────────────────────────
26
24
  const conversations = new Map();
27
25
  const sendFns = new Set();
@@ -29,24 +27,6 @@ let globalModel = null;
29
27
  const MODEL_PATTERN = /^[a-zA-Z0-9._-]+$/;
30
28
  const CONTEXT_TIERS = new Set(['default', 'long_context']);
31
29
  const REASONING_EFFORTS = new Set(['none', 'low', 'medium', 'high', 'xhigh', 'max']);
32
- const COPILOT_PLAN_INPUT_TIMEOUT_MS = 10 * 60_000;
33
- const COPILOT_CONTINUE_PLAN_ANSWER = 'Continue developing the plan. In your final response, ask me to confirm the plan or describe any changes.';
34
- const pendingCopilotDecisions = new Map();
35
- class CopilotDecisionCancelledError extends Error {
36
- constructor() {
37
- super('Copilot decision request was cancelled');
38
- }
39
- }
40
- function setCopilotModeState(state, mode) {
41
- state.copilotMode = mode;
42
- state.planMode = mode === 'plan';
43
- sendFn({
44
- type: 'plan_mode_changed',
45
- enabled: state.planMode,
46
- mode,
47
- conversationId: state.conversationId,
48
- });
49
- }
50
30
  const COPILOT_NATIVE_ATTACHMENT_MIME_TYPES = new Set([
51
31
  'application/pdf',
52
32
  'image/gif',
@@ -80,107 +60,6 @@ export function addCopilotSendFn(fn) {
80
60
  sendFns.add(fn);
81
61
  return () => { sendFns.delete(fn); };
82
62
  }
83
- function emitCopilotDecisionRequest(pending) {
84
- sendFn({
85
- type: 'backend_event',
86
- conversationId: pending.conversationId,
87
- providerId: pending.providerId,
88
- claudeSessionId: pending.sessionId,
89
- event: {
90
- type: 'user_input_requested',
91
- session: sessionRef(pending.sessionId),
92
- requestId: pending.requestId,
93
- request: pending.request,
94
- },
95
- });
96
- }
97
- function settleCopilotDecision(pending, answer) {
98
- if (pendingCopilotDecisions.get(pending.requestId) !== pending)
99
- return false;
100
- pendingCopilotDecisions.delete(pending.requestId);
101
- if (pending.timer)
102
- clearTimeout(pending.timer);
103
- pending.timer = null;
104
- sendFn({
105
- type: 'backend_event',
106
- conversationId: pending.conversationId,
107
- providerId: pending.providerId,
108
- claudeSessionId: pending.sessionId,
109
- event: {
110
- type: 'user_input_resolved',
111
- session: sessionRef(pending.sessionId),
112
- requestId: pending.requestId,
113
- resolution: answer.action === 'cancel' ? 'cancelled' : 'answered',
114
- },
115
- });
116
- pending.resolve(answer);
117
- return true;
118
- }
119
- function waitForCopilotDecision(state, sessionId, request, choices = []) {
120
- const requestId = randomUUID();
121
- return new Promise(resolvePromise => {
122
- const pending = {
123
- requestId,
124
- conversationId: state.conversationId,
125
- providerId: state.providerId,
126
- sessionId,
127
- request,
128
- choices,
129
- resolve: resolvePromise,
130
- timer: null,
131
- };
132
- pendingCopilotDecisions.set(requestId, pending);
133
- if (request.expiresAt) {
134
- pending.timer = setTimeout(() => {
135
- if (choices.length > 0) {
136
- settleCopilotDecision(pending, { action: 'answer', answer: choices[0] });
137
- }
138
- else {
139
- settleCopilotDecision(pending, {
140
- action: 'answer',
141
- answer: COPILOT_CONTINUE_PLAN_ANSWER,
142
- });
143
- }
144
- }, Math.max(0, request.expiresAt - Date.now()));
145
- }
146
- emitCopilotDecisionRequest(pending);
147
- });
148
- }
149
- function cancelPendingCopilotDecisions(conversationId) {
150
- for (const pending of [...pendingCopilotDecisions.values()]) {
151
- if (pending.conversationId === conversationId) {
152
- settleCopilotDecision(pending, { action: 'cancel' });
153
- }
154
- }
155
- }
156
- export function getPendingCopilotDecisions(conversationId) {
157
- return [...pendingCopilotDecisions.values()]
158
- .filter(pending => pending.conversationId === conversationId)
159
- .map(pending => ({
160
- requestId: pending.requestId,
161
- conversationId: pending.conversationId,
162
- providerId: pending.providerId,
163
- sessionId: pending.sessionId,
164
- request: pending.request,
165
- }));
166
- }
167
- export function handleCopilotUserAnswer(requestId, answers, scope = {}) {
168
- const pending = pendingCopilotDecisions.get(requestId);
169
- if (!pending)
170
- return false;
171
- if (scope.conversationId && pending.conversationId !== scope.conversationId)
172
- return false;
173
- if (scope.providerId && pending.providerId !== scope.providerId)
174
- return false;
175
- const action = typeof answers.__action === 'string' ? answers.__action : undefined;
176
- const feedback = typeof answers.__feedback === 'string' ? answers.__feedback.trim() : undefined;
177
- const firstAnswer = Object.entries(answers)
178
- .find(([key, value]) => !key.startsWith('__') && typeof value === 'string')?.[1];
179
- const answer = action || (typeof firstAnswer === 'string' ? firstAnswer : '');
180
- if (!answer)
181
- return false;
182
- return settleCopilotDecision(pending, { action: 'answer', answer, feedback });
183
- }
184
63
  function resolveDirectExecution(cmdPath) {
185
64
  if (platform() !== 'win32')
186
65
  return null;
@@ -254,18 +133,10 @@ function finishCopilotTurn(state) {
254
133
  state.heartbeatTimer = null;
255
134
  }
256
135
  }
257
- function ensureCopilotCommandTurnActive(state, turnId) {
258
- if (!state.turnActive || state.turnId !== turnId) {
259
- throw new CopilotDecisionCancelledError();
260
- }
261
- }
262
136
  function emitCopilotCancelled(conversationId, state) {
263
137
  sendFn({
264
138
  type: 'backend_event',
265
139
  conversationId,
266
- claudeSessionId: state?.copilotSessionId || null,
267
- turnId: state?.turnId || null,
268
- stateVersion: state?.stateVersion || 0,
269
140
  event: { type: 'turn_cancelled', session: sessionRef(state?.copilotSessionId || '') },
270
141
  });
271
142
  sendFn({
@@ -334,10 +205,6 @@ export function rebindCopilotConversation(copilotSessionId, newConvId) {
334
205
  conversations.delete(oldConvId);
335
206
  state.conversationId = newConvId;
336
207
  conversations.set(newConvId, state);
337
- for (const pending of pendingCopilotDecisions.values()) {
338
- if (pending.conversationId === oldConvId)
339
- pending.conversationId = newConvId;
340
- }
341
208
  return true;
342
209
  }
343
210
  }
@@ -362,13 +229,6 @@ export function createCopilotPlaceholder(conversationId, opts) {
362
229
  stateVersion: 0,
363
230
  heartbeatTimer: null,
364
231
  sessionNotified: false,
365
- copilotMode: 'interactive',
366
- planMode: false,
367
- planModePending: false,
368
- isCompacting: false,
369
- commandRuntime: null,
370
- commandSessionId: null,
371
- deferredCommandUsage: null,
372
232
  });
373
233
  }
374
234
  export function restartCopilotConversation(conversationId, opts) {
@@ -383,7 +243,7 @@ export function restartCopilotConversation(conversationId, opts) {
383
243
  child: null,
384
244
  abortController: null,
385
245
  copilotSessionId: null,
386
- lastCopilotSessionId: opts?.preserveSession === false ? null : copilotSessionId,
246
+ lastCopilotSessionId: copilotSessionId,
387
247
  workDir: state?.workDir || process.cwd(),
388
248
  turnActive: false,
389
249
  providerId: opts?.providerId || state?.providerId || 'copilot',
@@ -394,13 +254,6 @@ export function restartCopilotConversation(conversationId, opts) {
394
254
  stateVersion: (state?.stateVersion || 0) + 1,
395
255
  heartbeatTimer: null,
396
256
  sessionNotified: false,
397
- copilotMode: state?.copilotMode || (state?.planMode ? 'plan' : 'interactive'),
398
- planMode: state?.planMode || false,
399
- planModePending: state?.planModePending || false,
400
- isCompacting: false,
401
- commandRuntime: null,
402
- commandSessionId: null,
403
- deferredCommandUsage: null,
404
257
  });
405
258
  return { wasTurnActive, copilotSessionId };
406
259
  }
@@ -414,459 +267,6 @@ export function setModel(conversationId, model) {
414
267
  globalModel = model;
415
268
  }
416
269
  }
417
- function detachCopilotProviderSession(state, providerId) {
418
- if (state.providerId === providerId)
419
- return;
420
- state.providerId = providerId;
421
- state.copilotSessionId = null;
422
- state.lastCopilotSessionId = null;
423
- state.sessionNotified = false;
424
- }
425
- function isMissingCopilotSessionError(error) {
426
- const message = error instanceof Error ? error.message : String(error);
427
- return /(?:session|resource).*not found|not found.*(?:session|resource)|no (?:session|task|name).*matched/i.test(message);
428
- }
429
- function emitCopilotCompactionStarted(state) {
430
- if (state.isCompacting)
431
- return false;
432
- state.isCompacting = true;
433
- const session = sessionRef(state.copilotSessionId || state.commandSessionId || 'pending');
434
- sendFn({
435
- type: 'context_compaction',
436
- conversationId: state.conversationId,
437
- turnId: state.turnId,
438
- status: 'started',
439
- });
440
- sendFn({
441
- type: 'backend_event',
442
- conversationId: state.conversationId,
443
- turnId: state.turnId,
444
- event: { type: 'compact_started', session },
445
- });
446
- return true;
447
- }
448
- function emitCopilotCompactionCompleted(state) {
449
- if (!state.isCompacting)
450
- return false;
451
- state.isCompacting = false;
452
- const session = sessionRef(state.copilotSessionId || state.commandSessionId || '');
453
- sendFn({
454
- type: 'context_compaction',
455
- conversationId: state.conversationId,
456
- turnId: state.turnId,
457
- status: 'completed',
458
- });
459
- sendFn({
460
- type: 'backend_event',
461
- conversationId: state.conversationId,
462
- turnId: state.turnId,
463
- event: { type: 'compact_completed', session },
464
- });
465
- return true;
466
- }
467
- function emitCopilotCommandOutput(state, content) {
468
- if (!content.trim())
469
- return;
470
- sendFn({
471
- type: 'command_output',
472
- conversationId: state.conversationId,
473
- content,
474
- });
475
- }
476
- function emitCopilotCommandCompleted(state) {
477
- if (!state.turnActive)
478
- return;
479
- finishCopilotTurn(state);
480
- const session = sessionRef(state.copilotSessionId || '');
481
- const usage = state.deferredCommandUsage || undefined;
482
- state.deferredCommandUsage = null;
483
- sendFn({
484
- type: 'backend_event',
485
- conversationId: state.conversationId,
486
- event: { type: 'turn_completed', session, ...(usage ? { usage } : {}) },
487
- });
488
- sendFn({
489
- type: 'turn_completed',
490
- conversationId: state.conversationId,
491
- workDir: state.workDir,
492
- claudeSessionId: state.copilotSessionId,
493
- turnId: state.turnId,
494
- stateVersion: state.stateVersion,
495
- ...(usage ? { usage } : {}),
496
- });
497
- }
498
- function commandInputForDisplay(command, input) {
499
- return input.trim() ? `/${command} ${input}` : `/${command}`;
500
- }
501
- async function applyCopilotCommandResult(state, runtime, sessionId, command, input, result) {
502
- switch (result.kind) {
503
- case 'text':
504
- emitCopilotCommandOutput(state, result.text);
505
- return;
506
- case 'completed':
507
- if (result.message)
508
- emitCopilotCommandOutput(state, result.message);
509
- return;
510
- case 'select-subcommand':
511
- throw new Error(`${result.title}: ${result.options.map(option => option.name).join(', ')}`);
512
- case 'agent-prompt':
513
- if (result.notice)
514
- emitCopilotCommandOutput(state, result.notice);
515
- if (result.mode) {
516
- await runtime.setMode(sessionId, result.mode);
517
- setCopilotModeState(state, result.mode);
518
- }
519
- await runtime.send(sessionId, result.prompt, result.displayPrompt || commandInputForDisplay(command, input));
520
- }
521
- }
522
- async function requestCopilotPlanInput(state, sessionId, request) {
523
- const choices = request.choices || [];
524
- const expiresAt = Date.now() + COPILOT_PLAN_INPUT_TIMEOUT_MS;
525
- const decision = await waitForCopilotDecision(state, sessionId, {
526
- questions: [{
527
- question: request.question,
528
- header: 'Plan question',
529
- options: choices.map(choice => ({ label: choice, value: choice })),
530
- }],
531
- source: 'tool_request_user_input',
532
- presentation: 'inline',
533
- kind: 'plan_input',
534
- allowFreeform: request.allowFreeform !== false,
535
- expiresAt,
536
- timeoutAction: choices.length > 0 ? 'first_option' : 'continue_plan',
537
- }, choices);
538
- if (decision.action === 'cancel')
539
- throw new CopilotDecisionCancelledError();
540
- const answer = decision.answer || '';
541
- return {
542
- answer,
543
- wasFreeform: !choices.includes(answer),
544
- };
545
- }
546
- async function requestCopilotAcpElicitation(state, request) {
547
- if (!acp.CreateElicitationRequest.isForm(request) || !('sessionId' in request)) {
548
- return { action: 'cancel' };
549
- }
550
- const properties = Object.entries(request.requestedSchema.properties || {});
551
- const [fieldName, field] = properties[0] || [];
552
- if (!fieldName || !field)
553
- return { action: 'decline' };
554
- let choices = [];
555
- let multiSelect = false;
556
- if (acp.ElicitationPropertySchema.isString(field)) {
557
- choices = field.oneOf?.map(option => option.const) || field.enum || [];
558
- }
559
- else if (acp.ElicitationPropertySchema.isArray(field)) {
560
- multiSelect = true;
561
- choices = acp.MultiSelectItems.isTitled(field.items)
562
- ? field.items.anyOf.map(option => option.const)
563
- : (acp.MultiSelectItems.isString(field.items) ? field.items.enum : []);
564
- }
565
- else if (acp.ElicitationPropertySchema.isBoolean(field)) {
566
- choices = ['true', 'false'];
567
- }
568
- const fieldRecord = field;
569
- const fieldDescription = typeof fieldRecord.description === 'string'
570
- ? fieldRecord.description
571
- : request.message;
572
- const fieldTitle = typeof fieldRecord.title === 'string'
573
- ? fieldRecord.title
574
- : 'Plan question';
575
- const expiresAt = Date.now() + COPILOT_PLAN_INPUT_TIMEOUT_MS;
576
- const decision = await waitForCopilotDecision(state, request.sessionId, {
577
- questions: [{
578
- question: fieldDescription,
579
- header: fieldTitle,
580
- options: choices.map(choice => ({ label: choice, value: choice })),
581
- multiSelect,
582
- }],
583
- source: 'tool_request_user_input',
584
- presentation: 'inline',
585
- kind: 'plan_input',
586
- allowFreeform: choices.length === 0,
587
- expiresAt,
588
- timeoutAction: choices.length > 0 ? 'first_option' : 'continue_plan',
589
- }, choices);
590
- if (decision.action === 'cancel')
591
- return { action: 'cancel' };
592
- const raw = decision.answer || '';
593
- let value = raw;
594
- if (acp.ElicitationPropertySchema.isArray(field))
595
- value = raw ? raw.split(/,\s*/) : [];
596
- else if (acp.ElicitationPropertySchema.isBoolean(field))
597
- value = raw === 'true';
598
- else if (acp.ElicitationPropertySchema.isNumber(field)
599
- || acp.ElicitationPropertySchema.isInteger(field))
600
- value = Number(raw);
601
- return { action: 'accept', content: { [fieldName]: value } };
602
- }
603
- function copilotPlanConfirmationPrompt(content) {
604
- return /[\u3400-\u9fff]/.test(content)
605
- ? '请确认以上计划,或直接回复需要修改的内容。'
606
- : 'Please confirm the Plan above, or reply with any changes you want.';
607
- }
608
- function emitCopilotPlanReview(state, sessionId, content, languageHint = content || '') {
609
- const plan = content?.trim() || '';
610
- const prompt = copilotPlanConfirmationPrompt(languageHint);
611
- sendFn({
612
- type: 'backend_event',
613
- conversationId: state.conversationId,
614
- providerId: state.providerId,
615
- claudeSessionId: sessionId,
616
- event: {
617
- type: 'text_delta',
618
- session: sessionRef(sessionId),
619
- text: `${plan ? `\n\n${plan}\n\n` : '\n\n'}> ${prompt}`,
620
- },
621
- });
622
- }
623
- async function acceptCopilotPlanExitWithoutBlocking(state, request) {
624
- const selectedAction = request.actions.includes('exit_only')
625
- ? 'exit_only'
626
- : request.actions.includes('interactive')
627
- ? 'interactive'
628
- : null;
629
- if (!selectedAction) {
630
- return {
631
- approved: false,
632
- feedback: COPILOT_CONTINUE_PLAN_ANSWER,
633
- };
634
- }
635
- state.planModePending = false;
636
- return { approved: true, selectedAction };
637
- }
638
- export async function executeCopilotCommand(conversationId, command, input, workDir, options) {
639
- let state = conversations.get(conversationId);
640
- if (!state) {
641
- createCopilotPlaceholder(conversationId, { providerId: options?.providerId });
642
- state = conversations.get(conversationId);
643
- }
644
- if (state.turnActive || state.commandRuntime) {
645
- sendFn({
646
- type: 'backend_event',
647
- conversationId,
648
- event: { type: 'error', message: 'Copilot is already processing this conversation' },
649
- });
650
- return;
651
- }
652
- state.workDir = workDir;
653
- const providerChanged = !!options?.providerId && state.providerId !== options.providerId;
654
- if (options?.providerId)
655
- detachCopilotProviderSession(state, options.providerId);
656
- const resumeSessionId = providerChanged
657
- ? undefined
658
- : options?.resumeSessionId
659
- || state.copilotSessionId
660
- || state.lastCopilotSessionId
661
- || undefined;
662
- const normalized = normalizeCopilotRunOptions(options?.copilotOptions);
663
- if (normalized.error) {
664
- sendCopilotSetupError(state, normalized.error);
665
- return;
666
- }
667
- if (normalized.options.model)
668
- state.model = normalized.options.model;
669
- if (normalized.options.context)
670
- state.contextTier = normalized.options.context;
671
- if (normalized.options.effort)
672
- state.reasoningEffort = normalized.options.effort;
673
- state.deferredCommandUsage = null;
674
- let pendingPlanReview = null;
675
- const runtime = new CopilotCommandRuntime({
676
- providerId: state.providerId,
677
- workDir,
678
- onSessionEvent: (eventSessionId, event) => {
679
- if (eventSessionId === state.commandSessionId)
680
- handleCopilotEvent(state, event);
681
- },
682
- onUserInputRequest: command === 'plan'
683
- ? (eventSessionId, request) => requestCopilotPlanInput(state, eventSessionId, request)
684
- : undefined,
685
- onExitPlanModeRequest: command === 'plan'
686
- ? (eventSessionId, request) => {
687
- pendingPlanReview = {
688
- sessionId: eventSessionId,
689
- content: request.planContent || request.summary,
690
- languageHint: `${request.summary}\n${request.planContent || ''}`,
691
- };
692
- return acceptCopilotPlanExitWithoutBlocking(state, request);
693
- }
694
- : undefined,
695
- onAcpElicitationRequest: command === 'plan'
696
- ? request => requestCopilotAcpElicitation(state, request)
697
- : undefined,
698
- });
699
- state.commandRuntime = runtime;
700
- const previousMode = state.copilotMode;
701
- startCopilotTurn(state);
702
- const commandTurnId = state.turnId;
703
- sendFn({
704
- type: 'backend_event',
705
- conversationId,
706
- event: {
707
- type: 'turn_started',
708
- session: sessionRef(resumeSessionId || 'pending'),
709
- },
710
- });
711
- if (command === 'compact')
712
- emitCopilotCompactionStarted(state);
713
- let sessionId = null;
714
- try {
715
- await runtime.start();
716
- ensureCopilotCommandTurnActive(state, commandTurnId);
717
- const sessionOptions = {
718
- model: state.model || globalModel || undefined,
719
- contextTier: state.contextTier || undefined,
720
- reasoningEffort: state.reasoningEffort || undefined,
721
- };
722
- try {
723
- sessionId = await runtime.openSession(resumeSessionId, sessionOptions);
724
- ensureCopilotCommandTurnActive(state, commandTurnId);
725
- }
726
- catch (error) {
727
- if (!resumeSessionId || !isMissingCopilotSessionError(error))
728
- throw error;
729
- state.copilotSessionId = null;
730
- state.lastCopilotSessionId = null;
731
- state.sessionNotified = false;
732
- sessionId = await runtime.openSession(undefined, sessionOptions);
733
- ensureCopilotCommandTurnActive(state, commandTurnId);
734
- }
735
- state.commandSessionId = sessionId;
736
- state.copilotSessionId = sessionId;
737
- state.lastCopilotSessionId = sessionId;
738
- await runtime.enableAllowAll(sessionId);
739
- ensureCopilotCommandTurnActive(state, commandTurnId);
740
- if (!state.sessionNotified) {
741
- state.sessionNotified = true;
742
- sendFn({
743
- type: 'backend_event',
744
- conversationId,
745
- event: { type: 'session_started', session: sessionRef(sessionId) },
746
- });
747
- }
748
- const commands = await runtime.listSessionCommands(sessionId);
749
- ensureCopilotCommandTurnActive(state, commandTurnId);
750
- if (!commands.some(value => value.name === command)) {
751
- throw new Error(`Copilot provider ${state.providerId} does not expose /${command}`);
752
- }
753
- if (command === 'plan')
754
- state.planModePending = true;
755
- if (command === 'goal')
756
- state.planModePending = false;
757
- const result = await runtime.invokeCommand(sessionId, command, input);
758
- ensureCopilotCommandTurnActive(state, commandTurnId);
759
- await applyCopilotCommandResult(state, runtime, sessionId, command, input, result);
760
- ensureCopilotCommandTurnActive(state, commandTurnId);
761
- if (command === 'compact')
762
- emitCopilotCompactionCompleted(state);
763
- const reportedMode = await runtime.getMode(sessionId);
764
- ensureCopilotCommandTurnActive(state, commandTurnId);
765
- const mode = command === 'plan'
766
- ? (state.planModePending ? 'plan' : reportedMode)
767
- : command === 'goal'
768
- ? 'autopilot'
769
- : reportedMode;
770
- state.planModePending = mode === 'plan';
771
- setCopilotModeState(state, mode);
772
- if (command === 'plan') {
773
- const review = pendingPlanReview || {
774
- sessionId,
775
- languageHint: input,
776
- };
777
- emitCopilotPlanReview(state, review.sessionId, review.content, review.languageHint);
778
- pendingPlanReview = null;
779
- }
780
- }
781
- catch (error) {
782
- pendingPlanReview = null;
783
- if (command === 'compact')
784
- state.isCompacting = false;
785
- if (!resumeSessionId && sessionId && state.copilotSessionId === sessionId) {
786
- state.copilotSessionId = null;
787
- state.lastCopilotSessionId = null;
788
- state.sessionNotified = false;
789
- }
790
- if (command === 'plan') {
791
- state.planModePending = true;
792
- setCopilotModeState(state, 'plan');
793
- }
794
- else if (command === 'goal') {
795
- setCopilotModeState(state, previousMode);
796
- }
797
- if (!(error instanceof CopilotDecisionCancelledError) && state.turnActive) {
798
- sendFn({
799
- type: 'backend_event',
800
- conversationId,
801
- event: {
802
- type: 'error',
803
- message: error instanceof Error ? error.message : String(error),
804
- },
805
- });
806
- }
807
- }
808
- finally {
809
- if (sessionId)
810
- await runtime.destroySession(sessionId);
811
- await runtime.stop();
812
- state.commandRuntime = null;
813
- state.commandSessionId = null;
814
- state.isCompacting = false;
815
- emitCopilotCommandCompleted(state);
816
- }
817
- }
818
- export async function setCopilotPlanMode(conversationId, enabled, workDir, providerId = 'copilot') {
819
- let state = conversations.get(conversationId);
820
- if (!state) {
821
- createCopilotPlaceholder(conversationId, { providerId });
822
- state = conversations.get(conversationId);
823
- }
824
- if (state.turnActive || state.commandRuntime) {
825
- throw new Error('Copilot is already processing this conversation');
826
- }
827
- state.workDir = workDir;
828
- detachCopilotProviderSession(state, providerId);
829
- if (enabled) {
830
- state.planModePending = true;
831
- setCopilotModeState(state, 'plan');
832
- return;
833
- }
834
- state.planModePending = false;
835
- const sessionId = state.copilotSessionId || state.lastCopilotSessionId;
836
- if (!sessionId) {
837
- setCopilotModeState(state, 'interactive');
838
- return;
839
- }
840
- const runtime = new CopilotCommandRuntime({ providerId, workDir });
841
- state.commandRuntime = runtime;
842
- state.commandSessionId = sessionId;
843
- let activeSessionId = sessionId;
844
- try {
845
- await runtime.start();
846
- try {
847
- await runtime.openSession(sessionId);
848
- }
849
- catch (error) {
850
- if (!isMissingCopilotSessionError(error))
851
- throw error;
852
- state.copilotSessionId = null;
853
- state.lastCopilotSessionId = null;
854
- state.sessionNotified = false;
855
- activeSessionId = await runtime.openSession();
856
- state.copilotSessionId = activeSessionId;
857
- state.lastCopilotSessionId = activeSessionId;
858
- state.commandSessionId = activeSessionId;
859
- }
860
- await runtime.setMode(activeSessionId, 'interactive');
861
- setCopilotModeState(state, await runtime.getMode(activeSessionId));
862
- }
863
- finally {
864
- await runtime.destroySession(activeSessionId);
865
- await runtime.stop();
866
- state.commandRuntime = null;
867
- state.commandSessionId = null;
868
- }
869
- }
870
270
  export function normalizeCopilotRunOptions(input) {
871
271
  const options = {};
872
272
  if (!input)
@@ -943,30 +343,26 @@ export function handleCopilotChat(conversationId, text, workDir, options, files)
943
343
  stateVersion: 0,
944
344
  heartbeatTimer: null,
945
345
  sessionNotified: false,
946
- copilotMode: 'interactive',
947
- planMode: false,
948
- planModePending: false,
949
- isCompacting: false,
950
- commandRuntime: null,
951
- commandSessionId: null,
952
- deferredCommandUsage: null,
953
346
  };
954
347
  conversations.set(conversationId, state);
955
348
  }
956
349
  state.workDir = workDir;
957
- const providerChanged = !!options?.providerId && state.providerId !== options.providerId;
958
- if (options?.providerId)
959
- detachCopilotProviderSession(state, options.providerId);
350
+ if (options?.providerId) {
351
+ state.providerId = options.providerId;
352
+ }
960
353
  // Determine session to resume
961
- const resumeId = providerChanged
962
- ? null
963
- : options?.resumeSessionId
964
- || state.copilotSessionId
965
- || state.lastCopilotSessionId;
354
+ const resumeId = options?.resumeSessionId
355
+ || state.copilotSessionId
356
+ || state.lastCopilotSessionId;
966
357
  if (resumeId) {
967
358
  state.copilotSessionId = resumeId;
968
359
  state.lastCopilotSessionId = resumeId;
969
360
  }
361
+ // For new sessions, pre-assign a sessionId via --session-id so we know it
362
+ // immediately (don't have to wait for the `result` event).
363
+ if (!resumeId) {
364
+ state.copilotSessionId = randomUUID();
365
+ }
970
366
  const normalized = normalizeCopilotRunOptions(options?.copilotOptions);
971
367
  if (normalized.error) {
972
368
  sendCopilotSetupError(state, normalized.error);
@@ -978,19 +374,6 @@ export function handleCopilotChat(conversationId, text, workDir, options, files)
978
374
  state.contextTier = normalized.options.context;
979
375
  if (normalized.options.effort)
980
376
  state.reasoningEffort = normalized.options.effort;
981
- if (state.planModePending) {
982
- void executeCopilotCommand(conversationId, 'plan', text, workDir, {
983
- resumeSessionId: resumeId || undefined,
984
- providerId: state.providerId,
985
- copilotOptions: options?.copilotOptions,
986
- });
987
- return;
988
- }
989
- // For new one-shot sessions, pre-assign a sessionId via --session-id so we
990
- // know it immediately (the SDK command path creates its own persisted ID).
991
- if (!resumeId) {
992
- state.copilotSessionId = randomUUID();
993
- }
994
377
  let processedFiles;
995
378
  try {
996
379
  processedFiles = processFilesForCopilot(files);
@@ -1111,20 +494,7 @@ export function handleCopilotChat(conversationId, text, workDir, options, files)
1111
494
  state.child = null;
1112
495
  state.abortController = null;
1113
496
  if (state.turnActive) {
1114
- const missingResumeSession = !!resumeId
1115
- && code !== 0
1116
- && isMissingCopilotSessionError(stderrBuf);
1117
497
  finishCopilotTurn(state);
1118
- if (missingResumeSession) {
1119
- state.copilotSessionId = null;
1120
- state.lastCopilotSessionId = null;
1121
- state.sessionNotified = false;
1122
- handleCopilotChat(conversationId, text, workDir, {
1123
- providerId: state.providerId,
1124
- copilotOptions: options?.copilotOptions,
1125
- }, files);
1126
- return;
1127
- }
1128
498
  // If turn wasn't completed by a result event, emit error or cancellation
1129
499
  if (code !== 0 && code !== null) {
1130
500
  const errMsg = stderrBuf.trim() || `Copilot process exited with code ${code}`;
@@ -1202,44 +572,6 @@ function handleCopilotEvent(state, event) {
1202
572
  }
1203
573
  const session = sessionRef(state.copilotSessionId || 'pending');
1204
574
  switch (event.type) {
1205
- case 'session.mode_changed': {
1206
- const data = event.data;
1207
- if (data?.newMode) {
1208
- const mode = data.newMode;
1209
- if (state.planModePending && mode === 'interactive')
1210
- break;
1211
- setCopilotModeState(state, mode);
1212
- }
1213
- break;
1214
- }
1215
- case 'session.compaction_start': {
1216
- emitCopilotCompactionStarted(state);
1217
- break;
1218
- }
1219
- case 'session.compaction_complete': {
1220
- // Native providers can report a compaction boundary before the command
1221
- // request itself settles. Keep the synthesized row pending until
1222
- // invokeCommand succeeds so a later rejection cannot leave a false
1223
- // completed checkmark behind.
1224
- if (state.commandRuntime && state.isCompacting)
1225
- break;
1226
- emitCopilotCompactionCompleted(state);
1227
- break;
1228
- }
1229
- case 'session.compaction_failed': {
1230
- state.isCompacting = false;
1231
- const data = event.data;
1232
- sendFn({
1233
- type: 'backend_event',
1234
- conversationId: convId,
1235
- turnId: state.turnId,
1236
- event: {
1237
- type: 'error',
1238
- message: data?.error || 'Context compaction failed',
1239
- },
1240
- });
1241
- break;
1242
- }
1243
575
  case 'assistant.message_delta': {
1244
576
  const data = event.data;
1245
577
  if (data?.deltaContent) {
@@ -1330,15 +662,8 @@ function handleCopilotEvent(state, event) {
1330
662
  state.copilotSessionId = sessionId;
1331
663
  state.lastCopilotSessionId = sessionId;
1332
664
  }
1333
- const usage = buildCopilotUsage(state, event.usage);
1334
- // Native command turns can remain open after Copilot finishes generating
1335
- // while AgentLink waits for a required final Plan decision. Their outer
1336
- // command lifecycle emits the authoritative terminal event.
1337
- if (state.commandRuntime) {
1338
- state.deferredCommandUsage = usage;
1339
- break;
1340
- }
1341
665
  finishCopilotTurn(state);
666
+ const usage = buildCopilotUsage(state, event.usage);
1342
667
  // Emit backend_event turn_completed
1343
668
  sendFn({
1344
669
  type: 'backend_event',
@@ -1366,23 +691,10 @@ function handleCopilotEvent(state, event) {
1366
691
  // ── Cancel ──────────────────────────────────────────────────────────────────
1367
692
  export function cancelCopilotExecution(conversationId) {
1368
693
  const state = conversations.get(conversationId);
1369
- cancelPendingCopilotDecisions(conversationId);
1370
694
  if (!state) {
1371
695
  emitCopilotCancelled(conversationId);
1372
696
  return;
1373
697
  }
1374
- state.deferredCommandUsage = null;
1375
- if (state.commandRuntime && state.commandSessionId) {
1376
- const cancellation = state.isCompacting
1377
- ? state.commandRuntime.abortManualCompaction(state.commandSessionId)
1378
- : state.commandRuntime.abort(state.commandSessionId);
1379
- state.isCompacting = false;
1380
- void cancellation.catch(() => { });
1381
- if (state.turnActive)
1382
- finishCopilotTurn(state);
1383
- emitCopilotCancelled(conversationId, state);
1384
- return;
1385
- }
1386
698
  if (!state.child) {
1387
699
  if (state.turnActive) {
1388
700
  finishCopilotTurn(state);
@@ -1411,7 +723,7 @@ function cleanupCopilotConversation(conversationId) {
1411
723
  const state = conversations.get(conversationId);
1412
724
  if (!state)
1413
725
  return;
1414
- if (state.child || state.commandRuntime) {
726
+ if (state.child) {
1415
727
  cancelCopilotExecution(conversationId);
1416
728
  }
1417
729
  if (state.heartbeatTimer) {
@@ -1722,37 +1034,6 @@ export function readCopilotSessionMessages(_workDir, sessionId) {
1722
1034
  }
1723
1035
  return messages;
1724
1036
  }
1725
- export function readCopilotSessionMode(sessionId) {
1726
- const sessionRoot = resolve(COPILOT_SESSION_DIR);
1727
- const sessionDir = resolve(COPILOT_SESSION_DIR, sessionId);
1728
- if (sessionDir === sessionRoot || !sessionDir.startsWith(`${sessionRoot}${process.platform === 'win32' ? '\\' : '/'}`)) {
1729
- return 'interactive';
1730
- }
1731
- const eventsPath = join(sessionDir, 'events.jsonl');
1732
- if (!existsSync(eventsPath))
1733
- return 'interactive';
1734
- let mode = 'interactive';
1735
- try {
1736
- for (const line of readFileSync(eventsPath, 'utf-8').split('\n')) {
1737
- if (!line.trim())
1738
- continue;
1739
- try {
1740
- const event = JSON.parse(line);
1741
- const nextMode = event.type === 'session.mode_changed' ? event.data?.newMode : undefined;
1742
- if (nextMode === 'interactive' || nextMode === 'plan' || nextMode === 'autopilot') {
1743
- mode = nextMode;
1744
- }
1745
- }
1746
- catch {
1747
- // Ignore partial or malformed history lines.
1748
- }
1749
- }
1750
- }
1751
- catch {
1752
- return 'interactive';
1753
- }
1754
- return mode;
1755
- }
1756
1037
  export function getCopilotSessionWorkDir(sessionId) {
1757
1038
  const sessionRoot = resolve(COPILOT_SESSION_DIR);
1758
1039
  const sessionDir = resolve(COPILOT_SESSION_DIR, sessionId);