@firststep-studio/sdk 0.1.0 → 0.3.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/llms.txt CHANGED
@@ -11,7 +11,7 @@ cd my-handler
11
11
  npm run dev
12
12
  ```
13
13
 
14
- This scaffolds a working echo bot with streaming, forms, and tunnel support.
14
+ This scaffolds a working multi-stage handler with Gemini streaming, Dashboard persistence, and rich UI cards.
15
15
 
16
16
  ## Installation
17
17
 
@@ -31,9 +31,11 @@ User -> FirstStep Studio -> POST /message -> Your Handler -> ProtocolResponse
31
31
  The SDK provides:
32
32
  1. `ProtocolHandler` interface: what you implement
33
33
  2. `createServer()`: standalone HTTP server that handles routing, auth, and SSE streaming
34
- 3. Type definitions for all request/response shapes
35
- 4. Auth utilities for HMAC-SHA256 signature verification
36
- 5. UCP client for classifier integration
34
+ 3. `streamMetadata`: builder functions for Dashboard persistence (schema, form data, routing, agent transitions)
35
+ 4. `renderMarkers`: builder functions for rich UI cards (helplines, resources, providers, emergency alerts)
36
+ 5. Type definitions for all request/response shapes
37
+ 6. Auth utilities for HMAC-SHA256 signature verification
38
+ 7. UCP client for classifier integration
37
39
 
38
40
  ## Endpoints (handled by createServer)
39
41
 
@@ -121,7 +123,7 @@ interface ProtocolResponse {
121
123
  agentId: string; // REQUIRED: current agent identifier
122
124
  sessionStatus: SessionStatus; // REQUIRED: 'active' | 'completed' | 'error'
123
125
  form?: ProtocolForm; // OPTIONAL: render a form in the UI
124
- metadata?: Record<string, unknown>; // OPTIONAL: arbitrary metadata
126
+ metadata?: Record<string, unknown>; // OPTIONAL: metadata for Dashboard persistence
125
127
  }
126
128
  ```
127
129
 
