pi-roundtable 0.24.2 → 0.25.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/docs/plugins.md +45 -1
  3. package/package.json +1 -1
  4. package/src/core/agents/agent-book.ts +155 -0
  5. package/src/core/agents/agent-ports.ts +5 -1
  6. package/src/core/agents/agent-schema.ts +3 -0
  7. package/src/core/agents/agent-statements.ts +179 -0
  8. package/src/core/agents/agent-store.ts +184 -191
  9. package/src/core/agents/agent-team-fixture.ts +21 -2
  10. package/src/core/agents/agent-team.ts +42 -36
  11. package/src/core/agents/agent-tools.ts +12 -6
  12. package/src/core/agents/team-editing.ts +150 -32
  13. package/src/core/agents/team-keys.ts +17 -0
  14. package/src/core/agents/team-lifecycle.ts +96 -23
  15. package/src/core/agents/team-options.ts +6 -3
  16. package/src/core/agents/team-selection.ts +28 -0
  17. package/src/core/builtin/agent-server.ts +29 -5
  18. package/src/core/builtin/prompts.ts +40 -0
  19. package/src/core/builtin/skills.ts +9 -5
  20. package/src/core/define-roundtable.ts +3 -0
  21. package/src/core/i18n/agent-panel.ts +4 -0
  22. package/src/core/i18n/cards.ts +1 -1
  23. package/src/core/modules/prompts/prompt-documents.ts +425 -0
  24. package/src/core/modules/prompts/prompt-history.ts +103 -0
  25. package/src/core/modules/skills/skill-contract.ts +52 -0
  26. package/src/core/modules/skills/skill-document-files.ts +66 -0
  27. package/src/core/modules/skills/skill-registry.ts +136 -8
  28. package/src/core/modules/skills/skill-rules.ts +94 -0
  29. package/src/core/modules/skills/skill-store.ts +88 -17
  30. package/src/core/modules/skills/skill-tools.ts +18 -10
  31. package/src/core/runtime/extensions/agent-prompt.ts +20 -5
  32. package/src/core/runtime/extensions/persona-prompt.ts +39 -0
  33. package/src/core/runtime/extensions/prompt-read.ts +22 -0
  34. package/src/core/runtime/session-factory.ts +51 -35
  35. package/src/core/services.ts +88 -33
  36. package/src/index.ts +13 -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
