@ycodium-ai/agent-acp 0.2.1991 → 0.2.2014

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 +217 -3
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -24996,6 +24996,11 @@ var defaultVendorHooks = {
24996
24996
 
24997
24997
  // ../../packages/ycodium-acp-executor/src/executor.ts
24998
24998
  var DEFAULT_CANCEL_GRACE_MS = 5e3;
24999
+ var userInputAnswerOptionId = (question, answers) => {
25000
+ const answer = answers[question.id];
25001
+ const selected = typeof answer === "string" ? answer : Array.isArray(answer) && answer.length === 1 && typeof answer[0] === "string" ? answer[0] : void 0;
25002
+ return selected !== void 0 && question.options.some((option) => option.id === selected) ? selected : void 0;
25003
+ };
24999
25004
  function defaultNowIso() {
25000
25005
  return (/* @__PURE__ */ new Date()).toISOString();
25001
25006
  }
@@ -25046,6 +25051,14 @@ function createAcpExecutor(options) {
25046
25051
  singleResolver: false
25047
25052
  });
25048
25053
  const pendingUserInputs = /* @__PURE__ */ new Map();
25054
+ let pendingSettlement;
25055
+ const settledPromptKeys = [];
25056
+ const SETTLED_PROMPT_KEY_LIMIT = 32;
25057
+ const rememberSettledPromptKey = (key) => {
25058
+ if (settledPromptKeys.includes(key)) return;
25059
+ settledPromptKeys.push(key);
25060
+ if (settledPromptKeys.length > SETTLED_PROMPT_KEY_LIMIT) settledPromptKeys.shift();
25061
+ };
25049
25062
  const toolCalls = /* @__PURE__ */ new Map();
25050
25063
  let assistantSegment = { nextSegmentIndex: 0 };
25051
25064
  let modeState;
@@ -25221,10 +25234,118 @@ function createAcpExecutor(options) {
25221
25234
  emitResolved(entry, decision);
25222
25235
  }
25223
25236
  };
