@ai-workbench/remote-agent 0.8.13 → 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.
@@ -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
- constructor(options: SessionConfigOption[], rpcSet: ModelConfigurationSetter);
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
- unavailable('配置名称为空或无效');
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
- function select(option) {
20
- if (!text(option.currentValue) || !Array.isArray(option.options) || !option.options.length)
21
- unavailable('选择器缺少实际当前值或可选列表');
22
- const values = [];
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) || 'group' in value || 'options' in value)
26
- unavailable('可选值格式无效');
27
- label(value);
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
- unavailable('选择器包含重复可选值');
28
+ inform('CONFIG_UI_METADATA_INVALID');
30
29
  seen.add(value.value);
31
- values.push({ value: value.value, name: value.name, ...(value.description === undefined ? {} : { description: 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
- const grouped = record(option.options[0]) && 'group' in option.options[0];
34
- const groups = new Set();
35
- for (const entry of option.options) {
36
- if (grouped) {
37
- if (!record(entry) || !text(entry.group) || 'value' in entry || !Array.isArray(entry.options) || !entry.options.length)
38
- unavailable('分组选择器格式无效或分组为空');
39
- label(entry);
40
- if (groups.has(entry.group))
41
- unavailable('选择器包含重复分组标识');
42
- groups.add(entry.group);
43
- for (const value of entry.options)
44
- add(value);
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
- unavailable('实际当前值不在远端可选列表中');
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 ids = new Set();
57
- const models = [];
58
- const efforts = [];
59
- for (const option of options) {
60
- if (!record(option) || !text(option.id))
61
- unavailable('配置标识为空或无效');
62
- label(option);
63
- if (ids.has(option.id))
64
- unavailable('配置列表包含重复标识');
65
- ids.add(option.id);
66
- if (option.category !== undefined && option.category !== null && typeof option.category !== 'string')
67
- unavailable('配置类别无效');
68
- if (option.type === 'select') {
69
- const selector = select(option);
70
- if (option.category === 'model')
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
- else if (option.type === 'boolean') {
76
- if (typeof option.currentValue !== 'boolean')
77
- unavailable('布尔配置当前值无效');
78
- if (option.category === 'model' || option.category === 'thought_level')
79
- unavailable('模型或推理等级配置不是选择器');
84
+ if (candidates.length !== 1) {
85
+ if (!required && !candidates.length)
86
+ return;
87
+ unavailable(candidates.length ? '模型或推理配置入口不唯一' : '没有可靠的模型配置入口,不能猜测配置标识');
80
88
  }
81
- else
82
- unavailable('配置类型无法识别');
83
- }
84
- if (models.length !== 1)
85
- unavailable(models.length ? '模型选择器不唯一' : '没有可识别的模型选择器');
86
- if (efforts.length > 1)
87
- unavailable('推理等级选择器不唯一');
88
- return { model: models[0], effort: efforts[0] };
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
- constructor(options, rpcSet) {
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
- // The wire is JSON data; clone before validation so caller mutations cannot race ACK checks.
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: copy, models: [], effortAvailable: false, confirmed: false, failure: { code: error.code, message: error.message } });
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.update(response.configOptions);
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
- unavailable('所选模型不在实际远端列表中');
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
- unavailable('所选推理等级不在模型回执的实际列表中');
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)
@@ -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])
@@ -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[];
@@ -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;
@@ -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;
@@ -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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-workbench/remote-agent",
3
- "version": "0.8.13",
3
+ "version": "0.8.14",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",