pi-roundtable 0.10.0 → 0.12.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 +28 -0
- package/README.md +3 -0
- package/README.zh-TW.md +3 -0
- package/docs/plugins.md +32 -7
- package/package.json +1 -1
- package/src/core/agents/agent-claim.ts +11 -7
- package/src/core/attachments/image-prep.ts +32 -4
- package/src/core/builtin/discord.ts +22 -0
- package/src/core/config/adapters.ts +33 -0
- package/src/core/contract/channel-context.ts +88 -3
- package/src/core/define-roundtable.ts +1 -0
- package/src/core/define.ts +20 -4
- package/src/core/discord/card-copies.ts +192 -0
- package/src/core/discord/channel-context.ts +149 -16
- package/src/core/discord/context-images.ts +164 -0
- package/src/core/discord/owner-cards-fixture.ts +9 -2
- package/src/core/discord/owner-cards.ts +47 -1
- package/src/core/domain/attachment.ts +3 -0
- package/src/core/holds.ts +14 -4
- package/src/core/i18n/cards.ts +7 -0
- package/src/core/interactions/prompts.ts +10 -0
- package/src/core/runtime/extensions/approval-card.ts +4 -2
- package/src/core/runtime/extensions/confirmation-gate.ts +7 -6
- package/src/core/runtime/extensions/gate-refusals.ts +7 -2
- package/src/core/runtime/extensions/hold-lookups.ts +68 -16
- package/src/discord/index.ts +6 -1
- package/src/index.ts +6 -1
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type CardAudienceOptions,
|
|
25
25
|
CardAudiences,
|
|
26
26
|
} from "./card-audience.ts";
|
|
27
|
+
import { CardCopies, copyReply, offeredCopy } from "./card-copies.ts";
|
|
27
28
|
import { DiscordOwners, discordUser } from "./discord-owners.ts";
|
|
28
29
|
import type { InteractionModule } from "./interaction-module.ts";
|
|
29
30
|
import {
|
|
@@ -75,6 +76,13 @@ export interface OwnerCardsOptions {
|
|
|
75
76
|
* answered. Without it no turn stops waiting on its cards.
|
|
76
77
|
*/
|
|
77
78
|
resume?(turn: LateTurn): void;
|
|
79
|
+
/**
|
|
80
|
+
* Where and how long the text behind approval cards' copy buttons is kept. With a `path` it is
|
|
81
|
+
* a JSON file that survives a restart; without one the texts live in memory and a card's copy
|
|
82
|
+
* button stops working when the process restarts. `keep` (default 100) is how many cards'
|
|
83
|
+
* texts, the newest, and `keepDays` (default 30) how long.
|
|
84
|
+
*/
|
|
85
|
+
copies?: { path?: string; keep?: number; keepDays?: number };
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
/**
|
|
@@ -91,11 +99,18 @@ export class OwnerCards implements InteractionModule {
|
|
|
91
99
|
readonly #options: OwnerCardsOptions;
|
|
92
100
|
readonly #audiences: CardAudiences;
|
|
93
101
|
readonly #open = new Map<string, OpenCard>();
|
|
102
|
+
readonly #copies: CardCopies;
|
|
94
103
|
|
|
95
104
|
constructor(options: OwnerCardsOptions) {
|
|
96
105
|
this.#options = options;
|
|
106
|
+
this.#copies = new CardCopies({
|
|
107
|
+
...options.copies,
|
|
108
|
+
logger: options.logger,
|
|
109
|
+
});
|
|
97
110
|
this.#audiences = new CardAudiences(options, new DiscordOwners(options));
|
|
98
111
|
}
|
|
112
|
+
/** Resolves once the remembered copy texts are on disk. */
|
|
113
|
+
flushCopies = (): Promise<void> => this.#copies.flush();
|
|
99
114
|
|
|
100
115
|
commands() {
|
|
101
116
|
return [];
|
|
@@ -114,9 +129,15 @@ export class OwnerCards implements InteractionModule {
|
|
|
114
129
|
signal,
|
|
115
130
|
minTier = "owner",
|
|
116
131
|
wait,
|
|
117
|
-
|
|
132
|
+
details,
|
|
118
133
|
options,
|
|
119
134
|
) => {
|
|
135
|
+
const copy = offeredCopy(details?.copy, (chars) =>
|
|
136
|
+
this.#options.logger.warn(
|
|
137
|
+
{ channelId, chars },
|
|
138
|
+
"card copy text too long; the card has no copy button",
|
|
139
|
+
),
|
|
140
|
+
);
|
|
120
141
|
const audience = await this.#audiences.approval(
|
|
121
142
|
scope,
|
|
122
143
|
minTier,
|
|
@@ -154,6 +175,7 @@ export class OwnerCards implements InteractionModule {
|
|
|
154
175
|
: {}),
|
|
155
176
|
title,
|
|
156
177
|
sections: [message],
|
|
178
|
+
...(copy ? { copy: copy.text } : {}),
|
|
157
179
|
rows: (id, disabled) => [
|
|
158
180
|
new ActionRowBuilder<ButtonBuilder>().addComponents(
|
|
159
181
|
new ButtonBuilder()
|
|
@@ -168,6 +190,16 @@ export class OwnerCards implements InteractionModule {
|
|
|
168
190
|
.setEmoji("❌")
|
|
169
191
|
.setStyle(ButtonStyle.Secondary)
|
|
170
192
|
.setDisabled(disabled),
|
|
193
|
+
// Copying answers nothing, so it stays usable once the card has ended.
|
|
194
|
+
...(copy
|
|
195
|
+
? [
|
|
196
|
+
new ButtonBuilder()
|
|
197
|
+
.setCustomId(`${CARD_PREFIX}${id}:copy`)
|
|
198
|
+
.setLabel(copy.label)
|
|
199
|
+
.setEmoji("📋")
|
|
200
|
+
.setStyle(ButtonStyle.Secondary),
|
|
201
|
+
]
|
|
202
|
+
: []),
|
|
171
203
|
),
|
|
172
204
|
],
|
|
173
205
|
footer: messages().cardApprovalFooter,
|
|
@@ -221,6 +253,8 @@ export class OwnerCards implements InteractionModule {
|
|
|
221
253
|
audience: Audience;
|
|
222
254
|
title: string;
|
|
223
255
|
sections: string[];
|
|
256
|
+
/** The text behind the card's copy button, kept under the card's id. */
|
|
257
|
+
copy?: string;
|
|
224
258
|
question?: OwnerQuestion;
|
|
225
259
|
rows(id: string, disabled: boolean): Rows;
|
|
226
260
|
footer: string;
|
|
@@ -236,6 +270,7 @@ export class OwnerCards implements InteractionModule {
|
|
|
236
270
|
if (signal?.aborted) return Promise.resolve(cancelled);
|
|
237
271
|
const wait = resume ? card.wait : undefined;
|
|
238
272
|
const id = randomUUID();
|
|
273
|
+
if (card.copy !== undefined) this.#copies.remember(id, card.copy);
|
|
239
274
|
const sections = [
|
|
240
275
|
...card.sections,
|
|
241
276
|
...(card.audience.note ? [card.audience.note] : []),
|
|
@@ -361,6 +396,17 @@ export class OwnerCards implements InteractionModule {
|
|
|
361
396
|
const [id = "", action = ""] = interaction.customId
|
|
362
397
|
.slice(CARD_PREFIX.length)
|
|
363
398
|
.split(":");
|
|
399
|
+
// Copying answers nothing: anyone who sees the card may copy, as often as they like, also
|
|
400
|
+
// once the card has ended or the process restarted.
|
|
401
|
+
if (interaction.isButton() && action === "copy") {
|
|
402
|
+
const text = this.#copies.recall(id);
|
|
403
|
+
await interaction.reply(
|
|
404
|
+
text === undefined
|
|
405
|
+
? { content: messages().cardCopyGone, flags: MessageFlags.Ephemeral }
|
|
406
|
+
: copyReply(text),
|
|
407
|
+
);
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
364
410
|
const card = this.#open.get(id);
|
|
365
411
|
if (!card) {
|
|
366
412
|
await interaction.reply({
|
|
@@ -19,6 +19,9 @@ export interface AttachmentFailure {
|
|
|
19
19
|
fromReference: boolean;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** The most images one turn carries to the model. */
|
|
23
|
+
export const MAX_TURN_IMAGES = 4;
|
|
24
|
+
|
|
22
25
|
/** An image in the form the model accepts. */
|
|
23
26
|
export interface ModelImage {
|
|
24
27
|
data: string;
|
package/src/core/holds.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ToolTurn } from "./define.ts";
|
|
2
2
|
import { PluginError } from "./errors.ts";
|
|
3
|
+
import type { ApprovalDetails } from "./interactions/prompts.ts";
|
|
3
4
|
import { type Tier, tierAtLeast } from "./speakers.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -16,6 +17,14 @@ export interface HoldContext {
|
|
|
16
17
|
scratchDir?: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* What a hold that runs in a turn answers with: the description of the call, or the description
|
|
22
|
+
* as `text` with a `copy` the owner's approval card offers beside it; see `ApprovalDetails.copy`.
|
|
23
|
+
*/
|
|
24
|
+
export type HoldDescription =
|
|
25
|
+
| string
|
|
26
|
+
| { text: string; copy?: NonNullable<ApprovalDetails["copy"]> };
|
|
27
|
+
|
|
19
28
|
/**
|
|
20
29
|
* Decides whether a tool call waits for the owner's approval. A rule answers with a short
|
|
21
30
|
* description of what the call would do, or undefined when it has no opinion on the call.
|
|
@@ -40,14 +49,15 @@ export interface HoldRule {
|
|
|
40
49
|
* when a turn makes the call, and waits for the answer up to `HOLD_DESCRIBE_TIMEOUT_MS`; a
|
|
41
50
|
* rejection or a timeout holds the call under a generic description. `turn.signal` aborts when the
|
|
42
51
|
* turn stops or the wait ends. `describe` stays what a host without a turn sees, such as a
|
|
43
|
-
* precheck script's call, and answers at once.
|
|
52
|
+
* precheck script's call, and answers at once. It may answer `{ text, copy }` instead of the
|
|
53
|
+
* text alone, for the approval card to offer `copy`; a host that shows no card uses `text`.
|
|
44
54
|
*/
|
|
45
55
|
describeInTurn?(
|
|
46
56
|
tool: string,
|
|
47
57
|
input: Record<string, unknown>,
|
|
48
58
|
context: HoldContext,
|
|
49
59
|
turn: ToolTurn,
|
|
50
|
-
):
|
|
60
|
+
): HoldDescription | undefined | Promise<HoldDescription | undefined>;
|
|
51
61
|
/**
|
|
52
62
|
* The lowest tier that may approve a call this rule holds, when it is higher than the tool's own:
|
|
53
63
|
* for a call that stands for others, such as saving a script whose runs make held calls.
|
|
@@ -98,7 +108,7 @@ export type HoldCheck = ((
|
|
|
98
108
|
input: Record<string, unknown>,
|
|
99
109
|
context: HoldContext,
|
|
100
110
|
turn: ToolTurn,
|
|
101
|
-
):
|
|
111
|
+
): HoldDescription | undefined | Promise<HoldDescription | undefined>;
|
|
102
112
|
/** The highest tier any rule requires to approve the call; see `HoldRule.approvalTier`. */
|
|
103
113
|
approvalTier?(
|
|
104
114
|
tool: string,
|
|
@@ -150,7 +160,7 @@ export function holdChain(rules: readonly HoldRule[]): HoldCheck {
|
|
|
150
160
|
check.inTurn = (tool, input, context, turn) => {
|
|
151
161
|
const ask = (
|
|
152
162
|
from: number,
|
|
153
|
-
):
|
|
163
|
+
): HoldDescription | undefined | Promise<HoldDescription | undefined> => {
|
|
154
164
|
for (let at = from; at < rules.length; at++) {
|
|
155
165
|
const rule = rules[at];
|
|
156
166
|
if (!rule) continue;
|
package/src/core/i18n/cards.ts
CHANGED
|
@@ -5,6 +5,10 @@ export function cardsEn(ctx: CatalogContext) {
|
|
|
5
5
|
return {
|
|
6
6
|
cardRunLabel: "Run",
|
|
7
7
|
cardCancelLabel: "Cancel",
|
|
8
|
+
cardCopyLabel: "Copy",
|
|
9
|
+
cardCopyGone:
|
|
10
|
+
"The text of this card is no longer kept; ask the assistant to show it again.",
|
|
11
|
+
cardCopyFile: "The text is long, so it is attached as a file.",
|
|
8
12
|
cardApprovalFooter:
|
|
9
13
|
"Nothing runs until you approve; answer whenever you are ready.",
|
|
10
14
|
cardQuestionFooter: "Answer whenever you are ready.",
|
|
@@ -39,6 +43,9 @@ export function cardsZhTW(ctx: CatalogContext): ReturnType<typeof cardsEn> {
|
|
|
39
43
|
return {
|
|
40
44
|
cardRunLabel: "執行",
|
|
41
45
|
cardCancelLabel: "取消",
|
|
46
|
+
cardCopyLabel: "複製",
|
|
47
|
+
cardCopyGone: "這張卡片的文字已經不在了;請再請助理顯示一次。",
|
|
48
|
+
cardCopyFile: "文字比較長,所以用檔案附上。",
|
|
42
49
|
cardApprovalFooter: "你核准之前不會執行;準備好再回覆就好。",
|
|
43
50
|
cardQuestionFooter: "準備好再回答就好。",
|
|
44
51
|
cardStopped: "⏹️ 這一輪已停止。",
|
|
@@ -35,6 +35,16 @@ export interface ApprovalDetails {
|
|
|
35
35
|
input: Record<string, unknown>;
|
|
36
36
|
/** The files the call sends by path; absent when it sends none. */
|
|
37
37
|
files?: ApprovalFile[];
|
|
38
|
+
/**
|
|
39
|
+
* A text the card offers to copy, such as a command or a message the call would send. A surface
|
|
40
|
+
* that can shows a control, usable by anyone who sees the card, any number of times, that hands
|
|
41
|
+
* them `text` (`label` names the control, default the surface's own); using it never answers
|
|
42
|
+
* the prompt. It is informational: a surface that cannot show it ignores it. The text is
|
|
43
|
+
* visible to anyone who can see the card, so it holds nothing they may not read. A text over
|
|
44
|
+
* 20,000 characters is not offered (Discord logs a warning), and a surface with a size limit of
|
|
45
|
+
* its own leaves it out before it cuts the call's `input`.
|
|
46
|
+
*/
|
|
47
|
+
copy?: { label?: string; text: string };
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
/**
|
|
@@ -150,12 +150,13 @@ function prefix(text: string, max: number): string {
|
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
152
|
* A held call as data for a card that is not text: the action, the tool, the whole input, and
|
|
153
|
-
* each file it sends by path with its size now (absent when unreadable)
|
|
154
|
-
* nothing is cut and nothing is worded for a locale.
|
|
153
|
+
* each file it sends by path with its size now (absent when unreadable), and the `copy` its hold
|
|
154
|
+
* offered. Unlike the text card, nothing is cut and nothing is worded for a locale.
|
|
155
155
|
*/
|
|
156
156
|
export function approvalDetails(
|
|
157
157
|
call: HeldCall,
|
|
158
158
|
workspace?: string,
|
|
159
|
+
copy?: ApprovalDetails["copy"],
|
|
159
160
|
): ApprovalDetails {
|
|
160
161
|
const files = pathFiles(call.input).map((path) => {
|
|
161
162
|
const bytes = fileSize(path, workspace);
|
|
@@ -166,6 +167,7 @@ export function approvalDetails(
|
|
|
166
167
|
tool: call.tool,
|
|
167
168
|
input: parsedInput(call.input),
|
|
168
169
|
...(files.length > 0 ? { files } : {}),
|
|
170
|
+
...(copy ? { copy } : {}),
|
|
169
171
|
};
|
|
170
172
|
}
|
|
171
173
|
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
awaited,
|
|
40
40
|
type Described,
|
|
41
41
|
type HoldTurn,
|
|
42
|
+
type Judgment,
|
|
42
43
|
joinApprovers,
|
|
43
44
|
STOPPED,
|
|
44
45
|
} from "./hold-lookups.ts";
|
|
@@ -284,7 +285,7 @@ export class ConfirmationGate {
|
|
|
284
285
|
ask.signal,
|
|
285
286
|
higherTier(this.#tiers?.minTier(call.tool), call.minTier),
|
|
286
287
|
{ late: (late) => this.#late(call, late) },
|
|
287
|
-
approvalDetails(call, this.#fileRoot),
|
|
288
|
+
approvalDetails(call, this.#fileRoot, needing.copy),
|
|
288
289
|
options,
|
|
289
290
|
);
|
|
290
291
|
if (answer === "approved") {
|
|
@@ -361,7 +362,7 @@ export class ConfirmationGate {
|
|
|
361
362
|
if (!turn || !inTurn) return this.#needing(tool, input);
|
|
362
363
|
const controller = new AbortController();
|
|
363
364
|
if (turn.signal?.aborted) controller.abort();
|
|
364
|
-
let answer:
|
|
365
|
+
let answer: ReturnType<NonNullable<HoldCheck["inTurn"]>>;
|
|
365
366
|
try {
|
|
366
367
|
answer = inTurn.call(
|
|
367
368
|
this.#holds,
|
|
@@ -373,7 +374,7 @@ export class ConfirmationGate {
|
|
|
373
374
|
} catch {
|
|
374
375
|
answer = messages().holdGeneric(tool);
|
|
375
376
|
}
|
|
376
|
-
const
|
|
377
|
+
const found: Described | Promise<Described> = isPromise(answer)
|
|
377
378
|
? awaited(
|
|
378
379
|
answer,
|
|
379
380
|
messages().holdGeneric(tool),
|
|
@@ -384,7 +385,7 @@ export class ConfirmationGate {
|
|
|
384
385
|
: answer;
|
|
385
386
|
return this.#inOrder(
|
|
386
387
|
joinApprovers(
|
|
387
|
-
|
|
388
|
+
found,
|
|
388
389
|
{
|
|
389
390
|
holds: this.#holds,
|
|
390
391
|
context: this.#context,
|
|
@@ -431,7 +432,7 @@ export class ConfirmationGate {
|
|
|
431
432
|
#unlessApproved(
|
|
432
433
|
tool: string,
|
|
433
434
|
input: Record<string, unknown>,
|
|
434
|
-
judged:
|
|
435
|
+
judged: Judgment,
|
|
435
436
|
): Needing {
|
|
436
437
|
const minTier = this.#holds.approvalTier?.(tool, input, this.#context);
|
|
437
438
|
const call: HeldCall = {
|
|
@@ -448,7 +449,7 @@ export class ConfirmationGate {
|
|
|
448
449
|
const [spent] = approved === -1 ? [] : released.splice(approved, 1);
|
|
449
450
|
if (spent) return { released: spent.by };
|
|
450
451
|
}
|
|
451
|
-
return { held: call };
|
|
452
|
+
return { held: call, ...(judged.copy ? { copy: judged.copy } : {}) };
|
|
452
453
|
}
|
|
453
454
|
|
|
454
455
|
/** Records the call as held and returns what the model is told about it. */
|
|
@@ -3,7 +3,10 @@ import type {
|
|
|
3
3
|
PendingConfirmation,
|
|
4
4
|
} from "../../domain/conversation.ts";
|
|
5
5
|
import { type OwnerIdentity, ownerWords } from "../../identity.ts";
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
ApprovalDetails,
|
|
8
|
+
ApprovedBy,
|
|
9
|
+
} from "../../interactions/prompts.ts";
|
|
7
10
|
import type { Speaker } from "../../speakers.ts";
|
|
8
11
|
import { mayApproveNamed } from "./held-calls.ts";
|
|
9
12
|
|
|
@@ -23,7 +26,9 @@ export interface Release {
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
/** What a call needs of the gate: nothing, a hold, or a release that spends an approval. */
|
|
26
|
-
export type Needing =
|
|
29
|
+
export type Needing =
|
|
30
|
+
| { held: HeldCall; copy?: NonNullable<ApprovalDetails["copy"]> }
|
|
31
|
+
| { released: ApprovedBy | undefined };
|
|
27
32
|
|
|
28
33
|
/** The text for a held call that the speaker's own message confirms. */
|
|
29
34
|
export function askSpeaker(addressee: OwnerIdentity, call: HeldCall): string {
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { ToolTurn } from "../../define.ts";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type HoldCheck,
|
|
4
|
+
type HoldContext,
|
|
5
|
+
type HoldDescription,
|
|
6
|
+
isPromise,
|
|
7
|
+
} from "../../holds.ts";
|
|
8
|
+
import { messages } from "../../i18n/index.ts";
|
|
9
|
+
import type { ApprovalDetails } from "../../interactions/prompts.ts";
|
|
3
10
|
|
|
4
11
|
/** The turn a hold that looks things up sees; see `HoldRule.describeInTurn`. */
|
|
5
12
|
export interface HoldTurn {
|
|
@@ -13,13 +20,48 @@ export interface HoldTurn {
|
|
|
13
20
|
export const STOPPED = Symbol("stopped");
|
|
14
21
|
|
|
15
22
|
/** What waiting for a call's description comes to: its text, or the turn stopped first. */
|
|
16
|
-
export type Described =
|
|
23
|
+
export type Described = HoldDescription | undefined | typeof STOPPED;
|
|
17
24
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
/**
|
|
26
|
+
* A held call's description, the text its card offers to copy, and who may approve it instead of
|
|
27
|
+
* the speaker; see `HoldRule.approversInTurn`.
|
|
28
|
+
*/
|
|
29
|
+
export interface Judgment {
|
|
30
|
+
action: string;
|
|
31
|
+
copy?: NonNullable<ApprovalDetails["copy"]>;
|
|
32
|
+
approvers?: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type Judged = Judgment | undefined | typeof STOPPED;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A hold's description as the action and the copy it carries. An answer that is neither a text
|
|
39
|
+
* nor a text with a copy still holds the call, under the generic description; a copy that is not
|
|
40
|
+
* a non-empty text is left out, so a hold that gets it wrong never lets the call run.
|
|
41
|
+
*/
|
|
42
|
+
export function described(
|
|
43
|
+
answer: HoldDescription,
|
|
44
|
+
tool: string,
|
|
45
|
+
): { action: string; copy?: NonNullable<ApprovalDetails["copy"]> } {
|
|
46
|
+
if (typeof answer === "string") return { action: answer };
|
|
47
|
+
if (typeof answer !== "object" || answer === null)
|
|
48
|
+
return { action: messages().holdGeneric(tool) };
|
|
49
|
+
const { text, copy } = answer;
|
|
50
|
+
if (typeof text !== "string") return { action: messages().holdGeneric(tool) };
|
|
51
|
+
if (
|
|
52
|
+
!copy ||
|
|
53
|
+
typeof copy !== "object" ||
|
|
54
|
+
typeof copy.text !== "string" ||
|
|
55
|
+
copy.text === ""
|
|
56
|
+
)
|
|
57
|
+
return { action: text };
|
|
58
|
+
const label = typeof copy.label === "string" ? copy.label : undefined;
|
|
59
|
+
return {
|
|
60
|
+
action: text,
|
|
61
|
+
copy:
|
|
62
|
+
label === undefined ? { text: copy.text } : { label, text: copy.text },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
23
65
|
|
|
24
66
|
/**
|
|
25
67
|
* What a lookup that answers later comes to: its answer; `fallback` when it rejects or outlasts
|
|
@@ -58,16 +100,17 @@ export function awaited<T>(
|
|
|
58
100
|
* none, so only the owners approve the call. A call no rule holds asks no one.
|
|
59
101
|
*/
|
|
60
102
|
export function joinApprovers(
|
|
61
|
-
|
|
103
|
+
answer: Described | Promise<Described>,
|
|
62
104
|
from: { holds: HoldCheck; context: HoldContext; timeoutMs: number },
|
|
63
105
|
tool: string,
|
|
64
106
|
input: Record<string, unknown>,
|
|
65
107
|
turn: HoldTurn,
|
|
66
108
|
): Judged | Promise<Judged> {
|
|
67
109
|
const lookup = from.holds.approversInTurn;
|
|
68
|
-
if (!lookup) return withoutApprovers(
|
|
110
|
+
if (!lookup) return withoutApprovers(answer, tool);
|
|
69
111
|
const join = (action: Described): Judged | Promise<Judged> => {
|
|
70
112
|
if (action === STOPPED || action === undefined) return action;
|
|
113
|
+
const { action: text, copy } = described(action, tool);
|
|
71
114
|
const controller = new AbortController();
|
|
72
115
|
if (turn.signal?.aborted) controller.abort();
|
|
73
116
|
let named:
|
|
@@ -87,22 +130,31 @@ export function joinApprovers(
|
|
|
87
130
|
}
|
|
88
131
|
if (!isPromise(named)) {
|
|
89
132
|
controller.abort();
|
|
90
|
-
return
|
|
133
|
+
return {
|
|
134
|
+
action: text,
|
|
135
|
+
...(copy ? { copy } : {}),
|
|
136
|
+
...(named ? { approvers: named } : {}),
|
|
137
|
+
};
|
|
91
138
|
}
|
|
92
139
|
return awaited(named, [], controller, turn.signal, from.timeoutMs).then(
|
|
93
140
|
(approvers) => {
|
|
94
141
|
if (approvers === STOPPED) return STOPPED;
|
|
95
|
-
return
|
|
142
|
+
return {
|
|
143
|
+
action: text,
|
|
144
|
+
...(copy ? { copy } : {}),
|
|
145
|
+
...(approvers ? { approvers } : {}),
|
|
146
|
+
};
|
|
96
147
|
},
|
|
97
148
|
);
|
|
98
149
|
};
|
|
99
|
-
return isPromise(
|
|
150
|
+
return isPromise(answer) ? answer.then(join) : join(answer);
|
|
100
151
|
}
|
|
101
152
|
|
|
102
153
|
function withoutApprovers(
|
|
103
|
-
|
|
154
|
+
answer: Described | Promise<Described>,
|
|
155
|
+
tool: string,
|
|
104
156
|
): Judged | Promise<Judged> {
|
|
105
|
-
const wrap = (
|
|
106
|
-
|
|
107
|
-
return isPromise(
|
|
157
|
+
const wrap = (found: Described): Judged =>
|
|
158
|
+
found === STOPPED || found === undefined ? found : described(found, tool);
|
|
159
|
+
return isPromise(answer) ? answer.then(wrap) : wrap(answer);
|
|
108
160
|
}
|
package/src/discord/index.ts
CHANGED
|
@@ -6,10 +6,15 @@ export { DISCORD } from "../core/builtin/discord.ts";
|
|
|
6
6
|
export type { DiscordAdapterConfig } from "../core/config/config.ts";
|
|
7
7
|
export type {
|
|
8
8
|
ChannelContext,
|
|
9
|
+
ChannelContextAttachment,
|
|
10
|
+
ChannelContextAttachmentOptions,
|
|
9
11
|
ChannelContextMessage,
|
|
10
12
|
ChannelContextOptions,
|
|
11
13
|
} from "../core/contract/channel-context.ts";
|
|
12
|
-
export {
|
|
14
|
+
export {
|
|
15
|
+
withChannelContext,
|
|
16
|
+
withChannelContextImages,
|
|
17
|
+
} from "../core/contract/channel-context.ts";
|
|
13
18
|
export type { AgentPanelMessage } from "../core/discord/agent-commands.ts";
|
|
14
19
|
export type {
|
|
15
20
|
AgentPanel,
|
package/src/index.ts
CHANGED
|
@@ -147,7 +147,12 @@ export {
|
|
|
147
147
|
PluginError,
|
|
148
148
|
ProviderError,
|
|
149
149
|
} from "./core/errors.ts";
|
|
150
|
-
export type {
|
|
150
|
+
export type {
|
|
151
|
+
HoldCheck,
|
|
152
|
+
HoldContext,
|
|
153
|
+
HoldDescription,
|
|
154
|
+
HoldRule,
|
|
155
|
+
} from "./core/holds.ts";
|
|
151
156
|
export type { HostEnvironment, RoundtableOptions } from "./core/host.ts";
|
|
152
157
|
export { Roundtable } from "./core/host.ts";
|
|
153
158
|
export type {
|