pi-roundtable 0.1.0 → 0.2.1

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 (239) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/README.md +10 -6
  3. package/README.zh-TW.md +144 -0
  4. package/docs/plugins.md +1448 -66
  5. package/examples/echo-runtime.test.ts +122 -0
  6. package/examples/echo-runtime.ts +107 -0
  7. package/examples/events.test.ts +1 -0
  8. package/examples/events.ts +2 -2
  9. package/examples/fake-surface.test.ts +180 -0
  10. package/examples/fake-surface.ts +109 -0
  11. package/examples/interactions.test.ts +12 -3
  12. package/examples/interactions.ts +19 -19
  13. package/examples/shared-services.test.ts +74 -0
  14. package/examples/shared-services.ts +60 -0
  15. package/examples/study-room.test.ts +69 -0
  16. package/examples/study-room.ts +52 -0
  17. package/examples/support-desk.test.ts +34 -0
  18. package/examples/support-desk.ts +40 -0
  19. package/package.json +11 -3
  20. package/src/cli/checks/database.ts +11 -30
  21. package/src/cli/checks/images.ts +19 -0
  22. package/src/cli/doctor.ts +6 -0
  23. package/src/cli/main.ts +2 -1
  24. package/src/core/agents/agent-claim.ts +42 -16
  25. package/src/core/agents/agent-messages.ts +4 -4
  26. package/src/core/agents/agent-ports.ts +8 -21
  27. package/src/core/agents/agent-prompt.ts +3 -1
  28. package/src/core/agents/agent-store.ts +6 -4
  29. package/src/core/agents/agent-team-fixture.ts +389 -0
  30. package/src/core/agents/agent-team.ts +26 -25
  31. package/src/core/agents/agent-tools.ts +34 -15
  32. package/src/core/agents/avatar-studio.ts +40 -6
  33. package/src/core/agents/fallback-avatar.ts +85 -0
  34. package/src/core/agents/team-editing.ts +49 -10
  35. package/src/core/agents/team-keys.ts +22 -9
  36. package/src/core/agents/team-layout.ts +4 -3
  37. package/src/core/agents/team-lifecycle.ts +28 -2
  38. package/src/core/agents/team-options.ts +9 -5
  39. package/src/core/agents/team-status.ts +4 -4
  40. package/src/core/agents/team-text.ts +8 -6
  41. package/src/core/agents/team-turn-types.ts +4 -4
  42. package/src/core/agents/team-turns.ts +6 -5
  43. package/src/core/builtin/agent-server.ts +187 -121
  44. package/src/core/builtin/discord-admin.ts +41 -0
  45. package/src/core/builtin/discord.ts +65 -40
  46. package/src/core/builtin/modules.ts +67 -58
  47. package/src/core/builtin/session-tool.ts +24 -0
  48. package/src/core/builtin/skills.ts +72 -0
  49. package/src/core/builtin/stores.ts +55 -31
  50. package/src/core/config/config.ts +45 -4
  51. package/src/core/config/schema.ts +6 -0
  52. package/src/core/contract/channels.ts +54 -14
  53. package/src/core/contract/providers.ts +17 -0
  54. package/src/core/contract/runtime.ts +138 -0
  55. package/src/core/contract/services.ts +45 -0
  56. package/src/core/contract/surface.ts +88 -0
  57. package/src/core/db/migrations.ts +118 -3
  58. package/src/core/define-roundtable.ts +70 -46
  59. package/src/core/define.ts +2 -1
  60. package/src/core/discord/agent-commands.ts +39 -14
  61. package/src/core/discord/agent-panel.ts +66 -0
  62. package/src/core/discord/channel-executor.ts +6 -3
  63. package/src/core/discord/channel-operations.ts +14 -11
  64. package/src/core/discord/command-collection.ts +46 -0
  65. package/src/core/{registry/interactions.ts → discord/compose-commands.ts} +5 -5
  66. package/src/core/discord/connection.ts +35 -0
  67. package/src/core/discord/discord-surface.ts +32 -94
  68. package/src/core/discord/dispatch-threads.ts +2 -2
  69. package/src/core/discord/inbound-message.ts +99 -0
  70. package/src/core/discord/interaction-module.ts +57 -1
  71. package/src/core/discord/owner-cards.ts +4 -4
  72. package/src/core/discord/owner-command.ts +44 -11
  73. package/src/core/discord/owner-discord.ts +2 -2
  74. package/src/core/discord/schedule-commands.ts +21 -18
  75. package/src/core/discord/stop-button.ts +44 -1
  76. package/src/core/domain/attachment.ts +12 -8
  77. package/src/core/domain/conversation.ts +3 -4
  78. package/src/core/domain/owner-prompts.ts +5 -5
  79. package/src/core/domain/ports.ts +9 -53
  80. package/src/core/freeze.ts +9 -0
  81. package/src/core/host.ts +255 -82
  82. package/src/core/http/listeners.ts +55 -20
  83. package/src/core/i18n/agent-panel.ts +4 -0
  84. package/src/core/i18n/index.ts +12 -2
  85. package/src/core/i18n/owner.ts +7 -1
  86. package/src/core/i18n/schedules.ts +12 -6
  87. package/src/core/identity.ts +1 -1
  88. package/src/core/judging/effort-judge.ts +25 -3
  89. package/src/core/log.ts +21 -3
  90. package/src/core/models.ts +2 -2
  91. package/src/core/modules/background/background-turns.ts +6 -4
  92. package/src/core/modules/delegation/delegate.ts +3 -2
  93. package/src/core/modules/delegation/delegator.ts +15 -13
  94. package/src/core/modules/delegation/{sol-worker.ts → web-research-worker.ts} +6 -6
  95. package/src/core/modules/discord-admin/discord-admin.ts +6 -6
  96. package/src/core/modules/host-shell/shell-policy.ts +8 -3
  97. package/src/core/modules/memory/owner-memory-store.ts +27 -25
  98. package/src/core/modules/memory/owner-memory.ts +9 -13
  99. package/src/core/modules/schedules/recurrence.ts +5 -3
  100. package/src/core/modules/schedules/schedule-store.ts +11 -11
  101. package/src/core/modules/schedules/schedule-tools.ts +22 -20
  102. package/src/core/modules/schedules/scheduler.ts +7 -2
  103. package/src/core/modules/schedules/schedules.ts +9 -3
  104. package/src/core/modules/skills/skill-registry.ts +3 -2
  105. package/src/core/modules/skills/skill-store.ts +2 -15
  106. package/src/core/modules/skills/skill-tools.ts +10 -4
  107. package/src/core/ops/error-reporter.ts +1 -1
  108. package/src/core/plugin.ts +232 -28
  109. package/src/core/registry/contributions.ts +173 -14
  110. package/src/core/registry/providers.ts +35 -5
  111. package/src/core/registry/services.ts +229 -0
  112. package/src/core/routing/channel-queue.ts +5 -0
  113. package/src/core/routing/channel-router.ts +21 -7
  114. package/src/core/routing/conversation-turns.ts +139 -0
  115. package/src/core/routing/message-text.ts +9 -2
  116. package/src/core/routing/surface-port.ts +39 -0
  117. package/src/core/runtime/conversation-sessions.ts +4 -3
  118. package/src/core/runtime/extensions/confirmation-fixture.ts +11 -0
  119. package/src/core/runtime/extensions/confirmation-gate.ts +11 -9
  120. package/src/core/runtime/mcp.ts +1 -1
  121. package/src/core/runtime/pending-confirmation-store.ts +10 -12
  122. package/src/core/runtime/pi-agent-runtime.ts +9 -6
  123. package/src/core/runtime/prompt-slot.ts +8 -3
  124. package/src/core/runtime/runtime-types.ts +9 -35
  125. package/src/core/runtime/session-factory.ts +21 -5
  126. package/src/core/runtime/text-tools.ts +1 -3
  127. package/src/core/services.ts +238 -90
  128. package/src/core/sessions.ts +3 -2
  129. package/src/core/shared/{profile-tools.ts → active-tools.ts} +2 -2
  130. package/src/core/shared/delegate-tool.ts +4 -3
  131. package/src/core/shared/schedule-tools.ts +22 -13
  132. package/src/core/shared/session-messages.ts +1 -1
  133. package/src/core/speakers.ts +6 -38
  134. package/src/core/testing/database.ts +1 -7
  135. package/src/core/testing/eager-catalog.ts +30 -0
  136. package/src/core/testing/locale.ts +23 -5
  137. package/src/core/testing/modules.ts +105 -41
  138. package/src/core/testing/test-host.ts +279 -0
  139. package/src/core/testing/tool-set.ts +64 -0
  140. package/src/core/time.ts +6 -1
  141. package/src/core/tool-set.snapshot.json +288 -0
  142. package/src/core/tool-tiers.ts +17 -57
  143. package/src/discord/index.ts +64 -0
  144. package/src/index.ts +181 -10
  145. package/src/kit/channels.ts +14 -0
  146. package/src/kit/domain.ts +18 -0
  147. package/src/kit/holds.ts +4 -0
  148. package/src/kit/index.ts +129 -0
  149. package/src/kit/judging.ts +10 -0
  150. package/src/kit/memory.ts +3 -0
  151. package/src/kit/mirror.ts +19 -0
  152. package/src/kit/presentation.ts +7 -0
  153. package/src/kit/shell.ts +7 -0
  154. package/src/kit/skills.ts +8 -0
  155. package/src/kit/support.ts +21 -0
  156. package/src/kit/threads.ts +7 -0
  157. package/src/kit/tools.ts +14 -0
  158. package/src/kit/worker.ts +16 -0
  159. package/src/testing.ts +394 -34
  160. package/examples/guide.test.ts +0 -52
  161. package/src/cli/add-plugin.test.ts +0 -107
  162. package/src/cli/checks/basic.test.ts +0 -276
  163. package/src/cli/checks/database.test.ts +0 -117
  164. package/src/cli/checks/discord.test.ts +0 -200
  165. package/src/cli/cli.test.ts +0 -122
  166. package/src/cli/config-edit.test.ts +0 -130
  167. package/src/cli/doctor.test.ts +0 -184
  168. package/src/cli/init.test.ts +0 -127
  169. package/src/cli/size.test.ts +0 -9
  170. package/src/cli/templates.test.ts +0 -129
  171. package/src/cli/testing/fixtures.ts +0 -106
  172. package/src/core/agents/agent-claim.test.ts +0 -142
  173. package/src/core/agents/agent-dashboard.test.ts +0 -120
  174. package/src/core/agents/agent-guild.test.ts +0 -124
  175. package/src/core/agents/agent-store.test.ts +0 -205
  176. package/src/core/agents/avatar-studio.test.ts +0 -88
  177. package/src/core/agents/group-round.test.ts +0 -143
  178. package/src/core/agents/owner-identity.test.ts +0 -172
  179. package/src/core/agents/team-turns.test.ts +0 -278
  180. package/src/core/attachments/attachments.test.ts +0 -85
  181. package/src/core/boundary.test.ts +0 -45
  182. package/src/core/builtin/modules.test.ts +0 -127
  183. package/src/core/config/config.test.ts +0 -135
  184. package/src/core/contract/discord.ts +0 -33
  185. package/src/core/db/migrations.test.ts +0 -237
  186. package/src/core/define-roundtable.test.ts +0 -134
  187. package/src/core/define.test.ts +0 -144
  188. package/src/core/discord/agent-commands.test.ts +0 -58
  189. package/src/core/discord/agent-discord.test.ts +0 -70
  190. package/src/core/discord/dispatch-thread-host.test.ts +0 -114
  191. package/src/core/discord/dispatch-threads.test.ts +0 -94
  192. package/src/core/discord/owner-cards.test.ts +0 -435
  193. package/src/core/discord/owner-discord.test.ts +0 -325
  194. package/src/core/domain/expression.ts +0 -21
  195. package/src/core/domain/profile.ts +0 -32
  196. package/src/core/drain.test.ts +0 -46
  197. package/src/core/events.test.ts +0 -81
  198. package/src/core/holds.test.ts +0 -54
  199. package/src/core/host.test.ts +0 -536
  200. package/src/core/http/listeners.test.ts +0 -164
  201. package/src/core/i18n/i18n.test.ts +0 -139
  202. package/src/core/identity.test.ts +0 -18
  203. package/src/core/judging/effort-judge.test.ts +0 -112
  204. package/src/core/judging/model-judge.test.ts +0 -126
  205. package/src/core/modules/delegation/delegator.test.ts +0 -170
  206. package/src/core/modules/memory/owner-memory-store.test.ts +0 -244
  207. package/src/core/modules/schedules/schedule.test.ts +0 -358
  208. package/src/core/modules/skills/skill-kind.test.ts +0 -55
  209. package/src/core/modules/skills/skill-registry.test.ts +0 -376
  210. package/src/core/ops/error-reporter.test.ts +0 -283
  211. package/src/core/presentation/card-cadence.ts +0 -41
  212. package/src/core/presentation/presentation.test.ts +0 -166
  213. package/src/core/public-entry.test.ts +0 -59
  214. package/src/core/registry/contributions.test.ts +0 -215
  215. package/src/core/registry/interactions.test.ts +0 -87
  216. package/src/core/registry/providers.test.ts +0 -103
  217. package/src/core/routing/channel-queue.test.ts +0 -31
  218. package/src/core/routing/channel-router.test.ts +0 -326
  219. package/src/core/routing/conversation-kind.ts +0 -14
  220. package/src/core/routing/settle-turn.test.ts +0 -21
  221. package/src/core/runtime/compaction-tiers.test.ts +0 -227
  222. package/src/core/runtime/extensions/ask-user.test.ts +0 -114
  223. package/src/core/runtime/extensions/self-compact-guard.test.ts +0 -50
  224. package/src/core/runtime/pending-confirmation-store.test.ts +0 -52
  225. package/src/core/runtime/session-archive.test.ts +0 -20
  226. package/src/core/runtime/steerable-run.test.ts +0 -264
  227. package/src/core/runtime/text-tools.test.ts +0 -110
  228. package/src/core/runtime/turn-answer.test.ts +0 -72
  229. package/src/core/runtime/worker-task.test.ts +0 -82
  230. package/src/core/services.test.ts +0 -28
  231. package/src/core/sessions.test.ts +0 -85
  232. package/src/core/shared/unix-server.ts +0 -18
  233. package/src/core/size.test.ts +0 -9
  234. package/src/core/speakers.test.ts +0 -78
  235. package/src/core/testing/file-size.ts +0 -34
  236. package/src/core/time.test.ts +0 -166
  237. package/src/core/tool-tiers.test.ts +0 -117
  238. package/src/entries.test.ts +0 -112
  239. package/src/testing.test.ts +0 -156
