@mastra/factory 0.24.0 → 0.25.0-alpha.1

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 (181) hide show
  1. package/README.md +36 -9
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +18 -4
  4. package/dist/auth.js.map +1 -1
  5. package/dist/boards/review.d.ts.map +1 -1
  6. package/dist/boards/review.js +3 -2
  7. package/dist/boards/review.js.map +1 -1
  8. package/dist/factory.d.ts +3 -0
  9. package/dist/factory.d.ts.map +1 -1
  10. package/dist/factory.js +90 -97
  11. package/dist/factory.js.map +1 -1
  12. package/dist/integrations/base.d.ts +14 -1
  13. package/dist/integrations/base.d.ts.map +1 -1
  14. package/dist/integrations/github/integration.d.ts +3 -0
  15. package/dist/integrations/github/integration.d.ts.map +1 -1
  16. package/dist/integrations/github/integration.js +4 -0
  17. package/dist/integrations/github/integration.js.map +1 -1
  18. package/dist/integrations/github/sandbox.d.ts +4 -0
  19. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  20. package/dist/integrations/github/sandbox.js +2 -0
  21. package/dist/integrations/github/sandbox.js.map +1 -1
  22. package/dist/integrations/platform/github/integration.d.ts +3 -0
  23. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  24. package/dist/integrations/platform/github/integration.js +5 -0
  25. package/dist/integrations/platform/github/integration.js.map +1 -1
  26. package/dist/{session → integrations/slack}/channel-reply-processor.d.ts +4 -3
  27. package/dist/integrations/slack/channel-reply-processor.d.ts.map +1 -0
  28. package/dist/integrations/slack/channel-reply-processor.js +61 -0
  29. package/dist/integrations/slack/channel-reply-processor.js.map +1 -0
  30. package/dist/integrations/slack/channel-session-link-processor.d.ts.map +1 -0
  31. package/dist/{session → integrations/slack}/channel-session-link-processor.js +13 -4
  32. package/dist/integrations/slack/channel-session-link-processor.js.map +1 -0
  33. package/dist/integrations/slack/integration.d.ts +25 -1
  34. package/dist/integrations/slack/integration.d.ts.map +1 -1
  35. package/dist/integrations/slack/integration.js +55 -5
  36. package/dist/integrations/slack/integration.js.map +1 -1
  37. package/dist/integrations/slack/reply-reminder-processor.d.ts +23 -0
  38. package/dist/integrations/slack/reply-reminder-processor.d.ts.map +1 -0
  39. package/dist/integrations/slack/reply-reminder-processor.js +58 -0
  40. package/dist/integrations/slack/reply-reminder-processor.js.map +1 -0
  41. package/dist/integrations/slack/reply-tool.d.ts +31 -0
  42. package/dist/integrations/slack/reply-tool.d.ts.map +1 -0
  43. package/dist/integrations/slack/reply-tool.js +203 -0
  44. package/dist/integrations/slack/reply-tool.js.map +1 -0
  45. package/dist/integrations/slack/share-file-tool.d.ts +45 -0
  46. package/dist/integrations/slack/share-file-tool.d.ts.map +1 -0
  47. package/dist/integrations/slack/share-file-tool.js +106 -0
  48. package/dist/integrations/slack/share-file-tool.js.map +1 -0
  49. package/dist/integrations/slack/slack.d.ts.map +1 -1
  50. package/dist/integrations/slack/slack.js +54 -6
  51. package/dist/integrations/slack/slack.js.map +1 -1
  52. package/dist/mcp/events.d.ts +73 -0
  53. package/dist/mcp/events.d.ts.map +1 -0
  54. package/dist/mcp/events.js +158 -0
  55. package/dist/mcp/events.js.map +1 -0
  56. package/dist/mcp/routes.d.ts +0 -1
  57. package/dist/mcp/routes.d.ts.map +1 -1
  58. package/dist/mcp/routes.js +30 -3
  59. package/dist/mcp/routes.js.map +1 -1
  60. package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
  61. package/dist/platform-auth-host.d.ts.map +1 -1
  62. package/dist/platform-auth-host.js +1 -1
  63. package/dist/platform-auth-host.js.map +1 -1
  64. package/dist/routes/config.d.ts +2 -0
  65. package/dist/routes/config.d.ts.map +1 -1
  66. package/dist/routes/config.js +52 -11
  67. package/dist/routes/config.js.map +1 -1
  68. package/dist/routes/contracts.d.ts +2 -0
  69. package/dist/routes/contracts.d.ts.map +1 -1
  70. package/dist/routes/contracts.js +1 -1
  71. package/dist/routes/contracts.js.map +1 -1
  72. package/dist/routes/session-access.d.ts +7 -2
  73. package/dist/routes/session-access.d.ts.map +1 -1
  74. package/dist/routes/session-access.js +41 -4
  75. package/dist/routes/session-access.js.map +1 -1
  76. package/dist/routes/skills.d.ts.map +1 -1
  77. package/dist/routes/skills.js +4 -3
  78. package/dist/routes/skills.js.map +1 -1
  79. package/dist/routes/source-control-sessions.d.ts +2 -0
  80. package/dist/routes/source-control-sessions.d.ts.map +1 -1
  81. package/dist/routes/source-control-sessions.js +161 -24
  82. package/dist/routes/source-control-sessions.js.map +1 -1
  83. package/dist/routes/surface.d.ts +2 -0
  84. package/dist/routes/surface.d.ts.map +1 -1
  85. package/dist/routes/surface.js +10 -4
  86. package/dist/routes/surface.js.map +1 -1
  87. package/dist/rules/dispatcher.d.ts.map +1 -1
  88. package/dist/rules/dispatcher.js +11 -3
  89. package/dist/rules/dispatcher.js.map +1 -1
  90. package/dist/rules/processor.d.ts +2 -2
  91. package/dist/rules/processor.d.ts.map +1 -1
  92. package/dist/rules/processor.js +3 -3
  93. package/dist/rules/processor.js.map +1 -1
  94. package/dist/sandbox/session-retirement.d.ts +11 -0
  95. package/dist/sandbox/session-retirement.d.ts.map +1 -1
  96. package/dist/sandbox/session-retirement.js +38 -1
  97. package/dist/sandbox/session-retirement.js.map +1 -1
  98. package/dist/sandbox/session-sandbox.d.ts +2 -0
  99. package/dist/sandbox/session-sandbox.d.ts.map +1 -1
  100. package/dist/sandbox/session-sandbox.js +5 -1
  101. package/dist/sandbox/session-sandbox.js.map +1 -1
  102. package/dist/session/credential-policy.d.ts +4 -2
  103. package/dist/session/credential-policy.d.ts.map +1 -1
  104. package/dist/session/credential-policy.js +11 -1
  105. package/dist/session/credential-policy.js.map +1 -1
  106. package/dist/session/factory-session.d.ts.map +1 -1
  107. package/dist/session/factory-session.js +14 -1
  108. package/dist/session/factory-session.js.map +1 -1
  109. package/dist/session/memory-settings-hydration.d.ts +0 -8
  110. package/dist/session/memory-settings-hydration.d.ts.map +1 -1
  111. package/dist/session/memory-settings-hydration.js +1 -19
  112. package/dist/session/memory-settings-hydration.js.map +1 -1
  113. package/dist/session/run-audit.d.ts +1 -0
  114. package/dist/session/run-audit.d.ts.map +1 -1
  115. package/dist/session/run-audit.js +7 -2
  116. package/dist/session/run-audit.js.map +1 -1
  117. package/dist/session/session-threads.d.ts +5 -0
  118. package/dist/session/session-threads.d.ts.map +1 -1
  119. package/dist/session/session-threads.js +8 -1
  120. package/dist/session/session-threads.js.map +1 -1
  121. package/dist/session/thinking-default.d.ts +6 -13
  122. package/dist/session/thinking-default.d.ts.map +1 -1
  123. package/dist/session/thinking-default.js +18 -22
  124. package/dist/session/thinking-default.js.map +1 -1
  125. package/dist/session-owner-signal.d.ts +2 -0
  126. package/dist/session-owner-signal.d.ts.map +1 -0
  127. package/dist/session-owner-signal.js +6 -0
  128. package/dist/session-owner-signal.js.map +1 -0
  129. package/dist/skills/service.d.ts +6 -7
  130. package/dist/skills/service.d.ts.map +1 -1
  131. package/dist/skills/service.js +9 -19
  132. package/dist/skills/service.js.map +1 -1
  133. package/dist/storage/domains/audit/actions.d.ts +1 -0
  134. package/dist/storage/domains/audit/actions.d.ts.map +1 -1
  135. package/dist/storage/domains/audit/actions.js +2 -1
  136. package/dist/storage/domains/audit/actions.js.map +1 -1
  137. package/dist/storage/domains/audit/base.d.ts +18 -2
  138. package/dist/storage/domains/audit/base.d.ts.map +1 -1
  139. package/dist/storage/domains/audit/base.js +22 -4
  140. package/dist/storage/domains/audit/base.js.map +1 -1
  141. package/dist/storage/domains/model-defaults/base.d.ts +8 -0
  142. package/dist/storage/domains/model-defaults/base.d.ts.map +1 -1
  143. package/dist/storage/domains/model-defaults/base.js +18 -0
  144. package/dist/storage/domains/model-defaults/base.js.map +1 -1
  145. package/dist/storage/domains/source-control/base.d.ts +26 -1
  146. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  147. package/dist/storage/domains/source-control/base.js +69 -5
  148. package/dist/storage/domains/source-control/base.js.map +1 -1
  149. package/dist/storage/domains/source-control/inmemory.d.ts +19 -1
  150. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  151. package/dist/storage/domains/source-control/inmemory.js +30 -2
  152. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  153. package/dist/tools/permissions.d.ts.map +1 -1
  154. package/dist/tools/permissions.js +2 -1
  155. package/dist/tools/permissions.js.map +1 -1
  156. package/dist/tools/sandbox-tools.d.ts +12 -0
  157. package/dist/tools/sandbox-tools.d.ts.map +1 -0
  158. package/dist/tools/sandbox-tools.js +40 -0
  159. package/dist/tools/sandbox-tools.js.map +1 -0
  160. package/dist/tools/scope.d.ts.map +1 -1
  161. package/dist/tools/scope.js +2 -5
  162. package/dist/tools/scope.js.map +1 -1
  163. package/dist/tools/session-tools.d.ts.map +1 -1
  164. package/dist/tools/session-tools.js +6 -2
  165. package/dist/tools/session-tools.js.map +1 -1
  166. package/dist/work-item-branch.d.ts +11 -3
  167. package/dist/work-item-branch.d.ts.map +1 -1
  168. package/dist/work-item-branch.js +34 -9
  169. package/dist/work-item-branch.js.map +1 -1
  170. package/dist/workspace.d.ts +2 -1
  171. package/dist/workspace.d.ts.map +1 -1
  172. package/dist/workspace.js +10 -2
  173. package/dist/workspace.js.map +1 -1
  174. package/factory-skills/configure-factory-rules/SKILL.md +2 -2
  175. package/package.json +15 -14
  176. package/dist/session/channel-reply-processor.d.ts.map +0 -1
  177. package/dist/session/channel-reply-processor.js +0 -46
  178. package/dist/session/channel-reply-processor.js.map +0 -1
  179. package/dist/session/channel-session-link-processor.d.ts.map +0 -1
  180. package/dist/session/channel-session-link-processor.js.map +0 -1
  181. /package/dist/{session → integrations/slack}/channel-session-link-processor.d.ts +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-reply-processor.js","names":[],"sources":["../../../src/integrations/slack/channel-reply-processor.ts"],"sourcesContent":["import type { ProcessInputStepArgs, Processor } from '@mastra/core/processors';\n\n/** Mirrors `REPLY_TOOL_NAME` in `reply-tool.ts`, which imports this module. */\nconst REPLY_TOOL = 'reply_in_thread';\n\nconst ID = 'factory-channel-reply';\n\nexport const SESSION_LINK_LABEL = 'Open session ↗';\n\n/**\n * Tells the agent how to open a reply when the run came in over a chat\n * channel (Slack, …): acknowledge a job in one line that names it — \"On it —\n * sleeping 30s, then running the echo\" — before starting, so the sender sees\n * a task-specific first message rather than a silent typing status until the\n * first tool finishes. The channel driver posts that opener on its own before\n * the tool widget, so the instruction and the rendering agree. The session\n * link is not the model's job: `FactoryChannelSessionLinkProcessor` appends it\n * to the first reply of a new session, so it is there whether or not the\n * model judged the request worth an opener. A run on a Slack-linked session\n * that didn't start from Slack gets a shorter note on when to post there; runs\n * with no channel at all (Studio, API, boards) get nothing.\n */\nexport class FactoryChannelReplyProcessor implements Processor {\n readonly id = ID;\n\n processInputStep({ messageList, requestContext, tools }: ProcessInputStepArgs) {\n if (!requestContext?.get('channel')) {\n // A run on a Slack-linked session that didn't start from a Slack message\n // (a web prompt, a woken run) still has the reply tool; core's channel\n // prompt says the thread is linked, this says how to use it.\n if (!tools || !(REPLY_TOOL in tools)) return undefined;\n messageList.addSystem(\n {\n role: 'system',\n content: `Post to that thread with \\`${REPLY_TOOL}\\`. This request came from outside it, so don't post an opener or progress there; post the outcome when the people in the thread asked for this work or are waiting on it, or when you're asked to. Don't write a note about what you posted.`,\n },\n ID,\n );\n return { messageList };\n }\n // With reply text hidden the thread only sees what the reply tool posts,\n // so the opener advice becomes advice about when to call it.\n const content =\n tools && REPLY_TOOL in tools\n ? [\n `Only messages you send with \\`${REPLY_TOOL}\\` reach this thread; your other text stays in the session. Don't write a note about what you posted. After a reply, either keep working or end your turn with an empty message; any text you write after posting is wasted.`,\n `When a request will take tools or more than a moment, post one short line first that says what you are about to do, then do the work and post the outcome when you finish — not a replay of the steps.`,\n `Post again in between only when you need an answer or have news the people in the thread would want. When a message needs nothing from you, don't call it.`,\n ]\n : [\n 'When a request will take tools or more than a moment to answer, reply first with one short line that says what you are about to do — \"On it — <the task in a few words>.\" — then do the work; the opener posts as its own message before any progress is shown. Give direct answers and trivial replies without an opener.',\n 'Finish with the outcome, not a replay of the steps.',\n ];\n messageList.addSystem({ role: 'system', content: content.join('\\n') }, ID);\n return { messageList };\n }\n}\n\nexport interface SessionLinkTarget {\n threadId: string;\n sessionId: string;\n resourceId: string;\n factoryProjectId?: string;\n}\n\n/**\n * The web URL for a session, or undefined without a public origin. Same\n * routes the Factory UI navigates to: a repo-backed thread's Factory session\n * id is the workspace segment; chat-only threads keep the literal `channel`\n * segment and name their session in the query; a session with no factory\n * falls back to the factory-agnostic /threads/ redirect, which forwards the\n * query through.\n */\nexport function sessionLink({\n threadId,\n sessionId,\n resourceId,\n factoryProjectId,\n}: SessionLinkTarget): string | undefined {\n const origin = process.env.MASTRACODE_PUBLIC_URL;\n if (!origin) return undefined;\n const isChatOnly = resourceId.startsWith('channel:');\n const sessionParam = `?sessionId=${encodeURIComponent(sessionId)}`;\n if (!factoryProjectId) return `${origin}/threads/${encodeURIComponent(threadId)}${sessionParam}`;\n const workspaceSegment = isChatOnly ? 'channel' : encodeURIComponent(sessionId);\n return `${origin}/factories/${encodeURIComponent(factoryProjectId)}/workspaces/${workspaceSegment}/threads/${encodeURIComponent(threadId)}${isChatOnly ? sessionParam : ''}`;\n}\n"],"mappings":";;AAGA,MAAM,aAAa;AAEnB,MAAM,KAAK;AAEX,MAAa,qBAAqB;;;;;;;;;;;;;;AAelC,IAAa,+BAAb,MAA+D;CAC7D,KAAc;CAEd,iBAAiB,EAAE,aAAa,gBAAgB,SAA+B;EAC7E,IAAI,CAAC,gBAAgB,IAAI,SAAS,GAAG;GAInC,IAAI,CAAC,SAAS,EAAE,cAAc,QAAQ,OAAO,KAAA;GAC7C,YAAY,UACV;IACE,MAAM;IACN,SAAS,8BAA8B,WAAW;GACpD,GACA,EACF;GACA,OAAO,EAAE,YAAY;EACvB;EAGA,MAAM,UACJ,SAAS,cAAc,QACnB;GACE,iCAAiC,WAAW;GAC5C;GACA;EACF,IACA,CACE,gUACA,qDACF;EACN,YAAY,UAAU;GAAE,MAAM;GAAU,SAAS,QAAQ,KAAK,IAAI;EAAE,GAAG,EAAE;EACzE,OAAO,EAAE,YAAY;CACvB;AACF;;;;;;;;;AAiBA,SAAgB,YAAY,EAC1B,UACA,WACA,YACA,oBACwC;CACxC,MAAM,SAAS,QAAQ,IAAI;CAC3B,IAAI,CAAC,QAAQ,OAAO,KAAA;CACpB,MAAM,aAAa,WAAW,WAAW,UAAU;CACnD,MAAM,eAAe,cAAc,mBAAmB,SAAS;CAC/D,IAAI,CAAC,kBAAkB,OAAO,GAAG,OAAO,WAAW,mBAAmB,QAAQ,IAAI;CAElF,OAAO,GAAG,OAAO,aAAa,mBAAmB,gBAAgB,EAAE,cAD1C,aAAa,YAAY,mBAAmB,SAAS,EACoB,WAAW,mBAAmB,QAAQ,IAAI,aAAa,eAAe;AAC1K"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-session-link-processor.d.ts","sourceRoot":"","sources":["../../../src/integrations/slack/channel-session-link-processor.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AACzG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAKrD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,+BAA+B,CAAC;AAEpE,wBAAgB,qBAAqB,CAAC,cAAc,EAAE,cAAc,GAAG,IAAI,CAE1E;AAaD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,kCAAmC,YAAW,SAAS;IAClE,QAAQ,CAAC,EAAE,kCAAkC;IAEvC,mBAAmB,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,uBAAuB,GAAG,OAAO,CAAC,SAAS,CAAC;IAmBvG,iBAAiB,CAAC,EAAE,WAAW,EAAE,cAAc,EAAE,EAAE,qBAAqB;CAiBzE"}
@@ -1,6 +1,7 @@
1
- import { readFactorySessionScope } from "../rules/binding-context.js";
1
+ import { readFactorySessionScope } from "../../rules/binding-context.js";
2
2
  import { SESSION_LINK_LABEL, sessionLink } from "./channel-reply-processor.js";
3
- //#region src/session/channel-session-link-processor.ts
3
+ import { CHAT_CHANNEL_RENDER_CONTEXT_KEY } from "@mastra/core/channels";
4
+ //#region src/integrations/slack/channel-session-link-processor.ts
4
5
  /**
5
6
  * Request-context marker a channel handler sets on the run that opens a new
6
7
  * thread. Only that run gets the link; follow-ups in the same thread don't.
@@ -45,7 +46,7 @@ const LINK_PART_ID = "factory-session-link";
45
46
  var FactoryChannelSessionLinkProcessor = class {
46
47
  id = "factory-channel-session-link";
47
48
  async processOutputStream({ part, state, requestContext }) {
48
- if (state.linked || !requestContext?.get("factory__newChannelSession")) return part;
49
+ if (state.linked || !linksThisRun(requestContext)) return part;
49
50
  if (part.type === "text-delta") {
50
51
  state.textStarted = true;
51
52
  return part;
@@ -66,7 +67,7 @@ var FactoryChannelSessionLinkProcessor = class {
66
67
  };
67
68
  }
68
69
  processOutputStep({ messageList, requestContext }) {
69
- if (!requestContext?.get("factory__newChannelSession")) return messageList;
70
+ if (!linksThisRun(requestContext)) return messageList;
70
71
  const link = sessionLinkFromRequest(requestContext);
71
72
  if (!link) return messageList;
72
73
  const suffix = linkSuffix(link);
@@ -81,6 +82,14 @@ var FactoryChannelSessionLinkProcessor = class {
81
82
  return messageList;
82
83
  }
83
84
  };
85
+ /**
86
+ * With reply text hidden the reply text never reaches the thread, and
87
+ * `reply_in_thread` appends the link to its own first post instead.
88
+ */
89
+ function linksThisRun(requestContext) {
90
+ if (!requestContext?.get("factory__newChannelSession")) return false;
91
+ return requestContext.get(CHAT_CHANNEL_RENDER_CONTEXT_KEY)?.textDisplay !== "hidden";
92
+ }
84
93
  function linkSuffix(link) {
85
94
  return `\n\n[${SESSION_LINK_LABEL}](${link})`;
86
95
  }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-session-link-processor.js","names":[],"sources":["../../../src/integrations/slack/channel-session-link-processor.ts"],"sourcesContent":["import type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport { CHAT_CHANNEL_RENDER_CONTEXT_KEY } from '@mastra/core/channels';\nimport type { ChatChannelRenderContext } from '@mastra/core/channels';\nimport type { Processor, ProcessOutputStepArgs, ProcessOutputStreamArgs } from '@mastra/core/processors';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport type { ChunkType } from '@mastra/core/stream';\n\nimport { readFactorySessionScope } from '../../rules/binding-context.js';\nimport { SESSION_LINK_LABEL, sessionLink } from './channel-reply-processor.js';\n\n/**\n * Request-context marker a channel handler sets on the run that opens a new\n * thread. Only that run gets the link; follow-ups in the same thread don't.\n */\nexport const NEW_CHANNEL_SESSION_KEY = 'factory__newChannelSession';\n\nexport function markNewChannelSession(requestContext: RequestContext): void {\n requestContext.set(NEW_CHANNEL_SESSION_KEY, true);\n}\n\n/**\n * State key the core ProcessorRunner reads to re-drive one extra part through\n * the output chain after the part a processor returned. Internal to core\n * (`processors/stream-reprocess.ts`, used by `BatchPartsProcessor`); mirrored\n * here rather than exported so Factory's one use doesn't widen core's API.\n */\nconst REPROCESS_PART_KEY = '__mastraReprocessPart';\n\n/** Text-part id of the link delta; never collides with a model block id. */\nconst LINK_PART_ID = 'factory-session-link';\n\n/**\n * Emits an `Open session ↗` link right after the first reply of a new channel\n * session, so the link closes the reply rather than leading it. The channel\n * driver ends a post at `text-end`, so Slack shows the link as its own short\n * message after the reply.\n *\n * \"End of the reply\" is not `text-end`: a reply with citations is several\n * text blocks (`text … source … text`), each with its own `text-end`. The\n * reply ends at the first `tool-call` (the opener before tools run) or at\n * `step-finish` (a reply with no tools), whichever comes first after some\n * text. A processor can only return one part, so on that chunk this returns\n * a `text-delta` carrying the link and stashes the chunk itself under\n * `REPROCESS_PART_KEY`, which the runner re-drives through the chain right\n * after — the link lands before it and downstream processors see both.\n * Factory-emitted rather than left to the model so a skipped opener can't\n * drop it. No-op without the new-session marker or a public origin.\n *\n * The streamed text is also what gets persisted, and the link is a Slack\n * display detail: in memory it would show the model its own `[↗](…)` on\n * later turns (inviting it to fabricate links) and render a self-link in\n * the web transcript. The link delta opens its own text part, so\n * `processOutputStep` drops that part from the assistant message before\n * `MessageHistory` saves it.\n */\nexport class FactoryChannelSessionLinkProcessor implements Processor {\n readonly id = 'factory-channel-session-link';\n\n async processOutputStream({ part, state, requestContext }: ProcessOutputStreamArgs): Promise<ChunkType> {\n if (state.linked || !linksThisRun(requestContext)) return part;\n if (part.type === 'text-delta') {\n state.textStarted = true;\n return part;\n }\n if ((part.type !== 'tool-call' && part.type !== 'step-finish') || !state.textStarted) return part;\n state.linked = true;\n const link = sessionLinkFromRequest(requestContext);\n if (!link) return part;\n state[REPROCESS_PART_KEY] = part;\n return {\n type: 'text-delta',\n runId: part.runId,\n from: part.from,\n payload: { id: LINK_PART_ID, text: linkSuffix(link) },\n };\n }\n\n processOutputStep({ messageList, requestContext }: ProcessOutputStepArgs) {\n if (!linksThisRun(requestContext)) return messageList;\n const link = sessionLinkFromRequest(requestContext);\n if (!link) return messageList;\n const suffix = linkSuffix(link);\n for (const message of messageList.get.response.db()) {\n if (message.role !== 'assistant') continue;\n const index = message.content.parts.findIndex(part => part.type === 'text' && part.text === suffix);\n if (index === -1) continue;\n message.content.parts.splice(index, 1);\n if (typeof message.content.content === 'string' && message.content.content.endsWith(suffix)) {\n message.content.content = message.content.content.slice(0, -suffix.length);\n }\n return messageList;\n }\n return messageList;\n }\n}\n\n/**\n * With reply text hidden the reply text never reaches the thread, and\n * `reply_in_thread` appends the link to its own first post instead.\n */\nfunction linksThisRun(requestContext: RequestContext | undefined): requestContext is RequestContext {\n if (!requestContext?.get(NEW_CHANNEL_SESSION_KEY)) return false;\n const render = requestContext.get(CHAT_CHANNEL_RENDER_CONTEXT_KEY) as ChatChannelRenderContext | undefined;\n return render?.textDisplay !== 'hidden';\n}\n\nfunction linkSuffix(link: string): string {\n // Own paragraph: the link closes the message, it is not part of the last sentence.\n return `\\n\\n[${SESSION_LINK_LABEL}](${link})`;\n}\n\nfunction sessionLinkFromRequest(requestContext: RequestContext): string | undefined {\n const controller = requestContext.get('controller') as AgentControllerRequestContext | undefined;\n if (!controller?.threadId) return undefined;\n return sessionLink({\n threadId: controller.threadId,\n sessionId: controller.sessionId,\n resourceId: controller.resourceId,\n factoryProjectId: readFactorySessionScope(requestContext)?.factoryProjectId,\n });\n}\n"],"mappings":";;;;;;;;AAcA,MAAa,0BAA0B;AAEvC,SAAgB,sBAAsB,gBAAsC;CAC1E,eAAe,IAAI,yBAAyB,IAAI;AAClD;;;;;;;AAQA,MAAM,qBAAqB;;AAG3B,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;AA0BrB,IAAa,qCAAb,MAAqE;CACnE,KAAc;CAEd,MAAM,oBAAoB,EAAE,MAAM,OAAO,kBAA+D;EACtG,IAAI,MAAM,UAAU,CAAC,aAAa,cAAc,GAAG,OAAO;EAC1D,IAAI,KAAK,SAAS,cAAc;GAC9B,MAAM,cAAc;GACpB,OAAO;EACT;EACA,IAAK,KAAK,SAAS,eAAe,KAAK,SAAS,iBAAkB,CAAC,MAAM,aAAa,OAAO;EAC7F,MAAM,SAAS;EACf,MAAM,OAAO,uBAAuB,cAAc;EAClD,IAAI,CAAC,MAAM,OAAO;EAClB,MAAM,sBAAsB;EAC5B,OAAO;GACL,MAAM;GACN,OAAO,KAAK;GACZ,MAAM,KAAK;GACX,SAAS;IAAE,IAAI;IAAc,MAAM,WAAW,IAAI;GAAE;EACtD;CACF;CAEA,kBAAkB,EAAE,aAAa,kBAAyC;EACxE,IAAI,CAAC,aAAa,cAAc,GAAG,OAAO;EAC1C,MAAM,OAAO,uBAAuB,cAAc;EAClD,IAAI,CAAC,MAAM,OAAO;EAClB,MAAM,SAAS,WAAW,IAAI;EAC9B,KAAK,MAAM,WAAW,YAAY,IAAI,SAAS,GAAG,GAAG;GACnD,IAAI,QAAQ,SAAS,aAAa;GAClC,MAAM,QAAQ,QAAQ,QAAQ,MAAM,WAAU,SAAQ,KAAK,SAAS,UAAU,KAAK,SAAS,MAAM;GAClG,IAAI,UAAU,IAAI;GAClB,QAAQ,QAAQ,MAAM,OAAO,OAAO,CAAC;GACrC,IAAI,OAAO,QAAQ,QAAQ,YAAY,YAAY,QAAQ,QAAQ,QAAQ,SAAS,MAAM,GACxF,QAAQ,QAAQ,UAAU,QAAQ,QAAQ,QAAQ,MAAM,GAAG,CAAC,OAAO,MAAM;GAE3E,OAAO;EACT;EACA,OAAO;CACT;AACF;;;;;AAMA,SAAS,aAAa,gBAA8E;CAClG,IAAI,CAAC,gBAAgB,IAAA,4BAA2B,GAAG,OAAO;CAE1D,OADe,eAAe,IAAI,+BACtB,CAAC,EAAE,gBAAgB;AACjC;AAEA,SAAS,WAAW,MAAsB;CAExC,OAAO,QAAQ,mBAAmB,IAAI,KAAK;AAC7C;AAEA,SAAS,uBAAuB,gBAAoD;CAClF,MAAM,aAAa,eAAe,IAAI,YAAY;CAClD,IAAI,CAAC,YAAY,UAAU,OAAO,KAAA;CAClC,OAAO,YAAY;EACjB,UAAU,WAAW;EACrB,WAAW,WAAW;EACtB,YAAY,WAAW;EACvB,kBAAkB,wBAAwB,cAAc,CAAC,EAAE;CAC7D,CAAC;AACH"}
@@ -17,10 +17,13 @@
17
17
  * of the box). The integration interface remains open — third parties still add
18
18
  * capabilities by implementing `FactoryIntegration` from outside the package.
19
19
  */
20
+ import type { AgentChannels } from '@mastra/core/channels';
21
+ import type { InputProcessor, OutputProcessor } from '@mastra/core/processors';
22
+ import type { RequestContext } from '@mastra/core/request-context';
20
23
  import type { ApiRoute } from '@mastra/core/server';
21
24
  import type { SlackAdapterChannelConfig } from '@mastra/slack';
22
25
  import type { WorkItemFeedPublisher } from '../../storage/domains/comments/feed-sync.js';
23
- import type { FactoryChannelsConfig, FactoryIntegration, IntegrationContext } from '../base.js';
26
+ import type { FactoryChannelsConfig, FactoryIntegration, IntegrationContext, IntegrationTools } from '../base.js';
24
27
  /**
25
28
  * Slack app credentials, read from env ONCE by the deploy entry. `signingSecret`
26
29
  * is required because the Slack adapter validates it at construction — an
@@ -71,7 +74,28 @@ export declare class SlackIntegration implements FactoryIntegration {
71
74
  * replica signed.
72
75
  */
73
76
  readonly requiresStableStateSigner = true;
77
+ /**
78
+ * With reply text hidden (the Factory default) the agent speaks through
79
+ * `reply_in_thread`, so the prompt, reminder and live streaming for that
80
+ * tool come with it. The setting is fixed for the deployment, so it is
81
+ * decided once here rather than on every run.
82
+ */
83
+ readonly processors: {
84
+ input: InputProcessor[];
85
+ output: OutputProcessor[];
86
+ };
74
87
  constructor(config: SlackIntegrationConfig);
88
+ initialize(args: {
89
+ channels?: () => AgentChannels | null | undefined;
90
+ }): void;
91
+ /**
92
+ * `attach_file`, and `reply_in_thread` when reply text is hidden, on runs
93
+ * whose session is linked to a Slack thread. The tools find the thread to
94
+ * post into when they are called.
95
+ */
96
+ agentTools({ requestContext }: {
97
+ requestContext: RequestContext;
98
+ }): Promise<IntegrationTools>;
75
99
  channels(ctx: IntegrationContext): FactoryChannelsConfig;
76
100
  feedPublisher(ctx: IntegrationContext): WorkItemFeedPublisher;
77
101
  routes(ctx: IntegrationContext): ApiRoute[];
@@ -1 +1 @@
1
- {"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../src/integrations/slack/integration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAE/D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,6CAA6C,CAAC;AACzF,OAAO,KAAK,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAMhG;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,2DAA2D;IAC3D,aAAa,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,EAAE,yBAAyB,CAAC;CAC5C;AA4DD,qBAAa,gBAAiB,YAAW,kBAAkB;;IACzD,QAAQ,CAAC,EAAE,WAAW;IACtB;;;;OAIG;IACH,QAAQ,CAAC,yBAAyB,QAAQ;gBAU9B,MAAM,EAAE,sBAAsB;IAS1C,QAAQ,CAAC,GAAG,EAAE,kBAAkB,GAAG,qBAAqB;IAuBxD,aAAa,CAAC,GAAG,EAAE,kBAAkB,GAAG,qBAAqB;IAI7D,MAAM,CAAC,GAAG,EAAE,kBAAkB,GAAG,QAAQ,EAAE;IAc3C,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CASvC"}
1
+ {"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../src/integrations/slack/integration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,uBAAuB,CAAC;AAC3E,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAE/D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,6CAA6C,CAAC;AAEzF,OAAO,KAAK,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAWlH;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,2DAA2D;IAC3D,aAAa,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,EAAE,yBAAyB,CAAC;CAC5C;AAyED,qBAAa,gBAAiB,YAAW,kBAAkB;;IACzD,QAAQ,CAAC,EAAE,WAAW;IACtB;;;;OAIG;IACH,QAAQ,CAAC,yBAAyB,QAAQ;IAO1C;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,EAAE;QAAE,KAAK,EAAE,cAAc,EAAE,CAAC;QAAC,MAAM,EAAE,eAAe,EAAE,CAAA;KAAE,CAAC;gBAQhE,MAAM,EAAE,sBAAsB;IAyB1C,UAAU,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,aAAa,GAAG,IAAI,GAAG,SAAS,CAAA;KAAE,GAAG,IAAI;IAI7E;;;;OAIG;IACG,UAAU,CAAC,EAAE,cAAc,EAAE,EAAE;QAAE,cAAc,EAAE,cAAc,CAAA;KAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAQnG,QAAQ,CAAC,GAAG,EAAE,kBAAkB,GAAG,qBAAqB;IAuBxD,aAAa,CAAC,GAAG,EAAE,kBAAkB,GAAG,qBAAqB;IAI7D,MAAM,CAAC,GAAG,EAAE,kBAAkB,GAAG,QAAQ,EAAE;IAc3C,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CASvC"}
@@ -1,5 +1,10 @@
1
+ import { FactoryChannelReplyProcessor } from "./channel-reply-processor.js";
2
+ import { FactoryChannelSessionLinkProcessor } from "./channel-session-link-processor.js";
1
3
  import { createSlackConnectRoutes } from "./connect-route.js";
2
4
  import { SlackFeedPublisher } from "./feed-publisher.js";
5
+ import { createShareFileTool, isSlackThread } from "./share-file-tool.js";
6
+ import { FactoryReplyStreamProcessor, createReplyTool } from "./reply-tool.js";
7
+ import { FactoryReplyReminderProcessor } from "./reply-reminder-processor.js";
3
8
  import { createSlackChannelsConfig } from "./slack.js";
4
9
  //#region src/integrations/slack/integration.ts
5
10
  /** Slack renders the status inline next to the app name; keep it to one short line. */
@@ -18,11 +23,19 @@ function describeToolCall(args) {
18
23
  const text = line.length > MAX_STATUS_LENGTH ? `${line.slice(0, 79)}…` : line;
19
24
  return `is ${/^[A-Z][a-z]/.test(text) ? text.charAt(0).toLowerCase() + text.slice(1) : text}`;
20
25
  }
21
- const factoryTypingStatus = (chunk) => {
26
+ /**
27
+ * With reply text hidden, streamed text never reaches the thread, so it gets
28
+ * no status: an "is typing..." set after the reply tool's post would sit on
29
+ * the thread with nothing coming, and race the clear at the end of the run.
30
+ */
31
+ const factoryTypingStatus = (textDisplay) => (chunk) => {
22
32
  switch (chunk.type) {
23
33
  case "reasoning-delta": return "is thinking...";
24
- case "text-delta": return "is typing...";
25
- case "tool-call": return describeToolCall(chunk.payload.args) ?? "is working...";
34
+ case "text-delta": return textDisplay === "hidden" ? void 0 : "is typing...";
35
+ case "tool-call-input-streaming-start": return chunk.payload.toolName === "reply_in_thread" ? "is typing..." : void 0;
36
+ case "tool-call":
37
+ if (chunk.payload.toolName === "reply_in_thread") return "is typing...";
38
+ return describeToolCall(chunk.payload.args) ?? "is working...";
26
39
  default: return;
27
40
  }
28
41
  };
@@ -42,9 +55,11 @@ function adapterOverrides(options) {
42
55
  * who want Slack's "Thinking Steps" widget.
43
56
  */
44
57
  function factoryAdapterOptions(overrides) {
58
+ const textDisplay = overrides.textDisplay ?? "hidden";
45
59
  return {
46
- typingStatus: factoryTypingStatus,
60
+ typingStatus: factoryTypingStatus(textDisplay),
47
61
  toolDisplay: "hidden",
62
+ textDisplay,
48
63
  ...overrides
49
64
  };
50
65
  }
@@ -57,6 +72,17 @@ var SlackIntegration = class {
57
72
  */
58
73
  requiresStableStateSigner = true;
59
74
  #config;
75
+ #adapterOptions;
76
+ /** Bound in `initialize()`; the controller mounts after integrations are built. */
77
+ #channels;
78
+ #deps = { channels: () => this.#channels?.() };
79
+ /**
80
+ * With reply text hidden (the Factory default) the agent speaks through
81
+ * `reply_in_thread`, so the prompt, reminder and live streaming for that
82
+ * tool come with it. The setting is fixed for the deployment, so it is
83
+ * decided once here rather than on every run.
84
+ */
85
+ processors;
60
86
  /**
61
87
  * Whether `channels()` found a source-control owner on the context and wired
62
88
  * repo-backed sessions. Set at the channels() attach path, which runs once at
@@ -66,6 +92,30 @@ var SlackIntegration = class {
66
92
  constructor(config) {
67
93
  if (!config.signingSecret) throw new Error("SlackIntegration: 'signingSecret' is required — Slack cannot verify inbound requests without it.");
68
94
  this.#config = config;
95
+ this.#adapterOptions = factoryAdapterOptions(adapterOverrides(config.adapterOptions));
96
+ const textHidden = this.#textHidden;
97
+ this.processors = {
98
+ input: [new FactoryChannelReplyProcessor(), ...textHidden ? [new FactoryReplyReminderProcessor()] : []],
99
+ output: [new FactoryChannelSessionLinkProcessor(), ...textHidden ? [new FactoryReplyStreamProcessor(this.#deps)] : []]
100
+ };
101
+ }
102
+ get #textHidden() {
103
+ return this.#adapterOptions.textDisplay === "hidden";
104
+ }
105
+ initialize(args) {
106
+ this.#channels = args.channels;
107
+ }
108
+ /**
109
+ * `attach_file`, and `reply_in_thread` when reply text is hidden, on runs
110
+ * whose session is linked to a Slack thread. The tools find the thread to
111
+ * post into when they are called.
112
+ */
113
+ async agentTools({ requestContext }) {
114
+ if (!await isSlackThread(requestContext, this.#deps)) return {};
115
+ return {
116
+ ...createShareFileTool(requestContext, this.#deps),
117
+ ...this.#textHidden ? createReplyTool(requestContext, this.#deps) : {}
118
+ };
69
119
  }
70
120
  channels(ctx) {
71
121
  const sourceControls = ctx.storage.sourceControls ?? (ctx.storage.sourceControlOwner ? [ctx.storage.sourceControlOwner] : []);
@@ -83,7 +133,7 @@ var SlackIntegration = class {
83
133
  modelDefaults: ctx.storage.modelDefaults,
84
134
  workItems: ctx.runtime?.workItems,
85
135
  feed: ctx.feed,
86
- adapterOptions: factoryAdapterOptions(adapterOverrides(this.#config.adapterOptions))
136
+ adapterOptions: this.#adapterOptions
87
137
  });
88
138
  }
89
139
  feedPublisher(ctx) {
@@ -1 +1 @@
1
- {"version":3,"file":"integration.js","names":[],"sources":["../../../src/integrations/slack/integration.ts"],"sourcesContent":["/**\n * `SlackIntegration` — Slack as a `FactoryIntegration`.\n *\n * Slack contributes two things to a factory: the chat channels that carry\n * inbound messages into agent runs (`channels()`), and the browser-facing\n * account-link routes that bind a Slack sender to a Mastra tenant (`routes()`).\n * Both used to be assembled by hand in the deploy entry, which had to reach\n * into the prepared agent controller by string key, resolve storage domains\n * itself, mint its own state signer, and splice routes onto the factory's\n * assembled server config. Implementing the integration interface instead means\n * the factory does all of that the same way it already does for GitHub and\n * Linear, and the entry's job shrinks to reading Slack's env vars once.\n *\n * Slack ships as a factory built-in alongside GitHub and Linear (it originally\n * lived in `mastracode/web` to keep `@mastra/slack`/`chat` out of this package;\n * the team reversed that so `create-factory` consumers get Slack channels out\n * of the box). The integration interface remains open — third parties still add\n * capabilities by implementing `FactoryIntegration` from outside the package.\n */\n\nimport type { TypingStatusFn } from '@mastra/core/channels';\nimport type { ApiRoute } from '@mastra/core/server';\nimport type { SlackAdapterChannelConfig } from '@mastra/slack';\n\nimport type { WorkItemFeedPublisher } from '../../storage/domains/comments/feed-sync.js';\nimport type { FactoryChannelsConfig, FactoryIntegration, IntegrationContext } from '../base.js';\n\nimport { createSlackConnectRoutes } from './connect-route.js';\nimport { SlackFeedPublisher } from './feed-publisher.js';\nimport { createSlackChannelsConfig } from './slack.js';\n\n/**\n * Slack app credentials, read from env ONCE by the deploy entry. `signingSecret`\n * is required because the Slack adapter validates it at construction — an\n * integration constructed without it would throw during `prepare()` rather than\n * reporting itself unconfigured.\n */\nexport interface SlackIntegrationConfig {\n /** Verifies inbound Slack request signatures. Required. */\n signingSecret: string;\n /** Bot token used to post replies and ephemeral cards. */\n botToken?: string;\n /**\n * OAuth client credentials. Required for account linking: \"Sign in with\n * Slack\" (OIDC) is the only flow that writes a link, so without these the\n * settings surface reports `canConnect: false` and no sender can link.\n */\n clientId?: string;\n clientSecret?: string;\n /**\n * HTTPS origin Slack redirects back to. Slack requires HTTPS, so locally this\n * is the tunnel origin rather than the app's own public URL.\n */\n oidcRedirectBaseUrl?: string;\n /** SPA origin the post-connect redirect returns to. */\n uiOrigin?: string;\n /**\n * Overrides for the Slack channel adapter entry.\n *\n * @example\n * ```ts\n * new SlackIntegration({\n * signingSecret,\n * adapterOptions: {\n * streaming: true,\n * toolDisplay: 'grouped',\n * },\n * });\n * ```\n */\n adapterOptions?: SlackAdapterChannelConfig;\n}\n\n/** Slack renders the status inline next to the app name; keep it to one short line. */\nconst MAX_STATUS_LENGTH = 80;\n\n/**\n * Turns a tool's `description` arg (\"Listing the repo root\") into a status\n * line that reads after the app name (\"<App> is listing the repo root…\").\n * Only the first line counts: issue/PR creators carry whole bodies there.\n */\nfunction describeToolCall(args: unknown): string | undefined {\n if (!args || typeof args !== 'object') return undefined;\n const { description } = args as { description?: unknown };\n if (typeof description !== 'string') return undefined;\n const line = description\n .split('\\n')\n .map(part => part.replace(/\\s+/g, ' ').trim())\n .find(Boolean);\n if (!line) return undefined;\n const text = line.length > MAX_STATUS_LENGTH ? `${line.slice(0, MAX_STATUS_LENGTH - 1)}…` : line;\n // Drop the sentence-case capital (\"Listing\" → \"listing\") but leave acronyms\n // and identifiers (\"PR lookup\", \"CI status\") alone.\n return `is ${/^[A-Z][a-z]/.test(text) ? text.charAt(0).toLowerCase() + text.slice(1) : text}`;\n}\n\nconst factoryTypingStatus: TypingStatusFn = chunk => {\n switch (chunk.type) {\n case 'reasoning-delta':\n return 'is thinking...';\n case 'text-delta':\n return 'is typing...';\n case 'tool-call':\n return describeToolCall(chunk.payload.args) ?? 'is working...';\n default:\n return undefined;\n }\n};\n\n/**\n * Drops keys the caller set to `undefined` so spreading the overrides cannot\n * erase the Factory defaults they sit on top of.\n */\nfunction adapterOverrides(options: SlackAdapterChannelConfig | undefined): SlackAdapterChannelConfig {\n return Object.fromEntries(\n Object.entries(options ?? {}).filter(([, value]) => value !== undefined),\n ) as SlackAdapterChannelConfig;\n}\n\n/**\n * Factory defaults under the caller's overrides. Tool calls stay out of the\n * thread: a Factory session runs hundreds of them, and a row per call (or a\n * card per call) buries the reply. Progress shows through the typing status\n * instead, which names the tool's current step; the session link carries the\n * full trace. `grouped` remains available via `adapterOptions` for callers\n * who want Slack's \"Thinking Steps\" widget.\n */\nfunction factoryAdapterOptions(overrides: SlackAdapterChannelConfig): SlackAdapterChannelConfig {\n return { typingStatus: factoryTypingStatus, toolDisplay: 'hidden', ...overrides };\n}\n\nexport class SlackIntegration implements FactoryIntegration {\n readonly id = 'slack';\n /**\n * The OIDC connect flow round-trips a signed `state` through Slack, so the\n * replica handling the callback must be able to verify a state a different\n * replica signed.\n */\n readonly requiresStableStateSigner = true;\n\n readonly #config: SlackIntegrationConfig;\n /**\n * Whether `channels()` found a source-control owner on the context and wired\n * repo-backed sessions. Set at the channels() attach path, which runs once at\n * boot before diagnostics are served.\n */\n #repoBackedSessions = false;\n\n constructor(config: SlackIntegrationConfig) {\n if (!config.signingSecret) {\n throw new Error(\n \"SlackIntegration: 'signingSecret' is required — Slack cannot verify inbound requests without it.\",\n );\n }\n this.#config = config;\n }\n\n channels(ctx: IntegrationContext): FactoryChannelsConfig {\n // Repo-backed sessions select the source-control provider from the linked\n // repository on each Factory project.\n const sourceControls =\n ctx.storage.sourceControls ?? (ctx.storage.sourceControlOwner ? [ctx.storage.sourceControlOwner] : []);\n this.#repoBackedSessions = sourceControls.length > 0;\n return createSlackChannelsConfig({\n slack: {\n clientId: this.#config.clientId,\n clientSecret: this.#config.clientSecret,\n signingSecret: this.#config.signingSecret,\n botToken: this.#config.botToken,\n },\n accountLinks: ctx.storage.channelIdentity,\n projects: ctx.storage.projects,\n sourceControls,\n modelDefaults: ctx.storage.modelDefaults,\n workItems: ctx.runtime?.workItems,\n feed: ctx.feed,\n adapterOptions: factoryAdapterOptions(adapterOverrides(this.#config.adapterOptions)),\n });\n }\n\n feedPublisher(ctx: IntegrationContext): WorkItemFeedPublisher {\n return new SlackFeedPublisher({ controller: ctx.controller });\n }\n\n routes(ctx: IntegrationContext): ApiRoute[] {\n const { clientId, clientSecret, oidcRedirectBaseUrl, uiOrigin } = this.#config;\n return createSlackConnectRoutes({\n auth: ctx.auth,\n accountLinks: ctx.storage.channelIdentity,\n tenantStateSigner: ctx.stateSigner,\n oidc:\n clientId && clientSecret && oidcRedirectBaseUrl\n ? { clientId, clientSecret, redirectBaseUrl: oidcRedirectBaseUrl, uiOrigin }\n : undefined,\n projects: ctx.storage.projects,\n });\n }\n\n diagnostics(): Record<string, unknown> {\n const { clientId, clientSecret, botToken, oidcRedirectBaseUrl } = this.#config;\n return {\n configured: true,\n botTokenConfigured: Boolean(botToken),\n oidcConfigured: Boolean(clientId && clientSecret && oidcRedirectBaseUrl),\n repoBackedSessions: this.#repoBackedSessions,\n };\n }\n}\n"],"mappings":";;;;;AA0EA,MAAM,oBAAoB;;;;;;AAO1B,SAAS,iBAAiB,MAAmC;CAC3D,IAAI,CAAC,QAAQ,OAAO,SAAS,UAAU,OAAO,KAAA;CAC9C,MAAM,EAAE,gBAAgB;CACxB,IAAI,OAAO,gBAAgB,UAAU,OAAO,KAAA;CAC5C,MAAM,OAAO,YACV,MAAM,IAAI,CAAC,CACX,KAAI,SAAQ,KAAK,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAC7C,KAAK,OAAO;CACf,IAAI,CAAC,MAAM,OAAO,KAAA;CAClB,MAAM,OAAO,KAAK,SAAS,oBAAoB,GAAG,KAAK,MAAM,GAAG,EAAqB,EAAE,KAAK;CAG5F,OAAO,MAAM,cAAc,KAAK,IAAI,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,KAAK,MAAM,CAAC,IAAI;AACzF;AAEA,MAAM,uBAAsC,UAAS;CACnD,QAAQ,MAAM,MAAd;EACE,KAAK,mBACH,OAAO;EACT,KAAK,cACH,OAAO;EACT,KAAK,aACH,OAAO,iBAAiB,MAAM,QAAQ,IAAI,KAAK;EACjD,SACE;CACJ;AACF;;;;;AAMA,SAAS,iBAAiB,SAA2E;CACnG,OAAO,OAAO,YACZ,OAAO,QAAQ,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,GAAG,WAAW,UAAU,KAAA,CAAS,CACzE;AACF;;;;;;;;;AAUA,SAAS,sBAAsB,WAAiE;CAC9F,OAAO;EAAE,cAAc;EAAqB,aAAa;EAAU,GAAG;CAAU;AAClF;AAEA,IAAa,mBAAb,MAA4D;CAC1D,KAAc;;;;;;CAMd,4BAAqC;CAErC;;;;;;CAMA,sBAAsB;CAEtB,YAAY,QAAgC;EAC1C,IAAI,CAAC,OAAO,eACV,MAAM,IAAI,MACR,kGACF;EAEF,KAAK,UAAU;CACjB;CAEA,SAAS,KAAgD;EAGvD,MAAM,iBACJ,IAAI,QAAQ,mBAAmB,IAAI,QAAQ,qBAAqB,CAAC,IAAI,QAAQ,kBAAkB,IAAI,CAAC;EACtG,KAAK,sBAAsB,eAAe,SAAS;EACnD,OAAO,0BAA0B;GAC/B,OAAO;IACL,UAAU,KAAK,QAAQ;IACvB,cAAc,KAAK,QAAQ;IAC3B,eAAe,KAAK,QAAQ;IAC5B,UAAU,KAAK,QAAQ;GACzB;GACA,cAAc,IAAI,QAAQ;GAC1B,UAAU,IAAI,QAAQ;GACtB;GACA,eAAe,IAAI,QAAQ;GAC3B,WAAW,IAAI,SAAS;GACxB,MAAM,IAAI;GACV,gBAAgB,sBAAsB,iBAAiB,KAAK,QAAQ,cAAc,CAAC;EACrF,CAAC;CACH;CAEA,cAAc,KAAgD;EAC5D,OAAO,IAAI,mBAAmB,EAAE,YAAY,IAAI,WAAW,CAAC;CAC9D;CAEA,OAAO,KAAqC;EAC1C,MAAM,EAAE,UAAU,cAAc,qBAAqB,aAAa,KAAK;EACvE,OAAO,yBAAyB;GAC9B,MAAM,IAAI;GACV,cAAc,IAAI,QAAQ;GAC1B,mBAAmB,IAAI;GACvB,MACE,YAAY,gBAAgB,sBACxB;IAAE;IAAU;IAAc,iBAAiB;IAAqB;GAAS,IACzE,KAAA;GACN,UAAU,IAAI,QAAQ;EACxB,CAAC;CACH;CAEA,cAAuC;EACrC,MAAM,EAAE,UAAU,cAAc,UAAU,wBAAwB,KAAK;EACvE,OAAO;GACL,YAAY;GACZ,oBAAoB,QAAQ,QAAQ;GACpC,gBAAgB,QAAQ,YAAY,gBAAgB,mBAAmB;GACvE,oBAAoB,KAAK;EAC3B;CACF;AACF"}
1
+ {"version":3,"file":"integration.js","names":[],"sources":["../../../src/integrations/slack/integration.ts"],"sourcesContent":["/**\n * `SlackIntegration` — Slack as a `FactoryIntegration`.\n *\n * Slack contributes two things to a factory: the chat channels that carry\n * inbound messages into agent runs (`channels()`), and the browser-facing\n * account-link routes that bind a Slack sender to a Mastra tenant (`routes()`).\n * Both used to be assembled by hand in the deploy entry, which had to reach\n * into the prepared agent controller by string key, resolve storage domains\n * itself, mint its own state signer, and splice routes onto the factory's\n * assembled server config. Implementing the integration interface instead means\n * the factory does all of that the same way it already does for GitHub and\n * Linear, and the entry's job shrinks to reading Slack's env vars once.\n *\n * Slack ships as a factory built-in alongside GitHub and Linear (it originally\n * lived in `mastracode/web` to keep `@mastra/slack`/`chat` out of this package;\n * the team reversed that so `create-factory` consumers get Slack channels out\n * of the box). The integration interface remains open — third parties still add\n * capabilities by implementing `FactoryIntegration` from outside the package.\n */\n\nimport type { AgentChannels, TypingStatusFn } from '@mastra/core/channels';\nimport type { InputProcessor, OutputProcessor } from '@mastra/core/processors';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport type { ApiRoute } from '@mastra/core/server';\nimport type { SlackAdapterChannelConfig } from '@mastra/slack';\n\nimport type { WorkItemFeedPublisher } from '../../storage/domains/comments/feed-sync.js';\n\nimport type { FactoryChannelsConfig, FactoryIntegration, IntegrationContext, IntegrationTools } from '../base.js';\nimport { FactoryChannelReplyProcessor } from './channel-reply-processor.js';\nimport { FactoryChannelSessionLinkProcessor } from './channel-session-link-processor.js';\nimport { createSlackConnectRoutes } from './connect-route.js';\nimport { SlackFeedPublisher } from './feed-publisher.js';\nimport { FactoryReplyReminderProcessor } from './reply-reminder-processor.js';\nimport { createReplyTool, FactoryReplyStreamProcessor, REPLY_TOOL_NAME } from './reply-tool.js';\nimport { createShareFileTool, isSlackThread } from './share-file-tool.js';\nimport type { ShareFileToolDeps } from './share-file-tool.js';\nimport { createSlackChannelsConfig } from './slack.js';\n\n/**\n * Slack app credentials, read from env ONCE by the deploy entry. `signingSecret`\n * is required because the Slack adapter validates it at construction — an\n * integration constructed without it would throw during `prepare()` rather than\n * reporting itself unconfigured.\n */\nexport interface SlackIntegrationConfig {\n /** Verifies inbound Slack request signatures. Required. */\n signingSecret: string;\n /** Bot token used to post replies and ephemeral cards. */\n botToken?: string;\n /**\n * OAuth client credentials. Required for account linking: \"Sign in with\n * Slack\" (OIDC) is the only flow that writes a link, so without these the\n * settings surface reports `canConnect: false` and no sender can link.\n */\n clientId?: string;\n clientSecret?: string;\n /**\n * HTTPS origin Slack redirects back to. Slack requires HTTPS, so locally this\n * is the tunnel origin rather than the app's own public URL.\n */\n oidcRedirectBaseUrl?: string;\n /** SPA origin the post-connect redirect returns to. */\n uiOrigin?: string;\n /**\n * Overrides for the Slack channel adapter entry.\n *\n * @example\n * ```ts\n * new SlackIntegration({\n * signingSecret,\n * adapterOptions: {\n * streaming: true,\n * toolDisplay: 'grouped',\n * },\n * });\n * ```\n */\n adapterOptions?: SlackAdapterChannelConfig;\n}\n\n/** Slack renders the status inline next to the app name; keep it to one short line. */\nconst MAX_STATUS_LENGTH = 80;\n\n/**\n * Turns a tool's `description` arg (\"Listing the repo root\") into a status\n * line that reads after the app name (\"<App> is listing the repo root…\").\n * Only the first line counts: issue/PR creators carry whole bodies there.\n */\nfunction describeToolCall(args: unknown): string | undefined {\n if (!args || typeof args !== 'object') return undefined;\n const { description } = args as { description?: unknown };\n if (typeof description !== 'string') return undefined;\n const line = description\n .split('\\n')\n .map(part => part.replace(/\\s+/g, ' ').trim())\n .find(Boolean);\n if (!line) return undefined;\n const text = line.length > MAX_STATUS_LENGTH ? `${line.slice(0, MAX_STATUS_LENGTH - 1)}…` : line;\n // Drop the sentence-case capital (\"Listing\" → \"listing\") but leave acronyms\n // and identifiers (\"PR lookup\", \"CI status\") alone.\n return `is ${/^[A-Z][a-z]/.test(text) ? text.charAt(0).toLowerCase() + text.slice(1) : text}`;\n}\n\n/**\n * With reply text hidden, streamed text never reaches the thread, so it gets\n * no status: an \"is typing...\" set after the reply tool's post would sit on\n * the thread with nothing coming, and race the clear at the end of the run.\n */\nconst factoryTypingStatus =\n (textDisplay: SlackAdapterChannelConfig['textDisplay']): TypingStatusFn =>\n chunk => {\n switch (chunk.type) {\n case 'reasoning-delta':\n return 'is thinking...';\n case 'text-delta':\n return textDisplay === 'hidden' ? undefined : 'is typing...';\n // The reply tool's text streams into the thread as the model writes it.\n case 'tool-call-input-streaming-start':\n return chunk.payload.toolName === REPLY_TOOL_NAME ? 'is typing...' : undefined;\n case 'tool-call':\n // Still finishing the streamed reply; don't flip to \"is working...\".\n if (chunk.payload.toolName === REPLY_TOOL_NAME) return 'is typing...';\n return describeToolCall(chunk.payload.args) ?? 'is working...';\n default:\n return undefined;\n }\n};\n\n/**\n * Drops keys the caller set to `undefined` so spreading the overrides cannot\n * erase the Factory defaults they sit on top of.\n */\nfunction adapterOverrides(options: SlackAdapterChannelConfig | undefined): SlackAdapterChannelConfig {\n return Object.fromEntries(\n Object.entries(options ?? {}).filter(([, value]) => value !== undefined),\n ) as SlackAdapterChannelConfig;\n}\n\n/**\n * Factory defaults under the caller's overrides. Tool calls stay out of the\n * thread: a Factory session runs hundreds of them, and a row per call (or a\n * card per call) buries the reply. Progress shows through the typing status\n * instead, which names the tool's current step; the session link carries the\n * full trace. `grouped` remains available via `adapterOptions` for callers\n * who want Slack's \"Thinking Steps\" widget.\n */\nfunction factoryAdapterOptions(overrides: SlackAdapterChannelConfig): SlackAdapterChannelConfig {\n const textDisplay = overrides.textDisplay ?? 'hidden';\n return { typingStatus: factoryTypingStatus(textDisplay), toolDisplay: 'hidden', textDisplay, ...overrides };\n}\n\nexport class SlackIntegration implements FactoryIntegration {\n readonly id = 'slack';\n /**\n * The OIDC connect flow round-trips a signed `state` through Slack, so the\n * replica handling the callback must be able to verify a state a different\n * replica signed.\n */\n readonly requiresStableStateSigner = true;\n\n readonly #config: SlackIntegrationConfig;\n readonly #adapterOptions: SlackAdapterChannelConfig;\n /** Bound in `initialize()`; the controller mounts after integrations are built. */\n #channels: ShareFileToolDeps['channels'];\n readonly #deps: ShareFileToolDeps = { channels: () => this.#channels?.() };\n /**\n * With reply text hidden (the Factory default) the agent speaks through\n * `reply_in_thread`, so the prompt, reminder and live streaming for that\n * tool come with it. The setting is fixed for the deployment, so it is\n * decided once here rather than on every run.\n */\n readonly processors: { input: InputProcessor[]; output: OutputProcessor[] };\n /**\n * Whether `channels()` found a source-control owner on the context and wired\n * repo-backed sessions. Set at the channels() attach path, which runs once at\n * boot before diagnostics are served.\n */\n #repoBackedSessions = false;\n\n constructor(config: SlackIntegrationConfig) {\n if (!config.signingSecret) {\n throw new Error(\n \"SlackIntegration: 'signingSecret' is required — Slack cannot verify inbound requests without it.\",\n );\n }\n this.#config = config;\n this.#adapterOptions = factoryAdapterOptions(adapterOverrides(config.adapterOptions));\n const textHidden = this.#textHidden;\n this.processors = {\n input: [\n new FactoryChannelReplyProcessor(),\n ...(textHidden ? [new FactoryReplyReminderProcessor()] : []),\n ],\n output: [\n new FactoryChannelSessionLinkProcessor(),\n ...(textHidden ? [new FactoryReplyStreamProcessor(this.#deps)] : []),\n ],\n };\n }\n\n get #textHidden(): boolean {\n return this.#adapterOptions.textDisplay === 'hidden';\n }\n\n initialize(args: { channels?: () => AgentChannels | null | undefined }): void {\n this.#channels = args.channels;\n }\n\n /**\n * `attach_file`, and `reply_in_thread` when reply text is hidden, on runs\n * whose session is linked to a Slack thread. The tools find the thread to\n * post into when they are called.\n */\n async agentTools({ requestContext }: { requestContext: RequestContext }): Promise<IntegrationTools> {\n if (!(await isSlackThread(requestContext, this.#deps))) return {};\n return {\n ...createShareFileTool(requestContext, this.#deps),\n ...(this.#textHidden ? createReplyTool(requestContext, this.#deps) : {}),\n };\n }\n\n channels(ctx: IntegrationContext): FactoryChannelsConfig {\n // Repo-backed sessions select the source-control provider from the linked\n // repository on each Factory project.\n const sourceControls =\n ctx.storage.sourceControls ?? (ctx.storage.sourceControlOwner ? [ctx.storage.sourceControlOwner] : []);\n this.#repoBackedSessions = sourceControls.length > 0;\n return createSlackChannelsConfig({\n slack: {\n clientId: this.#config.clientId,\n clientSecret: this.#config.clientSecret,\n signingSecret: this.#config.signingSecret,\n botToken: this.#config.botToken,\n },\n accountLinks: ctx.storage.channelIdentity,\n projects: ctx.storage.projects,\n sourceControls,\n modelDefaults: ctx.storage.modelDefaults,\n workItems: ctx.runtime?.workItems,\n feed: ctx.feed,\n adapterOptions: this.#adapterOptions,\n });\n }\n\n feedPublisher(ctx: IntegrationContext): WorkItemFeedPublisher {\n return new SlackFeedPublisher({ controller: ctx.controller });\n }\n\n routes(ctx: IntegrationContext): ApiRoute[] {\n const { clientId, clientSecret, oidcRedirectBaseUrl, uiOrigin } = this.#config;\n return createSlackConnectRoutes({\n auth: ctx.auth,\n accountLinks: ctx.storage.channelIdentity,\n tenantStateSigner: ctx.stateSigner,\n oidc:\n clientId && clientSecret && oidcRedirectBaseUrl\n ? { clientId, clientSecret, redirectBaseUrl: oidcRedirectBaseUrl, uiOrigin }\n : undefined,\n projects: ctx.storage.projects,\n });\n }\n\n diagnostics(): Record<string, unknown> {\n const { clientId, clientSecret, botToken, oidcRedirectBaseUrl } = this.#config;\n return {\n configured: true,\n botTokenConfigured: Boolean(botToken),\n oidcConfigured: Boolean(clientId && clientSecret && oidcRedirectBaseUrl),\n repoBackedSessions: this.#repoBackedSessions,\n };\n }\n}\n"],"mappings":";;;;;;;;;;AAkFA,MAAM,oBAAoB;;;;;;AAO1B,SAAS,iBAAiB,MAAmC;CAC3D,IAAI,CAAC,QAAQ,OAAO,SAAS,UAAU,OAAO,KAAA;CAC9C,MAAM,EAAE,gBAAgB;CACxB,IAAI,OAAO,gBAAgB,UAAU,OAAO,KAAA;CAC5C,MAAM,OAAO,YACV,MAAM,IAAI,CAAC,CACX,KAAI,SAAQ,KAAK,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAC7C,KAAK,OAAO;CACf,IAAI,CAAC,MAAM,OAAO,KAAA;CAClB,MAAM,OAAO,KAAK,SAAS,oBAAoB,GAAG,KAAK,MAAM,GAAG,EAAqB,EAAE,KAAK;CAG5F,OAAO,MAAM,cAAc,KAAK,IAAI,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,KAAK,MAAM,CAAC,IAAI;AACzF;;;;;;AAOA,MAAM,uBACH,iBACD,UAAS;CACT,QAAQ,MAAM,MAAd;EACE,KAAK,mBACH,OAAO;EACT,KAAK,cACH,OAAO,gBAAgB,WAAW,KAAA,IAAY;EAEhD,KAAK,mCACH,OAAO,MAAM,QAAQ,aAAA,oBAA+B,iBAAiB,KAAA;EACvE,KAAK;GAEH,IAAI,MAAM,QAAQ,aAAA,mBAA8B,OAAO;GACvD,OAAO,iBAAiB,MAAM,QAAQ,IAAI,KAAK;EACjD,SACE;CACJ;AACF;;;;;AAMA,SAAS,iBAAiB,SAA2E;CACnG,OAAO,OAAO,YACZ,OAAO,QAAQ,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,GAAG,WAAW,UAAU,KAAA,CAAS,CACzE;AACF;;;;;;;;;AAUA,SAAS,sBAAsB,WAAiE;CAC9F,MAAM,cAAc,UAAU,eAAe;CAC7C,OAAO;EAAE,cAAc,oBAAoB,WAAW;EAAG,aAAa;EAAU;EAAa,GAAG;CAAU;AAC5G;AAEA,IAAa,mBAAb,MAA4D;CAC1D,KAAc;;;;;;CAMd,4BAAqC;CAErC;CACA;;CAEA;CACA,QAAoC,EAAE,gBAAgB,KAAK,YAAY,EAAE;;;;;;;CAOzE;;;;;;CAMA,sBAAsB;CAEtB,YAAY,QAAgC;EAC1C,IAAI,CAAC,OAAO,eACV,MAAM,IAAI,MACR,kGACF;EAEF,KAAK,UAAU;EACf,KAAK,kBAAkB,sBAAsB,iBAAiB,OAAO,cAAc,CAAC;EACpF,MAAM,aAAa,KAAK;EACxB,KAAK,aAAa;GAChB,OAAO,CACL,IAAI,6BAA6B,GACjC,GAAI,aAAa,CAAC,IAAI,8BAA8B,CAAC,IAAI,CAAC,CAC5D;GACA,QAAQ,CACN,IAAI,mCAAmC,GACvC,GAAI,aAAa,CAAC,IAAI,4BAA4B,KAAK,KAAK,CAAC,IAAI,CAAC,CACpE;EACF;CACF;CAEA,IAAI,cAAuB;EACzB,OAAO,KAAK,gBAAgB,gBAAgB;CAC9C;CAEA,WAAW,MAAmE;EAC5E,KAAK,YAAY,KAAK;CACxB;;;;;;CAOA,MAAM,WAAW,EAAE,kBAAiF;EAClG,IAAI,CAAE,MAAM,cAAc,gBAAgB,KAAK,KAAK,GAAI,OAAO,CAAC;EAChE,OAAO;GACL,GAAG,oBAAoB,gBAAgB,KAAK,KAAK;GACjD,GAAI,KAAK,cAAc,gBAAgB,gBAAgB,KAAK,KAAK,IAAI,CAAC;EACxE;CACF;CAEA,SAAS,KAAgD;EAGvD,MAAM,iBACJ,IAAI,QAAQ,mBAAmB,IAAI,QAAQ,qBAAqB,CAAC,IAAI,QAAQ,kBAAkB,IAAI,CAAC;EACtG,KAAK,sBAAsB,eAAe,SAAS;EACnD,OAAO,0BAA0B;GAC/B,OAAO;IACL,UAAU,KAAK,QAAQ;IACvB,cAAc,KAAK,QAAQ;IAC3B,eAAe,KAAK,QAAQ;IAC5B,UAAU,KAAK,QAAQ;GACzB;GACA,cAAc,IAAI,QAAQ;GAC1B,UAAU,IAAI,QAAQ;GACtB;GACA,eAAe,IAAI,QAAQ;GAC3B,WAAW,IAAI,SAAS;GACxB,MAAM,IAAI;GACV,gBAAgB,KAAK;EACvB,CAAC;CACH;CAEA,cAAc,KAAgD;EAC5D,OAAO,IAAI,mBAAmB,EAAE,YAAY,IAAI,WAAW,CAAC;CAC9D;CAEA,OAAO,KAAqC;EAC1C,MAAM,EAAE,UAAU,cAAc,qBAAqB,aAAa,KAAK;EACvE,OAAO,yBAAyB;GAC9B,MAAM,IAAI;GACV,cAAc,IAAI,QAAQ;GAC1B,mBAAmB,IAAI;GACvB,MACE,YAAY,gBAAgB,sBACxB;IAAE;IAAU;IAAc,iBAAiB;IAAqB;GAAS,IACzE,KAAA;GACN,UAAU,IAAI,QAAQ;EACxB,CAAC;CACH;CAEA,cAAuC;EACrC,MAAM,EAAE,UAAU,cAAc,UAAU,wBAAwB,KAAK;EACvE,OAAO;GACL,YAAY;GACZ,oBAAoB,QAAQ,QAAQ;GACpC,gBAAgB,QAAQ,YAAY,gBAAgB,mBAAmB;GACvE,oBAAoB,KAAK;EAC3B;CACF;AACF"}
@@ -0,0 +1,23 @@
1
+ import type { ProcessInputStepArgs, Processor } from '@mastra/core/processors';
2
+ /** How long a run may work without posting before it is nudged. */
3
+ export declare const REPLY_REMINDER_AFTER_MS: number;
4
+ export interface FactoryReplyReminderOptions {
5
+ afterMs?: number;
6
+ now?: () => number;
7
+ }
8
+ /**
9
+ * With reply text hidden, a long job can run silently for an hour while the
10
+ * person who asked has no idea whether anything is happening. Once a run has
11
+ * gone `afterMs` without calling `reply_in_thread`, this sends the agent one
12
+ * signal suggesting a status update, then restarts the clock. Ignoring it is
13
+ * how the agent stays quiet. A signal joins the end of the transcript, so the
14
+ * cached prompt prefix is untouched. Only runs that have the reply tool are
15
+ * touched.
16
+ */
17
+ export declare class FactoryReplyReminderProcessor implements Processor {
18
+ #private;
19
+ readonly id = "factory-reply-reminder";
20
+ constructor({ afterMs, now }?: FactoryReplyReminderOptions);
21
+ processInputStep({ messageList, tools, state, sendSignal }: ProcessInputStepArgs): Promise<undefined>;
22
+ }
23
+ //# sourceMappingURL=reply-reminder-processor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reply-reminder-processor.d.ts","sourceRoot":"","sources":["../../../src/integrations/slack/reply-reminder-processor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAM/E,mEAAmE;AACnE,eAAO,MAAM,uBAAuB,QAAa,CAAC;AAQlD,MAAM,WAAW,2BAA2B;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,qBAAa,6BAA8B,YAAW,SAAS;;IAC7D,QAAQ,CAAC,EAAE,4BAAM;gBAIL,EAAE,OAAiC,EAAE,GAAc,EAAE,GAAE,2BAAgC;IAK7F,gBAAgB,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,oBAAoB;CAqBvF"}
@@ -0,0 +1,58 @@
1
+ import { REPLY_TOOL_NAME } from "./reply-tool.js";
2
+ //#region src/integrations/slack/reply-reminder-processor.ts
3
+ const ID = "factory-reply-reminder";
4
+ /** How long a run may work without posting before it is nudged. */
5
+ const REPLY_REMINDER_AFTER_MS = 3e5;
6
+ /**
7
+ * With reply text hidden, a long job can run silently for an hour while the
8
+ * person who asked has no idea whether anything is happening. Once a run has
9
+ * gone `afterMs` without calling `reply_in_thread`, this sends the agent one
10
+ * signal suggesting a status update, then restarts the clock. Ignoring it is
11
+ * how the agent stays quiet. A signal joins the end of the transcript, so the
12
+ * cached prompt prefix is untouched. Only runs that have the reply tool are
13
+ * touched.
14
+ */
15
+ var FactoryReplyReminderProcessor = class {
16
+ id = ID;
17
+ #afterMs;
18
+ #now;
19
+ constructor({ afterMs = REPLY_REMINDER_AFTER_MS, now = Date.now } = {}) {
20
+ this.#afterMs = afterMs;
21
+ this.#now = now;
22
+ }
23
+ async processInputStep({ messageList, tools, state, sendSignal }) {
24
+ if (!tools || !("reply_in_thread" in tools) || !sendSignal) return void 0;
25
+ const reminder = state;
26
+ const now = this.#now();
27
+ reminder.quietSince ??= now;
28
+ const seen = new Set(reminder.seenCalls ?? []);
29
+ const calls = replyCalls(messageList.get.response.db());
30
+ if (calls.some((id) => !seen.has(id))) reminder.quietSince = now;
31
+ reminder.seenCalls = calls;
32
+ const silentFor = now - reminder.quietSince;
33
+ if (silentFor < this.#afterMs) return void 0;
34
+ const minutes = Math.floor(silentFor / 6e4);
35
+ await sendSignal({
36
+ type: "reactive",
37
+ contents: `It has been ${minutes} minute${minutes === 1 ? "" : "s"} since you last posted in this thread. If the people there would want to know where things stand, post a short status with \`${REPLY_TOOL_NAME}\`. Otherwise keep working.`
38
+ });
39
+ reminder.quietSince = now;
40
+ }
41
+ };
42
+ /** Ids of the `reply_in_thread` calls this run has finished. */
43
+ function replyCalls(messages) {
44
+ const ids = [];
45
+ for (const message of messages) {
46
+ if (message.role !== "assistant") continue;
47
+ for (const part of message.content.parts ?? []) {
48
+ if (part.type !== "tool-invocation") continue;
49
+ const { toolName, toolCallId, state } = part.toolInvocation;
50
+ if (toolName === "reply_in_thread" && state === "result") ids.push(toolCallId);
51
+ }
52
+ }
53
+ return ids;
54
+ }
55
+ //#endregion
56
+ export { FactoryReplyReminderProcessor, REPLY_REMINDER_AFTER_MS };
57
+
58
+ //# sourceMappingURL=reply-reminder-processor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reply-reminder-processor.js","names":[],"sources":["../../../src/integrations/slack/reply-reminder-processor.ts"],"sourcesContent":["import type { MastraDBMessage } from '@mastra/core/agent';\nimport type { ProcessInputStepArgs, Processor } from '@mastra/core/processors';\n\nimport { REPLY_TOOL_NAME } from './reply-tool.js';\n\nconst ID = 'factory-reply-reminder';\n\n/** How long a run may work without posting before it is nudged. */\nexport const REPLY_REMINDER_AFTER_MS = 5 * 60_000;\n\ninterface ReminderState {\n /** When the current silence started: run start, last post, or last nudge. */\n quietSince?: number;\n seenCalls?: string[];\n}\n\nexport interface FactoryReplyReminderOptions {\n afterMs?: number;\n now?: () => number;\n}\n\n/**\n * With reply text hidden, a long job can run silently for an hour while the\n * person who asked has no idea whether anything is happening. Once a run has\n * gone `afterMs` without calling `reply_in_thread`, this sends the agent one\n * signal suggesting a status update, then restarts the clock. Ignoring it is\n * how the agent stays quiet. A signal joins the end of the transcript, so the\n * cached prompt prefix is untouched. Only runs that have the reply tool are\n * touched.\n */\nexport class FactoryReplyReminderProcessor implements Processor {\n readonly id = ID;\n readonly #afterMs: number;\n readonly #now: () => number;\n\n constructor({ afterMs = REPLY_REMINDER_AFTER_MS, now = Date.now }: FactoryReplyReminderOptions = {}) {\n this.#afterMs = afterMs;\n this.#now = now;\n }\n\n async processInputStep({ messageList, tools, state, sendSignal }: ProcessInputStepArgs) {\n if (!tools || !(REPLY_TOOL_NAME in tools) || !sendSignal) return undefined;\n const reminder = state as ReminderState;\n const now = this.#now();\n reminder.quietSince ??= now;\n\n const seen = new Set(reminder.seenCalls ?? []);\n const calls = replyCalls(messageList.get.response.db());\n if (calls.some(id => !seen.has(id))) reminder.quietSince = now;\n reminder.seenCalls = calls;\n\n const silentFor = now - reminder.quietSince;\n if (silentFor < this.#afterMs) return undefined;\n const minutes = Math.floor(silentFor / 60_000);\n await sendSignal({\n type: 'reactive',\n contents: `It has been ${minutes} minute${minutes === 1 ? '' : 's'} since you last posted in this thread. If the people there would want to know where things stand, post a short status with \\`${REPLY_TOOL_NAME}\\`. Otherwise keep working.`,\n });\n reminder.quietSince = now;\n return undefined;\n }\n}\n\n/** Ids of the `reply_in_thread` calls this run has finished. */\nfunction replyCalls(messages: MastraDBMessage[]): string[] {\n const ids: string[] = [];\n for (const message of messages) {\n if (message.role !== 'assistant') continue;\n for (const part of message.content.parts ?? []) {\n if (part.type !== 'tool-invocation') continue;\n const { toolName, toolCallId, state } = part.toolInvocation;\n if (toolName === REPLY_TOOL_NAME && state === 'result') ids.push(toolCallId);\n }\n }\n return ids;\n}\n"],"mappings":";;AAKA,MAAM,KAAK;;AAGX,MAAa,0BAA0B;;;;;;;;;;AAsBvC,IAAa,gCAAb,MAAgE;CAC9D,KAAc;CACd;CACA;CAEA,YAAY,EAAE,UAAU,yBAAyB,MAAM,KAAK,QAAqC,CAAC,GAAG;EACnG,KAAK,WAAW;EAChB,KAAK,OAAO;CACd;CAEA,MAAM,iBAAiB,EAAE,aAAa,OAAO,OAAO,cAAoC;EACtF,IAAI,CAAC,SAAS,EAAA,qBAAqB,UAAU,CAAC,YAAY,OAAO,KAAA;EACjE,MAAM,WAAW;EACjB,MAAM,MAAM,KAAK,KAAK;EACtB,SAAS,eAAe;EAExB,MAAM,OAAO,IAAI,IAAI,SAAS,aAAa,CAAC,CAAC;EAC7C,MAAM,QAAQ,WAAW,YAAY,IAAI,SAAS,GAAG,CAAC;EACtD,IAAI,MAAM,MAAK,OAAM,CAAC,KAAK,IAAI,EAAE,CAAC,GAAG,SAAS,aAAa;EAC3D,SAAS,YAAY;EAErB,MAAM,YAAY,MAAM,SAAS;EACjC,IAAI,YAAY,KAAK,UAAU,OAAO,KAAA;EACtC,MAAM,UAAU,KAAK,MAAM,YAAY,GAAM;EAC7C,MAAM,WAAW;GACf,MAAM;GACN,UAAU,eAAe,QAAQ,SAAS,YAAY,IAAI,KAAK,IAAI,+HAA+H,gBAAgB;EACpN,CAAC;EACD,SAAS,aAAa;CAExB;AACF;;AAGA,SAAS,WAAW,UAAuC;CACzD,MAAM,MAAgB,CAAC;CACvB,KAAK,MAAM,WAAW,UAAU;EAC9B,IAAI,QAAQ,SAAS,aAAa;EAClC,KAAK,MAAM,QAAQ,QAAQ,QAAQ,SAAS,CAAC,GAAG;GAC9C,IAAI,KAAK,SAAS,mBAAmB;GACrC,MAAM,EAAE,UAAU,YAAY,UAAU,KAAK;GAC7C,IAAI,aAAA,qBAAgC,UAAU,UAAU,IAAI,KAAK,UAAU;EAC7E;CACF;CACA,OAAO;AACT"}
@@ -0,0 +1,31 @@
1
+ import type { Processor, ProcessOutputStreamArgs } from '@mastra/core/processors';
2
+ import type { RequestContext } from '@mastra/core/request-context';
3
+ import type { ChunkType } from '@mastra/core/stream';
4
+ import type { IntegrationTools } from '../base.js';
5
+ import type { ShareFileToolDeps } from './share-file-tool.js';
6
+ export declare const REPLY_TOOL_NAME = "reply_in_thread";
7
+ /**
8
+ * The only way a channel run speaks when the adapter hides reply text
9
+ * (`textDisplay: 'hidden'`). Posting becomes a choice the agent makes, so it
10
+ * stops narrating each step into the thread and can say nothing on purpose.
11
+ * Offered only when the Slack adapter hides reply text and the run's thread is
12
+ * linked to Slack; with text shown the reply stream already posts and this
13
+ * tool would double every message.
14
+ */
15
+ export declare function createReplyTool(requestContext: RequestContext, deps?: ShareFileToolDeps): IntegrationTools;
16
+ /**
17
+ * Streams a `reply_in_thread` call's `text` into the thread while the model is
18
+ * still writing it, so a long reply appears as it is written instead of all at
19
+ * once when the call completes. The tool finishes the post with the validated
20
+ * text. Runs whose adapter doesn't stream, or that show reply text, are left
21
+ * alone.
22
+ */
23
+ export declare class FactoryReplyStreamProcessor implements Processor {
24
+ private readonly deps;
25
+ readonly id = "factory-reply-stream";
26
+ constructor(deps?: ShareFileToolDeps);
27
+ processOutputStream({ part, state, requestContext }: ProcessOutputStreamArgs): Promise<ChunkType>;
28
+ }
29
+ /** The decoded value of a top-level string field in JSON that may still be cut off mid-string. */
30
+ export declare function partialStringField(json: string, field: string): string | undefined;
31
+ //# sourceMappingURL=reply-tool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reply-tool.d.ts","sourceRoot":"","sources":["../../../src/integrations/slack/reply-tool.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAKrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAInD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,eAAO,MAAM,eAAe,oBAAoB,CAAC;AAWjD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,cAAc,EAAE,cAAc,EAAE,IAAI,GAAE,iBAAsB,GAAG,gBAAgB,CAyB9G;AA8FD;;;;;;GAMG;AACH,qBAAa,2BAA4B,YAAW,SAAS;IAG/C,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,QAAQ,CAAC,EAAE,0BAA0B;gBAER,IAAI,GAAE,iBAAsB;IAEnD,mBAAmB,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,uBAAuB,GAAG,OAAO,CAAC,SAAS,CAAC;CAyBxG;AAmBD,kGAAkG;AAClG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAwBlF"}
@@ -0,0 +1,203 @@
1
+ import { readFactorySessionScope } from "../../rules/binding-context.js";
2
+ import { SESSION_LINK_LABEL, sessionLink } from "./channel-reply-processor.js";
3
+ import "./channel-session-link-processor.js";
4
+ import { postTarget, resolveRenderContext } from "./share-file-tool.js";
5
+ import "@mastra/core/channels";
6
+ import { z } from "zod";
7
+ import { createTool } from "@mastra/core/tools";
8
+ //#region src/integrations/slack/reply-tool.ts
9
+ const REPLY_TOOL_NAME = "reply_in_thread";
10
+ /** Set once the first reply of a new session carried the session link. */
11
+ const LINKED_KEY = "factory__replyLinked";
12
+ const replyInput = z.object({ text: z.string().trim().min(1).max(12e3).describe("The message to post, in markdown.") }).strict();
13
+ /**
14
+ * The only way a channel run speaks when the adapter hides reply text
15
+ * (`textDisplay: 'hidden'`). Posting becomes a choice the agent makes, so it
16
+ * stops narrating each step into the thread and can say nothing on purpose.
17
+ * Offered only when the Slack adapter hides reply text and the run's thread is
18
+ * linked to Slack; with text shown the reply stream already posts and this
19
+ * tool would double every message.
20
+ */
21
+ function createReplyTool(requestContext, deps = {}) {
22
+ return { [REPLY_TOOL_NAME]: createTool({
23
+ id: REPLY_TOOL_NAME,
24
+ description: "Post a message in this chat thread. Nothing you write outside this tool reaches the thread, so use it for anything the people there should see: a short note that you are starting a longer job, a question you need answered, and the outcome when you finish. Post when you have something new; do not repeat yourself or post that nothing changed. To stay quiet, do not call it.",
25
+ inputSchema: replyInput,
26
+ execute: async (input, context) => {
27
+ const link = firstReplyLink(requestContext);
28
+ const suffix = link ? `\n\n[${SESSION_LINK_LABEL}](${link})` : "";
29
+ const toolCallId = context?.agent?.toolCallId;
30
+ const streamed = toolCallId ? replyStreams(requestContext).get(toolCallId) : void 0;
31
+ if (toolCallId) replyStreams(requestContext).delete(toolCallId);
32
+ if (!streamed || !await streamed.finish(input.text, suffix)) await (await postTarget(requestContext, deps)).chatThread.post({ markdown: `${input.text}${suffix}` });
33
+ if (link) requestContext.set(LINKED_KEY, true);
34
+ return { posted: true };
35
+ }
36
+ }) };
37
+ }
38
+ /**
39
+ * The session link the first reply of a new session ends with. With reply
40
+ * text hidden, `FactoryChannelSessionLinkProcessor`'s text delta never
41
+ * reaches the thread, so the tool carries the link instead.
42
+ */
43
+ function firstReplyLink(requestContext) {
44
+ if (!requestContext.get("factory__newChannelSession") || requestContext.get(LINKED_KEY)) return void 0;
45
+ const controller = requestContext.get("controller");
46
+ if (!controller?.threadId) return void 0;
47
+ return sessionLink({
48
+ threadId: controller.threadId,
49
+ sessionId: controller.sessionId,
50
+ resourceId: controller.resourceId,
51
+ factoryProjectId: readFactorySessionScope(requestContext)?.factoryProjectId
52
+ });
53
+ }
54
+ /** Live posts of `reply_in_thread` calls whose text is still streaming in, by tool call id. */
55
+ const STREAMS_KEY = "factory__replyStreams";
56
+ function replyStreams(requestContext) {
57
+ let streams = requestContext.get(STREAMS_KEY);
58
+ if (!streams) {
59
+ streams = /* @__PURE__ */ new Map();
60
+ requestContext.set(STREAMS_KEY, streams);
61
+ }
62
+ return streams;
63
+ }
64
+ /**
65
+ * One streamed Slack post, opened on the first visible text so an abandoned
66
+ * call never leaves an empty message behind.
67
+ */
68
+ function openReplyStream(chatThread) {
69
+ const queue = [];
70
+ let sent = "";
71
+ let closed = false;
72
+ let wake;
73
+ let done;
74
+ async function* chunks() {
75
+ while (true) {
76
+ while (queue.length > 0) yield queue.shift();
77
+ if (closed) return;
78
+ await new Promise((resolve) => wake = resolve);
79
+ }
80
+ }
81
+ const push = (text) => {
82
+ if (!text) return;
83
+ queue.push(text);
84
+ done ??= chatThread.post(chunks()).then(() => true, (error) => {
85
+ console.warn("[factory] streamed reply failed; posting it whole", { error });
86
+ return false;
87
+ });
88
+ wake?.();
89
+ };
90
+ return {
91
+ update(text) {
92
+ const visible = text.trimStart();
93
+ if (!visible.startsWith(sent)) return;
94
+ push(visible.slice(sent.length));
95
+ sent = visible;
96
+ },
97
+ async finish(text, suffix) {
98
+ if (text.startsWith(sent)) push(text.slice(sent.length));
99
+ if (!done) return false;
100
+ push(suffix);
101
+ closed = true;
102
+ wake?.();
103
+ return done;
104
+ },
105
+ async close() {
106
+ closed = true;
107
+ wake?.();
108
+ return done ?? false;
109
+ }
110
+ };
111
+ }
112
+ /**
113
+ * Streams a `reply_in_thread` call's `text` into the thread while the model is
114
+ * still writing it, so a long reply appears as it is written instead of all at
115
+ * once when the call completes. The tool finishes the post with the validated
116
+ * text. Runs whose adapter doesn't stream, or that show reply text, are left
117
+ * alone.
118
+ */
119
+ var FactoryReplyStreamProcessor = class {
120
+ deps;
121
+ id = "factory-reply-stream";
122
+ constructor(deps = {}) {
123
+ this.deps = deps;
124
+ }
125
+ async processOutputStream({ part, state, requestContext }) {
126
+ if (!requestContext) return part;
127
+ const args = state.args ??= {};
128
+ if (part.type === "tool-call-input-streaming-start" && part.payload.toolName === "reply_in_thread") {
129
+ const render = await resolveRenderContext(requestContext, this.deps);
130
+ if (render?.textDisplay !== "hidden" || !render.streaming?.enabled) return part;
131
+ args[part.payload.toolCallId] = "";
132
+ replyStreams(requestContext).set(part.payload.toolCallId, openReplyStream(render.chatThread));
133
+ } else if (part.type === "tool-call-delta" && part.payload.toolCallId in args) {
134
+ const text = partialStringField(args[part.payload.toolCallId] += part.payload.argsTextDelta, "text");
135
+ if (text !== void 0) replyStreams(requestContext).get(part.payload.toolCallId)?.update(text);
136
+ } else if ((part.type === "tool-error" || part.type === "tool-result") && part.payload.toolCallId in args) {
137
+ delete args[part.payload.toolCallId];
138
+ await closeStream(requestContext, part.payload.toolCallId);
139
+ } else if (RUN_END.has(part.type)) {
140
+ for (const toolCallId of Object.keys(args)) await closeStream(requestContext, toolCallId);
141
+ state.args = {};
142
+ }
143
+ return part;
144
+ }
145
+ };
146
+ /** Chunks after which no `reply_in_thread` call of this run will still execute. */
147
+ const RUN_END = /* @__PURE__ */ new Set([
148
+ "finish",
149
+ "abort",
150
+ "error",
151
+ "tripwire"
152
+ ]);
153
+ /**
154
+ * Ends a streamed post whose call will never reach `execute`, keeping the text
155
+ * shown so far, so Slack isn't left with a message that never finishes.
156
+ */
157
+ async function closeStream(requestContext, toolCallId) {
158
+ const streams = replyStreams(requestContext);
159
+ const stream = streams.get(toolCallId);
160
+ if (!stream) return;
161
+ streams.delete(toolCallId);
162
+ await stream.close();
163
+ }
164
+ const ESCAPES = {
165
+ "\"": "\"",
166
+ "\\": "\\",
167
+ "/": "/",
168
+ b: "\b",
169
+ f: "\f",
170
+ n: "\n",
171
+ r: "\r",
172
+ t: " "
173
+ };
174
+ /** The decoded value of a top-level string field in JSON that may still be cut off mid-string. */
175
+ function partialStringField(json, field) {
176
+ const start = new RegExp(`"${field}"\\s*:\\s*"`).exec(json);
177
+ if (!start) return void 0;
178
+ let out = "";
179
+ for (let i = start.index + start[0].length; i < json.length; i++) {
180
+ const ch = json[i];
181
+ if (ch === "\"") return out;
182
+ if (ch !== "\\") {
183
+ out += ch;
184
+ continue;
185
+ }
186
+ const next = json[i + 1];
187
+ if (next === void 0) return out;
188
+ if (next === "u") {
189
+ const hex = json.slice(i + 2, i + 6);
190
+ if (hex.length < 4) return out;
191
+ out += String.fromCharCode(parseInt(hex, 16));
192
+ i += 5;
193
+ } else {
194
+ out += ESCAPES[next] ?? next;
195
+ i += 1;
196
+ }
197
+ }
198
+ return out;
199
+ }
200
+ //#endregion
201
+ export { FactoryReplyStreamProcessor, REPLY_TOOL_NAME, createReplyTool, partialStringField };
202
+
203
+ //# sourceMappingURL=reply-tool.js.map