experimental-ash 0.7.0 → 0.7.2

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 (41) hide show
  1. package/dist/docs/external-agent-protocol.md +5 -5
  2. package/dist/docs/internals/message-runtime.md +4 -4
  3. package/dist/docs/public/auth-and-route-protection.md +10 -10
  4. package/dist/docs/public/channels/README.md +9 -6
  5. package/dist/docs/public/cli-build-and-debugging.md +1 -2
  6. package/dist/docs/public/getting-started.md +0 -11
  7. package/dist/docs/public/skills.md +2 -2
  8. package/dist/docs/public/typescript-api.md +1 -1
  9. package/dist/src/chunks/{dev-authored-source-watcher-HzOplr1S.js → dev-authored-source-watcher-D3ybKVO9.js} +1 -1
  10. package/dist/src/chunks/{host-Ca8xvEQ1.js → host-Ck0qkepf.js} +2 -2
  11. package/dist/src/chunks/{paths-BiY7uVwD.js → paths-BFX2EgQO.js} +21 -21
  12. package/dist/src/chunks/{prewarm-DiZ_sYLy.js → prewarm-DJtOdukm.js} +1 -1
  13. package/dist/src/cli/commands/info.js +1 -1
  14. package/dist/src/cli/run.d.ts +0 -5
  15. package/dist/src/cli/run.js +2 -2
  16. package/dist/src/evals/cli/eval.js +1 -1
  17. package/dist/src/internal/application/package.js +1 -1
  18. package/dist/src/public/channels/{http.d.ts → ash.d.ts} +2 -2
  19. package/dist/src/public/channels/{http.js → ash.js} +2 -2
  20. package/dist/src/public/channels/index.d.ts +1 -1
  21. package/dist/src/public/channels/slack/attachments.d.ts +35 -0
  22. package/dist/src/public/channels/slack/attachments.js +100 -0
  23. package/dist/src/public/channels/slack/hitl.d.ts +67 -0
  24. package/dist/src/public/channels/slack/hitl.js +101 -0
  25. package/dist/src/public/channels/slack/index.d.ts +1 -0
  26. package/dist/src/public/channels/slack/slackChannel.d.ts +13 -0
  27. package/dist/src/public/channels/slack/slackChannel.js +40 -47
  28. package/dist/src/public/definitions/defineChannel.d.ts +9 -0
  29. package/dist/src/public/definitions/defineChannel.js +10 -8
  30. package/dist/src/runtime/framework-channels/index.d.ts +1 -1
  31. package/dist/src/runtime/framework-channels/index.js +7 -7
  32. package/package.json +5 -5
  33. package/dist/src/cli/commands/init.d.ts +0 -13
  34. package/dist/src/cli/commands/init.js +0 -1
  35. package/dist/src/cli/templates/init-app/.vercelignore +0 -11
  36. package/dist/src/cli/templates/init-app/agent/agent.ts +0 -5
  37. package/dist/src/cli/templates/init-app/agent/instructions.md +0 -3
  38. package/dist/src/cli/templates/init-app/agent/tools/hello.ts +0 -12
  39. package/dist/src/cli/templates/init-app/gitignore +0 -8
  40. package/dist/src/cli/templates/init-app/package.json +0 -18
  41. package/dist/src/cli/templates/init-app/tsconfig.json +0 -16
@@ -1,5 +1,8 @@
1
- import { defineChannel, POST, } from "#public/definitions/defineChannel.js";
2
1
  import { createLogger } from "#internal/logging.js";
2
+ import { buildSlackTurnMessage, collectSlackFileParts, createSlackAttachmentResolver, } from "#public/channels/slack/attachments.js";
3
+ import { deriveHitlResponse, isHitlAction, renderInputRequestBlocks, } from "#public/channels/slack/hitl.js";
4
+ import { mergeUploadPolicy } from "#public/channels/upload-policy.js";
5
+ import { defineChannel, POST, } from "#public/definitions/defineChannel.js";
3
6
  const log = createLogger("slack.channel");
4
7
  function decodeThreadId(id) {
5
8
  const parts = id.replace(/^slack:/u, "").split(":");
@@ -31,30 +34,6 @@ function buildSlackApiHandle(thread, botToken, teamId) {
31
34
  },
32
35
  };
33
36
  }
