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