@todoist/automations-sdk 0.0.0 → 1.0.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.
- package/README.md +20 -33
- package/dist/authoring.d.ts +76 -0
- package/dist/authoring.js +162 -0
- package/dist/automations-api-CLcJvEjY.d.ts +6093 -0
- package/dist/base-client.d.ts +2 -0
- package/dist/base-client.js +1 -0
- package/dist/chat-turn.d.ts +314 -0
- package/dist/chat-turn.js +710 -0
- package/dist/connection-selection-BjNp9A-d.js +226 -0
- package/dist/connection-selection-C9yMkCvL.d.ts +29 -0
- package/dist/context-CxEkwuyE.js +109 -0
- package/dist/context-DsrXdqq8.d.ts +65 -0
- package/dist/index.d.ts +1060 -0
- package/dist/index.js +6030 -0
- package/dist/request-error-DoLXUa2p.js +34 -0
- package/dist/webhook-event-subscription.constants-CBmz0huQ.js +31 -0
- package/dist/webhooks.d.ts +52 -0
- package/dist/webhooks.js +83 -0
- package/package.json +83 -8
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
import { a as chatStreamRequestErrorResponseSchema, l as getChatBillingRefusal, p as isShownChatAcknowledgement, t as connectionAccountLabel } from "./connection-selection-BjNp9A-d.js";
|
|
2
|
+
import { n as AutomationsRequestError, t as AutomationsNetworkError } from "./request-error-DoLXUa2p.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { DefaultChatTransport, readUIMessageStream } from "ai";
|
|
5
|
+
//#region ../core/dist/esm/schemas/automation-preview-artifact.schema.js
|
|
6
|
+
const automationPreviewSources = ["synthetic", "run"];
|
|
7
|
+
const automationPreviewKinds = [
|
|
8
|
+
"content",
|
|
9
|
+
"action",
|
|
10
|
+
"none",
|
|
11
|
+
"error"
|
|
12
|
+
];
|
|
13
|
+
const automationPreviewGenerationKinds = [
|
|
14
|
+
"content",
|
|
15
|
+
"action",
|
|
16
|
+
"none"
|
|
17
|
+
];
|
|
18
|
+
const automationPreviewSourceSchema = z.enum(automationPreviewSources);
|
|
19
|
+
const automationPreviewKindSchema = z.enum(automationPreviewKinds);
|
|
20
|
+
const automationPreviewGenerationKindSchema = z.enum(automationPreviewGenerationKinds);
|
|
21
|
+
const automationPreviewArtifactSchema = z.object({
|
|
22
|
+
source: automationPreviewSourceSchema,
|
|
23
|
+
kind: automationPreviewKindSchema,
|
|
24
|
+
manifestHash: z.string().regex(/^[a-f0-9]{64}$/, "manifestHash must be a sha256 hex digest"),
|
|
25
|
+
title: z.string().trim().min(1),
|
|
26
|
+
summary: z.string().trim().min(1),
|
|
27
|
+
markdown: z.string().trim().min(1).nullable()
|
|
28
|
+
}).superRefine((artifact, ctx) => {
|
|
29
|
+
if (artifact.kind === "content" && artifact.markdown == null) ctx.addIssue({
|
|
30
|
+
code: z.ZodIssueCode.custom,
|
|
31
|
+
message: "content artifacts must include markdown",
|
|
32
|
+
path: ["markdown"]
|
|
33
|
+
});
|
|
34
|
+
if (artifact.kind !== "content" && artifact.markdown != null) ctx.addIssue({
|
|
35
|
+
code: z.ZodIssueCode.custom,
|
|
36
|
+
message: "only content artifacts may include markdown",
|
|
37
|
+
path: ["markdown"]
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
z.object({
|
|
41
|
+
kind: automationPreviewGenerationKindSchema,
|
|
42
|
+
title: z.string().trim().min(1),
|
|
43
|
+
summary: z.string().trim().min(1),
|
|
44
|
+
markdown: z.string().trim().min(1).nullish()
|
|
45
|
+
});
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region ../core/dist/esm/chat-tools/complete-automation-draft.js
|
|
48
|
+
const CompleteAutomationDraftActionSchema = z.enum(["create", "update"]);
|
|
49
|
+
const CompleteAutomationDraftInputSchema = z.object({
|
|
50
|
+
draftId: z.string().min(1).describe("The draft ID to generate an automation preview for."),
|
|
51
|
+
language: z.string().trim().min(1).optional().describe("Optional human language name for the preview. When relevant, pass the language used in the current user conversation.")
|
|
52
|
+
});
|
|
53
|
+
const CompleteAutomationDraftOutputSchema = z.union([z.object({
|
|
54
|
+
success: z.literal(true),
|
|
55
|
+
message: z.string().min(1),
|
|
56
|
+
instructions: z.string().min(1),
|
|
57
|
+
draftId: z.string().min(1),
|
|
58
|
+
action: CompleteAutomationDraftActionSchema,
|
|
59
|
+
artifact: automationPreviewArtifactSchema
|
|
60
|
+
}), z.object({
|
|
61
|
+
success: z.literal(false),
|
|
62
|
+
message: z.string().min(1),
|
|
63
|
+
instructions: z.string().min(1),
|
|
64
|
+
draftId: z.string().min(1)
|
|
65
|
+
})]);
|
|
66
|
+
z.object({
|
|
67
|
+
type: z.literal("tool-complete-automation-draft"),
|
|
68
|
+
toolCallId: z.string().min(1),
|
|
69
|
+
state: z.literal("input-available"),
|
|
70
|
+
input: CompleteAutomationDraftInputSchema
|
|
71
|
+
});
|
|
72
|
+
z.object({
|
|
73
|
+
type: z.literal("tool-complete-automation-draft"),
|
|
74
|
+
toolCallId: z.string().min(1),
|
|
75
|
+
state: z.literal("output-available"),
|
|
76
|
+
input: CompleteAutomationDraftInputSchema,
|
|
77
|
+
output: CompleteAutomationDraftOutputSchema
|
|
78
|
+
});
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region ../core/dist/esm/chat-tools/publish-draft.js
|
|
81
|
+
const PublishDraftInputSchema = z.object({
|
|
82
|
+
draftId: z.string().min(1).describe("The draft ID to preview and publish."),
|
|
83
|
+
language: z.string().trim().min(1).optional().describe("Optional human language name for the preview. When relevant, pass the language used in the current user conversation.")
|
|
84
|
+
});
|
|
85
|
+
const PublishDraftOutputSchema = z.union([z.object({
|
|
86
|
+
success: z.literal(true),
|
|
87
|
+
message: z.string().min(1),
|
|
88
|
+
instructions: z.string().min(1),
|
|
89
|
+
draftId: z.string().min(1),
|
|
90
|
+
action: CompleteAutomationDraftActionSchema,
|
|
91
|
+
automationId: z.string().min(1),
|
|
92
|
+
previewArtifact: automationPreviewArtifactSchema.optional()
|
|
93
|
+
}), z.object({
|
|
94
|
+
success: z.literal(false),
|
|
95
|
+
message: z.string().min(1),
|
|
96
|
+
instructions: z.string().min(1),
|
|
97
|
+
draftId: z.string().min(1),
|
|
98
|
+
action: CompleteAutomationDraftActionSchema.optional(),
|
|
99
|
+
previewArtifact: automationPreviewArtifactSchema.optional()
|
|
100
|
+
})]);
|
|
101
|
+
z.object({
|
|
102
|
+
type: z.literal("tool-publish-draft"),
|
|
103
|
+
toolCallId: z.string().min(1),
|
|
104
|
+
state: z.literal("input-available"),
|
|
105
|
+
input: PublishDraftInputSchema
|
|
106
|
+
});
|
|
107
|
+
const toolPublishDraftResultSchema = z.object({
|
|
108
|
+
type: z.literal("tool-publish-draft"),
|
|
109
|
+
toolCallId: z.string().min(1),
|
|
110
|
+
state: z.literal("output-available"),
|
|
111
|
+
input: PublishDraftInputSchema,
|
|
112
|
+
output: PublishDraftOutputSchema
|
|
113
|
+
});
|
|
114
|
+
function isPublishDraftCandidate(part) {
|
|
115
|
+
return typeof part === "object" && part !== null && "type" in part && part.type === "tool-publish-draft";
|
|
116
|
+
}
|
|
117
|
+
function isPublishDraftResult(part) {
|
|
118
|
+
if (!isPublishDraftCandidate(part)) return false;
|
|
119
|
+
return toolPublishDraftResultSchema.safeParse(part).success;
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region ../core/dist/esm/chat-tools/prompt-enable-push.js
|
|
123
|
+
const PromptEnablePushInputSchema = z.object({ draftId: z.string().min(1).optional().describe("Optional draft ID currently being edited, if available") });
|
|
124
|
+
const PromptEnablePushOutputSchema = z.object({
|
|
125
|
+
enabled: z.boolean().describe("Whether the user finished enabling push notifications and has at least one active subscription."),
|
|
126
|
+
deviceCount: z.number().int().min(0).describe("Number of active push devices the user has registered."),
|
|
127
|
+
message: z.string().min(1).describe("Human-readable status describing the outcome of the prompt.")
|
|
128
|
+
});
|
|
129
|
+
const toolPromptEnablePushSchema = z.object({
|
|
130
|
+
type: z.literal("tool-prompt-enable-push-notifications"),
|
|
131
|
+
toolCallId: z.string().min(1),
|
|
132
|
+
state: z.literal("input-available"),
|
|
133
|
+
input: PromptEnablePushInputSchema
|
|
134
|
+
});
|
|
135
|
+
z.object({
|
|
136
|
+
type: z.literal("tool-prompt-enable-push-notifications"),
|
|
137
|
+
toolCallId: z.string().min(1),
|
|
138
|
+
state: z.literal("output-available"),
|
|
139
|
+
input: PromptEnablePushInputSchema,
|
|
140
|
+
output: PromptEnablePushOutputSchema
|
|
141
|
+
});
|
|
142
|
+
function isPromptEnablePush(part) {
|
|
143
|
+
return toolPromptEnablePushSchema.safeParse(part).success;
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region ../core/dist/esm/chat-tools/select-connections.js
|
|
147
|
+
const SelectConnectionTypeSchema = z.enum(["mcp", "trigger"]);
|
|
148
|
+
const ConnectionItemSchema = z.object({
|
|
149
|
+
connectorId: z.string().min(1).describe("The connector ID to select a connection for"),
|
|
150
|
+
serviceName: z.string().min(1).describe("The human-readable name of the service"),
|
|
151
|
+
connectionType: SelectConnectionTypeSchema.describe("Type of connection needed: \"mcp\" for actions, \"trigger\" for events")
|
|
152
|
+
});
|
|
153
|
+
const SelectConnectionsInputSchema = z.object({
|
|
154
|
+
draftId: z.string().min(1).describe("The draft ID these connections belong to"),
|
|
155
|
+
connections: z.array(ConnectionItemSchema).min(1).describe("List of connections needed for this automation")
|
|
156
|
+
});
|
|
157
|
+
const ConnectionResultItemSchema = z.union([z.object({
|
|
158
|
+
type: z.literal("success"),
|
|
159
|
+
connectionId: z.string().min(1),
|
|
160
|
+
connectorId: z.string().min(1),
|
|
161
|
+
connectionType: SelectConnectionTypeSchema
|
|
162
|
+
}), z.object({
|
|
163
|
+
type: z.literal("error"),
|
|
164
|
+
message: z.string().min(1),
|
|
165
|
+
connectorId: z.string().min(1),
|
|
166
|
+
connectionType: SelectConnectionTypeSchema
|
|
167
|
+
})]);
|
|
168
|
+
const SelectConnectionsOutputSchema = z.array(ConnectionResultItemSchema);
|
|
169
|
+
const toolSelectConnectionsSchema = z.object({
|
|
170
|
+
type: z.literal("tool-select-connections"),
|
|
171
|
+
toolCallId: z.string().min(1),
|
|
172
|
+
state: z.literal("input-available"),
|
|
173
|
+
input: SelectConnectionsInputSchema
|
|
174
|
+
});
|
|
175
|
+
z.object({
|
|
176
|
+
type: z.literal("tool-select-connections"),
|
|
177
|
+
toolCallId: z.string().min(1),
|
|
178
|
+
state: z.literal("output-available"),
|
|
179
|
+
input: SelectConnectionsInputSchema,
|
|
180
|
+
output: SelectConnectionsOutputSchema
|
|
181
|
+
});
|
|
182
|
+
function isSelectConnections(part) {
|
|
183
|
+
return toolSelectConnectionsSchema.safeParse(part).success;
|
|
184
|
+
}
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region src/chat-turn/connection-requirements.ts
|
|
187
|
+
const CONNECTION_LOOKUP_CONCURRENCY = 4;
|
|
188
|
+
function deduplicateConnectionRequirements(requirements) {
|
|
189
|
+
const seen = /* @__PURE__ */ new Set();
|
|
190
|
+
return requirements.filter((requirement) => {
|
|
191
|
+
const key = JSON.stringify([requirement.connectorId, requirement.connectionType]);
|
|
192
|
+
if (seen.has(key)) return false;
|
|
193
|
+
seen.add(key);
|
|
194
|
+
return true;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async function loadConnectionRequirements(connections, requirements, options) {
|
|
198
|
+
return mapWithConcurrency(deduplicateConnectionRequirements(requirements), CONNECTION_LOOKUP_CONCURRENCY, async (requirement) => {
|
|
199
|
+
const response = await connections.list({
|
|
200
|
+
connectorId: requirement.connectorId,
|
|
201
|
+
capability: requirement.connectionType,
|
|
202
|
+
status: "authorized"
|
|
203
|
+
}, options);
|
|
204
|
+
return {
|
|
205
|
+
...requirement,
|
|
206
|
+
availableConnections: response.connections.map((connection) => ({
|
|
207
|
+
id: connection.id,
|
|
208
|
+
label: connectionAccountLabel({
|
|
209
|
+
label: connection.accountDisplayLabel ?? connection.name ?? connection.id,
|
|
210
|
+
mcpServerUrl: connection.mcpServerUrl
|
|
211
|
+
})
|
|
212
|
+
}))
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
async function mapWithConcurrency(items, concurrency, map) {
|
|
217
|
+
const results = new Array(items.length);
|
|
218
|
+
let next = 0;
|
|
219
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
220
|
+
while (next < items.length) {
|
|
221
|
+
const index = next;
|
|
222
|
+
next += 1;
|
|
223
|
+
results[index] = await map(items[index]);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
await Promise.all(workers);
|
|
227
|
+
return results;
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/chat-turn/connection-setup.ts
|
|
231
|
+
async function startConnectionSetup(connections, request, options) {
|
|
232
|
+
const body = {
|
|
233
|
+
...request.automationIds?.length ? { automationIds: [...request.automationIds] } : {},
|
|
234
|
+
...request.tenant !== void 0 ? { tenant: request.tenant } : {},
|
|
235
|
+
...request.serverUrl !== void 0 ? { serverUrl: request.serverUrl } : {}
|
|
236
|
+
};
|
|
237
|
+
const created = await connections.create({
|
|
238
|
+
connectorId: request.connectorId,
|
|
239
|
+
capability: request.capability,
|
|
240
|
+
...Object.keys(body).length > 0 ? { body } : {}
|
|
241
|
+
}, options);
|
|
242
|
+
return {
|
|
243
|
+
connectionId: created.connectionId,
|
|
244
|
+
setup: created.setup,
|
|
245
|
+
resumed: false
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const INPUT_REQUIRED = "input_required";
|
|
249
|
+
function getConnectionInputRequiredMessage(error) {
|
|
250
|
+
if (!(error instanceof AutomationsRequestError) || error.httpStatusCode !== 400) return;
|
|
251
|
+
const data = error.responseData;
|
|
252
|
+
if (data?.code !== INPUT_REQUIRED) return;
|
|
253
|
+
return typeof data.message === "string" ? data.message : error.message;
|
|
254
|
+
}
|
|
255
|
+
function resumeConnectionSetup(connection) {
|
|
256
|
+
return {
|
|
257
|
+
connectionId: connection.id,
|
|
258
|
+
setup: connection.setup,
|
|
259
|
+
resumed: true
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function summarizeConnectionSetup(setup) {
|
|
263
|
+
if (setup?.kind === "oauth") return {
|
|
264
|
+
setupKind: "oauth",
|
|
265
|
+
authorizationUrl: setup.authorizationUrl
|
|
266
|
+
};
|
|
267
|
+
if (setup?.kind === "managed") return {
|
|
268
|
+
setupKind: "managed",
|
|
269
|
+
instructions: setup.instructions,
|
|
270
|
+
code: setup.code,
|
|
271
|
+
deepLinkUrl: setup.deepLinkUrl,
|
|
272
|
+
expiresAt: setup.expiresAt
|
|
273
|
+
};
|
|
274
|
+
return { setupKind: "none" };
|
|
275
|
+
}
|
|
276
|
+
function getConnectionSetupUrl(setup) {
|
|
277
|
+
if (setup?.kind === "oauth") return setup.authorizationUrl;
|
|
278
|
+
if (setup?.kind === "managed") return setup.deepLinkUrl;
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/chat-turn/messages.ts
|
|
283
|
+
const persistedToolPartSchema = z.object({
|
|
284
|
+
type: z.string().trim().min(6).refine((type) => type.startsWith("tool-")),
|
|
285
|
+
toolCallId: z.string().trim().min(1),
|
|
286
|
+
state: z.enum([
|
|
287
|
+
"input-streaming",
|
|
288
|
+
"input-available",
|
|
289
|
+
"output-available",
|
|
290
|
+
"output-error"
|
|
291
|
+
])
|
|
292
|
+
}).passthrough();
|
|
293
|
+
function createUserMessage(text) {
|
|
294
|
+
return {
|
|
295
|
+
id: globalThis.crypto.randomUUID(),
|
|
296
|
+
role: "user",
|
|
297
|
+
parts: [{
|
|
298
|
+
type: "text",
|
|
299
|
+
text
|
|
300
|
+
}]
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function toUiMessage(message) {
|
|
304
|
+
if (message.role !== "user" && message.role !== "assistant" && message.role !== "system") throw new Error(`Unsupported persisted chat role: ${message.role}`);
|
|
305
|
+
if (!Array.isArray(message.parts)) throw new Error(`Persisted chat message ${message.messageId} has invalid parts.`);
|
|
306
|
+
return {
|
|
307
|
+
id: message.messageId,
|
|
308
|
+
role: message.role,
|
|
309
|
+
parts: normalizePersistedMessageParts(message.parts)
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function normalizePersistedMessageParts(parts) {
|
|
313
|
+
return parts.filter((part) => isTextPart(part) || isStepStartPart(part) || isReasoningPart(part) || persistedToolPartSchema.safeParse(part).success || isShownChatAcknowledgement(part));
|
|
314
|
+
}
|
|
315
|
+
function isTextPart(part) {
|
|
316
|
+
if (typeof part !== "object" || part === null) return false;
|
|
317
|
+
const candidate = part;
|
|
318
|
+
return candidate.type === "text" && typeof candidate.text === "string";
|
|
319
|
+
}
|
|
320
|
+
function isStepStartPart(part) {
|
|
321
|
+
if (typeof part !== "object" || part === null) return false;
|
|
322
|
+
return part.type === "step-start";
|
|
323
|
+
}
|
|
324
|
+
function isReasoningPart(part) {
|
|
325
|
+
if (typeof part !== "object" || part === null) return false;
|
|
326
|
+
const candidate = part;
|
|
327
|
+
return candidate.type === "reasoning" && typeof candidate.text === "string" && (candidate.state === void 0 || candidate.state === "streaming" || candidate.state === "done");
|
|
328
|
+
}
|
|
329
|
+
function getVisibleText(message) {
|
|
330
|
+
return message.parts.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
331
|
+
}
|
|
332
|
+
function getPendingClientAction(message) {
|
|
333
|
+
for (let index = message.parts.length - 1; index >= 0; index -= 1) {
|
|
334
|
+
const part = message.parts[index];
|
|
335
|
+
if (isSelectConnections(part)) return {
|
|
336
|
+
kind: "select_connections",
|
|
337
|
+
message,
|
|
338
|
+
toolCallId: part.toolCallId,
|
|
339
|
+
draftId: part.input.draftId,
|
|
340
|
+
connections: part.input.connections
|
|
341
|
+
};
|
|
342
|
+
if (isPromptEnablePush(part)) return {
|
|
343
|
+
kind: "enable_push",
|
|
344
|
+
message,
|
|
345
|
+
toolCallId: part.toolCallId,
|
|
346
|
+
draftId: part.input.draftId
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
function getPendingActionFromMessages(messages) {
|
|
352
|
+
const latestMessage = messages.at(-1);
|
|
353
|
+
if (!latestMessage || latestMessage.role !== "assistant") return null;
|
|
354
|
+
return getPendingClientAction(toUiMessage(latestMessage));
|
|
355
|
+
}
|
|
356
|
+
function getPublishedAutomationId(message) {
|
|
357
|
+
for (let index = message.parts.length - 1; index >= 0; index -= 1) {
|
|
358
|
+
const part = message.parts[index];
|
|
359
|
+
if (isPublishDraftResult(part) && part.output.success) return part.output.automationId;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function createEnablePushContinuation(action, output) {
|
|
363
|
+
return {
|
|
364
|
+
id: action.message.id,
|
|
365
|
+
role: "assistant",
|
|
366
|
+
parts: [{
|
|
367
|
+
type: "tool-prompt-enable-push-notifications",
|
|
368
|
+
toolCallId: action.toolCallId,
|
|
369
|
+
state: "output-available",
|
|
370
|
+
output
|
|
371
|
+
}]
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function createSelectConnectionsContinuation(action, output) {
|
|
375
|
+
return {
|
|
376
|
+
id: action.message.id,
|
|
377
|
+
role: "assistant",
|
|
378
|
+
parts: [{
|
|
379
|
+
type: "tool-select-connections",
|
|
380
|
+
toolCallId: action.toolCallId,
|
|
381
|
+
state: "output-available",
|
|
382
|
+
output
|
|
383
|
+
}]
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
//#endregion
|
|
387
|
+
//#region src/chat-turn/types.ts
|
|
388
|
+
var ChatTurnInputError = class extends Error {
|
|
389
|
+
code;
|
|
390
|
+
constructor(code, message) {
|
|
391
|
+
super(message);
|
|
392
|
+
this.name = "ChatTurnInputError";
|
|
393
|
+
this.code = code;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/chat-turn/run-conversation-turn.ts
|
|
398
|
+
async function runConversationTurn(options) {
|
|
399
|
+
const { api, transport, conversationId, signal } = options;
|
|
400
|
+
const workspaceId = options.workspaceId ?? null;
|
|
401
|
+
const requestOptions = { signal };
|
|
402
|
+
const pending = await readPendingAction(options);
|
|
403
|
+
let nextMessage;
|
|
404
|
+
if (pending?.kind === "enable_push") {
|
|
405
|
+
if ("message" in options.input || options.input.selections.length > 0) throw new ChatTurnInputError("awaiting_push", "The conversation is waiting for the push notification prompt to be answered.");
|
|
406
|
+
nextMessage = await answerEnablePush(options, pending);
|
|
407
|
+
} else if ("message" in options.input) {
|
|
408
|
+
if (pending?.kind === "select_connections") throw new ChatTurnInputError("awaiting_selection", "The conversation is waiting for connection selection.");
|
|
409
|
+
nextMessage = createUserMessage(options.input.message);
|
|
410
|
+
} else {
|
|
411
|
+
const selections = options.input.selections;
|
|
412
|
+
if (pending?.kind !== "select_connections") throw new ChatTurnInputError(selections.length > 0 ? "not_awaiting_selection" : "input_required", selections.length > 0 ? "The conversation is not waiting for connection selection." : "A message is required to continue.");
|
|
413
|
+
const resolution = await resolveSelectConnections(options, pending, selections);
|
|
414
|
+
if (resolution.kind === "required") return actionRequired(conversationId, getVisibleText(pending.message), resolution.pendingAction);
|
|
415
|
+
nextMessage = resolution.continuation;
|
|
416
|
+
}
|
|
417
|
+
while (true) {
|
|
418
|
+
const response = await transport.streamTurn({
|
|
419
|
+
conversationId,
|
|
420
|
+
workspaceId,
|
|
421
|
+
message: nextMessage,
|
|
422
|
+
signal,
|
|
423
|
+
onText: options.onText,
|
|
424
|
+
onToolActivity: options.onToolActivity
|
|
425
|
+
});
|
|
426
|
+
const assistantText = getVisibleText(response);
|
|
427
|
+
const automationId = getPublishedAutomationId(response);
|
|
428
|
+
if (automationId) {
|
|
429
|
+
options.onPublished?.(automationId);
|
|
430
|
+
return {
|
|
431
|
+
conversationId,
|
|
432
|
+
outcome: "published",
|
|
433
|
+
assistantText,
|
|
434
|
+
automationId,
|
|
435
|
+
...await archiveConversation(api, conversationId, requestOptions)
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
const responseAction = getPendingClientAction(response);
|
|
439
|
+
if (responseAction?.kind === "select_connections") {
|
|
440
|
+
const resolution = await resolveSelectConnections(options, responseAction, []);
|
|
441
|
+
if (resolution.kind === "resolved") {
|
|
442
|
+
nextMessage = resolution.continuation;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
return actionRequired(conversationId, assistantText, resolution.pendingAction);
|
|
446
|
+
}
|
|
447
|
+
if (responseAction?.kind === "enable_push") return actionRequired(conversationId, assistantText, {
|
|
448
|
+
kind: "enable_push",
|
|
449
|
+
toolCallId: responseAction.toolCallId,
|
|
450
|
+
draftId: responseAction.draftId
|
|
451
|
+
});
|
|
452
|
+
return {
|
|
453
|
+
conversationId,
|
|
454
|
+
outcome: "awaiting_input",
|
|
455
|
+
assistantText
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
async function answerEnablePush(options, action) {
|
|
460
|
+
const { subscriptions } = await options.api.notifications.push.list({ signal: options.signal });
|
|
461
|
+
const deviceCount = subscriptions.length;
|
|
462
|
+
return createEnablePushContinuation(action, {
|
|
463
|
+
enabled: deviceCount > 0,
|
|
464
|
+
deviceCount,
|
|
465
|
+
message: deviceCount > 0 ? `Push notifications are enabled on ${deviceCount} device${deviceCount === 1 ? "" : "s"}.` : "The user has not enabled push notifications."
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
async function readPendingAction(options) {
|
|
469
|
+
if (options.pendingAction !== void 0) return options.pendingAction;
|
|
470
|
+
return getPendingActionFromMessages(await options.api.chat.getMessages(options.conversationId, { signal: options.signal }));
|
|
471
|
+
}
|
|
472
|
+
function actionRequired(conversationId, assistantText, pendingAction) {
|
|
473
|
+
return {
|
|
474
|
+
conversationId,
|
|
475
|
+
outcome: "action_required",
|
|
476
|
+
assistantText,
|
|
477
|
+
pendingAction
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
async function archiveConversation(api, conversationId, requestOptions) {
|
|
481
|
+
try {
|
|
482
|
+
if (!(await api.chat.updateConversation(conversationId, { isArchived: true }, requestOptions)).isArchived) throw new Error("The Automations API returned an active conversation.");
|
|
483
|
+
return { conversationArchived: true };
|
|
484
|
+
} catch (error) {
|
|
485
|
+
return {
|
|
486
|
+
conversationArchived: false,
|
|
487
|
+
conversationArchiveError: error instanceof Error ? error.message : String(error)
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async function resolveSelectConnections(options, action, selections) {
|
|
492
|
+
const { api, signal } = options;
|
|
493
|
+
const requestOptions = { signal };
|
|
494
|
+
const requirements = await loadConnectionRequirements(api.connections, action.connections, requestOptions);
|
|
495
|
+
assertSelectionsUnambiguous(requirements, selections);
|
|
496
|
+
const resolved = [];
|
|
497
|
+
for (const requirement of requirements) {
|
|
498
|
+
let connectionId = selections.find((selection) => selection.connectorId === requirement.connectorId && (!selection.connectionType || selection.connectionType === requirement.connectionType))?.connectionId;
|
|
499
|
+
let setup;
|
|
500
|
+
if (connectionId) {
|
|
501
|
+
if (!requirement.availableConnections.some((connection) => connection.id === connectionId)) throw new ChatTurnInputError("unknown_connection", `Connection ${connectionId} is not an authorized ${requirement.serviceName} connection.`);
|
|
502
|
+
} else if (requirement.availableConnections.length === 1) connectionId = requirement.availableConnections[0]?.id;
|
|
503
|
+
else if (requirement.availableConnections.length > 1) connectionId = await options.chooseConnection?.(requirement);
|
|
504
|
+
else if (options.authorizeConnection) connectionId = await options.authorizeConnection(requirement);
|
|
505
|
+
else setup = await startConnectionSetup(api.connections, {
|
|
506
|
+
connectorId: requirement.connectorId,
|
|
507
|
+
capability: requirement.connectionType
|
|
508
|
+
}, requestOptions).catch((error) => {
|
|
509
|
+
throw toConnectionInputRequiredError(error, requirement.serviceName) ?? error;
|
|
510
|
+
});
|
|
511
|
+
if (!connectionId) return {
|
|
512
|
+
kind: "required",
|
|
513
|
+
pendingAction: {
|
|
514
|
+
kind: "select_connections",
|
|
515
|
+
toolCallId: action.toolCallId,
|
|
516
|
+
draftId: action.draftId,
|
|
517
|
+
requirements: requirements.map((item) => item === requirement && setup ? {
|
|
518
|
+
...item,
|
|
519
|
+
setup
|
|
520
|
+
} : item)
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
resolved.push({
|
|
524
|
+
requirement,
|
|
525
|
+
connectionId
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
const output = [];
|
|
529
|
+
for (const item of resolved) {
|
|
530
|
+
const body = {
|
|
531
|
+
connectorId: item.requirement.connectorId,
|
|
532
|
+
connectionType: item.requirement.connectionType,
|
|
533
|
+
connectionId: item.connectionId
|
|
534
|
+
};
|
|
535
|
+
const workspaceId = options.workspaceId ?? null;
|
|
536
|
+
if (workspaceId === null) await api.automationDraftConnections.upsert(action.draftId, body, requestOptions);
|
|
537
|
+
else await api.workspaceAutomationDraftConnections.upsert(workspaceId, action.draftId, body, requestOptions);
|
|
538
|
+
output.push({
|
|
539
|
+
type: "success",
|
|
540
|
+
...body
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
kind: "resolved",
|
|
545
|
+
continuation: createSelectConnectionsContinuation(action, output)
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function assertSelectionsUnambiguous(requirements, selections) {
|
|
549
|
+
for (const selection of selections) {
|
|
550
|
+
if (selection.connectionType) continue;
|
|
551
|
+
if (new Set(requirements.filter((requirement) => requirement.connectorId === selection.connectorId).map((requirement) => requirement.connectionType)).size > 1) throw new ChatTurnInputError("ambiguous_selection", `Connection selection for ${selection.connectorId} is ambiguous. Specify ${selection.connectorId}:mcp or ${selection.connectorId}:trigger.`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function toConnectionInputRequiredError(error, serviceName) {
|
|
555
|
+
const message = getConnectionInputRequiredMessage(error);
|
|
556
|
+
if (message === void 0) return;
|
|
557
|
+
return new ChatTurnInputError("connection_input_required", `${serviceName} needs to be connected before the agent can continue. ${message}`);
|
|
558
|
+
}
|
|
559
|
+
//#endregion
|
|
560
|
+
//#region src/chat-turn/errors.ts
|
|
561
|
+
var ChatTurnError = class extends Error {
|
|
562
|
+
conversationId;
|
|
563
|
+
requestId;
|
|
564
|
+
stage;
|
|
565
|
+
response;
|
|
566
|
+
constructor({ conversationId, requestId, stage, cause }) {
|
|
567
|
+
const response = stage === "request" ? parseChatStreamRequestError(cause) : void 0;
|
|
568
|
+
super(response?.message ?? (cause instanceof Error ? cause.message : String(cause)), { cause });
|
|
569
|
+
this.name = "ChatTurnError";
|
|
570
|
+
this.conversationId = conversationId;
|
|
571
|
+
this.requestId = requestId;
|
|
572
|
+
this.stage = stage;
|
|
573
|
+
this.response = response;
|
|
574
|
+
}
|
|
575
|
+
get httpStatus() {
|
|
576
|
+
return this.response?.statusCode;
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
function parseChatStreamRequestError(error) {
|
|
580
|
+
if (!(error instanceof Error) || !error.message.trim().startsWith("{")) return void 0;
|
|
581
|
+
let body;
|
|
582
|
+
try {
|
|
583
|
+
body = JSON.parse(error.message);
|
|
584
|
+
} catch {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const parsed = chatStreamRequestErrorResponseSchema.safeParse(body);
|
|
588
|
+
return parsed.success ? parsed.data : void 0;
|
|
589
|
+
}
|
|
590
|
+
function getChatTurnBillingRefusal(error) {
|
|
591
|
+
return getChatBillingRefusal(error instanceof ChatTurnError ? error.cause : error);
|
|
592
|
+
}
|
|
593
|
+
function readFailedRequest(error) {
|
|
594
|
+
if (error instanceof ChatTurnError) return {
|
|
595
|
+
requestId: error.requestId,
|
|
596
|
+
...error.httpStatus !== void 0 ? { httpStatus: error.httpStatus } : {}
|
|
597
|
+
};
|
|
598
|
+
if (error instanceof AutomationsRequestError) return {
|
|
599
|
+
...error.requestId ? { requestId: error.requestId } : {},
|
|
600
|
+
httpStatus: error.httpStatusCode
|
|
601
|
+
};
|
|
602
|
+
return {};
|
|
603
|
+
}
|
|
604
|
+
function classifyChatTurnFailure(error) {
|
|
605
|
+
const { httpStatus } = readFailedRequest(error);
|
|
606
|
+
if (httpStatus !== void 0) return httpStatus < 500 ? "refused" : "broken";
|
|
607
|
+
return error instanceof ChatTurnError || error instanceof AutomationsNetworkError ? "broken" : "other";
|
|
608
|
+
}
|
|
609
|
+
//#endregion
|
|
610
|
+
//#region src/chat-turn/transport.ts
|
|
611
|
+
var ChatTurnTransport = class {
|
|
612
|
+
transport;
|
|
613
|
+
constructor({ token, baseUrl, headers, fetch }) {
|
|
614
|
+
const apiRoot = `${baseUrl.replace(/\/+$/, "")}/v1`;
|
|
615
|
+
this.transport = new DefaultChatTransport({
|
|
616
|
+
api: `${apiRoot}/chat`,
|
|
617
|
+
headers: {
|
|
618
|
+
Authorization: `Bearer ${token}`,
|
|
619
|
+
...headers
|
|
620
|
+
},
|
|
621
|
+
credentials: "include",
|
|
622
|
+
fetch,
|
|
623
|
+
prepareSendMessagesRequest({ messages, body, api }) {
|
|
624
|
+
const { workspaceId, ...rest } = body ?? {};
|
|
625
|
+
return {
|
|
626
|
+
api: workspaceId ? `${apiRoot}/workspaces/${encodeURIComponent(workspaceId)}/chat` : api,
|
|
627
|
+
body: {
|
|
628
|
+
...rest,
|
|
629
|
+
message: messages.at(-1)
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
async streamTurn({ conversationId, workspaceId = null, message, signal, onText, onToolActivity }) {
|
|
636
|
+
const requestId = globalThis.crypto.randomUUID();
|
|
637
|
+
const fail = (stage, cause) => signal?.aborted ? cause : new ChatTurnError({
|
|
638
|
+
conversationId,
|
|
639
|
+
requestId,
|
|
640
|
+
stage,
|
|
641
|
+
cause
|
|
642
|
+
});
|
|
643
|
+
let stream;
|
|
644
|
+
try {
|
|
645
|
+
stream = await this.transport.sendMessages({
|
|
646
|
+
trigger: "submit-message",
|
|
647
|
+
chatId: conversationId,
|
|
648
|
+
messageId: void 0,
|
|
649
|
+
messages: [message],
|
|
650
|
+
abortSignal: signal,
|
|
651
|
+
headers: { "X-Request-Id": requestId },
|
|
652
|
+
body: {
|
|
653
|
+
conversationId,
|
|
654
|
+
workspaceId
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
} catch (error) {
|
|
658
|
+
throw fail("request", error);
|
|
659
|
+
}
|
|
660
|
+
let finalMessage;
|
|
661
|
+
let emittedText = "";
|
|
662
|
+
const emittedToolStates = /* @__PURE__ */ new Set();
|
|
663
|
+
let callbackError;
|
|
664
|
+
const report = (callback) => {
|
|
665
|
+
try {
|
|
666
|
+
callback();
|
|
667
|
+
} catch (error) {
|
|
668
|
+
callbackError = { error };
|
|
669
|
+
throw error;
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
try {
|
|
673
|
+
for await (const currentMessage of readUIMessageStream({
|
|
674
|
+
stream,
|
|
675
|
+
terminateOnError: true
|
|
676
|
+
})) {
|
|
677
|
+
finalMessage = currentMessage;
|
|
678
|
+
const visibleText = getVisibleText(currentMessage);
|
|
679
|
+
if (visibleText.startsWith(emittedText)) {
|
|
680
|
+
const delta = visibleText.slice(emittedText.length);
|
|
681
|
+
if (delta) report(() => onText?.(delta));
|
|
682
|
+
emittedText = visibleText;
|
|
683
|
+
}
|
|
684
|
+
for (const part of currentMessage.parts) {
|
|
685
|
+
const activity = toToolActivity(part);
|
|
686
|
+
if (!activity) continue;
|
|
687
|
+
const key = `${activity.toolCallId}:${activity.state}`;
|
|
688
|
+
if (emittedToolStates.has(key)) continue;
|
|
689
|
+
emittedToolStates.add(key);
|
|
690
|
+
report(() => onToolActivity?.(activity));
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
} catch (error) {
|
|
694
|
+
if (callbackError) throw callbackError.error;
|
|
695
|
+
throw fail("stream", error);
|
|
696
|
+
}
|
|
697
|
+
if (!finalMessage) throw new Error("The Automations chat returned an empty response.");
|
|
698
|
+
return finalMessage;
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
function toToolActivity(part) {
|
|
702
|
+
if (!part.type.startsWith("tool-") || !("toolCallId" in part) || !("state" in part)) return null;
|
|
703
|
+
return {
|
|
704
|
+
tool: part.type.slice(5),
|
|
705
|
+
toolCallId: String(part.toolCallId),
|
|
706
|
+
state: String(part.state)
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
//#endregion
|
|
710
|
+
export { ChatTurnError, ChatTurnInputError, ChatTurnTransport, classifyChatTurnFailure, createEnablePushContinuation, createSelectConnectionsContinuation, createUserMessage, deduplicateConnectionRequirements, getChatTurnBillingRefusal, getConnectionInputRequiredMessage, getConnectionSetupUrl, getPendingActionFromMessages, getPendingClientAction, getPublishedAutomationId, getVisibleText, isReasoningPart, isStepStartPart, isTextPart, loadConnectionRequirements, normalizePersistedMessageParts, parseChatStreamRequestError, readFailedRequest, resumeConnectionSetup, runConversationTurn, startConnectionSetup, summarizeConnectionSetup, toUiMessage };
|