@ai-ecoverse/slicc-agent 5.5.4 → 5.7.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 (63) hide show
  1. package/README.md +10 -4
  2. package/dist/agent.js +4 -0
  3. package/dist/agent.js.map +2 -2
  4. package/dist/approvals/index.js +87 -45
  5. package/dist/approvals/index.js.map +2 -2
  6. package/dist/approvals/judge.js +7 -1
  7. package/dist/approvals/judge.js.map +2 -2
  8. package/dist/approvals/record.js +7 -2
  9. package/dist/approvals/record.js.map +2 -2
  10. package/dist/approvals/sudo-client.js +112 -0
  11. package/dist/approvals/sudo-client.js.map +7 -0
  12. package/dist/approvals/sudo.js +166 -0
  13. package/dist/approvals/sudo.js.map +7 -0
  14. package/dist/guests/attribution.js +19 -0
  15. package/dist/guests/attribution.js.map +2 -2
  16. package/dist/guests/client.js +4 -3
  17. package/dist/guests/client.js.map +2 -2
  18. package/dist/guests/index.js +116 -63
  19. package/dist/guests/index.js.map +2 -2
  20. package/dist/guests/review.js +262 -0
  21. package/dist/guests/review.js.map +7 -0
  22. package/dist/guests/seats.js.map +2 -2
  23. package/dist/guests/serve.js +13 -13
  24. package/dist/guests/serve.js.map +2 -2
  25. package/dist/host.js +48 -14
  26. package/dist/host.js.map +2 -2
  27. package/dist/index.js.map +2 -2
  28. package/dist/owner.js +22 -1
  29. package/dist/owner.js.map +2 -2
  30. package/dist/page.js +2 -0
  31. package/dist/page.js.map +2 -2
  32. package/dist/services.js +10 -0
  33. package/dist/services.js.map +2 -2
  34. package/dist/serving.js +2 -0
  35. package/dist/serving.js.map +2 -2
  36. package/dist/spectrum/agent-port.js +1 -1
  37. package/dist/spectrum/agent-port.js.map +2 -2
  38. package/dist/spectrum/messages.js +50 -6
  39. package/dist/spectrum/messages.js.map +2 -2
  40. package/dist/spectrum/proposals.js +7 -3
  41. package/dist/spectrum/proposals.js.map +2 -2
  42. package/dist/types/agent.d.ts +2 -0
  43. package/dist/types/approvals/index.d.ts +11 -3
  44. package/dist/types/approvals/judge.d.ts +10 -0
  45. package/dist/types/approvals/record.d.ts +6 -0
  46. package/dist/types/approvals/sudo-client.d.ts +12 -0
  47. package/dist/types/approvals/sudo.d.ts +52 -0
  48. package/dist/types/guests/attribution.d.ts +16 -0
  49. package/dist/types/guests/index.d.ts +7 -4
  50. package/dist/types/guests/review.d.ts +68 -0
  51. package/dist/types/guests/seats.d.ts +12 -1
  52. package/dist/types/guests/serve.d.ts +3 -3
  53. package/dist/types/host.d.ts +5 -2
  54. package/dist/types/index.d.ts +2 -1
  55. package/dist/types/owner.d.ts +4 -0
  56. package/dist/types/page.d.ts +2 -0
  57. package/dist/types/services.d.ts +3 -1
  58. package/dist/types/serving.d.ts +2 -0
  59. package/dist/types/spectrum/messages.d.ts +2 -0
  60. package/dist/types/spectrum/proposals.d.ts +2 -1
  61. package/dist/worker.js +7 -1
  62. package/dist/worker.js.map +2 -2
  63. package/package.json +2 -2
