gavio 0.1.0 → 0.3.0

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 (160) hide show
  1. package/dist/cjs/config.js +106 -0
  2. package/dist/cjs/errors.js +29 -1
  3. package/dist/cjs/gateway.js +88 -0
  4. package/dist/cjs/index.js +4 -2
  5. package/dist/cjs/interceptors/audit/index.js +4 -1
  6. package/dist/cjs/interceptors/audit/interceptor.js +11 -0
  7. package/dist/cjs/interceptors/audit/record.js +17 -3
  8. package/dist/cjs/interceptors/audit/trace.js +43 -0
  9. package/dist/cjs/interceptors/cache/embedding.js +53 -0
  10. package/dist/cjs/interceptors/cache/index.js +9 -5
  11. package/dist/cjs/interceptors/cache/interceptor.js +80 -0
  12. package/dist/cjs/interceptors/cache/vector.js +35 -0
  13. package/dist/cjs/interceptors/governance/budget.js +45 -0
  14. package/dist/cjs/interceptors/governance/index.js +10 -0
  15. package/dist/cjs/interceptors/governance/model-policy.js +18 -0
  16. package/dist/cjs/interceptors/governance/rate-limit.js +46 -0
  17. package/dist/cjs/interceptors/guardrails/index.js +11 -0
  18. package/dist/cjs/interceptors/guardrails/interceptor.js +40 -0
  19. package/dist/cjs/interceptors/guardrails/validator.js +8 -0
  20. package/dist/cjs/interceptors/guardrails/validators/regex.js +32 -0
  21. package/dist/cjs/interceptors/guardrails/validators/schema.js +63 -0
  22. package/dist/cjs/interceptors/injection.js +62 -0
  23. package/dist/cjs/interceptors/metrics/index.js +9 -0
  24. package/dist/cjs/interceptors/metrics/interceptor.js +37 -0
  25. package/dist/cjs/interceptors/metrics/registry.js +0 -0
  26. package/dist/cjs/interceptors/quality/index.js +7 -0
  27. package/dist/cjs/interceptors/quality/risk.js +49 -0
  28. package/dist/cjs/interceptors/reliability/circuit-breaker.js +82 -0
  29. package/dist/cjs/interceptors/reliability/index.js +8 -1
  30. package/dist/cjs/interceptors/reliability/load-balancer.js +38 -0
  31. package/dist/cjs/interceptors/reliability/stream-buffer.js +28 -0
  32. package/dist/cjs/pricing.js +5 -1
  33. package/dist/cjs/providers/azure-openai.js +56 -0
  34. package/dist/cjs/providers/base.js +9 -0
  35. package/dist/cjs/providers/gemini.js +73 -0
  36. package/dist/cjs/providers/index.js +22 -6
  37. package/dist/cjs/providers/ollama.js +41 -0
  38. package/dist/cjs/request.js +3 -0
  39. package/dist/cjs/shim/openai.js +57 -0
  40. package/dist/cjs/types.js +53 -1
  41. package/dist/esm/config.d.ts +12 -0
  42. package/dist/esm/config.js +102 -0
  43. package/dist/esm/errors.d.ts +17 -0
  44. package/dist/esm/errors.js +24 -0
  45. package/dist/esm/gateway.d.ts +18 -1
  46. package/dist/esm/gateway.js +55 -0
  47. package/dist/esm/index.d.ts +3 -3
  48. package/dist/esm/index.js +2 -2
  49. package/dist/esm/interceptors/audit/index.d.ts +2 -0
  50. package/dist/esm/interceptors/audit/index.js +1 -0
  51. package/dist/esm/interceptors/audit/interceptor.d.ts +2 -0
  52. package/dist/esm/interceptors/audit/interceptor.js +11 -0
  53. package/dist/esm/interceptors/audit/record.d.ts +4 -2
  54. package/dist/esm/interceptors/audit/record.js +18 -4
  55. package/dist/esm/interceptors/audit/trace.d.ts +19 -0
  56. package/dist/esm/interceptors/audit/trace.js +39 -0
  57. package/dist/esm/interceptors/cache/embedding.d.ts +14 -0
  58. package/dist/esm/interceptors/cache/embedding.js +49 -0
  59. package/dist/esm/interceptors/cache/index.d.ts +7 -4
  60. package/dist/esm/interceptors/cache/index.js +4 -4
  61. package/dist/esm/interceptors/cache/interceptor.d.ts +19 -0
  62. package/dist/esm/interceptors/cache/interceptor.js +77 -0
  63. package/dist/esm/interceptors/cache/vector.d.ts +9 -0
  64. package/dist/esm/interceptors/cache/vector.js +32 -0
  65. package/dist/esm/interceptors/governance/budget.d.ts +11 -0
  66. package/dist/esm/interceptors/governance/budget.js +42 -0
  67. package/dist/esm/interceptors/governance/index.d.ts +7 -0
  68. package/dist/esm/interceptors/governance/index.js +4 -0
  69. package/dist/esm/interceptors/governance/model-policy.d.ts +8 -0
  70. package/dist/esm/interceptors/governance/model-policy.js +15 -0
  71. package/dist/esm/interceptors/governance/rate-limit.d.ts +9 -0
  72. package/dist/esm/interceptors/governance/rate-limit.js +43 -0
  73. package/dist/esm/interceptors/guardrails/index.d.ts +6 -0
  74. package/dist/esm/interceptors/guardrails/index.js +4 -0
  75. package/dist/esm/interceptors/guardrails/interceptor.d.ts +15 -0
  76. package/dist/esm/interceptors/guardrails/interceptor.js +37 -0
  77. package/dist/esm/interceptors/guardrails/validator.d.ts +11 -0
  78. package/dist/esm/interceptors/guardrails/validator.js +3 -0
  79. package/dist/esm/interceptors/guardrails/validators/regex.d.ts +6 -0
  80. package/dist/esm/interceptors/guardrails/validators/regex.js +28 -0
  81. package/dist/esm/interceptors/guardrails/validators/schema.d.ts +5 -0
  82. package/dist/esm/interceptors/guardrails/validators/schema.js +60 -0
  83. package/dist/esm/interceptors/injection.d.ts +17 -0
  84. package/dist/esm/interceptors/injection.js +59 -0
  85. package/dist/esm/interceptors/metrics/index.d.ts +5 -0
  86. package/dist/esm/interceptors/metrics/index.js +3 -0
  87. package/dist/esm/interceptors/metrics/interceptor.d.ts +22 -0
  88. package/dist/esm/interceptors/metrics/interceptor.js +33 -0
  89. package/dist/esm/interceptors/metrics/registry.d.ts +31 -0
  90. package/dist/esm/interceptors/metrics/registry.js +0 -0
  91. package/dist/esm/interceptors/quality/index.d.ts +3 -0
  92. package/dist/esm/interceptors/quality/index.js +2 -0
  93. package/dist/esm/interceptors/quality/risk.d.ts +32 -0
  94. package/dist/esm/interceptors/quality/risk.js +44 -0
  95. package/dist/esm/interceptors/reliability/circuit-breaker.d.ts +15 -0
  96. package/dist/esm/interceptors/reliability/circuit-breaker.js +78 -0
  97. package/dist/esm/interceptors/reliability/index.d.ts +5 -0
  98. package/dist/esm/interceptors/reliability/index.js +3 -0
  99. package/dist/esm/interceptors/reliability/load-balancer.d.ts +8 -0
  100. package/dist/esm/interceptors/reliability/load-balancer.js +35 -0
  101. package/dist/esm/interceptors/reliability/stream-buffer.d.ts +18 -0
  102. package/dist/esm/interceptors/reliability/stream-buffer.js +24 -0
  103. package/dist/esm/pricing.js +5 -1
  104. package/dist/esm/providers/azure-openai.d.ts +28 -0
  105. package/dist/esm/providers/azure-openai.js +53 -0
  106. package/dist/esm/providers/base.d.ts +7 -0
  107. package/dist/esm/providers/base.js +9 -1
  108. package/dist/esm/providers/gemini.d.ts +36 -0
  109. package/dist/esm/providers/gemini.js +69 -0
  110. package/dist/esm/providers/index.d.ts +7 -1
  111. package/dist/esm/providers/index.js +18 -5
  112. package/dist/esm/providers/ollama.d.ts +21 -0
  113. package/dist/esm/providers/ollama.js +38 -0
  114. package/dist/esm/request.d.ts +4 -1
  115. package/dist/esm/request.js +4 -1
  116. package/dist/esm/shim/openai.d.ts +56 -0
  117. package/dist/esm/shim/openai.js +53 -0
  118. package/dist/esm/types.d.ts +54 -0
  119. package/dist/esm/types.js +50 -0
  120. package/package.json +41 -2
  121. package/src/config.ts +125 -0
  122. package/src/errors.ts +28 -0
  123. package/src/gateway.ts +62 -1
  124. package/src/index.ts +4 -2
  125. package/src/interceptors/audit/index.ts +2 -0
  126. package/src/interceptors/audit/interceptor.ts +13 -0
  127. package/src/interceptors/audit/record.ts +18 -4
  128. package/src/interceptors/audit/trace.ts +47 -0
  129. package/src/interceptors/cache/embedding.ts +53 -0
  130. package/src/interceptors/cache/index.ts +7 -4
  131. package/src/interceptors/cache/interceptor.ts +111 -0
  132. package/src/interceptors/cache/vector.ts +45 -0
  133. package/src/interceptors/governance/budget.ts +59 -0
  134. package/src/interceptors/governance/index.ts +8 -0
  135. package/src/interceptors/governance/model-policy.ts +25 -0
  136. package/src/interceptors/governance/rate-limit.ts +63 -0
  137. package/src/interceptors/guardrails/index.ts +7 -0
  138. package/src/interceptors/guardrails/interceptor.ts +56 -0
  139. package/src/interceptors/guardrails/validator.ts +14 -0
  140. package/src/interceptors/guardrails/validators/regex.ts +29 -0
  141. package/src/interceptors/guardrails/validators/schema.ts +62 -0
  142. package/src/interceptors/injection.ts +72 -0
  143. package/src/interceptors/metrics/index.ts +6 -0
  144. package/src/interceptors/metrics/interceptor.ts +46 -0
  145. package/src/interceptors/metrics/registry.ts +0 -0
  146. package/src/interceptors/quality/index.ts +4 -0
  147. package/src/interceptors/quality/risk.ts +64 -0
  148. package/src/interceptors/reliability/circuit-breaker.ts +102 -0
  149. package/src/interceptors/reliability/index.ts +5 -0
  150. package/src/interceptors/reliability/load-balancer.ts +56 -0
  151. package/src/interceptors/reliability/stream-buffer.ts +27 -0
  152. package/src/pricing.ts +5 -1
  153. package/src/providers/azure-openai.ts +77 -0
  154. package/src/providers/base.ts +21 -1
  155. package/src/providers/gemini.ts +95 -0
  156. package/src/providers/index.ts +21 -5
  157. package/src/providers/ollama.ts +61 -0
  158. package/src/request.ts +6 -2
  159. package/src/shim/openai.ts +76 -0
  160. package/src/types.ts +77 -0
