@ycodium-ai/agent-acp 0.2.3252 → 0.2.3265

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.
Files changed (2) hide show
  1. package/dist/index.js +277 -47
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -10,7 +10,11 @@ import { definePlugin } from "@ycodium-ai/plugin-api";
10
10
  // src/acpInstance.ts
11
11
  import * as NodeOS3 from "node:os";
12
12
  import * as NodePath4 from "node:path";
13
- import { mergeLaunchEnv, modelSelectionStringOption } from "@ycodium-ai/plugin-api/executor";
13
+ import {
14
+ mergeLaunchEnv,
15
+ modelSelectionStringOption,
16
+ raceAbort as raceAbort2
17
+ } from "@ycodium-ai/plugin-api/executor";
14
18
  import { nodeStreamsFromHandle as nodeStreamsFromHandle2 } from "@ycodium-ai/plugin-api/process-stdin";
15
19
 
16
20
  // ../../packages/ycodium-acp-executor/src/executor.ts
@@ -25043,11 +25047,15 @@ function createAcpConnection(options) {
25043
25047
  });
25044
25048
  };
25045
25049
  const close = (error62) => shutdown(error62 ?? new AcpConnectionClosedError());
25050
+ const abandonPending = (error62) => {
25051
+ rejectPending(error62);
25052
+ };
25046
25053
  return {
25047
25054
  request,
25048
25055
  begin,
25049
25056
  notify,
25050
25057
  awaitInboundIdle,
25058
+ abandonPending,
25051
25059
  close,
25052
25060
  closed
25053
25061
  };
