eve-lark 0.4.3 → 0.4.4

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/index.js CHANGED
@@ -610,10 +610,29 @@ function buildStreamingCard(opts) {
610
610
  lines.push(`<font color='grey'>${opts.status}</font>`);
611
611
  }
612
612
  lines.push(opts.buffer.length > 0 ? opts.buffer : "_\u2026_");
613
- return {
614
- config: { ...BASE_CONFIG },
615
- elements: [{ tag: "div", text: { tag: "lark_md", content: lines.join("\n\n") } }]
616
- };
613
+ if (opts.askRequest) {
614
+ lines.push(`**${opts.askRequest.prompt}**`);
615
+ if (opts.askRequest.allowFreeform && (opts.askRequest.options?.length ?? 0) === 0) {
616
+ lines.push(`<font color='grey'>_Reply to this chat with your answer_</font>`);
617
+ }
618
+ }
619
+ const elements = [
620
+ { tag: "div", text: { tag: "lark_md", content: lines.join("\n\n") } }
621
+ ];
622
+ if (opts.askRequest?.options && opts.askRequest.options.length > 0) {
623
+ const buttons = opts.askRequest.options.map((opt) => ({
624
+ tag: "button",
625
+ text: { tag: "plain_text", content: opt.label },
626
+ type: opt.style ?? "default",
627
+ value: {
628
+ [ASK_BUTTON_VALUE_MARKER]: true,
629
+ requestId: opts.askRequest.requestId,
630
+ optionId: opt.id
631
+ }
632
+ }));
633
+ elements.push({ tag: "action", actions: buttons });
634
+ }
635
+ return { config: { ...BASE_CONFIG }, elements };
617
636
  }
618
637
  __name(buildStreamingCard, "buildStreamingCard");
