@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 +752 -0
- package/dist/index.cjs +268 -0
- package/dist/index.d.cts +3495 -0
- package/dist/index.d.mts +3495 -0
- package/dist/index.mjs +212 -0
- package/package.json +54 -0
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.
|