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,5 +1,4 @@
|
|
|
1
1
|
import { rmSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
2
|
import {
|
|
4
3
|
type AgentSession,
|
|
5
4
|
type ModelRuntime,
|
|
@@ -15,38 +14,47 @@ import type {
|
|
|
15
14
|
TranscriptEntry,
|
|
16
15
|
TurnResult,
|
|
17
16
|
} from "../domain/conversation.ts";
|
|
18
|
-
import { AgentRunError
|
|
17
|
+
import { AgentRunError } from "../domain/errors.ts";
|
|
19
18
|
import type { TurnRequest } from "../domain/ports.ts";
|
|
20
19
|
import { assistantName } from "../i18n/index.ts";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
type ThinkingLevel,
|
|
25
|
-
type ThinkingSetting,
|
|
26
|
-
} from "../models.ts";
|
|
20
|
+
import { promptScopeOf } from "../interactions/prompts.ts";
|
|
21
|
+
import { AUTO_THINKING, type ThinkingLevel } from "../models.ts";
|
|
22
|
+
import { MEMORY_TOOLS } from "../modules/memory/owner-memory.ts";
|
|
27
23
|
import { withoutReplyFiles } from "../reply-files.ts";
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import { addressee, type Speaker, type Tier } from "../speakers.ts";
|
|
24
|
+
import type { TransientTask } from "../sessions.ts";
|
|
25
|
+
import type { Speaker, Tier } from "../speakers.ts";
|
|
31
26
|
import { type ToolTiers, toolsForTier, toolTiers } from "../tool-tiers.ts";
|
|
32
|
-
import { ConversationSessions } from "./conversation-sessions.ts";
|
|
33
|
-
import { ASK_USER_TOOL } from "./extensions/ask-user.ts";
|
|
34
27
|
import {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
type AgentModelSettings,
|
|
29
|
+
bridgeTurnRefusal,
|
|
30
|
+
chosenAgentModel,
|
|
31
|
+
useChosenAgentModel,
|
|
32
|
+
} from "./bridge-guard.ts";
|
|
33
|
+
import { ConversationSessions } from "./conversation-sessions.ts";
|
|
34
|
+
import { confirmedTurnText } from "./extensions/confirmation-gate.ts";
|
|
35
|
+
import { loadsMemory } from "./extensions/private-memory.ts";
|
|
36
|
+
import { missingToolsError } from "./extensions/self-compact-guard.ts";
|
|
39
37
|
import { interimPoster } from "./interim-text.ts";
|
|
38
|
+
import { preflightTools } from "./preflight-tools.ts";
|
|
40
39
|
import { PromptSlot, workTimeout } from "./prompt-slot.ts";
|
|
41
|
-
import {
|
|
42
|
-
|
|
43
|
-
TRANSCRIPT_ENTRY_CHARS,
|
|
44
|
-
type TurnMessage,
|
|
45
|
-
} from "./runtime-types.ts";
|
|
40
|
+
import { carriesMemory, recordMemoryTurn } from "./reader-history.ts";
|
|
41
|
+
import type { PiAgentRuntimeOptions, TurnMessage } from "./runtime-types.ts";
|
|
46
42
|
import { archiveSessions } from "./session-archive.ts";
|
|
43
|
+
import {
|
|
44
|
+
memoryReader,
|
|
45
|
+
refusedSpeaker,
|
|
46
|
+
turnAddressee,
|
|
47
|
+
} from "./session-conversation.ts";
|
|
47
48
|
import { SessionFactory } from "./session-factory.ts";
|
|
48
|
-
import {
|
|
49
|
-
import {
|
|
49
|
+
import { promptImages, SteerableRun } from "./steerable-run.ts";
|
|
50
|
+
import { runningCalls } from "./tool-call-scope.ts";
|
|
51
|
+
import {
|
|
52
|
+
lastReply,
|
|
53
|
+
transcriptOf,
|
|
54
|
+
turnAnswer,
|
|
55
|
+
unspokenTurn,
|
|
56
|
+
} from "./turn-answer.ts";
|
|
57
|
+
import { progressReporter } from "./turn-progress.ts";
|
|
50
58
|
import { workerReport } from "./worker-task.ts";
|
|
51
59
|
|
|
52
60
|
export {
|
|
@@ -56,6 +64,8 @@ export {
|
|
|
56
64
|
sessionExtensions,
|
|
57
65
|
} from "./runtime-types.ts";
|
|
58
66
|
|
|
67
|
+
const MEMORY = new Set<string>(MEMORY_TOOLS);
|
|
68
|
+
|
|
59
69
|
/** AgentRuntime over the Pi SDK: one persistent session per channel, each with its own loader. */
|
|
60
70
|
const STOPPED = "stopped by the owner";
|
|
61
71
|
|
|
@@ -69,10 +79,11 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
69
79
|
/** The tier and speaker of each conversation's running turn. */
|
|
70
80
|
readonly #turns = new Map<
|
|
71
81
|
ChannelKey,
|
|
72
|
-
{ tier: Tier; speaker: Speaker
|
|
82
|
+
{ tier: Tier; speaker: Speaker; withheld: boolean }
|
|
73
83
|
>();
|
|
74
84
|
/** Each conversation's turn in progress, which the owner may steer or stop. */
|
|
75
85
|
readonly #running = new Map<ChannelKey, SteerableRun>();
|
|
86
|
+
readonly #memoryTurns = new Map<ChannelKey, () => void>();
|
|
76
87
|
|
|
77
88
|
/** Builds the runtime without starting a session; the preflight proves it can run. */
|
|
78
89
|
constructor(options: PiAgentRuntimeOptions) {
|
|
@@ -81,7 +92,8 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
81
92
|
this.#tiers = options.toolTiers ?? toolTiers();
|
|
82
93
|
this.#factory = new SessionFactory(options, {
|
|
83
94
|
speaker: (channel) => this.#turns.get(channel)?.speaker,
|
|
84
|
-
|
|
95
|
+
withholdsMemory: (channel) => this.#turns.get(channel)?.withheld === true,
|
|
96
|
+
runTask: (scope, task) => this.#runTask(scope, task),
|
|
85
97
|
});
|
|
86
98
|
this.#sessions = new ConversationSessions(
|
|
87
99
|
options,
|
|
@@ -90,54 +102,64 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
90
102
|
);
|
|
91
103
|
}
|
|
92
104
|
|
|
93
|
-
/**
|
|
94
|
-
* Proves the model and every required tool resolve, in a throwaway session that runs no turn;
|
|
95
|
-
* startup stops here before anything connects.
|
|
96
|
-
*/
|
|
105
|
+
/** Resolves the model and required tools in throwaway shared and private sessions, without a turn. */
|
|
97
106
|
async preflight(): Promise<void> {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
join(this.#factory.workDir(), "probe-attachments"),
|
|
104
|
-
undefined,
|
|
105
|
-
"owner",
|
|
106
|
-
);
|
|
107
|
-
const registered = new Set(
|
|
108
|
-
probe.session.getAllTools().map((tool) => tool.name),
|
|
107
|
+
const required = this.#factory.requiredTools();
|
|
108
|
+
const registered = await preflightTools(
|
|
109
|
+
this.#factory,
|
|
110
|
+
this.#options.owner,
|
|
111
|
+
required,
|
|
109
112
|
);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
...this.#factory.link().requiredTools,
|
|
113
|
-
COMPACT_TOOL,
|
|
114
|
-
ASK_USER_TOOL,
|
|
115
|
-
...planOrder(this.#factory.plan).flatMap(
|
|
116
|
-
(tool) => tool.snapshot().requiredTools ?? [],
|
|
117
|
-
),
|
|
118
|
-
];
|
|
119
|
-
const missing = expected.filter((name) => !registered.has(name));
|
|
120
|
-
if (missing.length > 0) {
|
|
121
|
-
throw new ConfigError(
|
|
122
|
-
`required tools are not registered: ${missing.join(", ")}`,
|
|
123
|
-
);
|
|
124
|
-
}
|
|
113
|
+
const missing = required.filter((name) => !registered.has(name));
|
|
114
|
+
if (missing.length > 0) throw missingToolsError(missing);
|
|
125
115
|
}
|
|
126
116
|
|
|
127
117
|
// pi-lens-ignore: high-complexity, high-fan-out, mixed-async-styles — one turn's lifecycle end to end; batch 4 splits it with the channel router
|
|
128
118
|
async runTurn(request: TurnRequest): Promise<TurnResult> {
|
|
129
119
|
const { logger, turnTimeoutMs = 10 * 60_000 } = this.#options;
|
|
120
|
+
if (!request.speaker) return unspokenTurn();
|
|
130
121
|
const key = request.agent?.session ?? request.channel;
|
|
131
|
-
|
|
122
|
+
// Resolve current scope; refuse a foreign private speaker before touching its session.
|
|
123
|
+
const conversation = await this.#sessions.conversation(key, request);
|
|
124
|
+
const refused = refusedSpeaker(conversation, request.speaker);
|
|
125
|
+
if (refused) return { ok: false, error: new AgentRunError(refused) };
|
|
126
|
+
const scoped = { ...request, conversation };
|
|
127
|
+
const channelSession = await this.#sessions.freshSession(key, scoped);
|
|
128
|
+
const who = turnAddressee(
|
|
129
|
+
request.speaker,
|
|
130
|
+
channelSession,
|
|
131
|
+
this.#options.owner,
|
|
132
|
+
);
|
|
132
133
|
const { session } = channelSession;
|
|
133
|
-
const
|
|
134
|
+
const agentModel = chosenAgentModel(request.agent?.name, this.#options);
|
|
135
|
+
const bridgeRefused =
|
|
136
|
+
conversation.visibility === "shared"
|
|
137
|
+
? await bridgeTurnRefusal(
|
|
138
|
+
request.agent?.name,
|
|
139
|
+
this.#options,
|
|
140
|
+
{
|
|
141
|
+
messages: session.messages,
|
|
142
|
+
sessionManager: session.sessionManager,
|
|
143
|
+
reader: request.speaker.principalId,
|
|
144
|
+
},
|
|
145
|
+
agentModel,
|
|
146
|
+
)
|
|
147
|
+
: undefined;
|
|
148
|
+
if (bridgeRefused)
|
|
149
|
+
return { ok: false, error: new AgentRunError(bridgeRefused) };
|
|
150
|
+
const level = await this.#thinkingLevel(key, session, request, agentModel);
|
|
134
151
|
if (session.thinkingLevel !== level) session.setThinkingLevel(level);
|
|
135
152
|
const gate = await this.#sessions.gate(key, request.agent !== undefined);
|
|
136
153
|
const pending = gate.pending();
|
|
137
154
|
const registered = new Set(session.getAllTools().map((tool) => tool.name));
|
|
138
|
-
// The speaker's tier limits the tools
|
|
139
|
-
const tier = request.speaker
|
|
155
|
+
// The speaker's tier limits the tools.
|
|
156
|
+
const { tier } = request.speaker;
|
|
140
157
|
// An approving turn also has the held calls' tools, which a dispatched worker may have used.
|
|
158
|
+
// The agent server may keep a speaker below the owner tier out of memory: no tools, no block.
|
|
159
|
+
const withheld =
|
|
160
|
+
request.agent !== undefined &&
|
|
161
|
+
this.#options.agents?.memory === "owners" &&
|
|
162
|
+
tier !== "owner";
|
|
141
163
|
const wanted = toolsForTier(
|
|
142
164
|
[
|
|
143
165
|
...new Set([
|
|
@@ -149,8 +171,17 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
149
171
|
],
|
|
150
172
|
tier,
|
|
151
173
|
this.#tiers,
|
|
174
|
+
).filter((name) => !withheld || !MEMORY.has(name));
|
|
175
|
+
const markPrivateMemory = recordMemoryTurn(
|
|
176
|
+
session.sessionManager,
|
|
177
|
+
request.speaker.principalId,
|
|
178
|
+
!withheld &&
|
|
179
|
+
channelSession.memory !== "none" &&
|
|
180
|
+
memoryReader(conversation, request.speaker) !== undefined &&
|
|
181
|
+
loadsMemory(registered),
|
|
152
182
|
);
|
|
153
|
-
this.#
|
|
183
|
+
this.#memoryTurns.set(key, markPrivateMemory);
|
|
184
|
+
this.#turns.set(key, { tier, speaker: request.speaker, withheld });
|
|
154
185
|
const missing = wanted.filter((name) => !registered.has(name));
|
|
155
186
|
if (missing.length > 0) {
|
|
156
187
|
logger.warn(
|
|
@@ -167,7 +198,8 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
167
198
|
gate.beginTurn(
|
|
168
199
|
request.selection.id,
|
|
169
200
|
request.confirmed === true,
|
|
170
|
-
|
|
201
|
+
who,
|
|
202
|
+
request.speaker,
|
|
171
203
|
);
|
|
172
204
|
if (request.confirmed && pending) {
|
|
173
205
|
logger.info(
|
|
@@ -184,12 +216,18 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
184
216
|
const toolCalls: string[] = [];
|
|
185
217
|
// Text written before the final answer is posted as the turn goes; the final reply stays the caller's.
|
|
186
218
|
const interim = interimPoster(request, this.#options);
|
|
219
|
+
// The caller's live view: the text as it streams and each tool, never the thinking.
|
|
220
|
+
const progress = request.progress
|
|
221
|
+
? progressReporter(request.progress)
|
|
222
|
+
: undefined;
|
|
187
223
|
const unsubscribe = session.subscribe((event) => {
|
|
224
|
+
progress?.observe(event);
|
|
188
225
|
if (event.type === "tool_execution_start") {
|
|
189
226
|
toolCalls.push(event.toolName);
|
|
190
227
|
interim?.toolStart(event.toolName);
|
|
191
228
|
}
|
|
192
229
|
if (event.type === "message_end") {
|
|
230
|
+
if (carriesMemory(event.message)) markPrivateMemory();
|
|
193
231
|
turnMessages.push(event.message);
|
|
194
232
|
interim?.messageEnd(event.message);
|
|
195
233
|
}
|
|
@@ -197,7 +235,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
197
235
|
const slot = this.#sessions.slot(key);
|
|
198
236
|
slot.bind(
|
|
199
237
|
request.interactive
|
|
200
|
-
? this.#options.prompts?.(request.channel,
|
|
238
|
+
? this.#options.prompts?.(request.channel, promptScopeOf(scoped))
|
|
201
239
|
: undefined,
|
|
202
240
|
request.agent?.name ?? assistantName(),
|
|
203
241
|
interim ? () => interim.flush() : undefined,
|
|
@@ -220,11 +258,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
220
258
|
const attachments = request.attachments ?? NO_ATTACHMENTS;
|
|
221
259
|
const text =
|
|
222
260
|
request.confirmed && pending
|
|
223
|
-
? confirmedTurnText(
|
|
224
|
-
pending,
|
|
225
|
-
request.text,
|
|
226
|
-
addressee(request.speaker, this.#options.owner),
|
|
227
|
-
)
|
|
261
|
+
? confirmedTurnText(pending, request.text, who)
|
|
228
262
|
: request.text;
|
|
229
263
|
await running.run(() =>
|
|
230
264
|
session.prompt(
|
|
@@ -244,9 +278,11 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
244
278
|
} finally {
|
|
245
279
|
this.#running.delete(key);
|
|
246
280
|
this.#turns.delete(key);
|
|
281
|
+
this.#memoryTurns.delete(key);
|
|
247
282
|
cancelTimeout();
|
|
248
283
|
slot.unbind();
|
|
249
284
|
unsubscribe();
|
|
285
|
+
progress?.close();
|
|
250
286
|
await interim?.flush();
|
|
251
287
|
gate.endTurn();
|
|
252
288
|
const usage = session.getContextUsage();
|
|
@@ -324,23 +360,33 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
324
360
|
return stopped;
|
|
325
361
|
}
|
|
326
362
|
|
|
327
|
-
/**
|
|
328
|
-
* Runs a task in a fresh, unsaved session with the selected tools and the channel's
|
|
329
|
-
* confirmation gate, and returns its final text.
|
|
330
|
-
*/
|
|
363
|
+
/** Runs a task in an unsaved session with the selected tools and conversation's gate. */
|
|
331
364
|
// pi-lens-ignore: high-fan-out — builds, runs, and disposes one transient session; batch 4 turns it into the core's child-run API
|
|
332
|
-
async #runTask(
|
|
365
|
+
async #runTask(
|
|
366
|
+
scope: { turn: ChannelKey; home: ChannelKey },
|
|
367
|
+
task: TransientTask,
|
|
368
|
+
): Promise<string> {
|
|
333
369
|
const { dataDir } = this.#options;
|
|
334
370
|
const { signal } = task;
|
|
335
|
-
// A
|
|
371
|
+
// A task works at the tier of the turn that started it; without one there is no tier to take.
|
|
372
|
+
const turn = this.#turns.get(scope.turn);
|
|
373
|
+
if (!turn)
|
|
374
|
+
throw new AgentRunError(
|
|
375
|
+
`a task runs beside a turn of its conversation, at that turn's tier, and ${scope.turn} has no turn running`,
|
|
376
|
+
);
|
|
377
|
+
// A worker inherits the starting turn's conversation and memory policy, without asking.
|
|
378
|
+
const parent = await this.#sessions.session(scope.turn);
|
|
379
|
+
const { conversation, memory } = parent;
|
|
336
380
|
const worker = await this.#factory.create(
|
|
337
|
-
|
|
381
|
+
scope.home,
|
|
338
382
|
SessionManager.inMemory(this.#factory.workDir()),
|
|
339
|
-
await this.#sessions.gate(
|
|
383
|
+
await this.#sessions.gate(scope.home, false),
|
|
340
384
|
new PromptSlot(),
|
|
341
|
-
ownerAttachmentDir(dataDir,
|
|
385
|
+
ownerAttachmentDir(dataDir, scope.home),
|
|
342
386
|
undefined,
|
|
343
387
|
"owner",
|
|
388
|
+
conversation,
|
|
389
|
+
{ turn: scope.turn, memory },
|
|
344
390
|
);
|
|
345
391
|
const { session } = worker;
|
|
346
392
|
const toolCalls: string[] = [];
|
|
@@ -352,16 +398,17 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
352
398
|
const timer = setTimeout(abort, task.timeoutMs);
|
|
353
399
|
try {
|
|
354
400
|
const registered = new Set(session.getAllTools().map((t) => t.name));
|
|
355
|
-
|
|
356
|
-
//
|
|
357
|
-
|
|
401
|
+
// A worker that loaded the reader's memory may report it: the call that started it, and the
|
|
402
|
+
// calls that one runs within, then record their results as the reader's.
|
|
403
|
+
if (memoryReader(conversation, turn.speaker) && loadsMemory(registered)) {
|
|
404
|
+
parent.draws.drawn(runningCalls());
|
|
405
|
+
this.#memoryTurns.get(scope.turn)?.();
|
|
406
|
+
}
|
|
407
|
+
const { tier } = turn;
|
|
358
408
|
worker.tools = this.#factory
|
|
359
|
-
.
|
|
409
|
+
.taskTools(task.selection, task.exclude)
|
|
360
410
|
.filter(
|
|
361
|
-
(name) =>
|
|
362
|
-
!excluded.has(name) &&
|
|
363
|
-
registered.has(name) &&
|
|
364
|
-
this.#tiers.allows(tier, name),
|
|
411
|
+
(name) => registered.has(name) && this.#tiers.allows(tier, name),
|
|
365
412
|
);
|
|
366
413
|
session.setActiveToolsByName([...worker.tools]);
|
|
367
414
|
await withoutReplyFiles(() => session.prompt(task.text));
|
|
@@ -409,39 +456,31 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
409
456
|
async heldActions(
|
|
410
457
|
session: ChannelKey,
|
|
411
458
|
): Promise<PendingConfirmation | undefined> {
|
|
412
|
-
|
|
413
|
-
return gate.pending();
|
|
459
|
+
return this.#sessions.held(session);
|
|
414
460
|
}
|
|
415
461
|
|
|
416
462
|
async recentTranscript(
|
|
417
463
|
channel: ChannelKey,
|
|
418
464
|
limit: number,
|
|
419
465
|
): Promise<TranscriptEntry[]> {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
for (const message of session.messages) {
|
|
423
|
-
if (message.role !== "user" && message.role !== "assistant") continue;
|
|
424
|
-
const text = textOf(message.content).trim();
|
|
425
|
-
if (text)
|
|
426
|
-
entries.push({
|
|
427
|
-
role: message.role,
|
|
428
|
-
text: text.slice(0, TRANSCRIPT_ENTRY_CHARS),
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
return entries.slice(-limit);
|
|
466
|
+
await this.#sessions.held(channel);
|
|
467
|
+
return transcriptOf(await this.#sessions.messages(channel), limit);
|
|
432
468
|
}
|
|
433
469
|
|
|
434
|
-
/**
|
|
435
|
-
* The turn's thinking level: an agent's fixed setting, or the judge's pick, which is kept for the
|
|
436
|
-
* next turn while the judge is unsure. An agent session also switches to the agent's model here.
|
|
437
|
-
*/
|
|
470
|
+
/** Uses the checked model snapshot; auto thinking keeps the last pick while the judge is unsure. */
|
|
438
471
|
async #thinkingLevel(
|
|
439
472
|
key: ChannelKey,
|
|
440
473
|
session: AgentSession,
|
|
441
474
|
request: TurnRequest,
|
|
475
|
+
agentModel?: AgentModelSettings,
|
|
442
476
|
): Promise<ThinkingLevel> {
|
|
443
477
|
const setting = request.agent
|
|
444
|
-
? await
|
|
478
|
+
? await useChosenAgentModel(
|
|
479
|
+
session,
|
|
480
|
+
this.#modelRuntime,
|
|
481
|
+
request.agent.name,
|
|
482
|
+
agentModel,
|
|
483
|
+
)
|
|
445
484
|
: AUTO_THINKING;
|
|
446
485
|
if (setting !== AUTO_THINKING) {
|
|
447
486
|
this.#sessions.judged.delete(key);
|
|
@@ -455,38 +494,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
455
494
|
return level;
|
|
456
495
|
}
|
|
457
496
|
|
|
458
|
-
/** Switches an agent session to the agent's current model; returns its thinking setting. */
|
|
459
|
-
async #useAgentModel(
|
|
460
|
-
session: AgentSession,
|
|
461
|
-
name: string,
|
|
462
|
-
): Promise<ThinkingSetting> {
|
|
463
|
-
const agents = this.#options.agents;
|
|
464
|
-
if (!agents)
|
|
465
|
-
throw new ConfigError("agent turns need the runtime's agents option");
|
|
466
|
-
const { model, thinking } = agents.modelOf(name);
|
|
467
|
-
const ref = parseModelRef(model);
|
|
468
|
-
if (
|
|
469
|
-
session.model?.provider !== ref?.provider ||
|
|
470
|
-
session.model?.id !== ref?.id
|
|
471
|
-
) {
|
|
472
|
-
const resolved = ref && this.#modelRuntime.getModel(ref.provider, ref.id);
|
|
473
|
-
if (!resolved)
|
|
474
|
-
throw new AgentRunError(
|
|
475
|
-
`${name}'s model ${model} is not available on this host`,
|
|
476
|
-
);
|
|
477
|
-
await session.setModel(resolved);
|
|
478
|
-
}
|
|
479
|
-
return thinking;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
497
|
dispose(): void {
|
|
483
498
|
this.#sessions.dispose();
|
|
484
499
|
}
|
|
485
500
|
}
|
|
486
|
-
|
|
487
|
-
function promptImages(attachments: TurnAttachments): PromptImages {
|
|
488
|
-
return attachments.images.map((image) => ({
|
|
489
|
-
type: "image" as const,
|
|
490
|
-
...image,
|
|
491
|
-
}));
|
|
492
|
-
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { ConfirmationGate } from "./extensions/confirmation-gate.ts";
|
|
4
|
+
import { PromptSlot } from "./prompt-slot.ts";
|
|
5
|
+
import type { PiAgentRuntimeOptions } from "./runtime-types.ts";
|
|
6
|
+
import type { SessionFactory } from "./session-factory.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Tools available in either startup scope, without running a turn or retaining a session.
|
|
10
|
+
* The private probe is built only when the shared one lacks a required tool, so a host
|
|
11
|
+
* without scoped tools starts exactly as before.
|
|
12
|
+
*/
|
|
13
|
+
export async function preflightTools(
|
|
14
|
+
factory: SessionFactory,
|
|
15
|
+
owner: PiAgentRuntimeOptions["owner"],
|
|
16
|
+
required: readonly string[],
|
|
17
|
+
): Promise<Set<string>> {
|
|
18
|
+
const registered = new Set<string>();
|
|
19
|
+
for (const conversation of [
|
|
20
|
+
{ visibility: "shared" },
|
|
21
|
+
{ visibility: "private", principalId: owner.id },
|
|
22
|
+
] as const) {
|
|
23
|
+
if (
|
|
24
|
+
conversation.visibility === "private" &&
|
|
25
|
+
required.every((name) => registered.has(name))
|
|
26
|
+
)
|
|
27
|
+
break;
|
|
28
|
+
const probe = await factory.create(
|
|
29
|
+
conversation.visibility === "shared"
|
|
30
|
+
? "probe:startup"
|
|
31
|
+
: "probe:startup-private",
|
|
32
|
+
SessionManager.inMemory(factory.workDir()),
|
|
33
|
+
new ConfirmationGate(factory.link().holds, owner),
|
|
34
|
+
new PromptSlot(),
|
|
35
|
+
join(factory.workDir(), "probe-attachments"),
|
|
36
|
+
undefined,
|
|
37
|
+
"owner",
|
|
38
|
+
conversation,
|
|
39
|
+
);
|
|
40
|
+
try {
|
|
41
|
+
for (const tool of probe.session.getAllTools()) registered.add(tool.name);
|
|
42
|
+
} finally {
|
|
43
|
+
probe.session.dispose();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return registered;
|
|
47
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { OwnerPrompts, OwnerQuestion } from "../domain/owner-prompts.ts";
|
|
2
1
|
import { assistantName } from "../i18n/index.ts";
|
|
2
|
+
import type { OwnerQuestion, Prompts } from "../interactions/prompts.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* A run's way to ask the owner while it works. A turn they started or a report turn binds the
|
|
@@ -9,7 +9,7 @@ import { assistantName } from "../i18n/index.ts";
|
|
|
9
9
|
* leaves out.
|
|
10
10
|
*/
|
|
11
11
|
export class PromptSlot {
|
|
12
|
-
#prompts:
|
|
12
|
+
#prompts: Prompts | undefined;
|
|
13
13
|
#beforeCard: (() => Promise<void>) | undefined;
|
|
14
14
|
#asker = assistantName();
|
|
15
15
|
#open = 0;
|
|
@@ -21,7 +21,7 @@ export class PromptSlot {
|
|
|
21
21
|
* runs before each card opens, such as posting the text the turn wrote before it.
|
|
22
22
|
*/
|
|
23
23
|
bind(
|
|
24
|
-
prompts:
|
|
24
|
+
prompts: Prompts | undefined,
|
|
25
25
|
asker: string,
|
|
26
26
|
beforeCard?: () => Promise<void>,
|
|
27
27
|
): void {
|
|
@@ -42,12 +42,12 @@ export class PromptSlot {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** The turn's prompts, each open card counted as waiting; undefined when none is bound. */
|
|
45
|
-
get prompts():
|
|
45
|
+
get prompts(): Prompts | undefined {
|
|
46
46
|
const prompts = this.#prompts;
|
|
47
47
|
if (!prompts) return undefined;
|
|
48
48
|
return {
|
|
49
|
-
confirm: (title, message, signal) =>
|
|
50
|
-
this.#waiting(() => prompts.confirm(title, message, signal)),
|
|
49
|
+
confirm: (title, message, signal, minTier) =>
|
|
50
|
+
this.#waiting(() => prompts.confirm(title, message, signal, minTier)),
|
|
51
51
|
ask: (title, question: OwnerQuestion, signal) =>
|
|
52
52
|
this.#waiting(() => prompts.ask(title, question, signal)),
|
|
53
53
|
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ContextWithSystemEvent,
|
|
3
|
+
SessionManager,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { SYSTEM_PRINCIPAL } from "../identity/principal-store.ts";
|
|
6
|
+
import { MEMORY_TOOLS } from "../modules/memory/owner-memory.ts";
|
|
7
|
+
import { guardHidesPrivateExchange } from "./extensions/private-memory.ts";
|
|
8
|
+
import { SCOPE_ENTRY } from "./session-scope.ts";
|
|
9
|
+
|
|
10
|
+
/** A turn's reader and whether its request carried private memory, retained across compaction. */
|
|
11
|
+
export const MEMORY_TURN_ENTRY = "roundtable-memory-turn";
|
|
12
|
+
interface ReaderRecord {
|
|
13
|
+
reader: string;
|
|
14
|
+
privateMemory: boolean;
|
|
15
|
+
}
|
|
16
|
+
type Messages = ContextWithSystemEvent["messages"];
|
|
17
|
+
const MEMORY = new Set<string>(MEMORY_TOOLS);
|
|
18
|
+
|
|
19
|
+
/** Persist before prompting; the returned marker records later memory exchanges or memory-loaded workers. */
|
|
20
|
+
export function recordMemoryTurn(
|
|
21
|
+
manager: Pick<SessionManager, "appendCustomEntry">,
|
|
22
|
+
reader: string,
|
|
23
|
+
privateMemory: boolean,
|
|
24
|
+
): () => void {
|
|
25
|
+
const turnId = crypto.randomUUID();
|
|
26
|
+
manager.appendCustomEntry(MEMORY_TURN_ENTRY, {
|
|
27
|
+
turnId,
|
|
28
|
+
reader,
|
|
29
|
+
privateMemory,
|
|
30
|
+
});
|
|
31
|
+
return () => {
|
|
32
|
+
if (privateMemory) return;
|
|
33
|
+
privateMemory = true;
|
|
34
|
+
manager.appendCustomEntry(MEMORY_TURN_ENTRY, {
|
|
35
|
+
turnId,
|
|
36
|
+
reader,
|
|
37
|
+
privateMemory,
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Includes immediate validation/truncation failures and calls an aborted turn left unanswered. */
|
|
43
|
+
export function carriesMemory(
|
|
44
|
+
message: ContextWithSystemEvent["messages"][number],
|
|
45
|
+
): boolean {
|
|
46
|
+
if (message.role === "assistant")
|
|
47
|
+
return message.content.some(
|
|
48
|
+
(part) => part.type === "toolCall" && MEMORY.has(part.name),
|
|
49
|
+
);
|
|
50
|
+
if (message.role !== "toolResult") return false;
|
|
51
|
+
return (
|
|
52
|
+
MEMORY.has(message.toolName) ||
|
|
53
|
+
(typeof message.details === "object" &&
|
|
54
|
+
message.details !== null &&
|
|
55
|
+
"privateTo" in message.details)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readerRecord(data: unknown): ReaderRecord | undefined {
|
|
60
|
+
if (typeof data !== "object" || data === null) return undefined;
|
|
61
|
+
if (
|
|
62
|
+
!("reader" in data) ||
|
|
63
|
+
!("privateMemory" in data) ||
|
|
64
|
+
typeof data.reader !== "string" ||
|
|
65
|
+
data.reader.length === 0 ||
|
|
66
|
+
typeof data.privateMemory !== "boolean"
|
|
67
|
+
)
|
|
68
|
+
return undefined;
|
|
69
|
+
return { reader: data.reader, privateMemory: data.privateMemory };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Whether bridge's raw branch would replay another reader's private turn, including retained
|
|
74
|
+
* prompts/reasoning and compacted entries. Explicit privateTo results still restrict the reader,
|
|
75
|
+
* and SYSTEM is a reader of its own. Untagged memory before the first scope record is no one's
|
|
76
|
+
* in this public form; later unrecorded exchanges fail closed.
|
|
77
|
+
*/
|
|
78
|
+
export function bridgeHistoryHidesMemory(
|
|
79
|
+
branch: ReturnType<SessionManager["getBranch"]>,
|
|
80
|
+
reader: string | undefined,
|
|
81
|
+
): boolean {
|
|
82
|
+
return bridgeHistoryHidesMemoryFor(branch, reader);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The core's own check, which public `bridgeHistoryHidesMemory` leaves its attribution knob out
|
|
87
|
+
* of: untagged memory before the first scope record keeps the primary-owner legacy attribution,
|
|
88
|
+
* and SYSTEM counts as primary owner when `primaryOwner` is supplied.
|
|
89
|
+
*/
|
|
90
|
+
export function bridgeHistoryHidesMemoryFor(
|
|
91
|
+
branch: ReturnType<SessionManager["getBranch"]>,
|
|
92
|
+
reader: string | undefined,
|
|
93
|
+
primaryOwner?: string,
|
|
94
|
+
): boolean {
|
|
95
|
+
const normalized = (who: string | undefined) =>
|
|
96
|
+
who === SYSTEM_PRINCIPAL && primaryOwner !== undefined ? primaryOwner : who;
|
|
97
|
+
const current = normalized(reader);
|
|
98
|
+
let record: ReaderRecord | undefined;
|
|
99
|
+
let messages: Messages = [];
|
|
100
|
+
let legacy = true;
|
|
101
|
+
const hidden = () => {
|
|
102
|
+
const whose = normalized(record?.reader);
|
|
103
|
+
const carried = record?.privateMemory || messages.some(carriesMemory);
|
|
104
|
+
if (record && carried && whose !== current) return true;
|
|
105
|
+
return guardHidesPrivateExchange(messages, {
|
|
106
|
+
shared: true,
|
|
107
|
+
reader: current,
|
|
108
|
+
legacyOwner: record ? whose : legacy ? primaryOwner : undefined,
|
|
109
|
+
turnReader: whose,
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
for (const entry of branch) {
|
|
113
|
+
if (
|
|
114
|
+
entry.type === "custom" &&
|
|
115
|
+
(entry.customType === MEMORY_TURN_ENTRY ||
|
|
116
|
+
entry.customType === SCOPE_ENTRY)
|
|
117
|
+
) {
|
|
118
|
+
if (hidden()) return true;
|
|
119
|
+
messages = [];
|
|
120
|
+
if (entry.customType === SCOPE_ENTRY) legacy = false;
|
|
121
|
+
else {
|
|
122
|
+
record = readerRecord(entry.data);
|
|
123
|
+
// A corrupt record cannot establish safe ownership of a private turn.
|
|
124
|
+
if (!record) return true;
|
|
125
|
+
}
|
|
126
|
+
} else if (entry.type === "message") messages.push(entry.message);
|
|
127
|
+
}
|
|
128
|
+
return hidden();
|
|
129
|
+
}
|