vessels-sdk 0.11.0 → 0.13.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
@@ -273,19 +273,18 @@ vessels.textInput({
273
273
 
274
274
  Response shape: `{ text: string }`
275
275
 
276
- #### `vessels.confirmPreview(opts)`
276
+ #### Review-and-decide (the old `confirmPreview`)
277
277
 
278
- 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`:
279
280
 
280
281
  ```typescript
281
- vessels.confirmPreview({
282
- prompt: 'Review the draft email before sending.',
282
+ await vessels.push({
283
+ vessel: 'draft-123',
284
+ message: 'Draft email ready for review.',
283
285
  previewUrl: 'https://your-app.com/drafts/123',
284
- previewLabel: 'View draft',
285
- approveLabel: 'Send',
286
- rejectLabel: 'Edit',
287
- reasonRequiredOnReject: true,
288
- })
286
+ interaction: vessels.approval({ prompt: 'Send this email?', approveLabel: 'Send', rejectLabel: 'Edit' }),
287
+ });
289
288
  ```
290
289
 
291
290
  Response shape: `{ action: 'approved' | 'rejected', reason?: string }`
@@ -311,7 +310,7 @@ const { events, hasMore } = await vessels.poll({ ack: true });
311
310
 
312
311
  for (const event of events) {
313
312
  if (event.type === 'interaction.response') {
314
- // event.interactionType — 'approval' | 'choice' | 'checklist' | 'text_input' | 'confirm_preview'
313
+ // event.interactionType — 'approval' | 'choice' | 'checklist' | 'text_input'
315
314
  // event.response — response shape depends on interactionType (see above)
316
315
  // event.interactionMetadata — metadata object you passed when creating the interaction, or null
317
316
  // event.messageId — UUID of the message containing the interaction
@@ -490,7 +489,6 @@ import type {
490
489
  ChoiceInteraction,
491
490
  ChecklistInteraction,
492
491
  TextInputInteraction,
493
- ConfirmPreviewInteraction,
494
492
  } from 'vessels-sdk';
495
493
  ```
496
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,22 +51,11 @@ 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"]);
73
61
  var AgentTodoStatusSchema = zod.z.enum(["pending", "in_progress", "done"]);
@@ -87,7 +75,10 @@ var CardFieldSchema = zod.z.object({
87
75
  value: zod.z.string()
88
76
  });
89
77
  var CardSchema = zod.z.object({
90
- title: zod.z.string().min(1),
78
+ // Optional: a glance-facts card under a surface takes its heading from the
79
+ // surface `title`, so a card title is redundant there. Still allowed (e.g. a
80
+ // standalone card on a bubble, or a pinned card).
81
+ title: zod.z.string().min(1).optional(),
91
82
  fields: zod.z.array(CardFieldSchema)
92
83
  });
93
84
  var AttachmentSchema = zod.z.discriminatedUnion("type", [
@@ -95,6 +86,8 @@ var AttachmentSchema = zod.z.discriminatedUnion("type", [
95
86
  zod.z.object({ type: zod.z.literal("file"), url: zod.z.string().url(), filename: zod.z.string().optional() })
96
87
  ]);
97
88
  var VesselStatusSchema = zod.z.enum(["active", "waiting", "resolved"]);
89
+ var DisplaySchema = zod.z.enum(["bubble", "document"]);
90
+ var KindSchema = zod.z.enum(["bubble", "surface"]);
98
91
  var PushPayloadSchema = zod.z.object({
99
92
  message: zod.z.string().min(1).max(1e4).optional(),
100
93
  vessel: zod.z.string().optional(),
@@ -111,9 +104,23 @@ var PushPayloadSchema = zod.z.object({
111
104
  labels: zod.z.array(zod.z.string().min(1).max(50)).max(10).optional(),
112
105
  attachments: zod.z.array(AttachmentSchema).max(10).optional(),
113
106
  suggestions: zod.z.array(zod.z.string().min(1).max(500)).max(5).optional(),
114
- agentActivity: AgentActivitySchema.optional()
115
- }).refine((d) => d.message || d.agentActivity, {
116
- message: "Either message or agentActivity is required"
107
+ agentActivity: AgentActivitySchema.optional(),
108
+ /**
109
+ * Live token-stream buffer an ephemeral monospace block the human watches
110
+ * fill in real time (set it on the message you create, then keep replacing it
111
+ * via `PATCH /messages/:id`, and clear it with `null` when done). It is a live
112
+ * window, not a transcript: send the tail you want shown (the SDK trims to the
113
+ * last 8000 chars). Plaintext, like agentActivity. Vanishes when cleared.
114
+ */
115
+ tokenStream: zod.z.string().max(8e3).optional(),
116
+ /** Bubble (chat) vs surface (composed artifact). Defaults from interaction/card. */
117
+ kind: KindSchema.optional(),
118
+ /** Surface heading. Ignored on bubbles. */
119
+ title: zod.z.string().max(200).optional(),
120
+ /** @deprecated legacy presentation hint — use `kind`. 'document' → surface. */
121
+ display: DisplaySchema.optional()
122
+ }).refine((d) => d.message || d.agentActivity || d.tokenStream, {
123
+ message: "One of message, agentActivity, or tokenStream is required"
117
124
  });
118
125
  var PushManyPayloadSchema = zod.z.object({
119
126
  vessels: zod.z.array(zod.z.string().min(1)).min(1).max(100),
@@ -128,14 +135,26 @@ var PushManyPayloadSchema = zod.z.object({
128
135
  metadata: zod.z.record(zod.z.unknown()).refine(
129
136
  (v) => JSON.stringify(v).length < 16e3,
130
137
  "metadata exceeds 16KB limit"
131
- ).optional()
138
+ ).optional(),
139
+ kind: KindSchema.optional(),
140
+ title: zod.z.string().max(200).optional(),
141
+ /** @deprecated use `kind`. */
142
+ display: DisplaySchema.optional()
132
143
  });
133
144
  zod.z.object({
134
145
  content: zod.z.string().min(1).max(1e4).optional(),
135
146
  card: CardSchema.nullable().optional(),
136
147
  attachments: zod.z.array(AttachmentSchema).max(10).nullable().optional(),
137
148
  suggestions: zod.z.array(zod.z.string().min(1).max(500)).max(5).nullable().optional(),
138
- agentActivity: AgentActivitySchema.nullable().optional()
149
+ agentActivity: AgentActivitySchema.nullable().optional(),
150
+ /** Replace the live token-stream window, or `null` to clear it (block vanishes). */
151
+ tokenStream: zod.z.string().max(8e3).nullable().optional(),
152
+ /** Switch kind: 'surface' = full-width artifact, 'bubble' or null = chat bubble. */
153
+ kind: KindSchema.nullable().optional(),
154
+ /** Update the surface heading, or `null` to clear it. */
155
+ title: zod.z.string().max(200).nullable().optional(),
156
+ /** @deprecated use `kind`. */
157
+ display: DisplaySchema.nullable().optional()
139
158
  }).refine((d) => Object.values(d).some((v) => v !== void 0), {
140
159
  message: "At least one field required"
141
160
  });
@@ -153,16 +172,11 @@ var ChecklistResponseSchema = zod.z.object({
153
172
  var TextInputResponseSchema = zod.z.object({
154
173
  text: zod.z.string()
155
174
  });
156
- var ConfirmPreviewResponseSchema = zod.z.object({
157
- action: zod.z.enum(["approved", "rejected"]),
158
- reason: zod.z.string().optional()
159
- });
160
175
  zod.z.discriminatedUnion("interactionType", [
161
176
  zod.z.object({ interactionType: zod.z.literal("approval"), response: ApprovalResponseSchema }),
162
177
  zod.z.object({ interactionType: zod.z.literal("choice"), response: ChoiceResponseSchema }),
163
178
  zod.z.object({ interactionType: zod.z.literal("checklist"), response: ChecklistResponseSchema }),
164
- zod.z.object({ interactionType: zod.z.literal("text_input"), response: TextInputResponseSchema }),
165
- zod.z.object({ interactionType: zod.z.literal("confirm_preview"), response: ConfirmPreviewResponseSchema })
179
+ zod.z.object({ interactionType: zod.z.literal("text_input"), response: TextInputResponseSchema })
166
180
  ]);
167
181
  var WebhookVesselSchema = zod.z.object({
168
182
  id: zod.z.string(),
@@ -368,6 +382,31 @@ var Vessels = class {
368
382
  replayed: res.headers.get("Idempotent-Replayed") === "true"
369
383
  };
370
384
  }
385
+ /**
386
+ * Create a **surface** — a full-width composed artifact the human reviews and
387
+ * optionally acts on, rendered as one piece: a `title` heading, an optional
388
+ * `card` of glance-facts, a block-markdown `body` (tables, bullet/numbered
389
+ * lists, blockquotes, bold headings, links), and an optional `interaction`
390
+ * (the action bar). Use this for an email/message draft, a quote, an invoice
391
+ * review, a proposal, a report — anything substantial and structured.
392
+ *
393
+ * Sugar over {@link push}: it sets `kind: 'surface'` and maps `body` to the
394
+ * message content. For conversation, status updates and quick questions, use
395
+ * {@link push} instead (a chat **bubble** — the human just replies).
396
+ *
397
+ * @example
398
+ * await vessels.surface({
399
+ * vessel: 'inv-42',
400
+ * title: 'GreenTurf Invoice #GTL-2025-042',
401
+ * card: { fields: [{ label: 'Amount Due', value: '$7,400' }] },
402
+ * body: 'Invoice is **$1,200 over** the usual rate.\n\n| Item | Amount |\n|---|---:|\n| Overage | +$1,200 |',
403
+ * interaction: vessels.approval({ prompt: 'Approve the $7,400 payment?' }),
404
+ * });
405
+ */
406
+ async surface(options) {
407
+ const { body, ...rest } = options;
408
+ return this.push({ ...rest, message: body, kind: "surface" });
409
+ }
371
410
  async pushMany(payload) {
372
411
  const { idempotencyKey, ...body } = payload;
373
412
  const check = PushManyPayloadSchema.safeParse(body);
@@ -456,6 +495,79 @@ var Vessels = class {
456
495
  }
457
496
  };
458
497
  }
498
+ /**
499
+ * Stream live tokens into a message: the human watches a monospace block fill
500
+ * in real time. Sugar over {@link editMessage} — it keeps the buffer locally
501
+ * and PATCHes a throttled, tail-trimmed window (replace-semantics, so a lost
502
+ * flush self-heals on the next one). Seal with `done()` to clear the block.
503
+ *
504
+ * @param messageId the message to stream into (create it first via `push`).
505
+ * @param opts.throttleMs minimum gap between server flushes (default 120ms).
506
+ * @param opts.maxChars longest window kept; older text scrolls off (default 8000).
507
+ */
508
+ stream(messageId, opts) {
509
+ const throttleMs = opts?.throttleMs ?? 120;
510
+ const maxChars = opts?.maxChars ?? 8e3;
511
+ let buffer = "";
512
+ let lastSent = null;
513
+ let timer = null;
514
+ let pending = Promise.resolve();
515
+ const windowed = () => buffer.length > maxChars ? buffer.slice(-maxChars) : buffer;
516
+ const flushNow = () => {
517
+ const text = windowed();
518
+ if (text === lastSent) return pending;
519
+ lastSent = text;
520
+ pending = this.editMessage(messageId, { tokenStream: text }).catch(() => {
521
+ lastSent = null;
522
+ });
523
+ return pending;
524
+ };
525
+ const cancelTimer = () => {
526
+ if (timer) {
527
+ clearTimeout(timer);
528
+ timer = null;
529
+ }
530
+ };
531
+ const schedule = () => {
532
+ if (timer) return;
533
+ timer = setTimeout(() => {
534
+ timer = null;
535
+ void flushNow();
536
+ }, throttleMs);
537
+ timer?.unref?.();
538
+ };
539
+ const settle = async () => {
540
+ cancelTimer();
541
+ await pending.catch(() => {
542
+ });
543
+ };
544
+ return {
545
+ write: (text) => {
546
+ buffer += text;
547
+ schedule();
548
+ },
549
+ set: async (text) => {
550
+ buffer = text;
551
+ cancelTimer();
552
+ await flushNow();
553
+ },
554
+ // Clear the stream AND seal any working card (agentActivity: null is a no-op
555
+ // when there's none), so the final content actually renders.
556
+ done: async (finalContent) => {
557
+ await settle();
558
+ await this.editMessage(messageId, {
559
+ tokenStream: null,
560
+ agentActivity: null,
561
+ ...finalContent != null ? { content: finalContent } : {}
562
+ });
563
+ },
564
+ // Remove only the stream; leave the working card alone.
565
+ clear: async () => {
566
+ await settle();
567
+ await this.editMessage(messageId, { tokenStream: null });
568
+ }
569
+ };
570
+ }
459
571
  /**
460
572
  * Read a vessel's message history — the human-facing record, for re-reading
461
573
  * the channel (e.g. a stateless or just-restarted worker reconciling state).
@@ -501,9 +613,6 @@ var Vessels = class {
501
613
  textInput(opts) {
502
614
  return { type: "text_input", ...opts };
503
615
  }
504
- confirmPreview(opts) {
505
- return { type: "confirm_preview", ...opts };
506
- }
507
616
  async poll(options = {}) {
508
617
  const { since, limit = 50, ack = true } = options;
509
618
  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, 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';
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";
@@ -70,6 +70,34 @@ interface ActivityHandle {
70
70
  /** Seal the activity — finishes the open step and any in-progress task. */
71
71
  done(): Promise<void>;
72
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
+ }
73
101
  interface PushResponse {
74
102
  ok: true;
75
103
  messageId: string;
@@ -78,6 +106,14 @@ interface PushResponse {
78
106
  /** True when this response is a replay of an earlier request with the same idempotency key. */
79
107
  replayed: boolean;
80
108
  }
109
+ /**
110
+ * Options for {@link Vessels.surface}. Same as a push, but the artifact text is
111
+ * `body` (not `message`) and `kind` is forced to `'surface'`.
112
+ */
113
+ type SurfaceOptions = Omit<_vessels_types.PushOptions, 'message' | 'kind'> & {
114
+ /** The artifact body — block markdown (tables, lists, blockquotes, bold headings, links). */
115
+ body: string;
116
+ };
81
117
  interface PushManyResult {
82
118
  ok: true;
83
119
  results: Array<{
@@ -205,6 +241,28 @@ declare class Vessels {
205
241
  /** Like {@link validatePush}, but for a `pushMany()` broadcast payload. */
206
242
  validatePushMany(payload: _vessels_types.PushManyOptions): ValidationResult;
207
243
  push(payload: _vessels_types.PushOptions): Promise<PushResponse>;
244
+ /**
245
+ * Create a **surface** — a full-width composed artifact the human reviews and
246
+ * optionally acts on, rendered as one piece: a `title` heading, an optional
247
+ * `card` of glance-facts, a block-markdown `body` (tables, bullet/numbered
248
+ * lists, blockquotes, bold headings, links), and an optional `interaction`
249
+ * (the action bar). Use this for an email/message draft, a quote, an invoice
250
+ * review, a proposal, a report — anything substantial and structured.
251
+ *
252
+ * Sugar over {@link push}: it sets `kind: 'surface'` and maps `body` to the
253
+ * message content. For conversation, status updates and quick questions, use
254
+ * {@link push} instead (a chat **bubble** — the human just replies).
255
+ *
256
+ * @example
257
+ * await vessels.surface({
258
+ * vessel: 'inv-42',
259
+ * title: 'GreenTurf Invoice #GTL-2025-042',
260
+ * card: { fields: [{ label: 'Amount Due', value: '$7,400' }] },
261
+ * body: 'Invoice is **$1,200 over** the usual rate.\n\n| Item | Amount |\n|---|---:|\n| Overage | +$1,200 |',
262
+ * interaction: vessels.approval({ prompt: 'Approve the $7,400 payment?' }),
263
+ * });
264
+ */
265
+ surface(options: SurfaceOptions): Promise<PushResponse>;
208
266
  pushMany(payload: _vessels_types.PushManyOptions): Promise<PushManyResult>;
209
267
  editMessage(messageId: string, patch: _vessels_types.MessagePatch): Promise<{
210
268
  ok: true;
@@ -217,6 +275,20 @@ declare class Vessels {
217
275
  * its id from {@link push}.
218
276
  */
219
277
  activity(messageId: string): ActivityHandle;
278
+ /**
279
+ * Stream live tokens into a message: the human watches a monospace block fill
280
+ * in real time. Sugar over {@link editMessage} — it keeps the buffer locally
281
+ * and PATCHes a throttled, tail-trimmed window (replace-semantics, so a lost
282
+ * flush self-heals on the next one). Seal with `done()` to clear the block.
283
+ *
284
+ * @param messageId the message to stream into (create it first via `push`).
285
+ * @param opts.throttleMs minimum gap between server flushes (default 120ms).
286
+ * @param opts.maxChars longest window kept; older text scrolls off (default 8000).
287
+ */
288
+ stream(messageId: string, opts?: {
289
+ throttleMs?: number;
290
+ maxChars?: number;
291
+ }): StreamHandle;
220
292
  /**
221
293
  * Read a vessel's message history — the human-facing record, for re-reading
222
294
  * the channel (e.g. a stateless or just-restarted worker reconciling state).
@@ -268,18 +340,9 @@ declare class Vessels {
268
340
  submitLabel?: string;
269
341
  metadata?: Record<string, unknown>;
270
342
  }): _vessels_types.TextInputInteraction;
271
- confirmPreview(opts: {
272
- prompt: string;
273
- previewUrl: string;
274
- previewLabel?: string;
275
- approveLabel?: string;
276
- rejectLabel?: string;
277
- reasonRequiredOnReject?: boolean;
278
- metadata?: Record<string, unknown>;
279
- }): _vessels_types.ConfirmPreviewInteraction;
280
343
  poll(options?: PollOptions): Promise<PollResponse>;
281
344
  verifyWebhook(body: string, signature: string, webhookSecret: string): Promise<boolean>;
282
345
  parseWebhookEvent(body: string, signature: string, webhookSecret: string): Promise<InteractionResponseEvent | UserMessageEvent | VesselCreatedEvent | MessageCancelledEvent | null>;
283
346
  }
284
347
 
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 };
348
+ 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 SurfaceOptions, 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, 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';
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";
@@ -70,6 +70,34 @@ interface ActivityHandle {
70
70
  /** Seal the activity — finishes the open step and any in-progress task. */
71
71
  done(): Promise<void>;
72
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
+ }
73
101
  interface PushResponse {
74
102
  ok: true;
75
103
  messageId: string;
@@ -78,6 +106,14 @@ interface PushResponse {
78
106
  /** True when this response is a replay of an earlier request with the same idempotency key. */
79
107
  replayed: boolean;
80
108
  }
109
+ /**
110
+ * Options for {@link Vessels.surface}. Same as a push, but the artifact text is
111
+ * `body` (not `message`) and `kind` is forced to `'surface'`.
112
+ */
113
+ type SurfaceOptions = Omit<_vessels_types.PushOptions, 'message' | 'kind'> & {
114
+ /** The artifact body — block markdown (tables, lists, blockquotes, bold headings, links). */
115
+ body: string;
116
+ };
81
117
  interface PushManyResult {
82
118
  ok: true;
83
119
  results: Array<{
@@ -205,6 +241,28 @@ declare class Vessels {
205
241
  /** Like {@link validatePush}, but for a `pushMany()` broadcast payload. */
206
242
  validatePushMany(payload: _vessels_types.PushManyOptions): ValidationResult;
207
243
  push(payload: _vessels_types.PushOptions): Promise<PushResponse>;
244
+ /**
245
+ * Create a **surface** — a full-width composed artifact the human reviews and
246
+ * optionally acts on, rendered as one piece: a `title` heading, an optional
247
+ * `card` of glance-facts, a block-markdown `body` (tables, bullet/numbered
248
+ * lists, blockquotes, bold headings, links), and an optional `interaction`
249
+ * (the action bar). Use this for an email/message draft, a quote, an invoice
250
+ * review, a proposal, a report — anything substantial and structured.
251
+ *
252
+ * Sugar over {@link push}: it sets `kind: 'surface'` and maps `body` to the
253
+ * message content. For conversation, status updates and quick questions, use
254
+ * {@link push} instead (a chat **bubble** — the human just replies).
255
+ *
256
+ * @example
257
+ * await vessels.surface({
258
+ * vessel: 'inv-42',
259
+ * title: 'GreenTurf Invoice #GTL-2025-042',
260
+ * card: { fields: [{ label: 'Amount Due', value: '$7,400' }] },
261
+ * body: 'Invoice is **$1,200 over** the usual rate.\n\n| Item | Amount |\n|---|---:|\n| Overage | +$1,200 |',
262
+ * interaction: vessels.approval({ prompt: 'Approve the $7,400 payment?' }),
263
+ * });
264
+ */
265
+ surface(options: SurfaceOptions): Promise<PushResponse>;
208
266
  pushMany(payload: _vessels_types.PushManyOptions): Promise<PushManyResult>;
209
267
  editMessage(messageId: string, patch: _vessels_types.MessagePatch): Promise<{
210
268
  ok: true;
@@ -217,6 +275,20 @@ declare class Vessels {
217
275
  * its id from {@link push}.
218
276
  */
219
277
  activity(messageId: string): ActivityHandle;
278
+ /**
279
+ * Stream live tokens into a message: the human watches a monospace block fill
280
+ * in real time. Sugar over {@link editMessage} — it keeps the buffer locally
281
+ * and PATCHes a throttled, tail-trimmed window (replace-semantics, so a lost
282
+ * flush self-heals on the next one). Seal with `done()` to clear the block.
283
+ *
284
+ * @param messageId the message to stream into (create it first via `push`).
285
+ * @param opts.throttleMs minimum gap between server flushes (default 120ms).
286
+ * @param opts.maxChars longest window kept; older text scrolls off (default 8000).
287
+ */
288
+ stream(messageId: string, opts?: {
289
+ throttleMs?: number;
290
+ maxChars?: number;
291
+ }): StreamHandle;
220
292
  /**
221
293
  * Read a vessel's message history — the human-facing record, for re-reading
222
294
  * the channel (e.g. a stateless or just-restarted worker reconciling state).
@@ -268,18 +340,9 @@ declare class Vessels {
268
340
  submitLabel?: string;
269
341
  metadata?: Record<string, unknown>;
270
342
  }): _vessels_types.TextInputInteraction;
271
- confirmPreview(opts: {
272
- prompt: string;
273
- previewUrl: string;
274
- previewLabel?: string;
275
- approveLabel?: string;
276
- rejectLabel?: string;
277
- reasonRequiredOnReject?: boolean;
278
- metadata?: Record<string, unknown>;
279
- }): _vessels_types.ConfirmPreviewInteraction;
280
343
  poll(options?: PollOptions): Promise<PollResponse>;
281
344
  verifyWebhook(body: string, signature: string, webhookSecret: string): Promise<boolean>;
282
345
  parseWebhookEvent(body: string, signature: string, webhookSecret: string): Promise<InteractionResponseEvent | UserMessageEvent | VesselCreatedEvent | MessageCancelledEvent | null>;
283
346
  }
284
347
 
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 };
348
+ 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 SurfaceOptions, 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,22 +49,11 @@ 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"]);
71
59
  var AgentTodoStatusSchema = z.enum(["pending", "in_progress", "done"]);
@@ -85,7 +73,10 @@ var CardFieldSchema = z.object({
85
73
  value: z.string()
86
74
  });
87
75
  var CardSchema = z.object({
88
- title: z.string().min(1),
76
+ // Optional: a glance-facts card under a surface takes its heading from the
77
+ // surface `title`, so a card title is redundant there. Still allowed (e.g. a
78
+ // standalone card on a bubble, or a pinned card).
79
+ title: z.string().min(1).optional(),
89
80
  fields: z.array(CardFieldSchema)
90
81
  });
91
82
  var AttachmentSchema = z.discriminatedUnion("type", [
@@ -93,6 +84,8 @@ var AttachmentSchema = z.discriminatedUnion("type", [
93
84
  z.object({ type: z.literal("file"), url: z.string().url(), filename: z.string().optional() })
94
85
  ]);
95
86
  var VesselStatusSchema = z.enum(["active", "waiting", "resolved"]);
87
+ var DisplaySchema = z.enum(["bubble", "document"]);
88
+ var KindSchema = z.enum(["bubble", "surface"]);
96
89
  var PushPayloadSchema = z.object({
97
90
  message: z.string().min(1).max(1e4).optional(),
98
91
  vessel: z.string().optional(),
@@ -109,9 +102,23 @@ var PushPayloadSchema = z.object({
109
102
  labels: z.array(z.string().min(1).max(50)).max(10).optional(),
110
103
  attachments: z.array(AttachmentSchema).max(10).optional(),
111
104
  suggestions: z.array(z.string().min(1).max(500)).max(5).optional(),
112
- agentActivity: AgentActivitySchema.optional()
113
- }).refine((d) => d.message || d.agentActivity, {
114
- message: "Either message or agentActivity is required"
105
+ agentActivity: AgentActivitySchema.optional(),
106
+ /**
107
+ * Live token-stream buffer an ephemeral monospace block the human watches
108
+ * fill in real time (set it on the message you create, then keep replacing it
109
+ * via `PATCH /messages/:id`, and clear it with `null` when done). It is a live
110
+ * window, not a transcript: send the tail you want shown (the SDK trims to the
111
+ * last 8000 chars). Plaintext, like agentActivity. Vanishes when cleared.
112
+ */
113
+ tokenStream: z.string().max(8e3).optional(),
114
+ /** Bubble (chat) vs surface (composed artifact). Defaults from interaction/card. */
115
+ kind: KindSchema.optional(),
116
+ /** Surface heading. Ignored on bubbles. */
117
+ title: z.string().max(200).optional(),
118
+ /** @deprecated legacy presentation hint — use `kind`. 'document' → surface. */
119
+ display: DisplaySchema.optional()
120
+ }).refine((d) => d.message || d.agentActivity || d.tokenStream, {
121
+ message: "One of message, agentActivity, or tokenStream is required"
115
122
  });
116
123
  var PushManyPayloadSchema = z.object({
117
124
  vessels: z.array(z.string().min(1)).min(1).max(100),
@@ -126,14 +133,26 @@ var PushManyPayloadSchema = z.object({
126
133
  metadata: z.record(z.unknown()).refine(
127
134
  (v) => JSON.stringify(v).length < 16e3,
128
135
  "metadata exceeds 16KB limit"
129
- ).optional()
136
+ ).optional(),
137
+ kind: KindSchema.optional(),
138
+ title: z.string().max(200).optional(),
139
+ /** @deprecated use `kind`. */
140
+ display: DisplaySchema.optional()
130
141
  });
131
142
  z.object({
132
143
  content: z.string().min(1).max(1e4).optional(),
133
144
  card: CardSchema.nullable().optional(),
134
145
  attachments: z.array(AttachmentSchema).max(10).nullable().optional(),
135
146
  suggestions: z.array(z.string().min(1).max(500)).max(5).nullable().optional(),
136
- agentActivity: AgentActivitySchema.nullable().optional()
147
+ agentActivity: AgentActivitySchema.nullable().optional(),
148
+ /** Replace the live token-stream window, or `null` to clear it (block vanishes). */
149
+ tokenStream: z.string().max(8e3).nullable().optional(),
150
+ /** Switch kind: 'surface' = full-width artifact, 'bubble' or null = chat bubble. */
151
+ kind: KindSchema.nullable().optional(),
152
+ /** Update the surface heading, or `null` to clear it. */
153
+ title: z.string().max(200).nullable().optional(),
154
+ /** @deprecated use `kind`. */
155
+ display: DisplaySchema.nullable().optional()
137
156
  }).refine((d) => Object.values(d).some((v) => v !== void 0), {
138
157
  message: "At least one field required"
139
158
  });
@@ -151,16 +170,11 @@ var ChecklistResponseSchema = z.object({
151
170
  var TextInputResponseSchema = z.object({
152
171
  text: z.string()
153
172
  });
154
- var ConfirmPreviewResponseSchema = z.object({
155
- action: z.enum(["approved", "rejected"]),
156
- reason: z.string().optional()
157
- });
158
173
  z.discriminatedUnion("interactionType", [
159
174
  z.object({ interactionType: z.literal("approval"), response: ApprovalResponseSchema }),
160
175
  z.object({ interactionType: z.literal("choice"), response: ChoiceResponseSchema }),
161
176
  z.object({ interactionType: z.literal("checklist"), response: ChecklistResponseSchema }),
162
- z.object({ interactionType: z.literal("text_input"), response: TextInputResponseSchema }),
163
- z.object({ interactionType: z.literal("confirm_preview"), response: ConfirmPreviewResponseSchema })
177
+ z.object({ interactionType: z.literal("text_input"), response: TextInputResponseSchema })
164
178
  ]);
165
179
  var WebhookVesselSchema = z.object({
166
180
  id: z.string(),
@@ -366,6 +380,31 @@ var Vessels = class {
366
380
  replayed: res.headers.get("Idempotent-Replayed") === "true"
367
381
  };
368
382
  }
383
+ /**
384
+ * Create a **surface** — a full-width composed artifact the human reviews and
385
+ * optionally acts on, rendered as one piece: a `title` heading, an optional
386
+ * `card` of glance-facts, a block-markdown `body` (tables, bullet/numbered
387
+ * lists, blockquotes, bold headings, links), and an optional `interaction`
388
+ * (the action bar). Use this for an email/message draft, a quote, an invoice
389
+ * review, a proposal, a report — anything substantial and structured.
390
+ *
391
+ * Sugar over {@link push}: it sets `kind: 'surface'` and maps `body` to the
392
+ * message content. For conversation, status updates and quick questions, use
393
+ * {@link push} instead (a chat **bubble** — the human just replies).
394
+ *
395
+ * @example
396
+ * await vessels.surface({
397
+ * vessel: 'inv-42',
398
+ * title: 'GreenTurf Invoice #GTL-2025-042',
399
+ * card: { fields: [{ label: 'Amount Due', value: '$7,400' }] },
400
+ * body: 'Invoice is **$1,200 over** the usual rate.\n\n| Item | Amount |\n|---|---:|\n| Overage | +$1,200 |',
401
+ * interaction: vessels.approval({ prompt: 'Approve the $7,400 payment?' }),
402
+ * });
403
+ */
404
+ async surface(options) {
405
+ const { body, ...rest } = options;
406
+ return this.push({ ...rest, message: body, kind: "surface" });
407
+ }
369
408
  async pushMany(payload) {
370
409
  const { idempotencyKey, ...body } = payload;
371
410
  const check = PushManyPayloadSchema.safeParse(body);
@@ -454,6 +493,79 @@ var Vessels = class {
454
493
  }
455
494
  };
456
495
  }
496
+ /**
497
+ * Stream live tokens into a message: the human watches a monospace block fill
498
+ * in real time. Sugar over {@link editMessage} — it keeps the buffer locally
499
+ * and PATCHes a throttled, tail-trimmed window (replace-semantics, so a lost
500
+ * flush self-heals on the next one). Seal with `done()` to clear the block.
501
+ *
502
+ * @param messageId the message to stream into (create it first via `push`).
503
+ * @param opts.throttleMs minimum gap between server flushes (default 120ms).
504
+ * @param opts.maxChars longest window kept; older text scrolls off (default 8000).
505
+ */
506
+ stream(messageId, opts) {
507
+ const throttleMs = opts?.throttleMs ?? 120;
508
+ const maxChars = opts?.maxChars ?? 8e3;
509
+ let buffer = "";
510
+ let lastSent = null;
511
+ let timer = null;
512
+ let pending = Promise.resolve();
513
+ const windowed = () => buffer.length > maxChars ? buffer.slice(-maxChars) : buffer;
514
+ const flushNow = () => {
515
+ const text = windowed();
516
+ if (text === lastSent) return pending;
517
+ lastSent = text;
518
+ pending = this.editMessage(messageId, { tokenStream: text }).catch(() => {
519
+ lastSent = null;
520
+ });
521
+ return pending;
522
+ };
523
+ const cancelTimer = () => {
524
+ if (timer) {
525
+ clearTimeout(timer);
526
+ timer = null;
527
+ }
528
+ };
529
+ const schedule = () => {
530
+ if (timer) return;
531
+ timer = setTimeout(() => {
532
+ timer = null;
533
+ void flushNow();
534
+ }, throttleMs);
535
+ timer?.unref?.();
536
+ };
537
+ const settle = async () => {
538
+ cancelTimer();
539
+ await pending.catch(() => {
540
+ });
541
+ };
542
+ return {
543
+ write: (text) => {
544
+ buffer += text;
545
+ schedule();
546
+ },
547
+ set: async (text) => {
548
+ buffer = text;
549
+ cancelTimer();
550
+ await flushNow();
551
+ },
552
+ // Clear the stream AND seal any working card (agentActivity: null is a no-op
553
+ // when there's none), so the final content actually renders.
554
+ done: async (finalContent) => {
555
+ await settle();
556
+ await this.editMessage(messageId, {
557
+ tokenStream: null,
558
+ agentActivity: null,
559
+ ...finalContent != null ? { content: finalContent } : {}
560
+ });
561
+ },
562
+ // Remove only the stream; leave the working card alone.
563
+ clear: async () => {
564
+ await settle();
565
+ await this.editMessage(messageId, { tokenStream: null });
566
+ }
567
+ };
568
+ }
457
569
  /**
458
570
  * Read a vessel's message history — the human-facing record, for re-reading
459
571
  * the channel (e.g. a stateless or just-restarted worker reconciling state).
@@ -499,9 +611,6 @@ var Vessels = class {
499
611
  textInput(opts) {
500
612
  return { type: "text_input", ...opts };
501
613
  }
502
- confirmPreview(opts) {
503
- return { type: "confirm_preview", ...opts };
504
- }
505
614
  async poll(options = {}) {
506
615
  const { since, limit = 50, ack = true } = options;
507
616
  const params = new URLSearchParams();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vessels-sdk",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Let your agent reach you. Official Vessels SDK.",
5
5
  "type": "module",
6
6
  "exports": {