vessels-sdk 0.9.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(),
@@ -271,6 +279,13 @@ var VesselsConflictError = class extends Error {
271
279
  this.name = "VesselsConflictError";
272
280
  }
273
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
+ }
274
289
  var Vessels = class {
275
290
  apiKey;
276
291
  baseUrl;
@@ -303,8 +318,33 @@ var Vessels = class {
303
318
  }
304
319
  return res;
305
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
+ }
306
341
  async push(payload) {
307
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
+ }
308
348
  const res = await this._fetch(`${this.baseUrl}/api/v1/push`, {
309
349
  method: "POST",
310
350
  headers: {
@@ -330,6 +370,11 @@ var Vessels = class {
330
370
  }
331
371
  async pushMany(payload) {
332
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
+ }
333
378
  const res = await this._fetch(`${this.baseUrl}/api/v1/push/many`, {
334
379
  method: "POST",
335
380
  headers: {
@@ -373,6 +418,44 @@ var Vessels = class {
373
418
  if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
374
419
  return { ok: true };
375
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
+ }
376
459
  /**
377
460
  * Read a vessel's message history — the human-facing record, for re-reading
378
461
  * the channel (e.g. a stateless or just-restarted worker reconciling state).
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;
@@ -154,11 +196,27 @@ declare class Vessels {
154
196
  private _debug;
155
197
  constructor(config: VesselsConfig);
156
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;
157
207
  push(payload: _vessels_types.PushOptions): Promise<PushResponse>;
158
208
  pushMany(payload: _vessels_types.PushManyOptions): Promise<PushManyResult>;
159
209
  editMessage(messageId: string, patch: _vessels_types.MessagePatch): Promise<{
160
210
  ok: true;
161
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;
162
220
  /**
163
221
  * Read a vessel's message history — the human-facing record, for re-reading
164
222
  * the channel (e.g. a stateless or just-restarted worker reconciling state).
@@ -224,4 +282,4 @@ declare class Vessels {
224
282
  parseWebhookEvent(body: string, signature: string, webhookSecret: string): Promise<InteractionResponseEvent | UserMessageEvent | VesselCreatedEvent | MessageCancelledEvent | null>;
225
283
  }
226
284
 
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 };
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;
@@ -154,11 +196,27 @@ declare class Vessels {
154
196
  private _debug;
155
197
  constructor(config: VesselsConfig);
156
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;
157
207
  push(payload: _vessels_types.PushOptions): Promise<PushResponse>;
158
208
  pushMany(payload: _vessels_types.PushManyOptions): Promise<PushManyResult>;
159
209
  editMessage(messageId: string, patch: _vessels_types.MessagePatch): Promise<{
160
210
  ok: true;
161
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;
162
220
  /**
163
221
  * Read a vessel's message history — the human-facing record, for re-reading
164
222
  * the channel (e.g. a stateless or just-restarted worker reconciling state).
@@ -224,4 +282,4 @@ declare class Vessels {
224
282
  parseWebhookEvent(body: string, signature: string, webhookSecret: string): Promise<InteractionResponseEvent | UserMessageEvent | VesselCreatedEvent | MessageCancelledEvent | null>;
225
283
  }
226
284
 
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 };
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(),
@@ -269,6 +277,13 @@ var VesselsConflictError = class extends Error {
269
277
  this.name = "VesselsConflictError";
270
278
  }
271
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
+ }
272
287
  var Vessels = class {
273
288
  apiKey;
274
289
  baseUrl;
@@ -301,8 +316,33 @@ var Vessels = class {
301
316
  }
302
317
  return res;
303
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
+ }
304
339
  async push(payload) {
305
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
+ }
306
346
  const res = await this._fetch(`${this.baseUrl}/api/v1/push`, {
307
347
  method: "POST",
308
348
  headers: {
@@ -328,6 +368,11 @@ var Vessels = class {
328
368
  }
329
369
  async pushMany(payload) {
330
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
+ }
331
376
  const res = await this._fetch(`${this.baseUrl}/api/v1/push/many`, {
332
377
  method: "POST",
333
378
  headers: {
@@ -371,6 +416,44 @@ var Vessels = class {
371
416
  if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
372
417
  return { ok: true };
373
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
+ }
374
457
  /**
375
458
  * Read a vessel's message history — the human-facing record, for re-reading
376
459
  * the channel (e.g. a stateless or just-restarted worker reconciling state).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vessels-sdk",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Let your agent reach you. Official Vessels SDK.",
5
5
  "type": "module",
6
6
  "exports": {