@tanstack/ai 0.19.1 → 0.20.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/esm/activities/chat/adapter.d.ts +9 -3
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/index.d.ts +11 -2
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +3 -2
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/middlewares/otel.js +14 -2
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/system-prompts.d.ts +66 -0
- package/dist/esm/system-prompts.js +23 -0
- package/dist/esm/system-prompts.js.map +1 -0
- package/dist/esm/types.d.ts +16 -1
- package/package.json +2 -2
- package/src/activities/chat/adapter.ts +11 -2
- package/src/activities/chat/index.ts +15 -4
- package/src/activities/chat/middleware/types.ts +3 -2
- package/src/index.ts +4 -0
- package/src/middlewares/otel.ts +22 -2
- package/src/system-prompts.ts +98 -0
- package/src/types.ts +16 -1
package/src/middlewares/otel.ts
CHANGED
|
@@ -372,9 +372,29 @@ export function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {
|
|
|
372
372
|
state.assistantTextBufferTruncated = false
|
|
373
373
|
|
|
374
374
|
if (captureContent) {
|
|
375
|
+
const systemPromptContents = config.systemPrompts.map((p) =>
|
|
376
|
+
typeof p === 'string' ? p : p.content,
|
|
377
|
+
)
|
|
378
|
+
// Anthropic prompt-caching users need to know which prompt carried
|
|
379
|
+
// `cache_control`: it's the one attribute that explains cache
|
|
380
|
+
// hit/miss in observability. Serialise per-prompt metadata as a
|
|
381
|
+
// single JSON span attribute so backends that don't understand
|
|
382
|
+
// GenAI events can still surface it. Kept off span events to
|
|
383
|
+
// avoid breaking the one-event-per-message GenAI semconv contract.
|
|
384
|
+
const systemPromptMetadata = config.systemPrompts.map((p) =>
|
|
385
|
+
typeof p === 'string' || p.metadata === undefined
|
|
386
|
+
? null
|
|
387
|
+
: p.metadata,
|
|
388
|
+
)
|
|
389
|
+
if (systemPromptMetadata.some((m) => m !== null)) {
|
|
390
|
+
iterSpan.setAttribute(
|
|
391
|
+
'tanstack.ai.system_prompt.metadata',
|
|
392
|
+
JSON.stringify(systemPromptMetadata),
|
|
393
|
+
)
|
|
394
|
+
}
|
|
375
395
|
// Span events follow the original GenAI semconv (one event per
|
|
376
396
|
// message). Backends that read events get content this way.
|
|
377
|
-
for (const sys of
|
|
397
|
+
for (const sys of systemPromptContents) {
|
|
378
398
|
iterSpan.addEvent('gen_ai.system.message', {
|
|
379
399
|
content: redactContent(sys),
|
|
380
400
|
})
|
|
@@ -391,7 +411,7 @@ export function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {
|
|
|
391
411
|
// (`gen_ai.input.messages`) — backends like PostHog read prompt
|
|
392
412
|
// content from this attribute, not from span events.
|
|
393
413
|
const inputMessages: Array<{ role: string; content: string }> = []
|
|
394
|
-
for (const sys of
|
|
414
|
+
for (const sys of systemPromptContents) {
|
|
395
415
|
inputMessages.push({
|
|
396
416
|
role: 'system',
|
|
397
417
|
content: redactContent(sys),
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single entry in `chat({ systemPrompts: [...] })`.
|
|
3
|
+
*
|
|
4
|
+
* Accepts a plain string (the common case) or a structured object that lets
|
|
5
|
+
* providers attach typed metadata to the prompt — e.g. Anthropic
|
|
6
|
+
* `cache_control` for prompt caching, future per-prompt safety overrides for
|
|
7
|
+
* Gemini, etc.
|
|
8
|
+
*
|
|
9
|
+
* At the chat call site, `metadata` is narrowed by the adapter via
|
|
10
|
+
* `~types['systemPromptMetadata']`. Providers that don't declare one inherit
|
|
11
|
+
* the default `never`, which makes the field carry no meaningful value: TS
|
|
12
|
+
* only accepts `undefined` there, and provider-foreign metadata that reaches
|
|
13
|
+
* an adapter via JS / `as any` is silently dropped, never written to the
|
|
14
|
+
* wire. For type-safe per-provider metadata, refer to the provider's
|
|
15
|
+
* `<Provider>SystemPromptMetadata` interface (e.g. `AnthropicSystemPromptMetadata`).
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // The 90% case — plain strings work everywhere.
|
|
19
|
+
* systemPrompts: ['Be concise.', 'Cite sources.']
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // Provider-specific metadata via the object form. No `satisfies` cast
|
|
23
|
+
* // is needed — the adapter narrows the `metadata` field's type at the
|
|
24
|
+
* // call site so users get autocomplete and structural checking
|
|
25
|
+
* // automatically.
|
|
26
|
+
* import { anthropicText } from '@tanstack/ai-anthropic'
|
|
27
|
+
*
|
|
28
|
+
* chat({
|
|
29
|
+
* adapter: anthropicText(),
|
|
30
|
+
* systemPrompts: [
|
|
31
|
+
* {
|
|
32
|
+
* content: 'Stable instructions — cache me.',
|
|
33
|
+
* metadata: { cache_control: { type: 'ephemeral' } },
|
|
34
|
+
* },
|
|
35
|
+
* 'Volatile per-request instruction.',
|
|
36
|
+
* ],
|
|
37
|
+
* })
|
|
38
|
+
*/
|
|
39
|
+
export type SystemPrompt<TMetadata = unknown> =
|
|
40
|
+
| string
|
|
41
|
+
| {
|
|
42
|
+
content: string
|
|
43
|
+
metadata?: TMetadata
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Normalised shape adapters see after the chat layer turns string entries
|
|
48
|
+
* into `{ content }` objects. Adapters call `normalizeSystemPrompts` once at
|
|
49
|
+
* the top of their option-mapping pipeline so the rest of the code only has
|
|
50
|
+
* to handle one shape.
|
|
51
|
+
*/
|
|
52
|
+
export interface NormalizedSystemPrompt<TMetadata = unknown> {
|
|
53
|
+
content: string
|
|
54
|
+
metadata?: TMetadata
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Normalise the public `systemPrompts` shape (`Array<string | { content, metadata? }>`)
|
|
59
|
+
* to a homogenous `Array<{ content, metadata? }>`. Adapters use this so they
|
|
60
|
+
* don't have to type-narrow string vs object inline.
|
|
61
|
+
*
|
|
62
|
+
* Returns an empty array (never `undefined`) so callers can chain `.map` /
|
|
63
|
+
* `.join` without an extra null check.
|
|
64
|
+
*
|
|
65
|
+
* Throws a `TypeError` (naming the offending index) if an object-form entry's
|
|
66
|
+
* `content` isn't a string. Public API boundary — callers reaching this
|
|
67
|
+
* function through `as any` / external JS would otherwise stream a literal
|
|
68
|
+
* `"undefined"` into the model's system prompt with no signal.
|
|
69
|
+
*/
|
|
70
|
+
export function normalizeSystemPrompts<TMetadata = unknown>(
|
|
71
|
+
// Accept the wide public shape (`SystemPrompt<unknown>`) regardless of the
|
|
72
|
+
// caller's `TMetadata`. Adapters know their own metadata shape; the
|
|
73
|
+
// generic narrows the *output* so adapter code can read `p.metadata.X`
|
|
74
|
+
// without an additional cast.
|
|
75
|
+
prompts: ReadonlyArray<SystemPrompt> | undefined,
|
|
76
|
+
): Array<NormalizedSystemPrompt<TMetadata>> {
|
|
77
|
+
if (!prompts || prompts.length === 0) return []
|
|
78
|
+
return prompts.map((p, i) => {
|
|
79
|
+
if (typeof p === 'string') return { content: p }
|
|
80
|
+
// Defence in depth: TypeScript narrows `p` to the object arm here, but
|
|
81
|
+
// this function is a public API boundary that callers can reach via
|
|
82
|
+
// plain JS or `as any`. Re-validate at runtime so we never stream a
|
|
83
|
+
// literal `"undefined"` into the model.
|
|
84
|
+
const candidate = p as unknown
|
|
85
|
+
if (candidate === null || typeof candidate !== 'object') {
|
|
86
|
+
throw new TypeError(
|
|
87
|
+
`systemPrompts[${i}]: expected a string or { content, metadata? }, got ${candidate === null ? 'null' : typeof candidate}`,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
const { content } = candidate as { content?: unknown }
|
|
91
|
+
if (typeof content !== 'string') {
|
|
92
|
+
throw new TypeError(
|
|
93
|
+
`systemPrompts[${i}]: content must be a string, got ${typeof content}`,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
return p as NormalizedSystemPrompt<TMetadata>
|
|
97
|
+
})
|
|
98
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
StandardSchemaV1,
|
|
4
4
|
} from '@standard-schema/spec'
|
|
5
5
|
import type { InternalLogger } from './logger/internal-logger'
|
|
6
|
+
import type { SystemPrompt } from './system-prompts'
|
|
6
7
|
import type {
|
|
7
8
|
BaseEvent as AGUIBaseEvent,
|
|
8
9
|
CustomEvent as AGUICustomEvent,
|
|
@@ -729,7 +730,21 @@ export interface TextOptions<
|
|
|
729
730
|
model: string
|
|
730
731
|
messages: Array<ModelMessage>
|
|
731
732
|
tools?: Array<Tool<any, any, any>>
|
|
732
|
-
|
|
733
|
+
/**
|
|
734
|
+
* System prompts to include with the request.
|
|
735
|
+
*
|
|
736
|
+
* Accepts plain strings (the common case) or `{ content, metadata }`
|
|
737
|
+
* objects that let providers attach typed metadata (e.g. Anthropic
|
|
738
|
+
* `cache_control` for prompt caching) per prompt. At the chat call site
|
|
739
|
+
* the adapter narrows `metadata`'s type via `~types['systemPromptMetadata']`
|
|
740
|
+
* — providers that don't declare one default to `never`, which makes the
|
|
741
|
+
* field carry no meaningful value (TypeScript will only accept
|
|
742
|
+
* `undefined` there). Provider-foreign metadata that reaches an adapter
|
|
743
|
+
* via JS / `as any` is silently dropped, never written to the wire.
|
|
744
|
+
*
|
|
745
|
+
* @see SystemPrompt
|
|
746
|
+
*/
|
|
747
|
+
systemPrompts?: Array<SystemPrompt>
|
|
733
748
|
agentLoopStrategy?: AgentLoopStrategy
|
|
734
749
|
/**
|
|
735
750
|
* Controls the randomness of the output.
|