@ai-workbench/remote-agent 0.8.19 → 0.8.23
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 +27 -134
- package/dist/acp.d.ts +23 -16
- package/dist/acp.js +209 -226
- package/dist/agents.d.ts +0 -1
- package/dist/agents.js +2 -2
- package/dist/broker-runtime.d.ts +3 -5
- package/dist/broker-runtime.js +45 -342
- package/dist/broker.d.ts +6 -31
- package/dist/broker.js +42 -113
- package/dist/catalog-ssh.d.ts +2 -31
- package/dist/catalog-ssh.js +9 -39
- package/dist/claude-local.d.ts +1 -1
- package/dist/claude-local.js +41 -105
- package/dist/client.js +34 -126
- package/dist/close-diagnostics.d.ts +6 -20
- package/dist/close-diagnostics.js +13 -57
- package/dist/connect-contract.d.ts +4 -8
- package/dist/conversation-details.d.ts +0 -29
- package/dist/conversation-details.js +8 -99
- package/dist/employee-endpoint.d.ts +1 -1
- package/dist/employee-endpoint.js +21 -19
- package/dist/employee-exec.d.ts +4 -3
- package/dist/employee-exec.js +25 -13
- package/dist/employee-info.d.ts +7 -1
- package/dist/employee-info.js +24 -3
- package/dist/employee-location.js +3 -4
- package/dist/employee-mcp.js +44 -69
- package/dist/employee-native.d.ts +1 -3
- package/dist/employee-native.js +6 -27
- package/dist/employee-presentation.d.ts +0 -5
- package/dist/employee-presentation.js +0 -15
- package/dist/employee-provider.d.ts +3 -11
- package/dist/employee-provider.js +69 -205
- package/dist/employee-reasons.js +1 -5
- package/dist/employee-shell.d.ts +2 -1
- package/dist/employee-shell.js +10 -13
- package/dist/employee-ssh.d.ts +1 -3
- package/dist/employee-ssh.js +13 -16
- package/dist/employee-tools.d.ts +5 -5
- package/dist/failure-evidence.d.ts +2 -10
- package/dist/failure-evidence.js +12 -61
- package/dist/host-workspace.d.ts +4 -2
- package/dist/host-workspace.js +6 -2
- package/dist/index.d.ts +13 -43
- package/dist/index.js +192 -484
- package/dist/input.js +2 -3
- package/dist/local-user-turn.js +5 -6
- package/dist/mapping.d.ts +5 -3
- package/dist/mapping.js +31 -36
- package/dist/model-catalog.d.ts +2 -41
- package/dist/model-catalog.js +24 -282
- package/dist/model-configuration.d.ts +0 -3
- package/dist/model-configuration.js +8 -47
- package/dist/model-presentation.d.ts +1 -0
- package/dist/model-presentation.js +2 -2
- package/dist/permissions.d.ts +15 -0
- package/dist/permissions.js +30 -0
- package/dist/preflight-diagnostic.js +11 -22
- package/dist/preflight-payload.d.ts +1 -1
- package/dist/preflight-payload.js +2 -2
- package/dist/presentation.js +21 -41
- package/dist/provider.d.ts +12 -34
- package/dist/provider.js +17 -92
- package/dist/readiness.js +1 -7
- package/dist/remote-details.d.ts +41 -17
- package/dist/remote-details.js +84 -381
- package/dist/workspace-source.d.ts +1 -25
- package/dist/workspace-source.js +1 -138
- package/package.json +11 -12
- package/dist/catalog-diagnostics.d.ts +0 -16
- package/dist/catalog-diagnostics.js +0 -108
- package/dist/host-layout.d.ts +0 -8
- package/dist/host-layout.js +0 -9
- package/dist/job.d.ts +0 -17
- package/dist/job.js +0 -57
- package/dist/model-defaults.d.ts +0 -29
- package/dist/model-defaults.js +0 -62
- package/dist/oneshot.d.ts +0 -13
- package/dist/oneshot.js +0 -78
- package/dist/protocol-boundary.d.ts +0 -13
- package/dist/protocol-boundary.js +0 -49
- package/dist/provider-locale.d.ts +0 -19
- package/dist/provider-locale.js +0 -73
- package/dist/recovery-checkpoint.d.ts +0 -6
- package/dist/recovery-checkpoint.js +0 -71
- package/dist/recovery.d.ts +0 -51
- package/dist/recovery.js +0 -61
- package/dist/sdk-ssh-wrapper.d.ts +0 -2
- package/dist/sdk-ssh-wrapper.js +0 -34
- package/dist/stage-timing.d.ts +0 -31
- package/dist/stage-timing.js +0 -61
- package/dist/workspace-notice.d.ts +0 -47
- package/dist/workspace-notice.js +0 -273
- package/dist/workspace-source-confirmation.d.ts +0 -4
- package/dist/workspace-source-confirmation.js +0 -12
package/dist/input.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { NeedError } from "./remote.js";
|
|
2
|
-
import { isWorkspaceNoticeMessage, withoutWorkspaceNotice } from "./workspace-notice.js";
|
|
3
2
|
function runtimeSections(message) {
|
|
4
3
|
const source = message.source;
|
|
5
4
|
if (message.role !== 'user' || source?.kind !== 'runtime-context' || source.form !== 'snapshot' || !Array.isArray(source.sections) || message.content.length !== 1 || message.content[0].type !== 'text')
|
|
@@ -52,7 +51,7 @@ async function publicBlock(block, options, readImage, history = false) {
|
|
|
52
51
|
}
|
|
53
52
|
/** Role-labelled public history only; never serialize source/replay/tool internals. */
|
|
54
53
|
export async function prepareContinuation(options, input, readImage) {
|
|
55
|
-
const messages = options.messages
|
|
54
|
+
const messages = options.messages;
|
|
56
55
|
for (const message of messages)
|
|
57
56
|
if (!['user', 'assistant', 'system', 'developer'].includes(message.role))
|
|
58
57
|
throw new NeedError('HISTORY_CONTENT_UNSUPPORTED', `此前含有无法可靠承接的历史角色:${message.role};本轮未新建或发送`, '完整公开的用户及助手历史;工具记录的公开内容映射');
|
|
@@ -94,7 +93,7 @@ export async function prepareContinuation(options, input, readImage) {
|
|
|
94
93
|
/** The attachment service supplies verified bytes; client paths never cross SSH. */
|
|
95
94
|
export async function prepareInput(options, readImage) {
|
|
96
95
|
options.signal?.throwIfAborted();
|
|
97
|
-
const messages = options.messages
|
|
96
|
+
const messages = options.messages;
|
|
98
97
|
const context = [];
|
|
99
98
|
if (options.system)
|
|
100
99
|
context.push('系统上下文:\n' + options.system);
|
package/dist/local-user-turn.js
CHANGED
|
@@ -42,14 +42,13 @@ export async function readLocalUserTurn(ctx, request) {
|
|
|
42
42
|
step = undefined;
|
|
43
43
|
input = undefined;
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
// Human proof belongs to the native turn; approval continuations add a new
|
|
46
|
+
// step without re-emitting that human message. An active own step is still
|
|
47
|
+
// required, and turn/start/end above permanently clear the previous input.
|
|
48
|
+
else if (e.type === 'step/start')
|
|
46
49
|
step = e;
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
else if (e.type === 'step/end') {
|
|
50
|
+
else if (e.type === 'step/end')
|
|
50
51
|
step = undefined;
|
|
51
|
-
input = undefined;
|
|
52
|
-
}
|
|
53
52
|
else if (e.type === 'user/message' && step && e.data.source.kind === 'user')
|
|
54
53
|
input = e;
|
|
55
54
|
else if (e.type === 'request/header')
|
package/dist/mapping.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
2
2
|
import type { PromptResponse, SessionUpdate } from '@agentclientprotocol/sdk/unstable';
|
|
3
|
-
import type { ThreadEvent } from '@openai/codex-sdk';
|
|
4
3
|
import { type SanitizedObservation } from './conversation-details.ts';
|
|
5
4
|
/** Persist alongside the exact remote session identity; null means no trusted baseline. */
|
|
6
5
|
export type UsageSnapshot = {
|
|
@@ -23,9 +22,11 @@ export type UsageSnapshot = {
|
|
|
23
22
|
export declare class StreamMapper {
|
|
24
23
|
private index;
|
|
25
24
|
private active;
|
|
26
|
-
private seen;
|
|
27
25
|
private baseline;
|
|
28
26
|
private readonly observations;
|
|
27
|
+
private readonly modelMetadata;
|
|
28
|
+
private anonymousMetadata;
|
|
29
|
+
private lastMetadataType;
|
|
29
30
|
private readonly onDiagnostic;
|
|
30
31
|
/** Omitted argument is exclusively for a known new session; pass null on unknown resume. */
|
|
31
32
|
constructor(previousUsage?: UsageSnapshot | null, onDiagnostic?: (observation: Readonly<SanitizedObservation>) => void);
|
|
@@ -33,6 +34,8 @@ export declare class StreamMapper {
|
|
|
33
34
|
get diagnostics(): readonly Readonly<SanitizedObservation>[];
|
|
34
35
|
private diagnostic;
|
|
35
36
|
private usageWarning;
|
|
37
|
+
/** One durable observation per official message; keep ordered, sanitized fragment snapshots. */
|
|
38
|
+
flushMetadata(): void;
|
|
36
39
|
text(text: string, reasoning?: boolean): StreamChunk[];
|
|
37
40
|
/** Close the current text or reasoning block once, before the stream settles. */
|
|
38
41
|
flush(): StreamChunk[];
|
|
@@ -42,5 +45,4 @@ export declare class StreamMapper {
|
|
|
42
45
|
private usageGap;
|
|
43
46
|
private cumulativeUsage;
|
|
44
47
|
private usage;
|
|
45
|
-
sdk(event: ThreadEvent): StreamChunk[];
|
|
46
48
|
}
|
package/dist/mapping.js
CHANGED
|
@@ -6,9 +6,11 @@ const optionalUsageCounters = ['cachedReadTokens', 'cachedWriteTokens', 'thought
|
|
|
6
6
|
export class StreamMapper {
|
|
7
7
|
index = 0;
|
|
8
8
|
active;
|
|
9
|
-
seen = new Map();
|
|
10
9
|
baseline;
|
|
11
10
|
observations = [];
|
|
11
|
+
modelMetadata = new Map();
|
|
12
|
+
anonymousMetadata = 0;
|
|
13
|
+
lastMetadataType;
|
|
12
14
|
onDiagnostic;
|
|
13
15
|
/** Omitted argument is exclusively for a known new session; pass null on unknown resume. */
|
|
14
16
|
constructor(previousUsage = { version: 1, kind: 'zero' }, onDiagnostic) {
|
|
@@ -30,6 +32,15 @@ export class StreamMapper {
|
|
|
30
32
|
usageWarning(summary) {
|
|
31
33
|
return this.diagnostic({ kind: 'usage', severity: 'warning', code: 'USAGE_UNAVAILABLE', summary });
|
|
32
34
|
}
|
|
35
|
+
/** One durable observation per official message; keep ordered, sanitized fragment snapshots. */
|
|
36
|
+
flushMetadata() {
|
|
37
|
+
for (const [key, payload] of this.modelMetadata) {
|
|
38
|
+
// A throwing sink may already have enqueued its write. Never replay that message.
|
|
39
|
+
this.modelMetadata.delete(key);
|
|
40
|
+
this.diagnostic({ kind: 'extension', severity: 'info', code: 'ACP_MODEL_METADATA', summary: '远端模型消息扩展(按消息汇总)', method: 'session/update', payload });
|
|
41
|
+
}
|
|
42
|
+
this.lastMetadataType = undefined;
|
|
43
|
+
}
|
|
33
44
|
text(text, reasoning = false) {
|
|
34
45
|
if (!text)
|
|
35
46
|
return [];
|
|
@@ -55,15 +66,31 @@ export class StreamMapper {
|
|
|
55
66
|
acp(update) {
|
|
56
67
|
if (update.sessionUpdate === 'agent_message_chunk' || update.sessionUpdate === 'agent_thought_chunk') {
|
|
57
68
|
if (update.content.type === 'text' && typeof update.content.text === 'string') {
|
|
58
|
-
|
|
69
|
+
const messageId = typeof update.messageId === 'string' ? update.messageId : undefined;
|
|
70
|
+
if (messageId !== undefined)
|
|
71
|
+
this.lastMetadataType = undefined;
|
|
72
|
+
else {
|
|
73
|
+
if (this.lastMetadataType !== update.sessionUpdate)
|
|
74
|
+
this.anonymousMetadata++;
|
|
75
|
+
this.lastMetadataType = update.sessionUpdate;
|
|
76
|
+
}
|
|
77
|
+
// Capture each extension layer now, before the caller can mutate it. Text mapping is unchanged.
|
|
59
78
|
if (Object.keys(update).some(key => key !== 'sessionUpdate' && key !== 'content')
|
|
60
79
|
|| Object.keys(update.content).some(key => key !== 'type' && key !== 'text')) {
|
|
61
|
-
|
|
80
|
+
const key = JSON.stringify(messageId !== undefined ? ['message', messageId] : ['segment', this.anonymousMetadata]);
|
|
81
|
+
let pending = this.modelMetadata.get(key);
|
|
82
|
+
if (!pending) {
|
|
83
|
+
pending = { sessionUpdate: update.sessionUpdate, ...(messageId !== undefined ? { messageId } : {}), fragments: [] };
|
|
84
|
+
this.modelMetadata.set(key, pending);
|
|
85
|
+
}
|
|
86
|
+
pending.fragments.push(sanitizeObservation({ kind: 'extension', severity: 'info', code: 'ACP_MODEL_METADATA', summary: '远端模型消息扩展', payload: update }).payload);
|
|
62
87
|
}
|
|
63
88
|
return this.text(update.content.text, update.sessionUpdate === 'agent_thought_chunk');
|
|
64
89
|
}
|
|
90
|
+
this.lastMetadataType = undefined;
|
|
65
91
|
return this.diagnostic({ kind: 'extension', severity: 'warning', code: 'ACP_CONTENT_UNSUPPORTED', summary: '远端非文本内容,需要对应的公开显示映射', method: 'session/update', payload: update });
|
|
66
92
|
}
|
|
93
|
+
this.lastMetadataType = undefined;
|
|
67
94
|
let kind = 'session';
|
|
68
95
|
let code = 'ACP_SESSION_UPDATE';
|
|
69
96
|
let summary = '远端会话事件';
|
|
@@ -111,6 +138,7 @@ export class StreamMapper {
|
|
|
111
138
|
}
|
|
112
139
|
/** ACP usage and the fixed Claude quota both accumulate across the session, not per turn. */
|
|
113
140
|
acpResponse(response) {
|
|
141
|
+
this.flushMetadata();
|
|
114
142
|
this.diagnostic({ kind: 'usage', severity: 'info', code: 'ACP_PROMPT_RESPONSE', summary: '远端响应与真实用量', method: 'session/prompt', payload: response });
|
|
115
143
|
const quota = record(response._meta?.quota);
|
|
116
144
|
const tokens = record(quota?.token_count);
|
|
@@ -180,39 +208,6 @@ export class StreamMapper {
|
|
|
180
208
|
...(read == null ? {} : { cacheReadTokens: read }), ...(write == null ? {} : { cacheWriteTokens: write }),
|
|
181
209
|
...(thought == null ? {} : { reasoningTokens: thought }) } }];
|
|
182
210
|
}
|
|
183
|
-
sdk(event) {
|
|
184
|
-
if (event.type === 'item.started' || event.type === 'item.updated' || event.type === 'item.completed') {
|
|
185
|
-
const item = event.item;
|
|
186
|
-
if ((item.type === 'agent_message' || item.type === 'reasoning') && typeof item.text === 'string') {
|
|
187
|
-
if (Object.keys(event).some(key => key !== 'type' && key !== 'item')
|
|
188
|
-
|| Object.keys(item).some(key => key !== 'id' && key !== 'type' && key !== 'text')) {
|
|
189
|
-
this.diagnostic({ kind: 'extension', severity: 'info', code: 'SDK_MODEL_METADATA', summary: '远端模型消息扩展', method: event.type, payload: event });
|
|
190
|
-
}
|
|
191
|
-
const previous = this.seen.get(item.id) ?? '';
|
|
192
|
-
if (!item.text.startsWith(previous))
|
|
193
|
-
throw new NeedError('EVENT_REWRITE_UNSUPPORTED', '远端更改了已输出的文本,当前流不能无损替换', '支持文本替换的展示适配器');
|
|
194
|
-
this.seen.set(item.id, item.text);
|
|
195
|
-
return this.text(item.text.slice(previous.length), item.type === 'reasoning');
|
|
196
|
-
}
|
|
197
|
-
const tool = item.type === 'command_execution' || item.type === 'file_change' || item.type === 'mcp_tool_call';
|
|
198
|
-
this.diagnostic({ kind: tool ? 'tool' : 'session', severity: item.type === 'error' ? 'warning' : 'info', code: tool ? 'SDK_TOOL' : 'SDK_ITEM', summary: '远端非模型事件', method: event.type, payload: event });
|
|
199
|
-
if (item.type === 'error')
|
|
200
|
-
throw new NeedError('ONESHOT_FAILED', 'Codex 返回了错误,根因未查明', '远端诊断');
|
|
201
|
-
return [];
|
|
202
|
-
}
|
|
203
|
-
this.diagnostic({ kind: event.type === 'turn.completed' ? 'usage' : 'session', severity: event.type === 'error' || event.type === 'turn.failed' ? 'warning' : 'info', code: 'SDK_EVENT', summary: '远端会话事件', method: event.type, payload: event });
|
|
204
|
-
if (event.type === 'turn.completed') {
|
|
205
|
-
const u = event.usage;
|
|
206
|
-
const write = u.cache_write_input_tokens ?? 0;
|
|
207
|
-
const uncached = u.input_tokens - u.cached_input_tokens - write;
|
|
208
|
-
if (![u.input_tokens, u.cached_input_tokens, write, u.output_tokens, uncached].every(n => Number.isSafeInteger(n) && n >= 0))
|
|
209
|
-
throw new NeedError('USAGE_INVALID', '远端用量计数不一致', '有效的 Codex 用量');
|
|
210
|
-
return [{ type: 'usage', usage: { inputTokens: uncached, cacheReadTokens: u.cached_input_tokens, outputTokens: u.output_tokens,
|
|
211
|
-
...(u.cache_write_input_tokens !== undefined ? { cacheWriteTokens: write } : {}),
|
|
212
|
-
...(u.reasoning_output_tokens !== undefined ? { reasoningTokens: u.reasoning_output_tokens } : {}) } }];
|
|
213
|
-
}
|
|
214
|
-
return [];
|
|
215
|
-
}
|
|
216
211
|
}
|
|
217
212
|
function validCount(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0; }
|
|
218
213
|
function validUsageSnapshot(value) {
|
package/dist/model-catalog.d.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import type { CloseChange } from './close-diagnostics.ts';
|
|
2
1
|
import { type LlmResolvedModelInfo } from '@deepseek-ai/dsh-llm';
|
|
3
2
|
import type { ModelCatalogTarget } from '@ai-workbench/connect/api';
|
|
4
3
|
import type { AgentDefinition } from './agents.ts';
|
|
5
4
|
import { type ConnectApi, type ConnectTarget } from './provider.ts';
|
|
6
|
-
import type { DetailObservation } from './conversation-details.ts';
|
|
7
5
|
export { runCatalog } from './catalog-ssh.ts';
|
|
8
6
|
export interface HostModel {
|
|
9
7
|
id: string;
|
|
@@ -20,20 +18,11 @@ export interface HostModel {
|
|
|
20
18
|
};
|
|
21
19
|
source: unknown;
|
|
22
20
|
}
|
|
23
|
-
export interface CatalogRevocation {
|
|
24
|
-
readonly fields: readonly CatalogChangeField[];
|
|
25
|
-
readonly changes: readonly CloseChange[];
|
|
26
|
-
readonly reason?: 'MODEL_CATALOG_REVOKED' | 'MODEL_CATALOG_SCOPE_CHANGED';
|
|
27
|
-
}
|
|
28
21
|
export interface CatalogLease {
|
|
29
|
-
revocation?(): CatalogRevocation | undefined;
|
|
30
|
-
bindSelection?(id: string, effort?: string): void;
|
|
31
22
|
readonly target: ModelCatalogTarget;
|
|
32
23
|
readonly models: readonly HostModel[];
|
|
33
24
|
readonly signal: AbortSignal;
|
|
34
|
-
assertCurrent(): void;
|
|
35
25
|
release(): void;
|
|
36
|
-
onInformation?(listener: (entry: DetailObservation) => void): () => void;
|
|
37
26
|
}
|
|
38
27
|
export interface ModelDirectory {
|
|
39
28
|
read(agent: AgentDefinition, signal?: AbortSignal): Promise<CatalogLease>;
|
|
@@ -49,41 +38,13 @@ export interface ParsedCatalog {
|
|
|
49
38
|
readonly issues: readonly CatalogIssue[];
|
|
50
39
|
}
|
|
51
40
|
export declare function catalogDiagnostic(issues: readonly CatalogIssue[]): string | undefined;
|
|
52
|
-
/** Defaults are provider declarations, never catalog order or display names. */
|
|
53
|
-
export declare function declaredDefault(agent: AgentDefinition, models: readonly HostModel[]): HostModel | undefined;
|
|
54
41
|
export declare function parseHostCatalog(input: string, provider: string, username: string): ParsedCatalog;
|
|
55
|
-
|
|
56
|
-
/** Share only pending work; settled results are never cached. */
|
|
42
|
+
/** Queried at startup and by the model menu. No execution admission or revocation. */
|
|
57
43
|
export declare class ModelCatalog implements ModelDirectory {
|
|
58
44
|
private get;
|
|
59
45
|
private maxBytes;
|
|
60
46
|
private run;
|
|
61
|
-
|
|
62
|
-
private diagnostic;
|
|
63
|
-
private loaded;
|
|
64
|
-
private revoking;
|
|
65
|
-
private api?;
|
|
66
|
-
private unsubscribe?;
|
|
67
|
-
private signature?;
|
|
68
|
-
private revision;
|
|
69
|
-
private disposed;
|
|
70
|
-
private initialized;
|
|
71
|
-
private active;
|
|
72
|
-
private revocations;
|
|
73
|
-
private presentation?;
|
|
74
|
-
private presentationNeedCode?;
|
|
75
|
-
private information;
|
|
76
|
-
private flights;
|
|
77
|
-
private selections;
|
|
78
|
-
private serial;
|
|
79
|
-
private latest;
|
|
80
|
-
constructor(get: () => ConnectApi | undefined, maxBytes: () => number | undefined, run?: CatalogRunner, changed?: () => void, diagnostic?: (provider: 'claude' | 'codex', message: string | undefined) => void, loaded?: (agent: AgentDefinition, models: readonly HostModel[], assertCurrent: () => void) => void | Promise<void>, revoking?: (fields: readonly CatalogChangeField[], changes: readonly CloseChange[]) => void);
|
|
81
|
-
refresh(): void;
|
|
82
|
-
private trusted;
|
|
83
|
-
private assertFlight;
|
|
84
|
-
private failure;
|
|
85
|
-
private checkSelections;
|
|
86
|
-
private start;
|
|
47
|
+
constructor(get: () => ConnectApi | undefined, maxBytes: () => number | undefined, run?: CatalogRunner);
|
|
87
48
|
read(agent: AgentDefinition, signal?: AbortSignal): Promise<CatalogLease>;
|
|
88
49
|
dispose(): void;
|
|
89
50
|
}
|
package/dist/model-catalog.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
1
|
import { LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { runCatalog, CatalogSshError, catalogSafeText } from "./catalog-ssh.js";
|
|
2
|
+
import { validateTarget } from "./provider.js";
|
|
3
|
+
import { runCatalog, catalogSafeText } from "./catalog-ssh.js";
|
|
6
4
|
export { runCatalog } from "./catalog-ssh.js";
|
|
7
5
|
const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
8
6
|
const text = (value) => typeof value === 'string' && value.trim().length > 0;
|
|
@@ -20,11 +18,6 @@ function freeze(value) { if (value && typeof value === 'object') {
|
|
|
20
18
|
export function catalogDiagnostic(issues) {
|
|
21
19
|
return issues.length ? '以下模型未加入可选列表:\n' + issues.map(issue => `模型「${issue.modelName}」:${issue.reason}(诊断代码:${issue.code})`).join('\n') : undefined;
|
|
22
20
|
}
|
|
23
|
-
/** Defaults are provider declarations, never catalog order or display names. */
|
|
24
|
-
export function declaredDefault(agent, models) {
|
|
25
|
-
const selected = models.filter(model => agent.loginProvider === 'claude' ? model.value === 'default' : object(model.source) && model.source.isDefault === true);
|
|
26
|
-
return selected.length === 1 ? selected[0] : undefined;
|
|
27
|
-
}
|
|
28
21
|
export function parseHostCatalog(input, provider, username) {
|
|
29
22
|
let wire;
|
|
30
23
|
try {
|
|
@@ -39,7 +32,7 @@ export function parseHostCatalog(input, provider, username) {
|
|
|
39
32
|
const error = object(wire.error) ? wire.error : undefined;
|
|
40
33
|
const code = typeof error?.code === 'string' && /^[A-Z][A-Z0-9_]*$/.test(error.code) ? error.code : 'PROTOCOL_INVALID';
|
|
41
34
|
const reason = text(error?.message) ? safe(error.message) : '主机未提供失败说明';
|
|
42
|
-
fail('
|
|
35
|
+
fail('主机目录查询失败,请检查连接后重新打开模型菜单。', `主机目录查询失败(诊断代码:${code}):${reason}`);
|
|
43
36
|
}
|
|
44
37
|
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))
|
|
45
38
|
fail('主机目录缺少完整元数据');
|
|
@@ -93,288 +86,37 @@ export function parseHostCatalog(input, provider, username) {
|
|
|
93
86
|
}
|
|
94
87
|
return freeze(structuredClone({ models, issues }));
|
|
95
88
|
}
|
|
96
|
-
|
|
97
|
-
function changedCatalogFields(previous, next, switched) {
|
|
98
|
-
const before = previous ? JSON.parse(previous) : {}, after = JSON.parse(next), fields = [];
|
|
99
|
-
if (switched)
|
|
100
|
-
fields.push('api-instance');
|
|
101
|
-
for (const [key, label] of [['instanceId', 'connection-instance'], ['epoch', 'connection-epoch'], ['phase', 'connection-phase'], ['needCode', 'connection-need-code']])
|
|
102
|
-
if (JSON.stringify(before.state?.[key]) !== JSON.stringify(after.state?.[key]))
|
|
103
|
-
fields.push(label);
|
|
104
|
-
if (before.capability !== after.capability)
|
|
105
|
-
fields.push('catalog-capability');
|
|
106
|
-
for (const [index, label] of [[0, 'codex-target'], [1, 'claude-target']])
|
|
107
|
-
if (JSON.stringify(before.targets?.[index]) !== JSON.stringify(after.targets?.[index]))
|
|
108
|
-
fields.push(label);
|
|
109
|
-
return fields.length ? fields : ['other-metadata'];
|
|
110
|
-
}
|
|
111
|
-
function catalogChanges(previous, next, switched) {
|
|
112
|
-
const before = previous ? JSON.parse(previous) : {}, after = JSON.parse(next), changes = [];
|
|
113
|
-
const scalar = (value) => typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ? value : null;
|
|
114
|
-
const compare = (field, old, current) => { if (JSON.stringify(old) !== JSON.stringify(current))
|
|
115
|
-
changes.push({ field, before: scalar(old), after: scalar(current) }); };
|
|
116
|
-
const fingerprint = (value) => value === undefined ? null : createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
117
|
-
if (switched)
|
|
118
|
-
changes.push({ field: 'catalog.api-replaced', before: false, after: true });
|
|
119
|
-
for (const [key, name] of [['instanceId', 'instance-id'], ['epoch', 'epoch'], ['phase', 'phase'], ['needCode', 'need-code']])
|
|
120
|
-
compare('connection.' + name, before.state?.[key], after.state?.[key]);
|
|
121
|
-
compare('catalog.capability', before.capability, after.capability);
|
|
122
|
-
for (const [index, provider] of [[0, 'codex'], [1, 'claude']]) {
|
|
123
|
-
const a = before.targets?.[index], b = after.targets?.[index], prefix = 'catalog.' + provider + '.';
|
|
124
|
-
compare(prefix + 'ok', a?.ok, b?.ok);
|
|
125
|
-
compare(prefix + 'error-code', a?.code, b?.code);
|
|
126
|
-
for (const [key, name] of [['instanceId', 'instance-id'], ['epoch', 'epoch'], ['provider', 'provider']])
|
|
127
|
-
compare(prefix + name, a?.value?.[key], b?.value?.[key]);
|
|
128
|
-
// Protect executable argv, trust/config paths and arbitrary future metadata: values are digests.
|
|
129
|
-
compare(prefix + 'target-sha256', fingerprint(a?.value?.target), fingerprint(b?.value?.target));
|
|
130
|
-
compare(prefix + 'metadata-sha256', fingerprint(a), fingerprint(b));
|
|
131
|
-
}
|
|
132
|
-
if (!changes.length)
|
|
133
|
-
changes.push({ field: 'catalog.signature-sha256', before: fingerprint(before), after: fingerprint(after) });
|
|
134
|
-
return changes;
|
|
135
|
-
}
|
|
136
|
-
/** Share only pending work; settled results are never cached. */
|
|
89
|
+
/** Queried at startup and by the model menu. No execution admission or revocation. */
|
|
137
90
|
export class ModelCatalog {
|
|
138
91
|
get;
|
|
139
92
|
maxBytes;
|
|
140
93
|
run;
|
|
141
|
-
|
|
142
|
-
diagnostic;
|
|
143
|
-
loaded;
|
|
144
|
-
revoking;
|
|
145
|
-
api;
|
|
146
|
-
unsubscribe;
|
|
147
|
-
signature;
|
|
148
|
-
revision = 0;
|
|
149
|
-
disposed = false;
|
|
150
|
-
initialized = false;
|
|
151
|
-
active = new Set();
|
|
152
|
-
revocations = new WeakMap();
|
|
153
|
-
presentation;
|
|
154
|
-
presentationNeedCode;
|
|
155
|
-
information = new Set();
|
|
156
|
-
flights = new Map();
|
|
157
|
-
selections = new Set();
|
|
158
|
-
serial = 0;
|
|
159
|
-
latest = new Map();
|
|
160
|
-
constructor(get, maxBytes, run = runCatalog, changed = () => { }, diagnostic = () => { }, loaded = () => { }, revoking = () => { }) {
|
|
94
|
+
constructor(get, maxBytes, run = runCatalog) {
|
|
161
95
|
this.get = get;
|
|
162
96
|
this.maxBytes = maxBytes;
|
|
163
97
|
this.run = run;
|
|
164
|
-
this.changed = changed;
|
|
165
|
-
this.diagnostic = diagnostic;
|
|
166
|
-
this.loaded = loaded;
|
|
167
|
-
this.revoking = revoking;
|
|
168
|
-
this.refresh();
|
|
169
|
-
}
|
|
170
|
-
refresh() {
|
|
171
|
-
const api = this.get();
|
|
172
|
-
const state = api?.getConnectionState?.();
|
|
173
|
-
const info = api?.getContractInfo?.();
|
|
174
|
-
const targets = info?.modelCatalogTarget === 1 && api?.getModelCatalogTarget ? ['codex', 'claude'].map(brand => api.getModelCatalogTarget(brand)) : undefined;
|
|
175
|
-
// L's same-epoch recovery wording is presentation metadata, not an execution change.
|
|
176
|
-
const executionState = state && { instanceId: state.instanceId, epoch: state.epoch, phase: state.phase };
|
|
177
|
-
const executionTargets = targets?.map(result => result.ok ? result : { ok: false, code: result.error.code });
|
|
178
|
-
const signature = semanticJson({ state: executionState, capability: info?.modelCatalogTarget, targets: executionTargets });
|
|
179
|
-
const switched = api !== this.api;
|
|
180
|
-
const changed = this.initialized && (switched || signature !== this.signature);
|
|
181
|
-
const presentation = createHash('sha256').update(JSON.stringify({ need: { code: state?.need?.code, message: state?.need?.message, requires: state?.need?.requires }, recovery: state?.recovery && { phase: state.recovery.phase, message: state.recovery.message, startedAt: state.recovery.startedAt, administratorAfterMs: state.recovery.administratorAfterMs } })).digest('hex');
|
|
182
|
-
if (this.initialized && !changed && presentation !== this.presentation) {
|
|
183
|
-
const entry = { kind: 'session', severity: 'info', code: 'CATALOG_SCOPE_INFORMATION', summary: '连接说明已变化;主机身份、连接代际和可信目标未变,本轮继续。', payload: { reason: 'CATALOG_PRESENTATION_CHANGED', before: { needCode: this.presentationNeedCode ?? null, presentationSha256: this.presentation ?? null }, after: { needCode: state?.need?.code ?? null, presentationSha256: presentation } } };
|
|
184
|
-
for (const listener of this.information)
|
|
185
|
-
listener(entry);
|
|
186
|
-
}
|
|
187
|
-
this.presentation = presentation;
|
|
188
|
-
this.presentationNeedCode = state?.need?.code;
|
|
189
|
-
this.initialized = true;
|
|
190
|
-
if (switched) {
|
|
191
|
-
this.unsubscribe?.();
|
|
192
|
-
this.unsubscribe = undefined;
|
|
193
|
-
this.api = api;
|
|
194
|
-
const off = [];
|
|
195
|
-
if (typeof api?.onConnectionState === 'function')
|
|
196
|
-
off.push(api.onConnectionState(() => this.refresh()));
|
|
197
|
-
if (typeof api?.onLoginPromptState === 'function')
|
|
198
|
-
off.push(api.onLoginPromptState(() => this.refresh()));
|
|
199
|
-
this.unsubscribe = () => { for (const stop of off)
|
|
200
|
-
stop(); };
|
|
201
|
-
}
|
|
202
|
-
const changedFields = changed ? changedCatalogFields(this.signature, signature, switched) : [], changes = changed ? catalogChanges(this.signature, signature, switched) : [];
|
|
203
|
-
this.signature = signature;
|
|
204
|
-
if (changed) {
|
|
205
|
-
this.revoking(changedFields, changes);
|
|
206
|
-
this.revision++;
|
|
207
|
-
this.latest.clear();
|
|
208
|
-
for (const controller of this.active) {
|
|
209
|
-
this.revocations.set(controller, freeze({ fields: changedFields, changes, reason: 'MODEL_CATALOG_SCOPE_CHANGED' }));
|
|
210
|
-
controller.abort();
|
|
211
|
-
}
|
|
212
|
-
this.active.clear();
|
|
213
|
-
this.diagnostic('claude', undefined);
|
|
214
|
-
this.diagnostic('codex', undefined);
|
|
215
|
-
this.changed();
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
trusted(agent, api) {
|
|
219
|
-
const info = api.getContractInfo?.();
|
|
220
|
-
if (info?.modelCatalogTarget !== 1 || typeof api.getModelCatalogTarget !== 'function')
|
|
221
|
-
fail('远程主机连接组件尚未提供模型目录目标能力');
|
|
222
|
-
const state = api.getConnectionState();
|
|
223
|
-
if (state.phase !== 'connected' || !text(state.instanceId) || !Number.isSafeInteger(state.epoch) || state.epoch < 0)
|
|
224
|
-
fail('当前没有已验证的远程主机连接');
|
|
225
|
-
const result = api.getModelCatalogTarget(agent.loginProvider);
|
|
226
|
-
if (!result.ok)
|
|
227
|
-
fail('远程主机连接暂未提供可信的模型目录目标,请到设置 → 模型查看诊断。', `模型目录目标不可用(诊断代码:${safe(result.error.code)}):${safe(result.error.message)}`);
|
|
228
|
-
const target = result.value;
|
|
229
|
-
if (target.version !== 1 || target.purpose !== 'model-catalog' || target.scope !== 'host-user' || target.provider !== agent.loginProvider || target.instanceId !== state.instanceId || target.epoch !== state.epoch)
|
|
230
|
-
fail('模型目录目标与当前连接或品牌不匹配');
|
|
231
|
-
try {
|
|
232
|
-
validateTarget(target.target);
|
|
233
|
-
}
|
|
234
|
-
catch {
|
|
235
|
-
fail('模型目录的可信 SSH 目标不完整');
|
|
236
|
-
}
|
|
237
|
-
return structuredClone(target);
|
|
238
|
-
}
|
|
239
|
-
assertFlight(flight) {
|
|
240
|
-
if (flight.controller.signal.aborted || this.disposed || this.get() !== flight.api || flight.revision !== this.revision)
|
|
241
|
-
fail('连接已改变,原模型目录已失效');
|
|
242
|
-
const agent = { loginProvider: flight.target.provider }, current = this.trusted(agent, flight.api);
|
|
243
|
-
if (current.instanceId !== flight.target.instanceId || current.epoch !== flight.target.epoch || current.provider !== flight.target.provider || !sameTarget(current.target, flight.target.target))
|
|
244
|
-
fail('可信目标或连接代次已改变,原模型目录已失效');
|
|
245
|
-
}
|
|
246
|
-
failure(error) {
|
|
247
|
-
// Each consumer needs its own Error identity for its original input/signal proof.
|
|
248
|
-
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:目录查询失败,根因未查明');
|
|
249
|
-
}
|
|
250
|
-
checkSelections(agent, api, target, models, serial) {
|
|
251
|
-
for (const lease of this.selections) {
|
|
252
|
-
const chosen = lease.selected, old = lease.flight.target;
|
|
253
|
-
if (lease.controller.signal.aborted || lease.flight.serial > serial || (lease.observed?.serial ?? 0) > serial || lease.flight.api !== api || old.provider !== agent.loginProvider || old.instanceId !== target.instanceId || old.epoch !== target.epoch || !sameTarget(old.target, target.target))
|
|
254
|
-
continue;
|
|
255
|
-
lease.observed = { serial, models };
|
|
256
|
-
if (!chosen)
|
|
257
|
-
continue;
|
|
258
|
-
const current = models.find(model => model.id === chosen.id && model.value === chosen.value);
|
|
259
|
-
if (current)
|
|
260
|
-
continue;
|
|
261
|
-
const changes = [{ field: 'catalog.' + agent.loginProvider + '.selected-model-valid', before: true, after: false }];
|
|
262
|
-
this.revocations.set(lease.controller, freeze({ fields: ['selected-model'], changes, reason: 'MODEL_CATALOG_REVOKED' }));
|
|
263
|
-
lease.controller.abort();
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
start(agent, api, target, max, key) {
|
|
267
|
-
const deferred = Promise.withResolvers(), controller = new AbortController();
|
|
268
|
-
const flight = { key, api, target, revision: this.revision, serial: ++this.serial, controller, pending: true, users: 0, promise: deferred.promise };
|
|
269
|
-
this.latest.set(agent.loginProvider, flight.serial);
|
|
270
|
-
this.flights.set(key, flight);
|
|
271
|
-
this.active.add(controller);
|
|
272
|
-
const current = () => !this.disposed && flight.revision === this.revision && api === this.get() && this.latest.get(agent.loginProvider) === flight.serial && !controller.signal.aborted;
|
|
273
|
-
const query = async () => {
|
|
274
|
-
try {
|
|
275
|
-
this.assertFlight(flight);
|
|
276
|
-
const output = await this.run(target.target, '/usr/local/aw-host/bin/aw-model-catalog --provider ' + agent.loginProvider, controller.signal, max);
|
|
277
|
-
this.assertFlight(flight);
|
|
278
|
-
const parsed = parseHostCatalog(output, agent.loginProvider, target.target.username), diagnostic = catalogDiagnostic(parsed.issues);
|
|
279
|
-
this.checkSelections(agent, api, target, parsed.models, flight.serial);
|
|
280
|
-
if (!parsed.models.length)
|
|
281
|
-
fail('当前没有可选模型,请到设置 → 模型查看诊断。', diagnostic ?? '主机返回的完整模型目录为空(诊断代码:MODEL_CATALOG_EMPTY)');
|
|
282
|
-
if (current())
|
|
283
|
-
this.diagnostic(agent.loginProvider, diagnostic);
|
|
284
|
-
if (current())
|
|
285
|
-
await this.loaded(agent, parsed.models, () => this.assertFlight(flight));
|
|
286
|
-
this.assertFlight(flight);
|
|
287
|
-
return parsed.models;
|
|
288
|
-
}
|
|
289
|
-
catch (error) {
|
|
290
|
-
const failure = this.failure(error);
|
|
291
|
-
if (current())
|
|
292
|
-
this.diagnostic(agent.loginProvider, failure.diagnostic);
|
|
293
|
-
throw failure;
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
const settled = () => { flight.pending = false; if (this.flights.get(key) === flight)
|
|
297
|
-
this.flights.delete(key); if (!flight.users)
|
|
298
|
-
this.active.delete(controller); };
|
|
299
|
-
void query().then(models => { settled(); deferred.resolve(models); }, error => { settled(); deferred.reject(error); });
|
|
300
|
-
return flight;
|
|
301
98
|
}
|
|
302
99
|
async read(agent, signal) {
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (!api)
|
|
322
|
-
fail('远程主机连接组件没有装好');
|
|
323
|
-
const target = freeze(this.trusted(agent, api)), max = this.maxBytes();
|
|
324
|
-
if (!Number.isSafeInteger(max) || max < 1)
|
|
325
|
-
fail('acpMaxMessageBytes 设置无效');
|
|
326
|
-
const key = JSON.stringify([agent.loginProvider, target.instanceId, target.epoch]);
|
|
327
|
-
flight = this.flights.get(key);
|
|
328
|
-
if (flight && (flight.api !== api || flight.revision !== this.revision || !sameTarget(flight.target.target, target.target) || flight.controller.signal.aborted)) {
|
|
329
|
-
const closing = flight;
|
|
330
|
-
closing.controller.abort();
|
|
331
|
-
flight = undefined;
|
|
332
|
-
// Closing work owns the tuple until SSH/runner actually settles. This new
|
|
333
|
-
// caller waits for cleanup, then begins its own query; it never uses old data.
|
|
334
|
-
const waiting = new AbortController(), revision = this.revision;
|
|
335
|
-
this.active.add(waiting);
|
|
336
|
-
const waitSignal = signal ? AbortSignal.any([signal, waiting.signal]) : waiting.signal;
|
|
337
|
-
try {
|
|
338
|
-
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)
|
|
339
|
-
abort(); });
|
|
340
|
-
}
|
|
341
|
-
finally {
|
|
342
|
-
this.active.delete(waiting);
|
|
343
|
-
}
|
|
344
|
-
if (revision !== this.revision || this.get() !== api)
|
|
345
|
-
fail('连接已改变,原模型目录已失效');
|
|
346
|
-
return this.read(agent, signal);
|
|
347
|
-
}
|
|
348
|
-
flight ??= this.start(agent, api, target, max, key);
|
|
349
|
-
flight.users++;
|
|
350
|
-
const own = flight, combined = AbortSignal.any([own.controller.signal, selectedController.signal, ...(signal ? [signal] : [])]);
|
|
351
|
-
const assertCurrent = () => { if (combined.aborted)
|
|
352
|
-
fail('目录查询已取消'); this.assertFlight(own); };
|
|
353
|
-
const models = await new Promise((resolve, reject) => {
|
|
354
|
-
const abort = () => { combined.removeEventListener('abort', abort); reject(new ModelConfigurationError('目录查询已取消')); };
|
|
355
|
-
combined.addEventListener('abort', abort, { once: true });
|
|
356
|
-
own.promise.then(value => { combined.removeEventListener('abort', abort); resolve(value); }, error => { combined.removeEventListener('abort', abort); reject(error); });
|
|
357
|
-
if (combined.aborted)
|
|
358
|
-
abort();
|
|
359
|
-
});
|
|
360
|
-
assertCurrent();
|
|
361
|
-
selection = { flight: own, controller: selectedController };
|
|
362
|
-
this.selections.add(selection);
|
|
363
|
-
return { target: own.target, get models() { return selection?.observed?.models ?? models; }, signal: combined, assertCurrent, release, bindSelection: (id, effort) => { assertCurrent(); if (released)
|
|
364
|
-
fail('模型目录租约已经释放'); const observed = selection.observed?.models ?? models, model = observed.find(model => model.id === id); if (!model || effort !== undefined && !model.reasoning.options.some(option => option.value === effort))
|
|
365
|
-
fail('所选型号或推理等级已不在当前主机目录中,请重新选择'); selection.selected = { id: model.id, value: model.value }; }, revocation: () => this.revocations.get(selectedController) ?? this.revocations.get(own.controller), onInformation: listener => { if (released)
|
|
366
|
-
return () => { }; listeners.add(listener); this.information.add(listener); return () => { listeners.delete(listener); this.information.delete(listener); }; } };
|
|
367
|
-
}
|
|
368
|
-
catch (error) {
|
|
369
|
-
release();
|
|
370
|
-
const failure = this.failure(error);
|
|
371
|
-
if (!flight && !this.disposed)
|
|
372
|
-
this.diagnostic(agent.loginProvider, failure.diagnostic);
|
|
373
|
-
throw failure;
|
|
374
|
-
}
|
|
100
|
+
signal?.throwIfAborted();
|
|
101
|
+
const api = this.get();
|
|
102
|
+
if (!api || typeof api.getModelCatalogTarget !== 'function')
|
|
103
|
+
fail('远程主机连接组件没有提供模型目录');
|
|
104
|
+
const reply = api.getModelCatalogTarget(agent.loginProvider);
|
|
105
|
+
if (!reply.ok)
|
|
106
|
+
fail(reply.error.message);
|
|
107
|
+
const target = reply.value;
|
|
108
|
+
validateTarget(target.target);
|
|
109
|
+
const controller = new AbortController(), combined = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
110
|
+
const max = this.maxBytes();
|
|
111
|
+
if (!Number.isSafeInteger(max) || max < 1)
|
|
112
|
+
fail('模型目录容量设置无效');
|
|
113
|
+
const output = await this.run(target.target, '/usr/local/aw-host/bin/aw-model-catalog --provider ' + agent.loginProvider, combined, max);
|
|
114
|
+
const parsed = parseHostCatalog(output, agent.loginProvider, target.target.username);
|
|
115
|
+
if (parsed.issues.length)
|
|
116
|
+
console.warn('[remote-agent] 模型菜单部分条目无效:' + parsed.issues.map(issue => issue.code).join(','));
|
|
117
|
+
return { target, models: parsed.models, signal: combined, release() { } };
|
|
375
118
|
}
|
|
376
|
-
dispose() {
|
|
377
|
-
controller.abort(); this.active.clear(); this.selections.clear(); this.information.clear(); }
|
|
119
|
+
dispose() { }
|
|
378
120
|
}
|
|
379
121
|
export function modelInfo(provider, model) {
|
|
380
122
|
// Official rc.2 represents no reasoning levels by absence, and rejects an empty efforts array.
|
|
@@ -55,10 +55,7 @@ export declare class ModelConfiguration {
|
|
|
55
55
|
/** A different ACP session cannot inherit selector-role discovery or ACKs. */
|
|
56
56
|
invalidate(clearRoles?: boolean): void;
|
|
57
57
|
private fail;
|
|
58
|
-
private checkAckRevision;
|
|
59
58
|
private set;
|
|
60
59
|
select(model: string, effort?: string): Promise<ModelConfigurationSnapshot>;
|
|
61
|
-
/** Call immediately before prompt; an idle/new/load/resume update withdraws the ACK. */
|
|
62
|
-
assertSelection(model: string, effort?: string): void;
|
|
63
60
|
}
|
|
64
61
|
export {};
|