fix: render consultation replies and typed tool activity
This commit is contained in:
@@ -83,6 +83,9 @@ export interface TeacherRequest {
|
||||
discussionContext?: TeacherDiscussionContext;
|
||||
discussionSnapshot?: TeacherDiscussionContent;
|
||||
discussionError?: string;
|
||||
/** Original answer retained when discussion parsing fails; never fed back as context. */
|
||||
unparsedResponse?: string;
|
||||
toolActivity?: TeacherToolActivity[];
|
||||
|
||||
intent?: TeacherRequestIntent;
|
||||
sourceConversationId?: string;
|
||||
@@ -105,6 +108,11 @@ export interface TeacherRequest {
|
||||
cloudRequestId?: string;
|
||||
progress?: string;
|
||||
}
|
||||
export interface TeacherToolActivity {
|
||||
id: string;
|
||||
name: string;
|
||||
status: 'running' | 'completed' | 'failed';
|
||||
}
|
||||
export interface TeacherTopic {
|
||||
discussion?: TeacherDiscussion;
|
||||
role?: LegacyConsultationRole;
|
||||
|
||||
@@ -223,8 +223,8 @@ function stringToken(raw: string, start: number): { value: string; end: number }
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Recover only a complete top-level JSON string field, never nested tool data or a partial string. */
|
||||
function recoverReply(raw: string): string | undefined {
|
||||
/** Inspect only top-level fields, including when a later value was truncated. */
|
||||
function findTopLevelField(raw: string, names: readonly string[]): number | undefined {
|
||||
if (!raw.trimStart().startsWith('{')) return undefined;
|
||||
let depth = 0, expectingKey = false;
|
||||
for (let cursor = 0; cursor < raw.length; cursor++) {
|
||||
@@ -232,16 +232,13 @@ function recoverReply(raw: string): string | undefined {
|
||||
if (character === '"') {
|
||||
const token = stringToken(raw, cursor);
|
||||
if (!token) return undefined;
|
||||
if (depth === 1 && expectingKey && token.value === 'reply') {
|
||||
if (depth === 1 && expectingKey && names.includes(token.value)) {
|
||||
let valueStart = token.end;
|
||||
while (/\s/.test(raw[valueStart] ?? '') && valueStart < raw.length) valueStart++;
|
||||
if (raw[valueStart] !== ':') return undefined;
|
||||
valueStart++;
|
||||
while (/\s/.test(raw[valueStart] ?? '') && valueStart < raw.length) valueStart++;
|
||||
if (raw[valueStart] !== '"') return undefined;
|
||||
const value = stringToken(raw, valueStart)?.value;
|
||||
if (value?.trim() && value.length <= MAX_REPLY_LENGTH) return value.trim();
|
||||
return undefined;
|
||||
return valueStart;
|
||||
}
|
||||
if (depth === 1) expectingKey = false;
|
||||
cursor = token.end - 1;
|
||||
@@ -257,6 +254,14 @@ function recoverReply(raw: string): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Recover a complete reply string, never nested tool data or a partial string. */
|
||||
function recoverReply(raw: string): string | undefined {
|
||||
const start = findTopLevelField(raw, ['reply']);
|
||||
if (start === undefined || raw[start] !== '"') return undefined;
|
||||
const value = stringToken(raw, start)?.value;
|
||||
return value?.trim() && value.length <= MAX_REPLY_LENGTH ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function invalidReply(candidate: string): TeacherDiscussionReply {
|
||||
return { reply: recoverReply(candidate) ?? INVALID_REPLY, quickReplies: [], toolError: INVALID_TOOL };
|
||||
}
|
||||
@@ -264,18 +269,15 @@ function invalidReply(candidate: string): TeacherDiscussionReply {
|
||||
/** Parse final model output. Callers keep the existing tool whenever toolError is present. */
|
||||
export function parseTeacherDiscussionReply(raw: string): TeacherDiscussionReply {
|
||||
const trimmed = raw.trim();
|
||||
// JSON-labelled fences are transport, including truncated fences. Ordinary programming fences remain text.
|
||||
const fence = /(?:^|\n)[ \t]*```(?:json|makelore-teacher(?:-discussion)?)[ \t]*(?:\r?\n|$)/i.exec(trimmed);
|
||||
const bareFence = /^```[ \t]*\r?\n(?=\s*[{[])/.exec(trimmed);
|
||||
const opening = fence ?? bareFence;
|
||||
const unfencedEnvelope = /\{\s*"(?:reply|quickReplies|tool)"\s*:/.exec(trimmed);
|
||||
const startsJson = /^[{[]/.test(trimmed);
|
||||
const looksStructured = !!opening || !!unfencedEnvelope || startsJson || /^```(?:json|makelore-teacher)/i.test(trimmed);
|
||||
if (!looksStructured) {
|
||||
return { reply: trimmed.slice(0, MAX_REPLY_LENGTH) || INVALID_REPLY, quickReplies: [] };
|
||||
}
|
||||
|
||||
let candidate = !opening && !startsJson && unfencedEnvelope ? trimmed.slice(unfencedEnvelope.index) : trimmed;
|
||||
// A Markdown link, JSON example or code fence is not a discussion envelope.
|
||||
// Only our explicit fence or top-level protocol fields select this parser.
|
||||
const firstFence = /(?:^|\n)[ \t]*```([^\r\n]*)[ \t]*(?:\r?\n|$)/.exec(trimmed);
|
||||
const language = firstFence?.[1].trim().toLowerCase();
|
||||
const explicitFence = language === 'makelore-teacher' || language === 'makelore-teacher-discussion';
|
||||
const opening = firstFence && (explicitFence || language === 'json' || language === '') ? firstFence : null;
|
||||
const unfencedEnvelope = !firstFence && !trimmed.startsWith('{')
|
||||
? /(?:^|\n)[ \t]*(\{\s*"(?:reply|quickReplies|tool)"\s*:)/.exec(trimmed) : null;
|
||||
let candidate = unfencedEnvelope ? trimmed.slice(unfencedEnvelope.index).trimStart() : trimmed;
|
||||
let validFence = true;
|
||||
if (opening) {
|
||||
const remainder = trimmed.slice(opening.index + opening[0].length);
|
||||
@@ -283,6 +285,9 @@ export function parseTeacherDiscussionReply(raw: string): TeacherDiscussionReply
|
||||
candidate = closing ? remainder.slice(0, closing.index) : remainder;
|
||||
validFence = !!closing && !/(?:^|\n)[ \t]*```/.test(candidate);
|
||||
}
|
||||
if (!explicitFence && findTopLevelField(candidate, ['reply', 'quickReplies', 'tool']) === undefined) {
|
||||
return { reply: trimmed.slice(0, MAX_REPLY_LENGTH) || INVALID_REPLY, quickReplies: [] };
|
||||
}
|
||||
if (new TextEncoder().encode(trimmed).length > MAX_INPUT_BYTES) return invalidReply(candidate.slice(0, MAX_INPUT_BYTES));
|
||||
if (!validFence) return invalidReply(candidate);
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user