@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/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Hono } from 'hono';
2
- import { FrankieConfig, AuthConfig, WorkspaceSkill, HostToolRequest, HostToolResult, ChatAttachment, FrankieServerEvent, McpServerConfig, ToolPolicy, ApiToolInventory, ApiSourceConfig } from '@frankieai/core';
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. Its publishable key is its lookup key. */
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
- * The runtime's own operating instructions, followed by whatever the tenant
856
- * configured. Host-supplied page context is fenced off as data: it comes from
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 };