@voltro/ai 0.63.0 → 0.65.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.
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@ import { EmbeddingModelV4 } from '@ai-sdk/provider';
8
8
  import { Experimental_VideoModelV4 } from '@ai-sdk/provider';
9
9
  import { extractReasoningMiddleware } from 'ai';
10
10
  import { GatewayLanguageModelEntry } from '@ai-sdk/gateway';
11
+ import { Guards } from '@voltro/protocol';
11
12
  import { ImageModelV4 } from '@ai-sdk/provider';
12
13
  import { LanguageModelMiddleware } from 'ai';
13
14
  import { LanguageModelV4 } from '@ai-sdk/provider';
@@ -37,6 +38,31 @@ export declare const AGENT_MESSAGES_TABLE = "_voltro_agent_messages";
37
38
  /** Thread metadata table (one row per conversation). */
38
39
  export declare const AGENT_THREADS_TABLE = "_voltro_agent_threads";
39
40
 
41
+ /**
42
+ * WHO MAY TALK TO an agent — the same three-state decision a mutation makes,
43
+ * applied to BOTH synthesized routes (`<name>.send` and `<name>.messages`).
44
+ *
45
+ * The two routes used to carry a hard-coded `openAccess`, honestly worded:
46
+ * "serves any session this app authenticates; defineAgent declares no per-agent
47
+ * guard vocabulary today". That sentence made every agent a free model-spend
48
+ * surface for anyone who could open the socket — anonymous included, since the
49
+ * routes never looked at the subject — and it made the boot gate's carve-out
50
+ * ("synthesized routes are not the app's to guard") true only because there
51
+ * was nothing on the descriptor to read. There is now, so the gate judges an
52
+ * agent like a workflow: one that declares neither refuses the boot under
53
+ * `security.defaultDeny`.
54
+ */
55
+ export declare interface AgentAccessOptions {
56
+ /** A caller must hold a scope (`{ scope: 'support:chat' }`), or a
57
+ * relationship to the thread (`{ action, resourceType, resource: (i) =>
58
+ * i.threadId }`). Checked on every `send` and on every `messages` delivery. */
59
+ readonly guards?: Guards<AgentRouteInput>;
60
+ /** Anyone who can open the socket may talk to it — and WHY. A reason, not a
61
+ * boolean. Note what it does NOT buy: an anonymous caller owns no thread, so
62
+ * thread privacy (below) is only enforced for authenticated subjects. */
63
+ readonly openAccess?: string;
64
+ }
65
+
40
66
  /** Stamp an agent-initiated call's actor with `via: 'agent'` so audit + trace
41
67
  * + provenance (08) can distinguish "user X" from "user X via agent". The
42
68
  * actor id stays the calling subject's — the agent never escalates identity. */
@@ -52,6 +78,9 @@ export declare interface AgentDescriptor<S extends Schema.Schema.Any = DefaultAg
52
78
  /** Input schema (default `{ prompt: string }`). The send route's wire input
53
79
  * extends this with `{ threadId, order }`. */
54
80
  readonly input?: S;
81
+ /** WHO MAY TALK TO IT — see {@link AgentAccessOptions}. */
82
+ readonly guards?: Guards<AgentRouteInput>;
83
+ readonly openAccess?: string;
55
84
  }
56
85
 
57
86
  /** Error taxonomy. `retryable` on the event drives client/agent retry. */
@@ -201,7 +230,7 @@ export declare const agentMessagesTable: Table<typeof AGENT_MESSAGES_TABLE, Reco
201
230
  export declare const agentRouteDescriptors: (desc: AnyAgentDescriptor) => {
202
231
  send: ActionProcedureDescriptor<`${string}.send`, Schema.Schema.Any, Schema.Struct<{
203
232
  text: typeof Schema.String;
204
- }>, typeof Schema.Never>;
233
+ }>, typeof AgentThreadAccessDenied>;
205
234
  messages: QueryProcedureDescriptor<`${string}.messages`, Schema.Struct<{
206
235
  threadId: typeof Schema.String;
207
236
  }>, Schema.Array$<Schema.Struct<{
@@ -218,17 +247,40 @@ export declare const agentRouteDescriptors: (desc: AnyAgentDescriptor) => {
218
247
  readonly key: typeof Schema.String;
219
248
  readonly value: typeof Schema.Unknown;
220
249
  }]>>;
221
- }>>, typeof Schema.Never>;
250
+ }>>, typeof AgentThreadAccessDenied>;
222
251
  };
223
252
 
253
+ /** The one field both synthesized routes share on the wire — what a
254
+ * resource-scoped agent guard may read its id from. */
255
+ export declare interface AgentRouteInput {
256
+ readonly threadId: string;
257
+ }
258
+
224
259
  export declare interface AgentThread {
225
260
  readonly id: string;
226
261
  readonly tenantId: string | null;
262
+ readonly subjectId: string | null;
227
263
  readonly title: string | null;
228
264
  readonly createdAt: Date;
229
265
  readonly updatedAt: Date;
230
266
  }
