pi-roundtable 0.8.0 → 0.9.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 (150) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +35 -11
  3. package/README.zh-TW.md +27 -10
  4. package/docs/migration-0.9.md +247 -0
  5. package/docs/plugins.md +606 -78
  6. package/examples/channel-context.test.ts +88 -0
  7. package/examples/channel-context.ts +62 -0
  8. package/examples/echo-runtime.test.ts +1 -0
  9. package/examples/fake-surface.test.ts +69 -6
  10. package/examples/fake-surface.ts +73 -7
  11. package/examples/headless.ts +5 -1
  12. package/examples/prechecks.test.ts +1 -0
  13. package/examples/principals.test.ts +60 -0
  14. package/examples/principals.ts +48 -0
  15. package/examples/prompt.test.ts +1 -1
  16. package/examples/study-room.test.ts +29 -1
  17. package/examples/study-room.ts +16 -19
  18. package/examples/support-desk.test.ts +3 -1
  19. package/examples/tools.test.ts +6 -1
  20. package/package.json +5 -2
  21. package/src/cli/checks/database.ts +11 -0
  22. package/src/cli/checks/identity.ts +225 -0
  23. package/src/cli/checks/model.ts +21 -3
  24. package/src/cli/cli.ts +115 -1
  25. package/src/cli/config-edit.ts +83 -13
  26. package/src/cli/doctor.ts +7 -0
  27. package/src/cli/line-diff.ts +72 -0
  28. package/src/cli/principal.ts +412 -0
  29. package/src/cli/project.ts +5 -0
  30. package/src/cli/report.ts +12 -3
  31. package/src/cli/upgrade-rewrite.ts +102 -0
  32. package/src/cli/upgrade-source.ts +430 -0
  33. package/src/cli/upgrade.ts +176 -0
  34. package/src/core/agents/agent-claim.ts +56 -34
  35. package/src/core/agents/team-turns.ts +8 -4
  36. package/src/core/builtin/agent-server.ts +9 -7
  37. package/src/core/builtin/discord-admin.ts +4 -1
  38. package/src/core/builtin/discord.ts +71 -13
  39. package/src/core/builtin/modules.ts +140 -85
  40. package/src/core/builtin/stores.ts +40 -21
  41. package/src/core/config/access.ts +229 -0
  42. package/src/core/config/adapters.ts +64 -1
  43. package/src/core/config/config.ts +78 -41
  44. package/src/core/contract/channel-context.ts +53 -0
  45. package/src/core/contract/channels.ts +77 -9
  46. package/src/core/contract/runtime.ts +23 -7
  47. package/src/core/contract/surface.ts +22 -6
  48. package/src/core/conversations/conversation-registry.ts +14 -0
  49. package/src/core/conversations/conversation-store.ts +13 -0
  50. package/src/core/define-roundtable.ts +41 -11
  51. package/src/core/discord/agent-panel.ts +2 -2
  52. package/src/core/discord/card-audience.ts +148 -0
  53. package/src/core/discord/channel-context.ts +348 -0
  54. package/src/core/discord/connection.ts +15 -3
  55. package/src/core/discord/direct-channel.ts +54 -0
  56. package/src/core/discord/discord-owners.ts +133 -0
  57. package/src/core/discord/discord-surface.ts +69 -14
  58. package/src/core/discord/fresh-marker.ts +24 -0
  59. package/src/core/discord/inbound-message.ts +21 -7
  60. package/src/core/discord/interaction-module.ts +11 -1
  61. package/src/core/discord/owner-cards-fixture.ts +80 -0
  62. package/src/core/discord/owner-cards.ts +58 -97
  63. package/src/core/discord/owner-command.ts +27 -13
  64. package/src/core/discord/stop-button.ts +4 -4
  65. package/src/core/domain/conversation.ts +7 -2
  66. package/src/core/domain/errors.ts +5 -0
  67. package/src/core/domain/ports.ts +27 -2
  68. package/src/core/host.ts +27 -8
  69. package/src/core/i18n/cards.ts +8 -4
  70. package/src/core/identity/access-policy.ts +227 -0
  71. package/src/core/identity/actor-facts.ts +65 -0
  72. package/src/core/identity/caching-principal-store.ts +111 -0
  73. package/src/core/identity/config-owner.ts +145 -0
  74. package/src/core/identity/contact.ts +27 -0
  75. package/src/core/identity/identity-plugin.ts +56 -0
  76. package/src/core/identity/identity-schema.ts +282 -0
  77. package/src/core/identity/identity-service.ts +489 -0
  78. package/src/core/identity/identity-view.ts +77 -0
  79. package/src/core/identity/plugin-identities.ts +109 -0
  80. package/src/core/identity/principal-reads.ts +38 -0
  81. package/src/core/identity/principal-store.ts +478 -0
  82. package/src/core/identity/principal-tables.ts +53 -0
  83. package/src/core/identity/seen-throttle.ts +57 -0
  84. package/src/core/identity/ulid.ts +36 -0
  85. package/src/core/identity.ts +34 -5
  86. package/src/core/interactions/prompts.ts +127 -0
  87. package/src/core/modules/background/background-turns.ts +56 -17
  88. package/src/core/modules/background/personal-target.ts +50 -0
  89. package/src/core/modules/delegation/delegate.ts +28 -14
  90. package/src/core/modules/delegation/delegator.ts +23 -7
  91. package/src/core/modules/memory/owner-memory.ts +123 -50
  92. package/src/core/modules/notify/notify.ts +53 -13
  93. package/src/core/modules/notify/session-notify.ts +110 -0
  94. package/src/core/modules/schedules/prechecks.ts +8 -2
  95. package/src/core/modules/schedules/schedule-prechecks.ts +4 -1
  96. package/src/core/modules/schedules/schedule-store.ts +51 -20
  97. package/src/core/modules/schedules/schedule-tools.ts +122 -12
  98. package/src/core/modules/schedules/scheduler.ts +43 -2
  99. package/src/core/modules/schedules/schedules.ts +43 -14
  100. package/src/core/plugin.ts +48 -0
  101. package/src/core/presence/direct-channels.ts +104 -0
  102. package/src/core/registry/contributions.ts +63 -3
  103. package/src/core/routing/channel-router.ts +218 -17
  104. package/src/core/routing/conversation-turns.ts +38 -8
  105. package/src/core/routing/surface-port.ts +32 -2
  106. package/src/core/routing/system-turns.ts +29 -0
  107. package/src/core/runtime/bridge-guard.ts +225 -0
  108. package/src/core/runtime/compaction-tiers.ts +5 -1
  109. package/src/core/runtime/conversation-sessions.ts +124 -12
  110. package/src/core/runtime/extensions/ask-user.ts +1 -1
  111. package/src/core/runtime/extensions/confirmation-gate.ts +28 -14
  112. package/src/core/runtime/extensions/private-memory.ts +447 -0
  113. package/src/core/runtime/pending-confirmation-store.ts +30 -5
  114. package/src/core/runtime/pi-agent-runtime.ts +135 -134
  115. package/src/core/runtime/preflight-tools.ts +47 -0
  116. package/src/core/runtime/prompt-slot.ts +4 -4
  117. package/src/core/runtime/reader-history.ts +129 -0
  118. package/src/core/runtime/runtime-plugin.ts +50 -5
  119. package/src/core/runtime/runtime-types.ts +40 -12
  120. package/src/core/runtime/session-conversation.ts +143 -0
  121. package/src/core/runtime/session-factory.ts +115 -15
  122. package/src/core/runtime/session-scope.ts +78 -0
  123. package/src/core/runtime/steerable-run.ts +9 -0
  124. package/src/core/runtime/tool-call-scope.ts +43 -0
  125. package/src/core/runtime/turn-answer.ts +30 -1
  126. package/src/core/services.ts +27 -1
  127. package/src/core/sessions.ts +30 -0
  128. package/src/core/speakers.ts +21 -19
  129. package/src/core/testing/background.ts +31 -0
  130. package/src/core/testing/fixture-database.ts +55 -0
  131. package/src/core/testing/map-identity.ts +76 -0
  132. package/src/core/testing/module-sessions.ts +134 -0
  133. package/src/core/testing/modules.ts +108 -21
  134. package/src/core/testing/owner.ts +17 -0
  135. package/src/core/testing/surface-contract.ts +118 -41
  136. package/src/core/testing/test-host.ts +79 -16
  137. package/src/core/testing/tool-set.ts +32 -0
  138. package/src/core/tool-set.snapshot.json +4 -4
  139. package/src/core/tool-tiers.ts +57 -3
  140. package/src/discord/index.ts +8 -0
  141. package/src/index.ts +47 -8
  142. package/src/kit/compaction.ts +13 -0
  143. package/src/kit/index.ts +10 -0
  144. package/src/kit/support.ts +1 -1
  145. package/src/testing.ts +55 -16
  146. package/templates/.env.example +1 -1
  147. package/templates/adapters/web/README.md +7 -0
  148. package/templates/adapters/web/roundtable.config.ts +14 -10
  149. package/templates/roundtable.config.ts +12 -1
  150. package/src/core/domain/owner-prompts.ts +0 -45