@@ -4,6 +4,7 @@ import { ConfigurationError } from './errors.js';
4
4
  import { auditInterceptor, isAuditInterceptor } from './interceptors/audit/index.js';
5
5
  import { isExecutorPolicy } from './interceptors/base.js';
6
6
  import { InterceptorChain } from './interceptors/chain.js';
7
+ import { StreamBuffer } from './interceptors/reliability/stream-buffer.js';
7
8
  import { PricingProvider } from './pricing.js';
8
9
  import { buildAdapter } from './providers/index.js';
9
10
  import { mockProvider } from './providers/mock.js';
@@ -37,6 +38,15 @@ export class Gateway {
37
38
  this.dryRunMode = options.dryRun ?? false;
38
39
  this.pricing = options.pricing ?? new PricingProvider();
39
40
  }
41
+ /**
42
+ * Build a Gateway from a config object or a JSON file path (F-DX-05).
43
+ * Async so the config module loads lazily (avoids a circular import).
44
+ */
45
+ static async fromConfig(config) {
46
+ const mod = await import('./config.js');
47
+ const data = typeof config === 'string' ? mod.loadConfig(config) : config;
48
+ return mod.buildFromConfig(data);
49
+ }
40
50
  /** Register an interceptor or executor policy. First-registered = outermost. */
