@deepstrike/sdk 0.2.7 → 0.2.9

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/index.d.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  export { RuntimeRunner, collectText } from "./runtime/runner.js";
2
2
  export type { RuntimeOptions, SchedulerBudget } from "./runtime/runner.js";
3
3
  export type { MemoryPolicy, MemoryWriteRateLimit, ResourceQuota } from "./kernel.js";
4
- export { createTournament, createLoopUntilDone } from "./kernel.js";
5
- export type { TournamentMatch, TournamentAction, StopConditionSpec, RoundReport, LoopAction, } from "./kernel.js";
6
4
  export { KernelPrimitivesDashboard } from "./runtime/kernel-primitives-dashboard.js";
7
5
  export { FilteredExecutionPlane } from "./runtime/filtered-plane.js";
8
6
  export { SubAgentOrchestrator, defaultSubAgentOrchestrator, spawnStandalone } from "./runtime/sub-agent-orchestrator.js";
@@ -43,6 +41,8 @@ export { endpointProfiles, modelProfiles, getModelProfile } from "./providers/pr
43
41
  export type { ModelProfileId, ProviderId } from "./providers/profiles.js";
44
42
  export { createProvider } from "./providers/catalog.js";
45
43
  export type { CreateProviderOptions, EndpointProfileId } from "./providers/catalog.js";
44
+ export { ProviderReplayValidationError, DEGRADED_REASONING_PLACEHOLDER } from "./providers/replay-validator.js";
45
+ export { assessProviderReplayability, peekProviderReplay, seedProviderReplayFromEvents, isReplayCompatibleWithProvider, } from "./runtime/provider-replay.js";
46
46
  export { tool, streamingTool, executeTools, readFile, validateToolArguments } from "./tools/index.js";
47
47
  export type { RegisteredTool } from "./tools/index.js";
48
48
  export { scanSkillDir, readSkillFile } from "./skills/loader.js";
@@ -59,7 +59,7 @@ export { Governance, governancePolicyToKernelEvent } from "./governance.js";
59
59
  export type { GovernanceVerdict, GovernancePolicy, GovernanceConstraint } from "./governance.js";
60
60
  export { SinglePassHarness, EvalLoopHarness, HarnessLoop } from "./harness/harness.js";
61
61
  export type { HarnessRequest, HarnessOutcome, HarnessLoopOptions, QualityGate } from "./harness/harness.js";
62
- export type { Message, ToolCall, ToolResult, ToolSchema, ContentPart, TextPart, ImagePart, AudioPart, StreamEvent, TextDelta, ThinkingDelta, ToolCallEvent, ToolChunk, ToolDeltaEvent, ToolSuspendEvent, ToolResultEvent, DoneEvent, ErrorEvent, PermissionRequestEvent, PermissionResolvedEvent, PermissionResponse, LLMProvider, RetryConfig, TokenUsage, ProviderToolSpec, ProviderRunState, ProviderReplay, RenderedContext, } from "./types.js";
62
+ export type { Message, ToolCall, ToolResult, ToolSchema, ContentPart, TextPart, ImagePart, AudioPart, StreamEvent, TextDelta, ThinkingDelta, ToolCallEvent, ToolChunk, ToolDeltaEvent, ToolSuspendEvent, ToolResultEvent, DoneEvent, ErrorEvent, PermissionRequestEvent, PermissionResolvedEvent, PermissionResponse, LLMProvider, RetryConfig, TokenUsage, ProviderToolSpec, ProviderRunState, ProviderReplay, RenderedContext, ReplayabilityAssessment, } from "./types.js";
63
63
  export type { AgentCapabilityFilter, AgentIdentity, AgentIsolation, AgentRunSpec, AgentProcessChangedObservation, ContextInheritance, KernelAgentRole, LoopResult, MilestoneCheckResult, MilestoneContract, MilestonePhase, MilestonePolicy, SubAgentResult, TerminationReason, WorkflowSpec, WorkflowNodeSpec, WorkflowTaskSpec, WorkflowSpawnInfo, } from "./types/agent.js";
64
64
  export { agentIdentitySub, agentRunSpecToKernel, milestoneCheckFail, milestoneCheckPass, milestoneCheckResultToKernel, subAgentResultToKernel, workflowSpecToKernel, fanoutSynthesize, generateAndFilter, verifyRules, } from "./types/agent.js";
