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