pi-roundtable 0.9.12 → 0.10.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/docs/migration-0.9.md +3 -1
  3. package/docs/plugins.md +146 -21
  4. package/examples/fake-surface.test.ts +137 -0
  5. package/examples/fake-surface.ts +71 -14
  6. package/package.json +1 -1
  7. package/src/cli/checks/identity.ts +73 -5
  8. package/src/core/agents/agent-claim.ts +12 -0
  9. package/src/core/agents/team-turns.ts +11 -4
  10. package/src/core/builtin/agent-server.ts +6 -0
  11. package/src/core/config/access.ts +23 -3
  12. package/src/core/config/adapters.ts +11 -0
  13. package/src/core/config/schema.ts +14 -0
  14. package/src/core/define-roundtable.ts +1 -0
  15. package/src/core/define.ts +44 -6
  16. package/src/core/discord/card-audience.ts +104 -7
  17. package/src/core/discord/discord-owners.ts +19 -13
  18. package/src/core/discord/owner-card-types.ts +73 -0
  19. package/src/core/discord/owner-cards.ts +56 -77
  20. package/src/core/domain/conversation.ts +7 -0
  21. package/src/core/domain/ports.ts +4 -1
  22. package/src/core/holds.ts +60 -0
  23. package/src/core/i18n/cards.ts +3 -0
  24. package/src/core/identity/access-policy.ts +65 -2
  25. package/src/core/identity/caching-principal-store.ts +2 -2
  26. package/src/core/identity/identity-links.ts +96 -0
  27. package/src/core/identity/identity-plugin.ts +10 -3
  28. package/src/core/identity/identity-schema.ts +3 -1
  29. package/src/core/identity/identity-service.ts +5 -3
  30. package/src/core/identity/principal-store.ts +59 -142
  31. package/src/core/identity/principal-tables.ts +10 -2
  32. package/src/core/identity/principal-types.ts +146 -0
  33. package/src/core/interactions/prompts.ts +47 -3
  34. package/src/core/modules/schedules/precheck-tools.ts +115 -20
  35. package/src/core/routing/conversation-turns.ts +4 -1
  36. package/src/core/runtime/conversation-sessions.ts +8 -0
  37. package/src/core/runtime/extensions/confirmation-gate-extension.ts +50 -0
  38. package/src/core/runtime/extensions/confirmation-gate.ts +252 -204
  39. package/src/core/runtime/extensions/gate-refusals.ts +133 -0
  40. package/src/core/runtime/extensions/held-calls.ts +91 -0
  41. package/src/core/runtime/extensions/hold-lookups.ts +108 -0
  42. package/src/core/runtime/pi-agent-runtime.ts +7 -3
  43. package/src/core/runtime/prompt-slot.ts +10 -2
  44. package/src/core/runtime/session-factory.ts +1 -0
  45. package/src/core/services.ts +11 -0
  46. package/src/core/sessions.ts +7 -0
  47. package/src/core/testing/surface-contract-support.ts +194 -0
  48. package/src/core/testing/surface-contract.ts +54 -120
  49. package/src/core/tool-turn.ts +5 -1
  50. package/src/index.ts +7 -0
@@ -1,15 +1,23 @@
1
1
  import { describe, test } from "bun:test";
2
- import type { InboundMessage } from "../contract/channels.ts";
3
2
  import type { ChatSurface } from "../contract/surface.ts";
4
3
  import { parseChannelKey } from "../contract/surface.ts";
5
4
  import type { TurnProgress } from "../domain/progress.ts";
6
- import {
7
- type Approval,
8
- type PromptScope,
9
- promptScope,
10
- } from "../interactions/prompts.ts";
5
+ import type { Approval } from "../interactions/prompts.ts";
11
6
  import type { ChannelKey } from "../sessions.ts";
12
7
  import type { Speaker } from "../speakers.ts";
8
+ import {
9
+ answeredOnlyByThem,
10
+ approval,
11
+ ContractBroken,
12
+ expectThat,
13
+ namedApproverAnswers,
14
+ nextPrompt,
15
+ type Running,
16
+ scopeOf,
17
+ seen,
18
+ speakerOf,
19
+ until,
20
+ } from "./surface-contract-support.ts";
13
21
 
14
22
  /** What the person on a surface's network has seen, as the contract reads it. */
15
23
  export type SurfaceObservation =
@@ -55,6 +63,20 @@ export interface SurfaceContractSubject {
55
63
  * check is skipped.
56
64
  */
57
65
  owner?: SurfaceContractAnswerer;
66
+ /**
67
+ * Another principal, not an owner, who holds the tier of the person's approvals and whom an
68
+ * approval may name in `approvers`; they know the id of the person's open prompt and answer it.
69
+ * A surface must report them through `ApprovalOptions.approvedBy`, because the runtime accepts
70
+ * an approval of a call that names approvers only from one it names. Without it the
71
+ * named-approver checks are skipped.
72
+ */
73
+ approver?: SurfaceContractAnswerer & { principalId: string };
74
+ /**
75
+ * Set by a surface that serves only conversations private to one person, such as the web chat:
76
+ * it has no shared conversation in which an approval is the named approvers', so it skips that
77
+ * check and keeps the one that its person alone answers a private conversation's.
78
+ */
79
+ privateOnly?: boolean;
58
80
  close?(): Promise<void>;
59
81
  }
