@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.
- package/README.md +29 -6
- 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 +133 -27
- package/dist/model-catalog.d.ts +24 -5
- package/dist/model-catalog.js +174 -132
- package/dist/model-defaults.d.ts +29 -0
- package/dist/model-defaults.js +62 -0
- package/dist/model-input-retention.d.ts +22 -0
- package/dist/model-input-retention.js +137 -0
- 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 +6 -0
- package/dist/provider.d.ts +5 -1
- package/dist/provider.js +48 -21
- package/dist/remote-details.d.ts +18 -0
- package/dist/remote-details.js +140 -125
- package/dist/workspace-source-confirmation.d.ts +4 -0
- package/dist/workspace-source-confirmation.js +12 -0
- package/dist/workspace-source.d.ts +25 -0
- package/dist/workspace-source.js +136 -0
- package/package.json +5 -5
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import { Session } from '@deepseek-ai/dsh-session';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
+
const preparationFailures = new WeakMap();
|
|
5
|
+
/** Mark only at the actual X adapter's preparation catch, then rethrow the same
|
|
6
|
+
* error. One error object can prove only its first preparation, for that signal.
|
|
7
|
+
* Public request config is an intermediate proposal and is not route evidence. */
|
|
8
|
+
export function markModelPreparationFailure(provider, error, signal) {
|
|
9
|
+
if (!signal || signal.aborted || error === null || typeof error !== 'object' || preparationFailures.has(error))
|
|
10
|
+
return;
|
|
11
|
+
preparationFailures.set(error, { provider, signal, consumed: false });
|
|
12
|
+
}
|
|
13
|
+
// Even a source-proven unknown early failure is deliberately not replayed.
|
|
14
|
+
const preparationCodes = new Set(['MODEL_REQUIRED', 'MODEL_CATALOG_UNAVAILABLE', 'INVALID_MODEL_INFO', 'INVALID_MODEL_CONTEXT', 'INVALID_MODEL_MAX_TOKENS', 'INVALID_MODEL_REASONING', 'UNSUPPORTED_REASONING_EFFORT']);
|
|
15
|
+
function codeOf(error) {
|
|
16
|
+
const code = error !== null && typeof error === 'object' && 'code' in error ? error.code : undefined;
|
|
17
|
+
return typeof code === 'string' && /^[A-Z][A-Z0-9_]*$/u.test(code) ? code : undefined;
|
|
18
|
+
}
|
|
19
|
+
function committed(events, messages) {
|
|
20
|
+
return events.some(event => event.type === 'user/message' && messages.some(message => message.id === event.data.id));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Retain a claimed human batch only after a known, undispatched metadata prepare
|
|
24
|
+
* failure. Restoration uses the public durable inbox without sending or waking
|
|
25
|
+
* the agent. This cannot cover errors before pre-step/request, or unknown core
|
|
26
|
+
* admission failures; it never manufactures transcript messages or model routes.
|
|
27
|
+
*/
|
|
28
|
+
export function installModelInputRetention(ctx, options) {
|
|
29
|
+
const attempts = new Map(), pending = new Set();
|
|
30
|
+
let disposed = false;
|
|
31
|
+
const live = (a) => !disposed && attempts.get(a.agent) === a && !a.signal.aborted
|
|
32
|
+
&& ctx.get('agents')?.get(a.agent.id) === a.agent && ctx.get('sessions')?.get(a.session.id) === a.session && a.agent.session === a.session;
|
|
33
|
+
const safe = (a) => {
|
|
34
|
+
const events = a.session.snapshotEvents();
|
|
35
|
+
return !a.dispatched && !committed(events, a.messages) && !events.slice(a.offset).some(event => event.type === 'request/header' || event.type === 'assistant/message' || event.type === 'assistant/attempt');
|
|
36
|
+
};
|
|
37
|
+
const report = (a, message) => { if (!disposed && a.provider)
|
|
38
|
+
options.diagnostic(a.provider, message); };
|
|
39
|
+
const restore = async (a, originalCode) => {
|
|
40
|
+
try {
|
|
41
|
+
await a.agent.whenIdle();
|
|
42
|
+
if (!live(a) || !safe(a))
|
|
43
|
+
return;
|
|
44
|
+
// Claim true idle synchronously. A competing maintenance claim is reported,
|
|
45
|
+
// not retried. Input waking during this task remains the driver's own work.
|
|
46
|
+
await a.agent.runMaintenance(async (signal) => {
|
|
47
|
+
if (!live(a) || signal.aborted || !safe(a))
|
|
48
|
+
return;
|
|
49
|
+
const queue = [...a.agent.inbox.nextTurn, ...a.agent.inbox.nextStep];
|
|
50
|
+
if (a.messages.some(message => queue.some(item => item.id === message.id)))
|
|
51
|
+
return;
|
|
52
|
+
a.agent.inbox.splice('next-turn', 0, 0, a.messages);
|
|
53
|
+
const store = ctx.get('sessions'), persistence = ctx.get('sessionPersistence');
|
|
54
|
+
if (!store || typeof persistence?.open !== 'function' || !await store.flush(a.session))
|
|
55
|
+
throw Object.assign(new Error('缺少官方持久化能力'), { code: 'INPUT_RETENTION_PERSISTENCE_REQUIRED' });
|
|
56
|
+
if (!live(a) || signal.aborted)
|
|
57
|
+
return;
|
|
58
|
+
const handle = await persistence.open(a.session.id, 'read');
|
|
59
|
+
try {
|
|
60
|
+
if (handle.id !== a.session.id || handle.header?.id !== a.session.id || handle.access !== 'read')
|
|
61
|
+
throw Object.assign(new Error('持久会话身份不符'), { code: 'INPUT_RETENTION_VERIFY_FAILED' });
|
|
62
|
+
const { events } = await handle.read();
|
|
63
|
+
// Replay through the official Session validator and registered inbox fold,
|
|
64
|
+
// including exact fork metadata. Do not normalize invalid durable splices.
|
|
65
|
+
const replay = Session.create(a.session.id, events, handle.header, handle.inheritedEventCount);
|
|
66
|
+
const inbox = ctx.get('sessionProjections')?.snapshot(replay, ['inbox']).values.inbox;
|
|
67
|
+
if (!inbox)
|
|
68
|
+
throw Object.assign(new Error('缺少官方待发队列投影'), { code: 'INPUT_RETENTION_PROJECTION_REQUIRED' });
|
|
69
|
+
const restored = [...inbox['next-turn'], ...inbox['next-step']];
|
|
70
|
+
if (committed(events, a.messages) || a.messages.some(message => restored.filter(item => item !== null && typeof item === 'object' && !Array.isArray(item) && item.id === message.id).length !== 1 || !restored.some(item => isDeepStrictEqual(item, message))))
|
|
71
|
+
throw Object.assign(new Error('持久待发队列核对失败'), { code: 'INPUT_RETENTION_VERIFY_FAILED' });
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
await handle.close();
|
|
75
|
+
}
|
|
76
|
+
if (live(a) && !signal.aborted)
|
|
77
|
+
report(a, `本次模型准备失败,员工原话已保留在待发送队列;请先选择模型或修正设置后手动发送。没有自动重试。(诊断代码:${originalCode})`);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
report(a, `本次模型准备失败;员工原话的恢复或持久化未能确认,请联系管理员核对,不要重复发送。没有自动重试。(原诊断代码:${originalCode};恢复诊断代码:${codeOf(error) ?? 'INPUT_RETENTION_FAILED'})`);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
if (attempts.get(a.agent) === a)
|
|
85
|
+
attempts.delete(a.agent);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
ctx.on('agent/pre-step', async (payload, next) => {
|
|
89
|
+
const decision = await next();
|
|
90
|
+
if (disposed)
|
|
91
|
+
return decision;
|
|
92
|
+
const messages = payload.messages.filter(message => message.source.kind === 'user');
|
|
93
|
+
if (decision.kind !== 'enter' || messages.length === 0 || messages.some(message => !decision.messages.some(accepted => accepted === message || isDeepStrictEqual(accepted, message)))) {
|
|
94
|
+
attempts.delete(payload.agent);
|
|
95
|
+
return decision;
|
|
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, preflight: options.diagnosticForAttempt?.({ sessionId: payload.agent.session.id, turn: payload.turn, step: payload.step }) });
|
|
98
|
+
return decision;
|
|
99
|
+
});
|
|
100
|
+
ctx.on('agent/request', async (payload, next) => {
|
|
101
|
+
const config = await next(), a = attempts.get(payload.agent);
|
|
102
|
+
if (a && a.turn === payload.turn && a.step === payload.step && a.signal === payload.signal && options.isOurProvider(config.provider)) {
|
|
103
|
+
if (!config.model?.trim()) {
|
|
104
|
+
const error = new LlmError('请先选择模型', 'MODEL_REQUIRED');
|
|
105
|
+
markModelPreparationFailure(config.provider, error, payload.signal);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return config;
|
|
110
|
+
});
|
|
111
|
+
ctx.on('agent/assistant-stream', ({ agent, frame }) => { const a = attempts.get(agent); if (a && frame.type === 'start' && frame.turn === a.turn && frame.step === a.step)
|
|
112
|
+
a.dispatched = true; });
|
|
113
|
+
ctx.on('agent/error', ({ agent, turn, step, error }) => {
|
|
114
|
+
const a = attempts.get(agent), code = codeOf(error);
|
|
115
|
+
const proof = error !== null && typeof error === 'object' ? preparationFailures.get(error) : undefined;
|
|
116
|
+
if (!a || a.turn !== turn || a.step !== step || !proof || proof.consumed || proof.signal !== a.signal || !options.isOurProvider(proof.provider))
|
|
117
|
+
return;
|
|
118
|
+
// Consume before further checks: canceled/unsafe attempts must not leave a
|
|
119
|
+
// reusable proof that could authorize a later failure of the same object.
|
|
120
|
+
proof.consumed = true;
|
|
121
|
+
if (a.scheduled || !live(a) || !safe(a))
|
|
122
|
+
return;
|
|
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;
|
|
132
|
+
a.scheduled = true;
|
|
133
|
+
void restore(a, code);
|
|
134
|
+
});
|
|
135
|
+
ctx.on('agent/disposed', ({ agent }) => { attempts.delete(agent); });
|
|
136
|
+
ctx.effect(() => async () => { disposed = true; attempts.clear(); await Promise.allSettled(pending); });
|
|
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 {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
2
|
+
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
+
import { NeedError } from "./remote.js";
|
|
5
|
+
import { ModelConfigurationError } from "./model-configuration.js";
|
|
6
|
+
import { officialEmployeeWorkspace } from "./employee-workspace.js";
|
|
7
|
+
import { AGENTS } from "./agents.js";
|
|
8
|
+
import { validPreflightPayload } from "./preflight-payload.js";
|
|
9
|
+
const unknownSource = () => ({ workspaceId: null, bindingId: null, source: null, status: null });
|
|
10
|
+
export function preflightCode(error) {
|
|
11
|
+
if (typeof error === 'string' && /^[A-Z][A-Z0-9_]*$/.test(error))
|
|
12
|
+
return error;
|
|
13
|
+
if (error instanceof NeedError || error instanceof LlmError || error instanceof ModelConfigurationError)
|
|
14
|
+
return error.code;
|
|
15
|
+
return error instanceof Error && error.name === 'AbortError' ? 'ABORTED' : 'PREFLIGHT_FAILED';
|
|
16
|
+
}
|
|
17
|
+
export function createPreflightDiagnostics(options) {
|
|
18
|
+
const scopes = new WeakMap();
|
|
19
|
+
function capture(sessionId, position) {
|
|
20
|
+
const api = options.getApi();
|
|
21
|
+
let connection, session, header, source = unknownSource(), captureError;
|
|
22
|
+
let context = position ? { ...position } : { turn: null, step: null };
|
|
23
|
+
try {
|
|
24
|
+
connection = api?.getConnectionState();
|
|
25
|
+
session = sessionId ? options.ctx.get('sessions')?.get(SessionId(sessionId)) : undefined;
|
|
26
|
+
header = session && structuredClone(session.header);
|
|
27
|
+
if (!position)
|
|
28
|
+
for (const event of session?.snapshotEvents() ?? []) {
|
|
29
|
+
if (event.type === 'turn/start')
|
|
30
|
+
context = { turn: event.data.turn, step: null };
|
|
31
|
+
else if (event.type === 'turn/end')
|
|
32
|
+
context = { turn: null, step: null };
|
|
33
|
+
else if (event.type === 'step/start' && event.data.turn === context.turn)
|
|
34
|
+
context.step = event.data.step;
|
|
35
|
+
else if (event.type === 'step/end')
|
|
36
|
+
context.step = null;
|
|
37
|
+
}
|
|
38
|
+
if (sessionId && session) {
|
|
39
|
+
let workspace;
|
|
40
|
+
try {
|
|
41
|
+
workspace = officialEmployeeWorkspace(sessionId, options.ctx.get('workspaceRegistry'), session.header);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (!(error instanceof NeedError && error.code === 'EMPLOYEE_WORKSPACE_UNAVAILABLE'))
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
if (workspace) {
|
|
48
|
+
source.workspaceId = workspace.id;
|
|
49
|
+
const state = api?.getWorkspaceLocationState?.();
|
|
50
|
+
const rows = state?.items.filter(row => row.workspaceId === workspace.id) ?? [];
|
|
51
|
+
if (state?.phase === 'ready' && state.instanceId === connection?.instanceId && state.epoch === connection.epoch && rows.length === 1) {
|
|
52
|
+
const row = rows[0];
|
|
53
|
+
source = { workspaceId: row.workspaceId, bindingId: row.bindingId, source: row.source, status: row.status };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
captureError = error;
|
|
60
|
+
}
|
|
61
|
+
const assertScope = () => {
|
|
62
|
+
if (captureError !== undefined)
|
|
63
|
+
throw Object.assign(new NeedError('REMOTE_DETAIL_SCOPE_INVALID', '连接前诊断上下文无法核对。', '真实官方会话与连接快照'), { cause: captureError });
|
|
64
|
+
if (!sessionId || !session || options.ctx.get('sessions')?.get(SessionId(sessionId)) !== session || !isDeepStrictEqual(session.header, header))
|
|
65
|
+
throw new NeedError('REMOTE_DETAIL_SCOPE_INVALID', '连接前诊断所属会话已变化。', '原官方session/header');
|
|
66
|
+
};
|
|
67
|
+
return { sessionId, binding: { api, connection }, context, source, assertScope };
|
|
68
|
+
}
|
|
69
|
+
const scopeFor = (request) => { let scope = scopes.get(request); if (!scope) {
|
|
70
|
+
scope = capture(request.sessionId);
|
|
71
|
+
scopes.set(request, scope);
|
|
72
|
+
} return scope; };
|
|
73
|
+
const write = (scope, provider, observation) => options.manager().recordPreflightCheck({ sessionId: scope.sessionId, provider, observation, context: scope.context, binding: scope.binding, assertScope: scope.assertScope });
|
|
74
|
+
async function record(request, provider, observation) {
|
|
75
|
+
const scope = scopeFor(request), payload = observation.payload;
|
|
76
|
+
if (validPreflightPayload(payload) && ['failed', 'declined', 'cancelled'].includes(payload.outcome))
|
|
77
|
+
request.preflightFailureRecorded = true;
|
|
78
|
+
try {
|
|
79
|
+
await write(scope, provider, observation);
|
|
80
|
+
if (validPreflightPayload(payload) && payload.outcome === 'confirmed')
|
|
81
|
+
scope.source = { workspaceId: payload.workspaceId, bindingId: payload.bindingId, source: payload.source, status: payload.status };
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
request.preflightFailureRecorded = true;
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
begin(request, _provider) { scopeFor(request); }, record,
|
|
90
|
+
async failure(request, provider, error, stage) {
|
|
91
|
+
if (request.preflightFailureRecorded)
|
|
92
|
+
return;
|
|
93
|
+
request.preflightFailureRecorded = true;
|
|
94
|
+
const scope = scopeFor(request), code = preflightCode(error);
|
|
95
|
+
try {
|
|
96
|
+
await record(request, provider, { kind: 'need', severity: 'action-required', code, summary: '本轮在连接前准备阶段停止;原始请求保留,未重发。', payload: { phase: 'workspace-source-admission', stage, ...scope.source, outcome: code === 'ABORTED' ? 'cancelled' : 'failed' } });
|
|
97
|
+
}
|
|
98
|
+
catch (diagnostic) {
|
|
99
|
+
throw Object.assign(new NeedError('CONVERSATION_DETAILS_FAILED', '本轮连接前诊断未能保存,已停止。', '原错误代码 ' + code + ';诊断错误代码 ' + preflightCode(diagnostic)), { cause: new AggregateError([error, diagnostic], 'PREFLIGHT_AND_DIAGNOSTIC_FAILED') });
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
forPreparation(sessionId, context) {
|
|
103
|
+
const scope = capture(sessionId, context);
|
|
104
|
+
return async (provider, code) => {
|
|
105
|
+
const brand = AGENTS.find(agent => agent.id === provider)?.loginProvider;
|
|
106
|
+
if (!brand)
|
|
107
|
+
throw new NeedError('REMOTE_DETAIL_SCOPE_INVALID', '不是本插件的远程品牌。', '真实X品牌');
|
|
108
|
+
await write(scope, brand, { kind: 'need', severity: 'action-required', code, summary: '模型准备在派发前失败;没有自动重发任务。', payload: { phase: 'workspace-source-admission', stage: code === 'MODEL_REQUIRED' ? 'input-prepare' : 'model-catalog', ...scope.source, outcome: code === 'ABORTED' ? 'cancelled' : 'failed' } });
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SourceAdmissionStage } from './workspace-source.ts';
|
|
2
|
+
export interface PreflightPayload {
|
|
3
|
+
phase: 'workspace-source-admission';
|
|
4
|
+
stage: SourceAdmissionStage;
|
|
5
|
+
workspaceId: string | null;
|
|
6
|
+
bindingId: string | null;
|
|
7
|
+
source: 'local' | 'host' | 'legacy-host' | null;
|
|
8
|
+
status: 'unselected' | 'selected' | 'legacy-confirmation-required' | 'invalid' | null;
|
|
9
|
+
outcome: 'confirmation-required' | 'confirmed' | 'declined' | 'cancelled' | 'failed';
|
|
10
|
+
}
|
|
11
|
+
export declare function validPreflightPayload(value: unknown): value is PreflightPayload;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function validPreflightPayload(value) {
|
|
2
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
3
|
+
return false;
|
|
4
|
+
const p = value, keys = ['phase', 'stage', 'workspaceId', 'bindingId', 'source', 'status', 'outcome'];
|
|
5
|
+
const id = (v) => v === null || typeof v === 'string' && v.length > 0 && !/[\x00-\x1f\x7f]/.test(v);
|
|
6
|
+
return Object.keys(p).length === keys.length && keys.every(key => Object.hasOwn(p, key)) && p.phase === 'workspace-source-admission'
|
|
7
|
+
&& ['source-context', 'source-confirmation', 'input-acceptance', 'login-readiness', 'local-admission', 'input-prepare', 'model-catalog', 'runtime-resolve'].includes(p.stage)
|
|
8
|
+
&& id(p.workspaceId) && id(p.bindingId) && [null, 'local', 'host', 'legacy-host'].includes(p.source)
|
|
9
|
+
&& [null, 'unselected', 'selected', 'legacy-confirmation-required', 'invalid'].includes(p.status)
|
|
10
|
+
&& ['confirmation-required', 'confirmed', 'declined', 'cancelled', 'failed'].includes(p.outcome);
|
|
11
|
+
}
|
package/dist/presentation.js
CHANGED
|
@@ -5,6 +5,12 @@ export const TEST_HOST_NOTICE = '远程主机正在使用测试配置,遇到
|
|
|
5
5
|
/** Employee failures use business language; diagnostic codes remain separately available. */
|
|
6
6
|
export function employeeFailure(code, brand = 'codex') {
|
|
7
7
|
const messages = {
|
|
8
|
+
WORKSPACE_SOURCE_CONFIRMATION_DECLINED: '你没有确认改用本机文件夹,本轮未执行;你的原话仍保留。',
|
|
9
|
+
WORKSPACE_SOURCE_CONFIRMATION_UNAVAILABLE: '我需要打开工作区来源确认,目前无法显示确认动作;本轮未执行,你的原话仍保留。',
|
|
10
|
+
WORKSPACE_SOURCE_SELECTION_UNCONFIRMED: '无法核对工作区来源的确认结果,本轮已停止;你的原话仍保留。',
|
|
11
|
+
WORKSPACE_SOURCE_CONFIRMATION_FAILED: '工作区来源确认未完成,本轮已停止;你的原话仍保留。',
|
|
12
|
+
WORKSPACE_LOCATION_REQUIRED: '我需要你确认这个工作区使用哪一个文件夹,本轮未执行;你的原话仍保留。',
|
|
13
|
+
WORKSPACE_LOCATION_CONFLICT: '工作区来源选择已更新,本次确认不能覆盖新的选择;本轮未执行,你的原话仍保留。',
|
|
8
14
|
LOCAL_USER_TURN_CHANGED: '本次输入已变化,本轮已停止,请确认对话后重新发送。',
|
|
9
15
|
LOCAL_USER_TURN_UNCONFIRMED: '无法核对本次员工输入,本轮已停止;请在当前会话重新发送。',
|
|
10
16
|
LOCAL_USER_TURN_ACCEPTANCE_INVALID: '本次输入的核验回执不一致,本轮已停止,请联系管理员。',
|
package/dist/provider.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { WorkspaceNoticePresentation } from './workspace-notice.ts';
|
|
|
12
12
|
import { StageTiming } from './stage-timing.ts';
|
|
13
13
|
import type { RecoveryJournal, AcpRecovery } from './recovery.ts';
|
|
14
14
|
import type { EmployeeProviderOptions } from './employee-provider.ts';
|
|
15
|
+
import { type WorkspaceSourceOptions, type SourceAdmissionStage } from './workspace-source.ts';
|
|
15
16
|
import type { LocalUserTurnAcceptance } from '@ai-workbench/connect/api';
|
|
16
17
|
import type { EmployeeRouting } from './employee-relay.ts';
|
|
17
18
|
export interface EmployeeExecution {
|
|
@@ -64,6 +65,8 @@ export interface Runtime {
|
|
|
64
65
|
employeePresentation?: string;
|
|
65
66
|
}
|
|
66
67
|
export interface RuntimeRequest extends Invocation {
|
|
68
|
+
preflightFailureRecorded?: boolean;
|
|
69
|
+
recordEarlyFailure?: (error: unknown, stage: SourceAdmissionStage) => Promise<void>;
|
|
67
70
|
userInputIds?: readonly string[];
|
|
68
71
|
localAcceptance?: Readonly<LocalUserTurnAcceptance>;
|
|
69
72
|
localAdmissionCheck?: () => void;
|
|
@@ -135,7 +138,7 @@ type Result<T> = {
|
|
|
135
138
|
} & Pick<ConnectNeed, 'officialReason'>;
|
|
136
139
|
};
|
|
137
140
|
/** 消费正式connect-api 0.7公开子集;X不响应L登录票据、不直接调用登录。 */
|
|
138
|
-
export interface ConnectApi extends Pick<LConnectApi, 'getConnectionState' | 'getLoginPromptState' | 'requestLoginPrompt' | 'refreshLoginPrompts'>, Partial<NativeWorkspaceApi>, Partial<Pick<LConnectApi, 'getWorkspaceBindingState' | 'getModelCatalogTarget' | 'onConnectionState' | 'onLoginPromptState' | 'appendRemoteConversationDetail' | 'getRemoteConversationDetailReceipt' | 'listRemoteConversationDetails' | 'getWorkspaceLocationState' | 'getLocalWorkspaceContext' | 'onWorkspaceLocationState' | 'acceptLocalWorkspaceUserTurn'>> {
|
|
141
|
+
export interface ConnectApi extends Pick<LConnectApi, 'getConnectionState' | 'getLoginPromptState' | 'requestLoginPrompt' | 'refreshLoginPrompts'>, Partial<NativeWorkspaceApi>, Partial<Pick<LConnectApi, 'getWorkspaceBindingState' | 'getModelCatalogTarget' | 'onConnectionState' | 'onLoginPromptState' | 'appendRemoteConversationDetail' | 'getRemoteConversationDetailReceipt' | 'listRemoteConversationDetails' | 'getWorkspaceLocationState' | 'getLocalWorkspaceContext' | 'onWorkspaceLocationState' | 'acceptLocalWorkspaceUserTurn' | 'selectWorkspaceLocation'>> {
|
|
139
142
|
getState?(): ConnectState;
|
|
140
143
|
getSshTarget(): Promise<Result<ConnectTarget>>;
|
|
141
144
|
checkLogin(provider: LoginProvider): Promise<Result<{
|
|
@@ -189,6 +192,7 @@ export declare function connectProvider(get: () => ConnectApi | undefined, optio
|
|
|
189
192
|
agent?: AgentDefinition;
|
|
190
193
|
ask?: Interaction;
|
|
191
194
|
notice?: NoticeConsumer;
|
|
195
|
+
workspaceSource?: WorkspaceSourceOptions;
|
|
192
196
|
employee?: EmployeeProviderOptions;
|
|
193
197
|
local?: LocalRuntimeProvider;
|
|
194
198
|
details?: (request: RuntimeRequest, api: ConnectApi, connection: ConnectionState, target: ConnectTarget) => Promise<DetailChannel>;
|
package/dist/provider.js
CHANGED
|
@@ -3,6 +3,7 @@ import { NeedError } from "./remote.js";
|
|
|
3
3
|
import { CONNECTION_REQUIRED, TEST_HOST_NOTICE } from "./presentation.js";
|
|
4
4
|
import { CODEX_AGENT } from "./agents.js";
|
|
5
5
|
import { StageTiming } from "./stage-timing.js";
|
|
6
|
+
import { confirmLegacyWorkspaceSource } from "./workspace-source.js";
|
|
6
7
|
export function requireLoginCoordinator(api) {
|
|
7
8
|
if (!['getConnectionState', 'getLoginPromptState', 'requestLoginPrompt', 'refreshLoginPrompts'].every(key => typeof api[key] === 'function'))
|
|
8
9
|
throw new NeedError('CONNECT_API_MISMATCH', '我需要 connect-api 0.6.1 的统一登录询问与连接状态能力', 'L >=0.6.1');
|
|
@@ -168,28 +169,53 @@ export function connectProvider(get, options = {}) {
|
|
|
168
169
|
requireLoginCoordinator(api);
|
|
169
170
|
if (request.mode === 'one-shot' && !agent.oneShot)
|
|
170
171
|
throw new NeedError('MODE_UNSUPPORTED', '此远程模型仅支持会话模式', '使用会话模式');
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
172
|
+
const confirmedSource = options.workspaceSource ? await confirmLegacyWorkspaceSource(get, api, request, options.workspaceSource) : undefined;
|
|
173
|
+
let sourceStage = 'input-acceptance';
|
|
174
|
+
if (confirmedSource && !request.recordEarlyFailure)
|
|
175
|
+
request.recordEarlyFailure = (error, stage) => confirmedSource.recordFailure(error, stage);
|
|
176
|
+
try {
|
|
177
|
+
confirmedSource?.assertCurrent();
|
|
178
|
+
if (options.employee) {
|
|
179
|
+
const module = await import("./employee-provider.js");
|
|
180
|
+
request.localAcceptance = await module.acceptEmployeeUserTurn(get, options.employee, agent, api, request);
|
|
181
|
+
}
|
|
182
|
+
confirmedSource?.assertCurrent();
|
|
183
|
+
if (confirmedSource) {
|
|
184
|
+
const previous = request.localAdmissionCheck;
|
|
185
|
+
request.localAdmissionCheck = () => { confirmedSource.assertCurrent(); previous?.(); };
|
|
186
|
+
}
|
|
187
|
+
sourceStage = 'login-readiness';
|
|
188
|
+
const { target, connection } = await request.timing.run('login-ready', async () => {
|
|
189
|
+
request.localAdmissionCheck?.();
|
|
190
|
+
const before = api.getConnectionState();
|
|
191
|
+
const target = await ensureReady(api, agent.loginProvider, request.signal);
|
|
192
|
+
validateTarget(target);
|
|
193
|
+
request.signal?.throwIfAborted();
|
|
194
|
+
if (get() !== api)
|
|
195
|
+
throw new NeedError('CONNECTION_LOST', '远程主机连接组件已更换', '当前员工连接');
|
|
196
|
+
const connection = await waitForLogin(api, agent.loginProvider, before, get, request.signal);
|
|
197
|
+
return { target, connection };
|
|
198
|
+
});
|
|
199
|
+
if (request.identity && request.identity !== identity(target))
|
|
200
|
+
throw new NeedError('SESSION_CONTEXT_MISMATCH', '员工主机与原会话不一致,请新建会话', '原员工主机');
|
|
201
|
+
if (options.employee && request.localAcceptance)
|
|
202
|
+
await (await import("./employee-provider.js")).validateEmployeeUserTurn(get, options.employee, api, request, connection);
|
|
176
203
|
request.localAdmissionCheck?.();
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
await options.local?.admit?.(request, api, connection, target);
|
|
204
|
+
admissions.set(request, { api, target, connection });
|
|
205
|
+
sourceStage = 'local-admission';
|
|
206
|
+
await options.local?.admit?.(request, api, connection, target);
|
|
207
|
+
confirmedSource?.assertCurrent();
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
admissions.delete(request);
|
|
211
|
+
const record = request.recordEarlyFailure;
|
|
212
|
+
request.recordEarlyFailure = undefined;
|
|
213
|
+
if (record)
|
|
214
|
+
await record(error, sourceStage);
|
|
215
|
+
else
|
|
216
|
+
await confirmedSource?.recordFailure(error, sourceStage);
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
193
219
|
};
|
|
194
220
|
const resolve = async (request) => {
|
|
195
221
|
if (!admissions.has(request))
|
|
@@ -199,6 +225,7 @@ export function connectProvider(get, options = {}) {
|
|
|
199
225
|
const { api, target, connection } = admitted;
|
|
200
226
|
const stable = () => request.timing.run('connection-confirm', () => waitForLogin(api, agent.loginProvider, connection, get, request.signal));
|
|
201
227
|
await stable();
|
|
228
|
+
request.localAdmissionCheck?.();
|
|
202
229
|
if (!request.sessionId)
|
|
203
230
|
throw new NeedError('PROJECT_BINDING_REQUIRED', '我需要当前会话和已选择的工作区,请使用原生工作区入口', 'DSH会话与原生工作区');
|
|
204
231
|
if (options.employee) {
|
package/dist/remote-details.d.ts
CHANGED
|
@@ -23,6 +23,10 @@ export interface DetailRequest {
|
|
|
23
23
|
export declare function privateDirectory(path: string): Promise<void>;
|
|
24
24
|
export declare function durableWrite(path: string, value: unknown): Promise<void>;
|
|
25
25
|
export declare function readJournal(path: string): Promise<unknown>;
|
|
26
|
+
export interface DiagnosticBinding {
|
|
27
|
+
api: RemoteDetailsApi | undefined;
|
|
28
|
+
connection: Connection | undefined;
|
|
29
|
+
}
|
|
26
30
|
export declare function createRemoteDetails(options: {
|
|
27
31
|
profileDir: string;
|
|
28
32
|
getApi: () => RemoteDetailsApi | undefined;
|
|
@@ -45,5 +49,19 @@ export declare function createRemoteDetails(options: {
|
|
|
45
49
|
eventId: string;
|
|
46
50
|
recorded: true;
|
|
47
51
|
}>;
|
|
52
|
+
recordPreflightCheck(request: {
|
|
53
|
+
sessionId?: string;
|
|
54
|
+
provider: "claude" | "codex";
|
|
55
|
+
observation: DetailObservation;
|
|
56
|
+
context?: {
|
|
57
|
+
turn: string | number | null;
|
|
58
|
+
step: string | number | null;
|
|
59
|
+
};
|
|
60
|
+
binding?: DiagnosticBinding;
|
|
61
|
+
assertScope?: () => void;
|
|
62
|
+
}): Promise<{
|
|
63
|
+
eventId: string;
|
|
64
|
+
recorded: true;
|
|
65
|
+
}>;
|
|
48
66
|
open(request: DetailRequest): Promise<DetailChannel>;
|
|
49
67
|
};
|