wip: desparate models fucking around
This commit is contained in:
@@ -7,7 +7,8 @@ import type {
|
||||
ProtocolValidationError,
|
||||
} from './types';
|
||||
import { validateProtocolResponseEnvelope } from './validator';
|
||||
import { extractAssistantUiSpec } from './uiSpecParser';
|
||||
import { extractAssistantUiSpec, normalizeAssistantUiSpec } from './uiSpecParser';
|
||||
import { assistantPanelSpecSchema } from './uiSchema';
|
||||
import { resolveActionPolicy } from '../policy/actionPolicy';
|
||||
|
||||
export interface ProtocolResponseBuildInput {
|
||||
@@ -30,7 +31,8 @@ export class ProtocolResponseBuilder {
|
||||
|
||||
const directEnvelope = this.parseCanonicalEnvelope(input.rawAssistantOutput);
|
||||
if (directEnvelope) {
|
||||
const normalizedDirectEnvelope = this.applyActionPolicies(directEnvelope);
|
||||
const sanitizedDirectEnvelope = this.sanitizeUiPayload(directEnvelope, warnings);
|
||||
const normalizedDirectEnvelope = this.applyActionPolicies(sanitizedDirectEnvelope);
|
||||
const { filteredEnvelope, warnings: capabilityWarnings } = this.applyCapabilityGuards(normalizedDirectEnvelope, input.capabilities);
|
||||
warnings.push(...capabilityWarnings);
|
||||
const validated = validateProtocolResponseEnvelope(filteredEnvelope);
|
||||
@@ -55,7 +57,8 @@ export class ProtocolResponseBuilder {
|
||||
|
||||
const repaired = this.repairRawEnvelope(input.rawAssistantOutput);
|
||||
if (repaired) {
|
||||
const normalizedRepairedEnvelope = this.applyActionPolicies(repaired);
|
||||
const sanitizedRepairedEnvelope = this.sanitizeUiPayload(repaired, warnings);
|
||||
const normalizedRepairedEnvelope = this.applyActionPolicies(sanitizedRepairedEnvelope);
|
||||
const { filteredEnvelope, warnings: capabilityWarnings } = this.applyCapabilityGuards(normalizedRepairedEnvelope, input.capabilities);
|
||||
warnings.push(...capabilityWarnings);
|
||||
const validated = validateProtocolResponseEnvelope(filteredEnvelope);
|
||||
@@ -88,7 +91,8 @@ export class ProtocolResponseBuilder {
|
||||
traceId: randomUUID(),
|
||||
};
|
||||
|
||||
const normalizedBaseEnvelope = this.applyActionPolicies(baseEnvelope);
|
||||
const sanitizedBaseEnvelope = this.sanitizeUiPayload(baseEnvelope, warnings);
|
||||
const normalizedBaseEnvelope = this.applyActionPolicies(sanitizedBaseEnvelope);
|
||||
const { filteredEnvelope, warnings: capabilityWarnings } = this.applyCapabilityGuards(normalizedBaseEnvelope, input.capabilities);
|
||||
warnings.push(...capabilityWarnings);
|
||||
|
||||
@@ -112,9 +116,48 @@ export class ProtocolResponseBuilder {
|
||||
};
|
||||
}
|
||||
|
||||
private sanitizeUiPayload(envelope: ProtocolResponseEnvelope, warnings: string[]): ProtocolResponseEnvelope {
|
||||
if (!envelope.ui) {
|
||||
return envelope;
|
||||
}
|
||||
|
||||
const parsedUi = assistantPanelSpecSchema.safeParse(envelope.ui);
|
||||
if (parsedUi.success) {
|
||||
return {
|
||||
...envelope,
|
||||
ui: parsedUi.data,
|
||||
};
|
||||
}
|
||||
|
||||
const normalizedUi = normalizeAssistantUiSpec(envelope.ui);
|
||||
if (normalizedUi) {
|
||||
warnings.push('Normalized non-canonical ui payload to canonical AGUI schema');
|
||||
return {
|
||||
...envelope,
|
||||
ui: normalizedUi,
|
||||
};
|
||||
}
|
||||
|
||||
warnings.push('Invalid ui payload removed from response envelope');
|
||||
return {
|
||||
...envelope,
|
||||
ui: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private extractJsonFromMarkdown(raw: string): string {
|
||||
const trimmed = raw.trim();
|
||||
const match = trimmed.match(/```(?:[a-zA-Z0-9_-]+)?\s*([\s\S]*?)```/i);
|
||||
if (match) {
|
||||
return match[1].trim();
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
private parseCanonicalEnvelope(raw: string): ProtocolResponseEnvelope | null {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
const jsonString = this.extractJsonFromMarkdown(raw);
|
||||
const parsed = JSON.parse(jsonString);
|
||||
const validated = validateProtocolResponseEnvelope(parsed);
|
||||
return validated.ok && validated.value ? validated.value : null;
|
||||
} catch {
|
||||
@@ -124,14 +167,16 @@ export class ProtocolResponseBuilder {
|
||||
|
||||
private repairRawEnvelope(raw: string): ProtocolResponseEnvelope | null {
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||
const jsonString = this.extractJsonFromMarkdown(raw);
|
||||
const parsed = JSON.parse(jsonString) as Record<string, unknown>;
|
||||
const looksLikeEnvelope = Boolean(
|
||||
parsed.assistantText
|
||||
|| parsed.assistant_text
|
||||
|| parsed.intent
|
||||
|| parsed.needsInput
|
||||
|| parsed.needs_input
|
||||
|| parsed.actions,
|
||||
|| parsed.actions
|
||||
|| parsed.ui,
|
||||
);
|
||||
|
||||
if (!looksLikeEnvelope) {
|
||||
|
||||
@@ -9,15 +9,39 @@ function toRecord(value: unknown): Record<string, unknown> | null {
|
||||
}
|
||||
|
||||
function normalizeChartElement(record: Record<string, unknown>): Record<string, unknown> {
|
||||
const chartType = record.chartType;
|
||||
const normalized: Record<string, unknown> = {
|
||||
...record,
|
||||
type: 'chart',
|
||||
chartType: chartType === 'line' || chartType === 'pie' ? chartType : 'bar',
|
||||
};
|
||||
|
||||
const dataRecord = toRecord(record.data);
|
||||
if (Array.isArray(record.series)) {
|
||||
return normalized;
|
||||
if (typeof record.title === 'string' && record.title.trim().length > 0) {
|
||||
normalized.title = record.title;
|
||||
}
|
||||
|
||||
if (Array.isArray(record.series)) {
|
||||
const series = record.series
|
||||
.map((entry) => {
|
||||
const item = toRecord(entry);
|
||||
if (!item || typeof item.label !== 'string' || typeof item.value !== 'number') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
};
|
||||
})
|
||||
.filter((entry): entry is { label: string; value: number } => Boolean(entry));
|
||||
|
||||
if (series.length > 0) {
|
||||
normalized.series = series;
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
const dataRecord = toRecord(record.data);
|
||||
|
||||
if (!dataRecord) {
|
||||
return normalized;
|
||||
}
|
||||
@@ -43,7 +67,6 @@ function normalizeChartElement(record: Record<string, unknown>): Record<string,
|
||||
}
|
||||
|
||||
normalized.series = series;
|
||||
delete normalized.data;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
@@ -106,6 +129,10 @@ function normalizeElement(value: unknown): Record<string, unknown> | null {
|
||||
}
|
||||
|
||||
const type = typeof record.type === 'string' ? record.type : '';
|
||||
if (type === 'text' && typeof record.content === 'string' && typeof record.text !== 'string') {
|
||||
return { type: 'text', text: record.content };
|
||||
}
|
||||
|
||||
if (type === 'markdown') {
|
||||
const textValue = typeof record.content === 'string'
|
||||
? record.content
|
||||
@@ -145,6 +172,10 @@ function normalizeCandidate(parsed: unknown): AssistantPanelSpec | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (record.protocolVersion === '2.0' && record.ui) {
|
||||
return normalizeCandidate(record.ui);
|
||||
}
|
||||
|
||||
if (record.type === 'tab' && record.content) {
|
||||
return normalizeCandidate(record.content);
|
||||
}
|
||||
@@ -202,6 +233,10 @@ function parseSpecCandidate(raw: string): AssistantPanelSpec | null {
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeAssistantUiSpec(input: unknown): AssistantPanelSpec | null {
|
||||
return normalizeCandidate(input);
|
||||
}
|
||||
|
||||
export interface ParsedAssistantUiResult {
|
||||
assistantText: string;
|
||||
ui: AssistantPanelSpec | null;
|
||||
@@ -210,9 +245,9 @@ export interface ParsedAssistantUiResult {
|
||||
export function extractAssistantUiSpec(message: string): ParsedAssistantUiResult {
|
||||
const trimmed = message.trim();
|
||||
|
||||
const fencedMatches = [...trimmed.matchAll(/```(json)?\s*([\s\S]*?)```/gi)];
|
||||
const fencedMatches = [...trimmed.matchAll(/```(?:[a-zA-Z0-9_-]+)?\s*([\s\S]*?)```/gi)];
|
||||
for (const match of fencedMatches) {
|
||||
const candidate = match[2]?.trim();
|
||||
const candidate = match[1]?.trim();
|
||||
if (!candidate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ Agentic UI Contract:
|
||||
- You may include structured UI payloads in your assistant response so the app can render interactive widgets.
|
||||
- You DO have the ability to return interactive AGUI payloads (including bar charts) as JSON, even though you cannot draw bitmap images.
|
||||
- When the user asks for a chart or guided workflow, prefer returning a valid AGUI payload over refusing.
|
||||
- Use JSON with specVersion: "1" and an elements array.
|
||||
- Place the AGUI payload in the "ui" field of the protocol response envelope. DO NOT output markdown code blocks containing JSON.
|
||||
- Prefer actionable widgets (cards, forms, tabs, inputs, metrics, tables, charts) when they reduce follow-up friction.
|
||||
- Keep textual guidance and UI semantically consistent.
|
||||
- Include only valid, supported action names. Supported actions include: openSettings, openPost, openMedia, openPanel, setActiveView, toggleSidebar, togglePanel, toggleAssistantSidebar.
|
||||
|
||||
@@ -18,7 +18,7 @@ import { MediaEngine } from './MediaEngine';
|
||||
import { getPostMediaEngine } from './PostMediaEngine';
|
||||
import { ProtocolResponseBuilder } from '../agentic/protocol/responseBuilder';
|
||||
import { CapabilityRegistryService } from '../agentic/capabilities/registry';
|
||||
import { validateProtocolRequestEnvelope } from '../agentic/protocol/validator';
|
||||
import { validateProtocolRequestEnvelope, validateProtocolResponseEnvelope } from '../agentic/protocol/validator';
|
||||
import type { ProtocolResponseEnvelope } from '../agentic/protocol/types';
|
||||
import { AgentTurnStateMachine, type AgentTurnState } from '../agentic/workflow/turnStateMachine';
|
||||
import { WorkflowCheckpointStore } from '../agentic/workflow/checkpointStore';
|
||||
@@ -149,6 +149,15 @@ export class OpenCodeManager {
|
||||
private apiKey: string = '';
|
||||
private abortControllers: Map<string, AbortController> = new Map();
|
||||
|
||||
private readonly protocolBoundaryInstructions = `Protocol response requirements (strict):
|
||||
- Return a single JSON object that matches this exact envelope schema:
|
||||
{"protocolVersion":"2.0","assistantText":"string","ui":{"specVersion":"1","elements":[]}?,"intent":"analyze|ask_input|propose_action|execute_action|summarize","needsInput":{"required":boolean,"fields":[]},"actions":[],"confidence":number,"traceId":"string"}
|
||||
- Do not return any top-level shape other than this envelope.
|
||||
- Do not use legacy top-level keys like title/widgets/tabs/content/data/widgets.
|
||||
- ui, if present, must use specVersion "1" and canonical element structures only.
|
||||
- DO NOT output markdown code blocks containing JSON. The entire response must be the JSON envelope.
|
||||
- If uncertain, return an envelope with assistantText and empty actions/ui rather than alternative JSON formats.`;
|
||||
|
||||
constructor(
|
||||
chatEngine: ChatEngine,
|
||||
postEngine: PostEngine,
|
||||
@@ -294,6 +303,7 @@ export class OpenCodeManager {
|
||||
// Get system prompt
|
||||
const systemMessage = conversation.messages.find(m => m.role === 'system');
|
||||
const systemPrompt = systemMessage?.content || await this.chatEngine.getDefaultSystemPrompt();
|
||||
const protocolSystemPrompt = `${systemPrompt}\n\n${this.protocolBoundaryInstructions}`;
|
||||
|
||||
// Build message history from DB (excluding system messages)
|
||||
const dbMessages = conversation.messages.filter(m => m.role !== 'system');
|
||||
@@ -339,29 +349,34 @@ export class OpenCodeManager {
|
||||
let fullResponse = '';
|
||||
const toolCallsCollected: Array<{ name: string; args: unknown }> = [];
|
||||
|
||||
const requestProvider = async (
|
||||
prompt: string,
|
||||
messages: Array<{ role: string; content?: string; toolCalls?: string; toolCallId?: string }>,
|
||||
) => {
|
||||
if (provider === 'anthropic') {
|
||||
return this.sendAnthropicMessage(
|
||||
modelId,
|
||||
prompt,
|
||||
messages,
|
||||
abortController.signal,
|
||||
{ onDelta, onToolCall, onToolResult },
|
||||
);
|
||||
}
|
||||
|
||||
return this.sendOpenAIMessage(
|
||||
modelId,
|
||||
prompt,
|
||||
messages,
|
||||
abortController.signal,
|
||||
{ onDelta, onToolCall, onToolResult },
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
console.log('[OpenCodeManager] Sending to provider:', provider, 'model:', modelId);
|
||||
if (provider === 'anthropic') {
|
||||
const result = await this.sendAnthropicMessage(
|
||||
modelId,
|
||||
systemPrompt,
|
||||
dbMessages,
|
||||
abortController.signal,
|
||||
{ onDelta, onToolCall, onToolResult }
|
||||
);
|
||||
fullResponse = result.content;
|
||||
toolCallsCollected.push(...result.toolCalls);
|
||||
} else {
|
||||
const result = await this.sendOpenAIMessage(
|
||||
modelId,
|
||||
systemPrompt,
|
||||
dbMessages,
|
||||
abortController.signal,
|
||||
{ onDelta, onToolCall, onToolResult }
|
||||
);
|
||||
fullResponse = result.content;
|
||||
toolCallsCollected.push(...result.toolCalls);
|
||||
}
|
||||
const firstResult = await requestProvider(protocolSystemPrompt, dbMessages);
|
||||
fullResponse = firstResult.content;
|
||||
toolCallsCollected.push(...firstResult.toolCalls);
|
||||
console.log('[OpenCodeManager] fullResponse length:', fullResponse.length);
|
||||
} catch (error) {
|
||||
console.error('[OpenCodeManager] Request error:', (error as Error).message);
|
||||
@@ -374,23 +389,55 @@ export class OpenCodeManager {
|
||||
this.abortControllers.delete(conversationId);
|
||||
}
|
||||
|
||||
// Save assistant response (including partial content from aborted requests)
|
||||
if (fullResponse) {
|
||||
await this.chatEngine.addMessage({
|
||||
conversationId,
|
||||
role: 'assistant',
|
||||
content: fullResponse,
|
||||
toolCalls: toolCallsCollected.length > 0 ? JSON.stringify(toolCallsCollected) : undefined,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
}
|
||||
const isCanonicalProtocolEnvelope = (() => {
|
||||
try {
|
||||
const parsed = JSON.parse(fullResponse);
|
||||
const validated = validateProtocolResponseEnvelope(parsed);
|
||||
return validated.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
const protocolResult = this.protocolResponseBuilder.build({
|
||||
let protocolResult = this.protocolResponseBuilder.build({
|
||||
rawAssistantOutput: fullResponse,
|
||||
surface,
|
||||
capabilities,
|
||||
});
|
||||
|
||||
if (!isCanonicalProtocolEnvelope && fullResponse.trim().length > 0 && !abortController.signal.aborted) {
|
||||
const retryReason = protocolResult.validationError?.message || 'previous output was not a canonical protocol envelope';
|
||||
const retryPrompt = `Your previous output failed protocol validation: ${retryReason}.\nReturn ONLY one valid protocol envelope JSON object and nothing else.`;
|
||||
const retryMessages = [
|
||||
...dbMessages,
|
||||
{
|
||||
conversationId,
|
||||
role: 'assistant',
|
||||
content: fullResponse,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
{
|
||||
conversationId,
|
||||
role: 'user',
|
||||
content: retryPrompt,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
try {
|
||||
const retryResult = await requestProvider(protocolSystemPrompt, retryMessages);
|
||||
fullResponse = retryResult.content;
|
||||
toolCallsCollected.push(...retryResult.toolCalls);
|
||||
protocolResult = this.protocolResponseBuilder.build({
|
||||
rawAssistantOutput: fullResponse,
|
||||
surface,
|
||||
capabilities,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[OpenCodeManager] Protocol retry failed:', (error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
const previousCheckpoint = await this.workflowCheckpointStore.load(conversationId);
|
||||
const previousState: AgentTurnState = previousCheckpoint?.state || 'planning';
|
||||
const nextState = this.turnStateMachine.transition({
|
||||
@@ -417,6 +464,17 @@ export class OpenCodeManager {
|
||||
blockedActions: blockedActionWarnings.length,
|
||||
});
|
||||
|
||||
// Save normalized assistant response to history so transcript does not render raw protocol JSON.
|
||||
if (fullResponse) {
|
||||
await this.chatEngine.addMessage({
|
||||
conversationId,
|
||||
role: 'assistant',
|
||||
content: protocolResult.envelope.assistantText,
|
||||
toolCalls: toolCallsCollected.length > 0 ? JSON.stringify(toolCallsCollected) : undefined,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
// Generate title after first exchange
|
||||
const userMsgCount = conversation.messages.filter(m => m.role === 'user').length;
|
||||
if (userMsgCount === 0 && fullResponse) {
|
||||
|
||||
Reference in New Issue
Block a user