- name: requiredString(i, "name"),
132
- description: requiredString(i, "description"),
133
- body: requiredString(i, "body"),
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(requiredString(i, "name"), {
150
- ...(description !== undefined ? { description } : {}),
151
- ...(body !== undefined ? { body } : {}),
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
  {
@@ -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(prompt: () => string): ExtensionFactory {
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
- ? `${prompt()}\n\n${appended}`
13
- : prompt();
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
+ }
@@ -49,6 +49,7 @@ import {
49
49
  type ConfirmationGate,
50
50
  confirmationGateExtension,
51
51
  } from "./extensions/confirmation-gate.ts";
52
+ import { personaPromptExtension } from "./extensions/persona-prompt.ts";
52
53
  import {
53
54
  MemoryDraws,
54
55
  privateCompaction,
@@ -390,42 +391,57 @@ export class SessionFactory {
390
391
  noThemes: true,
391
392
  noContextFiles: true,
392
393
  additionalExtensionPaths: this.link().extensionPaths,
393
- extensionFactories: sessionExtensions(this.plan, context, {
394
- readAttachment: readAttachmentExtension(attachmentDir),
395
- ...(context.workspace
396
- ? { attachFile: attachFileExtension(context.workspace) }
397
- : {}),
398
- ...imageTools(
399
- this.#options.images,
400
- context.workspace ?? { workspace: cwd },
401
- ),
402
- confirmationGate: confirmationGateExtension(gate, slot, (signal) =>
403
- toolTurn(context, signal),
404
- ),
405
- askUser: askUserExtension(slot, addressee, () =>
406
- turnAddressee(
407
- this.#deps.speaker(turnKey),
408
- state,
409
- this.#options.owner,
394
+ extensionFactories: [
395
+ ...(persona === undefined
396
+ ? []
397
+ : [
398
+ {
399
+ name: "persona-prompt",
400
+ factory: personaPromptExtension(
401
+ persona,
402
+ () => this.personaOf(kind),
403
+ logger,
404
+ kind,
405
+ ),
406
+ },
407
+ ]),
408
+ ...sessionExtensions(this.plan, context, {
409
+ readAttachment: readAttachmentExtension(attachmentDir),
410
+ ...(context.workspace
411
+ ? { attachFile: attachFileExtension(context.workspace) }
412
+ : {}),
413
+ ...imageTools(
414
+ this.#options.images,
415
+ context.workspace ?? { workspace: cwd },
410
416
  ),
411
- ),
412
- selfCompactGuard: selfCompactGuardExtension(),
413
- // Each request carries no one's memory but the running turn's reader's.
414
- privateMemory: privateMemoryExtension(
415
- conversation.visibility === "shared",
416
- () =>
417
- deps.withholdsMemory(turnKey)
418
- ? undefined
419
- : memoryReader(conversation, deps.speaker(turnKey)),
420
- state.draws,
421
- ),
422
- surfaceFormatting: surfaceFormattingExtension(() =>
423
- // A worker writes a report for the turn, not a reply on its surface.
424
- worker ? undefined : deps.formatting(turnKey),
425
- ),
426
- activeTools: activeToolsExtension(() => state.tools),
427
- }),
428
- // An agent's prompt is set before each run by the agent-prompt extension.
417
+ confirmationGate: confirmationGateExtension(gate, slot, (signal) =>
418
+ toolTurn(context, signal),
419
+ ),
420
+ askUser: askUserExtension(slot, addressee, () =>
421
+ turnAddressee(
422
+ this.#deps.speaker(turnKey),
423
+ state,
424
+ this.#options.owner,
425
+ ),
426
+ ),
427
+ selfCompactGuard: selfCompactGuardExtension(),
428
+ // Each request carries no one's memory but the running turn's reader's.
429
+ privateMemory: privateMemoryExtension(
430
+ conversation.visibility === "shared",
431
+ () =>
432
+ deps.withholdsMemory(turnKey)
433
+ ? undefined
434
+ : memoryReader(conversation, deps.speaker(turnKey)),
435
+ state.draws,
436
+ ),
437
+ surfaceFormatting: surfaceFormattingExtension(() =>
438
+ // A worker writes a report for the turn, not a reply on its surface.
439
+ worker ? undefined : deps.formatting(turnKey),
440
+ ),
441
+ activeTools: activeToolsExtension(() => state.tools),
442
+ }),
443
+ ],
444
+ // 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
445
  appendSystemPrompt: persona === undefined ? [] : [persona],
430
446
  });
431
447
  await resourceLoader.reload();
@@ -25,6 +25,10 @@ import type {
25
25
  MemoryKind,
26
26
  PromptMemory,
27
27
  } from "./modules/memory/owner-memory-store.ts";
28
+ import type {
29
+ PromptDocuments,
30
+ PromptVersion,
31
+ } from "./modules/prompts/prompt-documents.ts";
28
32
  import type {
29
33
  PrecheckFinding,
30
34
  PrecheckRegistry,
@@ -34,11 +38,9 @@ import type {
34
38
  Schedule,
35
39
  ScheduleChange,
36
40
  } from "./modules/schedules/schedule-store.ts";
37
- import type {
38
- SkillCatalogEntry,
39
- SkillSet,
40
- } from "./modules/skills/skill-rules.ts";
41
+ import type { SkillRegistry } from "./modules/skills/skill-contract.ts";
41
42
  import type { AgentTurnScope, ChannelKey } from "./sessions.ts";
43
+ import type { Speaker } from "./speakers.ts";
42
44
  import type { DispatchThreads } from "./threads/dispatch-threads.ts";
43
45
 
44
46
  // The services the built-in plugins provide, and the ports they keep to. Each port is an
@@ -145,6 +147,16 @@ export const SKILLS: ServiceKey<SkillRegistry> = serviceKey<SkillRegistry>(
145
147
  },
146
148
  );
147
149
 
150
+ /**
151
+ * The prompt texts the owner edits in the app, each with a file version, an override and a
152
+ * history. Provided by the built-in `prompts` plugin, which registers `shared` and `guest` (the
153
+ * prompts of the agent server) and which every plugin set up after it may register more with; a
154
+ * plugin reads a document's `text()` when it builds a prompt, so an override applies from the next
155
+ * turn. Read it with `get`: the plugin is always on.
156
+ */
157
+ export const PROMPTS: ServiceKey<PromptDocuments> =
158
+ serviceKey<PromptDocuments>("roundtable.prompts");
159
+
148
160
  /** What the agent server builds, for the plugins set up after it. */
149
161
  export interface AgentServer {
150
162
  team: AgentTeam;
@@ -209,6 +221,29 @@ export interface AgentDirectory {
209
221
  groupByChannel(channelId: string): AgentGroup | undefined;
210
222
  }
211
223
 
224
+ /** What an owner gives to create an agent: the fields of `agent_create`, with the settings `update` takes. */
225
+ export interface NewAgent {
226
+ /** Permanent: lowercase letters, digits and dashes, 1 to 32. */
227
+ name: string;
228
+ displayName: string;
229
+ prompt: string;
230
+ /** Fields of its look; any left out come from the look its role suggests. */
231
+ character?: CharacterInput;
232
+ /** Its opening task; without it the agent only exists. */
233
+ task?: string;
234
+ /**
235
+ * The surface whose room the opening task runs in, so the person who asked sees it start
236
+ * there. Without it, or for a surface that hosts no rooms, the task runs where `agent_create`
237
+ * runs it: the agent's Discord channel, else its first room.
238
+ */
239
+ openIn?: string;
240
+ category?: string;
241
+ /** Skills it carries; refused when the skills addon is off. */
242
+ skills?: string[];
243
+ model?: string;
244
+ thinking?: string;
245
+ }
246
+
212
247
  /** What an agent's settings may change, as `update` takes it. */
213
248
  export interface AgentChange {
214
249
  displayName?: string;
@@ -259,8 +294,54 @@ export interface AgentTeam {
259
294
  postAs(caller: AgentTurnScope, text: string): Promise<void>;
260
295
  /** The assistant's own notice, posted in the coordinator's channel under its name. */
261
296
  announce(text: string): Promise<void>;
262
- /** Changes an agent's display name, prompt, model, or thinking level; a model must be one the host runs. */
263
- update(name: string, change: AgentChange): Promise<string>;
297
+ /**
298
+ * Changes an agent's display name, prompt, model, or thinking level; a model must be one the
299
+ * host runs. `by` names who asks; a changed prompt is kept in the agent's prompt history under it.
300
+ */
301
+ update(name: string, change: AgentChange, by?: string): Promise<string>;
302
+ /** The changes of an agent's prompt, newest first: who made each, when, and the text it replaced. */
303
+ promptHistory(name: string, limit?: number): Promise<PromptVersion[]>;
304
+ /**
305
+ * Creates an agent for an owner, outside any agent turn: the checks, picture, channel and
306
+ * opening task of `agent_create`, with `speaker` as the one who asked. `model` and `thinking`
307
+ * take what `update` takes and are checked before anything is made; `task` is optional, and
308
+ * without it the agent waits in its room. Throws AgentError for a bad or taken name, an
309
+ * unusable model or an unknown skill.
310
+ */
311
+ createAgent(speaker: Speaker, input: NewAgent): Promise<Agent>;
312
+ /**
313
+ * Archives an agent or group like the `archive` tool: it stops answering, its schedules are
314
+ * cancelled, an agent leaves its groups, and its history is kept. `caller` is the agent that
315
+ * asks, which cannot archive itself; an owner's request has none. The coordinator cannot be
316
+ * archived.
317
+ */
318
+ archive(caller: AgentTurnScope | undefined, name: string): Promise<string>;
319
+ /**
320
+ * Returns an archived agent or group to the active ones. An agent rejoins no group; a group
321
+ * needs all its members active. Throws AgentError for an unknown or active name.
322
+ */
323
+ restore(name: string): Promise<string>;
324
+ /**
325
+ * Deletes an archived agent or group for good and frees its name. Throws AgentError for an
326
+ * unknown name, for an active one (archive it first), and for an agent the host's seeds name,
327
+ * which the next start would seed again. `cleanup` is a surface's own part of the delete (what
328
+ * it kept under the name): it runs once the checks have passed and before anything is deleted,
329
+ * and one that throws leaves the agent or group in place, so the delete can be tried again.
330
+ */
331
+ remove(name: string, cleanup?: () => Promise<void>): Promise<string>;
332
+ /** Creates a group of 2 to 6 agents; the host is the first member unless given. */
333
+ createGroup(input: {
334
+ name: string;
335
+ displayName: string;
336
+ members: string[];
337
+ host?: string;
338
+ category?: string;
339
+ }): Promise<string>;
340
+ /** Changes a group's display name, members (all of them) or host. */
341
+ updateGroup(
342
+ name: string,
343
+ change: { displayName?: string; members?: string[]; host?: string },
344
+ ): Promise<string>;
264
345
  /**
265
346
  * Changes an agent's look: `input` is resolved over its current character (a new class keeps
266
347
  * its colors unless the input gives a palette or colors), rendered, and stored with the new
@@ -406,30 +487,4 @@ export interface SpeakerMemory {
406
487
  remove(text: string): Promise<string[]>;
407
488
  }
408
489
 
409
- /** The skills agents carry: built in, linked from repositories, or written by an agent. */
410
- export interface SkillRegistry {
411
- /** The built-in skills plus the named registered ones, resolved to their files. */
412
- resolve(names: readonly string[]): SkillSet;
413
- /** What an agent carries, built-in skills included. */
414
- carried(agent: string): SkillSet;
415
- /** The registered skills an agent carries, as the dashboard lists them. */
416
- carriedNames(agent: string): string[];
417
- /** An agent's skills as `agent_get` shows them. */
418
- describeCarried(agent: string): string;
419
- /** Every skill, built-in ones first. */
420
- catalog(): SkillCatalogEntry[];
421
- /** Every skill, or those of a group or matching a query, as the `skill_list` tool shows them. */
422
- list(filter?: { group?: string; query?: string }): string;
423
- /** The skills linked from a managed repository. */
424
- linkedFrom(repo: string): string[];
425
- /** Throws AgentError when a name is not registered; built-in names are carried already. */
426
- checkRegistered(names: readonly string[]): void;
427
- /** Links the skill at a repository path, or every child folder with a SKILL.md. */
428
- link(repo: string, path: string): Promise<string>;
429
- /** Adds and removes an agent's registered skills together, or changes nothing; returns what it carries after. */
430
- attach(
431
- agent: string,
432
- add: readonly string[],
433
- remove: readonly string[],
434
- ): Promise<string[]>;
435
- }
490
+ export type { SkillRegistry } from "./modules/skills/skill-contract.ts";
package/src/index.ts CHANGED
@@ -264,6 +264,14 @@ export type {
264
264
  PromptMemory,
265
265
  } from "./core/modules/memory/owner-memory-store.ts";
266
266
  export { MEMORY_KINDS } from "./core/modules/memory/owner-memory-store.ts";
267
+ export type {
268
+ PromptDocumentSpec,
269
+ PromptDocuments,
270
+ PromptDocumentView,
271
+ PromptHandle,
272
+ PromptVersion,
273
+ } from "./core/modules/prompts/prompt-documents.ts";
274
+ export { PromptConflictError } from "./core/modules/prompts/prompt-documents.ts";
267
275
  export type { PrecheckTool } from "./core/modules/schedules/precheck-tools.ts";
268
276
  export type {
269
277
  Precheck,
@@ -291,9 +299,12 @@ export type {
291
299
  export type {
292
300
  ResolvedSkill,
293
301
  SkillCatalogEntry,
302
+ SkillDocument,
294
303
  SkillSet,
295
304
  SkillSource,
305
+ SkillVersion,
296
306
  } from "./core/modules/skills/skill-rules.ts";
307
+ export { SkillConflictError } from "./core/modules/skills/skill-rules.ts";
297
308
  export type {
298
309
  Contribution,
299
310
  EventHandlers,
@@ -339,6 +350,7 @@ export type {
339
350
  DelegationRequest,
340
351
  Delegator,
341
352
  MemoryStore,
353
+ NewAgent,
342
354
  ScheduleStore,
343
355
  SkillRegistry,
344
356
  SpeakerMemory,
@@ -355,6 +367,7 @@ export {
355
367
  MEMORY,
356
368
  MODES,
357
369
  PRECHECKS,
370
+ PROMPTS,
358
371
  RUNTIME,
359
372
  SCHEDULES,
360
373
  SKILLS,