experimental-ash 0.18.0 → 0.18.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.
- package/CHANGELOG.md +43 -2
- package/dist/docs/public/channels/README.md +75 -9
- package/dist/docs/public/schedules.md +13 -4
- package/dist/src/channel/adapter-context.d.ts +4 -0
- package/dist/src/channel/adapter-context.js +6 -0
- package/dist/src/channel/adapter.d.ts +10 -10
- package/dist/src/channel/cross-channel-receive.d.ts +1 -1
- package/dist/src/channel/cross-channel-receive.js +40 -0
- package/dist/src/channel/routes.d.ts +7 -0
- package/dist/src/channel/send.js +1 -1
- package/dist/src/channel/session.d.ts +47 -1
- package/dist/src/channel/session.js +46 -0
- package/dist/src/channel/types.d.ts +6 -5
- package/dist/src/chunks/client-CKsU8Li3.js +4 -0
- package/dist/src/chunks/{dev-authored-source-watcher-CG6kri3T.js → dev-authored-source-watcher-j7YWh2Gx.js} +1 -1
- package/dist/src/chunks/{host-CIU0NATc.js → host-DkTSR6YJ.js} +2 -2
- package/dist/src/chunks/{paths-CvbqpwTh.js → paths-Dwv0Eash.js} +22 -22
- package/dist/src/chunks/{prewarm-C_Vd0JR7.js → prewarm-CQYfka30.js} +1 -1
- package/dist/src/cli/commands/info.js +1 -1
- package/dist/src/cli/dev/repl.js +1 -1
- package/dist/src/cli/run.js +1 -1
- package/dist/src/client/client.js +2 -1
- package/dist/src/client/index.d.ts +3 -0
- package/dist/src/client/index.js +1 -0
- package/dist/src/client/message-reducer-types.d.ts +130 -0
- package/dist/src/client/message-reducer-types.js +1 -0
- package/dist/src/client/message-reducer.d.ts +14 -0
- package/dist/src/client/message-reducer.js +462 -0
- package/dist/src/client/open-stream.js +2 -4
- package/dist/src/client/reducer.d.ts +63 -0
- package/dist/src/client/reducer.js +1 -0
- package/dist/src/client/session.js +3 -5
- package/dist/src/client/url.d.ts +8 -0
- package/dist/src/client/url.js +34 -0
- package/dist/src/compiler/module-map.js +12 -0
- package/dist/src/evals/cli/eval.js +1 -1
- package/dist/src/execution/sandbox/bindings/vercel.d.ts +2 -2
- package/dist/src/execution/sandbox/bindings/vercel.js +1 -34
- package/dist/src/execution/workflow-entry.js +35 -31
- package/dist/src/execution/workflow-steps.d.ts +16 -0
- package/dist/src/execution/workflow-steps.js +32 -4
- package/dist/src/harness/attachment-staging.js +2 -1
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/public/channels/slack/api.d.ts +13 -8
- package/dist/src/public/channels/slack/api.js +31 -17
- package/dist/src/public/channels/slack/index.d.ts +2 -2
- package/dist/src/public/channels/slack/index.js +1 -0
- package/dist/src/public/channels/slack/interactions.js +3 -3
- package/dist/src/public/channels/slack/slackChannel.d.ts +5 -3
- package/dist/src/public/channels/slack/slackChannel.js +26 -15
- package/dist/src/public/channels/twilio/api.d.ts +9 -0
- package/dist/src/public/channels/twilio/api.js +11 -0
- package/dist/src/public/channels/twilio/index.d.ts +1 -1
- package/dist/src/public/channels/twilio/index.js +1 -1
- package/dist/src/public/channels/twilio/twilioChannel.d.ts +2 -0
- package/dist/src/public/channels/twilio/twilioChannel.js +8 -11
- package/dist/src/public/definitions/defineChannel.d.ts +9 -1
- package/dist/src/public/definitions/defineChannel.js +7 -11
- package/dist/src/public/definitions/sandbox.d.ts +2 -3
- package/dist/src/public/sandbox/backends/vercel.d.ts +4 -4
- package/dist/src/public/sandbox/backends/vercel.js +2 -2
- package/dist/src/public/sandbox/index.d.ts +1 -1
- package/dist/src/public/sandbox/vercel-sandbox.d.ts +3 -28
- package/dist/src/react/index.d.ts +3 -0
- package/dist/src/react/index.js +3 -0
- package/dist/src/react/use-ash-agent.d.ts +79 -0
- package/dist/src/react/use-ash-agent.js +330 -0
- package/dist/src/runtime/types.d.ts +1 -2
- package/dist/src/shared/sandbox-backend.d.ts +4 -4
- package/dist/src/shared/sandbox-definition.d.ts +6 -6
- package/package.json +15 -2
- package/dist/src/chunks/client-BeZ_W7vl.js +0 -4
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createAshMessageStreamRoutePath } from "#protocol/routes.js";
|
|
2
2
|
import { ClientError } from "#client/client-error.js";
|
|
3
3
|
import { isStreamDisconnectError, readNdjsonStream } from "#client/ndjson.js";
|
|
4
|
+
import { createClientUrl } from "#client/url.js";
|
|
4
5
|
/**
|
|
5
6
|
* Opens a durable NDJSON event stream with automatic reconnection on socket
|
|
6
7
|
* disconnection. Used by both {@link Client.openStream} and
|
|
@@ -10,10 +11,7 @@ export async function* openStreamIterable(input) {
|
|
|
10
11
|
let startIndex = input.startIndex;
|
|
11
12
|
let remainingReconnectAttempts = input.maxReconnectAttempts;
|
|
12
13
|
while (true) {
|
|
13
|
-
const url =
|
|
14
|
-
if (startIndex > 0) {
|
|
15
|
-
url.searchParams.set("startIndex", String(startIndex));
|
|
16
|
-
}
|
|
14
|
+
const url = createClientUrl(input.host, createAshMessageStreamRoutePath(input.sessionId), startIndex > 0 ? { startIndex: String(startIndex) } : undefined);
|
|
17
15
|
const headers = await input.resolveHeaders();
|
|
18
16
|
const response = await fetch(url, {
|
|
19
17
|
headers,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { HandleMessageStreamEvent } from "#protocol/message.js";
|
|
2
|
+
import type { InputResponse } from "#runtime/input/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Client-side reducer event emitted before Ash confirms a submitted user
|
|
5
|
+
* message with a `message.received` stream event.
|
|
6
|
+
*/
|
|
7
|
+
export interface ClientMessageSubmittedEvent {
|
|
8
|
+
readonly data: {
|
|
9
|
+
readonly createdAt: number;
|
|
10
|
+
readonly message: string;
|
|
11
|
+
readonly submissionId: string;
|
|
12
|
+
};
|
|
13
|
+
readonly type: "client.message.submitted";
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Client-side reducer event emitted when a submitted user message fails before
|
|
17
|
+
* Ash confirms it with a `message.received` stream event.
|
|
18
|
+
*/
|
|
19
|
+
export interface ClientMessageFailedEvent {
|
|
20
|
+
readonly data: {
|
|
21
|
+
readonly createdAt: number;
|
|
22
|
+
readonly error: {
|
|
23
|
+
readonly message: string;
|
|
24
|
+
};
|
|
25
|
+
readonly message: string;
|
|
26
|
+
readonly submissionId: string;
|
|
27
|
+
};
|
|
28
|
+
readonly type: "client.message.failed";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Client-side reducer event emitted when the client submits HITL responses for
|
|
32
|
+
* pending input requests.
|
|
33
|
+
*/
|
|
34
|
+
export interface ClientInputRespondedEvent {
|
|
35
|
+
readonly data: {
|
|
36
|
+
readonly createdAt: number;
|
|
37
|
+
readonly responses: readonly InputResponse[];
|
|
38
|
+
};
|
|
39
|
+
readonly type: "client.input.responded";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Event consumed by Ash agent reducers.
|
|
43
|
+
*
|
|
44
|
+
* Server events are authoritative Ash stream events. They include text,
|
|
45
|
+
* reasoning, tool/action requests and results, HITL input requests, connection
|
|
46
|
+
* authorization events, subagent events, and session lifecycle events. Client
|
|
47
|
+
* events are projection-only events created by client state machines for local
|
|
48
|
+
* UI state such as optimistic user messages and submitted HITL responses.
|
|
49
|
+
*/
|
|
50
|
+
export type AshAgentReducerEvent = ClientInputRespondedEvent | ClientMessageFailedEvent | ClientMessageSubmittedEvent | HandleMessageStreamEvent;
|
|
51
|
+
/**
|
|
52
|
+
* Projects Ash stream events into accumulated consumer data.
|
|
53
|
+
*/
|
|
54
|
+
export interface AshAgentReducer<TData> {
|
|
55
|
+
/**
|
|
56
|
+
* Creates the initial projection state.
|
|
57
|
+
*/
|
|
58
|
+
initial(): TData;
|
|
59
|
+
/**
|
|
60
|
+
* Applies one server or client projection event to the current projection.
|
|
61
|
+
*/
|
|
62
|
+
reduce(data: TData, event: AshAgentReducerEvent): TData;
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -5,6 +5,7 @@ import { MessageResponse } from "#client/message-response.js";
|
|
|
5
5
|
import { isStreamDisconnectError, readNdjsonStream } from "#client/ndjson.js";
|
|
6
6
|
import { openStreamIterable } from "#client/open-stream.js";
|
|
7
7
|
import { advanceSession } from "#client/session-utils.js";
|
|
8
|
+
import { createClientUrl } from "#client/url.js";
|
|
8
9
|
/**
|
|
9
10
|
* One conversation with an Ash agent.
|
|
10
11
|
*
|
|
@@ -84,7 +85,7 @@ export class ClientSession {
|
|
|
84
85
|
const routePath = session.sessionId
|
|
85
86
|
? createAshContinueSessionRoutePath(session.sessionId)
|
|
86
87
|
: ASH_CREATE_SESSION_ROUTE_PATH;
|
|
87
|
-
const url =
|
|
88
|
+
const url = createClientUrl(this.#context.host, routePath);
|
|
88
89
|
const headers = await this.#context.resolveHeaders(options?.headers);
|
|
89
90
|
headers.set("content-type", "application/json");
|
|
90
91
|
const body = createHandleMessageBody({
|
|
@@ -160,10 +161,7 @@ export class ClientSession {
|
|
|
160
161
|
}
|
|
161
162
|
}
|
|
162
163
|
async #openStreamBody(sessionId, startIndex, signal) {
|
|
163
|
-
const url =
|
|
164
|
-
if (startIndex > 0) {
|
|
165
|
-
url.searchParams.set("startIndex", String(startIndex));
|
|
166
|
-
}
|
|
164
|
+
const url = createClientUrl(this.#context.host, createAshMessageStreamRoutePath(sessionId), startIndex > 0 ? { startIndex: String(startIndex) } : undefined);
|
|
167
165
|
const headers = await this.#context.resolveHeaders();
|
|
168
166
|
const response = await fetch(url, {
|
|
169
167
|
headers,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a fetchable URL from a caller-provided host and an Ash route path.
|
|
3
|
+
*
|
|
4
|
+
* `host` may be an absolute origin (`https://agent.example.com`) or a
|
|
5
|
+
* same-origin prefix (`/api`). Prefixes are important for browser clients that
|
|
6
|
+
* talk to an app-owned proxy instead of the Ash deployment directly.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createClientUrl(host: string, routePath: string, searchParams?: Readonly<Record<string, string>>): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a fetchable URL from a caller-provided host and an Ash route path.
|
|
3
|
+
*
|
|
4
|
+
* `host` may be an absolute origin (`https://agent.example.com`) or a
|
|
5
|
+
* same-origin prefix (`/api`). Prefixes are important for browser clients that
|
|
6
|
+
* talk to an app-owned proxy instead of the Ash deployment directly.
|
|
7
|
+
*/
|
|
8
|
+
export function createClientUrl(host, routePath, searchParams) {
|
|
9
|
+
const normalizedRoute = routePath.startsWith("/") ? routePath : `/${routePath}`;
|
|
10
|
+
const search = formatSearch(searchParams);
|
|
11
|
+
if (isAbsoluteUrl(host)) {
|
|
12
|
+
const url = new URL(host);
|
|
13
|
+
const basePath = trimTrailingSlash(url.pathname);
|
|
14
|
+
url.pathname = `${basePath}${normalizedRoute}`;
|
|
15
|
+
url.search = search;
|
|
16
|
+
url.hash = "";
|
|
17
|
+
return url.toString();
|
|
18
|
+
}
|
|
19
|
+
return `${trimTrailingSlash(host)}${normalizedRoute}${search}`;
|
|
20
|
+
}
|
|
21
|
+
function isAbsoluteUrl(value) {
|
|
22
|
+
return /^[a-z][a-z\d+\-.]*:/i.test(value);
|
|
23
|
+
}
|
|
24
|
+
function trimTrailingSlash(value) {
|
|
25
|
+
if (value === "/")
|
|
26
|
+
return "";
|
|
27
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
28
|
+
}
|
|
29
|
+
function formatSearch(searchParams) {
|
|
30
|
+
if (!searchParams || Object.keys(searchParams).length === 0) {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
return `?${new URLSearchParams(searchParams).toString()}`;
|
|
34
|
+
}
|
|
@@ -127,6 +127,18 @@ export function collectModuleRefsForManifest(manifest) {
|
|
|
127
127
|
sourceId: hook.sourceId,
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
|
+
for (const schedule of manifest.schedules) {
|
|
131
|
+
// Only `run`-handler schedules need their source loaded at dispatch
|
|
132
|
+
// time. Markdown schedules execute from `manifest.markdown`.
|
|
133
|
+
if (schedule.sourceKind !== "module" || !schedule.hasRun) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
moduleSourceRefs.set(schedule.sourceId, {
|
|
137
|
+
sourceKind: "module",
|
|
138
|
+
logicalPath: schedule.logicalPath,
|
|
139
|
+
sourceId: schedule.sourceId,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
130
142
|
if (manifest.sandbox !== null) {
|
|
131
143
|
moduleSourceRefs.set(manifest.sandbox.sourceId, {
|
|
132
144
|
exportName: manifest.sandbox.exportName,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e}from"../../chunks/paths-
|
|
1
|
+
import{n as e}from"../../chunks/paths-Dwv0Eash.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-DkTSR6YJ.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,7 +1,7 @@
|
|
|
1
1
|
import type * as VercelSandboxSdk from "#compiled/@vercel/sandbox/index.js";
|
|
2
2
|
import type { Sandbox as SdkSandbox } from "#compiled/@vercel/sandbox/index.js";
|
|
3
3
|
import type { SandboxBackend } from "#public/definitions/sandbox-backend.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { VercelSandboxBootstrapUseOptions, VercelSandboxSessionUseOptions } from "#public/sandbox/vercel-sandbox.js";
|
|
5
5
|
type VercelSandboxModule = typeof VercelSandboxSdk;
|
|
6
6
|
/**
|
|
7
7
|
* User-controllable subset of `Sandbox.create` parameters.
|
|
@@ -18,5 +18,5 @@ export interface CreateVercelSandboxBackendInput {
|
|
|
18
18
|
/**
|
|
19
19
|
* Creates the Vercel-backed sandbox backend.
|
|
20
20
|
*/
|
|
21
|
-
export declare function createVercelSandboxBackend(input?: CreateVercelSandboxBackendInput): SandboxBackend<
|
|
21
|
+
export declare function createVercelSandboxBackend(input?: CreateVercelSandboxBackendInput): SandboxBackend<VercelSandboxBootstrapUseOptions, VercelSandboxSessionUseOptions>;
|
|
22
22
|
export {};
|