@parlor/chat 0.0.0-stage → 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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -2
  3. package/dist/events.d.ts +4 -0
  4. package/dist/events.d.ts.map +1 -0
  5. package/dist/events.js +12 -0
  6. package/dist/events.js.map +1 -0
  7. package/dist/id-schemas.d.ts +5 -0
  8. package/dist/id-schemas.d.ts.map +1 -0
  9. package/dist/id-schemas.js +11 -0
  10. package/dist/id-schemas.js.map +1 -0
  11. package/dist/index.d.ts +9 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +7 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/items.d.ts +295 -0
  16. package/dist/items.d.ts.map +1 -0
  17. package/dist/items.js +113 -0
  18. package/dist/items.js.map +1 -0
  19. package/dist/limits.d.ts +4 -0
  20. package/dist/limits.d.ts.map +1 -0
  21. package/dist/limits.js +4 -0
  22. package/dist/limits.js.map +1 -0
  23. package/dist/model.d.ts +143 -0
  24. package/dist/model.d.ts.map +1 -0
  25. package/dist/model.js +2 -0
  26. package/dist/model.js.map +1 -0
  27. package/dist/open-turns.d.ts +19 -0
  28. package/dist/open-turns.d.ts.map +1 -0
  29. package/dist/open-turns.js +31 -0
  30. package/dist/open-turns.js.map +1 -0
  31. package/dist/protocol.d.ts +69 -0
  32. package/dist/protocol.d.ts.map +1 -0
  33. package/dist/protocol.js +80 -0
  34. package/dist/protocol.js.map +1 -0
  35. package/dist/server/actions.d.ts +561 -0
  36. package/dist/server/actions.d.ts.map +1 -0
  37. package/dist/server/actions.js +422 -0
  38. package/dist/server/actions.js.map +1 -0
  39. package/dist/server/context.d.ts +24 -0
  40. package/dist/server/context.d.ts.map +1 -0
  41. package/dist/server/context.js +77 -0
  42. package/dist/server/context.js.map +1 -0
  43. package/dist/server/index.d.ts +50 -0
  44. package/dist/server/index.d.ts.map +1 -0
  45. package/dist/server/index.js +145 -0
  46. package/dist/server/index.js.map +1 -0
  47. package/dist/server/live.d.ts +24 -0
  48. package/dist/server/live.d.ts.map +1 -0
  49. package/dist/server/live.js +130 -0
  50. package/dist/server/live.js.map +1 -0
  51. package/dist/server/migrations.d.ts +3 -0
  52. package/dist/server/migrations.d.ts.map +1 -0
  53. package/dist/server/migrations.js +96 -0
  54. package/dist/server/migrations.js.map +1 -0
  55. package/dist/server/party.d.ts +24 -0
  56. package/dist/server/party.d.ts.map +1 -0
  57. package/dist/server/party.js +38 -0
  58. package/dist/server/party.js.map +1 -0
  59. package/dist/server/routes.d.ts +17 -0
  60. package/dist/server/routes.d.ts.map +1 -0
  61. package/dist/server/routes.js +105 -0
  62. package/dist/server/routes.js.map +1 -0
  63. package/dist/server/rows.d.ts +49 -0
  64. package/dist/server/rows.d.ts.map +1 -0
  65. package/dist/server/rows.js +136 -0
  66. package/dist/server/rows.js.map +1 -0
  67. package/dist/server/store.d.ts +130 -0
  68. package/dist/server/store.d.ts.map +1 -0
  69. package/dist/server/store.js +539 -0
  70. package/dist/server/store.js.map +1 -0
  71. package/dist/tags.d.ts +7 -0
  72. package/dist/tags.d.ts.map +1 -0
  73. package/dist/tags.js +7 -0
  74. package/dist/tags.js.map +1 -0
  75. package/dist/testing/index.d.ts +46 -0
  76. package/dist/testing/index.d.ts.map +1 -0
  77. package/dist/testing/index.js +96 -0
  78. package/dist/testing/index.js.map +1 -0
  79. package/dist/view.d.ts +87 -0
  80. package/dist/view.d.ts.map +1 -0
  81. package/dist/view.js +236 -0
  82. package/dist/view.js.map +1 -0
  83. package/package.json +47 -3
