333
return COPILOT_SDK_TOOL_OUTPUT_BASENAME_RE.test(basename);
334
}
336
>
/**
337
>
* Options for constructing a {@link CopilotAgentSession}.
338
>
*/
339
>
export interface ICopilotAgentSessionOptions {
340
>
readonly sessionUri: URI;
341
>
readonly chatChannelUri: URI;
342
>
readonly rawSessionId: string;
343
>
readonly onDidSessionProgress: Emitter<AgentSignal>;
344
>
readonly sessionLauncher: ICopilotSessionLauncher;
345
>
readonly launchPlan: CopilotSessionLaunchPlan;
346
>
readonly shellManager: ShellManager | undefined;
347
>
/** Working directory associated with the session, used to strip redundant `cd` prefixes from shell commands. */
348
>
readonly workingDirectory?: URI;
349
>
/** Directory used to resolve workspace-scoped customizations for this session. */
350
>
readonly customizationDirectory?: URI;
351
>
/** Snapshot of the active client's tools and plugins at session creation time. */
352
>
readonly clientSnapshot?: IActiveClientSnapshot;
353
>
/**
354
>
* Looks up the AHP id of an existing child MCP customization by
355
>
* server name, so SDK MCP state events can target plugin-derived
356
>
* entries narrowly. Returns `undefined` for SDK servers that have
357
>
* no corresponding plugin entry — the session surfaces those as
358
>
* bare top-level customizations via {@link CopilotAgentSession.topLevelMcpCustomizations}.
359
>
*/
360
>
readonly resolveMcpChildId: (serverName: string) => string | undefined;
361
>
/**
362
>
* Live registry of every active client's tool contributions, shared by
363
>
* reference with the agent's per-session {@link ActiveClient}. Read at
364
>
* tool-call stamp time so a window reload (new `clientId`, identical
365
>
* tools) stamps with the current owning id, and so each tool call is
366
>
* attributed to whichever client contributed it. When omitted, a fresh
367
>
* empty registry is used (test / standalone path) and client tool calls
368
>
* are left unstamped.
369
>
*/
370
>
readonly activeClientToolSet?: ActiveClientToolSet;
371
>
/**
372
>
* Server-side host for the agent host's server tools. When provided, the
373
>
* session advertises the server tools (feedback "comments" today, more in
374
>
* the future) and exposes SDK tool handlers that execute them in-process.
375
>
*/
376
>
readonly serverToolHost?: IAgentServerToolHost;
377
>
/** Returns whether the token that launched this session is still the active account token. */
378
>
readonly isLaunchTokenCurrent?: () => boolean;
379
>
380
>
/**
381
>
* Platform used to compute the SDK sandbox policy. Defaults to
382
>
* `process.platform`; injectable so tests can exercise the per-OS gating
383
>
* (notably that the sandbox is ignored on Windows) deterministically.
384
>
*/
385
>
readonly platform?: NodeJS.Platform;
386
>
}
387
>
388
>
/**
389
>
* Lifecycle state of a {@link CopilotTurn}.
390
>
*
391
>
* - `pending` — the host has dispatched the message (`send()`), but the SDK
392
>
* has not yet emitted any event for this turn's agentic loop.
393
>
* - `running` — the SDK has emitted at least one event for this turn.
394
>
* - `completed` — the turn finished normally (the loop went idle).
395
>
* - `aborted` — the turn's loop was cancelled via an abort.
396
>
*/
397
>
type CopilotTurnState = 'pending' | 'running' | 'completed' | 'aborted';
398
>
399
>
/**
400
>
* Encapsulates all per-turn bookkeeping for a single protocol turn, plus an
401
>
* explicit lifecycle {@link CopilotTurn.state}. Holding this state on one
402
>
* object (created fresh per turn) rather than as a handful of mutable session
403
>
* fields means there is a single, atomic notion of "the current turn": there
404
>
* is no set of counters/maps that must be reset in lockstep, and turn
405
>
* transitions (running/completed/aborted) are explicit and checkable.
406
>
*
407
>
* The `pending → running` distinction guards turn completion against a stray
408
>
* idle: an abort's terminal `session.idle` finds a queued message's turn still
409
>
* `pending` (the SDK has not begun it) and leaves it open, rather than
410
>
* completing it and orphaning its real response. A non-abort idle still
411
>
* completes a `pending` turn defensively, so a degenerate no-op send cannot
412
>
* hang the session.
413
>
*/
414
>
415
>
/**
416
>
* The token/model/cost context for a single model call, used to build a
417
>
* `UsageInfo`. All fields are optional so a partial or empty context (e.g. a
418
>
* subagent usage event seen before the parent's own context) is representable.
419
>
*/
420
>
interface UsageContext {
421
>
inputTokens?: number;
422
>
outputTokens?: number;
423
>
model?: string;
424
>
cacheReadTokens?: number;
425
>
cost?: number;
426
>
}
427
>
428
>
/** Which SDK source produced an MCP lifecycle log record. */
429
>
type McpLifecycleOrigin = 'loaded' | 'statusChanged' | 'inventory';
430
>
431
>
/**
432
>
* SDK-neutral fields carried into a single MCP lifecycle log record. The
433
>
* `session.mcp_servers_loaded` event, the `session.mcp_server_status_changed`
434
>
* event, and the `rpc.mcp.list` inventory each populate the subset they carry.
435
>
*/
436
>
interface IMcpLifecycleLogInfo {
437
>
readonly name: string;
438
>
readonly status: SdkMcpServerStatus;
439
>
readonly error?: string;
440
>
readonly source?: string;
441
>
readonly transport?: string;
442
>
readonly pluginName?: string;
443
>
readonly pluginVersion?: string;
444
>
}
445
>
446
>
class CopilotTurn {
447
>
448
>
private _state: CopilotTurnState = 'pending';
449
>
private readonly _stopWatch = StopWatch.create(false);
450
>
451
>
/**
452
>
* Accumulated Copilot usage for this turn, in nano-AIU, keyed by scope.
453
>
* Scope `''` is the parent turn aggregate (parent agent calls plus every
454
>
* subagent call), so the parent turn's reported cost is the full turn
455
>
* total. Each subagent additionally accumulates under its `parentToolCallId`
456
>
* so its own component cost can be reported on the subagent's child session.
457
>
*/
458
>
readonly copilotUsageTotalNanoAiuByScope = new Map<string, number>();
459
>
460
>
/**
461
>
* The parent (main-agent) turn's own last context usage — model plus token
462
>
* counts and per-event cost. Subagent usage events are folded into the
463
>
* parent aggregate for credit purposes only, so they must not overwrite the
464
>
* parent turn's model/context-token usage. Retaining the parent's own last
465
>
* values lets each subagent usage event refresh the parent aggregate's
466
>
* credit total while preserving the model that produced the parent response.
467
>
*/
468
>
parentContextUsage: UsageContext | undefined;
469
>
470
>
/**
471
>
* Current markdown response part IDs for this turn, keyed by
472
>
* `parentToolCallId ?? ''`. Parent and subagent text stream through the
473
>
* same SDK session but land in different AHP sessions, so their markdown
474
>
* part state must not mask or append to each other.
475
>
*/
476
>
readonly markdownPartIds = new Map<string, string>();
477
>
478
>
/** Current reasoning response part IDs for this turn, keyed by `parentToolCallId ?? ''`. */
479
>
readonly reasoningPartIds = new Map<string, string>();
480
>
481
>
/**
482
>
* Per-turn tool-call aggregate accumulated across the turn's `assistant.message` rounds (main
483
>
* agent only), for the restricted `toolCallDetails` telemetry. `toolCounts` is keyed by tool name.
484
>
*/
485
>
readonly toolCounts = new Map<string, number>();
486
>
toolCallRounds = 0;
487
>
totalToolCalls = 0;
488
>
parallelToolCallRounds = 0;
489
>
parallelToolCallsTotal = 0;
490
>
/** Model of the most recent round, reported as the turn's model. */
491
>
lastModel: string | undefined;
492
>
493
>
constructor(readonly id: string, readonly ordinal: number, readonly senderClientId: string | undefined) { }
494
>
495
>
get state(): CopilotTurnState { return this._state; }
496
>
get isPending(): boolean { return this._state === 'pending'; }
497
>
get isRunning(): boolean { return this._state === 'running'; }
498
>
get duration(): number { return Math.max(0, this._stopWatch.elapsed()); }
499
>
500
>
/** Transition `pending → running` on the first SDK event. No-op once running/finished. */
501
>
markRunning(): void {
502
if (this._state === 'pending') {
503
this._state = 'running';
504
}
505
}
507
>
markCompleted(): void { this._state = 'completed'; }
508
>
markAborted(): void { this._state = 'aborted'; }
509
>
}
510
>
511
>
/**
512
>
* Encapsulates a single Copilot SDK session and all its associated bookkeeping.
513
>
*
514
>
* Created by {@link CopilotAgent}, one instance per active session. Disposing
515
>
* this class tears down all per-session resources (SDK wrapper, edit tracker,
516
>
* database reference, pending permissions).
517
>
*/
518
>
export class CopilotAgentSession extends Disposable {
519
>
readonly sessionId: string;
520
>
readonly sessionUri: URI;
521
>
private readonly _chatChannelUri: URI;
522
>
523
>
/** Working directory this session operates in, if any. */
524
>
get workingDirectory(): URI | undefined { return this._workingDirectory; }
525
>
526
>
/** Tracks active tool invocations so we can produce past-tense messages on completion. */
527
>
private readonly _activeToolCalls = new Map<string, { toolName: string; displayName: string; parameters: Record<string, unknown> | undefined; content: ToolResultContent[]; parentToolCallId: string | undefined; mcpServerName: string | undefined; meta: IToolCallMeta | undefined }>();
528
>
/**
529
>
* Maps a running subagent's `agentId` to its parent tool call id. Session-
530
>
* scoped rather than per-turn: a subagent's lifetime is bounded by its
531
>
* `subagent.started` / `subagent.completed` events (and background
532
>
* subagents can outlive the parent tool call), so this routing must not be
533
>
* cleared on turn boundaries.
534
>
*/
535
>
private readonly _parentToolCallIdsByAgentId = new Map<string, string>();
536
>
private readonly _autoApprovals = new Map<string, PermissionAutoApproval | null>();
537
>
private readonly _pendingAutoApprovals = new Map<string, DeferredPromise<PermissionAutoApproval | undefined>>();
538
>
/** Pending permission requests awaiting a renderer-side decision. */
539
>
private readonly _pendingPermissions = new Map<string, DeferredPromise<PermissionRequestResult>>();
540
>
/**
541
>
* Signatures ({@link safeStringify}) of user-approved `read`/`write`
542
>
* permission requests, keyed by tool call id. The Copilot CLI runtime emits
543
>
* two identical `permission.requested` events for a single file read or
544
>
* write (an internal `path` prompt followed by a `read`/`write` prompt), so
545
>
* without this the user would be asked to approve the same operation twice
546
>
* (issue #324477). An entry is single-use: it auto-approves exactly one
547
>
* subsequent request that is byte-identical to the approved one, then is
548
>
* removed, so approval never carries across a different tool call, a changed
549
>
* path/diff/contents, or a different kind.
550
>
*/
551
>
private readonly _approvedDuplicablePermissionSignatures = new Map<string, string>();
552
>
/** Pending user input requests awaiting a renderer-side answer. */
553
>
private readonly _pendingUserInputs = new Map<string, { deferred: DeferredPromise<{ response: ChatInputResponseKind; answers?: Record<string, ChatInputAnswer> }>; questionId: string }>();
554
>
/**
555
>
* Pending elicitation requests awaiting a renderer-side answer. Keyed
556
>
* by request id; the schema is retained so the completion handler can
557
>
* project the submitted {@link ChatInputAnswer}s back into the
558
>
* SDK's {@link ElicitationResult.content} shape.
559
>
*/
560
>
private readonly _pendingElicitations = new Map<string, {
561
>
readonly deferred: DeferredPromise<{ response: ChatInputResponseKind; answers?: Record<string, ChatInputAnswer> }>;
562
>
readonly schema: ElicitationSchema | undefined;
563
>
}>();
564
>
/**
565
>
* Pending plan-review requests originating from the CLI's
566
>
* `exitPlanMode.request` RPC. Tracked separately from
567
>
* {@link _pendingUserInputs} so the completion handler can resolve the
568
>
* RPC with a structured {@link IExitPlanModeResponse} (which the CLI
569
>
* forwards to `session.respondToExitPlanMode`) rather than feeding it
570
>
* back through the SDK's `ask_user` callback.
571
>
*/
572
>
private readonly _pendingPlanReviews = new Map<string, {
573
>
readonly actions: readonly string[];
574
>
readonly recommendedAction: string;
575
>
readonly questionId: string;
576
>
readonly deferred: DeferredPromise<IExitPlanModeResponse>;
577
>
}>();
578
>
/** File edit tracker for this session. */
579
>
private readonly _editTracker: FileEditTracker;
580
>
/** Session database reference. */
581
>
private readonly _databaseRef: IReference<ISessionDatabase>;
582
>
/** On-disk root for per-session data (database, attachments, …). */
583
>
private readonly _sessionDataDir: URI;
584
>
/**
585
>
* The current protocol turn and its per-turn bookkeeping, or `undefined`
586
>
* when the session is idle (no active turn). Replaces the former set of
587
>
* loosely-coupled per-turn fields (`_turnId`, usage counter, streaming
588
>
* part-id maps) with a single object carrying an explicit
589
>
* {@link CopilotTurn.state} lifecycle. Created (`pending`) by
590
>
* {@link resetTurnState}, finalized by {@link _completeActiveTurn}.
591
>
*/
592
>
private _currentTurn: CopilotTurn | undefined;
593
>
/** Monotonic 0-based ordinal assigned to each turn as it starts, for numeric `turnIndex` telemetry parity. */
594
>
private _nextTurnOrdinal = 0;
595
>
/**
596
>
* Protocol turn ID of the active turn, or `''` when idle. Used by file
597
>
* edit tracking and emitted on per-turn actions.
598
>
*/
599
>
private get _turnId(): string { return this._currentTurn?.id ?? ''; }
600
>
/** 0-based ordinal of the active turn within the session, or `0` when idle. */
601
>
private get _turnOrdinal(): number { return this._currentTurn?.ordinal ?? 0; }
602
>
/**
603
>
* Whether the session currently has an in-flight turn. Used by
604
>
* non-destructive idle release to avoid disconnecting mid-turn.
605
>
*/
606
>
get hasActiveTurn(): boolean { return this._currentTurn !== undefined; }
607
>
/**
608
>
* Last model id seen on the SDK's per-LLM-call `Usage` event (or a
609
>
* direct {@link setModel} call). We rely on the
610
>
* `Usage` event rather than the tool-call event itself because
611
>
* tool-call events don't carry the model id; the `Usage` event for
612
>
* an LLM turn precedes that turn's `tool_use` events.
613
>
*/
614
>
private _lastSeenModelId: string | undefined;
615
>
/** SDK session wrapper, set by {@link initializeSession}. */
616
>
private _wrapper!: CopilotSessionWrapper;
617
>
private readonly _slashCommandProvider: CopilotSlashCommandProvider;
618
>
/** Last agent mode pushed to the SDK via {@link applyMode}, to elide redundant `rpc.mode.set` calls. */
619
>
private _lastAppliedMode: CopilotSdkMode | undefined;
620
>
private _lastAppliedPermissionMode: PermissionAllowAllMode | undefined;
621
>
private _autoApprovalExperimentalModeEnabled = false;
622
>
private readonly _permissionModeSequencer = new Sequencer();
623
>
private readonly _steeringMessagesInFlight = new Set<string>();
624
>
/**
625
>
* Steering messages that have been accepted by the SDK but not yet
626
>
* surfaced to the chat UI as a separate user message. When the SDK
627
>
* echoes a steering through a `user.message` event whose `content`
628
>
* matches one of these entries, we finalize the in-flight turn and
629
>
* dispatch a new {@link ActionType.ChatTurnStarted} whose
630
>
* `userMessage` is the steering content. The reducer also removes
631
>
* the pending steering via the action's `queuedMessageId`.
632
>
*
633
>
* Entries left here at abort/dispose time are flushed as
634
>
* `steering_consumed` signals so the chat UI's pending state still
635
>
* clears in cleanup paths where we never observe the echo.
636
>
*/
637
>
private readonly _pendingSteeringFlips = new Map<string, PendingMessage>();
638
>
639
>
/** Snapshot captured at session creation for refresh detection. */
640
>
private readonly _appliedSnapshot: IActiveClientSnapshot;
641
>
/**
642
>
* Live owning-client identity, read at tool-call stamp time so a window
643
>
* reload that re-pushes identical tools with a new `clientId` stamps
644
>
* subsequent client tool calls with the current id rather than the one
645
>
* frozen into {@link _appliedSnapshot}.
646
>
*/
647
>
private readonly _activeClientToolSet: ActiveClientToolSet;
648
>
/** Tool names that are client-provided, derived from snapshot. */
649
>
private readonly _clientToolNames: ReadonlySet<string>;
650
>
/** Launch-time tool-search decision; kept stable for the lifetime of the SDK session. */
651
>
private readonly _toolSearchActive: boolean;
652
>
/** Deferred promises for pending client tool calls, keyed by toolCallId. */
653
>
private readonly _pendingClientToolCalls = new PendingRequestRegistry<ToolResultObject>();
654
>
/** Pending SDK MCP auth handler promises, keyed by SDK auth request id. */
655
>
private readonly _pendingMcpAuthRequests = new Map<string, IPendingMcpAuthRequest>();
656
>
/** `pending-edit-content:` URIs written during permission requests, keyed
657
>
* by toolCallId. Cleaned up when the permission resolves or the session
658
>
* is disposed. */
659
>
private readonly _pendingEditContentUris = new Map<string, URI>();
660
>
661
>
private readonly _onDidSessionProgress: Emitter<AgentSignal>;
662
>
private readonly _sessionLauncher: ICopilotSessionLauncher;
663
>
private readonly _launchPlan: CopilotSessionLaunchPlan;
664
>
private readonly _isLaunchTokenStillCurrent: () => boolean;
665
>
private readonly _shellManager: ShellManager | undefined;
666
>
/** Streams runtime-executed shell output into output-only (non-pty) terminal channels. */
667
>
private readonly _nonPtyShellTerminals: NonPtyShellTerminalStreams;
668
>
private readonly _workingDirectory: URI | undefined;
669
>
private readonly _customizationDirectory: URI | undefined;
670
>
private readonly _serverToolHost: IAgentServerToolHost | undefined;
671
>
/** Bridges SDK-reported MCP server state into AHP customization actions. */
672
>
private readonly _mcpCustomizations: McpCustomizationController;
673
>
674
>
private get _storageUri(): URI {
675
return isDefaultChatUri(this._chatChannelUri) ? this.sessionUri : this._chatChannelUri;
676
}
678
>
/**
679
>
* Fans MCP server notifications (today: `notifications/tools/list_changed`)
680
>
* up to the agent and on to the protocol server. Fired by the
681
>
* `onToolsUpdated` listener once per ready MCP channel.
682
>
*/
683
>
private readonly _onMcpNotification = this._register(new Emitter<IMcpNotification>());
684
>
readonly onMcpNotification = this._onMcpNotification.event;
685
>
686
>
/**
687
>
* Pending MCP `sampling/createMessage` requests received over the
688
>
* AHP `mcp://` channel, keyed by the cancellation handle we passed
689
>
* into {@link rpc.mcp.executeSampling}. Tracked so that session
690
>
* teardown can issue a best-effort
691
>
* {@link rpc.mcp.cancelSamplingExecution} for each one instead of
692
>
* leaving the SDK-side promise (and the upstream App) hanging.
693
>
*/
694
>
private readonly _pendingMcpSamplings = new Set<string>();
695
>
696
>
/** Tracks whether a non-empty activity has been published, so we only emit a clear when needed. */
697
>
private _hasActivity = false;
698
>
699
>
/**
700
>
* Last SDK-reported MCP status logged for each server (keyed by server
701
>
* name). Used to suppress duplicate lifecycle log records when the SDK
702
>
* re-reports an unchanged status — the `rpc.mcp.list` seed and the
703
>
* `session.mcp_servers_loaded` event routinely carry the same snapshot.
704
>
*/
705
>
private readonly _lastLoggedMcpStatus = new Map<string, SdkMcpServerStatus>();
706
>
707
>
/** Platform used to compute the SDK sandbox policy (injectable for tests). */
708
>
private readonly _platform: NodeJS.Platform;
709
>
710
>
get mcpServerStates() {
711
return this._mcpCustomizations.runtimeStates;
712
}
714
>
/** Stateless reporter used to emit restricted GH/MSFT telemetry for this session's model calls. */
715
>
private readonly _telemetryReporter: AgentHostTelemetryReporter;
716
>
private readonly _repoInfoTelemetry: AgentHostRepoInfoTelemetry;
717
>
private _activeRepoInfoTurn: {
718
>
readonly telemetryMessageId: string;
719
>
cancelled: boolean;
720
>
begin: Promise<{ readonly context: IAgentHostRestrictedTelemetryContext; readonly baseBranch: string | undefined } | undefined>;
721
>
} | undefined;
722
>
723
>
constructor(
724
options: ICopilotAgentSessionOptions,
725
@IInstantiationService private readonly _instantiationService: IInstantiationService,