@ai-workbench/remote-agent 0.8.11 → 0.8.13
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 +14 -5
- package/dist/acp.js +1 -1
- package/dist/catalog-diagnostics.d.ts +16 -0
- package/dist/catalog-diagnostics.js +108 -0
- package/dist/catalog-ssh.d.ts +51 -0
- package/dist/catalog-ssh.js +165 -0
- package/dist/client.js +41 -1
- package/dist/employee-mcp.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +49 -17
- package/dist/local-user-turn.js +2 -1
- package/dist/model-catalog.d.ts +6 -3
- package/dist/model-catalog.js +102 -125
- package/dist/model-input-retention.d.ts +6 -0
- package/dist/model-input-retention.js +12 -4
- package/dist/preflight-diagnostic.d.ts +22 -0
- package/dist/preflight-diagnostic.js +112 -0
- package/dist/preflight-payload.d.ts +11 -0
- package/dist/preflight-payload.js +11 -0
- package/dist/presentation.js +1 -1
- package/dist/provider.d.ts +1 -0
- package/dist/provider.js +6 -2
- package/dist/remote-details.d.ts +18 -0
- package/dist/remote-details.js +140 -125
- package/dist/session-header.d.ts +4 -0
- package/dist/session-header.js +12 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { runCatalog } from "./catalog-ssh.js";
|
|
2
|
+
import { createCatalogDiagnostics, CatalogDiagnosticsRemote } from "./catalog-diagnostics.js";
|
|
1
3
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
2
4
|
import Schema from '@deepseek-ai/schemastery';
|
|
3
5
|
import { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm';
|
|
@@ -31,6 +33,7 @@ import { createClaudeLocalProvider } from "./claude-local.js";
|
|
|
31
33
|
import { readLocalUserTurn } from "./local-user-turn.js";
|
|
32
34
|
import { readEmployeeWorkspace } from "./employee-workspace.js";
|
|
33
35
|
import { createModelDefaults, createDefaultSelectionWriter } from "./model-defaults.js";
|
|
36
|
+
import { createPreflightDiagnostics } from "./preflight-diagnostic.js";
|
|
34
37
|
import { installModelInputRetention, markModelPreparationFailure } from "./model-input-retention.js";
|
|
35
38
|
export const name = 'ai-workbench-remote-agent';
|
|
36
39
|
export const settingsNamespace = 'ai-workbench-codex';
|
|
@@ -68,6 +71,23 @@ function readPointer(options, agent) {
|
|
|
68
71
|
throw new NeedError('SESSION_CONTEXT_DIGEST_INVALID', '原Codex会话的初建上下文摘要未知或无效,不能猜测是否相同,本轮未发送', '原会话合法的64位十六进制初建上下文摘要');
|
|
69
72
|
return pointer;
|
|
70
73
|
}
|
|
74
|
+
// A reporting failure can wrap the original admission error. Preserve its
|
|
75
|
+
// already-safe login snapshot even when a newer preflight delivery is unknown.
|
|
76
|
+
function originalLoginFailure(error) {
|
|
77
|
+
const pending = [error], seen = new Set();
|
|
78
|
+
while (pending.length) {
|
|
79
|
+
const current = pending.pop();
|
|
80
|
+
if (!current || typeof current !== 'object' || seen.has(current))
|
|
81
|
+
continue;
|
|
82
|
+
seen.add(current);
|
|
83
|
+
if (current instanceof NeedError && current.technical)
|
|
84
|
+
return current;
|
|
85
|
+
if (current instanceof AggregateError)
|
|
86
|
+
pending.push(...current.errors);
|
|
87
|
+
if (current instanceof Error && current.cause !== undefined)
|
|
88
|
+
pending.push(current.cause);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
71
91
|
export class CodexRemoteAdapter extends LlmAdapter {
|
|
72
92
|
resolveRuntime;
|
|
73
93
|
approve;
|
|
@@ -76,6 +96,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
76
96
|
readImage;
|
|
77
97
|
reportLoginFailure;
|
|
78
98
|
directory;
|
|
99
|
+
preflightDiagnostics;
|
|
79
100
|
config;
|
|
80
101
|
sessions = new Map();
|
|
81
102
|
turns = new Map();
|
|
@@ -83,7 +104,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
83
104
|
shuttingDown = false;
|
|
84
105
|
detached = new Set();
|
|
85
106
|
cleanups = new WeakMap();
|
|
86
|
-
constructor(config, resolveRuntime, approve, providerName = () => PROVIDER_NAMES.en, agent = CODEX_AGENT, readImage, reportLoginFailure, directory) {
|
|
107
|
+
constructor(config, resolveRuntime, approve, providerName = () => PROVIDER_NAMES.en, agent = CODEX_AGENT, readImage, reportLoginFailure, directory, preflightDiagnostics) {
|
|
87
108
|
super();
|
|
88
109
|
this.resolveRuntime = resolveRuntime;
|
|
89
110
|
this.approve = approve;
|
|
@@ -92,6 +113,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
92
113
|
this.readImage = readImage;
|
|
93
114
|
this.reportLoginFailure = reportLoginFailure;
|
|
94
115
|
this.directory = directory;
|
|
116
|
+
this.preflightDiagnostics = preflightDiagnostics;
|
|
95
117
|
this.config = typeof config === 'function' ? config : () => config;
|
|
96
118
|
}
|
|
97
119
|
providerInfo(provider) { return { id: provider, name: this.providerName() }; }
|
|
@@ -187,10 +209,11 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
187
209
|
if (error instanceof NeedError) {
|
|
188
210
|
const rpc = /RPC 错误码 (-?\d+)/u.exec(error.requires)?.[1];
|
|
189
211
|
const message = employeeFailure(error.code, this.agent.loginProvider) + (rpc ? `(技术详情:RPC 错误码 ${rpc})` : '');
|
|
190
|
-
|
|
191
|
-
|
|
212
|
+
const login = originalLoginFailure(error);
|
|
213
|
+
let diagnostic = login?.technical ? JSON.stringify(login.technical) : '';
|
|
214
|
+
if (login?.technical && this.reportLoginFailure) {
|
|
192
215
|
try {
|
|
193
|
-
const receipt = await this.reportLoginFailure(options.sessionId,
|
|
216
|
+
const receipt = await this.reportLoginFailure(options.sessionId, login.code, login.technical);
|
|
194
217
|
diagnostic += ';诊断 eventId=' + receipt.eventId;
|
|
195
218
|
}
|
|
196
219
|
catch (failure) {
|
|
@@ -230,6 +253,10 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
230
253
|
let modelConfiguration;
|
|
231
254
|
const config = this.config();
|
|
232
255
|
const mode = this.agent.oneShot ? config.mode : 'session';
|
|
256
|
+
request = { sessionId: options.sessionId, mode: mode ?? 'session', timing, signal };
|
|
257
|
+
this.preflightDiagnostics?.begin(request, this.agent.loginProvider);
|
|
258
|
+
if (this.preflightDiagnostics)
|
|
259
|
+
request.recordEarlyFailure = (error, stage) => this.preflightDiagnostics.failure(request, this.agent.loginProvider, error, stage);
|
|
233
260
|
try {
|
|
234
261
|
if (!mode || (mode === 'session' && (!Number.isSafeInteger(config.acpMaxMessageBytes) || config.acpMaxMessageBytes < 1)))
|
|
235
262
|
throw new NeedError('SETTINGS_REQUIRED', '远程模型连接设置无效,请联系管理员检查', '有效的mode/acpMaxMessageBytes');
|
|
@@ -248,11 +275,13 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
248
275
|
const configuration = JSON.stringify([mode, config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes, config.employeeShellKillGraceMs]);
|
|
249
276
|
if (live && live.configuration !== configuration)
|
|
250
277
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '会话运行设置已改变,请新建会话', '原会话运行设置');
|
|
251
|
-
request
|
|
278
|
+
Object.assign(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 }) });
|
|
252
279
|
const after = options.messages.findLastIndex(message => message.role === 'assistant' && !isWorkspaceNoticeMessage(message));
|
|
253
280
|
request.userInputIds = Object.freeze(options.messages.slice(after + 1).flatMap(message => message.role === 'user' && message.source?.kind === 'user' && typeof message.id === 'string' ? [message.id] : []));
|
|
254
281
|
request.userMessageIds = request.userInputIds;
|
|
282
|
+
earlyStage = 'source-context';
|
|
255
283
|
await this.resolveRuntime.preflight?.(request);
|
|
284
|
+
earlyStage = 'input-prepare';
|
|
256
285
|
let input = await timing.run('input-prepare', () => prepareInput(options, this.readImage));
|
|
257
286
|
const contextSha256 = createHash('sha256').update(input.systemPrompt ?? '').digest('hex');
|
|
258
287
|
request.contextSha256 = contextSha256;
|
|
@@ -525,7 +554,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
525
554
|
request.recordEarlyFailure = undefined;
|
|
526
555
|
const code = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.failure.code : signal.aborted ? 'ABORTED' : 'WORKSPACE_SOURCE_CONFIRMATION_FAILED';
|
|
527
556
|
try {
|
|
528
|
-
await record(new NeedError(code, '
|
|
557
|
+
await record(Object.assign(new NeedError(code, '本轮在执行前失败。', '白名单阶段与错误代码'), { cause: error }), earlyStage);
|
|
529
558
|
}
|
|
530
559
|
catch (failure) {
|
|
531
560
|
reportingFailure = { cause: failure };
|
|
@@ -576,8 +605,8 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
576
605
|
/** One official registration owns both routes, preserving opaque replay ownership. */
|
|
577
606
|
export class RemoteAgentsAdapter extends LlmAdapter {
|
|
578
607
|
adapters = new Map();
|
|
579
|
-
constructor(config, makeRuntime, approve, nameFor, readImage, reportLoginFailure, directory) { super(); for (const agent of AGENTS)
|
|
580
|
-
this.adapters.set(agent.id, new CodexRemoteAdapter(config, makeRuntime(agent), approve, () => nameFor(agent), agent, readImage, reportLoginFailure, directory)); }
|
|
608
|
+
constructor(config, makeRuntime, approve, nameFor, readImage, reportLoginFailure, directory, preflightDiagnostics) { super(); for (const agent of AGENTS)
|
|
609
|
+
this.adapters.set(agent.id, new CodexRemoteAdapter(config, makeRuntime(agent), approve, () => nameFor(agent), agent, readImage, reportLoginFailure, directory, preflightDiagnostics)); }
|
|
581
610
|
adapter(provider) { const adapter = this.adapters.get(provider); if (!adapter)
|
|
582
611
|
throw new LlmError('远程模型未注册', 'PROVIDER_NOT_FOUND'); return adapter; }
|
|
583
612
|
providerInfo(provider) { return this.adapter(provider).providerInfo(provider); }
|
|
@@ -615,7 +644,8 @@ export function apply(ctx, live) {
|
|
|
615
644
|
}
|
|
616
645
|
return manager;
|
|
617
646
|
};
|
|
618
|
-
|
|
647
|
+
const preflightDiagnostics = createPreflightDiagnostics({ ctx, getApi: get, manager: managerForProfile });
|
|
648
|
+
let changed = () => { };
|
|
619
649
|
const diagnostics = new Map();
|
|
620
650
|
const diagnostic = (scope, provider, message) => {
|
|
621
651
|
let entry = diagnostics.get(provider);
|
|
@@ -629,10 +659,13 @@ export function apply(ctx, live) {
|
|
|
629
659
|
entry.delete(scope);
|
|
630
660
|
else
|
|
631
661
|
entry.set(scope, message);
|
|
632
|
-
publishDiagnostics();
|
|
633
662
|
};
|
|
634
663
|
let defaults;
|
|
635
|
-
const
|
|
664
|
+
const catalogDiagnostics = createCatalogDiagnostics(() => { const profile = ctx.get('profileContext'); return profile?.dir; });
|
|
665
|
+
// Public Loader Entry.options.id; optional when mounted without Loader.
|
|
666
|
+
const actualSettingsNamespace = ctx.fiber.entry?.options.id;
|
|
667
|
+
new CatalogDiagnosticsRemote(ctx, async () => ({ ...await catalogDiagnostics.snapshot(), settingsNamespace: actualSettingsNamespace ?? null, providers: AGENTS.map(agent => ({ provider: agent.id, name: agent.name[language], messages: [...(diagnostics.get(agent.id)?.values() ?? [])] })) }));
|
|
668
|
+
const modelDirectory = new ModelCatalog(get, () => config().acpMaxMessageBytes, (...args) => { const state = get()?.getConnectionState(); const connection = state ? { instanceId: state.instanceId, epoch: state.epoch, phase: state.phase } : null; return runCatalog(...args, event => catalogDiagnostics.record({ ...event, connection })); }, () => changed(), (brand, message) => {
|
|
636
669
|
diagnostic('catalog', AGENTS.find(agent => agent.loginProvider === brand).id, message);
|
|
637
670
|
}, (agent, models, assertCurrent) => defaults?.observe(agent, models, assertCurrent));
|
|
638
671
|
const adapter = new RemoteAgentsAdapter(config, agent => {
|
|
@@ -668,7 +701,7 @@ export function apply(ctx, live) {
|
|
|
668
701
|
};
|
|
669
702
|
return runtime;
|
|
670
703
|
};
|
|
671
|
-
const local = agent.loginProvider === 'claude' ? createClaudeLocalProvider({ getConnect: get, getNetwork: () => ctx.get('aiWorkbenchNetwork'), readWorkspace: id => readEmployeeWorkspace(ctx, id), readUserTurn: request => readLocalUserTurn(ctx, { sessionId: request.sessionId, route: agent.id, userMessageIds: request.userMessageIds ?? [] }), reportAdmissionFailure: (request, code) =>
|
|
704
|
+
const local = agent.loginProvider === 'claude' ? createClaudeLocalProvider({ getConnect: get, getNetwork: () => ctx.get('aiWorkbenchNetwork'), readWorkspace: id => readEmployeeWorkspace(ctx, id), readUserTurn: request => readLocalUserTurn(ctx, { sessionId: request.sessionId, route: agent.id, userMessageIds: request.userMessageIds ?? [] }), reportAdmissionFailure: (request, code) => preflightDiagnostics.failure(request, 'claude', code, 'local-admission'), recordUnscoped: (request, observation) => managerForProfile().recordLocalFailure({ sessionId: request.sessionId, provider: 'claude', code: observation.code, observation }), configuration: config, details, open }) : undefined;
|
|
672
705
|
const workspace = async (request) => {
|
|
673
706
|
const services = ctx;
|
|
674
707
|
const registry = services.get('workspaceRegistry');
|
|
@@ -677,7 +710,7 @@ export function apply(ctx, live) {
|
|
|
677
710
|
};
|
|
678
711
|
return connectProvider(get, { agent, workspaceSource: {
|
|
679
712
|
workspace, currentUserTurn: request => localTurns.current(request), confirm: nativeWorkspaceSourceConfirmation(ctx),
|
|
680
|
-
record: (request, observation) =>
|
|
713
|
+
record: (request, observation) => preflightDiagnostics.record(request, agent.loginProvider, observation),
|
|
681
714
|
}, ...(agent.loginProvider === 'codex' ? { employee: {
|
|
682
715
|
currentUserTurn: request => localTurns.current(request),
|
|
683
716
|
getNetwork: () => ctx.get('aiWorkbenchNetwork'),
|
|
@@ -701,14 +734,13 @@ export function apply(ctx, live) {
|
|
|
701
734
|
step = null;
|
|
702
735
|
}
|
|
703
736
|
return managerForProfile().recordLoginFailure({ sessionId, code, diagnostic, context: { turn, step } });
|
|
704
|
-
}, modelDirectory);
|
|
737
|
+
}, modelDirectory, preflightDiagnostics);
|
|
705
738
|
const ids = AGENTS.map(agent => agent.id);
|
|
706
|
-
const entries = () => AGENTS.map(agent => ({ provider: agent.id, displayName: agent.name[language], settingsNs: settingsNamespace, settingsPath: []
|
|
739
|
+
const entries = () => AGENTS.map(agent => ({ provider: agent.id, displayName: agent.name[language], settingsNs: actualSettingsNamespace ?? settingsNamespace, settingsPath: [] }));
|
|
707
740
|
const directory = ctx.llm.registerConfigurableProviders(entries());
|
|
708
741
|
const registration = ctx.llm.registerAdapter(ids, adapter);
|
|
709
|
-
installModelInputRetention(ctx, { isOurProvider: provider => ids.includes(provider), diagnostic: (provider, message) => diagnostic('input', provider, message) });
|
|
742
|
+
installModelInputRetention(ctx, { isOurProvider: provider => ids.includes(provider), diagnosticForAttempt: position => preflightDiagnostics.forPreparation(position.sessionId, position), preflightDiagnostic: (provider, message) => diagnostic('preflight', provider, message), diagnostic: (provider, message) => diagnostic('input', provider, message) });
|
|
710
743
|
const serviceValue = (name) => ctx.get(name);
|
|
711
|
-
publishDiagnostics = () => directory.replace(entries());
|
|
712
744
|
const refreshDefault = () => {
|
|
713
745
|
const service = serviceValue('agentDefaultModel');
|
|
714
746
|
const selected = service?.currentSelection();
|
package/dist/local-user-turn.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
2
|
+
import { sameStoredSessionHeader } from "./session-header.js";
|
|
2
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
3
4
|
import { NeedError } from "./remote.js";
|
|
4
5
|
const failure = (code = 'REMOTE_DETAIL_SCOPE_INVALID') => new NeedError(code, '本轮输入无法核对,已停止本次处理。', '官方当前员工输入与所选远程品牌');
|
|
@@ -20,7 +21,7 @@ export async function readLocalUserTurn(ctx, request) {
|
|
|
20
21
|
let proof, firstFailure;
|
|
21
22
|
try {
|
|
22
23
|
assertCurrent();
|
|
23
|
-
if (handle.id !== id || handle.access !== 'read' || !
|
|
24
|
+
if (handle.id !== id || handle.access !== 'read' || !sameStoredSessionHeader(handle.header, header) || handle.inheritedEventCount !== session.inheritedEventCount)
|
|
24
25
|
throw failure();
|
|
25
26
|
const { events } = await handle.read();
|
|
26
27
|
assertCurrent();
|
package/dist/model-catalog.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type LlmResolvedModelInfo } from '@deepseek-ai/dsh-llm';
|
|
|
2
2
|
import type { ModelCatalogTarget } from '@ai-workbench/connect/api';
|
|
3
3
|
import type { AgentDefinition } from './agents.ts';
|
|
4
4
|
import { type ConnectApi, type ConnectTarget } from './provider.ts';
|
|
5
|
+
export { runCatalog } from './catalog-ssh.ts';
|
|
5
6
|
export interface HostModel {
|
|
6
7
|
id: string;
|
|
7
8
|
value: string;
|
|
@@ -41,9 +42,7 @@ export declare function catalogDiagnostic(issues: readonly CatalogIssue[]): stri
|
|
|
41
42
|
/** Defaults are provider declarations, never catalog order or display names. */
|
|
42
43
|
export declare function declaredDefault(agent: AgentDefinition, models: readonly HostModel[]): HostModel | undefined;
|
|
43
44
|
export declare function parseHostCatalog(input: string, provider: string, username: string): ParsedCatalog;
|
|
44
|
-
/**
|
|
45
|
-
export declare const runCatalog: CatalogRunner;
|
|
46
|
-
/** No cache: every query binds to L's current object, host-user and generation. */
|
|
45
|
+
/** Share only pending work; settled results are never cached. */
|
|
47
46
|
export declare class ModelCatalog implements ModelDirectory {
|
|
48
47
|
private get;
|
|
49
48
|
private maxBytes;
|
|
@@ -58,11 +57,15 @@ export declare class ModelCatalog implements ModelDirectory {
|
|
|
58
57
|
private disposed;
|
|
59
58
|
private initialized;
|
|
60
59
|
private active;
|
|
60
|
+
private flights;
|
|
61
61
|
private serial;
|
|
62
62
|
private latest;
|
|
63
63
|
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>);
|
|
64
64
|
refresh(): void;
|
|
65
65
|
private trusted;
|
|
66
|
+
private assertFlight;
|
|
67
|
+
private failure;
|
|
68
|
+
private start;
|
|
66
69
|
read(agent: AgentDefinition, signal?: AbortSignal): Promise<CatalogLease>;
|
|
67
70
|
dispose(): void;
|
|
68
71
|
}
|
package/dist/model-catalog.js
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
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 {
|
|
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
8
|
class CatalogError extends LlmError {
|
|
17
9
|
diagnostic;
|
|
18
10
|
constructor(reason, diagnostic) { super('无法获取模型列表:' + reason, 'MODEL_CATALOG_UNAVAILABLE'); this.diagnostic = diagnostic + '(诊断代码:MODEL_CATALOG_UNAVAILABLE)'; }
|
|
19
11
|
}
|
|
20
|
-
const safe =
|
|
12
|
+
const safe = catalogSafeText;
|
|
21
13
|
function fail(reason, diagnostic = reason) { throw new CatalogError(reason, diagnostic); }
|
|
22
14
|
function freeze(value) { if (value && typeof value === 'object') {
|
|
23
15
|
for (const item of Object.values(value))
|
|
@@ -100,79 +92,7 @@ export function parseHostCatalog(input, provider, username) {
|
|
|
100
92
|
}
|
|
101
93
|
return freeze(structuredClone({ models, issues }));
|
|
102
94
|
}
|
|
103
|
-
/**
|
|
104
|
-
export const runCatalog = async (target, command, signal, maxBytes) => {
|
|
105
|
-
let argv;
|
|
106
|
-
try {
|
|
107
|
-
const moduleName = '@ai-workbench/connect';
|
|
108
|
-
const l = await import(__rewriteRelativeImportExtension(moduleName));
|
|
109
|
-
if (typeof l.sshArgs !== 'function')
|
|
110
|
-
fail('远程主机连接组件缺少可信 SSH 能力');
|
|
111
|
-
argv = l.sshArgs(target);
|
|
112
|
-
}
|
|
113
|
-
catch (error) {
|
|
114
|
-
if (error instanceof LlmError)
|
|
115
|
-
throw error;
|
|
116
|
-
fail('远程主机连接组件缺少可信 SSH 能力');
|
|
117
|
-
}
|
|
118
|
-
signal.throwIfAborted();
|
|
119
|
-
return new Promise((resolve, reject) => {
|
|
120
|
-
const child = spawn('ssh', ['-n', ...argv, command], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
121
|
-
const parts = [];
|
|
122
|
-
let total = 0, stderr = 0;
|
|
123
|
-
let failure;
|
|
124
|
-
const abort = () => { failure = new ModelConfigurationError('连接已改变或目录查询已取消'); child.kill('SIGTERM'); };
|
|
125
|
-
signal.addEventListener('abort', abort, { once: true });
|
|
126
|
-
const receive = (buffer, output) => { if (failure)
|
|
127
|
-
return; total += buffer.length; if (total > maxBytes) {
|
|
128
|
-
failure = new ModelConfigurationError('目录响应超过 acpMaxMessageBytes,未截断返回');
|
|
129
|
-
child.kill('SIGTERM');
|
|
130
|
-
return;
|
|
131
|
-
} if (output)
|
|
132
|
-
parts.push(buffer);
|
|
133
|
-
else
|
|
134
|
-
stderr += buffer.length; };
|
|
135
|
-
child.stdout.on('data', (b) => receive(b, true));
|
|
136
|
-
child.stderr.on('data', (b) => receive(b, false));
|
|
137
|
-
child.on('error', () => { failure = new ModelConfigurationError('可信 SSH 进程启动失败'); });
|
|
138
|
-
child.on('close', code => {
|
|
139
|
-
signal.removeEventListener('abort', abort);
|
|
140
|
-
if (failure) {
|
|
141
|
-
reject(failure);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
let output;
|
|
145
|
-
try {
|
|
146
|
-
output = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(parts));
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
reject(new ModelConfigurationError('主机目录响应不是有效 UTF-8,未替换模型值'));
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
// S publishes its typed failure JSON on stdout even on nonzero exit.
|
|
153
|
-
if (code !== 0) {
|
|
154
|
-
try {
|
|
155
|
-
const wire = JSON.parse(output);
|
|
156
|
-
if (wire?.ok === false) {
|
|
157
|
-
resolve(output);
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
catch { }
|
|
162
|
-
reject(new ModelConfigurationError('可信 SSH 或主机目录命令失败(退出码 ' + String(code) + ')'));
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
if (stderr) {
|
|
166
|
-
reject(new ModelConfigurationError('目录命令返回了非预期诊断输出'));
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
resolve(output);
|
|
170
|
-
});
|
|
171
|
-
if (signal.aborted)
|
|
172
|
-
abort();
|
|
173
|
-
});
|
|
174
|
-
};
|
|
175
|
-
/** No cache: every query binds to L's current object, host-user and generation. */
|
|
95
|
+
/** Share only pending work; settled results are never cached. */
|
|
176
96
|
export class ModelCatalog {
|
|
177
97
|
get;
|
|
178
98
|
maxBytes;
|
|
@@ -187,6 +107,7 @@ export class ModelCatalog {
|
|
|
187
107
|
disposed = false;
|
|
188
108
|
initialized = false;
|
|
189
109
|
active = new Set();
|
|
110
|
+
flights = new Map();
|
|
190
111
|
serial = 0;
|
|
191
112
|
latest = new Map();
|
|
192
113
|
constructor(get, maxBytes, run = runCatalog, changed = () => { }, diagnostic = () => { }, loaded = () => { }) {
|
|
@@ -203,11 +124,10 @@ export class ModelCatalog {
|
|
|
203
124
|
const state = api?.getConnectionState?.();
|
|
204
125
|
const info = api?.getContractInfo?.();
|
|
205
126
|
const targets = info?.modelCatalogTarget === 1 && api?.getModelCatalogTarget ? ['codex', 'claude'].map(brand => api.getModelCatalogTarget(brand)) : undefined;
|
|
206
|
-
const login = api?.getLoginPromptState?.();
|
|
207
127
|
// L's same-epoch recovery wording is presentation metadata, not an execution change.
|
|
208
128
|
const executionState = state && { instanceId: state.instanceId, epoch: state.epoch, phase: state.phase, needCode: state.need?.code };
|
|
209
129
|
const executionTargets = targets?.map(result => result.ok ? result : { ok: false, code: result.error.code });
|
|
210
|
-
const signature = JSON.stringify({ state: executionState, capability: info?.modelCatalogTarget, targets: executionTargets
|
|
130
|
+
const signature = JSON.stringify({ state: executionState, capability: info?.modelCatalogTarget, targets: executionTargets });
|
|
211
131
|
const switched = api !== this.api;
|
|
212
132
|
const changed = this.initialized && (switched || signature !== this.signature);
|
|
213
133
|
this.initialized = true;
|
|
@@ -256,61 +176,118 @@ export class ModelCatalog {
|
|
|
256
176
|
}
|
|
257
177
|
return structuredClone(target);
|
|
258
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
|
+
}
|
|
259
225
|
async read(agent, signal) {
|
|
260
|
-
let
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const report = (message) => { if (current()) {
|
|
268
|
-
reported = true;
|
|
269
|
-
this.diagnostic(agent.loginProvider, message);
|
|
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);
|
|
270
233
|
} };
|
|
271
234
|
try {
|
|
272
235
|
if (this.disposed)
|
|
273
236
|
fail('远程模型插件已关闭');
|
|
237
|
+
signal?.throwIfAborted();
|
|
274
238
|
this.refresh();
|
|
275
|
-
api = this.get();
|
|
276
|
-
revision = this.revision;
|
|
239
|
+
const api = this.get();
|
|
277
240
|
if (!api)
|
|
278
241
|
fail('远程主机连接组件没有装好');
|
|
279
|
-
|
|
280
|
-
this.latest.set(agent.loginProvider, serial);
|
|
281
|
-
const trustedApi = api, target = freeze(this.trusted(agent, trustedApi));
|
|
282
|
-
const max = this.maxBytes();
|
|
242
|
+
const target = freeze(this.trusted(agent, api)), max = this.maxBytes();
|
|
283
243
|
if (!Number.isSafeInteger(max) || max < 1)
|
|
284
244
|
fail('acpMaxMessageBytes 设置无效');
|
|
285
|
-
|
|
286
|
-
this.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
+
});
|
|
300
279
|
assertCurrent();
|
|
301
|
-
|
|
302
|
-
return { target, models: parsed.models, signal: combined, assertCurrent, release: () => this.active.delete(own) };
|
|
280
|
+
return { target: own.target, models, signal: combined, assertCurrent, release };
|
|
303
281
|
}
|
|
304
282
|
catch (error) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
report(failure.diagnostic);
|
|
283
|
+
release();
|
|
284
|
+
const failure = this.failure(error);
|
|
285
|
+
if (!flight && !this.disposed)
|
|
286
|
+
this.diagnostic(agent.loginProvider, failure.diagnostic);
|
|
310
287
|
throw failure;
|
|
311
288
|
}
|
|
312
289
|
}
|
|
313
|
-
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)
|
|
314
291
|
controller.abort(); this.active.clear(); }
|
|
315
292
|
}
|
|
316
293
|
export function modelInfo(provider, model) {
|
|
@@ -2,6 +2,12 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
2
2
|
export interface ModelInputRetentionOptions {
|
|
3
3
|
isOurProvider: (provider: string) => boolean;
|
|
4
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;
|
|
5
11
|
}
|
|
6
12
|
/** Mark only at the actual X adapter's preparation catch, then rethrow the same
|
|
7
13
|
* error. One error object can prove only its first preparation, for that signal.
|
|
@@ -26,7 +26,7 @@ function committed(events, messages) {
|
|
|
26
26
|
* admission failures; it never manufactures transcript messages or model routes.
|
|
27
27
|
*/
|
|
28
28
|
export function installModelInputRetention(ctx, options) {
|
|
29
|
-
const attempts = new Map();
|
|
29
|
+
const attempts = new Map(), pending = new Set();
|
|
30
30
|
let disposed = false;
|
|
31
31
|
const live = (a) => !disposed && attempts.get(a.agent) === a && !a.signal.aborted
|
|
32
32
|
&& ctx.get('agents')?.get(a.agent.id) === a.agent && ctx.get('sessions')?.get(a.session.id) === a.session && a.agent.session === a.session;
|
|
@@ -94,7 +94,7 @@ export function installModelInputRetention(ctx, options) {
|
|
|
94
94
|
attempts.delete(payload.agent);
|
|
95
95
|
return decision;
|
|
96
96
|
}
|
|
97
|
-
attempts.set(payload.agent, { agent: payload.agent, session: payload.agent.session, turn: payload.turn, step: payload.step, signal: payload.signal, messages, offset: payload.agent.session.snapshotEvents().length, scheduled: false, dispatched: false });
|
|
97
|
+
attempts.set(payload.agent, { agent: payload.agent, session: payload.agent.session, turn: payload.turn, step: payload.step, signal: payload.signal, messages, offset: payload.agent.session.snapshotEvents().length, scheduled: false, dispatched: false, preflight: options.diagnosticForAttempt?.({ sessionId: payload.agent.session.id, turn: payload.turn, step: payload.step }) });
|
|
98
98
|
return decision;
|
|
99
99
|
});
|
|
100
100
|
ctx.on('agent/request', async (payload, next) => {
|
|
@@ -118,12 +118,20 @@ export function installModelInputRetention(ctx, options) {
|
|
|
118
118
|
// Consume before further checks: canceled/unsafe attempts must not leave a
|
|
119
119
|
// reusable proof that could authorize a later failure of the same object.
|
|
120
120
|
proof.consumed = true;
|
|
121
|
-
if (a.scheduled || !
|
|
121
|
+
if (a.scheduled || !live(a) || !safe(a))
|
|
122
122
|
return;
|
|
123
123
|
a.provider = proof.provider;
|
|
124
|
+
if (a.preflight) {
|
|
125
|
+
const writing = a.preflight(proof.provider, code ?? 'PREFLIGHT_FAILED').catch(error => { if (!disposed)
|
|
126
|
+
options.preflightDiagnostic?.(proof.provider, `连接前诊断保存未能确认;没有自动重发。(原诊断代码:${code ?? 'PREFLIGHT_FAILED'};保存诊断代码:${codeOf(error) ?? 'DIAGNOSTIC_WRITE_FAILED'})`); });
|
|
127
|
+
pending.add(writing);
|
|
128
|
+
void writing.finally(() => pending.delete(writing));
|
|
129
|
+
}
|
|
130
|
+
if (!code || !preparationCodes.has(code))
|
|
131
|
+
return;
|
|
124
132
|
a.scheduled = true;
|
|
125
133
|
void restore(a, code);
|
|
126
134
|
});
|
|
127
135
|
ctx.on('agent/disposed', ({ agent }) => { attempts.delete(agent); });
|
|
128
|
-
ctx.effect(() => () => { disposed = true; attempts.clear(); });
|
|
136
|
+
ctx.effect(() => async () => { disposed = true; attempts.clear(); await Promise.allSettled(pending); });
|
|
129
137
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { ConnectApi, RuntimeRequest } from './provider.ts';
|
|
3
|
+
import type { DetailObservation } from './conversation-details.ts';
|
|
4
|
+
import type { createRemoteDetails } from './remote-details.ts';
|
|
5
|
+
import type { SourceAdmissionStage } from './workspace-source.ts';
|
|
6
|
+
type Position = {
|
|
7
|
+
turn: string | number | null;
|
|
8
|
+
step: string | number | null;
|
|
9
|
+
};
|
|
10
|
+
export declare function preflightCode(error: unknown): string;
|
|
11
|
+
export declare function createPreflightDiagnostics(options: {
|
|
12
|
+
ctx: Pick<Context, 'get'>;
|
|
13
|
+
getApi: () => ConnectApi | undefined;
|
|
14
|
+
manager: () => Pick<ReturnType<typeof createRemoteDetails>, 'recordPreflightCheck'>;
|
|
15
|
+
}): {
|
|
16
|
+
begin(request: RuntimeRequest, _provider: "claude" | "codex"): void;
|
|
17
|
+
record: (request: RuntimeRequest, provider: "claude" | "codex", observation: DetailObservation) => Promise<void>;
|
|
18
|
+
failure(request: RuntimeRequest, provider: "claude" | "codex", error: unknown, stage: SourceAdmissionStage): Promise<void>;
|
|
19
|
+
forPreparation(sessionId: string, context: Position): (provider: string, code: string) => Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
export type PreflightDiagnostics = ReturnType<typeof createPreflightDiagnostics>;
|
|
22
|
+
export {};
|