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,4 +1,8 @@
|
|
|
1
1
|
import { attachmentsOf } from "../attachments/turn-attachments.ts";
|
|
2
|
+
import {
|
|
3
|
+
type ChannelContext,
|
|
4
|
+
withChannelContext,
|
|
5
|
+
} from "../contract/channel-context.ts";
|
|
2
6
|
import type {
|
|
3
7
|
Admission,
|
|
4
8
|
BackgroundTarget,
|
|
@@ -8,12 +12,13 @@ import type {
|
|
|
8
12
|
import type { AgentRuntime } from "../contract/runtime.ts";
|
|
9
13
|
import type { SurfacePort } from "../contract/surface.ts";
|
|
10
14
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
11
|
-
import { messages } from "../i18n/index.ts";
|
|
12
15
|
import type { OwnerIdentity } from "../identity.ts";
|
|
13
16
|
import type { Logger } from "../log.ts";
|
|
17
|
+
import { PERSONAL_TARGET } from "../modules/background/personal-target.ts";
|
|
14
18
|
import { withReference } from "../routing/message-text.ts";
|
|
15
19
|
import { outcome } from "../routing/settle-turn.ts";
|
|
16
|
-
import {
|
|
20
|
+
import { systemTurn } from "../routing/system-turns.ts";
|
|
21
|
+
import { addressee, attributed } from "../speakers.ts";
|
|
17
22
|
import type { DiscordAgentTeam } from "./agent-team.ts";
|
|
18
23
|
import { discordIdOf } from "./team-keys.ts";
|
|
19
24
|
|
|
@@ -27,23 +32,15 @@ import { discordIdOf } from "./team-keys.ts";
|
|
|
27
32
|
export const AGENT_SERVER_PRIORITY = 100;
|
|
28
33
|
|
|
29
34
|
/**
|
|
30
|
-
* The host's own background target
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* because the host applies its catalog at startup.
|
|
35
|
+
* The host's own background target, which the agent server answers.
|
|
36
|
+
* @deprecated Use `PERSONAL_TARGET`, the same target, which the core contributes on every host;
|
|
37
|
+
* this name goes away in 1.0.
|
|
34
38
|
*/
|
|
35
|
-
export const OWNER_TARGET: BackgroundTarget =
|
|
36
|
-
name: "owner",
|
|
37
|
-
label: () => messages().scheduleModeOwner,
|
|
38
|
-
schedules: { perChannel: 20, promptChars: 8_000, aheadDays: 366 },
|
|
39
|
-
delegation: { maxRunning: 3 },
|
|
40
|
-
};
|
|
39
|
+
export const OWNER_TARGET: BackgroundTarget = PERSONAL_TARGET;
|
|
41
40
|
|
|
42
41
|
export interface AgentClaimOptions {
|
|
43
42
|
/** The owner, as forwarded messages name them. */
|
|
44
43
|
owner: OwnerIdentity;
|
|
45
|
-
/** Who may talk to the agents, and at which tier. */
|
|
46
|
-
speakers: SpeakerPolicy;
|
|
47
44
|
team: Pick<
|
|
48
45
|
DiscordAgentTeam,
|
|
49
46
|
| "guildId"
|
|
@@ -60,6 +57,13 @@ export interface AgentClaimOptions {
|
|
|
60
57
|
logger: Logger;
|
|
61
58
|
/** Replaceable in tests. */
|
|
62
59
|
fetchImpl?: typeof fetch;
|
|
60
|
+
/**
|
|
61
|
+
* What was said in an agent's channel since its last post, `DISCORD.channelContext`; without it
|
|
62
|
+
* the turns carry none. A group's round carries what was said since its last turn already.
|
|
63
|
+
*/
|
|
64
|
+
channelContext?: (
|
|
65
|
+
message: InboundMessage,
|
|
66
|
+
) => Promise<ChannelContext | undefined>;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
/**
|
|
@@ -68,24 +72,39 @@ export interface AgentClaimOptions {
|
|
|
68
72
|
* failure notice, is a labelled report turn for the agent.
|
|
69
73
|
*/
|
|
70
74
|
export function agentClaim(options: AgentClaimOptions): ChannelClaim {
|
|
71
|
-
const {
|
|
72
|
-
options;
|
|
75
|
+
const { team, runtime, surface, attachmentDir, logger, fetchImpl } = options;
|
|
73
76
|
const attachments = (message: InboundMessage) =>
|
|
74
77
|
attachmentsOf(message, attachmentDir(message.channel), logger, fetchImpl);
|
|
78
|
+
/** The channel context of a message in an agent's channel; none, logged, when it cannot be read. */
|
|
79
|
+
const around = async (
|
|
80
|
+
message: InboundMessage,
|
|
81
|
+
): Promise<ChannelContext | undefined> => {
|
|
82
|
+
try {
|
|
83
|
+
return await options.channelContext?.(message);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
logger.warn(
|
|
86
|
+
{ channel: message.channel, err: error },
|
|
87
|
+
"channel context not read; the turn runs without it",
|
|
88
|
+
);
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
75
92
|
|
|
76
93
|
const webhookReport = (message: InboundMessage): Admission | undefined => {
|
|
77
94
|
const text = message.text.trim();
|
|
78
95
|
if (!text) return undefined;
|
|
79
96
|
return {
|
|
80
97
|
kind: "background",
|
|
81
|
-
turn
|
|
98
|
+
// The host's own turn, at the owner tier as in 0.8; what the webhook posted is untrusted input.
|
|
99
|
+
turn: systemTurn({
|
|
82
100
|
channel: message.channel,
|
|
83
|
-
target:
|
|
101
|
+
target: PERSONAL_TARGET.name,
|
|
84
102
|
author: { id: message.authorId, name: message.authorName },
|
|
103
|
+
tier: "owner",
|
|
85
104
|
turnId: `webhook-${message.messageId}`,
|
|
86
105
|
text: `Webhook "${message.authorName}" posted in your channel:\n${text}`,
|
|
87
106
|
report: true,
|
|
88
|
-
},
|
|
107
|
+
}),
|
|
89
108
|
unanswered: (result) =>
|
|
90
109
|
logger.warn(
|
|
91
110
|
{
|
|
@@ -112,13 +131,9 @@ export function agentClaim(options: AgentClaimOptions): ChannelClaim {
|
|
|
112
131
|
if (!owned) return undefined;
|
|
113
132
|
if (owned === "agent" && message.integration && !message.integration.own)
|
|
114
133
|
return webhookReport(message);
|
|
115
|
-
|
|
116
|
-
const speaker =
|
|
117
|
-
|
|
118
|
-
name: message.authorName,
|
|
119
|
-
...(message.authorRoleIds ? { roleIds: message.authorRoleIds } : {}),
|
|
120
|
-
});
|
|
121
|
-
if (!speaker) return undefined;
|
|
134
|
+
// Who may talk to the agents, and at which tier, the router resolved through the identity service.
|
|
135
|
+
const { speaker } = message;
|
|
136
|
+
if (message.authorIsBot || !speaker) return undefined;
|
|
122
137
|
const { channel, messageId } = message;
|
|
123
138
|
const forwarder = addressee(speaker, options.owner);
|
|
124
139
|
return {
|
|
@@ -147,7 +162,10 @@ export function agentClaim(options: AgentClaimOptions): ChannelClaim {
|
|
|
147
162
|
await team.answerOwner(
|
|
148
163
|
channel,
|
|
149
164
|
speaker,
|
|
150
|
-
attributed(
|
|
165
|
+
attributed(
|
|
166
|
+
speaker,
|
|
167
|
+
withChannelContext(text, await around(message)),
|
|
168
|
+
),
|
|
151
169
|
message.text,
|
|
152
170
|
saved,
|
|
153
171
|
);
|
|
@@ -166,22 +184,26 @@ export function agentClaim(options: AgentClaimOptions): ChannelClaim {
|
|
|
166
184
|
},
|
|
167
185
|
background: async (turn) => {
|
|
168
186
|
// Fail closed: a turn for any target but the owner's never reaches the owner's tools.
|
|
169
|
-
if (turn.target !==
|
|
187
|
+
if (turn.target !== PERSONAL_TARGET.name)
|
|
170
188
|
return {
|
|
171
189
|
status: "skipped",
|
|
172
|
-
reason: `the agent server answers only "${
|
|
190
|
+
reason: `the agent server answers only "${PERSONAL_TARGET.name}" background turns`,
|
|
173
191
|
};
|
|
174
192
|
const owned = team.owns(turn.channel);
|
|
175
193
|
if (owned === "group")
|
|
176
194
|
return { status: "skipped", reason: "groups have no schedules" };
|
|
195
|
+
// Who the turn runs as, checked by the router: its author's principal, at a tier they hold.
|
|
196
|
+
const { speaker } = turn;
|
|
197
|
+
if (!speaker)
|
|
198
|
+
return {
|
|
199
|
+
status: "skipped",
|
|
200
|
+
reason:
|
|
201
|
+
"a background turn reaches the agent server through the router, which says whom it runs as",
|
|
202
|
+
};
|
|
177
203
|
return outcome(
|
|
178
204
|
await team.answerBackground(
|
|
179
205
|
turn.channel,
|
|
180
|
-
|
|
181
|
-
id: turn.author.id,
|
|
182
|
-
name: turn.author.name,
|
|
183
|
-
tier: turn.tier ?? "owner",
|
|
184
|
-
},
|
|
206
|
+
speaker,
|
|
185
207
|
turn.text,
|
|
186
208
|
turn.report === true,
|
|
187
209
|
),
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "../testing/database.ts";
|
|
26
26
|
import { useTestLocale } from "../testing/locale.ts";
|
|
27
27
|
import { OWNER_SPEAKER, TEST_OWNER } from "../testing/owner.ts";
|
|
28
|
+
import { toolTiers } from "../tool-tiers.ts";
|
|
28
29
|
import type {
|
|
29
30
|
AgentCategory,
|
|
30
31
|
AgentChannels,
|
|
@@ -176,6 +177,10 @@ export interface TeamFixture {
|
|
|
176
177
|
skills: SkillRegistry;
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
/** The tool tiers the team reads; `deploy` is an admin's tool, so an admin may approve it. */
|
|
181
|
+
const FIXTURE_TIERS = toolTiers();
|
|
182
|
+
FIXTURE_TIERS.declare("test", { deploy: "admin" });
|
|
183
|
+
|
|
179
184
|
/** The fixture of the running test; set by `useTeamFixture` before each test. */
|
|
180
185
|
export const fx = {} as TeamFixture;
|
|
181
186
|
|
|
@@ -297,6 +302,7 @@ export function useTeamFixture(): void {
|
|
|
297
302
|
eventLog.length = 0;
|
|
298
303
|
team = new AgentTeam({
|
|
299
304
|
owner: TEST_OWNER,
|
|
305
|
+
toolTiers: FIXTURE_TIERS,
|
|
300
306
|
shellUser: "mcops",
|
|
301
307
|
guildId: "1",
|
|
302
308
|
entryChannelId: ENTRY,
|
|
@@ -143,8 +143,17 @@ export class TeamTurns {
|
|
|
143
143
|
);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
/**
|
|
146
|
+
/**
|
|
147
|
+
* Whether the speaker may approve the held actions by a message: an owner, as the agents'
|
|
148
|
+
* channels are shared, or the principal whose turn held them (by their id, for actions held
|
|
149
|
+
* before 0.9), when their tier now holds every tool. Nobody else in the channel may.
|
|
150
|
+
*/
|
|
147
151
|
#mayApprove(speaker: Speaker, pending: PendingConfirmation): boolean {
|
|
152
|
+
const theirs =
|
|
153
|
+
pending.principalId !== undefined
|
|
154
|
+
? speaker.principalId === pending.principalId
|
|
155
|
+
: speaker.id === pending.speakerId;
|
|
156
|
+
if (speaker.tier !== "owner" && !theirs) return false;
|
|
148
157
|
const tiers = this.#options.toolTiers ?? toolTiers();
|
|
149
158
|
return pending.calls.every(
|
|
150
159
|
(call) =>
|
|
@@ -2,7 +2,7 @@ import { chmodSync, lstatSync, mkdirSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { SQL } from "bun";
|
|
5
|
-
import { agentClaim
|
|
5
|
+
import { agentClaim } from "../agents/agent-claim.ts";
|
|
6
6
|
import { AgentDashboard } from "../agents/agent-dashboard.ts";
|
|
7
7
|
import { PgAgentStore } from "../agents/agent-store.ts";
|
|
8
8
|
import { DiscordAgentTeam } from "../agents/agent-team.ts";
|
|
@@ -11,28 +11,26 @@ import { FileAvatarStudio } from "../agents/avatar-studio.ts";
|
|
|
11
11
|
import { RelevanceScorer } from "../agents/group-round.ts";
|
|
12
12
|
import { discordKey } from "../agents/team-keys.ts";
|
|
13
13
|
import { ownerAttachmentDir } from "../attachments/attachment-dir.ts";
|
|
14
|
-
import type {
|
|
14
|
+
import type { AgentSessions } from "../contract/runtime.ts";
|
|
15
15
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
16
16
|
import { ConfigError } from "../domain/errors.ts";
|
|
17
|
-
import type { InterimTextMode } from "../domain/interim.ts";
|
|
18
17
|
import type { OwnerIdentity } from "../identity.ts";
|
|
19
18
|
import { ConfirmationJudge } from "../judging/confirmation-judge.ts";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
AUTO_THINKING,
|
|
23
|
-
formatModelRef,
|
|
24
|
-
type ModelRef,
|
|
25
|
-
type ThinkingLevel,
|
|
26
|
-
} from "../models.ts";
|
|
19
|
+
import { AUTO_THINKING, formatModelRef, type ModelRef } from "../models.ts";
|
|
27
20
|
import type { ErrorReporter } from "../ops/error-reporter.ts";
|
|
28
21
|
import type { PluginContext, RoundtablePlugin } from "../plugin.ts";
|
|
29
22
|
import { splitReply } from "../presentation/reply-splitter.ts";
|
|
30
23
|
import { agentPromptExtension } from "../runtime/extensions/agent-prompt.ts";
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
|
|
24
|
+
import type { AgentSessionsSlot } from "../runtime/runtime-plugin.ts";
|
|
25
|
+
import {
|
|
26
|
+
AGENTS,
|
|
27
|
+
BACKGROUND_TURNS,
|
|
28
|
+
RUNTIME,
|
|
29
|
+
SCHEDULES,
|
|
30
|
+
SKILLS,
|
|
31
|
+
} from "../services.ts";
|
|
34
32
|
import type { SessionTool } from "../sessions.ts";
|
|
35
|
-
import type {
|
|
33
|
+
import type { Tier } from "../speakers.ts";
|
|
36
34
|
import { DISCORD } from "./discord.ts";
|
|
37
35
|
import { agentOnly } from "./session-tool.ts";
|
|
38
36
|
|
|
@@ -43,17 +41,15 @@ export interface AgentServerOptions {
|
|
|
43
41
|
owner: OwnerIdentity & { id: string };
|
|
44
42
|
/** The assistant's display name, as the confirmation judge names it. */
|
|
45
43
|
assistant: string;
|
|
46
|
-
/**
|
|
47
|
-
speakers: SpeakerPolicy;
|
|
48
|
-
/** Shared with the rest of the host, so logins refresh in one place. */
|
|
44
|
+
/** Shared with the rest of the host, so logins refresh in one place; lists the models an agent may run. */
|
|
49
45
|
modelRuntime: ModelRuntime;
|
|
50
|
-
agentDir: string;
|
|
51
46
|
dataDir: string;
|
|
52
|
-
/** The model of agents and of the assistant
|
|
47
|
+
/** The model of agents and of the assistant. */
|
|
53
48
|
model: ModelRef;
|
|
54
|
-
|
|
55
|
-
/** How sure the judges must be before their answer is used. */
|
|
49
|
+
/** How sure the confirmation judge must be before its answer is used. */
|
|
56
50
|
judgeThreshold: number;
|
|
51
|
+
/** Where the agent server hands the runtime plugin its per-agent settings. */
|
|
52
|
+
agents?: AgentSessionsSlot;
|
|
57
53
|
/** The shell's shared working directory. */
|
|
58
54
|
workDir: string;
|
|
59
55
|
/**
|
|
@@ -63,6 +59,11 @@ export interface AgentServerOptions {
|
|
|
63
59
|
scratchDir?: string;
|
|
64
60
|
/** The host account the agents' shell and file tools run as. */
|
|
65
61
|
shellUser: string;
|
|
62
|
+
/**
|
|
63
|
+
* Whose turns in the agents' channels load memory: `"everyone"` (the default) or `"owners"`;
|
|
64
|
+
* see `AgentSessions.memory`.
|
|
65
|
+
*/
|
|
66
|
+
memory?: "everyone" | "owners";
|
|
66
67
|
/** The prompt every agent starts with, and the one for speakers other than the owner. */
|
|
67
68
|
prompts: { shared: string; guest?: string };
|
|
68
69
|
/** The listener the avatar pictures are served on. */
|
|
@@ -73,10 +74,6 @@ export interface AgentServerOptions {
|
|
|
73
74
|
avatarReference: string;
|
|
74
75
|
/** Reports the process's own errors to an agent; without one nothing is reported. */
|
|
75
76
|
errorReporter?: ErrorReporter;
|
|
76
|
-
/** Whether turns post the text they write before their final answer as they go; default "on". */
|
|
77
|
-
interimText?: InterimTextMode;
|
|
78
|
-
/** An intermediate text this long or longer is posted as an ordinary message; default 400. */
|
|
79
|
-
interimPrimaryChars?: number;
|
|
80
77
|
}
|
|
81
78
|
|
|
82
79
|
/** The name of the agent server's plugin, as `serviceStarted` events name it. */
|
|
@@ -109,11 +106,9 @@ const AGENT_TIERS: Readonly<Record<string, Tier>> = {
|
|
|
109
106
|
channel_read: "member",
|
|
110
107
|
};
|
|
111
108
|
|
|
112
|
-
/** The tables the agent server keeps: its agents and groups
|
|
109
|
+
/** The tables the agent server keeps: its agents and groups. */
|
|
113
110
|
export interface AgentServerStores {
|
|
114
111
|
agents: PgAgentStore;
|
|
115
|
-
/** Held actions, kept across a restart. */
|
|
116
|
-
confirmations: PendingConfirmationStore;
|
|
117
112
|
}
|
|
118
113
|
|
|
119
114
|
/** Attaches the agent server's stores over the host's migrated pool. */
|
|
@@ -121,16 +116,14 @@ async function attachStores(
|
|
|
121
116
|
sql: SQL,
|
|
122
117
|
guildId: string,
|
|
123
118
|
): Promise<AgentServerStores> {
|
|
124
|
-
return {
|
|
125
|
-
agents: await PgAgentStore.attach(sql, guildId),
|
|
126
|
-
confirmations: await PendingConfirmationStore.attach(sql),
|
|
127
|
-
};
|
|
119
|
+
return { agents: await PgAgentStore.attach(sql, guildId) };
|
|
128
120
|
}
|
|
129
121
|
|
|
130
122
|
/**
|
|
131
|
-
* The agent server: the team with its channels and dashboard,
|
|
132
|
-
* agent
|
|
133
|
-
*
|
|
123
|
+
* The agent server: the team with its channels and dashboard, run on the runtime plugin's runtime,
|
|
124
|
+
* which it hands its per-agent settings. The skills the agents carry come from the skills addon
|
|
125
|
+
* when it is on. It claims the agent channels, starts the team in the background, and hands what
|
|
126
|
+
* it built to the plugins after it.
|
|
134
127
|
*/
|
|
135
128
|
export function agentServerPlugin(
|
|
136
129
|
options: AgentServerOptions,
|
|
@@ -141,18 +134,13 @@ export function agentServerPlugin(
|
|
|
141
134
|
) => Promise<AgentServerStores> = (context, guildId) =>
|
|
142
135
|
attachStores(context.database(), guildId),
|
|
143
136
|
): RoundtablePlugin {
|
|
144
|
-
let runtime: AgentRuntime | undefined;
|
|
145
137
|
return {
|
|
146
138
|
name: AGENT_SERVER_PLUGIN,
|
|
147
|
-
// The agent server's own tables; the host runs them before any setup, in this order.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
...PgAgentStore.migrations(options.guildId),
|
|
151
|
-
],
|
|
139
|
+
// The agent server's own tables; the host runs them before any setup, in this order. The
|
|
140
|
+
// held actions' table is the runtime plugin's.
|
|
141
|
+
migrations: PgAgentStore.migrations(options.guildId),
|
|
152
142
|
provides: [AGENTS],
|
|
153
|
-
|
|
154
|
-
await runtime?.preflight?.();
|
|
155
|
-
},
|
|
143
|
+
requires: [RUNTIME],
|
|
156
144
|
setup: async (context) => {
|
|
157
145
|
const discord = context.services.get(DISCORD);
|
|
158
146
|
const schedules = context.services.get(SCHEDULES);
|
|
@@ -168,10 +156,8 @@ export function agentServerPlugin(
|
|
|
168
156
|
await studio.init();
|
|
169
157
|
// Absent when the skills addon is off: the agents then carry none.
|
|
170
158
|
const skills = context.services.find(SKILLS);
|
|
171
|
-
const { agents: store
|
|
172
|
-
|
|
173
|
-
options.guildId,
|
|
174
|
-
);
|
|
159
|
+
const { agents: store } = await openStores(context, options.guildId);
|
|
160
|
+
const running = context.services.get(RUNTIME);
|
|
175
161
|
const judge = providers.judge;
|
|
176
162
|
const confirmations = new ConfirmationJudge({
|
|
177
163
|
judge,
|
|
@@ -194,11 +180,7 @@ export function agentServerPlugin(
|
|
|
194
180
|
store,
|
|
195
181
|
channels: connection.agentChannels(options.guildId),
|
|
196
182
|
studio,
|
|
197
|
-
|
|
198
|
-
runtime: () => {
|
|
199
|
-
if (!runtime) throw new Error("the runtime is built with the team");
|
|
200
|
-
return runtime;
|
|
201
|
-
},
|
|
183
|
+
runtime: () => running,
|
|
202
184
|
confirmations,
|
|
203
185
|
scorer: new RelevanceScorer(judge, logger, options.owner),
|
|
204
186
|
models: {
|
|
@@ -246,51 +228,9 @@ export function agentServerPlugin(
|
|
|
246
228
|
modelOf: (name) => built.modelOf(name),
|
|
247
229
|
skills: (name) => built.skillsOf(name),
|
|
248
230
|
turnChannel: (scope) => built.turnChannel(scope),
|
|
231
|
+
...(options.memory ? { memory: options.memory } : {}),
|
|
249
232
|
};
|
|
250
|
-
|
|
251
|
-
context.surfaces.prompts(channel, speaker);
|
|
252
|
-
// A plugin that fills the runtime slot replaces the whole conversation runtime; without
|
|
253
|
-
// one the agent server runs Pi, built from the options below.
|
|
254
|
-
const running: AgentRuntime = providers.filled.has("runtime")
|
|
255
|
-
? providers.runtime({
|
|
256
|
-
logger,
|
|
257
|
-
env: context.env,
|
|
258
|
-
owner: { id: options.owner.id, name: options.owner.name },
|
|
259
|
-
sessions: context.sessions,
|
|
260
|
-
toolTiers: context.toolTiers,
|
|
261
|
-
prompts,
|
|
262
|
-
agents: agentSessions,
|
|
263
|
-
confirmations: heldActions,
|
|
264
|
-
judge,
|
|
265
|
-
})
|
|
266
|
-
: new PiAgentRuntime({
|
|
267
|
-
owner: options.owner,
|
|
268
|
-
sessions: context.sessions,
|
|
269
|
-
agentDir: options.agentDir,
|
|
270
|
-
modelRuntime: options.modelRuntime,
|
|
271
|
-
dataDir: options.dataDir,
|
|
272
|
-
model: options.model,
|
|
273
|
-
thinking: options.thinking,
|
|
274
|
-
effort: new EffortJudge({
|
|
275
|
-
judge,
|
|
276
|
-
brief: AGENT_BRIEF,
|
|
277
|
-
fallback: options.thinking,
|
|
278
|
-
threshold: options.judgeThreshold,
|
|
279
|
-
logger,
|
|
280
|
-
}),
|
|
281
|
-
confirmations: heldActions,
|
|
282
|
-
toolTiers: context.toolTiers,
|
|
283
|
-
agents: agentSessions,
|
|
284
|
-
prompts,
|
|
285
|
-
logger,
|
|
286
|
-
...(options.interimText
|
|
287
|
-
? { interimText: options.interimText }
|
|
288
|
-
: {}),
|
|
289
|
-
...(options.interimPrimaryChars
|
|
290
|
-
? { interimPrimaryChars: options.interimPrimaryChars }
|
|
291
|
-
: {}),
|
|
292
|
-
});
|
|
293
|
-
runtime = running;
|
|
233
|
+
options.agents?.bind(agentSessions);
|
|
294
234
|
context.services.provide(AGENTS, {
|
|
295
235
|
team: built,
|
|
296
236
|
directory: store,
|
|
@@ -325,7 +265,6 @@ export function agentServerPlugin(
|
|
|
325
265
|
});
|
|
326
266
|
return {
|
|
327
267
|
services: [
|
|
328
|
-
{ name: "runtime", stop: () => running.dispose?.() },
|
|
329
268
|
{ name: "dashboard", stop: () => dashboard.stop() },
|
|
330
269
|
{
|
|
331
270
|
name: AGENT_TEAM_SERVICE,
|
|
@@ -341,17 +280,15 @@ export function agentServerPlugin(
|
|
|
341
280
|
},
|
|
342
281
|
},
|
|
343
282
|
],
|
|
344
|
-
// The schedules and delegated tasks of the owner's and the agents' conversations are its own.
|
|
345
|
-
backgroundTargets: [OWNER_TARGET],
|
|
346
283
|
channels: [
|
|
347
284
|
agentClaim({
|
|
348
285
|
owner: options.owner,
|
|
349
|
-
speakers: options.speakers,
|
|
350
286
|
team: built,
|
|
351
287
|
runtime: running,
|
|
352
288
|
surface: context.surfaces,
|
|
353
289
|
attachmentDir,
|
|
354
290
|
logger,
|
|
291
|
+
channelContext: (message) => discord.channelContext(message),
|
|
355
292
|
}),
|
|
356
293
|
],
|
|
357
294
|
http: [studio.route(options.avatarListener)],
|
|
@@ -8,7 +8,10 @@ import { DISCORD } from "./discord.ts";
|
|
|
8
8
|
import { fixed } from "./session-tool.ts";
|
|
9
9
|
|
|
10
10
|
export interface DiscordAdminOptions {
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Whose Discord permissions the tools check, and who they serve: the primary owner, by their
|
|
13
|
+
* Discord user id, whichever owner's turn calls them.
|
|
14
|
+
*/
|
|
12
15
|
owner: OwnerIdentity & { id: string };
|
|
13
16
|
}
|
|
14
17
|
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import type {
|
|
3
|
+
ChannelContext,
|
|
4
|
+
ChannelContextOptions,
|
|
5
|
+
} from "../contract/channel-context.ts";
|
|
2
6
|
import { type ServiceKey, serviceKey } from "../contract/services.ts";
|
|
7
|
+
import { ChannelContextReader } from "../discord/channel-context.ts";
|
|
3
8
|
import { CommandCollection } from "../discord/command-collection.ts";
|
|
4
9
|
import type { DiscordConnection } from "../discord/connection.ts";
|
|
10
|
+
import { discordDirectChannel } from "../discord/direct-channel.ts";
|
|
11
|
+
import { DiscordOwners } from "../discord/discord-owners.ts";
|
|
5
12
|
import { DiscordSurface } from "../discord/discord-surface.ts";
|
|
6
13
|
import { DispatchThreads } from "../discord/dispatch-threads.ts";
|
|
7
14
|
import type {
|
|
@@ -9,20 +16,33 @@ import type {
|
|
|
9
16
|
CommandRegistrar,
|
|
10
17
|
} from "../discord/interaction-module.ts";
|
|
11
18
|
import { OwnerCards } from "../discord/owner-cards.ts";
|
|
12
|
-
import {
|
|
19
|
+
import { commandGuard, ownerRootCommand } from "../discord/owner-command.ts";
|
|
13
20
|
import { stopButtonModule } from "../discord/stop-button.ts";
|
|
21
|
+
import type { InboundMessage } from "../domain/conversation.ts";
|
|
14
22
|
import type { RoundtablePlugin } from "../plugin.ts";
|
|
15
|
-
import
|
|
23
|
+
import { IDENTITY } from "../services.ts";
|
|
16
24
|
|
|
17
25
|
/** The Discord connection and what stands on it, as plugins use it. Provided by the Discord plugin. */
|
|
18
26
|
export interface DiscordServices {
|
|
19
27
|
connection: DiscordConnection;
|
|
20
28
|
/** Adds a plugin's slash commands and components; during setup only. */
|
|
21
29
|
commands: CommandRegistrar;
|
|
22
|
-
/** Who may use the owner's commands. */
|
|
30
|
+
/** Who may use the owner's commands: every owner, by their Discord identity. */
|
|
23
31
|
guard: CommandGuard;
|
|
24
32
|
/** The threads that carry background reports. */
|
|
25
33
|
threads: DispatchThreads;
|
|
34
|
+
/**
|
|
35
|
+
* What was said in a server channel since the assistant last posted there, before `message`:
|
|
36
|
+
* for a claim that answers it, to append to its turn's text with `withChannelContext`. Call it
|
|
37
|
+
* once per answered message, inside the turn's `run`: the messages it returns, and `message`,
|
|
38
|
+
* are remembered per channel and not returned again. Undefined in a direct message, on another
|
|
39
|
+
* surface, when the host's `discord.channelContext` is `false`, when nothing new was said, and
|
|
40
|
+
* when Discord cannot be read (logged). Never rejects. `options` override the host's for this call.
|
|
41
|
+
*/
|
|
42
|
+
channelContext(
|
|
43
|
+
message: InboundMessage,
|
|
44
|
+
options?: ChannelContextOptions,
|
|
45
|
+
): Promise<ChannelContext | undefined>;
|
|
26
46
|
}
|
|
27
47
|
|
|
28
48
|
/** The Discord connection, provided by the Discord plugin. */
|
|
@@ -31,16 +51,22 @@ export const DISCORD: ServiceKey<DiscordServices> =
|
|
|
31
51
|
|
|
32
52
|
export interface DiscordOptions {
|
|
33
53
|
token: string;
|
|
54
|
+
/**
|
|
55
|
+
* The primary owner's Discord user id: an owner always, and the one the admin tools act as.
|
|
56
|
+
* Every other owner the identity service knows uses the owner's commands and cards too.
|
|
57
|
+
*/
|
|
34
58
|
ownerId: string;
|
|
35
|
-
/** How the owner is named in audit-log reasons and refusals. */
|
|
59
|
+
/** How the primary owner is named in audit-log reasons and refusals. */
|
|
36
60
|
ownerName: string;
|
|
37
|
-
/** Who may answer a card besides the owner. */
|
|
38
|
-
speakers: SpeakerPolicy;
|
|
39
61
|
dataDir: string;
|
|
40
62
|
/** The name of the root slash command, without the slash. */
|
|
41
63
|
rootCommand: string;
|
|
42
64
|
/** Text appended as it is to the refusal a non-owner gets, such as a pointer to the commands anyone may use. */
|
|
43
65
|
refusalHint?: string;
|
|
66
|
+
/** The host's channel context settings; `false` turns it off. Default: on, with the defaults. */
|
|
67
|
+
channelContext?: ChannelContextOptions | false;
|
|
68
|
+
/** The divider posted in a server channel when its conversation starts over; `false` posts none. Default: a short English divider. */
|
|
69
|
+
freshMarker?: string | false;
|
|
44
70
|
}
|
|
45
71
|
|
|
46
72
|
/**
|
|
@@ -62,10 +88,12 @@ export function discordPlugin(options: DiscordOptions): RoundtablePlugin {
|
|
|
62
88
|
);
|
|
63
89
|
},
|
|
64
90
|
setup: async ({ conversations, services, logger }) => {
|
|
91
|
+
const identity = services.find(IDENTITY);
|
|
65
92
|
// The surface exists by the time a turn posts a card.
|
|
66
93
|
const cards: OwnerCards = new OwnerCards({
|
|
67
94
|
ownerId: options.ownerId,
|
|
68
|
-
|
|
95
|
+
// Who may answer a card besides the primary owner, by the identity service.
|
|
96
|
+
...(identity ? { identity } : {}),
|
|
69
97
|
channel: (channelId) => connected.cardChannel(channelId),
|
|
70
98
|
logger,
|
|
71
99
|
});
|
|
@@ -73,16 +101,23 @@ export function discordPlugin(options: DiscordOptions): RoundtablePlugin {
|
|
|
73
101
|
token: options.token,
|
|
74
102
|
ownerId: options.ownerId,
|
|
75
103
|
ownerName: options.ownerName,
|
|
76
|
-
prompts: (channel,
|
|
104
|
+
prompts: (channel, scope) => cards.prompts(channel, scope),
|
|
77
105
|
logger,
|
|
106
|
+
...(options.freshMarker === undefined
|
|
107
|
+
? {}
|
|
108
|
+
: { freshMarker: options.freshMarker }),
|
|
78
109
|
});
|
|
79
110
|
surface = connected;
|
|
80
|
-
|
|
81
|
-
|
|
111
|
+
// Every owner may use the owner's commands, by the identity service; the primary owner always.
|
|
112
|
+
const guard = commandGuard({
|
|
113
|
+
ownerId: options.ownerId,
|
|
114
|
+
...(identity ? { identity } : {}),
|
|
115
|
+
root: options.rootCommand,
|
|
82
116
|
logger,
|
|
83
|
-
options.
|
|
84
|
-
|
|
85
|
-
|
|
117
|
+
...(options.refusalHint === undefined
|
|
118
|
+
? {}
|
|
119
|
+
: { refusalHint: options.refusalHint }),
|
|
120
|
+
});
|
|
86
121
|
// A channel whose claim keeps reports in place, such as an open channel, opens no thread.
|
|
87
122
|
const threads = new DispatchThreads({
|
|
88
123
|
host: {
|
|
@@ -101,15 +136,38 @@ export function discordPlugin(options: DiscordOptions): RoundtablePlugin {
|
|
|
101
136
|
collection.registrar.add({
|
|
102
137
|
module: stopButtonModule({ guard, conversations }),
|
|
103
138
|
});
|
|
139
|
+
// Marks the owners in channel context: the primary owner, and every other owner's Discord identities.
|
|
140
|
+
const owners = new DiscordOwners({
|
|
141
|
+
ownerId: options.ownerId,
|
|
142
|
+
...(identity ? { identity } : {}),
|
|
143
|
+
logger,
|
|
144
|
+
});
|
|
145
|
+
const context = new ChannelContextReader({
|
|
146
|
+
read: (channelId, before, limit) =>
|
|
147
|
+
connected.messagesBefore(channelId, before, limit),
|
|
148
|
+
owners: () => owners.ids(),
|
|
149
|
+
settings: options.channelContext ?? {},
|
|
150
|
+
logger,
|
|
151
|
+
});
|
|
104
152
|
services.provide(DISCORD, {
|
|
105
153
|
connection: connected,
|
|
106
154
|
commands: collection.registrar,
|
|
107
155
|
guard,
|
|
108
156
|
threads,
|
|
157
|
+
channelContext: (message, given) => context.of(message, given),
|
|
109
158
|
});
|
|
110
159
|
return {
|
|
111
160
|
// The host starts the surface first; it registers the composed commands as it connects.
|
|
112
161
|
surfaces: [connected],
|
|
162
|
+
// A person's Discord direct messages, through a Discord identity of theirs.
|
|
163
|
+
directChannels: [
|
|
164
|
+
discordDirectChannel({
|
|
165
|
+
directChannel: (userId) => connected.directChannel(userId),
|
|
166
|
+
sendDirect: (userId, text) => connected.sendDirect(userId, text),
|
|
167
|
+
...(identity ? { identity } : {}),
|
|
168
|
+
ownerId: options.ownerId,
|
|
169
|
+
}),
|
|
170
|
+
],
|
|
113
171
|
services: [
|
|
114
172
|
// The sweep reads threads through the connection, so it follows the surface's start.
|
|
115
173
|
{ name: "threads", start: () => void threads.sweep() },
|