experimental-ash 0.22.2 → 0.23.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 (117) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/docs/public/faqs.md +67 -0
  3. package/dist/docs/public/meta.json +1 -0
  4. package/dist/docs/public/schedules.md +11 -0
  5. package/dist/docs/public/session-context.md +46 -89
  6. package/dist/docs/public/skills.md +13 -0
  7. package/dist/docs/public/subagents.md +12 -6
  8. package/dist/docs/public/tools.md +9 -13
  9. package/dist/docs/public/typescript-api.md +2 -2
  10. package/dist/src/chunks/{client-CKsU8Li3.js → client-nshDsWNF.js} +1 -1
  11. package/dist/src/chunks/{dev-authored-source-watcher-BLzYWh05.js → dev-authored-source-watcher-d_35Mp8T.js} +1 -1
  12. package/dist/src/chunks/{host-DREC8e8Z.js → host-tji7W0Nn.js} +2 -2
  13. package/dist/src/chunks/paths-YoCQlavu.js +89 -0
  14. package/dist/src/chunks/{prewarm-hz8p2jlZ.js → prewarm-6duWvvb5.js} +1 -1
  15. package/dist/src/cli/commands/info.js +1 -1
  16. package/dist/src/cli/dev/repl.js +2 -2
  17. package/dist/src/cli/run.js +1 -1
  18. package/dist/src/client/session.js +8 -0
  19. package/dist/src/client/types.d.ts +12 -1
  20. package/dist/src/compiled/.vendor-stamp.json +1 -1
  21. package/dist/src/compiled/@workflow/core/_ms.d.ts +4 -0
  22. package/dist/src/compiled/@workflow/core/_workflow-serde.d.ts +5 -0
  23. package/dist/src/compiled/@workflow/core/_workflow-utils.d.ts +8 -0
  24. package/dist/src/compiled/@workflow/core/_workflow-world.d.ts +59 -0
  25. package/dist/src/compiled/@workflow/core/capabilities.d.ts +45 -0
  26. package/dist/src/compiled/@workflow/core/capture-stack.d.ts +16 -0
  27. package/dist/src/compiled/@workflow/core/class-serialization.d.ts +31 -0
  28. package/dist/src/compiled/@workflow/core/classify-error.d.ts +19 -0
  29. package/dist/src/compiled/@workflow/core/context-errors.d.ts +27 -0
  30. package/dist/src/compiled/@workflow/core/context-violation-error.d.ts +97 -0
  31. package/dist/src/compiled/@workflow/core/create-hook.d.ts +179 -0
  32. package/dist/src/compiled/@workflow/core/define-hook.d.ts +68 -0
  33. package/dist/src/compiled/@workflow/core/describe-error.d.ts +70 -0
  34. package/dist/src/compiled/@workflow/core/encryption.d.ts +45 -0
  35. package/dist/src/compiled/@workflow/core/events-consumer.d.ts +64 -0
  36. package/dist/src/compiled/@workflow/core/flushable-stream.d.ts +82 -0
  37. package/dist/src/compiled/@workflow/core/global.d.ts +48 -0
  38. package/dist/src/compiled/@workflow/core/index.d.ts +19 -38
  39. package/dist/src/compiled/@workflow/core/log-format.d.ts +25 -0
  40. package/dist/src/compiled/@workflow/core/logger.d.ts +29 -0
  41. package/dist/src/compiled/@workflow/core/private.d.ts +59 -10
  42. package/dist/src/compiled/@workflow/core/runtime/constants.d.ts +4 -0
  43. package/dist/src/compiled/@workflow/core/runtime/get-port-lazy.d.ts +10 -0
  44. package/dist/src/compiled/@workflow/core/runtime/get-world-lazy.d.ts +32 -0
  45. package/dist/src/compiled/@workflow/core/runtime/helpers.d.ts +97 -0
  46. package/dist/src/compiled/@workflow/core/runtime/resume-hook.d.ts +77 -0
  47. package/dist/src/compiled/@workflow/core/runtime/run.d.ts +134 -0
  48. package/dist/src/compiled/@workflow/core/runtime/runs.d.ts +50 -0
  49. package/dist/src/compiled/@workflow/core/runtime/start.d.ts +59 -0
  50. package/dist/src/compiled/@workflow/core/runtime/step-executor.d.ts +40 -0
  51. package/dist/src/compiled/@workflow/core/runtime/step-handler.d.ts +2 -0
  52. package/dist/src/compiled/@workflow/core/runtime/suspension-handler.d.ts +42 -0
  53. package/dist/src/compiled/@workflow/core/runtime/world-init.d.ts +75 -0
  54. package/dist/src/compiled/@workflow/core/runtime/world.d.ts +32 -0
  55. package/dist/src/compiled/@workflow/core/runtime.d.ts +22 -67
  56. package/dist/src/compiled/@workflow/core/schemas.d.ts +15 -0
  57. package/dist/src/compiled/@workflow/core/serialization/client.d.ts +17 -0
  58. package/dist/src/compiled/@workflow/core/serialization/codec-devalue.d.ts +14 -0
  59. package/dist/src/compiled/@workflow/core/serialization/codec.d.ts +90 -0
  60. package/dist/src/compiled/@workflow/core/serialization/encryption.d.ts +32 -0
  61. package/dist/src/compiled/@workflow/core/serialization/errors.d.ts +21 -0
  62. package/dist/src/compiled/@workflow/core/serialization/format.d.ts +60 -0
  63. package/dist/src/compiled/@workflow/core/serialization/index.d.ts +18 -0
  64. package/dist/src/compiled/@workflow/core/serialization/reducers/class.d.ts +11 -0
  65. package/dist/src/compiled/@workflow/core/serialization/reducers/common.d.ts +16 -0
  66. package/dist/src/compiled/@workflow/core/serialization/reducers/step-function.d.ts +35 -0
  67. package/dist/src/compiled/@workflow/core/serialization/step.d.ts +17 -0
  68. package/dist/src/compiled/@workflow/core/serialization/types.d.ts +201 -0
  69. package/dist/src/compiled/@workflow/core/serialization/workflow.d.ts +29 -0
  70. package/dist/src/compiled/@workflow/core/serialization-format.d.ts +171 -0
  71. package/dist/src/compiled/@workflow/core/serialization.d.ts +329 -0
  72. package/dist/src/compiled/@workflow/core/sleep.d.ts +33 -0
  73. package/dist/src/compiled/@workflow/core/source-map.d.ts +10 -0
  74. package/dist/src/compiled/@workflow/core/step/context-storage.d.ts +13 -0
  75. package/dist/src/compiled/@workflow/core/step/get-closure-vars.d.ts +9 -0
  76. package/dist/src/compiled/@workflow/core/step/get-step-metadata.d.ts +42 -0
  77. package/dist/src/compiled/@workflow/core/step/get-workflow-metadata.d.ts +7 -0
  78. package/dist/src/compiled/@workflow/core/step/writable-stream.d.ts +22 -0
  79. package/dist/src/compiled/@workflow/core/step.d.ts +4 -0
  80. package/dist/src/compiled/@workflow/core/symbols.d.ts +20 -0
  81. package/dist/src/compiled/@workflow/core/telemetry/semantic-conventions.d.ts +283 -0
  82. package/dist/src/compiled/@workflow/core/telemetry.d.ts +53 -0
  83. package/dist/src/compiled/@workflow/core/types.d.ts +14 -0
  84. package/dist/src/compiled/@workflow/core/util.d.ts +40 -0
  85. package/dist/src/compiled/@workflow/core/version.d.ts +2 -0
  86. package/dist/src/compiled/@workflow/core/vm/index.d.ts +17 -0
  87. package/dist/src/compiled/@workflow/core/vm/uint8array-base64.d.ts +21 -0
  88. package/dist/src/compiled/@workflow/core/vm/uuid.d.ts +10 -0
  89. package/dist/src/compiled/@workflow/core/workflow/abort-controller.d.ts +65 -0
  90. package/dist/src/compiled/@workflow/core/workflow/create-hook.d.ts +7 -0
  91. package/dist/src/compiled/@workflow/core/workflow/define-hook.d.ts +10 -0
  92. package/dist/src/compiled/@workflow/core/workflow/get-workflow-metadata.d.ts +32 -0
  93. package/dist/src/compiled/@workflow/core/workflow/hook.d.ts +4 -0
  94. package/dist/src/compiled/@workflow/core/workflow/index.d.ts +11 -0
  95. package/dist/src/compiled/@workflow/core/workflow/sleep.d.ts +4 -0
  96. package/dist/src/compiled/@workflow/core/workflow/world-init-stub.d.ts +15 -0
  97. package/dist/src/compiled/@workflow/core/workflow/writable-stream.d.ts +3 -0
  98. package/dist/src/compiled/@workflow/core/workflow.d.ts +1 -38
  99. package/dist/src/evals/cli/eval.js +1 -1
  100. package/dist/src/internal/application/package.js +1 -1
  101. package/dist/src/protocol/message.d.ts +6 -1
  102. package/dist/src/public/channels/ash.js +50 -3
  103. package/dist/src/public/context/index.d.ts +4 -7
  104. package/dist/src/public/context/index.js +4 -5
  105. package/dist/src/public/definitions/state.d.ts +33 -0
  106. package/dist/src/public/definitions/state.js +34 -0
  107. package/dist/src/public/next/index.d.ts +7 -0
  108. package/dist/src/public/next/index.js +2 -0
  109. package/dist/src/public/next/vercel-json.d.ts +1 -0
  110. package/dist/src/public/next/vercel-json.js +1 -0
  111. package/dist/src/react/index.d.ts +1 -1
  112. package/dist/src/react/use-ash-agent.d.ts +8 -0
  113. package/dist/src/react/use-ash-agent.js +26 -4
  114. package/dist/src/services/dev-client.d.ts +4 -1
  115. package/dist/src/services/dev-client.js +1 -0
  116. package/package.json +1 -1
  117. package/dist/src/chunks/paths-C6sp4T2U.js +0 -88
