eve-lark 0.4.3 → 0.4.5

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,82 @@ 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
+ }
941
+ /**
942
+ * Full reset for a brand-new inbound user message (not a continuation
943
+ * of the same conversation flow). Same as {@link resetForNewTurn} but
944
+ * ALSO clears the card messageId so the next `sendCard` creates a fresh
945
+ * card. Without this, all top-level messages in the same chat would
946
+ * patch the first card — the user would only see the latest reply
947
+ * overwritten onto one card instead of N independent cards.
948
+ */
949
+ resetForNewMessage() {
950
+ this.resetForNewTurn();
951
+ this.messageId = void 0;
952
+ this.state = "idle";
953
+ }
820
954
  async finalize(fullText) {
821
955
  if (this.state === "completed" || this.state === "aborted") return;
822
956
  this.cancelCreateTimer();
@@ -836,7 +970,7 @@ var StreamingCardController = class {
836
970
  try {
837
971
  const res = await this.client.sendCard({
838
972
  chatId: this.deps.chatId,
839
- card: this.deps.useCardKitV2 ? buildCardKitFinalCard(fullText, this.toolCalls) : buildTextCard(fullText),
973
+ card: this.deps.useCardKitV2 ? buildCardKitFinalCard(fullText, this.toolCalls, this.askRequest) : buildTextCard(fullText),
840
974
  rootId: this.deps.rootId,
841
975
  parentId: this.deps.parentId
842
976
  });
@@ -862,7 +996,7 @@ var StreamingCardController = class {
862
996
  }
863
997
  await this.client.patchCard({
864
998
  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 })
999
+ 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
1000
  });
867
1001
  this.state = "completed";
868
1002
  }
@@ -914,7 +1048,7 @@ var StreamingCardController = class {
914
1048
  try {
915
1049
  const res = await this.client.sendCard({
916
1050
  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 }),
1051
+ 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
1052
  rootId: this.deps.rootId,
919
1053
  parentId: this.deps.parentId
920
1054
  });
@@ -952,7 +1086,7 @@ var StreamingCardController = class {
952
1086
  if (this.state !== "streaming") return;
953
1087
  if (this.patchInFlight) return;
954
1088
  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 });
