@fastagent-sh/fastagent 0.15.0 → 0.16.0

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 (168) hide show
  1. package/README.md +13 -3
  2. package/dist/channels/agentcore-limits.d.ts +7 -0
  3. package/dist/channels/agentcore-limits.js +9 -0
  4. package/dist/channels/agentcore-state.d.ts +83 -0
  5. package/dist/channels/agentcore-state.js +258 -0
  6. package/dist/channels/agentcore.d.ts +98 -0
  7. package/dist/channels/agentcore.js +260 -0
  8. package/dist/channels/busy.d.ts +23 -0
  9. package/dist/channels/busy.js +53 -0
  10. package/dist/channels/context-buffer.d.ts +5 -0
  11. package/dist/channels/context-buffer.js +5 -0
  12. package/dist/channels/feishu/context-buffer.d.ts +13 -8
  13. package/dist/channels/feishu/context-buffer.js +60 -6
  14. package/dist/channels/feishu/feishu-api.js +4 -1
  15. package/dist/channels/feishu/feishu.d.ts +0 -12
  16. package/dist/channels/feishu/feishu.js +143 -54
  17. package/dist/channels/feishu/invoke-turn.js +35 -18
  18. package/dist/channels/feishu/model.d.ts +0 -1
  19. package/dist/channels/feishu/normalize.js +0 -1
  20. package/dist/channels/feishu/parse.d.ts +21 -7
  21. package/dist/channels/feishu/parse.js +24 -7
  22. package/dist/channels/feishu/preview.js +3 -2
  23. package/dist/channels/feishu/scaffold/channel.ts +9 -8
  24. package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
  25. package/dist/channels/feishu/setup-mode.d.ts +30 -1
  26. package/dist/channels/feishu/setup-mode.js +27 -1
  27. package/dist/channels/github/github.js +8 -1
  28. package/dist/channels/http.js +1 -1
  29. package/dist/channels/lark/scaffold/channel.ts +9 -8
  30. package/dist/channels/lark/scaffold/lark-send.ts +6 -4
  31. package/dist/channels/preview-kit.d.ts +7 -1
  32. package/dist/channels/preview-kit.js +3 -2
  33. package/dist/channels/slack/parse.d.ts +16 -1
  34. package/dist/channels/slack/parse.js +46 -3
  35. package/dist/channels/slack/preview.d.ts +1 -2
  36. package/dist/channels/slack/preview.js +68 -24
  37. package/dist/channels/slack/scaffold/channel.ts +5 -5
  38. package/dist/channels/slack/slack-api.d.ts +3 -23
  39. package/dist/channels/slack/slack-api.js +6 -22
  40. package/dist/channels/slack/slack.d.ts +13 -20
  41. package/dist/channels/slack/slack.js +95 -50
  42. package/dist/channels/state.d.ts +11 -4
  43. package/dist/channels/state.js +19 -12
  44. package/dist/channels/tasks.d.ts +0 -6
  45. package/dist/channels/tasks.js +16 -1
  46. package/dist/channels/telegram/parse.d.ts +0 -7
  47. package/dist/channels/telegram/parse.js +4 -2
  48. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
  49. package/dist/channels/telegram/telegram.js +1 -1
  50. package/dist/channels/text.d.ts +14 -0
  51. package/dist/channels/text.js +14 -0
  52. package/dist/channels/thread-participants.d.ts +21 -0
  53. package/dist/channels/thread-participants.js +132 -0
  54. package/dist/channels/turn-queue.js +7 -0
  55. package/dist/cli/add-feishu.d.ts +7 -4
  56. package/dist/cli/add-feishu.js +57 -37
  57. package/dist/cli/add-slack.d.ts +2 -1
  58. package/dist/cli/add-slack.js +6 -11
  59. package/dist/cli/commands/add.js +50 -51
  60. package/dist/cli/commands/attach.js +8 -4
  61. package/dist/cli/commands/chat.js +8 -8
  62. package/dist/cli/commands/deploy.d.ts +1 -1
  63. package/dist/cli/commands/deploy.js +323 -77
  64. package/dist/cli/commands/dev.js +24 -22
  65. package/dist/cli/commands/fire.js +15 -16
  66. package/dist/cli/commands/info.js +36 -29
  67. package/dist/cli/commands/init.d.ts +1 -1
  68. package/dist/cli/commands/init.js +65 -53
  69. package/dist/cli/commands/invoke.js +9 -6
  70. package/dist/cli/commands/login.js +35 -21
  71. package/dist/cli/commands/schedule.js +6 -8
  72. package/dist/cli/commands/start.js +93 -37
  73. package/dist/cli/commands/tool.js +22 -18
  74. package/dist/cli/fail.d.ts +17 -0
  75. package/dist/cli/fail.js +24 -0
  76. package/dist/cli/program.js +57 -36
  77. package/dist/cli/serve.d.ts +26 -6
  78. package/dist/cli/serve.js +62 -15
  79. package/dist/cli/shared.d.ts +15 -2
  80. package/dist/cli/shared.js +30 -18
  81. package/dist/deploy/agentcore/plan.d.ts +117 -0
  82. package/dist/deploy/agentcore/plan.js +721 -0
  83. package/dist/deploy/agentcore/run.d.ts +73 -0
  84. package/dist/deploy/agentcore/run.js +412 -0
  85. package/dist/deploy/agentcore/zip.d.ts +17 -0
  86. package/dist/deploy/agentcore/zip.js +68 -0
  87. package/dist/deploy/container.d.ts +26 -25
  88. package/dist/deploy/container.js +93 -89
  89. package/dist/deploy/docker/plan.d.ts +1 -1
  90. package/dist/deploy/docker/plan.js +12 -17
  91. package/dist/deploy/fly/plan.d.ts +2 -0
  92. package/dist/deploy/fly/plan.js +27 -19
  93. package/dist/deploy/fly/run.d.ts +12 -1
  94. package/dist/deploy/fly/run.js +36 -2
  95. package/dist/deploy/preflight.d.ts +11 -5
  96. package/dist/deploy/preflight.js +217 -65
  97. package/dist/deploy/railway/plan.d.ts +7 -0
  98. package/dist/deploy/railway/plan.js +41 -16
  99. package/dist/deploy/railway/run.d.ts +8 -1
  100. package/dist/deploy/railway/run.js +7 -2
  101. package/dist/deploy/runner.d.ts +5 -2
  102. package/dist/deploy/runner.js +9 -3
  103. package/dist/dev-supervisor.d.ts +11 -8
  104. package/dist/dev-supervisor.js +53 -51
  105. package/dist/engines/pi/auth.d.ts +8 -7
  106. package/dist/engines/pi/auth.js +12 -10
  107. package/dist/engines/pi/channel.d.ts +1 -1
  108. package/dist/engines/pi/channel.js +5 -5
  109. package/dist/engines/pi/chat.js +2 -2
  110. package/dist/engines/pi/config.d.ts +6 -46
  111. package/dist/engines/pi/config.js +21 -108
  112. package/dist/engines/pi/create.d.ts +8 -8
  113. package/dist/engines/pi/create.js +13 -12
  114. package/dist/engines/pi/definition.d.ts +7 -26
  115. package/dist/engines/pi/definition.js +8 -54
  116. package/dist/engines/pi/login.d.ts +1 -1
  117. package/dist/engines/pi/models.d.ts +3 -3
  118. package/dist/engines/pi/models.js +1 -1
  119. package/dist/engines/pi/{workspace.d.ts → open.d.ts} +29 -23
  120. package/dist/engines/pi/{workspace.js → open.js} +27 -29
  121. package/dist/engines/pi/session-builder.d.ts +2 -2
  122. package/dist/engines/pi/session-builder.js +11 -11
  123. package/dist/engines/pi/tool.js +4 -0
  124. package/dist/env.d.ts +16 -4
  125. package/dist/env.js +43 -5
  126. package/dist/host/node.d.ts +2 -2
  127. package/dist/loader.d.ts +2 -2
  128. package/dist/loader.js +3 -3
  129. package/dist/log.d.ts +1 -1
  130. package/dist/log.js +1 -1
  131. package/dist/paths.d.ts +138 -0
  132. package/dist/paths.js +326 -0
  133. package/dist/pi.d.ts +1 -1
  134. package/dist/pi.js +2 -2
  135. package/dist/runtime.d.ts +7 -5
  136. package/dist/runtime.js +2 -2
  137. package/dist/scaffold/add-channel.d.ts +7 -3
  138. package/dist/scaffold/add-channel.js +55 -29
  139. package/dist/scaffold/init.d.ts +32 -41
  140. package/dist/scaffold/init.js +161 -185
  141. package/dist/scaffold/templates/env.example +15 -6
  142. package/dist/scaffold/templates/fastagent.config.mjs +1 -1
  143. package/dist/scaffold/templates/gitignore +14 -6
  144. package/dist/scaffold/templates/persona.md +4 -2
  145. package/dist/scaffold/templates/secrets.gitignore +5 -0
  146. package/dist/scaffold/templates.d.ts +1 -7
  147. package/dist/scaffold/templates.js +3 -25
  148. package/dist/scaffold/vendor-skill.d.ts +2 -2
  149. package/dist/scaffold/vendor-skill.js +13 -13
  150. package/dist/schedule/discover.js +4 -4
  151. package/dist/schedule/scheduler.d.ts +40 -1
  152. package/dist/schedule/scheduler.js +89 -56
  153. package/dist/schedule/state.js +1 -1
  154. package/dist/schedule/wake-alarm.d.ts +47 -0
  155. package/dist/schedule/wake-alarm.js +136 -0
  156. package/dist/schedule/wakeups.d.ts +1 -0
  157. package/dist/schedule/wakeups.js +18 -0
  158. package/dist/tunnel.d.ts +3 -3
  159. package/dist/tunnel.js +7 -7
  160. package/package.json +4 -1
  161. package/dist/channels/feishu/owned-threads.d.ts +0 -7
  162. package/dist/channels/feishu/owned-threads.js +0 -47
  163. package/dist/channels/slack/owned-threads.d.ts +0 -6
  164. package/dist/channels/slack/owned-threads.js +0 -43
  165. package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
  166. package/dist/scaffold/templates/gitignore.kit +0 -2
  167. package/dist/workspace.d.ts +0 -9
  168. package/dist/workspace.js +0 -45
