u
This commit is contained in:
@@ -163,48 +163,61 @@ async function buildSystemPrompt(vfsRoot, googleAuthId) {
|
||||
* `idle`/`busy` state. The finer SSE states (`thinking`, `working`, `done`)
|
||||
* and the streamed `delta` chunks are SSE-only.
|
||||
*/
|
||||
export function toLlmMessage(m) {
|
||||
let payload = {};
|
||||
if (typeof m.message === 'string') {
|
||||
export function historyToLlmMessages(history) {
|
||||
const messages = [];
|
||||
for (const m of history) {
|
||||
if (m.role === 'usage' || m.role === 'reasoning') continue;
|
||||
let payload;
|
||||
try {
|
||||
payload = JSON.parse(m.message);
|
||||
payload = typeof m.message === 'string' ? JSON.parse(m.message) : m.message;
|
||||
} catch {
|
||||
payload = { content: m.message };
|
||||
}
|
||||
} else if (m.message && typeof m.message === 'object') {
|
||||
payload = m.message;
|
||||
}
|
||||
|
||||
if (m.role === 'user') {
|
||||
return { role: 'user', content: payload.content ?? (typeof m.message === 'string' ? m.message : '') };
|
||||
}
|
||||
if (m.role === 'assistant') {
|
||||
const msg = {
|
||||
role: 'assistant',
|
||||
content: payload.content ?? null,
|
||||
};
|
||||
if (Array.isArray(payload.tool_calls) && payload.tool_calls.length > 0) {
|
||||
msg.tool_calls = payload.tool_calls;
|
||||
if (m.role === 'user') {
|
||||
messages.push({ role: 'user', content: payload.content ?? (typeof m.message === 'string' ? m.message : '') });
|
||||
} else if (m.role === 'assistant') {
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: payload.content ?? null,
|
||||
...(Array.isArray(payload.tool_calls) && payload.tool_calls.length > 0 ? { tool_calls: payload.tool_calls } : {}),
|
||||
});
|
||||
} else if (m.role === 'tool') {
|
||||
const toolCallId = payload.tool_call_id ?? '';
|
||||
const toolName = payload.name ?? '';
|
||||
const toolParams = payload.parameters ?? payload.arguments ?? '{}';
|
||||
const prevMsg = messages[messages.length - 1];
|
||||
if (!prevMsg || prevMsg.role !== 'assistant' || !prevMsg.tool_calls) {
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: null,
|
||||
tool_calls: [{
|
||||
id: toolCallId,
|
||||
type: 'function',
|
||||
function: {
|
||||
name: toolName,
|
||||
arguments: typeof toolParams === 'string' ? toolParams : JSON.stringify(toolParams),
|
||||
},
|
||||
}],
|
||||
});
|
||||
} else if (!prevMsg.tool_calls.some((tc) => tc.id === toolCallId)) {
|
||||
prevMsg.tool_calls.push({
|
||||
id: toolCallId,
|
||||
type: 'function',
|
||||
function: {
|
||||
name: toolName,
|
||||
arguments: typeof toolParams === 'string' ? toolParams : JSON.stringify(toolParams),
|
||||
},
|
||||
});
|
||||
}
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
tool_call_id: toolCallId,
|
||||
content: typeof payload.content === 'string' ? payload.content : JSON.stringify(payload.content ?? ''),
|
||||
...(toolName ? { name: toolName } : {}),
|
||||
});
|
||||
}
|
||||
if (Array.isArray(payload.reasoning_details) && payload.reasoning_details.length > 0) {
|
||||
msg.reasoning_details = payload.reasoning_details;
|
||||
}
|
||||
if (payload.reasoning) {
|
||||
msg.reasoning = payload.reasoning;
|
||||
} else if (payload.reasoning_content) {
|
||||
msg.reasoning = payload.reasoning_content;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
if (m.role === 'tool') {
|
||||
return {
|
||||
role: 'tool',
|
||||
tool_call_id: payload.tool_call_id ?? '',
|
||||
content: typeof payload.content === 'string' ? payload.content : JSON.stringify(payload.content ?? ''),
|
||||
...(payload.name ? { name: payload.name } : {}),
|
||||
};
|
||||
}
|
||||
return { role: m.role, content: payload.content ?? m.message };
|
||||
return messages;
|
||||
}
|
||||
|
||||
export class ConversationManager {
|
||||
@@ -391,12 +404,11 @@ export class ConversationManager {
|
||||
|
||||
const messages = [
|
||||
{ role: 'system', content: await buildSystemPrompt(vfsRoot, googleAuthId) },
|
||||
...history.map(toLlmMessage),
|
||||
...historyToLlmMessages(history),
|
||||
];
|
||||
|
||||
let text = '';
|
||||
let currentReasoning = '';
|
||||
let savedToolCallsTurn = null;
|
||||
|
||||
this.orbiter = new LLMOrbiter(new OpenRouterProvider({ apiKey, apiUrl: env.openrouterApiUrl }), {
|
||||
onStatus: (status, meta) => {
|
||||
@@ -410,43 +422,37 @@ export class ConversationManager {
|
||||
text += chunk;
|
||||
this.broadcast('delta', { delta: chunk, text });
|
||||
},
|
||||
onToolCall: async ({ name, args, id, callItem, tool_calls, reasoning_details, reasoning, content }) => {
|
||||
try {
|
||||
const effectiveToolCalls = tool_calls || (callItem ? [callItem] : [{
|
||||
id: id || randomUUID(),
|
||||
type: 'function',
|
||||
function: {
|
||||
name,
|
||||
arguments: typeof args === 'string' ? args : JSON.stringify(args || {}),
|
||||
},
|
||||
}]);
|
||||
onAssistantMessage: async ({ content, tool_calls, reasoning_details, reasoning }) => {
|
||||
const plain = (reasoning || currentReasoning.trim() || reasoning_details?.find((d) => d?.type === 'reasoning.summary')?.summary || '').trim();
|
||||
const encrypted = reasoning_details?.find((d) => d?.type === 'reasoning.encrypted')?.data || '';
|
||||
currentReasoning = '';
|
||||
|
||||
if (savedToolCallsTurn !== effectiveToolCalls) {
|
||||
savedToolCallsTurn = effectiveToolCalls;
|
||||
const effectiveReasoningDetails = (Array.isArray(reasoning_details) && reasoning_details.length > 0)
|
||||
? reasoning_details
|
||||
: undefined;
|
||||
const effectiveReasoning = reasoning || currentReasoning.trim() || undefined;
|
||||
currentReasoning = '';
|
||||
|
||||
const assistantPayload = {
|
||||
content: (content || text).trim() || '',
|
||||
tool_calls: effectiveToolCalls,
|
||||
...(effectiveReasoningDetails ? { reasoning_details: effectiveReasoningDetails } : {}),
|
||||
...(effectiveReasoning ? { reasoning: effectiveReasoning } : {}),
|
||||
};
|
||||
if (plain || encrypted) {
|
||||
try {
|
||||
await db.addMessage(googleAuthId, this.conversationId, {
|
||||
role: 'reasoning',
|
||||
message: JSON.stringify({ plain, encrypted }),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`[ConversationManager ${this.conversationId}] failed to save reasoning message:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
const trimmedContent = (content || text).trim();
|
||||
if (trimmedContent) {
|
||||
try {
|
||||
await db.addMessage(googleAuthId, this.conversationId, {
|
||||
role: 'assistant',
|
||||
message: JSON.stringify(assistantPayload),
|
||||
message: JSON.stringify({ content: trimmedContent }),
|
||||
});
|
||||
text = '';
|
||||
emitCompanyEvent('modelconversation', { companyId: this.companyId });
|
||||
} catch (err) {
|
||||
console.error(`[ConversationManager ${this.conversationId}] failed to save assistant message:`, err);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[ConversationManager ${this.conversationId}] failed to save tool call message:`, err);
|
||||
}
|
||||
},
|
||||
onToolCall: async () => {},
|
||||
onToolResult: async ({ name, result, id, callItem }) => {
|
||||
const rawArgs = callItem?.function?.arguments || '';
|
||||
this.broadcast('state', {
|
||||
@@ -461,6 +467,8 @@ export class ConversationManager {
|
||||
await db.addMessage(googleAuthId, this.conversationId, {
|
||||
role: 'tool',
|
||||
message: JSON.stringify({
|
||||
name,
|
||||
parameters: rawArgs,
|
||||
content: typeof result === 'string' ? result : JSON.stringify(result),
|
||||
tool_call_id: id || callItem?.id || '',
|
||||
}),
|
||||
@@ -485,6 +493,8 @@ export class ConversationManager {
|
||||
await db.addMessage(googleAuthId, this.conversationId, {
|
||||
role: 'tool',
|
||||
message: JSON.stringify({
|
||||
name,
|
||||
parameters: rawArgs,
|
||||
content: `Tool error [${name}]: ${error}`,
|
||||
tool_call_id: id || callItem?.id || '',
|
||||
}),
|
||||
@@ -494,7 +504,7 @@ export class ConversationManager {
|
||||
console.error(`[ConversationManager ${this.conversationId}] failed to save tool error message:`, err);
|
||||
}
|
||||
},
|
||||
onUsage: (u) => {
|
||||
onUsage: async (u) => {
|
||||
if (u) {
|
||||
this.broadcast('usage', {
|
||||
input: u.input ?? 0,
|
||||
@@ -502,6 +512,20 @@ export class ConversationManager {
|
||||
output: u.output ?? 0,
|
||||
...(u.cost != null ? { cost: u.cost } : {}),
|
||||
});
|
||||
try {
|
||||
await db.addMessage(googleAuthId, this.conversationId, {
|
||||
role: 'usage',
|
||||
message: JSON.stringify({
|
||||
input: u.input ?? 0,
|
||||
cached: u.cached ?? 0,
|
||||
output: u.output ?? 0,
|
||||
...(u.cost != null ? { cost: u.cost } : {}),
|
||||
}),
|
||||
});
|
||||
emitCompanyEvent('modelconversation', { companyId: this.companyId });
|
||||
} catch (err) {
|
||||
console.error(`[ConversationManager ${this.conversationId}] failed to save usage message:`, err);
|
||||
}
|
||||
}
|
||||
// A call that consumed 0 input tokens AND produced 0 output means the
|
||||
// provider returned nothing at all — not a real generation. LLMOrbiter
|
||||
@@ -585,17 +609,6 @@ export class ConversationManager {
|
||||
);
|
||||
error = 'The model returned an empty response. Please try again.';
|
||||
}
|
||||
|
||||
if (content || finalReasoningDetails || finalReasoning) {
|
||||
await db.addMessage(googleAuthId, this.conversationId, {
|
||||
message: JSON.stringify({
|
||||
content: content || '',
|
||||
...(finalReasoningDetails ? { reasoning_details: finalReasoningDetails } : {}),
|
||||
...(finalReasoning ? { reasoning: finalReasoning } : {}),
|
||||
}),
|
||||
role: 'assistant',
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (this.destroyed) return;
|
||||
error = err.message;
|
||||
|
||||
Reference in New Issue
Block a user