@ai-workbench/remote-agent 0.8.2 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -4
- package/dist/acp.d.ts +25 -0
- package/dist/acp.js +159 -17
- package/dist/approval-label.d.ts +6 -0
- package/dist/approval-label.js +45 -0
- package/dist/broker-runtime.d.ts +10 -0
- package/dist/broker-runtime.js +302 -0
- package/dist/broker.d.ts +29 -6
- package/dist/broker.js +141 -141
- package/dist/conversation-details.d.ts +1 -0
- package/dist/conversation-details.js +58 -8
- package/dist/index.d.ts +25 -14
- package/dist/index.js +205 -32
- package/dist/input.d.ts +4 -0
- package/dist/input.js +4 -1
- package/dist/model-catalog.d.ts +55 -0
- package/dist/model-catalog.js +254 -0
- package/dist/model-configuration.d.ts +51 -0
- package/dist/model-configuration.js +212 -0
- package/dist/presentation.js +4 -0
- package/dist/protocol-boundary.d.ts +13 -0
- package/dist/protocol-boundary.js +49 -0
- package/dist/provider.d.ts +19 -1
- package/dist/provider.js +59 -29
- package/dist/recovery-checkpoint.d.ts +6 -0
- package/dist/recovery-checkpoint.js +71 -0
- package/dist/recovery.d.ts +34 -0
- package/dist/recovery.js +52 -0
- package/dist/remote-details.d.ts +3 -0
- package/dist/remote-details.js +3 -3
- package/dist/rpc-namespace.d.ts +3 -0
- package/dist/rpc-namespace.js +50 -0
- package/dist/stage-timing.d.ts +27 -0
- package/dist/stage-timing.js +39 -0
- package/package.json +4 -4
package/dist/provider.js
CHANGED
|
@@ -2,6 +2,7 @@ import { loginDiagnostic, loginReason } from "./login-diagnostic.js";
|
|
|
2
2
|
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
|
+
import { StageTiming } from "./stage-timing.js";
|
|
5
6
|
export function requireLoginCoordinator(api) {
|
|
6
7
|
if (!['getConnectionState', 'getLoginPromptState', 'requestLoginPrompt', 'refreshLoginPrompts'].every(key => typeof api[key] === 'function'))
|
|
7
8
|
throw new NeedError('CONNECT_API_MISMATCH', '我需要 connect-api 0.6.1 的统一登录询问与连接状态能力', 'L >=0.6.1');
|
|
@@ -155,6 +156,7 @@ export function connectProvider(get, options = {}) {
|
|
|
155
156
|
const agent = options.agent ?? CODEX_AGENT;
|
|
156
157
|
const admissions = new WeakMap();
|
|
157
158
|
const preflight = async (request) => {
|
|
159
|
+
request.timing ??= new StageTiming();
|
|
158
160
|
request.signal?.throwIfAborted();
|
|
159
161
|
const api = get();
|
|
160
162
|
if (!api)
|
|
@@ -163,13 +165,16 @@ export function connectProvider(get, options = {}) {
|
|
|
163
165
|
requireLoginCoordinator(api);
|
|
164
166
|
if (request.mode === 'one-shot' && !agent.oneShot)
|
|
165
167
|
throw new NeedError('MODE_UNSUPPORTED', '此远程模型仅支持会话模式', '使用会话模式');
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
168
|
+
const { target, connection } = await request.timing.run('login-ready', async () => {
|
|
169
|
+
const before = api.getConnectionState();
|
|
170
|
+
const target = await ensureReady(api, agent.loginProvider, request.signal);
|
|
171
|
+
validateTarget(target);
|
|
172
|
+
request.signal?.throwIfAborted();
|
|
173
|
+
if (get() !== api)
|
|
174
|
+
throw new NeedError('CONNECTION_LOST', '远程主机连接组件已更换', '当前员工连接');
|
|
175
|
+
const connection = await waitForLogin(api, agent.loginProvider, before, get, request.signal);
|
|
176
|
+
return { target, connection };
|
|
177
|
+
});
|
|
173
178
|
if (request.identity && request.identity !== identity(target))
|
|
174
179
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '员工主机与原会话不一致,请新建会话', '原员工主机');
|
|
175
180
|
admissions.set(request, { api, target, connection });
|
|
@@ -180,13 +185,13 @@ export function connectProvider(get, options = {}) {
|
|
|
180
185
|
const admitted = admissions.get(request);
|
|
181
186
|
admissions.delete(request);
|
|
182
187
|
const { api, target, connection } = admitted;
|
|
183
|
-
const stable = () => waitForLogin(api, agent.loginProvider, connection, get, request.signal);
|
|
188
|
+
const stable = () => request.timing.run('connection-confirm', () => waitForLogin(api, agent.loginProvider, connection, get, request.signal));
|
|
184
189
|
await stable();
|
|
185
190
|
if (!request.sessionId)
|
|
186
191
|
throw new NeedError('PROJECT_BINDING_REQUIRED', '我需要当前会话和已选择的工作区,请使用原生工作区入口', 'DSH会话与原生工作区');
|
|
187
192
|
await options.notice?.reconcile(request, api);
|
|
188
193
|
await stable();
|
|
189
|
-
const resolved = unwrap(await awaitL(() => api.resolveSessionProject(request.sessionId, { provider: agent.loginProvider, purpose: 'user-turn' }), request.signal));
|
|
194
|
+
const resolved = await request.timing.run('workspace-resolve', async () => unwrap(await awaitL(() => api.resolveSessionProject(request.sessionId, { provider: agent.loginProvider, purpose: 'user-turn' }), request.signal)));
|
|
190
195
|
await stable();
|
|
191
196
|
if (!resolved || resolved.instanceId !== connection.instanceId || resolved.epoch !== connection.epoch)
|
|
192
197
|
throw new NeedError('CONNECTION_LOST', '工作区解析回复已失效', '当前连接代次');
|
|
@@ -196,32 +201,57 @@ export function connectProvider(get, options = {}) {
|
|
|
196
201
|
throw new NeedError('PROJECT_BINDING_INVALID', '工作区绑定与当前会话或可信主机不一致', 'L核验的原生工作区绑定');
|
|
197
202
|
if (request.cwd && request.cwd !== project.path || request.bindingId && request.bindingId !== resolved.bindingId || request.workspaceId && request.workspaceId !== resolved.workspaceId)
|
|
198
203
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '原会话的工作区绑定已改变,请新建会话', '原工作区绑定');
|
|
199
|
-
const details = await options.details?.(request, api, connection, target);
|
|
200
|
-
await stable();
|
|
201
|
-
let workspaceNotice = undefined;
|
|
202
|
-
if (resolved.notice) {
|
|
203
|
-
if (!options.notice)
|
|
204
|
-
throw new NeedError('DEPENDENCY_REQUIRED', '我需要官方会话持久记录来确认工作区说明', '官方会话与持久化服务');
|
|
205
|
-
workspaceNotice = await options.notice.deliver(resolved.notice, resolved, request, api);
|
|
206
|
-
await stable();
|
|
207
|
-
}
|
|
208
|
-
if (!options.open)
|
|
209
|
-
throw new NeedError('RUNTIME_REQUIRED', '我需要主机运行适配器', '主机broker适配');
|
|
210
|
-
const runtime = await options.open(resolved.target, project, { ...request, agent });
|
|
204
|
+
const details = await request.timing.run('details-open', async () => options.details?.(request, api, connection, target));
|
|
211
205
|
try {
|
|
206
|
+
if (details)
|
|
207
|
+
request.timing.bind(detail => details.record(detail));
|
|
212
208
|
await stable();
|
|
209
|
+
let workspaceNotice = undefined;
|
|
210
|
+
if (resolved.notice) {
|
|
211
|
+
if (!options.notice)
|
|
212
|
+
throw new NeedError('DEPENDENCY_REQUIRED', '我需要官方会话持久记录来确认工作区说明', '官方会话与持久化服务');
|
|
213
|
+
workspaceNotice = await request.timing.run('workspace-notice', () => options.notice.deliver(resolved.notice, resolved, request, api));
|
|
214
|
+
await stable();
|
|
215
|
+
}
|
|
216
|
+
if (!options.open)
|
|
217
|
+
throw new NeedError('RUNTIME_REQUIRED', '我需要主机运行适配器', '主机broker适配');
|
|
218
|
+
const observedBinding = api.getWorkspaceBindingState?.();
|
|
219
|
+
const validateContext = async (ignoreCancellation = false) => {
|
|
220
|
+
const check = () => waitForLogin(api, agent.loginProvider, connection, get, ignoreCancellation ? undefined : request.signal);
|
|
221
|
+
await check();
|
|
222
|
+
if (observedBinding) {
|
|
223
|
+
const current = api.getWorkspaceBindingState();
|
|
224
|
+
if (current.instanceId !== observedBinding.instanceId || !current.items.some(item => item.bindingId === resolved.bindingId && item.workspaceId === resolved.workspaceId && item.status === 'ready'))
|
|
225
|
+
throw new NeedError('SESSION_CONTEXT_MISMATCH', '当前工作区绑定已改变,原会话操作未执行', '原生会话原绑定');
|
|
226
|
+
}
|
|
227
|
+
await check();
|
|
228
|
+
};
|
|
229
|
+
const runtime = await options.open(resolved.target, project, { ...request, agent, bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, bindingRevision: resolved.bindingRevision, detailChannel: details, validateContext });
|
|
230
|
+
try {
|
|
231
|
+
await stable();
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
await runtime.dispose?.();
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
// The transport can outlive a turn; presentation belongs only to this invocation.
|
|
238
|
+
runtime.workspaceNotice = workspaceNotice || undefined;
|
|
239
|
+
runtime.details = details ?? runtime.details;
|
|
240
|
+
runtime.binding = { bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, revision: resolved.bindingRevision };
|
|
241
|
+
runtime.connection = { ...connection };
|
|
242
|
+
runtime.loginRequired = () => offerLogin(api, request, get, agent);
|
|
243
|
+
return runtime;
|
|
213
244
|
}
|
|
214
245
|
catch (error) {
|
|
215
|
-
|
|
246
|
+
try {
|
|
247
|
+
await details?.flush();
|
|
248
|
+
}
|
|
249
|
+
catch (diagnostic) {
|
|
250
|
+
const code = (value) => value instanceof NeedError && /^[A-Z][A-Z0-9_]*$/.test(value.code) ? value.code : 'UNKNOWN_FAILURE';
|
|
251
|
+
throw new NeedError('CONVERSATION_DETAILS_FAILED', '技术详情未能保存;本轮已停止', `原错误代码 ${code(error)};诊断错误代码 ${code(diagnostic)}`);
|
|
252
|
+
}
|
|
216
253
|
throw error;
|
|
217
254
|
}
|
|
218
|
-
// The transport can outlive a turn; presentation belongs only to this invocation.
|
|
219
|
-
runtime.workspaceNotice = workspaceNotice || undefined;
|
|
220
|
-
runtime.details = details ?? runtime.details;
|
|
221
|
-
runtime.binding = { bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, revision: resolved.bindingRevision };
|
|
222
|
-
runtime.connection = { ...connection };
|
|
223
|
-
runtime.loginRequired = () => offerLogin(api, request, get, agent);
|
|
224
|
-
return runtime;
|
|
225
255
|
};
|
|
226
256
|
resolve.preflight = preflight;
|
|
227
257
|
return resolve;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { RuntimeRequest } from './provider.ts';
|
|
3
|
+
/** A step's accepted batch follows step/start in the official event contract. */
|
|
4
|
+
export declare function currentInputIds(ctx: Context, request: RuntimeRequest): Promise<readonly string[]>;
|
|
5
|
+
/** Read only the official persistence API after its awaited durability checkpoint. */
|
|
6
|
+
export declare function verifyRecoveryCheckpoint(ctx: Context, request: RuntimeRequest, provider: string): Promise<boolean>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
2
|
+
import { BlockAssembler, expandAssistantStream } from '@deepseek-ai/dsh-llm';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
+
/** A step's accepted batch follows step/start in the official event contract. */
|
|
5
|
+
export async function currentInputIds(ctx, request) {
|
|
6
|
+
if (!request.sessionId)
|
|
7
|
+
return [];
|
|
8
|
+
const id = SessionId(request.sessionId), store = ctx.get('sessions'), session = store?.get(id);
|
|
9
|
+
const persistence = ctx.get('sessionPersistence');
|
|
10
|
+
if (!session || !store || !persistence || !await store.flush(session))
|
|
11
|
+
return [];
|
|
12
|
+
const handle = await persistence.open(id, 'read');
|
|
13
|
+
try {
|
|
14
|
+
if (handle.id !== id || handle.header?.id !== id || handle.access !== 'read')
|
|
15
|
+
return [];
|
|
16
|
+
const { events } = await handle.read();
|
|
17
|
+
if (!Array.isArray(events))
|
|
18
|
+
return [];
|
|
19
|
+
let step;
|
|
20
|
+
let ids = [];
|
|
21
|
+
for (const event of events) {
|
|
22
|
+
if (event.type === 'step/start') {
|
|
23
|
+
step = event;
|
|
24
|
+
ids = [];
|
|
25
|
+
}
|
|
26
|
+
else if (event.type === 'turn/start' || event.type === 'step/end' || event.type === 'turn/end') {
|
|
27
|
+
step = undefined;
|
|
28
|
+
ids = [];
|
|
29
|
+
}
|
|
30
|
+
else if (event.type === 'user/message' && step)
|
|
31
|
+
ids.push(event.data.id);
|
|
32
|
+
}
|
|
33
|
+
return ids;
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await handle.close();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Read only the official persistence API after its awaited durability checkpoint. */
|
|
40
|
+
export async function verifyRecoveryCheckpoint(ctx, request, provider) {
|
|
41
|
+
if (!request.sessionId || !request.recoveryCheckpointId || !request.resumeSessionId)
|
|
42
|
+
return false;
|
|
43
|
+
const id = SessionId(request.sessionId), store = ctx.get('sessions'), session = store?.get(id);
|
|
44
|
+
const persistence = ctx.get('sessionPersistence');
|
|
45
|
+
if (!session || typeof store?.flush !== 'function' || typeof persistence?.open !== 'function')
|
|
46
|
+
return false;
|
|
47
|
+
if (!await store.flush(session))
|
|
48
|
+
return false;
|
|
49
|
+
const handle = await persistence.open(id, 'read');
|
|
50
|
+
try {
|
|
51
|
+
if (handle?.id !== id || handle.header?.id !== id || handle.access !== 'read')
|
|
52
|
+
return false;
|
|
53
|
+
const { events } = await handle.read();
|
|
54
|
+
if (!Array.isArray(events))
|
|
55
|
+
return false;
|
|
56
|
+
const final = events.findLast(e => e.type === 'assistant/message' && e.surfaceOp === 'append');
|
|
57
|
+
const live = session.deriveMessages().findLast(m => m.role === 'assistant');
|
|
58
|
+
if (final?.type !== 'assistant/message' || final.data.interrupted || !live || live.id !== final.data.message.id || !isDeepStrictEqual(live, final.data.message))
|
|
59
|
+
return false;
|
|
60
|
+
const message = final.data.message, pointer = message.source.replayState?.response;
|
|
61
|
+
if (message.source.provider !== provider || pointer?.agent !== provider || pointer.dshSessionId !== id || pointer.sessionId !== request.resumeSessionId || pointer.recoveryCheckpointId !== request.recoveryCheckpointId)
|
|
62
|
+
return false;
|
|
63
|
+
const assembled = new BlockAssembler();
|
|
64
|
+
for (const part of expandAssistantStream(final.data.stream))
|
|
65
|
+
assembled.push(part.chunk);
|
|
66
|
+
return ['stop', 'max-tokens'].includes(assembled.finish.kind) && isDeepStrictEqual(assembled.replayState, message.source.replayState) && isDeepStrictEqual(assembled.blocks(), message.content);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await handle.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AgentCapabilities, SessionConfigOption } from '@agentclientprotocol/sdk/unstable';
|
|
2
|
+
export interface AcpRecovery {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
capabilities: AgentCapabilities;
|
|
5
|
+
modelConfiguration?: {
|
|
6
|
+
configOptions: SessionConfigOption[];
|
|
7
|
+
resolvedEvidence?: {
|
|
8
|
+
modelValue: string;
|
|
9
|
+
resolvedModel: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
configUpdates?: SessionConfigOption[][];
|
|
13
|
+
}
|
|
14
|
+
export interface RecoveryState {
|
|
15
|
+
version: 1;
|
|
16
|
+
phase: 'dirty' | 'clean' | 'ended';
|
|
17
|
+
scope: string;
|
|
18
|
+
brokerId: string;
|
|
19
|
+
instanceId?: string;
|
|
20
|
+
pid?: number;
|
|
21
|
+
cursor?: number;
|
|
22
|
+
checkpointId?: string;
|
|
23
|
+
acp?: AcpRecovery;
|
|
24
|
+
}
|
|
25
|
+
export interface RecoveryJournal {
|
|
26
|
+
read(): Promise<RecoveryState | undefined>;
|
|
27
|
+
write(state: RecoveryState): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare function validateRecovery(value: unknown): RecoveryState;
|
|
30
|
+
/** Only negotiated booleans/known empty capability markers, never agent metadata. */
|
|
31
|
+
export declare function recoveryCapabilities(c: AgentCapabilities): AgentCapabilities;
|
|
32
|
+
export declare function recoveryKey(value: unknown): string;
|
|
33
|
+
/** History is immutable; current is atomically replaced only after fsync. No expiry. */
|
|
34
|
+
export declare function recoveryJournal(profileDir: string, key: string): RecoveryJournal;
|
package/dist/recovery.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { open, rename } from 'node:fs/promises';
|
|
4
|
+
import { constants } from 'node:fs';
|
|
5
|
+
import { privateDirectory, durableWrite, readJournal } from "./remote-details.js";
|
|
6
|
+
import { NeedError } from "./remote.js";
|
|
7
|
+
const invalid = () => new NeedError('RECOVERY_STORAGE_INVALID', '会话恢复记录无效,本轮尚未发送', '有效的私有会话恢复记录');
|
|
8
|
+
export function validateRecovery(value) {
|
|
9
|
+
const s = value;
|
|
10
|
+
if (!s || s.version !== 1 || !['dirty', 'clean', 'ended'].includes(s.phase) || !/^[a-f0-9]{64}$/.test(s.scope) || !/^r[a-f0-9]{23}$/.test(s.brokerId))
|
|
11
|
+
throw invalid();
|
|
12
|
+
if (s.instanceId !== undefined && !/^[a-f0-9-]{36}$/.test(s.instanceId) || s.pid !== undefined && (!Number.isSafeInteger(s.pid) || s.pid < 1))
|
|
13
|
+
throw invalid();
|
|
14
|
+
if (s.phase === 'clean' && (!s.instanceId || !Number.isSafeInteger(s.cursor) || s.cursor < 0 || typeof s.checkpointId !== 'string' || !s.checkpointId || !s.acp || typeof s.acp.sessionId !== 'string' || !s.acp.sessionId || !s.acp.capabilities || typeof s.acp.capabilities !== 'object' || Array.isArray(s.acp.capabilities)))
|
|
15
|
+
throw invalid();
|
|
16
|
+
return structuredClone(s);
|
|
17
|
+
}
|
|
18
|
+
/** Only negotiated booleans/known empty capability markers, never agent metadata. */
|
|
19
|
+
export function recoveryCapabilities(c) {
|
|
20
|
+
return { loadSession: c.loadSession === true, promptCapabilities: { image: c.promptCapabilities?.image === true, audio: c.promptCapabilities?.audio === true, embeddedContext: c.promptCapabilities?.embeddedContext === true }, sessionCapabilities: { ...(c.sessionCapabilities?.resume ? { resume: {} } : {}), ...(c.sessionCapabilities?.systemPrompt ? { systemPrompt: {} } : {}) } };
|
|
21
|
+
}
|
|
22
|
+
export function recoveryKey(value) { return createHash('sha256').update(JSON.stringify(value)).digest('hex'); }
|
|
23
|
+
/** History is immutable; current is atomically replaced only after fsync. No expiry. */
|
|
24
|
+
export function recoveryJournal(profileDir, key) {
|
|
25
|
+
if (!/^[a-f0-9]{64}$/.test(key))
|
|
26
|
+
throw invalid();
|
|
27
|
+
const parent = join(profileDir, 'ai-workbench-remote-agent'), dir = join(parent, 'recovery', key), current = join(dir, 'current.json');
|
|
28
|
+
let tail = Promise.resolve();
|
|
29
|
+
const prepare = async () => { await privateDirectory(parent); await privateDirectory(join(parent, 'recovery')); await privateDirectory(dir); };
|
|
30
|
+
return {
|
|
31
|
+
async read() { await tail; await prepare(); try {
|
|
32
|
+
return validateRecovery(await readJournal(current));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error?.code === 'ENOENT')
|
|
36
|
+
return;
|
|
37
|
+
throw error;
|
|
38
|
+
} },
|
|
39
|
+
write(state) { const value = validateRecovery(state); const task = tail.then(async () => { await prepare(); const id = randomUUID(); await durableWrite(join(dir, id + '.json'), value); const temp = join(dir, id + '.current'); const file = await open(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600); try {
|
|
40
|
+
await file.writeFile(JSON.stringify(value) + '\n');
|
|
41
|
+
await file.sync();
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await file.close();
|
|
45
|
+
} await rename(temp, current); const directory = await open(dir, 'r'); try {
|
|
46
|
+
await directory.sync();
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await directory.close();
|
|
50
|
+
} }); tail = task; return task; },
|
|
51
|
+
};
|
|
52
|
+
}
|
package/dist/remote-details.d.ts
CHANGED
|
@@ -19,6 +19,9 @@ export interface DetailRequest {
|
|
|
19
19
|
identity: string;
|
|
20
20
|
connection: Connection;
|
|
21
21
|
}
|
|
22
|
+
export declare function privateDirectory(path: string): Promise<void>;
|
|
23
|
+
export declare function durableWrite(path: string, value: unknown): Promise<void>;
|
|
24
|
+
export declare function readJournal(path: string): Promise<unknown>;
|
|
22
25
|
export declare function createRemoteDetails(options: {
|
|
23
26
|
profileDir: string;
|
|
24
27
|
getApi: () => RemoteDetailsApi | undefined;
|
package/dist/remote-details.js
CHANGED
|
@@ -23,7 +23,7 @@ function kind(observation) {
|
|
|
23
23
|
return observation.severity === 'action-required' ? 'action-required' : 'unknown';
|
|
24
24
|
return { extension: 'extensions', tool: 'tools', need: 'action-required', notice: 'extensions', session: 'extensions' }[observation.kind] ?? observation.kind;
|
|
25
25
|
}
|
|
26
|
-
async function privateDirectory(path) {
|
|
26
|
+
export async function privateDirectory(path) {
|
|
27
27
|
if (!isAbsolute(path))
|
|
28
28
|
throw failure('REMOTE_DETAIL_STORAGE_FAILED');
|
|
29
29
|
// Existing parent/profile ownership is the host's responsibility; reject symlinks
|
|
@@ -57,7 +57,7 @@ async function privateDirectory(path) {
|
|
|
57
57
|
if ((info.mode & 0o077) !== 0 || (process.getuid && info.uid !== process.getuid()))
|
|
58
58
|
throw failure('REMOTE_DETAIL_STORAGE_FAILED');
|
|
59
59
|
}
|
|
60
|
-
async function durableWrite(path, value) {
|
|
60
|
+
export async function durableWrite(path, value) {
|
|
61
61
|
const file = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
62
62
|
try {
|
|
63
63
|
await file.writeFile(JSON.stringify(value) + '\n');
|
|
@@ -74,7 +74,7 @@ async function durableWrite(path, value) {
|
|
|
74
74
|
await parent.close();
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
-
async function readJournal(path) {
|
|
77
|
+
export async function readJournal(path) {
|
|
78
78
|
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
79
79
|
try {
|
|
80
80
|
const info = await file.stat();
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { NeedError } from "./remote.js";
|
|
3
|
+
/** Public Stream only. Server request/approval IDs are never changed. */
|
|
4
|
+
export function isolateRpcIds(stream) {
|
|
5
|
+
const prefix = randomUUID() + '/';
|
|
6
|
+
const pending = new Map();
|
|
7
|
+
const readable = stream.readable.pipeThrough(new TransformStream({ transform(message, controller) {
|
|
8
|
+
const raw = message;
|
|
9
|
+
if (!raw.method && Object.hasOwn(raw, 'id')) {
|
|
10
|
+
if (typeof raw.id !== 'string' || !pending.has(raw.id))
|
|
11
|
+
throw new NeedError('ACP_FOREIGN_RESPONSE', '收到不属于当前客户端的协议响应', '原会话的完整客户端状态');
|
|
12
|
+
const id = pending.get(raw.id);
|
|
13
|
+
pending.delete(raw.id);
|
|
14
|
+
controller.enqueue({ ...raw, id });
|
|
15
|
+
}
|
|
16
|
+
else
|
|
17
|
+
controller.enqueue(message);
|
|
18
|
+
} }));
|
|
19
|
+
const writable = new WritableStream({ async write(message) {
|
|
20
|
+
const raw = message;
|
|
21
|
+
let outgoing = raw;
|
|
22
|
+
if (typeof raw.method === 'string' && Object.hasOwn(raw, 'id')) {
|
|
23
|
+
const id = prefix + JSON.stringify(raw.id);
|
|
24
|
+
pending.set(id, raw.id);
|
|
25
|
+
outgoing = { ...raw, id };
|
|
26
|
+
}
|
|
27
|
+
else if (raw.method === '$/cancel_request' && raw.params && typeof raw.params === 'object') {
|
|
28
|
+
const params = raw.params;
|
|
29
|
+
outgoing = { ...raw, params: { ...params, requestId: prefix + JSON.stringify(params.requestId) } };
|
|
30
|
+
}
|
|
31
|
+
const writer = stream.writable.getWriter();
|
|
32
|
+
try {
|
|
33
|
+
await writer.write(outgoing);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
writer.releaseLock();
|
|
37
|
+
}
|
|
38
|
+
}, async close() { const writer = stream.writable.getWriter(); try {
|
|
39
|
+
await writer.close();
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
writer.releaseLock();
|
|
43
|
+
} }, async abort(reason) { const writer = stream.writable.getWriter(); try {
|
|
44
|
+
await writer.abort(reason);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
writer.releaseLock();
|
|
48
|
+
} } });
|
|
49
|
+
return { readable, writable };
|
|
50
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DetailObservation } from './conversation-details.ts';
|
|
2
|
+
export declare const STAGES: Readonly<{
|
|
3
|
+
readonly 'login-ready': "登录就绪检查";
|
|
4
|
+
readonly 'connection-confirm': "核对当前连接状态";
|
|
5
|
+
readonly 'input-prepare': "准备本轮输入";
|
|
6
|
+
readonly 'workspace-resolve': "解析工作区";
|
|
7
|
+
readonly 'details-open': "打开技术详情";
|
|
8
|
+
readonly 'details-flush': "等待技术详情持久确认";
|
|
9
|
+
readonly 'workspace-notice': "记录工作区说明";
|
|
10
|
+
readonly 'broker-list': "查询主机会话";
|
|
11
|
+
readonly 'broker-start': "启动主机会话";
|
|
12
|
+
readonly 'broker-attach': "连接主机会话";
|
|
13
|
+
readonly 'acp-initialize': "初始化远程模型";
|
|
14
|
+
readonly 'acp-session-new': "新建模型会话";
|
|
15
|
+
readonly 'acp-session-load': "加载模型会话";
|
|
16
|
+
readonly 'acp-session-resume': "恢复模型会话";
|
|
17
|
+
readonly 'acp-prompt': "模型处理本轮(含等待审批)";
|
|
18
|
+
}>;
|
|
19
|
+
export type Stage = keyof typeof STAGES;
|
|
20
|
+
/** Passive client observations; no thresholds, retries, or remote-clock claims. */
|
|
21
|
+
export declare class StageTiming {
|
|
22
|
+
private readonly started;
|
|
23
|
+
private pending;
|
|
24
|
+
private sink?;
|
|
25
|
+
bind(sink: (detail: DetailObservation) => void): void;
|
|
26
|
+
run<T>(phase: Stage, operation: () => Promise<T>): Promise<T>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const STAGES = Object.freeze({
|
|
2
|
+
'login-ready': '登录就绪检查', 'connection-confirm': '核对当前连接状态', 'input-prepare': '准备本轮输入', 'workspace-resolve': '解析工作区',
|
|
3
|
+
'details-open': '打开技术详情', 'details-flush': '等待技术详情持久确认', 'workspace-notice': '记录工作区说明',
|
|
4
|
+
'broker-list': '查询主机会话', 'broker-start': '启动主机会话', 'broker-attach': '连接主机会话',
|
|
5
|
+
'acp-initialize': '初始化远程模型', 'acp-session-new': '新建模型会话',
|
|
6
|
+
'acp-session-load': '加载模型会话', 'acp-session-resume': '恢复模型会话',
|
|
7
|
+
'acp-prompt': '模型处理本轮(含等待审批)',
|
|
8
|
+
});
|
|
9
|
+
/** Passive client observations; no thresholds, retries, or remote-clock claims. */
|
|
10
|
+
export class StageTiming {
|
|
11
|
+
started = performance.now();
|
|
12
|
+
pending = [];
|
|
13
|
+
sink;
|
|
14
|
+
bind(sink) {
|
|
15
|
+
this.sink = sink;
|
|
16
|
+
const pending = this.pending;
|
|
17
|
+
this.pending = [];
|
|
18
|
+
for (const detail of pending)
|
|
19
|
+
sink(detail);
|
|
20
|
+
}
|
|
21
|
+
async run(phase, operation) {
|
|
22
|
+
const started = performance.now();
|
|
23
|
+
let outcome = 'failed';
|
|
24
|
+
try {
|
|
25
|
+
const result = await operation();
|
|
26
|
+
outcome = 'ok';
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
const ended = performance.now();
|
|
31
|
+
const durationMs = ended - started, elapsedMs = ended - this.started;
|
|
32
|
+
const detail = { kind: 'session', severity: outcome === 'ok' ? 'info' : 'warning', code: 'REMOTE_STAGE_TIMING', summary: STAGES[phase] + ':' + durationMs.toFixed(1) + '毫秒', payload: { phase, durationMs, elapsedMs, outcome, clock: 'client-monotonic' } };
|
|
33
|
+
if (this.sink)
|
|
34
|
+
this.sink(detail);
|
|
35
|
+
else
|
|
36
|
+
this.pending.push(detail);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-workbench/remote-agent",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"cordis.patch.yml"
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
|
-
"test": "npm run build && node --test lib/test/*.test.js",
|
|
22
|
+
"test": "npm run build && node --test --test-concurrency=1 lib/test/*.test.js",
|
|
23
23
|
"typecheck": "node node_modules/typescript/bin/tsc --noEmit",
|
|
24
24
|
"build": "node node_modules/typescript/bin/tsc && node node_modules/typescript/bin/tsc -p tsconfig.build.json && chmod 755 lib/src/sdk-ssh-wrapper.js dist/sdk-ssh-wrapper.js && node tools/build-client.mjs",
|
|
25
25
|
"probe:host": "node tools/probe-host.ts",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"zod": "4.6.7"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.
|
|
39
|
+
"@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.13.tgz",
|
|
40
40
|
"@deepseek-ai/cordis": "4.0.4",
|
|
41
41
|
"@deepseek-ai/cordis-plugin-group": "1.0.4",
|
|
42
42
|
"@deepseek-ai/cordis-plugin-include": "1.0.9",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@deepseek-ai/dsh-attachment": "0.2.0-rc.2"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@ai-workbench/connect": "~0.7.
|
|
59
|
+
"@ai-workbench/connect": "~0.7.13",
|
|
60
60
|
"@deepseek-ai/cordis": "4.0.4",
|
|
61
61
|
"@deepseek-ai/dsh-llm": "0.2.0-rc.2",
|
|
62
62
|
"@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
|