@tanstack/ai 0.38.0 → 0.39.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 (34) hide show
  1. package/dist/esm/activities/chat/index.js +28 -0
  2. package/dist/esm/activities/chat/index.js.map +1 -1
  3. package/dist/esm/activities/chat/middleware/compose.d.ts +7 -1
  4. package/dist/esm/activities/chat/middleware/compose.js +27 -0
  5. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  6. package/dist/esm/activities/chat/middleware/index.d.ts +1 -1
  7. package/dist/esm/activities/chat/middleware/sandbox-runtime.d.ts +8 -0
  8. package/dist/esm/activities/chat/middleware/sandbox-runtime.js +9 -0
  9. package/dist/esm/activities/chat/middleware/sandbox-runtime.js.map +1 -0
  10. package/dist/esm/activities/chat/middleware/types.d.ts +22 -0
  11. package/dist/esm/adapter-internals.d.ts +2 -0
  12. package/dist/esm/adapter-internals.js +4 -0
  13. package/dist/esm/adapter-internals.js.map +1 -1
  14. package/dist/esm/index.d.ts +2 -2
  15. package/dist/esm/logger/internal-logger.d.ts +2 -0
  16. package/dist/esm/logger/internal-logger.js +6 -1
  17. package/dist/esm/logger/internal-logger.js.map +1 -1
  18. package/dist/esm/logger/resolve.js +6 -3
  19. package/dist/esm/logger/resolve.js.map +1 -1
  20. package/dist/esm/logger/types.d.ts +5 -0
  21. package/dist/esm/types.d.ts +18 -0
  22. package/package.json +1 -1
  23. package/skills/ai-core/adapter-configuration/SKILL.md +25 -12
  24. package/src/activities/chat/index.ts +39 -0
  25. package/src/activities/chat/middleware/compose.ts +34 -0
  26. package/src/activities/chat/middleware/index.ts +2 -0
  27. package/src/activities/chat/middleware/sandbox-runtime.ts +21 -0
  28. package/src/activities/chat/middleware/types.ts +37 -0
  29. package/src/adapter-internals.ts +6 -0
  30. package/src/index.ts +4 -0
  31. package/src/logger/internal-logger.ts +6 -0
  32. package/src/logger/resolve.ts +3 -0
  33. package/src/logger/types.ts +5 -0
  34. package/src/types.ts +20 -0
@@ -27,6 +27,7 @@ import {
27
27
  import { maxIterations as maxIterationsStrategy } from './agent-loop-strategies'
28
28
  import { convertMessagesToModelMessages, generateMessageId } from './messages'
29
29
  import { MiddlewareRunner } from './middleware/compose'
30
+ import { provideSandboxRuntime } from './middleware/sandbox-runtime'
30
31
  import { CapabilityRegistry } from './middleware/capabilities'
31
32
  import { validateCapabilities } from './middleware/validate'
32
33
  import { MCPManager } from './mcp/manager'
@@ -67,6 +68,7 @@ import type {
67
68
  ChatMiddleware,
68
69
  ChatMiddlewareConfig,
69
70
  ChatMiddlewareContext,
71
+ SandboxFileEvent,
70
72
  StructuredOutputMiddlewareConfig,
71
73
  } from './middleware/types'
72
74
  import type { CheckCoverage } from './middleware/builder'
@@ -542,6 +544,7 @@ class TextEngine<
542
544
  // Middleware support
543
545
  private readonly middlewareRunner: MiddlewareRunner<TContext>
544
546
  private readonly middlewareCtx: ChatMiddlewareContext<TContext>
547
+ private readonly sandboxFileQueue: Array<StreamChunk> = []
545
548
  private readonly deferredPromises: Array<Promise<unknown>> = []
546
549
  private abortReason?: string
547
550
  private readonly middlewareAbortController?: AbortController
@@ -701,6 +704,21 @@ class TextEngine<
701
704
  capability[0](this.middlewareCtx, { optional: true }),
702
705
  provide: (capability, value) => capability[1](this.middlewareCtx, value),
703
706
  }
707
+
708
+ // Provide the internal SandboxRuntime capability so harness adapters and
709
+ // sandbox middleware can emit file events. The sink logs, fans the event
710
+ // out through the middleware `onFile*` hooks (fire-and-forget), and queues
711
+ // a `sandbox.file` custom chunk to be drained into the public stream.
712
+ provideSandboxRuntime(this.middlewareCtx, {
713
+ logger: this.logger,
714
+ emit: (event: SandboxFileEvent) => {
715
+ this.logger.sandbox(`file ${event.type} ${event.path}`, { event })
716
+ void this.middlewareRunner.runSandboxFile(this.middlewareCtx, event)
717
+ this.sandboxFileQueue.push(
718
+ this.createCustomEventChunk('sandbox.file', { ...event }),
719
+ )
720
+ },
721
+ })
704
722
  }
