vessels-sdk 0.9.0 → 0.12.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 +35 -14
- package/dist/index.cjs +177 -31
- package/dist/index.d.cts +102 -11
- package/dist/index.d.ts +102 -11
- package/dist/index.js +177 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,11 +82,11 @@ Returns `{ ok: true, messageId: string, vesselId: string, createdAt: string }`.
|
|
|
82
82
|
| `labels` | `string[]` | Tags for filtering in the dashboard. Max 10, 50 chars each. Replaces the existing set on every push — send all labels you want, not just new ones. |
|
|
83
83
|
| `metadata` | `object` | Arbitrary JSON stored on the vessel, passed through in webhook callbacks. |
|
|
84
84
|
| `card` | `Card` | Structured key-value info attached to this message. `{ title: string, fields: [{ label, value }] }` |
|
|
85
|
-
| `interaction` | `Interaction` | Interactive prompt for the human (one of
|
|
85
|
+
| `interaction` | `Interaction` | Interactive prompt for the human (one of 4 types — see helpers below). Max one per message; immutable after the human responds. |
|
|
86
86
|
| `pinCard` | `Card \| null` | Persistent card pinned to the vessel header. Always visible above the message stream. Replaces any existing pinned card. Pass `null` to clear. |
|
|
87
87
|
| `attachments` | `Attachment[]` | Images or files to show in the message. Max 10. You host the files; Vessels renders them. `[{ type: 'image' \| 'file', url: string, filename?: string }]` |
|
|
88
88
|
| `suggestions` | `string[]` | Quick-reply chips shown below the message. Max 5. Tapping fills the text input. Disappear after the user sends any message. |
|
|
89
|
-
| `previewUrl` | `string` | URL
|
|
89
|
+
| `previewUrl` | `string` | A single URL rendered as a tappable link card below the message. Presentation only — no response. Compose with any interaction. |
|
|
90
90
|
| `notify` | `boolean` | Whether to send a push notification. Default `true`. |
|
|
91
91
|
|
|
92
92
|
```typescript
|
|
@@ -144,6 +144,29 @@ for (const r of results) {
|
|
|
144
144
|
|
|
145
145
|
---
|
|
146
146
|
|
|
147
|
+
### `vessels.validatePush(payload)` / `vessels.validatePushMany(payload)`
|
|
148
|
+
|
|
149
|
+
Check that a payload complies with the required syntax **without sending anything**. Useful for confirming an agent's message is well-formed in a test or dry run before it reaches a human. This runs the **same schema the server enforces**, so a payload that passes here is exactly one the API will accept.
|
|
150
|
+
|
|
151
|
+
Returns `{ valid: boolean, errors: string[], details? }` — `errors` is a list of readable `field.path: message` lines; `details` is Zod's `flatten()` output (`{ formErrors, fieldErrors }`).
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
const check = vessels.validatePush({
|
|
155
|
+
vessel: 'booking-123',
|
|
156
|
+
message: 'New booking',
|
|
157
|
+
interaction: { type: 'approval', prompt: 'Confirm?' },
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (!check.valid) {
|
|
161
|
+
console.error(check.errors);
|
|
162
|
+
// e.g. ['suggestions: Array must contain at most 5 element(s)']
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
You don't have to call this explicitly: `push()` and `pushMany()` run the same check internally and throw a `VesselsValidationError` **before** the network request, so a malformed payload fails fast without burning an API call. Call `validatePush` directly when you want a result object instead of an exception.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
147
170
|
### `vessels.editMessage(messageId, patch)`
|
|
148
171
|
|
|
149
172
|
Edit an existing agent-sent message in place. The message re-renders via Supabase Realtime without reloading.
|
|
@@ -250,19 +273,18 @@ vessels.textInput({
|
|
|
250
273
|
|
|
251
274
|
Response shape: `{ text: string }`
|
|
252
275
|
|
|
253
|
-
#### `
|
|
276
|
+
#### Review-and-decide (the old `confirmPreview`)
|
|
254
277
|
|
|
255
|
-
|
|
278
|
+
There is no separate preview interaction. To have the human review something then decide,
|
|
279
|
+
attach a [`previewUrl`](#push) (a link card) to the message and use `approval`:
|
|
256
280
|
|
|
257
281
|
```typescript
|
|
258
|
-
vessels.
|
|
259
|
-
|
|
282
|
+
await vessels.push({
|
|
283
|
+
vessel: 'draft-123',
|
|
284
|
+
message: 'Draft email ready for review.',
|
|
260
285
|
previewUrl: 'https://your-app.com/drafts/123',
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
rejectLabel: 'Edit',
|
|
264
|
-
reasonRequiredOnReject: true,
|
|
265
|
-
})
|
|
286
|
+
interaction: vessels.approval({ prompt: 'Send this email?', approveLabel: 'Send', rejectLabel: 'Edit' }),
|
|
287
|
+
});
|
|
266
288
|
```
|
|
267
289
|
|
|
268
290
|
Response shape: `{ action: 'approved' | 'rejected', reason?: string }`
|
|
@@ -288,7 +310,7 @@ const { events, hasMore } = await vessels.poll({ ack: true });
|
|
|
288
310
|
|
|
289
311
|
for (const event of events) {
|
|
290
312
|
if (event.type === 'interaction.response') {
|
|
291
|
-
// event.interactionType — 'approval' | 'choice' | 'checklist' | 'text_input'
|
|
313
|
+
// event.interactionType — 'approval' | 'choice' | 'checklist' | 'text_input'
|
|
292
314
|
// event.response — response shape depends on interactionType (see above)
|
|
293
315
|
// event.interactionMetadata — metadata object you passed when creating the interaction, or null
|
|
294
316
|
// event.messageId — UUID of the message containing the interaction
|
|
@@ -416,7 +438,7 @@ Three typed error classes are exported alongside `Vessels`:
|
|
|
416
438
|
| Class | HTTP status | Description |
|
|
417
439
|
|-------|-------------|-------------|
|
|
418
440
|
| `VesselsAuthError` | 401 | Invalid or revoked API key |
|
|
419
|
-
| `VesselsValidationError` | 400 | Bad request payload. Check `.details` for field-level errors. |
|
|
441
|
+
| `VesselsValidationError` | 400 | Bad request payload. Also thrown **locally, before the request** by `push()`/`pushMany()` when the payload fails client-side validation. Check `.details` for field-level errors. |
|
|
420
442
|
| `VesselsRateLimitError` | 429 | Rate limit exceeded. Check `.retryAfter` (seconds) before retrying. |
|
|
421
443
|
|
|
422
444
|
```typescript
|
|
@@ -467,7 +489,6 @@ import type {
|
|
|
467
489
|
ChoiceInteraction,
|
|
468
490
|
ChecklistInteraction,
|
|
469
491
|
TextInputInteraction,
|
|
470
|
-
ConfirmPreviewInteraction,
|
|
471
492
|
} from 'vessels-sdk';
|
|
472
493
|
```
|
|
473
494
|
|
package/dist/index.cjs
CHANGED
|
@@ -8,8 +8,7 @@ var InteractionTypeSchema = zod.z.enum([
|
|
|
8
8
|
"approval",
|
|
9
9
|
"choice",
|
|
10
10
|
"checklist",
|
|
11
|
-
"text_input"
|
|
12
|
-
"confirm_preview"
|
|
11
|
+
"text_input"
|
|
13
12
|
]);
|
|
14
13
|
var ApprovalInteractionSchema = zod.z.object({
|
|
15
14
|
type: zod.z.literal("approval"),
|
|
@@ -52,27 +51,24 @@ var TextInputInteractionSchema = zod.z.object({
|
|
|
52
51
|
submitLabel: zod.z.string().optional(),
|
|
53
52
|
metadata: zod.z.record(zod.z.unknown()).optional()
|
|
54
53
|
});
|
|
55
|
-
var ConfirmPreviewInteractionSchema = zod.z.object({
|
|
56
|
-
type: zod.z.literal("confirm_preview"),
|
|
57
|
-
prompt: zod.z.string().min(1),
|
|
58
|
-
previewUrl: zod.z.string().url(),
|
|
59
|
-
previewLabel: zod.z.string().optional(),
|
|
60
|
-
approveLabel: zod.z.string().optional(),
|
|
61
|
-
rejectLabel: zod.z.string().optional(),
|
|
62
|
-
reasonRequiredOnReject: zod.z.boolean().optional(),
|
|
63
|
-
metadata: zod.z.record(zod.z.unknown()).optional()
|
|
64
|
-
});
|
|
65
54
|
var InteractionSchema = zod.z.discriminatedUnion("type", [
|
|
66
55
|
ApprovalInteractionSchema,
|
|
67
56
|
ChoiceInteractionSchema,
|
|
68
57
|
ChecklistInteractionSchema,
|
|
69
|
-
TextInputInteractionSchema
|
|
70
|
-
ConfirmPreviewInteractionSchema
|
|
58
|
+
TextInputInteractionSchema
|
|
71
59
|
]);
|
|
72
60
|
var AgentActivityTypeSchema = zod.z.enum(["thinking", "searching", "tool_use", "browsing", "processing"]);
|
|
61
|
+
var AgentTodoStatusSchema = zod.z.enum(["pending", "in_progress", "done"]);
|
|
62
|
+
var AgentTodoInputSchema = zod.z.object({
|
|
63
|
+
label: zod.z.string().min(1).max(200),
|
|
64
|
+
status: AgentTodoStatusSchema.optional()
|
|
65
|
+
});
|
|
73
66
|
var AgentActivitySchema = zod.z.object({
|
|
74
|
-
type: AgentActivityTypeSchema,
|
|
75
|
-
label: zod.z.string().max(200).optional()
|
|
67
|
+
type: AgentActivityTypeSchema.optional(),
|
|
68
|
+
label: zod.z.string().max(200).optional(),
|
|
69
|
+
todos: zod.z.array(AgentTodoInputSchema).max(50).optional()
|
|
70
|
+
}).refine((d) => d.type != null || d.todos != null, {
|
|
71
|
+
message: "agentActivity requires `type` (a step) or `todos` (a plan)"
|
|
76
72
|
});
|
|
77
73
|
var CardFieldSchema = zod.z.object({
|
|
78
74
|
label: zod.z.string().min(1),
|
|
@@ -87,7 +83,7 @@ var AttachmentSchema = zod.z.discriminatedUnion("type", [
|
|
|
87
83
|
zod.z.object({ type: zod.z.literal("file"), url: zod.z.string().url(), filename: zod.z.string().optional() })
|
|
88
84
|
]);
|
|
89
85
|
var VesselStatusSchema = zod.z.enum(["active", "waiting", "resolved"]);
|
|
90
|
-
zod.z.object({
|
|
86
|
+
var PushPayloadSchema = zod.z.object({
|
|
91
87
|
message: zod.z.string().min(1).max(1e4).optional(),
|
|
92
88
|
vessel: zod.z.string().optional(),
|
|
93
89
|
vesselTitle: zod.z.string().optional(),
|
|
@@ -103,11 +99,19 @@ zod.z.object({
|
|
|
103
99
|
labels: zod.z.array(zod.z.string().min(1).max(50)).max(10).optional(),
|
|
104
100
|
attachments: zod.z.array(AttachmentSchema).max(10).optional(),
|
|
105
101
|
suggestions: zod.z.array(zod.z.string().min(1).max(500)).max(5).optional(),
|
|
106
|
-
agentActivity: AgentActivitySchema.optional()
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
agentActivity: AgentActivitySchema.optional(),
|
|
103
|
+
/**
|
|
104
|
+
* Live token-stream buffer — an ephemeral monospace block the human watches
|
|
105
|
+
* fill in real time (set it on the message you create, then keep replacing it
|
|
106
|
+
* via `PATCH /messages/:id`, and clear it with `null` when done). It is a live
|
|
107
|
+
* window, not a transcript: send the tail you want shown (the SDK trims to the
|
|
108
|
+
* last 8000 chars). Plaintext, like agentActivity. Vanishes when cleared.
|
|
109
|
+
*/
|
|
110
|
+
tokenStream: zod.z.string().max(8e3).optional()
|
|
111
|
+
}).refine((d) => d.message || d.agentActivity || d.tokenStream, {
|
|
112
|
+
message: "One of message, agentActivity, or tokenStream is required"
|
|
109
113
|
});
|
|
110
|
-
zod.z.object({
|
|
114
|
+
var PushManyPayloadSchema = zod.z.object({
|
|
111
115
|
vessels: zod.z.array(zod.z.string().min(1)).min(1).max(100),
|
|
112
116
|
message: zod.z.string().min(1).max(1e4),
|
|
113
117
|
vesselTitle: zod.z.string().optional(),
|
|
@@ -127,7 +131,9 @@ zod.z.object({
|
|
|
127
131
|
card: CardSchema.nullable().optional(),
|
|
128
132
|
attachments: zod.z.array(AttachmentSchema).max(10).nullable().optional(),
|
|
129
133
|
suggestions: zod.z.array(zod.z.string().min(1).max(500)).max(5).nullable().optional(),
|
|
130
|
-
agentActivity: AgentActivitySchema.nullable().optional()
|
|
134
|
+
agentActivity: AgentActivitySchema.nullable().optional(),
|
|
135
|
+
/** Replace the live token-stream window, or `null` to clear it (block vanishes). */
|
|
136
|
+
tokenStream: zod.z.string().max(8e3).nullable().optional()
|
|
131
137
|
}).refine((d) => Object.values(d).some((v) => v !== void 0), {
|
|
132
138
|
message: "At least one field required"
|
|
133
139
|
});
|
|
@@ -145,16 +151,11 @@ var ChecklistResponseSchema = zod.z.object({
|
|
|
145
151
|
var TextInputResponseSchema = zod.z.object({
|
|
146
152
|
text: zod.z.string()
|
|
147
153
|
});
|
|
148
|
-
var ConfirmPreviewResponseSchema = zod.z.object({
|
|
149
|
-
action: zod.z.enum(["approved", "rejected"]),
|
|
150
|
-
reason: zod.z.string().optional()
|
|
151
|
-
});
|
|
152
154
|
zod.z.discriminatedUnion("interactionType", [
|
|
153
155
|
zod.z.object({ interactionType: zod.z.literal("approval"), response: ApprovalResponseSchema }),
|
|
154
156
|
zod.z.object({ interactionType: zod.z.literal("choice"), response: ChoiceResponseSchema }),
|
|
155
157
|
zod.z.object({ interactionType: zod.z.literal("checklist"), response: ChecklistResponseSchema }),
|
|
156
|
-
zod.z.object({ interactionType: zod.z.literal("text_input"), response: TextInputResponseSchema })
|
|
157
|
-
zod.z.object({ interactionType: zod.z.literal("confirm_preview"), response: ConfirmPreviewResponseSchema })
|
|
158
|
+
zod.z.object({ interactionType: zod.z.literal("text_input"), response: TextInputResponseSchema })
|
|
158
159
|
]);
|
|
159
160
|
var WebhookVesselSchema = zod.z.object({
|
|
160
161
|
id: zod.z.string(),
|
|
@@ -271,6 +272,13 @@ var VesselsConflictError = class extends Error {
|
|
|
271
272
|
this.name = "VesselsConflictError";
|
|
272
273
|
}
|
|
273
274
|
};
|
|
275
|
+
function formatZodError(error) {
|
|
276
|
+
const errors = error.issues.map((i) => {
|
|
277
|
+
const path = i.path.join(".") || "(payload)";
|
|
278
|
+
return `${path}: ${i.message}`;
|
|
279
|
+
});
|
|
280
|
+
return { errors, details: error.flatten() };
|
|
281
|
+
}
|
|
274
282
|
var Vessels = class {
|
|
275
283
|
apiKey;
|
|
276
284
|
baseUrl;
|
|
@@ -303,8 +311,33 @@ var Vessels = class {
|
|
|
303
311
|
}
|
|
304
312
|
return res;
|
|
305
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Check a push payload against the schema the server enforces, WITHOUT sending
|
|
316
|
+
* anything. Use this to confirm an agent's message complies before it goes
|
|
317
|
+
* live (e.g. in a test or a dry-run). `push()` runs the same check internally.
|
|
318
|
+
*/
|
|
319
|
+
validatePush(payload) {
|
|
320
|
+
const { idempotencyKey: _ignored, ...body } = payload;
|
|
321
|
+
const result = PushPayloadSchema.safeParse(body);
|
|
322
|
+
if (result.success) return { valid: true, errors: [] };
|
|
323
|
+
const { errors, details } = formatZodError(result.error);
|
|
324
|
+
return { valid: false, errors, details };
|
|
325
|
+
}
|
|
326
|
+
/** Like {@link validatePush}, but for a `pushMany()` broadcast payload. */
|
|
327
|
+
validatePushMany(payload) {
|
|
328
|
+
const { idempotencyKey: _ignored, ...body } = payload;
|
|
329
|
+
const result = PushManyPayloadSchema.safeParse(body);
|
|
330
|
+
if (result.success) return { valid: true, errors: [] };
|
|
331
|
+
const { errors, details } = formatZodError(result.error);
|
|
332
|
+
return { valid: false, errors, details };
|
|
333
|
+
}
|
|
306
334
|
async push(payload) {
|
|
307
335
|
const { idempotencyKey, ...body } = payload;
|
|
336
|
+
const check = PushPayloadSchema.safeParse(body);
|
|
337
|
+
if (!check.success) {
|
|
338
|
+
const { errors, details } = formatZodError(check.error);
|
|
339
|
+
throw new VesselsValidationError(`Invalid push payload \u2014 ${errors.join("; ")}`, details);
|
|
340
|
+
}
|
|
308
341
|
const res = await this._fetch(`${this.baseUrl}/api/v1/push`, {
|
|
309
342
|
method: "POST",
|
|
310
343
|
headers: {
|
|
@@ -330,6 +363,11 @@ var Vessels = class {
|
|
|
330
363
|
}
|
|
331
364
|
async pushMany(payload) {
|
|
332
365
|
const { idempotencyKey, ...body } = payload;
|
|
366
|
+
const check = PushManyPayloadSchema.safeParse(body);
|
|
367
|
+
if (!check.success) {
|
|
368
|
+
const { errors, details } = formatZodError(check.error);
|
|
369
|
+
throw new VesselsValidationError(`Invalid pushMany payload \u2014 ${errors.join("; ")}`, details);
|
|
370
|
+
}
|
|
333
371
|
const res = await this._fetch(`${this.baseUrl}/api/v1/push/many`, {
|
|
334
372
|
method: "POST",
|
|
335
373
|
headers: {
|
|
@@ -373,6 +411,117 @@ var Vessels = class {
|
|
|
373
411
|
if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
374
412
|
return { ok: true };
|
|
375
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Narrate a working message: declare a plan, file steps under each task as you
|
|
416
|
+
* go, and seal it when done. Sugar over {@link editMessage} — it tracks the
|
|
417
|
+
* todo list locally and PATCHes the full list each update (the server is
|
|
418
|
+
* authoritative and reconciles by label). The message must already exist; get
|
|
419
|
+
* its id from {@link push}.
|
|
420
|
+
*/
|
|
421
|
+
activity(messageId) {
|
|
422
|
+
let todos = [];
|
|
423
|
+
const sendTodos = () => this.editMessage(messageId, { agentActivity: { todos } });
|
|
424
|
+
return {
|
|
425
|
+
plan: async (tasks) => {
|
|
426
|
+
todos = tasks.map(
|
|
427
|
+
(t) => typeof t === "string" ? { label: t, status: "pending" } : { label: t.label, status: t.status ?? "pending" }
|
|
428
|
+
);
|
|
429
|
+
await sendTodos();
|
|
430
|
+
},
|
|
431
|
+
start: async (label) => {
|
|
432
|
+
todos = todos.map(
|
|
433
|
+
(t) => t.label === label ? { ...t, status: "in_progress" } : t.status === "in_progress" ? { ...t, status: "done" } : t
|
|
434
|
+
);
|
|
435
|
+
if (!todos.some((t) => t.label === label)) todos.push({ label, status: "in_progress" });
|
|
436
|
+
await sendTodos();
|
|
437
|
+
},
|
|
438
|
+
step: async (type, label) => {
|
|
439
|
+
await this.editMessage(messageId, { agentActivity: { type, label } });
|
|
440
|
+
},
|
|
441
|
+
complete: async (label) => {
|
|
442
|
+
todos = todos.map(
|
|
443
|
+
(t) => (label ? t.label === label : t.status === "in_progress") ? { ...t, status: "done" } : t
|
|
444
|
+
);
|
|
445
|
+
await sendTodos();
|
|
446
|
+
},
|
|
447
|
+
done: async () => {
|
|
448
|
+
await this.editMessage(messageId, { agentActivity: null });
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Stream live tokens into a message: the human watches a monospace block fill
|
|
454
|
+
* in real time. Sugar over {@link editMessage} — it keeps the buffer locally
|
|
455
|
+
* and PATCHes a throttled, tail-trimmed window (replace-semantics, so a lost
|
|
456
|
+
* flush self-heals on the next one). Seal with `done()` to clear the block.
|
|
457
|
+
*
|
|
458
|
+
* @param messageId the message to stream into (create it first via `push`).
|
|
459
|
+
* @param opts.throttleMs minimum gap between server flushes (default 120ms).
|
|
460
|
+
* @param opts.maxChars longest window kept; older text scrolls off (default 8000).
|
|
461
|
+
*/
|
|
462
|
+
stream(messageId, opts) {
|
|
463
|
+
const throttleMs = opts?.throttleMs ?? 120;
|
|
464
|
+
const maxChars = opts?.maxChars ?? 8e3;
|
|
465
|
+
let buffer = "";
|
|
466
|
+
let lastSent = null;
|
|
467
|
+
let timer = null;
|
|
468
|
+
let pending = Promise.resolve();
|
|
469
|
+
const windowed = () => buffer.length > maxChars ? buffer.slice(-maxChars) : buffer;
|
|
470
|
+
const flushNow = () => {
|
|
471
|
+
const text = windowed();
|
|
472
|
+
if (text === lastSent) return pending;
|
|
473
|
+
lastSent = text;
|
|
474
|
+
pending = this.editMessage(messageId, { tokenStream: text }).catch(() => {
|
|
475
|
+
lastSent = null;
|
|
476
|
+
});
|
|
477
|
+
return pending;
|
|
478
|
+
};
|
|
479
|
+
const cancelTimer = () => {
|
|
480
|
+
if (timer) {
|
|
481
|
+
clearTimeout(timer);
|
|
482
|
+
timer = null;
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
const schedule = () => {
|
|
486
|
+
if (timer) return;
|
|
487
|
+
timer = setTimeout(() => {
|
|
488
|
+
timer = null;
|
|
489
|
+
void flushNow();
|
|
490
|
+
}, throttleMs);
|
|
491
|
+
timer?.unref?.();
|
|
492
|
+
};
|
|
493
|
+
const settle = async () => {
|
|
494
|
+
cancelTimer();
|
|
495
|
+
await pending.catch(() => {
|
|
496
|
+
});
|
|
497
|
+
};
|
|
498
|
+
return {
|
|
499
|
+
write: (text) => {
|
|
500
|
+
buffer += text;
|
|
501
|
+
schedule();
|
|
502
|
+
},
|
|
503
|
+
set: async (text) => {
|
|
504
|
+
buffer = text;
|
|
505
|
+
cancelTimer();
|
|
506
|
+
await flushNow();
|
|
507
|
+
},
|
|
508
|
+
// Clear the stream AND seal any working card (agentActivity: null is a no-op
|
|
509
|
+
// when there's none), so the final content actually renders.
|
|
510
|
+
done: async (finalContent) => {
|
|
511
|
+
await settle();
|
|
512
|
+
await this.editMessage(messageId, {
|
|
513
|
+
tokenStream: null,
|
|
514
|
+
agentActivity: null,
|
|
515
|
+
...finalContent != null ? { content: finalContent } : {}
|
|
516
|
+
});
|
|
517
|
+
},
|
|
518
|
+
// Remove only the stream; leave the working card alone.
|
|
519
|
+
clear: async () => {
|
|
520
|
+
await settle();
|
|
521
|
+
await this.editMessage(messageId, { tokenStream: null });
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
}
|
|
376
525
|
/**
|
|
377
526
|
* Read a vessel's message history — the human-facing record, for re-reading
|
|
378
527
|
* the channel (e.g. a stateless or just-restarted worker reconciling state).
|
|
@@ -418,9 +567,6 @@ var Vessels = class {
|
|
|
418
567
|
textInput(opts) {
|
|
419
568
|
return { type: "text_input", ...opts };
|
|
420
569
|
}
|
|
421
|
-
confirmPreview(opts) {
|
|
422
|
-
return { type: "confirm_preview", ...opts };
|
|
423
|
-
}
|
|
424
570
|
async poll(options = {}) {
|
|
425
571
|
const { since, limit = 50, ack = true } = options;
|
|
426
572
|
const params = new URLSearchParams();
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _vessels_types from '@vessels/types';
|
|
2
|
-
export { AgentActivity, AgentActivityType, ApprovalInteraction, Attachment, Card, ChecklistInteraction, ChoiceInteraction,
|
|
2
|
+
export { AgentActivity, AgentActivityType, AgentTodoInput, AgentTodoStatus, ApprovalInteraction, Attachment, Card, ChecklistInteraction, ChoiceInteraction, Interaction, MessagePatch, PushManyOptions, PushManyPayload, PushOptions, PushPayload, TextInputInteraction, WebhookInteractionResponsePayload, WebhookInteractionResponsePayloadSchema, WebhookPayload, WebhookPayloadSchema, WebhookUserMessagePayload, WebhookUserMessagePayloadSchema, WebhookVesselCreatedPayload, WebhookVesselCreatedPayloadSchema } from '@vessels/types';
|
|
3
3
|
|
|
4
4
|
declare const AgentActivityTypes: {
|
|
5
5
|
readonly thinking: "thinking";
|
|
@@ -23,11 +23,81 @@ declare class VesselsRateLimitError extends Error {
|
|
|
23
23
|
declare class VesselsConflictError extends Error {
|
|
24
24
|
constructor(message: string);
|
|
25
25
|
}
|
|
26
|
+
/** Result of a client-side payload check via {@link Vessels.validatePush}. */
|
|
27
|
+
interface ValidationResult {
|
|
28
|
+
/** True when the payload satisfies the schema the server enforces. */
|
|
29
|
+
valid: boolean;
|
|
30
|
+
/** Human-readable `field.path: message` lines — empty when valid. */
|
|
31
|
+
errors: string[];
|
|
32
|
+
/** Zod `flatten()` output (field + form errors) — present only when invalid. */
|
|
33
|
+
details?: {
|
|
34
|
+
formErrors: string[];
|
|
35
|
+
fieldErrors: Record<string, string[]>;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
26
38
|
interface VesselsConfig {
|
|
27
39
|
apiKey: string;
|
|
28
40
|
baseUrl?: string;
|
|
29
41
|
debug?: boolean;
|
|
30
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* A stateful handle for narrating a working message — the plan and the work it
|
|
45
|
+
* produces resolve into one artifact. Steps emitted while a task is active are
|
|
46
|
+
* filed under it server-side. Obtain one with {@link Vessels.activity}.
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* const act = vessels.activity(messageId);
|
|
50
|
+
* await act.plan(['Check availability', 'Draft email', 'Send to customer']);
|
|
51
|
+
* await act.start('Check availability'); // → in_progress, step target
|
|
52
|
+
* await act.step('searching', 'Querying calendar');
|
|
53
|
+
* await act.start('Draft email'); // auto-finishes the prior task
|
|
54
|
+
* await act.step('tool_use', 'Sending via SendGrid');
|
|
55
|
+
* await act.done(); // seals everything
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
interface ActivityHandle {
|
|
59
|
+
/** Declare (or replace) the plan. Tasks default to `pending`. */
|
|
60
|
+
plan(tasks: Array<string | {
|
|
61
|
+
label: string;
|
|
62
|
+
status?: _vessels_types.AgentTodoStatus;
|
|
63
|
+
}>): Promise<void>;
|
|
64
|
+
/** Mark a task in-progress (creating it if new); any other in-progress task is finished. */
|
|
65
|
+
start(label: string): Promise<void>;
|
|
66
|
+
/** Append a step, filed under the active task. */
|
|
67
|
+
step(type: _vessels_types.AgentActivityType, label?: string): Promise<void>;
|
|
68
|
+
/** Finish a task by label, or the current in-progress task if omitted. */
|
|
69
|
+
complete(label?: string): Promise<void>;
|
|
70
|
+
/** Seal the activity — finishes the open step and any in-progress task. */
|
|
71
|
+
done(): Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A handle for streaming live tokens into a message — the human watches a
|
|
75
|
+
* monospace block fill in real time, and it vanishes when you seal it. Obtain
|
|
76
|
+
* one with {@link Vessels.stream}; the message must already exist (get its id
|
|
77
|
+
* from {@link Vessels.push}). It is a live WINDOW, not a transcript: write all
|
|
78
|
+
* you like, only the last `maxChars` (default 8000) are shown.
|
|
79
|
+
*
|
|
80
|
+
* ```ts
|
|
81
|
+
* const { messageId } = await vessels.push({ vessel, agentActivity: { type: 'thinking' } });
|
|
82
|
+
* const out = vessels.stream(messageId);
|
|
83
|
+
* for await (const tok of llm) out.write(tok); // throttled PATCHes under the hood
|
|
84
|
+
* await out.done('Booked you in for 2pm Thursday.'); // clears the stream, sets the final reply
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
interface StreamHandle {
|
|
88
|
+
/** Append text to the live buffer; flushed to the server on a throttle. */
|
|
89
|
+
write(text: string): void;
|
|
90
|
+
/** Replace the whole buffer and flush immediately. */
|
|
91
|
+
set(text: string): Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Finish the turn: flush, clear the block, and seal any working agent-activity
|
|
94
|
+
* card (a no-op if there is none) — optionally setting the message's final
|
|
95
|
+
* content. Use this when the stream was the last thing you were doing.
|
|
96
|
+
*/
|
|
97
|
+
done(finalContent?: string): Promise<void>;
|
|
98
|
+
/** Just remove the block, leaving any working card untouched (you're still working). */
|
|
99
|
+
clear(): Promise<void>;
|
|
100
|
+
}
|
|
31
101
|
interface PushResponse {
|
|
32
102
|
ok: true;
|
|
33
103
|
messageId: string;
|
|
@@ -154,11 +224,41 @@ declare class Vessels {
|
|
|
154
224
|
private _debug;
|
|
155
225
|
constructor(config: VesselsConfig);
|
|
156
226
|
private _fetch;
|
|
227
|
+
/**
|
|
228
|
+
* Check a push payload against the schema the server enforces, WITHOUT sending
|
|
229
|
+
* anything. Use this to confirm an agent's message complies before it goes
|
|
230
|
+
* live (e.g. in a test or a dry-run). `push()` runs the same check internally.
|
|
231
|
+
*/
|
|
232
|
+
validatePush(payload: _vessels_types.PushOptions): ValidationResult;
|
|
233
|
+
/** Like {@link validatePush}, but for a `pushMany()` broadcast payload. */
|
|
234
|
+
validatePushMany(payload: _vessels_types.PushManyOptions): ValidationResult;
|
|
157
235
|
push(payload: _vessels_types.PushOptions): Promise<PushResponse>;
|
|
158
236
|
pushMany(payload: _vessels_types.PushManyOptions): Promise<PushManyResult>;
|
|
159
237
|
editMessage(messageId: string, patch: _vessels_types.MessagePatch): Promise<{
|
|
160
238
|
ok: true;
|
|
161
239
|
}>;
|
|
240
|
+
/**
|
|
241
|
+
* Narrate a working message: declare a plan, file steps under each task as you
|
|
242
|
+
* go, and seal it when done. Sugar over {@link editMessage} — it tracks the
|
|
243
|
+
* todo list locally and PATCHes the full list each update (the server is
|
|
244
|
+
* authoritative and reconciles by label). The message must already exist; get
|
|
245
|
+
* its id from {@link push}.
|
|
246
|
+
*/
|
|
247
|
+
activity(messageId: string): ActivityHandle;
|
|
248
|
+
/**
|
|
249
|
+
* Stream live tokens into a message: the human watches a monospace block fill
|
|
250
|
+
* in real time. Sugar over {@link editMessage} — it keeps the buffer locally
|
|
251
|
+
* and PATCHes a throttled, tail-trimmed window (replace-semantics, so a lost
|
|
252
|
+
* flush self-heals on the next one). Seal with `done()` to clear the block.
|
|
253
|
+
*
|
|
254
|
+
* @param messageId the message to stream into (create it first via `push`).
|
|
255
|
+
* @param opts.throttleMs minimum gap between server flushes (default 120ms).
|
|
256
|
+
* @param opts.maxChars longest window kept; older text scrolls off (default 8000).
|
|
257
|
+
*/
|
|
258
|
+
stream(messageId: string, opts?: {
|
|
259
|
+
throttleMs?: number;
|
|
260
|
+
maxChars?: number;
|
|
261
|
+
}): StreamHandle;
|
|
162
262
|
/**
|
|
163
263
|
* Read a vessel's message history — the human-facing record, for re-reading
|
|
164
264
|
* the channel (e.g. a stateless or just-restarted worker reconciling state).
|
|
@@ -210,18 +310,9 @@ declare class Vessels {
|
|
|
210
310
|
submitLabel?: string;
|
|
211
311
|
metadata?: Record<string, unknown>;
|
|
212
312
|
}): _vessels_types.TextInputInteraction;
|
|
213
|
-
confirmPreview(opts: {
|
|
214
|
-
prompt: string;
|
|
215
|
-
previewUrl: string;
|
|
216
|
-
previewLabel?: string;
|
|
217
|
-
approveLabel?: string;
|
|
218
|
-
rejectLabel?: string;
|
|
219
|
-
reasonRequiredOnReject?: boolean;
|
|
220
|
-
metadata?: Record<string, unknown>;
|
|
221
|
-
}): _vessels_types.ConfirmPreviewInteraction;
|
|
222
313
|
poll(options?: PollOptions): Promise<PollResponse>;
|
|
223
314
|
verifyWebhook(body: string, signature: string, webhookSecret: string): Promise<boolean>;
|
|
224
315
|
parseWebhookEvent(body: string, signature: string, webhookSecret: string): Promise<InteractionResponseEvent | UserMessageEvent | VesselCreatedEvent | MessageCancelledEvent | null>;
|
|
225
316
|
}
|
|
226
317
|
|
|
227
|
-
export { AgentActivityTypes, type InteractionResponseEvent, type Message, type MessageCancelledEvent, type OriginMessage, type PollEvent, type PollOptions, type PollResponse, type PushManyResult, type PushResponse, type UserMessageEvent, type VesselContext, type VesselCreatedEvent, Vessels, VesselsAuthError, type VesselsConfig, VesselsConflictError, VesselsRateLimitError, VesselsValidationError };
|
|
318
|
+
export { type ActivityHandle, AgentActivityTypes, type InteractionResponseEvent, type Message, type MessageCancelledEvent, type OriginMessage, type PollEvent, type PollOptions, type PollResponse, type PushManyResult, type PushResponse, type StreamHandle, type UserMessageEvent, type ValidationResult, type VesselContext, type VesselCreatedEvent, Vessels, VesselsAuthError, type VesselsConfig, VesselsConflictError, VesselsRateLimitError, VesselsValidationError };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _vessels_types from '@vessels/types';
|
|
2
|
-
export { AgentActivity, AgentActivityType, ApprovalInteraction, Attachment, Card, ChecklistInteraction, ChoiceInteraction,
|
|
2
|
+
export { AgentActivity, AgentActivityType, AgentTodoInput, AgentTodoStatus, ApprovalInteraction, Attachment, Card, ChecklistInteraction, ChoiceInteraction, Interaction, MessagePatch, PushManyOptions, PushManyPayload, PushOptions, PushPayload, TextInputInteraction, WebhookInteractionResponsePayload, WebhookInteractionResponsePayloadSchema, WebhookPayload, WebhookPayloadSchema, WebhookUserMessagePayload, WebhookUserMessagePayloadSchema, WebhookVesselCreatedPayload, WebhookVesselCreatedPayloadSchema } from '@vessels/types';
|
|
3
3
|
|
|
4
4
|
declare const AgentActivityTypes: {
|
|
5
5
|
readonly thinking: "thinking";
|
|
@@ -23,11 +23,81 @@ declare class VesselsRateLimitError extends Error {
|
|
|
23
23
|
declare class VesselsConflictError extends Error {
|
|
24
24
|
constructor(message: string);
|
|
25
25
|
}
|
|
26
|
+
/** Result of a client-side payload check via {@link Vessels.validatePush}. */
|
|
27
|
+
interface ValidationResult {
|
|
28
|
+
/** True when the payload satisfies the schema the server enforces. */
|
|
29
|
+
valid: boolean;
|
|
30
|
+
/** Human-readable `field.path: message` lines — empty when valid. */
|
|
31
|
+
errors: string[];
|
|
32
|
+
/** Zod `flatten()` output (field + form errors) — present only when invalid. */
|
|
33
|
+
details?: {
|
|
34
|
+
formErrors: string[];
|
|
35
|
+
fieldErrors: Record<string, string[]>;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
26
38
|
interface VesselsConfig {
|
|
27
39
|
apiKey: string;
|
|
28
40
|
baseUrl?: string;
|
|
29
41
|
debug?: boolean;
|
|
30
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* A stateful handle for narrating a working message — the plan and the work it
|
|
45
|
+
* produces resolve into one artifact. Steps emitted while a task is active are
|
|
46
|
+
* filed under it server-side. Obtain one with {@link Vessels.activity}.
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* const act = vessels.activity(messageId);
|
|
50
|
+
* await act.plan(['Check availability', 'Draft email', 'Send to customer']);
|
|
51
|
+
* await act.start('Check availability'); // → in_progress, step target
|
|
52
|
+
* await act.step('searching', 'Querying calendar');
|
|
53
|
+
* await act.start('Draft email'); // auto-finishes the prior task
|
|
54
|
+
* await act.step('tool_use', 'Sending via SendGrid');
|
|
55
|
+
* await act.done(); // seals everything
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
interface ActivityHandle {
|
|
59
|
+
/** Declare (or replace) the plan. Tasks default to `pending`. */
|
|
60
|
+
plan(tasks: Array<string | {
|
|
61
|
+
label: string;
|
|
62
|
+
status?: _vessels_types.AgentTodoStatus;
|
|
63
|
+
}>): Promise<void>;
|
|
64
|
+
/** Mark a task in-progress (creating it if new); any other in-progress task is finished. */
|
|
65
|
+
start(label: string): Promise<void>;
|
|
66
|
+
/** Append a step, filed under the active task. */
|
|
67
|
+
step(type: _vessels_types.AgentActivityType, label?: string): Promise<void>;
|
|
68
|
+
/** Finish a task by label, or the current in-progress task if omitted. */
|
|
69
|
+
complete(label?: string): Promise<void>;
|
|
70
|
+
/** Seal the activity — finishes the open step and any in-progress task. */
|
|
71
|
+
done(): Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A handle for streaming live tokens into a message — the human watches a
|
|
75
|
+
* monospace block fill in real time, and it vanishes when you seal it. Obtain
|
|
76
|
+
* one with {@link Vessels.stream}; the message must already exist (get its id
|
|
77
|
+
* from {@link Vessels.push}). It is a live WINDOW, not a transcript: write all
|
|
78
|
+
* you like, only the last `maxChars` (default 8000) are shown.
|
|
79
|
+
*
|
|
80
|
+
* ```ts
|
|
81
|
+
* const { messageId } = await vessels.push({ vessel, agentActivity: { type: 'thinking' } });
|
|
82
|
+
* const out = vessels.stream(messageId);
|
|
83
|
+
* for await (const tok of llm) out.write(tok); // throttled PATCHes under the hood
|
|
84
|
+
* await out.done('Booked you in for 2pm Thursday.'); // clears the stream, sets the final reply
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
interface StreamHandle {
|
|
88
|
+
/** Append text to the live buffer; flushed to the server on a throttle. */
|
|
89
|
+
write(text: string): void;
|
|
90
|
+
/** Replace the whole buffer and flush immediately. */
|
|
91
|
+
set(text: string): Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Finish the turn: flush, clear the block, and seal any working agent-activity
|
|
94
|
+
* card (a no-op if there is none) — optionally setting the message's final
|
|
95
|
+
* content. Use this when the stream was the last thing you were doing.
|
|
96
|
+
*/
|
|
97
|
+
done(finalContent?: string): Promise<void>;
|
|
98
|
+
/** Just remove the block, leaving any working card untouched (you're still working). */
|
|
99
|
+
clear(): Promise<void>;
|
|
100
|
+
}
|
|
31
101
|
interface PushResponse {
|
|
32
102
|
ok: true;
|
|
33
103
|
messageId: string;
|
|
@@ -154,11 +224,41 @@ declare class Vessels {
|
|
|
154
224
|
private _debug;
|
|
155
225
|
constructor(config: VesselsConfig);
|
|
156
226
|
private _fetch;
|
|
227
|
+
/**
|
|
228
|
+
* Check a push payload against the schema the server enforces, WITHOUT sending
|
|
229
|
+
* anything. Use this to confirm an agent's message complies before it goes
|
|
230
|
+
* live (e.g. in a test or a dry-run). `push()` runs the same check internally.
|
|
231
|
+
*/
|
|
232
|
+
validatePush(payload: _vessels_types.PushOptions): ValidationResult;
|
|
233
|
+
/** Like {@link validatePush}, but for a `pushMany()` broadcast payload. */
|
|
234
|
+
validatePushMany(payload: _vessels_types.PushManyOptions): ValidationResult;
|
|
157
235
|
push(payload: _vessels_types.PushOptions): Promise<PushResponse>;
|
|
158
236
|
pushMany(payload: _vessels_types.PushManyOptions): Promise<PushManyResult>;
|
|
159
237
|
editMessage(messageId: string, patch: _vessels_types.MessagePatch): Promise<{
|
|
160
238
|
ok: true;
|
|
161
239
|
}>;
|
|
240
|
+
/**
|
|
241
|
+
* Narrate a working message: declare a plan, file steps under each task as you
|
|
242
|
+
* go, and seal it when done. Sugar over {@link editMessage} — it tracks the
|
|
243
|
+
* todo list locally and PATCHes the full list each update (the server is
|
|
244
|
+
* authoritative and reconciles by label). The message must already exist; get
|
|
245
|
+
* its id from {@link push}.
|
|
246
|
+
*/
|
|
247
|
+
activity(messageId: string): ActivityHandle;
|
|
248
|
+
/**
|
|
249
|
+
* Stream live tokens into a message: the human watches a monospace block fill
|
|
250
|
+
* in real time. Sugar over {@link editMessage} — it keeps the buffer locally
|
|
251
|
+
* and PATCHes a throttled, tail-trimmed window (replace-semantics, so a lost
|
|
252
|
+
* flush self-heals on the next one). Seal with `done()` to clear the block.
|
|
253
|
+
*
|
|
254
|
+
* @param messageId the message to stream into (create it first via `push`).
|
|
255
|
+
* @param opts.throttleMs minimum gap between server flushes (default 120ms).
|
|
256
|
+
* @param opts.maxChars longest window kept; older text scrolls off (default 8000).
|
|
257
|
+
*/
|
|
258
|
+
stream(messageId: string, opts?: {
|
|
259
|
+
throttleMs?: number;
|
|
260
|
+
maxChars?: number;
|
|
261
|
+
}): StreamHandle;
|
|
162
262
|
/**
|
|
163
263
|
* Read a vessel's message history — the human-facing record, for re-reading
|
|
164
264
|
* the channel (e.g. a stateless or just-restarted worker reconciling state).
|
|
@@ -210,18 +310,9 @@ declare class Vessels {
|
|
|
210
310
|
submitLabel?: string;
|
|
211
311
|
metadata?: Record<string, unknown>;
|
|
212
312
|
}): _vessels_types.TextInputInteraction;
|
|
213
|
-
confirmPreview(opts: {
|
|
214
|
-
prompt: string;
|
|
215
|
-
previewUrl: string;
|
|
216
|
-
previewLabel?: string;
|
|
217
|
-
approveLabel?: string;
|
|
218
|
-
rejectLabel?: string;
|
|
219
|
-
reasonRequiredOnReject?: boolean;
|
|
220
|
-
metadata?: Record<string, unknown>;
|
|
221
|
-
}): _vessels_types.ConfirmPreviewInteraction;
|
|
222
313
|
poll(options?: PollOptions): Promise<PollResponse>;
|
|
223
314
|
verifyWebhook(body: string, signature: string, webhookSecret: string): Promise<boolean>;
|
|
224
315
|
parseWebhookEvent(body: string, signature: string, webhookSecret: string): Promise<InteractionResponseEvent | UserMessageEvent | VesselCreatedEvent | MessageCancelledEvent | null>;
|
|
225
316
|
}
|
|
226
317
|
|
|
227
|
-
export { AgentActivityTypes, type InteractionResponseEvent, type Message, type MessageCancelledEvent, type OriginMessage, type PollEvent, type PollOptions, type PollResponse, type PushManyResult, type PushResponse, type UserMessageEvent, type VesselContext, type VesselCreatedEvent, Vessels, VesselsAuthError, type VesselsConfig, VesselsConflictError, VesselsRateLimitError, VesselsValidationError };
|
|
318
|
+
export { type ActivityHandle, AgentActivityTypes, type InteractionResponseEvent, type Message, type MessageCancelledEvent, type OriginMessage, type PollEvent, type PollOptions, type PollResponse, type PushManyResult, type PushResponse, type StreamHandle, type UserMessageEvent, type ValidationResult, type VesselContext, type VesselCreatedEvent, Vessels, VesselsAuthError, type VesselsConfig, VesselsConflictError, VesselsRateLimitError, VesselsValidationError };
|
package/dist/index.js
CHANGED
|
@@ -6,8 +6,7 @@ var InteractionTypeSchema = z.enum([
|
|
|
6
6
|
"approval",
|
|
7
7
|
"choice",
|
|
8
8
|
"checklist",
|
|
9
|
-
"text_input"
|
|
10
|
-
"confirm_preview"
|
|
9
|
+
"text_input"
|
|
11
10
|
]);
|
|
12
11
|
var ApprovalInteractionSchema = z.object({
|
|
13
12
|
type: z.literal("approval"),
|
|
@@ -50,27 +49,24 @@ var TextInputInteractionSchema = z.object({
|
|
|
50
49
|
submitLabel: z.string().optional(),
|
|
51
50
|
metadata: z.record(z.unknown()).optional()
|
|
52
51
|
});
|
|
53
|
-
var ConfirmPreviewInteractionSchema = z.object({
|
|
54
|
-
type: z.literal("confirm_preview"),
|
|
55
|
-
prompt: z.string().min(1),
|
|
56
|
-
previewUrl: z.string().url(),
|
|
57
|
-
previewLabel: z.string().optional(),
|
|
58
|
-
approveLabel: z.string().optional(),
|
|
59
|
-
rejectLabel: z.string().optional(),
|
|
60
|
-
reasonRequiredOnReject: z.boolean().optional(),
|
|
61
|
-
metadata: z.record(z.unknown()).optional()
|
|
62
|
-
});
|
|
63
52
|
var InteractionSchema = z.discriminatedUnion("type", [
|
|
64
53
|
ApprovalInteractionSchema,
|
|
65
54
|
ChoiceInteractionSchema,
|
|
66
55
|
ChecklistInteractionSchema,
|
|
67
|
-
TextInputInteractionSchema
|
|
68
|
-
ConfirmPreviewInteractionSchema
|
|
56
|
+
TextInputInteractionSchema
|
|
69
57
|
]);
|
|
70
58
|
var AgentActivityTypeSchema = z.enum(["thinking", "searching", "tool_use", "browsing", "processing"]);
|
|
59
|
+
var AgentTodoStatusSchema = z.enum(["pending", "in_progress", "done"]);
|
|
60
|
+
var AgentTodoInputSchema = z.object({
|
|
61
|
+
label: z.string().min(1).max(200),
|
|
62
|
+
status: AgentTodoStatusSchema.optional()
|
|
63
|
+
});
|
|
71
64
|
var AgentActivitySchema = z.object({
|
|
72
|
-
type: AgentActivityTypeSchema,
|
|
73
|
-
label: z.string().max(200).optional()
|
|
65
|
+
type: AgentActivityTypeSchema.optional(),
|
|
66
|
+
label: z.string().max(200).optional(),
|
|
67
|
+
todos: z.array(AgentTodoInputSchema).max(50).optional()
|
|
68
|
+
}).refine((d) => d.type != null || d.todos != null, {
|
|
69
|
+
message: "agentActivity requires `type` (a step) or `todos` (a plan)"
|
|
74
70
|
});
|
|
75
71
|
var CardFieldSchema = z.object({
|
|
76
72
|
label: z.string().min(1),
|
|
@@ -85,7 +81,7 @@ var AttachmentSchema = z.discriminatedUnion("type", [
|
|
|
85
81
|
z.object({ type: z.literal("file"), url: z.string().url(), filename: z.string().optional() })
|
|
86
82
|
]);
|
|
87
83
|
var VesselStatusSchema = z.enum(["active", "waiting", "resolved"]);
|
|
88
|
-
z.object({
|
|
84
|
+
var PushPayloadSchema = z.object({
|
|
89
85
|
message: z.string().min(1).max(1e4).optional(),
|
|
90
86
|
vessel: z.string().optional(),
|
|
91
87
|
vesselTitle: z.string().optional(),
|
|
@@ -101,11 +97,19 @@ z.object({
|
|
|
101
97
|
labels: z.array(z.string().min(1).max(50)).max(10).optional(),
|
|
102
98
|
attachments: z.array(AttachmentSchema).max(10).optional(),
|
|
103
99
|
suggestions: z.array(z.string().min(1).max(500)).max(5).optional(),
|
|
104
|
-
agentActivity: AgentActivitySchema.optional()
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
agentActivity: AgentActivitySchema.optional(),
|
|
101
|
+
/**
|
|
102
|
+
* Live token-stream buffer — an ephemeral monospace block the human watches
|
|
103
|
+
* fill in real time (set it on the message you create, then keep replacing it
|
|
104
|
+
* via `PATCH /messages/:id`, and clear it with `null` when done). It is a live
|
|
105
|
+
* window, not a transcript: send the tail you want shown (the SDK trims to the
|
|
106
|
+
* last 8000 chars). Plaintext, like agentActivity. Vanishes when cleared.
|
|
107
|
+
*/
|
|
108
|
+
tokenStream: z.string().max(8e3).optional()
|
|
109
|
+
}).refine((d) => d.message || d.agentActivity || d.tokenStream, {
|
|
110
|
+
message: "One of message, agentActivity, or tokenStream is required"
|
|
107
111
|
});
|
|
108
|
-
z.object({
|
|
112
|
+
var PushManyPayloadSchema = z.object({
|
|
109
113
|
vessels: z.array(z.string().min(1)).min(1).max(100),
|
|
110
114
|
message: z.string().min(1).max(1e4),
|
|
111
115
|
vesselTitle: z.string().optional(),
|
|
@@ -125,7 +129,9 @@ z.object({
|
|
|
125
129
|
card: CardSchema.nullable().optional(),
|
|
126
130
|
attachments: z.array(AttachmentSchema).max(10).nullable().optional(),
|
|
127
131
|
suggestions: z.array(z.string().min(1).max(500)).max(5).nullable().optional(),
|
|
128
|
-
agentActivity: AgentActivitySchema.nullable().optional()
|
|
132
|
+
agentActivity: AgentActivitySchema.nullable().optional(),
|
|
133
|
+
/** Replace the live token-stream window, or `null` to clear it (block vanishes). */
|
|
134
|
+
tokenStream: z.string().max(8e3).nullable().optional()
|
|
129
135
|
}).refine((d) => Object.values(d).some((v) => v !== void 0), {
|
|
130
136
|
message: "At least one field required"
|
|
131
137
|
});
|
|
@@ -143,16 +149,11 @@ var ChecklistResponseSchema = z.object({
|
|
|
143
149
|
var TextInputResponseSchema = z.object({
|
|
144
150
|
text: z.string()
|
|
145
151
|
});
|
|
146
|
-
var ConfirmPreviewResponseSchema = z.object({
|
|
147
|
-
action: z.enum(["approved", "rejected"]),
|
|
148
|
-
reason: z.string().optional()
|
|
149
|
-
});
|
|
150
152
|
z.discriminatedUnion("interactionType", [
|
|
151
153
|
z.object({ interactionType: z.literal("approval"), response: ApprovalResponseSchema }),
|
|
152
154
|
z.object({ interactionType: z.literal("choice"), response: ChoiceResponseSchema }),
|
|
153
155
|
z.object({ interactionType: z.literal("checklist"), response: ChecklistResponseSchema }),
|
|
154
|
-
z.object({ interactionType: z.literal("text_input"), response: TextInputResponseSchema })
|
|
155
|
-
z.object({ interactionType: z.literal("confirm_preview"), response: ConfirmPreviewResponseSchema })
|
|
156
|
+
z.object({ interactionType: z.literal("text_input"), response: TextInputResponseSchema })
|
|
156
157
|
]);
|
|
157
158
|
var WebhookVesselSchema = z.object({
|
|
158
159
|
id: z.string(),
|
|
@@ -269,6 +270,13 @@ var VesselsConflictError = class extends Error {
|
|
|
269
270
|
this.name = "VesselsConflictError";
|
|
270
271
|
}
|
|
271
272
|
};
|
|
273
|
+
function formatZodError(error) {
|
|
274
|
+
const errors = error.issues.map((i) => {
|
|
275
|
+
const path = i.path.join(".") || "(payload)";
|
|
276
|
+
return `${path}: ${i.message}`;
|
|
277
|
+
});
|
|
278
|
+
return { errors, details: error.flatten() };
|
|
279
|
+
}
|
|
272
280
|
var Vessels = class {
|
|
273
281
|
apiKey;
|
|
274
282
|
baseUrl;
|
|
@@ -301,8 +309,33 @@ var Vessels = class {
|
|
|
301
309
|
}
|
|
302
310
|
return res;
|
|
303
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* Check a push payload against the schema the server enforces, WITHOUT sending
|
|
314
|
+
* anything. Use this to confirm an agent's message complies before it goes
|
|
315
|
+
* live (e.g. in a test or a dry-run). `push()` runs the same check internally.
|
|
316
|
+
*/
|
|
317
|
+
validatePush(payload) {
|
|
318
|
+
const { idempotencyKey: _ignored, ...body } = payload;
|
|
319
|
+
const result = PushPayloadSchema.safeParse(body);
|
|
320
|
+
if (result.success) return { valid: true, errors: [] };
|
|
321
|
+
const { errors, details } = formatZodError(result.error);
|
|
322
|
+
return { valid: false, errors, details };
|
|
323
|
+
}
|
|
324
|
+
/** Like {@link validatePush}, but for a `pushMany()` broadcast payload. */
|
|
325
|
+
validatePushMany(payload) {
|
|
326
|
+
const { idempotencyKey: _ignored, ...body } = payload;
|
|
327
|
+
const result = PushManyPayloadSchema.safeParse(body);
|
|
328
|
+
if (result.success) return { valid: true, errors: [] };
|
|
329
|
+
const { errors, details } = formatZodError(result.error);
|
|
330
|
+
return { valid: false, errors, details };
|
|
331
|
+
}
|
|
304
332
|
async push(payload) {
|
|
305
333
|
const { idempotencyKey, ...body } = payload;
|
|
334
|
+
const check = PushPayloadSchema.safeParse(body);
|
|
335
|
+
if (!check.success) {
|
|
336
|
+
const { errors, details } = formatZodError(check.error);
|
|
337
|
+
throw new VesselsValidationError(`Invalid push payload \u2014 ${errors.join("; ")}`, details);
|
|
338
|
+
}
|
|
306
339
|
const res = await this._fetch(`${this.baseUrl}/api/v1/push`, {
|
|
307
340
|
method: "POST",
|
|
308
341
|
headers: {
|
|
@@ -328,6 +361,11 @@ var Vessels = class {
|
|
|
328
361
|
}
|
|
329
362
|
async pushMany(payload) {
|
|
330
363
|
const { idempotencyKey, ...body } = payload;
|
|
364
|
+
const check = PushManyPayloadSchema.safeParse(body);
|
|
365
|
+
if (!check.success) {
|
|
366
|
+
const { errors, details } = formatZodError(check.error);
|
|
367
|
+
throw new VesselsValidationError(`Invalid pushMany payload \u2014 ${errors.join("; ")}`, details);
|
|
368
|
+
}
|
|
331
369
|
const res = await this._fetch(`${this.baseUrl}/api/v1/push/many`, {
|
|
332
370
|
method: "POST",
|
|
333
371
|
headers: {
|
|
@@ -371,6 +409,117 @@ var Vessels = class {
|
|
|
371
409
|
if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
372
410
|
return { ok: true };
|
|
373
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Narrate a working message: declare a plan, file steps under each task as you
|
|
414
|
+
* go, and seal it when done. Sugar over {@link editMessage} — it tracks the
|
|
415
|
+
* todo list locally and PATCHes the full list each update (the server is
|
|
416
|
+
* authoritative and reconciles by label). The message must already exist; get
|
|
417
|
+
* its id from {@link push}.
|
|
418
|
+
*/
|
|
419
|
+
activity(messageId) {
|
|
420
|
+
let todos = [];
|
|
421
|
+
const sendTodos = () => this.editMessage(messageId, { agentActivity: { todos } });
|
|
422
|
+
return {
|
|
423
|
+
plan: async (tasks) => {
|
|
424
|
+
todos = tasks.map(
|
|
425
|
+
(t) => typeof t === "string" ? { label: t, status: "pending" } : { label: t.label, status: t.status ?? "pending" }
|
|
426
|
+
);
|
|
427
|
+
await sendTodos();
|
|
428
|
+
},
|
|
429
|
+
start: async (label) => {
|
|
430
|
+
todos = todos.map(
|
|
431
|
+
(t) => t.label === label ? { ...t, status: "in_progress" } : t.status === "in_progress" ? { ...t, status: "done" } : t
|
|
432
|
+
);
|
|
433
|
+
if (!todos.some((t) => t.label === label)) todos.push({ label, status: "in_progress" });
|
|
434
|
+
await sendTodos();
|
|
435
|
+
},
|
|
436
|
+
step: async (type, label) => {
|
|
437
|
+
await this.editMessage(messageId, { agentActivity: { type, label } });
|
|
438
|
+
},
|
|
439
|
+
complete: async (label) => {
|
|
440
|
+
todos = todos.map(
|
|
441
|
+
(t) => (label ? t.label === label : t.status === "in_progress") ? { ...t, status: "done" } : t
|
|
442
|
+
);
|
|
443
|
+
await sendTodos();
|
|
444
|
+
},
|
|
445
|
+
done: async () => {
|
|
446
|
+
await this.editMessage(messageId, { agentActivity: null });
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Stream live tokens into a message: the human watches a monospace block fill
|
|
452
|
+
* in real time. Sugar over {@link editMessage} — it keeps the buffer locally
|
|
453
|
+
* and PATCHes a throttled, tail-trimmed window (replace-semantics, so a lost
|
|
454
|
+
* flush self-heals on the next one). Seal with `done()` to clear the block.
|
|
455
|
+
*
|
|
456
|
+
* @param messageId the message to stream into (create it first via `push`).
|
|
457
|
+
* @param opts.throttleMs minimum gap between server flushes (default 120ms).
|
|
458
|
+
* @param opts.maxChars longest window kept; older text scrolls off (default 8000).
|
|
459
|
+
*/
|
|
460
|
+
stream(messageId, opts) {
|
|
461
|
+
const throttleMs = opts?.throttleMs ?? 120;
|
|
462
|
+
const maxChars = opts?.maxChars ?? 8e3;
|
|
463
|
+
let buffer = "";
|
|
464
|
+
let lastSent = null;
|
|
465
|
+
let timer = null;
|
|
466
|
+
let pending = Promise.resolve();
|
|
467
|
+
const windowed = () => buffer.length > maxChars ? buffer.slice(-maxChars) : buffer;
|
|
468
|
+
const flushNow = () => {
|
|
469
|
+
const text = windowed();
|
|
470
|
+
if (text === lastSent) return pending;
|
|
471
|
+
lastSent = text;
|
|
472
|
+
pending = this.editMessage(messageId, { tokenStream: text }).catch(() => {
|
|
473
|
+
lastSent = null;
|
|
474
|
+
});
|
|
475
|
+
return pending;
|
|
476
|
+
};
|
|
477
|
+
const cancelTimer = () => {
|
|
478
|
+
if (timer) {
|
|
479
|
+
clearTimeout(timer);
|
|
480
|
+
timer = null;
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
const schedule = () => {
|
|
484
|
+
if (timer) return;
|
|
485
|
+
timer = setTimeout(() => {
|
|
486
|
+
timer = null;
|
|
487
|
+
void flushNow();
|
|
488
|
+
}, throttleMs);
|
|
489
|
+
timer?.unref?.();
|
|
490
|
+
};
|
|
491
|
+
const settle = async () => {
|
|
492
|
+
cancelTimer();
|
|
493
|
+
await pending.catch(() => {
|
|
494
|
+
});
|
|
495
|
+
};
|
|
496
|
+
return {
|
|
497
|
+
write: (text) => {
|
|
498
|
+
buffer += text;
|
|
499
|
+
schedule();
|
|
500
|
+
},
|
|
501
|
+
set: async (text) => {
|
|
502
|
+
buffer = text;
|
|
503
|
+
cancelTimer();
|
|
504
|
+
await flushNow();
|
|
505
|
+
},
|
|
506
|
+
// Clear the stream AND seal any working card (agentActivity: null is a no-op
|
|
507
|
+
// when there's none), so the final content actually renders.
|
|
508
|
+
done: async (finalContent) => {
|
|
509
|
+
await settle();
|
|
510
|
+
await this.editMessage(messageId, {
|
|
511
|
+
tokenStream: null,
|
|
512
|
+
agentActivity: null,
|
|
513
|
+
...finalContent != null ? { content: finalContent } : {}
|
|
514
|
+
});
|
|
515
|
+
},
|
|
516
|
+
// Remove only the stream; leave the working card alone.
|
|
517
|
+
clear: async () => {
|
|
518
|
+
await settle();
|
|
519
|
+
await this.editMessage(messageId, { tokenStream: null });
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
}
|
|
374
523
|
/**
|
|
375
524
|
* Read a vessel's message history — the human-facing record, for re-reading
|
|
376
525
|
* the channel (e.g. a stateless or just-restarted worker reconciling state).
|
|
@@ -416,9 +565,6 @@ var Vessels = class {
|
|
|
416
565
|
textInput(opts) {
|
|
417
566
|
return { type: "text_input", ...opts };
|
|
418
567
|
}
|
|
419
|
-
confirmPreview(opts) {
|
|
420
|
-
return { type: "confirm_preview", ...opts };
|
|
421
|
-
}
|
|
422
568
|
async poll(options = {}) {
|
|
423
569
|
const { since, limit = 50, ack = true } = options;
|
|
424
570
|
const params = new URLSearchParams();
|