@ai-workbench/remote-agent 0.8.2 → 0.8.10
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 +42 -7
- package/dist/acp.d.ts +46 -1
- package/dist/acp.js +296 -36
- package/dist/approval-label.d.ts +6 -0
- package/dist/approval-label.js +57 -0
- package/dist/broker-runtime.d.ts +10 -0
- package/dist/broker-runtime.js +334 -0
- package/dist/broker.d.ts +29 -6
- package/dist/broker.js +146 -142
- package/dist/claude-local.d.ts +37 -0
- package/dist/claude-local.js +207 -0
- package/dist/client.js +71 -2
- package/dist/conversation-details.d.ts +1 -1
- package/dist/conversation-details.js +88 -8
- package/dist/employee-endpoint.d.ts +91 -0
- package/dist/employee-endpoint.js +141 -0
- package/dist/employee-exec.d.ts +53 -0
- package/dist/employee-exec.js +423 -0
- package/dist/employee-location.d.ts +66 -0
- package/dist/employee-location.js +51 -0
- package/dist/employee-mcp.d.ts +42 -0
- package/dist/employee-mcp.js +181 -0
- package/dist/employee-native.d.ts +18 -0
- package/dist/employee-native.js +38 -0
- package/dist/employee-provider.d.ts +42 -0
- package/dist/employee-provider.js +326 -0
- package/dist/employee-relay.d.ts +18 -0
- package/dist/employee-relay.js +49 -0
- package/dist/employee-shell.d.ts +26 -0
- package/dist/employee-shell.js +89 -0
- package/dist/employee-ssh.d.ts +22 -0
- package/dist/employee-ssh.js +123 -0
- package/dist/employee-tools.d.ts +117 -0
- package/dist/employee-tools.js +292 -0
- package/dist/employee-workspace.d.ts +26 -0
- package/dist/employee-workspace.js +61 -0
- package/dist/index.d.ts +31 -14
- package/dist/index.js +335 -42
- package/dist/input.d.ts +15 -0
- package/dist/input.js +65 -24
- package/dist/local-user-turn.d.ts +24 -0
- package/dist/local-user-turn.js +103 -0
- 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.d.ts +1 -1
- package/dist/presentation.js +50 -2
- package/dist/protocol-boundary.d.ts +13 -0
- package/dist/protocol-boundary.js +49 -0
- package/dist/provider.d.ts +54 -2
- package/dist/provider.js +92 -29
- package/dist/readiness.js +1 -1
- package/dist/recovery-checkpoint.d.ts +6 -0
- package/dist/recovery-checkpoint.js +71 -0
- package/dist/recovery.d.ts +47 -0
- package/dist/recovery.js +61 -0
- package/dist/remote-details.d.ts +10 -0
- package/dist/remote-details.js +14 -4
- 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/helpers/ripgrep/LICENSE-vscode +13 -0
- package/helpers/ripgrep/PROVENANCE.md +9 -0
- package/helpers/ripgrep/darwin-arm64/rg +0 -0
- package/helpers/ripgrep/darwin-x64/rg +0 -0
- package/helpers/ripgrep/manifest.json +56 -0
- package/helpers/ripgrep/notices/COPYING-ripgrep +3 -0
- package/helpers/ripgrep/notices/DEPENDENCIES.json +946 -0
- package/helpers/ripgrep/notices/LICENCE-PCRE2.md +103 -0
- package/helpers/ripgrep/notices/LICENSE-MIT-ripgrep +21 -0
- package/helpers/ripgrep/notices/UNLICENSE-ripgrep +24 -0
- package/helpers/ripgrep/notices/aho-corasick-1.1.3/COPYING +3 -0
- package/helpers/ripgrep/notices/aho-corasick-1.1.3/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/aho-corasick-1.1.3/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/bstr-1.12.0/COPYING +8 -0
- package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-THIRD-PARTY +593 -0
- package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-MIT +27 -0
- package/helpers/ripgrep/notices/encoding_rs-0.8.35/COPYRIGHT +17 -0
- package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-APACHE +202 -0
- package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-WHATWG +26 -0
- package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/COPYING +8 -0
- package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-MIT +26 -0
- package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/lexopt-0.3.1/LICENSE +19 -0
- package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/log-0.4.28/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/log-0.4.28/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/memchr-2.7.6/COPYING +3 -0
- package/helpers/ripgrep/notices/memchr-2.7.6/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/memchr-2.7.6/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-MIT +26 -0
- package/helpers/ripgrep/notices/msvc_spectre_libs-0.1.3/LICENSE +21 -0
- package/helpers/ripgrep/notices/pcre2-0.2.11/COPYING +3 -0
- package/helpers/ripgrep/notices/pcre2-0.2.11/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/pcre2-0.2.11/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/pcre2-sys-0.2.10/COPYING +3 -0
- package/helpers/ripgrep/notices/pcre2-sys-0.2.10/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/pcre2-sys-0.2.10/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/r-efi-5.3.0/AUTHORS +74 -0
- package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/regex-syntax-0.8.8/src/unicode_tables/LICENSE-UNICODE +57 -0
- package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-BOOST +23 -0
- package/helpers/ripgrep/notices/same-file-1.0.6/COPYING +3 -0
- package/helpers/ripgrep/notices/same-file-1.0.6/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/same-file-1.0.6/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-APACHE +13 -0
- package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/termcolor-1.4.1/COPYING +3 -0
- package/helpers/ripgrep/notices/termcolor-1.4.1/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/termcolor-1.4.1/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/textwrap-0.16.2/LICENSE +21 -0
- package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/jemalloc/COPYING +27 -0
- package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-MIT +25 -0
- package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-APACHE +176 -0
- package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-UNICODE +39 -0
- package/helpers/ripgrep/notices/walkdir-2.5.0/COPYING +3 -0
- package/helpers/ripgrep/notices/walkdir-2.5.0/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/walkdir-2.5.0/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
- package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-MIT +23 -0
- package/helpers/ripgrep/notices/winapi-util-0.1.11/COPYING +3 -0
- package/helpers/ripgrep/notices/winapi-util-0.1.11/LICENSE-MIT +21 -0
- package/helpers/ripgrep/notices/winapi-util-0.1.11/UNLICENSE +24 -0
- package/helpers/ripgrep/notices/windows-link-0.2.1/license-apache-2.0 +201 -0
- package/helpers/ripgrep/notices/windows-link-0.2.1/license-mit +21 -0
- package/helpers/ripgrep/notices/windows-sys-0.61.2/license-apache-2.0 +201 -0
- package/helpers/ripgrep/notices/windows-sys-0.61.2/license-mit +21 -0
- package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-APACHE +201 -0
- package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
- package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-MIT +23 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +2 -2
- package/package.json +30 -12
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@ import { SessionId } from '@deepseek-ai/dsh-session';
|
|
|
2
2
|
import Schema from '@deepseek-ai/schemastery';
|
|
3
3
|
import { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { createHash } from 'node:crypto';
|
|
5
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
6
|
+
import { recoveryJournal, runtimeRecoveryKey, supportsCodexContextPolicy } from "./recovery.js";
|
|
6
7
|
import * as unstable from '@agentclientprotocol/sdk/unstable';
|
|
7
8
|
import * as acp from '@agentclientprotocol/sdk';
|
|
8
9
|
import { AcpSession } from "./acp.js";
|
|
@@ -11,18 +12,29 @@ import { StreamMapper } from "./mapping.js";
|
|
|
11
12
|
import { connectProvider } from "./provider.js";
|
|
12
13
|
import { brokerFactory } from "./broker.js";
|
|
13
14
|
import { AGENTS, CODEX_AGENT } from "./agents.js";
|
|
14
|
-
import { prepareInput } from "./input.js";
|
|
15
|
+
import { prepareInput, prepareContinuation } from "./input.js";
|
|
15
16
|
import { createWorkspaceNoticeConsumer, isWorkspaceNoticeMessage } from "./workspace-notice.js";
|
|
16
17
|
import { NeedError } from "./remote.js";
|
|
17
18
|
import { employeeFailure } from "./presentation.js";
|
|
18
19
|
import { createRemoteDetails } from "./remote-details.js";
|
|
19
20
|
import { PROVIDER_NAMES, ProviderLocaleRemote } from "./provider-locale.js";
|
|
21
|
+
import { approvalLabel } from "./approval-label.js";
|
|
22
|
+
import { StageTiming } from "./stage-timing.js";
|
|
23
|
+
import { verifyRecoveryCheckpoint, currentInputIds } from "./recovery-checkpoint.js";
|
|
24
|
+
import { ModelCatalog, modelInfo } from "./model-catalog.js";
|
|
25
|
+
import { ModelConfigurationError } from "./model-configuration.js";
|
|
26
|
+
import { officialEmployeeWorkspace } from "./employee-workspace.js";
|
|
27
|
+
import { resolveEmployeeNative } from "./employee-native.js";
|
|
28
|
+
import { createLocalUserTurnObserver } from "./local-user-turn.js";
|
|
29
|
+
import { createClaudeLocalProvider } from "./claude-local.js";
|
|
30
|
+
import { readLocalUserTurn } from "./local-user-turn.js";
|
|
31
|
+
import { readEmployeeWorkspace } from "./employee-workspace.js";
|
|
20
32
|
export const name = 'ai-workbench-remote-agent';
|
|
21
33
|
export const settingsNamespace = 'ai-workbench-codex';
|
|
22
34
|
// L can be installed/enabled after X; keeping X mounted lets it explain the prerequisite.
|
|
23
35
|
export const inject = ['llm'];
|
|
24
36
|
/** Owner-approved defaults; administrators may override through the existing settings. */
|
|
25
|
-
export const DEFAULT_CONFIG = Object.freeze({ mode: 'session', acpMaxMessageBytes: 4194304, brokerMaxFrameBytes: 2097152, brokerControlMaxBytes: 1048576, oneShotSandbox: 'workspace-write', oneShotApproval: 'never' });
|
|
37
|
+
export const DEFAULT_CONFIG = Object.freeze({ mode: 'session', acpMaxMessageBytes: 4194304, brokerMaxFrameBytes: 2097152, brokerControlMaxBytes: 1048576, oneShotSandbox: 'workspace-write', oneShotApproval: 'never', employeeShellKillGraceMs: 1000 });
|
|
26
38
|
export const Config = Schema.object({
|
|
27
39
|
mode: Schema.union([Schema.const('session').i18n({ en: 'Session', zh: '会话模式' }), Schema.const('one-shot').i18n({ en: 'One-shot', zh: '单次模式' })]).i18n({ en: 'Run mode; start a new session after switching modes', zh: 'Codex运行模式;Claude固定会话模式;切换后请新建会话' }).default(DEFAULT_CONFIG.mode).volatile(),
|
|
28
40
|
acpMaxMessageBytes: Schema.number().min(1).step(1).i18n({ en: 'Session message limit; default 4 MiB, adjustable by administrators', zh: '会话消息容量;默认4 MiB,管理员可调整' }).default(DEFAULT_CONFIG.acpMaxMessageBytes).volatile(),
|
|
@@ -30,11 +42,13 @@ export const Config = Schema.object({
|
|
|
30
42
|
brokerControlMaxBytes: Schema.number().min(1).step(1).i18n({ en: 'Host session control reply limit; default 1 MiB, adjustable by administrators', zh: '主机会话管理回复容量;默认1 MiB,管理员可调整' }).default(DEFAULT_CONFIG.brokerControlMaxBytes).volatile(),
|
|
31
43
|
oneShotSandbox: Schema.union(['read-only', 'workspace-write', 'danger-full-access']).i18n({ en: 'One-shot sandbox; workspace changes are allowed by default', zh: '单次任务沙箱;默认允许工作区内修改' }).default(DEFAULT_CONFIG.oneShotSandbox).volatile(),
|
|
32
44
|
oneShotApproval: Schema.const('never').i18n({ en: 'Non-interactive approval; permissions follow the launch policy, never automatic approval', zh: '单次模式无交互审批;权限由启动策略决定,不表示自动批准' }).default(DEFAULT_CONFIG.oneShotApproval).volatile(),
|
|
45
|
+
employeeShellKillGraceMs: Schema.number().min(0).max(2147483647).step(1).i18n({ en: 'Local command SSH cleanup grace in milliseconds; cancellation keeps the remote result unknown', zh: '本机命令SSH取消后的清理宽限毫秒;默认1000,取消仍保留员工端结果未知' }).default(DEFAULT_CONFIG.employeeShellKillGraceMs).volatile(),
|
|
33
46
|
});
|
|
34
47
|
/** DSH volatile settings are handles; adapters freeze their values once per invocation. */
|
|
35
48
|
export function readConfig(config) {
|
|
36
49
|
return Object.fromEntries(Object.entries(config).map(([key, value]) => [key, typeof value === 'object' && value !== null && 'get' in value ? value.get() : value]));
|
|
37
50
|
}
|
|
51
|
+
function validContextDigest(value) { return typeof value === 'string' && /^[a-fA-F0-9]{64}$/u.test(value); }
|
|
38
52
|
function readPointer(options, agent) {
|
|
39
53
|
const last = options.messages.findLast(message => message.role === 'assistant' && !isWorkspaceNoticeMessage(message));
|
|
40
54
|
if (!last || last.role !== 'assistant')
|
|
@@ -47,6 +61,8 @@ function readPointer(options, agent) {
|
|
|
47
61
|
throw new NeedError('SESSION_STATE_INVALID', '原对话缺少新版工作区/模型绑定,不能静默迁移,请新建会话', '有效的新一代远端会话恢复记录');
|
|
48
62
|
if (options.sessionId && pointer.dshSessionId !== options.sessionId)
|
|
49
63
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '此恢复记录属于另一会话,复制或分叉后不能沿用原远端进程,请新建会话', '当前DSH会话自己的远端恢复记录');
|
|
64
|
+
if (agent.loginProvider === 'codex' && (!validContextDigest(pointer.contextSha256) || (pointer.previousRemoteSessions !== undefined && (!Array.isArray(pointer.previousRemoteSessions) || !pointer.previousRemoteSessions.every(ref => ref && typeof ref.sessionId === 'string' && ref.sessionId.length > 0 && validContextDigest(ref.contextSha256))))))
|
|
65
|
+
throw new NeedError('SESSION_CONTEXT_DIGEST_INVALID', '原Codex会话的初建上下文摘要未知或无效,不能猜测是否相同,本轮未发送', '原会话合法的64位十六进制初建上下文摘要');
|
|
50
66
|
return pointer;
|
|
51
67
|
}
|
|
52
68
|
export class CodexRemoteAdapter extends LlmAdapter {
|
|
@@ -56,11 +72,15 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
56
72
|
agent;
|
|
57
73
|
readImage;
|
|
58
74
|
reportLoginFailure;
|
|
75
|
+
directory;
|
|
59
76
|
config;
|
|
60
77
|
sessions = new Map();
|
|
61
78
|
turns = new Map();
|
|
62
79
|
lifetime = new AbortController();
|
|
63
|
-
|
|
80
|
+
shuttingDown = false;
|
|
81
|
+
detached = new Set();
|
|
82
|
+
cleanups = new WeakMap();
|
|
83
|
+
constructor(config, resolveRuntime, approve, providerName = () => PROVIDER_NAMES.en, agent = CODEX_AGENT, readImage, reportLoginFailure, directory) {
|
|
64
84
|
super();
|
|
65
85
|
this.resolveRuntime = resolveRuntime;
|
|
66
86
|
this.approve = approve;
|
|
@@ -68,23 +88,81 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
68
88
|
this.agent = agent;
|
|
69
89
|
this.readImage = readImage;
|
|
70
90
|
this.reportLoginFailure = reportLoginFailure;
|
|
91
|
+
this.directory = directory;
|
|
71
92
|
this.config = typeof config === 'function' ? config : () => config;
|
|
72
93
|
}
|
|
73
94
|
providerInfo(provider) { return { id: provider, name: this.providerName() }; }
|
|
74
|
-
|
|
95
|
+
catalog(signal) { if (!this.directory)
|
|
96
|
+
throw new LlmError('无法获取模型列表:缺少可信主机目录能力', 'MODEL_CATALOG_UNAVAILABLE'); return this.directory.read(this.agent, signal); }
|
|
97
|
+
async listModels(provider) { const catalog = await this.catalog(); try {
|
|
98
|
+
return catalog.models.map(model => modelInfo(provider, model));
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
catalog.release();
|
|
102
|
+
} }
|
|
103
|
+
async resolveModel(provider, id, signal) { const catalog = await this.catalog(signal); try {
|
|
104
|
+
const model = catalog.models.find(model => model.id === id);
|
|
105
|
+
if (!model)
|
|
106
|
+
throw new LlmError('无法获取模型列表:所选模型已不在当前主机目录中', 'MODEL_CATALOG_UNAVAILABLE');
|
|
107
|
+
return modelInfo(provider, model);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
catalog.release();
|
|
111
|
+
} }
|
|
112
|
+
async invalidateModelState() { for (const turn of this.turns.values())
|
|
113
|
+
turn.abort(); await Promise.all([...this.sessions.keys()].map(id => this.disposeSession(id))); }
|
|
114
|
+
async invalidateEmployeeState() { await Promise.all([...this.sessions].filter(([, live]) => live.runtime.employeeExecutionMeta || live.runtime.execution).map(([id]) => this.disposeSession(id))); }
|
|
75
115
|
providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
|
|
116
|
+
cleanupRuntime(runtime) {
|
|
117
|
+
const existing = this.cleanups.get(runtime);
|
|
118
|
+
if (existing)
|
|
119
|
+
return existing;
|
|
120
|
+
const cleanup = Promise.resolve().then(() => runtime.dispose?.()).catch(() => {
|
|
121
|
+
const failure = new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '原主机会话的清理结果未确认,不能继续或重开此会话', '管理员核对并结束原主机会话');
|
|
122
|
+
for (const live of this.sessions.values())
|
|
123
|
+
if (live.runtime === runtime) {
|
|
124
|
+
live.cleanupFailure = failure;
|
|
125
|
+
live.failure ??= { cause: failure };
|
|
126
|
+
}
|
|
127
|
+
throw failure;
|
|
128
|
+
});
|
|
129
|
+
this.cleanups.set(runtime, cleanup);
|
|
130
|
+
return cleanup;
|
|
131
|
+
}
|
|
76
132
|
async disposeSession(sessionId) {
|
|
77
133
|
this.turns.get(sessionId)?.abort();
|
|
78
134
|
const current = this.sessions.get(sessionId);
|
|
79
135
|
if (!current)
|
|
80
136
|
return;
|
|
81
|
-
|
|
137
|
+
if (current.disposal)
|
|
138
|
+
return current.disposal;
|
|
82
139
|
current.session.close();
|
|
83
|
-
|
|
140
|
+
current.disposal = this.cleanupRuntime(current.runtime).then(() => { if (this.sessions.get(sessionId) === current)
|
|
141
|
+
this.sessions.delete(sessionId); });
|
|
142
|
+
return current.disposal;
|
|
143
|
+
}
|
|
144
|
+
async detachSession(sessionId) {
|
|
145
|
+
const current = this.sessions.get(sessionId), turn = this.turns.get(sessionId);
|
|
146
|
+
if (!current && !turn)
|
|
147
|
+
return;
|
|
148
|
+
this.detached.add(sessionId);
|
|
149
|
+
if (current) {
|
|
150
|
+
try {
|
|
151
|
+
await current.runtime.detach?.();
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
current.session.close();
|
|
155
|
+
this.sessions.delete(sessionId);
|
|
156
|
+
turn?.abort();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else
|
|
160
|
+
turn?.abort();
|
|
84
161
|
}
|
|
85
162
|
async dispose() {
|
|
163
|
+
this.shuttingDown = true;
|
|
164
|
+
const results = await Promise.allSettled([...new Set([...this.sessions.keys(), ...this.turns.keys()])].map(id => this.detachSession(id)));
|
|
86
165
|
this.lifetime.abort();
|
|
87
|
-
const results = await Promise.allSettled([...this.sessions.keys()].map(id => this.disposeSession(id)));
|
|
88
166
|
const failure = results.find(result => result.status === 'rejected');
|
|
89
167
|
if (failure?.status === 'rejected')
|
|
90
168
|
throw failure.reason;
|
|
@@ -94,9 +172,11 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
94
172
|
yield* this.generate(options);
|
|
95
173
|
}
|
|
96
174
|
catch (error) {
|
|
175
|
+
if (error instanceof ModelConfigurationError)
|
|
176
|
+
throw new LlmError(error.message, error.code, { cause: error });
|
|
97
177
|
if (error instanceof NeedError) {
|
|
98
178
|
const rpc = /RPC 错误码 (-?\d+)/u.exec(error.requires)?.[1];
|
|
99
|
-
const message = employeeFailure(error.code) + (rpc ? `(技术详情:RPC 错误码 ${rpc})` : '');
|
|
179
|
+
const message = employeeFailure(error.code, this.agent.loginProvider) + (rpc ? `(技术详情:RPC 错误码 ${rpc})` : '');
|
|
100
180
|
let diagnostic = error.technical ? JSON.stringify(error.technical) : '';
|
|
101
181
|
if (error.technical && this.reportLoginFailure) {
|
|
102
182
|
try {
|
|
@@ -125,12 +205,17 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
125
205
|
this.turns.set(key, turn);
|
|
126
206
|
const signal = AbortSignal.any([this.lifetime.signal, turn.signal, ...(original.signal ? [original.signal] : [])]);
|
|
127
207
|
const options = { ...original, signal };
|
|
208
|
+
const timing = new StageTiming();
|
|
128
209
|
let runtime;
|
|
129
210
|
let session;
|
|
130
211
|
let live;
|
|
131
212
|
let succeeded = false;
|
|
132
213
|
let remoteAttempted = false;
|
|
214
|
+
let preserveRecoveredRuntime = false;
|
|
133
215
|
let activeMapper;
|
|
216
|
+
let catalog;
|
|
217
|
+
let selected;
|
|
218
|
+
let modelConfiguration;
|
|
134
219
|
const config = this.config();
|
|
135
220
|
const mode = this.agent.oneShot ? config.mode : 'session';
|
|
136
221
|
try {
|
|
@@ -139,43 +224,118 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
139
224
|
const previous = readPointer(options, this.agent);
|
|
140
225
|
if (previous && previous.mode !== mode)
|
|
141
226
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '运行模式与原会话不一致,请新建会话', '原会话模式');
|
|
227
|
+
if (previous && !previous.modelConfiguration)
|
|
228
|
+
throw new NeedError('MODEL_SELECTION_REQUIRED', '旧远端恢复记录没有明确模型选择,请新建会话并选择模型', '本轮明确选择的模型');
|
|
142
229
|
live = key ? this.sessions.get(key) : undefined;
|
|
230
|
+
if (live?.disposal || live?.cleanupFailure)
|
|
231
|
+
throw live.cleanupFailure ?? new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '原主机会话正在结束,清理结果尚未确认,不能继续或重开此会话', '管理员核对原主机会话清理结果');
|
|
143
232
|
if (live?.failure)
|
|
144
233
|
throw new NeedError('SESSION_STATE_UNCONFIRMED', '上轮远端状态未确认,不能自动重发或重开,请新建会话并核对项目', '管理员核对原会话状态');
|
|
145
234
|
if (live && !previous)
|
|
146
235
|
throw new NeedError('SESSION_STATE_MISSING', '上一轮回复尚无持久恢复记录,不能继续当前远端会话', '原会话的持久回复记录');
|
|
147
|
-
const configuration = JSON.stringify([mode, config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes]);
|
|
236
|
+
const configuration = JSON.stringify([mode, config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes, config.employeeShellKillGraceMs]);
|
|
148
237
|
if (live && live.configuration !== configuration)
|
|
149
238
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '会话运行设置已改变,请新建会话', '原会话运行设置');
|
|
150
|
-
const request = { 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 }) };
|
|
239
|
+
const 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 }) };
|
|
240
|
+
const after = options.messages.findLastIndex(message => message.role === 'assistant' && !isWorkspaceNoticeMessage(message));
|
|
241
|
+
request.userInputIds = Object.freeze(options.messages.slice(after + 1).flatMap(message => message.role === 'user' && message.source?.kind === 'user' && typeof message.id === 'string' ? [message.id] : []));
|
|
242
|
+
request.userMessageIds = request.userInputIds;
|
|
151
243
|
await this.resolveRuntime.preflight?.(request);
|
|
152
|
-
|
|
244
|
+
let input = await timing.run('input-prepare', () => prepareInput(options, this.readImage));
|
|
153
245
|
const contextSha256 = createHash('sha256').update(input.systemPrompt ?? '').digest('hex');
|
|
246
|
+
request.contextSha256 = contextSha256;
|
|
247
|
+
const codex = this.agent.loginProvider === 'codex';
|
|
248
|
+
if (codex && live && previous && live.contextSha256 !== previous.contextSha256.toLowerCase())
|
|
249
|
+
throw new NeedError('SESSION_CONTEXT_DIGEST_INVALID', '持久上下文摘要与当前远端会话的初建摘要不一致,本轮未发送', '原会话一致的初建上下文摘要');
|
|
250
|
+
const contextChanged = !!previous && codex && previous.contextSha256.toLowerCase() !== contextSha256;
|
|
251
|
+
// Only identical durable context can prove a legacy context-bound journal.
|
|
252
|
+
// New Codex transport scopes are independent of the native context digest.
|
|
253
|
+
if (contextChanged)
|
|
254
|
+
request.contextSha256 = undefined;
|
|
154
255
|
if (previous && this.agent.loginProvider === 'claude' && previous.contextSha256 !== contextSha256)
|
|
155
256
|
throw new NeedError('SYSTEM_PROMPT_UPDATE_PENDING', 'Claude原会话的系统或开发者要求已变化;主机仅承诺下次上下文整理后生效,本轮未发送,请新建会话应用新要求', '新建Claude会话以立即应用当前系统与开发者要求');
|
|
257
|
+
if (contextChanged)
|
|
258
|
+
input = await prepareContinuation(options, input, this.readImage);
|
|
156
259
|
if (live) {
|
|
157
|
-
|
|
260
|
+
if (contextChanged)
|
|
261
|
+
live.session.validateNewSessionContext({ systemPrompt: input.systemPrompt });
|
|
262
|
+
else
|
|
263
|
+
live.session.validateContext({ systemPrompt: input.systemPrompt });
|
|
158
264
|
live.session.validatePrompt(input.prompt);
|
|
159
265
|
}
|
|
160
266
|
if (mode === 'one-shot' && (input.systemPrompt || input.prompt.some(block => block.type !== 'text')))
|
|
161
267
|
throw new NeedError('ONESHOT_CONTEXT_REQUIRED', '首版单次模式不支持图片或系统/开发者上下文,请使用会话模式', 'Codex会话模式');
|
|
162
268
|
if (mode === 'session' && Buffer.byteLength(JSON.stringify({ prompt: input.prompt, systemPrompt: input.systemPrompt }), 'utf8') > config.acpMaxMessageBytes)
|
|
163
269
|
throw new NeedError('ACP_MESSAGE_TOO_LARGE', '本轮输入超过已设置的会话消息容量,未截断或发送', '管理员调整acpMaxMessageBytes');
|
|
270
|
+
if (mode === 'one-shot')
|
|
271
|
+
throw new NeedError('MODEL_CONFIGURATION_UNSUPPORTED', '单次模式不支持确认当前模型与推理等级,请改用会话模式', '支持模型配置确认的会话模式');
|
|
272
|
+
request.localAdmissionCheck?.();
|
|
273
|
+
catalog = await this.catalog(signal);
|
|
274
|
+
selected = catalog.models.find(model => model.id === options.model);
|
|
275
|
+
if (!selected)
|
|
276
|
+
throw new ModelConfigurationError('所选模型不在本次可信主机目录中,请重新选择模型');
|
|
277
|
+
if (options.reasoningEffort !== undefined && !selected.reasoning.options.some(option => option.value === options.reasoningEffort))
|
|
278
|
+
throw new ModelConfigurationError('所选推理等级不在本次模型目录中');
|
|
279
|
+
let modelSignal = AbortSignal.any([signal, catalog.signal]);
|
|
280
|
+
options.signal = modelSignal;
|
|
281
|
+
request.signal = modelSignal;
|
|
164
282
|
runtime = await this.resolveRuntime(request);
|
|
165
283
|
signal.throwIfAborted();
|
|
284
|
+
if (runtime.execution?.signal) {
|
|
285
|
+
modelSignal = AbortSignal.any([modelSignal, runtime.execution.signal]);
|
|
286
|
+
options.signal = modelSignal;
|
|
287
|
+
request.signal = modelSignal;
|
|
288
|
+
modelSignal.throwIfAborted();
|
|
289
|
+
}
|
|
290
|
+
catalog.assertCurrent();
|
|
291
|
+
if (runtime.connection && (runtime.connection.instanceId !== catalog.target.instanceId || runtime.connection.epoch !== catalog.target.epoch) || runtime.identity !== `${catalog.target.target.username}@${catalog.target.target.host}:${catalog.target.target.port}`)
|
|
292
|
+
throw new ModelConfigurationError('模型目录与当前会话可信员工主机不一致');
|
|
166
293
|
if (previous && (previous.identity !== runtime.identity || previous.cwd !== runtime.cwd || previous.bindingId && previous.bindingId !== runtime.binding?.bindingId || previous.workspaceId && previous.workspaceId !== runtime.binding?.workspaceId))
|
|
167
294
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '原会话的员工主机或工作区已改变,请新建会话', '原会话的可信目标和工作区');
|
|
295
|
+
if (contextChanged && runtime.recoveredSession) {
|
|
296
|
+
const capabilities = runtime.recoveredSession.capabilities;
|
|
297
|
+
// A clean reattach has sent no ACP input. A known policy refusal must
|
|
298
|
+
// detach it, preserving the original native session and checkpoint.
|
|
299
|
+
if (!capabilities.sessionCapabilities?.systemPrompt || !supportsCodexContextPolicy(capabilities)) {
|
|
300
|
+
preserveRecoveredRuntime = true;
|
|
301
|
+
if (!capabilities.sessionCapabilities?.systemPrompt)
|
|
302
|
+
throw new NeedError('SYSTEM_PROMPT_UNSUPPORTED', '远端未声明系统与开发者上下文能力,不能丢弃这些要求继续', '支持 systemPrompt 的公开 ACP 能力');
|
|
303
|
+
throw new NeedError('SYSTEM_PROMPT_POLICY_UNSUPPORTED', '远端没有明确声明Codex上下文仅新建生效的兼容政策,本轮未发送', 'systemPrompt与精确的aiWorkbenchSystemPrompt新建政策');
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (runtime.restartNotice) {
|
|
307
|
+
const ids = runtime.restartInputIds;
|
|
308
|
+
if (!ids?.length || new Set(ids).size !== ids.length || ids.some(id => input.users.filter(u => u.id === id).length !== 1))
|
|
309
|
+
throw new NeedError('CURRENT_INPUT_UNCONFIRMED', '无法核对本次输入,尚未发送给模型', '官方当前步骤的已接纳用户输入');
|
|
310
|
+
input = { ...input, prompt: ids.flatMap(id => input.users.find(u => u.id === id).prompt) };
|
|
311
|
+
if (contextChanged)
|
|
312
|
+
input = await timing.run('input-prepare', () => prepareContinuation(options, input, this.readImage));
|
|
313
|
+
}
|
|
168
314
|
const details = runtime.details;
|
|
169
315
|
if (!details)
|
|
170
316
|
throw new NeedError('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。', 'L技术详情持久接收能力');
|
|
171
|
-
|
|
317
|
+
timing.bind(detail => details.record(detail));
|
|
318
|
+
const mapper = new StreamMapper(mode === 'session' && !contextChanged ? (runtime.restartNotice ? undefined : live ? live.usageSnapshot : previous ? (previous.usageSnapshot ?? null) : undefined) : undefined, observation => details.record(observation));
|
|
172
319
|
activeMapper = mapper;
|
|
173
320
|
let id;
|
|
174
321
|
let stop;
|
|
322
|
+
if (runtime.employeePresentation) {
|
|
323
|
+
yield* mapper.text(runtime.employeePresentation + '\n\n');
|
|
324
|
+
yield* mapper.flush();
|
|
325
|
+
}
|
|
175
326
|
if (runtime.workspaceNotice) {
|
|
176
327
|
yield* mapper.text(runtime.workspaceNotice.text);
|
|
177
328
|
yield* mapper.flush();
|
|
178
329
|
}
|
|
330
|
+
if (runtime.restartNotice) {
|
|
331
|
+
yield* mapper.text(runtime.restartNotice + '\n\n');
|
|
332
|
+
yield* mapper.flush();
|
|
333
|
+
}
|
|
334
|
+
if (runtime.execution) {
|
|
335
|
+
details.record({ kind: 'session', severity: 'warning', code: 'CLAUDE_EMPLOYEE_SCOPE', summary: '本机工具路由及未覆盖操作范围', payload: runtime.execution.scope });
|
|
336
|
+
yield* mapper.text(runtime.execution.notice + '\n\n');
|
|
337
|
+
yield* mapper.flush();
|
|
338
|
+
}
|
|
179
339
|
if (mode === 'session') {
|
|
180
340
|
if (live && live.runtime !== runtime)
|
|
181
341
|
throw new NeedError('SESSION_TRANSPORT_MISMATCH', '原会话的主机通道已更换,不能自动切换进程', '原主机会话通道');
|
|
@@ -186,7 +346,22 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
186
346
|
let done = false;
|
|
187
347
|
let failure;
|
|
188
348
|
const push = (chunks) => { queue.push(...chunks); wake?.(); wake = undefined; };
|
|
189
|
-
const hooks = { broker: runtime.broker,
|
|
349
|
+
const hooks = { broker: runtime.broker, employeeExecution: runtime.employeeExecution, timing, approve: async (permission, approvalSignal) => {
|
|
350
|
+
const execution = runtime.execution;
|
|
351
|
+
const toolName = permission.toolCall.name ?? permission.toolCall._meta?.claudeCode?.toolName;
|
|
352
|
+
if (execution) {
|
|
353
|
+
if (toolName && ['Agent', 'Task', 'TaskOutput', 'TaskStop', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash'].includes(toolName)) {
|
|
354
|
+
details.record({ kind: 'need', severity: 'warning', code: 'CLAUDE_HOST_TOOL_UNSUPPORTED', summary: '这类原生调用没有经过员工端别名,本机来源不支持;本次审批已取消', payload: { operation: toolName } });
|
|
355
|
+
push(mapper.text('\n这类操作尚不支持在本机执行,已取消本次审批,没有在主机代跑。\n'));
|
|
356
|
+
return 'cancelled';
|
|
357
|
+
}
|
|
358
|
+
await execution.validate(approvalSignal);
|
|
359
|
+
}
|
|
360
|
+
const answer = await this.approve?.(permission, approvalSignal, options, execution && toolName?.startsWith('mcp__employee__') ? 'employee-local' : undefined) ?? 'unavailable';
|
|
361
|
+
if (execution && answer === 'allowed-once')
|
|
362
|
+
await execution.validate(approvalSignal);
|
|
363
|
+
return answer;
|
|
364
|
+
}, onUpdate: notification => { try {
|
|
190
365
|
push(mapper.acp(notification.update));
|
|
191
366
|
}
|
|
192
367
|
catch (error) {
|
|
@@ -197,7 +372,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
197
372
|
if (fresh) {
|
|
198
373
|
const transport = runtime.acpTransport instanceof acp.AgentApp || runtime.acpTransport instanceof unstable.AgentApp ? runtime.acpTransport : acp.ndJsonStream(runtime.acpTransport.writable, runtime.acpTransport.readable, { maxMessageBytes: config.acpMaxMessageBytes });
|
|
199
374
|
session = new AcpSession(transport, hooks);
|
|
200
|
-
live = { session, runtime, configuration, usageSnapshot: mapper.usageSnapshot };
|
|
375
|
+
live = { session, runtime, configuration, contextSha256, usageSnapshot: mapper.usageSnapshot };
|
|
201
376
|
if (key)
|
|
202
377
|
this.sessions.set(key, live);
|
|
203
378
|
}
|
|
@@ -209,20 +384,67 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
209
384
|
}
|
|
210
385
|
const task = (async () => {
|
|
211
386
|
remoteAttempted = true;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
387
|
+
try {
|
|
388
|
+
if (fresh && runtime.recoveredSession) {
|
|
389
|
+
if (previous && runtime.recoveredSession.sessionId !== previous.sessionId)
|
|
390
|
+
throw new NeedError('SESSION_STATE_INVALID', '已接回的会话与持久标识不一致,本轮未发送', '原会话的恢复标识');
|
|
391
|
+
session.restore(runtime.execution?.hostCwd ?? runtime.cwd, runtime.recoveredSession, { systemPrompt: input.systemPrompt, ...(runtime.execution ? { employeeRouting: runtime.execution.routing } : {}), ...(codex ? { codexNewSessionOnly: true } : {}) }, codex && !!previous && !contextChanged);
|
|
392
|
+
session.validatePrompt(input.prompt);
|
|
393
|
+
if (contextChanged)
|
|
394
|
+
await session.newContextSession({ systemPrompt: input.systemPrompt }, modelSignal);
|
|
395
|
+
}
|
|
396
|
+
else if (fresh)
|
|
397
|
+
await session.start(runtime.execution?.hostCwd ?? runtime.cwd, contextChanged || runtime.restartNotice ? undefined : previous?.sessionId, modelSignal, { systemPrompt: contextChanged ? (input.systemPrompt ?? '') : input.systemPrompt, ...(runtime.execution ? { employeeRouting: runtime.execution.routing } : {}), ...(codex ? { codexNewSessionOnly: true } : {}), ...(runtime.employeeExecutionMeta ? { employeeExecution: runtime.employeeExecutionMeta } : {}) }, input.prompt);
|
|
398
|
+
else if (contextChanged)
|
|
399
|
+
await session.newContextSession({ systemPrompt: input.systemPrompt }, modelSignal);
|
|
400
|
+
else {
|
|
401
|
+
const updated = await session.updateContext({ systemPrompt: input.systemPrompt }, modelSignal);
|
|
402
|
+
if (updated && this.agent.loginProvider === 'claude')
|
|
403
|
+
throw new NeedError('SYSTEM_PROMPT_UPDATE_PENDING', 'Claude原会话的系统或开发者要求已变化;主机仅承诺下次上下文整理后生效,本轮未发送,请新建会话应用新要求', '新建Claude会话以立即应用当前系统与开发者要求');
|
|
404
|
+
}
|
|
405
|
+
if (contextChanged) {
|
|
406
|
+
if (session.id === previous.sessionId)
|
|
407
|
+
throw new NeedError('SESSION_STATE_INVALID', '新会话标识与原会话相同,无法确认独立承接,本轮未发送', '独立的新远端会话标识');
|
|
408
|
+
live.contextSha256 = contextSha256;
|
|
409
|
+
live.usageSnapshot = mapper.usageSnapshot;
|
|
410
|
+
details.record({ kind: 'session', severity: 'info', code: 'CODEX_CONTEXT_SESSION_REPLACED', summary: 'Codex要求仅在新建生效,已保留原会话;新会话承接可见用户及助手正文,内部推理不转发', payload: { reason: 'system-context-changed', oldSessionId: previous.sessionId, newSessionId: session.id, oldContextSha256: previous.contextSha256, newContextSha256: contextSha256, history: input.historyTransfer } });
|
|
411
|
+
}
|
|
412
|
+
catalog.assertCurrent();
|
|
413
|
+
const actual = await session.selectModel(selected.value, options.reasoningEffort, modelSignal);
|
|
414
|
+
catalog.assertCurrent();
|
|
415
|
+
session.assertModelSelection(selected.value, options.reasoningEffort);
|
|
416
|
+
let resolved = selected.resolvedModel;
|
|
417
|
+
if (this.agent.loginProvider === 'claude') {
|
|
418
|
+
const evidence = session.resolvedModelEvidence;
|
|
419
|
+
if (!resolved || (selected.value !== resolved && (!evidence || evidence.modelValue !== selected.value || evidence.resolvedModel !== resolved)))
|
|
420
|
+
throw new NeedError('MODEL_RESOLUTION_UNCONFIRMED', '所选模型的具体型号未获得当前会话确认,本轮未发送', '当前会话的具体型号证据');
|
|
421
|
+
if (evidence && evidence.resolvedModel !== resolved)
|
|
422
|
+
throw new NeedError('MODEL_RESOLUTION_UNCONFIRMED', '当前会话具体型号与所选目录不一致,本轮未发送', '完全一致的具体型号证据');
|
|
423
|
+
}
|
|
424
|
+
if (!resolved)
|
|
425
|
+
throw new NeedError('MODEL_RESOLUTION_UNCONFIRMED', '远端没有提供具体型号证据,本轮未发送', '具体型号证据');
|
|
426
|
+
modelConfiguration = { selected: { ...selected, ...(options.reasoningEffort === undefined ? {} : { effort: options.reasoningEffort }) }, configured: actual, resolvedModel: resolved, status: 'remote-configured' };
|
|
427
|
+
details.record({ kind: 'session', severity: 'info', code: 'MODEL_CONFIGURATION_CONFIRMED', summary: '本轮选择已由远端配置回执确认;不代表推理效果已验证', payload: modelConfiguration });
|
|
218
428
|
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
details.record({ kind: 'session', severity: 'action-required', code: 'MODEL_CONFIGURATION_FAILED', summary: '本轮模型配置未获完整确认,未发送用户输入', payload: { selected, requestedEffort: options.reasoningEffort ?? null, actual: session.modelConfiguration ?? null, reason: error instanceof NeedError ? error.code : error instanceof ModelConfigurationError ? error.message : 'MODEL_CONFIGURATION_FAILED' } });
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
await timing.run('details-flush', () => details.flush());
|
|
219
434
|
await details.flush();
|
|
220
435
|
details.beginTurn(runtime.detailContext?.() ?? { turn: null, step: null });
|
|
436
|
+
catalog.assertCurrent();
|
|
437
|
+
session.assertModelSelection(selected.value, options.reasoningEffort);
|
|
438
|
+
await runtime.execution?.beginTurn(modelSignal);
|
|
221
439
|
id = session.id;
|
|
222
440
|
try {
|
|
223
|
-
|
|
441
|
+
const result = await session.prompt(input.prompt, modelSignal);
|
|
442
|
+
if (contextChanged)
|
|
443
|
+
push(mapper.text(runtime.restartNotice ? '\n\n会话要求已变化,已把此前可见对话带入新 Codex 会话继续处理。' : '\n\n会话要求已变化,已新建 Codex 会话继续处理,原会话仍保留。'));
|
|
444
|
+
return result;
|
|
224
445
|
}
|
|
225
446
|
finally {
|
|
447
|
+
runtime.execution?.endTurn();
|
|
226
448
|
details.endTurn();
|
|
227
449
|
}
|
|
228
450
|
})();
|
|
@@ -254,13 +476,28 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
254
476
|
}
|
|
255
477
|
}
|
|
256
478
|
details.endTurn();
|
|
257
|
-
await details.flush();
|
|
479
|
+
await timing.run('details-flush', () => details.flush());
|
|
480
|
+
await details.flush(); // The timing record is a new ordered write, not a retry.
|
|
258
481
|
if (!id)
|
|
259
482
|
throw new NeedError('SESSION_ID_MISSING', '远端没有返回会话标识', '有效的远端会话标识');
|
|
260
483
|
if (live)
|
|
261
484
|
live.usageSnapshot = mapper.usageSnapshot;
|
|
485
|
+
if (modelConfiguration) {
|
|
486
|
+
const actual = session.modelConfiguration;
|
|
487
|
+
if (!actual?.confirmed || actual.model !== modelConfiguration.configured.model || actual.effort !== modelConfiguration.configured.effort)
|
|
488
|
+
throw new ModelConfigurationError('远端会话在本轮期间改变了模型配置,不能沿用旧回执');
|
|
489
|
+
catalog.assertCurrent();
|
|
490
|
+
yield* mapper.text(`\n\n本轮所选模型:${modelConfiguration.selected.name}(${modelConfiguration.selected.value});推理等级:${modelConfiguration.selected.effort ?? '沿用远端配置'}。远端已配置模型:${modelConfiguration.configured.model};具体型号:${modelConfiguration.resolvedModel};推理等级:${modelConfiguration.configured.effort ?? '远端未提供'}(配置已确认,推理效果未验证)。`);
|
|
491
|
+
}
|
|
262
492
|
yield* mapper.flush();
|
|
263
|
-
|
|
493
|
+
let recoveryCheckpointId;
|
|
494
|
+
if (session?.checkpoint && runtime.saveCheckpoint) {
|
|
495
|
+
const id = randomUUID();
|
|
496
|
+
if (await runtime.saveCheckpoint(id, session.checkpoint))
|
|
497
|
+
recoveryCheckpointId = id;
|
|
498
|
+
}
|
|
499
|
+
const previousRemoteSessions = [...(previous?.previousRemoteSessions ?? []), ...(contextChanged ? [{ sessionId: previous.sessionId, contextSha256: previous.contextSha256 }] : [])];
|
|
500
|
+
const pointer = { ...(previousRemoteSessions.length ? { previousRemoteSessions } : {}), ...(modelConfiguration ? { modelConfiguration } : {}), ...(recoveryCheckpointId ? { recoveryCheckpointId } : {}), ...(runtime.workspaceNotice ? { presentation: { workspaceNotice: runtime.workspaceNotice } } : {}), version: 2, agent: this.agent.id, contextSha256, ...(mode === 'session' ? { usageSnapshot: mapper.usageSnapshot } : {}), ...(options.sessionId ? { dshSessionId: options.sessionId } : {}), mode, identity: runtime.identity, cwd: runtime.cwd, sessionId: id, ...(runtime.binding ? { bindingId: runtime.binding.bindingId, workspaceId: runtime.binding.workspaceId } : {}) };
|
|
264
501
|
succeeded = true;
|
|
265
502
|
yield { type: 'finish', reason: { kind: stop === 'max_tokens' || stop === 'max_turn_requests' ? 'max-tokens' : 'stop' }, replayState: { response: pointer } };
|
|
266
503
|
}
|
|
@@ -281,27 +518,44 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
281
518
|
throw error;
|
|
282
519
|
}
|
|
283
520
|
finally {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
521
|
+
runtime?.execution?.endTurn();
|
|
522
|
+
catalog?.release();
|
|
523
|
+
try {
|
|
524
|
+
if (preserveRecoveredRuntime || this.shuttingDown || key && this.detached.has(key)) {
|
|
525
|
+
session?.close();
|
|
526
|
+
await runtime?.detach?.();
|
|
527
|
+
}
|
|
528
|
+
else if (!succeeded || !key || mode !== 'session') {
|
|
529
|
+
session?.close();
|
|
530
|
+
if (runtime)
|
|
531
|
+
await this.cleanupRuntime(runtime);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
finally {
|
|
535
|
+
if (key && this.turns.get(key) === turn)
|
|
536
|
+
this.turns.delete(key);
|
|
537
|
+
if (key)
|
|
538
|
+
this.detached.delete(key);
|
|
287
539
|
}
|
|
288
|
-
if (key && this.turns.get(key) === turn)
|
|
289
|
-
this.turns.delete(key);
|
|
290
540
|
}
|
|
291
541
|
}
|
|
292
542
|
}
|
|
293
543
|
/** One official registration owns both routes, preserving opaque replay ownership. */
|
|
294
544
|
export class RemoteAgentsAdapter extends LlmAdapter {
|
|
295
545
|
adapters = new Map();
|
|
296
|
-
constructor(config, makeRuntime, approve, nameFor, readImage, reportLoginFailure) { super(); for (const agent of AGENTS)
|
|
297
|
-
this.adapters.set(agent.id, new CodexRemoteAdapter(config, makeRuntime(agent), approve, () => nameFor(agent), agent, readImage, reportLoginFailure)); }
|
|
546
|
+
constructor(config, makeRuntime, approve, nameFor, readImage, reportLoginFailure, directory) { super(); for (const agent of AGENTS)
|
|
547
|
+
this.adapters.set(agent.id, new CodexRemoteAdapter(config, makeRuntime(agent), approve, () => nameFor(agent), agent, readImage, reportLoginFailure, directory)); }
|
|
298
548
|
adapter(provider) { const adapter = this.adapters.get(provider); if (!adapter)
|
|
299
549
|
throw new LlmError('远程模型未注册', 'PROVIDER_NOT_FOUND'); return adapter; }
|
|
300
550
|
providerInfo(provider) { return this.adapter(provider).providerInfo(provider); }
|
|
301
551
|
listModels(provider) { return this.adapter(provider).listModels(provider); }
|
|
552
|
+
resolveModel(provider, model, signal) { return this.adapter(provider).resolveModel(provider, model, signal); }
|
|
553
|
+
async invalidateModelState() { await Promise.all([...this.adapters.values()].map(adapter => adapter.invalidateModelState())); }
|
|
554
|
+
async invalidateEmployeeState() { await Promise.all([...this.adapters.values()].map(adapter => adapter.invalidateEmployeeState())); }
|
|
302
555
|
providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
|
|
303
556
|
stream(options) { return this.adapter(options.provider).stream(options); }
|
|
304
557
|
async disposeSession(sessionId) { await Promise.all([...this.adapters.values()].map(adapter => adapter.disposeSession(sessionId))); }
|
|
558
|
+
async detachSession(sessionId) { await Promise.all([...this.adapters.values()].map(adapter => adapter.detachSession(sessionId))); }
|
|
305
559
|
async dispose() { await Promise.all([...this.adapters.values()].map(adapter => adapter.dispose())); }
|
|
306
560
|
}
|
|
307
561
|
export function apply(ctx, live) {
|
|
@@ -315,6 +569,7 @@ export function apply(ctx, live) {
|
|
|
315
569
|
return attachments.readImage(ref, signal);
|
|
316
570
|
};
|
|
317
571
|
const openByAgent = new Map(AGENTS.map(agent => [agent.id, brokerFactory(get, undefined, config)]));
|
|
572
|
+
const localTurns = createLocalUserTurnObserver(ctx);
|
|
318
573
|
const detailManagers = new Map();
|
|
319
574
|
const managerForProfile = () => {
|
|
320
575
|
const profile = ctx.get('profileContext');
|
|
@@ -327,9 +582,19 @@ export function apply(ctx, live) {
|
|
|
327
582
|
}
|
|
328
583
|
return manager;
|
|
329
584
|
};
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
585
|
+
let changed = () => { };
|
|
586
|
+
const modelDirectory = new ModelCatalog(get, () => config().acpMaxMessageBytes, undefined, () => changed());
|
|
587
|
+
const adapter = new RemoteAgentsAdapter(config, agent => {
|
|
588
|
+
const details = async (request, api, connection, target) => {
|
|
589
|
+
return managerForProfile().open({ sessionId: request.sessionId, provider: agent.loginProvider, identity: `${target.username}@${target.host}:${target.port}`, connection, localAcceptanceId: request.localAcceptanceId ?? request.localAcceptance?.acceptanceId });
|
|
590
|
+
};
|
|
591
|
+
const open = async (...args) => {
|
|
592
|
+
const profile = ctx.get('profileContext');
|
|
593
|
+
if (!profile?.dir)
|
|
594
|
+
throw new NeedError('RECOVERY_STORAGE_REQUIRED', '会话恢复需要官方profile目录', '官方profileContext.dir');
|
|
595
|
+
args[2].recoveryJournal = recoveryJournal(profile.dir, runtimeRecoveryKey(`${args[0].username}@${args[0].host}:${args[0].port}`, agent.id, args[2].sessionId, args[2].employeeScope));
|
|
596
|
+
args[2].verifyRecovery = () => verifyRecoveryCheckpoint(ctx, args[2], agent.id);
|
|
597
|
+
args[2].currentInputIds = await currentInputIds(ctx, args[2]);
|
|
333
598
|
const runtime = await openByAgent.get(agent.id)(...args);
|
|
334
599
|
runtime.detailContext = () => {
|
|
335
600
|
const events = args[2].sessionId ? ctx.get('agents')?.get(SessionId(args[2].sessionId))?.session.snapshotEvents() : undefined;
|
|
@@ -351,7 +616,19 @@ export function apply(ctx, live) {
|
|
|
351
616
|
return { turn, step };
|
|
352
617
|
};
|
|
353
618
|
return runtime;
|
|
354
|
-
}
|
|
619
|
+
};
|
|
620
|
+
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) => managerForProfile().recordLocalFailure({ sessionId: request.sessionId, provider: 'claude', code }), recordUnscoped: (request, observation) => managerForProfile().recordLocalFailure({ sessionId: request.sessionId, provider: 'claude', code: observation.code, observation }), configuration: config, details, open }) : undefined;
|
|
621
|
+
return connectProvider(get, { agent, ...(agent.loginProvider === 'codex' ? { employee: {
|
|
622
|
+
currentUserTurn: request => localTurns.current(request),
|
|
623
|
+
getNetwork: () => ctx.get('aiWorkbenchNetwork'),
|
|
624
|
+
workspace: async (request) => {
|
|
625
|
+
const services = ctx;
|
|
626
|
+
const registry = services.get('workspaceRegistry');
|
|
627
|
+
const sessions = services.get('sessions');
|
|
628
|
+
return officialEmployeeWorkspace(request.sessionId, registry, sessions?.get(request.sessionId)?.header);
|
|
629
|
+
}, native: () => resolveEmployeeNative(),
|
|
630
|
+
} } : {}), notice: createWorkspaceNoticeConsumer(ctx, agent), details, open, local });
|
|
631
|
+
}, dshApproval(ctx), agent => agent.name[language], readImage, (sessionId, code, diagnostic) => {
|
|
355
632
|
const events = sessionId ? ctx.get('agents')?.get(SessionId(sessionId))?.session.snapshotEvents() : undefined;
|
|
356
633
|
let turn = null, step = null;
|
|
357
634
|
for (const event of events ?? []) {
|
|
@@ -369,17 +646,31 @@ export function apply(ctx, live) {
|
|
|
369
646
|
step = null;
|
|
370
647
|
}
|
|
371
648
|
return managerForProfile().recordLoginFailure({ sessionId, code, diagnostic, context: { turn, step } });
|
|
372
|
-
});
|
|
649
|
+
}, modelDirectory);
|
|
373
650
|
const ids = AGENTS.map(agent => agent.id);
|
|
374
651
|
const entries = () => AGENTS.map(agent => ({ provider: agent.id, displayName: agent.name[language], settingsNs: settingsNamespace, settingsPath: [] }));
|
|
375
652
|
const directory = ctx.llm.registerConfigurableProviders(entries());
|
|
376
653
|
const registration = ctx.llm.registerAdapter(ids, adapter);
|
|
654
|
+
changed = () => { registration.replace(ids); void adapter.invalidateModelState().catch(() => { ctx.logger('remote-agent').warn('原会话清理结果未确认,已阻断继续发送;请联系管理员核对。'); }); };
|
|
655
|
+
const serviceValue = (name) => ctx.get(name);
|
|
656
|
+
let employeeConnect = serviceValue('aiWorkbenchConnect'), employeeNetwork = serviceValue('aiWorkbenchNetwork');
|
|
657
|
+
ctx.on('internal/service', service => {
|
|
658
|
+
if (service === 'aiWorkbenchConnect')
|
|
659
|
+
modelDirectory.refresh();
|
|
660
|
+
if (service !== 'aiWorkbenchConnect' && service !== 'aiWorkbenchNetwork')
|
|
661
|
+
return;
|
|
662
|
+
const nextConnect = serviceValue('aiWorkbenchConnect'), nextNetwork = serviceValue('aiWorkbenchNetwork');
|
|
663
|
+
if (nextConnect === employeeConnect && nextNetwork === employeeNetwork)
|
|
664
|
+
return;
|
|
665
|
+
employeeConnect = nextConnect;
|
|
666
|
+
employeeNetwork = nextNetwork;
|
|
667
|
+
void adapter.invalidateEmployeeState().catch(() => { ctx.logger('remote-agent').warn('本机执行连接的清理结果未确认,已阻断继续发送;请联系管理员核对。'); });
|
|
668
|
+
});
|
|
377
669
|
new ProviderLocaleRemote(ctx, next => { language = next; directory.replace(entries()); registration.replace(ids); });
|
|
378
|
-
const cleanup = (id) => adapter.
|
|
670
|
+
const cleanup = (id) => adapter.detachSession(id);
|
|
379
671
|
ctx.on('agent/disposed', ({ agent }) => cleanup(agent.session.id));
|
|
380
672
|
ctx.on('session/disposed', session => cleanup(session.id));
|
|
381
|
-
ctx.
|
|
382
|
-
ctx.effect(() => () => adapter.dispose());
|
|
673
|
+
ctx.effect(() => () => { modelDirectory.dispose(); return adapter.dispose(); });
|
|
383
674
|
}
|
|
384
675
|
export function dshApproval(ctx) {
|
|
385
676
|
return async (request, signal, options, context) => {
|
|
@@ -387,7 +678,9 @@ export function dshApproval(ctx) {
|
|
|
387
678
|
const approval = ctx.get('approval');
|
|
388
679
|
if (!agent || !approval)
|
|
389
680
|
return 'unavailable';
|
|
390
|
-
|
|
391
|
-
|
|
681
|
+
const label = approvalLabel(request, context === 'employee-local' ? 'local' : 'host');
|
|
682
|
+
if (!label)
|
|
683
|
+
return 'unavailable';
|
|
684
|
+
return approval.request({ agent, ...label, signal });
|
|
392
685
|
};
|
|
393
686
|
}
|