1089
+ 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
1090
  this.patchInFlight = this.client.patchCard({ messageId: this.messageId, card }).catch((e) => {
957
1091
  console.warn(
958
1092
  "[eve-lark] streaming card patch failed:",
@@ -1582,6 +1716,7 @@ function createLarkChannel(optionsInput) {
1582
1716
  const pendingInputsByRequestId = /* @__PURE__ */ new Map();
1583
1717
  const pendingInputsByChatToken = /* @__PURE__ */ new Map();
1584
1718
  const authCards = /* @__PURE__ */ new Map();
1719
+ const expectFreshCard = /* @__PURE__ */ new Set();
1585
1720
  function getController(sessionId, meta) {
1586
1721
  let ctrl = controllers.get(sessionId);
1587
1722
  if (!ctrl) {
@@ -1609,11 +1744,6 @@ function createLarkChannel(optionsInput) {
1609
1744
  return ctrl;
1610
1745
  }
1611
1746
  __name(getController, "getController");
1612
- function dropController(sessionId) {
1613
- controllers.delete(sessionId);
1614
- sessionMeta.delete(sessionId);
1615
- }
1616
- __name(dropController, "dropController");
1617
1747
  async function cleanupAckReaction(sessionId) {
1618
1748
  const meta = sessionMeta.get(sessionId);
1619
1749
  if (!meta?.ackReactionId || !meta.messageId) return;
@@ -1940,6 +2070,7 @@ function createLarkChannel(optionsInput) {
1940
2070
  console.log(
1941
2071
  `[eve-lark] helpers.send returned sessionId=${session.id} for chatId=${parsed.chatId}`
1942
2072
  );
2073
+ expectFreshCard.add(session.id);
1943
2074
  sessionMeta.set(session.id, {
1944
2075
  chatId: parsed.chatId,
1945
2076
  rootId: parsed.rootId ?? void 0,
@@ -1983,14 +2114,18 @@ function createLarkChannel(optionsInput) {
1983
2114
  helpers.waitUntil(
1984
2115
  (async () => {
1985
2116
  if (pending.cardMessageId && selectedOpt) {
2117
+ const ctrlForBuffer = controllers.get(pending.sessionId);
2118
+ const priorBuffer = ctrlForBuffer?.getBuffer() ?? void 0;
1986
2119
  try {
1987
2120
  await client.patchCard({
1988
2121
  messageId: pending.cardMessageId,
1989
- card: buildAskAnsweredCard(pending.request, {
1990
- kind: "option",
1991
- label: selectedOpt.label
1992
- })
2122
+ card: buildAskAnsweredCard(
2123
+ pending.request,
2124
+ { kind: "option", label: selectedOpt.label },
2125
+ priorBuffer
2126
+ )
1993
2127
  });
2128
+ ctrlForBuffer?.clearAskRequest();
1994
2129
  } catch (e) {
1995
2130
  console.warn(
1996
2131
  "[eve-lark] patchCard after ask-answer failed:",
@@ -2101,22 +2236,29 @@ function createLarkChannel(optionsInput) {
2101
2236
  `[eve-lark] input.requested sessionId=${sessionId} chatId=${info.chatId} count=${requests.length}`
2102
2237
  );
2103
2238
  for (const req of requests) {
2104
- const card = buildAskCard(req);
2239
+ const existingCtrl = controllers.get(sessionId);
2240
+ const canPatchExisting = existingCtrl && existingCtrl.getMessageId() && (options.replyMode === "streaming" || options.replyMode === "streaming-v2");
2105
2241
  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;
2242
+ if (canPatchExisting && existingCtrl) {
2243
+ existingCtrl.setAskRequest(req);
2244
+ cardMessageId = existingCtrl.getMessageId();
2245
+ } else {
2246
+ const card = buildAskCard(req);
2247
+ try {
2248
+ const res = await client.sendCard({
2249
+ chatId: info.chatId,
2250
+ card,
2251
+ rootId: info.rootId,
2252
+ parentId: info.parentId
2253
+ });
2254
+ cardMessageId = res.messageId;
2255
+ } catch (e) {
2256
+ console.error(
2257
+ `[eve-lark] ask card send failed (requestId=${req.requestId}):`,
2258
+ e instanceof Error ? e.message : e
2259
+ );
2260
+ continue;
2261
+ }
2120
2262
  }
2121
2263
  const pending = {
2122
2264
  requestId: req.requestId,
@@ -2155,7 +2297,6 @@ function createLarkChannel(optionsInput) {
2155
2297
  await deliverReply(sessionId, info, text);
2156
2298
  } finally {
2157
2299
  await cleanupAckReaction(sessionId);
2158
- dropController(sessionId);
2159
2300
  }
2160
2301
  },
2161
2302
  async "turn.failed"(data, _channel, ctx) {
@@ -2196,7 +2337,6 @@ function createLarkChannel(optionsInput) {
2196
2337
  }
2197
2338
  }
2198
2339
  await cleanupAckReaction(sessionId);
2199
- dropController(sessionId);
2200
2340
  },
2201
2341
  async "session.failed"(data) {
2202
2342
  const userText = formatFailureMessage(data, "session failed", { sentence: "session" });
@@ -2205,11 +2345,29 @@ function createLarkChannel(optionsInput) {
2205
2345
  `[eve-lark] session.failed: ${userText}` + (errorId ? ` (errorId=${errorId})` : "")
2206
2346
  );
2207
2347
  },
2348
+ // A new turn is starting within an existing session. Two cases:
2349
+ //
2350
+ // 1. NEW inbound message (the webhook handler set `expectFreshCard`):
2351
+ // force a fresh card so each user message gets its own reply card.
2352
+ // 2. Continuation (HITL answer via button click / freeform, or tool
2353
+ // resume): keep the same card so the whole flow stays on one card.
2354
+ async "turn.started"(_data, _channel, ctx) {
2355
+ const sessionId = ctx?.session?.id;
2356
+ if (!sessionId) return;
2357
+ const ctrl = controllers.get(sessionId);
2358
+ if (ctrl) {
2359
+ if (expectFreshCard.has(sessionId)) {
2360
+ ctrl.resetForNewMessage();
2361
+ expectFreshCard.delete(sessionId);
2362
+ } else {
2363
+ ctrl.resetForNewTurn();
2364
+ }
2365
+ }
2366
+ },
2208
2367
  // Turn ended cleanly. eve fires this after the final message.completed
2209
2368
  // (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.
2369
+ // visible text). Just clean up the ack reaction — the controller stays
2370
+ // for the next turn (cleaned by stale-sweep if the session goes quiet).
2213
2371
  async "turn.completed"(data, _channel, ctx) {
2214
2372
  const sessionId = ctx?.session?.id;
2215
2373
  if (!sessionId) return;
@@ -2217,7 +2375,6 @@ function createLarkChannel(optionsInput) {
2217
2375
  await cleanupAckReaction(sessionId);
2218
2376
  } catch {
2219
2377
  }
2220
- dropController(sessionId);
2221
2378
  },
2222
2379
  // The agent needs the user to sign in to an external service (e.g.
2223
2380
  // GitHub, Slack, Linear). Render a card with a "Sign in with <X>"