705
723
 
706
724
  /** Get the accumulated content after the chat loop completes */
@@ -1021,6 +1039,10 @@ class TextEngine<
1021
1039
  threadId: this.threadId,
1022
1040
  runId: this.runIdOverride,
1023
1041
  parentRunId: this.parentRunIdOverride,
1042
+ // Expose provided capabilities (e.g. sandbox) to harness adapters.
1043
+ capabilities: this.middlewareCtx,
1044
+ // Client approval decisions, for harness interactive-approval resolution.
1045
+ approvals: this.initialApprovals,
1024
1046
  ...(combinedSchema ? { outputSchema: combinedSchema } : {}),
1025
1047
  })) {
1026
1048
  if (this.isCancelled()) {
@@ -1105,10 +1127,16 @@ class TextEngine<
1105
1127
  await this.middlewareRunner.runOnUsage(this.middlewareCtx, chunk.usage)
1106
1128
  }
1107
1129
 
1130
+ // Drain any sandbox.file events emitted while processing this chunk.
1131
+ yield* this.drainSandboxFileQueue()
1132
+
1108
1133
  if (this.earlyTermination) {
1109
1134
  break
1110
1135
  }
1111
1136
  }
1137
+
1138
+ // Drain any remaining sandbox.file events emitted after the stream ended.
1139
+ yield* this.drainSandboxFileQueue()
1112
1140
  }
1113
1141
 
1114
1142
  private handleStreamChunk(chunk: StreamChunk): void {
@@ -2502,6 +2530,17 @@ class TextEngine<
2502
2530
  }
2503
2531
  }
2504
2532
 
2533
+ /**
2534
+ * Drain queued `sandbox.file` chunks (emitted via the SandboxRuntime sink)
2535
+ * through the middleware pipeline and into the public stream.
2536
+ */
2537
+ private async *drainSandboxFileQueue(): AsyncGenerator<StreamChunk> {
2538
+ while (this.sandboxFileQueue.length > 0) {
2539
+ const chunk = this.sandboxFileQueue.shift()
2540
+ if (chunk) yield* this.pipeThroughMiddleware(chunk)
2541
+ }
2542
+ }
2543
+
2505
2544
  /**
2506
2545
  * Drain an executeToolCalls async generator, yielding any CustomEvent chunks
2507
2546
  * through the middleware pipeline and returning the final ExecuteToolCallsResult.
@@ -11,6 +11,7 @@ import type {
11
11
  ErrorInfo,
12
12
  FinishInfo,
13
13
  IterationInfo,
14
+ SandboxFileEvent,
14
15
  StructuredOutputMiddlewareConfig,
15
16
  ToolCallHookContext,
16
17
  ToolPhaseCompleteInfo,
@@ -344,6 +345,39 @@ export class MiddlewareRunner<TContext = unknown> {
344
345
  return chunks
345
346
  }
346
347
 
348
+ /**
349
+ * Dispatch a sandbox file event to every middleware's `sandbox` hooks, in
350
+ * array order: the catch-all `onFile` then the type-specific hook. Errors are
351
+ * logged and swallowed so one bad hook can't break the run.
352
+ */
353
+ async runSandboxFile(
354
+ ctx: ChatMiddlewareContext<TContext>,
355
+ event: SandboxFileEvent,
356
+ ): Promise<void> {
357
+ const typed = (
358
+ {
359
+ create: 'onFileCreate',
360
+ change: 'onFileChange',
361
+ delete: 'onFileDelete',
362
+ } as const
363
+ )[event.type]
364
+ for (const mw of this.middlewares) {
365
+ const hooks = mw.sandbox
366
+ if (!hooks) continue
367
+ for (const fn of [hooks.onFile, hooks[typed]]) {
368
+ if (!fn) continue
369
+ try {
370
+ await fn(ctx, event)
371
+ } catch (error) {
372
+ this.logger.sandbox(
373
+ `hook=${typed} middleware=${mw.name ?? 'unnamed'} threw`,
374
+ { middleware: mw.name ?? 'unnamed', error },
375
+ )
376
+ }
377
+ }
378
+ }
379
+ }
380
+
347
381
  /**
348
382
  * Run onBeforeToolCall through middleware in order.
349
383
  * Returns the first non-void decision, or undefined to continue normally.
@@ -13,6 +13,8 @@ export type {
13
13
  FinishInfo,
14
14
  AbortInfo,
15
15
  ErrorInfo,
16
+ SandboxFileEvent,
17
+ ChatSandboxHooks,
16
18
  } from './types'
17
19
 
18
20
  export { MiddlewareRunner } from './compose'
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Internal runtime seam the chat engine PROVIDES so the sandbox middleware can
3
+ * surface file events without a public ctx method. `emit` runs every
4
+ * middleware's `sandbox` hooks AND emits a CUSTOM `sandbox.file` chunk into the
5
+ * stream; `logger` lets the sandbox layer log under the `sandbox` debug
6
+ * category. Consumed (optionally) by `withSandbox` in `@tanstack/ai-sandbox`.
7
+ */
8
+ import { createCapability } from './capabilities'
9
+ import type { InternalLogger } from '../../../logger/internal-logger'
10
+ import type { SandboxFileEvent } from './types'
11
+
12
+ export interface SandboxRuntime {
13
+ emit: (event: SandboxFileEvent) => void
14
+ logger: InternalLogger
15
+ }
16
+
17
+ export const SandboxRuntimeCapability =
18
+ createCapability<SandboxRuntime>()('sandbox-runtime')
19
+
20
+ export const [getSandboxRuntime, provideSandboxRuntime] =
21
+ SandboxRuntimeCapability
@@ -13,6 +13,37 @@ import type {
13
13
  CapabilityRegistry,
14
14
  } from './capabilities'