619
638
  function buildErrorCard(message) {
@@ -681,10 +700,12 @@ function buildAskCard(request) {
681
700
  return { config: { ...BASE_CONFIG }, elements };
682
701
  }
683
702
  __name(buildAskCard, "buildAskCard");
684
- function buildAskAnsweredCard(request, selected) {
685
- const elements = [
686
- { tag: "div", text: { tag: "lark_md", content: request.prompt } }
687
- ];
703
+ function buildAskAnsweredCard(request, selected, priorBuffer) {
704
+ const elements = [];
705
+ if (priorBuffer && priorBuffer.length > 0) {
706
+ elements.push({ tag: "div", text: { tag: "lark_md", content: priorBuffer } });
707
+ }
708
+ elements.push({ tag: "div", text: { tag: "lark_md", content: request.prompt } });
688
709
  const summary = selected.kind === "option" ? `<font color='green'>\u2713 ${escapeMarkdown(selected.label)}</font>` : `<font color='green'>\u2713 ${escapeMarkdown(selected.text)}</font>`;
689
710
  elements.push({ tag: "div", text: { tag: "lark_md", content: summary } });
690
711
  return { config: { ...BASE_CONFIG }, elements };
@@ -702,6 +723,28 @@ function buildCardKitStreamingCard(opts) {
702
723
  lines.push(`<font color='grey'>${opts.status}</font>`);
703
724
  }
704
725
  lines.push(opts.buffer.length > 0 ? opts.buffer : "_\u2026_");
726
+ if (opts.askRequest) {
727
+ lines.push(`**${opts.askRequest.prompt}**`);
728
+ if (opts.askRequest.allowFreeform && (opts.askRequest.options?.length ?? 0) === 0) {
729
+ lines.push(`<font color='grey'>_Reply to this chat with your answer_</font>`);
730
+ }
731
+ }
732
+ const elements = [
733
+ { tag: "markdown", content: lines.join("\n\n") }
734
+ ];
735
+ if (opts.askRequest?.options && opts.askRequest.options.length > 0) {
736
+ const buttons = opts.askRequest.options.map((opt) => ({
737
+ tag: "button",
738
+ text: { tag: "plain_text", content: opt.label },
739
+ type: opt.style ?? "default",
740
+ value: {
741
+ [ASK_BUTTON_VALUE_MARKER]: true,
742
+ requestId: opts.askRequest.requestId,
743
+ optionId: opt.id
744
+ }
745
+ }));
746
+ elements.push({ tag: "action", actions: buttons });
747
+ }
705
748
  return {
706
749
  schema: "2.0",
707
750
  config: {
@@ -709,27 +752,38 @@ function buildCardKitStreamingCard(opts) {
709
752
  wide_screen_mode: true,
710
753
  update_multi: true
711
754
  },
712
- body: {
713
- elements: [
714
- { tag: "markdown", content: lines.join("\n\n") }
715
- ]
716
- }
755
+ body: { elements }
717
756
  };
718
757
  }
719
758
  __name(buildCardKitStreamingCard, "buildCardKitStreamingCard");
720
- function buildCardKitFinalCard(text, toolCalls) {
759
+ function buildCardKitFinalCard(text, toolCalls, askRequest) {
721
760
  const lines = [];
722
761
  const toolLine = renderToolCalls(toolCalls ?? []);
723
762
  if (toolLine) lines.push(toolLine);
724
763
  lines.push(text);
764
+ if (askRequest) {
765
+ lines.push(`**${askRequest.prompt}**`);
766
+ }
767
+ const elements = [
768
+ { tag: "markdown", content: lines.join("\n\n") }
769
+ ];
770
+ if (askRequest?.options && askRequest.options.length > 0) {
771
+ const buttons = askRequest.options.map((opt) => ({
772
+ tag: "button",
773
+ text: { tag: "plain_text", content: opt.label },
774
+ type: opt.style ?? "default",
775
+ value: {
776
+ [ASK_BUTTON_VALUE_MARKER]: true,
777
+ requestId: askRequest.requestId,
778
+ optionId: opt.id
779
+ }
780
+ }));
781
+ elements.push({ tag: "action", actions: buttons });
782
+ }
725
783
  return {
726
784
  schema: "2.0",
727
785
  config: { streaming_mode: false, wide_screen_mode: true, update_multi: true },
728
- body: {
729
- elements: [
730
- { tag: "markdown", content: lines.join("\n\n") }
731
- ]
732
- }
786
+ body: { elements }
733
787
  };
734
788
  }
735
789
  __name(buildCardKitFinalCard, "buildCardKitFinalCard");
@@ -746,6 +800,10 @@ var StreamingCardController = class {
746
800
  status;
747
801
  messageId;
748
802
  fallbackToText = false;
803
+ /** Active HITL input request, when set via `setAskRequest`. The card
804
+ * builder appends prompt + buttons to the same streaming card so the
805
+ * user can answer inline instead of getting a separate ask-card. */
806
+ askRequest = null;
749
807
  /** Tool calls made during this turn, in order. Rendered above the buffer
750
808
  * so users can see what the agent is doing / has done, Claude-Code-style. */
751
809
  toolCalls = [];
@@ -817,6 +875,69 @@ var StreamingCardController = class {
817
875
  getToolCalls() {
818
876
  return this.toolCalls;
819
877
  }
878
+ /** Current streaming buffer (for handleCardAction to preserve when patching
879
+ * the "answered" state of an inline ask card). */
880
+ getBuffer() {
881
+ return this.buffer;
882
+ }
883
+ /** Card message id (so input.requested can reuse the existing card instead
884
+ * of creating a separate ask-card). */
885
+ getMessageId() {
886
+ return this.messageId;
887
+ }
888
+ /** Active HITL request being rendered inline on this card, or null. */
889
+ getAskRequest() {
890
+ return this.askRequest;
891
+ }
892
+ /**
893
+ * Render an `ask_question` request inline on this card by appending prompt
894
+ * + option buttons below the streaming text. Patches immediately so the
895
+ * user sees the buttons as soon as the agent asks.
896
+ *
897
+ * If the prior turn already finalized (state="completed"), transition back
898
+ * to "streaming" so the next patch can update the same card. The card
899
+ * keeps its messageId — no new card is sent.
900
+ */
901
+ setAskRequest(req) {
902
+ if (this.state === "aborted") return;
903
+ this.askRequest = req;
904
+ if (this.state === "completed" && this.messageId) {
905
+ this.state = "streaming";
906
+ }
907
+ if (this.state === "streaming") {
908
+ this.schedulePatch();
909
+ }
910
+ }
911
+ /** Clear the inline ask request (e.g. after the user clicked an option). */
912
+ clearAskRequest() {
913
+ this.askRequest = null;
914
+ }
915
+ /**
916
+ * Reset per-turn state for the next turn within the same session. Clears
917
+ * the streaming buffer, tool-call history, status, and any inline ask —
918
+ * but keeps the card messageId and transitions back to "streaming" so the
919
+ * next message.appended patches the SAME card. Without this, the second
920
+ * turn's message.completed would create a brand-new card and the user
921
+ * would see N cards for N turns within one logical conversation.
922
+ *
923
+ * Called from the channel's `turn.started` event handler.
924
+ */
925
+ resetForNewTurn() {
926
+ this.buffer = "";
927
+ this.status = void 0;
928
+ this.toolCalls = [];
929
+ this.askRequest = null;
930
+ this.fallbackToText = false;
931
+ this.cancelCreateTimer();
932
+ this.cancelPatchTimer();
933
+ this.patchInFlight = null;
934
+ this.patchScheduled = false;
935
+ if (this.messageId) {
936
+ this.state = "streaming";
937
+ } else {
938
+ this.state = "idle";
939
+ }
940
+ }
820
941
  async finalize(fullText) {
821
942
  if (this.state === "completed" || this.state === "aborted") return;
822
943
  this.cancelCreateTimer();
@@ -836,7 +957,7 @@ var StreamingCardController = class {
836
957
  try {
837
958
  const res = await this.client.sendCard({
838
959
  chatId: this.deps.chatId,
839
- card: this.deps.useCardKitV2 ? buildCardKitFinalCard(fullText, this.toolCalls) : buildTextCard(fullText),
960
+ card: this.deps.useCardKitV2 ? buildCardKitFinalCard(fullText, this.toolCalls, this.askRequest) : buildTextCard(fullText),
840
961
  rootId: this.deps.rootId,
841
962
  parentId: this.deps.parentId
842
963
  });
@@ -862,7 +983,7 @@ var StreamingCardController = class {
862
983
  }
863
984
  await this.client.patchCard({
864
985
  messageId: this.messageId,
865
- card: this.deps.useCardKitV2 ? buildCardKitStreamingCard({ buffer: fullText, streamingMode: false, toolCalls: this.toolCalls }) : buildStreamingCard({ buffer: fullText, status: void 0, toolCalls: this.toolCalls })
986
+ card: this.deps.useCardKitV2 ? buildCardKitStreamingCard({ buffer: fullText, streamingMode: false, toolCalls: this.toolCalls, askRequest: this.askRequest }) : buildStreamingCard({ buffer: fullText, status: void 0, toolCalls: this.toolCalls, askRequest: this.askRequest })
866
987
  });
867
988
  this.state = "completed";
868
989
  }
@@ -914,7 +1035,7 @@ var StreamingCardController = class {
914
1035
  try {
915
1036
  const res = await this.client.sendCard({
916
1037
  chatId: this.deps.chatId,
917
- card: this.deps.useCardKitV2 ? buildCardKitStreamingCard({ buffer: this.buffer, status: this.status, streamingMode: true, toolCalls: this.toolCalls }) : buildStreamingCard({ buffer: this.buffer, status: this.status, toolCalls: this.toolCalls }),
1038
+ card: this.deps.useCardKitV2 ? buildCardKitStreamingCard({ buffer: this.buffer, status: this.status, streamingMode: true, toolCalls: this.toolCalls, askRequest: this.askRequest }) : buildStreamingCard({ buffer: this.buffer, status: this.status, toolCalls: this.toolCalls, askRequest: this.askRequest }),
918
1039
  rootId: this.deps.rootId,
919
1040
  parentId: this.deps.parentId
920
1041
  });
@@ -952,7 +1073,7 @@ var StreamingCardController = class {
952
1073
  if (this.state !== "streaming") return;
953
1074
  if (this.patchInFlight) return;
954
1075
  if (this.messageId === void 0) return;
955
- const card = this.deps.useCardKitV2 ? buildCardKitStreamingCard({ buffer: this.buffer, status: this.status, streamingMode: true, toolCalls: this.toolCalls }) : buildStreamingCard({ buffer: this.buffer, status: this.status, toolCalls: this.toolCalls });
1076
+ const card = this.deps.useCardKitV2 ? buildCardKitStreamingCard({ buffer: this.buffer, status: this.status, streamingMode: true, toolCalls: this.toolCalls, askRequest: this.askRequest }) : buildStreamingCard({ buffer: this.buffer, status: this.status, toolCalls: this.toolCalls, askRequest: this.askRequest });
956
1077
  this.patchInFlight = this.client.patchCard({ messageId: this.messageId, card }).catch((e) => {
957
1078
  console.warn(
958
1079
  "[eve-lark] streaming card patch failed:",
@@ -1609,11 +1730,6 @@ function createLarkChannel(optionsInput) {
1609
1730
  return ctrl;
1610
1731
  }
1611
1732
  __name(getController, "getController");
1612
- function dropController(sessionId) {
1613
- controllers.delete(sessionId);
1614
- sessionMeta.delete(sessionId);
1615
- }
1616
- __name(dropController, "dropController");
1617
1733
  async function cleanupAckReaction(sessionId) {
1618
1734
  const meta = sessionMeta.get(sessionId);
1619
1735
  if (!meta?.ackReactionId || !meta.messageId) return;
@@ -1983,14 +2099,18 @@ function createLarkChannel(optionsInput) {
1983
2099
  helpers.waitUntil(
1984
2100
  (async () => {
1985
2101
  if (pending.cardMessageId && selectedOpt) {
2102
+ const ctrlForBuffer = controllers.get(pending.sessionId);
2103
+ const priorBuffer = ctrlForBuffer?.getBuffer() ?? void 0;
1986
2104
  try {
1987
2105
  await client.patchCard({
1988
2106
  messageId: pending.cardMessageId,
1989
- card: buildAskAnsweredCard(pending.request, {
1990
- kind: "option",
1991
- label: selectedOpt.label
1992
- })
2107
+ card: buildAskAnsweredCard(
2108
+ pending.request,
2109
+ { kind: "option", label: selectedOpt.label },
2110
+ priorBuffer
2111
+ )
1993
2112
  });
2113
+ ctrlForBuffer?.clearAskRequest();
1994
2114
  } catch (e) {
1995
2115
  console.warn(
1996
2116
  "[eve-lark] patchCard after ask-answer failed:",
@@ -2101,22 +2221,29 @@ function createLarkChannel(optionsInput) {
2101
2221
  `[eve-lark] input.requested sessionId=${sessionId} chatId=${info.chatId} count=${requests.length}`
2102
2222
  );
2103
2223
  for (const req of requests) {
2104
- const card = buildAskCard(req);
2224
+ const existingCtrl = controllers.get(sessionId);
2225
+ const canPatchExisting = existingCtrl && existingCtrl.getMessageId() && (options.replyMode === "streaming" || options.replyMode === "streaming-v2");
2105
2226
  let cardMessageId;
2106
- try {
2107
- const res = await client.sendCard({
2108
- chatId: info.chatId,
2109
- card,
2110
- rootId: info.rootId,
2111
- parentId: info.parentId
2112
- });
2113
- cardMessageId = res.messageId;
2114
- } catch (e) {
2115
- console.error(
2116
- `[eve-lark] ask card send failed (requestId=${req.requestId}):`,
2117
- e instanceof Error ? e.message : e
2118
- );
2119
- continue;
2227
+ if (canPatchExisting && existingCtrl) {
2228
+ existingCtrl.setAskRequest(req);
2229
+ cardMessageId = existingCtrl.getMessageId();
2230
+ } else {
2231
+ const card = buildAskCard(req);
2232
+ try {
2233
+ const res = await client.sendCard({
2234
+ chatId: info.chatId,
2235
+ card,
2236
+ rootId: info.rootId,
2237
+ parentId: info.parentId
2238
+ });
2239
+ cardMessageId = res.messageId;
2240
+ } catch (e) {
2241
+ console.error(
2242
+ `[eve-lark] ask card send failed (requestId=${req.requestId}):`,
2243
+ e instanceof Error ? e.message : e
2244
+ );
2245
+ continue;
2246
+ }
2120
2247
  }
2121
2248
  const pending = {
2122
2249
  requestId: req.requestId,
@@ -2155,7 +2282,6 @@ function createLarkChannel(optionsInput) {
2155
2282
  await deliverReply(sessionId, info, text);
2156
2283
  } finally {
2157
2284
  await cleanupAckReaction(sessionId);
2158
- dropController(sessionId);
2159
2285
  }
2160
2286
  },
2161
2287
  async "turn.failed"(data, _channel, ctx) {
@@ -2196,7 +2322,6 @@ function createLarkChannel(optionsInput) {
2196
2322
  }
2197
2323
  }
2198
2324
  await cleanupAckReaction(sessionId);
2199
- dropController(sessionId);
2200
2325
  },
2201
2326
  async "session.failed"(data) {
2202
2327
  const userText = formatFailureMessage(data, "session failed", { sentence: "session" });
@@ -2205,11 +2330,23 @@ function createLarkChannel(optionsInput) {
2205
2330
  `[eve-lark] session.failed: ${userText}` + (errorId ? ` (errorId=${errorId})` : "")
2206
2331
  );
2207
2332
  },
2333
+ // A new turn is starting within an existing session (e.g. user clicked
2334
+ // an inline ask button, eve resumed with their InputResponse). Reset
2335
+ // the controller's per-turn state so the new turn's text replaces the
2336
+ // prior turn's text on the SAME card — instead of creating a fresh
2337
+ // card per turn.
2338
+ async "turn.started"(_data, _channel, ctx) {
2339
+ const sessionId = ctx?.session?.id;
2340
+ if (!sessionId) return;
2341
+ const ctrl = controllers.get(sessionId);
2342
+ if (ctrl) {
2343
+ ctrl.resetForNewTurn();
2344
+ }
2345
+ },
2208
2346
  // Turn ended cleanly. eve fires this after the final message.completed
2209
2347
  // (or instead of it when the assistant step ended in tool-calls with no
2210
- // visible text). Either way, free this session's controller + ack
2211
- // reaction so we don't leak waiting for a message.completed that's
2212
- // never coming.
2348
+ // visible text). Just clean up the ack reaction — the controller stays
2349
+ // for the next turn (cleaned by stale-sweep if the session goes quiet).
2213
2350
  async "turn.completed"(data, _channel, ctx) {
2214
2351
  const sessionId = ctx?.session?.id;
2215
2352
  if (!sessionId) return;
@@ -2217,7 +2354,6 @@ function createLarkChannel(optionsInput) {
2217
2354
  await cleanupAckReaction(sessionId);
2218
2355
  } catch {
2219
2356
  }
2220
- dropController(sessionId);
2221
2357
  },
2222
2358
  // The agent needs the user to sign in to an external service (e.g.
2223
2359
  // GitHub, Slack, Linear). Render a card with a "Sign in with <X>"