@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.
package/dist/bun/local.js CHANGED
@@ -1975,6 +1975,8 @@ function fillConfiguredContextLength(route, models) {
1975
1975
  return {
1976
1976
  ...entry,
1977
1977
  contextLength: configured,
1978
+ // FT-002: typed provenance (canonical) + metadata alias (back-compat).
1979
+ contextWindowSource: "configured",
1978
1980
  metadata: { ...entry.metadata ?? {}, contextWindowSource: "configured" }
1979
1981
  };
1980
1982
  });
@@ -3304,7 +3306,8 @@ function createClient(configOrInput, options = {}) {
3304
3306
  ...catalog.canonicalModelId ? { canonicalModelId: catalog.canonicalModelId } : {},
3305
3307
  ...catalog.resolvedModelId ? { resolvedModelId: catalog.resolvedModelId } : {},
3306
3308
  ...catalog.currentModelId ? { currentModelId: catalog.currentModelId } : {},
3307
- availableModels: catalog.availableModels
3309
+ availableModels: catalog.availableModels,
3310
+ ...catalog.warnings?.length ? { warnings: catalog.warnings } : {}
3308
3311
  });
3309
3312
  wideAttempts.push({
3310
3313
  index: wideAttempts.length + 1,
@@ -4989,7 +4992,7 @@ function parseOpenAiModelCatalog(route, payload) {
4989
4992
  modelId: entry.id,
4990
4993
  name: typeof entry.display_name === "string" ? entry.display_name : typeof entry.name === "string" ? entry.name : entry.id,
4991
4994
  ...typeof entry.description === "string" ? { description: entry.description } : {},
4992
- ...contextLength !== void 0 ? { contextLength } : {},
4995
+ ...contextLength !== void 0 ? { contextLength, contextWindowSource: "discovered" } : {},
4993
4996
  ...pricing ? { pricing } : {},
4994
4997
  ...free ? { free } : {},
4995
4998
  ...metadata ? {
@@ -6544,6 +6547,20 @@ var TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
6544
6547
  function isRecord2(value) {
6545
6548
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6546
6549
  }
6550
+ var ACP_HARNESS_NOISE_PATTERNS = [
6551
+ /^модель переключена на .+?\.?(?:\s*готов к работе[.!…]*)?$/iu,
6552
+ /^готов к работе[.!…]*$/iu,
6553
+ /^жду (?:вашего вопроса|вопроса или задачи|вашей задачи|вашего ответа|явного запроса)[^]*$/iu,
6554
+ /^это сообщение помечено как локальная команда[^]*$/iu,
6555
+ /^<local-command-(?:caveat|stdout|name|message|args)[^>]*>[^]*$/iu
6556
+ ];
6557
+ function isHarnessNoiseLine(line) {
6558
+ const trimmed = line.trim();
6559
+ if (trimmed.length === 0) {
6560
+ return false;
6561
+ }
6562
+ return ACP_HARNESS_NOISE_PATTERNS.some((pattern) => pattern.test(trimmed));
6563
+ }
6547
6564
  function splitCommandLine(commandLine) {
6548
6565
  const matches = commandLine.match(/"[^"]*"|'[^']*'|[^\s]+/g) ?? [];
6549
6566
  const parts = matches.map((part) => {
@@ -7380,6 +7397,7 @@ function buildAcpLifecycle(route, authRequest, mode) {
7380
7397
  }
7381
7398
  steps.push("session/new");
7382
7399
  if (mode === "prompt") {
7400
+ steps.push("session/set_model");
7383
7401
  steps.push("session/prompt");
7384
7402
  }
7385
7403
  return { steps, keys };
@@ -7467,6 +7485,9 @@ var AcpConnection = class {
7467
7485
  env;
7468
7486
  timeoutMs;
7469
7487
  permissionMode;
7488
+ selectModel;
7489
+ suppressHarnessNoise;
7490
+ failOnHarnessOnlyTurn;
7470
7491
  command;
7471
7492
  args;
7472
7493
  child;
@@ -7488,6 +7509,9 @@ var AcpConnection = class {
7488
7509
  this.env = options.env;
7489
7510
  this.timeoutMs = options.timeoutMs;
7490
7511
  this.permissionMode = options.permissionMode;
7512
+ this.selectModel = options.selectModel;
7513
+ this.suppressHarnessNoise = options.suppressHarnessNoise;
7514
+ this.failOnHarnessOnlyTurn = options.failOnHarnessOnlyTurn;
7491
7515
  const resolved = splitCommandLine(options.commandLine);
7492
7516
  this.command = resolved.command;
7493
7517
  this.args = resolved.args;
@@ -7520,8 +7544,18 @@ var AcpConnection = class {
7520
7544
  usage: void 0,
7521
7545
  attachments: [],
7522
7546
  warnings: [],
7547
+ suppressNoise: this.suppressHarnessNoise,
7548
+ lineBuffer: "",
7549
+ harnessMarkers: [],
7523
7550
  ...onDelta ? { onDelta } : {}
7524
7551
  };
7552
+ await this.selectSessionModel(
7553
+ sessionId,
7554
+ context.route,
7555
+ session,
7556
+ transport.phases ?? [],
7557
+ this.activePrompt.warnings
7558
+ );
7525
7559
  let stopReason;
7526
7560
  const abortSignal = context.abort.signal;
7527
7561
  let abortListener;
@@ -7564,13 +7598,27 @@ var AcpConnection = class {
7564
7598
  }
7565
7599
  this.bumpIdleTimer();
7566
7600
  }
7601
+ this.flushHarnessLineBuffer();
7567
7602
  const stderrSlice = this.stderr.slice(stderrOffset).trim();
7568
7603
  const warnings = [...this.activePrompt.warnings];
7604
+ if (this.activePrompt.harnessMarkers.length > 0) {
7605
+ warnings.push(
7606
+ `ACP: filtered ${this.activePrompt.harnessMarkers.length} interactive-harness line(s) from the response.`
7607
+ );
7608
+ }
7569
7609
  if (stderrSlice.length > 0) {
7570
7610
  warnings.push(stderrSlice);
7571
7611
  }
7612
+ const text = this.activePrompt.text.trim();
7613
+ if (this.failOnHarnessOnlyTurn && this.activePrompt.harnessMarkers.length > 0 && text.length === 0) {
7614
+ this.activePrompt = void 0;
7615
+ throw new AiConnectError(
7616
+ "temporary_unavailable",
7617
+ "ACP turn returned only interactive-harness output (no task output)."
7618
+ );
7619
+ }
7572
7620
  const result = {
7573
- text: this.activePrompt.text.trim(),
7621
+ text,
7574
7622
  attachments: [...this.activePrompt.attachments],
7575
7623
  warnings,
7576
7624
  ...this.activePrompt.usage ? { usage: this.activePrompt.usage } : {},
@@ -7877,6 +7925,108 @@ var AcpConnection = class {
7877
7925
  }
7878
7926
  });
7879
7927
  }
7928
+ /**
7929
+ * Select the route's model through the ACP protocol (TASK-033 / UR-002).
7930
+ * Best-effort matrix: skip when disabled / no model / no advertised catalog /
7931
+ * already current; warn (not silently default) when the requested model is not
7932
+ * in the agent's `availableModels`; otherwise `session/set_model` and surface
7933
+ * any error as a warning. Never throws — a failed protocol selection must not
7934
+ * fail the generation.
7935
+ */
7936
+ async selectSessionModel(sessionId, route, sessionResult, phases, warnings) {
7937
+ if (!this.selectModel) {
7938
+ return;
7939
+ }
7940
+ const desiredModel = route.model?.trim();
7941
+ if (!desiredModel) {
7942
+ return;
7943
+ }
7944
+ const catalog = modelCatalogFromSession(sessionResult);
7945
+ if (!catalog) {
7946
+ return;
7947
+ }
7948
+ if (catalog.currentModelId === desiredModel) {
7949
+ return;
7950
+ }
7951
+ const available = catalog.availableModels ?? [];
7952
+ if (available.length > 0 && !available.some((entry) => entry.modelId === desiredModel)) {
7953
+ warnings.push(
7954
+ `ACP agent's available models [${available.map((entry) => entry.modelId).join(", ")}] do not include route model "${desiredModel}"; model not switched via protocol.`
7955
+ );
7956
+ return;
7957
+ }
7958
+ try {
7959
+ await measurePhase(
7960
+ phases,
7961
+ "session/set_model",
7962
+ async () => this.request("session/set_model", {
7963
+ sessionId,
7964
+ modelId: desiredModel
7965
+ })
7966
+ );
7967
+ } catch (error) {
7968
+ const message = error instanceof Error ? error.message : isRecord2(error) && typeof error.message === "string" ? error.message : String(error);
7969
+ warnings.push(
7970
+ `ACP session/set_model("${desiredModel}") failed: ${message}`
7971
+ );
7972
+ }
7973
+ }
7974
+ /**
7975
+ * Append `agent_message_chunk` text (TASK-033 / UR-003). With suppression off
7976
+ * (legacy) the chunk streams + accumulates raw. With suppression on, the chunk
7977
+ * is line-buffered: each COMPLETE line is classified and either dropped (a
7978
+ * harness marker — no delta, no accumulation) or emitted; the trailing partial
7979
+ * line waits in `lineBuffer` for the next chunk or the end-of-turn flush.
7980
+ */
7981
+ appendAgentText(chunk) {
7982
+ const state = this.activePrompt;
7983
+ if (!state) {
7984
+ return;
7985
+ }
7986
+ if (!state.suppressNoise) {
7987
+ state.onDelta?.(chunk);
7988
+ state.text += chunk;
7989
+ return;
7990
+ }
7991
+ state.lineBuffer += chunk;
7992
+ let newlineIndex = state.lineBuffer.indexOf("\n");
7993
+ while (newlineIndex !== -1) {
7994
+ const line = state.lineBuffer.slice(0, newlineIndex + 1);
7995
+ state.lineBuffer = state.lineBuffer.slice(newlineIndex + 1);
7996
+ this.emitAgentLine(line);
7997
+ newlineIndex = state.lineBuffer.indexOf("\n");
7998
+ }
7999
+ }
8000
+ /** Flush any trailing partial line through the harness-noise classifier. */
8001
+ flushHarnessLineBuffer() {
8002
+ const state = this.activePrompt;
8003
+ if (!state || !state.suppressNoise) {
8004
+ return;
8005
+ }
8006
+ const remaining = state.lineBuffer;
8007
+ state.lineBuffer = "";
8008
+ if (remaining.length > 0) {
8009
+ this.emitAgentLine(remaining);
8010
+ }
8011
+ }
8012
+ /**
8013
+ * Emit one classified line: a curated harness marker is recorded + dropped
8014
+ * (no delta, no accumulation); any other line streams + accumulates. `line`
8015
+ * may carry a trailing newline — classification runs on the content.
8016
+ */
8017
+ emitAgentLine(line) {
8018
+ const state = this.activePrompt;
8019
+ if (!state) {
8020
+ return;
8021
+ }
8022
+ const content = line.endsWith("\n") ? line.slice(0, -1) : line;
8023
+ if (isHarnessNoiseLine(content)) {
8024
+ state.harnessMarkers.push(content.trim());
8025
+ return;
8026
+ }
8027
+ state.onDelta?.(line);
8028
+ state.text += line;
8029
+ }
7880
8030
  handleNotification(message) {
7881
8031
  if (message.method !== "session/update" || !this.activePrompt) {
7882
8032
  return;
@@ -7891,8 +8041,7 @@ var AcpConnection = class {
7891
8041
  }
7892
8042
  const chunk = extractTextChunk(update);
7893
8043
  if (update.sessionUpdate === "agent_message_chunk" && chunk) {
7894
- this.activePrompt.onDelta?.(chunk);
7895
- this.activePrompt.text += chunk;
8044
+ this.appendAgentText(chunk);
7896
8045
  }
7897
8046
  if (update.sessionUpdate === "agent_thought_chunk" && chunk) {
7898
8047
  this.activePrompt.warnings.push(chunk);
@@ -8079,7 +8228,10 @@ function createAcpTransportManager(options) {
8079
8228
  cwd: runtime.cwd,
8080
8229
  env: runtime.env,
8081
8230
  timeoutMs: promptTimeout(options),
8082
- permissionMode: options?.permissionMode ?? "deny-all"
8231
+ permissionMode: options?.permissionMode ?? "deny-all",
8232
+ selectModel: options?.selectModel ?? true,
8233
+ suppressHarnessNoise: options?.suppressHarnessNoise ?? true,
8234
+ failOnHarnessOnlyTurn: options?.failOnHarnessOnlyTurn ?? true
8083
8235
  };
8084
8236
  }
8085
8237
  function getPool(key) {
@@ -8765,16 +8917,20 @@ function buildCliDiscoveryInvocation(route, command, options) {
8765
8917
  parameterKeys
8766
8918
  };
8767
8919
  }
8768
- function buildStaticCliCatalog(route) {
8920
+ function buildStaticCliCatalog(route, warning) {
8769
8921
  const models = route.advertisedModels.map((id) => ({
8770
8922
  modelId: id,
8771
8923
  name: id
8772
8924
  }));
8773
- return buildModelCatalog(
8925
+ const catalog = buildModelCatalog(
8774
8926
  route,
8775
8927
  models,
8776
8928
  currentModelIdForRoute(route, route.advertisedModels)
8777
8929
  );
8930
+ if (warning) {
8931
+ catalog.warnings = [...catalog.warnings ?? [], warning];
8932
+ }
8933
+ return catalog;
8778
8934
  }
8779
8935
  function statsToUsage(stats) {
8780
8936
  if (!stats || typeof stats !== "object") {
@@ -8917,7 +9073,9 @@ function modelInfoFromRecord(record, selector) {
8917
9073
  modelId,
8918
9074
  name,
8919
9075
  ...description ? { description } : {},
8920
- ...contextLength !== void 0 ? { contextLength } : {}
9076
+ // FT-002: a contextLength read from the list-command output is a discovered
9077
+ // value (consistent with the API parsers), so tag its provenance.
9078
+ ...contextLength !== void 0 ? { contextLength, contextWindowSource: "discovered" } : {}
8921
9079
  };
8922
9080
  }
8923
9081
  function parseCliModelList(stdout, parser, selector) {
@@ -9188,13 +9346,20 @@ function createCliTransportManager(options) {
9188
9346
  throw error;
9189
9347
  }
9190
9348
  if (source.fallback === "static") {
9191
- return buildStaticCliCatalog(context.route);
9349
+ const detail = error instanceof Error ? error.message : String(error);
9350
+ return buildStaticCliCatalog(
9351
+ context.route,
9352
+ `CLI discovery command for "${context.route.transport.id}" failed (${detail}); fell back to the static models[] catalog.`
9353
+ );
9192
9354
  }
9193
9355
  throw error;
9194
9356
  }
9195
9357
  if (models.length === 0) {
9196
9358
  if (source.fallback === "static") {
9197
- return buildStaticCliCatalog(context.route);
9359
+ return buildStaticCliCatalog(
9360
+ context.route,
9361
+ `CLI discovery command for "${context.route.transport.id}" returned no models; fell back to the static models[] catalog.`
9362
+ );
9198
9363
  }
9199
9364
  throw new AiConnectError(
9200
9365
  "temporary_unavailable",