Merge 423eb3f54a into 451a864096
This commit is contained in:
commit
dc5b702551
|
|
@ -0,0 +1,147 @@
|
|||
/**
|
||||
* Evidence Unit Tests
|
||||
*
|
||||
* Evidence rides on the terminal chunk of a dialectic stream, which the
|
||||
* stream reader would otherwise discard. These tests drive mocked SSE bodies
|
||||
* so they cover that without a server.
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { createDialecticStream } from '../src/http/streaming'
|
||||
import type { Evidence } from '../src/types/api'
|
||||
|
||||
const EVIDENCE: Evidence = {
|
||||
conclusions: [
|
||||
{
|
||||
id: 'doc-sentinel',
|
||||
level: 'deductive',
|
||||
content: 'User drinks coffee in the morning',
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
session_id: 'session-1',
|
||||
source_ids: ['doc-a', 'doc-b'],
|
||||
},
|
||||
],
|
||||
messages: [
|
||||
{
|
||||
id: 'msg-sentinel',
|
||||
session_id: 'session-1',
|
||||
peer_id: 'alice',
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
],
|
||||
tool_calls: [{ tool_name: 'search_memory', tool_input: { query: 'coffee' } }],
|
||||
reasoning_trace_id: null,
|
||||
}
|
||||
|
||||
function mockSSEResponse(lines: string[]): Response {
|
||||
const encoder = new TextEncoder()
|
||||
let index = 0
|
||||
const stream = new ReadableStream({
|
||||
pull(controller) {
|
||||
if (index < lines.length) {
|
||||
controller.enqueue(encoder.encode(lines[index]))
|
||||
index++
|
||||
} else {
|
||||
controller.close()
|
||||
}
|
||||
},
|
||||
})
|
||||
return new Response(stream, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/event-stream' },
|
||||
})
|
||||
}
|
||||
|
||||
function contentFrame(content: string): string {
|
||||
return `data: ${JSON.stringify({ delta: { content }, done: false })}\n\n`
|
||||
}
|
||||
|
||||
function doneFrame(evidence?: Evidence): string {
|
||||
return `data: ${JSON.stringify({ done: true, ...(evidence && { evidence })})}\n\n`
|
||||
}
|
||||
|
||||
describe('streamed evidence', () => {
|
||||
test('is captured off the terminal chunk', async () => {
|
||||
const stream = createDialecticStream(
|
||||
mockSSEResponse([
|
||||
contentFrame('The user '),
|
||||
contentFrame('drinks coffee.'),
|
||||
doneFrame(EVIDENCE),
|
||||
])
|
||||
)
|
||||
|
||||
const chunks: string[] = []
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
expect(chunks.join('')).toBe('The user drinks coffee.')
|
||||
expect(stream.evidence).toEqual(EVIDENCE)
|
||||
})
|
||||
|
||||
test('survives being split across network chunks', async () => {
|
||||
const frame = doneFrame(EVIDENCE)
|
||||
const midpoint = Math.floor(frame.length / 2)
|
||||
const stream = createDialecticStream(
|
||||
mockSSEResponse([
|
||||
contentFrame('Answer.'),
|
||||
frame.slice(0, midpoint),
|
||||
frame.slice(midpoint),
|
||||
])
|
||||
)
|
||||
|
||||
await stream.getFinalResponse()
|
||||
|
||||
expect(stream.evidence?.conclusions[0]?.id).toBe('doc-sentinel')
|
||||
})
|
||||
|
||||
test('is null until the stream has been consumed', async () => {
|
||||
const stream = createDialecticStream(
|
||||
mockSSEResponse([
|
||||
contentFrame('Answer.'),
|
||||
doneFrame(EVIDENCE),
|
||||
])
|
||||
)
|
||||
|
||||
// Evidence cannot be known before the answer is complete.
|
||||
expect(stream.evidence).toBeNull()
|
||||
|
||||
await stream.getFinalResponse()
|
||||
|
||||
expect(stream.evidence).not.toBeNull()
|
||||
})
|
||||
|
||||
test('is null when the request did not ask for it', async () => {
|
||||
const stream = createDialecticStream(
|
||||
mockSSEResponse([
|
||||
contentFrame('Answer.'),
|
||||
doneFrame(),
|
||||
])
|
||||
)
|
||||
|
||||
await stream.getFinalResponse()
|
||||
|
||||
expect(stream.evidence).toBeNull()
|
||||
})
|
||||
|
||||
test('does not disturb the content chunks', async () => {
|
||||
const withEvidence = createDialecticStream(
|
||||
mockSSEResponse([
|
||||
contentFrame('a'),
|
||||
contentFrame('b'),
|
||||
doneFrame(EVIDENCE),
|
||||
])
|
||||
)
|
||||
const withoutEvidence = createDialecticStream(
|
||||
mockSSEResponse([
|
||||
contentFrame('a'),
|
||||
contentFrame('b'),
|
||||
doneFrame(),
|
||||
])
|
||||
)
|
||||
|
||||
expect(await withEvidence.toArray()).toEqual(
|
||||
await withoutEvidence.toArray()
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -590,6 +590,37 @@ describe('Peer', () => {
|
|||
expect(response === null || typeof response === 'string').toBe(true)
|
||||
})
|
||||
|
||||
test('chat with includeEvidence returns answer and evidence', async () => {
|
||||
const peer = await client.peer('chat-evidence-peer')
|
||||
const session = await client.session('chat-evidence-session', {
|
||||
metadata: {},
|
||||
})
|
||||
|
||||
await session.addPeers([peer.id])
|
||||
await session.addMessages([peer.message('I enjoy hiking')])
|
||||
|
||||
const result = await peer.chat('What does this user enjoy?', {
|
||||
includeEvidence: true,
|
||||
})
|
||||
|
||||
// The answer is wrapped rather than returned bare, and evidence is
|
||||
// present even when the run read nothing -- empty is not absent.
|
||||
expect(result).toHaveProperty('content')
|
||||
expect(result).toHaveProperty('evidence')
|
||||
expect(result.evidence).not.toBeNull()
|
||||
expect(Array.isArray(result.evidence?.conclusions)).toBe(true)
|
||||
expect(Array.isArray(result.evidence?.messages)).toBe(true)
|
||||
expect(Array.isArray(result.evidence?.tool_calls)).toBe(true)
|
||||
})
|
||||
|
||||
test('chat without includeEvidence still returns a bare answer', async () => {
|
||||
const peer = await client.peer('chat-no-evidence-peer')
|
||||
|
||||
const response = await peer.chat('What does this user enjoy?')
|
||||
|
||||
expect(response === null || typeof response === 'string').toBe(true)
|
||||
})
|
||||
|
||||
test('chat with session scope', async () => {
|
||||
const peer = await client.peer('chat-session-peer')
|
||||
const session = await client.session('chat-scoped-session', { metadata: {} })
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { Peer } from './peer'
|
|||
import { Scope } from './scope'
|
||||
import { Session } from './session'
|
||||
import type {
|
||||
ChatResponse,
|
||||
MessageResponse,
|
||||
PageResponse,
|
||||
PeerResponse,
|
||||
|
|
@ -998,13 +999,33 @@ export class Honcho {
|
|||
* @param options.responseFormat - Optional JSON Schema (root type "object") the response
|
||||
* must conform to. When provided, the response content is a
|
||||
* JSON string matching this schema.
|
||||
* @returns Promise resolving to the response string, or null if no relevant information
|
||||
* @param options.includeEvidence - When true, resolves to a ChatResponse carrying the
|
||||
* answer alongside what the dialectic read to produce it.
|
||||
* Evidence is collated from the agent's own reads rather
|
||||
* than reported by the model, so it is broader than a
|
||||
* citation list.
|
||||
* @returns Promise resolving to the response string, or null if no relevant information.
|
||||
* With includeEvidence, a ChatResponse wrapping that content plus its evidence.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const response = await honcho.chat('What are common themes across all users?')
|
||||
*
|
||||
* const { content, evidence } = await honcho.chat('What are common themes?', {
|
||||
* includeEvidence: true,
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
async chat(
|
||||
query: string,
|
||||
options: {
|
||||
session?: string | Session
|
||||
reasoningLevel?: ReasoningLevel
|
||||
responseFormat?: Record<string, unknown>
|
||||
scope?: string | string[]
|
||||
includeEvidence: true
|
||||
}
|
||||
): Promise<ChatResponse<string>>
|
||||
async chat(
|
||||
query: string,
|
||||
options?: {
|
||||
|
|
@ -1012,8 +1033,19 @@ export class Honcho {
|
|||
reasoningLevel?: ReasoningLevel
|
||||
responseFormat?: Record<string, unknown>
|
||||
scope?: string | string[]
|
||||
includeEvidence?: false
|
||||
}
|
||||
): Promise<string | null> {
|
||||
): Promise<string | null>
|
||||
async chat(
|
||||
query: string,
|
||||
options?: {
|
||||
session?: string | Session
|
||||
reasoningLevel?: ReasoningLevel
|
||||
responseFormat?: Record<string, unknown>
|
||||
scope?: string | string[]
|
||||
includeEvidence?: boolean
|
||||
}
|
||||
): Promise<ChatResponse<string> | string | null> {
|
||||
const validatedQuery = SearchQuerySchema.parse(query)
|
||||
const resolvedSessionId = options?.session
|
||||
? resolveId(options.session)
|
||||
|
|
@ -1026,11 +1058,13 @@ export class Honcho {
|
|||
reasoning_level: options?.reasoningLevel,
|
||||
response_format: options?.responseFormat,
|
||||
scope: options?.scope,
|
||||
include_evidence: options?.includeEvidence ? true : undefined,
|
||||
})
|
||||
if (!response.content) {
|
||||
return null
|
||||
const content = response.content || null
|
||||
if (!options?.includeEvidence) {
|
||||
return content
|
||||
}
|
||||
return response.content
|
||||
return { content, evidence: response.evidence ?? null }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1048,6 +1082,10 @@ export class Honcho {
|
|||
* @param options.responseFormat - Optional JSON Schema (root type "object") the response
|
||||
* must conform to. When provided, the response content is a
|
||||
* JSON string matching this schema.
|
||||
* @param options.includeEvidence - When true, the returned stream's `evidence` is
|
||||
* populated once it has been fully consumed. It cannot
|
||||
* be known before then, so the server sends it on the
|
||||
* terminal chunk.
|
||||
* @returns Promise resolving to a DialecticStreamResponse that can be iterated over
|
||||
*
|
||||
* @example
|
||||
|
|
@ -1065,6 +1103,7 @@ export class Honcho {
|
|||
reasoningLevel?: ReasoningLevel
|
||||
responseFormat?: Record<string, unknown>
|
||||
scope?: string | string[]
|
||||
includeEvidence?: boolean
|
||||
}
|
||||
): Promise<DialecticStreamResponse> {
|
||||
const validatedQuery = SearchQuerySchema.parse(query)
|
||||
|
|
@ -1078,6 +1117,7 @@ export class Honcho {
|
|||
reasoning_level: options?.reasoningLevel,
|
||||
response_format: options?.responseFormat,
|
||||
scope: options?.scope,
|
||||
include_evidence: options?.includeEvidence ? true : undefined,
|
||||
})
|
||||
|
||||
return createDialecticStream(response)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import type { Evidence } from '../types/api'
|
||||
|
||||
/**
|
||||
* Parse Server-Sent Events from a Response body.
|
||||
*
|
||||
|
|
@ -64,6 +66,8 @@ export interface DialecticStreamChunk {
|
|||
delta: {
|
||||
content?: string
|
||||
}
|
||||
/** Set only on the terminal chunk, and only when it was requested. */
|
||||
evidence?: Evidence | null
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -76,9 +80,25 @@ export class DialecticStreamResponse implements AsyncIterable<string> {
|
|||
private generator: AsyncGenerator<string, void, undefined>
|
||||
private chunks: string[] = []
|
||||
private consumed = false
|
||||
private evidenceSource?: () => Evidence | null
|
||||
|
||||
constructor(generator: AsyncGenerator<string, void, undefined>) {
|
||||
constructor(
|
||||
generator: AsyncGenerator<string, void, undefined>,
|
||||
evidenceSource?: () => Evidence | null
|
||||
) {
|
||||
this.generator = generator
|
||||
this.evidenceSource = evidenceSource
|
||||
}
|
||||
|
||||
/**
|
||||
* What the answer was built from, once the stream has finished.
|
||||
*
|
||||
* The server can only know this after the answer is complete, so it arrives
|
||||
* on the stream's terminal chunk. Reading it before the stream is fully
|
||||
* consumed returns null, as does a request that did not ask for evidence.
|
||||
*/
|
||||
get evidence(): Evidence | null {
|
||||
return this.evidenceSource?.() ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -131,9 +151,13 @@ export class DialecticStreamResponse implements AsyncIterable<string> {
|
|||
export function createDialecticStream(
|
||||
response: Response
|
||||
): DialecticStreamResponse {
|
||||
// Captured from the terminal chunk, which is otherwise discarded.
|
||||
let evidence: Evidence | null = null
|
||||
|
||||
async function* streamContent(): AsyncGenerator<string, void, undefined> {
|
||||
for await (const chunk of parseSSE<DialecticStreamChunk>(response)) {
|
||||
if (chunk.done) {
|
||||
evidence = chunk.evidence ?? null
|
||||
return
|
||||
}
|
||||
const content = chunk.delta?.content
|
||||
|
|
@ -143,5 +167,5 @@ export function createDialecticStream(
|
|||
}
|
||||
}
|
||||
|
||||
return new DialecticStreamResponse(streamContent())
|
||||
return new DialecticStreamResponse(streamContent(), () => evidence)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,9 +51,14 @@ export {
|
|||
|
||||
// API types (snake_case, for advanced usage)
|
||||
export type {
|
||||
ChatResponse,
|
||||
ConclusionLevel,
|
||||
ConclusionQueryParams,
|
||||
ConclusionResponse,
|
||||
Evidence,
|
||||
EvidenceMessageRef,
|
||||
EvidenceObservation,
|
||||
EvidenceToolCall,
|
||||
MessageResponse,
|
||||
PageResponse,
|
||||
PeerContextResponse,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { Page } from './pagination'
|
|||
import type { Scope } from './scope'
|
||||
import { Session } from './session'
|
||||
import type {
|
||||
ChatResponse,
|
||||
MessageResponse,
|
||||
PageResponse,
|
||||
PeerCardResponse,
|
||||
|
|
@ -259,6 +260,7 @@ export class Peer {
|
|||
filters?: Record<string, unknown>
|
||||
reasoning_level?: string
|
||||
response_format?: Record<string, unknown>
|
||||
include_evidence?: boolean
|
||||
}): Promise<PeerChatResponse> {
|
||||
await this._ensureWorkspace()
|
||||
return this._http.post<PeerChatResponse>(
|
||||
|
|
@ -275,6 +277,7 @@ export class Peer {
|
|||
filters?: Record<string, unknown>
|
||||
reasoning_level?: string
|
||||
response_format?: Record<string, unknown>
|
||||
include_evidence?: boolean
|
||||
}): Promise<Response> {
|
||||
await this._ensureWorkspace()
|
||||
return this._http.stream(
|
||||
|
|
@ -422,8 +425,33 @@ export class Peer {
|
|||
sessions?: (string | Session)[]
|
||||
reasoningLevel?: string
|
||||
responseFormat: ZodType<T>
|
||||
includeEvidence: true
|
||||
}
|
||||
): Promise<ChatResponse<T>>
|
||||
async chat<T>(
|
||||
query: string,
|
||||
options: {
|
||||
target?: string | Peer
|
||||
session?: string | Session
|
||||
scope?: string | Scope | (string | Scope)[]
|
||||
sessions?: (string | Session)[]
|
||||
reasoningLevel?: string
|
||||
responseFormat: ZodType<T>
|
||||
includeEvidence?: false
|
||||
}
|
||||
): Promise<T | null>
|
||||
async chat(
|
||||
query: string,
|
||||
options: {
|
||||
target?: string | Peer
|
||||
session?: string | Session
|
||||
scope?: string | Scope | (string | Scope)[]
|
||||
sessions?: (string | Session)[]
|
||||
reasoningLevel?: string
|
||||
responseFormat?: Record<string, unknown>
|
||||
includeEvidence: true
|
||||
}
|
||||
): Promise<ChatResponse<string>>
|
||||
async chat(
|
||||
query: string,
|
||||
options?: {
|
||||
|
|
@ -433,6 +461,7 @@ export class Peer {
|
|||
sessions?: (string | Session)[]
|
||||
reasoningLevel?: string
|
||||
responseFormat?: Record<string, unknown>
|
||||
includeEvidence?: false
|
||||
}
|
||||
): Promise<string | null>
|
||||
async chat<T>(
|
||||
|
|
@ -444,8 +473,9 @@ export class Peer {
|
|||
sessions?: (string | Session)[]
|
||||
reasoningLevel?: string
|
||||
responseFormat?: ZodType<T> | Record<string, unknown>
|
||||
includeEvidence?: boolean
|
||||
}
|
||||
): Promise<T | string | null> {
|
||||
): Promise<ChatResponse<T | string> | T | string | null> {
|
||||
const targetId = options?.target
|
||||
? typeof options.target === 'string'
|
||||
? options.target
|
||||
|
|
@ -465,6 +495,7 @@ export class Peer {
|
|||
sessions: options?.sessions,
|
||||
reasoningLevel: options?.reasoningLevel,
|
||||
responseFormat: options?.responseFormat,
|
||||
includeEvidence: options?.includeEvidence,
|
||||
})
|
||||
|
||||
const zodSchema =
|
||||
|
|
@ -480,14 +511,20 @@ export class Peer {
|
|||
...scopeRecallFields(chatParams),
|
||||
reasoning_level: chatParams.reasoningLevel,
|
||||
response_format: Peer.toResponseFormatSchema(options?.responseFormat),
|
||||
include_evidence: chatParams.includeEvidence ? true : undefined,
|
||||
})
|
||||
if (!response.content) {
|
||||
return null
|
||||
|
||||
// An empty answer stays null either way, so evidence is still available
|
||||
// for a run that found nothing to say.
|
||||
const content: T | string | null = response.content
|
||||
? zodSchema
|
||||
? (zodSchema.parse(JSON.parse(response.content)) as T)
|
||||
: response.content
|
||||
: null
|
||||
if (!chatParams.includeEvidence) {
|
||||
return content
|
||||
}
|
||||
if (zodSchema) {
|
||||
return zodSchema.parse(JSON.parse(response.content))
|
||||
}
|
||||
return response.content
|
||||
return { content, evidence: response.evidence ?? null }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -511,6 +548,10 @@ export class Peer {
|
|||
* See {@link Peer.chat} for the depth caveat.
|
||||
* @param options.reasoningLevel - Optional reasoning level for the query: "minimal", "low", "medium",
|
||||
* "high", or "max". Defaults to "low" if not provided.
|
||||
* @param options.includeEvidence - When true, the returned stream's `evidence` is
|
||||
* populated once it has been fully consumed. Evidence
|
||||
* cannot be known before the answer is complete, so the
|
||||
* server sends it on the stream's terminal chunk.
|
||||
* @returns Promise resolving to a DialecticStreamResponse that can be iterated over
|
||||
*
|
||||
* @example
|
||||
|
|
@ -537,6 +578,7 @@ export class Peer {
|
|||
sessions?: (string | Session)[]
|
||||
reasoningLevel?: string
|
||||
responseFormat?: ZodType | Record<string, unknown>
|
||||
includeEvidence?: boolean
|
||||
}
|
||||
): Promise<DialecticStreamResponse> {
|
||||
const targetId = options?.target
|
||||
|
|
@ -558,6 +600,7 @@ export class Peer {
|
|||
sessions: options?.sessions,
|
||||
reasoningLevel: options?.reasoningLevel,
|
||||
responseFormat: options?.responseFormat,
|
||||
includeEvidence: options?.includeEvidence,
|
||||
})
|
||||
|
||||
const response = await this._chatStream({
|
||||
|
|
@ -567,6 +610,7 @@ export class Peer {
|
|||
...scopeRecallFields(chatParams),
|
||||
reasoning_level: chatParams.reasoningLevel,
|
||||
response_format: Peer.toResponseFormatSchema(options?.responseFormat),
|
||||
include_evidence: chatParams.includeEvidence ? true : undefined,
|
||||
})
|
||||
|
||||
return createDialecticStream(response)
|
||||
|
|
|
|||
|
|
@ -75,10 +75,57 @@ export interface PeerChatParams {
|
|||
target?: string
|
||||
reasoning_level?: 'minimal' | 'low' | 'medium' | 'high' | 'max'
|
||||
response_format?: Record<string, unknown>
|
||||
include_evidence?: boolean
|
||||
}
|
||||
|
||||
/** A conclusion the dialectic read while answering. */
|
||||
export interface EvidenceObservation {
|
||||
id: string
|
||||
level: 'explicit' | 'deductive' | 'inductive' | 'contradiction'
|
||||
content: string
|
||||
created_at: string
|
||||
session_id: string | null
|
||||
/** Conclusions this one was derived from; empty for explicit conclusions. */
|
||||
source_ids: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* A message the dialectic read while answering.
|
||||
*
|
||||
* Identity and provenance only — no content. Fetch the message by `id` when
|
||||
* you need its text; evidence is for auditing what was read, not for reading
|
||||
* messages in bulk.
|
||||
*/
|
||||
export interface EvidenceMessageRef {
|
||||
id: string
|
||||
session_id: string
|
||||
peer_id: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
/** A tool the dialectic invoked while answering. */
|
||||
export interface EvidenceToolCall {
|
||||
tool_name: string
|
||||
tool_input: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* What the dialectic read and did while answering.
|
||||
*
|
||||
* Collated from what the agent accessed rather than reported by the model, so
|
||||
* it over-reports: a listed conclusion was read, which is not proof the answer
|
||||
* leaned on it. `toolCalls` omits results and failed calls.
|
||||
*/
|
||||
export interface Evidence {
|
||||
conclusions: EvidenceObservation[]
|
||||
messages: EvidenceMessageRef[]
|
||||
tool_calls: EvidenceToolCall[]
|
||||
reasoning_trace_id: string | null
|
||||
}
|
||||
|
||||
export interface PeerChatResponse {
|
||||
content: string | null
|
||||
evidence?: Evidence | null
|
||||
}
|
||||
|
||||
export interface WorkspaceChatParams {
|
||||
|
|
@ -88,10 +135,23 @@ export interface WorkspaceChatParams {
|
|||
reasoning_level?: 'minimal' | 'low' | 'medium' | 'high' | 'max'
|
||||
response_format?: Record<string, unknown>
|
||||
scope?: string | string[]
|
||||
include_evidence?: boolean
|
||||
}
|
||||
|
||||
export interface WorkspaceChatResponse {
|
||||
content: string | null
|
||||
evidence?: Evidence | null
|
||||
}
|
||||
|
||||
/**
|
||||
* An answer together with what it was built from.
|
||||
*
|
||||
* Returned by `chat` when `includeEvidence` is set; without it, `chat` returns
|
||||
* the answer on its own.
|
||||
*/
|
||||
export interface ChatResponse<TContent = string> {
|
||||
content: TContent | null
|
||||
evidence: Evidence | null
|
||||
}
|
||||
|
||||
export interface PeerRepresentationParams {
|
||||
|
|
|
|||
|
|
@ -514,6 +514,7 @@ export const ChatQuerySchema = z
|
|||
responseFormat: z
|
||||
.union([z.instanceof(z.ZodType), z.record(z.string(), z.unknown())])
|
||||
.optional(),
|
||||
includeEvidence: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.superRefine(scopeExclusivityIssues)
|
||||
|
|
|
|||
Loading…
Reference in New Issue