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.
- package/CHANGELOG.md +34 -0
- package/docs/migration-0.9.md +3 -1
- package/docs/plugins.md +146 -21
- package/examples/fake-surface.test.ts +137 -0
- package/examples/fake-surface.ts +71 -14
- package/package.json +1 -1
- package/src/cli/checks/identity.ts +73 -5
- package/src/core/agents/agent-claim.ts +12 -0
- package/src/core/agents/team-turns.ts +11 -4
- package/src/core/builtin/agent-server.ts +6 -0
- package/src/core/config/access.ts +23 -3
- package/src/core/config/adapters.ts +11 -0
- package/src/core/config/schema.ts +14 -0
- package/src/core/define-roundtable.ts +1 -0
- package/src/core/define.ts +44 -6
- package/src/core/discord/card-audience.ts +104 -7
- package/src/core/discord/discord-owners.ts +19 -13
- package/src/core/discord/owner-card-types.ts +73 -0
- package/src/core/discord/owner-cards.ts +56 -77
- package/src/core/domain/conversation.ts +7 -0
- package/src/core/domain/ports.ts +4 -1
- package/src/core/holds.ts +60 -0
- package/src/core/i18n/cards.ts +3 -0
- package/src/core/identity/access-policy.ts +65 -2
- package/src/core/identity/caching-principal-store.ts +2 -2
- package/src/core/identity/identity-links.ts +96 -0
- package/src/core/identity/identity-plugin.ts +10 -3
- package/src/core/identity/identity-schema.ts +3 -1
- package/src/core/identity/identity-service.ts +5 -3
- package/src/core/identity/principal-store.ts +59 -142
- package/src/core/identity/principal-tables.ts +10 -2
- package/src/core/identity/principal-types.ts +146 -0
- package/src/core/interactions/prompts.ts +47 -3
- package/src/core/modules/schedules/precheck-tools.ts +115 -20
- package/src/core/routing/conversation-turns.ts +4 -1
- package/src/core/runtime/conversation-sessions.ts +8 -0
- package/src/core/runtime/extensions/confirmation-gate-extension.ts +50 -0
- package/src/core/runtime/extensions/confirmation-gate.ts +252 -204
- package/src/core/runtime/extensions/gate-refusals.ts +133 -0
- package/src/core/runtime/extensions/held-calls.ts +91 -0
- package/src/core/runtime/extensions/hold-lookups.ts +108 -0
- package/src/core/runtime/pi-agent-runtime.ts +7 -3
- package/src/core/runtime/prompt-slot.ts +10 -2
- package/src/core/runtime/session-factory.ts +1 -0
- package/src/core/services.ts +11 -0
- package/src/core/sessions.ts +7 -0
- package/src/core/testing/surface-contract-support.ts +194 -0
- package/src/core/testing/surface-contract.ts +54 -120
- package/src/core/tool-turn.ts +5 -1
- 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.
|
|
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(
|
package/src/core/tool-turn.ts
CHANGED
|
@@ -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,
|