pi-roundtable 0.1.0 → 0.2.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 +138 -0
- package/README.md +6 -4
- package/docs/plugins.md +1448 -66
- package/examples/echo-runtime.test.ts +122 -0
- package/examples/echo-runtime.ts +107 -0
- package/examples/events.test.ts +1 -0
- package/examples/events.ts +2 -2
- package/examples/fake-surface.test.ts +180 -0
- package/examples/fake-surface.ts +109 -0
- package/examples/interactions.test.ts +12 -3
- package/examples/interactions.ts +19 -19
- package/examples/shared-services.test.ts +74 -0
- package/examples/shared-services.ts +60 -0
- package/examples/study-room.test.ts +69 -0
- package/examples/study-room.ts +52 -0
- package/examples/support-desk.test.ts +34 -0
- package/examples/support-desk.ts +40 -0
- package/package.json +9 -2
- package/src/cli/checks/database.ts +11 -30
- package/src/cli/checks/images.ts +19 -0
- package/src/cli/doctor.ts +6 -0
- package/src/cli/main.ts +2 -1
- package/src/core/agents/agent-claim.ts +42 -16
- package/src/core/agents/agent-messages.ts +4 -4
- package/src/core/agents/agent-ports.ts +8 -21
- package/src/core/agents/agent-prompt.ts +3 -1
- package/src/core/agents/agent-store.ts +6 -4
- package/src/core/agents/agent-team-fixture.ts +389 -0
- package/src/core/agents/agent-team.ts +26 -25
- package/src/core/agents/agent-tools.ts +34 -15
- package/src/core/agents/avatar-studio.ts +40 -6
- package/src/core/agents/fallback-avatar.ts +85 -0
- package/src/core/agents/team-editing.ts +49 -10
- package/src/core/agents/team-keys.ts +22 -9
- package/src/core/agents/team-layout.ts +4 -3
- package/src/core/agents/team-lifecycle.ts +28 -2
- package/src/core/agents/team-options.ts +9 -5
- package/src/core/agents/team-status.ts +4 -4
- package/src/core/agents/team-text.ts +8 -6
- package/src/core/agents/team-turn-types.ts +4 -4
- package/src/core/agents/team-turns.ts +6 -5
- package/src/core/builtin/agent-server.ts +187 -121
- package/src/core/builtin/discord-admin.ts +41 -0
- package/src/core/builtin/discord.ts +65 -40
- package/src/core/builtin/modules.ts +67 -58
- package/src/core/builtin/session-tool.ts +24 -0
- package/src/core/builtin/skills.ts +72 -0
- package/src/core/builtin/stores.ts +55 -31
- package/src/core/config/config.ts +45 -4
- package/src/core/config/schema.ts +6 -0
- package/src/core/contract/channels.ts +54 -14
- package/src/core/contract/providers.ts +17 -0
- package/src/core/contract/runtime.ts +138 -0
- package/src/core/contract/services.ts +45 -0
- package/src/core/contract/surface.ts +88 -0
- package/src/core/db/migrations.ts +118 -3
- package/src/core/define-roundtable.ts +70 -46
- package/src/core/define.ts +2 -1
- package/src/core/discord/agent-commands.ts +39 -14
- package/src/core/discord/agent-panel.ts +66 -0
- package/src/core/discord/channel-executor.ts +6 -3
- package/src/core/discord/channel-operations.ts +14 -11
- package/src/core/discord/command-collection.ts +46 -0
- package/src/core/{registry/interactions.ts → discord/compose-commands.ts} +5 -5
- package/src/core/discord/connection.ts +35 -0
- package/src/core/discord/discord-surface.ts +32 -94
- package/src/core/discord/dispatch-threads.ts +2 -2
- package/src/core/discord/inbound-message.ts +99 -0
- package/src/core/discord/interaction-module.ts +57 -1
- package/src/core/discord/owner-cards.ts +4 -4
- package/src/core/discord/owner-command.ts +44 -11
- package/src/core/discord/owner-discord.ts +2 -2
- package/src/core/discord/schedule-commands.ts +21 -18
- package/src/core/discord/stop-button.ts +44 -1
- package/src/core/domain/attachment.ts +12 -8
- package/src/core/domain/conversation.ts +3 -4
- package/src/core/domain/owner-prompts.ts +5 -5
- package/src/core/domain/ports.ts +9 -53
- package/src/core/freeze.ts +9 -0
- package/src/core/host.ts +255 -82
- package/src/core/http/listeners.ts +55 -20
- package/src/core/i18n/agent-panel.ts +4 -0
- package/src/core/i18n/index.ts +12 -2
- package/src/core/i18n/owner.ts +7 -1
- package/src/core/i18n/schedules.ts +12 -6
- package/src/core/identity.ts +1 -1
- package/src/core/judging/effort-judge.ts +25 -3
- package/src/core/log.ts +21 -3
- package/src/core/models.ts +2 -2
- package/src/core/modules/background/background-turns.ts +6 -4
- package/src/core/modules/delegation/delegate.ts +3 -2
- package/src/core/modules/delegation/delegator.ts +15 -13
- package/src/core/modules/delegation/{sol-worker.ts → web-research-worker.ts} +6 -6
- package/src/core/modules/discord-admin/discord-admin.ts +6 -6
- package/src/core/modules/host-shell/shell-policy.ts +8 -3
- package/src/core/modules/memory/owner-memory-store.ts +27 -25
- package/src/core/modules/memory/owner-memory.ts +9 -13
- package/src/core/modules/schedules/recurrence.ts +5 -3
- package/src/core/modules/schedules/schedule-store.ts +11 -11
- package/src/core/modules/schedules/schedule-tools.ts +22 -20
- package/src/core/modules/schedules/scheduler.ts +7 -2
- package/src/core/modules/schedules/schedules.ts +9 -3
- package/src/core/modules/skills/skill-registry.ts +3 -2
- package/src/core/modules/skills/skill-store.ts +2 -15
- package/src/core/modules/skills/skill-tools.ts +10 -4
- package/src/core/ops/error-reporter.ts +1 -1
- package/src/core/plugin.ts +232 -28
- package/src/core/registry/contributions.ts +173 -14
- package/src/core/registry/providers.ts +35 -5
- package/src/core/registry/services.ts +229 -0
- package/src/core/routing/channel-queue.ts +5 -0
- package/src/core/routing/channel-router.ts +21 -7
- package/src/core/routing/conversation-turns.ts +139 -0
- package/src/core/routing/message-text.ts +9 -2
- package/src/core/routing/surface-port.ts +39 -0
- package/src/core/runtime/conversation-sessions.ts +4 -3
- package/src/core/runtime/extensions/confirmation-fixture.ts +11 -0
- package/src/core/runtime/extensions/confirmation-gate.ts +11 -9
- package/src/core/runtime/mcp.ts +1 -1
- package/src/core/runtime/pending-confirmation-store.ts +10 -12
- package/src/core/runtime/pi-agent-runtime.ts +9 -6
- package/src/core/runtime/prompt-slot.ts +8 -3
- package/src/core/runtime/runtime-types.ts +9 -35
- package/src/core/runtime/session-factory.ts +21 -5
- package/src/core/runtime/text-tools.ts +1 -3
- package/src/core/services.ts +238 -90
- package/src/core/sessions.ts +3 -2
- package/src/core/shared/{profile-tools.ts → active-tools.ts} +2 -2
- package/src/core/shared/delegate-tool.ts +4 -3
- package/src/core/shared/schedule-tools.ts +22 -13
- package/src/core/shared/session-messages.ts +1 -1
- package/src/core/speakers.ts +6 -38
- package/src/core/testing/database.ts +1 -7
- package/src/core/testing/eager-catalog.ts +30 -0
- package/src/core/testing/locale.ts +23 -5
- package/src/core/testing/modules.ts +105 -41
- package/src/core/testing/test-host.ts +279 -0
- package/src/core/testing/tool-set.ts +64 -0
- package/src/core/time.ts +6 -1
- package/src/core/tool-set.snapshot.json +288 -0
- package/src/core/tool-tiers.ts +17 -57
- package/src/discord/index.ts +64 -0
- package/src/index.ts +181 -10
- package/src/kit/channels.ts +14 -0
- package/src/kit/domain.ts +18 -0
- package/src/kit/holds.ts +4 -0
- package/src/kit/index.ts +129 -0
- package/src/kit/judging.ts +10 -0
- package/src/kit/memory.ts +3 -0
- package/src/kit/mirror.ts +19 -0
- package/src/kit/presentation.ts +7 -0
- package/src/kit/shell.ts +7 -0
- package/src/kit/skills.ts +8 -0
- package/src/kit/support.ts +21 -0
- package/src/kit/threads.ts +7 -0
- package/src/kit/tools.ts +14 -0
- package/src/kit/worker.ts +16 -0
- package/src/testing.ts +394 -34
- package/examples/guide.test.ts +0 -52
- package/src/cli/add-plugin.test.ts +0 -107
- package/src/cli/checks/basic.test.ts +0 -276
- package/src/cli/checks/database.test.ts +0 -117
- package/src/cli/checks/discord.test.ts +0 -200
- package/src/cli/cli.test.ts +0 -122
- package/src/cli/config-edit.test.ts +0 -130
- package/src/cli/doctor.test.ts +0 -184
- package/src/cli/init.test.ts +0 -127
- package/src/cli/size.test.ts +0 -9
- package/src/cli/templates.test.ts +0 -129
- package/src/cli/testing/fixtures.ts +0 -106
- package/src/core/agents/agent-claim.test.ts +0 -142
- package/src/core/agents/agent-dashboard.test.ts +0 -120
- package/src/core/agents/agent-guild.test.ts +0 -124
- package/src/core/agents/agent-store.test.ts +0 -205
- package/src/core/agents/avatar-studio.test.ts +0 -88
- package/src/core/agents/group-round.test.ts +0 -143
- package/src/core/agents/owner-identity.test.ts +0 -172
- package/src/core/agents/team-turns.test.ts +0 -278
- package/src/core/attachments/attachments.test.ts +0 -85
- package/src/core/boundary.test.ts +0 -45
- package/src/core/builtin/modules.test.ts +0 -127
- package/src/core/config/config.test.ts +0 -135
- package/src/core/contract/discord.ts +0 -33
- package/src/core/db/migrations.test.ts +0 -237
- package/src/core/define-roundtable.test.ts +0 -134
- package/src/core/define.test.ts +0 -144
- package/src/core/discord/agent-commands.test.ts +0 -58
- package/src/core/discord/agent-discord.test.ts +0 -70
- package/src/core/discord/dispatch-thread-host.test.ts +0 -114
- package/src/core/discord/dispatch-threads.test.ts +0 -94
- package/src/core/discord/owner-cards.test.ts +0 -435
- package/src/core/discord/owner-discord.test.ts +0 -325
- package/src/core/domain/expression.ts +0 -21
- package/src/core/domain/profile.ts +0 -32
- package/src/core/drain.test.ts +0 -46
- package/src/core/events.test.ts +0 -81
- package/src/core/holds.test.ts +0 -54
- package/src/core/host.test.ts +0 -536
- package/src/core/http/listeners.test.ts +0 -164
- package/src/core/i18n/i18n.test.ts +0 -139
- package/src/core/identity.test.ts +0 -18
- package/src/core/judging/effort-judge.test.ts +0 -112
- package/src/core/judging/model-judge.test.ts +0 -126
- package/src/core/modules/delegation/delegator.test.ts +0 -170
- package/src/core/modules/memory/owner-memory-store.test.ts +0 -244
- package/src/core/modules/schedules/schedule.test.ts +0 -358
- package/src/core/modules/skills/skill-kind.test.ts +0 -55
- package/src/core/modules/skills/skill-registry.test.ts +0 -376
- package/src/core/ops/error-reporter.test.ts +0 -283
- package/src/core/presentation/card-cadence.ts +0 -41
- package/src/core/presentation/presentation.test.ts +0 -166
- package/src/core/public-entry.test.ts +0 -59
- package/src/core/registry/contributions.test.ts +0 -215
- package/src/core/registry/interactions.test.ts +0 -87
- package/src/core/registry/providers.test.ts +0 -103
- package/src/core/routing/channel-queue.test.ts +0 -31
- package/src/core/routing/channel-router.test.ts +0 -326
- package/src/core/routing/conversation-kind.ts +0 -14
- package/src/core/routing/settle-turn.test.ts +0 -21
- package/src/core/runtime/compaction-tiers.test.ts +0 -227
- package/src/core/runtime/extensions/ask-user.test.ts +0 -114
- package/src/core/runtime/extensions/self-compact-guard.test.ts +0 -50
- package/src/core/runtime/pending-confirmation-store.test.ts +0 -52
- package/src/core/runtime/session-archive.test.ts +0 -20
- package/src/core/runtime/steerable-run.test.ts +0 -264
- package/src/core/runtime/text-tools.test.ts +0 -110
- package/src/core/runtime/turn-answer.test.ts +0 -72
- package/src/core/runtime/worker-task.test.ts +0 -82
- package/src/core/services.test.ts +0 -28
- package/src/core/sessions.test.ts +0 -85
- package/src/core/shared/unix-server.ts +0 -18
- package/src/core/size.test.ts +0 -9
- package/src/core/speakers.test.ts +0 -78
- package/src/core/testing/file-size.ts +0 -34
- package/src/core/time.test.ts +0 -166
- package/src/core/tool-tiers.test.ts +0 -117
- package/src/entries.test.ts +0 -112
- package/src/testing.test.ts +0 -156
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type BackgroundTarget,
|
|
2
3
|
type BackgroundTurn,
|
|
3
4
|
type ChannelClaim,
|
|
4
5
|
type ConversationPort,
|
|
@@ -17,8 +18,8 @@ export interface ChannelRouterOptions {
|
|
|
17
18
|
/** Claims in registration order; the router orders them by priority. */
|
|
18
19
|
claims: readonly ChannelClaim[];
|
|
19
20
|
queue: ChannelQueue;
|
|
20
|
-
/**
|
|
21
|
-
|
|
21
|
+
/** The contributed background target of a name, read at use time. */
|
|
22
|
+
targets(name: string): BackgroundTarget | undefined;
|
|
22
23
|
logger: Logger;
|
|
23
24
|
/** How long a bare forward waits for the text sent with it; FORWARD_JOIN_MS by default. */
|
|
24
25
|
forwardJoinMs?: number;
|
|
@@ -53,8 +54,8 @@ export class ChannelRouter implements ConversationPort {
|
|
|
53
54
|
this.#forwards = new ForwardJoin(options.forwardJoinMs);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
#owner(channel: ChannelKey,
|
|
57
|
-
return this.#claims.find((claim) => claim.owns(channel,
|
|
57
|
+
#owner(channel: ChannelKey, space?: string): ChannelClaim | undefined {
|
|
58
|
+
return this.#claims.find((claim) => claim.owns(channel, space));
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
/**
|
|
@@ -74,7 +75,7 @@ export class ChannelRouter implements ConversationPort {
|
|
|
74
75
|
!message.forwarded?.text.trim()
|
|
75
76
|
)
|
|
76
77
|
return;
|
|
77
|
-
const admission = this.#owner(message.channel, message.
|
|
78
|
+
const admission = this.#owner(message.channel, message.space)?.admit(
|
|
78
79
|
message,
|
|
79
80
|
);
|
|
80
81
|
if (!admission) return;
|
|
@@ -108,8 +109,20 @@ export class ChannelRouter implements ConversationPort {
|
|
|
108
109
|
);
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
|
|
112
|
+
target(name: string): BackgroundTarget | undefined {
|
|
113
|
+
return this.#options.targets(name);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A turn nobody wrote, run by the channel's claim inside its queue; never rejects. A turn whose
|
|
118
|
+
* target no plugin contributes is skipped, never handed to another target's claim.
|
|
119
|
+
*/
|
|
112
120
|
async background(turn: BackgroundTurn): Promise<ScheduledOutcome> {
|
|
121
|
+
if (!this.#options.targets(turn.target))
|
|
122
|
+
return {
|
|
123
|
+
status: "skipped",
|
|
124
|
+
reason: `no plugin contributes the background target "${turn.target}"`,
|
|
125
|
+
};
|
|
113
126
|
try {
|
|
114
127
|
return await this.#options.queue.run(turn.channel, async () => {
|
|
115
128
|
const claim = this.#owner(turn.channel);
|
|
@@ -147,8 +160,9 @@ export class ChannelRouter implements ConversationPort {
|
|
|
147
160
|
});
|
|
148
161
|
}
|
|
149
162
|
|
|
163
|
+
/** Asks only the claim that owns the channel; false when none does or it has no `stop`. */
|
|
150
164
|
stop(channel: ChannelKey): boolean {
|
|
151
|
-
return this.#
|
|
165
|
+
return this.#owner(channel)?.stop?.(channel) ?? false;
|
|
152
166
|
}
|
|
153
167
|
|
|
154
168
|
postsInPlace(channel: ChannelKey): boolean {
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { AgentRuntime } from "../contract/runtime.ts";
|
|
2
|
+
import type { SurfacePort } from "../contract/surface.ts";
|
|
3
|
+
import type { TurnAttachments } from "../domain/attachment.ts";
|
|
4
|
+
import type { TurnResult } from "../domain/conversation.ts";
|
|
5
|
+
import { messages } from "../i18n/index.ts";
|
|
6
|
+
import type { Logger } from "../log.ts";
|
|
7
|
+
import type { EventSink } from "../plugin.ts";
|
|
8
|
+
import { splitReply } from "../presentation/reply-splitter.ts";
|
|
9
|
+
import { thinkingLine } from "../presentation/thinking-line.ts";
|
|
10
|
+
import type { ChannelKey, ToolSelection, TurnSelection } from "../sessions.ts";
|
|
11
|
+
import type { Speaker } from "../speakers.ts";
|
|
12
|
+
import { settleTurn } from "./settle-turn.ts";
|
|
13
|
+
|
|
14
|
+
/** One turn of a conversation, as a claim asks for it. */
|
|
15
|
+
export interface ConversationTurnInput {
|
|
16
|
+
/** The conversation's channel; its surface shows typing and the stop control, and posts the reply. */
|
|
17
|
+
channel: ChannelKey;
|
|
18
|
+
/** The conversation's kind: picks the persona of its session, fixed when the session is made. */
|
|
19
|
+
kind: string;
|
|
20
|
+
text: string;
|
|
21
|
+
/** Who the turn is for; its tier limits the tools. */
|
|
22
|
+
speaker: Speaker;
|
|
23
|
+
/** Files and images the message carries. */
|
|
24
|
+
attachments?: TurnAttachments;
|
|
25
|
+
/** The turn's tools; by default the plugins' `agentSelection`, read at the turn. */
|
|
26
|
+
selection?: TurnSelection;
|
|
27
|
+
/** The owner's own chat turn, which their next messages may steer. */
|
|
28
|
+
steerable?: boolean;
|
|
29
|
+
/** The turn may ask the owner on the surface's prompts, such as approvals of held actions. */
|
|
30
|
+
interactive?: boolean;
|
|
31
|
+
/** The owner confirmed the channel's held actions, so they may run in this turn. */
|
|
32
|
+
confirmed?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Posts the turn's outcome in place of the default reply (the text, or a failure or stopped
|
|
35
|
+
* notice); the claim formats its own. A throw is logged, never rethrown.
|
|
36
|
+
*/
|
|
37
|
+
reply?(result: TurnResult): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Runs the turns of conversations a claim owns, through the runtime and the surfaces, so a claim
|
|
42
|
+
* writes no turn pipeline of its own.
|
|
43
|
+
*/
|
|
44
|
+
export interface ConversationTurns {
|
|
45
|
+
/**
|
|
46
|
+
* Runs one turn in a conversation the caller's claim owns; call it inside the claim's queue
|
|
47
|
+
* task. It shows typing and the stop control on the channel's surface, emits `turnStarted` and
|
|
48
|
+
* `turnEnded` with the turn's `kind`, settles a runtime that throws into a failed result, and
|
|
49
|
+
* posts the reply, or a failure or stopped notice, through the surface unless `reply` is given.
|
|
50
|
+
* It rejects during setup (NotLinkedError) and when the host has no runtime to run the turn on.
|
|
51
|
+
*/
|
|
52
|
+
run(input: ConversationTurnInput): Promise<TurnResult>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ConversationTurnsOptions {
|
|
56
|
+
/** Throws NotLinkedError until every plugin is set up and linked. */
|
|
57
|
+
linked: () => void;
|
|
58
|
+
/** The runtime that runs the turn; throws PluginError when the host has none yet. */
|
|
59
|
+
runtime: () => AgentRuntime;
|
|
60
|
+
surfaces: SurfacePort;
|
|
61
|
+
events: EventSink;
|
|
62
|
+
/** The plugins' agent selection, read at each turn that gives none of its own. */
|
|
63
|
+
selection: () => ToolSelection;
|
|
64
|
+
logger: Logger;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The selection of a turn that gives none: the plugins' `agentSelection`. */
|
|
68
|
+
const DEFAULT_SELECTION = "turns";
|
|
69
|
+
|
|
70
|
+
export function conversationTurns(
|
|
71
|
+
options: ConversationTurnsOptions,
|
|
72
|
+
): ConversationTurns {
|
|
73
|
+
const { surfaces, events, logger } = options;
|
|
74
|
+
return {
|
|
75
|
+
async run(input) {
|
|
76
|
+
const { channel, kind, speaker } = input;
|
|
77
|
+
options.linked();
|
|
78
|
+
// A host without a runtime is a setup mistake, so it is refused before anything is shown.
|
|
79
|
+
const runtime = options.runtime();
|
|
80
|
+
const stopTyping = surfaces.startTyping(channel);
|
|
81
|
+
const hideStop = surfaces.showStop(channel);
|
|
82
|
+
const turn = { kind, channel, speaker };
|
|
83
|
+
events.turnStarted(turn);
|
|
84
|
+
let result: TurnResult;
|
|
85
|
+
try {
|
|
86
|
+
result = await settleTurn(
|
|
87
|
+
() =>
|
|
88
|
+
runtime.runTurn({
|
|
89
|
+
channel,
|
|
90
|
+
kind,
|
|
91
|
+
selection: input.selection ?? {
|
|
92
|
+
id: DEFAULT_SELECTION,
|
|
93
|
+
...options.selection(),
|
|
94
|
+
},
|
|
95
|
+
text: input.text,
|
|
96
|
+
speaker,
|
|
97
|
+
...(input.attachments ? { attachments: input.attachments } : {}),
|
|
98
|
+
...(input.confirmed ? { confirmed: true } : {}),
|
|
99
|
+
...(input.steerable ? { steerable: true } : {}),
|
|
100
|
+
...(input.interactive ? { interactive: true } : {}),
|
|
101
|
+
}),
|
|
102
|
+
"conversation turn",
|
|
103
|
+
);
|
|
104
|
+
} finally {
|
|
105
|
+
hideStop();
|
|
106
|
+
}
|
|
107
|
+
events.turnEnded({
|
|
108
|
+
...turn,
|
|
109
|
+
result: result.ok ? "ok" : result.stopped ? "stopped" : "failed",
|
|
110
|
+
});
|
|
111
|
+
try {
|
|
112
|
+
if (!result.ok && !result.stopped)
|
|
113
|
+
logger.error({ channel, kind, err: result.error }, "turn failed");
|
|
114
|
+
if (input.reply) await input.reply(result);
|
|
115
|
+
else await surfaces.sendReply(channel, replyOf(result));
|
|
116
|
+
} catch (error) {
|
|
117
|
+
logger.error({ channel, kind, err: error }, "reply not posted");
|
|
118
|
+
} finally {
|
|
119
|
+
stopTyping();
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The default post: the answer with its thinking line, or the failure or stopped notice. */
|
|
127
|
+
function replyOf(result: TurnResult) {
|
|
128
|
+
if (!result.ok)
|
|
129
|
+
return {
|
|
130
|
+
chunks: [
|
|
131
|
+
result.stopped ? messages().stoppedNotice : messages().failureNotice,
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
const thinking = result.thinking ? thinkingLine(result.thinking) : undefined;
|
|
135
|
+
return {
|
|
136
|
+
...(thinking ? { thinking } : {}),
|
|
137
|
+
chunks: splitReply(result.text),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { parseChannelKey } from "../contract/surface.ts";
|
|
2
|
+
import type { ChannelKey, InboundMessage } from "../domain/conversation.ts";
|
|
2
3
|
import type { OwnerIdentity } from "../identity.ts";
|
|
3
4
|
|
|
5
|
+
/** How the model is told where a forward came from: a Discord channel as its mention, another surface's as its key. */
|
|
6
|
+
function sourceLabel(source: ChannelKey): string {
|
|
7
|
+
const { surface, id } = parseChannelKey(source);
|
|
8
|
+
return surface === "discord" ? `<#${id}>` : source;
|
|
9
|
+
}
|
|
10
|
+
|
|
4
11
|
/** The message text, then the message it replies to and the message it forwards, when there are. */
|
|
5
12
|
export function withReference(
|
|
6
13
|
message: InboundMessage,
|
|
@@ -12,5 +19,5 @@ export function withReference(
|
|
|
12
19
|
: message.text;
|
|
13
20
|
const forwarded = message.forwarded;
|
|
14
21
|
if (!forwarded) return replied;
|
|
15
|
-
return `${replied}\n\n## Forwarded by ${owner.name} from ${forwarded.
|
|
22
|
+
return `${replied}\n\n## Forwarded by ${owner.name} from ${sourceLabel(forwarded.source)} (${forwarded.url})\n${forwarded.text.trim() || "(no text; see the attachments)"}`.trim();
|
|
16
23
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ChatSurface, SurfacePort } from "../contract/surface.ts";
|
|
2
|
+
import { parseChannelKey } from "../contract/surface.ts";
|
|
3
|
+
import { PluginError } from "../errors.ts";
|
|
4
|
+
import type { ChannelKey } from "../sessions.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The port over the contributed surfaces, each chosen by the prefix of a channel's key. `linked`
|
|
8
|
+
* gives the surfaces once every plugin is set up and throws NotLinkedError before.
|
|
9
|
+
*/
|
|
10
|
+
export function surfacePort(linked: () => readonly ChatSurface[]): SurfacePort {
|
|
11
|
+
const of = (channel: ChannelKey): ChatSurface | undefined => {
|
|
12
|
+
const at = channel.indexOf(":");
|
|
13
|
+
if (at <= 0) return undefined;
|
|
14
|
+
const prefix = channel.slice(0, at);
|
|
15
|
+
return linked().find((surface) => surface.surface === prefix);
|
|
16
|
+
};
|
|
17
|
+
const served = (channel: ChannelKey): ChatSurface => {
|
|
18
|
+
const surface = of(channel);
|
|
19
|
+
if (!surface)
|
|
20
|
+
throw new PluginError(
|
|
21
|
+
`no chat surface serves "${parseChannelKey(channel).surface}" channels, such as ${channel}. A plugin contributes one with \`surfaces\`.`,
|
|
22
|
+
);
|
|
23
|
+
return surface;
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
of,
|
|
27
|
+
sendReply: async (channel, reply) =>
|
|
28
|
+
served(channel).sendReply(channel, reply),
|
|
29
|
+
startTyping: (channel) =>
|
|
30
|
+
of(channel)?.startTyping?.(channel) ?? (() => undefined),
|
|
31
|
+
showStop: (channel) =>
|
|
32
|
+
of(channel)?.showStop?.(channel) ?? (() => undefined),
|
|
33
|
+
react: async (channel, messageId, emoji) =>
|
|
34
|
+
of(channel)?.react?.(channel, messageId, emoji),
|
|
35
|
+
unreact: async (channel, messageId, emoji) =>
|
|
36
|
+
of(channel)?.unreact?.(channel, messageId, emoji),
|
|
37
|
+
prompts: (channel, speaker) => of(channel)?.prompts?.(channel, speaker),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -91,7 +91,7 @@ export class ConversationSessions {
|
|
|
91
91
|
*/
|
|
92
92
|
session(
|
|
93
93
|
key: ChannelKey,
|
|
94
|
-
request?: Pick<TurnRequest, "channel" | "agent">,
|
|
94
|
+
request?: Pick<TurnRequest, "channel" | "agent" | "kind">,
|
|
95
95
|
): Promise<ChannelSession> {
|
|
96
96
|
let pending = this.#sessions.get(key);
|
|
97
97
|
if (!pending) {
|
|
@@ -107,6 +107,7 @@ export class ConversationSessions {
|
|
|
107
107
|
this.slot(key),
|
|
108
108
|
ownerAttachmentDir(this.#options.dataDir, channel),
|
|
109
109
|
agent,
|
|
110
|
+
request?.kind ?? "owner",
|
|
110
111
|
))();
|
|
111
112
|
pending.catch(() => this.#sessions.delete(key));
|
|
112
113
|
this.#sessions.set(key, pending);
|
|
@@ -114,7 +115,7 @@ export class ConversationSessions {
|
|
|
114
115
|
return pending;
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
/** The conversation's session, rebuilt with its history when its
|
|
118
|
+
/** The conversation's session, rebuilt with its history when its session tools or skills changed. */
|
|
118
119
|
async freshSession(
|
|
119
120
|
key: ChannelKey,
|
|
120
121
|
request: TurnRequest,
|
|
@@ -137,7 +138,7 @@ export class ConversationSessions {
|
|
|
137
138
|
request: TurnRequest,
|
|
138
139
|
): string | undefined {
|
|
139
140
|
if (channelSession.revisions !== revisionsKey(this.#factory.plan))
|
|
140
|
-
return "
|
|
141
|
+
return "session tools changed";
|
|
141
142
|
if (
|
|
142
143
|
channelSession.skills !== skillsKey(this.#factory.skillsOf(request.agent))
|
|
143
144
|
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { HoldRule } from "../../holds.ts";
|
|
2
|
+
|
|
3
|
+
/** A tool that waits for the owner's confirmation in the gate's tests. */
|
|
4
|
+
export const MAIL = "send-mail";
|
|
5
|
+
|
|
6
|
+
/** The one neutral hold rule the gate's tests use: sending mail waits for the owner. */
|
|
7
|
+
export const MAIL_RULE: HoldRule = {
|
|
8
|
+
name: "mail",
|
|
9
|
+
describe: (tool, input) =>
|
|
10
|
+
tool === MAIL ? `send an email to ${String(input.to)}` : undefined,
|
|
11
|
+
};
|
|
@@ -4,7 +4,6 @@ import type {
|
|
|
4
4
|
PendingConfirmation,
|
|
5
5
|
} from "../../domain/conversation.ts";
|
|
6
6
|
import type { OwnerPrompts } from "../../domain/owner-prompts.ts";
|
|
7
|
-
import type { ProfileId } from "../../domain/profile.ts";
|
|
8
7
|
import type { HoldCheck } from "../../holds.ts";
|
|
9
8
|
import { messages } from "../../i18n/index.ts";
|
|
10
9
|
import { type OwnerIdentity, ownerWords } from "../../identity.ts";
|
|
@@ -46,15 +45,15 @@ function byCodeUnit(a: string, b: string): number {
|
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
/**
|
|
49
|
-
* Per-channel state of held actions. With the owner's prompts bound, a call that needs
|
|
50
|
-
* confirmation waits for
|
|
48
|
+
* Per-channel state of held actions. With the owner's prompts bound, a call that needs their
|
|
49
|
+
* confirmation waits for their answer on a card and runs or is refused in the same turn. Otherwise,
|
|
51
50
|
* or when the card expires, the call is held: it records its tool and exact input, and the
|
|
52
51
|
* owner's next message either confirms them, which lets each identical call run once in that
|
|
53
52
|
* turn, or drops them.
|
|
54
53
|
*/
|
|
55
54
|
// pi-lens-ignore: large-class — one gate per conversation; its state is the held calls and their approvals
|
|
56
55
|
export class ConfirmationGate {
|
|
57
|
-
#
|
|
56
|
+
#selectionId: string | undefined;
|
|
58
57
|
#pending: PendingConfirmation | undefined;
|
|
59
58
|
#approved: HeldCall[] = [];
|
|
60
59
|
readonly #holds: HoldCheck;
|
|
@@ -94,11 +93,11 @@ export class ConfirmationGate {
|
|
|
94
93
|
|
|
95
94
|
/** `addressee` is who the turn is for; without one, the owner. */
|
|
96
95
|
beginTurn(
|
|
97
|
-
|
|
96
|
+
selectionId: string,
|
|
98
97
|
confirmed: boolean,
|
|
99
98
|
addressee?: OwnerIdentity,
|
|
100
99
|
): void {
|
|
101
|
-
this.#
|
|
100
|
+
this.#selectionId = selectionId;
|
|
102
101
|
this.#addressee = addressee ?? this.#owner;
|
|
103
102
|
this.#approved = confirmed ? [...(this.pending()?.calls ?? [])] : [];
|
|
104
103
|
this.#pending = undefined;
|
|
@@ -115,8 +114,8 @@ export class ConfirmationGate {
|
|
|
115
114
|
}
|
|
116
115
|
|
|
117
116
|
/**
|
|
118
|
-
* Like `hold`, but asks the owner on a card first when `prompts` is given:
|
|
119
|
-
* the call,
|
|
117
|
+
* Like `hold`, but asks the owner on a card first when `prompts` is given: their approval runs
|
|
118
|
+
* the call, their refusal blocks it, and only an expired card holds it.
|
|
120
119
|
*/
|
|
121
120
|
async review(
|
|
122
121
|
tool: string,
|
|
@@ -175,8 +174,11 @@ export class ConfirmationGate {
|
|
|
175
174
|
}
|
|
176
175
|
|
|
177
176
|
#record(call: HeldCall): string {
|
|
177
|
+
const selectionId = this.#selectionId;
|
|
178
|
+
if (selectionId === undefined)
|
|
179
|
+
throw new Error("a call was held outside a turn");
|
|
178
180
|
const pending = this.#pending ?? {
|
|
179
|
-
|
|
181
|
+
selectionId,
|
|
180
182
|
heldAt: new Date(),
|
|
181
183
|
calls: [],
|
|
182
184
|
};
|
package/src/core/runtime/mcp.ts
CHANGED
|
@@ -8,7 +8,7 @@ export interface VirtualServer {
|
|
|
8
8
|
tools: string[];
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
/**
|
|
11
|
+
/** MCP endpoints supplied by a session plugin, reached with its bearer token. */
|
|
12
12
|
export function mcpExtension(
|
|
13
13
|
servers: readonly VirtualServer[],
|
|
14
14
|
token: string,
|
|
@@ -5,10 +5,9 @@ import type {
|
|
|
5
5
|
HeldCall,
|
|
6
6
|
PendingConfirmation,
|
|
7
7
|
} from "../domain/conversation.ts";
|
|
8
|
-
import type { ProfileId } from "../domain/profile.ts";
|
|
9
8
|
|
|
10
9
|
interface Row {
|
|
11
|
-
|
|
10
|
+
selection_id: string;
|
|
12
11
|
held_at: Date;
|
|
13
12
|
calls: string;
|
|
14
13
|
}
|
|
@@ -23,12 +22,12 @@ export class PendingConfirmationStore {
|
|
|
23
22
|
|
|
24
23
|
/** The store's tables; the host runs this before any store attaches. */
|
|
25
24
|
static readonly migration: Migration = {
|
|
26
|
-
name: "
|
|
25
|
+
name: "held-actions",
|
|
27
26
|
up: async (sql) => {
|
|
28
27
|
await sql`
|
|
29
|
-
CREATE TABLE IF NOT EXISTS
|
|
28
|
+
CREATE TABLE IF NOT EXISTS held_actions (
|
|
30
29
|
channel_key text PRIMARY KEY,
|
|
31
|
-
|
|
30
|
+
selection_id text NOT NULL,
|
|
32
31
|
held_at timestamptz NOT NULL,
|
|
33
32
|
calls text NOT NULL
|
|
34
33
|
)`;
|
|
@@ -42,12 +41,12 @@ export class PendingConfirmationStore {
|
|
|
42
41
|
|
|
43
42
|
async load(channel: ChannelKey): Promise<PendingConfirmation | undefined> {
|
|
44
43
|
const rows: Row[] = await this.#sql`
|
|
45
|
-
SELECT
|
|
44
|
+
SELECT selection_id, held_at, calls FROM held_actions
|
|
46
45
|
WHERE channel_key = ${channel}`;
|
|
47
46
|
const row = rows[0];
|
|
48
47
|
return row
|
|
49
48
|
? {
|
|
50
|
-
|
|
49
|
+
selectionId: row.selection_id,
|
|
51
50
|
heldAt: row.held_at,
|
|
52
51
|
// pi-lens-ignore: unchecked-throwing-call — this store wrote the JSON; a corrupt row should fail loudly
|
|
53
52
|
calls: JSON.parse(row.calls) as HeldCall[],
|
|
@@ -61,15 +60,14 @@ export class PendingConfirmationStore {
|
|
|
61
60
|
pending: PendingConfirmation | undefined,
|
|
62
61
|
): Promise<void> {
|
|
63
62
|
if (!pending) {
|
|
64
|
-
await this
|
|
65
|
-
.#sql`DELETE FROM pending_confirmations WHERE channel_key = ${channel}`;
|
|
63
|
+
await this.#sql`DELETE FROM held_actions WHERE channel_key = ${channel}`;
|
|
66
64
|
return;
|
|
67
65
|
}
|
|
68
66
|
await this.#sql`
|
|
69
|
-
INSERT INTO
|
|
70
|
-
VALUES (${channel}, ${pending.
|
|
67
|
+
INSERT INTO held_actions (channel_key, selection_id, held_at, calls)
|
|
68
|
+
VALUES (${channel}, ${pending.selectionId}, ${pending.heldAt},
|
|
71
69
|
${JSON.stringify(pending.calls)})
|
|
72
|
-
ON CONFLICT (channel_key) DO UPDATE SET
|
|
70
|
+
ON CONFLICT (channel_key) DO UPDATE SET selection_id = EXCLUDED.selection_id,
|
|
73
71
|
held_at = EXCLUDED.held_at, calls = EXCLUDED.calls`;
|
|
74
72
|
}
|
|
75
73
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { ownerAttachmentDir } from "../attachments/attachment-dir.ts";
|
|
9
9
|
import { withAttachmentsBlock } from "../attachments/prompt-block.ts";
|
|
10
|
+
import type { AgentRuntime } from "../contract/runtime.ts";
|
|
10
11
|
import { NO_ATTACHMENTS, type TurnAttachments } from "../domain/attachment.ts";
|
|
11
12
|
import type {
|
|
12
13
|
ChannelKey,
|
|
@@ -15,7 +16,7 @@ import type {
|
|
|
15
16
|
TurnResult,
|
|
16
17
|
} from "../domain/conversation.ts";
|
|
17
18
|
import { AgentRunError, ConfigError } from "../domain/errors.ts";
|
|
18
|
-
import type {
|
|
19
|
+
import type { TurnRequest } from "../domain/ports.ts";
|
|
19
20
|
import { assistantName } from "../i18n/index.ts";
|
|
20
21
|
import {
|
|
21
22
|
AUTO_THINKING,
|
|
@@ -99,13 +100,14 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
99
100
|
new PromptSlot(),
|
|
100
101
|
join(this.#factory.workDir(), "probe-attachments"),
|
|
101
102
|
undefined,
|
|
103
|
+
"owner",
|
|
102
104
|
);
|
|
103
105
|
const registered = new Set(
|
|
104
106
|
probe.session.getAllTools().map((tool) => tool.name),
|
|
105
107
|
);
|
|
106
108
|
probe.session.dispose();
|
|
107
109
|
const expected = [
|
|
108
|
-
...this.#
|
|
110
|
+
...this.#factory.link().requiredTools,
|
|
109
111
|
COMPACT_TOOL,
|
|
110
112
|
ASK_USER_TOOL,
|
|
111
113
|
...planOrder(this.#factory.plan).flatMap(
|
|
@@ -115,7 +117,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
115
117
|
const missing = expected.filter((name) => !registered.has(name));
|
|
116
118
|
if (missing.length > 0) {
|
|
117
119
|
throw new ConfigError(
|
|
118
|
-
`
|
|
120
|
+
`required tools are not registered: ${missing.join(", ")}`,
|
|
119
121
|
);
|
|
120
122
|
}
|
|
121
123
|
}
|
|
@@ -152,10 +154,10 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
152
154
|
logger.warn(
|
|
153
155
|
{
|
|
154
156
|
channel: request.channel,
|
|
155
|
-
|
|
157
|
+
selection: request.selection.id,
|
|
156
158
|
missing,
|
|
157
159
|
},
|
|
158
|
-
"
|
|
160
|
+
"selected tools missing; running without them",
|
|
159
161
|
);
|
|
160
162
|
}
|
|
161
163
|
channelSession.tools = wanted.filter((name) => registered.has(name));
|
|
@@ -256,7 +258,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
256
258
|
logger.info(
|
|
257
259
|
{
|
|
258
260
|
channel: key,
|
|
259
|
-
|
|
261
|
+
selection: request.selection.id,
|
|
260
262
|
toolCalls,
|
|
261
263
|
thinking: session.thinkingLevel,
|
|
262
264
|
// pi-lens-ignore: no-conditional-empty-object-spread — owner turns keep their log line without a model key
|
|
@@ -326,6 +328,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
326
328
|
new PromptSlot(),
|
|
327
329
|
ownerAttachmentDir(dataDir, channel),
|
|
328
330
|
undefined,
|
|
331
|
+
"owner",
|
|
329
332
|
);
|
|
330
333
|
const { session } = worker;
|
|
331
334
|
const toolCalls: string[] = [];
|
|
@@ -2,10 +2,10 @@ import type { OwnerPrompts, OwnerQuestion } from "../domain/owner-prompts.ts";
|
|
|
2
2
|
import { assistantName } from "../i18n/index.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* A run's way to ask the owner while it works. A turn
|
|
5
|
+
* A run's way to ask the owner while it works. A turn they started or a report turn binds the
|
|
6
6
|
* cards of its channel, and a coding worker those of its dispatch thread; a schedule's turn, or
|
|
7
|
-
* a session with no Discord channel, leaves the slot empty and held actions wait for
|
|
8
|
-
* message instead. The slot also counts how long the run waited on
|
|
7
|
+
* a session with no Discord channel, leaves the slot empty and held actions wait for their next
|
|
8
|
+
* message instead. The slot also counts how long the run waited on them, which its time limit
|
|
9
9
|
* leaves out.
|
|
10
10
|
*/
|
|
11
11
|
export class PromptSlot {
|
|
@@ -58,6 +58,11 @@ export class PromptSlot {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/** A fresh slot, for a run that asks the owner while it works. */
|
|
62
|
+
export function promptSlot(): PromptSlot {
|
|
63
|
+
return new PromptSlot();
|
|
64
|
+
}
|
|
65
|
+
|
|
61
66
|
/**
|
|
62
67
|
* Calls `expire` once the run has worked `ms`, not counting time the slot spent waiting on the
|
|
63
68
|
* owner's cards; returns a function that cancels it.
|
|
@@ -4,16 +4,12 @@ import type {
|
|
|
4
4
|
ExtensionFactory,
|
|
5
5
|
ModelRuntime,
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { AgentSessions, LoadedSkill } from "../contract/runtime.ts";
|
|
7
8
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
8
9
|
import type { OwnerPrompts } from "../domain/owner-prompts.ts";
|
|
9
|
-
import type { AgentTurnScope } from "../domain/ports.ts";
|
|
10
10
|
import type { OwnerIdentity } from "../identity.ts";
|
|
11
11
|
import type { Logger } from "../log.ts";
|
|
12
|
-
import type {
|
|
13
|
-
ThinkingLevel,
|
|
14
|
-
ThinkingPicker,
|
|
15
|
-
ThinkingSetting,
|
|
16
|
-
} from "../models.ts";
|
|
12
|
+
import type { ThinkingLevel, ThinkingPicker } from "../models.ts";
|
|
17
13
|
import type { LinkedSessions } from "../plugin.ts";
|
|
18
14
|
import {
|
|
19
15
|
planOrder,
|
|
@@ -39,7 +35,7 @@ export interface PiAgentRuntimeOptions {
|
|
|
39
35
|
/** Who the conversations serve, as prompts and tool results name them. */
|
|
40
36
|
owner: OwnerIdentity;
|
|
41
37
|
agentDir: string;
|
|
42
|
-
/** Shared
|
|
38
|
+
/** Shared by the host's sessions, so logins refresh in one place. */
|
|
43
39
|
modelRuntime: ModelRuntime;
|
|
44
40
|
dataDir: string;
|
|
45
41
|
model: { provider: string; id: string };
|
|
@@ -47,7 +43,6 @@ export interface PiAgentRuntimeOptions {
|
|
|
47
43
|
thinking: ThinkingLevel;
|
|
48
44
|
/** Picks the level of the assistant's turns and of agents whose thinking is `auto`. */
|
|
49
45
|
effort: ThinkingPicker;
|
|
50
|
-
persona: string;
|
|
51
46
|
/** Held actions survive a restart here, so a confirmation after one still runs. */
|
|
52
47
|
confirmations: Pick<PendingConfirmationStore, "load" | "save">;
|
|
53
48
|
logger: Logger;
|
|
@@ -57,13 +52,11 @@ export interface PiAgentRuntimeOptions {
|
|
|
57
52
|
* A changed snapshot revision rebuilds each session on its next turn, keeping its history.
|
|
58
53
|
*/
|
|
59
54
|
sessions: () => LinkedSessions;
|
|
60
|
-
/** Tools startup refuses to run without, besides those the session tools require. */
|
|
61
|
-
requiredTools: readonly string[];
|
|
62
55
|
/** The agent server's sessions: their shared workspace, model, and skills. */
|
|
63
56
|
agents?: AgentSessions;
|
|
64
57
|
/**
|
|
65
58
|
* Cards the owner answers in a channel, for interactive turns; undefined for a channel that
|
|
66
|
-
* cannot show them. Without it every held action waits for
|
|
59
|
+
* cannot show them. Without it every held action waits for their next message.
|
|
67
60
|
*/
|
|
68
61
|
prompts?: (
|
|
69
62
|
channel: ChannelKey,
|
|
@@ -77,26 +70,7 @@ export interface PiAgentRuntimeOptions {
|
|
|
77
70
|
mcpConnectTimeoutMs?: number;
|
|
78
71
|
}
|
|
79
72
|
|
|
80
|
-
export
|
|
81
|
-
/** The shell's working directory, shared by every agent; writes outside it are held. */
|
|
82
|
-
workDir: string;
|
|
83
|
-
/**
|
|
84
|
-
* The skills the agent carries, read at the start of every run; a change rebuilds its
|
|
85
|
-
* sessions, keeping their history (repos-and-skills spec behavior 21).
|
|
86
|
-
*/
|
|
87
|
-
skills(name: string): readonly LoadedSkill[];
|
|
88
|
-
/** The agent's model (`<provider>/<id>`) and thinking setting, read at the start of every run. */
|
|
89
|
-
modelOf(name: string): { model: string; thinking: ThinkingSetting };
|
|
90
|
-
/** The channel the scope's turns run in: the group's for a seat in one, else the agent's own. */
|
|
91
|
-
turnChannel(scope: AgentTurnScope): ChannelKey;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** A skill file a session loads; only its name and description enter the prompt. */
|
|
95
|
-
export interface LoadedSkill {
|
|
96
|
-
name: string;
|
|
97
|
-
description: string;
|
|
98
|
-
file: string;
|
|
99
|
-
}
|
|
73
|
+
export type { AgentSessions, LoadedSkill };
|
|
100
74
|
|
|
101
75
|
export const skillsKey = (skills: readonly LoadedSkill[]) =>
|
|
102
76
|
JSON.stringify(skills);
|
|
@@ -109,12 +83,12 @@ export interface CoreExtensions {
|
|
|
109
83
|
confirmationGate: ExtensionFactory;
|
|
110
84
|
askUser: ExtensionFactory;
|
|
111
85
|
selfCompactGuard: ExtensionFactory;
|
|
112
|
-
|
|
86
|
+
activeTools: ExtensionFactory;
|
|
113
87
|
}
|
|
114
88
|
|
|
115
89
|
/**
|
|
116
90
|
* A session's extensions in load order: the tools phase, the core's attachment, gate, ask-user,
|
|
117
|
-
* and compact guard, the compactor, the MCP phase, and
|
|
91
|
+
* and compact guard, the compactor, the MCP phase, and active-tools last, so its
|
|
118
92
|
* before_agent_start handler runs after every other extension's.
|
|
119
93
|
*/
|
|
120
94
|
export function sessionExtensions(
|
|
@@ -135,13 +109,13 @@ export function sessionExtensions(
|
|
|
135
109
|
{ name: "self-compact-guard", factory: core.selfCompactGuard },
|
|
136
110
|
...contributed(plan.compaction ? [plan.compaction] : []),
|
|
137
111
|
...contributed(plan.mcp),
|
|
138
|
-
{ name: "
|
|
112
|
+
{ name: "active-tools", factory: core.activeTools },
|
|
139
113
|
];
|
|
140
114
|
}
|
|
141
115
|
|
|
142
116
|
export interface ChannelSession {
|
|
143
117
|
session: AgentSession;
|
|
144
|
-
/** Read by the
|
|
118
|
+
/** Read by the active-tools extension at the start of every run. */
|
|
145
119
|
tools: readonly string[];
|
|
146
120
|
/** The session tools' revisions the session was built with, as revisionsKey. */
|
|
147
121
|
revisions: string;
|