@deepstrike/sdk 0.2.6 → 0.2.7

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 (38) hide show
  1. package/dist/index.d.ts +5 -3
  2. package/dist/index.js +3 -2
  3. package/dist/kernel.d.ts +54 -0
  4. package/dist/kernel.js +8 -0
  5. package/dist/providers/anthropic.d.ts +4 -1
  6. package/dist/providers/anthropic.js +51 -0
  7. package/dist/providers/catalog.js +5 -2
  8. package/dist/providers/deepseek.d.ts +4 -1
  9. package/dist/providers/deepseek.js +76 -8
  10. package/dist/providers/glm.d.ts +2 -1
  11. package/dist/providers/glm.js +7 -0
  12. package/dist/providers/kimi.d.ts +2 -1
  13. package/dist/providers/kimi.js +7 -0
  14. package/dist/providers/minimax.d.ts +28 -2
  15. package/dist/providers/minimax.js +197 -1
  16. package/dist/providers/openai-chat.d.ts +6 -2
  17. package/dist/providers/openai-chat.js +20 -3
  18. package/dist/providers/openai.d.ts +4 -1
  19. package/dist/providers/openai.js +31 -7
  20. package/dist/providers/profiles.d.ts +6 -0
  21. package/dist/providers/profiles.js +6 -0
  22. package/dist/providers/qwen.d.ts +3 -1
  23. package/dist/providers/qwen.js +20 -2
  24. package/dist/providers/replay-validator.d.ts +10 -0
  25. package/dist/providers/replay-validator.js +57 -0
  26. package/dist/runtime/kernel-event-log.js +22 -0
  27. package/dist/runtime/kernel-step.d.ts +13 -0
  28. package/dist/runtime/provider-replay.d.ts +8 -1
  29. package/dist/runtime/provider-replay.js +37 -4
  30. package/dist/runtime/runner.d.ts +22 -1
  31. package/dist/runtime/runner.js +68 -2
  32. package/dist/runtime/session-log.d.ts +22 -0
  33. package/dist/runtime/session-repair.d.ts +26 -3
  34. package/dist/runtime/session-repair.js +33 -32
  35. package/dist/types/agent.d.ts +50 -0
  36. package/dist/types/agent.js +110 -0
  37. package/dist/types.d.ts +23 -0
  38. package/package.json +2 -2
@@ -1,5 +1,7 @@
1
1
  import { AnthropicProvider } from "./anthropic.js";
2
+ import { OpenAIChatProvider } from "./openai.js";
2
3
  import { endpointProfiles } from "./profiles.js";