@@ -0,0 +1,282 @@
1
+ import type { SQL } from "bun";
2
+ import type { Migration } from "../db/migrations.ts";
3
+ import { type Tier, tierAtLeast } from "../speakers.ts";
4
+ import type { AccessRules, AccessTier } from "./access-policy.ts";
5
+ import { parseIdentity } from "./actor-facts.ts";
6
+ import {
7
+ LEGACY_PROVIDER,
8
+ LEGACY_REMOTE_SPEAKER,
9
+ SYSTEM_PRINCIPAL,
10
+ } from "./principal-store.ts";
11
+ import {
12
+ PRINCIPAL_PLUGIN_LINKS,
13
+ PRINCIPAL_TABLES,
14
+ PRINCIPALS_CLAIMABLE,
15
+ } from "./principal-tables.ts";
16
+
17
+ /** Where the backfill finds the ids of people 0.8 stored: the configured owners, then four tables. */
18
+ export type BackfillSource =
19
+ | "config"
20
+ | "owner_memory"
21
+ | "schedules"
22
+ | "conversations"
23
+ | "held_actions";
24
+
25
+ /** The rules the backfill reads: the owners with a principal id, made first by their names, and the tiers that cap what a carried-over author of schedules is seen at. */
26
+ export type BackfillRules = Pick<AccessRules, "owners" | "admins" | "members">;
27
+
28
+ /** What one backfill made: the principals it created, and how many distinct ids each source held. */
29
+ export interface BackfillSummary {
30
+ created: number;
31
+ sources: Record<BackfillSource, number>;
32
+ }
33
+
34
+ /**
35
+ * The column each table keeps a person's id in, and the column naming them, where there is one.
36
+ * `principal` is a column a newer build fills with the principal beside an actor id: a row with
37
+ * it filled is not 0.8's, so its id makes no principal. Where `principalHold` names the hold the
38
+ * principal was written with, it counts only while that is the row's (`principalHold = held`),
39
+ * as 0.8 writes over a row and leaves the principal as it was.
40
+ */
41
+ const TABLES: readonly {
42
+ table: Exclude<BackfillSource, "config">;
43
+ column: string;
44
+ name?: string;
45
+ order?: string;
46
+ principal?: string;
47
+ principalHold?: { column: string; held: string };
48
+ }[] = [
49
+ { table: "owner_memory", column: "speaker_id" },
50
+ {
51
+ table: "schedules",
52
+ column: "created_by_id",
53
+ name: "created_by_name",
54
+ order: "created_at DESC",
55
+ },
56
+ { table: "conversations", column: "principal_id" },
57
+ {
58
+ table: "held_actions",
59
+ column: "speaker_id",
60
+ principal: "principal_id",
61
+ principalHold: { column: "principal_held_at", held: "held_at" },
62
+ },
63
+ ];
64
+
65
+ /** Whether the tier's rules could hold the person of a 0.8 speaker id, whose surface and roles are unknown here. */
66
+ function couldHold(tier: AccessTier | undefined, id: string): boolean {
67
+ if (!tier) return false;
68
+ const { everyone } = tier;
69
+ if (everyone === true || (Array.isArray(everyone) && everyone.length > 0))
70
+ return true;
71
+ if ((tier.roles?.length ?? 0) > 0) return true;
72
+ return (tier.identities ?? []).some(
73
+ (identity) => identity === id || parseIdentity(identity)?.subject === id,
74
+ );
75
+ }
76
+
77
+ /** The most the rules could give the person of a 0.8 speaker id: admin, member, or nothing; never owner, which only the owners hold. */
78
+ function ceilingOf(
79
+ rules: BackfillRules,
80
+ id: string,
81
+ ): Exclude<Tier, "owner"> | undefined {
82
+ if (couldHold(rules.admins, id)) return "admin";
83
+ if (couldHold(rules.members, id)) return "member";
84
+ return undefined;
85
+ }
86
+
87
+ /** The highest tier each author created a schedule at, where the table records it. */
88
+ async function scheduledTiers(sql: SQL): Promise<Map<string, Tier>> {
89
+ const tiers = new Map<string, Tier>();
90
+ if (
91
+ !(
92
+ (await hasColumn(sql, "schedules", "created_by_id")) &&
93
+ (await hasColumn(sql, "schedules", "created_tier"))
94
+ )
95
+ )
96
+ return tiers;
97
+ const rows = (await sql`
98
+ SELECT created_by_id AS id, created_tier AS tier FROM schedules
99
+ WHERE created_by_id IS NOT NULL`) as { id: string; tier: Tier }[];
100
+ for (const { id, tier } of rows) {
101
+ const best = tiers.get(id);
102
+ if (!best || tierAtLeast(tier, best)) tiers.set(id, tier);
103
+ }
104
+ return tiers;
105
+ }
106
+
107
+ async function hasColumn(
108
+ sql: SQL,
109
+ table: string,
110
+ column: string,
111
+ ): Promise<boolean> {
112
+ const rows = await sql`
113
+ SELECT 1 FROM information_schema.columns
114
+ WHERE table_schema = current_schema() AND table_name = ${table} AND column_name = ${column}`;
115
+ return rows.length > 0;
116
+ }
117
+
118
+ /** The tier a carried-over author of schedules is seen at: the highest they scheduled at, at most what the rules could give them. */
119
+ function seenTier(
120
+ rules: BackfillRules,
121
+ id: string,
122
+ scheduled: ReadonlyMap<string, Tier>,
123
+ ): Tier | null {
124
+ const tier = scheduled.get(id);
125
+ const ceiling = tier && ceilingOf(rules, id);
126
+ if (!tier || !ceiling) return null;
127
+ return tierAtLeast(tier, ceiling) ? ceiling : tier;
128
+ }
129
+
130
+ /** The ids a table holds, each with the name it last gave them, or none. */
131
+ async function idsIn(
132
+ sql: SQL,
133
+ source: (typeof TABLES)[number],
134
+ ): Promise<{ id: string; name: string | null }[]> {
135
+ if (!(await hasColumn(sql, source.table, source.column))) return [];
136
+ // The identifiers are this module's constants, never input.
137
+ const name = source.name ?? "NULL::text";
138
+ const order = source.order ? `, ${source.order}` : "";
139
+ const hold =
140
+ source.principalHold &&
141
+ (await hasColumn(sql, source.table, source.principalHold.column))
142
+ ? ` OR ${source.principalHold.column} IS DISTINCT FROM ${source.principalHold.held}`
143
+ : "";
144
+ const legacy =
145
+ source.principal && (await hasColumn(sql, source.table, source.principal))
146
+ ? ` AND (${source.principal} IS NULL${hold})`
147
+ : "";
148
+ return (await sql.unsafe(
149
+ `SELECT DISTINCT ON (${source.column}) ${source.column} AS id, ${name} AS name
150
+ FROM ${source.table} WHERE ${source.column} IS NOT NULL AND ${source.column} <> ''${legacy}
151
+ ORDER BY ${source.column}${order}`,
152
+ )) as { id: string; name: string | null }[];
153
+ }
154
+
155
+ /**
156
+ * Makes a principal of the same id for every person 0.8 stored: the configured owners, and each
157
+ * id in `owner_memory`, `schedules`, `conversations`, and `held_actions`, skipping a table or a
158
+ * column the database does not have yet, and a held action that names its principal in
159
+ * `principal_id` for the hold it holds (`principal_held_at`), whose `speaker_id` is an actor id
160
+ * this version stored, and any id a surface
161
+ * knows a linked identity by (its subject, or `<provider>:<subject>`, as `ActorFacts.legacyId`
162
+ * names it): that person has a principal already, and this version wrote the row, so the boot
163
+ * after someone is admitted makes no second principal of their actor id. Each but a configured
164
+ * owner is claimable once, by the person of that id at their first contact
165
+ * (IdentityService.resolve); a configured owner never is, even once the configuration names
166
+ * another owner. 0.8's `remote-mcp` speaker, the owner writing over MCP, gets no principal: it is
167
+ * linked as `legacy:remote-mcp` to the primary owner's, so what it created stays the owner's. One that created
168
+ * schedules, other than a configured owner, is recorded as seen now at the highest tier it
169
+ * scheduled at, capped at the most the rules could give it, so its schedules keep running until
170
+ * `backgroundStaleDays` pass unseen. It only inserts, so it changes no row, and a second run
171
+ * makes nothing; an id an older build wrote since gets its principal at the next run. The owners
172
+ * are named as configured, a schedule's author as the schedule names them, anyone else by their
173
+ * id. `dryRun` counts and writes nothing. The system principal is never made.
174
+ */
175
+ export async function backfillPrincipals(
176
+ sql: SQL,
177
+ rules: BackfillRules,
178
+ options: { dryRun?: boolean } = {},
179
+ ): Promise<BackfillSummary> {
180
+ const owners = rules.owners.flatMap((owner) =>
181
+ owner.principal === undefined
182
+ ? []
183
+ : [{ id: owner.principal, name: owner.name }],
184
+ );
185
+ const names = new Map<string, string | undefined>();
186
+ const sources = {
187
+ config: 0,
188
+ owner_memory: 0,
189
+ schedules: 0,
190
+ conversations: 0,
191
+ held_actions: 0,
192
+ } satisfies Record<BackfillSource, number>;
193
+ // 0.8's remote-mcp speaker was the owner over MCP: it stands for the primary owner.
194
+ const primary = owners[0]?.id;
195
+ let remote = false;
196
+ const add = (source: BackfillSource, id: string, name: string | null) => {
197
+ if (id === SYSTEM_PRINCIPAL) return;
198
+ sources[source]++;
199
+ if (primary !== undefined && id === LEGACY_REMOTE_SPEAKER) {
200
+ remote = true;
201
+ return;
202
+ }
203
+ if (!names.get(id)) names.set(id, name?.trim() || undefined);
204
+ };
205
+ for (const owner of owners) add("config", owner.id, owner.name);
206
+ for (const source of TABLES)
207
+ for (const row of await idsIn(sql, source))
208
+ add(source.table, row.id, row.name);
209
+
210
+ const [table] = await sql`SELECT to_regclass('principals') AS made`;
211
+ const existing = new Set<string>(
212
+ table?.made
213
+ ? (await sql`SELECT id FROM principals`).map(
214
+ (row: { id: string }) => row.id,
215
+ )
216
+ : [],
217
+ );
218
+ // The id a surface knows a linked person by is no 0.8 speaker's: this version wrote it.
219
+ const linked = new Set<string>(
220
+ table?.made
221
+ ? (
222
+ (await sql`SELECT provider, subject FROM principal_identities`) as {
223
+ provider: string;
224
+ subject: string;
225
+ }[]
226
+ ).flatMap((row) => [row.subject, `${row.provider}:${row.subject}`])
227
+ : [],
228
+ );
229
+ const missing = [...names].filter(
230
+ ([id]) => !existing.has(id) && !linked.has(id),
231
+ );
232
+ if (options.dryRun) return { created: missing.length, sources };
233
+ const scheduled = await scheduledTiers(sql);
234
+ const configured = new Set(owners.map((owner) => owner.id));
235
+ const upgraded = new Date();
236
+ let created = 0;
237
+ for (const [id, displayName] of missing) {
238
+ const owner = configured.has(id);
239
+ const seen = owner ? null : seenTier(rules, id, scheduled);
240
+ const rows = await sql`
241
+ INSERT INTO principals (id, display_name, claimable, last_tier, last_seen_at)
242
+ VALUES (${id}, ${displayName ?? id}, ${!owner}, ${seen}, ${seen ? upgraded : null})
243
+ ON CONFLICT (id) DO NOTHING
244
+ RETURNING id`;
245
+ created += rows.length;
246
+ }
247
+ if (remote && primary !== undefined)
248
+ await sql`
249
+ INSERT INTO principal_identities (provider, subject, principal_id, source)
250
+ VALUES (${LEGACY_PROVIDER}, ${LEGACY_REMOTE_SPEAKER}, ${primary}, 'legacy')
251
+ ON CONFLICT (provider, subject) DO NOTHING`;
252
+ // An owner is one by the configuration, never by a claim, even after it names someone else.
253
+ if (configured.size > 0)
254
+ await sql`
255
+ UPDATE principals SET claimable = false
256
+ WHERE claimable AND id IN ${sql([...configured])}`;
257
+ return { created, sources };
258
+ }
259
+
260
+ /**
261
+ * The identity plugin's migrations: its tables once, the claimable column and the plugins' link
262
+ * source once each (apart, so a database an earlier build made the tables on gets them too), then
263
+ * the backfill at every boot, which hands its summary to `report`.
264
+ */
265
+ export function identityMigrations(
266
+ rules: BackfillRules,
267
+ report?: (summary: BackfillSummary) => void,
268
+ ): Migration[] {
269
+ return [
270
+ { name: "principals", up: PRINCIPAL_TABLES },
271
+ { name: "principals-claimable", up: PRINCIPALS_CLAIMABLE },
272
+ { name: "principals-plugin-links", up: PRINCIPAL_PLUGIN_LINKS },
273
+ {
274
+ name: "backfill",
275
+ runs: "every-boot",
276
+ up: async (sql) => {
277
+ const summary = await backfillPrincipals(sql, rules);
278
+ report?.(summary);
279
+ },
280
+ },
281
+ ];
282
+ }