diff --git a/packages/junior/src/chat/slack/channel.ts b/packages/junior/src/chat/slack/channel.ts index a05ef1e72..6835d396a 100644 --- a/packages/junior/src/chat/slack/channel.ts +++ b/packages/junior/src/chat/slack/channel.ts @@ -32,6 +32,11 @@ export interface SlackThreadReply { type?: string; files?: SlackFileRef[]; attachments?: unknown[]; + reactions?: Array<{ + name?: string; + count?: number; + users?: string[]; + }>; } /** List channel history using Slack-native, pre-validated timestamp bounds. */ diff --git a/packages/junior/src/chat/slack/tools/channel-list-messages.ts b/packages/junior/src/chat/slack/tools/channel-list-messages.ts index 090a7a93f..c3aa4d5d0 100644 --- a/packages/junior/src/chat/slack/tools/channel-list-messages.ts +++ b/packages/junior/src/chat/slack/tools/channel-list-messages.ts @@ -79,34 +79,45 @@ export function createSlackChannelListMessagesTool(context: SlackToolContext) { readOnlyHint: true, }, inputSchema: z.object({ - channel_id: slackChannelRefParam.optional(), + channel_id: slackChannelRefParam.nullable().optional(), limit: z.coerce .number() .int() .min(1) .max(1000) .describe("Maximum number of messages to return across pages.") + .nullable() .optional(), cursor: z .string() .min(1) - .describe("Optional cursor to continue from a prior call.") + .describe( + "Cursor from `next_cursor` in a prior result. Use null or omit it for the first page; never invent a cursor.", + ) + .nullable() .optional(), oldest: slackTimestampParam( "Oldest message timestamp (Slack ts) for range filtering.", - ).optional(), + ) + .nullable() + .optional(), latest: slackTimestampParam( "Latest message timestamp (Slack ts) for range filtering.", - ).optional(), + ) + .nullable() + .optional(), inclusive: booleanInput( "Whether oldest/latest bounds should be inclusive.", - ).optional(), + ) + .nullable() + .optional(), max_pages: z.coerce .number() .int() .min(1) .max(10) .describe("Maximum number of API pages to traverse in a single call.") + .nullable() .optional(), }), outputSchema: juniorToolOutputSchema, @@ -121,7 +132,7 @@ export function createSlackChannelListMessagesTool(context: SlackToolContext) { }) => { const target = await resolveOptionalSlackChannelRef({ field: "channel_id", - value: channel_id, + value: channel_id ?? undefined, defaultChannelId: context.destinationChannelId, teamId: context.teamId, }); @@ -141,7 +152,7 @@ export function createSlackChannelListMessagesTool(context: SlackToolContext) { const normalizedOldest = normalizeRangeTimestamp( "oldest", - oldest, + oldest ?? undefined, targetChannelId, ); if (!normalizedOldest.ok) { @@ -149,7 +160,7 @@ export function createSlackChannelListMessagesTool(context: SlackToolContext) { } const normalizedLatest = normalizeRangeTimestamp( "latest", - latest, + latest ?? undefined, targetChannelId, ); if (!normalizedLatest.ok) { @@ -160,11 +171,11 @@ export function createSlackChannelListMessagesTool(context: SlackToolContext) { listChannelMessages({ channelId: targetChannelId, limit: limit ?? 100, - cursor, + cursor: cursor ?? undefined, oldest: normalizedOldest.value, latest: normalizedLatest.value, - inclusive, - maxPages: max_pages, + inclusive: inclusive ?? undefined, + maxPages: max_pages ?? undefined, }); let result: Awaited> | undefined; diff --git a/packages/junior/src/chat/slack/tools/thread-read.ts b/packages/junior/src/chat/slack/tools/thread-read.ts index 7cb3c712d..de9c9ef5d 100644 --- a/packages/junior/src/chat/slack/tools/thread-read.ts +++ b/packages/junior/src/chat/slack/tools/thread-read.ts @@ -69,6 +69,15 @@ function sanitizeMessage( subtype: msg.subtype, bot_id: msg.bot_id, type: msg.type, + ...(msg.reactions?.length + ? { + reactions: msg.reactions.map((reaction) => ({ + name: reaction.name, + count: reaction.count, + users: reaction.users, + })), + } + : undefined), ...(attachmentText ? { attachment_text: attachmentText } : undefined), ...(files.length ? { @@ -119,7 +128,7 @@ export function createSlackThreadReadTool( ) { return zodTool({ description: - "Read a Slack thread from a shared archive URL or explicit channel + timestamp. Works for the current conversation and public channels.", + "Read a Slack thread, including message reaction users, from a shared archive URL or explicit channel + timestamp. Works for the current conversation and public channels.", annotations: { destructiveHint: false, idempotentHint: true, @@ -127,17 +136,25 @@ export function createSlackThreadReadTool( readOnlyHint: true, }, inputSchema: z.object({ - url: z.string().min(1).describe("Slack message archive URL.").optional(), - channel_id: slackChannelRefParam.optional(), + url: z + .string() + .min(1) + .describe("Slack message archive URL.") + .nullable() + .optional(), + channel_id: slackChannelRefParam.nullable().optional(), ts: slackTimestampParam( "Slack message timestamp. May be the thread root or any message in the thread.", - ).optional(), + ) + .nullable() + .optional(), limit: z.coerce .number() .int() .min(1) .max(1000) .describe("Maximum number of thread messages to fetch.") + .nullable() .optional(), max_pages: z.coerce .number() @@ -145,6 +162,7 @@ export function createSlackThreadReadTool( .min(1) .max(10) .describe("Maximum number of Slack API pages to traverse.") + .nullable() .optional(), }), outputSchema: juniorToolOutputSchema, @@ -197,7 +215,7 @@ export function createSlackThreadReadTool( channelId, threadTs: lookupTs, limit: limit ?? 1000, - maxPages: max_pages, + maxPages: max_pages ?? undefined, }); let replies: SlackThreadReply[] | undefined; diff --git a/packages/junior/tests/integration/slack-channel-tools.test.ts b/packages/junior/tests/integration/slack-channel-tools.test.ts index 3d9d29957..0f5958ffa 100644 --- a/packages/junior/tests/integration/slack-channel-tools.test.ts +++ b/packages/junior/tests/integration/slack-channel-tools.test.ts @@ -193,9 +193,12 @@ describe("slack channel tools", () => { ); const result = await executeTool(tool, { + channel_id: null, limit: 150, + cursor: null, oldest: "1690000000.000", latest: "1710000000", + inclusive: null, max_pages: 3, }); diff --git a/packages/junior/tests/integration/slack-thread-read.test.ts b/packages/junior/tests/integration/slack-thread-read.test.ts index 4557b25c3..ac4776a68 100644 --- a/packages/junior/tests/integration/slack-thread-read.test.ts +++ b/packages/junior/tests/integration/slack-thread-read.test.ts @@ -171,6 +171,13 @@ describe("slackThreadRead", () => { thread_ts: "1700000000.500000", user: "U1", text: "standalone message", + reactions: [ + { + name: "raised_hands", + count: 2, + users: ["U2", "U3"], + }, + ], }, ], }), @@ -180,13 +187,27 @@ describe("slackThreadRead", () => { const result = await executeTool(tool, { channel_id: "C0MANUAL", ts: "1700000000.500000", + url: null, + limit: null, + max_pages: null, }); expect(result).toMatchObject({ channel_id: "C0MANUAL", count: 1, + messages: [ + { + text: "standalone message", + reactions: [ + { + name: "raised_hands", + count: 2, + users: ["U2", "U3"], + }, + ], + }, + ], }); - expect(result.messages[0].text).toBe("standalone message"); }); it("allows reading a private channel when it matches the current channel", async () => {