@driftengine/ai 3.61.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 (82) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +103 -0
  4. package/dist/adapters/local.d.ts +29 -0
  5. package/dist/adapters/local.js +24 -0
  6. package/dist/adapters/proxy.d.ts +28 -0
  7. package/dist/adapters/proxy.js +138 -0
  8. package/dist/bridges/authority.d.ts +153 -0
  9. package/dist/bridges/authority.js +179 -0
  10. package/dist/bridges/navigation.d.ts +100 -0
  11. package/dist/bridges/navigation.js +139 -0
  12. package/dist/budget/budget.d.ts +34 -0
  13. package/dist/budget/budget.js +57 -0
  14. package/dist/command/apply.d.ts +24 -0
  15. package/dist/command/apply.js +40 -0
  16. package/dist/command/log.d.ts +55 -0
  17. package/dist/command/log.js +50 -0
  18. package/dist/context/assemble.d.ts +48 -0
  19. package/dist/context/assemble.js +55 -0
  20. package/dist/context/continuation.d.ts +14 -0
  21. package/dist/context/continuation.js +36 -0
  22. package/dist/describe/manifest.d.ts +70 -0
  23. package/dist/describe/manifest.js +99 -0
  24. package/dist/entities/context.d.ts +52 -0
  25. package/dist/entities/context.js +83 -0
  26. package/dist/index.d.ts +61 -0
  27. package/dist/index.js +40 -0
  28. package/dist/policy/types.d.ts +55 -0
  29. package/dist/policy/types.js +26 -0
  30. package/dist/policy/utility.d.ts +18 -0
  31. package/dist/policy/utility.js +47 -0
  32. package/dist/provider/create.d.ts +16 -0
  33. package/dist/provider/create.js +57 -0
  34. package/dist/provider/latency.d.ts +27 -0
  35. package/dist/provider/latency.js +52 -0
  36. package/dist/provider/types.d.ts +90 -0
  37. package/dist/provider/types.js +8 -0
  38. package/dist/realtime/session.d.ts +35 -0
  39. package/dist/realtime/session.js +34 -0
  40. package/dist/session/agent.d.ts +217 -0
  41. package/dist/session/agent.js +506 -0
  42. package/dist/session/replay.d.ts +32 -0
  43. package/dist/session/replay.js +81 -0
  44. package/dist/session/states.d.ts +28 -0
  45. package/dist/session/states.js +33 -0
  46. package/dist/session/usage.d.ts +43 -0
  47. package/dist/session/usage.js +38 -0
  48. package/dist/testing/deterministic.d.ts +65 -0
  49. package/dist/testing/deterministic.js +150 -0
  50. package/dist/tools/policy.d.ts +47 -0
  51. package/dist/tools/policy.js +84 -0
  52. package/dist/tools/registry.d.ts +69 -0
  53. package/dist/tools/registry.js +75 -0
  54. package/dist/tools/validate.d.ts +24 -0
  55. package/dist/tools/validate.js +80 -0
  56. package/package.json +59 -0
  57. package/src/adapters/local.ts +64 -0
  58. package/src/adapters/proxy.ts +187 -0
  59. package/src/bridges/authority.ts +244 -0
  60. package/src/bridges/navigation.ts +207 -0
  61. package/src/budget/budget.ts +73 -0
  62. package/src/command/apply.ts +52 -0
  63. package/src/command/log.ts +81 -0
  64. package/src/context/assemble.ts +104 -0
  65. package/src/context/continuation.ts +39 -0
  66. package/src/describe/manifest.ts +148 -0
  67. package/src/entities/context.ts +112 -0
  68. package/src/index.ts +94 -0
  69. package/src/policy/types.ts +70 -0
  70. package/src/policy/utility.ts +53 -0
  71. package/src/provider/create.ts +70 -0
  72. package/src/provider/latency.ts +57 -0
  73. package/src/provider/types.ts +96 -0
  74. package/src/realtime/session.ts +63 -0
  75. package/src/session/agent.ts +622 -0
  76. package/src/session/replay.ts +96 -0
  77. package/src/session/states.ts +63 -0
  78. package/src/session/usage.ts +66 -0
  79. package/src/testing/deterministic.ts +204 -0
  80. package/src/tools/policy.ts +114 -0
  81. package/src/tools/registry.ts +122 -0
  82. package/src/tools/validate.ts +92 -0