60
82
 
@@ -71,11 +93,6 @@ export interface SurfaceContractFailure {
71
93
  error: unknown;
72
94
  }
73
95
 
74
- interface Running {
75
- subject: SurfaceContractSubject;
76
- delivered: InboundMessage[];
77
- }
78
-
79
96
  interface Check {
80
97
  name: string;
81
98
  /** Whether the check applies to this subject; a check that does not is skipped. */
@@ -83,72 +100,6 @@ interface Check {
83
100
  run(running: Running): Promise<void>;
84
101
  }
85
102
 
86
- class ContractBroken extends Error {
87
- override name = "ContractBroken";
88
- }
89
-
90
- function expectThat(condition: boolean, message: string): void {
91
- if (!condition) throw new ContractBroken(message);
92
- }
93
-
94
- async function until(
95
- done: () => boolean,
96
- what: string,
97
- ms = 2000,
98
- ): Promise<void> {
99
- const deadline = Date.now() + ms;
100
- while (!done()) {
101
- if (Date.now() > deadline)
102
- throw new ContractBroken(`timed out waiting until ${what}`);
103
- await Bun.sleep(5);
104
- }
105
- }
106
-
107
- const seen = (running: Running, kind: SurfaceObservation["kind"]) =>
108
- running.subject.observations().filter((o) => o.kind === kind);
109
-
110
- const speakerOf = (subject: SurfaceContractSubject): Speaker =>
111
- subject.speaker ?? {
112
- id: "contract-speaker",
113
- name: "Ada",
114
- tier: "owner",
115
- principalId: "contract-speaker",
116
- };
117
-
118
- /** The person's prompt scope in a conversation of this visibility. */
119
- const scopeOf = (
120
- subject: SurfaceContractSubject,
121
- visibility: "private" | "shared" = "shared",
122
- ): PromptScope => promptScope(speakerOf(subject), visibility);
123
-
124
- /** The newest prompt the person has seen, once one more than `before` is open. */
125
- async function nextPrompt(running: Running, before: number): Promise<string> {
126
- await until(
127
- () => seen(running, "prompt").length > before,
128
- "the person sees the approval",
129
- );
130
- const prompt = seen(running, "prompt").at(-1);
131
- if (prompt?.kind !== "prompt") throw new ContractBroken("no prompt seen");
132
- return prompt.id;
133
- }
134
-
135
- /** Asks for one approval and answers it as the person does. */
136
- async function approval(running: Running, approved: boolean) {
137
- const { surface, channel, answer } = running.subject;
138
- const prompts = surface.prompts?.(channel, scopeOf(running.subject));
139
- if (!prompts || !answer)
140
- throw new ContractBroken("the surface shows no prompts");
141
- const before = seen(running, "prompt").length;
142
- const result = prompts.confirm(
143
- "Approve?",
144
- "Send the report.",
145
- undefined,
146
- speakerOf(running.subject).tier,
147
- );
148
- await answer(await nextPrompt(running, before), approved);
149
- return result;
150
- }
151
-
152
103
  const CHECKS: readonly Check[] = [
153
104
  {
154
105
  name: "names a surface prefix without a colon, and owns the subject's channel",
@@ -331,6 +282,28 @@ const CHECKS: readonly Check[] = [
331
282
  run: (running) =>
332
283
  answeredOnlyByThem(running, running.subject.owner, "private"),
333
284
  },
285
+ {
286
+ name: "a named approver answers an approval instead of the person; the person and another person cannot",
287
+ applies: (subject) =>
288
+ subject.surface.prompts !== undefined &&
289
+ subject.answer !== undefined &&
290
+ subject.stranger !== undefined &&
291
+ subject.approver !== undefined &&
292
+ subject.privateOnly !== true,
293
+ run: namedApproverAnswers,
294
+ },
295
+ {
296
+ name: "a private conversation is answered by its person alone, whoever else a call names",
297
+ applies: (subject) =>
298
+ subject.surface.prompts !== undefined &&
299
+ subject.answer !== undefined &&
300
+ subject.approver !== undefined,
301
+ run: (running) =>
302
+ answeredOnlyByThem(running, running.subject.approver, "private", [
303
+ speakerOf(running.subject).principalId,
304
+ running.subject.approver?.principalId ?? "",
305
+ ]),
306
+ },
334
307
  {
335
308
  name: "a private conversation's approval above the person's tier goes to no one: it comes back at once, unshown",
336
309
  applies: (subject) =>
@@ -361,48 +334,6 @@ const CHECKS: readonly Check[] = [
361
334
  },
362
335
  ];
363
336
 
364
- /**
365
- * Opens the person's approval in a conversation of `visibility`, has `other` answer it by its id,
366
- * and checks that only the person's own answer settles it.
367
- */
368
- async function answeredOnlyByThem(
369
- running: Running,
370
- other: SurfaceContractAnswerer | undefined,
371
- visibility: "private" | "shared",
372
- ): Promise<void> {
373
- const { surface, channel, answer } = running.subject;
374
- const prompts = surface.prompts?.(
375
- channel,
376
- scopeOf(running.subject, visibility),
377
- );
378
- if (!prompts || !answer || !other)
379
- throw new ContractBroken("the surface shows no prompts");
380
- await other.join?.();
381
- const before = seen(running, "prompt").length;
382
- let settled: Approval | undefined;
383
- const result = prompts
384
- .confirm(
385
- "Approve?",
386
- "Send the report.",
387
- undefined,
388
- speakerOf(running.subject).tier,
389
- )
390
- .then((value) => {
391
- settled = value;
392
- return value;
393
- });
394
- const id = await nextPrompt(running, before);
395
- await other.answer(id, true);
396
- await Bun.sleep(50);
397
- expectThat(
398
- settled === undefined,
399
- `another principal's answer settled the approval as ${settled}`,
400
- );
401
- await answer(id, false);
402
- const own = await result;
403
- expectThat(own === "declined", `the person's answer came back ${own}`);
404
- }
405
-
406
337
  async function runCheck(
407
338
  check: Check,
408
339
  make: () => Promise<SurfaceContractSubject>,
@@ -445,7 +376,10 @@ export async function checkSurfaceContract(
445
376
  * where it offers them, delivers files, ends typing and stop controls idempotently, shows
446
377
  * progress, resolves approvals as the person answers or as a stopped turn cancels them, ignores
447
378
  * another person's answer to the person's approval, and, in a private conversation, an owner's
448
- * too, and lets an approval above the person's tier there come back at once, shown to no one. Each
379
+ * too, and lets an approval above the person's tier there come back at once, shown to no one.
380
+ * Where it can name approvers, an approval that does is answered by them, reported as the
381
+ * approver, and not by the person or another person, and a private conversation's is answered by
382
+ * its person alone, whoever the call names besides them. Each
449
383
  * check gets a fresh subject from `make`; a check for an optional part the surface lacks passes.
450
384
  */
451
385
  export function describeSurfaceContract(
@@ -31,13 +31,16 @@ function clientIn(port: ClientPort, turn: AbortSignal | undefined): ClientPort {
31
31
 
32
32
  /**
33
33
  * The turn a tool's `run` and `hold` receive, built from the session that makes the call and the
34
- * signal that stops it.
34
+ * signal that stops it. For a call being run, `toolCallId` finds who approved it.
35
35
  */
36
36
  export function toolTurn(
37
37
  context: SessionContext,
38
38
  signal: AbortSignal | undefined,
39
+ toolCallId?: string,
39
40
  ): ToolTurn {
40
41
  const origin = context.origin?.();
42
+ const approvedBy =
43
+ toolCallId === undefined ? undefined : context.approvedBy?.(toolCallId);
41
44
  const client = context.client?.();
42
45
  return {
43
46
  speaker: context.speaker(),
@@ -45,6 +48,7 @@ export function toolTurn(
45
48
  agent: context.agent,
46
49
  signal,
47
50
  ...(origin === undefined ? {} : { origin }),
51
+ ...(approvedBy === undefined ? {} : { approvedBy }),
48
52
  ...(client === undefined ? {} : { client: clientIn(client, signal) }),
49
53
  ...(context.workspace ? { workspace: context.workspace } : {}),
50
54
  attachFile: attachReplyFile,
package/src/index.ts CHANGED
@@ -166,8 +166,12 @@ export type {
166
166
  AccessOwner,
167
167
  AccessRules,
168
168
  AccessTier,
169
+ Provisioning,
170
+ ProvisioningRule,
171
+ SelfLinkRules,
169
172
  } from "./core/identity/access-policy.ts";
170
173
  export type { ActorFacts } from "./core/identity/actor-facts.ts";
174
+ export type { IdentityLinks } from "./core/identity/identity-links.ts";
171
175
  export type { IdentityService } from "./core/identity/identity-service.ts";
172
176
  export type {
173
177
  IdentityLink,
@@ -182,6 +186,8 @@ export type {
182
186
  Approval,
183
187
  ApprovalDetails,
184
188
  ApprovalFile,
189
+ ApprovalOptions,
190
+ ApprovedBy,
185
191
  AskOption,
186
192
  LateAnswer,
187
193
  OwnerAnswer,
@@ -293,6 +299,7 @@ export {
293
299
  CONVERSATIONS,
294
300
  DELEGATION,
295
301
  IDENTITY,
302
+ IDENTITY_LINKS,
296
303
  MEMORY,
297
304
  PRECHECKS,
298
305
  RUNTIME,