@ai-workbench/remote-agent 0.8.10 → 0.8.12

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.
@@ -1,24 +1,29 @@
1
- var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
- if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
- return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
- return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
- });
6
- }
7
- return path;
8
- };
9
- import { spawn } from 'node:child_process';
10
1
  import { LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
11
2
  import { sameTarget, validateTarget } from "./provider.js";
12
3
  import { ModelConfigurationError } from "./model-configuration.js";
13
- import { sanitizeObservation } from "./conversation-details.js";
4
+ import { runCatalog, CatalogSshError, catalogSafeText } from "./catalog-ssh.js";
5
+ export { runCatalog } from "./catalog-ssh.js";
14
6
  const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
15
7
  const text = (value) => typeof value === 'string' && value.trim().length > 0;
16
- function fail(reason) { throw new LlmError('无法获取模型列表:' + reason, 'MODEL_CATALOG_UNAVAILABLE'); }
8
+ class CatalogError extends LlmError {
9
+ diagnostic;
10
+ constructor(reason, diagnostic) { super('无法获取模型列表:' + reason, 'MODEL_CATALOG_UNAVAILABLE'); this.diagnostic = diagnostic + '(诊断代码:MODEL_CATALOG_UNAVAILABLE)'; }
11
+ }
12
+ const safe = catalogSafeText;
13
+ function fail(reason, diagnostic = reason) { throw new CatalogError(reason, diagnostic); }
17
14
  function freeze(value) { if (value && typeof value === 'object') {
18
15
  for (const item of Object.values(value))
19
16
  freeze(item);
20
17
  Object.freeze(value);
21
18
  } return value; }
19
+ export function catalogDiagnostic(issues) {
20
+ return issues.length ? '以下模型未加入可选列表:\n' + issues.map(issue => `模型「${issue.modelName}」:${issue.reason}(诊断代码:${issue.code})`).join('\n') : undefined;
21
+ }
22
+ /** Defaults are provider declarations, never catalog order or display names. */
23
+ export function declaredDefault(agent, models) {
24
+ const selected = models.filter(model => agent.loginProvider === 'claude' ? model.value === 'default' : object(model.source) && model.source.isDefault === true);
25
+ return selected.length === 1 ? selected[0] : undefined;
26
+ }
22
27
  export function parseHostCatalog(input, provider, username) {
23
28
  let wire;
24
29
  try {
@@ -32,115 +37,69 @@ export function parseHostCatalog(input, provider, username) {
32
37
  if (wire.ok === false) {
33
38
  const error = object(wire.error) ? wire.error : undefined;
34
39
  const code = typeof error?.code === 'string' && /^[A-Z][A-Z0-9_]*$/.test(error.code) ? error.code : 'PROTOCOL_INVALID';
35
- const reason = text(error?.message) ? sanitizeObservation({ kind: 'session', severity: 'warning', code, summary: error.message, payload: {} }).summary : '主机未提供失败说明';
36
- fail('主机目录查询失败(' + code + '):' + reason);
40
+ const reason = text(error?.message) ? safe(error.message) : '主机未提供失败说明';
41
+ fail('主机目录查询失败,请到设置 → 模型查看诊断。', `主机目录查询失败(诊断代码:${code}):${reason}`);
37
42
  }
38
43
  if (wire.ok !== true || wire.permissionProbe !== false || !object(wire.subject) || wire.subject.username !== username || !Number.isSafeInteger(wire.subject.uid) || !text(wire.observedAt) || !object(wire.source) || !object(wire.settings) || !Array.isArray(wire.models))
39
44
  fail('主机目录缺少完整元数据');
40
- const ids = new Set(), values = new Set();
41
- for (const model of wire.models) {
42
- if (!object(model) || !text(model.id) || !text(model.value) || !text(model.name) || (model.resolvedModel !== null && !text(model.resolvedModel)) || !object(model.source) || !object(model.reasoning))
43
- fail('主机模型条目不完整');
44
- if (provider === 'codex' && model.resolvedModel !== model.value)
45
- fail('Codex目录具体型号与模型值不一致');
46
- if (ids.has(model.id) || values.has(model.value))
47
- fail('主机目录含重复模型标识或值');
48
- ids.add(model.id);
49
- values.add(model.value);
45
+ const ids = new Map(), values = new Map();
46
+ for (const model of wire.models)
47
+ if (object(model)) {
48
+ if (text(model.id))
49
+ ids.set(model.id, (ids.get(model.id) ?? 0) + 1);
50
+ if (text(model.value))
51
+ values.set(model.value, (values.get(model.value) ?? 0) + 1);
52
+ }
53
+ const models = [], issues = [];
54
+ for (const [index, model] of wire.models.entries()) {
55
+ const modelName = object(model) && text(model.name) ? safe(model.name) : object(model) && text(model.id) ? safe(model.id) : `第 ${index + 1} 项(缺少模型名称)`;
56
+ const reject = (reason, code = 'MODEL_CATALOG_ITEM_INVALID') => issues.push({ modelName, reason, code });
57
+ if (!object(model) || !text(model.id) || !text(model.value) || !text(model.name) || (model.resolvedModel !== null && !text(model.resolvedModel)) || !object(model.source) || !object(model.reasoning)) {
58
+ reject('模型标识、名称或能力资料不完整');
59
+ continue;
60
+ }
61
+ if ((ids.get(model.id) ?? 0) > 1 || (values.get(model.value) ?? 0) > 1) {
62
+ reject('模型标识或发送值重复,无法确定对应关系', 'MODEL_CATALOG_DUPLICATE');
63
+ continue;
64
+ }
65
+ if (provider === 'codex' && model.resolvedModel !== model.value) {
66
+ reject('具体型号与发送模型值不一致');
67
+ continue;
68
+ }
50
69
  const r = model.reasoning;
51
- if (!Array.isArray(r.options) || typeof r.defaultKnown !== 'boolean' || (r.defaultValue !== null && !text(r.defaultValue)) || (!r.defaultKnown && r.defaultValue !== null))
52
- fail('主机推理等级或默认值无效');
70
+ if (!Array.isArray(r.options) || typeof r.defaultKnown !== 'boolean' || (r.defaultValue !== null && !text(r.defaultValue)) || (!r.defaultKnown && r.defaultValue !== null)) {
71
+ reject('推理档位或默认档位资料无效', 'MODEL_REASONING_INVALID');
72
+ continue;
73
+ }
53
74
  const efforts = new Set();
75
+ let invalid = false;
54
76
  for (const effort of r.options) {
55
- if (!object(effort) || !text(effort.value) || !text(effort.name) || efforts.has(effort.value))
56
- fail('主机推理等级条目无效或重复');
77
+ if (!object(effort) || !text(effort.value) || !text(effort.name) || efforts.has(effort.value)) {
78
+ invalid = true;
79
+ break;
80
+ }
57
81
  efforts.add(effort.value);
58
82
  }
59
- if (r.defaultKnown && (r.defaultValue === null || !efforts.has(r.defaultValue)))
60
- fail('主机默认推理等级缺少对应选项');
83
+ if (invalid) {
84
+ reject('推理档位缺少名称、值无效或重复', 'MODEL_REASONING_INVALID');
85
+ continue;
86
+ }
87
+ if (r.defaultKnown && (r.defaultValue === null || !efforts.has(r.defaultValue))) {
88
+ reject('默认推理档位不在该模型提供的选项中', 'MODEL_REASONING_DEFAULT_INVALID');
89
+ continue;
90
+ }
91
+ models.push(model);
61
92
  }
62
- if (!wire.models.length)
63
- fail('主机返回的完整模型目录为空');
64
- return freeze(structuredClone(wire.models));
93
+ return freeze(structuredClone({ models, issues }));
65
94
  }
66
- /** Only the public L sshArgs function chooses SSH trust and transport arguments. */
67
- export const runCatalog = async (target, command, signal, maxBytes) => {
68
- let argv;
69
- try {
70
- const moduleName = '@ai-workbench/connect';
71
- const l = await import(__rewriteRelativeImportExtension(moduleName));
72
- if (typeof l.sshArgs !== 'function')
73
- fail('远程主机连接组件缺少可信 SSH 能力');
74
- argv = l.sshArgs(target);
75
- }
76
- catch (error) {
77
- if (error instanceof LlmError)
78
- throw error;
79
- fail('远程主机连接组件缺少可信 SSH 能力');
80
- }
81
- signal.throwIfAborted();
82
- return new Promise((resolve, reject) => {
83
- const child = spawn('ssh', ['-n', ...argv, command], { stdio: ['ignore', 'pipe', 'pipe'] });
84
- const parts = [];
85
- let total = 0, stderr = 0;
86
- let failure;
87
- const abort = () => { failure = new ModelConfigurationError('连接已改变或目录查询已取消'); child.kill('SIGTERM'); };
88
- signal.addEventListener('abort', abort, { once: true });
89
- const receive = (buffer, output) => { if (failure)
90
- return; total += buffer.length; if (total > maxBytes) {
91
- failure = new ModelConfigurationError('目录响应超过 acpMaxMessageBytes,未截断返回');
92
- child.kill('SIGTERM');
93
- return;
94
- } if (output)
95
- parts.push(buffer);
96
- else
97
- stderr += buffer.length; };
98
- child.stdout.on('data', (b) => receive(b, true));
99
- child.stderr.on('data', (b) => receive(b, false));
100
- child.on('error', () => { failure = new ModelConfigurationError('可信 SSH 进程启动失败'); });
101
- child.on('close', code => {
102
- signal.removeEventListener('abort', abort);
103
- if (failure) {
104
- reject(failure);
105
- return;
106
- }
107
- let output;
108
- try {
109
- output = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(parts));
110
- }
111
- catch {
112
- reject(new ModelConfigurationError('主机目录响应不是有效 UTF-8,未替换模型值'));
113
- return;
114
- }
115
- // S publishes its typed failure JSON on stdout even on nonzero exit.
116
- if (code !== 0) {
117
- try {
118
- const wire = JSON.parse(output);
119
- if (wire?.ok === false) {
120
- resolve(output);
121
- return;
122
- }
123
- }
124
- catch { }
125
- reject(new ModelConfigurationError('可信 SSH 或主机目录命令失败(退出码 ' + String(code) + ')'));
126
- return;
127
- }
128
- if (stderr) {
129
- reject(new ModelConfigurationError('目录命令返回了非预期诊断输出'));
130
- return;
131
- }
132
- resolve(output);
133
- });
134
- if (signal.aborted)
135
- abort();
136
- });
137
- };
138
- /** No cache: every query binds to L's current object, host-user and generation. */
95
+ /** Share only pending work; settled results are never cached. */
139
96
  export class ModelCatalog {
140
97
  get;
141
98
  maxBytes;
142
99
  run;
143
100
  changed;
101
+ diagnostic;
102
+ loaded;
144
103
  api;
145
104
  unsubscribe;
146
105
  signature;
@@ -148,11 +107,16 @@ export class ModelCatalog {
148
107
  disposed = false;
149
108
  initialized = false;
150
109
  active = new Set();
151
- constructor(get, maxBytes, run = runCatalog, changed = () => { }) {
110
+ flights = new Map();
111
+ serial = 0;
112
+ latest = new Map();
113
+ constructor(get, maxBytes, run = runCatalog, changed = () => { }, diagnostic = () => { }, loaded = () => { }) {
152
114
  this.get = get;
153
115
  this.maxBytes = maxBytes;
154
116
  this.run = run;
155
117
  this.changed = changed;
118
+ this.diagnostic = diagnostic;
119
+ this.loaded = loaded;
156
120
  this.refresh();
157
121
  }
158
122
  refresh() {
@@ -160,11 +124,10 @@ export class ModelCatalog {
160
124
  const state = api?.getConnectionState?.();
161
125
  const info = api?.getContractInfo?.();
162
126
  const targets = info?.modelCatalogTarget === 1 && api?.getModelCatalogTarget ? ['codex', 'claude'].map(brand => api.getModelCatalogTarget(brand)) : undefined;
163
- const login = api?.getLoginPromptState?.();
164
127
  // L's same-epoch recovery wording is presentation metadata, not an execution change.
165
128
  const executionState = state && { instanceId: state.instanceId, epoch: state.epoch, phase: state.phase, needCode: state.need?.code };
166
129
  const executionTargets = targets?.map(result => result.ok ? result : { ok: false, code: result.error.code });
167
- const signature = JSON.stringify({ state: executionState, capability: info?.modelCatalogTarget, targets: executionTargets, logins: login?.logins, authorizing: login?.phase === 'authorizing' });
130
+ const signature = JSON.stringify({ state: executionState, capability: info?.modelCatalogTarget, targets: executionTargets });
168
131
  const switched = api !== this.api;
169
132
  const changed = this.initialized && (switched || signature !== this.signature);
170
133
  this.initialized = true;
@@ -183,9 +146,12 @@ export class ModelCatalog {
183
146
  this.signature = signature;
184
147
  if (changed) {
185
148
  this.revision++;
149
+ this.latest.clear();
186
150
  for (const controller of this.active)
187
151
  controller.abort();
188
152
  this.active.clear();
153
+ this.diagnostic('claude', undefined);
154
+ this.diagnostic('codex', undefined);
189
155
  this.changed();
190
156
  }
191
157
  }
@@ -198,7 +164,7 @@ export class ModelCatalog {
198
164
  fail('当前没有已验证的远程主机连接');
199
165
  const result = api.getModelCatalogTarget(agent.loginProvider);
200
166
  if (!result.ok)
201
- fail(result.error.message);
167
+ fail('远程主机连接暂未提供可信的模型目录目标,请到设置 → 模型查看诊断。', `模型目录目标不可用(诊断代码:${safe(result.error.code)}):${safe(result.error.message)}`);
202
168
  const target = result.value;
203
169
  if (target.version !== 1 || target.purpose !== 'model-catalog' || target.scope !== 'host-user' || target.provider !== agent.loginProvider || target.instanceId !== state.instanceId || target.epoch !== state.epoch)
204
170
  fail('模型目录目标与当前连接或品牌不匹配');
@@ -210,45 +176,121 @@ export class ModelCatalog {
210
176
  }
211
177
  return structuredClone(target);
212
178
  }
179
+ assertFlight(flight) {
180
+ if (flight.controller.signal.aborted || this.disposed || this.get() !== flight.api || flight.revision !== this.revision)
181
+ fail('连接已改变,原模型目录已失效');
182
+ const agent = { loginProvider: flight.target.provider }, current = this.trusted(agent, flight.api);
183
+ if (current.instanceId !== flight.target.instanceId || current.epoch !== flight.target.epoch || current.provider !== flight.target.provider || !sameTarget(current.target, flight.target.target))
184
+ fail('可信目标或连接代次已改变,原模型目录已失效');
185
+ }
186
+ failure(error) {
187
+ // Each consumer needs its own Error identity for its original input/signal proof.
188
+ return new CatalogError(error instanceof CatalogError || error instanceof ModelConfigurationError ? error.message.replace(/^无法获取模型列表:/, '') : '目录查询失败,请到设置 → 模型查看诊断。', error instanceof CatalogError ? error.diagnostic : error instanceof CatalogSshError ? error.diagnostic : error instanceof Error ? `${error instanceof LlmError ? error.code : 'MODEL_CATALOG_QUERY_FAILED'}:${safe(error.message)}` : 'MODEL_CATALOG_QUERY_FAILED:目录查询失败,根因未查明');
189
+ }
190
+ start(agent, api, target, max, key) {
191
+ const deferred = Promise.withResolvers(), controller = new AbortController();
192
+ const flight = { key, api, target, revision: this.revision, serial: ++this.serial, controller, pending: true, users: 0, promise: deferred.promise };
193
+ this.latest.set(agent.loginProvider, flight.serial);
194
+ this.flights.set(key, flight);
195
+ this.active.add(controller);
196
+ const current = () => !this.disposed && flight.revision === this.revision && api === this.get() && this.latest.get(agent.loginProvider) === flight.serial && !controller.signal.aborted;
197
+ const query = async () => {
198
+ try {
199
+ this.assertFlight(flight);
200
+ const output = await this.run(target.target, '/usr/local/aw-host/bin/aw-model-catalog --provider ' + agent.loginProvider, controller.signal, max);
201
+ this.assertFlight(flight);
202
+ const parsed = parseHostCatalog(output, agent.loginProvider, target.target.username), diagnostic = catalogDiagnostic(parsed.issues);
203
+ if (!parsed.models.length)
204
+ fail('当前没有可选模型,请到设置 → 模型查看诊断。', diagnostic ?? '主机返回的完整模型目录为空(诊断代码:MODEL_CATALOG_EMPTY)');
205
+ if (current())
206
+ this.diagnostic(agent.loginProvider, diagnostic);
207
+ if (current())
208
+ await this.loaded(agent, parsed.models, () => this.assertFlight(flight));
209
+ this.assertFlight(flight);
210
+ return parsed.models;
211
+ }
212
+ catch (error) {
213
+ const failure = this.failure(error);
214
+ if (current())
215
+ this.diagnostic(agent.loginProvider, failure.diagnostic);
216
+ throw failure;
217
+ }
218
+ };
219
+ const settled = () => { flight.pending = false; if (this.flights.get(key) === flight)
220
+ this.flights.delete(key); if (!flight.users)
221
+ this.active.delete(controller); };
222
+ void query().then(models => { settled(); deferred.resolve(models); }, error => { settled(); deferred.reject(error); });
223
+ return flight;
224
+ }
213
225
  async read(agent, signal) {
214
- let controller;
226
+ let flight, released = false;
227
+ const release = () => { if (released || !flight)
228
+ return; released = true; flight.users--; if (!flight.users) {
229
+ if (flight.pending)
230
+ flight.controller.abort();
231
+ else
232
+ this.active.delete(flight.controller);
233
+ } };
215
234
  try {
216
235
  if (this.disposed)
217
236
  fail('远程模型插件已关闭');
237
+ signal?.throwIfAborted();
218
238
  this.refresh();
219
239
  const api = this.get();
220
240
  if (!api)
221
241
  fail('远程主机连接组件没有装好');
222
- const target = freeze(this.trusted(agent, api)), revision = this.revision;
223
- const max = this.maxBytes();
242
+ const target = freeze(this.trusted(agent, api)), max = this.maxBytes();
224
243
  if (!Number.isSafeInteger(max) || max < 1)
225
244
  fail('acpMaxMessageBytes 设置无效');
226
- controller = new AbortController();
227
- this.active.add(controller);
228
- const combined = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
229
- const assertCurrent = () => { if (combined.aborted || this.disposed || this.get() !== api || revision !== this.revision)
230
- fail('连接已改变,原模型目录已失效'); const current = this.trusted(agent, api); if (current.instanceId !== target.instanceId || current.epoch !== target.epoch || current.provider !== target.provider || !sameTarget(current.target, target.target))
231
- fail('可信目标或连接代次已改变,原模型目录已失效'); };
232
- assertCurrent();
233
- const output = await this.run(target.target, '/usr/local/aw-host/bin/aw-model-catalog --provider ' + agent.loginProvider, combined, max);
245
+ const key = JSON.stringify([agent.loginProvider, target.instanceId, target.epoch]);
246
+ flight = this.flights.get(key);
247
+ if (flight && (flight.api !== api || flight.revision !== this.revision || !sameTarget(flight.target.target, target.target) || flight.controller.signal.aborted)) {
248
+ const closing = flight;
249
+ closing.controller.abort();
250
+ flight = undefined;
251
+ // Closing work owns the tuple until SSH/runner actually settles. This new
252
+ // caller waits for cleanup, then begins its own query; it never uses old data.
253
+ const waiting = new AbortController(), revision = this.revision;
254
+ this.active.add(waiting);
255
+ const waitSignal = signal ? AbortSignal.any([signal, waiting.signal]) : waiting.signal;
256
+ try {
257
+ await new Promise((resolve, reject) => { const abort = () => { waitSignal.removeEventListener('abort', abort); reject(new ModelConfigurationError('目录查询已取消')); }; waitSignal.addEventListener('abort', abort, { once: true }); closing.promise.then(() => { }, () => { }).then(() => { waitSignal.removeEventListener('abort', abort); resolve(); }); if (waitSignal.aborted)
258
+ abort(); });
259
+ }
260
+ finally {
261
+ this.active.delete(waiting);
262
+ }
263
+ if (revision !== this.revision || this.get() !== api)
264
+ fail('连接已改变,原模型目录已失效');
265
+ return this.read(agent, signal);
266
+ }
267
+ flight ??= this.start(agent, api, target, max, key);
268
+ flight.users++;
269
+ const own = flight, combined = signal ? AbortSignal.any([signal, own.controller.signal]) : own.controller.signal;
270
+ const assertCurrent = () => { if (combined.aborted)
271
+ fail('目录查询已取消'); this.assertFlight(own); };
272
+ const models = await new Promise((resolve, reject) => {
273
+ const abort = () => { combined.removeEventListener('abort', abort); reject(new ModelConfigurationError('目录查询已取消')); };
274
+ combined.addEventListener('abort', abort, { once: true });
275
+ own.promise.then(value => { combined.removeEventListener('abort', abort); resolve(value); }, error => { combined.removeEventListener('abort', abort); reject(error); });
276
+ if (combined.aborted)
277
+ abort();
278
+ });
234
279
  assertCurrent();
235
- const models = parseHostCatalog(output, agent.loginProvider, target.target.username);
236
- const own = controller;
237
- return { target, models, signal: combined, assertCurrent, release: () => this.active.delete(own) };
280
+ return { target: own.target, models, signal: combined, assertCurrent, release };
238
281
  }
239
282
  catch (error) {
240
- if (controller)
241
- this.active.delete(controller);
242
- if (error instanceof LlmError)
243
- throw error;
244
- if (error instanceof ModelConfigurationError)
245
- fail(error.message.replace(/^无法获取模型列表:/, ''));
246
- fail('目录查询失败,根因未查明');
283
+ release();
284
+ const failure = this.failure(error);
285
+ if (!flight && !this.disposed)
286
+ this.diagnostic(agent.loginProvider, failure.diagnostic);
287
+ throw failure;
247
288
  }
248
289
  }
249
- dispose() { this.disposed = true; this.unsubscribe?.(); for (const controller of this.active)
290
+ dispose() { this.disposed = true; this.unsubscribe?.(); this.flights.clear(); for (const controller of this.active)
250
291
  controller.abort(); this.active.clear(); }
251
292
  }
252
293
  export function modelInfo(provider, model) {
253
- return { provider, id: model.id, name: model.name, inputModalities: ['text', 'image'], reasoning: { efforts: model.reasoning.options.map(option => ({ id: ReasoningEffortId(option.value), name: option.name })), ...(model.reasoning.defaultKnown && model.reasoning.defaultValue !== null ? { defaultEffort: ReasoningEffortId(model.reasoning.defaultValue) } : {}) }, catalog: model };
294
+ // Official rc.2 represents no reasoning levels by absence, and rejects an empty efforts array.
295
+ return { provider, id: model.id, name: model.name, inputModalities: ['text', 'image'], ...(model.reasoning.options.length ? { reasoning: { efforts: model.reasoning.options.map(option => ({ id: ReasoningEffortId(option.value), name: option.name })), ...(model.reasoning.defaultKnown && model.reasoning.defaultValue !== null ? { defaultEffort: ReasoningEffortId(model.reasoning.defaultValue) } : {}) } } : {}), catalog: model };
254
296
  }
@@ -0,0 +1,29 @@
1
+ import type { AgentDefinition } from './agents.ts';
2
+ import { type HostModel } from './model-catalog.ts';
3
+ export interface DefaultSelection {
4
+ provider: string;
5
+ model: string;
6
+ reasoningEffort?: string;
7
+ }
8
+ /** Public agentDefaultModel service; writes below use the atomic config editor. */
9
+ export interface DefaultModelService {
10
+ currentSelection(): DefaultSelection;
11
+ }
12
+ interface DefaultEntry {
13
+ options: {
14
+ name?: string;
15
+ };
16
+ }
17
+ /** Public rc.2 configEditor.entries/edit subset; no ownerContext/private config access. */
18
+ export interface DefaultConfigEditor {
19
+ entries(): readonly DefaultEntry[];
20
+ edit(entry: DefaultEntry, change: (current: Record<string, unknown>, inherited: Record<string, unknown>) => Record<string, unknown>): Promise<void>;
21
+ }
22
+ export type DefaultSelectionWriter = (expected: DefaultSelection, next: DefaultSelection, assertCurrent: () => void) => Promise<void>;
23
+ /** The editor invokes the guard synchronously under its profile lock after reconciliation. */
24
+ export declare function createDefaultSelectionWriter(service: DefaultModelService, getEditor: () => DefaultConfigEditor | undefined): DefaultSelectionWriter;
25
+ export declare function createModelDefaults(service: DefaultModelService, report: (provider: string, message: string | undefined) => void, write?: DefaultSelectionWriter): {
26
+ observe(agent: AgentDefinition, models: readonly HostModel[], assertCurrent?: () => void): Promise<void>;
27
+ dispose(): void;
28
+ };
29
+ export {};
@@ -0,0 +1,62 @@
1
+ import { declaredDefault } from "./model-catalog.js";
2
+ const same = (a, b) => a.provider === b.provider && a.model === b.model && a.reasoningEffort === b.reasoningEffort;
3
+ /** The editor invokes the guard synchronously under its profile lock after reconciliation. */
4
+ export function createDefaultSelectionWriter(service, getEditor) {
5
+ return async (expected, next, assertCurrent) => {
6
+ const editor = getEditor();
7
+ const entries = editor?.entries().filter(entry => entry.options.name === '@deepseek-ai/dsh-agent-default-model');
8
+ if (!editor || entries?.length !== 1)
9
+ throw Error('缺少唯一可原子编辑的官方默认模型设置');
10
+ await editor.edit(entries[0], current => {
11
+ assertCurrent();
12
+ if (!same(expected, service.currentSelection()))
13
+ throw Error('员工已改变模型选择,本次默认更新已撤回');
14
+ const updated = { ...current, provider: next.provider, model: next.model };
15
+ Reflect.deleteProperty(updated, 'reasoningEffort');
16
+ if (next.reasoningEffort !== undefined)
17
+ Object.assign(updated, { reasoningEffort: next.reasoningEffort });
18
+ return updated;
19
+ });
20
+ };
21
+ }
22
+ export function createModelDefaults(service, report, write) {
23
+ let disposed = false, queue = Promise.resolve();
24
+ return {
25
+ observe(agent, models, assertCurrent = () => { }) {
26
+ const snapshot = { ...service.currentSelection() };
27
+ const job = queue.then(async () => {
28
+ if (disposed || snapshot.provider !== agent.id || !same(snapshot, service.currentSelection()))
29
+ return;
30
+ if (snapshot.model !== 'remote' && models.some(model => model.id === snapshot.model)) {
31
+ report(agent.id, undefined);
32
+ return;
33
+ }
34
+ const model = declaredDefault(agent, models);
35
+ const notice = model ? undefined : '提供方没有声明唯一的默认模型,请先选择模型。(诊断代码:MODEL_DEFAULT_UNAVAILABLE)';
36
+ const next = { provider: agent.id, model: model?.id ?? '', ...(model?.reasoning.defaultKnown && model.reasoning.defaultValue !== null ? { reasoningEffort: model.reasoning.defaultValue } : {}) };
37
+ try {
38
+ const guard = () => { assertCurrent(); if (disposed)
39
+ throw Error('默认设置消费已撤回'); };
40
+ guard();
41
+ if (!same(snapshot, next)) {
42
+ if (!write)
43
+ throw Error('缺少原子配置能力');
44
+ await write(snapshot, next, guard);
45
+ }
46
+ if (disposed)
47
+ return;
48
+ if (!same(next, service.currentSelection()))
49
+ throw Error('未确认官方默认模型设置已保存');
50
+ report(agent.id, notice);
51
+ }
52
+ catch {
53
+ if (!disposed)
54
+ report(agent.id, (notice ? notice + '\n' : '') + '未能确认默认模型设置已更新,员工新选择不会被迟到的自动更新覆盖;请在模型菜单选择当前目录中的模型。(诊断代码:MODEL_DEFAULT_SAVE_FAILED)');
55
+ }
56
+ });
57
+ queue = job;
58
+ return job;
59
+ },
60
+ dispose() { disposed = true; },
61
+ };
62
+ }
@@ -0,0 +1,22 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ export interface ModelInputRetentionOptions {
3
+ isOurProvider: (provider: string) => boolean;
4
+ diagnostic: (provider: string, message: string | undefined) => void;
5
+ diagnosticForAttempt?: (position: {
6
+ sessionId: string;
7
+ turn: number;
8
+ step: number;
9
+ }) => ((provider: string, code: string) => Promise<void>);
10
+ preflightDiagnostic?: (provider: string, message: string | undefined) => void;
11
+ }
12
+ /** Mark only at the actual X adapter's preparation catch, then rethrow the same
13
+ * error. One error object can prove only its first preparation, for that signal.
14
+ * Public request config is an intermediate proposal and is not route evidence. */
15
+ export declare function markModelPreparationFailure(provider: string, error: unknown, signal?: AbortSignal): void;
16
+ /**
17
+ * Retain a claimed human batch only after a known, undispatched metadata prepare
18
+ * failure. Restoration uses the public durable inbox without sending or waking
19
+ * the agent. This cannot cover errors before pre-step/request, or unknown core
20
+ * admission failures; it never manufactures transcript messages or model routes.
21
+ */
22
+ export declare function installModelInputRetention(ctx: Context, options: ModelInputRetentionOptions): void;