@kuralle-agents/core 0.7.2 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +6 -0
  2. package/dist/capabilities/validators/groundingValidator.d.ts +21 -0
  3. package/dist/capabilities/validators/groundingValidator.js +109 -0
  4. package/dist/escalation/escalation.d.ts +20 -0
  5. package/dist/escalation/escalation.js +85 -0
  6. package/dist/escalation/types.d.ts +40 -0
  7. package/dist/eval/simulation.d.ts +99 -0
  8. package/dist/eval/simulation.js +168 -0
  9. package/dist/flow/collectUntilComplete.js +5 -2
  10. package/dist/flow/runFlow.js +6 -3
  11. package/dist/index.d.ts +25 -3
  12. package/dist/index.js +12 -0
  13. package/dist/memory/factMemoryService.d.ts +28 -0
  14. package/dist/memory/factMemoryService.js +137 -0
  15. package/dist/memory/index.d.ts +1 -0
  16. package/dist/memory/index.js +1 -0
  17. package/dist/processors/builtin/moderationGuard.d.ts +24 -0
  18. package/dist/processors/builtin/moderationGuard.js +101 -0
  19. package/dist/processors/builtin/piiGuard.d.ts +41 -0
  20. package/dist/processors/builtin/piiGuard.js +143 -0
  21. package/dist/processors/builtin/promptInjectionGuard.d.ts +16 -0
  22. package/dist/processors/builtin/promptInjectionGuard.js +28 -0
  23. package/dist/runtime/Runtime.d.ts +63 -2
  24. package/dist/runtime/Runtime.js +185 -10
  25. package/dist/runtime/channels/TextDriver.js +6 -0
  26. package/dist/runtime/channels/VoiceDriver.js +6 -0
  27. package/dist/runtime/channels/inputBuffer.d.ts +4 -3
  28. package/dist/runtime/closeRun.js +4 -0
  29. package/dist/runtime/compaction.d.ts +51 -0
  30. package/dist/runtime/compaction.js +111 -0
  31. package/dist/runtime/ctx.js +9 -0
  32. package/dist/runtime/hostLoop.d.ts +2 -0
  33. package/dist/runtime/hostLoop.js +7 -1
  34. package/dist/runtime/index.d.ts +1 -0
  35. package/dist/runtime/index.js +4 -0
  36. package/dist/runtime/openRun.d.ts +9 -2
  37. package/dist/runtime/openRun.js +26 -2
  38. package/dist/runtime/policies/agentTurn.d.ts +4 -0
  39. package/dist/runtime/policies/agentTurn.js +35 -4
  40. package/dist/runtime/userInput.d.ts +24 -0
  41. package/dist/runtime/userInput.js +67 -0
  42. package/dist/scheduler/index.d.ts +89 -0
  43. package/dist/scheduler/index.js +104 -0
  44. package/dist/testing/mocks.d.ts +2 -1
  45. package/dist/types/channel.d.ts +4 -1
  46. package/dist/types/run-context.d.ts +4 -0
  47. package/dist/types/stream.d.ts +29 -0
  48. package/guides/GUARDRAILS.md +44 -0
  49. package/package.json +2 -2
package/dist/index.d.ts CHANGED
@@ -12,6 +12,8 @@ export { buildMarkOutcomeTool, OUTCOMES_MARK_TOOL_NAME } from './outcomes/index.
12
12
  export { EvalRunner } from './eval/EvalRunner.js';
13
13
  export { scoreTurn, aggregateScores } from './eval/scoring.js';
14
14
  export type { EvalScenario, EvalTurn, ScenarioScore, TurnScore } from './eval/types.js';
15
+ export { simulateConversation, createJudge, runSimulationSuite, DEFAULT_JUDGE_DIMENSIONS, } from './eval/simulation.js';
16
+ export type { SimulatedUserPersona, SimulatedTranscriptTurn, SimulationResult, SimulationEnd, SimulatableRuntime, SimulateConversationOptions, JudgeDimension, JudgeVerdict, CreateJudgeOptions, ConversationJudge, SimulationScenario, SimulationSuiteResult, RunSimulationSuiteOptions, } from './eval/simulation.js';
15
17
  export { SessionWorkingMemory } from './runtime/WorkingMemory.js';
16
18
  export type { Tool, ToolSet, ToolDefinition, ToolWithFiller, ToolExecutionOptions, ToolExecutionContext, } from './tools/Tool.js';
17
19
  export { createTool, createToolWithFiller } from './tools/Tool.js';
@@ -34,6 +36,14 @@ export type { Metrics } from './hooks/builtin/metrics.js';
34
36
  export { createObservabilityHooks } from './hooks/builtin/observability.js';
35
37
  export type { ObservabilityConfig } from './hooks/builtin/observability.js';
36
38
  export type { SessionTrace, TraceStreamEvent } from './types/telemetry.js';
