pi-roundtable 0.9.6 → 0.9.8
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 +26 -0
- package/docs/plugins.md +55 -9
- package/examples/fake-surface.ts +2 -2
- package/package.json +1 -1
- package/src/core/define.ts +52 -33
- package/src/core/discord/owner-cards.ts +45 -39
- package/src/core/domain/progress.ts +18 -1
- package/src/core/holds.ts +61 -0
- package/src/core/i18n/conversation.ts +5 -0
- package/src/core/interactions/prompts.ts +30 -5
- package/src/core/runtime/extensions/approval-card.ts +210 -0
- package/src/core/runtime/extensions/confirmation-gate.ts +172 -57
- package/src/core/runtime/pi-agent-runtime.ts +4 -2
- package/src/core/runtime/prompt-slot.ts +17 -2
- package/src/core/runtime/session-factory.ts +4 -1
- package/src/core/runtime/turn-progress.ts +14 -2
- package/src/core/testing/surface-contract.ts +4 -3
- package/src/core/tool-turn.ts +37 -0
- package/src/index.ts +3 -1
- package/src/kit/holds.ts +3 -2
- package/src/kit/index.ts +3 -1
- package/src/kit/worker.ts +4 -2
- package/src/testing.ts +1 -1
|
@@ -31,6 +31,7 @@ import { packageDir } from "../shared/package-dir.ts";
|
|
|
31
31
|
import { readAttachmentExtension } from "../shared/read-attachment-tool.ts";
|
|
32
32
|
import { type Speaker, THE_SPEAKER } from "../speakers.ts";
|
|
33
33
|
import { currentToolNames } from "../tool-tiers.ts";
|
|
34
|
+
import { toolTurn } from "../tool-turn.ts";
|
|
34
35
|
import {
|
|
35
36
|
CompactionTiers,
|
|
36
37
|
compactionEngine,
|
|
@@ -373,7 +374,9 @@ export class SessionFactory {
|
|
|
373
374
|
...(context.workspace
|
|
374
375
|
? { attachFile: attachFileExtension(context.workspace) }
|
|
375
376
|
: {}),
|
|
376
|
-
confirmationGate: confirmationGateExtension(gate, slot)
|
|
377
|
+
confirmationGate: confirmationGateExtension(gate, slot, (signal) =>
|
|
378
|
+
toolTurn(context, signal),
|
|
379
|
+
),
|
|
377
380
|
askUser: askUserExtension(slot, addressee, () =>
|
|
378
381
|
turnAddressee(
|
|
379
382
|
this.#deps.speaker(turnKey),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { TurnProgress } from "../domain/progress.ts";
|
|
2
|
+
import type { HoldRefusal, TurnProgress } from "../domain/progress.ts";
|
|
3
3
|
|
|
4
4
|
/** How long text is joined before it is sent, so a fast stream costs a few events a second. */
|
|
5
5
|
export const PROGRESS_INTERVAL_MS = 250;
|
|
@@ -19,6 +19,11 @@ export interface ProgressReporter {
|
|
|
19
19
|
|
|
20
20
|
export interface ProgressReporterOptions {
|
|
21
21
|
intervalMs?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Why a hold refused a tool's call, asked when a failed tool ends and read once; the gate that
|
|
24
|
+
* refused it is how a surface tells that from a failure.
|
|
25
|
+
*/
|
|
26
|
+
refusalOf?: (toolCallId: string) => HoldRefusal | undefined;
|
|
22
27
|
/** Runs `run` after `ms`; returns a cancel. Replaceable in tests. */
|
|
23
28
|
setTimer?: (run: () => void, ms: number) => () => void;
|
|
24
29
|
}
|
|
@@ -104,7 +109,14 @@ export function progressReporter(
|
|
|
104
109
|
toolEnd(id, tool, isError) {
|
|
105
110
|
if (closed) return;
|
|
106
111
|
flush();
|
|
107
|
-
|
|
112
|
+
const refused = isError ? options.refusalOf?.(id) : undefined;
|
|
113
|
+
send({
|
|
114
|
+
type: "tool_end",
|
|
115
|
+
id,
|
|
116
|
+
tool,
|
|
117
|
+
ok: !isError,
|
|
118
|
+
...(refused ? { refused } : {}),
|
|
119
|
+
});
|
|
108
120
|
},
|
|
109
121
|
close() {
|
|
110
122
|
if (closed) return;
|
|
@@ -332,7 +332,7 @@ const CHECKS: readonly Check[] = [
|
|
|
332
332
|
answeredOnlyByThem(running, running.subject.owner, "private"),
|
|
333
333
|
},
|
|
334
334
|
{
|
|
335
|
-
name: "a private conversation's approval above the person's tier goes to no one: it
|
|
335
|
+
name: "a private conversation's approval above the person's tier goes to no one: it comes back at once, unshown",
|
|
336
336
|
applies: (subject) =>
|
|
337
337
|
subject.surface.prompts !== undefined &&
|
|
338
338
|
speakerOf(subject).tier !== "owner",
|
|
@@ -349,7 +349,8 @@ const CHECKS: readonly Check[] = [
|
|
|
349
349
|
Bun.sleep(1000).then(() => "still open" as const),
|
|
350
350
|
]);
|
|
351
351
|
expectThat(
|
|
352
|
-
|
|
352
|
+
// `expired` is what surfaces answered before `unavailable` existed.
|
|
353
|
+
answer === "unavailable" || answer === "expired",
|
|
353
354
|
`an approval no one may answer came back ${answer}`,
|
|
354
355
|
);
|
|
355
356
|
expectThat(
|
|
@@ -444,7 +445,7 @@ export async function checkSurfaceContract(
|
|
|
444
445
|
* where it offers them, delivers files, ends typing and stop controls idempotently, shows
|
|
445
446
|
* progress, resolves approvals as the person answers or as a stopped turn cancels them, ignores
|
|
446
447
|
* another person's answer to the person's approval, and, in a private conversation, an owner's
|
|
447
|
-
* too, and lets an approval above the person's tier there
|
|
448
|
+
* too, and lets an approval above the person's tier there come back at once, shown to no one. Each
|
|
448
449
|
* check gets a fresh subject from `make`; a check for an optional part the surface lacks passes.
|
|
449
450
|
*/
|
|
450
451
|
export function describeSurfaceContract(
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AttachmentLookupError,
|
|
3
|
+
openAttachment,
|
|
4
|
+
} from "./attachments/tool-attachment.ts";
|
|
5
|
+
import type { ToolTurn } from "./define.ts";
|
|
6
|
+
import { ToolRefusal } from "./define.ts";
|
|
7
|
+
import { attachReplyFile } from "./reply-files.ts";
|
|
8
|
+
import type { SessionContext } from "./sessions.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The turn a tool's `run` and `hold` receive, built from the session that makes the call and the
|
|
12
|
+
* signal that stops it.
|
|
13
|
+
*/
|
|
14
|
+
export function toolTurn(
|
|
15
|
+
context: SessionContext,
|
|
16
|
+
signal: AbortSignal | undefined,
|
|
17
|
+
): ToolTurn {
|
|
18
|
+
return {
|
|
19
|
+
speaker: context.speaker(),
|
|
20
|
+
channel: context.turnChannel,
|
|
21
|
+
agent: context.agent,
|
|
22
|
+
signal,
|
|
23
|
+
...(context.workspace ? { workspace: context.workspace } : {}),
|
|
24
|
+
attachFile: attachReplyFile,
|
|
25
|
+
attachment: async (file) => {
|
|
26
|
+
if (context.attachmentDir === undefined)
|
|
27
|
+
throw new ToolRefusal("this session keeps no attachments");
|
|
28
|
+
try {
|
|
29
|
+
return await openAttachment(context.attachmentDir, file);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error instanceof AttachmentLookupError)
|
|
32
|
+
throw new ToolRefusal(error.message);
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -118,7 +118,7 @@ export type {
|
|
|
118
118
|
InterimTextMode,
|
|
119
119
|
} from "./core/domain/interim.ts";
|
|
120
120
|
export type { TurnConversation, TurnRequest } from "./core/domain/ports.ts";
|
|
121
|
-
export type { TurnProgress } from "./core/domain/progress.ts";
|
|
121
|
+
export type { HoldRefusal, TurnProgress } from "./core/domain/progress.ts";
|
|
122
122
|
export type { DrainOptions } from "./core/drain.ts";
|
|
123
123
|
export {
|
|
124
124
|
HostStoppingError,
|
|
@@ -161,6 +161,8 @@ export type {
|
|
|
161
161
|
export { SYSTEM_PRINCIPAL } from "./core/identity/principal-store.ts";
|
|
162
162
|
export type {
|
|
163
163
|
Approval,
|
|
164
|
+
ApprovalDetails,
|
|
165
|
+
ApprovalFile,
|
|
164
166
|
AskOption,
|
|
165
167
|
LateAnswer,
|
|
166
168
|
OwnerAnswer,
|
package/src/kit/holds.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Plugin helpers, versioned like the main entry; see the plugin guide.
|
|
2
|
-
// The chain a host links from every plugin's hold rules, to test a rule set as the host runs it
|
|
2
|
+
// The chain a host links from every plugin's hold rules, to test a rule set as the host runs it,
|
|
3
|
+
// and how long a hold that looks things up may take.
|
|
3
4
|
|
|
4
|
-
export { holdChain } from "../core/holds.ts";
|
|
5
|
+
export { HOLD_DESCRIBE_TIMEOUT_MS, holdChain } from "../core/holds.ts";
|
package/src/kit/index.ts
CHANGED
|
@@ -52,7 +52,7 @@ export {
|
|
|
52
52
|
THINKING_LEVELS,
|
|
53
53
|
thinkingLabel,
|
|
54
54
|
} from "./domain.ts";
|
|
55
|
-
export { holdChain } from "./holds.ts";
|
|
55
|
+
export { HOLD_DESCRIBE_TIMEOUT_MS, holdChain } from "./holds.ts";
|
|
56
56
|
export type {
|
|
57
57
|
JevCompactInput,
|
|
58
58
|
JevCompactOptions,
|
|
@@ -158,6 +158,7 @@ export {
|
|
|
158
158
|
toolText,
|
|
159
159
|
} from "./tools.ts";
|
|
160
160
|
export type {
|
|
161
|
+
CardLimits,
|
|
161
162
|
InterimPosterOptions,
|
|
162
163
|
McpEndpoint,
|
|
163
164
|
PromptSlot,
|
|
@@ -165,6 +166,7 @@ export type {
|
|
|
165
166
|
} from "./worker.ts";
|
|
166
167
|
export {
|
|
167
168
|
approvalCard,
|
|
169
|
+
approvalDetails,
|
|
168
170
|
archiveSessions,
|
|
169
171
|
canonicalJson,
|
|
170
172
|
InterimPoster,
|
package/src/kit/worker.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Plugin helpers, versioned like the main entry; see the plugin guide.
|
|
2
2
|
// Running a Pi session of your own: MCP servers for it, its work timeout, its held-action cards.
|
|
3
3
|
|
|
4
|
+
export type { CardLimits } from "../core/runtime/extensions/approval-card.ts";
|
|
4
5
|
export {
|
|
5
6
|
approvalCard,
|
|
6
|
-
|
|
7
|
-
} from "../core/runtime/extensions/
|
|
7
|
+
approvalDetails,
|
|
8
|
+
} from "../core/runtime/extensions/approval-card.ts";
|
|
9
|
+
export { canonicalJson } from "../core/runtime/extensions/confirmation-gate.ts";
|
|
8
10
|
export type { InterimPosterOptions } from "../core/runtime/interim-text.ts";
|
|
9
11
|
export { InterimPoster } from "../core/runtime/interim-text.ts";
|
|
10
12
|
export type { VirtualServer } from "../core/runtime/mcp.ts";
|
package/src/testing.ts
CHANGED
|
@@ -249,7 +249,7 @@ export interface RecordedEvent {
|
|
|
249
249
|
|
|
250
250
|
export interface TestPluginResult {
|
|
251
251
|
contribution: Contribution;
|
|
252
|
-
/** The hold rules the plugin contributed, chained as the host links them: the description of a call that must be approved first, or undefined. */
|
|
252
|
+
/** The hold rules the plugin contributed, chained as the host links them: the description of a call that must be approved first, or undefined. `holds.inTurn(tool, input, context, turn)` asks them as a turn does, so a hold that looks things up (and may return a promise) is called with the turn; the plain call has no turn, and counts a hold that declares `turn` as held under `run <tool>`. */
|
|
253
253
|
holds: HoldCheck;
|
|
254
254
|
/** What the plugin sees as `context.conversations`, for a test to drive its claims. */
|
|
255
255
|
conversations: ConversationPort;
|