@tanstack/ai 0.39.1 → 0.40.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.
@@ -11,6 +11,7 @@ library: tanstack-ai
11
11
  library_version: '0.10.0'
12
12
  sources:
13
13
  - 'TanStack/ai:docs/advanced/middleware.md'
14
+ - 'TanStack/ai:docs/sandbox/observability.md'
14
15
  ---
15
16
 
16
17
  # Middleware
@@ -371,6 +372,95 @@ Options: `maxSize` (default 100), `ttl` (default Infinity), `toolNames` (default
371
372
  `keyFn` (custom cache key), `storage` (custom backend like Redis). See
372
373
  `docs/advanced/middleware.md` for custom storage examples.
373
374
 
375
+ ## Sandbox File-Event Hooks (`sandbox` group)
376
+
377
+ Declare a `sandbox: ChatSandboxHooks` group on `defineChatMiddleware` to react
378
+ to every file created/changed/deleted inside a sandbox provided by
379
+ `withSandbox` (from `@tanstack/ai-sandbox`). These fire **per-run**,
380
+ server-side, and each handler receives the run's `ChatMiddlewareContext` as
381
+ the first argument:
382
+
383
+ ```typescript
384
+ import { defineChatMiddleware } from '@tanstack/ai'
385
+ import { db } from './db'
386
+
387
+ const auditMiddleware = defineChatMiddleware({
388
+ name: 'audit',
389
+ sandbox: {
390
+ onFile: (ctx, e) => console.log(ctx.runId, e.type, e.path),
391
+ onFileCreate: (ctx, e) => db.log({ run: ctx.runId, event: e }),
392
+ },
393
+ })
394
+ ```
395
+
396
+ | Hook | Fires for |
397
+ | -------------- | -------------------------- |
398
+ | `onFile` | Every create/change/delete |
399
+ | `onFileCreate` | File creates only |
400
+ | `onFileChange` | File changes only |
401
+ | `onFileDelete` | File deletes only |
402
+
403
+ These are independent of the stream: the engine also emits a `sandbox.file`
404
+ `CUSTOM` chunk per change regardless of whether any `sandbox` hooks are
405
+ registered, so a client can react to the same edits without middleware. See
406
+ `ai-core/ag-ui-protocol/SKILL.md` for reading that chunk (and the opt-in
407
+ `sandbox.file.diff` chunk) off `ChatStream`.
408
+
409
+ ### `before()` / `after()` / `diff()` — lazy, git-backed content accessors
410
+
411
+ Each hook receives a `SandboxFileHookEvent`: the serializable
412
+ `{ type, path, timestamp }` plus three lazy accessors for the file's content:
413
+
414
+ ```ts
415
+ interface SandboxFileHookEvent {
416
+ type: 'create' | 'change' | 'delete'
417
+ path: string
418
+ timestamp: number
419
+ before(): Promise<string> // content at the session baseline ('' if new / non-git)
420
+ after(): Promise<string> // current content ('' if deleted)
421
+ diff(): Promise<string> // unified patch vs the baseline
422
+ }
423
+ ```
424
+
425
+ ```typescript
426
+ import { defineChatMiddleware } from '@tanstack/ai'
427
+ import { db } from './db'
428
+
429
+ const auditMiddleware = defineChatMiddleware({
430
+ name: 'audit',
431
+ sandbox: {
432
+ onFileChange: async (ctx, e) => {
433
+ const [before, after] = await Promise.all([e.before(), e.after()])
434
+ db.log({ run: ctx.runId, path: e.path, before, after })
435
+ },
436
+ },
437
+ })
438
+ ```
439
+
440
+ **Lazy — path-only hooks pay nothing.** `before()`, `after()`, and `diff()`
441
+ are methods, not fields: each only reads the file or shells out to `git` when
442
+ called. A hook that only reads `e.path`/`e.type` (like the `onFile` logger
443
+ above) never touches the filesystem or spawns a process.
444
+
445
+ **Git session baseline.** The sandbox snapshots `git rev-parse HEAD` once at
446
+ setup as the session baseline (empty string if the workspace isn't a git repo
447
+ or has no commits). `before()` and `diff()` always diff against that same
448
+ fixed baseline for the rest of the run, so `onFileChange` reports the file's
449
+ **cumulative** change since the run started, not just the delta since the
450
+ last poll. `after()` always reads current on-disk content. None of the three
451
+ accessors throw: a deleted file resolves `after()` to `''` (it still has
452
+ `before()`); a new file resolves `before()` to `''` (it still has `after()`);
453
+ a non-git workspace resolves **both** `before()` and `after()` to `''` and
454
+ makes `diff()` fall back to a synthesized add-patch built from `after()` —
455
+ except for a `delete` event in a non-git workspace, where there's nothing to
456
+ synthesize and `diff()` resolves to `''`.
457
+
458
+ **Hook errors are swallowed per hook.** A throwing `sandbox` hook is caught
459
+ and logged under the `sandbox` debug category — it cannot break the run or
460
+ stop other hooks (or the `sandbox.file` chunk) from continuing.
461
+
462
+ Source: docs/sandbox/observability.md
463
+
374
464
  ## Common Mistakes
375
465
 
376
466
  ### a. MEDIUM: Trying to modify StreamChunks in middleware
@@ -451,3 +541,4 @@ Source: docs/advanced/middleware.md
451
541
 
452
542
  - See also: **ai-core/chat-experience/SKILL.md** -- Middleware hooks into the chat lifecycle
453
543
  - See also: **ai-core/structured-outputs/SKILL.md** -- Middleware now wraps the final structured-output call; use `onStructuredOutputConfig` for JSON-Schema transforms
544
+ - See also: **ai-core/ag-ui-protocol/SKILL.md** -- Reading the `sandbox.file` / `sandbox.file.diff` `CUSTOM` chunks the sandbox runtime emits alongside these `sandbox` hooks, via `ChatStream`'s typed `KnownCustomEvent` narrowing
@@ -45,6 +45,7 @@ import type {
45
45
  import type {
46
46
  AgentLoopStrategy,
47
47
  AnyTool,
48
+ ChatStream,
48
49
  ConstrainedModelMessage,
49
50
  CustomEvent,
50
51
  InferSchemaType,
@@ -69,7 +70,7 @@ import type {
69
70
  ChatMiddleware,
70
71
  ChatMiddlewareConfig,
71
72
  ChatMiddlewareContext,
72
- SandboxFileEvent,
73
+ SandboxFileHookEvent,
73
74
  StructuredOutputMiddlewareConfig,
74
75
  } from './middleware/types'
75
76
  import type { CheckCoverage } from './middleware/builder'
@@ -404,7 +405,7 @@ export type TextActivityResult<
404
405
  : Promise<InferSchemaType<TSchema>>
405
406
  : [TStream] extends [false]
406
407
  ? Promise<string>
407
- : AsyncIterable<StreamChunk>
408
+ : ChatStream
408
409
 
409
410
  // ===========================
410
411
  // ChatEngine Implementation
@@ -712,11 +713,30 @@ class TextEngine<
712
713
  // a `sandbox.file` custom chunk to be drained into the public stream.
713
714
  provideSandboxRuntime(this.middlewareCtx, {
714
715
  logger: this.logger,
715
- emit: (event: SandboxFileEvent) => {
716
- this.logger.sandbox(`file ${event.type} ${event.path}`, { event })
717
- void this.middlewareRunner.runSandboxFile(this.middlewareCtx, event)
716
+ emit: (event: SandboxFileHookEvent) => {
717
+ this.logger.sandbox(`file ${event.type} ${event.path}`, {
718
+ event: {
719
+ type: event.type,
720
+ path: event.path,
721
+ timestamp: event.timestamp,
722
+ },
723
+ })
724
+ void this.middlewareRunner
725
+ .runSandboxFile(this.middlewareCtx, event)
726
+ .catch((err: unknown) => {
727
+ this.logger.errors('sandbox file hook failed', { error: err })
728
+ })
729
+ this.sandboxFileQueue.push(
730
+ this.createCustomEventChunk('sandbox.file', {
731
+ type: event.type,
732
+ path: event.path,
733
+ timestamp: event.timestamp,
734
+ }),
735
+ )
736
+ },
737
+ emitFileDiff: (value: { path: string; diff: string }) => {
718
738
  this.sandboxFileQueue.push(
719
- this.createCustomEventChunk('sandbox.file', { ...event }),
739
+ this.createCustomEventChunk('sandbox.file.diff', value),
720
740
  )
721
741
  },
722
742
  })
@@ -11,7 +11,7 @@ import type {
11
11
  ErrorInfo,
12
12
  FinishInfo,
13
13
  IterationInfo,
14
- SandboxFileEvent,
14
+ SandboxFileHookEvent,
15
15
  StructuredOutputMiddlewareConfig,
16
16
  ToolCallHookContext,
17
17
  ToolPhaseCompleteInfo,
@@ -352,7 +352,7 @@ export class MiddlewareRunner<TContext = unknown> {
352
352
  */
353
353
  async runSandboxFile(
354
354
  ctx: ChatMiddlewareContext<TContext>,
355
- event: SandboxFileEvent,
355
+ event: SandboxFileHookEvent,
356
356
  ): Promise<void> {
357
357
  const typed = (
358
358
  {
@@ -14,6 +14,7 @@ export type {
14
14
  AbortInfo,
15
15
  ErrorInfo,
16
16
  SandboxFileEvent,
17
+ SandboxFileHookEvent,
17
18
  ChatSandboxHooks,
18
19
  } from './types'
19
20
 
@@ -7,10 +7,12 @@
7
7
  */
8
8
  import { createCapability } from './capabilities'
9
9
  import type { InternalLogger } from '../../../logger/internal-logger'
10
- import type { SandboxFileEvent } from './types'
10
+ import type { SandboxFileHookEvent } from './types'
11
11
 
12
12
  export interface SandboxRuntime {
13
- emit: (event: SandboxFileEvent) => void
13
+ emit: (event: SandboxFileHookEvent) => void
14
+ /** Emit an opt-in per-file `sandbox.file.diff` CUSTOM chunk. */
15
+ emitFileDiff: (value: { path: string; diff: string }) => void
14
16
  logger: InternalLogger
15
17
  }
16
18
 
@@ -21,6 +21,19 @@ export interface SandboxFileEvent {
21
21
  timestamp: number
22
22
  }
23
23
 
24
+ /** The file event a sandbox hook receives: the serializable {@link SandboxFileEvent}
25
+ * plus lazy, git-backed content accessors. Accessors compute on call, so a hook
26
+ * that only reads `path`/`type` pays nothing. Never present on the serialized
27
+ * `sandbox.file` CUSTOM chunk. */
28
+ export interface SandboxFileHookEvent extends SandboxFileEvent {
29
+ /** Content at the session baseline (`''` for a new file or non-git workspace). */
30
+ before: () => Promise<string>
31
+ /** Current content (`''` when the event is a delete). */
32
+ after: () => Promise<string>
33
+ /** Unified patch vs the session baseline (synthesized add-patch when non-git). */
34
+ diff: () => Promise<string>
35
+ }
36
+
24
37
  /**
25
38
  * Sandbox file-event hooks a chat middleware can declare. Fire server-side for
26
39
  * every file create/change/delete observed in the sandbox during the run.
@@ -28,19 +41,19 @@ export interface SandboxFileEvent {
28
41
  export interface ChatSandboxHooks<TContext = unknown> {
29
42
  onFile?: (
30
43
  ctx: ChatMiddlewareContext<TContext>,
31
- e: SandboxFileEvent,
44
+ e: SandboxFileHookEvent,
32
45
  ) => void | Promise<void>
33
46
  onFileCreate?: (
34
47
  ctx: ChatMiddlewareContext<TContext>,
35
- e: SandboxFileEvent,
48
+ e: SandboxFileHookEvent,
36
49
  ) => void | Promise<void>
37
50
  onFileChange?: (
38
51
  ctx: ChatMiddlewareContext<TContext>,
39
- e: SandboxFileEvent,
52
+ e: SandboxFileHookEvent,
40
53
  ) => void | Promise<void>
41
54
  onFileDelete?: (
42
55
  ctx: ChatMiddlewareContext<TContext>,
43
- e: SandboxFileEvent,
56
+ e: SandboxFileHookEvent,
44
57
  ) => void | Promise<void>
45
58
  }
46
59
 
@@ -19,7 +19,11 @@ import type { InternalLogger } from '../../logger/internal-logger'
19
19
  import type { DebugOption } from '../../logger/types'
20
20
  import type { GenerationMiddleware } from '../middleware'
21
21
  import type { TranscriptionAdapter } from './adapter'
22
- import type { StreamChunk, TranscriptionResult } from '../../types'
22
+ import type {
23
+ StreamChunk,
24
+ TranscriptionResponseFormat,
25
+ TranscriptionResult,
26
+ } from '../../types'
23
27
 
24
28
  // ===========================
25
29
  // Activity Kind
@@ -67,7 +71,7 @@ export interface TranscriptionActivityOptions<
67
71
  /** An optional prompt to guide the transcription */
68
72
  prompt?: string
69
73
  /** The format of the transcription output */
70
- responseFormat?: 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt'
74
+ responseFormat?: TranscriptionResponseFormat
71
75
  /** Provider-specific options for transcription */
72
76
  modelOptions?: TranscriptionProviderOptions<TAdapter>
73
77
  /**
package/src/index.ts CHANGED
@@ -121,6 +121,7 @@ export type {
121
121
  AbortInfo,
122
122
  ErrorInfo,
123
123
  SandboxFileEvent,
124
+ SandboxFileHookEvent,
124
125
  ChatSandboxHooks,
125
126
  } from './activities/chat/middleware/index'
126
127
 
package/src/types.ts CHANGED
@@ -1381,6 +1381,105 @@ export interface UIResourceEvent extends CustomEvent {
1381
1381
  }
1382
1382
  }
1383
1383
 
1384
+ // ── Sandbox events ──────────────────────────────────────────────────────────
1385
+ export interface SandboxFileCustomEvent extends CustomEvent {
1386
+ name: 'sandbox.file'
1387
+ value: {
1388
+ type: 'create' | 'change' | 'delete'
1389
+ path: string
1390
+ timestamp: number
1391
+ }
1392
+ }
1393
+ export interface SandboxFileDiffEvent extends CustomEvent {
1394
+ name: 'sandbox.file.diff'
1395
+ value: { path: string; diff: string }
1396
+ }
1397
+
1398
+ // ── Harness events ──────────────────────────────────────────────────────────
1399
+ export interface FileChangedEvent extends CustomEvent {
1400
+ name: 'file.changed'
1401
+ value: { path: string; diff: string }
1402
+ }
1403
+ export interface SessionIdEvent extends CustomEvent {
1404
+ name: `${string}.session-id`
1405
+ value: { sessionId: string }
1406
+ }
1407
+
1408
+ // ── Code-mode events ────────────────────────────────────────────────────────
1409
+ export interface CodeModeExecutionStartedEvent extends CustomEvent {
1410
+ name: 'code_mode:execution_started'
1411
+ value: { timestamp: number; codeLength: number }
1412
+ }
1413
+ export interface CodeModeConsoleEvent extends CustomEvent {
1414
+ name: 'code_mode:console'
1415
+ value: {
1416
+ level: 'log' | 'warn' | 'error' | 'info'
1417
+ message: string
1418
+ timestamp: number
1419
+ }
1420
+ }
1421
+ export interface CodeModeExternalCallEvent extends CustomEvent {
1422
+ name: 'code_mode:external_call'
1423
+ value: { function: string; args: unknown; timestamp: number }
1424
+ }
1425
+ export interface CodeModeExternalResultEvent extends CustomEvent {
1426
+ name: 'code_mode:external_result'
1427
+ value: { function: string; result: unknown; duration: number }
1428
+ }
1429
+ export interface CodeModeExternalErrorEvent extends CustomEvent {
1430
+ name: 'code_mode:external_error'
1431
+ value: { function: string; error: string; duration: number }
1432
+ }
1433
+ export interface CodeModeSkillCallEvent extends CustomEvent {
1434
+ name: 'code_mode:skill_call'
1435
+ value: { skill: string; input: unknown; timestamp: number }
1436
+ }
1437
+ export interface CodeModeSkillResultEvent extends CustomEvent {
1438
+ name: 'code_mode:skill_result'
1439
+ value: { skill: string; result: unknown; duration: number; timestamp: number }
1440
+ }
1441
+ export interface CodeModeSkillErrorEvent extends CustomEvent {
1442
+ name: 'code_mode:skill_error'
1443
+ value: { skill: string; error: string; duration: number; timestamp: number }
1444
+ }
1445
+ export interface SkillRegisteredEvent extends CustomEvent {
1446
+ name: 'skill:registered'
1447
+ value: { id: string; name: string; description: string; timestamp: number }
1448
+ }
1449
+
1450
+ /**
1451
+ * Every CUSTOM event TanStack AI itself emits, as a discriminated union on
1452
+ * `name`. User-emitted custom events (via `emitCustomEvent` with a custom name)
1453
+ * are intentionally absent — they still flow at runtime.
1454
+ */
1455
+ export type KnownCustomEvent =
1456
+ | SandboxFileCustomEvent
1457
+ | SandboxFileDiffEvent
1458
+ | FileChangedEvent
1459
+ | SessionIdEvent
1460
+ | CodeModeExecutionStartedEvent
1461
+ | CodeModeConsoleEvent
1462
+ | CodeModeExternalCallEvent
1463
+ | CodeModeExternalResultEvent
1464
+ | CodeModeExternalErrorEvent
1465
+ | CodeModeSkillCallEvent
1466
+ | CodeModeSkillResultEvent
1467
+ | CodeModeSkillErrorEvent
1468
+ | SkillRegisteredEvent
1469
+ | StructuredOutputStartEvent
1470
+ | StructuredOutputCompleteEvent
1471
+ | ApprovalRequestedEvent
1472
+ | ToolInputAvailableEvent
1473
+ | UIResourceEvent
1474
+
1475
+ /** The default chat streaming result: standard chunks plus every typed
1476
+ * framework CUSTOM event, with the `value: any` catch-all excluded so
1477
+ * literal-`name` narrowing types `value`. User-emitted custom names are typed
1478
+ * out (still flow at runtime — branch outside the name narrows or cast). */
1479
+ export type ChatStream = AsyncIterable<
1480
+ Exclude<StreamChunk, CustomEvent> | KnownCustomEvent
1481
+ >
1482
+
1384
1483
  /**
1385
1484
  * Public type for streams returned by `chat({ outputSchema, stream: true })`.
1386
1485
  *
@@ -1930,6 +2029,13 @@ export interface TTSResult {
1930
2029
  * Options for audio transcription.
1931
2030
  * These are the common options supported across providers.
1932
2031
  */
2032
+ export type TranscriptionResponseFormat =
2033
+ | 'json'
2034
+ | 'text'
2035
+ | 'srt'
2036
+ | 'verbose_json'
2037
+ | 'vtt'
2038
+
1933
2039
  export interface TranscriptionOptions<
1934
2040
  TProviderOptions extends object = object,
1935
2041
  > {
@@ -1942,7 +2048,7 @@ export interface TranscriptionOptions<
1942
2048
  /** An optional prompt to guide the transcription */
1943
2049
  prompt?: string
1944
2050
  /** The format of the transcription output */
1945
- responseFormat?: 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt'
2051
+ responseFormat?: TranscriptionResponseFormat
1946
2052
  /** Model-specific options for transcription */
1947
2053
  modelOptions?: TProviderOptions
1948
2054
  /**