65
65
  export type { AcceptanceCriterion, VerificationContract, ContractCheckResult, } from "./collaboration/contract.js";
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  // ── Runtime (Layer 1.5) ────────────────────────────────────────────────────
2
2
  export { RuntimeRunner, collectText } from "./runtime/runner.js";
3
- export { createTournament, createLoopUntilDone } from "./kernel.js";
4
3
  export { KernelPrimitivesDashboard } from "./runtime/kernel-primitives-dashboard.js";
5
4
  export { FilteredExecutionPlane } from "./runtime/filtered-plane.js";
6
5
  export { SubAgentOrchestrator, defaultSubAgentOrchestrator, spawnStandalone } from "./runtime/sub-agent-orchestrator.js";
@@ -28,6 +27,8 @@ export { OpenAIChatAdapter } from "./providers/openai-chat.js";
28
27
  export { OpenAIResponsesAdapter, OpenAIResponsesProvider } from "./providers/openai-responses.js";
29
28
  export { endpointProfiles, modelProfiles, getModelProfile } from "./providers/profiles.js";
30
29
  export { createProvider } from "./providers/catalog.js";
30
+ export { ProviderReplayValidationError, DEGRADED_REASONING_PLACEHOLDER } from "./providers/replay-validator.js";
31
+ export { assessProviderReplayability, peekProviderReplay, seedProviderReplayFromEvents, isReplayCompatibleWithProvider, } from "./runtime/provider-replay.js";
31
32
  // ── Tools & Skills ─────────────────────────────────────────────────────────
32
33
  export { tool, streamingTool, executeTools, readFile, validateToolArguments } from "./tools/index.js";
33
34
  export { scanSkillDir, readSkillFile } from "./skills/loader.js";
package/dist/kernel.d.ts CHANGED
@@ -147,54 +147,6 @@ export interface KernelRuntimeInstance {
147
147
  drainNewMessages(): Message[];
148
148
  preservedRefs(): string[];
149
149
  }
