experimental-ash 0.18.0 → 0.18.1

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 (31) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/src/chunks/client-CKsU8Li3.js +4 -0
  3. package/dist/src/chunks/{dev-authored-source-watcher-CG6kri3T.js → dev-authored-source-watcher-DtLxnrXI.js} +1 -1
  4. package/dist/src/chunks/{host-CIU0NATc.js → host-Dor4C8jo.js} +2 -2
  5. package/dist/src/chunks/{paths-CvbqpwTh.js → paths-AVYgVLR3.js} +1 -1
  6. package/dist/src/chunks/{prewarm-C_Vd0JR7.js → prewarm-DsMkM8wg.js} +1 -1
  7. package/dist/src/cli/commands/info.js +1 -1
  8. package/dist/src/cli/dev/repl.js +1 -1
  9. package/dist/src/cli/run.js +1 -1
  10. package/dist/src/client/client.js +2 -1
  11. package/dist/src/client/index.d.ts +3 -0
  12. package/dist/src/client/index.js +1 -0
  13. package/dist/src/client/message-reducer-types.d.ts +130 -0
  14. package/dist/src/client/message-reducer-types.js +1 -0
  15. package/dist/src/client/message-reducer.d.ts +14 -0
  16. package/dist/src/client/message-reducer.js +462 -0
  17. package/dist/src/client/open-stream.js +2 -4
  18. package/dist/src/client/reducer.d.ts +63 -0
  19. package/dist/src/client/reducer.js +1 -0
  20. package/dist/src/client/session.js +3 -5
  21. package/dist/src/client/url.d.ts +8 -0
  22. package/dist/src/client/url.js +34 -0
  23. package/dist/src/compiler/module-map.js +12 -0
  24. package/dist/src/evals/cli/eval.js +1 -1
  25. package/dist/src/internal/application/package.js +1 -1
  26. package/dist/src/react/index.d.ts +3 -0
  27. package/dist/src/react/index.js +3 -0
  28. package/dist/src/react/use-ash-agent.d.ts +79 -0
  29. package/dist/src/react/use-ash-agent.js +330 -0
  30. package/package.json +15 -2
  31. package/dist/src/chunks/client-BeZ_W7vl.js +0 -4
