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.
@@ -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
- send({ type: "tool_end", id, tool, ok: !isError });
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 expires at once",
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
- answer === "expired",
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 expire at once, shown to no one. Each
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
- canonicalJson,
7
- } from "../core/runtime/extensions/confirmation-gate.ts";
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;