pi-roundtable 0.22.5 → 0.23.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.
Files changed (129) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/README.md +35 -9
  3. package/README.zh-TW.md +51 -13
  4. package/docs/migrating-0.8.md +2 -2
  5. package/docs/plugins.md +351 -28
  6. package/examples/discord-adapter.ts +9 -1
  7. package/examples/seeds.test.ts +2 -2
  8. package/examples/seeds.ts +1 -2
  9. package/package.json +6 -2
  10. package/src/characters/catalog.ts +1055 -0
  11. package/src/characters/characters.css +68 -0
  12. package/src/characters/colors.ts +108 -0
  13. package/src/characters/compose.ts +281 -0
  14. package/src/characters/css.ts +63 -0
  15. package/src/characters/index.ts +33 -0
  16. package/src/characters/parse.ts +196 -0
  17. package/src/characters/parts-face.ts +354 -0
  18. package/src/characters/parts-hair.ts +56 -0
  19. package/src/characters/parts-hats.ts +57 -0
  20. package/src/characters/parts-outfits.ts +81 -0
  21. package/src/characters/parts-props.ts +137 -0
  22. package/src/characters/spec.ts +224 -0
  23. package/src/characters/suggest.ts +101 -0
  24. package/src/cli/checks/characters.ts +23 -0
  25. package/src/cli/checks/images.ts +5 -3
  26. package/src/cli/doctor.ts +2 -0
  27. package/src/core/agents/agent-claim.ts +30 -11
  28. package/src/core/agents/agent-dashboard.ts +1 -1
  29. package/src/core/agents/agent-messages.ts +43 -15
  30. package/src/core/agents/agent-prompt.ts +2 -4
  31. package/src/core/agents/agent-rules.ts +42 -3
  32. package/src/core/agents/agent-schema.ts +37 -15
  33. package/src/core/agents/agent-store.ts +57 -13
  34. package/src/core/agents/agent-team-fixture.ts +90 -14
  35. package/src/core/agents/agent-team.ts +83 -50
  36. package/src/core/agents/agent-tools.ts +94 -41
  37. package/src/core/agents/avatar-studio.ts +40 -4
  38. package/src/core/agents/character-png.ts +40 -0
  39. package/src/core/agents/group-messages.ts +7 -4
  40. package/src/core/agents/group-turns.ts +27 -14
  41. package/src/core/agents/team-editing.ts +99 -58
  42. package/src/core/agents/team-keys.ts +177 -11
  43. package/src/core/agents/team-lifecycle.ts +151 -84
  44. package/src/core/agents/team-options.ts +13 -6
  45. package/src/core/agents/team-status.ts +31 -13
  46. package/src/core/agents/team-text.ts +13 -3
  47. package/src/core/agents/team-turn-types.ts +25 -7
  48. package/src/core/agents/team-turns.ts +143 -28
  49. package/src/core/attachments/attachment-fetcher.ts +162 -64
  50. package/src/core/attachments/prompt-block.ts +7 -1
  51. package/src/core/attachments/tool-attachment.ts +20 -1
  52. package/src/core/attachments/turn-attachments.ts +18 -2
  53. package/src/core/builtin/agent-server.ts +103 -40
  54. package/src/core/builtin/discord.ts +4 -17
  55. package/src/core/builtin/modules.ts +6 -5
  56. package/src/core/builtin/threads.ts +37 -0
  57. package/src/core/config/agent-team.ts +43 -0
  58. package/src/core/config/config.ts +50 -59
  59. package/src/core/config/limits.ts +93 -0
  60. package/src/core/config/ops.ts +34 -0
  61. package/src/core/config/schema.ts +26 -0
  62. package/src/core/contract/channels.ts +20 -0
  63. package/src/core/contract/surface.ts +77 -7
  64. package/src/core/define-roundtable.ts +70 -42
  65. package/src/core/discord/agent-commands.ts +108 -86
  66. package/src/core/discord/agent-panel.ts +11 -2
  67. package/src/core/discord/connection.ts +1 -1
  68. package/src/core/discord/discord-surface.ts +10 -1
  69. package/src/core/discord/dispatch-thread-host.ts +1 -1
  70. package/src/core/domain/conversation.ts +24 -0
  71. package/src/core/holds.ts +221 -15
  72. package/src/core/host-registration.ts +2 -1
  73. package/src/core/http/listeners.ts +63 -3
  74. package/src/core/i18n/agent-panel.ts +14 -21
  75. package/src/core/interactions/prompts.ts +12 -0
  76. package/src/core/modes/autonomy.ts +23 -0
  77. package/src/core/modes/conversation-modes.ts +213 -0
  78. package/src/core/modes/modes-plugin.ts +24 -0
  79. package/src/core/modules/delegation/delegator.ts +4 -4
  80. package/src/core/modules/host-shell/shell-arithmetic.ts +81 -0
  81. package/src/core/modules/host-shell/shell-lexer-types.ts +53 -0
  82. package/src/core/modules/host-shell/shell-lexer.ts +493 -0
  83. package/src/core/modules/host-shell/shell-paths.ts +85 -4
  84. package/src/core/modules/host-shell/shell-policy.ts +341 -332
  85. package/src/core/modules/host-shell/shell-programs.ts +298 -0
  86. package/src/core/modules/host-shell/shell-quoting.ts +237 -0
  87. package/src/core/modules/host-shell/shell-scripts.ts +114 -0
  88. package/src/core/modules/host-shell/shell-wrappers.ts +104 -0
  89. package/src/core/modules/schedules/precheck-tools.ts +17 -0
  90. package/src/core/modules/schedules/schedules.ts +9 -3
  91. package/src/core/plugin.ts +8 -1
  92. package/src/core/presence/direct-channels.ts +30 -0
  93. package/src/core/presentation/reply-body.ts +25 -0
  94. package/src/core/registry/contributions.ts +31 -7
  95. package/src/core/registry/providers.ts +1 -1
  96. package/src/core/routing/conversation-turns.ts +2 -15
  97. package/src/core/routing/surface-port.ts +5 -1
  98. package/src/core/runtime/extensions/approval-card.ts +1 -5
  99. package/src/core/runtime/extensions/approval-fields.ts +3 -7
  100. package/src/core/runtime/extensions/confirmation-gate-extension.ts +13 -2
  101. package/src/core/runtime/extensions/confirmation-gate.ts +41 -40
  102. package/src/core/runtime/extensions/gate-refusals.ts +23 -0
  103. package/src/core/runtime/extensions/held-calls.ts +16 -1
  104. package/src/core/runtime/extensions/hold-describer.ts +58 -8
  105. package/src/core/runtime/extensions/hold-lookups.ts +19 -19
  106. package/src/core/runtime/extensions/yolo.ts +105 -0
  107. package/src/core/runtime/pi-agent-runtime.ts +3 -5
  108. package/src/core/runtime/runtime-plugin.ts +42 -1
  109. package/src/core/runtime/runtime-types.ts +11 -0
  110. package/src/core/runtime/turn-budget.ts +32 -5
  111. package/src/core/runtime/turn-limits.ts +14 -3
  112. package/src/core/services.ts +61 -9
  113. package/src/core/shared/attachment-reader.ts +6 -0
  114. package/src/core/shared/unix-server.ts +10 -1
  115. package/src/core/testing/modules.ts +12 -8
  116. package/src/core/testing/surface-threads.ts +58 -0
  117. package/src/core/testing/thread-host.ts +17 -5
  118. package/src/core/threads/dispatch-threads.ts +299 -0
  119. package/src/core/threads/host-threads.ts +19 -0
  120. package/src/core/tool-set.snapshot.json +5 -0
  121. package/src/index.ts +27 -1
  122. package/src/kit/characters.ts +5 -0
  123. package/src/kit/index.ts +2 -0
  124. package/src/kit/shell.ts +1 -0
  125. package/src/kit/support.ts +1 -1
  126. package/src/kit/threads.ts +1 -1
  127. package/src/testing.ts +4 -0
  128. package/templates/agents.ts +1 -1
  129. package/src/core/discord/dispatch-threads.ts +0 -194
