pi-roundtable 0.8.0 → 0.9.1
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 +115 -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 +618 -86
- 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 +150 -86
- 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 +87 -41
- package/src/core/contract/channel-context.ts +53 -0
- package/src/core/contract/channels.ts +82 -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 +42 -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/drain.ts +56 -5
- package/src/core/errors.ts +13 -0
- package/src/core/host-background.ts +56 -0
- package/src/core/host.ts +52 -68
- package/src/core/i18n/cards.ts +8 -4
- package/src/core/i18n/conversation.ts +2 -0
- 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 +56 -4
- 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-queue.ts +18 -1
- package/src/core/routing/channel-router.ts +274 -21
- package/src/core/routing/conversation-turns.ts +45 -10
- package/src/core/routing/surface-port.ts +32 -2
- package/src/core/routing/system-turns.ts +29 -0
- package/src/core/runtime/abort-guard.ts +58 -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 +139 -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 +174 -190
- 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 +51 -5
- package/src/core/runtime/runtime-types.ts +48 -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 +21 -3
- package/src/core/runtime/thinking-level.ts +47 -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-wrapup.ts +63 -0
- 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 +48 -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
|
@@ -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
|
+
}
|
|
@@ -7,12 +7,24 @@ import type {
|
|
|
7
7
|
} from "../contract/runtime.ts";
|
|
8
8
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
9
9
|
import type { InterimTextMode } from "../domain/interim.ts";
|
|
10
|
+
import type { IdentityService } from "../identity/identity-service.ts";
|
|
10
11
|
import type { OwnerIdentity } from "../identity.ts";
|
|
12
|
+
import type { PromptScope } from "../interactions/prompts.ts";
|
|
11
13
|
import { AGENT_BRIEF, EffortJudge } from "../judging/effort-judge.ts";
|
|
12
|
-
import
|
|
14
|
+
import {
|
|
15
|
+
formatModelRef,
|
|
16
|
+
type ModelRef,
|
|
17
|
+
type ThinkingLevel,
|
|
18
|
+
} from "../models.ts";
|
|
13
19
|
import type { PluginContext, RoundtablePlugin } from "../plugin.ts";
|
|
14
|
-
import { RUNTIME } from "../services.ts";
|
|
20
|
+
import { CONVERSATIONS, IDENTITY, RUNTIME } from "../services.ts";
|
|
15
21
|
import type { Speaker } from "../speakers.ts";
|
|
22
|
+
import {
|
|
23
|
+
BRIDGE_MEMORY_FIX,
|
|
24
|
+
bridgeMemoryProblem,
|
|
25
|
+
liveBridgeRefusal,
|
|
26
|
+
onClaudeBridge,
|
|
27
|
+
} from "./bridge-guard.ts";
|
|
16
28
|
import { PendingConfirmationStore } from "./pending-confirmation-store.ts";
|
|
17
29
|
import { PiAgentRuntime } from "./pi-agent-runtime.ts";
|
|
18
30
|
|
|
@@ -63,6 +75,10 @@ export interface RuntimePluginOptions {
|
|
|
63
75
|
interimText?: InterimTextMode;
|
|
64
76
|
/** An intermediate text this long or longer is posted as an ordinary message; default 400. */
|
|
65
77
|
interimPrimaryChars?: number;
|
|
78
|
+
/** Whether the host keeps each person's memory; bridge turns then check private history. */
|
|
79
|
+
memory?: boolean;
|
|
80
|
+
/** Why the configuration lets several people speak in a shared conversation, as `configuredCrowd` says. */
|
|
81
|
+
crowd?: string;
|
|
66
82
|
}
|
|
67
83
|
|
|
68
84
|
/**
|
|
@@ -79,18 +95,36 @@ export function runtimePlugin(
|
|
|
79
95
|
PendingConfirmationStore.attach(context.database()),
|
|
80
96
|
): RoundtablePlugin {
|
|
81
97
|
let runtime: AgentRuntime | undefined;
|
|
98
|
+
let identity: IdentityService | undefined;
|
|
99
|
+
let warn: ((message: string) => void) | undefined;
|
|
100
|
+
// Admission is a startup risk warning, not proof that this conversation holds private history.
|
|
101
|
+
const bridgeRefusal = liveBridgeRefusal({
|
|
102
|
+
memory: options.memory,
|
|
103
|
+
crowd: options.crowd,
|
|
104
|
+
identity: () => identity,
|
|
105
|
+
primary: options.owner.id,
|
|
106
|
+
});
|
|
82
107
|
return {
|
|
83
108
|
name: RUNTIME_PLUGIN,
|
|
84
109
|
migrations: PendingConfirmationStore.migrations(),
|
|
85
110
|
provides: [RUNTIME],
|
|
86
111
|
preflight: async () => {
|
|
112
|
+
if (onClaudeBridge(options.model)) {
|
|
113
|
+
const why = await bridgeRefusal();
|
|
114
|
+
if (why)
|
|
115
|
+
warn?.(
|
|
116
|
+
`${bridgeMemoryProblem(formatModelRef(options.model), why)} ${BRIDGE_MEMORY_FIX}`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
87
119
|
await runtime?.preflight?.();
|
|
88
120
|
},
|
|
89
121
|
setup: async (context) => {
|
|
90
|
-
const { logger, providers } = context;
|
|
122
|
+
const { logger, providers, services } = context;
|
|
123
|
+
identity = services.find(IDENTITY);
|
|
124
|
+
warn = (message) => logger.warn({}, message);
|
|
91
125
|
const heldActions = await openHeldActions(context);
|
|
92
|
-
const prompts = (channel: ChannelKey,
|
|
93
|
-
context.surfaces.prompts(channel,
|
|
126
|
+
const prompts = (channel: ChannelKey, scope?: PromptScope | Speaker) =>
|
|
127
|
+
context.surfaces.prompts(channel, scope);
|
|
94
128
|
const agents = () => options.agents?.current();
|
|
95
129
|
const running: AgentRuntime = providers.filled.has("runtime")
|
|
96
130
|
? providers.runtime(
|
|
@@ -108,6 +142,17 @@ export function runtimePlugin(
|
|
|
108
142
|
)
|
|
109
143
|
: new PiAgentRuntime({
|
|
110
144
|
owner: options.owner,
|
|
145
|
+
// Read at every turn that names no conversation, after every plugin set up.
|
|
146
|
+
conversationOf: async (key) => {
|
|
147
|
+
const record = await services.find(CONVERSATIONS)?.get(key);
|
|
148
|
+
if (!record) return undefined;
|
|
149
|
+
return record.visibility === "private" &&
|
|
150
|
+
record.principalId !== undefined
|
|
151
|
+
? { visibility: "private", principalId: record.principalId }
|
|
152
|
+
: { visibility: "shared" };
|
|
153
|
+
},
|
|
154
|
+
principalOf: async (id) => services.find(IDENTITY)?.principal(id),
|
|
155
|
+
memory: options.memory === true,
|
|
111
156
|
sessions: context.sessions,
|
|
112
157
|
agentDir: options.agentDir,
|
|
113
158
|
modelRuntime: options.modelRuntime,
|
|
@@ -123,6 +168,7 @@ export function runtimePlugin(
|
|
|
123
168
|
}),
|
|
124
169
|
confirmations: heldActions,
|
|
125
170
|
toolTiers: context.toolTiers,
|
|
171
|
+
stopping: () => context.queue.closed,
|
|
126
172
|
get agents() {
|
|
127
173
|
return agents();
|
|
128
174
|
},
|
|
@@ -7,20 +7,24 @@ import type {
|
|
|
7
7
|
import type { AgentSessions, LoadedSkill } from "../contract/runtime.ts";
|
|
8
8
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
9
9
|
import type { InterimTextMode } from "../domain/interim.ts";
|
|
10
|
-
import type {
|
|
10
|
+
import type { TurnConversation } from "../domain/ports.ts";
|
|
11
|
+
import type { Principal } from "../identity/principal-store.ts";
|
|
11
12
|
import type { OwnerIdentity } from "../identity.ts";
|
|
13
|
+
import type { PromptScope, Prompts } from "../interactions/prompts.ts";
|
|
12
14
|
import type { Logger } from "../log.ts";
|
|
13
15
|
import type { ThinkingLevel, ThinkingPicker } from "../models.ts";
|
|
14
16
|
import type { LinkedSessions } from "../plugin.ts";
|
|
15
17
|
import {
|
|
16
18
|
planOrder,
|
|
17
19
|
type SessionContext,
|
|
20
|
+
type SessionConversation,
|
|
18
21
|
type SessionPlan,
|
|
19
22
|
type SessionTool,
|
|
20
23
|
} from "../sessions.ts";
|
|
21
|
-
import type { Speaker } from "../speakers.ts";
|
|
22
24
|
import type { ToolTiers } from "../tool-tiers.ts";
|
|
25
|
+
import type { MemoryDraws } from "./extensions/private-memory.ts";
|
|
23
26
|
import type { PendingConfirmationStore } from "./pending-confirmation-store.ts";
|
|
27
|
+
import { scopedCalls } from "./tool-call-scope.ts";
|
|
24
28
|
|
|
25
29
|
export type TurnMessage = Extract<
|
|
26
30
|
AgentSessionEvent,
|
|
@@ -33,8 +37,19 @@ export type CompactionEnd = Extract<
|
|
|
33
37
|
export const TRANSCRIPT_ENTRY_CHARS = 1000;
|
|
34
38
|
|
|
35
39
|
export interface PiAgentRuntimeOptions {
|
|
36
|
-
/**
|
|
37
|
-
|
|
40
|
+
/**
|
|
41
|
+
* The primary owner, by their principal id: their conversations' prompts and tool results name
|
|
42
|
+
* them as configured, and the host's own turns address them.
|
|
43
|
+
*/
|
|
44
|
+
owner: OwnerIdentity & { id: string };
|
|
45
|
+
/**
|
|
46
|
+
* The host's record of a conversation, read at every turn that names none, so a record changed
|
|
47
|
+
* while its session is open counts, and when its transcript is read; without it, or without a
|
|
48
|
+
* record, the conversation is as its open session or its history was made, else shared.
|
|
49
|
+
*/
|
|
50
|
+
conversationOf?: (key: ChannelKey) => Promise<TurnConversation | undefined>;
|
|
51
|
+
/** A principal's name and pronouns, which a private conversation's tool descriptions use. */
|
|
52
|
+
principalOf?: (id: string) => Promise<Principal | undefined>;
|
|
38
53
|
agentDir: string;
|
|
39
54
|
/** Shared by the host's sessions, so logins refresh in one place. */
|
|
40
55
|
modelRuntime: ModelRuntime;
|
|
@@ -56,17 +71,23 @@ export interface PiAgentRuntimeOptions {
|
|
|
56
71
|
/** The agent server's sessions: their shared workspace, model, and skills. */
|
|
57
72
|
agents?: AgentSessions;
|
|
58
73
|
/**
|
|
59
|
-
*
|
|
60
|
-
* cannot show them. Without it every
|
|
74
|
+
* The prompts of a channel for an interactive turn's scope, made from its speaker and its
|
|
75
|
+
* conversation's visibility; undefined for a channel that cannot show them. Without it every
|
|
76
|
+
* held action waits for a message that approves it.
|
|
61
77
|
*/
|
|
62
|
-
prompts?: (
|
|
63
|
-
channel: ChannelKey,
|
|
64
|
-
speaker?: Speaker,
|
|
65
|
-
) => OwnerPrompts | undefined;
|
|
78
|
+
prompts?: (channel: ChannelKey, scope?: PromptScope) => Prompts | undefined;
|
|
66
79
|
/** The lowest tier that may use each tool; the default names the core's tools and leaves the rest to the owner. */
|
|
67
80
|
toolTiers?: ToolTiers;
|
|
68
81
|
/** A run that takes longer, not counting time spent waiting on the owner's cards, is aborted and reported as failed. */
|
|
69
82
|
turnTimeoutMs?: number;
|
|
83
|
+
/**
|
|
84
|
+
* How long a turn that was aborted, by its timeout or a stop, gets to end before its session is
|
|
85
|
+
* disposed of and the turn is reported failed; default 15 seconds. A model that ignores the
|
|
86
|
+
* abort would otherwise hold the turn, and the shutdown drain, for ever.
|
|
87
|
+
*/
|
|
88
|
+
turnAbortGraceMs?: number;
|
|
89
|
+
/** Whether the host is shutting down: a turn or a task is then refused before it starts. */
|
|
90
|
+
stopping?: () => boolean;
|
|
70
91
|
/**
|
|
71
92
|
* Whether a turn given a place for interim posts shows the text it writes before its final
|
|
72
93
|
* answer as it goes; default "on". "off" posts only the final reply.
|
|
@@ -76,6 +97,8 @@ export interface PiAgentRuntimeOptions {
|
|
|
76
97
|
interimPrimaryChars?: number;
|
|
77
98
|
/** How long a new session waits for its MCP tools to register. */
|
|
78
99
|
mcpConnectTimeoutMs?: number;
|
|
100
|
+
/** Defaults to true: bridge shared turns refuse raw private exchanges inaccessible to the reader. */
|
|
101
|
+
memory?: boolean;
|
|
79
102
|
}
|
|
80
103
|
|
|
81
104
|
export type { AgentSessions, LoadedSkill };
|
|
@@ -91,12 +114,14 @@ export interface CoreExtensions {
|
|
|
91
114
|
confirmationGate: ExtensionFactory;
|
|
92
115
|
askUser: ExtensionFactory;
|
|
93
116
|
selfCompactGuard: ExtensionFactory;
|
|
117
|
+
privateMemory: ExtensionFactory;
|
|
94
118
|
activeTools: ExtensionFactory;
|
|
95
119
|
}
|
|
96
120
|
|
|
97
121
|
/**
|
|
98
122
|
* A session's extensions in load order: the tools phase, the core's attachment, gate, ask-user,
|
|
99
|
-
* and compact guard, the compactor, the MCP phase,
|
|
123
|
+
* and compact guard, the compactor, the MCP phase, the memory projection, so each request is
|
|
124
|
+
* projected after every other extension changed it, and active-tools last, so its
|
|
100
125
|
* before_agent_start handler runs after every other extension's.
|
|
101
126
|
*/
|
|
102
127
|
export function sessionExtensions(
|
|
@@ -107,7 +132,9 @@ export function sessionExtensions(
|
|
|
107
132
|
const contributed = (tools: readonly SessionTool[]) =>
|
|
108
133
|
tools.flatMap((tool) => {
|
|
109
134
|
const factory = tool.snapshot().factory(session);
|
|
110
|
-
return factory
|
|
135
|
+
return factory
|
|
136
|
+
? [{ name: tool.name, factory: scopedCalls(factory) }]
|
|
137
|
+
: [];
|
|
111
138
|
});
|
|
112
139
|
return [
|
|
113
140
|
...contributed(plan.tools),
|
|
@@ -117,6 +144,7 @@ export function sessionExtensions(
|
|
|
117
144
|
{ name: "self-compact-guard", factory: core.selfCompactGuard },
|
|
118
145
|
...contributed(plan.compaction ? [plan.compaction] : []),
|
|
119
146
|
...contributed(plan.mcp),
|
|
147
|
+
{ name: "private-memory", factory: core.privateMemory },
|
|
120
148
|
{ name: "active-tools", factory: core.activeTools },
|
|
121
149
|
];
|
|
122
150
|
}
|
|
@@ -129,4 +157,12 @@ export interface ChannelSession {
|
|
|
129
157
|
revisions: string;
|
|
130
158
|
/** The skills an agent session was built with, as skillsKey. */
|
|
131
159
|
skills: string;
|
|
160
|
+
/** Whom the session's conversation serves, fixed when it was built. */
|
|
161
|
+
conversation: SessionConversation;
|
|
162
|
+
/** Whom its tool descriptions address. */
|
|
163
|
+
addressee: OwnerIdentity;
|
|
164
|
+
/** Whose memory its turns read, as resolved when it was built; a worker beside it keeps the same. */
|
|
165
|
+
memory: SessionContext["memory"];
|
|
166
|
+
/** Its tool calls running now, which a task that reads the reader's memory marks as drawing on it. */
|
|
167
|
+
draws: MemoryDraws;
|
|
132
168
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { ChannelKey } from "../domain/conversation.ts";
|
|
2
|
+
import type { TurnConversation, TurnRequest } from "../domain/ports.ts";
|
|
3
|
+
import {
|
|
4
|
+
type Principal,
|
|
5
|
+
SYSTEM_PRINCIPAL,
|
|
6
|
+
} from "../identity/principal-store.ts";
|
|
7
|
+
import { addresseeOf, type OwnerIdentity } from "../identity.ts";
|
|
8
|
+
import type { SessionConversation } from "../sessions.ts";
|
|
9
|
+
import { addressee, type Speaker, THE_SPEAKER } from "../speakers.ts";
|
|
10
|
+
import type { PiAgentRuntimeOptions } from "./runtime-types.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Whom a conversation serves as something says so: an agent's is shared; another is as the turn
|
|
14
|
+
* says, else as the host's record says; undefined when neither does.
|
|
15
|
+
*/
|
|
16
|
+
export async function namedConversation(
|
|
17
|
+
key: ChannelKey,
|
|
18
|
+
request: Pick<TurnRequest, "agent" | "conversation"> | undefined,
|
|
19
|
+
options: Pick<PiAgentRuntimeOptions, "conversationOf">,
|
|
20
|
+
): Promise<TurnConversation | undefined> {
|
|
21
|
+
if (request?.agent) return { visibility: "shared" };
|
|
22
|
+
return request?.conversation ?? (await options.conversationOf?.(key));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whom a new session's conversation serves: as the turn or the host's record names it, else
|
|
27
|
+
* shared, never the owner's by default. A private one's person is looked up for their name and
|
|
28
|
+
* pronouns.
|
|
29
|
+
*/
|
|
30
|
+
export async function sessionConversation(
|
|
31
|
+
key: ChannelKey,
|
|
32
|
+
request: Pick<TurnRequest, "agent" | "conversation"> | undefined,
|
|
33
|
+
options: Pick<PiAgentRuntimeOptions, "conversationOf" | "principalOf">,
|
|
34
|
+
): Promise<SessionConversation> {
|
|
35
|
+
const given = await namedConversation(key, request, options);
|
|
36
|
+
if (given?.visibility !== "private") return { visibility: "shared" };
|
|
37
|
+
const principal = await options.principalOf?.(given.principalId);
|
|
38
|
+
return {
|
|
39
|
+
visibility: "private",
|
|
40
|
+
principalId: given.principalId,
|
|
41
|
+
...(principal ? { principal } : {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Whether the turn names its conversation otherwise than the session was made for, so it is rebuilt. */
|
|
46
|
+
export function conversationChanged(
|
|
47
|
+
session: SessionConversation,
|
|
48
|
+
given: TurnConversation | undefined,
|
|
49
|
+
): boolean {
|
|
50
|
+
if (!given) return false;
|
|
51
|
+
if (given.visibility !== session.visibility) return true;
|
|
52
|
+
return (
|
|
53
|
+
given.visibility === "private" &&
|
|
54
|
+
session.visibility === "private" &&
|
|
55
|
+
given.principalId !== session.principalId
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whom a session's tool descriptions address: the primary owner as configured, so their own
|
|
61
|
+
* conversations read as in 0.8; another private conversation's person by their name and
|
|
62
|
+
* pronouns; and the speaker, unnamed, in a shared one or a private one of someone the host has
|
|
63
|
+
* no name for.
|
|
64
|
+
*/
|
|
65
|
+
export function sessionAddressee(
|
|
66
|
+
conversation: SessionConversation,
|
|
67
|
+
owner: OwnerIdentity & { id: string },
|
|
68
|
+
): OwnerIdentity {
|
|
69
|
+
if (conversation.visibility !== "private") return THE_SPEAKER;
|
|
70
|
+
if (conversation.principalId === owner.id) return owner;
|
|
71
|
+
const named = namedPrincipal(conversation);
|
|
72
|
+
return named ? addresseeOf(named) : THE_SPEAKER;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A private conversation's person, when the host knows them by a name: one carried over from 0.8
|
|
77
|
+
* may be named by their id alone until someone names them, and an id is no name to address.
|
|
78
|
+
*/
|
|
79
|
+
export function namedPrincipal(
|
|
80
|
+
conversation: SessionConversation,
|
|
81
|
+
): Principal | undefined {
|
|
82
|
+
if (conversation.visibility !== "private") return undefined;
|
|
83
|
+
const { principal } = conversation;
|
|
84
|
+
return principal && principal.displayName !== principal.id
|
|
85
|
+
? principal
|
|
86
|
+
: undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Whom a turn's approvals and answers name: a private conversation's person as its session
|
|
91
|
+
* addresses them; the host's own turns and the primary owner as the owner; anyone else, a
|
|
92
|
+
* second owner too, by their name.
|
|
93
|
+
*/
|
|
94
|
+
export function turnAddressee(
|
|
95
|
+
speaker: Speaker | undefined,
|
|
96
|
+
session: { conversation: SessionConversation; addressee: OwnerIdentity },
|
|
97
|
+
owner: OwnerIdentity & { id: string },
|
|
98
|
+
): OwnerIdentity {
|
|
99
|
+
const { conversation } = session;
|
|
100
|
+
if (
|
|
101
|
+
speaker &&
|
|
102
|
+
conversation.visibility === "private" &&
|
|
103
|
+
speaker.principalId === conversation.principalId &&
|
|
104
|
+
session.addressee !== THE_SPEAKER
|
|
105
|
+
)
|
|
106
|
+
return session.addressee;
|
|
107
|
+
const own =
|
|
108
|
+
!speaker ||
|
|
109
|
+
speaker.principalId === owner.id ||
|
|
110
|
+
speaker.principalId === SYSTEM_PRINCIPAL;
|
|
111
|
+
return own ? owner : addressee({ ...speaker, tier: "member" }, owner);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Whose memory a session's running turn reads: a private conversation's person's, whoever speaks
|
|
116
|
+
* in it, and in a shared one the speaker's; no one's for the host's own turns there, nor while
|
|
117
|
+
* no turn runs.
|
|
118
|
+
*/
|
|
119
|
+
export function memoryReader(
|
|
120
|
+
conversation: SessionConversation,
|
|
121
|
+
speaker: Speaker | undefined,
|
|
122
|
+
): string | undefined {
|
|
123
|
+
if (conversation.visibility === "private") return conversation.principalId;
|
|
124
|
+
if (!speaker || speaker.principalId === SYSTEM_PRINCIPAL) return undefined;
|
|
125
|
+
return speaker.principalId;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Why a turn may not run in the session's conversation: someone other than its person speaking
|
|
130
|
+
* in a private one. The host's own turns may.
|
|
131
|
+
*/
|
|
132
|
+
export function refusedSpeaker(
|
|
133
|
+
conversation: SessionConversation,
|
|
134
|
+
speaker: Speaker,
|
|
135
|
+
): string | undefined {
|
|
136
|
+
if (conversation.visibility !== "private") return undefined;
|
|
137
|
+
if (
|
|
138
|
+
speaker.principalId === conversation.principalId ||
|
|
139
|
+
speaker.principalId === SYSTEM_PRINCIPAL
|
|
140
|
+
)
|
|
141
|
+
return undefined;
|
|
142
|
+
return `the conversation is private to ${conversation.principalId}, so a turn of ${speaker.principalId} may not run in it`;
|
|
143
|
+
}
|