pi-roundtable 0.7.19 → 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 (177) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/README.md +72 -15
  3. package/README.zh-TW.md +65 -13
  4. package/docs/migrating-0.8.md +132 -0
  5. package/docs/migration-0.9.md +247 -0
  6. package/docs/plugins.md +853 -81
  7. package/examples/channel-context.test.ts +88 -0
  8. package/examples/channel-context.ts +62 -0
  9. package/examples/discord-adapter.test.ts +42 -0
  10. package/examples/discord-adapter.ts +26 -0
  11. package/examples/echo-runtime.test.ts +1 -0
  12. package/examples/echo-runtime.ts +1 -1
  13. package/examples/fake-surface.test.ts +69 -6
  14. package/examples/fake-surface.ts +73 -7
  15. package/examples/headless.test.ts +72 -0
  16. package/examples/headless.ts +35 -0
  17. package/examples/http.test.ts +31 -1
  18. package/examples/http.ts +39 -0
  19. package/examples/prechecks.test.ts +1 -0
  20. package/examples/principals.test.ts +60 -0
  21. package/examples/principals.ts +48 -0
  22. package/examples/prompt.test.ts +1 -1
  23. package/examples/study-room.test.ts +29 -1
  24. package/examples/study-room.ts +16 -19
  25. package/examples/support-desk.test.ts +3 -1
  26. package/examples/tools.test.ts +6 -1
  27. package/package.json +7 -3
  28. package/src/cli/checks/database.ts +11 -0
  29. package/src/cli/checks/identity.ts +225 -0
  30. package/src/cli/checks/model.ts +21 -3
  31. package/src/cli/cli.ts +130 -5
  32. package/src/cli/config-edit.ts +83 -13
  33. package/src/cli/doctor.ts +28 -5
  34. package/src/cli/init.ts +9 -1
  35. package/src/cli/line-diff.ts +72 -0
  36. package/src/cli/principal.ts +412 -0
  37. package/src/cli/project.ts +33 -2
  38. package/src/cli/report.ts +12 -3
  39. package/src/cli/templates.ts +46 -14
  40. package/src/cli/upgrade-rewrite.ts +102 -0
  41. package/src/cli/upgrade-source.ts +430 -0
  42. package/src/cli/upgrade.ts +176 -0
  43. package/src/core/agents/agent-claim.ts +56 -34
  44. package/src/core/agents/agent-team-fixture.ts +6 -0
  45. package/src/core/agents/team-turns.ts +10 -1
  46. package/src/core/builtin/agent-server.ts +38 -101
  47. package/src/core/builtin/discord-admin.ts +4 -1
  48. package/src/core/builtin/discord.ts +71 -13
  49. package/src/core/builtin/modules.ts +197 -43
  50. package/src/core/builtin/stores.ts +40 -21
  51. package/src/core/config/access.ts +229 -0
  52. package/src/core/config/adapters.ts +120 -0
  53. package/src/core/config/config.ts +228 -94
  54. package/src/core/contract/channel-context.ts +53 -0
  55. package/src/core/contract/channels.ts +81 -9
  56. package/src/core/contract/providers.ts +3 -2
  57. package/src/core/contract/runtime.ts +31 -11
  58. package/src/core/contract/surface.ts +35 -6
  59. package/src/core/conversations/conversation-registry.ts +56 -0
  60. package/src/core/conversations/conversation-store.ts +126 -0
  61. package/src/core/conversations/conversations-plugin.ts +29 -0
  62. package/src/core/define-roundtable.ts +157 -57
  63. package/src/core/discord/agent-panel.ts +2 -2
  64. package/src/core/discord/card-audience.ts +148 -0
  65. package/src/core/discord/channel-context.ts +348 -0
  66. package/src/core/discord/connection.ts +15 -3
  67. package/src/core/discord/direct-channel.ts +54 -0
  68. package/src/core/discord/discord-adapter.ts +9 -0
  69. package/src/core/discord/discord-owners.ts +133 -0
  70. package/src/core/discord/discord-surface.ts +69 -14
  71. package/src/core/discord/fresh-marker.ts +24 -0
  72. package/src/core/discord/inbound-message.ts +21 -7
  73. package/src/core/discord/interaction-module.ts +11 -1
  74. package/src/core/discord/owner-cards-fixture.ts +80 -0
  75. package/src/core/discord/owner-cards.ts +58 -86
  76. package/src/core/discord/owner-command.ts +27 -13
  77. package/src/core/discord/stop-button.ts +4 -4
  78. package/src/core/domain/conversation.ts +10 -0
  79. package/src/core/domain/errors.ts +5 -0
  80. package/src/core/domain/ports.ts +33 -2
  81. package/src/core/domain/progress.ts +10 -0
  82. package/src/core/events.ts +1 -0
  83. package/src/core/host.ts +32 -10
  84. package/src/core/http/listeners.ts +174 -21
  85. package/src/core/http/websocket.ts +316 -0
  86. package/src/core/i18n/cards.ts +11 -9
  87. package/src/core/identity/access-policy.ts +227 -0
  88. package/src/core/identity/actor-facts.ts +65 -0
  89. package/src/core/identity/caching-principal-store.ts +111 -0
  90. package/src/core/identity/config-owner.ts +145 -0
  91. package/src/core/identity/contact.ts +27 -0
  92. package/src/core/identity/identity-plugin.ts +56 -0
  93. package/src/core/identity/identity-schema.ts +282 -0
  94. package/src/core/identity/identity-service.ts +489 -0
  95. package/src/core/identity/identity-view.ts +77 -0
  96. package/src/core/identity/plugin-identities.ts +109 -0
  97. package/src/core/identity/principal-reads.ts +38 -0
  98. package/src/core/identity/principal-store.ts +478 -0
  99. package/src/core/identity/principal-tables.ts +53 -0
  100. package/src/core/identity/seen-throttle.ts +57 -0
  101. package/src/core/identity/ulid.ts +36 -0
  102. package/src/core/identity.ts +34 -5
  103. package/src/core/interactions/prompts.ts +127 -0
  104. package/src/core/modules/background/background-turns.ts +56 -17
  105. package/src/core/modules/background/personal-target.ts +50 -0
  106. package/src/core/modules/delegation/delegate.ts +28 -14
  107. package/src/core/modules/delegation/delegator.ts +23 -7
  108. package/src/core/modules/memory/owner-memory.ts +123 -50
  109. package/src/core/modules/notify/notify.ts +53 -13
  110. package/src/core/modules/notify/session-notify.ts +110 -0
  111. package/src/core/modules/schedules/prechecks.ts +8 -2
  112. package/src/core/modules/schedules/schedule-prechecks.ts +4 -1
  113. package/src/core/modules/schedules/schedule-store.ts +51 -20
  114. package/src/core/modules/schedules/schedule-tools.ts +122 -12
  115. package/src/core/modules/schedules/scheduler.ts +43 -2
  116. package/src/core/modules/schedules/schedules.ts +43 -14
  117. package/src/core/ops/error-reporter.ts +24 -12
  118. package/src/core/plugin.ts +62 -0
  119. package/src/core/presence/direct-channels.ts +104 -0
  120. package/src/core/registry/contributions.ts +63 -3
  121. package/src/core/registry/providers.ts +1 -1
  122. package/src/core/routing/channel-router.ts +226 -17
  123. package/src/core/routing/conversation-turns.ts +85 -1
  124. package/src/core/routing/surface-port.ts +35 -2
  125. package/src/core/routing/system-turns.ts +29 -0
  126. package/src/core/runtime/bridge-guard.ts +225 -0
  127. package/src/core/runtime/compaction-tiers.ts +5 -1
  128. package/src/core/runtime/conversation-sessions.ts +124 -12
  129. package/src/core/runtime/extensions/ask-user.ts +1 -1
  130. package/src/core/runtime/extensions/confirmation-gate.ts +32 -10
  131. package/src/core/runtime/extensions/private-memory.ts +447 -0
  132. package/src/core/runtime/extensions/self-compact-guard.ts +11 -0
  133. package/src/core/runtime/pending-confirmation-store.ts +67 -4
  134. package/src/core/runtime/pi-agent-runtime.ts +143 -135
  135. package/src/core/runtime/preflight-tools.ts +47 -0
  136. package/src/core/runtime/prompt-slot.ts +6 -6
  137. package/src/core/runtime/reader-history.ts +129 -0
  138. package/src/core/runtime/runtime-plugin.ts +205 -0
  139. package/src/core/runtime/runtime-types.ts +40 -12
  140. package/src/core/runtime/session-conversation.ts +143 -0
  141. package/src/core/runtime/session-factory.ts +115 -15
  142. package/src/core/runtime/session-scope.ts +78 -0
  143. package/src/core/runtime/steerable-run.ts +9 -0
  144. package/src/core/runtime/tool-call-scope.ts +43 -0
  145. package/src/core/runtime/turn-answer.ts +30 -1
  146. package/src/core/runtime/turn-progress.ts +116 -0
  147. package/src/core/services.ts +42 -2
  148. package/src/core/sessions.ts +30 -0
  149. package/src/core/shared/unix-server.ts +35 -5
  150. package/src/core/speakers.ts +21 -19
  151. package/src/core/testing/background.ts +31 -0
  152. package/src/core/testing/fixture-database.ts +55 -0
  153. package/src/core/testing/map-identity.ts +76 -0
  154. package/src/core/testing/module-sessions.ts +134 -0
  155. package/src/core/testing/modules.ts +149 -38
  156. package/src/core/testing/owner.ts +17 -0
  157. package/src/core/testing/surface-contract.ts +460 -0
  158. package/src/core/testing/test-host.ts +100 -19
  159. package/src/core/testing/tool-set.ts +32 -0
  160. package/src/core/tool-set.snapshot.json +4 -4
  161. package/src/core/tool-tiers.ts +57 -3
  162. package/src/discord/index.ts +10 -0
  163. package/src/index.ts +65 -8
  164. package/src/kit/compaction.ts +13 -0
  165. package/src/kit/index.ts +10 -0
  166. package/src/kit/support.ts +1 -1
  167. package/src/testing.ts +96 -21
  168. package/templates/.env.example +1 -1
  169. package/templates/adapters/web/.env.example +24 -0
  170. package/templates/adapters/web/README.md +22 -0
  171. package/templates/adapters/web/package.json.tmpl +26 -0
  172. package/templates/adapters/web/persona/assistant.md +2 -0
  173. package/templates/adapters/web/roundtable.config.ts +59 -0
  174. package/templates/package.json.tmpl +1 -0
  175. package/templates/plugins/self-compact.ts +11 -0
  176. package/templates/roundtable.config.ts +14 -2
  177. package/src/core/domain/owner-prompts.ts +0 -45