@@ -131,6 +133,35 @@ interface ProtocolResponse {
131
133
  - `'completed'` - session is done, no more messages expected
132
134
  - `'error'` - something went wrong
133
135
 
136
+ ### Non-streaming metadata
137
+
138
+ When using `handleMessage()` (non-streaming), include Dashboard metadata in the `metadata` field of the response. The Studio proxy reads `metadata.schema`, `metadata.formData`, `metadata.currentAgent`, and `metadata.routing` and persists them.
139
+
140
+ The recommended pattern is to delegate to `handleStream()` and merge metadata:
141
+
142
+ ```typescript
143
+ async handleMessage(request, context) {
144
+ const chunks: string[] = [];
145
+ const mergedMetadata: Record<string, unknown> = {};
146
+
147
+ for await (const chunk of this.handleStream!(request, context)) {
148
+ if (chunk.type === 'text') {
149
+ chunks.push(typeof chunk.content === 'string' ? chunk.content : '');
150
+ } else if (chunk.type === 'metadata' && chunk.content) {
151
+ Object.assign(mergedMetadata, chunk.content);
152
+ }
153
+ }
154
+
155
+ return {
156
+ message: chunks.join(''),
157
+ sessionId: request.sessionId || 'new',
158
+ agentId: 'main',
159
+ sessionStatus: 'active',
160
+ metadata: Object.keys(mergedMetadata).length > 0 ? mergedMetadata : undefined,
161
+ };
162
+ },
163
+ ```
164
+
134
165
  ## Streaming
135
166
 
136
167
  If your handler declares `streaming: true` in capabilities and implements `handleStream`, the server exposes `/message/stream` as an SSE endpoint.
@@ -180,6 +211,305 @@ data: {"sessionId":"abc"}
180
211
 
181
212
  If `handleStream` is not implemented, the server falls back to `handleMessage` and sends the full response as a single SSE burst.
182
213
 
214
+ ## Stream Metadata (Dashboard Persistence)
215
+
216
+ The `streamMetadata` module provides builder functions that return ready-to-yield `ProtocolStreamChunk` objects. These chunks are intercepted by the Studio proxy and persisted to MongoDB for Dashboard features (Form Insights, Session History, Routing Logs, Agent Transitions).
217
+
218
+ ```typescript
219
+ import { streamMetadata } from '@firststep-studio/sdk';
220
+ ```
221
+
222
+ ### declareSchema
223
+
224
+ Declare your handler's agents and questions. Yield once during session init (welcome). The Studio proxy stores this so the Dashboard can display question-level analytics.
225
+
226
+ ```typescript
227
+ yield streamMetadata.declareSchema({
228
+ agents: [
229
+ { id: 'intake', title: 'Intake', order: 0 },
230
+ { id: 'support', title: 'Support', order: 1 },
231
+ { id: 'closing', title: 'Closing', order: 2 },
232
+ ],
233
+ questions: [
234
+ { id: 'name', agentId: 'intake', title: 'Name', type: 'text' },
235
+ { id: 'concern', agentId: 'support', title: 'Concern', type: 'text' },
236
+ { id: 'mood', agentId: 'support', title: 'Mood', type: 'choice' },
237
+ ],
238
+ });
239
+ // Produces: { type: 'metadata', content: { schema: { agents: [...], questions: [...] } } }
240
+ ```
241
+
242
+ #### SchemaAgent
243
+
244
+ ```typescript
245
+ interface SchemaAgent {
246
+ id: string; // Agent/stage identifier
247
+ title: string; // Display title
248
+ order?: number; // Display order (0-based)
249
+ }
250
+ ```
251
+
252
+ #### SchemaQuestion
253
+
254
+ ```typescript
255
+ interface SchemaQuestion {
256
+ id: string; // Question/field identifier
257
+ agentId: string; // Which agent owns this question
258
+ title: string; // Display title
259
+ type: 'text' | 'choice' | 'rating' | 'date' | 'location' | 'preference';
260
+ choices?: { id: string; text: string }[]; // For choice type
261
+ maxRating?: number; // For rating type
262
+ ratingStyle?: string; // For rating type
263
+ preferenceLabels?: { positive: string; negative: string }; // For preference type
264
+ }
265
+ ```
266
+
267
+ ### formDataUpdate
268
+
269
+ Send collected form field values for Dashboard persistence. Call after each turn when new fields are captured. Values are incrementally merged into the session's form data.
270
+
271
+ ```typescript
272
+ yield streamMetadata.formDataUpdate({
273
+ name: 'Alice',
274
+ name_completed_at: Date.now(),
275
+ name_turn_number: 2,
276
+ });
277
+ // Produces: { type: 'metadata', content: { formData: { name: 'Alice', ... } } }
278
+ ```
279
+
280
+ Convention: alongside each field value, include `{fieldId}_completed_at` (timestamp) and `{fieldId}_turn_number` (turn index) for analytics tracking.
281
+
282
+ ### agentTransition
283
+
284
+ Signal an agent/stage transition. The Studio proxy records this in routing logs so the Dashboard can show the conversation's agent flow.
285
+
286
+ ```typescript
287
+ yield streamMetadata.agentTransition({ id: 'support', title: 'Support' });
288
+ // Produces: { type: 'metadata', content: { currentAgent: { id: 'support', title: 'Support' } } }
289
+ ```
290
+
291
+ ### routingResult
292
+
293
+ Send a routing/classification result. The Studio proxy records this in routing logs as a classification event with category, level, and confidence score.
294
+
295
+ ```typescript
296
+ yield streamMetadata.routingResult({
297
+ decision: 'classified',
298
+ reason: 'User message contains crisis indicators',
299
+ category: 'crisis',
300
+ level: 'high',
301
+ score: 92,
302
+ });
303
+ // Produces: { type: 'metadata', content: { routing: { decision: 'classified', ... } } }
304
+ ```
305
+
306
+ #### RoutingClassificationPayload
307
+
308
+ ```typescript
309
+ interface RoutingClassificationPayload {
310
+ decision: string; // e.g., 'classified', 'routed'
311
+ reason: string; // Human-readable explanation
312
+ category: string; // Classification category
313
+ level: string; // Risk/priority level
314
+ score: number; // Confidence score (0-100)
315
+ }
316
+ ```
317
+
318
+ ## Render Markers (Rich UI Cards)
319
+
320
+ The `renderMarkers` module provides builder functions that return marker strings. These are yielded as `text` chunks. The Studio frontend parses them from the finalized message and renders rich UI components (carousels, alerts, cards).
321
+
322
+ ```typescript
323
+ import { renderMarkers } from '@firststep-studio/sdk';
324
+ ```
325
+
326
+ Render markers are always yielded as text chunks:
327
+ ```typescript
328
+ yield { type: 'text', content: renderMarkers.helplineCard({ ... }) };
329
+ ```
330
+
331
+ The marker format is: `[RENDER_TYPE]{"json":"payload"}[/RENDER_TYPE]`
332
+
333
+ ### helplineCard
334
+
335
+ Renders a carousel of helpline cards with contact buttons (call, text, chat, WhatsApp).
336
+
337
+ ```typescript
338
+ yield {
339
+ type: 'text',
340
+ content: renderMarkers.helplineCard({
341
+ helplines: [
342
+ {
343
+ name: '988 Suicide & Crisis Lifeline',
344
+ phoneNumber: '988',
345
+ categories: ['crisis', 'mental health'],
346
+ status: 'open',
347
+ statusLabel: 'Available 24/7',
348
+ website: 'https://988lifeline.org',
349
+ },
350
+ {
351
+ name: 'Crisis Text Line',
352
+ smsNumber: '741741',
353
+ categories: ['crisis', 'text support'],
354
+ status: 'open',
355
+ statusLabel: 'Text HOME',
356
+ },
357
+ ],
358
+ type: 'throughline', // 'throughline' | 'throughline_fallback' | 'stage'
359
+ }),
360
+ };
361
+ ```
362
+
363
+ #### HelplineCardItem
364
+
365
+ ```typescript
366
+ interface HelplineCardItem {
367
+ name: string;
368
+ description?: string;
369
+ categories: string[];
370
+ status: 'open' | 'closed';
371
+ statusLabel: string;
372
+ statusBadge?: string; // e.g., 'Best Match'
373
+ hoursText?: string;
374
+ supportTypes?: string;
375
+ smsNumber?: string;
376
+ phoneNumber?: string;
377
+ website?: string;
378
+ webchat?: string;
379
+ whatsapp?: string;
380
+ specialties?: string[];
381
+ highlightedTag?: string;
382
+ verified?: boolean;
383
+ }
384
+ ```
385
+
386
+ ### emergency
387
+
388
+ Renders a prominent emergency alert with a large call button.
389
+
390
+ ```typescript
391
+ yield {
392
+ type: 'text',
393
+ content: renderMarkers.emergency({
394
+ number: '911',
395
+ countryName: 'United States',
396
+ countryCode: 'US',
397
+ }),
398
+ };
399
+ ```
400
+
401
+ ### resourceCard
402
+
403
+ Renders a carousel of resource cards with descriptions, tags, and visit buttons.
404
+
405
+ ```typescript
406
+ yield {
407
+ type: 'text',
408
+ content: renderMarkers.resourceCard({
409
+ resources: [
410
+ {
411
+ name: 'Mindfulness Exercises',
412
+ url: 'https://example.com/mindfulness',
413
+ description: 'Simple breathing and grounding techniques.',
414
+ tags: ['wellness', 'self-care'],
415
+ video_url: 'https://example.com/video.mp4', // optional video thumbnail
416
+ },
417
+ ],
418
+ }),
419
+ };
420
+ ```
421
+
422
+ #### ResourceCardItem
423
+
424
+ ```typescript
425
+ interface ResourceCardItem {
426
+ name: string;
427
+ url?: string;
428
+ description?: string;
429
+ video_url?: string;
430
+ type?: string;
431
+ tags?: string[];
432
+ highlightedTag?: string;
433
+ }
434
+ ```
435
+
436
+ ### providerCard
437
+
438
+ Renders a carousel of provider directory cards with specialty/language tags and contact info.
439
+
440
+ ```typescript
441
+ yield {
442
+ type: 'text',
443
+ content: renderMarkers.providerCard({
444
+ providers: [
445
+ {
446
+ id: 'provider-1',
447
+ name: 'Dr. Smith',
448
+ type: 'therapist',
449
+ specialty: ['anxiety', 'depression'],
450
+ language: ['English', 'Spanish'],
451
+ description: 'Licensed clinical psychologist.',
452
+ contact_phone: '+1-555-0100',
453
+ contact_email: 'dr.smith@example.com',
454
+ address: '123 Main St, City, ST',
455
+ },
456
+ ],
457
+ }),
458
+ };
459
+ ```
460
+
461
+ ### safetyPlan
462
+
463
+ Renders a multi-section safety plan with expandable sections and save/export actions.
464
+
465
+ ```typescript
466
+ yield {
467
+ type: 'text',
468
+ content: renderMarkers.safetyPlan({
469
+ sections: [
470
+ {
471
+ id: 'warning-signs',
472
+ title: 'Warning Signs',
473
+ items: ['Feeling overwhelmed', 'Withdrawing from others'],
474
+ },
475
+ {
476
+ id: 'coping-strategies',
477
+ title: 'Coping Strategies',
478
+ items: ['Deep breathing', 'Going for a walk'],
479
+ },
480
+ {
481
+ id: 'contacts',
482
+ title: 'People to Contact',
483
+ items: [
484
+ { name: 'Trusted Friend', phone: '555-0101' },
485
+ { name: '988 Lifeline', phone: '988' },
486
+ ],
487
+ },
488
+ ],
489
+ actions: { savePng: true, saveTxt: true, copy: true },
490
+ }),
491
+ };
492
+ ```
493
+
494
+ ### reportCard
495
+
496
+ Renders a summary card of collected report data with a submit button.
497
+
498
+ ```typescript
499
+ yield {
500
+ type: 'text',
501
+ content: renderMarkers.reportCard({
502
+ topic: 'Workplace harassment',
503
+ location: 'Office Building A',
504
+ description: 'Detailed description of the incident...',
505
+ perpetrator_known: true,
506
+ contact_mode: 'email',
507
+ contact_value: 'reporter@example.com',
508
+ submitEndpoint: 'https://api.example.com/reports',
509
+ }),
510
+ };
511
+ ```
512
+
183
513
  ## Forms
184
514
 
185
515
  Forms let your handler render structured input fields in the FirstStep Studio UI.
@@ -465,10 +795,13 @@ interface ClassificationResult {
465
795
  }
466
796
  ```
467
797
 
468
- ## Complete Example: Echo Handler
798
+ ## Complete Example: Multi-Stage Handler
799
+
800
+ A handler that uses every SDK feature: streaming, Dashboard metadata, rich UI cards, and stage-based conversation flow.
469
801
 
470
802
  ```typescript
471
803
  import { createServer } from '@firststep-studio/sdk/server';
804
+ import { streamMetadata, renderMarkers } from '@firststep-studio/sdk';
472
805
  import type {
473
806
  ProtocolHandler,
474
807
  ProtocolRequest,
@@ -479,63 +812,140 @@ import type {
479
812
  HandlerInfo,
480
813
  } from '@firststep-studio/sdk';
481
814
 
815
+ // In-memory session state (use a database in production)
816
+ const sessions = new Map<string, { stage: string; name?: string; concern?: string }>();
817
+
482
818
  const handler: ProtocolHandler = {
819
+ // Non-streaming: delegate to handleStream and merge metadata
483
820
  async handleMessage(request, context): Promise<ProtocolResponse> {
484
- const sessionId = request.sessionId || `session-${Date.now()}`;
485
- const userMessage = request.message?.trim();
486
-
487
- // Session init
488
- if (!userMessage) {
489
- return {
490
- message: 'Hello! Send me a message.',
491
- sessionId,
492
- agentId: 'main',
493
- sessionStatus: 'active',
494
- };
821
+ const chunks: string[] = [];
822
+ const mergedMetadata: Record<string, unknown> = {};
823
+
824
+ for await (const chunk of this.handleStream!(request, context)) {
825
+ if (chunk.type === 'text') {
826
+ chunks.push(typeof chunk.content === 'string' ? chunk.content : '');
827
+ } else if (chunk.type === 'metadata' && chunk.content) {
828
+ Object.assign(mergedMetadata, chunk.content);
829
+ }
495
830
  }
496
831
 
497
- // Echo back
498
832
  return {
499
- message: `Echo: ${userMessage}`,
500
- sessionId,
833
+ message: chunks.join(''),
834
+ sessionId: request.sessionId || 'new',
501
835
  agentId: 'main',
502
836
  sessionStatus: 'active',
837
+ metadata: Object.keys(mergedMetadata).length > 0 ? mergedMetadata : undefined,
503
838
  };
504
839
  },
505
840
 
506
841
  async *handleStream(request, context): AsyncGenerator<ProtocolStreamChunk> {
507
- const response = await handler.handleMessage(request, context);
508
- const words = response.message.split(' ');
842
+ const sessionId = request.sessionId || `session-${Date.now()}`;
509
843
 
510
- for (let i = 0; i < words.length; i++) {
511
- yield { type: 'text', content: (i === 0 ? '' : ' ') + words[i] };
512
- await new Promise(r => setTimeout(r, 30));
844
+ // ── WELCOME (no message = session init) ──
845
+ if (!request.message) {
846
+ // 1. Declare schema for Dashboard Form Insights
847
+ yield streamMetadata.declareSchema({
848
+ agents: [
849
+ { id: 'intake', title: 'Intake', order: 0 },
850
+ { id: 'support', title: 'Support', order: 1 },
851
+ ],
852
+ questions: [
853
+ { id: 'name', agentId: 'intake', title: 'Name', type: 'text' },
854
+ { id: 'concern', agentId: 'support', title: 'Concern', type: 'text' },
855
+ ],
856
+ });
857
+
858
+ yield { type: 'text', content: 'Welcome! What is your name?' };
859
+ yield { type: 'status', content: 'active' };
860
+ return;
861
+ }
862
+
863
+ // ── NORMAL MESSAGE ──
864
+ const session = sessions.get(sessionId) || { stage: 'intake' };
865
+
866
+ if (session.stage === 'intake') {
867
+ session.name = request.message;
868
+ session.stage = 'support';
869
+ sessions.set(sessionId, session);
870
+
871
+ // 2. Persist collected field to Dashboard
872
+ yield streamMetadata.formDataUpdate({
873
+ name: session.name,
874
+ name_completed_at: Date.now(),
875
+ name_turn_number: 1,
876
+ });
877
+
878
+ // 3. Signal agent transition
879
+ yield streamMetadata.agentTransition({ id: 'support', title: 'Support' });
880
+
881
+ yield { type: 'text', content: `Hi ${session.name}! What can I help you with?` };
882
+ } else {
883
+ session.concern = request.message;
884
+ sessions.set(sessionId, session);
885
+
886
+ // 4. Persist concern field
887
+ yield streamMetadata.formDataUpdate({
888
+ concern: session.concern,
889
+ concern_completed_at: Date.now(),
890
+ concern_turn_number: 2,
891
+ });
892
+
893
+ // 5. Send routing classification
894
+ const isUrgent = /crisis|emergency|hurt/i.test(request.message);
895
+ yield streamMetadata.routingResult({
896
+ decision: 'classified',
897
+ reason: isUrgent ? 'Crisis indicators detected' : 'Standard request',
898
+ category: isUrgent ? 'crisis' : 'general',
899
+ level: isUrgent ? 'high' : 'low',
900
+ score: isUrgent ? 90 : 20,
901
+ });
902
+
903
+ yield { type: 'text', content: 'Thank you for sharing. Here are some resources:' };
904
+
905
+ // 6. Emit rich UI cards
906
+ yield {
907
+ type: 'text',
908
+ content: renderMarkers.helplineCard({
909
+ helplines: [{
910
+ name: '988 Suicide & Crisis Lifeline',
911
+ phoneNumber: '988',
912
+ categories: ['crisis'],
913
+ status: 'open',
914
+ statusLabel: 'Available 24/7',
915
+ }],
916
+ }),
917
+ };
918
+
919
+ yield {
920
+ type: 'text',
921
+ content: renderMarkers.resourceCard({
922
+ resources: [{
923
+ name: 'Coping Strategies Guide',
924
+ url: 'https://example.com/coping',
925
+ description: 'Evidence-based techniques for managing stress.',
926
+ tags: ['wellness', 'self-care'],
927
+ }],
928
+ }),
929
+ };
513
930
  }
514
931
 
515
- yield { type: 'status', content: response.sessionStatus };
932
+ yield { type: 'status', content: 'active' };
516
933
  },
517
934
 
518
935
  getCapabilities(): ProtocolCapabilities {
519
- return {
520
- streaming: true,
521
- formQuestions: false,
522
- knowledgeActions: false,
523
- integrations: false,
524
- };
936
+ return { streaming: true, formQuestions: false, knowledgeActions: false, integrations: false };
525
937
  },
526
938
 
527
939
  getHandlerInfo(): HandlerInfo {
528
- return { name: 'Echo Handler' };
940
+ return { name: 'My Handler', description: 'Multi-stage support handler.' };
529
941
  },
530
942
  };
531
943
 
532
- const token = process.env.FIRSTSTEP_TOKEN || '';
533
- const port = parseInt(process.env.PORT || '4001', 10);
534
-
944
+ // Start server
535
945
  const server = createServer(handler, {
536
- token,
537
- port,
538
- skipSignatureVerification: !token,
946
+ token: process.env.FIRSTSTEP_TOKEN || '',
947
+ port: parseInt(process.env.PORT || '4001', 10),
948
+ skipSignatureVerification: !process.env.FIRSTSTEP_TOKEN,
539
949
  });
540
950
 
541
951
  server.start();
@@ -552,6 +962,9 @@ server.start();
552
962
  // Server (separate entry point)
553
963
  import { createServer } from '@firststep-studio/sdk/server';
554
964
 
965
+ // Builder modules (main entry point)
966
+ import { streamMetadata, renderMarkers, MARKER_TYPES } from '@firststep-studio/sdk';
967
+
555
968
  // Types (main entry point)
556
969
  import type {
557
970
  // Core
@@ -561,6 +974,18 @@ import type {
561
974
  // Streaming
562
975
  ProtocolStreamChunk, ProtocolError,
563
976
 
977
+ // Stream Metadata Payloads
978
+ SchemaDeclarationPayload, SchemaAgent, SchemaQuestion,
979
+ AgentTransitionPayload, RoutingClassificationPayload,
980
+
981
+ // Render Marker Payloads
982
+ HelplineCardPayload, HelplineCardItem,
983
+ EmergencyPayload,
984
+ ResourceCardPayload, ResourceCardItem,
985
+ ProviderCardPayload, ProviderCardItem,
986
+ SafetyPlanPayload, SafetyPlanSection,
987
+ ReportCardPayload, MarkerType,
988
+
564
989
  // Forms
565
990
  ProtocolForm, ProtocolFormField, ProtocolFormOption, ProtocolFieldValidation,
566
991
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firststep-studio/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "SDK for building protocol handlers that integrate with FirstStep Studio",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",