4
+ import { omitExtensionKeys } from "./base.js";
3
5
  const MINIMAX_POLICIES = {
4
6
  "MiniMax-M2.7": { maxTurns: 35 },
5
7
  "MiniMax-M2.7-highspeed": { maxTurns: 35 },
@@ -10,14 +12,208 @@ const MINIMAX_POLICIES = {
10
12
  "MiniMax-M2": { maxTurns: 20 },
11
13
  "MiniMax-Text-01": { maxTurns: 20 },
12
14
  };
13
- export class MiniMaxProvider extends AnthropicProvider {
15
+ /**
16
+ * MiniMax over its Anthropic-compatible endpoint. Replay is carried as Anthropic
17
+ * `native_blocks` (thinking/text/tool_use), identical to the first-party
18
+ * Anthropic provider.
19
+ */
20
+ export class MiniMaxAnthropicProvider extends AnthropicProvider {
14
21
  constructor(apiKey, model = "MiniMax-M2.7", retry, baseURL = endpointProfiles["minimax.anthropic"].baseURL) {
15
22
  super(apiKey, model, retry, {
16
23
  baseURL,
17
24
  authMode: "api-key",
18
25
  });
19
26
  }
27
+ providerName() {
28
+ return "minimax";
29
+ }
30
+ runtimePolicy() {
31
+ return MINIMAX_POLICIES[this.model] ?? {};
32
+ }
33
+ }
34
+ /**
35
+ * MiniMax over its OpenAI-compatible endpoint. Replay is carried as
36
+ * `reasoning_content` / `reasoning_details` (split reasoning), and requests
37
+ * default to `reasoning_split: true` so reasoning is returned out-of-band rather
38
+ * than embedded in the message content.
39
+ */
40
+ export class MiniMaxOpenAIProvider extends OpenAIChatProvider {
41
+ constructor(apiKey, model = "MiniMax-M2.7", retry, baseURL = endpointProfiles["minimax.openai"].baseURL) {
42
+ super(apiKey, model, retry, baseURL);
43
+ }
20
44
  runtimePolicy() {
21
45
  return MINIMAX_POLICIES[this.model] ?? {};
22
46
  }
47
+ descriptor() {
48
+ return {
49
+ provider: "minimax",
50
+ protocol: "openai-chat",
51
+ model: this.model,
52
+ reasoning: {
53
+ supported: true,
54
+ preserveAcrossToolTurns: true,
55
+ requiresReplayForToolTurns: true,
56
+ },
57
+ toolCalls: {
58
+ supported: true,
59
+ requiresStrictPairing: true,
60
+ },
61
+ };
62
+ }
63
+ requireNonEmptyReasoningReplayForToolTurns(extensions) {
64
+ if (extensions?.__deepstrikeThinkingEnabled === false)
65
+ return false;
66
+ return extensions?.reasoning_split !== false;
67
+ }
68
+ buildRequestExtensions(extensions) {
69
+ const reasoningSplit = extensions?.reasoning_split !== false;
70
+ return {
71
+ ...omitExtensionKeys(extensions, ["reasoning_split", "exposeReasoning"]),
72
+ __deepstrikeThinkingEnabled: reasoningSplit,
73
+ reasoning_split: reasoningSplit,
74
+ };
75
+ }
76
+ async complete(context, tools, extensions) {
77
+ const requestExtensions = this.buildRequestExtensions(extensions);
78
+ if (this.circuit.isOpen())
79
+ throw new Error("Circuit breaker open");
80
+ const msgs = this.buildChatMessages(context, requestExtensions);
81
+ let lastErr;
82
+ for (let i = 0; i < this.maxRetries; i++) {
83
+ try {
84
+ const resp = await this.client.chat.completions.create({
85
+ ...this.requestExtensions(requestExtensions),
86
+ model: this.model,
87
+ messages: msgs,
88
+ ...(tools.length ? { tools: this.chat.buildTools(tools) } : {}),
89
+ });
90
+ this.circuit.recordSuccess();
91
+ const choice = resp.choices[0].message;
92
+ const nativeToolCalls = choice.tool_calls ?? [];
93
+ const toolCalls = this.chat.normalizeToolCalls(nativeToolCalls);
94
+ const content = choice.content ?? "";
95
+ this.rememberMiniMaxReplay(content, toolCalls, choice.reasoning_content, choice.reasoning_details, nativeToolCalls);
96
+ return { role: "assistant", content, tokenCount: resp.usage?.completion_tokens ?? resp.usage?.total_tokens, toolCalls };
97
+ }
98
+ catch (err) {
99
+ lastErr = err;
100
+ this.circuit.recordFailure();
101
+ if (i < this.maxRetries - 1)
102
+ await new Promise(r => setTimeout(r, this.baseDelay * 2 ** i));
103
+ }
104
+ }
105
+ throw lastErr;
106
+ }
107
+ async *stream(context, tools, extensions) {
108
+ const exposeReasoning = extensions?.exposeReasoning ?? false;
109
+ const requestExtensions = this.buildRequestExtensions(extensions);
110
+ const msgs = this.buildChatMessages(context, requestExtensions);
111
+ const toolCallBufs = {};
112
+ const emittedToolCallIndexes = new Set();
113
+ let reasoningContent = "";
114
+ let reasoningDetails;
115
+ let finalText = "";
116
+ const stream = await this.client.chat.completions.create({
117
+ ...this.requestExtensions(requestExtensions),
118
+ model: this.model,
119
+ messages: msgs,
120
+ ...(tools.length ? { tools: this.chat.buildTools(tools) } : {}),
121
+ stream: true,
122
+ stream_options: { include_usage: true },
123
+ });
124
+ let totalTokens = 0;
125
+ let inputTokens = 0;
126
+ let outputTokens = 0;
127
+ for await (const chunk of stream) {
128
+ if (chunk.usage) {
129
+ totalTokens = chunk.usage.total_tokens;
130
+ inputTokens = chunk.usage.prompt_tokens ?? 0;
131
+ outputTokens = chunk.usage.completion_tokens ?? 0;
132
+ continue;
133
+ }
134
+ const choice = chunk.choices[0];
135
+ if (!choice)
136
+ continue;
137
+ const delta = choice.delta;
138
+ if (!delta)
139
+ continue;
140
+ if (exposeReasoning && delta.reasoning_content) {
141
+ yield { type: "thinking_delta", delta: String(delta.reasoning_content) };
142
+ }
143
+ if (delta.reasoning_content)
144
+ reasoningContent += String(delta.reasoning_content);
145
+ if (delta.reasoning_details !== undefined && delta.reasoning_details !== null)
146
+ reasoningDetails = delta.reasoning_details;
147
+ if (delta.content) {
148
+ finalText += String(delta.content);
149
+ yield { type: "text_delta", delta: delta.content };
150
+ }
151
+ for (const tc of delta.tool_calls ?? []) {
152
+ const idx = tc.index;
153
+ if (!toolCallBufs[idx])
154
+ toolCallBufs[idx] = { id: tc.id ?? "", name: "", argsBuf: "" };
155
+ if (tc.function?.name)
156
+ toolCallBufs[idx].name += tc.function.name;
157
+ toolCallBufs[idx].argsBuf += tc.function?.arguments ?? "";
158
+ }
159
+ if (choice.finish_reason === "tool_calls") {
160
+ const toolCalls = Object.values(toolCallBufs).map(tb => ({ id: tb.id, name: tb.name, arguments: tb.argsBuf || "{}" }));
161
+ this.rememberMiniMaxReplay(finalText, toolCalls, reasoningContent, reasoningDetails, nativeToolCallsFromBuffers(toolCallBufs));
162
+ for (const [index, tb] of Object.entries(toolCallBufs)) {
163
+ const idx = Number(index);
164
+ if (emittedToolCallIndexes.has(idx))
165
+ continue;
166
+ let args = {};
167
+ try {
168
+ args = JSON.parse(tb.argsBuf || "{}");
169
+ }
170
+ catch {
171
+ args = {};
172
+ }
173
+ emittedToolCallIndexes.add(idx);
174
+ yield { type: "tool_call", id: tb.id, name: tb.name, arguments: args };
175
+ }
176
+ }
177
+ }
178
+ const toolCalls = Object.values(toolCallBufs).map(tb => ({ id: tb.id, name: tb.name, arguments: tb.argsBuf || "{}" }));
179
+ this.rememberMiniMaxReplay(finalText, toolCalls, reasoningContent, reasoningDetails, nativeToolCallsFromBuffers(toolCallBufs));
180
+ for (const [index, tb] of Object.entries(toolCallBufs)) {
181
+ const idx = Number(index);
182
+ if (emittedToolCallIndexes.has(idx))
183
+ continue;
184
+ let args = {};
185
+ try {
186
+ args = JSON.parse(tb.argsBuf || "{}");
187
+ }
188
+ catch {
189
+ args = {};
190
+ }
191
+ emittedToolCallIndexes.add(idx);
192
+ yield { type: "tool_call", id: tb.id, name: tb.name, arguments: args };
193
+ }
194
+ if (totalTokens > 0)
195
+ yield { type: "usage", totalTokens, inputTokens, outputTokens };
196
+ }
197
+ rememberMiniMaxReplay(content, toolCalls, reasoningContent, reasoningDetails, nativeToolCalls) {
198
+ const hasReasoning = typeof reasoningContent === "string" && reasoningContent.trim().length > 0;
199
+ const hasDetails = reasoningDetails !== undefined && reasoningDetails !== null;
200
+ if (!hasReasoning && !hasDetails)
201
+ return;
202
+ this.chat.rememberReplayFields({ content, toolCalls }, {
203
+ schema_version: 2,
204
+ provider: "minimax",
205
+ protocol: "openai-chat",
206
+ model: this.model,
207
+ ...(hasReasoning ? { reasoning_content: reasoningContent } : {}),
208
+ ...(hasDetails ? { reasoning_details: reasoningDetails } : {}),
209
+ ...(nativeToolCalls.length ? { tool_calls: nativeToolCalls } : {}),
210
+ });
211
+ }
212
+ }
213
+ function nativeToolCallsFromBuffers(toolCallBufs) {
214
+ return Object.values(toolCallBufs).map(tb => ({
215
+ id: tb.id,
216
+ type: "function",
217
+ function: { name: tb.name, arguments: tb.argsBuf || "{}" },
218
+ }));
23
219
  }
@@ -1,5 +1,9 @@
1
1
  import type OpenAI from "openai";
2
- import type { Message, RenderedContext, ToolSchema } from "../types.js";
2
+ import type { Message, ProviderDescriptor, RenderedContext, ToolSchema } from "../types.js";
3
+ export interface OpenAIChatBuildMessageOptions {
4
+ descriptor?: ProviderDescriptor;
5
+ requireNonEmptyReasoningForToolCalls?: boolean;
6
+ }
3
7
  export declare class OpenAIChatAdapter {
4
8
  private replayFields;
5
9
  buildTools(tools: ToolSchema[]): {
@@ -10,7 +14,7 @@ export declare class OpenAIChatAdapter {
10
14
  parameters: any;
11
15
  };
12
16
  }[];
13
- buildMessages(context: RenderedContext): OpenAI.ChatCompletionMessageParam[];
17
+ buildMessages(context: RenderedContext, options?: OpenAIChatBuildMessageOptions): OpenAI.ChatCompletionMessageParam[];
14
18
  normalizeToolCalls(toolCalls?: OpenAI.ChatCompletionMessageToolCall[]): Array<{
15
19
  id: string;
16
20
  name: string;
@@ -1,5 +1,6 @@
1
1
  import { assistantReplayKey } from "../runtime/provider-replay.js";
2
2
  import { normalizeToolCall, toOpenAIMessageParams } from "./base.js";
3
+ import { validateOpenAIChatReplay } from "./replay-validator.js";
3
4
  export class OpenAIChatAdapter {
4
5
  replayFields = new Map();
5
6
  buildTools(tools) {
@@ -8,7 +9,12 @@ export class OpenAIChatAdapter {
8
9
  function: { name: t.name, description: t.description, parameters: JSON.parse(t.parameters) },
9
10
  }));
10
11
  }
11
- buildMessages(context) {
12
+ buildMessages(context, options = {}) {
13
+ validateOpenAIChatReplay(context, {
14
+ descriptor: options.descriptor,
15
+ requireNonEmptyReasoningForToolCalls: options.requireNonEmptyReasoningForToolCalls,
16
+ replayForAssistant: message => this.replayFields.get(assistantReplayKey(message)),
17
+ });
12
18
  // toOpenAIMessageParams prepends systemText as messages[0], then turns.
13
19
  const serialized = toOpenAIMessageParams(context);
14
20
  // Cursor starts at 1 to skip the system message injected by toOpenAIMessageParams.
@@ -20,8 +26,9 @@ export class OpenAIChatAdapter {
20
26
  }
21
27
  if (source.role === "assistant") {
22
28
  const replay = this.replayFields.get(assistantReplayKey(source));
23
- if (replay)
24
- serialized[cursor] = { ...serialized[cursor], ...replay };
29
+ const wireReplay = openAIChatWireReplayFields(replay);
30
+ if (wireReplay)
31
+ serialized[cursor] = { ...serialized[cursor], ...wireReplay };
25
32
  }
26
33
  cursor += 1;
27
34
  }
@@ -40,3 +47,13 @@ export class OpenAIChatAdapter {
40
47
  return this.replayFields.get(assistantReplayKey(message));
41
48
  }
42
49
  }
50
+ function openAIChatWireReplayFields(replay) {
51
+ if (!replay)
52
+ return undefined;
53
+ const fields = {};
54
+ if (typeof replay.reasoning_content === "string")
55
+ fields.reasoning_content = replay.reasoning_content;
56
+ if (replay.reasoning_details !== undefined)
57
+ fields.reasoning_details = replay.reasoning_details;
58
+ return Object.keys(fields).length ? fields : undefined;
59
+ }
@@ -1,5 +1,5 @@
1
1
  import OpenAI from "openai";
2
- import type { Message, ProviderReplay, RenderedContext, ToolSchema, StreamEvent, LLMProvider, RuntimePolicy } from "../types.js";
2
+ import type { Message, ProviderDescriptor, ProviderReplay, RenderedContext, ToolSchema, StreamEvent, LLMProvider, RuntimePolicy } from "../types.js";
3
3
  import { CircuitBreaker } from "./base.js";
4
4
  import { OpenAIChatAdapter } from "./openai-chat.js";
5
5
  export declare class OpenAIChatProvider implements LLMProvider {
@@ -14,6 +14,9 @@ export declare class OpenAIChatProvider implements LLMProvider {
14
14
  baseDelay: number;
15
15
  }, baseURL?: string);
16
16
  runtimePolicy(): RuntimePolicy;
17
+ descriptor(): ProviderDescriptor;
18
+ protected requireNonEmptyReasoningReplayForToolTurns(_extensions?: Record<string, unknown>): boolean;
19
+ protected buildChatMessages(context: RenderedContext, extensions?: Record<string, unknown>): OpenAI.Chat.Completions.ChatCompletionMessageParam[];
17
20
  peekProviderReplay(message: Pick<Message, "content" | "toolCalls">): ProviderReplay | undefined;
18
21
  seedProviderReplay(message: Pick<Message, "content" | "toolCalls">, replay: ProviderReplay): void;
19
22
  complete(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>): Promise<Message>;
@@ -42,21 +42,45 @@ export class OpenAIChatProvider {
42
42
  runtimePolicy() {
43
43
  return OPENAI_POLICIES[this.model] ?? {};
44
44
  }
45
+ descriptor() {
46
+ return {
47
+ provider: "openai",
48
+ protocol: "openai-chat",
49
+ model: this.model,
50
+ reasoning: {
51
+ supported: true,
52
+ preserveAcrossToolTurns: false,
53
+ },
54
+ toolCalls: {
55
+ supported: true,
56
+ requiresStrictPairing: true,
57
+ },
58
+ };
59
+ }
60
+ requireNonEmptyReasoningReplayForToolTurns(_extensions) {
61
+ return false;
62
+ }
63
+ buildChatMessages(context, extensions) {
64
+ return this.chat.buildMessages(context, {
65
+ descriptor: this.descriptor(),
66
+ requireNonEmptyReasoningForToolCalls: this.requireNonEmptyReasoningReplayForToolTurns(extensions),
67
+ });
68
+ }
45
69
  peekProviderReplay(message) {
46
70
  const fields = this.chat.peekReplayFields(message);
47
- if (!fields || !("reasoning_content" in fields))
71
+ if (!fields || !("reasoning_content" in fields || "reasoning_details" in fields))
48
72
  return undefined;
49
- return { reasoning_content: String(fields.reasoning_content ?? "") };
73
+ return fields;
50
74
  }
51
75
  seedProviderReplay(message, replay) {
52
- if (replay.reasoning_content !== undefined) {
53
- this.chat.rememberReplayFields(message, { reasoning_content: replay.reasoning_content });
76
+ if (replay.reasoning_content !== undefined || replay.reasoning_details !== undefined) {
77
+ this.chat.rememberReplayFields(message, replay);
54
78
  }
55
79
  }
56
80
  async complete(context, tools, extensions) {
57
81
  if (this.circuit.isOpen())
58
82
  throw new Error("Circuit breaker open");
59
- const msgs = this.chat.buildMessages(context);
83
+ const msgs = this.buildChatMessages(context, extensions);
60
84
  let lastErr;
61
85
  for (let i = 0; i < this.maxRetries; i++) {
62
86
  try {
@@ -81,7 +105,7 @@ export class OpenAIChatProvider {
81
105
  throw lastErr;
82
106
  }
83
107
  async *stream(context, tools, extensions) {
84
- const msgs = this.chat.buildMessages(context);
108
+ const msgs = this.buildChatMessages(context, extensions);
85
109
  const toolCallBufs = {};
86
110
  const emittedToolCallIndexes = new Set();
87
111
  const extractor = new ThinkingTagStreamExtractor();
@@ -190,7 +214,7 @@ export class OpenAIChatProvider {
190
214
  yield { type: "usage", totalTokens, inputTokens, outputTokens };
191
215
  }
192
216
  requestExtensions(extensions) {
193
- return omitExtensionKeys(extensions, ["model", "messages", "tools", "stream", "stream_options"]);
217
+ return omitExtensionKeys(extensions, ["model", "messages", "tools", "stream", "stream_options", "__deepstrikeThinkingEnabled"]);
194
218
  }
195
219
  }
196
220
  export { OpenAIChatProvider as OpenAIProvider };
@@ -59,6 +59,12 @@ export declare const endpointProfiles: {
59
59
  readonly protocol: "anthropic-messages";
60
60
  readonly baseURL: "https://api.minimaxi.com/anthropic";
61
61
  };
62
+ readonly "minimax.openai": {
63
+ readonly id: "minimax.openai";
64
+ readonly providerId: "minimax";
65
+ readonly protocol: "openai-chat";
66
+ readonly baseURL: "https://api.minimaxi.com/v1";
67
+ };
62
68
  readonly "deepseek.openai": {
63
69
  readonly id: "deepseek.openai";
64
70
  readonly providerId: "deepseek";
@@ -29,6 +29,12 @@ export const endpointProfiles = {
29
29
  protocol: "anthropic-messages",
30
30
  baseURL: "https://api.minimaxi.com/anthropic",
31
31
  },
32
+ "minimax.openai": {
33
+ id: "minimax.openai",
34
+ providerId: "minimax",
35
+ protocol: "openai-chat",
36
+ baseURL: "https://api.minimaxi.com/v1",
37
+ },
32
38
  "deepseek.openai": {
33
39
  id: "deepseek.openai",
34
40
  providerId: "deepseek",
@@ -1,5 +1,5 @@
1
1
  import OpenAI from "openai";
2
- import type { LLMProvider, Message, RenderedContext, StreamEvent, ToolSchema, RuntimePolicy, ProviderReplay } from "../types.js";
2
+ import type { LLMProvider, Message, ProviderDescriptor, RenderedContext, StreamEvent, ToolSchema, RuntimePolicy, ProviderReplay } from "../types.js";
3
3
  import { CircuitBreaker } from "./base.js";
4
4
  import { OpenAIChatAdapter } from "./openai-chat.js";
5
5
  export declare class QwenProvider implements LLMProvider {
@@ -14,6 +14,8 @@ export declare class QwenProvider implements LLMProvider {
14
14
  baseDelay: number;
15
15
  }, baseURL?: string);
16
16
  runtimePolicy(): RuntimePolicy;
17
+ descriptor(): ProviderDescriptor;
18
+ private buildChatMessages;
17
19
  peekProviderReplay(message: Pick<Message, "content" | "toolCalls">): ProviderReplay | undefined;
18
20
  seedProviderReplay(message: Pick<Message, "content" | "toolCalls">, replay: ProviderReplay): void;
19
21
  complete(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>): Promise<Message>;
@@ -36,6 +36,24 @@ export class QwenProvider {
36
36
  runtimePolicy() {
37
37
  return QWEN_POLICIES[this.model] ?? {};
38
38
  }
39
+ descriptor() {
40
+ return {
41
+ provider: "qwen",
42
+ protocol: "openai-chat",
43
+ model: this.model,
44
+ reasoning: {
45
+ supported: true,
46
+ preserveAcrossToolTurns: true,
47
+ },
48
+ toolCalls: {
49
+ supported: true,
50
+ requiresStrictPairing: true,
51
+ },
52
+ };
53
+ }
54
+ buildChatMessages(context) {
55
+ return this.chat.buildMessages(context, { descriptor: this.descriptor() });
56
+ }
39
57
  peekProviderReplay(message) {
40
58
  const fields = this.chat.peekReplayFields(message);
41
59
  if (!fields || !("reasoning_content" in fields))
@@ -50,7 +68,7 @@ export class QwenProvider {
50
68
  async complete(context, tools, extensions) {
51
69
  if (this.circuit.isOpen())
52
70
  throw new Error("Circuit breaker open");
53
- const msgs = this.chat.buildMessages(context);
71
+ const msgs = this.buildChatMessages(context);
54
72
  const extraBody = this.thinkingExtraBody(extensions);
55
73
  let lastErr;
56
74
  for (let i = 0; i < this.maxRetries; i++) {
@@ -77,7 +95,7 @@ export class QwenProvider {
77
95
  throw lastErr;
78
96
  }
79
97
  async *stream(context, tools, extensions) {
80
- const msgs = this.chat.buildMessages(context);
98
+ const msgs = this.buildChatMessages(context);
81
99
  const toolCallBufs = {};
82
100
  const emittedToolCallIndexes = new Set();
83
101
  let reasoningContent = "";
@@ -0,0 +1,10 @@
1
+ import type { Message, ProviderDescriptor, ProviderReplay, RenderedContext } from "../types.js";
2
+ export declare class ProviderReplayValidationError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ export interface OpenAIChatReplayValidationOptions {
6
+ descriptor?: ProviderDescriptor;
7
+ requireNonEmptyReasoningForToolCalls?: boolean;
8
+ replayForAssistant?: (message: Pick<Message, "content" | "toolCalls">) => ProviderReplay | Record<string, unknown> | undefined;
9
+ }
10
+ export declare function validateOpenAIChatReplay(context: RenderedContext, options?: OpenAIChatReplayValidationOptions): void;
@@ -0,0 +1,57 @@
1
+ export class ProviderReplayValidationError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "ProviderReplayValidationError";
5
+ }
6
+ }
7
+ export function validateOpenAIChatReplay(context, options = {}) {
8
+ validateStrictToolResultPairing(context.turns);
9
+ if (options.requireNonEmptyReasoningForToolCalls) {
10
+ validateReasoningReplayForAssistantToolCalls(context.turns, options);
11
+ }
12
+ }
13
+ function toolResultParts(message) {
14
+ return (message.contentParts ?? [])
15
+ .filter((part) => part.type === "tool_result");
16
+ }
17
+ function validateStrictToolResultPairing(turns) {
18
+ let pendingIds;
19
+ let completedIds = new Set();
20
+ for (const message of turns) {
21
+ if (message.role === "assistant") {
22
+ const toolCalls = message.toolCalls ?? [];
23
+ pendingIds = toolCalls.length ? new Set(toolCalls.map(tc => tc.id)) : undefined;
24
+ completedIds = new Set();
25
+ continue;
26
+ }
27
+ if (message.role !== "tool") {
28
+ pendingIds = undefined;
29
+ completedIds = new Set();
30
+ continue;
31
+ }
32
+ for (const part of toolResultParts(message)) {
33
+ if (!pendingIds?.has(part.callId)) {
34
+ throw new ProviderReplayValidationError(`OpenAI-compatible replay has orphan tool result ${part.callId}: no preceding assistant tool_call with the same id.`);
35
+ }
36
+ if (completedIds.has(part.callId)) {
37
+ throw new ProviderReplayValidationError(`OpenAI-compatible replay has duplicate tool result ${part.callId}.`);
38
+ }
39
+ completedIds.add(part.callId);
40
+ }
41
+ }
42
+ }
43
+ function validateReasoningReplayForAssistantToolCalls(turns, options) {
44
+ const descriptor = options.descriptor;
45
+ for (const message of turns) {
46
+ if (message.role !== "assistant" || !message.toolCalls?.length)
47
+ continue;
48
+ const replay = options.replayForAssistant?.(message);
49
+ const reasoning = typeof replay?.reasoning_content === "string" ? replay.reasoning_content.trim() : "";
50
+ if (!reasoning) {
51
+ const callIds = message.toolCalls.map(tc => tc.id).join(", ");
52
+ const provider = descriptor ? `${descriptor.provider}/${descriptor.model}` : "provider";
53
+ throw new ProviderReplayValidationError(`${provider} replay requires non-empty reasoning_content for assistant tool call turn ${callIds}. ` +
54
+ "Disable thinking, rebuild this history with provider replay, or switch to a provider that can replay this turn.");
55
+ }
56
+ }
57
+ }
@@ -198,6 +198,28 @@ export function kernelObservationToSessionEvent(obs, turn, opts = {}) {
198
198
  memory_id: obs.memory_id ?? "",
199
199
  error: obs.error ?? "",
200
200
  });
201
+ case "workflow_batch_spawned": {
202
+ // Batch metadata persisted for resume recovery; individual nodes are
203
+ // recorded when they complete (via workflow_node_completed).
204
+ const nodes = obs.nodes ?? [];
205
+ return withCategory({
206
+ kind: "workflow_batch_spawned",
207
+ turn: t,
208
+ node_count: nodes.length,
209
+ node_ids: nodes.map((n) => n.agent_id ?? ""),
210
+ });
211
+ }
212
+ case "workflow_completed": {
213
+ const completed = obs.completed ?? [];
214
+ const failed = obs.failed ?? [];
215
+ return withCategory({
216
+ kind: "workflow_completed",
217
+ turn: t,
218
+ completed,
219
+ failed,
220
+ total_nodes: completed.length + failed.length,
221
+ });
222
+ }
201
223
  default:
202
224
  return null;
203
225
  }
@@ -90,6 +90,19 @@ export interface KernelObservation {
90
90
  requires_async_response?: boolean;
91
91
  /** memory_validation_failed (Phase 7). */
92
92
  error?: string;
93
+ /** workflow_batch_spawned: per-node spawn descriptors (agent_id + goal + role/isolation). */
94
+ nodes?: Array<{
95
+ agent_id: string;
96
+ goal: string;
97
+ role: string;
98
+ isolation: string;
99
+ context_inheritance: string;
100
+ model_hint?: string;
101
+ trust?: string;
102
+ }>;
103
+ /** workflow_completed. */
104
+ completed?: string[];
105
+ failed?: string[];
93
106
  }
94
107
  export declare function toolSchemaToKernel(schema: ToolSchema): Record<string, unknown>;
95
108
  export declare function skillMetadataToKernel(skill: SkillMetadata): Record<string, unknown>;
@@ -1,6 +1,13 @@
1
- import type { LLMProvider, Message, ProviderReplay, ToolCall } from "../types.js";
1
+ import type { LLMProvider, Message, ProviderDescriptor, ProviderReplay, ToolCall } from "../types.js";
2
2
  import type { SessionEvent } from "./session-log.js";
3
3
  export declare function assistantReplayKey(message: Pick<Message, "content" | "toolCalls">): string;
4
+ /**
5
+ * A stored replay may only be seeded into a provider speaking the same wire
6
+ * protocol. On a cross-protocol fallback (provider A -> provider B) the
7
+ * incompatible envelope is skipped so B re-serializes neutral context instead
8
+ * of replaying A's protocol-specific shape.
9
+ */
10
+ export declare function isReplayCompatibleWithProvider(replay: ProviderReplay, descriptor: ProviderDescriptor | undefined): boolean;
4
11
  export declare function seedProviderReplayFromEvents(provider: LLMProvider, events: Array<{
5
12
  event: SessionEvent;
6
13
  }>): void;
@@ -1,4 +1,3 @@
1
- import { effectiveProviderReplay } from "./session-repair.js";
2
1
  function sortObjectKeys(val) {
3
2
  if (val === null || typeof val !== "object") {
4
3
  return val;
@@ -34,17 +33,51 @@ export function assistantReplayKey(message) {
34
33
  toolCalls,
35
34
  });
36
35
  }
36
+ /**
37
+ * Infer the wire protocol a stored replay envelope belongs to.
38
+ *
39
+ * New envelopes carry an explicit `protocol`. Legacy envelopes are inferred
40
+ * from their shape: Anthropic persisted `native_blocks`, OpenAI-compatible
41
+ * persisted `reasoning_content` / `reasoning_details`.
42
+ */
43
+ function replayProtocol(replay) {
44
+ if (replay.protocol)
45
+ return replay.protocol;
46
+ if (replay.native_blocks?.length)
47
+ return "anthropic-messages";
48
+ if (replay.reasoning_content != null || replay.reasoning_details !== undefined)
49
+ return "openai-chat";
50
+ return undefined;
51
+ }
52
+ /**
53
+ * A stored replay may only be seeded into a provider speaking the same wire
54
+ * protocol. On a cross-protocol fallback (provider A -> provider B) the
55
+ * incompatible envelope is skipped so B re-serializes neutral context instead
56
+ * of replaying A's protocol-specific shape.
57
+ */
58
+ export function isReplayCompatibleWithProvider(replay, descriptor) {
59
+ if (!descriptor)
60
+ return true;
61
+ const protocol = replayProtocol(replay);
62
+ if (!protocol)
63
+ return true;
64
+ return protocol === descriptor.protocol;
65
+ }
37
66
  export function seedProviderReplayFromEvents(provider, events) {
38
67
  if (!provider.seedProviderReplay)
39
68
  return;
69
+ const descriptor = provider.descriptor?.();
40
70
  for (const { event } of events) {
41
71
  if (event.kind !== "llm_completed")
42
72
  continue;
43
73
  const toolCalls = event.tool_calls ?? [];
44
- const replay = effectiveProviderReplay(event.content, toolCalls, event.provider_replay);
45
- if (!replay)
74
+ const stored = event.provider_replay;
75
+ if (stored && !isReplayCompatibleWithProvider(stored, descriptor))
46
76
  continue;
47
- provider.seedProviderReplay({ content: event.content, toolCalls }, replay);
77
+ // Pass the message even when no replay was persisted: a provider may
78
+ // reconstruct a legacy replay (e.g. Anthropic native_blocks) from the
79
+ // neutral transcript. Providers that cannot reconstruct simply no-op.
80
+ provider.seedProviderReplay({ content: event.content, toolCalls }, stored ?? {});
48
81
  }
49
82
  }
50
83
  export function peekProviderReplay(provider, content, toolCalls) {