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 { IntervalTimer } from '../../../base/common/async.js';
7
>
import { Disposable } from '../../../base/common/lifecycle.js';
8
>
import { IObservable, autorun } from '../../../base/common/observable.js';
9
>
import { ILogService } from '../../log/common/log.js';
10
>
import { IAgent } from '../common/agentService.js';
11
>
12
>
/**
13
>
* How often every provider re-enumerates its model catalog.
14
>
*
15
>
* Model refreshes are otherwise edge-triggered — authentication, transport
16
>
* flips, client restarts — so without a periodic tick a model added on the
17
>
* service side stays invisible until the GitHub token happens to rotate, which
18
>
* on a long-lived window can be hours or never.
19
>
*/
20
>
export const MODEL_REFRESH_INTERVAL_MS = 10 * 60 * 1000;
21
>
22
>
/**
23
>
* Ticks {@link IAgent.refreshModels} across every registered provider on a
24
>
* fixed interval.
25
>
*
26
>
* Lives in the host rather than the client because model freshness is provider
27
>
* truth: only the provider knows its backend, holds the credential, and owns
28
>
* the stale-write and backoff policy. Keeping the timer here also means one
29
>
* tick per provider regardless of how many windows are connected, and requires
30
>
* no AHP surface — the existing `models` observable already fans a change out
31
>
* through `RootAgentsChanged` to every client.
32
>
*
33
>
* A tick is cheap when nothing changed: providers short-circuit without a
34
>
* credential, and `AgentSideEffects` compares the published `AgentInfo[]`
35
>
* before dispatching, so an unchanged catalog produces no protocol traffic.
36
>
*/
37
>
export class AgentModelRefreshScheduler extends Disposable {
38
>
39
>
private readonly _timer = this._register(new IntervalTimer());
40
>
private _agents: readonly IAgent[] = [];
41
>
private _isTimerRunning = false;
42
>
43
>
constructor(
44
>
agents: IObservable<readonly IAgent[]>,
45
>
/**
46
>
* Tick cadence, normally {@link MODEL_REFRESH_INTERVAL_MS}. Passed in
47
>
* rather than read from an overridable field because the timer is armed
48
>
* from this constructor, which runs before a subclass could install its
49
>
* own value.
50
>
*/
51
>
intervalMs: number,
52
>
@ILogService private readonly _logService: ILogService,
53
>
) {
54
>
super();
55
>
56
>
// Start ticking once there is at least one provider, and stop again if
57
>
// they all go away. Provider changes update the snapshot read by each
58
>
// tick without re-arming the timer: dynamically registering Codex must
59
>
// not postpone the next refresh for providers that were already present.
60
>
this._register(autorun(reader => {
61
>
this._agents = agents.read(reader);
62
>
if (this._agents.length === 0) {
63
this._timer.cancel();
64
this._isTimerRunning = false;
65
return;
66
}
68
>
this._timer.cancelAndSet(() => this._refreshAll(this._agents), intervalMs);
69
>
this._isTimerRunning = true;
70
>
}
71
>
}));
72
>
}
73
>
74
>
private _refreshAll(agents: readonly IAgent[]): void {
75
>
for (const agent of agents) {
76
>
if (!agent.refreshModels) {
77
continue;
78
}
80
>
this._logService.trace(`[AgentHost] Periodic model refresh for ${provider}`);
81
>
// `refreshModels` is contractually non-rejecting, but a provider bug
82
>
// must not take down the tick for the providers after it.
83
>
agent.refreshModels().catch(err => this._logService.error(err, `[AgentHost] Periodic model refresh failed for ${provider}`));
84
>
}
85
>
}
86
>
}