34
- function renderInputRequestBlocks(requests) {
35
- const blocks = [];
36
- for (const request of requests) {
37
- blocks.push({ type: "section", text: { type: "mrkdwn", text: request.prompt } });
38
- if (request.options && request.options.length > 0) {
39
- blocks.push({
40
- type: "actions",
41
- elements: request.options.map((opt) => {
42
- const button = {
43
- type: "button",
44
- text: { type: "plain_text", text: opt.label },
45
- action_id: `ash_input_${request.requestId}_${opt.id}`,
46
- value: opt.id,
47
- };
48
- if (opt.style === "primary" || opt.style === "danger") {
49
- button.style = opt.style;
50
- }
51
- return button;
52
- }),
53
- });
54
- }
55
- }
56
- return blocks;
57
- }
58
37
  function parseBlockActionsPayload(body) {
59
38
  const actions = body.actions;
60
39
  if (!Array.isArray(actions))
@@ -76,6 +55,7 @@ function parseBlockActionsPayload(body) {
76
55
  actionId: String(a.action_id ?? ""),
77
56
  value: a.value != null ? String(a.value) : undefined,
78
57
  blockId: a.block_id != null ? String(a.block_id) : undefined,
58
+ selectedOptionValue: extractSelectedOptionValue(a),
79
59
  messageTs,
80
60
  })),
81
61
  channelId: channel,
@@ -83,6 +63,16 @@ function parseBlockActionsPayload(body) {
83
63
  teamId,
84
64
  };
85
65
  }
66
+ /**
67
+ * Reads `selected_option.value` off a `block_actions` action entry.
68
+ * Slack populates this on `radio_buttons` / `static_select` /
69
+ * `external_select` clicks. Multi-select widgets surface
70
+ * `selected_options[]` instead and are not consumed here.
71
+ */
72
+ function extractSelectedOptionValue(action) {
73
+ const selected = action.selected_option;
74
+ return typeof selected?.value === "string" ? selected.value : undefined;
75
+ }
86
76
  function rebuildSlackContext(state, botToken) {
87
77
  const chatModule = require("#compiled/chat/index.js");
88
78
  const thread = state.serializedThread
@@ -99,24 +89,29 @@ function rebuildSlackContext(state, botToken) {
99
89
  };
100
90
  }
101
91
  /**
102
- * Default input.requested handler — renders HITL option buttons
103
- * as Slack block_actions.
92
+ * Default `input.requested` handler — renders each pending HITL
93
+ * request as Slack `block_actions`. Buttons by default; radio for
94
+ * ≤6-option select requests; static_select for >6-option select
95
+ * requests. Override by declaring `events["input.requested"]`.
104
96
  */
105
97
  function defaultInputRequestedHandler() {
106
98
  return async (data, ctx) => {
107
- const blocks = renderInputRequestBlocks(data.requests);
108
- if (blocks.length > 0) {
109
- const decoded = decodeThreadId(ctx.thread.id ?? "");
110
- await ctx.slack.request("chat.postMessage", {
111
- channel: decoded.channelId,
112
- thread_ts: decoded.threadTs,
113
- blocks,
114
- text: data.requests.map((r) => r.prompt).join("\n"),
115
- });
116
- }
99
+ if (data.requests.length === 0)
100
+ return;
101
+ const decoded = decodeThreadId(ctx.thread.id ?? "");
102
+ await ctx.slack.request("chat.postMessage", {
103
+ channel: decoded.channelId,
104
+ thread_ts: decoded.threadTs,
105
+ blocks: data.requests.flatMap(renderInputRequestBlocks),
106
+ text: data.requests.map((r) => r.prompt).join("\n"),
107
+ });
117
108
  };
118
109
  }