41
51
  use(interceptor) {
42
52
  this.interceptors.push(interceptor);
@@ -65,6 +75,7 @@ export class Gateway {
65
75
  sessionId: opts.sessionId ?? null,
66
76
  options: opts.options ?? {},
67
77
  metadata: opts.metadata ?? {},
78
+ lineage: opts.lineage ?? null,
68
79
  });
69
80
  const ctx = new InterceptorContext({
70
81
  traceId: request.traceId,
@@ -76,6 +87,50 @@ export class Gateway {
76
87
  const { chain, executor } = this.buildPipeline(adapter, ctx);
77
88
  return chain.execute(request, ctx, executor);
78
89
  }
90
+ /**
91
+ * Stream a completion, buffering the provider stream (F-REL-06).
92
+ *
93
+ * The provider stream is buffered in full so the post-interceptor pipeline
94
+ * (guardrails, PII restore, audit) runs on the complete response before any
95
+ * chunk reaches the caller. Pre/post interceptors run via the chain; executor
96
+ * policies (retry, circuit breaker, cache) are not applied to the streaming
97
+ * path.
98
+ */
99
+ async *stream(opts) {
100
+ const adapter = this.resolveAdapter();
101
+ if (adapter.stream === undefined || adapter.buildStreamResponse === undefined) {
102
+ throw new ConfigurationError(`${adapter.providerName} does not support streaming`);
103
+ }
104
+ const model = opts.model ?? this.modelHint ?? this.resolveModel(adapter);
105
+ const request = new GavioRequest({
106
+ messages: opts.messages,
107
+ model,
108
+ provider: coerceProvider(adapter.providerName),
109
+ agentId: opts.agentId ?? null,
110
+ parentTraceId: opts.parentTraceId ?? null,
111
+ sessionId: opts.sessionId ?? null,
112
+ options: opts.options ?? {},
113
+ metadata: opts.metadata ?? {},
114
+ });
115
+ const ctx = new InterceptorContext({
116
+ traceId: request.traceId,
117
+ agentId: request.agentId,
118
+ parentTraceId: request.parentTraceId,
119
+ sessionId: request.sessionId,
120
+ dryRun: this.dryRunMode,
121
+ });
122
+ const startedAt = performance.now();
123
+ const buffer = new StreamBuffer();
124
+ const { chain } = this.buildPipeline(adapter, ctx);
125
+ const bufferingExecutor = async (req) => {
126
+ for await (const chunk of adapter.stream(req))
127
+ buffer.append(chunk);
128
+ return adapter.buildStreamResponse(req, buffer.text(), startedAt);
129
+ };
130
+ const response = await chain.execute(request, ctx, bufferingExecutor);
131
+ // Post-interceptors have run on the fully buffered response; emit it now.
132
+ yield response.content;
133
+ }
79
134
  async healthCheck() {
80
135
  return this.resolveAdapter().healthCheck();
81
136
  }
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * See https://gavio.io for documentation. MIT licensed.
9
9
  */
10
- export declare const VERSION = "0.1.0";
10
+ export declare const VERSION = "0.3.0";
11
11
  export { Gateway } from './gateway.js';
12
12
  export type { GatewayOptions, CompleteOptions } from './gateway.js';
13
13
  export { GavioRequest } from './request.js';
@@ -18,8 +18,8 @@ export { InterceptorContext } from './context.js';
18
18
  export type { InterceptorContextInit } from './context.js';
19
19
  export { uuid7, newTraceId } from './ids.js';
20
20
  export { PricingProvider, estimateTokens } from './pricing.js';
21
- export { Provider, CacheType, PiiMode, Sensitivity, GuardrailOutcome, TokenUsage, coerceProvider, } from './types.js';
22
- export type { Message } from './types.js';
21
+ export { Provider, CacheType, PiiMode, Sensitivity, GuardrailOutcome, TokenUsage, PromptLineage, RagChunk, coerceProvider, } from './types.js';
22
+ export type { Message, PromptLineageInit, RagChunkInit } from './types.js';
23
23
  export type { Interceptor, Executor, ExecutorPolicy } from './interceptors/base.js';
24
24
  export { InterceptorChain } from './interceptors/chain.js';
25
25
  export { GavioError, ConfigurationError, ProviderError, ProviderUnavailableError, RateLimitError, ServerError, TimeoutError, PiiBlockedError, BudgetExceededError, GuardrailViolationError, } from './errors.js';
package/dist/esm/index.js CHANGED
@@ -7,14 +7,14 @@
7
7
  *
8
8
  * See https://gavio.io for documentation. MIT licensed.
9
9
  */
10
- export const VERSION = '0.1.0';
10
+ export const VERSION = '0.3.0';
11
11
  export { Gateway } from './gateway.js';
12
12
  export { GavioRequest } from './request.js';
13
13
  export { GavioResponse } from './response.js';
14
14
  export { InterceptorContext } from './context.js';
15
15
  export { uuid7, newTraceId } from './ids.js';
16
16
  export { PricingProvider, estimateTokens } from './pricing.js';
17
- export { Provider, CacheType, PiiMode, Sensitivity, GuardrailOutcome, TokenUsage, coerceProvider, } from './types.js';
17
+ export { Provider, CacheType, PiiMode, Sensitivity, GuardrailOutcome, TokenUsage, PromptLineage, RagChunk, coerceProvider, } from './types.js';
18
18
  export { InterceptorChain } from './interceptors/chain.js';
19
19
  // errors
20
20
  export { GavioError, ConfigurationError, ProviderError, ProviderUnavailableError, RateLimitError, ServerError, TimeoutError, PiiBlockedError, BudgetExceededError, GuardrailViolationError, } from './errors.js';
@@ -5,3 +5,5 @@ export type { AuditRecordInit } from './record.js';
5
5
  export type { AuditSink } from './sink.js';
6
6
  export { stdoutSink } from './sinks/stdout.js';
7
7
  export type { StdoutSinkOptions } from './sinks/stdout.js';
8
+ export { verifyChain, buildCallGraph } from './trace.js';
9
+ export type { TraceNode } from './trace.js';
@@ -1,3 +1,4 @@
1
1
  export { auditInterceptor, isAuditInterceptor, AUDIT_NAME } from './interceptor.js';
2
2
  export { AuditRecord, SCHEMA_VERSION } from './record.js';
3
3
  export { stdoutSink } from './sinks/stdout.js';
4
+ export { verifyChain, buildCallGraph } from './trace.js';
@@ -4,6 +4,8 @@ import type { AuditSink } from './sink.js';
4
4
  export declare const AUDIT_NAME = "audit";
5
5
  export interface AuditInterceptorOptions {
6
6
  sink?: AuditSink | 'stdout';
7
+ /** F-OBS-02: link each record via previousHash into a tamper-evident chain. */
8
+ hashChain?: boolean;
7
9
  }
8
10
  /** Factory: build an audit interceptor. */
9
11
  export declare function auditInterceptor(options?: AuditInterceptorOptions): Interceptor;
@@ -2,6 +2,7 @@
2
2
  import { AuditRecord } from './record.js';
3
3
  import { stdoutSink } from './sinks/stdout.js';
4
4
  const PROMPT_HASH_KEY = 'audit_prompt_hash';
5
+ const LINEAGE_KEY = 'audit_lineage';
5
6
  export const AUDIT_NAME = 'audit';
6
7
  /**
7
8
  * Build an AuditRecord per request and write it to a sink.
@@ -15,11 +16,16 @@ class AuditInterceptor {
15
16
  name = AUDIT_NAME;
16
17
  dryRunSafe = true; // auditing is observation-only, so it always runs
17
18
  sink;
19
+ hashChain;
20
+ lastHash = '';
18
21
  constructor(options = {}) {
19
22
  this.sink = resolveSink(options.sink);
23
+ this.hashChain = options.hashChain ?? false;
20
24
  }
21
25
  async before(request, ctx) {
22
26
  ctx.state[PROMPT_HASH_KEY] = AuditRecord.hashText(request.promptText());
27
+ if (request.lineage != null)
28
+ ctx.state[LINEAGE_KEY] = request.lineage;
23
29
  return request;
24
30
  }
25
31
  async after(response, ctx) {
@@ -44,7 +50,12 @@ class AuditInterceptor {
44
50
  cacheType: response.cacheType,
45
51
  guardrailOutcome: ctx.guardrailOutcome,
46
52
  riskScore: ctx.riskScore,
53
+ lineage: ctx.state[LINEAGE_KEY] ?? null,
47
54
  });
55
+ if (this.hashChain) {
56
+ record.previousHash = this.lastHash;
57
+ this.lastHash = record.contentHash();
58
+ }
48
59
  response.audit = record;
49
60
  try {
50
61
  await this.sink.write(record);
@@ -1,5 +1,5 @@
1
1
  /** AuditRecord — the immutable, per-request audit entry. */
2
- import { TokenUsage } from '../../types.js';
2
+ import { PromptLineage, TokenUsage } from '../../types.js';
3
3
  export declare const SCHEMA_VERSION = "1.0";
4
4
  export interface AuditRecordInit {
5
5
  traceId: string;
@@ -22,6 +22,7 @@ export interface AuditRecordInit {
22
22
  cacheType?: string | null;
23
23
  guardrailOutcome?: string | null;
24
24
  riskScore?: number | null;
25
+ lineage?: PromptLineage | null;
25
26
  previousHash?: string;
26
27
  schemaVersion?: string;
27
28
  }
@@ -53,13 +54,14 @@ export declare class AuditRecord {
53
54
  cacheType: string | null;
54
55
  guardrailOutcome: string | null;
55
56
  riskScore: number | null;
57
+ lineage: PromptLineage | null;
56
58
  previousHash: string;
57
59
  schemaVersion: string;
58
60
  constructor(init: AuditRecordInit);
59
61
  static nowUtc(): string;
60
62
  static hashText(text: string): string;
61
63
  toJSON(): Record<string, unknown>;
62
- /** Stable JSON with sorted keys — used for the v0.2.0 hash chain. */
64
+ /** Stable JSON with recursively sorted keys — used for the v0.2.0 hash chain. */
63
65
  toCanonicalJson(): string;
64
66
  /** Hash of this record's content — used to build the v0.2.0 chain. */
65
67
  contentHash(): string;
@@ -1,10 +1,22 @@
1
1
  /** AuditRecord — the immutable, per-request audit entry. */
2
2
  import { createHash } from 'node:crypto';
3
- import { TokenUsage } from '../../types.js';
3
+ import { PromptLineage, TokenUsage } from '../../types.js';
4
4
  export const SCHEMA_VERSION = '1.0';
5
5
  function sha256(text) {
6
6
  return createHash('sha256').update(text, 'utf-8').digest('hex');
7
7
  }
8
+ /** Deterministic JSON with keys sorted at every nesting level. */
9
+ function stableStringify(value) {
10
+ if (value === null || typeof value !== 'object')
11
+ return JSON.stringify(value) ?? 'null';
12
+ if (Array.isArray(value))
13
+ return `[${value.map(stableStringify).join(',')}]`;
14
+ const obj = value;
15
+ const parts = Object.keys(obj)
16
+ .sort()
17
+ .map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`);
18
+ return `{${parts.join(',')}}`;
19
+ }
8
20
  /**
9
21
  * One append-only audit entry. Carries metadata only — never raw content.
10
22
  *
@@ -33,6 +45,7 @@ export class AuditRecord {
33
45
  cacheType;
34
46
  guardrailOutcome;
35
47
  riskScore;
48
+ lineage;
36
49
  previousHash;
37
50
  schemaVersion;
38
51
  constructor(init) {
@@ -56,6 +69,7 @@ export class AuditRecord {
56
69
  this.cacheType = init.cacheType ?? null;
57
70
  this.guardrailOutcome = init.guardrailOutcome ?? null;
58
71
  this.riskScore = init.riskScore ?? null;
72
+ this.lineage = init.lineage ?? null;
59
73
  this.previousHash = init.previousHash ?? '';
60
74
  this.schemaVersion = init.schemaVersion ?? SCHEMA_VERSION;
61
75
  }
@@ -87,14 +101,14 @@ export class AuditRecord {
87
101
  cacheType: this.cacheType,
88
102
  guardrailOutcome: this.guardrailOutcome,
89
103
  riskScore: this.riskScore,
104
+ lineage: this.lineage ? this.lineage.toJSON() : null,
90
105
  previousHash: this.previousHash,
91
106
  schemaVersion: this.schemaVersion,
92
107
  };
93
108
  }
94
- /** Stable JSON with sorted keys — used for the v0.2.0 hash chain. */
109
+ /** Stable JSON with recursively sorted keys — used for the v0.2.0 hash chain. */
95
110
  toCanonicalJson() {
96
- const data = this.toJSON();
97
- return JSON.stringify(data, Object.keys(data).sort());
111
+ return stableStringify(this.toJSON());
98
112
  }
99
113
  /** Hash of this record's content — used to build the v0.2.0 chain. */
100
114
  contentHash() {
@@ -0,0 +1,19 @@
1
+ /** Audit-chain verification (F-OBS-02) and multi-agent DAG trace (F-OBS-03). */
2
+ import type { AuditRecord } from './record.js';
3
+ /**
4
+ * Return true if the records form an intact hash chain. Each record's
5
+ * previousHash must equal the content hash of the record before it; the first
6
+ * must be empty. Any edit, reorder, or deletion breaks the chain.
7
+ */
8
+ export declare function verifyChain(records: AuditRecord[]): boolean;
9
+ export interface TraceNode {
10
+ traceId: string;
11
+ agentId: string | null;
12
+ parentTraceId: string | null;
13
+ children: TraceNode[];
14
+ }
15
+ /**
16
+ * Reconstruct the multi-agent DAG from audit records using parentTraceId +
17
+ * traceId. Returns the root nodes (those with no known parent).
18
+ */
19
+ export declare function buildCallGraph(records: AuditRecord[]): TraceNode[];
@@ -0,0 +1,39 @@
1
+ /** Audit-chain verification (F-OBS-02) and multi-agent DAG trace (F-OBS-03). */
2
+ /**
3
+ * Return true if the records form an intact hash chain. Each record's
4
+ * previousHash must equal the content hash of the record before it; the first
5
+ * must be empty. Any edit, reorder, or deletion breaks the chain.
6
+ */
7
+ export function verifyChain(records) {
8
+ let prevHash = '';
9
+ for (const rec of records) {
10
+ if (rec.previousHash !== prevHash)
11
+ return false;
12
+ prevHash = rec.contentHash();
13
+ }
14
+ return true;
15
+ }
16
+ /**
17
+ * Reconstruct the multi-agent DAG from audit records using parentTraceId +
18
+ * traceId. Returns the root nodes (those with no known parent).
19
+ */
20
+ export function buildCallGraph(records) {
21
+ const nodes = new Map();
22
+ for (const rec of records) {
23
+ nodes.set(rec.traceId, {
24
+ traceId: rec.traceId,
25
+ agentId: rec.agentId,
26
+ parentTraceId: rec.parentTraceId,
27
+ children: [],
28
+ });
29
+ }
30
+ const roots = [];
31
+ for (const node of nodes.values()) {
32
+ const parent = node.parentTraceId ? nodes.get(node.parentTraceId) : undefined;
33
+ if (parent)
34
+ parent.children.push(node);
35
+ else
36
+ roots.push(node);
37
+ }
38
+ return roots;
39
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Embeddings for the semantic cache (F-CACHE-02).
3
+ *
4
+ * Zero-dependency hashed bag-of-words embedder (L2-normalised) — good enough to
5
+ * dedup near-identical prompts. Plug in a real embedder implementing `Embedder`
6
+ * for production semantic matching.
7
+ */
8
+ export interface Embedder {
9
+ embed(text: string): number[];
10
+ }
11
+ /** Deterministic hashed bag-of-words embedder. */
12
+ export declare function hashingEmbedder(dim?: number): Embedder;
13
+ /** Cosine similarity; safe for zero vectors. */
14
+ export declare function cosineSimilarity(a: number[], b: number[]): number;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Embeddings for the semantic cache (F-CACHE-02).
3
+ *
4
+ * Zero-dependency hashed bag-of-words embedder (L2-normalised) — good enough to
5
+ * dedup near-identical prompts. Plug in a real embedder implementing `Embedder`
6
+ * for production semantic matching.
7
+ */
8
+ import { createHash } from 'node:crypto';
9
+ const TOKEN = /[a-z0-9]+/g;
10
+ /** Deterministic hashed bag-of-words embedder. */
11
+ export function hashingEmbedder(dim = 256) {
12
+ return {
13
+ embed(text) {
14
+ const vec = new Array(dim).fill(0);
15
+ const tokens = text.toLowerCase().match(TOKEN) ?? [];
16
+ for (const token of tokens) {
17
+ // Parity note: Python uses blake2b(digest_size=8); here we take the
18
+ // first 8 bytes of blake2b512. Both are deterministic; the JS cache is
19
+ // per-process so cross-language byte-parity is not required.
20
+ const digest = createHash('blake2b512').update(token).digest();
21
+ let n = 0n;
22
+ for (let i = 0; i < 8; i++)
23
+ n = (n << 8n) | BigInt(digest[i]);
24
+ const bucket = Number(n % BigInt(dim));
25
+ vec[bucket] += 1;
26
+ }
27
+ const norm = Math.sqrt(vec.reduce((s, x) => s + x * x, 0));
28
+ if (norm === 0)
29
+ return vec;
30
+ return vec.map((x) => x / norm);
31
+ },
32
+ };
33
+ }
34
+ /** Cosine similarity; safe for zero vectors. */
35
+ export function cosineSimilarity(a, b) {
36
+ if (a.length !== b.length)
37
+ throw new Error('vectors must have equal length');
38
+ let dot = 0;
39
+ let na = 0;
40
+ let nb = 0;
41
+ for (let i = 0; i < a.length; i++) {
42
+ dot += a[i] * b[i];
43
+ na += a[i] * a[i];
44
+ nb += b[i] * b[i];
45
+ }
46
+ if (na === 0 || nb === 0)
47
+ return 0;
48
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
49
+ }
@@ -1,7 +1,10 @@
1
- /**
2
- * Caching substrate. The SemanticCache interceptor ships in v0.2.0; v0.1.0
3
- * exposes the CacheBackend interface and the in-memory backend only.
4
- */
1
+ /** Caching (F-CACHE-01 exact, F-CACHE-02 semantic, F-CACHE-03 in-memory). */
5
2
  export type { CacheBackend } from './backend.js';
6
3
  export { memoryCacheBackend } from './backends/memory.js';
7
4
  export type { MemoryCacheBackendOptions } from './backends/memory.js';
5
+ export { semanticCache } from './interceptor.js';
6
+ export type { SemanticCacheOptions } from './interceptor.js';
7
+ export { hashingEmbedder, cosineSimilarity } from './embedding.js';
8
+ export type { Embedder } from './embedding.js';
9
+ export { inMemoryVectorBackend } from './vector.js';
10
+ export type { VectorBackend } from './vector.js';
@@ -1,5 +1,5 @@
1
- /**
2
- * Caching substrate. The SemanticCache interceptor ships in v0.2.0; v0.1.0
3
- * exposes the CacheBackend interface and the in-memory backend only.
4
- */
1
+ /** Caching (F-CACHE-01 exact, F-CACHE-02 semantic, F-CACHE-03 in-memory). */
5
2
  export { memoryCacheBackend } from './backends/memory.js';
3
+ export { semanticCache } from './interceptor.js';
4
+ export { hashingEmbedder, cosineSimilarity } from './embedding.js';
5
+ export { inMemoryVectorBackend } from './vector.js';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * semanticCache (F-CACHE-01, F-CACHE-02) — two-level cache as an ExecutorPolicy.
3
+ *
4
+ * Exact SHA-256 cache, then optional semantic cosine cache; a hit returns the
5
+ * cached response and skips the provider. Register outermost.
6
+ */
7
+ import type { ExecutorPolicy } from '../base.js';
8
+ import type { CacheBackend } from './backend.js';
9
+ import type { Embedder } from './embedding.js';
10
+ import { type VectorBackend } from './vector.js';
11
+ export interface SemanticCacheOptions {
12
+ backend?: CacheBackend;
13
+ embedder?: Embedder;
14
+ vectorBackend?: VectorBackend;
15
+ exactTtlSeconds?: number;
16
+ semanticTtlSeconds?: number;
17
+ similarityThreshold?: number;
18
+ }
19
+ export declare function semanticCache(options?: SemanticCacheOptions): ExecutorPolicy;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * semanticCache (F-CACHE-01, F-CACHE-02) — two-level cache as an ExecutorPolicy.
3
+ *
4
+ * Exact SHA-256 cache, then optional semantic cosine cache; a hit returns the
5
+ * cached response and skips the provider. Register outermost.
6
+ */
7
+ import { createHash } from 'node:crypto';
8
+ import { GavioResponse } from '../../response.js';
9
+ import { CacheType, TokenUsage } from '../../types.js';
10
+ import { memoryCacheBackend } from './backends/memory.js';
11
+ import { inMemoryVectorBackend } from './vector.js';
12
+ export function semanticCache(options = {}) {
13
+ const backend = options.backend ?? memoryCacheBackend();
14
+ const embedder = options.embedder;
15
+ const semantic = embedder != null;
16
+ const vector = options.vectorBackend ?? (semantic ? inMemoryVectorBackend() : null);
17
+ const exactTtl = options.exactTtlSeconds ?? 3600;
18
+ const semanticTtl = options.semanticTtlSeconds ?? 86400;
19
+ const threshold = options.similarityThreshold ?? 0.95;
20
+ function exactKey(request) {
21
+ const opts = request.options ?? {};
22
+ const sorted = {};
23
+ for (const k of Object.keys(opts).sort())
24
+ sorted[k] = opts[k];
25
+ const payload = JSON.stringify({
26
+ provider: String(request.provider),
27
+ model: request.model,
28
+ messages: request.messages,
29
+ options: sorted,
30
+ });
31
+ return 'gavio:exact:' + createHash('sha256').update(payload).digest('hex');
32
+ }
33
+ function hit(request, ctx, entry, type) {
34
+ ctx.cacheHit = true;
35
+ ctx.cacheType = type;
36
+ return new GavioResponse({
37
+ traceId: request.traceId,
38
+ content: entry.content,
39
+ model: request.model,
40
+ provider: String(request.provider),
41
+ modelVersion: entry.modelVersion,
42
+ usage: new TokenUsage(entry.promptTokens, entry.completionTokens),
43
+ costUsd: 0,
44
+ cacheHit: true,
45
+ cacheType: type,
46
+ });
47
+ }
48
+ return {
49
+ name: 'semantic_cache',
50
+ isExecutorPolicy: true,
51
+ async around(request, ctx, callNext) {
52
+ ctx.markFired('semantic_cache');
53
+ const key = exactKey(request);
54
+ const cached = (await backend.get(key));
55
+ if (cached)
56
+ return hit(request, ctx, cached, CacheType.EXACT);
57
+ let embedding = null;
58
+ if (semantic && vector && embedder) {
59
+ embedding = embedder.embed(request.promptText());
60
+ const semHit = (await vector.query(embedding, threshold));
61
+ if (semHit)
62
+ return hit(request, ctx, semHit, CacheType.SEMANTIC);
63
+ }
64
+ const response = await callNext(request);
65
+ const entry = {
66
+ content: response.content,
67
+ modelVersion: response.modelVersion,
68
+ promptTokens: response.usage.promptTokens,
69
+ completionTokens: response.usage.completionTokens,
70
+ };
71
+ await backend.set(key, entry, exactTtl);
72
+ if (embedding && vector)
73
+ await vector.add(embedding, entry, semanticTtl);
74
+ return response;
75
+ },
76
+ };
77
+ }
@@ -0,0 +1,9 @@
1
+ /** VectorBackend — nearest-neighbour store for the semantic cache (F-CACHE-02). */
2
+ export interface VectorBackend {
3
+ add(vector: number[], value: unknown, ttlSeconds?: number | null): Promise<void>;
4
+ /** Return the value of the nearest entry with similarity >= threshold. */
5
+ query(vector: number[], threshold: number): Promise<unknown | null>;
6
+ clear(): Promise<void>;
7
+ }
8
+ /** Bounded, brute-force in-memory vector store (default dev backend). */
9
+ export declare function inMemoryVectorBackend(maxSize?: number): VectorBackend;
@@ -0,0 +1,32 @@
1
+ /** VectorBackend — nearest-neighbour store for the semantic cache (F-CACHE-02). */
2
+ import { cosineSimilarity } from './embedding.js';
3
+ /** Bounded, brute-force in-memory vector store (default dev backend). */
4
+ export function inMemoryVectorBackend(maxSize = 1000) {
5
+ const items = [];
6
+ return {
7
+ async add(vector, value, ttlSeconds) {
8
+ const expiresAt = ttlSeconds ? Date.now() + ttlSeconds * 1000 : null;
9
+ items.push({ vector, value, expiresAt });
10
+ if (items.length > maxSize)
11
+ items.shift();
12
+ },
13
+ async query(vector, threshold) {
14
+ const now = Date.now();
15
+ let best = null;
16
+ let bestSim = threshold;
17
+ for (const item of items) {
18
+ if (item.expiresAt !== null && now > item.expiresAt)
19
+ continue;
20
+ const sim = cosineSimilarity(vector, item.vector);
21
+ if (sim >= bestSim) {
22
+ bestSim = sim;
23
+ best = item.value;
24
+ }
25
+ }
26
+ return best;
27
+ },
28
+ async clear() {
29
+ items.length = 0;
30
+ },
31
+ };
32
+ }
@@ -0,0 +1,11 @@
1
+ /** costControl (F-GOV-02) — soft/hard budget caps per scope and window. */
2
+ import type { Interceptor } from '../base.js';
3
+ export type Scope = 'agent' | 'session' | 'global';
4
+ export type Window = 'day' | 'month' | 'total';
5
+ export interface CostControlOptions {
6
+ hardCapUsd: number;
7
+ softCapUsd?: number;
8
+ scope?: Scope;
9
+ window?: Window;
10
+ }
11
+ export declare function costControl(options: CostControlOptions): Interceptor;
@@ -0,0 +1,42 @@
1
+ /** costControl (F-GOV-02) — soft/hard budget caps per scope and window. */
2
+ import { BudgetExceededError } from '../../errors.js';
3
+ function scopeKey(scope, ctx) {
4
+ if (scope === 'agent')
5
+ return `agent:${ctx.agentId ?? 'unknown'}`;
6
+ if (scope === 'session')
7
+ return `session:${ctx.sessionId ?? 'unknown'}`;
8
+ return 'global';
9
+ }
10
+ function windowBucket(window) {
11
+ const now = new Date().toISOString();
12
+ if (window === 'day')
13
+ return now.slice(0, 10);
14
+ if (window === 'month')
15
+ return now.slice(0, 7);
16
+ return 'total';
17
+ }
18
+ export function costControl(options) {
19
+ const { hardCapUsd, softCapUsd, scope = 'global', window = 'day' } = options;
20
+ const spend = new Map();
21
+ const key = (ctx) => `${scopeKey(scope, ctx)}|${windowBucket(window)}`;
22
+ return {
23
+ name: 'cost_control',
24
+ before(request, ctx) {
25
+ const spent = spend.get(key(ctx)) ?? 0;
26
+ if (spent >= hardCapUsd) {
27
+ throw new BudgetExceededError(`budget hard cap $${hardCapUsd.toFixed(2)} reached (spent $${spent.toFixed(4)})`);
28
+ }
29
+ return request;
30
+ },
31
+ after(response, ctx) {
32
+ const k = key(ctx);
33
+ const total = (spend.get(k) ?? 0) + response.costUsd;
34
+ spend.set(k, total);
35
+ if (softCapUsd !== undefined && total >= softCapUsd) {
36
+ // eslint-disable-next-line no-console
37
+ console.warn(`[gavio:budget] soft cap: $${total.toFixed(4)} of $${softCapUsd} for ${k}`);
38
+ }
39
+ return response;
40
+ },
41
+ };
42
+ }
@@ -0,0 +1,7 @@
1
+ /** Cost & governance (F-GOV-02 budget, F-GOV-03 rate limit, F-GOV-04 RBAC). */
2
+ export { costControl } from './budget.js';
3
+ export type { CostControlOptions, Scope, Window } from './budget.js';
4
+ export { rateLimiter } from './rate-limit.js';
5
+ export type { RateLimiterOptions } from './rate-limit.js';
6
+ export { modelPolicy } from './model-policy.js';
7
+ export type { ModelPolicyOptions } from './model-policy.js';
@@ -0,0 +1,4 @@
1
+ /** Cost & governance (F-GOV-02 budget, F-GOV-03 rate limit, F-GOV-04 RBAC). */
2
+ export { costControl } from './budget.js';
3
+ export { rateLimiter } from './rate-limit.js';
4
+ export { modelPolicy } from './model-policy.js';
@@ -0,0 +1,8 @@
1
+ /** modelPolicy (F-GOV-04) — per-role model allowlists (RBAC). */
2
+ import type { Interceptor } from '../base.js';
3
+ export interface ModelPolicyOptions {
4
+ roles: Record<string, string[]>;
5
+ defaultRole?: string;
6
+ roleKey?: string;
7
+ }
8
+ export declare function modelPolicy(options: ModelPolicyOptions): Interceptor;