@strav/brain 1.0.0-alpha.17 → 1.0.0-alpha.19

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/src/thread.ts CHANGED
@@ -35,6 +35,14 @@ export interface ThreadState {
35
35
  messages: Message[]
36
36
  system?: SystemPrompt
37
37
  options?: ChatOptions
38
+ /**
39
+ * Last provider response id captured by `send(...)` — restored on
40
+ * `fromJSON` so subsequent sends thread it via
41
+ * `ChatOptions.previousResponseId` automatically. Only ever set
42
+ * when the underlying provider surfaces `responseId` (OpenAI
43
+ * Responses API today).
44
+ */
45
+ lastResponseId?: string
38
46
  }
39
47
 
40
48
  export class Thread {
@@ -42,6 +50,13 @@ export class Thread {
42
50
  readonly messages: Message[] = []
43
51
  readonly system?: SystemPrompt
44
52
  readonly options?: ChatOptions
53
+ /**
54
+ * Last response id returned by the provider on this thread. Used to
55
+ * thread stateful-conversation hints (OpenAI Responses API) into
56
+ * the next `send(...)` so apps don't have to manage it manually.
57
+ * `undefined` for providers that don't surface a response id.
58
+ */
59
+ lastResponseId?: string
45
60
  private readonly brain: BrainManager
46
61
 
47
62
  constructor(brain: BrainManager, opts: ThreadOptions = {}) {
@@ -54,6 +69,11 @@ export class Thread {
54
69
  * Append a user turn, call the model, append the assistant reply,
55
70
  * and return the reply text. Per-call options override the
56
71
  * thread's defaults; `system` always comes from the thread.
72
+ *
73
+ * When the underlying provider supports stateful conversations
74
+ * (OpenAI Responses API), `previousResponseId` is auto-threaded
75
+ * from the prior turn — apps don't need to manage it. Per-call
76
+ * `options.previousResponseId` wins if supplied explicitly.
57
77
  */
58
78
  async send(text: string, options: ChatOptions = {}): Promise<string> {
59
79
  this.messages.push({ role: 'user', content: text })
@@ -65,8 +85,25 @@ export class Thread {
65
85
  // mid-thread by changing the system prompt every turn.
66
86
  ...(this.system !== undefined ? { system: this.system } : {}),
67
87
  }
88
+ if (
89
+ merged.previousResponseId === undefined &&
90
+ this.lastResponseId !== undefined
91
+ ) {
92
+ merged.previousResponseId = this.lastResponseId
93
+ }
68
94
  const result = await this.brain.chat(this.messages, merged)
69
- this.messages.push({ role: 'assistant', content: result.text })
95
+ // Preserve structured assistant content when present (compaction
96
+ // blocks today; reasoning blocks later). Round-tripping these
97
+ // back to the provider on subsequent sends is what makes
98
+ // server-side compaction actually save tokens — once a turn
99
+ // carries a `compaction` block, the older raw turns drop out
100
+ // and the model only re-reads the summary.
101
+ if (result.content !== undefined && result.content.length > 0) {
102
+ this.messages.push({ role: 'assistant', content: result.content })
103
+ } else {
104
+ this.messages.push({ role: 'assistant', content: result.text })
105
+ }
106
+ if (result.responseId !== undefined) this.lastResponseId = result.responseId
70
107
  return result.text
71
108
  }
72
109
 
@@ -80,6 +117,7 @@ export class Thread {
80
117
  const state: ThreadState = { messages: [...this.messages] }
81
118
  if (this.system !== undefined) state.system = this.system
82
119
  if (this.options !== undefined) state.options = this.options
120
+ if (this.lastResponseId !== undefined) state.lastResponseId = this.lastResponseId
83
121
  return state
84
122
  }
85
123
 
@@ -94,6 +132,7 @@ export class Thread {
94
132
  if (state.options !== undefined) options.options = state.options
95
133
  const thread = new Thread(brain, options)
96
134
  for (const m of state.messages) thread.messages.push(m)
135
+ if (state.lastResponseId !== undefined) thread.lastResponseId = state.lastResponseId
97
136
  return thread
98
137
  }
99
138
  }
package/src/types.ts CHANGED
@@ -173,6 +173,35 @@ export interface AudioBlock {
173
173
  | { type: 'url'; url: string }
174
174
  }
175
175
 
176
+ /**
177
+ * Server-side compaction block. Anthropic's `compact-2026-01-12`
178
+ * beta returns a `compaction` block when an auto-compaction trigger
179
+ * fires during a request. The framework surfaces it on
180
+ * `result.content` and Thread persists it on the assistant turn so
181
+ * subsequent requests echo it back verbatim — the model only sees
182
+ * the summary + opaque blob from then on, and the older raw turns
183
+ * stay out of context.
184
+ *
185
+ * V1 produces these on Anthropic only. Other providers ignore the
186
+ * `compact` option silently, and never emit a `CompactionBlock`.
187
+ *
188
+ * Round-trip invariant: pass the block back unchanged. The
189
+ * `encryptedContent` blob is opaque metadata the server uses to
190
+ * stitch the compaction history together; the framework never
191
+ * mutates it.
192
+ *
193
+ * `content === null` means a compaction attempt failed (e.g.,
194
+ * malformed model output). The server treats these as no-ops on
195
+ * the next request, so apps don't need to special-case them.
196
+ */
197
+ export interface CompactionBlock {
198
+ type: 'compaction'
199
+ /** Summary of compacted content. Null when compaction failed. */
200
+ content: string | null
201
+ /** Opaque metadata round-tripped verbatim on subsequent requests. */
202
+ encryptedContent: string | null
203
+ }
204
+
176
205
  export type ContentBlock =
177
206
  | TextBlock
178
207
  | ImageBlock
@@ -182,6 +211,7 @@ export type ContentBlock =
182
211
  | ToolResultBlock
183
212
  | MCPToolUseBlock
184
213
  | MCPToolResultBlock
214
+ | CompactionBlock
185
215
 
186
216
  /** A single conversation turn. `content` can be a bare string or a typed block list. */
187
217
  export interface Message {
@@ -254,6 +284,36 @@ export type ServerTool =
254
284
  /** Gemini fetches the URL and surfaces grounded answers from it. */
255
285
  }
256
286
 
287
+ /**
288
+ * Per-call compaction configuration. Maps to Anthropic's
289
+ * `compact-2026-01-12` beta `edits[]` entry. All fields optional —
290
+ * omitting one falls back to the server's default (trigger:
291
+ * 150,000 input tokens; no extra instructions; no pause).
292
+ */
293
+ export interface CompactConfig {
294
+ /**
295
+ * Trigger threshold in input tokens. Compaction fires once the
296
+ * conversation crosses this token count. Default 150,000 — same
297
+ * as the server-side default.
298
+ */
299
+ trigger?: number
300
+ /**
301
+ * Extra hint to the summarization model. Useful for biasing the
302
+ * compaction toward what your app actually cares to preserve
303
+ * ("keep all customer ids referenced", "preserve every diff
304
+ * hunk", ...).
305
+ */
306
+ instructions?: string
307
+ /**
308
+ * When `true`, the server returns the compaction block in-line
309
+ * but does NOT continue generation — the next assistant turn
310
+ * waits for an explicit re-prompt. Apps that want to inspect or
311
+ * gate compaction set this; default `false` (compaction is
312
+ * transparent).
313
+ */
314
+ pauseAfterCompaction?: boolean
315
+ }
316
+
257
317
  export interface ChatOptions {
258
318
  /** Override the configured default model. Wins over `tier`. */
259
319
  model?: string
@@ -308,6 +368,36 @@ export interface ChatOptions {
308
368
  * route to Anthropic / Gemini).
309
369
  */
310
370
  serverTools?: readonly ServerTool[]
371
+ /**
372
+ * Server-side conversation compaction. When set, the provider
373
+ * auto-summarizes the older part of the message history once the
374
+ * `trigger` token threshold is reached; the summary lives on the
375
+ * response as a `CompactionBlock` that apps round-trip on
376
+ * subsequent requests (Thread does this automatically). Saves
377
+ * tokens on long threads without lossy client-side pruning.
378
+ *
379
+ * Only honored by `AnthropicProvider` (driver `'anthropic'`),
380
+ * via the `compact-2026-01-12` beta. Silently ignored by every
381
+ * other provider so apps targeting multiple providers with the
382
+ * same options object don't have to special-case.
383
+ */
384
+ compact?: CompactConfig
385
+ /**
386
+ * Stateful conversation pointer — OpenAI Responses API. When set,
387
+ * the provider sends only the new turn(s); the server picks up
388
+ * from the prior `Response` identified by this id and replays
389
+ * the conversation server-side. Saves tokens on long threads.
390
+ *
391
+ * Only honored by `OpenAIResponsesProvider` (driver
392
+ * `'openai-responses'`); silently ignored by every other provider
393
+ * — apps that target multiple providers with the same options
394
+ * object don't have to special-case.
395
+ *
396
+ * Pair with `ChatResult.responseId` (returned by every call) to
397
+ * thread the conversation forward. `Thread` does this
398
+ * automatically when its underlying provider supports it.
399
+ */
400
+ previousResponseId?: string
311
401
  }
312
402
 
313
403
  /** Token usage for a single call. Cache-hit fields are populated when caching is in play. */
@@ -330,6 +420,24 @@ export interface ChatResult<Raw = unknown> {
330
420
  stopReason: string | null
331
421
  usage: ChatUsage
332
422
  raw: Raw
423
+ /**
424
+ * Structured assistant content blocks — populated when the model
425
+ * emitted more than plain text on this turn (compaction blocks
426
+ * today; reasoning blocks once those surface). Apps that
427
+ * persist the conversation (`Thread`, custom stores) push this
428
+ * onto the message history when present so round-trippable
429
+ * blocks survive subsequent requests. Undefined when the turn
430
+ * was plain text only.
431
+ */
432
+ content?: ContentBlock[]
433
+ /**
434
+ * Provider response id when the provider exposes stateful
435
+ * conversations (currently OpenAI Responses API). Apps thread
436
+ * this forward via `ChatOptions.previousResponseId` so the
437
+ * server replays prior turns without re-sending them.
438
+ * Undefined for providers that don't support the pattern.
439
+ */
440
+ responseId?: string
333
441
  }
334
442
 
335
443
  /**
@@ -447,4 +555,6 @@ export interface GenerateResult<T = unknown, Raw = unknown> {
447
555
  stopReason: string | null
448
556
  usage: ChatUsage
449
557
  raw: Raw
558
+ /** See `ChatResult.responseId`. */
559
+ responseId?: string
450
560
  }