mirror of
https://github.com/supabase/supabase.git
synced 2026-09-11 04:21:47 +08:00
## Summary - Plumb pre-update notebook snapshot through `update_notebook` tool response as `previous_content` - Add sanitizers in `tool-sanitizer.ts` to strip snapshot before model sees it - Add client-side stripping in `prepareMessagesForAPI` to avoid re-uploading snapshot on subsequent turns - This is PR 2 of 3 fixing Linear issue FE-4243 (notebook update proposal shows 'unapplyable' error for already-completed updates) - Ships no visible behavior change on its own; enables PR 3 to restore diff preview for completed updates ## Test plan - [x] Unit tests: 80/80 passing across notebook-tools.test.ts, tool-sanitizer.test.ts, generate-assistant-response.utils.test.ts, message-utils.test.ts, and mock-tools.test.ts - [x] Typecheck: clean for all changed files - [x] ESLint: zero errors, lint:ratchet passes (exit 0) - [x] Integration: previous_content is correctly populated with pre-update notebook, stripped before model context, and stripped on client-side re-upload <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Notebook updates now retain previous content for recovery and history. - AI responses expose only the notebook’s ID and name, keeping previous content out of model-visible data. - **Tests** - Added coverage for notebook update results, content sanitization, and message preparation, including cases where previous content is absent or preserved. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
82 lines
3.1 KiB
TypeScript
82 lines
3.1 KiB
TypeScript
import {
|
|
isToolUIPart,
|
|
type UIDataTypes,
|
|
type UIMessage,
|
|
type UIMessagePart,
|
|
type UITools,
|
|
} from 'ai'
|
|
|
|
type UIPart = UIMessagePart<UIDataTypes, UITools>
|
|
|
|
/** Strips `update_notebook`'s `previous_content` snapshot — display-only, never re-uploaded. */
|
|
function stripNotebookSnapshot(part: UIPart): UIPart {
|
|
if (!isToolUIPart(part) || part.type !== 'tool-update_notebook') return part
|
|
if (!part.output || typeof part.output !== 'object') return part
|
|
|
|
const { previous_content, ...sanitizedOutput } = part.output as Record<string, unknown>
|
|
return { ...part, output: sanitizedOutput } as UIPart
|
|
}
|
|
|
|
/**
|
|
* Prepares messages for API transmission by cleaning and limiting history
|
|
*/
|
|
export function prepareMessagesForAPI(messages: UIMessage[]): UIMessage[] {
|
|
// [Joshen] Specifically limiting the chat history that get's sent to reduce the
|
|
// size of the context that goes into the model. This should always be an odd number
|
|
// as much as possible so that the first message is always the user's
|
|
const MAX_CHAT_HISTORY = 7
|
|
|
|
const slicedMessages = messages.slice(-MAX_CHAT_HISTORY)
|
|
|
|
// Filter out results from messages before sending to the model
|
|
const cleanedMessages = slicedMessages.map((_message) => {
|
|
const message = _message as UIMessage & { results?: unknown }
|
|
const cleanedMessage = { ...message } as UIMessage & { results?: unknown }
|
|
if (message.role === 'assistant' && message.results) {
|
|
delete cleanedMessage.results
|
|
}
|
|
// Map into a new array rather than mutating in place — `parts` is shared by reference
|
|
// with the locally persisted message the assistant panel reads from.
|
|
if (cleanedMessage.parts) {
|
|
cleanedMessage.parts = cleanedMessage.parts.map(stripNotebookSnapshot)
|
|
}
|
|
return cleanedMessage as UIMessage
|
|
})
|
|
|
|
return cleanedMessages
|
|
}
|
|
|
|
/**
|
|
* Approval id when the part is waiting on a human Approve/Deny.
|
|
* Narrows with `isToolUIPart` first, matching the AI SDK `useChat` approval pattern:
|
|
* `state === 'approval-requested' && !approval.isAutomatic`.
|
|
*
|
|
* @see https://ai-sdk.dev/docs/agents/tool-approvals
|
|
*/
|
|
export function getManualApprovalId(part: UIPart): string | undefined {
|
|
if (!isToolUIPart(part) || part.state !== 'approval-requested') return undefined
|
|
if ('isAutomatic' in part.approval && part.approval.isAutomatic === true) return undefined
|
|
return part.approval.id
|
|
}
|
|
|
|
/** True when the part is waiting on a human Approve/Deny, not an automatic policy decision. */
|
|
export function isManualApprovalRequested(part: UIPart): boolean {
|
|
return getManualApprovalId(part) !== undefined
|
|
}
|
|
|
|
/**
|
|
* Returns approval IDs to auto-deny when the model issues multiple approval-required
|
|
* tool calls in the same turn — all but the first, so the model reissues them sequentially.
|
|
*/
|
|
export function getParallelApprovalIdsToReject(messages: UIMessage[]): string[] {
|
|
const lastMessage = messages.findLast((m) => m.role === 'assistant')
|
|
if (!lastMessage) return []
|
|
|
|
const pendingIds: string[] = []
|
|
for (const part of lastMessage.parts ?? []) {
|
|
const id = getManualApprovalId(part)
|
|
if (id) pendingIds.push(id)
|
|
}
|
|
return pendingIds.slice(1)
|
|
}
|