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 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 5 types — see helpers below). Max one per message; immutable after the human responds. |
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 used by `confirm_preview` interactions. |
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
- #### `vessels.confirmPreview(opts)`
276
+ #### Review-and-decide (the old `confirmPreview`)
254
277
 
255
- Approve or reject after reviewing an external preview (draft email, document, image).
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.confirmPreview({
259
- prompt: 'Review the draft email before sending.',
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
- previewLabel: 'View draft',
262
- approveLabel: 'Send',
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' | 'confirm_preview'
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
- }).refine((d) => d.message || d.agentActivity, {
108
- message: "Either message or agentActivity is required"
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, 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, 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, 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, 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
- }).refine((d) => d.message || d.agentActivity, {
106
- message: "Either message or agentActivity is required"
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vessels-sdk",
3
- "version": "0.9.0",
3
+ "version": "0.12.0",
4
4
  "description": "Let your agent reach you. Official Vessels SDK.",
5
5
  "type": "module",
6
6
  "exports": {