@cursor/july 0.1.25 → 0.1.26
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/package.json +1 -1
- package/src/channels/github/github-channel.ts +19 -2
- package/src/channels/github/github.test.ts +42 -0
- package/src/channels/github/types.ts +6 -0
- package/src/channels/slack/api.ts +3 -1
- package/src/channels/slack/block-actions.test.ts +18 -1
- package/src/channels/slack/cursor-account.test.ts +49 -1
- package/src/channels/slack/cursor-account.ts +3 -1
- package/src/channels/slack/dispatch.test.ts +80 -0
- package/src/channels/slack/dispatch.ts +12 -1
- package/src/channels/slack/interactive.ts +3 -0
- package/src/channels/slack/slack-channel.ts +6 -0
- package/src/channels/slack/types.ts +6 -0
- package/src/internal/reminder-runner.test.ts +43 -1
- package/src/internal/reminder-runner.ts +1 -0
- package/src/internal/schedule-runner.ts +1 -0
- package/src/internal/server.artifacts.test.ts +36 -5
- package/src/internal/server.ts +2 -0
- package/src/internal/session-engine.artifacts.test.ts +53 -3
- package/src/internal/session-engine.ts +4 -1
- package/src/types.ts +18 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cursor/july",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "(early alpha) Filesystem-first framework for defining Cursor agents as markdown and TypeScript and serving them over channels with the Cursor SDK.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"repository": {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { isDevMode } from "../../internal/dev-mode.js";
|
|
11
11
|
import type {
|
|
12
12
|
AgentCloudOptions,
|
|
13
|
+
ArtifactsApi,
|
|
13
14
|
AuthContext,
|
|
14
15
|
ChannelDefinition,
|
|
15
16
|
ChannelEventHandlers,
|
|
@@ -68,7 +69,7 @@ import type {
|
|
|
68
69
|
*/
|
|
69
70
|
type GitHubParsedContext = Omit<
|
|
70
71
|
GitHubEventContext,
|
|
71
|
-
"github" | "thread" | "host"
|
|
72
|
+
"github" | "thread" | "host" | "artifacts"
|
|
72
73
|
>;
|
|
73
74
|
|
|
74
75
|
const DEFAULT_PULL_REQUEST_ACTIONS: ReadonlySet<string> = new Set([
|
|
@@ -737,6 +738,7 @@ async function handlePullRequest(input: {
|
|
|
737
738
|
bindingState: binding,
|
|
738
739
|
installationId,
|
|
739
740
|
host: input.args.host,
|
|
741
|
+
artifacts: input.args.artifacts,
|
|
740
742
|
});
|
|
741
743
|
const dispatch =
|
|
742
744
|
input.options.onPullRequest === undefined
|
|
@@ -828,6 +830,7 @@ async function handleIssueComment(input: {
|
|
|
828
830
|
bindingState: binding,
|
|
829
831
|
installationId,
|
|
830
832
|
host: input.args.host,
|
|
833
|
+
artifacts: input.args.artifacts,
|
|
831
834
|
});
|
|
832
835
|
|
|
833
836
|
const dispatch =
|
|
@@ -918,6 +921,7 @@ async function handleReviewComment(input: {
|
|
|
918
921
|
bindingState: binding,
|
|
919
922
|
installationId,
|
|
920
923
|
host: input.args.host,
|
|
924
|
+
artifacts: input.args.artifacts,
|
|
921
925
|
});
|
|
922
926
|
|
|
923
927
|
const dispatch =
|
|
@@ -994,6 +998,7 @@ async function handleIssue(input: {
|
|
|
994
998
|
bindingState: binding,
|
|
995
999
|
installationId,
|
|
996
1000
|
host: input.args.host,
|
|
1001
|
+
artifacts: input.args.artifacts,
|
|
997
1002
|
});
|
|
998
1003
|
const dispatch = await input.options.onIssue(ctx, parsed.issue);
|
|
999
1004
|
if (dispatch === null) {
|
|
@@ -1053,6 +1058,7 @@ async function handleCheckSuite(input: {
|
|
|
1053
1058
|
ctx: parsed.unanchoredCtx,
|
|
1054
1059
|
installationId,
|
|
1055
1060
|
host: input.args.host,
|
|
1061
|
+
artifacts: input.args.artifacts,
|
|
1056
1062
|
}),
|
|
1057
1063
|
parsed.checkSuite
|
|
1058
1064
|
);
|
|
@@ -1079,6 +1085,7 @@ async function handleCheckSuite(input: {
|
|
|
1079
1085
|
bindingState: binding,
|
|
1080
1086
|
installationId,
|
|
1081
1087
|
host: input.args.host,
|
|
1088
|
+
artifacts: input.args.artifacts,
|
|
1082
1089
|
});
|
|
1083
1090
|
const dispatch = await input.options.onCheckSuite(ctx, parsed.checkSuite);
|
|
1084
1091
|
if (dispatch === null) {
|
|
@@ -1143,6 +1150,7 @@ async function handleCheckRun(input: {
|
|
|
1143
1150
|
ctx: parsed.unanchoredCtx,
|
|
1144
1151
|
installationId,
|
|
1145
1152
|
host: input.args.host,
|
|
1153
|
+
artifacts: input.args.artifacts,
|
|
1146
1154
|
}),
|
|
1147
1155
|
parsed.checkRun
|
|
1148
1156
|
);
|
|
@@ -1169,6 +1177,7 @@ async function handleCheckRun(input: {
|
|
|
1169
1177
|
bindingState: binding,
|
|
1170
1178
|
installationId,
|
|
1171
1179
|
host: input.args.host,
|
|
1180
|
+
artifacts: input.args.artifacts,
|
|
1172
1181
|
});
|
|
1173
1182
|
const dispatch = await input.options.onCheckRun(ctx, parsed.checkRun);
|
|
1174
1183
|
if (dispatch === null) {
|
|
@@ -1229,6 +1238,7 @@ async function handleWorkflowRun(input: {
|
|
|
1229
1238
|
ctx: parsed.unanchoredCtx,
|
|
1230
1239
|
installationId,
|
|
1231
1240
|
host: input.args.host,
|
|
1241
|
+
artifacts: input.args.artifacts,
|
|
1232
1242
|
}),
|
|
1233
1243
|
parsed.workflowRun
|
|
1234
1244
|
);
|
|
@@ -1255,6 +1265,7 @@ async function handleWorkflowRun(input: {
|
|
|
1255
1265
|
bindingState: binding,
|
|
1256
1266
|
installationId,
|
|
1257
1267
|
host: input.args.host,
|
|
1268
|
+
artifacts: input.args.artifacts,
|
|
1258
1269
|
});
|
|
1259
1270
|
const dispatch = await input.options.onWorkflowRun(ctx, parsed.workflowRun);
|
|
1260
1271
|
if (dispatch === null) {
|
|
@@ -1326,6 +1337,7 @@ async function handleStatus(input: {
|
|
|
1326
1337
|
bindingState: binding,
|
|
1327
1338
|
installationId,
|
|
1328
1339
|
host: input.args.host,
|
|
1340
|
+
artifacts: input.args.artifacts,
|
|
1329
1341
|
});
|
|
1330
1342
|
const dispatch = await input.options.onStatus(ctx, parsed.status);
|
|
1331
1343
|
if (dispatch === null) {
|
|
@@ -1399,6 +1411,7 @@ async function handleGenericEvent(input: {
|
|
|
1399
1411
|
bindingState: binding,
|
|
1400
1412
|
installationId,
|
|
1401
1413
|
host: input.args.host,
|
|
1414
|
+
artifacts: input.args.artifacts,
|
|
1402
1415
|
});
|
|
1403
1416
|
const dispatch = await input.options.onEvent(ctx, parsed.event);
|
|
1404
1417
|
if (dispatch === null) {
|
|
@@ -2224,11 +2237,12 @@ function escapeRegExp(value: string): string {
|
|
|
2224
2237
|
*/
|
|
2225
2238
|
function enrichEventContext(input: {
|
|
2226
2239
|
bindingConfig: GitHubBindingConfig;
|
|
2227
|
-
ctx: Omit<GitHubEventContext, "github" | "thread" | "host"> &
|
|
2240
|
+
ctx: Omit<GitHubEventContext, "github" | "thread" | "host" | "artifacts"> &
|
|
2228
2241
|
Partial<Pick<GitHubEventContext, "github" | "thread">>;
|
|
2229
2242
|
bindingState: GitHubBindingState;
|
|
2230
2243
|
installationId?: number;
|
|
2231
2244
|
host: HostContext;
|
|
2245
|
+
artifacts: ArtifactsApi;
|
|
2232
2246
|
}): GitHubEventContext {
|
|
2233
2247
|
const { github, thread } = buildGitHubBinding({
|
|
2234
2248
|
config: input.bindingConfig,
|
|
@@ -2242,6 +2256,7 @@ function enrichEventContext(input: {
|
|
|
2242
2256
|
github,
|
|
2243
2257
|
thread,
|
|
2244
2258
|
host: input.host,
|
|
2259
|
+
artifacts: input.artifacts,
|
|
2245
2260
|
};
|
|
2246
2261
|
}
|
|
2247
2262
|
|
|
@@ -2254,6 +2269,7 @@ function enrichCiContext(input: {
|
|
|
2254
2269
|
ctx: GitHubParsedContext;
|
|
2255
2270
|
installationId?: number;
|
|
2256
2271
|
host: HostContext;
|
|
2272
|
+
artifacts: ArtifactsApi;
|
|
2257
2273
|
}): GitHubEventContext {
|
|
2258
2274
|
const binding = githubBindingStateForEvent({
|
|
2259
2275
|
kind: input.ctx.conversation.kind,
|
|
@@ -2269,6 +2285,7 @@ function enrichCiContext(input: {
|
|
|
2269
2285
|
bindingState: binding,
|
|
2270
2286
|
installationId: input.installationId,
|
|
2271
2287
|
host: input.host,
|
|
2288
|
+
artifacts: input.artifacts,
|
|
2272
2289
|
});
|
|
2273
2290
|
}
|
|
2274
2291
|
|
|
@@ -2,6 +2,7 @@ import { createHmac } from "node:crypto";
|
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { scmMetadataToGitHubPayload } from "../../internal/cursor-event-relay.js";
|
|
4
4
|
import type {
|
|
5
|
+
ArtifactsApi,
|
|
5
6
|
ChannelHandlerArgs,
|
|
6
7
|
ChannelRouteHandler,
|
|
7
8
|
ChannelSession,
|
|
@@ -334,6 +335,47 @@ describe("githubChannel", () => {
|
|
|
334
335
|
);
|
|
335
336
|
});
|
|
336
337
|
|
|
338
|
+
it("hands the artifacts facade to inbound hooks (ctx.artifacts)", async () => {
|
|
339
|
+
const tags: Array<Parameters<ArtifactsApi["tag"]>[0]> = [];
|
|
340
|
+
const artifacts: ArtifactsApi = {
|
|
341
|
+
tag: async (input) => {
|
|
342
|
+
tags.push(input);
|
|
343
|
+
return {
|
|
344
|
+
id: `art_${tags.length}`,
|
|
345
|
+
kind: input.kind ?? "artifact",
|
|
346
|
+
key: input.key,
|
|
347
|
+
data: input.data,
|
|
348
|
+
source: "host",
|
|
349
|
+
createdAt: new Date().toISOString(),
|
|
350
|
+
updatedAt: new Date().toISOString(),
|
|
351
|
+
};
|
|
352
|
+
},
|
|
353
|
+
list: async () => [],
|
|
354
|
+
};
|
|
355
|
+
const send = vi.fn<SendMessageFn>(async () => fakeSession());
|
|
356
|
+
const response = await routeHandler(
|
|
357
|
+
githubChannel({
|
|
358
|
+
auth: [],
|
|
359
|
+
onPullRequest: async (ctx, pr) => {
|
|
360
|
+
await ctx.artifacts.tag({
|
|
361
|
+
key: `pr:${pr.number}`,
|
|
362
|
+
data: { headSha: pr.headSha },
|
|
363
|
+
});
|
|
364
|
+
return null;
|
|
365
|
+
},
|
|
366
|
+
})
|
|
367
|
+
)(
|
|
368
|
+
new Request("http://127.0.0.1:3000/v1/channels/github", {
|
|
369
|
+
method: "POST",
|
|
370
|
+
headers: { "x-github-event": "pull_request" },
|
|
371
|
+
body: JSON.stringify(pullRequestPayload()),
|
|
372
|
+
}),
|
|
373
|
+
{ ...handlerArgs(send), artifacts }
|
|
374
|
+
);
|
|
375
|
+
await expect(response.json()).resolves.toMatchObject({ ok: true });
|
|
376
|
+
expect(tags).toEqual([{ key: "pr:42", data: { headSha: "abc123" } }]);
|
|
377
|
+
});
|
|
378
|
+
|
|
337
379
|
it("dispatches @mention issue comments via the default onComment gate", async () => {
|
|
338
380
|
const send = vi.fn<SendMessageFn>(async () => fakeSession());
|
|
339
381
|
const response = await routeHandler(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AgentCloudOptions,
|
|
3
|
+
ArtifactsApi,
|
|
3
4
|
AuthContext,
|
|
4
5
|
AuthPolicy,
|
|
5
6
|
ChannelEventHandlers,
|
|
@@ -306,6 +307,11 @@ export interface GitHubEventContext {
|
|
|
306
307
|
thread: GitHubThread;
|
|
307
308
|
/** Shared host services (MCP / host GitHub / Slack). Same as tool `ctx.host`. */
|
|
308
309
|
host: HostContext;
|
|
310
|
+
/**
|
|
311
|
+
* Durable artifacts (unbound — the hook runs before any session is
|
|
312
|
+
* admitted; pass `sessionId` in `tag` input to attribute one).
|
|
313
|
+
*/
|
|
314
|
+
artifacts: ArtifactsApi;
|
|
309
315
|
}
|
|
310
316
|
|
|
311
317
|
/**
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { ErrorCode, type WebAPIPlatformError, WebClient } from "@slack/web-api";
|
|
6
|
-
import type { HostContext } from "../../types.js";
|
|
6
|
+
import type { ArtifactsApi, HostContext } from "../../types.js";
|
|
7
7
|
import {
|
|
8
8
|
SLACK_DEFAULT_LOADING_MESSAGES,
|
|
9
9
|
SLACK_LOADING_MESSAGE_MAX_LEN,
|
|
@@ -540,6 +540,7 @@ export function buildSlackBinding(args: {
|
|
|
540
540
|
threadTs: string;
|
|
541
541
|
teamId?: string;
|
|
542
542
|
host: HostContext;
|
|
543
|
+
artifacts: ArtifactsApi;
|
|
543
544
|
onThreadTsChanged?: (threadTs: string) => void;
|
|
544
545
|
log?: SlackLog;
|
|
545
546
|
}): SlackBinding {
|
|
@@ -643,5 +644,6 @@ export function buildSlackBinding(args: {
|
|
|
643
644
|
...(args.teamId === undefined ? {} : { teamId: args.teamId }),
|
|
644
645
|
},
|
|
645
646
|
host: args.host,
|
|
647
|
+
artifacts: args.artifacts,
|
|
646
648
|
};
|
|
647
649
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
ArtifactsApi,
|
|
4
|
+
ChannelSession,
|
|
5
|
+
SendMessageFn,
|
|
6
|
+
} from "../../types.js";
|
|
3
7
|
import { blockActionAuth } from "./auth.js";
|
|
4
8
|
import {
|
|
5
9
|
mrkdwnSections,
|
|
@@ -57,6 +61,15 @@ function createSendRecorder(): {
|
|
|
57
61
|
return { send, calls };
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
function testArtifacts(): ArtifactsApi {
|
|
65
|
+
return {
|
|
66
|
+
tag: async () => {
|
|
67
|
+
throw new Error("artifacts not configured in test");
|
|
68
|
+
},
|
|
69
|
+
list: async () => [],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
60
73
|
function testHost() {
|
|
61
74
|
return {
|
|
62
75
|
mcp: {
|
|
@@ -171,6 +184,7 @@ describe("dispatchBlockAction", () => {
|
|
|
171
184
|
},
|
|
172
185
|
send,
|
|
173
186
|
credentials: undefined,
|
|
187
|
+
artifacts: testArtifacts(),
|
|
174
188
|
host: testHost(),
|
|
175
189
|
log: createSlackLogger(() => undefined),
|
|
176
190
|
});
|
|
@@ -204,6 +218,7 @@ describe("dispatchBlockAction", () => {
|
|
|
204
218
|
handler: () => ({ auth: testAuth }),
|
|
205
219
|
send,
|
|
206
220
|
credentials: undefined,
|
|
221
|
+
artifacts: testArtifacts(),
|
|
207
222
|
host: testHost(),
|
|
208
223
|
log: createSlackLogger(() => undefined),
|
|
209
224
|
});
|
|
@@ -217,6 +232,7 @@ describe("dispatchBlockAction", () => {
|
|
|
217
232
|
handler: () => null,
|
|
218
233
|
send,
|
|
219
234
|
credentials: undefined,
|
|
235
|
+
artifacts: testArtifacts(),
|
|
220
236
|
host: testHost(),
|
|
221
237
|
log: createSlackLogger(() => undefined),
|
|
222
238
|
});
|
|
@@ -232,6 +248,7 @@ describe("dispatchBlockAction", () => {
|
|
|
232
248
|
handler: () => ({ auth: testAuth }),
|
|
233
249
|
send,
|
|
234
250
|
credentials: undefined,
|
|
251
|
+
artifacts: testArtifacts(),
|
|
235
252
|
host: testHost(),
|
|
236
253
|
log: createSlackLogger(() => undefined),
|
|
237
254
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import type { HostContext, SendMessageFn } from "../../types.js";
|
|
2
|
+
import type { ArtifactsApi, HostContext, SendMessageFn } from "../../types.js";
|
|
3
3
|
import {
|
|
4
4
|
type CursorSlackEnvelope,
|
|
5
5
|
createPendingSlackApiTransport,
|
|
@@ -33,6 +33,15 @@ function fakeHost(): HostContext {
|
|
|
33
33
|
return {} as HostContext;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function testArtifacts(): ArtifactsApi {
|
|
37
|
+
return {
|
|
38
|
+
tag: async () => {
|
|
39
|
+
throw new Error("artifacts not configured in test");
|
|
40
|
+
},
|
|
41
|
+
list: async () => [],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
describe("slackChannel({ cursorAccount: true })", () => {
|
|
37
46
|
it("registers a runtime with a pending transport and no Socket Mode routes", () => {
|
|
38
47
|
const definition = slackChannel({ cursorAccount: true });
|
|
@@ -167,6 +176,7 @@ describe("dispatchCursorSlackEnvelope", () => {
|
|
|
167
176
|
envelope: envelope(),
|
|
168
177
|
send,
|
|
169
178
|
host: fakeHost(),
|
|
179
|
+
artifacts: testArtifacts(),
|
|
170
180
|
});
|
|
171
181
|
|
|
172
182
|
expect(sent).toHaveLength(1);
|
|
@@ -184,6 +194,39 @@ describe("dispatchCursorSlackEnvelope", () => {
|
|
|
184
194
|
expect(methods).toContain("assistant.threads.setStatus");
|
|
185
195
|
});
|
|
186
196
|
|
|
197
|
+
it("hands the artifacts facade to the mention handler binding", async () => {
|
|
198
|
+
const tags: Array<Parameters<ArtifactsApi["tag"]>[0]> = [];
|
|
199
|
+
const artifacts: ArtifactsApi = {
|
|
200
|
+
tag: async (input) => {
|
|
201
|
+
tags.push(input);
|
|
202
|
+
return {
|
|
203
|
+
id: `art_${tags.length}`,
|
|
204
|
+
kind: input.kind ?? "artifact",
|
|
205
|
+
key: input.key,
|
|
206
|
+
data: input.data,
|
|
207
|
+
source: "host",
|
|
208
|
+
createdAt: new Date().toISOString(),
|
|
209
|
+
updatedAt: new Date().toISOString(),
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
list: async () => [],
|
|
213
|
+
};
|
|
214
|
+
const { runtime, send } = setup({
|
|
215
|
+
onAppMention: async (ctx) => {
|
|
216
|
+
await ctx.artifacts.tag({ key: "from-relay", data: "r" });
|
|
217
|
+
return null;
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
await dispatchCursorSlackEnvelope({
|
|
221
|
+
runtime,
|
|
222
|
+
envelope: envelope(),
|
|
223
|
+
send,
|
|
224
|
+
host: fakeHost(),
|
|
225
|
+
artifacts,
|
|
226
|
+
});
|
|
227
|
+
expect(tags).toEqual([{ key: "from-relay", data: "r" }]);
|
|
228
|
+
});
|
|
229
|
+
|
|
187
230
|
it("routes DMs to the direct-message handler", async () => {
|
|
188
231
|
const kinds: string[] = [];
|
|
189
232
|
const { runtime, send } = setup({
|
|
@@ -205,12 +248,14 @@ describe("dispatchCursorSlackEnvelope", () => {
|
|
|
205
248
|
}),
|
|
206
249
|
send,
|
|
207
250
|
host: fakeHost(),
|
|
251
|
+
artifacts: testArtifacts(),
|
|
208
252
|
});
|
|
209
253
|
await dispatchCursorSlackEnvelope({
|
|
210
254
|
runtime,
|
|
211
255
|
envelope: envelope({ eventId: "Ev3" }),
|
|
212
256
|
send,
|
|
213
257
|
host: fakeHost(),
|
|
258
|
+
artifacts: testArtifacts(),
|
|
214
259
|
});
|
|
215
260
|
expect(kinds).toEqual(["dm", "mention"]);
|
|
216
261
|
});
|
|
@@ -222,12 +267,14 @@ describe("dispatchCursorSlackEnvelope", () => {
|
|
|
222
267
|
envelope: envelope({ eventId: "EvDup" }),
|
|
223
268
|
send,
|
|
224
269
|
host: fakeHost(),
|
|
270
|
+
artifacts: testArtifacts(),
|
|
225
271
|
});
|
|
226
272
|
await dispatchCursorSlackEnvelope({
|
|
227
273
|
runtime,
|
|
228
274
|
envelope: envelope({ eventId: "EvDup" }),
|
|
229
275
|
send,
|
|
230
276
|
host: fakeHost(),
|
|
277
|
+
artifacts: testArtifacts(),
|
|
231
278
|
});
|
|
232
279
|
expect(sent).toHaveLength(1);
|
|
233
280
|
});
|
|
@@ -245,6 +292,7 @@ describe("dispatchCursorSlackEnvelope", () => {
|
|
|
245
292
|
}),
|
|
246
293
|
send,
|
|
247
294
|
host: fakeHost(),
|
|
295
|
+
artifacts: testArtifacts(),
|
|
248
296
|
});
|
|
249
297
|
expect(sent).toHaveLength(1);
|
|
250
298
|
expect(sent[0].options.continuationToken).toBe("C0100:111.222");
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* dispatches relayed envelopes through the pack's normal inbound path.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import type { HostContext, SendMessageFn } from "../../types.js";
|
|
15
|
+
import type { ArtifactsApi, HostContext, SendMessageFn } from "../../types.js";
|
|
16
16
|
import { dispatchInboundMessage } from "./dispatch.js";
|
|
17
17
|
import type { EventIdDedupe } from "./event-id-dedupe.js";
|
|
18
18
|
import { slackTextToMarkdown } from "./inbound.js";
|
|
@@ -114,6 +114,7 @@ export async function dispatchCursorSlackEnvelope(args: {
|
|
|
114
114
|
envelope: CursorSlackEnvelope;
|
|
115
115
|
send: SendMessageFn;
|
|
116
116
|
host: HostContext;
|
|
117
|
+
artifacts: ArtifactsApi;
|
|
117
118
|
hasContinuationSession?: (continuationToken: string) => Promise<boolean>;
|
|
118
119
|
isContinuationBusy?: (continuationToken: string) => Promise<boolean>;
|
|
119
120
|
getContinuationLastBotMessageTs?: (
|
|
@@ -167,6 +168,7 @@ export async function dispatchCursorSlackEnvelope(args: {
|
|
|
167
168
|
send: args.send,
|
|
168
169
|
credentials: runtime.credentials,
|
|
169
170
|
host: args.host,
|
|
171
|
+
artifacts: args.artifacts,
|
|
170
172
|
log,
|
|
171
173
|
evalCommandsEnabled: runtime.options.evalCommands !== false,
|
|
172
174
|
cursorAgentName: envelope.agentName,
|
|
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import type {
|
|
6
|
+
ArtifactsApi,
|
|
6
7
|
ChannelSession,
|
|
7
8
|
HostContext,
|
|
8
9
|
SendMessageFn,
|
|
@@ -67,6 +68,15 @@ function acceptHandler(): SlackMessageHandler {
|
|
|
67
68
|
return async () => ({ auth: testAuth });
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
function testArtifacts(): ArtifactsApi {
|
|
72
|
+
return {
|
|
73
|
+
tag: async () => {
|
|
74
|
+
throw new Error("artifacts not configured in test");
|
|
75
|
+
},
|
|
76
|
+
list: async () => [],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
70
80
|
function testHost(options?: {
|
|
71
81
|
nudgeRoot?: string;
|
|
72
82
|
askRoot?: string;
|
|
@@ -170,6 +180,7 @@ describe("dispatchInboundFromEventBody", () => {
|
|
|
170
180
|
onAppMention: acceptHandler(),
|
|
171
181
|
onDirectMessage: async () => null,
|
|
172
182
|
credentials: undefined,
|
|
183
|
+
artifacts: testArtifacts(),
|
|
173
184
|
eventIds: createEventIdDedupe(),
|
|
174
185
|
host: testHost(),
|
|
175
186
|
log: createSlackLogger((line) => lines.push(line)),
|
|
@@ -196,6 +207,7 @@ describe("dispatchInboundFromEventBody", () => {
|
|
|
196
207
|
onAppMention: acceptHandler(),
|
|
197
208
|
onDirectMessage: async () => null,
|
|
198
209
|
credentials: undefined as undefined,
|
|
210
|
+
artifacts: testArtifacts(),
|
|
199
211
|
host: testHost(),
|
|
200
212
|
eventIds,
|
|
201
213
|
log: createSlackLogger(() => undefined),
|
|
@@ -219,6 +231,7 @@ describe("dispatchInboundFromEventBody", () => {
|
|
|
219
231
|
onAppMention: async () => null,
|
|
220
232
|
onDirectMessage: async () => null,
|
|
221
233
|
credentials: undefined,
|
|
234
|
+
artifacts: testArtifacts(),
|
|
222
235
|
eventIds: createEventIdDedupe(),
|
|
223
236
|
host: testHost(),
|
|
224
237
|
});
|
|
@@ -239,6 +252,7 @@ describe("dispatchInboundFromEventBody", () => {
|
|
|
239
252
|
},
|
|
240
253
|
onDirectMessage: async () => null,
|
|
241
254
|
credentials: undefined,
|
|
255
|
+
artifacts: testArtifacts(),
|
|
242
256
|
eventIds: createEventIdDedupe(),
|
|
243
257
|
host: testHost(),
|
|
244
258
|
log: createSlackLogger(() => undefined),
|
|
@@ -273,6 +287,7 @@ describe("dispatchInboundFromEventBody", () => {
|
|
|
273
287
|
onAppMention: async () => null,
|
|
274
288
|
onDirectMessage: acceptHandler(),
|
|
275
289
|
credentials: undefined,
|
|
290
|
+
artifacts: testArtifacts(),
|
|
276
291
|
eventIds: createEventIdDedupe(),
|
|
277
292
|
host: testHost(),
|
|
278
293
|
}
|
|
@@ -292,6 +307,7 @@ describe("dispatchInboundFromEventBody", () => {
|
|
|
292
307
|
onAppMention: async () => null,
|
|
293
308
|
onDirectMessage: async () => null,
|
|
294
309
|
credentials: undefined,
|
|
310
|
+
artifacts: testArtifacts(),
|
|
295
311
|
eventIds: createEventIdDedupe(),
|
|
296
312
|
host: testHost(),
|
|
297
313
|
}
|
|
@@ -324,6 +340,7 @@ describe("dispatchInboundFromEventBody", () => {
|
|
|
324
340
|
}),
|
|
325
341
|
onDirectMessage: async () => null,
|
|
326
342
|
credentials: undefined,
|
|
343
|
+
artifacts: testArtifacts(),
|
|
327
344
|
eventIds: createEventIdDedupe(),
|
|
328
345
|
host: testHost(),
|
|
329
346
|
});
|
|
@@ -357,6 +374,7 @@ describe("engagement routing", () => {
|
|
|
357
374
|
onAppMention: acceptHandler(),
|
|
358
375
|
onDirectMessage: async () => null,
|
|
359
376
|
credentials: undefined,
|
|
377
|
+
artifacts: testArtifacts(),
|
|
360
378
|
eventIds: createEventIdDedupe(),
|
|
361
379
|
host: testHost(),
|
|
362
380
|
log: createSlackLogger(() => undefined),
|
|
@@ -379,6 +397,7 @@ describe("engagement routing", () => {
|
|
|
379
397
|
onAppMention: acceptHandler(),
|
|
380
398
|
onDirectMessage: async () => null,
|
|
381
399
|
credentials: undefined as undefined,
|
|
400
|
+
artifacts: testArtifacts(),
|
|
382
401
|
eventIds: createEventIdDedupe(),
|
|
383
402
|
host: testHost(),
|
|
384
403
|
log: createSlackLogger(() => undefined),
|
|
@@ -433,6 +452,7 @@ describe("engagement routing", () => {
|
|
|
433
452
|
onAppMention: acceptHandler(),
|
|
434
453
|
onDirectMessage: async () => null,
|
|
435
454
|
credentials: undefined,
|
|
455
|
+
artifacts: testArtifacts(),
|
|
436
456
|
eventIds: createEventIdDedupe(),
|
|
437
457
|
host: testHost(),
|
|
438
458
|
log: createSlackLogger(() => undefined),
|
|
@@ -443,6 +463,56 @@ describe("engagement routing", () => {
|
|
|
443
463
|
});
|
|
444
464
|
|
|
445
465
|
describe("dispatchInboundMessage", () => {
|
|
466
|
+
it("hands the artifacts facade to the handler binding (ctx.artifacts)", async () => {
|
|
467
|
+
const tags: Array<Parameters<ArtifactsApi["tag"]>[0]> = [];
|
|
468
|
+
const artifacts: ArtifactsApi = {
|
|
469
|
+
tag: async (input) => {
|
|
470
|
+
tags.push(input);
|
|
471
|
+
return {
|
|
472
|
+
id: `art_${tags.length}`,
|
|
473
|
+
kind: input.kind ?? "artifact",
|
|
474
|
+
key: input.key,
|
|
475
|
+
data: input.data,
|
|
476
|
+
source: "host",
|
|
477
|
+
createdAt: new Date().toISOString(),
|
|
478
|
+
updatedAt: new Date().toISOString(),
|
|
479
|
+
};
|
|
480
|
+
},
|
|
481
|
+
list: async () => [],
|
|
482
|
+
};
|
|
483
|
+
const { send, calls } = createSendRecorder();
|
|
484
|
+
await dispatchInboundMessage({
|
|
485
|
+
kind: "channel_post",
|
|
486
|
+
message: {
|
|
487
|
+
text: "alert fired",
|
|
488
|
+
body: "alert fired",
|
|
489
|
+
markdown: "alert fired",
|
|
490
|
+
ts: "2.0",
|
|
491
|
+
threadTs: "1.0",
|
|
492
|
+
channelId: "C1",
|
|
493
|
+
teamId: "T1",
|
|
494
|
+
author: { userId: "U1", isBot: false },
|
|
495
|
+
},
|
|
496
|
+
handler: async (ctx) => {
|
|
497
|
+
await ctx.artifacts.tag({
|
|
498
|
+
kind: "alert_watch",
|
|
499
|
+
key: `${ctx.slack.channelId}:${ctx.slack.threadTs}`,
|
|
500
|
+
data: { status: "watching" },
|
|
501
|
+
});
|
|
502
|
+
return { auth: testAuth };
|
|
503
|
+
},
|
|
504
|
+
send,
|
|
505
|
+
credentials: undefined,
|
|
506
|
+
artifacts,
|
|
507
|
+
host: testHost(),
|
|
508
|
+
log: createSlackLogger(() => undefined),
|
|
509
|
+
});
|
|
510
|
+
expect(tags).toEqual([
|
|
511
|
+
{ kind: "alert_watch", key: "C1:1.0", data: { status: "watching" } },
|
|
512
|
+
]);
|
|
513
|
+
expect(calls).toHaveLength(1);
|
|
514
|
+
});
|
|
515
|
+
|
|
446
516
|
it("logs delivery failure when send throws", async () => {
|
|
447
517
|
const lines: string[] = [];
|
|
448
518
|
await dispatchInboundMessage({
|
|
@@ -462,6 +532,7 @@ describe("dispatchInboundMessage", () => {
|
|
|
462
532
|
throw new Error("engine down");
|
|
463
533
|
},
|
|
464
534
|
credentials: undefined,
|
|
535
|
+
artifacts: testArtifacts(),
|
|
465
536
|
host: testHost(),
|
|
466
537
|
log: createSlackLogger((line) => lines.push(line)),
|
|
467
538
|
});
|
|
@@ -500,6 +571,7 @@ describe("dispatchInboundMessage", () => {
|
|
|
500
571
|
handler: acceptHandler(),
|
|
501
572
|
send,
|
|
502
573
|
credentials: { botToken: "xoxb-test" },
|
|
574
|
+
artifacts: testArtifacts(),
|
|
503
575
|
host,
|
|
504
576
|
hasContinuationSession: async () => true,
|
|
505
577
|
log: createSlackLogger((line) => lines.push(line)),
|
|
@@ -538,6 +610,7 @@ describe("dispatchInboundMessage", () => {
|
|
|
538
610
|
handler: acceptHandler(),
|
|
539
611
|
send,
|
|
540
612
|
credentials: { botToken: "xoxb-test" },
|
|
613
|
+
artifacts: testArtifacts(),
|
|
541
614
|
host: testHost({
|
|
542
615
|
nudgeRoot: join(root, "nudges"),
|
|
543
616
|
askRoot: join(root, "asks"),
|
|
@@ -572,6 +645,7 @@ describe("dispatchInboundMessage", () => {
|
|
|
572
645
|
handler: acceptHandler(),
|
|
573
646
|
send,
|
|
574
647
|
credentials: { botToken: "xoxb-test" },
|
|
648
|
+
artifacts: testArtifacts(),
|
|
575
649
|
host: testHost({
|
|
576
650
|
evals: {
|
|
577
651
|
list: async () => {
|
|
@@ -613,6 +687,7 @@ describe("dispatchInboundMessage", () => {
|
|
|
613
687
|
handler: acceptHandler(),
|
|
614
688
|
send,
|
|
615
689
|
credentials: undefined,
|
|
690
|
+
artifacts: testArtifacts(),
|
|
616
691
|
host: testHost({
|
|
617
692
|
evals: {
|
|
618
693
|
list: async () => {
|
|
@@ -657,6 +732,7 @@ describe("dispatchInboundMessage", () => {
|
|
|
657
732
|
handler: acceptHandler(),
|
|
658
733
|
send,
|
|
659
734
|
credentials: undefined,
|
|
735
|
+
artifacts: testArtifacts(),
|
|
660
736
|
host,
|
|
661
737
|
});
|
|
662
738
|
await dispatchInboundMessage({
|
|
@@ -665,6 +741,7 @@ describe("dispatchInboundMessage", () => {
|
|
|
665
741
|
handler: acceptHandler(),
|
|
666
742
|
send,
|
|
667
743
|
credentials: undefined,
|
|
744
|
+
artifacts: testArtifacts(),
|
|
668
745
|
host,
|
|
669
746
|
});
|
|
670
747
|
expect(calls).toHaveLength(1);
|
|
@@ -705,6 +782,7 @@ describe("dispatchInboundMessage", () => {
|
|
|
705
782
|
handler: acceptHandler(),
|
|
706
783
|
send,
|
|
707
784
|
credentials: { botToken: "xoxb-test" },
|
|
785
|
+
artifacts: testArtifacts(),
|
|
708
786
|
host: testHost(),
|
|
709
787
|
log: createSlackLogger((line) => lines.push(line)),
|
|
710
788
|
});
|
|
@@ -749,6 +827,7 @@ describe("dispatchInboundMessage", () => {
|
|
|
749
827
|
handler: acceptHandler(),
|
|
750
828
|
send,
|
|
751
829
|
credentials: { botToken: "xoxb-test" },
|
|
830
|
+
artifacts: testArtifacts(),
|
|
752
831
|
host: testHost(),
|
|
753
832
|
hasContinuationSession: async () => true,
|
|
754
833
|
// Agent watermark (not Datadog's parent) — human reply at 2.0 is in delta.
|
|
@@ -787,6 +866,7 @@ describe("dispatchInboundMessage", () => {
|
|
|
787
866
|
handler: acceptHandler(),
|
|
788
867
|
send,
|
|
789
868
|
credentials: undefined,
|
|
869
|
+
artifacts: testArtifacts(),
|
|
790
870
|
host,
|
|
791
871
|
log: createSlackLogger(() => undefined),
|
|
792
872
|
});
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
* Shared inbound dispatch for Slack Events API bodies and Socket Mode.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
ArtifactsApi,
|
|
7
|
+
HostContext,
|
|
8
|
+
JsonValue,
|
|
9
|
+
SendMessageFn,
|
|
10
|
+
} from "../../types.js";
|
|
6
11
|
import { buildSlackBinding, slackContinuationKey } from "./api.js";
|
|
7
12
|
import { firstLineTitle, maybeSetThreadTitle } from "./assistant.js";
|
|
8
13
|
import type { SlackBotMentionGate } from "./bot-mentions.js";
|
|
@@ -65,6 +70,8 @@ export interface SlackInboundDispatchDeps extends SlackContinuationProbes {
|
|
|
65
70
|
credentials: SlackCredentials | undefined;
|
|
66
71
|
eventIds: EventIdDedupe;
|
|
67
72
|
host: HostContext;
|
|
73
|
+
/** Unbound artifacts facade handed to handler bindings (`ctx.artifacts`). */
|
|
74
|
+
artifacts: ArtifactsApi;
|
|
68
75
|
log?: SlackLog;
|
|
69
76
|
/** Dispatch on `app_mention`. Defaults to `true`. */
|
|
70
77
|
mentionsEnabled?: boolean;
|
|
@@ -110,6 +117,7 @@ export async function dispatchInboundMessage(args: {
|
|
|
110
117
|
send: SendMessageFn;
|
|
111
118
|
credentials: SlackCredentials | undefined;
|
|
112
119
|
host: HostContext;
|
|
120
|
+
artifacts: ArtifactsApi;
|
|
113
121
|
hasContinuationSession?: (continuationToken: string) => Promise<boolean>;
|
|
114
122
|
isContinuationBusy?: (continuationToken: string) => Promise<boolean>;
|
|
115
123
|
getContinuationLastBotMessageTs?: (
|
|
@@ -132,6 +140,7 @@ export async function dispatchInboundMessage(args: {
|
|
|
132
140
|
channelId: message.channelId,
|
|
133
141
|
threadTs: message.threadTs,
|
|
134
142
|
host: args.host,
|
|
143
|
+
artifacts: args.artifacts,
|
|
135
144
|
log,
|
|
136
145
|
...(message.teamId === undefined ? {} : { teamId: message.teamId }),
|
|
137
146
|
});
|
|
@@ -450,6 +459,7 @@ export function dispatchInboundFromEventBody(
|
|
|
450
459
|
send: deps.send,
|
|
451
460
|
credentials: deps.credentials,
|
|
452
461
|
host: deps.host,
|
|
462
|
+
artifacts: deps.artifacts,
|
|
453
463
|
log,
|
|
454
464
|
evalCommandsEnabled: deps.evalCommandsEnabled,
|
|
455
465
|
cursorAgentName: deps.cursorAgentName,
|
|
@@ -490,6 +500,7 @@ export function dispatchInboundFromEventBody(
|
|
|
490
500
|
send: deps.send,
|
|
491
501
|
credentials: deps.credentials,
|
|
492
502
|
host: deps.host,
|
|
503
|
+
artifacts: deps.artifacts,
|
|
493
504
|
log,
|
|
494
505
|
evalCommandsEnabled: deps.evalCommandsEnabled,
|
|
495
506
|
cursorAgentName: deps.cursorAgentName,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type {
|
|
11
11
|
ApprovalDecision,
|
|
12
|
+
ArtifactsApi,
|
|
12
13
|
AuthContext,
|
|
13
14
|
ChannelSession,
|
|
14
15
|
HostContext,
|
|
@@ -275,6 +276,7 @@ export interface SlackBlockActionDispatchDeps extends SlackContinuationProbes {
|
|
|
275
276
|
send: SendMessageFn;
|
|
276
277
|
credentials: SlackCredentials | undefined;
|
|
277
278
|
host: HostContext;
|
|
279
|
+
artifacts: ArtifactsApi;
|
|
278
280
|
log?: SlackLog;
|
|
279
281
|
}
|
|
280
282
|
|
|
@@ -347,6 +349,7 @@ export async function dispatchBlockAction(
|
|
|
347
349
|
send: deps.send,
|
|
348
350
|
credentials: deps.credentials,
|
|
349
351
|
host: deps.host,
|
|
352
|
+
artifacts: deps.artifacts,
|
|
350
353
|
log,
|
|
351
354
|
// Click text is synthetic — never treat it as a stop/eval directive.
|
|
352
355
|
evalCommandsEnabled: false,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { allowAll, defineChannel } from "../../channels.js";
|
|
11
11
|
import type {
|
|
12
|
+
ArtifactsApi,
|
|
12
13
|
ChannelDefinition,
|
|
13
14
|
ChannelEventHandlers,
|
|
14
15
|
HostContext,
|
|
@@ -215,6 +216,7 @@ export function slackChannel<const Name extends string = string>(
|
|
|
215
216
|
send: SendMessageFn;
|
|
216
217
|
waitUntil: (promise: Promise<unknown>) => void;
|
|
217
218
|
host: HostContext;
|
|
219
|
+
artifacts: ArtifactsApi;
|
|
218
220
|
hasContinuationSession?: (continuationToken: string) => Promise<boolean>;
|
|
219
221
|
isContinuationBusy?: (continuationToken: string) => Promise<boolean>;
|
|
220
222
|
getContinuationLastBotMessageTs?: (
|
|
@@ -230,6 +232,7 @@ export function slackChannel<const Name extends string = string>(
|
|
|
230
232
|
credentials,
|
|
231
233
|
eventIds,
|
|
232
234
|
host: args.host,
|
|
235
|
+
artifacts: args.artifacts,
|
|
233
236
|
log: getLog(),
|
|
234
237
|
mentionsEnabled,
|
|
235
238
|
directMessagesEnabled,
|
|
@@ -410,6 +413,7 @@ export function slackChannel<const Name extends string = string>(
|
|
|
410
413
|
send: args.send,
|
|
411
414
|
credentials,
|
|
412
415
|
host: args.host,
|
|
416
|
+
artifacts: args.artifacts,
|
|
413
417
|
log: getLog(),
|
|
414
418
|
evalCommandsEnabled,
|
|
415
419
|
...(args.hasContinuationSession === undefined
|
|
@@ -454,6 +458,7 @@ export function slackChannel<const Name extends string = string>(
|
|
|
454
458
|
send: args.send,
|
|
455
459
|
waitUntil: args.waitUntil,
|
|
456
460
|
host: args.host,
|
|
461
|
+
artifacts: args.artifacts,
|
|
457
462
|
...(args.hasContinuationSession === undefined
|
|
458
463
|
? {}
|
|
459
464
|
: { hasContinuationSession: args.hasContinuationSession }),
|
|
@@ -496,6 +501,7 @@ export function slackChannel<const Name extends string = string>(
|
|
|
496
501
|
send: args.send,
|
|
497
502
|
credentials,
|
|
498
503
|
host: args.host,
|
|
504
|
+
artifacts: args.artifacts,
|
|
499
505
|
log: getLog(),
|
|
500
506
|
hasContinuationSession: args.hasContinuationSession,
|
|
501
507
|
isContinuationBusy: args.isContinuationBusy,
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type {
|
|
6
6
|
AgentCloudOptions,
|
|
7
|
+
ArtifactsApi,
|
|
7
8
|
AuthContext,
|
|
8
9
|
ChannelEventHandlers,
|
|
9
10
|
HostContext,
|
|
@@ -189,6 +190,11 @@ export interface SlackBinding {
|
|
|
189
190
|
};
|
|
190
191
|
/** Shared host services (MCP / GitHub / Slack). */
|
|
191
192
|
host: HostContext;
|
|
193
|
+
/**
|
|
194
|
+
* Durable artifacts (unbound — the handler runs before any session is
|
|
195
|
+
* admitted; pass `sessionId` in `tag` input to attribute one).
|
|
196
|
+
*/
|
|
197
|
+
artifacts: ArtifactsApi;
|
|
192
198
|
}
|
|
193
199
|
|
|
194
200
|
export type SlackMessageHandler = (
|
|
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { defineStorage } from "../storage.js";
|
|
6
|
-
import type { JsonValue } from "../types.js";
|
|
6
|
+
import type { ArtifactRecord, ArtifactsApi, JsonValue } from "../types.js";
|
|
7
7
|
import { ReminderRunner, ReminderValidationError } from "./reminder-runner.js";
|
|
8
8
|
import type { SessionEngine } from "./session-engine.js";
|
|
9
9
|
import { StorageCoordinator } from "./storage-coordinator.js";
|
|
@@ -25,9 +25,28 @@ function mockEngine(
|
|
|
25
25
|
getEventStream: () => ({}) as never,
|
|
26
26
|
waitForCompletion: async () => ({ status: "completed" as const }),
|
|
27
27
|
}));
|
|
28
|
+
const artifactRows: ArtifactRecord[] = [];
|
|
29
|
+
const artifacts: ArtifactsApi = {
|
|
30
|
+
tag: async (input) => {
|
|
31
|
+
const record: ArtifactRecord = {
|
|
32
|
+
id: `art_${artifactRows.length + 1}`,
|
|
33
|
+
kind: input.kind ?? "artifact",
|
|
34
|
+
key: input.key,
|
|
35
|
+
data: input.data,
|
|
36
|
+
sessionId: input.sessionId,
|
|
37
|
+
source: "host",
|
|
38
|
+
createdAt: new Date().toISOString(),
|
|
39
|
+
updatedAt: new Date().toISOString(),
|
|
40
|
+
};
|
|
41
|
+
artifactRows.push(record);
|
|
42
|
+
return record;
|
|
43
|
+
},
|
|
44
|
+
list: async () => [...artifactRows],
|
|
45
|
+
};
|
|
28
46
|
return {
|
|
29
47
|
stateRoot,
|
|
30
48
|
storage,
|
|
49
|
+
artifacts,
|
|
31
50
|
host: {
|
|
32
51
|
mcp: {} as never,
|
|
33
52
|
github: {} as never,
|
|
@@ -145,6 +164,29 @@ describe("ReminderRunner", () => {
|
|
|
145
164
|
expect(message).toContain("check ci");
|
|
146
165
|
});
|
|
147
166
|
|
|
167
|
+
it("run handlers get the unbound artifacts facade", async () => {
|
|
168
|
+
dir = await mkdtemp(join(tmpdir(), "rem-runner-"));
|
|
169
|
+
const engine = mockEngine(dir);
|
|
170
|
+
const runner = new ReminderRunner(engine);
|
|
171
|
+
await runner.start();
|
|
172
|
+
|
|
173
|
+
const created = await runner.create({
|
|
174
|
+
purpose: "alert_watch",
|
|
175
|
+
channelId: "drive",
|
|
176
|
+
continuationToken: "tok",
|
|
177
|
+
every: "1m",
|
|
178
|
+
run: async ({ artifacts }) => {
|
|
179
|
+
await artifacts.tag({ key: "from-reminder", data: "r" });
|
|
180
|
+
return { action: "skip" };
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
await runner.dispatch(created.id);
|
|
184
|
+
|
|
185
|
+
const rows = await engine.artifacts.list();
|
|
186
|
+
expect(rows.map((row) => row.key)).toEqual(["from-reminder"]);
|
|
187
|
+
expect(rows[0]?.sessionId).toBeUndefined();
|
|
188
|
+
});
|
|
189
|
+
|
|
148
190
|
it("disarms prompt reminders when the bound session is gone", async () => {
|
|
149
191
|
dir = await mkdtemp(join(tmpdir(), "rem-runner-"));
|
|
150
192
|
const engine = mockEngine(dir);
|
|
@@ -664,6 +664,7 @@ export class ReminderRunner {
|
|
|
664
664
|
host: this.engine.host,
|
|
665
665
|
mcp: this.engine.host.mcp,
|
|
666
666
|
appAuth: APP_AUTH,
|
|
667
|
+
artifacts: this.engine.artifacts,
|
|
667
668
|
async followup(args: { message: string }) {
|
|
668
669
|
const auth = await self.resolveFollowupAuth(
|
|
669
670
|
record.channelId,
|
|
@@ -151,6 +151,7 @@ export class ScheduleRunner {
|
|
|
151
151
|
},
|
|
152
152
|
appAuth: APP_AUTH,
|
|
153
153
|
host: this.engine.host,
|
|
154
|
+
artifacts: this.engine.artifacts,
|
|
154
155
|
});
|
|
155
156
|
// Collect ids from receive() calls the handler chose not to await
|
|
156
157
|
// (commonly wrapped in waitUntil); receive resolves at message accept,
|
|
@@ -4,12 +4,13 @@ import { join } from "node:path";
|
|
|
4
4
|
import { afterEach, describe, expect, it } from "vitest";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { defineArtifacts } from "../artifacts.js";
|
|
7
|
-
import { httpChannel } from "../channels.js";
|
|
7
|
+
import { defineChannel, httpChannel } from "../channels.js";
|
|
8
8
|
import { defineTool } from "../tools.js";
|
|
9
9
|
import type {
|
|
10
10
|
AgentProject,
|
|
11
11
|
ArtifactRecord,
|
|
12
12
|
AuthContext,
|
|
13
|
+
DiscoveredChannel,
|
|
13
14
|
DiscoveredTool,
|
|
14
15
|
ServeOptions,
|
|
15
16
|
} from "../types.js";
|
|
@@ -35,7 +36,10 @@ const seedTool = defineTool({
|
|
|
35
36
|
},
|
|
36
37
|
});
|
|
37
38
|
|
|
38
|
-
function testProject(
|
|
39
|
+
function testProject(
|
|
40
|
+
rootDir: string,
|
|
41
|
+
options?: { channels?: DiscoveredChannel[] }
|
|
42
|
+
): AgentProject {
|
|
39
43
|
const discovered: DiscoveredTool = {
|
|
40
44
|
name: "seed",
|
|
41
45
|
description: seedTool.description,
|
|
@@ -59,7 +63,7 @@ function testProject(rootDir: string): AgentProject {
|
|
|
59
63
|
seedFiles: [],
|
|
60
64
|
},
|
|
61
65
|
httpChannel: httpChannel(),
|
|
62
|
-
channels: [],
|
|
66
|
+
channels: options?.channels ?? [],
|
|
63
67
|
schedules: [],
|
|
64
68
|
hooks: [],
|
|
65
69
|
abs: [],
|
|
@@ -87,10 +91,15 @@ afterEach(async () => {
|
|
|
87
91
|
}
|
|
88
92
|
});
|
|
89
93
|
|
|
90
|
-
async function startTestServer(
|
|
94
|
+
async function startTestServer(
|
|
95
|
+
options?: Partial<ServeOptions>,
|
|
96
|
+
projectOptions?: { channels?: DiscoveredChannel[] }
|
|
97
|
+
) {
|
|
91
98
|
const stateRoot = await mkdtemp(join(tmpdir(), "agent-serve-artroute-"));
|
|
92
99
|
cleanups.push(() => rm(stateRoot, { recursive: true, force: true }));
|
|
93
|
-
const mounts: AgentMount[] = [
|
|
100
|
+
const mounts: AgentMount[] = [
|
|
101
|
+
{ slug: "", project: testProject(stateRoot, projectOptions) },
|
|
102
|
+
];
|
|
94
103
|
const handle = await startServer(mounts, {
|
|
95
104
|
port: 0,
|
|
96
105
|
stateRoot,
|
|
@@ -181,6 +190,28 @@ describe("GET /v1/artifacts", () => {
|
|
|
181
190
|
expect(listed.artifacts).toEqual([]);
|
|
182
191
|
});
|
|
183
192
|
|
|
193
|
+
it("channel onStart gets the unbound artifacts facade", async () => {
|
|
194
|
+
const channel: DiscoveredChannel = {
|
|
195
|
+
id: "starter",
|
|
196
|
+
definition: defineChannel({
|
|
197
|
+
async onStart({ artifacts }) {
|
|
198
|
+
await artifacts.tag({
|
|
199
|
+
kind: "report",
|
|
200
|
+
key: "from-onstart",
|
|
201
|
+
data: 1,
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
}),
|
|
205
|
+
};
|
|
206
|
+
const handle = await startTestServer(
|
|
207
|
+
{ dev: true },
|
|
208
|
+
{ channels: [channel] }
|
|
209
|
+
);
|
|
210
|
+
const listed = await listArtifacts(handle.url);
|
|
211
|
+
expect(listed.artifacts.map((a) => a.key)).toEqual(["from-onstart"]);
|
|
212
|
+
expect(listed.artifacts[0]?.sessionId).toBeUndefined();
|
|
213
|
+
});
|
|
214
|
+
|
|
184
215
|
it("requires the bearer token when one is configured", async () => {
|
|
185
216
|
const handle = await startTestServer({ authToken: "sekrit" });
|
|
186
217
|
const denied = await fetch(`${handle.url}/v1/artifacts`);
|
package/src/internal/server.ts
CHANGED
|
@@ -494,6 +494,7 @@ export async function startServer(
|
|
|
494
494
|
interruptContinuation: (continuationToken) =>
|
|
495
495
|
engine.interruptContinuation(channel.id, continuationToken),
|
|
496
496
|
logger,
|
|
497
|
+
artifacts: engine.artifacts,
|
|
497
498
|
};
|
|
498
499
|
try {
|
|
499
500
|
await definition.onStart(startArgs);
|
|
@@ -1778,6 +1779,7 @@ async function startCursorSlackRelay(input: {
|
|
|
1778
1779
|
envelope,
|
|
1779
1780
|
send: args.send,
|
|
1780
1781
|
host: args.host,
|
|
1782
|
+
artifacts: args.artifacts,
|
|
1781
1783
|
hasContinuationSession: (continuationToken) =>
|
|
1782
1784
|
target.runtime.engine.hasContinuationSession(
|
|
1783
1785
|
target.channelId,
|
|
@@ -3,17 +3,21 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { defineArtifacts } from "../artifacts.js";
|
|
6
|
-
import { httpChannel } from "../channels.js";
|
|
6
|
+
import { defineChannel, httpChannel } from "../channels.js";
|
|
7
7
|
import { defineHook } from "../hooks.js";
|
|
8
|
+
import { defineSchedule } from "../schedules.js";
|
|
8
9
|
import { defineTool } from "../tools.js";
|
|
9
10
|
import type {
|
|
10
11
|
AgentProject,
|
|
11
12
|
ArtifactsDefinition,
|
|
13
|
+
DiscoveredChannel,
|
|
12
14
|
DiscoveredHook,
|
|
15
|
+
DiscoveredSchedule,
|
|
13
16
|
DiscoveredTool,
|
|
14
17
|
JsonObject,
|
|
15
18
|
ToolContext,
|
|
16
19
|
} from "../types.js";
|
|
20
|
+
import { ScheduleRunner } from "./schedule-runner.js";
|
|
17
21
|
import type { AgentRunner } from "./sdk-runner.js";
|
|
18
22
|
import { SessionEngine } from "./session-engine.js";
|
|
19
23
|
|
|
@@ -46,6 +50,8 @@ function testProject(options?: {
|
|
|
46
50
|
artifacts?: ArtifactsDefinition;
|
|
47
51
|
tools?: DiscoveredTool[];
|
|
48
52
|
hooks?: DiscoveredHook[];
|
|
53
|
+
schedules?: DiscoveredSchedule[];
|
|
54
|
+
channels?: DiscoveredChannel[];
|
|
49
55
|
}): AgentProject {
|
|
50
56
|
return {
|
|
51
57
|
rootDir: "/tmp/artifacts-fixture",
|
|
@@ -62,8 +68,8 @@ function testProject(options?: {
|
|
|
62
68
|
seedFiles: [],
|
|
63
69
|
},
|
|
64
70
|
httpChannel: httpChannel(),
|
|
65
|
-
channels: [],
|
|
66
|
-
schedules: [],
|
|
71
|
+
channels: options?.channels ?? [],
|
|
72
|
+
schedules: options?.schedules ?? [],
|
|
67
73
|
hooks: options?.hooks ?? [],
|
|
68
74
|
abs: [],
|
|
69
75
|
artifacts: options?.artifacts,
|
|
@@ -198,6 +204,50 @@ describe("session engine artifacts wiring", () => {
|
|
|
198
204
|
expect(await engine.artifacts.list()).toHaveLength(0);
|
|
199
205
|
});
|
|
200
206
|
|
|
207
|
+
it("schedule handlers get the unbound artifacts facade", async () => {
|
|
208
|
+
const cron = "* * * * *";
|
|
209
|
+
const schedule: DiscoveredSchedule = {
|
|
210
|
+
name: "tagger",
|
|
211
|
+
cron,
|
|
212
|
+
kind: "run",
|
|
213
|
+
definition: defineSchedule({
|
|
214
|
+
cron,
|
|
215
|
+
async run({ artifacts }) {
|
|
216
|
+
await artifacts.tag({ key: "from-schedule", data: "s" });
|
|
217
|
+
},
|
|
218
|
+
}),
|
|
219
|
+
};
|
|
220
|
+
const project = testProject({ schedules: [schedule] });
|
|
221
|
+
const engine = await makeEngine(project, echoRunner());
|
|
222
|
+
const runner = new ScheduleRunner(engine, project);
|
|
223
|
+
await runner.dispatch("tagger");
|
|
224
|
+
|
|
225
|
+
const rows = await engine.artifacts.list();
|
|
226
|
+
expect(rows.map((row) => row.key)).toEqual(["from-schedule"]);
|
|
227
|
+
expect(rows[0]?.sessionId).toBeUndefined();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("channel receive hooks get the unbound artifacts facade", async () => {
|
|
231
|
+
const channel: DiscoveredChannel = {
|
|
232
|
+
id: "handoff",
|
|
233
|
+
definition: defineChannel({
|
|
234
|
+
async receive(input, { send, artifacts }) {
|
|
235
|
+
await artifacts.tag({ key: "from-receive", data: "r" });
|
|
236
|
+
return send(input.message, { auth: input.auth });
|
|
237
|
+
},
|
|
238
|
+
}),
|
|
239
|
+
};
|
|
240
|
+
const engine = await makeEngine(
|
|
241
|
+
testProject({ channels: [channel] }),
|
|
242
|
+
echoRunner()
|
|
243
|
+
);
|
|
244
|
+
await engine.receive("handoff", { message: "hand-off", auth: null });
|
|
245
|
+
|
|
246
|
+
const rows = await engine.artifacts.list();
|
|
247
|
+
expect(rows.map((row) => row.key)).toEqual(["from-receive"]);
|
|
248
|
+
expect(rows[0]?.sessionId).toBeUndefined();
|
|
249
|
+
});
|
|
250
|
+
|
|
201
251
|
it("hook ctx.artifacts is bound to the hook's session", async () => {
|
|
202
252
|
const hook: DiscoveredHook = {
|
|
203
253
|
name: "tagger",
|
|
@@ -716,7 +716,10 @@ export class SessionEngine {
|
|
|
716
716
|
lastCreated = session;
|
|
717
717
|
return session;
|
|
718
718
|
};
|
|
719
|
-
const returned = await definition.receive(input, {
|
|
719
|
+
const returned = await definition.receive(input, {
|
|
720
|
+
send,
|
|
721
|
+
artifacts: this.artifactsApi(),
|
|
722
|
+
});
|
|
720
723
|
const session = returned ?? lastCreated;
|
|
721
724
|
if (session === undefined) {
|
|
722
725
|
throw new Error(
|
package/src/types.ts
CHANGED
|
@@ -1505,6 +1505,12 @@ export interface ChannelStartArgs {
|
|
|
1505
1505
|
* should prefer this over `console.*` so CLI/eval hosts can redirect.
|
|
1506
1506
|
*/
|
|
1507
1507
|
logger: (line: string) => void;
|
|
1508
|
+
/**
|
|
1509
|
+
* Durable artifacts (unbound, same facade as
|
|
1510
|
+
* {@link ChannelHandlerArgs.artifacts}) — for host pipelines the channel
|
|
1511
|
+
* drives outside any route handler.
|
|
1512
|
+
*/
|
|
1513
|
+
artifacts: ArtifactsApi;
|
|
1508
1514
|
}
|
|
1509
1515
|
|
|
1510
1516
|
/**
|
|
@@ -1551,7 +1557,7 @@ export interface ChannelConfig<TState = JsonValue> {
|
|
|
1551
1557
|
*/
|
|
1552
1558
|
receive?: (
|
|
1553
1559
|
input: ReceiveInput,
|
|
1554
|
-
args: { send: SendMessageFn }
|
|
1560
|
+
args: { send: SendMessageFn; artifacts: ArtifactsApi }
|
|
1555
1561
|
// biome-ignore lint/suspicious/noConfusingVoidType: hooks may return a session, nothing, or an async nothing
|
|
1556
1562
|
) => Promise<ChannelSession | undefined | void> | ChannelSession | void;
|
|
1557
1563
|
/** Stream-event handlers for sessions owned by this channel. */
|
|
@@ -1742,6 +1748,11 @@ export interface ScheduleHandlerArgs {
|
|
|
1742
1748
|
appAuth: AuthContext;
|
|
1743
1749
|
/** Shared host services (MCP / GitHub / Slack). Same as tool `ctx.host`. */
|
|
1744
1750
|
host: HostContext;
|
|
1751
|
+
/**
|
|
1752
|
+
* Durable artifacts (unbound — schedule handlers have no ambient session;
|
|
1753
|
+
* pass `sessionId` explicitly to attribute).
|
|
1754
|
+
*/
|
|
1755
|
+
artifacts: ArtifactsApi;
|
|
1745
1756
|
}
|
|
1746
1757
|
|
|
1747
1758
|
export interface ScheduleConfig {
|
|
@@ -1795,6 +1806,12 @@ export interface ReminderFireContext extends ReminderUntilContext {
|
|
|
1795
1806
|
/** Wake the reminder's already-bound session (target is implicit). */
|
|
1796
1807
|
followup(args: { message: string }): Promise<ChannelSession>;
|
|
1797
1808
|
cancel(reason?: string): Promise<void>;
|
|
1809
|
+
/**
|
|
1810
|
+
* Durable artifacts (unbound — the reminder is bound to a continuation
|
|
1811
|
+
* token, not a resolved session; pass `sessionId` in `tag` input to
|
|
1812
|
+
* attribute one).
|
|
1813
|
+
*/
|
|
1814
|
+
artifacts: ArtifactsApi;
|
|
1798
1815
|
}
|
|
1799
1816
|
|
|
1800
1817
|
/**
|