experimental-ash 0.25.1 → 0.26.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/bin/ash.d.ts +4 -4
  3. package/bin/ash.js +12 -8
  4. package/dist/docs/public/channels/README.md +26 -2
  5. package/dist/docs/public/channels/discord.md +159 -0
  6. package/dist/docs/public/channels/slack.md +14 -2
  7. package/dist/src/channel/routes.d.ts +6 -1
  8. package/dist/src/channel/send.js +5 -2
  9. package/dist/src/channel/session-callback.d.ts +10 -0
  10. package/dist/src/channel/session-callback.js +65 -0
  11. package/dist/src/channel/types.d.ts +19 -0
  12. package/dist/src/chunks/{client-BShLWzR6.js → client-ZqNLLMZB.js} +3 -3
  13. package/dist/src/chunks/{compile-agent-CyP6FrL8.js → compile-agent-DrIyb818.js} +1 -1
  14. package/dist/src/chunks/{dev-authored-source-watcher-DIWfVUsu.js → dev-authored-source-watcher-C1WUVv9F.js} +1 -1
  15. package/dist/src/chunks/host-CwAcCrg7.js +70 -0
  16. package/dist/src/chunks/paths-CWZN-XRX.js +85 -0
  17. package/dist/src/chunks/{token-BOkIxJeV.js → token-YW4VSeBB.js} +1 -1
  18. package/dist/src/chunks/types-BJSR0JNV.js +1 -0
  19. package/dist/src/cli/commands/channels.d.ts +15 -0
  20. package/dist/src/cli/commands/channels.js +9 -0
  21. package/dist/src/cli/commands/info.js +1 -1
  22. package/dist/src/cli/dev/repl.js +3 -3
  23. package/dist/src/cli/run.js +1 -1
  24. package/dist/src/client/message-reducer.js +6 -0
  25. package/dist/src/context/keys.d.ts +1 -1
  26. package/dist/src/context/keys.js +1 -1
  27. package/dist/src/context/seed-keys.d.ts +5 -1
  28. package/dist/src/context/seed-keys.js +4 -0
  29. package/dist/src/evals/cli/eval.js +1 -1
  30. package/dist/src/execution/await-authorization-orchestrator.js +1 -1
  31. package/dist/src/execution/node-step.js +13 -0
  32. package/dist/src/execution/remote-agent-dispatch.d.ts +15 -0
  33. package/dist/src/execution/remote-agent-dispatch.js +79 -0
  34. package/dist/src/execution/runtime-context.js +4 -1
  35. package/dist/src/execution/session-callback-step.d.ts +16 -0
  36. package/dist/src/execution/session-callback-step.js +72 -0
  37. package/dist/src/execution/subagent-invocation.d.ts +16 -0
  38. package/dist/src/execution/subagent-invocation.js +16 -0
  39. package/dist/src/execution/subagent-tool.js +5 -8
  40. package/dist/src/execution/workflow-entry.js +21 -1
  41. package/dist/src/execution/workflow-steps.d.ts +6 -1
  42. package/dist/src/execution/workflow-steps.js +76 -25
  43. package/dist/src/harness/execute-tool.d.ts +3 -3
  44. package/dist/src/harness/runtime-actions.d.ts +1 -0
  45. package/dist/src/harness/runtime-actions.js +18 -1
  46. package/dist/src/internal/application/package.js +1 -1
  47. package/dist/src/internal/process/pnpm.d.ts +28 -0
  48. package/dist/src/internal/process/pnpm.js +50 -0
  49. package/dist/src/protocol/message.d.ts +6 -0
  50. package/dist/src/protocol/message.js +1 -0
  51. package/dist/src/protocol/routes.d.ts +11 -0
  52. package/dist/src/protocol/routes.js +13 -0
  53. package/dist/src/public/channels/ash.js +25 -1
  54. package/dist/src/public/channels/discord/api.d.ts +99 -0
  55. package/dist/src/public/channels/discord/api.js +167 -0
  56. package/dist/src/public/channels/discord/defaults.d.ts +9 -0
  57. package/dist/src/public/channels/discord/defaults.js +74 -0
  58. package/dist/src/public/channels/discord/discordChannel.d.ts +132 -0
  59. package/dist/src/public/channels/discord/discordChannel.js +402 -0
  60. package/dist/src/public/channels/discord/hitl.d.ts +34 -0
  61. package/dist/src/public/channels/discord/hitl.js +194 -0
  62. package/dist/src/public/channels/discord/inbound.d.ts +97 -0
  63. package/dist/src/public/channels/discord/inbound.js +238 -0
  64. package/dist/src/public/channels/discord/index.d.ts +7 -0
  65. package/dist/src/public/channels/discord/index.js +6 -0
  66. package/dist/src/public/channels/discord/responses.d.ts +11 -0
  67. package/dist/src/public/channels/discord/responses.js +40 -0
  68. package/dist/src/public/channels/discord/verify.d.ts +38 -0
  69. package/dist/src/public/channels/discord/verify.js +72 -0
  70. package/dist/src/public/channels/discord/verifyInbound.d.ts +6 -0
  71. package/dist/src/public/channels/discord/verifyInbound.js +19 -0
  72. package/dist/src/public/channels/slack/constants.d.ts +7 -0
  73. package/dist/src/public/channels/slack/constants.js +7 -0
  74. package/dist/src/public/channels/slack/slackChannel.js +2 -1
  75. package/dist/src/runtime/actions/keys.js +2 -0
  76. package/dist/src/runtime/actions/types.d.ts +47 -1
  77. package/dist/src/runtime/actions/types.js +23 -0
  78. package/dist/src/runtime/connections/callback-route.d.ts +1 -1
  79. package/dist/src/runtime/connections/callback-route.js +1 -1
  80. package/dist/src/runtime/connections/mcp-client.d.ts +1 -2
  81. package/dist/src/runtime/connections/mcp-client.js +69 -3
  82. package/dist/src/runtime/framework-channels/index.js +7 -2
  83. package/dist/src/runtime/session-callback-route.d.ts +6 -0
  84. package/dist/src/runtime/session-callback-route.js +87 -0
  85. package/package.json +9 -3
  86. package/dist/src/chunks/host-BxT35q6K.js +0 -70
  87. package/dist/src/chunks/paths-B2hLA0Fn.js +0 -85
  88. package/dist/src/chunks/types-CjIyrcYo.js +0 -1