15
15
 
16
+ /** A file change observed inside a sandbox during a chat run. */
17
+ export interface SandboxFileEvent {
18
+ type: 'create' | 'change' | 'delete'
19
+ /** Absolute path inside the sandbox (under the workspace root). */
20
+ path: string
21
+ timestamp: number
22
+ }
23
+
24
+ /**
25
+ * Sandbox file-event hooks a chat middleware can declare. Fire server-side for
26
+ * every file create/change/delete observed in the sandbox during the run.
27
+ */
28
+ export interface ChatSandboxHooks<TContext = unknown> {
29
+ onFile?: (
30
+ ctx: ChatMiddlewareContext<TContext>,
31
+ e: SandboxFileEvent,
32
+ ) => void | Promise<void>
33
+ onFileCreate?: (
34
+ ctx: ChatMiddlewareContext<TContext>,
35
+ e: SandboxFileEvent,
36
+ ) => void | Promise<void>
37
+ onFileChange?: (
38
+ ctx: ChatMiddlewareContext<TContext>,
39
+ e: SandboxFileEvent,
40
+ ) => void | Promise<void>
41
+ onFileDelete?: (
42
+ ctx: ChatMiddlewareContext<TContext>,
43
+ e: SandboxFileEvent,
44
+ ) => void | Promise<void>
45
+ }
46
+
16
47
  // ===========================
17
48
  // Middleware Context
18
49
  // ===========================
@@ -539,6 +570,12 @@ export interface ChatMiddleware<TContext = unknown> {
539
570
  ctx: ChatMiddlewareContext<TContext>,
540
571
  info: ErrorInfo,
541
572
  ) => void | Promise<void>
573
+
574
+ /**
575
+ * Sandbox file-event hooks. Fire when a sandbox provided by `withSandbox` is
576
+ * active during the run and a file is created/changed/deleted. Server-side.
577
+ */
578
+ sandbox?: ChatSandboxHooks<TContext>
542
579
  }
543
580
 
544
581
  /** A `ChatMiddleware` with a permissive context — for use as a constraint. */
@@ -10,3 +10,9 @@ export {
10
10
  toRunErrorPayload,
11
11
  toRunErrorRawEvent,
12
12
  } from './activities/error-payload'
13
+ export {
14
+ getSandboxRuntime,
15
+ provideSandboxRuntime,
16
+ SandboxRuntimeCapability,
17
+ } from './activities/chat/middleware/sandbox-runtime'
18
+ export type { SandboxRuntime } from './activities/chat/middleware/sandbox-runtime'
package/src/index.ts CHANGED
@@ -120,6 +120,8 @@ export type {
120
120
  FinishInfo,
121
121
  AbortInfo,
122
122
  ErrorInfo,
123
+ SandboxFileEvent,
124
+ ChatSandboxHooks,
123
125
  } from './activities/chat/middleware/index'
124
126
 
125
127
  // Base, activity-agnostic middleware. The observe-only superset that media