150
- /** One pairwise match-up in a tournament round. */
151
- export interface TournamentMatch {
152
- id: number;
153
- left: string;
154
- right: string;
155
- }
156
- /** Discriminated action returned by {@link TournamentInstance} methods. */
157
- export interface TournamentAction {
158
- kind: "judgeRound" | "done";
159
- /** `judgeRound`: 1-based round number. */
160
- round?: number;
161
- /** `judgeRound`: run one fresh-context judge per match (parallelisable). */
162
- matches?: TournamentMatch[];
163
- /** `done`: the winning entrant id. */
164
- winner?: string;
165
- /** `done`: number of rounds played. */
166
- roundsUsed?: number;
167
- }
168
- interface TournamentInstance {
169
- start(): TournamentAction;
170
- feedRound(winners: string[]): TournamentAction;
171
- isDone(): boolean;
172
- }
173
- /** A single loop stop predicate. `maxRounds` is required when `kind === "maxRounds"`. */
174
- export interface StopConditionSpec {
175
- kind: "noNewFindings" | "noErrors" | "maxRounds";
176
- maxRounds?: number;
177
- }
178
- /** What the SDK reports after running a loop round's worker. */
179
- export interface RoundReport {
180
- newFindings: number;
181
- errors: number;
182
- }
183
- /** Discriminated action returned by {@link LoopUntilDoneInstance} methods. */
184
- export interface LoopAction {
185
- kind: "spawn" | "done";
186
- /** `spawn`: 1-based round number to run. */
187
- round?: number;
188
- /** `done`: number of rounds run. */
189
- roundsUsed?: number;
190
- /** `done`: which condition fired. */
191
- reason?: "noNewFindings" | "noErrors" | "maxRounds";
192
- }
193
- interface LoopUntilDoneInstance {
194
- start(): LoopAction;
195
- feed(report: RoundReport): LoopAction;
196
- isDone(): boolean;
197
- }
198
150
  interface KernelModule {
199
151
  Governance: new (defaultAction?: "allow" | "deny" | "ask_user") => GovernanceInstance;
200
152
  KernelRuntime: new (policy: {
@@ -208,12 +160,6 @@ interface KernelModule {
208
160
  extractSkillOnPass?: boolean;
209
161
  }) => EvalPipelineInstance;
210
162
  IdlePipeline: new (agentId: string) => IdlePipelineInstance;
211
- Tournament: new (entrants: string[]) => TournamentInstance;
212
- LoopUntilDone: new (conditions: StopConditionSpec[]) => LoopUntilDoneInstance;
213
163
  }
214
- /** Create a single-elimination tournament (pairwise comparative judging). Throws if `entrants` is empty. */
215
- export declare function createTournament(entrants: string[]): TournamentInstance;
216
- /** Create a loop-until-done state machine. A `maxRounds` backstop is injected if none is given. */
217
- export declare function createLoopUntilDone(conditions: StopConditionSpec[]): LoopUntilDoneInstance;
218
164
  export declare function getKernel(): KernelModule;
219
165
  export {};
package/dist/kernel.js CHANGED
@@ -2,14 +2,6 @@ import { createRequire } from "module";
2
2
  import { existsSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- /** Create a single-elimination tournament (pairwise comparative judging). Throws if `entrants` is empty. */
6
- export function createTournament(entrants) {
7
- return new (getKernel().Tournament)(entrants);
8
- }
9
- /** Create a loop-until-done state machine. A `maxRounds` backstop is injected if none is given. */
10
- export function createLoopUntilDone(conditions) {
11
- return new (getKernel().LoopUntilDone)(conditions);
12
- }
13
5
  const cjsRequire = createRequire(import.meta.url);
14
6
  let cachedKernel;
15
7
  function resolveCoreModule() {
@@ -9,6 +9,12 @@ export declare class CircuitBreaker {
9
9
  recordSuccess(): void;
10
10
  recordFailure(): void;
11
11
  }
12
+ /**
13
+ * Internal control flags that steer DeepStrike's own serialization/validation
14
+ * and must never be forwarded to any provider's wire request, regardless of the
15
+ * per-call omit list.
16
+ */
17
+ export declare const INTERNAL_EXTENSION_KEYS: readonly string[];
12
18
  export declare function omitExtensionKeys(extensions: Record<string, unknown> | undefined, keys: readonly string[]): Record<string, unknown>;
13
19
  export declare function normalizeToolCall(id: string, name: string, args: unknown): {
14
20
  id: string;
@@ -26,10 +26,19 @@ export class CircuitBreaker {
26
26
  this.openedAt = Date.now();
27
27
  }
28
28
  }
29
+ /**
30
+ * Internal control flags that steer DeepStrike's own serialization/validation
31
+ * and must never be forwarded to any provider's wire request, regardless of the
32
+ * per-call omit list.
33
+ */
34
+ export const INTERNAL_EXTENSION_KEYS = [
35
+ "__deepstrikeThinkingEnabled",
36
+ "degradeMissingReasoningReplay",
37
+ ];
29
38
  export function omitExtensionKeys(extensions, keys) {
30
39
  if (!extensions)
31
40
  return {};
32
- const blocked = new Set(keys);
41
+ const blocked = new Set([...keys, ...INTERNAL_EXTENSION_KEYS]);
33
42
  return Object.fromEntries(Object.entries(extensions).filter(([key]) => !blocked.has(key)));
34
43
  }
35
44
  export function normalizeToolCall(id, name, args) {
@@ -43,6 +43,9 @@ export class DeepSeekProvider extends OpenAIChatProvider {
43
43
  const requestExtensions = {
44
44
  ...omitExtensionKeys(extensions, ["thinking", "reasoningEffort", "exposeReasoning", "extra_body", "reasoning_effort"]),
45
45
  __deepstrikeThinkingEnabled: thinkingEnabled,
46
+ // Re-thread the degrade control flag (omitExtensionKeys strips internal
47
+ // keys) so buildChatMessages can honor it; the wire-request omit drops it.
48
+ ...(extensions?.degradeMissingReasoningReplay === true ? { degradeMissingReasoningReplay: true } : {}),
46
49
  reasoning_effort: reasoningEffort,
47
50
  extra_body: { thinking: { type: thinking } },
48
51
  };
@@ -70,6 +70,9 @@ export class MiniMaxOpenAIProvider extends OpenAIChatProvider {
70
70
  return {
71
71
  ...omitExtensionKeys(extensions, ["reasoning_split", "exposeReasoning"]),
72
72
  __deepstrikeThinkingEnabled: reasoningSplit,
73
+ // Re-thread the degrade control flag (omitExtensionKeys strips internal
74
+ // keys) so buildChatMessages can honor it; the wire-request omit drops it.
75
+ ...(extensions?.degradeMissingReasoningReplay === true ? { degradeMissingReasoningReplay: true } : {}),
73
76
  reasoning_split: reasoningSplit,
74
77
  };
75
78
  }
@@ -1,8 +1,15 @@
1
1
  import type OpenAI from "openai";
2
2
  import type { Message, ProviderDescriptor, RenderedContext, ToolSchema } from "../types.js";
3
+ import { type ReplayabilityAssessment } from "./replay-validator.js";
3
4
  export interface OpenAIChatBuildMessageOptions {
4
5
  descriptor?: ProviderDescriptor;
5
6
  requireNonEmptyReasoningForToolCalls?: boolean;
7
+ /**
8
+ * Degrade (rather than throw) when a reasoning-requiring tool-call turn has
9
+ * no stored reasoning replay: a placeholder reasoning is injected so the
10
+ * request still goes out in degraded form.
11
+ */
12
+ degradeMissingReasoning?: boolean;
6
13
  }
7
14
  export declare class OpenAIChatAdapter {
8
15
  private replayFields;
@@ -15,6 +22,11 @@ export declare class OpenAIChatAdapter {
15
22
  };
16
23
  }[];
17
24
  buildMessages(context: RenderedContext, options?: OpenAIChatBuildMessageOptions): OpenAI.ChatCompletionMessageParam[];
25
+ /**
26
+ * Throw-free pre-flight check: which assistant tool-call turns in `context`
27
+ * lack the non-empty reasoning replay a reasoning-requiring provider needs.
28
+ */
29
+ assessReasoning(context: RenderedContext): ReplayabilityAssessment;
18
30
  normalizeToolCalls(toolCalls?: OpenAI.ChatCompletionMessageToolCall[]): Array<{
19
31
  id: string;
20
32
  name: string;
@@ -1,6 +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
+ import { DEGRADED_REASONING_PLACEHOLDER, assessReasoningReplay, validateOpenAIChatReplay, } from "./replay-validator.js";
4
4
  export class OpenAIChatAdapter {
5
5
  replayFields = new Map();
6
6
  buildTools(tools) {
@@ -13,8 +13,10 @@ export class OpenAIChatAdapter {
13
13
  validateOpenAIChatReplay(context, {
14
14
  descriptor: options.descriptor,
15
15
  requireNonEmptyReasoningForToolCalls: options.requireNonEmptyReasoningForToolCalls,
16
+ degradeMissingReasoning: options.degradeMissingReasoning,
16
17
  replayForAssistant: message => this.replayFields.get(assistantReplayKey(message)),
17
18
  });
19
+ const degradeReasoning = Boolean(options.requireNonEmptyReasoningForToolCalls && options.degradeMissingReasoning);
18
20
  // toOpenAIMessageParams prepends systemText as messages[0], then turns.
19
21
  const serialized = toOpenAIMessageParams(context);
20
22
  // Cursor starts at 1 to skip the system message injected by toOpenAIMessageParams.
@@ -26,7 +28,13 @@ export class OpenAIChatAdapter {
26
28
  }
27
29
  if (source.role === "assistant") {
28
30
  const replay = this.replayFields.get(assistantReplayKey(source));
29
- const wireReplay = openAIChatWireReplayFields(replay);
31
+ let wireReplay = openAIChatWireReplayFields(replay);
32
+ if (!wireReplay && degradeReasoning && source.toolCalls?.length) {
33
+ // Reasoning-requiring provider, no stored reasoning for this tool-call
34
+ // turn, caller opted into degradation: inject a placeholder so the
35
+ // wire message stays well-formed instead of failing the whole request.
36
+ wireReplay = { reasoning_content: DEGRADED_REASONING_PLACEHOLDER };
37
+ }
30
38
  if (wireReplay)
31
39
  serialized[cursor] = { ...serialized[cursor], ...wireReplay };
32
40
  }
@@ -34,6 +42,15 @@ export class OpenAIChatAdapter {
34
42
  }
35
43
  return serialized;
36
44
  }
45
+ /**
46
+ * Throw-free pre-flight check: which assistant tool-call turns in `context`
47
+ * lack the non-empty reasoning replay a reasoning-requiring provider needs.
48
+ */
49
+ assessReasoning(context) {
50
+ return assessReasoningReplay(context.turns, {
51
+ replayForAssistant: message => this.replayFields.get(assistantReplayKey(message)),
52
+ });
53
+ }
37
54
  normalizeToolCalls(toolCalls = []) {
38
55
  return toolCalls
39
56
  .filter((tc) => tc.type === "function")
@@ -2,6 +2,7 @@ import OpenAI from "openai";
2
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
+ import type { ReplayabilityAssessment } from "./replay-validator.js";
5
6
  export declare class OpenAIChatProvider implements LLMProvider {
6
7
  protected readonly model: string;
7
8
  protected client: OpenAI;
@@ -16,7 +17,16 @@ export declare class OpenAIChatProvider implements LLMProvider {
16
17
  runtimePolicy(): RuntimePolicy;
17
18
  descriptor(): ProviderDescriptor;
18
19
  protected requireNonEmptyReasoningReplayForToolTurns(_extensions?: Record<string, unknown>): boolean;
20
+ protected degradeMissingReasoningReplay(extensions?: Record<string, unknown>): boolean;
19
21
  protected buildChatMessages(context: RenderedContext, extensions?: Record<string, unknown>): OpenAI.Chat.Completions.ChatCompletionMessageParam[];
22
+ /**
23
+ * Pre-flight query: would this history validate against this provider with the
24
+ * given extensions, without sending the request? Lets an embedder route around
25
+ * a reasoning-replay failure (keep thinking on, disable it, or skip this
26
+ * candidate) before issuing the request. `ok: true` when this provider does
27
+ * not require reasoning replay for the current extensions.
28
+ */
29
+ assessReplayability(context: RenderedContext, extensions?: Record<string, unknown>): ReplayabilityAssessment;
20
30
  peekProviderReplay(message: Pick<Message, "content" | "toolCalls">): ProviderReplay | undefined;
21
31
  seedProviderReplay(message: Pick<Message, "content" | "toolCalls">, replay: ProviderReplay): void;
22
32
  complete(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>): Promise<Message>;
@@ -60,12 +60,29 @@ export class OpenAIChatProvider {
60
60
  requireNonEmptyReasoningReplayForToolTurns(_extensions) {
61
61
  return false;
62
62
  }
63
+ degradeMissingReasoningReplay(extensions) {
64
+ return extensions?.degradeMissingReasoningReplay === true;
65
+ }
63
66
  buildChatMessages(context, extensions) {
64
67
  return this.chat.buildMessages(context, {
65
68
  descriptor: this.descriptor(),
66
69
  requireNonEmptyReasoningForToolCalls: this.requireNonEmptyReasoningReplayForToolTurns(extensions),
70
+ degradeMissingReasoning: this.degradeMissingReasoningReplay(extensions),
67
71
  });
68
72
  }
73
+ /**
74
+ * Pre-flight query: would this history validate against this provider with the
75
+ * given extensions, without sending the request? Lets an embedder route around
76
+ * a reasoning-replay failure (keep thinking on, disable it, or skip this
77
+ * candidate) before issuing the request. `ok: true` when this provider does
78
+ * not require reasoning replay for the current extensions.
79
+ */
80
+ assessReplayability(context, extensions) {
81
+ if (!this.requireNonEmptyReasoningReplayForToolTurns(extensions)) {
82
+ return { ok: true, offendingCallIds: [] };
83
+ }
84
+ return this.chat.assessReasoning(context);
85
+ }
69
86
  peekProviderReplay(message) {
70
87
  const fields = this.chat.peekReplayFields(message);
71
88
  if (!fields || !("reasoning_content" in fields || "reasoning_details" in fields))
@@ -1,10 +1,32 @@
1
- import type { Message, ProviderDescriptor, ProviderReplay, RenderedContext } from "../types.js";
1
+ import type { Message, ProviderDescriptor, ProviderReplay, RenderedContext, ReplayabilityAssessment } from "../types.js";
2
+ export type { ReplayabilityAssessment };
2
3
  export declare class ProviderReplayValidationError extends Error {
3
4
  constructor(message: string);
4
5
  }
6
+ /**
7
+ * Placeholder reasoning injected for an assistant tool-call turn that has no
8
+ * stored reasoning replay when the caller opted into graceful degradation
9
+ * (`degradeMissingReasoning`). It keeps the wire message well-formed for a
10
+ * thinking-on provider without fabricating substantive reasoning.
11
+ */
12
+ export declare const DEGRADED_REASONING_PLACEHOLDER = "[reasoning unavailable on replay]";
5
13
  export interface OpenAIChatReplayValidationOptions {
6
14
  descriptor?: ProviderDescriptor;
7
15
  requireNonEmptyReasoningForToolCalls?: boolean;
16
+ /**
17
+ * When true, an assistant tool-call turn that lacks reasoning replay is
18
+ * degraded (serialized without/with a placeholder reasoning) instead of
19
+ * throwing. Lets a recovery/fallback request succeed in degraded form
20
+ * rather than fail outright.
21
+ */
22
+ degradeMissingReasoning?: boolean;
8
23
  replayForAssistant?: (message: Pick<Message, "content" | "toolCalls">) => ProviderReplay | Record<string, unknown> | undefined;
9
24
  }
10
25
  export declare function validateOpenAIChatReplay(context: RenderedContext, options?: OpenAIChatReplayValidationOptions): void;
26
+ /**
27
+ * Pure, throw-free assessment: which assistant tool-call turns lack the
28
+ * non-empty reasoning replay a reasoning-requiring provider needs. Lets an
29
+ * embedder decide per-candidate whether to keep thinking on, disable it, or
30
+ * skip the candidate — before sending.
31
+ */
32
+ export declare function assessReasoningReplay(turns: Message[], options: Pick<OpenAIChatReplayValidationOptions, "replayForAssistant">): ReplayabilityAssessment;
@@ -4,12 +4,48 @@ export class ProviderReplayValidationError extends Error {
4
4
  this.name = "ProviderReplayValidationError";
5
5
  }
6
6
  }
7
+ /**
8
+ * Placeholder reasoning injected for an assistant tool-call turn that has no
9
+ * stored reasoning replay when the caller opted into graceful degradation
10
+ * (`degradeMissingReasoning`). It keeps the wire message well-formed for a
11
+ * thinking-on provider without fabricating substantive reasoning.
12
+ */
13
+ export const DEGRADED_REASONING_PLACEHOLDER = "[reasoning unavailable on replay]";
7
14
  export function validateOpenAIChatReplay(context, options = {}) {
8
15
  validateStrictToolResultPairing(context.turns);
9
- if (options.requireNonEmptyReasoningForToolCalls) {
10
- validateReasoningReplayForAssistantToolCalls(context.turns, options);
16
+ if (options.requireNonEmptyReasoningForToolCalls && !options.degradeMissingReasoning) {
17
+ const assessment = assessReasoningReplay(context.turns, options);
18
+ if (!assessment.ok) {
19
+ throw reasoningReplayError(assessment.offendingCallIds, options.descriptor);
20
+ }
11
21
  }
12
22
  }
23
+ /**
24
+ * Pure, throw-free assessment: which assistant tool-call turns lack the
25
+ * non-empty reasoning replay a reasoning-requiring provider needs. Lets an
26
+ * embedder decide per-candidate whether to keep thinking on, disable it, or
27
+ * skip the candidate — before sending.
28
+ */
29
+ export function assessReasoningReplay(turns, options) {
30
+ const offendingCallIds = [];
31
+ for (const message of turns) {
32
+ if (message.role !== "assistant" || !message.toolCalls?.length)
33
+ continue;
34
+ const replay = options.replayForAssistant?.(message);
35
+ const reasoning = typeof replay?.reasoning_content === "string" ? replay.reasoning_content.trim() : "";
36
+ if (!reasoning) {
37
+ for (const tc of message.toolCalls)
38
+ offendingCallIds.push(tc.id);
39
+ }
40
+ }
41
+ return { ok: offendingCallIds.length === 0, offendingCallIds };
42
+ }
43
+ function reasoningReplayError(callIds, descriptor) {
44
+ const provider = descriptor ? `${descriptor.provider}/${descriptor.model}` : "provider";
45
+ return new ProviderReplayValidationError(`${provider} replay requires non-empty reasoning_content for assistant tool call turn ${callIds.join(", ")}. ` +
46
+ "Disable thinking, rebuild this history with provider replay, switch to a provider that can replay this turn, " +
47
+ "or pass extensions.degradeMissingReasoningReplay to send a degraded turn.");
48
+ }
13
49
  function toolResultParts(message) {
14
50
  return (message.contentParts ?? [])
15
51
  .filter((part) => part.type === "tool_result");
@@ -17,14 +53,25 @@ function toolResultParts(message) {
17
53
  function validateStrictToolResultPairing(turns) {
18
54
  let pendingIds;
19
55
  let completedIds = new Set();
56
+ const assertAllCompleted = () => {
57
+ if (!pendingIds)
58
+ return;
59
+ const missing = [...pendingIds].filter(id => !completedIds.has(id));
60
+ if (missing.length) {
61
+ throw new ProviderReplayValidationError(`OpenAI-compatible replay has assistant tool_calls with no tool result for ${missing.join(", ")}: ` +
62
+ "every tool_call must be answered by a tool message before the next assistant or user turn.");
63
+ }
64
+ };
20
65
  for (const message of turns) {
21
66
  if (message.role === "assistant") {
67
+ assertAllCompleted();
22
68
  const toolCalls = message.toolCalls ?? [];
23
69
  pendingIds = toolCalls.length ? new Set(toolCalls.map(tc => tc.id)) : undefined;
24
70
  completedIds = new Set();
25
71
  continue;
26
72
  }
27
73
  if (message.role !== "tool") {
74
+ assertAllCompleted();
28
75
  pendingIds = undefined;
29
76
  completedIds = new Set();
30
77
  continue;
@@ -39,19 +86,5 @@ function validateStrictToolResultPairing(turns) {
39
86
  completedIds.add(part.callId);
40
87
  }
41
88
  }
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
- }
89
+ assertAllCompleted();
57
90
  }
@@ -1,4 +1,4 @@
1
- import type { LLMProvider, Message, ProviderDescriptor, ProviderReplay, ToolCall } from "../types.js";
1
+ import type { LLMProvider, Message, ProviderDescriptor, ProviderReplay, RenderedContext, ReplayabilityAssessment, 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
4
  /**
@@ -12,3 +12,11 @@ export declare function seedProviderReplayFromEvents(provider: LLMProvider, even
12
12
  event: SessionEvent;
13
13
  }>): void;
14
14
  export declare function peekProviderReplay(provider: LLMProvider, content: string, toolCalls: ToolCall[]): ProviderReplay | undefined;
15
+ /**
16
+ * Pre-flight query for fallback routing: would `context` validate against
17
+ * `provider` (with `extensions`) before the request is sent? Seed any persisted
18
+ * replay (via `seedProviderReplayFromEvents`) first so the assessment reflects
19
+ * what the provider can actually replay. Providers that do not implement
20
+ * `assessReplayability` (no reasoning-replay requirement) are reported as ok.
21
+ */
22
+ export declare function assessProviderReplayability(provider: LLMProvider, context: RenderedContext, extensions?: Record<string, unknown>): ReplayabilityAssessment;
@@ -83,3 +83,13 @@ export function seedProviderReplayFromEvents(provider, events) {
83
83
  export function peekProviderReplay(provider, content, toolCalls) {
84
84
  return provider.peekProviderReplay?.({ content, toolCalls });
85
85
  }
86
+ /**
87
+ * Pre-flight query for fallback routing: would `context` validate against
88
+ * `provider` (with `extensions`) before the request is sent? Seed any persisted
89
+ * replay (via `seedProviderReplayFromEvents`) first so the assessment reflects
90
+ * what the provider can actually replay. Providers that do not implement
91
+ * `assessReplayability` (no reasoning-replay requirement) are reported as ok.
92
+ */
93
+ export function assessProviderReplayability(provider, context, extensions) {
94
+ return provider.assessReplayability?.(context, extensions) ?? { ok: true, offendingCallIds: [] };
95
+ }
@@ -276,15 +276,17 @@ export class RuntimeRunner {
276
276
  // W0-ABI resume: skip nodes already completed before an interruption.
277
277
  ...(opts?.resumedCompleted?.length ? { resumed_completed: opts.resumedCompleted } : {}),
278
278
  });
279
+ const collectNodes = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")
280
+ ?.nodes ?? [];
281
+ const findDone = (obs) => obs.find(o => o.kind === "workflow_completed");
282
+ let done = findDone(observations);
283
+ if (done)
284
+ return { completed: done.completed ?? [], failed: done.failed ?? [] };
285
+ let nodes = collectNodes(observations);
279
286
  for (;;) {
280
- const done = observations.find(o => o.kind === "workflow_completed");
281
- if (done)
282
- return { completed: done.completed ?? [], failed: done.failed ?? [] };
283
- const batch = observations.find(o => o.kind === "workflow_batch_spawned");
284
- const nodes = batch?.nodes ?? [];
285
287
  if (nodes.length === 0)
286
288
  return { completed: [], failed: [] }; // nothing to run (e.g. all gated)
287
- // Run the batch's nodes in parallel — each is independent within a round.
289
+ // Run the currently-runnable nodes in parallel — each is independent within a round.
288
290
  const results = await Promise.all(nodes.map(node => orchestrator.run({
289
291
  parentOpts: this.opts,
290
292
  parentSessionId,
@@ -293,13 +295,23 @@ export class RuntimeRunner {
293
295
  sessionLog: this.opts.sessionLog,
294
296
  ...(this.opts.subAgentHarness ? { harness: this.opts.subAgentHarness } : {}),
295
297
  })));
296
- // Feed completions back; the draining feed yields the next batch or completion.
297
- observations = [];
298
+ // Feed completions back one at a time. The kernel's run-queue executor may spawn a node's
299
+ // dependents the moment *that* node completes (per-node unblock), so each feed can emit its
300
+ // own `workflow_batch_spawned`; ACCUMULATE them across the round rather than keeping only the
301
+ // last feed's (the old code overwrote `observations` per feed and dropped nodes unblocked by
302
+ // earlier completions — stalling uneven DAGs). Completion and new spawns are mutually
303
+ // exclusive per feed, so a `workflow_completed` only arrives once nothing remains to run.
304
+ const nextNodes = [];
305
+ done = undefined;
298
306
  for (const result of results) {
299
- observations = kernelApply(runtime, this.pendingObservations, {
307
+ const obs = kernelApply(runtime, this.pendingObservations, {
300
308
  kind: "sub_agent_completed",
301
309
  result: subAgentResultToKernel(result),
302
310
  });
311
+ nextNodes.push(...collectNodes(obs));
312
+ const d = findDone(obs);
313
+ if (d)
314
+ done = d;
303
315
  // Persist node completion for resume recovery.
304
316
  await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodeCompletedEvent({
305
317
  turn: runtime.turn(),
@@ -307,6 +319,10 @@ export class RuntimeRunner {
307
319
  termination: result.result.termination,
308
320
  }));
309
321
  }
322
+ if (done && nextNodes.length === 0) {
323
+ return { completed: done.completed ?? [], failed: done.failed ?? [] };
324
+ }
325
+ nodes = nextNodes;
310
326
  }
311
327
  }
312
328
  /**
package/dist/types.d.ts CHANGED
@@ -213,6 +213,13 @@ export interface ProviderReplay {
213
213
  native_message?: unknown;
214
214
  tool_calls?: unknown[];
215
215
  }
216
+ /** Result of a pre-flight reasoning-replay assessment for a target provider. */
217
+ export interface ReplayabilityAssessment {
218
+ /** True when every reasoning-requiring tool-call turn has replay available. */
219
+ ok: boolean;
220
+ /** Tool-call ids whose turn lacks the required non-empty reasoning replay. */
221
+ offendingCallIds: string[];
222
+ }
216
223
  /** Structured render output produced by the kernel for each LLM call. */
217
224
  export interface RenderedContext {
218
225
  /** Identity + Knowledge combined — for providers with a single system slot (OpenAI). */
@@ -247,6 +254,14 @@ export interface LLMProvider {
247
254
  peekProviderReplay?(message: Pick<Message, "content" | "toolCalls">): ProviderReplay | undefined;
248
255
  /** Restore provider-native replay fields when rebuilding history from SessionLog. */
249
256
  seedProviderReplay?(message: Pick<Message, "content" | "toolCalls">, replay: ProviderReplay): void;
257
+ /**
258
+ * Pre-flight query: would this history validate against this provider with the
259
+ * given extensions, without sending the request? Returns the tool-call ids
260
+ * whose turn lacks the reasoning replay this provider requires, so an embedder
261
+ * can route around the failure (keep thinking on, disable it, or skip this
262
+ * candidate) before issuing the request. Seed any persisted replay first.
263
+ */
264
+ assessReplayability?(context: RenderedContext, extensions?: Record<string, unknown>): ReplayabilityAssessment;
250
265
  complete(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>): Promise<Message>;
251
266
  stream(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>, state?: ProviderRunState): AsyncIterable<StreamEvent>;
252
267
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/sdk",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "DeepStrike Node.js SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@anthropic-ai/sdk": "^0.99.0",
23
- "@deepstrike/core": "0.2.7",
23
+ "@deepstrike/core": "0.2.9",
24
24
  "@google/generative-ai": "^0.24.1",
25
25
  "openai": "^5.23.2"
26
26
  },