@roj-ai/sdk 0.1.34 → 0.1.36
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/bootstrap.d.ts +36 -0
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/core/agents/agent.d.ts +11 -0
- package/dist/core/agents/agent.d.ts.map +1 -1
- package/dist/core/agents/agent.js +26 -5
- package/dist/core/agents/agent.js.map +1 -1
- package/dist/core/agents/state.d.ts +24 -0
- package/dist/core/agents/state.d.ts.map +1 -1
- package/dist/core/agents/state.js.map +1 -1
- package/dist/core/context/state.d.ts.map +1 -1
- package/dist/core/context/state.js +4 -0
- package/dist/core/context/state.js.map +1 -1
- package/dist/core/llm/anthropic.d.ts.map +1 -1
- package/dist/core/llm/anthropic.js +49 -8
- package/dist/core/llm/anthropic.js.map +1 -1
- package/dist/core/llm/model-shortlist-live.test.d.ts +35 -0
- package/dist/core/llm/model-shortlist-live.test.d.ts.map +1 -0
- package/dist/core/llm/model-shortlist-live.test.js +452 -0
- package/dist/core/llm/model-shortlist-live.test.js.map +1 -0
- package/dist/core/llm/openrouter-mapping.test.js +18 -0
- package/dist/core/llm/openrouter-mapping.test.js.map +1 -1
- package/dist/core/llm/openrouter.d.ts.map +1 -1
- package/dist/core/llm/openrouter.js +20 -11
- package/dist/core/llm/openrouter.js.map +1 -1
- package/dist/core/llm/provider.d.ts +34 -3
- package/dist/core/llm/provider.d.ts.map +1 -1
- package/dist/core/llm/provider.js.map +1 -1
- package/dist/core/llm/reasoning-details.test.d.ts +2 -0
- package/dist/core/llm/reasoning-details.test.d.ts.map +1 -0
- package/dist/core/llm/reasoning-details.test.js +144 -0
- package/dist/core/llm/reasoning-details.test.js.map +1 -0
- package/dist/core/llm/state.d.ts +2 -0
- package/dist/core/llm/state.d.ts.map +1 -1
- package/dist/core/llm/state.js +11 -0
- package/dist/core/llm/state.js.map +1 -1
- package/dist/core/llm/thinking-blocks.test.d.ts +2 -0
- package/dist/core/llm/thinking-blocks.test.d.ts.map +1 -0
- package/dist/core/llm/thinking-blocks.test.js +291 -0
- package/dist/core/llm/thinking-blocks.test.js.map +1 -0
- package/dist/core/preset/index.d.ts +1 -0
- package/dist/core/preset/index.d.ts.map +1 -1
- package/dist/core/preset/index.js +2 -0
- package/dist/core/preset/index.js.map +1 -1
- package/dist/core/preset/overrides.d.ts +14 -0
- package/dist/core/preset/overrides.d.ts.map +1 -0
- package/dist/core/preset/overrides.js +20 -0
- package/dist/core/preset/overrides.js.map +1 -0
- package/dist/core/reasoning-round-trip.integration.test.d.ts +2 -0
- package/dist/core/reasoning-round-trip.integration.test.d.ts.map +1 -0
- package/dist/core/reasoning-round-trip.integration.test.js +150 -0
- package/dist/core/reasoning-round-trip.integration.test.js.map +1 -0
- package/dist/core/sessions/session-manager.d.ts +2 -0
- package/dist/core/sessions/session-manager.d.ts.map +1 -1
- package/dist/core/sessions/session-manager.js +17 -5
- package/dist/core/sessions/session-manager.js.map +1 -1
- package/dist/core/sessions/session-overrides.integration.test.d.ts +2 -0
- package/dist/core/sessions/session-overrides.integration.test.d.ts.map +1 -0
- package/dist/core/sessions/session-overrides.integration.test.js +154 -0
- package/dist/core/sessions/session-overrides.integration.test.js.map +1 -0
- package/dist/core/sessions/session.d.ts +10 -1
- package/dist/core/sessions/session.d.ts.map +1 -1
- package/dist/core/sessions/session.js +15 -0
- package/dist/core/sessions/session.js.map +1 -1
- package/dist/core/sessions/state.d.ts +57 -0
- package/dist/core/sessions/state.d.ts.map +1 -1
- package/dist/core/sessions/state.js +53 -0
- package/dist/core/sessions/state.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/services/plugin.d.ts +3 -0
- package/dist/plugins/services/plugin.d.ts.map +1 -1
- package/dist/plugins/services/plugin.js +50 -14
- package/dist/plugins/services/plugin.js.map +1 -1
- package/dist/plugins/services/prompt.d.ts.map +1 -1
- package/dist/plugins/services/prompt.js +8 -0
- package/dist/plugins/services/prompt.js.map +1 -1
- package/dist/plugins/services/schema.d.ts +33 -0
- package/dist/plugins/services/schema.d.ts.map +1 -1
- package/dist/plugins/services/service.d.ts +20 -0
- package/dist/plugins/services/service.d.ts.map +1 -1
- package/dist/plugins/services/service.js +88 -1
- package/dist/plugins/services/service.js.map +1 -1
- package/dist/plugins/services/services.integration.test.js +254 -0
- package/dist/plugins/services/services.integration.test.js.map +1 -1
- package/dist/plugins/session-lifecycle/plugin.d.ts +36 -0
- package/dist/plugins/session-lifecycle/plugin.d.ts.map +1 -1
- package/dist/plugins/session-lifecycle/plugin.js +53 -1
- package/dist/plugins/session-lifecycle/plugin.js.map +1 -1
- package/dist/testing/test-harness.d.ts +12 -2
- package/dist/testing/test-harness.d.ts.map +1 -1
- package/dist/testing/test-harness.js +13 -2
- package/dist/testing/test-harness.js.map +1 -1
- package/package.json +3 -3
- package/src/core/agents/agent.ts +28 -5
- package/src/core/agents/state.ts +26 -0
- package/src/core/context/state.ts +4 -0
- package/src/core/llm/anthropic.ts +71 -10
- package/src/core/llm/model-shortlist-live.test.ts +577 -0
- package/src/core/llm/openrouter-mapping.test.ts +24 -0
- package/src/core/llm/openrouter.ts +28 -12
- package/src/core/llm/provider.ts +44 -3
- package/src/core/llm/reasoning-details.test.ts +177 -0
- package/src/core/llm/state.ts +12 -0
- package/src/core/llm/thinking-blocks.test.ts +345 -0
- package/src/core/preset/index.ts +3 -0
- package/src/core/preset/overrides.ts +24 -0
- package/src/core/reasoning-round-trip.integration.test.ts +188 -0
- package/src/core/sessions/session-manager.ts +22 -3
- package/src/core/sessions/session-overrides.integration.test.ts +210 -0
- package/src/core/sessions/session.ts +19 -1
- package/src/core/sessions/state.ts +82 -0
- package/src/index.ts +9 -1
- package/src/plugins/services/plugin.ts +50 -14
- package/src/plugins/services/prompt.ts +8 -0
- package/src/plugins/services/schema.ts +33 -0
- package/src/plugins/services/service.ts +113 -1
- package/src/plugins/services/services.integration.test.ts +270 -0
- package/src/plugins/session-lifecycle/plugin.ts +59 -1
- package/src/testing/test-harness.ts +22 -3
package/src/core/agents/agent.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import z from 'zod/v4'
|
|
11
11
|
import { DebounceCallback } from '~/core/agents/debounce.js'
|
|
12
12
|
import type { AgentId } from '~/core/agents/schema.js'
|
|
13
|
-
import type { AgentState, HandlerResult } from '~/core/agents/state.js'
|
|
13
|
+
import type { AgentState, HandlerResult, ReasoningDetails, ThinkingBlocks } from '~/core/agents/state.js'
|
|
14
14
|
import { agentEvents } from '~/core/agents/state.js'
|
|
15
15
|
import { withSessionId } from '~/core/events/test-helpers.js'
|
|
16
16
|
import type { FileStore } from '~/core/file-store/types.js'
|
|
@@ -51,6 +51,7 @@ import type { Logger } from '../../lib/logger/logger.js'
|
|
|
51
51
|
import type { SessionContext } from '../sessions/context.js'
|
|
52
52
|
import type { SessionStore } from '../sessions/session-store.js'
|
|
53
53
|
import type { SessionState } from '../sessions/state.js'
|
|
54
|
+
import { resolveAgentOverrides } from '../sessions/state.js'
|
|
54
55
|
import type { SessionEnvironment, ToolExecutor } from '../tools/index.js'
|
|
55
56
|
import type { AgentContext } from './context.js'
|
|
56
57
|
import { sanitizeLLMResponse } from './response-sanitizer.js'
|
|
@@ -200,6 +201,22 @@ export class Agent {
|
|
|
200
201
|
return this.store.getAgentState(this.id)
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Model for the next inference: the session override if one is set, else the
|
|
206
|
+
* model from the preset definition.
|
|
207
|
+
*
|
|
208
|
+
* Resolved per inference rather than frozen at construction so that changing
|
|
209
|
+
* the override applies to agents that are already running. Applying it in
|
|
210
|
+
* `Session.getAgentConfig()` instead would only reach agents created after the
|
|
211
|
+
* change, and catching up the rest would mean rebuilding live Agent instances
|
|
212
|
+
* mid-turn — abort controllers, debounce timers and mailbox state included.
|
|
213
|
+
*/
|
|
214
|
+
private resolveModel(): ModelId {
|
|
215
|
+
const definitionName = this.state?.definitionName
|
|
216
|
+
if (definitionName === undefined) return this.config.model
|
|
217
|
+
return resolveAgentOverrides(this.store.getState(), definitionName).model ?? this.config.model
|
|
218
|
+
}
|
|
219
|
+
|
|
203
220
|
// ============================================================================
|
|
204
221
|
// Public API
|
|
205
222
|
// ============================================================================
|
|
@@ -449,7 +466,7 @@ export class Agent {
|
|
|
449
466
|
const cachedMessages = applyCacheBreakpoint(messages, extraMessages.length, this.config.cacheTtl, agentState.preamble.length)
|
|
450
467
|
|
|
451
468
|
const request: InferenceRequest = {
|
|
452
|
-
model: this.
|
|
469
|
+
model: this.resolveModel(),
|
|
453
470
|
systemPrompt: this.buildSystemPrompt(),
|
|
454
471
|
messages: cachedMessages,
|
|
455
472
|
tools: this.tools.size > 0 ? [...this.tools.values()] : undefined,
|
|
@@ -628,7 +645,7 @@ export class Agent {
|
|
|
628
645
|
|
|
629
646
|
// 5. LLM inference (with retry)
|
|
630
647
|
const request: InferenceRequest = {
|
|
631
|
-
model: this.
|
|
648
|
+
model: this.resolveModel(),
|
|
632
649
|
systemPrompt: this.buildSystemPrompt(),
|
|
633
650
|
messages: cachedMessages,
|
|
634
651
|
tools: this.tools.size > 0 ? [...this.tools.values()] : undefined,
|
|
@@ -793,7 +810,7 @@ export class Agent {
|
|
|
793
810
|
if (afterResult !== null) {
|
|
794
811
|
if (afterResult.action === 'pause') {
|
|
795
812
|
// Commit the turn before pausing so pendingMessages move to conversationHistory.
|
|
796
|
-
await this.emitInferenceCompleted(response, llmCallId, llmResponse.value.metrics)
|
|
813
|
+
await this.emitInferenceCompleted(response, llmCallId, llmResponse.value.metrics, llmResponse.value.reasoningDetails, llmResponse.value.thinkingBlocks)
|
|
797
814
|
await this.consumeDequeuedTokens(pluginDequeued)
|
|
798
815
|
await this.emitHandlerPause(afterResult.reason)
|
|
799
816
|
return
|
|
@@ -827,7 +844,9 @@ export class Agent {
|
|
|
827
844
|
|
|
828
845
|
// 4d. Inference completed
|
|
829
846
|
// Tool calls will be executed in the next continue() cycle
|
|
830
|
-
|
|
847
|
+
// Reasoning travels beside `response`, not inside it: an afterInference handler may
|
|
848
|
+
// rewrite the text, but the blocks stay the model's own and go back untouched.
|
|
849
|
+
await this.emitInferenceCompleted(response, llmCallId, llmResponse.value.metrics, llmResponse.value.reasoningDetails, llmResponse.value.thinkingBlocks)
|
|
831
850
|
|
|
832
851
|
// Consume dequeue tokens after the commit: a crash in the gap re-delivers the
|
|
833
852
|
// message on restart (at-least-once) instead of dropping it (consume-first
|
|
@@ -852,6 +871,8 @@ export class Agent {
|
|
|
852
871
|
cachedTokens?: number
|
|
853
872
|
cacheWriteTokens?: number
|
|
854
873
|
},
|
|
874
|
+
reasoningDetails?: ReasoningDetails,
|
|
875
|
+
thinkingBlocks?: ThinkingBlocks,
|
|
855
876
|
): Promise<void> {
|
|
856
877
|
await this.store.emit(withSessionId(
|
|
857
878
|
this.store.sessionId,
|
|
@@ -865,6 +886,8 @@ export class Agent {
|
|
|
865
886
|
name: tc.name,
|
|
866
887
|
input: tc.input,
|
|
867
888
|
})),
|
|
889
|
+
reasoningDetails,
|
|
890
|
+
thinkingBlocks,
|
|
868
891
|
},
|
|
869
892
|
metrics: metrics ?? {
|
|
870
893
|
promptTokens: 0,
|
package/src/core/agents/state.ts
CHANGED
|
@@ -136,6 +136,28 @@ export interface UserLLMMessage {
|
|
|
136
136
|
cacheControl?: LLMMessageCacheControl
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Provider chain-of-thought blocks, kept opaque (OpenRouter `reasoning_details`).
|
|
141
|
+
*
|
|
142
|
+
* The provider requires the exact sequence it produced to come back verbatim, so
|
|
143
|
+
* these are never parsed, reordered or normalized — only stored and echoed. The
|
|
144
|
+
* shape is provider- and model-specific and changes without notice; nothing in
|
|
145
|
+
* the SDK may depend on it.
|
|
146
|
+
*/
|
|
147
|
+
export type ReasoningDetails = unknown[]
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Anthropic extended-thinking blocks (`thinking` and `redacted_thinking`), kept opaque
|
|
151
|
+
* for the same reason as {@link ReasoningDetails}: each carries a `signature` the API
|
|
152
|
+
* verifies, so an edited block is worse than a missing one.
|
|
153
|
+
*
|
|
154
|
+
* Deliberately a separate field rather than sharing `reasoningDetails`. A session can
|
|
155
|
+
* change model mid-conversation, so one field would eventually put Anthropic-shaped
|
|
156
|
+
* blocks in front of OpenRouter or the reverse. Two fields make that case degrade to
|
|
157
|
+
* "absent", which both providers accept, instead of "wrong shape", which neither does.
|
|
158
|
+
*/
|
|
159
|
+
export type ThinkingBlocks = unknown[]
|
|
160
|
+
|
|
139
161
|
/**
|
|
140
162
|
* Assistant message - LLM response.
|
|
141
163
|
*/
|
|
@@ -143,6 +165,10 @@ export interface AssistantLLMMessage {
|
|
|
143
165
|
role: 'assistant'
|
|
144
166
|
content: string
|
|
145
167
|
toolCalls?: ToolCall[]
|
|
168
|
+
/** Reasoning blocks to echo back on later requests, so the model keeps its chain of thought across tool calls. */
|
|
169
|
+
reasoningDetails?: ReasoningDetails
|
|
170
|
+
/** Anthropic thinking blocks to replay on later requests. Required by the API during tool use. */
|
|
171
|
+
thinkingBlocks?: ThinkingBlocks
|
|
146
172
|
/** Marks this message as a prompt cache breakpoint. */
|
|
147
173
|
cacheControl?: LLMMessageCacheControl
|
|
148
174
|
}
|
|
@@ -56,6 +56,10 @@ export const compactedConversationMessageSchema: z4.ZodType<LLMMessage> = z4.dis
|
|
|
56
56
|
name: z4.string(),
|
|
57
57
|
input: z4.unknown(),
|
|
58
58
|
})).optional(),
|
|
59
|
+
/** Opaque provider reasoning blocks — retained turns keep them, see ReasoningDetails. */
|
|
60
|
+
reasoningDetails: z4.array(z4.unknown()).optional(),
|
|
61
|
+
/** Anthropic thinking blocks — same. */
|
|
62
|
+
thinkingBlocks: z4.array(z4.unknown()).optional(),
|
|
59
63
|
cacheControl: cacheControlSchema.optional(),
|
|
60
64
|
}),
|
|
61
65
|
z4.object({
|
|
@@ -48,6 +48,14 @@ interface AnthropicTextBlock {
|
|
|
48
48
|
interface AnthropicThinkingBlock {
|
|
49
49
|
type: 'thinking'
|
|
50
50
|
thinking: string
|
|
51
|
+
/** Verified by the API when the block is replayed. An edited block is rejected. */
|
|
52
|
+
signature: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Thinking the API withheld. Opaque, and must still be replayed — see AnthropicThinkingBlock. */
|
|
56
|
+
interface AnthropicRedactedThinkingBlock {
|
|
57
|
+
type: 'redacted_thinking'
|
|
58
|
+
data: string
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
interface AnthropicToolUseBlock {
|
|
@@ -57,7 +65,29 @@ interface AnthropicToolUseBlock {
|
|
|
57
65
|
input: unknown
|
|
58
66
|
}
|
|
59
67
|
|
|
60
|
-
type AnthropicContentBlock =
|
|
68
|
+
type AnthropicContentBlock =
|
|
69
|
+
| AnthropicTextBlock
|
|
70
|
+
| AnthropicThinkingBlock
|
|
71
|
+
| AnthropicRedactedThinkingBlock
|
|
72
|
+
| AnthropicToolUseBlock
|
|
73
|
+
|
|
74
|
+
const isThinkingBlock = (block: AnthropicContentBlock): block is AnthropicThinkingBlock | AnthropicRedactedThinkingBlock =>
|
|
75
|
+
block.type === 'thinking' || block.type === 'redacted_thinking'
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Narrow a stored block back to a thinking block on the way out.
|
|
79
|
+
*
|
|
80
|
+
* `AssistantLLMMessage.thinkingBlocks` is `unknown[]` — the domain layer does not model
|
|
81
|
+
* provider shapes. This provider wrote those blocks, so it is entitled to recognise them,
|
|
82
|
+
* but it validates every field it declares rather than assuming. Anything else the API
|
|
83
|
+
* added travels along untouched, because the block is passed by reference and never rebuilt.
|
|
84
|
+
*/
|
|
85
|
+
const isEchoableThinkingBlock = (value: unknown): value is AnthropicThinkingBlock | AnthropicRedactedThinkingBlock => {
|
|
86
|
+
if (typeof value !== 'object' || value === null || !('type' in value)) return false
|
|
87
|
+
if (value.type === 'thinking') return 'thinking' in value && 'signature' in value && typeof value.signature === 'string'
|
|
88
|
+
if (value.type === 'redacted_thinking') return 'data' in value && typeof value.data === 'string'
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
61
91
|
|
|
62
92
|
interface AnthropicMessageResponse {
|
|
63
93
|
id: string
|
|
@@ -121,31 +151,46 @@ interface AnthropicToolResultBlockParam {
|
|
|
121
151
|
cache_control?: AnthropicCacheControl
|
|
122
152
|
}
|
|
123
153
|
|
|
124
|
-
|
|
154
|
+
/** Blocks the API accepts `cache_control` on — everything except a replayed thinking block. */
|
|
155
|
+
type AnthropicCacheableBlockParam =
|
|
125
156
|
| AnthropicTextBlockParam
|
|
126
157
|
| AnthropicImageBlockParam
|
|
127
158
|
| AnthropicToolUseBlockParam
|
|
128
159
|
| AnthropicToolResultBlockParam
|
|
129
160
|
|
|
161
|
+
type AnthropicContentBlockParam =
|
|
162
|
+
| AnthropicCacheableBlockParam
|
|
163
|
+
| AnthropicThinkingBlock
|
|
164
|
+
| AnthropicRedactedThinkingBlock
|
|
165
|
+
|
|
166
|
+
const isCacheableBlock = (block: AnthropicContentBlockParam): block is AnthropicCacheableBlockParam =>
|
|
167
|
+
block.type !== 'thinking' && block.type !== 'redacted_thinking'
|
|
168
|
+
|
|
130
169
|
interface AnthropicMessageParam {
|
|
131
170
|
role: 'user' | 'assistant'
|
|
132
171
|
content: string | AnthropicContentBlockParam[]
|
|
133
172
|
}
|
|
134
173
|
|
|
135
174
|
/**
|
|
136
|
-
* Add `cache_control` to the LAST content block of an AnthropicMessageParam
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
175
|
+
* Add `cache_control` to the LAST content block of an AnthropicMessageParam that can carry
|
|
176
|
+
* it. Converts string content to a single text block first so the mark has a place to live.
|
|
177
|
+
* Mutates in place so the cache breakpoint survives subsequent `mergeConsecutiveMessages`.
|
|
178
|
+
*
|
|
179
|
+
* Thinking blocks are skipped: they are replayed byte-for-byte and the API rejects an extra
|
|
180
|
+
* key on them. They lead the assistant message, so the only case where none of the blocks is
|
|
181
|
+
* cacheable is a message made of nothing but thinking — which then gets no breakpoint.
|
|
140
182
|
*/
|
|
141
183
|
function applyCacheControlToLastBlock(msg: AnthropicMessageParam, cacheControl: AnthropicCacheControl): void {
|
|
142
184
|
if (typeof msg.content === 'string') {
|
|
143
185
|
msg.content = [{ type: 'text', text: msg.content, cache_control: cacheControl }]
|
|
144
186
|
return
|
|
145
187
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
188
|
+
for (let i = msg.content.length - 1; i >= 0; i--) {
|
|
189
|
+
const block = msg.content[i]
|
|
190
|
+
if (!isCacheableBlock(block)) continue
|
|
191
|
+
msg.content[i] = { ...block, cache_control: cacheControl }
|
|
192
|
+
return
|
|
193
|
+
}
|
|
149
194
|
}
|
|
150
195
|
|
|
151
196
|
interface AnthropicToolParam {
|
|
@@ -283,7 +328,11 @@ export class AnthropicProvider implements RoutableLLMProvider {
|
|
|
283
328
|
.map((block) => block.text)
|
|
284
329
|
.join('')
|
|
285
330
|
|
|
286
|
-
|
|
331
|
+
// Kept whole, in order, signatures included: the API verifies them on replay.
|
|
332
|
+
// `redacted_thinking` has no readable text but still has to travel.
|
|
333
|
+
const thinkingBlocks = data.content.filter(isThinkingBlock)
|
|
334
|
+
|
|
335
|
+
const reasoning = thinkingBlocks
|
|
287
336
|
.filter((block): block is AnthropicThinkingBlock => block.type === 'thinking')
|
|
288
337
|
.map((block) => block.thinking)
|
|
289
338
|
.join('')
|
|
@@ -315,6 +364,8 @@ export class AnthropicProvider implements RoutableLLMProvider {
|
|
|
315
364
|
metrics,
|
|
316
365
|
providerRequestId: data.id,
|
|
317
366
|
reasoning: reasoning || undefined,
|
|
367
|
+
// Normalized to undefined so an empty array never reaches the wire on the way back.
|
|
368
|
+
thinkingBlocks: thinkingBlocks.length > 0 ? thinkingBlocks : undefined,
|
|
318
369
|
})
|
|
319
370
|
} catch (error) {
|
|
320
371
|
return Err(this.mapError(error))
|
|
@@ -399,6 +450,16 @@ export class AnthropicProvider implements RoutableLLMProvider {
|
|
|
399
450
|
}
|
|
400
451
|
case 'assistant': {
|
|
401
452
|
const contentBlocks: AnthropicContentBlockParam[] = []
|
|
453
|
+
// Thinking goes first — the API requires the block order the model produced, and
|
|
454
|
+
// during tool use it requires the blocks to be there at all. All or nothing: a
|
|
455
|
+
// partial sequence is rejected, so anything unrecognisable drops the whole set
|
|
456
|
+
// rather than sending a truncated one.
|
|
457
|
+
const thinking = msg.thinkingBlocks ?? []
|
|
458
|
+
if (thinking.length > 0 && thinking.every(isEchoableThinkingBlock)) {
|
|
459
|
+
contentBlocks.push(...thinking)
|
|
460
|
+
} else if (thinking.length > 0) {
|
|
461
|
+
this.logger?.warn('Dropping unrecognisable thinking blocks from an assistant message', { count: thinking.length })
|
|
462
|
+
}
|
|
402
463
|
if (msg.content) {
|
|
403
464
|
contentBlocks.push({ type: 'text', text: msg.content })
|
|
404
465
|
}
|