@@ -1,3 +1,3 @@
1
- import{t as e}from"../chunks/package-DmsQgn4v.js";import{createCliTheme as t,renderCliTaggedLine as n}from"./ui/output.js";import{i as r,n as i,r as a,t as o}from"../chunks/url-BVRhVE2O.js";import{resolve as s}from"node:path";async function c(){return(await import(`../chunks/host-CIU0NATc.js`).then(e=>e.t)).buildHost}async function l(){return(await import(`./commands/info.js`)).printApplicationInfo}async function u(){return(await import(`./dev/repl.js`)).runDevelopmentRepl}async function d(){return(await import(`../evals/cli/eval.js`)).runEvalCommand}async function f(){return(await import(`../chunks/host-CIU0NATc.js`).then(e=>e.t)).startHost}function p(e=process.cwd()){return s(e)}function m(e){return`Ash (v${e})`}function h(e){return e.name()===`info`||e.name()===`dev`}async function g(e){await new Promise((t,n)=>{let r=!1,i=()=>{process.off(`SIGINT`,a),process.off(`SIGTERM`,a)},a=()=>{r||(r=!0,i(),e.close().then(t,n))};process.once(`SIGINT`,a),process.once(`SIGTERM`,a)})}function _(e){if(!/^-?\d+$/.test(e))throw new r(`Expected a numeric port, received "${e}".`);let t=Number(e);if(!Number.isInteger(t))throw new r(`Expected a numeric port, received "${e}".`);if(t<0||t>65535)throw new r(`Expected a port between 0 and 65535, received "${e}".`);return t}function v(){return!!(process.stdin.isTTY&&process.stdout.isTTY)}function y(e){let t=e[1];return e[0]!==`dev`||e.length!==2||t===void 0||t.startsWith(`-`)?[...e]:[`dev`,`--url`,t]}function b(e){if(e.url){if(e.host!==void 0)throw new r(`The --host option cannot be used with --url.`);if(e.port!==void 0)throw new r(`The --port option cannot be used with --url.`);if(e.repl===!1)throw new r(`The --no-repl option cannot be used with --url.`);return e.url}}function x(r,a){let s=p(),y=e().version,x=new i,S=t();return x.name(`ash`).description(`Build and run an Ash application.`).version(y).showHelpAfterError().exitOverride().hook(`preAction`,(e,t)=>{h(t)&&r.log(m(y))}).configureOutput({writeErr:e=>{r.error(e.trimEnd())},writeOut:e=>{r.log(e.trimEnd())}}),x.command(`build`).description(`Build the current Ash application.`).action(async()=>{let{loadDevelopmentEnvironmentFiles:e}=await import(`./dev/environment.js`);e(s);let t=await(a.buildHost??await c())(s);r.log(n(S,{message:`built output at ${t}`,tag:`build`,tone:`success`}))}),x.command(`dev`).description(`Start the Ash development server or connect the REPL to an existing URL.`).option(`--host <host>`,`Host interface to bind`).option(`--no-repl`,`Start the server without the interactive REPL`).option(`--port <port>`,`Port to listen on (defaults to $PORT, then 3000)`,_).option(`--schedules`,`Run scheduled tasks during development (off by default)`).option(`-u, --url <url>`,`Connect the REPL to an existing server URL`,o).addHelpText(`after`,`
1
+ import{t as e}from"../chunks/package-DmsQgn4v.js";import{createCliTheme as t,renderCliTaggedLine as n}from"./ui/output.js";import{i as r,n as i,r as a,t as o}from"../chunks/url-BVRhVE2O.js";import{resolve as s}from"node:path";async function c(){return(await import(`../chunks/host-Dor4C8jo.js`).then(e=>e.t)).buildHost}async function l(){return(await import(`./commands/info.js`)).printApplicationInfo}async function u(){return(await import(`./dev/repl.js`)).runDevelopmentRepl}async function d(){return(await import(`../evals/cli/eval.js`)).runEvalCommand}async function f(){return(await import(`../chunks/host-Dor4C8jo.js`).then(e=>e.t)).startHost}function p(e=process.cwd()){return s(e)}function m(e){return`Ash (v${e})`}function h(e){return e.name()===`info`||e.name()===`dev`}async function g(e){await new Promise((t,n)=>{let r=!1,i=()=>{process.off(`SIGINT`,a),process.off(`SIGTERM`,a)},a=()=>{r||(r=!0,i(),e.close().then(t,n))};process.once(`SIGINT`,a),process.once(`SIGTERM`,a)})}function _(e){if(!/^-?\d+$/.test(e))throw new r(`Expected a numeric port, received "${e}".`);let t=Number(e);if(!Number.isInteger(t))throw new r(`Expected a numeric port, received "${e}".`);if(t<0||t>65535)throw new r(`Expected a port between 0 and 65535, received "${e}".`);return t}function v(){return!!(process.stdin.isTTY&&process.stdout.isTTY)}function y(e){let t=e[1];return e[0]!==`dev`||e.length!==2||t===void 0||t.startsWith(`-`)?[...e]:[`dev`,`--url`,t]}function b(e){if(e.url){if(e.host!==void 0)throw new r(`The --host option cannot be used with --url.`);if(e.port!==void 0)throw new r(`The --port option cannot be used with --url.`);if(e.repl===!1)throw new r(`The --no-repl option cannot be used with --url.`);return e.url}}function x(r,a){let s=p(),y=e().version,x=new i,S=t();return x.name(`ash`).description(`Build and run an Ash application.`).version(y).showHelpAfterError().exitOverride().hook(`preAction`,(e,t)=>{h(t)&&r.log(m(y))}).configureOutput({writeErr:e=>{r.error(e.trimEnd())},writeOut:e=>{r.log(e.trimEnd())}}),x.command(`build`).description(`Build the current Ash application.`).action(async()=>{let{loadDevelopmentEnvironmentFiles:e}=await import(`./dev/environment.js`);e(s);let t=await(a.buildHost??await c())(s);r.log(n(S,{message:`built output at ${t}`,tag:`build`,tone:`success`}))}),x.command(`dev`).description(`Start the Ash development server or connect the REPL to an existing URL.`).option(`--host <host>`,`Host interface to bind`).option(`--no-repl`,`Start the server without the interactive REPL`).option(`--port <port>`,`Port to listen on (defaults to $PORT, then 3000)`,_).option(`--schedules`,`Run scheduled tasks during development (off by default)`).option(`-u, --url <url>`,`Connect the REPL to an existing server URL`,o).addHelpText(`after`,`
2
2
  You can also pass a bare URL as the only argument, for example: ash dev https://example.com
3
3
  `).action(async e=>{let t=b(e),{loadDevelopmentEnvironmentFiles:i}=await import(`./dev/environment.js`);if(i(s),t){if(r.log(n(S,{message:`REPL connecting to ${t}`,tag:`dev`,tone:`info`})),!v()){r.log(n(S,{message:`Interactive REPL disabled because the current terminal is not a TTY.`,tag:`dev`,tone:`warning`}));return}r.log(``),await(a.runDevelopmentRepl??await u())({serverUrl:t});return}let o=await(a.startHost??await f())(s,{host:e.host,port:e.port,schedules:e.schedules===!0}),c=!1,l=async()=>{c||(c=!0,await o.close())};try{if(r.log(n(S,{message:`server listening at ${o.url}`,tag:`dev`,tone:`success`})),e.repl===!1)return await g({close:l});if(!v())return r.log(n(S,{message:`Interactive REPL disabled because the current terminal is not a TTY.`,tag:`dev`,tone:`warning`})),await g({close:l});r.log(``),await(a.runDevelopmentRepl??await u())({serverUrl:o.url})}finally{await l()}}),x.command(`info`).description(`Print resolved application information.`).action(async()=>{await(a.printApplicationInfo??await l())(r,s)}),x.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(a.runEvalCommand??await d())(e,r)}),x}async function S(e=process.argv.slice(2),t=console,n={}){let r=x(t,n),i=e.length===0?[`info`]:y(e);try{await r.parseAsync(i,{from:`user`})}catch(e){if(e instanceof a){if(e.exitCode===0)return;throw Error(e.message)}throw e}}export{S as runCli};
@@ -2,6 +2,7 @@ import { ASH_HEALTH_ROUTE_PATH } from "#protocol/routes.js";
2
2
  import { ClientError } from "#client/client-error.js";
3
3
  import { ClientSession } from "#client/session.js";
4
4
  import { createInitialSessionState } from "#client/session-utils.js";
5
+ import { createClientUrl } from "#client/url.js";
5
6
  /**
6
7
  * HTTP client for talking to a deployed Ash agent.
7
8
  *
@@ -26,7 +27,7 @@ export class Client {
26
27
  * @throws {ClientError} If the server returns a non-successful status.
27
28
  */
28
29
  async health() {
29
- const url = new URL(ASH_HEALTH_ROUTE_PATH, this.#host);
30
+ const url = createClientUrl(this.#host, ASH_HEALTH_ROUTE_PATH);
30
31
  const headers = await this.#resolveHeaders();
31
32
  const response = await fetch(url, { headers });
32
33
  if (!response.ok) {
@@ -1,8 +1,11 @@
1
1
  export { Client } from "#client/client.js";
2
2
  export { ClientError } from "#client/client-error.js";
3
+ export { defaultMessageReducer } from "#client/message-reducer.js";
3
4
  export { MessageResponse } from "#client/message-response.js";
4
5
  export { ClientSession } from "#client/session.js";
5
6
  export type { ClientAuth, ClientOptions, HeadersValue, HealthResult, MessageResult, OpenStreamOptions, SendMessageOptions, SendTurnInput, SessionState, TokenValue, } from "#client/types.js";
7
+ export type { AshAgentReducer, AshAgentReducerEvent, ClientInputRespondedEvent, ClientMessageFailedEvent, ClientMessageSubmittedEvent, } from "#client/reducer.js";
8
+ export type { AshMessageData, AshDynamicToolPart, AshMessageInputRequest, AshMessage, AshMessageMetadata, AshMessagePart, AshMessageToolMetadata, } from "#client/message-reducer.js";
6
9
  export type { ActionResultStreamEvent, ActionsRequestedStreamEvent, AssistantStepFinishReason, CompactionCompletedStreamEvent, CompactionRequestedStreamEvent, HandleMessageStreamEvent, InputRequestedStreamEvent, MessageAppendedStreamEvent, MessageCompletedStreamEvent, MessageReceivedStreamEvent, ReasoningAppendedStreamEvent, ReasoningCompletedStreamEvent, SessionCompletedStreamEvent, SessionFailedStreamEvent, SessionStartedStreamEvent, SessionWaitingStreamEvent, StepCompletedStreamEvent, StepFailedStreamEvent, StepStartedStreamEvent, SubagentCalledStreamEvent, SubagentChildEventStreamEvent, SubagentCompletedStreamEvent, SubagentStartedStreamEvent, TurnCompletedStreamEvent, TurnFailedStreamEvent, TurnStartedStreamEvent, } from "#protocol/message.js";
7
10
  export { isCurrentTurnBoundaryEvent } from "#protocol/message.js";
8
11
  export type { InputOption, InputRequest, InputResponse } from "#runtime/input/types.js";
@@ -3,6 +3,7 @@
3
3
  // ---------------------------------------------------------------------------
4
4
  export { Client } from "#client/client.js";
5
5
  export { ClientError } from "#client/client-error.js";
6
+ export { defaultMessageReducer } from "#client/message-reducer.js";
6
7
  export { MessageResponse } from "#client/message-response.js";
7
8
  export { ClientSession } from "#client/session.js";
8
9
  export { isCurrentTurnBoundaryEvent } from "#protocol/message.js";
@@ -0,0 +1,130 @@
1
+ import type { InputResponse } from "#runtime/input/types.js";
2
+ /**
3
+ * UIMessage-compatible Ash message projection for chat and agent UIs.
4
+ */
5
+ export interface AshMessageData {
6
+ readonly messages: readonly AshMessage[];
7
+ }
8
+ /**
9
+ * Ash-owned message shape that follows the AI SDK UIMessage convention.
10
+ */
11
+ export interface AshMessage {
12
+ readonly id: string;
13
+ readonly metadata?: AshMessageMetadata;
14
+ readonly parts: readonly AshMessagePart[];
15
+ readonly role: "assistant" | "user";
16
+ }
17
+ export interface AshMessageMetadata {
18
+ readonly optimistic?: true;
19
+ readonly status?: "complete" | "failed" | "streaming" | "submitted";
20
+ readonly turnId?: string;
21
+ }
22
+ export type AshMessagePart = {
23
+ readonly providerMetadata?: Record<string, unknown>;
24
+ readonly state?: "done" | "streaming";
25
+ readonly stepIndex?: number;
26
+ readonly text: string;
27
+ readonly type: "text";
28
+ } | {
29
+ readonly providerMetadata?: Record<string, unknown>;
30
+ readonly state?: "done" | "streaming";
31
+ readonly stepIndex?: number;
32
+ readonly text: string;
33
+ readonly type: "reasoning";
34
+ } | {
35
+ readonly type: "step-start";
36
+ } | AshDynamicToolPart;
37
+ export type AshDynamicToolPart = {
38
+ readonly stepIndex?: number;
39
+ readonly toolCallId: string;
40
+ readonly toolMetadata?: AshMessageToolMetadata;
41
+ readonly toolName: string;
42
+ readonly type: "dynamic-tool";
43
+ } & ({
44
+ readonly approval?: never;
45
+ readonly errorText?: never;
46
+ readonly input: unknown | undefined;
47
+ readonly output?: never;
48
+ readonly state: "input-streaming";
49
+ } | {
50
+ readonly approval?: never;
51
+ readonly errorText?: never;
52
+ readonly input: unknown;
53
+ readonly output?: never;
54
+ readonly state: "input-available";
55
+ } | {
56
+ readonly approval: {
57
+ readonly id: string;
58
+ readonly approved?: never;
59
+ readonly reason?: never;
60
+ readonly isAutomatic?: boolean;
61
+ };
62
+ readonly errorText?: never;
63
+ readonly input: unknown;
64
+ readonly output?: never;
65
+ readonly state: "approval-requested";
66
+ } | {
67
+ readonly approval: {
68
+ readonly id: string;
69
+ readonly approved?: boolean;
70
+ readonly reason?: string;
71
+ readonly isAutomatic?: boolean;
72
+ };
73
+ readonly errorText?: never;
74
+ readonly input: unknown;
75
+ readonly output?: never;
76
+ readonly state: "approval-responded";
77
+ } | {
78
+ readonly approval?: {
79
+ readonly id: string;
80
+ readonly approved: true;
81
+ readonly reason?: string;
82
+ readonly isAutomatic?: boolean;
83
+ };
84
+ readonly errorText?: never;
85
+ readonly input: unknown;
86
+ readonly output: unknown;
87
+ readonly state: "output-available";
88
+ } | {
89
+ readonly approval?: {
90
+ readonly id: string;
91
+ readonly approved: true;
92
+ readonly reason?: string;
93
+ readonly isAutomatic?: boolean;
94
+ };
95
+ readonly errorText: string;
96
+ readonly input: unknown | undefined;
97
+ readonly output?: never;
98
+ readonly state: "output-error";
99
+ } | {
100
+ readonly approval: {
101
+ readonly id: string;
102
+ readonly approved: false;
103
+ readonly reason?: string;
104
+ readonly isAutomatic?: boolean;
105
+ };
106
+ readonly errorText?: never;
107
+ readonly input: unknown;
108
+ readonly output?: never;
109
+ readonly state: "output-denied";
110
+ });
111
+ export interface AshMessageToolMetadata {
112
+ readonly ash?: {
113
+ readonly inputRequest?: AshMessageInputRequest;
114
+ readonly inputResponse?: InputResponse;
115
+ readonly kind: "load-skill" | "subagent-call" | "tool-call" | "unknown";
116
+ readonly name: string;
117
+ };
118
+ }
119
+ export interface AshMessageInputRequest {
120
+ readonly allowFreeform?: boolean;
121
+ readonly display?: "confirmation" | "select" | "text";
122
+ readonly options?: readonly {
123
+ readonly description?: string;
124
+ readonly id: string;
125
+ readonly label: string;
126
+ readonly style?: "danger" | "default" | "primary";
127
+ }[];
128
+ readonly prompt: string;
129
+ readonly requestId: string;
130
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import type { AshAgentReducer } from "#client/reducer.js";
2
+ import type { AshMessageData } from "#client/message-reducer-types.js";
3
+ export type { AshMessageData, AshDynamicToolPart, AshMessageInputRequest, AshMessage, AshMessageMetadata, AshMessagePart, AshMessageToolMetadata, } from "#client/message-reducer-types.js";
4
+ /**
5
+ * Creates a UIMessage-compatible Ash reducer for chat and agent UIs.
6
+ *
7
+ * The returned projection keeps Ash-owned types while following the AI SDK
8
+ * `messages[].parts[]` rendering convention used by AI Elements. It projects
9
+ * text, reasoning, tool calls, tool results, tool approvals, and submitted
10
+ * HITL responses. Connection authorization stream events remain available to
11
+ * custom reducers through the reducer event contract until Ash has a dedicated
12
+ * message-part shape for authorization UI.
13
+ */
14
+ export declare function defaultMessageReducer(): AshAgentReducer<AshMessageData>;
@@ -0,0 +1,462 @@
1
+ /**
2
+ * Creates a UIMessage-compatible Ash reducer for chat and agent UIs.
3
+ *
4
+ * The returned projection keeps Ash-owned types while following the AI SDK
5
+ * `messages[].parts[]` rendering convention used by AI Elements. It projects
6
+ * text, reasoning, tool calls, tool results, tool approvals, and submitted
7
+ * HITL responses. Connection authorization stream events remain available to
8
+ * custom reducers through the reducer event contract until Ash has a dedicated
9
+ * message-part shape for authorization UI.
10
+ */
11
+ export function defaultMessageReducer() {
12
+ return {
13
+ initial() {
14
+ return { messages: [] };
15
+ },
16
+ reduce(data, event) {
17
+ return reduceMessageData(data, event);
18
+ },
19
+ };
20
+ }
21
+ function reduceMessageData(data, event) {
22
+ switch (event.type) {
23
+ case "client.message.submitted":
24
+ return upsertMessage(data, {
25
+ id: optimisticUserMessageId(event.data.submissionId),
26
+ metadata: {
27
+ optimistic: true,
28
+ status: "submitted",
29
+ },
30
+ parts: [{ type: "text", text: event.data.message }],
31
+ role: "user",
32
+ });
33
+ case "client.message.failed":
34
+ return upsertMessage(data, {
35
+ id: optimisticUserMessageId(event.data.submissionId),
36
+ metadata: {
37
+ optimistic: true,
38
+ status: "failed",
39
+ },
40
+ parts: [{ type: "text", text: event.data.message }],
41
+ role: "user",
42
+ });
43
+ case "client.input.responded": {
44
+ let next = data;
45
+ for (const response of event.data.responses) {
46
+ next = respondToInputRequest(next, response);
47
+ }
48
+ return next;
49
+ }
50
+ case "message.received":
51
+ return upsertMessage(data, {
52
+ id: `${event.data.turnId}:user`,
53
+ metadata: {
54
+ status: "complete",
55
+ turnId: event.data.turnId,
56
+ },
57
+ parts: [{ type: "text", text: event.data.message, state: "done" }],
58
+ role: "user",
59
+ });
60
+ case "step.started":
61
+ return updateAssistantMessage(data, event.data.turnId, (message) => ensureStepStartPart(message, event.data.stepIndex));
62
+ case "reasoning.appended":
63
+ return updateAssistantMessage(data, event.data.turnId, (message) => upsertPart(ensureStepStartPart(message, event.data.stepIndex), {
64
+ state: "streaming",
65
+ stepIndex: event.data.stepIndex,
66
+ text: event.data.reasoningSoFar,
67
+ type: "reasoning",
68
+ }));
69
+ case "reasoning.completed":
70
+ return updateAssistantMessage(data, event.data.turnId, (message) => upsertPart(ensureStepStartPart(message, event.data.stepIndex), {
71
+ state: "done",
72
+ stepIndex: event.data.stepIndex,
73
+ text: event.data.reasoning,
74
+ type: "reasoning",
75
+ }));
76
+ case "actions.requested": {
77
+ let next = data;
78
+ for (const action of event.data.actions) {
79
+ const descriptor = normalizeActionRequest(action);
80
+ next = updateAssistantMessage(next, event.data.turnId, (message) => upsertPart(ensureStepStartPart(message, event.data.stepIndex), {
81
+ input: "input" in action ? action.input : undefined,
82
+ state: "input-available",
83
+ stepIndex: event.data.stepIndex,
84
+ toolCallId: action.callId,
85
+ toolMetadata: createToolMetadata(descriptor, event.data.stepIndex),
86
+ toolName: descriptor.toolName,
87
+ type: "dynamic-tool",
88
+ }));
89
+ }
90
+ return next;
91
+ }
92
+ case "input.requested": {
93
+ let next = data;
94
+ for (const request of event.data.requests) {
95
+ const descriptor = normalizeActionRequest(request.action);
96
+ next = updateAssistantMessage(next, event.data.turnId, (message) => upsertPart(ensureStepStartPart(message, event.data.stepIndex), {
97
+ approval: {
98
+ id: request.requestId,
99
+ },
100
+ input: request.action.input,
101
+ state: "approval-requested",
102
+ stepIndex: event.data.stepIndex,
103
+ toolCallId: request.action.callId,
104
+ toolMetadata: createToolMetadata(descriptor, event.data.stepIndex, {
105
+ inputRequest: toMessageInputRequest(request),
106
+ }),
107
+ toolName: descriptor.toolName,
108
+ type: "dynamic-tool",
109
+ }));
110
+ }
111
+ return next;
112
+ }
113
+ case "action.result": {
114
+ const descriptor = normalizeActionResult(event.data.result);
115
+ const existing = findToolPart(data, event.data.result.callId);
116
+ const denied = event.data.error?.code === "TOOL_EXECUTION_DENIED";
117
+ const failed = event.data.status === "failed" && !denied;
118
+ const approvalId = existing?.approval?.id ?? event.data.result.callId;
119
+ const toolMetadata = mergeToolMetadata(existing?.toolMetadata, createToolMetadata(descriptor, event.data.stepIndex));
120
+ const resultPartBase = {
121
+ input: existing?.input,
122
+ stepIndex: event.data.stepIndex,
123
+ toolCallId: event.data.result.callId,
124
+ toolMetadata,
125
+ toolName: existing?.toolName ?? descriptor.toolName,
126
+ type: "dynamic-tool",
127
+ };
128
+ let nextPart;
129
+ if (denied) {
130
+ nextPart = {
131
+ ...resultPartBase,
132
+ approval: {
133
+ approved: false,
134
+ id: approvalId,
135
+ reason: event.data.error?.message,
136
+ },
137
+ state: "output-denied",
138
+ };
139
+ }
140
+ else if (failed) {
141
+ nextPart = {
142
+ ...resultPartBase,
143
+ approval: approvedApproval(existing),
144
+ errorText: event.data.error?.message ?? stringifyUnknown(event.data.result.output),
145
+ state: "output-error",
146
+ };
147
+ }
148
+ else {
149
+ nextPart = {
150
+ ...resultPartBase,
151
+ approval: approvedApproval(existing),
152
+ output: event.data.result.output,
153
+ state: "output-available",
154
+ };
155
+ }
156
+ if (existing !== undefined) {
157
+ // Approved tool results can arrive on a later runtime turn; keep
158
+ // the UI lifecycle anchored to the original tool call.
159
+ return updateToolPart(data, event.data.result.callId, nextPart);
160
+ }
161
+ return updateAssistantMessage(data, event.data.turnId, (message) => upsertPart(ensureStepStartPart(message, event.data.stepIndex), nextPart));
162
+ }
163
+ case "message.appended":
164
+ return updateAssistantMessage(data, event.data.turnId, (message) => upsertPart(ensureStepStartPart(message, event.data.stepIndex), {
165
+ state: "streaming",
166
+ stepIndex: event.data.stepIndex,
167
+ text: event.data.messageSoFar,
168
+ type: "text",
169
+ }));
170
+ case "message.completed":
171
+ return updateAssistantMessage(data, event.data.turnId, (message) => {
172
+ if (event.data.message === null) {
173
+ return completeExistingTextPart(message);
174
+ }
175
+ return upsertPart(ensureStepStartPart(message, event.data.stepIndex), {
176
+ state: "done",
177
+ stepIndex: event.data.stepIndex,
178
+ text: event.data.message,
179
+ type: "text",
180
+ });
181
+ });
182
+ case "turn.completed":
183
+ return updateAssistantMetadata(data, event.data.turnId, { status: "complete" });
184
+ case "turn.failed":
185
+ case "session.failed":
186
+ return data;
187
+ default:
188
+ return data;
189
+ }
190
+ }
191
+ function respondToInputRequest(data, response) {
192
+ const existing = findToolPartByApprovalId(data, response.requestId);
193
+ if (!existing) {
194
+ return data;
195
+ }
196
+ const approval = {
197
+ id: response.requestId,
198
+ };
199
+ if (response.text !== undefined) {
200
+ approval.reason = response.text;
201
+ }
202
+ return updateToolPart(data, existing.toolCallId, {
203
+ approval,
204
+ input: existing.input,
205
+ state: "approval-responded",
206
+ stepIndex: existing.stepIndex,
207
+ toolCallId: existing.toolCallId,
208
+ toolMetadata: mergeToolMetadata(existing.toolMetadata, {
209
+ ash: {
210
+ inputResponse: response,
211
+ kind: existing.toolMetadata?.ash?.kind ?? "unknown",
212
+ name: existing.toolMetadata?.ash?.name ?? existing.toolName,
213
+ },
214
+ }),
215
+ toolName: existing.toolName,
216
+ type: "dynamic-tool",
217
+ });
218
+ }
219
+ function updateAssistantMessage(data, turnId, update) {
220
+ const existing = data.messages.find((message) => message.role === "assistant" && message.metadata?.turnId === turnId);
221
+ const message = existing ?? createAssistantMessage(turnId);
222
+ return upsertMessage(data, update(message));
223
+ }
224
+ function updateAssistantMetadata(data, turnId, metadata) {
225
+ return updateAssistantMessage(data, turnId, (message) => ({
226
+ ...message,
227
+ metadata: {
228
+ ...message.metadata,
229
+ ...metadata,
230
+ },
231
+ }));
232
+ }
233
+ function createAssistantMessage(turnId) {
234
+ return {
235
+ id: `${turnId}:assistant`,
236
+ metadata: {
237
+ status: "streaming",
238
+ turnId,
239
+ },
240
+ parts: [],
241
+ role: "assistant",
242
+ };
243
+ }
244
+ function ensureStepStartPart(message, stepIndex) {
245
+ const stepStartCount = message.parts.filter((part) => part.type === "step-start").length;
246
+ if (stepStartCount > stepIndex) {
247
+ return message;
248
+ }
249
+ const missingCount = stepIndex - stepStartCount + 1;
250
+ return {
251
+ ...message,
252
+ parts: [
253
+ ...message.parts,
254
+ ...Array.from({ length: missingCount }, () => ({ type: "step-start" })),
255
+ ],
256
+ };
257
+ }
258
+ function upsertPart(message, next) {
259
+ const index = message.parts.findIndex((part) => partKey(part) === partKey(next));
260
+ const parts = index === -1
261
+ ? [...message.parts, next]
262
+ : [...message.parts.slice(0, index), next, ...message.parts.slice(index + 1)];
263
+ return {
264
+ ...message,
265
+ metadata: {
266
+ ...message.metadata,
267
+ status: next.type === "text" && next.state === "done" ? "complete" : "streaming",
268
+ },
269
+ parts,
270
+ };
271
+ }
272
+ function completeExistingTextPart(message) {
273
+ const index = findLastIndex(message.parts, (part) => part.type === "text");
274
+ if (index === -1) {
275
+ return message;
276
+ }
277
+ const existing = message.parts[index];
278
+ if (existing?.type !== "text") {
279
+ return message;
280
+ }
281
+ return {
282
+ ...message,
283
+ metadata: {
284
+ ...message.metadata,
285
+ status: "complete",
286
+ },
287
+ parts: [
288
+ ...message.parts.slice(0, index),
289
+ { ...existing, state: "done" },
290
+ ...message.parts.slice(index + 1),
291
+ ],
292
+ };
293
+ }
294
+ function updateToolPart(data, toolCallId, next) {
295
+ const message = data.messages.find((candidate) => candidate.role === "assistant" &&
296
+ candidate.parts.some((part) => part.type === "dynamic-tool" && part.toolCallId === toolCallId));
297
+ if (!message) {
298
+ return data;
299
+ }
300
+ return upsertMessage(data, upsertPart(message, next));
301
+ }
302
+ function findToolPart(data, toolCallId) {
303
+ for (const message of data.messages) {
304
+ for (const part of message.parts) {
305
+ if (part.type === "dynamic-tool" && part.toolCallId === toolCallId) {
306
+ return part;
307
+ }
308
+ }
309
+ }
310
+ return undefined;
311
+ }
312
+ function findToolPartByApprovalId(data, approvalId) {
313
+ for (const message of data.messages) {
314
+ for (const part of message.parts) {
315
+ if (part.type === "dynamic-tool" && part.approval?.id === approvalId) {
316
+ return part;
317
+ }
318
+ }
319
+ }
320
+ return undefined;
321
+ }
322
+ function partKey(part) {
323
+ switch (part.type) {
324
+ case "text":
325
+ return `text:${part.stepIndex ?? 0}`;
326
+ case "reasoning":
327
+ return `reasoning:${part.stepIndex ?? 0}`;
328
+ case "step-start":
329
+ return "step-start";
330
+ case "dynamic-tool":
331
+ return `dynamic-tool:${part.toolCallId}`;
332
+ }
333
+ }
334
+ function upsertMessage(data, next) {
335
+ const index = data.messages.findIndex((message) => message.id === next.id);
336
+ if (index === -1) {
337
+ return { messages: [...data.messages, next] };
338
+ }
339
+ return {
340
+ messages: [...data.messages.slice(0, index), next, ...data.messages.slice(index + 1)],
341
+ };
342
+ }
343
+ function toMessageInputRequest(request) {
344
+ return {
345
+ allowFreeform: request.allowFreeform,
346
+ display: request.display,
347
+ options: request.options,
348
+ prompt: request.prompt,
349
+ requestId: request.requestId,
350
+ };
351
+ }
352
+ function createToolMetadata(descriptor, _stepIndex, extra) {
353
+ return {
354
+ ash: {
355
+ inputRequest: extra?.inputRequest,
356
+ kind: descriptor.kind,
357
+ name: descriptor.name,
358
+ },
359
+ };
360
+ }
361
+ function mergeToolMetadata(current, next) {
362
+ const kind = next.ash?.kind ?? current?.ash?.kind ?? "unknown";
363
+ const name = next.ash?.name ?? current?.ash?.name ?? "unknown";
364
+ return {
365
+ ash: {
366
+ ...current?.ash,
367
+ ...next.ash,
368
+ inputRequest: next.ash?.inputRequest ?? current?.ash?.inputRequest,
369
+ inputResponse: next.ash?.inputResponse ?? current?.ash?.inputResponse,
370
+ kind,
371
+ name,
372
+ },
373
+ };
374
+ }
375
+ function approvedApproval(part) {
376
+ if (!part?.approval?.id) {
377
+ return undefined;
378
+ }
379
+ return {
380
+ approved: true,
381
+ id: part.approval.id,
382
+ isAutomatic: part.approval.isAutomatic,
383
+ reason: part.approval.reason,
384
+ };
385
+ }
386
+ function normalizeActionRequest(action) {
387
+ switch (action.kind) {
388
+ case "load-skill":
389
+ return {
390
+ kind: "load-skill",
391
+ name: action.name ?? "load_skill",
392
+ toolName: "ash:load-skill",
393
+ };
394
+ case "tool-call":
395
+ return {
396
+ kind: "tool-call",
397
+ name: action.toolName ?? "tool",
398
+ toolName: action.toolName ?? "tool",
399
+ };
400
+ case "subagent-call":
401
+ return {
402
+ kind: "subagent-call",
403
+ name: action.subagentName ?? action.name ?? "subagent",
404
+ toolName: `ash:subagent:${action.subagentName ?? action.name ?? "subagent"}`,
405
+ };
406
+ default:
407
+ return {
408
+ kind: "unknown",
409
+ name: action.toolName ?? action.subagentName ?? action.name ?? action.kind,
410
+ toolName: action.toolName ?? action.subagentName ?? action.name ?? action.kind,
411
+ };
412
+ }
413
+ }
414
+ function normalizeActionResult(result) {
415
+ switch (result.kind) {
416
+ case "load-skill-result":
417
+ return {
418
+ kind: "load-skill",
419
+ name: result.name ?? "load_skill",
420
+ toolName: "ash:load-skill",
421
+ };
422
+ case "tool-result":
423
+ return {
424
+ kind: "tool-call",
425
+ name: result.toolName ?? "tool",
426
+ toolName: result.toolName ?? "tool",
427
+ };
428
+ case "subagent-result":
429
+ return {
430
+ kind: "subagent-call",
431
+ name: result.subagentName ?? "subagent",
432
+ toolName: `ash:subagent:${result.subagentName ?? "subagent"}`,
433
+ };
434
+ default:
435
+ return {
436
+ kind: "unknown",
437
+ name: result.toolName ?? result.subagentName ?? result.name ?? result.kind,
438
+ toolName: result.toolName ?? result.subagentName ?? result.name ?? result.kind,
439
+ };
440
+ }
441
+ }
442
+ function optimisticUserMessageId(submissionId) {
443
+ return `optimistic:${submissionId}:user`;
444
+ }
445
+ function stringifyUnknown(value) {
446
+ if (typeof value === "string")
447
+ return value;
448
+ try {
449
+ return JSON.stringify(value);
450
+ }
451
+ catch {
452
+ return "Action failed.";
453
+ }
454
+ }
455
+ function findLastIndex(items, predicate) {
456
+ for (let index = items.length - 1; index >= 0; index -= 1) {
457
+ if (predicate(items[index])) {
458
+ return index;
459
+ }
460
+ }
461
+ return -1;
462
+ }