-
Notifications
You must be signed in to change notification settings - Fork 41.5k
agentHost: make the orchestrator own session enumeration and chat lifecycle #329633
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
Sandeep Somavarapu (sandy081)
wants to merge
37
commits into
main
Choose a base branch
from
agents/agent-host-i3-removal
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
b0d2905
agentHost: relocate Session ownership into the orchestrator (T2/T4)
sandy081 0068f98
agentHost: drop orchestratorOwnsSession opt-in; chat surface is the s…
sandy081 a0118d5
agentHost: trim verbose comments on the T2/T4 session-ownership code
sandy081 43997a1
agentHost: drop redundant session-typed methods from IAgent (Category C)
sandy081 5613615
agentHost: rename IAgent.getSessionMetadata to getConversationMetadata
sandy081 cd3a7e7
Merge remote-tracking branch 'origin/main' into agents/multi-chat-ses…
sandy081 9608b87
agentHost: clarify IAgentConversationMetadata._meta is session-generi…
sandy081 b79c32a
agentHost: clarify Claude disposeSession takes the agent's own SDK se…
sandy081 4534471
agentHost: document the AH-session vs SDK-session terminology convention
sandy081 738eb38
agentHost: make IAgent enumeration session-keyed (orchestrator owns s…
sandy081 7a8523b
agentHost: stop agents synthesizing default-chat URIs at runtime
sandy081 d0211eb
agents: reuse orchestrator default-chat URI on the provision path
sandy081 335ed86
agentHost: flatten provider chat bindings
sandy081 13f5d62
Merge remote-tracking branch 'origin/main' into agents/multi-chat-ses…
sandy081 fac8ed5
agentHost: make chat lifecycle exact and retry-safe
sandy081 fbf6ac1
agentHost: restore subagent transcripts via the chat-surface getMessages
sandy081 db460de
agentHost: orchestrator owns session provisioning; agents only create…
sandy081 769991b
agentHost: enable multi-chat for Codex (base for I3-removal branch)
sandy081 e17c4ea
agentHost: add orchestrator-owned session registry (I3 removal stage 1)
sandy081 e3aa0b1
agentHost: enumerate sessions from the registry, not providers (I3 re…
sandy081 a7a36bd
agentHost: clarify Codex is already I3-decoupled (I3 removal stage 3a)
sandy081 b6ced95
agentHost: collapse fresh-session provisioning into chats.createSessi…
sandy081 7b9bed8
agentHost: implement chats.createSessionChat in Copilot and Codex (I3…
sandy081 9a2e26a
Merge remote-tracking branch 'origin/main' into agents/agent-host-i3-…
sandy081 41c3f40
Merge remote-tracking branch 'origin/main' into agents/agent-host-i3-…
sandy081 dfe5369
Merge remote-tracking branch 'origin/main' into agents/agent-host-i3-…
sandy081 623ccb4
Merge remote-tracking branch 'origin/main' into agents/agent-host-i3-…
sandy081 df662f2
Merge remote-tracking branch 'origin/main' into agents/agent-host-i3-…
sandy081 4df1a9e
Merge remote-tracking branch 'origin/main' into agents/agent-host-i3-…
sandy081 51de748
Merge remote-tracking branch 'origin/main' into agents/agent-host-i3-…
sandy081 5674540
Merge remote-tracking branch 'origin/main' into agents/agent-host-i3-…
sandy081 e8dea46
Merge remote-tracking branch 'origin/main' into agents/agent-host-i3-…
sandy081 9565089
agentHost: enable Codex multi-chat capability
sandy081 31aa3e9
agentHost: allow recording gated multi-chat E2E tests
sandy081 c219fb3
agentHost: harden session registry and chat lifecycle
sandy081 cc04df4
Merge remote-tracking branch 'origin/main' into agents/agent-host-i3-…
sandy081 34d3e45
agentHost: remove Claude default-chat URI inference
sandy081 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| /*--------------------------------------------------------------------------------------------- | ||
| * Copyright (c) Microsoft Corporation. All rights reserved. | ||
| * Licensed under the MIT License. See License.txt in the project root for license information. | ||
| *--------------------------------------------------------------------------------------------*/ | ||
|
|
||
| export { decodeProviderData, encodeProviderData } from './agentPeerChats.js'; | ||
| export type { IPersistedChat, IPersistedSideChat } from './agentPeerChats.js'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,210 @@ | ||
| /*--------------------------------------------------------------------------------------------- | ||
| * Copyright (c) Microsoft Corporation. All rights reserved. | ||
| * Licensed under the MIT License. See License.txt in the project root for license information. | ||
| *--------------------------------------------------------------------------------------------*/ | ||
|
|
||
| import { Disposable, IReference, toDisposable } from '../../../base/common/lifecycle.js'; | ||
| import { URI } from '../../../base/common/uri.js'; | ||
| import { ILogService } from '../../log/common/log.js'; | ||
| import { AgentProvider } from '../common/agentService.js'; | ||
| import { ISessionDatabase, ISessionDataService } from '../common/sessionDataService.js'; | ||
|
|
||
| /** A session recorded in the orchestrator-owned {@link AgentSessionRegistry}. */ | ||
| export interface IRegisteredSession { | ||
| readonly session: URI; | ||
| readonly provider: AgentProvider; | ||
| /** Session creation time (ms since epoch) as first observed by the orchestrator. */ | ||
| readonly startTime: number; | ||
| } | ||
|
|
||
| interface IPersistedRegistryEntry { | ||
| readonly provider: AgentProvider; | ||
| readonly startTime: number; | ||
| } | ||
|
|
||
| /** The persisted registry blob. */ | ||
| interface IPersistedRegistry { | ||
| readonly version: 1; | ||
| /** Whether the one-time provider backfill has run for this host. */ | ||
| readonly backfilled: boolean; | ||
| readonly sessions: Record<string, IPersistedRegistryEntry>; | ||
| } | ||
|
|
||
| interface IRegistryState { | ||
| readonly sessions: Map<string, IPersistedRegistryEntry>; | ||
| backfilled: boolean; | ||
| } | ||
|
|
||
| /** | ||
| * The reserved URI whose per-session database backs the registry index. Its | ||
| * scheme (`agent-host-registry`) cannot collide with a real session URI, which | ||
| * always carries a provider scheme (`copilot`/`claude`/`codex`/`copilotcli`). | ||
| */ | ||
| const REGISTRY_URI = URI.from({ scheme: 'agent-host-registry', path: '/sessions' }); | ||
| const REGISTRY_METADATA_KEY = 'sessionRegistry'; | ||
|
|
||
| /** | ||
| * A durable, orchestrator-owned index of the sessions that exist, keyed by | ||
| * session URI. Unlike the agents' `listSessions()` (which enumerates their own | ||
| * SDK sessions/threads and maps them to session URIs via invariant I3), this | ||
| * registry is authoritative on the AH side and does not depend on the agent | ||
| * exposing a session whose SDK id equals the session id. | ||
| * | ||
| * Persisted as a single JSON blob in a reserved session database, with | ||
| * serialized read-modify-write (mirroring the peer-chat catalog) so concurrent | ||
| * register/unregister calls never clobber each other. | ||
| * | ||
| * Stage 1 (this component) is purely additive: it is populated alongside the | ||
| * existing create/delete paths and validated against the live `listSessions` | ||
| * output, but does NOT yet drive enumeration. | ||
| */ | ||
| export class AgentSessionRegistry extends Disposable { | ||
|
|
||
| private _dbRef: IReference<ISessionDatabase> | undefined; | ||
| /** In-memory mirror of the persisted index; the source of truth once loaded. */ | ||
| private _cache: Map<string, IPersistedRegistryEntry> | undefined; | ||
| /** Whether the one-time provider backfill has run; part of the persisted blob. */ | ||
| private _backfilled = false; | ||
| /** Serializes read-modify-write of the persisted blob. */ | ||
| private _writeChain: Promise<void> = Promise.resolve(); | ||
| private _loadPromise: Promise<Map<string, IPersistedRegistryEntry>> | undefined; | ||
|
|
||
| constructor( | ||
| private readonly _sessionDataService: ISessionDataService, | ||
| private readonly _logService: ILogService, | ||
| ) { | ||
| super(); | ||
| this._register(toDisposable(() => this._dbRef?.dispose())); | ||
| } | ||
|
|
||
| /** Record (or refresh) a session in the registry. Idempotent per session URI. */ | ||
| async register(session: URI, provider: AgentProvider, startTime: number): Promise<void> { | ||
| await this._enqueueWrite(state => { | ||
| const key = session.toString(); | ||
| const existing = state.sessions.get(key); | ||
| // Preserve the first-observed startTime so a later re-register (e.g. | ||
| // a reconnect issuing createSession again) never rewrites it. | ||
| state.sessions.set(key, { provider, startTime: existing?.startTime ?? startTime }); | ||
| }); | ||
| } | ||
|
|
||
| /** Remove a session from the registry (true delete). No-op if absent. */ | ||
| async unregister(session: URI): Promise<void> { | ||
| await this._enqueueWrite(state => { | ||
| state.sessions.delete(session.toString()); | ||
| }); | ||
| } | ||
|
|
||
| /** Every session currently recorded, in no particular order. */ | ||
| async list(): Promise<IRegisteredSession[]> { | ||
| const cache = await this._load(); | ||
| const result: IRegisteredSession[] = []; | ||
| for (const [key, entry] of cache) { | ||
| result.push({ session: URI.parse(key), provider: entry.provider, startTime: entry.startTime }); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| /** Whether the registry has ever been populated (used to gate one-time backfill). */ | ||
| async isEmpty(): Promise<boolean> { | ||
| return (await this._load()).size === 0; | ||
| } | ||
|
|
||
| /** | ||
| * Whether the one-time provider backfill has completed for this host. Gated | ||
| * by a persisted marker rather than emptiness so a registry that a | ||
| * `createSession` has already populated is still backfilled from the legacy | ||
| * provider enumeration exactly once. | ||
| */ | ||
| async isBackfilled(): Promise<boolean> { | ||
| await this._load(); | ||
| return this._backfilled; | ||
| } | ||
|
|
||
| /** Records that the one-time provider backfill has completed. */ | ||
| async markBackfilled(): Promise<void> { | ||
| await this._enqueueWrite(state => { | ||
| state.backfilled = true; | ||
| }); | ||
| } | ||
|
|
||
| private _enqueueWrite(mutate: (state: IRegistryState) => void): Promise<void> { | ||
| const next = this._writeChain | ||
| .catch(() => { /* a failed prior write must not block later ones */ }) | ||
| .then(async () => { | ||
| const cache = await this._load(); | ||
| const state: IRegistryState = { | ||
| sessions: new Map(cache), | ||
| backfilled: this._backfilled, | ||
| }; | ||
| mutate(state); | ||
| await this._persist(state); | ||
| this._cache = state.sessions; | ||
| this._backfilled = state.backfilled; | ||
| }); | ||
| this._writeChain = next.catch(() => { /* keep the chain alive */ }); | ||
| return next; | ||
| } | ||
|
|
||
| private _load(): Promise<Map<string, IPersistedRegistryEntry>> { | ||
| if (this._cache) { | ||
| return Promise.resolve(this._cache); | ||
| } | ||
| if (!this._loadPromise) { | ||
| const load = this._doLoad(); | ||
| this._loadPromise = load; | ||
| void load.then(undefined, () => { | ||
| if (this._loadPromise === load) { | ||
| this._loadPromise = undefined; | ||
| } | ||
| }); | ||
| } | ||
| return this._loadPromise; | ||
| } | ||
|
|
||
| private async _doLoad(): Promise<Map<string, IPersistedRegistryEntry>> { | ||
| const cache = new Map<string, IPersistedRegistryEntry>(); | ||
| try { | ||
| const raw = await this._db().getMetadata(REGISTRY_METADATA_KEY); | ||
| if (raw !== undefined) { | ||
| const parsed = JSON.parse(raw) as Partial<IPersistedRegistry>; | ||
| if (parsed && typeof parsed === 'object') { | ||
| this._backfilled = parsed.backfilled === true; | ||
| const sessions = parsed.sessions; | ||
| if (sessions && typeof sessions === 'object') { | ||
| for (const [key, value] of Object.entries(sessions)) { | ||
| const entry = value as Partial<IPersistedRegistryEntry>; | ||
| if (entry && typeof entry.provider === 'string' && typeof entry.startTime === 'number') { | ||
| cache.set(key, { provider: entry.provider, startTime: entry.startTime }); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } catch (err) { | ||
| this._logService.warn(`[AgentSessionRegistry] Failed to load registry: ${err instanceof Error ? err.message : String(err)}`); | ||
| throw err; | ||
| } | ||
| this._cache = cache; | ||
| return cache; | ||
| } | ||
|
|
||
| private async _persist(state: IRegistryState): Promise<void> { | ||
| const sessions: Record<string, IPersistedRegistryEntry> = {}; | ||
| for (const [key, entry] of state.sessions) { | ||
| sessions[key] = entry; | ||
| } | ||
| const blob: IPersistedRegistry = { version: 1, backfilled: state.backfilled, sessions }; | ||
| try { | ||
| await this._db().setMetadata(REGISTRY_METADATA_KEY, JSON.stringify(blob)); | ||
| } catch (err) { | ||
| this._logService.warn(`[AgentSessionRegistry] Failed to persist registry: ${err instanceof Error ? err.message : String(err)}`); | ||
| throw err; | ||
| } | ||
| } | ||
|
|
||
| private _db(): ISessionDatabase { | ||
| this._dbRef ??= this._sessionDataService.openDatabase(REGISTRY_URI); | ||
| return this._dbRef.object; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.