pi-roundtable 0.24.2 → 0.26.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 +38 -0
- package/docs/plugins.md +174 -7
- package/package.json +1 -1
- package/src/cli/checks/agent-tools.ts +23 -0
- package/src/cli/doctor.ts +6 -0
- package/src/cli/principal.ts +9 -2
- package/src/core/agents/agent-book.ts +155 -0
- package/src/core/agents/agent-ports.ts +5 -1
- package/src/core/agents/agent-prompt.ts +6 -1
- package/src/core/agents/agent-schema.ts +3 -0
- package/src/core/agents/agent-statements.ts +179 -0
- package/src/core/agents/agent-store.ts +184 -191
- package/src/core/agents/agent-team-fixture.ts +21 -2
- package/src/core/agents/agent-team.ts +44 -36
- package/src/core/agents/agent-tools.ts +12 -6
- package/src/core/agents/team-editing.ts +154 -32
- package/src/core/agents/team-keys.ts +17 -0
- package/src/core/agents/team-lifecycle.ts +96 -23
- package/src/core/agents/team-options.ts +9 -4
- package/src/core/agents/team-selection.ts +32 -0
- package/src/core/builtin/agent-server.ts +30 -5
- package/src/core/builtin/channel-context.ts +21 -0
- package/src/core/builtin/discord.ts +4 -1
- package/src/core/builtin/prompts.ts +40 -0
- package/src/core/builtin/skills.ts +9 -5
- package/src/core/config/config-schema.ts +22 -6
- package/src/core/config/config.ts +30 -2
- package/src/core/config/conversations.ts +21 -0
- package/src/core/config/profiles.ts +53 -0
- package/src/core/contract/channel-context.ts +106 -0
- package/src/core/contract/surface.ts +16 -0
- package/src/core/conversations/conversation-registry.ts +6 -0
- package/src/core/conversations/conversation-retention.ts +107 -0
- package/src/core/conversations/conversation-store.ts +6 -0
- package/src/core/conversations/conversations-plugin.ts +19 -3
- package/src/core/define-roundtable.ts +12 -2
- package/src/core/discord/channel-context.ts +47 -78
- package/src/core/discord/discord-surface.ts +16 -0
- package/src/core/events.ts +1 -0
- package/src/core/host-registration.ts +4 -0
- package/src/core/host.ts +3 -1
- package/src/core/i18n/agent-panel.ts +4 -0
- package/src/core/i18n/cards.ts +1 -1
- package/src/core/identity/actor-facts.ts +7 -0
- package/src/core/identity/identity-plugin.ts +27 -5
- package/src/core/identity/identity-port.ts +62 -0
- package/src/core/identity/identity-schema.ts +2 -0
- package/src/core/identity/identity-service.ts +45 -52
- package/src/core/identity/identity-view.ts +1 -0
- package/src/core/identity/person-profiles.ts +275 -0
- package/src/core/identity/principal-tables.ts +10 -0
- package/src/core/identity/profile-choices.ts +45 -0
- package/src/core/identity/profile-memory.ts +65 -0
- package/src/core/identity/profile-picture.ts +117 -0
- package/src/core/identity/profiles-view.ts +20 -0
- package/src/core/identity/profiles.ts +82 -0
- package/src/core/modules/host-shell/shell-policy.ts +18 -3
- package/src/core/modules/prompts/prompt-documents.ts +425 -0
- package/src/core/modules/prompts/prompt-history.ts +103 -0
- package/src/core/modules/skills/skill-contract.ts +52 -0
- package/src/core/modules/skills/skill-document-files.ts +66 -0
- package/src/core/modules/skills/skill-registry.ts +136 -8
- package/src/core/modules/skills/skill-rules.ts +94 -0
- package/src/core/modules/skills/skill-store.ts +88 -17
- package/src/core/modules/skills/skill-tools.ts +18 -10
- package/src/core/plugin.ts +40 -0
- package/src/core/registry/contributions.ts +8 -1
- package/src/core/runtime/chat-only.ts +32 -0
- package/src/core/runtime/conversation-sessions.ts +2 -1
- package/src/core/runtime/extensions/agent-prompt.ts +20 -5
- package/src/core/runtime/extensions/persona-prompt.ts +39 -0
- package/src/core/runtime/extensions/prompt-read.ts +22 -0
- package/src/core/runtime/runtime-plugin.ts +1 -0
- package/src/core/runtime/runtime-types.ts +3 -1
- package/src/core/runtime/session-factory.ts +70 -65
- package/src/core/runtime/session-helpers.ts +27 -0
- package/src/core/services.ts +97 -33
- package/src/index.ts +31 -0
- package/src/kit/index.ts +1 -0
- package/src/kit/oidc.ts +16 -0
- package/src/testing.ts +1 -0
|
@@ -81,11 +81,12 @@ export function skillListExtension(
|
|
|
81
81
|
);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
/** The skill tools of an agent session. */
|
|
84
|
+
/** The skill tools of an agent session; a skill the agent writes or edits is kept under `by`, the agent's name. */
|
|
85
85
|
export function skillToolsExtension(
|
|
86
86
|
registry: StoredSkillRegistry,
|
|
87
87
|
/** Throws AgentError unless the name is an active agent. */
|
|
88
88
|
checkAgent: (name: string) => void,
|
|
89
|
+
by: string,
|
|
89
90
|
): ExtensionFactory {
|
|
90
91
|
return textToolsExtension(
|
|
91
92
|
[
|
|
@@ -127,11 +128,14 @@ export function skillToolsExtension(
|
|
|
127
128
|
body: BODY,
|
|
128
129
|
}),
|
|
129
130
|
run: (i) =>
|
|
130
|
-
registry.create(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
registry.create(
|
|
132
|
+
{
|
|
133
|
+
name: requiredString(i, "name"),
|
|
134
|
+
description: requiredString(i, "description"),
|
|
135
|
+
body: requiredString(i, "body"),
|
|
136
|
+
},
|
|
137
|
+
by,
|
|
138
|
+
),
|
|
135
139
|
},
|
|
136
140
|
{
|
|
137
141
|
name: "skill_update",
|
|
@@ -146,10 +150,14 @@ export function skillToolsExtension(
|
|
|
146
150
|
run: (i) => {
|
|
147
151
|
const description = optionalString(i, "description");
|
|
148
152
|
const body = optionalString(i, "body");
|
|
149
|
-
return registry.update(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
+
return registry.update(
|
|
154
|
+
requiredString(i, "name"),
|
|
155
|
+
{
|
|
156
|
+
...(description !== undefined ? { description } : {}),
|
|
157
|
+
...(body !== undefined ? { body } : {}),
|
|
158
|
+
},
|
|
159
|
+
by,
|
|
160
|
+
);
|
|
153
161
|
},
|
|
154
162
|
},
|
|
155
163
|
{
|
package/src/core/plugin.ts
CHANGED
|
@@ -107,10 +107,30 @@ export interface EventHandlers {
|
|
|
107
107
|
turnProgress?(event: TurnProgressEvent): Promise<void> | void;
|
|
108
108
|
/** The team changed: an agent or group was created, edited, arranged, archived, or started over. */
|
|
109
109
|
changed?(): Promise<void> | void;
|
|
110
|
+
/**
|
|
111
|
+
* Something a person did through a plugin's surface, reported by any plugin through
|
|
112
|
+
* `context.events.action`: for example a support inbox's "assign", "reply" or "close". It names
|
|
113
|
+
* the kind, who did it and whether it worked, never the content.
|
|
114
|
+
*/
|
|
115
|
+
action?(event: ActionEvent): Promise<void> | void;
|
|
110
116
|
/** The shutdown drain ended, before any service stops; `left` is the work it gave up on. */
|
|
111
117
|
shutdown?(left: readonly string[]): Promise<void> | void;
|
|
112
118
|
}
|
|
113
119
|
|
|
120
|
+
/** One thing a person did through a plugin's surface, as a handler hears of it. */
|
|
121
|
+
export interface ActionEvent {
|
|
122
|
+
/** What was done, in the reporting plugin's words, such as "assign" or "close". */
|
|
123
|
+
kind: string;
|
|
124
|
+
/** The `Speaker.id` of who did it. */
|
|
125
|
+
actor: string;
|
|
126
|
+
principalId: string;
|
|
127
|
+
/** The conversation or page it concerns, when there is one. */
|
|
128
|
+
channel?: ChannelKey | `page:${string}`;
|
|
129
|
+
/** Whether it worked. */
|
|
130
|
+
ok: boolean;
|
|
131
|
+
at: Date;
|
|
132
|
+
}
|
|
133
|
+
|
|
114
134
|
/** What the core reports to the plugins' handlers. */
|
|
115
135
|
export interface EventSink {
|
|
116
136
|
turnStarted(turn: TurnEvent): void;
|
|
@@ -118,6 +138,8 @@ export interface EventSink {
|
|
|
118
138
|
/** Optional, so a sink of your own written before it still fits. */
|
|
119
139
|
turnProgress?(event: TurnProgressEvent): void;
|
|
120
140
|
changed(): void;
|
|
141
|
+
/** Optional, as `turnProgress` is: a sink of your own written before it still fits. */
|
|
142
|
+
action?(event: ActionEvent): void;
|
|
121
143
|
}
|
|
122
144
|
|
|
123
145
|
/** What a prompt section is built for: one agent's turn. */
|
|
@@ -265,6 +287,12 @@ export interface LinkedSessions {
|
|
|
265
287
|
agentSelection(): ToolSelection;
|
|
266
288
|
}
|
|
267
289
|
|
|
290
|
+
/**
|
|
291
|
+
* What the agents' turns may use of the machine: "full" gives them the shell and file tools, and
|
|
292
|
+
* "chat-only" none of them, as `agentServer: { tools: "chat-only" }` configures.
|
|
293
|
+
*/
|
|
294
|
+
export type AgentTools = "full" | "chat-only";
|
|
295
|
+
|
|
268
296
|
/** The host's own locale and time zone, as its plugins read them. */
|
|
269
297
|
export interface HostEnv {
|
|
270
298
|
readonly locale: Locale;
|
|
@@ -284,6 +312,12 @@ export interface HostEnv {
|
|
|
284
312
|
* agent wears the same ones.
|
|
285
313
|
*/
|
|
286
314
|
readonly characterSkins: readonly Hex[];
|
|
315
|
+
/**
|
|
316
|
+
* Whether the agents' turns carry the shell and file tools. A plugin whose tools reach the host
|
|
317
|
+
* checks it before it offers them to agents; under "chat-only" the host refuses to start with a
|
|
318
|
+
* plugin that declares `hostAccess`.
|
|
319
|
+
*/
|
|
320
|
+
readonly agentTools: AgentTools;
|
|
287
321
|
}
|
|
288
322
|
|
|
289
323
|
/** What the host gives every plugin. */
|
|
@@ -391,6 +425,12 @@ export interface RoundtablePlugin {
|
|
|
391
425
|
* dropped plugin provides a service this one does not replace.
|
|
392
426
|
*/
|
|
393
427
|
replaces?: readonly ServiceKey<unknown>[];
|
|
428
|
+
/**
|
|
429
|
+
* Marks a plugin whose tools reach the host's machine, such as one that runs shells or edits
|
|
430
|
+
* its files for the agents. A host that configures `agentServer: { tools: "chat-only" }`
|
|
431
|
+
* refuses to start with one registered, with a ConfigError that names the plugin.
|
|
432
|
+
*/
|
|
433
|
+
hostAccess?: true;
|
|
394
434
|
setup(context: PluginContext): Promise<Contribution> | Contribution;
|
|
395
435
|
/**
|
|
396
436
|
* Runs once every plugin is set up and linked, before any service starts; a failure stops the
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type DirectChannelProvider,
|
|
26
26
|
directChannelsPort,
|
|
27
27
|
} from "../presence/direct-channels.ts";
|
|
28
|
+
import { refuseShellTools } from "../runtime/chat-only.ts";
|
|
28
29
|
import {
|
|
29
30
|
compileSessionPlan,
|
|
30
31
|
type SessionTool,
|
|
@@ -451,8 +452,14 @@ export async function collectContributions(
|
|
|
451
452
|
registry.holdRules.push(tool.hold);
|
|
452
453
|
}
|
|
453
454
|
}
|
|
454
|
-
if (contribution.agentSelection)
|
|
455
|
+
if (contribution.agentSelection) {
|
|
456
|
+
if (context.env.agentTools === "chat-only")
|
|
457
|
+
refuseShellTools(
|
|
458
|
+
contribution.agentSelection().tools,
|
|
459
|
+
`plugin ${plugin.name}: agentSelection`,
|
|
460
|
+
);
|
|
455
461
|
registry.agentSelections.push(contribution.agentSelection);
|
|
462
|
+
}
|
|
456
463
|
if (events) registry.handlers.push({ plugin: plugin.name, events });
|
|
457
464
|
registry.routes.push(...http);
|
|
458
465
|
registry.holdRules.push(...holdRules);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ConfigError } from "../domain/errors.ts";
|
|
2
|
+
import { SHELL_TOOLS } from "../modules/host-shell/shell-policy.ts";
|
|
3
|
+
import type { RoundtablePlugin } from "../plugin.ts";
|
|
4
|
+
|
|
5
|
+
const CHAT_ONLY = 'agentServer: { tools: "chat-only" }';
|
|
6
|
+
|
|
7
|
+
/** The shell and file tools among these names. */
|
|
8
|
+
export function shellToolsIn(names: Iterable<string>): string[] {
|
|
9
|
+
const shell: readonly string[] = SHELL_TOOLS;
|
|
10
|
+
return [...new Set(names)].filter((name) => shell.includes(name));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Refuses names that carry the machine's shell and file tools: the one place a chat-only host
|
|
15
|
+
* checks, where a selection turns into active tools. `where` says whose selection it is.
|
|
16
|
+
*/
|
|
17
|
+
export function refuseShellTools(names: Iterable<string>, where: string): void {
|
|
18
|
+
const found = shellToolsIn(names);
|
|
19
|
+
if (found.length > 0)
|
|
20
|
+
throw new ConfigError(
|
|
21
|
+
`${where} names ${found.join(", ")}, which ${CHAT_ONLY} withholds from every turn. Take the tool out of the selection, or remove tools: "chat-only" from agentServer to give the agents the shell.`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Refuses, at start, a plugin whose tools reach the host: an agent of a chat-only host must not. */
|
|
26
|
+
export function refuseHostAccess(plugins: readonly RoundtablePlugin[]): void {
|
|
27
|
+
const reaching = plugins.find((plugin) => plugin.hostAccess === true);
|
|
28
|
+
if (reaching)
|
|
29
|
+
throw new ConfigError(
|
|
30
|
+
`plugin ${reaching.name} declares hostAccess: its tools reach the host's machine, and the configuration says ${CHAT_ONLY}. Remove the plugin, or remove tools: "chat-only" from agentServer.`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -102,7 +102,8 @@ export class ConversationSessions {
|
|
|
102
102
|
this.#factory.link().holds,
|
|
103
103
|
this.#options.owner,
|
|
104
104
|
await this.#options.confirmations.load(key),
|
|
105
|
-
agents
|
|
105
|
+
// A chat-only host's agents have no shell, so their gate judges no workspace.
|
|
106
|
+
agents && this.#options.agentTools !== "chat-only"
|
|
106
107
|
? {
|
|
107
108
|
workspace: agents.workDir,
|
|
108
109
|
...(agents.scratchDir ? { scratchDir: agents.scratchDir } : {}),
|
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Logger } from "../../log.ts";
|
|
3
|
+
import { readPromptOrStop } from "./prompt-read.ts";
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Puts the agent's current prompt first in the system prompt's appended part before each run,
|
|
5
|
-
* keeping what earlier handlers appended, such as the owner memory section.
|
|
7
|
+
* keeping what earlier handlers appended, such as the owner memory section. A `prompt()` that
|
|
8
|
+
* throws is logged as an error and stops the run: an agent never runs without its prompt.
|
|
6
9
|
*/
|
|
7
|
-
export function agentPromptExtension(
|
|
10
|
+
export function agentPromptExtension(
|
|
11
|
+
prompt: () => string,
|
|
12
|
+
logger: Logger,
|
|
13
|
+
/** The agent's name, named in the log. */
|
|
14
|
+
agent: string,
|
|
15
|
+
): ExtensionFactory {
|
|
8
16
|
return (pi) => {
|
|
9
|
-
pi.on("before_agent_start", (event) => {
|
|
17
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
18
|
+
const text = readPromptOrStop(
|
|
19
|
+
prompt,
|
|
20
|
+
ctx,
|
|
21
|
+
logger,
|
|
22
|
+
`The prompt of the agent "${agent}"`,
|
|
23
|
+
);
|
|
24
|
+
if (text === undefined) return;
|
|
10
25
|
const appended = event.systemPromptOptions.appendSystemPrompt;
|
|
11
26
|
event.systemPromptOptions.appendSystemPrompt = appended
|
|
12
|
-
? `${
|
|
13
|
-
:
|
|
27
|
+
? `${text}\n\n${appended}`
|
|
28
|
+
: text;
|
|
14
29
|
});
|
|
15
30
|
};
|
|
16
31
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Logger } from "../../log.ts";
|
|
3
|
+
import { readPromptOrStop } from "./prompt-read.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Brings the persona in the system prompt up to date before each run. The session was built with
|
|
7
|
+
* `initial`, the persona's text at that moment; when `current()` reads differently now, such as a
|
|
8
|
+
* prompt document the owner edited in the app, this run carries the new text in the same place.
|
|
9
|
+
* A `current()` that throws is logged as an error and stops the run: it does not go on with the
|
|
10
|
+
* text the session was built with.
|
|
11
|
+
*/
|
|
12
|
+
export function personaPromptExtension(
|
|
13
|
+
initial: string,
|
|
14
|
+
current: () => string,
|
|
15
|
+
logger: Logger,
|
|
16
|
+
/** The conversation kind, named in the log. */
|
|
17
|
+
kind: string,
|
|
18
|
+
): ExtensionFactory {
|
|
19
|
+
return (pi) => {
|
|
20
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
21
|
+
const now = readPromptOrStop(
|
|
22
|
+
current,
|
|
23
|
+
ctx,
|
|
24
|
+
logger,
|
|
25
|
+
`The persona of the "${kind}" conversations`,
|
|
26
|
+
);
|
|
27
|
+
if (now === undefined || now === initial) return;
|
|
28
|
+
const appended = event.systemPromptOptions.appendSystemPrompt;
|
|
29
|
+
if (!appended) {
|
|
30
|
+
event.systemPromptOptions.appendSystemPrompt = now;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
event.systemPromptOptions.appendSystemPrompt =
|
|
34
|
+
initial === ""
|
|
35
|
+
? `${now}\n\n${appended}`
|
|
36
|
+
: appended.split(initial).join(now);
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Logger } from "../../log.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reads a prompt for the run that is starting. Pi swallows what a `before_agent_start` handler
|
|
5
|
+
* throws, so a prompt that cannot be read would let the run go on quietly with the prompt the
|
|
6
|
+
* session was built with, or with none at all. The failure is logged as an error and the run is
|
|
7
|
+
* stopped instead; the result is then undefined.
|
|
8
|
+
*/
|
|
9
|
+
export function readPromptOrStop(
|
|
10
|
+
read: () => string,
|
|
11
|
+
run: { abort(): void },
|
|
12
|
+
logger: Logger,
|
|
13
|
+
what: string,
|
|
14
|
+
): string | undefined {
|
|
15
|
+
try {
|
|
16
|
+
return read();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
logger.error({ err: error }, `${what} cannot be read; the run is stopped`);
|
|
19
|
+
run.abort();
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -16,7 +16,7 @@ import type { Logger } from "../log.ts";
|
|
|
16
16
|
import type { EffortPolicy, ThinkingLevel, ThinkingPicker } from "../models.ts";
|
|
17
17
|
import type { Autonomy } from "../modes/autonomy.ts";
|
|
18
18
|
import type { ConversationModes } from "../modes/conversation-modes.ts";
|
|
19
|
-
import type { LinkedSessions } from "../plugin.ts";
|
|
19
|
+
import type { AgentTools, LinkedSessions } from "../plugin.ts";
|
|
20
20
|
import {
|
|
21
21
|
planOrder,
|
|
22
22
|
type SessionContext,
|
|
@@ -75,6 +75,8 @@ export interface PiAgentRuntimeOptions {
|
|
|
75
75
|
sessions: () => LinkedSessions;
|
|
76
76
|
/** The agent server's sessions: their shared workspace, model, and skills. */
|
|
77
77
|
agents?: AgentSessions;
|
|
78
|
+
/** "chat-only": no turn carries the shell and file tools, and agent sessions have no workspace; default "full". */
|
|
79
|
+
agentTools?: AgentTools;
|
|
78
80
|
/**
|
|
79
81
|
* The prompts of a channel for an interactive turn's scope, made from its speaker and its
|
|
80
82
|
* conversation's visibility; undefined for a channel that cannot show them. Without it every
|
|
@@ -3,14 +3,11 @@ import { join } from "node:path";
|
|
|
3
3
|
import {
|
|
4
4
|
type AgentSession,
|
|
5
5
|
createAgentSession,
|
|
6
|
-
createBashToolDefinition,
|
|
7
6
|
DefaultResourceLoader,
|
|
8
7
|
type ModelRuntime,
|
|
9
8
|
type SessionManager,
|
|
10
|
-
type ToolDefinition,
|
|
11
9
|
} from "@earendil-works/pi-coding-agent";
|
|
12
10
|
import { channelSegment } from "../attachments/attachment-dir.ts";
|
|
13
|
-
import type { ImageDrawer } from "../contract/providers.ts";
|
|
14
11
|
import type { ClientPort } from "../domain/client.ts";
|
|
15
12
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
16
13
|
import { ConfigError } from "../domain/errors.ts";
|
|
@@ -29,16 +26,13 @@ import {
|
|
|
29
26
|
ATTACH_FILE_TOOL,
|
|
30
27
|
attachFileExtension,
|
|
31
28
|
} from "../shared/attach-file-tool.ts";
|
|
32
|
-
import {
|
|
33
|
-
GENERATE_IMAGE_TOOL,
|
|
34
|
-
generateImageExtension,
|
|
35
|
-
} from "../shared/generate-image-tool.ts";
|
|
29
|
+
import { GENERATE_IMAGE_TOOL } from "../shared/generate-image-tool.ts";
|
|
36
30
|
import { packageDir } from "../shared/package-dir.ts";
|
|
37
31
|
import { readAttachmentExtension } from "../shared/read-attachment-tool.ts";
|
|
38
|
-
import type { WorkspaceRoots } from "../shared/workspace-files.ts";
|
|
39
32
|
import { type Speaker, THE_SPEAKER } from "../speakers.ts";
|
|
40
33
|
import { currentToolNames } from "../tool-tiers.ts";
|
|
41
34
|
import { toolTurn } from "../tool-turn.ts";
|
|
35
|
+
import { refuseShellTools } from "./chat-only.ts";
|
|
42
36
|
import {
|
|
43
37
|
CompactionTiers,
|
|
44
38
|
compactionEngine,
|
|
@@ -49,6 +43,7 @@ import {
|
|
|
49
43
|
type ConfirmationGate,
|
|
50
44
|
confirmationGateExtension,
|
|
51
45
|
} from "./extensions/confirmation-gate.ts";
|
|
46
|
+
import { personaPromptExtension } from "./extensions/persona-prompt.ts";
|
|
52
47
|
import {
|
|
53
48
|
MemoryDraws,
|
|
54
49
|
privateCompaction,
|
|
@@ -63,7 +58,6 @@ import type { PromptSlot } from "./prompt-slot.ts";
|
|
|
63
58
|
import type {
|
|
64
59
|
ChannelSession,
|
|
65
60
|
CompactionEnd,
|
|
66
|
-
CoreExtensions,
|
|
67
61
|
PiAgentRuntimeOptions,
|
|
68
62
|
} from "./runtime-types.ts";
|
|
69
63
|
import {
|
|
@@ -77,6 +71,9 @@ import {
|
|
|
77
71
|
sessionAddressee,
|
|
78
72
|
turnAddressee,
|
|
79
73
|
} from "./session-conversation.ts";
|
|
74
|
+
import { imageTools, scratchBash } from "./session-helpers.ts";
|
|
75
|
+
|
|
76
|
+
export { imageTools, scratchBash };
|
|
80
77
|
|
|
81
78
|
/** What a session factory asks of the runtime that owns the turns. */
|
|
82
79
|
export interface SessionFactoryDeps {
|
|
@@ -138,11 +135,22 @@ export class SessionFactory {
|
|
|
138
135
|
);
|
|
139
136
|
}
|
|
140
137
|
|
|
138
|
+
/** Whether the host's agents carry no shell or file tools. */
|
|
139
|
+
get chatOnly(): boolean {
|
|
140
|
+
return this.#options.agentTools === "chat-only";
|
|
141
|
+
}
|
|
142
|
+
|
|
141
143
|
get plan(): SessionPlan {
|
|
142
144
|
return this.link().plan;
|
|
143
145
|
}
|
|
144
146
|
|
|
147
|
+
/**
|
|
148
|
+
* The tools a selection turns into. A chat-only host refuses a selection that names a shell or
|
|
149
|
+
* file tool, whatever built it (the agent team, `context.turns`, a plugin), so the turn fails
|
|
150
|
+
* closed before any session runs it.
|
|
151
|
+
*/
|
|
145
152
|
toolsFor(selection: ToolSelection): string[] {
|
|
153
|
+
if (this.chatOnly) refuseShellTools(selection.tools, "a tool selection");
|
|
146
154
|
const groups = planOrder(this.plan).flatMap(
|
|
147
155
|
(tool) => tool.snapshot().groups ?? [],
|
|
148
156
|
);
|
|
@@ -359,7 +367,8 @@ export class SessionFactory {
|
|
|
359
367
|
},
|
|
360
368
|
conversation,
|
|
361
369
|
addressee,
|
|
362
|
-
|
|
370
|
+
// A chat-only host's agents work in no workspace.
|
|
371
|
+
...(agents && !this.chatOnly
|
|
363
372
|
? {
|
|
364
373
|
workspace: {
|
|
365
374
|
workspace: cwd,
|
|
@@ -390,42 +399,57 @@ export class SessionFactory {
|
|
|
390
399
|
noThemes: true,
|
|
391
400
|
noContextFiles: true,
|
|
392
401
|
additionalExtensionPaths: this.link().extensionPaths,
|
|
393
|
-
extensionFactories:
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
402
|
+
extensionFactories: [
|
|
403
|
+
...(persona === undefined
|
|
404
|
+
? []
|
|
405
|
+
: [
|
|
406
|
+
{
|
|
407
|
+
name: "persona-prompt",
|
|
408
|
+
factory: personaPromptExtension(
|
|
409
|
+
persona,
|
|
410
|
+
() => this.personaOf(kind),
|
|
411
|
+
logger,
|
|
412
|
+
kind,
|
|
413
|
+
),
|
|
414
|
+
},
|
|
415
|
+
]),
|
|
416
|
+
...sessionExtensions(this.plan, context, {
|
|
417
|
+
readAttachment: readAttachmentExtension(attachmentDir),
|
|
418
|
+
...(context.workspace
|
|
419
|
+
? { attachFile: attachFileExtension(context.workspace) }
|
|
420
|
+
: {}),
|
|
421
|
+
...imageTools(
|
|
422
|
+
this.#options.images,
|
|
423
|
+
context.workspace ?? { workspace: cwd },
|
|
410
424
|
),
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
425
|
+
confirmationGate: confirmationGateExtension(gate, slot, (signal) =>
|
|
426
|
+
toolTurn(context, signal),
|
|
427
|
+
),
|
|
428
|
+
askUser: askUserExtension(slot, addressee, () =>
|
|
429
|
+
turnAddressee(
|
|
430
|
+
this.#deps.speaker(turnKey),
|
|
431
|
+
state,
|
|
432
|
+
this.#options.owner,
|
|
433
|
+
),
|
|
434
|
+
),
|
|
435
|
+
selfCompactGuard: selfCompactGuardExtension(),
|
|
436
|
+
// Each request carries no one's memory but the running turn's reader's.
|
|
437
|
+
privateMemory: privateMemoryExtension(
|
|
438
|
+
conversation.visibility === "shared",
|
|
439
|
+
() =>
|
|
440
|
+
deps.withholdsMemory(turnKey)
|
|
441
|
+
? undefined
|
|
442
|
+
: memoryReader(conversation, deps.speaker(turnKey)),
|
|
443
|
+
state.draws,
|
|
444
|
+
),
|
|
445
|
+
surfaceFormatting: surfaceFormattingExtension(() =>
|
|
446
|
+
// A worker writes a report for the turn, not a reply on its surface.
|
|
447
|
+
worker ? undefined : deps.formatting(turnKey),
|
|
448
|
+
),
|
|
449
|
+
activeTools: activeToolsExtension(() => state.tools),
|
|
450
|
+
}),
|
|
451
|
+
],
|
|
452
|
+
// An agent's prompt is set before each run by the agent-prompt extension, and another kind's persona is read again before each run.
|
|
429
453
|
appendSystemPrompt: persona === undefined ? [] : [persona],
|
|
430
454
|
});
|
|
431
455
|
await resourceLoader.reload();
|
|
@@ -434,7 +458,7 @@ export class SessionFactory {
|
|
|
434
458
|
cwd,
|
|
435
459
|
agentDir,
|
|
436
460
|
// Pi's bash under its own name, run with TMPDIR at the agents' scratch dir.
|
|
437
|
-
...(agents?.scratchDir
|
|
461
|
+
...(agents?.scratchDir && !this.chatOnly
|
|
438
462
|
? { customTools: [scratchBash(cwd, agents.scratchDir)] }
|
|
439
463
|
: {}),
|
|
440
464
|
thinkingLevel: thinking,
|
|
@@ -463,22 +487,3 @@ export class SessionFactory {
|
|
|
463
487
|
return Object.assign(state, { session });
|
|
464
488
|
}
|
|
465
489
|
}
|
|
466
|
-
|
|
467
|
-
/** `generate_image` for a session of a host with an image provider; nothing without one. */
|
|
468
|
-
export function imageTools(
|
|
469
|
-
images: ImageDrawer | undefined,
|
|
470
|
-
roots: WorkspaceRoots,
|
|
471
|
-
): Pick<CoreExtensions, "generateImage"> {
|
|
472
|
-
return images ? { generateImage: generateImageExtension(images, roots) } : {};
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/** Pi's bash tool with TMPDIR set to `scratchDir`, so tools that read TMPDIR and GNU `mktemp` write there (BSD `mktemp` needs `-p "$TMPDIR"`). */
|
|
476
|
-
export function scratchBash(cwd: string, scratchDir: string): ToolDefinition {
|
|
477
|
-
// SAFETY: Pi's bash definition is a ToolDefinition typed by its own parameter schema, which the session takes as any custom tool.
|
|
478
|
-
return createBashToolDefinition(cwd, {
|
|
479
|
-
spawnHook: (context) => ({
|
|
480
|
-
...context,
|
|
481
|
-
env: { ...context.env, TMPDIR: scratchDir },
|
|
482
|
-
}),
|
|
483
|
-
}) as unknown as ToolDefinition;
|
|
484
|
-
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createBashToolDefinition,
|
|
3
|
+
type ToolDefinition,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { ImageDrawer } from "../contract/providers.ts";
|
|
6
|
+
import { generateImageExtension } from "../shared/generate-image-tool.ts";
|
|
7
|
+
import type { WorkspaceRoots } from "../shared/workspace-files.ts";
|
|
8
|
+
import type { CoreExtensions } from "./runtime-types.ts";
|
|
9
|
+
|
|
10
|
+
/** `generate_image` for a session of a host with an image provider; nothing without one. */
|
|
11
|
+
export function imageTools(
|
|
12
|
+
images: ImageDrawer | undefined,
|
|
13
|
+
roots: WorkspaceRoots,
|
|
14
|
+
): Pick<CoreExtensions, "generateImage"> {
|
|
15
|
+
return images ? { generateImage: generateImageExtension(images, roots) } : {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Pi's bash tool with TMPDIR set to `scratchDir`, so tools that read TMPDIR and GNU `mktemp` write there (BSD `mktemp` needs `-p "$TMPDIR"`). */
|
|
19
|
+
export function scratchBash(cwd: string, scratchDir: string): ToolDefinition {
|
|
20
|
+
// SAFETY: Pi's bash definition is a ToolDefinition typed by its own parameter schema, which the session takes as any custom tool.
|
|
21
|
+
return createBashToolDefinition(cwd, {
|
|
22
|
+
spawnHook: (context) => ({
|
|
23
|
+
...context,
|
|
24
|
+
env: { ...context.env, TMPDIR: scratchDir },
|
|
25
|
+
}),
|
|
26
|
+
}) as unknown as ToolDefinition;
|
|
27
|
+
}
|