diff --git a/packages/functions/doc/functions/scheduling/cron.md b/packages/functions/doc/functions/scheduling/cron.md
index 9eee297e..9078144c 100644
--- a/packages/functions/doc/functions/scheduling/cron.md
+++ b/packages/functions/doc/functions/scheduling/cron.md
@@ -16,7 +16,7 @@ const start = new Tempo('2026-07-01T08:00:00Z');
// Every 5 minutes between 9 AM and 5 PM, Monday-Friday
const next = nextCron(start, '*/5 9-17 * * 1-5');
-console.log(next.format('{hhmiss}')); // '09:00:00'
+console.log(next.format('{hh}:{mi}:{ss}')); // '09:00:00'
```
### `prevCron`
@@ -31,7 +31,7 @@ const start = new Tempo('2026-07-01T18:00:00Z');
// Every 5 minutes between 9 AM and 5 PM, Monday-Friday
const prev = prevCron(start, '*/5 9-17 * * 1-5');
-console.log(prev.format('{hhmiss}')); // '17:55:00'
+console.log(prev.format('{hh}:{mi}:{ss}')); // '17:55:00'
```
### `parseCron`
diff --git a/packages/plugins/.setup/catalog.json b/packages/plugins/.setup/catalog.json
index e31c38e5..1644885b 100644
--- a/packages/plugins/.setup/catalog.json
+++ b/packages/plugins/.setup/catalog.json
@@ -45,7 +45,7 @@
"description": "Tempo community plugin for LLM-powered natural language processing and parsing.",
"packageName": "@magmacomputing/tempo-plugin-ai",
"plan": "community",
- "status": "experimental"
+ "status": "active"
},
{
"id": "ticker",
diff --git a/packages/plugins/.std/src/term.quarter.ts b/packages/plugins/.std/src/term.quarter.ts
index 8440f6c9..f22b5ab7 100644
--- a/packages/plugins/.std/src/term.quarter.ts
+++ b/packages/plugins/.std/src/term.quarter.ts
@@ -18,7 +18,7 @@ const groups = defineRange([
/** resolve the full candidate list for the current context */
function resolve(t: Tempo, anchor?: any): any[] {
- if (t.config.sphere === undefined && anchor?.sphere === undefined) {
+ if (t.sphere === undefined && anchor?.sphere === undefined) {
logWarn(`[tempo] QuarterTerm requires 'sphere' configuration (e.g. Tempo.init({ sphere: 'north' }) or { sphere: 'south' }).`, t.config);
return [];
}
diff --git a/packages/plugins/.std/src/term.season.ts b/packages/plugins/.std/src/term.season.ts
index 222ec400..441e8b01 100644
--- a/packages/plugins/.std/src/term.season.ts
+++ b/packages/plugins/.std/src/term.season.ts
@@ -18,7 +18,7 @@ const groups = defineRange([
/** resolve the full candidate list for the current context */
function resolve(t: Tempo, anchor?: any) {
- if (t.config.sphere === undefined && anchor?.sphere === undefined) {
+ if (t.sphere === undefined && anchor?.sphere === undefined) {
logWarn(`[tempo] SeasonTerm requires 'sphere' configuration (e.g. Tempo.init({ sphere: 'north' }) or { sphere: 'south' }).`, t.config);
return [];
}
diff --git a/packages/plugins/ai/CHANGELOG.md b/packages/plugins/ai/CHANGELOG.md
index 1f1864ca..664e1e13 100644
--- a/packages/plugins/ai/CHANGELOG.md
+++ b/packages/plugins/ai/CHANGELOG.md
@@ -5,9 +5,10 @@ All notable changes to the `@magmacomputing/tempo-plugin-ai` project will be doc
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## [0.3.0] - 2026-08-10
+## [1.0.0] - 2026-08-15
### Added
+- **Cache Eviction Synchronization**: Enhanced `aiCache.clear()` to flush matching keys and prefixes across both `Tempo.cache` and custom `AiCacheAdapter` storage engines, returning `Promise` to await async adapter eviction.
- **Temporal Difference & Relative Grounding (`diffAI`)**: Added natural language temporal difference calculation and narrative summarization between two `Tempo` points, dates, or timestamps.
- Pre-computes mathematical grounding metrics (`calendarDays`, `elapsedHours`, `businessDays` with weekend and holiday exclusion) to provide strict arithmetic backing for LLM narrative formatting.
- Supports domain-specific delta formatting (e.g. accounting terms, working days, human relative explanations, or business SLAs).
@@ -35,7 +36,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Cascading Cache TTL Hierarchy**: Implemented strict 4-tier TTL resolution (`options.ttl` > `provider.ttl` > `global config.ttl` > default 1 hour / 3,600,000 ms for `parseAI` or 24 hours / 86,400,000 ms for context/difference handlers) for fine-grained cache entry expiration control on stores enforcing TTL.
- **Fail-Open Storage Resilience**: Custom cache adapter errors during `get()` or `set()` operations fail open silently to direct LLM resolution, preserving application request uptime.
- **Dynamic Remote Provider Manifest (`loadRemoteManifest`)**: Lazily fetches remote provider defaults (`providers.v1.json`) on initialization with a 1500ms timeout and automatic air-gapped fallback to compiled `DEFAULT_PROVIDERS`.
-- **Cache Eviction Synchronization**: Enhanced `clearAiCache()` to flush matching keys and prefixes across both `Tempo.cache` and custom `AiCacheAdapter` storage engines, returning `Promise` to await async adapter eviction.
### Changed & Hardened
- **Consensus Mode TTL Resolution**: Fixed a runtime bug where standard provider TTL lookups failed in Consensus mode due to the synthetic sentinel provider ID (`'consensus'`), which caused lookups on the winning provider array to return undefined. Now reduces over all participating provider configs to select the minimum (most conservative) TTL.
@@ -65,11 +65,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Non-Destructive Glossary Appending**: Custom glossaries provided via `initAI({ cache })` are safely appended to `Tempo.cache` as static immortal terms without destructive overrides.
- **Silent Native Pre-Parsing & Cache Controls**: `parseAI` attempts fast, zero-latency native `Tempo` resolution and checks `Tempo.cache` before initiating LLM network calls. Supports `cache: false` to bypass cache lookups and `force: true` to force a fresh LLM API request.
- **Anchor Instance Reuse & Cache Salting**: Reuses anchor `Tempo` instances to minimize memory allocations and salts cache keys with the anchor's date and system context (`timeZone`, `calendar`, `locale`, `sphere`), preventing stale cache hits across midnight boundaries or context shifts.
-- **Resilient Cache Invalidation**: Normalized cache key input (whitespace trimming and case insensitivity) for `clearAiCache` and internal lookups.
+- **Resilient Cache Invalidation**: Normalized cache key input (whitespace trimming and case insensitivity) for `aiCache.clear()` and internal lookups.
## [0.1.0] - 2026-07-26
### Added
- Initial scaffolding of the AI natural language parsing plugin.
-- Functional exports for `parseAI`, `initAI`, and `clearAiCache`.
+- Functional exports for `parseAI`, `initAI`, and `aiCache`.
- Initial provider fallback-routing engine supporting HTTP requests to configured LLM provider endpoints.
diff --git a/packages/plugins/ai/README.md b/packages/plugins/ai/README.md
index cd51be0a..adeb6d3c 100644
--- a/packages/plugins/ai/README.md
+++ b/packages/plugins/ai/README.md
@@ -48,11 +48,13 @@ console.log(dt.ai?.confidence); // 0.98
| Endpoint | Description | Doc |
| :--- | :--- | :---: |
-| **`parseAI`** | Parse relative/point-in-time dates (e.g. *"next Friday at 4pm"*) | |
-| **`recurrenceAI`** | Convert repeating patterns (e.g. *"every 2 weeks on Friday"*) to RRULEs | |
-| **`diffAI`** | Calculate natural language difference & business days between dates | |
-| **`scheduleAI`** | Book appointment slots around busy calendar event bounds | |
-| **`contextAI`** | Infer timezone, locale, and calendar from user profiles/bios | |
+| **`parseAI`** | Parse relative/point-in-time dates (e.g. *"next Friday at 4pm"*) | |
+| **`formatAI`** | Format contextual narrative dates & relative countdowns | |
+| **`extractAI`** | Extract embedded temporal entities & calendar events from prose | |
+| **`recurrenceAI`** | Convert repeating patterns (e.g. *"every 2 weeks on Friday"*) to RRULEs | |
+| **`scheduleAI`** | Book appointment slots around busy calendar event bounds | |
+| **`diffAI`** | Calculate natural language difference & business days between dates | |
+| **`contextAI`** | Infer timezone, locale, and calendar from user profiles/bios | |
---
diff --git a/packages/plugins/ai/doc/architecture.md b/packages/plugins/ai/doc/architecture.md
index 8f7e7428..f6691dad 100644
--- a/packages/plugins/ai/doc/architecture.md
+++ b/packages/plugins/ai/doc/architecture.md
@@ -75,43 +75,164 @@ By default, `@magmacomputing/tempo-plugin-ai` lazily fetches provider defaults (
### Frontend Security Warning
> [!CAUTION]
-> **Never** expose a raw LLM API key in a client-side browser bundle (like React or Vue) or store it in browser storage (`localStorage`, `sessionStorage`, `IndexedDB`, or browser cache). Any Cross-Site Scripting (XSS) vulnerability, compromised NPM package, or malicious browser extension can easily inspect client-side storage and steal secret keys, leading to quota exhaustion, billing fraud, or permanent provider bans. BYOK keys are *only* safe on backend servers or edge workers.
+> **Never** expose a raw LLM API key in a client-side browser bundle (like React, Vue, or Svelte) or store it in browser storage (`localStorage`, `sessionStorage`, `IndexedDB`, or browser cache). Any Cross-Site Scripting (XSS) vulnerability, compromised NPM dependency, or malicious browser extension can inspect client-side memory/storage and extract secret keys, leading to quota drainage, unexpected billing spikes, or account bans. BYOK provider keys are *only* safe on backend servers and edge workers.
-## The Proxy Architecture
+## Browser & Client-Side Proxy Architecture
-If you need to execute AI functions directly on a public frontend application, you must route requests through a secure backend proxy.
+To execute AI functions within client-side browser applications safely, route requests through a secure self-hosted backend proxy or unified AI Gateway (such as a Cloudflare Worker, Next.js API route, Express server, OpenRouter, Portkey, or LiteLLM):
-A standard proxy architecture (e.g. using Cloudflare Workers or a custom Node/Express backend) involves:
-1. **Frontend Request**: The browser sends the prompt or temporal data to your own backend API (e.g., `/api/parse-date`).
-2. **Backend Authentication**: Your API validates the user's session or API token to prevent abuse.
-3. **LLM Inference**: Your backend runs the Tempo AI function (such as `parseAI`) using your securely stored BYOK keys.
-4. **Response**: Your backend returns the resulting ISO 8601 string to the frontend, where it can be instantiated into a native `Tempo` object.
+```mermaid
+flowchart LR
+ subgraph Browser ["Client-Side Browser (SPA)"]
+ Client["Tempo AI Plugin (initAI / parseAI / diffAI)"]
+ end
-Because LLM API calls typically take ~300-800ms, the ~20ms overhead of routing the request through your own backend proxy is negligible.
+ subgraph Backend ["Self-Hosted Proxy / AI Gateway"]
+ Proxy["Your Backend API / AI Gateway • User Authentication & Rate Limits • Secure Secret Management"]
+ end
-## Fallback Loops & Execution Modes
+ subgraph Providers ["Upstream LLM Providers"]
+ LLM["Groq • OpenAI • Gemini • Anthropic"]
+ end
-Because third-party APIs can experience downtime or aggressive rate limiting, the plugin supports flexible multi-provider execution strategies:
-
-### 1. Fallback Mode (Default)
-When configured with multiple providers in `initAI()`, AI functions execute requests sequentially. If the primary provider hits a timeout or a `429 Too Many Requests` limit, the plugin instantly and silently fails over to the next provider in the array. Rate limit headers are updated based on the successful provider response or error resolution.
+ Client -- "1. HTTPS (TLS 1.2+) Bearer Token / Auth Header" --> Proxy
+ Proxy -- "2. HTTPS (TLS 1.2+) Private Provider API Key" --> LLM
+ LLM -- "3. HTTPS (TLS 1.2+) Raw JSON Completion" --> Proxy
+ Proxy -- "4. HTTPS (TLS 1.2+) Validated Payload" --> Client
+```
-### 2. Race Mode (`mode: 'race'`)
-Dispatches requests to all available providers simultaneously using `Promise.allSettled`. Returns the fastest resolving provider response to minimize user-perceived latency.
+### 1. Browser Configuration Example
+Configure `initAI` in your browser code to target your backend proxy or AI Gateway URL:
```typescript
-const result = await parseAI("Thanksgiving 2026", { mode: 'race' });
+import { initAI, parseAI } from '@magmacomputing/tempo-plugin-ai';
+
+// Safe for browser deployment: No private LLM API keys are bundled
+await initAI({
+ providers: [
+ {
+ id: 'my-gateway',
+ url: 'https://api.mycompany.com/v1/ai/chat/completions', // Your secure proxy endpoint
+ key: userSessionToken, // Short-lived user Bearer JWT token
+ model: 'llama-3.3-70b-instruct'
+ }
+ ]
+});
+
+// All Tempo AI functions now execute securely through your proxy
+const date = await parseAI("Team standup next Wednesday at 9:30am");
```
-### 3. Consensus Mode (`mode: 'consensus'`)
-Executes all providers concurrently. If multiple providers agree on the resolved ISO timestamp, confidence score is boosted (to `1.0`) and the consensus result is returned. Rate limits are applied from the consensus provider.
+### 2. Backend Proxy Handler Example (Next.js / Cloudflare Worker / Express)
+Your backend endpoint receives the request, validates the user's session, enforces ingress quotas, attaches your private LLM API key, and forwards the validated payload to the upstream provider:
```typescript
-const result = await parseAI("The penultimate Tuesday before Thanksgiving", {
- mode: 'consensus',
- minConfidence: 0.85
-});
+// Example: Next.js API Route / Cloudflare Worker / Express Proxy Handler
+export async function POST(req: Request, env?: { GROQ_API_KEY?: string }) {
+ // 1. Authenticate user session
+ const authHeader = req.headers.get('Authorization');
+ const session = await validateUserSession(authHeader);
+ if (!session) {
+ return new Response('Unauthorized', { status: 401 });
+ }
+
+ // 2. Ingress validation & per-user quota enforcement
+ const body = await req.json();
+ if (typeof body?.prompt !== 'string' || body.prompt.length > 4096) {
+ return new Response('Invalid prompt or payload exceeds size limit', { status: 400 });
+ }
+ if (!checkUserRateLimit(session.userId)) {
+ return new Response('Too Many Requests', { status: 429 });
+ }
+
+ // 3. Resolve API key (Cloudflare Worker env binding or Node/Next.js process.env)
+ const apiKey = env?.GROQ_API_KEY || (typeof process !== 'undefined' ? process.env?.GROQ_API_KEY : undefined);
+ if (!apiKey) {
+ return new Response('Provider key configuration missing', { status: 500 });
+ }
+
+ // 4. Construct upstream fetch with bounded timeout and cleanup
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s upstream limit
+
+ try {
+ const upstreamResponse = await fetch('https://api.groq.com/openai/v1/chat/completions', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${apiKey}`
+ },
+ body: JSON.stringify({
+ model: 'openai/gpt-oss-120b',
+ messages: body.messages,
+ temperature: 0.1,
+ }),
+ signal: controller.signal
+ });
+
+ // 5. Return provider payload to client
+ const data = await upstreamResponse.json();
+ return new Response(JSON.stringify(data), {
+ status: upstreamResponse.status,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ } catch (err: any) {
+ if (err.name === 'AbortError' || controller.signal.aborted) {
+ return new Response(JSON.stringify({ error: 'Upstream provider gateway timeout' }), {
+ status: 504,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+ return new Response(JSON.stringify({ error: 'Upstream connection failure' }), {
+ status: 502,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ } finally {
+ clearTimeout(timeoutId);
+ }
+}
```
+---
+
+## 🔒 Security & Privacy Guarantees
+
+> [!TIP]
+> For an in-depth breakdown of our automated PII redaction, Smart Debug infrastructure, and tamper-resistant Proxy introspection, see the dedicated **[Security & Privacy Architecture Guide (`security.md`)](./security.md)**.
+
+Whether running directly on backend servers (Node.js, Deno, Bun, Edge Workers) or through a client-side browser proxy, `@magmacomputing/tempo-plugin-ai` enforces strict security and privacy standards:
+
+### 1. Transport Security (HTTPS / TLS)
+All network communication—both from client to proxy and from proxy/server to upstream LLM endpoints—is required over HTTPS. Negotiated TLS versions (such as TLS 1.2 or TLS 1.3) depend on deployment environment and server configuration unless strictly enforced by your reverse proxy. Plaintext HTTP endpoints are disallowed in production environments (permitted only on `localhost` during local development).
+
+### 2. Ephemeral Processing & Cache Retention Controls
+Temporal processing payloads (dates, times, context snippets, prompts) are processed ephemerally. The plugin does not send telemetry or store user prompt data on external analytics servers. However, functions supporting caching (e.g. `parseAI`, `formatAI`, `diffAI`) may retain prompt-derived cache keys and final results in local memory or configured custom cache adapters according to the resolved TTL. Requests requiring zero cache retention must explicitly pass `cache: false`.
+
+### 3. In-Memory Credential Redaction & Immutability
+* **Automated Key Redaction**: Calling `getAiConfig()` returns a sanitized, deeply read-only snapshot of active configurations with all provider `key` values replaced with `[REDACTED]`, preventing accidental exposure in log files, APM traces, or crash dumps.
+* **Frozen Metadata**: All diagnostic metadata attached to `Tempo` instances via `.ai` and all structured AI result objects (`TempoAiFormatResult`, `TempoAiExtractResult`, `TempoAiDiffResult`, `TempoScheduleResult`, `TempoRecurrenceResult`, `TempoContext`) are deeply frozen with `Object.freeze()` and guarded via runtime `Proxy` wrappers, protecting against direct runtime mutation.
+
+### 4. Deterministic Schema Guardrails & Confidence Range Verification
+All LLM prompts are paired with rigid, machine-verifiable JSON schemas. Responses undergo strict boundary validation, regex parsing, confidence range verification (`0.0` to `1.0`), and schema verification before any native `Tempo` date object or structured result payload is instantiated. If an LLM returns malformed, out-of-range, or unparseable data, the plugin throws a typed `TempoAiError` or triggers automatic provider fallback rather than silently propagating corrupt data.
+
+### 5. Partitioned Caching & Fail-Open Storage Resilience
+* **Strict Cache Key Partitioning**: Caches are namespaced and hashed (`ai:::...`) with timezone, locale, calendar, and anchor date isolation to prevent cross-tenant or cross-regional cache poisoning.
+* **Fail-Open Protection**: If a custom distributed cache adapter (e.g. Redis or Cloudflare KV) encounters network disruption or errors, the plugin automatically fails open to direct LLM resolution, preserving application uptime.
+
+## Multi-Provider Execution Strategies (`AiMode`)
+
+Because third-party APIs can experience downtime, latency spikes, or quota exhaustion, `@magmacomputing/tempo-plugin-ai` provides six dedicated dispatch strategies configured via `AiMode` (or string literals):
+
+| Strategy | Enum (`AiMode`) | Primary Advantage | Typical Use Case |
+| :--- | :--- | :--- | :--- |
+| **Fallback** *(Default)* | `AiMode.Fallback` | Minimum token cost (sequential cascade) | Default production baseline & background tasks |
+| **Hedged** | `AiMode.Hedged` | Ultra-fast latency with low token overhead (~1.15x) | Latency-sensitive interactive search & chatbots |
+| **RoundRobin** | `AiMode.RoundRobin` | Cyclic rotation across multi-key pools | High-throughput batch ingestion across API keys |
+| **Adaptive** | `AiMode.Adaptive` | Telemetry-driven rate-limit avoidance | Multi-tier provider pools with mixed quotas |
+| **Race** | `AiMode.Race` | Absolute minimum response latency | Real-time typeahead & autocomplete |
+| **Consensus** | `AiMode.Consensus` | Cross-LLM verification & hallucination trapping | High-stakes legal, financial, and contract dates |
+
+👉 For detailed architecture breakdowns, Mermaid decision trees, and configuration guides for each mode, see the **[Multi-Provider Execution Modes Guide (`modes.md`)](./modes.md)**.
+
### Provider ID Canonicalization
Provider IDs are normalized case-insensitively during `initAI` lookup (e.g. `'Gemini'`, `'gemini'`, `'OpenAI'`), automatically applying default endpoints and models while preserving the caller's registered identifier for logging and metadata.
diff --git a/packages/plugins/ai/doc/context.md b/packages/plugins/ai/doc/context.md
index 94b76beb..663d4560 100644
--- a/packages/plugins/ai/doc/context.md
+++ b/packages/plugins/ai/doc/context.md
@@ -1,63 +1,126 @@
-# Context & Natural Language Parsing
+# `contextAI` — Context & Regional Inference
-Because natural language dates are entirely relative (e.g., "next Tuesday") and often geographically ambiguous (e.g., "11/12"), an LLM cannot reliably parse them in a vacuum.
+`contextAI()` is designed to analyze unstructured or ambiguous text (e.g. user biographies, locations, or email bodies) and infer their regional configuration settings. It resolves these properties to a standard configuration object containing timezone, locale, calendar system, and hemisphere.
-The Tempo AI plugin solves this by automatically wrapping your input with rich environmental context before sending it to the LLM.
+> [!TIP]
+> **Smart Debug Telemetry**: Enabling `debug: true` activates operational logs. In production environments (`NODE_ENV === 'production'`), PII (emails, phone numbers, auth tokens) is automatically sanitized and masked in console output and terminal inspections. See the [Security & Privacy Architecture Guide](./security.md).
-## Geographic Context
+This is highly useful for user onboarding settings, automatic context mapping for calendar syncs, and geolocating inputs dynamically without maintaining static geographic mapping tables.
-The plugin automatically reads from the global `Tempo.config` to fetch the default TimeZone, Calendar, and Locale, and establishes the "current anchor time" the moment you call it.
+---
-Along with your string, the plugin passes a hidden context payload to the LLM:
-*`Current Time: [Anchor], Timezone: [TZ], Calendar: [Cal], Locale: [Locale], Hemisphere: [Sphere]`*
+## Basic Usage
-### Overriding Context
-You can explicitly override any of these global settings on a per-request basis by passing an `options` object as the second argument, identical to how you pass options to a standard `new Tempo()` constructor:
+> [!NOTE]
+> Like all Tempo AI functions, `contextAI` requires prior initialization with at least one active provider via `initAI()`.
```typescript
-// Explicitly evaluate this complex query from the perspective of September 1st
-const dt = await parseAI("The penultimate Tuesday before Thanksgiving", { anchor: '2026-09-01T00:00:00Z' });
-
-// Explicitly parse assuming a Japanese locale and timezone
-const tokyoDt = await parseAI("The second Sunday of May", { locale: 'ja-JP', timeZone: 'Asia/Tokyo' });
+import { initAI, contextAI } from '@magmacomputing/tempo-plugin-ai';
+
+// 1. Configure the AI provider farm
+await initAI({
+ providers: [
+ { id: 'groq', key: process.env.GROQ_API_KEY }
+ ]
+});
+
+// 2. Infer contextual settings from unstructured text
+const context = await contextAI("I'm a photographer based in Sydney, Australia.");
+
+console.log(context.timeZone); // "Australia/Sydney"
+console.log(context.locale); // "en-AU"
+console.log(context.calendar); // "gregory"
+console.log(context.sphere); // "south"
+console.log(context.confidence);// 0.98
```
-### Why Locale is Critical
-Passing the `Locale` is absolutely critical for the LLM to know whether "11/12" means November 12th (US format) or 11th of December (UK/EU format). The plugin handles this transparently based on your standard Tempo configuration!
+---
+
+## Configuration Options (`AiContextOptions`)
-> [!WARNING]
-> **Calendar Math Hallucinations**: LLMs are language predictors, not calculators. While they excel at parsing conversational times (like `"tomorrow at 5pm"`), smaller models are notoriously prone to hallucinations on complex, cross-year calendar math. For example, asking a lightweight model for `"Thanksgiving in 2026"` may result in a hallucinated day of the week because the model doesn't natively compute "the fourth Thursday of November 2026." If your application relies on heavy holiday logic or complex multi-year math, you *must* use a capable frontier model or rely on deterministic plugins instead of AI.
+| Option | Type | Description |
+| :--- | :--- | :--- |
+| **`force`** | `boolean` | If true, bypasses the cache to initiate a fresh LLM query. |
+| **`cache`** | `boolean` | If false, disables writing to and reading from cache adapters. |
+| **`cacheAdapter`** | `AiCacheAdapter` | Custom cache engine (e.g., Redis) for caching results on this request. |
+| **`ttl`** | `number` | Time-to-live override in milliseconds for cached results. |
+| **`minConfidence`**| `number` | Minimum confidence score threshold (0.0 to 1.0) required to return a valid slot. Throws `TempoAiError(422)` if lower. |
+| **`mode`** | `AiMode` | Concurrency routing strategy (`fallback`, `race`, `consensus`, `hedged`, `roundrobin`, `adaptive`). Refer to the [Multi-Provider Execution Modes Guide](./modes.md). |
+| **`providers`** | `AiProvider[]` | Per-request provider configuration overrides. |
+| **`timeout`** | `number` | Per-request timeout in milliseconds (overrides provider and global timeouts). |
+| **`hedgeDelay`** | `number` | Delay in milliseconds before initiating speculative hedging in `AiMode.Hedged` (default: `800ms`). |
+| **`debug`** | `boolean` | If true, logs prompt context and cache operations to console (automatically PII-sanitized in production). |
+| **`softErrors`** | `boolean` | If true, returns `TempoAiError` into array index position instead of rejecting the entire batch query. |
-## The Decoupled Output Bridge
+---
-To ensure deterministic behavior, the LLM is instructed to *only* return strict ISO 8601 strings.
+## Result Schema (`TempoContext`)
-The plugin executes the network request, the LLM returns a local ISO string without a timezone offset or 'Z' suffix (like `"2026-11-26T00:00:00"`), and the plugin immediately passes that string back into the native `new Tempo()` constructor. The provider response must omit timezone suffixes to match the local ISO contract enforced by Tempo AI functions. The developer seamlessly receives a valid, native `Tempo` instance. This eliminates AST-construction ambiguity, creating a decoupled bridge between AI text generation and native Tempo conversion.
+```typescript
+export interface TempoContext {
+ /** Inferred IANA time zone identifier (e.g. 'America/New_York') */
+ timeZone: string;
+
+ /** Inferred BCP 47 language/region tag (e.g. 'en-US') */
+ locale: string;
+
+ /** Inferred Unicode calendar system type (e.g. 'gregory') */
+ calendar: string;
+
+ /** Inferred hemisphere, or undefined if ambiguous */
+ sphere?: 'north' | 'south' | undefined;
+
+ /** Confidence rating from 0.0 (unparseable) to 1.0 (certain) */
+ confidence: number;
+
+ /** Resolution source ('cache' or provider ID like 'groq', 'gemini', 'openai') */
+ provider: string;
+
+ /** Step-by-step reasoning or justification provided by the engine/LLM */
+ reasoning?: string | undefined;
+}
+```
-### Relative Date Ambiguity Tie-Breakers
+---
-To eliminate model variance on idioms like "Next Friday" or "Last Tuesday", the plugin enforces static system prompt ambiguity rules:
-* `"next [weekday/unit]"`: Evaluated as the immediate next chronological occurrence after `Current Time`.
-* `"last [weekday/unit]"` / `"previous [weekday/unit]"`: Evaluated as the most recent past occurrence prior to `Current Time`.
-* `"this [weekday]"`: Evaluated as the occurrence within the current calendar week containing `Current Time`.
+## Key Architectural Behaviors
-### Confidence Thresholds & Metadata (`.ai`)
+### 1. Workspace Baseline Context
+`contextAI` inspects the host runtime or current `Tempo` configuration (`Tempo.options.timeZone`, `Tempo.options.locale`, etc.) as a fallback baseline. If an input like `"at home"` is provided, the LLM will ground its inference in the workstation's baseline defaults.
+
+### 2. Strict Confidence Thresholds
+Using `minConfidence`, developers can guarantee that low-certainty or completely ambiguous inputs (e.g., `"in the park"`) throw a `TempoAiError(422)` rather than silently returning guessed context parameters:
+
+```typescript
+const context = await contextAI("meeting somewhere online", { minConfidence: 0.9 });
+// Throws TempoAiError(422): Inferred context confidence (0.4) is below the required threshold of 0.9.
+```
-When `minConfidence` is supplied in options (e.g. `parseAI("...", { minConfidence: 0.8 })`), any LLM response returning a confidence score below that threshold produces a `Tempo` instance with `isValid === false`.
+### 3. Timezone Validation
+Before returning, the returned IANA timezone string is dynamically validated against the runtime's native JavaScript `Intl` API. If the LLM returns an unsupported or fake timezone identifier, `contextAI` throws a `TempoAiError(422)` to prevent application runtime failures.
-Every resolved `Tempo` instance returned by `parseAI` (and other AI functions) has a non-writable, frozen `.ai` metadata descriptor containing execution audit data:
+### 4. Parallel Batch Processing
+You can pass an array of strings to process multiple contexts concurrently:
```typescript
-const dt = await parseAI("Christmas 2026", { debug: true });
-console.log(dt.ai);
-// {
-// provider: 'openai',
-// cached: false,
-// confidence: 0.95,
-// ambiguous: false,
-// granularity: 'day',
-// rawIso: '2026-12-25T00:00:00',
-// rawPrompt: 'Christmas 2026', // Present when debug is enabled
-// normalizedPrompt: 'christmas 2026' // Present when debug is enabled
-// }
+const [context1, context2] = await contextAI([
+ "Working from Kyoto",
+ "Living in Melbourne"
+]);
```
+### Combining `contextAI` with `parseAI` (The Pivot Flow)
+
+Often, a user will mention their location in one sentence and a relative time in another. You can chain these APIs together to form a seamless date-resolution pipeline:
+
+```typescript
+import { contextAI, parseAI } from '@magmacomputing/tempo-plugin-ai';
+
+// 1. Deduces the context
+const ticketContext = await contextAI("customer issue from our London office");
+// ticketContext = { timeZone: 'Europe/London', locale: 'en-GB', sphere: 'north' }
+
+// 2. Feed the output context directly as options into parseAI
+const resolutionTime = await parseAI("issue occurred on 04/05/2026 at 3 PM", ticketContext);
+// 1. Correctly parses 04/05 to May 4th (UK format) rather than April 5th.
+// 2. Adjusts to BST/GMT (Europe/London).
+```
diff --git a/packages/plugins/ai/doc/contextAI.md b/packages/plugins/ai/doc/contextAI.md
deleted file mode 100644
index 6d6175e1..00000000
--- a/packages/plugins/ai/doc/contextAI.md
+++ /dev/null
@@ -1,123 +0,0 @@
-# `contextAI` — Context & Regional Inference
-
-`contextAI()` is designed to analyze unstructured or ambiguous text (e.g. user biographies, locations, or email bodies) and infer their regional configuration settings. It resolves these properties to a standard configuration object containing timezone, locale, calendar system, and hemisphere.
-
-This is highly useful for user onboarding settings, automatic context mapping for calendar syncs, and geolocating inputs dynamically without maintaining static geographic mapping tables.
-
----
-
-## Basic Usage
-
-> [!NOTE]
-> Like all Tempo AI functions, `contextAI` requires prior initialization with at least one active provider via `initAI()`.
-
-```typescript
-import { initAI, contextAI } from '@magmacomputing/tempo-plugin-ai';
-
-// 1. Configure the AI provider farm
-await initAI({
- providers: [
- { id: 'groq', key: process.env.GROQ_API_KEY, model: 'llama-3.3-70b-versatile' }
- ]
-});
-
-// 2. Infer contextual settings from unstructured text
-const context = await contextAI("I'm a photographer based in Sydney, Australia.");
-
-console.log(context.timeZone); // "Australia/Sydney"
-console.log(context.locale); // "en-AU"
-console.log(context.calendar); // "gregory"
-console.log(context.sphere); // "south"
-console.log(context.confidence); // 0.98
-```
-
----
-
-## Configuration Options (`AiContextOptions`)
-
-| Option | Type | Description |
-| :--- | :--- | :--- |
-| **`force`** | `boolean` | If true, bypasses the cache to initiate a fresh LLM query. |
-| **`cache`** | `boolean` | If false, disables writing to and reading from cache adapters. |
-| **`cacheAdapter`** | `AiCacheAdapter` | Custom cache engine (e.g., Redis) for caching results on this request. |
-| **`ttl`** | `number` | Time-to-live override in milliseconds for cached results. |
-| **`minConfidence`**| `number` | Minimum confidence score threshold (0.0 to 1.0) required to return a valid slot. Throws `TempoAiError(422)` if lower. |
-| **`mode`** | `AiMode` | Concurrency routing strategy (`fallback`, `race`, `consensus`, `hedged`, `roundrobin`, `adaptive`). Refer to the [Multi-Provider Execution Modes Guide](./modes.md). |
-| **`providers`** | `AiProvider[]` | Per-request provider configuration overrides. |
-| **`timeout`** | `number` | Per-request timeout in milliseconds (overrides provider and global timeouts). |
-| **`hedgeDelay`** | `number` | Delay in milliseconds before initiating speculative hedging in `AiMode.Hedged` (default: `800ms`). |
-| **`debug`** | `boolean` | If true, logs prompt context and LLM payloads to console. |
-| **`softErrors`** | `boolean` | If true, returns `TempoAiError` into array index position instead of rejecting the entire batch query. |
-
----
-
-## Result Schema (`TempoContext`)
-
-```typescript
-export interface TempoContext {
- /** Inferred IANA time zone identifier (e.g. 'America/New_York') */
- timeZone: string;
-
- /** Inferred BCP 47 language/region tag (e.g. 'en-US') */
- locale: string;
-
- /** Inferred Unicode calendar system type (e.g. 'gregory') */
- calendar: string;
-
- /** Inferred hemisphere, or undefined if ambiguous */
- sphere?: 'north' | 'south' | undefined;
-
- /** Confidence rating from 0.0 (unparseable) to 1.0 (certain) */
- confidence: number;
-
- /** Resolution source ('cache' or provider ID like 'groq', 'gemini', 'openai') */
- provider: string;
-
- /** Step-by-step reasoning or justification provided by the engine/LLM */
- reasoning?: string | undefined;
-}
-```
-
----
-
-## Key Architectural Behaviors
-
-### 1. Workspace Baseline Context
-`contextAI` inspects the host runtime or current `Tempo` configuration (`Tempo.options.timeZone`, `Tempo.options.locale`, etc.) as a fallback baseline. If an input like `"at home"` is provided, the LLM will ground its inference in the workstation's baseline defaults.
-
-### 2. Strict Confidence Thresholds
-Using `minConfidence`, developers can guarantee that low-certainty or completely ambiguous inputs (e.g., `"in the park"`) throw a `TempoAiError(422)` rather than silently returning guessed context parameters:
-
-```typescript
-const context = await contextAI("meeting somewhere online", { minConfidence: 0.9 });
-// Throws TempoAiError(422): Inferred context confidence (0.4) is below the required threshold of 0.9.
-```
-
-### 3. Timezone Validation
-Before returning, the returned IANA timezone string is dynamically validated against the runtime's native JavaScript `Intl` API. If the LLM returns an unsupported or fake timezone identifier, `contextAI` throws a `TempoAiError(422)` to prevent application runtime failures.
-
-### 4. Parallel Batch Processing
-You can pass an array of strings to process multiple contexts concurrently:
-```typescript
-const [context1, context2] = await contextAI([
- "Working from Kyoto",
- "Living in Melbourne"
-]);
-```
-
-### Combining `contextAI` with `parseAI` (The Pivot Flow)
-
-Often, a user will mention their location in one sentence and a relative time in another. You can chain these APIs together to form a seamless date-resolution pipeline:
-
-```typescript
-import { contextAI, parseAI } from '@magmacomputing/tempo-plugin-ai';
-
-// 1. Deduces the context
-const ticketContext = await contextAI("customer issue from our London office");
-// ticketContext = { timeZone: 'Europe/London', locale: 'en-GB', sphere: 'north' }
-
-// 2. Feed the output context directly as options into parseAI
-const resolutionTime = await parseAI("issue occurred on 04/05/2026 at 3 PM", ticketContext);
-// 1. Correctly parses 04/05 to May 4th (UK format) rather than April 5th.
-// 2. Adjusts to BST/GMT (Europe/London).
-```
diff --git a/packages/plugins/ai/doc/diffAI.md b/packages/plugins/ai/doc/diff.md
similarity index 98%
rename from packages/plugins/ai/doc/diffAI.md
rename to packages/plugins/ai/doc/diff.md
index fc8bd5aa..fba1eb85 100644
--- a/packages/plugins/ai/doc/diffAI.md
+++ b/packages/plugins/ai/doc/diff.md
@@ -15,7 +15,7 @@ import { initAI, diffAI } from '@magmacomputing/tempo-plugin-ai';
// 1. Initialize AI providers
await initAI({
providers: [
- { id: 'groq', key: process.env.GROQ_API_KEY, model: 'llama-3.3-70b-versatile' }
+ { id: 'groq', key: process.env.GROQ_API_KEY }
]
});
diff --git a/packages/plugins/ai/doc/extract.md b/packages/plugins/ai/doc/extract.md
new file mode 100644
index 00000000..c5602ada
--- /dev/null
+++ b/packages/plugins/ai/doc/extract.md
@@ -0,0 +1,145 @@
+# `extractAI` — Unstructured Text & Calendar Event Extraction
+
+`extractAI()` scans unstructured, multi-paragraph text (emails, meeting transcripts, chat logs, task notes, calendar invitations) to automatically identify, parse, and extract all embedded temporal entities and time-bound events into structured `TempoAiExtractResult` records containing native `Tempo` instances.
+
+Relative expressions (such as *"tomorrow at 10am"*, *"next Tuesday from 1 to 3pm"*, *"final deliverables due Friday EOD"*) are resolved and mathematically grounded against an explicit or current reference `anchor` timestamp, timezone, and calendar system.
+
+---
+
+## Basic Usage
+
+```typescript
+import { Tempo } from '@magmacomputing/tempo';
+import { initAI, extractAI } from '@magmacomputing/tempo-plugin-ai';
+
+// 1. Initialize AI providers
+await initAI({
+ providers: [
+ { id: 'groq', key: process.env.GROQ_API_KEY }
+ ]
+});
+
+const emailText = `
+Hi team,
+Let's schedule our Sprint Review tomorrow from 10:00 AM to 11:30 AM in Room 4A.
+Also, reminder that all pull requests and documentation are due next Friday by 5:00 PM.
+`;
+
+const anchor = new Tempo('2026-08-10T09:00:00Z'); // Monday morning
+
+const result = await extractAI(emailText, { anchor, timeZone: 'America/New_York' });
+
+for (const event of result.events) {
+ console.log(`[${event.type}] ${event.label}`);
+ console.log(` Start: ${event.start.format('{yyyy}-{mm}-{dd} {hh}:{mi}')}`);
+ if (event.end) {
+ console.log(` End: ${event.end.format('{yyyy}-{mm}-{dd} {hh}:{mi}')}`);
+ }
+ console.log(` Source: "${event.rawText}" (Confidence: ${event.confidence})`);
+}
+```
+
+---
+
+## Configuration Options (`AiExtractOptions`)
+
+| Option | Type | Description |
+| :--- | :--- | :--- |
+| **`anchor`** | `TempoDateInput` | Reference anchor date for relative expressions (defaults to current time). |
+| **`timeZone`** | `string` | Target IANA timezone for grounding and output Tempo instances. |
+| **`locale`** | `string \| string[]` | Target BCP 47 locale or language tag (e.g. `'en-US'`, `'fr-FR'`). |
+| **`calendar`** | `string` | Calendar system (e.g. `'gregory'`, `'hebrew'`, `'islamic'`). |
+| **`categories`** | `string[]` | Optional list of categories to filter entities (e.g. `['meeting', 'deadline']`). |
+| **`region`** | `string` | Regional context (e.g. `'AU-NSW'`, `'US-NY'`) passed to LLM grounding. |
+| **`force`** | `boolean` | If true, bypasses cache to force a fresh LLM query. |
+| **`cache`** | `boolean` | If false, disables writing to and reading from cache adapters. |
+| **`cacheAdapter`** | `AiCacheAdapter` | Custom cache engine (e.g., Redis, Cloudflare KV) for caching results. |
+| **`ttl`** | `number` | Time-to-live override in milliseconds for cached results (defaults to 24h). |
+| **`minConfidence`**| `number` | Minimum confidence score threshold (0.0 to 1.0) required. Throws `TempoAiError(422)` if lower. |
+| **`mode`** | `AiMode` | Concurrency routing strategy (`fallback`, `race`, `consensus`, `hedged`, `roundrobin`, `adaptive`). Refer to the [Multi-Provider Execution Modes Guide](./modes.md). |
+| **`softErrors`** | `boolean` | If true, returns `TempoAiError` into array indices instead of rejecting batch queries. |
+
+---
+
+## Result Schema (`TempoAiExtractResult`)
+
+```typescript
+export interface TempoAiExtractResult {
+ /** Array of extracted events with instantiated Tempo objects. */
+ events: TempoExtractedEvent[];
+
+ /** Overall extraction confidence score between 0.0 and 1.0. */
+ confidence: number;
+
+ /** ID of the provider that fulfilled the request (or 'cache'). */
+ provider: string;
+
+ /** Optional summary or reasoning from the LLM. */
+ reasoning?: string | undefined;
+}
+
+export interface TempoExtractedEvent {
+ /** Short descriptive label or title of the extracted event/activity. */
+ label: string;
+
+ /** Start date-time point as an instantiated Tempo instance. */
+ start: Tempo;
+
+ /** Optional end date-time point (if an interval or duration was mentioned). */
+ end?: Tempo | undefined;
+
+ /** Classification category ('point' | 'interval' | 'deadline' | 'recurrence' | 'tentative'). */
+ type: TempoEventType;
+
+ /** Raw text snippet extracted from the source document. */
+ rawText?: string | undefined;
+
+ /** Confidence score for this specific entity extraction (0.0 to 1.0). */
+ confidence: number;
+}
+```
+
+---
+
+## Key Architectural Behaviors
+
+### 1. Mathematical Grounding & Hallucination Suppression
+To prevent hallucinated dates, `extractAI` calculates grounding anchor coordinates before dispatching to the LLM:
+- Localized ISO reference timestamp and timezone
+- Day of the week name and ordinal index
+- Target calendar system and regional context
+- Constrained JSON schema ensuring valid ISO dates
+
+### 2. Native `Tempo` Instances
+Extracted start and end points are immediately instantiated as live `Tempo` objects, ready for subsequent date math, interval arithmetic, or timezone shifting:
+
+```typescript
+const result = await extractAI(transcript);
+const meeting = result.events[0];
+
+// Instant date operations with Tempo
+const reminderTime = meeting.start.subtract('15 minutes');
+console.log(`Set alarm for: ${reminderTime.format('{h12}:{mi} {mer}')}`);
+```
+
+### 3. Multi-Tier Distributed Caching
+`extractAI` integrates multi-tier caching (in-memory and optional asynchronous `AiCacheAdapter` such as Redis or Cloudflare KV). Cached ISO timestamps are rehydrated into live `Tempo` objects upon cache hits:
+
+```typescript
+const result = await extractAI(documentText, {
+ cacheAdapter: redisCacheAdapter,
+ ttl: 86_400_000, // 24 hours
+});
+```
+
+### 4. Parallel Batch Extraction
+Process arrays of documents concurrently with optional `softErrors` fault-tolerance:
+
+```typescript
+const documents = [
+ "Team offsite next Thursday from 9am to 5pm.",
+ "Project proposal submission deadline is August 20 at midnight."
+];
+
+const results = await extractAI(documents, { softErrors: true });
+```
diff --git a/packages/plugins/ai/doc/format.md b/packages/plugins/ai/doc/format.md
new file mode 100644
index 00000000..ec83f5c4
--- /dev/null
+++ b/packages/plugins/ai/doc/format.md
@@ -0,0 +1,113 @@
+# `formatAI` — Contextual & Narrative Date Formatting
+
+`formatAI()` formats a `Tempo` instance, TC39 `Temporal` object, Date, or timestamp into human-friendly, contextual narrative text tailored to specific UI tones, relative time frames, or business domains.
+
+While core `Tempo` provides token-based template formatting (`t.format('{yyyy}-{mm}-{dd}')`), `formatAI` bridges the gap to contextual, localized human descriptions that token patterns alone cannot capture (e.g. countdowns, calendar invites, conversational reminders, and domain summaries), backed by mathematical grounding.
+
+---
+
+## Basic Usage
+
+```typescript
+import { Tempo } from '@magmacomputing/tempo';
+import { initAI, formatAI } from '@magmacomputing/tempo-plugin-ai';
+
+// 1. Initialize AI providers
+await initAI({
+ providers: [
+ { id: 'groq', key: process.env.GROQ_API_KEY }
+ ]
+});
+
+const target = new Tempo('2026-08-07T17:00:00[America/New_York]');
+const anchor = new Tempo('2026-08-02T17:00:00[America/New_York]');
+
+// "this Friday at 5:00 PM EDT (in 5 days)"
+const result = await formatAI(target, 'friendly reminder tone with relative countdown', { anchor });
+
+console.log(result.formatted); // "this Friday at 5:00 PM EDT (in 5 days)"
+console.log(result.confidence); // 0.98
+console.log(result.provider); // 'groq'
+```
+
+---
+
+## Configuration Options (`AiFormatOptions`)
+
+| Option | Type | Description |
+| :--- | :--- | :--- |
+| **`anchor`** | `TempoDateInput` | Reference anchor date for relative delta calculations (defaults to current time). |
+| **`style`** | `string` | Narrative style or tone hint (e.g. `'casual'`, `'formal'`, `'compact'`, `'countdown'`). |
+| **`region`** | `string` | Regional context (e.g., `'AU-NSW'`, `'US-CA'`) passed to LLM grounding. |
+| **`timeZone`** | `string` | Target IANA timezone for output formatting. |
+| **`locale`** | `string \| string[]` | Target BCP 47 locale or language tag (e.g. `'fr-FR'`, `'en-US'`). |
+| **`force`** | `boolean` | If true, bypasses the cache to initiate a fresh LLM query. |
+| **`cache`** | `boolean` | If false, disables writing to and reading from cache adapters. |
+| **`cacheAdapter`** | `AiCacheAdapter` | Custom cache engine (e.g., Redis, Cloudflare KV) for caching results. |
+| **`ttl`** | `number` | Time-to-live override in milliseconds for cached results (defaults to 24h). |
+| **`minConfidence`**| `number` | Minimum confidence score threshold (0.0 to 1.0) required. Throws `TempoAiError(422)` if lower. |
+| **`mode`** | `AiMode` | Concurrency routing strategy (`fallback`, `race`, `consensus`, `hedged`, `roundrobin`, `adaptive`). Refer to the [Multi-Provider Execution Modes Guide](./modes.md). |
+| **`softErrors`** | `boolean` | If true, returns `TempoAiError` into array indices instead of rejecting batch queries. |
+
+---
+
+## Result Schema (`TempoAiFormatResult`)
+
+```typescript
+export interface TempoAiFormatResult {
+ /** Formatted narrative string. */
+ formatted: string;
+
+ /** Confidence score between 0.0 and 1.0. */
+ confidence: number;
+
+ /** ID of the provider that fulfilled the request (or 'cache'). */
+ provider: string;
+
+ /** Optional step-by-step rationale from the LLM. */
+ reasoning?: string | undefined;
+}
+```
+
+---
+
+## Key Architectural Behaviors
+
+### 1. Native Grounding Context
+To eliminate LLM date and day-of-week hallucinations, `formatAI` computes deterministic grounding metrics before constructing the prompt:
+- Exact ISO timestamp and timezone
+- Day of the week name and ordinal (e.g. `Friday`, Day 5)
+- Relative delta in calendar days and elapsed hours compared to anchor
+- Directionality (`'past'`, `'present'`, `'future'`)
+
+These metrics are injected into the system prompt as immutable constraints.
+
+### 2. TC39 Temporal & Universal Interoperability
+`formatAI` seamlessly accepts `Tempo` instances, native JavaScript `Date` objects, ISO strings, timestamps, and TC39 `Temporal` objects (`Temporal.ZonedDateTime`, `Temporal.Instant`, `Temporal.PlainDateTime`, `Temporal.PlainDate`):
+
+```typescript
+import { Temporal } from '@magmacomputing/tempo/library';
+
+const zdt = Temporal.ZonedDateTime.from('2026-08-05T15:00:00+10:00[Australia/Sydney]');
+const result = await formatAI(zdt, 'compact relative format');
+```
+
+### 3. Multi-Tier Distributed Caching
+`formatAI` integrates multi-tier caching (in-memory + optional asynchronous `AiCacheAdapter` such as Redis or Cloudflare KV). Cache keys incorporate input timestamp, anchor timestamp, normalized prompt, timezone, locale, region, and style to ensure complete cache correctness:
+
+```typescript
+const result = await formatAI(target, 'casual invitation', {
+ cacheAdapter: redisCacheAdapter,
+ ttl: 3_600_000, // 1 hour
+});
+```
+
+### 4. Parallel Batch Formatting
+Format multiple dates and prompts concurrently with optional `softErrors` resilience:
+
+```typescript
+const results = await formatAI([
+ { date: '2026-08-03T09:00:00Z', prompt: 'calendar invite' },
+ { date: '2026-08-05T18:00:00Z', prompt: 'flight departure notification' },
+], { softErrors: true });
+```
diff --git a/packages/plugins/ai/doc/grounding.md b/packages/plugins/ai/doc/grounding.md
new file mode 100644
index 00000000..87cb554a
--- /dev/null
+++ b/packages/plugins/ai/doc/grounding.md
@@ -0,0 +1,77 @@
+# Grounding & Natural Language Parsing
+
+Because natural language dates are entirely relative (e.g., *"next Tuesday"*) and culturally ambiguous (e.g., *"11/12"*), an LLM cannot reliably parse them in a vacuum.
+
+The Tempo AI plugin solves this by automatically injecting **deterministic temporal and regional grounding coordinates** before dispatching queries to the LLM.
+
+## Temporal & Regional Grounding
+
+The plugin automatically resolves the active `Tempo.config` to establish the exact reference time and regional coordinates:
+- **Anchor Reference Clock**: The exact ISO timestamp at the moment of invocation.
+- **Regional Coordinates**: TimeZone (e.g., `America/New_York`), Calendar system (`iso8601`), Locale (`en-US`), and Hemisphere (`northern`).
+
+Along with your text query, the plugin passes these grounding coordinates directly to the model's system prompt:
+> *`Grounding Anchor: [Anchor], Timezone: [TZ], Calendar: [Cal], Locale: [Loc], Hemisphere: [Sphere]`*
+
+### Custom Grounding Anchors & Options
+You can explicitly override any grounding coordinate on a per-request basis by passing an options object as the second argument, identical to how you pass configuration options to a standard `new Tempo()` constructor:
+
+```typescript
+// Explicitly evaluate this complex relative query from the perspective of September 1st
+const dt = await parseAI("The penultimate Tuesday before Thanksgiving", {
+ anchor: '2026-09-01T00:00:00Z'
+});
+
+// Explicitly parse assuming a Japanese locale and timezone
+const tokyoDt = await parseAI("The second Sunday of May", {
+ locale: 'ja-JP',
+ timeZone: 'Asia/Tokyo'
+});
+```
+
+### Why Cultural & Regional Grounding is Critical
+Passing the `Locale` and `TimeZone` is critical for the LLM to know whether `"11/12"` represents November 12th (US format) or 11th of December (UK/EU format). The plugin grounds these ambiguous tokens transparently based on your standard Tempo configuration!
+
+> [!WARNING]
+> **Calendar Math Hallucinations**: LLMs are language predictors, not calculators. While they excel at parsing conversational times (like `"tomorrow at 5pm"`), smaller models are notoriously prone to hallucinations on complex, cross-year calendar math. For example, asking a lightweight model for `"Thanksgiving in 2026"` may result in a hallucinated day of the week because the model doesn't natively compute "the fourth Thursday of November 2026." If your application relies on heavy holiday logic or complex multi-year math, you *must* use a capable frontier model or rely on deterministic plugins instead of AI.
+
+## The Decoupled Output Bridge
+
+To ensure deterministic, type-safe behavior, the plugin enforces a strict decoupled bridge between AI text generation and JavaScript object hydration:
+
+* **For Point-in-Time Parsing (`parseAI`)**: The LLM is instructed to return a strict local ISO 8601 string without a timezone offset or 'Z' suffix (e.g. `"2026-11-26T00:00:00"`). The plugin immediately constructs a native `new Tempo()` instance with caller-defined timezone and calendar context.
+* **For Structured Functions (`formatAI`, `extractAI`, `diffAI`, `contextAI`)**: The LLM completes rigid JSON schemas validated against strict boundary rules, instantiating typed result objects (`TempoAiFormatResult`, `TempoAiExtractResult`, `TempoAiDiffResult`, `TempoContext`).
+* **For Intervals & Generators (`scheduleAI`, `recurrenceAI`)**: The plugin hydrates interval boundaries into a proxied `Interval` or exposes an iterable generator yielding sequential `Tempo` instances.
+
+This eliminates AST-construction ambiguity and provides clean runtime contracts for every operation.
+
+### Relative Date Ambiguity Tie-Breakers
+
+To eliminate model variance on idioms like "Next Friday" or "Last Tuesday", the plugin enforces static system prompt ambiguity rules:
+* `"next [weekday/unit]"`: Evaluated as the immediate next chronological occurrence after the grounding anchor.
+* `"last [weekday/unit]"` / `"previous [weekday/unit]"`: Evaluated as the most recent past occurrence prior to the grounding anchor.
+* `"this [weekday]"`: Evaluated as the occurrence within the current calendar week containing the grounding anchor.
+
+### Confidence Thresholds & Metadata Handling
+
+When `minConfidence` is supplied in options (e.g. `{ minConfidence: 0.85 }`):
+* **`parseAI`**: Any LLM response returning a confidence score below the threshold produces a `Tempo` instance with `isValid === false` (when using `softErrors: true`) or throws a `TempoAiError(422)`.
+* **Structured Functions (`formatAI`, `extractAI`, `diffAI`, `contextAI`, `scheduleAI`, `recurrenceAI`)**: Low-confidence completions immediately throw a `TempoAiError(422)` (or return a `TempoAiError` in batch arrays when `softErrors: true` is enabled).
+
+Every resolved `Tempo` instance returned by `parseAI` has a non-writable, frozen `.ai` metadata descriptor containing execution audit data:
+```typescript
+const dt = await parseAI("Christmas 2026", { debug: true });
+console.log(dt.ai);
+// {
+// provider: 'openai',
+// cached: false,
+// confidence: 0.95,
+// ambiguous: false,
+// granularity: 'day',
+// rawIso: '2026-12-25T00:00:00',
+// rawPrompt: 'Christmas 2026', // Present when debug is enabled
+// normalizedPrompt: 'christmas 2026' // Present when debug is enabled
+// }
+```
+
+*(For other AI functions like `extractAI` or `diffAI`, diagnostic metadata including `confidence`, `reasoning`, and `provider` is attached directly to the returned result object.)*
diff --git a/packages/plugins/ai/doc/index.md b/packages/plugins/ai/doc/index.md
index c417d500..9d265930 100644
--- a/packages/plugins/ai/doc/index.md
+++ b/packages/plugins/ai/doc/index.md
@@ -6,18 +6,13 @@
-> [!WARNING]
-> **🧪 EXPERIMENTAL PLUGIN**
-> This plugin relies on Generative AI. While it uses strict JSON schemas and validation to force deterministic outputs, LLMs (especially smaller models) can still hallucinate complex calendar math. We are actively collecting feedback on prompt engineering and model reliability. Please report any strange behavior or unexpected hallucinations on the [Magma GitHub Bug Report Form](https://github.com/magmacomputing/magma/issues/new?template=bug_report_ai.yml)!
->
-> [!CAUTION]
-> **LLM Output Disclaimer**: Magma Computing Solutions and the Tempo core maintainers provide `@magmacomputing/tempo-plugin-ai` "as-is" without warranty of any kind. Large Language Models are probabilistic text generators, not deterministic calculators. Developers and organization operators are solely responsible for validating AI-generated date and time outputs before relying on them in financial, legal, medical, or time-critical production systems.
-
Tempo community plugin for LLM-powered natural language date parsing, schedule compilation, and temporal processing.
This plugin bridges the gap between deterministic date-math and unstructured NLP inputs, utilizing large language models (like Gemini, Groq, or OpenAI) to safely and asynchronously parse, format, and process complex natural language temporal expressions into `Tempo` instances.
-> **CRITICAL SECURITY WARNING**: Raw LLM API keys must **never** be exposed in a client-side browser bundle or stored in browser storage (`localStorage`, `sessionStorage`, `IndexedDB`, or browser cache). BYOK (Bring Your Own Key) is only secure on backend servers (Node, edge workers). For public frontend applications, route requests through a secure backend proxy service.
+::: warning 🔒 Security Notice
+Raw LLM API keys must **never** be exposed in client-side browser bundles or stored in browser storage (`localStorage`, `sessionStorage`, `IndexedDB`, or browser cache). BYOK (Bring Your Own Key) is only secure on backend servers (Node, edge workers). For public frontend applications, route requests through a secure backend proxy service.
+:::
## Installation & Quickstart
@@ -43,23 +38,44 @@ All AI functions return a standard ES Promise wrapped object.
| Function | Input | Returns (`Promise<...>`) | Description | Doc |
| :--- | :--- | :--- | :--- | :---: |
+| **`parseAI`** | Natural language text string(s) | `Tempo` \| `Tempo[]` \| `(Tempo \| TempoAiError)[]` | Single point-in-time `Tempo` instance (or batch array) | |
+| **`formatAI`** | Date-time + prompt / style | `TempoAiFormatResult` \| `TempoAiFormatResult[]` \| `(TempoAiFormatResult \| TempoAiError)[]` | **Contextual narrative date formatting** (`formatted`, `confidence`, `provider`, `reasoning`) | |
+| **`extractAI`** | Unstructured text string(s) | `TempoAiExtractResult` \| `TempoAiExtractResult[]` \| `(TempoAiExtractResult \| TempoAiError)[]` | **Extracted temporal entities & calendar events** (`events: TempoExtractedEvent[]`, `confidence`, `reasoning`) | |
+| **`recurrenceAI`** | Natural language pattern or RRULE string | `TempoRecurrenceResult` | **Iterable series of `Tempo` dates** (with `.take(n)`, `[Symbol.iterator]`, & RRULE string) | |
+| **`scheduleAI`** | Booking prompt + busy constraints | `TempoScheduleResult` | **Resolved appointment slot** (`start`, `end`, `slot`, `alternatives`, `ai.conflictBumped`) | |
+| **`diffAI`** | Start & End dates + prompt | `TempoAiDiffResult` \| `TempoAiDiffResult[]` \| `(TempoAiDiffResult \| TempoAiError)[]` | **Narrative time delta & business days** (`formatted`, `businessDays`, `days`, `hours`, `holidays`) | |
+| **`contextAI`** | Context text string(s) | `TempoContext` \| `TempoContext[]` \| `(TempoContext \| TempoAiError)[]` | **Inferred regional context** (`timeZone`, `locale`, `calendar`, `sphere`) | |
| **`initAI`** | Provider config & API keys | `void` | Configured AI provider farm | |
-| **`parseAI`** | Natural language text string(s) | `Tempo` \| `Tempo[]` \| `(Tempo \| TempoAiError)[]` | Single point-in-time `Tempo` instance (or batch array) | |
-| **`recurrenceAI`** | Natural language pattern or RRULE string | `TempoRecurrenceResult` | **Iterable series of `Tempo` dates** (with `.take(n)`, `[Symbol.iterator]`, & RRULE string) | |
-| **`scheduleAI`** | Booking prompt + busy constraints | `TempoScheduleResult` | **Resolved appointment slot** (`start`, `end`, `slot`, `alternatives`, `ai.conflictBumped`) | |
-| **`contextAI`** | Context text string(s) | `TempoContext` \| `TempoContext[]` \| `(TempoContext \| TempoAiError)[]` | **Inferred regional context** (`timeZone`, `locale`, `calendar`, `sphere`) | |
-| **`diffAI`** | Start & End dates + prompt | `TempoAiDiffResult` \| `TempoAiDiffResult[]` \| `(TempoAiDiffResult \| TempoAiError)[]` | **Narrative time delta & business days** (`formatted`, `businessDays`, `days`, `hours`, `holidays`) | |
+
+### Summary of Distinct Return Contracts
+
+To streamline error handling and data consumption, return shapes across the AI plugin follow three distinct contracts:
+
+| Category | Functions | Return Type | Single Query Low-Confidence / Failure | Batch Array `softErrors: true` Contract |
+| :--- | :--- | :--- | :--- | :--- |
+| **Point-in-Time Date** | `parseAI` | `Tempo` (with `.ai`) | Throws `TempoAiError` (or returns invalid `Tempo` if `minConfidence` threshold unmet) | Returns invalid `Tempo` (`isValid === false`) in array position |
+| **Structured AI Objects** | `formatAI` `extractAI` `diffAI` `contextAI` | `TempoAiFormatResult` `TempoAiExtractResult` `TempoAiDiffResult` `TempoContext` | Throws `TempoAiError` (422 for low confidence, 429 for quota, 500 for network) | Returns typed `TempoAiError` object directly in array position |
+| **Intervals & Generators** | `scheduleAI` `recurrenceAI` | `TempoScheduleResult` (Proxied `Interval`) `TempoRecurrenceResult` (`.take(n)`) | Throws `TempoAiError` (Single item query only) | N/A (Single query operations) |
## Architecture & Infrastructure Guides
> [!IMPORTANT]
-> **Production Recommendation**: Due to the complexities of LLM APIs, including caching gotchas, context injection, rate limits, and calendar math hallucinations, we politely but firmly recommend reading the dedicated guides below before deploying this plugin in a production environment.
+> **Production Recommendation**: Due to the complexities of LLM APIs, including caching gotchas, context injection, rate limits, and calendar math hallucinations, we politely but strongly recommend reading the dedicated guides below before deploying this plugin in a production environment.
+- [Security & Privacy Architecture](./security.md) (Smart Debug Telemetry, PII Masking, HTTPS & Proxy Introspection)
- [Multi-Provider Execution Modes](./modes.md) (Hedged, RoundRobin, Adaptive, Race, Consensus, Fallback)
-- [Provider Architecture & Security](./architecture.md) (BYOK vs Proxy patterns, Frontend Security)
-- [Context & Natural Language Parsing](./context.md) (How Timezone and Locale are injected)
+- [Provider Architecture & Security](./architecture.md) (BYOK vs Proxy patterns, Browser Security, TLS 1.3 & Privacy Guarantees)
+- [Grounding & Natural Language Parsing](./grounding.md) (How Timezone and Locale are injected)
- [Rate Limits & Cache Management](./rate-limits.md) (Tracking API quotas, handling 429 errors, and custom Redis caches)
+## Community Feedback & Production Notice
+
+> [!NOTE]
+> **Community Feedback & Prompt Engineering**
+> While `@magmacomputing/tempo-plugin-ai` utilizes deterministic grounding, schema enforcement, and confidence validation, LLM outputs can vary across models and prompt styles. We actively welcome community feedback and prompt optimizations—please report any edge cases or suggestions on the [Magma GitHub Issue Tracker](https://github.com/magmacomputing/magma/issues/new?template=bug_report_ai.yml).
+>
+> **Production Notice & "As-Is" Disclaimer**: Magma Computing Solutions and the Tempo core maintainers provide `@magmacomputing/tempo-plugin-ai` "as-is" without warranty of any kind. Large Language Models operate probabilistically; developers and system architects are responsible for validating AI-generated temporal outputs before committing them to financial, legal, medical, or life-critical applications.
+
## Licensing
This is a **Community** plugin. It is completely free and open-source for personal and commercial use. No license token is required.
diff --git a/packages/plugins/ai/doc/init.md b/packages/plugins/ai/doc/init.md
index 49c93810..84265d11 100644
--- a/packages/plugins/ai/doc/init.md
+++ b/packages/plugins/ai/doc/init.md
@@ -14,7 +14,7 @@ await initAI({
{ id: 'openai', key: process.env.OPENAI_API_KEY, model: 'gpt-4o' }
],
timeout: 5000, // 5-second global SLA default
- debug: true // Enable operational trace logging (development-only)
+ debug: true // Enable operational trace logging (automatically PII-sanitized in production)
});
```
@@ -80,10 +80,10 @@ const dt = await parseAI("Next Tuesday at 4pm", { timeout: 3000 });
**Operational Trace Logging**
Passing `debug: true` into `initAI` (or setting `{ debug: true }` on a per-request options object) outputs concise operational trace logs (prefixed with `[tempo-plugin-ai]`) to the developer console for monitoring provider fallback decisions and execution timing.
-Detailed diagnostic context—including `rawPrompt`, `normalizedPrompt`, `reasoning`, confidence scores, and rate limit snapshots—is attached directly to the returned `Tempo` instance via the `.ai` property when `debug: true` is enabled.
+Detailed diagnostic context—including provider resolution, execution lineage, confidence scores, and when `debug: true` is active, `rawPrompt`, `normalizedPrompt`, and rate-limit snapshots—is attached directly to the returned `Tempo` instance via the `.ai` property for `parseAI`. Structured functions (`formatAI`, `diffAI`, `extractAI`, `contextAI`, `scheduleAI`) surface their respective typed properties directly on the result object (such as `res.confidence`, `res.provider`, and optional `res.reasoning`).
-> [!WARNING]
-> **Diagnostic Security Notice**: Inspecting or exposing the `.ai` metadata property (such as `rawPrompt` or `reasoning`) in public UI components or client-side telemetry may expose raw user inputs. Ensure sensitive diagnostic fields on `Tempo.ai` are sanitized before forwarding instances to external monitoring tools.
+> [!TIP]
+> **Smart Debug & Proxy Introspection**: In production environments (`NODE_ENV === 'production'`), terminal logging via `console.log(date.ai)` or `console.log(result)` automatically sanitizes and masks PII (emails, phones, bearer tokens) while preserving 100% in-memory data integrity for application code. Refer to the [Security & Privacy Architecture Guide](./security.md).
## Configuration Options Reference
diff --git a/packages/plugins/ai/doc/modes.md b/packages/plugins/ai/doc/modes.md
index 17cd66b3..b1a599af 100644
--- a/packages/plugins/ai/doc/modes.md
+++ b/packages/plugins/ai/doc/modes.md
@@ -28,12 +28,36 @@ const dt = await parseAI('next friday at 3pm', {
| Mode | Dispatch | Token Cost | Latency | Rate-Limit Resilience |
| :--- | :--- | :---: | :---: | :---: |
-| **`Fallback`** *(Default)* | Sequential | 🟢 1 request | 🟡 Moderate | 🟡 Reactive |
-| **`Hedged`** | Staggered (primary + timer) | 🟢 ~1.15 avg | 🟢 Ultra-Fast | 🟡 Reactive |
-| **`RoundRobin`** | Cyclic rotation | 🟢 1 request | 🟡 Moderate | 🟢 High |
-| **`Adaptive`** | Quota-sorted rotation | 🟢 1 request | 🟡 Moderate | 🟢 Maximum |
-| **`Race`** | Full parallel | 🔴 N requests | 🟢 Ultra-Fast | 🟡 Reactive |
-| **`Consensus`** | Full parallel + voting | 🔴 N requests | 🟡 Moderate | 🟡 Reactive |
+| **`Fallback`** *(Default)* | Sequential | 🟢 1 request | 🟡 Moderate | 🟢 Proactive Cooldown Filter |
+| **`Hedged`** | Staggered (primary + timer) | 🟢 ~1.15 avg | 🟢 Ultra-Fast | 🟢 Proactive Cooldown Filter |
+| **`RoundRobin`** | Cyclic rotation | 🟢 1 request | 🟡 Moderate | 🟢 High (Cyclic + Filter) |
+| **`Adaptive`** | Quota-sorted rotation | 🟢 1 request | 🟡 Moderate | 🟢 Maximum (Telemetry-Ranked) |
+| **`Race`** | Full parallel | 🔴 N requests | 🟢 Ultra-Fast | 🟢 Proactive Cooldown Filter |
+| **`Consensus`** | Full parallel + voting | 🔴 N requests | 🟡 Moderate | 🟢 Proactive Cooldown Filter |
+
+---
+
+## Global Telemetry & Cooldown Filtering
+
+Regardless of the execution mode chosen, the AI dispatch engine actively monitors per-provider rate-limiting metadata across all network responses (`x-ratelimit-remaining-requests`, `x-ratelimit-remaining-tokens`, `x-ratelimit-reset-requests`, `retry-after`).
+
+```mermaid
+flowchart LR
+ A["Incoming Request\n(Any AiMode)"] --> B{"Check Provider Farm\nCooldown State"}
+ B -- "Exhausted (remaining === 0\n& resetAt > now)" --> C["🚫 Proactively Filter Out\n(Skip 429 endpoints)"]
+ B -- "Ready / High Quota" --> D["✅ Active Provider Pool"]
+ C -. "If ALL in cooldown" .-> D
+ D --> E["Dispatch via Selected Mode\n(Fallback, Race, Hedged, etc.)"]
+```
+
+### Proactive Cooldown Avoidance
+Before dispatching any request:
+1. **Cooldown Detection**: The orchestrator checks if any configured provider has exhausted its request or token quota (`remainingRequests === 0` or `remainingTokens === 0`) and is within an active reset window (`resetAt > now`, derived from response reset timestamps or `retry-after` metadata).
+2. **Pre-Dispatch Filtering**: In `Fallback`, `Race`, `Hedged`, and `RoundRobin` modes, exhausted providers are automatically removed from the active candidate pool for that request.
+ - **`Fallback` & `Hedged`**: Avoids stalling on primary providers that are guaranteed to reject with HTTP 429.
+ - **`Race`**: Saves network bandwidth and avoids firing wasted requests to rate-limited models.
+ - **`RoundRobin`**: Skips over cooling-down keys without breaking the cyclic load-balancing progression.
+3. **Fail-Open Resilience**: If *all* providers in the farm are currently in a cooldown window, the orchestrator keeps all providers available rather than failing prematurely, allowing the request to cascade or surface accurate rate-limit errors.
---
@@ -59,7 +83,6 @@ flowchart TD
Audit --> Consensus["🗳️ AiMode.Consensus\nCross-LLM voting • Highest accuracy"]
```
-
---
## Mode Deep-Dives & Code Examples
@@ -93,7 +116,7 @@ const dt = await parseAI('schedule team sync for next wednesday at 2pm', {
```
> [!TIP]
-> `hedgeDelay` can also be set globally in `initAI({ hedgeDelay: 600 })` so it applies to all functions (`parseAI`, `recurrenceAI`, `scheduleAI`).
+> `hedgeDelay` can also be set globally in `initAI({ hedgeDelay: 600 })` so it applies to all functions (`parseAI`, `recurrenceAI`, `scheduleAI`, `extractAI`).
---
@@ -118,7 +141,7 @@ await initAI({
### 4. `AiMode.Adaptive` — Rate-Limit Telemetry Prioritization
-Reads `x-ratelimit-*` HTTP headers after every provider response and stores per-provider quota snapshots. On the next request, providers with `remainingRequests === 0` in an active reset window are automatically deprioritized; remaining providers are sorted by highest available quota.
+Reads `x-ratelimit-*` HTTP headers after every provider response and stores per-provider quota snapshots. On subsequent requests, providers are ranked dynamically by highest remaining quota descending, guaranteeing that providers with ample headroom are prioritized ahead of constrained models.
**Best for:** Multi-tier production gateways — mixed free/paid provider pools where proactively avoiding `429 Too Many Requests` is essential.
@@ -149,16 +172,23 @@ const dt = await parseAI('tomorrow at noon', {
### 6. `AiMode.Consensus` — Multi-LLM Cross-Validation
-Dispatches requests concurrently across all providers and compares the normalized ISO timestamps or RRULE strings. If all responding providers agree, confidence is elevated to `1.0` (unanimous). If providers disagree, the highest-confidence candidate is returned and flagged with `dt.ai.ambiguous = true`.
+Dispatches requests concurrently across all providers and compares the normalized outputs (e.g. ISO timestamps for `parseAI`, RRULE strings for `recurrenceAI`, formatted strings for `diffAI`/`formatAI`, or structured entity counts for `extractAI`). If all responding providers agree, confidence is elevated to `1.0` (unanimous). If providers disagree, the highest-confidence candidate is returned and flagged with `ai.ambiguous = true` (attached to `Tempo.ai` on `parseAI` or returned on structured result objects).
**Best for:** High-stakes legal, financial, and scheduling — contract dates, event conflict resolution, or auditing where hallucination prevention requires unanimous LLM agreement.
```typescript
+// 1. Point-in-time cross validation
const dt = await parseAI('contract renewal date', {
mode: AiMode.Consensus
});
if (dt.ai?.ambiguous) {
- console.warn('Providers disagreed — treat this result with caution.');
+ console.warn('Providers disagreed — treat this date with caution.');
}
+
+// 2. High-precision duration calculation across multiple providers
+const diff = await diffAI(startDate, endDate, 'in business days excluding UK bank holidays', {
+ mode: AiMode.Consensus
+});
```
+
diff --git a/packages/plugins/ai/doc/parseAI.md b/packages/plugins/ai/doc/parse.md
similarity index 95%
rename from packages/plugins/ai/doc/parseAI.md
rename to packages/plugins/ai/doc/parse.md
index 5189c5bb..537ddd6b 100644
--- a/packages/plugins/ai/doc/parseAI.md
+++ b/packages/plugins/ai/doc/parse.md
@@ -31,7 +31,7 @@ const dt = await parseAI("Third Friday of October", {
timeZone: 'Australia/Sydney', // Context timezone
locale: 'en-AU', // Context locale
minConfidence: 0.85, // Require at least 0.85 confidence score
- timeout: 3000, // 3-second SLA call-site timeout
+ timeout: 3000, // 3-second request timeout (throws TempoAiError(504) if exceeded)
force: true, // Skip native pre-parsing & cache lookup
debug: true // Enable operational trace logging & .ai metadata
});
diff --git a/packages/plugins/ai/doc/rate-limits.md b/packages/plugins/ai/doc/rate-limits.md
index d8d10562..a6c944fd 100644
--- a/packages/plugins/ai/doc/rate-limits.md
+++ b/packages/plugins/ai/doc/rate-limits.md
@@ -9,7 +9,7 @@ The plugin automatically tracks these limits by reading the standard `x-ratelimi
Quota and rate-limit metadata can be inspected in two convenient ways:
### 1. Request-Locked Instance Metadata (`dt.ai.limits`)
-Every `Tempo` instance returned by `parseAI` includes a frozen `.ai.limits` snapshot representing the rate limit state returned by provider HTTP headers for *that specific request*. Note that `.ai.limits` is guaranteed only for provider-backed network requests where rate-limit headers are returned by the selected provider; results resolved via native parsing (`provider: 'native'`) or cache hits (`provider: 'cache'`) may omit `.ai.limits` (`undefined`).
+For `parseAI`, every resolved `Tempo` instance includes a frozen `.ai.limits` snapshot representing the rate limit state returned by provider HTTP headers for *that specific request*. Note that `.ai.limits` is guaranteed only for provider-backed network requests where rate-limit headers are returned by the selected provider; results resolved via native parsing (`provider: 'native'`) or cache hits (`provider: 'cache'`) may omit `.ai.limits` (`undefined`).
```typescript
const dt = await parseAI("The third Friday of next month");
@@ -67,19 +67,31 @@ When you pass an array of inputs to AI functions (such as `parseAI`), the plugin
This is by design for three critical reasons:
1. **Cache Efficiency**: Individual processing allows AI functions to instantly resolve duplicate strings against `Tempo.cache`, saving massive amounts of API tokens. If you pass an array of 10,000 dates, but only 1,000 are unique, only 1,000 network requests are made.
2. **Token Economics**: A single request consumes ~100 tokens (System Prompt + User String + Output ISO). Given that frontier models cost cents per million tokens, the risk of array-misalignment bugs (see below) far outweighs the negligible savings of batching system prompts.
-3. **Deterministic Safety**: LLMs are language models, not arrays. If you pass 50 strings, smaller models often hallucinate and return 49 strings, completely breaking your array indexing. By querying sequentially, we guarantee a strict 1:1 mapping and ensure one invalid string doesn't crash the entire batch.
+3. **Deterministic Safety**: LLMs are language models, not arrays. If you pass 50 strings in a single prompt, smaller models often hallucinate and return 49 strings, completely breaking your array indexing. By dispatching items individually, we guarantee a strict 1:1 index alignment, prevent hallucinations from corrupting sibling entries, and allow granular per-item error isolation when `softErrors: true` is enabled.
> [!WARNING]
> **Granular Time Gotcha**: The cache key is automatically salted with the **calendar date** (`yyyy-mm-dd`) of the execution anchor. By default this uses the system execution date, but when `options.anchor` is explicitly set, it uses the caller-provided anchor date. Note that keeping a fixed anchor date retains the same cache key across midnight boundaries, so an automatic midnight cache miss is not guaranteed.
### Soft Errors in Array Batches
-When processing arrays of inputs, an unparseable input or provider failure on one item will throw an error by default, stopping execution. Passing `softErrors: true` allows AI functions to return invalid `Tempo` instances (`isValid === false`) for failing items while completing the rest of the array:
+When processing arrays of inputs, an unparseable input or provider failure on one item will throw an error and reject the entire batch operation by default. Passing `softErrors: true` allows batch operations to continue processing all items and return per-item failure representations:
+
+* **For `parseAI`**: Failed array items return an invalid `Tempo` instance (`isValid === false`).
+* **For Structured Functions (`formatAI`, `extractAI`, `diffAI`, `contextAI`)**: Failed array items return the typed `TempoAiError` object directly in that array position.
```typescript
+// 1. parseAI with softErrors returns invalid Tempo instances
const dates = await parseAI(["Thanksgiving 2026", "INVALID_PROMPT_STRING"], { softErrors: true });
console.log(dates[0].isValid); // true
console.log(dates[1].isValid); // false
+
+// 2. Structured functions return TempoAiError objects into the array
+import { formatAI, TempoAiError } from '@magmacomputing/tempo-plugin-ai';
+
+const formatted = await formatAI([validDate, invalidDate], 'casual tone', { softErrors: true });
+if (formatted[1] instanceof TempoAiError) {
+ console.warn(`Format failed with code: ${formatted[1].code}`);
+}
```
### Static Glossary Seeding
@@ -112,10 +124,13 @@ const dt = await parseAI("The last Friday before Christmas", { force: true, cach
If the LLM hallucinates or returns an incorrect absolute date, you can explicitly purge the string from the cache:
```typescript
-import { clearAiCache } from '@magmacomputing/tempo-plugin-ai';
+import { aiCache } from '@magmacomputing/tempo-plugin-ai';
// Evict a single string
-clearAiCache("2nd tuesday in nov");
+await aiCache.clear("2nd tuesday in nov");
+
+// Or purge all AI cached entries
+await aiCache.clear();
```
### Forcing a Refresh
diff --git a/packages/plugins/ai/doc/recurrenceAI.md b/packages/plugins/ai/doc/recurrence.md
similarity index 99%
rename from packages/plugins/ai/doc/recurrenceAI.md
rename to packages/plugins/ai/doc/recurrence.md
index e2127cf8..5646f698 100644
--- a/packages/plugins/ai/doc/recurrenceAI.md
+++ b/packages/plugins/ai/doc/recurrence.md
@@ -70,7 +70,7 @@ const schedule = await recurrenceAI("Every Friday");
for (const occurrence of schedule) {
// Always include a termination condition for open-ended schedules
- if (occurrence.year > 2028) break;
+ if (occurrence.yy > 2028) break;
console.log(occurrence.format('{yyyy}-{mm}-{dd}'));
}
diff --git a/packages/plugins/ai/doc/scheduleAI.md b/packages/plugins/ai/doc/schedule.md
similarity index 77%
rename from packages/plugins/ai/doc/scheduleAI.md
rename to packages/plugins/ai/doc/schedule.md
index d0760c6b..3a05ff6a 100644
--- a/packages/plugins/ai/doc/scheduleAI.md
+++ b/packages/plugins/ai/doc/schedule.md
@@ -32,26 +32,36 @@ console.log(booking.ai?.conflictBumped); // true (pushed
---
-## Configuration Options (`AiScheduleOptions`)
+## Configuration Options (`TempoScheduleOptions`)
| Option | Type | Description |
| :--- | :--- | :--- |
-| **`anchor`** | `Tempo \| Date \| string \| number` | Anchor reference time to evaluate relative dates from. Defaults to workstation/browser current time. |
-| **`events`** | `TempoEvent[]` | A list of existing busy calendar intervals that the meeting must not overlap with. |
-| **`workingHours`** | `{ start: string; end: string }` | Daily time window constraint (HH:MM formats) inside which slots must fit. |
+| **`anchor`** | `TempoDateInput` | Anchor reference time to evaluate relative dates from. Defaults to workstation/browser current time. |
+| **`events`** | `Array` | A list of existing busy calendar intervals or booked events that the meeting must not overlap with. |
+| **`workingHours`** | `TempoWorkingHours` | Daily time window constraint (HH:MM formats) inside which slots must fit. |
| **`timeZone`** | `string` | Target IANA timezone to calculate the booking slot and boundaries within. |
| **`minConfidence`**| `number` | Minimum confidence score threshold (0.0 to 1.0) required to return a valid slot. |
| **`mode`** | `AiMode` | Concurrency routing strategy (`fallback`, `race`, `consensus`, `hedged`, `roundrobin`, `adaptive`). Refer to the [Multi-Provider Execution Modes Guide](./modes.md). |
-### `TempoEvent` Interface
+### `TempoInterval` Interface
```typescript
-interface TempoEvent {
- start: Tempo | Date | string | number;
- end: Tempo | Date | string | number;
- title?: string;
+export interface TempoInterval {
+ start: Tempo;
+ end: Tempo;
}
```
+### Event Input Shape (`ScheduleEventInput`)
+The `events` (or `intervals`) option accepts raw event objects, continuous `TempoInterval` pairs, native `Interval` instances, or `[start, end]` tuples:
+```typescript
+export type ScheduleEventInput =
+ | { start: TempoDateInput; end: TempoDateInput; title?: string; label?: string }
+ | TempoInterval
+ | Interval
+ | [TempoDateInput, TempoDateInput]
+ | TempoDateInput;
+```
+
---
## Result Schema (`TempoScheduleResult`)
diff --git a/packages/plugins/ai/doc/security.md b/packages/plugins/ai/doc/security.md
new file mode 100644
index 00000000..5b2dc8ac
--- /dev/null
+++ b/packages/plugins/ai/doc/security.md
@@ -0,0 +1,150 @@
+# Security & Privacy Architecture
+
+The `@magmacomputing/tempo-plugin-ai` plugin is engineered with a **"Privacy and Security by Default"** philosophy. Because date parsing and calendar scheduling frequently interact with Personally Identifiable Information (PII)—such as meeting attendees, emails, phone numbers, and sensitive notes—the plugin incorporates multi-layered security controls to protect user data across transit, runtime memory, log output, and caching tiers.
+
+```mermaid
+flowchart TD
+ subgraph Input ["1. Ingress & Transport"]
+ User["User Prompt / Event Data"] -->|"HTTPS / TLS 1.3 Enforcement"| Transport["Secure Transport Layer"]
+ end
+
+ subgraph Memory ["2. In-Memory Processing & Storage"]
+ Transport --> Schema["Rigid Schema Validation & Grounding"]
+ Schema --> Runtime["In-Memory Execution (Full Fidelity Access)"]
+ Runtime --> Cache["Partitioned Multi-Tier Cache (Namespaced by Tenant/TZ/Locale)"]
+ end
+
+ subgraph Egress ["3. Egress & Smart Debugging"]
+ Runtime --> ProxyMeta["Proxy-Wrapped Result Objects (attachCustomInspect)"]
+ ProxyMeta --> Logic["Application Business Logic (100% Raw Data Access)"]
+ ProxyMeta -->|"console.log() / util.inspect"| Logger["Smart Logger (logDebug) • NODE_ENV=production: Auto-Masked PII • NODE_ENV=development: Full Diagnostic Logs"]
+ end
+```
+
+---
+
+## 1. Smart Debug Telemetry & PII Hardening
+
+Debugging LLM integrations traditionally presents a major security dilemma: enabling debug logs often inadvertently dumps raw prompts containing sensitive user emails, phone numbers, and auth tokens into centralized log aggregators (e.g. Datadog, CloudWatch, Sentry).
+
+`@magmacomputing/tempo-plugin-ai` significantly mitigates this risk through **Smart Debug Infrastructure**:
+
+### Universal Environment Detection & Zero-Config Safety
+* **Unified Flag**: Telemetry is enabled directly using `{ debug: true }` on individual requests or globally via `initAI({ debug: true })`.
+* **Environment-Aware Sanitization**: The runtime automatically inspects `NODE_ENV`. In production environments (`NODE_ENV === 'production'`), all debug logs and terminal outputs automatically sanitize sensitive data before printing to `console.log` or `console.warn`.
+* **Development Fidelity**: In non-production environments (local development, testing), full diagnostic strings are preserved for seamless prompt debugging.
+
+### Automatic PII Redaction
+In production mode, all debug telemetry is scrubbed through automated regex sanitizers:
+* **Email Addresses**: Masked to initial and domain (e.g., `john.doe@enterprise.com` → `j***@enterprise.com`).
+* **Phone Numbers**: Masked to last four digits (e.g., `+1-555-867-5309` → `***-***-5309`).
+* **Bearer & API Tokens**: Redacted with prefix/suffix preservation (e.g., `Bearer sk-proj-1234...` → `Bearer sk-p...1234`).
+* **Length Bounds**: Exceptionally long strings (> 256 characters) are safely truncated with character count annotations to prevent log bloat and denial-of-service attacks.
+
+```typescript
+import { parseAI, initAI } from '@magmacomputing/tempo-plugin-ai';
+
+await initAI({
+ providers: [{ id: 'groq', key: process.env.GROQ_API_KEY }],
+ debug: true // Safe in all environments
+});
+
+// Input containing sensitive attendee data
+const date = await parseAI("Meeting with john.smith@company.org (call 555-123-4567) next Friday");
+
+// In Production, console.log(date.ai) outputs:
+// {
+// provider: 'groq',
+// confidence: 0.98,
+// rawPrompt: 'Meeting with j***@company.org (call ***-***-4567) next Friday',
+// reasoning: 'Parsed meeting for next Friday with j***@company.org'
+// }
+```
+
+---
+
+## 2. Tamper-Resistant Proxy Introspection
+
+All AI return objects (`Tempo.ai`, `TempoAiFormatResult`, `TempoAiExtractResult`, `TempoAiDiffResult`, `TempoScheduleResult`, `TempoRecurrenceResult`) utilize JavaScript `Proxy` wrappers and Node.js custom inspection hooks (`Symbol.for('nodejs.util.inspect.custom')` and `.toJSON()`):
+
+1. **Terminal & Log Safety**: When an AI result object is logged via `console.log()`, `util.inspect()`, or serialized for telemetry, the custom inspection hook intercepts the call and outputs the PII-masked view.
+2. **100% In-Memory Code Integrity**: In-memory property access within your application code (`date.ai?.rawPrompt`, `result.events[0].rawText`, `res.reasoning`) retains full, unmodified data fidelity.
+3. **Deep Immutability**: Metadata properties attached to `Tempo` instances are frozen using `Object.freeze()`, preventing runtime tampering or prototype pollution by downstream code or dependencies.
+
+```typescript
+const result = await formatAI(targetDate, 'Notify alice.cooper@domain.com');
+
+// 1. Terminal / Log Aggregators see sanitized PII in production:
+console.log(result);
+// => { formatted: '...', reasoning: '... client a***@domain.com ...' }
+
+// 2. Your application code receives full raw fidelity:
+const rawReasoning = result.reasoning;
+// => "Formatted for client alice.cooper@domain.com"
+```
+
+---
+
+## 3. Transport Security & Network Hardening
+
+### Enforced HTTPS / TLS
+* **Strict HTTPS Requirement**: All network communication with upstream LLM APIs and remote configuration servers must use HTTPS with modern TLS (TLS 1.2 or TLS 1.3).
+* **Plaintext HTTP Disallowed**: Unencrypted HTTP endpoints are rejected at runtime, with an exception allowed exclusively for `localhost` origins during local development or unit testing with mock servers.
+
+### Dynamic Manifest Host Verification
+* **Trusted Remote Endpoints**: When `loadRemoteManifest` resolves provider manifests, it enforces trusted origin allowlists.
+* **Provider URL Sanitization**: Any dynamic endpoint received via remote manifests or the `fetchDefaults` hook is verified before runtime merging. Disallowed hosts are rejected and stripped to prevent server-side request forgery (SSRF).
+
+---
+
+## 4. Credential Isolation & BYOK Architecture
+
+### Automated In-Memory Key Redaction
+* Calling `getAiConfig()` returns a sanitized, read-only configuration snapshot.
+* All provider `key` values, authorization tokens, and shared secrets are permanently replaced with `[REDACTED]`, ensuring secrets cannot be leaked via diagnostic endpoints or error monitors.
+
+### Frontend Zero-Storage Principle
+* **No Client-Side Secrets**: LLM API keys must **never** be bundled into client-side single-page applications (React, Vue, Svelte) or stored in browser storage (`localStorage`, `sessionStorage`, `IndexedDB`).
+* **Proxy Architecture**: Public frontend web applications must route requests through a self-hosted backend proxy or secure AI Gateway (Cloudflare Worker, Next.js API Route) where private API keys are kept server-side.
+
+---
+
+## 5. Ephemeral Processing & Partitioned Caching
+
+### Zero External Telemetry Policy
+* The plugin does not transmit telemetry, analytics, or prompt logs to external tracking servers.
+* Prompt processing and temporal computations occur ephemerally during request execution.
+
+### Partitioned Multi-Tier Caching
+* **Namespaced Cache Keys**: Cache keys are generated with multi-factor domain partitioning (e.g., `diff::`, `format::`, `extract::`) incorporating the prompt text, anchor epoch, target timezone, locale, calendar system, and regional parameters to prevent contextual collision.
+* **Storage Lifecycle**: Cached entries persist in the local `Tempo.cache` (`BoundedCache`) or caller-provided `AiCacheAdapter` (e.g. Redis, KV) strictly until TTL expiration or LRU capacity eviction.
+* **Granular Bypass Controls**: Operations requiring zero cache persistence can supply `cache: false` or `force: true` on any individual request, or programmatically flush entries using `await aiCache.clear()`.
+
+---
+
+## 6. Schema Enforcement & Hallucination Defense
+
+Large Language Models can occasionally hallucinate dates or output non-deterministic formats. `@magmacomputing/tempo-plugin-ai` prevents invalid data propagation through strict input/output boundaries:
+
+1. **Rigid Schema Validation**: All provider completions are validated against deterministic schemas and regex patterns prior to object construction.
+2. **Confidence Threshold Gating**: The plugin enforces configurable `minConfidence` thresholds (e.g. `minConfidence: 0.85`). Results falling below the threshold throw a typed `TempoAiError` or trigger automatic fallback.
+3. **Deterministic Grounding Fallbacks**: Grounding metrics (such as business days, calendar day offsets, and duration calculations) are verified using deterministic `Tempo` calculations rather than unverified LLM assumptions.
+
+---
+
+## 7. Residual Risks & Threat Model Matrix
+
+> [!IMPORTANT]
+> **Primary Production Strategy**: The primary recommendation for production environments is to keep **`debug: false`** (the default). Smart Debug is designed as an automated safety net to prevent catastrophic PII leaks when developers troubleshoot live issues, but no automated sanitization layer can eliminate 100% of risk when raw diagnostic telemetry is captured.
+
+The following matrix documents residual threat vectors and recommended mitigations:
+
+| Threat Vector | Source | Risk Level | Architectural Behavior | Recommended Mitigation |
+| :--- | :--- | :---: | :--- | :--- |
+| **Direct Primitive Logging** | Developer `console.log(res.reasoning)` | Medium | Evaluates to the raw in-memory string and bypasses object inspection hooks. | Log entire result objects (`console.log(res)`) or leave `debug: false`. |
+| **Object Spread Logging** | `console.log({ ...res })` | Low | Spreading copies raw enumerable keys into a plain object without non-enumerable inspect symbols. | Log the object directly (`console.log(res)`) rather than shallow spreading. |
+| **Network-Layer APM Tracing** | Datadog, OpenTelemetry, Sentry HTTP capture | High | APM agents monkey-patching `fetch` capture raw outbound HTTP payloads in transit. | Disable full HTTP body capture on LLM routes in your APM configuration. |
+| **Semantic PII** | Unstructured names, physical addresses, health info | Medium | Regexes catch structured PII (emails, phones, tokens) but not unstructured names/addresses. | Rely on payload truncation limits (< 256 chars) and avoid `debug: true` on sensitive workflows. |
+| **Environment Variable Drift** | `NODE_ENV` not set or misconfigured | Low | Logger checks `NODE_ENV` (`production`, `prod`, `live`) and `PROD=true`. If unset, defaults to dev mode. | Verify deployment manifests explicitly export `NODE_ENV=production`. |
+| **External Cache Driver Logs** | Third-party Redis/DB client debug logs | Low | Distributed cache adapters store raw JSON required to rehydrate `Tempo` instances. | Ensure production Redis/database clients have debug logging disabled. |
+
diff --git a/packages/plugins/ai/package.json b/packages/plugins/ai/package.json
index 59d49efd..ce53fb84 100644
--- a/packages/plugins/ai/package.json
+++ b/packages/plugins/ai/package.json
@@ -1,6 +1,6 @@
{
"name": "@magmacomputing/tempo-plugin-ai",
- "version": "0.3.0",
+ "version": "1.0.0",
"description": "Tempo community plugin for LLM-powered natural language parsing.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
diff --git a/packages/plugins/ai/plan/extractAI.plan.md b/packages/plugins/ai/plan/extractAI.plan.md
deleted file mode 100644
index 43e8e1ef..00000000
--- a/packages/plugins/ai/plan/extractAI.plan.md
+++ /dev/null
@@ -1,137 +0,0 @@
-# Implementation Plan: `extractAI`
-
-## 1. Overview & Goal
-`extractAI` scans unstructured, multi-paragraph text (emails, transcripts, chat logs, meeting agendas, task notes) to identify, parse, and extract all embedded temporal entities and time-bound events into structured `TempoAiExtractResult` records containing `TempoExtractedEvent[]` (`label`, `start`, `end`, `type`, `rawText`, `confidence`).
-
-It anchors relative mentions (e.g., *"tomorrow at 2pm"*, *"next Tuesday from 9 to 11am"*, *"the last day of next month"*) against an explicit or current reference `anchor` timestamp and timezone.
-
----
-
-## 2. Public API & Type Definitions
-
-### 2.1 Types (`packages/plugins/ai/src/types/extract.type.ts`)
-```typescript
-import type { Tempo } from '@magmacomputing/tempo';
-import type { AiOptions } from './common.type.js';
-import type { TempoAiError } from '../core/error.js';
-
-export type TempoEventType = 'point' | 'interval' | 'deadline' | 'recurrence' | 'tentative';
-
-export interface TempoExtractedEvent {
- /** Short descriptive label or title of the extracted event/activity. */
- label: string;
- /** Start date-time point as an instantiated Tempo instance. */
- start: Tempo;
- /** Optional end date-time point (if an interval or duration was mentioned). */
- end?: Tempo;
- /** Classification category of the temporal mention. */
- type: TempoEventType;
- /** Raw text snippet extracted from the source document. */
- rawText?: string;
- /** Confidence score for this specific entity extraction (0.0 to 1.0). */
- confidence: number;
-}
-
-/** Backward compatibility alias for TempoExtractedEvent */
-export type TempoEvent = TempoExtractedEvent;
-
-export interface TempoAiExtractResult {
- /** Array of extracted events with instantiated Tempo objects. */
- events: TempoExtractedEvent[];
- /** Overall confidence score. */
- confidence: number;
- /** Provider ID that fulfilled the request (or 'cache'). */
- provider: string;
- /** Optional summary or reasoning from the LLM. */
- reasoning?: string;
-}
-
-export interface AiExtractOptions extends AiOptions {
- /** Reference anchor date-time for relative expressions (defaults to now). */
- anchor?: Tempo | Date | string | number;
- /** Reference IANA timezone (defaults to global options or 'UTC'). */
- timeZone?: string;
- /** Reference BCP 47 locale (defaults to global options or 'en-US'). */
- locale?: string | string[];
- /** Preferred calendar system (e.g. 'gregory', 'islamic', 'hebrew'). */
- calendar?: string;
- /** Optional category filter to restrict extracted entities (e.g. ['meeting', 'deadline']). */
- categories?: string[];
- /** Optional regional context (e.g. 'US-NY', 'GB'). */
- region?: string;
-}
-```
-
-### 2.2 Function Signature (`packages/plugins/ai/src/functions/extract.ts`)
-```typescript
-export async function extractAI(texts: string[], options?: AiExtractOptions): Promise<(TempoAiExtractResult | TempoAiError)[]>;
-export async function extractAI(text: string, options?: AiExtractOptions): Promise;
-```
-
----
-
-## 3. Grounding & Prompt Strategy
-
-### 3.1 Context Construction
-Pass anchor metadata so the LLM has a solid temporal baseline:
-* **Reference Anchor**: `anchorTempo.format('{yyyy}-{mm}-{dd}T{hh}:{mi}:{ss}')` (`anchorTempo.tz`)
-* **Reference Day of Week**: `anchorTempo.dow` / weekday name
-* **Current Year / Month / Day**: Pre-resolved ISO components
-* **Target Categories / Constraints**: e.g., Filter meetings, deadlines, or flights
-
-### 3.2 System Prompt & Schema
-```markdown
-You are an expert temporal entity extraction engine.
-Scan the user-provided text for all temporal expressions, deadlines, meetings, and intervals.
-Resolve relative references ("tomorrow", "next Monday", "in 2 hours") strictly against the Reference Anchor date and timezone.
-
-Return ONLY a valid JSON object matching this schema:
-{
- "events": [
- {
- "label": "Brief descriptive title",
- "start": "ISO 8601 string (e.g. 2026-08-13T14:00:00)",
- "end": "ISO 8601 string or null",
- "type": "point | interval | deadline | recurrence | tentative",
- "rawText": "Exact text fragment from the input",
- "confidence": 0.95
- }
- ],
- "confidence": 0.95,
- "reasoning": "Identified 2 scheduled meetings and 1 project deadline."
-}
-```
-
-### 3.3 Post-Processing & Validation
-1. For each item in `events`, validate `start` using `new Tempo(item.start, { timeZone: tz, locale: loc, calendar: cal })`.
-2. If `item.end` is present, construct `new Tempo(item.end, { timeZone: tz, locale: loc, calendar: cal })`.
-3. Filter out invalid date results gracefully.
-4. Ensure returned `start` and `end` are native `Tempo` instances for immediate date arithmetic.
-
----
-
-## 4. Caching & Dispatch Pipeline
-
-1. **Cache Key Partition**:
- `extract::${normalizedTextHash}::${anchorTempo.format('{yyyy}-{mm}-{dd}')}::${tz}::${loc}::${cal}::${region}`
-2. **Multi-tier Caching**:
- - Check `AiCacheAdapter` then `Tempo.cache`.
- - Reconstitute cached ISO strings into `Tempo` instances upon cache hit.
-3. **Execution Modes**:
- - Dispatch via `executeWithMode` supporting all 6 modes.
-4. **Batch Processing**:
- - `Promise.all` / `Promise.allSettled` (with `softErrors` normalization).
-
----
-
-## 5. Verification & Test Plan
-* **Unit Tests (`packages/plugins/ai/test/extract.test.ts`)**:
- - Extract multiple events from email text (e.g., meeting + follow-up deadline).
- - Resolve relative dates against custom `anchor` timestamps.
- - Interval extraction with start and end times.
- - Handle inputs containing no temporal entities (returns `events: []`).
- - Cache hit rehydration into `Tempo` instances.
- - Multi-provider execution modes (Fallback, Race, Adaptive).
- - Batch processing with `softErrors: true`.
-* **Documentation (`packages/plugins/ai/doc/extractAI.md`)**:
- - TSDoc, usage examples with email text, calendar creation, and options guide.
diff --git a/packages/plugins/ai/plan/formatAI.plan.md b/packages/plugins/ai/plan/formatAI.plan.md
deleted file mode 100644
index dcaa60ee..00000000
--- a/packages/plugins/ai/plan/formatAI.plan.md
+++ /dev/null
@@ -1,116 +0,0 @@
-# Implementation Plan: `formatAI`
-
-## 1. Overview & Goal
-`formatAI` transforms a `Tempo` instance, `Date`, timestamp, or ISO string into human-friendly, contextual narrative text tailored to specific prompts, UI tones, business domains, or relative countdown styles (e.g., *"this Friday at 5:00 PM EST (in 5 days)"*, *"Q3 Fiscal Close — 14 business days remaining"*).
-
-By combining deterministic date-time grounding (formatted ISO components, day of week, relative difference to anchor/now, season, quarter) with LLM prompt execution, `formatAI` eliminates date hallucination while delivering expressive, localized language.
-
----
-
-## 2. Public API & Type Definitions
-
-### 2.1 Types (`packages/plugins/ai/src/types/format.type.ts`)
-```typescript
-import type { Tempo } from '@magmacomputing/tempo';
-import type { AiOptions } from './common.type.js';
-import type { TempoAiError } from '../core/error.js';
-
-export interface AiFormatOptions extends AiOptions {
- /** Reference anchor date for relative calculations (defaults to now). */
- anchor?: Tempo | Date | string | number;
- /** Target IANA timezone (defaults to Tempo instance timezone or global options). */
- timeZone?: string;
- /** Target BCP 47 locale or language tag (defaults to global options or 'en-US'). */
- locale?: string | string[];
- /** Desired narrative tone or formatting style hint (e.g. 'casual', 'formal', 'compact', 'countdown'). */
- style?: string;
- /** Custom regional context (e.g. 'AU-NSW', 'US-CA'). */
- region?: string;
-}
-
-export interface FormatItem {
- /** Date-time instance or string to format. */
- date: Tempo | Date | string | number;
- /** Prompt instructions guiding the output narrative. */
- prompt?: string;
-}
-
-export interface TempoAiFormatResult {
- /** Formatted narrative string. */
- formatted: string;
- /** Confidence score between 0.0 and 1.0. */
- confidence: number;
- /** ID of the provider that fulfilled the request (or 'cache'). */
- provider: string;
- /** Optional step-by-step rationale from the LLM. */
- reasoning?: string;
-}
-```
-
-### 2.2 Function Signature (`packages/plugins/ai/src/functions/format.ts`)
-```typescript
-export async function formatAI(items: FormatItem[], options?: AiFormatOptions): Promise<(TempoAiFormatResult | TempoAiError)[]>;
-export async function formatAI(date: any, prompt?: string, options?: AiFormatOptions): Promise;
-```
-
----
-
-## 3. Mathematical Grounding & Prompt Strategy
-
-### 3.1 Grounding Calculation
-To guarantee accuracy, pre-compute:
-* **Canonical ISO Representation**: `targetTempo.format('{yyyy}-{mm}-{dd}T{hh}:{mi}:{ss}')`
-* **Target Timezone & Offset**: `targetTempo.tz`, `targetTempo.offset`
-* **Day of Week & Ordinal**: `targetTempo.dow` (1=Mon, 7=Sun), weekday name
-* **Relative Delta to Anchor**:
- * Calendar days: `Math.round(anchorTempo.until(targetTempo, 'day') * 100) / 100`
- * Elapsed hours: `Math.round(anchorTempo.until(targetTempo, 'hour') * 100) / 100`
- * Relative direction: Past / Present / Future
-
-### 3.2 Context & System Prompt
-```markdown
-Grounding Context:
-- Target Date-Time: 2026-08-14T17:00:00 (America/New_York)
-- Day of Week: Friday (Day 5)
-- Reference Anchor: 2026-08-12T08:00:00 (America/New_York)
-- Relative Delta: +2.38 days (+57.0 hours) in the Future
-- Target Locale: en-US
-- Formatting Style: casual
-- Prompt: "Express as an upcoming meeting reminder with relative countdown"
-```
-
-Schema enforcement:
-```json
-{
- "formatted": "this Friday at 5:00 PM EST (in 2 days)",
- "confidence": 0.98,
- "reasoning": "Target timestamp is in 2 days on Friday afternoon."
-}
-```
-
----
-
-## 4. Caching & Dispatch Pipeline
-
-1. **Cache Key Partition**:
- `format::${targetTempo.epoch.ms}::${anchorTempo.epoch.ms}::${normalizedPrompt}::${tz}::${loc}::${style}::${region}`
-2. **Multi-tier Caching**:
- - Check `AiCacheAdapter` (Redis / Cloudflare KV) then local `Tempo.cache`.
- - Validate non-empty `formatted` and check `effectiveMinConfidence`.
-3. **Execution Modes**:
- - Dispatch via `executeWithMode` supporting all 6 modes (`Fallback`, `Race`, `Consensus`, `Hedged`, `RoundRobin`, `Adaptive`).
-4. **Batch Processing**:
- - Concurrent `Promise.all` / `Promise.allSettled` (with `softErrors: true` normalizing rejections to `TempoAiError`).
-
----
-
-## 5. Verification & Test Plan
-* **Unit Tests (`packages/plugins/ai/test/format.test.ts`)**:
- - Valid date formatting across diverse prompt instructions (business SLA, casual relative, compact countdown).
- - Timezone normalization (preserves target instant in requested timeZone).
- - Cache hit preservation and region/style cache isolation.
- - Multi-provider execution modes (Race, Consensus, Hedged).
- - Batch array processing with `softErrors: true`.
- - Confidence threshold rejection (`minConfidence`).
-* **Documentation (`packages/plugins/ai/doc/formatAI.md`)**:
- - TSDoc, basic usage with `initAI`, options guide, batch examples.
diff --git a/packages/plugins/ai/plan/provider-farm-auto-discovery.md b/packages/plugins/ai/plan/provider-farm-auto-discovery.md
new file mode 100644
index 00000000..1469ea93
--- /dev/null
+++ b/packages/plugins/ai/plan/provider-farm-auto-discovery.md
@@ -0,0 +1,200 @@
+# Provider Farm Auto-Discovery & Zero-Config Architecture Plan
+
+## 1. Objective
+
+Enable seamless, zero-configuration auto-discovery of AI provider farm credentials, SLA defaults, and execution modes for `@magmacomputing/tempo-plugin-ai`.
+
+This design:
+1. Replaces manual `initAI({ providers: [...] })` boilerplate with automated discovery.
+2. Integrates directly with Tempo's unified configuration (`tempo.config.*` under `plugins.ai`).
+3. Uses `@magmacomputing/tempo/library`'s `getContext()` for runtime-safe JavaScript engine identification (Node.js, Deno, Bun, Browser, Apps Script).
+4. Enables instant execution of all AI functions (`parseAI`, `formatAI`, `diffAI`, `extractAI`, `recurrenceAI`, `scheduleAI`, `contextAI`) when standard environment variables or configuration files are present.
+
+---
+
+## 2. Current State & Limitations
+
+- **Mandatory Initialization**: `initAI(config: AiConfig)` currently requires a full configuration object with explicit `providers` array mapping.
+- **Immediate Rejection on Missing Config**: Directly calling `parseAI('...')` without prior `initAI()` throws a `TempoAiError('No AI providers configured. Please call initAI().', 400)`.
+- **Configuration Sprawl**: There is no direct synchronization between Tempo core's `tempo.config.*` (discovered by `Tempo.bootstrap()`) and the AI plugin state.
+- **Runtime Environment Fragility**: Ad-hoc checks like `typeof process !== 'undefined'` fail to leverage Tempo's standardized environment abstractions.
+
+---
+
+## 3. Architectural Design
+
+```
+ ┌──────────────────────────────────────────────┐
+ │ AI Function Call (e.g. parseAI, formatAI) │
+ └──────────────────────┬───────────────────────┘
+ │
+ ▼
+ ┌─────────────────────────────────┐
+ │ Are providers loaded in _state? │
+ └────────┬─────────────────┬──────┘
+ (Yes) │ │ (No)
+ ▼ ▼
+ ┌─────────────────┐ ┌────────────────────────────────┐
+ │ Execute Handler │ │ resolveAutoDiscoveredConfig() │
+ └─────────────────┘ └────────────────┬───────────────┘
+ │
+ ┌─────────────────────────────────────────┴─────────────────────────────────────────┐
+ ▼ ▼ ▼
+ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐
+ │ 1. Active Tempo Config │ │ 2. Runtime File Resolution │ │ 3. Environment Variable Scan │
+ │ Inspect `Tempo.config` for │ ───► │ If getContext() is NodeJS/Deno, │ ───► │ Scan process.env / global env │
+ │ `plugins.ai` or `ai` block │ │ invoke Tempo's resolveConfig() │ │ for GROQ_API_KEY, OPENAI_..., │
+ │ (e.g. from Tempo.bootstrap()) │ │ on `tempo.config.*` │ │ GEMINI_..., MISTRAL_API_KEY │
+ └─────────────────────────────────┘ └─────────────────────────────────┘ └─────────────────────────────────┘
+ │
+ ▼
+ ┌─────────────────────────────────┐
+ │ Interpolate ${ENV} Variables & │
+ │ Merge with DEFAULT_PROVIDERS │
+ └────────────────┬────────────────┘
+ │
+ ▼
+ ┌─────────────────────────────────┐
+ │ Cache in _state & Execute │
+ └─────────────────────────────────┘
+```
+
+---
+
+## 4. Key Components & Specifications
+
+### 4.1. Runtime Discovery via `getContext()`
+
+Instead of raw `process` checks, use `getContext()` from `@magmacomputing/tempo/library` to determine environment capabilities safely:
+
+```ts
+import { getContext, CONTEXT } from '@magmacomputing/tempo/library';
+
+export function isServerRuntime(): boolean {
+ const { type } = getContext();
+ return type === CONTEXT.NodeJS || type === CONTEXT.Deno;
+}
+
+export function isBrowserRuntime(): boolean {
+ const { type } = getContext();
+ return type === CONTEXT.Browser;
+}
+```
+
+### 4.2. Unified `tempo.config.*` Schema
+
+AI configuration is declared directly in `tempo.config.json`, `tempo.config.ts`, or `tempo.config.js`:
+
+```json
+{
+ "timeZone": "Australia/Sydney",
+ "locale": "en-AU",
+ "plugins": {
+ "ai": {
+ "mode": "fallback",
+ "timeout": 5000,
+ "minConfidence": 0.85,
+ "providers": [
+ { "id": "groq", "key": "${GROQ_API_KEY}" },
+ { "id": "openai", "key": "${OPENAI_API_KEY}", "model": "gpt-4o-mini" }
+ ]
+ },
+ "snap": {
+ "mi": 15
+ }
+ }
+}
+```
+
+### 4.3. Environment Variable Interpolation
+
+Support `${VAR_NAME}` and `$env:VAR_NAME` template strings in configuration values:
+
+```ts
+function interpolateEnvValue(value: string, env: Record): string {
+ return value.replace(/\$\{(?:env:)?([A-Z0-9_]+)\}/gi, (_, varName) => env[varName] ?? '');
+}
+```
+
+### 4.4. Well-Known Provider Auto-Detection Table
+
+When no explicit configuration file or provider list is provided, the engine scans the environment for standard provider tokens and maps them to built-in `DEFAULT_PROVIDERS`:
+
+| Provider ID | Target Environment Variable(s) | Default Model Target |
+| :--- | :--- | :--- |
+| `groq` | `GROQ_API_KEY` | `openai/gpt-oss-120b` |
+| `openai` | `OPENAI_API_KEY` | `gpt-5.4-mini` |
+| `gemini` | `GEMINI_API_KEY`, `GOOGLE_API_KEY` | `gemini-3.6-flash` |
+| `mistral` | `MISTRAL_API_KEY` | `mistral-small-latest` |
+
+---
+
+## 5. API Surface Changes
+
+### 5.1. Optional `initAI(config?: AiConfig)`
+
+`initAI` becomes fully optional-parameterized:
+
+```ts
+/**
+ * Initializes the AI provider farm.
+ * If called with no arguments or omitted providers, automatically discovers
+ * configuration from Tempo.config, tempo.config.* files, or runtime environment variables.
+ *
+ * @param config - Optional AI configuration overrides
+ */
+export async function initAI(config: AiConfig = {}): Promise
+```
+
+### 5.2. Lazy Auto-Discovery in AI Handlers
+
+All AI entrypoints (`parseAI`, `formatAI`, `diffAI`, `extractAI`, `recurrenceAI`, `scheduleAI`, `contextAI`) resolve the provider farm dynamically if uninitialized:
+
+```ts
+// src/functions/parse.ts
+if (!availableProviders || availableProviders.length === 0) {
+ const discovered = await resolveAutoDiscoveredProviders(options?.debug);
+ if (!discovered || discovered.length === 0) {
+ throw new TempoAiError(
+ 'No AI providers configured. Set GROQ_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, or configure tempo.config.json.',
+ 400
+ );
+ }
+ availableProviders = discovered;
+}
+```
+
+---
+
+## 6. Implementation Roadmap
+
+### Phase 1: Discovery Subsystem (`src/core/discovery.ts`)
+- [ ] Implement `isServerRuntime()` and `getRuntimeEnv()` using `getContext()`.
+- [ ] Implement `interpolateEnv()` utility for recursive string expansion on config objects.
+- [ ] Implement `scanWellKnownEnvProviders()` against `DEFAULT_PROVIDERS`.
+- [ ] Implement `resolveTempoConfigAi()` to query `Tempo.config.plugins?.ai` or invoke `resolveConfig()`.
+
+### Phase 2: `initAI` Signature & Lifecycle Update (`src/core/init.ts`)
+- [ ] Update `initAI(config: AiConfig = {})` default argument handling.
+- [ ] Integrate discovery resolution into synchronous and asynchronous `initAI` initialization branches.
+- [ ] Ensure key redaction (`[REDACTED]`) in `getAiConfig()` properly handles auto-discovered credentials.
+
+### Phase 3: JIT Lazy Discovery in AI Handlers
+- [ ] Update `parseAI`, `formatAI`, `diffAI`, `extractAI`, `recurrenceAI`, `scheduleAI`, and `contextAI` to invoke lazy discovery before failing.
+- [ ] Preserve custom call-site overrides (`options.providers`).
+
+### Phase 4: Testing & Verification
+- [ ] **Unit Tests**:
+ - Test `getContext()` environment branching (mocking Browser vs Node.js vs Deno).
+ - Test `${ENV_VAR}` interpolation (found vs missing).
+ - Test provider farm construction from environment variables (`GROQ_API_KEY`, etc.).
+ - Test `Tempo.bootstrap()` loading `plugins.ai` config block.
+- [ ] **Integration Tests**:
+ - Zero-arg `initAI()` with mock environment variables.
+ - Zero-call `parseAI('...')` direct invocation with mock environment variables.
+ - Error assertion when no keys and no config files are present.
+
+### Phase 5: Documentation
+- [ ] Update `doc/init.md` with Zero-Config & Auto-Discovery instructions.
+- [ ] Add `tempo.config.json` configuration recipe to `doc/index.md`.
+- [ ] Document browser vs server auto-discovery patterns in `doc/architecture.md`.
diff --git a/packages/plugins/ai/plan/v0.3.0-roadmap.md b/packages/plugins/ai/plan/v0.3.0-roadmap.md
deleted file mode 100644
index 3a9bbfa9..00000000
--- a/packages/plugins/ai/plan/v0.3.0-roadmap.md
+++ /dev/null
@@ -1,66 +0,0 @@
-# @magmacomputing/tempo-plugin-ai: v0.3.0 Release Roadmap & Requirements
-
-This document captures the planned feature set, architectural requirements, and design specifications for the **v0.3.0** release of `@magmacomputing/tempo-plugin-ai`.
-
----
-
-## 1. AI Function Handler Implementations (v0.3.0 Status)
-
-### 1.1 ✅ `scheduleAI(prompt: string, options?: TempoScheduleOptions): Promise`
-* Resolves natural language scheduling prompts against working hours, existing calendar events, and timezones into an optimal start/end `TempoScheduleResult` interval (`slot`, `alternatives`, `ai.conflictBumped`).
-
-### 1.2 ✅ `recurrenceAI(prompt: string, options?: TempoRecurrenceOptions): Promise`
-* Translates complex natural language repeating schedule descriptions into standard RFC 5545 RRULE strings and stateful `Tempo` date batches (`rule.take(count)`).
-
-### 1.3 ✅ `contextAI(text: string, options?: AiContextOptions): Promise`
-* Infers `timeZone`, `locale`, and preferred `calendar` system from ambiguous location descriptions or user bios.
-
-### 1.4 ✅ `diffAI(start: any, end: any, prompt?: string, options?: AiDiffOptions): Promise`
-* Calculates and summarizes the delta between two `Tempo` instances in human, business, or operational terms (e.g., `"5 business days (48 hours)"`), backed by native grounding metrics (calendar days, hours, business days with weekend & holiday exclusion).
-
----
-
-## 2. Upcoming AI Function Handlers (Post-v0.3.0 Roadmap)
-
-The following functions remain scaffolded for upcoming releases:
-
-### 2.1 `formatAI(date: any, prompt?: string, options?: AiFormatOptions): Promise`
-* Formats a `Tempo` instance into human-friendly, contextual narrative text tailored to UI tones or relative countdowns.
-* **Example**: `"this Friday at 5:00 PM EST (in 5 days)"`.
-
-### 2.2 `extractAI(text: string, options?: AiExtractOptions): Promise`
-* Scans unstructured text (emails, transcripts, task notes) to extract embedded temporal entities into structured `TempoAiExtractResult` records (`events: TempoExtractedEvent[]`).
-
-
----
-Conceptually, the execution modes actually govern two different, orthogonal concerns:
-
-Concurrency Strategy (How do we invoke providers?):
-
-Sequential: Call one-by-one (e.g., Fallback, RoundRobin, Adaptive).
-Speculative: Call with a staggered delay (e.g., Hedged).
-Parallel: Call all at once (e.g., Race, Consensus).
-Provider Prioritization Strategy (Which providers do we call, and in what order?):
-
-Static: Configured order (e.g., Fallback, Hedged, Race, Consensus).
-Cyclic: Round-robin rotation (e.g., RoundRobin).
-Telemetry-Aware: Sorted dynamically by remaining rate-limit quota (e.g., Adaptive).
-Should Telemetry (Adaptive) be "on" for all modes?
-Yes! Conceptually, Telemetry-Awareness (filtering out exhausted providers in an active cooldown window) should ideally be active globally, regardless of the concurrency strategy:
-
-In Hedged / Fallback: Instead of starting with a hardcoded primary provider (which might have 0 remaining requests), we should start with the provider that telemetry reports has the highest quota, and hedge to the one with the second-highest.
-In Race / Consensus: If we know a provider is currently rate-limited (in a 429 cooldown reset window), launching a request to it is a waste of network resources and will immediately fail. We should filter it out of the race/consensus pool before firing.
-Why it isn't "on" everywhere by default in practice
-Stale Telemetry Risks: Telemetry depends on header ingestion from previous calls. If the reset window is short (e.g., resets in 2 seconds), but our cache reports "exhausted" for another 3 seconds, we might unnecessarily skip a provider that has recovered. Sequential fallback acts as a natural check.
-Simple/Deterministic expectation: Users choosing Race or Fallback often expect absolute determinism based strictly on their provider configuration array order.
-Single-Provider setups: For single-provider setups, telemetry-aware prioritization has no effect.
-Future Design Direction
-If we wanted to support combining them in a future version of Tempo, we could decouple the selection strategy from the concurrency mode:
-
-```typescript
-await initAI({
- mode: AiMode.Hedged, // How we invoke
- prioritization: 'telemetry' // How we order/filter (static | cyclic | telemetry)
-});
-```
-For the current codebase, keeping them as separate named presets (RoundRobin vs. Adaptive) keeps the setup simple and easy to reason about, but we could certainly update the other strategies to check for and skip active cooldown limits in a future update!
\ No newline at end of file
diff --git a/packages/plugins/ai/src/core/cache.ts b/packages/plugins/ai/src/core/cache.ts
new file mode 100644
index 00000000..dce3283e
--- /dev/null
+++ b/packages/plugins/ai/src/core/cache.ts
@@ -0,0 +1,260 @@
+import { Tempo } from '@magmacomputing/tempo';
+import { secure } from '@magmacomputing/tempo/library';
+import { _state } from './init.js';
+import { logDebug, warnDebug } from './logger.js';
+import type { AiCacheAdapter } from '../types/index.js';
+
+export const AI_CACHE_NAMESPACE_PREFIX = 'ai:';
+
+const _entryExpiries = new Map();
+
+/**
+ * Normalizes input string for deterministic cache lookups by trimming excess whitespace and lowercasing.
+ */
+export function normalizeCacheInput(input: string): string {
+ return input.trim().toLowerCase().replace(/\s+/g, ' ');
+}
+
+/**
+ * Generates a namespaced cache key for domain-specific AI functions.
+ */
+export function getNamespacedCacheKey(namespace: string, key: string): string {
+ return `${AI_CACHE_NAMESPACE_PREFIX}${namespace}::${key}`;
+}
+
+/**
+ * Reads from multi-tier cache (Tier 2 external async adapter first, Tier 1 local in-memory Tempo.cache fallback).
+ */
+export async function readMultiTierCache(
+ cacheKey: string,
+ options: {
+ force?: boolean | undefined;
+ cache?: boolean | undefined;
+ cacheAdapter?: AiCacheAdapter | undefined;
+ debug?: boolean | undefined;
+ tag?: string | undefined;
+ },
+): Promise {
+ if (options.force) return undefined;
+ if (options.cache === false || _state.config.cache === false) return undefined;
+
+ const tag = options.tag ?? 'tempo-plugin-ai';
+ const adapter = options.cacheAdapter || _state.config.cacheAdapter;
+ if (adapter) {
+ try {
+ const val = await adapter.get(cacheKey);
+ if (val !== undefined && val !== null) {
+ logDebug(tag, `Cache hit (adapter): ${cacheKey}`, undefined, { debug: options.debug });
+ return val;
+ }
+ } catch (err: any) {
+ warnDebug(tag, `Cache adapter get failed for ${cacheKey}`, err, { debug: options.debug });
+ }
+ }
+
+ if (_entryExpiries.has(cacheKey) && Date.now() > _entryExpiries.get(cacheKey)!) {
+ _entryExpiries.delete(cacheKey);
+ Tempo.cache.delete(cacheKey);
+ return undefined;
+ }
+
+ const localVal = Tempo.cache.get(cacheKey);
+ if (localVal !== undefined) {
+ logDebug(tag, `Cache hit (local): ${cacheKey}`, undefined, { debug: options.debug });
+ return localVal;
+ }
+
+ return undefined;
+}
+
+/**
+ * Writes to multi-tier cache (Tier 1 local in-memory Tempo.cache and Tier 2 external async adapter).
+ */
+export async function writeMultiTierCache(
+ cacheKey: string,
+ value: string,
+ ttl: number,
+ options: {
+ cache?: boolean | undefined;
+ cacheAdapter?: AiCacheAdapter | undefined;
+ debug?: boolean | undefined;
+ tag?: string | undefined;
+ },
+): Promise {
+ if (options.cache === false || _state.config.cache === false) return;
+
+ const tag = options.tag ?? 'tempo-plugin-ai';
+ Tempo.cache.set(cacheKey, value);
+ if (typeof ttl === 'number' && Number.isFinite(ttl) && ttl > 0) {
+ _entryExpiries.set(cacheKey, Date.now() + ttl);
+ } else {
+ _entryExpiries.delete(cacheKey);
+ }
+
+ const adapter = options.cacheAdapter || _state.config.cacheAdapter;
+ if (adapter) {
+ try {
+ await adapter.set(cacheKey, value, ttl);
+ } catch (err: any) {
+ warnDebug(tag, `Cache adapter set failed for ${cacheKey}`, err, { debug: options.debug });
+ }
+ }
+}
+
+/**
+ * ## aiCache
+ * Unified, secure multi-tier cache manager for the Tempo AI plugin suite.
+ * Manages both local in-memory `Tempo.cache` (Tier 1) and external distributed storage adapters (Tier 2).
+ *
+ * Protected with `secure()` proxy to prevent direct external manipulation while providing a full store interface.
+ */
+export const aiCache = secure({
+ /**
+ * Clears AI entries from the in-memory cache and any external storage adapters.
+ * If specific input strings or keys are provided, selectively purges only those entries and prefix trees.
+ *
+ * @param input - Optional string key, input prompt, or array of keys to purge
+ * @returns Promise that resolves once cache eviction is complete
+ */
+ async clear(input?: string | string[]): Promise {
+ const adapter = _state.config.cacheAdapter;
+
+ if (!input) {
+ Tempo.cache.clear();
+ _entryExpiries.clear();
+ if (adapter?.clear) {
+ try {
+ await Promise.resolve(adapter.clear()).catch(() => { });
+ } catch { }
+ }
+ return;
+ }
+
+ const inputs = Array.isArray(input) ? input : [input];
+ for (const i of inputs) {
+ const normalized = normalizeCacheInput(i);
+ const prefix = `${normalized}::`;
+ Tempo.cache.delete(normalized);
+ Tempo.cache.delete(i);
+ Tempo.cache.deletePrefix(prefix);
+ _entryExpiries.delete(normalized);
+ _entryExpiries.delete(i);
+
+ const keysToDelete: string[] = [];
+ for (const [key] of Tempo.cache.entries()) {
+ if (key.includes(normalized) || key.includes(i)) {
+ keysToDelete.push(key);
+ }
+ }
+ for (const k of keysToDelete) {
+ Tempo.cache.delete(k);
+ _entryExpiries.delete(k);
+ }
+
+ if (adapter) {
+ try {
+ if (adapter.delete) {
+ await Promise.resolve(adapter.delete(normalized)).catch(() => { });
+ await Promise.resolve(adapter.delete(i)).catch(() => { });
+ for (const k of keysToDelete) {
+ await Promise.resolve(adapter.delete(k)).catch(() => { });
+ }
+ }
+ if (adapter.clear) {
+ await Promise.resolve(adapter.clear(prefix)).catch(() => { });
+ }
+ } catch { }
+ }
+ }
+ },
+
+ /**
+ * Deletes a specific key from both Tier 1 in-memory cache and Tier 2 storage adapter.
+ *
+ * @param key - The cache key to delete
+ * @returns True if the key was present in the in-memory cache, false otherwise
+ */
+ async delete(key: string): Promise {
+ _entryExpiries.delete(key);
+ const deletedLocal = Tempo.cache.delete(key);
+ const adapter = _state.config.cacheAdapter;
+ if (adapter?.delete) {
+ try {
+ await Promise.resolve(adapter.delete(key)).catch(() => { });
+ } catch { }
+ }
+ return deletedLocal;
+ },
+
+ /**
+ * Retrieves a cached string by key across multi-tier storage.
+ *
+ * @param key - The cache key to fetch
+ * @returns The cached string value, or undefined if not found
+ */
+ async get(key: string): Promise {
+ if (_entryExpiries.has(key) && Date.now() > _entryExpiries.get(key)!) {
+ _entryExpiries.delete(key);
+ Tempo.cache.delete(key);
+ return undefined;
+ }
+
+ const adapter = _state.config.cacheAdapter;
+ if (adapter?.get) {
+ try {
+ const val = await adapter.get(key);
+ if (val !== undefined && val !== null) return val;
+ } catch { }
+ }
+ return Tempo.cache.get(key);
+ },
+
+ /**
+ * Checks if a key exists in either Tier 1 in-memory cache or Tier 2 storage adapter.
+ *
+ * @param key - The cache key to check
+ * @returns True if the key exists, false otherwise
+ */
+ async has(key: string): Promise {
+ const val = await this.get(key);
+ return val !== undefined;
+ },
+
+ /**
+ * Sets a string value into multi-tier cache with an optional TTL.
+ *
+ * @param key - The cache key
+ * @param value - The serialized string value to cache
+ * @param ttl - Optional TTL in milliseconds
+ */
+ async set(key: string, value: string, ttl?: number): Promise {
+ Tempo.cache.set(key, value);
+ if (typeof ttl === 'number' && Number.isFinite(ttl) && ttl > 0) {
+ _entryExpiries.set(key, Date.now() + ttl);
+ } else {
+ _entryExpiries.delete(key);
+ }
+
+ const adapter = _state.config.cacheAdapter;
+ if (adapter?.set) {
+ try {
+ await adapter.set(key, value, ttl);
+ } catch { }
+ }
+ },
+
+ /**
+ * Returns an iterator over active in-memory cache entries.
+ */
+ entries(): IterableIterator<[string, string]> {
+ return Tempo.cache.entries();
+ },
+
+ /**
+ * Returns a plain object snapshot of active in-memory cache entries.
+ */
+ toJSON(): Record {
+ return Tempo.cache.toJSON();
+ },
+});
+
diff --git a/packages/plugins/ai/src/core/config.ts b/packages/plugins/ai/src/core/config.ts
index 9a963d6c..29e901bf 100644
--- a/packages/plugins/ai/src/core/config.ts
+++ b/packages/plugins/ai/src/core/config.ts
@@ -33,7 +33,7 @@ export const RESERVED_PROVIDER_IDS: ReadonlySet = new Set(['native', 'ca
export const DEFAULT_PROVIDERS: Readonly>> = secure({
groq: {
url: 'https://api.groq.com/openai/v1/chat/completions',
- model: 'llama-3.3-70b-versatile',
+ model: 'openai/gpt-oss-120b',
tokenParam: 'max_tokens'
},
openai: {
diff --git a/packages/plugins/ai/src/core/dispatch.ts b/packages/plugins/ai/src/core/dispatch.ts
index d6235dbe..9b6c5a8c 100644
--- a/packages/plugins/ai/src/core/dispatch.ts
+++ b/packages/plugins/ai/src/core/dispatch.ts
@@ -1,6 +1,7 @@
import { TempoAiError } from './error.js';
import { AiMode } from './config.js';
import { _state } from './init.js';
+import { logDebug, warnDebug } from './logger.js';
import type { AiProvider } from '../types/index.js';
/**
@@ -102,14 +103,22 @@ async function executeFallbackMode(
if (options?.minConfidence === undefined || confidence >= options.minConfidence)
return candidate;
- if (options?.debug)
- console.log(`[${options.tag || 'tempo-plugin-ai'}] Provider '${candidate.providerId}' confidence (${confidence}) below minConfidence (${options.minConfidence}). Cascading to next provider...`);
+ logDebug(
+ options?.tag || 'tempo-plugin-ai',
+ `Provider '${candidate.providerId}' confidence (${confidence}) below minConfidence (${options.minConfidence}). Cascading to next provider...`,
+ undefined,
+ { debug: options?.debug },
+ );
} catch (err: any) {
lastError = err;
if (err instanceof TempoAiError && err.code === 422 && options?.minConfidence === undefined) break;
- if (options?.debug)
- console.warn(`[${options.tag || 'tempo-plugin-ai'}] Provider '${provider.id}' failed:`, err);
+ warnDebug(
+ options?.tag || 'tempo-plugin-ai',
+ `Provider '${provider.id}' failed`,
+ err,
+ { debug: options?.debug },
+ );
}
}
@@ -363,7 +372,7 @@ async function executeAdaptiveMode(
if (limits) {
const resetMs = limits.resetAt?.epoch?.ms ?? now;
- isExhausted = limits.remainingRequests === 0 && resetMs > now;
+ isExhausted = (limits.remainingRequests === 0 || limits.remainingTokens === 0) && resetMs > now;
}
return {
@@ -387,6 +396,55 @@ async function executeAdaptiveMode(
return executeFallbackMode(sortedProviders, task, options);
}
+/**
+ * Checks if a provider has exhausted its request quota and is currently within an active cooldown window.
+ *
+ * @internal
+ */
+export function isProviderInCooldown(provider: AiProvider, now = Date.now()): boolean {
+ const limits = _state.providerLimits.get(provider.id);
+ if (!limits) return false;
+ const resetMs = limits.resetAt?.epoch?.ms ?? now;
+ const isExhausted = limits.remainingRequests === 0 || limits.remainingTokens === 0;
+ return isExhausted && resetMs > now;
+}
+
+/**
+ * Filters out providers currently in an active rate-limit cooldown window,
+ * provided there is at least one non-exhausted provider available.
+ * If all providers are in cooldown, returns all providers so execution can attempt or fail naturally.
+ *
+ * @internal
+ */
+export function filterCooldownProviders(
+ providers: AiProvider[],
+ options?: ExecuteModeOptions,
+): AiProvider[] {
+ if (providers.length <= 1) return providers;
+ const now = Date.now();
+ const available: AiProvider[] = [];
+ const skipped: string[] = [];
+
+ for (const p of providers) {
+ if (isProviderInCooldown(p, now)) {
+ skipped.push(p.id);
+ } else {
+ available.push(p);
+ }
+ }
+
+ if (available.length > 0 && skipped.length > 0) {
+ logDebug(
+ options?.tag || 'tempo-plugin-ai',
+ `Proactively filtered ${skipped.length} provider(s) in active 429 cooldown: ${skipped.join(', ')}`,
+ undefined,
+ { debug: options?.debug },
+ );
+ return available;
+ }
+ return providers;
+}
+
/**
* ## executeWithMode
* Central multi-provider execution orchestrator for Tempo AI plugins.
@@ -413,21 +471,23 @@ export async function executeWithMode(
task: ProviderTask,
options?: ExecuteModeOptions,
): Promise> {
+ const effectiveProviders = filterCooldownProviders(providers, options);
+
switch (mode) {
case AiMode.Fallback:
- return executeFallbackMode(providers, task, options);
+ return executeFallbackMode(effectiveProviders, task, options);
case AiMode.Race:
- return executeRaceMode(providers, task, options);
+ return executeRaceMode(effectiveProviders, task, options);
case AiMode.Consensus:
- return executeConsensusMode(providers, task);
+ return executeConsensusMode(effectiveProviders, task);
case AiMode.Hedged:
- return executeHedgedMode(providers, task, options);
+ return executeHedgedMode(effectiveProviders, task, options);
case AiMode.RoundRobin:
- return executeRoundRobinMode(providers, task, options);
+ return executeRoundRobinMode(effectiveProviders, task, options);
case AiMode.Adaptive:
return executeAdaptiveMode(providers, task, options);
@@ -436,3 +496,4 @@ export async function executeWithMode(
throw new TempoAiError(`Invalid execution mode: '${mode}'. Supported modes: ${Object.values(AiMode).map(m => `'${m}'`).join(', ')}.`, 400);
}
}
+
diff --git a/packages/plugins/ai/src/core/error.ts b/packages/plugins/ai/src/core/error.ts
index 4dce5b55..bcadddc1 100644
--- a/packages/plugins/ai/src/core/error.ts
+++ b/packages/plugins/ai/src/core/error.ts
@@ -11,12 +11,12 @@ export class TempoAiError extends Error {
/** A Tempo instance representing the rate limit reset time (extracted from Headers) */
#retryAt?: Tempo | undefined;
- constructor(message: string, code: number, retryAt?: Tempo) {
- super(message);
- this.name = 'TempoAiError';
- this.#code = code;
- this.#retryAt = retryAt;
- }
+ constructor(message: string, code: number, retryAt?: Tempo, options?: ErrorOptions) {
+ super(message, options);
+ this.name = 'TempoAiError';
+ this.#code = code;
+ this.#retryAt = retryAt;
+ }
get code(): number {
return this.#code;
diff --git a/packages/plugins/ai/src/core/init.ts b/packages/plugins/ai/src/core/init.ts
index 1ee7a9d1..94ffa31a 100644
--- a/packages/plugins/ai/src/core/init.ts
+++ b/packages/plugins/ai/src/core/init.ts
@@ -1,7 +1,8 @@
import { Tempo } from '@magmacomputing/tempo';
import { getResolvedProviderDefaults, loadRemoteManifest, resetManifestCache } from './manifest.js';
-import { normalizeCacheInput, assertNoReservedProviderId } from './support.js';
+import { assertNoReservedProviderId } from './support.js';
+import { warnDebug } from './logger.js';
import type { AiConfig, AiRateLimits, AiProvider } from '../types/index.js';
/**
@@ -71,39 +72,41 @@ export function initAI(config: AiConfig): Promise {
Tempo.init({ cache: config.cache, silent: true });
}
- return (async () => {
- if (remoteUrl !== false) {
- try {
- await loadRemoteManifest(remoteUrl, undefined, config.debug ?? _state.config.debug);
- } catch { }
- }
+ return (async () => {
+ if (remoteUrl !== false) {
+ try {
+ await loadRemoteManifest(remoteUrl, undefined, config.debug ?? _state.config.debug);
+ } catch { }
+ }
- if (_state.revision !== currentRevision) return;
+ if (_state.revision !== currentRevision) return;
- const fetchDefaults = config.fetchDefaults ?? _state.config.fetchDefaults;
- const currentProviders = callerProviders;
+ const fetchDefaults = config.fetchDefaults ?? _state.config.fetchDefaults;
+ const currentProviders = callerProviders;
- if (fetchDefaults && currentProviders) {
- const asyncProviders = await Promise.all(currentProviders.map(async p => {
- const normalizedId = p.id?.toLowerCase() ?? '';
- const defaults = getResolvedProviderDefaults(normalizedId, remoteUrl, config.debug ?? _state.config.debug);
- let hookOptions: Partial | null = null;
- try {
- hookOptions = await fetchDefaults(normalizedId);
- } catch { }
- return {
- ...defaults,
- ...(hookOptions ?? {}),
- ...p,
- } as AiProvider;
- }));
- if (_state.revision === currentRevision)
- _state.config.providers = asyncProviders;
- } else if (currentProviders) {
- if (_state.revision === currentRevision)
- _state.config.providers = resolveSyncProviders(currentProviders);
- }
- })();
+ if (fetchDefaults && currentProviders) {
+ const asyncProviders = await Promise.all(currentProviders.map(async p => {
+ const normalizedId = p.id?.toLowerCase() ?? '';
+ const defaults = getResolvedProviderDefaults(normalizedId, remoteUrl, config.debug ?? _state.config.debug);
+ let hookOptions: Partial | null = null;
+ try {
+ hookOptions = await fetchDefaults(normalizedId);
+ } catch (err: any) {
+ warnDebug('tempo-plugin-ai:init', `fetchDefaults hook failed for provider '${normalizedId}'`, err, { debug: config.debug ?? _state.config.debug });
+ }
+ return {
+ ...defaults,
+ ...(hookOptions ?? {}),
+ ...p,
+ } as AiProvider;
+ }));
+ if (_state.revision === currentRevision)
+ _state.config.providers = asyncProviders;
+ } else if (currentProviders) {
+ if (_state.revision === currentRevision)
+ _state.config.providers = resolveSyncProviders(currentProviders);
+ }
+ })();
}
/**
@@ -119,53 +122,6 @@ export function resetAI(): void {
resetManifestCache();
}
-/**
- * Clears AI parsing results from the in-memory cache and any external storage adapters.
- * If specific input strings or keys are provided, selectively purges only those entries.
- *
- * @param input - Optional string key, date string, or array of strings to purge from the cache
- * @returns A Promise that resolves once cache eviction is completed
- * @example
- * ```ts
- * await clearAiCache('next tuesday');
- * await clearAiCache(); // Clears all cached AI entries
- * ```
- */
-export async function clearAiCache(input?: string | string[]): Promise {
- const adapter = _state.config.cacheAdapter;
-
- if (!input) {
- Tempo.cache.clear();
- if (adapter?.clear) {
- try {
- await Promise.resolve(adapter.clear()).catch(() => { });
- } catch { }
- }
- return;
- }
-
- const inputs = Array.isArray(input) ? input : [input];
- for (const i of inputs) {
- const normalized = normalizeCacheInput(i);
- const prefix = `${normalized}::`;
- Tempo.cache.delete(normalized);
- Tempo.cache.delete(i);
- Tempo.cache.deletePrefix(prefix);
-
- if (adapter) {
- try {
- if (adapter.delete) {
- await Promise.resolve(adapter.delete(normalized)).catch(() => { });
- await Promise.resolve(adapter.delete(i)).catch(() => { });
- }
- if (adapter.clear) {
- await Promise.resolve(adapter.clear(prefix)).catch(() => { });
- }
- } catch { }
- }
- }
-}
-
/**
* Retrieves the latest observed rate limits across all provider responses.
*
diff --git a/packages/plugins/ai/src/core/logger.ts b/packages/plugins/ai/src/core/logger.ts
new file mode 100644
index 00000000..ae7a676c
--- /dev/null
+++ b/packages/plugins/ai/src/core/logger.ts
@@ -0,0 +1,192 @@
+import { _state } from './init.js';
+
+export const CUSTOM_INSPECT_SYMBOL = Symbol.for('nodejs.util.inspect.custom');
+
+const RE_EMAIL = /[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+/g;
+const RE_PHONE = /(?:\+?\d{1,3}[-.\s])?\(?\d{3}\)?[-.\s]\d{3}[-.\s](\d{4})\b/g;
+const RE_BEARER = /Bearer\s+[A-Za-z0-9_\-\.]+/gi;
+const RE_API_KEY = /\b(?:sk-[a-zA-Z0-9_\-]{6,}|gsk_[a-zA-Z0-9_\-]{6,}|key-[a-zA-Z0-9_\-]{6,})\b/gi;
+
+/**
+ * Universal runtime environment detector.
+ * Safely checks if the execution context is in production mode.
+ */
+export function isProductionEnvironment(): boolean {
+ try {
+ if (typeof process === 'undefined' || !process?.env) return false;
+ const nodeEnv = (process.env.NODE_ENV ?? '').toLowerCase();
+ if (nodeEnv === 'production' || nodeEnv === 'prod' || nodeEnv === 'live') return true;
+ if (process.env.PROD === 'true' || process.env.PRODUCTION === 'true') return true;
+ return false;
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * Sanitizes and masks potential PII from strings for terminal/log output.
+ *
+ * In production mode:
+ * - Emails are masked (e.g. `j***@example.com`)
+ * - Phone numbers are masked (e.g. `***-***-5309`)
+ * - Bearer tokens and API keys are redacted
+ *
+ * In development mode:
+ * - Full fidelity text is preserved for local debugging.
+ */
+export function maskPii(input: string, isProd: boolean = isProductionEnvironment()): string {
+ if (typeof input !== 'string') return String(input);
+ if (!isProd) return input;
+
+ return input
+ .replace(RE_BEARER, match => {
+ const token = match.replace(/Bearer\s+/i, '');
+ if (token.length <= 8) return 'Bearer [REDACTED]';
+ return `Bearer ${token.slice(0, 4)}...${token.slice(-4)}`;
+ })
+ .replace(RE_API_KEY, match => {
+ if (match.length <= 8) return '[REDACTED_KEY]';
+ return `${match.slice(0, 5)}...${match.slice(-4)}`;
+ })
+ .replace(RE_EMAIL, match => {
+ const parts = match.split('@');
+ const name = parts[0] || '';
+ const domain = parts[1] || '';
+ return `${name.slice(0, 1)}***@${domain}`;
+ })
+ .replace(RE_PHONE, '***-***-$1');
+}
+
+/**
+ * Sanitizes arbitrary objects, arrays, or primitives for safe log printing.
+ * Protects against circular object references via a visited tracker.
+ */
+export function sanitizeForLog(
+ data: any,
+ isProd: boolean = isProductionEnvironment(),
+ visited: WeakSet