@firstlovecenter/ai-chat 0.1.1 → 0.2.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.
@@ -453,6 +453,36 @@ type ConfigureAiChatOpts<S = unknown> = {
453
453
  * - `onSessionEnd` — cleanup (always-runs, never throws out).
454
454
  */
455
455
  hooks?: AgentCustomHooks$1<S>;
456
+ /**
457
+ * Persona / role given to the AI as the very first cached system block
458
+ * on every turn. Use this to give the assistant a consistent voice and
459
+ * domain perspective across all conversations in your app.
460
+ *
461
+ * Accepts:
462
+ * - a `string` for a static org-wide role, or
463
+ * - a `(ctx) => string | Promise<string>` function for per-request
464
+ * variation (different role per scope, per locale, per A/B cohort).
465
+ *
466
+ * Token economics: the role text is sent on every turn (Vertex APIs
467
+ * are stateless), but it is marked `cached: true`. On Claude that
468
+ * becomes an ephemeral `cache_control` marker — first turn pays ~1.25×
469
+ * for the cache write; subsequent turns within ~5 min pay ~0.1× for
470
+ * cache reads. On Gemini the hint is informational; Vertex auto-caches
471
+ * stable prefixes regardless. For a typical 200–500 token role, the
472
+ * per-turn marginal cost after the first is negligible. The package
473
+ * caps total `cache_control` markers at Anthropic's 4-per-request
474
+ * limit; if the host already marks 4 blocks cached, the extras
475
+ * (including this one if last) silently drop the cache hint rather
476
+ * than reject the request.
477
+ *
478
+ * Example (static):
479
+ * rolePrompt: `You are the head pastor of First Love Center, a
480
+ * multinational church operating across multiple countries. Frame
481
+ * answers from the perspective of advancing the gospel through
482
+ * sustainable growth, financial discipline, compliance, and church
483
+ * planting. Be direct, pastoral, and action-oriented.`
484
+ */
485
+ rolePrompt?: string | ((ctx: ToolContext<S>) => string | Promise<string>);
456
486
  };
457
487
  type AiChatRuntime<S = unknown> = {
458
488
  /**
@@ -453,6 +453,36 @@ type ConfigureAiChatOpts<S = unknown> = {
453
453
  * - `onSessionEnd` — cleanup (always-runs, never throws out).
454
454
  */
455
455
  hooks?: AgentCustomHooks$1<S>;
456
+ /**
457
+ * Persona / role given to the AI as the very first cached system block
458
+ * on every turn. Use this to give the assistant a consistent voice and
459
+ * domain perspective across all conversations in your app.
460
+ *
461
+ * Accepts:
462
+ * - a `string` for a static org-wide role, or
463
+ * - a `(ctx) => string | Promise<string>` function for per-request
464
+ * variation (different role per scope, per locale, per A/B cohort).
465
+ *
466
+ * Token economics: the role text is sent on every turn (Vertex APIs
467
+ * are stateless), but it is marked `cached: true`. On Claude that
468
+ * becomes an ephemeral `cache_control` marker — first turn pays ~1.25×
469
+ * for the cache write; subsequent turns within ~5 min pay ~0.1× for
470
+ * cache reads. On Gemini the hint is informational; Vertex auto-caches
471
+ * stable prefixes regardless. For a typical 200–500 token role, the
472
+ * per-turn marginal cost after the first is negligible. The package
473
+ * caps total `cache_control` markers at Anthropic's 4-per-request
474
+ * limit; if the host already marks 4 blocks cached, the extras
475
+ * (including this one if last) silently drop the cache hint rather
476
+ * than reject the request.
477
+ *
478
+ * Example (static):
479
+ * rolePrompt: `You are the head pastor of First Love Center, a
480
+ * multinational church operating across multiple countries. Frame
481
+ * answers from the perspective of advancing the gospel through
482
+ * sustainable growth, financial discipline, compliance, and church
483
+ * planting. Be direct, pastoral, and action-oriented.`
484
+ */
485
+ rolePrompt?: string | ((ctx: ToolContext<S>) => string | Promise<string>);
456
486
  };
457
487
  type AiChatRuntime<S = unknown> = {
458
488
  /**
@@ -144,11 +144,19 @@ var ClaudeToolProvider = class {
144
144
  patchVertexBuildRequestSync(this.client);
145
145
  }
146
146
  async runTurn(input) {
147
- const system = input.system.map((b) => ({
148
- type: "text",
149
- text: b.text,
150
- ...b.cached ? { cache_control: { type: "ephemeral" } } : {}
151
- }));
147
+ let cacheMarkersUsed = 0;
148
+ const MAX_CACHE_MARKERS = 4;
149
+ const system = input.system.map((b) => {
150
+ if (b.cached && cacheMarkersUsed < MAX_CACHE_MARKERS) {
151
+ cacheMarkersUsed++;
152
+ return {
153
+ type: "text",
154
+ text: b.text,
155
+ cache_control: { type: "ephemeral" }
156
+ };
157
+ }
158
+ return { type: "text", text: b.text };
159
+ });
152
160
  const messages = toAnthropicMessages(input.messages);
153
161
  const response = await this.client.messages.create({
154
162
  model: this.modelId,
@@ -1391,6 +1399,16 @@ function configureAiChat(opts) {
1391
1399
  ];
1392
1400
  const getProvider = (id) => toolProviders2.find((p) => p.id === id) ?? getToolProvider(id);
1393
1401
  const chatInterfaces = opts.chatInterfaces ?? BUILTIN_CHAT_INTERFACE_IDS.map((id) => ({ id }));
1402
+ const tools = opts.rolePrompt ? {
1403
+ tools: opts.tools.tools,
1404
+ async buildSystemBlocks(ctx) {
1405
+ const inner = await opts.tools.buildSystemBlocks(ctx);
1406
+ const rolePrompt = opts.rolePrompt;
1407
+ const role = typeof rolePrompt === "function" ? await rolePrompt(ctx) : rolePrompt;
1408
+ if (!role || !role.trim()) return inner;
1409
+ return [{ text: role, cached: true }, ...inner];
1410
+ }
1411
+ } : opts.tools;
1394
1412
  const runAgentBound = async ({
1395
1413
  question,
1396
1414
  ctx,
@@ -1413,11 +1431,11 @@ function configureAiChat(opts) {
1413
1431
  modelIds: opts.vertex.modelIds,
1414
1432
  location: location ?? settings.gcpLocation
1415
1433
  });
1416
- const systemBlocks = await opts.tools.buildSystemBlocks(ctx);
1434
+ const systemBlocks = await tools.buildSystemBlocks(ctx);
1417
1435
  const input = {
1418
1436
  question,
1419
1437
  ctx,
1420
- tools: opts.tools.tools,
1438
+ tools: tools.tools,
1421
1439
  systemBlocks,
1422
1440
  provider,
1423
1441
  maxToolTurns,
@@ -1433,7 +1451,7 @@ function configureAiChat(opts) {
1433
1451
  persistence: opts.persistence,
1434
1452
  auth: opts.auth,
1435
1453
  scope: opts.scope,
1436
- tools: opts.tools,
1454
+ tools,
1437
1455
  vertex: opts.vertex,
1438
1456
  logger: opts.logger,
1439
1457
  resolveNarratorId: opts.resolveNarratorId,