25237
+ const emitUserInputResolved = (pending, answers) => {
25238
+ emit({
25239
+ type: "user-input.resolved",
25240
+ ...stamp(),
25241
+ provider: options.provider,
25242
+ threadId: options.threadId,
25243
+ ...pending.turnId !== void 0 ? { turnId: pending.turnId } : {},
25244
+ requestId: pending.requestId,
25245
+ payload: { answers }
25246
+ });
25247
+ };
25248
+ const abortPendingSettlement = (reason) => {
25249
+ const waiting = pendingSettlement;
25250
+ if (waiting === void 0) return;
25251
+ pendingSettlement = void 0;
25252
+ waiting.settle({ error: new AcpExecutorRequestError("session/prompt", reason) });
25253
+ };
25254
+ const settleAllUserInputs = () => {
25255
+ for (const pending of pendingUserInputs.values()) {
25256
+ if (pending.settled) continue;
25257
+ pending.settled = true;
25258
+ pending.resolve({ kind: "cancelled" });
25259
+ }
25260
+ pendingUserInputs.clear();
25261
+ };
25262
+ const settleFromNotification = (method, params) => {
25263
+ const hook = hooks.promptSettlement;
25264
+ if (hook === void 0 || pendingSettlement === void 0) return false;
25265
+ const signal = hook.fromNotification(method, params);
25266
+ if (signal === void 0) return false;
25267
+ const waiting = pendingSettlement;
25268
+ if (signal.sessionId !== waiting.sessionId) return false;
25269
+ if (signal.promptKey !== void 0 && signal.promptKey !== waiting.promptKey) {
25270
+ return settledPromptKeys.includes(signal.promptKey);
25271
+ }
25272
+ pendingSettlement = void 0;
25273
+ rememberSettledPromptKey(waiting.promptKey);
25274
+ if (signal.outcome === "error") {
25275
+ waiting.settle({
25276
+ error: new AcpExecutorRequestError(
25277
+ "session/prompt",
25278
+ signal.detail ?? "prompt failed without an error detail"
25279
+ )
25280
+ });
25281
+ } else {
25282
+ waiting.settle({
25283
+ response: { stopReason: signal.stopReason ?? "end_turn" }
25284
+ });
25285
+ }
25286
+ return true;
25287
+ };
25288
+ const askUserInput = async (input2) => {
25289
+ failIfDisposed();
25290
+ const requestId = randomId();
25291
+ const { promise: settlement, resolve: settle2 } = Promise.withResolvers();
25292
+ const pending = {
25293
+ requestId,
25294
+ questions: input2.questions,
25295
+ turnId: activeTurnId,
25296
+ settled: false,
25297
+ resolve: settle2
25298
+ };
25299
+ pendingUserInputs.set(requestId, pending);
25300
+ emit({
25301
+ type: "user-input.requested",
25302
+ ...stamp(),
25303
+ provider: options.provider,
25304
+ threadId: options.threadId,
25305
+ ...activeTurnId !== void 0 ? { turnId: activeTurnId } : {},
25306
+ requestId,
25307
+ payload: { questions: input2.questions },
25308
+ raw: {
25309
+ source: "acp.jsonrpc",
25310
+ method: input2.method,
25311
+ payload: input2.payload
25312
+ }
25313
+ });
25314
+ const resolution = await settlement;
25315
+ pendingUserInputs.delete(requestId);
25316
+ emitUserInputResolved(pending, resolution.kind === "answered" ? resolution.answers : {});
25317
+ return resolution;
25318
+ };
25319
+ const makeExtensionContext = (method, params) => ({
25320
+ askUserInput: (questions) => askUserInput({ questions, method, payload: params })
25321
+ });
25322
+ const handlePermissionAsUserInput = async (params) => {
25323
+ if (hooks.permissionUserInput === void 0) return void 0;
25324
+ let question;
25325
+ try {
25326
+ question = hooks.permissionUserInput(params);
25327
+ } catch (error62) {
25328
+ throw RequestError.invalidParams({
25329
+ detail: error62 instanceof Error ? error62.message : String(error62)
25330
+ });
25331
+ }
25332
+ if (question === void 0) return void 0;
25333
+ const resolution = await askUserInput({
25334
+ questions: [question],
25335
+ method: CLIENT_METHODS.session_request_permission,
25336
+ payload: params
25337
+ });
25338
+ const optionId = resolution.kind === "answered" ? userInputAnswerOptionId(question, resolution.answers) : void 0;
25339
+ return {
25340
+ outcome: optionId !== void 0 ? { outcome: "selected", optionId } : { outcome: "cancelled" }
25341
+ };
25342
+ };
25224
25343
  const handlePermissionRequest = async (params) => {
25225
25344
  if (!isAcpRequestPermissionParams(params)) {
25226
25345
  throw RequestError.invalidParams({ detail: "Malformed session/request_permission" });
25227
25346
  }
25347
+ const userInputOutcome = await handlePermissionAsUserInput(params);
25348
+ if (userInputOutcome !== void 0) return userInputOutcome;
25228
25349
  const permissionRequest = parsePermissionRequest(params);
25229
25350
  const requestId = randomId();
25230
25351
  const outcome = await new Promise((resolve) => {
@@ -25265,6 +25386,7 @@ function createAcpExecutor(options) {
25265
25386
  handleSessionUpdate(notification.params);
25266
25387
  return;
25267
25388
  }
25389
+ if (settleFromNotification(notification.method, notification.params)) return;
25268
25390
  void hooks.onExtensionNotification?.(notification.method, notification.params);
25269
25391
  },
25270
25392
  onRequest: async (method, params) => {
@@ -25272,7 +25394,12 @@ function createAcpExecutor(options) {
25272
25394
  return handlePermissionRequest(params);
25273
25395
  }
25274
25396
  if (hooks.onExtensionRequest) {
25275
- return hooks.onExtensionRequest(method, params);
25397
+ const handled = await hooks.onExtensionRequest(
25398
+ method,
25399
+ params,
25400
+ makeExtensionContext(method, params)
25401
+ );
25402
+ if (handled !== void 0) return handled;
25276
25403
  }
25277
25404
  throw RequestError.methodNotFound(method);
25278
25405
  }
@@ -25284,12 +25411,24 @@ function createAcpExecutor(options) {
25284
25411
  return;
25285
25412
  }
25286
25413
  settleAllApprovals("cancel");
25414
+ settleAllUserInputs();
25415
+ abortPendingSettlement("connection closed");
25287
25416
  promptWaiters.splice(0).forEach((wake) => wake());
25288
25417
  },
25289
25418
  () => {
25290
25419
  connectionClosed = true;
25291
25420
  }
25292
25421
  );
