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
@@ -1,5 +1,65 @@
1
+ import type { ChannelContextOptions } from "../contract/channel-context.ts";
1
2
  import { ConfigError } from "../domain/errors.ts";
2
- import { bool, optional, shape, text } from "./schema.ts";
3
+ import {
4
+ bool,
5
+ integer,
6
+ number,
7
+ oneOf,
8
+ optional,
9
+ orOff,
10
+ shape,
11
+ text,
12
+ } from "./schema.ts";
13
+
14
+ /** The Discord bot and its guild, as the top-level `discord` or `discord()` from `pi-roundtable/discord` gives them. */
15
+ export interface DiscordConfig {
16
+ /** The bot token; keep it in `.env`, not in this file. */
17
+ token: string;
18
+ /** The agent server's guild id. */
19
+ guild: string;
20
+ /** The channel the coordinator lives in. */
21
+ entryChannel: string;
22
+ /** The root slash command, without the slash; default the lowercase assistant name. */
23
+ rootCommand?: string;
24
+ /**
25
+ * Whether the owner's conversations have Discord administration tools; default true. `false`
26
+ * leaves the `discord-admin` addon out.
27
+ */
28
+ admin?: boolean;
29
+ /**
30
+ * Text appended as it is to the refusal a non-owner gets from the root command, such as a
31
+ * pointer to the commands anyone may use; include the space or punctuation your language
32
+ * needs before it. Default none.
33
+ */
34
+ refusalHint?: string;
35
+ /**
36
+ * Whose turns in the agents' channels load memory: `"everyone"` (default) lets each speaker
37
+ * read their own, `"owners"` only the owners, so a guest's turn has no memory in its prompt and
38
+ * no memory tools, and never makes claude-bridge refuse the owner afterwards.
39
+ */
40
+ agentMemory?: "everyone" | "owners";
41
+ /**
42
+ * What a turn addressed to the assistant in a server channel reads of the messages posted there
43
+ * since its last post, mentions or not; on with the defaults when left out. `false` turns it off
44
+ * for the agent server and every plugin's claim.
45
+ */
46
+ channelContext?: ChannelContextOptions | false;
47
+ /**
48
+ * The divider the assistant posts in a server channel or thread when its conversation starts
49
+ * over (a new conversation by an owner's command), which ends the window channel context reads;
50
+ * never in a direct message. Default `─── new conversation ───`; `false` posts none.
51
+ */
52
+ freshMarker?: string | false;
53
+ }
54
+
55
+ /** What `discord.channelContext` takes besides `false`. */
56
+ const channelContextShape = shape({
57
+ fetch: optional(integer(1, 100)),
58
+ keep: optional(integer(1, 100)),
59
+ similarity: optional(number(0, 1)),
60
+ messageChars: optional(integer(1, 4000)),
61
+ botMessageChars: optional(integer(1, 4000)),
62
+ });
3
63
 
4
64
  /** The Discord settings, at the top level or in a Discord adapter. */
