pi-roundtable 0.8.0 → 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 +102 -0
- package/README.md +35 -11
- package/README.zh-TW.md +27 -10
- package/docs/migration-0.9.md +247 -0
- package/docs/plugins.md +606 -78
- package/examples/channel-context.test.ts +88 -0
- package/examples/channel-context.ts +62 -0
- package/examples/echo-runtime.test.ts +1 -0
- package/examples/fake-surface.test.ts +69 -6
- package/examples/fake-surface.ts +73 -7
- package/examples/headless.ts +5 -1
- 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 +5 -2
- 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 +115 -1
- package/src/cli/config-edit.ts +83 -13
- package/src/cli/doctor.ts +7 -0
- package/src/cli/line-diff.ts +72 -0
- package/src/cli/principal.ts +412 -0
- package/src/cli/project.ts +5 -0
- package/src/cli/report.ts +12 -3
- 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/team-turns.ts +8 -4
- package/src/core/builtin/agent-server.ts +9 -7
- package/src/core/builtin/discord-admin.ts +4 -1
- package/src/core/builtin/discord.ts +71 -13
- package/src/core/builtin/modules.ts +140 -85
- package/src/core/builtin/stores.ts +40 -21
- package/src/core/config/access.ts +229 -0
- package/src/core/config/adapters.ts +64 -1
- package/src/core/config/config.ts +78 -41
- package/src/core/contract/channel-context.ts +53 -0
- package/src/core/contract/channels.ts +77 -9
- package/src/core/contract/runtime.ts +23 -7
- package/src/core/contract/surface.ts +22 -6
- package/src/core/conversations/conversation-registry.ts +14 -0
- package/src/core/conversations/conversation-store.ts +13 -0
- package/src/core/define-roundtable.ts +41 -11
- 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-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 -97
- package/src/core/discord/owner-command.ts +27 -13
- package/src/core/discord/stop-button.ts +4 -4
- package/src/core/domain/conversation.ts +7 -2
- package/src/core/domain/errors.ts +5 -0
- package/src/core/domain/ports.ts +27 -2
- package/src/core/host.ts +27 -8
- package/src/core/i18n/cards.ts +8 -4
- 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/plugin.ts +48 -0
- package/src/core/presence/direct-channels.ts +104 -0
- package/src/core/registry/contributions.ts +63 -3
- package/src/core/routing/channel-router.ts +218 -17
- package/src/core/routing/conversation-turns.ts +38 -8
- package/src/core/routing/surface-port.ts +32 -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 +28 -14
- package/src/core/runtime/extensions/private-memory.ts +447 -0
- package/src/core/runtime/pending-confirmation-store.ts +30 -5
- package/src/core/runtime/pi-agent-runtime.ts +135 -134
- package/src/core/runtime/preflight-tools.ts +47 -0
- package/src/core/runtime/prompt-slot.ts +4 -4
- package/src/core/runtime/reader-history.ts +129 -0
- package/src/core/runtime/runtime-plugin.ts +50 -5
- 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/services.ts +27 -1
- package/src/core/sessions.ts +30 -0
- 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 +108 -21
- package/src/core/testing/owner.ts +17 -0
- package/src/core/testing/surface-contract.ts +118 -41
- package/src/core/testing/test-host.ts +79 -16
- 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 +8 -0
- package/src/index.ts +47 -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 +55 -16
- package/templates/.env.example +1 -1
- package/templates/adapters/web/README.md +7 -0
- package/templates/adapters/web/roundtable.config.ts +14 -10
- package/templates/roundtable.config.ts +12 -1
- 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,41 +14,46 @@ 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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} from "./extensions/
|
|
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";
|
|
42
37
|
import { interimPoster } from "./interim-text.ts";
|
|
38
|
+
import { preflightTools } from "./preflight-tools.ts";
|
|
43
39
|
import { PromptSlot, workTimeout } from "./prompt-slot.ts";
|
|
44
|
-
import {
|
|
45
|
-
|
|
46
|
-
TRANSCRIPT_ENTRY_CHARS,
|
|
47
|
-
type TurnMessage,
|
|
48
|
-
} from "./runtime-types.ts";
|
|
40
|
+
import { carriesMemory, recordMemoryTurn } from "./reader-history.ts";
|
|
41
|
+
import type { PiAgentRuntimeOptions, TurnMessage } from "./runtime-types.ts";
|
|
49
42
|
import { archiveSessions } from "./session-archive.ts";
|
|
43
|
+
import {
|
|
44
|
+
memoryReader,
|
|
45
|
+
refusedSpeaker,
|
|
46
|
+
turnAddressee,
|
|
47
|
+
} from "./session-conversation.ts";
|
|
50
48
|
import { SessionFactory } from "./session-factory.ts";
|
|
51
|
-
import {
|
|
52
|
-
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";
|
|
53
57
|
import { progressReporter } from "./turn-progress.ts";
|
|
54
58
|
import { workerReport } from "./worker-task.ts";
|
|
55
59
|
|
|
@@ -60,6 +64,8 @@ export {
|
|
|
60
64
|
sessionExtensions,
|
|
61
65
|
} from "./runtime-types.ts";
|
|
62
66
|
|
|
67
|
+
const MEMORY = new Set<string>(MEMORY_TOOLS);
|
|
68
|
+
|
|
63
69
|
/** AgentRuntime over the Pi SDK: one persistent session per channel, each with its own loader. */
|
|
64
70
|
const STOPPED = "stopped by the owner";
|
|
65
71
|
|
|
@@ -73,10 +79,11 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
73
79
|
/** The tier and speaker of each conversation's running turn. */
|
|
74
80
|
readonly #turns = new Map<
|
|
75
81
|
ChannelKey,
|
|
76
|
-
{ tier: Tier; speaker: Speaker
|
|
82
|
+
{ tier: Tier; speaker: Speaker; withheld: boolean }
|
|
77
83
|
>();
|
|
78
84
|
/** Each conversation's turn in progress, which the owner may steer or stop. */
|
|
79
85
|
readonly #running = new Map<ChannelKey, SteerableRun>();
|
|
86
|
+
readonly #memoryTurns = new Map<ChannelKey, () => void>();
|
|
80
87
|
|
|
81
88
|
/** Builds the runtime without starting a session; the preflight proves it can run. */
|
|
82
89
|
constructor(options: PiAgentRuntimeOptions) {
|
|
@@ -85,7 +92,8 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
85
92
|
this.#tiers = options.toolTiers ?? toolTiers();
|
|
86
93
|
this.#factory = new SessionFactory(options, {
|
|
87
94
|
speaker: (channel) => this.#turns.get(channel)?.speaker,
|
|
88
|
-
|
|
95
|
+
withholdsMemory: (channel) => this.#turns.get(channel)?.withheld === true,
|
|
96
|
+
runTask: (scope, task) => this.#runTask(scope, task),
|
|
89
97
|
});
|
|
90
98
|
this.#sessions = new ConversationSessions(
|
|
91
99
|
options,
|
|
@@ -94,50 +102,64 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
94
102
|
);
|
|
95
103
|
}
|
|
96
104
|
|
|
97
|
-
/**
|
|
98
|
-
* Proves the model and every required tool resolve, in a throwaway session that runs no turn;
|
|
99
|
-
* startup stops here before anything connects.
|
|
100
|
-
*/
|
|
105
|
+
/** Resolves the model and required tools in throwaway shared and private sessions, without a turn. */
|
|
101
106
|
async preflight(): Promise<void> {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
join(this.#factory.workDir(), "probe-attachments"),
|
|
108
|
-
undefined,
|
|
109
|
-
"owner",
|
|
110
|
-
);
|
|
111
|
-
const registered = new Set(
|
|
112
|
-
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,
|
|
113
112
|
);
|
|
114
|
-
|
|
115
|
-
const expected = [
|
|
116
|
-
...this.#factory.link().requiredTools,
|
|
117
|
-
COMPACT_TOOL,
|
|
118
|
-
ASK_USER_TOOL,
|
|
119
|
-
...planOrder(this.#factory.plan).flatMap(
|
|
120
|
-
(tool) => tool.snapshot().requiredTools ?? [],
|
|
121
|
-
),
|
|
122
|
-
];
|
|
123
|
-
const missing = expected.filter((name) => !registered.has(name));
|
|
113
|
+
const missing = required.filter((name) => !registered.has(name));
|
|
124
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,8 +198,8 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
167
198
|
gate.beginTurn(
|
|
168
199
|
request.selection.id,
|
|
169
200
|
request.confirmed === true,
|
|
170
|
-
|
|
171
|
-
request.speaker
|
|
201
|
+
who,
|
|
202
|
+
request.speaker,
|
|
172
203
|
);
|
|
173
204
|
if (request.confirmed && pending) {
|
|
174
205
|
logger.info(
|
|
@@ -196,6 +227,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
196
227
|
interim?.toolStart(event.toolName);
|
|
197
228
|
}
|
|
198
229
|
if (event.type === "message_end") {
|
|
230
|
+
if (carriesMemory(event.message)) markPrivateMemory();
|
|
199
231
|
turnMessages.push(event.message);
|
|
200
232
|
interim?.messageEnd(event.message);
|
|
201
233
|
}
|
|
@@ -203,7 +235,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
203
235
|
const slot = this.#sessions.slot(key);
|
|
204
236
|
slot.bind(
|
|
205
237
|
request.interactive
|
|
206
|
-
? this.#options.prompts?.(request.channel,
|
|
238
|
+
? this.#options.prompts?.(request.channel, promptScopeOf(scoped))
|
|
207
239
|
: undefined,
|
|
208
240
|
request.agent?.name ?? assistantName(),
|
|
209
241
|
interim ? () => interim.flush() : undefined,
|
|
@@ -226,11 +258,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
226
258
|
const attachments = request.attachments ?? NO_ATTACHMENTS;
|
|
227
259
|
const text =
|
|
228
260
|
request.confirmed && pending
|
|
229
|
-
? confirmedTurnText(
|
|
230
|
-
pending,
|
|
231
|
-
request.text,
|
|
232
|
-
addressee(request.speaker, this.#options.owner),
|
|
233
|
-
)
|
|
261
|
+
? confirmedTurnText(pending, request.text, who)
|
|
234
262
|
: request.text;
|
|
235
263
|
await running.run(() =>
|
|
236
264
|
session.prompt(
|
|
@@ -250,6 +278,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
250
278
|
} finally {
|
|
251
279
|
this.#running.delete(key);
|
|
252
280
|
this.#turns.delete(key);
|
|
281
|
+
this.#memoryTurns.delete(key);
|
|
253
282
|
cancelTimeout();
|
|
254
283
|
slot.unbind();
|
|
255
284
|
unsubscribe();
|
|
@@ -331,23 +360,33 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
331
360
|
return stopped;
|
|
332
361
|
}
|
|
333
362
|
|
|
334
|
-
/**
|
|
335
|
-
* Runs a task in a fresh, unsaved session with the selected tools and the channel's
|
|
336
|
-
* confirmation gate, and returns its final text.
|
|
337
|
-
*/
|
|
363
|
+
/** Runs a task in an unsaved session with the selected tools and conversation's gate. */
|
|
338
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
|
|
339
|
-
async #runTask(
|
|
365
|
+
async #runTask(
|
|
366
|
+
scope: { turn: ChannelKey; home: ChannelKey },
|
|
367
|
+
task: TransientTask,
|
|
368
|
+
): Promise<string> {
|
|
340
369
|
const { dataDir } = this.#options;
|
|
341
370
|
const { signal } = task;
|
|
342
|
-
// 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;
|
|
343
380
|
const worker = await this.#factory.create(
|
|
344
|
-
|
|
381
|
+
scope.home,
|
|
345
382
|
SessionManager.inMemory(this.#factory.workDir()),
|
|
346
|
-
await this.#sessions.gate(
|
|
383
|
+
await this.#sessions.gate(scope.home, false),
|
|
347
384
|
new PromptSlot(),
|
|
348
|
-
ownerAttachmentDir(dataDir,
|
|
385
|
+
ownerAttachmentDir(dataDir, scope.home),
|
|
349
386
|
undefined,
|
|
350
387
|
"owner",
|
|
388
|
+
conversation,
|
|
389
|
+
{ turn: scope.turn, memory },
|
|
351
390
|
);
|
|
352
391
|
const { session } = worker;
|
|
353
392
|
const toolCalls: string[] = [];
|
|
@@ -359,16 +398,17 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
359
398
|
const timer = setTimeout(abort, task.timeoutMs);
|
|
360
399
|
try {
|
|
361
400
|
const registered = new Set(session.getAllTools().map((t) => t.name));
|
|
362
|
-
|
|
363
|
-
//
|
|
364
|
-
|
|
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;
|
|
365
408
|
worker.tools = this.#factory
|
|
366
|
-
.
|
|
409
|
+
.taskTools(task.selection, task.exclude)
|
|
367
410
|
.filter(
|
|
368
|
-
(name) =>
|
|
369
|
-
!excluded.has(name) &&
|
|
370
|
-
registered.has(name) &&
|
|
371
|
-
this.#tiers.allows(tier, name),
|
|
411
|
+
(name) => registered.has(name) && this.#tiers.allows(tier, name),
|
|
372
412
|
);
|
|
373
413
|
session.setActiveToolsByName([...worker.tools]);
|
|
374
414
|
await withoutReplyFiles(() => session.prompt(task.text));
|
|
@@ -416,39 +456,31 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
416
456
|
async heldActions(
|
|
417
457
|
session: ChannelKey,
|
|
418
458
|
): Promise<PendingConfirmation | undefined> {
|
|
419
|
-
|
|
420
|
-
return gate.pending();
|
|
459
|
+
return this.#sessions.held(session);
|
|
421
460
|
}
|
|
422
461
|
|
|
423
462
|
async recentTranscript(
|
|
424
463
|
channel: ChannelKey,
|
|
425
464
|
limit: number,
|
|
426
465
|
): Promise<TranscriptEntry[]> {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
for (const message of session.messages) {
|
|
430
|
-
if (message.role !== "user" && message.role !== "assistant") continue;
|
|
431
|
-
const text = textOf(message.content).trim();
|
|
432
|
-
if (text)
|
|
433
|
-
entries.push({
|
|
434
|
-
role: message.role,
|
|
435
|
-
text: text.slice(0, TRANSCRIPT_ENTRY_CHARS),
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
return entries.slice(-limit);
|
|
466
|
+
await this.#sessions.held(channel);
|
|
467
|
+
return transcriptOf(await this.#sessions.messages(channel), limit);
|
|
439
468
|
}
|
|
440
469
|
|
|
441
|
-
/**
|
|
442
|
-
* The turn's thinking level: an agent's fixed setting, or the judge's pick, which is kept for the
|
|
443
|
-
* next turn while the judge is unsure. An agent session also switches to the agent's model here.
|
|
444
|
-
*/
|
|
470
|
+
/** Uses the checked model snapshot; auto thinking keeps the last pick while the judge is unsure. */
|
|
445
471
|
async #thinkingLevel(
|
|
446
472
|
key: ChannelKey,
|
|
447
473
|
session: AgentSession,
|
|
448
474
|
request: TurnRequest,
|
|
475
|
+
agentModel?: AgentModelSettings,
|
|
449
476
|
): Promise<ThinkingLevel> {
|
|
450
477
|
const setting = request.agent
|
|
451
|
-
? await
|
|
478
|
+
? await useChosenAgentModel(
|
|
479
|
+
session,
|
|
480
|
+
this.#modelRuntime,
|
|
481
|
+
request.agent.name,
|
|
482
|
+
agentModel,
|
|
483
|
+
)
|
|
452
484
|
: AUTO_THINKING;
|
|
453
485
|
if (setting !== AUTO_THINKING) {
|
|
454
486
|
this.#sessions.judged.delete(key);
|
|
@@ -462,38 +494,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
462
494
|
return level;
|
|
463
495
|
}
|
|
464
496
|
|
|
465
|
-
/** Switches an agent session to the agent's current model; returns its thinking setting. */
|
|
466
|
-
async #useAgentModel(
|
|
467
|
-
session: AgentSession,
|
|
468
|
-
name: string,
|
|
469
|
-
): Promise<ThinkingSetting> {
|
|
470
|
-
const agents = this.#options.agents;
|
|
471
|
-
if (!agents)
|
|
472
|
-
throw new ConfigError("agent turns need the runtime's agents option");
|
|
473
|
-
const { model, thinking } = agents.modelOf(name);
|
|
474
|
-
const ref = parseModelRef(model);
|
|
475
|
-
if (
|
|
476
|
-
session.model?.provider !== ref?.provider ||
|
|
477
|
-
session.model?.id !== ref?.id
|
|
478
|
-
) {
|
|
479
|
-
const resolved = ref && this.#modelRuntime.getModel(ref.provider, ref.id);
|
|
480
|
-
if (!resolved)
|
|
481
|
-
throw new AgentRunError(
|
|
482
|
-
`${name}'s model ${model} is not available on this host`,
|
|
483
|
-
);
|
|
484
|
-
await session.setModel(resolved);
|
|
485
|
-
}
|
|
486
|
-
return thinking;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
497
|
dispose(): void {
|
|
490
498
|
this.#sessions.dispose();
|
|
491
499
|
}
|
|
492
500
|
}
|
|
493
|
-
|
|
494
|
-
function promptImages(attachments: TurnAttachments): PromptImages {
|
|
495
|
-
return attachments.images.map((image) => ({
|
|
496
|
-
type: "image" as const,
|
|
497
|
-
...image,
|
|
498
|
-
}));
|
|
499
|
-
}
|
|
@@ -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,7 +42,7 @@ 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 {
|
|
@@ -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
|
+
}
|