pi-roundtable 0.22.5 → 0.23.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 +55 -1
- package/README.md +35 -9
- package/README.zh-TW.md +51 -13
- package/docs/migrating-0.8.md +2 -2
- package/docs/plugins.md +350 -28
- package/examples/discord-adapter.ts +9 -1
- package/examples/seeds.test.ts +2 -2
- package/examples/seeds.ts +1 -2
- package/package.json +6 -2
- package/src/characters/catalog.ts +1055 -0
- package/src/characters/characters.css +68 -0
- package/src/characters/colors.ts +108 -0
- package/src/characters/compose.ts +281 -0
- package/src/characters/css.ts +63 -0
- package/src/characters/index.ts +33 -0
- package/src/characters/parse.ts +196 -0
- package/src/characters/parts-face.ts +354 -0
- package/src/characters/parts-hair.ts +56 -0
- package/src/characters/parts-hats.ts +57 -0
- package/src/characters/parts-outfits.ts +81 -0
- package/src/characters/parts-props.ts +137 -0
- package/src/characters/spec.ts +224 -0
- package/src/characters/suggest.ts +101 -0
- package/src/cli/checks/characters.ts +23 -0
- package/src/cli/checks/images.ts +5 -3
- package/src/cli/doctor.ts +2 -0
- package/src/core/agents/agent-claim.ts +30 -11
- package/src/core/agents/agent-dashboard.ts +1 -1
- package/src/core/agents/agent-messages.ts +43 -15
- package/src/core/agents/agent-prompt.ts +2 -4
- package/src/core/agents/agent-rules.ts +42 -3
- package/src/core/agents/agent-schema.ts +37 -15
- package/src/core/agents/agent-store.ts +57 -13
- package/src/core/agents/agent-team-fixture.ts +90 -14
- package/src/core/agents/agent-team.ts +83 -50
- package/src/core/agents/agent-tools.ts +94 -41
- package/src/core/agents/avatar-studio.ts +40 -4
- package/src/core/agents/character-png.ts +40 -0
- package/src/core/agents/group-messages.ts +7 -4
- package/src/core/agents/group-turns.ts +27 -14
- package/src/core/agents/team-editing.ts +99 -58
- package/src/core/agents/team-keys.ts +177 -11
- package/src/core/agents/team-lifecycle.ts +151 -84
- package/src/core/agents/team-options.ts +13 -6
- package/src/core/agents/team-status.ts +31 -13
- package/src/core/agents/team-text.ts +13 -3
- package/src/core/agents/team-turn-types.ts +25 -7
- package/src/core/agents/team-turns.ts +143 -28
- package/src/core/attachments/attachment-fetcher.ts +162 -64
- package/src/core/attachments/prompt-block.ts +7 -1
- package/src/core/attachments/tool-attachment.ts +20 -1
- package/src/core/attachments/turn-attachments.ts +18 -2
- package/src/core/builtin/agent-server.ts +103 -40
- package/src/core/builtin/discord.ts +4 -17
- package/src/core/builtin/modules.ts +6 -5
- package/src/core/builtin/threads.ts +37 -0
- package/src/core/config/agent-team.ts +43 -0
- package/src/core/config/config.ts +50 -59
- package/src/core/config/limits.ts +93 -0
- package/src/core/config/ops.ts +34 -0
- package/src/core/config/schema.ts +26 -0
- package/src/core/contract/channels.ts +20 -0
- package/src/core/contract/surface.ts +77 -7
- package/src/core/define-roundtable.ts +70 -42
- package/src/core/discord/agent-commands.ts +108 -86
- package/src/core/discord/agent-panel.ts +11 -2
- package/src/core/discord/connection.ts +1 -1
- package/src/core/discord/discord-surface.ts +10 -1
- package/src/core/discord/dispatch-thread-host.ts +1 -1
- package/src/core/domain/conversation.ts +24 -0
- package/src/core/holds.ts +221 -15
- package/src/core/host-registration.ts +2 -1
- package/src/core/http/listeners.ts +63 -3
- package/src/core/i18n/agent-panel.ts +14 -21
- package/src/core/interactions/prompts.ts +12 -0
- package/src/core/modes/autonomy.ts +23 -0
- package/src/core/modes/conversation-modes.ts +213 -0
- package/src/core/modes/modes-plugin.ts +24 -0
- package/src/core/modules/delegation/delegator.ts +4 -4
- package/src/core/modules/host-shell/shell-arithmetic.ts +81 -0
- package/src/core/modules/host-shell/shell-lexer-types.ts +53 -0
- package/src/core/modules/host-shell/shell-lexer.ts +493 -0
- package/src/core/modules/host-shell/shell-paths.ts +85 -4
- package/src/core/modules/host-shell/shell-policy.ts +341 -332
- package/src/core/modules/host-shell/shell-programs.ts +298 -0
- package/src/core/modules/host-shell/shell-quoting.ts +237 -0
- package/src/core/modules/host-shell/shell-scripts.ts +114 -0
- package/src/core/modules/host-shell/shell-wrappers.ts +104 -0
- package/src/core/modules/schedules/precheck-tools.ts +17 -0
- package/src/core/modules/schedules/schedules.ts +9 -3
- package/src/core/plugin.ts +8 -1
- package/src/core/presence/direct-channels.ts +30 -0
- package/src/core/presentation/reply-body.ts +25 -0
- package/src/core/registry/contributions.ts +31 -7
- package/src/core/registry/providers.ts +1 -1
- package/src/core/routing/conversation-turns.ts +2 -15
- package/src/core/routing/surface-port.ts +5 -1
- package/src/core/runtime/extensions/approval-card.ts +1 -5
- package/src/core/runtime/extensions/approval-fields.ts +3 -7
- package/src/core/runtime/extensions/confirmation-gate-extension.ts +13 -2
- package/src/core/runtime/extensions/confirmation-gate.ts +41 -40
- package/src/core/runtime/extensions/gate-refusals.ts +23 -0
- package/src/core/runtime/extensions/held-calls.ts +16 -1
- package/src/core/runtime/extensions/hold-describer.ts +58 -8
- package/src/core/runtime/extensions/hold-lookups.ts +19 -19
- package/src/core/runtime/extensions/yolo.ts +105 -0
- package/src/core/runtime/pi-agent-runtime.ts +3 -5
- package/src/core/runtime/runtime-plugin.ts +42 -1
- package/src/core/runtime/runtime-types.ts +11 -0
- package/src/core/runtime/turn-budget.ts +32 -5
- package/src/core/runtime/turn-limits.ts +14 -3
- package/src/core/services.ts +61 -9
- package/src/core/shared/attachment-reader.ts +6 -0
- package/src/core/shared/unix-server.ts +10 -1
- package/src/core/testing/modules.ts +12 -8
- package/src/core/testing/surface-threads.ts +58 -0
- package/src/core/testing/thread-host.ts +17 -5
- package/src/core/threads/dispatch-threads.ts +299 -0
- package/src/core/threads/host-threads.ts +19 -0
- package/src/core/tool-set.snapshot.json +5 -0
- package/src/index.ts +27 -1
- package/src/kit/characters.ts +5 -0
- package/src/kit/index.ts +2 -0
- package/src/kit/shell.ts +1 -0
- package/src/kit/support.ts +1 -1
- package/src/kit/threads.ts +1 -1
- package/src/testing.ts +4 -0
- package/templates/agents.ts +1 -1
- package/src/core/discord/dispatch-threads.ts +0 -194
package/src/core/holds.ts
CHANGED
|
@@ -44,6 +44,9 @@ export interface HoldContext {
|
|
|
44
44
|
* signal that is already aborted when the hold answers refuses the call without a card, and
|
|
45
45
|
* drops a confirmation of it, which is how a hold withdraws what a restart left held: the
|
|
46
46
|
* signals do not survive a restart, but a repeated call is described again.
|
|
47
|
+
*
|
|
48
|
+
* `classes` are the hold classes of the call, for a rule that describes it in a turn (see
|
|
49
|
+
* `HoldRule.holdClasses`); a rule that leaves them out leaves its call unclassed.
|
|
47
50
|
*/
|
|
48
51
|
export type HoldDescription =
|
|
49
52
|
| string
|
|
@@ -52,6 +55,7 @@ export type HoldDescription =
|
|
|
52
55
|
copy?: NonNullable<ApprovalDetails["copy"]>;
|
|
53
56
|
supersede?: AbortSignal;
|
|
54
57
|
approval?: "speaker";
|
|
58
|
+
classes?: readonly string[];
|
|
55
59
|
};
|
|
56
60
|
|
|
57
61
|
/**
|
|
@@ -60,11 +64,34 @@ export type HoldDescription =
|
|
|
60
64
|
*/
|
|
61
65
|
export interface HoldRule {
|
|
62
66
|
name: string;
|
|
67
|
+
/**
|
|
68
|
+
* Whether the rule stands for other rules' calls, as the precheck scripts' does, and may report
|
|
69
|
+
* their ids in `holdClasses`. Any other rule may report only ids that start with its own name
|
|
70
|
+
* and a dot. A relay's calls also carry `<name>.relay`, so `autonomy.yolo.card` naming the relay
|
|
71
|
+
* cards them.
|
|
72
|
+
*/
|
|
73
|
+
relay?: true;
|
|
63
74
|
describe(
|
|
64
75
|
tool: string,
|
|
65
76
|
input: Record<string, unknown>,
|
|
66
77
|
context: HoldContext,
|
|
67
78
|
): string | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* The classes of a call this rule holds, as `<rule>.<class>` ids such as `shell.rm`, for yolo
|
|
81
|
+
* mode to tell a call it may approve from one it never does (see `autonomy.yolo`). Report only
|
|
82
|
+
* classes that are safe to approve with no person looking: listing a class opts the call in, and
|
|
83
|
+
* yolo then approves it unless `autonomy.yolo.card` names the class or the rule. List every
|
|
84
|
+
* reason the call is held, not only the first: yolo approves only a call whose classes it all
|
|
85
|
+
* allows. Every id starts with the rule's name and a dot; a `relay` rule, which stands for other
|
|
86
|
+
* rules' calls, may list their ids instead. A rule that reports none,
|
|
87
|
+
* or an id that is not so named, leaves the call unclassed: yolo cards it unless
|
|
88
|
+
* `autonomy.yolo.allow` names the rule. Asked for a call `describe` held. It must change nothing.
|
|
89
|
+
*/
|
|
90
|
+
holdClasses?(
|
|
91
|
+
tool: string,
|
|
92
|
+
input: Record<string, unknown>,
|
|
93
|
+
context: HoldContext,
|
|
94
|
+
): readonly string[] | undefined;
|
|
68
95
|
/**
|
|
69
96
|
* Whether this rule may hold some call of `tool`, for a rule whose verdict depends on the input
|
|
70
97
|
* (such as an action argument). Asked when the input is not known yet, as for a precheck
|
|
@@ -119,6 +146,12 @@ export interface HoldRule {
|
|
|
119
146
|
mayName?(tool: string): boolean;
|
|
120
147
|
}
|
|
121
148
|
|
|
149
|
+
/** The description of a held call with its hold classes. */
|
|
150
|
+
export interface ClassedHold {
|
|
151
|
+
description: HoldDescription;
|
|
152
|
+
classes: readonly string[];
|
|
153
|
+
}
|
|
154
|
+
|
|
122
155
|
/** The description of a call that must be approved first, or undefined to let it run. */
|
|
123
156
|
export type HoldCheck = ((
|
|
124
157
|
tool: string,
|
|
@@ -127,6 +160,17 @@ export type HoldCheck = ((
|
|
|
127
160
|
) => string | undefined) & {
|
|
128
161
|
/** The name of a rule that may hold some call of `tool` whatever its input; see `HoldRule.mayHold`. */
|
|
129
162
|
mayHold?(tool: string): string | undefined;
|
|
163
|
+
/**
|
|
164
|
+
* The classes of the call, from every rule whose `describe` holds it; see
|
|
165
|
+
* `HoldRule.holdClasses`. A rule without valid classes of its own gives its bare name, which
|
|
166
|
+
* marks the call unclassed. Undefined when no rule holds the call. A check made without `holdChain` has none, and yolo
|
|
167
|
+
* mode then never approves its calls.
|
|
168
|
+
*/
|
|
169
|
+
classesOf?(
|
|
170
|
+
tool: string,
|
|
171
|
+
input: Record<string, unknown>,
|
|
172
|
+
context: HoldContext,
|
|
173
|
+
): readonly string[] | undefined;
|
|
130
174
|
/**
|
|
131
175
|
* The check as a turn runs it: each rule's `describeInTurn`, or its `describe` for a rule
|
|
132
176
|
* without one, asked in order until one describes the call. It answers at once when every rule
|
|
@@ -138,6 +182,18 @@ export type HoldCheck = ((
|
|
|
138
182
|
context: HoldContext,
|
|
139
183
|
turn: ToolTurn,
|
|
140
184
|
): HoldDescription | undefined | Promise<HoldDescription | undefined>;
|
|
185
|
+
/**
|
|
186
|
+
* `inTurn`, with the classes of every rule that holds the call, as the confirmation gate records
|
|
187
|
+
* them with it; see `HoldRule.holdClasses`. The rules after the first that holds the call are
|
|
188
|
+
* asked too, and one whose lookup fails leaves its bare name. A description that names its own
|
|
189
|
+
* `classes` keeps them. A check without it has no classes for its calls.
|
|
190
|
+
*/
|
|
191
|
+
inTurnWithClasses?(
|
|
192
|
+
tool: string,
|
|
193
|
+
input: Record<string, unknown>,
|
|
194
|
+
context: HoldContext,
|
|
195
|
+
turn: ToolTurn,
|
|
196
|
+
): ClassedHold | undefined | Promise<ClassedHold | undefined>;
|
|
141
197
|
/** The highest tier any rule requires to approve the call; see `HoldRule.approvalTier`. */
|
|
142
198
|
approvalTier?(
|
|
143
199
|
tool: string,
|
|
@@ -173,6 +229,10 @@ export function higherTier(
|
|
|
173
229
|
export function holdChain(rules: readonly HoldRule[]): HoldCheck {
|
|
174
230
|
const names = new Set<string>();
|
|
175
231
|
for (const rule of rules) {
|
|
232
|
+
if (rule.name.includes("."))
|
|
233
|
+
throw new PluginError(
|
|
234
|
+
`hold rule ${rule.name}: the name has a dot, which separates a rule from its hold classes. Rename it.`,
|
|
235
|
+
);
|
|
176
236
|
if (names.has(rule.name))
|
|
177
237
|
throw new PluginError(
|
|
178
238
|
`hold rule ${rule.name} is registered twice. Rename one of the two.`,
|
|
@@ -186,25 +246,90 @@ export function holdChain(rules: readonly HoldRule[]): HoldCheck {
|
|
|
186
246
|
}
|
|
187
247
|
return undefined;
|
|
188
248
|
};
|
|
189
|
-
check.
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
249
|
+
check.classesOf = (tool, input, context) => {
|
|
250
|
+
const holding = rules.filter(
|
|
251
|
+
(r) => r.describe(tool, input, context) !== undefined,
|
|
252
|
+
);
|
|
253
|
+
return holding.length === 0
|
|
254
|
+
? undefined
|
|
255
|
+
: unionOf(
|
|
256
|
+
holding.map((rule) => classesFor(rule, names, tool, input, context)),
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
/**
|
|
260
|
+
* Asks the rules from `from` in order, each holding rule joining `found`. Stops at the first
|
|
261
|
+
* holding rule unless `all`; then a later rule whose lookup fails holds the call undescribed.
|
|
262
|
+
*/
|
|
263
|
+
const ask = (
|
|
264
|
+
tool: string,
|
|
265
|
+
input: Record<string, unknown>,
|
|
266
|
+
context: HoldContext,
|
|
267
|
+
turn: ToolTurn,
|
|
268
|
+
from: number,
|
|
269
|
+
all: boolean,
|
|
270
|
+
found: readonly Holding[] = [],
|
|
271
|
+
): readonly Holding[] | Promise<readonly Holding[]> => {
|
|
272
|
+
let holding = found;
|
|
273
|
+
for (let at = from; at < rules.length; at++) {
|
|
274
|
+
const rule = rules[at];
|
|
275
|
+
if (!rule) continue;
|
|
276
|
+
let description: ReturnType<NonNullable<HoldRule["describeInTurn"]>>;
|
|
277
|
+
try {
|
|
278
|
+
description = rule.describeInTurn
|
|
197
279
|
? rule.describeInTurn(tool, input, context, turn)
|
|
198
280
|
: rule.describe(tool, input, context);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (holding.length === 0) throw error;
|
|
283
|
+
holding = [...holding, { description: undefined, rule }];
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (isPromise(description)) {
|
|
287
|
+
const before = holding;
|
|
288
|
+
return description.then(
|
|
289
|
+
(answer) => {
|
|
290
|
+
const next =
|
|
291
|
+
answer === undefined
|
|
292
|
+
? before
|
|
293
|
+
: [...before, { description: answer, rule }];
|
|
294
|
+
return all || next.length === 0
|
|
295
|
+
? ask(tool, input, context, turn, at + 1, all, next)
|
|
296
|
+
: next;
|
|
297
|
+
},
|
|
298
|
+
(error) => {
|
|
299
|
+
if (before.length === 0) throw error;
|
|
300
|
+
return ask(tool, input, context, turn, at + 1, all, [
|
|
301
|
+
...before,
|
|
302
|
+
{ description: undefined, rule },
|
|
303
|
+
]);
|
|
304
|
+
},
|
|
305
|
+
);
|
|
204
306
|
}
|
|
205
|
-
|
|
307
|
+
if (description !== undefined) {
|
|
308
|
+
holding = [...holding, { description, rule }];
|
|
309
|
+
if (!all) return holding;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return holding;
|
|
313
|
+
};
|
|
314
|
+
check.inTurn = (tool, input, context, turn) => {
|
|
315
|
+
const found = ask(tool, input, context, turn, 0, false);
|
|
316
|
+
return isPromise(found)
|
|
317
|
+
? found.then((holding) => holding[0]?.description)
|
|
318
|
+
: found[0]?.description;
|
|
319
|
+
};
|
|
320
|
+
check.inTurnWithClasses = (tool, input, context, turn) => {
|
|
321
|
+
const classed = (holding: readonly Holding[]) => {
|
|
322
|
+
const first = holding.find((held) => held.description !== undefined);
|
|
323
|
+
if (first?.description === undefined) return undefined;
|
|
324
|
+
return {
|
|
325
|
+
description: first.description,
|
|
326
|
+
classes: unionOf(
|
|
327
|
+
holding.map((held) => classesIn(held, names, tool, input, context)),
|
|
328
|
+
),
|
|
329
|
+
};
|
|
206
330
|
};
|
|
207
|
-
|
|
331
|
+
const found = ask(tool, input, context, turn, 0, true);
|
|
332
|
+
return isPromise(found) ? found.then(classed) : classed(found);
|
|
208
333
|
};
|
|
209
334
|
check.mayHold = (tool) => rules.find((rule) => rule.mayHold?.(tool))?.name;
|
|
210
335
|
check.approvalTier = (tool, input, context) =>
|
|
@@ -232,6 +357,87 @@ export function holdChain(rules: readonly HoldRule[]): HoldCheck {
|
|
|
232
357
|
return check;
|
|
233
358
|
}
|
|
234
359
|
|
|
360
|
+
/** The classes of a call a rule holds: its own, or the rule's bare name, which leaves the call unclassed. */
|
|
361
|
+
function classesFor(
|
|
362
|
+
rule: HoldRule,
|
|
363
|
+
names: ReadonlySet<string>,
|
|
364
|
+
tool: string,
|
|
365
|
+
input: Record<string, unknown>,
|
|
366
|
+
context: HoldContext,
|
|
367
|
+
): readonly string[] {
|
|
368
|
+
let own: unknown;
|
|
369
|
+
try {
|
|
370
|
+
own = rule.holdClasses?.(tool, input, context);
|
|
371
|
+
} catch {
|
|
372
|
+
// A rule whose classes fail leaves its call unclassed.
|
|
373
|
+
own = undefined;
|
|
374
|
+
}
|
|
375
|
+
return validClasses(own, rule, names);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* The ids a rule reports once each, when it reports some and every one starts with the name of a
|
|
380
|
+
* rule of the chain and a dot or is that name; otherwise the rule's bare name, which yolo mode
|
|
381
|
+
* treats as unclassed and cards.
|
|
382
|
+
*/
|
|
383
|
+
function validClasses(
|
|
384
|
+
reported: unknown,
|
|
385
|
+
rule: HoldRule,
|
|
386
|
+
names: ReadonlySet<string>,
|
|
387
|
+
): readonly string[] {
|
|
388
|
+
if (!Array.isArray(reported) || reported.length === 0) return [rule.name];
|
|
389
|
+
const ids = reported as unknown[];
|
|
390
|
+
if (!ids.every((id): id is string => isClassOf(id, rule, names)))
|
|
391
|
+
return [rule.name];
|
|
392
|
+
return [
|
|
393
|
+
...new Set(rule.relay ? [...ids, `${rule.name}.relay`] : ids),
|
|
394
|
+
] as string[];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** A rule's description, if it gave one, and the rule; a rule whose lookup failed has none. */
|
|
398
|
+
interface Holding {
|
|
399
|
+
description: HoldDescription | undefined;
|
|
400
|
+
rule: HoldRule;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** The classes of a held call: those its description names, else its rule's. */
|
|
404
|
+
function classesIn(
|
|
405
|
+
{ description, rule }: Holding,
|
|
406
|
+
names: ReadonlySet<string>,
|
|
407
|
+
tool: string,
|
|
408
|
+
input: Record<string, unknown>,
|
|
409
|
+
context: HoldContext,
|
|
410
|
+
): readonly string[] {
|
|
411
|
+
// A rule whose lookup failed could not say what the call is.
|
|
412
|
+
if (description === undefined) return [rule.name];
|
|
413
|
+
if (typeof description === "object" && description !== null) {
|
|
414
|
+
const named = description.classes;
|
|
415
|
+
if (Array.isArray(named) && named.length > 0)
|
|
416
|
+
return validClasses(named, rule, names);
|
|
417
|
+
}
|
|
418
|
+
return classesFor(rule, names, tool, input, context);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Whether a rule may report the id: it is the rule's own name or one of its classes, or the rule
|
|
423
|
+
* is a relay and the id is a rule of the chain or one of that rule's classes.
|
|
424
|
+
*/
|
|
425
|
+
function isClassOf(
|
|
426
|
+
id: unknown,
|
|
427
|
+
rule: HoldRule,
|
|
428
|
+
names: ReadonlySet<string>,
|
|
429
|
+
): id is string {
|
|
430
|
+
if (typeof id !== "string") return false;
|
|
431
|
+
const dot = id.indexOf(".");
|
|
432
|
+
const owner = dot === -1 ? id : id.slice(0, dot);
|
|
433
|
+
return rule.relay ? names.has(owner) : owner === rule.name;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** The ids of the lists once each, in order. */
|
|
437
|
+
function unionOf(lists: readonly (readonly string[])[]): readonly string[] {
|
|
438
|
+
return [...new Set(lists.flat())];
|
|
439
|
+
}
|
|
440
|
+
|
|
235
441
|
/** The principals every list names, in the first list's order; undefined when there are no lists. */
|
|
236
442
|
function commonApprovers(
|
|
237
443
|
lists: readonly (readonly string[] | undefined)[],
|
|
@@ -8,7 +8,7 @@ import type { SurfacePort } from "./contract/surface.ts";
|
|
|
8
8
|
import { NotLinkedError } from "./errors.ts";
|
|
9
9
|
import { EventBus } from "./events.ts";
|
|
10
10
|
import type { RoundtableOptions } from "./host.ts";
|
|
11
|
-
import { HttpListeners } from "./http/listeners.ts";
|
|
11
|
+
import { HttpListeners, listenerAddress } from "./http/listeners.ts";
|
|
12
12
|
import { setLocale } from "./i18n/index.ts";
|
|
13
13
|
import { contactsOf } from "./identity/identity-view.ts";
|
|
14
14
|
import type {
|
|
@@ -178,6 +178,7 @@ export class Linkage {
|
|
|
178
178
|
return this.registry.dashboard;
|
|
179
179
|
},
|
|
180
180
|
apiKey: async (provider) => this.#options.apiKey?.(provider),
|
|
181
|
+
listener: (id) => listenerAddress(listeners, id),
|
|
181
182
|
},
|
|
182
183
|
this.tiers,
|
|
183
184
|
services,
|
|
@@ -20,6 +20,14 @@ export interface HttpRoute {
|
|
|
20
20
|
path: { exact: string } | { prefix: string };
|
|
21
21
|
/** Omitted: every method, for a handler that answers the rest itself. */
|
|
22
22
|
methods?: readonly string[];
|
|
23
|
+
/**
|
|
24
|
+
* The most bytes of request body this route takes, such as a plugin's upload limit plus its
|
|
25
|
+
* multipart overhead. The listener serves the largest value among its routes (a route that
|
|
26
|
+
* says nothing counts as `DEFAULT_MAX_REQUEST_BODY_BYTES`) unless the host set the listener's
|
|
27
|
+
* own `maxRequestBodyBytes`, which decides; a request over it is answered 413 before the
|
|
28
|
+
* handler runs. A positive integer.
|
|
29
|
+
*/
|
|
30
|
+
maxRequestBodyBytes?: number;
|
|
23
31
|
handle(request: Request): Response | Promise<Response>;
|
|
24
32
|
/**
|
|
25
33
|
* Takes the route's WebSocket upgrade requests (GET with `Upgrade: websocket`); every other
|
|
@@ -37,8 +45,44 @@ export type ListenerAddress =
|
|
|
37
45
|
}
|
|
38
46
|
| { port: number; hostname?: string };
|
|
39
47
|
|
|
40
|
-
/**
|
|
41
|
-
|
|
48
|
+
/**
|
|
49
|
+
* An address the host serves HTTP on, with the id routes name it by. `maxRequestBodyBytes` is the
|
|
50
|
+
* most bytes of request body the listener takes (a positive integer); without it the listener
|
|
51
|
+
* takes what its routes ask for, see `HttpRoute.maxRequestBodyBytes`.
|
|
52
|
+
*/
|
|
53
|
+
export type ListenerConfig = {
|
|
54
|
+
id: string;
|
|
55
|
+
maxRequestBodyBytes?: number;
|
|
56
|
+
} & ListenerAddress;
|
|
57
|
+
|
|
58
|
+
/** Bun's own default request body limit, 128 MiB, which a listener sets explicitly. */
|
|
59
|
+
export const DEFAULT_MAX_REQUEST_BODY_BYTES = 128 * 1024 * 1024;
|
|
60
|
+
|
|
61
|
+
/** The request body limit a listener is opened with: its own option, else its routes' largest ask, else the default. */
|
|
62
|
+
export function requestBodyLimit(
|
|
63
|
+
listener: ListenerConfig,
|
|
64
|
+
routes: readonly HttpRoute[],
|
|
65
|
+
): number {
|
|
66
|
+
if (listener.maxRequestBodyBytes !== undefined)
|
|
67
|
+
return listener.maxRequestBodyBytes;
|
|
68
|
+
const asked = routes.map(
|
|
69
|
+
(route) => route.maxRequestBodyBytes ?? DEFAULT_MAX_REQUEST_BODY_BYTES,
|
|
70
|
+
);
|
|
71
|
+
return asked.length === 0
|
|
72
|
+
? DEFAULT_MAX_REQUEST_BODY_BYTES
|
|
73
|
+
: Math.max(...asked);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The address of the listener `id`, without the id, as a copy a plugin cannot use to change the host's. */
|
|
77
|
+
export function listenerAddress(
|
|
78
|
+
listeners: readonly ListenerConfig[],
|
|
79
|
+
id: string,
|
|
80
|
+
): Readonly<ListenerAddress> | undefined {
|
|
81
|
+
const found = listeners.find((listener) => listener.id === id);
|
|
82
|
+
if (!found) return undefined;
|
|
83
|
+
const { id: _id, maxRequestBodyBytes: _limit, ...address } = found;
|
|
84
|
+
return Object.freeze(address);
|
|
85
|
+
}
|
|
42
86
|
|
|
43
87
|
const pathOf = (route: HttpRoute) =>
|
|
44
88
|
"exact" in route.path ? route.path.exact : route.path.prefix;
|
|
@@ -88,6 +132,14 @@ function validateLimits(name: string, websocket: WebSocketRoute): void {
|
|
|
88
132
|
);
|
|
89
133
|
}
|
|
90
134
|
|
|
135
|
+
/** Refuses a request body limit that is not a positive integer, naming what set it. */
|
|
136
|
+
function validateBodyLimit(owner: string, value: number | undefined): void {
|
|
137
|
+
if (value !== undefined && !(Number.isSafeInteger(value) && value > 0))
|
|
138
|
+
throw new PluginError(
|
|
139
|
+
`${owner} sets maxRequestBodyBytes to ${value}, which is not a positive integer`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
91
143
|
/** Refuses routes on unknown listeners, reused names, and any request two routes could both take. */
|
|
92
144
|
function validate(
|
|
93
145
|
listeners: readonly ListenerConfig[],
|
|
@@ -96,7 +148,10 @@ function validate(
|
|
|
96
148
|
const ids = new Set(listeners.map((l) => l.id));
|
|
97
149
|
if (ids.size !== listeners.length)
|
|
98
150
|
throw new PluginError("a listener is configured twice");
|
|
151
|
+
for (const listener of listeners)
|
|
152
|
+
validateBodyLimit(`listener ${listener.id}`, listener.maxRequestBodyBytes);
|
|
99
153
|
routes.forEach((route, index) => {
|
|
154
|
+
validateBodyLimit(`route ${route.name}`, route.maxRequestBodyBytes);
|
|
100
155
|
if (route.websocket && route.methods && !route.methods.includes("GET"))
|
|
101
156
|
throw new PluginError(
|
|
102
157
|
`route ${route.name} takes WebSockets, so its methods must include GET`,
|
|
@@ -198,11 +253,13 @@ function serveTcp(
|
|
|
198
253
|
hostname: string | undefined,
|
|
199
254
|
fetch: Fetch,
|
|
200
255
|
websocket: WebSocketHandler<Connection> | undefined,
|
|
256
|
+
maxRequestBodySize: number,
|
|
201
257
|
): Server<Connection> {
|
|
202
258
|
const options = {
|
|
203
259
|
port,
|
|
204
260
|
...(hostname ? { hostname } : {}),
|
|
205
261
|
idleTimeout: 0,
|
|
262
|
+
maxRequestBodySize,
|
|
206
263
|
fetch,
|
|
207
264
|
error: serverError,
|
|
208
265
|
...(websocket ? { websocket } : {}),
|
|
@@ -256,18 +313,20 @@ export class HttpListeners {
|
|
|
256
313
|
sockets: this.#sockets,
|
|
257
314
|
});
|
|
258
315
|
const websocket = this.#websocketHandler(listener.id, routes);
|
|
316
|
+
const maxRequestBodySize = requestBodyLimit(listener, routes);
|
|
259
317
|
if ("socketPath" in listener) {
|
|
260
318
|
this.#servers.push(
|
|
261
319
|
websocket
|
|
262
320
|
? serveUnix(listener.socketPath, handle, {
|
|
263
321
|
error: serverError,
|
|
322
|
+
maxRequestBodySize,
|
|
264
323
|
websocket,
|
|
265
324
|
})
|
|
266
325
|
: serveUnix(
|
|
267
326
|
listener.socketPath,
|
|
268
327
|
(request) =>
|
|
269
328
|
routeRequest(routes, request, listener.id, this.#logger),
|
|
270
|
-
{ error: serverError },
|
|
329
|
+
{ error: serverError, maxRequestBodySize },
|
|
271
330
|
),
|
|
272
331
|
);
|
|
273
332
|
chmodSync(listener.socketPath, listener.mode ?? 0o660);
|
|
@@ -277,6 +336,7 @@ export class HttpListeners {
|
|
|
277
336
|
listener.hostname,
|
|
278
337
|
handle,
|
|
279
338
|
websocket,
|
|
339
|
+
maxRequestBodySize,
|
|
280
340
|
);
|
|
281
341
|
this.#servers.push(server);
|
|
282
342
|
if (server.port !== undefined)
|
|
@@ -20,17 +20,11 @@ export function agentPanelEn(ctx: CatalogContext) {
|
|
|
20
20
|
skills: string;
|
|
21
21
|
}) =>
|
|
22
22
|
`### ${v.displayName}\n-# agent name \`${v.name}\`, cannot be changed\n**Model** · \`${v.model}\` · thinking \`${v.thinking}\`${v.follows}\n${v.skills}\n-# To add or remove a skill, ask any agent to use agent_skills.`,
|
|
23
|
-
agentAvatarPromptText: (prompt: string) => `**Avatar prompt**\n${prompt}`,
|
|
24
|
-
agentNoImageProvider:
|
|
25
|
-
"**Avatar** · No image provider is configured, so this avatar is generated from the agent's display name and cannot be redrawn or edited. A plugin that fills the `images` slot adds drawing.",
|
|
26
23
|
agentPromptInline: (prompt: string) =>
|
|
27
24
|
`**Prompt**\n\`\`\`\n${prompt}\n\`\`\``,
|
|
28
25
|
agentPromptFile: (length: number) =>
|
|
29
26
|
`**Prompt** · ${length} characters, the full text is in the file below.`,
|
|
30
27
|
agentEditPromptLabel: "Edit prompt",
|
|
31
|
-
agentRedrawLabel: "Redraw avatar",
|
|
32
|
-
agentNewAvatarPromptLabel: "New avatar prompt",
|
|
33
|
-
agentEditAvatarLabel: "Edit avatar",
|
|
34
28
|
agentModelLabel: "Model",
|
|
35
29
|
agentDefaultNote:
|
|
36
30
|
"A changed prompt takes effect from this agent's next turn; the conversation history is kept.",
|
|
@@ -41,12 +35,15 @@ export function agentPanelEn(ctx: CatalogContext) {
|
|
|
41
35
|
agentEditModalTitle: (displayName: string) => `Edit ${displayName}`,
|
|
42
36
|
agentDisplayNameLabel: "Display name",
|
|
43
37
|
agentPromptLabel: "Prompt",
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
agentLook: (label: string, color: string) =>
|
|
39
|
+
`**Look** · ${label}, ${color}`,
|
|
40
|
+
agentClassPlaceholder: "Class",
|
|
41
|
+
agentColorPlaceholder: "Color",
|
|
42
|
+
agentLookChanged: "The new picture shows from this agent's next message.",
|
|
43
|
+
agentLookFailed: (message: string) =>
|
|
44
|
+
`The look was not changed: ${message}`,
|
|
45
|
+
agentPanelOutdated: `This panel is out of date; open it again with \`/${ctx.root} profile\`.`,
|
|
46
46
|
agentSaved: "Saved; it takes effect from the next turn.",
|
|
47
|
-
agentAvatarRedrawn: "The new avatar shows from this agent's next message.",
|
|
48
|
-
agentAvatarFailed: (message: string) =>
|
|
49
|
-
`The avatar was not drawn: ${message}`,
|
|
50
47
|
};
|
|
51
48
|
}
|
|
52
49
|
|
|
@@ -70,17 +67,11 @@ export function agentPanelZhTW(
|
|
|
70
67
|
skills: string;
|
|
71
68
|
}) =>
|
|
72
69
|
`### ${v.displayName}\n-# agent 名稱 \`${v.name}\`,不能更改\n**模型** \`${v.model}\` thinking \`${v.thinking}\`${v.follows}\n${v.skills}\n-# 要增減 skill,請任一個 agent 用 agent_skills。`,
|
|
73
|
-
agentAvatarPromptText: (prompt: string) => `**頭像提示詞**\n${prompt}`,
|
|
74
|
-
agentNoImageProvider:
|
|
75
|
-
"**頭像** 尚未設定圖像提供者,所以這個頭像由 agent 的顯示名自動產生,不能重畫或修改。要能畫頭像,需要一個填入 `images` 槽位的外掛。",
|
|
76
70
|
agentPromptInline: (prompt: string) =>
|
|
77
71
|
`**提示詞**\n\`\`\`\n${prompt}\n\`\`\``,
|
|
78
72
|
agentPromptFile: (length: number) =>
|
|
79
73
|
`**提示詞** ${length} 字,完整內容在下面的檔案。`,
|
|
80
74
|
agentEditPromptLabel: "編輯提示詞",
|
|
81
|
-
agentRedrawLabel: "重畫頭像",
|
|
82
|
-
agentNewAvatarPromptLabel: "新頭像提示詞",
|
|
83
|
-
agentEditAvatarLabel: "修改頭像",
|
|
84
75
|
agentModelLabel: "模型",
|
|
85
76
|
agentDefaultNote:
|
|
86
77
|
"提示詞改完,從這個 agent 的下一回合開始生效;對話紀錄保留。",
|
|
@@ -91,10 +82,12 @@ export function agentPanelZhTW(
|
|
|
91
82
|
agentEditModalTitle: (displayName: string) => `編輯 ${displayName}`,
|
|
92
83
|
agentDisplayNameLabel: "顯示名",
|
|
93
84
|
agentPromptLabel: "提示詞",
|
|
94
|
-
|
|
95
|
-
|
|
85
|
+
agentLook: (label: string, color: string) => `**外觀** ${label},${color}`,
|
|
86
|
+
agentClassPlaceholder: "職業",
|
|
87
|
+
agentColorPlaceholder: "顏色",
|
|
88
|
+
agentLookChanged: "新的頭像從這個 agent 的下一則訊息開始顯示。",
|
|
89
|
+
agentLookFailed: (message: string) => `外觀沒有改成:${message}`,
|
|
90
|
+
agentPanelOutdated: `這個面板已經過期,請重新用 \`/${ctx.root} profile\` 開啟。`,
|
|
96
91
|
agentSaved: "已儲存,從下一回合開始生效。",
|
|
97
|
-
agentAvatarRedrawn: "新頭像從這個 agent 的下一則訊息開始顯示。",
|
|
98
|
-
agentAvatarFailed: (message: string) => `頭像沒有畫成:${message}`,
|
|
99
92
|
};
|
|
100
93
|
}
|
|
@@ -78,6 +78,12 @@ export interface ApprovedBy {
|
|
|
78
78
|
* approval without it, from anyone else, is not accepted.
|
|
79
79
|
*/
|
|
80
80
|
owner?: true;
|
|
81
|
+
/**
|
|
82
|
+
* Set only when the server approved the call itself and no person answered: `"yolo"` is the
|
|
83
|
+
* conversation's yolo mode. A surface that reports an answer from a person never sets it;
|
|
84
|
+
* `ToolTurn.approvedBy` shows it to the tool.
|
|
85
|
+
*/
|
|
86
|
+
auto?: "yolo";
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
/**
|
|
@@ -133,6 +139,12 @@ export interface ApprovalOptions {
|
|
|
133
139
|
* such as a turn's timeout card; a surface that cannot show them keeps its own.
|
|
134
140
|
*/
|
|
135
141
|
wording?: ApprovalWording;
|
|
142
|
+
/**
|
|
143
|
+
* What the card asks: `"action"` (the default, also when absent) whether to run a held call,
|
|
144
|
+
* `"turn-timeout"` whether to let a turn that reached its time limit go on. A surface may show
|
|
145
|
+
* the two differently or notify on the second; Discord ignores it.
|
|
146
|
+
*/
|
|
147
|
+
purpose?: "action" | "turn-timeout";
|
|
136
148
|
}
|
|
137
149
|
|
|
138
150
|
/** A card's words in place of an approval's; see `ApprovalOptions.wording`. */
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** How far the host lets a conversation run without a person: the limits behind its modes. */
|
|
2
|
+
export interface Autonomy {
|
|
3
|
+
yolo: {
|
|
4
|
+
/**
|
|
5
|
+
* Hold classes yolo may approve although it cards them by default, such as `shell.rm`, and the
|
|
6
|
+
* names of rules that report no classes, such as `tool:repo_push`, whose calls it then approves.
|
|
7
|
+
*/
|
|
8
|
+
allow: readonly string[];
|
|
9
|
+
/** Hold classes or rule names yolo never approves, such as `shell.push` or `tool:repo_push`. */
|
|
10
|
+
card: readonly string[];
|
|
11
|
+
};
|
|
12
|
+
/** The most one long-running turn works in all before it asks or stops, in ms. */
|
|
13
|
+
longRunningMaxMs: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The longest a long-running turn works without a person: four hours. */
|
|
17
|
+
export const DEFAULT_LONG_RUNNING_MS = 4 * 60 * 60_000;
|
|
18
|
+
|
|
19
|
+
/** Yolo approves nothing beyond its built-in classes, and a long-running turn stops at four hours. */
|
|
20
|
+
export const DEFAULT_AUTONOMY: Autonomy = Object.freeze({
|
|
21
|
+
yolo: Object.freeze({ allow: [], card: [] }),
|
|
22
|
+
longRunningMaxMs: DEFAULT_LONG_RUNNING_MS,
|
|
23
|
+
});
|