@centerforagenticai/pi-multi-account 0.1.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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +29 -0
  3. package/README.md +999 -0
  4. package/config/models/pi-multi-account.v1.json +32 -0
  5. package/config/subscription-plans.v1.json +122 -0
  6. package/package.json +76 -0
  7. package/packages/pi-anthropic-oauth/LICENSE +21 -0
  8. package/packages/pi-anthropic-oauth/package.json +54 -0
  9. package/packages/pi-anthropic-oauth/src/auth.ts +396 -0
  10. package/packages/pi-anthropic-oauth/src/context.ts +116 -0
  11. package/packages/pi-anthropic-oauth/src/convert.ts +303 -0
  12. package/packages/pi-anthropic-oauth/src/index.ts +37 -0
  13. package/packages/pi-anthropic-oauth/src/prompt.ts +137 -0
  14. package/packages/pi-anthropic-oauth/src/stream.ts +476 -0
  15. package/packages/pi-antigravity/LICENSE +21 -0
  16. package/packages/pi-antigravity/package.json +77 -0
  17. package/packages/pi-antigravity/src/auth/index.ts +14 -0
  18. package/packages/pi-antigravity/src/auth/oauth.ts +442 -0
  19. package/packages/pi-antigravity/src/client/client.ts +561 -0
  20. package/packages/pi-antigravity/src/client/index.ts +1 -0
  21. package/packages/pi-antigravity/src/context.ts +110 -0
  22. package/packages/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
  23. package/packages/pi-antigravity/src/diagnostics/index.ts +1 -0
  24. package/packages/pi-antigravity/src/image/image.ts +336 -0
  25. package/packages/pi-antigravity/src/image/index.ts +1 -0
  26. package/packages/pi-antigravity/src/index.ts +280 -0
  27. package/packages/pi-antigravity/src/models/discovery.ts +154 -0
  28. package/packages/pi-antigravity/src/models/grouping.ts +424 -0
  29. package/packages/pi-antigravity/src/models/index.ts +3 -0
  30. package/packages/pi-antigravity/src/models/models.ts +500 -0
  31. package/packages/pi-antigravity/src/stream/index.ts +1 -0
  32. package/packages/pi-antigravity/src/stream/stream.ts +1478 -0
  33. package/packages/pi-antigravity/src/types/enums.ts +42 -0
  34. package/packages/pi-antigravity/src/types/index.ts +2 -0
  35. package/packages/pi-antigravity/src/types/types.ts +292 -0
  36. package/packages/pi-antigravity/src/usage/index.ts +1 -0
  37. package/packages/pi-antigravity/src/usage/usage.ts +416 -0
  38. package/packages/pi-antigravity/src/utils/http.ts +91 -0
  39. package/packages/pi-antigravity/src/utils/index.ts +3 -0
  40. package/packages/pi-antigravity/src/utils/security.ts +73 -0
  41. package/packages/pi-antigravity/src/utils/util.ts +132 -0
  42. package/scripts/multi-account.mjs +44 -0
  43. package/src/account-labels.ts +223 -0
  44. package/src/account-plan-assignment.ts +340 -0
  45. package/src/account-rate-history.ts +372 -0
  46. package/src/anthropic-adaptive-stream.ts +531 -0
  47. package/src/anthropic-alias-stream.ts +140 -0
  48. package/src/anthropic-context-compat.ts +80 -0
  49. package/src/api-pricing.ts +579 -0
  50. package/src/bounded-file-lines.ts +97 -0
  51. package/src/catalog-rebinding.ts +177 -0
  52. package/src/catalog-registration-probe.ts +111 -0
  53. package/src/codex-adapter.ts +345 -0
  54. package/src/codex-model-defaults.ts +785 -0
  55. package/src/command-completions.ts +404 -0
  56. package/src/commands.ts +2000 -0
  57. package/src/compaction.ts +14 -0
  58. package/src/config.ts +1317 -0
  59. package/src/continuation.ts +569 -0
  60. package/src/cooldowns.ts +110 -0
  61. package/src/cost-digest-store.ts +332 -0
  62. package/src/cost-digest.ts +1044 -0
  63. package/src/cost-history.ts +251 -0
  64. package/src/cost-period-closer.ts +160 -0
  65. package/src/cost-report-json.ts +318 -0
  66. package/src/cost-report-reader.ts +368 -0
  67. package/src/cost-report-render.ts +207 -0
  68. package/src/cost-report.ts +1104 -0
  69. package/src/coverage-attestation.ts +397 -0
  70. package/src/credential-lifecycle.ts +169 -0
  71. package/src/credential-refresh.ts +248 -0
  72. package/src/declaration-notice-marker.ts +238 -0
  73. package/src/diagnostic-store.ts +276 -0
  74. package/src/diagnostics.ts +309 -0
  75. package/src/discovery.ts +471 -0
  76. package/src/duration.ts +13 -0
  77. package/src/error-classification.ts +256 -0
  78. package/src/fuzzy.ts +15 -0
  79. package/src/group-policy.ts +81 -0
  80. package/src/history-store.ts +897 -0
  81. package/src/index.ts +5572 -0
  82. package/src/lifecycle.ts +378 -0
  83. package/src/logical-dispatch.ts +279 -0
  84. package/src/logical-model-selector.ts +254 -0
  85. package/src/logical-model-switcher.ts +430 -0
  86. package/src/logical-provider-attribution.ts +544 -0
  87. package/src/logical-provider.ts +1237 -0
  88. package/src/logical-route-indicator.ts +215 -0
  89. package/src/machine-lease.ts +445 -0
  90. package/src/model-support.ts +66 -0
  91. package/src/models-declaration.ts +1091 -0
  92. package/src/openai-adapter.ts +117 -0
  93. package/src/openrouter-budget.ts +304 -0
  94. package/src/openrouter-fallback.ts +146 -0
  95. package/src/period-boundaries.ts +376 -0
  96. package/src/pi-anthropic-oauth.d.ts +6 -0
  97. package/src/preflight.ts +253 -0
  98. package/src/pricing-cache.ts +235 -0
  99. package/src/project-identity.ts +100 -0
  100. package/src/provider-registration.ts +942 -0
  101. package/src/rate-formula.ts +163 -0
  102. package/src/recovery-engine.ts +853 -0
  103. package/src/recovery-output.ts +837 -0
  104. package/src/recovery-plan.ts +239 -0
  105. package/src/report-range.ts +203 -0
  106. package/src/route-resolver.ts +789 -0
  107. package/src/routing-config-transaction.ts +232 -0
  108. package/src/routing.ts +1163 -0
  109. package/src/runtime-state.ts +630 -0
  110. package/src/session-account-groups.ts +284 -0
  111. package/src/session-restore.ts +287 -0
  112. package/src/shared-usage.ts +1392 -0
  113. package/src/standalone-cli.ts +720 -0
  114. package/src/status-view.ts +578 -0
  115. package/src/subscription-plan-catalog.ts +346 -0
  116. package/src/tier-model-resolver.ts +46 -0
  117. package/src/upstream-anthropic.ts +315 -0
  118. package/src/upstream-antigravity.ts +327 -0
  119. package/src/usage-fetch.ts +1634 -0
  120. package/src/usage.ts +1026 -0
  121. package/src/vendor.ts +87 -0
  122. package/src/warmer.ts +231 -0
  123. package/src/watchdog.ts +219 -0
  124. package/src/window-history.ts +270 -0
