@frankieai/server 0.2.5 → 0.2.7
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/dist/{chunk-SQ5NJKFE.js → chunk-4XAOVSTL.js} +769 -52
- package/dist/chunk-4XAOVSTL.js.map +1 -0
- package/dist/{chunk-HPYT6ZIU.js → chunk-TPVDQKD3.js} +1105 -105
- package/dist/chunk-TPVDQKD3.js.map +1 -0
- package/dist/control-server.js +5 -5
- package/dist/control-server.js.map +1 -1
- package/dist/index.d.ts +417 -84
- package/dist/index.js +2 -2
- package/dist/server.js +8 -2
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-HPYT6ZIU.js.map +0 -1
- package/dist/chunk-SQ5NJKFE.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
import { FrankieConfig, AuthConfig, WorkspaceSkill, HostToolRequest,
|
|
2
|
+
import { FrankieConfig, AuthConfig, HostToolResult, ActionPreview, WorkspaceSkill, HostToolRequest, ChatAttachment, FrankieServerEvent, McpServerConfig, ToolPolicy, ApiToolInventory, ApiSourceConfig } from '@frankieai/core';
|
|
3
3
|
export { matchPath } from '@frankieai/core';
|
|
4
4
|
import { AgentMessage, AgentTool } from '@earendil-works/pi-agent-core';
|
|
5
5
|
import { JWTPayload } from 'jose';
|
|
@@ -112,57 +112,6 @@ declare function safeEqual(a: string, b: string): boolean;
|
|
|
112
112
|
/** Stable pseudonymous id for an anonymous visitor. */
|
|
113
113
|
declare function anonymousUser(seed: string): VerifiedUser;
|
|
114
114
|
|
|
115
|
-
interface ChatSession {
|
|
116
|
-
id: string;
|
|
117
|
-
tenantId: string;
|
|
118
|
-
user: VerifiedUser;
|
|
119
|
-
/** Raw host-signed token, forwarded only to tenant tools that explicitly opt in. */
|
|
120
|
-
userToken?: string;
|
|
121
|
-
origin?: string;
|
|
122
|
-
context: Record<string, string | number | boolean | null>;
|
|
123
|
-
/** Agent transcript used to continue the model conversation on the server. */
|
|
124
|
-
messages: AgentMessage[];
|
|
125
|
-
/** UI transcript kept separately so the exact rendered chat can sync across devices. */
|
|
126
|
-
clientMessages?: unknown[];
|
|
127
|
-
/** User-editable title, otherwise derived from the first user message. */
|
|
128
|
-
title?: string;
|
|
129
|
-
createdAt: number;
|
|
130
|
-
lastActiveAt: number;
|
|
131
|
-
messageCount: number;
|
|
132
|
-
costUsd: number;
|
|
133
|
-
busy: boolean;
|
|
134
|
-
}
|
|
135
|
-
interface SessionStore {
|
|
136
|
-
create(session: ChatSession): Promise<void>;
|
|
137
|
-
get(id: string): Promise<ChatSession | undefined>;
|
|
138
|
-
/** List one user's conversations for a tenant, newest first. */
|
|
139
|
-
listByUser(tenantId: string, userId: string): Promise<ChatSession[]>;
|
|
140
|
-
/** Workspace-owner history view. This is only exposed through the authenticated control plane. */
|
|
141
|
-
listByTenant(tenantId: string): Promise<ChatSession[]>;
|
|
142
|
-
/**
|
|
143
|
-
* Non-expired conversation count per tenant id, in one pass. Used by the
|
|
144
|
-
* workspace directory to show a workspace as "Live" once it has real chats —
|
|
145
|
-
* cheaper than a listByTenant call per workspace.
|
|
146
|
-
*/
|
|
147
|
-
countsByTenant(): Promise<Record<string, number>>;
|
|
148
|
-
update(id: string, patch: Partial<ChatSession>): Promise<void>;
|
|
149
|
-
delete(id: string): Promise<void>;
|
|
150
|
-
}
|
|
151
|
-
declare class MemorySessionStore implements SessionStore {
|
|
152
|
-
private readonly ttlMs;
|
|
153
|
-
private readonly sessions;
|
|
154
|
-
constructor(ttlMs?: number);
|
|
155
|
-
create(session: ChatSession): Promise<void>;
|
|
156
|
-
get(id: string): Promise<ChatSession | undefined>;
|
|
157
|
-
listByUser(tenantId: string, userId: string): Promise<ChatSession[]>;
|
|
158
|
-
listByTenant(tenantId: string): Promise<ChatSession[]>;
|
|
159
|
-
countsByTenant(): Promise<Record<string, number>>;
|
|
160
|
-
update(id: string, patch: Partial<ChatSession>): Promise<void>;
|
|
161
|
-
delete(id: string): Promise<void>;
|
|
162
|
-
private evictExpired;
|
|
163
|
-
}
|
|
164
|
-
declare function newSessionId(): string;
|
|
165
|
-
|
|
166
115
|
declare class ModelResolutionError extends Error {
|
|
167
116
|
}
|
|
168
117
|
interface ResolvedModel {
|
|
@@ -249,35 +198,11 @@ declare class SingleTenantStore implements TenantStore {
|
|
|
249
198
|
declare class MemoryTenantStore implements TenantStore {
|
|
250
199
|
private readonly byKey;
|
|
251
200
|
constructor(tenants?: ReadonlyArray<ResolvedConfig>);
|
|
252
|
-
/** Register (or replace) a tenant
|
|
201
|
+
/** Register (or replace) a tenant, under its base key and each persona's key. */
|
|
253
202
|
add(resolved: ResolvedConfig): void;
|
|
254
203
|
byPublishableKey(key: string): Promise<ResolvedConfig | null>;
|
|
255
204
|
}
|
|
256
205
|
|
|
257
|
-
interface CreateAppOptions {
|
|
258
|
-
/** A single tenant, resolved at startup — the self-hosted case. */
|
|
259
|
-
resolved?: ResolvedConfig;
|
|
260
|
-
/** Many tenants, each resolved by publishable key at request time — the SaaS case. */
|
|
261
|
-
tenants?: TenantStore | TenantRegistry;
|
|
262
|
-
sessions?: SessionStore;
|
|
263
|
-
rateLimiter?: RateLimiter;
|
|
264
|
-
logger?: (event: Record<string, unknown>) => void;
|
|
265
|
-
}
|
|
266
|
-
type AppEnv = {
|
|
267
|
-
Variables: {
|
|
268
|
-
tenant: TenantRuntime;
|
|
269
|
-
};
|
|
270
|
-
};
|
|
271
|
-
declare function createFrankieApp(options: CreateAppOptions): Hono<AppEnv>;
|
|
272
|
-
|
|
273
|
-
interface McpHostOptions {
|
|
274
|
-
tenants: TenantStore | TenantRegistry;
|
|
275
|
-
logger?: (event: Record<string, unknown>) => void;
|
|
276
|
-
/** Injected in tests so generated API tools hit a stub rather than the network. */
|
|
277
|
-
fetchImpl?: typeof fetch;
|
|
278
|
-
}
|
|
279
|
-
declare function createMcpHostApp(options: McpHostOptions): Hono;
|
|
280
|
-
|
|
281
206
|
/**
|
|
282
207
|
* What the control plane stores per tenant. The model provider key is *not*
|
|
283
208
|
* here — in the hosted shape that key is Frankie's own, one shared key read
|
|
@@ -302,6 +227,10 @@ interface RepositoryInstallRecord {
|
|
|
302
227
|
pullRequestState?: "draft" | "open" | "closed" | "merged";
|
|
303
228
|
checkRollup?: "success" | "pending" | "failure" | "none";
|
|
304
229
|
checkedAt?: string;
|
|
230
|
+
/** The current integration stage (see apps/web/lib/integration-status.ts), so Overview renders it on first paint. */
|
|
231
|
+
stage?: string;
|
|
232
|
+
/** The fully-integrated checklist rows that are done (apps/web/lib/integration-checklist.ts) — the funnel position. */
|
|
233
|
+
checklist?: string[];
|
|
305
234
|
updatedAt: string;
|
|
306
235
|
}
|
|
307
236
|
/**
|
|
@@ -322,6 +251,28 @@ interface VercelLinkRecord {
|
|
|
322
251
|
}
|
|
323
252
|
/** `VercelLinkRecord` with the token stripped — what the dashboard sees. */
|
|
324
253
|
type PublicVercelLink = Omit<VercelLinkRecord, "accessToken">;
|
|
254
|
+
/** What the installer read from the repository's own styling (see @frankieai/core inferTheme). */
|
|
255
|
+
interface SuggestedThemeRecord {
|
|
256
|
+
theme: {
|
|
257
|
+
primary: string;
|
|
258
|
+
background?: string;
|
|
259
|
+
foreground?: string;
|
|
260
|
+
radius?: string;
|
|
261
|
+
};
|
|
262
|
+
sources: Array<{
|
|
263
|
+
field: "primary" | "background" | "foreground" | "radius";
|
|
264
|
+
path: string;
|
|
265
|
+
}>;
|
|
266
|
+
repository: string;
|
|
267
|
+
inferredAt: string;
|
|
268
|
+
}
|
|
269
|
+
/** An uploaded file's text, kept so every re-index includes it. */
|
|
270
|
+
interface KnowledgeDocument {
|
|
271
|
+
id: string;
|
|
272
|
+
name: string;
|
|
273
|
+
text: string;
|
|
274
|
+
addedAt: string;
|
|
275
|
+
}
|
|
325
276
|
interface TenantRecord {
|
|
326
277
|
apiSpecSync?: Record<string, {
|
|
327
278
|
repository: string;
|
|
@@ -343,8 +294,25 @@ interface TenantRecord {
|
|
|
343
294
|
sessionSecret: string;
|
|
344
295
|
/** When true, the workspace is protected: it cannot be deleted until unlocked. */
|
|
345
296
|
locked?: boolean;
|
|
297
|
+
/** When true, the dashboard applies a changed repository spec on its own (new operations still land unapproved). */
|
|
298
|
+
apiAutoRefresh?: boolean;
|
|
299
|
+
/** A widget theme inferred from the repository at install time, awaiting Accept / Keep in the Theme editor. */
|
|
300
|
+
suggestedTheme?: SuggestedThemeRecord;
|
|
346
301
|
/** Durable repository-install state surfaced in the dashboard and setup chat. */
|
|
347
302
|
repositoryInstall?: RepositoryInstallRecord;
|
|
303
|
+
/**
|
|
304
|
+
* Per-persona repository-install state, keyed by persona id. The primary
|
|
305
|
+
* agent keeps `repositoryInstall`; each embedded persona (see
|
|
306
|
+
* `config.personas`) gets its own install record here, so a workspace can
|
|
307
|
+
* open a separate scoped-install PR per agent.
|
|
308
|
+
*/
|
|
309
|
+
personaInstalls?: Record<string, RepositoryInstallRecord>;
|
|
310
|
+
/**
|
|
311
|
+
* Files the team uploaded for its agents to answer from (an FAQ, a guide),
|
|
312
|
+
* as extracted text. Indexed alongside the crawled pages; never sent to the
|
|
313
|
+
* dashboard in full.
|
|
314
|
+
*/
|
|
315
|
+
knowledgeDocuments?: KnowledgeDocument[];
|
|
348
316
|
/** Deployment provider connection, when the workspace has connected Vercel. */
|
|
349
317
|
vercel?: VercelLinkRecord;
|
|
350
318
|
createdAt: string;
|
|
@@ -407,14 +375,30 @@ interface PublicTenant {
|
|
|
407
375
|
}>;
|
|
408
376
|
/** Protected from deletion until unlocked. */
|
|
409
377
|
locked: boolean;
|
|
378
|
+
/** Apply a changed repository spec without a click (see TenantRecord). */
|
|
379
|
+
apiAutoRefresh: boolean;
|
|
380
|
+
suggestedTheme?: SuggestedThemeRecord;
|
|
410
381
|
/** A submitted or in-progress SDK installation for this workspace. */
|
|
411
382
|
repositoryInstall?: RepositoryInstallRecord;
|
|
383
|
+
/** A submitted or in-progress scoped-install PR per persona id (`config.personas`). */
|
|
384
|
+
personaInstalls?: Record<string, RepositoryInstallRecord>;
|
|
385
|
+
/** Uploaded files agents answer from: names and sizes only. */
|
|
386
|
+
knowledgeDocuments: Array<{
|
|
387
|
+
id: string;
|
|
388
|
+
name: string;
|
|
389
|
+
chars: number;
|
|
390
|
+
addedAt: string;
|
|
391
|
+
}>;
|
|
412
392
|
/** The connected Vercel installation and matched project — never its token. */
|
|
413
393
|
vercel?: PublicVercelLink;
|
|
414
394
|
/** The customer's brand theme for the widget, so the dashboard can edit it. */
|
|
415
395
|
theme: TenantRecord["config"]["theme"];
|
|
416
396
|
/** Opt-in chat capabilities (attachments, model picker) for the dashboard to edit. */
|
|
417
397
|
features: TenantRecord["config"]["features"];
|
|
398
|
+
/** Documentation sources indexed for the knowledge-base agent, for the dashboard to edit. */
|
|
399
|
+
knowledgeBase: TenantRecord["config"]["knowledgeBase"];
|
|
400
|
+
/** Embedded page-scoped personas (extra agents) on this workspace, for the dashboard to edit. Their publishable keys are public. */
|
|
401
|
+
personas: TenantRecord["config"]["personas"];
|
|
418
402
|
/** Editable chat copy: the greeting and the starter cards shown on the empty screen. */
|
|
419
403
|
agent: {
|
|
420
404
|
greeting: string;
|
|
@@ -453,6 +437,27 @@ interface PlatformAdminRecord {
|
|
|
453
437
|
grantedBy: string;
|
|
454
438
|
createdAt: string;
|
|
455
439
|
}
|
|
440
|
+
/** Billing plan tiers. Free is the default; Pro lifts the workspace cap. */
|
|
441
|
+
type Plan = "free" | "pro";
|
|
442
|
+
/**
|
|
443
|
+
* A per-org (owner id) billing subscription. One org, one subscription,
|
|
444
|
+
* covering all its workspaces and members — matching the model where inviting
|
|
445
|
+
* someone grants access to every workspace. The Stripe ids are written only by
|
|
446
|
+
* the webhook (platform admin), never by the org itself, so an org can't
|
|
447
|
+
* self-upgrade. Absent record ⇒ the Free plan.
|
|
448
|
+
*/
|
|
449
|
+
interface SubscriptionRecord {
|
|
450
|
+
/** The org's owner id, e.g. "github:42". */
|
|
451
|
+
org: string;
|
|
452
|
+
plan: Plan;
|
|
453
|
+
/** Mirrors the Stripe subscription status; anything but active/trialing is treated as Free. */
|
|
454
|
+
status: "active" | "trialing" | "past_due" | "canceled" | "incomplete";
|
|
455
|
+
stripeCustomerId?: string;
|
|
456
|
+
stripeSubscriptionId?: string;
|
|
457
|
+
/** ISO timestamp the current paid period ends, for display. */
|
|
458
|
+
currentPeriodEnd?: string;
|
|
459
|
+
updatedAt: string;
|
|
460
|
+
}
|
|
456
461
|
interface ControlStore extends TenantStore {
|
|
457
462
|
listOrganizations(): Promise<string[]>;
|
|
458
463
|
listPlatformAdmins(): Promise<PlatformAdminRecord[]>;
|
|
@@ -463,6 +468,12 @@ interface ControlStore extends TenantStore {
|
|
|
463
468
|
list(ownerId?: string): Promise<TenantRecord[]>;
|
|
464
469
|
get(id: string): Promise<TenantRecord | null>;
|
|
465
470
|
put(record: TenantRecord): Promise<void>;
|
|
471
|
+
/**
|
|
472
|
+
* Store a brand-new tenant, never replacing one: false when the id (or its
|
|
473
|
+
* publishable key) is already taken. Creating a workspace must use this, so
|
|
474
|
+
* a colliding id can't overwrite someone else's workspace.
|
|
475
|
+
*/
|
|
476
|
+
insert(record: TenantRecord): Promise<boolean>;
|
|
466
477
|
/** Remove a tenant for good. A no-op if it does not exist. */
|
|
467
478
|
delete(id: string): Promise<void>;
|
|
468
479
|
/** All members of the org owned by `org`. */
|
|
@@ -473,6 +484,10 @@ interface ControlStore extends TenantStore {
|
|
|
473
484
|
deleteMember(org: string, login: string): Promise<void>;
|
|
474
485
|
/** Every org membership held by a given login, across all orgs. */
|
|
475
486
|
membershipsForLogin(login: string): Promise<MembershipRecord[]>;
|
|
487
|
+
/** The org's billing subscription, or null for an org on the Free plan. */
|
|
488
|
+
getSubscription(org: string): Promise<SubscriptionRecord | null>;
|
|
489
|
+
/** Create or replace an org's subscription. Written by the Stripe webhook. */
|
|
490
|
+
putSubscription(subscription: SubscriptionRecord): Promise<void>;
|
|
476
491
|
}
|
|
477
492
|
/**
|
|
478
493
|
* The control plane's persistence, and at the same time a `TenantStore` the
|
|
@@ -495,6 +510,7 @@ declare class FileTenantStore implements ControlStore {
|
|
|
495
510
|
private index;
|
|
496
511
|
private byId;
|
|
497
512
|
private members;
|
|
513
|
+
private subscriptions;
|
|
498
514
|
private loaded;
|
|
499
515
|
private static memberKey;
|
|
500
516
|
constructor(options: {
|
|
@@ -508,13 +524,23 @@ declare class FileTenantStore implements ControlStore {
|
|
|
508
524
|
putMember(member: MembershipRecord): Promise<void>;
|
|
509
525
|
deleteMember(org: string, login: string): Promise<void>;
|
|
510
526
|
membershipsForLogin(login: string): Promise<MembershipRecord[]>;
|
|
527
|
+
private persistSubscriptions;
|
|
528
|
+
getSubscription(org: string): Promise<SubscriptionRecord | null>;
|
|
529
|
+
putSubscription(subscription: SubscriptionRecord): Promise<void>;
|
|
511
530
|
private remember;
|
|
512
531
|
byPublishableKey(key: string): Promise<ResolvedConfig | null>;
|
|
513
532
|
/** All tenants, or — when an owner is given — only that owner's. */
|
|
514
533
|
list(ownerId?: string): Promise<TenantRecord[]>;
|
|
515
534
|
get(id: string): Promise<TenantRecord | null>;
|
|
516
535
|
put(record: TenantRecord): Promise<void>;
|
|
536
|
+
insert(record: TenantRecord): Promise<boolean>;
|
|
517
537
|
compareAndSwap(previous: TenantRecord, next: TenantRecord): Promise<boolean>;
|
|
538
|
+
/**
|
|
539
|
+
* A record holds the workspace's signing and session secrets and source
|
|
540
|
+
* credentials: owner-only permissions, and written whole (temp file, then
|
|
541
|
+
* rename) so a crash never leaves half a record behind.
|
|
542
|
+
*/
|
|
543
|
+
private write;
|
|
518
544
|
delete(id: string): Promise<void>;
|
|
519
545
|
}
|
|
520
546
|
|
|
@@ -557,6 +583,7 @@ declare class PostgresTenantStore implements ControlStore {
|
|
|
557
583
|
private ensureReady;
|
|
558
584
|
/** Create the table if it isn't there yet. Safe to call repeatedly. */
|
|
559
585
|
init(): Promise<void>;
|
|
586
|
+
private subscriptionRow;
|
|
560
587
|
private memberRow;
|
|
561
588
|
private adminRow;
|
|
562
589
|
listOrganizations(): Promise<string[]>;
|
|
@@ -567,16 +594,218 @@ declare class PostgresTenantStore implements ControlStore {
|
|
|
567
594
|
listMembers(org: string): Promise<MembershipRecord[]>;
|
|
568
595
|
putMember(member: MembershipRecord): Promise<void>;
|
|
569
596
|
deleteMember(org: string, login: string): Promise<void>;
|
|
597
|
+
getSubscription(org: string): Promise<SubscriptionRecord | null>;
|
|
598
|
+
putSubscription(subscription: SubscriptionRecord): Promise<void>;
|
|
570
599
|
membershipsForLogin(login: string): Promise<MembershipRecord[]>;
|
|
571
600
|
private parse;
|
|
572
601
|
byPublishableKey(key: string): Promise<ResolvedConfig | null>;
|
|
573
602
|
list(ownerId?: string): Promise<TenantRecord[]>;
|
|
574
603
|
get(id: string): Promise<TenantRecord | null>;
|
|
604
|
+
insert(record: TenantRecord): Promise<boolean>;
|
|
575
605
|
put(record: TenantRecord): Promise<void>;
|
|
576
606
|
delete(id: string): Promise<void>;
|
|
577
607
|
compareAndSwap(previous: TenantRecord, next: TenantRecord): Promise<boolean>;
|
|
578
608
|
}
|
|
579
609
|
|
|
610
|
+
/**
|
|
611
|
+
* Something said about a running turn from a request that may have landed on a
|
|
612
|
+
* different server instance than the one running it: the user pressed Stop,
|
|
613
|
+
* or their browser finished a tool call the turn is waiting on.
|
|
614
|
+
*/
|
|
615
|
+
type TurnSignal = {
|
|
616
|
+
kind: "stop";
|
|
617
|
+
} | {
|
|
618
|
+
kind: "tool_result";
|
|
619
|
+
callId: string;
|
|
620
|
+
result: HostToolResult;
|
|
621
|
+
};
|
|
622
|
+
/**
|
|
623
|
+
* Carries turn signals between instances. A turn polls for its session's
|
|
624
|
+
* signals while it runs; a request that can't reach the turn locally sends one.
|
|
625
|
+
* Single-instance deployments don't need it: everything is local.
|
|
626
|
+
*/
|
|
627
|
+
interface TurnSignals {
|
|
628
|
+
send(sessionId: string, signal: TurnSignal): Promise<void>;
|
|
629
|
+
/** Signals for the session after `afterId`, oldest first. */
|
|
630
|
+
poll(sessionId: string, afterId: number): Promise<Array<{
|
|
631
|
+
id: number;
|
|
632
|
+
signal: TurnSignal;
|
|
633
|
+
}>>;
|
|
634
|
+
/** The newest signal id, so a turn starting now ignores everything before it. */
|
|
635
|
+
latestId(sessionId: string): Promise<number>;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/** One recorded hand-off to the workspace's team. Stored on the conversation. */
|
|
639
|
+
interface EscalationRecord {
|
|
640
|
+
id: string;
|
|
641
|
+
at: number;
|
|
642
|
+
/** Why the agent escalated (its own words / the visitor's ask). */
|
|
643
|
+
reason: string;
|
|
644
|
+
/** A short recap of what the visitor needs and what was already tried. */
|
|
645
|
+
summary: string;
|
|
646
|
+
/** How to reach the visitor, when they provided it. */
|
|
647
|
+
email?: string;
|
|
648
|
+
name?: string;
|
|
649
|
+
}
|
|
650
|
+
/** What the tool hands back to the app: the record plus who/where it came from. */
|
|
651
|
+
interface EscalationEvent extends EscalationRecord {
|
|
652
|
+
tenantId: string;
|
|
653
|
+
conversationId: string;
|
|
654
|
+
userId: string;
|
|
655
|
+
}
|
|
656
|
+
type EscalationSink = (event: EscalationEvent) => Promise<void>;
|
|
657
|
+
|
|
658
|
+
interface ChatSession {
|
|
659
|
+
id: string;
|
|
660
|
+
tenantId: string;
|
|
661
|
+
user: VerifiedUser;
|
|
662
|
+
/** Raw host-signed token, forwarded only to tenant tools that explicitly opt in. */
|
|
663
|
+
userToken?: string;
|
|
664
|
+
origin?: string;
|
|
665
|
+
/**
|
|
666
|
+
* The publishable key the chat was opened with: the workspace's own key or
|
|
667
|
+
* one of its agents' keys. Lets the dashboard say which agent answered.
|
|
668
|
+
*/
|
|
669
|
+
agentKey?: string;
|
|
670
|
+
context: Record<string, string | number | boolean | null>;
|
|
671
|
+
/** Agent transcript used to continue the model conversation on the server. */
|
|
672
|
+
messages: AgentMessage[];
|
|
673
|
+
/** UI transcript kept separately so the exact rendered chat can sync across devices. */
|
|
674
|
+
clientMessages?: unknown[];
|
|
675
|
+
/** User-editable title, otherwise derived from the first user message. */
|
|
676
|
+
title?: string;
|
|
677
|
+
createdAt: number;
|
|
678
|
+
lastActiveAt: number;
|
|
679
|
+
messageCount: number;
|
|
680
|
+
costUsd: number;
|
|
681
|
+
/** True while a chat turn holds this session; see `busySince` for how long. */
|
|
682
|
+
busy: boolean;
|
|
683
|
+
/**
|
|
684
|
+
* When the running turn took the lock. A turn can only hold the session for
|
|
685
|
+
* as long as its timeout, so a lock older than that is one whose release
|
|
686
|
+
* never landed (the instance was frozen or restarted mid-turn) and is free
|
|
687
|
+
* for the taking — otherwise the session would answer 409 forever.
|
|
688
|
+
*/
|
|
689
|
+
busySince?: number;
|
|
690
|
+
/** When someone pressed Stop for a turn running on another instance; that turn then doesn't save its reply. */
|
|
691
|
+
stopRequestedAt?: number;
|
|
692
|
+
/** Human hand-offs the agent made in this conversation (the escalation inbox). */
|
|
693
|
+
escalations?: EscalationRecord[];
|
|
694
|
+
/** An action that changes data, paused until the user approves or cancels it. */
|
|
695
|
+
pendingAction?: PendingAction;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* A write the agent asked for and the user hasn't answered yet. On Approve the
|
|
699
|
+
* server runs exactly this call, with these inputs, not whatever the model
|
|
700
|
+
* would ask for next time.
|
|
701
|
+
*/
|
|
702
|
+
interface PendingAction {
|
|
703
|
+
toolUseId: string;
|
|
704
|
+
name: string;
|
|
705
|
+
label: string;
|
|
706
|
+
args: unknown;
|
|
707
|
+
preview: ActionPreview;
|
|
708
|
+
createdAt: number;
|
|
709
|
+
}
|
|
710
|
+
interface SessionStore {
|
|
711
|
+
create(session: ChatSession): Promise<void>;
|
|
712
|
+
get(id: string): Promise<ChatSession | undefined>;
|
|
713
|
+
/** List one user's conversations for a tenant, newest first. */
|
|
714
|
+
listByUser(tenantId: string, userId: string): Promise<ChatSession[]>;
|
|
715
|
+
/** Workspace-owner history view. This is only exposed through the authenticated control plane. */
|
|
716
|
+
listByTenant(tenantId: string): Promise<ChatSession[]>;
|
|
717
|
+
/**
|
|
718
|
+
* Non-expired conversation count per tenant id, in one pass. Used by the
|
|
719
|
+
* workspace directory to show a workspace as "Live" once it has real chats —
|
|
720
|
+
* cheaper than a listByTenant call per workspace.
|
|
721
|
+
*/
|
|
722
|
+
countsByTenant(): Promise<Record<string, number>>;
|
|
723
|
+
update(id: string, patch: Partial<ChatSession>): Promise<void>;
|
|
724
|
+
delete(id: string): Promise<void>;
|
|
725
|
+
}
|
|
726
|
+
declare class MemorySessionStore implements SessionStore {
|
|
727
|
+
private readonly ttlMs;
|
|
728
|
+
private readonly sessions;
|
|
729
|
+
constructor(ttlMs?: number);
|
|
730
|
+
create(session: ChatSession): Promise<void>;
|
|
731
|
+
get(id: string): Promise<ChatSession | undefined>;
|
|
732
|
+
listByUser(tenantId: string, userId: string): Promise<ChatSession[]>;
|
|
733
|
+
listByTenant(tenantId: string): Promise<ChatSession[]>;
|
|
734
|
+
countsByTenant(): Promise<Record<string, number>>;
|
|
735
|
+
update(id: string, patch: Partial<ChatSession>): Promise<void>;
|
|
736
|
+
delete(id: string): Promise<void>;
|
|
737
|
+
private evictExpired;
|
|
738
|
+
}
|
|
739
|
+
declare function newSessionId(): string;
|
|
740
|
+
|
|
741
|
+
/** One indexed passage of a document, with its embedding vector. */
|
|
742
|
+
interface KnowledgeChunk {
|
|
743
|
+
id: string;
|
|
744
|
+
url: string;
|
|
745
|
+
title: string;
|
|
746
|
+
text: string;
|
|
747
|
+
embedding: number[];
|
|
748
|
+
}
|
|
749
|
+
/** A retrieval result: the passage and how close it was to the query. */
|
|
750
|
+
interface KnowledgeHit {
|
|
751
|
+
url: string;
|
|
752
|
+
title: string;
|
|
753
|
+
text: string;
|
|
754
|
+
score: number;
|
|
755
|
+
}
|
|
756
|
+
/** What the dashboard shows about a workspace's index. */
|
|
757
|
+
interface KnowledgeStats {
|
|
758
|
+
chunks: number;
|
|
759
|
+
pages: number;
|
|
760
|
+
sources: string[];
|
|
761
|
+
ingestedAt?: number;
|
|
762
|
+
}
|
|
763
|
+
interface KnowledgeStore {
|
|
764
|
+
/** Replace a workspace's whole index with a freshly ingested set (atomic swap). */
|
|
765
|
+
replace(tenantId: string, chunks: KnowledgeChunk[], meta: {
|
|
766
|
+
sources: string[];
|
|
767
|
+
pages: number;
|
|
768
|
+
ingestedAt: number;
|
|
769
|
+
}): Promise<void>;
|
|
770
|
+
/** Top-k passages by cosine similarity to the query vector. */
|
|
771
|
+
search(tenantId: string, queryVector: readonly number[], k: number): Promise<KnowledgeHit[]>;
|
|
772
|
+
stats(tenantId: string): Promise<KnowledgeStats>;
|
|
773
|
+
clear(tenantId: string): Promise<void>;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
interface CreateAppOptions {
|
|
777
|
+
/** A single tenant, resolved at startup — the self-hosted case. */
|
|
778
|
+
resolved?: ResolvedConfig;
|
|
779
|
+
/** Many tenants, each resolved by publishable key at request time — the SaaS case. */
|
|
780
|
+
tenants?: TenantStore | TenantRegistry;
|
|
781
|
+
sessions?: SessionStore;
|
|
782
|
+
/** Knowledge-base index for the docs-search tool; in-memory when omitted. */
|
|
783
|
+
knowledge?: KnowledgeStore;
|
|
784
|
+
rateLimiter?: RateLimiter;
|
|
785
|
+
/**
|
|
786
|
+
* Reaches a turn running on another instance: Stop, and browser tool
|
|
787
|
+
* results. Needed whenever more than one instance serves chat.
|
|
788
|
+
*/
|
|
789
|
+
turnSignals?: TurnSignals;
|
|
790
|
+
logger?: (event: Record<string, unknown>) => void;
|
|
791
|
+
}
|
|
792
|
+
type AppEnv = {
|
|
793
|
+
Variables: {
|
|
794
|
+
tenant: TenantRuntime;
|
|
795
|
+
};
|
|
796
|
+
};
|
|
797
|
+
declare function createFrankieApp(options: CreateAppOptions): Hono<AppEnv>;
|
|
798
|
+
|
|
799
|
+
interface McpHostOptions {
|
|
800
|
+
tenants: TenantStore | TenantRegistry;
|
|
801
|
+
logger?: (event: Record<string, unknown>) => void;
|
|
802
|
+
/** Injected in tests so generated API tools hit a stub rather than the network. */
|
|
803
|
+
fetchImpl?: typeof fetch;
|
|
804
|
+
/** Shared with the chat API; each request counts against the tenant's message budget. */
|
|
805
|
+
rateLimiter?: RateLimiter;
|
|
806
|
+
}
|
|
807
|
+
declare function createMcpHostApp(options: McpHostOptions): Hono;
|
|
808
|
+
|
|
580
809
|
interface ResolvedStore {
|
|
581
810
|
store: ControlStore;
|
|
582
811
|
describe: string;
|
|
@@ -584,6 +813,10 @@ interface ResolvedStore {
|
|
|
584
813
|
interface ResolvedStores {
|
|
585
814
|
tenants: ControlStore;
|
|
586
815
|
sessions: SessionStore;
|
|
816
|
+
knowledge: KnowledgeStore;
|
|
817
|
+
/** Shared across instances with Postgres; undefined means in-memory (one instance). */
|
|
818
|
+
rateLimiter?: RateLimiter;
|
|
819
|
+
turnSignals?: TurnSignals;
|
|
587
820
|
describe: string;
|
|
588
821
|
}
|
|
589
822
|
/**
|
|
@@ -612,10 +845,21 @@ declare class PostgresSessionStore implements SessionStore {
|
|
|
612
845
|
private readonly db;
|
|
613
846
|
private readonly ttlMs;
|
|
614
847
|
private ready?;
|
|
848
|
+
private lastPurge;
|
|
615
849
|
constructor(options: {
|
|
616
850
|
db: Queryable;
|
|
617
851
|
ttlMs?: number;
|
|
618
852
|
});
|
|
853
|
+
/** Deletes every conversation idle past the TTL. Returns how many went. */
|
|
854
|
+
purgeExpired(): Promise<number>;
|
|
855
|
+
/**
|
|
856
|
+
* Purges at most hourly, piggybacking on the reads that scan the table
|
|
857
|
+
* anyway. Housekeeping must never fail the read it rides on: a slow DELETE
|
|
858
|
+
* hitting a statement timeout, or a role that can read but not delete,
|
|
859
|
+
* would otherwise turn a user's conversation list into an error. The
|
|
860
|
+
* expired rows are still filtered out of every read regardless.
|
|
861
|
+
*/
|
|
862
|
+
private purgeIfDue;
|
|
619
863
|
private ensureReady;
|
|
620
864
|
init(): Promise<void>;
|
|
621
865
|
create(session: ChatSession): Promise<void>;
|
|
@@ -641,20 +885,57 @@ interface SkillGenerationInput {
|
|
|
641
885
|
*/
|
|
642
886
|
toolDetails?: Array<{
|
|
643
887
|
id: string;
|
|
644
|
-
description: string;
|
|
888
|
+
description: string; /** What the tool takes (`describeInputs`), when the inventory knows. */
|
|
889
|
+
inputs?: string;
|
|
645
890
|
}>;
|
|
646
891
|
/** Present when refining an existing skill rather than starting fresh. */
|
|
647
892
|
instructions?: string;
|
|
648
893
|
}
|
|
649
894
|
declare function generateSkill(resolved: ResolvedConfig, input: SkillGenerationInput): Promise<WorkspaceSkill>;
|
|
650
895
|
|
|
896
|
+
interface ConversationQuestion {
|
|
897
|
+
/** The full conversation log (see `conversationLog` in @frankieai/core), built server-side. */
|
|
898
|
+
log: string;
|
|
899
|
+
question: string;
|
|
900
|
+
/** Earlier questions and answers in this thread, oldest first. */
|
|
901
|
+
history: Array<{
|
|
902
|
+
role: "user" | "assistant";
|
|
903
|
+
text: string;
|
|
904
|
+
}>;
|
|
905
|
+
}
|
|
906
|
+
/** Answer one question about one conversation with the workspace's own model. */
|
|
907
|
+
declare function askAboutConversation(resolved: ResolvedConfig, input: ConversationQuestion): Promise<string>;
|
|
908
|
+
|
|
909
|
+
/** What a GitHub Actions OIDC token says about the run that minted it. */
|
|
910
|
+
interface GithubRunClaims {
|
|
911
|
+
/** "owner/name" of the repository the workflow ran in. */
|
|
912
|
+
repository: string;
|
|
913
|
+
/** e.g. "refs/heads/main". */
|
|
914
|
+
ref: string;
|
|
915
|
+
/** The commit the run is for. */
|
|
916
|
+
sha: string;
|
|
917
|
+
/** "push", "workflow_dispatch", … */
|
|
918
|
+
eventName: string;
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Verify a token GitHub Actions minted for a workflow run (`id-token: write`).
|
|
922
|
+
* It proves which repository and commit the request comes from without any
|
|
923
|
+
* secret stored in the repository: GitHub signs it, we check the signature
|
|
924
|
+
* against GitHub's published keys, the issuer, the audience and its expiry.
|
|
925
|
+
*/
|
|
926
|
+
declare function verifyGithubRunToken(token: string): Promise<GithubRunClaims>;
|
|
927
|
+
|
|
651
928
|
interface ControlOptions {
|
|
652
929
|
/** Immutable GitHub IDs configured by the platform operator, never by a client. */
|
|
653
930
|
bootstrapSuperAdmins?: string[];
|
|
654
931
|
generateSkill?: typeof generateSkill;
|
|
932
|
+
askAboutConversation?: typeof askAboutConversation;
|
|
933
|
+
verifyGithubRunToken?: typeof verifyGithubRunToken;
|
|
655
934
|
store: ControlStore;
|
|
656
935
|
/** Durable session store shared with the data plane when Postgres is configured. */
|
|
657
936
|
sessions?: SessionStore;
|
|
937
|
+
/** Knowledge index shared with the data plane, so a dashboard-triggered ingest is what the docs-search tool reads. */
|
|
938
|
+
knowledge?: KnowledgeStore;
|
|
658
939
|
/** Bearer token that gates the admin API. A stand-in for real user auth. */
|
|
659
940
|
adminToken?: string;
|
|
660
941
|
/**
|
|
@@ -679,6 +960,26 @@ interface ControlOptions {
|
|
|
679
960
|
*/
|
|
680
961
|
declare function createControlApp(options: ControlOptions): Hono;
|
|
681
962
|
|
|
963
|
+
/** Where and how to compute embeddings — an OpenAI-compatible `/embeddings` endpoint. */
|
|
964
|
+
interface EmbeddingConfig {
|
|
965
|
+
endpoint: string;
|
|
966
|
+
apiKey: string;
|
|
967
|
+
model: string;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Embed a batch of texts. Sends them in bounded requests so a large ingest
|
|
971
|
+
* doesn't build one enormous body. Returns one vector per input, in order.
|
|
972
|
+
*/
|
|
973
|
+
declare function embedTexts(texts: string[], config: EmbeddingConfig, fetchImpl?: typeof fetch, batchSize?: number): Promise<number[][]>;
|
|
974
|
+
|
|
975
|
+
/** Everything the retrieval tool needs, injected by the runner from the app. */
|
|
976
|
+
interface KnowledgeDeps {
|
|
977
|
+
store: KnowledgeStore;
|
|
978
|
+
embedding: EmbeddingConfig;
|
|
979
|
+
/** Override the embed call in tests. */
|
|
980
|
+
embedImpl?: typeof embedTexts;
|
|
981
|
+
}
|
|
982
|
+
|
|
682
983
|
/**
|
|
683
984
|
* The per-turn handle an API tool uses to run itself in the user's browser
|
|
684
985
|
* instead of on Frankie's server. `available` says whether a live widget
|
|
@@ -713,7 +1014,23 @@ interface RunTurnParams {
|
|
|
713
1014
|
signal: AbortSignal;
|
|
714
1015
|
/** Called with the full transcript once the turn settles, to persist it. */
|
|
715
1016
|
onTranscript?: (messages: AgentMessage[]) => void;
|
|
1017
|
+
/** Receives a human-escalation when the built-in escalation tool is called. */
|
|
1018
|
+
onEscalation?: EscalationSink;
|
|
716
1019
|
onWarning?: (message: string, detail: Record<string, unknown>) => void;
|
|
1020
|
+
/** Knowledge-base index + embedding config for the docs-search tool. */
|
|
1021
|
+
knowledge?: KnowledgeDeps;
|
|
1022
|
+
/**
|
|
1023
|
+
* Pause every action that changes data until the user approves it. Set when
|
|
1024
|
+
* the workspace asks for it and the chat can show the approval card.
|
|
1025
|
+
*/
|
|
1026
|
+
confirmWrites?: boolean;
|
|
1027
|
+
/** The user's answer to the action the previous turn paused. */
|
|
1028
|
+
decision?: {
|
|
1029
|
+
action: PendingAction;
|
|
1030
|
+
approved: boolean;
|
|
1031
|
+
};
|
|
1032
|
+
/** Called when this turn pauses an action for approval, so the caller can keep it. */
|
|
1033
|
+
onPendingAction?: (action: PendingAction) => void;
|
|
717
1034
|
}
|
|
718
1035
|
/**
|
|
719
1036
|
* Run one user turn and stream it back as widget-facing events.
|
|
@@ -733,6 +1050,8 @@ interface McpConnection {
|
|
|
733
1050
|
name: string;
|
|
734
1051
|
error: string;
|
|
735
1052
|
}>;
|
|
1053
|
+
/** Tools that may change data: every tool its server doesn't mark `readOnlyHint`. */
|
|
1054
|
+
writes: Set<string>;
|
|
736
1055
|
close(): Promise<void>;
|
|
737
1056
|
}
|
|
738
1057
|
interface ConnectMcpOptions {
|
|
@@ -779,6 +1098,11 @@ interface ApiToolSet {
|
|
|
779
1098
|
}>;
|
|
780
1099
|
/** One entry per source that built successfully, for chat-facing visibility. */
|
|
781
1100
|
apis: ApiToolInventory[];
|
|
1101
|
+
/** Tools that change data (any method but GET), by tool name, for the approval card. */
|
|
1102
|
+
writes: Map<string, {
|
|
1103
|
+
method: string;
|
|
1104
|
+
path: string;
|
|
1105
|
+
}>;
|
|
782
1106
|
}
|
|
783
1107
|
/**
|
|
784
1108
|
* Turn a tenant's OpenAPI document into agent tools.
|
|
@@ -808,6 +1132,10 @@ interface CollectToolsOptions {
|
|
|
808
1132
|
accountApiUrl?: string;
|
|
809
1133
|
/** Browser channel for API sources marked `execution: "client"`. */
|
|
810
1134
|
clientBridge?: ClientToolBridge;
|
|
1135
|
+
/** Knowledge-base index + embedding config for the docs-search tool. */
|
|
1136
|
+
knowledge?: KnowledgeDeps;
|
|
1137
|
+
/** Receives a human-escalation when the built-in escalation tool is called. */
|
|
1138
|
+
onEscalation?: EscalationSink;
|
|
811
1139
|
onWarning?: (message: string, detail: Record<string, unknown>) => void;
|
|
812
1140
|
}
|
|
813
1141
|
interface CollectedTools {
|
|
@@ -819,6 +1147,14 @@ interface CollectedTools {
|
|
|
819
1147
|
}>;
|
|
820
1148
|
/** OpenAPI sources that built successfully this turn, for chat-facing visibility. */
|
|
821
1149
|
apis: ApiToolInventory[];
|
|
1150
|
+
/**
|
|
1151
|
+
* Tools that change data, by name, with the HTTP method and path when there
|
|
1152
|
+
* is one. These ask the user before they run (`agent.confirmWrites`).
|
|
1153
|
+
*/
|
|
1154
|
+
writes: Map<string, {
|
|
1155
|
+
method?: string;
|
|
1156
|
+
path?: string;
|
|
1157
|
+
}>;
|
|
822
1158
|
close(): Promise<void>;
|
|
823
1159
|
}
|
|
824
1160
|
/**
|
|
@@ -851,11 +1187,8 @@ interface ForwardingConfig {
|
|
|
851
1187
|
*/
|
|
852
1188
|
declare function buildForwardedHeaders(source: ForwardingConfig, session: ChatSession): Record<string, string>;
|
|
853
1189
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
* a web page and must never be able to redirect the agent.
|
|
858
|
-
*/
|
|
859
|
-
declare function buildSystemPrompt(config: FrankieConfig, session: ChatSession, availableTools?: string[]): string;
|
|
1190
|
+
declare function buildSystemPrompt(config: FrankieConfig, session: ChatSession, availableTools?: string[], options?: {
|
|
1191
|
+
confirmWrites?: boolean;
|
|
1192
|
+
}): string;
|
|
860
1193
|
|
|
861
1194
|
export { API_TOOL_PREFIX, type ApiToolSet, AuthError, type BuildApiToolsOptions, type ChatSession, type CollectToolsOptions, type CollectedTools, ConfigError, type ConnectMcpOptions, type ControlOptions, type ControlStore, type CreateAppOptions, FileTenantStore, type ForwardingConfig, MCP_TOOL_PREFIX, type McpConnection, type McpHostOptions, MemoryRateLimiter, MemorySessionStore, MemoryTenantStore, ModelResolutionError, OpenApiError, PostgresSessionStore, PostgresTenantStore, type PublicTenant, type Queryable, type RateLimiter, type ResolvedConfig, type ResolvedModel, type ResolvedStore, type ResolvedStores, type RunTurnParams, type SessionStore, SingleTenantStore, type TenantRecord, TenantRegistry, type TenantRuntime, type TenantStore, TokenService, type VerifiedUser, anonymousUser, applyEnvOverrides, buildApiTools, buildForwardedHeaders, buildSystemPrompt, collectTools, connectMcpTools, createControlApp, createFrankieApp, createMcpHostApp, isOriginAllowed, loadConfigFromFile, loadResolvedConfig, loadSpec, modelHasApi, newSessionId, resolveModel, resolveSecrets, resolveStores, resolveTenantStore, runTurn, safeEqual, toPublicTenant };
|