@@ -0,0 +1,143 @@
1
+ import type { HostId, ItemId, ThreadId, TurnId, UserId } from "@parlor/core";
2
+ import type { Delegation, DeviceKeys } from "@parlor/server";
3
+ import type { ChatItemBody, LoggedItem, UserItemKind } from "./items.js";
4
+ export interface AgentRef {
5
+ readonly hostId: HostId;
6
+ /** A profile id on that host (`claude`, `codex`), not an entity. */
7
+ readonly agentId: string;
8
+ }
9
+ /**
10
+ * Whose chat a row is. Every thread, turn and host row carries both and every chat query
11
+ * filters by both, so members of one organization scope never see each other's chats.
12
+ */
13
+ export interface ChatOwner {
14
+ readonly scope: string;
15
+ readonly userId: UserId;
16
+ }
17
+ export interface HostSummary {
18
+ readonly id: HostId;
19
+ readonly name: string;
20
+ readonly platform: string;
21
+ readonly online: boolean;
22
+ readonly agents: readonly AgentInfo[];
23
+ /** The devices this computer runs prompts from; absent when it is too old to say. */
24
+ readonly devices?: readonly string[] | undefined;
25
+ /**
26
+ * When each of those devices must sign its next prompt with Face ID or Touch ID again
27
+ * (epoch ms); a device it never asks is missing.
28
+ */
29
+ readonly presenceDue?: Readonly<Record<string, number>> | undefined;
30
+ }
31
+ export interface AgentInfo {
32
+ readonly id: string;
33
+ readonly name: string;
34
+ readonly version?: string | undefined;
35
+ readonly ready: boolean;
36
+ /** Why a not-ready agent cannot run yet ("run `claude` to log in"). */
37
+ readonly problem?: string | undefined;
38
+ }
39
+ export interface ThreadSummary {
40
+ readonly id: ThreadId;
41
+ readonly title: string;
42
+ readonly agent: AgentRef | undefined;
43
+ readonly updatedAt: number;
44
+ readonly lastSeq: number;
45
+ }
46
+ export type TurnState = "queued" | "running" | "done" | "failed" | "cancelled";
47
+ export type TurnFailure = "agent_exited" | "agent_unavailable" | "device_rejected" | "signature_invalid" | "prompt_expired" | "host_lost";
48
+ export type TurnOutcome = {
49
+ readonly state: "done";
50
+ readonly stopReason: string;
51
+ } | {
52
+ readonly state: "cancelled";
53
+ } | {
54
+ readonly state: "failed";
55
+ readonly reason: TurnFailure;
56
+ };
57
+ /** Still being produced: only in memory, on the instance the host posts to. */
58
+ export type OpenItem = {
59
+ readonly id: ItemId;
60
+ readonly kind: "reply" | "reasoning";
61
+ readonly text: string;
62
+ } | {
63
+ readonly id: ItemId;
64
+ readonly kind: "tool";
65
+ readonly title: string;
66
+ readonly status: "pending" | "running";
67
+ } | {
68
+ readonly id: ItemId;
69
+ readonly kind: "plan";
70
+ readonly entries: ChatItemBody<"plan">["entries"];
71
+ };
72
+ export interface OpenTurn {
73
+ readonly turnId: TurnId;
74
+ readonly threadId: ThreadId;
75
+ readonly items: readonly OpenItem[];
76
+ }
77
+ export interface TurnStatus {
78
+ readonly turnId: TurnId;
79
+ readonly state: TurnState;
80
+ readonly agent: AgentRef;
81
+ readonly failure?: TurnFailure | undefined;
82
+ }
83
+ export interface ThreadPage {
84
+ readonly thread: ThreadSummary | undefined;
85
+ readonly items: readonly LoggedItem[];
86
+ readonly open: readonly OpenTurn[];
87
+ readonly turns: readonly TurnStatus[];
88
+ readonly hasMore: boolean;
89
+ }
90
+ export interface ChatSnapshot {
91
+ readonly threads: readonly ThreadSummary[];
92
+ /** `threads` was cut at its limit; `threadsBefore` reads on from its last one. */
93
+ readonly moreThreads?: boolean | undefined;
94
+ readonly hosts: readonly HostSummary[];
95
+ readonly page: ThreadPage;
96
+ }
97
+ /** What `history` returns: earlier items of a thread, oldest first, with their turns. */
98
+ export interface ThreadHistory {
99
+ readonly items: readonly LoggedItem[];
100
+ readonly turns: readonly TurnStatus[];
101
+ readonly hasMore: boolean;
102
+ }
103
+ /** What `threadsBefore` returns: the next older threads, newest first. */
104
+ export interface ThreadList {
105
+ readonly threads: readonly ThreadSummary[];
106
+ readonly hasMore: boolean;
107
+ }
108
+ /** The device's keys as the account has them; the host compares them with its own pins. */
109
+ export type ClaimedDevice = DeviceKeys;
110
+ export interface ClaimedTurn {
111
+ /** The thread id; every chat write returns it as `id`, which becomes its sync tag. */
112
+ readonly id: ThreadId;
113
+ readonly turnId: TurnId;
114
+ readonly agent: AgentRef;
115
+ /** The client the prompt came from; the turn token binds `ui_*` to it. */
116
+ readonly clientId: string;
117
+ readonly prompt: LoggedItem<"prompt">;
118
+ readonly device: ClaimedDevice | undefined;
119
+ /** The `<parlor-context>` block for a fresh agent session. */
120
+ readonly context: string;
121
+ readonly token: Delegation;
122
+ readonly leaseUntil: number;
123
+ }
124
+ export interface TurnSignals {
125
+ readonly turnId: TurnId;
126
+ readonly items: readonly LoggedItem<Exclude<UserItemKind, "prompt">>[];
127
+ /** The keys of each device that answered, as the account lists them; revoked ones are left out. */
128
+ readonly devices?: readonly DeviceKeys[];
129
+ }
130
+ /** What a device signs for a prompt; the host rebuilds the same statement from the item. */
131
+ export interface PromptToSign {
132
+ readonly threadId: ThreadId;
133
+ readonly turnId: TurnId;
134
+ readonly agent: AgentRef;
135
+ readonly text: string;
136
+ }
137
+ export interface AnswerToSign {
138
+ readonly threadId: ThreadId;
139
+ readonly askId: ItemId;
140
+ readonly choice: string;
141
+ readonly values?: ChatItemBody<"answer">["values"];
142
+ }
143
+ //# sourceMappingURL=model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC7E,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE7D,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEzE,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC;IACtC,qFAAqF;IACrF,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACjD;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;CACrE;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,uEAAuE;IACvE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACvC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,WAAW,CAAC;AAE/E,MAAM,MAAM,WAAW,GACnB,cAAc,GACd,mBAAmB,GACnB,iBAAiB,GACjB,mBAAmB,GACnB,gBAAgB,GAChB,WAAW,CAAC;AAEhB,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACvD;IAAE,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAA;CAAE,GAC/B;IAAE,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;CAAE,CAAC;AAE/D,+EAA+E;AAC/E,MAAM,MAAM,QAAQ,GAChB;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,WAAW,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACpF;IACE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,CAAC;CACxC,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC;CACnD,CAAC;AAEN,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CAC5C;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,CAAC;IAC3C,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,SAAS,QAAQ,EAAE,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,CAAC;IAC3C,kFAAkF;IAClF,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3C,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;IACvC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;CAC3B;AAED,yFAAyF;AACzF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,0EAA0E;AAC1E,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,2FAA2F;AAC3F,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC;AAEvC,MAAM,WAAW,WAAW;IAC1B,sFAAsF;IACtF,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,CAAC;IAC3C,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,CAAC,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC;IACvE,mGAAmG;IACnG,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;CAC1C;AAED,4FAA4F;AAC5F,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC;CACpD"}
package/dist/model.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"","sourcesContent":["import type { HostId, ItemId, ThreadId, TurnId, UserId } from \"@parlor/core\";\nimport type { Delegation, DeviceKeys } from \"@parlor/server\";\n\nimport type { ChatItemBody, LoggedItem, UserItemKind } from \"./items.js\";\n\nexport interface AgentRef {\n readonly hostId: HostId;\n /** A profile id on that host (`claude`, `codex`), not an entity. */\n readonly agentId: string;\n}\n\n/**\n * Whose chat a row is. Every thread, turn and host row carries both and every chat query\n * filters by both, so members of one organization scope never see each other's chats.\n */\nexport interface ChatOwner {\n readonly scope: string;\n readonly userId: UserId;\n}\n\nexport interface HostSummary {\n readonly id: HostId;\n readonly name: string;\n readonly platform: string;\n readonly online: boolean;\n readonly agents: readonly AgentInfo[];\n /** The devices this computer runs prompts from; absent when it is too old to say. */\n readonly devices?: readonly string[] | undefined;\n /**\n * When each of those devices must sign its next prompt with Face ID or Touch ID again\n * (epoch ms); a device it never asks is missing.\n */\n readonly presenceDue?: Readonly<Record<string, number>> | undefined;\n}\n\nexport interface AgentInfo {\n readonly id: string;\n readonly name: string;\n readonly version?: string | undefined;\n readonly ready: boolean;\n /** Why a not-ready agent cannot run yet (\"run `claude` to log in\"). */\n readonly problem?: string | undefined;\n}\n\nexport interface ThreadSummary {\n readonly id: ThreadId;\n readonly title: string;\n readonly agent: AgentRef | undefined;\n readonly updatedAt: number;\n readonly lastSeq: number;\n}\n\nexport type TurnState = \"queued\" | \"running\" | \"done\" | \"failed\" | \"cancelled\";\n\nexport type TurnFailure =\n | \"agent_exited\"\n | \"agent_unavailable\"\n | \"device_rejected\"\n | \"signature_invalid\"\n | \"prompt_expired\"\n | \"host_lost\";\n\nexport type TurnOutcome =\n | { readonly state: \"done\"; readonly stopReason: string }\n | { readonly state: \"cancelled\" }\n | { readonly state: \"failed\"; readonly reason: TurnFailure };\n\n/** Still being produced: only in memory, on the instance the host posts to. */\nexport type OpenItem =\n | { readonly id: ItemId; readonly kind: \"reply\" | \"reasoning\"; readonly text: string }\n | {\n readonly id: ItemId;\n readonly kind: \"tool\";\n readonly title: string;\n readonly status: \"pending\" | \"running\";\n }\n | {\n readonly id: ItemId;\n readonly kind: \"plan\";\n readonly entries: ChatItemBody<\"plan\">[\"entries\"];\n };\n\nexport interface OpenTurn {\n readonly turnId: TurnId;\n readonly threadId: ThreadId;\n readonly items: readonly OpenItem[];\n}\n\nexport interface TurnStatus {\n readonly turnId: TurnId;\n readonly state: TurnState;\n readonly agent: AgentRef;\n readonly failure?: TurnFailure | undefined;\n}\n\nexport interface ThreadPage {\n readonly thread: ThreadSummary | undefined;\n readonly items: readonly LoggedItem[];\n readonly open: readonly OpenTurn[];\n readonly turns: readonly TurnStatus[];\n readonly hasMore: boolean;\n}\n\nexport interface ChatSnapshot {\n readonly threads: readonly ThreadSummary[];\n /** `threads` was cut at its limit; `threadsBefore` reads on from its last one. */\n readonly moreThreads?: boolean | undefined;\n readonly hosts: readonly HostSummary[];\n readonly page: ThreadPage;\n}\n\n/** What `history` returns: earlier items of a thread, oldest first, with their turns. */\nexport interface ThreadHistory {\n readonly items: readonly LoggedItem[];\n readonly turns: readonly TurnStatus[];\n readonly hasMore: boolean;\n}\n\n/** What `threadsBefore` returns: the next older threads, newest first. */\nexport interface ThreadList {\n readonly threads: readonly ThreadSummary[];\n readonly hasMore: boolean;\n}\n\n/** The device's keys as the account has them; the host compares them with its own pins. */\nexport type ClaimedDevice = DeviceKeys;\n\nexport interface ClaimedTurn {\n /** The thread id; every chat write returns it as `id`, which becomes its sync tag. */\n readonly id: ThreadId;\n readonly turnId: TurnId;\n readonly agent: AgentRef;\n /** The client the prompt came from; the turn token binds `ui_*` to it. */\n readonly clientId: string;\n readonly prompt: LoggedItem<\"prompt\">;\n readonly device: ClaimedDevice | undefined;\n /** The `<parlor-context>` block for a fresh agent session. */\n readonly context: string;\n readonly token: Delegation;\n readonly leaseUntil: number;\n}\n\nexport interface TurnSignals {\n readonly turnId: TurnId;\n readonly items: readonly LoggedItem<Exclude<UserItemKind, \"prompt\">>[];\n /** The keys of each device that answered, as the account lists them; revoked ones are left out. */\n readonly devices?: readonly DeviceKeys[];\n}\n\n/** What a device signs for a prompt; the host rebuilds the same statement from the item. */\nexport interface PromptToSign {\n readonly threadId: ThreadId;\n readonly turnId: TurnId;\n readonly agent: AgentRef;\n readonly text: string;\n}\n\nexport interface AnswerToSign {\n readonly threadId: ThreadId;\n readonly askId: ItemId;\n readonly choice: string;\n readonly values?: ChatItemBody<\"answer\">[\"values\"];\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import type { OpenItem } from "./model.js";
2
+ import type { ChatFrame } from "./protocol.js";
3
+ export interface AppliedFrame {
4
+ readonly items: readonly OpenItem[];
5
+ /** What actually changed: a delta overlapping the text already held is cut to its new part. */
6
+ readonly frame: Exclude<ChatFrame, {
7
+ type: "settled";
8
+ }>;
9
+ }
10
+ /**
11
+ * Applies a `delta` or `item` frame to one turn's open items. A delta whose text is
12
+ * already held is skipped and one that starts past the end of the text is a gap; both
13
+ * answer `undefined`, and after a gap every later delta is a gap too, so the item waits
14
+ * for its durable twin instead of drawing a hole.
15
+ */
16
+ export declare function applyFrame(items: readonly OpenItem[], frame: Exclude<ChatFrame, {
17
+ type: "settled";
18
+ }>): AppliedFrame | undefined;
19
+ //# sourceMappingURL=open-turns.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-turns.d.ts","sourceRoot":"","sources":["../src/open-turns.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE/C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IACpC,+FAA+F;IAC/F,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,EAAE;QAAE,IAAI,EAAE,SAAS,CAAA;KAAE,CAAC,CAAC;CACzD;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,SAAS,QAAQ,EAAE,EAC1B,KAAK,EAAE,OAAO,CAAC,SAAS,EAAE;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC,GAC7C,YAAY,GAAG,SAAS,CAqB1B"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Applies a `delta` or `item` frame to one turn's open items. A delta whose text is
3
+ * already held is skipped and one that starts past the end of the text is a gap; both
4
+ * answer `undefined`, and after a gap every later delta is a gap too, so the item waits
5
+ * for its durable twin instead of drawing a hole.
6
+ */
7
+ export function applyFrame(items, frame) {
8
+ if (frame.type === "item") {
9
+ const index = items.findIndex((item) => item.id === frame.item.id);
10
+ const next = index < 0
11
+ ? [...items, frame.item]
12
+ : items.map((item, at) => (at === index ? frame.item : item));
13
+ return { items: next, frame };
14
+ }
15
+ const index = items.findIndex((item) => item.id === frame.itemId);
16
+ const held = index < 0 ? undefined : items[index];
17
+ if (held && held.kind !== "reply" && held.kind !== "reasoning")
18
+ return undefined;
19
+ const have = held ? held.text : "";
20
+ if (frame.offset > have.length)
21
+ return undefined;
22
+ const fresh = frame.text.slice(have.length - frame.offset);
23
+ if (fresh.length === 0)
24
+ return undefined;
25
+ const grown = { id: frame.itemId, kind: frame.kind, text: have + fresh };
26
+ const next = held
27
+ ? items.map((item, at) => (at === index ? grown : item))
28
+ : [...items, grown];
29
+ return { items: next, frame: { ...frame, offset: have.length, text: fresh } };
30
+ }
31
+ //# sourceMappingURL=open-turns.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-turns.js","sourceRoot":"","sources":["../src/open-turns.ts"],"names":[],"mappings":"AASA;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CACxB,KAA0B,EAC1B,KAA8C;IAE9C,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC1B,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACnE,MAAM,IAAI,GACR,KAAK,GAAG,CAAC;YACP,CAAC,CAAC,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC;YACxB,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QAClE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAChC,CAAC;IACD,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,KAAK,CAAC,MAAM,CAAC,CAAC;IAClE,MAAM,IAAI,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAClD,IAAI,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW;QAAE,OAAO,SAAS,CAAC;IACjF,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IACnC,IAAI,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IACjD,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IAC3D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACzC,MAAM,KAAK,GAAa,EAAE,EAAE,EAAE,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,GAAG,KAAK,EAAE,CAAC;IACnF,MAAM,IAAI,GAAG,IAAI;QACf,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACxD,CAAC,CAAC,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,CAAC;IACtB,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;AAChF,CAAC","sourcesContent":["import type { OpenItem } from \"./model.js\";\nimport type { ChatFrame } from \"./protocol.js\";\n\nexport interface AppliedFrame {\n readonly items: readonly OpenItem[];\n /** What actually changed: a delta overlapping the text already held is cut to its new part. */\n readonly frame: Exclude<ChatFrame, { type: \"settled\" }>;\n}\n\n/**\n * Applies a `delta` or `item` frame to one turn's open items. A delta whose text is\n * already held is skipped and one that starts past the end of the text is a gap; both\n * answer `undefined`, and after a gap every later delta is a gap too, so the item waits\n * for its durable twin instead of drawing a hole.\n */\nexport function applyFrame(\n items: readonly OpenItem[],\n frame: Exclude<ChatFrame, { type: \"settled\" }>,\n): AppliedFrame | undefined {\n if (frame.type === \"item\") {\n const index = items.findIndex((item) => item.id === frame.item.id);\n const next =\n index < 0\n ? [...items, frame.item]\n : items.map((item, at) => (at === index ? frame.item : item));\n return { items: next, frame };\n }\n const index = items.findIndex((item) => item.id === frame.itemId);\n const held = index < 0 ? undefined : items[index];\n if (held && held.kind !== \"reply\" && held.kind !== \"reasoning\") return undefined;\n const have = held ? held.text : \"\";\n if (frame.offset > have.length) return undefined;\n const fresh = frame.text.slice(have.length - frame.offset);\n if (fresh.length === 0) return undefined;\n const grown: OpenItem = { id: frame.itemId, kind: frame.kind, text: have + fresh };\n const next = held\n ? items.map((item, at) => (at === index ? grown : item))\n : [...items, grown];\n return { items: next, frame: { ...frame, offset: have.length, text: fresh } };\n}\n"]}
@@ -0,0 +1,69 @@
1
+ import { type ItemId, type ThreadId, type TurnId } from "@parlor/core";
2
+ import type { AgentInfo, OpenItem } from "./model.js";
3
+ export declare const CHAT_CHANNEL = "chat";
4
+ /** The plugin name, so its routes live under `/_parlor/chat/`. */
5
+ export declare const CHAT_PLUGIN = "chat";
6
+ export type StreamingKind = "reply" | "reasoning";
7
+ /**
8
+ * Server to browser on the `chat` channel; never persisted. `offset` makes a duplicate
9
+ * delta skippable and a gap detectable, after which the client waits for the durable
10
+ * item. `settled` drops a turn's open items: durable now, or gone.
11
+ */
12
+ export type ChatFrame = {
13
+ readonly type: "delta";
14
+ readonly threadId: ThreadId;
15
+ readonly turnId: TurnId;
16
+ readonly itemId: ItemId;
17
+ readonly kind: StreamingKind;
18
+ readonly offset: number;
19
+ readonly text: string;
20
+ } | {
21
+ readonly type: "item";
22
+ readonly threadId: ThreadId;
23
+ readonly turnId: TurnId;
24
+ readonly item: Extract<OpenItem, {
25
+ kind: "tool" | "plan";
26
+ }>;
27
+ } | {
28
+ readonly type: "settled";
29
+ readonly threadId: ThreadId;
30
+ readonly turnId: TurnId;
31
+ };
32
+ /** Host to server: the same frames minus what the batch's turn implies. Only the server settles. */
33
+ export type HostFrame = ChatFrame extends infer F ? F extends {
34
+ readonly type: "delta" | "item";
35
+ } ? Omit<F, "threadId" | "turnId"> : never : never;
36
+ /** `POST /_parlor/chat/stream` */
37
+ export interface HostStreamBatch {
38
+ readonly turnId: TurnId;
39
+ readonly frames: readonly HostFrame[];
40
+ }
41
+ /**
42
+ * The versioned contract between an app's backend and Parlor Host: host actions, item
43
+ * kinds, frames and cursor rules. Bumped on any change an older peer could misread.
44
+ */
45
+ export declare const HOST_PROTOCOL = 2;
46
+ export interface ProtocolRange {
47
+ readonly min: number;
48
+ readonly max: number;
49
+ }
50
+ export interface HostHello {
51
+ readonly protocols: ProtocolRange;
52
+ readonly hostVersion: string;
53
+ readonly name: string;
54
+ readonly platform: string;
55
+ readonly agents: readonly AgentInfo[];
56
+ }
57
+ export type HostWelcome = {
58
+ readonly accepted: true;
59
+ readonly protocol: number;
60
+ readonly leaseMs: number;
61
+ readonly heartbeatMs: number;
62
+ readonly userActivity?: true;
63
+ } | {
64
+ readonly accepted: false;
65
+ readonly reason: "host_too_old" | "app_too_old";
66
+ readonly protocols: ProtocolRange;
67
+ };
68
+ export declare function parseChatFrame(data: string): ChatFrame | undefined;
69
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,MAAM,EACX,KAAK,QAAQ,EACb,KAAK,MAAM,EACZ,MAAM,cAAc,CAAC;AAGtB,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtD,eAAO,MAAM,YAAY,SAAS,CAAC;AACnC,kEAAkE;AAClE,eAAO,MAAM,WAAW,SAAS,CAAC;AAElC,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,WAAW,CAAC;AAElD;;;;GAIG;AACH,MAAM,MAAM,SAAS,GACjB;IACE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC,CAAC;CAC7D,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvF,oGAAoG;AACpG,MAAM,MAAM,SAAS,GAAG,SAAS,SAAS,MAAM,CAAC,GAC7C,CAAC,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAA;CAAE,GAC3C,IAAI,CAAC,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAC,GAC9B,KAAK,GACP,KAAK,CAAC;AAEV,kCAAkC;AAClC,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC;CACvC;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,IAAI,CAAC;AAE/B,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC;CACvC;AAED,MAAM,MAAM,WAAW,GACnB;IACE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,CAAC,EAAE,IAAI,CAAC;CAC9B,GACD;IACE,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,aAAa,CAAC;IAChD,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;CACnC,CAAC;AAwCN,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CA8BlE"}
@@ -0,0 +1,80 @@
1
+ import { asId, } from "@parlor/core";
2
+ export const CHAT_CHANNEL = "chat";
3
+ /** The plugin name, so its routes live under `/_parlor/chat/`. */
4
+ export const CHAT_PLUGIN = "chat";
5
+ /**
6
+ * The versioned contract between an app's backend and Parlor Host: host actions, item
7
+ * kinds, frames and cursor rules. Bumped on any change an older peer could misread.
8
+ */
9
+ export const HOST_PROTOCOL = 2;
10
+ function isFields(value) {
11
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12
+ }
13
+ function oneOf(values, value) {
14
+ return values.find((candidate) => candidate === value);
15
+ }
16
+ function idOf(kind, value) {
17
+ return typeof value === "string" && value.length > 0 ? asId(kind, value) : undefined;
18
+ }
19
+ function openItemOf(value) {
20
+ if (!isFields(value))
21
+ return undefined;
22
+ const id = idOf("item", value.id);
23
+ if (!id)
24
+ return undefined;
25
+ if (value.kind === "tool") {
26
+ const status = oneOf(["pending", "running"], value.status);
27
+ if (typeof value.title !== "string" || !status)
28
+ return undefined;
29
+ return { id, kind: "tool", title: value.title, status };
30
+ }
31
+ if (value.kind === "plan" && Array.isArray(value.entries)) {
32
+ const entries = [];
33
+ for (const entry of value.entries) {
34
+ if (!isFields(entry) || typeof entry.content !== "string")
35
+ return undefined;
36
+ const status = oneOf(["pending", "in_progress", "completed"], entry.status);
37
+ if (!status)
38
+ return undefined;
39
+ entries.push({ content: entry.content, status });
40
+ }
41
+ return { id, kind: "plan", entries };
42
+ }
43
+ return undefined;
44
+ }
45
+ export function parseChatFrame(data) {
46
+ let value;
47
+ try {
48
+ value = JSON.parse(data);
49
+ }
50
+ catch {
51
+ return undefined;
52
+ }
53
+ if (!isFields(value))
54
+ return undefined;
55
+ const threadId = idOf("thread", value.threadId);
56
+ const turnId = idOf("turn", value.turnId);
57
+ if (!threadId || !turnId)
58
+ return undefined;
59
+ switch (value.type) {
60
+ case "settled":
61
+ return { type: "settled", threadId, turnId };
62
+ case "item": {
63
+ const item = openItemOf(value.item);
64
+ return item ? { type: "item", threadId, turnId, item } : undefined;
65
+ }
66
+ case "delta": {
67
+ const itemId = idOf("item", value.itemId);
68
+ const kind = oneOf(["reply", "reasoning"], value.kind);
69
+ const { offset, text } = value;
70
+ if (!itemId || !kind || typeof text !== "string")
71
+ return undefined;
72
+ if (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0)
73
+ return undefined;
74
+ return { type: "delta", threadId, turnId, itemId, kind, offset, text };
75
+ }
76
+ default:
77
+ return undefined;
78
+ }
79
+ }
80
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n asId,\n type Id,\n type IdKind,\n type ItemId,\n type ThreadId,\n type TurnId,\n} from \"@parlor/core\";\n\nimport type { ChatItemBody } from \"./items.js\";\nimport type { AgentInfo, OpenItem } from \"./model.js\";\n\nexport const CHAT_CHANNEL = \"chat\";\n/** The plugin name, so its routes live under `/_parlor/chat/`. */\nexport const CHAT_PLUGIN = \"chat\";\n\nexport type StreamingKind = \"reply\" | \"reasoning\";\n\n/**\n * Server to browser on the `chat` channel; never persisted. `offset` makes a duplicate\n * delta skippable and a gap detectable, after which the client waits for the durable\n * item. `settled` drops a turn's open items: durable now, or gone.\n */\nexport type ChatFrame =\n | {\n readonly type: \"delta\";\n readonly threadId: ThreadId;\n readonly turnId: TurnId;\n readonly itemId: ItemId;\n readonly kind: StreamingKind;\n readonly offset: number;\n readonly text: string;\n }\n | {\n readonly type: \"item\";\n readonly threadId: ThreadId;\n readonly turnId: TurnId;\n readonly item: Extract<OpenItem, { kind: \"tool\" | \"plan\" }>;\n }\n | { readonly type: \"settled\"; readonly threadId: ThreadId; readonly turnId: TurnId };\n\n/** Host to server: the same frames minus what the batch's turn implies. Only the server settles. */\nexport type HostFrame = ChatFrame extends infer F\n ? F extends { readonly type: \"delta\" | \"item\" }\n ? Omit<F, \"threadId\" | \"turnId\">\n : never\n : never;\n\n/** `POST /_parlor/chat/stream` */\nexport interface HostStreamBatch {\n readonly turnId: TurnId;\n readonly frames: readonly HostFrame[];\n}\n\n/**\n * The versioned contract between an app's backend and Parlor Host: host actions, item\n * kinds, frames and cursor rules. Bumped on any change an older peer could misread.\n */\nexport const HOST_PROTOCOL = 2;\n\nexport interface ProtocolRange {\n readonly min: number;\n readonly max: number;\n}\n\nexport interface HostHello {\n readonly protocols: ProtocolRange;\n readonly hostVersion: string;\n readonly name: string;\n readonly platform: string;\n readonly agents: readonly AgentInfo[];\n}\n\nexport type HostWelcome =\n | {\n readonly accepted: true;\n readonly protocol: number;\n readonly leaseMs: number;\n readonly heartbeatMs: number;\n readonly userActivity?: true;\n }\n | {\n readonly accepted: false;\n readonly reason: \"host_too_old\" | \"app_too_old\";\n readonly protocols: ProtocolRange;\n };\n\ntype Fields = Readonly<Record<string, unknown>>;\n\nfunction isFields(value: unknown): value is Fields {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction oneOf<T extends string>(values: readonly T[], value: unknown): T | undefined {\n return values.find((candidate) => candidate === value);\n}\n\nfunction idOf<K extends IdKind>(kind: K, value: unknown): Id<K> | undefined {\n return typeof value === \"string\" && value.length > 0 ? asId(kind, value) : undefined;\n}\n\nfunction openItemOf(\n value: unknown,\n): Extract<OpenItem, { kind: \"tool\" | \"plan\" }> | undefined {\n if (!isFields(value)) return undefined;\n const id = idOf(\"item\", value.id);\n if (!id) return undefined;\n if (value.kind === \"tool\") {\n const status = oneOf([\"pending\", \"running\"], value.status);\n if (typeof value.title !== \"string\" || !status) return undefined;\n return { id, kind: \"tool\", title: value.title, status };\n }\n if (value.kind === \"plan\" && Array.isArray(value.entries)) {\n const entries: ChatItemBody<\"plan\">[\"entries\"][number][] = [];\n for (const entry of value.entries as unknown[]) {\n if (!isFields(entry) || typeof entry.content !== \"string\") return undefined;\n const status = oneOf([\"pending\", \"in_progress\", \"completed\"], entry.status);\n if (!status) return undefined;\n entries.push({ content: entry.content, status });\n }\n return { id, kind: \"plan\", entries };\n }\n return undefined;\n}\n\nexport function parseChatFrame(data: string): ChatFrame | undefined {\n let value: unknown;\n try {\n value = JSON.parse(data);\n } catch {\n return undefined;\n }\n if (!isFields(value)) return undefined;\n const threadId = idOf(\"thread\", value.threadId);\n const turnId = idOf(\"turn\", value.turnId);\n if (!threadId || !turnId) return undefined;\n switch (value.type) {\n case \"settled\":\n return { type: \"settled\", threadId, turnId };\n case \"item\": {\n const item = openItemOf(value.item);\n return item ? { type: \"item\", threadId, turnId, item } : undefined;\n }\n case \"delta\": {\n const itemId = idOf(\"item\", value.itemId);\n const kind = oneOf([\"reply\", \"reasoning\"], value.kind);\n const { offset, text } = value;\n if (!itemId || !kind || typeof text !== \"string\") return undefined;\n if (typeof offset !== \"number\" || !Number.isInteger(offset) || offset < 0)\n return undefined;\n return { type: \"delta\", threadId, turnId, itemId, kind, offset, text };\n }\n default:\n return undefined;\n }\n}\n"]}