@@ -0,0 +1,262 @@
1
+ import { BACKGROUND_CONTEXT, withCancel } from "@earendil-works/chord/context";
2
+ import {
3
+ defineDoc,
4
+ defineExtension,
5
+ defineTask
6
+ } from "@earendil-works/pi-durable";
7
+ import { ApprovalsDoc } from "../approvals/record.js";
8
+ import {
9
+ GUEST_INPUT_KIND,
10
+ guestContent,
11
+ REVIEWED_KIND
12
+ } from "./attribution.js";
13
+ import { GUEST_REQUEST } from "./guard.js";
14
+ import { GuestsDoc, liveSeat } from "./seats.js";
15
+ const GuestReviewsDoc = defineDoc({
16
+ kind: "slicc.guest-reviews",
17
+ version: 1,
18
+ scope: "session",
19
+ initial: () => ({ next: 1, reviews: {} })
20
+ });
21
+ const GUEST_REVIEWS = "slicc-guest-reviews";
22
+ const REVIEW_TASK = "slicc.guest-review";
23
+ const MAX_QUEUED = 8;
24
+ const KEPT_REVIEWS = 100;
25
+ const KEPT_SUBMISSIONS = 100;
26
+ const DONE = { status: "terminal", outcome: { status: "completed", result: null } };
27
+ const liveReviews = (state, seat) => Object.values(state?.reviews ?? {}).filter((review) => review.seat === seat && !review.ended).sort((a, b) => a.n - b.n);
28
+ function endReview(review, state, reason, at) {
29
+ review.state = state;
30
+ review.reason = reason;
31
+ review.decidedAt = review.decidedAt ?? at;
32
+ review.ended = true;
33
+ }
34
+ async function told(tx, review) {
35
+ if (review.told) return;
36
+ review.told = true;
37
+ const data = { review: review.id, outcome: review.state };
38
+ await tx.appendEntry(review.conversation, {
39
+ kind: REVIEWED_KIND,
40
+ data: { ...data }
41
+ });
42
+ }
43
+ function pruneReviews(state, kept = KEPT_REVIEWS) {
44
+ const ended2 = Object.values(state.reviews).filter((review) => review.ended).sort((a, b) => a.n - b.n);
45
+ for (const review of ended2.slice(0, Math.max(0, ended2.length - kept)))
46
+ delete state.reviews[review.id];
47
+ }
48
+ const deliveryId = (seat, review) => `${GUEST_REQUEST}${seat}:${review}`;
49
+ const reviewOf = async (tx, id) => (await tx.doc(GuestReviewsDoc)).reviews[id];
50
+ const ended = (task) => task.status === "terminal" || task.status === "completing";
51
+ async function turn(id, runtime, context) {
52
+ await runtime.commit(async (tx) => {
53
+ const doc = await tx.doc(GuestReviewsDoc);
54
+ const review = doc.reviews[id];
55
+ if (!review || review.ended) return DONE;
56
+ const seat = (await tx.doc(GuestsDoc)).seats[review.seat];
57
+ if (!liveSeat(seat, runtime.now())) {
58
+ endReview(review, "withdrawn", "revoked", runtime.now());
59
+ await told(tx, review);
60
+ return DONE;
61
+ }
62
+ const before = liveReviews(doc, review.seat).filter((other) => other.n < review.n).reverse();
63
+ for (const prior of before) {
64
+ const record = await tx.task(prior.task);
65
+ if (record && !ended(record.state))
66
+ return {
67
+ status: "waiting",
68
+ checkpoint: { phase: "turn", after: prior.task },
69
+ on: [prior.task],
70
+ policy: "allSettled"
71
+ };
72
+ endReview(prior, "unanswered", "failed", runtime.now());
73
+ await told(tx, prior);
74
+ }
75
+ return { status: "running", checkpoint: { phase: "ask" } };
76
+ }, context);
77
+ }
78
+ async function ask(id, runtime, host, context) {
79
+ const first = (await runtime.snapshot(GuestReviewsDoc, context))?.reviews[id];
80
+ if (!first) return runtime.commit(async () => DONE, context);
81
+ return host.line(first.seat, () => asking(id, runtime, host, context));
82
+ }
83
+ async function asking(id, runtime, host, context) {
84
+ const review = (await runtime.snapshot(GuestReviewsDoc, context))?.reviews[id];
85
+ const seat = review && (await runtime.snapshot(GuestsDoc, context))?.seats[review.seat];
86
+ if (seat?.gates.messages === "off")
87
+ return runtime.commit(async (tx) => {
88
+ const kept = await reviewOf(tx, id);
89
+ if (!kept || kept.ended) return DONE;
90
+ kept.state = "approved";
91
+ kept.reason = "always";
92
+ kept.auto = true;
93
+ kept.decidedAt = runtime.now();
94
+ await told(tx, kept);
95
+ return { status: "running", checkpoint: { phase: "deliver" } };
96
+ }, context);
97
+ if (review && !review.ended)
98
+ await host.approvals.request(
99
+ {
100
+ id: review.id,
101
+ kind: "guest-message",
102
+ seat: review.seat,
103
+ agentId: review.agentId,
104
+ requester: { kind: "guest", name: review.label, seat: review.seat },
105
+ tier: review.tier,
106
+ detail: { text: review.text, attachments: [] }
107
+ },
108
+ BACKGROUND_CONTEXT
109
+ );
110
+ await runtime.commit(async (tx) => {
111
+ const kept = await reviewOf(tx, id);
112
+ if (!kept || kept.ended) return DONE;
113
+ kept.state = "pending";
114
+ return { status: "running", checkpoint: { phase: "decide" } };
115
+ }, context);
116
+ }
117
+ const itemIn = (doc, id) => doc?.items.find((item) => item.id === id);
118
+ async function settledOrDue(runtime, id, context) {
119
+ const { context: using, cancel } = withCancel(context);
120
+ const watch = await runtime.watchDoc(ApprovalsDoc, using);
121
+ try {
122
+ const now = itemIn(watch?.value, id);
123
+ if (now?.state !== "pending" || now.expiresAt <= runtime.now()) return { item: now };
124
+ const slept = runtime.sleep(now.expiresAt, using);
125
+ slept.catch(() => void 0);
126
+ const changed = new Promise((resolve) => {
127
+ watch?.start(async (value) => {
128
+ if (itemIn(value, id)?.state !== "pending") resolve();
129
+ });
130
+ });
131
+ await Promise.race([slept, changed]);
132
+ return null;
133
+ } finally {
134
+ cancel();
135
+ await watch?.stop();
136
+ }
137
+ }
138
+ async function decide(id, runtime, host, context) {
139
+ let item;
140
+ for (; ; ) {
141
+ const seen = await settledOrDue(runtime, id, context);
142
+ if (!seen) continue;
143
+ item = seen.item;
144
+ if (item?.state !== "pending") break;
145
+ item = await host.approvals.expire(id, BACKGROUND_CONTEXT);
146
+ if (item?.state !== "pending") break;
147
+ }
148
+ const settled = item;
149
+ await runtime.commit(async (tx) => {
150
+ const review = await reviewOf(tx, id);
151
+ if (!review || review.ended) return DONE;
152
+ const at = settled?.decidedAt ?? runtime.now();
153
+ if (settled?.state === "approved") {
154
+ review.state = "approved";
155
+ review.decidedAt = at;
156
+ await told(tx, review);
157
+ if (settled.answer === "always" && host.alwaysAllowed()) {
158
+ const record = (await tx.doc(GuestsDoc)).seats[review.seat];
159
+ if (record && liveSeat(record, runtime.now())) {
160
+ record.gates.messages = "off";
161
+ record.always = { by: settled.decidedBy, approval: settled.id, at };
162
+ }
163
+ }
164
+ return { status: "running", checkpoint: { phase: "deliver" } };
165
+ }
166
+ endReview(review, settled?.state ?? "unanswered", settled?.reason ?? null, at);
167
+ await told(tx, review);
168
+ return DONE;
169
+ }, context);
170
+ }
171
+ async function deliver(id, runtime, host, context) {
172
+ const review = (await runtime.snapshot(GuestReviewsDoc, context))?.reviews[id];
173
+ if (!review || review.ended) return runtime.commit(async () => DONE, context);
174
+ await host.line(review.seat, async () => {
175
+ const seat = (await runtime.snapshot(GuestsDoc, context))?.seats[review.seat];
176
+ const live = liveSeat(seat, runtime.now());
177
+ const conversation = live ? await host.agents.conversation(review.agentId, BACKGROUND_CONTEXT) : void 0;
178
+ const approval = review.auto ? null : review.id;
179
+ const data = { seat: review.seat, name: review.label, approval };
180
+ const submitted = conversation ? await conversation.submit(
181
+ {
182
+ type: "input",
183
+ content: guestContent(review.label, review.text),
184
+ whenBusy: "followUp",
185
+ entry: { kind: GUEST_INPUT_KIND, data: { ...data } },
186
+ requestId: deliveryId(review.seat, review.id)
187
+ },
188
+ BACKGROUND_CONTEXT
189
+ ) : void 0;
190
+ await runtime.commit(async (tx) => {
191
+ const kept = await reviewOf(tx, id);
192
+ if (!kept) return DONE;
193
+ kept.ended = true;
194
+ if (!submitted) {
195
+ kept.reason = live ? "gone" : "revoked";
196
+ return DONE;
197
+ }
198
+ kept.submissionId = String(submitted.id);
199
+ const record = (await tx.doc(GuestsDoc)).seats[kept.seat];
200
+ if (record) {
201
+ record.delivered = [...record.delivered ?? [], kept.id].slice(-KEPT_SUBMISSIONS);
202
+ record.submissions = [
203
+ ...record.submissions ?? [],
204
+ { id: kept.submissionId, approval, tools: kept.tools }
205
+ ].slice(-KEPT_SUBMISSIONS);
206
+ }
207
+ return DONE;
208
+ }, context);
209
+ });
210
+ }
211
+ function reviewTask(lookup) {
212
+ return defineTask({
213
+ name: REVIEW_TASK,
214
+ version: 1,
215
+ initial: () => ({ phase: "turn" }),
216
+ phases: {
217
+ turn: (task, runtime, context) => turn(task.input.review, runtime, context),
218
+ ask: async (task, runtime, context) => ask(task.input.review, runtime, await lookup(), context),
219
+ decide: async (task, runtime, context) => decide(task.input.review, runtime, await lookup(), context),
220
+ deliver: async (task, runtime, context) => deliver(task.input.review, runtime, await lookup(), context)
221
+ },
222
+ async abort(task, runtime, context) {
223
+ const host = await lookup();
224
+ await host.approvals.withdraw(task.input.review, "aborted", BACKGROUND_CONTEXT);
225
+ await runtime.commit(async (tx) => {
226
+ const review = await reviewOf(tx, task.input.review);
227
+ if (review && !review.ended) {
228
+ endReview(review, "withdrawn", "aborted", runtime.now());
229
+ await told(tx, review);
230
+ }
231
+ return { status: "terminal", outcome: { status: "aborted" } };
232
+ }, context);
233
+ }
234
+ });
235
+ }
236
+ function guestReviews() {
237
+ let bind;
238
+ const bound = new Promise((resolve) => {
239
+ bind = resolve;
240
+ });
241
+ const task = reviewTask(() => bound);
242
+ return {
243
+ extension: defineExtension({ name: GUEST_REVIEWS, tasks: [task] }),
244
+ task,
245
+ bind: (host) => bind(host)
246
+ };
247
+ }
248
+ export {
249
+ GUEST_REVIEWS,
250
+ GuestReviewsDoc,
251
+ KEPT_REVIEWS,
252
+ KEPT_SUBMISSIONS,
253
+ MAX_QUEUED,
254
+ REVIEW_TASK,
255
+ deliveryId,
256
+ endReview,
257
+ guestReviews,
258
+ liveReviews,
259
+ pruneReviews,
260
+ told
261
+ };
262
+ //# sourceMappingURL=review.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/guests/review.ts"],
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT, withCancel } from '@earendil-works/chord/context';\nimport {\n type ConversationId,\n defineDoc,\n defineExtension,\n defineTask,\n type Extension,\n type NextTaskState,\n type TaskId,\n type TaskRuntime,\n type Tx,\n} from '@earendil-works/pi-durable';\nimport type { Agents } from '../agents.ts';\nimport type { Approval, Approvals, ApprovalsState } from '../approvals/index.ts';\nimport { ApprovalsDoc } from '../approvals/record.ts';\nimport {\n GUEST_INPUT_KIND,\n type GuestInput,\n type GuestOutcome,\n type GuestReviewed,\n guestContent,\n REVIEWED_KIND,\n} from './attribution.ts';\nimport { GUEST_REQUEST } from './guard.ts';\nimport { type GuestGate, GuestsDoc, liveSeat } from './seats.ts';\n\nexport type ReviewState =\n | 'queued'\n | 'pending'\n | 'approved'\n | 'rejected'\n | 'unanswered'\n | 'withdrawn';\n\nexport type Review = {\n id: string;\n seat: string;\n agentId: string;\n label: string;\n text: string;\n request: string;\n tier: GuestGate;\n tools: GuestGate;\n n: number;\n task: number;\n state: ReviewState;\n reason: string | null;\n createdAt: number;\n decidedAt: number | null;\n submissionId: string | null;\n ended: boolean;\n conversation: number;\n auto?: boolean;\n told?: boolean;\n};\n\nexport type ReviewsState = { next: number; reviews: Record<string, Review> };\n\nexport const GuestReviewsDoc = defineDoc<ReviewsState>({\n kind: 'slicc.guest-reviews',\n version: 1,\n scope: 'session',\n initial: () => ({ next: 1, reviews: {} }),\n});\n\nexport const GUEST_REVIEWS = 'slicc-guest-reviews';\nexport const REVIEW_TASK = 'slicc.guest-review';\nexport const MAX_QUEUED = 8;\nexport const KEPT_REVIEWS = 100;\nexport const KEPT_SUBMISSIONS = 100;\n\nexport interface ReviewHost {\n approvals: Pick<Approvals, 'request' | 'expire' | 'withdraw'>;\n agents: Pick<Agents, 'conversation'>;\n line<T>(seat: string, run: () => Promise<T>): Promise<T>;\n alwaysAllowed(): boolean;\n}\n\ntype Input = { review: string };\ntype Phase =\n | { phase: 'turn'; after?: number }\n | { phase: 'ask' }\n | { phase: 'decide' }\n | { phase: 'deliver' };\ntype Runtime = TaskRuntime<Input, Phase, null, object>;\ntype Next = NextTaskState<Phase, null>;\n\nconst DONE: Next = { status: 'terminal', outcome: { status: 'completed', result: null } };\n\nexport const liveReviews = (state: ReviewsState | null | undefined, seat: string): Review[] =>\n Object.values(state?.reviews ?? {})\n .filter((review) => review.seat === seat && !review.ended)\n .sort((a, b) => a.n - b.n);\n\nexport function endReview(review: Review, state: ReviewState, reason: string | null, at: number) {\n review.state = state;\n review.reason = reason;\n review.decidedAt = review.decidedAt ?? at;\n review.ended = true;\n}\n\nexport async function told(tx: Pick<Tx, 'appendEntry'>, review: Review): Promise<void> {\n if (review.told) return;\n review.told = true;\n const data: GuestReviewed = { review: review.id, outcome: review.state as GuestOutcome };\n await tx.appendEntry(review.conversation as unknown as ConversationId, {\n kind: REVIEWED_KIND,\n data: { ...data },\n });\n}\n\nexport function pruneReviews(state: ReviewsState, kept = KEPT_REVIEWS) {\n const ended = Object.values(state.reviews)\n .filter((review) => review.ended)\n .sort((a, b) => a.n - b.n);\n for (const review of ended.slice(0, Math.max(0, ended.length - kept)))\n delete state.reviews[review.id];\n}\n\nexport const deliveryId = (seat: string, review: string): string =>\n `${GUEST_REQUEST}${seat}:${review}`;\n\nconst reviewOf = async (tx: Tx, id: string) => (await tx.doc(GuestReviewsDoc)).reviews[id];\n\nconst ended = (task: { status: string }) =>\n task.status === 'terminal' || task.status === 'completing';\n\nasync function turn(id: string, runtime: Runtime, context: Context) {\n await runtime.commit(async (tx): Promise<Next> => {\n const doc = await tx.doc(GuestReviewsDoc);\n const review = doc.reviews[id];\n if (!review || review.ended) return DONE;\n const seat = (await tx.doc(GuestsDoc)).seats[review.seat];\n if (!liveSeat(seat, runtime.now())) {\n endReview(review, 'withdrawn', 'revoked', runtime.now());\n await told(tx, review);\n return DONE;\n }\n const before = liveReviews(doc, review.seat)\n .filter((other) => other.n < review.n)\n .reverse();\n for (const prior of before) {\n const record = await tx.task(prior.task as unknown as TaskId);\n if (record && !ended(record.state))\n return {\n status: 'waiting',\n checkpoint: { phase: 'turn', after: prior.task },\n on: [prior.task as unknown as TaskId],\n policy: 'allSettled',\n };\n endReview(prior, 'unanswered', 'failed', runtime.now());\n await told(tx, prior);\n }\n return { status: 'running', checkpoint: { phase: 'ask' } };\n }, context);\n}\n\nasync function ask(id: string, runtime: Runtime, host: ReviewHost, context: Context) {\n const first = (await runtime.snapshot(GuestReviewsDoc, context))?.reviews[id];\n if (!first) return runtime.commit(async () => DONE, context);\n return host.line(first.seat, () => asking(id, runtime, host, context));\n}\n\nasync function asking(id: string, runtime: Runtime, host: ReviewHost, context: Context) {\n const review = (await runtime.snapshot(GuestReviewsDoc, context))?.reviews[id];\n const seat = review && (await runtime.snapshot(GuestsDoc, context))?.seats[review.seat];\n if (seat?.gates.messages === 'off')\n return runtime.commit(async (tx): Promise<Next> => {\n const kept = await reviewOf(tx, id);\n if (!kept || kept.ended) return DONE;\n kept.state = 'approved';\n kept.reason = 'always';\n kept.auto = true;\n kept.decidedAt = runtime.now();\n await told(tx, kept);\n return { status: 'running', checkpoint: { phase: 'deliver' } };\n }, context);\n if (review && !review.ended)\n await host.approvals.request(\n {\n id: review.id,\n kind: 'guest-message',\n seat: review.seat,\n agentId: review.agentId,\n requester: { kind: 'guest', name: review.label, seat: review.seat },\n tier: review.tier,\n detail: { text: review.text, attachments: [] },\n },\n BACKGROUND_CONTEXT\n );\n await runtime.commit(async (tx): Promise<Next> => {\n const kept = await reviewOf(tx, id);\n if (!kept || kept.ended) return DONE;\n kept.state = 'pending';\n return { status: 'running', checkpoint: { phase: 'decide' } };\n }, context);\n}\n\nconst itemIn = (doc: Readonly<ApprovalsState> | null | undefined, id: string) =>\n doc?.items.find((item) => item.id === id);\n\nasync function settledOrDue(\n runtime: Runtime,\n id: string,\n context: Context\n): Promise<{ item: Approval | undefined } | null> {\n const { context: using, cancel } = withCancel(context);\n const watch = await runtime.watchDoc(ApprovalsDoc, using);\n try {\n const now = itemIn(watch?.value, id);\n if (now?.state !== 'pending' || now.expiresAt <= runtime.now()) return { item: now };\n const slept = runtime.sleep(now.expiresAt, using);\n slept.catch(() => undefined);\n const changed = new Promise<void>((resolve) => {\n watch?.start(async (value) => {\n if (itemIn(value, id)?.state !== 'pending') resolve();\n });\n });\n await Promise.race([slept, changed]);\n return null;\n } finally {\n cancel();\n await watch?.stop();\n }\n}\n\nasync function decide(id: string, runtime: Runtime, host: ReviewHost, context: Context) {\n let item: Approval | undefined;\n for (;;) {\n const seen = await settledOrDue(runtime, id, context);\n if (!seen) continue;\n item = seen.item;\n if (item?.state !== 'pending') break;\n item = await host.approvals.expire(id, BACKGROUND_CONTEXT);\n if (item?.state !== 'pending') break;\n }\n const settled = item;\n await runtime.commit(async (tx): Promise<Next> => {\n const review = await reviewOf(tx, id);\n if (!review || review.ended) return DONE;\n const at = settled?.decidedAt ?? runtime.now();\n if (settled?.state === 'approved') {\n review.state = 'approved';\n review.decidedAt = at;\n await told(tx, review);\n if (settled.answer === 'always' && host.alwaysAllowed()) {\n const record = (await tx.doc(GuestsDoc)).seats[review.seat];\n if (record && liveSeat(record, runtime.now())) {\n record.gates.messages = 'off';\n record.always = { by: settled.decidedBy, approval: settled.id, at };\n }\n }\n return { status: 'running', checkpoint: { phase: 'deliver' } };\n }\n endReview(review, settled?.state ?? 'unanswered', settled?.reason ?? null, at);\n await told(tx, review);\n return DONE;\n }, context);\n}\n\nasync function deliver(id: string, runtime: Runtime, host: ReviewHost, context: Context) {\n const review = (await runtime.snapshot(GuestReviewsDoc, context))?.reviews[id];\n if (!review || review.ended) return runtime.commit(async () => DONE, context);\n await host.line(review.seat, async () => {\n const seat = (await runtime.snapshot(GuestsDoc, context))?.seats[review.seat];\n const live = liveSeat(seat, runtime.now());\n const conversation = live\n ? await host.agents.conversation(review.agentId, BACKGROUND_CONTEXT)\n : undefined;\n const approval = review.auto ? null : review.id;\n const data: GuestInput = { seat: review.seat, name: review.label, approval };\n const submitted = conversation\n ? await conversation.submit(\n {\n type: 'input',\n content: guestContent(review.label, review.text),\n whenBusy: 'followUp',\n entry: { kind: GUEST_INPUT_KIND, data: { ...data } },\n requestId: deliveryId(review.seat, review.id),\n },\n BACKGROUND_CONTEXT\n )\n : undefined;\n await runtime.commit(async (tx): Promise<Next> => {\n const kept = await reviewOf(tx, id);\n if (!kept) return DONE;\n kept.ended = true;\n if (!submitted) {\n kept.reason = live ? 'gone' : 'revoked';\n return DONE;\n }\n kept.submissionId = String(submitted.id);\n const record = (await tx.doc(GuestsDoc)).seats[kept.seat];\n if (record) {\n record.delivered = [...(record.delivered ?? []), kept.id].slice(-KEPT_SUBMISSIONS);\n record.submissions = [\n ...(record.submissions ?? []),\n { id: kept.submissionId, approval, tools: kept.tools },\n ].slice(-KEPT_SUBMISSIONS);\n }\n return DONE;\n }, context);\n });\n}\n\nexport interface GuestReviews {\n readonly extension: Extension;\n readonly task: ReturnType<typeof reviewTask>;\n bind(host: ReviewHost): void;\n}\n\nfunction reviewTask(lookup: () => Promise<ReviewHost>) {\n return defineTask<Input, Phase, null>({\n name: REVIEW_TASK,\n version: 1,\n initial: () => ({ phase: 'turn' }),\n phases: {\n turn: (task, runtime, context) => turn(task.input.review, runtime, context),\n ask: async (task, runtime, context) =>\n ask(task.input.review, runtime, await lookup(), context),\n decide: async (task, runtime, context) =>\n decide(task.input.review, runtime, await lookup(), context),\n deliver: async (task, runtime, context) =>\n deliver(task.input.review, runtime, await lookup(), context),\n },\n async abort(task, runtime, context) {\n const host = await lookup();\n await host.approvals.withdraw(task.input.review, 'aborted', BACKGROUND_CONTEXT);\n await runtime.commit(async (tx) => {\n const review = await reviewOf(tx, task.input.review);\n if (review && !review.ended) {\n endReview(review, 'withdrawn', 'aborted', runtime.now());\n await told(tx, review);\n }\n return { status: 'terminal', outcome: { status: 'aborted' } };\n }, context);\n },\n });\n}\n\nexport function guestReviews(): GuestReviews {\n let bind: (host: ReviewHost) => void;\n const bound = new Promise<ReviewHost>((resolve) => {\n bind = resolve;\n });\n const task = reviewTask(() => bound);\n return {\n extension: defineExtension({ name: GUEST_REVIEWS, tasks: [task] }),\n task,\n bind: (host) => bind(host),\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["ended"]
7
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/guests/seats.ts"],
4
- "sourcesContent": ["import { defineDoc } from '@earendil-works/pi-durable';\nimport type { ApprovalTier } from '../approvals/record.ts';\nimport {\n DAY_MS,\n DEFAULT_GUEST_POLICY,\n type GuestPolicy,\n gateKind,\n gateRefusal,\n MAX_SEAT_DAYS,\n} from './policy.ts';\n\nexport type GuestGate = ApprovalTier | 'off';\n\nexport type GuestGates = {\n messages: GuestGate;\n tools: GuestGate;\n};\n\nexport type GuestDraft = {\n id: string;\n label: string;\n agentId: string;\n expiresAt: number | null;\n gates: GuestGates;\n};\n\nexport type SeatRecord = GuestDraft & {\n createdAt: number;\n epoch: number;\n revoked: number | null;\n delivered?: string[];\n};\n\nexport interface GuestSeat {\n id: string;\n label: string;\n agentId: string;\n createdAt: number;\n expiresAt: number | null;\n gates: GuestGates;\n connected: boolean;\n pending: number;\n}\n\nexport type GuestsState = { seats: Record<string, SeatRecord> };\n\nexport const GuestsDoc = defineDoc<GuestsState>({\n kind: 'slicc.guests',\n version: 1,\n scope: 'session',\n initial: () => ({ seats: {} }),\n});\n\nexport const MAX_SEAT_MS = MAX_SEAT_DAYS * DAY_MS;\nexport const MAX_LABEL = 64;\nconst MAX_ID = 256;\n\nexport function cleanLabel(raw: unknown): string {\n return String(raw ?? '')\n .replace(/[\\p{Cc}\\p{Cf}]/gu, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, MAX_LABEL);\n}\n\nexport function isGate(value: unknown): value is GuestGate {\n if (typeof value !== 'string') return false;\n if (value === 'user' || value === 'agent' || value === 'cone' || value === 'off') return true;\n return /^scoop:\\S+$/.test(value);\n}\n\nexport function checkDraft(\n draft: GuestDraft,\n now: number,\n policy: GuestPolicy = DEFAULT_GUEST_POLICY\n): string | null {\n if (typeof draft?.id !== 'string' || !draft.id || draft.id.length > MAX_ID)\n return 'A guest seat needs an id from the tray hub.';\n if (!cleanLabel(draft.label)) return 'A guest seat needs a label.';\n if (!isGate(draft.gates?.messages) || !isGate(draft.gates?.tools))\n return 'A guest seat needs a known gate for messages and for tools.';\n if (!policy.allowedMessageGates.includes(gateKind(draft.gates.messages)))\n return gateRefusal(\"a guest's messages\", policy.allowedMessageGates);\n if (!policy.allowedToolGates.includes(gateKind(draft.gates.tools)))\n return gateRefusal(\"a guest's tool calls\", policy.allowedToolGates);\n const { expiresAt } = draft;\n if (expiresAt !== null && (typeof expiresAt !== 'number' || !Number.isFinite(expiresAt)))\n return 'A guest seat expires at a time, or never.';\n if (expiresAt !== null && expiresAt <= now) return 'That expiry has already passed.';\n const days = policy.maxSeatDays;\n if (expiresAt !== null && expiresAt - now > days * DAY_MS)\n return `A guest seat lasts at most ${days} ${days === 1 ? 'day' : 'days'}.`;\n return null;\n}\n\nexport function liveSeat(seat: SeatRecord | undefined, now: number): boolean {\n return Boolean(\n seat && seat.revoked === null && (seat.expiresAt === null || seat.expiresAt > now)\n );\n}\n"],
5
- "mappings": "AAAA,SAAS,iBAAiB;AAE1B;AAAA,EACE;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAqCA,MAAM,YAAY,UAAuB;AAAA,EAC9C,MAAM;AAAA,EACN,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,OAAO,EAAE,OAAO,CAAC,EAAE;AAC9B,CAAC;AAEM,MAAM,cAAc,gBAAgB;AACpC,MAAM,YAAY;AACzB,MAAM,SAAS;AAER,SAAS,WAAW,KAAsB;AAC/C,SAAO,OAAO,OAAO,EAAE,EACpB,QAAQ,oBAAoB,GAAG,EAC/B,QAAQ,QAAQ,GAAG,EACnB,KAAK,EACL,MAAM,GAAG,SAAS;AACvB;AAEO,SAAS,OAAO,OAAoC;AACzD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,UAAU,UAAU,UAAU,WAAW,UAAU,UAAU,UAAU,MAAO,QAAO;AACzF,SAAO,cAAc,KAAK,KAAK;AACjC;AAEO,SAAS,WACd,OACA,KACA,SAAsB,sBACP;AACf,MAAI,OAAO,OAAO,OAAO,YAAY,CAAC,MAAM,MAAM,MAAM,GAAG,SAAS;AAClE,WAAO;AACT,MAAI,CAAC,WAAW,MAAM,KAAK,EAAG,QAAO;AACrC,MAAI,CAAC,OAAO,MAAM,OAAO,QAAQ,KAAK,CAAC,OAAO,MAAM,OAAO,KAAK;AAC9D,WAAO;AACT,MAAI,CAAC,OAAO,oBAAoB,SAAS,SAAS,MAAM,MAAM,QAAQ,CAAC;AACrE,WAAO,YAAY,sBAAsB,OAAO,mBAAmB;AACrE,MAAI,CAAC,OAAO,iBAAiB,SAAS,SAAS,MAAM,MAAM,KAAK,CAAC;AAC/D,WAAO,YAAY,wBAAwB,OAAO,gBAAgB;AACpE,QAAM,EAAE,UAAU,IAAI;AACtB,MAAI,cAAc,SAAS,OAAO,cAAc,YAAY,CAAC,OAAO,SAAS,SAAS;AACpF,WAAO;AACT,MAAI,cAAc,QAAQ,aAAa,IAAK,QAAO;AACnD,QAAM,OAAO,OAAO;AACpB,MAAI,cAAc,QAAQ,YAAY,MAAM,OAAO;AACjD,WAAO,8BAA8B,IAAI,IAAI,SAAS,IAAI,QAAQ,MAAM;AAC1E,SAAO;AACT;AAEO,SAAS,SAAS,MAA8B,KAAsB;AAC3E,SAAO;AAAA,IACL,QAAQ,KAAK,YAAY,SAAS,KAAK,cAAc,QAAQ,KAAK,YAAY;AAAA,EAChF;AACF;",
4
+ "sourcesContent": ["import { defineDoc } from '@earendil-works/pi-durable';\nimport type { ApprovalTier, DecidedBy } from '../approvals/record.ts';\nimport {\n DAY_MS,\n DEFAULT_GUEST_POLICY,\n type GuestPolicy,\n gateKind,\n gateRefusal,\n MAX_SEAT_DAYS,\n} from './policy.ts';\n\nexport type GuestGate = ApprovalTier | 'off';\n\nexport type GuestGates = {\n messages: GuestGate;\n tools: GuestGate;\n};\n\nexport type GuestDraft = {\n id: string;\n label: string;\n agentId: string;\n expiresAt: number | null;\n gates: GuestGates;\n};\n\nexport type SeatSubmission = { id: string; approval: string | null; tools: GuestGate };\n\nexport type SeatRecord = GuestDraft & {\n createdAt: number;\n epoch: number;\n revoked: number | null;\n delivered?: string[];\n submissions?: SeatSubmission[];\n always?: { by: DecidedBy | null; approval: string; at: number };\n};\n\nexport interface GuestSeat {\n id: string;\n label: string;\n agentId: string;\n createdAt: number;\n expiresAt: number | null;\n gates: GuestGates;\n connected: boolean;\n pending: number;\n}\n\nexport type GuestsState = { seats: Record<string, SeatRecord> };\n\nexport const GuestsDoc = defineDoc<GuestsState>({\n kind: 'slicc.guests',\n version: 1,\n scope: 'session',\n initial: () => ({ seats: {} }),\n});\n\nexport const MAX_SEAT_MS = MAX_SEAT_DAYS * DAY_MS;\nexport const MAX_LABEL = 64;\nconst MAX_ID = 256;\n\nexport function cleanLabel(raw: unknown): string {\n return String(raw ?? '')\n .replace(/[\\p{Cc}\\p{Cf}]/gu, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, MAX_LABEL);\n}\n\nexport function isGate(value: unknown): value is GuestGate {\n if (typeof value !== 'string') return false;\n if (value === 'user' || value === 'agent' || value === 'cone' || value === 'off') return true;\n return /^scoop:\\S+$/.test(value);\n}\n\nexport function checkDraft(\n draft: GuestDraft,\n now: number,\n policy: GuestPolicy = DEFAULT_GUEST_POLICY\n): string | null {\n if (typeof draft?.id !== 'string' || !draft.id || draft.id.length > MAX_ID)\n return 'A guest seat needs an id from the tray hub.';\n if (!cleanLabel(draft.label)) return 'A guest seat needs a label.';\n if (!isGate(draft.gates?.messages) || !isGate(draft.gates?.tools))\n return 'A guest seat needs a known gate for messages and for tools.';\n if (!policy.allowedMessageGates.includes(gateKind(draft.gates.messages)))\n return gateRefusal(\"a guest's messages\", policy.allowedMessageGates);\n if (!policy.allowedToolGates.includes(gateKind(draft.gates.tools)))\n return gateRefusal(\"a guest's tool calls\", policy.allowedToolGates);\n const { expiresAt } = draft;\n if (expiresAt !== null && (typeof expiresAt !== 'number' || !Number.isFinite(expiresAt)))\n return 'A guest seat expires at a time, or never.';\n if (expiresAt !== null && expiresAt <= now) return 'That expiry has already passed.';\n const days = policy.maxSeatDays;\n if (expiresAt !== null && expiresAt - now > days * DAY_MS)\n return `A guest seat lasts at most ${days} ${days === 1 ? 'day' : 'days'}.`;\n return null;\n}\n\nexport function liveSeat(seat: SeatRecord | undefined, now: number): boolean {\n return Boolean(\n seat && seat.revoked === null && (seat.expiresAt === null || seat.expiresAt > now)\n );\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAE1B;AAAA,EACE;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAyCA,MAAM,YAAY,UAAuB;AAAA,EAC9C,MAAM;AAAA,EACN,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,OAAO,EAAE,OAAO,CAAC,EAAE;AAC9B,CAAC;AAEM,MAAM,cAAc,gBAAgB;AACpC,MAAM,YAAY;AACzB,MAAM,SAAS;AAER,SAAS,WAAW,KAAsB;AAC/C,SAAO,OAAO,OAAO,EAAE,EACpB,QAAQ,oBAAoB,GAAG,EAC/B,QAAQ,QAAQ,GAAG,EACnB,KAAK,EACL,MAAM,GAAG,SAAS;AACvB;AAEO,SAAS,OAAO,OAAoC;AACzD,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,UAAU,UAAU,UAAU,WAAW,UAAU,UAAU,UAAU,MAAO,QAAO;AACzF,SAAO,cAAc,KAAK,KAAK;AACjC;AAEO,SAAS,WACd,OACA,KACA,SAAsB,sBACP;AACf,MAAI,OAAO,OAAO,OAAO,YAAY,CAAC,MAAM,MAAM,MAAM,GAAG,SAAS;AAClE,WAAO;AACT,MAAI,CAAC,WAAW,MAAM,KAAK,EAAG,QAAO;AACrC,MAAI,CAAC,OAAO,MAAM,OAAO,QAAQ,KAAK,CAAC,OAAO,MAAM,OAAO,KAAK;AAC9D,WAAO;AACT,MAAI,CAAC,OAAO,oBAAoB,SAAS,SAAS,MAAM,MAAM,QAAQ,CAAC;AACrE,WAAO,YAAY,sBAAsB,OAAO,mBAAmB;AACrE,MAAI,CAAC,OAAO,iBAAiB,SAAS,SAAS,MAAM,MAAM,KAAK,CAAC;AAC/D,WAAO,YAAY,wBAAwB,OAAO,gBAAgB;AACpE,QAAM,EAAE,UAAU,IAAI;AACtB,MAAI,cAAc,SAAS,OAAO,cAAc,YAAY,CAAC,OAAO,SAAS,SAAS;AACpF,WAAO;AACT,MAAI,cAAc,QAAQ,aAAa,IAAK,QAAO;AACnD,QAAM,OAAO,OAAO;AACpB,MAAI,cAAc,QAAQ,YAAY,MAAM,OAAO;AACjD,WAAO,8BAA8B,IAAI,IAAI,SAAS,IAAI,QAAQ,MAAM;AAC1E,SAAO;AACT;AAEO,SAAS,SAAS,MAA8B,KAAsB;AAC3E,SAAO;AAAA,IACL,QAAQ,KAAK,YAAY,SAAS,KAAK,cAAc,QAAQ,KAAK,YAAY;AAAA,EAChF;AACF;",
6
6
  "names": []
7
7
  }
@@ -2,20 +2,20 @@ import { forbidden, replicatedState } from "@earendil-works/chord";
2
2
  import { PortLink } from "../wire.js";
3
3
  import { GUESTS_MANAGE_DENIED, GUESTS_MANAGE_LATER } from "./policy.js";
4
4
  import { guestView } from "./view.js";
5
- function proposalsOf(items, seat) {
6
- return (items ?? []).filter((item) => item.kind === "guest-message" && item.seat === seat).map((item) => ({
7
- id: item.id,
8
- text: item.detail.text ?? "",
9
- state: item.state,
10
- reason: item.reason ?? null,
11
- createdAt: item.createdAt,
12
- decidedAt: item.decidedAt ?? null
5
+ function proposalsOf(state, seat) {
6
+ return Object.values(state?.reviews ?? {}).filter((review) => review.seat === seat).sort((a, b) => a.n - b.n).map((review) => ({
7
+ id: review.id,
8
+ text: review.text,
9
+ state: review.state,
10
+ reason: review.reason,
11
+ createdAt: review.createdAt,
12
+ decidedAt: review.decidedAt
13
13
  }));
14
14
  }
15
15
  const managing = (guests) => Promise.reject(
16
16
  forbidden(guests.policy().manageGuests === "deny" ? GUESTS_MANAGE_DENIED : GUESTS_MANAGE_LATER)
17
17
  );
18
- function guestOpener(guests, agents, views, context, approvals) {
18
+ function guestOpener(guests, agents, views, context) {
19
19
  return (principal) => {
20
20
  const record = guests.current(principal);
21
21
  if (!record) return void 0;
@@ -35,10 +35,10 @@ function guestOpener(guests, agents, views, context, approvals) {
35
35
  expiresAt: record.expiresAt,
36
36
  gates: { ...record.gates }
37
37
  });
38
- let listed = JSON.stringify(proposalsOf(approvals?.value, record.id));
38
+ let listed = JSON.stringify(proposalsOf(guests.reviews.value, record.id));
39
39
  const proposals = replicatedState(JSON.parse(listed));
40
- const offProposals = approvals?.subscribe((items) => {
41
- const next = JSON.stringify(proposalsOf(items, record.id));
40
+ const offProposals = guests.reviews.subscribe((state) => {
41
+ const next = JSON.stringify(proposalsOf(state, record.id));
42
42
  if (next === listed) return;
43
43
  listed = next;
44
44
  proposals.replace(context, JSON.parse(next));
@@ -54,7 +54,7 @@ function guestOpener(guests, agents, views, context, approvals) {
54
54
  },
55
55
  dispose() {
56
56
  off();
57
- offProposals?.();
57
+ offProposals();
58
58
  }
59
59
  };
60
60
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/guests/serve.ts"],
4
- "sourcesContent": ["import type { Context, Host, ReplicatedState } from '@earendil-works/chord';\nimport { forbidden, replicatedState } from '@earendil-works/chord';\nimport type { ConversationView } from '@earendil-works/pi-durable';\nimport type { Agents } from '../agents.ts';\nimport type { Approval } from '../approvals/index.ts';\nimport type { AgentGuest, GuestProposal, GuestShown } from '../services.ts';\nimport type { AgentPrincipal, GuestPrincipal } from '../serving.ts';\nimport { type PortEndpoint, PortLink } from '../wire.ts';\nimport type { Guests } from './index.ts';\nimport { GUESTS_MANAGE_DENIED, GUESTS_MANAGE_LATER } from './policy.ts';\nimport { guestView } from './view.ts';\n\nexport function proposalsOf(items: readonly Approval[] | undefined, seat: string): GuestProposal[] {\n return (items ?? [])\n .filter((item) => item.kind === 'guest-message' && item.seat === seat)\n .map((item) => ({\n id: item.id,\n text: (item.detail as { text?: string }).text ?? '',\n state: item.state,\n reason: item.reason ?? null,\n createdAt: item.createdAt,\n decidedAt: item.decidedAt ?? null,\n }));\n}\n\nconst managing = (guests: Pick<Guests, 'policy'>): Promise<never> =>\n Promise.reject(\n forbidden(guests.policy().manageGuests === 'deny' ? GUESTS_MANAGE_DENIED : GUESTS_MANAGE_LATER)\n );\n\nexport function guestOpener(\n guests: Guests,\n agents: Pick<Agents, 'state'>,\n views: ReplicatedState<Record<string, ConversationView>>,\n context: Context,\n approvals?: ReplicatedState<Approval[]>\n) {\n return (principal: GuestPrincipal): { value: AgentGuest; dispose(): void } | undefined => {\n const record = guests.current(principal);\n if (!record) return undefined;\n const pick = (all: Record<string, ConversationView> | undefined) => all?.[record.agentId];\n let shown = pick(views.value);\n const transcript = replicatedState<ConversationView>(guestView(shown, record.id));\n const off = views.subscribe((all) => {\n if (pick(all) === shown) return;\n shown = pick(all);\n transcript.replace(context, guestView(shown, record.id));\n });\n const seat = replicatedState<GuestShown>({\n id: record.id,\n label: record.label,\n agentId: record.agentId,\n name: agents.state().cones[record.agentId]?.name ?? record.agentId,\n expiresAt: record.expiresAt,\n gates: { ...record.gates },\n });\n let listed = JSON.stringify(proposalsOf(approvals?.value, record.id));\n const proposals = replicatedState<GuestProposal[]>(JSON.parse(listed) as GuestProposal[]);\n const offProposals = approvals?.subscribe((items) => {\n const next = JSON.stringify(proposalsOf(items, record.id));\n if (next === listed) return;\n listed = next;\n proposals.replace(context, JSON.parse(next) as GuestProposal[]);\n });\n return {\n value: {\n transcript,\n seat,\n proposals,\n propose: (request, using) => guests.propose(principal, request, using),\n guestOpen: () => managing(guests),\n guestRevoke: () => managing(guests),\n },\n dispose() {\n off();\n offProposals?.();\n },\n };\n };\n}\n\nexport function admitGuest(\n guests: Guests,\n endpoint: PortEndpoint,\n asked: { id: string; label: string },\n serverId: string,\n link: number,\n host: Pick<Host, 'accept'>\n): void {\n const port = new PortLink(endpoint);\n const guest = guests.admit(String(asked.id), String(asked.label));\n if (!guest) {\n port.close('this guest seat is not valid');\n return;\n }\n void port.ended.then(guests.joined(guest.id));\n port.greet(serverId);\n host.accept(port, { principal: { seat: 'guest', link, guest } satisfies AgentPrincipal });\n}\n"],
5
- "mappings": "AACA,SAAS,WAAW,uBAAuB;AAM3C,SAA4B,gBAAgB;AAE5C,SAAS,sBAAsB,2BAA2B;AAC1D,SAAS,iBAAiB;AAEnB,SAAS,YAAY,OAAwC,MAA+B;AACjG,UAAQ,SAAS,CAAC,GACf,OAAO,CAAC,SAAS,KAAK,SAAS,mBAAmB,KAAK,SAAS,IAAI,EACpE,IAAI,CAAC,UAAU;AAAA,IACd,IAAI,KAAK;AAAA,IACT,MAAO,KAAK,OAA6B,QAAQ;AAAA,IACjD,OAAO,KAAK;AAAA,IACZ,QAAQ,KAAK,UAAU;AAAA,IACvB,WAAW,KAAK;AAAA,IAChB,WAAW,KAAK,aAAa;AAAA,EAC/B,EAAE;AACN;AAEA,MAAM,WAAW,CAAC,WAChB,QAAQ;AAAA,EACN,UAAU,OAAO,OAAO,EAAE,iBAAiB,SAAS,uBAAuB,mBAAmB;AAChG;AAEK,SAAS,YACd,QACA,QACA,OACA,SACA,WACA;AACA,SAAO,CAAC,cAAkF;AACxF,UAAM,SAAS,OAAO,QAAQ,SAAS;AACvC,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,OAAO,CAAC,QAAsD,MAAM,OAAO,OAAO;AACxF,QAAI,QAAQ,KAAK,MAAM,KAAK;AAC5B,UAAM,aAAa,gBAAkC,UAAU,OAAO,OAAO,EAAE,CAAC;AAChF,UAAM,MAAM,MAAM,UAAU,CAAC,QAAQ;AACnC,UAAI,KAAK,GAAG,MAAM,MAAO;AACzB,cAAQ,KAAK,GAAG;AAChB,iBAAW,QAAQ,SAAS,UAAU,OAAO,OAAO,EAAE,CAAC;AAAA,IACzD,CAAC;AACD,UAAM,OAAO,gBAA4B;AAAA,MACvC,IAAI,OAAO;AAAA,MACX,OAAO,OAAO;AAAA,MACd,SAAS,OAAO;AAAA,MAChB,MAAM,OAAO,MAAM,EAAE,MAAM,OAAO,OAAO,GAAG,QAAQ,OAAO;AAAA,MAC3D,WAAW,OAAO;AAAA,MAClB,OAAO,EAAE,GAAG,OAAO,MAAM;AAAA,IAC3B,CAAC;AACD,QAAI,SAAS,KAAK,UAAU,YAAY,WAAW,OAAO,OAAO,EAAE,CAAC;AACpE,UAAM,YAAY,gBAAiC,KAAK,MAAM,MAAM,CAAoB;AACxF,UAAM,eAAe,WAAW,UAAU,CAAC,UAAU;AACnD,YAAM,OAAO,KAAK,UAAU,YAAY,OAAO,OAAO,EAAE,CAAC;AACzD,UAAI,SAAS,OAAQ;AACrB,eAAS;AACT,gBAAU,QAAQ,SAAS,KAAK,MAAM,IAAI,CAAoB;AAAA,IAChE,CAAC;AACD,WAAO;AAAA,MACL,OAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,CAAC,SAAS,UAAU,OAAO,QAAQ,WAAW,SAAS,KAAK;AAAA,QACrE,WAAW,MAAM,SAAS,MAAM;AAAA,QAChC,aAAa,MAAM,SAAS,MAAM;AAAA,MACpC;AAAA,MACA,UAAU;AACR,YAAI;AACJ,uBAAe;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,WACd,QACA,UACA,OACA,UACA,MACA,MACM;AACN,QAAM,OAAO,IAAI,SAAS,QAAQ;AAClC,QAAM,QAAQ,OAAO,MAAM,OAAO,MAAM,EAAE,GAAG,OAAO,MAAM,KAAK,CAAC;AAChE,MAAI,CAAC,OAAO;AACV,SAAK,MAAM,8BAA8B;AACzC;AAAA,EACF;AACA,OAAK,KAAK,MAAM,KAAK,OAAO,OAAO,MAAM,EAAE,CAAC;AAC5C,OAAK,MAAM,QAAQ;AACnB,OAAK,OAAO,MAAM,EAAE,WAAW,EAAE,MAAM,SAAS,MAAM,MAAM,EAA2B,CAAC;AAC1F;",
4
+ "sourcesContent": ["import type { Context, Host, ReplicatedState } from '@earendil-works/chord';\nimport { forbidden, replicatedState } from '@earendil-works/chord';\nimport type { ConversationView } from '@earendil-works/pi-durable';\nimport type { Agents } from '../agents.ts';\nimport type { AgentGuest, GuestProposal, GuestShown } from '../services.ts';\nimport type { AgentPrincipal, GuestPrincipal } from '../serving.ts';\nimport { type PortEndpoint, PortLink } from '../wire.ts';\nimport type { Guests } from './index.ts';\nimport { GUESTS_MANAGE_DENIED, GUESTS_MANAGE_LATER } from './policy.ts';\nimport type { ReviewsState } from './review.ts';\nimport { guestView } from './view.ts';\n\nexport function proposalsOf(state: ReviewsState | null | undefined, seat: string): GuestProposal[] {\n return Object.values(state?.reviews ?? {})\n .filter((review) => review.seat === seat)\n .sort((a, b) => a.n - b.n)\n .map((review) => ({\n id: review.id,\n text: review.text,\n state: review.state,\n reason: review.reason,\n createdAt: review.createdAt,\n decidedAt: review.decidedAt,\n }));\n}\n\nconst managing = (guests: Pick<Guests, 'policy'>): Promise<never> =>\n Promise.reject(\n forbidden(guests.policy().manageGuests === 'deny' ? GUESTS_MANAGE_DENIED : GUESTS_MANAGE_LATER)\n );\n\nexport function guestOpener(\n guests: Guests,\n agents: Pick<Agents, 'state'>,\n views: ReplicatedState<Record<string, ConversationView>>,\n context: Context\n) {\n return (principal: GuestPrincipal): { value: AgentGuest; dispose(): void } | undefined => {\n const record = guests.current(principal);\n if (!record) return undefined;\n const pick = (all: Record<string, ConversationView> | undefined) => all?.[record.agentId];\n let shown = pick(views.value);\n const transcript = replicatedState<ConversationView>(guestView(shown, record.id));\n const off = views.subscribe((all) => {\n if (pick(all) === shown) return;\n shown = pick(all);\n transcript.replace(context, guestView(shown, record.id));\n });\n const seat = replicatedState<GuestShown>({\n id: record.id,\n label: record.label,\n agentId: record.agentId,\n name: agents.state().cones[record.agentId]?.name ?? record.agentId,\n expiresAt: record.expiresAt,\n gates: { ...record.gates },\n });\n let listed = JSON.stringify(proposalsOf(guests.reviews.value, record.id));\n const proposals = replicatedState<GuestProposal[]>(JSON.parse(listed) as GuestProposal[]);\n const offProposals = guests.reviews.subscribe((state) => {\n const next = JSON.stringify(proposalsOf(state, record.id));\n if (next === listed) return;\n listed = next;\n proposals.replace(context, JSON.parse(next) as GuestProposal[]);\n });\n return {\n value: {\n transcript,\n seat,\n proposals,\n propose: (request, using) => guests.propose(principal, request, using),\n guestOpen: () => managing(guests),\n guestRevoke: () => managing(guests),\n },\n dispose() {\n off();\n offProposals();\n },\n };\n };\n}\n\nexport function admitGuest(\n guests: Guests,\n endpoint: PortEndpoint,\n asked: { id: string; label: string },\n serverId: string,\n link: number,\n host: Pick<Host, 'accept'>\n): void {\n const port = new PortLink(endpoint);\n const guest = guests.admit(String(asked.id), String(asked.label));\n if (!guest) {\n port.close('this guest seat is not valid');\n return;\n }\n void port.ended.then(guests.joined(guest.id));\n port.greet(serverId);\n host.accept(port, { principal: { seat: 'guest', link, guest } satisfies AgentPrincipal });\n}\n"],
5
+ "mappings": "AACA,SAAS,WAAW,uBAAuB;AAK3C,SAA4B,gBAAgB;AAE5C,SAAS,sBAAsB,2BAA2B;AAE1D,SAAS,iBAAiB;AAEnB,SAAS,YAAY,OAAwC,MAA+B;AACjG,SAAO,OAAO,OAAO,OAAO,WAAW,CAAC,CAAC,EACtC,OAAO,CAAC,WAAW,OAAO,SAAS,IAAI,EACvC,KAAK,CAAC,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC,EACxB,IAAI,CAAC,YAAY;AAAA,IAChB,IAAI,OAAO;AAAA,IACX,MAAM,OAAO;AAAA,IACb,OAAO,OAAO;AAAA,IACd,QAAQ,OAAO;AAAA,IACf,WAAW,OAAO;AAAA,IAClB,WAAW,OAAO;AAAA,EACpB,EAAE;AACN;AAEA,MAAM,WAAW,CAAC,WAChB,QAAQ;AAAA,EACN,UAAU,OAAO,OAAO,EAAE,iBAAiB,SAAS,uBAAuB,mBAAmB;AAChG;AAEK,SAAS,YACd,QACA,QACA,OACA,SACA;AACA,SAAO,CAAC,cAAkF;AACxF,UAAM,SAAS,OAAO,QAAQ,SAAS;AACvC,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,OAAO,CAAC,QAAsD,MAAM,OAAO,OAAO;AACxF,QAAI,QAAQ,KAAK,MAAM,KAAK;AAC5B,UAAM,aAAa,gBAAkC,UAAU,OAAO,OAAO,EAAE,CAAC;AAChF,UAAM,MAAM,MAAM,UAAU,CAAC,QAAQ;AACnC,UAAI,KAAK,GAAG,MAAM,MAAO;AACzB,cAAQ,KAAK,GAAG;AAChB,iBAAW,QAAQ,SAAS,UAAU,OAAO,OAAO,EAAE,CAAC;AAAA,IACzD,CAAC;AACD,UAAM,OAAO,gBAA4B;AAAA,MACvC,IAAI,OAAO;AAAA,MACX,OAAO,OAAO;AAAA,MACd,SAAS,OAAO;AAAA,MAChB,MAAM,OAAO,MAAM,EAAE,MAAM,OAAO,OAAO,GAAG,QAAQ,OAAO;AAAA,MAC3D,WAAW,OAAO;AAAA,MAClB,OAAO,EAAE,GAAG,OAAO,MAAM;AAAA,IAC3B,CAAC;AACD,QAAI,SAAS,KAAK,UAAU,YAAY,OAAO,QAAQ,OAAO,OAAO,EAAE,CAAC;AACxE,UAAM,YAAY,gBAAiC,KAAK,MAAM,MAAM,CAAoB;AACxF,UAAM,eAAe,OAAO,QAAQ,UAAU,CAAC,UAAU;AACvD,YAAM,OAAO,KAAK,UAAU,YAAY,OAAO,OAAO,EAAE,CAAC;AACzD,UAAI,SAAS,OAAQ;AACrB,eAAS;AACT,gBAAU,QAAQ,SAAS,KAAK,MAAM,IAAI,CAAoB;AAAA,IAChE,CAAC;AACD,WAAO;AAAA,MACL,OAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,CAAC,SAAS,UAAU,OAAO,QAAQ,WAAW,SAAS,KAAK;AAAA,QACrE,WAAW,MAAM,SAAS,MAAM;AAAA,QAChC,aAAa,MAAM,SAAS,MAAM;AAAA,MACpC;AAAA,MACA,UAAU;AACR,YAAI;AACJ,qBAAa;AAAA,MACf;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,WACd,QACA,UACA,OACA,UACA,MACA,MACM;AACN,QAAM,OAAO,IAAI,SAAS,QAAQ;AAClC,QAAM,QAAQ,OAAO,MAAM,OAAO,MAAM,EAAE,GAAG,OAAO,MAAM,KAAK,CAAC;AAChE,MAAI,CAAC,OAAO;AACV,SAAK,MAAM,8BAA8B;AACzC;AAAA,EACF;AACA,OAAK,KAAK,MAAM,KAAK,OAAO,OAAO,MAAM,EAAE,CAAC;AAC5C,OAAK,MAAM,QAAQ;AACnB,OAAK,OAAO,MAAM,EAAE,WAAW,EAAE,MAAM,SAAS,MAAM,MAAM,EAA2B,CAAC;AAC1F;",
6
6
  "names": []
7
7
  }
package/dist/host.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  attachApprovals
10
10
  } from "./approvals/index.js";
11
11
  import { alwaysFor, attachApprovalPolicy } from "./approvals/policy.js";
12
+ import { createSudo, ownedSudo } from "./approvals/sudo.js";
12
13
  import { createAgentControl } from "./control.js";
13
14
  import { trackDeliveries } from "./deliveries.js";
14
15
  import { createGuests } from "./guests/index.js";
@@ -23,6 +24,7 @@ import {
23
24
  AgentPagesService,
24
25
  AgentRemoteService,
25
26
  AgentSettings,
27
+ AgentSudoService,
26
28
  AgentTranscript,
27
29
  CONTROL_METHODS,
28
30
  CONTROL_READS,
@@ -31,10 +33,12 @@ import {
31
33
  PAGE_METHODS,
32
34
  REMOTE_METHODS,
33
35
  REMOTE_READS,
34
- SETTINGS_METHODS
36
+ SETTINGS_METHODS,
37
+ SUDO_METHODS
35
38
  } from "./services.js";
36
39
  import {
37
40
  AGENT_HOST,
41
+ deviceOf,
38
42
  guestOf,
39
43
  principalKey,
40
44
  readable,
@@ -94,6 +98,17 @@ function gatedControls(control, shown, views, labels) {
94
98
  const viewed = { ...control, image: gated(visible), toolLabel: watchLabel(labels) };
95
99
  return { trusted, viewed };
96
100
  }
101
+ function approving(agent, policy, judge, context) {
102
+ return attachApprovals(
103
+ {
104
+ harness: agent.harness,
105
+ guestAlways: alwaysFor(policy),
106
+ sudoApprover: () => policy.current().sudo.approver,
107
+ ...judge ? { judge } : {}
108
+ },
109
+ context
110
+ );
111
+ }
97
112
  const noRevision = () => replicatedState({ revision: 0 });
98
113
  async function hostAgent(agent, options = {}) {
99
114
  const context = options.context ?? BACKGROUND_CONTEXT;
@@ -113,14 +128,7 @@ async function hostAgent(agent, options = {}) {
113
128
  const pages = options.pages ?? createPages();
114
129
  const deliveries = trackDeliveries(agent.harness, state, context);
115
130
  const policy = await attachApprovalPolicy(agent.harness, context);
116
- const approvals = options.approvals ?? await attachApprovals(
117
- {
118
- harness: agent.harness,
119
- guestAlways: alwaysFor(policy),
120
- ...options.judge ? { judge: options.judge } : {}
121
- },
122
- context
123
- );
131
+ const approvals = options.approvals ?? await approving(agent, policy, options.judge, context);
124
132
  const monitor = createMonitor({
125
133
  ...options.monitor,
126
134
  harness: agent.harness,
@@ -133,6 +141,7 @@ async function hostAgent(agent, options = {}) {
133
141
  agents: agent.agents,
134
142
  approvals,
135
143
  policy,
144
+ reviews: agent.reviews,
136
145
  context
137
146
  });
138
147
  const control = {
@@ -188,6 +197,11 @@ async function hostAgent(agent, options = {}) {
188
197
  approvals: approvals.state,
189
198
  approvalPolicy: policy.state
190
199
  };
200
+ const sudo = createSudo({
201
+ approvals,
202
+ agents: () => agent.agents.state(),
203
+ ...options.licks ? { licks: options.licks.licks } : {}
204
+ });
191
205
  const settings = options.settings ?? noSettings;
192
206
  const gates = gatedControls(control, slims.shown, shownViews, transcriptService.labels);
193
207
  const { trusted, viewed } = gates;
@@ -196,10 +210,11 @@ async function hostAgent(agent, options = {}) {
196
210
  viewed,
197
211
  transcript: transcriptService,
198
212
  viewMonitor: monitor.viewState,
213
+ sudo,
199
214
  settings,
200
215
  remote,
201
216
  pages,
202
- guest: guestOpener(guests, agent.agents, mounted.views, context, approvals.state),
217
+ guest: guestOpener(guests, agent.agents, mounted.views, context),
203
218
  decide: (by) => (id, decision, reason, using) => approvals.decide(id, decision, reason, by, using)
204
219
  });
205
220
  const offGuests = guests.onEnd((id) => {
@@ -223,7 +238,10 @@ async function hostAgent(agent, options = {}) {
223
238
  links += 1;
224
239
  const link = new PortLink(endpoint);
225
240
  link.greet(serverId);
226
- host.accept(link, { principal: { seat, link: links } });
241
+ const device = connectOptions?.device ? { device: connectOptions.device } : {};
242
+ host.accept(link, {
243
+ principal: { seat, link: links, ...device }
244
+ });
227
245
  },
228
246
  async close() {
229
247
  offGuests();
@@ -233,6 +251,7 @@ async function hostAgent(agent, options = {}) {
233
251
  await monitor.dispose();
234
252
  await remote.dispose();
235
253
  deliveries.dispose();
254
+ sudo.close();
236
255
  approvals.close();
237
256
  transcript.dispose();
238
257
  mounted.dispose();
@@ -285,9 +304,12 @@ function lazily(state) {
285
304
  events: (listener) => readable(state()).events(listener)
286
305
  };
287
306
  }
288
- const seated = (serving, seat) => ({
307
+ const seated = (serving, seat, device) => ({
289
308
  ...seat === "view" ? serving.viewed : serving.trusted,
290
- decide: serving.decide(seat === "owner" ? "owner" : "follower")
309
+ decide: serving.decide({
310
+ by: seat === "owner" ? "owner" : "follower",
311
+ ...device ? { device } : {}
312
+ })
291
313
  });
292
314
  const VIEW_METHODS = [...CONTROL_READS, ...OWNER_DEVICE_METHODS];
293
315
  function serveAgent(serving) {
@@ -295,7 +317,7 @@ function serveAgent(serving) {
295
317
  const viewing = { ...serving.transcript, monitor: serving.viewMonitor };
296
318
  host.serve(AgentControl, ((_input, principal) => {
297
319
  const seat = seatOf(principal);
298
- const control = seated(serving, seat);
320
+ const control = seated(serving, seat, deviceOf(principal));
299
321
  if (seat === "view") return { kind: "view", ...served(() => control, VIEW_METHODS) };
300
322
  return { kind: "control", ...served(() => control, CONTROL_METHODS) };
301
323
  }));
@@ -345,6 +367,18 @@ function serveAgent(serving) {
345
367
  };
346
368
  })
347
369
  );
370
+ host.serve(AgentSudoService, ((_input, principal, self) => {
371
+ const sudo = serving.sudo;
372
+ if (seatOf(principal) !== "owner" || !sudo) return { kind: "none" };
373
+ const held = self.resources({
374
+ mine: resource({ rev: 1, open: () => ownedSudo(sudo), close: (mine) => mine.release() })
375
+ });
376
+ return {
377
+ kind: "sudo",
378
+ approvals: readable(sudo.approvals),
379
+ ...served(() => held.mine.value, SUDO_METHODS)
380
+ };
381
+ }));
348
382
  return host;
349
383
  }
350
384
  export {