@@ -1 +1 @@
1
- import{n as e}from"../../chunks/paths-C6sp4T2U.js";import{loadDevelopmentEnvironmentFiles as t}from"../../cli/dev/environment.js";import{a as n,n as r,t as i}from"../../chunks/client-CKsU8Li3.js";import{n as a}from"../../chunks/host-DREC8e8Z.js";import{discoverAndImportSuites as o,discoverSuiteFiles as s,importSuiteFile as c}from"../runner/discover.js";import{executeSuite as l}from"../runner/execute-suite.js";import{ConsoleReporter as u}from"../runner/reporters/console.js";var d=n();function f(e,t){e.command(`eval`).description(`Run eval suites against an Ash agent.`).option(`--suite <id...>`,`Suite IDs to run (repeatable)`).option(`--all`,`Run all discovered suites`).option(`--url <url>`,`Remote agent URL (skip local host startup)`).option(`--timeout <ms>`,`Per-case timeout in milliseconds`).option(`--max-concurrency <n>`,`Max concurrent case executions per suite`).option(`--json`,`Output results as JSON`).option(`--list-suites`,`List discovered suites and exit`).option(`--skip-report`,`Skip suite-defined reporters (e.g. Braintrust)`).action(async e=>{await p(e,t)})}async function p(n,r){let i=e();if(t(i),n.listSuites){await y(i,r);return}let s=n.suite,c=await o(i,s);if(c.length===0){s&&s.length>0?r.error(`No suites found matching: ${s.join(`, `)}`):r.error(`No eval suites found. Create suite files under evals/ with the *.eval.ts extension.`),process.exitCode=1;return}let u,d;n.url?d={kind:`remote`,url:n.url}:(u=await a(i,{host:`127.0.0.1`,port:0}),d={kind:`local`,url:u.url});let f=m(d);try{let e=[];for(let t of c){let r=_(t,n),a=v(r,{json:n.json===!0,skipReport:n.skipReport===!0}),o=await l({suite:r,target:d,reporters:a,appRoot:i,client:f});e.push(o)}n.json&&r.log(JSON.stringify(e,null,2)),e.some(e=>e.errored>0)&&(process.exitCode=1)}finally{u&&await u.close()}process.exit(process.exitCode??0)}function m(e){if(e.kind===`local`)return new i({host:e.url});let t={},n=process.env.VERCEL_AUTOMATION_BYPASS_SECRET?.trim();return n&&(t[r]=n),new i({auth:h(),headers:Object.keys(t).length>0?t:void 0,host:e.url})}function h(){let e=process.env.ASH_EVAL_AUTH_TOKEN?.trim();return e?{bearer:e}:{bearer:g}}async function g(){try{let e=(await(0,d.getVercelOidcToken)()).trim();if(e.length>0)return e}catch{}return process.env.VERCEL_OIDC_TOKEN?.trim()??``}function _(e,t){let n=t.maxConcurrency?Number.parseInt(t.maxConcurrency,10):void 0,r=t.timeout?Number.parseInt(t.timeout,10):void 0;if(n===void 0&&r===void 0)return e;let i={...e};return n!==void 0&&(i.maxConcurrency=n),r!==void 0&&(i.timeoutMs=r),i}function v(e,t){let n=t.json?[]:[new u];return!t.skipReport&&e.reporters&&n.push(...e.reporters),n}async function y(e,t){let n=await s(e);if(n.length===0){t.log(`No eval suites found.`);return}t.log(`Found ${n.length} eval suite file(s):\n`);for(let r of n){let n=await c(e,r);t.log(` ${n.id}${n.description?` - ${n.description}`:``}`)}}export{f as registerEvalCommand,p as runEvalCommand};
1
+ import{n as e}from"../../chunks/paths-YoCQlavu.js";import{loadDevelopmentEnvironmentFiles as t}from"../../cli/dev/environment.js";import{a as n,n as r,t as i}from"../../chunks/client-nshDsWNF.js";import{n as a}from"../../chunks/host-tji7W0Nn.js";import{discoverAndImportSuites as o,discoverSuiteFiles as s,importSuiteFile as c}from"../runner/discover.js";import{executeSuite as l}from"../runner/execute-suite.js";import{ConsoleReporter as u}from"../runner/reporters/console.js";var d=n();function f(e,t){e.command(`eval`).description(`Run eval suites against an Ash agent.`).option(`--suite <id...>`,`Suite IDs to run (repeatable)`).option(`--all`,`Run all discovered suites`).option(`--url <url>`,`Remote agent URL (skip local host startup)`).option(`--timeout <ms>`,`Per-case timeout in milliseconds`).option(`--max-concurrency <n>`,`Max concurrent case executions per suite`).option(`--json`,`Output results as JSON`).option(`--list-suites`,`List discovered suites and exit`).option(`--skip-report`,`Skip suite-defined reporters (e.g. Braintrust)`).action(async e=>{await p(e,t)})}async function p(n,r){let i=e();if(t(i),n.listSuites){await y(i,r);return}let s=n.suite,c=await o(i,s);if(c.length===0){s&&s.length>0?r.error(`No suites found matching: ${s.join(`, `)}`):r.error(`No eval suites found. Create suite files under evals/ with the *.eval.ts extension.`),process.exitCode=1;return}let u,d;n.url?d={kind:`remote`,url:n.url}:(u=await a(i,{host:`127.0.0.1`,port:0}),d={kind:`local`,url:u.url});let f=m(d);try{let e=[];for(let t of c){let r=_(t,n),a=v(r,{json:n.json===!0,skipReport:n.skipReport===!0}),o=await l({suite:r,target:d,reporters:a,appRoot:i,client:f});e.push(o)}n.json&&r.log(JSON.stringify(e,null,2)),e.some(e=>e.errored>0)&&(process.exitCode=1)}finally{u&&await u.close()}process.exit(process.exitCode??0)}function m(e){if(e.kind===`local`)return new i({host:e.url});let t={},n=process.env.VERCEL_AUTOMATION_BYPASS_SECRET?.trim();return n&&(t[r]=n),new i({auth:h(),headers:Object.keys(t).length>0?t:void 0,host:e.url})}function h(){let e=process.env.ASH_EVAL_AUTH_TOKEN?.trim();return e?{bearer:e}:{bearer:g}}async function g(){try{let e=(await(0,d.getVercelOidcToken)()).trim();if(e.length>0)return e}catch{}return process.env.VERCEL_OIDC_TOKEN?.trim()??``}function _(e,t){let n=t.maxConcurrency?Number.parseInt(t.maxConcurrency,10):void 0,r=t.timeout?Number.parseInt(t.timeout,10):void 0;if(n===void 0&&r===void 0)return e;let i={...e};return n!==void 0&&(i.maxConcurrency=n),r!==void 0&&(i.timeoutMs=r),i}function v(e,t){let n=t.json?[]:[new u];return!t.skipReport&&e.reporters&&n.push(...e.reporters),n}async function y(e,t){let n=await s(e);if(n.length===0){t.log(`No eval suites found.`);return}t.log(`Found ${n.length} eval suite file(s):\n`);for(let r of n){let n=await c(e,r);t.log(` ${n.id}${n.description?` - ${n.description}`:``}`)}}export{f as registerEvalCommand,p as runEvalCommand};
@@ -6,7 +6,7 @@ import { ASH_PACKAGE_NAME } from "#package-name.js";
6
6
  let cachedPackageInfo;