5
65
  export const discordShape = shape({
@@ -9,6 +69,9 @@ export const discordShape = shape({
9
69
  rootCommand: optional(text),
10
70
  admin: optional(bool),
11
71
  refusalHint: optional(text),
72
+ agentMemory: optional(oneOf<"everyone" | "owners">("everyone", "owners")),
73
+ channelContext: optional(orOff(channelContextShape)),
74
+ freshMarker: optional(orOff(text)),
12
75
  });
13
76
 
14
77
  /** The adapters `adapters` takes, by the name their factory gives them. */
@@ -4,6 +4,7 @@ import type { AgentSeed } from "../agents/agent-rules.ts";
4
4
  import { ConfigError } from "../domain/errors.ts";
5
5
  import type { InterimTextMode } from "../domain/interim.ts";
6
6
  import { isLocale, type Locale } from "../i18n/index.ts";
7
+ import type { AccessRules } from "../identity/access-policy.ts";
7
8
  import type { OwnerIdentity } from "../identity.ts";
8
9
  import {
9
10
  type ModelRef,
@@ -11,11 +12,13 @@ import {
11
12
  THINKING_LEVELS,
12
13
  type ThinkingLevel,
13
14
  } from "../models.ts";
15
+ import type { PerPrincipalLimits } from "../modules/background/personal-target.ts";
14
16
  import type { RoundtablePlugin } from "../plugin.ts";
15
17
  import { PRIMARY_CHARS } from "../runtime/interim-text.ts";
16
18
  import type { ChannelKey } from "../sessions.ts";
17
19
  import type { Tier, TierMembers } from "../speakers.ts";
18
- import { discordShape, liftAdapters } from "./adapters.ts";
20
+ import { type AccessConfig, accessOf, accessShape } from "./access.ts";
21
+ import { type DiscordConfig, discordShape, liftAdapters } from "./adapters.ts";
19
22
  import {
20
23
  bool,
21
24
  guarded,
@@ -30,6 +33,8 @@ import {
30
33
  text,
31
34
  } from "./schema.ts";
32
35
 
36
+ export type { DiscordConfig };
37
+
33
38
  /** How prompts refer back to the owner. */
34
39
  export type Pronouns = "he" | "she" | "they";
35
40
 
@@ -43,29 +48,6 @@ export interface TierConfig {
43
48
  everyone?: boolean;
44
49
  }
45
50
 
46
- /** The Discord bot and its guild, as the top-level `discord` or `discord()` from `pi-roundtable/discord` gives them. */
47
- export interface DiscordConfig {
48
- /** The bot token; keep it in `.env`, not in this file. */
49
- token: string;
50
- /** The agent server's guild id. */
51
- guild: string;
52
- /** The channel the coordinator lives in. */
53
- entryChannel: string;
54
- /** The root slash command, without the slash; default the lowercase assistant name. */
55
- rootCommand?: string;
56
- /**
57
- * Whether the owner's conversations have Discord administration tools; default true. `false`
58
- * leaves the `discord-admin` addon out.
59
- */
60
- admin?: boolean;
61
- /**
62
- * Text appended as it is to the refusal a non-owner gets from the root command, such as a
63
- * pointer to the commands anyone may use; include the space or punctuation your language
64
- * needs before it. Default none.
65
- */
66
- refusalHint?: string;
67
- }
68
-
69
51
  /** A chat network the host talks through, as its adapter's factory makes it; `adapter` names the network. */
70
52
  export interface AdapterConfig {
71
53
  readonly adapter: string;
@@ -77,12 +59,21 @@ export interface DiscordAdapterConfig extends AdapterConfig {
77
59
  readonly discord: DiscordConfig;
78
60
  }
79
61
 
62
+ /** The configuration's `background`: each person's share of the host's background work. */
63
+ export interface BackgroundConfig {
64
+ perPrincipal?: PerPrincipalLimits;
65
+ }
66
+
80
67
  /** What `roundtable.config.ts` gives `defineRoundtable`. */
81
68
  export interface RoundtableConfig {
82
69
  /** The assistant's display name; default "Roundtable". */
83
70
  name?: string;
84
- /** The one owner: the only person who can change everything. */
85
- owner: { id: string; name: string; pronouns?: Pronouns };
71
+ /**
72
+ * The one owner, as 0.8 wrote it: their id, which is both their principal and their Discord
73
+ * user id, their name, and pronouns. Deprecated, going away in 0.10: write `access` instead,
74
+ * never both; `roundtable upgrade` rewrites it.
75
+ */
76
+ owner?: { id: string; name: string; pronouns?: Pronouns };
86
77
  /**
87
78
  * The Discord bot, its guild, and the agent server that lives there. Leave it out for a host
88
79
  * without Discord: no Discord plugins, no agent server or agents, no skills, and no tool that
@@ -113,8 +104,19 @@ export interface RoundtableConfig {
113
104
  locale?: Locale;
114
105
  /** The IANA time zone schedules and stamps use; default "UTC". */
115
106
  timeZone?: string;
116
- /** Who may talk to the agents besides the owner, and what each tier may use. */
107
+ /** Who may talk to the agents besides the owner, by Discord user and role ids, as 0.8 wrote it. Deprecated with `owner`: write `access` instead. */
117
108
  speakers?: { admins?: TierConfig; members?: TierConfig };
109
+ /**
110
+ * Who the host serves: the owners, the first the primary one, and the admins and members by
111
+ * identity (`<provider>:<subject>`), role (`<surface>:role:<name>`), or everyone. Required
112
+ * unless the deprecated `owner` is given, and never with it.
113
+ */
114
+ access?: AccessConfig;
115
+ /**
116
+ * Each person's limits across all their conversations: `perPrincipal.schedules` they may keep and
117
+ * `perPrincipal.delegations` running at once. Unset, only each conversation's, as in 0.8.
118
+ */
119
+ background?: BackgroundConfig;
118
120
  toolTiers?: Record<string, Tier>;
119
121
  /** Prompt files, read once at start; `shared` starts every agent's prompt. */
120
122
  prompts?: { shared: string; guest?: string };
@@ -187,11 +189,13 @@ const isPlugin = (value: unknown): value is RoundtablePlugin =>
187
189
 
188
190
  const schema = shape({
189
191
  name: optional(text),
190
- owner: shape({
191
- id: text,
192
- name: text,
193
- pronouns: optional(oneOf<Pronouns>("he", "she", "they")),
194
- }),
192
+ owner: optional(
193
+ shape({
194
+ id: text,
195
+ name: text,
196
+ pronouns: optional(oneOf<Pronouns>("he", "she", "they")),
197
+ }),
198
+ ),
195
199
  discord: optional(discordShape),
196
200
  database: shape({ url: text }),
197
201
  dataDir: text,
@@ -216,6 +220,17 @@ const schema = shape({
216
220
  speakers: optional(
217
221
  shape({ admins: optional(tierMembers), members: optional(tierMembers) }),
218
222
  ),
223
+ access: optional(accessShape),
224
+ background: optional(
225
+ shape({
226
+ perPrincipal: optional(
227
+ shape({
228
+ schedules: optional(integer(1, 10_000)),
229
+ delegations: optional(integer(1, 1_000)),
230
+ }),
231
+ ),
232
+ }),
233
+ ),
219
234
  toolTiers: optional(record(oneOf<Tier>("owner", "admin", "member"))),
220
235
  prompts: optional(shape({ shared: text, guest: optional(text) })),
221
236
  agents: optional(
@@ -255,12 +270,6 @@ const schema = shape({
255
270
  ),
256
271
  });
257
272
 
258
- const PRONOUNS: Record<Pronouns, OwnerIdentity["pronouns"]> = {
259
- he: { subject: "he", object: "him", possessive: "his" },
260
- she: { subject: "she", object: "her", possessive: "her" },
261
- they: { subject: "they", object: "them", possessive: "their" },
262
- };
263
-
264
273
  /** The configuration with every default filled in. */
265
274
  export interface ResolvedConfig {
266
275
  name: string;
@@ -269,7 +278,17 @@ export interface ResolvedConfig {
269
278
  * with Discord. `discord.rootCommand` when Discord is on, else the lowercase assistant name.
270
279
  */
271
280
  slug: string;
281
+ /**
282
+ * The primary owner, `access.owners[0]` (or the 0.8 `owner`): their principal id, name, and
283
+ * pronouns, and the Discord user id the Discord parts act for until 0.10.
284
+ */
285
+ primaryOwner: OwnerIdentity & { id: string; discordId?: string };
286
+ /** The primary owner as 0.8 knew them, `id` their principal id. Deprecated: read `primaryOwner`. */
272
287
  owner: OwnerIdentity & { id: string };
288
+ /** Who the host serves, the 0.8 `owner` and `speakers` converted when they are what the configuration gives. */
289
+ access: AccessRules;
290
+ /** What the configuration wrote in a deprecated form, which the host logs once. */
291
+ deprecations: string[];
273
292
  /** Undefined on a host without Discord. */
274
293
  discord?: {
275
294
  token: string;
@@ -278,6 +297,10 @@ export interface ResolvedConfig {
278
297
  rootCommand: string;
279
298
  admin: boolean;
280
299
  refusalHint?: string;
300
+ agentMemory?: "everyone" | "owners";
301
+ /** As configured: `{}` when left out, so the defaults apply, or `false`. */
302
+ channelContext: NonNullable<DiscordConfig["channelContext"]>;
303
+ freshMarker?: string | false;
281
304
  };
282
305
  databaseUrl: string;
283
306
  dataDir: string;
@@ -288,7 +311,10 @@ export interface ResolvedConfig {
288
311
  delegation: { model: ModelRef; thinking: ThinkingLevel };
289
312
  locale: Locale;
290
313
  timeZone: string;
314
+ /** The 0.8 `speakers`; empty when the configuration writes `access`. Deprecated: read `access`. */
291
315
  speakers: { admins?: TierMembers; members?: TierMembers };
316
+ /** Each person's limits across their conversations; a limit left out is unset. */
317
+ background: { perPrincipal: PerPrincipalLimits };
292
318
  toolTiers: Record<string, Tier>;
293
319
  prompts: { shared?: string; guest?: string };
294
320
  agents: AgentSeed[];
@@ -378,13 +404,16 @@ export function resolveConfig(input: unknown): ResolvedConfig {
378
404
  const slug =
379
405
  discord?.rootCommand ?? name.toLowerCase().replace(/[^a-z0-9-]+/g, "-");
380
406
  const ops = opsOf(config.ops, discord !== undefined);
407
+ const access = accessOf(config, discord !== undefined);
408
+ const { primaryOwner } = access;
381
409
  return {
382
410
  name,
383
411
  slug,
412
+ ...access,
384
413
  owner: {
385
- id: config.owner.id,
386
- name: config.owner.name,
387
- pronouns: PRONOUNS[config.owner.pronouns ?? "they"],
414
+ id: primaryOwner.id,
415
+ name: primaryOwner.name,
416
+ pronouns: primaryOwner.pronouns,
388
417
  },
389
418
  ...(discord
390
419
  ? {
@@ -397,6 +426,13 @@ export function resolveConfig(input: unknown): ResolvedConfig {
397
426
  ...(discord.refusalHint === undefined
398
427
  ? {}
399
428
  : { refusalHint: discord.refusalHint }),
429
+ ...(discord.agentMemory === undefined
430
+ ? {}
431
+ : { agentMemory: discord.agentMemory }),
432
+ channelContext: discord.channelContext ?? {},
433
+ ...(discord.freshMarker === undefined
434
+ ? {}
435
+ : { freshMarker: discord.freshMarker }),
400
436
  },
401
437
  }
402
438
  : {}),
@@ -423,6 +459,7 @@ export function resolveConfig(input: unknown): ResolvedConfig {
423
459
  ...(config.speakers?.admins ? { admins: config.speakers.admins } : {}),
424
460
  ...(config.speakers?.members ? { members: config.speakers.members } : {}),
425
461
  },
462
+ background: { perPrincipal: { ...config.background?.perPrincipal } },
426
463
  toolTiers: config.toolTiers ?? {},
427
464
  prompts: config.prompts ?? {},
428
465
  agents: config.agents ?? [],
@@ -0,0 +1,53 @@
1
+ // What a surface tells a turn about the channel around the message it answers. Discord fills it
2
+ // (`DISCORD.channelContext`); the types and the way a turn carries it are any surface's.
3
+
4
+ /** How much of a channel a turn reads as its channel context; every member has a default. */
5
+ export interface ChannelContextOptions {
6
+ /** How many messages before the addressed one are fetched; default 50, at most 100 (Discord's page). */
7
+ fetch?: number;
8
+ /** How many of the window's messages are kept, the newest; default 15. */
9
+ keep?: number;
10
+ /**
11
+ * How alike (0 to 1) two consecutive messages of one author must be to count as one, the later
12
+ * kept, such as a message and its corrected repost; default 0.8.
13
+ */
14
+ similarity?: number;
15
+ /** Where a message's text is cut; default 500 characters. */
16
+ messageChars?: number;
17
+ /** Where another bot's message is cut; default 80 characters. */
18
+ botMessageChars?: number;
19
+ }
20
+
21
+ /** One channel message a turn reads around the message that addressed the assistant. */
22
+ export interface ChannelContextMessage {
23
+ id: string;
24
+ authorId: string;
25
+ /** How the author appears in the channel. */
26
+ authorName: string;
27
+ /** Another bot, or an integration such as a webhook that is not the assistant's. */
28
+ bot: boolean;
29
+ /** One of the host's owners. */
30
+ owner: boolean;
31
+ /** The text, with mentions by name and stickers and files as text, cut where the options say. */
32
+ text: string;
33
+ at: Date;
34
+ }
35
+
36
+ /**
37
+ * What was said in a server channel since the assistant last posted there, before the message that
38
+ * addressed it: public channel text, not anyone's private memory, and not from the turn's speaker.
39
+ */
40
+ export interface ChannelContext {
41
+ /** Oldest first; never empty. */
42
+ readonly messages: readonly ChannelContextMessage[];
43
+ /** The delimited block `withChannelContext` appends to a turn's text. */
44
+ readonly text: string;
45
+ }
46
+
47
+ /** The turn's text, then its channel context when there is one, as `withReference` adds a replied-to message. */
48
+ export function withChannelContext(
49
+ text: string,
50
+ context: ChannelContext | undefined,
51
+ ): string {
52
+ return context ? `${text}\n\n${context.text}` : text;
53
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Locale } from "../i18n/index.ts";
2
+ import type { ActorFacts } from "../identity/actor-facts.ts";
2
3
  import type { ChannelKey } from "../sessions.ts";
3
- import type { Tier } from "../speakers.ts";
4
+ import type { Speaker, Tier } from "../speakers.ts";
4
5
 
5
6
  /** A file attached to a chat message, as the surface reports it; nothing is downloaded yet. */
6
7
  export interface AttachmentRef {
@@ -16,6 +17,21 @@ export interface InboundMessage {
16
17
  channel: ChannelKey;
17
18
  /** Unique per surface; keeps the files of different messages apart. */
18
19
  messageId: string;
20
+ /**
21
+ * Who wrote it, as the surface reports them: the router resolves them to `speaker` through the
22
+ * identity service. A surface that leaves it out has it read from `authorId`, `authorName`, and
23
+ * `authorRoleIds` under its own name as the provider, with a `deprecated` warning; that goes
24
+ * away in 1.0.
25
+ */
26
+ actor?: ActorFacts;
27
+ /**
28
+ * Who the author is, with their principal and tier, as the router resolved them before any
29
+ * claim admits the message: undefined when the access rules serve no one by them, and for a bot
30
+ * or an integration, which are not resolved. Only the router sets it; a surface's own value is
31
+ * dropped. A claim that serves only people the host serves answers only when it is set.
32
+ */
33
+ speaker?: Speaker;
34
+ /** The id the surface knows the author by; who they are to the host is `speaker`. */
19
35
  authorId: string;
20
36
  /** How the author appears in the channel, for example a server nickname. */
21
37
  authorName: string;
@@ -77,10 +93,22 @@ export interface BackgroundTarget {
77
93
  name: string;
78
94
  /** How lists such as `/<root> schedule` name it, in the host's locale. */
79
95
  label(locale: Locale): string;
80
- /** Limits of the schedules made for it; absent, nothing may be scheduled for it. */
81
- schedules?: { perChannel: number; promptChars: number; aheadDays: number };
82
- /** Limits of the delegated tasks reporting to it; absent, nothing may be delegated to it. */
83
- delegation?: { maxRunning: number };
96
+ /**
97
+ * Limits of the schedules made for it; absent, nothing may be scheduled for it. `perPrincipal`,
98
+ * when set, holds each person to that many of its schedules across all their conversations.
99
+ */
100
+ schedules?: {
101
+ perChannel: number;
102
+ perPrincipal?: number;
103
+ promptChars: number;
104
+ aheadDays: number;
105
+ };
106
+ /**
107
+ * Limits of the delegated tasks reporting to it; absent, nothing may be delegated to it.
108
+ * `maxRunningPerPrincipal`, when set, holds each person to that many running at once across
109
+ * all their conversations.
110
+ */
111
+ delegation?: { maxRunning: number; maxRunningPerPrincipal?: number };
84
112
  }
85
113
 
86
114
  /** A turn nobody wrote in the channel: a schedule's, or a report of work done elsewhere. */
@@ -91,18 +119,31 @@ export interface BackgroundTurn {
91
119
  * plugin contributes, and a claim skips one it does not serve.
92
120
  */
93
121
  target: string;
94
- author: { id: string; name: string };
95
122
  /**
96
- * The tier the turn runs at, which is its creator's when a person set it up; absent for
97
- * the operator's own automation, which runs at the owner's.
123
+ * Whom the turn is for: the principal who set up the work, and the id and name they are shown
124
+ * by, such as the identity they spoke as. The system principal is the host's alone.
125
+ */
126
+ author: { principalId: string; id: string; name: string };
127
+ /**
128
+ * The tier the turn asks to run at, its creator's when they set it up. It runs at that tier or
129
+ * the principal's own now, whichever is lower.
98
130
  */
99
- tier?: Tier;
131
+ tier: Tier;
100
132
  turnId: string;
101
133
  text: string;
102
134
  /** It delivers a report the owner is waiting for, so it may ask them on cards. */
103
135
  report?: boolean;
136
+ /**
137
+ * Who the turn runs as, set by the router alone once it checked the author: their principal
138
+ * exists and is not disabled, and the tier is capped at theirs. A claim runs the turn as this
139
+ * speaker; one the caller sets is replaced.
140
+ */
141
+ speaker?: Speaker;
104
142
  }
105
143
 
144
+ /** Who a background turn runs as, once checked; or why it may not run now. */
145
+ export type BackgroundRunsAs = { speaker: Speaker } | { skipped: string };
146
+
106
147
  /**
107
148
  * Whose conversation a channel holds, as the claim that owns it names it when `startFresh` says
108
149
  * whose it was: any string, such as "owner" or "study". The host reads none of them, so a plugin
@@ -125,6 +166,12 @@ export type Admission =
125
166
  };
126
167
  /** The turn, run in the channel's queue. */
127
168
  run(): Promise<void>;
169
+ /**
170
+ * Called instead of `run` when the router drops the message the claim admitted, because
171
+ * its author, once recorded, was not who they were when admitted; frees what the claim
172
+ * holds for it.
173
+ */
174
+ dropped?(): void;
128
175
  /** The log line when the turn throws. */
129
176
  failure: string;
130
177
  }
@@ -161,13 +208,32 @@ export interface ChannelClaim {
161
208
  deleteConversation?(channel: ChannelKey): Promise<void>;
162
209
  /** Reports of work started in an owned channel stay in it instead of opening a thread. */
163
210
  postsInPlace?: boolean;
211
+ /**
212
+ * Whether the claim's `background` answers the host's own turns (`SYSTEM_PRINCIPAL`), such as
213
+ * `ops.conversation`'s error reports, in its conversations; true when left out. A claim whose
214
+ * conversations are each private to the person who opened them, such as the web chat's, says
215
+ * false, and a host whose `ops.conversation` names one of them does not start.
216
+ */
217
+ takesSystemReports?: boolean;
164
218
  }
165
219
 
166
220
  /** The conversations of every claimed channel, through the channel queue. */
167
221
  export interface ConversationPort {
168
222
  /** Never rejects; resolves when the message's turn is done or dropped. */
169
223
  handle(message: InboundMessage): Promise<void>;
224
+ /**
225
+ * Runs a turn nobody wrote, as its author's principal at the lower of its tier and theirs; it
226
+ * is skipped, with the reason, when the principal is unknown, disabled, holds no tier, or was
227
+ * last seen too long ago (`access.backgroundStaleDays`), or when the turn names no principal or
228
+ * tier. Never rejects.
229
+ */
170
230
  background(turn: BackgroundTurn): Promise<ScheduledOutcome>;
231
+ /**
232
+ * Who a background turn would run as now, checked as `background` checks it, without running
233
+ * it: for work that must not happen for an author who may not run the turn, such as a
234
+ * schedule's precheck. Throws when the identity service cannot tell.
235
+ */
236
+ runsAs(turn: BackgroundTurn): Promise<BackgroundRunsAs>;
171
237
  /** A contributed background target, read when used; undefined when no plugin contributes it. */
172
238
  target(name: string): BackgroundTarget | undefined;
173
239
  /** Waits for the channel's running turn, then starts its conversation over; says whose it was. */
@@ -182,6 +248,8 @@ export interface ConversationPort {
182
248
  owns(channel: ChannelKey): boolean;
183
249
  /** Whether the claim that owns the channel runs background turns there; false when none owns it. */
184
250
  takesBackground(channel: ChannelKey): boolean;
251
+ /** Whether the claim that owns the channel takes the host's own turns; false when none owns it. */
252
+ takesSystemReports(channel: ChannelKey): boolean;
185
253
  }
186
254
 
187
255
  /** The host's channel queue, as plugins use it. */
@@ -5,8 +5,8 @@ import type {
5
5
  TranscriptEntry,
6
6
  TurnResult,
7
7
  } from "../domain/conversation.ts";
8
- import type { OwnerPrompts } from "../domain/owner-prompts.ts";
9
8
  import type { TurnRequest } from "../domain/ports.ts";
9
+ import type { PromptScope, Prompts } from "../interactions/prompts.ts";
10
10
  import type { Logger } from "../log.ts";
11
11
  import type { ThinkingSetting } from "../models.ts";
12
12
  import type { HostEnv, LinkedSessions } from "../plugin.ts";
@@ -51,11 +51,18 @@ export interface AgentRuntime {
51
51
  startFresh(conversation: ChannelKey): Promise<void>;
52
52
  /** Like `startFresh`, but removes the conversation and every archive of it for good; called between turns. */
53
53
  deleteConversation(conversation: ChannelKey): Promise<void>;
54
- /** The conversation's held actions known in memory since startup. */
54
+ /**
55
+ * The conversation's held actions known in memory: those of a turn run since startup, and those
56
+ * `heldActions` or `recentTranscript` restored from the store for it. After a restart, ask one of
57
+ * those first.
58
+ */
55
59
  pendingConfirmation(
56
60
  conversation: ChannelKey,
57
61
  ): PendingConfirmation | undefined;
58
- /** The conversation's held actions, restored from the store after a restart. */
62
+ /**
63
+ * The conversation's held actions, restored from the store after a restart, so that
64
+ * `pendingConfirmation` knows them too, as `recentTranscript` does. Expired ones count as none.
65
+ */
59
66
  heldActions(
60
67
  conversation: ChannelKey,
61
68
  ): Promise<PendingConfirmation | undefined>;
@@ -96,6 +103,13 @@ export interface AgentSessions {
96
103
  modelOf(name: string): { model: string; thinking: ThinkingSetting };
97
104
  /** The channel the scope's turns run in: the group's for a seat in one, else the agent's own. */
98
105
  turnChannel(scope: AgentTurnScope): ChannelKey;
106
+ /**
107
+ * Whose turns in an agent's conversation load memory: `"everyone"` (the default) lets each
108
+ * speaker read their own, `"owners"` only the owners, so a speaker below the owner tier gets no
109
+ * memory in their prompt and no memory tools, and their turn never carries private memory into
110
+ * the conversation's history.
111
+ */
112
+ memory?: "everyone" | "owners";
99
113
  }
100
114
 
101
115
  /** Where a runtime keeps each conversation's held actions, so they survive a restart. */
@@ -124,13 +138,15 @@ export interface RuntimeDeps {
124
138
  /** What each tool needs; plugin tools are added when the host links, so ask at use time. */
125
139
  toolTiers: ToolTiers;
126
140
  /**
127
- * The owner's approval and question prompts in a conversation, from its chat surface; undefined
128
- * when the surface has none, and the action then waits for the owner's next message.
141
+ * The approval and question prompts in a conversation for a turn's scope, made with
142
+ * `promptScope(speaker, visibility)`, from its chat surface; undefined when the surface has
143
+ * none, and the action then waits for a message that approves it. A `Speaker`, the 0.8 form,
144
+ * is read as theirs in a shared conversation, with a deprecation warning, until 1.0.
129
145
  */
130
146
  prompts(
131
147
  conversation: ChannelKey,
132
- speaker?: Speaker,
133
- ): OwnerPrompts | undefined;
148
+ scope?: PromptScope | Speaker,
149
+ ): Prompts | undefined;
134
150
  /**
135
151
  * The agent server's per-agent settings, for agent turns. The runtime is built before the agent
136
152
  * server, so read this when a turn runs, not in the factory; it stays undefined on a host without
@@ -1,8 +1,8 @@
1
1
  import type { OutboundReply } from "../domain/conversation.ts";
2
2
  import type { InterimPosts } from "../domain/interim.ts";
3
- import type { OwnerPrompts } from "../domain/owner-prompts.ts";
4
3
  import type { TurnProgress } from "../domain/progress.ts";
5
4
  import { PluginError } from "../errors.ts";
5
+ import type { PromptScope, Prompts } from "../interactions/prompts.ts";
6
6
  import type { ChannelKey } from "../sessions.ts";
7
7
  import type { Speaker } from "../speakers.ts";
8
8
  import type { InboundMessage } from "./channels.ts";
@@ -58,6 +58,13 @@ export interface ChatSurface {
58
58
  * `conversations.stop(channel)` when used. Absent = none shown.
59
59
  */
60
60
  showStop?(channel: ChannelKey): () => void;
61
+ /**
62
+ * Marks in the channel, publicly, that its conversation just started over, so what a reader of
63
+ * the channel's history would take for the conversation's context ends there; called after the
64
+ * claim's `startFresh` succeeded. A rejection is logged and the restart still counts. Absent =
65
+ * nothing is posted.
66
+ */
67
+ markFresh?(channel: ChannelKey): Promise<void>;
61
68
  /** Adds or removes the bot's reaction on a message; failures are logged, never thrown. */
62
69
  react?(channel: ChannelKey, messageId: string, emoji: string): Promise<void>;
63
70
  unreact?(
@@ -66,10 +73,12 @@ export interface ChatSurface {
66
73
  emoji: string,
67
74
  ): Promise<void>;
68
75
  /**
69
- * How the owner approves held actions or answers ask_user in a running turn; undefined, or
70
- * absent, = the action is held until the owner's next message.
76
+ * How those the scope names approve held actions or answer ask_user in a running turn: the
77
+ * scope's speaker, and in a shared conversation the owners; see `Prompts` and `PromptScope`.
78
+ * Without a scope the prompts are the owners'. Undefined, or absent, = the action is held
79
+ * until a message approves it.
71
80
  */
72
- prompts?(channel: ChannelKey, speaker?: Speaker): OwnerPrompts | undefined;
81
+ prompts?(channel: ChannelKey, scope?: PromptScope): Prompts | undefined;
73
82
  /**
74
83
  * Where a running turn posts the text it writes before its final answer: ordinary messages it
75
84
  * may edit in place. Absent, or undefined, = only the final reply is posted.
@@ -98,8 +107,15 @@ export interface SurfacePort {
98
107
  showStop(channel: ChannelKey): () => void;
99
108
  react(channel: ChannelKey, messageId: string, emoji: string): Promise<void>;
100
109
  unreact(channel: ChannelKey, messageId: string, emoji: string): Promise<void>;
101
- /** The owner's prompts in the channel; undefined when its surface has none. */
102
- prompts(channel: ChannelKey, speaker?: Speaker): OwnerPrompts | undefined;
110
+ /**
111
+ * The prompts in the channel for the scope's turn; undefined when its surface has none. A
112
+ * `Speaker`, the 0.8 form, is read as theirs in a shared conversation, with a deprecation
113
+ * warning, until 1.0.
114
+ */
115
+ prompts(
116
+ channel: ChannelKey,
117
+ scope?: PromptScope | Speaker,
118
+ ): Prompts | undefined;
103
119
  /** The channel's interim posts; undefined when its surface has none. */
104
120
  interim(channel: ChannelKey): InterimPosts | undefined;
105
121
  /**
@@ -34,6 +34,20 @@ export interface ConversationRegistry {
34
34
  get(key: ChannelKey): Promise<ConversationRecord | undefined>;
35
35
  /** One principal's conversations, or every one; the most recently active first. */
36
36
  list(filter?: { principal?: string }): Promise<ConversationRecord[]>;
37
+ /**
38
+ * Only for migrating a conversation recorded before 0.9, when every turn run without a
39
+ * visibility was recorded `shared` with no principal: makes such a conversation private to
40
+ * `principalId`, as the plugin that hands over what 0.8 left behind decides, such as remote-mcp
41
+ * giving 0.8's sessions to the primary owner. A conversation that is private or names a
42
+ * principal never changes, so adopting is safe to repeat and to race: one principal wins.
43
+ * Returns what is stored, or undefined when the key is unknown. Adopting a conversation 0.9 or
44
+ * later recorded is outside this contract: today it changes one recorded shared like any other,
45
+ * and a later version may refuse it.
46
+ */
47
+ adopt(
48
+ key: ChannelKey,
49
+ principalId: string,
50
+ ): Promise<ConversationRecord | undefined>;
37
51
  /** Names a conversation, or clears its name; undefined when the key is unknown. */
38
52
  setTitle(
39
53
  key: ChannelKey,