39
+ export { createPromptInjectionGuard } from './processors/builtin/promptInjectionGuard.js';
40
+ export type { PromptInjectionGuardOptions } from './processors/builtin/promptInjectionGuard.js';
41
+ export { createPiiInputGuard, createPiiOutputGuard, redactPii, } from './processors/builtin/piiGuard.js';
42
+ export type { PiiDetector, PiiGuardOptions, PiiMatch, PiiScanResult, } from './processors/builtin/piiGuard.js';
43
+ export { createModerationGuard, createModerationOutputGuard, } from './processors/builtin/moderationGuard.js';
44
+ export type { ModerationGuardOptions } from './processors/builtin/moderationGuard.js';
45
+ export { createGroundingValidator } from './capabilities/validators/groundingValidator.js';
46
+ export type { GroundingValidatorOptions } from './capabilities/validators/groundingValidator.js';
37
47
  export { ToolEnforcer, createToolEnforcer } from './guards/ToolEnforcer.js';
38
48
  export * as StopConditions from './guards/StopConditions.js';
39
49
  export * from './guards/rules.js';
@@ -47,6 +57,8 @@ export type { MemoryEntry, SearchMemoryRequest, SearchMemoryResponse, MemoryInge
47
57
  export { InMemoryMemoryService } from './memory/index.js';
48
58
  export { preloadMemoryContext } from './memory/index.js';
49
59
  export { extractMemories } from './memory/index.js';
60
+ export { createFactMemoryService } from './memory/index.js';
61
+ export type { FactMemoryServiceOptions } from './memory/index.js';
50
62
  export type { PersistentMemoryStore, PersistentMemoryBlock, PersistentMemoryConfig, MemoryBlockScope, WorkingMemoryBlockSpec, WorkingMemoryConfig, } from './memory/index.js';
51
63
  export { FilePersistentMemoryStore, InMemoryPersistentMemoryStore, RoutedPersistentMemoryStore, TieredPersistentMemoryStore, scanMemoryWrite, buildMemoryBlockTool, DEFAULT_BLOCK_CHAR_LIMIT, DEFAULT_AUTO_LOAD_BLOCKS, } from './memory/index.js';
52
64
  export type { RoutedPersistentMemoryStoreConfig, MemoryRouteFn, } from './memory/index.js';
@@ -55,6 +67,10 @@ export { resolveAgentWorkspace, type ResolvedAgentWorkspace, } from './runtime/r
55
67
  export { DEFAULT_CONTEXT_BUDGET, VOICE_CONTEXT_BUDGET, computeMessageHistoryBudget, truncateToTokenBudget, formatMemoryWithBudget, estimateTokenCount, ContextBudget, } from './runtime/ContextBudget.js';
56
68
  export type { ContextBudgetConfig } from './runtime/ContextBudget.js';
57
69
  export { TokenAccumulator } from './runtime/TokenAccumulator.js';
70
+ export { compactMessages, estimateMessagesTokens, DEFAULT_COMPACTION_TRIGGER_TOKENS, DEFAULT_COMPACTION_KEEP_RECENT, } from './runtime/compaction.js';
71
+ export type { CompactionConfig, CompactionResult } from './runtime/compaction.js';
72
+ export { isContextOverflowError, recoverFromContextOverflow, } from './runtime/contextOverflow.js';
73
+ export type { OverflowRecoveryResult } from './runtime/contextOverflow.js';
58
74
  export { applyPromptCache, applyAnthropicCacheControl, buildOpenAIResponsesProviderOptions, isAnthropicLanguageModel, isOpenAIResponsesModel, } from './runtime/promptCache.js';
59
75
  export type { AnthropicCacheTtl, OpenAIResponsesCompactOptions } from './runtime/promptCache.js';
60
76
  export { handoffFilters, removeToolHistory, keepRecentMessages, removeKeys, composeFilters, } from './runtime/handoffFilters.js';
@@ -64,7 +80,10 @@ export type { ToolExecutor, ConversationState, ConversationEventLog, Conversatio
64
80
  export type * from './types/index.js';
65
81
  export { CapabilityHost, ExtractionCapability, GuardrailCapability, AutoRetrieveCapability, PassThroughRefinement, PassThroughValidation, toGeminiDeclarations, toAISDKTools, } from './capabilities/index.js';
66
82
  export type { Capability, CapabilityAction, ExtractionToolResponseEnvelope, FlowReconfigureTransition, ToolDeclaration, PromptSection as CapabilityPromptSection, GeminiFunctionDeclaration, ExtractionCapabilityConfig, RetrieveProvider, AutoRetrieveCapabilityConfig, RefinementCapability, RefineInput, RefineDecision, ValidationCapability, ValidateInput, ValidateDecision, SourceRef, } from './capabilities/index.js';
67
- export type { EscalationOutcome, EscalationReason } from './escalation/types.js';
83
+ export type { EscalationOutcome, EscalationReason, EscalationRequest, EscalationHandler, EscalationConfig, } from './escalation/types.js';
84
+ export { buildEscalationRequest, ensureSessionMetadata } from './escalation/escalation.js';
85
+ export { createInProcessScheduler, createWakeJobRunner, createScheduleFollowupTool, wakeJob, isWakeJob, WAKE_JOB_KIND, } from './scheduler/index.js';
86
+ export type { Scheduler, ScheduledJob, InjectableTimer, WakeOptions, WakeJobPayload, WakeDelivery, WakeRunnable, } from './scheduler/index.js';
68
87
  export { filterAuditEntries } from './audit/index.js';
69
88
  export type { AuditConfig, AuditEntryBase, AuditEntryType, AuditListOptions, AuditReplayOptions, ConversationAuditEntry, ConversationAuditLog, } from './audit/types.js';
70
89
  export type { RealtimeAudioClient, RealtimeSessionConfig, RealtimeAudioConfig, RealtimeToolResponse, RealtimeEventMap, RealtimeSessionHandle, } from './realtime/index.js';
@@ -90,10 +109,13 @@ export type { HarnessStreamPart } from './types/stream.js';
90
109
  export { harnessToUIMessageStream } from './ai-sdk/uiMessageStream.js';
91
110
  export type { KuralleMetadata, KuralleDataParts, KuralleUIMessage, } from './ai-sdk/uiMessageStream.js';
92
111
  export type { ChoiceOption, ResolvedSelection } from './types/selection.js';
93
- export type { RunState, StepRecord } from './runtime/durable/types.js';
112
+ export type { RunState, StepRecord, SignalDelivery, SessionDurableRuns, PersistedRun, } from './runtime/durable/types.js';
113
+ export { DURABLE_RUNS_KEY } from './runtime/durable/types.js';
94
114
  export type { RunStore } from './runtime/durable/RunStore.js';
95
115
  export type { ChannelDriver, TextDriver } from './runtime/channels/index.js';
96
116
  export type { TurnResult } from './types/channel.js';
97
- export type { RunContext } from './types/run-context.js';
117
+ export type { RunContext, ToolContext, ActionContext } from './types/run-context.js';
118
+ export type { AnyTool } from './types/effectTool.js';
98
119
  export { createRuntime, Runtime, type HarnessConfig, type RunOptions, } from './runtime/Runtime.js';
99
120
  export type { RuntimeLike } from './runtime/RuntimeLike.js';
121
+ export { userInputToText, hasMediaParts, transcribeAudioParts, type UserInputContent, } from './runtime/userInput.js';
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ export { InMemoryConversationStore } from './conversations/index.js';
7
7
  export { buildMarkOutcomeTool, OUTCOMES_MARK_TOOL_NAME } from './outcomes/index.js';
8
8
  export { EvalRunner } from './eval/EvalRunner.js';
9
9
  export { scoreTurn, aggregateScores } from './eval/scoring.js';
10
+ export { simulateConversation, createJudge, runSimulationSuite, DEFAULT_JUDGE_DIMENSIONS, } from './eval/simulation.js';
10
11
  export { SessionWorkingMemory } from './runtime/WorkingMemory.js';
11
12
  export { createTool, createToolWithFiller } from './tools/Tool.js';
12
13
  export { createHandoffTool, isHandoffResult } from './tools/handoff.js';
@@ -20,6 +21,10 @@ export { HookRunner, createHookRunner } from './hooks/HookRunner.js';
20
21
  export { loggingHooks, createLoggingHooks } from './hooks/builtin/logging.js';
21
22
  export { createMetricsHooks, InMemoryMetrics } from './hooks/builtin/metrics.js';
22
23
  export { createObservabilityHooks } from './hooks/builtin/observability.js';
24
+ export { createPromptInjectionGuard } from './processors/builtin/promptInjectionGuard.js';
25
+ export { createPiiInputGuard, createPiiOutputGuard, redactPii, } from './processors/builtin/piiGuard.js';
26
+ export { createModerationGuard, createModerationOutputGuard, } from './processors/builtin/moderationGuard.js';
27
+ export { createGroundingValidator } from './capabilities/validators/groundingValidator.js';
23
28
  export { ToolEnforcer, createToolEnforcer } from './guards/ToolEnforcer.js';
24
29
  export * as StopConditions from './guards/StopConditions.js';
25
30
  export * from './guards/rules.js';
@@ -29,15 +34,20 @@ export { getTemplate, registerTemplate, listTemplates, getAllTemplates } from '.
29
34
  export { InMemoryMemoryService } from './memory/index.js';
30
35
  export { preloadMemoryContext } from './memory/index.js';
31
36
  export { extractMemories } from './memory/index.js';
37
+ export { createFactMemoryService } from './memory/index.js';
32
38
  export { FilePersistentMemoryStore, InMemoryPersistentMemoryStore, RoutedPersistentMemoryStore, TieredPersistentMemoryStore, scanMemoryWrite, buildMemoryBlockTool, DEFAULT_BLOCK_CHAR_LIMIT, DEFAULT_AUTO_LOAD_BLOCKS, } from './memory/index.js';
33
39
  export { wireWorkingMemory, loadWorkingMemoryBlocks, formatWorkingMemorySection, resolveWorkingMemoryStore, } from './runtime/grounding/workingMemory.js';
34
40
  export { resolveAgentWorkspace, } from './runtime/resolveAgentWorkspace.js';
35
41
  export { DEFAULT_CONTEXT_BUDGET, VOICE_CONTEXT_BUDGET, computeMessageHistoryBudget, truncateToTokenBudget, formatMemoryWithBudget, estimateTokenCount, ContextBudget, } from './runtime/ContextBudget.js';
36
42
  export { TokenAccumulator } from './runtime/TokenAccumulator.js';
43
+ export { compactMessages, estimateMessagesTokens, DEFAULT_COMPACTION_TRIGGER_TOKENS, DEFAULT_COMPACTION_KEEP_RECENT, } from './runtime/compaction.js';
44
+ export { isContextOverflowError, recoverFromContextOverflow, } from './runtime/contextOverflow.js';
37
45
  export { applyPromptCache, applyAnthropicCacheControl, buildOpenAIResponsesProviderOptions, isAnthropicLanguageModel, isOpenAIResponsesModel, } from './runtime/promptCache.js';
38
46
  export { handoffFilters, removeToolHistory, keepRecentMessages, removeKeys, composeFilters, } from './runtime/handoffFilters.js';
39
47
  export { DefaultToolExecutor, DefaultConversationState, DefaultConversationEventLog, DefaultAgentStateController, createFoundation, } from './foundation/index.js';
40
48
  export { CapabilityHost, ExtractionCapability, GuardrailCapability, AutoRetrieveCapability, PassThroughRefinement, PassThroughValidation, toGeminiDeclarations, toAISDKTools, } from './capabilities/index.js';
49
+ export { buildEscalationRequest, ensureSessionMetadata } from './escalation/escalation.js';
50
+ export { createInProcessScheduler, createWakeJobRunner, createScheduleFollowupTool, wakeJob, isWakeJob, WAKE_JOB_KIND, } from './scheduler/index.js';
41
51
  export { filterAuditEntries } from './audit/index.js';
42
52
  export { defineAgent, defineFlow, reply, collect, action, decide, confirmGate, } from './authoring/index.js';
43
53
  export { defineTool } from './types/effectTool.js';
@@ -47,4 +57,6 @@ export { SkillsCapability, wireAgentSkills, collectRegisteredNames, validateSkil
47
57
  export { buildToolSet, toolToAiSdk, wrapAiSdkTool, ToolApprovalDeniedError, ToolTimeoutError, } from './tools/effect/index.js';
48
58
  export { parseConfirmation } from './flow/confirmParse.js';
49
59
  export { harnessToUIMessageStream } from './ai-sdk/uiMessageStream.js';
60
+ export { DURABLE_RUNS_KEY } from './runtime/durable/types.js';
50
61
  export { createRuntime, Runtime, } from './runtime/Runtime.js';
62
+ export { userInputToText, hasMediaParts, transcribeAudioParts, } from './runtime/userInput.js';
@@ -0,0 +1,28 @@
1
+ import type { LanguageModel } from 'ai';
2
+ import type { MemoryService } from './MemoryService.js';
3
+ import type { PersistentMemoryStore } from './blocks/types.js';
4
+ /**
5
+ * Fact-extracting cross-session memory, backed by a `PersistentMemoryStore`
6
+ * block per user (scope `user`, owner = `session.userId`, one fact per line).
7
+ *
8
+ * Unlike the raw-message ingestion path (`extractMemories`), this service
9
+ * runs an LLM merge at ingest: existing facts + the new transcript produce
10
+ * the COMPLETE updated fact list (still-true facts kept, changed ones
11
+ * updated, obsolete ones dropped) — so the block stays small, current, and
12
+ * deduplicated instead of growing append-only.
13
+ *
14
+ * Works on every block backend: file (node), Postgres/Redis adapters, and
15
+ * Cloudflare DO SQLite via `SqlPersistentMemoryStore`.
16
+ */
17
+ export interface FactMemoryServiceOptions {
18
+ store: PersistentMemoryStore;
19
+ /** Extractor/merger model, run at temperature 0. */
20
+ model: LanguageModel;
21
+ /** Block key per user. Default: `'FACTS'`. */
22
+ blockKey?: string;
23
+ /** Char limit for the fact block. Default: `DEFAULT_BLOCK_CHAR_LIMIT`. */
24
+ charLimit?: number;
25
+ /** Max facts kept per user. Default: 25. */
26
+ maxFacts?: number;
27
+ }
28
+ export declare function createFactMemoryService(options: FactMemoryServiceOptions): MemoryService;
@@ -0,0 +1,137 @@
1
+ import { generateObject } from 'ai';
2
+ import { z } from 'zod';
3
+ import { DEFAULT_BLOCK_CHAR_LIMIT } from './blocks/types.js';
4
+ import { scanMemoryWrite } from './blocks/safetyScanner.js';
5
+ const factsSchema = z.object({
6
+ facts: z.array(z.string()),
7
+ });
8
+ function parseFactLines(content) {
9
+ return content
10
+ .split('\n')
11
+ .map((line) => line.replace(/^-\s*/, '').trim())
12
+ .filter((line) => line.length > 0);
13
+ }
14
+ function renderTranscript(session) {
15
+ const lines = [];
16
+ for (const message of session.messages) {
17
+ if (message.role !== 'user' && message.role !== 'assistant')
18
+ continue;
19
+ const content = typeof message.content === 'string'
20
+ ? message.content
21
+ : Array.isArray(message.content)
22
+ ? message.content
23
+ .filter((part) => part.type === 'text')
24
+ .map((part) => part.text)
25
+ .join('\n')
26
+ : '';
27
+ if (content.trim()) {
28
+ lines.push(`${message.role}: ${content}`);
29
+ }
30
+ }
31
+ return lines.join('\n');
32
+ }
33
+ const QUERY_TOKEN_MIN_LENGTH = 4;
34
+ function lexicalScore(query, fact) {
35
+ const factLower = fact.toLowerCase();
36
+ const tokens = query
37
+ .toLowerCase()
38
+ .split(/[^a-z0-9]+/)
39
+ .filter((token) => token.length >= QUERY_TOKEN_MIN_LENGTH);
40
+ if (tokens.length === 0)
41
+ return 0;
42
+ let hits = 0;
43
+ for (const token of tokens) {
44
+ if (factLower.includes(token))
45
+ hits += 1;
46
+ }
47
+ return hits / tokens.length;
48
+ }
49
+ export function createFactMemoryService(options) {
50
+ const blockKey = options.blockKey ?? 'FACTS';
51
+ const charLimit = options.charLimit ?? DEFAULT_BLOCK_CHAR_LIMIT;
52
+ const maxFacts = options.maxFacts ?? 25;
53
+ return {
54
+ async addSessionToMemory(session) {
55
+ if (!session.userId)
56
+ return;
57
+ const transcript = renderTranscript(session);
58
+ if (!transcript.trim())
59
+ return;
60
+ try {
61
+ const existing = await options.store.loadBlock('user', session.userId, blockKey);
62
+ const existingFacts = existing ? parseFactLines(existing.content) : [];
63
+ const { object } = await generateObject({
64
+ model: options.model,
65
+ schema: factsSchema,
66
+ temperature: 0,
67
+ system: [
68
+ 'You maintain the long-term memory of a customer-facing assistant.',
69
+ 'From the EXISTING FACTS and the NEW CONVERSATION, produce the complete updated fact list about this user.',
70
+ 'Keep facts that are still true, update ones that changed, drop obsolete or duplicate ones.',
71
+ 'Only durable facts worth remembering across conversations: stable preferences, profile details',
72
+ '(name, address, sizes), recurring context (orders they reference, their business).',
73
+ 'Exclude one-off details, small talk, and sensitive payment data (card numbers, passwords).',
74
+ `At most ${maxFacts} facts, each a single self-contained sentence under 200 characters.`,
75
+ ].join('\n'),
76
+ prompt: [
77
+ `EXISTING FACTS:\n${existingFacts.length > 0 ? existingFacts.map((f) => `- ${f}`).join('\n') : '(none)'}`,
78
+ `NEW CONVERSATION:\n${transcript}`,
79
+ ].join('\n\n'),
80
+ });
81
+ const safeFacts = object.facts
82
+ .map((fact) => fact.trim())
83
+ .filter((fact) => fact.length > 0 && scanMemoryWrite(fact).safe)
84
+ .slice(0, maxFacts);
85
+ let content = safeFacts.map((fact) => `- ${fact}`).join('\n');
86
+ while (content.length > charLimit && safeFacts.length > 0) {
87
+ safeFacts.pop();
88
+ content = safeFacts.map((fact) => `- ${fact}`).join('\n');
89
+ }
90
+ await options.store.saveBlock({
91
+ key: blockKey,
92
+ scope: 'user',
93
+ content,
94
+ charLimit,
95
+ updatedAt: new Date().toISOString(),
96
+ }, session.userId);
97
+ }
98
+ catch (error) {
99
+ // Memory must never take down a turn — ingest failures are logged, not thrown.
100
+ console.warn(`[Kuralle] fact-memory ingest failed for user ${session.userId}:`, error instanceof Error ? error.message : error);
101
+ }
102
+ },
103
+ async searchMemory(request) {
104
+ const block = await options.store.loadBlock('user', request.userId, blockKey);
105
+ if (!block) {
106
+ return { memories: [] };
107
+ }
108
+ const facts = parseFactLines(block.content);
109
+ const createdAt = block.updatedAt ? new Date(block.updatedAt) : new Date();
110
+ const limit = request.limit ?? 10;
111
+ const scored = facts.map((fact, index) => ({
112
+ fact,
113
+ index,
114
+ score: lexicalScore(request.query, fact),
115
+ }));
116
+ const relevant = scored.filter((entry) => entry.score > 0);
117
+ // Facts are few and curated: when nothing matches lexically, return them
118
+ // all (up to limit) — continuity beats false-negative emptiness.
119
+ const selected = (relevant.length > 0 ? relevant.sort((a, b) => b.score - a.score) : scored)
120
+ .slice(0, limit);
121
+ return {
122
+ memories: selected.map((entry) => ({
123
+ id: `${request.userId}:${blockKey}:${entry.index}`,
124
+ sessionId: 'fact-memory',
125
+ userId: request.userId,
126
+ content: entry.fact,
127
+ author: 'memory',
128
+ createdAt,
129
+ score: entry.score,
130
+ })),
131
+ };
132
+ },
133
+ async deleteMemories(userId) {
134
+ await options.store.deleteBlock('user', userId, blockKey);
135
+ },
136
+ };
137
+ }
@@ -3,6 +3,7 @@ export type { MemoryEntry, SearchMemoryRequest, SearchMemoryResponse, MemoryInge
3
3
  export { InMemoryMemoryService } from './stores/InMemoryMemoryService.js';
4
4
  export { preloadMemoryContext } from './preloadMemory.js';
5
5
  export { extractMemories } from './utils.js';
6
+ export { createFactMemoryService, type FactMemoryServiceOptions, } from './factMemoryService.js';
6
7
  export type { PersistentMemoryStore, PersistentMemoryBlock, PersistentMemoryConfig, MemoryBlockScope, } from './blocks/types.js';
7
8
  export type { WorkingMemoryBlockSpec, WorkingMemoryConfig } from '../types/grounding.js';
8
9
  export { DEFAULT_BLOCK_CHAR_LIMIT, DEFAULT_AUTO_LOAD_BLOCKS, } from './blocks/types.js';
@@ -1,6 +1,7 @@
1
1
  export { InMemoryMemoryService } from './stores/InMemoryMemoryService.js';
2
2
  export { preloadMemoryContext } from './preloadMemory.js';
3
3
  export { extractMemories } from './utils.js';
4
+ export { createFactMemoryService, } from './factMemoryService.js';
4
5
  export { DEFAULT_BLOCK_CHAR_LIMIT, DEFAULT_AUTO_LOAD_BLOCKS, } from './blocks/types.js';
5
6
  export { InMemoryPersistentMemoryStore } from './blocks/InMemoryPersistentMemoryStore.js';
6
7
  export { FilePersistentMemoryStore, } from './blocks/FilePersistentMemoryStore.js';
@@ -0,0 +1,24 @@
1
+ import type { LanguageModel } from 'ai';
2
+ import type { InputProcessor, OutputProcessor } from '../../types/processors.js';
3
+ export interface ModerationGuardOptions {
4
+ /** Classifier model. Use a small/fast model — this runs on every turn it guards. */
5
+ model: LanguageModel;
6
+ /** Policy categories to flag. Defaults cover the standard abuse set. */
7
+ categories?: string[];
8
+ /** User-facing message when content is flagged. */
9
+ message?: string;
10
+ /**
11
+ * What to do when the classifier itself errors. `allow` (default) fails open —
12
+ * deterministic guards still run and an outage does not take the agent down.
13
+ * `block` fails closed for zero-tolerance deployments.
14
+ */
15
+ onError?: 'allow' | 'block';
16
+ id?: string;
17
+ }
18
+ /**
19
+ * LLM moderation guard over inbound user text. Runs the configured model as a
20
+ * temperature-0 classifier against the policy categories and blocks on a match.
21
+ */
22
+ export declare function createModerationGuard(options: ModerationGuardOptions): InputProcessor;
23
+ /** LLM moderation guard over assistant output — the post-turn counterpart. */
24
+ export declare function createModerationOutputGuard(options: ModerationGuardOptions): OutputProcessor;
@@ -0,0 +1,101 @@
1
+ import { generateObject } from 'ai';
2
+ import { z } from 'zod';
3
+ const DEFAULT_CATEGORIES = [
4
+ 'violence or threats of violence',
5
+ 'sexual content involving minors',
6
+ 'self-harm encouragement or instructions',
7
+ 'hate or harassment targeting a protected group',
8
+ 'facilitation of clearly illegal activity (weapons, drugs, fraud)',
9
+ ];
10
+ const moderationSchema = z.object({
11
+ flagged: z.boolean(),
12
+ category: z.union([z.string(), z.null()]),
13
+ rationale: z.union([z.string(), z.null()]),
14
+ });
15
+ async function classify(model, categories, role, text, abortSignal) {
16
+ const { object } = await generateObject({
17
+ model,
18
+ schema: moderationSchema,
19
+ temperature: 0,
20
+ abortSignal,
21
+ system: [
22
+ `You are a content-policy classifier. Flag the ${role} ONLY if it clearly falls into one of these categories:`,
23
+ ...categories.map((category) => `- ${category}`),
24
+ 'Ordinary rudeness, frustration, or sensitive-but-legitimate topics (health, finance, returns/refunds) are NOT flagged.',
25
+ 'Respond with flagged=false unless the text is a clear match.',
26
+ ].join('\n'),
27
+ prompt: text,
28
+ });
29
+ return object;
30
+ }
31
+ /**
32
+ * LLM moderation guard over inbound user text. Runs the configured model as a
33
+ * temperature-0 classifier against the policy categories and blocks on a match.
34
+ */
35
+ export function createModerationGuard(options) {
36
+ const categories = options.categories ?? DEFAULT_CATEGORIES;
37
+ const onError = options.onError ?? 'allow';
38
+ return {
39
+ id: options.id ?? 'moderation-guard',
40
+ name: 'Moderation guard',
41
+ description: 'LLM content-policy classifier over user input.',
42
+ process: async ({ input, context }) => {
43
+ let verdict;
44
+ try {
45
+ verdict = await classify(options.model, categories, 'user message', input, context.abortSignal);
46
+ }
47
+ catch {
48
+ if (onError === 'block') {
49
+ return {
50
+ action: 'block',
51
+ reason: 'moderation-error',
52
+ message: options.message ?? "Sorry, I can't help with that.",
53
+ };
54
+ }
55
+ return { action: 'allow' };
56
+ }
57
+ if (!verdict.flagged) {
58
+ return { action: 'allow' };
59
+ }
60
+ return {
61
+ action: 'block',
62
+ reason: `moderation: ${verdict.category ?? 'flagged'}`,
63
+ message: options.message ?? "Sorry, I can't help with that.",
64
+ };
65
+ },
66
+ };
67
+ }
68
+ /** LLM moderation guard over assistant output — the post-turn counterpart. */
69
+ export function createModerationOutputGuard(options) {
70
+ const categories = options.categories ?? DEFAULT_CATEGORIES;
71
+ const onError = options.onError ?? 'allow';
72
+ return {
73
+ id: options.id ?? 'moderation-output-guard',
74
+ name: 'Moderation output guard',
75
+ description: 'LLM content-policy classifier over assistant output.',
76
+ process: async ({ text, context }) => {
77
+ let verdict;
78
+ try {
79
+ verdict = await classify(options.model, categories, 'assistant response', text, context.abortSignal);
80
+ }
81
+ catch {
82
+ if (onError === 'block') {
83
+ return {
84
+ action: 'block',
85
+ reason: 'moderation-error',
86
+ message: options.message ?? 'Sorry, I cannot provide that response.',
87
+ };
88
+ }
89
+ return { action: 'allow' };
90
+ }
91
+ if (!verdict.flagged) {
92
+ return { action: 'allow' };
93
+ }
94
+ return {
95
+ action: 'block',
96
+ reason: `moderation: ${verdict.category ?? 'flagged'}`,
97
+ message: options.message ?? 'Sorry, I cannot provide that response.',
98
+ };
99
+ },
100
+ };
101
+ }
@@ -0,0 +1,41 @@
1
+ import type { InputProcessor, OutputProcessor } from '../../types/processors.js';
2
+ /**
3
+ * Deterministic PII detection + redaction over message text.
4
+ *
5
+ * Detectors are conservative by default: `credit-card` (separator-tolerant,
6
+ * Luhn-validated so order/tracking numbers don't false-positive) and `email`.
7
+ * `phone` and `iban` are opt-in because their shapes collide with order ids,
8
+ * reference codes, and similar commerce artifacts.
9
+ */
10
+ export type PiiDetector = 'credit-card' | 'email' | 'phone' | 'iban';
11
+ export interface PiiGuardOptions {
12
+ /** Which detectors to run. Default: `['credit-card', 'email']`. */
13
+ detect?: PiiDetector[];
14
+ /** `redact` replaces matches in place; `block` refuses the message. Default: `redact`. */
15
+ mode?: 'redact' | 'block';
16
+ /** User-facing message when `mode: 'block'` trips. */
17
+ message?: string;
18
+ id?: string;
19
+ }
20
+ export interface PiiMatch {
21
+ detector: PiiDetector;
22
+ matchedText: string;
23
+ }
24
+ export interface PiiScanResult {
25
+ text: string;
26
+ matches: PiiMatch[];
27
+ }
28
+ /**
29
+ * Scan and redact PII in `text`. Returns the redacted text plus the list of
30
+ * matches (detector + original matched text) so callers can audit what was
31
+ * found without re-detecting.
32
+ */
33
+ export declare function redactPii(text: string, detect?: PiiDetector[]): PiiScanResult;
34
+ /**
35
+ * PII guard over inbound user text. `redact` mode rewrites the message before
36
+ * the model (and persisted history) sees the raw value — credit-card redaction
37
+ * inbound is the PCI-relevant default for commerce agents.
38
+ */
39
+ export declare function createPiiInputGuard(options?: PiiGuardOptions): InputProcessor;
40
+ /** PII guard over assistant output — stops the model echoing sensitive values back. */
41
+ export declare function createPiiOutputGuard(options?: PiiGuardOptions): OutputProcessor;
@@ -0,0 +1,143 @@
1
+ const DEFAULT_DETECTORS = ['credit-card', 'email'];
2
+ const EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
3
+ // 13-19 digits with optional single space/dash separators; candidates are
4
+ // Luhn-checked before redaction so plain long numbers survive.
5
+ const CARD_CANDIDATE_PATTERN = /\b(?:\d[ -]?){12,18}\d\b/g;
6
+ // Opt-in: international or separator-formatted numbers with 9+ digits total.
7
+ const PHONE_PATTERN = /(?:\+\d{1,3}[ -.]?)?(?:\(\d{2,4}\)[ -.]?)?\d{2,4}[ -.]\d{3,4}[ -.]?\d{3,4}\b|\+\d{9,15}\b/g;
8
+ const IBAN_PATTERN = /\b[A-Z]{2}\d{2}(?:[ ]?[A-Z0-9]){11,30}\b/g;
9
+ function luhnValid(digits) {
10
+ let sum = 0;
11
+ let double = false;
12
+ for (let index = digits.length - 1; index >= 0; index -= 1) {
13
+ let digit = digits.charCodeAt(index) - 48;
14
+ if (double) {
15
+ digit *= 2;
16
+ if (digit > 9)
17
+ digit -= 9;
18
+ }
19
+ sum += digit;
20
+ double = !double;
21
+ }
22
+ return sum % 10 === 0;
23
+ }
24
+ function ibanChecksumValid(candidate) {
25
+ const compact = candidate.replace(/\s+/g, '').toUpperCase();
26
+ if (compact.length < 15 || compact.length > 34)
27
+ return false;
28
+ const rearranged = compact.slice(4) + compact.slice(0, 4);
29
+ let remainder = 0;
30
+ for (const char of rearranged) {
31
+ const value = /[A-Z]/.test(char) ? String(char.charCodeAt(0) - 55) : char;
32
+ for (const digit of value) {
33
+ remainder = (remainder * 10 + (digit.charCodeAt(0) - 48)) % 97;
34
+ }
35
+ }
36
+ return remainder === 1;
37
+ }
38
+ /**
39
+ * Scan and redact PII in `text`. Returns the redacted text plus the list of
40
+ * matches (detector + original matched text) so callers can audit what was
41
+ * found without re-detecting.
42
+ */
43
+ export function redactPii(text, detect = DEFAULT_DETECTORS) {
44
+ const matches = [];
45
+ let current = text;
46
+ if (detect.includes('credit-card')) {
47
+ current = current.replace(CARD_CANDIDATE_PATTERN, (candidate) => {
48
+ const digits = candidate.replace(/[ -]/g, '');
49
+ if (digits.length < 13 || digits.length > 19 || !luhnValid(digits)) {
50
+ return candidate;
51
+ }
52
+ matches.push({ detector: 'credit-card', matchedText: candidate });
53
+ return '[redacted card number]';
54
+ });
55
+ }
56
+ if (detect.includes('iban')) {
57
+ current = current.replace(IBAN_PATTERN, (candidate) => {
58
+ if (!ibanChecksumValid(candidate)) {
59
+ return candidate;
60
+ }
61
+ matches.push({ detector: 'iban', matchedText: candidate });
62
+ return '[redacted iban]';
63
+ });
64
+ }
65
+ if (detect.includes('email')) {
66
+ current = current.replace(EMAIL_PATTERN, (candidate) => {
67
+ matches.push({ detector: 'email', matchedText: candidate });
68
+ return '[redacted email]';
69
+ });
70
+ }
71
+ if (detect.includes('phone')) {
72
+ current = current.replace(PHONE_PATTERN, (candidate) => {
73
+ const digits = candidate.replace(/\D/g, '');
74
+ if (digits.length < 9 || digits.length > 15) {
75
+ return candidate;
76
+ }
77
+ matches.push({ detector: 'phone', matchedText: candidate });
78
+ return '[redacted phone]';
79
+ });
80
+ }
81
+ return { text: current, matches };
82
+ }
83
+ /**
84
+ * PII guard over inbound user text. `redact` mode rewrites the message before
85
+ * the model (and persisted history) sees the raw value — credit-card redaction
86
+ * inbound is the PCI-relevant default for commerce agents.
87
+ */
88
+ export function createPiiInputGuard(options = {}) {
89
+ const detect = options.detect ?? DEFAULT_DETECTORS;
90
+ const mode = options.mode ?? 'redact';
91
+ return {
92
+ id: options.id ?? 'pii-input-guard',
93
+ name: 'PII input guard',
94
+ description: `Detects ${detect.join(', ')} in user input and ${mode}s it.`,
95
+ process: ({ input }) => {
96
+ const scan = redactPii(input, detect);
97
+ if (scan.matches.length === 0) {
98
+ return { action: 'allow' };
99
+ }
100
+ if (mode === 'block') {
101
+ return {
102
+ action: 'block',
103
+ reason: `pii-detected: ${scan.matches.map((m) => m.detector).join(', ')}`,
104
+ message: options.message ??
105
+ 'For your security, please do not share sensitive details like card numbers in chat.',
106
+ };
107
+ }
108
+ return {
109
+ action: 'modify',
110
+ input: scan.text,
111
+ reason: `pii-redacted: ${scan.matches.map((m) => m.detector).join(', ')}`,
112
+ };
113
+ },
114
+ };
115
+ }
116
+ /** PII guard over assistant output — stops the model echoing sensitive values back. */
117
+ export function createPiiOutputGuard(options = {}) {
118
+ const detect = options.detect ?? DEFAULT_DETECTORS;
119
+ const mode = options.mode ?? 'redact';
120
+ return {
121
+ id: options.id ?? 'pii-output-guard',
122
+ name: 'PII output guard',
123
+ description: `Detects ${detect.join(', ')} in assistant output and ${mode}s it.`,
124
+ process: ({ text }) => {
125
+ const scan = redactPii(text, detect);
126
+ if (scan.matches.length === 0) {
127
+ return { action: 'allow' };
128
+ }
129
+ if (mode === 'block') {
130
+ return {
131
+ action: 'block',
132
+ reason: `pii-detected: ${scan.matches.map((m) => m.detector).join(', ')}`,
133
+ message: options.message ?? 'Sorry, I cannot share that information.',
134
+ };
135
+ }
136
+ return {
137
+ action: 'modify',
138
+ text: scan.text,
139
+ reason: `pii-redacted: ${scan.matches.map((m) => m.detector).join(', ')}`,
140
+ };
141
+ },
142
+ };
143
+ }
@@ -0,0 +1,16 @@
1
+ import type { InputProcessor } from '../../types/processors.js';
2
+ export interface PromptInjectionGuardOptions {
3
+ /** User-facing message when a pattern matches. */
4
+ message?: string;
5
+ id?: string;
6
+ }
7
+ /**
8
+ * Deterministic prompt-injection guard over inbound user text. Reuses the
9
+ * injection pattern set that already protects persistent memory writes
10
+ * (`scanMemoryWrite`) — one audited pattern source, two enforcement points.
11
+ *
12
+ * False positive cost: the turn is refused with a polite message and the user
13
+ * can rephrase. False negative cost: instruction override — so the patterns
14
+ * err toward catching more.
15
+ */
16
+ export declare function createPromptInjectionGuard(options?: PromptInjectionGuardOptions): InputProcessor;