wip: desparate models fucking around

This commit is contained in:
2026-02-26 00:13:52 +01:00
parent 021cddefa3
commit 2a923c7e48
16 changed files with 684 additions and 113 deletions

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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) {

View File

@@ -5,6 +5,7 @@ import { planAssistantRequest } from '../../navigation/assistantConversation';
import { dispatchAssistantAction } from '../../navigation/assistantActionDispatcher';
import { extractAssistantResponseContent, type AssistantPanelElement } from '../../navigation/assistantPanelSpec';
import { toClarificationElements } from '../../navigation/protocolNeedsInput';
import { buildActionPoliciesFromEnvelope } from '../../navigation/protocolActionPolicies';
import { ensureConversationId } from '../../navigation/chatSession';
import { getChatSurfaceMode } from '../../navigation/chatSurfaceMode';
import { useChatMessageSender } from '../../navigation/useChatMessageSender';
@@ -54,6 +55,7 @@ export const AssistantSidebar: React.FC = () => {
finalizeAssistantTurn,
appendAssistantMessage,
stopStreaming,
getStreamingContent,
} = useChatSurfaceState();
const activeTab = useMemo(() => tabs.find((tab) => tab.id === activeTabId) ?? null, [tabs, activeTabId]);
@@ -173,20 +175,18 @@ export const AssistantSidebar: React.FC = () => {
? (sendResult.envelope.ui?.elements as AssistantPanelElement[])
: toClarificationElements(sendResult.envelope.needsInput);
setPanelElements(uiElements);
setActionPolicies(
sendResult.envelope.actions.reduce<Record<string, 'silent' | 'confirm' | 'danger'>>((accumulator, action) => {
accumulator[action.action] = action.policy;
return accumulator;
}, {}),
);
} else if (sendResult.message) {
const parsedResponse = extractAssistantResponseContent(sendResult.message);
finalizeAssistantTurn(resolvedConversationId, parsedResponse.displayText);
setPanelElements(parsedResponse.panelSpec?.elements ?? []);
setActionPolicies({});
setActionPolicies(buildActionPoliciesFromEnvelope(sendResult.envelope));
} else {
appendAssistantMessage(resolvedConversationId, tr('chat.errorEmptyResponse'));
stopStreaming();
const assistantContent = getStreamingContent() || sendResult.message;
if (assistantContent) {
const parsedResponse = extractAssistantResponseContent(assistantContent);
finalizeAssistantTurn(resolvedConversationId, parsedResponse.displayText);
setPanelElements(parsedResponse.panelSpec?.elements ?? []);
setActionPolicies({});
} else {
appendAssistantMessage(resolvedConversationId, tr('chat.errorEmptyResponse'));
stopStreaming();
}
}
setPrompt('');
@@ -230,17 +230,13 @@ export const AssistantSidebar: React.FC = () => {
? (sendResult.envelope.ui?.elements as AssistantPanelElement[])
: toClarificationElements(sendResult.envelope.needsInput);
setPanelElements(uiElements);
setActionPolicies(
sendResult.envelope.actions.reduce<Record<string, 'silent' | 'confirm' | 'danger'>>((accumulator, action) => {
accumulator[action.action] = action.policy;
return accumulator;
}, {}),
);
setActionPolicies(buildActionPoliciesFromEnvelope(sendResult.envelope));
return;
}
if (sendResult.message) {
const parsedResponse = extractAssistantResponseContent(sendResult.message);
const assistantContent = getStreamingContent() || sendResult.message;
if (assistantContent) {
const parsedResponse = extractAssistantResponseContent(assistantContent);
finalizeAssistantTurn(conversationId, parsedResponse.displayText);
setPanelElements(parsedResponse.panelSpec?.elements ?? []);
setActionPolicies({});

View File

@@ -6,6 +6,7 @@ import { getChatSurfaceMode } from '../../navigation/chatSurfaceMode';
import { dispatchAssistantAction } from '../../navigation/assistantActionDispatcher';
import { extractAssistantResponseContent, type AssistantPanelElement } from '../../navigation/assistantPanelSpec';
import { toClarificationElements } from '../../navigation/protocolNeedsInput';
import { buildActionPoliciesFromEnvelope } from '../../navigation/protocolActionPolicies';
import { useAppStore } from '../../store';
import { ChatTranscript } from '../ChatSurface';
import { AssistantPanelControls } from '../AssistantPanelControls';
@@ -198,12 +199,7 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
? (result.envelope.ui?.elements as AssistantPanelElement[])
: toClarificationElements(result.envelope.needsInput);
setPanelElements(uiElements);
setActionPolicies(
result.envelope.actions.reduce<Record<string, 'silent' | 'confirm' | 'danger'>>((accumulator, action) => {
accumulator[action.action] = action.policy;
return accumulator;
}, {}),
);
setActionPolicies(buildActionPoliciesFromEnvelope(result.envelope));
} else if (assistantContent) {
const parsedResponse = extractAssistantResponseContent(assistantContent);
finalizeAssistantTurn(conversationId, parsedResponse.displayText);
@@ -272,12 +268,7 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
? (result.envelope.ui?.elements as AssistantPanelElement[])
: toClarificationElements(result.envelope.needsInput);
setPanelElements(uiElements);
setActionPolicies(
result.envelope.actions.reduce<Record<string, 'silent' | 'confirm' | 'danger'>>((accumulator, action) => {
accumulator[action.action] = action.policy;
return accumulator;
}, {}),
);
setActionPolicies(buildActionPoliciesFromEnvelope(result.envelope));
return;
}

View File

@@ -170,15 +170,35 @@ function toRecord(value: unknown): Record<string, unknown> | null {
}
function normalizeChartElement(record: Record<string, unknown>): Record<string, unknown> | null {
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;
}
@@ -204,7 +224,6 @@ function normalizeChartElement(record: Record<string, unknown>): Record<string,
}
normalized.series = series;
delete normalized.data;
return normalized;
}
@@ -269,6 +288,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 : typeof record.text === 'string' ? record.text : '';
if (!textValue.trim()) {
@@ -302,6 +325,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);
}
@@ -366,9 +393,9 @@ export function extractAssistantPanelSpec(message: string): AssistantPanelSpec |
export function extractAssistantResponseContent(message: string): AssistantResponseContent {
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;
}
@@ -384,8 +411,21 @@ export function extractAssistantResponseContent(message: string): AssistantRespo
}
const parsedWholeMessage = parseSpecCandidate(trimmed);
let displayText = parsedWholeMessage ? '' : trimmed;
if (parsedWholeMessage) {
try {
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
if (parsed.protocolVersion === '2.0' && typeof parsed.assistantText === 'string') {
displayText = parsed.assistantText;
}
} catch {
// no-op
}
}
return {
displayText: parsedWholeMessage ? '' : trimmed,
displayText,
panelSpec: parsedWholeMessage,
};
}

