ggkhappy-wire 0.1.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 ADDED
@@ -0,0 +1,741 @@
1
+ # @slopus/happy-wire
2
+
3
+ Canonical wire specification package for Happy clients and services.
4
+
5
+ This package defines shared wire contracts as TypeScript types + Zod schemas. It is intentionally small and focused on protocol-level data only.
6
+
7
+ ## Quick Examples (Legacy vs New)
8
+
9
+ Both legacy and new formats are transported inside encrypted session messages.
10
+
11
+ Legacy format examples (decrypted payload):
12
+
13
+ ```json
14
+ {
15
+ "role": "user",
16
+ "content": {
17
+ "type": "text",
18
+ "text": "fix the failing test"
19
+ },
20
+ "meta": {
21
+ "sentFrom": "mobile"
22
+ }
23
+ }
24
+ ```
25
+
26
+ ```json
27
+ {
28
+ "role": "agent",
29
+ "content": {
30
+ "type": "output",
31
+ "data": {
32
+ "type": "message",
33
+ "message": "I found the issue in api/session.ts"
34
+ }
35
+ },
36
+ "meta": {
37
+ "sentFrom": "cli"
38
+ }
39
+ }
40
+ ```
41
+
42
+ New session protocol format example (decrypted payload):
43
+
44
+ ```json
45
+ {
46
+ "role": "session",
47
+ "content": {
48
+ "id": "msg_01",
49
+ "time": 1739347230000,
50
+ "role": "agent",
51
+ "turn": "turn_01",
52
+ "ev": {
53
+ "t": "text",
54
+ "text": "I found the issue in api/session.ts"
55
+ }
56
+ },
57
+ "meta": {
58
+ "sentFrom": "cli"
59
+ }
60
+ }
61
+ ```
62
+
63
+ Modern session protocol user envelope (decrypted payload):
64
+
65
+ ```json
66
+ {
67
+ "role": "session",
68
+ "content": {
69
+ "id": "msg_legacy_user_01",
70
+ "time": 1739347231000,
71
+ "role": "user",
72
+ "ev": {
73
+ "t": "text",
74
+ "text": "fix the failing test"
75
+ }
76
+ },
77
+ "meta": {
78
+ "sentFrom": "cli"
79
+ }
80
+ }
81
+ ```
82
+
83
+ Protocol invariant:
84
+ - outer `role = "session"` marks modern session-protocol payloads.
85
+ - inside `content`, envelope `role` is only `"user"` or `"agent"`.
86
+
87
+ Wire-level encrypted container (same for legacy and new):
88
+
89
+ ```json
90
+ {
91
+ "id": "msg-db-row-id",
92
+ "seq": 101,
93
+ "localId": null,
94
+ "content": {
95
+ "t": "encrypted",
96
+ "c": "BASE64_ENCRYPTED_PAYLOAD"
97
+ },
98
+ "createdAt": 1739347230000,
99
+ "updatedAt": 1739347230000
100
+ }
101
+ ```
102
+
103
+ ## Purpose
104
+
105
+ `@slopus/happy-wire` centralizes definitions for:
106
+ - encrypted message/update payloads
107
+ - session protocol envelope and event stream
108
+ - helper for creating valid session envelopes
109
+
110
+ The goal is to keep CLI/app/server/agent on the same wire contract and avoid schema drift.
111
+
112
+ ## Package Identity
113
+
114
+ - Name: `@slopus/happy-wire`
115
+ - Workspace path: `packages/happy-wire`
116
+ - Entry: `src/index.ts`
117
+ - Runtime deps: `zod`, `@paralleldrive/cuid2`
118
+
119
+ ## Public Exports
120
+
121
+ `src/index.ts` exports everything from:
122
+ - `src/messages.ts`
123
+ - `src/legacyProtocol.ts`
124
+ - `src/sessionProtocol.ts`
125
+
126
+ ### `messages.ts` exports
127
+
128
+ Schemas + inferred types:
129
+ - `SessionMessageContentSchema`
130
+ - `SessionMessage`
131
+ - `SessionMessageSchema`
132
+ - `MessageMetaSchema`
133
+ - `MessageMeta`
134
+ - `SessionProtocolMessageSchema`
135
+ - `SessionProtocolMessage`
136
+ - `MessageContentSchema`
137
+ - `MessageContent`
138
+ - `VersionedEncryptedValueSchema`
139
+ - `VersionedEncryptedValue`
140
+ - `VersionedNullableEncryptedValueSchema`
141
+ - `VersionedNullableEncryptedValue`
142
+ - `UpdateNewMessageBodySchema`
143
+ - `UpdateNewMessageBody`
144
+ - `UpdateSessionBodySchema`
145
+ - `UpdateSessionBody`
146
+ - `VersionedMachineEncryptedValueSchema`
147
+ - `VersionedMachineEncryptedValue`
148
+ - `UpdateMachineBodySchema`
149
+ - `UpdateMachineBody`
150
+ - `CoreUpdateBodySchema`
151
+ - `CoreUpdateBody`
152
+ - `CoreUpdateContainerSchema`
153
+ - `CoreUpdateContainer`
154
+
155
+ Compatibility aliases:
156
+ - `ApiMessageSchema` -> `SessionMessageSchema`
157
+ - `ApiMessage` -> `SessionMessage`
158
+ - `ApiUpdateNewMessageSchema` -> `UpdateNewMessageBodySchema`
159
+ - `ApiUpdateNewMessage` -> `UpdateNewMessageBody`
160
+ - `ApiUpdateSessionStateSchema` -> `UpdateSessionBodySchema`
161
+ - `ApiUpdateSessionState` -> `UpdateSessionBody`
162
+ - `ApiUpdateMachineStateSchema` -> `UpdateMachineBodySchema`
163
+ - `ApiUpdateMachineState` -> `UpdateMachineBody`
164
+ - `UpdateBodySchema` -> `UpdateNewMessageBodySchema`
165
+ - `UpdateBody` -> `UpdateNewMessageBody`
166
+ - `UpdateSchema` -> `CoreUpdateContainerSchema`
167
+ - `Update` -> `CoreUpdateContainer`
168
+
169
+ ### `legacyProtocol.ts` exports
170
+
171
+ Schemas + inferred types:
172
+ - `UserMessageSchema`
173
+ - `UserMessage`
174
+ - `AgentMessageSchema`
175
+ - `AgentMessage`
176
+ - `LegacyMessageContentSchema`
177
+ - `LegacyMessageContent`
178
+
179
+ ### `sessionProtocol.ts` exports
180
+
181
+ Schemas + inferred types:
182
+ - `sessionRoleSchema`
183
+ - `SessionRole`
184
+ - `sessionTextEventSchema`
185
+ - `sessionServiceMessageEventSchema`
186
+ - `sessionToolCallStartEventSchema`
187
+ - `sessionToolCallEndEventSchema`
188
+ - `sessionFileEventSchema`
189
+ - `sessionTurnStartEventSchema`
190
+ - `sessionStartEventSchema`
191
+ - `sessionTurnEndStatusSchema`
192
+ - `SessionTurnEndStatus`
193
+ - `sessionTurnEndEventSchema`
194
+ - `sessionStopEventSchema`
195
+ - `sessionEventSchema`
196
+ - `SessionEvent`
197
+ - `sessionEnvelopeSchema`
198
+ - `SessionEnvelope`
199
+ - `CreateEnvelopeOptions`
200
+ - `createEnvelope(...)`
201
+
202
+ ## Wire Type Specifications
203
+
204
+ ## Common Primitive Rules
205
+
206
+ These are schema-level requirements, not just recommendations.
207
+
208
+ - `id`, `sid`, `machineId`, `call`, `name`, `title`, `description`, `ref`: `string`
209
+ - `seq`, `createdAt`, `updatedAt`, `size`, `width`, `height`, `version`, `activeAt`: `number`
210
+ - All nullable fields are explicitly marked with `.nullable()`.
211
+ - All optional fields are explicitly marked with `.optional()`.
212
+ - `.nullish()` means `undefined | null | <type>`.
213
+
214
+ ## Message/Update Specs (`messages.ts`)
215
+
216
+ ### `SessionMessageContentSchema`
217
+
218
+ ```ts
219
+ {
220
+ t: 'encrypted';
221
+ c: string;
222
+ }
223
+ ```
224
+
225
+ Meaning:
226
+ - `t` is a strict discriminator with value `'encrypted'`.
227
+ - `c` is encrypted payload bytes encoded as a string (typically base64 in current usage).
228
+
229
+ ### `SessionMessageSchema`
230
+
231
+ ```ts
232
+ {
233
+ id: string;
234
+ seq: number;
235
+ localId?: string | null;
236
+ content: SessionMessageContent;
237
+ createdAt: number;
238
+ updatedAt: number;
239
+ }
240
+ ```
241
+
242
+ Notes:
243
+ - `localId` is `.nullish()` for compatibility with different producers.
244
+ - `createdAt` and `updatedAt` are required in this shared schema.
245
+
246
+ ### `MessageMetaSchema`
247
+
248
+ ```ts
249
+ {
250
+ sentFrom?: string;
251
+ permissionMode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'read-only' | 'safe-yolo' | 'yolo';
252
+ model?: string | null;
253
+ fallbackModel?: string | null;
254
+ customSystemPrompt?: string | null;
255
+ appendSystemPrompt?: string | null;
256
+ allowedTools?: string[] | null;
257
+ disallowedTools?: string[] | null;
258
+ displayText?: string;
259
+ }
260
+ ```
261
+
262
+ ## Legacy Decrypted Payload Specs (`legacyProtocol.ts`)
263
+
264
+ ### `UserMessageSchema` (legacy decrypted payload)
265
+
266
+ ```ts
267
+ {
268
+ role: 'user';
269
+ content: {
270
+ type: 'text';
271
+ text: string;
272
+ };
273
+ localKey?: string;
274
+ meta?: MessageMeta;
275
+ }
276
+ ```
277
+
278
+ ### `AgentMessageSchema` (legacy decrypted payload)
279
+
280
+ ```ts
281
+ {
282
+ role: 'agent';
283
+ content: {
284
+ type: string;
285
+ [key: string]: unknown;
286
+ };
287
+ meta?: MessageMeta;
288
+ }
289
+ ```
290
+
291
+ ### `LegacyMessageContentSchema`
292
+
293
+ Discriminated union on `role`:
294
+ - `'user'` -> `UserMessageSchema`
295
+ - `'agent'` -> `AgentMessageSchema`
296
+
297
+ ## Top-Level Decrypted Payload Specs (`messages.ts`)
298
+
299
+ ### `SessionProtocolMessageSchema` (modern decrypted payload wrapper)
300
+
301
+ ```ts
302
+ {
303
+ role: 'session';
304
+ content: SessionEnvelope;
305
+ meta?: MessageMeta;
306
+ }
307
+ ```
308
+
309
+ ### `MessageContentSchema`
310
+
311
+ Discriminated union on top-level `role`:
312
+ - `'user'` -> `UserMessageSchema` (legacy)
313
+ - `'agent'` -> `AgentMessageSchema` (legacy)
314
+ - `'session'` -> `SessionProtocolMessageSchema` (modern)
315
+
316
+ ## Message/Update Specs (`messages.ts`) Continued
317
+
318
+ ### `VersionedEncryptedValueSchema`
319
+
320
+ ```ts
321
+ {
322
+ version: number;
323
+ value: string;
324
+ }
325
+ ```
326
+
327
+ Used for encrypted, version-tracked blobs that cannot be null when present.
328
+
329
+ ### `VersionedNullableEncryptedValueSchema`
330
+
331
+ ```ts
332
+ {
333
+ version: number;
334
+ value: string | null;
335
+ }
336
+ ```
337
+
338
+ Used where payload presence can be intentionally reset to null while still versioning.
339
+
340
+ ### `VersionedMachineEncryptedValueSchema`
341
+
342
+ ```ts
343
+ {
344
+ version: number;
345
+ value: string;
346
+ }
347
+ ```
348
+
349
+ Machine update variant. Equivalent shape to `VersionedEncryptedValueSchema`.
350
+
351
+ ### `UpdateNewMessageBodySchema`
352
+
353
+ ```ts
354
+ {
355
+ t: 'new-message';
356
+ sid: string;
357
+ message: SessionMessage;
358
+ }
359
+ ```
360
+
361
+ ### `UpdateSessionBodySchema`
362
+
363
+ ```ts
364
+ {
365
+ t: 'update-session';
366
+ id: string;
367
+ metadata?: VersionedEncryptedValue | null;
368
+ agentState?: VersionedNullableEncryptedValue | null;
369
+ }
370
+ ```
371
+
372
+ Important distinction:
373
+ - `metadata.value` is `string` when metadata block exists.
374
+ - `agentState.value` may be `string` or `null` when block exists.
375
+
376
+ ### `UpdateMachineBodySchema`
377
+
378
+ ```ts
379
+ {
380
+ t: 'update-machine';
381
+ machineId: string;
382
+ metadata?: VersionedMachineEncryptedValue | null;
383
+ daemonState?: VersionedMachineEncryptedValue | null;
384
+ active?: boolean;
385
+ activeAt?: number;
386
+ }
387
+ ```
388
+
389
+ ### `CoreUpdateBodySchema`
390
+
391
+ Discriminated union on `t` with exactly 3 variants:
392
+ - `'new-message'`
393
+ - `'update-session'`
394
+ - `'update-machine'`
395
+
396
+ ### `CoreUpdateContainerSchema`
397
+
398
+ ```ts
399
+ {
400
+ id: string;
401
+ seq: number;
402
+ body: CoreUpdateBody;
403
+ createdAt: number;
404
+ }
405
+ ```
406
+
407
+ ## Session Protocol Specs (`sessionProtocol.ts`)
408
+
409
+ ## Role
410
+
411
+ ### `sessionRoleSchema`
412
+
413
+ ```ts
414
+ 'user' | 'agent'
415
+ ```
416
+
417
+ Role meaning:
418
+ - `'user'`: user-originated envelope.
419
+ - `'agent'`: agent-originated envelope.
420
+
421
+ ## Event Variants
422
+
423
+ `sessionEventSchema` is a discriminated union on `t` with 9 variants.
424
+
425
+ ### 1) Text event
426
+
427
+ ```ts
428
+ {
429
+ t: 'text';
430
+ text: string;
431
+ thinking?: boolean;
432
+ }
433
+ ```
434
+
435
+ ### 2) Service event
436
+
437
+ ```ts
438
+ {
439
+ t: 'service';
440
+ text: string;
441
+ }
442
+ ```
443
+
444
+ ### 3) Tool-call-start event
445
+
446
+ ```ts
447
+ {
448
+ t: 'tool-call-start';
449
+ call: string;
450
+ name: string;
451
+ title: string;
452
+ description: string;
453
+ args: Record<string, unknown>;
454
+ }
455
+ ```
456
+
457
+ ### 4) Tool-call-end event
458
+
459
+ ```ts
460
+ {
461
+ t: 'tool-call-end';
462
+ call: string;
463
+ }
464
+ ```
465
+
466
+ ### 5) File event
467
+
468
+ ```ts
469
+ {
470
+ t: 'file';
471
+ ref: string;
472
+ name: string;
473
+ size: number;
474
+ image?: {
475
+ width: number;
476
+ height: number;
477
+ thumbhash: string;
478
+ };
479
+ }
480
+ ```
481
+
482
+ ### 6) Turn-start event
483
+
484
+ ```ts
485
+ {
486
+ t: 'turn-start';
487
+ }
488
+ ```
489
+
490
+ ### 7) Start event
491
+
492
+ ```ts
493
+ {
494
+ t: 'start';
495
+ title?: string;
496
+ }
497
+ ```
498
+
499
+ ### 8) Turn-end event
500
+
501
+ ```ts
502
+ {
503
+ t: 'turn-end';
504
+ status: 'completed' | 'failed' | 'cancelled';
505
+ }
506
+ ```
507
+
508
+ ### 9) Stop event
509
+
510
+ ```ts
511
+ {
512
+ t: 'stop';
513
+ }
514
+ ```
515
+
516
+ ## Envelope
517
+
518
+ ### `sessionEnvelopeSchema`
519
+
520
+ ```ts
521
+ {
522
+ id: string;
523
+ time: number;
524
+ role: 'user' | 'agent';
525
+ turn?: string;
526
+ subagent?: string; // must pass cuid2 validation when present
527
+ ev: SessionEvent;
528
+ }
529
+ ```
530
+
531
+ Additional validation (`superRefine`):
532
+ - If `ev.t === 'service'`, then `role` MUST be `'agent'`.
533
+ - If `ev.t === 'start'` or `ev.t === 'stop'`, then `role` MUST be `'agent'`.
534
+ - If `subagent` is present, it MUST satisfy `isCuid(...)`.
535
+
536
+ ## Helper Function Contract
537
+
538
+ ### `createEnvelope(role, ev, opts?)`
539
+
540
+ Input:
541
+ - `role: SessionRole`
542
+ - `ev: SessionEvent`
543
+ - `opts?: { id?: string; time?: number; turn?: string; subagent?: string }`
544
+
545
+ Behavior:
546
+ - If `opts.id` is absent, generates id using `createId()`.
547
+ - If `opts.time` is absent, sets `time` to `Date.now()`.
548
+ - Includes `turn` only when provided.
549
+ - Includes `subagent` only when provided.
550
+
551
+ Output:
552
+ - Returns a `SessionEnvelope` parsed by `sessionEnvelopeSchema`.
553
+ - Throws on invalid combinations (for example `role = 'user'` with `ev.t = 'service'`).
554
+
555
+ ## Normative JSON Examples
556
+
557
+ ## Update container with `new-message`
558
+
559
+ ```json
560
+ {
561
+ "id": "upd-1",
562
+ "seq": 100,
563
+ "createdAt": 1739347200000,
564
+ "body": {
565
+ "t": "new-message",
566
+ "sid": "session-1",
567
+ "message": {
568
+ "id": "msg-1",
569
+ "seq": 55,
570
+ "localId": null,
571
+ "content": {
572
+ "t": "encrypted",
573
+ "c": "Zm9v"
574
+ },
575
+ "createdAt": 1739347199000,
576
+ "updatedAt": 1739347199000
577
+ }
578
+ }
579
+ }
580
+ ```
581
+
582
+ ### Decrypted `new-message` content example
583
+
584
+ `message.content.c` (ciphertext) decrypts into the payload below for a session-protocol message:
585
+
586
+ ```json
587
+ {
588
+ "role": "session",
589
+ "content": {
590
+ "id": "env_01",
591
+ "time": 1739347232000,
592
+ "role": "agent",
593
+ "turn": "turn_01",
594
+ "ev": {
595
+ "t": "text",
596
+ "text": "I found 3 TODOs."
597
+ }
598
+ },
599
+ "meta": {
600
+ "sentFrom": "cli"
601
+ }
602
+ }
603
+ ```
604
+
605
+ ## Update container with `update-session`
606
+
607
+ ```json
608
+ {
609
+ "id": "upd-2",
610
+ "seq": 101,
611
+ "createdAt": 1739347210000,
612
+ "body": {
613
+ "t": "update-session",
614
+ "id": "session-1",
615
+ "metadata": {
616
+ "version": 8,
617
+ "value": "BASE64..."
618
+ },
619
+ "agentState": {
620
+ "version": 13,
621
+ "value": null
622
+ }
623
+ }
624
+ }
625
+ ```
626
+
627
+ ## Update container with `update-machine`
628
+
629
+ ```json
630
+ {
631
+ "id": "upd-3",
632
+ "seq": 102,
633
+ "createdAt": 1739347220000,
634
+ "body": {
635
+ "t": "update-machine",
636
+ "machineId": "machine-1",
637
+ "metadata": {
638
+ "version": 2,
639
+ "value": "BASE64..."
640
+ },
641
+ "daemonState": {
642
+ "version": 3,
643
+ "value": "BASE64..."
644
+ },
645
+ "active": true,
646
+ "activeAt": 1739347220000
647
+ }
648
+ }
649
+ ```
650
+
651
+ ## Session protocol envelope
652
+
653
+ ```json
654
+ {
655
+ "id": "x8s1k2...",
656
+ "role": "agent",
657
+ "turn": "turn-42",
658
+ "ev": {
659
+ "t": "turn-start"
660
+ }
661
+ }
662
+ ```
663
+
664
+ ## Parsing/Validation Usage
665
+
666
+ ```ts
667
+ import {
668
+ CoreUpdateContainerSchema,
669
+ sessionEnvelopeSchema,
670
+ } from '@slopus/happy-wire';
671
+
672
+ const maybeUpdate = CoreUpdateContainerSchema.safeParse(input);
673
+ if (!maybeUpdate.success) {
674
+ // invalid update payload
675
+ }
676
+
677
+ const maybeEnvelope = sessionEnvelopeSchema.safeParse(envelopeInput);
678
+ if (!maybeEnvelope.success) {
679
+ // invalid envelope/event payload
680
+ }
681
+ ```
682
+
683
+ ## Build and Distribution Specification
684
+
685
+ `package.json` contract:
686
+ - `main`: `./dist/index.cjs`
687
+ - `module`: `./dist/index.mjs`
688
+ - `types`: `./dist/index.d.cts`
689
+ - `exports["."]` provides both CJS and ESM entrypoints with type paths.
690
+
691
+ Build script:
692
+ - `shx rm -rf dist && tsc --noEmit && pkgroll`
693
+
694
+ Tests:
695
+ - `vitest` against `src/*.test.ts`
696
+
697
+ Publish gate:
698
+ - `prepublishOnly` runs build + test
699
+
700
+ Published files:
701
+ - `dist`
702
+ - `package.json`
703
+ - `README.md`
704
+
705
+ ## Monorepo Build Dependency Behavior
706
+
707
+ In this repository, consumer workspaces import `@slopus/happy-wire` through package exports that point at `dist/*`.
708
+
709
+ That means on a clean checkout:
710
+ 1. Build wire first: `yarn workspace @slopus/happy-wire build`
711
+ 2. Then build/typecheck dependents.
712
+
713
+ After publishing to npm, dependents consume prebuilt artifacts from the published tarball.
714
+
715
+ ## Change Policy
716
+
717
+ When modifying wire schemas:
718
+ - Prefer additive changes to keep older consumers compatible.
719
+ - Treat discriminator values (`t`) as protocol-level API and avoid breaking renames.
720
+ - Document semantic changes in this README.
721
+ - Bump package version before downstream releases that depend on new schema behavior.
722
+
723
+ ## Development Commands
724
+
725
+ ```bash
726
+ # from repository root
727
+ yarn workspace @slopus/happy-wire build
728
+ yarn workspace @slopus/happy-wire test
729
+ ```
730
+
731
+ ## Release Commands (maintainers)
732
+
733
+ ```bash
734
+ # interactive release target selection from repo root
735
+ yarn release
736
+
737
+ # direct release invocation
738
+ yarn workspace @slopus/happy-wire release
739
+ ```
740
+
741
+ This prepares release artifacts using the same `release-it` flow as other publishable libraries in the monorepo.