vessels-sdk 0.8.0 → 0.11.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 CHANGED
@@ -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.
@@ -416,7 +439,7 @@ Three typed error classes are exported alongside `Vessels`:
416
439
  | Class | HTTP status | Description |
417
440
  |-------|-------------|-------------|
418
441
  | `VesselsAuthError` | 401 | Invalid or revoked API key |
419
- | `VesselsValidationError` | 400 | Bad request payload. Check `.details` for field-level errors. |
442
+ | `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
443
  | `VesselsRateLimitError` | 429 | Rate limit exceeded. Check `.retryAfter` (seconds) before retrying. |
421
444
 
422
445
  ```typescript
package/dist/index.cjs CHANGED
@@ -70,9 +70,17 @@ var InteractionSchema = zod.z.discriminatedUnion("type", [
70
70
  ConfirmPreviewInteractionSchema
71
71
  ]);
72
72
  var AgentActivityTypeSchema = zod.z.enum(["thinking", "searching", "tool_use", "browsing", "processing"]);
73
+ var AgentTodoStatusSchema = zod.z.enum(["pending", "in_progress", "done"]);
74
+ var AgentTodoInputSchema = zod.z.object({
75
+ label: zod.z.string().min(1).max(200),
76
+ status: AgentTodoStatusSchema.optional()
77
+ });
73
78
  var AgentActivitySchema = zod.z.object({
74
- type: AgentActivityTypeSchema,
75
- label: zod.z.string().max(200).optional()
79
+ type: AgentActivityTypeSchema.optional(),
80
+ label: zod.z.string().max(200).optional(),
81
+ todos: zod.z.array(AgentTodoInputSchema).max(50).optional()
82
+ }).refine((d) => d.type != null || d.todos != null, {
83
+ message: "agentActivity requires `type` (a step) or `todos` (a plan)"
76
84
  });