7
7
  // The package build stamps the published version into `dist` so bundled
8
8
  // deployments can still report package metadata without resolving package.json.
9
- const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.22.2";
9
+ const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.23.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",
@@ -76,20 +76,25 @@ export interface RuntimeIdentity {
76
76
  * `message` is either a plain text string or an AI SDK `UserContent`
77
77
  * array (mixing `text`, `image`, and `file` parts). Clients pass
78
78
  * multimodal attachments with the same shape AI SDK's `useChat`
79
- * `sendMessage({ files })` produces.
79
+ * `sendMessage({ files })` produces. `clientContext` is one-turn
80
+ * client/page context; the channel converts it into internal model context.
80
81
  */
81
82
  export type HandleMessageRequestBody = {
82
83
  readonly message: string | UserContent;
84
+ readonly clientContext?: string | readonly string[] | JsonObject;
83
85
  } | {
84
86
  readonly continuationToken: string;
85
87
  readonly message: string | UserContent;
88
+ readonly clientContext?: string | readonly string[] | JsonObject;
86
89
  } | {
87
90
  readonly continuationToken: string;
88
91
  readonly inputResponses: readonly InputResponse[];
92
+ readonly clientContext?: string | readonly string[] | JsonObject;
89
93
  } | {
90
94
  readonly continuationToken: string;
91
95
  readonly inputResponses: readonly InputResponse[];
92
96
  readonly message: string | UserContent;
97
+ readonly clientContext?: string | readonly string[] | JsonObject;
93
98
  };
94
99
  /**
95
100
  * JSON response returned when the message route accepts a start or resume
@@ -1,8 +1,10 @@
1
+ import {} from "ai";
1
2
  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";
2
3
  import { isInputResponse } from "#runtime/input/types.js";
3
4
  import { createUnauthorizedResponse } from "#public/channels/auth.js";
4
5
  import { collectUploadPolicyViolations, formatUploadPolicyViolation, mergeUploadPolicy, } from "#public/channels/upload-policy.js";
5
6
  import { defineChannel, POST, GET } from "#public/definitions/defineChannel.js";
7
+ import { parseJsonObject } from "#shared/json.js";
6
8
  export function ashChannel(input) {
7
9
  const uploadPolicy = mergeUploadPolicy(input.uploadPolicy);
8
10
  return defineChannel({
@@ -29,7 +31,7 @@ export function ashChannel(input) {
29
31
  if (policyRejection !== null)
30
32
  return policyRejection;
31
33
  const token = `ash:${crypto.randomUUID()}`;
32
- const session = await send(body.message, {
34
+ const session = await send(createSendPayload(body), {
33
35
  auth: sessionAuth,
34
36
  continuationToken: token,
35
37
  });
@@ -79,6 +81,7 @@ export function ashChannel(input) {
79
81
  const session = await send({
80
82
  inputResponses: body.inputResponses,
81
83
  message: body.message,
84
+ modelContext: body.modelContext,
82
85
  }, {
83
86
  auth: sessionAuth,
84
87
  continuationToken: body.continuationToken,
@@ -137,10 +140,13 @@ function parseCreateBody(payload) {
137
140
  const message = parseMessageField(payload.message);
138
141
  if (message instanceof Response)
139
142
  return message;
143
+ const modelContext = parseClientContextField(payload.clientContext);
144
+ if (modelContext instanceof Response)
145
+ return modelContext;
140
146
  if (message === undefined) {
141
147
  return Response.json({ error: "Missing or empty 'message' field.", ok: false }, { status: 400 });
142
148
  }
143
- return { message };
149
+ return { message, modelContext };
144
150
  }
145
151
  function parseContinueBody(payload) {
146
152
  const continuationToken = typeof payload.continuationToken === "string" && payload.continuationToken.length > 0
@@ -155,13 +161,22 @@ function parseContinueBody(payload) {
155
161
  const inputResponses = parseInputResponses(payload.inputResponses);
156
162
  if (inputResponses instanceof Response)
157
163
  return inputResponses;
164
+ const modelContext = parseClientContextField(payload.clientContext);
165
+ if (modelContext instanceof Response)
166
+ return modelContext;
158
167
  if (message === undefined && inputResponses === undefined) {
159
168
  return Response.json({
160
169
  error: "Expected a non-empty 'message', a non-empty 'inputResponses' array, or both.",
161
170
  ok: false,
162
171
  }, { status: 400 });
163
172
  }
164
- return { message, continuationToken, inputResponses };
173
+ return { message, continuationToken, inputResponses, modelContext };
174
+ }
175
+ function createSendPayload(body) {
176
+ if (body.modelContext === undefined) {
177
+ return body.message;
178
+ }
179
+ return { message: body.message, modelContext: body.modelContext };
165
180
  }
166
181
  function parseMessageField(value) {
167
182
  if (value === undefined)
@@ -255,6 +270,38 @@ function parseInputResponses(value) {
255
270
  }
256
271
  return inputResponses;
257
272
  }
273
+ const CLIENT_CONTEXT_PREFIX = "Ephemeral client context:\n";
274
+ function parseClientContextField(value) {
275
+ if (value === undefined)
276
+ return undefined;
277
+ if (typeof value === "string") {
278
+ return value.length > 0 ? [toClientContextMessage(value)] : undefined;
279
+ }
280
+ if (Array.isArray(value)) {
281
+ if (value.length === 0)
282
+ return undefined;
283
+ if (!value.every((entry) => typeof entry === "string" && entry.length > 0)) {
284
+ return Response.json({ error: "Expected 'clientContext' array entries to be non-empty strings.", ok: false }, { status: 400 });
285
+ }
286
+ return value.map((entry) => toClientContextMessage(entry));
287
+ }
288
+ if (value === null || typeof value !== "object") {
289
+ return Response.json({
290
+ error: "Expected 'clientContext' to be a string, string array, or JSON object.",
291
+ ok: false,
292
+ }, { status: 400 });
293
+ }
294
+ try {
295
+ const json = parseJsonObject(value);
296
+ return [toClientContextMessage(JSON.stringify(json))];
297
+ }
298
+ catch {
299
+ return Response.json({ error: "Expected 'clientContext' to be a JSON-serializable object.", ok: false }, { status: 400 });
300
+ }
301
+ }
302
+ function toClientContextMessage(content) {
303
+ return { role: "user", content: `${CLIENT_CONTEXT_PREFIX}${content}` };
304
+ }
258
305
  function parseStartIndex(request) {
259
306
  const raw = new URL(request.url).searchParams.get("startIndex");
260
307
  if (raw === null)
@@ -2,15 +2,12 @@
2
2
  * Runtime context helpers for authored code.
3
3
  *
4
4
  * These APIs work only inside active authored runtime execution such as
5
- * tools and other Ash-invoked callbacks. They read from a single
6
- * `AshContext` container bound via `AsyncLocalStorage`.
5
+ * tools and other Ash-invoked callbacks.
7
6
  *
8
7
  * @example
9
8
  * ```ts
10
- * import { ContextKey, getContext, setContext } from "experimental-ash/context";
9
+ * import { defineState, getSession } from "experimental-ash/context";
11
10
  * ```
12
11
  */
13
- export { ensureContext, getContext, getSession, hasContext, requireContext, type Session, type SessionAuth, type SessionAuthContext, type SessionParent, type SessionTurn, setContext, } from "#context/accessors.js";
14
- export type { AshContext } from "#context/container.js";
15
- export { type ContextAccessor, ContextKey, type ContextKeyOptions } from "#context/key.js";
16
- export type { ContextProvider, ContextReader } from "#context/provider.js";
12
+ export { getSession, type Session, type SessionAuth, type SessionAuthContext, type SessionParent, type SessionTurn, } from "#context/accessors.js";
13
+ export { defineState, type StateHandle } from "#public/definitions/state.js";
@@ -2,13 +2,12 @@
2
2
  * Runtime context helpers for authored code.
3
3
  *
4
4
  * These APIs work only inside active authored runtime execution such as
5
- * tools and other Ash-invoked callbacks. They read from a single
6
- * `AshContext` container bound via `AsyncLocalStorage`.
5
+ * tools and other Ash-invoked callbacks.
7
6
  *
8
7
  * @example
9
8
  * ```ts
10
- * import { ContextKey, getContext, setContext } from "experimental-ash/context";
9
+ * import { defineState, getSession } from "experimental-ash/context";
11
10
  * ```
12
11
  */
13
- export { ensureContext, getContext, getSession, hasContext, requireContext, setContext, } from "#context/accessors.js";
14
- export { ContextKey } from "#context/key.js";
12
+ export { getSession, } from "#context/accessors.js";
13
+ export { defineState } from "#public/definitions/state.js";
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Typed handle returned by {@link defineState}. Provides read and
3
+ * update operations over a named context slot.
4
+ *
5
+ * All operations require an active Ash context (ALS scope) and throw
6
+ * when called outside one.
7
+ */
8
+ export interface StateHandle<T> {
9
+ /** Read the current value. Returns `initial()` on first access within a context. */
10
+ get(): T;
11
+ /** Update the value with a function that receives the current value. */
12
+ update(fn: (current: T) => T): void;
13
+ }
14
+ /**
15
+ * Creates a typed, named state slot backed by a durable `ContextKey`.
16
+ *
17
+ * All operations require an active Ash context — calling `get()` or
18
+ * `update()` outside of tools, hooks, or other framework-managed
19
+ * code throws.
20
+ *
21
+ * State is durable: values survive across workflow step boundaries.
22
+ * To reset per-turn, use `update(() => freshValue)` in a lifecycle
23
+ * hook.
24
+ *
25
+ * ```ts
26
+ * const budget = defineState("my-agent.budget", () => ({ count: 0, cap: 25 }));
27
+ *
28
+ * // In a tool or hook:
29
+ * budget.update((s) => ({ ...s, count: s.count + 1 }));
30
+ * const current = budget.get();
31
+ * ```
32
+ */
33
+ export declare function defineState<T>(name: string, initial: () => T): StateHandle<T>;
@@ -0,0 +1,34 @@
1
+ import { ContextKey } from "#context/key.js";
2
+ import { loadContext } from "#context/container.js";
3
+ /**
4
+ * Creates a typed, named state slot backed by a durable `ContextKey`.
5
+ *
6
+ * All operations require an active Ash context — calling `get()` or
7
+ * `update()` outside of tools, hooks, or other framework-managed
8
+ * code throws.
9
+ *
10
+ * State is durable: values survive across workflow step boundaries.
11
+ * To reset per-turn, use `update(() => freshValue)` in a lifecycle
12
+ * hook.
13
+ *
14
+ * ```ts
15
+ * const budget = defineState("my-agent.budget", () => ({ count: 0, cap: 25 }));
16
+ *
17
+ * // In a tool or hook:
18
+ * budget.update((s) => ({ ...s, count: s.count + 1 }));
19
+ * const current = budget.get();
20
+ * ```
21
+ */
22
+ export function defineState(name, initial) {
23
+ const key = new ContextKey(name);
24
+ return {
25
+ get() {
26
+ return loadContext().ensure(key, initial);
27
+ },
28
+ update(fn) {
29
+ const ctx = loadContext();
30
+ const current = ctx.ensure(key, initial);
31
+ ctx.set(key, fn(current));
32
+ },
33
+ };
34
+ }
@@ -45,6 +45,13 @@ export interface WithAshOptions {
45
45
  * absolute. Defaults to the Next.js app root.
46
46
  */
47
47
  readonly ashRoot?: string;
48
+ /**
49
+ * Build command for the generated Ash Vercel service. Defaults to `ash build`.
50
+ *
51
+ * Use this when the Ash service needs project-specific prework before the
52
+ * framework build, without changing the Next.js service build command.
53
+ */
54
+ readonly ashBuildCommand?: string;
48
55
  /**
49
56
  * Set to `false` to skip creating or updating `vercel.json`.
50
57
  *
@@ -9,6 +9,7 @@ import { ensureAshVercelJson } from "./vercel-json.js";
9
9
  export const ASH_NEXT_SERVICE_PREFIX = "/_ash_internal/ash";
10
10
  const ASH_NEXT_PRODUCTION_ORIGIN_ENV = "ASH_NEXT_PRODUCTION_ORIGIN";
11
11
  const ASH_NEXT_PRODUCTION_PORT_ENV = "ASH_NEXT_PRODUCTION_PORT";
12
+ const DEFAULT_ASH_BUILD_COMMAND = "ash build";
12
13
  const DEFAULT_ASH_NEXT_PRODUCTION_PORT = 4274;
13
14
  const ASH_PROXY_REWRITE_SOURCES = [`${ASH_ROUTE_PREFIX}/:path+`];
14
15
  function resolveApplicationRoot(appPath) {
@@ -129,6 +130,7 @@ export function withAsh(configOrFunction, options = {}) {
129
130
  const configuredVercel = shouldConfigureVercelJson
130
131
  ? await ensureAshVercelJson({
131
132
  appRoot,
133
+ ashBuildCommand: options.ashBuildCommand ?? DEFAULT_ASH_BUILD_COMMAND,
132
134
  nextRoot,
133
135
  servicePrefix: servicePrefixInput,
134
136
  })
@@ -3,6 +3,7 @@ export interface EnsureVercelJsonResult {
3
3
  }
4
4
  export declare function ensureAshVercelJson(input: {
5
5
  readonly appRoot: string;
6
+ readonly ashBuildCommand: string;
6
7
  readonly nextRoot: string;
7
8
  readonly servicePrefix: string;
8
9
  }): Promise<EnsureVercelJsonResult>;
@@ -57,6 +57,7 @@ export async function ensureAshVercelJson(input) {
57
57
  }
58
58
  if (configuredAshService === undefined) {
59
59
  experimentalServices.ash = {
60
+ buildCommand: input.ashBuildCommand,
60
61
  entrypoint: ashEntrypoint,
61
62
  framework: "ash",
62
63
  routePrefix: servicePrefix,
@@ -1,3 +1,3 @@
1
- export { useAshAgent, type UseAshAgentHelpers, type UseAshAgentOptions, type UseAshAgentSnapshot, type UseAshAgentStatus, } from "#react/use-ash-agent.js";
1
+ export { useAshAgent, type PrepareSend, type UseAshAgentHelpers, type UseAshAgentOptions, type UseAshAgentSnapshot, type UseAshAgentStatus, } from "#react/use-ash-agent.js";
2
2
  export { type AshAgentReducer, type AshAgentReducerEvent, type ClientInputRespondedEvent, type ClientMessageFailedEvent, type ClientMessageSubmittedEvent, } from "#client/reducer.js";
3
3
  export { defaultMessageReducer, type AshMessageData, type AshDynamicToolPart, type AshMessageInputRequest, type AshMessage, type AshMessageMetadata, type AshMessagePart, type AshMessageToolMetadata, } from "#client/message-reducer.js";
@@ -4,6 +4,13 @@ import { type AshMessageData } from "#client/message-reducer.js";
4
4
  import type { HandleMessageStreamEvent } from "#protocol/message.js";
5
5
  import type { ClientAuth, HeadersValue, SendMessageOptions, SendTurnInput, SessionState } from "#client/types.js";
6
6
  export type UseAshAgentStatus = "error" | "ready" | "streaming" | "submitted";
7
+ /**
8
+ * Prepares one outbound turn immediately before the client sends it.
9
+ *
10
+ * Use this to attach fresh, one-turn client state such as page context via
11
+ * `clientContext`.
12
+ */
13
+ export type PrepareSend = (input: SendTurnInput) => SendTurnInput | Promise<SendTurnInput>;
7
14
  /**
8
15
  * Current projected state for an Ash agent session.
9
16
  */
@@ -22,6 +29,7 @@ export interface UseAshAgentCallbacks<TData> {
22
29
  readonly onEvent?: (event: HandleMessageStreamEvent) => void;
23
30
  readonly onFinish?: (snapshot: UseAshAgentSnapshot<TData>) => void;
24
31
  readonly onSessionChange?: (session: SessionState) => void;
32
+ readonly prepareSend?: PrepareSend;
25
33
  }
26
34
  /**
27
35
  * Snapshot plus commands returned by `useAshAgent`.
@@ -26,6 +26,7 @@ export function useAshAgent(options = {}) {
26
26
  onEvent: options.onEvent,
27
27
  onFinish: options.onFinish,
28
28
  onSessionChange: options.onSessionChange,
29
+ prepareSend: options.prepareSend,
29
30
  });
30
31
  const subscribe = useCallback((onStoreChange) => store.subscribe(onStoreChange), [store]);
31
32
  const snapshot = useSyncExternalStore(subscribe, () => store.snapshot, () => store.snapshot);
@@ -102,11 +103,16 @@ class UseAshAgentStore {
102
103
  this.#abortController = abortController;
103
104
  this.#error = undefined;
104
105
  this.#status = "submitted";
105
- this.#projectOptimisticMessage(input);
106
- this.#projectInputResponses(input);
107
106
  this.#publish();
108
107
  try {
109
- const response = await this.#session.send(input, {
108
+ const preparedInput = (await this.#callbacks.prepareSend?.(input)) ?? input;
109
+ if (!this.#isCurrentOperation(operationId)) {
110
+ return;
111
+ }
112
+ this.#projectOptimisticMessage(preparedInput);
113
+ this.#projectInputResponses(preparedInput);
114
+ this.#publish();
115
+ const response = await this.#session.send(preparedInput, {
110
116
  ...options,
111
117
  signal: createAbortSignal(options?.signal, abortController.signal),
112
118
  });
@@ -195,7 +201,7 @@ class UseAshAgentStore {
195
201
  const pending = {
196
202
  createdAt: Date.now(),
197
203
  id,
198
- message: input.message,
204
+ message: summarizeUserContent(input.message),
199
205
  };
200
206
  this.#pendingMessageSubmission = pending;
201
207
  this.#appendProjectionEvent({
@@ -312,6 +318,22 @@ function createSubmissionId() {
312
318
  function createAbortSignal(first, second) {
313
319
  return first ? AbortSignal.any([first, second]) : second;
314
320
  }
321
+ function summarizeUserContent(message) {
322
+ if (typeof message === "string") {
323
+ return message;
324
+ }
325
+ const parts = [];
326
+ for (const part of message) {
327
+ if (part.type === "text") {
328
+ parts.push(part.text);
329
+ continue;
330
+ }
331
+ if (part.type === "file") {
332
+ parts.push(part.filename ? `[file: ${part.filename}]` : "[file]");
333
+ }
334
+ }
335
+ return parts.join("\n");
336
+ }
315
337
  function isAbortError(error) {
316
338
  return error instanceof Error && error.name === "AbortError";
317
339
  }
@@ -1,6 +1,8 @@
1
+ import type { UserContent } from "ai";
1
2
  import { type SessionState } from "#client/index.js";
2
3
  import type { HandleMessageStreamEvent } from "#protocol/message.js";
3
4
  import type { InputResponse } from "#runtime/input/types.js";
5
+ import type { JsonObject } from "#shared/json.js";
4
6
  export type { DevelopmentRequestHeaders } from "#services/dev-client/request-headers.js";
5
7
  export { extractCompletedMessage } from "#services/dev-client/stream.js";
6
8
  /**
@@ -41,8 +43,9 @@ export interface DevelopmentMessageClient {
41
43
  * Sends one turn payload through the active development session.
42
44
  */
43
45
  send(input: {
46
+ readonly clientContext?: string | readonly string[] | JsonObject;
44
47
  readonly inputResponses?: readonly InputResponse[];
45
- readonly message?: string;
48
+ readonly message?: string | UserContent;
46
49
  onEvent?(event: HandleMessageStreamEvent): void;
47
50
  onResponseStart?(response: {
48
51
  sessionId?: string;
@@ -109,6 +109,7 @@ class BufferedDevelopmentMessageClient {
109
109
  }
110
110
  async send(input) {
111
111
  const res = await this.#session.send({
112
+ clientContext: input.clientContext,
112
113
  inputResponses: input.inputResponses,
113
114
  message: input.message,
114
115
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "experimental-ash",
3
- "version": "0.22.2",
3
+ "version": "0.23.0",
4
4
  "bin": {
5
5
  "ash": "./bin/ash.js",
6
6
  "experimental-ash": "./bin/ash.js"