@@ -4,5 +4,34 @@ export type FeishuSubscriptionMode = "webhook" | "websocket";
4
4
  * runtime remains capability-driven because the platform, not channel source, decides which events
5
5
  * are delivered. */
6
6
  export type FeishuGroupBehavior = "context" | "mentions";
7
- /** The sensitive tenant scope behind both managed-thread bare replies and group context buffering. */
7
+ /** The sensitive tenant scope behind both bare replies in the agent's threads and group context buffering. */
8
8
  export declare const FEISHU_GROUP_CONTEXT_SCOPE = "im:message.group_msg";
9
+ /** Reading a QUOTED message by id, so an ask carries what it replies to (participant-model.md §8).
10
+ * INDEPENDENT of the delivery scope above and of the group posture: the read runs in every chat type
11
+ * (a p2p thread's opening ask, any quoted @mention in a group). It is also a softer dependency —
12
+ * without it everything still works, and an unreadable quote degrades to a marker in the prompt.
13
+ * Bundled into the context-aware request only because that path already needs an approval round; a
14
+ * mention-only app wanting referents must add it by hand. */
15
+ export declare const FEISHU_MESSAGE_READ_SCOPE = "im:message:readonly";
16
+ /** A scope the onboarding asks for, plus any BROADER spelling that already satisfies it. One concept,
17
+ * so adding a superset means editing the entry — not every call site that tests a scope. */
18
+ export interface FeishuScopeRequest {
19
+ /** What to add to the app draft when nothing satisfies it. Always counts as satisfying itself. */
20
+ request: string;
21
+ /** EXTRA spellings that also count — supersets. Optional; `request` is implicit. Modelled this way
22
+ * so a request that can never be satisfied (a list omitting its own `request`) cannot be
23
+ * written: onboarding would add the scope, the tenant would grant it, and it would still read as
24
+ * missing forever. */
25
+ supersets?: string[];
26
+ }
27
+ /** Whether `predicate` holds for any spelling that satisfies this request. */
28
+ export declare function scopeSatisfied(entry: FeishuScopeRequest, predicate: (name: string) => boolean): boolean;
29
+ /** Reading a quoted message: `im:message` is the read/write superset, so an app holding it can already
30
+ * do so — checking only the readonly spelling would warn a correctly configured app forever and push
31
+ * its author into a redundant approval round. Exported on its own because the serving-time capability
32
+ * report asks the same question, and a second mechanism there is what this type exists to prevent. */
33
+ export declare const FEISHU_MESSAGE_READ_REQUEST: FeishuScopeRequest;
34
+ /** What `--group-behavior context` REQUESTS in one approval round — not a dependency set. Only the
35
+ * delivery scope is required for the context path; the read scope rides along because it shares the
36
+ * round and its absence merely degrades quoted messages to a marker. */
37
+ export declare const FEISHU_CONTEXT_ONBOARDING_SCOPES: FeishuScopeRequest[];
@@ -1,2 +1,28 @@
1
- /** The sensitive tenant scope behind both managed-thread bare replies and group context buffering. */
1
+ /** The sensitive tenant scope behind both bare replies in the agent's threads and group context buffering. */
2
2
  export const FEISHU_GROUP_CONTEXT_SCOPE = "im:message.group_msg";