@@ -149,6 +151,8 @@ export type {
149
151
  CapabilityContext,
150
152
  CapabilityGetter,
151
153
  CapabilityProvider,
154
+ DefinedChatMiddleware,
155
+ AnyChatMiddleware,
152
156
  } from './activities/chat/middleware/index'
153
157
 
154
158
  // All types
@@ -31,6 +31,7 @@ const CATEGORY_EMOJI: Record<keyof ResolvedCategories, string> = {
31
31
  agentLoop: '🔁',
32
32
  config: '⚙️',
33
33
  errors: '❌',
34
+ sandbox: '📦',
34
35
  }
35
36
 
36
37
  export class InternalLogger {
@@ -82,6 +83,11 @@ export class InternalLogger {
82
83
  this.emit('debug', 'tools', message, meta)
83
84
  }
84
85
 
86
+ /** Log sandbox internals (watcher, file events, hook dispatch). Chat-only. */
87
+ sandbox(message: string, meta?: Record<string, unknown>): void {
88
+ this.emit('debug', 'sandbox', message, meta)
89
+ }
90
+
85
91
  /** Log an agent-loop iteration marker or phase transition. Chat-only. */
86
92
  agentLoop(message: string, meta?: Record<string, unknown>): void {
87
93
  this.emit('debug', 'agentLoop', message, meta)
@@ -12,6 +12,7 @@ const ALL_OFF: ResolvedCategories = {
12
12
  config: false,
13
13
  errors: false,
14
14
  request: false,
15
+ sandbox: false,
15
16
  }
16
17
 
17
18
  const ALL_ON: ResolvedCategories = {
@@ -23,6 +24,7 @@ const ALL_ON: ResolvedCategories = {
23
24
  config: true,
24
25
  errors: true,
25
26
  request: true,
27
+ sandbox: true,
26
28
  }
27
29
 
28
30
  const errorsOnlyCategories = (): ResolvedCategories => ({
@@ -41,6 +43,7 @@ const resolveCategoriesFromPartial = (
41
43
  config: partial.config ?? true,
42
44
  errors: partial.errors ?? true,
43
45
  request: partial.request ?? true,
46
+ sandbox: partial.sandbox ?? true,
44
47
  })
45
48
 
46
49
  /**
@@ -60,6 +60,11 @@ export interface DebugCategories {
60
60
  * Outgoing call metadata (provider, model, message/tool counts) emitted before each adapter SDK call.
61
61
  */
62
62
  request?: boolean
63
+ /**
64
+ * Sandbox internals: watcher start/stop + mechanism, file events, sandbox
65
+ * hook dispatch, ensure/bootstrap and lifecycle transitions. Chat-only.
66
+ */
67
+ sandbox?: boolean
63
68
  }
64
69
 
65
70
  /**
package/src/types.ts CHANGED
@@ -4,6 +4,7 @@ import type {
4
4
  } from '@standard-schema/spec'
5
5
  import type { InternalLogger } from './logger/internal-logger'
6
6
  import type { SystemPrompt } from './system-prompts'
7
+ import type { CapabilityContext } from './activities/chat/middleware/capabilities'
7
8
  // The canonical usage types live in the leaf `@tanstack/ai-event-client`
8
9
  // package (which `@tanstack/ai` already depends on) so there is a single source
9
10
  // of truth without a dependency cycle. They are re-exported below.
@@ -946,6 +947,25 @@ export interface TextOptions<
946
947
  * Surfaced for observability/middleware; not consumed by the LLM call.
947
948
  */
948
949
  parentRunId?: string
950
+
951
+ /**
952
+ * Middleware capability context for this run. The engine populates it with
953
+ * the live middleware context so harness adapters that declare
954
+ * `requires: [SomeCapability]` can read provided capabilities from inside
955
+ * `chatStream` — e.g. `getSandbox(options.capabilities)`. Capabilities are
956
+ * provisioned by middleware `setup` before the adapter runs. Undefined for
957
+ * direct adapter usage outside the chat engine.
958
+ */
959
+ capabilities?: CapabilityContext
960
+
961
+ /**
962
+ * Client approval decisions for this run, keyed by approval id. The engine
963
+ * populates this from approvals carried on the incoming messages. Harness
964
+ * adapters consult it to resolve `ask`-policy permission requests (the agent
965
+ * pauses on a risky action; the client re-runs with a decision recorded
966
+ * here). Undefined for direct adapter usage outside the chat engine.
967
+ */
968
+ approvals?: ReadonlyMap<string, boolean>
949
969
  }
950
970
 
951
971
  // ============================================================================