119
110
  export function slackChannel(config = {}) {
111
+ const uploadPolicy = mergeUploadPolicy(config.uploadPolicy);
112
+ const slackAttachments = createSlackAttachmentResolver({
113
+ botToken: config.credentials?.botToken,
114
+ });
120
115
  let activeSend = null;
121
116
  let chatPromise = null;
122
117
  async function getChat() {
@@ -166,7 +161,9 @@ export function slackChannel(config = {}) {
166
161
  return;
167
162
  const decoded = decodeThreadId(thread.id ?? "");
168
163
  const continuationToken = `slack:${decoded.channelId}:${decoded.threadTs}`;
169
- await send(message.text, {
164
+ const fileParts = collectSlackFileParts(message, uploadPolicy);
165
+ const turnMessage = buildSlackTurnMessage(message.text, fileParts);
166
+ await send(turnMessage, {
170
167
  auth: runOpts.auth,
171
168
  continuationToken,
172
169
  state: {
@@ -183,10 +180,10 @@ export function slackChannel(config = {}) {
183
180
  });
184
181
  return promise;
185
182
  }
186
- // Build the effective event handlers, applying defaults
187
183
  const inputHandler = config.events?.["input.requested"] ?? defaultInputRequestedHandler();
188
184
  return defineChannel({
189
185
  state: { serializedThread: null, teamId: null },
186
+ attachments: slackAttachments,
190
187
  context(state) {
191
188
  return rebuildSlackContext(state, config.credentials?.botToken);
192
189
  },
@@ -206,13 +203,9 @@ export function slackChannel(config = {}) {
206
203
  const continuationToken = `slack:${interaction.channelId}:${interaction.threadTs}`;
207
204
  const inputResponses = [];
208
205
  for (const action of interaction.actions) {
209
- if (action.actionId.startsWith("ash_input_")) {
210
- const parts = action.actionId.split("_");
211
- const requestId = parts[2];
212
- const optionId = action.value;
213
- if (requestId && optionId) {
214
- inputResponses.push({ requestId, optionId });
215
- }
206
+ const response = deriveHitlResponse(action);
207
+ if (response !== null) {
208
+ inputResponses.push(response);
216
209
  }
217
210
  }
218
211
  if (inputResponses.length > 0) {
@@ -228,7 +221,7 @@ export function slackChannel(config = {}) {
228
221
  }));
229
222
  }
230
223
  if (config.onInteraction) {
231
- const customActions = interaction.actions.filter((a) => !a.actionId.startsWith("ash_input_"));
224
+ const customActions = interaction.actions.filter((a) => !isHitlAction(a.actionId));
232
225
  if (customActions.length > 0) {
233
226
  const chatModule = await import("#compiled/chat/index.js");
234
227
  const thread = new chatModule.ThreadImpl({
@@ -1,3 +1,4 @@
1
+ import type { AttachmentResolver } from "#channel/adapter.js";
1
2
  import { CHANNEL_SENTINEL } from "#channel/compiled-channel.js";
2
3
  import type { HandleMessageStreamEvent } from "#protocol/message.js";
3
4
  import type { RouteDefinition, SendFn } from "#channel/routes.js";
@@ -5,6 +6,7 @@ import type { Session, SessionHandle } from "#channel/session.js";
5
6
  export type { Session, SessionHandle } from "#channel/session.js";
6
7
  export { POST, GET, PUT, DELETE } from "#channel/routes.js";
7
8
  export type { RouteDefinition, RouteHandlerArgs, SendFn, SendOptions, SendPayload, GetSessionFn, } from "#channel/routes.js";
9
+ export type { AttachmentResolver } from "#channel/adapter.js";
8
10
  type EventData<T extends HandleMessageStreamEvent["type"]> = Extract<HandleMessageStreamEvent, {
9
11
  type: T;
10
12
  }> extends {
@@ -38,6 +40,13 @@ export interface ChannelConfig<TState = undefined, TCtx = void> {
38
40
  send: SendFn<TState>;
39
41
  }): Promise<Session>;
40
42
  readonly events?: ChannelEvents<TCtx>;
43
+ /**
44
+ * Resolver for `ash-attachment:` URLs this channel mints during
45
+ * inbound message handling. The framework's staging layer dispatches
46
+ * to `resolve(ref, ctx)` before each model call so the bytes can be
47
+ * written to the sandbox and rewritten as `ash-sandbox:` refs.
48
+ */
49
+ readonly attachments?: AttachmentResolver;
41
50
  }
42
51
  export interface Channel<TState = undefined> {
43
52
  readonly __kind: typeof CHANNEL_SENTINEL;
@@ -14,6 +14,7 @@ export function defineChannel(config) {
14
14
  function buildAdapter(config) {
15
15
  const hasState = config.state != null;
16
16
  const hasContext = config.context != null;
17
+ const hasAttachments = config.attachments !== undefined;
17
18
  const hasBehavior = hasState || hasContext;
18
19
  const eventHandlers = {};
19
20
  let hasEventHandlers = false;
@@ -41,19 +42,20 @@ function buildAdapter(config) {
41
42
  };
42
43
  }
43
44
  }
44
- // When the channel has no state, no context, and no event handlers,
45
- // return a bare pass-through adapter with framework kind "http".
46
- // This avoids the registry conflict when the adapter carries behavior
47
- // on a reserved framework kind.
48
- if (!hasBehavior && !hasEventHandlers) {
45
+ // When the channel carries no behavior at all, return a bare
46
+ // pass-through adapter with framework kind "http". This avoids the
47
+ // registry conflict when the adapter would otherwise reserve a
48
+ // framework kind with no behavior to register against it.
49
+ if (!hasBehavior && !hasEventHandlers && !hasAttachments) {
49
50
  return { kind: "http" };
50
51
  }
51
- // Channels with state, context, or event handlers use a dedicated
52
- // adapter kind so the registry can rehydrate behavior across step
53
- // boundaries without conflicting with framework-reserved kinds.
52
+ // Channels with any authored behavior use a dedicated adapter kind so
53
+ // the registry can rehydrate it across step boundaries without
54
+ // conflicting with framework-reserved kinds.
54
55
  return {
55
56
  kind: "defineChannel",
56
57
  state: hasState ? { ...config.state } : {},
58
+ attachments: config.attachments,
57
59
  createAdapterContext(base) {
58
60
  const state = base.state;
59
61
  const channelCtx = hasContext ? config.context(state) : {};
@@ -1,4 +1,4 @@
1
1
  import type { ResolvedChannelDefinition } from "#runtime/types.js";
2
- export declare const HTTP_CHANNEL_NAME = "default";
2
+ export declare const ASH_CHANNEL_NAME = "ash";
3
3
  export declare function getFrameworkChannelDefinitions(): readonly ResolvedChannelDefinition[];
4
4
  export declare function getAllFrameworkChannelNames(): ReadonlySet<string>;
@@ -1,14 +1,14 @@
1
1
  import { none, vercelOidc } from "#public/channels/auth.js";
2
- import { httpChannel } from "#public/channels/http.js";
2
+ import { ashChannel } from "#public/channels/ash.js";
3
3
  import { getConnectionCallbackChannelDefinitions, getConnectionCallbackChannelNames, } from "#runtime/connections/callback-route.js";
4
- export const HTTP_CHANNEL_NAME = "default";
4
+ export const ASH_CHANNEL_NAME = "ash";
5
5
  export function getFrameworkChannelDefinitions() {
6
- const auth = resolveFrameworkHttpAuth();
7
- const compiled = httpChannel({ auth });
6
+ const auth = resolveFrameworkAshAuth();
7
+ const compiled = ashChannel({ auth });
8
8
  const result = [];
9
9
  for (const route of compiled.routes) {
10
10
  result.push({
11
- name: HTTP_CHANNEL_NAME,
11
+ name: ASH_CHANNEL_NAME,
12
12
  method: route.method.toUpperCase(),
13
13
  urlPath: route.path,
14
14
  fetch: async (req, ctx) => route.handler(req, ctx),
@@ -23,9 +23,9 @@ export function getFrameworkChannelDefinitions() {
23
23
  return result;
24
24
  }
25
25
  export function getAllFrameworkChannelNames() {
26
- return new Set([HTTP_CHANNEL_NAME, ...getConnectionCallbackChannelNames()]);
26
+ return new Set([ASH_CHANNEL_NAME, ...getConnectionCallbackChannelNames()]);
27
27
  }
28
- function resolveFrameworkHttpAuth() {
28
+ function resolveFrameworkAshAuth() {
29
29
  if (process.env.VERCEL) {
30
30
  return vercelOidc();
31
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "experimental-ash",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "bin": {
5
5
  "ash": "./bin/ash.js",
6
6
  "experimental-ash": "./bin/ash.js"
@@ -120,10 +120,10 @@
120
120
  "import": "./dist/src/public/channels/index.js",
121
121
  "default": "./dist/src/public/channels/index.js"
122
122
  },
123
- "./channels/http": {
124
- "types": "./dist/src/public/channels/http.d.ts",
125
- "import": "./dist/src/public/channels/http.js",
126
- "default": "./dist/src/public/channels/http.js"
123
+ "./channels/ash": {
124
+ "types": "./dist/src/public/channels/ash.d.ts",
125
+ "import": "./dist/src/public/channels/ash.js",
126
+ "default": "./dist/src/public/channels/ash.js"
127
127
  },
128
128
  "./channels/auth": {
129
129
  "types": "./dist/src/public/channels/auth.d.ts",
@@ -1,13 +0,0 @@
1
- interface CliInitLogger {
2
- log(message: string): void;
3
- }
4
- interface InitializeApplicationInput {
5
- readonly logger: CliInitLogger;
6
- readonly parentDirectoryPath: string;
7
- readonly targetName: string;
8
- }
9
- /**
10
- * Scaffolds a new Ash application directory from the package-owned init template.
11
- */
12
- export declare function initializeApplication(input: InitializeApplicationInput): Promise<string>;
13
- export {};
@@ -1 +0,0 @@
1
- import{r as e,t}from"../../chunks/package-DmsQgn4v.js";import{createCliTheme as n,renderCliTaggedLine as r}from"../ui/output.js";import{basename as i,dirname as a,extname as o,join as s,resolve as c}from"node:path";import{access as l,cp as u,mkdir as d,readFile as f,readdir as p,rm as m,writeFile as h}from"node:fs/promises";const g=new Set([`.json`,`.md`,`.ts`]),_=new Set([`.gitignore`,`.vercelignore`,`gitignore`]);function v(e){return _.has(e)||g.has(o(e))}async function y(e){try{return await l(e),!0}catch{return!1}}function b(){return e(`src/cli/templates/init-app`)}async function x(e,t){let n=await p(e,{withFileTypes:!0});await Promise.all(n.map(async n=>{let r=s(e,n.name);if(n.isDirectory()){await x(r,t);return}if(!n.isFile()||!v(n.name))return;let i=await f(r,`utf8`),a=s(e,n.name===`gitignore`?`.gitignore`:n.name);for(let[e,n]of Object.entries(t))i=i.replaceAll(e,n);await h(a,i,`utf8`),n.name===`gitignore`&&await m(r)}))}async function S(e){let o=c(e.parentDirectoryPath,e.targetName);if(await y(o))throw Error(`Cannot initialize Ash app because "${o}" already exists.`);let s=i(o);if(s.trim().length===0)throw Error(`Cannot initialize Ash app because "${e.targetName}" is not a valid name.`);let l=t();await d(a(o),{recursive:!0}),await u(b(),o,{recursive:!0}),await x(o,{__ASH_INIT_APP_NAME__:s,__ASH_INIT_PACKAGE_VERSION__:`^${l.version}`});let f=n();return e.logger.log(r(f,{message:`created ${o}`,tag:`init`,tone:`success`})),e.logger.log(r(f,{message:`next: cd ${o}\npnpm install\npnpm dev`,tag:`init`,tone:`info`})),o}export{S as initializeApplication};
@@ -1,11 +0,0 @@
1
- .output
2
- .swc
3
- .ash
4
- .turbo
5
- .agents
6
- .codex
7
- .github
8
- .workflow-data
9
- dist
10
- research
11
- node_modules
@@ -1,5 +0,0 @@
1
- import { defineAgent } from "experimental-ash";
2
-
3
- export default defineAgent({
4
- model: "anthropic/claude-opus-4.7",
5
- });
@@ -1,3 +0,0 @@
1
- # Identity
2
-
3
- You are a helpful assistant.
@@ -1,12 +0,0 @@
1
- import { defineTool } from "experimental-ash/tools";
2
- import z from "zod";
3
-
4
- export default defineTool({
5
- description: "Say hello",
6
- inputSchema: z.object({
7
- greeting: z.string(),
8
- }),
9
- async execute(input) {
10
- return `Hello, ${input.greeting}!`;
11
- },
12
- });
@@ -1,8 +0,0 @@
1
- .ash
2
- .swc
3
- .output
4
- .vercel
5
- .env*
6
- .workflow-data
7
- .DS_Store
8
- node_modules
@@ -1,18 +0,0 @@
1
- {
2
- "name": "__ASH_INIT_APP_NAME__",
3
- "version": "0.0.0",
4
- "type": "module",
5
- "scripts": {
6
- "build": "ash build",
7
- "dev": "ash dev",
8
- "typecheck": "tsgo"
9
- },
10
- "dependencies": {
11
- "ai": "7.0.0-canary.131",
12
- "experimental-ash": "__ASH_INIT_PACKAGE_VERSION__",
13
- "zod": "^4.3.6"
14
- },
15
- "devDependencies": {
16
- "@typescript/native-preview": "7.0.0-dev.20260320.1"
17
- }
18
- }
@@ -1,16 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "outDir": "dist",
7
- "rootDir": ".",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "declaration": true,
13
- "noEmit": true
14
- },
15
- "include": ["agent/**/*.ts", "evals/**/*.ts", ".ash/**/*.d.ts"]
16
- }