@@ -1,3 +1,5 @@
1
+ import type { ChannelKey } from "../domain/conversation.ts";
2
+ import type { RoundtablePlugin } from "../plugin.ts";
1
3
  import { ASK_USER_TOOL } from "../runtime/extensions/ask-user.ts";
2
4
  import { COMPACT_TOOL } from "../runtime/extensions/self-compact-guard.ts";
3
5
  import { AGENTS } from "../services.ts";
@@ -16,6 +18,36 @@ export const seat: AgentTurnScope = {
16
18
  group: "war-room",
17
19
  };
18
20
 
21
+ /**
22
+ * The conversations the recorded sessions live in, served as a host serves them: the owner's
23
+ * direct messages by an app's claim, such as one that answers the owner there, and the agent's
24
+ * channel by the agent server; both answer background turns, so their sessions schedule and
25
+ * delegate. Give it to the test host the tool set is captured on.
26
+ */
27
+ export function servedConversations(
28
+ ownerChannel: ChannelKey,
29
+ ): RoundtablePlugin {
30
+ const served = new Set<ChannelKey>([ownerChannel, scout.home, seat.home]);
31
+ return {
32
+ name: "served-conversations",
33
+ setup: () => ({
34
+ channels: [
35
+ {
36
+ name: "served-conversations",
37
+ priority: 0,
38
+ owns: (channel) => served.has(channel),
39
+ admit: () => undefined,
40
+ background: async () => ({
41
+ status: "skipped",
42
+ reason: "the tool set's host runs no background turns",
43
+ }),
44
+ startFresh: async () => "owner",
45
+ },
46
+ ],
47
+ }),
48
+ };
49
+ }
50
+
19
51
  /** Tools the core's own extensions and pi-web-access register, which no fake session here loads. */
