@ai-workbench/remote-agent 0.8.12 → 0.8.14
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 +11 -7
- package/dist/acp.d.ts +12 -7
- package/dist/acp.js +65 -41
- package/dist/broker-runtime.js +46 -20
- package/dist/broker.d.ts +4 -2
- package/dist/broker.js +18 -9
- package/dist/claude-local.d.ts +2 -1
- package/dist/claude-local.js +61 -34
- package/dist/close-diagnostics.d.ts +56 -0
- package/dist/close-diagnostics.js +104 -0
- package/dist/conversation-details.js +67 -9
- package/dist/employee-endpoint.d.ts +3 -1
- package/dist/employee-endpoint.js +8 -6
- package/dist/employee-exec.d.ts +7 -4
- package/dist/employee-exec.js +39 -29
- package/dist/employee-mcp.d.ts +6 -3
- package/dist/employee-mcp.js +75 -39
- package/dist/employee-provider.js +58 -36
- package/dist/employee-shell.d.ts +2 -0
- package/dist/employee-shell.js +17 -5
- package/dist/employee-ssh.d.ts +2 -0
- package/dist/employee-ssh.js +6 -4
- package/dist/employee-tools.d.ts +3 -1
- package/dist/employee-tools.js +6 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.js +133 -44
- package/dist/local-user-turn.js +2 -1
- package/dist/model-catalog.d.ts +9 -1
- package/dist/model-catalog.js +63 -5
- package/dist/model-configuration.d.ts +14 -1
- package/dist/model-configuration.js +122 -89
- package/dist/presentation.js +3 -2
- package/dist/provider.d.ts +8 -2
- package/dist/recovery.d.ts +4 -0
- package/dist/rpc-namespace.d.ts +2 -2
- package/dist/rpc-namespace.js +2 -1
- package/dist/session-header.d.ts +4 -0
- package/dist/session-header.js +12 -0
- package/package.json +3 -3
package/dist/model-catalog.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
2
3
|
import { sameTarget, validateTarget } from "./provider.js";
|
|
3
4
|
import { ModelConfigurationError } from "./model-configuration.js";
|
|
@@ -92,6 +93,45 @@ export function parseHostCatalog(input, provider, username) {
|
|
|
92
93
|
}
|
|
93
94
|
return freeze(structuredClone({ models, issues }));
|
|
94
95
|
}
|
|
96
|
+
function changedCatalogFields(previous, next, switched) {
|
|
97
|
+
const before = previous ? JSON.parse(previous) : {}, after = JSON.parse(next), fields = [];
|
|
98
|
+
if (switched)
|
|
99
|
+
fields.push('api-instance');
|
|
100
|
+
for (const [key, label] of [['instanceId', 'connection-instance'], ['epoch', 'connection-epoch'], ['phase', 'connection-phase'], ['needCode', 'connection-need-code']])
|
|
101
|
+
if (JSON.stringify(before.state?.[key]) !== JSON.stringify(after.state?.[key]))
|
|
102
|
+
fields.push(label);
|
|
103
|
+
if (before.capability !== after.capability)
|
|
104
|
+
fields.push('catalog-capability');
|
|
105
|
+
for (const [index, label] of [[0, 'codex-target'], [1, 'claude-target']])
|
|
106
|
+
if (JSON.stringify(before.targets?.[index]) !== JSON.stringify(after.targets?.[index]))
|
|
107
|
+
fields.push(label);
|
|
108
|
+
return fields.length ? fields : ['other-metadata'];
|
|
109
|
+
}
|
|
110
|
+
function catalogChanges(previous, next, switched) {
|
|
111
|
+
const before = previous ? JSON.parse(previous) : {}, after = JSON.parse(next), changes = [];
|
|
112
|
+
const scalar = (value) => typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ? value : null;
|
|
113
|
+
const compare = (field, old, current) => { if (JSON.stringify(old) !== JSON.stringify(current))
|
|
114
|
+
changes.push({ field, before: scalar(old), after: scalar(current) }); };
|
|
115
|
+
const fingerprint = (value) => value === undefined ? null : createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
116
|
+
if (switched)
|
|
117
|
+
changes.push({ field: 'catalog.api-replaced', before: false, after: true });
|
|
118
|
+
for (const [key, name] of [['instanceId', 'instance-id'], ['epoch', 'epoch'], ['phase', 'phase'], ['needCode', 'need-code']])
|
|
119
|
+
compare('connection.' + name, before.state?.[key], after.state?.[key]);
|
|
120
|
+
compare('catalog.capability', before.capability, after.capability);
|
|
121
|
+
for (const [index, provider] of [[0, 'codex'], [1, 'claude']]) {
|
|
122
|
+
const a = before.targets?.[index], b = after.targets?.[index], prefix = 'catalog.' + provider + '.';
|
|
123
|
+
compare(prefix + 'ok', a?.ok, b?.ok);
|
|
124
|
+
compare(prefix + 'error-code', a?.code, b?.code);
|
|
125
|
+
for (const [key, name] of [['instanceId', 'instance-id'], ['epoch', 'epoch'], ['provider', 'provider']])
|
|
126
|
+
compare(prefix + name, a?.value?.[key], b?.value?.[key]);
|
|
127
|
+
// Protect executable argv, trust/config paths and arbitrary future metadata: values are digests.
|
|
128
|
+
compare(prefix + 'target-sha256', fingerprint(a?.value?.target), fingerprint(b?.value?.target));
|
|
129
|
+
compare(prefix + 'metadata-sha256', fingerprint(a), fingerprint(b));
|
|
130
|
+
}
|
|
131
|
+
if (!changes.length)
|
|
132
|
+
changes.push({ field: 'catalog.signature-sha256', before: fingerprint(before), after: fingerprint(after) });
|
|
133
|
+
return changes;
|
|
134
|
+
}
|
|
95
135
|
/** Share only pending work; settled results are never cached. */
|
|
96
136
|
export class ModelCatalog {
|
|
97
137
|
get;
|
|
@@ -100,6 +140,7 @@ export class ModelCatalog {
|
|
|
100
140
|
changed;
|
|
101
141
|
diagnostic;
|
|
102
142
|
loaded;
|
|
143
|
+
revoking;
|
|
103
144
|
api;
|
|
104
145
|
unsubscribe;
|
|
105
146
|
signature;
|
|
@@ -107,16 +148,20 @@ export class ModelCatalog {
|
|
|
107
148
|
disposed = false;
|
|
108
149
|
initialized = false;
|
|
109
150
|
active = new Set();
|
|
151
|
+
presentation;
|
|
152
|
+
presentationNeedCode;
|
|
153
|
+
information = new Set();
|
|
110
154
|
flights = new Map();
|
|
111
155
|
serial = 0;
|
|
112
156
|
latest = new Map();
|
|
113
|
-
constructor(get, maxBytes, run = runCatalog, changed = () => { }, diagnostic = () => { }, loaded = () => { }) {
|
|
157
|
+
constructor(get, maxBytes, run = runCatalog, changed = () => { }, diagnostic = () => { }, loaded = () => { }, revoking = () => { }) {
|
|
114
158
|
this.get = get;
|
|
115
159
|
this.maxBytes = maxBytes;
|
|
116
160
|
this.run = run;
|
|
117
161
|
this.changed = changed;
|
|
118
162
|
this.diagnostic = diagnostic;
|
|
119
163
|
this.loaded = loaded;
|
|
164
|
+
this.revoking = revoking;
|
|
120
165
|
this.refresh();
|
|
121
166
|
}
|
|
122
167
|
refresh() {
|
|
@@ -125,11 +170,19 @@ export class ModelCatalog {
|
|
|
125
170
|
const info = api?.getContractInfo?.();
|
|
126
171
|
const targets = info?.modelCatalogTarget === 1 && api?.getModelCatalogTarget ? ['codex', 'claude'].map(brand => api.getModelCatalogTarget(brand)) : undefined;
|
|
127
172
|
// L's same-epoch recovery wording is presentation metadata, not an execution change.
|
|
128
|
-
const executionState = state && { instanceId: state.instanceId, epoch: state.epoch, phase: state.phase
|
|
173
|
+
const executionState = state && { instanceId: state.instanceId, epoch: state.epoch, phase: state.phase };
|
|
129
174
|
const executionTargets = targets?.map(result => result.ok ? result : { ok: false, code: result.error.code });
|
|
130
175
|
const signature = JSON.stringify({ state: executionState, capability: info?.modelCatalogTarget, targets: executionTargets });
|
|
131
176
|
const switched = api !== this.api;
|
|
132
177
|
const changed = this.initialized && (switched || signature !== this.signature);
|
|
178
|
+
const presentation = createHash('sha256').update(JSON.stringify({ need: { code: state?.need?.code, message: state?.need?.message, requires: state?.need?.requires }, recovery: state?.recovery && { phase: state.recovery.phase, message: state.recovery.message, startedAt: state.recovery.startedAt, administratorAfterMs: state.recovery.administratorAfterMs } })).digest('hex');
|
|
179
|
+
if (this.initialized && !changed && presentation !== this.presentation) {
|
|
180
|
+
const entry = { kind: 'session', severity: 'info', code: 'CATALOG_SCOPE_INFORMATION', summary: '连接说明已变化;主机身份、连接代际和可信目标未变,本轮继续。', payload: { reason: 'CATALOG_PRESENTATION_CHANGED', before: { needCode: this.presentationNeedCode ?? null, presentationSha256: this.presentation ?? null }, after: { needCode: state?.need?.code ?? null, presentationSha256: presentation } } };
|
|
181
|
+
for (const listener of this.information)
|
|
182
|
+
listener(entry);
|
|
183
|
+
}
|
|
184
|
+
this.presentation = presentation;
|
|
185
|
+
this.presentationNeedCode = state?.need?.code;
|
|
133
186
|
this.initialized = true;
|
|
134
187
|
if (switched) {
|
|
135
188
|
this.unsubscribe?.();
|
|
@@ -143,8 +196,10 @@ export class ModelCatalog {
|
|
|
143
196
|
this.unsubscribe = () => { for (const stop of off)
|
|
144
197
|
stop(); };
|
|
145
198
|
}
|
|
199
|
+
const changedFields = changed ? changedCatalogFields(this.signature, signature, switched) : [], changes = changed ? catalogChanges(this.signature, signature, switched) : [];
|
|
146
200
|
this.signature = signature;
|
|
147
201
|
if (changed) {
|
|
202
|
+
this.revoking(changedFields, changes);
|
|
148
203
|
this.revision++;
|
|
149
204
|
this.latest.clear();
|
|
150
205
|
for (const controller of this.active)
|
|
@@ -224,8 +279,10 @@ export class ModelCatalog {
|
|
|
224
279
|
}
|
|
225
280
|
async read(agent, signal) {
|
|
226
281
|
let flight, released = false;
|
|
282
|
+
const listeners = new Set();
|
|
227
283
|
const release = () => { if (released || !flight)
|
|
228
|
-
return; released = true;
|
|
284
|
+
return; released = true; for (const listener of listeners)
|
|
285
|
+
this.information.delete(listener); listeners.clear(); flight.users--; if (!flight.users) {
|
|
229
286
|
if (flight.pending)
|
|
230
287
|
flight.controller.abort();
|
|
231
288
|
else
|
|
@@ -277,7 +334,8 @@ export class ModelCatalog {
|
|
|
277
334
|
abort();
|
|
278
335
|
});
|
|
279
336
|
assertCurrent();
|
|
280
|
-
return { target: own.target, models, signal: combined, assertCurrent, release
|
|
337
|
+
return { target: own.target, models, signal: combined, assertCurrent, release, onInformation: listener => { if (released)
|
|
338
|
+
return () => { }; listeners.add(listener); this.information.add(listener); return () => { listeners.delete(listener); this.information.delete(listener); }; } };
|
|
281
339
|
}
|
|
282
340
|
catch (error) {
|
|
283
341
|
release();
|
|
@@ -288,7 +346,7 @@ export class ModelCatalog {
|
|
|
288
346
|
}
|
|
289
347
|
}
|
|
290
348
|
dispose() { this.disposed = true; this.unsubscribe?.(); this.flights.clear(); for (const controller of this.active)
|
|
291
|
-
controller.abort(); this.active.clear(); }
|
|
349
|
+
controller.abort(); this.active.clear(); this.information.clear(); }
|
|
292
350
|
}
|
|
293
351
|
export function modelInfo(provider, model) {
|
|
294
352
|
// Official rc.2 represents no reasoning levels by absence, and rejects an empty efforts array.
|
|
@@ -31,17 +31,29 @@ export declare class ModelConfigurationError extends Error {
|
|
|
31
31
|
readonly code = "MODEL_CATALOG_UNAVAILABLE";
|
|
32
32
|
constructor(reason: string, options?: ErrorOptions);
|
|
33
33
|
}
|
|
34
|
+
export type ConfigurationInformation = 'PICKER_INCOMPLETE' | 'CONFIG_UI_METADATA_INVALID' | 'UNRELATED_CONFIG_IGNORED' | 'OPTIONAL_CATEGORY_OMITTED' | 'SAME_SELECTION_UPDATE';
|
|
35
|
+
type Inform = (reason: ConfigurationInformation) => void;
|
|
36
|
+
type Roles = {
|
|
37
|
+
model?: string;
|
|
38
|
+
effort?: string;
|
|
39
|
+
};
|
|
34
40
|
/** Pure ACP configuration state. All IDs and values come from the given peer data. */
|
|
35
41
|
export declare class ModelConfiguration {
|
|
36
42
|
private readonly rpcSet;
|
|
43
|
+
private readonly inform;
|
|
37
44
|
private state;
|
|
38
45
|
private parsed?;
|
|
39
46
|
private revision;
|
|
40
47
|
private busy;
|
|
41
|
-
|
|
48
|
+
private known;
|
|
49
|
+
constructor(options: SessionConfigOption[], rpcSet: ModelConfigurationSetter, inform?: Inform, routing?: Roles);
|
|
42
50
|
get models(): readonly ModelCatalogValue[];
|
|
43
51
|
get snapshot(): ModelConfigurationSnapshot;
|
|
52
|
+
private prepare;
|
|
53
|
+
private apply;
|
|
44
54
|
update(options: SessionConfigOption[]): void;
|
|
55
|
+
/** A different ACP session cannot inherit selector-role discovery or ACKs. */
|
|
56
|
+
invalidate(clearRoles?: boolean): void;
|
|
45
57
|
private fail;
|
|
46
58
|
private checkAckRevision;
|
|
47
59
|
private set;
|
|
@@ -49,3 +61,4 @@ export declare class ModelConfiguration {
|
|
|
49
61
|
/** Call immediately before prompt; an idle/new/load/resume update withdraws the ACK. */
|
|
50
62
|
assertSelection(model: string, effort?: string): void;
|
|
51
63
|
}
|
|
64
|
+
export {};
|
|
@@ -6,86 +6,103 @@ export class ModelConfigurationError extends Error {
|
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
8
|
function unavailable(reason) { throw new ModelConfigurationError(reason); }
|
|
9
|
-
function record(value) {
|
|
10
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
11
|
-
}
|
|
9
|
+
function record(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
|
|
12
10
|
function text(value) { return typeof value === 'string' && value.trim().length > 0; }
|
|
13
|
-
function label(value) {
|
|
14
|
-
if (!text(value.name))
|
|
15
|
-
|
|
16
|
-
if (value.description !== undefined && value.description !== null && typeof value.description !== 'string')
|
|
17
|
-
unavailable('配置说明无效');
|
|
11
|
+
function label(value, inform) {
|
|
12
|
+
if (!text(value.name) || value.description !== undefined && value.description !== null && typeof value.description !== 'string')
|
|
13
|
+
inform('CONFIG_UI_METADATA_INVALID');
|
|
18
14
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const seen = new Set();
|
|
15
|
+
/** Picker entries are presentation, not an exhaustive list of legal setter values. */
|
|
16
|
+
function select(option, inform) {
|
|
17
|
+
if (option.type !== 'select' || !text(option.currentValue))
|
|
18
|
+
unavailable('模型或推理配置没有有效的选择器与实际当前值');
|
|
19
|
+
const values = [], seen = new Set(), groups = new Set();
|
|
20
|
+
let flat = false, grouped = false;
|
|
24
21
|
const add = (value) => {
|
|
25
|
-
if (!record(value) || !text(value.value)
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
if (!record(value) || !text(value.value)) {
|
|
23
|
+
inform('CONFIG_UI_METADATA_INVALID');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
label(value, inform);
|
|
28
27
|
if (seen.has(value.value))
|
|
29
|
-
|
|
28
|
+
inform('CONFIG_UI_METADATA_INVALID');
|
|
30
29
|
seen.add(value.value);
|
|
31
|
-
values.push({ value: value.value, name: value.name, ...(value.description ===
|
|
30
|
+
values.push({ value: value.value, name: typeof value.name === 'string' ? value.name : '', ...(typeof value.description === 'string' || value.description === null ? { description: value.description } : {}) });
|
|
32
31
|
};
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
32
|
+
if (!Array.isArray(option.options) || !option.options.length)
|
|
33
|
+
inform('PICKER_INCOMPLETE');
|
|
34
|
+
else
|
|
35
|
+
for (const entry of option.options) {
|
|
36
|
+
if (record(entry) && 'group' in entry) {
|
|
37
|
+
grouped = true;
|
|
38
|
+
if (flat)
|
|
39
|
+
inform('CONFIG_UI_METADATA_INVALID');
|
|
40
|
+
if (typeof entry.group !== 'string' || groups.has(entry.group))
|
|
41
|
+
inform('CONFIG_UI_METADATA_INVALID');
|
|
42
|
+
if (typeof entry.group === 'string')
|
|
43
|
+
groups.add(entry.group);
|
|
44
|
+
label(entry, inform);
|
|
45
|
+
if (!Array.isArray(entry.options)) {
|
|
46
|
+
inform('CONFIG_UI_METADATA_INVALID');
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (!entry.options.length)
|
|
50
|
+
inform('PICKER_INCOMPLETE');
|
|
51
|
+
for (const value of entry.options)
|
|
52
|
+
add(value);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
flat = true;
|
|
56
|
+
if (grouped)
|
|
57
|
+
inform('CONFIG_UI_METADATA_INVALID');
|
|
58
|
+
add(entry);
|
|
59
|
+
}
|
|
45
60
|
}
|
|
46
|
-
else
|
|
47
|
-
add(entry);
|
|
48
|
-
}
|
|
49
61
|
if (!seen.has(option.currentValue))
|
|
50
|
-
|
|
62
|
+
inform('PICKER_INCOMPLETE');
|
|
51
63
|
return { id: option.id, currentValue: option.currentValue, values };
|
|
52
64
|
}
|
|
53
|
-
function parse(options) {
|
|
65
|
+
function parse(options, known, inform) {
|
|
54
66
|
if (!Array.isArray(options) || !options.length)
|
|
55
|
-
unavailable('
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
models.push(selector);
|
|
72
|
-
if (option.category === 'thought_level')
|
|
73
|
-
efforts.push(selector);
|
|
67
|
+
unavailable('远端没有提供可核对的模型配置入口');
|
|
68
|
+
const entries = options.filter(record), counts = new Map();
|
|
69
|
+
for (const option of entries) {
|
|
70
|
+
label(option, inform);
|
|
71
|
+
if (text(option.id))
|
|
72
|
+
counts.set(option.id, (counts.get(option.id) ?? 0) + 1);
|
|
73
|
+
}
|
|
74
|
+
const role = (category, id, required) => {
|
|
75
|
+
let candidates = entries.filter(option => option.category === category);
|
|
76
|
+
const known = id ? entries.filter(option => option.id === id) : [];
|
|
77
|
+
if (known.length) {
|
|
78
|
+
if (candidates.some(option => option.id !== id))
|
|
79
|
+
unavailable('同一会话的模型或推理配置入口存在冲突,不能改用另一个入口的值作回执');
|
|
80
|
+
candidates = known;
|
|
81
|
+
if (known[0].category !== category)
|
|
82
|
+
inform('OPTIONAL_CATEGORY_OMITTED');
|
|
74
83
|
}
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
unavailable('模型或推理等级配置不是选择器');
|
|
84
|
+
if (candidates.length !== 1) {
|
|
85
|
+
if (!required && !candidates.length)
|
|
86
|
+
return;
|
|
87
|
+
unavailable(candidates.length ? '模型或推理配置入口不唯一' : '没有可靠的模型配置入口,不能猜测配置标识');
|
|
80
88
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
const option = candidates[0];
|
|
90
|
+
if (!text(option.id) || (counts.get(option.id) ?? 0) !== 1)
|
|
91
|
+
unavailable('模型或推理配置标识无效或重复');
|
|
92
|
+
return select(option, inform);
|
|
93
|
+
};
|
|
94
|
+
const model = role('model', known.model, true), effort = role('thought_level', known.effort, false);
|
|
95
|
+
if (model.id === effort?.id)
|
|
96
|
+
unavailable('模型和推理等级不能共用同一个配置入口');
|
|
97
|
+
for (const option of options)
|
|
98
|
+
if (!record(option) || option.id !== model.id && option.id !== effort?.id) {
|
|
99
|
+
if (!record(option) || !text(option.id) || !['select', 'boolean'].includes(option.type))
|
|
100
|
+
inform('UNRELATED_CONFIG_IGNORED');
|
|
101
|
+
}
|
|
102
|
+
return { model, effort };
|
|
103
|
+
}
|
|
104
|
+
function sameSelection(a, b) {
|
|
105
|
+
return !!a && !!b && a.model.id === b.model.id && a.model.currentValue === b.model.currentValue && a.effort?.id === b.effort?.id && a.effort?.currentValue === b.effort?.currentValue;
|
|
89
106
|
}
|
|
90
107
|
function freeze(value, seen = new WeakSet()) {
|
|
91
108
|
if (value !== null && typeof value === 'object' && !seen.has(value)) {
|
|
@@ -99,12 +116,17 @@ function freeze(value, seen = new WeakSet()) {
|
|
|
99
116
|
/** Pure ACP configuration state. All IDs and values come from the given peer data. */
|
|
100
117
|
export class ModelConfiguration {
|
|
101
118
|
rpcSet;
|
|
119
|
+
inform;
|
|
102
120
|
state;
|
|
103
121
|
parsed;
|
|
104
122
|
revision = 0;
|
|
105
123
|
busy = false;
|
|
106
|
-
|
|
124
|
+
known = {};
|
|
125
|
+
constructor(options, rpcSet, inform = () => { }, routing) {
|
|
107
126
|
this.rpcSet = rpcSet;
|
|
127
|
+
this.inform = inform;
|
|
128
|
+
if (routing)
|
|
129
|
+
this.known = { ...routing };
|
|
108
130
|
this.update(options);
|
|
109
131
|
}
|
|
110
132
|
get models() {
|
|
@@ -113,30 +135,36 @@ export class ModelConfiguration {
|
|
|
113
135
|
return this.state.models;
|
|
114
136
|
}
|
|
115
137
|
get snapshot() { return this.state; }
|
|
138
|
+
prepare(options) {
|
|
139
|
+
const copy = structuredClone(options), parsed = parse(copy, this.known, this.inform);
|
|
140
|
+
return { copy: copy, parsed };
|
|
141
|
+
}
|
|
142
|
+
apply({ copy, parsed }) {
|
|
143
|
+
const same = sameSelection(this.parsed, parsed), confirmed = same && this.state?.confirmed === true;
|
|
144
|
+
if (!same)
|
|
145
|
+
this.revision++;
|
|
146
|
+
else
|
|
147
|
+
this.inform('SAME_SELECTION_UPDATE');
|
|
148
|
+
this.parsed = parsed;
|
|
149
|
+
this.known = { model: parsed.model.id, ...parsed.effort ? { effort: parsed.effort.id } : {} };
|
|
150
|
+
this.state = freeze({ configOptions: copy, models: parsed.model.values, modelConfigId: parsed.model.id, model: parsed.model.currentValue,
|
|
151
|
+
...(parsed.effort ? { effortConfigId: parsed.effort.id, effort: parsed.effort.currentValue } : {}), effortAvailable: !!parsed.effort, confirmed });
|
|
152
|
+
}
|
|
116
153
|
update(options) {
|
|
117
|
-
this.revision++;
|
|
118
|
-
let copy = [];
|
|
119
154
|
try {
|
|
120
|
-
|
|
121
|
-
const copied = structuredClone(options);
|
|
122
|
-
if (Array.isArray(copied))
|
|
123
|
-
copy = copied;
|
|
124
|
-
const parsed = parse(copied);
|
|
125
|
-
this.parsed = parsed;
|
|
126
|
-
this.state = freeze({
|
|
127
|
-
configOptions: copy, models: parsed.model.values,
|
|
128
|
-
modelConfigId: parsed.model.id, model: parsed.model.currentValue,
|
|
129
|
-
...(parsed.effort ? { effortConfigId: parsed.effort.id, effort: parsed.effort.currentValue } : {}),
|
|
130
|
-
effortAvailable: !!parsed.effort, confirmed: false,
|
|
131
|
-
});
|
|
155
|
+
this.apply(this.prepare(options));
|
|
132
156
|
}
|
|
133
157
|
catch (cause) {
|
|
134
158
|
const error = cause instanceof ModelConfigurationError ? cause : new ModelConfigurationError('配置数据无法复制或解析', { cause });
|
|
159
|
+
this.revision++;
|
|
135
160
|
this.parsed = undefined;
|
|
136
|
-
this.state = freeze({ configOptions:
|
|
161
|
+
this.state = freeze({ configOptions: [], models: [], effortAvailable: false, confirmed: false, failure: { code: error.code, message: error.message } });
|
|
137
162
|
throw error;
|
|
138
163
|
}
|
|
139
164
|
}
|
|
165
|
+
/** A different ACP session cannot inherit selector-role discovery or ACKs. */
|
|
166
|
+
invalidate(clearRoles = true) { if (clearRoles)
|
|
167
|
+
this.known = {}; this.update([]); }
|
|
140
168
|
fail(error) {
|
|
141
169
|
this.state = freeze({ ...this.state, confirmed: false, failure: { code: error.code, message: error.message } });
|
|
142
170
|
}
|
|
@@ -145,7 +173,7 @@ export class ModelConfiguration {
|
|
|
145
173
|
unavailable(`${stage}回执之后收到其他配置更新,原选择未获确认`);
|
|
146
174
|
}
|
|
147
175
|
async set(id, value, stage) {
|
|
148
|
-
const revision = this.revision;
|
|
176
|
+
const revision = this.revision, modelId = this.parsed?.model.id, effortId = this.parsed?.effort?.id;
|
|
149
177
|
let response;
|
|
150
178
|
try {
|
|
151
179
|
response = await this.rpcSet(id, value);
|
|
@@ -153,11 +181,16 @@ export class ModelConfiguration {
|
|
|
153
181
|
catch (cause) {
|
|
154
182
|
throw new ModelConfigurationError(`${stage}配置请求失败,未获得远端确认`, { cause });
|
|
155
183
|
}
|
|
156
|
-
if (revision !== this.revision)
|
|
157
|
-
unavailable(`${stage}配置请求期间收到其他配置更新,无法确认回执顺序`);
|
|
158
184
|
if (!record(response) || !Array.isArray(response.configOptions))
|
|
159
185
|
unavailable(`${stage}配置回执缺少完整配置列表`);
|
|
160
|
-
this.
|
|
186
|
+
const prepared = this.prepare(response.configOptions);
|
|
187
|
+
if (prepared.parsed.model.id !== modelId || stage === '推理等级' && prepared.parsed.effort && prepared.parsed.effort.id !== effortId)
|
|
188
|
+
unavailable('配置回执改用了其他入口,原请求没有得到确认');
|
|
189
|
+
// Notifications can precede their own setter ACK. Only a conflicting real
|
|
190
|
+
// selection, rather than unrelated/same-value UI updates, invalidates it.
|
|
191
|
+
if (revision !== this.revision && !sameSelection(this.parsed, prepared.parsed))
|
|
192
|
+
unavailable(`${stage}配置请求期间收到不同的实际配置,无法确认回执顺序`);
|
|
193
|
+
this.apply(prepared);
|
|
161
194
|
return this.revision;
|
|
162
195
|
}
|
|
163
196
|
async select(model, effort) {
|
|
@@ -169,7 +202,7 @@ export class ModelConfiguration {
|
|
|
169
202
|
if (!this.parsed)
|
|
170
203
|
unavailable('当前远端配置列表无效');
|
|
171
204
|
if (!this.parsed.model.values.some(entry => entry.value === model))
|
|
172
|
-
|
|
205
|
+
this.inform('PICKER_INCOMPLETE');
|
|
173
206
|
let ackRevision = await this.set(this.parsed.model.id, model, '模型');
|
|
174
207
|
this.checkAckRevision(ackRevision, '模型');
|
|
175
208
|
if (this.state.model !== model)
|
|
@@ -179,7 +212,7 @@ export class ModelConfiguration {
|
|
|
179
212
|
if (!selector)
|
|
180
213
|
unavailable('远端没有提供实际推理等级选择器');
|
|
181
214
|
if (!selector.values.some(entry => entry.value === effort))
|
|
182
|
-
|
|
215
|
+
this.inform('PICKER_INCOMPLETE');
|
|
183
216
|
ackRevision = await this.set(selector.id, effort, '推理等级');
|
|
184
217
|
this.checkAckRevision(ackRevision, '推理等级');
|
|
185
218
|
if (this.state.model !== model)
|
package/dist/presentation.js
CHANGED
|
@@ -17,7 +17,7 @@ export function employeeFailure(code, brand = 'codex') {
|
|
|
17
17
|
TECHNICAL_DETAILS_UNAVAILABLE: '技术详情暂不可用,请更新远程主机连接组件。',
|
|
18
18
|
REMOTE_DETAIL_STORAGE_FAILED: '技术详情保存失败,本轮已停止,请联系管理员检查。',
|
|
19
19
|
REMOTE_DETAIL_DELIVERY_UNKNOWN: '技术详情的持久结果无法核对,本轮已停止。',
|
|
20
|
-
REMOTE_DETAIL_SCOPE_INVALID: '
|
|
20
|
+
REMOTE_DETAIL_SCOPE_INVALID: '无法核对本次输入与当前会话的关联,本轮已停止;请联系管理员核对技术详情。',
|
|
21
21
|
EMPLOYEE_CONTEXT_UNAVAILABLE: '我需要远程主机连接组件提供当前本机工作区,本轮未执行。',
|
|
22
22
|
EMPLOYEE_ENDPOINT_UNAVAILABLE: '我需要员工电脑的执行连接就绪,本轮未执行;请联系管理员检查连接组件。',
|
|
23
23
|
EMPLOYEE_CONTEXT_CHANGED: '本机文件夹或连接已改变,原工具已停止;请在当前工作区重新开始。',
|
|
@@ -61,8 +61,9 @@ export function employeeFailure(code, brand = 'codex') {
|
|
|
61
61
|
ACP_CLIENT_LOST: '我需要管理员核对并结束遗留的主机会话;当前保留原进程,没有代答旧审批。',
|
|
62
62
|
ONESHOT_CONTEXT_REQUIRED: '首版单次模式不支持图片或系统与开发者上下文,请改用会话模式。',
|
|
63
63
|
MODEL_CONFIGURATION_UNSUPPORTED: '单次模式不支持确认当前模型与推理等级,本轮未发送;请改用会话模式。',
|
|
64
|
+
INPUT_DELIVERY_UNCONFIRMED: '上一轮输入的处理结果尚未确认;没有自动重发,你的原话仍保留,请先核对技术详情。',
|
|
64
65
|
MODEL_SELECTION_REQUIRED: '旧远端恢复记录没有明确模型选择,本轮未发送;请新建会话并选择模型。',
|
|
65
|
-
MODEL_RESOLUTION_UNCONFIRMED: '
|
|
66
|
+
MODEL_RESOLUTION_UNCONFIRMED: '主机目录尚未给出所选模型的唯一具体型号,本轮未发送;你的原话仍保留,请联系管理员核对模型目录。',
|
|
66
67
|
REMOTE_CLEANUP_UNCONFIRMED: '原主机会话的清理结果尚未确认,已阻断继续发送;请联系管理员核对,不能自动重开。',
|
|
67
68
|
};
|
|
68
69
|
if (messages[code])
|
package/dist/provider.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CloseCause, CloseDiagnostics } from './close-diagnostics.ts';
|
|
1
2
|
import type { AgentApp } from '@agentclientprotocol/sdk';
|
|
2
3
|
import type { AgentApp as ExtendedAgentApp } from '@agentclientprotocol/sdk/unstable';
|
|
3
4
|
import type { ConnectState, ConnectNeed, ConnectApi as LConnectApi, ConnectionState, LoginPromptState } from '@ai-workbench/connect/api';
|
|
@@ -36,7 +37,7 @@ export interface Runtime {
|
|
|
36
37
|
broker?: BrokerTransport;
|
|
37
38
|
oneShot?: OneShotRuntime;
|
|
38
39
|
loginRequired?: () => Promise<void>;
|
|
39
|
-
dispose?: () => Promise<void>;
|
|
40
|
+
dispose?: (cause?: CloseCause) => Promise<void>;
|
|
40
41
|
binding?: {
|
|
41
42
|
bindingId: string;
|
|
42
43
|
workspaceId: string;
|
|
@@ -47,8 +48,9 @@ export interface Runtime {
|
|
|
47
48
|
details?: DetailChannel;
|
|
48
49
|
recoveredSession?: AcpRecovery;
|
|
49
50
|
restartNotice?: string;
|
|
50
|
-
detach?: () => Promise<void>;
|
|
51
|
+
detach?: (cause?: CloseCause) => Promise<void>;
|
|
51
52
|
restartInputIds?: readonly string[];
|
|
53
|
+
freshSessionConfirmed?: boolean;
|
|
52
54
|
saveCheckpoint?: (id: string, acp: AcpRecovery) => Promise<boolean>;
|
|
53
55
|
detailContext?: () => {
|
|
54
56
|
turn: string | number | null;
|
|
@@ -63,8 +65,12 @@ export interface Runtime {
|
|
|
63
65
|
};
|
|
64
66
|
employeeExecution?: (params: unknown, signal?: AbortSignal) => Promise<Record<string, unknown>>;
|
|
65
67
|
employeePresentation?: string;
|
|
68
|
+
closeDiagnostics?: CloseDiagnostics;
|
|
66
69
|
}
|
|
67
70
|
export interface RuntimeRequest extends Invocation {
|
|
71
|
+
closeDiagnostics?: CloseDiagnostics;
|
|
72
|
+
requireFreshSession?: boolean;
|
|
73
|
+
previousContextSha256?: string;
|
|
68
74
|
preflightFailureRecorded?: boolean;
|
|
69
75
|
recordEarlyFailure?: (error: unknown, stage: SourceAdmissionStage) => Promise<void>;
|
|
70
76
|
userInputIds?: readonly string[];
|
package/dist/recovery.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ export interface AcpRecovery {
|
|
|
4
4
|
capabilities: AgentCapabilities;
|
|
5
5
|
modelConfiguration?: {
|
|
6
6
|
configOptions: SessionConfigOption[];
|
|
7
|
+
selectorRoles?: {
|
|
8
|
+
model?: string;
|
|
9
|
+
effort?: string;
|
|
10
|
+
};
|
|
7
11
|
resolvedEvidence?: {
|
|
8
12
|
modelValue: string;
|
|
9
13
|
resolvedModel: string;
|
package/dist/rpc-namespace.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { Stream } from '@agentclientprotocol/sdk/unstable';
|
|
1
|
+
import type { Stream, AnyMessage } from '@agentclientprotocol/sdk/unstable';
|
|
2
2
|
/** Public Stream only. Server request/approval IDs are never changed. */
|
|
3
|
-
export declare function isolateRpcIds(stream: Stream): Stream;
|
|
3
|
+
export declare function isolateRpcIds(stream: Stream, onWrite?: (message: AnyMessage) => void): Stream;
|
package/dist/rpc-namespace.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { NeedError } from "./remote.js";
|
|
3
3
|
/** Public Stream only. Server request/approval IDs are never changed. */
|
|
4
|
-
export function isolateRpcIds(stream) {
|
|
4
|
+
export function isolateRpcIds(stream, onWrite) {
|
|
5
5
|
const prefix = randomUUID() + '/';
|
|
6
6
|
const pending = new Map();
|
|
7
7
|
const readable = stream.readable.pipeThrough(new TransformStream({ transform(message, controller) {
|
|
@@ -30,6 +30,7 @@ export function isolateRpcIds(stream) {
|
|
|
30
30
|
}
|
|
31
31
|
const writer = stream.writable.getWriter();
|
|
32
32
|
try {
|
|
33
|
+
onWrite?.(outgoing);
|
|
33
34
|
await writer.write(outgoing);
|
|
34
35
|
}
|
|
35
36
|
finally {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SessionHeader } from '@deepseek-ai/dsh-session';
|
|
2
|
+
/** Only a persisted/live representation comparison. Live lifecycle fences still
|
|
3
|
+
* compare their original header exactly. Official JSONL stores omitted depth as 0. */
|
|
4
|
+
export declare function sameStoredSessionHeader(a: SessionHeader, b: SessionHeader): boolean;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
/** Only a persisted/live representation comparison. Live lifecycle fences still
|
|
3
|
+
* compare their original header exactly. Official JSONL stores omitted depth as 0. */
|
|
4
|
+
export function sameStoredSessionHeader(a, b) {
|
|
5
|
+
const depth = (header) => !Object.hasOwn(header, 'delegationDepth') ? 0 :
|
|
6
|
+
typeof header.delegationDepth === 'number' && Number.isSafeInteger(header.delegationDepth) && header.delegationDepth >= 0 && !Object.is(header.delegationDepth, -0) ? header.delegationDepth : undefined;
|
|
7
|
+
const left = depth(a), right = depth(b);
|
|
8
|
+
if (left === undefined || right === undefined)
|
|
9
|
+
return false;
|
|
10
|
+
const normalized = (header, value) => Object.setPrototypeOf({ ...header, delegationDepth: value }, Object.getPrototypeOf(header));
|
|
11
|
+
return isDeepStrictEqual(normalized(a, left), normalized(b, right));
|
|
12
|
+
}
|
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.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"zod": "4.6.7"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.
|
|
42
|
+
"@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.19.tgz",
|
|
43
43
|
"@ai-workbench/network": "file:test/fixtures/ai-workbench-network-0.4.4.tgz",
|
|
44
44
|
"@deepseek-ai/cordis": "4.0.4",
|
|
45
45
|
"@deepseek-ai/cordis-plugin-group": "1.0.4",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"zustand": "4.4.7"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
|
-
"@ai-workbench/connect": "~0.7.
|
|
71
|
+
"@ai-workbench/connect": "~0.7.19",
|
|
72
72
|
"@ai-workbench/network": "~0.4.4",
|
|
73
73
|
"@deepseek-ai/cordis": "4.0.4",
|
|
74
74
|
"@deepseek-ai/dsh-agent": "0.2.0-rc.2",
|