@@ -1,5 +1,14 @@
1
- import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
1
+ import {
2
+ ActionRowBuilder,
3
+ ButtonBuilder,
4
+ ButtonStyle,
5
+ type Interaction,
6
+ MessageFlags,
7
+ } from "discord.js";
8
+ import type { ConversationPort } from "../contract/channels.ts";
9
+ import { channelKey } from "../contract/surface.ts";
2
10
  import { messages } from "../i18n/index.ts";
11
+ import type { CommandGuard, InteractionModule } from "./interaction-module.ts";
3
12
 
4
13
  export const STOP_BUTTON_ID = "roundtable:stop";
5
14
 
@@ -18,3 +27,37 @@ export function stopPanel() {
18
27
  ],
19
28
  };
20
29
  }
30
+
31
+ export interface StopButtonOptions {
32
+ guard: Pick<CommandGuard, "isOwner">;
33
+ /** Stops the running turn of the channel the button was pressed in, through its claim. */
34
+ conversations: Pick<ConversationPort, "stop">;
35
+ }
36
+
37
+ /**
38
+ * Answers a press of the stop button: only the owner stops the channel's turn, and the answer,
39
+ * visible only to whoever pressed, says whether one was running.
40
+ */
41
+ export function stopButtonModule(
42
+ options: StopButtonOptions,
43
+ ): InteractionModule {
44
+ return {
45
+ commands: () => [],
46
+ handle: async (interaction: Interaction) => {
47
+ if (!interaction.isButton() || interaction.customId !== STOP_BUTTON_ID)
48
+ return false;
49
+ const owner = options.guard.isOwner(interaction);
50
+ const stopped =
51
+ owner &&
52
+ options.conversations.stop(
53
+ channelKey("discord", interaction.channelId),
54
+ );
55
+ const text = messages();
56
+ let content = text.stopIdle;
57
+ if (!owner) content = text.stopOwnerOnly;
58
+ else if (stopped) content = text.stopDone;
59
+ await interaction.reply({ content, flags: MessageFlags.Ephemeral });
60
+ return true;
61
+ },
62
+ };
63
+ }
@@ -27,13 +27,17 @@ export interface ModelImage {
27
27
 
28
28
  /** Everything a turn carries besides its text. */
29
29
  export interface TurnAttachments {
30
- files: StoredAttachment[];
31
- images: ModelImage[];
32
- failures: AttachmentFailure[];
30
+ readonly files: readonly StoredAttachment[];
31
+ readonly images: readonly ModelImage[];
32
+ readonly failures: readonly AttachmentFailure[];
33
33
  }
34
34
 
35
- export const NO_ATTACHMENTS: TurnAttachments = Object.freeze({
36
- files: [],
37
- images: [],
38
- failures: [],
39
- }) as TurnAttachments;
35
+ export const NO_ATTACHMENTS: Readonly<{
36
+ files: readonly StoredAttachment[];
37
+ images: readonly ModelImage[];
38
+ failures: readonly AttachmentFailure[];
39
+ }> = Object.freeze({
40
+ files: Object.freeze([]),
41
+ images: Object.freeze([]),
42
+ failures: Object.freeze([]),
43
+ });
@@ -1,7 +1,5 @@
1
1
  import type { InboundMessage } from "../contract/channels.ts";
2
2
  import type { ChannelKey } from "../sessions.ts";
3
- import type { AgentRunError } from "./errors.ts";
4
- import type { ProfileId } from "./profile.ts";
5
3
 
6
4
  export { QUEUED_MARK, STEERED_MARK } from "../contract/channels.ts";
7
5
  export type { ChannelKey, InboundMessage };
@@ -21,14 +19,15 @@ export interface HeldCall {
21
19
  }
22
20
 
23
21
  export interface PendingConfirmation {
24
- profile: ProfileId;
22
+ /** The id of the TurnSelection whose turn held the calls; the caller resolves it on replay. */
23
+ selectionId: string;
25
24
  heldAt: Date;
26
25
  calls: HeldCall[];
27
26
  }
28
27
 
29
28
  export type TurnResult =
30
29
  | { ok: true; text: string; thinking?: string }
31
- | { ok: false; error: AgentRunError; stopped?: true };
30
+ | { ok: false; error: Error; stopped?: true };
32
31
 
33
32
  export interface OutboundReply {
34
33
  /** The assistant's thinking for this turn, posted as a quiet line before the card. */
@@ -1,6 +1,6 @@
1
1
  import type { Tier } from "../speakers.ts";
2
2
  /**
3
- * The owner answering inside a running turn: a card in the turn's channel that only he may
3
+ * The owner answering inside a running turn: a card in the turn's channel that only they may
4
4
  * answer. Unanswered cards expire; a stopped turn cancels its open cards.
5
5
  */
6
6
  export type Approval = "approved" | "declined" | "expired" | "cancelled";
@@ -16,19 +16,19 @@ export interface OwnerQuestion {
16
16
  options: readonly AskOption[];
17
17
  /** More than one option may be chosen. */
18
18
  multi: boolean;
19
- /** He may write an answer of his own besides the options. */
19
+ /** They may write an answer of their own besides the options. */
20
20
  allowOther: boolean;
21
21
  }
22
22
 
23
23
  export interface OwnerAnswer {
24
24
  /** The chosen options' labels, in the order they were offered. */
25
25
  choices: string[];
26
- /** What he wrote himself, if anything. */
26
+ /** What they wrote himself, if anything. */
27
27
  text?: string;
28
28
  }
29
29
 
30
30
  export interface OwnerPrompts {
31
- /** Asks him to approve an action; `signal` cancels the card when the turn stops. */
31
+ /** Asks them to approve an action; `signal` cancels the card when the turn stops. */
32
32
  confirm(
33
33
  title: string,
34
34
  message: string,
@@ -36,7 +36,7 @@ export interface OwnerPrompts {
36
36
  /** The lowest tier that may approve; the owner only when absent. */
37
37
  minTier?: Tier,
38
38
  ): Promise<Approval>;
39
- /** Asks him a question; undefined when it expired or was cancelled. */
39
+ /** Asks them a question; undefined when it expired or was cancelled. */
40
40
  ask(
41
41
  title: string,
42
42
  question: OwnerQuestion,
@@ -1,36 +1,14 @@
1
1
  import type { AgentTurnScope, TurnSelection } from "../sessions.ts";
2
2
  import type { Speaker } from "../speakers.ts";
3
3
  import type { TurnAttachments } from "./attachment.ts";
4
- import type {
5
- ChannelKey,
6
- InboundMessage,
7
- OutboundReply,
8
- PendingConfirmation,
9
- TranscriptEntry,
10
- TurnResult,
11
- } from "./conversation.ts";
12
-
13
- export interface ChatSurface {
14
- start(onMessage: (message: InboundMessage) => void): Promise<void>;
15
- sendReply(channel: ChannelKey, reply: OutboundReply): Promise<void>;
16
- /** Shows the typing indicator until the returned function is called. */
17
- startTyping(channel: ChannelKey): () => void;
18
- /**
19
- * Shows the owner a stop button once the turn has run a few seconds, until the returned
20
- * function is called.
21
- */
22
- showStop(channel: ChannelKey): () => void;
23
- /** Adds or removes the bot's reaction on a message; failures are logged, never thrown. */
24
- react(channel: ChannelKey, messageId: string, emoji: string): Promise<void>;
25
- unreact(channel: ChannelKey, messageId: string, emoji: string): Promise<void>;
26
- }
4
+ import type { ChannelKey } from "./conversation.ts";
27
5
 
28
6
  export type { AgentTurnScope };
29
7
 
30
8
  export interface TurnRequest {
31
9
  /** Where the turn's message arrived and its attachments are saved. */
32
10
  channel: ChannelKey;
33
- /** The turn's tools; the owner's conversations pick a profile's, an agent's are the agent union. */
11
+ /** The caller's choice of tools for the turn, such as an agent's union. */
34
12
  selection: TurnSelection;
35
13
  text: string;
36
14
  /** Files and images the message carries; none when absent. */
@@ -41,43 +19,21 @@ export interface TurnRequest {
41
19
  agent?: AgentTurnScope;
42
20
  /** The person the turn is for: who wrote the message, or who set up the work. */
43
21
  speaker?: Speaker;
44
- /** The owner's own chat turn, which his next messages may steer. */
22
+ /**
23
+ * The conversation's kind, fixed when its session is made: the string a claim's `startFresh`
24
+ * returns, which picks the persona of a non-agent conversation. "owner" when absent.
25
+ */
26
+ kind?: string;
27
+ /** The owner's own chat turn, which their next messages may steer. */
45
28
  steerable?: boolean;
46
29
  /**
47
30
  * The owner started the turn by writing in its channel, or it delivers a dispatch's report,
48
- * so it may ask him there on cards: approvals of held actions and ask_user questions. A
31
+ * so it may ask them there on cards: approvals of held actions and ask_user questions. A
49
32
  * schedule's turn is not.
50
33
  */
51
34
  interactive?: boolean;
52
35
  }
53
36
 
54
- export interface AgentRuntime {
55
- runTurn(request: TurnRequest): Promise<TurnResult>;
56
- /** Held actions of a conversation: a channel, or an agent's session key. */
57
- pendingConfirmation(channel: ChannelKey): PendingConfirmation | undefined;
58
- recentTranscript(
59
- channel: ChannelKey,
60
- limit: number,
61
- ): Promise<TranscriptEntry[]>;
62
- /** Archives a conversation (a channel, or an agent's session key) and drops its held actions; call between turns. */
63
- startFresh(channel: ChannelKey): Promise<void>;
64
- /** Like `startFresh`, but removes the conversation and every archive of it for good; call between turns. */
65
- deleteConversation(channel: ChannelKey): Promise<void>;
66
- /**
67
- * Adds the message to the conversation's running turn when that turn is steerable and holds
68
- * no actions; false when the message must wait for its own turn.
69
- */
70
- steer(
71
- channel: ChannelKey,
72
- text: string,
73
- attachments: TurnAttachments,
74
- /** The message's author; a turn another speaker started takes no steering from them. */
75
- speakerId?: string,
76
- ): Promise<boolean>;
77
- /** Aborts the conversation's running turn and drops what was steered into it; false when none runs. */
78
- stop(channel: ChannelKey): boolean;
79
- }
80
-
81
37
  /** The judge's question and answer shapes. */
82
38
  export type {
83
39
  ChoiceAnswer,
@@ -0,0 +1,9 @@
1
+ /** Freeze shared protocol data recursively, including symbol-keyed schema metadata. */
2
+ export function freeze<T extends object>(value: T): Readonly<T> {
3
+ for (const key of Reflect.ownKeys(value)) {
4
+ const child: unknown = (value as Record<PropertyKey, unknown>)[key];
5
+ if (child && typeof child === "object" && !Object.isFrozen(child))
6
+ freeze(child);
7
+ }
8
+ return Object.freeze(value);
9
+ }