@firststep-studio/sdk 0.1.0 → 0.2.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/dist/index.d.mts +268 -3
- package/dist/index.d.ts +268 -3
- package/dist/index.js +93 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +91 -3
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +8 -4
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +9 -5
- package/dist/server.mjs.map +1 -1
- package/dist/{types-Bm98aHcd.d.mts → types-B71xClvf.d.mts} +65 -1
- package/dist/{types-Bm98aHcd.d.ts → types-B71xClvf.d.ts} +65 -1
- package/llms.txt +464 -39
- package/package.json +1 -1
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
|
|
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.
|
|
35
|
-
4.
|
|
36
|
-
5.
|
|
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:
|
|
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:
|
|
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
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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:
|
|
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
|
|
508
|
-
const words = response.message.split(' ');
|
|
842
|
+
const sessionId = request.sessionId || `session-${Date.now()}`;
|
|
509
843
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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:
|
|
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: '
|
|
940
|
+
return { name: 'My Handler', description: 'Multi-stage support handler.' };
|
|
529
941
|
},
|
|
530
942
|
};
|
|
531
943
|
|
|
532
|
-
|
|
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: !
|
|
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
|
|