experimental-ash 0.9.0 → 0.10.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/CHANGELOG.md +12 -0
- package/dist/docs/public/channels/slack.md +23 -0
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/public/channels/slack/index.d.ts +1 -1
- package/dist/src/public/channels/slack/slack.d.ts +2 -1
- package/dist/src/public/channels/slack/slack.js +1 -0
- package/dist/src/public/channels/slack/slackChannel.d.ts +20 -1
- package/dist/src/public/channels/slack/slackChannel.js +6 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# experimental-ash
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 5bc39de: feat(slack): pluggable `stateAdapter` for production persistence
|
|
8
|
+
|
|
9
|
+
`slackChannel` and `slack()` now accept a `stateAdapter` option implementing the chat SDK `StateAdapter` contract. Used for callback URLs on posted cards, `thread.state` / `thread.setState`, `thread.subscribe`, message queues, and locks. Defaults to an in-memory adapter (development only — loses state on restart, not shared across processes). Pass a Redis/Postgres-backed adapter for production.
|
|
10
|
+
|
|
11
|
+
The supplied instance is shared across the inbound webhook path (`Chat` constructor) and every event-handler context rebuild, so state stays coherent within a single agent run. The default in-memory adapter is now constructed once at channel-construction time and shared the same way, replacing the previous behavior where the inbound path and each rebuild call each got their own isolated memory adapter.
|
|
12
|
+
|
|
13
|
+
A new exported type alias `SlackStateAdapter` aliases the chat SDK's `StateAdapter` interface.
|
|
14
|
+
|
|
3
15
|
## 0.9.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -169,6 +169,29 @@ export default slackChannel({
|
|
|
169
169
|
(`turn.started`, `message.completed`, `session.failed`, etc.). They run inside the workflow context,
|
|
170
170
|
not on the inbound webhook side.
|
|
171
171
|
|
|
172
|
+
## State Persistence
|
|
173
|
+
|
|
174
|
+
The Slack channel uses a state backend for callback URLs on posted cards, `thread.state` /
|
|
175
|
+
`thread.setState`, `thread.subscribe`, message queues, and locks. By default this is an in-memory
|
|
176
|
+
adapter scoped to the current process, which loses state on restart and is not coherent across
|
|
177
|
+
processes. For deployed agents, pass a production adapter (Redis, Postgres, etc.) implementing the
|
|
178
|
+
chat SDK `StateAdapter` contract. Construct it once at module top-level so all requests share one
|
|
179
|
+
connection pool:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { createRedisState } from "@chat-adapter/state-redis";
|
|
183
|
+
import { slack } from "experimental-ash/channels/slack";
|
|
184
|
+
|
|
185
|
+
const stateAdapter = createRedisState({ url: process.env.REDIS_URL! });
|
|
186
|
+
|
|
187
|
+
export default slack({
|
|
188
|
+
stateAdapter,
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The same instance is reused across the inbound webhook path and every event-handler context rebuild,
|
|
193
|
+
so state stays coherent within a single agent run.
|
|
194
|
+
|
|
172
195
|
## Typing Indicators
|
|
173
196
|
|
|
174
197
|
Out of the box, `slack()` posts typing statuses so the user sees feedback before the agent starts
|
|
@@ -6,7 +6,7 @@ import { ASH_PACKAGE_NAME } from "#package-name.js";
|
|
|
6
6
|
let cachedPackageInfo;
|
|
7
7
|
// The package build stamps the published version into `dist` so bundled
|
|
8
8
|
// deployments can still report package metadata without resolving package.json.
|
|
9
|
-
const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.
|
|
9
|
+
const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.10.0";
|
|
10
10
|
const BUNDLED_FALLBACK_PACKAGE_VERSION_PLACEHOLDER = "__ASH_PACKAGE_VERSION_PLACEHOLDER__";
|
|
11
11
|
const WORKFLOW_MODULE_ALIASES = {
|
|
12
12
|
"workflow/api": "src/compiled/@workflow/core/runtime.js",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { slack, type SlackOptions } from "#public/channels/slack/slack.js";
|
|
2
|
-
export { slackChannel, type SlackApiHandle, type SlackApiResponse, type SlackChannel, type SlackChannelConfig, type SlackChannelEvents, type SlackChannelCredentials, type SlackChannelState, type SlackContext, type SlackInteractionAction, type SlackReceiveArgs, type SlackWebhookVerifier, } from "#public/channels/slack/slackChannel.js";
|
|
2
|
+
export { slackChannel, type SlackApiHandle, type SlackApiResponse, type SlackChannel, type SlackChannelConfig, type SlackChannelEvents, type SlackChannelCredentials, type SlackChannelState, type SlackContext, type SlackInteractionAction, type SlackReceiveArgs, type SlackStateAdapter, type SlackWebhookVerifier, } from "#public/channels/slack/slackChannel.js";
|
|
3
3
|
export { Actions, Button, Card, CardText, Divider, Fields, Image, LinkButton, Modal, RadioSelect, Section, Select, SelectOption, Table, TextInput, } from "#compiled/chat/index.js";
|
|
4
4
|
export type { AdapterPostableMessage, Attachment, Author, CardElement, FileUpload, Message, PostableMessage, SentMessage, Thread, } from "#compiled/chat/index.js";
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { SessionAuthContext } from "#channel/types.js";
|
|
2
2
|
import type { Channel } from "#public/definitions/defineChannel.js";
|
|
3
|
-
import { type SlackChannelCredentials, type SlackChannelState } from "#public/channels/slack/slackChannel.js";
|
|
3
|
+
import { type SlackChannelCredentials, type SlackChannelState, type SlackStateAdapter } from "#public/channels/slack/slackChannel.js";
|
|
4
4
|
export interface SlackOptions {
|
|
5
5
|
readonly auth?: (message: import("#compiled/chat/index.js").Message) => SessionAuthContext | null;
|
|
6
6
|
readonly credentials?: SlackChannelCredentials;
|
|
7
7
|
readonly botName?: string;
|
|
8
|
+
readonly stateAdapter?: SlackStateAdapter;
|
|
8
9
|
}
|
|
9
10
|
export declare function slack(options?: SlackOptions): Channel<SlackChannelState>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type SlackBotToken } from "#compiled/@chat-adapter/slack/index.js";
|
|
2
|
-
import { type Message, type SerializedThread, type Thread } from "#compiled/chat/index.js";
|
|
2
|
+
import { type Message, type SerializedThread, type StateAdapter, type Thread } from "#compiled/chat/index.js";
|
|
3
3
|
import type { HandleMessageStreamEvent } from "#protocol/message.js";
|
|
4
4
|
import { type UploadPolicy } from "#public/channels/upload-policy.js";
|
|
5
5
|
import { type Channel } from "#public/definitions/defineChannel.js";
|
|
@@ -36,6 +36,14 @@ export interface SlackChannelState {
|
|
|
36
36
|
* with Vercel OIDC instead of Slack's signing secret.
|
|
37
37
|
*/
|
|
38
38
|
export type SlackWebhookVerifier = (request: Request, body: string) => unknown | Promise<unknown>;
|
|
39
|
+
/**
|
|
40
|
+
* State backend used by the chat SDK for callback URL persistence on
|
|
41
|
+
* posted cards, `thread.state`/`setState`, `thread.subscribe`, message
|
|
42
|
+
* queues, and locks. Any value implementing the chat SDK
|
|
43
|
+
* `StateAdapter` contract works — `@chat-adapter/state-memory` for
|
|
44
|
+
* local development, a Redis/Postgres-backed adapter for production.
|
|
45
|
+
*/
|
|
46
|
+
export type SlackStateAdapter = StateAdapter;
|
|
39
47
|
export interface SlackChannelCredentials {
|
|
40
48
|
readonly botToken?: SlackBotToken;
|
|
41
49
|
/**
|
|
@@ -93,6 +101,17 @@ export interface SlackChannelEvents {
|
|
|
93
101
|
export interface SlackChannelConfig {
|
|
94
102
|
readonly credentials?: SlackChannelCredentials;
|
|
95
103
|
readonly botName?: string;
|
|
104
|
+
/**
|
|
105
|
+
* State backend for chat SDK persistence — callback URLs on posted
|
|
106
|
+
* cards, `thread.state` / `thread.setState`, `thread.subscribe`,
|
|
107
|
+
* message queues, locks. Defaults to an in-memory adapter scoped to
|
|
108
|
+
* the current process, which loses state on restart and is not
|
|
109
|
+
* shared across processes. Pass a production adapter (Redis,
|
|
110
|
+
* Postgres, etc.) for deployed agents. The same instance is reused
|
|
111
|
+
* across the inbound webhook path and every event-handler context
|
|
112
|
+
* rebuild, keeping state coherent within a single agent run.
|
|
113
|
+
*/
|
|
114
|
+
readonly stateAdapter?: SlackStateAdapter;
|
|
96
115
|
/**
|
|
97
116
|
* Override the default webhook route path (`/ash/v1/slack`).
|
|
98
117
|
*/
|
|
@@ -94,7 +94,7 @@ function resolveSlackAdapterCredentials(credentials) {
|
|
|
94
94
|
const signingSecret = credentials?.signingSecret ?? (webhookVerifier ? undefined : process.env.SLACK_SIGNING_SECRET);
|
|
95
95
|
return { botToken, signingSecret, webhookVerifier };
|
|
96
96
|
}
|
|
97
|
-
function rebuildSlackContext(state, credentials) {
|
|
97
|
+
function rebuildSlackContext(state, credentials, stateAdapter) {
|
|
98
98
|
const { botToken, signingSecret, webhookVerifier } = resolveSlackAdapterCredentials(credentials);
|
|
99
99
|
const adapter = createSlackAdapter({
|
|
100
100
|
botToken,
|
|
@@ -109,20 +109,8 @@ function rebuildSlackContext(state, credentials) {
|
|
|
109
109
|
id: "slack::",
|
|
110
110
|
isDM: false,
|
|
111
111
|
});
|
|
112
|
-
// Pin both private adapter slots so the thread is fully self-
|
|
113
|
-
// contained — no `Chat.getSingleton()` lookup needed when handlers
|
|
114
|
-
// call `thread.post()`, `thread.startTyping()`, `thread.setState()`,
|
|
115
|
-
// `thread.subscribe()`, etc. The `_adapter` line matches the runtime
|
|
116
|
-
// behavior chat itself relies on for `ThreadImpl.fromJSON(json,
|
|
117
|
-
// adapter)`. `ThreadImpl.fromJSON` does NOT pin a state adapter, so
|
|
118
|
-
// without `_stateAdapterInstance` set, state-touching paths (cards,
|
|
119
|
-
// callback URLs, `state`/`setState`, `subscribe()`/`isSubscribed()`)
|
|
120
|
-
// would fall through to the singleton and throw — we use an in-
|
|
121
|
-
// memory state adapter scoped to this rebuild so the asymmetry with
|
|
122
|
-
// the fallback branch goes away and both branches converge to a
|
|
123
|
-
// fully-pinned thread.
|
|
124
112
|
Reflect.set(thread, "_adapter", adapter);
|
|
125
|
-
Reflect.set(thread, "_stateAdapterInstance",
|
|
113
|
+
Reflect.set(thread, "_stateAdapterInstance", stateAdapter);
|
|
126
114
|
return {
|
|
127
115
|
thread,
|
|
128
116
|
slack: buildSlackApiHandle(thread, botToken, state.teamId ?? undefined),
|
|
@@ -152,6 +140,7 @@ export function slackChannel(config = {}) {
|
|
|
152
140
|
const slackFetchFile = createSlackFetchFile({
|
|
153
141
|
botToken: config.credentials?.botToken,
|
|
154
142
|
});
|
|
143
|
+
const stateAdapter = config.stateAdapter ?? createMemoryState();
|
|
155
144
|
let chatPromise = null;
|
|
156
145
|
async function getChat() {
|
|
157
146
|
if (chatPromise)
|
|
@@ -161,9 +150,8 @@ export function slackChannel(config = {}) {
|
|
|
161
150
|
if (!botToken) {
|
|
162
151
|
throw new Error("slackChannel requires a bot token. Pass credentials.botToken or set SLACK_BOT_TOKEN.");
|
|
163
152
|
}
|
|
164
|
-
const [slackModule,
|
|
153
|
+
const [slackModule, chatModule] = await Promise.all([
|
|
165
154
|
import("#compiled/@chat-adapter/slack/index.js"),
|
|
166
|
-
import("#compiled/@chat-adapter/state-memory/index.js"),
|
|
167
155
|
import("#compiled/chat/index.js"),
|
|
168
156
|
]);
|
|
169
157
|
const slackAdapter = slackModule.createSlackAdapter({
|
|
@@ -174,7 +162,7 @@ export function slackChannel(config = {}) {
|
|
|
174
162
|
});
|
|
175
163
|
const chat = new chatModule.Chat({
|
|
176
164
|
adapters: { slack: slackAdapter },
|
|
177
|
-
state:
|
|
165
|
+
state: stateAdapter,
|
|
178
166
|
userName: config.botName ?? "ash-agent",
|
|
179
167
|
});
|
|
180
168
|
await chat.initialize();
|
|
@@ -191,7 +179,7 @@ export function slackChannel(config = {}) {
|
|
|
191
179
|
state: { serializedThread: null, teamId: null },
|
|
192
180
|
fetchFile: slackFetchFile,
|
|
193
181
|
context(state) {
|
|
194
|
-
return rebuildSlackContext(state, config.credentials);
|
|
182
|
+
return rebuildSlackContext(state, config.credentials, stateAdapter);
|
|
195
183
|
},
|
|
196
184
|
routes: [
|
|
197
185
|
POST(config.route ?? "/ash/v1/slack", async (req, { send, waitUntil }) => {
|