@@ -25513,10 +25521,11 @@ function createExecutorPrompt({
25513
25521
  const prompt = async (input2) => {
25514
25522
  failIfPromptUnavailable();
25515
25523
  const started = requireStarted();
25524
+ let sessionId = started.sessionId;
25516
25525
  let record2;
25517
25526
  const settle2 = (rest) => settleTurn({
25518
25527
  expectedGeneration: started.generation,
25519
- expectedSessionId: started.sessionId,
25528
+ expectedSessionId: sessionId,
25520
25529
  record: record2,
25521
25530
  ...rest
25522
25531
  });
@@ -25600,12 +25609,32 @@ function createExecutorPrompt({
25600
25609
  const queuedTurnId = turnId;
25601
25610
  const isStale = () => interruptedTurnIds.has(queuedTurnId) || input2.expectedActiveTurnId !== void 0 && shared.activeTurnId !== queuedTurnId;
25602
25611
  if (!isStale()) {
25603
- shared.modelSelectionState = await applyRequestedModel({
25604
- state: shared.modelSelectionState,
25605
- acpSessionId: started.sessionId,
25606
- requestedModelId: input2.requestedModelId,
25607
- expectedGeneration: started.generation
25608
- });
25612
+ try {
25613
+ const applied = await applyRequestedModel({
25614
+ state: shared.modelSelectionState,
25615
+ acpSessionId: sessionId,
25616
+ setup: shared.startResult?.sessionSetupResult ?? {},
25617
+ requestedModelId: input2.requestedModelId,
25618
+ expectedGeneration: started.generation
25619
+ });
25620
+ shared.modelSelectionState = applied.state;
25621
+ sessionId = applied.sessionId;
25622
+ record2.sessionId = applied.sessionId;
25623
+ } catch (error62) {
25624
+ const failureCategory = readAcpFailureCategory(error62);
25625
+ if (failureCategory === "model-unavailable") {
25626
+ const errorMessage = error62 instanceof Error && error62.message.trim().length > 0 ? error62.message : "Requested model is not in this agent's catalog. This turn was not sent.";
25627
+ settle2({
25628
+ turnId,
25629
+ stopReason: "error",
25630
+ state: "failed",
25631
+ errorMessage,
25632
+ failureCategory
25633
+ });
25634
+ throw error62 instanceof Error ? withFailureCategory(error62, failureCategory) : error62;
25635
+ }
25636
+ throw error62;
25637
+ }
25609
25638
  }
25610
25639
  failIfDisposed();
25611
25640
  if (isStale()) {
@@ -25616,7 +25645,7 @@ function createExecutorPrompt({
25616
25645
  let written = false;
25617
25646
  try {
25618
25647
  response = await sendPromptRpc(
25619
- started.sessionId,
25648
+ sessionId,
25620
25649
  promptParts,
25621
25650
  () => {
25622
25651
  written = true;
@@ -25666,7 +25695,7 @@ function createExecutorPrompt({
25666
25695
  if (hooks.readContextWindow !== void 0) {
25667
25696
  try {
25668
25697
  contextWindow = await hooks.readContextWindow({
25669
- sessionId: started.sessionId,
25698
+ sessionId,
25670
25699
  promptResponse: response
25671
25700
  }) ?? void 0;
25672
25701
  } catch {
@@ -26341,6 +26370,56 @@ function acpInitializePayload(clientInfo, overrides) {
26341
26370
  };
26342
26371
  }
26343
26372
 
26373
+ // ../../packages/ycodium-acp-executor/src/refreshDeadline.ts
26374
+ var MODEL_CATALOG_REFRESH_TIMEOUT_MS = 9e4;
26375
+
26376
+ // ../../packages/ycodium-acp-executor/src/model-send.ts
26377
+ function judgeRequestedModel(input2) {
26378
+ const requested = normalizeModelId(input2.requestedModelId);
26379
+ const current = input2.state.currentModelId;
26380
+ if (requested === void 0 || requested === current) return { kind: "keep" };
26381
+ const hasCatalog = input2.state.advertisedModelValues.length > 0;
26382
+ const hasCurrent = current !== void 0;
26383
+ if (!hasCatalog && !hasCurrent) return { kind: "keep" };
26384
+ const canSwitch = input2.state.strategy === "session/set_model" && input2.state.sessionModelsAdvertised || input2.state.strategy === "config-option" && input2.state.modelConfigId !== void 0;
26385
+ const value = hasCatalog ? matchAdvertisedModelValue(requested, input2.state.advertisedModelValues) : void 0;
26386
+ if (value !== void 0 && value === current) return { kind: "keep" };
26387
+ if (value !== void 0 && canSwitch) return { kind: "switch", value };
26388
+ return input2.refreshed ? { kind: "unavailable" } : { kind: "refresh" };
26389
+ }
26390
+ function modelUnavailableError(requestedModelId, currentModelId) {
26391
+ const current = currentModelId ?? "its own model";
26392
+ return withFailureCategory(
26393
+ new Error(
26394
+ `Requested model "${requestedModelId}" is not in this agent's catalog; it is on "${current}". This turn was not sent. Refresh the model list in this agent's settings and try again.`
26395
+ ),
26396
+ "model-unavailable"
26397
+ );
26398
+ }
26399
+ var ModelCatalogRefreshTimeoutError = class extends Error {
26400
+ constructor() {
26401
+ super("model catalog refresh timed out");
26402
+ this.name = "ModelCatalogRefreshTimeoutError";
26403
+ }
26404
+ };
26405
+ function modelRefreshTimedOutError(requestedModelId) {
26406
+ return withFailureCategory(
26407
+ new Error(
26408
+ `Refreshing this agent's model list timed out, so this turn was not sent. Requested model "${requestedModelId}".`
26409
+ ),
26410
+ "model-unavailable"
26411
+ );
26412
+ }
26413
+ function modelRefreshFailedError(requestedModelId, error62) {
26414
+ const detail = error62 instanceof Error && error62.message.trim().length > 0 ? error62.message : "the model list could not be read";
26415
+ return withFailureCategory(
26416
+ new Error(
26417
+ `Refreshing this agent's model list failed (${detail}), so this turn was not sent. Requested model "${requestedModelId}".`
26418
+ ),
26419
+ "model-unavailable"
26420
+ );
26421
+ }
26422
+
26344
26423
  // ../../packages/ycodium-acp-executor/src/runtime-mode.ts
26345
26424
  function isRecord5(value) {
26346
26425
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -26424,43 +26503,131 @@ function createExecutorSession({
26424
26503
  failIfDisposed,
26425
26504
  shared
26426
26505
  }) {
26427
- const applyRequestedModel = async (input2) => {
26428
- const requestedModelId = normalizeModelId(input2.requestedModelId);
26429
- if (requestedModelId === void 0 || requestedModelId === input2.state.currentModelId || input2.state.strategy === "session/set_model" && !input2.state.sessionModelsAdvertised || input2.state.strategy === "config-option" && input2.state.modelConfigId === void 0) {
26430
- return input2.state;
26431
- }
26432
- const value = matchAdvertisedModelValue(requestedModelId, input2.state.advertisedModelValues);
26433
- if (value === void 0) {
26434
- if (requestedModelId !== shared.lastUnmatchedModelId) {
26435
- shared.lastUnmatchedModelId = requestedModelId;
26436
- options.log?.info("requested model is not in the agent catalog; keeping its own model", {
26437
- requestedModelId,
26438
- currentModelId: input2.state.currentModelId,
26439
- advertisedModelValues: input2.state.advertisedModelValues
26440
- });
26441
- }
26442
- return input2.state;
26506
+ const openFreshSession = async (expectedGeneration) => {
26507
+ const initialized = shared.initializeResult;
26508
+ if (initialized === void 0) {
26509
+ throw new Error("no handshake");
26443
26510
  }
26444
- if (value === input2.state.currentModelId) {
26445
- return input2.state;
26511
+ const mcpServers = filterMcpServersByAgentCapabilities(
26512
+ options.mcpServers ?? [],
26513
+ initialized.agentCapabilities
26514
+ );
26515
+ const additionalDirectories = options.additionalDirectories !== void 0 && options.additionalDirectories.length > 0 ? { additionalDirectories: options.additionalDirectories } : {};
26516
+ const created = await connection.request("session/new", {
26517
+ cwd: options.cwd,
26518
+ mcpServers,
26519
+ ...additionalDirectories
26520
+ });
26521
+ if (!isLive(expectedGeneration)) {
26522
+ throw new AcpExecutorDisposedError();
26523
+ }
26524
+ if (created.sessionId === void 0 || created.sessionId.length === 0) {
26525
+ throw new Error("session/new did not return a session id");
26446
26526
  }
26527
+ return { sessionId: created.sessionId, setup: created };
26528
+ };
26529
+ const switchModel = async (input2) => {
26447
26530
  let configOptions = input2.state.configOptions;
26448
26531
  if (input2.state.strategy === "config-option") {
26449
26532
  const switched = await connection.request(
26450
26533
  "session/set_config_option",
26451
- { sessionId: input2.acpSessionId, configId: input2.state.modelConfigId, value }
26534
+ { sessionId: input2.sessionId, configId: input2.state.modelConfigId, value: input2.value }
26452
26535
  );
26453
26536
  configOptions = switched?.configOptions ?? configOptions;
26454
26537
  } else {
26455
26538
  await connection.request("session/set_model", {
26456
- sessionId: input2.acpSessionId,
26457
- modelId: value
26539
+ sessionId: input2.sessionId,
26540
+ modelId: input2.value
26458
26541
  });
26459
26542
  }
26460
26543
  if (!isLive(input2.expectedGeneration)) {
26461
26544
  throw new AcpExecutorDisposedError();
26462
26545
  }
26463
- return { ...input2.state, currentModelId: value, configOptions };
26546
+ return { ...input2.state, currentModelId: input2.value, configOptions };
26547
+ };
26548
+ const adoptReopenedSession = (applied) => {
26549
+ if (shared.sessionId === void 0 && shared.startResult === void 0) return;
26550
+ const setup = applied.setup;
26551
+ shared.sessionId = applied.sessionId;
26552
+ shared.modelSelectionState = applied.state;
26553
+ shared.modeState = parseSessionModeState(setup);
26554
+ if (shared.startResult !== void 0) {
26555
+ shared.startResult = {
26556
+ ...shared.startResult,
26557
+ sessionId: applied.sessionId,
26558
+ sessionSetupResult: setup,
26559
+ modelConfigId: extractModelConfigId(setup),
26560
+ modelId: applied.state.currentModelId
26561
+ };
26562
+ }
26563
+ };
26564
+ const applyRequestedModel = async (input2) => {
26565
+ async function applyOnce(state, sessionId, setup, refreshed) {
26566
+ const requestedModelId = normalizeModelId(input2.requestedModelId) ?? "";
26567
+ const judgment = judgeRequestedModel({
26568
+ state,
26569
+ requestedModelId: input2.requestedModelId,
26570
+ refreshed
26571
+ });
26572
+ if (judgment.kind === "keep") return { state, sessionId, setup };
26573
+ if (judgment.kind === "unavailable") {
26574
+ throw modelUnavailableError(requestedModelId, state.currentModelId);
26575
+ }
26576
+ if (judgment.kind === "switch") {
26577
+ return {
26578
+ state: await switchModel({
26579
+ state,
26580
+ sessionId,
26581
+ value: judgment.value,
26582
+ expectedGeneration: input2.expectedGeneration
26583
+ }),
26584
+ sessionId,
26585
+ setup
26586
+ };
26587
+ }
26588
+ return refreshAndRejudge(requestedModelId);
26589
+ }
26590
+ async function refreshAndRejudge(requestedModelId) {
26591
+ const timeoutMs = options.modelCatalogRefreshTimeoutMs ?? MODEL_CATALOG_REFRESH_TIMEOUT_MS;
26592
+ let timedOut = false;
26593
+ const timeoutError = new ModelCatalogRefreshTimeoutError();
26594
+ let timer;
26595
+ const work = (async () => {
26596
+ await options.refreshCatalogForSend?.();
26597
+ if (timedOut) throw timeoutError;
26598
+ const reopened = await openFreshSession(input2.expectedGeneration);
26599
+ if (timedOut) throw timeoutError;
26600
+ const next = modelSelectionStateFromSetup(reopened.setup);
26601
+ const applied = await applyOnce(next, reopened.sessionId, reopened.setup, true);
26602
+ if (timedOut) throw timeoutError;
26603
+ adoptReopenedSession(applied);
26604
+ return applied;
26605
+ })();
26606
+ void work.then(
26607
+ () => void 0,
26608
+ () => void 0
26609
+ );
26610
+ const deadline = new Promise((_resolve, reject) => {
26611
+ timer = setTimeout(() => {
26612
+ timedOut = true;
26613
+ connection.abandonPending(timeoutError);
26614
+ reject(timeoutError);
26615
+ }, timeoutMs);
26616
+ });
26617
+ try {
26618
+ return await Promise.race([work, deadline]);
26619
+ } catch (error62) {
26620
+ if (error62 instanceof AcpExecutorDisposedError) throw error62;
26621
+ if (timedOut || error62 instanceof ModelCatalogRefreshTimeoutError) {
26622
+ throw modelRefreshTimedOutError(requestedModelId);
26623
+ }
26624
+ if (readAcpFailureCategory(error62) === "model-unavailable") throw error62;
26625
+ throw modelRefreshFailedError(requestedModelId, error62);
26626
+ } finally {
26627
+ if (timer !== void 0) clearTimeout(timer);
26628
+ }
26629
+ }
26630
+ return applyOnce(input2.state, input2.acpSessionId, input2.setup, false);
26464
26631
  };
26465
26632
  const startOnce = async (expectedGeneration) => {
26466
26633
  const initializePayload = acpInitializePayload(options.clientInfo, options.clientCapabilities);
@@ -26529,12 +26696,17 @@ function createExecutorSession({
26529
26696
  categories: unhandledCategories
26530
26697
  });
26531
26698
  }
26532
- const nextModelSelectionState = await applyRequestedModel({
26533
- state: modelSelectionStateFromSetup(setup),
26699
+ const openedSetup = setup;
26700
+ const appliedModel = await applyRequestedModel({
26701
+ state: modelSelectionStateFromSetup(openedSetup),
26534
26702
  acpSessionId: nextSessionId,
26703
+ setup: openedSetup,
26535
26704
  requestedModelId: options.requestedModelId,
26536
26705
  expectedGeneration
26537
26706
  });
26707
+ nextSessionId = appliedModel.sessionId;
26708
+ setup = appliedModel.setup;
26709
+ const nextModelSelectionState = appliedModel.state;
26538
26710
  await applyRequestedEffort({
26539
26711
  connection,
26540
26712
  log: options.log,
@@ -27069,7 +27241,6 @@ function createAcpExecutor(options) {
27069
27241
  currentModelId: void 0,
27070
27242
  advertisedModelValues: []
27071
27243
  },
27072
- lastUnmatchedModelId: void 0,
27073
27244
  initializeResult: void 0,
27074
27245
  startResult: void 0,
27075
27246
  startInFlight: void 0,
@@ -27724,7 +27895,9 @@ import * as NodeOS2 from "node:os";
27724
27895
  import * as NodePath3 from "node:path";
27725
27896
  import {
27726
27897
  describeExecutorSpawnFailure,
27898
+ ExecutorSpawnAbortedError,
27727
27899
  ExecutorSpawnRefusedError,
27900
+ raceAbort,
27728
27901
  REASONING_OPTION_LABEL
27729
27902
  } from "@ycodium-ai/plugin-api/executor";
27730
27903
  import { nodeStreamsFromHandle } from "@ycodium-ai/plugin-api/process-stdin";
@@ -27807,22 +27980,28 @@ async function probeAcpModelsViaSession(input2) {
27807
27980
  args: tokenizeLaunchArgs(recipe.launchArgs),
27808
27981
  cwd: probeCwd,
27809
27982
  owner: { bindingId: host.bindingId },
27810
- ...Object.keys(env).length > 0 ? { env } : {}
27983
+ ...Object.keys(env).length > 0 ? { env } : {},
27984
+ ...input2.signal === void 0 ? {} : { signal: input2.signal }
27811
27985
  });
27812
27986
  } catch (error62) {
27813
- if (error62 instanceof ExecutorSpawnRefusedError) throw error62;
27987
+ if (error62 instanceof ExecutorSpawnRefusedError || error62 instanceof ExecutorSpawnAbortedError) {
27988
+ throw error62;
27989
+ }
27814
27990
  return { kind: "not-started", ...describeExecutorSpawnFailure(error62) };
27815
27991
  }
27816
27992
  let handshake;
27817
27993
  try {
27818
27994
  const { readable, writable } = nodeStreamsFromHandle(handle);
27819
- handshake = await openAcpProbeHandshake({
27820
- readable,
27821
- writable,
27822
- clientInfo: CLIENT_INFO,
27823
- requestTimeoutMs: PROBE_REQUEST_TIMEOUT_MS
27824
- });
27825
- const setup = await probeSessionSetup(handshake, probeCwd);
27995
+ handshake = await raceAbort(
27996
+ openAcpProbeHandshake({
27997
+ readable,
27998
+ writable,
27999
+ clientInfo: CLIENT_INFO,
28000
+ requestTimeoutMs: PROBE_REQUEST_TIMEOUT_MS
28001
+ }),
28002
+ input2.signal
28003
+ );
28004
+ const setup = await raceAbort(probeSessionSetup(handshake, probeCwd), input2.signal);
27826
28005
  return {
27827
28006
  kind: "answered",
27828
28007
  version: acpAgentVersion(handshake.initializeResult),
@@ -27847,6 +28026,43 @@ async function probeAcpModelsViaSession(input2) {
27847
28026
  await handle.kill("SIGTERM").catch(() => void 0);
27848
28027
  }
27849
28028
  }
28029
+ async function probeAcpAgentVersion(input2) {
28030
+ if (input2.recipe.binaryPath.length === 0) return null;
28031
+ const probeCwd = acpProbeSessionCwd(input2.host.instanceId);
28032
+ try {
28033
+ NodeFS2.mkdirSync(probeCwd, { recursive: true });
28034
+ } catch {
28035
+ return null;
28036
+ }
28037
+ let handle;
28038
+ try {
28039
+ handle = await input2.host.process.spawn({
28040
+ program: input2.recipe.binaryPath,
28041
+ args: tokenizeLaunchArgs(input2.recipe.launchArgs),
28042
+ cwd: probeCwd,
28043
+ owner: { bindingId: input2.host.bindingId },
28044
+ ...Object.keys(input2.env).length > 0 ? { env: input2.env } : {}
28045
+ });
28046
+ } catch {
28047
+ return null;
28048
+ }
28049
+ let handshake;
28050
+ try {
28051
+ const { readable, writable } = nodeStreamsFromHandle(handle);
28052
+ handshake = await openAcpProbeHandshake({
28053
+ readable,
28054
+ writable,
28055
+ clientInfo: CLIENT_INFO,
28056
+ requestTimeoutMs: PROBE_REQUEST_TIMEOUT_MS
28057
+ });
28058
+ return acpAgentVersion(handshake.initializeResult);
28059
+ } catch {
28060
+ return acpAgentVersion(handshake?.initializeResult);
28061
+ } finally {
28062
+ await handshake?.close().catch(() => void 0);
28063
+ await handle.kill("SIGTERM").catch(() => void 0);
28064
+ }
28065
+ }
27850
28066
 
27851
28067
  // src/runtimeModeApproval.ts
27852
28068
  function acpRuntimeModeAutoApproves(runtimeMode, toolKind) {
@@ -27956,7 +28172,7 @@ var createAcpInstance = (config2, host) => {
27956
28172
  events.close();
27957
28173
  });
27958
28174
  const instance = {
27959
- probe: async () => {
28175
+ probe: async (signal) => {
27960
28176
  if (recipe.binaryPath.length === 0) {
27961
28177
  return {
27962
28178
  installed: false,
@@ -27966,8 +28182,17 @@ var createAcpInstance = (config2, host) => {
27966
28182
  detail: "ACP program path is not configured"
27967
28183
  };
27968
28184
  }
28185
+ if (signal?.aborted) throw new Error("aborted");
27969
28186
  const env = buildSpawnEnv(recipe, recipe.homePath);
27970
- const outcome = await probeAcpModelsViaSession({ recipe, host, env });
28187
+ const outcome = await raceAbort2(
28188
+ probeAcpModelsViaSession({
28189
+ recipe,
28190
+ host,
28191
+ env,
28192
+ ...signal === void 0 ? {} : { signal }
28193
+ }),
28194
+ signal
28195
+ );
27971
28196
  switch (outcome.kind) {
27972
28197
  case "not-started":
27973
28198
  return {
@@ -28007,6 +28232,11 @@ var createAcpInstance = (config2, host) => {
28007
28232
  };
28008
28233
  }
28009
28234
  },
28235
+ readCliVersion: () => recipe.binaryPath.length === 0 ? Promise.resolve(null) : probeAcpAgentVersion({
28236
+ recipe,
28237
+ host,
28238
+ env: buildSpawnEnv(recipe, recipe.homePath)
28239
+ }),
28010
28240
  // 暂不处理 input.readOnlyDirectories(有意为之):acp 经共享的 ycodium-acp-executor 起会话,
28011
28241
  // 还没用真机确认这家 CLI 对 cwd 之外的读怎么处理、要不要放行。确认之后再接这一栏。
28012
28242
  startSession: async (input2, signal) => {
@@ -28220,7 +28450,7 @@ var executors = [
28220
28450
  ];
28221
28451
  var index_default = definePlugin({
28222
28452
  id: "agent-acp",
28223
- apiVersion: 29,
28453
+ apiVersion: 31,
28224
28454
  executors,
28225
28455
  activate(ctx) {
28226
28456
  ctx.log.info("generic ACP executor ready");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/agent-acp",
3
- "version": "0.2.3252",
3
+ "version": "0.2.3265",
4
4
  "description": "Generic ACP executor: user-supplied CLI over Agent Client Protocol stdio.",
5
5
  "keywords": [
6
6
  "ycodium",
@@ -21,9 +21,9 @@
21
21
  "@types/node": "24.12.4",
22
22
  "esbuild": "0.28.1",
23
23
  "vite-plus": "0.2.2",
24
- "@ycodium-ai/plugin-api": "0.2.3252",
25
- "ycodium-executor-testing": "0.0.0",
26
- "ycodium-acp-executor": "0.2.3252"
24
+ "@ycodium-ai/plugin-api": "0.2.3265",
25
+ "ycodium-acp-executor": "0.2.3265",
26
+ "ycodium-executor-testing": "0.0.0"
27
27
  },
28
28
  "ycodium": {
29
29
  "plugin": "./dist/index.js",