3
+ /** Reading a QUOTED message by id, so an ask carries what it replies to (participant-model.md §8).
4
+ * INDEPENDENT of the delivery scope above and of the group posture: the read runs in every chat type
5
+ * (a p2p thread's opening ask, any quoted @mention in a group). It is also a softer dependency —
6
+ * without it everything still works, and an unreadable quote degrades to a marker in the prompt.
7
+ * Bundled into the context-aware request only because that path already needs an approval round; a
8
+ * mention-only app wanting referents must add it by hand. */
9
+ export const FEISHU_MESSAGE_READ_SCOPE = "im:message:readonly";
10
+ /** Whether `predicate` holds for any spelling that satisfies this request. */
11
+ export function scopeSatisfied(entry, predicate) {
12
+ return [entry.request, ...(entry.supersets ?? [])].some(predicate);
13
+ }
14
+ /** Reading a quoted message: `im:message` is the read/write superset, so an app holding it can already
15
+ * do so — checking only the readonly spelling would warn a correctly configured app forever and push
16
+ * its author into a redundant approval round. Exported on its own because the serving-time capability
17
+ * report asks the same question, and a second mechanism there is what this type exists to prevent. */
18
+ export const FEISHU_MESSAGE_READ_REQUEST = {
19
+ request: FEISHU_MESSAGE_READ_SCOPE,
20
+ supersets: ["im:message"],
21
+ };
22
+ /** What `--group-behavior context` REQUESTS in one approval round — not a dependency set. Only the
23
+ * delivery scope is required for the context path; the read scope rides along because it shares the
24
+ * round and its absence merely degrades quoted messages to a marker. */
25
+ export const FEISHU_CONTEXT_ONBOARDING_SCOPES = [
26
+ { request: FEISHU_GROUP_CONTEXT_SCOPE },
27
+ FEISHU_MESSAGE_READ_REQUEST,
28
+ ];
@@ -7,6 +7,7 @@ import { verify } from "@octokit/webhooks-methods";
7
7
  import { collect } from "../../collect.js";
8
8
  import { log } from "../../log.js";
9
9
  import { readBodyCapped } from "../body.js";
10
+ import { beginWork } from "../busy.js";
10
11
  import { text } from "../respond.js";
11
12
  /** Raw body cap before verification — GitHub caps webhook payloads at 25 MB; reject larger early. */
12
13
  const MAX_WEBHOOK_BYTES = 25 << 20;
@@ -67,7 +68,13 @@ export function githubChannel({ secret, on }) {
67
68
  const turn = `${event.deliveryId}#${i}`;
68
69
  log.info(`[github] turn start: turn=${turn} session=${session} event=${label}`);
69
70
  const startedAt = Date.now();
70
- void collect(agent.invoke({ session }, { text })).then(() => log.info(`[github] turn done: turn=${turn} session=${session} (${Date.now() - startedAt}ms)`), (error) => log.error(`[github] turn failed: turn=${turn} session=${session} (${Date.now() - startedAt}ms): ${String(error)}`));
71
+ // Post-ACK turns are process-wide in-flight work (busy.ts): a serving surface that must not
72
+ // idle mid-turn (the AgentCore /ping's HealthyBusy) has no other way to see them — and github
73
+ // turns have NO replay, so an idle reclaim here loses the review outright.
74
+ const workDone = beginWork();
75
+ void collect(agent.invoke({ session }, { text }))
76
+ .then(() => log.info(`[github] turn done: turn=${turn} session=${session} (${Date.now() - startedAt}ms)`), (error) => log.error(`[github] turn failed: turn=${turn} session=${session} (${Date.now() - startedAt}ms): ${String(error)}`))
77
+ .finally(workDone);
71
78
  }
72
79
  return new Response(null, { status: 202 });
73
80
  },
@@ -6,7 +6,7 @@
6
6
  * web stream primitives give cancellation (consumer disconnect → cancel() → iterator.return() →
7
7
  * invoke cancellation, SPEC MUST 3), backpressure (pull-based), and the body cap natively.
8
8
  *
9
- * `nodeListener` is the thin node:http adapter for the standalone `fastagent dev/start` server.
9
+ * `nodeListener` is the thin node:http adapter for the embedded `fastagent dev/start` server.
10
10
  */
11
11
  import { Readable } from "node:stream";
12
12
  import { log } from "../log.js";
@@ -8,8 +8,10 @@ import { larkChannel } from "@fastagent-sh/fastagent/lark";
8
8
  // 2. Permissions: add `im:message.p2p_msg:readonly` (direct messages), `im:message.group_at_msg:readonly`
9
9
  // (group @mentions), `im:message:send_as_bot` (reply), `im:resource` (attachments), and the
10
10
  // card scope ("Create and update card" — the live preview streams through a card). To answer bare
11
- // messages in Agent-managed threads and buffer other unsummoned group/thread context, also add the
12
- // sensitive `im:message.group_msg` scope (tenant-admin approval) and publish a new version.
11
+ // messages in threads the Agent takes part in, and buffer other unsummoned group/thread context, also add the
12
+ // sensitive `im:message.group_msg` scope (tenant-admin approval) and publish a new version. Add a
13
+ // message-read scope (e.g. `im:message:readonly`) too, so a thread's opening ask can carry the
14
+ // message it quotes; without it that quote degrades to a marker in the prompt.
13
15
  // 3. Events & Callbacks → subscribe to `im.message.receive_v1`; copy the Verification Token into
14
16
  // .env; RECOMMENDED: set an Encrypt Key there and mirror it in LARK_ENCRYPT_KEY
15
17
  // 4. run `fastagent dev --tunnel`: it attempts to switch Subscription mode to webhook + register
