@tanstack/ai 0.38.0 → 0.39.1

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 (46) hide show
  1. package/dist/esm/activities/chat/index.js +32 -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/activities/chat/stream/processor.d.ts +18 -0
  12. package/dist/esm/activities/chat/stream/processor.js +86 -4
  13. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  14. package/dist/esm/adapter-internals.d.ts +2 -0
  15. package/dist/esm/adapter-internals.js +4 -0
  16. package/dist/esm/adapter-internals.js.map +1 -1
  17. package/dist/esm/index.d.ts +3 -2
  18. package/dist/esm/index.js +3 -0
  19. package/dist/esm/index.js.map +1 -1
  20. package/dist/esm/logger/internal-logger.d.ts +2 -0
  21. package/dist/esm/logger/internal-logger.js +6 -1
  22. package/dist/esm/logger/internal-logger.js.map +1 -1
  23. package/dist/esm/logger/resolve.js +6 -3
  24. package/dist/esm/logger/resolve.js.map +1 -1
  25. package/dist/esm/logger/types.d.ts +5 -0
  26. package/dist/esm/types.d.ts +40 -1
  27. package/dist/esm/utilities/provider-executed.d.ts +18 -0
  28. package/dist/esm/utilities/provider-executed.js +15 -0
  29. package/dist/esm/utilities/provider-executed.js.map +1 -0
  30. package/package.json +1 -1
  31. package/skills/ai-core/adapter-configuration/SKILL.md +25 -12
  32. package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +43 -12
  33. package/skills/ai-core/media-generation/SKILL.md +1 -1
  34. package/src/activities/chat/index.ts +47 -0
  35. package/src/activities/chat/middleware/compose.ts +34 -0
  36. package/src/activities/chat/middleware/index.ts +2 -0
  37. package/src/activities/chat/middleware/sandbox-runtime.ts +21 -0
  38. package/src/activities/chat/middleware/types.ts +37 -0
  39. package/src/activities/chat/stream/processor.ts +141 -3
  40. package/src/adapter-internals.ts +6 -0
  41. package/src/index.ts +9 -0
  42. package/src/logger/internal-logger.ts +6 -0
  43. package/src/logger/resolve.ts +3 -0
  44. package/src/logger/types.ts +5 -0
  45. package/src/types.ts +43 -1
  46. package/src/utilities/provider-executed.ts +32 -0
@@ -23,6 +23,7 @@ import {
23
23
  uiMessageToModelMessages,
24
24
  } from '../messages.js'
25
25
  import { normalizeToolResult } from '../../../utilities/tool-result'
26
+ import { isProviderExecutedToolCall } from '../../../utilities/provider-executed'
26
27
  import { defaultJSONParser } from './json-parser'
