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.
- package/CHANGELOG.md +140 -0
- package/README.md +72 -15
- package/README.zh-TW.md +65 -13
- package/docs/migrating-0.8.md +132 -0
- package/docs/migration-0.9.md +247 -0
- package/docs/plugins.md +853 -81
- package/examples/channel-context.test.ts +88 -0
- package/examples/channel-context.ts +62 -0
- package/examples/discord-adapter.test.ts +42 -0
- package/examples/discord-adapter.ts +26 -0
- package/examples/echo-runtime.test.ts +1 -0
- package/examples/echo-runtime.ts +1 -1
- package/examples/fake-surface.test.ts +69 -6
- package/examples/fake-surface.ts +73 -7
- package/examples/headless.test.ts +72 -0
- package/examples/headless.ts +35 -0
- package/examples/http.test.ts +31 -1
- package/examples/http.ts +39 -0
- package/examples/prechecks.test.ts +1 -0
- package/examples/principals.test.ts +60 -0
- package/examples/principals.ts +48 -0
- package/examples/prompt.test.ts +1 -1
- package/examples/study-room.test.ts +29 -1
- package/examples/study-room.ts +16 -19
- package/examples/support-desk.test.ts +3 -1
- package/examples/tools.test.ts +6 -1
- package/package.json +7 -3
- package/src/cli/checks/database.ts +11 -0
- package/src/cli/checks/identity.ts +225 -0
- package/src/cli/checks/model.ts +21 -3
- package/src/cli/cli.ts +130 -5
- package/src/cli/config-edit.ts +83 -13
- package/src/cli/doctor.ts +28 -5
- package/src/cli/init.ts +9 -1
- package/src/cli/line-diff.ts +72 -0
- package/src/cli/principal.ts +412 -0
- package/src/cli/project.ts +33 -2
- package/src/cli/report.ts +12 -3
- package/src/cli/templates.ts +46 -14
- package/src/cli/upgrade-rewrite.ts +102 -0
- package/src/cli/upgrade-source.ts +430 -0
- package/src/cli/upgrade.ts +176 -0
- package/src/core/agents/agent-claim.ts +56 -34
- package/src/core/agents/agent-team-fixture.ts +6 -0
- package/src/core/agents/team-turns.ts +10 -1
- package/src/core/builtin/agent-server.ts +38 -101
- package/src/core/builtin/discord-admin.ts +4 -1
- package/src/core/builtin/discord.ts +71 -13
- package/src/core/builtin/modules.ts +197 -43
- package/src/core/builtin/stores.ts +40 -21
- package/src/core/config/access.ts +229 -0
- package/src/core/config/adapters.ts +120 -0
- package/src/core/config/config.ts +228 -94
- package/src/core/contract/channel-context.ts +53 -0
- package/src/core/contract/channels.ts +81 -9
- package/src/core/contract/providers.ts +3 -2
- package/src/core/contract/runtime.ts +31 -11
- package/src/core/contract/surface.ts +35 -6
- package/src/core/conversations/conversation-registry.ts +56 -0
- package/src/core/conversations/conversation-store.ts +126 -0
- package/src/core/conversations/conversations-plugin.ts +29 -0
- package/src/core/define-roundtable.ts +157 -57
- package/src/core/discord/agent-panel.ts +2 -2
- package/src/core/discord/card-audience.ts +148 -0
- package/src/core/discord/channel-context.ts +348 -0
- package/src/core/discord/connection.ts +15 -3
- package/src/core/discord/direct-channel.ts +54 -0
- package/src/core/discord/discord-adapter.ts +9 -0
- package/src/core/discord/discord-owners.ts +133 -0
- package/src/core/discord/discord-surface.ts +69 -14
- package/src/core/discord/fresh-marker.ts +24 -0
- package/src/core/discord/inbound-message.ts +21 -7
- package/src/core/discord/interaction-module.ts +11 -1
- package/src/core/discord/owner-cards-fixture.ts +80 -0
- package/src/core/discord/owner-cards.ts +58 -86
- package/src/core/discord/owner-command.ts +27 -13
- package/src/core/discord/stop-button.ts +4 -4
- package/src/core/domain/conversation.ts +10 -0
- package/src/core/domain/errors.ts +5 -0
- package/src/core/domain/ports.ts +33 -2
- package/src/core/domain/progress.ts +10 -0
- package/src/core/events.ts +1 -0
- package/src/core/host.ts +32 -10
- package/src/core/http/listeners.ts +174 -21
- package/src/core/http/websocket.ts +316 -0
- package/src/core/i18n/cards.ts +11 -9
- package/src/core/identity/access-policy.ts +227 -0
- package/src/core/identity/actor-facts.ts +65 -0
- package/src/core/identity/caching-principal-store.ts +111 -0
- package/src/core/identity/config-owner.ts +145 -0
- package/src/core/identity/contact.ts +27 -0
- package/src/core/identity/identity-plugin.ts +56 -0
- package/src/core/identity/identity-schema.ts +282 -0
- package/src/core/identity/identity-service.ts +489 -0
- package/src/core/identity/identity-view.ts +77 -0
- package/src/core/identity/plugin-identities.ts +109 -0
- package/src/core/identity/principal-reads.ts +38 -0
- package/src/core/identity/principal-store.ts +478 -0
- package/src/core/identity/principal-tables.ts +53 -0
- package/src/core/identity/seen-throttle.ts +57 -0
- package/src/core/identity/ulid.ts +36 -0
- package/src/core/identity.ts +34 -5
- package/src/core/interactions/prompts.ts +127 -0
- package/src/core/modules/background/background-turns.ts +56 -17
- package/src/core/modules/background/personal-target.ts +50 -0
- package/src/core/modules/delegation/delegate.ts +28 -14
- package/src/core/modules/delegation/delegator.ts +23 -7
- package/src/core/modules/memory/owner-memory.ts +123 -50
- package/src/core/modules/notify/notify.ts +53 -13
- package/src/core/modules/notify/session-notify.ts +110 -0
- package/src/core/modules/schedules/prechecks.ts +8 -2
- package/src/core/modules/schedules/schedule-prechecks.ts +4 -1
- package/src/core/modules/schedules/schedule-store.ts +51 -20
- package/src/core/modules/schedules/schedule-tools.ts +122 -12
- package/src/core/modules/schedules/scheduler.ts +43 -2
- package/src/core/modules/schedules/schedules.ts +43 -14
- package/src/core/ops/error-reporter.ts +24 -12
- package/src/core/plugin.ts +62 -0
- package/src/core/presence/direct-channels.ts +104 -0
- package/src/core/registry/contributions.ts +63 -3
- package/src/core/registry/providers.ts +1 -1
- package/src/core/routing/channel-router.ts +226 -17
- package/src/core/routing/conversation-turns.ts +85 -1
- package/src/core/routing/surface-port.ts +35 -2
- package/src/core/routing/system-turns.ts +29 -0
- package/src/core/runtime/bridge-guard.ts +225 -0
- package/src/core/runtime/compaction-tiers.ts +5 -1
- package/src/core/runtime/conversation-sessions.ts +124 -12
- package/src/core/runtime/extensions/ask-user.ts +1 -1
- package/src/core/runtime/extensions/confirmation-gate.ts +32 -10
- package/src/core/runtime/extensions/private-memory.ts +447 -0
- package/src/core/runtime/extensions/self-compact-guard.ts +11 -0
- package/src/core/runtime/pending-confirmation-store.ts +67 -4
- package/src/core/runtime/pi-agent-runtime.ts +143 -135
- package/src/core/runtime/preflight-tools.ts +47 -0
- package/src/core/runtime/prompt-slot.ts +6 -6
- package/src/core/runtime/reader-history.ts +129 -0
- package/src/core/runtime/runtime-plugin.ts +205 -0
- package/src/core/runtime/runtime-types.ts +40 -12
- package/src/core/runtime/session-conversation.ts +143 -0
- package/src/core/runtime/session-factory.ts +115 -15
- package/src/core/runtime/session-scope.ts +78 -0
- package/src/core/runtime/steerable-run.ts +9 -0
- package/src/core/runtime/tool-call-scope.ts +43 -0
- package/src/core/runtime/turn-answer.ts +30 -1
- package/src/core/runtime/turn-progress.ts +116 -0
- package/src/core/services.ts +42 -2
- package/src/core/sessions.ts +30 -0
- package/src/core/shared/unix-server.ts +35 -5
- package/src/core/speakers.ts +21 -19
- package/src/core/testing/background.ts +31 -0
- package/src/core/testing/fixture-database.ts +55 -0
- package/src/core/testing/map-identity.ts +76 -0
- package/src/core/testing/module-sessions.ts +134 -0
- package/src/core/testing/modules.ts +149 -38
- package/src/core/testing/owner.ts +17 -0
- package/src/core/testing/surface-contract.ts +460 -0
- package/src/core/testing/test-host.ts +100 -19
- package/src/core/testing/tool-set.ts +32 -0
- package/src/core/tool-set.snapshot.json +4 -4
- package/src/core/tool-tiers.ts +57 -3
- package/src/discord/index.ts +10 -0
- package/src/index.ts +65 -8
- package/src/kit/compaction.ts +13 -0
- package/src/kit/index.ts +10 -0
- package/src/kit/support.ts +1 -1
- package/src/testing.ts +96 -21
- package/templates/.env.example +1 -1
- package/templates/adapters/web/.env.example +24 -0
- package/templates/adapters/web/README.md +22 -0
- package/templates/adapters/web/package.json.tmpl +26 -0
- package/templates/adapters/web/persona/assistant.md +2 -0
- package/templates/adapters/web/roundtable.config.ts +59 -0
- package/templates/package.json.tmpl +1 -0
- package/templates/plugins/self-compact.ts +11 -0
- package/templates/roundtable.config.ts +14 -2
- package/src/core/domain/owner-prompts.ts +0 -45
|
@@ -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
|
-
/**
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
*
|
|
97
|
-
* the
|
|
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
|
|
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. */
|
|
@@ -178,6 +244,12 @@ export interface ConversationPort {
|
|
|
178
244
|
stop(channel: ChannelKey): boolean;
|
|
179
245
|
/** Whether reports of work started in the channel stay in it, by its owner's policy. */
|
|
180
246
|
postsInPlace(channel: ChannelKey): boolean;
|
|
247
|
+
/** Whether a claim owns the channel, so its messages and background turns reach a conversation. */
|
|
248
|
+
owns(channel: ChannelKey): boolean;
|
|
249
|
+
/** Whether the claim that owns the channel runs background turns there; false when none owns it. */
|
|
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;
|
|
181
253
|
}
|
|
182
254
|
|
|
183
255
|
/** The host's channel queue, as plugins use it. */
|
|
@@ -68,8 +68,9 @@ export interface Providers {
|
|
|
68
68
|
images: ImageDrawer;
|
|
69
69
|
/**
|
|
70
70
|
* Builds the runtime that runs the agent server's conversations and every turn run through
|
|
71
|
-
* `context.turns`, in place of the Pi runtime. The default refuses: the
|
|
72
|
-
* Pi runtime itself when no plugin fills this slot
|
|
71
|
+
* `context.turns`, in place of the Pi runtime. The default refuses: the host's runtime plugin
|
|
72
|
+
* builds the Pi runtime itself when no plugin fills this slot. Read the running runtime from
|
|
73
|
+
* `services.get(RUNTIME)` rather than calling this.
|
|
73
74
|
*/
|
|
74
75
|
runtime: RuntimeFactory;
|
|
75
76
|
}
|
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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. */
|
|
@@ -109,7 +123,7 @@ export interface HeldActionStore {
|
|
|
109
123
|
): Promise<void>;
|
|
110
124
|
}
|
|
111
125
|
|
|
112
|
-
/** What the
|
|
126
|
+
/** What the host's runtime plugin hands a runtime provider when it builds the runtime. */
|
|
113
127
|
export interface RuntimeDeps {
|
|
114
128
|
logger: Logger;
|
|
115
129
|
/** The host's locale and time zone. */
|
|
@@ -124,20 +138,26 @@ 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
|
|
128
|
-
*
|
|
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
|
-
|
|
133
|
-
):
|
|
134
|
-
/**
|
|
135
|
-
|
|
148
|
+
scope?: PromptScope | Speaker,
|
|
149
|
+
): Prompts | undefined;
|
|
150
|
+
/**
|
|
151
|
+
* The agent server's per-agent settings, for agent turns. The runtime is built before the agent
|
|
152
|
+
* server, so read this when a turn runs, not in the factory; it stays undefined on a host without
|
|
153
|
+
* the agent server, where no agent turn runs.
|
|
154
|
+
*/
|
|
155
|
+
readonly agents?: AgentSessions;
|
|
136
156
|
/** Where held actions persist across restarts. */
|
|
137
157
|
confirmations: HeldActionStore;
|
|
138
158
|
/** The host's judge, resolved from the `judge` slot. */
|
|
139
159
|
judge: Judge;
|
|
140
160
|
}
|
|
141
161
|
|
|
142
|
-
/** Builds the runtime, once, when the
|
|
162
|
+
/** Builds the runtime, once, when the host's runtime plugin sets up. */
|
|
143
163
|
export type RuntimeFactory = (deps: RuntimeDeps) => AgentRuntime;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { OutboundReply } from "../domain/conversation.ts";
|
|
2
2
|
import type { InterimPosts } from "../domain/interim.ts";
|
|
3
|
-
import type {
|
|
3
|
+
import type { TurnProgress } from "../domain/progress.ts";
|
|
4
4
|
import { PluginError } from "../errors.ts";
|
|
5
|
+
import type { PromptScope, Prompts } from "../interactions/prompts.ts";
|
|
5
6
|
import type { ChannelKey } from "../sessions.ts";
|
|
6
7
|
import type { Speaker } from "../speakers.ts";
|
|
7
8
|
import type { InboundMessage } from "./channels.ts";
|
|
@@ -57,6 +58,13 @@ export interface ChatSurface {
|
|
|
57
58
|
* `conversations.stop(channel)` when used. Absent = none shown.
|
|
58
59
|
*/
|
|
59
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>;
|
|
60
68
|
/** Adds or removes the bot's reaction on a message; failures are logged, never thrown. */
|
|
61
69
|
react?(channel: ChannelKey, messageId: string, emoji: string): Promise<void>;
|
|
62
70
|
unreact?(
|
|
@@ -65,15 +73,23 @@ export interface ChatSurface {
|
|
|
65
73
|
emoji: string,
|
|
66
74
|
): Promise<void>;
|
|
67
75
|
/**
|
|
68
|
-
* How the
|
|
69
|
-
*
|
|
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.
|
|
70
80
|
*/
|
|
71
|
-
prompts?(channel: ChannelKey,
|
|
81
|
+
prompts?(channel: ChannelKey, scope?: PromptScope): Prompts | undefined;
|
|
72
82
|
/**
|
|
73
83
|
* Where a running turn posts the text it writes before its final answer: ordinary messages it
|
|
74
84
|
* may edit in place. Absent, or undefined, = only the final reply is posted.
|
|
75
85
|
*/
|
|
76
86
|
interim?(channel: ChannelKey): InterimPosts | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* Shows what a running turn writes and which tools it runs, as it goes, such as a live preview
|
|
89
|
+
* in a web chat. Called between the turn's start and its reply; a rejection is logged and the
|
|
90
|
+
* turn goes on. Absent = the surface shows only the final reply.
|
|
91
|
+
*/
|
|
92
|
+
progress?(channel: ChannelKey, event: TurnProgress): Promise<void> | void;
|
|
77
93
|
}
|
|
78
94
|
|
|
79
95
|
/**
|
|
@@ -91,8 +107,21 @@ export interface SurfacePort {
|
|
|
91
107
|
showStop(channel: ChannelKey): () => void;
|
|
92
108
|
react(channel: ChannelKey, messageId: string, emoji: string): Promise<void>;
|
|
93
109
|
unreact(channel: ChannelKey, messageId: string, emoji: string): Promise<void>;
|
|
94
|
-
/**
|
|
95
|
-
|
|
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;
|
|
96
119
|
/** The channel's interim posts; undefined when its surface has none. */
|
|
97
120
|
interim(channel: ChannelKey): InterimPosts | undefined;
|
|
121
|
+
/**
|
|
122
|
+
* Shows a running turn's progress; a no-op when no surface serves the channel or its surface
|
|
123
|
+
* shows none. The host's port always has it; it is optional so a port written before 0.8 still
|
|
124
|
+
* type-checks, and a turn run over one without it shows no progress.
|
|
125
|
+
*/
|
|
126
|
+
progress?(channel: ChannelKey, event: TurnProgress): Promise<void>;
|
|
98
127
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ChannelKey } from "../sessions.ts";
|
|
2
|
+
|
|
3
|
+
/** Who may take part: `private` belongs to one principal, `shared` to whoever its claim admits. */
|
|
4
|
+
export type ConversationVisibility = "private" | "shared";
|
|
5
|
+
|
|
6
|
+
/** What the first turn of a conversation records about it. */
|
|
7
|
+
export interface ConversationRegistration {
|
|
8
|
+
key: ChannelKey;
|
|
9
|
+
/** The conversation's kind, such as "study"; the persona its session runs. */
|
|
10
|
+
kind: string;
|
|
11
|
+
visibility: ConversationVisibility;
|
|
12
|
+
/** Whose conversation it is: the speaker's id, for a private one. */
|
|
13
|
+
principalId?: string;
|
|
14
|
+
/** A name to list it by. */
|
|
15
|
+
title?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A conversation the host knows: who it belongs to and when it was last active. */
|
|
19
|
+
export interface ConversationRecord extends ConversationRegistration {
|
|
20
|
+
/** The prefix of its key, such as "web". */
|
|
21
|
+
surface: string;
|
|
22
|
+
createdAt: Date;
|
|
23
|
+
lastActiveAt: Date;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The conversations run through `context.turns`, recorded at their first turn. Its kind,
|
|
28
|
+
* visibility, and principal are fixed then; each later turn only marks it active. Who may speak
|
|
29
|
+
* in it stays the claim's decision: the registry records, it does not refuse.
|
|
30
|
+
*/
|
|
31
|
+
export interface ConversationRegistry {
|
|
32
|
+
/** Records the conversation at its first turn, or marks a known one active; returns what is stored. */
|
|
33
|
+
register(entry: ConversationRegistration): Promise<ConversationRecord>;
|
|
34
|
+
get(key: ChannelKey): Promise<ConversationRecord | undefined>;
|
|
35
|
+
/** One principal's conversations, or every one; the most recently active first. */
|
|
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>;
|
|
51
|
+
/** Names a conversation, or clears its name; undefined when the key is unknown. */
|
|
52
|
+
setTitle(
|
|
53
|
+
key: ChannelKey,
|
|
54
|
+
title: string | undefined,
|
|
55
|
+
): Promise<ConversationRecord | undefined>;
|
|
56
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
|
+
import { parseChannelKey } from "../contract/surface.ts";
|
|
3
|
+
import type { Migration } from "../db/migrations.ts";
|
|
4
|
+
import type { ChannelKey } from "../sessions.ts";
|
|
5
|
+
import type {
|
|
6
|
+
ConversationRecord,
|
|
7
|
+
ConversationRegistration,
|
|
8
|
+
ConversationRegistry,
|
|
9
|
+
ConversationVisibility,
|
|
10
|
+
} from "./conversation-registry.ts";
|
|
11
|
+
|
|
12
|
+
interface Row {
|
|
13
|
+
key: string;
|
|
14
|
+
surface: string;
|
|
15
|
+
kind: string;
|
|
16
|
+
principal_id: string | null;
|
|
17
|
+
visibility: ConversationVisibility;
|
|
18
|
+
title: string | null;
|
|
19
|
+
created_at: Date;
|
|
20
|
+
last_active_at: Date;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function recordOf(row: Row): ConversationRecord {
|
|
24
|
+
return {
|
|
25
|
+
key: row.key as ChannelKey,
|
|
26
|
+
surface: row.surface,
|
|
27
|
+
kind: row.kind,
|
|
28
|
+
visibility: row.visibility,
|
|
29
|
+
...(row.principal_id === null ? {} : { principalId: row.principal_id }),
|
|
30
|
+
...(row.title === null ? {} : { title: row.title }),
|
|
31
|
+
createdAt: row.created_at,
|
|
32
|
+
lastActiveAt: row.last_active_at,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The conversation registry in the `conversations` table. */
|
|
37
|
+
export class PgConversationRegistry implements ConversationRegistry {
|
|
38
|
+
readonly #sql: SQL;
|
|
39
|
+
|
|
40
|
+
private constructor(sql: SQL) {
|
|
41
|
+
this.#sql = sql;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The registry's table; the host runs this before the registry attaches. */
|
|
45
|
+
static readonly migration: Migration = {
|
|
46
|
+
name: "conversations",
|
|
47
|
+
up: async (sql) => {
|
|
48
|
+
await sql`
|
|
49
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
50
|
+
key text PRIMARY KEY,
|
|
51
|
+
surface text NOT NULL,
|
|
52
|
+
kind text NOT NULL,
|
|
53
|
+
principal_id text,
|
|
54
|
+
visibility text NOT NULL CHECK (visibility IN ('private', 'shared')),
|
|
55
|
+
title text,
|
|
56
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
57
|
+
last_active_at timestamptz NOT NULL DEFAULT now()
|
|
58
|
+
)`;
|
|
59
|
+
await sql`
|
|
60
|
+
CREATE INDEX IF NOT EXISTS conversations_principal
|
|
61
|
+
ON conversations (principal_id, last_active_at DESC)`;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** The registry over the host's migrated pool. */
|
|
66
|
+
static async attach(sql: SQL): Promise<PgConversationRegistry> {
|
|
67
|
+
return new PgConversationRegistry(sql);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async register(entry: ConversationRegistration): Promise<ConversationRecord> {
|
|
71
|
+
const { surface } = parseChannelKey(entry.key);
|
|
72
|
+
const rows: Row[] = await this.#sql`
|
|
73
|
+
INSERT INTO conversations (key, surface, kind, principal_id, visibility, title)
|
|
74
|
+
VALUES (${entry.key}, ${surface}, ${entry.kind}, ${entry.principalId ?? null},
|
|
75
|
+
${entry.visibility}, ${entry.title ?? null})
|
|
76
|
+
ON CONFLICT (key) DO UPDATE SET last_active_at = now()
|
|
77
|
+
RETURNING *`;
|
|
78
|
+
const [row] = rows;
|
|
79
|
+
if (!row) throw new Error(`conversation ${entry.key} was not recorded`);
|
|
80
|
+
return recordOf(row);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async get(key: ChannelKey): Promise<ConversationRecord | undefined> {
|
|
84
|
+
const rows: Row[] = await this.#sql`
|
|
85
|
+
SELECT * FROM conversations WHERE key = ${key}`;
|
|
86
|
+
const [row] = rows;
|
|
87
|
+
return row ? recordOf(row) : undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async list(
|
|
91
|
+
filter: { principal?: string } = {},
|
|
92
|
+
): Promise<ConversationRecord[]> {
|
|
93
|
+
const rows: Row[] =
|
|
94
|
+
filter.principal === undefined
|
|
95
|
+
? await this.#sql`
|
|
96
|
+
SELECT * FROM conversations ORDER BY last_active_at DESC, key`
|
|
97
|
+
: await this.#sql`
|
|
98
|
+
SELECT * FROM conversations WHERE principal_id = ${filter.principal}
|
|
99
|
+
ORDER BY last_active_at DESC, key`;
|
|
100
|
+
return rows.map(recordOf);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async adopt(
|
|
104
|
+
key: ChannelKey,
|
|
105
|
+
principalId: string,
|
|
106
|
+
): Promise<ConversationRecord | undefined> {
|
|
107
|
+
// The row lock makes a racing adopt wait and then find the conversation no longer no one's.
|
|
108
|
+
const rows: Row[] = await this.#sql`
|
|
109
|
+
UPDATE conversations SET visibility = 'private', principal_id = ${principalId}
|
|
110
|
+
WHERE key = ${key} AND visibility = 'shared' AND principal_id IS NULL
|
|
111
|
+
RETURNING *`;
|
|
112
|
+
const [row] = rows;
|
|
113
|
+
return row ? recordOf(row) : this.get(key);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async setTitle(
|
|
117
|
+
key: ChannelKey,
|
|
118
|
+
title: string | undefined,
|
|
119
|
+
): Promise<ConversationRecord | undefined> {
|
|
120
|
+
const rows: Row[] = await this.#sql`
|
|
121
|
+
UPDATE conversations SET title = ${title ?? null} WHERE key = ${key}
|
|
122
|
+
RETURNING *`;
|
|
123
|
+
const [row] = rows;
|
|
124
|
+
return row ? recordOf(row) : undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SQL } from "bun";
|
|
2
|
+
import type { RoundtablePlugin } from "../plugin.ts";
|
|
3
|
+
import { CONVERSATIONS } from "../services.ts";
|
|
4
|
+
import type { ConversationRegistry } from "./conversation-registry.ts";
|
|
5
|
+
import { PgConversationRegistry } from "./conversation-store.ts";
|
|
6
|
+
|
|
7
|
+
/** The name of the conversation registry's plugin, as the migration ledger names it. */
|
|
8
|
+
export const CONVERSATIONS_PLUGIN = "conversations";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The conversation registry, provided as `CONVERSATIONS`: every conversation run through
|
|
12
|
+
* `context.turns`, recorded at its first turn. No session file moves; a conversation from before
|
|
13
|
+
* the registry is recorded at its next turn.
|
|
14
|
+
*/
|
|
15
|
+
export function conversationsPlugin(
|
|
16
|
+
/** Opens the registry; a test hands a stand-in, so the plugin sets up without a database. */
|
|
17
|
+
open: (sql: SQL) => Promise<ConversationRegistry> = (sql) =>
|
|
18
|
+
PgConversationRegistry.attach(sql),
|
|
19
|
+
): RoundtablePlugin {
|
|
20
|
+
return {
|
|
21
|
+
name: CONVERSATIONS_PLUGIN,
|
|
22
|
+
migrations: [PgConversationRegistry.migration],
|
|
23
|
+
provides: [CONVERSATIONS],
|
|
24
|
+
setup: async ({ database, services }) => {
|
|
25
|
+
services.provide(CONVERSATIONS, await open(database()));
|
|
26
|
+
return {};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|