@@ -21,18 +23,17 @@ export default larkChannel({
21
23
  appSecret: process.env.LARK_APP_SECRET ?? "",
22
24
  verificationToken: process.env.LARK_VERIFICATION_TOKEN ?? "", // authenticates inbound events
23
25
  encryptKey: process.env.LARK_ENCRYPT_KEY || undefined, // optional; when set, plaintext events are refused
24
- // Direct and group chats default to one Agent session/thread per top-level ask. Opt out independently:
25
- // directMessageSession: "continuous",
26
- // groupMessageSession: "continuous",
26
+ // No session modes: a chat is one session and a thread is another, and where the answer goes follows
27
+ // from that (docs/design/participant-model.md).
27
28
  // Dev/personal bot: surface raw errors to the chat so you (and your AI agent) can act on them. The
28
29
  // chat is customer-facing by default — for a public bot, drop this or return a neutral string;
29
30
  // full details always go to the server log regardless.
30
31
  onError: (failed) => `⚠️ ${failed.details}`,
31
32
  // The channel owns transport + format (markdown card) + attachments (image→vision, file→disk) +
32
33
  // the live streaming preview. `route` (POLICY) is OPTIONAL — omitted, it uses defaultLarkRoute:
33
- // p2p chats always answer; groups answer on @this-bot, plus bare continuations in Agent-managed
34
- // threads. Other human group/thread discussion buffers until that place's next answered turn;
35
- // @other-only messages in managed threads buffer rather than triggering the Agent.
34
+ // p2p chats always answer; groups answer on @this-bot, plus bare messages in a thread where the
35
+ // Agent takes part and exactly ONE human does. Other human group/thread discussion buffers until
36
+ // that place's next answered turn; @other-only messages buffer rather than triggering the Agent.
36
37
  // Override to customise explicit routing, reusing the export:
37
38
  // route: (e) => defaultLarkRoute(e, { botOpenId: "ou_xxx" }) && { session: `user:${e.sender?.sender_id?.open_id}` },
38
39
  // route: (e) => defaultLarkRoute(e, { botOpenId: "ou_xxx" }) && { text: `${larkEnvelope(e)}\n[extra]` },
@@ -6,7 +6,7 @@ import { defineTool, z } from "@fastagent-sh/fastagent";
6
6
  // context line in a chat turn; a scheduled turn has no such line, so the schedule's prompt must name
7
7
  // the target chat id. tools/ is auto-discovered.
8
8
 
9
- // Standalone copy of the channel transport's discipline: a timeout so a wedged connection can't hang
9
+ // Embedded copy of the channel transport's discipline: a timeout so a wedged connection can't hang
10
10
  // the tool call (and the turn), named errors, and success gated on the body's own code===0.
11
11
  // Deliberately NO rate-limit retry — a tool error goes back to the agent, which can decide to retry;
12
12
  // fail-fast beats a silently sleeping tool.
@@ -57,9 +57,11 @@ async function tenantToken(): Promise<string> {
57
57
  export default defineTool({
58
58
  description:
59
59
  "Send a message to a Lark chat: plain `text`, or `markdown` (rendered as a card — headings, " +
60
- "bold, code blocks, links). Exactly one of the two. In a chat turn take chatId from the " +
61
- "[lark: chat …] context line; in a scheduled/woken turn (no context line) the chat id must come " +
62
- "from your instruction.",
60
+ "bold, code blocks, links). Exactly one of the two. Use it for a turn NO channel is carrying — a " +
61
+ "scheduled or self-scheduled (wake) turn or to reach a chat OTHER than the one you are " +
62
+ "answering. In a normal chat turn the channel already delivers your reply, so do NOT call this to " +
63
+ "answer (it would post the message twice). chatId comes from the [lark: chat …] context line in a " +
64
+ "chat turn; a scheduled/woken turn has no context line, so name the destination in your instruction.",
63
65
  input: z.object({
64
66
  chatId: z.string().describe("target chat id (oc_…)"),
65
67
  text: z.string().optional().describe("plain text message to send"),
@@ -8,7 +8,7 @@
8
8
  * everything platform-independent lives here, so a new event type or a wording change lands in ONE
9
9
  * place instead of one hunk per channel.
10
10
  */
11
- import type { AgentEvent } from "../agent.ts";
11
+ import type { AgentEvent, Json } from "../agent.ts";
12
12
  /** A terminal failure, as a channel hands it to its `onError`. */
13
13
  export interface ChannelFailure {
14
14
  details: string;
@@ -97,6 +97,12 @@ export declare function createPreviewPump(opts: {
97
97
  * must be diagnosable, not silent — and not a log flood. */
98
98
  onError: (error: unknown) => void;
99
99
  }): PreviewPump;
100
+ /**
101
+ * A compact, human-readable preview of a tool call's args so the live view reads `🔧 read AGENTS.md`
102
+ * rather than just `🔧 read`. Generic (a channel knows no tool schemas): show the salient value — the
103
+ * first primitive field, conventionally the subject (path / command / query / url) — else compact JSON.
104
+ */
105
+ export declare function summarizeToolArgs(args: Json): string;
100
106
  /**
101
107
  * A plain-language label for a tool call, following Slack's agent-design guidance to name what a tool
102
108
  * does rather than expose a raw identifier ("Create issue", not "create_issue"; "Github: create issue",
@@ -138,7 +138,8 @@ export function createPreviewPump(opts) {
138
138
  },
139
139
  };
140
140
  }
141
- /** Max length (code points) of a tool's arg preview in the live view. */
141
+ /** Max length (code points) of a tool's arg preview. Slack's native stream cannot be retracted, so
142
+ * this is a disclosure bound, not only a layout one. */
142
143
  const TOOL_ARG_MAX = 48;
143
144
  /** Max length (code points) of a humanized tool label. */
144
145
  const TOOL_NAME_MAX = 80;
@@ -153,7 +154,7 @@ function clip(s) {
153
154
  * rather than just `🔧 read`. Generic (a channel knows no tool schemas): show the salient value — the
154
155
  * first primitive field, conventionally the subject (path / command / query / url) — else compact JSON.
155
156
  */
156
- function summarizeToolArgs(args) {
157
+ export function summarizeToolArgs(args) {
157
158
  if (args === null || typeof args !== "object" || Array.isArray(args))
158
159
  return clip(String(args));
159
160
  const values = Object.values(args);
@@ -1,5 +1,14 @@
1
1
  import type { SlackEventEnvelope, SlackMessageEvent, SlackRoute } from "./model.ts";
2
2
  export type { SlackEventEnvelope, SlackFile, SlackMessageEvent, SlackRoute } from "./model.ts";
3
+ /** Does this text address ANYONE — a user, a broadcast, or a user group? The §3 discussion guard. */
4
+ export declare function hasSlackMention(text: string): boolean;
5
+ /** Does this text mention a USER? Distinct from the above because only a user mention can be the bot:
6
+ * a broadcast never is, so it must not make a message look like a possible summon. */
7
+ export declare function hasSlackUserMention(text: string): boolean;
8
+ /** Does this text mention this specific user (either form)? The id is MATCHED, not interpreted. */
9
+ export declare function mentionsSlackUser(text: string, userId: string): boolean;
10
+ /** Strip every mention, e.g. before matching a bare command word or building a title. */
11
+ export declare function stripSlackMentions(text: string, replacement?: string): string;
3
12
  /** Slack message events whose content represents a new human message rather than a mutation/service event. */
4
13
  export declare function isSlackHumanMessage(event: SlackMessageEvent | undefined): event is SlackMessageEvent & {
5
14
  channel: string;
@@ -14,7 +23,13 @@ export declare function slackFileIds(event: SlackMessageEvent): string[];
14
23
  /** Text plus a structural marker for file-only or captioned file shares. */
15
24
  export declare function slackMessageText(event: SlackMessageEvent): string;
16
25
  export declare function slackSenderLabel(event: SlackMessageEvent): string;
17
- /** Main-channel discussion and each concrete thread are independent context buckets. */
26
+ /** Main-channel discussion and each concrete thread are independent context buckets.
27
+ *
28
+ * The `:root:` segment carries `thread_ts`, which in Slack IS the thread's parent message and is
29
+ * stable for the life of the thread — unrelated to the `<chat>:root:<root_id>` shape Feishu retired
30
+ * (its `root_id` moves with the reply chain, so it could not identify a side conversation at all).
31
+ * Same token, different platform meaning; the key is left as-is because renaming it would discard
32
+ * live context buckets for no semantic gain. */
18
33
  export declare function slackPlaceKey(teamId: string, event: Pick<SlackMessageEvent, "channel" | "thread_ts">): string;
19
34
  export declare function slackBufferText(text: string): string;
20
35
  /** Canonical prompt envelope. The channel/thread ids also give slack-send an explicit delivery target. */
@@ -1,5 +1,42 @@
1
- import { codePointPrefix } from "../text.js";
1
+ import { BUFFER_LINE_MAX_CHARS } from "../context-buffer.js";
2
+ import { codePointPrefix, truncateCodePointPrefix } from "../text.js";
2
3
  const HUMAN_MESSAGE_SUBTYPES = new Set(["file_share", "thread_broadcast"]);
4
+ /**
5
+ * Both forms Slack writes a mention in: `<@U123>` and the labelled `<@U123|name>`. ONE definition,
6
+ * because the forms have to agree across every site that reads them — the "@-mentions only other
7
+ * people is discussion" guard, the structural bot-summon check, the stop-command strip, and the
8
+ * assistant-thread title. They drifted once: widening the first two while the strip still matched only
9
+ * the bare form turned `<@bot|name> stop` into an ordinary turn, queued behind the run it meant to stop.
10
+ */
11
+ const mentionSource = (idPattern) => String.raw `<@${idPattern}(?:\|[^>]*)?>`;
12
+ const USER_MENTION = mentionSource("[A-Z0-9]+");
13
+ /** Broadcasts and user groups address people too: `<!here>`, `<!channel>`, `<!everyone>`,
14
+ * `<!subteam^S123|@team>`. Feishu's twin guard counts every mention, and the summon rule is supposed
15
+ * to be the same on both — without these, "@here can someone look at this" in a thread the agent takes
16
+ * part in reads as a bare message addressed to IT, and gets answered. */
17
+ const ANY_MENTION = String.raw `(?:${USER_MENTION}|<!(?:here|channel|everyone)(?:\|[^>]*)?>|<!subteam\^[^>]*>)`;
18
+ /** Does this text address ANYONE — a user, a broadcast, or a user group? The §3 discussion guard. */
19
+ export function hasSlackMention(text) {
20
+ return new RegExp(ANY_MENTION, "i").test(text);
21
+ }
22
+ /** Does this text mention a USER? Distinct from the above because only a user mention can be the bot:
23
+ * a broadcast never is, so it must not make a message look like a possible summon. */
24
+ export function hasSlackUserMention(text) {
25
+ return new RegExp(USER_MENTION, "i").test(text);
26
+ }
27
+ /** Escape a platform-supplied id before it becomes part of a pattern. Slack ids are alphanumeric in
28
+ * practice, but `auth.test`'s value is not validated here, and this runs on every group message: an
29
+ * unescaped metacharacter would either mis-answer the summon question or throw on the acceptance
30
+ * path, which Slack answers with an endless redelivery. */
31
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
32
+ /** Does this text mention this specific user (either form)? The id is MATCHED, not interpreted. */
33
+ export function mentionsSlackUser(text, userId) {
34
+ return new RegExp(mentionSource(escapeRegExp(userId)), "i").test(text);
35
+ }
36
+ /** Strip every mention, e.g. before matching a bare command word or building a title. */
37
+ export function stripSlackMentions(text, replacement = " ") {
38
+ return text.replace(new RegExp(ANY_MENTION, "gi"), replacement);
39
+ }
3
40
  /** Slack message events whose content represents a new human message rather than a mutation/service event. */
4
41
  export function isSlackHumanMessage(event) {
5
42
  if (!event || (event.type !== "message" && event.type !== "app_mention"))
@@ -38,13 +75,19 @@ export function slackMessageText(event) {
38
75
  export function slackSenderLabel(event) {
39
76
  return `user ${event.user ?? "unknown"}`;
40
77
  }
41
- /** Main-channel discussion and each concrete thread are independent context buckets. */
78
+ /** Main-channel discussion and each concrete thread are independent context buckets.
79
+ *
80
+ * The `:root:` segment carries `thread_ts`, which in Slack IS the thread's parent message and is
81
+ * stable for the life of the thread — unrelated to the `<chat>:root:<root_id>` shape Feishu retired
82
+ * (its `root_id` moves with the reply chain, so it could not identify a side conversation at all).
83
+ * Same token, different platform meaning; the key is left as-is because renaming it would discard
84
+ * live context buckets for no semantic gain. */
42
85
  export function slackPlaceKey(teamId, event) {
43
86
  const base = `${teamId}:${event.channel ?? "unknown-channel"}`;
44
87
  return event.thread_ts ? `${base}:root:${event.thread_ts}` : base;
45
88
  }
46
89
  export function slackBufferText(text) {
47
- return codePointPrefix(text.replace(/\s+/g, " ").trim(), 280);
90
+ return truncateCodePointPrefix(text.replace(/\s+/g, " ").trim(), BUFFER_LINE_MAX_CHARS);
48
91
  }
49
92
  /** Canonical prompt envelope. The channel/thread ids also give slack-send an explicit delivery target. */
50
93
  export function slackEnvelope(envelope) {
@@ -1,7 +1,7 @@
1
1
  /** Slack reply rendering: native Agent streams first, rate-safe edited-message compatibility second. */
2
2
  import type { AgentEvent } from "../../agent.ts";
3
3
  import { type ChannelFailure, defaultErrorMessage } from "../preview-kit.ts";
4
- import { type SlackApi, type SlackTarget, type SlackTaskDisplayMode } from "./slack-api.ts";
4
+ import { type SlackApi, type SlackTarget } from "./slack-api.ts";
5
5
  export type SlackFailure = ChannelFailure;
6
6
  export type SlackRendering = "native" | "classic";
7
7
  export { defaultErrorMessage };
@@ -19,6 +19,5 @@ export declare function streamSlackReply(events: AsyncIterable<AgentEvent>, api:
19
19
  initialPreviewTs?: string;
20
20
  threadTitle?: string;
21
21
  disclaimer?: string | false;
22
- taskDisplay?: SlackTaskDisplayMode;
23
22
  label?: string;
24
23
  }): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { log } from "../../log.js";
2
- import { RETRY_NOTICE, THINKING_PLACEHOLDER, applyTurnEvent, composeTurnBody, createTurnView, defaultErrorMessage, humanizeToolName, revealedAnswer, toolLines, } from "../preview-kit.js";
2
+ import { RETRY_NOTICE, THINKING_PLACEHOLDER, applyTurnEvent, composeTurnBody, createTurnView, defaultErrorMessage, humanizeToolName, revealedAnswer, summarizeToolArgs, toolLines, } from "../preview-kit.js";
3
3
  import { chunkSlackMarkdown, chunkSlackText, isSlackNativeUnavailable, } from "./slack-api.js";
4
4
  export { defaultErrorMessage };
5
5
  const CLASSIC_UPDATE_INTERVAL_MS = 3_000;
@@ -16,6 +16,39 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
16
16
  export function sanitizeSlackMarkdown(markdown) {
17
17
  return markdown.replace(/<[@!][^<>]*>/g, (control) => `&lt;${control.slice(1)}`);
18
18
  }
19
+ /** Render untrusted tool names/arguments as one standard-Markdown code span. A fence longer than any
20
+ * backtick run in the value keeps the span balanced without changing the factual text. */
21
+ function inlineCode(value) {
22
+ const longest = Math.max(0, ...(value.match(/`+/g) ?? []).map((run) => run.length));
23
+ const fence = "`".repeat(longest + 1);
24
+ const content = value.startsWith("`") || value.endsWith("`") ? ` ${value} ` : value;
25
+ return `${fence}${content}${fence}`;
26
+ }
27
+ function nativeToolTrace(name, args) {
28
+ const operation = sanitizeSlackMarkdown(summarizeToolArgs(args));
29
+ return {
30
+ label: sanitizeSlackMarkdown(humanizeToolName(name)),
31
+ ...(operation ? { operation } : {}),
32
+ };
33
+ }
34
+ /** Bold a factual label. `humanizeToolName` normalizes `_` away for every ordinary identifier, but it
35
+ * falls back to the raw name when normalization empties it (a tool literally named `_`), so emphasis
36
+ * characters are escaped rather than assumed absent. */
37
+ function boldText(value) {
38
+ return `**${value.replace(/[\\*_]/g, "\\$&")}**`;
39
+ }
40
+ /**
41
+ * One trace line: the tool, what it was called on, and — on the failure line — that it failed.
42
+ *
43
+ * Tool OUTPUT never leaves the process, failed or not: the channel would have to guess the engine's
44
+ * result shape to read it, and the agent already explains a failure it recovered from in its answer.
45
+ * The failure line therefore repeats the operation instead of adding one: it states WHICH call failed
46
+ * (six `Bash` calls in a turn are otherwise indistinguishable) without exposing anything the start
47
+ * line did not already show. Operators get the detail from the logs.
48
+ */
49
+ function nativeToolLine(trace, outcome = "") {
50
+ return `${boldText(trace.label)}${outcome}${trace.operation ? ` — ${inlineCode(trace.operation)}` : ""}`;
51
+ }
19
52
  function withDisclaimer(markdown, disclaimer) {
20
53
  const body = markdown.trim() || "(no reply)";
21
54
  return disclaimer === false || !disclaimer?.trim() ? body : `${body}\n\n_${disclaimer.trim()}_`;
@@ -163,7 +196,7 @@ async function streamClassicSlackReply(events, api, target, formatError, initial
163
196
  }
164
197
  }
165
198
  }
166
- async function streamNativeSlackReply(events, api, target, formatError, initialPreviewTs, threadTitle, disclaimer, label, taskDisplayMode) {
199
+ async function streamNativeSlackReply(events, api, target, formatError, initialPreviewTs, threadTitle, disclaimer, label) {
167
200
  if (initialPreviewTs) {
168
201
  await api
169
202
  .deleteMessage(target.channelId, initialPreviewTs)
@@ -192,7 +225,7 @@ async function streamNativeSlackReply(events, api, target, formatError, initialP
192
225
  .setThreadStatus(target, status)
193
226
  .catch((error) => log.warn(`${label} could not set Slack Agent status: ${String(error)}`)));
194
227
  };
195
- const toolNames = new Map();
228
+ const toolTraces = new Map();
196
229
  let pendingText = "";
197
230
  let fullAnswer = "";
198
231
  let textTimer;
@@ -200,6 +233,8 @@ async function streamNativeSlackReply(events, api, target, formatError, initialP
200
233
  let operation = Promise.resolve();
201
234
  let renderError;
202
235
  let finalized = false;
236
+ let streamHasContent = false;
237
+ let streamEndsWithBlankLine = false;
203
238
  const enqueue = (work) => {
204
239
  operation = operation.then(async () => {
205
240
  if (renderError !== undefined)
@@ -212,14 +247,21 @@ async function streamNativeSlackReply(events, api, target, formatError, initialP
212
247
  }
213
248
  });
214
249
  };
215
- const sendContent = async (content) => {
250
+ const sendContent = async (markdown) => {
216
251
  if (streamTs) {
217
- await api.appendStream(target.channelId, streamTs, content);
252
+ await api.appendStream(target.channelId, streamTs, markdown);
218
253
  }
219
254
  else {
220
- streamTs = await api.startStream(target, content, taskDisplayMode);
255
+ streamTs = await api.startStream(target, markdown);
221
256
  }
222
257
  };
258
+ const queueMarkdown = (markdown) => {
259
+ if (!markdown)
260
+ return;
261
+ streamHasContent = true;
262
+ streamEndsWithBlankLine = markdown.endsWith("\n\n");
263
+ enqueue(() => sendContent(markdown));
264
+ };
223
265
  const flushText = (final = false) => {
224
266
  if (textTimer) {
225
267
  clearTimeout(textTimer);
@@ -239,9 +281,8 @@ async function streamNativeSlackReply(events, api, target, formatError, initialP
239
281
  if (!value)
240
282
  return;
241
283
  lastTextFlushAt = Date.now();
242
- for (const chunk of chunkSlackText(sanitizeSlackMarkdown(value))) {
243
- enqueue(() => sendContent({ markdownText: chunk }));
244
- }
284
+ for (const chunk of chunkSlackText(sanitizeSlackMarkdown(value)))
285
+ queueMarkdown(chunk);
245
286
  };
246
287
  const scheduleText = () => {
247
288
  if (textTimer)
@@ -252,9 +293,14 @@ async function streamNativeSlackReply(events, api, target, formatError, initialP
252
293
  flushText();
253
294
  }, delay);
254
295
  };
255
- const sendTask = (chunk) => {
296
+ // ponytail: a trace appended while the answer has an unclosed ``` fence lands inside it, and the
297
+ // trace's own backticks can close it early. Tracking fence parity across chunk boundaries (the job
298
+ // chunkSlackMarkdown does for the classic renderer) is the fix if a model is ever seen calling a
299
+ // tool mid-fence; the trace's blank-line framing keeps every other case well-formed.
300
+ const sendToolTrace = (line) => {
256
301
  flushText();
257
- enqueue(() => sendContent({ chunks: [chunk] }));
302
+ const separator = streamHasContent && !streamEndsWithBlankLine ? "\n\n" : "";
303
+ queueMarkdown(`${separator}${line}\n\n`);
258
304
  };
259
305
  const settleNative = async (terminalMarkdown) => {
260
306
  flushText(true);
@@ -267,12 +313,12 @@ async function streamNativeSlackReply(events, api, target, formatError, initialP
267
313
  return;
268
314
  }
269
315
  if (streamTs) {
270
- await api.stopStream(target.channelId, streamTs, { markdownText: `\n\n${GENERIC_FAILURE}` }).catch(() => { });
316
+ await api.stopStream(target.channelId, streamTs, `\n\n${GENERIC_FAILURE}`).catch(() => { });
271
317
  }
272
318
  throw renderError;
273
319
  }
274
320
  if (!streamTs)
275
- streamTs = await api.startStream(target, { markdownText: safeTerminal }, taskDisplayMode);
321
+ streamTs = await api.startStream(target, safeTerminal);
276
322
  await api.stopStream(target.channelId, streamTs);
277
323
  };
278
324
  try {
@@ -300,17 +346,15 @@ async function streamNativeSlackReply(events, api, target, formatError, initialP
300
346
  }
301
347
  }
302
348
  else if (event.type === "tool_started") {
303
- const title = humanizeToolName(event.name);
304
- toolNames.set(event.id, title);
305
- sendTask({ type: "task_update", id: event.id, title, status: "in_progress" });
349
+ const trace = nativeToolTrace(event.name, event.args);
350
+ toolTraces.set(event.id, trace);
351
+ sendToolTrace(nativeToolLine(trace));
306
352
  }
307
353
  else if (event.type === "tool_ended") {
308
- sendTask({
309
- type: "task_update",
310
- id: event.id,
311
- title: toolNames.get(event.id) ?? "Tool",
312
- status: event.isError ? "error" : "complete",
313
- });
354
+ const trace = toolTraces.get(event.id) ?? { label: "Tool" };
355
+ toolTraces.delete(event.id);
356
+ if (event.isError)
357
+ sendToolTrace(nativeToolLine(trace, " failed"));
314
358
  }
315
359
  else if (event.type === "completed") {
316
360
  finalized = true;
@@ -348,9 +392,9 @@ async function streamNativeSlackReply(events, api, target, formatError, initialP
348
392
  }
349
393
  }
350
394
  export async function streamSlackReply(events, api, target, formatError, options = {}) {
351
- const { rendering = "native", initialPreviewTs, threadTitle, disclaimer, taskDisplay = "plan", label = "[slack]", } = options;
395
+ const { rendering = "native", initialPreviewTs, threadTitle, disclaimer, label = "[slack]" } = options;
352
396
  if (rendering === "native" && target.threadTs) {
353
- return streamNativeSlackReply(events, api, target, formatError, initialPreviewTs, threadTitle, disclaimer, label, taskDisplay);
397
+ return streamNativeSlackReply(events, api, target, formatError, initialPreviewTs, threadTitle, disclaimer, label);
354
398
  }
355
399
  if (rendering === "native") {
356
400
  log.info(`${label} native streaming needs a thread target — using the classic renderer for this turn`);
@@ -20,14 +20,14 @@ export default slackChannel({
20
20
  ? Number(process.env.SLACK_BOT_TOKEN_EXPIRES_AT)
21
21
  : undefined,
22
22
  groupBehavior: "context", // default; use "mentions" only for an explicit least-privilege setup
23
- rendering: "native", // Slack Agent streams + task timeline; use "classic" only for compatibility
24
- // taskDisplay: "plan", // native task-card layout: "plan" (default) | "timeline" | "dense"
23
+ // Slack Agent stream; its inline tool traces show each call's first argument and stay in the
24
+ // delivered message. "classic" settles into the answer alone (and gives up native streaming).
25
+ rendering: "native",
25
26
  // Optional per-reply footer, if your policy requires one: aiDisclaimer: "AI-generated; verify important information.",
26
27
  // welcome: "Custom first-run DM greeting", // sent once on first DM open; false disables (default: a generic greeting)
27
28
  // reactionAck: false, // disable the 👀→✅ ack on the user's message (default on; needs reactions:write)
28
- // Direct and group asks default to independent sessions + Slack threads; opt out independently:
29
- // directMessageSession: "continuous",
30
- // groupMessageSession: "continuous",
29
+ // No session modes: an answer attaches to its question with a thread (Slack has no quote primitive),
30
+ // and that thread is the session — see docs/design/participant-model.md.
31
31
  // Dev/personal bot: surface raw errors. Remove this for a customer-facing bot; details remain in logs.
32
32
  onError: (failed) => `⚠️ ${failed.details}`,
33
33
  });
@@ -7,25 +7,6 @@ export interface SlackTarget {
7
7
  recipientUserId?: string;
8
8
  recipientTeamId?: string;
9
9
  }
10
- interface SlackMarkdownTextChunk {
11
- type: "markdown_text";
12
- text: string;
13
- }
14
- interface SlackTaskUpdateChunk {
15
- type: "task_update";
16
- id: string;
17
- title: string;
18
- status: "pending" | "in_progress" | "complete" | "error";
19
- }
20
- export type SlackStreamChunk = SlackMarkdownTextChunk | SlackTaskUpdateChunk;
21
- interface SlackStreamContent {
22
- markdownText?: string;
23
- chunks?: SlackStreamChunk[];
24
- }
25
- /** How Slack lays out task cards in a native stream (chat.startStream `task_display_mode`): `timeline`
26
- * lists steps sequentially, `plan` groups them under one heading, `dense` collapses consecutive tool
27
- * calls into one summarized card. */
28
- export type SlackTaskDisplayMode = "timeline" | "plan" | "dense";
29
10
  export interface DownloadedSlackFile {
30
11
  path: string;
31
12
  name: string;
@@ -58,9 +39,9 @@ export interface SlackApi {
58
39
  updateMarkdown(channelId: string, ts: string, markdown: string): Promise<void>;
59
40
  deleteMessage(channelId: string, ts: string): Promise<void>;
60
41
  sendMarkdown(target: SlackTarget, markdown: string): Promise<string | undefined>;
61
- startStream(target: SlackTarget, content?: SlackStreamContent, taskDisplayMode?: SlackTaskDisplayMode): Promise<string>;
62
- appendStream(channelId: string, ts: string, content: SlackStreamContent): Promise<void>;
63
- stopStream(channelId: string, ts: string, content?: SlackStreamContent): Promise<void>;
42
+ startStream(target: SlackTarget, markdown?: string): Promise<string>;
43
+ appendStream(channelId: string, ts: string, markdown: string): Promise<void>;
44
+ stopStream(channelId: string, ts: string, markdown?: string): Promise<void>;
64
45
  setThreadStatus(target: SlackTarget, status: string): Promise<void>;
65
46
  setThreadTitle(target: SlackTarget, title: string): Promise<void>;
66
47
  addReaction(channelId: string, timestamp: string, emoji: string): Promise<void>;
@@ -74,4 +55,3 @@ export declare function chunkSlackText(text: string, maxPoints?: number): string
74
55
  /** Split standard Markdown while balancing fenced code blocks across separately posted messages. */
75
56
  export declare function chunkSlackMarkdown(markdown: string, maxPoints?: number): string[];
76
57
  export declare function createSlackApi({ botToken, baseUrl }: SlackApiOptions): SlackApi;
77
- export {};
@@ -95,14 +95,6 @@ export function chunkSlackMarkdown(markdown, maxPoints = SLACK_MAX_MARKDOWN) {
95
95
  }
96
96
  return output;
97
97
  }
98
- /** Slack locks a stream to top-level `markdown_text` or `chunks` mode on its first write. Tasks need
99
- * chunks, so encode text as markdown chunks too; mixing the two modes yields `streaming_mode_mismatch`. */
100
- function streamChunks(content) {
101
- return [
102
- ...(content.markdownText ? [{ type: "markdown_text", text: content.markdownText }] : []),
103
- ...(content.chunks ?? []),
104
- ];
105
- }
106
98
  function safeFileName(file) {
107
99
  const raw = file.name ?? file.title ?? file.id ?? "file";
108
100
  const safe = raw.replace(/[/\\]/g, "_").replace(/^\.+/, "_") || "file";
@@ -310,7 +302,7 @@ export function createSlackApi({ botToken, baseUrl = "https://slack.com/api" })
310
302
  }
311
303
  return first;
312
304
  },
313
- async startStream(target, content = {}, taskDisplayMode = "plan") {
305
+ async startStream(target, markdown) {
314
306
  if (!target.threadTs)
315
307
  throw new Error("Slack native streams require a parent thread timestamp");
316
308
  const channelRecipient = target.channelId.startsWith("D")
@@ -322,32 +314,24 @@ export function createSlackApi({ botToken, baseUrl = "https://slack.com/api" })
322
314
  if (!target.channelId.startsWith("D") && (!target.recipientUserId || !target.recipientTeamId)) {
323
315
  throw new Error("Slack native channel streams require recipient user and team IDs");
324
316
  }
325
- const chunks = streamChunks(content);
326
317
  const data = await call("chat.startStream", {
327
318
  channel: target.channelId,
328
319
  thread_ts: target.threadTs,
329
- task_display_mode: taskDisplayMode,
330
320
  ...channelRecipient,
331
- ...(chunks.length ? { chunks } : {}),
321
+ ...(markdown ? { markdown_text: markdown } : {}),
332
322
  });
333
323
  if (!data.ts)
334
324
  throw new SlackApiError("chat.startStream", 200, "response carried no ts");
335
325
  return data.ts;
336
326
  },
337
- async appendStream(channelId, ts, content) {
338
- const chunks = streamChunks(content);
339
- await call("chat.appendStream", {
340
- channel: channelId,
341
- ts,
342
- ...(chunks.length ? { chunks } : {}),
343
- });
327
+ async appendStream(channelId, ts, markdown) {
328
+ await call("chat.appendStream", { channel: channelId, ts, markdown_text: markdown });
344
329
  },
345
- async stopStream(channelId, ts, content = {}) {
346
- const chunks = streamChunks(content);
330
+ async stopStream(channelId, ts, markdown) {
347
331
  await call("chat.stopStream", {
348
332
  channel: channelId,
349
333
  ts,
350
- ...(chunks.length ? { chunks } : {}),
334
+ ...(markdown ? { markdown_text: markdown } : {}),
351
335
  });
352
336
  },
353
337
  async setThreadStatus(target, status) {