25422
+ const onInputDead = () => {
25423
+ if (disposed) {
25424
+ return;
25425
+ }
25426
+ settleAllApprovals("cancel");
25427
+ settleAllUserInputs();
25428
+ promptWaiters.splice(0).forEach((wake) => wake());
25429
+ };
25430
+ options.readable.once("end", onInputDead);
25431
+ options.readable.once("error", onInputDead);
25293
25432
  const applyRequestedModel = async (input2) => {
25294
25433
  const requestedModelId = normalizeModelId(input2.requestedModelId);
25295
25434
  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) {
@@ -25546,12 +25685,35 @@ function createAcpExecutor(options) {
25546
25685
  expectedGeneration: started.generation
25547
25686
  });
25548
25687
  closeActiveAssistantSegment(turnId);
25688
+ const settlementHooks = hooks.promptSettlement;
25689
+ const promptKey = settlementHooks === void 0 ? void 0 : randomId();
25549
25690
  const promptRpc = connection.request("session/prompt", {
25550
25691
  sessionId: started.sessionId,
25551
- prompt: promptParts
25692
+ prompt: promptParts,
25693
+ ...settlementHooks !== void 0 && promptKey !== void 0 ? settlementHooks.requestFields(promptKey) : {}
25552
25694
  });
25553
25695
  try {
25554
- response = await promptRpc;
25696
+ if (settlementHooks === void 0 || promptKey === void 0) {
25697
+ response = await promptRpc;
25698
+ } else {
25699
+ const { promise: early, resolve: settleEarly } = Promise.withResolvers();
25700
+ pendingSettlement = { promptKey, sessionId: started.sessionId, settle: settleEarly };
25701
+ try {
25702
+ const outcome = await Promise.race([
25703
+ promptRpc.then((value) => {
25704
+ const key = settlementHooks.responseKey?.(value);
25705
+ if (key !== void 0) rememberSettledPromptKey(key);
25706
+ return { response: value };
25707
+ }),
25708
+ early
25709
+ ]);
25710
+ if (outcome.error !== void 0) throw outcome.error;
25711
+ response = outcome.response;
25712
+ } finally {
25713
+ pendingSettlement = void 0;
25714
+ void promptRpc.catch(() => void 0);
25715
+ }
25716
+ }
25555
25717
  } catch (error62) {
25556
25718
  if (disposed) {
25557
25719
  throw new AcpExecutorDisposedError();
@@ -25573,6 +25735,17 @@ function createAcpExecutor(options) {
25573
25735
  throw new AcpExecutorError("internal", "ACP prompt settled without a turn id");
25574
25736
  }
25575
25737
  const settledTurnId = turnId;
25738
+ let contextWindow;
25739
+ if (hooks.readContextWindow !== void 0) {
25740
+ try {
25741
+ contextWindow = await hooks.readContextWindow({
25742
+ sessionId: started.sessionId,
25743
+ promptResponse: response
25744
+ }) ?? void 0;
25745
+ } catch {
25746
+ contextWindow = void 0;
25747
+ }
25748
+ }
25576
25749
  const finishPrompt = () => {
25577
25750
  if (interruptedTurnIds.has(settledTurnId)) {
25578
25751
  settleTurn({
@@ -25589,6 +25762,21 @@ function createAcpExecutor(options) {
25589
25762
  };
25590
25763
  }
25591
25764
  const stopReason = hooks.completedStopReason?.(response) ?? response.stopReason;
25765
+ if (contextWindow !== void 0) {
25766
+ emit({
25767
+ type: "thread.token-usage.updated",
25768
+ ...stamp(),
25769
+ provider: options.provider,
25770
+ threadId: options.threadId,
25771
+ turnId: settledTurnId,
25772
+ payload: {
25773
+ usage: {
25774
+ usedTokens: contextWindow.usedTokens,
25775
+ ...contextWindow.maxTokens === void 0 ? {} : { maxTokens: contextWindow.maxTokens }
25776
+ }
25777
+ }
25778
+ });
25779
+ }
25592
25780
  settleTurn({
25593
25781
  expectedGeneration: started.generation,
25594
25782
  expectedSessionId: started.sessionId,
@@ -25625,6 +25813,8 @@ function createAcpExecutor(options) {
25625
25813
  interruptedTurnIds.add(targetTurnId);
25626
25814
  }
25627
25815
  settleAllApprovals("cancel");
25816
+ settleAllUserInputs();
25817
+ abortPendingSettlement("turn cancelled");
25628
25818
  const runCancel = async () => {
25629
25819
  if (sessionId) {
25630
25820
  await connection.notify("session/cancel", { sessionId });
@@ -25687,11 +25877,32 @@ function createAcpExecutor(options) {
25687
25877
  };
25688
25878
  apply();
25689
25879
  };
25880
+ const respondToUserInput = async (requestId, answers) => {
25881
+ failIfDisposed();
25882
+ const pending = pendingUserInputs.get(requestId);
25883
+ if (pending === void 0) {
25884
+ throw new AcpExecutorRequestError(
25885
+ "session/request_permission",
25886
+ `Unknown pending user-input request: ${requestId}`
25887
+ );
25888
+ }
25889
+ if (pending.settled) {
25890
+ return;
25891
+ }
25892
+ pending.settled = true;
25893
+ pendingUserInputs.delete(requestId);
25894
+ pending.resolve({
25895
+ kind: "answered",
25896
+ answers: isRecord3(answers) ? answers : {}
25897
+ });
25898
+ };
25690
25899
  const dispose = async () => {
25691
25900
  if (disposed) {
25692
25901
  return;
25693
25902
  }
25694
25903
  settleAllApprovals("cancel");
25904
+ settleAllUserInputs();
25905
+ abortPendingSettlement("executor disposed");
25695
25906
  notifyPromptSettled();
25696
25907
  abortLocalPrompt?.();
25697
25908
  emit({
@@ -25705,6 +25916,8 @@ function createAcpExecutor(options) {
25705
25916
  generation += 1;
25706
25917
  status = "disposed";
25707
25918
  eventStream.close();
25919
+ options.readable.off("end", onInputDead);
25920
+ options.readable.off("error", onInputDead);
25708
25921
  options.readable.pause();
25709
25922
  if (!connectionClosed) {
25710
25923
  await connection.awaitInboundIdle().catch(() => void 0);
@@ -25729,6 +25942,7 @@ function createAcpExecutor(options) {
25729
25942
  prompt,
25730
25943
  cancel,
25731
25944
  respondToRequest,
25945
+ respondToUserInput,
25732
25946
  dispose
25733
25947
  };
25734
25948
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/agent-acp",
3
- "version": "0.2.1991",
3
+ "version": "0.2.2014",
4
4
  "description": "Generic ACP executor: user-supplied CLI over Agent Client Protocol stdio.",
5
5
  "keywords": [
6
6
  "ycodium",
@@ -20,7 +20,7 @@
20
20
  "@types/node": "24.12.4",
21
21
  "esbuild": "0.28.1",
22
22
  "vite-plus": "0.2.2",
23
- "@ycodium-ai/plugin-api": "0.2.1991",
23
+ "@ycodium-ai/plugin-api": "0.2.2014",
24
24
  "ycodium-acp-executor": "0.0.0",
25
25
  "ycodium-acp-protocol": "0.0.0"
26
26
  },