Slack: implement replyToMode threading for tool path
- Add shared hasRepliedRef state between auto-reply and tool paths - Extract buildSlackThreadingContext helper in agent-runner.ts - Extract resolveThreadTsFromContext helper in slack-actions.ts - Update docs with clear replyToMode table (off/first/all) - Add tests for first mode behavior across multiple messages
This commit is contained in:
committed by
Peter Steinberger
parent
29e6f13b29
commit
b4663ed11c
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { isSlackRoomAllowedByPolicy } from "./monitor.js";
|
||||
import { isSlackRoomAllowedByPolicy, resolveSlackThreadTs } from "./monitor.js";
|
||||
|
||||
describe("slack groupPolicy gating", () => {
|
||||
it("allows when policy is open", () => {
|
||||
@@ -53,3 +53,83 @@ describe("slack groupPolicy gating", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSlackThreadTs", () => {
|
||||
const threadTs = "1234567890.123456";
|
||||
|
||||
describe("replyToMode=off", () => {
|
||||
it("returns baseThreadTs when in a thread", () => {
|
||||
expect(
|
||||
resolveSlackThreadTs({
|
||||
replyToMode: "off",
|
||||
baseThreadTs: threadTs,
|
||||
hasReplied: false,
|
||||
}),
|
||||
).toBe(threadTs);
|
||||
});
|
||||
|
||||
it("returns baseThreadTs even after replies (stays in thread)", () => {
|
||||
expect(
|
||||
resolveSlackThreadTs({
|
||||
replyToMode: "off",
|
||||
baseThreadTs: threadTs,
|
||||
hasReplied: true,
|
||||
}),
|
||||
).toBe(threadTs);
|
||||
});
|
||||
|
||||
it("returns undefined when not in a thread", () => {
|
||||
expect(
|
||||
resolveSlackThreadTs({
|
||||
replyToMode: "off",
|
||||
baseThreadTs: undefined,
|
||||
hasReplied: false,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("replyToMode=first", () => {
|
||||
it("returns baseThreadTs for first reply", () => {
|
||||
expect(
|
||||
resolveSlackThreadTs({
|
||||
replyToMode: "first",
|
||||
baseThreadTs: threadTs,
|
||||
hasReplied: false,
|
||||
}),
|
||||
).toBe(threadTs);
|
||||
});
|
||||
|
||||
it("returns undefined for subsequent replies (goes to main channel)", () => {
|
||||
expect(
|
||||
resolveSlackThreadTs({
|
||||
replyToMode: "first",
|
||||
baseThreadTs: threadTs,
|
||||
hasReplied: true,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("replyToMode=all", () => {
|
||||
it("returns baseThreadTs for first reply", () => {
|
||||
expect(
|
||||
resolveSlackThreadTs({
|
||||
replyToMode: "all",
|
||||
baseThreadTs: threadTs,
|
||||
hasReplied: false,
|
||||
}),
|
||||
).toBe(threadTs);
|
||||
});
|
||||
|
||||
it("returns baseThreadTs for subsequent replies (all go to thread)", () => {
|
||||
expect(
|
||||
resolveSlackThreadTs({
|
||||
replyToMode: "all",
|
||||
baseThreadTs: threadTs,
|
||||
hasReplied: true,
|
||||
}),
|
||||
).toBe(threadTs);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1069,11 +1069,22 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
const { replyThreadTs, statusThreadTs } = resolveSlackThreadTargets({
|
||||
// Use helper for status thread; compute baseThreadTs for "first" mode support
|
||||
const { statusThreadTs } = resolveSlackThreadTargets({
|
||||
message,
|
||||
replyToMode,
|
||||
});
|
||||
// Base thread timestamp: where should first reply go?
|
||||
// - "off": only thread if already in a thread
|
||||
// - "first"/"all": start thread under the message
|
||||
const baseThreadTs =
|
||||
replyToMode === "off"
|
||||
? message.thread_ts
|
||||
: (message.thread_ts ?? message.ts);
|
||||
let didSetStatus = false;
|
||||
// Shared mutable ref for tracking if a reply was sent (used by both
|
||||
// auto-reply path and tool path for "first" threading mode).
|
||||
const hasRepliedRef = { value: false };
|
||||
const onReplyStart = async () => {
|
||||
didSetStatus = true;
|
||||
await setSlackThreadStatus({
|
||||
@@ -1087,6 +1098,11 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId)
|
||||
.responsePrefix,
|
||||
deliver: async (payload) => {
|
||||
const effectiveThreadTs = resolveSlackThreadTs({
|
||||
replyToMode,
|
||||
baseThreadTs,
|
||||
hasReplied: hasRepliedRef.value,
|
||||
});
|
||||
await deliverReplies({
|
||||
replies: [payload],
|
||||
target: replyTarget,
|
||||
@@ -1094,8 +1110,9 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
accountId: account.accountId,
|
||||
runtime,
|
||||
textLimit,
|
||||
replyThreadTs,
|
||||
threadTs: effectiveThreadTs,
|
||||
});
|
||||
hasRepliedRef.value = true;
|
||||
},
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(
|
||||
@@ -1119,6 +1136,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
replyOptions: {
|
||||
...replyOptions,
|
||||
skillFilter: channelConfig?.skills,
|
||||
hasRepliedRef,
|
||||
disableBlockStreaming:
|
||||
typeof account.config.blockStreaming === "boolean"
|
||||
? !account.config.blockStreaming
|
||||
@@ -1958,6 +1976,30 @@ export function isSlackRoomAllowedByPolicy(params: {
|
||||
return channelAllowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute effective threadTs for a Slack reply based on replyToMode.
|
||||
* - "off": stay in thread if already in one, otherwise main channel
|
||||
* - "first": first reply goes to thread, subsequent replies to main channel
|
||||
* - "all": all replies go to thread
|
||||
*/
|
||||
export function resolveSlackThreadTs(params: {
|
||||
replyToMode: "off" | "first" | "all";
|
||||
baseThreadTs: string | undefined;
|
||||
hasReplied: boolean;
|
||||
}): string | undefined {
|
||||
const { replyToMode, baseThreadTs, hasReplied } = params;
|
||||
if (replyToMode === "off") {
|
||||
// Always stay in thread if already in one
|
||||
return baseThreadTs;
|
||||
}
|
||||
if (replyToMode === "all") {
|
||||
// All replies go to thread
|
||||
return baseThreadTs;
|
||||
}
|
||||
// "first": only first reply goes to thread
|
||||
return hasReplied ? undefined : baseThreadTs;
|
||||
}
|
||||
|
||||
async function deliverSlackSlashReplies(params: {
|
||||
replies: ReplyPayload[];
|
||||
respond: SlackRespondFn;
|
||||
|
||||
Reference in New Issue
Block a user