@ai-workbench/remote-agent 0.8.1 → 0.8.5
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/README.md +19 -4
- package/dist/acp.d.ts +25 -0
- package/dist/acp.js +159 -17
- package/dist/approval-label.d.ts +6 -0
- package/dist/approval-label.js +45 -0
- package/dist/auth-reason.d.ts +1 -0
- package/dist/auth-reason.js +27 -0
- package/dist/broker-runtime.d.ts +10 -0
- package/dist/broker-runtime.js +302 -0
- package/dist/broker.d.ts +29 -6
- package/dist/broker.js +141 -141
- package/dist/connect-contract.d.ts +2 -1
- package/dist/conversation-details.d.ts +1 -0
- package/dist/conversation-details.js +58 -8
- package/dist/index.d.ts +31 -14
- package/dist/index.js +248 -42
- package/dist/input.d.ts +4 -0
- package/dist/input.js +4 -1
- package/dist/login-diagnostic.d.ts +38 -0
- package/dist/login-diagnostic.js +29 -0
- package/dist/model-catalog.d.ts +55 -0
- package/dist/model-catalog.js +254 -0
- package/dist/model-configuration.d.ts +51 -0
- package/dist/model-configuration.js +212 -0
- package/dist/presentation.js +4 -0
- package/dist/protocol-boundary.d.ts +13 -0
- package/dist/protocol-boundary.js +49 -0
- package/dist/provider.d.ts +21 -3
- package/dist/provider.js +135 -41
- package/dist/readiness.js +7 -5
- package/dist/recovery-checkpoint.d.ts +6 -0
- package/dist/recovery-checkpoint.js +71 -0
- package/dist/recovery.d.ts +34 -0
- package/dist/recovery.js +52 -0
- package/dist/remote-details.d.ts +17 -1
- package/dist/remote-details.js +137 -6
- package/dist/remote.d.ts +3 -0
- package/dist/remote.js +2 -0
- package/dist/rpc-namespace.d.ts +3 -0
- package/dist/rpc-namespace.js +50 -0
- package/dist/stage-timing.d.ts +27 -0
- package/dist/stage-timing.js +39 -0
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@ import { SessionId } from '@deepseek-ai/dsh-session';
|
|
|
2
2
|
import Schema from '@deepseek-ai/schemastery';
|
|
3
3
|
import { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { createHash } from 'node:crypto';
|
|
5
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
6
|
+
import { recoveryJournal, recoveryKey } from "./recovery.js";
|
|
6
7
|
import * as unstable from '@agentclientprotocol/sdk/unstable';
|
|
7
8
|
import * as acp from '@agentclientprotocol/sdk';
|
|
8
9
|
import { AcpSession } from "./acp.js";
|
|
@@ -17,6 +18,11 @@ import { NeedError } from "./remote.js";
|
|
|
17
18
|
import { employeeFailure } from "./presentation.js";
|
|
18
19
|
import { createRemoteDetails } from "./remote-details.js";
|
|
19
20
|
import { PROVIDER_NAMES, ProviderLocaleRemote } from "./provider-locale.js";
|
|
21
|
+
import { approvalLabel } from "./approval-label.js";
|
|
22
|
+
import { StageTiming } from "./stage-timing.js";
|
|
23
|
+
import { verifyRecoveryCheckpoint, currentInputIds } from "./recovery-checkpoint.js";
|
|
24
|
+
import { ModelCatalog, modelInfo } from "./model-catalog.js";
|
|
25
|
+
import { ModelConfigurationError } from "./model-configuration.js";
|
|
20
26
|
export const name = 'ai-workbench-remote-agent';
|
|
21
27
|
export const settingsNamespace = 'ai-workbench-codex';
|
|
22
28
|
// L can be installed/enabled after X; keeping X mounted lets it explain the prerequisite.
|
|
@@ -55,34 +61,97 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
55
61
|
providerName;
|
|
56
62
|
agent;
|
|
57
63
|
readImage;
|
|
64
|
+
reportLoginFailure;
|
|
65
|
+
directory;
|
|
58
66
|
config;
|
|
59
67
|
sessions = new Map();
|
|
60
68
|
turns = new Map();
|
|
61
69
|
lifetime = new AbortController();
|
|
62
|
-
|
|
70
|
+
shuttingDown = false;
|
|
71
|
+
detached = new Set();
|
|
72
|
+
cleanups = new WeakMap();
|
|
73
|
+
constructor(config, resolveRuntime, approve, providerName = () => PROVIDER_NAMES.en, agent = CODEX_AGENT, readImage, reportLoginFailure, directory) {
|
|
63
74
|
super();
|
|
64
75
|
this.resolveRuntime = resolveRuntime;
|
|
65
76
|
this.approve = approve;
|
|
66
77
|
this.providerName = providerName;
|
|
67
78
|
this.agent = agent;
|
|
68
79
|
this.readImage = readImage;
|
|
80
|
+
this.reportLoginFailure = reportLoginFailure;
|
|
81
|
+
this.directory = directory;
|
|
69
82
|
this.config = typeof config === 'function' ? config : () => config;
|
|
70
83
|
}
|
|
71
84
|
providerInfo(provider) { return { id: provider, name: this.providerName() }; }
|
|
72
|
-
|
|
85
|
+
catalog(signal) { if (!this.directory)
|
|
86
|
+
throw new LlmError('无法获取模型列表:缺少可信主机目录能力', 'MODEL_CATALOG_UNAVAILABLE'); return this.directory.read(this.agent, signal); }
|
|
87
|
+
async listModels(provider) { const catalog = await this.catalog(); try {
|
|
88
|
+
return catalog.models.map(model => modelInfo(provider, model));
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
catalog.release();
|
|
92
|
+
} }
|
|
93
|
+
async resolveModel(provider, id, signal) { const catalog = await this.catalog(signal); try {
|
|
94
|
+
const model = catalog.models.find(model => model.id === id);
|
|
95
|
+
if (!model)
|
|
96
|
+
throw new LlmError('无法获取模型列表:所选模型已不在当前主机目录中', 'MODEL_CATALOG_UNAVAILABLE');
|
|
97
|
+
return modelInfo(provider, model);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
catalog.release();
|
|
101
|
+
} }
|
|
102
|
+
async invalidateModelState() { for (const turn of this.turns.values())
|
|
103
|
+
turn.abort(); await Promise.all([...this.sessions.keys()].map(id => this.disposeSession(id))); }
|
|
73
104
|
providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
|
|
105
|
+
cleanupRuntime(runtime) {
|
|
106
|
+
const existing = this.cleanups.get(runtime);
|
|
107
|
+
if (existing)
|
|
108
|
+
return existing;
|
|
109
|
+
const cleanup = Promise.resolve().then(() => runtime.dispose?.()).catch(() => {
|
|
110
|
+
const failure = new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '原主机会话的清理结果未确认,不能继续或重开此会话', '管理员核对并结束原主机会话');
|
|
111
|
+
for (const live of this.sessions.values())
|
|
112
|
+
if (live.runtime === runtime) {
|
|
113
|
+
live.cleanupFailure = failure;
|
|
114
|
+
live.failure ??= { cause: failure };
|
|
115
|
+
}
|
|
116
|
+
throw failure;
|
|
117
|
+
});
|
|
118
|
+
this.cleanups.set(runtime, cleanup);
|
|
119
|
+
return cleanup;
|
|
120
|
+
}
|
|
74
121
|
async disposeSession(sessionId) {
|
|
75
122
|
this.turns.get(sessionId)?.abort();
|
|
76
123
|
const current = this.sessions.get(sessionId);
|
|
77
124
|
if (!current)
|
|
78
125
|
return;
|
|
79
|
-
|
|
126
|
+
if (current.disposal)
|
|
127
|
+
return current.disposal;
|
|
80
128
|
current.session.close();
|
|
81
|
-
|
|
129
|
+
current.disposal = this.cleanupRuntime(current.runtime).then(() => { if (this.sessions.get(sessionId) === current)
|
|
130
|
+
this.sessions.delete(sessionId); });
|
|
131
|
+
return current.disposal;
|
|
132
|
+
}
|
|
133
|
+
async detachSession(sessionId) {
|
|
134
|
+
const current = this.sessions.get(sessionId), turn = this.turns.get(sessionId);
|
|
135
|
+
if (!current && !turn)
|
|
136
|
+
return;
|
|
137
|
+
this.detached.add(sessionId);
|
|
138
|
+
if (current) {
|
|
139
|
+
try {
|
|
140
|
+
await current.runtime.detach?.();
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
current.session.close();
|
|
144
|
+
this.sessions.delete(sessionId);
|
|
145
|
+
turn?.abort();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else
|
|
149
|
+
turn?.abort();
|
|
82
150
|
}
|
|
83
151
|
async dispose() {
|
|
152
|
+
this.shuttingDown = true;
|
|
153
|
+
const results = await Promise.allSettled([...new Set([...this.sessions.keys(), ...this.turns.keys()])].map(id => this.detachSession(id)));
|
|
84
154
|
this.lifetime.abort();
|
|
85
|
-
const results = await Promise.allSettled([...this.sessions.keys()].map(id => this.disposeSession(id)));
|
|
86
155
|
const failure = results.find(result => result.status === 'rejected');
|
|
87
156
|
if (failure?.status === 'rejected')
|
|
88
157
|
throw failure.reason;
|
|
@@ -92,10 +161,22 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
92
161
|
yield* this.generate(options);
|
|
93
162
|
}
|
|
94
163
|
catch (error) {
|
|
164
|
+
if (error instanceof ModelConfigurationError)
|
|
165
|
+
throw new LlmError(error.message, error.code, { cause: error });
|
|
95
166
|
if (error instanceof NeedError) {
|
|
96
167
|
const rpc = /RPC 错误码 (-?\d+)/u.exec(error.requires)?.[1];
|
|
97
168
|
const message = employeeFailure(error.code) + (rpc ? `(技术详情:RPC 错误码 ${rpc})` : '');
|
|
98
|
-
|
|
169
|
+
let diagnostic = error.technical ? JSON.stringify(error.technical) : '';
|
|
170
|
+
if (error.technical && this.reportLoginFailure) {
|
|
171
|
+
try {
|
|
172
|
+
const receipt = await this.reportLoginFailure(options.sessionId, error.code, error.technical);
|
|
173
|
+
diagnostic += ';诊断 eventId=' + receipt.eventId;
|
|
174
|
+
}
|
|
175
|
+
catch (failure) {
|
|
176
|
+
diagnostic += ';诊断保存失败:' + (failure instanceof NeedError ? failure.code : 'DIAGNOSTIC_WRITE_FAILED');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
throw new LlmError(message, error.code, { cause: new Error(`诊断代码:${error.code};${error.requires}` + (diagnostic ? ';' + diagnostic : '')) });
|
|
99
180
|
}
|
|
100
181
|
throw error;
|
|
101
182
|
}
|
|
@@ -113,12 +194,16 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
113
194
|
this.turns.set(key, turn);
|
|
114
195
|
const signal = AbortSignal.any([this.lifetime.signal, turn.signal, ...(original.signal ? [original.signal] : [])]);
|
|
115
196
|
const options = { ...original, signal };
|
|
197
|
+
const timing = new StageTiming();
|
|
116
198
|
let runtime;
|
|
117
199
|
let session;
|
|
118
200
|
let live;
|
|
119
201
|
let succeeded = false;
|
|
120
202
|
let remoteAttempted = false;
|
|
121
203
|
let activeMapper;
|
|
204
|
+
let catalog;
|
|
205
|
+
let selected;
|
|
206
|
+
let modelConfiguration;
|
|
122
207
|
const config = this.config();
|
|
123
208
|
const mode = this.agent.oneShot ? config.mode : 'session';
|
|
124
209
|
try {
|
|
@@ -127,7 +212,11 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
127
212
|
const previous = readPointer(options, this.agent);
|
|
128
213
|
if (previous && previous.mode !== mode)
|
|
129
214
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '运行模式与原会话不一致,请新建会话', '原会话模式');
|
|
215
|
+
if (previous && !previous.modelConfiguration)
|
|
216
|
+
throw new NeedError('MODEL_SELECTION_REQUIRED', '旧远端恢复记录没有明确模型选择,请新建会话并选择模型', '本轮明确选择的模型');
|
|
130
217
|
live = key ? this.sessions.get(key) : undefined;
|
|
218
|
+
if (live?.disposal || live?.cleanupFailure)
|
|
219
|
+
throw live.cleanupFailure ?? new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '原主机会话正在结束,清理结果尚未确认,不能继续或重开此会话', '管理员核对原主机会话清理结果');
|
|
131
220
|
if (live?.failure)
|
|
132
221
|
throw new NeedError('SESSION_STATE_UNCONFIRMED', '上轮远端状态未确认,不能自动重发或重开,请新建会话并核对项目', '管理员核对原会话状态');
|
|
133
222
|
if (live && !previous)
|
|
@@ -135,10 +224,11 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
135
224
|
const configuration = JSON.stringify([mode, config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes]);
|
|
136
225
|
if (live && live.configuration !== configuration)
|
|
137
226
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '会话运行设置已改变,请新建会话', '原会话运行设置');
|
|
138
|
-
const request = { sessionId: options.sessionId, mode, cwd: previous?.cwd, identity: previous?.identity, bindingId: previous?.bindingId, workspaceId: previous?.workspaceId, agent: this.agent, retainProcess: !!key && mode === 'session', signal, brokerSettings: Object.freeze({ brokerMaxFrameBytes: config.brokerMaxFrameBytes, brokerControlMaxBytes: config.brokerControlMaxBytes }) };
|
|
227
|
+
const request = { timing, acpMaxMessageBytes: config.acpMaxMessageBytes, resumeSessionId: previous?.sessionId, recoveryCheckpointId: previous?.recoveryCheckpointId, sessionId: options.sessionId, mode, cwd: previous?.cwd, identity: previous?.identity, bindingId: previous?.bindingId, workspaceId: previous?.workspaceId, agent: this.agent, retainProcess: !!key && mode === 'session', signal, brokerSettings: Object.freeze({ brokerMaxFrameBytes: config.brokerMaxFrameBytes, brokerControlMaxBytes: config.brokerControlMaxBytes }) };
|
|
139
228
|
await this.resolveRuntime.preflight?.(request);
|
|
140
|
-
const input = await prepareInput(options, this.readImage);
|
|
229
|
+
const input = await timing.run('input-prepare', () => prepareInput(options, this.readImage));
|
|
141
230
|
const contextSha256 = createHash('sha256').update(input.systemPrompt ?? '').digest('hex');
|
|
231
|
+
request.contextSha256 = contextSha256;
|
|
142
232
|
if (previous && this.agent.loginProvider === 'claude' && previous.contextSha256 !== contextSha256)
|
|
143
233
|
throw new NeedError('SYSTEM_PROMPT_UPDATE_PENDING', 'Claude原会话的系统或开发者要求已变化;主机仅承诺下次上下文整理后生效,本轮未发送,请新建会话应用新要求', '新建Claude会话以立即应用当前系统与开发者要求');
|
|
144
234
|
if (live) {
|
|
@@ -149,14 +239,35 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
149
239
|
throw new NeedError('ONESHOT_CONTEXT_REQUIRED', '首版单次模式不支持图片或系统/开发者上下文,请使用会话模式', 'Codex会话模式');
|
|
150
240
|
if (mode === 'session' && Buffer.byteLength(JSON.stringify({ prompt: input.prompt, systemPrompt: input.systemPrompt }), 'utf8') > config.acpMaxMessageBytes)
|
|
151
241
|
throw new NeedError('ACP_MESSAGE_TOO_LARGE', '本轮输入超过已设置的会话消息容量,未截断或发送', '管理员调整acpMaxMessageBytes');
|
|
242
|
+
if (mode === 'one-shot')
|
|
243
|
+
throw new NeedError('MODEL_CONFIGURATION_UNSUPPORTED', '单次模式不支持确认当前模型与推理等级,请改用会话模式', '支持模型配置确认的会话模式');
|
|
244
|
+
catalog = await this.catalog(signal);
|
|
245
|
+
selected = catalog.models.find(model => model.id === options.model);
|
|
246
|
+
if (!selected)
|
|
247
|
+
throw new ModelConfigurationError('所选模型不在本次可信主机目录中,请重新选择模型');
|
|
248
|
+
if (options.reasoningEffort !== undefined && !selected.reasoning.options.some(option => option.value === options.reasoningEffort))
|
|
249
|
+
throw new ModelConfigurationError('所选推理等级不在本次模型目录中');
|
|
250
|
+
const modelSignal = AbortSignal.any([signal, catalog.signal]);
|
|
251
|
+
options.signal = modelSignal;
|
|
252
|
+
request.signal = modelSignal;
|
|
152
253
|
runtime = await this.resolveRuntime(request);
|
|
153
254
|
signal.throwIfAborted();
|
|
255
|
+
catalog.assertCurrent();
|
|
256
|
+
if (runtime.connection && (runtime.connection.instanceId !== catalog.target.instanceId || runtime.connection.epoch !== catalog.target.epoch) || runtime.identity !== `${catalog.target.target.username}@${catalog.target.target.host}:${catalog.target.target.port}`)
|
|
257
|
+
throw new ModelConfigurationError('模型目录与当前会话可信员工主机不一致');
|
|
154
258
|
if (previous && (previous.identity !== runtime.identity || previous.cwd !== runtime.cwd || previous.bindingId && previous.bindingId !== runtime.binding?.bindingId || previous.workspaceId && previous.workspaceId !== runtime.binding?.workspaceId))
|
|
155
259
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '原会话的员工主机或工作区已改变,请新建会话', '原会话的可信目标和工作区');
|
|
260
|
+
if (runtime.restartNotice) {
|
|
261
|
+
const ids = runtime.restartInputIds;
|
|
262
|
+
if (!ids?.length || new Set(ids).size !== ids.length || ids.some(id => input.users.filter(u => u.id === id).length !== 1))
|
|
263
|
+
throw new NeedError('CURRENT_INPUT_UNCONFIRMED', '无法核对本次输入,尚未发送给模型', '官方当前步骤的已接纳用户输入');
|
|
264
|
+
input.prompt = ids.flatMap(id => input.users.find(u => u.id === id).prompt);
|
|
265
|
+
}
|
|
156
266
|
const details = runtime.details;
|
|
157
267
|
if (!details)
|
|
158
268
|
throw new NeedError('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。', 'L技术详情持久接收能力');
|
|
159
|
-
|
|
269
|
+
timing.bind(detail => details.record(detail));
|
|
270
|
+
const mapper = new StreamMapper(mode === 'session' ? (runtime.restartNotice ? undefined : live ? live.usageSnapshot : previous ? (previous.usageSnapshot ?? null) : undefined) : undefined, observation => details.record(observation));
|
|
160
271
|
activeMapper = mapper;
|
|
161
272
|
let id;
|
|
162
273
|
let stop;
|
|
@@ -164,6 +275,10 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
164
275
|
yield* mapper.text(runtime.workspaceNotice.text);
|
|
165
276
|
yield* mapper.flush();
|
|
166
277
|
}
|
|
278
|
+
if (runtime.restartNotice) {
|
|
279
|
+
yield* mapper.text(runtime.restartNotice + '\n\n');
|
|
280
|
+
yield* mapper.flush();
|
|
281
|
+
}
|
|
167
282
|
if (mode === 'session') {
|
|
168
283
|
if (live && live.runtime !== runtime)
|
|
169
284
|
throw new NeedError('SESSION_TRANSPORT_MISMATCH', '原会话的主机通道已更换,不能自动切换进程', '原主机会话通道');
|
|
@@ -174,7 +289,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
174
289
|
let done = false;
|
|
175
290
|
let failure;
|
|
176
291
|
const push = (chunks) => { queue.push(...chunks); wake?.(); wake = undefined; };
|
|
177
|
-
const hooks = { broker: runtime.broker, approve: (permission, approvalSignal) => this.approve?.(permission, approvalSignal, options) ?? Promise.resolve('unavailable'), onUpdate: notification => { try {
|
|
292
|
+
const hooks = { broker: runtime.broker, timing, approve: (permission, approvalSignal) => this.approve?.(permission, approvalSignal, options) ?? Promise.resolve('unavailable'), onUpdate: notification => { try {
|
|
178
293
|
push(mapper.acp(notification.update));
|
|
179
294
|
}
|
|
180
295
|
catch (error) {
|
|
@@ -197,18 +312,47 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
197
312
|
}
|
|
198
313
|
const task = (async () => {
|
|
199
314
|
remoteAttempted = true;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
315
|
+
try {
|
|
316
|
+
if (fresh) {
|
|
317
|
+
if (runtime.recoveredSession)
|
|
318
|
+
session.restore(runtime.cwd, runtime.recoveredSession, { systemPrompt: input.systemPrompt });
|
|
319
|
+
else
|
|
320
|
+
await session.start(runtime.cwd, runtime.restartNotice ? undefined : previous?.sessionId, modelSignal, { systemPrompt: input.systemPrompt });
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
const updated = await session.updateContext({ systemPrompt: input.systemPrompt }, modelSignal);
|
|
324
|
+
if (updated && this.agent.loginProvider === 'claude')
|
|
325
|
+
throw new NeedError('SYSTEM_PROMPT_UPDATE_PENDING', 'Claude原会话的系统或开发者要求已变化;主机仅承诺下次上下文整理后生效,本轮未发送,请新建会话应用新要求', '新建Claude会话以立即应用当前系统与开发者要求');
|
|
326
|
+
}
|
|
327
|
+
catalog.assertCurrent();
|
|
328
|
+
const actual = await session.selectModel(selected.value, options.reasoningEffort, modelSignal);
|
|
329
|
+
catalog.assertCurrent();
|
|
330
|
+
session.assertModelSelection(selected.value, options.reasoningEffort);
|
|
331
|
+
let resolved = selected.resolvedModel;
|
|
332
|
+
if (this.agent.loginProvider === 'claude') {
|
|
333
|
+
const evidence = session.resolvedModelEvidence;
|
|
334
|
+
if (!resolved || (selected.value !== resolved && (!evidence || evidence.modelValue !== selected.value || evidence.resolvedModel !== resolved)))
|
|
335
|
+
throw new NeedError('MODEL_RESOLUTION_UNCONFIRMED', '所选模型的具体型号未获得当前会话确认,本轮未发送', '当前会话的具体型号证据');
|
|
336
|
+
if (evidence && evidence.resolvedModel !== resolved)
|
|
337
|
+
throw new NeedError('MODEL_RESOLUTION_UNCONFIRMED', '当前会话具体型号与所选目录不一致,本轮未发送', '完全一致的具体型号证据');
|
|
338
|
+
}
|
|
339
|
+
if (!resolved)
|
|
340
|
+
throw new NeedError('MODEL_RESOLUTION_UNCONFIRMED', '远端没有提供具体型号证据,本轮未发送', '具体型号证据');
|
|
341
|
+
modelConfiguration = { selected: { ...selected, ...(options.reasoningEffort === undefined ? {} : { effort: options.reasoningEffort }) }, configured: actual, resolvedModel: resolved, status: 'remote-configured' };
|
|
342
|
+
details.record({ kind: 'session', severity: 'info', code: 'MODEL_CONFIGURATION_CONFIRMED', summary: '本轮选择已由远端配置回执确认;不代表推理效果已验证', payload: modelConfiguration });
|
|
206
343
|
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
details.record({ kind: 'session', severity: 'action-required', code: 'MODEL_CONFIGURATION_FAILED', summary: '本轮模型配置未获完整确认,未发送用户输入', payload: { selected, requestedEffort: options.reasoningEffort ?? null, actual: session.modelConfiguration ?? null, reason: error instanceof NeedError ? error.code : error instanceof ModelConfigurationError ? error.message : 'MODEL_CONFIGURATION_FAILED' } });
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
await timing.run('details-flush', () => details.flush());
|
|
207
349
|
await details.flush();
|
|
208
350
|
details.beginTurn(runtime.detailContext?.() ?? { turn: null, step: null });
|
|
351
|
+
catalog.assertCurrent();
|
|
352
|
+
session.assertModelSelection(selected.value, options.reasoningEffort);
|
|
209
353
|
id = session.id;
|
|
210
354
|
try {
|
|
211
|
-
return await session.prompt(input.prompt,
|
|
355
|
+
return await session.prompt(input.prompt, modelSignal);
|
|
212
356
|
}
|
|
213
357
|
finally {
|
|
214
358
|
details.endTurn();
|
|
@@ -242,13 +386,27 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
242
386
|
}
|
|
243
387
|
}
|
|
244
388
|
details.endTurn();
|
|
245
|
-
await details.flush();
|
|
389
|
+
await timing.run('details-flush', () => details.flush());
|
|
390
|
+
await details.flush(); // The timing record is a new ordered write, not a retry.
|
|
246
391
|
if (!id)
|
|
247
392
|
throw new NeedError('SESSION_ID_MISSING', '远端没有返回会话标识', '有效的远端会话标识');
|
|
248
393
|
if (live)
|
|
249
394
|
live.usageSnapshot = mapper.usageSnapshot;
|
|
395
|
+
if (modelConfiguration) {
|
|
396
|
+
const actual = session.modelConfiguration;
|
|
397
|
+
if (!actual?.confirmed || actual.model !== modelConfiguration.configured.model || actual.effort !== modelConfiguration.configured.effort)
|
|
398
|
+
throw new ModelConfigurationError('远端会话在本轮期间改变了模型配置,不能沿用旧回执');
|
|
399
|
+
catalog.assertCurrent();
|
|
400
|
+
yield* mapper.text(`\n\n本轮所选模型:${modelConfiguration.selected.name}(${modelConfiguration.selected.value});推理等级:${modelConfiguration.selected.effort ?? '沿用远端配置'}。远端已配置模型:${modelConfiguration.configured.model};具体型号:${modelConfiguration.resolvedModel};推理等级:${modelConfiguration.configured.effort ?? '远端未提供'}(配置已确认,推理效果未验证)。`);
|
|
401
|
+
}
|
|
250
402
|
yield* mapper.flush();
|
|
251
|
-
|
|
403
|
+
let recoveryCheckpointId;
|
|
404
|
+
if (session?.checkpoint && runtime.saveCheckpoint) {
|
|
405
|
+
const id = randomUUID();
|
|
406
|
+
if (await runtime.saveCheckpoint(id, session.checkpoint))
|
|
407
|
+
recoveryCheckpointId = id;
|
|
408
|
+
}
|
|
409
|
+
const pointer = { ...(modelConfiguration ? { modelConfiguration } : {}), ...(recoveryCheckpointId ? { recoveryCheckpointId } : {}), ...(runtime.workspaceNotice ? { presentation: { workspaceNotice: runtime.workspaceNotice } } : {}), version: 2, agent: this.agent.id, contextSha256, ...(mode === 'session' ? { usageSnapshot: mapper.usageSnapshot } : {}), ...(options.sessionId ? { dshSessionId: options.sessionId } : {}), mode, identity: runtime.identity, cwd: runtime.cwd, sessionId: id, ...(runtime.binding ? { bindingId: runtime.binding.bindingId, workspaceId: runtime.binding.workspaceId } : {}) };
|
|
252
410
|
succeeded = true;
|
|
253
411
|
yield { type: 'finish', reason: { kind: stop === 'max_tokens' || stop === 'max_turn_requests' ? 'max-tokens' : 'stop' }, replayState: { response: pointer } };
|
|
254
412
|
}
|
|
@@ -269,27 +427,42 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
269
427
|
throw error;
|
|
270
428
|
}
|
|
271
429
|
finally {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
430
|
+
catalog?.release();
|
|
431
|
+
try {
|
|
432
|
+
if (this.shuttingDown || key && this.detached.has(key)) {
|
|
433
|
+
session?.close();
|
|
434
|
+
await runtime?.detach?.();
|
|
435
|
+
}
|
|
436
|
+
else if (!succeeded || !key || mode !== 'session') {
|
|
437
|
+
session?.close();
|
|
438
|
+
if (runtime)
|
|
439
|
+
await this.cleanupRuntime(runtime);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
finally {
|
|
443
|
+
if (key && this.turns.get(key) === turn)
|
|
444
|
+
this.turns.delete(key);
|
|
445
|
+
if (key)
|
|
446
|
+
this.detached.delete(key);
|
|
275
447
|
}
|
|
276
|
-
if (key && this.turns.get(key) === turn)
|
|
277
|
-
this.turns.delete(key);
|
|
278
448
|
}
|
|
279
449
|
}
|
|
280
450
|
}
|
|
281
451
|
/** One official registration owns both routes, preserving opaque replay ownership. */
|
|
282
452
|
export class RemoteAgentsAdapter extends LlmAdapter {
|
|
283
453
|
adapters = new Map();
|
|
284
|
-
constructor(config, makeRuntime, approve, nameFor, readImage) { super(); for (const agent of AGENTS)
|
|
285
|
-
this.adapters.set(agent.id, new CodexRemoteAdapter(config, makeRuntime(agent), approve, () => nameFor(agent), agent, readImage)); }
|
|
454
|
+
constructor(config, makeRuntime, approve, nameFor, readImage, reportLoginFailure, directory) { super(); for (const agent of AGENTS)
|
|
455
|
+
this.adapters.set(agent.id, new CodexRemoteAdapter(config, makeRuntime(agent), approve, () => nameFor(agent), agent, readImage, reportLoginFailure, directory)); }
|
|
286
456
|
adapter(provider) { const adapter = this.adapters.get(provider); if (!adapter)
|
|
287
457
|
throw new LlmError('远程模型未注册', 'PROVIDER_NOT_FOUND'); return adapter; }
|
|
288
458
|
providerInfo(provider) { return this.adapter(provider).providerInfo(provider); }
|
|
289
459
|
listModels(provider) { return this.adapter(provider).listModels(provider); }
|
|
460
|
+
resolveModel(provider, model, signal) { return this.adapter(provider).resolveModel(provider, model, signal); }
|
|
461
|
+
async invalidateModelState() { await Promise.all([...this.adapters.values()].map(adapter => adapter.invalidateModelState())); }
|
|
290
462
|
providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
|
|
291
463
|
stream(options) { return this.adapter(options.provider).stream(options); }
|
|
292
464
|
async disposeSession(sessionId) { await Promise.all([...this.adapters.values()].map(adapter => adapter.disposeSession(sessionId))); }
|
|
465
|
+
async detachSession(sessionId) { await Promise.all([...this.adapters.values()].map(adapter => adapter.detachSession(sessionId))); }
|
|
293
466
|
async dispose() { await Promise.all([...this.adapters.values()].map(adapter => adapter.dispose())); }
|
|
294
467
|
}
|
|
295
468
|
export function apply(ctx, live) {
|
|
@@ -304,17 +477,28 @@ export function apply(ctx, live) {
|
|
|
304
477
|
};
|
|
305
478
|
const openByAgent = new Map(AGENTS.map(agent => [agent.id, brokerFactory(get, undefined, config)]));
|
|
306
479
|
const detailManagers = new Map();
|
|
480
|
+
const managerForProfile = () => {
|
|
481
|
+
const profile = ctx.get('profileContext');
|
|
482
|
+
if (typeof profile?.dir !== 'string')
|
|
483
|
+
throw new NeedError('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用:缺少官方profile目录。', '官方profileContext.dir');
|
|
484
|
+
let manager = detailManagers.get(profile.dir);
|
|
485
|
+
if (!manager) {
|
|
486
|
+
manager = createRemoteDetails({ profileDir: profile.dir, getApi: get });
|
|
487
|
+
detailManagers.set(profile.dir, manager);
|
|
488
|
+
}
|
|
489
|
+
return manager;
|
|
490
|
+
};
|
|
491
|
+
let changed = () => { };
|
|
492
|
+
const modelDirectory = new ModelCatalog(get, () => config().acpMaxMessageBytes, undefined, () => changed());
|
|
307
493
|
const adapter = new RemoteAgentsAdapter(config, agent => connectProvider(get, { agent, notice: createWorkspaceNoticeConsumer(ctx, agent), details: async (request, api, connection, target) => {
|
|
308
|
-
|
|
309
|
-
if (typeof profile?.dir !== 'string')
|
|
310
|
-
throw new NeedError('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用:缺少官方profile目录。', '官方profileContext.dir');
|
|
311
|
-
let manager = detailManagers.get(profile.dir);
|
|
312
|
-
if (!manager) {
|
|
313
|
-
manager = createRemoteDetails({ profileDir: profile.dir, getApi: get });
|
|
314
|
-
detailManagers.set(profile.dir, manager);
|
|
315
|
-
}
|
|
316
|
-
return manager.open({ sessionId: request.sessionId, provider: agent.loginProvider, identity: `${target.username}@${target.host}:${target.port}`, connection });
|
|
494
|
+
return managerForProfile().open({ sessionId: request.sessionId, provider: agent.loginProvider, identity: `${target.username}@${target.host}:${target.port}`, connection });
|
|
317
495
|
}, open: async (...args) => {
|
|
496
|
+
const profile = ctx.get('profileContext');
|
|
497
|
+
if (!profile?.dir)
|
|
498
|
+
throw new NeedError('RECOVERY_STORAGE_REQUIRED', '会话恢复需要官方profile目录', '官方profileContext.dir');
|
|
499
|
+
args[2].recoveryJournal = recoveryJournal(profile.dir, recoveryKey([`${args[0].username}@${args[0].host}:${args[0].port}`, agent.id, args[2].sessionId]));
|
|
500
|
+
args[2].verifyRecovery = () => verifyRecoveryCheckpoint(ctx, args[2], agent.id);
|
|
501
|
+
args[2].currentInputIds = await currentInputIds(ctx, args[2]);
|
|
318
502
|
const runtime = await openByAgent.get(agent.id)(...args);
|
|
319
503
|
runtime.detailContext = () => {
|
|
320
504
|
const events = args[2].sessionId ? ctx.get('agents')?.get(SessionId(args[2].sessionId))?.session.snapshotEvents() : undefined;
|
|
@@ -336,17 +520,37 @@ export function apply(ctx, live) {
|
|
|
336
520
|
return { turn, step };
|
|
337
521
|
};
|
|
338
522
|
return runtime;
|
|
339
|
-
} }), dshApproval(ctx), agent => agent.name[language], readImage)
|
|
523
|
+
} }), dshApproval(ctx), agent => agent.name[language], readImage, (sessionId, code, diagnostic) => {
|
|
524
|
+
const events = sessionId ? ctx.get('agents')?.get(SessionId(sessionId))?.session.snapshotEvents() : undefined;
|
|
525
|
+
let turn = null, step = null;
|
|
526
|
+
for (const event of events ?? []) {
|
|
527
|
+
if (event.type === 'turn/start') {
|
|
528
|
+
turn = event.data.turn;
|
|
529
|
+
step = null;
|
|
530
|
+
}
|
|
531
|
+
else if (event.type === 'turn/end') {
|
|
532
|
+
turn = null;
|
|
533
|
+
step = null;
|
|
534
|
+
}
|
|
535
|
+
else if (event.type === 'step/start' && event.data.turn === turn)
|
|
536
|
+
step = event.data.step;
|
|
537
|
+
else if (event.type === 'step/end')
|
|
538
|
+
step = null;
|
|
539
|
+
}
|
|
540
|
+
return managerForProfile().recordLoginFailure({ sessionId, code, diagnostic, context: { turn, step } });
|
|
541
|
+
}, modelDirectory);
|
|
340
542
|
const ids = AGENTS.map(agent => agent.id);
|
|
341
543
|
const entries = () => AGENTS.map(agent => ({ provider: agent.id, displayName: agent.name[language], settingsNs: settingsNamespace, settingsPath: [] }));
|
|
342
544
|
const directory = ctx.llm.registerConfigurableProviders(entries());
|
|
343
545
|
const registration = ctx.llm.registerAdapter(ids, adapter);
|
|
546
|
+
changed = () => { registration.replace(ids); void adapter.invalidateModelState().catch(() => { ctx.logger('remote-agent').warn('原会话清理结果未确认,已阻断继续发送;请联系管理员核对。'); }); };
|
|
547
|
+
ctx.on('internal/service', service => { if (service === 'aiWorkbenchConnect')
|
|
548
|
+
modelDirectory.refresh(); });
|
|
344
549
|
new ProviderLocaleRemote(ctx, next => { language = next; directory.replace(entries()); registration.replace(ids); });
|
|
345
|
-
const cleanup = (id) => adapter.
|
|
550
|
+
const cleanup = (id) => adapter.detachSession(id);
|
|
346
551
|
ctx.on('agent/disposed', ({ agent }) => cleanup(agent.session.id));
|
|
347
552
|
ctx.on('session/disposed', session => cleanup(session.id));
|
|
348
|
-
ctx.
|
|
349
|
-
ctx.effect(() => () => adapter.dispose());
|
|
553
|
+
ctx.effect(() => () => { modelDirectory.dispose(); return adapter.dispose(); });
|
|
350
554
|
}
|
|
351
555
|
export function dshApproval(ctx) {
|
|
352
556
|
return async (request, signal, options, context) => {
|
|
@@ -354,7 +558,9 @@ export function dshApproval(ctx) {
|
|
|
354
558
|
const approval = ctx.get('approval');
|
|
355
559
|
if (!agent || !approval)
|
|
356
560
|
return 'unavailable';
|
|
357
|
-
|
|
358
|
-
|
|
561
|
+
const label = approvalLabel(request);
|
|
562
|
+
if (!label)
|
|
563
|
+
return 'unavailable';
|
|
564
|
+
return approval.request({ agent, ...label, signal });
|
|
359
565
|
};
|
|
360
566
|
}
|
package/dist/input.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ export type ImageReader = (ref: ImageBlock['attachment'], signal?: AbortSignal)
|
|
|
6
6
|
}>;
|
|
7
7
|
export interface PreparedInput {
|
|
8
8
|
prompt: AcpContentBlock[];
|
|
9
|
+
users: {
|
|
10
|
+
id: string | undefined;
|
|
11
|
+
prompt: AcpContentBlock[];
|
|
12
|
+
}[];
|
|
9
13
|
systemPrompt?: string;
|
|
10
14
|
}
|
|
11
15
|
/** The attachment service supplies verified bytes; client paths never cross SSH. */
|
package/dist/input.js
CHANGED
|
@@ -19,7 +19,9 @@ export async function prepareInput(options, readImage) {
|
|
|
19
19
|
if (!users.length)
|
|
20
20
|
throw new NeedError('INPUT_REQUIRED', '我需要本轮用户输入', '用户输入');
|
|
21
21
|
const prompt = [];
|
|
22
|
+
const preparedUsers = [];
|
|
22
23
|
for (const message of users) {
|
|
24
|
+
const before = prompt.length;
|
|
23
25
|
for (const block of message.content) {
|
|
24
26
|
options.signal?.throwIfAborted();
|
|
25
27
|
if (block.type === 'text') {
|
|
@@ -42,8 +44,9 @@ export async function prepareInput(options, readImage) {
|
|
|
42
44
|
}
|
|
43
45
|
throw new NeedError('INPUT_TYPE_REQUIRED', `我需要此输入类型的远程转接能力:${block.type}`, '对应类型的公开ACP输入映射');
|
|
44
46
|
}
|
|
47
|
+
preparedUsers.push({ id: message.id, prompt: prompt.slice(before) });
|
|
45
48
|
}
|
|
46
49
|
if (!prompt.length)
|
|
47
50
|
throw new NeedError('INPUT_REQUIRED', '我需要本轮用户输入', '用户输入');
|
|
48
|
-
return { prompt, ...(context.length ? { systemPrompt: context.join('\n\n') } : {}) };
|
|
51
|
+
return { prompt, users: preparedUsers, ...(context.length ? { systemPrompt: context.join('\n\n') } : {}) };
|
|
49
52
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ConnectionState, LoginPromptState } from '@ai-workbench/connect/api';
|
|
2
|
+
export type LoginReason = 'LOGIN_INSTANCE_MISMATCH' | 'LOGIN_PHASE_BLOCKED' | 'LOGIN_PHASE_DISPOSED' | 'LOGIN_CHECKING' | 'LOGIN_AUTHORIZING' | 'LOGIN_PROVIDER_NEED' | 'LOGIN_VALUE_INVALID' | 'LOGIN_REQUIRED' | 'L_ENSURE_READY_FAILED' | 'LOGIN_QUEUED';
|
|
3
|
+
/** Allowlisted protocol facts only: never messages, requires, host details or status command output. */
|
|
4
|
+
export interface LoginDiagnostic {
|
|
5
|
+
version: 1;
|
|
6
|
+
source: 'verified-login' | 'readiness' | 'ensure-ready';
|
|
7
|
+
reason: LoginReason;
|
|
8
|
+
provider: 'claude' | 'codex';
|
|
9
|
+
officialReason?: string;
|
|
10
|
+
officialReasonRejected?: {
|
|
11
|
+
bytes: number;
|
|
12
|
+
sha256: string;
|
|
13
|
+
};
|
|
14
|
+
connection: {
|
|
15
|
+
instanceId: string;
|
|
16
|
+
phase: string;
|
|
17
|
+
epoch: number;
|
|
18
|
+
};
|
|
19
|
+
login: {
|
|
20
|
+
instanceId: string;
|
|
21
|
+
phase: string;
|
|
22
|
+
revision: number;
|
|
23
|
+
provider: 'claude' | 'codex' | null;
|
|
24
|
+
logins: {
|
|
25
|
+
claude: boolean | 'absent' | 'invalid';
|
|
26
|
+
codex: boolean | 'absent' | 'invalid';
|
|
27
|
+
};
|
|
28
|
+
queuedProviders: ('claude' | 'codex')[];
|
|
29
|
+
checkingProviders: ('claude' | 'codex')[];
|
|
30
|
+
providerNeeds: {
|
|
31
|
+
claude: string | null;
|
|
32
|
+
codex: string | null;
|
|
33
|
+
};
|
|
34
|
+
code: string | null;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export declare function loginDiagnostic(source: LoginDiagnostic['source'], reason: LoginReason, provider: 'claude' | 'codex', connection: ConnectionState, state: LoginPromptState, officialReason?: string): LoginDiagnostic;
|
|
38
|
+
export declare function loginReason(state: LoginPromptState, connection: ConnectionState, provider: 'claude' | 'codex'): LoginReason | undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { validOfficialReason } from "./auth-reason.js";
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
const code = (value) => typeof value === 'string' && /^[A-Z][A-Z0-9_]*$/u.test(value) ? value : null;
|
|
4
|
+
export function loginDiagnostic(source, reason, provider, connection, state, officialReason) {
|
|
5
|
+
const value = (brand) => typeof state.logins?.[brand] === 'boolean' ? state.logins[brand] : state.logins?.[brand] === undefined ? 'absent' : 'invalid';
|
|
6
|
+
const rawReason = officialReason ?? state.providerNeeds?.[provider]?.officialReason;
|
|
7
|
+
const safeReason = typeof rawReason !== 'string' ? {} : validOfficialReason(rawReason) ? { officialReason: rawReason } : { officialReasonRejected: { bytes: Buffer.byteLength(rawReason, 'utf8'), sha256: createHash('sha256').update(rawReason).digest('hex') } };
|
|
8
|
+
return { version: 1, source, reason, provider, ...safeReason, connection: { instanceId: connection.instanceId, phase: connection.phase, epoch: connection.epoch }, login: { instanceId: state.instanceId, phase: state.phase, revision: state.revision, provider: state.provider ?? null, logins: { claude: value('claude'), codex: value('codex') }, queuedProviders: state.queuedProviders ?? [], checkingProviders: Array.isArray(state.checkingProviders) ? state.checkingProviders.filter(p => p === 'claude' || p === 'codex') : [], providerNeeds: { claude: code(state.providerNeeds?.claude?.code), codex: code(state.providerNeeds?.codex?.code) }, code: code(state.technicalDetails?.code) } };
|
|
9
|
+
}
|
|
10
|
+
export function loginReason(state, connection, provider) {
|
|
11
|
+
if (state.instanceId !== connection.instanceId)
|
|
12
|
+
return 'LOGIN_INSTANCE_MISMATCH';
|
|
13
|
+
if (state.phase === 'blocked')
|
|
14
|
+
return 'LOGIN_PHASE_BLOCKED';
|
|
15
|
+
if (state.phase === 'disposed')
|
|
16
|
+
return 'LOGIN_PHASE_DISPOSED';
|
|
17
|
+
if (state.queuedProviders?.includes(provider))
|
|
18
|
+
return 'LOGIN_QUEUED';
|
|
19
|
+
if (state.checkingProviders.includes(provider))
|
|
20
|
+
return 'LOGIN_CHECKING';
|
|
21
|
+
if (state.phase === 'authorizing' && state.provider === provider)
|
|
22
|
+
return 'LOGIN_AUTHORIZING';
|
|
23
|
+
if (state.providerNeeds[provider])
|
|
24
|
+
return 'LOGIN_PROVIDER_NEED';
|
|
25
|
+
if (typeof state.logins?.[provider] !== 'boolean')
|
|
26
|
+
return 'LOGIN_VALUE_INVALID';
|
|
27
|
+
if (!state.logins[provider])
|
|
28
|
+
return 'LOGIN_REQUIRED';
|
|
29
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type LlmResolvedModelInfo } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import type { ModelCatalogTarget } from '@ai-workbench/connect/api';
|
|
3
|
+
import type { AgentDefinition } from './agents.ts';
|
|
4
|
+
import { type ConnectApi, type ConnectTarget } from './provider.ts';
|
|
5
|
+
export interface HostModel {
|
|
6
|
+
id: string;
|
|
7
|
+
value: string;
|
|
8
|
+
name: string;
|
|
9
|
+
resolvedModel: string | null;
|
|
10
|
+
reasoning: {
|
|
11
|
+
options: {
|
|
12
|
+
value: string;
|
|
13
|
+
name: string;
|
|
14
|
+
}[];
|
|
15
|
+
defaultValue: string | null;
|
|
16
|
+
defaultKnown: boolean;
|
|
17
|
+
};
|
|
18
|
+
source: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface CatalogLease {
|
|
21
|
+
readonly target: ModelCatalogTarget;
|
|
22
|
+
readonly models: readonly HostModel[];
|
|
23
|
+
readonly signal: AbortSignal;
|
|
24
|
+
assertCurrent(): void;
|
|
25
|
+
release(): void;
|
|
26
|
+
}
|
|
27
|
+
export interface ModelDirectory {
|
|
28
|
+
read(agent: AgentDefinition, signal?: AbortSignal): Promise<CatalogLease>;
|
|
29
|
+
}
|
|
30
|
+
export type CatalogRunner = (target: ConnectTarget, command: string, signal: AbortSignal, maxBytes: number) => Promise<string>;
|
|
31
|
+
export declare function parseHostCatalog(input: string, provider: string, username: string): HostModel[];
|
|
32
|
+
/** Only the public L sshArgs function chooses SSH trust and transport arguments. */
|
|
33
|
+
export declare const runCatalog: CatalogRunner;
|
|
34
|
+
/** No cache: every query binds to L's current object, host-user and generation. */
|
|
35
|
+
export declare class ModelCatalog implements ModelDirectory {
|
|
36
|
+
private get;
|
|
37
|
+
private maxBytes;
|
|
38
|
+
private run;
|
|
39
|
+
private changed;
|
|
40
|
+
private api?;
|
|
41
|
+
private unsubscribe?;
|
|
42
|
+
private signature?;
|
|
43
|
+
private revision;
|
|
44
|
+
private disposed;
|
|
45
|
+
private initialized;
|
|
46
|
+
private active;
|
|
47
|
+
constructor(get: () => ConnectApi | undefined, maxBytes: () => number | undefined, run?: CatalogRunner, changed?: () => void);
|
|
48
|
+
refresh(): void;
|
|
49
|
+
private trusted;
|
|
50
|
+
read(agent: AgentDefinition, signal?: AbortSignal): Promise<CatalogLease>;
|
|
51
|
+
dispose(): void;
|
|
52
|
+
}
|
|
53
|
+
export declare function modelInfo(provider: string, model: HostModel): LlmResolvedModelInfo & {
|
|
54
|
+
catalog: HostModel;
|
|
55
|
+
};
|