1
>
/*---------------------------------------------------------------------------------------------
agentHostLocalTurns.ts
2
>
* Copyright (c) Microsoft Corporation. All rights reserved.
3
>
* Licensed under the MIT License. See License.txt in the project root for license information.
4
>
*--------------------------------------------------------------------------------------------*/
5
>
6
>
import type { IReference } from '../../../base/common/lifecycle.js';
7
>
import { URI } from '../../../base/common/uri.js';
8
>
import { ILogService } from '../../log/common/log.js';
9
>
import type { ILocalTurnRecord, ISessionDatabase, ISessionDataService } from '../common/sessionDataService.js';
10
>
import type { Turn } from '../common/state/sessionState.js';
11
>
12
>
/**
13
>
* Tracks host-injected ("local") turns — completed protocol turns the agent SDK
14
>
* never saw, such as the `/rename` acknowledgement or a `!command` terminal run.
15
>
*
16
>
* These turns exist only in the agent host: they are never forwarded to the
17
>
* agent SDK, so they are absent from the SDK transcript that
18
>
* {@link AgentService} replays on restore. This registry persists them (so they
19
>
* survive reload) and remembers, for each, the id of the preceding concrete
20
>
* (SDK-backed) turn — the *anchor* — so that fork/truncate operations targeting
21
>
* a local turn can be redirected to the concrete SDK message before it.
22
>
*
23
>
* Everything is scoped to a **chat** (its channel URI): a session's default
24
>
* chat and each of its peer chats are handled identically. Persistence lives in
25
>
* the owning session's database (one per session, shared across its chats),
26
>
* discriminated by {@link ILocalTurnRecord.chatUri}.
27
>
*/
28
>
export class AgentHostLocalTurns {
29
>
30
>
/** chat URI → (localTurnId → { anchorTurnId, seq }). */
31
>
private readonly _byChat = new Map<string, Map<string, { readonly anchorTurnId: string | undefined; readonly seq: number }>>();
32
>
/** session URI → highest `seq` assigned so far (seq is session-global for stable ordering). */
33
>
private readonly _seqBySession = new Map<string, number>();
34
>
35
>
constructor(
36
private readonly _sessionDataService: ISessionDataService,
37
private readonly _logService: ILogService,
38
) { }
40
>
/** Whether `turnId` is a known host-injected local turn in `chat`. */
41
>
isLocal(chat: string, turnId: string): boolean {
42
return this._byChat.get(chat)?.has(turnId) ?? false;
43
}
45
>
/** All known local turn ids for `chat`. */
46
>
getLocalTurnIds(chat: string): string[] {
47
const map = this._byChat.get(chat);
48
return map ? [...map.keys()] : [];
49
}
51
>
/**
52
>
* Resolves `turnId` to the concrete (SDK-backed) turn a fork/truncate should
53
>
* operate on within `chat`. For a local turn this is its anchor (the
54
>
* preceding real turn, or `undefined` when it precedes any real turn); for a
55
>
* concrete turn it is the turn itself.
56
>
*/
57
>
resolveConcreteTurnId(chat: string, turnId: string): string | undefined {
58
const entry = this._byChat.get(chat)?.get(turnId);
59
return entry ? entry.anchorTurnId : turnId;
60
}
62
>
/**
63
>
* Persist a local turn and remember it in memory. `anchorTurnId` is the id
64
>
* of the preceding concrete turn in `chat` (or `undefined` when there is
65
>
* none). `session` identifies the database to persist into.
66
>
*/
67
>
record(session: string, chat: string, turn: Turn, anchorTurnId: string | undefined): void {
68
const seq = (this._seqBySession.get(session) ?? 0) + 1;
69
this._noteInMemory(session, chat, turn.id, anchorTurnId, seq);