27
28
  import {
28
29
  appendStructuredOutputDelta,
@@ -403,12 +404,15 @@ export class StreamProcessor {
403
404
  // 1. It was approved/denied (approval-responded state)
404
405
  // 2. It has an output field set (client tool completed via addToolResult)
405
406
  // 3. It has a corresponding tool-result part (server tool completed)
407
+ // 4. It is provider-executed (e.g. Anthropic web_search) — already run by
408
+ // the provider, so there is no client result to wait for.
406
409
  return toolParts.every(
407
410
  (part) =>
408
411
  part.state === 'complete' ||
409
412
  part.state === 'approval-responded' ||
410
413
  (part.output !== undefined && !part.approval) ||
411
- toolResultIds.has(part.id),
414
+ toolResultIds.has(part.id) ||
415
+ isProviderExecutedToolCall(part),
412
416
  )
413
417
  }
414
418
 
@@ -881,10 +885,140 @@ export class StreamProcessor {
881
885
  // them directly to UIMessage[] is unsafe and causes "Cannot read properties
882
886
  // of undefined (reading 'find')" when code later reads message.parts (e.g.
883
887
  // the onToolCallStateChange devtools handler).
884
- this.messages = chunk.messages.map(aguiSnapshotMessageToUIMessage)
888
+ //
889
+ // The AG-UI `MESSAGES_SNAPSHOT` wire shape cannot reconstruct client-side
890
+ // tool-call metadata a server may omit: a `role: 'tool'` message only carries
891
+ // `toolCallId` + `content`, and an assistant message in the snapshot may
892
+ // drop `toolCalls` the client already observed via `TOOL_CALL_*` events.
893
+ // Without the matching `tool-call` part, later `addToolResult(toolCallId)`
894
+ // calls cannot locate the call and warn + no-op (see #859). To keep the
895
+ // UI representation consistent with the streaming fan-out and preserve the
896
+ // unreconstructable metadata, reconcile the normalized snapshot against
897
+ // the pre-snapshot state; see `reconcileSnapshotToolCalls`.
898
+ const prevMessages = this.messages
899
+ const normalized = chunk.messages.map(aguiSnapshotMessageToUIMessage)
900
+ this.messages = this.reconcileSnapshotToolCalls(normalized, prevMessages)
885
901
  this.emitMessagesChange()
886
902
  }
887
903
 
904
+ /**
905
+ * Reconcile a freshly normalized snapshot with the pre-snapshot message
906
+ * state so unreconstructable tool-call metadata is preserved.
907
+ *
908
+ * Post-pass (a): anchor `tool-result`-only assistant messages (the shape
909
+ * `aguiSnapshotMessageToUIMessage` emits for AG-UI `role: 'tool'` wire
910
+ * messages) into the message containing the matching `tool-call` part, or —
911
+ * when the snapshot supplies no such part — the nearest earlier anchorable
912
+ * assistant message, matching the in-stream fan-out shape
913
+ * `assistant: [text, tool-call, tool-result, ...]`. Detached messages with
914
+ * no earlier anchorable assistant are kept verbatim.
915
+ *
916
+ * Post-pass (b): when a `tool-result` part references a `toolCallId` whose
917
+ * `tool-call` part is absent from the snapshot, carry the `tool-call` part
918
+ * forward from the pre-snapshot state (state and output untouched) so a
919
+ * subsequent `addToolResult(toolCallId)` can still locate the call.
920
+ */
921
+ private reconcileSnapshotToolCalls(
922
+ snapshot: Array<UIMessage>,
923
+ prevMessages: Array<UIMessage>,
924
+ ): Array<UIMessage> {
925
+ // Index tool-call parts observed before the snapshot by id so we can
926
+ // restore metadata the snapshot cannot re-emit. Duplicate ids resolve
927
+ // last-write-wins: the same tool call can appear in multiple messages
928
+ // across reconnects, and the most recent part carries the freshest state.
929
+ const prevToolCalls = new Map<string, ToolCallPart>()
930
+ for (const msg of prevMessages) {
931
+ for (const part of msg.parts) {
932
+ if (part.type === 'tool-call') {
933
+ prevToolCalls.set(part.id, part)
934
+ }
935
+ }
936
+ }
937
+ // Index tool-call parts already present in the snapshot so (b) only fills
938
+ // genuine gaps rather than duplicating a tool-call the snapshot supplies.
939
+ const snapshotToolCallIds = new Set<string>()
940
+ for (const msg of snapshot) {
941
+ for (const part of msg.parts) {
942
+ if (part.type === 'tool-call') {
943
+ snapshotToolCallIds.add(part.id)
944
+ }
945
+ }
946
+ }
947
+
948
+ const reconciled: Array<UIMessage> = []
949
+ for (const msg of snapshot) {
950
+ const toolResultPart =
951
+ msg.role === 'assistant' && msg.parts.length === 1
952
+ ? msg.parts.find((p): p is ToolResultPart => p.type === 'tool-result')
953
+ : undefined
954
+
955
+ if (!toolResultPart) {
956
+ reconciled.push(msg)
957
+ continue
958
+ }
959
+
960
+ // Prefer the message that actually contains the matching tool-call
961
+ // part. AG-UI `reasoning`/`activity` messages also normalize to
962
+ // `role: 'assistant'`, so anchoring into the nearest assistant alone
963
+ // could separate a result from its call (and a later
964
+ // `addToolResult(toolCallId)` would then append a duplicate result
965
+ // next to the call).
966
+ const target =
967
+ reconciled.findLast((m) =>
968
+ m.parts.some(
969
+ (p) => p.type === 'tool-call' && p.id === toolResultPart.toolCallId,
970
+ ),
971
+ ) ??
972
+ reconciled.findLast(
973
+ (m) =>
974
+ m.role === 'assistant' &&
975
+ !(m.parts.length === 1 && m.parts[0]?.type === 'tool-result'),
976
+ )
977
+
978
+ if (!target) {
979
+ // No assistant to anchor into — keep the detached message intact.
980
+ if (!snapshotToolCallIds.has(toolResultPart.toolCallId)) {
981
+ console.warn(
982
+ `[StreamProcessor] MESSAGES_SNAPSHOT contains a tool-result for "${toolResultPart.toolCallId}" but no matching tool-call exists in the snapshot, and there is no assistant message to anchor into; addToolResult("${toolResultPart.toolCallId}") will not be able to locate this call`,
983
+ )
984
+ }
985
+ reconciled.push(msg)
986
+ continue
987
+ }
988
+
989
+ const parts = [...target.parts]
990
+ // (b) Fill in a missing tool-call part from the pre-snapshot state when
991
+ // the snapshot references its id via a tool-result but supplies no
992
+ // tool-call metadata of its own.
993
+ if (
994
+ !snapshotToolCallIds.has(toolResultPart.toolCallId) &&
995
+ !parts.some(
996
+ (p) => p.type === 'tool-call' && p.id === toolResultPart.toolCallId,
997
+ )
998
+ ) {
999
+ const prev = prevToolCalls.get(toolResultPart.toolCallId)
1000
+ if (prev) {
1001
+ // Insert the carried-over tool-call before its tool-result (pushed
1002
+ // below) so call→result ordering matches the streaming fan-out.
1003
+ parts.push({ ...prev })
1004
+ snapshotToolCallIds.add(prev.id)
1005
+ } else {
1006
+ console.warn(
1007
+ `[StreamProcessor] MESSAGES_SNAPSHOT contains a tool-result for "${toolResultPart.toolCallId}" but no matching tool-call exists in the snapshot or the pre-snapshot state; addToolResult("${toolResultPart.toolCallId}") will not be able to locate this call`,
1008
+ )
1009
+ }
1010
+ }
1011
+ parts.push(toolResultPart)
1012
+ // Replace rather than push into `target.parts`: a snapshot message that
1013
+ // arrived already carrying `parts` (TanStack server echoing UIMessages)
1014
+ // shares its array with the incoming chunk, and mutating it in place
1015
+ // would corrupt the caller's event object.
1016
+ target.parts = parts
1017
+ }
1018
+
1019
+ return reconciled
1020
+ }
1021
+
888
1022
  /**
889
1023
  * Handle TEXT_MESSAGE_CONTENT event.
890
1024
  *
@@ -1781,8 +1915,12 @@ export class StreamProcessor {
1781
1915
  * downgrading a failed call back to 'input-complete'.
1782
1916
  */
1783
1917
  private isToolCallPartErrored(toolCallId: string): boolean {
1918
+ // `initialMessages` may be ModelMessage-shaped (no `parts`) — e.g. the
1919
+ // common pattern of seeding a processor with the same messages passed to
1920
+ // `chat()`. Guard the access so iterating them never throws.
1784
1921
  return this.messages.some((msg) =>
1785
- msg.parts.some(
1922
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- `parts` is typed as required, but seeded ModelMessage-shaped messages can lack it at runtime.
1923
+ msg.parts?.some(
1786
1924
  (part) =>
1787
1925
  part.type === 'tool-call' &&
1788
1926
  part.id === toolCallId &&
@@ -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
@@ -252,6 +256,11 @@ export {
252
256
  normalizeToolResult,
253
257
  } from './utilities/tool-result'
254
258
 
259
+ export {
260
+ getProviderExecutedMetadata,
261
+ isProviderExecutedToolCall,
262
+ } from './utilities/provider-executed'
263
+
255
264
  // Adapter extension utilities
256
265
  export { createModel, extendAdapter } from './extend-adapter'
257
266
  export type { ExtendedModelDef, ModelCapabilities } from './extend-adapter'
@@ -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.
@@ -159,6 +160,26 @@ export interface ToolCall<TMetadata = unknown> {
159
160
  metadata?: TMetadata
160
161
  }
161
162
 
163
+ /**
164
+ * Convention for tool-call `metadata` that marks a call as **provider-executed**
165
+ * — run by the provider's own infrastructure (e.g. Anthropic `web_search` /
166
+ * `web_fetch` server tools) rather than by the agent loop. Adapters set
167
+ * `providerExecuted: true` so that:
168
+ *
169
+ * 1. The agent loop never tries to execute the call client-side (see
170
+ * {@link isProviderExecutedToolCall} usage in the chat engine), and
171
+ * 2. The adapter can stash the raw provider result alongside it so the call —
172
+ * and its evidence — round-trips into the next turn's request.
173
+ *
174
+ * Provider-specific payloads live under a namespaced key (e.g. `anthropic`),
175
+ * keeping this convention opaque to the framework core. The index signature
176
+ * preserves those per-adapter fields.
177
+ */
178
+ export interface ProviderExecutedToolMetadata {
179
+ providerExecuted?: boolean
180
+ [key: string]: unknown
181
+ }
182
+
162
183
  // ============================================================================
163
184
  // Multimodal Content Types
164
185
  // ============================================================================
@@ -361,7 +382,9 @@ export interface ToolCallPart<TMetadata = unknown> {
361
382
  /** Tool execution output (for client tools or after approval) */
362
383
  output?: any
363
384
  /** Provider-specific metadata that round-trips with the tool call.
364
- * Typed per-adapter via `TToolCallMetadata`. */
385
+ * Typed per-adapter via `TToolCallMetadata`. May follow the
386
+ * {@link ProviderExecutedToolMetadata} convention to mark provider-executed
387
+ * server tools (e.g. Anthropic `web_search`). */
365
388
  metadata?: TMetadata
366
389
  }
367
390
 
@@ -946,6 +969,25 @@ export interface TextOptions<
946
969
  * Surfaced for observability/middleware; not consumed by the LLM call.
947
970
  */
948
971
  parentRunId?: string
972
+
973
+ /**
974
+ * Middleware capability context for this run. The engine populates it with
975
+ * the live middleware context so harness adapters that declare
976
+ * `requires: [SomeCapability]` can read provided capabilities from inside
977
+ * `chatStream` — e.g. `getSandbox(options.capabilities)`. Capabilities are
978
+ * provisioned by middleware `setup` before the adapter runs. Undefined for
979
+ * direct adapter usage outside the chat engine.
980
+ */
981
+ capabilities?: CapabilityContext
982
+
983
+ /**
984
+ * Client approval decisions for this run, keyed by approval id. The engine
985
+ * populates this from approvals carried on the incoming messages. Harness
986
+ * adapters consult it to resolve `ask`-policy permission requests (the agent
987
+ * pauses on a risky action; the client re-runs with a decision recorded
988
+ * here). Undefined for direct adapter usage outside the chat engine.
989
+ */
990
+ approvals?: ReadonlyMap<string, boolean>
949
991
  }
950
992
 
951
993
  // ============================================================================
@@ -0,0 +1,32 @@
1
+ import type { ProviderExecutedToolMetadata } from '../types'
2
+
3
+ /**
4
+ * Narrow a tool call's opaque `metadata` to the provider-executed convention.
5
+ * Returns the typed metadata when the call is provider-executed, else `null`.
6
+ *
7
+ * @see ProviderExecutedToolMetadata
8
+ */
9
+ export function getProviderExecutedMetadata(
10
+ toolCall: { metadata?: unknown } | null | undefined,
11
+ ): ProviderExecutedToolMetadata | null {
12
+ const metadata = toolCall?.metadata
13
+ if (
14
+ typeof metadata === 'object' &&
15
+ metadata !== null &&
16
+ (metadata as ProviderExecutedToolMetadata).providerExecuted === true
17
+ ) {
18
+ return metadata as ProviderExecutedToolMetadata
19
+ }
20
+ return null
21
+ }
22
+
23
+ /**
24
+ * True when a tool call was executed by the provider (e.g. Anthropic
25
+ * `web_search` / `web_fetch` server tools) rather than the agent loop. Such
26
+ * calls must not be routed to client-side execution and are already "complete".
27
+ */
28
+ export function isProviderExecutedToolCall(
29
+ toolCall: { metadata?: unknown } | null | undefined,
30
+ ): boolean {
31
+ return getProviderExecutedMetadata(toolCall) !== null
32
+ }