231
267
 
268
+ /**
269
+ * A thread belongs to the subject that opened it. Raised by `<name>.send` and
270
+ * `<name>.messages` when an authenticated caller addresses a thread another
271
+ * subject created — the isolation the old `openAccess` sentence said it could
272
+ * not claim. In the routes' error union, so it reaches the client typed.
273
+ */
274
+ export declare class AgentThreadAccessDenied extends AgentThreadAccessDenied_base {
275
+ get message(): string;
276
+ }
277
+
278
+ declare const AgentThreadAccessDenied_base: Schema.TaggedErrorClass<AgentThreadAccessDenied, "AgentThreadAccessDenied", {
279
+ readonly _tag: Schema.tag<"AgentThreadAccessDenied">;
280
+ } & {
281
+ threadId: typeof Schema.String;
282
+ }>;
283
+
232
284
  export declare const agentThreadsTable: Table<typeof AGENT_THREADS_TABLE, Record<string, ColumnDefinition<unknown>>, true>;
233
285
 
234
286
  export declare const AI_BUDGET_TABLE = "_voltro_ai_budget";
@@ -745,6 +797,7 @@ export declare interface CostBreakdown {
745
797
  /** Open a new thread (the store generates the id). */
746
798
  export declare const createThread: (store: DataStore, input?: {
747
799
  readonly tenantId?: string | null;
800
+ readonly subjectId?: string | null;
748
801
  readonly title?: string | null;
749
802
  }) => Effect.Effect<AgentThread>;
750
803
 
@@ -880,7 +933,7 @@ export { defaultSettingsMiddleware }
880
933
  export declare const defineAgent: <S extends Schema.Schema.Any = DefaultAgentInputSchema>(config: {
881
934
  readonly name: string;
882
935
  readonly input?: S;
883
- }) => AgentDescriptor<S>;
936
+ } & AgentAccessOptions) => AgentDescriptor<S>;
884
937
 
885
938
  /**
886
939
  * Bind a server executor to its descriptor. Default-export the result from
@@ -1018,6 +1071,24 @@ export declare type EmbeddingProviderName = 'mock' | 'openai' | 'voyage' | 'cohe
1018
1071
  */
1019
1072
  export declare const embedMany: (texts: ReadonlyArray<string>, config?: EmbeddingProviderConfig) => Effect.Effect<ReadonlyArray<ReadonlyArray<number>>, AiError>;
1020
1073
 
1074
+ /**
1075
+ * The ownership rule, in one place for both synthesized routes.
1076
+ *
1077
+ * `threadId` is caller-supplied, so the first message on an id is what opens
1078
+ * the thread: the row is created then, owned by the caller. Every later call
1079
+ * on that id compares owners — a thread with an owner refuses every OTHER
1080
+ * authenticated subject with `AgentThreadAccessDenied`; a thread with none
1081
+ * (opened anonymously, or before ownership existed) admits anyone, because a
1082
+ * row that records no owner cannot honestly claim one.
1083
+ *
1084
+ * Returns the thread. Never a silent `null`: a refusal is the typed error.
1085
+ */
1086
+ export declare const ensureThreadOwned: (store: DataStore, input: {
1087
+ readonly threadId: string;
1088
+ readonly subjectId: string | null;
1089
+ readonly tenantId?: string | null;
1090
+ }) => Effect.Effect<AgentThread, AgentThreadAccessDenied>;
1091
+
1021
1092
  /**
1022
1093
  * Derive a USD cost from a token `usage` tally + a model price. An
1023
1094
  * unknown model (or the `mock` provider) resolves to zero cost rather
@@ -1462,6 +1533,14 @@ export declare interface GenerateSpeechResult {
1462
1533
  export declare const generateText: (options: GenerateTextOptions) => Effect.Effect<GenerateTextResult, AiError>;
1463
1534
 
1464
1535
  export declare interface GenerateTextOptions {
1536
+ /** Tools the model may call on the way to its answer — the same tool-loop
1537
+ * `generateObjectWithTools` runs, for a TEXT result. With tools, `usage` is
1538
+ * the SUM over every step, so a usage row records the whole loop. */
1539
+ readonly tools?: Record<string, AnyTool>;
1540
+ /** Max LLM↔tool round-trips before the model must answer. Default 8. */
1541
+ readonly maxSteps?: number;
1542
+ /** Request context threaded into the tool executors. */
1543
+ readonly toolContext?: ToolContext;
1465
1544
  /** The user prompt. The SDK auto-wraps it as the single user message. */
1466
1545
  readonly prompt: string;
1467
1546
  /** Optional system message. Steers the model independently of the prompt. */