@@ -65,7 +65,13 @@ export function recordPendingSubagentChildToken(input) {
65
65
  */
66
66
  export async function accumulateRuntimeActionResults(input) {
67
67
  const batch = getPendingRuntimeActionBatch(input.session);
68
- const buffered = [];
68
+ const buffered = [...(input.initialResults ?? [])];
69
+ if (batch !== undefined && buffered.length > 0) {
70
+ const ready = resolveRuntimeActionResultsForBatch({ batch, results: buffered });
71
+ if (ready !== undefined) {
72
+ return ready;
73
+ }
74
+ }
69
75
  while (true) {
70
76
  const item = await input.getNext();
71
77
  if (item === null) {
@@ -249,6 +255,17 @@ export function createRuntimeActionRequestFromToolCall(input) {
249
255
  subagentName: definition.runtimeAction.subagentName,
250
256
  };
251
257
  }
258
+ if (definition?.runtimeAction?.kind === "remote-agent-call") {
259
+ return {
260
+ callId: input.toolCall.toolCallId,
261
+ description: definition.description,
262
+ input: resolveToolCallInputObject(input.toolCall.input),
263
+ kind: "remote-agent-call",
264
+ name: definition.name,
265
+ nodeId: definition.runtimeAction.nodeId,
266
+ remoteAgentName: definition.runtimeAction.remoteAgentName ?? definition.name,
267
+ };
268
+ }
252
269
  return {
253
270
  callId: input.toolCall.toolCallId,
254
271
  input: resolveToolCallInputObject(input.toolCall.input),
@@ -6,7 +6,7 @@ import { ASH_PACKAGE_NAME } from "#internal/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.25.1";
9
+ const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.26.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",
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Resolved invocation for the host's pnpm executable. The shape carries
3
+ * everything `child_process.spawn` and `execFile` need to dispatch to the
4
+ * right binary across macOS/Linux PATH installs, Corepack-managed shims,
5
+ * and Windows runners that surface pnpm only through `PNPM_HOME`.
6
+ */
7
+ export interface PnpmInvocation {
8
+ readonly args: readonly string[];
9
+ readonly command: string;
10
+ readonly shell?: boolean;
11
+ }
12
+ /**
13
+ * Picks the right pnpm executable for the current host. Resolution order:
14
+ *
15
+ * 1. `PNPM_HOME` — the standard install location used by Corepack and the
16
+ * pnpm installers. On Windows, points at `pnpm.CMD` because the bare
17
+ * `pnpm` shim is not directly invokable from a non-shell spawn.
18
+ * 2. `npm_execpath` — set when the current process was launched by an
19
+ * npm-compatible package manager. Pointing at a `.cjs`/`.js` entry
20
+ * means we have to run it through `node` (typical for Corepack
21
+ * shims); otherwise treat it as a bare executable path.
22
+ * 3. Bare `pnpm` on PATH — the macOS/Linux happy path.
23
+ *
24
+ * Pure: no side effects, returns the invocation shape; the caller picks
25
+ * `spawn` vs. `execFile`. The test harness uses this to keep platform
26
+ * handling in one place.
27
+ */
28
+ export declare function resolvePnpmInvocation(args: readonly string[]): PnpmInvocation;
@@ -0,0 +1,50 @@
1
+ import { existsSync } from "node:fs";
2
+ import { extname, join } from "node:path";
3
+ /**
4
+ * Picks the right pnpm executable for the current host. Resolution order:
5
+ *
6
+ * 1. `PNPM_HOME` — the standard install location used by Corepack and the
7
+ * pnpm installers. On Windows, points at `pnpm.CMD` because the bare
8
+ * `pnpm` shim is not directly invokable from a non-shell spawn.
9
+ * 2. `npm_execpath` — set when the current process was launched by an
10
+ * npm-compatible package manager. Pointing at a `.cjs`/`.js` entry
11
+ * means we have to run it through `node` (typical for Corepack
12
+ * shims); otherwise treat it as a bare executable path.
13
+ * 3. Bare `pnpm` on PATH — the macOS/Linux happy path.
14
+ *
15
+ * Pure: no side effects, returns the invocation shape; the caller picks
16
+ * `spawn` vs. `execFile`. The test harness uses this to keep platform
17
+ * handling in one place.
18
+ */
19
+ export function resolvePnpmInvocation(args) {
20
+ const pnpmHome = process.env.PNPM_HOME;
21
+ if (pnpmHome !== undefined) {
22
+ const command = join(pnpmHome, process.platform === "win32" ? "pnpm.CMD" : "pnpm");
23
+ if (existsSync(command)) {
24
+ return {
25
+ args,
26
+ command,
27
+ shell: process.platform === "win32",
28
+ };
29
+ }
30
+ }
31
+ const npmExecPath = process.env.npm_execpath;
32
+ if (npmExecPath !== undefined && npmExecPath.toLowerCase().includes("pnpm")) {
33
+ const extension = extname(npmExecPath).toLowerCase();
34
+ if (extension === ".cjs" || extension === ".js") {
35
+ return {
36
+ args: [npmExecPath, ...args],
37
+ command: process.execPath,
38
+ };
39
+ }
40
+ return {
41
+ args,
42
+ command: npmExecPath,
43
+ shell: process.platform === "win32",
44
+ };
45
+ }
46
+ return {
47
+ args,
48
+ command: "pnpm",
49
+ };
50
+ }
@@ -180,6 +180,9 @@ export interface SubagentCalledStreamEvent {
180
180
  sessionId: string;
181
181
  sequence: number;
182
182
  name: string;
183
+ remote?: {
184
+ url: string;
185
+ };
183
186
  toolName: string;
184
187
  turnId: string;
185
188
  workflowId: string;
@@ -579,6 +582,9 @@ export declare function createSubagentCalledEvent(input: {
579
582
  readonly sessionId: string;
580
583
  readonly sequence: number;
581
584
  readonly name: string;
585
+ readonly remote?: {
586
+ readonly url: string;
587
+ };
582
588
  readonly toolName: string;
583
589
  readonly turnId: string;
584
590
  readonly workflowId: string;
@@ -200,6 +200,7 @@ export function createSubagentCalledEvent(input) {
200
200
  sessionId: input.sessionId,
201
201
  sequence: input.sequence,
202
202
  name: input.name,
203
+ remote: input.remote,
203
204
  toolName: input.toolName,
204
205
  turnId: input.turnId,
205
206
  workflowId: input.workflowId,
@@ -45,6 +45,13 @@ export declare const ASH_MESSAGE_STREAM_ROUTE_PATTERN = "/ash/v1/session/:sessio
45
45
  * exactly what the IdP needs to do.
46
46
  */
47
47
  export declare const ASH_CONNECTION_CALLBACK_ROUTE_PATTERN = "/ash/v1/connections/:name/callback/:token";
48
+ /**
49
+ * Stable framework-owned route pattern for terminal session callbacks.
50
+ *
51
+ * The `:token` segment is an unguessable workflow hook capability. The route
52
+ * is unauthenticated by design and resumes the matching parked runtime action.
53
+ */
54
+ export declare const ASH_CALLBACK_ROUTE_PATTERN = "/ash/v1/callback/:token";
48
55
  /**
49
56
  * Creates the stable framework-owned message stream route path for one session.
50
57
  */
@@ -64,3 +71,7 @@ export declare function createAshContinueSessionRoutePath(sessionId: string): st
64
71
  * `resumeHook(token, payload)`.
65
72
  */
66
73
  export declare function createAshConnectionCallbackRoutePath(name: string, token: string): string;
74
+ /**
75
+ * Creates the stable framework-owned terminal callback route path.
76
+ */
77
+ export declare function createAshCallbackRoutePath(token: string): string;
@@ -45,6 +45,13 @@ export const ASH_MESSAGE_STREAM_ROUTE_PATTERN = `${ASH_ROUTE_PREFIX}/session/:se
45
45
  * exactly what the IdP needs to do.
46
46
  */
47
47
  export const ASH_CONNECTION_CALLBACK_ROUTE_PATTERN = `${ASH_ROUTE_PREFIX}/connections/:name/callback/:token`;
48
+ /**
49
+ * Stable framework-owned route pattern for terminal session callbacks.
50
+ *
51
+ * The `:token` segment is an unguessable workflow hook capability. The route
52
+ * is unauthenticated by design and resumes the matching parked runtime action.
53
+ */
54
+ export const ASH_CALLBACK_ROUTE_PATTERN = `${ASH_ROUTE_PREFIX}/callback/:token`;
48
55
  /**
49
56
  * Creates the stable framework-owned message stream route path for one session.
50
57
  */
@@ -70,3 +77,9 @@ export function createAshContinueSessionRoutePath(sessionId) {
70
77
  export function createAshConnectionCallbackRoutePath(name, token) {
71
78
  return `${ASH_ROUTE_PREFIX}/connections/${encodeURIComponent(name)}/callback/${encodeURIComponent(token)}`;
72
79
  }
80
+ /**
81
+ * Creates the stable framework-owned terminal callback route path.
82
+ */
83
+ export function createAshCallbackRoutePath(token) {
84
+ return `${ASH_ROUTE_PREFIX}/callback/${encodeURIComponent(token)}`;
85
+ }
@@ -1,4 +1,5 @@
1
1
  import {} from "ai";
2
+ import { parseSessionCallback } from "#channel/session-callback.js";
2
3
  import { ASH_MESSAGE_STREAM_CONTENT_TYPE, ASH_MESSAGE_STREAM_FORMAT, ASH_MESSAGE_STREAM_VERSION, ASH_SESSION_ID_HEADER, ASH_STREAM_FORMAT_HEADER, ASH_STREAM_VERSION_HEADER, } from "#protocol/message.js";
3
4
  import { isInputResponse } from "#runtime/input/types.js";
4
5
  import { createUnauthorizedResponse } from "#public/channels/auth.js";
@@ -33,7 +34,9 @@ export function ashChannel(input) {
33
34
  const token = `ash:${crypto.randomUUID()}`;
34
35
  const session = await send(createSendPayload(body), {
35
36
  auth: sessionAuth,
37
+ callback: body.callback,
36
38
  continuationToken: token,
39
+ mode: body.mode,
37
40
  });
38
41
  return Response.json({
39
42
  continuationToken: session.continuationToken,
@@ -143,10 +146,16 @@ function parseCreateBody(payload) {
143
146
  const modelContext = parseClientContextField(payload.clientContext);
144
147
  if (modelContext instanceof Response)
145
148
  return modelContext;
149
+ const callback = parseCallbackField(payload.callback);
150
+ if (callback instanceof Response)
151
+ return callback;
152
+ const mode = parseModeField(payload.mode);
153
+ if (mode instanceof Response)
154
+ return mode;
146
155
  if (message === undefined) {
147
156
  return Response.json({ error: "Missing or empty 'message' field.", ok: false }, { status: 400 });
148
157
  }
149
- return { message, modelContext };
158
+ return { callback, message, mode, modelContext };
150
159
  }
151
160
  function parseContinueBody(payload) {
152
161
  const continuationToken = typeof payload.continuationToken === "string" && payload.continuationToken.length > 0
@@ -178,6 +187,21 @@ function createSendPayload(body) {
178
187
  }
179
188
  return { message: body.message, modelContext: body.modelContext };
180
189
  }
190
+ function parseCallbackField(value) {
191
+ if (value === undefined)
192
+ return undefined;
193
+ const parsed = parseSessionCallback(value);
194
+ if (parsed.ok)
195
+ return parsed.callback;
196
+ return Response.json({ error: parsed.message, ok: false }, { status: 400 });
197
+ }
198
+ function parseModeField(value) {
199
+ if (value === undefined)
200
+ return undefined;
201
+ if (value === "conversation" || value === "task")
202
+ return value;
203
+ return Response.json({ error: "Expected 'mode' to be either 'conversation' or 'task'.", ok: false }, { status: 400 });
204
+ }
181
205
  function parseMessageField(value) {
182
206
  if (value === undefined)
183
207
  return undefined;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Minimal Discord REST API wrapper used by the Discord channel.
3
+ *
4
+ * The channel talks directly to Discord's JSON HTTP API instead of
5
+ * exposing a third-party SDK through Ash public surfaces.
6
+ */
7
+ import { type JsonObject } from "#shared/json.js";
8
+ import { resolveDiscordPublicKey, type DiscordPublicKey } from "#public/channels/discord/verify.js";
9
+ /** Discord application id, materialized directly or from an async secret provider. */
10
+ export type DiscordApplicationId = string | (() => string | Promise<string>);
11
+ /** Discord bot token, materialized directly or from an async secret provider. */
12
+ export type DiscordBotToken = string | (() => string | Promise<string>);
13
+ /** Fetch implementation override used by tests or non-standard runtimes. */
14
+ export type DiscordFetch = typeof fetch;
15
+ /** Credentials used by the native Discord channel. */
16
+ export interface DiscordCredentials {
17
+ readonly applicationId?: DiscordApplicationId;
18
+ readonly botToken?: DiscordBotToken;
19
+ readonly publicKey?: DiscordPublicKey;
20
+ }
21
+ /** Shared Discord API options. */
22
+ export interface DiscordApiOptions {
23
+ readonly apiBaseUrl?: string;
24
+ readonly credentials?: DiscordCredentials;
25
+ readonly fetch?: DiscordFetch;
26
+ }
27
+ /** Raw Discord API response body. */
28
+ export interface DiscordApiResponse {
29
+ readonly status: number;
30
+ readonly ok: boolean;
31
+ readonly body: unknown;
32
+ }
33
+ /** Minimal Discord message object returned by channel write operations. */
34
+ export interface DiscordPostedMessage {
35
+ /** Discord message id, when Discord returned one. */
36
+ readonly id: string;
37
+ /** Channel id associated with the message, when Discord returned one. */
38
+ readonly channelId?: string;
39
+ /** Discord's raw JSON response. */
40
+ readonly raw: unknown;
41
+ }
42
+ /** Allowed mentions payload that suppresses all generated pings. */
43
+ export declare const DISCORD_NO_MENTIONS: JsonObject;
44
+ /** Discord's documented message-content cap. */
45
+ export declare const DISCORD_MESSAGE_CONTENT_MAX_LENGTH = 2000;
46
+ /** Builds the channel-local continuation token (`<channelId>:<conversationId>`). */
47
+ export declare function discordContinuationToken(channelId: string, conversationId: string | undefined): string;
48
+ /** Resolves a Discord application id, falling back to `DISCORD_APPLICATION_ID`. */
49
+ export declare function resolveDiscordApplicationId(applicationId?: DiscordApplicationId): Promise<string>;
50
+ /** Resolves a Discord bot token, falling back to `DISCORD_BOT_TOKEN`. */
51
+ export declare function resolveDiscordBotToken(botToken?: DiscordBotToken): Promise<string>;
52
+ /** Resolves a Discord public key, falling back to `DISCORD_PUBLIC_KEY`. */
53
+ export { resolveDiscordPublicKey };
54
+ /**
55
+ * Low-level Discord JSON API call. Bot-token auth is included only when
56
+ * a token is supplied; interaction webhook endpoints intentionally run
57
+ * without bot auth.
58
+ */
59
+ export declare function callDiscordApi(input: {
60
+ readonly apiBaseUrl?: string;
61
+ readonly body?: JsonObject;
62
+ readonly botToken?: DiscordBotToken;
63
+ readonly fetch?: DiscordFetch;
64
+ readonly method?: "DELETE" | "GET" | "PATCH" | "POST" | "PUT";
65
+ readonly path: string;
66
+ }): Promise<DiscordApiResponse>;
67
+ /** Sends a bot-authenticated message to one Discord channel. */
68
+ export declare function sendDiscordChannelMessage(input: DiscordApiOptions & {
69
+ readonly body: DiscordMessageBody;
70
+ readonly channelId: string;
71
+ }): Promise<DiscordPostedMessage>;
72
+ /** Triggers Discord's short-lived channel typing indicator with bot auth. */
73
+ export declare function triggerDiscordTypingIndicator(input: DiscordApiOptions & {
74
+ readonly channelId: string;
75
+ }): Promise<void>;
76
+ /** Edits the original response for a deferred Discord interaction. */
77
+ export declare function editDiscordOriginalResponse(input: DiscordApiOptions & {
78
+ readonly body: DiscordMessageBody;
79
+ readonly interactionToken: string;
80
+ }): Promise<DiscordPostedMessage>;
81
+ /** Creates a Discord interaction followup message. */
82
+ export declare function createDiscordFollowupMessage(input: DiscordApiOptions & {
83
+ readonly body: DiscordMessageBody;
84
+ readonly interactionToken: string;
85
+ }): Promise<DiscordPostedMessage>;
86
+ /** JSON body supported by Discord message endpoints used by Ash. */
87
+ export interface DiscordMessageBody {
88
+ readonly allowed_mentions?: Readonly<Record<string, unknown>>;
89
+ readonly components?: readonly Readonly<Record<string, unknown>>[];
90
+ readonly content?: string;
91
+ readonly flags?: number;
92
+ readonly tts?: boolean;
93
+ }
94
+ /**
95
+ * Splits text into chunks Discord will accept as individual message
96
+ * contents. Empty strings produce one empty chunk so callers can still
97
+ * decide how to handle a no-content message.
98
+ */
99
+ export declare function splitDiscordMessageContent(content: string): readonly string[];
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Minimal Discord REST API wrapper used by the Discord channel.
3
+ *
4
+ * The channel talks directly to Discord's JSON HTTP API instead of
5
+ * exposing a third-party SDK through Ash public surfaces.
6
+ */
7
+ import { parseJsonObject } from "#shared/json.js";
8
+ import { isObject } from "#shared/guards.js";
9
+ import { resolveDiscordPublicKey } from "#public/channels/discord/verify.js";
10
+ /** Allowed mentions payload that suppresses all generated pings. */
11
+ export const DISCORD_NO_MENTIONS = { parse: [] };
12
+ /** Discord's documented message-content cap. */
13
+ export const DISCORD_MESSAGE_CONTENT_MAX_LENGTH = 2000;
14
+ /** Builds the channel-local continuation token (`<channelId>:<conversationId>`). */
15
+ export function discordContinuationToken(channelId, conversationId) {
16
+ return `${channelId}:${conversationId ?? ""}`;
17
+ }
18
+ /** Resolves a Discord application id, falling back to `DISCORD_APPLICATION_ID`. */
19
+ export async function resolveDiscordApplicationId(applicationId) {
20
+ const source = applicationId ?? process.env.DISCORD_APPLICATION_ID;
21
+ if (!source)
22
+ throw new Error("DISCORD_APPLICATION_ID is required.");
23
+ return typeof source === "function" ? await source() : source;
24
+ }
25
+ /** Resolves a Discord bot token, falling back to `DISCORD_BOT_TOKEN`. */
26
+ export async function resolveDiscordBotToken(botToken) {
27
+ const source = botToken ?? process.env.DISCORD_BOT_TOKEN;
28
+ if (!source)
29
+ throw new Error("DISCORD_BOT_TOKEN is required.");
30
+ return typeof source === "function" ? await source() : source;
31
+ }
32
+ /** Resolves a Discord public key, falling back to `DISCORD_PUBLIC_KEY`. */
33
+ export { resolveDiscordPublicKey };
34
+ /**
35
+ * Low-level Discord JSON API call. Bot-token auth is included only when
36
+ * a token is supplied; interaction webhook endpoints intentionally run
37
+ * without bot auth.
38
+ */
39
+ export async function callDiscordApi(input) {
40
+ const apiFetch = input.fetch ?? fetch;
41
+ const headers = new Headers();
42
+ headers.set("content-type", "application/json; charset=utf-8");
43
+ if (input.botToken !== undefined) {
44
+ const token = await resolveDiscordBotToken(input.botToken);
45
+ headers.set("authorization", `Bot ${token}`);
46
+ }
47
+ const init = {
48
+ headers,
49
+ method: input.method ?? "POST",
50
+ };
51
+ if (input.body !== undefined) {
52
+ init.body = JSON.stringify(parseJsonObject(input.body));
53
+ }
54
+ const response = await apiFetch(`${input.apiBaseUrl ?? "https://discord.com/api/v10"}${input.path}`, init);
55
+ return {
56
+ body: await parseResponseBody(response),
57
+ ok: response.ok,
58
+ status: response.status,
59
+ };
60
+ }
61
+ /** Sends a bot-authenticated message to one Discord channel. */
62
+ export async function sendDiscordChannelMessage(input) {
63
+ const response = await callDiscordApi({
64
+ apiBaseUrl: input.apiBaseUrl,
65
+ body: normalizeMessageBody(input.body),
66
+ botToken: input.credentials?.botToken,
67
+ fetch: input.fetch,
68
+ path: `/channels/${encodeURIComponent(input.channelId)}/messages`,
69
+ });
70
+ if (!response.ok) {
71
+ throw new Error(`Discord create message failed with HTTP ${response.status}.`);
72
+ }
73
+ return toPostedMessage(response.body);
74
+ }
75
+ /** Triggers Discord's short-lived channel typing indicator with bot auth. */
76
+ export async function triggerDiscordTypingIndicator(input) {
77
+ const response = await callDiscordApi({
78
+ apiBaseUrl: input.apiBaseUrl,
79
+ botToken: input.credentials?.botToken,
80
+ fetch: input.fetch,
81
+ path: `/channels/${encodeURIComponent(input.channelId)}/typing`,
82
+ });
83
+ if (!response.ok) {
84
+ throw new Error(`Discord typing indicator failed with HTTP ${response.status}.`);
85
+ }
86
+ }
87
+ /** Edits the original response for a deferred Discord interaction. */
88
+ export async function editDiscordOriginalResponse(input) {
89
+ const applicationId = await resolveDiscordApplicationId(input.credentials?.applicationId);
90
+ const response = await callDiscordApi({
91
+ apiBaseUrl: input.apiBaseUrl,
92
+ body: normalizeMessageBody(input.body),
93
+ fetch: input.fetch,
94
+ method: "PATCH",
95
+ path: `/webhooks/${encodeURIComponent(applicationId)}/${encodeURIComponent(input.interactionToken)}/messages/@original`,
96
+ });
97
+ if (!response.ok) {
98
+ throw new Error(`Discord edit original response failed with HTTP ${response.status}.`);
99
+ }
100
+ return toPostedMessage(response.body);
101
+ }
102
+ /** Creates a Discord interaction followup message. */
103
+ export async function createDiscordFollowupMessage(input) {
104
+ const applicationId = await resolveDiscordApplicationId(input.credentials?.applicationId);
105
+ const response = await callDiscordApi({
106
+ apiBaseUrl: input.apiBaseUrl,
107
+ body: normalizeMessageBody(input.body),
108
+ fetch: input.fetch,
109
+ path: `/webhooks/${encodeURIComponent(applicationId)}/${encodeURIComponent(input.interactionToken)}`,
110
+ });
111
+ if (!response.ok) {
112
+ throw new Error(`Discord followup message failed with HTTP ${response.status}.`);
113
+ }
114
+ return toPostedMessage(response.body);
115
+ }
116
+ /**
117
+ * Splits text into chunks Discord will accept as individual message
118
+ * contents. Empty strings produce one empty chunk so callers can still
119
+ * decide how to handle a no-content message.
120
+ */
121
+ export function splitDiscordMessageContent(content) {
122
+ if (content.length <= DISCORD_MESSAGE_CONTENT_MAX_LENGTH)
123
+ return [content];
124
+ const chunks = [];
125
+ let rest = content;
126
+ while (rest.length > DISCORD_MESSAGE_CONTENT_MAX_LENGTH) {
127
+ let cut = rest.lastIndexOf("\n", DISCORD_MESSAGE_CONTENT_MAX_LENGTH);
128
+ if (cut <= 0) {
129
+ cut = rest.lastIndexOf(" ", DISCORD_MESSAGE_CONTENT_MAX_LENGTH);
130
+ }
131
+ if (cut <= 0)
132
+ cut = DISCORD_MESSAGE_CONTENT_MAX_LENGTH;
133
+ chunks.push(rest.slice(0, cut).trimEnd());
134
+ rest = rest.slice(cut).trimStart();
135
+ }
136
+ chunks.push(rest);
137
+ return chunks;
138
+ }
139
+ function normalizeMessageBody(body) {
140
+ const normalized = { ...body };
141
+ if (normalized.allowed_mentions === undefined) {
142
+ normalized.allowed_mentions = DISCORD_NO_MENTIONS;
143
+ }
144
+ return parseJsonObject(normalized);
145
+ }
146
+ function toPostedMessage(body) {
147
+ const raw = parseMaybeObject(body);
148
+ return {
149
+ channelId: typeof raw.channel_id === "string" ? raw.channel_id : undefined,
150
+ id: typeof raw.id === "string" ? raw.id : "",
151
+ raw: body,
152
+ };
153
+ }
154
+ function parseMaybeObject(value) {
155
+ return isObject(value) ? value : {};
156
+ }
157
+ async function parseResponseBody(response) {
158
+ const text = await response.text();
159
+ if (!text)
160
+ return null;
161
+ try {
162
+ return JSON.parse(text);
163
+ }
164
+ catch {
165
+ return text;
166
+ }
167
+ }
@@ -0,0 +1,9 @@
1
+ import type { SessionAuthContext } from "#channel/types.js";
2
+ import type { DiscordCommandInteraction } from "#public/channels/discord/inbound.js";
3
+ import type { DiscordChannelEvents, DiscordCommandResult, DiscordContext } from "#public/channels/discord/discordChannel.js";
4
+ /** Default auth projection for Discord interaction actors. */
5
+ export declare function defaultDiscordAuth(interaction: DiscordCommandInteraction): SessionAuthContext;
6
+ /** Default command hook: dispatch with Discord user auth. */
7
+ export declare function defaultOnCommand(_ctx: DiscordContext, interaction: DiscordCommandInteraction): DiscordCommandResult;
8
+ /** Built-in Discord event handlers for typing, replies, HITL, and terminal errors. */
9
+ export declare const defaultEvents: DiscordChannelEvents;
@@ -0,0 +1,74 @@
1
+ import { extractErrorId, formatErrorHint } from "#internal/logging.js";
2
+ import { splitDiscordMessageContent } from "#public/channels/discord/api.js";
3
+ import { renderInputRequestComponents } from "#public/channels/discord/hitl.js";
4
+ /** Default auth projection for Discord interaction actors. */
5
+ export function defaultDiscordAuth(interaction) {
6
+ const attributes = {
7
+ channel_id: interaction.channelId,
8
+ interaction_id: interaction.id,
9
+ user_id: interaction.user.id,
10
+ username: interaction.user.username,
11
+ };
12
+ if (interaction.guildId !== undefined)
13
+ attributes.guild_id = interaction.guildId;
14
+ if (interaction.member?.nick !== undefined)
15
+ attributes.member_nick = interaction.member.nick;
16
+ const issuer = interaction.guildId ? `discord:${interaction.guildId}` : "discord";
17
+ const principalId = interaction.guildId
18
+ ? `discord:${interaction.guildId}:${interaction.user.id}`
19
+ : `discord:${interaction.user.id}`;
20
+ return {
21
+ attributes,
22
+ authenticator: "discord-interaction",
23
+ issuer,
24
+ principalId,
25
+ principalType: interaction.user.isBot ? "service" : "user",
26
+ };
27
+ }
28
+ /** Default command hook: dispatch with Discord user auth. */
29
+ export function defaultOnCommand(_ctx, interaction) {
30
+ return { auth: defaultDiscordAuth(interaction) };
31
+ }
32
+ /** Built-in Discord event handlers for typing, replies, HITL, and terminal errors. */
33
+ export const defaultEvents = {
34
+ async "turn.started"(_event, ctx) {
35
+ await ctx.discord.startTyping();
36
+ },
37
+ async "actions.requested"(_event, ctx) {
38
+ await ctx.discord.startTyping();
39
+ },
40
+ async "input.requested"(event, ctx) {
41
+ for (const request of event.requests) {
42
+ const content = splitDiscordMessageContent(request.prompt)[0] ?? request.prompt;
43
+ await ctx.discord.post({
44
+ components: renderInputRequestComponents(request),
45
+ content,
46
+ });
47
+ }
48
+ },
49
+ async "message.completed"(event, ctx) {
50
+ if (event.finishReason === "tool-calls" || !event.message)
51
+ return;
52
+ await ctx.discord.post(event.message);
53
+ },
54
+ async "session.failed"(event, ctx) {
55
+ const hint = formatErrorHint(event);
56
+ const errorId = extractErrorId(event.details);
57
+ await ctx.discord.post([
58
+ `This session could not recover from an error${hint}.`,
59
+ "",
60
+ "Start a new command to continue.",
61
+ ...(errorId ? ["", `Error id: ${errorId}`] : []),
62
+ ].join("\n"));
63
+ },
64
+ async "turn.failed"(event, ctx) {
65
+ const hint = formatErrorHint(event);
66
+ const errorId = extractErrorId(event.details);
67
+ await ctx.discord.post([
68
+ `I hit an error while handling your request${hint}.`,
69
+ "",
70
+ "Please try again, rephrase, or reach out if it keeps failing.",
71
+ ...(errorId ? ["", `Error id: ${errorId}`] : []),
72
+ ].join("\n"));
73
+ },
74
+ };