@@ -0,0 +1,213 @@
1
+ import type { SQL } from "bun";
2
+ import type { Migration } from "../db/migrations.ts";
3
+ import { PluginError } from "../errors.ts";
4
+ import { SYSTEM_PRINCIPAL } from "../identity/principal-store.ts";
5
+ import type { Logger } from "../log.ts";
6
+ import type { ChannelKey } from "../sessions.ts";
7
+ import type { Speaker } from "../speakers.ts";
8
+
9
+ /** What a conversation is set to run unattended: both off until an owner switches them on. */
10
+ export interface ConversationMode {
11
+ /** Held calls that are safe enough are approved by the server instead of asked on a card. */
12
+ yolo: boolean;
13
+ /** A turn that reaches its time limit goes on by itself, up to `autonomy.longRunning.maxSeconds`. */
14
+ longRunning: boolean;
15
+ /** The principal who last changed it; absent for a conversation never changed. */
16
+ changedBy?: string;
17
+ changedAt?: Date;
18
+ }
19
+
20
+ /** Which modes a change sets; a mode left out stays as it is. */
21
+ export interface ModeChange {
22
+ yolo?: boolean;
23
+ longRunning?: boolean;
24
+ }
25
+
26
+ /** A held call the server approved in yolo mode, for the room's log and the people watching it. */
27
+ export interface AutoApproval {
28
+ channel: ChannelKey;
29
+ tool: string;
30
+ /** What the call would do, as the card would have said. */
31
+ action: string;
32
+ /** The hold classes that were all inside what yolo may approve. */
33
+ classes: readonly string[];
34
+ /** The owner whose message started the turn. */
35
+ principalId: string;
36
+ at: Date;
37
+ }
38
+
39
+ /**
40
+ * The per-conversation modes, provided as `MODES`. They are stored per conversation key, so they
41
+ * work for any surface; a surface decides who may toggle them. Every mode survives a restart.
42
+ */
43
+ export interface ConversationModes {
44
+ /** A conversation's modes from memory, both off by default; never waits on the database. */
45
+ peek(channel: ChannelKey): ConversationMode;
46
+ /**
47
+ * Sets the modes `change` names and stores them before they take effect; resolves with the
48
+ * result. Throws `PluginError` unless `by` is an owner other than the system principal, so the
49
+ * host's own turns and webhook-borne speakers cannot switch a conversation to run unattended.
50
+ */
51
+ set(
52
+ channel: ChannelKey,
53
+ change: ModeChange,
54
+ by: Speaker,
55
+ ): Promise<ConversationMode>;
56
+ /** Called after each change. Returns the function that stops it. A throwing listener is logged. */
57
+ onChange(
58
+ listener: (channel: ChannelKey, mode: ConversationMode) => void,
59
+ ): () => void;
60
+ /** Records that yolo mode approved a held call: logs it and tells `onAutoApproved` listeners. */
61
+ autoApproved(entry: AutoApproval): void;
62
+ onAutoApproved(listener: (entry: AutoApproval) => void): () => void;
63
+ }
64
+
65
+ interface Row {
66
+ channel: string;
67
+ yolo: boolean;
68
+ long_running: boolean;
69
+ changed_by: string | null;
70
+ changed_at: Date;
71
+ }
72
+
73
+ const OFF: ConversationMode = Object.freeze({
74
+ yolo: false,
75
+ longRunning: false,
76
+ });
77
+
78
+ function modeOf(row: Row): ConversationMode {
79
+ return Object.freeze({
80
+ yolo: row.yolo,
81
+ longRunning: row.long_running,
82
+ ...(row.changed_by === null ? {} : { changedBy: row.changed_by }),
83
+ changedAt: row.changed_at,
84
+ });
85
+ }
86
+
87
+ /** The conversation modes in the `conversation_modes` table, cached in memory after one load. */
88
+ export class PgConversationModes implements ConversationModes {
89
+ readonly #sql: SQL;
90
+ readonly #logger: Logger;
91
+ readonly #modes = new Map<ChannelKey, ConversationMode>();
92
+ readonly #changed = new Set<
93
+ (channel: ChannelKey, mode: ConversationMode) => void
94
+ >();
95
+ readonly #approved = new Set<(entry: AutoApproval) => void>();
96
+
97
+ private constructor(sql: SQL, logger: Logger) {
98
+ this.#sql = sql;
99
+ this.#logger = logger;
100
+ }
101
+
102
+ /** The modes' table; the host runs this before the modes attach. */
103
+ static readonly migration: Migration = {
104
+ name: "conversation-modes",
105
+ up: async (sql) => {
106
+ await sql`
107
+ CREATE TABLE IF NOT EXISTS conversation_modes (
108
+ channel text PRIMARY KEY,
109
+ yolo boolean NOT NULL DEFAULT false,
110
+ long_running boolean NOT NULL DEFAULT false,
111
+ changed_by text,
112
+ changed_at timestamptz NOT NULL DEFAULT now()
113
+ )`;
114
+ },
115
+ };
116
+
117
+ /**
118
+ * The modes over the host's migrated pool, with none loaded: `Roundtable.check()` opens no
119
+ * connection, so the rows are read by `load`, from the plugin's service start.
120
+ */
121
+ static attach(sql: SQL, logger: Logger): PgConversationModes {
122
+ return new PgConversationModes(sql, logger);
123
+ }
124
+
125
+ /** Reads every stored row into memory; until then every conversation reads as both modes off. */
126
+ async load(): Promise<void> {
127
+ const rows: Row[] = await this.#sql`SELECT * FROM conversation_modes`;
128
+ // A mode `set` wrote while the rows were read is newer than its row here.
129
+ for (const row of rows)
130
+ if (!this.#modes.has(row.channel as ChannelKey))
131
+ this.#modes.set(row.channel as ChannelKey, modeOf(row));
132
+ }
133
+
134
+ peek(channel: ChannelKey): ConversationMode {
135
+ return this.#modes.get(channel) ?? OFF;
136
+ }
137
+
138
+ async set(
139
+ channel: ChannelKey,
140
+ change: ModeChange,
141
+ by: Speaker,
142
+ ): Promise<ConversationMode> {
143
+ if (by.tier !== "owner" || by.principalId === SYSTEM_PRINCIPAL)
144
+ throw new PluginError(
145
+ "only an owner sets a conversation's modes, and not the host's own turns",
146
+ );
147
+ const yolo = change.yolo ?? null;
148
+ const longRunning = change.longRunning ?? null;
149
+ // One statement, so two changes that name different modes cannot undo each other.
150
+ const rows: Row[] = await this.#sql`
151
+ INSERT INTO conversation_modes (channel, yolo, long_running, changed_by, changed_at)
152
+ VALUES (${channel}, COALESCE(${yolo}::boolean, false),
153
+ COALESCE(${longRunning}::boolean, false), ${by.principalId}, now())
154
+ ON CONFLICT (channel) DO UPDATE SET
155
+ yolo = COALESCE(${yolo}::boolean, conversation_modes.yolo),
156
+ long_running = COALESCE(${longRunning}::boolean, conversation_modes.long_running),
157
+ changed_by = ${by.principalId},
158
+ changed_at = now()
159
+ RETURNING *`;
160
+ const [row] = rows;
161
+ if (!row)
162
+ throw new Error(`conversation modes of ${channel} were not stored`);
163
+ const mode = modeOf(row);
164
+ this.#modes.set(channel, mode);
165
+ this.#logger.info(
166
+ {
167
+ channel,
168
+ yolo: mode.yolo,
169
+ longRunning: mode.longRunning,
170
+ by: by.principalId,
171
+ },
172
+ "conversation modes changed",
173
+ );
174
+ this.#tell(this.#changed, "change", (listener) => listener(channel, mode));
175
+ return mode;
176
+ }
177
+
178
+ onChange(
179
+ listener: (channel: ChannelKey, mode: ConversationMode) => void,
180
+ ): () => void {
181
+ this.#changed.add(listener);
182
+ return () => void this.#changed.delete(listener);
183
+ }
184
+
185
+ autoApproved(entry: AutoApproval): void {
186
+ this.#logger.info(
187
+ {
188
+ channel: entry.channel,
189
+ tool: entry.tool,
190
+ classes: entry.classes,
191
+ principalId: entry.principalId,
192
+ },
193
+ "held call approved by yolo mode",
194
+ );
195
+ this.#tell(this.#approved, "auto-approval", (listener) => listener(entry));
196
+ }
197
+
198
+ onAutoApproved(listener: (entry: AutoApproval) => void): () => void {
199
+ this.#approved.add(listener);
200
+ return () => void this.#approved.delete(listener);
201
+ }
202
+
203
+ /** Runs each listener; one that throws is logged and the rest still hear it. */
204
+ #tell<L>(listeners: Set<L>, what: string, call: (listener: L) => void): void {
205
+ for (const listener of [...listeners]) {
206
+ try {
207
+ call(listener);
208
+ } catch (error) {
209
+ this.#logger.error({ err: error }, `a ${what} listener threw`);
210
+ }
211
+ }
212
+ }
213
+ }
@@ -0,0 +1,24 @@
1
+ import type { RoundtablePlugin } from "../plugin.ts";
2
+ import { MODES } from "../services.ts";
3
+ import { PgConversationModes } from "./conversation-modes.ts";
4
+
5
+ /** The name of the modes' plugin, as the migration ledger names it. */
6
+ export const MODES_PLUGIN = "modes";
7
+
8
+ /**
9
+ * The per-conversation modes (yolo and long-running), provided as `MODES`. They live in their own
10
+ * table, so they survive a restart: the service loads them when the host starts, before it
11
+ * serves a turn, and until then every conversation reads as both modes off.
12
+ */
13
+ export function modesPlugin(): RoundtablePlugin {
14
+ return {
15
+ name: MODES_PLUGIN,
16
+ migrations: [PgConversationModes.migration],
17
+ provides: [MODES],
18
+ setup: ({ database, logger, services }) => {
19
+ const modes = PgConversationModes.attach(database(), logger);
20
+ services.provide(MODES, modes);
21
+ return { services: [{ name: "modes", start: () => modes.load() }] };
22
+ },
23
+ };
24
+ }
@@ -1,14 +1,14 @@
1
1
  import type { BackgroundTarget } from "../../contract/channels.ts";
2
- import type {
3
- DispatchThread,
4
- DispatchThreads,
5
- } from "../../discord/dispatch-threads.ts";
6
2
  import type { ChannelKey } from "../../domain/conversation.ts";
7
3
  import { DelegationError } from "../../domain/errors.ts";
8
4
  import { messages } from "../../i18n/index.ts";
9
5
  import type { Logger } from "../../log.ts";
10
6
  import type { Delegator } from "../../services.ts";
11
7
  import type { Tier } from "../../speakers.ts";
8
+ import type {
9
+ DispatchThread,
10
+ DispatchThreads,
11
+ } from "../../threads/dispatch-threads.ts";
12
12
  import { timeZone } from "../../time.ts";
13
13
 
14
14
  export interface DelegationJob {
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The `((` groups of one list the lexer reads. Words in arithmetic are no commands, but `((a); (b))`
3
+ * is two subshells, so the commands read inside an open group are kept apart and dropped only once
4
+ * the group closes as `))`; any group left open or closed otherwise passes them on.
5
+ */
6
+
7
+ /** A `((` that may be arithmetic: where it opened and how many commands were read before it. */
8
+ interface Group {
9
+ depth: number;
10
+ pos: number;
11
+ start: number;
12
+ /** Set when the `)` behind its closing one proved the group arithmetic. */
13
+ confirmed: boolean;
14
+ }
15
+
16
+ export class ArithGroups {
17
+ readonly #groups: Group[] = [];
18
+ readonly #held: string[][] = [];
19
+
20
+ /**
21
+ * @param out where commands go when they are not in arithmetic
22
+ * @param lone whether the list starts inside `$(`, so its first `(` is the second of `$((`
23
+ * @param listStart where the list starts
24
+ */
25
+ constructor(
26
+ readonly out: string[][],
27
+ readonly src: string,
28
+ readonly listStart: number,
29
+ readonly lone: boolean,
30
+ ) {}
31
+
32
+ /** Whether a command read now is inside a `((` group. */
33
+ get open(): boolean {
34
+ return this.#groups.length > 0;
35
+ }
36
+
37
+ /** A finished simple command: held while a group is open, else passed on. */
38
+ add(words: string[]) {
39
+ (this.open ? this.#held : this.out).push(words);
40
+ }
41
+
42
+ /** The `(` at `pos`, at paren depth `depth`, opens a group. */
43
+ push(depth: number, pos: number) {
44
+ this.#groups.push({
45
+ depth,
46
+ pos,
47
+ start: this.#held.length,
48
+ confirmed: false,
49
+ });
50
+ }
51
+
52
+ /** Passes on every command held. */
53
+ flush() {
54
+ for (const command of this.#held.splice(0)) this.out.push(command);
55
+ }
56
+
57
+ /**
58
+ * The `)` before `pos` closes paren depth `depth`. Returns whether it closed a group that is
59
+ * arithmetic, whose commands are dropped. `))` closes arithmetic: the `)` right behind this one
60
+ * proves the group below it (`((`) or the `$(` that started the list (`$((`).
61
+ */
62
+ close(depth: number, pos: number): boolean {
63
+ const top = this.#groups[this.#groups.length - 1];
64
+ if (!top || top.depth !== depth) return false;
65
+ this.#groups.pop();
66
+ if (this.src[pos] === ")") {
67
+ const below = this.#groups[this.#groups.length - 1];
68
+ if (below && below.depth === depth - 1) below.confirmed = true;
69
+ else if (
70
+ !below &&
71
+ this.lone &&
72
+ top.pos === this.listStart &&
73
+ this.src[this.listStart - 1] === "("
74
+ )
75
+ top.confirmed = true;
76
+ }
77
+ if (top.confirmed) this.#held.length = top.start;
78
+ else if (!this.open) this.flush();
79
+ return top.confirmed;
80
+ }
81
+ }
@@ -0,0 +1,53 @@
1
+ /** The words, marks and records the shell lexer shares with the hold rule. */
2
+
3
+ /** Words that can start a command line before the command itself: `then sudo ls` runs `sudo`. */
4
+ export const COMMAND_PREFIX_WORDS: ReadonlySet<string> = new Set([
5
+ "if",
6
+ "then",
7
+ "elif",
8
+ "else",
9
+ "do",
10
+ "while",
11
+ "until",
12
+ "!",
13
+ "coproc",
14
+ "function",
15
+ ]);
16
+
17
+ /** Words that can come before `case` or `esac` in a command that is still at its start. */
18
+ export const KEYWORDS_BEFORE_CASE: ReadonlySet<string> = new Set([
19
+ ...COMMAND_PREFIX_WORDS,
20
+ "time",
21
+ ]);
22
+
23
+ /**
24
+ * Follows the `$(` or backtick a word keeps in place of a substitution that the shell running the
25
+ * line expands, so a `bash -c` script holding it is built at run time; a `$(` that came from single
26
+ * quotes or an escape has no mark, and the inner shell is the one that runs it.
27
+ */
28
+ export const EXPANDED = "\uE000";
29
+
30
+ /** The commands of a line, or `unreadable` when the lexer could not tell what bash would run. */
31
+ export interface Lexed {
32
+ commands: string[][];
33
+ unreadable: boolean;
34
+ /** Whether the line evaluates arithmetic (`(( ))`, `$(( ))`, `$[ ]`), which runs a `$(` held in a variable's value. */
35
+ arithmetic: boolean;
36
+ /** Whether a `$(` or a backtick stands as text where no shell expands it (single quotes, an escape), which a value can carry into arithmetic. */
37
+ literalSubstitution: boolean;
38
+ }
39
+
40
+ /** A here-document waiting for the end of its line. */
41
+ export interface Pending {
42
+ owner: number;
43
+ delimiter: string;
44
+ quoted: boolean;
45
+ stripTabs: boolean;
46
+ }
47
+
48
+ /** What every lexer of one line shares: how deep the substitutions go, and what the line turned out to hold. */
49
+ export interface Shared {
50
+ depth: number;
51
+ arithmetic: boolean;
52
+ literalSubstitution: boolean;
53
+ }