@vedmalex/ai-connect 0.6.0 → 0.8.0

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.
@@ -1984,6 +1984,8 @@ function fillConfiguredContextLength(route, models) {
1984
1984
  return {
1985
1985
  ...entry,
1986
1986
  contextLength: configured,
1987
+ // FT-002: typed provenance (canonical) + metadata alias (back-compat).
1988
+ contextWindowSource: "configured",
1987
1989
  metadata: { ...entry.metadata ?? {}, contextWindowSource: "configured" }
1988
1990
  };
1989
1991
  });
@@ -3316,7 +3318,8 @@ function createClient(configOrInput, options = {}) {
3316
3318
  ...catalog.canonicalModelId ? { canonicalModelId: catalog.canonicalModelId } : {},
3317
3319
  ...catalog.resolvedModelId ? { resolvedModelId: catalog.resolvedModelId } : {},
3318
3320
  ...catalog.currentModelId ? { currentModelId: catalog.currentModelId } : {},
3319
- availableModels: catalog.availableModels
3321
+ availableModels: catalog.availableModels,
3322
+ ...catalog.warnings?.length ? { warnings: catalog.warnings } : {}
3320
3323
  });
3321
3324
  wideAttempts.push({
3322
3325
  index: wideAttempts.length + 1,
@@ -5019,7 +5022,7 @@ function parseOpenAiModelCatalog(route, payload) {
5019
5022
  modelId: entry.id,
5020
5023
  name: typeof entry.display_name === "string" ? entry.display_name : typeof entry.name === "string" ? entry.name : entry.id,
5021
5024
  ...typeof entry.description === "string" ? { description: entry.description } : {},
5022
- ...contextLength !== void 0 ? { contextLength } : {},
5025
+ ...contextLength !== void 0 ? { contextLength, contextWindowSource: "discovered" } : {},
5023
5026
  ...pricing ? { pricing } : {},
5024
5027
  ...free ? { free } : {},
5025
5028
  ...metadata ? {
@@ -6794,6 +6797,20 @@ var TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
6794
6797
  function isRecord2(value) {
6795
6798
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6796
6799
  }
6800
+ var ACP_HARNESS_NOISE_PATTERNS = [
6801
+ /^модель переключена на .+?\.?(?:\s*готов к работе[.!…]*)?$/iu,
6802
+ /^готов к работе[.!…]*$/iu,
6803
+ /^жду (?:вашего вопроса|вопроса или задачи|вашей задачи|вашего ответа|явного запроса)[^]*$/iu,
6804
+ /^это сообщение помечено как локальная команда[^]*$/iu,
6805
+ /^<local-command-(?:caveat|stdout|name|message|args)[^>]*>[^]*$/iu
6806
+ ];
6807
+ function isHarnessNoiseLine(line) {
6808
+ const trimmed = line.trim();
6809
+ if (trimmed.length === 0) {
6810
+ return false;
6811
+ }
6812
+ return ACP_HARNESS_NOISE_PATTERNS.some((pattern) => pattern.test(trimmed));
6813
+ }
6797
6814
  function splitCommandLine(commandLine) {
6798
6815
  const matches = commandLine.match(/"[^"]*"|'[^']*'|[^\s]+/g) ?? [];
6799
6816
  const parts = matches.map((part) => {
@@ -7630,6 +7647,7 @@ function buildAcpLifecycle(route, authRequest, mode) {
7630
7647
  }
7631
7648
  steps.push("session/new");
7632
7649
  if (mode === "prompt") {
7650
+ steps.push("session/set_model");
7633
7651
  steps.push("session/prompt");
7634
7652
  }
7635
7653
  return { steps, keys };
@@ -7717,6 +7735,9 @@ var AcpConnection = class {
7717
7735
  env;
7718
7736
  timeoutMs;
7719
7737
  permissionMode;
7738
+ selectModel;
7739
+ suppressHarnessNoise;
7740
+ failOnHarnessOnlyTurn;
7720
7741
  command;
7721
7742
  args;
7722
7743
  child;
@@ -7738,6 +7759,9 @@ var AcpConnection = class {
7738
7759
  this.env = options.env;
7739
7760
  this.timeoutMs = options.timeoutMs;
7740
7761
  this.permissionMode = options.permissionMode;
7762
+ this.selectModel = options.selectModel;
7763
+ this.suppressHarnessNoise = options.suppressHarnessNoise;
7764
+ this.failOnHarnessOnlyTurn = options.failOnHarnessOnlyTurn;
7741
7765
  const resolved = splitCommandLine(options.commandLine);
7742
7766
  this.command = resolved.command;
7743
7767
  this.args = resolved.args;
@@ -7770,8 +7794,18 @@ var AcpConnection = class {
7770
7794
  usage: void 0,
7771
7795
  attachments: [],
7772
7796
  warnings: [],
7797
+ suppressNoise: this.suppressHarnessNoise,
7798
+ lineBuffer: "",
7799
+ harnessMarkers: [],
7773
7800
  ...onDelta ? { onDelta } : {}
7774
7801
  };
7802
+ await this.selectSessionModel(
7803
+ sessionId,
7804
+ context.route,
7805
+ session,
7806
+ transport.phases ?? [],
7807
+ this.activePrompt.warnings
7808
+ );
7775
7809
  let stopReason;
7776
7810
  const abortSignal = context.abort.signal;
7777
7811
  let abortListener;
@@ -7814,13 +7848,27 @@ var AcpConnection = class {
7814
7848
  }
7815
7849
  this.bumpIdleTimer();
7816
7850
  }
7851
+ this.flushHarnessLineBuffer();
7817
7852
  const stderrSlice = this.stderr.slice(stderrOffset).trim();
7818
7853
  const warnings = [...this.activePrompt.warnings];
7854
+ if (this.activePrompt.harnessMarkers.length > 0) {
7855
+ warnings.push(
7856
+ `ACP: filtered ${this.activePrompt.harnessMarkers.length} interactive-harness line(s) from the response.`
7857
+ );
7858
+ }
7819
7859
  if (stderrSlice.length > 0) {
7820
7860
  warnings.push(stderrSlice);
7821
7861
  }
7862
+ const text = this.activePrompt.text.trim();
7863
+ if (this.failOnHarnessOnlyTurn && this.activePrompt.harnessMarkers.length > 0 && text.length === 0) {
7864
+ this.activePrompt = void 0;
7865
+ throw new AiConnectError(
7866
+ "temporary_unavailable",
7867
+ "ACP turn returned only interactive-harness output (no task output)."
7868
+ );
7869
+ }
7822
7870
  const result = {
7823
- text: this.activePrompt.text.trim(),
7871
+ text,
7824
7872
  attachments: [...this.activePrompt.attachments],
7825
7873
  warnings,
7826
7874
  ...this.activePrompt.usage ? { usage: this.activePrompt.usage } : {},
@@ -8127,6 +8175,108 @@ var AcpConnection = class {
8127
8175
  }
8128
8176
  });
8129
8177
  }
8178
+ /**
8179
+ * Select the route's model through the ACP protocol (TASK-033 / UR-002).
8180
+ * Best-effort matrix: skip when disabled / no model / no advertised catalog /
8181
+ * already current; warn (not silently default) when the requested model is not
8182
+ * in the agent's `availableModels`; otherwise `session/set_model` and surface
8183
+ * any error as a warning. Never throws — a failed protocol selection must not
8184
+ * fail the generation.
8185
+ */
8186
+ async selectSessionModel(sessionId, route, sessionResult, phases, warnings) {
8187
+ if (!this.selectModel) {
8188
+ return;
8189
+ }
8190
+ const desiredModel = route.model?.trim();
8191
+ if (!desiredModel) {
8192
+ return;
8193
+ }
8194
+ const catalog = modelCatalogFromSession(sessionResult);
8195
+ if (!catalog) {
8196
+ return;
8197
+ }
8198
+ if (catalog.currentModelId === desiredModel) {
8199
+ return;
8200
+ }
8201
+ const available = catalog.availableModels ?? [];
8202
+ if (available.length > 0 && !available.some((entry) => entry.modelId === desiredModel)) {
8203
+ warnings.push(
8204
+ `ACP agent's available models [${available.map((entry) => entry.modelId).join(", ")}] do not include route model "${desiredModel}"; model not switched via protocol.`
8205
+ );
8206
+ return;
8207
+ }
8208
+ try {
8209
+ await measurePhase(
8210
+ phases,
8211
+ "session/set_model",
8212
+ async () => this.request("session/set_model", {
8213
+ sessionId,
8214
+ modelId: desiredModel
8215
+ })
8216
+ );
8217
+ } catch (error) {
8218
+ const message = error instanceof Error ? error.message : isRecord2(error) && typeof error.message === "string" ? error.message : String(error);
8219
+ warnings.push(
8220
+ `ACP session/set_model("${desiredModel}") failed: ${message}`
8221
+ );
8222
+ }
8223
+ }
8224
+ /**
8225
+ * Append `agent_message_chunk` text (TASK-033 / UR-003). With suppression off
8226
+ * (legacy) the chunk streams + accumulates raw. With suppression on, the chunk
8227
+ * is line-buffered: each COMPLETE line is classified and either dropped (a
8228
+ * harness marker — no delta, no accumulation) or emitted; the trailing partial
8229
+ * line waits in `lineBuffer` for the next chunk or the end-of-turn flush.
8230
+ */
8231
+ appendAgentText(chunk) {
8232
+ const state = this.activePrompt;
8233
+ if (!state) {
8234
+ return;
8235
+ }
8236
+ if (!state.suppressNoise) {
8237
+ state.onDelta?.(chunk);
8238
+ state.text += chunk;
8239
+ return;
8240
+ }
8241
+ state.lineBuffer += chunk;
8242
+ let newlineIndex = state.lineBuffer.indexOf("\n");
8243
+ while (newlineIndex !== -1) {
8244
+ const line = state.lineBuffer.slice(0, newlineIndex + 1);
8245
+ state.lineBuffer = state.lineBuffer.slice(newlineIndex + 1);
8246
+ this.emitAgentLine(line);
8247
+ newlineIndex = state.lineBuffer.indexOf("\n");
8248
+ }
8249
+ }
8250
+ /** Flush any trailing partial line through the harness-noise classifier. */
8251
+ flushHarnessLineBuffer() {
8252
+ const state = this.activePrompt;
8253
+ if (!state || !state.suppressNoise) {
8254
+ return;
8255
+ }
8256
+ const remaining = state.lineBuffer;
8257
+ state.lineBuffer = "";
8258
+ if (remaining.length > 0) {
8259
+ this.emitAgentLine(remaining);
8260
+ }
8261
+ }
8262
+ /**
8263
+ * Emit one classified line: a curated harness marker is recorded + dropped
8264
+ * (no delta, no accumulation); any other line streams + accumulates. `line`
8265
+ * may carry a trailing newline — classification runs on the content.
8266
+ */
8267
+ emitAgentLine(line) {
8268
+ const state = this.activePrompt;
8269
+ if (!state) {
8270
+ return;
8271
+ }
8272
+ const content = line.endsWith("\n") ? line.slice(0, -1) : line;
8273
+ if (isHarnessNoiseLine(content)) {
8274
+ state.harnessMarkers.push(content.trim());
8275
+ return;
8276
+ }
8277
+ state.onDelta?.(line);
8278
+ state.text += line;
8279
+ }
8130
8280
  handleNotification(message) {
8131
8281
  if (message.method !== "session/update" || !this.activePrompt) {
8132
8282
  return;
@@ -8141,8 +8291,7 @@ var AcpConnection = class {
8141
8291
  }
8142
8292
  const chunk = extractTextChunk(update);
8143
8293
  if (update.sessionUpdate === "agent_message_chunk" && chunk) {
8144
- this.activePrompt.onDelta?.(chunk);
8145
- this.activePrompt.text += chunk;
8294
+ this.appendAgentText(chunk);
8146
8295
  }
8147
8296
  if (update.sessionUpdate === "agent_thought_chunk" && chunk) {
8148
8297
  this.activePrompt.warnings.push(chunk);
@@ -8329,7 +8478,10 @@ function createAcpTransportManager(options) {
8329
8478
  cwd: runtime.cwd,
8330
8479
  env: runtime.env,
8331
8480
  timeoutMs: promptTimeout(options),
8332
- permissionMode: options?.permissionMode ?? "deny-all"
8481
+ permissionMode: options?.permissionMode ?? "deny-all",
8482
+ selectModel: options?.selectModel ?? true,
8483
+ suppressHarnessNoise: options?.suppressHarnessNoise ?? true,
8484
+ failOnHarnessOnlyTurn: options?.failOnHarnessOnlyTurn ?? true
8333
8485
  };
8334
8486
  }
8335
8487
  function getPool(key) {
@@ -9015,16 +9167,20 @@ function buildCliDiscoveryInvocation(route, command, options) {
9015
9167
  parameterKeys
9016
9168
  };
9017
9169
  }
9018
- function buildStaticCliCatalog(route) {
9170
+ function buildStaticCliCatalog(route, warning) {
9019
9171
  const models = route.advertisedModels.map((id) => ({
9020
9172
  modelId: id,
9021
9173
  name: id
9022
9174
  }));
9023
- return buildModelCatalog(
9175
+ const catalog = buildModelCatalog(
9024
9176
  route,
9025
9177
  models,
9026
9178
  currentModelIdForRoute(route, route.advertisedModels)
9027
9179
  );
9180
+ if (warning) {
9181
+ catalog.warnings = [...catalog.warnings ?? [], warning];
9182
+ }
9183
+ return catalog;
9028
9184
  }
9029
9185
  function statsToUsage(stats) {
9030
9186
  if (!stats || typeof stats !== "object") {
@@ -9167,7 +9323,9 @@ function modelInfoFromRecord(record, selector) {
9167
9323
  modelId,
9168
9324
  name,
9169
9325
  ...description ? { description } : {},
9170
- ...contextLength !== void 0 ? { contextLength } : {}
9326
+ // FT-002: a contextLength read from the list-command output is a discovered
9327
+ // value (consistent with the API parsers), so tag its provenance.
9328
+ ...contextLength !== void 0 ? { contextLength, contextWindowSource: "discovered" } : {}
9171
9329
  };
9172
9330
  }
9173
9331
  function parseCliModelList(stdout, parser, selector) {
@@ -9438,13 +9596,20 @@ function createCliTransportManager(options) {
9438
9596
  throw error;
9439
9597
  }
9440
9598
  if (source.fallback === "static") {
9441
- return buildStaticCliCatalog(context.route);
9599
+ const detail = error instanceof Error ? error.message : String(error);
9600
+ return buildStaticCliCatalog(
9601
+ context.route,
9602
+ `CLI discovery command for "${context.route.transport.id}" failed (${detail}); fell back to the static models[] catalog.`
9603
+ );
9442
9604
  }
9443
9605
  throw error;
9444
9606
  }
9445
9607
  if (models.length === 0) {
9446
9608
  if (source.fallback === "static") {
9447
- return buildStaticCliCatalog(context.route);
9609
+ return buildStaticCliCatalog(
9610
+ context.route,
9611
+ `CLI discovery command for "${context.route.transport.id}" returned no models; fell back to the static models[] catalog.`
9612
+ );
9448
9613
  }
9449
9614
  throw new AiConnectError(
9450
9615
  "temporary_unavailable",