paseo-acp-agy 1.2.15 → 1.2.16

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.
@@ -10,6 +10,8 @@ export declare class ACPServer extends EventEmitter {
10
10
  private skipNarration;
11
11
  private rl;
12
12
  private isRunning;
13
+ private pendingRequests;
14
+ private nextRequestId;
13
15
  constructor(options?: {
14
16
  input?: Readable;
15
17
  output?: Writable;
@@ -22,6 +24,7 @@ export declare class ACPServer extends EventEmitter {
22
24
  private sendNotification;
23
25
  private sendSuccess;
24
26
  private sendError;
27
+ sendRequest<T = any>(method: string, params: unknown): Promise<T>;
25
28
  private publishCommands;
26
29
  private publishUsageUpdate;
27
30
  private sessionState;
@@ -29,6 +29,8 @@ export class ACPServer extends EventEmitter {
29
29
  skipNarration;
30
30
  rl = null;
31
31
  isRunning = false;
32
+ pendingRequests = new Map();
33
+ nextRequestId = 1;
32
34
  constructor(options = {}) {
33
35
  super();
34
36
  this.input = options.input || process.stdin;
@@ -98,6 +100,13 @@ export class ACPServer extends EventEmitter {
98
100
  };
99
101
  this.send(response);
100
102
  }
103
+ sendRequest(method, params) {
104
+ return new Promise((resolve, reject) => {
105
+ const id = this.nextRequestId++;
106
+ this.pendingRequests.set(id, { resolve, reject });
107
+ this.send({ jsonrpc: "2.0", id, method, params });
108
+ });
109
+ }
101
110
  publishCommands(sessionId) {
102
111
  this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
103
112
  sessionId,
@@ -198,6 +207,20 @@ export class ACPServer extends EventEmitter {
198
207
  const isNotification = id === undefined || id === null;
199
208
  const params = (parsed.params || {});
200
209
  logger.debug("Received ACP message", { method, isNotification, id });
210
+ // Handle responses to requests initiated by the server (e.g. session/request_permission)
211
+ if (id !== undefined && !method) {
212
+ const pending = this.pendingRequests.get(id);
213
+ if (pending) {
214
+ this.pendingRequests.delete(id);
215
+ if (parsed.error) {
216
+ pending.reject(parsed.error);
217
+ }
218
+ else {
219
+ pending.resolve(parsed.result);
220
+ }
221
+ return;
222
+ }
223
+ }
201
224
  try {
202
225
  switch (method) {
203
226
  case ACP_METHODS.INITIALIZE: {
@@ -559,13 +582,70 @@ export class ACPServer extends EventEmitter {
559
582
  rawInput: toolCall.rawInput,
560
583
  },
561
584
  });
562
- if (toolCall.questionCard) {
563
- this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
585
+ if (toolName === "ask_question") {
586
+ const rawParams = step.tool_info.parameters || {};
587
+ const rawQuestions = Array.isArray(rawParams.questions)
588
+ ? rawParams.questions
589
+ : [];
590
+ const options = [];
591
+ for (let qIdx = 0; qIdx < rawQuestions.length; qIdx++) {
592
+ const q = rawQuestions[qIdx];
593
+ const opts = Array.isArray(q.options) ? q.options : [];
594
+ for (let optIdx = 0; optIdx < opts.length; optIdx++) {
595
+ const opt = opts[optIdx];
596
+ const clean = typeof opt === "string" ? opt.replace(/^\(Recommended\)\s*/i, "").trim() : String(opt);
597
+ const isRec = typeof opt === "string" && (opt.includes("(Recommended)") || opt.includes("(Recomendado)"));
598
+ options.push({
599
+ optionId: `q${qIdx + 1}_opt${optIdx + 1}`,
600
+ name: `${clean}${isRec ? " (Recomendado)" : ""}`,
601
+ kind: "allow_once",
602
+ });
603
+ }
604
+ }
605
+ options.push({
606
+ optionId: "cancel",
607
+ name: "Cancelar",
608
+ kind: "reject_once",
609
+ });
610
+ const promptText = rawQuestions.map((q) => q.question).filter(Boolean).join(" / ") || "Pergunta do Assistente";
611
+ this.sendRequest("session/request_permission", {
564
612
  sessionId: session.id,
565
- update: {
566
- sessionUpdate: "agent_message_chunk",
567
- content: { type: "text", text: `\n\n${toolCall.questionCard}\n\n` },
613
+ toolCall: {
614
+ toolCallId: toolCall.toolCallId,
615
+ title: toolCall.title,
616
+ kind: "question",
617
+ content: [{ type: "text", text: promptText }],
618
+ rawInput: toolCall.rawInput,
568
619
  },
620
+ options,
621
+ questions: rawQuestions,
622
+ }).then((permRes) => {
623
+ logger.info("Interactive question answered in Paseo", { permRes });
624
+ const outcome = permRes?.outcome;
625
+ let answerText = "";
626
+ if (outcome?._meta?.answers && typeof outcome._meta.answers === "object") {
627
+ answerText = Object.entries(outcome._meta.answers)
628
+ .map(([k, v]) => `• **${k}**: ${v}`)
629
+ .join("\n");
630
+ }
631
+ else if (outcome?.optionId && outcome.optionId !== "cancel") {
632
+ const matched = options.find((o) => o.optionId === outcome.optionId);
633
+ answerText = matched ? matched.name : outcome.optionId;
634
+ }
635
+ if (answerText) {
636
+ this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
637
+ sessionId: session.id,
638
+ update: {
639
+ sessionUpdate: "agent_message_chunk",
640
+ content: {
641
+ type: "text",
642
+ text: `\n\n> 💡 **Opção Selecionada:**\n${answerText}\n\n`,
643
+ },
644
+ },
645
+ });
646
+ }
647
+ }).catch((err) => {
648
+ logger.debug("Interactive question cancelled or dismissed", { error: err.message });
569
649
  });
570
650
  }
571
651
  }
@@ -1195,6 +1195,63 @@ ${braceIndent}}`;
1195
1195
  }
1196
1196
  }
1197
1197
  }
1198
+ // Patch mapPermissionRequest for native QuestionFormCard support in Paseo UI
1199
+ if (!acpCode.includes("snapshot?.kind === \"question\" || params?.kind === \"question\"")) {
1200
+ const oldMapPermRegex = /function\s+mapPermissionRequest\s*\([^)]*\)\s*\{[\s\S]*?const\s+kind\s*=\s*snapshot\.kind\s*===\s*"switch_mode"\s*\?\s*"mode"\s*:\s*"tool";/m;
1201
+ const newMapPerm = `function mapPermissionRequest(provider, requestId, params, snapshot) {
1202
+ if (snapshot?.kind === "question" || params?.kind === "question" || (params?.questions && Array.isArray(params.questions)) || (snapshot?.rawInput?.questions && Array.isArray(snapshot?.rawInput?.questions))) {
1203
+ const rawQ = params.questions || snapshot?.rawInput?.questions || [];
1204
+ const questions = rawQ.map((q, idx) => ({
1205
+ header: q.header || q.title || ("Question " + (idx + 1)),
1206
+ question: q.question || q.prompt || "Escolha uma opção:",
1207
+ options: (q.options || []).map((opt) => typeof opt === "string" ? {
1208
+ label: opt.replace(/^\\(Recommended\\)\\s*/i, "").replace(/^\\(Recomendado\\)\\s*/i, "").trim(),
1209
+ description: (opt.includes("(Recommended)") || opt.includes("(Recomendado)")) ? "Recomendado" : undefined
1210
+ } : { label: opt.label || String(opt), description: opt.description }),
1211
+ multiSelect: q.multiSelect === true || q.is_multi_select === true || q.multiple === true,
1212
+ allowOther: true,
1213
+ allowEmpty: false,
1214
+ }));
1215
+ return {
1216
+ id: requestId,
1217
+ provider,
1218
+ name: "question",
1219
+ kind: "question",
1220
+ title: params.toolCall?.title ?? snapshot?.title ?? "Pergunta do Assistente",
1221
+ input: { questions },
1222
+ actions: (params.options || []).map((option) => ({
1223
+ id: option.optionId,
1224
+ label: option.name,
1225
+ behavior: option.kind.startsWith("allow") ? "allow" : "deny",
1226
+ })),
1227
+ metadata: {
1228
+ toolCallId: params.toolCall?.toolCallId,
1229
+ rawRequest: params,
1230
+ options: params.options,
1231
+ },
1232
+ };
1233
+ }
1234
+ const kind = snapshot.kind === "switch_mode" ? "mode" : "tool";`;
1235
+ if (oldMapPermRegex.test(acpCode)) {
1236
+ acpCode = acpCode.replace(oldMapPermRegex, newMapPerm);
1237
+ acpModified = true;
1238
+ changes.push(`Patched ${acpFile} for interactive QuestionFormCard support`);
1239
+ }
1240
+ }
1241
+ // Patch respondToPermission for QuestionFormCard updatedInput.answers
1242
+ if (!acpCode.includes("response.updatedInput?.answers")) {
1243
+ const oldRespondMatch = acpCode.match(/this\.pendingPermissions\.delete\(requestId\);[\s\S]*?pending\.resolve\([\s\S]*?\);\s*\r?\n\s*this\.pushEvent/m);
1244
+ if (oldRespondMatch) {
1245
+ const newRespond = `this.pendingPermissions.delete(requestId);
1246
+ const outcome = response.updatedInput?.answers
1247
+ ? { outcome: "selected", optionId: selectedOption?.optionId || "form_submit", _meta: { answers: response.updatedInput.answers } }
1248
+ : (selectedOption ? { outcome: { outcome: "selected", optionId: selectedOption.optionId } } : { outcome: { outcome: "cancelled" } });
1249
+ pending.resolve(response.updatedInput?.answers ? { outcome } : (selectedOption ? { outcome: { outcome: "selected", optionId: selectedOption.optionId } } : { outcome: { outcome: "cancelled" } }));
1250
+ this.pushEvent`;
1251
+ acpCode = acpCode.replace(oldRespondMatch[0], newRespond);
1252
+ acpModified = true;
1253
+ }
1254
+ }
1198
1255
  if (acpModified) {
1199
1256
  fs.writeFileSync(acpFile, acpCode, "utf-8");
1200
1257
  changes.push(`Patched ${acpFile} for context-window token telemetry and usage updates`);
@@ -135,7 +135,7 @@ export interface SessionInfo {
135
135
  updatedAt?: string | null;
136
136
  _meta?: Record<string, unknown> | null;
137
137
  }
138
- export type ToolKind = "read" | "edit" | "execute" | "fetch" | "search" | "other";
138
+ export type ToolKind = "read" | "edit" | "execute" | "fetch" | "search" | "question" | "other";
139
139
  export declare function mapToolNameToKind(toolName: string): ToolKind;
140
140
  export declare function extractPromptText(promptInput: unknown): string;
141
141
  export interface ModelDefinition {
@@ -102,7 +102,7 @@ export function normalizeToolCall(toolName, rawParams = {}, stepIndex) {
102
102
  return {
103
103
  toolCallId,
104
104
  title: "❓ Pergunta do Assistente",
105
- kind: "other",
105
+ kind: "question",
106
106
  rawInput: normalizedParams,
107
107
  questionCard: questionCard || undefined,
108
108
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paseo-acp-agy",
3
- "version": "1.2.15",
3
+ "version": "1.2.16",
4
4
  "description": "ACP (Agent Client Protocol) provider for Google Antigravity in Paseo and Zed",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",