pi-roundtable-web 0.22.5 → 0.23.1
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 +53 -1
- package/README.md +444 -104
- package/assets/icons/192.png +0 -0
- package/assets/icons/512.png +0 -0
- package/assets/icons/apple-180.png +0 -0
- package/assets/icons/badge-96.png +0 -0
- package/assets/icons/mark-maskable.svg +1 -0
- package/assets/icons/mark.svg +1 -0
- package/assets/icons/maskable-512.png +0 -0
- package/dist/chunk-character-creator-660d2t6p.js +1 -0
- package/dist/chunk-connectors-d19v8cjc.js +1 -0
- package/dist/chunk-conversations-4cmexmqq.js +1 -0
- package/dist/chunk-entry-8xpx35ef.js +1 -0
- package/dist/chunk-index-19g8jmr8.js +2 -0
- package/dist/chunk-index-362dfedj.js +1 -0
- package/dist/chunk-index-6g69tqt0.js +47 -0
- package/dist/chunk-index-89q2qdkj.css +1 -0
- package/dist/chunk-index-9tvf6es7.js +41 -0
- package/dist/chunk-index-p8h0e861.js +2 -0
- package/dist/chunk-index-xw5hhmfq.js +2 -0
- package/dist/chunk-notes-fefwds14.js +1 -0
- package/dist/chunk-overview-bxpwpmn6.js +1 -0
- package/dist/chunk-skills-pce3zgxy.js +1 -0
- package/dist/chunk-table-zmpbrfvz.js +1 -0
- package/dist/chunk-table2d-ac2gez71.js +1 -0
- package/dist/chunk-table3d-g258jemr.js +4229 -0
- package/dist/chunk-transcript-yhtjp3pd.js +1 -0
- package/dist/chunk-zh-tw-anqhqpaz.js +1 -0
- package/dist/index.html +6 -2
- package/dist/sw.js +1 -0
- package/package.json +8 -3
- package/src/api-types.ts +448 -1
- package/src/app/app.ts +351 -0
- package/src/app/card-log.ts +119 -0
- package/src/app/desk.ts +533 -0
- package/src/app/files.ts +262 -0
- package/src/app/frames.ts +132 -0
- package/src/app/multipart.ts +172 -0
- package/src/app/presence.ts +73 -0
- package/src/app/protocol.ts +144 -0
- package/src/app/room-log.ts +137 -0
- package/src/app/rooms.ts +189 -0
- package/src/app/routes.ts +434 -0
- package/src/app/socket.ts +406 -0
- package/src/app/sockets.ts +84 -0
- package/src/app/surface-threads.ts +92 -0
- package/src/app/surface.ts +384 -0
- package/src/app/threads.ts +99 -0
- package/src/app/uploads.ts +363 -0
- package/src/assets.ts +1 -3
- package/src/auth/authenticator.ts +167 -0
- package/src/auth/login-page.ts +44 -0
- package/src/auth/none.ts +38 -0
- package/src/auth/password.ts +250 -0
- package/src/auth/session-key.ts +65 -0
- package/src/cleanup-api.ts +1 -1
- package/src/cloudflare-access.ts +5 -2
- package/src/console-api.ts +13 -15
- package/src/console-server.ts +77 -49
- package/src/features.ts +6 -1
- package/src/http.ts +8 -0
- package/src/i18n/index.ts +13 -0
- package/src/i18n/locale.ts +11 -0
- package/src/i18n/zh-tw.ts +696 -0
- package/src/index.ts +1 -1
- package/src/options.ts +308 -12
- package/src/page-colors.ts +9 -0
- package/src/page-presentation.ts +17 -12
- package/src/push/api.ts +190 -0
- package/src/push/channel.ts +50 -0
- package/src/push/encrypt.ts +136 -0
- package/src/push/endpoint.ts +54 -0
- package/src/push/sender.ts +109 -0
- package/src/push/subscriptions.ts +140 -0
- package/src/push/test-fetch.ts +17 -0
- package/src/push/triggers.ts +155 -0
- package/src/push/vapid.ts +124 -0
- package/src/pwa.ts +130 -0
- package/src/request-body.ts +20 -0
- package/src/verifier.ts +7 -1
- package/src/web-plugin.ts +123 -11
- package/dist/chunk-82frzte9.js +0 -36
- package/dist/chunk-e8z9q554.css +0 -1
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentChange,
|
|
3
|
+
AgentDirectory,
|
|
4
|
+
AgentTeam,
|
|
5
|
+
AvatarStudio,
|
|
6
|
+
ConversationModes,
|
|
7
|
+
ConversationPort,
|
|
8
|
+
IdentityService,
|
|
9
|
+
Logger,
|
|
10
|
+
ModeChange,
|
|
11
|
+
ScheduleStore,
|
|
12
|
+
Speaker,
|
|
13
|
+
} from "pi-roundtable";
|
|
14
|
+
import { characterOf, PluginError } from "pi-roundtable";
|
|
15
|
+
import { CharacterError, resolveCharacter } from "pi-roundtable/characters";
|
|
16
|
+
import { AgentError } from "pi-roundtable/kit";
|
|
17
|
+
import type {
|
|
18
|
+
AgentPatch,
|
|
19
|
+
AgentProfileView,
|
|
20
|
+
ApiError,
|
|
21
|
+
CharacterRefusal,
|
|
22
|
+
CharacterRequest,
|
|
23
|
+
MessagesView,
|
|
24
|
+
ModelsView,
|
|
25
|
+
RoomsView,
|
|
26
|
+
SchedulesView,
|
|
27
|
+
ScheduleView,
|
|
28
|
+
SessionView,
|
|
29
|
+
} from "../api-types.ts";
|
|
30
|
+
import type { Authenticated } from "../auth/authenticator.ts";
|
|
31
|
+
import { json } from "../http.ts";
|
|
32
|
+
import { readLimited } from "../request-body.ts";
|
|
33
|
+
import type { CardLog } from "./card-log.ts";
|
|
34
|
+
import { type FileDir, fileResponse, isFileId } from "./files.ts";
|
|
35
|
+
import { PAGE_LIMIT, type RoomLog } from "./room-log.ts";
|
|
36
|
+
import type { Rooms } from "./rooms.ts";
|
|
37
|
+
import type { AppSurface } from "./surface.ts";
|
|
38
|
+
import { UploadRefusal, type Uploads } from "./uploads.ts";
|
|
39
|
+
|
|
40
|
+
/** The largest JSON body a write reads: 64 KiB. */
|
|
41
|
+
const MAX_BODY_BYTES = 64 * 1024;
|
|
42
|
+
/** The largest body of a character write: 4 KiB. */
|
|
43
|
+
const MAX_CHARACTER_BYTES = 4 * 1024;
|
|
44
|
+
|
|
45
|
+
export interface AppApiOptions {
|
|
46
|
+
surface: AppSurface;
|
|
47
|
+
rooms: Rooms;
|
|
48
|
+
log: RoomLog;
|
|
49
|
+
cards: CardLog;
|
|
50
|
+
uploads: Uploads;
|
|
51
|
+
/** Where stored files are read from. */
|
|
52
|
+
files: FileDir;
|
|
53
|
+
modes: Pick<ConversationModes, "set" | "peek">;
|
|
54
|
+
conversations: Pick<ConversationPort, "startFresh">;
|
|
55
|
+
identity: Pick<IdentityService, "tierOf">;
|
|
56
|
+
/** Present when the schedules pane is served. */
|
|
57
|
+
schedules?: Pick<ScheduleStore, "all" | "remove">;
|
|
58
|
+
directory: Pick<AgentDirectory, "agent">;
|
|
59
|
+
team: Pick<
|
|
60
|
+
AgentTeam,
|
|
61
|
+
"update" | "dress" | "usableModels" | "modelOf" | "defaultModel"
|
|
62
|
+
>;
|
|
63
|
+
avatars: Pick<AvatarStudio, "serve">;
|
|
64
|
+
/** The mount, which a room's agent picture hangs under. */
|
|
65
|
+
mount: string;
|
|
66
|
+
/** How the console signs the person in, as the session reports it. */
|
|
67
|
+
authMode: SessionView["auth"];
|
|
68
|
+
/** Called after a change the other pages should refetch. */
|
|
69
|
+
changed(): void;
|
|
70
|
+
logger: Logger;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class HttpError extends Error {
|
|
74
|
+
constructor(
|
|
75
|
+
readonly status: number,
|
|
76
|
+
message: string,
|
|
77
|
+
) {
|
|
78
|
+
super(message);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const bad = (message: string) => new HttpError(400, message);
|
|
83
|
+
const notFound = () =>
|
|
84
|
+
new HttpError(404, "There is no such room, agent or file.");
|
|
85
|
+
|
|
86
|
+
/** The parts of a path under the mount, each decoded; undefined for one that is not valid. */
|
|
87
|
+
export function partsOf(path: string): string[] | undefined {
|
|
88
|
+
try {
|
|
89
|
+
return path.split("/").filter(Boolean).map(decodeURIComponent);
|
|
90
|
+
} catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function bodyOf(
|
|
96
|
+
request: Request,
|
|
97
|
+
limit = MAX_BODY_BYTES,
|
|
98
|
+
): Promise<Record<string, unknown>> {
|
|
99
|
+
const raw = await readLimited(request, limit);
|
|
100
|
+
if (!raw) throw new HttpError(413, "The body is too large.");
|
|
101
|
+
let value: unknown;
|
|
102
|
+
try {
|
|
103
|
+
value = JSON.parse(raw.toString("utf8"));
|
|
104
|
+
} catch {
|
|
105
|
+
throw bad("The body is not JSON.");
|
|
106
|
+
}
|
|
107
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
108
|
+
throw bad("The body is not an object.");
|
|
109
|
+
return value as Record<string, unknown>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const optionalText = (
|
|
113
|
+
body: Record<string, unknown>,
|
|
114
|
+
key: string,
|
|
115
|
+
): string | undefined => {
|
|
116
|
+
const value = body[key];
|
|
117
|
+
if (value === undefined) return undefined;
|
|
118
|
+
if (typeof value !== "string") throw bad(`${key} must be text.`);
|
|
119
|
+
return value;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The owner app's REST under `<mount>/api/` and `<mount>/avatars/`. Every route is behind the
|
|
124
|
+
* authenticator, which also checks the `Origin` of a write; a write also reads the person's tier
|
|
125
|
+
* again. A room in a path is one of the app's rooms or the answer is 404, so no other surface's
|
|
126
|
+
* key is ever read or changed here.
|
|
127
|
+
*/
|
|
128
|
+
export class AppApi {
|
|
129
|
+
readonly #options: AppApiOptions;
|
|
130
|
+
|
|
131
|
+
constructor(options: AppApiOptions) {
|
|
132
|
+
this.#options = options;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Answers a path of the app's, or undefined for any other. `rest` is what follows the mount. */
|
|
136
|
+
async handle(
|
|
137
|
+
request: Request,
|
|
138
|
+
rest: string,
|
|
139
|
+
admission: Authenticated,
|
|
140
|
+
): Promise<Response | undefined> {
|
|
141
|
+
const parts = partsOf(rest);
|
|
142
|
+
if (!parts) return undefined;
|
|
143
|
+
const [first, second] = parts;
|
|
144
|
+
const mine =
|
|
145
|
+
(first === "avatars" && parts.length === 2) ||
|
|
146
|
+
(first === "api" &&
|
|
147
|
+
[
|
|
148
|
+
"session",
|
|
149
|
+
"rooms",
|
|
150
|
+
"uploads",
|
|
151
|
+
"files",
|
|
152
|
+
"schedules",
|
|
153
|
+
"agents",
|
|
154
|
+
"models",
|
|
155
|
+
].includes(second ?? ""));
|
|
156
|
+
if (!mine) return undefined;
|
|
157
|
+
try {
|
|
158
|
+
return await this.#route(request, parts, admission);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error instanceof HttpError || error instanceof UploadRefusal)
|
|
161
|
+
return json({ error: error.message } satisfies ApiError, error.status);
|
|
162
|
+
if (error instanceof CharacterError)
|
|
163
|
+
return json(
|
|
164
|
+
{
|
|
165
|
+
error: error.message,
|
|
166
|
+
field: error.field,
|
|
167
|
+
} satisfies CharacterRefusal,
|
|
168
|
+
400,
|
|
169
|
+
);
|
|
170
|
+
if (error instanceof AgentError || error instanceof PluginError)
|
|
171
|
+
return json({ error: error.message } satisfies ApiError, 400);
|
|
172
|
+
this.#options.logger.error({ err: error }, "app api failed");
|
|
173
|
+
return json(
|
|
174
|
+
{ error: "The app could not complete the request." } satisfies ApiError,
|
|
175
|
+
500,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The owner who writes: their tier read again, as a speaker for the calls that need one. */
|
|
181
|
+
async #owner(admission: Authenticated): Promise<Speaker> {
|
|
182
|
+
const { principal } = admission.visitor;
|
|
183
|
+
const tier = await this.#options.identity.tierOf(principal.id);
|
|
184
|
+
if (tier !== "owner")
|
|
185
|
+
throw new HttpError(403, "Only an owner may do this.");
|
|
186
|
+
return {
|
|
187
|
+
id: admission.actor
|
|
188
|
+
? `${admission.actor.provider}:${admission.actor.subject}`
|
|
189
|
+
: principal.id,
|
|
190
|
+
name: principal.displayName,
|
|
191
|
+
tier,
|
|
192
|
+
principalId: principal.id,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async #route(
|
|
197
|
+
request: Request,
|
|
198
|
+
parts: string[],
|
|
199
|
+
admission: Authenticated,
|
|
200
|
+
): Promise<Response> {
|
|
201
|
+
const { method } = request;
|
|
202
|
+
const [head, area, id, action] = parts;
|
|
203
|
+
if (head === "avatars") {
|
|
204
|
+
if (method !== "GET") throw new HttpError(405, "Method not allowed.");
|
|
205
|
+
return (
|
|
206
|
+
this.#options.avatars.serve(`/avatars/${parts[1]}`) ??
|
|
207
|
+
new Response("Not found", { status: 404 })
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (area === "session" && method === "GET") return this.#session(admission);
|
|
211
|
+
if (area === "models" && method === "GET")
|
|
212
|
+
return json({
|
|
213
|
+
models: await this.#options.team.usableModels(),
|
|
214
|
+
} satisfies ModelsView);
|
|
215
|
+
if (area === "rooms") return this.#rooms(request, parts, admission);
|
|
216
|
+
if (area === "uploads" && method === "POST" && !id) {
|
|
217
|
+
const owner = await this.#owner(admission);
|
|
218
|
+
return json(
|
|
219
|
+
await this.#options.uploads.receive(request, owner.principalId),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (area === "uploads" && method === "DELETE" && id && !action) {
|
|
223
|
+
const owner = await this.#owner(admission);
|
|
224
|
+
if (!this.#options.uploads.discard(id, owner.principalId))
|
|
225
|
+
throw notFound();
|
|
226
|
+
return json({ ok: true });
|
|
227
|
+
}
|
|
228
|
+
if (area === "files" && method === "GET" && id && !action)
|
|
229
|
+
return this.#file(id);
|
|
230
|
+
if (area === "schedules") return this.#schedules(method, id, admission);
|
|
231
|
+
if (area === "agents" && id)
|
|
232
|
+
return this.#agent(request, id, action, admission);
|
|
233
|
+
throw new HttpError(404, "There is no such API.");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#session(admission: Authenticated): Response {
|
|
237
|
+
const { principal } = admission.visitor;
|
|
238
|
+
return json({
|
|
239
|
+
principal: { id: principal.id, name: principal.displayName },
|
|
240
|
+
surface: this.#options.surface.surface,
|
|
241
|
+
auth: this.#options.authMode,
|
|
242
|
+
push: true,
|
|
243
|
+
...(admission.expiresAt
|
|
244
|
+
? { expiresAt: admission.expiresAt.toISOString() }
|
|
245
|
+
: {}),
|
|
246
|
+
} satisfies SessionView);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async #rooms(
|
|
250
|
+
request: Request,
|
|
251
|
+
parts: string[],
|
|
252
|
+
admission: Authenticated,
|
|
253
|
+
): Promise<Response> {
|
|
254
|
+
const { method } = request;
|
|
255
|
+
const [, , key, action] = parts;
|
|
256
|
+
const { rooms, log, cards, modes, conversations } = this.#options;
|
|
257
|
+
if (!key) {
|
|
258
|
+
if (method !== "GET") throw new HttpError(405, "Method not allowed.");
|
|
259
|
+
return json({ rooms: await rooms.list() } satisfies RoomsView);
|
|
260
|
+
}
|
|
261
|
+
const room = await rooms.resolve(key);
|
|
262
|
+
if (!room) throw notFound();
|
|
263
|
+
if (action === "messages" && method === "GET" && parts.length === 4) {
|
|
264
|
+
const params = new URL(request.url).searchParams;
|
|
265
|
+
const before = params.get("before");
|
|
266
|
+
const limit = params.get("limit");
|
|
267
|
+
const beforeId = before === null ? undefined : Number(before);
|
|
268
|
+
const count = limit === null ? PAGE_LIMIT : Number(limit);
|
|
269
|
+
if (
|
|
270
|
+
(beforeId !== undefined && !Number.isInteger(beforeId)) ||
|
|
271
|
+
!Number.isInteger(count) ||
|
|
272
|
+
count < 1
|
|
273
|
+
)
|
|
274
|
+
throw bad("before and limit must be whole numbers.");
|
|
275
|
+
const page = await log.page(room.key, beforeId, count);
|
|
276
|
+
// The cards stand among the messages by the time they opened; a page that has older
|
|
277
|
+
// messages leaves the cards before its first one to the older page.
|
|
278
|
+
const first = page.messages[0];
|
|
279
|
+
return json({
|
|
280
|
+
...page,
|
|
281
|
+
cards: await cards.during(room.key, {
|
|
282
|
+
...(page.more && first ? { from: first.at } : {}),
|
|
283
|
+
...(beforeId === undefined ? {} : { before: beforeId }),
|
|
284
|
+
}),
|
|
285
|
+
} satisfies MessagesView);
|
|
286
|
+
}
|
|
287
|
+
if (action === "mode" && method === "PUT" && parts.length === 4) {
|
|
288
|
+
// Only an agent's or group's room has modes; a thread runs no turn of the owner's.
|
|
289
|
+
if (room.readOnly) throw notFound();
|
|
290
|
+
const owner = await this.#owner(admission);
|
|
291
|
+
const body = await bodyOf(request);
|
|
292
|
+
const change: ModeChange = {};
|
|
293
|
+
for (const name of ["yolo", "longRunning"] as const) {
|
|
294
|
+
const value = body[name];
|
|
295
|
+
if (value === undefined) continue;
|
|
296
|
+
if (typeof value !== "boolean")
|
|
297
|
+
throw bad(`${name} must be true or false.`);
|
|
298
|
+
change[name] = value;
|
|
299
|
+
}
|
|
300
|
+
if (Object.keys(change).length === 0)
|
|
301
|
+
throw bad("Give yolo or longRunning.");
|
|
302
|
+
const mode = await modes.set(room.key, change, owner);
|
|
303
|
+
return json({ mode: { yolo: mode.yolo, longRunning: mode.longRunning } });
|
|
304
|
+
}
|
|
305
|
+
if (action === "start-over" && method === "POST" && parts.length === 4) {
|
|
306
|
+
if (room.readOnly) throw notFound();
|
|
307
|
+
await this.#owner(admission);
|
|
308
|
+
await conversations.startFresh(room.key);
|
|
309
|
+
return json({ ok: true });
|
|
310
|
+
}
|
|
311
|
+
throw new HttpError(404, "There is no such API.");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
#file(id: string): Response {
|
|
315
|
+
if (!isFileId(id)) throw notFound();
|
|
316
|
+
const meta = this.#options.files.meta(id);
|
|
317
|
+
const blob = this.#options.files.blob(id);
|
|
318
|
+
if (!meta || !blob) throw notFound();
|
|
319
|
+
return fileResponse(meta, blob);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async #schedules(
|
|
323
|
+
method: string,
|
|
324
|
+
id: string | undefined,
|
|
325
|
+
admission: Authenticated,
|
|
326
|
+
): Promise<Response> {
|
|
327
|
+
const { schedules, rooms } = this.#options;
|
|
328
|
+
if (!schedules) throw new HttpError(404, "This pane is not served.");
|
|
329
|
+
if (method === "GET" && !id) {
|
|
330
|
+
const all = await schedules.all();
|
|
331
|
+
const views: ScheduleView[] = [];
|
|
332
|
+
for (const schedule of all) {
|
|
333
|
+
const room = await rooms.resolve(schedule.channel);
|
|
334
|
+
views.push({
|
|
335
|
+
id: schedule.id,
|
|
336
|
+
...(room ? { room: room.key } : {}),
|
|
337
|
+
channel: schedule.channel,
|
|
338
|
+
label: room?.displayName ?? schedule.channel,
|
|
339
|
+
title: schedule.title,
|
|
340
|
+
prompt: schedule.prompt,
|
|
341
|
+
recurrence: schedule.recurrence,
|
|
342
|
+
nextRun: schedule.nextRun.toISOString(),
|
|
343
|
+
...(schedule.lastRun
|
|
344
|
+
? { lastRun: schedule.lastRun.toISOString() }
|
|
345
|
+
: {}),
|
|
346
|
+
...(schedule.lastStatus ? { lastStatus: schedule.lastStatus } : {}),
|
|
347
|
+
createdBy: schedule.createdByName,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return json({ schedules: views } satisfies SchedulesView);
|
|
351
|
+
}
|
|
352
|
+
if (method === "DELETE" && id) {
|
|
353
|
+
await this.#owner(admission);
|
|
354
|
+
const number = Number(id);
|
|
355
|
+
if (!Number.isInteger(number)) throw bad("The schedule id is a number.");
|
|
356
|
+
const removed = await schedules.remove(number);
|
|
357
|
+
if (!removed) throw new HttpError(404, "There is no such schedule.");
|
|
358
|
+
this.#options.changed();
|
|
359
|
+
return json({ ok: true });
|
|
360
|
+
}
|
|
361
|
+
throw new HttpError(405, "Method not allowed.");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async #agent(
|
|
365
|
+
request: Request,
|
|
366
|
+
name: string,
|
|
367
|
+
action: string | undefined,
|
|
368
|
+
admission: Authenticated,
|
|
369
|
+
): Promise<Response> {
|
|
370
|
+
const { directory, team } = this.#options;
|
|
371
|
+
const agent = directory.agent(name);
|
|
372
|
+
if (agent?.status !== "active") throw notFound();
|
|
373
|
+
if (!action && request.method === "GET") return json(this.#profile(name));
|
|
374
|
+
if (!action && request.method === "PATCH") {
|
|
375
|
+
await this.#owner(admission);
|
|
376
|
+
const body = await bodyOf(request);
|
|
377
|
+
const patch: AgentPatch = {};
|
|
378
|
+
for (const key of [
|
|
379
|
+
"displayName",
|
|
380
|
+
"prompt",
|
|
381
|
+
"model",
|
|
382
|
+
"thinking",
|
|
383
|
+
] as const) {
|
|
384
|
+
const value = optionalText(body, key);
|
|
385
|
+
if (value !== undefined) patch[key] = value;
|
|
386
|
+
}
|
|
387
|
+
const change: AgentChange = patch;
|
|
388
|
+
await team.update(name, change);
|
|
389
|
+
this.#options.changed();
|
|
390
|
+
return json(this.#profile(name));
|
|
391
|
+
}
|
|
392
|
+
if (action === "character" && request.method === "PUT") {
|
|
393
|
+
await this.#owner(admission);
|
|
394
|
+
const body = await bodyOf(request, MAX_CHARACTER_BYTES);
|
|
395
|
+
const { character } = body;
|
|
396
|
+
if (
|
|
397
|
+
typeof character !== "object" ||
|
|
398
|
+
character === null ||
|
|
399
|
+
Array.isArray(character)
|
|
400
|
+
)
|
|
401
|
+
throw bad("character must be an object.");
|
|
402
|
+
const asked: CharacterRequest = {
|
|
403
|
+
character: character as CharacterRequest["character"],
|
|
404
|
+
};
|
|
405
|
+
// `dress` words a refusal as an agent error; checking here first names the field.
|
|
406
|
+
resolveCharacter(asked.character, characterOf(agent));
|
|
407
|
+
await team.dress(name, asked.character);
|
|
408
|
+
this.#options.changed();
|
|
409
|
+
return json(this.#profile(name));
|
|
410
|
+
}
|
|
411
|
+
throw new HttpError(404, "There is no such API.");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
#profile(name: string): AgentProfileView {
|
|
415
|
+
const { directory, team, rooms, surface } = this.#options;
|
|
416
|
+
const agent = directory.agent(name);
|
|
417
|
+
if (!agent) throw notFound();
|
|
418
|
+
const effective = team.modelOf(name);
|
|
419
|
+
return {
|
|
420
|
+
name: agent.name,
|
|
421
|
+
displayName: agent.displayName,
|
|
422
|
+
prompt: agent.prompt,
|
|
423
|
+
...(agent.model ? { model: agent.model } : {}),
|
|
424
|
+
...(agent.thinking ? { thinking: agent.thinking } : {}),
|
|
425
|
+
effective: {
|
|
426
|
+
model: effective.model,
|
|
427
|
+
thinking: String(effective.thinking),
|
|
428
|
+
},
|
|
429
|
+
avatar: rooms.avatarOf(agent),
|
|
430
|
+
character: characterOf(agent),
|
|
431
|
+
room: `${surface.surface}:agent.${agent.name}`,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|