@pylonsync/functions 0.3.197 → 0.3.200
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/package.json +1 -1
- package/src/runtime.ts +106 -5
- package/src/types.ts +174 -1
package/package.json
CHANGED
package/src/runtime.ts
CHANGED
|
@@ -21,6 +21,10 @@ import type {
|
|
|
21
21
|
EmailSender,
|
|
22
22
|
Stream,
|
|
23
23
|
Scheduler,
|
|
24
|
+
Llm,
|
|
25
|
+
LlmCompleteRequest,
|
|
26
|
+
LlmCompleteResponse,
|
|
27
|
+
Connections,
|
|
24
28
|
QueryCtx,
|
|
25
29
|
MutationCtx,
|
|
26
30
|
ActionCtx,
|
|
@@ -529,12 +533,103 @@ function buildEmail(callId: string): EmailSender {
|
|
|
529
533
|
};
|
|
530
534
|
}
|
|
531
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Build the LLM client that round-trips through the host runtime.
|
|
538
|
+
*
|
|
539
|
+
* Each call emits an `llm_complete` protocol message; the runtime
|
|
540
|
+
* forwards to the configured provider (PYLON_LLM_PROVIDER) and replies
|
|
541
|
+
* with the parsed response. The host enforces model-allowlist gating
|
|
542
|
+
* for non-admin callers — that's why the API key never leaves the
|
|
543
|
+
* server process.
|
|
544
|
+
*
|
|
545
|
+
* Errors carry an `err.code` so handlers can branch on `LLM_NOT_CONFIGURED`
|
|
546
|
+
* vs `PROVIDER_HTTP_429` vs `MODEL_NOT_ALLOWED` without parsing message
|
|
547
|
+
* strings.
|
|
548
|
+
*/
|
|
549
|
+
function buildLlm(callId: string): Llm {
|
|
550
|
+
return {
|
|
551
|
+
async complete(request: LlmCompleteRequest): Promise<LlmCompleteResponse> {
|
|
552
|
+
return (await rpc(callId, {
|
|
553
|
+
type: "llm_complete",
|
|
554
|
+
request,
|
|
555
|
+
})) as LlmCompleteResponse;
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Build the connection registry that round-trips through the host.
|
|
562
|
+
* Each method emits a `{type:"connection", op:"..."}` message;
|
|
563
|
+
* the host's ConnectionManager runs the actual OAuth flow + DB
|
|
564
|
+
* read/write and returns the typed reply.
|
|
565
|
+
*/
|
|
566
|
+
function buildConnections(callId: string): Connections {
|
|
567
|
+
return {
|
|
568
|
+
async authorizeUrl(name, opts) {
|
|
569
|
+
const data = (await rpc(callId, {
|
|
570
|
+
type: "connection",
|
|
571
|
+
op: "authorize_url",
|
|
572
|
+
payload: {
|
|
573
|
+
name,
|
|
574
|
+
post_redirect: opts?.postRedirect,
|
|
575
|
+
},
|
|
576
|
+
})) as { url: string };
|
|
577
|
+
return data;
|
|
578
|
+
},
|
|
579
|
+
async get(name) {
|
|
580
|
+
const data = (await rpc(callId, {
|
|
581
|
+
type: "connection",
|
|
582
|
+
op: "get",
|
|
583
|
+
payload: { name },
|
|
584
|
+
})) as { access_token: string; scope: string | null; expires_at: number | null };
|
|
585
|
+
return {
|
|
586
|
+
accessToken: data.access_token,
|
|
587
|
+
scope: data.scope,
|
|
588
|
+
expiresAt: data.expires_at,
|
|
589
|
+
};
|
|
590
|
+
},
|
|
591
|
+
async list() {
|
|
592
|
+
const data = (await rpc(callId, {
|
|
593
|
+
type: "connection",
|
|
594
|
+
op: "list",
|
|
595
|
+
payload: {},
|
|
596
|
+
})) as {
|
|
597
|
+
connections: Array<{
|
|
598
|
+
name: string;
|
|
599
|
+
provider: string;
|
|
600
|
+
scope: string | null;
|
|
601
|
+
expires_at: number | null;
|
|
602
|
+
updated_at: number;
|
|
603
|
+
}>;
|
|
604
|
+
};
|
|
605
|
+
return {
|
|
606
|
+
connections: data.connections.map((c) => ({
|
|
607
|
+
name: c.name,
|
|
608
|
+
provider: c.provider,
|
|
609
|
+
scope: c.scope,
|
|
610
|
+
expiresAt: c.expires_at,
|
|
611
|
+
updatedAt: c.updated_at,
|
|
612
|
+
})),
|
|
613
|
+
};
|
|
614
|
+
},
|
|
615
|
+
async disconnect(name) {
|
|
616
|
+
return (await rpc(callId, {
|
|
617
|
+
type: "connection",
|
|
618
|
+
op: "disconnect",
|
|
619
|
+
payload: { name },
|
|
620
|
+
})) as { disconnected: boolean };
|
|
621
|
+
},
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
532
625
|
function buildActionCtx(
|
|
533
626
|
callId: string,
|
|
534
627
|
auth: AuthInfo,
|
|
535
628
|
stream: Stream,
|
|
536
629
|
scheduler: Scheduler,
|
|
537
630
|
email: EmailSender,
|
|
631
|
+
llm: Llm,
|
|
632
|
+
connections: Connections,
|
|
538
633
|
request?: unknown
|
|
539
634
|
): ActionCtx {
|
|
540
635
|
// The host sends `request` as snake_case JSON (`raw_body`); normalize it
|
|
@@ -555,6 +650,8 @@ function buildActionCtx(
|
|
|
555
650
|
stream,
|
|
556
651
|
scheduler,
|
|
557
652
|
email,
|
|
653
|
+
llm,
|
|
654
|
+
connections,
|
|
558
655
|
env: process.env as Record<string, string>,
|
|
559
656
|
async runQuery(fnName, args) {
|
|
560
657
|
return rpc(callId, {
|
|
@@ -635,6 +732,8 @@ async function handleCall(msg: CallMessage): Promise<void> {
|
|
|
635
732
|
const stream = buildStream(msg.call_id);
|
|
636
733
|
const scheduler = buildScheduler(msg.call_id);
|
|
637
734
|
const email = buildEmail(msg.call_id);
|
|
735
|
+
const llm = buildLlm(msg.call_id);
|
|
736
|
+
const connections = buildConnections(msg.call_id);
|
|
638
737
|
|
|
639
738
|
// Normalize the Rust-side auth envelope (snake_case) to the camelCase
|
|
640
739
|
// shape that AuthInfo documents. Handlers read `ctx.auth.userId`; the
|
|
@@ -680,6 +779,9 @@ async function handleCall(msg: CallMessage): Promise<void> {
|
|
|
680
779
|
let ctx: QueryCtx | MutationCtx | ActionCtx;
|
|
681
780
|
switch (def.type) {
|
|
682
781
|
case "query":
|
|
782
|
+
// ctx.llm is intentionally absent on queries — reactive
|
|
783
|
+
// re-runs would re-bill the LLM call on every dep change.
|
|
784
|
+
// Move LLM calls into actions / mutations.
|
|
683
785
|
ctx = { db: buildDbReader(msg.call_id), auth, env };
|
|
684
786
|
break;
|
|
685
787
|
case "mutation":
|
|
@@ -689,6 +791,8 @@ async function handleCall(msg: CallMessage): Promise<void> {
|
|
|
689
791
|
stream,
|
|
690
792
|
scheduler,
|
|
691
793
|
env,
|
|
794
|
+
llm,
|
|
795
|
+
connections,
|
|
692
796
|
error(code, message) {
|
|
693
797
|
const err = new Error(message);
|
|
694
798
|
(err as any).code = code;
|
|
@@ -697,17 +801,14 @@ async function handleCall(msg: CallMessage): Promise<void> {
|
|
|
697
801
|
};
|
|
698
802
|
break;
|
|
699
803
|
case "action":
|
|
700
|
-
// Pass `msg.request` so actions invoked via `defineRoute` HTTP
|
|
701
|
-
// bindings can reach raw headers + body (for webhook signature
|
|
702
|
-
// verification). Programmatic invocations (runAction, jobs) get
|
|
703
|
-
// undefined here and `ctx.request` reads as undefined — the type
|
|
704
|
-
// is optional on purpose.
|
|
705
804
|
ctx = buildActionCtx(
|
|
706
805
|
msg.call_id,
|
|
707
806
|
auth,
|
|
708
807
|
stream,
|
|
709
808
|
scheduler,
|
|
710
809
|
email,
|
|
810
|
+
llm,
|
|
811
|
+
connections,
|
|
711
812
|
(msg as unknown as { request?: unknown }).request,
|
|
712
813
|
);
|
|
713
814
|
break;
|
package/src/types.ts
CHANGED
|
@@ -305,11 +305,176 @@ export interface EmailSender {
|
|
|
305
305
|
send(to: string, subject: string, body: string): Promise<void>;
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// LLM — provider-abstracted text/tool-use completion
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Server-side LLM client. Available on every ctx variant (query,
|
|
314
|
+
* mutation, action) because agent loops often run as queries —
|
|
315
|
+
* read tool args from the message, ship the response back.
|
|
316
|
+
*
|
|
317
|
+
* Provider is configured at the server boot (PYLON_LLM_PROVIDER +
|
|
318
|
+
* ANTHROPIC_API_KEY or OPENAI_API_KEY). The wire shape is Anthropic
|
|
319
|
+
* Messages — OpenAI calls translate at the transport boundary, so
|
|
320
|
+
* the same caller code works against either provider.
|
|
321
|
+
*
|
|
322
|
+
* The framework does NOT expose this surface to the browser; clients
|
|
323
|
+
* that need streaming should call POST /api/ai/stream directly.
|
|
324
|
+
* `ctx.llm.complete` is server-only on purpose — the API key never
|
|
325
|
+
* leaves the runtime process.
|
|
326
|
+
*/
|
|
327
|
+
export interface Llm {
|
|
328
|
+
/**
|
|
329
|
+
* Send a completion request to the configured LLM provider. The
|
|
330
|
+
* shape is Anthropic Messages: a list of {role, content} pairs
|
|
331
|
+
* where content is either a string or a list of content blocks
|
|
332
|
+
* (text, tool_use, tool_result). Returns the full response once
|
|
333
|
+
* the model finishes generating.
|
|
334
|
+
*
|
|
335
|
+
* For agent tool-use loops, inspect `response.stopReason` — when
|
|
336
|
+
* it's `"tool_use"`, append the assistant's content (which
|
|
337
|
+
* includes the `tool_use` blocks) plus your `tool_result`
|
|
338
|
+
* follow-ups to the message list and call again. Loop until
|
|
339
|
+
* `stopReason === "end_turn"`.
|
|
340
|
+
*
|
|
341
|
+
* Errors are thrown as standard Error objects with an `err.code`
|
|
342
|
+
* property set to one of: `LLM_NOT_CONFIGURED`, `MODEL_NOT_ALLOWED`,
|
|
343
|
+
* `MODEL_OVERRIDE_FORBIDDEN`, `PROVIDER_HTTP_<code>`,
|
|
344
|
+
* `PROVIDER_UNREACHABLE`, `INVALID_REQUEST`.
|
|
345
|
+
*/
|
|
346
|
+
complete(request: LlmCompleteRequest): Promise<LlmCompleteResponse>;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export interface LlmMessage {
|
|
350
|
+
role: "user" | "assistant" | "system" | "tool";
|
|
351
|
+
content: string | LlmContentBlock[];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export type LlmContentBlock =
|
|
355
|
+
| { type: "text"; text: string }
|
|
356
|
+
| {
|
|
357
|
+
type: "tool_use";
|
|
358
|
+
id: string;
|
|
359
|
+
name: string;
|
|
360
|
+
input: Record<string, unknown>;
|
|
361
|
+
}
|
|
362
|
+
| {
|
|
363
|
+
type: "tool_result";
|
|
364
|
+
tool_use_id: string;
|
|
365
|
+
content: string;
|
|
366
|
+
is_error?: boolean;
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
export interface LlmTool {
|
|
370
|
+
name: string;
|
|
371
|
+
description?: string;
|
|
372
|
+
/** JSON Schema object describing the tool's input shape. */
|
|
373
|
+
input_schema: Record<string, unknown>;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export interface LlmCompleteRequest {
|
|
377
|
+
/** Override the server's default model. Subject to
|
|
378
|
+
* PYLON_AI_MODELS_ALLOWED gating for non-admin callers. */
|
|
379
|
+
model?: string;
|
|
380
|
+
messages: LlmMessage[];
|
|
381
|
+
system?: string;
|
|
382
|
+
tools?: LlmTool[];
|
|
383
|
+
/** Defaults to 4096. */
|
|
384
|
+
max_tokens?: number;
|
|
385
|
+
temperature?: number;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export interface LlmCompleteResponse {
|
|
389
|
+
model: string;
|
|
390
|
+
content: LlmContentBlock[];
|
|
391
|
+
/** `end_turn` | `tool_use` | `max_tokens` | `stop_sequence` */
|
|
392
|
+
stop_reason: string;
|
|
393
|
+
usage: { input_tokens: number; output_tokens: number };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
// Connections — per-user OAuth integrations
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Server-side OAuth connection registry. Apps declare connections
|
|
402
|
+
* via `defineConnection({...})` in `app.ts`; this surface lets
|
|
403
|
+
* actions fetch fresh access tokens (auto-refresh) and start the
|
|
404
|
+
* OAuth dance.
|
|
405
|
+
*
|
|
406
|
+
* Available on mutation + action ctx only — connections perform
|
|
407
|
+
* external I/O (token refresh, DB writes) that doesn't belong
|
|
408
|
+
* inside a reactive query.
|
|
409
|
+
*
|
|
410
|
+
* All ops require an authenticated caller (`ctx.auth.userId !==
|
|
411
|
+
* null`). Public functions must `ctx.auth.elevate({ admin: true,
|
|
412
|
+
* reason: "..." })` before reaching `ctx.connections.*`.
|
|
413
|
+
*/
|
|
414
|
+
export interface Connections {
|
|
415
|
+
/**
|
|
416
|
+
* Mint the URL the browser should navigate to so the user can
|
|
417
|
+
* link an external account. `name` matches a `defineConnection({...})`
|
|
418
|
+
* entry. `postRedirect` (optional) is where the browser lands
|
|
419
|
+
* after a successful callback — defaults to `/`.
|
|
420
|
+
*
|
|
421
|
+
* Throws `CONNECTIONS_NOT_CONFIGURED`, `CONNECTION_UNKNOWN`,
|
|
422
|
+
* `PROVIDER_NOT_CONFIGURED`, or `ENCRYPTION_REQUIRED` (refresh
|
|
423
|
+
* tokens are not allowed to land in plaintext).
|
|
424
|
+
*/
|
|
425
|
+
authorizeUrl(
|
|
426
|
+
name: string,
|
|
427
|
+
opts?: { postRedirect?: string }
|
|
428
|
+
): Promise<{ url: string }>;
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Returns a fresh access token for `(ctx.auth.userId, name)`. If
|
|
432
|
+
* the stored token expires within 60s, the framework refreshes
|
|
433
|
+
* via the provider's refresh-token grant FIRST, persists the new
|
|
434
|
+
* token pair, then returns the new access token.
|
|
435
|
+
*
|
|
436
|
+
* Throws `CONNECTION_NOT_LINKED` when the user hasn't started
|
|
437
|
+
* the OAuth flow, `REFRESH_FAILED` when the provider rejects
|
|
438
|
+
* the refresh token (user must re-link).
|
|
439
|
+
*/
|
|
440
|
+
get(name: string): Promise<{
|
|
441
|
+
accessToken: string;
|
|
442
|
+
scope: string | null;
|
|
443
|
+
expiresAt: number | null;
|
|
444
|
+
}>;
|
|
445
|
+
|
|
446
|
+
/** List the signed-in user's linked connections. Token values
|
|
447
|
+
* are NOT included — call `get(name)` for those. */
|
|
448
|
+
list(): Promise<{
|
|
449
|
+
connections: Array<{
|
|
450
|
+
name: string;
|
|
451
|
+
provider: string;
|
|
452
|
+
scope: string | null;
|
|
453
|
+
expiresAt: number | null;
|
|
454
|
+
updatedAt: number;
|
|
455
|
+
}>;
|
|
456
|
+
}>;
|
|
457
|
+
|
|
458
|
+
/** Remove the stored connection. Provider-side revocation is
|
|
459
|
+
* the caller's responsibility — most providers expose a separate
|
|
460
|
+
* `/revoke` endpoint that this surface intentionally doesn't
|
|
461
|
+
* call (revoke vs unlink semantics differ per provider). */
|
|
462
|
+
disconnect(name: string): Promise<{ disconnected: boolean }>;
|
|
463
|
+
}
|
|
464
|
+
|
|
308
465
|
// ---------------------------------------------------------------------------
|
|
309
466
|
// Context objects — what handlers receive
|
|
310
467
|
// ---------------------------------------------------------------------------
|
|
311
468
|
|
|
312
|
-
/** Context for query handlers (read-only).
|
|
469
|
+
/** Context for query handlers (read-only).
|
|
470
|
+
*
|
|
471
|
+
* NOTE: `ctx.llm` is NOT exposed here. Queries are reactive: a
|
|
472
|
+
* subscribed query re-runs whenever its `ctx.db.*` reads change.
|
|
473
|
+
* Calling a stochastic, paid LLM from a query would (a) silently
|
|
474
|
+
* burn the framework's API key on every dep invalidation, and
|
|
475
|
+
* (b) violate the reactive purity contract (same inputs → same
|
|
476
|
+
* outputs). LLM calls belong in mutations (transactional) or
|
|
477
|
+
* actions (external I/O). */
|
|
313
478
|
export interface QueryCtx<R extends AuthRequirement = "optional"> {
|
|
314
479
|
db: DbReader;
|
|
315
480
|
auth: AuthInfo<R>;
|
|
@@ -325,6 +490,10 @@ export interface MutationCtx<R extends AuthRequirement = "optional"> {
|
|
|
325
490
|
scheduler: Scheduler;
|
|
326
491
|
/** Environment variables / secrets. */
|
|
327
492
|
env: Record<string, string>;
|
|
493
|
+
/** Provider-abstracted LLM client. */
|
|
494
|
+
llm: Llm;
|
|
495
|
+
/** Per-user OAuth connection registry. */
|
|
496
|
+
connections: Connections;
|
|
328
497
|
/** Create a typed error that triggers rollback. */
|
|
329
498
|
error(code: string, message: string): Error;
|
|
330
499
|
}
|
|
@@ -336,6 +505,10 @@ export interface ActionCtx<R extends AuthRequirement = "optional"> {
|
|
|
336
505
|
scheduler: Scheduler;
|
|
337
506
|
/** Send transactional email via the runtime's configured provider. */
|
|
338
507
|
email: EmailSender;
|
|
508
|
+
/** Provider-abstracted LLM client. */
|
|
509
|
+
llm: Llm;
|
|
510
|
+
/** Per-user OAuth connection registry. */
|
|
511
|
+
connections: Connections;
|
|
339
512
|
/** Environment variables / secrets. */
|
|
340
513
|
env: Record<string, string>;
|
|
341
514
|
/** Run a registered query within its own read transaction. */
|