20
52
  const NAMED_ELSEWHERE = [
21
53
  ASK_USER_TOOL,
@@ -37,7 +37,7 @@
37
37
  "memory_add",
38
38
  "memory_remove",
39
39
  "memory_search",
40
- "notify_owner",
40
+ "notify",
41
41
  "schedule_cancel",
42
42
  "schedule_create",
43
43
  "schedule_list",
@@ -92,7 +92,7 @@
92
92
  "memory_remove",
93
93
  "memory_search",
94
94
  "message_agent",
95
- "notify_owner",
95
+ "notify",
96
96
  "schedule_cancel",
97
97
  "schedule_create",
98
98
  "schedule_list",
@@ -179,7 +179,7 @@
179
179
  "memory_add",
180
180
  "memory_remove",
181
181
  "memory_search",
182
- "notify_owner",
182
+ "notify",
183
183
  "schedule_cancel",
184
184
  "schedule_create",
185
185
  "schedule_list",
@@ -272,7 +272,7 @@
272
272
  "memory_remove": "member",
273
273
  "memory_search": "member",
274
274
  "message_agent": "member",
275
- "notify_owner": "owner",
275
+ "notify": "owner",
276
276
  "read": "owner",
277
277
  "read_attachment": "member",
278
278
  "schedule_cancel": "admin",
@@ -1,4 +1,5 @@
1
1
  import { PluginError } from "./errors.ts";
2
+ import type { Logger } from "./log.ts";
2
3
  import { ASK_USER_TOOL } from "./runtime/extensions/ask-user.ts";
3
4
  import { COMPACT_TOOL } from "./runtime/extensions/self-compact-guard.ts";
4
5
  import { type Tier, tierAtLeast } from "./speakers.ts";
@@ -27,6 +28,40 @@ export const CORE_TOOL_TIERS: Readonly<Record<string, Tier>> = set("member", [
27
28
  "read_attachment",
28
29
  ]);
29
30
 
31
+ /**
32
+ * Tools renamed since 0.8, by their old name: until 1.0 the old name still names the tool in a
33
+ * selection, a task's `exclude`, a plugin's `requiredTools`, and the operator's and plugins'
34
+ * `toolTiers`, where the new name's own setting wins.
35
+ */
36
+ export const RENAMED_TOOLS: Readonly<Record<string, string>> = {
37
+ notify_owner: "notify",
38
+ };
39
+
40
+ const warnedAliases = new WeakMap<Logger, Set<string>>();
41
+
42
+ /** Each logger warns once for an old name used in tiers or selections. */
43
+ function warnAliases(tools: readonly string[], logger: Logger): void {
44
+ const warned = warnedAliases.get(logger) ?? new Set<string>();
45
+ warnedAliases.set(logger, warned);
46
+ for (const tool of tools) {
47
+ const renamed = RENAMED_TOOLS[tool];
48
+ if (!renamed || warned.has(tool)) continue;
49
+ warned.add(tool);
50
+ logger.warn(
51
+ `deprecated: ${tool} is now ${renamed}; update selections and toolTiers before 1.0`,
52
+ );
53
+ }
54
+ }
55
+
56
+ /** The tools named, each by its current name and once, in order. */
57
+ export function currentToolNames(
58
+ tools: readonly string[],
59
+ logger?: Logger,
60
+ ): string[] {
61
+ if (logger) warnAliases(tools, logger);
62
+ return [...new Set(tools.map((tool) => RENAMED_TOOLS[tool] ?? tool))];
63
+ }
64
+
30
65
  /**
31
66
  * What each tool needs: the operator's setting first, then the tier its plugin declared, then
32
67
  * the core's default, and the owner for a tool nobody named. Plugins declare theirs when the
@@ -38,7 +73,15 @@ export class ToolTierTable implements ToolTiers {
38
73
  readonly #core = new Map(Object.entries(CORE_TOOL_TIERS));
39
74
 
40
75
  constructor(operator: Readonly<Record<string, Tier>> = {}) {
41
- this.#operator = new Map(Object.entries(operator));
76
+ const entries = Object.entries(operator);
77
+ // An old name first, so the new name's own setting overrides it.
78
+ this.#operator = new Map([
79
+ ...entries.flatMap(([tool, tier]) => {
80
+ const renamed = RENAMED_TOOLS[tool];
81
+ return renamed ? [[renamed, tier] as const] : [];
82
+ }),
83
+ ...entries,
84
+ ]);
42
85
  }
43
86
 
44
87
  /**
@@ -46,7 +89,16 @@ export class ToolTierTable implements ToolTiers {
46
89
  * plugin declaring its own tool again, as when the host retries its start, replaces it.
47
90
  */
48
91
  declare(plugin: string, tiers: Readonly<Record<string, Tier>>): void {
49
- for (const [tool, tier] of Object.entries(tiers)) {
92
+ const entries = Object.entries(tiers);
93
+ // An old name is the new name's, declared first so the new name's own tier wins.
94
+ const named = [
95
+ ...entries.flatMap(([tool, tier]) => {
96
+ const renamed = RENAMED_TOOLS[tool];
97
+ return renamed ? [[renamed, tier] as const] : [];
98
+ }),
99
+ ...entries.filter(([tool]) => !RENAMED_TOOLS[tool]),
100
+ ];
101
+ for (const [tool, tier] of named) {
50
102
  const other = this.#declared.get(tool);
51
103
  if (other && other.plugin !== plugin)
52
104
  throw new PluginError(
@@ -73,11 +125,13 @@ export class ToolTierTable implements ToolTiers {
73
125
  /** The core's tiers with the operator's on top; plugins add theirs through `declare`. */
74
126
  export function toolTiers(
75
127
  operator: Readonly<Record<string, Tier>> = {},
128
+ logger?: Logger,
76
129
  ): ToolTierTable {
130
+ if (logger) warnAliases(Object.keys(operator), logger);
77
131
  return new ToolTierTable(operator);
78
132
  }
79
133
 
80
- /** The tools a tier may use, in order; a turn nobody spoke in has the owner's tier. */
134
+ /** The tools a tier may use, in order; a turn's tier is its speaker's, and a turn without one is refused. */
81
135
  export function toolsForTier(
82
136
  tools: readonly string[],
83
137
  tier: Tier,
@@ -3,12 +3,20 @@
3
3
 
4
4
  export type { DiscordServices } from "../core/builtin/discord.ts";
5
5
  export { DISCORD } from "../core/builtin/discord.ts";
6
+ export type { DiscordAdapterConfig } from "../core/config/config.ts";
7
+ export type {
8
+ ChannelContext,
9
+ ChannelContextMessage,
10
+ ChannelContextOptions,
11
+ } from "../core/contract/channel-context.ts";
12
+ export { withChannelContext } from "../core/contract/channel-context.ts";
6
13
  export type { AgentPanelMessage } from "../core/discord/agent-commands.ts";
7
14
  export type {
8
15
  AgentPanel,
9
16
  AgentPanelOptions,
10
17
  } from "../core/discord/agent-panel.ts";
11
18
  export { agentPanel } from "../core/discord/agent-panel.ts";
19
+ export { CHANNEL_CONTEXT_DEFAULTS } from "../core/discord/channel-context.ts";
12
20
  export type { ManagedChannel } from "../core/discord/channel-executor.ts";
13
21
  export {
14
22
  fetchManagedChannel,
@@ -33,6 +41,8 @@ export type {
33
41
  ChannelInfo,
34
42
  DiscordConnection,
35
43
  } from "../core/discord/connection.ts";
44
+ export { discord } from "../core/discord/discord-adapter.ts";
45
+ export type { DiscordActor } from "../core/discord/discord-owners.ts";
36
46
  export type {
37
47
  CommandGuard,
38
48
  CommandRegistrar,
package/src/index.ts CHANGED
@@ -21,6 +21,14 @@ export {
21
21
  AGENT_TEAM_SERVICE,
22
22
  } from "./core/builtin/agent-server.ts";
23
23
  export type {
24
+ AccessConfig,
25
+ AccessOwnerConfig,
26
+ AccessTierConfig,
27
+ } from "./core/config/access.ts";
28
+ export type {
29
+ AdapterConfig,
30
+ BackgroundConfig,
31
+ DiscordConfig,
24
32
  Pronouns,
25
33
  RoundtableConfig,
26
34
  TierConfig,
@@ -28,6 +36,7 @@ export type {
28
36
  export type {
29
37
  Admission,
30
38
  AttachmentRef,
39
+ BackgroundRunsAs,
31
40
  BackgroundTarget,
32
41
  BackgroundTurn,
33
42
  ChannelClaim,
@@ -57,6 +66,12 @@ export type { ServiceKey, Services } from "./core/contract/services.ts";
57
66
  export { serviceKey } from "./core/contract/services.ts";
58
67
  export type { ChatSurface, SurfacePort } from "./core/contract/surface.ts";
59
68
  export { channelKey, parseChannelKey } from "./core/contract/surface.ts";
69
+ export type {
70
+ ConversationRecord,
71
+ ConversationRegistration,
72
+ ConversationRegistry,
73
+ ConversationVisibility,
74
+ } from "./core/conversations/conversation-registry.ts";
60
75
  export type { Migration, MigrationReport } from "./core/db/migrations.ts";
61
76
  export { migrateDatabase } from "./core/db/migrations.ts";
62
77
  export type { ToolContribution, ToolSpec, ToolTurn } from "./core/define.ts";
@@ -85,6 +100,7 @@ export {
85
100
  AgentRunError,
86
101
  ConfigError,
87
102
  DelegationError,
103
+ IdentityError,
88
104
  MemoryError,
89
105
  ScheduleError,
90
106
  } from "./core/domain/errors.ts";
@@ -93,14 +109,8 @@ export type {
93
109
  InterimPosts,
94
110
  InterimTextMode,
95
111
  } from "./core/domain/interim.ts";
96
- export type {
97
- Approval,
98
- AskOption,
99
- OwnerAnswer,
100
- OwnerPrompts,
101
- OwnerQuestion,
102
- } from "./core/domain/owner-prompts.ts";
103
- export type { TurnRequest } from "./core/domain/ports.ts";
112
+ export type { TurnConversation, TurnRequest } from "./core/domain/ports.ts";
113
+ export type { TurnProgress } from "./core/domain/progress.ts";
104
114
  export type { DrainOptions } from "./core/drain.ts";
105
115
  export {
106
116
  JudgeError,
@@ -117,10 +127,46 @@ export type {
117
127
  ListenerAddress,
118
128
  ListenerConfig,
119
129
  } from "./core/http/listeners.ts";
130
+ export type {
131
+ RouteSocket,
132
+ WebSocketAccept,
133
+ WebSocketRoute,
134
+ WebSocketSendResult,
135
+ } from "./core/http/websocket.ts";
120
136
  export type { Locale } from "./core/i18n/index.ts";
137
+ export type {
138
+ AccessOwner,
139
+ AccessRules,
140
+ AccessTier,
141
+ } from "./core/identity/access-policy.ts";
142
+ export type { ActorFacts } from "./core/identity/actor-facts.ts";
143
+ export type { IdentityService } from "./core/identity/identity-service.ts";
144
+ export type {
145
+ IdentityLink,
146
+ IdentityRef,
147
+ LinkSource,
148
+ Principal,
149
+ RoleGrant,
150
+ RoleSource,
151
+ } from "./core/identity/principal-store.ts";
152
+ export { SYSTEM_PRINCIPAL } from "./core/identity/principal-store.ts";
153
+ export type {
154
+ Approval,
155
+ AskOption,
156
+ OwnerAnswer,
157
+ OwnerPrompts,
158
+ OwnerQuestion,
159
+ PromptScope,
160
+ Prompts,
161
+ } from "./core/interactions/prompts.ts";
162
+ export { promptScope } from "./core/interactions/prompts.ts";
121
163
  export type { JudgeModel } from "./core/judging/model-judge.ts";
122
164
  export type { LogEntry, LogFn, Logger } from "./core/log.ts";
123
165
  export type { ThinkingLevel, ThinkingSetting } from "./core/models.ts";
166
+ export {
167
+ PERSONAL_TARGET,
168
+ type PerPrincipalLimits,
169
+ } from "./core/modules/background/personal-target.ts";
124
170
  export type {
125
171
  DelegationJob,
126
172
  DelegationOutcome,
@@ -169,6 +215,7 @@ export type {
169
215
  LinkedSessions,
170
216
  Persona,
171
217
  PluginContext,
218
+ PluginIdentity,
172
219
  PromptSection,
173
220
  PromptTurn,
174
221
  RoundtablePlugin,
@@ -177,7 +224,13 @@ export type {
177
224
  ServiceStartOutcome,
178
225
  TurnEndEvent,
179
226
  TurnEvent,
227
+ TurnProgressEvent,
180
228
  } from "./core/plugin.ts";
229
+ export type {
230
+ DirectChannelProvider,
231
+ DirectChannels,
232
+ DirectReach,
233
+ } from "./core/presence/direct-channels.ts";
181
234
  export {
182
235
  attachReplyFile,
183
236
  REPLY_FILE_LIMITS,
@@ -205,9 +258,12 @@ export type {
205
258
  export {
206
259
  AGENTS,
207
260
  BACKGROUND_TURNS,
261
+ CONVERSATIONS,
208
262
  DELEGATION,
263
+ IDENTITY,
209
264
  MEMORY,
210
265
  PRECHECKS,
266
+ RUNTIME,
211
267
  SCHEDULES,
212
268
  SKILLS,
213
269
  } from "./core/services.ts";
@@ -215,6 +271,7 @@ export type {
215
271
  AgentTurnScope,
216
272
  ChannelKey,
217
273
  SessionContext,
274
+ SessionConversation,
218
275
  SessionPlan,
219
276
  SessionTool,
220
277
  SessionToolSnapshot,
@@ -12,3 +12,16 @@ export {
12
12
  HARD_COMPACT_TOKENS,
13
13
  SOFT_COMPACT_TOKENS,
14
14
  } from "../core/runtime/compaction-tiers.ts";
15
+ export type { MemoryView } from "../core/runtime/extensions/private-memory.ts";
16
+ export {
17
+ hidesPrivateExchange,
18
+ memoryProjection,
19
+ privateCompaction,
20
+ summaryProjection,
21
+ } from "../core/runtime/extensions/private-memory.ts";
22
+ export {
23
+ bridgeHistoryHidesMemory,
24
+ carriesMemory,
25
+ MEMORY_TURN_ENTRY,
26
+ recordMemoryTurn,
27
+ } from "../core/runtime/reader-history.ts";
package/src/kit/index.ts CHANGED
@@ -18,13 +18,22 @@ export type {
18
18
  CompactionEngine,
19
19
  CompactionHistory,
20
20
  LatestCompaction,
21
+ MemoryView,
21
22
  } from "./compaction.ts";
22
23
  export {
24
+ bridgeHistoryHidesMemory,
23
25
  COMPACT_HEADROOM_TOKENS,
24
26
  CompactionTiers,
27
+ carriesMemory,
25
28
  compactionEngine,
26
29
  HARD_COMPACT_TOKENS,
30
+ hidesPrivateExchange,
31
+ MEMORY_TURN_ENTRY,
32
+ memoryProjection,
33
+ privateCompaction,
34
+ recordMemoryTurn,
27
35
  SOFT_COMPACT_TOKENS,
36
+ summaryProjection,
28
37
  } from "./compaction.ts";
29
38
  export { scrubDiagnostic } from "./diagnostics.ts";
30
39
  export type {
@@ -122,6 +131,7 @@ export type {
122
131
  DashboardBoard,
123
132
  DelegationWorker,
124
133
  GroupMessage,
134
+ Notifier,
125
135
  OwnerNotifier,
126
136
  SpeakerFacts,
127
137
  SpeakerPolicy,
@@ -13,7 +13,7 @@ export type {
13
13
  export type { Backlog, GroupMessage } from "../core/agents/agent-rules.ts";
14
14
  export type { AgentOps } from "../core/agents/agent-tools.ts";
15
15
  export type { ThreadHost } from "../core/discord/dispatch-threads.ts";
16
- export type { OwnerNotifier } from "../core/domain/ports.ts";
16
+ export type { Notifier, OwnerNotifier } from "../core/domain/ports.ts";
17
17
  export type { ThinkingPicker } from "../core/models.ts";
18
18
  export type { DelegationWorker } from "../core/modules/delegation/delegator.ts";
19
19
  export type { AgentChannelLookup } from "../core/modules/schedules/schedules.ts";
package/src/testing.ts CHANGED
@@ -3,8 +3,8 @@ import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
5
  import type { SQL } from "bun";
6
- import { agentClaim, OWNER_TARGET } from "./core/agents/agent-claim.ts";
7
- import { DISCORD } from "./core/builtin/discord.ts";
6
+ import { agentClaim } from "./core/agents/agent-claim.ts";
7
+ import { DISCORD, type DiscordServices } from "./core/builtin/discord.ts";
8
8
  import type {
9
9
  ConversationPort,
10
10
  InboundMessage,
@@ -24,7 +24,10 @@ import type {
24
24
  CommandRegistrar,
25
25
  InteractionContribution,
26
26
  } from "./core/discord/interaction-module.ts";
27
- import { OwnerGuard, ownerRootCommand } from "./core/discord/owner-command.ts";
27
+ import {
28
+ commandGuard,
29
+ ownerRootCommand,
30
+ } from "./core/discord/owner-command.ts";
28
31
  import type {
29
32
  PendingConfirmation,
30
33
  ReplyFile,
@@ -32,10 +35,12 @@ import type {
32
35
  import { NotLinkedError, PluginError } from "./core/errors.ts";
33
36
  import { EventBus } from "./core/events.ts";
34
37
  import type { HoldCheck } from "./core/holds.ts";
38
+ import { contactsOf } from "./core/identity/identity-view.ts";
35
39
  import type { OwnerIdentity } from "./core/identity.ts";
36
40
  import { ConfirmationJudge } from "./core/judging/confirmation-judge.ts";
37
41
  import { silentLogger } from "./core/log.ts";
38
42
  import { ConversationBackgroundTurns } from "./core/modules/background/background-turns.ts";
43
+ import { PERSONAL_TARGET } from "./core/modules/background/personal-target.ts";
39
44
  import type {
40
45
  Contribution,
41
46
  HostEnv,
@@ -44,6 +49,7 @@ import type {
44
49
  RoundtablePlugin,
45
50
  TurnEndEvent,
46
51
  TurnEvent,
52
+ TurnProgressEvent,
47
53
  } from "./core/plugin.ts";
48
54
  import {
49
55
  collectContributions,
@@ -63,9 +69,17 @@ import {
63
69
  conversationTurns,
64
70
  } from "./core/routing/conversation-turns.ts";
65
71
  import { surfacePort } from "./core/routing/surface-port.ts";
66
- import { AGENTS, type AgentServer, BACKGROUND_TURNS } from "./core/services.ts";
72
+ import {
73
+ AGENTS,
74
+ type AgentServer,
75
+ BACKGROUND_TURNS,
76
+ CONVERSATIONS,
77
+ IDENTITY,
78
+ RUNTIME,
79
+ } from "./core/services.ts";
67
80
  import type { ChannelKey, SessionContext } from "./core/sessions.ts";
68
- import { type Speaker, speakerPolicy } from "./core/speakers.ts";
81
+ import { type Speaker, THE_SPEAKER } from "./core/speakers.ts";
82
+ import { mapIdentity } from "./core/testing/map-identity.ts";
69
83
  import { type ToolTierTable, toolTiers } from "./core/tool-tiers.ts";
70
84
 
71
85
  // Fixtures for plugin tests.
@@ -99,6 +113,16 @@ export type {
99
113
  RecordingLogger,
100
114
  } from "./core/testing/recording-logger.ts";
101
115
  export { recordingLogger } from "./core/testing/recording-logger.ts";
116
+ export type {
117
+ SurfaceContractAnswerer,
118
+ SurfaceContractFailure,
119
+ SurfaceContractSubject,
120
+ SurfaceObservation,
121
+ } from "./core/testing/surface-contract.ts";
122
+ export {
123
+ checkSurfaceContract,
124
+ describeSurfaceContract,
125
+ } from "./core/testing/surface-contract.ts";
102
126
  export type { TestHost, TestHostOptions } from "./core/testing/test-host.ts";
103
127
  export { testHost } from "./core/testing/test-host.ts";
104
128
  export type { FakeThreadHost } from "./core/testing/thread-host.ts";
@@ -139,20 +163,30 @@ export interface FakeDiscord {
139
163
  /**
140
164
  * A `DISCORD` service for a plugin that adds slash commands, without a Discord connection: the
141
165
  * plugin's `commands.add` calls are recorded, and `compose()` shows the tree Discord would get.
142
- * `rootCommand` is the root command's name, `roundtable` by default. Only `commands` and `guard` are given; reading another member throws.
166
+ * `rootCommand` is the root command's name, `roundtable` by default. `channelContext` is what
167
+ * `DISCORD.channelContext` answers, by default no context, as on a host with it off. Only
168
+ * `commands`, `guard`, and `channelContext` are given; reading another member throws.
143
169
  */
144
170
  export function fakeDiscord(
145
- options: { ownerId?: string; rootCommand?: string } = {},
171
+ options: {
172
+ ownerId?: string;
173
+ rootCommand?: string;
174
+ channelContext?: DiscordServices["channelContext"];
175
+ } = {},
146
176
  ): FakeDiscord {
147
177
  const collection = new CommandCollection();
148
178
  const { rootCommand = "roundtable" } = options;
149
- const guard = new OwnerGuard(
150
- options.ownerId ?? "owner",
151
- silentLogger(),
152
- rootCommand,
153
- );
179
+ const guard = commandGuard({
180
+ ownerId: options.ownerId ?? "owner",
181
+ root: rootCommand,
182
+ logger: silentLogger(),
183
+ });
154
184
  return {
155
- service: servicePair(DISCORD, { commands: collection.registrar, guard }),
185
+ service: servicePair(DISCORD, {
186
+ commands: collection.registrar,
187
+ guard,
188
+ channelContext: options.channelContext ?? (async () => undefined),
189
+ }),
156
190
  commands: collection.registrar,
157
191
  guard,
158
192
  added: () => collection.added(),
@@ -177,13 +211,17 @@ export interface TestPluginOptions {
177
211
  surfaces?: readonly ChatSurface[];
178
212
  /**
179
213
  * What the plugin reads from `context.services`, one `servicePair(KEY, { ... })` per service.
180
- * `servicePair(AGENTS, { runtime })` is the runtime that `context.turns` runs turns on; without
181
- * it, a plugin that fills the `runtime` slot gets the runtime its provider builds. A service
214
+ * `servicePair(RUNTIME, runtime)` is the runtime that `context.turns` runs turns on, given
215
+ * whole rather than member by member; `servicePair(AGENTS, { runtime })` still works when no
216
+ * `RUNTIME` is given. Without either, a plugin that fills the `runtime` slot gets the runtime its
217
+ * provider builds, under both keys. A service
182
218
  * not given here reads as absent to `find`, and `get` names this option, except for what the
183
219
  * host supplies anyway: `BACKGROUND_TURNS`, the real background turns over the test's router,
184
220
  * and, once `AGENTS` is given, its `approvals` (the real confirmation judge over
185
221
  * `providers.judge`, when the test gives a judge). Giving `AGENTS` a `team` adds the agent
186
- * server's own claim to the router, answering as the host's would for the `owner`.
222
+ * server's own claim to the router, answering as the host's would for the `owner`. The router
223
+ * resolves who wrote each message through a given `IDENTITY`; without one only the `owner` is
224
+ * anyone, at the owner tier, their principal their id.
187
225
  */
188
226
  services?: readonly ServicePair[];
189
227
  /**
@@ -315,6 +353,7 @@ export async function testPlugin(
315
353
  (router ?? unlinked("conversations")).handle(message),
316
354
  background: async (turn) =>
317
355
  (router ?? unlinked("conversations")).background(turn),
356
+ runsAs: async (turn) => (router ?? unlinked("conversations")).runsAs(turn),
318
357
  target: (name) => (router ?? unlinked("conversations")).target(name),
319
358
  startFresh: async (channel) =>
320
359
  (router ?? unlinked("conversations")).startFresh(channel),
@@ -323,6 +362,11 @@ export async function testPlugin(
323
362
  stop: (channel) => (router ?? unlinked("conversations")).stop(channel),
324
363
  postsInPlace: (channel) =>
325
364
  (router ?? unlinked("conversations")).postsInPlace(channel),
365
+ owns: (channel) => (router ?? unlinked("conversations")).owns(channel),
366
+ takesBackground: (channel) =>
367
+ (router ?? unlinked("conversations")).takesBackground(channel),
368
+ takesSystemReports: (channel) =>
369
+ (router ?? unlinked("conversations")).takesSystemReports(channel),
326
370
  ...options.conversations,
327
371
  };
328
372
  const surfaces = surfacePort(() =>
@@ -339,6 +383,8 @@ export async function testPlugin(
339
383
  events.push({ name: "turnEnded", turn });
340
384
  bus.sink.turnEnded(turn);
341
385
  },
386
+ // Delivered to the plugin's handlers, not recorded: a turn reports many.
387
+ turnProgress: (event: TurnProgressEvent) => bus.sink.turnProgress?.(event),
342
388
  changed: () => {
343
389
  events.push({ name: "changed" });
344
390
  bus.sink.changed();
@@ -350,7 +396,14 @@ export async function testPlugin(
350
396
  linked: () => {
351
397
  if (!linked) unlinked("turns");
352
398
  },
353
- runtime: () => services.get(AGENTS).runtime,
399
+ runtime: () => {
400
+ const given = services.find(RUNTIME);
401
+ if (given) return given;
402
+ const server = services.find(AGENTS);
403
+ return server ? server.runtime : services.get(RUNTIME);
404
+ },
405
+ // A test that gives `CONVERSATIONS` sees each turn record its conversation.
406
+ registry: () => services.find(CONVERSATIONS),
354
407
  surfaces,
355
408
  events: sink,
356
409
  selection: () => (linked ?? unlinked("sessions")).agentSelection(),
@@ -391,6 +444,8 @@ export async function testPlugin(
391
444
  };
392
445
  runtime = providers.runtime(deps);
393
446
  // The provider's runtime serves `context.turns` unless the test gave another.
447
+ if (!given.has(RUNTIME.id))
448
+ given.set(RUNTIME.id, { key: RUNTIME, given: runtime });
394
449
  if (!agents || !("runtime" in agents.given))
395
450
  given.set(AGENTS.id, {
396
451
  key: AGENTS,
@@ -411,8 +466,12 @@ export async function testPlugin(
411
466
  }),
412
467
  } satisfies Partial<AgentServer>,
413
468
  });
469
+ // A runtime is given whole: a class's private members do not survive the partial service's proxy.
414
470
  for (const { key, given: members } of given.values())
415
- services.preset(key, partialService(key, members));
471
+ services.preset(
472
+ key,
473
+ key.id === RUNTIME.id ? members : partialService(key, members),
474
+ );
416
475
  // The turns nobody wrote run through the same router, unless the test gave others or the plugin provides them.
417
476
  if (
418
477
  !given.has(BACKGROUND_TURNS.id) &&
@@ -427,7 +486,7 @@ export async function testPlugin(
427
486
  }),
428
487
  );
429
488
  services.checkRequires();
430
- const context: Omit<PluginContext, "services"> = {
489
+ const context: Omit<PluginContext, "services" | "directChannels"> = {
431
490
  logger,
432
491
  env,
433
492
  sessions: () => linked ?? unlinked("sessions"),
@@ -460,13 +519,17 @@ export async function testPlugin(
460
519
  const team = given.get(AGENTS.id)?.given;
461
520
  const agentServer = team && "team" in team ? services.get(AGENTS) : undefined;
462
521
  const attachmentDir = mkdtempSync(join(tmpdir(), "roundtable-test-plugin-"));
522
+ const identity = services.find(IDENTITY);
523
+ // Who wrote a message, and whom a background turn runs as: by the given IDENTITY, else the owner alone, at the owner tier.
524
+ const people = mapIdentity({ owners: [owner.id] });
463
525
  router = new ChannelRouter({
526
+ contacts: identity ? contactsOf(identity) : people,
527
+ principals: identity ?? people,
464
528
  claims: [
465
529
  ...(agentServer
466
530
  ? [
467
531
  agentClaim({
468
532
  owner,
469
- speakers: speakerPolicy({ owners: [owner.id] }),
470
533
  // SAFETY: the claim reads the concrete team's members (guildId, owns, answerOwner, answerGroup, answerBackground, startFresh), which a test that gives a team for it supplies.
471
534
  team: agentServer.team as unknown as Parameters<
472
535
  typeof agentClaim
@@ -478,6 +541,13 @@ export async function testPlugin(
478
541
  surface: surfaces,
479
542
  attachmentDir: () => attachmentDir,
480
543
  logger,
544
+ // As on a host: the given DISCORD's channel context, when the test gives one.
545
+ channelContext: async (message) =>
546
+ (
547
+ given.get(DISCORD.id)?.given as
548
+ | Partial<DiscordServices>
549
+ | undefined
550
+ )?.channelContext?.(message),
481
551
  }),
482
552
  ]
483
553
  : []),
@@ -485,7 +555,9 @@ export async function testPlugin(
485
555
  ],
486
556
  targets: (name) =>
487
557
  registry.backgroundTargets.find((t) => t.name === name) ??
488
- (agentServer && name === OWNER_TARGET.name ? OWNER_TARGET : undefined),
558
+ (agentServer && name === PERSONAL_TARGET.name
559
+ ? PERSONAL_TARGET
560
+ : undefined),
489
561
  queue,
490
562
  logger,
491
563
  ...(options.forwardJoinMs === undefined
@@ -550,6 +622,9 @@ export async function testPlugin(
550
622
  homeChannel: channel,
551
623
  turnChannel: channel,
552
624
  compaction: { wrap: (factory) => factory },
625
+ conversation: { visibility: "shared" },
626
+ addressee: THE_SPEAKER,
627
+ memory: "speaker",
553
628
  speaker: () => runOptions?.speaker,
554
629
  runTask: async () => {
555
630
  throw new Error("test session cannot run tasks");
@@ -7,7 +7,7 @@ DISCORD_TOKEN=
7
7
  DISCORD_GUILD_ID=
8
8
  DISCORD_ENTRY_CHANNEL_ID=
9
9
 
10
- # You: the one owner who can change everything (right-click your name, Copy User ID).
10
+ # You: the owner, who can change everything; OWNER_ID is your Discord user id (right-click your name, Copy User ID).
11
11
  OWNER_ID=
12
12
  OWNER_NAME=
13
13