@@ -1746,8 +1825,14 @@ export declare const knowledgeCutoffOf: (raw: RawGatewayModel) => string | undef
1746
1825
  */
1747
1826
  export declare const llmJudge: EvalJudge;
1748
1827
 
1749
- /** The synthesized `<name>.messages` body — load the thread's messages. */
1750
- export declare const loadAgentMessages: (store: DataStore, threadId: string) => Effect.Effect<ReadonlyArray<AgentMessage>>;
1828
+ /**
1829
+ * The synthesized `<name>.messages` body — load the thread's messages, for the
1830
+ * subject that owns the thread. Same rule as `send`: an owned thread refuses
1831
+ * every other authenticated subject; an unknown or unowned thread reads as
1832
+ * its rows (empty, for an id nobody has written to). Re-checked on every
1833
+ * reactive delivery, since the query re-runs its executor.
1834
+ */
1835
+ export declare const loadAgentMessages: (store: DataStore, threadId: string, subjectId?: string | null) => Effect.Effect<ReadonlyArray<AgentMessage>, AgentThreadAccessDenied>;
1751
1836
 
1752
1837
  /**
1753
1838
  * A middleware that logs every generate/stream call's model (and token usage
@@ -2490,18 +2575,14 @@ export declare type ProviderConfig = {
2490
2575
  */
2491
2576
  export declare type ProviderFallbacks = ReadonlyArray<ProviderConfig>;
2492
2577
 
2578
+ export declare const providerFromEnv: (env?: Record<string, string | undefined>) => ProviderConfig;
2579
+
2493
2580
  /**
2494
- * Read the provider config. The process-global override (set by `useMockAi`)
2495
- * wins over env. Otherwise honours:
2496
- * AI_PROVIDER=mock|anthropic|openai|gateway (default: mock)
2497
- * AI_MODEL=<model-id> (default per provider: 'claude-opus-4-8' for
2498
- * anthropic, 'gpt-5.5' for openai, 'mock' for mock;
2499
- * REQUIRED for gateway — a `creator/model` id)
2500
- *
2501
- * Provider keys are read by the underlying AI SDK packages from their standard
2502
- * env vars: ANTHROPIC_API_KEY, OPENAI_API_KEY, AI_GATEWAY_API_KEY.
2581
+ * The env var a provider's SDK reads its key from — `undefined` for `mock`,
2582
+ * which needs none. ONE map, exported, so the boot gate that checks a key is
2583
+ * present cannot drift from the provider that reads it.
2503
2584
  */
2504
- export declare const providerFromEnv: (env?: Record<string, string | undefined>) => ProviderConfig;
2585
+ export declare const providerKeyEnvVar: (name: string) => string | undefined;
2505
2586
 
2506
2587
  export declare type ProviderName = 'mock' | 'anthropic' | 'openai' | 'gateway';
2507
2588
 
@@ -2989,11 +3070,15 @@ export declare const runAgentTurn: (exec: AnyAgentExecutor, store: DataStore, ar
2989
3070
  readonly threadId: string;
2990
3071
  readonly input: unknown;
2991
3072
  readonly tenantId?: string | null;
3073
+ /** The caller — `null` for an anonymous subject. Opens the thread on its
3074
+ * first message and is checked against the thread's owner on every later
3075
+ * one (`ensureThreadOwned`). Omitted ⇒ treated as anonymous. */
3076
+ readonly subjectId?: string | null;
2992
3077
  readonly order: number;
2993
3078
  readonly toolContext?: ToolContext;
2994
3079
  readonly throttleMs?: number;
2995
3080
  readonly promptOf?: (input: unknown) => string;
2996
- }) => Effect.Effect<string, Error>;
3081
+ }) => Effect.Effect<string, Error | AgentThreadAccessDenied>;
2997
3082
 
2998
3083
  /**
2999
3084
  * Drive a streaming assistant turn end-to-end against the persisted row.
@@ -3028,6 +3113,18 @@ export declare interface RunAssistantInput {
3028
3113
  readonly order: number;
3029
3114
  /** Throttle window for the live `parts` patch. Default 100ms. */
3030
3115
  readonly throttleMs?: number;
3116
+ /** The agent this turn belongs to — stamped onto the usage ledger row so
3117
+ * spend is attributable per agent. The synthesized send passes its name. */
3118
+ readonly agent?: string;
3119
+ /**
3120
+ * Record the turn's token usage into `_voltro_ai_usage` (default `true`).
3121
+ * A tool loop is a model call like any other, and this was the one path
3122
+ * that spent without recording: `aiStep` records, and the agent turn — which
3123
+ * owns a store by construction — wrote nothing, so an app whose model sites
3124
+ * are mostly tool loops could show zero spend. `false` for a caller that
3125
+ * records elsewhere.
3126
+ */
3127
+ readonly recordUsage?: boolean;
3031
3128
  }
3032
3129
 
3033
3130
  /**