@@ -0,0 +1,61 @@
1
+ /*! DriftEngine | Copyright 2026 Drift Technologies | Apache-2.0 | https://github.com/drftrun/driftengine */
2
+ /**
3
+ * Drift AI — provider-neutral intelligence sessions.
4
+ *
5
+ * The abstraction is not "an LLM-controlled character". It is typed intelligence
6
+ * sessions over consumer-defined capabilities, which is what makes it an engine
7
+ * package rather than a game feature: the same runtime serves a game exposing
8
+ * movement and dialogue tools, a site exposing camera and material tools, and an
9
+ * editor exposing selection and transform tools.
10
+ *
11
+ * The one property worth stating at the barrel: **an agent is never without a
12
+ * purpose**. A deterministic policy floor runs inside the simulation whether or
13
+ * not a provider exists, and the request for the next intent is issued while the
14
+ * current one is still executing. A provider that is slow, absent, or over budget
15
+ * costs quality, never motion.
16
+ */
17
+ export { Budget } from './budget/budget.ts';
18
+ export type { BudgetLimits } from './budget/budget.ts';
19
+ export { createAiProvider } from './provider/create.ts';
20
+ export { hasKnownExtent, UNKNOWN_EXTENT, validateIntent } from './policy/types.ts';
21
+ export type { AgentPolicy, Intent, IntentCheck, PolicyContext, PolicyOption, } from './policy/types.ts';
22
+ export { UtilityPolicy } from './policy/utility.ts';
23
+ export { describeAgent, describeForDevelopment, requireStructuredOutput, } from './describe/manifest.ts';
24
+ export type { AiManifest, DevelopmentManifest, StructuredResult } from './describe/manifest.ts';
25
+ export { entityContext, entityRef, entityTool, parseEntityRef } from './entities/context.ts';
26
+ export type { EntityContextOptions } from './entities/context.ts';
27
+ export { createRealtimeSession } from './realtime/session.ts';
28
+ export type { RealtimeOptions, RealtimeResult } from './realtime/session.ts';
29
+ export { createLocalProvider } from './adapters/local.ts';
30
+ export type { LocalProviderConfig } from './adapters/local.ts';
31
+ export { createProxyProvider } from './adapters/proxy.ts';
32
+ export type { ProxyProviderConfig } from './adapters/proxy.ts';
33
+ export { assembleContext } from './context/assemble.ts';
34
+ export type { AssembledContext, ContextProvider, ContextSection } from './context/assemble.ts';
35
+ export { continuationPreamble } from './context/continuation.ts';
36
+ export { applyCommand } from './command/apply.ts';
37
+ export type { ApplyOutcome } from './command/apply.ts';
38
+ export { CommandLog } from './command/log.ts';
39
+ export type { AiCommand, AiPreemption, LogEntry } from './command/log.ts';
40
+ export { ReplaySession } from './session/replay.ts';
41
+ export type { ReplaySource } from './session/replay.ts';
42
+ export { AgentSession } from './session/agent.ts';
43
+ export type { AgentSessionOptions, Observation, WhileBusy } from './session/agent.ts';
44
+ export { nextState } from './session/states.ts';
45
+ export type { AgentState, AgentTransition } from './session/states.ts';
46
+ export { admitToolCall, RateWindows } from './tools/policy.ts';
47
+ export type { Admission, ExecutionPolicy } from './tools/policy.ts';
48
+ export { ToolRegistry } from './tools/registry.ts';
49
+ export type { ToolDefinition, ToolSchema } from './tools/registry.ts';
50
+ export type { NavigateArgs, NavigateResult, NavigationAdapter, NavigationBridgeOptions, } from './bridges/navigation.ts';
51
+ export { navigationBridge, reachableBy } from './bridges/navigation.ts';
52
+ export type { AgentRole, AuthoritativeAgentOptions, DecisionChannel } from './bridges/authority.ts';
53
+ export { AI_NETWORK_AUTHORITY, AuthoritativeAgent, loopbackDecisionChannel, } from './bridges/authority.ts';
54
+ export { validateArgs } from './tools/validate.ts';
55
+ export type { ValidationResult } from './tools/validate.ts';
56
+ export { LatencyEstimator } from './provider/latency.ts';
57
+ export { chargeUsage, createUsage, noteAbort, notePreemption } from './session/usage.ts';
58
+ export type { AiUsage } from './session/usage.ts';
59
+ export { DeterministicProvider } from './testing/deterministic.ts';
60
+ export type { DeterministicScript } from './testing/deterministic.ts';
61
+ export type { AiEvent, AiProvider, AiProviderCapabilities, AiProviderConfig, AiProviderResult, AiRequest, AiSession, AiSessionOptions, AssembledContextLike, } from './provider/types.ts';
package/dist/index.js ADDED
@@ -0,0 +1,40 @@
1
+ /*! DriftEngine | Copyright 2026 Drift Technologies | Apache-2.0 | https://github.com/drftrun/driftengine */
2
+ /**
3
+ * Drift AI — provider-neutral intelligence sessions.
4
+ *
5
+ * The abstraction is not "an LLM-controlled character". It is typed intelligence
6
+ * sessions over consumer-defined capabilities, which is what makes it an engine
7
+ * package rather than a game feature: the same runtime serves a game exposing
8
+ * movement and dialogue tools, a site exposing camera and material tools, and an
9
+ * editor exposing selection and transform tools.
10
+ *
11
+ * The one property worth stating at the barrel: **an agent is never without a
12
+ * purpose**. A deterministic policy floor runs inside the simulation whether or
13
+ * not a provider exists, and the request for the next intent is issued while the
14
+ * current one is still executing. A provider that is slow, absent, or over budget
15
+ * costs quality, never motion.
16
+ */
17
+ export { Budget } from './budget/budget.js';
18
+ export { createAiProvider } from './provider/create.js';
19
+ export { hasKnownExtent, UNKNOWN_EXTENT, validateIntent } from './policy/types.js';
20
+ export { UtilityPolicy } from './policy/utility.js';
21
+ export { describeAgent, describeForDevelopment, requireStructuredOutput, } from './describe/manifest.js';
22
+ export { entityContext, entityRef, entityTool, parseEntityRef } from './entities/context.js';
23
+ export { createRealtimeSession } from './realtime/session.js';
24
+ export { createLocalProvider } from './adapters/local.js';
25
+ export { createProxyProvider } from './adapters/proxy.js';
26
+ export { assembleContext } from './context/assemble.js';
27
+ export { continuationPreamble } from './context/continuation.js';
28
+ export { applyCommand } from './command/apply.js';
29
+ export { CommandLog } from './command/log.js';
30
+ export { ReplaySession } from './session/replay.js';
31
+ export { AgentSession } from './session/agent.js';
32
+ export { nextState } from './session/states.js';
33
+ export { admitToolCall, RateWindows } from './tools/policy.js';
34
+ export { ToolRegistry } from './tools/registry.js';
35
+ export { navigationBridge, reachableBy } from './bridges/navigation.js';
36
+ export { AI_NETWORK_AUTHORITY, AuthoritativeAgent, loopbackDecisionChannel, } from './bridges/authority.js';
37
+ export { validateArgs } from './tools/validate.js';
38
+ export { LatencyEstimator } from './provider/latency.js';
39
+ export { chargeUsage, createUsage, noteAbort, notePreemption } from './session/usage.js';
40
+ export { DeterministicProvider } from './testing/deterministic.js';
@@ -0,0 +1,55 @@
1
+ /**
2
+ * What an agent is doing, and the seam that decides it when nothing else can.
3
+ */
4
+ /** An intent whose extent is unknown. The watermark reads it as "ask immediately". */
5
+ export declare const UNKNOWN_EXTENT = -1;
6
+ export interface Intent {
7
+ readonly id: string;
8
+ readonly priority: number;
9
+ readonly toolIds: readonly string[];
10
+ readonly args: readonly unknown[];
11
+ /**
12
+ * How long this is expected to take, in milliseconds, or `UNKNOWN_EXTENT`.
13
+ *
14
+ * The continuation watermark subtracts the provider's measured p90 from this to
15
+ * decide when to ask what comes next. An unknown extent asks immediately, which is
16
+ * the behaviour a design that waited would have had and is correct for a one-shot.
17
+ */
18
+ readonly expectedExtentMs: number;
19
+ readonly source: 'floor' | 'model';
20
+ }
21
+ export interface PolicyContext {
22
+ readonly tick: number;
23
+ readonly agentId: string;
24
+ /** Milliseconds the current intent has been running. */
25
+ readonly elapsedMs: number;
26
+ }
27
+ export interface PolicyOption {
28
+ /**
29
+ * Pre-built and reused, never constructed per tick.
30
+ *
31
+ * The floor claims it allocates nothing per tick, and selecting among N existing
32
+ * objects makes that true by construction rather than by care.
33
+ */
34
+ readonly intent: Intent;
35
+ score(context: PolicyContext): number;
36
+ }
37
+ export interface AgentPolicy {
38
+ select(context: PolicyContext): Intent;
39
+ }
40
+ export declare function hasKnownExtent(intent: Intent): boolean;
41
+ export type IntentCheck = {
42
+ readonly ok: true;
43
+ } | {
44
+ readonly ok: false;
45
+ readonly reason: string;
46
+ };
47
+ /**
48
+ * Whether an intent is internally coherent.
49
+ *
50
+ * Only the pairing of tools to arguments, because that is the one an intent can get
51
+ * wrong on its own. Whether the tools *exist* is the registry's question and whether
52
+ * the call is *permitted* is the policy's; asking all three here would put three
53
+ * failures behind one message.
54
+ */
55
+ export declare function validateIntent(intent: Intent): IntentCheck;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * What an agent is doing, and the seam that decides it when nothing else can.
3
+ */
4
+ /** An intent whose extent is unknown. The watermark reads it as "ask immediately". */
5
+ export const UNKNOWN_EXTENT = -1;
6
+ export function hasKnownExtent(intent) {
7
+ return intent.expectedExtentMs > UNKNOWN_EXTENT;
8
+ }
9
+ /**
10
+ * Whether an intent is internally coherent.
11
+ *
12
+ * Only the pairing of tools to arguments, because that is the one an intent can get
13
+ * wrong on its own. Whether the tools *exist* is the registry's question and whether
14
+ * the call is *permitted* is the policy's; asking all three here would put three
15
+ * failures behind one message.
16
+ */
17
+ export function validateIntent(intent) {
18
+ if (intent.toolIds.length !== intent.args.length) {
19
+ return {
20
+ ok: false,
21
+ reason: `intent "${intent.id}" names ${intent.toolIds.length} tools but carries ` +
22
+ `${intent.args.length} argument sets`,
23
+ };
24
+ }
25
+ return { ok: true };
26
+ }
@@ -0,0 +1,18 @@
1
+ import type { AgentPolicy, Intent, PolicyContext, PolicyOption } from './types.ts';
2
+ /**
3
+ * The default floor: score every option, take the highest.
4
+ *
5
+ * Small on purpose. The seam is the deliverable and this is a convenience that has to
6
+ * earn its place by being used. *What it costs:* every consumer links a scorer it may
7
+ * not want. *What would make it wrong:* if consumers universally replace it, it is
8
+ * dead weight and belongs behind its own export rather than in the barrel.
9
+ *
10
+ * **Allocates nothing per tick.** `select` returns one of the options' pre-built
11
+ * intents — the same object identity every time that option wins. That is what makes
12
+ * the claim true by construction, and `utility.alloc.test.ts` is what keeps it true.
13
+ */
14
+ export declare class UtilityPolicy implements AgentPolicy {
15
+ private readonly options;
16
+ constructor(options: readonly PolicyOption[]);
17
+ select(context: PolicyContext): Intent;
18
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The default floor: score every option, take the highest.
3
+ *
4
+ * Small on purpose. The seam is the deliverable and this is a convenience that has to
5
+ * earn its place by being used. *What it costs:* every consumer links a scorer it may
6
+ * not want. *What would make it wrong:* if consumers universally replace it, it is
7
+ * dead weight and belongs behind its own export rather than in the barrel.
8
+ *
9
+ * **Allocates nothing per tick.** `select` returns one of the options' pre-built
10
+ * intents — the same object identity every time that option wins. That is what makes
11
+ * the claim true by construction, and `utility.alloc.test.ts` is what keeps it true.
12
+ */
13
+ export class UtilityPolicy {
14
+ options;
15
+ constructor(options) {
16
+ if (options.length === 0) {
17
+ /*
18
+ * At construction rather than at `select`. A floor with nothing to do is a
19
+ * configuration error, and discovering it inside a fixed step is discovering it
20
+ * at the worst possible moment — the one where there is nothing to fall back to.
21
+ */
22
+ throw new Error('a UtilityPolicy needs at least one option — a floor with nothing to do is not a floor');
23
+ }
24
+ this.options = options;
25
+ }
26
+ select(context) {
27
+ let best = 0;
28
+ let bestScore = Number.NEGATIVE_INFINITY;
29
+ for (let i = 0; i < this.options.length; i++) {
30
+ const option = this.options[i];
31
+ if (option === undefined)
32
+ continue;
33
+ const score = option.score(context);
34
+ /* Strictly greater, so a tie goes to the earlier declaration. Declaration order
35
+ is something a consumer controls and can read off the file; any other
36
+ tie-break is a rule they would have to be told. */
37
+ if (score > bestScore) {
38
+ bestScore = score;
39
+ best = i;
40
+ }
41
+ }
42
+ const chosen = this.options[best];
43
+ if (chosen === undefined)
44
+ throw new Error('unreachable: options is non-empty');
45
+ return chosen.intent;
46
+ }
47
+ }
@@ -0,0 +1,16 @@
1
+ import type { AiProviderCapabilities, AiProviderConfig, AiProviderResult } from './types.ts';
2
+ /**
3
+ * Accept a provider only if it declares what was asked of it, and say so either way.
4
+ *
5
+ * Renderer creation already works this way: `createRenderer` reports which backend it
6
+ * took and why, and `probeDevice` refuses in the device's own words. A provider is the
7
+ * same shape of decision with a bigger bill attached — a request to a provider that
8
+ * cannot do the thing costs latency and tokens before it fails, where a refusal here
9
+ * costs a string comparison.
10
+ *
11
+ * **There is no fallback path.** A downgrade from a local model to a paid remote one
12
+ * changes where a user's data goes, and a package that does it quietly makes every
13
+ * privacy notice written against it wrong. A caller wanting a second choice asks for
14
+ * it explicitly, having read the first refusal.
15
+ */
16
+ export declare function createAiProvider(config: AiProviderConfig, required: Partial<AiProviderCapabilities>): Promise<AiProviderResult>;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Accept a provider only if it declares what was asked of it, and say so either way.
3
+ *
4
+ * Renderer creation already works this way: `createRenderer` reports which backend it
5
+ * took and why, and `probeDevice` refuses in the device's own words. A provider is the
6
+ * same shape of decision with a bigger bill attached — a request to a provider that
7
+ * cannot do the thing costs latency and tokens before it fails, where a refusal here
8
+ * costs a string comparison.
9
+ *
10
+ * **There is no fallback path.** A downgrade from a local model to a paid remote one
11
+ * changes where a user's data goes, and a package that does it quietly makes every
12
+ * privacy notice written against it wrong. A caller wanting a second choice asks for
13
+ * it explicitly, having read the first refusal.
14
+ */
15
+ export async function createAiProvider(config, required) {
16
+ const provider = config.provider;
17
+ if (provider === undefined) {
18
+ return {
19
+ provider: null,
20
+ reason: `no provider was supplied for kind "${config.kind}" — build one with its own factory and pass it in`,
21
+ };
22
+ }
23
+ const missing = unsupported(provider, required);
24
+ if (missing.length > 0) {
25
+ return {
26
+ provider: null,
27
+ reason: `provider "${provider.id}" is unsupported for this request: ` +
28
+ `it declares no ${missing.join(', no ')}`,
29
+ };
30
+ }
31
+ return {
32
+ provider,
33
+ reason: `provider "${provider.id}" ready, declaring ${declared(provider).join(', ')}`,
34
+ };
35
+ }
36
+ /**
37
+ * Only the capabilities that were asked for *and* are absent.
38
+ *
39
+ * Naming a satisfied requirement in a refusal sends a reader looking for a fault in
40
+ * the half that worked, which is how a message costs more time than no message.
41
+ */
42
+ function unsupported(provider, required) {
43
+ const missing = [];
44
+ for (const key of Object.keys(required)) {
45
+ if (required[key] === true && provider.capabilities[key] !== true)
46
+ missing.push(key);
47
+ }
48
+ return missing;
49
+ }
50
+ function declared(provider) {
51
+ const names = [];
52
+ for (const key of Object.keys(provider.capabilities)) {
53
+ if (provider.capabilities[key])
54
+ names.push(key);
55
+ }
56
+ return names.length > 0 ? names : ['nothing'];
57
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A p90 that revises as it measures, and says so before it knows.
3
+ *
4
+ * The continuation watermark issues the next request when the current intent's
5
+ * remaining extent falls to this number. Measuring it rather than configuring it is
6
+ * what removes the tuning constant that would be wrong on every device, and wrong
7
+ * again the moment a consumer switched providers.
8
+ *
9
+ * **p90 rather than p50 or p99.** At p50 half the continuations land late and the
10
+ * agent visibly drops to its floor mid-behaviour. At p99 the lead is long enough that
11
+ * the context goes stale for the tail. *What would make this wrong:* a provider whose
12
+ * latency is bimodal — a cache hit at 40ms and a miss at 3s — has a p90 that describes
13
+ * neither, and the answer there is a per-request estimate from the request's own
14
+ * shape, not a different percentile.
15
+ */
16
+ export declare class LatencyEstimator {
17
+ private readonly window;
18
+ private readonly scratch;
19
+ private readonly minSamples;
20
+ private cursor;
21
+ private filled;
22
+ constructor(capacity?: number, minSamples?: number);
23
+ get samples(): number;
24
+ /** `-1` until `minSamples` are in, which the watermark reads as "issue immediately". */
25
+ get p90(): number;
26
+ record(ms: number): void;
27
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * A p90 that revises as it measures, and says so before it knows.
3
+ *
4
+ * The continuation watermark issues the next request when the current intent's
5
+ * remaining extent falls to this number. Measuring it rather than configuring it is
6
+ * what removes the tuning constant that would be wrong on every device, and wrong
7
+ * again the moment a consumer switched providers.
8
+ *
9
+ * **p90 rather than p50 or p99.** At p50 half the continuations land late and the
10
+ * agent visibly drops to its floor mid-behaviour. At p99 the lead is long enough that
11
+ * the context goes stale for the tail. *What would make this wrong:* a provider whose
12
+ * latency is bimodal — a cache hit at 40ms and a miss at 3s — has a p90 that describes
13
+ * neither, and the answer there is a per-request estimate from the request's own
14
+ * shape, not a different percentile.
15
+ */
16
+ export class LatencyEstimator {
17
+ window;
18
+ scratch;
19
+ minSamples;
20
+ cursor = 0;
21
+ filled = 0;
22
+ constructor(capacity = 64, minSamples = 8) {
23
+ const slots = Math.max(1, capacity | 0);
24
+ this.window = new Float64Array(slots);
25
+ /* Sorted into a buffer owned once rather than allocated per read. The watermark
26
+ asks for this on the tick an intent starts, which is a per-intent path. */
27
+ this.scratch = new Float64Array(slots);
28
+ this.minSamples = Math.max(1, minSamples | 0);
29
+ }
30
+ get samples() {
31
+ return this.filled;
32
+ }
33
+ /** `-1` until `minSamples` are in, which the watermark reads as "issue immediately". */
34
+ get p90() {
35
+ if (this.filled < this.minSamples)
36
+ return -1;
37
+ for (let i = 0; i < this.filled; i++) {
38
+ const value = this.window[i];
39
+ this.scratch[i] = value === undefined ? 0 : value;
40
+ }
41
+ const view = this.scratch.subarray(0, this.filled);
42
+ view.sort();
43
+ const index = Math.min(this.filled - 1, Math.ceil(this.filled * 0.9) - 1);
44
+ return view[Math.max(0, index)] ?? -1;
45
+ }
46
+ record(ms) {
47
+ this.window[this.cursor] = ms;
48
+ this.cursor = (this.cursor + 1) % this.window.length;
49
+ if (this.filled < this.window.length)
50
+ this.filled++;
51
+ }
52
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The provider seam. Provider-specific concerns stay behind adapters.
3
+ *
4
+ * Capability is **declared rather than assumed**, so a session refuses an
5
+ * unsupported requirement early and in words instead of discovering it in
6
+ * production at whatever a failed request costs.
7
+ */
8
+ /**
9
+ * What a provider says it can do.
10
+ *
11
+ * Declared by the adapter rather than probed, except where a probe is cheap and
12
+ * honest — `createLocalProvider` runs one, because a local model's availability is
13
+ * a property of the device rather than of the configuration.
14
+ */
15
+ export interface AiProviderCapabilities {
16
+ readonly text: boolean;
17
+ readonly streamingText: boolean;
18
+ readonly structuredOutput: boolean;
19
+ readonly toolCalling: boolean;
20
+ readonly realtimeAudio: boolean;
21
+ readonly imageInput: boolean;
22
+ readonly local: boolean;
23
+ }
24
+ /**
25
+ * Context assembled for one request.
26
+ *
27
+ * Declared minimally here and widened where it is built. What the seam needs is
28
+ * the capture tick: a model must be able to know its snapshot may be stale, and a
29
+ * buffered agent widens that gap on purpose.
30
+ */
31
+ export interface AssembledContextLike {
32
+ readonly capturedAtTick: number;
33
+ }
34
+ export type AiEvent = {
35
+ readonly kind: 'text';
36
+ readonly text: string;
37
+ } | {
38
+ readonly kind: 'toolCall';
39
+ readonly callId: string;
40
+ readonly toolId: string;
41
+ readonly args: unknown;
42
+ } | {
43
+ readonly kind: 'usage';
44
+ readonly inputTokens: number;
45
+ readonly outputTokens: number;
46
+ } | {
47
+ readonly kind: 'done';
48
+ readonly reason: 'complete' | 'aborted' | 'error';
49
+ readonly message?: string;
50
+ };
51
+ export interface AiRequest {
52
+ readonly preamble: string;
53
+ readonly context: AssembledContextLike;
54
+ readonly toolIds: readonly string[];
55
+ readonly signal: AbortSignal;
56
+ }
57
+ export interface AiSessionOptions {
58
+ readonly model: string;
59
+ readonly systemPrompt?: string;
60
+ readonly maxOutputTokens?: number;
61
+ readonly temperature?: number;
62
+ }
63
+ export interface AiSession {
64
+ run(request: AiRequest): AsyncIterable<AiEvent>;
65
+ abort(reason?: string): void;
66
+ }
67
+ export interface AiProvider {
68
+ readonly id: string;
69
+ readonly capabilities: AiProviderCapabilities;
70
+ createSession(options: AiSessionOptions): AiSession;
71
+ }
72
+ export interface AiProviderConfig {
73
+ readonly kind: 'proxy' | 'local' | 'deterministic';
74
+ readonly session: AiSessionOptions;
75
+ /**
76
+ * The provider itself, where the caller already has one.
77
+ *
78
+ * Adapters that must be constructed — a local model that has to probe the device —
79
+ * are built by their own factory and handed here, so this function does one job:
80
+ * comparing what is declared against what is required.
81
+ */
82
+ readonly provider?: AiProvider;
83
+ /** Adapter-specific, and never a credential — see `adapters/proxy.ts`. */
84
+ readonly adapter?: Readonly<Record<string, unknown>>;
85
+ }
86
+ export interface AiProviderResult {
87
+ readonly provider: AiProvider | null;
88
+ /** Always a sentence, on both paths. An empty reason is the silent no-op. */
89
+ readonly reason: string;
90
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The provider seam. Provider-specific concerns stay behind adapters.
3
+ *
4
+ * Capability is **declared rather than assumed**, so a session refuses an
5
+ * unsupported requirement early and in words instead of discovering it in
6
+ * production at whatever a failed request costs.
7
+ */
8
+ export {};
@@ -0,0 +1,35 @@
1
+ import type { AiProvider, AiSession } from '../provider/types.ts';
2
+ /**
3
+ * A realtime transport, emitting the same events every other session emits.
4
+ *
5
+ * **Additive.** Nothing downstream branches on transport: the buffered loop, the floor,
6
+ * the admission guards and the command log all see the same `AiEvent` union they
7
+ * already see. A session with realtime configured still has exactly one request in
8
+ * flight, because the state machine does not know what a transport is.
9
+ *
10
+ * **Audio stays bridged, not coupled.** This module imports nothing from
11
+ * `@driftengine/audio`, and the package declares no dependency on it. A consumer wires
12
+ * its own microphone and speaker to whatever this yields, because the moment an AI
13
+ * package owned an audio graph it would be an AI package that knew what a mix was.
14
+ */
15
+ export interface RealtimeOptions {
16
+ readonly model: string;
17
+ /** Frames a consumer pushes in. Text here, because audio is the consumer's to carry. */
18
+ readonly input?: AsyncIterable<string>;
19
+ }
20
+ export type RealtimeResult = {
21
+ readonly session: AiSession;
22
+ readonly reason: string;
23
+ } | {
24
+ readonly session: null;
25
+ readonly reason: string;
26
+ };
27
+ /**
28
+ * Open a realtime session, or refuse before connecting.
29
+ *
30
+ * Refused **before** any transport is opened, in the shape §32 requires of every other
31
+ * provider decision: a socket opened against a provider that cannot hold a realtime
32
+ * conversation is a socket that fails later and more expensively than a string
33
+ * comparison.
34
+ */
35
+ export declare function createRealtimeSession(provider: AiProvider, options: RealtimeOptions): RealtimeResult;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Open a realtime session, or refuse before connecting.
3
+ *
4
+ * Refused **before** any transport is opened, in the shape §32 requires of every other
5
+ * provider decision: a socket opened against a provider that cannot hold a realtime
6
+ * conversation is a socket that fails later and more expensively than a string
7
+ * comparison.
8
+ */
9
+ export function createRealtimeSession(provider, options) {
10
+ if (!provider.capabilities.realtimeAudio) {
11
+ return {
12
+ session: null,
13
+ reason: `provider "${provider.id}" declares no realtime transport — a session cannot be opened against it`,
14
+ };
15
+ }
16
+ const inner = provider.createSession({ model: options.model });
17
+ let disposed = false;
18
+ return {
19
+ session: {
20
+ run(request) {
21
+ return inner.run(request);
22
+ },
23
+ abort(reason) {
24
+ /* A second disposal is a no-op rather than an error. Disposal races a scene
25
+ replacement and a hot reload, and both may reach the same session. */
26
+ if (disposed)
27
+ return;
28
+ disposed = true;
29
+ inner.abort(reason ?? 'realtime session disposed');
30
+ },
31
+ },
32
+ reason: `realtime session open against "${provider.id}"`,
33
+ };
34
+ }