View File

@@ -0,0 +1,18 @@
import type { ProtocolResponseEnvelope } from '../types/electron';
export type ActionPolicyLevel = 'silent' | 'confirm' | 'danger';
export function buildActionPoliciesFromEnvelope(
envelope: Pick<ProtocolResponseEnvelope, 'actions' | 'needsInput'>,
): Record<string, ActionPolicyLevel> {
const policies = envelope.actions.reduce<Record<string, ActionPolicyLevel>>((accumulator, action) => {
accumulator[action.action] = action.policy;
return accumulator;
}, {});
if (envelope.needsInput.required && envelope.needsInput.fields.length > 0 && !policies.submitNeedsInput) {
policies.submitNeedsInput = 'confirm';
}
return policies;
}

View File

@@ -186,7 +186,7 @@ const METHODS_V1: PythonApiMethodContractV1[] = [
method('chat.getConversation', 'Fetch one chat conversation by id.', [requiredString('id')], 'ChatConversation | null'),
method('chat.updateConversation', 'Update chat conversation metadata.', [requiredString('id'), requiredObject('updates')], 'ChatConversation | null'),
method('chat.deleteConversation', 'Delete chat conversation by id.', [requiredString('id')], 'boolean'),
method('chat.sendMessage', 'Send message to chat conversation.', [requiredString('conversationId'), requiredString('message')], '{ success: boolean; message?: string; error?: string }'),
method('chat.sendMessage', 'Send message to chat conversation.', [requiredString('conversationId'), requiredString('message'), optionalObject('metadata')], "{ success: boolean; message?: string; envelope?: ProtocolResponseEnvelope; protocolVersion?: '2.0'; traceId?: string; warnings?: string[]; error?: string }"),
method('chat.abortMessage', 'Abort active streaming chat response.', [requiredString('conversationId')], 'void'),
method('chat.getHistory', 'Get message history for conversation.', [requiredString('conversationId')], 'ChatMessage[]'),
method('chat.clearMessages', 'Clear messages for conversation.', [requiredString('conversationId')], 'void'),
@@ -360,6 +360,45 @@ const DATA_STRUCTURES_V1: PythonApiDataStructureContractV1[] = [
{ name: 'maskedKey', type: 'string', required: true, description: 'Masked key representation for UI display.' },
],
},
{
name: 'ProtocolNeedsInputField',
description: 'A required clarification input field used for needsInput prompts.',
fields: [
{ name: 'key', type: 'string', required: true, description: 'Stable field key used in submitted values.' },
{ name: 'label', type: 'string', required: true, description: 'User-facing field label.' },
{ name: 'inputType', type: "'text' | 'textarea' | 'select' | 'checkbox' | 'date' | 'number'", required: true, description: 'Rendered input control type.' },
{ name: 'required', type: 'boolean', required: false, description: 'Whether user input is required.' },
{ name: 'options', type: 'Array<{ label: string; value: string }>', required: false, description: 'Selectable options for select controls.' },
{ name: 'placeholder', type: 'string', required: false, description: 'Optional placeholder text for text-like controls.' },
{ name: 'defaultValue', type: 'string | number | boolean', required: false, description: 'Default field value shown in UI.' },
],
},
{
name: 'ProtocolAction',
description: 'A declarative assistant action exposed to the UI runtime.',
fields: [
{ name: 'id', type: 'string', required: true, description: 'Stable action id within a response envelope.' },
{ name: 'action', type: 'string', required: true, description: 'Action name to dispatch in renderer.' },
{ name: 'label', type: 'string', required: false, description: 'Optional user-facing action label.' },
{ name: 'payload', type: 'Record<string, unknown>', required: false, description: 'Optional action payload arguments.' },
{ name: 'policy', type: "'silent' | 'confirm' | 'danger'", required: true, description: 'Action confirmation policy level.' },
{ name: 'requiresConfirmation', type: 'boolean', required: true, description: 'Whether confirmation is required before dispatch.' },
],
},
{
name: 'ProtocolResponseEnvelope',
description: 'Canonical AGUI response envelope returned from chat.sendMessage.',
fields: [
{ name: 'protocolVersion', type: "'2.0'", required: true, description: 'Envelope protocol version.' },
{ name: 'assistantText', type: 'string', required: true, description: 'Assistant text content rendered in transcript.' },
{ name: 'ui', type: "{ specVersion: '1'; elements: unknown[] }", required: false, description: 'Optional structured UI payload.' },
{ name: 'intent', type: "'analyze' | 'ask_input' | 'propose_action' | 'execute_action' | 'summarize'", required: true, description: 'Turn intent classification.' },
{ name: 'needsInput', type: '{ required: boolean; fields: ProtocolNeedsInputField[] }', required: true, description: 'Clarification requirements for next step.' },
{ name: 'actions', type: 'ProtocolAction[]', required: true, description: 'Declarative actions available for this turn.' },
{ name: 'confidence', type: 'number', required: true, description: 'Model confidence score from 0 to 1.' },
{ name: 'traceId', type: 'string', required: true, description: 'Trace id for observability and debugging.' },
],
},
{
name: 'ProtocolTelemetrySnapshot',
description: 'Aggregated protocol telemetry metrics for AGUI response health.',
@@ -377,7 +416,7 @@ const DATA_STRUCTURES_V1: PythonApiDataStructureContractV1[] = [
];
export const BDS_PYTHON_API_CONTRACT_V1: PythonApiContractV1 = {
version: '1.4.0',
version: '1.5.0',
generatedAt: '2026-02-25T00:00:00.000Z',
methods: METHODS_V1,
dataStructures: DATA_STRUCTURES_V1,