@@ -0,0 +1,396 @@
1
+ import { createServer } from "node:http";
2
+ import type {
3
+ OAuthCredentials,
4
+ OAuthLoginCallbacks,
5
+ } from "@earendil-works/pi-ai";
6
+
7
+ const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
8
+ const AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
9
+ const TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
10
+ const REDIRECT_URI = "https://platform.claude.com/oauth/code/callback";
11
+ const SCOPES = [
12
+ "org:create_api_key",
13
+ "user:profile",
14
+ "user:inference",
15
+ "user:sessions:claude_code",
16
+ "user:mcp_servers",
17
+ "user:file_upload",
18
+ ].join(" ");
19
+ const CLAUDE_CODE_VERSION_ENV = "PI_ANTHROPIC_OAUTH_CLAUDE_CODE_VERSION";
20
+ const DEFAULT_CLAUDE_CODE_VERSION = "2.1.280";
21
+ const USER_AGENT = makeClaudeCodeUserAgent();
22
+ const CALLBACK_PORT = 53692;
23
+ const CALLBACK_HOST = "127.0.0.1";
24
+ const LOCAL_CALLBACK_TIMEOUT = 5 * 60 * 1000;
25
+ const MAX_TOKEN_RETRIES = 2;
26
+ const INITIAL_RETRY_DELAY_MS = 5000;
27
+
28
+ export { USER_AGENT };
29
+
30
+ export function makeClaudeCodeUserAgent(
31
+ env: NodeJS.ProcessEnv = process.env,
32
+ ): string {
33
+ const version =
34
+ env[CLAUDE_CODE_VERSION_ENV]?.trim() || DEFAULT_CLAUDE_CODE_VERSION;
35
+ return `claude-code/${version}`;
36
+ }
37
+
38
+ type ParsedAuthInput = { code: string; state: string };
39
+ type LocalAuthorization = {
40
+ redirectUri: string;
41
+ waitForCallback: () => Promise<string | null>;
42
+ cancel: () => void;
43
+ };
44
+
45
+ export function isClaudeOAuthAccessToken(apiKey: string): boolean {
46
+ return apiKey.includes("sk-ant-oat");
47
+ }
48
+
49
+ export async function loginAnthropic(
50
+ callbacks: OAuthLoginCallbacks,
51
+ ): Promise<OAuthCredentials> {
52
+ const { verifier, challenge } = await generatePKCE();
53
+ const state = crypto.randomUUID().replace(/-/g, "");
54
+
55
+ let authInput: string | null = null;
56
+ let redirectUri = REDIRECT_URI;
57
+
58
+ try {
59
+ const localAuthorization = await createLocalAuthorization(state);
60
+ redirectUri = localAuthorization.redirectUri;
61
+
62
+ callbacks.onAuth({
63
+ url: makeAuthorizeUrl(challenge, state, redirectUri),
64
+ instructions:
65
+ "Complete login in your browser. If the browser is on another machine, paste the final redirect URL here.",
66
+ });
67
+
68
+ if (callbacks.onManualCodeInput) {
69
+ let manualInput: string | undefined;
70
+ let manualError: Error | undefined;
71
+ const manualPromise = callbacks
72
+ .onManualCodeInput()
73
+ .then((input) => {
74
+ manualInput = input;
75
+ localAuthorization.cancel();
76
+ })
77
+ .catch((err) => {
78
+ manualError =
79
+ err instanceof Error ? err : new Error(String(err));
80
+ localAuthorization.cancel();
81
+ });
82
+
83
+ const callbackResult = await localAuthorization.waitForCallback();
84
+
85
+ if (manualError) throw manualError;
86
+
87
+ if (callbackResult) {
88
+ authInput = callbackResult;
89
+ } else if (manualInput) {
90
+ authInput = manualInput;
91
+ }
92
+
93
+ if (!authInput) {
94
+ await manualPromise;
95
+ if (manualError) throw manualError;
96
+ if (manualInput) authInput = manualInput;
97
+ }
98
+ } else {
99
+ authInput = await localAuthorization.waitForCallback();
100
+ }
101
+ } catch {
102
+ }
103
+
104
+ if (!authInput) {
105
+ redirectUri = REDIRECT_URI;
106
+ callbacks.onAuth({
107
+ url: makeAuthorizeUrl(challenge, state, redirectUri),
108
+ instructions:
109
+ "Sign in with Claude, then paste the full callback URL or the code#state value.",
110
+ });
111
+ authInput = await callbacks.onPrompt({
112
+ message: "Paste the callback URL or code#state:",
113
+ });
114
+ }
115
+
116
+ const parsed = parseAuthInput(authInput);
117
+ if (!parsed) throw new Error("Could not parse authorization callback input.");
118
+ if (parsed.state !== state) throw new Error("OAuth state mismatch.");
119
+
120
+ const tokenResponse = await fetchWithRetry(
121
+ TOKEN_URL,
122
+ {
123
+ method: "POST",
124
+ headers: makeTokenHeaders(),
125
+ body: JSON.stringify({
126
+ grant_type: "authorization_code",
127
+ client_id: CLIENT_ID,
128
+ code: parsed.code,
129
+ state: parsed.state,
130
+ redirect_uri: redirectUri,
131
+ code_verifier: verifier,
132
+ }),
133
+ signal: callbacks.signal,
134
+ },
135
+ "Token exchange",
136
+ );
137
+
138
+ const data = (await tokenResponse.json()) as {
139
+ access_token: string;
140
+ refresh_token: string;
141
+ expires_in: number;
142
+ };
143
+
144
+ return {
145
+ access: data.access_token,
146
+ refresh: data.refresh_token,
147
+ expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
148
+ };
149
+ }
150
+
151
+ export async function refreshAnthropicToken(
152
+ credentials: OAuthCredentials,
153
+ ): Promise<OAuthCredentials> {
154
+ let response: Response;
155
+ try {
156
+ response = await fetchWithRetry(
157
+ TOKEN_URL,
158
+ {
159
+ method: "POST",
160
+ headers: makeTokenHeaders(),
161
+ body: JSON.stringify({
162
+ grant_type: "refresh_token",
163
+ client_id: CLIENT_ID,
164
+ refresh_token: credentials.refresh,
165
+ }),
166
+ },
167
+ "Token refresh",
168
+ );
169
+ } catch {
170
+ if (credentials.expires > Date.now()) {
171
+ return { ...credentials, expires: Date.now() + 30_000 };
172
+ }
173
+ throw new Error("Token refresh failed and token has expired.");
174
+ }
175
+
176
+ const data = (await response.json()) as {
177
+ access_token: string;
178
+ refresh_token: string;
179
+ expires_in: number;
180
+ };
181
+
182
+ return {
183
+ access: data.access_token,
184
+ refresh: data.refresh_token || credentials.refresh,
185
+ expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
186
+ };
187
+ }
188
+
189
+ function makeAuthorizeUrl(
190
+ challenge: string,
191
+ state: string,
192
+ redirectUri: string,
193
+ ): string {
194
+ const authParams = new URLSearchParams({
195
+ code: "true",
196
+ client_id: CLIENT_ID,
197
+ response_type: "code",
198
+ redirect_uri: redirectUri,
199
+ scope: SCOPES,
200
+ code_challenge: challenge,
201
+ code_challenge_method: "S256",
202
+ state,
203
+ });
204
+
205
+ return `${AUTHORIZE_URL}?${authParams.toString()}`;
206
+ }
207
+
208
+ async function fetchWithRetry(
209
+ url: string,
210
+ init: RequestInit,
211
+ label: string,
212
+ ): Promise<Response> {
213
+ let lastError: Error | undefined;
214
+
215
+ for (let attempt = 0; attempt <= MAX_TOKEN_RETRIES; attempt++) {
216
+ const response = await fetch(url, init);
217
+
218
+ if (response.ok) return response;
219
+
220
+ const bodyText = await response.text();
221
+
222
+ const shouldRetry = response.headers.get("x-should-retry");
223
+ if (shouldRetry === "false") {
224
+ throw new Error(`${label} failed: ${response.status} ${bodyText}`);
225
+ }
226
+
227
+ if (
228
+ attempt < MAX_TOKEN_RETRIES &&
229
+ (response.status === 429 || response.status >= 500)
230
+ ) {
231
+ const retryAfter = response.headers.get("retry-after");
232
+ const delayMs = retryAfter
233
+ ? Math.min(Number(retryAfter) * 1000, 30_000)
234
+ : INITIAL_RETRY_DELAY_MS * 2 ** attempt;
235
+
236
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
237
+ lastError = new Error(
238
+ `${label} failed: ${response.status} ${bodyText}`,
239
+ );
240
+ continue;
241
+ }
242
+
243
+ throw new Error(`${label} failed: ${response.status} ${bodyText}`);
244
+ }
245
+
246
+ throw lastError ?? new Error(`${label} failed after retries`);
247
+ }
248
+
249
+ /**
250
+ * Headers for the OAuth token endpoint (authorization-code exchange and
251
+ * refresh). Deliberately sets no `user-agent` of its own.
252
+ *
253
+ * `USER_AGENT` identifies the API client and belongs on inference requests
254
+ * only (`src/stream.ts`). Setting it here made Anthropic answer the token
255
+ * endpoint with 429 `rate_limit_error`, which failed both login and every
256
+ * credential refresh. Authentication is not inference: do not re-couple them.
257
+ *
258
+ * This does not mean the request reaches the wire with no user agent at all.
259
+ * Node's `fetch` supplies its own default when the caller sets none, so the
260
+ * endpoint still sees something generic. The point is that it must not see a
261
+ * `claude-code/<version>` string identifying the inference client.
262
+ */
263
+ function makeTokenHeaders(): HeadersInit {
264
+ return {
265
+ "Content-Type": "application/json",
266
+ };
267
+ }
268
+
269
+ async function createLocalAuthorization(
270
+ state: string,
271
+ ): Promise<LocalAuthorization> {
272
+ const server = createServer();
273
+
274
+ return new Promise((resolve, reject) => {
275
+ let done = false;
276
+ let timer: ReturnType<typeof setTimeout> | undefined;
277
+ let complete!: (value: string | null) => void;
278
+ const wait = new Promise<string | null>((innerResolve) => {
279
+ complete = innerResolve;
280
+ });
281
+
282
+ const finish = (value: string | null) => {
283
+ if (done) return;
284
+ done = true;
285
+ if (timer) clearTimeout(timer);
286
+ complete(value);
287
+ if (server.listening) {
288
+ server.closeAllConnections();
289
+ server.close();
290
+ }
291
+ };
292
+
293
+ server.on("request", (req, res) => {
294
+ const url = new URL(
295
+ req.url ?? "/",
296
+ `http://${req.headers.host ?? "localhost"}`,
297
+ );
298
+
299
+ if (url.pathname !== "/callback") {
300
+ res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
301
+ res.end("Not found");
302
+ return;
303
+ }
304
+
305
+ const code = url.searchParams.get("code");
306
+ const gotState = url.searchParams.get("state");
307
+ if (!code || !gotState) {
308
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
309
+ res.end("Missing code or state");
310
+ return;
311
+ }
312
+
313
+ if (gotState !== state) {
314
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
315
+ res.end("Invalid state");
316
+ finish(null);
317
+ return;
318
+ }
319
+
320
+ res.writeHead(200, {
321
+ "Content-Type": "text/html; charset=utf-8",
322
+ Connection: "close",
323
+ });
324
+ res.end(makeCallbackPage());
325
+ finish(url.toString());
326
+ });
327
+
328
+ server.once("error", reject);
329
+
330
+ server.listen(CALLBACK_PORT, CALLBACK_HOST, () => {
331
+ timer = setTimeout(() => finish(null), LOCAL_CALLBACK_TIMEOUT);
332
+ resolve({
333
+ redirectUri: `http://localhost:${CALLBACK_PORT}/callback`,
334
+ waitForCallback: () => wait,
335
+ cancel: () => finish(null),
336
+ });
337
+ });
338
+ });
339
+ }
340
+
341
+ function makeCallbackPage(): string {
342
+ return `<!doctype html>
343
+ <html>
344
+ <head><meta charset="utf-8" /><title>Authorization complete</title></head>
345
+ <body>
346
+ <h1>Authorization complete</h1>
347
+ <p>You can close this window and return to Pi.</p>
348
+ </body>
349
+ </html>`;
350
+ }
351
+
352
+ async function generatePKCE(): Promise<{
353
+ verifier: string;
354
+ challenge: string;
355
+ }> {
356
+ const bytes = new Uint8Array(32);
357
+ crypto.getRandomValues(bytes);
358
+ const verifier = toBase64Url(bytes);
359
+ const digest = await crypto.subtle.digest(
360
+ "SHA-256",
361
+ new TextEncoder().encode(verifier),
362
+ );
363
+ return {
364
+ verifier,
365
+ challenge: toBase64Url(new Uint8Array(digest)),
366
+ };
367
+ }
368
+
369
+ function toBase64Url(bytes: Uint8Array): string {
370
+ return Buffer.from(bytes)
371
+ .toString("base64")
372
+ .replace(/\+/g, "-")
373
+ .replace(/\//g, "_")
374
+ .replace(/=+$/g, "");
375
+ }
376
+
377
+ function parseAuthInput(input: string): ParsedAuthInput | null {
378
+ const text = input.trim();
379
+
380
+ try {
381
+ const url = new URL(text);
382
+ const code = url.searchParams.get("code");
383
+ const state = url.searchParams.get("state");
384
+ if (code && state) return { code, state };
385
+ } catch {}
386
+
387
+ const split = text.split("#");
388
+ if (split.length === 2 && split[0] && split[1]) {
389
+ return { code: split[0], state: split[1] };
390
+ }
391
+
392
+ const params = new URLSearchParams(text);
393
+ const code = params.get("code");
394
+ const state = params.get("state");
395
+ return code && state ? { code, state } : null;
396
+ }
@@ -0,0 +1,116 @@
1
+ import type { Context, Message, Tool } from "@earendil-works/pi-ai";
2
+
3
+ interface ProjectedContext {
4
+ messages: Message[];
5
+ systemPrompt: string;
6
+ tools: Tool[];
7
+ }
8
+
9
+ type UnknownRecord = Record<PropertyKey, unknown>;
10
+
11
+ function isRecord(value: unknown): value is UnknownRecord {
12
+ return typeof value === "object" && value !== null;
13
+ }
14
+
15
+ function isSystemMessage(value: unknown): value is UnknownRecord & {
16
+ role: "system";
17
+ } {
18
+ return isRecord(value) && value.role === "system";
19
+ }
20
+
21
+ function isConversationMessage(value: unknown): value is Message {
22
+ return (
23
+ isRecord(value) &&
24
+ (value.role === "user" ||
25
+ value.role === "assistant" ||
26
+ value.role === "toolResult")
27
+ );
28
+ }
29
+
30
+ function isTool(value: unknown): value is Tool {
31
+ return (
32
+ isRecord(value) &&
33
+ typeof value.name === "string" &&
34
+ typeof value.description === "string" &&
35
+ isRecord(value.parameters)
36
+ );
37
+ }
38
+
39
+ function contentText(content: unknown): string {
40
+ if (typeof content === "string") return content;
41
+ if (!Array.isArray(content)) return "";
42
+
43
+ const texts: string[] = [];
44
+ for (const block of content) {
45
+ if (
46
+ isRecord(block) &&
47
+ block.type === "text" &&
48
+ typeof block.text === "string"
49
+ ) {
50
+ texts.push(block.text);
51
+ }
52
+ }
53
+ return texts.join("\n");
54
+ }
55
+
56
+ /**
57
+ * Project both legacy Context fields and current transcript system messages into
58
+ * the single prompt/tool snapshot required by the OAuth Anthropic endpoint.
59
+ *
60
+ * This mirrors Pi's transcript replay helpers without importing their newer-only
61
+ * module path, preserving this package's Pi 0.80.8 peer compatibility.
62
+ */
63
+ export function projectContext(context: Context): ProjectedContext {
64
+ const promptParts = context.systemPrompt ? [context.systemPrompt] : [];
65
+ const sections = new Map<string, string>();
66
+ const tools = new Map<string, Tool>();
67
+ const messages: Message[] = [];
68
+
69
+ for (const tool of context.tools ?? []) tools.set(tool.name, tool);
70
+
71
+ const sourceMessages: readonly unknown[] = context.messages;
72
+ for (const message of sourceMessages) {
73
+ if (!isSystemMessage(message)) {
74
+ if (isConversationMessage(message)) messages.push(message);
75
+ continue;
76
+ }
77
+
78
+ if (message.replace === true) {
79
+ promptParts.length = 0;
80
+ sections.clear();
81
+ tools.clear();
82
+ }
83
+
84
+ const text = contentText(message.content);
85
+ if (text.length > 0) promptParts.push(text);
86
+
87
+ if (isRecord(message.sections)) {
88
+ for (const [name, value] of Object.entries(message.sections)) {
89
+ if (value === null) sections.delete(name);
90
+ else if (typeof value === "string") sections.set(name, value);
91
+ }
92
+ }
93
+
94
+ if (Array.isArray(message.toolsRemoved)) {
95
+ for (const reference of message.toolsRemoved) {
96
+ if (isRecord(reference) && typeof reference.name === "string") {
97
+ tools.delete(reference.name);
98
+ }
99
+ }
100
+ }
101
+
102
+ if (Array.isArray(message.toolsAdded)) {
103
+ for (const tool of message.toolsAdded) {
104
+ if (isTool(tool)) tools.set(tool.name, tool);
105
+ }
106
+ }
107
+ }
108
+
109
+ return {
110
+ messages,
111
+ systemPrompt: [...promptParts, ...sections.values()]
112
+ .filter((part) => part.length > 0)
113
+ .join("\n\n"),
114
+ tools: [...tools.values()],
115
+ };
116
+ }