77
85
  var CardFieldSchema = zod.z.object({
78
86
  label: zod.z.string().min(1),
@@ -87,7 +95,7 @@ var AttachmentSchema = zod.z.discriminatedUnion("type", [
87
95
  zod.z.object({ type: zod.z.literal("file"), url: zod.z.string().url(), filename: zod.z.string().optional() })
88
96
  ]);
89
97
  var VesselStatusSchema = zod.z.enum(["active", "waiting", "resolved"]);
90
- zod.z.object({
98
+ var PushPayloadSchema = zod.z.object({
91
99
  message: zod.z.string().min(1).max(1e4).optional(),
92
100
  vessel: zod.z.string().optional(),
93
101
  vesselTitle: zod.z.string().optional(),
@@ -107,7 +115,7 @@ zod.z.object({
107
115
  }).refine((d) => d.message || d.agentActivity, {
108
116
  message: "Either message or agentActivity is required"
109
117
  });
110
- zod.z.object({
118
+ var PushManyPayloadSchema = zod.z.object({
111
119
  vessels: zod.z.array(zod.z.string().min(1)).min(1).max(100),
112
120
  message: zod.z.string().min(1).max(1e4),
113
121
  vesselTitle: zod.z.string().optional(),
@@ -218,10 +226,21 @@ var WebhookVesselCreatedPayloadSchema = zod.z.object({
218
226
  })
219
227
  })
220
228
  });
229
+ var WebhookMessageCancelledPayloadSchema = zod.z.object({
230
+ event: zod.z.literal("message.cancelled"),
231
+ vessel_id: zod.z.string(),
232
+ workspace_id: zod.z.string(),
233
+ timestamp: zod.z.string(),
234
+ data: zod.z.object({
235
+ message_id: zod.z.string(),
236
+ vessel: WebhookVesselSchema
237
+ })
238
+ });
221
239
  var WebhookPayloadSchema = zod.z.discriminatedUnion("event", [
222
240
  WebhookInteractionResponsePayloadSchema,
223
241
  WebhookUserMessagePayloadSchema,
224
- WebhookVesselCreatedPayloadSchema
242
+ WebhookVesselCreatedPayloadSchema,
243
+ WebhookMessageCancelledPayloadSchema
225
244
  ]);
226
245
 
227
246
  // src/index.ts
@@ -260,6 +279,13 @@ var VesselsConflictError = class extends Error {
260
279
  this.name = "VesselsConflictError";
261
280
  }
262
281
  };
282
+ function formatZodError(error) {
283
+ const errors = error.issues.map((i) => {
284
+ const path = i.path.join(".") || "(payload)";
285
+ return `${path}: ${i.message}`;
286
+ });
287
+ return { errors, details: error.flatten() };
288
+ }
263
289
  var Vessels = class {
264
290
  apiKey;
265
291
  baseUrl;
@@ -292,8 +318,33 @@ var Vessels = class {
292
318
  }
293
319
  return res;
294
320
  }
321
+ /**
322
+ * Check a push payload against the schema the server enforces, WITHOUT sending
323
+ * anything. Use this to confirm an agent's message complies before it goes
324
+ * live (e.g. in a test or a dry-run). `push()` runs the same check internally.
325
+ */
326
+ validatePush(payload) {
327
+ const { idempotencyKey: _ignored, ...body } = payload;
328
+ const result = PushPayloadSchema.safeParse(body);
329
+ if (result.success) return { valid: true, errors: [] };
330
+ const { errors, details } = formatZodError(result.error);
331
+ return { valid: false, errors, details };
332
+ }
333
+ /** Like {@link validatePush}, but for a `pushMany()` broadcast payload. */
334
+ validatePushMany(payload) {
335
+ const { idempotencyKey: _ignored, ...body } = payload;
336
+ const result = PushManyPayloadSchema.safeParse(body);
337
+ if (result.success) return { valid: true, errors: [] };
338
+ const { errors, details } = formatZodError(result.error);
339
+ return { valid: false, errors, details };
340
+ }
295
341
  async push(payload) {
296
342
  const { idempotencyKey, ...body } = payload;
343
+ const check = PushPayloadSchema.safeParse(body);
344
+ if (!check.success) {
345
+ const { errors, details } = formatZodError(check.error);
346
+ throw new VesselsValidationError(`Invalid push payload \u2014 ${errors.join("; ")}`, details);
347
+ }
297
348
  const res = await this._fetch(`${this.baseUrl}/api/v1/push`, {
298
349
  method: "POST",
299
350
  headers: {
@@ -319,6 +370,11 @@ var Vessels = class {
319
370
  }
320
371
  async pushMany(payload) {
321
372
  const { idempotencyKey, ...body } = payload;
373
+ const check = PushManyPayloadSchema.safeParse(body);
374
+ if (!check.success) {
375
+ const { errors, details } = formatZodError(check.error);
376
+ throw new VesselsValidationError(`Invalid pushMany payload \u2014 ${errors.join("; ")}`, details);
377
+ }
322
378
  const res = await this._fetch(`${this.baseUrl}/api/v1/push/many`, {
323
379
  method: "POST",
324
380
  headers: {
@@ -362,6 +418,44 @@ var Vessels = class {
362
418
  if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
363
419
  return { ok: true };
364
420
  }
421
+ /**
422
+ * Narrate a working message: declare a plan, file steps under each task as you
423
+ * go, and seal it when done. Sugar over {@link editMessage} — it tracks the
424
+ * todo list locally and PATCHes the full list each update (the server is
425
+ * authoritative and reconciles by label). The message must already exist; get
426
+ * its id from {@link push}.
427
+ */
428
+ activity(messageId) {
429
+ let todos = [];
430
+ const sendTodos = () => this.editMessage(messageId, { agentActivity: { todos } });
431
+ return {
432
+ plan: async (tasks) => {
433
+ todos = tasks.map(
434
+ (t) => typeof t === "string" ? { label: t, status: "pending" } : { label: t.label, status: t.status ?? "pending" }
435
+ );
436
+ await sendTodos();
437
+ },
438
+ start: async (label) => {
439
+ todos = todos.map(
440
+ (t) => t.label === label ? { ...t, status: "in_progress" } : t.status === "in_progress" ? { ...t, status: "done" } : t
441
+ );
442
+ if (!todos.some((t) => t.label === label)) todos.push({ label, status: "in_progress" });
443
+ await sendTodos();
444
+ },
445
+ step: async (type, label) => {
446
+ await this.editMessage(messageId, { agentActivity: { type, label } });
447
+ },
448
+ complete: async (label) => {
449
+ todos = todos.map(
450
+ (t) => (label ? t.label === label : t.status === "in_progress") ? { ...t, status: "done" } : t
451
+ );
452
+ await sendTodos();
453
+ },
454
+ done: async () => {
455
+ await this.editMessage(messageId, { agentActivity: null });
456
+ }
457
+ };
458
+ }
365
459
  /**
366
460
  * Read a vessel's message history — the human-facing record, for re-reading
367
461
  * the channel (e.g. a stateless or just-restarted worker reconciling state).
@@ -547,6 +641,15 @@ var Vessels = class {
547
641
  user: raw.data.user ?? null
548
642
  };
549
643
  }
644
+ if (raw.event === "message.cancelled") {
645
+ return {
646
+ id: raw.data.message_id,
647
+ type: "message.cancelled",
648
+ timestamp: raw.timestamp,
649
+ vessel,
650
+ messageId: raw.data.message_id
651
+ };
652
+ }
550
653
  if (raw.event === "message.user") {
551
654
  return {
552
655
  id: raw.data.message_id,
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, ConfirmPreviewInteraction, Interaction, MessagePatch, PushManyOptions, PushManyPayload, PushOptions, PushPayload, TextInputInteraction, WebhookInteractionResponsePayload, WebhookInteractionResponsePayloadSchema, WebhookPayload, WebhookPayloadSchema, WebhookUserMessagePayload, WebhookUserMessagePayloadSchema, WebhookVesselCreatedPayload, WebhookVesselCreatedPayloadSchema } from '@vessels/types';
2
+ export { AgentActivity, AgentActivityType, AgentTodoInput, AgentTodoStatus, ApprovalInteraction, Attachment, Card, ChecklistInteraction, ChoiceInteraction, ConfirmPreviewInteraction, 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,53 @@ 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
+ }
31
73
  interface PushResponse {
32
74
  ok: true;
33
75
  messageId: string;
@@ -116,6 +158,20 @@ interface VesselCreatedEvent {
116
158
  content: string | null;
117
159
  };
118
160
  }
161
+ /**
162
+ * Fired when a human taps "Stop" on an in-progress (working) agent message in
163
+ * the app. Vessels does NOT force-stop anything — you decide what to do (abort
164
+ * a task, stop polling, ignore it). Delivery is synchronous: the app tells the
165
+ * human whether your handler accepted it (return 2xx) so they know cancel is
166
+ * wired up. `messageId` is the working agent message the human wants stopped.
167
+ */
168
+ interface MessageCancelledEvent {
169
+ id: string;
170
+ type: 'message.cancelled';
171
+ timestamp: string;
172
+ vessel: VesselContext;
173
+ messageId: string;
174
+ }
119
175
  /** A message in a vessel, as returned by getMessages — the human-facing record. */
120
176
  interface Message {
121
177
  id: string;
@@ -140,11 +196,27 @@ declare class Vessels {
140
196
  private _debug;
141
197
  constructor(config: VesselsConfig);
142
198
  private _fetch;
199
+ /**
200
+ * Check a push payload against the schema the server enforces, WITHOUT sending
201
+ * anything. Use this to confirm an agent's message complies before it goes
202
+ * live (e.g. in a test or a dry-run). `push()` runs the same check internally.
203
+ */
204
+ validatePush(payload: _vessels_types.PushOptions): ValidationResult;
205
+ /** Like {@link validatePush}, but for a `pushMany()` broadcast payload. */
206
+ validatePushMany(payload: _vessels_types.PushManyOptions): ValidationResult;
143
207
  push(payload: _vessels_types.PushOptions): Promise<PushResponse>;
144
208
  pushMany(payload: _vessels_types.PushManyOptions): Promise<PushManyResult>;
145
209
  editMessage(messageId: string, patch: _vessels_types.MessagePatch): Promise<{
146
210
  ok: true;
147
211
  }>;
212
+ /**
213
+ * Narrate a working message: declare a plan, file steps under each task as you
214
+ * go, and seal it when done. Sugar over {@link editMessage} — it tracks the
215
+ * todo list locally and PATCHes the full list each update (the server is
216
+ * authoritative and reconciles by label). The message must already exist; get
217
+ * its id from {@link push}.
218
+ */
219
+ activity(messageId: string): ActivityHandle;
148
220
  /**
149
221
  * Read a vessel's message history — the human-facing record, for re-reading
150
222
  * the channel (e.g. a stateless or just-restarted worker reconciling state).
@@ -207,7 +279,7 @@ declare class Vessels {
207
279
  }): _vessels_types.ConfirmPreviewInteraction;
208
280
  poll(options?: PollOptions): Promise<PollResponse>;
209
281
  verifyWebhook(body: string, signature: string, webhookSecret: string): Promise<boolean>;
210
- parseWebhookEvent(body: string, signature: string, webhookSecret: string): Promise<InteractionResponseEvent | UserMessageEvent | VesselCreatedEvent | null>;
282
+ parseWebhookEvent(body: string, signature: string, webhookSecret: string): Promise<InteractionResponseEvent | UserMessageEvent | VesselCreatedEvent | MessageCancelledEvent | null>;
211
283
  }
212
284
 
213
- export { AgentActivityTypes, type InteractionResponseEvent, type Message, 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 };
285
+ export { type ActivityHandle, AgentActivityTypes, type InteractionResponseEvent, type Message, type MessageCancelledEvent, type OriginMessage, type PollEvent, type PollOptions, type PollResponse, type PushManyResult, type PushResponse, 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, ConfirmPreviewInteraction, Interaction, MessagePatch, PushManyOptions, PushManyPayload, PushOptions, PushPayload, TextInputInteraction, WebhookInteractionResponsePayload, WebhookInteractionResponsePayloadSchema, WebhookPayload, WebhookPayloadSchema, WebhookUserMessagePayload, WebhookUserMessagePayloadSchema, WebhookVesselCreatedPayload, WebhookVesselCreatedPayloadSchema } from '@vessels/types';
2
+ export { AgentActivity, AgentActivityType, AgentTodoInput, AgentTodoStatus, ApprovalInteraction, Attachment, Card, ChecklistInteraction, ChoiceInteraction, ConfirmPreviewInteraction, 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,53 @@ 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
+ }
31
73
  interface PushResponse {
32
74
  ok: true;
33
75
  messageId: string;
@@ -116,6 +158,20 @@ interface VesselCreatedEvent {
116
158
  content: string | null;
117
159
  };
118
160
  }
161
+ /**
162
+ * Fired when a human taps "Stop" on an in-progress (working) agent message in
163
+ * the app. Vessels does NOT force-stop anything — you decide what to do (abort
164
+ * a task, stop polling, ignore it). Delivery is synchronous: the app tells the
165
+ * human whether your handler accepted it (return 2xx) so they know cancel is
166
+ * wired up. `messageId` is the working agent message the human wants stopped.
167
+ */
168
+ interface MessageCancelledEvent {
169
+ id: string;
170
+ type: 'message.cancelled';
171
+ timestamp: string;
172
+ vessel: VesselContext;
173
+ messageId: string;
174
+ }
119
175
  /** A message in a vessel, as returned by getMessages — the human-facing record. */
120
176
  interface Message {
121
177
  id: string;
@@ -140,11 +196,27 @@ declare class Vessels {
140
196
  private _debug;
141
197
  constructor(config: VesselsConfig);
142
198
  private _fetch;
199
+ /**
200
+ * Check a push payload against the schema the server enforces, WITHOUT sending
201
+ * anything. Use this to confirm an agent's message complies before it goes
202
+ * live (e.g. in a test or a dry-run). `push()` runs the same check internally.
203
+ */
204
+ validatePush(payload: _vessels_types.PushOptions): ValidationResult;
205
+ /** Like {@link validatePush}, but for a `pushMany()` broadcast payload. */
206
+ validatePushMany(payload: _vessels_types.PushManyOptions): ValidationResult;
143
207
  push(payload: _vessels_types.PushOptions): Promise<PushResponse>;
144
208
  pushMany(payload: _vessels_types.PushManyOptions): Promise<PushManyResult>;
145
209
  editMessage(messageId: string, patch: _vessels_types.MessagePatch): Promise<{
146
210
  ok: true;
147
211
  }>;
212
+ /**
213
+ * Narrate a working message: declare a plan, file steps under each task as you
214
+ * go, and seal it when done. Sugar over {@link editMessage} — it tracks the
215
+ * todo list locally and PATCHes the full list each update (the server is
216
+ * authoritative and reconciles by label). The message must already exist; get
217
+ * its id from {@link push}.
218
+ */
219
+ activity(messageId: string): ActivityHandle;
148
220
  /**
149
221
  * Read a vessel's message history — the human-facing record, for re-reading
150
222
  * the channel (e.g. a stateless or just-restarted worker reconciling state).
@@ -207,7 +279,7 @@ declare class Vessels {
207
279
  }): _vessels_types.ConfirmPreviewInteraction;
208
280
  poll(options?: PollOptions): Promise<PollResponse>;
209
281
  verifyWebhook(body: string, signature: string, webhookSecret: string): Promise<boolean>;
210
- parseWebhookEvent(body: string, signature: string, webhookSecret: string): Promise<InteractionResponseEvent | UserMessageEvent | VesselCreatedEvent | null>;
282
+ parseWebhookEvent(body: string, signature: string, webhookSecret: string): Promise<InteractionResponseEvent | UserMessageEvent | VesselCreatedEvent | MessageCancelledEvent | null>;
211
283
  }
212
284
 
213
- export { AgentActivityTypes, type InteractionResponseEvent, type Message, 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 };
285
+ export { type ActivityHandle, AgentActivityTypes, type InteractionResponseEvent, type Message, type MessageCancelledEvent, type OriginMessage, type PollEvent, type PollOptions, type PollResponse, type PushManyResult, type PushResponse, type UserMessageEvent, type ValidationResult, type VesselContext, type VesselCreatedEvent, Vessels, VesselsAuthError, type VesselsConfig, VesselsConflictError, VesselsRateLimitError, VesselsValidationError };
package/dist/index.js CHANGED
@@ -68,9 +68,17 @@ var InteractionSchema = z.discriminatedUnion("type", [
68
68
  ConfirmPreviewInteractionSchema
69
69
  ]);
70
70
  var AgentActivityTypeSchema = z.enum(["thinking", "searching", "tool_use", "browsing", "processing"]);
71
+ var AgentTodoStatusSchema = z.enum(["pending", "in_progress", "done"]);
72
+ var AgentTodoInputSchema = z.object({
73
+ label: z.string().min(1).max(200),
74
+ status: AgentTodoStatusSchema.optional()
75
+ });
71
76
  var AgentActivitySchema = z.object({
72
- type: AgentActivityTypeSchema,
73
- label: z.string().max(200).optional()
77
+ type: AgentActivityTypeSchema.optional(),
78
+ label: z.string().max(200).optional(),
79
+ todos: z.array(AgentTodoInputSchema).max(50).optional()
80
+ }).refine((d) => d.type != null || d.todos != null, {
81
+ message: "agentActivity requires `type` (a step) or `todos` (a plan)"
74
82
  });
75
83
  var CardFieldSchema = z.object({
76
84
  label: z.string().min(1),
@@ -85,7 +93,7 @@ var AttachmentSchema = z.discriminatedUnion("type", [
85
93
  z.object({ type: z.literal("file"), url: z.string().url(), filename: z.string().optional() })
86
94
  ]);
87
95
  var VesselStatusSchema = z.enum(["active", "waiting", "resolved"]);
88
- z.object({
96
+ var PushPayloadSchema = z.object({
89
97
  message: z.string().min(1).max(1e4).optional(),
90
98
  vessel: z.string().optional(),
91
99
  vesselTitle: z.string().optional(),
@@ -105,7 +113,7 @@ z.object({
105
113
  }).refine((d) => d.message || d.agentActivity, {
106
114
  message: "Either message or agentActivity is required"
107
115
  });
108
- z.object({
116
+ var PushManyPayloadSchema = z.object({
109
117
  vessels: z.array(z.string().min(1)).min(1).max(100),
110
118
  message: z.string().min(1).max(1e4),
111
119
  vesselTitle: z.string().optional(),
@@ -216,10 +224,21 @@ var WebhookVesselCreatedPayloadSchema = z.object({
216
224
  })
217
225
  })
218
226
  });
227
+ var WebhookMessageCancelledPayloadSchema = z.object({
228
+ event: z.literal("message.cancelled"),
229
+ vessel_id: z.string(),
230
+ workspace_id: z.string(),
231
+ timestamp: z.string(),
232
+ data: z.object({
233
+ message_id: z.string(),
234
+ vessel: WebhookVesselSchema
235
+ })
236
+ });
219
237
  var WebhookPayloadSchema = z.discriminatedUnion("event", [
220
238
  WebhookInteractionResponsePayloadSchema,
221
239
  WebhookUserMessagePayloadSchema,
222
- WebhookVesselCreatedPayloadSchema
240
+ WebhookVesselCreatedPayloadSchema,
241
+ WebhookMessageCancelledPayloadSchema
223
242
  ]);
224
243
 
225
244
  // src/index.ts
@@ -258,6 +277,13 @@ var VesselsConflictError = class extends Error {
258
277
  this.name = "VesselsConflictError";
259
278
  }
260
279
  };
280
+ function formatZodError(error) {
281
+ const errors = error.issues.map((i) => {
282
+ const path = i.path.join(".") || "(payload)";
283
+ return `${path}: ${i.message}`;
284
+ });
285
+ return { errors, details: error.flatten() };
286
+ }
261
287
  var Vessels = class {
262
288
  apiKey;
263
289
  baseUrl;
@@ -290,8 +316,33 @@ var Vessels = class {
290
316
  }
291
317
  return res;
292
318
  }
319
+ /**
320
+ * Check a push payload against the schema the server enforces, WITHOUT sending
321
+ * anything. Use this to confirm an agent's message complies before it goes
322
+ * live (e.g. in a test or a dry-run). `push()` runs the same check internally.
323
+ */
324
+ validatePush(payload) {
325
+ const { idempotencyKey: _ignored, ...body } = payload;
326
+ const result = PushPayloadSchema.safeParse(body);
327
+ if (result.success) return { valid: true, errors: [] };
328
+ const { errors, details } = formatZodError(result.error);
329
+ return { valid: false, errors, details };
330
+ }
331
+ /** Like {@link validatePush}, but for a `pushMany()` broadcast payload. */
332
+ validatePushMany(payload) {
333
+ const { idempotencyKey: _ignored, ...body } = payload;
334
+ const result = PushManyPayloadSchema.safeParse(body);
335
+ if (result.success) return { valid: true, errors: [] };
336
+ const { errors, details } = formatZodError(result.error);
337
+ return { valid: false, errors, details };
338
+ }
293
339
  async push(payload) {
294
340
  const { idempotencyKey, ...body } = payload;
341
+ const check = PushPayloadSchema.safeParse(body);
342
+ if (!check.success) {
343
+ const { errors, details } = formatZodError(check.error);
344
+ throw new VesselsValidationError(`Invalid push payload \u2014 ${errors.join("; ")}`, details);
345
+ }
295
346
  const res = await this._fetch(`${this.baseUrl}/api/v1/push`, {
296
347
  method: "POST",
297
348
  headers: {
@@ -317,6 +368,11 @@ var Vessels = class {
317
368
  }
318
369
  async pushMany(payload) {
319
370
  const { idempotencyKey, ...body } = payload;
371
+ const check = PushManyPayloadSchema.safeParse(body);
372
+ if (!check.success) {
373
+ const { errors, details } = formatZodError(check.error);
374
+ throw new VesselsValidationError(`Invalid pushMany payload \u2014 ${errors.join("; ")}`, details);
375
+ }
320
376
  const res = await this._fetch(`${this.baseUrl}/api/v1/push/many`, {
321
377
  method: "POST",
322
378
  headers: {
@@ -360,6 +416,44 @@ var Vessels = class {
360
416
  if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
361
417
  return { ok: true };
362
418
  }
419
+ /**
420
+ * Narrate a working message: declare a plan, file steps under each task as you
421
+ * go, and seal it when done. Sugar over {@link editMessage} — it tracks the
422
+ * todo list locally and PATCHes the full list each update (the server is
423
+ * authoritative and reconciles by label). The message must already exist; get
424
+ * its id from {@link push}.
425
+ */
426
+ activity(messageId) {
427
+ let todos = [];
428
+ const sendTodos = () => this.editMessage(messageId, { agentActivity: { todos } });
429
+ return {
430
+ plan: async (tasks) => {
431
+ todos = tasks.map(
432
+ (t) => typeof t === "string" ? { label: t, status: "pending" } : { label: t.label, status: t.status ?? "pending" }
433
+ );
434
+ await sendTodos();
435
+ },
436
+ start: async (label) => {
437
+ todos = todos.map(
438
+ (t) => t.label === label ? { ...t, status: "in_progress" } : t.status === "in_progress" ? { ...t, status: "done" } : t
439
+ );
440
+ if (!todos.some((t) => t.label === label)) todos.push({ label, status: "in_progress" });
441
+ await sendTodos();
442
+ },
443
+ step: async (type, label) => {
444
+ await this.editMessage(messageId, { agentActivity: { type, label } });
445
+ },
446
+ complete: async (label) => {
447
+ todos = todos.map(
448
+ (t) => (label ? t.label === label : t.status === "in_progress") ? { ...t, status: "done" } : t
449
+ );
450
+ await sendTodos();
451
+ },
452
+ done: async () => {
453
+ await this.editMessage(messageId, { agentActivity: null });
454
+ }
455
+ };
456
+ }
363
457
  /**
364
458
  * Read a vessel's message history — the human-facing record, for re-reading
365
459
  * the channel (e.g. a stateless or just-restarted worker reconciling state).
@@ -545,6 +639,15 @@ var Vessels = class {
545
639
  user: raw.data.user ?? null
546
640
  };
547
641
  }
642
+ if (raw.event === "message.cancelled") {
643
+ return {
644
+ id: raw.data.message_id,
645
+ type: "message.cancelled",
646
+ timestamp: raw.timestamp,
647
+ vessel,
648
+ messageId: raw.data.message_id
649
+ };
650
+ }
548
651
  if (raw.event === "message.user") {
549
652
  return {
550
653
  id: raw.data.message_id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vessels-sdk",
3
- "version": "0.8.0",
3
+ "version": "0.11.0",
4
4
  "description": "Let your agent reach you. Official Vessels SDK.",
5
5
  "type": "module",
6
6
  "exports": {