Skip to content

Commit f8f3391

Browse files
committed
feat(sampling): require JSON responses and log model/role
Add model and role fields to LLMResponse and capture stopReason for better observability. Initialize conversationHistory with an initial user prompt that enforces returning raw JSON (Claude requirement) and ensures at least one message. Increase maxTokens to 55_000 to allow larger responses. Implement minimal self-healing: when parsed responses lack required fields (action or decision), re-prompt for JSON-only output and continue the loop. Pass model, role, and stopReason into logIterationProgress and record them on iteration spans (including stopReason when present). Also add a default user message for the summary call. These changes improve robustness against malformed LLM outputs and surface model metadata for debugging and tracing.
1 parent 845ec2f commit f8f3391

File tree

1 file changed

+55
-5
lines changed

1 file changed

+55
-5
lines changed

packages/core/src/executors/sampling/base-sampling-executor.ts

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export interface ResponseContent {
3636
export interface LLMResponse {
3737
content: ResponseContent[];
3838
stopReason?: string;
39+
model: string;
40+
role: "user" | "assistant";
3941
}
4042

4143
export interface ExternalTool {
@@ -95,8 +97,16 @@ export abstract class BaseSamplingExecutor {
9597
schema: Record<string, unknown>,
9698
state?: TState,
9799
) {
98-
// Initialize conversation
99-
this.conversationHistory = [];
100+
// Initialize conversation with an initial user message
101+
// Ensure at least one message (Claude requirement) and enforce JSON-only output
102+
this.conversationHistory = [{
103+
role: "user",
104+
content: {
105+
type: "text",
106+
text:
107+
'Return ONLY raw JSON (no code fences or explanations). The JSON MUST include action and decision. Example: {"action":"<tool>","decision":"proceed|complete","<tool>":{}}',
108+
},
109+
}];
100110

101111
// Create a root span for the entire sampling loop
102112
const loopSpan: Span | null = this.tracingEnabled
@@ -133,10 +143,13 @@ export abstract class BaseSamplingExecutor {
133143
const response = await this.server.createMessage({
134144
systemPrompt: systemPrompt(),
135145
messages: this.conversationHistory,
136-
maxTokens: Number.MAX_SAFE_INTEGER,
146+
maxTokens: 55_000,
137147
});
138148

139149
const responseContent = (response.content.text as string) || "{}";
150+
const model = response.model;
151+
const stopReason = response.stopReason;
152+
const role = response.role;
140153

141154
// Parse JSON response
142155
let parsedData: Record<string, unknown>;
@@ -184,14 +197,34 @@ export abstract class BaseSamplingExecutor {
184197
}
185198
}
186199

200+
// Minimal self-healing: ensure required fields exist
201+
if (!action || typeof parsedData["decision"] !== "string") {
202+
this.conversationHistory.push({
203+
role: "user",
204+
content: {
205+
type: "text",
206+
text:
207+
'Required fields missing: action or decision. Return ONLY raw JSON, no code fences or explanations. Example: {"action":"<tool>","decision":"proceed|complete","<tool>":{}}',
208+
},
209+
});
210+
if (iterationSpan) endSpan(iterationSpan);
211+
continue;
212+
}
213+
187214
// Process the parsed data using subclass implementation
188215
const result = await this.processAction(
189216
parsedData,
190217
schema,
191218
state,
192219
loopSpan,
193220
);
194-
this.logIterationProgress(parsedData, result);
221+
this.logIterationProgress(
222+
parsedData,
223+
result,
224+
model,
225+
stopReason,
226+
role,
227+
);
195228

196229
if (iterationSpan) {
197230
// Simplified: store full raw JSON, raw LLM response, and full tool result if present (no truncation)
@@ -210,7 +243,12 @@ export abstract class BaseSamplingExecutor {
210243
action: typeof action === "string" ? action : String(action),
211244
samplingResponse: responseContent,
212245
toolResult: JSON.stringify(result),
246+
model: model,
247+
role: role,
213248
};
249+
if (stopReason) {
250+
attr.stopReason = stopReason;
251+
}
214252
iterationSpan.setAttributes(attr);
215253
}
216254

@@ -374,7 +412,13 @@ Actions Taken: (high-level flow)
374412
Errors/Warnings: (if any)
375413
376414
${history}`,
377-
messages: [],
415+
messages: [{
416+
role: "user",
417+
content: {
418+
type: "text",
419+
text: "Please provide a concise summary.",
420+
},
421+
}],
378422
maxTokens: 3000,
379423
});
380424

@@ -442,13 +486,19 @@ ${history}`,
442486
protected logIterationProgress(
443487
parsedData: Record<string, unknown>,
444488
result: CallToolResult,
489+
model?: string,
490+
stopReason?: string,
491+
role?: string,
445492
): void {
446493
// Log iteration progress using MCP logging
447494
this.logger.debug({
448495
iteration: `${this.currentIteration + 1}/${this.maxIterations}`,
449496
parsedData,
450497
isError: result.isError,
451498
isComplete: result.isComplete,
499+
model,
500+
stopReason,
501+
role,
452502
result: inspect(result, {
453503
depth: 5,
454504
maxArrayLength: 10,

0 commit comments

Comments
 (0)