@mastra/factory 0.24.0 → 0.25.0-alpha.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.
- package/README.md +36 -9
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +18 -4
- package/dist/auth.js.map +1 -1
- package/dist/boards/review.d.ts.map +1 -1
- package/dist/boards/review.js +3 -2
- package/dist/boards/review.js.map +1 -1
- package/dist/factory.d.ts +3 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +61 -69
- package/dist/factory.js.map +1 -1
- package/dist/integrations/base.d.ts +14 -1
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/{session → integrations/slack}/channel-reply-processor.d.ts +4 -3
- package/dist/integrations/slack/channel-reply-processor.d.ts.map +1 -0
- package/dist/integrations/slack/channel-reply-processor.js +61 -0
- package/dist/integrations/slack/channel-reply-processor.js.map +1 -0
- package/dist/integrations/slack/channel-session-link-processor.d.ts.map +1 -0
- package/dist/{session → integrations/slack}/channel-session-link-processor.js +13 -4
- package/dist/integrations/slack/channel-session-link-processor.js.map +1 -0
- package/dist/integrations/slack/integration.d.ts +25 -1
- package/dist/integrations/slack/integration.d.ts.map +1 -1
- package/dist/integrations/slack/integration.js +55 -5
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/reply-reminder-processor.d.ts +23 -0
- package/dist/integrations/slack/reply-reminder-processor.d.ts.map +1 -0
- package/dist/integrations/slack/reply-reminder-processor.js +58 -0
- package/dist/integrations/slack/reply-reminder-processor.js.map +1 -0
- package/dist/integrations/slack/reply-tool.d.ts +31 -0
- package/dist/integrations/slack/reply-tool.d.ts.map +1 -0
- package/dist/integrations/slack/reply-tool.js +203 -0
- package/dist/integrations/slack/reply-tool.js.map +1 -0
- package/dist/integrations/slack/share-file-tool.d.ts +45 -0
- package/dist/integrations/slack/share-file-tool.d.ts.map +1 -0
- package/dist/integrations/slack/share-file-tool.js +106 -0
- package/dist/integrations/slack/share-file-tool.js.map +1 -0
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +54 -6
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/mcp/events.d.ts +73 -0
- package/dist/mcp/events.d.ts.map +1 -0
- package/dist/mcp/events.js +158 -0
- package/dist/mcp/events.js.map +1 -0
- package/dist/mcp/routes.d.ts +0 -1
- package/dist/mcp/routes.d.ts.map +1 -1
- package/dist/mcp/routes.js +30 -3
- package/dist/mcp/routes.js.map +1 -1
- package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
- package/dist/platform-auth-host.d.ts.map +1 -1
- package/dist/platform-auth-host.js +1 -1
- package/dist/platform-auth-host.js.map +1 -1
- package/dist/routes/config.d.ts +2 -0
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +50 -9
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/contracts.d.ts +2 -0
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/contracts.js +1 -1
- package/dist/routes/contracts.js.map +1 -1
- package/dist/routes/session-access.d.ts +7 -2
- package/dist/routes/session-access.d.ts.map +1 -1
- package/dist/routes/session-access.js +41 -4
- package/dist/routes/session-access.js.map +1 -1
- package/dist/routes/skills.d.ts.map +1 -1
- package/dist/routes/skills.js +4 -3
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/source-control-sessions.d.ts +2 -0
- package/dist/routes/source-control-sessions.d.ts.map +1 -1
- package/dist/routes/source-control-sessions.js +161 -24
- package/dist/routes/source-control-sessions.js.map +1 -1
- package/dist/routes/surface.d.ts +2 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +10 -4
- package/dist/routes/surface.js.map +1 -1
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +11 -3
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/processor.d.ts +2 -2
- package/dist/rules/processor.d.ts.map +1 -1
- package/dist/rules/processor.js +3 -3
- package/dist/rules/processor.js.map +1 -1
- package/dist/sandbox/session-retirement.d.ts +11 -0
- package/dist/sandbox/session-retirement.d.ts.map +1 -1
- package/dist/sandbox/session-retirement.js +38 -1
- package/dist/sandbox/session-retirement.js.map +1 -1
- package/dist/sandbox/session-sandbox.d.ts +2 -0
- package/dist/sandbox/session-sandbox.d.ts.map +1 -1
- package/dist/sandbox/session-sandbox.js +5 -1
- package/dist/sandbox/session-sandbox.js.map +1 -1
- package/dist/session/credential-policy.d.ts +4 -2
- package/dist/session/credential-policy.d.ts.map +1 -1
- package/dist/session/credential-policy.js +11 -1
- package/dist/session/credential-policy.js.map +1 -1
- package/dist/session/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js +14 -1
- package/dist/session/factory-session.js.map +1 -1
- package/dist/session/run-audit.d.ts +1 -0
- package/dist/session/run-audit.d.ts.map +1 -1
- package/dist/session/run-audit.js +7 -2
- package/dist/session/run-audit.js.map +1 -1
- package/dist/session/session-threads.d.ts +5 -0
- package/dist/session/session-threads.d.ts.map +1 -1
- package/dist/session/session-threads.js +8 -1
- package/dist/session/session-threads.js.map +1 -1
- package/dist/session/thinking-default.d.ts +6 -13
- package/dist/session/thinking-default.d.ts.map +1 -1
- package/dist/session/thinking-default.js +18 -22
- package/dist/session/thinking-default.js.map +1 -1
- package/dist/session-owner-signal.d.ts +2 -0
- package/dist/session-owner-signal.d.ts.map +1 -0
- package/dist/session-owner-signal.js +6 -0
- package/dist/session-owner-signal.js.map +1 -0
- package/dist/skills/service.d.ts +6 -7
- package/dist/skills/service.d.ts.map +1 -1
- package/dist/skills/service.js +9 -19
- package/dist/skills/service.js.map +1 -1
- package/dist/storage/domains/audit/actions.d.ts +1 -0
- package/dist/storage/domains/audit/actions.d.ts.map +1 -1
- package/dist/storage/domains/audit/actions.js +2 -1
- package/dist/storage/domains/audit/actions.js.map +1 -1
- package/dist/storage/domains/audit/base.d.ts +18 -2
- package/dist/storage/domains/audit/base.d.ts.map +1 -1
- package/dist/storage/domains/audit/base.js +22 -4
- package/dist/storage/domains/audit/base.js.map +1 -1
- package/dist/storage/domains/model-defaults/base.d.ts +8 -0
- package/dist/storage/domains/model-defaults/base.d.ts.map +1 -1
- package/dist/storage/domains/model-defaults/base.js +18 -0
- package/dist/storage/domains/model-defaults/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +26 -1
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +69 -5
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +19 -1
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +30 -2
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/tools/permissions.d.ts.map +1 -1
- package/dist/tools/permissions.js +2 -1
- package/dist/tools/permissions.js.map +1 -1
- package/dist/tools/sandbox-tools.d.ts +12 -0
- package/dist/tools/sandbox-tools.d.ts.map +1 -0
- package/dist/tools/sandbox-tools.js +40 -0
- package/dist/tools/sandbox-tools.js.map +1 -0
- package/dist/tools/scope.d.ts.map +1 -1
- package/dist/tools/scope.js +2 -5
- package/dist/tools/scope.js.map +1 -1
- package/dist/tools/session-tools.d.ts.map +1 -1
- package/dist/tools/session-tools.js +6 -2
- package/dist/tools/session-tools.js.map +1 -1
- package/dist/work-item-branch.d.ts +11 -3
- package/dist/work-item-branch.d.ts.map +1 -1
- package/dist/work-item-branch.js +34 -9
- package/dist/work-item-branch.js.map +1 -1
- package/dist/workspace.d.ts +2 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +4 -2
- package/dist/workspace.js.map +1 -1
- package/factory-skills/configure-factory-rules/SKILL.md +2 -2
- package/package.json +14 -13
- package/dist/session/channel-reply-processor.d.ts.map +0 -1
- package/dist/session/channel-reply-processor.js +0 -46
- package/dist/session/channel-reply-processor.js.map +0 -1
- package/dist/session/channel-session-link-processor.d.ts.map +0 -1
- package/dist/session/channel-session-link-processor.js.map +0 -1
- /package/dist/{session → integrations/slack}/channel-session-link-processor.d.ts +0 -0
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* sandbox machine's `derive()` gate).
|
|
18
18
|
*/
|
|
19
19
|
import type { MastraCodeConfig, MountedMastraCode } from '@mastra/code-sdk';
|
|
20
|
-
import type { AgentControllerChannelsConfig, ChannelAdapterConfig } from '@mastra/core/channels';
|
|
20
|
+
import type { AgentChannels, AgentControllerChannelsConfig, ChannelAdapterConfig } from '@mastra/core/channels';
|
|
21
|
+
import type { InputProcessor, OutputProcessor } from '@mastra/core/processors';
|
|
21
22
|
import type { RequestContext } from '@mastra/core/request-context';
|
|
22
23
|
import type { ApiRoute, IUserProvider } from '@mastra/core/server';
|
|
23
24
|
import type { FactoryStorage } from '@mastra/core/storage';
|
|
@@ -213,6 +214,8 @@ export interface FactoryIntegration {
|
|
|
213
214
|
/** Cross-integration intake selection/binding domain, for per-request authorization. */
|
|
214
215
|
intake: IntakeStorage;
|
|
215
216
|
sourceControl?: SourceControlStorageHandle;
|
|
217
|
+
/** The controller's chat channels, once it has mounted. */
|
|
218
|
+
channels?: () => AgentChannels | null | undefined;
|
|
216
219
|
}): void;
|
|
217
220
|
/**
|
|
218
221
|
* The integration's full HTTP surface (status, OAuth, webhooks, feature
|
|
@@ -235,6 +238,16 @@ export interface FactoryIntegration {
|
|
|
235
238
|
sessionTools?(args: {
|
|
236
239
|
requestContext: RequestContext;
|
|
237
240
|
}): IntegrationTools;
|
|
241
|
+
/**
|
|
242
|
+
* Processors the integration adds to every session run. Input processors
|
|
243
|
+
* run before Factory's own state processors and output processors before
|
|
244
|
+
* the credential stamp, in the order integrations are configured. Each
|
|
245
|
+
* processor decides per run whether it applies.
|
|
246
|
+
*/
|
|
247
|
+
processors?: {
|
|
248
|
+
input?: InputProcessor[];
|
|
249
|
+
output?: OutputProcessor[];
|
|
250
|
+
};
|
|
238
251
|
/**
|
|
239
252
|
* Optional provider-owned observer for successful or failed tool calls.
|
|
240
253
|
* The factory invokes every configured observer independently so one
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/integrations/base.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,KAAK,EAAE,6BAA6B,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/integrations/base.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,12 +9,13 @@ export declare const SESSION_LINK_LABEL = "Open session \u2197";
|
|
|
9
9
|
* the tool widget, so the instruction and the rendering agree. The session
|
|
10
10
|
* link is not the model's job: `FactoryChannelSessionLinkProcessor` appends it
|
|
11
11
|
* to the first reply of a new session, so it is there whether or not the
|
|
12
|
-
* model judged the request worth an opener.
|
|
13
|
-
*
|
|
12
|
+
* model judged the request worth an opener. A run on a Slack-linked session
|
|
13
|
+
* that didn't start from Slack gets a shorter note on when to post there; runs
|
|
14
|
+
* with no channel at all (Studio, API, boards) get nothing.
|
|
14
15
|
*/
|
|
15
16
|
export declare class FactoryChannelReplyProcessor implements Processor {
|
|
16
17
|
readonly id = "factory-channel-reply";
|
|
17
|
-
processInputStep({ messageList, requestContext }: ProcessInputStepArgs): {
|
|
18
|
+
processInputStep({ messageList, requestContext, tools }: ProcessInputStepArgs): {
|
|
18
19
|
messageList: import("@mastra/core/agent").MessageList;
|
|
19
20
|
} | undefined;
|
|
20
21
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-reply-processor.d.ts","sourceRoot":"","sources":["../../../src/integrations/slack/channel-reply-processor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAO/E,eAAO,MAAM,kBAAkB,wBAAmB,CAAC;AAEnD;;;;;;;;;;;;GAYG;AACH,qBAAa,4BAA6B,YAAW,SAAS;IAC5D,QAAQ,CAAC,EAAE,2BAAM;IAEjB,gBAAgB,CAAC,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,EAAE,EAAE,oBAAoB;;;CA+B9E;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,SAAS,EACT,UAAU,EACV,gBAAgB,GACjB,EAAE,iBAAiB,GAAG,MAAM,GAAG,SAAS,CAQxC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/integrations/slack/channel-reply-processor.ts
|
|
2
|
+
/** Mirrors `REPLY_TOOL_NAME` in `reply-tool.ts`, which imports this module. */
|
|
3
|
+
const REPLY_TOOL = "reply_in_thread";
|
|
4
|
+
const ID = "factory-channel-reply";
|
|
5
|
+
const SESSION_LINK_LABEL = "Open session ↗";
|
|
6
|
+
/**
|
|
7
|
+
* Tells the agent how to open a reply when the run came in over a chat
|
|
8
|
+
* channel (Slack, …): acknowledge a job in one line that names it — "On it —
|
|
9
|
+
* sleeping 30s, then running the echo" — before starting, so the sender sees
|
|
10
|
+
* a task-specific first message rather than a silent typing status until the
|
|
11
|
+
* first tool finishes. The channel driver posts that opener on its own before
|
|
12
|
+
* the tool widget, so the instruction and the rendering agree. The session
|
|
13
|
+
* link is not the model's job: `FactoryChannelSessionLinkProcessor` appends it
|
|
14
|
+
* to the first reply of a new session, so it is there whether or not the
|
|
15
|
+
* model judged the request worth an opener. A run on a Slack-linked session
|
|
16
|
+
* that didn't start from Slack gets a shorter note on when to post there; runs
|
|
17
|
+
* with no channel at all (Studio, API, boards) get nothing.
|
|
18
|
+
*/
|
|
19
|
+
var FactoryChannelReplyProcessor = class {
|
|
20
|
+
id = ID;
|
|
21
|
+
processInputStep({ messageList, requestContext, tools }) {
|
|
22
|
+
if (!requestContext?.get("channel")) {
|
|
23
|
+
if (!tools || !(REPLY_TOOL in tools)) return void 0;
|
|
24
|
+
messageList.addSystem({
|
|
25
|
+
role: "system",
|
|
26
|
+
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.`
|
|
27
|
+
}, ID);
|
|
28
|
+
return { messageList };
|
|
29
|
+
}
|
|
30
|
+
const content = tools && REPLY_TOOL in tools ? [
|
|
31
|
+
`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.`,
|
|
32
|
+
`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.`,
|
|
33
|
+
`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.`
|
|
34
|
+
] : ["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.", "Finish with the outcome, not a replay of the steps."];
|
|
35
|
+
messageList.addSystem({
|
|
36
|
+
role: "system",
|
|
37
|
+
content: content.join("\n")
|
|
38
|
+
}, ID);
|
|
39
|
+
return { messageList };
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The web URL for a session, or undefined without a public origin. Same
|
|
44
|
+
* routes the Factory UI navigates to: a repo-backed thread's Factory session
|
|
45
|
+
* id is the workspace segment; chat-only threads keep the literal `channel`
|
|
46
|
+
* segment and name their session in the query; a session with no factory
|
|
47
|
+
* falls back to the factory-agnostic /threads/ redirect, which forwards the
|
|
48
|
+
* query through.
|
|
49
|
+
*/
|
|
50
|
+
function sessionLink({ threadId, sessionId, resourceId, factoryProjectId }) {
|
|
51
|
+
const origin = process.env.MASTRACODE_PUBLIC_URL;
|
|
52
|
+
if (!origin) return void 0;
|
|
53
|
+
const isChatOnly = resourceId.startsWith("channel:");
|
|
54
|
+
const sessionParam = `?sessionId=${encodeURIComponent(sessionId)}`;
|
|
55
|
+
if (!factoryProjectId) return `${origin}/threads/${encodeURIComponent(threadId)}${sessionParam}`;
|
|
56
|
+
return `${origin}/factories/${encodeURIComponent(factoryProjectId)}/workspaces/${isChatOnly ? "channel" : encodeURIComponent(sessionId)}/threads/${encodeURIComponent(threadId)}${isChatOnly ? sessionParam : ""}`;
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
export { FactoryChannelReplyProcessor, SESSION_LINK_LABEL, sessionLink };
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=channel-reply-processor.js.map
|
|
@@ -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 "
|
|
1
|
+
import { readFactorySessionScope } from "../../rules/binding-context.js";
|
|
2
2
|
import { SESSION_LINK_LABEL, sessionLink } from "./channel-reply-processor.js";
|
|
3
|
-
|
|
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
|
|
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
|
|
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;
|
|
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
|
-
|
|
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
|
|
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:
|
|
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":"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"}
|
|
@@ -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"}
|